Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AND-147: add EventQueue and rename to ViewModelEvents #57

Merged
merged 4 commits into from
Jul 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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/)**
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe better to use 💬 :speech_balloon: here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could, but would it be better to leave it for TextValue?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Otherwise i don't have any ideas which emoji we should use there :D


- [![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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think ViewModelEvents should be removed from this README. It would be great to add a note about this removal to the changelog of lifecycle-livedata-ktx

- [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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you remove the date for now? We'll add the actual release date later


- 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"/>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This tag is not needed anymore. It was previously required for rendering in our knowledge base

Suggested change
# ViewModelEvents <GitHub path="RedMadRobot/gears-android/tree/main/ktx/viewmodel-events-ktx"/>
# ViewModelEvents

[![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]
Comment on lines +2 to +3
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
[![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]
[![Version](https://img.shields.io/maven-central/v/com.redmadrobot.viewmodelevents/viewmodelevents-common?style=flat-square)][mavenCentral]
[![License](https://img.shields.io/github/license/RedMadRobot/gears-android?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
Copy link
Collaborator

@osipxd osipxd Jul 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[mavenCentral]: https://central.sonatype.com/artifact/com.redmadrobot.viewmodelevents/viewmodelevents-common

[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