Events and States for Kotlin
What is Evas?
Evas is a library providing
Events
A performant, scalable and most importantly scoped Event-Channel integrated with coroutines (and compose), which
offers advantages like a traditional "Event Bus" (such as flexibility, decoupling, simplicity), whilst retaining
structured concurrency, clear scoping, and easy testing.
States
A pragmatic state container, offering the accessibility and ease of use of a Singleton, whilst also retaining clearly
structured concurrency, scoping, and easy testing.
Discussions about architecture are fun, but at its core usually evolve around answering the following questions
- Where does my 'State' live? ->
States
- How to pass 'Events' around? ->
Events
- How are Events and States connected? ->
kotlinx.coroutines
✅ Multiplatform (jvm, android, iOS, watchOS, macOS, linux, windows, wasm, js, ...)
✅ Fast / Performance benchmarked (kotlinx.benchmark)
✅ Concurrency tested (kotlinx.lincheck)
✅ API stability tested (kotlinx.binary-compatibility-validator)
✅ Tiny Binary Size (~ 90kb)
➕ Compose Extensions
➕ Inline documentation with 'usage examples'
Dependencies
implementation("io.sellmair:evas:1.4.0")
Maven:
<dependency>
<groupId>io.sellmair</groupId>
<artifactId>evas-jvm</artifactId>
<version>1.4.0</version>
</dependency>
(Compose Extensions)
implementation("io.sellmair:evas-compose:1.4.0")
Simple Usage
Setup
Instances of the Events (Event Bus) and States(State Container) can simply be created using the
Events() and States() factory functions.
Coroutines Context
Binding them to the current coroutine context is as simple as
val events = Events()
val states = States()
withContext(events + states) {
}
Compose Extensions
Binding the event bus or state container to compose is as simple as
val events = Events()
val states = States()
@Composable
fun App() {
installEvas(events, states) {
MainPage()
}
}
Send and Subscribe to Events
The following snippet shows how two Events are processed:
LoginEvent: Will be fired once a user successfully logged into our application
LogoutEvent: Will be fired once a user intends to log out
Once the Events instance is installed in the current 'Coroutine Context', listening for them
can be done usinge the collectEvents method.
Firing an event can simply be done by calling emit:
Note: The emit() function will suspend until all listening coroutines finished processing.
(See emitAsync() to dispatch events without waiting for all listeners.
Simple State
Defining a simple State counting the number of 'clicks' performed by a user
Using this state and print updates to the console
snippet: (usingClickCounterState.kt)
fun CoroutineScope.launchClickCounterPrinter() = launch {
ClickCounterState.collect { state ->
println("Click Count: ${state.count}")
}
}
Launch State Producer & Show Compose UI
Define a 'hot' state and 'hot' state producer
In this example we're going to model the 'Login State' of a user which can be
a) Logged Out
b) Currently Logging In
c) Logged In
For this the State can be modeled using a sealed class.
The state will be produced by a 'launchState' coroutine, which will try to find the user data
from a local database and handles login requests (sent as events)
Use State in UI development (e.g., compose, using io.sellmair:evas-compose)
@Composable
fun App() {
val loginState = UserLoginState.composeValue()
when (loginState) {
is LoggedOut -> ShowLoginScreen()
is LoggingIn -> ShowLoginSpinner()
is LoggedIn -> ShowMainScreen()
null -> Unit
}
}
Sample Projects
Login Screen App (iOS, Android, Desktop App)
Joke App (iOS, Android, Desktop App)
CLI Application (Directory Scanner)
