web-dev-qa-db-ja.com

Android using Java(Kotlinではない)でRxJavaを使用して状態を管理する方法

JakeWhartonによる次の講演に基づいてAndroidアプリケーションを開発しようとしています

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

ジェイクは、私が見つけることができないパート2やGITHUBの例を約束しました(実際にどちらかが存在する場合)

大まかに言えば、私は上記の話の大部分をフォロー/理解することができます。

しかし、私は次の質問があります。

UiEvent、UiModel、Action、およびResultを使用することで、懸念がどのように分離されているかがわかります。

私が混乱しているのは次のとおりです:-

スライド194の図は、オブザーバブルの「フロー/ストリーム」を次のように示しています。

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

スライド210には、このコードスニペットが含まれており、Result(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));
_

これは、アクションから結果への変換を示しています

uiEvent/UiModelをアクション/結果ストリームに組み合わせる方法について、このトーク/スライドデッキから何が欠けていますか?

ストリームはUiEventsによって駆動されます。UiEvent(s)からAction、Result、そして最後にUiModelへのフローをどのように完了しますか?

[〜#〜] update [〜#〜] Star Wars APIを使用して次のアプローチを採用しましたUIイベントを使用して、アクションを介してUIイベントから結果への変換を駆動し、結果をスキャンしますUIモデルにマップし直します。

これが私のクラスとコードです:-

_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);
    }
}
_

次に、Activity onCreate()メソッドから次のようにRxストリームを設定します:-

_   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}]
_

.startWith()ステートメントが原因で、これらの_IN FLIGHT_の結果が得られると思います。

[検索]ボタンをクリックするか、EditTextビューにテキストを入力すると、次のログが表示されます。-

_2018-10-09 14:55:19.463 E/MainActivity: scan() state = UiModel{isProgress=false, message='null', isSuccess=true, data=com.test.model.JsonData@5e0b6f1} result = FilmResult{lifecycle=LANDED_SAFELY, data=com.test.model.JsonData@8ae4d86, errorMessage='null'}
2018-10-09 14:55:19.463 D/MainActivity: report() called with: model = [UiModel{isProgress=false, message='null', isSuccess=true, data=com.test.model.JsonData@8ae4d86}]
_

「機内」、「着陸安全」の順に表示されないのはなぜですか?

「LANDEDSAFELY」しか出ない

UIイベント->アクション->結果-> UIモデルの間で変換する私のアプローチは、Jウォートン氏が説明したものに近いところにありますか?

どこが間違っているのですか?

PDATE(II)

私の間違いは、すべてのダウンストリームRxを.flatmap()操作に含めなかったことです。

[〜#〜]説明[〜#〜]

このUIイベントのパターン--->アクション--->結果---> UIモデルは、「バックエンド」自体がない場合でも適用されますか?例えばホーム画面には、アプリケーション内の下位レベルの画面に移動するためのいくつかのオプション(ボタン)が表示される場合があります。 UIイベントは「ボタンクリック」であり、UIモデルは関連するActivityクラスとともに返され、startActivity()メソッド呼び出しで使用されます。

ログイン画面のUI入力イベントを、2つのEditTextフィールド(ユーザー名とパスワード)とログインボタンがある単一のUIイベントストリームに統合するにはどうすればよいですか。ボタンクリックUIイベントに、入力したユーザー名とユーザーパスワードを含める必要があります。 RxBindingを使用してEditTextsを処理し、[ログイン]ボタンをクリックした場合、これら3つのObservableをUIイベントストリームに結合し、EditTextsを検証してデータが入力されていることを確認してから、このユーザーが入力したデータを背中に渡す方法がわかりません。ログインAPIを終了します(または、たとえばGoogleサインイン)

14
Hector

(コメントを追加していましたが、長すぎました)

ジェイクの講演などは仕方がない。しかし、あなたの最後の質問に関して:

このUIイベントのパターン--->アクション--->結果---> UIモデルは、「バックエンド」自体がない場合でも適用されますか?

バックエンドがアプリケーションの状態リポジトリであるというだけです。

この種のアーキテクチャでは、アプリケーションの真実は1つだけである必要があります。それは、バックエンド、ローカルデータベース、両方の組み合わせ、またはユースケースに適したソリューションです。


これを念頭に置いて、アクションストリームは、バックエンドへの呼び出しを行うか、データベースに変更を投稿するか、sharedSettingに要素を書き込むことによって状態を変更する必要があります。同様に、状態が変化すると、結果がストリームに送信されるようになります。

具体的な詳細は、アプリケーションの真実のソースとして何を使用するかによって異なります。

2
Eric Martori

データフローと状態

主にPacoとJakeWharton RxStateのアイデアを使用し、さらにいくつかのものを追加しました。

  • UiEvent→Action、Result→UiModelトランスフォーマーを使用し、RxJavaオペレーターの助けを借りて常に単一の状態で動作するには(イベントの単一のストリームを形成し、異なるトランスフォーマーでアクションを処理するタイプに基づいて、結果を再度結合し、状態を変更しますそして最後にそれをUIにレンダリングします。
  • または、トランスフォーマーを使用せず、少し「シンプル」にします。

したがって、トランスフォーマーを使用しない「フル」ビューモデルコードは次のとおりです。

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つのストリーム:UIからすべての入力イベントを取得するuiEvents(最初のストリーム)。 UIが存在する限り、それらのイベントをキャッチします。イベントタイプに基づいて、応答を返すいくつかのリポジトリ関数(ユースケース)を呼び出し、モデル(2番目のストリーム)を更新して、成功、エラー、または読み込みのいずれかを実行します。

また、APIのエラーをいわゆるRetrofitErrorsに変換すると、そのタイプに基づいて、ユーザーにさまざまなエラーメッセージを表示できます。

簡単に回避できる重複もいくつかありますが、ここで示したかったのは、常に読み込み結果から始まり、次に成功またはエラーのいずれかであるということです。

最も重要なことの1つは、この方法です "LiveDataであるストリームの状態を維持するため。"

この設定の利点の1つは(BehaviourSubjectを使用するのと同じように)、常に最後の状態を返すことです。向きを変更すると、使用可能な最後の状態をロードするだけなので、非常に便利です。

また、モックされたリポジトリまたはビューを提供して各ピースを個別にテストできるため、非常にテスト可能です。また、ストリームには常に現在の状態があるため、デバッグも非常に簡単です。

1
Kousic