【问题标题】:How to Manage State with RxJava in Android using Java (Not Kotlin)如何使用 Java(不是 Kotlin)在 Android 中使用 RxJava 管理状态
【发布时间】:2018-10-18 22:02:38
【问题描述】:

我正在尝试根据 Jake Wharton 提出的以下演讲开发一个 Android 应用程序

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

Jake 承诺提供我无法找到的第 2 部分和/或 GITHUB 示例(如果确实存在)

在高层次上,我可以听懂/理解上述大部分内容。

但是我有以下问题。

我可以看到使用 UiEvent、UiModel、Action 和 Result 如何将关注点分开。

我感到困惑的是:-

幻灯片 194 上的图表显示了 Observables 的“流/流”

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

幻灯片 210 包含此代码 sn-p,显示了如何将 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 上的最终代码 sn-p,代码 sn-p 类似于:-

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 与 Action/Result 流相结合的演讲/幻灯片中,我遗漏了什么?

流由 UiEvents 驱动 你如何完成从 UiEvent(s) 到 Action 再到 Result 然后最后是 UiModel 的流程?

更新 使用 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}]

我猜我得到这些 IN FLIGHT 结果是由于我的 .startWith() 语句。

当我点击我的搜索按钮或在我的 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}]

为什么我看不到“飞行中”然后“安全着陆”?

我只会得到“安全着陆”

我在 UI Event -> Action -> Result -> UI Model 之间转换的方法是否与 J Wharton 先生所描述的相近?

我哪里出错了?

更新(二)

我的错误是没有在 .flatmap() 操作中包含我所有的下游 Rx。

澄清

这种 UI Event ---> Action ---> Result ---> UI Model 的模式是否仍然适用于没有“后端”的情况?例如主屏幕可以为用户提供许多选项(按钮)以导航到应用程序中的较低级别的屏幕。 UI 事件将是“按钮单击”,UI 模型将返回关联的 Activity 类以使用 startActivity() 方法调用。

如何将登录屏幕的 UI 输入事件合并为一个 UI 事件流,其中我有两个 EditText 字段(用户名和密码)和一个登录按钮。 我希望按钮单击 UI 事件包含输入的用户名和用户密码。如果我使用 RxBinding 处理 EditTexts 和 Login 按钮单击,我看不到如何将这三个 Observables 组合到我的 UI 事件流中并验证 EditTexts 以确保它们已输入数据,然后将此用户输入的数据传递给我结束登录 API(或者可能是 Google 登录)

【问题讨论】:

  • 嗨 Hector,你看到这个项目了吗github.com/nmwilk/soms-jw?,它似乎与 Jake 的谈话有关。
  • @Hector 你能告诉你原来的问题是什么吗?
  • @Kousic 我的问题是杰克斯的演讲不完整。一些代码示例有小错误。然而,他的演讲最大的问题是它没有解释如何从 UiEvent -> Action -> Result -> Ui Model 过渡的整个循环,并以代码 sn-ps 为后盾。从本次演讲的 YouTube 视频和其他地方(例如 Reddit)所附的 cmets 来看,许多其他开发人员也有类似的看法。
  • 对于 我如何将登录屏幕的 UI 输入事件合并到一个 UI 事件流中,其中我有两个 EditText 字段(用户名和密码)和一个登录按钮。我希望按钮单击 UI 事件包含输入的用户名和用户密码...试试这个:Retrofit, a Simple HTTP Client for Android and Java。我希望这会有所帮助!

标签: android rx-java2


【解决方案1】:

(我正在添加评论,但太长了)

我对 Jake 提出的谈话等内容无能为力。但是关于你的最后一个问题:

这种模式的 UI Event ---> Action ---> Result ---> UI Model 仍然适用于没有“后端”的情况?

确实如此,只是 backend 是您的应用程序状态 repository

在这种架构中,您的应用程序应该只有一个真实的地方:无论是后端、本地数据库、两者的组合,还是适合您的用例的任何解决方案。


考虑到这一点,您的 Action 流 应该通过调用后端、将更改发布到数据库或在 sharedSetting 中写入元素来修改状态。同样,您的状态更改应该触发将结果发送到您的流中。

具体细节取决于您使用什么作为应用程序的真实来源。

【讨论】:

  • 我使用 Realm 本地数据库作为我的事实来源,但是我已经用一些我认为需要使用“虚拟”动作/结果对的纯 UI 场景更新了我的问题
【解决方案2】:

数据流和状态

它主要使用了 Paco 和 Jake Wharton 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 个流:一个 uiEvents(第一个流),它从 UI 获取所有输入事件。只要 UI 存在,它就会捕获这些事件。根据事件类型,它调用一些返回一些响应的存储库函数(用例),然后它们使用可能的结果之一更新模型(第二个流):成功、错误或加载。

还将 API 中的错误转换为所谓的 RetrofitErrors,并根据它们的类型向用户显示不同的错误消息。

也有一些重复可以很容易地避免,但我想在这里展示的是它总是以加载结果开始,然后是成功或错误。

其中最重要的一点是,这种方式“将状态保持在流中,这是一个 LiveData。

这种设置的一个好处(就像使用 BehaviourSubject 一样)是它总是返回最后一个状态 — 在方向改变时它非常有用,因为它只是加载最后一个可用状态。

此外,它还具有高度的可测试性,因为每个部分都可以通过提供模拟的 repo 或视图来单独测试,而且它也很容易调试,因为我们始终在流中拥有当前状态。

【讨论】:

    猜你喜欢
    • 2019-02-27
    • 1970-01-01
    • 2021-07-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-01-17
    • 2020-07-29
    • 1970-01-01
    相关资源
    最近更新 更多