I want to be able to easily bind StateFlow, in par...
# coroutines
m
I want to be able to easily bind StateFlow, in particular as part of Android ViewModels. I'm going to use this code:
Copy code
interface BindableMutableStateFlow<T> : MutableStateFlow<T> {
    val coroutineScope: CoroutineScope

    fun bind(other: StateFlow<T>)
    fun unbind()
}
and
Copy code
fun <T> ViewModel.BindableMutableStateFlow(initialValue: T) : BindableMutableStateFlow<T> =
    MutableStateFlow<T>(initialValue).let { _stateFlow ->
        object : BindableMutableStateFlow<T>, MutableStateFlow<T> by _stateFlow {
            override val coroutineScope: CoroutineScope get() = viewModelScope

            private var job: Job? = null
            override fun bind(other: StateFlow<T>) {
                unbind()
                job = viewModelScope.launch {
                    other.collect {
                        value = it
                    }
                }
            }

            override fun unbind() {
                job?.cancel()
                job = null
            }
        }
    }
Am I duplicating or misusing (under-using) any existing Kotlin API? Do you see any problems with this code?
w
could you explain your use case a little more?
m
I have a
ViewModelOne
which exposes one or more states with a
StateFlow
. A
ViewModelTwo
depends on one of the states of
ViewModelOne
. I don't want to
ViewModelTwo
to know about
ViewModelOne
. I just want to bind one of its states to another state from somewhere else, just like I would do with
LiveData
. Possibly rebinding it to something else.
I edited the code because
job
was never set.
w
will
ViewModelTwo
also mutate that
MutableStateFlow
or is it just using it as a readable copy after it is bound ?
m
It uses it as a readable copy to compute other states with
map
.
w
maybe it would be simpler to just pass a reference directly to the original flow? i’m not sure what you gain from the ‘bind’ approach
if you did want to stay with the bind approach maybe you could
combine(emptyFlow(), yourOtherFlow)
but even thats weird
m
Suppose I have an observable property called
sourceFlow
, as you suggest. When
sourceFlow
changes, I have to update all depending properties in order to depend on the new source. Otherwise I could create a "link"
StateFlow
, let the depending properties depend on it, and when
sourceFlow
changes, I stop collecting the values of the old source and start collecting the values of the new source. That's exactly what I encapsulated in that "bindable flow"
The main feature of this "bindable state flow" is not that it can be bound. It's that the binding can be changed.
w
so the binding can be changed and those observing it are still subscribed
interesting
m
It looks like this could be expressed in terms of
flatMapLatest
.
w
how so?
m
It would just eliminate the job-cancelling machinery from my code, not the need for the "bindable state flow". I'm still wondering if I missing anything. I'll post the code last.
Something like this:
Copy code
@ExperimentalCoroutinesApi
fun <T> BindableMutableStateFlow2(
    coroutineScope: CoroutineScope,
    initialValue: T
) : BindableMutableStateFlow<T> = MutableStateFlow<StateFlow<T>>(MutableStateFlow<T>(initialValue)).let { flows ->
    flows.flatMapLatest { it }.stateIn(coroutineScope, SharingStarted.Eagerly, initialValue).let { impl ->
        object : BindableMutableStateFlow<T>, StateFlow<T> by impl {
            override val coroutineScope: CoroutineScope
                get() = coroutineScope

            override fun bind(other: StateFlow<T>) {
                flows.value = other
            }
            override fun unbind() {
                flows.value = MutableStateFlow<T>(impl.value)
            }
        }
    }
}


@ExperimentalCoroutinesApi
fun <T> ViewModel.BindableMutableStateFlow2(initialValue: T) : BindableMutableStateFlow<T> =
    BindableMutableStateFlow2(viewModelScope, initialValue)
with
Copy code
interface BindableMutableStateFlow<T> : StateFlow<T> {
    val coroutineScope: CoroutineScope

    fun bind(other: StateFlow<T>)
    fun unbind()
}
They are not equivalent, but very similar. They are equivalent for the basic use case.
w
@melatonina what did you end up settling on for this?I think i have a similar use case coming up
m
@William Reed This is my current implementation of both versions:
Copy code
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.ExperimentalCoroutinesApi

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope

interface BindableMutableStateFlow<T> : MutableStateFlow<T> {
    val coroutineScope: CoroutineScope

    fun bind(other: StateFlow<T>)
    fun unbind()
}

class BindableMutableStateFlowImpl<T> private constructor(
    override val coroutineScope: CoroutineScope,
    private val _stateFlow: MutableStateFlow<T>
) : BindableMutableStateFlow<T>, MutableStateFlow<T> by _stateFlow {
    constructor(
        coroutineScope: CoroutineScope,
        initialValue: T
    ) : this(coroutineScope, MutableStateFlow<T>(initialValue))

    private var job: Job? = null
    override fun bind(other: StateFlow<T>) {
        unbind()
        job = coroutineScope.launch {
            other.collect {
                value = it
            }
        }
    }

    override fun unbind() {
        job?.cancel()
    }
}

fun <T> BindableMutableStateFlow(coroutineScope: CoroutineScope, initialValue: T) : BindableMutableStateFlow<T> =
    MutableStateFlow<T>(initialValue).let { _stateFlow ->
        object : BindableMutableStateFlow<T>, MutableStateFlow<T> by _stateFlow {
            override val coroutineScope: CoroutineScope get() = coroutineScope

            private var job: Job? = null
            override fun bind(other: StateFlow<T>) {
                unbind()
                job = coroutineScope.launch {
                    other.collect {
                        value = it
                    }
                }
            }

            override fun unbind() {
                job?.cancel()
            }
        }
    }

fun <T> ViewModel.BindableMutableStateFlow(initialValue: T) : BindableMutableStateFlow<T> =
    BindableMutableStateFlow(viewModelScope, initialValue)



interface BindableStateFlow<T> : StateFlow<T> {
    val coroutineScope: CoroutineScope

    fun bind(other: StateFlow<T>)
    fun unbind()
}

@ExperimentalCoroutinesApi
fun <T> BindableStateFlow(
    coroutineScope: CoroutineScope,
    initialValue: T
) : BindableStateFlow<T> = MutableStateFlow<StateFlow<T>>(MutableStateFlow<T>(initialValue)).let { flows ->
    flows.flatMapLatest { it }.stateIn(coroutineScope, SharingStarted.Eagerly, initialValue).let { impl ->
        object : BindableStateFlow<T>, StateFlow<T> by impl {
            override val coroutineScope: CoroutineScope
                get() = coroutineScope

            override fun bind(other: StateFlow<T>) {
                flows.value = other
            }
            override fun unbind() {
                flows.value = MutableStateFlow<T>(impl.value)
            }
        }
    }
}


@ExperimentalCoroutinesApi
fun <T> ViewModel.BindableStateFlow(initialValue: T) : BindableStateFlow<T> =
    BindableStateFlow(viewModelScope, initialValue)
As you can see, I called the first
BindableMutableStateFlow
and the second
BindableStateFlow
. You can also use the first
StateFlow
without binding it, as it's just a
MutableStateFlow
.
BindableStateFlow
is just a place where you can bind other flows. I think that, in most cases, the second solution is the more elegant, because currently there is nothing to prevent you from setting the value of a
BindableMutableStateFlow
manually, even if it's bound to something. Exposing the
CoroutineScope
is not actually needed. You could remove that from the interface definitions, if you want. I'm still not sure that I'm not just disregarding some piece of
StateFlow
API which renders all this avoidable, but I'm using it, in the meantime.
🙌 1
The
BindableMutableStateFlowImpl<T>
class, I don't use it either, as I create an anonymous object in the builder. So you could drop that piece of code, too.