Dispatch → Reduce → Effects — A lightweight, opinionated state management library for Kotlin & Android.
dre-kt implements the DRE pattern: a unidirectional data flow architecture where Actions are dispatched, a pure Reducer computes new state + side effects, and Effects are handled externally. Async operations feed results back as new actions.
Add to your build.gradle.kts:
Check the latest version on Maven Central.
dependencies {
// Core only (no Android dependency)
implementation("io.github.dantech0xff:dre-core:<latest-version>")
// Android ViewModel integration (includes dre-core)
implementation("io.github.dantech0xff:dre-android:<latest-version>")
}Install the dre-integrate skill for Claude Code and Codex:
gh api -H "Accept: application/vnd.github.raw" "repos/dantech0xff/dre-kt/contents/install-skill.sh?ref=master" | bashRequires authenticated GitHub CLI (gh auth login) or GH_TOKEN.
Then use in Claude Code:
/dre-integrate setup— Add dependency to your project/dre-integrate feature login— Scaffold a new feature/dre-integrate migrate— Migrate existing ViewModel to DRE
Or use $dre-integrate in Codex.
| Module | Description | Dependencies |
|---|---|---|
dre-core |
Platform-agnostic dispatch loop, reducer, and types | kotlinx-coroutines |
dre-android |
Android ViewModel integration | dre-core, lifecycle-viewmodel |
sample |
Counter app demonstrating the DRE pattern | dre-android, Compose |
data class MyState(val count: Int = 0) : DreState
sealed interface MyAction : DreAction {
data object Increment : MyAction
data class DataLoaded(val value: Int) : MyAction
}
sealed interface MyEffect : DreEffect {
data class ShowToast(val msg: String) : MyEffect
}
sealed interface MyAsyncOp : DreAsyncOp {
data object FetchData : MyAsyncOp
}class MyReducer : Reducer<MyState, MyAction, MyEffect, MyAsyncOp> {
override fun reduce(state: MyState, action: MyAction) = when (action) {
is MyAction.Increment -> ReduceResult(state.copy(count = state.count + 1))
is MyAction.DataLoaded -> ReduceResult(
state = state.copy(count = action.value),
sideEffects = listOf(MyEffect.ShowToast("Loaded!")),
)
}
}class MyViewModel(reducer: MyReducer) : DreStoreViewModel<MyState, MyAction, MyEffect, MyAsyncOp>(
reducer = reducer,
) {
override val initialState = MyState()
override suspend fun executeAsyncOp(op: MyAsyncOp, stateSnapshot: MyState) {
when (op) {
is MyAsyncOp.FetchData -> {
val data = repository.load()
dispatch(MyAction.DataLoaded(data))
}
}
}
fun increment() = dispatch(MyAction.Increment)
}val store = DreStore(
reducer = MyReducer(),
initialState = MyState(),
scope = coroutineScope,
dispatchContext = Dispatchers.Main.immediate,
onAsyncOp = { op, snapshot -> /* handle async */ },
)
store.dispatch(MyAction.Increment)
store.state.collect { /* observe */ }Action ──→ Reducer (pure) ──→ ReduceResult
│ │
│ ┌─────┼──────────┐
│ ▼ ▼ ▼
│ State Effects AsyncOp
│ (Flow) (handlers) (I/O)
│ │
└────────────────────────────────┘
(result action)
Reducers are pure functions — test without coroutines:
@Test fun `increment updates count`() {
reducer.assertReduce(given = MyState(0), action = MyAction.Increment) {
assertThat(state.count).isEqualTo(1)
}
}
@Test fun `action rejected in wrong state`() {
reducer.assertNoChange(MyState(phase = "loading"), MyAction.Submit)
}- Kotlin 2.3+
- Android: minSdk 26, compileSdk 36
- Java 17
MIT License — free to use, modify, distribute. Do whatever you want with it.
Just give me a star if you find it useful.
See LICENSE for details.