androidkotlinandroid-jetpackkotlin-flowviewmodel-savedstate

Trying to expose SavedStateHandle.getLiveData() as MutableStateFlow, but the UI thread freezes


I am trying to use the following code:

suspend fun <T> SavedStateHandle.getStateFlow(
    key: String,
    initialValue: T? = get(key)
): MutableStateFlow<T?> = this.let { handle ->
    withContext(Dispatchers.Main.immediate) {
        val liveData = handle.getLiveData<T?>(key, initialValue).also { liveData ->
            if (liveData.value === initialValue) {
                liveData.value = initialValue
            }
        }

        val mutableStateFlow = MutableStateFlow(liveData.value)

        val observer: Observer<T?> = Observer { value ->
            if (value != mutableStateFlow.value) {
                mutableStateFlow.value = value
            }
        }

        liveData.observeForever(observer)

        mutableStateFlow.also { flow ->
            flow.onCompletion {
                withContext(Dispatchers.Main.immediate) {
                    liveData.removeObserver(observer)
                }
            }.onEach { value ->
                withContext(Dispatchers.Main.immediate) {
                    if (liveData.value != value) {
                        liveData.value = value
                    }
                }
            }.collect()
        }
    }
}

I am trying to use it like so:

    // in a Jetpack ViewModel
    var currentUserId: MutableStateFlow<String?>
        private set

    init {
        runBlocking(viewModelScope.coroutineContext) {
            currentUserId = state.getStateFlow("currentUserId", sessionManager.chatUserFlow.value?.uid)
            // <--- this line is never reached
        }
    }

UI thread freezes. I have a feeling it's because of collect() as I'm trying to create an internal subscription managed by the enclosing coroutine context, but I also need to get this StateFlow as a field. There's also the cross-writing of values (if either changes, update the other if it's a new value).

Overall, the issue seems to like on that collect() is suspending, as I never actually reach the line after getStateFlow().

Does anyone know a good way to create an "inner subscription" to a Flow, without ending up freezing the surrounding thread? The runBlocking { is needed so that I can synchronously assign the value to the field in the ViewModel constructor. (Is this even possible within the confines of 'structured concurrency'?)


Solution

  • I am in a similar position, but I do not want to modify the value through the LiveData (as in the accepted solution). I want to use only flow and leave LiveData as an implementation detail of the state handle.

    I also did not want to have a var and initialize it in the init block. I changed your code to satisfy both of these constraints and it does not block the UI thread. This would be the syntax:

     val currentUserId: MutableStateFlow<String?> = state.getStateFlow("currentUserId", viewModelScope, sessionManager.chatUserFlow.value?.uid)
    

    I provide a scope and use it to launch a coroutine that handles flow's onCompletion and collection. Here is the full code:

    fun <T> SavedStateHandle.getStateFlow(
        key: String,
        scope: CoroutineScope,
        initialValue: T? = get(key)
    ): MutableStateFlow<T?> = this.let { handle ->
        val liveData = handle.getLiveData<T?>(key, initialValue).also { liveData ->
            if (liveData.value === initialValue) {
                liveData.value = initialValue
            }
        }
        val mutableStateFlow = MutableStateFlow(liveData.value)
    
        val observer: Observer<T?> = Observer { value ->
            if (value != mutableStateFlow.value) {
                mutableStateFlow.value = value
            }
        }
        liveData.observeForever(observer)
    
        scope.launch {
            mutableStateFlow.also { flow ->
                flow.onCompletion {
                    withContext(Dispatchers.Main.immediate) {
                        liveData.removeObserver(observer)
                    }
                }.collect { value ->
                    withContext(Dispatchers.Main.immediate) {
                        if (liveData.value != value) {
                            liveData.value = value
                        }
                    }
                }
            }
        }
        mutableStateFlow
    }