PreCompose

repository·master·Indexed 21 days ago

https://github.com/tlaster/precompose

A Kotlin Multiplatform library that brings Jetpack-inspired Navigation, ViewModel, and Lifecycle components to Compose Multiplatform. It enables shared business logic and UI across Android, iOS, JVM, macOS, and Web, featuring a familiar API surface, managed lifecycles, and integrations with Koin for dependency injection and Molecule for business logic.

Tokens
8.1K
Snippets
27
Records
35
Agent score
75%

What's inside PreCompose

  1. Overview of PreCompose

    master

    PreCompose is a Compose Multiplatform library providing Navigation, ViewModel, and Lifecycle components. It is inspired by Android Jetpack's Navigation, ViewModel, and Lifecycle libraries, but is written in pure Kotlin for Kotlin Multiplatform (KMP) compatibility.

    Key benefits include:

    • Write Once, Run Anywhere: Business logic and UI code can be written in commonMain for Android, iOS, JVM, macOS, and Web.
    • Familiar API: If you are familiar with Jetpack components, the API surface is nearly identical.
    • Managed Lifecycle: PreCompose handles lifecycle management automatically.
    • Integrations: Supports Molecule for business logic and Koin for dependency injection.
  2. Core Components of PreCompose

    master

    PreCompose is composed of several key modules that allow you to build multiplatform applications:

    • Navigation: Provides routing and navigation capabilities similar to Jetpack Navigation.
    • ViewModel: Provides a multiplatform implementation of the ViewModel pattern to manage UI-related data and state.
    • Molecule Integration: Allows you to write business logic using Molecule within a Kotlin Multiplatform project.
    • Koin Integration: Enables seamless dependency injection using Koin.
  3. Define scene routes using variables, regex, and optional paths

    master

    PreCompose supports several patterns for defining scene routes:

    Variable Routes

    Use curly braces to define path variables. You can retrieve these from the BackStackEntry using .path<T>(name).

    • Standard: route = "/detail/{id}"
    • Optional: route = "/detail/{id}?" (matches /detail, /detail/123, or /detail/asd)

    Regex Routes

    You can constrain path variables using regex syntax after a colon: route = "/detail/{id:[0-9]+}".

    • Optional Regex: /user/{id:[0-9]+}? matches /user or /user/123.

    Groups

    You can group scenes together. Note that group routes and initialRoute only support static routes; they do not support path variables or regex.

    group(route = "/group", initialRoute = "/nestedScreen1") {
        scene(route = "/nestedScreen1") { ... }
        scene(route = "/nestedScreen2") { ... }
    }
    // Variable route example
    scene(route = "/detail/{id}") { backStackEntry ->
        val id: Int? = backStackEntry.path<Int>("id")
    }
    
    // Regex route example
    scene(route = "/detail/{id:[0-9]+}") { backStackEntry ->
        val id: Int? = backStackEntry.path<Int>("id")
    }
  4. Configure Navigation Transitions

    master

    You can define NavTransition objects at both the NavHost level and the individual scene level. If a scene defines its own NavTransition, it will be used; otherwise, the system falls back to the NavHost's transition.

    NavTransition supports four lifecycle-based transition types:

    1. createTransition: For a scene appearing for the first time (similar to onCreate).
    2. destroyTransition: For a scene disappearing forever (similar to onDestroy).
    3. pauseTransition: For a scene being pushed into the back stack (similar to onPause).
    4. resumeTransition: For a scene returning from the back stack (similar to onResume).
  5. Integrate Molecule with PreCompose

    master
    Molecule is a library by CashApp for writing business logic in Compose. While Molecule handles the logic, it does not include Lifecycle or Navigation state management. PreCompose provides the integration layer to connect Molecule's logic with Lifecycle and Navigation components.
  6. Save and restore state in ViewModel using SavedStateHolder

    master

    To persist and restore state within a ViewModel, use SavedStateHolder.

    1. Inject SavedStateHolder: Pass the savedStateHolder provided by the viewModel() factory lambda into your ViewModel constructor.
    2. Consume State: Use savedStateHolder.consumeRestored(key) to retrieve a previously saved value.
    3. Register Provider: Use savedStateHolder.registerProvider(key) to tell the holder how to retrieve the current value for saving when the state is being persisted.

    Note: When using SavedStateHolder, you should use the modelClass parameter in the viewModel() function.

    // 1. Define ViewModel with SavedStateHolder
    class SomeViewModel(private val someKey: Int?, savedStateHolder: SavedStateHolder) : ViewModel() {
        val someSavedValue = MutableStateFlow(savedStateHolder.consumeRestored("someValue") as String? ?: "")
        
        init {
            savedStateHolder.registerProvider("someValue") {
                someSavedValue.value
            }
        }
        
        fun setSomeValue(value: String) {
            someSavedValue.value = value
        }
    }
    
    // 2. Use in Compose
    val viewModel = viewModel(modelClass = SomeViewModel::class, keys = listOf(someKey)) {
        savedStateHolder ->
        SomeViewModel(someKey, savedStateHolder)
    }
  7. Use State Action with producePresenter

    master

    If you prefer a State Action pattern, you can define a Presenter that returns a state object containing its own action handler. Use producePresenter to instantiate it in your UI.

    1. Define a @Composable function that returns a State object. The state object should include a lambda to handle actions.
    2. Use producePresenter in your UI to obtain the state.
    // 1. Define the Presenter with embedded action logic
    @Composable
    fun CounterPresenter(): CounterState {
        var count by remember { mutableStateOf(0) }
        return CounterState("Clicked $count times") {
            when (it) {
                CounterAction.Increment -> count++
                CounterAction.Decrement -> count--
            }
        }
    }
    
    // 2. Use in UI
    val state by producePresenter { CounterPresenter() }
    
    // 3. Trigger actions via the state's action handler
    state.onAction(CounterAction.Increment)
  8. Inject PreCompose ViewModels with Koin

    master

    When using Koin with PreCompose, do not use a standard Koin inject() for ViewModels, as it will not manage the ViewModel's lifecycle correctly. Instead, use the koinViewModel function provided by the precompose-koin library.

    Standard Usage

    In your Compose code, call koinViewModel<T> to retrieve your ViewModel. You can pass parameters using parametersOf.

    Kotlin/Native Usage

    If you are targeting Kotlin/Native, you must explicitly provide the vmClass parameter to koinViewModel because of platform-specific limitations.

    Koin Module Definition

    Define your ViewModel in your Koin module using factory (or viewModel if using Koin's specific DSL) and use get() to resolve dependencies.

    // 1. Define in Koin module
    factory { (initialCount: Int) ->
        CounterViewModel(
            initialCount = initialCount,
            counterRepository = get(),
        )
    }
    
    // 2. Use in Compose (Standard)
    val viewModel = koinViewModel<CounterViewModel> { parametersOf(initialCount) }
    
    // 2. Use in Compose (Kotlin/Native)
    val viewModel = koinViewModel(vmClass = CounterViewModel::class) { parametersOf(initialCount) }
  9. Implement basic navigation with NavHost and Navigator

    master

    To set up navigation in PreCompose, use the NavHost composable and a Navigator instance. The Navigator acts as a replacement for Jetpack Navigation's NavController. You create the navigator using rememberNavigator() and pass it to the NavHost. Inside the NavHost block, you define your navigation graph using scene functions, specifying a route for each destination.

    // Define a navigator, which is a replacement for Jetpack Navigation's NavController
    val navigator = rememberNavigator()
    NavHost(
        // Assign the navigator to the NavHost
        navigator = navigator,
        // Navigation transition for the scenes in this NavHost, this is optional
        navTransition = NavTransition(),
        // The start destination
        initialRoute = "/home",
    ) {
        // Define a scene to the navigation graph
        scene(
            // Scene's route path
            route = "/home",
            // Navigation transition for this scene, this is optional
            navTransition = NavTransition(),
        ) {
            Text(text = "Hello!")
        }
    }