empress

Project Url: nofrills-io/empress
Introduction: Android framework for ruling your app
More: Author   ReportBugs   OfficialWebsite   
Tags:

API docs Release Build Status Codecov License

Empress is a framework for managing application state and its representation in UI. It's targeted to be used with Android's activities and fragments, but you can also use it standalone.

Empress is similar to androidx ViewModel library in a way that it:

  • can survive configuration changes
  • can be shared among multiple clients (e.g. fragments)

Additionally:

  • supports "save & restore" flow (it's automatic, as long as your model is Parcelable)
  • supports long-running suspending requests
  • uses kotlinx.coroutines.flow.Flow for propagating changes
  • aims for compatibility with Jetpack Compose (with mutable models)

Install

repositories {
    maven { url("https://jitpack.io") }
}

dependencies {
    implementation("com.github.nofrills-io:empress:empress_version")
}

// Note: skip `::class` if you're using Groovy instead of Kotlin
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class).configureEach {
    kotlinOptions {
        jvmTarget = "1.8"
    }
}

Sample usage

Let's say you need to build an app to count things, and send the final value to a server (full example in sample app module).

  1. First, define your events, model and requests:
// In Empress, your model can be defined as a set of subclasses,
// where each subclass is responsible for single aspect of application state.
// Your model should be either fully immutable, or fully mutable.
sealed class Model {
    // In case our process is temporarily killed by the OS, we can make sure
    // our state will be brought back, by implementing `android.os.Parcelable`
    @Parcelize
    data class Counter(val count: Int) : Model(), Parcelable

    // In this case it doesn't make sense to implement `Parcelable`,
    // because if our process gets killed, our async request will also die
    data class Sender(val requestId: RequestId?) : Model()
}

sealed class Signal {
    object CounterSent : Signal()
}
  1. Next, define your empress.

Implement Empress interface:

class SampleEmpress : Empress<Model, Signal>() {
    override fun initialModels(): Collection<Model> {
        return listOf(Model.Counter(0), Model.Sender(null))
    }

    suspend fun increment() = onEvent {
        val count = get<Model.Counter>().count
        update(Model.Counter(count + 1))
    }

    suspend fun sendCounter() = onEvent {
        // If request is already in progress, return early.
        if (get<Model.Sender>().requestId != null) return@onEvent

        val count = get<Model.Counter>().count
        val requestId = request { sendCounter(count) } // schedule a request
        update(Model.Sender(requestId))
    }

    private suspend fun sendCounter(count: Int) = onRequest {
        delay(count * 1000L) // emulate sending the value
        onCounterSent() // call an event handler
    }

    private suspend fun onCounterSent() = onEvent {
        signal(Signal.CounterSent)
        update(Model.Sender(null))
    }
}
  1. In your Activity or Fragment, attach your empress, send events and listen for updates:
private lateinit var api: EmpressApi<SampleEmpress, Model, Signal>

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    // install our empress
    api = enthrone(SampleEmpress())

    // pass events to empress
    increment_button.setOnClickListener {
        api.post { increment() }
    }
    send_button.setOnClickListener {
        api.post { sendCounter() }
    }

    // Listen for updated models and signals:
    lifecycle.coroutineScope.launch {
        api.updates().collect { model ->
            render(model)
        }
        api.signals().collect { signal ->
            if (signal is Signal.CounterSent) {
                Toast.makeText(context, "Counter has been sent!", Toast.LENGTH_LONG).show()
            }
        }
    }
}

private fun render(model: Model) {
    when (model) {
        is Model.Counter -> text_view.text = model.count.toString()
        is Model.Sender -> loader_view.visibility = if (model.requestId != null) View.VISIBLE else View.GONE
    }
}

Status

Empress library is stable and tested, but considered alpha, and its public API may change without backwards compatibility.

License

This project is published under Apache License, Version 2.0 (see the LICENSE file for details).

Apps
About Me
GitHub: Trinea
Facebook: Dev Tools