Skip to content

Commit d6d78ca

Browse files
committed
rxjava published and mvi post added
1 parent 03d7e70 commit d6d78ca

File tree

5 files changed

+358
-1
lines changed

5 files changed

+358
-1
lines changed

_drafts/2019-08-26-mvi.md

Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
---
2+
layout: post
3+
title: "MVI"
4+
date: 2019-08-26
5+
categories: ["Wzorce projektowe"]
6+
permalink: /blog/wzorce/:title/
7+
image: patterns/mvi
8+
github: design-patterns/tree/master/mvi
9+
description: "Wzorce projektowe / architektoniczny"
10+
keywords: "mvi, model, view, intent, wzorzec, wzorce projektowe, wzorzec architektoniczny, design patterns, android, java, programowanie, programming"
11+
---
12+
13+
## Zastosowanie
14+
`MVI` (ang. `Model-View-Intent`) (wzorzec architektoniczny) usprawnia proces tworzenia i rozwijania aplikacji pisanych przy użyciu programowania reaktywnego poprzez wyróżnienie podziału odpowiedzialności pomiędzy trzy podmioty: widoku (`View`), modelu (`Model`) i intencji (`Intent`). Wzorzec ten jest w pewnym sensie pochodną `MVP` i `MVVM` dostosowaną do `programowania reaktywnego`. Eliminuje użycie metod zwrotnych (`callback`) oraz znacznie redukuje ilość metod wejście/wyjście (`input/output`). Jest także odpowiedzią na pojawiający się problem synchronizacji i rozbieżności przyjmowanych stanów przez warstwę widoku i logiki biznesowej wynikający z faktu iż stan jest sterowany przez warstwę `Presenter` lub `ViewModel`. W efekcie może prowadzić to do nieoczekiwanych zachowań i trudności w ich przewidzeniu oraz utrudnić debugowanie. `MVI` opiera się cyklicznym i jednokierunkowym przepływie oraz na istnieniu jednego niezmiennego (`immutable`) stanu wspólnego dla wszystkich warstw jako jedynego źródła prawdy. Rolę reprezentanta stanu przyjmuje model, który definiuje możliwe stany na podstawie wartości przechowywanych danych. `Model` nie jest zatem kontenerem danych i łącznikiem do logiki biznesowej lecz instancją stanu, który może zawierać pewne informacje jednoznacznie go definiujące. `Warstwa widoku` obserwuje akcje użytkownika i zdarzenia systemu w efekcie których nadaje intencję dla wywołanego zdarzenia, a także nasłuchuje i reaguje na zmianę stanu modelu. Widok nie musi zatem znać zarówno odbiorcy jak i nadawcy strumienia danych. `Intencja` przeważnie nie jest osobną warstwą lecz reprezentantem przyszłej akcji zmieniającej stan modelu.
15+
16+
## Ograniczenia
17+
Wykorzystanie wzorca `MVI` jest propozycją na reaktywną architekture aplikacji co jednocześnie definuje jego charakterystykę oraz ogranicza jego zastosowanie. W przeciwieństwie do `MVP` i `MVVM` nie może być użyty w oderwaniu od programowania reaktywnego ponieważ stanowi jego implementacje dla architektury systemu. Pomimo iż programowanie reaktywne może być realizowane bez użycia dodatkowych zewnętrznych zależności przeważnie jednak jest implementowane z wykorzystaniem zewnętrznej biblioteki np. `RxJava` co dodatkowo wiąże `MVI` z zależnościami. Wymaga od programisty znajomości mechanizmów tworzenia aplikacji w sposób reaktywny. Ponadto nie eliminuje problemu zmiany konfiguracji czy wycieku pamięci.
18+
19+
## Użycie
20+
Podobnie jak w przypadku innych wzorców architektonicznych zastosowanie `MVI` ma za zadanie wyeliminować problem `God Activity` oraz zwiększyć czytelność kodu i możliwości jego rozwoju. Może zostać zaimplementowany zarówno przy tworzeniu nowej aplikacji jak i już istniejącej poprzez migracje obecnej architektury. Sprawdzi się jednak przede wszystkim tam, gdzie aplikacja już jest realizowana w sposób reaktywny. Ponadto jest dobrym wyborem w przypadku skomplikowanych przepływów pracy, które cechują się dużą ilością metod wejściowych i wyjściowych w odpowiadającej implementacji `MVP` i `MVVM`.
21+
22+
## Implementacja
23+
Realizacja wzorca `MVI` może przypominać tą znaną z `MVP`, różnica polega jednak na implementacji i interakcji komponentów. Na podstawie klasy `Model` będącej kontenerem danych opisujących stan, definiowane są finalne klasy stanów o wspólnym rodzicu `PartialState`. Warstwa widoku `ViewImpl` implementuje interfejs `View` dostarczając zachowania obsługi otrzymanych stanów w metodzie `render` oraz emisji intencji w odpowiedzi na akcje użytkownika. Klasa `Presenter` odpowiada za odbieranie i przetwarzanie intencji z widoku oraz delegowanie ich do logiki biznesowej. W odpowiedzi na otrzymany stan tworzy nowy niezmienny obiekt modelu w metodzie `reduce`, który trafia do widoku.
24+
25+
![MVI diagram](/assets/img/diagrams/patterns/mvvi.svg){: .center-image }
26+
27+
Poniższy listing przedstawia realizację wzorca `MVI` z pominięciem zależności `Androidowych` oraz zastosowaniem `RxJava` w celu realizacji programowania reaktywnego.
28+
29+
{% highlight kotlin %}
30+
//define Model where fields values describe the states of the screen
31+
//e.g. screen can be in progress loading data, showing error or the final result
32+
data class Model(
33+
val progress : Boolean = false,
34+
val error : Boolean = false,
35+
val result : String = ""
36+
)
37+
38+
//decompose Model's fields into parts which represent single State
39+
//Kotlin sealead class is great for this
40+
sealed class PartialState {
41+
42+
object ProgressState : PartialState()
43+
object ErrorState : PartialState()
44+
class SuccessState(val result : String) : PartialState()
45+
}
46+
47+
interface View {
48+
49+
//the main responsibility is to react for received state in single method
50+
fun render(state : Model)
51+
52+
//declare methods for emitting Intents like click on the button
53+
fun emitActionIntent() : Observable<Boolean> //observable of RxJava
54+
}
55+
56+
//to simplify MVI idea the View is implemented by some class, in Android it should be e.g. Activity
57+
class ViewImpl : View {
58+
59+
private val presenter = Presenter(Interactor())
60+
61+
public ViewImpl() {
62+
presenter.bindIntents(this)
63+
64+
//init views
65+
66+
emitActionIntent() //emit action on start
67+
}
68+
69+
override fun emitActionIntent() : Observable<Boolean> {
70+
return Observable.just(true)
71+
}
72+
73+
//render received model and react for changes i
74+
override fun render(state : Model) {
75+
with(state) {
76+
showProgress(progress)
77+
showError(error)
78+
showResult(result)
79+
}
80+
}
81+
82+
private fun showProgress(enable : Boolean) {
83+
//show or hide progress bar
84+
}
85+
86+
private fun showError(enable : Boolean) {
87+
//show or hide some error
88+
}
89+
90+
private fun showResult(result : String) {
91+
//show some result
92+
}
93+
}
94+
95+
class Presenter(private val interactor : Interactor) {
96+
97+
fun bindIntents(view : View) {
98+
//bindIntents to observer Intents from View
99+
val actionObservable = view.emitActionIntent()
100+
.flatMap { interactor.fetchResult() }
101+
.startWith(PartialState.ProgressState)
102+
103+
//subscribe to backend response
104+
actionObservable.subscribe {
105+
view.render(reduce(it))
106+
}
107+
}
108+
109+
//remember to proper manage lifecycle and memory leaks and unbind observers and destroy observable
110+
111+
//map received partial State to Model
112+
private fun reduce(partialState : PartialState) : Model {
113+
return when(partialState) {
114+
PartialState.ProgressState -> Model(progress = true)
115+
PartialState.ErrorState -> Model(error = true)
116+
is PartialState.SuccessState -> Model(result = partialState.result)
117+
}
118+
}
119+
}
120+
121+
//define some backend by Repository or Manager or Interactor class etc.
122+
class Interactor {
123+
124+
fun fetchResult() : Observable<PartialState> {
125+
//try to download some data and return proper partial state based on result
126+
return Observable.just(PartialState.SuccessState("some result")) //mock
127+
}
128+
}
129+
{% endhighlight %}
130+
131+
Implementacja `MVI` może zostać zrealizowana na różne sposoby. Częstą praktyką jest wykorzystanie elementów ze wzorca`MVP`, dzięki czemu podział warstw i przepływ pracy staje się przejrzysty. Wprowadzenie interfejsu `View` umożliwia zachowanie ogólności typów i pozwala na ponowną implementacje warstwy widoku. `Presenter` wyraźnie wskazuje miejsce odbierania i przetwarzania intencji oraz decyduje o wartości modelu, natomiast `Interactor` pełni rolę realizacji logiki biznesowej. Niezmiennym elementem wzorca `MVI` jest programowanie reaktywne, które może zostać zrealizowane natywnie lub poprzez zewnętrzne biblioteki. Przeważnie jednak w tym celu wykorzystywana jest `RxJava` wraz z `RxBinding`, które służy do emisji strumienia w odpowiedzi na zdarzenie interfejsu użytkownika. Wyzwanie stanowi także odpowiednie zarządzanie cyklem życia strumieni oraz zachowanie ostatniego znanego stanu co może zostać częściowo osiągnięte przy użyciu klasy `ViewModel`. Ponadto ważnym asptektem jest optymalna implementacja metody `reduce` uwzględniającej stan poprzedni oraz metody `render`, szczególnie w przypadku wielu możliwych stanów.
132+
133+
## Przykład
134+
Aplikacja Cantor umożliwia śledzenie kursów walut względem waluty bazowej. Wyniki przedstawiane są w postaci listy nieskończonej, która pobiera dane dzień po dniu (wstecz) w odpowiedzi na przesunięcie listy elementów na dół. Aby zapewnić dobry odbiór aplikacji (User Experience) należy odpowiednio prezentować postęp ładowania danych oraz komunikaty błędu. W tym celu zastosowano wzorzec `MVI` wraz z realizacją programowania reaktywnego przez `RxJava` i `RxBinding`.
135+
136+
{% highlight kotlin %}
137+
data class CantorState(
138+
val progress : Boolean = false,
139+
val error : Boolean = false,
140+
val progressList : Boolean = false,
141+
val errorList : Boolean = false,
142+
val items : List<ExchangeRate> = listOf()
143+
)
144+
145+
sealed class PartialCantorState {
146+
147+
object ProgressState : PartialCantorState()
148+
object ErrorState : PartialCantorState()
149+
object ProgressMoreState : PartialCantorState()
150+
object ErrorMoreState : PartialCantorState()
151+
class SuccessState(val result : List<ExchangeRate>) : PartialCantorState()
152+
}
153+
154+
interface CantorView {
155+
156+
fun render(state : CantorState)
157+
158+
fun loadTodayIntent() : Observable<Boolean>
159+
fun loadMoreIntent() : Observable<Boolean>
160+
}
161+
162+
class MainActivity : AppCompatActivity(), CantorView {
163+
164+
private val presenter = CantorPresenter(CantorInteractor())
165+
private val itemAdapter = ExchangeRatesAdapter()
166+
167+
override fun onCreate(savedInstanceState: Bundle?) {
168+
super.onCreate(savedInstanceState)
169+
setContentView(R.layout.activity_main)
170+
171+
recyclerView.apply {
172+
adapter = itemAdapter
173+
layoutManager = LinearLayoutManager(this@MainActivity)
174+
}
175+
176+
presenter.bind(this)
177+
}
178+
179+
//avoid it, this could makes memory leak, provide saving last state in presenter
180+
override fun onDestroy() {
181+
presenter.unbind()
182+
super.onDestroy()
183+
}
184+
185+
override fun loadTodayIntent(): Observable<Boolean> {
186+
return Observable.just(true)
187+
}
188+
189+
override fun loadMoreIntent(): Observable<Boolean> {
190+
//use RxBinding
191+
return recyclerView.scrollStateChanges()
192+
.filter { isBottomScroll() }
193+
.map { true }
194+
}
195+
196+
override fun render(state : CantorState) {
197+
with(state) {
198+
showProgress(progress)
199+
showError(error)
200+
showProgressMore(progressList)
201+
showErrorMore(errorList)
202+
showResult(items)
203+
}
204+
}
205+
206+
private fun showProgress(enable : Boolean) {
207+
//show or hide main progress bar
208+
}
209+
210+
private fun showError(enable : Boolean) {
211+
//show or hide some main error container
212+
}
213+
214+
private fun showProgressMore(enable : Boolean) {
215+
//show or hide progress bar on the bottom of the list
216+
}
217+
218+
private fun showErrorMore(enable : Boolean) {
219+
//show or hide some error about load more
220+
}
221+
222+
private fun showResult(newItems : List<ExchangeRate>) {
223+
itemAdapter.addItems(newItems)
224+
}
225+
226+
private fun isBottomScroll() : Boolean {
227+
//detect somehow is the bottom of the list to load more items
228+
val linearLayoutManager = recyclerView.layoutManager as LinearLayoutManager
229+
return linearLayoutManager.findLastCompletelyVisibleItemPosition() == itemAdapter.itemCount - 1
230+
}
231+
}
232+
233+
class CantorPresenter(private val interactor : CantorInteractor) {
234+
235+
//for proper init and unit stream
236+
private val compositeDisposable = CompositeDisposable()
237+
238+
//downgrade date by one day for every load more in real app
239+
private var date = Date(System.currentTimeMillis())
240+
241+
fun bind(view : CantorView) {
242+
//create and set all intents from view
243+
val loadToday = view.loadTodayIntent()
244+
.flatMap { interactor.fetchDataToday()
245+
.startWith(PartialCantorState.ProgressState)
246+
.onErrorReturn { PartialCantorState.ErrorState }
247+
}
248+
249+
val loadMore = view.loadMoreIntent()
250+
.flatMap { interactor.fetchDataDay(date)
251+
.startWith(PartialCantorState.ProgressMoreState)
252+
.onErrorReturn { PartialCantorState.ErrorMoreState }
253+
}
254+
255+
//merge them for proper UI and states manage
256+
val allIntents = Observable.merge(listOf(loadToday, loadMore))
257+
258+
compositeDisposable.add(
259+
//use state reducer by scan operator to merge current and previous state
260+
allIntents.scan(CantorState(), this::reduce).subscribe { view.render(it) }
261+
)
262+
}
263+
264+
fun unbind() {
265+
compositeDisposable.clear()
266+
}
267+
268+
private fun reduce(previous : CantorState, changes : PartialCantorState) : CantorState {
269+
//for some states there could be need previous state data model
270+
return when(changes) {
271+
PartialCantorState.ProgressState -> CantorState(progress = true)
272+
PartialCantorState.ErrorState -> CantorState(error = true)
273+
PartialCantorState.ProgressMoreState -> CantorState(progressList = true, items = previous.items) //save the data
274+
PartialCantorState.ErrorMoreState -> CantorState(errorList = true, items = previous.items) //save the data
275+
is PartialCantorState.SuccessState -> {
276+
//add data to previous model instead of init only by changes
277+
val data = mutableListOf<ExchangeRate>()
278+
data.addAll(previous.items)
279+
data.addAll(changes.result)
280+
CantorState(items = data)
281+
}
282+
}
283+
}
284+
}
285+
286+
class CantorInteractor {
287+
288+
//get data exchange rates from remote server, this is just mock implementation
289+
//return proper state depends on the fetch items
290+
291+
fun fetchDataToday() : Observable<PartialCantorState> {
292+
val items = mutableListOf<ExchangeRate>()
293+
repeat(20) {
294+
Random.nextDouble(0.1, 100.0)
295+
items.add(ExchangeRate("Currency $it", random()))
296+
}
297+
return Observable.just(PartialCantorState.SuccessState(items))
298+
}
299+
300+
fun fetchDataDay(date : Date) : Observable<PartialCantorState> {
301+
val items = mutableListOf<ExchangeRate>()
302+
repeat(20) {
303+
items.add(ExchangeRate("Currency $it", random()))
304+
}
305+
return Observable.just(PartialCantorState.SuccessState(items))
306+
}
307+
308+
private fun random() : Double {
309+
return ((1..10000).random() / 100).toDouble()
310+
}
311+
}
312+
{% endhighlight %}
313+
314+
## Biblioteki
315+
Biblioteka `Mosby` jest przykładem realizacji wzorca `MVI`, która znacznie usprawnia implementację oraz eliminuje problem zmiany konfiguracji i wycieku pamięci. Do realizacji programowania reaktywnego wykorzystuje `RxJava`. W przypadku własnej implementacji zalecanym jest również wykorzystanie biblioteki programowania reaktywnego `RxJava` wraz z `RxBinding`, która upraszczają zarządzanie strumieniami.
Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ image: libraries/rxjava
77
github: libraries/tree/master/rxjava
88
description: "Biblioteki"
99
version: RxJava 2.2, RxAndroid 2.1
10-
keywords: "reaktywne, reactive, rxandroid, rxjava, reactivex, obserwator, obserwowany, observer, observable, schedulers, operator, disposable, subscribe, async, thread, java, android, programowanie, programming"
10+
keywords: "reaktywne, reactive, rxandroid, rxjava, rxbinding, reactivex, obserwator, obserwowany, observer, observable, schedulers, operator, disposable, subscribe, async, thread, java, android, programowanie, programming"
1111
---
1212

1313
## Programowanie reaktywne
@@ -247,3 +247,45 @@ public class MultipleObserversActivity extends AppCompatActivity {
247247
}
248248
}
249249
{% endhighlight %}
250+
251+
## RxBinding
252+
Przechwycenie zdarzeń interfejsu użytkownika takich jak np. kliknięcie na przycisk, wpisanie tekstu itp. w programowaniu imperatywnym wymagają metod zwrotnych (`callback`). W `RxJava` zdarzenie te mogą być emitowane za pomocą strumieni danych przy użyciu `RxBinding` dzięki czemu komunikacja kontrolek widoku z innymi warstwami aplikacji staje się uproszczona. W tym celu wystarczy na kontrolce widoku wywołać metodę oczekiwanej akcji co w efekcie zwróci obiekt typu `Observable`.
253+
254+
{% highlight java %}
255+
public class RxBindingActivity extends AppCompatActivity {
256+
257+
private Button button, buttonBinding;
258+
259+
@Override
260+
protected void onCreate(@Nullable Bundle savedInstanceState) {
261+
super.onCreate(savedInstanceState);
262+
setContentView(R.layout.activity_rxbinding);
263+
264+
button = findViewById(R.id.button);
265+
buttonBinding = findViewById(R.id.buttonBinding);
266+
267+
//provide click actions
268+
setButtonClickByListener();
269+
setButtonClickByRxBinding();
270+
}
271+
272+
//standard callback way
273+
private void setButtonClickByListener() {
274+
button.setOnClickListener(v -> {
275+
//perform job, for reactive programming emmit stream for Observer
276+
Observable.just(true).subscribe(aVoid -> {
277+
//action form Observer
278+
});
279+
});
280+
}
281+
282+
//RxBinding way
283+
private void setButtonClickByRxBinding() {
284+
RxView.clicks(buttonBinding).subscribe(aVoid -> {
285+
//action form Observer
286+
});
287+
}
288+
289+
//provide RxBinding for other widgets
290+
}
291+
{% endhighlight %}

assets/img/posts/patterns/mvi.jpg

526 KB
Loading
20.3 KB
Loading
58.7 KB
Loading

0 commit comments

Comments
 (0)