MVI (Model-View-Intent) 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.
Ograniczenia
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.
Użycie
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.
Implementacja
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.
Poniższy listing przedstawia realizację wzorca MVI z pominięciem zależności Android oraz zastosowaniem RxJava w celu realizacji programowania reaktywnego.
Implementacja MVI może zostać zrealizowana na różne sposoby. Częstą praktyką jest wykorzystanie elementów ze wzorcaMVP, 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.
Przykład
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.
Biblioteki
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.