-
Couldn't load subscription status.
- Fork 134
[CIAB] BookingListViewModel refactoring and some fixes #14780
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
[CIAB] BookingListViewModel refactoring and some fixes #14780
Conversation
📲 You can test the changes from this Pull Request in WooCommerce-Wear Android by scanning the QR code below to install the corresponding build.
|
| if (searchQuery != previousQuery) { | ||
| searchResults.value = emptyList() | ||
| } |
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.
This is the main part of the second change mentioned in the description, it fixes an issue where pull-to-refresh would result in displaying the Empty state during the fetch. I added two unit tests to confirm the behavior too.
Check the below recordings to see the diffference:
| Before | After |
|---|---|
Screen_recording_20251017_181115.mp4 |
Screen_recording_20251017_181007.mp4 |
| // Keep TextFieldValue in sync with external value | ||
| val textFieldValue = textFieldValueState.copy(text = value) |
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.
Without this, when the value is changed externally (for example by tapping on the clear button), the update is not reflected on the text field.
This line is copied from the BasicTextField String implementation.
| Before | After |
|---|---|
Screen_recording_20251017_181130.mp4 |
Screen_recording_20251017_181025.mp4 |
|
📲 You can test the changes from this Pull Request in WooCommerce Android by scanning the QR code below to install the corresponding build.
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## trunk #14780 +/- ##
============================================
+ Coverage 38.04% 38.06% +0.01%
- Complexity 9997 10003 +6
============================================
Files 2120 2120
Lines 119445 119477 +32
Branches 16254 16261 +7
============================================
+ Hits 45447 45480 +33
+ Misses 69412 69409 -3
- Partials 4586 4588 +2 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
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.
Pull Request Overview
This PR refactors the BookingListViewModel to implement a more reactive approach that avoids reading fetch parameters from StateFlow values, while fixing issues with pull-to-refresh functionality during search operations and WCSearchView text field synchronization.
- Replaced imperative fetch parameter reading with reactive parameter combinations using Flow operators
- Fixed search result handling to properly clear results when search query changes and maintain previous results during refresh
- Corrected text field value synchronization in WCSearchView component
Reviewed Changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| BookingListViewModel.kt | Refactored to use reactive parameter combining and added refresh trigger mechanism |
| BookingListHandler.kt | Fixed search result handling logic and pagination for search operations |
| TextFields.kt | Fixed text field value synchronization issue in WCSearchField component |
| BookingListHandlerTest.kt | Added comprehensive tests for search refresh and query change scenarios |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| refreshTrigger.collect { | ||
| fetchBookings( | ||
| initialLoadingState = BookingListLoadingState.Refreshing, | ||
| fetchParams = fetchParams | ||
| ) | ||
| } |
Copilot
AI
Oct 20, 2025
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.
The refreshTrigger.collect call inside collectLatest creates a nested collection that will never complete. This should be handled using combine or merge to properly react to refresh triggers alongside parameter changes.
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 we are good here, but I tend to agree, I never like a collect inside another collect.
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.
Yes we are good, the collectLatest means that we will cancel the nested collect on each new emission.
The above patterns allows us to retrigger the fetching with the same parameters without reverting to reading the values from the Flows like how we did before, so I think it fits our need well.
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 ended up getting rid of the nested collection, as it caused a minor issue before: pull-to-refresh action needed to wait for any pending fetch to finish before triggerring, with the last change in the commit 090a148, we will cancel pending fetches on pull-to-refresh, which matches the previous behavior.
| private suspend fun fetchBookings( | ||
| initialLoadingState: BookingListLoadingState, | ||
| fetchParams: FetchParams, | ||
| ) { |
Copilot
AI
Oct 20, 2025
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.
The fetchBookings function is now suspend but is called from within collectLatest which already runs in a coroutine context. Consider making this a regular function and launching coroutines internally where needed, or ensure proper structured concurrency.
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.
There is an issue when I try to pull to refresh with a search field activated but empty:
Screen_recording_20251020_133633.mp4
| ) | ||
| } else { | ||
| searchResults | ||
| searchResults.take(page * PAGE_SIZE) |
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.
Do we need that?
searchResults returns a List of bookings, not single bookings so I'm not sure why we take page * PAGE_SIZE 🤔
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.
Given we don't clear the results now when after refresh if the query is not modified, in case more than a single page was previously fetched, this will mean that the "load more" login won't work as expected without this, as we won't reach the view that triggers it.
This is similar to what we do with the DB observation too.
Does this clarifies it?
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.
To be honest, no 😅
This is similar to what we do with the DB observation too.
DB uses that value to limit the nr of entries returned. Here we are taking a specific number of values (list#1, list#2...) so those seem like different things 🤔
I'm testing the code and if I remove the take operator it it behaves in the same way. I can reach the view that triggers the next page load.
Something like this would make more sense searchResults.map { it.take(page * PAGE_SIZE) } but I'm not sure we need that at all. When the pull to refresh is triggered the page is reset to 1, so we for the short moment when the refresh is happening we would show less items than before triggering it 🤔
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.
searchResults.map { it.take(page * PAGE_SIZE) }
That's what I thought I was doing 🤦 😅
I'm testing the code and if I remove the take operator it it behaves in the same way. I can reach the view that triggers the next page load.
The question here, is what's the end of the list? If we keep all the items, then the end of the list is further more than our current page, this breaks the pagination logic, because when the first page is fetched, we'll then get to this line while will remove the items you are seeing at the end of list, and then the list will jump above, and won't be a good experience.
Again, this logic is similar to what we do with DB, because we clear the DB after the fetch, we ask the DB to return only the number of items that fits in our pages count.
Let me know if this still doesn't clarify it 🙂, if you agree with the above, then I'll fix the logic to apply take on the list of items and not the Flow.
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.
The question here, is what's the end of the list? If we keep all the items, then the end of the list is further more than our current page, this breaks the pagination logic, because when the first page is fetched, we'll then get to this line while will remove the items you are seeing at the end of list, and then the list will jump above, and won't be a good experience.
When you pull to refresh you are already at the top, so there's no visible jump in the list as there no visible change.
Anyway
Let me know if this still doesn't clarify it 🙂, if you agree with the above, then I'll fix the logic to apply take on the list of items and not the Flow.
That sounds good! My main issue was the .take on the Flow, which was breaking my brain, because I though I'm missing something obvious.
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.
When you pull to refresh you are already at the top, so there's no visible jump in the list as there no visible change.
But the user can scroll down when the fetch is ongoing, and if the fetch takes long, then this would be noticeable.
Thanks @AdamGrzybkowski for catching the issue, I made the change in the last commit edcbf72
| refreshTrigger.collect { | ||
| fetchBookings( | ||
| initialLoadingState = BookingListLoadingState.Refreshing, | ||
| fetchParams = fetchParams | ||
| ) | ||
| } |
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 we are good here, but I tend to agree, I never like a collect inside another collect.
|
Hmm the issue above might not be related to this PR 🤔 |
|
@AdamGrzybkowski let me take a look, even if unrelated to the PR, it would be good to investigate it and fix it in this PR. |
To solve an issue where the loading state changes are conflated, resulting in a stuck refresh indicator in the screen.
Also fixes an issue where pull-to-refresh action needed to wait for current fetch to finish before triggering.
|
@AdamGrzybkowski the issue you found should be fixed by the commit 6fe7f69. The cause of the issue was that |
This is fixed, thanks! |
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.
![]()
Description
This PR has three main changes:
StateFlow#value.Steps to reproduce
Testing information
The tests that have been performed
The above.
RELEASE-NOTES.txtif necessary. Use the "[Internal]" label for non-user-facing changes.