Boat
Boat is an implementation of a scoped, simple and composable way to navigate
val moduleFirst: BoatNavigationEffect = Boat {
compose("/first") { FirstActivity::class }
}.effect()
val moduleSecond: BoatNavigationEffect = Boat {
compose("/second") { SecondActivity::class }
}.effect()
val moduleThird: BoatNavigationEffect = Boat {
compose("/third") { ThirdActivity::class }
compose("/fourth") { FourthActivity::class }
}.effect()
val appNavigation: BoatNavigationEffect = moduleFirst + moduleSecond + moduleThird
suspend fun main(context: Context) {
navigate(context, appNavigation)
}
suspend fun navigate(context: Context, navigation: BoatNavigationEffect) {
navigation.navigate(context, "/first") // Navigating to FirstActivity
navigation.navigate(context, "/second") // Navigating to SecondActivity
navigation.navigate(context, "/third") // Navigating to ThirdActivity
navigation.navigate(context, "/fourth") // Navigating to FourthActivity
}Concept
Boat is build on top of a simple concept: It's all about effects composition. Boat provides some effects that are built on top of its compositions, all effects must respect some laws:
- All identities are immutable and composition doesn't break it.
- Every composition create a new effect with correct configuration.
- During the composition none of composed effects are affected.
Effects
As seeing before, effects are totally composables and Boat provides some of them:
Navigation effect
BoatNavigationEffect is an effect that navigates to a specific route, this effect is built on top of Boat type with a effect() function, and we can simple call navigate function to start the navigation:
val appNavigation: BoatNavigationEffect = Boat {
compose("/first") { FirstActivity::class }
compose("/second") { SecondActivity::class }
}.effect()
suspend fun main(context: Context) {
appNavigation.navigate(context, "/second") // Navigating to SecondActivity
}Since we work with effects composition with all immutable and scoped laws preserved, we are free to play as we want:
val appNavigation: BoatNavigationEffect = Boat {
compose("/first") { FirstActivity::class }
compose("/second") { SecondActivity::class }
}.effect()
suspend fun main() {
myExternalModule(appNavigation)
}
...
private val myModuleNavigation: BoatNavigationEffect = Boat {
compose("/my_module_1") { MyModuleFirstActivity::class }
compose("/my_module_2") { MyModuleSecondActivity::class }
}.effect()
suspend fun myExternalModule(navigation: BoatNavigationEffect) {
registerInMyDI(navigation + myModuleNavigation)
}
suspend fun registerInMyDI(navigation: BoatNavigationEffect) {
// work with a composed navigation
}To compose effects in Boat we use + operator, in this example we just created an internal navigation effect for a external module and composed with an injected navigation effect, by that my external module can navigate to /my_module_1, /my_module_2, /first and /second routes with no impact to appNavigation that never knows about /my_module_1 and /my_module_2 routes.
Route contract effect
BoatRouteContractEffect validates if N routes are composed in boat navigation identity during the composition:
val navigation: BoatNavigationEffect = Boat {
compose("/first") { FirstActivity::class }
compose("/second") { SecondActivity::class }
}.effect()
val appRouteContracts: BoatRouteContractEffect = RouteContract {
compose("/first")
compose("/second")
compose("/third")
compose("/fourth")
}.effect { "Routes $this should be composed in navigation" }
val appNavigation: BoatNavigationEffect = navigation + appRouteContracts // java.lang.IllegalArgumentException: Routes /third, /fourth should be composed in navigationIn this example we've created a navigation with two routes and a contract with 4 routes, by creating this contract effect what we want is to make sure that composed navigation effect identity compose all these routes that we've declared in contract. In this case we receive a throw of a exception with our custom message saying that composed navigation effect doesn't satisfies our contract. Once we compose the other two missing routes, we're good:
val navigation: BoatNavigationEffect = Boat {
...
compose("/third") { FirstActivity::class }
compose("/fourth") { SecondActivity::class }
}.effect()
...
val appNavigation: BoatNavigationEffect = navigation + appRouteContracts // OK!This is useful when we have for example multiple modules and in its navigation injection we want to establish a contract saying that my module need N routes to be composed in the injected navigation effect.
Middleware effect
BoatMiddlewareEffect provides a way to intercept and write extra custom effects over a navigation. As example let's create a solution that prints "Navigating to route X..." before and Navigated to route X after the navigation every time we navigate to a route:
val navigation: BoatNavigationEffect = Boat {
compose("/first") { FirstActivity::class }
compose("/second") { SecondActivity::class }
}.effect()
val printMiddleware: BoatMiddlewareEffect = boatMiddleware { route, _, _, _, navigate ->
println("Navigating to route $route...")
navigate()
println("Navigated to route $route")
}
val appNavigation: BoatNavigationEffect = navigation + printMiddlewareIn this example we've created a middleware that prints before and after navigation, navigate() function represents the moment that effect navigates. It's triggered when we call navigate() function from the BoatNavigationEffect:
appNavigation.navigate(context, "/first")
> "Navigating to route /first..."
*Navigation occurs*
> "Navigated to route /first"navigate() function is just a representation of the navigation continuation that means we can't modify navigation parameters.
We can also compose more middlewares
val navigation: BoatNavigationEffect = Boat {
compose("/first") { FirstActivity::class }
compose("/second") { SecondActivity::class }
}.effect()
val printMiddleware: BoatMiddlewareEffect = boatMiddleware { route, _, _, _, navigate ->
println("Navigating to route $route...")
navigate()
println("Navigated to route $route")
}
val Tracker.middleware: BoatMiddlewareEffect get() = boatMiddleware { route, _, _, _, navigate ->
track(route)
navigate()
}
suspend fun main(context: Context, tracker: Tracker) {
val appNavigation: BoatNavigationEffect = navigation + printMiddleware + tracker.middleware
}Composed middlewares means that we have closures with effects, then in this case we have this behavior:
val appNavigation: BoatNavigationEffect = navigation + printMiddleware + tracker.middleware
appNavigation.navigate(context, "/first")
> "Navigating to route /first..."
> Tracking "/first"
*Navigation occurs*
> "Navigated to route /first" Side effects controlling
We're working with effects in everywhere, performing a lot of uncontrolled side effects to our program. Thinking on that all Boat effects has its Unit returned functions marked with a suspend operator. Once we're declaring our impure functions with suspend operator, Kotlin compiler "declares" in compile time for us a Continuation<A> extra parameter that proves that we know how to handle success and failure results from our effect. With that we can control our program in a way that Boat is not allowed to perform effects out of a pure environment (inside another suspend function or a continuation).
Enhancement
Next studies and improvements:
- Integration with Compose
NavHostAPI - Use
FIRto validateBoatRouteContractEffectin compile time