How to Make a CRUD Todo List App - Full MVVM Beginner Guide - Android Studio Tutorial

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
hey guys welcome back to the video in this video i will give you a full crash course on mvvm because i realized i don't really have that on my channel like yes i do have a playlist about mvvm which was my very first youtube playlist and i think till then like two years ago android changed a little bit and my explanation skills improved a little bit so i think i will just make an up-to-date crash course just a single video which will teach you mvvm from the ground up so what will actually be the prerequisites here if you want to learn mvvm that is on the one hand of course just android basics in this in this case here jetpack compose so you should know jetpack compose i do have a playlist about that you should of course know kotlin i also do have a playlist about that and you should watch my mvvm 100 seconds theory video before so that's where i basically tell you why we use mvvm what that is and how it just works together you can click up here as i said in only 100 seconds you will see how that works it's a nicely animated video and after that you will be ready to jump into practice here in this video and what is helpful not necessary to know that in detail before is kotlin curoutines which i also have a playlist about so if you already know that it will be helpful but it's not like a really not like a real requirement before you jump into this video here so if you at least know jetpack compose in kotlin you are ready to start and let's actually next go through what you will learn in this course that will of course on the one hand be mvvm we will make a very simple to-do list app so you can actually learn how all the different parts of mvvm interact with each other that will be what we will build here we have a simple floating action button with which we can add a new to-do you can give it a title like hello youtube an optional description hello and then if we click check and it will be saved in our local database it is a very ugly app i know that the ui is really not the focus here but you will basically learn how we make a simple crud app with mvvm because that's really i think the most basic example that teaches you how mvvm works you can see we can delete this if we click here we get a snag bar to do delete it we can undo this so it's inserted back and of course we can check and uncheck it like we know it from a normal to do app and to show you that it's really a database let's remove this app relaunch it and there you go you can see it's it's still checked it's still contained here and yeah that's what we're going to build on the other hand you will actually also learn dagger hilt so that will be a library for dependency injection you will learn what that is i will also explain that from the ground up and just how we can use it with an mvvm project and of course as you've seen we use a local database here that will be a room database which you will also learn so you also don't need to know anything about room to jump into this project so assuming you have actually watched the theory video the 100 seconds or you already know why we use nvm let's jump into android studio and what you need to make sure is to get the initial project from my github repository down below that means the dependencies here i don't need to write this here in this video so we on the one hand we have composed dependencies for navigation and viewmodel daggerhill for dependency injection room for our local database and curtin's extension or rather kotlin extensions for room so we can use that together with cool routines in a very clean way and that's pretty much it like in our project file we include this class path for dagger hilt and like here in the build file we also use these two plugins that we actually apply but as i said you can just get this from my github repository and then you're settled let's clear this here and jump right into it so just as a very quick recap we use mvvm to give our project a very clear structure this structure will help us to improve testability to improve extendability so how easy is it to to add new features to our app and just how easy it is to understand what we already built just for us but also especially for other people who might work with us together on a project if your project has a clear structure and everybody in the team knows that structure it's just very clear where things are when you want to change something so the end goal should always be to divide our app into three layers which is ui business logic and data business logic would mean just the pure application logic like if you just want to search an element in a list for example you loop through it and you just check okay is that current element actually does it contain the search query that would be an example of business logic which would be put in the view model in the data layer we just access our data sources which in this case would just be our local database or room one but it could also be an api that we access and the data layer would then provide the data for our view model and the view model would then put it into state i will get to all these um terms what that means and would then provide that state for our ui so our ui can then in the end display what the view model provides sounds super complex if you hear this the first time i know it actually isn't that complex but let's jump into it and i want to start implementing our data layer so we will actually start creating our room database here at the very beginning in our root package we will create a new package called data and in that we will create a new carton class of file so when it comes to databases especially sql databases which room actually implements it's an sqlite database then we first need an entity an entity is basically the class or the table that contains the data you want to save so in our case since we want to save to do items this entity which will be called to do will contain all the fields that we want to save with a single to-do item the first step when we have such an entity with room is we want to annotate this with entity that just tells room hey this is an entity we we want to have that as a table in our database and in here we now need to think about what do we want to actually save with the to-do item on the one hand that's of course a title which is a string then we have a description but as you've seen in the app the description is optional so we actually need to make this nullable and if we don't pass a description then we just pass null here for this to do and on the other hand each shadow item actually needs to save if it is already done or not so val is done which will be a boolean here now for sql databases it's the case that we also need an additional property which will be the so-called primary key that will be usually an ide that we save with each item and that needs to be unique it will be used to just uniquely identify an item in our database and we can just call this id here will be integer and we make this nullable because in the end it won't be now but the way room handles this if we just don't pass a valley here when creating this entity room will automatically generate this id for us so we don't need to make sure that we pass a unique one here we can just annotate this with primary key to tell room hey this should be our primary key and then we are settled for this entity and ready to jump into the next file and that will be our so-called dao object so in our data package new card like class of file and that will be an interface which will be called to do dao so what the heck is a dao object dao stands for data access object and it is exactly that it is used to access our data so it defines the different ways we want to access our data so now we just defined what kind of data we have which are to do items now we want to define how we would like to retrieve and change that data in our database and that's what we do here so we basically define the functions we want to we need to interact with our database so we want to have a function to insert new to use we want to have a function to retrieve all to do's for our list we want to have a function to delete it to do and we want to have a function to get a signal to do by its id when we load it in the detail view so um let's actually start with a suspend function insert to do so as i said it's helpful if you know cool routines this is related to curtin's this keyword if you don't know that it basically means we suspend so we stop the current function where this is executed in until we actually got the result so until we actually know that the execution or that the insertion here finished and that's just very very cool way to a very cool way of asynchronous programming because we can do this in a sequential way so it basically gets rid of callbacks so instead of uh having a callback that gets triggered when we insert it or to do we can make this suspend and we just wait in the current function kind of to get the result you will see how this works we don't need to understand it at this point we will also have another suspend function delete to do where we pass the to-do we want to delete we want to have a suspend function get to do by id which will do exactly that we pass the id which is an integer here and that will now return a nullable to do because if the id doesn't exist we want to make sure the app doesn't crash and we want to have a function this is now not a suspend function get to juice so that will just retrieve all to do's and that will return a so-called flow of kotlin x curtains here the flow of a list of to do so that's a cool thing about room that we can return on flows which means that we basically get real-time updates as soon as something in our database changes so as soon as we insert it to do deleted to do or whatever just change something that would affect this query that we define here then this flow will trigger again and give us the new and updated list of to do's that's pretty cool and we're actually not done yet because how should actually room know what we want to do we just have our very own names here that's cool but it doesn't know how we still want to access the db so on the one hand we first of all need to tell it that this is actually our dao object so we annotate it with that then we also need to annotate this insert function simply with insert here and i actually want to pass a so-called on conflict strategy which is yeah it's a strategy what should happen if we want to insert it to do with an id that already exists because in that case we just want to say onconflictstrategy.replace so if we want to insert it to do and new to do here and the idea of that new to-do already exists in our database we simply replace the old one so then that way we have an insert and update function just in one function which is pretty cool for the deleted one here we just annotated with delete for get to do by id we need to um define a custom sql query to just define how it should get that to do so we just want to select all the properties from our to-do table where the id is actually equal to the id we passed so we do this with this little colon here to refer to the parameter and here we also have a query and we just want to select everything from our to dos table so it's really simple in the end that's just as i said the the interface that defines how we access our database and room will then worry about all the implementations and generate that code for us so far so good what is the next step the next step is to define one final class in regards to room and that is our actual database instance which then has an instance of this dao object to actually access the actual db so also in our data package new carton class and we call this to do database that will be a class and it will inherit from room database it will actually be an abstract class and we annotate this with at database here we need to define two properties on the one hand the entities so just all the tables we have in rdb which in this case is just the the to-do table so to do double colon class and we also need to define a version which is just one here the initial version every time you change your database in future if you have a running app in production you need to increase this version so room can actually recognize if it should migrate your data or not and one more thing in here is that we need our dao reference which is also an abstract valve which room will then automatically kind of insert for us or it will create that instance for us here so we can say dao is of tab to do dao and then we're settled so that's really everything for this very simple to do database instance which we will then later provide with daggerfield and what that means you will understand that when we get to that the next step is to define our repository if you watch the mvvm in 100 seconds video then i've also mentioned that we usually for the data layer and mvm use a so-called repository the job of the repository is just to access all of our data sources so in this case we only have a database but it could also be a database and an api it could be a database an api and some preferences or so like all of our data sources and then the repository's job is to decide which data it should actually forward to the view model so you can imagine that you have a local cache in your app let's say we have like an an online api of this to do list here we would like to save our to-do's remotely as well so what we could do is or that we could have a repository that fetches the data the new to-do's from the api and then saves it in the local database as a cache so we can also use the app offline and then forward that local data to the viewmodel sounds quite complex but in the end it decides which data should be forwarded to the view model like if we have a cache will we show the remote data should we show the local data should we show some preference data that is what the repository is for so in our data package new cart and class file first of all interface again which will be called to do repository i like to define an interface for that because it just serves as an abstraction that's usually a good idea to depend on abstractions not on concretions so an interface would be an abstraction because that way you're just flexible with your architecture this on the one hand makes testing easier because you can create your own fake versions of this to do repository for your tests because usually you don't want to test with your actual database or actual api and on the other hand you don't need to change all your code in case your repository actually changes because then you only need to change the implementation of that repository and this will do repository we will now have a function for each function we have in our dao this will look like some dumb boilerplate code we have here especially in the small app but for larger project it really pays off to have that so we can actually just let's just copy this from our to do dial because it will contain the same functions just without annotations so we can delete these make sure to remove the imports and then we can now actually write the implementations for these functions so re create a new class in our data package to do repository repository implementation i call it and this will now implement the studio repository interface you can see now we have to implement these functions pressing ctrl i control a and enter and here we now define how we actually get the to do elements now we of course now need access to our actual database instance how do we get that well we just pass it to the constructor of this to do repository so we actually need the dao to access our database so we just pass it there and here for these single functions we just say dao insert to do and pass or to do here that we got from that we got as a parameter dow delete to do dow actually return dao you get to do by id pass the id and we can say dao get all to do is here we return that so alice had that looks really not necessary if we just call these functions from our dao object but it really gives it a good structure you need to think about what happens actually if you extend this with an api then these functions here suddenly get more complex and then a repository really makes sense in this case it really doesn't make sense i know but since i want to teach you the real way how or give you some kind of template you can apply for any project i want to still show it now that is everything for our data layer so we now define how we access our database we have a repository that our future view models can access what is the next step the next step is to set up daggerfield dagger hill is used for so-called dependency injection what the heck is dependency injection well it's quite complex why this is helpful if you've never heard it but if it's just about what that is it's really simple in the end dependency injection is all about giving an object its instance variables so that means for example what we do right here with this to do dao that is dependency injection because we we pass a dependency so in this case this would be the dependency don't confuse this with gradle dependencies a dependency is just an object a specific class depends on so this to do repository needs right to do now so it depends on that so that is the dependency of our 2d repository and here when we pass this from the outside we inject the dependencies so that is what dependent injection is really about we say okay we pass it here in the constructor that's called constructor injection however there are different ways of injecting dependencies another way is using dagger hill which is a library for that dagger hill actually makes all this stuff a lot easier so we just have a central place where we define all of our dependencies so in this case our database and our repository not much more but you could imagine that you would also have your api there that you want to provide you could imagine that you have some kind of maybe glide instance before image loading all that stuff would be put in a central so-called module where dagger hilt will basically learn how it can construct these objects to be able to then pass it to all of your different classes in your project like to your repository to your review models in the end and all that stuff let's actually start with setting it up and i will explain everything on the fly in our root package now let's close our data package here we create a new class call to do app so when we use daggerfield we just need to have an application class that inherits from application and that we annotate with hilt android app so that will just give it access to the application to also be able to use the application context to provide some objects and dependencies of us we then go to our manifest i need to specify that we have an application class by using a name attribute and passing to do app and close this again and now as i said we will define the so-called module which will be the central place where we define how our dependencies are created so dagger hill actually can inject these into single classes so here root package we create a di package for dependency injection and in that we create a so-called app module so why is it called app module well there are different modules we could create like in a bigger project you usually have more than just one module these modules not only define the dependencies we have on our project but also their lifetime so there are some dependencies we might not need throughout the whole lifetime of our application so they don't need to be so called singletons here we do that but if you maybe have an app that has multiple activities there might be some dependencies you only need for a single activity then you can put them in a single module which only keeps them alive for the lifetime of that specific activity but since we don't have that here we don't do that also by the way make sure that this is an object not a class we want to annotate this with module to tell dagger hey we are going to have a module here and another annotation will be installed in so this now defines how long our dependencies we define here are going to live since as i said this is the app module which contains only singletons so just application just dependencies that live as long as our application does we install this in the so-called singleton component and then in here we define with functions how the dependencies should be created so let's start by defining how dagger can create our room database so we can say provides because this function will provide a dependency and singleton so the actual dependency is a singleton there will only be a single instance that exists of it provides to do database it will need the application context to be created so what we need to do is we need to pass it here just the application and for the application dagger hill actually knows how to provide this so it will do all that behind the scenes we don't call these functions on our own that's all daggers job that will now return it to do database and we can just construct this the normal way here room database um builder it is i think uh actual database builder just room i think it's it's room database builder yeah this one here we need to pass our context which is app the class of our database is to do database class java and the name is just to dodb for example so that's how we just construct a new instance of this to do database object which we can then use in the repository to access the database and insert stuff we then want to call that build and you can see the errors are gone now dagger hill knows how i should construct this object to be able to inject it into our single classes now we actually need one more dependency that we provide here not only our database but also our repositories because in the end we will have a central place where we inject these and that will be our view models so in the end we need to think about what do we need in our view models to use our app and that of course are typically the repositories since now daggerhill only knows how to create this database instance but not yet how it should create the repository instance we still need to define that but to create the repository instance we need this database instance as you can see we need that here in the constructor so that's why we first provided the database and now the repository but the order here really doesn't matter the same step as before provide to do repository this time and this time we don't need the context to create the repository instead we need our database instance so we passed this here which is a to-do database and we turned over to do repository so here dagger hilt now will automatically detect okay we provided to do database here so it will know and pass it here behind the scenes so we just say we return to do repository implementation here and pass our db.dao and that's now how we create a 2d repository given our database instance now we have all the dependencies we need in our project since dagger now knows how to create a repository we can simply inject this in our future view models okay so far so good the next step is to implement the view models on the one hand we have a view model for our to-do list screen and we have a view model for an ad edit to do screen the screen on which we will add a new to-do or added an existing one so in the end usually in your apps you have one view model per screen if you have like an activity fragment architecture and no not jetpack compose then sometimes you also have a so-called shared view model so that you bind your review model to your activity and not to your fragments and then multiple fragments can use the same viewmodel instance it's not the case that you do that this often but sometimes it does make sense sometimes you want to share some data between different fragments and then you can also use a shared one in here by compose we usually use one view multiple screen you're totally fine with that if you do it like that what is now actually the job of the viewmodel in mvvm on the one hand business logic that is what you've probably learned in the 100 seconds mvvm video however it also contains a little bit more like it contains our state of the specific screen so what is state data is basically how our ui looks in a given moment so basically the data and values our ui reflects in a given moment so state in the end is a value that can change over time so if your background color can change over time then that would be state if you have some kind of text field and the content of that text field can change the content of that text field would be considered state and like the sum of all of these single states would be considered our ui state at a given moment and that is what our view model contains and keeps the advantage that we put this in the view model is that our app will survive screen rotations so in case you're not familiar with that when we actually rotate our device then we speak of a so-called configuration change so that means the activity is actually recreated and if you put these state variables like let's say you have a text field state you put these directly in the activity then that would be reset after screen rotation it could also be like you switch from light mode to dark mode that would also be a configuration change or you change your device language then it's also recreated like there are different scenarios where your activity is recreated and if you don't put your state in a class that outlives the activity or fragment life cycle or composables life cycle here then it will just be lost because it will be reset to the initial value since you initialize that in your activity or fragment and by just using a viewmodel we make sure that this can't happen so that the the same values or the values you actually um have when we rotate our device will just be kept and our ui will then automatically be notified about the current values after screen rotation so let's actually jump into our ui package and create a package for both of our screens first of all to do underscore list and in that we're going to have a to-do list view model and that will be a class every view model in android inherits from the model that just gives it some life cycle specific behavior and since we now want to inject the the repository using dagger hilt we do this in the constructor using this inject keyword or annotation so the way this works we import inject here we pass the repository instance here of type to do repository and for a viewmodel specifically we also need another annotation here which is hill to view model so quite complex how does it work as soon as we annotate something like either a constructor or a property without inject that will tell daggerfield okay please look into your modules if there is any type or if there's any dependency of this type that you know how to create and that it can pass behind the scenes for us here so we now don't create this viewmodel instance on our own instead daggerhild will generate code for us that already knows or that already created this instance for us and injects this into our composables later and since in our app module we defined how to create it to do repository dagger hilt will know um where's my viewmodel here uh we'll know how to simply pass it here in the constructor behind the scenes so again we don't create this instance it's all dagger healed which will do this in generated code for us so in here we will now put business logic we will now call our repository functions and we will now have our state variables okay what kind of state do we actually now need here let's first think about the the to do list screen what kind of state would make sense there if we open this here then in the end that's really just a single variable in this case and that is the list of to-do's that we actually want to display that's the only value on the screen that can potentially change over time because of course at the beginning the list is empty then we load it from the db we update the state with the list that we got from the db and then we reflect that and show that in the uh ui so we can just get that from the repository val to loose is equal to repository and get to use and that returns a flow which we can then easily transform to compose state in the ui layer by using collect as state but that's a composable function and we need to execute that in the ui but we also need something else and that is a so-called channel or if you want you can also use a shared flow so let's open the app again here a channel is actually used if we want to send one time events or shared flow both will in the end do the same thing channels are meant to be used with single observers and shared flows can be shared so multiple observers are acceptable there but you can use both for the same use case so what is what does one time even mean here it means well a thing that only happens once like if we want to show a snack bar here if we want to undo that like these are one time events that we actually don't want to assign to a new state because as soon as we have some kind of state in a state variable then that means the state will also be kept on screen rotations but if we actually delete this um to do item and we show the snack bar which is just one a one-time one-time thing and we rotate the device we don't want to see the snack bar again but if we would have that as a state it would fire off again and we would see the snapper again for maybe error messages we had in the past or deleted items we had in the past so that's why we need a channel here to just send these one-time events another use case would be to navigate if we actually click this button here then what will happen is our viewmodel will send a navigate event to our ui and the ui will then call the navigate function of our nav controller that's also just a one-time thing when we click the button we just want to do that once we don't want to do it again on screen rotations so just to make this clear what state what one time events here actually are i will here use a channel because we only have a single observer let's call this underscore ui event i call this underscore ui event because this is actually the mutable version of that so when we use things like state flow or state or in this case channels then in the view model we always define immutable version so that's the version we can send events into and then we also define an immutable version of that so a version where we can send events into and that immutable version will be exposed to the ui layer so that the ui layer can receive events but not send send new events into that so that's how it's supposed to be we can set this to a new channel here and well now we need to define what kind of events we want to send into that since i called this ui event these will be ui events that's a class we don't have yet so let's actually create a util package for that in our root package and in the neutral package i will create that ui event class that will be a sealed class because we will have different types of ui events here and that will just be a thing that we want to adjust an event that we want to center a ui so our ui does something in the ad so either showing a snack bar navigating maybe popping the back stack these things that ref that are relevant for the eye layer and yeah that we want to send from our review model so something we could start with here is just a very simple object to pop the back stack that will be an event we sent to the ui when we want to navigate back so object pop backstack black stack of type ui event doesn't need any parameters then we want to have estimated class navigate which will be an event we send from the view model to the ui when we want to navigate your new screen that needs the route we actually want to navigate to and we will have a final data class show snack bar that will actually take the message we want to show in the ui in the snack bar and it will take an optional action which is a nullable string so if we want to attach an action to that snag bar which we want in our app that undo action then we can also attach that here to that event and that's already it for the sealed class we can jump back to our review model import that pressing out plus enter and then we can expose an immutable version of this channel or rather this channel as a flow because then we can easily receive these events so ui event without an underscore now is equal to ui event receive as flow then we can subscribe to these changes in our ui layer and we receive these one-time events just once another thing that i always like to do with mbvm is that each screen also has a so-called event class so with that i don't mean this ui event instead i mean specific events to this screen and those will be events that we send from the screen from the ui layer to our view model when there is some kind of user interaction so when we click a button when we maybe want to reload the list when we click something on the toolbar those would be events that we would send from the ui to the view model so the view model should do something and this event class that we will create here in the to-do list package we'll just make this very clean and easy that will be a sealed class as well and we will call this to-do list event so you just always call this as you call your screen followed by event and as i said you do this for every single screen and now for this event class we need to think about what kind of user interactions could we possibly have on this to-do list screen well we could for example click on this delete icon we could for example toggle the done state of our to-do we could click on an item on a to-do item to edit it we could click on this floating action button to add a new to-do and actually one more we could click undo which is also new event that we sent to our review model another ui event in terms of this ui event class i mean an event of of this class here so let's start with deleting a to do so that will be a data class delete to do and if we'll just get the to do we want to delete and we send that to the view model of type to-do list event we also have another data class which will be on a done change so when we actually want to change the done state of a specific to do so if we want to toggle that we need to first pass the to do we want to do that for and we need to pass the new is done state which is a boolean so the viewmodel can in the end then update that in our database then we also want to have something okay that we actually can undo the deletion of it to do that will just be an object it doesn't need any parameters because we will just cache the recently deleted to do in rov model and then just restore it if we click that so object on undo delete click of type to-do list event and we also want to have a data class onto the click when we click on to do item to get to its detail view and one final one is actually to just add a new to-do so data class or actually an object because that does not need any parameters um on add to do click to-do list event so far so good now we can hit uh jump back over to the viewmodel and implement a function on event so that would be the function that we trigger from the ui given such a to-do list event and in here we can now very easily distinguish what kind of event that is and depending on that we just execute a different function or just some different type of code so if that is on to do click we do this if that is on add to do click we do this on delete or undo delete let's start with that um is delete to do i actually want to rename to on delete to do click so i just um had the cursor on that press shift f6 and that way you can rename this everywhere in your code and i'm missing one event that is undone change just like this and now that's very very clean and easy so you don't need a separate function for every of these events in your viewmodel instead you just have one event function and that handles them all so let's actually start with one of the most simple ones and that is on add to do click because all that should really happen there is that we send a navigate event to our ui that we now want to navigate to the add edit to-do list screen and for that i actually like to also have another object in our util package which just defines our different navigation routes will be an object and called routes and here we will define our two routes that we have on the one hand that is to-do list equal to to-do list and cons val adds edit to do just like this and then here in on add to do click we can say that we actually want to send an event into this chat flow or yeah we use a channel here right yeah i want to send an event into this channel or shared flow if you use that so we can say ui event send that is how we send something into a channel and we want to send a ui event dot navigate and here we can now specify the road we want to navigate to that will be routes at edit to do when we click that button we want to get to that route i think that's pretty clear we still get an error here because this event um these these send functions of these ui events of channels need to be executed in a crew routine so since that is an asynchronous call we need to first launch a crew routine if you're unfamiliar with that i really recommend to watch my current playlist that will teach it in detail but for this scenario here what we basically want is we want to have a separate function to just send an event into this channel you will see how this works private send ui event here we want to be able to pass such an ui event oops and here we can launch a curtin we can do this using view model scope so that is just used to bind the kind of lifetime of this routine to our viewmodel lifecycle so if we do some kind of asynchronous call like let's say you want to make a network call and your viewer model is clear then you actually want to stop executing that network call that ongoing one and that's what this go will do here so it will determine okay if the view model is cleared all the cretins in that scope will be cancelled so we can say that launch to launch a new crew routine and in this block we can now execute these so called suspend functions as we also have with our room database so we can take this and simply put it in here and now you can see the error is gone because we execute this in this launch block if we click on this and uh click control q you can also see that's also suspend function so that's why we got that error because it needs to be executed in such a launch block or in another suspend function now we can go in here call this function send ui event and we say actually we don't want to hard code this to ui event navigate instead we just want to pass the event we pass as a parameter and then we can pass that here to navigate to our add edit to do screen another easy one is on to do click because in that case we also want to navigate to add edit to do but with a minor difference and that is that we also want to attach the id of the to-do item we clicked on because if we click on an existing to-do item we of course want to load that item on the add edit to do screen because otherwise it would just have empty text fields it would assume we want to add a new to do but if it exists we want to edit the existing one instead so what we do is we say routes add edit to do and we just add a to-do id argument here and we can say that will be event dot to-do dot id so that is what we basically pass here well what can we do next when we click on undone change so when the uh checkbox is actually toggled then we want to just update that change in our database with the new boolean so what we can do is we can say the modal scope.launch because to execute our database functions we also need a creatine these are also suspend functions and in here what we want to do is we want to say repository now we access our repository which accesses our database and we want to insert a to do well we don't want to really insert it to do because it already is in our database we only want to update the done change the the done state for that however if you remember in our to-do dial we defined this on conflict strategy replace so if we try to insert a to do that already exists we will update it instead and that is what we will make use of here so we will use the to do we actually passed for the event that will come from our ui layer and we copy that so we keep its id that way it will be updated and we just say is done is now equal to event is done so that will be the new is done state that we also received from the ui so whether this checkbox is now checked or not then what's missing well we still need to be able to delete it to do and undo that deletion let's start with deleting a to-do and also launch a curtain here how would we do this we of course want to say repository delete to do and we pass our event to do that is already enough to delete it however we need to also consider something here because we want to be able to undo that deletion and how do we know which to do we actually want to undo when we click that because of that what i will do is up here we will have a private var deleted to do and that is a nullable to do so that is used to cache the recently deleted to do and if we then click undo we simply take this and insert it back into our database so here we before deleting it we want to say deleted to do is event to do and then in under delete click we can simply restore it however we also want to send a ui event here which will be show snack bar because we want to show that snack bar to also be able to undo it the message will be to do delete it and the action will be undo and then our ui will receive that event and show the snack bar and then finally for on undo delete click we also want to we actually first want to check that the deleted to do is not equal to null and if that's not the case i'm going to also launch a qrutin and view model scope again and we just want to say we insert that to do back into our database oops and that is already everything for our to-do list view model the next step would be to implement the to-do list screen so just this screen here in jetpack compose it's really not a difficult screen we just need to define such a to do item composable which we will do we need a scaffold to display this floating action button but that's already it for this screen and then after that we will implement the add edit to do view model and the add edit to do screen so let's actually take a look at our ui and think about how we will structure this in the end we just have for such a list item we just have a row which contains this box and this checkbox just horizontally aligned then here we will have a column inside of that row which contains another row for these two items and just another text here that's how that is arranged and we're now going to jump into our code and apply that so in to-do lists we create a new carton class a file will be a plain file just like we always do it in jetpack compose alt to do item and we can create a composable call to do item and this to-do item will take three parameters on the one hand the to do item from our database or to-do entity that we actually want to show in the ui then it will actually need on event as a lambda function which will take a to-do list event and doesn't return anything so you will see why that is helpful i won't go into that here but you will understand that in a moment and we will be able to pass a modifier which should usually be possible for custom composables just like this as i said the very very outer container will be just a row where we apply our modifier actually the modifier we passed and we make sure that we set the vertical alignment to center vertically for that row then inside that row as i said we will have a column here and a checkbox so on one hand we have our column for which we will apply a modifier of modifier.weight one f with that we just make sure that the column here the width of the column will just take all the space it can get while the remaining elements will just take the space they have or they need rather we also set the vertical arrangement to center and then finally in that column we will have another row as i said which will contain this text and this icon so a row we say modifier is modifier dot fill max size do we actually want to fill the maximum size no i don't think we want that i think we don't even need a modifier so i think we're pretty fine just saying vertical alignment and set that to center and in that row we will just have our text that displays the to-do title so we say text is to do dot title we want to set the font size to maybe 20 sp import sp and we can say font weight is bold then let's have a little bit of spacing between the text and the delete icon atp import dp and then we have our icon button it will display an icon which will be icons default delete and content description will be delete when we click this button we will now call our own event function with uh on delete to do click because that's what we want to do and we pass our to-do statement so now the cool thing about having our to-do list event class is that all we really need to do for our child composables like this is not the the parent composable this is not what reflects our whole screen this is just a single item and in a more complex layout you can have tons of these single items and these could also contain like other custom composables that you implemented and all of these now only need this own event function to propagate events up to the parent screen which then talks to the view model if you don't do this then you would need to have a callback function here for every single thing a user could do so we would need a parameter for actually clicking the to-do item we would need a parameter for deleting it we would need a parameter for toggling the is done state having this event class we only need this on event function for every single of our child composables and that is pretty cool then however if we scroll down we're not done yet for this column we now have this row with our title and the delete icon below this row we want to have a little bit of spacer with height of atp and then actually we first want to check if the to do description is not null so since that's an optional parameter we want to check if that actually exists if it's not we want to apply our spacing and another text with a description so here we can just pass it and the final thing that's missing is the checkbox that will be in the very outer row so that's right here we can say check box the checked boolean here will be to do is done and uncheck change here we get the newest checked state which we can then pass to our own event function saying on [Music] done change and we say the new one is now is checked and we actually also want to pass the to-do we actually changed the state for so that's it for our to-do item the next step is to implement or to do screen to to do list screen actually also here in this package to do the screen playing kotlin file composable to-do list screen which parameters will that need on the one hand we need a function on navigate which we will call whenever we want to navigate from this screen to another screen so the screens themselves don't handle the navigation that will be done by the parent composable which will be purely for navigation which will contain the nav controller instance so that way we don't need to pass this instance down to all the very lowest level com composables here instead we just have all the navigation happening in a single file basically and we just propagate up these navigation events so those will be ui event navigate and we pass unit here and what we of course need is our view model reference because now we're in the ui layer now we're actually talking with the view model because the view model now contains the state of this specific screen so that we also want to be able to retrieve that state when we rotate the device so that it's actually not lost so view model to-do list viewmodel and since we use daggerfield to inject that viewmodel we just need to say hilt your model and that will automatically pass this view model here for us we don't need to pass this on our own or create it on our own dagger will do all that for us anything regarding dagger here is unclear i know this is often hard to understand if you've never faced that before and just ask below and i will answer your questions so first of all we want to retrieve our to-do list state so bow to deuce we get that from our viewmodel.todos which is now a flow but we need it as a compose state because only that will trigger recompositions so that everything is updated on our screen collect as state and we need to provide an initial value here so just the value we start with and that will just be an empty list then the next thing we want to do on pretty much every screen is we want to collect these ui events so for that what we need is we need a so-called launch defect block passing true as q1 here so what that will do is it will basically execute this code here independently of this composable function because this composable function is called every single time our to-do list screen updates we of course don't want to subscribe to our ui event flow every time the ui updates but that we only want to do that once when we actually show it up the first time and that's what this launch defect block will take care of here we can save your model ui event collect and that will now be triggered for every single event we send into this new i event channel so every time here we call send ui event and we call this send function we now trigger this block of code here and now depending on what this event is if it is maybe show snack bar we want to do this if it is navigate we actually just want to call on navigate with this event and if it is pop backstack we want to actually also call a function to pop the back stack oh actually we don't need this here because on this screen we never want to pop the backside we only want to do that in the add edit to do screen because when we insert a new to do we automatically want to go back to the list screen so here we don't need this we can just say else unit so else we don't do anything so what happens if we receive the snack bar event well then we want to show a snack bar obviously and for that we can do that for that we can actually have a scaffold state with which we can do that easily so well scaffold state is remember scaffold state and then we can use that to say scaffold state snack bar host state chose nagbar and the message we attach will be event.message and the action label will be event action now how do we detect if we actually clicked on undo here for the snack bar well we get a result here and after that we can check if the result is snack bar result action performed so that will be the result if we actually clicked on the undo button if that's the case we want to say view model on event that's now in ui event that we sent from the ui to our review model and the event that we sent here will be to-do list event dot on undo delete click because that's what we did in this case when we click on this snag bar undo button then we just want to send this undo event to the view model and the view model will then take care of that here that's how this will always work you can use the template in any of your projects just having um this event class for every single screen and this on event function will always be the same you call that whenever you whenever the user does something on your screen and when the view model wants to send a ui event back to the ui you just create this ui event class and different types of that for whatever you need that so down here below this launch effect block we will now have our actual content which will be a scaffold here we pass our scaffold state and we want to make sure we have a floating action button floating action button well when we click on that floating action button we also want to call viewmodel or event that's again just so you just something the user does on your screen this time it will be on add to do click because that's what we did and the content of that will be an icon icons default add just to get that little plus icon and the con description will be at that's already it for this floating action button in the scaffold we now only want to have our to-do list list our to-do list so a list of our to-do item composables which will be a lazy list lazy column i want to say modifier is modifier film exercise and in here we will have our items block so i will use this one with a list of t so we can just pass or to do's state here which we actually let's just pass to do the value here to get the actual list from that and we get the to-do item for the current composable here and we want to have to do item here with password to do and the cool thing is now for all your child composables that you get that you actually have on your screen on this screen we only have the single to do item but you remember that we have this on event function in this to do item which we here just trigger for every single action like when we click on the delete icon when we click on the check box and for these things we now send these events up and the cool thing we can now do is because our view model directly receives these events we can say view model double colon on event that's super clean so you don't need any crazy lambda function here you can just delegate this event that we propagate up directly to the view model and that's super cool super clean here i always use this approach in my projects because it just works so well and doesn't require a lot of code one thing we need here is a modifier compose one and make sure to fill the max with i want to make it clickable because when we click on our to do item we want to say viewmodel on event on to do click and we clicked on this to do that we got from the list and let's add some padding of 16dp and import that and that's it for today's screen i guess yeah so now that is implemented um we certainly can't try this out yet i mean we could we would see something but all we would see is the floating action button because our database is empty so we wouldn't load any to do's and yeah so we first actually need to implement the add edit to-do list screen before we can add new items before we can see them in our to-do list screen okay next up we have our add added to-do screen so let's go to our ui package create a new package for that add edit to do and let's also create a viewmodel for that add edit to do view model select class make sure that inherits from viewmodel and we just have the same procedure again as we had for the to-do list one we just have our healthviewmodel annotations or hilda knows how to inject its dependencies into this view model and this inject constructor with all the dependencies we need for the add edit list add edit to dovie model like that so let's start with hill view model then have inject constructor import inject and we again just need a reference to our repository so private val repository of tab to do repository and yes we actually need another dependency here well what could that be we only provided two dependencies in our repository and the repository already uses the database we need the so-called saved state handle so we can actually say save state handle off tab save to stakehandle so what is that that is yeah kind of a key value object it basically contains a bunch of state variables so on the one hand we could use that to restore a viewmodel state maybe after process death but on the other hand what's really useful is that this contains our navigation arguments so on this screen we actually have this navigation argument the to do id which we would actually like to load from the database if we clicked on a to do to open its details here on this page so hilt will do all that for us behind the scenes since we use inject here we don't need to provide this on our own anywhere it will just work magically so what do we need in here in here we now need our different states again as we did here for the to-do listview model and we only had a single state so that wasn't really a compose date because we got the flow from the room database but here for the add editor duelist screen we need a separate state like different multiple separate states on the one hand we needed that to do title so if we enter a title in our text field we want to change the state for that we do we need the same for the description and we actually need yeah a state for the actual to do that we potentially loaded here if we clicked on an existing to-do so let's start with that actually that will be a public variable to do and that's equal to immutable state of um a nullable to do here because if we want to add a new to-do to our database then it will simply stay null and want to make sure that we have a private set here that means we can only change the value from within our view model but we can read it from outside it's basically for the same reason as we had these two versions here so that we only expose an immutable version of these states or flows or channels to the ui then we want to have a variable for the title which will be a mutable state of empty string by default private set and we want to have a variable for the description which will be also immutable state of empty string private set so those were all of our state variables let's further think what comes next do we need some kind of events we send from the view model to our ui yes we do need that we again need this ui event channel so we can simply copy this paste it here what kind of events would we like to send well for example if we want to if we actually finish saving it to do so when we click the check button the check button we validated the inputs everything goes well then we want to send the um the navigate event or the backstage event rather to the ui so that we navigate it back to the list screen where we then see the edit to do so let's first overwrite or rather implement the init block which will be executed as soon as our viewmodel is initialized here we first want to check do we actually or did we actually open this add edit page by clicking on an existing to do or by clicking on adding a new to do because if we want to open or if we open this from an existing to-do then we want to load this to do from the database by its id so then we would get the to-do id using our saved state handle that is an integer the navigation argument is called to-do id and if that's null we simply want to oh that's never now because we make that we don't make it a mandatory argument or i think we have to um not not sure i think we can make it an optional one and just have a default value because for integer values we can't pass null here for the arguments but it should work like that if we take a look here i made it an optional one but we actually always pass it because we will define a default value of minus one for the reason i mentioned we can't have nullable integers and if we don't oh if we didn't click on it to do then we will simply pass -1 for the to-do id and that way we can detect if we actually want to add a new one or open an existing one so if this id is not equal to -1 that means we clicked on an existing one and we want to now load this so we save your model scope.launch and we say our to-do or state is equal to repository get to do by id and we simply pass our to-do id we do get an error here because the to-do id could theoretically be null which will never be the case here so we just assert it's not equal to null here and we still get an error immutable state oh because i set these equal to this we actually want to say buy mutable state off and import this delegate way here way we can easily assign the state without using that value all the time and then the error is gone we also want to make sure to assign the corresponding title of that to do to our title state so title is equal to to do that title let's actually make the null assertion here and remove this one um then why do we still need this oh because i want to [Music] you know let's do it the other way let's say that let here reread the to-do and then we're sure it's not now we can say titleist to do title description is to do description and finally um the description is actually yeah if that doesn't exist we pass an empty one and finally we say this at attitude view model dot to do so our to-do state is the to-do item here that we read from the database so i hope that makes sense that was a little bit uh confusing i think so we just read the to-do id if it's not minus one that means we open the to do from an existing to do so we click on an existing one and we want to load its data into our ui so we load that with our repository function assign the to juice title to our title state so it's showing up in our text field doing the same for the description it's optional so we if this doesn't exist we just assign an empty string to the description state and finally we assign that to do uh the to do to our to-do state here so now we have this in the blog what is the next step the next step is to do the same thing again as we did here in the to-do listview model we want to have this on event function in the add editor-do-view model as i said that will be the same for every view model at least every model that is bound to a specific screen so function on event and which events do we pass here well we need a new event class so we go to our add edit to do package new class um see class add edit to do event and here we now need to think about it again what kind of events we could possibly send or what what could the user possibly do in this add edit to do screen on the one hand they could change the text content of our title text so let's call it on title change with a new title um add edit to do event same for the description on the description change description then we could click on save to do so object on or rather on how do we call this once yeah let's call it unsafe should you click add editor to event and let me think about it do we need something more no i don't think so let's go back to the view model now use this add edit to the event and again just have a one expression and check what that event is if it is on title change we simply want to change our title state to event title if it is on description uh change we do the same for our description state and finally if it is on safe to do click we what do we want to do in that case well we want to launch a curtin again because now we want to insert the to-do into our database but first we want to check if the title is actually not empty because only then we want to insert it into our database so if title is not blank we say send ui event let's actually copy over the function from here the send ui event function place it in our add added to do view model call it here and say the event will be show snack bar i want to say message the title can't be empty and we return at launch if that's not the case we can simply insert a new to do into our database so repository insert to do and we construct a new to do um actually we don't want to construct a new to do yeah we do so you should do the title will be our title the description will be our description the is done state will be either the is done state from our to juice state so to do is done so if we loaded the to-do then we want to keep the is done state at whatever it is if we didn't then this will be null we just want to say false so initially these dues are not checked and the id will be to do id and if that is null then room will generate a new id and after we did that after we finished the insertion here i want to send another ui event which will be pop backstack because then we want to navigate back to the to-do list screen it's really that easy so let's finish this off by implementing our add edit to do screen in our add edit to do package so now we implement the ui that talks to this view model that we just created so add edit to do screen select file and make that a new composable edit to screen that will again take leather function that we will trigger from our viewmodel this time only on pop back stack when we receive that backstack event we normally need a navigate event because we don't want to navigate anywhere we just want to pop the backstack here and we need a reference to a review model here of course and also equal to hill view model so hild automatically injects that here in our composable well we again need a scaffold state to be able to show snack bars so remember scaffold state then as on the other screen we have a launch effect block to collect these ui events that we get from a review model so we can save your model um ui event collect get the events in here and we can check whatever that event is if it is pop back stack we say on pop backstack so we just trigger our callback function so we propagate this event up to the navigation uh composable so we can then pop the back stack there using our nav controller and we want to check if it's a snack bar event in that event in that case we want to use our scaffold state snack the host state show snack bar message will be what will it be event.message and the action label is event action which we actually don't have on the screen we can still assign it here so now we have that actually let's also have the else case here so we don't get this warning down here we will now have our scaffold where we pass the scaffold state that we created above i want to add a modifier here compose one fill max size and i want to add a little bit of padding of 16 dp and import dp and we of course also need a floating action button here on the screen to be able to save the new to do so floating come on floating action button here we pass a floating action button composable well when we click on that we again want to fire off the on event function of our review model which will be add edit to the event this time dot on safe to do click and in here we use an icon icons default save or is it it's called check yeah and the content description will be save so that is pretty similar as on the other screen in here we basically now only have our two different text fields and i think that is already it so let's create a column modifier is a modifier fill mag size and then in that column we will just implement two text fields so text field the value will be view model title so the first one will be our title text on value change in that case we want to say umodel on event that is a normal user event on our screen we want to say on title change and pass the new title here let's also say we have a placeholder here just as a little hint will be normal text um just title then what else could we have here a modifier modifier fill max with definitely and that's it for this text field then let's add a little space of height hdp and then we copy this text field paste it down below view model description this time on a description change change the placeholder to description this friction modifier will stay the same and yeah here we could also theoretically say single line is false and max lines is five so we could also have a little bit of of a longer description but other than that that's already it for this add edit to do screen which is a very simple screen now that we have both our screens now that we have all of our view models and we have a repository the only thing that's missing is the navigation which is luckily very easy with compose i will put it directly in main activity here in a more complex project where you also have other stuff in main activity i would create a separate navigation composable for that i will leave that away here and one thing we also need to consider is that we annotate this activity with android entry point that is an annotation that comes from daggerfield and is necessary as soon as we want to inject dependencies in an android component an android component could be an activity it could be a fragment could be a service all that stuff if that's the case if you want to inject dependencies in that like we want it here with our view models you need this annotation if you want to inject that in other classes then you don't need that so in here we basically just define our nav controller first which we use to navigate by remember nav controller and then we have a nav host where we pass our nav controller and we want to also define start destination which will be routes to-do list and then we open this lambda block here so in the nav hose we now define our different screens we have by using these composable blocks and defining a route for that the first one will be routes to-do list and here in this block we simply call that screen composable which will be to-do list screen and in on navigate well what happens there we simply want to say nav controller dot actually in a lambda block nav controller dot navigate to it dot route so we get this ui event in here and then we use the nav controller to navigate and finally one more screen that will be another composable the route will be routes.add edit to do here and we want to have an argument here which is the to-do id that we want to be able to pass to the screen so we say to do id is equal to to do id so whatever we then pass for the to-do id will be replaced here and passed to our other screen so arguments will now be a list of nav arguments and in here we define how our argument looks like so let's oops so that here name is to-do id so this must be the same as this and we can customize this argument we want to say the type of that is nav type that in type because the to-do id is an integer we want to be able to pass integers here and if we don't pass a to-do id if we want to directly create a new to do we need to pass some kind of default value to detect that and that will be -1 oops minus one as i showed you in the add edit to do v model so here if this to do id is not equal to minus one that means we passed one and then we load it if it is minus one well we don't load it so here we open the block of code and we have our ad added to this screen on pop backstack well in here we can just say nav controller pop-back stack it's that easy and i guess and that is everything for our project was quite a big project here at least for a single video but i hope this was helpful let's actually launch this app and i am excited if i messed something up and i forgot something or if everything works and the app is opening it's at least not crashing which is usually a good sign let's try to add a new to do by clicking on our floating action button we get to our new screen let's enter test title here test description and we then click on this check mark and the title can be empty okay it seems like i messed something up there um let's see at edit to do screen when we enter title we send this on title change event and in the view model what do we do when we receive that event we assign a new title to that and here we check if the title is not blank oh actually if it is blank then we want to show the snack bar of course so it is working as it should so the title is assigned correctly but that was just a messed up statement let's try that again test title and test description but then we've already seen that the snack bar works so if we click check then there's our to do if we check that if we minimize actually close this app and the other one as well and then relaunch that that was the other one um this one oh that was the other one again so it is the first one this one yes there we go it's still checked it's still in there in the list if we delete it to do delete it we can click undo if we rotate our device um then you can see it also survives screen rotation which is one of the purposes why we should use an architecture like mvvm so that is working pretty well let's actually also check if we can update this existing to do if we click on it yes the test title and test description is loading test title updated save that and there we go then the title is updated the checked state is still checked and yeah we can toggle that as we like so let me give you a quick little recap of what we have done on the one hand we have started with our data layer we defined our to do entity which is the table in our database which contains whatever we want to save for one specific to do item then we use that in our to-do dao in which we define how we access our database and what we want to do with it we defined our to-do database class which is just for room to define okay this is how a database would look like these are the entities you want to include this is the version and then room will generate all the necessary code behind the scenes for us so we can use it then we define this to do repository interface in which we just defined the same functions of our dow again this looks not necessary here in this project it is not necessary but the bigger your project gets the more these repositories will pay off since they access your different data sources if you not only have a database like also an api then the repository you would also access the api that is its job then here's the implementation of that which is also very easy here we just use our dao and we call the corresponding functions on it we have the app module for which we use daggerfield so here we basically provide all of the dependencies all of the objects we want to be able to inject somewhere in our code um which is just our to-do database and repository here in this project we have our to-do list screen for which we define this to-do list event class that is used to send events from the ui to our review model or this side of the communication and we have the other side of the communication which is this ui event which we send from the view model to the ui when something in the view model changes like popping the backstack navigating showing the snag bar and much more if you need that in the to-do list view model we then have this on event function which we trigger from the ui when there is some kind of user interaction so we send these events here and then distinguish whether this event is on to do click if we clicked on add to do and so on and just do some different stuff here and the same actually in add additive model it's yeah it's really the same so you can use this kind of template in all of your projects as i said just have a repository and for each screen you have an event class you have a screen composable with potential child composables and you have a view model that receives these events and sends events back to the ui in form of ui events so i think this actually serves as a pretty good course now for any android beginners who just learned the android basics and now want to learn a real architecture which is you you have to know that if you want to make real apps it's it's mandatory there is no way around architecture so this is super important what you have learned here now it's also time to be proud that you actually follow through this video if at some point you want to actually take this to the next level then i suggest you to watch this video which is about making a dictionary app with clean architecture so that is yeah some more concepts on architecture that are basically on top of mvvm that will yeah even make your app architecture cleaner if your app grows
Info
Channel: Philipp Lackner
Views: 108,893
Rating: undefined out of 5
Keywords: android, tutorial, philip, philipp, filipp, filip, fillip, fillipp, phillipp, phillip, lackener, leckener, leckner, lackner, kotlin, mobile, mvvm, architecture, jetpack compose, compose, jetpack, viewmodel, lifecycle, configuration change, config change, rotation, screen rotation, crash course, course, 2022, android 12, android 31
Id: A7CGcFjQQtQ
Channel Id: undefined
Length: 93min 6sec (5586 seconds)
Published: Wed Dec 29 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.