moko-mvvm

repository·master·Indexed 22 days ago

https://github.com/icerockdev/moko-mvvm

A Kotlin Multiplatform library providing Model-View-ViewModel (MVVM) architecture components to share UI logic, ViewModels, and reactive data holders across Android, iOS, and other Kotlin targets. It includes lifecycle-aware ViewModel, LiveData, MutableLiveData, MediatorLiveData, and an EventsDispatcher. The library supports integration with Jetpack Compose, SwiftUI, DataBinding, and ViewBinding, and provides a ResourceState sealed class for managing data-fetching states.

Tokens
3.9K
Snippets
10
Records
13
Agent score
28%

What's inside moko-mvvm

  1. Overview of moko-mvvm features

    master

    moko-mvvm is a Kotlin Multiplatform library providing Model-View-ViewModel architecture components for UI applications. It is designed to be lifecycle-aware on Android and supports all Kotlin targets.

    Key features include:

    • ViewModel: Manages UI-related data. On Android, it interoperates with androidx.lifecycle.ViewModel.
    • LiveData, MutableLiveData, MediatorLiveData: Lifecycle-aware reactive data holders with transformation and merging operators.
    • EventsDispatcher: Dispatches events from a ViewModel to a View with automatic lifecycle control and explicit event interfaces.
    • UI Integration: Support for DataBinding, ViewBinding, Jetpack Compose, and SwiftUI.
    • Multiplatform Support: The core, flow, and livedata modules support all Kotlin targets.
  2. Send events from ViewModel to View using EventsDispatcher

    master

    Use EventsDispatcher<T> to send one-time events (like navigation or showing alerts) from a ViewModel to a View. This prevents memory leaks by automatically managing observers based on the lifecycle.

    Common Implementation

    Define an interface (e.g., EventsListener) that the View will implement. Pass the EventsDispatcher into the ViewModel constructor.

    Android Implementation

    1. Manual Binding: In an MvvmActivity, call viewModel.eventsDispatcher.bind(lifecycleOwner = this, listener = this) inside onCreate to attach the dispatcher to the Activity lifecycle.
    2. Simplified Binding: Implement EventsDispatcherOwner<T> in your ViewModel. Then, use MvvmEventsActivity in your Activity. This automatically handles the binding of the dispatcher to the lifecycle.

    iOS Implementation

    On iOS, create an instance of EventsDispatcher by passing the listener (the UIViewController) directly into the constructor: EventsDispatcher<ListenerType>(listener: self). Note that bind is not used on iOS.

    // commonMain
    class EventsViewModel(
        val eventsDispatcher: EventsDispatcher<EventsListener>
    ) : ViewModel() {
    
        fun onButtonPressed() {
            eventsDispatcher.dispatchEvent { routeToMainPage() }
        }
    
        interface EventsListener {
            fun routeToMainPage()
        }
    }
  3. Manage Coroutines in ViewModel with viewModelScope

    master

    The ViewModel class provides a viewModelScope property, which is a CoroutineScope that uses a default UI dispatcher on both Android and iOS.

    Any coroutines launched within viewModelScope are automatically canceled when the ViewModel is cleared (in onCleared), preventing memory leaks and background work on destroyed components.

    fun onLoginButtonPressed() {
        viewModelScope.launch {
            _isLoading.value = true
            try {
                // ... perform async work
            } finally {
                _isLoading.value = false
            }
        }
    }
  4. Implement a simple ViewModel

    master

    To create a basic ViewModel in commonMain, extend the ViewModel class and use MutableLiveData for internal state and LiveData for public exposure. You can use the .map { ... } operator to transform data for the View.

    Android Integration

    Use MvvmActivity to automatically load a DataBinding layout, resolve the ViewModel, and set the binding variable. You must provide the layoutId, viewModelVariableId (from your BR class), and viewModelClass. Use createViewModelFactory to instantiate your ViewModel.

    In your XML layout, declare the viewModel variable in the <data> block and use the .ld suffix (e.g., @{viewModel.counter.ld}) to bind to LiveData values.

    iOS Integration

    In your UIViewController, instantiate the ViewModel manually. Use the bindText(liveData:) extension from the MultiPlatformLibraryMvvm CocoaPod to bind LiveData to UI elements like UILabel.

    // commonMain
    class SimpleViewModel : ViewModel() {
        private val _counter: MutableLiveData<Int> = MutableLiveData(0)
        val counter: LiveData<String> = _counter.map { it.toString() }
    
        fun onCounterButtonPressed() {
            val current = _counter.value
            _counter.value = current + 1
        }
    }
  5. Use moko-mvvm with SwiftUI

    master

    To integrate moko-mvvm with SwiftUI, follow these two steps:

    1. Set the name of your Kotlin framework to MultiPlatformLibrary.
    2. Add the mokoMvvmFlowSwiftUI pod to your CocoaPods dependency list.

    Note: You must also export mvvm-core and mvvm-flow to your framework as described in the iOS export guide.

    # In your Podfile
    pod 'mokoMvvmFlowSwiftUI', :podspec => 'https://raw.githubusercontent.com/icerockdev/moko-mvvm/release/0.16.1/mokoMvvmFlowSwiftUI.podspec'
  6. Install moko-mvvm dependencies

    master

    First, ensure mavenCentral() is added to your root build.gradle repositories.

    Then, add the required modules to your project's build.gradle dependencies. Choose the modules that match your architecture (e.g., mvvm-core for basic ViewModel functionality, mvvm-flow for Flow support, or mvvm-livedata for LiveData support).

    // root build.gradle
    allprojects {
        repositories {
            mavenCentral()
        }
    }
    
    // project build.gradle
    dependencies {
        commonMainApi("dev.icerock.moko:mvvm-core:0.16.1")
        commonMainApi("dev.icerock.moko:mvvm-flow:0.16.1")
        commonMainApi("dev.icerock.moko:mvvm-livedata:0.16.1")
        // ... other modules as needed
    }
  7. Explore moko-mvvm samples and local project structure

    master

    The repository contains several directories that organize the library's components and usage examples:

    Library Modules

    • mvvm: The umbrella library.
    • mvvm-core: Contains core components like ViewModel and EventsDispatcher.
    • mvvm-livedata: Contains LiveData classes and extensions.
    • mvvm-databinding: Android-specific DataBinding support code.
    • mvvm-viewbinding: Android-specific ViewBinding support code.
    • mvvm-test: Test utilities for the library.

    Sample Applications

    • sample: Contains sample apps for Android and iOS, including the MPP (Multiplatform Project) library connected to them.
    • sample-declarative-ui: Contains sample apps utilizing modern declarative UI frameworks: Jetpack Compose for Android and SwiftUI for iOS.
  8. Export moko-mvvm to iOS framework

    master

    To use moko-mvvm classes directly from Swift, you must export the relevant artifacts in your Kotlin Multiplatform configuration. This ensures the symbols are visible in the generated iOS framework.

    kotlin {
        targets.withType(org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget::class.java).all {
            binaries.withType(org.jetbrains.kotlin.gradle.plugin.mpp.Framework::class.java).all {
                export("dev.icerock.moko:mvvm-core:0.16.1")
                export("dev.icerock.moko:mvvm-livedata:0.16.1")
                export("dev.icerock.moko:mvvm-livedata-resources:0.16.1")
                export("dev.icerock.moko:mvvm-state:0.16.1")
            }
        }
    }
  9. Manage resource states with ResourceState

    master

    The ResourceState sealed class is used to represent the lifecycle and outcome of a data-fetching operation or a resource's availability. It allows you to handle different UI states (like loading spinners, error messages, or empty states) in a type-safe manner.

    Available States

    • Success<T, E>: Contains the successfully retrieved data of type T.
    • Failed<T, E>: Contains an error of type E.
    • Loading<T, E>: Represents an ongoing operation.
    • Empty<T, E>: Represents a state where no data is available (e.g., an empty list or null result).
    // Example usage in a ViewModel or UI layer
    val state: ResourceState<String, Exception> = ResourceState.Success("Hello World")
    
    when (state) {
        is ResourceState.Success -> println("Data: ${state.data}")
        is ResourceState.Failed -> println("Error: ${state.error}")
        is ResourceState.Loading -> println("Loading...")
        is ResourceState.Empty -> println("No data found")
    }
  10. Validate user input with LiveData operators

    master

    Combine multiple LiveData objects to create derived state, such as enabling a button only when all input fields are valid.

    Using mergeWith

    Use email.mergeWith(password) { email, password -> ... } to react to changes in multiple fields simultaneously and return a single calculated value.

    Using all

    If you have multiple boolean LiveData objects representing validation states, you can combine them using listOf(isEmailValid, isPasswordValid).all(true). This returns a LiveData<Boolean> that is true only when all inputs are true.

    // Example: Merging two inputs
    class ValidationMergeViewModel : ViewModel() {
        val email: MutableLiveData<String> = MutableLiveData("")
        val password: MutableLiveData<String> = MutableLiveData("")
    
        val isLoginButtonEnabled: LiveData<Boolean> = email.mergeWith(password) { email, password ->
            email.isNotEmpty() && password.isNotEmpty()
        }
    }
    
    // Example: Combining multiple validation flags
    class ValidationAllViewModel : ViewModel() {
        val email: MutableLiveData<String> = MutableLiveData("")
        val password: MutableLiveData<String> = MutableLiveData("")
    
        private val isEmailValid: LiveData<Boolean> = email.map { it.isNotEmpty() }
        private val isPasswordValid: LiveData<Boolean> = password.map { it.isNotEmpty() }
        val isLoginButtonEnabled: LiveData<Boolean> = listOf(isEmailValid, isPasswordValid).all(true)
    }
  11. Reference: moko-mvvm dependency modules

    master

    The following modules are available for installation (version 0.16.1):

    Common Modules:

    • dev.icerock.moko:mvvm-core: Basic ViewModel, EventsDispatcher, and Dispatchers.UI.
    • dev.icerock.moko:mvvm-flow: Includes mvvm-core, adds CFlow for native and binding extensions.
    • dev.icerock.moko:mvvm-livedata: Includes mvvm-core, adds LiveData and extensions.
    • dev.icerock.moko:mvvm-state: Includes mvvm-livedata, adds ResourceState class and extensions.
    • dev.icerock.moko:mvvm-livedata-resources: Includes mvvm-core, adds moko-resources extensions for LiveData.
    • dev.icerock.moko:mvvm-flow-resources: Includes mvvm-core, adds moko-resources extensions for Flow.

    Compose Multiplatform Modules:

    • dev.icerock.moko:mvvm-compose: Includes mvvm-core, adds getViewModel for Compose Multiplatform.
    • dev.icerock.moko:mvvm-flow-compose: Includes mvvm-flow, adds binding extensions for Compose Multiplatform.
    • dev.icerock.moko:mvvm-livedata-compose: Includes mvvm-livedata, adds binding extensions for Compose Multiplatform.

    Android Specific Modules:

    • dev.icerock.moko:mvvm-livedata-material: Material library extensions.
    • dev.icerock.moko:mvvm-livedata-glide: Glide library extensions.
    • dev.icerock.moko:mvvm-livedata-swiperefresh: SwipeRefreshLayout library extensions.
    • dev.icerock.moko:mvvm-databinding: DataBinding support.
    • dev.icerock.moko:mvvm-viewbinding: ViewBinding support.

    Testing:

    • dev.icerock.moko:mvvm-test: Test utilities.
    // Example dependency selection
    commonMainApi("dev.icerock.moko:mvvm-core:0.16.1")
    commonMainApi("dev.icerock.moko:mvvm-flow:0.16.1")
    commonMainApi("dev.icerock.moko:mvvm-livedata:0.16.1")