Trying to get all the channels from Twilio chat using the twilio SDK. Want to wait for the channel list to load(using Observables) and then display it in my UI. Below is a rough idea of what i'm trying to do:
private List<Paginator<ChannelDescriptor> getAllChannels() {
ChatClient.Properties props = new ChatClient.Properties.Builder()
.createProperties();
ChatClient chatClient = ChatClient.create(context.getApplicationContext(),
accessToken,
props,
null);
List<Paginator<ChannelDescriptor> channelList = new ArrayList<>()
chatClient.getChannels().getUserChannelsList(new CallbackListener<Paginator<ChannelDescriptor>>() {
#Override
public void onSuccess(Paginator<ChannelDescriptor> firstPaginator) {
channelList.add(firstPaginator);
Paginator<ChannelDescriptor> nextPaginator = firstPaginator;
while (nextPaginator != null && nextPaginator.hasNextPage()) {
nextPaginator = loadNextChatChannelPage(firstPaginator);
if(nextPaginator != null) {
channelList.add(nextPaginator);
}
}
}
});
return channelList;
}
public Paginator<ChannelDescriptor> loadNextChatChannelPage(Paginator<ChannelDescriptor> paginator) {
paginator.requestNextPage(new CallbackListener<Paginator<ChannelDescriptor>>() {
#Override
public void onSuccess(Paginator<ChannelDescriptor> channelDescriptorPaginator) {
return channelDescriptorPaginator;
}
#Override
public void onError(ErrorInfo errorInfo) {
return null.
}
}));
}
What i ended up doing is this:
/**
* Start loading the Twilio chat channel pages
*
* #return Single containing a list of all the chat channel pages
*/
public Single<List<Paginator<ChannelDescriptor>>> loadAllChatChannelPages(ChatClientManager chatClientManager) {
return Single.create(emitter -> chatClientManager.getChatClient().getChannels().getUserChannelsList(new CallbackListener<Paginator<ChannelDescriptor>>() {
#Override
public void onSuccess(Paginator<ChannelDescriptor> channelDescriptorPaginator) {
if(channelDescriptorPaginator != null) {
emitter.onSuccess(channelDescriptorPaginator);
}
}
#Override
public void onError(ErrorInfo errorInfo) {
String errorMessage = "";
if(errorInfo != null) {
errorMessage = errorInfo.getMessage();
}
emitter.onError(new Throwable(errorMessage));
}
})).subscribeOn(Schedulers.io())
.flatMap(firstPaginator -> {
if(firstPaginator != null) {
return loadChannelPaginator((Paginator<ChannelDescriptor>) firstPaginator).toList()
.subscribeOn(Schedulers.io());
} else {
return Single.error(new Throwable("Could not get chat channels"));
}
});
}
/**
* Recursively loads the chat channel pages and returns them as a single observable
*
* #param paginator this needs to be the first chat channel paginator from the chat client
* #return Observable containing a flattened version of all the available chat channel paginators
*/
private Observable<Paginator<ChannelDescriptor>> loadChannelPaginator(Paginator<ChannelDescriptor> paginator) {
if (paginator.hasNextPage()) {
return Observable.mergeDelayError(
Observable.just(paginator),
loadNextChatChannelPage(paginator)
.flatMap(this::loadChannelPaginator));
}
return Observable.just(paginator);
}
/**
* Loads a single chat channel page
*
* #param previousPage the previous page of chat channels
* #return Observable containing the next chat channel page
*/
private Observable<Paginator<ChannelDescriptor>> loadNextChatChannelPage(Paginator<ChannelDescriptor> previousPage) {
return Observable.create(emitter -> previousPage.requestNextPage(new CallbackListener<Paginator<ChannelDescriptor>>() {
#Override
public void onSuccess(Paginator<ChannelDescriptor> channelDescriptorPaginator) {
if(channelDescriptorPaginator != null) {
emitter.onNext(channelDescriptorPaginator);
}
emitter.onComplete();
}
#Override
public void onError(ErrorInfo errorInfo) {
if(errorInfo != null) {
String errorMessage = errorInfo.getMessage();
Timber.e(errorMessage);
}
// emitter.onError(new Throwable(errorMessage));
emitter.onComplete();
}
}));
}
In the above code loadAllChatChannelPages loads the first paginator.
If that's not null then loadChannelPaginator takes over and recursively grabs each next paginator by executing loadNextChatChannelPage, a method that returns an observable for each single paginator.
Then mergeDelayError flattens all the paginators and returns them as one single Observable.
Finally in getAllChannels i apply Observable.toList(), this return a Single containing the list of Paginated chat channels that i needed.
I have two DAOs, two Repositories and two POJOs. There is some way to create one Livedata of two? I need it to make single list for Recyclerview.
POJOs are similar objects.
ExpenseRepository:
public class ExpenseRepository {
private ExpenseDao expenseDao;
private LiveData<List<Expense>> allExpenses;
public ExpenseRepository(Application application) {
ExpenseIncomeDatabase database = ExpenseIncomeDatabase.getInstance(application);
expenseDao = database.expenseDao();
allExpenses = expenseDao.getExpensesByDay();
}
public LiveData<List<Expense>> getAllExpensesByDay() {
return allExpenses;
}
IncomeRepository:
public class IncomeRepository {
private IncomeDao incomeDao;
private LiveData<List<Income>> allIncomes;
public IncomeRepository(Application application) {
ExpenseIncomeDatabase database = ExpenseIncomeDatabase.getInstance(application);
incomeDao = database.incomeDao();
allIncomes = incomeDao.getIncomesByDay();
}
public LiveData<List<Income>> getAllIncomesByDay() {
return allIncomes;
}
ExpenseDao:
#Dao
public interface ExpenseDao {
#Query("SELECT * FROM expense_table ORDER BY day")
LiveData<List<Expense>> getExpensesByDay();
IncomeDao:
#Dao
public interface IncomeDao {
#Query("SELECT * FROM income_table ORDER BY day")
LiveData<List<Income>> getIncomesByDay();
DailyViewModel:
public class DailyFragmentViewModel extends AndroidViewModel {
private ExpenseRepository expenseRepository;
private IncomeRepository incomeRepository;
private LiveData<Pair<List<Expense>, List<Income>>> combined;
private ExpenseDao expenseDao;
private IncomeDao incomeDao;
public DailyFragmentViewModel(#NonNull Application application) {
super(application);
expenseRepository = new ExpenseRepository(application);
incomeRepository = new IncomeRepository(application);
combined = new DailyCombinedLiveData(expenseDao.getExpensesByDay(), incomeDao.getIncomesByDay());
}
public LiveData<Pair<List<Expense>, List<Income>>> getExpensesAndIncomes() {
return combined;
}
I assume you want to combine them, yes? You'll need a MediatorLiveData, but the guy saying you now need Object is wrong. What you need is a MediatorLiveData<Pair<List<Expense>, List<Income>>>.
public class CombinedLiveData extends MediatorLiveData<Pair<List<Expense>, List<Income>>> {
private List<Expense> expenses = Collections.emptyList();
private List<Income> incomes = Collections.emptyList();
public CombinedLiveData(LiveData<List<Expense>> ld1, LiveData<List<Income>> ld2) {
setValue(Pair.create(expenses, incomes));
addSource(ld1, expenses -> {
if(expenses != null) {
this.expenses = expenses;
}
setValue(Pair.create(expenses, incomes));
});
addSource(ld2, incomes -> {
if(incomes != null) {
this.incomes = incomes;
}
setValue(Pair.create(expenses, incomes));
});
}
}
You could potentially make this generic and it'd be the implementation of combineLatest for two LiveData using tuples of 2-arity (Pair).
EDIT: like this:
public class CombinedLiveData2<A, B> extends MediatorLiveData<Pair<A, B>> {
private A a;
private B b;
public CombinedLiveData2(LiveData<A> ld1, LiveData<B> ld2) {
setValue(Pair.create(a, b));
addSource(ld1, a -> {
if(a != null) {
this.a = a;
}
setValue(Pair.create(a, b));
});
addSource(ld2, b -> {
if(b != null) {
this.b = b;
}
setValue(Pair.create(a, b));
});
}
}
Beware that I lost the ability to set Collections.emptyList() as initial values of A and B with this scenario, and you WILL need to check for nulls when you access the data inside the pair.
EDIT: You can use the library https://github.com/Zhuinden/livedata-combinetuple-kt (Kotlin) or https://github.com/Zhuinden/livedata-combineutil-java/ (Java) which does the same thing.
Created this extension function in kotlin
fun <A, B> LiveData<A>.zipWith(stream: LiveData<B>): LiveData<Pair<A, B>> {
val result = MediatorLiveData<Pair<A, B>>()
result.addSource(this) { a ->
if (a != null && stream.value != null) {
result.value = Pair(a, stream.value!!)
}
}
result.addSource(stream) { b ->
if (b != null && this.value != null) {
result.value = Pair(this.value!!, b)
}
}
return result
}
Instead of having a class to add 2 live datas, another class to add 3 live datas, etc. We can use a more abstract way where we can add as many live datas as we want.
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
/**
* CombinedLiveData is a helper class to combine results from multiple LiveData sources.
* #param liveDatas Variable number of LiveData arguments.
* #param combine Function reference that will be used to combine all LiveData data.
* #param R The type of data returned after combining all LiveData data.
* Usage:
* CombinedLiveData<SomeType>(
* getLiveData1(),
* getLiveData2(),
* ... ,
* getLiveDataN()
* ) { datas: List<Any?> ->
* // Use datas[0], datas[1], ..., datas[N] to return a SomeType value
* }
*/
class CombinedLiveData<R>(vararg liveDatas: LiveData<*>,
private val combine: (datas: List<Any?>) -> R) : MediatorLiveData<R>() {
private val datas: MutableList<Any?> = MutableList(liveDatas.size) { null }
init {
for(i in liveDatas.indices){
super.addSource(liveDatas[i]) {
datas[i] = it
value = combine(datas)
}
}
}
}
I am attempting to developed an Android application based on the following talk presented by Jake Wharton
The State of Managing State with RxJava
21 March 2017 – Devoxx (San Jose, CA, USA)
Jake promised a part 2 and/or GITHUB example which I am unable to find (If indeed either exists)
At a high level I can follow/understand the majority of the above talk.
However I have the following questions.
I can see how employing UiEvent, UiModel, Action, and Result keeps concerns separated.
What I am confused about is the following:-
The diagram on slide 194 shows the "flow/stream" of Observables as
Android Device -----> Observable<UiEvent> -----> <application code> -----> Observable<Action> -----> {Backend}
{Backend} -----> Observable<Result> -----> <application code> -----> Observable<UiModel> -----> Android Device
Slide 210 contains this code snippet, showing how the Result(s) stream is "scan"ned into 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);
});
and the final code snippet on slide 215, the code snippet resembles this:-
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));
which illustrates conversion from Action(s) to Result(s)
what am I missing from this talk/slide-deck on how to combine the UiEvent/UiModel to the Action/Result stream?
The stream is driven by UiEvents
How do you complete the flow from UiEvent(s) to Action back to Result then finally UiModel?
UPDATE
Using the Star Wars API I have taken the following approach
I use my UI Events to drive the transformation between UI Events to Results via Actions, then scan the results to map back to UI Model.
Heres my classes and code:-
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);
}
}
I then set up my Rx Streams as follows from my Activity onCreate() method:-
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));
As soon as my activity displays I get the following logs:-
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}]
Im guessing I get these IN FLIGHT results due to my .startWith() statements.
When I either click my Search button or enter any text in my EditText views I see the following logs:-
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}]
Why do I not see "IN FLIGHT" then "LANDED SAFELY"?
I only get "LANDED SAFELY"
Is my approach to transforming between UI Event -> Action -> Result -> UI Model anywhere close to what is described by Mr J Wharton?
Where have I gone wrong?
UPDATE (II)
My mistake was to not include all my downstream Rx within the .flatmap() operation.
CLARIFICATION
Does this pattern of UI Event ---> Action ---> Result ---> UI Model still apply for cases where there is no "Backend" as such? e.g. a Home screen could present the user with a number of options (buttons) to navigate to lower level screens within the application. The UI Event would be "Button Click" the UI Model would return with the associated Activity class to employ with the startActivity() method call.
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)
(I was adding a comment but it was too long)
I cannot help with the talks and so on presented by Jake. But regarding your last question:
Does this pattern of UI Event ---> Action ---> Result ---> UI Model
still apply for cases where there is no "Backend" as such?
It does, it is just that the backend is your application state repository.
In this kind of architecture there should only be one place of truth for your application: be it a backend, a local database, a combination of both or whatever solution is appropriate for your usecase.
Having that in mind your Action streams should modify the state either by doing calls to the backend, posting changes to a database or writing elements in the sharedSetting. Similarly, changes in your state should trigger sending Results down your streams.
The specific details would depend on what you use as a source of truth for your application.
DataFlow and State
It uses mainly Paco and Jake Wharton RxState idea plus added some more stuff.
To use UiEvent → Action, Result → UiModel transformers and always act
on a single state with the help of RxJava operators (Forming a single
stream of events, then based on their types handling actions with
different transformers, then combine results again, modifying the
state and then finally render it on the UI.
or not to use transformers and make it a little bit “simpler”.
So here is the “full” view model code without using any transformers:
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
}
}
}
As you can see here 2 streams: a uiEvents (1st stream) which gets all the input events from the UI. As long as the UI exists it will be catching those events. Based on the event types it calls some repository functions (use cases) that are returning some response and then they are updating the model (2nd stream) with one of the possible outcomes: Success, Error or Loading.
Also transform the errors in API to so called RetrofitErrors, and based on their type it can show different error messages to the User.
There is some duplication as well that can be avoided easily, but what I wanted to show here is that it always start with a Loading result, then either a Success or an Error.
One of the most important thing is that this way "To keep state in the stream, which is a LiveData."
One benefit of this setup (just like using a BehaviourSubject) is that it will always return the last state — on orientation change it is very useful as it just loads the last available state.
Also it is highly testable as each piece can be tested in separation with providing mocked repo or view and it is also very easy to debug as we always have a current state in the stream.
I am using RxJava2.
i have some observable, and few subscribers that can be subscribed for it.
each time when new subscribers arrive, some job should be done and each of subscribers should be notified.
for this i decide to use PublishSubject. but when doOnSubscribe received from firs subscriber, myPublishSubject.hasObservers() return false...
any idea why it happens and how can i fix this?
private val myPublishSubject = PublishSubject.create<Boolean>()
fun getPublishObservable():Observable<Boolean> {
return myPublishSubject.doOnSubscribe {
//do some job when new subscriber arrived and notify all subscribers
//via
myPublishSubject.onNext(true)
}
}
Do I understand it correct, that when doOnSubscribe called it mean that there is at least one subscribers already present?
i did not find ready answer, so i create my own version of subject and call it RefreshSubject.
it based on PublishSubject but with one difference: if you would like to return observable and be notified when new subscriber arrives and ready to receive some data you should use method getSubscriberReady.
here a small example:
private RefreshSubject<Boolean> refreshSubject = RefreshSubject.create();
//ordinary publish behavior
public Observable<Boolean> getObservable(){
return refreshSubject;
}
//refreshSubject behaviour
public Observable<Boolean> getRefreshObserver(){
return refreshSubject.getSubscriberReady(new Action() {
#Override
public void run() throws Exception {
//new subscriber arrives and ready to receive some data
//so you can make some data request and all your subscribers (with new one just arrived)
//will receive new content
}
});
}
and here is full class:
public class RefreshSubject<T> extends Subject<T> {
/** The terminated indicator for the subscribers array. */
#SuppressWarnings("rawtypes")
private static final RefreshSubject.RefreshDisposable[] TERMINATED = new RefreshSubject.RefreshDisposable[0];
/** An empty subscribers array to avoid allocating it all the time. */
#SuppressWarnings("rawtypes")
private static final RefreshSubject.RefreshDisposable[] EMPTY = new RefreshSubject.RefreshDisposable[0];
/** The array of currently subscribed subscribers. */
private final AtomicReference<RefreshDisposable<T>[]> subscribers;
/** The error, write before terminating and read after checking subscribers. */
Throwable error;
/**
* Constructs a RefreshSubject.
* #param <T> the value type
* #return the new RefreshSubject
*/
#CheckReturnValue
public static <T> RefreshSubject<T> create() {
return new RefreshSubject<T>();
}
/**
* Constructs a RefreshSubject.
* #since 2.0
*/
#SuppressWarnings("unchecked")
private RefreshSubject() {
subscribers = new AtomicReference<RefreshSubject.RefreshDisposable<T>[]>(EMPTY);
}
#Override
public void subscribeActual(Observer<? super T> t) {
RefreshSubject.RefreshDisposable<T> ps = new RefreshSubject.RefreshDisposable<T>(t, RefreshSubject.this);
t.onSubscribe(ps);
if (add(ps)) {
// if cancellation happened while a successful add, the remove() didn't work
// so we need to do it again
if (ps.isDisposed()) {
remove(ps);
}
} else {
Throwable ex = error;
if (ex != null) {
t.onError(ex);
} else {
t.onComplete();
}
}
}
public Observable<T> getSubscriberReady(final Action onReady){
return Observable.create(new ObservableOnSubscribe<T>() {
#Override
public void subscribe(ObservableEmitter<T> e) throws Exception {
add(new RefreshDisposable(e, RefreshSubject.this));
onReady.run();
}
});
}
/**
* Tries to add the given subscriber to the subscribers array atomically
* or returns false if the subject has terminated.
* #param ps the subscriber to add
* #return true if successful, false if the subject has terminated
*/
private boolean add(RefreshSubject.RefreshDisposable<T> ps) {
for (;;) {
RefreshSubject.RefreshDisposable<T>[] a = subscribers.get();
if (a == TERMINATED) {
return false;
}
int n = a.length;
#SuppressWarnings("unchecked")
RefreshSubject.RefreshDisposable<T>[] b = new RefreshSubject.RefreshDisposable[n + 1];
System.arraycopy(a, 0, b, 0, n);
b[n] = ps;
if (subscribers.compareAndSet(a, b)) {
return true;
}
}
}
/**
* Atomically removes the given subscriber if it is subscribed to the subject.
* #param ps the subject to remove
*/
#SuppressWarnings("unchecked")
private void remove(RefreshSubject.RefreshDisposable<T> ps) {
for (;;) {
RefreshSubject.RefreshDisposable<T>[] a = subscribers.get();
if (a == TERMINATED || a == EMPTY) {
return;
}
int n = a.length;
int j = -1;
for (int i = 0; i < n; i++) {
if (a[i] == ps) {
j = i;
break;
}
}
if (j < 0) {
return;
}
RefreshSubject.RefreshDisposable<T>[] b;
if (n == 1) {
b = EMPTY;
} else {
b = new RefreshSubject.RefreshDisposable[n - 1];
System.arraycopy(a, 0, b, 0, j);
System.arraycopy(a, j + 1, b, j, n - j - 1);
}
if (subscribers.compareAndSet(a, b)) {
return;
}
}
}
#Override
public void onSubscribe(Disposable s) {
if (subscribers.get() == TERMINATED) {
s.dispose();
}
}
#Override
public void onNext(T t) {
if (subscribers.get() == TERMINATED) {
return;
}
if (t == null) {
onError(new NullPointerException("onNext called with null. Null values are generally not allowed in 2.x operators and sources."));
return;
}
for (RefreshSubject.RefreshDisposable<T> s : subscribers.get()) {
s.onNext(t);
}
}
#SuppressWarnings("unchecked")
#Override
public void onError(Throwable t) {
if (subscribers.get() == TERMINATED) {
RxJavaPlugins.onError(t);
return;
}
if (t == null) {
t = new NullPointerException("onError called with null. Null values are generally not allowed in 2.x operators and sources.");
}
error = t;
for (RefreshSubject.RefreshDisposable<T> s : subscribers.getAndSet(TERMINATED)) {
s.onError(t);
}
}
#SuppressWarnings("unchecked")
#Override
public void onComplete() {
if (subscribers.get() == TERMINATED) {
return;
}
for (RefreshSubject.RefreshDisposable<T> s : subscribers.getAndSet(TERMINATED)) {
s.onComplete();
}
}
#Override
public boolean hasObservers() {
return subscribers.get().length != 0;
}
#Override
public Throwable getThrowable() {
if (subscribers.get() == TERMINATED) {
return error;
}
return null;
}
#Override
public boolean hasThrowable() {
return subscribers.get() == TERMINATED && error != null;
}
#Override
public boolean hasComplete() {
return subscribers.get() == TERMINATED && error == null;
}
/**
* Wraps the actualEmitter subscriber, tracks its requests and makes cancellation
* to remove itself from the current subscribers array.
*
* #param <T> the value type
*/
private static final class RefreshDisposable<T> extends AtomicBoolean implements Disposable {
private static final long serialVersionUID = 3562861878281475070L;
/** The actualEmitter subscriber. */
final Emitter<? super T> actualEmitter;
/** The actualEmitter subscriber. */
final Observer<? super T> actualObserver;
/** The subject state. */
final RefreshSubject<T> parent;
/**
* Constructs a PublishSubscriber, wraps the actualEmitter subscriber and the state.
* #param actualEmitter the actualEmitter subscriber
* #param parent the parent RefreshProcessor
*/
RefreshDisposable(Emitter<? super T> actualEmitter, RefreshSubject<T> parent) {
this.actualEmitter = actualEmitter;
this.parent = parent;
actualObserver = null;
}
/**
* Constructs a PublishSubscriber, wraps the actualEmitter subscriber and the state.
* #param actualObserver the actualObserver subscriber
* #param parent the parent RefreshProcessor
*/
RefreshDisposable(Observer<? super T> actualObserver, RefreshSubject<T> parent) {
this.actualObserver = actualObserver;
this.parent = parent;
actualEmitter = null;
}
public void onNext(T t) {
if (!get()) {
if (actualEmitter != null)
actualEmitter.onNext(t);
if (actualObserver != null)
actualObserver.onNext(t);
}
}
public void onError(Throwable t) {
if (get()) {
RxJavaPlugins.onError(t);
} else {
if (actualEmitter != null)
actualEmitter.onError(t);
if (actualObserver != null)
actualObserver.onError(t);
}
}
public void onComplete() {
if (!get()) {
if (actualEmitter != null)
actualEmitter.onComplete();
if (actualObserver != null)
actualObserver.onComplete();
}
}
#Override
public void dispose() {
if (compareAndSet(false, true)) {
parent.remove(this);
}
}
#Override
public boolean isDisposed() {
return get();
}
}
}
Usecase: I am developing an Android app with has a viewpager with 4 tabs, all of them are Fragments. For each tab/fragment I have to connect to a REST Api with Oauth and token expiration every 5 minutes.
Current Solution: Using RxJava and retryWhen operator I can re-authenticate when receive an 401 HTTP Error. For every Observable stream subscribed and consumed and use the:
retryWhen(refreshTokenAuthenticator)
So when the token expires the stream consume it and then execute the real api call.
Problem: this only works for one observable consumed in one subscribed but I need to allow the user to switch between tabs without blocking him/her taking into account that the 401 Error could appear in any time in any fragment in any Api Call.
Question: Is there a way to make observables wait for other observables finish with onNext() which are not in the same stream/subscriber? In fact in different Fragments? So the api call scenarios will be like this:
Api Call Fragment A --> request
Api Call Fragment A <-- response 200 Code
Api Call Fragment B --> request
Api Call Fragment B <-- response 401 Code (retryWhen in action)
Api Call Fragment B --> request (refreshToken)
Api Call Fragment B <-- response 200 (with new access token saved in the app)
Almost at the same time...
Api Call Fragment C --> request
Api Call Fragment C <-- response 401 Code (retryWhen in action)
Observable in Fragment C Waits till Observable in Fragment B finish (onNext())
Api Call Fragment C --> request
Api Call Fragment C <-- response 200
This is what I already have, each API call looks almost the same:
public void getDashboardDetail() {
Subscription subscription = repository.getDashboard()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.retryWhen(tokenAuthenticator)
.subscribe(new RestHttpObserver<UserDataDto>() {
#Override
public void onUnknownError(Throwable e) {
getMvpView().onError(e);
}
#Override
public void onHostUnreachable() {
getMvpView().onHostUnreachable();
}
#Override
public void onHttpErrorCode(int errorCode, ErrorDto errorDto) {
getMvpView().onHttpErrorCode(errorCode, errorDto);
}
#Override
public void onCompleted() {
//Do nothing...
}
#Override
public void onNext(UserDataDto response) {
getMvpView().onReceiveUserData(response);
}
});
this.compositeSubscription.add(subscription);
}
And my RefreshTokenAuthenticator:
public class RefreshTokenAuthenticator implements Func1<Observable<? extends Throwable>, Observable<?>> {
private static final int RETRY_COUNT = 1;
private static final int HTTP_ERROR_CODE = 401;
#Inject
private UserRepository repository;
#Inject
private SessionManager sessionManager;
#Inject
private MyApplication application;
#Inject
private RefreshTokenAuthenticator() {
}
#Override
public synchronized Observable<?> call(Observable<? extends Throwable> observable) {
return observable
.flatMap(new Func1<Throwable, Observable<?>>() {
int retryCount = 0;
#Override
public Observable<?> call(final Throwable throwable) {
retryCount++;
if (retryCount <= RETRY_COUNT && throwable instanceof HttpException) {
int errorCode = ((HttpException) throwable).code();
if (errorCode == HTTP_ERROR_CODE) {
return repository
.refreshToken(sessionManager.getAuthToken().getRefreshToken())
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.doOnNext(tokenDto -> sessionManager.saveAuthToken(tokenDto))
.doOnError(throwable1 -> {
Log.e("RefreshTokenAuth", "DoOnError", throwable1);
application.logout();
});
}
}
// No more retries. Pass the original Retrofit error through.
return Observable.error(throwable);
}
});
}
}
1) Make source of auth tokens cache last successful result + provide method to invalidate this cached result:
class Auth {
private Observable<AuthToken> validToken;
synchronized void invalidateAuthToken() {
validToken = null;
}
synchronized Observable<AuthToken> getAuthToken() {
if (validToken == null) {
validToken = repository
.refreshToken(...) // start async request
.doOnError(e -> invalidateAuthToken())
.replay(1); // cache result
}
return validToken; // share among all subscribers
}
}
2) To access web service use the following pattern:
Observable<Data1> dataSource1 =
Observable.defer(auth.getAuthToken()) // always start from token
.flatMap(token ->
repository.fetchData1(token, ...)) // use token to call web service
.doOnError(e -> auth.invalidateAuthToken())
.retry(N); // retry N times
Finally make it work just adding a global (in my Application class) boolean if the app is currently re-authenticating or not. It actually allows two 401 HTTP errors but the second one continues in the onNext() and re execute the initial observable. I would like to do something more reactive but at least this solves my main problem.
public class RefreshTokenAuthenticator implements Func1<Observable<? extends Throwable>, Observable<?>> {
private static final int RETRY_COUNT = 1;
private static final int HTTP_ERROR_CODE = 401;
#Inject
private UserRepository repository;
#Inject
private SessionManager sessionManager;
#Inject
private MyApplication application;
#Inject
private RefreshTokenAuthenticator() {
}
#Override
public Observable<?> call(Observable<? extends Throwable> observable) {
return observable
.flatMap(new Func1<Throwable, Observable<?>>() {
int retryCount = 0;
#Override
public Observable<?> call(final Throwable throwable) {
retryCount++;
if (retryCount <= RETRY_COUNT && throwable instanceof HttpException) {
int errorCode = ((HttpException) throwable).code();
if (errorCode == HTTP_ERROR_CODE) {
Log.i("RefreshTokenAuth", "APPLICATION IS AUTHENTICATING = " + application.isAuthenticating);
if (!application.isAuthenticating) {
application.isAuthenticating = true;
String refreshToken = sessionManager.getAuthToken().getRefreshToken();
return repository
.refreshToken(refreshToken)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.doOnCompleted(() -> application.isAuthenticating = false)
.doOnNext(tokenDto -> sessionManager.saveAuthToken(tokenDto))
.doOnError(throwable1 -> {
Log.e("RefreshTokenAuth", "DoOnError", throwable1);
application.logout();
});
} else {
return Observable.just(1).doOnNext(o -> Log.i("RefreshTokenAuth", "Let's try another shot!"));
}
}
}
// No more retries. Pass the original Retrofit error through.
return Observable.error(throwable);
}
});
}
}