Kotlin Rex is a type-safe and predictable state management library for Android. Based on Redux/Flux architecture patterns, it elegantly handles asynchronous operations using Kotlin Coroutines.
- π― Type Safety: Complete type safety leveraging Kotlin's powerful type system
- π Predictable State Management: Manage state changes predictably with unidirectional data flow
- β‘ Coroutines-based: Efficient asynchronous processing using Kotlin Coroutines
- π Middleware Support: Various middlewares including logging and time-travel debugging
- π¨ Effect System: Powerful and flexible side effect handling
- π EventBus: Built-in EventBus for inter-component event communication
- π§© Modular: Each component can be used independently
- πͺ Pipeline Hooks 2.0: Phase-based observability (logging, analytics, time travel)
- πΊ State Graph: Tree-shaped navigation with mount/unmount lifecycle and scoped effect cancellation
- π§© Composable Reducers:
ScopeReducer,CombineReducer,GraphNavigationReducer - π Derived State: Memoized computed values from base state
- π¨ Compose Integration:
ObservableStorefor Jetpack Compose - π§ͺ RexTesting:
TestStoreharness for reducer and effect testing
Aligned with swift-rex:
kotlin-rex/
βββ library/ # Rex (main runtime)
β βββ com/pelagornis/rex/
β βββ ActionType.kt # Actionable
β βββ StateType.kt # Statable
β βββ compose/ # ObservableStore (SwiftUI parity)
β βββ derive/ # DerivedState
β βββ effect/ # Effect, EffectID, EffectEmitter
β βββ graph/ # StateGraph, GraphStore, GraphNavigationReducer
β βββ middleware/ # Legacy middleware (+ MiddlewarePipelineHook adapter)
β βββ pipeline/ # ActionPipeline, PipelineHook, Logging/TimeTravel hooks
β βββ reducer/ # Reducer, AnyReducer, CombineReducer, ScopeReducer
β βββ store/ # Store, EffectRunner
β βββ utils/
βββ rex-testing/ # RexTesting β TestStore harness
βββ example/ # Sample Compose app
Data flow: UI β Action β Pipeline Hooks β Reducer β (State + Effects) β UI
Kotlin-Rex is the Android runtime for apps generated by Trinity Build System (--target=android).
| Repository | Role |
|---|---|
| trinity | Studio IDE β ERD, source inspector, Run profiles |
| trinity-graph-system | Graph Β· canonical Β· Rex IR |
| trinity-build-system | Graph JSON β Kotlin-Rex codegen |
| kotlin-rex (this repo) | Runtime used by generated apps |
Graph JSON β trinity-build-system β Kotlin-Rex store + Compose UI
β²
trinity Studio (edit graph, inspect source, run build)
Codegen parity sample in example-trinity-todo/:
cd kotlin-rex
./gradlew :example-trinity-todo:assembleDebug
./gradlew :example-trinity-todo:testDebugUnitTestGraph source: example-trinity-todo/todo.nodes.graph.json (aligned with trinity examples).
- Android API 24+ (Android 7.0+)
- Kotlin 2.0+
- Android Gradle Plugin 8.0+
Add the dependency to your build.gradle.kts:
dependencies {
implementation("com.pelagornis:rex:1.0.0")
}Add the GitHub Packages repository:
repositories {
maven {
url = uri("https://maven.pkg.github.com/pelagornis/kotlin-rex")
credentials {
username = findProperty("gpr.user") as String? ?: System.getenv("GITHUB_USERNAME")
password = findProperty("gpr.key") as String? ?: System.getenv("GITHUB_TOKEN")
}
}
}
dependencies {
implementation("com.pelagornis:rex:1.0.0")
}# Clone and build locally
git clone https://github.com/pelagornis/kotlin-rex.git
cd kotlin-rex
./gradlew :library:publishToMavenLocalThen in your project:
repositories {
mavenLocal()
}
dependencies {
implementation("com.pelagornis:rex:1.0.0")
}For detailed publishing instructions, see:
- USER_TOKEN_SETUP.md - Maven Central User Token setup (2024 updated) β
- PUBLISHING.md - Complete publishing guide
- GPG_SETUP.md - GPG key setup
- SONATYPE_SETUP.md - Sonatype account setup
An immutable data structure representing the application state.
data class AppState(
val count: Int = 0,
val isLoading: Boolean = false,
val errorMessage: String? = null,
val lastUpdated: Long = System.currentTimeMillis()
) : StateTypeEvents that describe state changes.
sealed class AppAction : ActionType {
object Increment : AppAction()
object Decrement : AppAction()
data class SetCount(val count: Int) : AppAction()
object LoadData : AppAction()
data class DataLoaded(val data: String) : AppAction()
data class ErrorOccurred(val message: String) : AppAction()
}A pure function that takes the current state and an action, then returns a new state and effects to execute.
class AppReducer : Reducer<AppState, AppAction> {
override fun reduce(state: AppState, action: AppAction): Pair<AppState, List<Effect<AppAction>>> {
return when (action) {
is AppAction.Increment -> {
state.copy(count = state.count + 1) to emptyList()
}
is AppAction.Decrement -> {
state.copy(count = state.count - 1) to emptyList()
}
is AppAction.SetCount -> {
state.copy(count = action.count) to emptyList()
}
is AppAction.LoadData -> {
val effect = Effect<AppAction> { emitter ->
try {
val data = fetchDataFromApi()
emitter.send(AppAction.DataLoaded(data))
} catch (e: Exception) {
emitter.send(AppAction.ErrorOccurred(e.message ?: "Unknown error"))
}
}
state.copy(isLoading = true) to listOf(effect)
}
is AppAction.DataLoaded -> {
state.copy(isLoading = false) to emptyList()
}
is AppAction.ErrorOccurred -> {
state.copy(isLoading = false, errorMessage = action.message) to emptyList()
}
}
}
private suspend fun fetchDataFromApi(): String {
// API call logic
return "Data from API"
}
}The central repository that holds the state, processes actions, and notifies subscribers of state changes.
class MyViewModel : ViewModel() {
private val store = Store(
initialState = AppState(),
reducer = AppReducer(),
middlewares = listOf(LoggingMiddleware())
)
val state: StateFlow<AppState> = store.state
fun dispatch(action: AppAction) {
store.dispatch(action)
}
override fun onCleared() {
super.onCleared()
store.clear()
}
}// 1. Define State
data class CounterState(
val count: Int = 0
) : StateType
// 2. Define Actions
sealed class CounterAction : ActionType {
object Increment : CounterAction()
object Decrement : CounterAction()
}
// 3. Implement Reducer
class CounterReducer : Reducer<CounterState, CounterAction> {
override fun reduce(
state: CounterState,
action: CounterAction
): Pair<CounterState, List<Effect<CounterAction>>> {
return when (action) {
is CounterAction.Increment ->
state.copy(count = state.count + 1) to emptyList()
is CounterAction.Decrement ->
state.copy(count = state.count - 1) to emptyList()
}
}
}
// 4. Create and Use Store
val store = Store(
initialState = CounterState(),
reducer = CounterReducer()
)
// Subscribe to state changes
store.subscribe { state ->
println("Current count: ${state.count}")
}
// Dispatch actions
store.dispatch(CounterAction.Increment)
store.dispatch(CounterAction.Increment)
store.dispatch(CounterAction.Decrement)Effects handle side effects like asynchronous operations, network requests, and timers.
// Basic Effect
val effect = Effect<AppAction> { emitter ->
val result = performNetworkRequest()
emitter.send(AppAction.RequestSuccess(result))
}
// Delayed Effect
val delayedEffect = Effect.delayed(
action = AppAction.ShowMessage("Hello!"),
delayMillis = 1000
)
// Retryable Effect
val retryEffect = Effect.retry(
effect = networkEffect,
maxAttempts = 3,
delayMillis = 1000,
shouldRetry = { error -> error is NetworkException },
onError = { error ->
Log.e("Effect", "Failed after retries: $error")
}
)
// Combining Multiple Effects
val combinedEffect = Effect.combine(effect1, effect2, effect3)Middleware can intercept and process actions before they reach the reducer.
// Custom Middleware
class AnalyticsMiddleware : Middleware<AppState, AppAction> {
override suspend fun process(
state: AppState,
action: AppAction,
emit: (AppAction) -> Unit
): List<Effect<AppAction>> {
// Send action to analytics tool
Analytics.logEvent(action.javaClass.simpleName)
return emptyList()
}
}
// Apply Middleware to Store
val store = Store(
initialState = AppState(),
reducer = AppReducer(),
middlewares = listOf(
LoggingMiddleware(),
AnalyticsMiddleware(),
TimeTravelMiddleware()
)
)Use EventBus to send and receive events between components.
// Define Events
sealed class AppEvent : EventType {
data class ShowToast(val message: String) : AppEvent()
object NavigateToHome : AppEvent()
}
// Use EventBus
val eventBus = store.getEventBus()
// Subscribe to specific event type
eventBus.subscribe<AppEvent.ShowToast> { event ->
Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show()
}
// Subscribe to all events
eventBus.subscribe { event ->
when (event) {
is AppEvent.ShowToast -> showToast(event.message)
is AppEvent.NavigateToHome -> navigateToHome()
}
}
// Publish event
eventBus.publish(AppEvent.ShowToast("Hello, World!"))@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
val state by viewModel.state.collectAsState()
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "Count: ${state.count}",
style = MaterialTheme.typography.headlineLarge
)
Spacer(modifier = Modifier.height(16.dp))
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Button(onClick = { viewModel.dispatch(CounterAction.Decrement) }) {
Text("-")
}
Button(onClick = { viewModel.dispatch(CounterAction.Increment) }) {
Text("+")
}
}
}
}Logs all actions and state changes.
val store = Store(
initialState = AppState(),
reducer = AppReducer(),
middlewares = listOf(LoggingMiddleware())
)Tracks state history to enable time-travel debugging.
val timeTravelMiddleware = TimeTravelMiddleware<AppState, AppAction>()
val store = Store(
initialState = AppState(),
reducer = AppReducer(),
middlewares = listOf(timeTravelMiddleware)
)
// Undo to previous state
timeTravelMiddleware.undo()
// Redo to next state
timeTravelMiddleware.redo()
// View history
val history = timeTravelMiddleware.getHistory()βββββββββββββββββββββββββββββββββββββββββββββββ
β View β
β (Activity, Fragment, Composable) β
ββββββββββββββββββ¬βββββββββββββββββββββββββββββ
β dispatch(action)
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββ
β Store β
β βββββββββββββββββββββββββββββββββββββββ β
β β Middleware Chain β β
β β ββββββββββ ββββββββββ ββββββββββ β β
β β β Logger βββAnalyticsβββTimeTravelβ β β
β β ββββββββββ ββββββββββ ββββββββββ β β
β βββββββββββββββββββββββββββββββββββββββ β
β βΌ β
β βββββββββββββββββββββββββββββββββββββββ β
β β Reducer β β
β β (state, action) β (state, effects) β β
β βββββββββββββββββββββββββββββββββββββββ β
β βΌ β
β βββββββββββββββββββββββββββββββββββββββ β
β β New State β β
β βββββββββββββββββββββββββββββββββββββββ β
β βΌ β
β βββββββββββββββββββββββββββββββββββββββ β
β β Execute Effects β β
β β (async operations, side effects) β β
β βββββββββββββββββββββββββββββββββββββββ β
ββββββββββββββββββ¬βββββββββββββββββββββββββββββ
β state updates
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββ
β Subscribers β
β (UI updates via StateFlow) β
βββββββββββββββββββββββββββββββββββββββββββββββ
-
Keep State Immutable: Always use the
copy()method ofdata classto create a new state. -
Keep Reducers Pure: Don't cause side effects inside reducers; separate them into Effects.
-
Make Actions Clear: Action names should clearly express "what happened".
-
Make Effects Reusable: Create reusable Effects for common asynchronous operations.
-
Single Responsibility for Middleware: Each Middleware should perform one clear role.
-
Manage Store in ViewModel: In Android, create and manage Store in ViewModel.
class CounterReducerTest {
private lateinit var reducer: CounterReducer
@Before
fun setup() {
reducer = CounterReducer()
}
@Test
fun `increment action increases count by one`() {
val initialState = CounterState(count = 0)
val action = CounterAction.Increment
val (newState, effects) = reducer.reduce(initialState, action)
assertEquals(1, newState.count)
assertTrue(effects.isEmpty())
}
@Test
fun `decrement action decreases count by one`() {
val initialState = CounterState(count = 5)
val action = CounterAction.Decrement
val (newState, effects) = reducer.reduce(initialState, action)
assertEquals(4, newState.count)
}
}# Publish to GitHub Packages + Maven Central
./gradlew :library:publish- Go to Actions tab
- Select Publish Library workflow
- Choose release type:
all- Publish to all repositories (GitHub Packages + Maven Central) βgithub- GitHub Packages onlysonatype- Maven Central onlylocal- Local testing only
Create a release on GitHub to automatically publish to all repositories:
git tag v0.1.2
git push origin v0.1.2For detailed instructions, see PUBLISHING.md and GPG_SETUP.md.
Check out the complete working example in the example module of this repository.
./gradlew :example:assembleDebugContributions are always welcome! Please refer to CONTRIBUTING.md.
kotlin-rex is distributed under the MIT License. See the LICENSE file for more details.
Kotlin Rex is developed and maintained by Pelagornis.
Made with β€οΈ by Pelagornis