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
- 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