Coder Social home page Coder Social logo

trildadevcenter-kmm / kmm-viewmodel Goto Github PK

View Code? Open in Web Editor NEW

This project forked from rickclephas/kmp-observableviewmodel

0.0 0.0 0.0 458 KB

Library to share Kotlin ViewModels with SwiftUI

License: MIT License

Ruby 4.59% Objective-C 1.48% Kotlin 51.30% Swift 42.63%

kmm-viewmodel's Introduction

KMP-ObservableViewModel

A library (previously known as KMM-ViewModel) that allows you to use AndroidX/Kotlin ViewModels with SwiftUI.

Compatibility

You can use this library in any KMP project, but not all targets support AndroidX and/or SwiftUI interop:

Target Supported AndroidX SwiftUI
Android -
JVM -
iOS
macOS
tvOS -
watchOS -
linuxX64 -
linuxArm64 - -
mingwX64 - -
JS - -
Wasm - -

The latest version of the library uses Kotlin version 2.0.0.
Compatibility versions for older and/or preview Kotlin versions are also available:

Version Version suffix Kotlin Coroutines AndroidX Lifecycle
latest -kotlin-2.0.20-Beta1 2.0.20-Beta1 1.9.0-RC 2.8.0
latest no suffix 2.0.0 1.8.1 2.8.0
1.0.0-BETA-2 no suffix 1.9.24 1.8.1 2.8.0

Kotlin

Add the library to your shared Kotlin module and opt-in to the ExperimentalForeignApi:

kotlin {
    sourceSets {
        all {
            languageSettings.optIn("kotlinx.cinterop.ExperimentalForeignApi")
        }
        commonMain {
            dependencies {
                api("com.rickclephas.kmp:kmp-observableviewmodel-core:1.0.0-BETA-3")
            }
        }
    }
}

And create your ViewModels:

import com.rickclephas.kmp.observableviewmodel.ViewModel
import com.rickclephas.kmp.observableviewmodel.MutableStateFlow
import com.rickclephas.kmp.observableviewmodel.stateIn

open class TimeTravelViewModel: ViewModel() {

    private val clockTime = Clock.time

    /**
     * A [StateFlow] that emits the actual time.
     */
    val actualTime = clockTime.map { formatTime(it) }
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), "N/A")

    private val _travelEffect = MutableStateFlow<TravelEffect?>(viewModelScope, null)
    /**
     * A [StateFlow] that emits the applied [TravelEffect].
     */
    val travelEffect = _travelEffect.asStateFlow()
}

As you might notice it isn't much different from an AndroidX ViewModel.
We are obviously using a different ViewModel superclass:

- import androidx.lifecycle.ViewModel
+ import com.rickclephas.kmp.observableviewmodel.ViewModel

open class TimeTravelViewModel: ViewModel() {

But besides that there are only 2 minor differences.
The first being a different import for stateIn:

- import kotlinx.coroutines.flow.stateIn
+ import com.rickclephas.kmp.observableviewmodel.stateIn

        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), "N/A")

And the second being a different MutableStateFlow constructor:

- import kotlinx.coroutines.flow.MutableStateFlow
+ import com.rickclephas.kmp.observableviewmodel.MutableStateFlow

-    private val _travelEffect = MutableStateFlow<TravelEffect?>(null)
+    private val _travelEffect = MutableStateFlow<TravelEffect?>(viewModelScope, null)

These minor differences will make sure that state changes are propagated to SwiftUI.

Note

viewModelScope is a wrapper around the actual CoroutineScope which can be accessed via the ViewModelScope.coroutineScope property.

KMP-NativeCoroutines

I highly recommend you to use the @NativeCoroutinesState annotation from KMP-NativeCoroutines to turn your StateFlows into properties in Swift:

@NativeCoroutinesState
val travelEffect = _travelEffect.asStateFlow()

Checkout the KMP-NativeCoroutines README for more information and installation instructions.

Alternative

Alternatively you can create extension properties in your iOS/Apple source-set yourself:

val TimeTravelViewModel.travelEffectValue: TravelEffect?
    get() = travelEffect.value

Android

Use the view model like you would any other AndroidX ViewModel:

class TimeTravelFragment: Fragment(R.layout.fragment_time_travel) {
    private val viewModel: TimeTravelViewModel by viewModels()
}

Swift

After you have configured your shared Kotlin module and created a ViewModel it's time to configure your Swift project.
Start by adding the Swift package to your Package.swift file:

dependencies: [
    .package(url: "https://github.com/rickclephas/KMP-ObservableViewModel.git", from: "1.0.0-BETA-3")
]

Or add it in Xcode by going to File > Add Packages... and providing the URL: https://github.com/rickclephas/KMP-ObservableViewModel.git.

CocoaPods

If you like you can also use CocoaPods instead of SPM:

pod 'KMPObservableViewModelSwiftUI', '1.0.0-BETA-3'

Create a KMPObservableViewModel.swift file with the following contents:

import KMPObservableViewModelCore
import shared // This should be your shared KMP module

extension Kmp_observableviewmodel_coreViewModel: ViewModel { }

After that you can use your view model almost as if it were an ObservableObject.
Just use the view model specific property wrappers and functions:

ObservableObject ViewModel
@StateObject @StateViewModel
@ObservedObject @ObservedViewModel
@EnvironmentObject @EnvironmentViewModel
environmentObject(_:) environmentViewModel(_:)

E.g. to use the TimeTravelViewModel as a StateObject:

import SwiftUI
import KMPObservableViewModelSwiftUI
import shared // This should be your shared KMP module

struct ContentView: View {
    @StateViewModel var viewModel = TimeTravelViewModel()
}

It's also possible to subclass your view model in Swift:

import Combine
import shared // This should be your shared KMP module

class TimeTravelViewModel: shared.TimeTravelViewModel {
    @Published var isResetDisabled: Bool = false
}

Child view models

You'll need some additional logic if your ViewModels expose child view models.

First make sure to use the NativeCoroutinesRefinedState annotation instead of the NativeCoroutinesState annotation:

class MyParentViewModel: ViewModel() {
    @NativeCoroutinesRefinedState
    val myChildViewModel: StateFlow<MyChildViewModel?> = MutableStateFlow(null)
}

After that you should create a Swift extension property using the childViewModel(at:) function:

extension MyParentViewModel {
    var myChildViewModel: MyChildViewModel? {
        childViewModel(at: \.__myChildViewModel)
    }
}

This will prevent your Swift view models from being deallocated too soon.

Note

For lists, sets and dictionaries containing view models there is childViewModels(at:).

Cancellable ViewModel

When subclassing your Kotlin ViewModel in Swift you might experience some issues in the way those view models are cleared.

An example of such an issue is when you are using a Combine publisher to observe a Flow through KMP-NativeCoroutines:

import Combine
import KMPNativeCoroutinesCombine
import shared // This should be your shared KMP module

class TimeTravelViewModel: shared.TimeTravelViewModel {

    private var cancellables = Set<AnyCancellable>()

    override init() {
        super.init()
        createPublisher(for: currentTimeFlow)
            .assertNoFailure()
            .sink { time in print("It's \(time)") }
            .store(in: &cancellables)
    }
}

Since currentTimeFlow is a StateFlow we don't ever expect it to fail, which is why we are using the assertNoFailure. However, in this case you'll notice that the publisher will fail with a JobCancellationException.

The problem here is that before the TimeTravelViewModel is deinited it will already be cleared. Meaning the viewModelScope is cancelled and onCleared is called. This results in the Combine publisher outliving the underlying StateFlow collection.

To solve such issues you should have your Swift view model conform to Cancellable and perform the required cleanup in the cancel function:

class TimeTravelViewModel: shared.TimeTravelViewModel, Cancellable {
    func cancel() {
        cancellables = []
    }
}

KMP-ObservableViewModel will make sure to call the cancel function before the ViewModel is being cleared.

kmm-viewmodel's People

Contributors

rickclephas avatar chrisbanes avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.