{"id":149,"date":"2024-12-02T23:08:22","date_gmt":"2024-12-02T23:08:22","guid":{"rendered":"https:\/\/thecodepredator.com\/?p=149"},"modified":"2024-12-02T23:13:30","modified_gmt":"2024-12-02T23:13:30","slug":"navigation-system-with-kmm-part-2","status":"publish","type":"post","link":"https:\/\/thecodepredator.com\/index.php\/2024\/12\/02\/navigation-system-with-kmm-part-2\/","title":{"rendered":"Navigation System with KMM (Part 2)"},"content":{"rendered":"\n<p>In the <a href=\"https:\/\/thecodepredator.com\/index.php\/2024\/07\/10\/navigation-system-with-kmm-part-1\/\" data-type=\"post\" data-id=\"116\">previous article<\/a> of this series about my KMM Navigation system I talked about the Android part mainly, and I showed a little bit of the iOS side.<\/p>\n\n\n\n<p>I talked about NavInstance and NavBuilder, two helper classes to manage the navigation stack on the iOS side of the app. Now I&#8217;m going to link all pieces together to get a global vision of how everything works<\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre class=\"CodeMirror\" data-setting=\"{&quot;showPanel&quot;:true,&quot;languageLabel&quot;:&quot;language&quot;,&quot;fullScreenButton&quot;:true,&quot;copyButton&quot;:true,&quot;mode&quot;:&quot;swift&quot;,&quot;mime&quot;:&quot;text\/x-swift&quot;,&quot;theme&quot;:&quot;material&quot;,&quot;lineNumbers&quot;:false,&quot;styleActiveLine&quot;:false,&quot;lineWrapping&quot;:false,&quot;readOnly&quot;:true,&quot;fileName&quot;:&quot;Swift&quot;,&quot;language&quot;:&quot;Swift&quot;,&quot;maxHeight&quot;:&quot;400px&quot;,&quot;modeName&quot;:&quot;swift&quot;}\">import SwiftUI\nimport shared\n\nstruct KMMNavigationView: View {\n    \n    let associations: [String : NavBuilder]\n    @ObservedObject var navigationBridge : NavigationBridge\n    @State var navigator: INavigator\n    \n    init(associations: [String : NavBuilder]){\n        let navigator = DIPresentationHelper.shared.getNavigator()\n        \n        self.navigator = navigator\n        self.associations = associations\n        self.navigationBridge = NavigationBridge(associations: associations, navigator: navigator)\n        navigator.navigatorBridge = self.navigationBridge\n    }\n    \n    var body: some View {\n        NavigationStack(path: self.navigationBridge.navPath) {\n            getRootView()\n                .navigationDestination(\n                    for: NavInstance.self,\n                    destination: { instance in AnyView(\n                            instance.viewBuilder(instance.viewModel)\n                        )\n                    }\n                ).navigationViewStyle(.stack)\n        }\n    }\n    \n    @ViewBuilder\n    func getRootView() -&gt; some View {\n        let instance = navigationBridge.navInstances.first(where: { $0.request == navigationBridge.rootRoute })\n        AnyView(instance!.viewBuilder(instance!.viewModel))\n    }\n}<\/pre><\/div>\n\n\n\n<p>This is the place where everything gathers and starts to make sense. Let&#8217;s dive in the code:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>There are 3 field here: the navigation bridge, the navigator and a map of associations.<\/li>\n\n\n\n<li>This is a view so it has a body, which is a NavigationStack which has a list of NavInstance as parameter.<\/li>\n\n\n\n<li>That NavigationStack has a getRootView viewBuilder as child. That method &#8220;renders&#8221; the root view, the view which we have selected as initial one.<\/li>\n\n\n\n<li>Finally, we have two modifiers applied here (sorry if that has a different name on iOS, I use Android way to name things :P). The first one is .navigationDestination, which links the NavInstances created by the NavigationBridge and stored in the navigationPath with their corresponding views. The last one is to set that our navigation behavior is a stack (last in, first out). <\/li>\n<\/ul>\n\n\n\n<p>Ok, but how do I set up that associations?, let&#8217;s check it:<\/p>\n\n\n\n<p><\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre class=\"CodeMirror\" data-setting=\"{&quot;showPanel&quot;:true,&quot;languageLabel&quot;:&quot;language&quot;,&quot;fullScreenButton&quot;:true,&quot;copyButton&quot;:true,&quot;mode&quot;:&quot;swift&quot;,&quot;mime&quot;:&quot;text\/x-swift&quot;,&quot;theme&quot;:&quot;material&quot;,&quot;lineNumbers&quot;:false,&quot;styleActiveLine&quot;:false,&quot;lineWrapping&quot;:false,&quot;readOnly&quot;:true,&quot;fileName&quot;:&quot;Swift&quot;,&quot;language&quot;:&quot;Swift&quot;,&quot;maxHeight&quot;:&quot;400px&quot;,&quot;modeName&quot;:&quot;swift&quot;}\">import SwiftUI\nimport shared\nimport KMPObservableViewModelCore\n\nstruct ContentView: View {\t\n\n\tvar body: some View {      \n        \n        KMMNavigationView(\n            associations: [\n                MainScreenNavigationRequest.shared.route : NavBuilder({ request in DIPresentationHelper.shared.getMainViewModel(request: request) }, { viewModel in MainScreenView(viewModel: viewModel as! MainViewModel) }),\n                Test1ScreenNavigationRequest.shared.route : NavBuilder({ request in DIPresentationHelper.shared.getMainViewModel(request: request) }, { viewModel in Test1ScreenView(viewModel: viewModel as! MainViewModel) }),\n                Test2ScreenNavigationRequest.shared.route : NavBuilder({ request in DIPresentationHelper.shared.getMainViewModel(request: request) }, { viewModel in Test2ScreenView(viewModel: viewModel as! MainViewModel) })\n            ]\n        )\n\t}\n}<\/pre><\/div>\n\n\n\n<p>Here you have a map which links a route with a NavBuilder. NavBuilder is a container which has all needed information to create a NavInstance (how to create or obtain the viewModel and how to build the UI).<\/p>\n\n\n\n<p>DIPresentationHelper is a helper class of the KMM side to be able to get instances of classes created by the dependency injection system (Koin, in this case).<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How everything works?<\/h3>\n\n\n\n<p>Everything is kind of abstract or complex at this point, so I&#8217;m going to explain step by step how everything works:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Let&#8217;s start from the end: ContentView. Here we set up the associations route&lt;-&gt;views (check NavBuilder class in the previous article of this series)<\/li>\n\n\n\n<li>Now we have the associations!. When we start the app, we will only have one NavInstance in our stack, the root one. If you look in NavigationBridge yo will see that it is defined in let rootRoute = NavigationRequestsKt.rootRequest. I will dive in NavigationRequests in the next section.<\/li>\n\n\n\n<li>Ok, now, we navigate from root to another screen. Let&#8217;s assume that we press a button to navigate. In that moment, navigateTo method of NavigationBridge is called, and getOrCreateNavInstance is also called. If the NavInstance associated with the provided navigation request exists, we will send the existing one, otherwise, we will create a new one using the provided associations in the first step (remember, NavBuilder -&gt; NavInstance)<\/li>\n\n\n\n<li>When the new NavInstance is created, we add it to navPathStack (which is a @Published var, and the whole Navigator is an ObservableObject).<\/li>\n\n\n\n<li>Since we have updated navPathStack, the UI is notified about it because we marked navigationBridge as @ObservedObject in KMMNavigationView. In this moment, navPathStack has one more element, and we have configured the navigation as a &#8216;stack&#8217; so with the new NavInstance previously created, we build the new view on top of the previous one and that&#8217;s it!<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Navigation requests<\/h3>\n\n\n\n<p>A navigation request is a data model which has:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>A route. For example \/home\/screen1<\/li>\n\n\n\n<li>A list of arguments to be consumed by the destination. For example, an id to be requested to a backend.<\/li>\n\n\n\n<li>A continuation to be used when the intent is to navigate for a result. For example, to pick an element from a list and add it to a form.<\/li>\n<\/ul>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre class=\"CodeMirror\" data-setting=\"{&quot;showPanel&quot;:true,&quot;languageLabel&quot;:&quot;language&quot;,&quot;fullScreenButton&quot;:true,&quot;copyButton&quot;:true,&quot;mode&quot;:&quot;clike&quot;,&quot;mime&quot;:&quot;text\/x-kotlin&quot;,&quot;theme&quot;:&quot;material&quot;,&quot;lineNumbers&quot;:false,&quot;styleActiveLine&quot;:false,&quot;lineWrapping&quot;:false,&quot;readOnly&quot;:true,&quot;fileName&quot;:&quot;Kotlin&quot;,&quot;language&quot;:&quot;Kotlin&quot;,&quot;maxHeight&quot;:&quot;400px&quot;,&quot;modeName&quot;:&quot;kotlin&quot;}\">open class NavigationRequest&lt;ARGS&gt;(\n    val route: String,\n    val arguments: ARGS? = null,\n    var continuation: Continuation&lt;Any?&gt;? = null\n) {\n    override fun equals(other: Any?): Boolean {\n        if (this === other) return true\n        if (other == null || this::class != other::class) return false\n\n        other as NavigationRequest&lt;*&gt;\n\n        if (route != other.route) return false\n        if (arguments != other.arguments) return false\n        if (continuation != other.continuation) return false\n\n        return true\n    }\n\n    override fun hashCode(): Int {\n        var result = route.hashCode()\n        result = 31 * result + (arguments?.hashCode() ?: 0)\n        result = 31 * result + (continuation?.hashCode() ?: 0)\n        return result\n    }\n}<\/pre><\/div>\n\n\n\n<p>But how all this is used? Ok, do you remember the INavigator interface described in the previous article? Let&#8217;s check a &#8216;real&#8217; implementation of it: the Navigator class.<\/p>\n\n\n\n<p><\/p>\n\n\n\n<div class=\"wp-block-codemirror-blocks-code-block code-block\"><pre class=\"CodeMirror\" data-setting=\"{&quot;showPanel&quot;:true,&quot;languageLabel&quot;:&quot;language&quot;,&quot;fullScreenButton&quot;:true,&quot;copyButton&quot;:true,&quot;mode&quot;:&quot;clike&quot;,&quot;mime&quot;:&quot;text\/x-kotlin&quot;,&quot;theme&quot;:&quot;material&quot;,&quot;lineNumbers&quot;:false,&quot;styleActiveLine&quot;:false,&quot;lineWrapping&quot;:false,&quot;readOnly&quot;:true,&quot;fileName&quot;:&quot;Kotlin&quot;,&quot;language&quot;:&quot;Kotlin&quot;,&quot;maxHeight&quot;:&quot;400px&quot;,&quot;modeName&quot;:&quot;kotlin&quot;}\">package com.codepredator.navigation\n\nimport com.codepredator.navigation.models.NavigationRequest\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport kotlin.coroutines.Continuation\nimport kotlin.coroutines.resume\nimport kotlin.coroutines.suspendCoroutine\n\nclass Navigator(override val initialRoute: NavigationRequest&lt;*&gt;) : INavigator {\n\n    override lateinit var navigatorBridge: INavigatorBridge\n\n    override var currentRequest : NavigationRequest&lt;*&gt;? = null\n    override var stack = mutableListOf(initialRoute)\n    \n    override suspend fun navigateTo(request: NavigationRequest&lt;*&gt;){\n        stack.add(request)\n\n        navigatorBridge.navigateTo(request)\n    }\n\n    override suspend fun &lt;T&gt; navigateToForResult(request: NavigationRequest&lt;*&gt;) : T {\n        return withContext(Dispatchers.Main){\n            suspendCoroutine {\n                if (navigatorBridge.navigateTo(request)) {\n                    request.continuation = it as Continuation&lt;Any?&gt;?\n\n                    currentRequest = request\n\n                    stack.add(request)\n                }\n            }\n        }\n    }\n\n    override fun popBack(result: Any?) : Boolean {\n        val success = navigatorBridge.popBack()\n\n        if (success) {\n            stack.remove(currentRequest)\n            currentRequest?.continuation?.resume(result)\n        }\n\n        return success\n    }\n\n    override suspend fun popUpToRoot() = withContext(Dispatchers.Main) {\n        val success = navigatorBridge.popUpToRoot()\n\n        if (success){\n            while (stack.size &gt; 1){\n                val request = stack.last()\n                request.continuation?.resume(null)\n                stack = stack.dropLast(1).toMutableList()\n                currentRequest = stack.last()\n            }\n        }\n\n        success\n    }\n\n\n}<\/pre><\/div>\n\n\n\n<p>This class is the way to connect the shared login written in KMM with each platform navigation bridge. Let&#8217;s analyze it method by method:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>navigateTo: it adds the request to the stack and send it to the bridge.<\/li>\n\n\n\n<li>navigateToForResult: same as previous one but populating the &#8216;continuation&#8217; parameter of the request object.<\/li>\n\n\n\n<li>popBack: it sends the popBack request to the navigator and ,if the popBack is possible, the request is removed from the stack and the result (if any) is sent back.<\/li>\n\n\n\n<li>popUpToRoot: it is similar to the previous one but it &#8216;consumes&#8217; all requests except the first one (the initial route).<\/li>\n<\/ul>\n\n\n\n<p>And that&#8217;s it. I know this is a pretty complex topic and these two articles are not very comprehensive, but making step by step guides is not my plan now. I want to publish my navigation library on GitHub and Maven. I&#8217;ll keep you posted!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>In the previous article of this series about my KMM Navigation system I talked about the Android part mainly, and I showed a little bit of the iOS side. I talked about NavInstance and NavBuilder, two helper classes to manage the navigation stack on the iOS side of the app. Now I&#8217;m going to link [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":126,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"iawp_total_views":11,"footnotes":""},"categories":[9,10,8,7,1],"tags":[15,16,13,12],"class_list":["post-149","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-android","category-ios","category-kotlin","category-kotlin-multiplatform-mobile-kmm","category-sin-categoria","tag-android","tag-ios","tag-kmm","tag-kotlin"],"_links":{"self":[{"href":"https:\/\/thecodepredator.com\/index.php\/wp-json\/wp\/v2\/posts\/149","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/thecodepredator.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/thecodepredator.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/thecodepredator.com\/index.php\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/thecodepredator.com\/index.php\/wp-json\/wp\/v2\/comments?post=149"}],"version-history":[{"count":4,"href":"https:\/\/thecodepredator.com\/index.php\/wp-json\/wp\/v2\/posts\/149\/revisions"}],"predecessor-version":[{"id":158,"href":"https:\/\/thecodepredator.com\/index.php\/wp-json\/wp\/v2\/posts\/149\/revisions\/158"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/thecodepredator.com\/index.php\/wp-json\/wp\/v2\/media\/126"}],"wp:attachment":[{"href":"https:\/\/thecodepredator.com\/index.php\/wp-json\/wp\/v2\/media?parent=149"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/thecodepredator.com\/index.php\/wp-json\/wp\/v2\/categories?post=149"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/thecodepredator.com\/index.php\/wp-json\/wp\/v2\/tags?post=149"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}