如何在Android中使用Java(不是Kotlin)在RxJava中管理状态

时间:2018-10-08 15:08:02

标签: android rx-java2

我正在尝试根据杰克·沃顿(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上的图表将Observable的“流/流”显示为

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

幻灯片210包含此代码段,显示了如何将“结果”流“扫描”到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到操作再到Result再到UiModel的流程?

更新 使用《星球大战》 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);
    }
}

然后,我通过“活动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事件->动作->结果-> UI模型之间转换的方式接近J沃顿先生所描述的吗?

我哪里出错了?

更新(II)

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

澄清

这种UI事件模式--->操作--->结果---> UI模型是否仍然适用于没有“后端”的情况?例如主页屏幕可以向用户显示许多选项(按钮),以导航到应用程序中的下级屏幕。 UI事件将是“按钮单击”,UI模型将随相关的Activity类一起返回,以与startActivity()方法调用一起使用。

如何将登录屏幕的UI输入事件合并为一个UI事件流,其中有两个EditText字段(用户名和密码)和一个登录按钮。 我希望按钮click UI事件包含输入的用户名和用户密码。如果我使用RxBinding处理EditTexts和Login按钮,则无法看到如何将这三个Observable组合到UI事件流中,并验证EditTexts以确保输入了数据,然后将该用户输入的数据传递给我结束登录API(例如Google登录)

2 个答案:

答案 0 :(得分:2)

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

我不由杰克(Jake)主持的谈话等。但是关于您的最后一个问题:

  

是否执行此模式的UI事件--->操作--->结果---> UI模型   仍然适用于没有此类“后端”的情况?

确实如此,只是后端是您的应用程序状态存储库

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


请记住,您的动作流应该通过调用后端,将更改发布到数据库或在sharedSetting中写入元素来修改状态。同样,您的状态更改应触发将结果发送到您的流中。

具体细节取决于您用作应用程序真相的来源。

答案 1 :(得分:1)

数据流和状态

它主要使用Paco和Jake Wharton RxState的想法,并添加了更多内容。

  • 要使用UiEvent→操作,结果→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获取所有输入事件。只要用户界面存在,它将捕获这些事件。根据事件类型,它调用一些存储库函数(用例),这些函数返回一些响应,然后使用可能的结果之一(成功,错误或加载)更新模型(第二个流)。

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

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

最重要的事情之一就是这种方式“ 将状态保留在流中,即LiveData。

此设置(就像使用BehaviourSubject一样)的一个好处是,它将始终返回最后一个状态-在方向更改时,它非常有用,因为它仅加载了最后一个可用状态。

它也是高度可测试的,因为可以通过提供模拟回购或视图来单独测试每件作品,并且由于流中始终具有当前状态,因此调试起来也非常容易。