我试图根据杰克沃顿提出的以下谈话开发一个Android应用程序
The State of Managing State with RxJava 21 March 2017 – Devoxx (San Jose, CA, USA)
杰克答应了我无法找到的第2部分和/或GITHUB示例(如果确实存在)
在高层次上,我可以关注/理解上述谈话的大部分内容.
但是我有以下问题.
我可以看到如何使用UiEvent,UiModel,Action和Result来保持关注点.
令我困惑的是以下几点: -
幻灯片194上的图表显示了Observables的"流/流"
Android Device -----> Observable-----> -----> Observable -----> {Backend} {Backend} -----> Observable -----> -----> Observable -----> Android Device
幻灯片210包含此代码片段,显示结果流如何"扫描"到UiModel中
SubmitUiModel initialState = SubmitUiModel.idle(); Observableresults = /* ... */; Observable 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上的最终代码片段,代码片段类似于: -
ObservableTransformersubmit = 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 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到Action再到Result然后最终UiModel的流程?
更新 使用星球大战API我采取了以下方法,我使用我的UI事件来驱动UI事件到结果之间通过动作的转换,然后扫描结果以映射回UI模型.
继承我的课程和代码: -
ACTION CLASSES ============== public abstract class Action{ Api service = Service.instance(); final T data; public Action(final T data) { this.data = data; } public T getData() { return data; } public abstract Observable > execute(); } public class CheckCharacterAction extends Action { public CheckCharacterAction(final String characterName) { super(characterName); } @Override public Observable > execute() { return service.peopleSearch(getData()); } } public class CheckFilmAction extends Action { public CheckFilmAction(final String filmTitle) { super(filmTitle); } @Override public Observable > execute() { return service.filmSearch(getData()); } } public class SearchAction extends Action { public SearchAction(final String search) { super(search); } @Override public Observable > execute() { return service.filmSearch(getData()); } } EVENT CLASSES ============= public abstract class UiEvent { private final T data; public UiEvent(final T data) { this.data = data; } public T getData() { return data; } } public class CharacterUiEvent extends UiEvent { public CharacterUiEvent(final String name) { super(name); } } public class FilmUiEvent extends UiEvent { public FilmUiEvent(final String title) { super(title); } } public class SearchUiEvent extends UiEvent { public SearchUiEvent(final String data) { super(data); } } UI MODEL CLASSES ================ public class UiModel { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 Streams如下: -
final ObservablesearchEvents = RxView.clicks(activityMainBinding.searchButton) .observeOn(AndroidSchedulers.mainThread()) .subscribeOn(AndroidSchedulers.mainThread()) .map(ignored -> new SearchUiEvent(activityMainBinding.filmTitle.getText().toString())); final Observable 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 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 uiEvents = Observable.merge(searchEvents, filmEvents, characterEvents); /********* * */ final ObservableTransformer 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 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 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 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 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沃顿先生描述的内容吗?
我哪里出错了?
更新(二)
我的错误是不在操作中包含我的所有下游Rx .flatmap()
.
澄清
这种UI事件模式--->动作--->结果---> UI模型是否仍然适用于没有"后端"的情况?例如,主屏幕可以向用户呈现许多选项(按钮)以导航到应用程序内的较低级别屏幕.UI事件将是"按钮单击",UI模型将返回关联的Activity类以与startActivity()
方法调用一起使用.
How can I amalgamate the UI input events of a login screen into a single stream of UI events where I have two EditText fields (User Name and Password) and a Login Button. I would want the button click UI event to contain the user name and user password entered. If I was using RxBinding to process the EditTexts and the Login button click I cannot see how I can combine these three Observables into my UI event stream and have the EditTexts validated to ensure they have data entered and then pass this user entered data to my back end login API (or maybe Google Sign In for example)