duks 0.2.5 indexed Lightweight, type-safe state management inspired by Redux, featuring predictable unidirectional data flow, middleware support, and seamless integration with Compose UI. Offers action caching, saga pattern support, and minimal dependencies.
Bring this to kpkg
This library is indexed from the KMP ecosystem and already resolves through kpkg.dev's Maven Central proxy. Maintainers can verify the namespace and publish future versions to kpkg for free hosting, real download stats, and signed-provenance pages.
Publishing coming soonMetadata
Owner crowded-libs
Stars 6
Used by 2 libs
Health —
License Apache License 2.0
Latest 0.2.5
Repository github.com/crowded-libs/duks
Updated 2025-09-16 Readme Changelog
Duks - Kotlin Compose State Management and Control Flow
Duks is a lightweight, type-safe state management library for Kotlin Multiplatform applications, inspired by Redux. It provides a predictable, unidirectional data flow pattern with built-in support for middleware and Compose UI integration.
Features
Installation
Add Duks to your project by including it in your Gradle build file:
dependencies {
implementation("io.github.crowded-libs:duks:0.2.5" )
}
Quick Start
1. Define Your State
data class AppState (
val counter: Int = 0 ,
val user: User? = null ,
val isLoading: Boolean = false
) : StateModel
2. Define Actions
sealed class AppAction : Action {
data object Increment : AppAction()
data object Decrement : AppAction()
data class SetUser (val user: User) : AppAction()
data object LoadUser : AppAction(), AsyncAction<User>
}
3. Create a Reducer
val appReducer: Reducer<AppState> = { state, action ->
when (action) {
is AppAction.Increment -> state.copy(counter = state.counter + 1 )
is AppAction.Decrement -> state.copy(counter = state.counter - 1 )
is AppAction.SetUser -> state.copy(user = action.user)
is AsyncAction.Processing -> state.copy(isLoading = )
AsyncAction.Result -> (action.initiatedBy) {
AppAction.LoadUser -> state.copy(user = action. User, isLoading = )
-> state
}
-> state
}
}
4. Create the Store
val store = KStore(
initialState = AppState(),
reducer = appReducer,
middleware = listOf(
exceptionHandling(),
logging(),
async())
)
5. Use in Compose
@Composable
fun CounterScreen (store: KStore <CounterState >) {
val state by store.state.collectAsState()
Column(modifier = Modifier.padding(16. dp)) {
Text(text = "Count: ${state.count} " )
Button(onClick = { store.dispatch(Increment()) }) {
Text( )
}
Button(onClick = { store.dispatch(Increment( )) }) {
Text( )
}
}
}
Complete Compose Example
Here's a complete example showing a todo app with Duks and Compose:
Advanced Features
Sagas
Sagas provide powerful workflow orchestration for complex async scenarios. Each saga maintains its own independent state throughout its lifecycle:
Custom Async Actions
Create specialized async actions with custom lifecycle:
Persistence
Flexible persistence with multiple strategies:
Action Caching
Optimize performance by caching expensive operations:
data class SearchProducts (val query: String) : AppAction(), CacheableAction {
override val cacheKey = "search_$query "
override val cacheDuration = 5. minutes
}
val cacheMiddleware = CachingMiddleware<AppState>(
cache = MapActionCache()
)
Best Practices
State Design : Keep state immutable and normalized
Action Design : Use sealed classes for type-safe action hierarchies
Performance : Use mapToPropsAsState for Compose to minimize recomposition
Persistence : Choose appropriate strategy (Debounced for frequent updates, OnAction for critical state)
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
This project is licensed under the MIT License - see the LICENSE file for details.
🎯 Type-safe state management with Redux-like architecture
🚀 Kotlin Multiplatform Works across Android, iOS, JVM, watchOS, tvOS, Linux, Windows, and WebAssembly targets
⚡ Built-in async support with customizable lifecycle actions
🔄 Saga pattern for complex workflow orchestration
💾 Flexible persistence with multiple strategies
🧩 Composable middleware for extensibility
🎨 Compose integration with optimized recomposition true
is
when
is
data
as
false
else
else
"Increment"
5
"Increment by 5"
data class TodoState (
val items: List<TodoItem> = emptyList(),
val inputText: String = "" ,
val isLoading: Boolean = false
) : StateModel
data class TodoItem (val id: String, val text: String, val completed: Boolean = false )
data class UpdateInputText (val text: String) : Action
data class AddTodo (val text: String) : Action
data class ToggleTodo (val id: String) : Action
data class DeleteTodo (val id: String) : Action
data class LoadTodos : AsyncAction <List<TodoItem >> {
override suspend fun execute () : Result<List<TodoItem>> {
return try {
val todos = todoRepository.getAllTodos()
Result.success(todos)
} catch (e: Exception) {
Result.failure(e)
}
}
}
val todoReducer: Reducer<TodoState> = { state, action ->
when (action) {
is UpdateInputText -> state.copy(inputText = action.text)
is AddTodo -> state.copy(
items = state.items + TodoItem(UUID.randomUUID().toString(), action.text),
inputText = ""
)
is ToggleTodo -> state.copy(
items = state.items.map {
if (it.id == action.id) it.copy(completed = !it.completed) else it
}
)
is DeleteTodo -> state.copy(
items = state.items.filterNot { it.id == action.id }
)
is AsyncInitiatedByAction -> {
if (action.initiator is LoadTodos) {
state.copy(isLoading = true )
} else state
}
is AsyncSuccessAction<*, *> -> {
if (action.initiator is LoadTodos && action.result is List<*>) {
@Suppress("UNCHECKED_CAST" )
state.copy(
items = action.result as List<TodoItem>,
isLoading = false
)
} else state
}
else -> state
}
}
@Composable
fun TodoApp () {
val store = remember {
createStore(TodoState()) {
middleware {
async()
logging()
}
reduceWith(todoReducer)
}
}
LaunchedEffect(Unit ) {
store.dispatch(LoadTodos())
}
TodoScreen(store)
}
@Composable
fun TodoScreen (store: KStore <TodoState >) {
val state by store.state.collectAsState()
Column(modifier = Modifier.fillMaxSize().padding(16. dp)) {
Row(modifier = Modifier.fillMaxWidth()) {
TextField(
value = state.inputText,
onValueChange = { store.dispatch(UpdateInputText(it)) },
modifier = Modifier.weight(1f ),
placeholder = { Text("Add a todo" ) }
)
Button(
onClick = {
if (state.inputText.isNotBlank()) {
store.dispatch(AddTodo(state.inputText))
}
},
modifier = Modifier.padding(start = 8. dp)
) {
Text("Add" )
}
}
if (state.isLoading) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.CenterHorizontally).padding(16. dp)
)
}
LazyColumn(
modifier = Modifier.fillMaxWidth().padding(top = 16. dp)
) {
items(state.items) { todo ->
TodoItem(
todo = todo,
onToggle = { store.dispatch(ToggleTodo(todo.id)) },
onDelete = { store.dispatch(DeleteTodo(todo.id)) }
)
}
}
}
}
@Composable
fun TodoItem (todo: TodoItem , onToggle: () -> Unit , onDelete: () -> Unit ) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8. dp),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = todo.completed,
onCheckedChange = { onToggle() }
)
Text(
text = todo.text,
modifier = Modifier
.weight(1f )
.padding(horizontal = 8. dp),
textDecoration = if (todo.completed) TextDecoration.LineThrough else null ,
color = if (todo.completed) Color.Gray else Color.Black
)
IconButton(onClick = onDelete) {
Icon(Icons.Default.Delete, contentDescription = "Delete" )
}
}
}
data class OnboardingSagaState (
val userId: String,
val profileComplete: Boolean = false ,
val tutorialComplete: Boolean = false ,
val currentStep: String = "started"
)
data class UserSignedUp (val userId: String, val email: String) : Action
data class ProfileCompleted (val userId: String) : Action
data class TutorialFinished (val userId: String) : Action
data class OnboardingCompleted (val userId: String) : Action
class OnboardingSaga : SagaDefinition <OnboardingSagaState > {
override val name = "onboarding"
override fun configure (saga: SagaConfiguration <OnboardingSagaState >) {
saga.startsOn<UserSignedUp> { action ->
SagaTransition.Continue(
OnboardingSagaState(
userId = action.userId,
currentStep = "profile_setup"
),
effects = listOf(
SagaEffect.Dispatch(ShowProfileSetupScreen(action.userId))
)
)
}
saga.on<ProfileCompleted>(
condition = { action, state -> action.userId == state.userId }
) { action, state ->
val newState = state.copy(
profileComplete = true ,
currentStep = "tutorial"
)
if (state.tutorialComplete) {
SagaTransition.Complete(
effects = listOf(
SagaEffect.Dispatch(OnboardingCompleted(state.userId))
)
)
} else {
SagaTransition.Continue(
newState,
effects = listOf(
SagaEffect.Dispatch(ShowTutorialScreen(state.userId))
)
)
}
}
saga.on<TutorialFinished>(
condition = { action, state -> action.userId == state.userId }
) { action, state ->
val newState = state.copy(
tutorialComplete = true ,
currentStep = "completed"
)
if (state.profileComplete) {
SagaTransition.Complete(
effects = listOf(
SagaEffect.Dispatch(OnboardingCompleted(state.userId))
)
)
} else {
SagaTransition.Continue(
newState,
effects = listOf(
SagaEffect.Dispatch(ShowProfileSetupScreen(state.userId))
)
)
}
}
}
}
val store = createStore(AppState()) {
middleware {
sagas {
register(OnboardingSaga())
saga<PaymentSagaState>(
name = "payment" ,
initialState = { PaymentSagaState() }
) {
startsOn<InitiatePayment> { action ->
SagaTransition.Continue(
PaymentSagaState(orderId = action.orderId),
effects = listOf(
SagaEffect.Dispatch(ProcessPayment(action.orderId)),
SagaEffect.Delay(30000 ),
SagaEffect.Dispatch(PaymentTimeout(action.orderId))
)
)
}
}
}
}
}
interface NetworkAction <T > : AsyncAction <T > {
data class Loading (override val initiatedBy: Action) : NetworkAction<Nothing >, AsyncAction.Processing
data class Success <T >(override val initiatedBy: Action, override val data : T) : NetworkAction<T>, AsyncAction.Result<T>
data class Failure (override val initiatedBy: Action, val error: Throwable) : NetworkAction<Nothing >, AsyncAction.Error
data class Retry (override val initiatedBy: Action) : NetworkAction<Nothing >
}
data class FetchPosts (val userId: String) : AppAction(), NetworkAction<List<Post>> {
override fun createProcessingAction () = NetworkAction.Loading(this )
override fun createResultAction (data : List <Post >) = NetworkAction.Success(this , data )
override fun createErrorAction (error: Throwable ) = NetworkAction.Failure(this , error)
}
val reducer: Reducer<AppState> = { state, action ->
when (action) {
is NetworkAction.Loading -> state.copy(isLoading = true )
is NetworkAction.Success<*> -> when (action.initiatedBy) {
is FetchPosts -> state.copy(
posts = action.data as List<Post>,
isLoading = false
)
else -> state
}
is NetworkAction.Failure -> state.copy(
error = action.error.message,
isLoading = false
)
is NetworkAction.Retry -> {
store.dispatch(action.initiatedBy)
state
}
else -> state
}
}
class FileStateStorage : StateStorage <AppState > {
override suspend fun save (state: AppState ) {
File("app_state.json" ).writeText(Json.encodeToString(state))
}
override suspend fun load () : AppState? {
return try {
Json.decodeFromString(File("app_state.json" ).readText())
} catch (e: Exception) {
null
}
}
}
val persistenceMiddleware = PersistenceMiddleware(
storage = FileStateStorage(),
strategy = PersistenceStrategy.Debounced(500. milliseconds)
)
val sagaStorage = InMemorySagaStorage()
val sagaMiddleware = SagaMiddleware(
sagaDefinitions = setOf(OnboardingSaga()),
sagaStateSerializer = JsonSagaSerializer(),
sagaStorage = sagaStorage,
persistenceStrategy = SagaPersistenceStrategy.Combined(
SagaPersistenceStrategy.OnCheckpoint,
SagaPersistenceStrategy.OnCompletion
)
)
Related libraries Surfaced from shared tags and platforms — no rankings paid for.
KSafe ★ 301
ioannisa Effortlessly encrypts and persists data using hardware-backed security, offering one-code-path simplicity, seamless integration, and protection for sensitive information like OAuth tokens. Shared: storage, state-management, compose summon ★ 158
codeyousef Powerful type-safe frontend framework delivers reactive state management, component-based architecture, and comprehensive styling for building elegant, responsive applications with declarative syntax and enhanced UI capabilities. Shared: state-management, compose-multiplatform, compose LazyCardStack ★ 92
Hukumister Provides a Tinder-like card stack interface with a LazyColumn-like API, supporting swipe gestures, advanced animations, and programmatic card swiping. Includes callback for swipe detection and method to return previous cards. Shared: state-management, compose-multiplatform, compose compose-rich-editor ★ 1.8k
MohamedRejeb Rich text editor library for creating customizable WYSIWYG editors, supporting text styling, links, code blocks, lists, and HTML/Markdown import/export with minimal boilerplate. Shared: compose-multiplatform, compose compose-hot-reload ★ 1.4k
JetBrains Enhances UI development speed by enabling real-time updates without restarts. Uses a specialized JVM for intelligent code reloading, optimizing the iterative design process. Shared: compose-multiplatform, compose charty ★ 1.3k
hi-manshu Lightweight charting toolkit delivering sleek, customizable charts with a minimal API, performant rendering, easy theming, and concise documentation for rapid UI integration. Shared: compose-multiplatform, compose