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

3.1k views Asked by At

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'?)

2

There are 2 answers

5
lotdrops On BEST ANSWER

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
}
0
EpicPandaForce On

EDIT:

// For more details, check: https://gist.github.com/marcellogalhardo/2a1ec56b7d00ba9af1ec9fd3583d53dc
fun <T> SavedStateHandle.getStateFlow(
    scope: CoroutineScope,
    key: String,
    initialValue: T
): MutableStateFlow<T> {
    val liveData = getLiveData(key, initialValue)
    val stateFlow = MutableStateFlow(initialValue)

    val observer = Observer<T> { value ->
        if (value != stateFlow.value) {
            stateFlow.value = value
        }
    }
    liveData.observeForever(observer)

    stateFlow.onCompletion {
        withContext(Dispatchers.Main.immediate) {
            liveData.removeObserver(observer)
        }
    }.onEach { value ->
        withContext(Dispatchers.Main.immediate) {
            if (liveData.value != value) {
                liveData.value = value
            }
        }
    }.launchIn(scope)

    return stateFlow
}

ORIGINAL:

You can piggyback over the built-in notification system in SavedStateHandle, so that

val state = savedStateHandle.getLiveData<State>(Key).asFlow().shareIn(viewModelScope, SharingStarted.Lazily)

...
savedStateHandle.set(Key, "someState")

The mutator happens not through methods of MutableLiveData, but through the SavedStateHandle that will update the LiveData (and therefore the flow) externally.