
Redux-style state management with modern toolkit features: type-safe reducer DSL, reactive StateFlow, memoized selectors, async thunks, entity adapter, listener middleware, slice pattern.
A Kotlin Multiplatform Redux library with modern Redux Toolkit features, type-safe DSLs, and Compose Multiplatform integration.
| Feature | Description |
|---|---|
| Kotlin DSL | Type-safe reducers with reducer { on<Action> { } } syntax |
| StateFlow | Reactive state powered by Kotlin Coroutines |
| Memoized Selectors |
createSelector() with automatic caching |
| Async Thunks |
createAsyncThunk() with pending/fulfilled/rejected lifecycle |
| Entity Adapter |
createEntityAdapter() for normalized CRUD operations |
| Listener Middleware | Side-effect handling with addListener()
|
| Slice Pattern |
createSlice() to combine reducer + actions |
| Compose Integration |
collectAsState() for Compose Multiplatform |
| Platform | Status |
|---|---|
| Android | โ |
| iOS | โ |
| Desktop (JVM) | โ |
| Web (JS) | โ |
| Web (WASM) |
// build.gradle.kts
dependencies {
implementation("in.sitharaj:redux-kmp:1.0.0")
}data class CounterState(
val count: Int = 0,
val loading: Boolean = false
) : State
sealed interface CounterAction : Action {
data object Increment : CounterAction
data object Decrement : CounterAction
data class SetCount(val value: Int) : CounterAction
}val counterReducer = reducer<CounterState> {
on<CounterAction.Increment> { state, _ ->
state.copy(count = state.count + 1)
}
on<CounterAction.Decrement> { state, _ ->
state.copy(count = state.count - 1)
}
on<CounterAction.SetCount> { state, action ->
state.copy(count = action.value)
}
}val store = createStore(
initialState = CounterState(),
reducer = counterReducer,
scope = CoroutineScope(Dispatchers.Main)
) {
addMiddleware(ThunkMiddleware())
addMiddleware(LoggingMiddleware(tag = "Counter"))
}store.dispatch(CounterAction.Increment)
// Observe state
store.state.collect { state ->
println("Count: ${state.count}")
}@Composable
fun CounterScreen() {
val state by store.state.collectAsState()
Column {
Text("Count: ${state.count}")
Button(onClick = { store.dispatch(CounterAction.Increment) }) {
Text("Increment")
}
}
}val selectCount = selector<AppState, Int> { it.counter }
val selectDoubleCount = createSelector(selectCount) { count ->
count * 2 // Only recomputes when count changes
}
// Multiple inputs
val selectTotal = createSelector(
{ state: CartState -> state.items },
{ state: CartState -> state.taxRate }
) { items, taxRate ->
items.sumOf { it.price } * (1 + taxRate)
}val fetchUser = createAsyncThunk<String, User, AppState>(
typePrefix = "users/fetchById"
) { userId, thunkApi ->
api.getUser(userId)
}
// Handle in reducer
val userReducer = reducer<UserState> {
on<AsyncThunkPending<String>> { state, _ ->
state.copy(loading = true)
}
on<AsyncThunkFulfilled<String, User>> { state, action ->
state.copy(loading = false, user = action.payload)
}
on<AsyncThunkRejected<String>> { state, action ->
state.copy(loading = false, error = action.error.message)
}
}val usersAdapter = createEntityAdapter<User> { it.id }
data class UsersState(
val users: EntityState<User> = usersAdapter.getInitialState()
) : State
// CRUD operations
usersAdapter.addOne(state.users, newUser)
usersAdapter.updateOne(state.users, id) { it.copy(name = "New") }
usersAdapter.removeOne(state.users, id)
// Selectors
val allUsers = usersAdapter.selectAll(state.users)
val user = usersAdapter.selectById(state.users, "123")val listenerMiddleware = createListenerMiddleware<AppState>()
listenerMiddleware.addListener<UserAction.Login> { action, api ->
api.dispatch(AnalyticsAction.Track("login"))
api.fork {
delay(1000)
api.dispatch(NotificationAction.Show("Welcome!"))
}
}val counterSlice = createSlice<CounterState>(
name = "counter",
initialState = CounterState()
) {
reduce("increment") { state, _ ->
state.copy(count = state.count + 1)
}
reduce<Int>("addAmount") { state, payload ->
state.copy(count = state.count + payload)
}
}
// Use
val reducer = counterSlice.reducer
store.dispatch(counterSlice.actions.invoke("increment"))redux-kmp/src/commonMain/kotlin/in/sitharaj/reduxkmp/
โโโ core/
โ โโโ Action.kt # Base action interface
โ โโโ State.kt # Base state interface
โ โโโ Reducer.kt # Reducer typealias
โ โโโ ReducerDSL.kt # reducer { on<T> { } } DSL
โ โโโ Store.kt # Redux store implementation
โ โโโ StoreDSL.kt # createStore { } DSL
โ โโโ Selector.kt # createSelector, MemoizedSelector
โโโ middleware/
โ โโโ Middleware.kt # Base middleware interface
โ โโโ ThunkMiddleware.kt # Async action support
โ โโโ LoggingMiddleware.kt # Debug logging
โ โโโ ListenerMiddleware.kt # Side effects
โโโ toolkit/
โ โโโ AsyncThunk.kt # createAsyncThunk
โ โโโ EntityAdapter.kt # createEntityAdapter
โ โโโ Slice.kt # createSlice
โโโ compose/
โ โโโ ComposeIntegration.kt # Compose helpers
โโโ ReduxKmp.kt # Main entry point
The sample app demonstrates all Redux features in a Chat Application:
# Run Desktop
./gradlew :sample:run
# Run Android
./gradlew :androidApp:installDebug
# Run Web
./gradlew :sample:jsBrowserDevelopmentRun# Run docs locally
cd docs-site
npm install
npm run dev./gradlew :redux-kmp:check # All tests
./gradlew :redux-kmp:desktopTest # Desktop
./gradlew :redux-kmp:jsTest # JavaScript
./gradlew :redux-kmp:iosSimulatorArm64Test # iOS# Publish to local Maven
./gradlew :redux-kmp:publishToMavenLocal
# Create signed bundle for Maven Central
./gradlew :redux-kmp:zipBundleCopyright 2024 Sitharaj Seenivasan
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
A Kotlin Multiplatform Redux library with modern Redux Toolkit features, type-safe DSLs, and Compose Multiplatform integration.
| Feature | Description |
|---|---|
| Kotlin DSL | Type-safe reducers with reducer { on<Action> { } } syntax |
| StateFlow | Reactive state powered by Kotlin Coroutines |
| Memoized Selectors |
createSelector() with automatic caching |
| Async Thunks |
createAsyncThunk() with pending/fulfilled/rejected lifecycle |
| Entity Adapter |
createEntityAdapter() for normalized CRUD operations |
| Listener Middleware | Side-effect handling with addListener()
|
| Slice Pattern |
createSlice() to combine reducer + actions |
| Compose Integration |
collectAsState() for Compose Multiplatform |
| Platform | Status |
|---|---|
| Android | โ |
| iOS | โ |
| Desktop (JVM) | โ |
| Web (JS) | โ |
| Web (WASM) |
// build.gradle.kts
dependencies {
implementation("in.sitharaj:redux-kmp:1.0.0")
}data class CounterState(
val count: Int = 0,
val loading: Boolean = false
) : State
sealed interface CounterAction : Action {
data object Increment : CounterAction
data object Decrement : CounterAction
data class SetCount(val value: Int) : CounterAction
}val counterReducer = reducer<CounterState> {
on<CounterAction.Increment> { state, _ ->
state.copy(count = state.count + 1)
}
on<CounterAction.Decrement> { state, _ ->
state.copy(count = state.count - 1)
}
on<CounterAction.SetCount> { state, action ->
state.copy(count = action.value)
}
}val store = createStore(
initialState = CounterState(),
reducer = counterReducer,
scope = CoroutineScope(Dispatchers.Main)
) {
addMiddleware(ThunkMiddleware())
addMiddleware(LoggingMiddleware(tag = "Counter"))
}store.dispatch(CounterAction.Increment)
// Observe state
store.state.collect { state ->
println("Count: ${state.count}")
}@Composable
fun CounterScreen() {
val state by store.state.collectAsState()
Column {
Text("Count: ${state.count}")
Button(onClick = { store.dispatch(CounterAction.Increment) }) {
Text("Increment")
}
}
}val selectCount = selector<AppState, Int> { it.counter }
val selectDoubleCount = createSelector(selectCount) { count ->
count * 2 // Only recomputes when count changes
}
// Multiple inputs
val selectTotal = createSelector(
{ state: CartState -> state.items },
{ state: CartState -> state.taxRate }
) { items, taxRate ->
items.sumOf { it.price } * (1 + taxRate)
}val fetchUser = createAsyncThunk<String, User, AppState>(
typePrefix = "users/fetchById"
) { userId, thunkApi ->
api.getUser(userId)
}
// Handle in reducer
val userReducer = reducer<UserState> {
on<AsyncThunkPending<String>> { state, _ ->
state.copy(loading = true)
}
on<AsyncThunkFulfilled<String, User>> { state, action ->
state.copy(loading = false, user = action.payload)
}
on<AsyncThunkRejected<String>> { state, action ->
state.copy(loading = false, error = action.error.message)
}
}val usersAdapter = createEntityAdapter<User> { it.id }
data class UsersState(
val users: EntityState<User> = usersAdapter.getInitialState()
) : State
// CRUD operations
usersAdapter.addOne(state.users, newUser)
usersAdapter.updateOne(state.users, id) { it.copy(name = "New") }
usersAdapter.removeOne(state.users, id)
// Selectors
val allUsers = usersAdapter.selectAll(state.users)
val user = usersAdapter.selectById(state.users, "123")val listenerMiddleware = createListenerMiddleware<AppState>()
listenerMiddleware.addListener<UserAction.Login> { action, api ->
api.dispatch(AnalyticsAction.Track("login"))
api.fork {
delay(1000)
api.dispatch(NotificationAction.Show("Welcome!"))
}
}val counterSlice = createSlice<CounterState>(
name = "counter",
initialState = CounterState()
) {
reduce("increment") { state, _ ->
state.copy(count = state.count + 1)
}
reduce<Int>("addAmount") { state, payload ->
state.copy(count = state.count + payload)
}
}
// Use
val reducer = counterSlice.reducer
store.dispatch(counterSlice.actions.invoke("increment"))redux-kmp/src/commonMain/kotlin/in/sitharaj/reduxkmp/
โโโ core/
โ โโโ Action.kt # Base action interface
โ โโโ State.kt # Base state interface
โ โโโ Reducer.kt # Reducer typealias
โ โโโ ReducerDSL.kt # reducer { on<T> { } } DSL
โ โโโ Store.kt # Redux store implementation
โ โโโ StoreDSL.kt # createStore { } DSL
โ โโโ Selector.kt # createSelector, MemoizedSelector
โโโ middleware/
โ โโโ Middleware.kt # Base middleware interface
โ โโโ ThunkMiddleware.kt # Async action support
โ โโโ LoggingMiddleware.kt # Debug logging
โ โโโ ListenerMiddleware.kt # Side effects
โโโ toolkit/
โ โโโ AsyncThunk.kt # createAsyncThunk
โ โโโ EntityAdapter.kt # createEntityAdapter
โ โโโ Slice.kt # createSlice
โโโ compose/
โ โโโ ComposeIntegration.kt # Compose helpers
โโโ ReduxKmp.kt # Main entry point
The sample app demonstrates all Redux features in a Chat Application:
# Run Desktop
./gradlew :sample:run
# Run Android
./gradlew :androidApp:installDebug
# Run Web
./gradlew :sample:jsBrowserDevelopmentRun# Run docs locally
cd docs-site
npm install
npm run dev./gradlew :redux-kmp:check # All tests
./gradlew :redux-kmp:desktopTest # Desktop
./gradlew :redux-kmp:jsTest # JavaScript
./gradlew :redux-kmp:iosSimulatorArm64Test # iOS# Publish to local Maven
./gradlew :redux-kmp:publishToMavenLocal
# Create signed bundle for Maven Central
./gradlew :redux-kmp:zipBundleCopyright 2024 Sitharaj Seenivasan
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0