Shared Navigation on Kotlin Multiplatform with Decompose (KMP)

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
hey guys and welcome back to a new video in this video I will share how you can Implement navigation on a multi-platform project with the decomposed Library so I'm pretty sure many of you are now aware that there is something called compos multiplatform which we can use to build IOS and Android apps together with composed UI however normally navigation Works quite differently on iOS than it works on Android and now we have a decomposed Library which is there for a longer time than there is composed multiplatform which we can now used to have the navigation logic itself in our shared code in a Caron multiplatform project and then we can take that shared Logic for navigation and use it in our native section with a minimal amount of code so in particular what we'll build is this little app here as I had it uses compos multi platforms so it's only one UI and we're going to have a screen a with a text field and a screen B so we can enter something like Hello World in that text field and that will then be the navigation parameter for the next screen so if you click next screen you can see there's also a screen transition which I will show you here it displays that parameter value and then we can click go back to pop the backstack get back to the previous screen we can also see that the state is properly restored also if there are screen rotations you can see everything is properly retained as we are used to with view models in Native Android and the same obviously works on iOS so if we enter test here go to the next screen there is just a similar screen animation you can see we see our test string here if we click go back we get get to the previous screen we can then enter something else and we again get to this screen in this video we don't have to write a single line of iOS code it's really all cotland 100% including all that navigation logic and you will learn how that works and before we start let's first of all understand what decompose really is so decompose is just a cotton multiplatform Library as you can see I really recommend the docks here because this video will be an introduction to navigation so how we can use parameters how you first of all navigate how you can go back how you can pop the back STI these kinds of things how we can retain the state a navigation itself is quite complex you can dive much deeper into that with a deep linking with uh Pro death restoration and all that kind of stuff but that is all supported by decomposed so if you want to really build a real app with that then take a look at these docks they are quite good I personally found it hard to get into this decompos logic when I first heard of this but I think after this video it will be quite clear to you and then it will also be much easier for you to resume and dive deeper into this topic the idea of decompose is that we break down our code into life cycle aware business logic components so in the end that is exactly what we are used to on Android if we have an activity that is a live cycle aware component because we have some kind of start of the live cycle which is on Create and we have an end of the live cycle which is on Destroy so all in all we have some kind of lifetime of our activity another life cycle component would be a view Model A View model would have a different life cycle than our activity because that is exactly the purpose of them view models live longer than activities so we put our state in these view models rather so they are not destroyed when the activity is destroyed for example with a configuration change and what decompos now allows us to do is to Define our own life cycle aare components like our own view model but all that in the shared code of a cotton multiplatform project so it's really pure cotton code and to get started with that I would like to ask you to go to this website which I will put in this video description of course so km. Jet brains.com and here you will get this cotton multiplatform wizard which you can use to create a very empty jetp compos multiplatform project because there's quite some setup involved and this tool just creates that initially for you so sets up all the dependencies and all that kind of stuff make sure to choose some kind of product name a fitting package name then you want to select Android and iOS you want to share the UI with composed multiplatform and no we don't want to have a desktop app and a server app we also don't need so then you can click on download it will download a zip file extract that zip file and then you can just open that um extracted folder directly in Android Studio then when you open this it will look somehow like this so there will be a read me file and other than that we have our very normal cotland multiplatform structure so we want to select the project view so we just get a little bit better overview here and that is a little bit different at least that changed um that we now only have one compose app module and that is then structured into Android main for our Android app part common main for the shared code and iOS main for the iOS specific code the first thing we want to do here is want to open the build- at grad file of our shared module and we want to add the dependencies we need in order to use the decomposed Library so down here we can find the Android specific dependencies and the shared dependencies and you can also see that nowadays coton multiplatform uses version catalogs by default so we have one central place of managing our versions that is also where we want to declare our remaining versions We want to find that in our Gradle folder uh lips. versions. tumml and here at this point I really just recommend you to take a look in this video description copy the or go to my GitHub repository and then copy this file and paste it so you have the versions I will just copy these over from my prepared project so I will just paste them all here if we take a look what I just added here on the one hand um decompose as you can see I recommend you to use at least this version 2.2.0 Alpha because in Alpha 01 and Alpha 03 there were some changes and I did not want uh that this new changes once they become stable will make this video outdated that is why I really want to stick to this Alpha version so that is the decomposed Library as I said just helps us to build these live cyle aware components then we have some extensions for composed jet brains um that is quite a strange version name um it's currently experimental of course just as OS multiplatform is but that allows us to now take these live cycle errare components with our cotland specific navigation and routing logic and then apply that routing logic in our compos UI we have cotland a serialization Json so that we can just serialize classes using cotlin and I think last but not least we have the cotlin serialization Cradle plug-in down here and we can then go to our build. Gradle file again and apply these versions mostly in our common Main Source set so here we want to have our implementation on the one hand lips. decompose um lips. decompose no oh we need to synchronize first of course lips. decompose no something failed okay I had to remove this implementation line in order to sync but now we should be able to add this back so ellip that decompose um yeah just like that and then we want to have lips that decompose that jet brains as well for the compos multi platform specific decomposed logic and then last but not least we want to have cotlon civilization so lips. cotlon X cin x. serialization Json and we also want to take this decomposed library and applied in our Android specific code and last but not least scroll up and apply the calization grd plugin we do this with this Alias block lips plugins. cutland serialization that is how we can use version cataloges to apply all these dependencies can then click sync now to apply these changes in our app and then we are ready to start defining our life cycle Weare components and we want to do that in our common main source of course because that is now really our shared code which we can share between all Platforms in this case Android and iOS but of course of course if you're also targeting something like desktop or later even web then you can have one central place where you define the same navigation logic and reuse that on all platforms without redefining it so here in the codon package we want to create a new directory called navigation or navigation specific classes and in there I want to create a new class called root component this is really the core of the decomposed library that we have some kind of root component and when I say component I really mean such a life cycle aware component now the root component will just live as long as the application does in this case because it's really meant to be like that and it will host all other components but what could also be a component here for example a single screen a single screen obviously also contains some kind of life cycle because when the user is on that screen the life cycle is active but when they then maybe pop the backstack go back to the previous screen that screen is not active anymore therefore we will also build such subcomponents which Cor respond to to a single screen so we can also Define the navigation logic between these components first of all what we need in such a root component is the so-called component context you going think of this like the context on Android so it just gives us some extra information about the current life cycle for example but also helps us to implement a behavior like for navigation for example and to really get that behavior I want to use the component context interface Implement that and delegate all that add to our component context that we passed Here and Now what this component context provides us here is we can have a navigation and that navigation is a simple stack navigation so stack navigation is what we used to on Android we have a stack when we go to the next screen that lands on top of our stack we then go to the previous screen so we click back for example then we pop that top most screen from the back stack and we get to the previous screen decompos also offers different mechanis Ms for navigation apart from stack navigation but since yeah that is what we typically use on Android I want to stick to that in this video so to this stack navigation and what the stack navigation here wants from us is a so-called configuration what is a configuration well that is something we have to Define ourselves a configuration in decompose really refers to the config behind such a component so the config behind such a screen for example what could that be that could be for example the set of parameters so screen or navigation arguments that we pass to that screen so let me just type this off and then I think it will get clear if we have a sealed class here called configuration and we include all of our screens that we want to have in our app here in this configuration seal class on the one hand for example we have a data object screen a which is a configuration and then we have a data class screen b a data class because our screen B requires a parameter if we take a look back in our app then you can see this is the string that we want to pass from screen a to screen B therefore we want to add that to our configuration of screen B Val text which is the text we pass and that is now a configuration as well so therefore we can still have one screen which is screen B which always looks the same with different configurations because we could on the one hand open screen B with a text hello world but then we could also have another open instance of screen B with a text test for example and therefore these two same screens which are both screen B have different configurations which in this case refer to the uh navigation arguments we pass and this configuration needs to be passed to this stack navigation here here it's important that this configuration is actually serializable which is why we need to add this coton X calization Library because obviously when we navigate from screen one to screen two then the library somehow needs to serialize the data we have here on Android it will probably um yeah just pass it as adjacent string because that is what we added c x calization adjon but all that will be handled by the library so we also want to add this in this annotation here and here and then we are good another SE class we need to Define here is called child in the context of decompose a child is really just a screen so here in this SE class we Define all the screens our app has on the one hand we have screen a which is our initial screen and this now needs the context the component context of our screen a so now we need to have another sub component so just like this root component which is used for our screen a which then again has a life cycle for as long as screen a is active and on the backstack so let's just Define such an empty screen a component for now we will extend that code a little bit later in this video in this navigation folder new class and we call this screen a component so this is now a subcomponent of our root component but the same as our root component this screen a component also has a component context and we also want to make this a component context by component context this way we can really structure our apps navigation in form of a graph um which is typically also referred to a nav graph so if we take a look here um in under component you can see here we can uh form such a hierarchy for example here under plug UI hierarchy we have our UI this would be for example our root nav graph or main nav graph which connects all other nav graphs then here we would have nav graph for feature one you would have the first screen of feature one the second screen of feature one you would have the navra for feature 2 um first screen of feature 2 and so on so that is really how we can form the same kind of structure for our screen navigation as we used to on Android I want to do the same for screen B so screen a component copy paste call that screen B component and here one change I want to make is I want to have Val text since this screen be component requires a text um field a text parameter which we would like to access here in our component and these single screen components can now really be thought of view models for these particular screens so with the setup that we will use for this uh the the state that we're definining this components will also be retained on screen rotations so if you stick to what I show you here you really don't need View models anymore because these components would be the replacement for that because in the end a view model is just a life Cy R component and whether we call that view model or component in our Shar code section that doesn't matter but for now let's switch back to the root component we want to Define our different uh screens which are called children so single child and here this first screen a now takes a reference to this screen a component oh let's just call it component and that is of type screen a component and that is of course a child and we also want to have this screen B where we just replace this with screen B component okay what is next next up we want to define a little function which creates our um child from a given configuration so we just check okay is the configuration screen a we want to create a screen a reference is it screen B we create the screen B reference so let's call this create child it will take the config on the one hand and it will take the context and it will then return a child right here so we can say return When the config is I enter to add the remaining branches if the config is um whoops that looks weird the config is configuration screen a then we want to return child. screen a and here we now need to create this screen EG component we can just do this with a normal Constructor uh I don't know why it always prepends navigation um so screen a component and the component context of this is just the context we pass to this function and the same should happen for screen B so here we have screen B component this time we also need the uh text attribute which we can get from the config in this case so config do text and also make sure that this is screen B and then we don't have any more errors the last thing we need to create is the so-called child stack so that is really where the magic happens that is the internal logic of decompose which manages the navigation and the navigation stack so we have Val CH stack is equal to um a child stack which we can create like this a little bit complex to create that on the one hand we need a source which is just the navigation we have above then we need a serializer which uh defines for the CH dech how to serialize the configuration from here thanks to codon serialization we can just pass configuration. serializer we need an initial configuration so the initial screen the first screen that shows up that is just our screen a next up we can say handle back button set that to true so it automatically handles back clicks and pops the back stack in that case and last but not least we have a child Factory which is yeah just a factory function which we've created before which creates the children based on the config that this child Factory passes so just create child and if you take a look at what this child dech really is you can see that is just a value what is a value a value in decompose is really um just the observable State type of decompose so it's really the equivalent to Jetpack comp poos dat the equivalent to a state flow and they decided to use a custom class for that to to keep it open to you what you want to convert that to so whether you want to use RX Java whether you want to use State flow compos state that is something you can choose um thanks to them making that a value and their own custom class but let's now take a look at how we need to manipulate and change the single subcomponent so screen a component and Screen B component I've already said that we can kind of treat them as view models now that means we can include our normal screen State here in these components so in this case the text so private bar let's have underscore text this case it's again a mutable uh mutable value here you could also use State Flor or so since you are in the shared code section of cin but I would like to use the mutable value so the um observable data type of the decompos library and initially we have an empty string we now want to say we have a Val text without an underscore the immutable version um which is not a state but rather a value of type string and we then set that equal to the text so we only expose the immutable variant of that State field and then what we commonly do is we have some kind of Steel interface which defines all the user actions a user could potentially perform on the screen so clicking a button entering something in a textt field um and I would also like to have this kind of structure for our screen components so let's go to navigation Define screen a event let's call it like that make that a sealed interface open this block of code and here we want to have our two events so basically um what a user can do on the first screen that is on the one hand to enter something in the text field and on the other hand it is to click on the button to get to the next screen on the one hand data object um let's call it click button a or just yeah let's let's call it click button a screen a event and we have a data class called update text with the new text that we pass here so these are the events we now or we will later send from the UI itself screen a event and then we want to go to our screen a component again have an on event function which takes these events so screen a event and here we have a when block we can just check what kind of event that is I'll enter to add the remaining branches if we click button a well what do we want to do in that case in that case we want to navigate to the next screen so how do we do that here we don't really want to do that here in this component we want to keep it more flexible and more plugable so we want to rather expose a Lambda whoops what am I doing here um want to rather expose a Lambda something like private Val on navigate to screen B which is a Lambda that takes in the string so the value you want to pass to the next screen and doesn't return anything so if we have that we can now call or trigger this Lambda here and the string want to pass is just our text. value so the actual string that is currently in our text field if want to update the text however we can simply say text. value is event. text so how do we now perform the navigation because that is really what this library is about let's find out since we of course expose this Lambda to the root component since that is where we create the screen component let's go in there you can see we now get an error in here and we can resolve that on the one hand let's have a name parameter here for the component context to make it a bit more clear and on the other hand we now have our Lambda on navigate to screen B and here in this root component we now have access to this navigation and that is how we can also perform this navigation so we get the string here we get the text and we can now say navigation. push and that will push a new screen a new configuration here on top of our backstack I'd like to use push new that is a new function of decompose um so sometimes it happens if you quickly um press a button before the navigation happened then two screens will be pushed to the backstack and this push new function will just make sure if you are already at that screen where you want to navigate to then nothing will happen so push new we then want to add the configuration which you want to push that is screen actually configuration do screen B want to go to screen B and the text is just our text we also need to add this experimental decompose API so I'll enter opt into that and that is how we navigate so you can see all the navigation logic now is is not defined in the UI itself but rather in these pure cutl classes and the same we canot do in screen B component because what we want to be able to do here is we want to be able to go back if we take a look in our app here this is the go back button which will simply pop our backtack and uh lead us to the previous screen we can do this the same way by just extending the Constructor with Lambda private Val um on go back for example and then we have function here go back we don't need the uh events here so just like we did here you could of course also do this for the screen B but we only have one single action in this simple example so we just want to trigger our Lambda in this single function and then we can go to root component again because we again get an issue here on the one hand we want to Define our text then we want to Define our component context and last but not least we want to Define our ongo back Lambda in which we want to Simply refer to navigation and say pop and that will just pop the backstack as we're used to on Android and that is all everything for this shared navigation logic so the cool thing about this is of course that we have all this logic now in pure cotton code that means we can run and test this logic with a local unit test we don't need to spin up some some UI devices in order to UI test the navigation logic and we can also test this at a single place so if we target four platforms we only need one test which tests them all if you want to be more flexible with testing you can also make these components in interface and that is also what you will see in the documentation of decompose so you can swap these out out with um test doubles for example for testing but for the sake of Simplicity um I decided to not do this for this video so let's next go to codin main to the shared code and Define our single screens so the actual UI for that I want to make a new folder for that called screens and I want to have a screen a simple file just a normal composable called screen a and the screen a will now just get our component the um component which is the screen a component and that now contains the whole state for component a so this text field in this case and it also allows us to send events to that component so it's really just like a view model okay so on the one hand we want to get our state which is a vext by component. text and there's already an extension to subscribe to the value as a compost State I'll enter to import that um change to Val yes that needs to be a Val and then we get this text as a composed state so we can easily observe that then let's just have a simple column so we're first building our screen a here with a text field and the button modifier modifier. fil Max size let's say I want to Center everything horizontally so alignment. Center horizontally and vertical Arrangement is arrangement. Center then we want to have a little text that says we are on screen a we want to have our text field let's make it an outline text field the value is just our text on value change so when the text changes we now want to use our component and send that new text to our component so it can update the state um this is update text and we send a new text and we want to have a modifier here which is modifier. filx width and let's add a little bit of padding um padding of 16p and Alt Enter to import that DP value and last but not least let's have a little button to go to the next screen on click in this case we again want to trigger an event of our component so component. onevent screen a event do click button a because that is really what happens when we click on that button the text of the button can be go to screen B and that should already be the UI for our screen a let's duplicate that screen so we can also have that for screen B rename this to screen B this to screen B component of course I'll enter to import that um here we don't have a state so we can remove this line we want to change this to screen B here we don't have an outline text field but rather just a button to go back so go back and when we click this button we say component. go back cool in screen a we still have an issue um what is it okay it seems like it's just a bug um let's go to app so that is really the the root composable of our composed multiplatform project and here we now want to really use the screen a and Screen B together with the navigation logic with' Define in our root component so let's first of all remove everything that we have here in our material theme block and what really happens here is we get our child stack so what we Define in our root component which really contains our navigation stack and we observe that as a normal compos state so by the root component that our app gets by root. child stack. subscribe as state so that way we just get that child stack and now decompose adds a composable for us which is called children which takes this child stack so here we can pass this and this children composable will handle all the navigation Logic for us so it will now take the logic with thein in the root component and just make sure that the right screen is shown and this is now completely independent of the normal composed navigation way because under the hood this probably just uses some kind of animated content block and then swaps out the composes depending on um the the most up to-date or the most recent screen that's also why we can Define any TP of Animation here so we can have an animation which is um actually just a stack animation so there are some predefined animations and here we could pass something like slide so we have a normal slide animation you can also um create your custom animations here but let's keep it simple again here in this blog we then get the current child so the screen we're at and then we can check what that is to lead the user to the correct composable Alt Enter um actually wildchild do instance that refers to the seal class we've created and then we can hit Alt Enter to add the remaining branches if we're on screen a want to create the screen a composable with child. instance. component and let's actually also have Val instance here and then refer to that instance here so to the component of the instance and then we also do the same for screen B here we then pass instance. component again and that is how that works that is everything we need to do for the shared code section the only little change we need to make for Android is we need to go to Android main cotlin come open main activity and here we of course need to pass the uh root component in Android we need to create this with um Val root is equal to retained component that is important now retained will make sure that the state will also survive screen rotations for that it's important that you don't pass some kind of activity reference or um the fragment reference to your components because then you would cause a memory leak uh but that is also what I yeah what you can do with a normal Android view model so um the same rules apply here for decompose uh don't pass activity context to your components don't pass fragment context and then you're good so in here we can then just have our root component root component and we pass it so the component context this block gives us and then we can pass our root component down here in our app composable let's remove the preview and the same works on iOS so in iOS main main view controller this is the the main entry point for the iOS compos code here we just want to Define um our root as well set that equal to remember and just create the root component manually here with a default component context and this needs a live cycle which we can just uh create with live CLE registry so we just create the default um lifetime for this component we can then pass it here and that's it I would say let's try this out and run this app on our Android device first it should just look like this after it was launched there we go um by default it uses material 2 um but you can just switch that to material 3 if you want in the dependencies Let's ignore that for now um and type something here test rotate if that survives screen rotations yes it does rotate back go to screen B There is our transition to screen B um we don't display that value yet I'm just noticing but that is very straightforward here in our screen be composable we would just need to pass the text here as a parameter and then add the value here and when we then call this screen B here in our app we first of all say um instance. component. text so we pass the text to our screen B if we relaunch this we tap something here go to screen B you can see now the navigation argument is properly passed if we go back we get to the previous screen if we navigate back we also get to the previous screen and we have a normal um backstack Behavior this is the previous app so this isn't isn't a bug how do we try this on iOS well for that we want to go to IOS app this folder here open this and open this excode project and then open this workspace file in a code so right click open in a code then this window will open up we want to open our IOS app I think no actually the content View and since we created the product with the um compose multiplatform wizard all that code is already set up that we need to make this run on iOS um no such module compos app is pretty normal that it shows that initially we can try to launch it and maybe after build that works if it fails we might need to rebuild our um composed multiplatform app in Android Studio but let's try that first of all check our simulator and it seems like everything is running fine so we do see our app here we can type something go to screen B navigation is working perfectly fine going back also works super cool I especially love how easy it is nowadays to um just make your application run on iOS you can see um we don't need to add any Swift Code it's all set up and all the rest of the code is defined in the shared code section of our Android Studio project so I hope you enjoyed this video if you did then you will definitely also enjoy my more advanced Android premium courses which you can all find by clicking the first link down below so they will just contain details and topics that I never covered on YouTube before that are still super relevant for the indust IND so if you want to get ready for the industry as an an developer check them out and other than that thanks for watching I will see you back in the next video have an amazing rest of your week [Music] bye-bye
Info
Channel: Philipp Lackner
Views: 15,475
Rating: undefined out of 5
Keywords:
Id: g4XSWQ7QT8g
Channel Id: undefined
Length: 34min 22sec (2062 seconds)
Published: Thu Nov 30 2023
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.