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

[MVI] First draft of a reusable architecture #57

Open
wants to merge 50 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
75f52f8
Introduce generic definitions for Middleware, Reducer and Store
tobiasheine Jun 19, 2019
fcf8f47
Introduce packages to separate domain from data and presentation
tobiasheine Jun 20, 2019
cff2005
Introduce a search middleware
tobiasheine Jun 20, 2019
da7c6c7
implement basic binding
zegnus Jun 25, 2019
be9a4b9
implement basic reducer
zegnus Jun 27, 2019
43043b8
make Activity implement MVI interface
zegnus Jul 2, 2019
b6ee901
add baseStore into the Activity
zegnus Jul 4, 2019
94d0756
bind execute search action
zegnus Jul 9, 2019
6c12a66
Subscribe on worker scheduler for search execution
tobiasheine Jul 11, 2019
698f827
Replace SearchState with a lce sealed state
tobiasheine Jul 11, 2019
86ce87f
Show search results
tobiasheine Jul 11, 2019
79ff127
hook clear query
gbasile Jul 18, 2019
a203daf
separate lifecycles of bind and wire
gbasile Jul 18, 2019
5d8bcf6
removed tests
gbasile Jul 25, 2019
3f32fb0
move mvi to core
tobiasheine Jul 26, 2019
1de22d6
delete test app for coroutines
zegnus Jul 26, 2019
9b7813c
Merge branch 'mvi_reducer' of github.com:novoda/android-demos into mv…
tobiasheine Jul 26, 2019
f3ba3e3
Remove unused classes
tobiasheine Jul 26, 2019
a47ec05
First draft of a readme
tobiasheine Jul 26, 2019
7433c8e
Add diagrams
tobiasheine Jul 26, 2019
0dd91de
Add diagrams to readme
tobiasheine Jul 26, 2019
dca82f3
Update ModelViewIntentSample/readme.md
tobiasheine Jul 26, 2019
ca4136a
Test search middle ware
tobiasheine Jul 31, 2019
3b72d11
create abstraction fort the MovieDataSource
gbasile Aug 1, 2019
8594308
split render and action provider out of a single interface
gbasile Aug 1, 2019
24f4df2
use viewModel to allows state to survive device rotation
gbasile Aug 1, 2019
b65314b
handle loading
gbasile Aug 1, 2019
aea3e85
handling error states
gbasile Aug 1, 2019
033fbe0
avoid unnecessary refreshes of the input field
gbasile Aug 1, 2019
96f48b2
Rename domain specific changes to domain agnostic screen state changes
tobiasheine Aug 13, 2019
34f4cfd
Move domain agnostic screen state and screen state changes from domai…
tobiasheine Aug 13, 2019
60c2daa
Expect SearchMiddleware to hide progress
tobiasheine Aug 13, 2019
2114248
add hideLoading and removeResults actions
zegnus Aug 15, 2019
c90fbe7
Change wording to highlight that the query is updated
tobiasheine Aug 20, 2019
f78e7ab
Use scan operator to reduce changes to a state
tobiasheine Aug 20, 2019
56ce7db
Change changes to be a PublishSubject
tobiasheine Aug 20, 2019
e121814
Remove not needed null check
tobiasheine Aug 20, 2019
f7e4799
Merge pull request #58 from novoda/mvi_reducer_state_changes
gbasile Aug 23, 2019
eaceff7
Merge branch 'mvi_reducer' into mvi_view_model
gbasile Aug 23, 2019
df90942
renamed viewRender to Displayer
gbasile Sep 5, 2019
2dfbca7
automatic wiring
gbasile Sep 5, 2019
f9a219e
Displayer implement actionProvider and render
gbasile Sep 5, 2019
0e80ea2
scope types
gbasile Sep 5, 2019
7c5a2ad
simplify VM logic
gbasile Sep 5, 2019
cb5648c
added import
gbasile Sep 5, 2019
bea331c
removed extra lines
gbasile Sep 5, 2019
efebcc1
Moved scope for State and Action into the ViewModel
gbasile Sep 9, 2019
2c0d3e2
removed abstraction
gbasile Sep 9, 2019
2e02c6c
reverted variable names
gbasile Sep 9, 2019
2737f08
Merge pull request #59 from novoda/mvi_view_model
tobiasheine Sep 10, 2019
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
2 changes: 1 addition & 1 deletion ModelViewIntentSample/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:name=".MoviesApplication">
<activity android:name=".search.SearchActivity">
<activity android:name=".search.presentation.SearchActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.novoda.movies.mvi.search

import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
import io.reactivex.functions.BiFunction
import io.reactivex.subjects.BehaviorSubject
import io.reactivex.subjects.PublishSubject

class BaseStore<A, S, C>(
private val schedulingStrategy: SchedulingStrategy,
private val reducer: Reducer<S, C>,
private val middlewares: List<Middleware<A, S, C>>,
initialValue: S
) : Store<A, S, C> {
private val changes = BehaviorSubject.create<C>()
private val state = BehaviorSubject.createDefault(initialValue)
private val actions: PublishSubject<A> = PublishSubject.create()

override fun wire(): Disposable {
val disposables = CompositeDisposable()
val newState = changes.withLatestFrom(state, BiFunction<C, S, S> { change, state ->
reducer.reduce(state, change)
})
disposables.add(newState
.subscribeOn(schedulingStrategy.work)
.subscribe(state::onNext)
)

for (middleware in middlewares) {
val observable = middleware
.bind(actions, state)
.subscribeOn(schedulingStrategy.work)
disposables.add(observable.subscribe(changes::onNext))
}

return disposables
}

override fun bind(actionProvider: ActionProvider<A>, viewRender: ViewRender<S>): Disposable {
val disposables = CompositeDisposable()

disposables.add(actionProvider.actions.subscribe(actions::onNext))

disposables.add(state
.observeOn(schedulingStrategy.ui)
.subscribe(viewRender::render)
)

return disposables
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.novoda.movies.mvi.search

import io.reactivex.Observable
import io.reactivex.disposables.Disposable

interface ActionProvider<A> {
val actions: Observable<A>
}

interface ViewRender<S> {
fun render(state: S)
}

interface Reducer<S, C> {
fun reduce(state: S, change: C): S
}

interface Middleware<A, S, C> {
fun bind(actions: Observable<A>, state: Observable<S>): Observable<C>
}

interface Store<A, S, C> {
fun wire(): Disposable
fun bind(actionProvider: ActionProvider<A>, viewRender: ViewRender<S>): Disposable
}

This file was deleted.

This file was deleted.

This file was deleted.

Binary file added ModelViewIntentSample/high_level_diagram.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
45 changes: 45 additions & 0 deletions ModelViewIntentSample/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Model-View-Intent Sample

The goal of this project is to showcase a reactive implementation of [model-view-intent](http://hannesdorfmann.com/android/model-view-intent).

The core of this MVI implementation is an unidirectional data flow where user intents are processed and mapped to view states. The view state, in this example, is an immutable value object representing the different states of one screen.

# Components

In order to make this implementation reusable we extracted a couple of generic components:

A **MVIView** is UI component which exposes a stream of intents, which we call actions in this project to not confuse with android intents, and can render a view state.

The **Middleware** processes these actions using domain specific business rules and emits a stream of changes.

The **Reducer** consumes use-case specific changes from the Middleware and converts them to a view state.

The **Store** is the glue between the above components and is mainly responsible for forwarding events between these.

## MVIView < Action >

In this concrete implementation the MVIView is a wrapper around the Activity which collaborates between multiple custom views. It merges and exposes user actions from all custom view and is capable of rendering a view state, without performing any further logic.

For example a `SearchInputView` will emit a `SubmitQuery:Action`.

## Middleware <Action, Change>

The Middlewares implement the domain specific business rules. They operate on the user actions and map them to domain specific change events.

A `SearchMiddleware` would consume an `SubmitQuery:Action` and will perform a asynchronous operation to execute a search and map the result to a `SearchCompleted:Change`.
The idea is to have multiple middlewares for the different aspects of the domain and business requirements (e.g. tracking).

## Reducer<State, Change>

The Reducer consumes the use-case specific changes and maps them to a view-state. This means multiple use-case specific actions (`SearchInProgress, FilterInProgress`) might end up in the same view state indicating progress on the screen.

Following the above example the `SearchReducer` would map `SearchCompleted:Change` to a view state containing the data needed to render search results.

## Store<Action, State, Changes>

The Store is the glue between the above mentioned components. It listens to user actions, forwards them to all middlewares, forwards their changes to the reducer and passes the generated view state back to the view.

# Diagrams
| High-level Diagram|Sequence Diagram|
|----|----|
|![MainView](https://user-images.githubusercontent.com/1046688/61949720-071baa00-afac-11e9-96e1-4e68c5b0844e.png)| ![Untitled](https://user-images.githubusercontent.com/1046688/61949761-20245b00-afac-11e9-94ab-bf51764b6cca.png) |
1 change: 1 addition & 0 deletions ModelViewIntentSample/search/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,5 @@ dependencies {
testImplementation libraries.test.mockitoKotlin
testImplementation libraries.test.mockitoInline
testImplementation libraries.test.assertj
implementation 'android.arch.lifecycle:extensions:1.1.1'
}
3 changes: 1 addition & 2 deletions ModelViewIntentSample/search/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".SearchActivity"
android:screenOrientation="portrait"/>
<activity android:name=".presentation.SearchActivity" />
</application>

</manifest>
Loading