From a494ebd79193f8743c4c4cfcd6acc8070c066be9 Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Sat, 8 Aug 2026 20:04:41 +0600 Subject: [PATCH] Make HistoryViewModel collect history only while subscribed (#3728) Assisted-by: Claude:claude-opus-5 --- .../kanade/tachiyomi/ui/history/HistoryTab.kt | 4 +- .../tachiyomi/ui/history/HistoryViewModel.kt | 65 +++++++++++-------- 2 files changed, 40 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryTab.kt index 7720aca7f..fd0b0d3ea 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryTab.kt @@ -7,9 +7,9 @@ import androidx.compose.animation.graphics.vector.AnimatedImageVector import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.platform.LocalContext +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.Navigator @@ -63,7 +63,7 @@ data object HistoryTab : Tab { val navigator = LocalNavigator.currentOrThrow val context = LocalContext.current val viewModel = viewModel() - val state by viewModel.state.collectAsState() + val state by viewModel.state.collectAsStateWithLifecycle() HistoryScreen( state = state, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryViewModel.kt index 7056efb6d..761210730 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryViewModel.kt @@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.history import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Immutable +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import eu.kanade.core.util.insertSeparators import eu.kanade.domain.manga.interactor.UpdateManga @@ -11,16 +12,21 @@ import eu.kanade.tachiyomi.util.lang.toLocalDate import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.WhileSubscribed import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import logcat.LogPriority -import mihon.core.viewmodel.StateViewModel import tachiyomi.core.common.preference.CheckboxState import tachiyomi.core.common.preference.mapAsCheckboxState import tachiyomi.core.common.util.lang.launchIO @@ -42,6 +48,7 @@ import tachiyomi.domain.manga.model.MangaWithChapterCount import tachiyomi.domain.source.service.SourceManager import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import kotlin.time.Duration.Companion.seconds class HistoryViewModel( private val addTracks: AddTracks = Injekt.get(), @@ -56,28 +63,36 @@ class HistoryViewModel( private val updateManga: UpdateManga = Injekt.get(), val snackbarHostState: SnackbarHostState = SnackbarHostState(), private val sourceManager: SourceManager = Injekt.get(), -) : StateViewModel(State()) { +) : ViewModel() { private val _events: Channel = Channel(Channel.UNLIMITED) val events: Flow = _events.receiveAsFlow() - init { - viewModelScope.launch { - state.map { it.searchQuery } + private val searchQuery = MutableStateFlow(null) + + private val dialog = MutableStateFlow(null) + + private val history = searchQuery + .flatMapLatest { query -> + getHistory.subscribe(query ?: "") .distinctUntilChanged() - .flatMapLatest { query -> - getHistory.subscribe(query ?: "") - .distinctUntilChanged() - .catch { error -> - logcat(LogPriority.ERROR, error) - _events.send(Event.InternalError) - } - .map { it.toHistoryUiModels() } - .flowOn(Dispatchers.IO) + .catch { error -> + logcat(LogPriority.ERROR, error) + _events.send(Event.InternalError) } - .collect { newList -> mutableState.update { it.copy(list = newList) } } + .map { it.toHistoryUiModels() } + .flowOn(Dispatchers.IO) } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5.seconds), emptyList()) + + val state: StateFlow = combine( + searchQuery, + history, + dialog, + ) { searchQuery, history, dialog -> + State(searchQuery = searchQuery, list = history, dialog = dialog) } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5.seconds), State()) private fun List.toHistoryUiModels(): List { return map { HistoryUiModel.Item(it) } @@ -128,11 +143,11 @@ class HistoryViewModel( } fun updateSearchQuery(query: String?) { - mutableState.update { it.copy(searchQuery = query) } + searchQuery.update { query } } fun setDialog(dialog: Dialog?) { - mutableState.update { it.copy(dialog = dialog) } + this.dialog.update { dialog } } /** @@ -175,7 +190,7 @@ class HistoryViewModel( val duplicates = getDuplicateLibraryManga(manga) if (duplicates.isNotEmpty()) { - mutableState.update { it.copy(dialog = Dialog.DuplicateManga(manga, duplicates)) } + dialog.update { Dialog.DuplicateManga(manga, duplicates) } return@launchIO } @@ -215,21 +230,17 @@ class HistoryViewModel( } fun showMigrateDialog(target: Manga, current: Manga) { - mutableState.update { currentState -> - currentState.copy(dialog = Dialog.Migrate(target = target, current = current)) - } + dialog.update { Dialog.Migrate(target = target, current = current) } } fun showChangeCategoryDialog(manga: Manga) { viewModelScope.launch { val categories = getCategories() val selection = getMangaCategoryIds(manga) - mutableState.update { currentState -> - currentState.copy( - dialog = Dialog.ChangeCategory( - manga = manga, - initialSelection = categories.mapAsCheckboxState { it.id in selection }, - ), + dialog.update { + Dialog.ChangeCategory( + manga = manga, + initialSelection = categories.mapAsCheckboxState { it.id in selection }, ) } }