Boat - A scoped and composable way to navigate

Related tags

Jetpack Compose boat
Overview

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 navigation

In 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 + printMiddleware

In 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 NavHost API
  • Use FIR to validate BoatRouteContractEffect in compile time
You might also like...
Row Coloumn Box Compose Constraint Layout Modifier.xyz Animator Tween animation MutableState Creating custom composable Corners Canvas LaunchedEffect
Row Coloumn Box Compose Constraint Layout Modifier.xyz Animator Tween animation MutableState Creating custom composable Corners Canvas LaunchedEffect

Row Coloumn Box Compose Constraint Layout Modifier.xyz Animator Tween animation MutableState Creating custom composable Corners Canvas LaunchedEffect

GlassmorphicColumn @Composable
GlassmorphicColumn @Composable

glassmorphic-composables GlassmorphicColumn @Composable GlassmorphicRow @Composable With Non-Image background Setup Gradle: allprojects { reposito

Gymber: a searcher app for sports clubs using Jetpack compose way

GymberCompose GymberCompose is a searcher app for sports clubs using Jetpack com

Notes is a simple and private notes app. Organize your thoughts, discoveries, and ideas and simplify planning important moments in your life with your digital notepad.
Notes is a simple and private notes app. Organize your thoughts, discoveries, and ideas and simplify planning important moments in your life with your digital notepad.

Notes Example Download Download the latest version of the Android app from this link. Building Using Android Studio Clone the repo, open it in Android

Zoom Modifiers, zoomable image and layouts with limit pan bounds, fling and moving back to valid bounds and callbacks that return current transformation or visible image section

Zoom Modifiers, zoomable image and layouts with limit pan bounds, fling and moving back to valid bounds and callbacks that return current transformation or visible image section

🍂 Jetpack Compose image loading library which can fetch and display network images using Glide, Coil, and Fresco.
🍂 Jetpack Compose image loading library which can fetch and display network images using Glide, Coil, and Fresco.

Landscapist 🍂 Jetpack Compose image loading library which can fetch and display network images using Glide, Coil, Fresco Usecase You can see the use

Example Jetpack Compose Android App, that uses the newest mechanisms, like StateFlow, SharedFlow, etc. to manage states and handle events. ViewModel, UI and Screenshot tests included :)
Example Jetpack Compose Android App, that uses the newest mechanisms, like StateFlow, SharedFlow, etc. to manage states and handle events. ViewModel, UI and Screenshot tests included :)

AndroidMVIExample Example Jetpack Compose Android App, that uses the newest mechanisms, like StateFlow, SharedFlow, etc. to manage states and handle e

Scrobble is a wip music tracking and browsing app. It uses the Lastf.fm and spotify APIs to deliver data. The whole UI is created using Jetpack compose.
Scrobble is a wip music tracking and browsing app. It uses the Lastf.fm and spotify APIs to deliver data. The whole UI is created using Jetpack compose.

Scrobble (WIP, name not final) Scrobble is a wip music tracking and browsing app. It uses the Lastf.fm API to realize music tracking and browsing and

An example concepts of MVVM and Kotlin. Display, Filter & Sort the given restaurants from assets with Jetpack Compose and AAC
Owner
Bloder
FP enthusiast interested in compilers and type systems
Bloder
Capturable - 🚀Jetpack Compose utility library for capturing Composable content and transforming it into Bitmap Image🖼️

Capturable ?? A Jetpack Compose utility library for converting Composable content into Bitmap image ??️ . Made with ❤️ for Android Developers and Comp

Shreyas Patil 494 Dec 29, 2022
Pinocchio is a group of libraries for various common UI components. It could contain Composable, View, and everything related to UI.

Pinocchio Pinocchio is a group of libraries for various common UI components. It could contain Composable, View, and everything related to UI. All UI

NAVER Z 24 Nov 30, 2022
Mocking with Jetpack Compose - stubbing and verification of Composable functions

Mockposable A companion to mocking libraries that enables stubbing and verification of functions annotated with @androidx.compose.runtime.Composable.

Jesper Åman 21 Nov 15, 2022
Simple composable for rendering transitions between backstacks.

compose-backstack Simple library for Jetpack Compose for rendering backstacks of screens and animated transitions when the stack changes. It is not a

Zach Klippenstein 408 Jan 3, 2023
a set of Settings like composable items to help android Jetpack Compose developers build complex settings screens

This library provides a set of Settings like composable items to help android Jetpack Compose developers build complex settings screens without all the boilerplate

Bernat Borrás Paronella 178 Jan 4, 2023
Sample app that shows how to create a bitmap from a Jetpack composable

Creating Bitmaps From Jetpack Composables This app demonstrates how to create a bitmap from a Jetpack composable without the need to display the compo

Johann Blake 14 Nov 29, 2022
Jetpack Compose Text composable to show html text from resources

HtmlText Current Compose Version: 1.0.3 Compose HtmlText Text composable to show html text from resources Add to your project Add actual HtmlText libr

Alexander Karkossa 57 Dec 23, 2022
A library that enables Safe Navigation for you Composable destinations when using Jetpack Compose Navigation

A library that enables Safe Navigation for you Composable destinations when using Jetpack Compose Navigation

Roman Levinzon 59 Oct 19, 2022
Kapture - A small library for Jetpack Compose to capture Composable content to Android Bitmap

kapture A small utility library for Jetpack Compose to capture Composable conten

Kaustubh Patange 10 Dec 9, 2022
Flippable - A Jetpack Compose utility library to create flipping Composable views with 2 sides

?? Flippable A Jetpack Compose utility library to create flipping Composable views with 2 sides. Built with ❤︎ by Wajahat Karim and contributors Demo

Wajahat Karim 298 Dec 23, 2022