-
-
Notifications
You must be signed in to change notification settings - Fork 397
Move quest filtering logic to ViewModel #6205
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Move quest filtering logic to ViewModel #6205
Conversation
Refactors the quest filtering logic by relocating it from the Composable layer to the `QuestSelectionViewModel`. Key changes: - Introduced a `ResourceProvider` interface and `DefaultResourceProvider` implementation to abstract string resource access, decoupling the ViewModel from the `Context`. - Improved performance by preloading quest titles and using a `StateFlow` to manage and update the filtered quest list efficiently. - `QuestSelectionViewModel` now uses `ResourceProvider` to preload quest titles required for filtering. - Filtering logic moved into the ViewModel, operating on the preloaded titles. The core logic (case-insensitive, contains all query words) remains the same. - The filtered quest list is managed and exposed via a `StateFlow` for efficient UI updates. Add Compose annotations to quest selection classes Adds `@Immutable` annotations to `QuestType` and `QuestSelection` classes to improve Compose performance by allowing the runtime to skip recompositions when these objects have not changed. It also adds a remembered lambda for toggling quest selection and a content type to the `itemsIndexed` call in `QuestSelectionList.kt` for further optimization. Signed-off-by: Irakliy Kukava <irakliy.kukava@gmail.com>
Thanks for the effort! But, I noticed however there was this comment in the code:
Perhaps I'm misunderstanding it, but it seems to be saying that filtering in view model is unwanted (even if it brings performance improvements) for design decisions? Or does this PR address that concern somehow? |
|
To avoid the ViewModel directly accessing resources , I introduced the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think yours is a pretty swift solution! I didn't even think of that! In general, you've made it so that there are no direct dependencies of the view model to the UI parts, very cool! Thank you for this! I don't quite like that TextFieldValue is passed to the ViewModel, but it looks like it is just a pure data class, so I guess that's okay. (Couldn't it just be a String, though?)
Say, how did you spot this in the code? Were you familiarizing yourself with the codebase, or did you notice slowdowns yourself and started investigating as to the cause of this?
Note that ShowQuestFormsScreen the exact same quest filtering as in the quest selection setting screen is used. It would make sense if those two places would work consistently (But this needn't be done in this same PR).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note / FYI:
Sooner or later, we want to migrate to Compose Resources (org.jetbrains.compose.components), I hope this class won't make it more difficult to migrate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While I haven't worked directly with KMM before, I checked the documentation on accessing resources outside composables you mentioned. It looks like they provide ways to do that, which suggests the core functionality needed by the ViewModel should still be possible.
My hope is that introducing the ResourceProvider interface might actually help with the migration. Since the ViewModel depends on the interface, the DefaultResourceProvider (which uses Android Context) could potentially be swapped out later with a new implementation that uses the Compose Resources API, without needing major changes in the ViewModel itself.
| private val _searchText = MutableStateFlow(TextFieldValue()) | ||
| override val searchText: StateFlow<TextFieldValue> = _searchText |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| private val _searchText = MutableStateFlow(TextFieldValue()) | |
| override val searchText: StateFlow<TextFieldValue> = _searchText | |
| override val searchText = MutableStateFlow(TextFieldValue()) |
(etc.)
...would be more consistent with the rest of the codebase. I.e. the implementation uses MutableStateFlow while the interface just exposes a StateFlow. So, I don't need to effectively duplicate all StateFlow fields (one private MutableStateFlow, one public StateFlow).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I forgot to add _searchText.asStateFlow(). This ensures that the user of searchText can't cast it to MutableStateFlow and modify the state. Existing StateFlows in the ViewModel can be modified in the composable code like this:
LaunchedEffect(Unit) {
delay(2000)
(viewModel.searchText as MutableStateFlow<String>).value = "AAA"
}This is just an example. In real case scenarios I've seen desperate devs trying to modify states they shouldn't modify. This is why it's a common practice to expose StateFlows to the UI components and back them with MutableStateFlows.
(But those cases are so rare, that I can just change this code to make it more consistent with the codebase)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, I am not desperate. I don't cast :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, "desperate" is not the word I'd use for devs that break the interface by casting (instead of doing it properly). ;-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I meant in big projects sometimes managers push devs to make quick fixes. In those cases developers forget about clean code and start making mess in a project. Backing fields, Api/Impl and other patterns/techniques are meant to fight with that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I know what you mean. ...And it is only a matter of time until such botch comes back haunting the developers, oftentimes not even hitting the dabblers but those that come after... 😬 Hrr, memories, that gives me the creeps.
(So, maybe the pattern you use is generally preferable, as a guard against such practices. But well, being kind of the BDFL in this project, I won't allow botch in the first place, so such safeguard is less necessary. Readability and consistency is higher on my list.)
app/src/main/java/de/westnordost/streetcomplete/data/quest/QuestTypeRegistry.kt
Outdated
Show resolved
Hide resolved
...va/de/westnordost/streetcomplete/screens/settings/quest_selection/QuestSelectionViewModel.kt
Show resolved
Hide resolved
...va/de/westnordost/streetcomplete/screens/settings/quest_selection/QuestSelectionViewModel.kt
Outdated
Show resolved
Hide resolved
...in/java/de/westnordost/streetcomplete/screens/settings/quest_selection/QuestSelectionList.kt
Outdated
Show resolved
Hide resolved
...in/java/de/westnordost/streetcomplete/screens/settings/quest_selection/QuestSelectionList.kt
Outdated
Show resolved
Hide resolved
Yeah, I agree that probably a String would be better here than TextFieldValue for KMM. But I'll have to introduce my own version of TextFieldValue data class for cursor location to work.
I wanted to add a country-specific presets feature (before I found
I can update it in the next commit |
…ormsViewModel` Signed-off-by: Irakliy Kukava <irakliy.kukava@gmail.com>
This commit introduces locale change handling in the `QuestSelectionScreen` and `ShowQuestFormsScreen`. It ensures that the quest titles are reloaded when the locale configuration changes. Key changes: - Added a `LaunchedEffect` that triggers `viewModel.onConfigurationChanged()` when `LocalConfiguration.current.locale` changes. - Implemented `onConfigurationChanged()` in `QuestSelectionViewModel` and `ShowQuestFormsViewModel` to call `loadQuestTitles()` when triggered. Signed-off-by: Irakliy Kukava <irakliy.kukava@gmail.com>
This change enhances the search fields in the quest selection and debug screens by aligning the keyboard language with the user's system settings. This improvement ensures a more intuitive and efficient search experience for users. Signed-off-by: Irakliy Kukava <irakliy.kukava@gmail.com>
|
I can wait for #6210 to update deprecated |
westnordost
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But I'll have to introduce my own version of TextFieldValue data class for cursor location to work.
I think the cursor will be fine. Only on configuration change, the cursor location wouldn't be remembered. Otherwise it should work just as fine with a String. FWIW, TextFieldValue is available in common, i.e. no need to create an own class to make it work in multiplatform.
app/src/main/java/de/westnordost/streetcomplete/screens/settings/debug/ShowQuestFormsScreen.kt
Outdated
Show resolved
Hide resolved
...va/de/westnordost/streetcomplete/screens/settings/quest_selection/QuestSelectionTopAppBar.kt
Show resolved
Hide resolved
Remove onConfigurationChanged from `ShowQuestFormsViewModel` and `QuestSelectionViewModel` Signed-off-by: Irakliy Kukava <irakliy.kukava@gmail.com>
Signed-off-by: Irakliy Kukava <irakliy.kukava@gmail.com>
…ectionViewModel This commit refactors the `searchText` handling in `ShowQuestFormsViewModel` and `QuestSelectionViewModel` to use `String` directly instead of `TextFieldValue`. This simplifies the code and improves readability. Additionally, it updates the `ExpandableSearchField` and top app bars to work with the `String` type for search text. Signed-off-by: Irakliy Kukava <irakliy.kukava@gmail.com>
westnordost
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perfect.
By the way, collectAsStateWithLifecycle() is Android-only. But I can also change that later when I actually start migrating to Compose Multiplatform.
Anyway, for quests that are not only not relevant but even useless (because the thing is always X or never Y, e.g. if there are no tactile pavings on crosswalks at all, if there are no cycleways at all, etc.), you are invited to create a PR to disable such quests for Georgia. |
A small refactoring on quest filtering to improve performance. Now filtering is done in the ViewModel. Also introduced
ResourceProvider, useful for accessing resources from non-UI layers/components.