melatonina
07/09/2021, 12:22 PMinterface BindableMutableStateFlow<T> : MutableStateFlow<T> {
val coroutineScope: CoroutineScope
fun bind(other: StateFlow<T>)
fun unbind()
}
and
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?William Reed
07/09/2021, 12:42 PMmelatonina
07/09/2021, 1:22 PMViewModelOne
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.job
was never set.William Reed
07/09/2021, 1:36 PMViewModelTwo
also mutate that MutableStateFlow
or is it just using it as a readable copy after it is bound ?melatonina
07/09/2021, 1:42 PMmap
.William Reed
07/09/2021, 1:49 PMcombine(emptyFlow(), yourOtherFlow)
but even thats weirdmelatonina
07/09/2021, 1:52 PMsourceFlow
, 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"William Reed
07/09/2021, 2:01 PMmelatonina
07/09/2021, 2:02 PMflatMapLatest
.William Reed
07/09/2021, 2:03 PMmelatonina
07/09/2021, 2:05 PM@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
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.William Reed
07/28/2021, 12:50 PMmelatonina
07/28/2021, 1:33 PMimport 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.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.