Skip to content

fix(pages): resync congrats screen after filtered deck rebuild - #21409

Open
Ayush-Patel-56 wants to merge 1 commit into
ankidroid:mainfrom
Ayush-Patel-56:fix/congrats-page-stale-after-filtered-deck-rebuild
Open

fix(pages): resync congrats screen after filtered deck rebuild#21409
Ayush-Patel-56 wants to merge 1 commit into
ankidroid:mainfrom
Ayush-Patel-56:fix/congrats-page-stale-after-filtered-deck-rebuild

Conversation

@Ayush-Patel-56

Copy link
Copy Markdown
Contributor

Purpose / Description

The congrats screen for a filtered deck stays stuck on "Congratulations" after rebuilding, even when the rebuild pulls cards back in.

Fixes

Approach

CongratsPage reloaded its own congrats WebView route on every studyQueues change without checking if the deck was actually still empty. CongratsViewModel now checks sched.counts() first and navigates to StudyOptionsActivity instead of reloading when the deck has cards due. Desktop's Overview already does this same check on every refresh, AnkiDroid's split-screen version just never ported it over.

How Has This Been Tested?

Added CongratsViewModelTest, reproducing the repro steps from the issue (fill deck, empty it, rebuild). Ran the full unit test suite locally. Manually reproduced and verified the fix on a Pixel 7 emulator, API 33.

Learning (optional, can help others)

Compared against desktop's qt/aqt/overview.py to see how it avoids this class of bug: it re-checks sched._is_finished() on every refresh instead of assuming the previous state still holds.

Checklist

Please, go through these checks before submitting the PR.

  • You have a descriptive commit message with a short title (first line, max 50 chars).
  • You have commented your code, particularly in hard-to-understand areas
  • You have performed a self-review of your own code
  • UI changes: include screenshots of all affected screens (in particular showing any new or changed strings)
  • UI Changes: You have tested your change using the Google Accessibility Scanner

@Ayush-Patel-56

This comment was marked as outdated.

@ZornHadNoChoice

This comment was marked as outdated.

@Ayush-Patel-56

This comment was marked as outdated.

@ZornHadNoChoice

This comment was marked as outdated.

@Ayush-Patel-56

This comment was marked as outdated.

@BrayanDSO

This comment was marked as outdated.

@Ayush-Patel-56

This comment was marked as outdated.

@lukstbit

Copy link
Copy Markdown
Member

A fix for the main branch lint failure was merged. Rebase on top of main to clear CI failures(if your PR doesn't introduce any).

@Ayush-Patel-56
Ayush-Patel-56 force-pushed the fix/congrats-page-stale-after-filtered-deck-rebuild branch from 33440d6 to ae2247f Compare July 30, 2026 07:04
Comment thread AnkiDroid/src/main/java/com/ichi2/anki/pages/CongratsPage.kt Outdated
@Ayush-Patel-56
Ayush-Patel-56 force-pushed the fix/congrats-page-stale-after-filtered-deck-rebuild branch from ae2247f to 27612d1 Compare August 4, 2026 07:47
@sanjaysargam sanjaysargam added the Needs Second Approval Has one approval, one more approval to merge label Aug 8, 2026
Comment on lines +128 to +133
.onEach { state ->
when (state) {
CongratsRefreshState.ReloadPage -> webViewLayout.post { webViewLayout.reload() }
CongratsRefreshState.DeckHasCardsToStudy -> openStudyOptionsAndFinish()
}
}.launchIn(lifecycleScope)

@david-allison david-allison Aug 8, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see how this would work. We wouldn't be subscribed when the event is emitted as the screen wouldn't be active/visible.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

emit() suspends until something's collecting, it doesn't drop the value. flowWithLifecycle just cancels/restarts the collector on stop/start, so a rebuild while we're stopped just parks until we resume. can add a test for that if you want it nailed down.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

emit() suspends until something's collecting

I don't believe this is the case, could you confirm empirically. In your test, you have a subscriber, driven by turbine.

We are calling MutableSharedFlow.emit: https://github.com/Kotlin/kotlinx.coroutines/blob/b157ff07152dc2adcfdb2e7e45ff2a1fd4dca0bd/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt#L418-L421

We built the MutableStateFlow as: MutableSharedFlow<CongratsRefreshState>() which defines replay = 0

See the implementation of tryEmitNoCollectorsLocked: https://github.com/Kotlin/kotlinx.coroutines/blob/b157ff07152dc2adcfdb2e7e45ff2a1fd4dca0bd/kotlinx-coroutines-core/common/src/flow/SharedFlow.kt#L447-L455

     * If there are no subscribers, the buffer is not used.
     * Instead, the most recently emitted value is simply stored into
     * the replay cache if one was configured, displacing the older elements there,
     * or dropped if no replay cache was configured.

https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-mutable-shared-flow/emit.html

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you're right, with replay=0 and no active subscriber the emit is dropped, confirmed via the docs you linked. real fix: added an onResume() recheck in CongratsPage so it re-evaluates deck state every time the screen becomes visible again, not just when it catches the live event. wrote a test that rebuilds the deck while the fragment is stopped, then resumes it, failed without the fix, passes now.

@david-allison david-allison added Needs Author Reply Waiting for a reply from the original author and removed Needs Review Needs Author Reply Waiting for a reply from the original author labels Aug 8, 2026
@Ayush-Patel-56
Ayush-Patel-56 force-pushed the fix/congrats-page-stale-after-filtered-deck-rebuild branch from 27612d1 to 406a43d Compare August 8, 2026 19:23
@david-allison

Copy link
Copy Markdown
Member

Ok, I've looked into this more deeply:

This works, but only because finish() is called implicitly via:

            undoableOp<OpChangesWithId> {
                val safeAddUpdateResult = safeAddOrUpdateFilteredDeck(newFilterForUpdate)
                if (safeAddUpdateResult.isSuccess) {
                    state.update { DeckBuilt } // this calls through to `finish()`

Instead, could we explicitly perform a refresh on STARTED, rather than relying on an implicit ordering.

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Snapshot diff report vs main. Open screenshot-diff for diffs.

  • DeckPickerScreenshotTest: 1 change
  • PreferencesScreenshotTest: 1 change
All 2 changed screenshots

DeckPickerScreenshotTest

  • hierarchy_lines_many_siblings_compare.png

PreferencesScreenshotTest

  • ReviewerOptionsFragment_compare.png

@Ayush-Patel-56
Ayush-Patel-56 force-pushed the fix/congrats-page-stale-after-filtered-deck-rebuild branch from 406a43d to 6e04ec8 Compare August 8, 2026 19:50
@Ayush-Patel-56

Copy link
Copy Markdown
Contributor Author

Ok, I've looked into this more deeply:

This works, but only because finish() is called implicitly via:

            undoableOp<OpChangesWithId> {
                val safeAddUpdateResult = safeAddOrUpdateFilteredDeck(newFilterForUpdate)
                if (safeAddUpdateResult.isSuccess) {
                    state.update { DeckBuilt } // this calls through to `finish()`

Instead, could we explicitly perform a refresh on STARTED, rather than relying on an implicit ordering.

plain onStart() has the same race in a different spot (fires before the collector resubscribes). moved the refresh inside the same repeatOnLifecycle(STARTED) block as the collector so ordering is guaranteed. pushed just now.

lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch { viewModel.onStudyQueuesChanged() }
viewModel.congratsRefreshState.collect { state ->
when (state) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a Timber.d here, I suspect this is going to be called twice. In the case of reload(), this will cause noticeable latency (and we'll want to resolve this).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch, was scoped to the fragment's own lifecycle - onViewCreated can run more than once (webview gets torn down/recreated), so old collectors stuck around. switched to viewLifecycleOwner, also fixes the lint failure. added the Timber.d.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onViewCreated can run more than once (webview gets torn down/recreated)

I don't believe this is the case, and the fix adds complexity to the code (with a verbose comment).

Please revert this (unless there's a case I haven't thought about): SafeWebView is the only 'refresh' we have, and this works on the WebView component.

Note: My 'double reload' was referring to the case where the opChanges occurred from a rebuild after the screen moved back to STARTED - I don't think I was correct here, but the line is useful as reload is expensive.

@Ayush-Patel-56
Ayush-Patel-56 force-pushed the fix/congrats-page-stale-after-filtered-deck-rebuild branch from 6e04ec8 to 6e03c06 Compare August 9, 2026 06:07

@david-allison david-allison left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!! One more round I feel

Comment thread AnkiDroid/src/main/java/com/ichi2/anki/pages/CongratsPage.kt Outdated
Comment thread AnkiDroid/src/main/java/com/ichi2/anki/pages/CongratsPage.kt Outdated
Comment thread AnkiDroid/src/main/java/com/ichi2/anki/pages/CongratsPage.kt
Comment thread AnkiDroid/src/main/java/com/ichi2/anki/pages/CongratsPage.kt
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch { viewModel.onStudyQueuesChanged() }
viewModel.congratsRefreshState.collect { state ->
when (state) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onViewCreated can run more than once (webview gets torn down/recreated)

I don't believe this is the case, and the fix adds complexity to the code (with a verbose comment).

Please revert this (unless there's a case I haven't thought about): SafeWebView is the only 'refresh' we have, and this works on the WebView component.

Note: My 'double reload' was referring to the case where the opChanges occurred from a rebuild after the screen moved back to STARTED - I don't think I was correct here, but the line is useful as reload is expensive.

@Ayush-Patel-56
Ayush-Patel-56 force-pushed the fix/congrats-page-stale-after-filtered-deck-rebuild branch from 6e03c06 to 91501ba Compare August 10, 2026 04:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Needs Second Approval Has one approval, one more approval to merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Filtered deck overview doesn't update properly after rebuilding

6 participants