Skip to content

Yet another UDF loop implementation for KMP

License

Notifications You must be signed in to change notification settings

atomgomba/hurok

Repository files navigation

hurok

This is a Kotlin Multiplatform framework library for developing applications on the JVM, JavaScript and Android based on the unidirectional dataflow model. The basic idea can be considered as an MVI architecture with reducer, but without the reducer.

flowchart LR
    A[First State] -->|Input| B{Action} -->|Mutate| C(Model) --> D[Renderer] -->|Derive| E[Next State] -.->|Input| B
    B -->|Trigger| F([Effect]) --> B
Loading

Please click here for generated documentation.

How it looks like

This brief example is missing some glue code to provide a quick feel of the library mechanics.

// Data model for business logic
data class ScoreScreenModel(
    val user: User? = null,
    val isLoading: Boolean = false,
    val throwable: Throwable? = null,
)

// State for representation
data class ScoreScreenState(
    val playerName: String,
    val score: String,
    val isLoading: Boolean,
    val errorMessage: String?,
)

// Renderer converts the model into a state
class ScoreScreenRenderer : Renderer<ScoreScreenState, ScoreScreenModel> {
    fun renderState(model: ScoreScreenModel) = with(model) {
        ScoreScreenState(
            playerName = if (user == null) "N/A" else user.nickname,
            score = if (user == null) "N/A" else user.score.roundToInt().toString(),
            errorMessage = throwable?.run { message ?: "Unknown error" },
            isLoading = isLoading,
        )
    }
}

@Composable
fun ScoreScreenView(args: ScoreScreenArgs?) {
    // Wraps a loop as a @Composable
    LoopView(
        builder = ScoreScreenLoop,
        args = args,
    ) { emit ->
        Column {
            if (isLoading) {
                Text("Loading...")
            } else if (errorMessage != null) {
                Text(errorMessage)

                // Buttons emit an update action
                Button(onClick = { emit(OnUpdateScoreClick) }) {
                    Text("Retry")
                }
            } else {
                Text(playerName)
                Text(score)

                Button(onClick = { emit(OnUpdateScoreClick) }) {
                    Text("Update score")
                }
            }
        }
    }
}

// Action to start updating the score on user click
data object OnUpdateScoreClick : ScoreScreenAction {
    override fun ScoreScreenModel.proceed() =
        next(copy(isLoading = true, throwable = null), UpdateScore(user.id))
}

// Action on successful update
data class UpdateScoreSuccess(val user: User) : ScoreScreenAction {
    override fun ScoreScreenModel.proceed() =
        next(copy(user = user, isLoading = false))
}

// Action on update failure
data class UpdateScoreError(val throwable: Throwable) : ScoreScreenAction {
    override fun ScoreScreenModel.proceed() =
        next(copy(throwable = throwable, isLoading = false))
}

// Background side effect for updating the data
data class UpdateScore(val userId: String) : ScoreScreenEffect {
    override suspend fun ScoreScreenEmitter.trigger(dependency: ScoreScreenDependencv) {
        try {
            val user = scoreService.getUserScore(userId)
            emit(UpdateScoreSuccess(user))
        } catch (throwable: Throwable) {
            emit(UpdateScoreError(throwable))
        }
    }
}

Parts

Name Description
State Loop state derived from the Model
Model Holds data for business logic
Args A way to pass inputs to an existing Loop
ArgsApplyer Applies arguments to the Model
Renderer Uses the Model to create new State
Action Mutates the Model and can trigger (any) Effect
Effect Does background work and triggers (any) Action
Loop Glue that holds the parts together

Using hurok with Gradle

Packages are published to Maven Central, so make sure to add it to your list of repositories.

repositories {
    mavenCentral()
}

Kotlin DSL

dependencies {
    // core multiplatform package (added transitively when using the compose package below)
    implementation("com.ekezet.hurok:hurok-base:3.1.0")
    // library for using hurok with Compose Multiplatform
    implementation("com.ekezet.hurok:hurok-compose:3.1.0")
    // library for testing hurok-based applications
    testImplementation("com.ekezet.hurok:hurok-test:3.1.0")
}

Version catalog

[versions]
hurok = "3.1.0"

[libraries]
hurok-base = { group = "com.ekezet.hurok", name = "hurok-base", version.ref = "hurok" }
hurok-compose = { group = "com.ekezet.hurok", name = "hurok-compose", version.ref = "hurok" }
hurok-test = { group = "com.ekezet.hurok", name = "hurok-test", version.ref = "hurok" }

[bundles]
hurok = ["hurok-base", "hurok-compose"]
dependencies {
    implementation(libs.bundles.hurok)
    testImplementation(libs.hurok.test)
}

Technologies

  • Kotlin Multiplatform
  • Kotlin Coroutines
  • Compose Multiplatform
  • Android SDK

Example code

For code samples please see Othello for Android.

About

Yet another UDF loop implementation for KMP

Topics

Resources

License

Stars

Watchers

Forks

Languages