-
Notifications
You must be signed in to change notification settings - Fork 3
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
Changes from all commits
4e683da
078d6b7
d40259f
6e007aa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think |
||
- [Contributing](#contributing) | ||
|
||
<!-- END doctoc generated TOC please keep comment here to allow auto update --> | ||
|
@@ -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 | | ||
|
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
## Unreleased | ||
|
||
*No changes* | ||
|
||
## 1.0.0 - 2024.05.14 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
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"/> | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||||||||||
[][mavenCentral] | ||||||||||
[][license] | ||||||||||
Comment on lines
+2
to
+3
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
|
||||||||||
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 | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
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 } |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
group=com.redmadrobot.viewmodelevents |
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 |
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")) | ||
} |
There was a problem hiding this comment.
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?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤔
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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