Fix migration UI crash, various performance & QoL changes#635
Open
RsReez wants to merge 5 commits into
Open
Conversation
…rflow, and selection wipe
…rflow, and selection wipe
…ess bar to migration screen so progress is visible, improved performMigrations stability
…anga view when downloading chapters individually.
…that scales with library size on concurrent category updates
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bug Fixes
This PR adds performance improvements, QoL additions, and fixes several bugs found through crash log analysis and testing across the migration UI, library, and manga detail screens.
Migration UI Crash (DeferredField)
Problem:
DeferredField.set()was callingmutex.tryLock()but ignoring its return value, then unconditionally callingmutex.unlock(). Ifinitialize()had already unlocked the mutex beforeset()was called, the second unlock threwIllegalStateExceptionand crashed the app during manga source migration.Fix: Store the result of
tryLock()in alockedvariable and only callunlock()if it returnedtrue. Also guardinitialize()with an early return if already initialized to prevent being called twice.Migration UI Progressive Slowdown
Problem:
MigrationProcessHolder.bind()launched a coroutine vialaunchUIthat suspended onsearchResult.get()waiting for a search result. EverynotifyItemChanged()call after a migration completed would callbind()again on every visible holder, launching another suspended coroutine without cancelling the previous one. These accumulated indefinitely onDispatchers.Main, causing the UI to get progressively slower with each migration completed.Fix: Give each holder its own
MainScopewith a trackedbindJobthat gets cancelled at the start of everybind()call, so only one coroutine per holder ever waits at a time. AddonRecycled()for clean teardown and wire it up viaonViewRecycled()inMigrationProcessAdapter.Migration UI Coroutine Scope Cleanup (MigratingManga)
Problem:
MigratingMangaheld a bareCoroutineContextformigrationJobrather than a properCoroutineScope. This meant cancellation wasn't properly propagated to child coroutines, and there was no clean way to cancel all in-flight work for a manga entry.Fix: Replace the bare
CoroutineContextwith a properCoroutineScope(migrationScope) backed by aSupervisorJob, so one failing source search doesn't cancel sibling searches. Expose acancel()method for clean teardown. KeepmigrationJobas a property that returns the scope'scoroutineContextfor call sites that need it directly.Library CursorWindow Overflow (slight mitigation)
Partial Fix: Add a separate
findAllAsFlowquery that explicitly lists all columns exceptdescription, used only for the reactive flow subscription. The originalfindAllquery is preserved for one-shot fetches. A correspondingflowMapperwas added toLibraryMangaandMangaRepositoryImplupdated to usefindAllAsFlowwithflowMapperfor the flow subscription.Manga Description Blank When Opening from Library
Problem: As a consequence of excluding
descriptionfrom the library flow, manga objects passed from the library to the detail screen had anulldescription.MangaDetailsPresenter.onCreate()only re-fetches from the database ifmanga.initializedisfalse, so it would display no description until a manual refresh.Fix: Add a
manga.description == nullcheck alongside the existing!::manga.isInitializedcheck inMangaDetailsPresenter.onCreate(). If the manga arrives with anulldescription it immediately fetches fresh from themangastable by ID before rendering — a fast single-row lookup.Library Selection Wiped on Every Chapter Download
Problem:
onNextLibraryUpdatecalleddestroyActionModeIfNeeded()unconditionally on every library flow emission. Since downloads write to thechapterstable and invalidate the library flow, every chapter download completion wiped the user's entire manga selection in the library.Fix: Make the call conditional — only call
destroyActionModeIfNeeded()ifselectedMangasis empty, or if none of the selected manga exist in the updatedmangaMap. This preserves legitimate cases like selection being cleared after a manga is deleted or filtered out, while leaving selection intact during routine library refreshes.Chapter List Stutter & Stuck Download Completion Animation
Problem:
downloadManager.queueStateemits the entire global download queue on everystate change for any manga.
onQueueUpdateoriginally rebuilt the chapter list (a full DBread + RecyclerView dataset replacement) on every single emission, causing visible stutter
when other manga were downloading in the background.
A naive fix — early-returning when
queue.none { it.manga.id == mangaId }— caused a second,worse bug: the final progress update for a chapter often arrives in the same the download
completes and is removed from the queue, so that check is
trueright when the rebuild isneeded. Left the download button stuck mid-animation, not transitioning to the
completed state, until something else forced an unrelated rebind
Fix: Track whether this manga had any queued chapters as of the last emission
(
hadQueuedChapters). Skip the rebuild only when the manga has no queued chapters now andhad none last time, but always rebuild on the transition from "had queued chapters" to "has
none" — guaranteeing the final completion update is never dropped, while still skipping
emissions for unrelated manga downloading in the background.
Chapter Mark as Read Freeze, Missed Marks, and Start Reading Button
Problem: Three related issues caused by the same root:
Freeze (~1 second): Marking a single chapter as read or bookmarked called
adapter.setChapters()→updateDataSet(), replacing everyChapterItemobject in the adapter and rebinding the entire list including the manga header with cover art, metadata, genres and description.Missed marks: The same
updateDataSet()call replaced allChapterItemobjects mid-gesture when swiping rapidly.FlexibleAdapter's swipe recovery would then firenotifyItemChangedon the in-progress item, resetting the swipe without completing it — producing a consistent miss rate of roughly 6 in every 20 rapid swipes.Start reading button not updating: After fixing the above, the start reading button in
MangaHeaderHolderstopped updating its chapter number since it only refreshes during a fullbind()call.Fix:
updateChaptersQuick()toMangaDetailsController— for single chapter marks calladapter.notifyItemChanged(position)on just the one affected item, leaving all otherChapterItemobjects untouched. This eliminates both the freeze and the missed marks. Bulk operations still use the fullupdateChapters().updateReadingButton()method toMangaHeaderHolderthat only updates the button's text and enabled state based ongetNextUnreadChapter(), called directly on the live holder instance viagetHeader()from the controller. Avoids triggering a full header rebind which would cause a visible blink and reintroduce the lag.Migrate All Progress Dialog
Problem: Clicking Migrate All or Copy All showed no indication that migration was in progress — the screen appeared with no feedback while still being interactable.
Fix: Show a non-dismissible progress dialog during
performMigrationsthat updates withcurrent / totalas each manga is migrated. Also refactoredperformMigrationsto take a snapshot of items at start for stability, preventing the list changing mid-migration from causing skips or crashes.Library Update Spinner Not Animating / Getting Stuck Off
Problem: The per-category update (refresh) icon on the library screen frequently never
started spinning when tapped, or appeared to only start once a manga with an actual update
was found partway through the scan - even though the update was genuinely running the whole
time. With no updates found at all, the icon never spun.
Root cause:
LibraryUpdateJob.startNow()only enqueues a WorkManager job; the jobinstance's
categoryIdsset isn't populated until a few lines intodoWork(). WorkManagerreports the work as
RUNNINGright asdoWork()starts, beforecategoryIdsis filled in.LibraryController'sisRunningFlowcollector only fires once per state transition, so ifit processed that "now running" event during this gap, every header read
categoryInQueue() == falseand the spinner never started - and nothing re-checked it againuntil an unrelated rebind happened to occur later (most commonly triggered by a manga
actually having new chapters, which is why it looked correlated with "finding an update").
Fix: Add
isCategoryUpdating()toLibraryCategoryAdapter.LibraryListener, implementedin
LibraryControllervia apendingManualCategoryUpdatesset that's added to the instant acategory update is triggered (single-category tap or global update) and only cleared once
isRunningFlowconfirms the job has actually stopped.LibraryHeaderHoldernow checks thisinstead of calling
LibraryUpdateJob.categoryInQueue()directly, so the spinner is immune topremature/racy rebinds and stays on continuously from tap to genuine completion.
UI Freeze Adding a Category to an Already-Running Library Update
Problem: Tapping a category's update button while a different category's update was
already in progress froze the entire app for several seconds.
Root cause:
LibraryUpdateJob.addCategory()fetched the manga to update viarunBlocking { getMangaToUpdate(categoryId) }, which callsgetLibraryManga.await()- afull library database fetch. Since
addCategory()is called fromstartNow(), itselfcalled directly from the button tap handler on the main thread,
runBlockingfroze the UIfor as long as that query took.
Note. Freeze severity likely scales with library size, since addCategory()'s
runBlocking fetch pulls the entire library (not just the target
category) and competes with the running job's own DB activity - small
libraries may not exhibit this noticeably, larger ones more so.
Fix: Move
addCategory()'s work intoextraScope.launch(alreadyDispatchers.IOelsewhere in the same class) instead of blocking the calling thread. Also optimized
isRunning()to check the in-memoryinstancereference first (set for the exact durationa job is actually running) before falling back to a blocking WorkManager query, avoiding a
smaller redundant cost on every call from the same process.
Files Changed
DeferredField.ktmutexdouble-unlock inset(), guardinitialize()against duplicate callsMigrationProcessHolder.ktMainScopewithbindJobcancellation, addonRecycled()MigrationProcessAdapter.ktonViewRecycled(), batch snapshot inperformMigrations, progress callbackMigratingManga.ktCoroutineContextwith properCoroutineScope, exposecancel()LibraryController.ktdestroyActionModeIfNeeded()to preserve selected library manga during background downloads,isCategoryUpdating()/pendingManualCategoryUpdatesfor reliable update spinner stateLibraryManga.ktflowMapperexcludingdescriptionfor reactive flow subscriptionMangaDetailsController.ktupdateChaptersQuick()for single chapter updatesMangaDetailsPresenter.ktdescription == nullcheck,hadQueuedChaptersqueue-transition tracking inonQueueUpdate,updateChaptersQuickusageLibraryHeaderHolder.ktisCategoryUpdating()via listener instead ofLibraryUpdateJob.categoryInQueue()directlyLibraryCategoryAdapter.ktisCategoryUpdating()toLibraryListenerinterfaceLibraryUpdateJob.ktaddCategory()by moving DB fetch off the calling thread, optimizeisRunning()with in-memory instance checkMangaHeaderHolder.ktupdateReadingButton()for targeted button text update without full rebindlibrary_view.sqfindAllAsFlowquery excludingdescriptionMangaRepositoryImpl.ktfindAllAsFlowwithflowMapperfor flow subscription, fixLogger.e(e)MigrationListController.ktstrings.xmlmigrating_progressstringLibraryGridHolder.ktids.xmlmanga_cover_modifiedresource IDCloses #634
Fixes #595
Add a 👍 reaction to pull requests you find important.