Skip to content

Commit

Permalink
Update README.md
Browse files Browse the repository at this point in the history
  • Loading branch information
sellmair committed Aug 9, 2024
1 parent cf2ec12 commit 6aa3771
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 72 deletions.
Binary file added .img/logo_signed.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
188 changes: 116 additions & 72 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,31 @@
# Evas: **Ev**ents **a**nd **S**tates for Kotlin
# **Ev**ents **a**nd **S**tates for Kotlin

<p>
<img src=".img/banner.png" width="512" align="middle"
<img src=".img/logo_signed.png" width="365"
alt="Evas logo by Sebastian Sellmair">
</p>

## Overview
# What is Evas?

- ✅ 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'
Evas is a **library** providing

- `Events`: A performant / scalable EventBus integrated with coroutines (and compose)
- `States`: A pragmatic container for 'States' of your application

Discussions about architecture are fun, but at its core usually evolve around answering the following questions

1) Where does my 'State' live?
2) How do pass 'Events' around?

___

✅ Multiplatform (jvm, android, iOS, watchOS, macOS, linux, windows, wasm, js, ...)<br>
✅ Fast / Performance benchmarked (kotlinx.benchmark)<br>
✅ Concurrency tested (kotlinx.lincheck)<br>
✅ API stability tested (kotlinx.binary-compatibility-validator)<br>
✅ Tiny Binary Size (~ 90kb)<br>
➕ Compose Extensions<br>
➕ Inline documentation with 'usage examples'<br>

---

Expand All @@ -24,21 +36,26 @@ implementation("io.sellmair:evas:1.0.0-beta01")
```

(Compose Extensions)

```kotlin
implementation("io.sellmair:evas-compose:1.0.0-beta01")
```

---

# Simple Usage

## Setup

Instances of the `Events` (Event Bus) and `States`(State Container) can simply be created using the
`Events()` and `States()` factory functions.
`Events()` and `States()` factory functions.

### Coroutines Context

Binding them to the current coroutine context is as simple as

[snippet]: (setup-coroutines.kt)

```kotlin
val events = Events() // <- create new instance
val states = States() // <- create new instance
Expand All @@ -48,9 +65,11 @@ withContext(events + states) {
```

### Compose Extensions

Binding the event bus or state container to compose is as simple as

[snippet]: (setup-compose.kt)

```kotlin
val events = Events() // <- create new instance
val states = States() // <- create new instance
Expand All @@ -63,9 +82,57 @@ fun App() {
}
```

## Send and Subscribe to Events

The following snippet shows how two Events are processed:

1) `LoginEvent`: Will be fired once a user successfully logged into our application
2) `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 wihout waiting for all listeners.

[snippet]: (loginEvents.kt)

```kotlin
object LogoutEvent : Event

data class LoginEvent(val userName: String, val token: String) : Event

suspend fun listenForLogout() = collectEvents<LogoutEvent> {
println("User logged out")
}

suspend fun listenForLogin() = collectEvents<LoginEvent> { event ->
println("User: ${event.userName} logged in")
}

suspend fun login(userName: String, password: String) {
val token = httpClient().login(userName, password) ?: return
LoginEvent(userName, token).emit()
// ^
// Actually emit the event and suspend until
// All listeners have finished processing this event
}

suspend fun logout() {
deleteUserData()
LogoutEvent.emit()
// ^
// Actually emit the event and suspend until
// All listeners have finished processing this event
}
```

## Simple State

Defining a simple State counting the number of 'clicks' performed by a user
[snippet]: (simpleClickCounterState.kt)

```kotlin
data class ClickCounterState(val count: Int) : State {
/*
Expand Down Expand Up @@ -95,6 +162,7 @@ suspend fun onClick() {

Using this state and print updates to the console
[snippet]: (usingClickCounterState.kt)

```kotlin
fun CoroutineScope.launchClickCounterPrinter() = launch {
ClickCounterState.flow().collect { state ->
Expand All @@ -103,78 +171,49 @@ fun CoroutineScope.launchClickCounterPrinter() = launch {
}
```

## Send and Subscribe to Events
## Launch State Producer & Show Compose UI

[snippet]: (loginEvents.kt)
```kotlin
object LogoutEvent: Event
### Define a 'hot' state and 'hot' state producer

data class LoginEvent(val userName: String, val token: String): Event
```kotlin
// Define the state
sealed class UserLoginState : State {
companion object Key : State.Key<UserLoginState> {
override val default = LoggedOut
}

suspend fun listenForLogout() = collectEvents<LogoutEvent> {
println("User logged out")
data object LoggedOut : UserLoginState()
data object LoggingIn : UserLoginState()
data class LoggedIn(val userId: UserId) : UserLoginState()
}

suspend fun listenForLogin() = collectEvents<LoginEvent> { event ->
println("User: ${event.userName} logged in")
}
// Define the state producer
fun CoroutineScope.launchUserLoginState() = launchState(UserLoginState) {
val user = getUserFromDatabase()
if (user != null) {
LoggedIn(user.userId).emit()
return@launchState
}

suspend fun login(userName: String, password: String) {
val token = httpClient().login(userName, password) ?: return
LoginEvent(userName, token).emit()
// ^
// Actually emit the event and suspend until
// All listeners have finished processing this event
}
LoggedOut.emit()

suspend fun logout() {
deleteUserData()
LogoutEvent.emit()
// ^
// Actually emit the event and suspend until
// All listeners have finished processing this event
}
```
collectEvents<LoginRequest>() { request ->
LoggingIn.emit()

## Launch State Producer & Show Compose UI
### Define a 'hot' state and 'hot' state producer
```kotlin
// Define the state
sealed class UserLoginState: State {
companion object Key: State.Key<UserLoginState> {
override val default = LoggedOut
}

data object LoggedOut: UserLoginState()
data object LoggingIn: UserLoginState()
data class LoggedIn(val userId: UserId): UserLoginState()
}

// Define the state producer
fun CoroutineScope.launchUserLoginState() = launchState(UserLoginState) {
val user = getUserFromDatabase()
if(user!=null) {
LoggedIn(user.userId).emit()
return@launchState
}

LoggedOut.emit()

collectEvents<LoginRequest>() { request ->
LoggingIn.emit()

val response = sendLoginRequestToServer(request.user, request.password)
if(response.isSuccess) {
LoggedIn(response.userId).emit()
} else {
LoggedOut.emit()
}
}
}
val response = sendLoginRequestToServer(request.user, request.password)
if (response.isSuccess) {
LoggedIn(response.userId).emit()
} else {
LoggedOut.emit()
}
}
}
```

### Use State in UI development (e.g., compose, using `io.sellmair:evas-compose`)
### Use State in UI development (e.g., compose, using `io.sellmair:evas-compose`)

[snippet]: (composeValue.kt)

```kotlin
@Composable
fun App() {
Expand All @@ -194,18 +233,23 @@ fun App() {
___

# Sample Projects

## Login Screen App (iOS, Android, Desktop App)

- [Entry Point: Android](samples/login-screen/src/androidMain/kotlin/io/sellmair/sample/MainActivity.kt)
- [Entry Point: iOS]()

## Joke App (iOS, Android, Desktop App)

- [Entry Point: Android](samples/joke-app/src/androidMain/kotlin/io/sellmair/jokes/MainActivity.kt)
- [Entry Point: iOS](samples/login-screen/src/iosMain/kotlin/io/sellmair/sample/SampleAppViewController.kt)
- [Entry Point: Desktop](samples/login-screen/src/jvmMain/kotlin/io/sellmair/sample/SampleApplication.kt)
- [Login Screen: Compose UI](samples/login-screen/src/commonMain/kotlin/io/sellmair/sample/ui/LoginScreen.kt)

## CLI Application (Directory Scanner)

![directory-statistics-cli.gif](samples/directory-statistics-cli/.img/directory-statistics-cli.gif)

- [Entry Point: Main.kt](https://github.com/sellmair/evas/blob/895fcb39528ff008bcbbe5959b3f79298caabbdc/samples/directory-statistics-cli/src/nativeMain/kotlin/Main.kt)
- [SummaryState](samples/directory-statistics-cli/src/nativeMain/kotlin/SummaryState.kt)
- [Command Line UI](samples/directory-statistics-cli/src/nativeMain/kotlin/uiActor.kt)

0 comments on commit 6aa3771

Please sign in to comment.