Skip to content

pelagornis/kotlin-rex

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

39 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

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:

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

GitHub Packages

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

Local Maven (For Development)

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

Then in your project:

repositories {
    mavenLocal()
}

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

For detailed publishing instructions, see:

Core Concepts πŸ“š

State

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()
) : StateType

Action

Events 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()
}

Reducer

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

Store

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

Usage Examples πŸš€

1. Basic Usage

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

2. Using Effects

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)

3. Using Middleware

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

4. Using EventBus

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!"))

5. Integration with Jetpack Compose

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

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

TimeTravelMiddleware

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

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 πŸ§ͺ

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

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

git tag v0.1.2
git push origin v0.1.2

For detailed instructions, see PUBLISHING.md and GPG_SETUP.md.

Example Project πŸ“±

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

./gradlew :example:assembleDebug

Contributing 🀝

Contributions are always welcome! Please refer to CONTRIBUTING.md.

License πŸ“„

kotlin-rex is distributed under the MIT License. See the LICENSE file for more details.

Credits πŸ‘

Kotlin Rex is developed and maintained by Pelagornis.


Made with ❀️ by Pelagornis

About

Rex is a modular, state management architecture

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

No packages published