-
Notifications
You must be signed in to change notification settings - Fork 133
[WOOMOB-1400] - Observe booking object from DB on the details screen #14682
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
base: trunk
Are you sure you want to change the base?
Conversation
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 implements BookingRepository
in BookingDetailsViewModel
to observe booking objects from the database, allowing real-time updates of booking data in the details screen. It also introduces a centralized BookingMapper
class to handle all booking-related model mappings and refactors the existing code to use this mapper.
Key Changes
- Added database observation capability for individual bookings through new DAO and repository methods
- Created a centralized
BookingMapper
class to handle all booking model transformations - Refactored the booking details screen to use observed data instead of hardcoded values
Reviewed Changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.
Show a summary per file
File | Description |
---|---|
BookingsDao.kt |
Added observeBooking method to query and observe individual booking entities |
BookingsStore.kt |
Added observeBooking method to expose booking observation through the store layer |
BookingsRepository.kt |
Added observeBooking method to provide booking observation at the repository level |
BookingMapper.kt |
New centralized mapper class for all booking-related model transformations |
BookingDetailsViewModel.kt |
Updated to observe booking data from repository and use mapper for transformations |
BookingDetailsViewState.kt |
Refactored state structure to use nullable BookingUiState instead of hardcoded values |
BookingDetailsScreen.kt |
Updated to handle nullable booking state and display content conditionally |
BookingListViewModel.kt |
Updated to use the new centralized BookingMapper |
BookingListViewState.kt |
Removed inline mapping functions that were moved to BookingMapper |
Test files | Updated and added tests to support the new mapper and repository integration |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
staff = "Marianne Renoir", | ||
location = "238 Willow Creek Drive, Montgomery AL 36109", | ||
duration = "$durationMinutes min", | ||
price = currencyFormatter.formatCurrency(cost, currency) |
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 formatCurrency
method is called with only two parameters, but based on the test setup it expects three parameters including a boolean. This could cause a runtime error.
price = currencyFormatter.formatCurrency(cost, currency) | |
price = currencyFormatter.formatCurrency(cost, currency, true) |
Copilot uses AI. Check for mistakes.
Generated by 🚫 Danger |
data class BookingUiState( | ||
val bookingSummary: BookingSummaryModel, | ||
val bookingsAppointmentDetails: BookingAppointmentDetailsModel, | ||
val bookingCustomerDetails: BookingCustomerDetailsModel, | ||
val bookingPaymentDetails: BookingPaymentDetailsModel, | ||
) |
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.
All those models would be nullable after this PR, so it's handy to wrap them in a single class to avoid multiple null checks.
private val currencyFormatter: CurrencyFormatter, | ||
) { | ||
private val summaryDateFormatter: DateTimeFormatter = DateTimeFormatter.ofLocalizedDateTime( | ||
FormatStyle.MEDIUM, | ||
FormatStyle.SHORT | ||
).withZone(ZoneOffset.UTC) |
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.
cc @hichamboushaba I've used the UTC based on your discussion.
📲 You can test the changes from this Pull Request in WooCommerce-Wear Android by scanning the QR code below to install the corresponding build.
|
📲 You can test the changes from this Pull Request in WooCommerce Android by scanning the QR code below to install the corresponding build.
|
7c821d7
to
4533d98
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## trunk #14682 +/- ##
============================================
+ Coverage 38.38% 38.40% +0.01%
- Complexity 9824 9835 +11
============================================
Files 2091 2092 +1
Lines 116693 116728 +35
Branches 15618 15621 +3
============================================
+ Hits 44797 44828 +31
Misses 67738 67738
- Partials 4158 4162 +4 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
@hichamboushaba I pushed one extra commit with a one-liner that I forgot to do after rebasing with trunk. 509794d |
toolbarTitle = resourceProvider.getString(R.string.booking_details_title, navArgs.bookingId), | ||
) | ||
} | ||
observeBooking(navArgs.bookingId) |
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.
Before approving this, I just want to discuss one problem with this approach and decide whether we are OK with it or not.
With this approach, we observe the DB regardless of whether the app is in the foreground or not (this is part of what we discussed before on whether we should expose flows or LiveData).
I think a better approach (IMO 🙂) would be to have a cold flow exposed as LiveData
, so the cold flow won't start until the screen reaches the Started
lifecycle state, and to achieve it, we can think of something like the following patch:
Index: WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsViewModel.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsViewModel.kt
--- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsViewModel.kt (revision 509794d7b5731f41a1b4ea3847de4599bcf8a3f5)
+++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/details/BookingDetailsViewModel.kt (date 1759502430662)
@@ -4,7 +4,6 @@
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.asLiveData
import com.woocommerce.android.R
-import com.woocommerce.android.ui.bookings.Booking
import com.woocommerce.android.ui.bookings.BookingMapper
import com.woocommerce.android.ui.bookings.BookingsRepository
import com.woocommerce.android.ui.bookings.compose.BookingAttendanceStatus
@@ -15,69 +14,41 @@
import com.woocommerce.android.viewmodel.navArgs
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filterNotNull
import javax.inject.Inject
@HiltViewModel
class BookingDetailsViewModel @Inject constructor(
savedState: SavedStateHandle,
resourceProvider: ResourceProvider,
- private val bookingsRepository: BookingsRepository,
+ bookingsRepository: BookingsRepository,
private val bookingMapper: BookingMapper,
) : ScopedViewModel(savedState) {
private val navArgs: BookingDetailsFragmentArgs by savedState.navArgs()
- private val _state = MutableStateFlow(
- BookingDetailsViewState(
- onCancelBooking = ::onCancelBooking,
- onAttendanceStatusSelected = ::onAttendanceStatusSelected,
- )
- )
- val state: LiveData<BookingDetailsViewState> = _state.asLiveData()
-
- init {
- _state.update {
- it.copy(
- toolbarTitle = resourceProvider.getString(R.string.booking_details_title, navArgs.bookingId),
- )
- }
- observeBooking(navArgs.bookingId)
- }
-
- private fun onAttendanceStatusSelected(status: BookingAttendanceStatus) {
- val bookingState = _state.value.bookingUiState
- if (bookingState != null) {
- _state.update { current ->
- current.copy(
- bookingUiState = bookingState.copy(
- bookingSummary = bookingState.bookingSummary.copy(attendanceStatus = status)
- )
- )
- }
- }
- }
-
- private fun onCancelBooking() {
- // TODO Add logic to Cancel booking
- }
-
- private fun observeBooking(bookingId: Long) {
- bookingsRepository.observeBooking(bookingId)
- .onEach { booking ->
- booking?.let { updateStateWithBooking(it) }
- }
- .launchIn(this)
- }
-
- private fun updateStateWithBooking(booking: Booking) = with(bookingMapper) {
- _state.update { current ->
- current.copy(
+ private val booking = bookingsRepository.observeBooking(navArgs.bookingId)
+
+ // If we need this, then using `savedState.getStateFlow` would be better here to preserve state across process death,
+ private val bookingAttendanceStatus = MutableStateFlow<BookingAttendanceStatus?>(null)
+
+ val state: LiveData<BookingDetailsViewState> = combine(
+ booking.filterNotNull(),
+ bookingAttendanceStatus
+ ) { booking, attendanceStatus ->
+ with(bookingMapper) {
+ BookingDetailsViewState(
+ toolbarTitle = resourceProvider.getString(R.string.booking_details_title, booking.id),
orderId = booking.orderId,
bookingUiState = BookingUiState(
- bookingSummary = booking.toBookingSummaryModel(),
+ bookingSummary = booking.toBookingSummaryModel().let {
+ if (attendanceStatus != null) {
+ it.copy(attendanceStatus = attendanceStatus)
+ } else {
+ it
+ }
+ },
bookingsAppointmentDetails = booking.toAppointmentDetailsModel(),
bookingCustomerDetails = BookingCustomerDetailsModel(
name = "Margarita Nikolaevna",
@@ -96,7 +67,24 @@
total = "$59.50"
)
),
+ onCancelBooking = ::onCancelBooking,
+ onAttendanceStatusSelected = ::onAttendanceStatusSelected
)
}
+ }.asLiveData()
+
+ private fun onAttendanceStatusSelected(status: BookingAttendanceStatus) {
+ // Whether we'll need the `bookingAttendanceStatus` property or not depending
+ // on whether we'll have a "save" button for the whole screen or we'll update the status
+ // as soon as the user selects it from the bottom sheet.
+ // Personally I think we should update it immediately, and if we do, then we don't need the property
+ // we just need to manage the loading state, and when the API call is done, the booking will be updated accordingly
+ // via the `booking` flow.
+ // But this is a product decision. And this patch just preserves the current behavior.
+ bookingAttendanceStatus.value = status
+ }
+
+ private fun onCancelBooking() {
+ // TODO Add logic to Cancel booking
}
}
The patch also makes the whole screen reactive, so if during the implementation we find that we need the Order (which most probably will happen), then we can chain the Flows to observe also the Order from the DB, and then the DB will be the single source of truth, and drives the screen.
Please check it out, and share your thoughts. While I think observing the DB when the app is in the background should be avoided, if you don't agree with the above approach, I'm OK with accepting the PR as it's.
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.
Please check it out, and share your thoughts. While I think observing the DB when the app is in the background should be avoided, if you don't agree with the above approach, I'm OK with accepting the PR as it's.
Thanks @hichamboushaba! You're obviously correct. I think I was working on an autopilot, not being used to LiveData 🤦
I will update the PR on Monday!
WOOMOB-1400
Description
The main goal of this PR is to implement
BookingRepository
in theBookingDetailsViewModel
to observe the booking object in the DB.This only observes the local object. We will have to fetch more information anyway, so that's where I think we can add refreshing of the booking model as well (if we think it's necessary).
Additionally, since we have to handle object mapping, I went ahead and created a mapper class
BookingMapper
, for all booking-related models. In one of the commits, I have added this mapper to theBookingListViewModel
as well.Steps to reproduce
Testing information
No UI changes. The above should be enough.
The tests that have been performed
The above
Images/gif
Screen_recording_20251002_171432.mp4
RELEASE-NOTES.txt
if necessary. Use the "[Internal]" label for non-user-facing changes.