Replace RxJava in ChapterLoader and ReaderViewModel (#8915)

* Replace RxJava in ChapterLoader

* Don't swallow CancellationException

* Simplify loadChapter behavior

* Add error handling to loadAdjacent
This commit is contained in:
Two-Ai 2023-01-14 18:22:27 -05:00 committed by GitHub
parent e7937fe562
commit 62480f090b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 75 additions and 85 deletions

View file

@ -6,7 +6,6 @@ import android.net.Uri
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import eu.kanade.core.util.asFlow
import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.chapter.interactor.GetChapterByMangaId import eu.kanade.domain.chapter.interactor.GetChapterByMangaId
import eu.kanade.domain.chapter.interactor.UpdateChapter import eu.kanade.domain.chapter.interactor.UpdateChapter
@ -60,17 +59,15 @@ import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.cacheImageDir import eu.kanade.tachiyomi.util.storage.cacheImageDir
import eu.kanade.tachiyomi.util.system.isOnline import eu.kanade.tachiyomi.util.system.isOnline
import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -79,9 +76,6 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import logcat.LogPriority import logcat.LogPriority
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.Date import java.util.Date
@ -141,11 +135,6 @@ class ReaderViewModel(
*/ */
private var chapterReadStartTime: Long? = null private var chapterReadStartTime: Long? = null
/**
* Subscription to prevent setting chapters as active from multiple threads.
*/
private var activeChapterSubscription: Subscription? = null
private var chapterToDownload: Download? = null private var chapterToDownload: Download? = null
/** /**
@ -279,45 +268,39 @@ class ReaderViewModel(
val source = sourceManager.getOrStub(manga.source) val source = sourceManager.getOrStub(manga.source)
loader = ChapterLoader(context, downloadManager, downloadProvider, manga, source) loader = ChapterLoader(context, downloadManager, downloadProvider, manga, source)
getLoadObservable(loader!!, chapterList.first { chapterId == it.chapter.id }) loadChapter(loader!!, chapterList.first { chapterId == it.chapter.id })
.asFlow()
.first()
Result.success(true) Result.success(true)
} else { } else {
// Unlikely but okay // Unlikely but okay
Result.success(false) Result.success(false)
} }
} catch (e: Throwable) { } catch (e: Throwable) {
if (e is CancellationException) {
throw e
}
Result.failure(e) Result.failure(e)
} }
} }
} }
/** /**
* Returns an observable that loads the given [chapter] with this [loader]. This observable * Loads the given [chapter] with this [loader] and updates the currently active chapters.
* handles main thread synchronization and updating the currently active chapters on * Callers must handle errors.
* [viewerChaptersRelay], however callers must ensure there won't be more than one
* subscription active by unsubscribing any existing [activeChapterSubscription] before.
* Callers must also handle the onError event.
*/ */
private fun getLoadObservable( private suspend fun loadChapter(
loader: ChapterLoader, loader: ChapterLoader,
chapter: ReaderChapter, chapter: ReaderChapter,
): Observable<ViewerChapters> { ): ViewerChapters {
return loader.loadChapter(chapter) loader.loadChapter(chapter)
.andThen(
Observable.fromCallable {
val chapterPos = chapterList.indexOf(chapter)
ViewerChapters( val chapterPos = chapterList.indexOf(chapter)
val newChapters = ViewerChapters(
chapter, chapter,
chapterList.getOrNull(chapterPos - 1), chapterList.getOrNull(chapterPos - 1),
chapterList.getOrNull(chapterPos + 1), chapterList.getOrNull(chapterPos + 1),
) )
},
) withUIContext {
.observeOn(AndroidSchedulers.mainThread())
.doOnNext { newChapters ->
mutableState.update { mutableState.update {
// Add new references first to avoid unnecessary recycling // Add new references first to avoid unnecessary recycling
newChapters.ref() newChapters.ref()
@ -327,6 +310,7 @@ class ReaderViewModel(
it.copy(viewerChapters = newChapters) it.copy(viewerChapters = newChapters)
} }
} }
return newChapters
} }
/** /**
@ -339,17 +323,19 @@ class ReaderViewModel(
logcat { "Loading ${chapter.chapter.url}" } logcat { "Loading ${chapter.chapter.url}" }
withIOContext { withIOContext {
getLoadObservable(loader, chapter) try {
.asFlow() loadChapter(loader, chapter)
.catch { logcat(LogPriority.ERROR, it) } } catch (e: Throwable) {
.first() if (e is CancellationException) {
throw e
}
logcat(LogPriority.ERROR, e)
}
} }
} }
/** /**
* Called when the user is going to load the prev/next chapter through the menu button. It * Called when the user is going to load the prev/next chapter through the menu button.
* sets the [isLoadingAdjacentChapterRelay] that the view uses to prevent any further
* interaction until the chapter is loaded.
*/ */
private suspend fun loadAdjacent(chapter: ReaderChapter) { private suspend fun loadAdjacent(chapter: ReaderChapter) {
val loader = loader ?: return val loader = loader ?: return
@ -357,13 +343,19 @@ class ReaderViewModel(
logcat { "Loading adjacent ${chapter.chapter.url}" } logcat { "Loading adjacent ${chapter.chapter.url}" }
mutableState.update { it.copy(isLoadingAdjacentChapter = true) } mutableState.update { it.copy(isLoadingAdjacentChapter = true) }
try {
withIOContext { withIOContext {
getLoadObservable(loader, chapter) loadChapter(loader, chapter)
.asFlow()
.first()
} }
} catch (e: Throwable) {
if (e is CancellationException) {
throw e
}
logcat(LogPriority.ERROR, e)
} finally {
mutableState.update { it.copy(isLoadingAdjacentChapter = false) } mutableState.update { it.copy(isLoadingAdjacentChapter = false) }
} }
}
/** /**
* Called when the viewers decide it's a good time to preload a [chapter] and improve the UX so * Called when the viewers decide it's a good time to preload a [chapter] and improve the UX so
@ -393,12 +385,15 @@ class ReaderViewModel(
val loader = loader ?: return val loader = loader ?: return
withIOContext { withIOContext {
try {
loader.loadChapter(chapter) loader.loadChapter(chapter)
.doOnCompleted { eventChannel.trySend(Event.ReloadViewerChapters) } } catch (e: Throwable) {
.onErrorComplete() if (e is CancellationException) {
.toObservable<Unit>() throw e
.asFlow() }
.firstOrNull() return@withIOContext
}
eventChannel.trySend(Event.ReloadViewerChapters)
} }
} }

View file

@ -11,11 +11,9 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.util.lang.awaitSingle
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.system.logcat
import rx.Completable
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
/** /**
* Loader used to retrieve the [PageLoader] for a given chapter. * Loader used to retrieve the [PageLoader] for a given chapter.
@ -29,43 +27,40 @@ class ChapterLoader(
) { ) {
/** /**
* Returns a completable that assigns the page loader and loads the its pages. It just * Assigns the chapter's page loader and loads the its pages. Returns immediately if the chapter
* completes if the chapter is already loaded. * is already loaded.
*/ */
fun loadChapter(chapter: ReaderChapter): Completable { suspend fun loadChapter(chapter: ReaderChapter) {
if (chapterIsReady(chapter)) { if (chapterIsReady(chapter)) {
return Completable.complete() return
} }
return Observable.just(chapter) chapter.state = ReaderChapter.State.Loading
.doOnNext { chapter.state = ReaderChapter.State.Loading } withIOContext {
.observeOn(Schedulers.io())
.flatMap { readerChapter ->
logcat { "Loading pages for ${chapter.chapter.name}" } logcat { "Loading pages for ${chapter.chapter.name}" }
try {
val loader = getPageLoader(readerChapter) val loader = getPageLoader(chapter)
chapter.pageLoader = loader chapter.pageLoader = loader
loader.getPages().take(1).doOnNext { pages -> val pages = loader.getPages().awaitSingle()
pages.forEach { it.chapter = chapter } .onEach { it.chapter = chapter }
}
}
.observeOn(AndroidSchedulers.mainThread())
.doOnError { chapter.state = ReaderChapter.State.Error(it) }
.doOnNext { pages ->
if (pages.isEmpty()) { if (pages.isEmpty()) {
throw Exception(context.getString(R.string.page_list_empty_error)) throw Exception(context.getString(R.string.page_list_empty_error))
} }
chapter.state = ReaderChapter.State.Loaded(pages)
// If the chapter is partially read, set the starting page to the last the user read // If the chapter is partially read, set the starting page to the last the user read
// otherwise use the requested page. // otherwise use the requested page.
if (!chapter.chapter.read) { if (!chapter.chapter.read) {
chapter.requestedPage = chapter.chapter.last_page_read chapter.requestedPage = chapter.chapter.last_page_read
} }
chapter.state = ReaderChapter.State.Loaded(pages)
} catch (e: Throwable) {
chapter.state = ReaderChapter.State.Error(e)
throw e
}
} }
.toCompletable()
} }
/** /**