Skip to content

Instantly share code, notes, and snippets.

@atomofiron
Last active May 17, 2021 16:34
Show Gist options
  • Save atomofiron/851e325d404d05b698f9544117b32504 to your computer and use it in GitHub Desktop.
Save atomofiron/851e325d404d05b698f9544117b32504 to your computer and use it in GitHub Desktop.
SharedFlow instead of LiveData
  1. LiveData with single events
  2. (update of 1) RxJava instead of LiveData in MVVM
  3. (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()
        }
    }
}
@atomofiron
Copy link
Author

визуализация Flow.debounceAfterFirst()

line

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment