Voyager Navigation Library

repository·main·Indexed 25 days ago

https://github.com/adrielcafe/voyager

A multiplatform navigation library for Jetpack Compose supporting Android (API 21+), iOS, Desktop, and Web. It enables scalable Single-Activity applications with support for linear, BottomSheet, Tab, and nested navigation. Voyager provides state management via ScreenModel (a multiplatform ViewModel equivalent) and integrates with Koin, Kodein, Hilt, Coroutines, RxJava, and LiveData. Key features include a state-aware Stack API, built-in transitions, state restoration, and lifecycle callbacks.

Tokens
17.9K
Snippets
67
Records
90
Agent score
85%

What's inside Voyager

  1. Overview of Voyager features

    main

    Voyager provides several navigation patterns and lifecycle management tools:

    • Navigation Patterns: Linear navigation, BottomSheet navigation, Tab navigation, Nested navigation (multiple stacks/parent navigation), and Type-safe multi-module navigation.
    • State & Lifecycle: State-aware Stack API, built-in transitions, state restoration after Activity recreation, Lifecycle callbacks, Back press handling, and Deep linking support.
    • ScreenModel (ViewModel equivalent): Integrated with Koin, Kodein, Hilt, Coroutines, RxJava, and LiveData. Also supports Android ViewModel integration (including Hilt support) and Lifecycle KMP support.
  2. Explore Voyager extension libraries

    main

    The following open-source libraries provide extensions for Voyager to enhance navigation capabilities:

    • Voyant: Enables Voyager and Navigation Compose to use native iOS navigation within Compose Multiplatform.
    • Kotlin Routing: An extensible, multiplatform routing system powered by Ktor.
    • Rinku: Provides deep link handling for Kotlin Multiplatform projects.
  3. Voyager Features Overview

    main

    Voyager provides a comprehensive set of navigation and state management features for Compose multiplatform apps:

    • Navigation Types: Linear navigation, BottomSheet navigation, Tab navigation (YouTube-style), Nested navigation (multiple stacks/parent navigation), and Type-safe multi-module navigation.
    • State Management: ScreenModel (ViewModel equivalent) with integrations for Koin, Kodein, Hilt, Coroutines, RxJava, and LiveData. Also supports Android ViewModel with Hilt support.
    • Core Capabilities: State-aware Stack API, built-in transitions, state restoration after Activity recreation, Lifecycle callbacks, Back press handling, and Deep linking support.
    • Platform Support: Android (API 21+), iOS, Desktop, and Web.
  4. Learn from open source projects using Voyager

    main

    Several open-source projects implement Voyager in real-world scenarios. You can study their implementations for patterns involving Clean Architecture, MVI, and Compose Multiplatform:

    • ClimateTraceKMP: Climate emission data visualization.
    • Suwayomi-JUI: Manga reader.
    • TimePlanner: Task planning app using MVI and multi-module architecture.
    • KMP-News-App: Demonstrates Clean Architecture with Koin, Ktor, and SQLDelight.
    • NationExplorer: Country discovery app for Android and iOS.
    • KodeRunner: Multiplatform code execution app.
    • Cookit Recipes App: Recipe application for iOS and Android.
  5. Ensure Screen state restoration with Java Serializable

    main

    By default, Voyager expects screens to be stored inside an Android Bundle. To ensure your Screen can be restored, all parameters and properties within the Screen class must implement java.io.Serializable.

    Rules for Serializable Screens:

    • All constructor parameters must be serializable (e.g., UUID, String, or custom Serializable classes).
    • All class properties must be serializable.
    • Avoid including non-serializable types like Context or service instances as properties.
    // ✔️ DO
    data class Post(/*...*/) : Serializable
    
    data class ValidScreen(
        val userId: UUID, // Built-in serializable types
        val post: Post // Your own serializable types
    ) : Screen {
        // ...
    }
    
    // 🚫 DON'T
    class Post(/*...*/)
    
    data class InvalidScreen(
        val context: Context, // Built-in non-serializable types
        val post: Post, // Your own non-serializable types
        val parcelable: SomeParcelable // Android Parcelable is not Java Serializable by default
    ) : Screen {
        // ...
    }
  6. Implement BottomSheet navigation with BottomSheetNavigator

    main

    Voyager provides BottomSheetNavigator for use with ModalBottomSheetLayout. You must first import cafe.adriel.voyager:voyager-bottom-sheet-navigator.

    To set up the navigator, call BottomSheetNavigator and provide the back layer content. The BottomSheet content (the front layer) is then set on demand via the navigator. You can also nest a standard Navigator inside the BottomSheetNavigator to handle navigation within the back layer content.

    // Basic setup with back layer content
    setContent {
        BottomSheetNavigator {
            BackContent()
        }
    }
    
    // Setup with a nested Navigator for the back layer
    setContent {
        BottomSheetNavigator {
            Navigator(BackScreen())
        }
    }
  7. Create a Navigator-scoped ScreenModel

    main

    To share a ScreenModel across all screens within a specific Navigator, use rememberNavigatorScreenModel. This ScreenModel will be shared among all screens in the navigator's stack and will be disposed of when the Navigator leaves the Composition. This function is part of the navigator library (available since 1.0.0rc08).

    class HomeScreen : Screen {
    
        @Composable
        override fun Content() {
            val navigator = LocalNavigator.currentOrThrow
            val screenModel = navigator.rememberNavigatorScreenModel { HomeScreenModel() }
            // ...
        }
    }
  8. Observe LiveScreenModel state in Compose

    main

    To consume the state from a LiveScreenModel in a Composable function:

    1. Retrieve the ScreenModel using rememberScreenModel<T>().
    2. Convert the LiveData state into a Compose State object using the state.observeAsState() extension function.
    3. Use the resulting state in a when expression or similar logic to drive your UI.
    class PostDetailsScreen : Screen {
    
        @Composable
        override fun Content() {
            val screenModel = rememberScreenModel<PostDetailsScreenModel>()
            val state by screenModel.state.observeAsState()
    
            when (state) {
                is State.Loading -> LoadingContent()
                is State.Result -> PostContent(state.post)
            }
        }
    }