
Minimal Redux-style state management using reactive flows, immutable state classes, repo-as-action pattern, property-level flows, suspendable observers, and repo-restricted state mutation.
English | 中文
https://github.com/airbnb/mavericks
Not RxJava — a minimal Redux built on Kotlin Coroutines Flow.
Read the full introduction (Chinese): Redux-for-Android
A GUI app usually has 3 roles:
RxState marker interface)implementation("io.github.xechoz:mvs:1.1.0")Kotlin Multiplatform library targeting Android and JVM (mvs-android / mvs-jvm are resolved automatically via Gradle module metadata); depends on Kotlin Coroutines. Android minSdk 26.
For development inside this repository, use implementation(project(":lib:mvs")) instead.
// state
data class CounterState(val count: Int = 0) : RxState
// repo (= action)
class CounterRepo : RxRepo<CounterState> by RxRepo(initState = CounterState()) {
fun increment() {
setState {
copy(count = count + 1)
}
}
}
// view
class CounterView {
private val counter = CounterRepo()
private val scope = MainScope()
init {
scope.launch {
counter.onEach(CounterState::count) {
text = "Click $it"
}
}
setOnClickListener {
counter.increment()
}
}
}
// with ViewModel
class FooViewModel : ViewModel(),
RxRepo<FooState> by RxRepo(initState = FooState()) {
fun updateFooToBar(bar: Any) {
setState {
copy(foo = bar)
}
}
}| Member | Description |
|---|---|
val state: State |
Synchronously read the current state |
fun RxRepo<State>.setState(reducer: State.() -> State) |
Update state; the reducer receives the old state as receiver and returns the new state |
fun asFlow(): Flow<State> |
Expose the whole state as a Flow |
suspend fun onEach(action: (State) -> Unit) |
Observe the whole state |
suspend fun <A : Any> onEach(property: KProperty1<State, A>, action: (A) -> Unit) |
Observe a single property |
suspend fun <A : Any> onEach(property: KProperty1<State, A>): Flow<A> |
Expose a property as a Flow, ready to be combined |
fun <State : RxState> RxRepo(initState: State): RxRepo<State> |
Factory function to create a repo |
Notes:
setState is declared as an extension member of RxRepo, so it can only be called from a repo implementation or its extension functions, preventing external mutation. Add logic to a repo via extension functions:
fun TodoRepo.remove(todoRow: TodoRow) {
setState {
copy(todos = todos.filter { it != todoRow })
}
}The subscription lifetime of onEach is managed by the caller's coroutine scope: call it inside scope.launch { }; scope.cancel() stops observing.
If onEach is not enough (e.g. combining multiple properties), use asFlow() instead.
app module: Todo.kt, Todo.Repo.kt, Todo.Ui.kt
English | 中文
https://github.com/airbnb/mavericks
Not RxJava — a minimal Redux built on Kotlin Coroutines Flow.
Read the full introduction (Chinese): Redux-for-Android
A GUI app usually has 3 roles:
RxState marker interface)implementation("io.github.xechoz:mvs:1.1.0")Kotlin Multiplatform library targeting Android and JVM (mvs-android / mvs-jvm are resolved automatically via Gradle module metadata); depends on Kotlin Coroutines. Android minSdk 26.
For development inside this repository, use implementation(project(":lib:mvs")) instead.
// state
data class CounterState(val count: Int = 0) : RxState
// repo (= action)
class CounterRepo : RxRepo<CounterState> by RxRepo(initState = CounterState()) {
fun increment() {
setState {
copy(count = count + 1)
}
}
}
// view
class CounterView {
private val counter = CounterRepo()
private val scope = MainScope()
init {
scope.launch {
counter.onEach(CounterState::count) {
text = "Click $it"
}
}
setOnClickListener {
counter.increment()
}
}
}
// with ViewModel
class FooViewModel : ViewModel(),
RxRepo<FooState> by RxRepo(initState = FooState()) {
fun updateFooToBar(bar: Any) {
setState {
copy(foo = bar)
}
}
}| Member | Description |
|---|---|
val state: State |
Synchronously read the current state |
fun RxRepo<State>.setState(reducer: State.() -> State) |
Update state; the reducer receives the old state as receiver and returns the new state |
fun asFlow(): Flow<State> |
Expose the whole state as a Flow |
suspend fun onEach(action: (State) -> Unit) |
Observe the whole state |
suspend fun <A : Any> onEach(property: KProperty1<State, A>, action: (A) -> Unit) |
Observe a single property |
suspend fun <A : Any> onEach(property: KProperty1<State, A>): Flow<A> |
Expose a property as a Flow, ready to be combined |
fun <State : RxState> RxRepo(initState: State): RxRepo<State> |
Factory function to create a repo |
Notes:
setState is declared as an extension member of RxRepo, so it can only be called from a repo implementation or its extension functions, preventing external mutation. Add logic to a repo via extension functions:
fun TodoRepo.remove(todoRow: TodoRow) {
setState {
copy(todos = todos.filter { it != todoRow })
}
}The subscription lifetime of onEach is managed by the caller's coroutine scope: call it inside scope.launch { }; scope.cancel() stops observing.
If onEach is not enough (e.g. combining multiple properties), use asFlow() instead.
app module: Todo.kt, Todo.Repo.kt, Todo.Ui.kt