solivagant
solivagant copied to clipboard
π Compose Multiplatform Navigation library - πΈ Pragmatic, type safety navigation for Compose Multiplatform. Based on Freeletics Khonshu Navigation. β₯οΈ ViewModel, SavedStateHandle, Lifecycle, Multi-...
solivagant π
π 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
.
-
-
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
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.3.0")
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: 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
.
-
-
Simple 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. -
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
- [x] Add more samples
- [ ] Add docs
- [x] Review supported targets
- [ ] Polish and improve the implementation and the public API
- [x] Support transition when navigating (since 0.1.0).
- [x] Support more targets such as wasm, watchOS, tvOS, etc... (since 0.2.0).
License
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/