Skip to content

Commit

Permalink
refactor: Appearing replacement, validators update time regularly
Browse files Browse the repository at this point in the history
  • Loading branch information
Lastaapps committed Oct 6, 2024
1 parent 325eb85 commit 9a9f0dc
Show file tree
Hide file tree
Showing 11 changed files with 149 additions and 66 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import cz.lastaapps.api.core.domain.validity.ValidityKey
import cz.lastaapps.api.core.domain.validity.withCheckSince
import cz.lastaapps.core.domain.OutcomeIor
import cz.lastaapps.core.util.extensions.CET
import cz.lastaapps.core.util.extensions.durationTicker
import cz.lastaapps.core.util.extensions.findDayOfWeek
import cz.lastaapps.core.util.extensions.localLogger
import kotlinx.collections.immutable.ImmutableList
Expand Down Expand Up @@ -73,17 +74,18 @@ internal class DishLogicImpl(
) {
private val log = localLogger()

private val validFrom =
clock
.now()
.toLocalDateTime(TimeZone.CET)
.date
.findDayOfWeek(DayOfWeek.SATURDAY)
.let { LocalDateTime(it, LocalTime(12, 0)) }
.toInstant(TimeZone.CET)
private val validFrom get() =
clock.durationTicker().map {
it
.toLocalDateTime(TimeZone.CET)
.date
.findDayOfWeek(DayOfWeek.SATURDAY)
.let { LocalDateTime(it, LocalTime(12, 0)) }
.toInstant(TimeZone.CET)
}

private val validityKey = ValidityKey.buffetDish()
private val hasValidData =
private val hasValidData get() =
checker
.isUpdatedSince(validityKey, validFrom)
.onEach { log.i { "Validity changed to $it" } }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ import cz.lastaapps.api.core.domain.validity.isUpdatedSince
import cz.lastaapps.core.util.extensions.CET
import cz.lastaapps.core.util.extensions.atMidnight
import cz.lastaapps.core.util.extensions.deserializeValueOrNullFlow
import cz.lastaapps.core.util.extensions.durationTicker
import cz.lastaapps.core.util.extensions.findMonday
import cz.lastaapps.core.util.extensions.serializeValue
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.datetime.Clock
Expand All @@ -49,6 +51,7 @@ internal class ValidityCheckerImpl(
validitySettings: ValiditySettings,
) : ValidityChecker {
private val settings = validitySettings.settings
private val timeZone = TimeZone.CET

companion object {
private const val PREFIX = "validity_"
Expand All @@ -69,36 +72,44 @@ internal class ValidityCheckerImpl(

override fun isFromToday(key: ValidityKey): Flow<Boolean> {
val today =
clock
.now()
.toLocalDateTime(TimeZone.CET)
.date
.atMidnight()
.toInstant(TimeZone.CET)
clock.durationTicker().map {
it
.toLocalDateTime(timeZone)
.date
.atMidnight()
.toInstant(timeZone)
}
return isUpdatedSince(key, today).distinctUntilChanged()
}

override fun isThisWeek(key: ValidityKey): Flow<Boolean> {
val weekStart =
clock
.now()
.toLocalDateTime(TimeZone.CET)
.date
.findMonday()
return isUpdatedSince(key, weekStart, TimeZone.CET).distinctUntilChanged()
clock.durationTicker().map {
it
.toLocalDateTime(timeZone)
.date
.findMonday()
}
return isUpdatedSince(key, weekStart, timeZone).distinctUntilChanged()
}

override fun isUpdatedSince(
key: ValidityKey,
duration: Duration,
): Flow<Boolean> = isUpdatedSince(key, clock.now() - duration).distinctUntilChanged()
): Flow<Boolean> =
isUpdatedSince(
key,
clock.durationTicker().map { it - duration },
).distinctUntilChanged()

override fun isUpdatedSince(
key: ValidityKey,
date: Instant,
date: Flow<Instant>,
): Flow<Boolean> =
settings
.deserializeValueOrNullFlow(Instant.serializer(), key(key))
.map { it != null && it >= date }
.distinctUntilChanged()
combine(
settings.deserializeValueOrNullFlow(Instant.serializer(), key(key)).distinctUntilChanged(),
date.distinctUntilChanged(),
) { saved, date ->
saved != null && saved >= date
}.distinctUntilChanged()
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ package cz.lastaapps.api.core.domain.validity
import arrow.core.right
import cz.lastaapps.api.core.domain.sync.SyncOutcome
import cz.lastaapps.api.core.domain.sync.SyncResult
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.datetime.Instant
import kotlin.time.Duration
Expand All @@ -35,7 +36,7 @@ suspend inline fun ValidityChecker.withCheckRecent(
suspend inline fun ValidityChecker.withCheckSince(
key: ValidityKey,
forced: Boolean,
date: Instant,
date: Flow<Instant>,
block: () -> SyncOutcome,
): SyncOutcome = withCheck(key, forced, { isUpdatedSince(key, date).first() }, block)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ package cz.lastaapps.api.core.domain.validity
import cz.lastaapps.core.util.extensions.CET
import cz.lastaapps.core.util.extensions.atMidnight
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
Expand All @@ -36,25 +37,40 @@ interface ValidityChecker {

suspend fun onDataUpdated(key: ValidityKey)

/**
* Returns true if data was updated recently
*/
fun isRecent(key: ValidityKey): Flow<Boolean>

/**
* Returns true if data were fetched today or later
*/
fun isFromToday(key: ValidityKey): Flow<Boolean>

/**
* Returns true if data was fetched this week or later
*/
fun isThisWeek(key: ValidityKey): Flow<Boolean>

/**
* Returns true if data was updated in the last [duration]
*/
fun isUpdatedSince(
key: ValidityKey,
duration: Duration,
): Flow<Boolean>

/**
* Checks if the data were updated since the [date] given
*/
fun isUpdatedSince(
key: ValidityKey,
date: Instant,
date: Flow<Instant>,
): Flow<Boolean>
}

fun ValidityChecker.isUpdatedSince(
key: ValidityKey,
date: LocalDate,
date: Flow<LocalDate>,
zone: TimeZone = TimeZone.CET,
): Flow<Boolean> = isUpdatedSince(key, date.atMidnight().toInstant(zone))
): Flow<Boolean> = isUpdatedSince(key, date.map { it.atMidnight().toInstant(zone) })
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ import coil3.compose.SubcomposeAsyncImage
import coil3.request.CachePolicy.DISABLED
import coil3.request.ImageRequest.Builder
import cz.lastaapps.api.core.domain.model.Dish
import cz.lastaapps.core.ui.vm.HandleAppear
import cz.lastaapps.core.ui.vm.HandleDismiss
import cz.lastaapps.menza.R
import cz.lastaapps.menza.features.main.ui.widgets.WrapMenzaNotSelected
Expand Down Expand Up @@ -97,7 +96,6 @@ private fun TodayEffects(
viewModel: TodayViewModel,
onRating: (DishForRating) -> Unit,
) {
HandleAppear(viewModel)
HandleDismiss(
viewModel,
TodayState::dishForRating,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,41 +23,38 @@ import arrow.core.Option
import arrow.core.toOption
import cz.lastaapps.api.core.domain.model.Dish
import cz.lastaapps.api.core.domain.model.Menza
import cz.lastaapps.core.ui.vm.Appearing
import cz.lastaapps.core.ui.vm.StateViewModel
import cz.lastaapps.core.ui.vm.VMContext
import cz.lastaapps.core.ui.vm.VMState
import cz.lastaapps.menza.features.main.domain.usecase.GetSelectedMenzaUC
import cz.lastaapps.menza.features.settings.domain.model.DishLanguage
import cz.lastaapps.menza.features.settings.domain.usecase.settings.GetDishLanguageUC
import cz.lastaapps.menza.features.today.ui.model.DishForRating
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach

internal class TodayViewModel(
context: VMContext,
private val getSelectedMenza: GetSelectedMenzaUC,
private val getDishLanguageUC: GetDishLanguageUC,
) : StateViewModel<TodayState>(TodayState(), context),
Appearing {
override var hasAppeared: Boolean = false
) : StateViewModel<TodayState>(TodayState(), context) {
override suspend fun CoroutineScope.whileCollected() {
getSelectedMenza()
.onEach {
updateState {
copy(
selectedMenza = it.toOption(),
selectedDish = null,
)
}
}.launchIn(this)

override fun onAppeared() =
launchVM {
getSelectedMenza()
.onEach {
updateState {
copy(
selectedMenza = it.toOption(),
selectedDish = null,
)
}
}.launchInVM()

getDishLanguageUC()
.onEach {
updateState { copy(language = it) }
}.launchInVM()
}
getDishLanguageUC()
.onEach {
updateState { copy(language = it) }
}.launchIn(this)
}

fun selectDish(dish: Dish?) = updateState { copy(selectedDish = dish) }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -278,14 +278,15 @@ private fun DishItem(
ratio = null,
modifier =
Modifier
.fillMaxSize()
// zoom in effect
.graphicsLayer {
val max = 0.1f
val scale = 1f + (1f - progress()) * max
scaleX = scale
scaleY = scale
},
.fillMaxSize(),
// zoom in effect
// the carousel does not render correctly
// .graphicsLayer {
// val max = 0.1f
// val scale = 1f + (1f - progress()) * max
// scaleX = scale
// scaleY = scale
// },
)

val useGradient = dish.photoLink != null
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2023, Petr Laštovička as Lasta apps, All rights reserved
* Copyright 2024, Petr Laštovička as Lasta apps, All rights reserved
*
* This file is part of Menza.
*
Expand All @@ -23,6 +23,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import cz.lastaapps.core.util.extensions.localLogger

@Deprecated("Use [StateViewModel.onCollected] instead")
interface Appearing {
var hasAppeared: Boolean

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,14 @@ import androidx.compose.runtime.Immutable
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import arrow.fx.coroutines.resource
import arrow.fx.coroutines.resourceScope
import cz.lastaapps.core.util.extensions.whileSubscribed
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlin.time.Duration.Companion.seconds

abstract class StateViewModel<State : VMState>(
init: State,
Expand All @@ -38,10 +43,25 @@ abstract class StateViewModel<State : VMState>(

protected fun updateState(block: State.() -> State) = myState.update(block)

val flow = myState.asStateFlow()
/**
* Is called once an external entity starts collecting local state.
* Provides a coroutine scope that is cancelled 5 seconds after
* there are no more collectors.
*/
protected open suspend fun CoroutineScope.whileCollected() {}

val flow =
myState
.whileSubscribed { whileCollected() }
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5.seconds),
myState.value,
)

val flowState
@Composable
get() = myState.collectAsStateWithLifecycle()
get() = flow.collectAsStateWithLifecycle()

protected suspend fun <R> withLoading(
loading: State.(isLoading: Boolean) -> State,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import arrow.core.right
import cz.lastaapps.core.domain.error.CommonError
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.cancel
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
Expand Down Expand Up @@ -105,3 +107,21 @@ fun <T> List<Flow<T>>.foldBinary(
initial: T,
operation: (T, T) -> T,
): Flow<T> = foldBinary(initial, { it }, operation)

/**
* Similar to onStart, but provides a coroutine scope
* with the same lifetime as the collected flow,
* eg. closed after onCompletion is called
*/
fun <T> Flow<T>.whileSubscribed(onStart: suspend CoroutineScope.() -> Unit) =
flow {
val scope = CoroutineScope(currentCoroutineContext())
onStart(scope)
try {
collect(this)
} catch (e: Throwable) {
scope.cancel()
throw e
}
scope.cancel()
}
Loading

0 comments on commit 9a9f0dc

Please sign in to comment.