Full Guide to Dependency Injection With Koin for Compose Multiplatform - KMP for Beginners

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
hey guys and welcome back to a new video and a new video in this KMP for beginners playlist in which I will lead you through how you can set up proper dependency injection for cotlin and compose multiplatform projects using the dependency injection Library coin or coin as I thought the pronunciation would be because it actually stands for cotlin inject and on the other hand the package name says insert coin so pick whatever you prefer have some mercy if I still say Co in here and there but first of all I will lead you through what the pency injection is for those of you who don't know that and then I will show you how we can set that up for cotton multiplatform project and handle pretty much every scenario we might encounter on a daily basis working on Cotton multiplatform projects as usual you can create an empty compos multiplatform project here on K.J brains.com enter your details pick Android iOS and desktop download the zip file extracted and open the extracted folder in Android Studio which I've done here and then you can just follow along and before we actually set up coin in our project let's first of all understand what dependency injection really is at its core because so often I see people just completely over complicate this topic which is actually not too difficult so let's actually just take a quick look here in our compost app you don't need to follow along here I just want to show you a little example of what dependency injection now really is at its core we can for example take a look at this greeting in the sense of dependency injection a dependency really just refers to an instance of an object in our code so for example an instance of this greeting class so please don't confuse this with Gradle dependencies which are usually referred to as our libraries here there we also say dependency but in the sense of of dependency injection we really mean such instances of classes in our code so now that we understood what a dependency is we need to take a look at the whole term of dependency and injection so injection means that we take this dependency and inject or pass it somewhere and that's really all it is it's really just about passing instances of objects to other objects so if we would have some kind of Class B here and it would take a greeting instance in its Constructor we applied dependency injection because we took one dependency and injected it into another dependency on the other hand what would not be dependency injection is if we would do something like this so if we would create the greeting instance directly in the class body because then we don't really pass it from the outside into a Constructor into a function signature or so and the whole reason why we want to do this and why this is usually such a no-brainer to apply this design pattern so we pass such objects into the Constructor of other objects is that this leads to so much more flexibility in our code because it just allows us to be able to swap out implement ations of objects very easily so in this case we're really just dealing with a class so we wouldn't be able to swap out something here with a different implementation but if we were to have some kind of interface here let's say we would have some kind of repository and then we have an implementation of this repository which is the let's just call it production repository um which we wouldn't call like that in our real code but just to make it clear which is an instance of this my repository and then we might also have a test repository which just contains test specific code which maybe simulates the real behavior of the real repository so we have two implementations of one interface of one abstraction and if we now apply dependency injection so we pass this repository instance here in the Constructor of our class B then we are very free when we create an instance of this class B which type of specific repository we actually want to pass so if we're in a test case we can pass the test repository if we are in our production code we can pass the real production Repository however if we do it like this that we create the dependency in the class body so we have a private Val repository well then we somehow need to initialize that here so which repository do we now set this to if we say production repository then that will work for production but we force this class B in every single instance we have to use exactly that specific type of repository so here we aren't able to replace this with our test repository for a different instance and this is not only useful for testing so even if you would have multiple different implementations for your production app let's say you have one repository which uses Library a and you have another repository which uses Library b then you can just replace and swap out that specific instance from library a with Library b and the whole rest of your code will work just fine without needing any further adjustments so this is really just the concept of dependency injection it's really not connected to specific libraries or Frameworks at all so if you come from Android then you might know dagger which is a very popular dependency injection framework we use for Android or you might know coin which we use for this video but all those are just libraries that aim to make dependency injection easier for us and to implement this more easily but it's not the library that is dependency injection dependency injection is really just passing objects to objects so applying this concept of dependenc injection is really a no-brainer I would use this in every app I can't think of a single scenario where I wouldn't use this concept on the other hand using a DI so a dependency injection framework or library is optional so using a library like coin is is optional you don't have to use that you can also just use normal Constructors construct your dependencies and initialize them at a central place and then just pass these in your code when you need them to the right Constructors however especially on Android these di Frameworks are quite established because they really make our lives easier especially when it comes to more complex types of dependencies we need to construct like view model references for example where we usually don't just create a normal instance but rather couple the instance to a specific life cycle or so and then these libraries really make our lives easier that's also why I want to stti to using coin here in our project since that is usually the dependency injection library that we want to use in coton multiplatform because it's just the most established pure cotland library that currently exists so dagger for example is a library from Google which is very popular for Android but it actually generates Java code at this present moment so we can use this in a c multiplatform project maybe in future but I think it still has a long way to go so we want to use coin how do we now set up that in the end coin is just a library and we add it the same way we also to addit other libraries so make sure to open your lips versions TL file you can find this in your Gradle folder here and then make sure to add these four dependencies um now talking about Gradle dependencies you can find the source code and especially this liip versions file down below clicking the first GitHub Link in this video description and then just copy paste these versions and the dependencies that I will show you in a moment and then you will be good so on the one hand we have coin which has to have this version at least for some reason it marks this as not being the uh latest version and if I Alt Enter here then it shows me to update to Alpha 3 which is quite weird because beta 4 seems to be later than Alpha 3 but let's just leave it here we have coin composed multiplatform so with some um composed multiplatform specific functionality for dependency injection navigation compos that's not related to coin but I want to show you all scenarios we typically would want to inject something into in a c multiplatform project so also in view models for example with uh the official navigation composed Library and lastly in order to initialize and create a view model we need the live cycle view model dependency so make sure to copy these four lines and then scroll down and copy these how many are these seven lines for the specific dependencies you want to include make sure to sync Gradle after you've eded these and then we can hop into our build Gradle kts file in the compos app module and in here we will find our common main dependencies where we can now add all those coin navigation composed specific dependencies so we can also use these in our code so here for common main we want to write API not implementation API just allows us to also access the grer dependency we include here in modules that depend on this common main module so our Android main desktop main iOS Main and here I want to say lips. coin. core so just the core set of dependencies here and all the other grader dependencies can we uh we can add with implementation so on the one hand lips coin compose oops lips coin compost. viw model so we can also inject view models into composed multiplatform navigation stacks and we can duplicate this two more times because we also want to add the life cycle that view model dependency and as I said the navigation compost dependency so lips navigation compost and that is it there are two dependencies we need to add explicitly for Android because uh di Works a little bit differently there and so we need some additional dependencies here we can just add implementation uh lips Coen Android and lips coin Android x. compose let's it sync now because I think that should be everything we need here and then we can start understanding how coin works now we can use that to now inject dependencies in our project all right so first of all we need some sort of dependencies we can actually construct and inject s and for that I will just create some sample but realistic dependencies that we want to inject here in our common main cotland source set in here let's just create a little directory or package called dependencies in which you create some sample dependencies how they could really look like in a real project like for example a DB client we can make this a normal class make this an X class so let's just assume this has different um implementations or different mechanisms how we need to create an instance of this class on Android then it has on desktop and iOS so we just make this an xack class and assume this is our DB client where we can then hit Alt Enter and add the missing actual declarations yes want to do that that we can then go and Implement that in desktop Main in Android Main and in Native main hit okay and then for example here our three files actually opened here we are for the desktop variant let's assume for desktop we can just create a normal DB client instance without specific type of Constructor here on Android what is very common for such a DB in is that we need the context reference so on android. Contex is really just an object that provides access to all kinds of different operating system related functionalities such as creating a file for example a DB client of course needs to create some sort of DB file where the whole database and all those tables are saved and for that we need a context which is just a pure Android concept and lastly the native client can also stay like that so we can close this again and I really just have this example of a DB client which is an expect class to show you how you can also inject such um X dependencies in a c multiplatform project let's now assume we have another dependency which is my repository we had this before but let's make this a real instance which in this case um will it just returns a hello world string so let's say function hello world returns a string which we'll just use later on to verify that our injection really works and really injects the the real instances but for now let's just keep this here and we have our specific implementation here so my repository um implementation we can actually autocomplete this here so uh that is instance of my repository and it overwrites the hello world function where it just returns this hello world string and what's now very common is that our repository depends on some kind of DB client so we have our DB client instance which we now inject here in the Constructor and lastly what I want to show you is that if we now have a view model so the model for our view for our UI let's say we have a my view model class here in our dependencies package which is of type view model if you've seen the video about share models then this won't be new to you and in here we can then have a private Val repository of type by repository so this is again the scenario I showed you in the beginning of this video that providing this repository instance here in the Constructor of our view model allows us to pass either this my repository implementation so this specific implementation of the repository or different repository we might want to use for testing for example if we wanted to use this if wanted to test this my view model here in isolation without for example having the test fail because of an issue in the repository but only having it to fail because of issues in the view model itself and then here in the view model just to again verify this works we have a function get Hello World string which returns a string and then here we can just say return repository hello world this will then be called from our UI so we can just display this hello world string and verify that our dependency chain is properly injected all right next up we can come to coin specifically and in order to understand how how such typical di Frameworks work we first of all need to talk about the concept of so-called modules so here in our cotland common main source set let's create a new directory I'll call that di that's typically the name I choose for Di related functionality from our di framework and in here we want to have a file called modules this will just be a plain file and here we Define those modules a module in the sense of di is really just a container for a specific set of usually related dependencies so now that coin is actually able to take our dependencies and get to know these in the first place we of course have to declare them somewhere so we have to tell coin hey this is our my repository instance and this is how you have to create this instance this is our view model instance and this is how you have to create this view model instance because only if coin knows that it can of course also inject these dependencies somewhere in our project and when it comes to using coin on cotland multiplatform Project we have to actually distinguish between platform specific dependencies so just um object or instances of classes where creating them differs depending on the platform like for example here our DB client which requires a different creation process here on Android if we take a look here again uh dependencies DB client Android here you can see we need a context which we only need on Android while we don't need that on other platforms like desktop and iOS so for those platform specific dependencies we need a separate module and for all dependencies we can create in our shared code directly so in this common main source set for those we can just have a shared module so here in our modules KT let's first of all declare the shared module and usually I would call these modules um based on what kind of dependencies they actually contain but just to make it clear that we have these different types of modules for compos and Quon multiplatform project I will name this shared module and the other one platform module and of course in a bigger project you would also have more of these modules so typical one is just the app module which contains um Global Singleton app instances but you might also have such modules for specific features maybe for specific data layer of a feature or so so just feel free to use these to organize your dependency management a bit but here let's now think about what types of dependencies we actually have and we can create in our shared code in our case that's actually only our repository because we do have access to our DB client in the shared code we don't know how to create that in the shared code but we do have access to the ins we use so therefore we need to Define that we have an instance of my repository and want to bind a specific implementation of That So Co knows which implementation to inject when we try to inject an object of the type my repository and we do this here in our modules KT file by saying single so we say we want to have a Singleton and here we could then say we have our my repository implementation which requires a DB client of course at this point and if coin already knows how to create this instance that we need here so this DB client what we can then do is we can just call get which is a function only available here in this single block and the moment we call get coin which is actually not strictly a DI framework and doesn't strictly Implement dependency injection but it's actually a so-called Serv locator so you can think of coin just as a big pool of all your dependencies you need in your project and whenever you request a dependency so here by us saying we want to get the specific DB client instance then coin will look in its pool of dependencies where we can find that instance and uh it will simply pass it here in the Constructor so that's probably quite important to know that coin resolve such dependencies at runtime and not at compile time like um other Frameworks like dagger Hill do for example but once we did that right now we of course don't have the um instance of this DB client at least in the sense that Co knows that we will do this in a moment with the platform module but just assuming it knows that here we still need to tell coin that we want to bind a specific abstraction here so whenever you have an abstraction and an implementation but your classes depend on the abstraction you need to also tell coin which abstraction you want to bind so we say that bind and then specify our my repository abstraction or interface in here and that way whenever we try to inject an object of the type my repository coin will find this because we now declare this here in the signal block and it will inject this specific instance we create here and with coin we actually also have a little shortcut fun function so we don't need to use these get functions on our own actually at least as long as the Constructor only consists of instances where coin already knows these so what we can do is we can also say single off and then just say double colon my repository implementation and then we will just say we want to have this my repository implementation here as a dependency it has a Constructor and coin should just take a look at all the dependencies the Constructor needs and try to get these if coin knows these so I hope this was not too confusing um I know that using such a DI framework for the first time can be a little bit confusing um but let's actually move on with our platform module and at the end of this video no worries I will give you a little quick recap in case all that is new to you let's define a platform module platform module which is a coin module however as we've already discussed this platform module now differs depending on the platform as the name say and whenever we have something like that where we want to use a specific object in our shared code but the implementation differs based on the platform we need the expect actual mechanism so we make this an expect Val and the implementations of this platform module we can now put in our actual um platform specific sources so we can again hit Alt Enter at the missing actual declarations and just take all of our resour sets we have here click okay then Android Studio will open these new files so here we have a platform module specifically for Android and in here we have access to all of our Android specific dependencies um so we can again say here we have the actual implementation where we have a module and in this place we now want to create for example our DB client because the DB client needs to be created differently on Android because we need the context there then it needs to be on desktop and iOS devices so what we can do again is we can say single off and we say we have our DB client and put a double colon in before here we don't need to add this bind because we're not binding a specific fig interface here but this is just a normal class another thing we need here in order to also be able to inject our my view model we want to use view model off so if we suddenly want to inject The View model instance K needs to know that with this view model off function or just a view model to declare how creating this view model works because on Android creating a view model actually works differently than it does on desktop and iOS since on Android view models are bound to the life cycle of the current UI components of the current screen user and creating that requires a view model Factory so there's some complexity going on um behind which is why we can't simply create a normal view model instance here with its normal Constructor however still K knows that deals with that but we need to use this special view model off function for view models where we can just say my view model and then putting the double colon in before and this will properly be bound to Android specific screens later on so now we have the Android specific implementation of this platform module let's do this for Native as well so for iOS and Mac devices we also make this a module we also declare our DB client here um actually we first need to import it Alt Enter import that class and then put the double colon in before and make sure it's actually single off and then we don't have any more issues here on the native side if we would create a DB client you can see it doesn't take any parameters so coin will just create normal instance of this object how do we now do it with view models so iOS of course also wants to use this view model but here we don't have this I mean we do have this view model of function I'm actually not sure if we can use this on iOS um because on iOS we don't really need to bind a view model to a life cycle of an iOS component because the OS just works differently I'm actually little bit surprised that this exists maybe uh the team behind coin added this in a recent version let's give this a short and try out but normally I would have said um we can easily make this a single turn because on iOS um The View model will in the end just be a normal instance we create but let's do view model view model off and we declare my view model in that case we can actually also put it in our shared code because uh the implementation will be the same for every single platform so let's say let's say we remove this let's make this an experiment and we put this view model offline in our shared module since it's the same for every single platform and here we import this view model off we need to remove this dependencies and it seems like there is some kind of issue take a look is it because my V model doesn't isn't imported yes let's I'll enter to import this at the Double colon and then we don't have any more issues here in our shared code as well um so we removed this from Android we don't have it in Native and lastly we also need to add this DB client here to our desktop code so in desktop main open up di modules desktop and here we can just paste our mod module implementation again we need to import the dependency first before we can add the double colon and then we should be pretty good here so now that we declared these modules which we can use to tell coin which dependencies we have and how these are created we still have to tell coin because these modules are unused right now in order to do that we can create a coin initializer in our package here in the Shar code so new class this can be a simple function which we call init coin and in here we will have a function in it coin where we pass a config which is a coin app declaration which by default is null and you will see what this is actually for and in here we call start coin which initialize coin here want to say config question mark. invoke with this so with this coin application and after that we want to declare all the modules we already know here in our common code so we say hey we have a list of modules we want to introduce to you dear coin library and this list of modules will on the one hand be our shared module and our platform module and what this Coin App declaration allows us to do is we can in the end just extend the modules with pure platform specific modules we might not have in our shared code um you will see in a moment how this works um I actually got this function from a c lead developer from the coin team so shout out here at this point who uh reach out to me and slack really cool function I didn't know about this but now we have one Central function in our common code which we can call in each entry point of our platform specific code so in the end we just want to call this function exactly once per app launch let's start with let's start with desktop I think that's the most easiest function or the most easiest platform to call this function on the entry point for desktop programs is our main function so here in main KT we have a main function that main function gets called once we run our program and by default def fault it just calls this application where we can have a window with our app specific composable we want to cut this out and actually give this function real bodyy paste this in here and just before we want to initialize coin so we say in it coin and we don't have any special config here for desktop devices U we just want to initialize our shared and platform module with this library and we are completely good next up let's take a look in iOS how that works so here in iOS main or entry point is the main view controller we open this and the way this works is that this composed UI view controller has a function Lambda here we can pass which is called configure which is also called once um per initialization of this controller we only have one of this so we can also initialize our coin library in here we again call init coin there are no special dependencies we want to um introduce to coin we only have on iOS no actually in iOS we just want to have the share dependencies we declared but on Android that's actually not the case so let's see how it works on Android Android is a little bit special in that regard uh if we want to execute code on Android exactly once per app launch we need to create a so-called application class for that so here on Android Main in this uh com Bill coding blah blah blah package where we also have our main activity which is the entry point for Android apps we want to create a my application class this needs to inherit from application and in here we can overwrite the on create function which is called exactly ones who abbl and here we can now initialize coin we could just do it like this but this will lead to an issue why would it lead to an issue well because we have dependencies we need to introduce to a coin we only have on Android what is that well if we take a look in our DB client Android is this context reference that is a dependency we only have on Android so we can't introduce that to coin in our shared code here in the init coin function since we just don't have access to Android specific code here in the shared code in order to fix that we have this Coin App declaration which now allows us to do exactly that so here in where is it let's go back here my application here instead of just um calling this function like that we can open a Lambda block which lets us extend this function with the Android context so that is now that is now a function from coin that is specific to Android app where we can just say okay we introduced this Android context to coin just for this Android platform and here we can pass the application which is the application wide Android context and that way it will also work just fine on Android to construct our DB client instance which requires a context there but no context on the other platforms something we also need to do with these application classes is we need to register these in this Android manifest XML file just something we have to do if we open that we have this application tag and in here we just say name my application and then Android will know that we have a custom application class so uh there was a lot of coin specific and dependency injection specific code one last thing we need to do is we need to set up our UI in order to also inject our view model there and finally call the function that Returns the hello world strain we want to do that in our application KT composable in the common main source set here you will find some basic code want to remove that and first of all in order to be able to resolve and such dependency in our UI code coin wants us to use this coin context composable and everything we put in here um we will be able to inject dependencies if you wanted to inject specific non-vw model dependencies here in your UI code for example your DB client which I wouldn't recommend since it's not UI related but if you wanted to do that you can do this with uh coin inject like this specify the DB client instance I think it's actually equals like this yes so this coin inject here is a composable function you can safely use this in inside of composes and at this point where you call this function coin will look okay do I actually have this DB client instance somewhere do I know how to create that oh yes I actually have this declaration in my module so um in the platform it's actually the platform specific module so on Android if we are on Android then it will see okay here no still wrong this one it will take a look okay I actually have a platform module in this platform module I create this DB client instance so let's take this instance initialize it and just pass it to this DB client where we set that equal to coin inject and that's how this works in this case it doesn't make sense because we don't want to have our DB instance in our UI code however what we definitely do want to have no UI code is the V model instance and for that we need an NA host since VI models are bound to the current screen usually and let's first of all have enough controller we can also initialize this here remember n controller this really doesn't matter here it's really just a demo with one screen to show you how this works in a real world use case let's say we have a start destination of home and then we have one screen in here when we set composable route is also home and in here we can just have our home screen where we say we just make that a box make it fill the whole size of our screen we can Center its content with content alignment alignment center and in here we now want to display a text with the hello world string we get from The View model because if our dependen injection works fine we should be able to inject our view model here in the UI code where is it my view model coin will know okay this view model in order to create that I first of all need to understand how to create a my repository and then it will take a look here okay I have this declaration in my module with this my repository let's create that oh that actually requires a DB client instance so let's again take a look how to create this DB client instance it creates that and it doesn't need any further dependency since this DB client just has an empty Constructor at least on iOS and desktop it will create all these create them in the right order so it can properly inject The View model here injecting the view model works with valid view model is equal to coin view model and here we just declare the type of the view model so my view model in this case we need to opt into some experimental API here so just Alt Enter on this coin view model opt into this and refine and here for the text we can then say view model get Hello World string and that is how dependency injection works with coin and if you've never worked with such a dependency injection framework I know this will feel like completely Overkill and way too much set up just to make it work to pass instances to instances and in this simple example I agree it would be much easier to just pass these dependencies here in the function signature of this app composer just like here without this coin inject but as your project scales it really makes your life easier if you have your central place of dependencies if you don't need to worry about resolving these at the right time in the right order if you can easily swap these out for for your test cases so you use a different set of some dependencies for your test cases and all that is really made easier by using such a DI Library like coin so let's just try this out I've connected my device compose app let's try this out on Android first if we see our Hello wall string everything works because only if we see that that means that coin knows how to create all these different dependencies I will move over my running devices here open that up and that will connect to my device while Gradle is building and then I will see you back once uh the build process actually finished and hopefully shows us a hello well text there we go it's launching and it displays hello world just to maybe show you that this would break if coin doesn't know a specific dependency of us let's go to modules let's actually I don't know let's just comment out this single off here so coin doesn't know how to create our my repository instance if we now relaunch this then you will see our app crashes we can take a look here in lock CAD to see the actual error and it will give you this typical no beam de found exception um because coin did not find a definition for interface my repository so we need to check our modules configuration and add this missing type if you're working with coin you will see this um issue quite often sometimes just because you forgot or usually that's the reason but if you get this error it means coin doesn't know how to create a specific type of object in your code if we uncomment this again then it'll work just fine let's also run this on iOS by selecting IOS app here launching this on our simulator there we go the simulator is booting up and after it's loading we should be able to see a new app here which it launches and it should display our hello world string as well there we go hello world that's also working on iOS lastly let's check that on desk Des toop so we open our project Hier key and I hope you've been seeing the previous videos in this playlist we first of all need to go to main KT click this run button run this it will give you an error very likely but now that you have this main KT up here you can open your terminal and run this Gradle W run command hit enter and then your desktop program will compile and hopefully run there we go so my different monitor about here we're also seeing our hello world string which originally comes from our repository by the way so in case you forgot this is the place where we create that string and we pass it all the way to our UI just to show you a real dependency injection setup and I think you now really learned at least 95% of what you need um in in the real world by using coin in a real app so you've learned about how you can inject and declare dependencies in your shared code like here with the shared module you've learned how you can provide or introduce dependencies to coin that are platform specific with a platform specific module you've learned how you can even introduce dependencies to coin which you have only on specific platforms and not on all of them which you have seen here in modules now actually in the application class where we extended that with Android context but you could have of course also just extended that with even more modules you only have on Android so I think that really covers it all there are of course a few more functions keywords that coin offers which you don't need too often and if you need them you will find them quickly in The Docks but now you really have very solid foundation of what the peny injection is and how we can make that work using coin and cotlet and composed multiplatform down below you will find a link to my C multiplatform Advanced course in which you will really build a fully fledged app so if that sounds cool to you check that out and other than that thanks so much for watching this video I will see you back in the next one have an amazing rest of your week bye-bye [Music]
Info
Channel: Philipp Lackner
Views: 6,824
Rating: undefined out of 5
Keywords:
Id: TAKZy3uQTdE
Channel Id: undefined
Length: 35min 5sec (2105 seconds)
Published: Sun Jun 30 2024
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.