- LiveData with single events
- (update of 1) RxJava instead of LiveData in MVVM
- (update of 2) Save State By Using RxJava or Kotlin Flow
После поверхностного ознакомления со статьями пришёл к выводу, что нужно попробовать отказаться от LiveData
в пользу SharedFlow
.
- доставка всех данных/событий
- автоматическая отмена
CoroutineScope
(upd: уlifecycle
уже появился свой) - работа в нужном потоке
import androidx.fragment.app.Fragment
import androidx.lifecycle.coroutineScope
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import java.lang.NullPointerException
fun <T> sharedFlow(single: Boolean = false): MutableSharedFlow<T> {
val replayLimit = if (single) 0 else 1
return MutableSharedFlow(replayLimit, extraBufferCapacity = Int.MAX_VALUE)
}
fun <T> sharedFlow(value: T, single: Boolean = false): MutableSharedFlow<T> {
val flow = sharedFlow<T>(single)
flow.value = value
return flow
}
val <T> SharedFlow<T>.isInitialized: Boolean get() = replayCache.isNotEmpty()
val <T> SharedFlow<T>.value: T get() = replayCache.last()
var <T> MutableSharedFlow<T>.value: T
get() = replayCache.last()
set(value) {
@Suppress("EXPERIMENTAL_API_USAGE")
GlobalScope.launch(start = CoroutineStart.UNDISPATCHED) {
emit(value)
}
}
fun <T> MutableSharedFlow<T>.emitLast() = when {
isInitialized -> value = value
else -> throw NullPointerException("The flow doesn't contains any data to emit.")
}
fun <T> MutableSharedFlow<T>.emitNow(value: T) {
this.value = value
}
operator fun MutableSharedFlow<Unit>.invoke() = emitNow(Unit)
fun <T> Flow<T>.collect(scope: CoroutineScope, collector: suspend (value: T) -> Unit) {
scope.launch {
collect(collector)
}
}
fun <T> Fragment.viewCollect(
flow: SharedFlow<T>,
immediate: Boolean = true,
collector: suspend (value: T) -> Unit
) {
val context = when {
immediate -> Dispatchers.Main.immediate
else -> Dispatchers.Main
}
viewLifecycleOwner.lifecycle.coroutineScope.launch(context) {
flow.collect(collector)
}
}
fun <T> Fragment.fragmentCollect(
flow: SharedFlow<T>,
immediate: Boolean = true,
collector: suspend (value: T) -> Unit
) {
val context = when {
immediate -> Dispatchers.Main.immediate
else -> Dispatchers.Main
}
lifecycle.coroutineScope.launch(context) {
flow.collect(collector)
}
}
Под капотом в lifecycle.coroutineScope
используется диспатчер Main.immediate
, но при некоторых кейсах может понадобиться Main
, поэтому контекст указывается явно.
Если в определённый момент генерируется несколько состояний для одного экрана, некоторая очередь, обрабатывать их все не обязательно, достаточно показать первое, это важно, если экран только что был открыт, и последнее, после которого некоторый период времени новых состояний не появлялось.
fun <T> Flow<T>.debounceAfterFirst(duration: Long = 100L): Flow<T> = flow {
var delayedJob: Job? = null
var delayedValue: T? = null
var hasDelayedValue = false
fun post() {
delayedJob?.cancel()
delayedJob = CoroutineScope(Dispatchers.Default).launch {
delay(duration)
if (hasDelayedValue) {
hasDelayedValue = false
emit(delayedValue as T)
delayedValue = null
}
}
}
collect { value ->
if (delayedJob?.isActive == true) {
delayedValue = value
hasDelayedValue = true
post()
} else {
emit(value)
post()
}
}
}
визуализация Flow.debounceAfterFirst()