Multi-Model State
redux-kotlin-multimodel provides ModelState — an immutable, typesafe bag of
independent feature models keyed by their concrete class. Every screen or
subsystem owns its own model type; one store drives them all.
implementation("org.reduxkotlin:redux-kotlin-multimodel:<version>")
(Already included if you use redux-kotlin-bundle.)
ModelState
import org.reduxkotlin.multimodel.ModelState
data class UserModel(val user: String? = null)
data class CartModel(val items: List<Item> = emptyList())
val state = ModelState.of(UserModel(), CartModel())
val user: UserModel = state.get<UserModel>() // typed read
val next = state.with(UserModel(user = "ann")) // replace one slot
Key properties:
- Sealed key set. The set of model classes is locked at construction
(
ModelState.of(...)) — reducers can replace an instance but never add or remove a slot.get<M>()is therefore guaranteed non-null; asking for an undeclared model class throwsIllegalStateException(a programming error, not a runtime branch to handle). - "Not yet loaded" lives in the model. Give each model a default
constructor or a
NOT_SETsentinel so dependents readStringrather thanString?. - Identity-preserving updates.
with(model)and the batchwithAll(changes)copy the map once and share every untouched slot — unchanged models keep===identity, which is what makes granular subscriptions precise. withAll(other: ModelState)overlays every slot present inotheronto the receiver (its key set must be a subset) — the primitive behindpreloadedStaterehydration.- There is no
getModel()— typed access isstate.get<M>(), or the non-reifiedstate.get(M::class)for raw JS/TS/Swift consumers and generic helpers.
Driving it from a store
Two ways to reduce a ModelState:
Routing DSL (recommended) — redux-kotlin-routing /
the bundle's createConcurrentModelStore declare slots and per-action handlers
in one place:
val store = createConcurrentModelStore {
model(UserModel()) { on<LoggedIn> { s, a -> s.copy(user = a.user) } }
model(CartModel()) { on<AddItem> { s, a -> s.copy(items = s.items + a.item) } }
}
combineModelReducers — the plain-reducer composition from this module,
for stores assembled by hand:
import org.reduxkotlin.multimodel.combineModelReducers
import org.reduxkotlin.multimodel.modelReducer
val rootReducer = combineModelReducers(
modelReducer<UserModel> { model, action ->
when (action) {
is LoggedIn -> model.copy(user = action.user)
else -> model
}
},
modelReducer<CartModel> { model, action -> /* … */ model },
)
val store = createConcurrentStore(rootReducer, ModelState.of(UserModel(), CartModel()))
Each entry sees only its own model. Unlike the routing DSL, every reducer runs
on every action (when-cascade style); a non-reified modelReducerOf(KClass, reducer) exists for generic wiring.
Granular subscriptions: redux-kotlin-multimodel-granular
Subscribes to a single field of a single model — listeners fire only when that
value changes, with (old, new) semantics:
implementation("org.reduxkotlin:redux-kotlin-multimodel-granular:<version>")
import org.reduxkotlin.multimodel.granular.subscribeTo
// The model type is inferred from the property reference's receiver —
// the call site never names ModelState.
val sub = store.subscribeTo(UserModel::user) { old, new ->
println("user: $old -> $new")
}
Inside a subscribeFields { … } batch (one underlying store.subscribe for
many fields, possibly across models), use the on counterpart:
store.subscribeFields {
on(UserModel::user) { _, new -> render(new) }
on(CartModel::items) { _, new -> renderCart(new) }
}
The property-reference overloads are inline + reified, so they are hidden from
Swift and not @JsExported. Raw JS/TS and Swift consumers use the
class-based forms subscribeToModel(M::class, selector, listener) /
onModel(...). Subscription semantics (trigger-on-subscribe, threading,
error handling) are inherited from
granular subscriptions.
Compose bindings: redux-kotlin-compose-multimodel
Binds a model field to a Compose State<T> (included in
redux-kotlin-bundle-compose):
implementation("org.reduxkotlin:redux-kotlin-compose-multimodel:<version>")
import org.reduxkotlin.compose.multimodel.fieldState
@Composable
fun ProfileHeader(store: Store<ModelState>) {
val displayName by store.fieldState(LoggedInUserModel::displayName)
Text("Hello, $displayName")
}
For callers holding the model type as a KClass (raw JS/TS, generic helper
code), the non-inline form:
import org.reduxkotlin.compose.multimodel.fieldStateOf
val displayName by store.fieldStateOf(LoggedInUserModel::class) { it.displayName }
See Compose integration — multi-model stores
for skippability (StableStore) and lifecycle details.
See also
- Routing — the DSL that drives
ModelStateper-action. - Bundles — the one-dependency stack with
ModelStateat the center. - Granular subscriptions — the field-subscription layer underneath.