Optimize LiveData from flows

This commit is contained in:
Koitharu
2020-12-28 07:20:14 +02:00
parent 7fd71c13f3
commit e674e0f36f
19 changed files with 80 additions and 77 deletions

View File

@@ -77,7 +77,7 @@ class DetailsViewModel(
}
)
}
}.flowOn(Dispatchers.Default).asLiveData(viewModelScope.coroutineContext)
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
init {
launchLoadingJob(Dispatchers.Default) {

View File

@@ -1,7 +1,6 @@
package org.koitharu.kotatsu.favourites.ui
import android.os.Bundle
import android.os.Parcelable
import android.view.*
import androidx.core.graphics.Insets
import androidx.core.view.updatePadding
@@ -27,7 +26,6 @@ class FavouritesContainerFragment : BaseFragment<FragmentFavouritesBinding>(),
private val editDelegate by lazy(LazyThreadSafetyMode.NONE) {
CategoriesEditDelegate(requireContext(), this)
}
private var adapterState: Parcelable? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -49,25 +47,6 @@ class FavouritesContainerFragment : BaseFragment<FragmentFavouritesBinding>(),
viewModel.onError.observe(viewLifecycleOwner, ::onError)
}
override fun onViewStateRestored(savedInstanceState: Bundle?) {
super.onViewStateRestored(savedInstanceState)
// (savedInstanceState?.getParcelable(KEY_ADAPTER_STATE) ?: adapterState)?.let {
// (binding.pager.adapter as FavouritesPagerAdapter).restoreState(it)
// }
}
override fun onDestroyView() {
adapterState = (binding.pager.adapter as? FavouritesPagerAdapter)?.saveState()
super.onDestroyView()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
adapterState = (bindingOrNull()?.pager?.adapter as? FavouritesPagerAdapter)?.saveState()
?: adapterState
outState.putParcelable(KEY_ADAPTER_STATE, adapterState)
}
override fun onWindowInsetsChanged(insets: Insets) {
binding.tabs.updatePadding(
left = insets.left,
@@ -132,8 +111,6 @@ class FavouritesContainerFragment : BaseFragment<FragmentFavouritesBinding>(),
companion object {
private const val KEY_ADAPTER_STATE = "adapter_state"
fun newInstance() = FavouritesContainerFragment()
}
}

View File

@@ -1,12 +1,11 @@
package org.koitharu.kotatsu.favourites.ui.categories
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.flowOn
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
class FavouritesCategoriesViewModel(
private val repository: FavouritesRepository
@@ -15,7 +14,7 @@ class FavouritesCategoriesViewModel(
private var reorderJob: Job? = null
val categories = repository.observeCategories()
.flowOn(Dispatchers.Default).asLiveData(viewModelScope.coroutineContext)
.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
fun createCategory(name: String) {
launchJob(Dispatchers.Default) {

View File

@@ -1,14 +1,13 @@
package org.koitharu.kotatsu.favourites.ui.categories.select
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.favourites.ui.categories.select.model.MangaCategoryItem
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
class MangaCategoriesViewModel(
private val manga: Manga,
@@ -26,7 +25,7 @@ class MangaCategoriesViewModel(
isChecked = it.id in checked
)
}
}.flowOn(Dispatchers.Default).asLiveData(viewModelScope.coroutineContext)
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
fun setChecked(categoryId: Long, isChecked: Boolean) {
launchJob(Dispatchers.Default) {

View File

@@ -4,7 +4,6 @@ import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.prefs.AppSettings
@@ -14,7 +13,7 @@ import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.list.ui.model.toErrorState
import org.koitharu.kotatsu.list.ui.model.toUi
import org.koitharu.kotatsu.utils.ext.asLiveData
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.onFirst
class FavouritesListViewModel(
@@ -43,7 +42,7 @@ class FavouritesListViewModel(
isLoading.postValue(false)
}.catch {
emit(listOf(it.toErrorState(canRetry = false)))
}.flowOn(Dispatchers.Default).asLiveData(viewModelScope.coroutineContext, listOf(LoadingState))
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
override fun onRefresh() = Unit

View File

@@ -15,7 +15,7 @@ import org.koitharu.kotatsu.history.domain.MangaWithHistory
import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.*
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.asLiveData
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.daysDiff
import org.koitharu.kotatsu.utils.ext.onFirst
import java.util.*
@@ -51,7 +51,7 @@ class HistoryListViewModel(
isLoading.postValue(false)
}.catch {
it.toErrorState(canRetry = false)
}.flowOn(Dispatchers.Default).asLiveData(viewModelScope.coroutineContext, listOf(LoadingState))
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
override fun onRefresh() = Unit

View File

@@ -50,6 +50,9 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
private var paginationListener: PaginationScrollListener? = null
private val spanResolver = MangaListSpanResolver()
private val spanSizeLookup = SpanSizeLookup()
private val listCommitCallback = Runnable {
spanSizeLookup.invalidateCache()
}
open val isSwipeRefreshEnabled = true
protected abstract val viewModel: MangaListViewModel
@@ -148,8 +151,7 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
}
private fun onListChanged(list: List<ListModel>) {
spanSizeLookup.invalidateCache()
listAdapter?.items = list
listAdapter?.setItems(list, listCommitCallback)
}
private fun onError(e: Throwable) {
@@ -178,7 +180,7 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
@CallSuper
protected open fun onLoadingStateChanged(isLoading: Boolean) {
binding.swipeRefreshLayout.isEnabled =
binding.swipeRefreshLayout.isEnabled = binding.swipeRefreshLayout.isRefreshing ||
isSwipeRefreshEnabled && !isLoading
if (!isLoading) {
binding.swipeRefreshLayout.isRefreshing = false

View File

@@ -2,7 +2,6 @@ package org.koitharu.kotatsu.list.ui
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
@@ -10,6 +9,7 @@ import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
abstract class MangaListViewModel(
private val settings: AppSettings
@@ -21,16 +21,20 @@ abstract class MangaListViewModel(
val gridScale = settings.observe()
.filter { it == AppSettings.KEY_GRID_SIZE }
.map { settings.gridSize / 100f }
.onStart { emit(settings.gridSize / 100f) }
.flowOn(Dispatchers.IO)
.asLiveData(viewModelScope.coroutineContext)
.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.IO) {
settings.gridSize / 100f
}
protected fun createListModeFlow() = settings.observe()
.filter { it == AppSettings.KEY_LIST_MODE }
.map { settings.listMode }
.onStart { emit(settings.listMode) }
.distinctUntilChanged()
.onEach { listMode.postValue(it) }
.onEach {
if (listMode.value != it) {
listMode.postValue(it)
}
}
abstract fun onRefresh()

View File

@@ -6,6 +6,7 @@ import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.ui.DateTimeAgo
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.MangaGridModel
import org.koitharu.kotatsu.list.ui.model.MangaListDetailedModel
@@ -38,6 +39,10 @@ class MangaListAdapter(
.addDelegate(ITEM_TYPE_EMPTY, emptyStateListAD())
}
fun setItems(list: List<ListModel>, commitCallback: Runnable) {
differ.submitList(list, commitCallback)
}
private class DiffCallback : DiffUtil.ItemCallback<ListModel>() {
override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel) = when {
@@ -50,6 +55,9 @@ class MangaListAdapter(
oldItem is MangaGridModel && newItem is MangaGridModel -> {
oldItem.id == newItem.id
}
oldItem is DateTimeAgo && newItem is DateTimeAgo -> {
oldItem == newItem
}
else -> oldItem.javaClass == newItem.javaClass
}

View File

@@ -2,13 +2,10 @@ package org.koitharu.kotatsu.local.ui
import android.content.Context
import android.net.Uri
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException
@@ -24,6 +21,7 @@ import org.koitharu.kotatsu.list.ui.model.toUi
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.utils.MediaStoreCompat
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.safe
import org.koitharu.kotatsu.utils.ext.sub
import java.io.IOException
@@ -51,9 +49,7 @@ class LocalListViewModel(
list.isEmpty() -> listOf(EmptyState(R.string.text_local_holder))
else -> list.toUi(mode)
}
}.onStart {
emit(listOf(LoadingState))
}.flowOn(Dispatchers.Default).asLiveData(viewModelScope.coroutineContext)
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
init {
onRefresh()

View File

@@ -1,9 +1,10 @@
package org.koitharu.kotatsu.main.ui
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import org.koitharu.kotatsu.base.domain.MangaProviderFactory
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
@@ -11,6 +12,7 @@ import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
class MainViewModel(
private val historyRepository: HistoryRepository,
@@ -24,9 +26,7 @@ class MainViewModel(
.filter { it == AppSettings.KEY_SOURCES_ORDER || it == AppSettings.KEY_SOURCES_HIDDEN }
.onStart { emit("") }
.map { MangaProviderFactory.getSources(settings, includeHidden = false) }
.distinctUntilChanged()
.flowOn(Dispatchers.Default)
.asLiveData(viewModelScope.coroutineContext)
.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
fun openLastReader() {
launchLoadingJob {

View File

@@ -5,7 +5,6 @@ import android.net.Uri
import android.util.LongSparseArray
import android.webkit.URLUtil
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
@@ -58,7 +57,7 @@ class ReaderViewModel(
chapterNumber = chapter?.number ?: 0,
chaptersTotal = chapters.size()
)
}.flowOn(Dispatchers.Default).asLiveData(viewModelScope.coroutineContext)
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
val content = MutableLiveData<ReaderContent>(ReaderContent(emptyList(), null))
val manga: Manga?
@@ -68,9 +67,7 @@ class ReaderViewModel(
.filter { it == AppSettings.KEY_READER_ANIMATION }
.map { settings.readerAnimation }
.onStart { emit(settings.readerAnimation) }
.distinctUntilChanged()
.flowOn(Dispatchers.IO)
.asLiveData(viewModelScope.coroutineContext)
.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.IO)
val onZoomChanged = SingleLiveEvent<Unit>()

View File

@@ -5,7 +5,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga
@@ -15,7 +14,7 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.list.ui.MangaFilterConfig
import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.*
import org.koitharu.kotatsu.utils.ext.asLiveData
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import java.util.*
class RemoteListViewModel(
@@ -49,7 +48,7 @@ class RemoteListViewModel(
result
}
}
}.flowOn(Dispatchers.Default).asLiveData(viewModelScope.coroutineContext, listOf(LoadingState))
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
init {
loadList(false)

View File

@@ -5,14 +5,13 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.*
import org.koitharu.kotatsu.utils.ext.asLiveData
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import java.util.*
class SearchViewModel(
@@ -46,7 +45,7 @@ class SearchViewModel(
result
}
}
}.flowOn(Dispatchers.Default).asLiveData(viewModelScope.coroutineContext, listOf(LoadingState))
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
init {
loadList(append = false)

View File

@@ -11,7 +11,7 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.*
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
import org.koitharu.kotatsu.utils.ext.asLiveData
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.onFirst
import java.util.*
@@ -46,7 +46,7 @@ class GlobalSearchViewModel(
result
}
}
}.flowOn(Dispatchers.Default).asLiveData(viewModelScope.coroutineContext, listOf(LoadingState))
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
init {
onRefresh()

View File

@@ -8,7 +8,6 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flowOn
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.model.TrackingLogItem
@@ -17,7 +16,7 @@ import org.koitharu.kotatsu.list.ui.model.LoadingFooter
import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.tracker.ui.model.toFeedItem
import org.koitharu.kotatsu.utils.ext.asLiveData
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.mapItems
class FeedViewModel(
@@ -41,7 +40,7 @@ class FeedViewModel(
isHasNextPage -> list + LoadingFooter
else -> list
}
}.flowOn(Dispatchers.Default).asLiveData(viewModelScope.coroutineContext, listOf(LoadingState))
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
init {
loadList(append = false)

View File

@@ -26,14 +26,40 @@ fun <T> LiveData<T>.observeWithPrevious(owner: LifecycleOwner, observer: Buffere
}
}
fun <T> Flow<T>.asLiveData(
fun <T> Flow<T>.asLiveDataDistinct(
context: CoroutineContext = EmptyCoroutineContext
): LiveData<T> = liveData(context) {
collect {
if (it != latestValue) {
emit(it)
}
}
}
fun <T> Flow<T>.asLiveDataDistinct(
context: CoroutineContext = EmptyCoroutineContext,
defaultValue: T
): LiveData<T> = liveData(context) {
): LiveData<T> = liveData(context, 0L) {
if (latestValue == null) {
emit(defaultValue)
}
collect {
emit(it)
if (it != latestValue) {
emit(it)
}
}
}
fun <T> Flow<T>.asLiveDataDistinct(
context: CoroutineContext = EmptyCoroutineContext,
defaultValue: suspend () -> T
): LiveData<T> = liveData(context) {
if (latestValue == null) {
emit(defaultValue())
}
collect {
if (it != latestValue) {
emit(it)
}
}
}

View File

@@ -1,13 +1,12 @@
package org.koitharu.kotatsu.widget.shelf
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.widget.shelf.model.CategoryItem
import java.util.*
@@ -27,7 +26,7 @@ class ShelfConfigViewModel(
CategoryItem(it.id, it.title, selectedId == it.id)
}
list
}.flowOn(Dispatchers.Default).asLiveData(viewModelScope.coroutineContext)
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
var checkedId: Long by selectedCategoryId::value
}