solivagant 🔆¶
[🟢 ACTIVE] 🔆 Compose Multiplatform Navigation library - 🌸 Pragmatic, type safety navigation for Compose Multiplatform. Based on Freeletics Khonshu Navigation. ♥️ ViewModel, SavedStateHandle, Lifecycle, Multi-Backstacks, Transitions, Back-press handling, and more…¶
- Integrates with
Jetbrains Compose Multiplatform
seamlessly and easily.
-
Integrates with kmp-viewmodel library seamlessly and smoothly
- Stack entry scoped
ViewModel
, exists as long as the stack entry is on the navigation stack, including the configuration changes onAndroid
.
- Supports
SavedStateHandle
, used to save and restore data over configuration changes or process death onAndroid
.
- Stack entry scoped
- The navigation stack state is saved and restored automatically over configuration changes and process
death on
Android
. On other platforms, you can use a support class provided by this library to store the navigation stack state as long as you want.
- Type safety navigation, easy to pass data between destinations.
No more
String
route and dynamic query parameters. TheSolivagant
library usesNavRoute
s andNavRoot
s to define routes that can be navigated to. Arguments can be defined as part of the route (a.ka. properties of the route class) and are type safe. EachNavRoute
andNavRoot
has a correspondingNavDestination
that describes the UI (a.k.a@Composable
) of the route.
- Supports Multi-Backstacks, this is most commonly used in apps that use bottom navigation to separate the back stack of each tab. See Freeletics Khonshu Navigation - Multiple back stacks for more details.
- Supports
LifecycleOwner
,Lifecycle
events and states, similar toAndroidX Lifecycle
library.
[!NOTE] This library is still in alpha, so the API may change in the future.
Credits¶
- Most of the code in
solivagant-khonshu-navigation-core
andsolivagant-navigation
libraries is taken from Freeletics Khonshu Navigation, and ported toKotlin Multiplatform
andCompose Multiplatform
.
- The
solivagant-lifecycle
library is inspired by Essenty Lifecycle, and AndroidX Lifecycle.
Author: Petrus Nguyễn Thái Học¶
Liked some of my work? Buy me a coffee (or more likely a beer)
Docs & Installation¶
0.x release docs: https://hoc081098.github.io/solivagant/docs/0.x¶
Snapshot docs: https://hoc081098.github.io/solivagant/docs/latest¶
Installation¶
allprojects {
repositories {
[...]
mavenCentral()
}
}
implementation("io.github.hoc081098:solivagant-navigation:0.5.0")
Snapshot¶
Snapshots of the development version are available in Sonatype's snapshots repository.
allprojects {
repositories {
...
maven(url = "https://s01.oss.sonatype.org/content/repositories/snapshots/")
}
}
dependencies {
implementation("io.github.hoc081098:solivagant-navigation:0.5.1-SNAPSHOT")
}
Getting started¶
The library is ported from Freeletics Khonshu Navigation
library, so the concepts is similar.
You can read the Freeletics Khonshu Navigation to
understand
the concepts.
👉 Full samples are available here.
1. Create NavRoot
s, NavRoute
s¶
@Immutable
@Parcelize
data object StartScreenRoute : NavRoot
@Immutable
@Parcelize
data object SearchProductScreenRoute : NavRoute
[!NOTE]
@Parcelize
is provided bykmp-viewmodel-savedstate
library. See kmp-viewmodel-savedstate for more details.
2. Create NavDestination
s along with Composable
s and ViewModel
s¶
StartScreen.kt¶
@JvmField
val StartScreenDestination: NavDestination =
ScreenDestination<StartScreenRoute> { StartScreen() }
@Composable
internal fun StartScreen(
modifier: Modifier = Modifier,
// kmpViewModel or kojectKmpViewModel can be used instead.
viewModel: StartViewModel = koinKmpViewModel(),
) {
// UI Composable
}
internal class StartViewModel(
// used to trigger navigation actions from outside the view layer (e.g. from a ViewModel).
// Usually, it is singleton object, or the host Activity retained scope.
private val navigator: NavEventNavigator,
) : ViewModel() {
internal fun navigateToProductsScreen() = navigator.navigateTo(ProductsScreenRoute)
internal fun navigateToSearchProductScreen() = navigator.navigateTo(SearchProductScreenRoute)
}
SearchProductScreen.kt¶
@JvmField
val SearchProductScreenDestination: NavDestination =
ScreenDestination<SearchProductScreenRoute> { SearchProductsScreen() }
@Composable
internal fun SearchProductsScreen(
modifier: Modifier = Modifier,
// kmpViewModel or kojectKmpViewModel can be used instead.
viewModel: SearchProductsViewModel = koinKmpViewModel<SearchProductsViewModel>(),
) {
// UI Composable
}
internal class SearchProductsViewModel(
private val searchProducts: SearchProducts,
private val savedStateHandle: SavedStateHandle,
// used to trigger navigation actions from outside the view layer (e.g. from a ViewModel).
// Usually, it is singleton object, or the host Activity retained scope.
private val navigator: NavEventNavigator,
) : ViewModel() {
fun navigateToProductDetail(id: Int) {
navigator.navigateTo(ProductDetailScreenRoute(id))
}
}
3. Setup¶
3.1. NavHost¶
Gather all NavDestination
s in a set and use NavEventNavigator
to trigger navigation actions.
MyAwesomeApp.kt¶
@Stable
private val AllDestinations: ImmutableSet<NavDestination> = persistentSetOf(
StartScreenDestination,
SearchProductScreenDestination,
// and more ...
)
@Composable
fun MyAwesomeApp(
// used to trigger navigation actions from outside the view layer (e.g. from a ViewModel).
// Usually, it is singleton object, or the host Activity retained scope.
navigator: NavEventNavigator = koinInject(),
modifier: Modifier = Modifier,
) {
// BaseRoute is the parent interface of NavRoute and NavRoot.
// It implements Parcelable so that it can be used with rememberSavable.
var currentRoute: BaseRoute? by rememberSavable { mutableStateOf(null) }
NavHost(
modifier = modifier,
// route to the screen that should be shown initially
startRoute = StartScreenRoute,
// should contain all destinations that can be navigated to
destinations = AllDestinations,
// when passing a NavEventNavigator to NavHost, NavHost will take care of setting up the navigator by calling `NavigationSetup(navigator)`
navEventNavigator = navigator,
destinationChangedCallback = { currentRoute = it },
)
}
[!IMPORTANT] When passing a
NavEventNavigator
toNavHost
composable, the NavHost will take care of setting up the navigator by callingNavigationSetup(navigator)
.If you don’t pass a “global”
NavEventNavigator
toNavHost
composable, make sure there are property calls toNavigationSetup(navigator)
. For example, we can callNavigationSetup(navigator)
in each destination composable.@JvmField val StartScreenDestination: NavDestination = ScreenDestination<StartScreenRoute> { NavigationSetup(navigator = koinInject()) StartScreen() } @JvmField val SearchProductScreenDestination: NavDestination = ScreenDestination<SearchProductScreenRoute> { NavigationSetup(navigator = koinInject()) SearchProductsScreen() }
👉 Check out scoped navigator sample for more information.
3.2. Android¶
To display MyAwesomeApp
on Android
, use setContent
in Activity
/ Fragment
.
MainActivity.kt¶
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle) {
super.onCreate()
// navigator can be retrieved from the DI container, such as Koin, Dagger Hilt, etc...
setContent {
MyAwesomeApp()
}
}
}
3.3. Desktop¶
To display MyAwesomeApp
on Desktop
, use androidx.compose.ui.window.application
and Window
composable:
main.kt¶
fun main() {
val lifecycleRegistry = LifecycleRegistry()
val savedStateSupport = SavedStateSupport()
application {
val windowState = rememberWindowState()
val lifecycleOwner = rememberLifecycleOwner(lifecycleRegistry)
LifecycleControllerEffect(
lifecycleRegistry = lifecycleRegistry,
windowState = windowState,
)
savedStateSupport.ClearOnDispose()
Window(
onCloseRequest = ::exitApplication,
title = "Solivagant sample",
state = windowState,
) {
LifecycleOwnerProvider(lifecycleOwner) {
// navigator can be retrieved from the DI container, such as Koin, Koject, etc...
savedStateSupport.ProvideCompositionLocals { MyAwesomeApp() }
}
}
}
}
[!TIP] For more information please check out Desktop sample main.kt
3.4. iOS / tvOS / watchOS¶
To display MyAwesomeApp
on iOS/tvOS/watchOS
, use ComposeUIViewController
(Kotlin - iosMain SourceSet) and UIViewControllerRepresentable
(Swift - native code):
MainViewController.kt¶
val AppLifecycleOwner by lazy { AppLifecycleOwner() }
fun MainViewController(savedStateSupport: SavedStateSupport): UIViewController {
val lifecycleOwnerUIVcDelegate =
LifecycleOwnerComposeUIViewControllerDelegate(hostLifecycleOwner = AppLifecycleOwner)
.apply { bindTo(savedStateSupport) }
.apply { lifecycle.subscribe(LifecycleObserver) }
return ComposeUIViewController(
configure = { delegate = lifecycleOwnerUIVcDelegate },
) {
LifecycleOwnerProvider(lifecycleOwnerUIVcDelegate) {
savedStateSupport.ProvideCompositionLocals { MyAwesomeApp() }
}
}
}
ComposeView.swift¶
private struct ComposeView: UIViewControllerRepresentable {
let savedStateSupport: NavigationSavedStateSupport
func makeUIViewController(context: Context) -> UIViewController {
MainViewControllerKt.MainViewController(savedStateSupport: savedStateSupport)
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) { }
}
private class ComposeViewViewModel: ObservableObject {
let savedStateSupport = NavigationSavedStateSupport()
deinit {
self.savedStateSupport.clear()
}
}
struct ComposeViewContainer: View {
@StateObject private var viewModel = ComposeViewViewModel()
var body: some View {
ComposeView(savedStateSupport: viewModel.savedStateSupport)
.ignoresSafeArea(.keyboard) // Compose has own keyboard handler
}
}
[!TIP] For more information please check out iOS sample MainViewController.kt and iosApp sample ComposeView.swift
4. Use NavEventNavigator
in ViewModel
s / @Composable
s to trigger navigation actions¶
// navigate to the destination that the given route leads to
navigator.navigateTo(DetailScreenRoute("some-id"))
// navigate up in the hierarchy
navigator.navigateUp()
// navigate to the previous destination in the backstack
navigator.navigateBack()
// navigate back to the destination belonging to the referenced route and remove all destinations
// in between from the back stack, depending on inclusive the destination
navigator.navigateBackTo<MainScreenRoute>(inclusive = false)
Samples¶
-
Complete sample - Products app: a complete sample that demonstrates how to use
solivagant-navigation
inCompose Multiplatform (Android, Desktop, iOS)
solivagant-navigation
for navigation in Compose Multiplatform.kmp-viewmodel
to shareViewModel
andSavedStateHandle
.Koin DI
.
Desktop¶
https://github.com/user-attachments/assets/8b2d806f-3650-47ed-96a2-d9bc72342d25
- Simple sample - Multibackstacks sample: a simple sample
that demonstrates how to use
solivagant-navigation
inCompose Multiplatform (Android, Desktop, iOS)
to switch between tabs (bottom navigation), but can keep the back stack state of each tab. Basically, it’s a multi-backstack demo sample.###### Desktop
https://github.com/user-attachments/assets/300d7153-ccc0-49b3-a272-ff4f22f66e03
- Compose Multiplatform KmpViewModel KMM Unsplash Sample: A KMP template of the Unsplash App using Compose multiplatform for Android, Desktop, iOS. Share everything including data, domain, presentation, and UI.
- Compose Multiplatform Todo solivagant Sample: A KMP template of the Todo App using Compose multiplatform for Android, Desktop, iOS and Web. Share everything including data, domain, presentation, and UI.
Roadmap¶
- Add more tests
- Add more samples
- Add docs
- Review supported targets
- Polish and improve the implementation and the public API
- Support transition when navigating (since 0.1.0).
- Support more targets such as wasm, watchOS, tvOS, etc… (since 0.2.0).
🟢 Active status¶
This library is actively maintained and updated with new features and bug fixes.
License¶
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/