Как управлять состоянием с помощью RxJava в Android с помощью Java (Not Kotlin)

Я пытаюсь разработать приложение для Android, основанное на следующем разговоре, представленном Джейком Уортоном

The State of Managing State with RxJava
21 March 2017 – Devoxx (San Jose, CA, USA)

Джейк пообещал часть 2 и/или пример GITHUB, которые я не могу найти (если действительно существует)

На высоком уровне я могу следить за большинством вышеперечисленных разговоров.

Однако у меня есть следующие вопросы.

Я вижу, как использование UiEvent, UiModel, Action и Result устраняет проблемы.

Меня смущает следующее: -

Диаграмма на слайде 194 показывает "поток/поток" Observables as

Android Device -----> Observable<UiEvent> -----> <application code> -----> Observable<Action>  -----> {Backend}
{Backend}      -----> Observable<Result>  -----> <application code> -----> Observable<UiModel> -----> Android Device

Слайд 210 содержит этот фрагмент кода, показывающий, как поток результатов (s) передается в UiModel

SubmitUiModel initialState = SubmitUiModel.idle();
Observable<Result> results = /* ... */;
Observable<SubmitUiModel> uiModels = results.scan(initialState, (state, result) -> {
if (result == CheckNameResult.IN_FLIGHT
|| result == SubmitResult.IN_FLIGHT)
return SubmitUiModel.inProgress();
if (result == CheckNameResult.SUCCESS)
return SubmitUiModel.idle();
if (result == SubmitResult.SUCCESS)
return SubmitUiModel.success();
// TODO handle check name and submit failures...
throw new IllegalArgumentException("Unknown result: " + result);
});

и окончательный фрагмент кода на слайде 215, фрагмент кода напоминает это: -

ObservableTransformer<SubmitAction, SubmitResult> submit =
actions -> actions.flatMap(action -> service.setName(action.name)
.map(response -> SubmitResult.SUCCESS)
.onErrorReturn(t -> SubmitResult.failure(t.getMessage()))
.observeOn(AndroidSchedulers.mainThread())
.startWith(SubmitResult.IN_FLIGHT));

ObservableTransformer<CheckNameAction, CheckNameResult> checkName =
actions -> actions.switchMap(action -> action
.delay(200, MILLISECONDS, AndroidSchedulers.mainThread())
.flatMap(action -> service.checkName(action.name))
.map(response -> CheckNameResult.SUCCESS)
.onErrorReturn(t -> CheckNameResult.failure(t.getMessage()))
.observeOn(AndroidSchedulers.mainThread())
.startWith(CheckNameResult.IN_FLIGHT));

который иллюстрирует преобразование из Action (s) в Result (s)

что мне не хватает из этой беседы/слайд-колоды о том, как объединить UiEvent/UiModel с потоком Action/Result?

Поток управляется с помощью UiEvents. Как вы завершаете поток от UiEvent (s) до Action обратно к результату, а затем, наконец, UiModel?

ОБНОВЛЕНИЕ Использование API Star Wars. Я применил следующий подход. Я использую свои пользовательские интерфейсы для преобразования преобразований между событиями пользовательского интерфейса в результаты через действия, а затем просматриваю результаты, чтобы вернуться к модели пользовательского интерфейса.

Heres мои классы и код: -

ACTION CLASSES
==============

public abstract class Action<T> {

    Api service = Service.instance();

    final T data;

    public Action(final T data) {
        this.data = data;
    }

    public T getData() {
        return data;
    }

    public abstract Observable<Response<String>> execute();
}


public class CheckCharacterAction extends Action<String> {

    public CheckCharacterAction(final String characterName) {
        super(characterName);
    }

    @Override
    public Observable<Response<String>> execute() {
        return service.peopleSearch(getData());
    }    
}

public class CheckFilmAction extends Action<String> {    
    public CheckFilmAction(final String filmTitle) {
        super(filmTitle);
    }

    @Override
    public Observable<Response<String>> execute() {
        return service.filmSearch(getData());
    }    
}

public class SearchAction extends Action<String> {    
    public SearchAction(final String search) {
        super(search);
    }

    @Override
    public Observable<Response<String>>  execute() {
        return service.filmSearch(getData());
    }    
}

EVENT CLASSES
=============
public abstract class UiEvent<T> {

    private final T data;

    public UiEvent(final T data) {
        this.data = data;
    }

    public T getData() {
        return data;
    }
}

public class CharacterUiEvent extends UiEvent<String> {
    public CharacterUiEvent(final String name) {
        super(name);
    }
}

public class FilmUiEvent extends UiEvent<String> {
    public FilmUiEvent(final String title) {
        super(title);
    }
}

public class SearchUiEvent extends UiEvent<String> {
    public SearchUiEvent(final String data) {
        super(data);
    }
}

UI MODEL CLASSES
================
public class UiModel<T> {

    public final boolean isProgress;
    public final String message;
    public final boolean isSuccess;
    public T data;

    public UiModel(final boolean isProgress) {
        this.isProgress = isProgress;
        this.message = null;
        this.isSuccess = false;
        this.data = null;
    }

    public UiModel(final T data) {
        this.isProgress = false;
        this.message = null;
        this.isSuccess = true;
        this.data = data;
    }

    public UiModel(final String message) {
        this.isProgress = false;
        this.message = message;
        this.isSuccess = false;
        this.data = null;
    }

    public UiModel(final boolean isProgress, final String message, final boolean isSuccess, final T data) {
        this.isProgress = isProgress;
        this.message = message;
        this.isSuccess = isSuccess;
        this.data = data;
    }
}

public class CharacterUiModel extends UiModel<JsonData> {

    public CharacterUiModel(final boolean isProgress) {
        super(isProgress);
    }

    public CharacterUiModel(final JsonData data) {
        super(data);
    }

    public CharacterUiModel(final String message) {
        super(message);
    }

    public CharacterUiModel(final boolean isProgress, final String message, final boolean isSuccess, final JsonData data) {
        super(isProgress, message, isSuccess, data);
    }


    public static CharacterUiModel inProgress() {
        return new CharacterUiModel(true);
    }

    public static CharacterUiModel success(final JsonData data) {
        return new CharacterUiModel(data);
    }

    public static CharacterUiModel failure(final String message) {
        return new CharacterUiModel(message);
    }

}

public class FilmUiModel extends UiModel<JsonData> {


    public FilmUiModel(final boolean isProgress) {
        super(isProgress);
    }

    public FilmUiModel(final JsonData data) {
        super(data);
    }

    public FilmUiModel(final String message) {
        super(message);
    }

    public FilmUiModel(final boolean isProgress, final String message, final boolean isSuccess, final JsonData data) {
        super(isProgress, message, isSuccess, data);
    }


    public static FilmUiModel inProgress() {
        return new FilmUiModel(true);
    }

    public static FilmUiModel success(final JsonData data) {
        return new FilmUiModel(data);
    }

    public static FilmUiModel failure(final String message) {
        return new FilmUiModel(message);
    }

}

public class SearchUiModel extends UiModel<JsonData> {

    private SearchUiModel(final boolean isProgress) {
        super(isProgress);
    }

    private SearchUiModel(final JsonData data) {
        super(data);
    }

    private SearchUiModel(final String message) {
        super(message);
    }

    private SearchUiModel(final boolean isProgress, final String message, final boolean isSuccess, final JsonData data) {
        super(isProgress, message, isSuccess, data);
    }

    public static SearchUiModel idle() {
        return new SearchUiModel(false, null, false, null);
    }

    public static SearchUiModel inProgress() {
        return new SearchUiModel(true);
    }

    public static SearchUiModel success(final JsonData data) {
        return new SearchUiModel(data);
    }

    public static SearchUiModel failure(final String message) {
        return new SearchUiModel(message);
    }
}


RESULT CLASSES
==============

public abstract class Result<T> {

    public enum LIFECYCLE {
        DEPARTURE_LOUNGE,
        IN_FLIGHT,
        LANDED_SAFELY,
        CRASHED_BURNED
    }

    final LIFECYCLE lifecycle;
    final T data;
    final String errorMessage;

    public Result(final LIFECYCLE lifecycle, final T data, final String errorMessage) {
        this.lifecycle = lifecycle;
        this.data = data;
        this.errorMessage = errorMessage;
    }

    public T getData() {
        return data;
    }

    public String getErrorMessage() {
        return errorMessage;
    }

    public LIFECYCLE getLifecycle() {
        return lifecycle;
    }
}

public class CharacterResult extends Result<JsonData> {

    private CharacterResult(final LIFECYCLE lifecycle, final JsonData data, final String errorMessage) {
        super(lifecycle, data, errorMessage);
    }

    private CharacterResult(final LIFECYCLE lifecycle) {
        super(lifecycle, null, null);
    }

    public static CharacterResult departureLounge() {
        return new CharacterResult(LIFECYCLE.DEPARTURE_LOUNGE);
    }

    public static CharacterResult inflight() {
        return new CharacterResult(LIFECYCLE.IN_FLIGHT);
    }

    public static CharacterResult landedSafely(final JsonData data) {
        return new CharacterResult(LIFECYCLE.LANDED_SAFELY, data, null);
    }

    public static CharacterResult crashedBurned(final String errorMessage) {
        return new CharacterResult(LIFECYCLE.CRASHED_BURNED, null, errorMessage);
    }
}


public class FilmResult extends Result<JsonData> {

    private FilmResult(final LIFECYCLE lifecycle, final JsonData data, final String errorMessage) {
        super(lifecycle, data, errorMessage);
    }

    private FilmResult(final LIFECYCLE lifecycle) {
        super(lifecycle, null, null);
    }

    public static FilmResult departureLounge() {
        return new FilmResult(LIFECYCLE.DEPARTURE_LOUNGE);
    }

    public static FilmResult inflight() {
        return new FilmResult(LIFECYCLE.IN_FLIGHT);
    }

    public static FilmResult landedSafely(final JsonData data) {
        return new FilmResult(LIFECYCLE.LANDED_SAFELY, data, null);
    }

    public static FilmResult crashedBurned(final String errorMessage) {
        return new FilmResult(LIFECYCLE.CRASHED_BURNED, null, errorMessage);
    }
}

public class SearchResult extends Result<JsonData> {

    private SearchResult(final LIFECYCLE lifecycle, final JsonData data, final String errorMessage) {
        super(lifecycle, data, errorMessage);
    }

    private SearchResult(final LIFECYCLE lifecycle) {
        super(lifecycle, null, null);
    }

    public static SearchResult departureLounge() {
        return new SearchResult(LIFECYCLE.DEPARTURE_LOUNGE);
    }

    public static SearchResult inflight() {
        return new SearchResult(LIFECYCLE.IN_FLIGHT);
    }

    public static SearchResult landedSafely(final JsonData data) {
        return new SearchResult(LIFECYCLE.LANDED_SAFELY, data, null);
    }

    public static SearchResult crashedBurned(final String errorMessage) {
        return new SearchResult(LIFECYCLE.CRASHED_BURNED, null, errorMessage);
    }
}

Затем я настраивал свои Rx-потоки, как это следует из onCreate() Activity onCreate(): -

   final Observable<SearchUiEvent> searchEvents = RxView.clicks(activityMainBinding.searchButton)
            .observeOn(AndroidSchedulers.mainThread())
            .subscribeOn(AndroidSchedulers.mainThread())
            .map(ignored -> new SearchUiEvent(activityMainBinding.filmTitle.getText().toString()));

    final Observable<FilmUiEvent> filmEvents = RxTextView.afterTextChangeEvents(activityMainBinding.filmTitle)
            .skipInitialValue()
            .observeOn(AndroidSchedulers.mainThread())
            .subscribeOn(AndroidSchedulers.mainThread())
            .delay(1000, MILLISECONDS, AndroidSchedulers.mainThread())
            .map(text -> new FilmUiEvent(text.view().getText().toString()));

    final Observable<CharacterUiEvent> characterEvents = RxTextView.afterTextChangeEvents(activityMainBinding.people)
            .skipInitialValue()
            .observeOn(AndroidSchedulers.mainThread())
            .subscribeOn(AndroidSchedulers.mainThread())
            .delay(200, MILLISECONDS, AndroidSchedulers.mainThread())
            .map(text -> new CharacterUiEvent(text.view().getText().toString()));

    /**
     *
     */
    final Observable<UiEvent> uiEvents = Observable.merge(searchEvents, filmEvents, characterEvents);

    /*********
     *
     */

    final ObservableTransformer<SearchUiEvent, SearchResult> searchAction =
            events -> events.flatMap(event -> new SearchAction(event.getData()).execute().subscribeOn(Schedulers.io()))
                    .map(response -> SearchResult.landedSafely(new JsonData(response.body())))
                    .onErrorReturn(throwable -> SearchResult.crashedBurned(throwable.getMessage()))
                    .startWith(SearchResult.inflight());

    final ObservableTransformer<FilmUiEvent, FilmResult> filmAction =
            events -> events.flatMap(event -> new CheckFilmAction(event.getData()).execute().subscribeOn(Schedulers.io()))
                    .map(response -> FilmResult.landedSafely(new JsonData(response.body())))
                    .onErrorReturn(throwable -> FilmResult.crashedBurned(throwable.getMessage()))
                    .startWith(FilmResult.inflight());

    final ObservableTransformer<CharacterUiEvent, CharacterResult> characterAction =
            events -> events.flatMap(event -> new CheckCharacterAction(event.getData()).execute().subscribeOn(Schedulers.io()))
                    .map(response -> CharacterResult.landedSafely(new JsonData(response.body())))
                    .onErrorReturn(throwable -> CharacterResult.crashedBurned(throwable.getMessage()))
                    .startWith(CharacterResult.inflight());

    final ObservableTransformer<UiEvent, ? extends Result> whatever = events -> events.publish(shared -> Observable.merge(
            shared.ofType(SearchUiEvent.class).compose(searchAction),
            shared.ofType(CharacterUiEvent.class).compose(characterAction),
            shared.ofType(FilmUiEvent.class).compose(filmAction)));

    /**
     *
     */
    final UiModel initialState = SearchUiModel.idle();

    final Observable<? extends Result> results = uiEvents.compose(whatever).doOnSubscribe(COMPOSITE_DISPOSABLE::add);

    final Observable<UiModel> models = results.scan(initialState, (state, result) -> {
        Log.e(TAG, "scan() state = " + state + " result = " + result);
        if (result.getLifecycle().equals(SearchResult.LIFECYCLE.DEPARTURE_LOUNGE) ||
                result.getLifecycle().equals(CharacterResult.LIFECYCLE.DEPARTURE_LOUNGE) ||
                result.getLifecycle().equals(FilmResult.LIFECYCLE.DEPARTURE_LOUNGE)) {
            return SearchUiModel.idle();
        }

        if (result.getLifecycle().equals(SearchResult.LIFECYCLE.IN_FLIGHT) ||
                result.getLifecycle().equals(CharacterResult.LIFECYCLE.IN_FLIGHT) ||
                result.getLifecycle().equals(FilmResult.LIFECYCLE.IN_FLIGHT)) {
            return SearchUiModel.inProgress();
        }

        if (result.getLifecycle().equals(SearchResult.LIFECYCLE.LANDED_SAFELY) ||
                result.getLifecycle().equals(CharacterResult.LIFECYCLE.LANDED_SAFELY) ||
                result.getLifecycle().equals(FilmResult.LIFECYCLE.LANDED_SAFELY)) {
            return SearchUiModel.success((JsonData) result.getData());
        }

        if (result.getLifecycle().equals(SearchResult.LIFECYCLE.CRASHED_BURNED) ||
                result.getLifecycle().equals(CharacterResult.LIFECYCLE.CRASHED_BURNED) ||
                result.getLifecycle().equals(FilmResult.LIFECYCLE.CRASHED_BURNED)) {
            return SearchUiModel.failure(result.getErrorMessage());
        }


        return null;

    });

    models.doOnSubscribe(COMPOSITE_DISPOSABLE::add).subscribe(model -> report(model), throwable -> error(throwable));

Как только появится моя активность, я получаю следующие журналы: -

2018-10-09 14:22:33.310 D/MainActivity: report() called with: model = [UiModel{isProgress=false, message='null', isSuccess=false, data=null}]
2018-10-09 14:22:33.311 E/MainActivity: scan() state = UiModel{isProgress=false, message='null', isSuccess=false, data=null} result = SearchResult{lifecycle=IN_FLIGHT, data=null, errorMessage='null'}
2018-10-09 14:22:33.311 D/MainActivity: report() called with: model = [UiModel{isProgress=true, message='null', isSuccess=false, data=null}]
2018-10-09 14:22:33.313 E/MainActivity: scan() state = UiModel{isProgress=true, message='null', isSuccess=false, data=null} result = CharacterResult{lifecycle=IN_FLIGHT, data=null, errorMessage='null'}
2018-10-09 14:22:33.313 D/MainActivity: report() called with: model = [UiModel{isProgress=true, message='null', isSuccess=false, data=null}]
2018-10-09 14:22:33.313 E/MainActivity: scan() state = UiModel{isProgress=true, message='null', isSuccess=false, data=null} result = FilmResult{lifecycle=IN_FLIGHT, data=null, errorMessage='null'}
2018-10-09 14:22:33.313 D/MainActivity: report() called with: model = [UiModel{isProgress=true, message='null', isSuccess=false, data=null}]

Я предполагаю, что получаю результаты IN FLIGHT из-за моих .startWith().

Когда я либо нажимаю кнопку "Поиск", либо ввожу любой текст в мои просмотры EditText, я вижу следующие журналы:

2018-10-09 14:55:19.463 E/MainActivity: scan() state = UiModel{isProgress=false, message='null', isSuccess=true, [email protected]} result = FilmResult{lifecycle=LANDED_SAFELY, [email protected], errorMessage='null'}
2018-10-09 14:55:19.463 D/MainActivity: report() called with: model = [UiModel{isProgress=false, message='null', isSuccess=true, [email protected]}]

Почему я не вижу "В ПОЛЕТЕ", а затем "ПОЛУЧАЕТ БЕЗОПАСНО"?

Я только получаю "ПОЛНЫЙ БЕЗОПАСНЫЙ"

Является ли мой подход к преобразованию между UI Event → Action → Result → UI Model в любом месте, близком к тому, что описано г-ном Дж. Уортоном?

Где я ошибся?

ОБНОВЛЕНИЕ (II)

Моя ошибка заключалась в том, чтобы не включать все мои нисходящие Rx в .flatmap().

ПОЯСНЕНИЯ

Этот шаблон UI Event ---> Действие ---> Результат ---> Модель пользовательского интерфейса по-прежнему применяется для случаев, когда нет "Backend" как такового? например, главный экран может предоставить пользователю несколько опций (кнопок) для перехода к экранам более низкого уровня в приложении. Событие пользовательского интерфейса будет "Button Click", когда модель пользовательского интерфейса вернется с соответствующим классом Activity для использования с startActivity() метода startActivity().

Как я могу объединить входные события пользовательского интерфейса экрана входа в один поток событий пользовательского интерфейса, где у меня есть два поля EditText (имя пользователя и пароль) и кнопка входа. Я бы хотел, чтобы кнопка нажала кнопку UI, чтобы ввести имя пользователя и пароль пользователя. Если я использовал RxBinding для обработки кнопок EditTexts и Login, то я не могу видеть, как я могу объединить эти три Observables в поток событий пользовательского интерфейса и иметь проверенные EditTexts, чтобы убедиться, что у них введенные данные, а затем передать этот пользователь, введенные данные на мою спину end login API (или, возможно, Google Sign In)

Ответ 1

(Я добавлял комментарий, но он был слишком длинным)

Я не могу помочь с переговорами и так далее, представленными Джейком. Но в отношении вашего последнего вопроса:

Этот шаблон UI Event ---> Действие ---> Результат ---> Модель пользовательского интерфейса по-прежнему применяется для случаев, когда нет "Backend" как такового?

Это значит, что бэкэнд - это репозиторий состояния приложения.

В такой архитектуре должно быть только одно место истины для вашего приложения: будь то бэкэнд, локальная база данных, комбинация того или другого решения, подходящего для вашего использования.


Имея это в виду, ваши потоки действий должны изменять состояние либо путем выполнения вызовов на бэкэнд, проводки изменений в базе данных или записи элементов в sharedSetting. Аналогичным образом, изменения в вашем состоянии должны инициировать отправку результатов по вашим потокам.

Конкретные детали будут зависеть от того, что вы используете в качестве источника истины для своего приложения.

Ответ 2

DataFlow и State

Он использует в основном идеи Paco и Jake Wharton RxState плюс добавил еще кое-что.

  • Чтобы использовать UiEvent → Action, Result → UiModel transformers и всегда действовать в одном состоянии с помощью операторов RxJava (формируя один поток событий, затем на основе их типов, обрабатывающих действия с разными трансформаторами, затем снова объединяют результаты, изменяя состояние и затем, наконец, отобразите его в пользовательском интерфейсе.
  • или не использовать трансформаторы и сделать его немного "более простым".

Итак, вот "полный" код модели представления без использования каких-либо трансформаторов:

class SomeViewModel(private val someRepository: SomeRepository): ViewModel() {

    val uiEvents: PublishSubject<UiEvent> = PublishSubject.create()
    val outputState: MutableLiveData<Result<UiState>> = MutableLiveData()

    init {
        uiEvents.subscribe {
            when (it) {
                    is FirstEvent -> getSomeResultsFromRepo(it.id)
                    is SecondEvent -> handleSecondEvent()
                }
        }
    }

    fun getSomeResultsFromRepo(id: String) {
        someRepository.getResult(id)
                .map { UiState(it) }
                .map { Result.success(it) }
                .startWith(Result.loading())
                .onErrorReturn { handleError(it) }
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe({
                    outputState.postValue(it)
                })
    }

    fun handleSecondEvent() {
        /* do something here */
        someRepository.getSomeOtherResult()
                .map { UiState(it) }
                .map { Result.success(it) }
                .startWith(Result.loading())
                .onErrorReturn { handleError(it) }
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe({
                    outputState.postValue(it)
                })
    }

    private fun handleError(error: Throwable): Result<UiState> {
        return if (error is RetrofitException) {
            when (error.kind) {
                RetrofitException.Kind.NETWORK -> Result.failure(NetworkError(error))
                RetrofitException.Kind.HTTP -> Result.failure(ServerError(error))
                RetrofitException.Kind.UNEXPECTED -> Result.failure(UnknownError(error))
                else -> Result.failure(UnknownError(error))
            }
        } else {
            Result.failure(UnknownError(error))
        }
    }

    class Factory @Inject constructor(private val someRepo: SomeRepository) : ViewModelProvider.Factory {
        override fun <T : ViewModel?> create(modelClass: Class<T>): T {
            @Suppress("UNCHECKED_CAST")
            return SomeViewModel(someRepo) as T
        }
    }
}

Как вы можете видеть здесь 2 потока: uiEvents (1-й поток), который получает все входные события из пользовательского интерфейса. Пока существует пользовательский интерфейс, он будет захватывать эти события. На основе типов событий он вызывает некоторые функции репозитория (варианты использования), которые возвращают некоторый ответ, а затем они обновляют модель (2-й поток) одним из возможных результатов: Успех, Ошибка или Загрузка.

Также преобразуйте ошибки API в так называемые RetrofitErrors и, основываясь на их типе, он может показывать пользователю разные сообщения об ошибках.

Существует также некоторое дублирование, которого можно легко избежать, но то, что я хотел показать здесь, заключается в том, что он всегда начинается с результата "Загрузка", затем "Успех" или "Ошибка".

Одна из самых важных вещей заключается в том, что этот способ " Сохранить состояние в потоке, которое является LiveData ".

Одно из преимуществ этой настройки (так же, как использование BehaviourSubject) заключается в том, что она всегда будет возвращать последнее изменение ориентации состояния, это очень полезно, поскольку оно просто загружает последнее доступное состояние.

Кроме того, он очень подвержен тестированию, так как каждая часть может быть протестирована при разделении с предоставлением издеваемого репо или представления, а также очень легко отлаживать, поскольку у нас всегда есть текущее состояние в потоке.