How to Build an MVVM Parking Spot Finder with Maps Compose - Android Studio Tutorial

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
hey guys and welcome back to a new video in this video we're going to build a little parking spot finder app because there is a new cool library which is called maps compose with which is basically by google and it's a library that allows us to use google maps directly with jetpack compose so till now or till um like before the library was released the only way to use google maps with compose was using the android view composable so we just use a normal android xml view in combination with compose which is not optimal of course so now we have this library which is really cool and i want to show you that in a little bit more practical project because i think that makes more sense if you directly see how you can build something that's maybe a little bit useful with this so what we will basically do is let's see we have a map here and we can then simply set markers where we parked our car so we don't forget about that for example let's hop into berlin and say okay um this is where i parked my car and then i simply long click on the map and you can see i simply set a marker if i tap on the marker you can see that's my parking spot and you can also see when we long click this info banner here we can simply delete it again and the cool thing now is which is just something to show you because uh i like this is you can see this toggle down here and when we click this we can switch to a fallout map so for the gamers among you let's do this boom that looks cool of course not very very readable for the parking spots but it's just that you get an overall impression of how you can use the new maps compose sdk i wanted to add a little bit more of a cool feature like this one you can also add more styles i will show you a website where you can download these styles i like the style fits my my color my color theme of my channel as well so let's actually start right over and get our api key first here which we need for google maps to work you can see i already have a project here in my google cloud console which i will link down below so you of course need a google account which i guess you have and then you need to create a project here if you don't have that yet on your google maps platform basically so if you don't know to do that you simply click create project in the top right choose a project name choose an organization or you just leave it empty just like i did click create wait a little moment there you go then you simply click on maps sdk for android you want to click enable yes we want to enable that within our account and we wait again then this page will open up you can see you have the option to have additional apis which are related like directions if you want to navigate stuff or if you want to fetch some places which we're not going to need here we're just going to take the normal google maps api key so we can just display a simple map you want to go to credentials to create an api key and you can see um we have no api keys here right now so we can click create credentials api key and then it is creating that which is basically now what you need to copy into your android studio later now let's actually not do this later let's do this right now because you now already have this api key so you simply copy this go to android studio open your manifest because there that's where it belongs and you first of all get the initial product from my github repository down below so you already have the dependencies here um or you just create a new project and copy this over oh it's basically just compose google maps tiger hills room it will be a very simple clean architecture app here nothing special but if you have that you simply go here to your android manifest inside the application tag and you paste this line and here where it says android value is where your api key belongs so that's where you copy your key and if you've done that then you're already ready to use google maps and i would suggest we just start to create our very simple map screen in which we simply show our map in which we have that little toggle button to toggle between our normal and or fallout map so let's actually set up our very basic package structure first in our root package we go to new package and we say okay that is going to be on the one hand the presentation package and inside that we don't actually need another package because it's a very simple app so we don't have real features um yeah let's leave it at presentation so inside that we're going to create a new carton class of file called map screen um yes let me at that to get yep and let's create a composable here for our map screen there we go and to build this map screen i think it's more helpful if we create our view model and state first so that we can just refer to that state i will have one state for that just one state class i mean which is called map state that just combined combines all of our screens state make that a data class and in here we're going to have three variables yeah so on the one hand we have a variable for properties which is you can see it already suggests that map properties which can be used to just change some map specific configuration so we just set that to an empty map properties field here but you can see if we click ctrl p here then we get a bunch of options so that's what you can use if you want to toggle specific features of google maps like do you want to have a my location button which is fault by default um that's very easy you can just set that to true and you can press a button and it gets to your location i won't do that here because that requires location permission and all that permission handling which would be too much for this video but if you have that then you can simply toggle these features here to true like if you want to have traffic if you want to have um a specific map type like normal or satellite or so then you can simply change that here and if you make that a state then it simply also reflects these changes immediately once you update them and then we of course have a list of parking spots here that we will display so for each single marker which we can't do yet because we don't have the entity class for that but we can have another boolean whether we're currently showing the fallout map or not and that's false by default and with that we can already do quite some stuff on our map screen so let's switch back to that or actually first have that state in our review model that we create and then we can get to our map screen so in presentation new calling classifica class of file and call it maps view model select class here make that inherit from viewmodel and all i will do here for now is i will create an instance of that state mutable state off and here we're going to initialize our map state and we import this and then we can have such a view model here in our map screen view model maps view model is equal to hill view model or actually let's not use hill view model let's just use the normal view model here which for some reason does not work but it should so you go to a view model i want this composable okay for some reason it doesn't work like that not sure why maybe you need to redo this your model type mapsviewmodel is equal to viewmodel okay for some reason it doesn't like that not just writing view model i don't know why but let's take this and then the contents of that screen are very simple we simply have a scaffold because we want to show floating action button and for that we're going to use a state scaffold state is equal to remember scaffold state which we can then pass or scaffold so scaffold state oops is scaffold state and our floating action button is something we can create here so floating action button on click is something we don't want to deal with right now because we can't title this yet we don't have the functionality for that in our v model we'll do that later we don't need that for now but we can already set an icon um with a bit with an image vector actually and the image vector will be depending on whether our fallout map is toggled or not so if it's enabled or not so if view model that state is fallout map then this should be i can't default toggle off because if if the map is shown if we are in our followed map we want to toggle it off and else icons defaults toggle on and the content description is toggle fallout map okay so far for the less interesting stuff now it comes to the cool stuff and that is our google map inside the scaffold we want to have this google map fill our whole screen and the way we do this now with maps compose is we just say we have a google map so you can see that it's not composable it's not an android view anymore and that itself is already enough to kind of display that map but we have tons of ways to configure that so on the one hand we want it to fill our whole screen size so we say modifier is modifier fill mag size and after that we want to configure a little bit more because we want to set the properties of that you can see that it needs to be a map properties object which we have in our state so we set that to the model state properties and whenever that properties is updated in our remodel then our google map will also recompose and reflect that new state then what we want to do is we want to set the ui settings which is also kind of a yeah similar to this properties just more related to ui settings as the name says uh we also want to do this i'll use this that's equal to ui settings and here we um it's actually map ui settings here we want to map ui settings here we want to simply set the zoom controls enabled to false so that the the zoom controls would actually be in the bottom right corner and we don't want the floating action button here to overlap the zoom control so that's why i simply disable these but also to just show you how you can disable things in your ui layer we should actually take this you could make this state of course as well if you want to reflect changes but we don't change the way we display the ui so we can actually just simply make this a remember variable here so ui settings is equal to remember and put this in here so by by using remember we just make sure that we don't recreate this object on every recomposition of our google map which it would have done otherwise so we just set this to ui settings now and when we along click on the maps you can see we have on map long click then we of course want to set a marker later for our parking spot it gives us a latitude longitude object here which simply reflects the coordinate where we clicked on and that's the function we will then simply use to send that coordinate to our viewmodel save that in our room database which we will use here to simply save the marker the markers are something we can't set up yet because we don't have a list of parking spots which will of course need for the positions where we want to show the marker but yeah that's already the very simple setup for google map and let's actually quickly implement this toggle fallout map feature because that doesn't require us to implement a database or so and for that i will go to my presentation package create a new kotlin class of file and i will call this map event those who actually watch most of my project-based playlists or videos will know what this class is for it's actually not a data class it's a c class and here we just specify some kind of you events that all reflect ui events things that the user can do on our screen that we sent to our view model so the view model can then process that in a single function which is in my opinion just a lot cleaner than having one function per single thing the user can do so what can a user do on our map screen well on the one hand it uh the the user can of course set a new parking spot by long clicking um they can delete by long clicking on the info window those are things we can't do yet because we don't have that parking spot entity as i said but we can have an object here toggle fallout map and that's of type map event in our view model we can then have this on event function i mentioned that takes these events and now we can check what is that event and if that is our toggle fallout map event we can now respond to that in that case all we really want to do is we want to map our state to something new well how do we actually get this fallout map into this project here because of course the normal google maps library doesn't know how we would like our map to look like to find this map you simply go to this website that i'll link down below which is called snazzymaps.com and you can see here is this awesome map from mario and you can simply find a javascript style array here and that basically a json file adjacent text that google maps needs that we provide and then it knows okay which elements actually have which kind of color which saturation and all that stuff you find tons of more maps here on this website that you can simply copy and paste and you can even create your own map so your own map style just something that's very cool and a very um little feature that i wanted to implement because yeah i liked that you can actually easily customize your map which is something maybe not that many people know so what you simply do is you go to expand code you copy all that json code uh let's go that's very very slow i'll just copy this over from my other project but you'd simply copy this json code or you copy this from my github repository and then let me quickly do this i have a map style object here in kotlin and i'll copy this into presentation click ok and here you can see just an object singleton that has a single variable for the json and here it's all that stuff that i copied over um in a real project i would probably put that in a resource file like in a raw resource and then parse that but that would require the context and stuff like that which is something i don't want to deal with here since we're in a review model and we don't have the context so you would need a wrapper and stuff like that we're totally fine just having this as a normal kotlin string now we simply go to our map state because we specified this style in our map properties which is called map style options and we can set this to new map style options so if you want this to load from raw resource you use this function if not you can also use the normal constructor and provide the json string which is map style.json so by doing this we would always force this style to be there because we provided in the initial map properties we only wanted to show though if this is true so we don't want to specify this here oops instead we're going to go to our view model whoops um here we say state is now state dot copy and we say okay our properties are now our state properties that copy and we set the map style options to our json only if the state is fallout map is actually true so if it is true then that means after clicking on toggle we set it to false so we want to set the map style options to null if it's bold and we want to toggle it to true we want to set it to our json maps i hope that makes sense we also want to make sure that we toggle this um it's fallout map enabled to state as followed map and negate it and that's pretty much how we do this so if if it is currently active we now deactivate it and else we activate it and that's something we can already try out after we call this on event function in map screen when we click our floating action button oops here we say view model dot on event and we simply send this toggle event to our view model and we of course need to call our map screen in our main activity let's remove these composables here and instead of this greeting we can simply have our map screen and then let's actually launch this and hopefully we are already able to juggle this there we go my map is launching you can see we have our button we see our map that's working that's good and if we click this toggle button boom there is our fallout map so that is already working of course we can't set parking spots yet if we long click here that is what we're going to work on next so what is the next step in our app as i said we will have a very simple room database in which we will store our simple parking spot so where the user actually left their car so i will put that in a data package in our maps compose guides a root package new package called data inside of that um i'm not going to create any more packages if we would have two data sources here like local and remote i would have two more packages here but we really only have our database and it's all in our data package no reason to over complicate things here let's create that and in that new cotton class of file called parking spot entity and that will be a data class for room we need to annotate this with entity and in here we want to on the one hand have a val for the latitude value which is a double and we want to have a long or the longitude value in here take care don't call this long that was an issue that i took quite a while to find out that works here like it doesn't give us any errors however the room the room yeah the room code compiles to java code underneath and in java long is a keyword so this kind of compiled to this keyword and this java code didn't work because you had a variable name long and yeah it was very hard to find this out because there was no clear error so let's just call this lng and we're good so let's also have a primary key for our id which is now by default because room will assign that for us actually a val id like this so then the next step is to create our parking spot dao object our data access object in which we define which functions we have to basically insert new parking spots delete some queries some basically the interface between our app and our room database so new kotlin class of file called parking spot dao and we also annotate this with dao so room news that's our dial first of all we're going to use a suspend function here to insert a new parking spot insert parking spot and that takes a spot which is our parking spot entity then we're going to use insert here on conflict on conflict strategy replace so that means if we try to insert a new part a new parking spot here with an id that already exists it will simply replace the old one and then we need a delete function so we annotate it with delete suspend function delete parking spot also pass the parking spot entity here and one final function and that is simply one to create all of our parking spots so we get them to show them in our map as markers we can do this with the query annotation to provide an sql query here where we simply want to select everything from our parking spot entity table that will be a normal function not a suspend function because it will return the flow so we can automatically observe changes in our database that's a very common question i get why are these two suspend functions and the query one isn't well because the query one will return a flow and the flow is already asynchronous by default so we don't need to make it suspending to get that flow let's call this get parking spots it doesn't take any parameters and will return a flow of lists of parking spot entity so whenever something in our database changes like when we insert in parking spot or delete one this flow will emit the new list of parking spots which we can then show in our map cool next up we have our database class in our data package new carton class called parking spot database make that a normal class abstract class annotate it with database oops i don't want to open that and this will take two parameters this annotation on the one hand we need to define which entities we have that's just one here which is our parking spot entity double calling class and we need one more and that is the version which we set to one so whenever you migrate your database whenever you change something you need to increase this version so room knows that something changed we want to make sure this is this inherits from room database and this also implements an abstract val dao which is the parking spot dao so room will basically just assign the value for that behind the scenes for us and then we have access to the dao in this database class to simply access our database really easy stuff with room here and then um something i want to do is because we're dealing here with clean architecture and we have a data and presentation and domain layer we don't have the domain layer yet but the domain layer in clean architecture is the innermost layer so all layers are allowed to act as the domain layer but for example the presentation layer wouldn't be allowed to access something from the data layer so any class from the presentation package shouldn't actually have access to either the dao the database or an entity and since we of course want to show our parking spots in our ui layer but our ui layer isn't allowed to access the entity here we need some way to actually put this in our domain layer so you will see what i mean in our root package we create a new package called domain and inside that we're going to have a model package then here we create a parking spot class so without entity and that will just combine the dto and entity classes from our data package so maybe we would also get some parking spots from an api that we synchronized with a remote server or so so in the domain layer we really have a unified class that combines these two types of data layer classes and since it's in the domain layer our presentation layer will be able to access it so very simple in our case it's just the same class it's it's really that easy might not make a lot of sense to you if you're not familiar with clean architecture but that's how we do it so we really want to decouple our layers which gets more important if you're actually dealing with a multi-module project because then you can actually force your presentation layer to not be able to access anything from your data layer right now theoretically it would be possible but that would be too much i have a whole course about multi-module architecture which you'll also find down below um which goes into this in into that in much more detail however let's have our id nullable here as well it's basically the same class and to convert our parking spot entity to our parking spot class we typically use something called a mapper in clean architecture so that belongs to our data layer new kotlin class of file called parking spot and that's a simple file will contain a function that takes our entity and converts that to our parking spot so our domain layer model and since these basically have the same fields we can just return that here just return a parking spot with the latitude and longitude values from our entity and the id is equal to id um so yeah if you're if you're new to clean architecture then this will look completely weird to you why do we create the same class here but i would recommend you to watch one of my clean architecture videos like uh for example the dictionary out for example my um node app or so um i have a bunch of videos where i explain why we do it like this and then it might make more sense we'll just simply copy this i have the same way to convert a parking spot to a parking spot entity which just makes it very easy to convert between these two types of classes here we return that here as well and that's already it the next step is now to define a repository in which we call the the dao so the repository will be able to access all of our data sources which is only our local database here and then it will simply provide the results of that to our review model that will then provide the results of that to our ui i'm not going to use any use cases here because i think that's really really overkill for such a simple app and these use cases would just call a repository function it's just boilerplate code i don't like it in that case so yeah please have some mercy with that i'll just let the view model directly access our repository layer so let's create that repository first of all in our domain layer actually i will create a new package called repository so the way this will work is in the main layer we have an interface that describes the functions our repository needs to implement so our presentation layer can also access that interface since it's in the domain layer let's create that and then we will have an implementation of that interface in our data layer so how do we call this parking spot repository on the one hand we will have a suspend function called insert parking spot that takes the parking spot here make sure to use the domain layer model class here not the entity one you'll have a function to delete a parking spot and we'll finally have a function to get all parking spots as a flow flow of list of parking spots cool let's now write an implementation of this interface in our data layer so here new class called how do we call this parking spot repository implementation and make sure that implements our parking spot repository this one it will need one constructor parameter and that is of course our dao because that's needed to access our database so we have a private valve dao parking spot dao and yeah in here we can simply press ctrl i ctrl a and enter to implement all these functions to enter the parking spot we simply say dao insert parking spot passing our spot and you can see we get an error now because we of course need to insert our database entity but it found a parking spot from our domain layer that's why we have these mappers so we say to parking spot entity and that's now a very convenient way for us to convert between these two types of classes here we say dow delete parking spot say spot to parking spot entity and to get all parking spots we simply return that from our db as well and you can see we also get an error here and since we have a flow we can directly say to parking spots something no we need to call that map which gives us this list of parking spot entities so we could give this a name of spots and we can still not say spots.2 parking spot entities because it's a list and we can only convert single items so we need to map this list again so we say map it to parking spot and then it works so now that we have this repository we are ready to actually set up dagger hilt for dependency injection so we can inject these repositories or the single repository actually in our viewmodel to access it there so in our root package new carton class of file which is called parking spot application we always need an application class with dagger hilt because it needs to be annotated with hilt android app to work make that inherit from application and make sure that we add this to our manifest right here um actually not right here right here the name is parking spot application that just needs to be done for dagger hill to work we can close these two and the next step would be to [Music] create an app module so yeah an object in which we define which dependencies we have and we want to be able to inject we use our root package new package called di for dependency injection and in that di package we create a new class called app module and make that an object annotate it with a module so that your head knows that's a module and we say install in singleton component since we want all these dependencies we declare in here to be singletons then on the one hand we have a dependency for the repository and on the other hand the repository of course depends on our database so let's start to provide this database make that a singleton and say we want to provide that that's why we annotate this with provides and say provide working spot database that will need our application instance since that serves as a context and returns a parking spot spot database we can create that using return room database builder passing our contacts our application the class so what type of database we have here which is a parking spot database and the name of that database is something we can choose here like parking spots db or so and then simply call create oops just create or build build it is and then only our repository is missing singleton provides function provide parking spot repository that returns our interface parking spot repository and here we return our parking spot repository implementation passing our db.dao which we of course need to provide here so we have a database instance which is of type parking spot database yeah that's how we actually create such a repository with our database instance and now that aggregate knows how to create our parking spot repository we can inject that in our view model so the view model can access our data layer in the end so maps view model let's open that and annotate it with hildviewmodel because when we want to inject interview models with hilt we need this annotation combined with an inject constructor um again i go much more into detail and all that stuff in other videos this video should really focus on maps but here you will simply have a private valve repository have our parking spot repository and let's now simply yeah extend our map event class we have here because as i said we will have two more events on the one hand to set a parking spot when we long click on the map and on the other hand to delete a parking spot when we long click on the info window and since we now have our parking spot entity and model classes we can also create these events those will be data classes since we need to provide the coordinates where we clicked on data class on map long click let's call it val lat long that will simply provide this object to the v model and will be a map event and we will have a data class on infowindow long click um does that need something that will actually give us the parking spot we clicked on parking spot and that will be a map event and actually when i not think about it we don't need this on map long click as a map event because we can directly oh actually not so forget what i just said um we need these three map events here i was just thinking about actually just clicking on a marker to show the info window that's something we can directly do on our screen level but these two events need the view model since they both interact with our database when they happen so in view model let's say we need to extend our one block here when that event is on map long click that means we want to insert a new marker we do that in via modelscope.launch so we launch a new caroutine and we simply say repository insert parking spot and the parking spot here is a new parking spot with latitude and longitude which we can get from the event so event led long latitude and let's put that on separate lines here oops this one no ah come on let's do it like this and for the longitude event led long longitude like this that's already it for this block next one would be is on info window long click which is the last one here to delete a marker so we say um a repository actually first to be model scope launch a new curtain and we say repository delete parking spot and here we can directly just we delete the spot we got from the event so we long clicked on now we deal with all these events one thing that's missing is that we of course listen to changes from our database which we can do in init so we just start listening automatically we model scope that launch and then here we say okay we have our repository get parking spots which now returns a flow and we want to convert that to compose state kind of and we simply collect latest so when average something changes this collect latest block will fire off that will give us the new spots so in that case we can simply map our state to state copy and oh we need actually the list of parking spots here in the state as well let's open this map state class and have a list of parking spots a list of parking spot empty list by default there we go back to view model and we say parking spots is our new spots and now the last thing we need to do is to go to our ui layer and create these markers as composables based on these parking spots we get from our database here and yeah don't worry i will make a quick recap at the end of this video so if something was unclear then i will just go through everything quickly again let's first of all go to our map screen and in here what we want to do is we're going to go to our google map in on map long click we can already send the event rv model on event and that is on map long click passing over that long coordinates here where we clicked and well how do we now get the markers on our map how can we actually then you show this info window and all that stuff well we can do this by opening a block of code after the google map composable so in here we can put a bunch of composables that modify our map that draw on our map such as markers for example so in the end we take our view model and we say state parking spots which is our list of parking spots and we loop through that and for each single spot we now want to show a marker on our map and there is simply a marker composable it's really that easy we need a bunch of parameters for this one first of all the position of course google maps needs to know where it should put that marker and that just needs to be in form of a lat long object and we can say okay spot.latitude spot.longitude then what else do we need we want a title so that will be the title of the info window when we click on that marker and well what is that just a string i want to simply name this parking spot and that in parentheses simply the coordinate of that so spot.lat oops spot.latitude comma spot.longitude and then we need a snippet which is the simple text that shows below this just a little description i'll just give the user the information here to long click to delete that's of course not the optimal way to delete the marker but it's the quickest way to do it here to just show you how you can also delete markers without any extra ui layer stuff you would need to implement here otherwise then what else do we have we of course have on info window long click then we want to delete it so in that case we save your model and send our event remodel on event um the event will be on info window long click which takes our parking spot that we actually clicked on that's simply our spot here then when we normally click on a marker then we want to show this info window so by default this behavior is not there so we need to implement this and we can simply use this on click function which will give us access to this marker and we can then use this marker to simply say okay show infowindow so that's a little bit more the xml way of doing it or the the view way um so in compose i would actually expect something like a a state that says okay should show marker and when we set it to true it should show info window and we said it to true it's visible and it's not i think that's something that will come in future but right now it seems like that's not the case so we get a marker reference and then we can call a function on that to show it to show the info window and we can return true to just say hey we handled the click event and if you want to also see how you can actually change the color of that marker or change the marker at all so to some other kind of icon you can use this icon here which is a bitmap descriptor and to change the color here we can use the bitmap descriptor factory dot default marker and you can see we can pass a color value here or you can simply create it from a bitmap so if you want your own marker i will use default marker and pass bitmap descriptor factory and you can see we have all these color values here i will choose green of course fitting to my channel and that is almost it we now need to go to [Music] main activity and make this android entry point annotated which just allows us to inject stuff in our view model so if we know which allows us to inject our view model in this activity or here in our map screen rather which is just needed if you need to inject something into android components such as activities but i would say that's it let's launch this on my device and if everything works i will go through everything again quickly as i said oop there we go is that already launched yep looked like that so let's see if we can set a marker scrolling to berlin again and somewhere here let's say we parked there boom there's our marker that looks pretty good if we tap on it then we see okay that's our parking spot with our coordinate we can long click to delete hopefully yes it's gone along click again i'll relaunch the app um let's go to android studio again relaunch and there we go and you can see our marker is still there and reminds us where we actually left our car so we will never forget about that again and of course our fallout map is still working with our markers as well which looks even cooler when these are also green um yeah feel free to play around with these styles there are so many styles users created or you can create your own one let's just go through everything again so nothing gets left out here first of all in our map screen that's the main content here of this video actually because i wanted to show you google maps we have a scaffold which we just used to show our floating action button in the bottom right corner and when we click this we toggle our fallout map which just switches a boolean in our view model and changes these map properties that we attach here to this google map you can of course have a lot more uh configuration changes here for this google map composable by simply attaching more parameters feel free to play around with that when we click on a long click on our map we set a marker so we send this event to our view model which will then insert that in our data in our database and inside of this google map composable we just created a list of we just created a bunch of markers using our list of parking spots for each single spot we create a marker at that single position of that spot giving this title the snippet when we long click on the info window we send the event to the view model that we long clicked and the viewmodel will then delete it from our database when we click on it we simply show our info window and here we simply change the color a little bit let's take a look at the view model here we just have our on event function which receives these ui events the user actually performed and sent from the ui layer and then just maps the state to simply reflect these new changes in our ui as well access is the repository that will then access the dao so our data access object which is the yeah which directly puts these uh entities here in our database or deletes these or simply retrieves these and yeah that's pretty much it so that's the room stuff here which is nothing special like probably the most simple example you can have with the room database but i think that is pretty much it i hope you enjoyed this video and if you actually want to dive deeper into this clean architecture stuff as i said and understand that more in detail then i really recommend to watch my dictionary app tutorial which you can find here because that yeah just dive deep into that and then you better understand how that works because it also includes a caching approach you
Info
Channel: Philipp Lackner
Views: 30,180
Rating: undefined out of 5
Keywords: android, tutorial, philip, philipp, filipp, filip, fillip, fillipp, phillipp, phillip, lackener, leckener, leckner, lackner, kotlin, mobile, maps, compose, google maps, sdk, composable, marker
Id: 0rc75uR0CNs
Channel Id: undefined
Length: 49min 34sec (2974 seconds)
Published: Sun Feb 27 2022
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.