From e7146ceb0d425486df8bcd68c9a12b0979330469 Mon Sep 17 00:00:00 2001 From: Caleb Shim Date: Sat, 1 Nov 2025 12:39:31 -0400 Subject: [PATCH 1/2] Replace /eatery/ with /eatery/day/{day} and add cache for upcoming eateries --- .../android/eatery/MainActivity.kt | 2 +- .../android/eatery/data/NetworkingApi.kt | 16 +-- .../data/repositories/EateryRepository.kt | 126 +++++++++++++----- .../eatery/ui/screens/UpcomingMenuScreen.kt | 2 +- .../ui/viewmodels/EateryDetailViewModel.kt | 4 + .../eatery/ui/viewmodels/HomeViewModel.kt | 5 +- .../eatery/ui/viewmodels/UpcomingViewModel.kt | 31 +++-- 7 files changed, 118 insertions(+), 68 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/android/eatery/MainActivity.kt b/app/src/main/java/com/cornellappdev/android/eatery/MainActivity.kt index ee46a88..47d09ef 100644 --- a/app/src/main/java/com/cornellappdev/android/eatery/MainActivity.kt +++ b/app/src/main/java/com/cornellappdev/android/eatery/MainActivity.kt @@ -42,7 +42,7 @@ class MainActivity : ComponentActivity() { val dataRefresher = object : DefaultLifecycleObserver { override fun onResume(owner: LifecycleOwner) { super.onResume(owner) - eateryRepository.pingEateries() + eateryRepository.refresh() } } lifecycle.addObserver(dataRefresher) diff --git a/app/src/main/java/com/cornellappdev/android/eatery/data/NetworkingApi.kt b/app/src/main/java/com/cornellappdev/android/eatery/data/NetworkingApi.kt index ad07a45..f69e8b2 100644 --- a/app/src/main/java/com/cornellappdev/android/eatery/data/NetworkingApi.kt +++ b/app/src/main/java/com/cornellappdev/android/eatery/data/NetworkingApi.kt @@ -3,12 +3,10 @@ package com.cornellappdev.android.eatery.data import com.cornellappdev.android.eatery.data.models.AccountsResponse import com.cornellappdev.android.eatery.data.models.ApiResponse import com.cornellappdev.android.eatery.data.models.Eatery -import com.cornellappdev.android.eatery.data.models.Event import com.cornellappdev.android.eatery.data.models.GetApiAccountsParams import com.cornellappdev.android.eatery.data.models.GetApiRequestBody import com.cornellappdev.android.eatery.data.models.GetApiResponse import com.cornellappdev.android.eatery.data.models.GetApiTransactionHistoryParams -import com.cornellappdev.android.eatery.data.models.GetApiUserParams import com.cornellappdev.android.eatery.data.models.ReportSendBody import com.cornellappdev.android.eatery.data.models.TransactionsResponse import com.cornellappdev.android.eatery.data.models.User @@ -20,12 +18,6 @@ import retrofit2.http.Path import retrofit2.http.Url interface NetworkApi { - @POST() - suspend fun fetchUser( - @Url url: String, - @Body body: GetApiRequestBody - ): GetApiResponse - @POST() suspend fun fetchAccounts( @Url url: String, @@ -38,8 +30,8 @@ interface NetworkApi { @Body body: GetApiRequestBody ): GetApiResponse - @GET("/eatery/") - suspend fun fetchEateries(): List + @GET("/eatery/day/{day_id}") + suspend fun fetchEateriesByDay(@Path(value = "day_id") dayId: Int): List @GET("/eatery/{eatery_id}") suspend fun fetchEatery(@Path(value = "eatery_id") eateryId: String): Eatery @@ -47,10 +39,6 @@ interface NetworkApi { @GET("/eatery/simple") suspend fun fetchHomeEateries(): List - @GET("/event") - suspend fun fetchEvents(): ApiResponse> - - @POST("/report/") suspend fun sendReport( @Body report: ReportSendBody diff --git a/app/src/main/java/com/cornellappdev/android/eatery/data/repositories/EateryRepository.kt b/app/src/main/java/com/cornellappdev/android/eatery/data/repositories/EateryRepository.kt index 05c0e62..a56bf26 100644 --- a/app/src/main/java/com/cornellappdev/android/eatery/data/repositories/EateryRepository.kt +++ b/app/src/main/java/com/cornellappdev/android/eatery/data/repositories/EateryRepository.kt @@ -1,9 +1,7 @@ package com.cornellappdev.android.eatery.data.repositories import com.cornellappdev.android.eatery.data.NetworkApi -import com.cornellappdev.android.eatery.data.models.ApiResponse import com.cornellappdev.android.eatery.data.models.Eatery -import com.cornellappdev.android.eatery.data.models.Event import com.cornellappdev.android.eatery.ui.viewmodels.state.EateryApiResponse import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -19,8 +17,19 @@ import javax.inject.Singleton @Singleton class EateryRepository @Inject constructor(private val networkApi: NetworkApi) { - private suspend fun getAllEateries(): List = - networkApi.fetchEateries() + enum class Screen { + HOME, + DETAILS, + UPCOMING + } + + private var currentScreen: Screen = Screen.HOME + private var lastEateryPinged: Int? = null + private var lastDayRequested: Int? = null + + fun changeScreen(screen: Screen) { + currentScreen = screen + } private suspend fun getEatery(eateryId: Int): Eatery = networkApi.fetchEatery(eateryId = eateryId.toString()) @@ -28,8 +37,8 @@ class EateryRepository @Inject constructor(private val networkApi: NetworkApi) { private suspend fun getHomeEateries(): List = networkApi.fetchHomeEateries() - private suspend fun getAllEvents(): ApiResponse> = - networkApi.fetchEvents() + private suspend fun getUpcomingEateries(day: Int): List = + networkApi.fetchEateriesByDay(dayId = day) private val _eateryFlow: MutableStateFlow>> = MutableStateFlow(EateryApiResponse.Pending) @@ -47,45 +56,47 @@ class EateryRepository @Inject constructor(private val networkApi: NetworkApi) { */ val homeEateryFlow = _homeEateryFlow.asStateFlow() + private val _upcomingEateriesFlow: MutableStateFlow>> = + MutableStateFlow(EateryApiResponse.Pending) + + val upcomingEateriesFlow = _upcomingEateriesFlow.asStateFlow() + /** * A map from eatery ids to the states representing their API loading calls. */ - private val eateryApiCache: MutableStateFlow>> = + private val eateryDetailsCache: MutableStateFlow>> = MutableStateFlow(mapOf>().withDefault { EateryApiResponse.Error }) + private val upcomingEateriesCache: MutableStateFlow>>> = + MutableStateFlow(mapOf>>().withDefault { EateryApiResponse.Error }) + init { // Start loading backend as soon as the app initializes. - pingEateries() - } - - fun pingEateries() { - pingAllEateries() pingHomeEateries() } /** - * Makes a new call to backend for all the eatery data. + * Refreshes the data for the current screen and resets all other stale cache data. */ - private fun pingAllEateries() { - _eateryFlow.value = EateryApiResponse.Pending - eateryApiCache.update { map -> - map.mapValues { EateryApiResponse.Pending } - .withDefault { EateryApiResponse.Error } - } - CoroutineScope(Dispatchers.IO).launch { - try { - val eateries = getAllEateries() - _eateryFlow.value = EateryApiResponse.Success(eateries) - eateryApiCache.update { map -> - eateries.filter { it.id != null } - .associate { it.id!! to EateryApiResponse.Success(it) } - .withDefault { EateryApiResponse.Error } - } - } catch (_: Exception) { - _eateryFlow.value = EateryApiResponse.Error - eateryApiCache.update { - emptyMap>().withDefault { EateryApiResponse.Error } - } + fun refresh() { + // Re-ping based on current screen and clear all other caches. + when (currentScreen) { + Screen.HOME -> { + pingHomeEateries() + eateryDetailsCache.value = emptyMap() + upcomingEateriesCache.value = emptyMap() + } + + Screen.DETAILS -> lastEateryPinged?.let { + eateryDetailsCache.value = emptyMap() + pingEatery(it) + upcomingEateriesCache.value = emptyMap() + } + + Screen.UPCOMING -> lastDayRequested?.let { + upcomingEateriesCache.value = emptyMap() + pingUpcomingMenu(it) + eateryDetailsCache.value = emptyMap() } } } @@ -93,7 +104,7 @@ class EateryRepository @Inject constructor(private val networkApi: NetworkApi) { /** * Makes a new call to backend for all the abridged home eatery data. */ - private fun pingHomeEateries() { + fun pingHomeEateries() { _homeEateryFlow.value = EateryApiResponse.Pending CoroutineScope(Dispatchers.IO).launch { try { @@ -105,12 +116,53 @@ class EateryRepository @Inject constructor(private val networkApi: NetworkApi) { } } + /** + * Retrieves upcoming eatery data for the specified day, either from cache or by making a new + * backend call. + */ + fun retrieveUpcomingMenu(day: Int) { + lastDayRequested = day + val cachedResponse = upcomingEateriesCache.value[day] + if (cachedResponse != null) { + _upcomingEateriesFlow.value = cachedResponse + if (cachedResponse is EateryApiResponse.Success) { + return + } + } + pingUpcomingMenu(day) + } + + /** + * Makes a new call to backend for upcoming eatery data for the specified day. + * Only updates cache for [day]. + */ + private fun pingUpcomingMenu(day: Int) { + lastDayRequested = day + _upcomingEateriesFlow.value = EateryApiResponse.Pending + upcomingEateriesCache.update { map -> + (map + (day to EateryApiResponse.Pending)) + .withDefault { EateryApiResponse.Error } + } + CoroutineScope(Dispatchers.IO).launch { + try { + val eateries = getUpcomingEateries(day) + _upcomingEateriesFlow.value = EateryApiResponse.Success(eateries) + upcomingEateriesCache.update { map -> + map + (day to EateryApiResponse.Success(eateries)) + } + } catch (_: Exception) { + _upcomingEateriesFlow.value = EateryApiResponse.Error + } + } + } + /** * Makes a new call to backend for the specified eatery. After calling, * `eateryApiCache[eateryId]` is guaranteed to contain a state actively loading that eatery's * data. */ private fun pingEatery(eateryId: Int) { + lastEateryPinged = eateryId // If first time calling, make new state. updateCache(eateryId, EateryApiResponse.Pending) @@ -125,7 +177,7 @@ class EateryRepository @Inject constructor(private val networkApi: NetworkApi) { } private fun updateCache(eateryId: Int, response: EateryApiResponse) { - eateryApiCache.update { + eateryDetailsCache.update { (it + (eateryId to response)).withDefault { EateryApiResponse.Error } } } @@ -135,9 +187,9 @@ class EateryRepository @Inject constructor(private val networkApi: NetworkApi) { * If ALL eateries are already loaded, then this simply instantly returns that. */ fun getEateryFlow(eateryId: Int): Flow> { - if (!eateryApiCache.value.contains(eateryId)) { + if (!eateryDetailsCache.value.contains(eateryId)) { pingEatery(eateryId) } - return eateryApiCache.map { it.getValue(eateryId) } + return eateryDetailsCache.map { it.getValue(eateryId) } } } diff --git a/app/src/main/java/com/cornellappdev/android/eatery/ui/screens/UpcomingMenuScreen.kt b/app/src/main/java/com/cornellappdev/android/eatery/ui/screens/UpcomingMenuScreen.kt index 5aa0011..5e8ce7a 100644 --- a/app/src/main/java/com/cornellappdev/android/eatery/ui/screens/UpcomingMenuScreen.kt +++ b/app/src/main/java/com/cornellappdev/android/eatery/ui/screens/UpcomingMenuScreen.kt @@ -234,7 +234,7 @@ fun UpcomingMenuScreen( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { - ErrorContent(onTryAgain = upcomingViewModel::pingEateries) + ErrorContent(onTryAgain = upcomingViewModel::retrieveEateries) } } } diff --git a/app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/EateryDetailViewModel.kt b/app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/EateryDetailViewModel.kt index fffca36..ad8396c 100644 --- a/app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/EateryDetailViewModel.kt +++ b/app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/EateryDetailViewModel.kt @@ -63,6 +63,10 @@ class EateryDetailViewModel @Inject constructor( eateryRepository: EateryRepository, private val userRepository: UserRepository ) : ViewModel() { + init { + eateryRepository.changeScreen(EateryRepository.Screen.DETAILS) + } + private val eateryId: Int = checkNotNull(savedStateHandle["eateryId"]) private val _eateryDetailsViewState = diff --git a/app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/HomeViewModel.kt b/app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/HomeViewModel.kt index 4e4272c..181a884 100644 --- a/app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/HomeViewModel.kt +++ b/app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/HomeViewModel.kt @@ -30,6 +30,9 @@ class HomeViewModel @Inject constructor( private val userPreferencesRepository: UserPreferencesRepository, private val eateryRepository: EateryRepository ) : ViewModel() { + init { + eateryRepository.changeScreen(EateryRepository.Screen.HOME) + } private val _filtersFlow: MutableStateFlow> = MutableStateFlow(listOf()) /** @@ -167,6 +170,6 @@ class HomeViewModel @Inject constructor( } fun pingEateries() { - eateryRepository.pingEateries() + eateryRepository.pingHomeEateries() } } diff --git a/app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/UpcomingViewModel.kt b/app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/UpcomingViewModel.kt index 1ce69b1..7c21f77 100644 --- a/app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/UpcomingViewModel.kt +++ b/app/src/main/java/com/cornellappdev/android/eatery/ui/viewmodels/UpcomingViewModel.kt @@ -63,7 +63,7 @@ class UpcomingViewModel @Inject constructor( * A flow emitting all eateries with the appropriate filters applied. */ val viewStateFlow: StateFlow = combine( - eateryRepository.eateryFlow, + eateryRepository.upcomingEateriesFlow, selectedFiltersFlow, userPreferencesRepository.favoriteItemsFlow, mealFilterFlow, @@ -165,39 +165,42 @@ class UpcomingViewModel @Inject constructor( UpcomingMenusViewState(mealFilter = nextMeal() ?: MealFilter.LATE_DINNER) ) + init { + eateryRepository.changeScreen(EateryRepository.Screen.UPCOMING) + retrieveEateries() + } + fun onToggleFilterClicked(filter: Filter) { - if (viewStateFlow.value.menus is EateryApiResponse.Error) { - pingEateries() - } selectedFiltersFlow.update { it.updateFilters(filter) } + if (viewStateFlow.value.menus is EateryApiResponse.Error) { + retrieveEateries() + } } fun onResetFiltersClicked() { - if (viewStateFlow.value.menus is EateryApiResponse.Error) { - pingEateries() - } mealFilterFlow.value = nextMeal() ?: MealFilter.LATE_DINNER selectedFiltersFlow.update { emptyList() } + if (viewStateFlow.value.menus is EateryApiResponse.Error) { + retrieveEateries() + } } fun onMealFilterChanged(filter: MealFilter) { + mealFilterFlow.value = filter if (viewStateFlow.value.menus is EateryApiResponse.Error) { - pingEateries() + retrieveEateries() } - mealFilterFlow.value = filter } fun selectDayOffset(offset: Int) { - if (viewStateFlow.value.menus is EateryApiResponse.Error) { - pingEateries() - } selectedDayFlow.update { offset } + retrieveEateries() } - fun pingEateries() { - eateryRepository.pingEateries() + fun retrieveEateries() { + eateryRepository.retrieveUpcomingMenu(selectedDayFlow.value) } /** From d8c0c46284274779bc24b8449f1c9edce15ca40b Mon Sep 17 00:00:00 2001 From: Caleb Shim Date: Sat, 8 Nov 2025 23:01:25 -0500 Subject: [PATCH 2/2] Add missing withDefault --- .../data/repositories/EateryRepository.kt | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/cornellappdev/android/eatery/data/repositories/EateryRepository.kt b/app/src/main/java/com/cornellappdev/android/eatery/data/repositories/EateryRepository.kt index a56bf26..b7ceaf0 100644 --- a/app/src/main/java/com/cornellappdev/android/eatery/data/repositories/EateryRepository.kt +++ b/app/src/main/java/com/cornellappdev/android/eatery/data/repositories/EateryRepository.kt @@ -79,24 +79,27 @@ class EateryRepository @Inject constructor(private val networkApi: NetworkApi) { * Refreshes the data for the current screen and resets all other stale cache data. */ fun refresh() { - // Re-ping based on current screen and clear all other caches. + val emptyEateryMap = + emptyMap>().withDefault { EateryApiResponse.Error } + val emptyEateriesMap = + emptyMap>>().withDefault { EateryApiResponse.Error } when (currentScreen) { Screen.HOME -> { pingHomeEateries() - eateryDetailsCache.value = emptyMap() - upcomingEateriesCache.value = emptyMap() + eateryDetailsCache.value = emptyEateryMap + upcomingEateriesCache.value = emptyEateriesMap } Screen.DETAILS -> lastEateryPinged?.let { - eateryDetailsCache.value = emptyMap() + eateryDetailsCache.value = emptyEateryMap pingEatery(it) - upcomingEateriesCache.value = emptyMap() + upcomingEateriesCache.value = emptyEateriesMap } Screen.UPCOMING -> lastDayRequested?.let { - upcomingEateriesCache.value = emptyMap() + upcomingEateriesCache.value = emptyEateriesMap pingUpcomingMenu(it) - eateryDetailsCache.value = emptyMap() + eateryDetailsCache.value = emptyEateryMap } } }