rocketraman
05/05/2023, 5:30 PMInitialize
input in a repository and a subsequent input. The Initialize
input handler does observeFlows
but the next input executes before the observeFlows
side job starts. Doing the Initialize
earlier in an init
block or via an interceptor works, but feels a bit messy. Is there an idiomatic way to handle this?Casey Brooks
05/05/2023, 9:05 PMInitialize
Input which starts a sideJob, and in taht sideJob send that next Input before starting to observe the flow. observeFlows
is just a basic wrapper around a sideJobrocketraman
05/05/2023, 9:19 PMCasey Brooks
05/05/2023, 9:26 PMobject MyRepository {
data class State(
val repositoryValue: String? = null,
val initialValueFromUi: String? = null,
) {
val derivedValue: String = if(repositoryValue != null && initialValueFromUi != null) {
computeDerivedValue(repositoryValue, initialValueFromUi)
} else {
null
}
}
sealed class Inputs {
object InitializeFlow : Inputs()
data class RepositoryValueUpdated(val repositoryValue: String) : Inputs()
data class InitializeFromUi(val initialValueFromUi: String) : Inputs()
}
}
rocketraman
05/05/2023, 10:09 PMCasey Brooks
05/06/2023, 3:37 PMrocketraman
05/07/2023, 4:34 AMobserveFlows
side job that was kicked off by (3a), which meant no entities were retrieved.
Yes, my app only cares about the state of the user and the entities. But the ordering requirement here is simply that the fetch of the entities must occur after a user is available, so there is an ordering requirement between input (3a) and (3b). I can't have (3a) start (3b) as you initially suggested because that violates my UDF requirement (user repo doesn't know anything about entity repo, it sits above it).
I think one option that leverages state would be: (3a) checks the user state, and if no user is available, it sets some state like entityFetchRequested=true
and then triggers the user initialize. If a user is available in the state, it triggers the fetch directly. Once a user becomes available, we check if entityFetchRequested=true
, and if it is, trigger the fetch entities. Is that right? It feels messy to me to have some state that is only useful for this temporary ordering purpose, and of course in reality there are many of these entities so we would need 10s or 100s of these temporary state variables.
How would you do this?rocketraman
05/15/2023, 6:48 PMCasey Brooks
05/15/2023, 6:49 PMrocketraman
05/15/2023, 6:49 PMCasey Brooks
05/22/2023, 4:04 PMkotlin
object EntityRepositoryContract {
data class State(
val authState: AuthState = AuthState(false, null),
val entities: List<Entity>? = null, // null when not fetched yet
)
sealed class Inputs {
object Initialize : Inputs()
data class AuthStateLoaded(val authState: AuthState) : Inputs()
data class EntitiesLoaded(val entities: List<Entity>) : Inputs()
}
sealed class Events
}
which is pretty basic, and doesn’t really give much info about the required loading order between the AuthState and the Entities. And that’s actually a good thing: the UI really shouldn’t know much about how these values are fetches, just that they exist as a form of state. So the UI then just observes the State, or uses Flow operators to select the first valid combination of state values, and it needs to make sure that the data-loading gets kicked off at the right time.
kotlin
class EntityRepository : BallastViewModel<EntityRepositoryContract.Inputs, EntityRepositoryContract.Events, EntityRepositoryContract.State> {
// use this to observe all changes in the UI
fun observeEntities(): Flow<List<Entity>> = observeState()
// when the UI starts requesting this data, make sure we try to fetch it again
.onStart { trySend(EntityRepositoryContract.Initialize) }
// wait for the repository state to stabilize
.filter { it.authState.loggedIn && it.entities != null }
// select the specific value you need from the state
.mapNotNull { it.entities }
// or use this for a single-shot value
suspend fun observeEntities(): List<Entity> {
return observeEntities().first()
}
}
The complexity around the caching of the authState and entities should be handled entirely within the implementation (the InputHandler), so that the UI doesn’t have to worry about it. And internally, we’ll set up the fetches such that anytime the AuthState changes, we’ll re-fetch the entities.
kotlin
class EntityRepositoryInputHandler(
private val userRepository: UserRepository,
private val entityDatabase: EntityDatabase,
) : InputHandler<
EntityRepositoryContract.Inputs,
EntityRepositoryContract.Events,
EntityRepositoryContract.State> {
override suspend fun InputHandlerScope<
EntityRepositoryContract.Inputs,
EntityRepositoryContract.Events,
EntityRepositoryContract.State>.handleInput(
input: EntityRepositoryContract.Inputs
) = when (input) {
is EntityRepositoryContract.Inputs.Initialize -> {
observeFlows(
"authState",
userRepository
.getAuthState()
.map { EntityRepositoryContract.Inputs.AuthStateLoaded(it) }
)
}
is EntityRepositoryContract.Inputs.AuthStateLoaded -> {
val updatedState = updateStateAndGet { it.copy(authState = input.authState) }
if(updatedState.authState.loggedIn) {
observeFlows(
"entities",
entityDatabase
.getEntitiesForUser(username = updatedState.authState.username)
.map { EntityRepositoryContract.Inputs.EntitiesLoaded(it) }
)
} else {
noOp()
}
}
is EntityRepositoryContract.Inputs.EntitiesLoaded -> {
updateState { it.copy(entities = input.entities) }
}
}
}
Furthermore, with this setup, we can assume a similar situation is happening with the UserRepository. When the EntityRepository subscribes to the authState, it will internally fetch the authState if needed, or cache the value for faster access from the downstream repos. The EntityRepository really only cares about the current state of the user, and doesn’t really care about how the value is actually fetched or managed.
In this way, we are able to structure the repositories as separate things, where each “layer” is able to cache its own data and fetch it in the order it needs, without caring how its parent layers cache their data, and without leaking implementation details like input ordering to the parent/child layers. You can think of each VM as a “checkpoint”, where there is a lot of complexity, but ultimately it will eventually stabilize around some “good” value, and that “good” value is the only thing that the downstream layers really need to care about.rocketraman
05/23/2023, 2:33 PMCasey Brooks
05/23/2023, 4:59 PMballast-repository
module, introducing some kind of API to make these fetch/cache operations simpler and more feasible to do at scale, using this same basic idea