Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import retrofit2.http.POST
import retrofit2.http.Path

interface NetworkApi {
@GET("/eatery/")
suspend fun fetchEateries(): List<Eatery>
@GET("/eatery/day/{day_id}")
suspend fun fetchEateriesByDay(@Path(value = "day_id") dayId: Int): List<Eatery>

@GET("/eatery/{eatery_id}")
suspend fun fetchEatery(@Path(value = "eatery_id") eateryId: String): Eatery
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,29 @@ import javax.inject.Singleton

@Singleton
class EateryRepository @Inject constructor(private val networkApi: NetworkApi) {
private suspend fun getAllEateries(): List<Eatery> =
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())

private suspend fun getHomeEateries(): List<Eatery> =
networkApi.fetchHomeEateries()

private suspend fun getUpcomingEateries(day: Int): List<Eatery> =
networkApi.fetchEateriesByDay(dayId = day)

private val _eateryFlow: MutableStateFlow<EateryApiResponse<List<Eatery>>> =
MutableStateFlow(EateryApiResponse.Pending)

Expand All @@ -42,53 +56,58 @@ class EateryRepository @Inject constructor(private val networkApi: NetworkApi) {
*/
val homeEateryFlow = _homeEateryFlow.asStateFlow()

private val _upcomingEateriesFlow: MutableStateFlow<EateryApiResponse<List<Eatery>>> =
MutableStateFlow(EateryApiResponse.Pending)

val upcomingEateriesFlow = _upcomingEateriesFlow.asStateFlow()

/**
* A map from eatery ids to the states representing their API loading calls.
*/
private val eateryApiCache: MutableStateFlow<Map<Int, EateryApiResponse<Eatery>>> =
private val eateryDetailsCache: MutableStateFlow<Map<Int, EateryApiResponse<Eatery>>> =
MutableStateFlow(mapOf<Int, EateryApiResponse<Eatery>>().withDefault { EateryApiResponse.Error })

private val upcomingEateriesCache: MutableStateFlow<Map<Int, EateryApiResponse<List<Eatery>>>> =
MutableStateFlow(mapOf<Int, EateryApiResponse<List<Eatery>>>().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 { _ ->
eateries.filter { it.id != null }
.associate { it.id!! to EateryApiResponse.Success(it) }
.withDefault { EateryApiResponse.Error }
}
} catch (_: Exception) {
_eateryFlow.value = EateryApiResponse.Error
eateryApiCache.update {
emptyMap<Int, EateryApiResponse<Eatery>>().withDefault { EateryApiResponse.Error }
}
fun refresh() {
val emptyEateryMap =
emptyMap<Int, EateryApiResponse<Eatery>>().withDefault { EateryApiResponse.Error }
val emptyEateriesMap =
emptyMap<Int, EateryApiResponse<List<Eatery>>>().withDefault { EateryApiResponse.Error }
when (currentScreen) {
Screen.HOME -> {
pingHomeEateries()
eateryDetailsCache.value = emptyEateryMap
upcomingEateriesCache.value = emptyEateriesMap
}

Screen.DETAILS -> lastEateryPinged?.let {
eateryDetailsCache.value = emptyEateryMap
pingEatery(it)
upcomingEateriesCache.value = emptyEateriesMap
}

Screen.UPCOMING -> lastDayRequested?.let {
upcomingEateriesCache.value = emptyEateriesMap
pingUpcomingMenu(it)
eateryDetailsCache.value = emptyEateryMap
}
}
}

/**
* 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 {
Expand All @@ -100,12 +119,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)

Expand All @@ -120,7 +180,7 @@ class EateryRepository @Inject constructor(private val networkApi: NetworkApi) {
}

private fun updateCache(eateryId: Int, response: EateryApiResponse<Eatery>) {
eateryApiCache.update {
eateryDetailsCache.update {
(it + (eateryId to response)).withDefault { EateryApiResponse.Error }
}
}
Expand All @@ -130,9 +190,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<EateryApiResponse<Eatery>> {
if (!eateryApiCache.value.contains(eateryId)) {
if (!eateryDetailsCache.value.contains(eateryId)) {
pingEatery(eateryId)
}
return eateryApiCache.map { it.getValue(eateryId) }
return eateryDetailsCache.map { it.getValue(eateryId) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ fun UpcomingMenuScreen(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
ErrorContent(onTryAgain = upcomingViewModel::pingEateries)
ErrorContent(onTryAgain = upcomingViewModel::retrieveEateries)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<List<Filter>> = MutableStateFlow(listOf())

/**
Expand Down Expand Up @@ -167,6 +170,6 @@ class HomeViewModel @Inject constructor(
}

fun pingEateries() {
eateryRepository.pingEateries()
eateryRepository.pingHomeEateries()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ class UpcomingViewModel @Inject constructor(
* A flow emitting all eateries with the appropriate filters applied.
*/
val viewStateFlow: StateFlow<UpcomingMenusViewState> = combine(
eateryRepository.eateryFlow,
eateryRepository.upcomingEateriesFlow,
selectedFiltersFlow,
userPreferencesRepository.favoriteItemsFlow,
mealFilterFlow,
Expand Down Expand Up @@ -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)
}

/**
Expand Down