Skip to content

Commit

Permalink
Merge pull request #57 from RedMadRobot/feature/AND-147-add-eventqueue
Browse files Browse the repository at this point in the history
AND-147: add EventQueue and rename to ViewModelEvents
  • Loading branch information
EmogurovAnton authored Jul 30, 2024
2 parents f696bba + 6e007aa commit f7f4116
Show file tree
Hide file tree
Showing 18 changed files with 475 additions and 84 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ Gears could be used together or alone.
- [![Version](https://img.shields.io/maven-central/v/com.redmadrobot.extensions/resources-ktx?style=flat-square&label=resources-ktx)][resources-ktx] - A set of extensions for accessing resources
- [![Version](https://img.shields.io/maven-central/v/com.redmadrobot.extensions/viewbinding-ktx?style=flat-square&label=viewbinding-ktx)][viewbinding-ktx] - A set of extensions for dealing with ViewBinding

### :mag_right: **[ViewModelEvents](viewmodelevents/)**

- [![Version](https://img.shields.io/maven-central/v/com.redmadrobot.gears/kotlin?style=flat-square)][viewmodelevents-compose] - A set of extensions for dealing with ViewModelEvents inside `@Composable` functions
- [![Version](https://img.shields.io/maven-central/v/com.redmadrobot.gears/kotlin?style=flat-square)][viewmodelevents-flow] - An implementation of ViewModelEvents via `Flow`
- [![Version](https://img.shields.io/maven-central/v/com.redmadrobot.gears/kotlin?style=flat-square)][viewmodelevents-livedata] - An implementation of ViewModelEvents via `LiveData`

## Why Gears?

The goal of this monorepository is to simplify creation and publication of libraries.
Expand Down Expand Up @@ -61,5 +67,9 @@ For major changes, please open a [discussion][discussions] first to discuss what
[compose-gears]: gears/gears-compose
[kotlin-gears]: gears/gears-kotlin

[viewmodelevents-compose]: viewmodelevents/viewmodelevents-compose/
[viewmodelevents-flow]: viewmodelevents/viewmodelevents-flow/
[viewmodelevents-livedata]: viewmodelevents/viewmodelevents-livedata/

[ci]: https://github.com/RedMadRobot/gears-android/actions?query=branch%3Amain++
[discussions]: https://github.com/RedMadRobot/gears-android/discussions
27 changes: 14 additions & 13 deletions ktx/lifecycle-livedata-ktx/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@ Extended set of extensions for dealing with `LiveData`.
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->


- [Installation](#installation)
- [Usage](#usage)
- [`LiveData` delegate](#livedata-delegate)
- [Events Queue](#events-queue)
- [ViewModelEvents](#viewmodelevents)
- [Contributing](#contributing)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->
Expand Down Expand Up @@ -61,27 +60,29 @@ val liveData = MutableLiveData<SomeViewState>(initialState)
var state: SomeViewState by liveData
```

### Events Queue
### ViewModelEvents

`LiveData` stores only last value, so it is unusable for storing events.
All events should be added to some kind of buffer and emitted on call `LiveData.observe`.
`EventQueue` fits these needs.
`ViewModelEvents` implemented via `LiveData` fits these needs.

You can observe it like a normal `LiveData` and it will add to buffer all events you passed into:

```kotlin
data class MessageEvent(val message: String) : Event

val eventQueue = EventQueue()
val viewModelEvents = ViewModelEvents()

eventQueue.offerEvent(MessageEvent("A"))
eventQueue.offerEvent(MessageEvent("B"))
eventQueue.observeForever { println(it) }
eventQueue.offerEvent(MessageEvent("C"))
```
viewModelEvents.offerEvent(MessageEvent("A"))
viewModelEvents.offerEvent(MessageEvent("B"))
viewModelEvents.observeForever { println(it) }
viewModelEvents.offerEvent(MessageEvent("C"))
```
MessageEvent(message=A)
MessageEvent(message=B)
MessageEvent(message=C)

```kotlin
MessageEvent(message="A")
MessageEvent(message="B")
MessageEvent(message="C")
```

| Extension | Description |
Expand Down
52 changes: 0 additions & 52 deletions ktx/lifecycle-livedata-ktx/src/test/kotlin/EventQueueTest.kt

This file was deleted.

4 changes: 4 additions & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,8 @@ include(
":ktx:viewbinding-ktx",
":gears:gears-compose",
":gears:gears-kotlin",
":viewmodelevents:viewmodelevents-common",
":viewmodelevents:viewmodelevents-compose",
":viewmodelevents:viewmodelevents-flow",
":viewmodelevents:viewmodelevents-livedata",
)
7 changes: 7 additions & 0 deletions viewmodelevents/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## Unreleased

*No changes*

## 1.0.0 - 2024.05.14

- Public release viewModelEvents libraries
194 changes: 194 additions & 0 deletions viewmodelevents/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
# ViewModelEvents <GitHub path="RedMadRobot/gears-android/tree/main/ktx/viewmodel-events-ktx"/>
[![Version](https://img.shields.io/maven-central/v/com.redmadrobot.eventqueue/eventqueue-livedata?style=flat-square)][mavenCentral]
[![License](https://img.shields.io/github/license/RedMadRobot/EventQueue?style=flat-square)][license]

The entity to handle one-time viewModel events.

---
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

- [Installation](#installation)
- [Usage](#usage)
- [Flow implementation](#flow-implementation)
- [LiveData implementation](#livedata-implementation)
- [Best Practices](#best-practices)
- [`EventsDispatcher` interface](#eventsdispatcher-interface)
- [Shortcuts to send common events](#shortcuts-to-send-common-events)
- [Contributing](#contributing)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

## Installation

Add the dependency:

```groovy
repositories {
mavenCentral()
google()
}
dependencies {
implementation("com.redmadrobot.viewmodelevents:viewmodelevents-flow:<version>")
// or
implementation("com.redmadrobot.viewmodelevents:viewmodelevents-livedata:<version>")
// Compose extensions
implementation("com.redmadrobot.viewmodelevents:viewmodelevents-compose:<version>")
}
```

## Usage

One-time events (or single events) are a common pattern to display messages or errors in UI.
`ViewModelEvents` addresses the challenge of buffering and consuming one-time events:

- **Buffering:** When there are no subscribers to `ViewModelEvents`, emitted events are stored in a buffer.
All buffered events are then delivered sequentially as soon as you subscribe to the ViewModelEvents
- **Consumption:** Each event is emitted only once.
Thus, if you re-subscribe to the ViewModelEvents, you will not receive any events that have already been consumed.

There are two implementations: via flow (recommended for use) and via livedata (deprecated).

### Flow implementation

This implementation utilizes `StateFlow` under the hood and provides the `flow` field to observe events.

```kotlin
data class MessageEvent(val message: String) : Event

val viewModelEvents = ViewModelEvents()

viewModelEvents.offerEvent(MessageEvent("A"))
viewModelEvents.offerEvent(MessageEvent("B"))

// Observe
events.flow
.onEach { onEvent(it) }
.launchIn(scope)

viewModelEvents.offerEvent(MessageEvent("C"))
```

```kotlin
MessageEvent(message="A")
MessageEvent(message="B")
MessageEvent(message="C")
```

`ViewModelEvents` can also be used with Jetpack Compose:

```kotlin
// Remember to add viewmodelevents-compose dependency
@Composable
public fun Screen() {
// We assume, ViewModel implementation has `events` field providing ViewModelEvents instance
val viewModel = viewModel<FeatureViewModel>()

ViewModelEventsEffect(viewModel.events) { event: Event ->
when(event) {
is MessageEvent -> println("Message: $event")
is ErrorMessageEvent -> println("Error: $event")
}
}
}
```

That subscription emits values from `ViewModelEvents` when the lifecycle is at least at `minActiveState` (`Lifecycle.State.STARTED` by default).
Emission stops when the lifecycle state falls below the `minActiveState` state.

### LiveData implementation

This implementation utilizes `LiveData` under the hood and provides methods to observe events on the given lifecycle.

```kotlin
data class MessageEvent(val message: String) : Event

val viewModelEvents = ViewModelEvents()

viewModelEvents.offerEvent(MessageEvent("A"))
viewModelEvents.offerEvent(MessageEvent("B"))
viewModelEvents.observeForever { println(it) }
viewModelEvents.offerEvent(MessageEvent("C"))
```

```bash
MessageEvent(message=A)
MessageEvent(message=B)
MessageEvent(message=C)
```

| Extension | Description |
|-----------------------------------------------------------------------------|-------------|
| `Fragment.observe(liveData: ViewModelEvents, onEvent: (Event) -> Unit)` | Shorter way to observe `LiveData` in a fragment |
| `ComponentActivity.observe(liveData: ViewModelEvents, onEvent: (Event) -> Unit)` | Shorter way to observe `LiveData` in an activity |

### Best Practices

Here you can find the patterns that we found useful to simplify usage of `ViewModelEvents`.

#### `EventsDispatcher` interface

To simplify events sending, it is useful to declare the following interface:

```kotlin
/** Interface for ViewModel events dispatching. */
public interface EventsDispatcher {
public val events: ViewModelEvents

/** Offers the given [event] to be added to the ViewModel events. */
public fun offerEvent(event: Event) {
events.offerEvent(event)
}
}
```

That's it!
You can add this interface to any class you want to be able to send events.
For example, to your base `ViewModel`:

```kotlin
abstract class BaseViewModel : ViewModel(), EventsDispatcher {
override val events = ViewModelEvents()
}
```

Thus, all `ViewModel`s will be able to use `offerEvent(Event)` to send events:

```kotlin
class FeatureViewModel : BaseViewModel() {
fun onError(message: String) = offerEvent(ErrorMessageEvent(message))
}
```

#### Shortcuts to send common events

It is useful to create extension-functions to send common events.
Let's imagine you have the following events:

```kotlin
data class MessageEvent(val message: String) : Event
data class ErrorMessageEvent(val message: String) : Event
```

To simplify sending of these events, you can create shortcuts.
It works best with the [`EventsDispatcher` interface](#eventsdispatcher-interface).

```kotlin
fun EventsDispatcher.showMessage(message: String) {
offerEvent(MessageEvent(message))
}

fun EventsDispatcher.showError(message: String) {
offerEvent(ErrorMessageEvent(message))
}
```

## Contributing

Merge requests are welcome.
For major changes, please open an issue first to discuss what you would like to change.

[mavenCentral]: https://search.maven.org/artifact/com.redmadrobot.eventqueue/eventqueue-livedata
[license]: ../LICENSE
3 changes: 3 additions & 0 deletions viewmodelevents/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// For some reason gradle.properties in this project doesn't affect its subprojects
val viewModelEventsGroup = group
subprojects { group = viewModelEventsGroup }
1 change: 1 addition & 0 deletions viewmodelevents/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
group=com.redmadrobot.viewmodelevents
9 changes: 9 additions & 0 deletions viewmodelevents/viewmodelevents-common/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
plugins {
convention.library.kotlin
}

description = "ViewModelEvents common"

dependencies {
api(kotlin("stdlib"))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.redmadrobot.viewmodelevents

/** Marker interface for entities that can be put to the [ViewModelEvents]. */
public interface Event
25 changes: 25 additions & 0 deletions viewmodelevents/viewmodelevents-compose/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
plugins {
convention.library.android
}

description = "ViewModelEvents extensions for compose"

android {
namespace = "$group.compose"

buildFeatures {
compose = true
}

composeOptions {
kotlinCompilerExtensionVersion = androidx.versions.compose.compiler.get()
}
}

dependencies {
api(kotlin("stdlib"))
api(androidx.lifecycle.runtime)
api(androidx.compose.ui)
api(androidx.compose.runtime)
api(project(":viewmodelevents:viewmodelevents-flow"))
}
Loading

0 comments on commit f7f4116

Please sign in to comment.