From b9244bd11af713c53817c4a3a001dd920a60a092 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Tue, 26 Jul 2022 20:16:47 +0300 Subject: [PATCH] Experimental: FlowLiveData --- .../kotatsu/reader/ui/ReaderViewModel.kt | 6 +- .../koitharu/kotatsu/utils/FlowLiveData.kt | 75 +++++++++++++++++++ 2 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/utils/FlowLiveData.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt index 1ea0e1313..ebb57e2a1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt @@ -33,7 +33,7 @@ import org.koitharu.kotatsu.reader.domain.ChaptersLoader import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.ui.pager.ReaderUiState import org.koitharu.kotatsu.utils.SingleLiveEvent -import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct +import org.koitharu.kotatsu.utils.asFlowLiveData import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.processLifecycleScope import org.koitharu.kotatsu.utils.ext.requireValue @@ -86,7 +86,7 @@ class ReaderViewModel @AssistedInject constructor( ) { manga, policy -> policy == ScreenshotsPolicy.BLOCK_ALL || (policy == ScreenshotsPolicy.BLOCK_NSFW && manga != null && manga.isNsfw) - }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, false) + }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false) val onZoomChanged = SingleLiveEvent() @@ -98,7 +98,7 @@ class ReaderViewModel @AssistedInject constructor( bookmarksRepository.observeBookmark(manga, state.chapterId, state.page) .map { it != null } } - }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, false) + }.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, false) init { loadImpl() diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/FlowLiveData.kt b/app/src/main/java/org/koitharu/kotatsu/utils/FlowLiveData.kt new file mode 100644 index 000000000..c44ce3795 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/utils/FlowLiveData.kt @@ -0,0 +1,75 @@ +package org.koitharu.kotatsu.utils + +import androidx.lifecycle.LiveData +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.StateFlow + +private const val DEFAULT_TIMEOUT = 5_000L + +/** + * Similar to a CoroutineLiveData but optimized for using within infinite flows + */ +class FlowLiveData( + private val flow: Flow, + defaultValue: T, + context: CoroutineContext = EmptyCoroutineContext, + private val timeoutInMs: Long = DEFAULT_TIMEOUT, +) : LiveData(defaultValue) { + + private val scope = CoroutineScope(Dispatchers.Main.immediate + context + SupervisorJob(context[Job])) + private var job: Job? = null + private var cancellationJob: Job? = null + + override fun onActive() { + super.onActive() + cancellationJob?.cancel() + cancellationJob = null + if (job?.isActive == true) { + return + } + job = scope.launch { + flow.collect(Collector()) + } + } + + override fun onInactive() { + super.onInactive() + cancellationJob?.cancel() + cancellationJob = scope.launch(Dispatchers.Main.immediate) { + delay(timeoutInMs) + if (!hasActiveObservers()) { + job?.cancel() + job = null + } + } + } + + private inner class Collector : FlowCollector { + + private var previousValue: Any? = value + + override suspend fun emit(value: T) { + if (previousValue != value) { + previousValue = value + withContext(Dispatchers.Main.immediate) { + setValue(value) + } + } + } + } +} + +fun Flow.asFlowLiveData( + context: CoroutineContext = EmptyCoroutineContext, + defaultValue: T, + timeoutInMs: Long = DEFAULT_TIMEOUT, +): LiveData = FlowLiveData(this, defaultValue, context, timeoutInMs) + +fun StateFlow.asFlowLiveData( + context: CoroutineContext = EmptyCoroutineContext, + timeoutInMs: Long = DEFAULT_TIMEOUT, +): LiveData = FlowLiveData(this, value, context, timeoutInMs)