Объединить два потока состояний в новый поток состояний

#kotlin #collections #kotlin-flow

#kotlin #Коллекции #kotlin-flow

Вопрос:

У меня есть два потока состояний. Возможно ли их объединить и получить новый поток состояний? Логически это должно быть возможно, потому что оба потока состояний имеют начальные значения, но, как я вижу, функция объединения возвращает только поток, а не поток состояний.

Комментарии:

1. Я думаю, вы найдете это medium.com/better-programming /…

2. Зачем вам конкретно нужен a StateFlow ?

3. @LouisWasserman В некоторых местах мне нужно просто прочитать текущее значение, не собирая его

Ответ №1:

Пока я создал функцию:

 fun <T1, T2, R> combineState(
        flow1: StateFlow<T1>,
        flow2: StateFlow<T2>,
        scope: CoroutineScope = GlobalScope,
        sharingStarted: SharingStarted = SharingStarted.Eagerly,
        transform: (T1, T2) -> R
): StateFlow<R> = combine(flow1, flow2) {
    o1, o2 -> transform.invoke(o1, o2)
}.stateIn(scope, sharingStarted, transform.invoke(flow1.value, flow2.value))
 

Комментарии:

1. Можете ли вы показать, как вы используете это в коде? Похоже, у меня это не работает

Ответ №2:

Вы могли бы использовать combine оператор, а затем использовать stateIn функцию для Flow того, что из этого получается.

Из stateIn документации в репозитории сопрограмм kotlinx:

stateIn Функция «Преобразует холодное Flow в горячее StateFlow , которое запускается в заданной области сопрограммы, разделяя самое последнее переданное значение из одного запущенного экземпляра восходящего потока с несколькими нисходящими подписчиками».

На момент написания этой статьи его подпись:

 fun <T> Flow<T>.stateIn(
  scope: CoroutineScope,
  started: SharingStarted,
  initialValue: T
): StateFlow<T> (source)
 

Таким образом, вы должны быть в состоянии выполнять любые необходимые преобразования с вашими Flow s, включая их объединение, а затем в конечном итоге использовать stateIn для преобразования их обратно в a StateFlow .

Это может выглядеть примерно так (возможно, создание калькулятора игровых очков Scrabble):

 val wordFlow = MutableStateFlow("Hi")
val pointFlow = MutableStateFlow(5)

val stateString = wordFlow.combine(pointFlow) { word, points ->
    "$word is worth $points points"
}.stateIn(viewModelScope, SharingStarted.Eagerly, "Default is worth 0 points")
 

stateString будет иметь тип StateFlow<String> , и вы успешно объединили два других StateFlows в один StateFlow .

Комментарии:

1. И, как всегда, примерно через 5 секунд после публикации я понимаю, что вы ответили на свой вопрос, используя stateIn . Увы, я оставлю описание для потомков.

Ответ №3:

Аналогичное решение для @Nikola Despotoski, но в виде функции расширения

 /**
 * Combines two [StateFlow]s into a single [StateFlow]
 */
fun <T1, T2, R> StateFlow<T1>.combineState(
  flow2: StateFlow<T2>,
  scope: CoroutineScope = GlobalScope,
  sharingStarted: SharingStarted = SharingStarted.Eagerly,
  transform: (T1, T2) -> R
): StateFlow<R> = combine(this, flow2) { o1, o2 -> transform.invoke(o1, o2) }
  .stateIn(scope, sharingStarted, transform.invoke(this.value, flow2.value))
 

Ответ №4:

Используйте combine operator, для объединения результатов обоих потоков требуется два потока и функция преобразования.

  val int = MutableStateFlow(2)
 val double = MutableStateFlow(1.8)
 int.combine(double){ i, d ->
            i   d             
 }.collect(::println)
 

Комментарии:

1. Мне нужен конкретный поток состояний, но эта функция возвращает поток

2. Теперь это ошибка в IDE: «Недостаточно информации для вывода переменной типа R», также запрашивается преобразование. Все еще новичок в этом, поэтому, пожалуйста, объясните или измените свой ответ, если библиотека обновилась. Спасибо!

Ответ №5:

Объединить n потоков состояний

 @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
inline fun <reified T, R> combineStateFlow(
    vararg flows: StateFlow<T>,
    scope: CoroutineScope = GlobalScope,
    sharingStarted: SharingStarted = SharingStarted.Eagerly,
    crossinline transform: (Array<T>) -> R
): StateFlow<R> = combine(flows = flows) {
    transform.invoke(it)
}.stateIn(
    scope = scope,
    started = sharingStarted,
    initialValue = transform.invoke(flows.map {
        it.value
    }.toTypedArray())
)
 

Используя:

 data class A(val a: String)
data class B(val b: Int)

private val test1 = MutableStateFlow(A("a"))
private val test2 = MutableStateFlow(B(2))
@Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
private val _isValidForm = combineStateFlow(
    flows = arrayOf(test1, test2),
    scope = viewModelScope
) { combinedFlows: Array<Any> ->
    combinedFlows.map {
        val doSomething = when (it) {
            is A -> true
            is B -> false
            else -> false
        }
    }
}
 

Суть

Ответ №6:

Вышеупомянутые решения используют stateIn() with GlobalScope и policy as Eagerly , что означает, что эти потоки состояний никогда не перестанут наблюдаться после создания, что может привести к проблемам.

Я уже упоминал подробности в этом блоге. Вместо этого создайте отдельный класс, который получает новый StateFlow :

 private class TransformedStateFlow<T>(
    private val getValue: () -> T,
    private val flow: Flow<T>
) : StateFlow<T> {
    
    override val replayCache: List<T> get() = listOf(value)
    override val value: T get() = getValue()

    override suspend fun collect(collector: FlowCollector<T>): Nothing =
        coroutineScope { flow.stateIn(this).collect(collector) }
}

/**
 * Returns [StateFlow] from [flow] having initial value from calculation of [getValue]
 */
fun <T> stateFlow(
    getValue: () -> T,
    flow: Flow<T>
): StateFlow<T> = TransformedStateFlow(getValue, flow)

/**
 * Combines all [stateFlows] and transforms them into another [StateFlow] with [transform]
 */
inline fun <reified T, R> combineStates(
    vararg stateFlows: StateFlow<T>,
    crossinline transform: (Array<T>) -> R
): StateFlow<R> = stateFlow(
    getValue = { transform(stateFlows.map { it.value }.toTypedArray()) },
    flow = combine(*stateFlows) { transform(it) }
)

/**
 * Variant of [combineStates] for combining 3 state flows
 */
inline fun <reified T1, reified T2, reified T3, R> combineStates(
    flow1: StateFlow<T1>,
    flow2: StateFlow<T2>,
    flow3: StateFlow<T3>,
    crossinline transform: (T1, T2, T3) -> R
) = combineStates(flow1, flow2, flow3) { (t1, t2, t3) ->
    transform(
        t1 as T1,
        t2 as T2,
        t3 as T3
    )
}

// Other variants for combining N StateFlows
 

После этого вы можете реализовать его в своем варианте использования. Например:

 private val isLoading = MutableStateFlow(false)
private val loggedInUser = MutableStateFlow<User?>(null)
private val error = MutableStateFlow<String?>(null)

// Combining these states to form a LoginState
val state: StateFlow<LoginState> = combineStates(isLoading, loggedInUser, error) { loading, user, errorMessage ->
  LoginState(loading, user, errorMessage)
}
 

Этот подход безопаснее, чем другие упомянутые подходы, поскольку он будет прослушивать StateFlow обновления только тогда, когда они фактически собираются (внутри области сопрограммы потребителя)