An open API service indexing awesome lists of open source software.

https://github.com/pelagornis/kotlin-rex

Rex is a modular, state management architecture
https://github.com/pelagornis/kotlin-rex

kotlin pelagornis rex

Last synced: 6 months ago
JSON representation

Rex is a modular, state management architecture

Awesome Lists containing this project

README

          

# Kotlin Rex πŸ¦–

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

## Features ✨

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

## Requirements πŸ“‹

- Android API 24+ (Android 7.0+)
- Kotlin 2.0+
- Android Gradle Plugin 8.0+

## Installation πŸ”§

### Maven Central (Recommended)

Add the dependency to your `build.gradle.kts`:

```kotlin
dependencies {
implementation("com.pelagornis:rex:1.0.0")
}
```

### GitHub Packages

Add the GitHub Packages repository:

```kotlin
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")
}
```

### Local Maven (For Development)

```bash
# Clone and build locally
git clone https://github.com/pelagornis/kotlin-rex.git
cd kotlin-rex
./gradlew :library:publishToMavenLocal
```

Then in your project:

```kotlin
repositories {
mavenLocal()
}

dependencies {
implementation("com.pelagornis:rex:1.0.0")
}
```

For detailed publishing instructions, see:

- **[USER_TOKEN_SETUP.md](USER_TOKEN_SETUP.md)** - Maven Central User Token setup (2024 updated) ⭐
- [PUBLISHING.md](PUBLISHING.md) - Complete publishing guide
- [GPG_SETUP.md](GPG_SETUP.md) - GPG key setup
- [SONATYPE_SETUP.md](SONATYPE_SETUP.md) - Sonatype account setup

## Core Concepts πŸ“š

### State

An immutable data structure representing the application state.

```kotlin
data class AppState(
val count: Int = 0,
val isLoading: Boolean = false,
val errorMessage: String? = null,
val lastUpdated: Long = System.currentTimeMillis()
) : StateType
```

### Action

Events that describe state changes.

```kotlin
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()
}
```

### Reducer

A pure function that takes the current state and an action, then returns a new state and effects to execute.

```kotlin
class AppReducer : Reducer {
override fun reduce(state: AppState, action: AppAction): Pair>> {
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 { 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"
}
}
```

### Store

The central repository that holds the state, processes actions, and notifies subscribers of state changes.

```kotlin
class MyViewModel : ViewModel() {
private val store = Store(
initialState = AppState(),
reducer = AppReducer(),
middlewares = listOf(LoggingMiddleware())
)

val state: StateFlow = store.state

fun dispatch(action: AppAction) {
store.dispatch(action)
}

override fun onCleared() {
super.onCleared()
store.clear()
}
}
```

## Usage Examples πŸš€

### 1. Basic Usage

```kotlin
// 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 {
override fun reduce(
state: CounterState,
action: CounterAction
): Pair>> {
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)
```

### 2. Using Effects

Effects handle side effects like asynchronous operations, network requests, and timers.

```kotlin
// Basic Effect
val effect = Effect { 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)
```

### 3. Using Middleware

Middleware can intercept and process actions before they reach the reducer.

```kotlin
// Custom Middleware
class AnalyticsMiddleware : Middleware {
override suspend fun process(
state: AppState,
action: AppAction,
emit: (AppAction) -> Unit
): List> {
// 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()
)
)
```

### 4. Using EventBus

Use EventBus to send and receive events between components.

```kotlin
// 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 { 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!"))
```

### 5. Integration with Jetpack Compose

```kotlin
@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("+")
}
}
}
}
```

## Built-in Middleware πŸ“¦

### LoggingMiddleware

Logs all actions and state changes.

```kotlin
val store = Store(
initialState = AppState(),
reducer = AppReducer(),
middlewares = listOf(LoggingMiddleware())
)
```

### TimeTravelMiddleware

Tracks state history to enable time-travel debugging.

```kotlin
val timeTravelMiddleware = TimeTravelMiddleware()

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()
```

## Architecture πŸ—οΈ

```
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 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) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```

## Best Practices πŸ’‘

1. **Keep State Immutable**: Always use the `copy()` method of `data class` to create a new state.

2. **Keep Reducers Pure**: Don't cause side effects inside reducers; separate them into Effects.

3. **Make Actions Clear**: Action names should clearly express "what happened".

4. **Make Effects Reusable**: Create reusable Effects for common asynchronous operations.

5. **Single Responsibility for Middleware**: Each Middleware should perform one clear role.

6. **Manage Store in ViewModel**: In Android, create and manage Store in ViewModel.

## Testing πŸ§ͺ

```kotlin
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)
}
}
```

## Publishing πŸ“¦

### Quick Publish to All Repositories

```bash
# Publish to GitHub Packages + Maven Central
./gradlew :library:publish
```

### GitHub Actions (Recommended)

1. Go to **Actions** tab
2. Select **Publish Library** workflow
3. Choose release type:
- `all` - Publish to all repositories (GitHub Packages + Maven Central) ⭐
- `github` - GitHub Packages only
- `sonatype` - Maven Central only
- `local` - Local testing only

### Auto-Deploy on Release

Create a release on GitHub to automatically publish to all repositories:

```bash
git tag v0.1.2
git push origin v0.1.2
```

For detailed instructions, see [PUBLISHING.md](PUBLISHING.md) and [GPG_SETUP.md](GPG_SETUP.md).

## Example Project πŸ“±

Check out the complete working example in the `example` module of this repository.

```bash
./gradlew :example:assembleDebug
```

## Contributing 🀝

Contributions are always welcome! Please refer to [CONTRIBUTING.md](CONTRIBUTING.md).

## License πŸ“„

**kotlin-rex** is distributed under the MIT License. See the [LICENSE](LICENSE) file for more details.

## Credits πŸ‘

Kotlin Rex is developed and maintained by [Pelagornis](https://github.com/pelagornis).

---

Made with ❀️ by Pelagornis