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’m going to link all pieces together to get a global vision of how everything works
import SwiftUI import shared struct KMMNavigationView: View { let associations: [String : NavBuilder] @ObservedObject var navigationBridge : NavigationBridge @State var navigator: INavigator init(associations: [String : NavBuilder]){ let navigator = DIPresentationHelper.shared.getNavigator() self.navigator = navigator self.associations = associations self.navigationBridge = NavigationBridge(associations: associations, navigator: navigator) navigator.navigatorBridge = self.navigationBridge } var body: some View { NavigationStack(path: self.navigationBridge.navPath) { getRootView() .navigationDestination( for: NavInstance.self, destination: { instance in AnyView( instance.viewBuilder(instance.viewModel) ) } ).navigationViewStyle(.stack) } } @ViewBuilder func getRootView() -> some View { let instance = navigationBridge.navInstances.first(where: { $0.request == navigationBridge.rootRoute }) AnyView(instance!.viewBuilder(instance!.viewModel)) } }
This is the place where everything gathers and starts to make sense. Let’s dive in the code:
- There are 3 field here: the navigation bridge, the navigator and a map of associations.
- This is a view so it has a body, which is a NavigationStack which has a list of NavInstance as parameter.
- That NavigationStack has a getRootView viewBuilder as child. That method “renders” the root view, the view which we have selected as initial one.
- 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).
Ok, but how do I set up that associations?, let’s check it:
import SwiftUI import shared import KMPObservableViewModelCore struct ContentView: View { var body: some View { KMMNavigationView( associations: [ MainScreenNavigationRequest.shared.route : NavBuilder({ request in DIPresentationHelper.shared.getMainViewModel(request: request) }, { viewModel in MainScreenView(viewModel: viewModel as! MainViewModel) }), Test1ScreenNavigationRequest.shared.route : NavBuilder({ request in DIPresentationHelper.shared.getMainViewModel(request: request) }, { viewModel in Test1ScreenView(viewModel: viewModel as! MainViewModel) }), Test2ScreenNavigationRequest.shared.route : NavBuilder({ request in DIPresentationHelper.shared.getMainViewModel(request: request) }, { viewModel in Test2ScreenView(viewModel: viewModel as! MainViewModel) }) ] ) } }
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).
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).
How everything works?
Everything is kind of abstract or complex at this point, so I’m going to explain step by step how everything works:
- Let’s start from the end: ContentView. Here we set up the associations route<->views (check NavBuilder class in the previous article of this series)
- 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.
- Ok, now, we navigate from root to another screen. Let’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 -> NavInstance)
- When the new NavInstance is created, we add it to navPathStack (which is a @Published var, and the whole Navigator is an ObservableObject).
- 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 ‘stack’ so with the new NavInstance previously created, we build the new view on top of the previous one and that’s it!
Navigation requests
A navigation request is a data model which has:
- A route. For example /home/screen1
- A list of arguments to be consumed by the destination. For example, an id to be requested to a backend.
- 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.
open class NavigationRequest<ARGS>( val route: String, val arguments: ARGS? = null, var continuation: Continuation<Any?>? = null ) { override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || this::class != other::class) return false other as NavigationRequest<*> if (route != other.route) return false if (arguments != other.arguments) return false if (continuation != other.continuation) return false return true } override fun hashCode(): Int { var result = route.hashCode() result = 31 * result + (arguments?.hashCode() ?: 0) result = 31 * result + (continuation?.hashCode() ?: 0) return result } }
But how all this is used? Ok, do you remember the INavigator interface described in the previous article? Let’s check a ‘real’ implementation of it: the Navigator class.
package com.codepredator.navigation import com.codepredator.navigation.models.NavigationRequest import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlin.coroutines.Continuation import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine class Navigator(override val initialRoute: NavigationRequest<*>) : INavigator { override lateinit var navigatorBridge: INavigatorBridge override var currentRequest : NavigationRequest<*>? = null override var stack = mutableListOf(initialRoute) override suspend fun navigateTo(request: NavigationRequest<*>){ stack.add(request) navigatorBridge.navigateTo(request) } override suspend fun <T> navigateToForResult(request: NavigationRequest<*>) : T { return withContext(Dispatchers.Main){ suspendCoroutine { if (navigatorBridge.navigateTo(request)) { request.continuation = it as Continuation<Any?>? currentRequest = request stack.add(request) } } } } override fun popBack(result: Any?) : Boolean { val success = navigatorBridge.popBack() if (success) { stack.remove(currentRequest) currentRequest?.continuation?.resume(result) } return success } override suspend fun popUpToRoot() = withContext(Dispatchers.Main) { val success = navigatorBridge.popUpToRoot() if (success){ while (stack.size > 1){ val request = stack.last() request.continuation?.resume(null) stack = stack.dropLast(1).toMutableList() currentRequest = stack.last() } } success } }
This class is the way to connect the shared login written in KMM with each platform navigation bridge. Let’s analyze it method by method:
- navigateTo: it adds the request to the stack and send it to the bridge.
- navigateToForResult: same as previous one but populating the ‘continuation’ parameter of the request object.
- 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.
- popUpToRoot: it is similar to the previous one but it ‘consumes’ all requests except the first one (the initial route).
And that’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’ll keep you posted!
One response to “Navigation System with KMM (Part 2)”
[…] you in the next chapter of Navigation with KMM […]