docs/core-concepts.md
Mastering Mavericks only requires using three classes: MavericksState, MavericksViewModel, and MavericksView.
The first step in creating a Mavericks screen is to model it as a function of state. The MavericksState interface doesn't do anything itself but signals the intention of your class to be used as state.
Modeling a screen as a function of state is a useful concept because it is:
Mavericks will also enforce that your state class:
Mavericks enforces these through its debug checks
This concept makes reasoning about and testing a screen trivially easy because given a state class, you can have high confidence that your screen will look correct. Example
data class UserState(
val score: Int = 0,
val previousHighScore: Int = 150,
val livesLeft: Int = 99,
) : MavericksState
Because state is just a normal Kotlin data class, you can create derived properties to represent specific state conditions like this:
data class UserState(
val score: Int = 0,
val previousHighScore: Int = 150,
val livesLeft: Int = 99,
) : MavericksState {
// Properties inside the body of your state class are "derived".
val pointsUntilHighScore = (previousHighScore - score).coerceAtLeast(0)
val isHighScore = score >= previousHighScore
}
Placing logic as a derived property like pointsUntilHighScore or isHighScore means that:
onEach.A ViewModel is responsible for:
Mavericks ViewModels are conceptually nearly identical to Jetpack ViewModels with the addition of being generic on a MavericksState class.
From within a viewModel, you call setState { copy(yourProp = newValue) }. If this syntax is unfamiliar:
S.() -> S meaning the receiver (aka this) of the lambda is the current state when the lambda is invoked and it returns the new lambdacopy comes from the fact that the state class is a Kotlin data classHandling asynchronous operations with ease was one of the primary goals of Mavericks. Check out the docs for Async<T> and execute(...) to learn more.
You can subscribe to state changes in your ViewModel. You may want to do this for analytics, for example. This usually done in the init { ... } block.
// Invoked every time state changes
onEach { state ->
}
// Invoked whenever propA changes only.
onEach(YourState::propA) { a ->
}
// Invoked whenever propA, propB, or propC changes only.
onEach(YourState::propA, YourState::propB, YourState::propC) { a, b, c ->
}
TIP: If you are calling setState from within an onEach block, consider using a derived property.
MavericksViewModel exposes a stateFlow property which is a normal Kotlin Flow that emits the current state as well as any future updates and can be used however you would like. Helpers such as onEach above are just wrappers around it with automatic lifecycle cancellation.
If you just want to retrieve the value of state one time, you can use withState { state -> ... }.
When called from within a ViewModel, this will not be run synchronously. It will be placed on a background queue so that all pending setState reducers are called prior to your withState call.
MavericksView is where you actually render your state class to the screen. Most of the time, this will be a Fragment but it doesn't have to be.
By implementing MavericksView, you:
MavericksViewModel via any of the view model delegates. Doing so will automatically subscribe to changes and call invalidate().invalidate() function. It is called any time the state for any view model accessed by the above delegates changes. invalidate() is used to redraw the UI on each state changeactivityViewModel() scopes the ViewModel to the Activity. All Fragments within the Activity that request a ViewModel of this type will receive the same instance. Useful for sharing data between screens.fragmentViewModel() scopes the ViewModel to the Fragment. It will be accessible to children fragments but parent or sibling fragments would get a different instance.parentFragmentViewModel() walks up the parent fragment tree until it finds one that has already created a ViewModel of the desired type. If none is found, it will automatically create one scoped to the highest parent fragment.existingViewModel() Same as activityViewModel() except it throws if the ViewModel was not already created by somebody else.navGraphViewModel(navGraphId: Int) scopes the ViewModel to the Jetpack Navigation graph with that id. This requires the mvrx-navigation artifact (docs).If you want multiple ViewModels of the same type, you can pass a keyFactory into any of the delegates.
Most of the time, overriding invalidate() and updating your views is sufficient. However, if you want to subscribe to state to do things like start animations, you may call any of the onEach subscriptions on your ViewModel. If your view is a Fragment, these subscriptions should be set up in onCreate().
If you just want to retrieve the value of state one time, you can use withState { state -> ... }.
When called from outside a ViewModel, this will be run synchronously.
The ViewModel should expose named functions that can be called by the view. For example, a counter view model could expose a function incrementCount() to create a clear API accessible to the view.