Niestety niektóre stany mają nadmiarowe dane, bo łatwo się tego nie da uniknąć w Javie (zwłaszcza na Androida). Najszybsze rozwiązanie to takie, żeby skorzystać z jakiegoś kontenera typu Option
i wtedy nie trzeba używać R.string.empty
tylko Option.none()
. Można ewnetualnie zrobić @StringRes @Nullable final Integer progressInfo
, ale najzwyczajniej nie chce mi się użerać podczas bindowania z tym.
if (uiModel.state.progressInfo == null) {
progressInfo.setText(null);
} else {
progressInfo.setText(uiModel.state.progressInfo);
}
Nie mogę zrobić po prostu progressInfo.setText(uiModel.state.progressInfo)
, bo zostanie użyta zła przeciążona metoda i unboxing wywali apkę. Ale jest to i tak półśrodek, bo dalej jakaś informacja o progresie jest.
Ten UiModel
ma też inny problem, bo stany które nie powinny nieść ze sobą informacji np. o hotspocie albo wartości progresu, tę wartość niosą. Tutaj Java staje się już bardzo brzydka, bo albo akceptujemy, że te wartości opcjonalnie są, albo piszemy trochę boilerplatu i tworzymy typy unijne, albo korzystamy ze wzorca odwiedzającego. Wtedy informację o hotspocie niósłby tylko stan AWAITING_IPAD
.
W aplikacji renderowanie jest używane w ten sposób.
disposables.add(uiBinder.startTransferClicks()
.compose(presenter::processEvents)
.scan(UiModel.createIdle(), this::toModel)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(uiBinder::renderUi, RxUtil::rethrow));
// Sygnatury ważniejszych metod
Observable<Object> startTransferClicks()
Observable<Either<EnableHotspotResult, TransferFilesResult>> processEvents(Observable<Object> events)
UiModel toModel(UiModel previous, Either<EnableHotspotResult, TransferFilesResult> result)
Gdyby w UI był więcej niż jeden typ zdarzenia, to zamiast uiBinder.startTransferClicks()
można stworzyć połączony strumień różnych zdarzeń we własny typ i przekazać go do prezentera. Tak naprawdę już tutaj powinienem stworzyć własny typ TransferFilesEvent
.
W processEvents
prezenter korzysta z różnych operacji na strumieniach, żeby odpowiednio przetwarzać dane i obsługiwać wszystkie sytuacje błędu - brak plików, nagłe wyłączenie hotspota, niemożność włączenia hotspota itd. Łącznie około 20 linii kodu do samego łączenia strumieni. Biorąc pod uwagę jak skomplikowany jest cały przypadek użycia, myślę że wynik jest zadowalający.
toModel
po prostu mapuje wyniki z prezentera na UiModel
i ma dostęp do poprzednio ustawionego modelu gdybyśmy potrzebowali jakichś poprzednich danych. Żeby przyjemniej się mapowało, to UiModel
ma na sobię metodę UiModel.Builder toBuilder()
. Teoretycznie boilerplate, ale wystarczy użyć AutoValue (albo Lomboka jeżeli ktoś siebie nienawidzi).
To wyżej to trochę kłamsto, bo nie mam w tej aplikacji prezentera tylko wszystko jest tworzone i łączone w onCreate
, ale nie ma problemu, żeby taki prezenter stworzyć. I żeby nie być gołosłownym, to w moim głównym projekcie stosuję to podejście z prezeneterami.
Natomiast w Kotlinie dużo upierdliwości znika bo mamy data class
z copy()
, sealed class
, typealias
i korutyny.