Skip to content

Conversation

@sanjaysargam
Copy link
Member

Purpose / Description

This PR moves 8 deck-related business logic functions from DeckPicker.kt to DeckPickerViewModel.kt to improve code architecture and follow MVVM pattern properly.

Fixes

Approach

Moved business logic functions to ViewModel:
1. getDeckName() - Gets deck name for rename operations
2. onSubDeckCreated() - Notifies when subdeck is created
3. rebuildFilteredDeck() - Rebuilds filtered deck with current settings
4. exportDeck() - Handles deck export requests
5. createIcon() - Prepares data for deck shortcut creation
6. disableDeckAndChildrenShortcuts() - Gets deck IDs for disabling shortcuts
7. findDeckPosition() - Finds deck position in flattened list
8. isDeckAndSubdeckEmpty() - Checks if deck and subdecks are empty

Added reactive flows for UI communication:
- flowOfExportDeck - For export requests
- flowOfCreateShortcut - For shortcut creation with ShortcutData
- flowOfDisableShortcuts - For shortcut disabling
- flowOfSubDeckCreated - For subdeck creation notifications

Updated DeckPicker.kt:
- Modified methods to use ViewModel instead of direct collection access
- Added flow listeners for reactive UI updates

How Has This Been Tested?

Physical Device (Redmi)

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

@sanjaysargam
Copy link
Member Author

DeckPickerTest > [0] > ContextMenu creates deck shortcut when selecting CREATE_SHORTCUT[0] FAILED

Uff, I'll resolve this tomorrow

@lukstbit lukstbit added the Needs Author Reply Waiting for a reply from the original author label Sep 17, 2025
@david-allison david-allison removed the Needs Author Reply Waiting for a reply from the original author label Sep 18, 2025
@david-allison
Copy link
Member

Replying to a comment

@sanjaysargam
Copy link
Member Author

Replying to a comment

@david-allison Not sure I get this — looks like you haven’t replied to the comments

Copy link
Member

@david-allison david-allison left a comment

Choose a reason for hiding this comment

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

Hmm... This should have submitted them


fun onFocusedDeckChanged(deckId: DeckId?) {
val position = deckId?.let { findDeckPosition(it) } ?: 0
val position = deckId?.let { viewModel.findDeckPosition(it, deckListAdapter.currentList) } ?: 0
Copy link
Member

Choose a reason for hiding this comment

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

Ideally the viewModel should be the source of truth for the collection of decks

// This code should not be reachable with lower versions
val shortcut =
ShortcutInfoCompat
.Builder(this, did.toString())
Copy link
Member

Choose a reason for hiding this comment

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

Add a unit test in libanki on DeckId.toString being correct.

It's subject to being broken if we move to a value class

/**
* Gets the current name of a deck
*/
suspend fun getDeckName(deckId: DeckId): String = withCol { decks.name(deckId) }
Copy link
Member

Choose a reason for hiding this comment

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

Todo to remove this or make it private, fine for now

Copy link
Member

Choose a reason for hiding this comment

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

Probably remove, that's just a backend call.

fun onSubDeckCreated() =
viewModelScope.launch {
flowOfSubDeckCreated.emit(Unit)
flowOfDeckCountsChanged.emit(Unit)
Copy link
Member

Choose a reason for hiding this comment

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

Todo: this should be using ChangeManager

fun DeckNode.onlyHasDefaultDeck() = children.singleOrNull()?.did == DEFAULT_DECK_ID

/** Data for creating a deck shortcut */
data class ShortcutData(
Copy link
Member

Choose a reason for hiding this comment

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

Maybe comment an example of the long/short values

@david-allison david-allison added the Needs Author Reply Waiting for a reply from the original author label Sep 22, 2025
Comment on lines 2284 to 2288
val isEmpty =
withCol {
val node = sched.deckDueTree().find(did)
node?.all { decks.isEmpty(it.did) } ?: true
}
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
val isEmpty =
withCol {
val node = sched.deckDueTree().find(did)
node?.all { decks.isEmpty(it.did) } ?: true
}
val isEmpty = withCol { decks.cardCount(did, includeSubdecks = true)

val deck = getNodeByDid(did)
if (deck.hasCardsReadyToStudy()) {

val deck = withCol { sched.deckDueTree().find(did) }
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
val deck = withCol { sched.deckDueTree().find(did) }
val deck = withCol { decks.getLegacy(did) }

Copy link
Member Author

Choose a reason for hiding this comment

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

I noticed that decks.getLegacy(did) returns a Deck object, which doesn’t provide the hasCardsReadyToStudy() method. On the other hand, sched.deckDueTree().find(did) returns a DeckNode, and this one does have hasCardsReadyToStudy().

Because of this, I think your suggestion may not fully apply in this context — we specifically need the hasCardsReadyToStudy() functionality, which is only available via DeckNode

}

if (!deck.filtered && isDeckAndSubdeckEmpty(did)) {
val isFiltered = withCol { decks.isFiltered(did) }
Copy link
Member

Choose a reason for hiding this comment

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

We already have the deck instance from above and we can use its isFiltered property, so there's no need for this backend cal:

deck?.isFiltered.

Comment on lines 504 to 508
viewModel.flowOfCreateShortcut.test {
supportFragmentManager.selectContextMenuOption(DeckPickerContextMenuOption.CREATE_SHORTCUT, didA)
advanceUntilIdle()
awaitItem()
}
Copy link
Member

Choose a reason for hiding this comment

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

My comment was about the two lines below:

Suggested change
viewModel.flowOfCreateShortcut.test {
supportFragmentManager.selectContextMenuOption(DeckPickerContextMenuOption.CREATE_SHORTCUT, didA)
advanceUntilIdle()
awaitItem()
}
supportFragmentManager.selectContextMenuOption(DeckPickerContextMenuOption.CREATE_SHORTCUT, didA)
advanceUntilIdle()

being enough for the test to pass. If we require test {} then there's something wrong in the code.

hasSubDecks = tree.children.any { it.children.any() },
)
}
}.stateIn(viewModelScope, SharingStarted.Eagerly, initialValue = FlattenedDeckList.empty)
Copy link
Member

Choose a reason for hiding this comment

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

Why stateIn?

Copy link
Member Author

Choose a reason for hiding this comment

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

Using stateIn with SharingStarted.Eagerly is doing a few things:

Keeps state – Any new UI part (like a screen) directly gets the latest deck list without waiting.

Shares result – If many parts of the UI are watching, they all use the same result instead of redoing the work.

But yes, Eagerly can be wasteful since it starts the flow even if no UI is looking at it.

Other choices:

  1. Use WhileSubscribed() so it runs only when some UI is observing.

  2. Remove stateIn if normally only one UI part collects it.

  3. Keep Eagerly if calculation is heavy and we want to keep it cached always.

I would prefer WhileSubscribed() unless we really need eager caching.

 - Move 8 deck-related functions from DeckPicker to DeckPickerViewModel
  - Add reactive flows for UI communication (export, shortcuts, subdeck creation)
  - Update DeckPicker to use ViewModel methods instead of direct collection access
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants