7

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

EpicPandaForce
  • 79,669
  • 27
  • 256
  • 428
  • Is there a reason you're not using the [`LiveData.asFlow()`](https://developer.android.com/reference/kotlin/androidx/lifecycle/package-summary#(androidx.lifecycle.LiveData).asFlow()) extension and then using `stateIn()`? – ianhanniballake Dec 03 '20 at 05:36
  • @ianhanniballake I am attempting to create a MutableStateFlow that I see as a MutableStateFlow, while hiding the LiveData of the SavedStateHandle as a mere implementation detail. If I used `asFlow`, I'd see a StateFlow and not a MutableStateFlow, I think. – EpicPandaForce Dec 03 '20 at 06:20
  • It appears I was missing a `launch {` in order to create a new coroutine that would collect the flow. – EpicPandaForce Feb 02 '21 at 09:02

2 Answers2

7

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.

EpicPandaForce
  • 79,669
  • 27
  • 256
  • 428
2

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
}
lotdrops
  • 300
  • 5
  • 16
  • 2
    I don't like that observeForever... What about something like this? https://gist.github.com/wesalvaro/5183fa8623bdbe3de4c425161a962ff9 – Wes Alvaro Aug 13 '21 at 09:29
  • 1
    Without the observe forever the state flow does not get updated with changes from the SavedStateHandle side, does it? Also, despite the observe forever the observer is removed when the flow completes in this code. – lotdrops Aug 16 '21 at 07:02
  • Why are we checking `if (value != mutableStateFlow.value)` before assigning it to the `MutableStateFlow` which is conflated by default? – Kshitij Patil Sep 30 '21 at 15:32
  • @WesAlvaro I recommend checking `LiveData.asFlow()` extension in Ktx as it also uses observeForever in the same manner, and with the new `repeatOnLifecycle` method (`flowWithLifecycle`?) it would have equivalent behavior – EpicPandaForce Nov 08 '21 at 18:52
  • I've seen patterns that use it, I still don't like prescribing it in general if it can be avoided. My use case (and would guess others') is simple and does not need to be updated with underlying changes to the `SavedStateHandle`. I just need to extract the value in the beginning into a `Flow` and save the value of it back to the `SavedStateHandle`. Looking at `LiveData.asFlow()` is not applicable in that case as I don't want/need a `LiveData`. – Wes Alvaro Sep 06 '22 at 05:58