Search code examples
swiftkotlinswiftuikotlin-multiplatform

How to access KMMShared MutableStateFlow object in Swift


I have a KmmShared singleton class with a MutableStateFlow which works fine with Android. I need to know the best way to access this Singleton class and it's MutableStateFlow variable with updates in SwiftUI application.

class MySingeltonClass {
    private val _selectedType: MutableStateFlow<MyEnum> = MutableStateFlow(getUserPrefsType())
    var selectedType = _selectedType.asStateFlow()

    private fun getUserPrefsType() : MyEnum {
        // Get Enum from DB
    }

    fun setType(scheme: MyEnum) {
        // Set Enum in DB
        _selectedType.value = scheme
    }

    companion object {
        private lateinit var instance: MySingeltonClass
        fun getInstance(): MySingeltonClass {
            if (!this::instance.isInitialized) {
                instance = MySingeltonClass()
            }
            return instance
        }

    }
}

enum class MyEnum {
    TYPEA, TYPEB, TYPEC;
}

If I can have a MySingeltonWrapperClass with a @Published var selectedType or a @ObservedObject private var selectedType is something that I'm trying to achieve in Swift.


Solution

  • It's kinda tricky topic. The main problems with Flow / StateFlow in SwiftUI applications lay in Kotlin <-> Swift interoperability. Because it has several limitations:

    Good articles to understand better: one, two

    There are several workarounds available:

    1. Wrap Flow in Kotlin class. Great example of this technique you can find in the official KMM sample repo.

    At first, author introduces new class CFlow:

    fun interface Closeable {
        fun close()
    }
    
    class CFlow<T: Any> internal constructor(private val origin: Flow<T>) : Flow<T> by origin {
        fun watch(block: (T) -> Unit): Closeable {
            val job = Job()
    
            onEach {
                block(it)
            }.launchIn(CoroutineScope(Dispatchers.Main + job))
    
            return Closeable { job.cancel() }
        }
    }
    
    internal fun <T: Any> Flow<T>.wrap(): CFlow<T> = CFlow(this)
    

    Then uses it in SwiftUI:

    
    class ObservableFeedStore: ObservableObject {
        @Published public var state: FeedState =  FeedState(progress: false, feeds: [], selectedFeed: nil)
        
        var stateWatcher : Closeable?
    
        init(store: FeedStore) {
            stateWatcher = self.store.watchState().watch { [weak self] state in
                self?.state = state
            }
        }
        
        deinit {
            stateWatcher?.close()
        }
    }
    
    

    2. Use special libraries, e.g. KMP-NativeCoroutines.

    When you setup this library you can create Swift AnyPublisher for your Kotlin Flow / StateFlow:

    let publisher = createPublisher(for: yourFlow)
    

    3. Don't use Flow/ StateFlow. You can use simple alternative for it. Take a look at Decompose project and it's Value class.

    abstract class Value<out T : Any> {
        abstract val value: T
        abstract fun subscribe(observer: (T) -> Unit)
        abstract fun unsubscribe(observer: (T) -> Unit)
    }
    

    You can still pretty easy use it in your Compose Application to get State, sample from Decompose:

    @Composable
    fun <T : Any> Value<T>.subscribeAsState(policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy()): State<T> {
        val state = remember(this, policy) { mutableStateOf(value, policy) }
    
        DisposableEffect(this) {
            val observer: (T) -> Unit = { state.value = it }
    
            subscribe(observer)
    
            onDispose {
                unsubscribe(observer)
            }
        }
    
        return state
    }
    

    and in SwiftUI you can use, ObservableValue helper from Decompose:

    public class ObservableValue<T : AnyObject> : ObservableObject {
        private let observableValue: Value<T>
    
        @Published
        var value: T
    
        private var observer: ((T) -> Void)?
        
        init(_ value: Value<T>) {
            observableValue = value
            self.value = observableValue.value
            observer = { [weak self] value in self?.value = value }
            observableValue.subscribe(observer: observer!)
        }
    
        deinit {
            observableValue.unsubscribe(observer: self.observer!)
        }
    }