
Enables pragmatic, type-safe navigation with features like ViewModel integration, SavedStateHandle support, lifecycle management, multi-backstack, and custom transitions. Automatically saves and restores navigation stack states.
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 on Android.
Supports SavedStateHandle, used to save and restore data over configuration changes
or process death on Android.
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.
The Solivagant library uses NavRoutes and NavRoots 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.
Each NavRoute and NavRoot has a corresponding NavDestination 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 to AndroidX Lifecycle library.
[!NOTE] This library is still in alpha, so the API may change in the future.
Most of the code in solivagant-khonshu-navigation-core and solivagant-navigation libraries is
taken from Freeletics Khonshu Navigation,
and ported to Kotlin Multiplatform and Compose Multiplatform.
The solivagant-lifecycle library is inspired
by Essenty Lifecycle,
and AndroidX Lifecycle.
Liked some of my work? Buy me a coffee (or more likely a beer)
allprojects {
repositories {
[...]
mavenCentral()
}
}implementation("io.github.hoc081098:solivagant-navigation:0.5.0")allprojects {
repositories {
...
maven(url = "https://s01.oss.sonatype.org/content/repositories/snapshots/")
}
}
dependencies {
implementation("io.github.hoc081098:solivagant-navigation:0.5.1-SNAPSHOT")
}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.
@Immutable
@Parcelize
data object StartScreenRoute : NavRoot
@Immutable
@Parcelize
data object SearchProductScreenRoute : NavRoute[!NOTE]
@Parcelizeis provided bykmp-viewmodel-savedstatelibrary. See kmp-viewmodel-savedstate for more details.
@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)
}@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))
}
}Gather all NavDestinations in a set and use NavEventNavigator to trigger navigation actions.
@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
NavEventNavigatortoNavHostcomposable, the NavHost will take care of setting up the navigator by callingNavigationSetup(navigator).If you don't pass a "global"
NavEventNavigatortoNavHostcomposable, 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.
To display MyAwesomeApp on Android, use setContent in Activity / Fragment.
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()
}
}
}To display MyAwesomeApp on Desktop, use androidx.compose.ui.window.application and Window composable:
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
To display MyAwesomeApp on iOS/tvOS/watchOS, use ComposeUIViewController (Kotlin - iosMain SourceSet) and UIViewControllerRepresentable (Swift - native code):
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() }
}
}
}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
// 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)Complete sample - Products app: a complete sample
that demonstrates how to use solivagant-navigation in Compose Multiplatform (Android, Desktop, iOS)
solivagant-navigation for navigation in Compose Multiplatform.kmp-viewmodel to share ViewModel and SavedStateHandle.Koin DI.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 in Compose 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.
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.
Kotlin Multiplatform app template with shared UI, using solivagant-navigation.
This library is actively maintained and updated with new features and bug fixes.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
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 on Android.
Supports SavedStateHandle, used to save and restore data over configuration changes
or process death on Android.
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.
The Solivagant library uses NavRoutes and NavRoots 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.
Each NavRoute and NavRoot has a corresponding NavDestination 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 to AndroidX Lifecycle library.
[!NOTE] This library is still in alpha, so the API may change in the future.
Most of the code in solivagant-khonshu-navigation-core and solivagant-navigation libraries is
taken from Freeletics Khonshu Navigation,
and ported to Kotlin Multiplatform and Compose Multiplatform.
The solivagant-lifecycle library is inspired
by Essenty Lifecycle,
and AndroidX Lifecycle.
Liked some of my work? Buy me a coffee (or more likely a beer)
allprojects {
repositories {
[...]
mavenCentral()
}
}implementation("io.github.hoc081098:solivagant-navigation:0.5.0")allprojects {
repositories {
...
maven(url = "https://s01.oss.sonatype.org/content/repositories/snapshots/")
}
}
dependencies {
implementation("io.github.hoc081098:solivagant-navigation:0.5.1-SNAPSHOT")
}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.
@Immutable
@Parcelize
data object StartScreenRoute : NavRoot
@Immutable
@Parcelize
data object SearchProductScreenRoute : NavRoute[!NOTE]
@Parcelizeis provided bykmp-viewmodel-savedstatelibrary. See kmp-viewmodel-savedstate for more details.
@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)
}@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))
}
}Gather all NavDestinations in a set and use NavEventNavigator to trigger navigation actions.
@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
NavEventNavigatortoNavHostcomposable, the NavHost will take care of setting up the navigator by callingNavigationSetup(navigator).If you don't pass a "global"
NavEventNavigatortoNavHostcomposable, 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.
To display MyAwesomeApp on Android, use setContent in Activity / Fragment.
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()
}
}
}To display MyAwesomeApp on Desktop, use androidx.compose.ui.window.application and Window composable:
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
To display MyAwesomeApp on iOS/tvOS/watchOS, use ComposeUIViewController (Kotlin - iosMain SourceSet) and UIViewControllerRepresentable (Swift - native code):
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() }
}
}
}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
// 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)Complete sample - Products app: a complete sample
that demonstrates how to use solivagant-navigation in Compose Multiplatform (Android, Desktop, iOS)
solivagant-navigation for navigation in Compose Multiplatform.kmp-viewmodel to share ViewModel and SavedStateHandle.Koin DI.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 in Compose 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.
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.
Kotlin Multiplatform app template with shared UI, using solivagant-navigation.
This library is actively maintained and updated with new features and bug fixes.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/