MVIKotlin Documentation

repository·master·Indexed 21 days ago

https://github.com/arkivanov/mvikotlin

A Kotlin Multiplatform framework for implementing the Model-View-Intent (MVI) architectural pattern. It provides a toolset for unidirectional data flow, including a single source of truth for state, lifecycle-aware bindings via Binders, and debugging tools such as logging and time travel. MVIKotlin supports Android, JVM, iOS, watchOS, tvOS, macOS, linuxX64, JavaScript, and Wasm, with dedicated extensions for Kotlin Coroutines and Reaktive.

Tokens
15.3K
Snippets
34
Records
52
Agent score
76%

What's inside MVIKotlin

  1. What is MVIKotlin and its core responsibilities

    master

    MVIKotlin is a Kotlin Multiplatform framework designed for implementing the Model-View-Intent (MVI) architectural pattern using unidirectional data flow. It is not a rigid architecture but a toolset that provides:

    • A single source of truth for State: This can be scoped to an entire app, a screen, a feature, or a specific component.
    • UI Abstraction: Provides a way to handle efficient UI updates (though using a specific UI abstraction is optional).
    • Lifecycle-aware connections: Provides mechanisms (Binders) to connect inputs and outputs to lifecycle signals (though this is also optional).

    Because it does not enforce specific definitions for 'screens' or 'modules', MVIKotlin can be introduced incrementally into existing projects and used alongside any navigation, UI, or reactive framework of your choice.

  2. MVIKotlin Features and Platform Support

    master

    MVIKotlin provides several advanced features for multiplatform development:

    • Multiplatform Support: Android, JVM, iOS, watchOS, tvOS, macOS, linuxX64, JavaScript, and Wasm.
    • Reactive Extensions: Dedicated modules for both Reaktive and Coroutines.
    • Lifecycle Awareness: Bindings between inputs and outputs that respect lifecycle.
    • Logging: Customizable logging with configurable loggers and formatters.
    • Time Travel Debugging:
      • Multiplatform support for all targets.
      • Android: Export/import events and IntelliJ IDEA/Android Studio plugins.
      • Desktop: A client application for Android, Java, and native Apple (iOS, watchOS, tvOS, macOS) apps.
      • Web: Chrome DevTools extension for browser-based apps.
  3. Understand the View interfaces in MVIKotlin

    master

    MVIKotlin provides three primary interfaces for managing the relationship between your business logic (Store) and your UI (View):

    • ViewRenderer<Model>: Responsible for consuming and rendering Models (the state representation for the UI).
    • ViewEvents<Event>: Responsible for producing Events (user actions sent back to the Store).
    • MviView<Model, Event>: A combination of both ViewRenderer and ViewEvents.

    Note for Jetpack Compose users: If you are using Jetpack Compose, you typically do not need to implement MviView. Instead, you can observe the Store directly within @Composable functions by exposing the state (or a mapped UI model) via an Observable or Flow.

  4. Debug recorded events in inspection state

    master

    During inspection, you can re-trigger specific events to debug logic using breakpoints.

    Triggering Intents or Actions: When you click Debug the selected event for an Intent or Action, a new debug instance of the Executor is created. This instance starts with the exact State that existed at the time of recording. Any Messages dispatched during this session will update the debug State via the Reducer. Note that Labels published during this session are ignored.

    Triggering Messages: If the event is a Message, the Reducer is called directly with the Message and the corresponding State. The result of the Reducer is ignored in this specific debug context.

  5. Customize logging with custom Logger and LogFormatter

    master
    The LoggingStoreFactory allows for customization of the logging output. You can replace the default Logger and LogFormatter implementations with your own to control how logs are captured and how they are formatted (e.g., to integrate with specific platform logging systems or to change the visual structure of the logs).
  6. Use Binder to connect inputs and outputs

    master

    A Binder is a utility used to connect outputs (like a Store's states or labels) to inputs (like a View's events or another Store's intents). It provides a simple lifecycle management mechanism with two primary methods:

    • start(): Connects (subscribes) outputs to inputs.
    • stop(): Disconnects (unsubscribes) outputs from inputs.

    To use the bind { ... } DSL, you must include either the mvikotlin-extensions-coroutines or mvikotlin-extensions-reaktive modules.

    // Example of manual Binder management
    class CalculatorController {
        private val store = CalculatorStoreFactory(DefaultStoreFactory).create()
        private var binder: Binder? = null
    
        fun onViewCreated(view: CalculatorView) {
            binder = bind {
                store.states.map(stateToModel) bindTo view
                view.events.map(eventToIntent) bindTo store
            }
        }
    
        fun onStart() {
            binder?.start()
        }
    
        fun onStop() {
            binder?.stop()
        }
    
        fun onViewDestroyed() {
            binder = null
        }
        
        fun onDestroy() {
            store.dispose()
        }
    }
  7. How the Store structure works

    master

    A Store is composed of up to three main components that handle the flow of data:

    1. Bootstrapper: Kick-starts the Store. It produces Actions that are processed by the Executor. It runs on the main thread, and Actions must be dispatched on the main thread.
    2. Executor: The core of business logic and asynchronous operations.
      • In version 4.x, it accepts Intents and Actions. It outputs Messages (to the Reducer), Action (back to itself), and Labels (to the outside world).
      • In version 3.x, it accepts Intents and Actions (from the Bootstrapper). It outputs Messages (to the Reducer) and Labels (to the outside world).
      • The Executor runs on the main thread, but you can switch threads for processing. Messages and Labels must be dispatched on the main thread.
    3. Reducer: A function that takes a Message and the current State to return a new State. It is always called on the main thread.
  8. Record and inspect Time Travel events

    master

    Once a client is connected, you can capture the application's state transitions.

    Recording:

    • Click Start recording to begin capturing events. Events appear in a list on the left.
    • Click Stop recording to end the session.

    Inspection State: After recording, the application enters an inspection state where all Stores are disconnected from their inputs and outputs. Events are accumulated and postponed until inspection is finished.

    Navigation:

    • Move to start: Jump to the first recorded state.
    • Step backward: Move to the previous state.
    • Step forward: Move to the next state.
    • Move to end: Jump to the latest state.
  9. How data flows in MVIKotlin

    master

    MVIKotlin follows a unidirectional data flow where the Store (Model) and View (UI) are decoupled through mapping functions.

    The Data Loop

    1. Store to View: The Store produces a stream of States. These are transformed into a stream of View Models via a Mapper function ($f$). The View renders these View Models.
    2. View to Store: The View produces a stream of View Events. These are transformed into a stream of Intents via another Mapper function ($f$). The Store consumes these Intents to update its logic.

    One-time Events (Labels)

    The Store also produces a stream of Labels. These represent one-time events (e.g., showing an error, navigation, or routing) that are not part of the persistent State. Labels can be used directly by the UI or transformed into Intents to be redirected to other Stores.

    Decoupling and Scaling

    • Simplicity: For simple use cases, a View can directly render States and produce Intents without mappers.
    • Complexity: You can combine multiple States (from multiple Stores) into a single View Model, or combine multiple View Events (from multiple Views) into a single Intent.
  10. How reactivity and Bindings work in MVIKotlin

    master

    Bindings and Lifecycle

    To manage the connection between the Store and the View, MVIKotlin uses a Binder. The Binder manages subscriptions to the data streams and responds to start and stop signals to ensure lifecycle-aware updates. While provided, the Binder is optional; you can manually manage subscriptions as needed.

    Reactive Frameworks

    MVIKotlin is a reactive framework centered on data streams and transformations. However, it does not force a specific reactive library on you. It uses a tiny abstraction over Rx internally. To use your preferred reactive tool, you can use separate modules for:

  11. Kick-start a Store using a Bootstrapper

    master

    A Bootstrapper allows a Store to perform initial actions immediately upon creation (e.g., loading data from a database).

    1. Define an Action type that represents the bootstrapping task.
    2. Implement a Bootstrapper (e.g., SimpleBootstrapper for static actions or CoroutineBootstrapper/ReaktiveBootstrapper for async tasks).
    3. In your Executor, implement executeAction(action: Action, getState: () -> State) to handle the triggered action.
    4. Pass the Bootstrapper instance to storeFactory.create().
    // Using CoroutineBootstrapper
    private class BootstrapperImpl : CoroutineBootstrapper<Action>() {
        override fun invoke() {
            scope.launch {
                val data = fetchData()
                dispatch(Action.LoadData(data))
            }
        }
    }
    
    // In Factory
    fun create() = object : CalculatorStore, Store<Intent, State, Nothing> by storeFactory.create(
        name = "Store",
        initialState = State(),
        bootstrapper = BootstrapperImpl(),
        executorFactory = ::ExecutorImpl,
        reducer = ReducerImpl
    ) {}
  12. Core components: Store and MviView

    master

    MVIKotlin is built around two primary components:

    • Store: Represents the Model in the MVI pattern. This is where your business logic resides and where the State is managed.
    • MviView: Represents the View in the MVI pattern. This is the UI layer that renders the state. Note that MviView is optional; you can implement your own UI logic.

    Data flows between these components on the Main thread.