I am using a Roomdatabase and I wish to search get a single object from the database when I give it's name. For that I wrote this Query in the DAO :
#Query("SELECT * FROM kuh_table WHERE name = :kuhName ")
Kuh findKuh(String kuhName);
I call it in the repository this way :
public Kuh findKuh(String kuhName){
final Kuh[] kuh = new Kuh[1];
new Thread(new Runnable() {
volatile boolean running = true;
#Override
public void run() {
if(running!= true) {
return;
}
kuh[0] =kuhDAO.findKuh(kuhName);
running = false;
}
}).start();
return kuh[0];
}
then in my ViewModel this way :
public Kuh findKuh(String kuhName){ return repository.findKuh(kuhName);}
I then initialize my ViewModel in a fragment and try using the method by giving a String like this:
MarkerApiKt.setMarkerTapListener(mapView, (MarkerTapListener) (new MarkerTapListener() {
public void onMarkerTap(#NotNull View view, int x, int y) {
Intrinsics.checkNotNullParameter(view, "view");
if (view instanceof MapMarker) {
MarkerCallout callout = new MarkerCallout(context);
callout.setTitle(((MapMarker) view).getName());
callout.setSubTitle("position: " + ((MapMarker) view).getX() + " , " + ((MapMarker) view).getY());
Kuh kuh = kuhViewModel.findKuh(((MapMarker) view).getName());
Toast.makeText(context, "this is "+ kuh.getName(), Toast.LENGTH_SHORT).show();
but somehow the istance of my object is always null since I end up with a nullpointer exception.
Any idea what I may be doing wrong?
So, as #Olli said, the problem was that my thread in my Repository didn't finish its execution, which is why it returned a null object.
I just changed my code this way and now it works fine
public Kuh findKuh(String kuhName) throws InterruptedException {
final Kuh[] kuh = new Kuh[1];
Thread t1 = new Thread(new Runnable() {
volatile boolean running = true;
#Override
public void run() {
if(running!= true) {
return;
}
kuh[0] =kuhDAO.findKuh(kuhName);
running = false;
}
});
t1.start();
t1.join();
return kuh[0];
}
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 have two collections: Users and Books. I need to get the results of both of them whether Users OR Books is updated and then merge the results together into a LinkedHashMap to use as a listView menu.
I thought a MediatorLiveData would be the way to go, but if I put the query of Users and the Query of Books in then I get null from one of the two LiveData objects because only one or the other fires. I thought maybe if one of them fires, then perhaps I have a query run inside each addSource() in the MediatorLiveData, but I'm not sure if that's the way to go.
My post regarding the MediatorLiveData is here:
Using MediatorLiveData to merge to LiveData (Firestore) QuerySnapshot streams is producing weird results
My two queries and LiveData objects are as follows:
//getUsers query using FirebaseQueryLiveData class
private Query getUsersQuery() {
FirebaseAuth mAuth = FirebaseAuth.getInstance();
adminID = mAuth.getUid();
query = FirebaseFirestore.getInstance().collection("admins")
.document(adminID)
.collection("users")
return query;
}
private FirebaseQueryLiveData usersLiveData = new FirebaseQueryLiveData(getUsersQuery());
//getBooks query using FirebaseQueryLiveData class
private Query getBooksQuery () {
FirebaseGroupID firebaseGroupID = new FirebaseGroupID(getApplication());
groupID = firebaseGroupID.getGroupID();
query = FirebaseFirestore.getInstance().collection("books")
.whereEqualTo("groupID", groupID)
return query;
}
private FirebaseQueryLiveData booksLiveData = new FirebaseQueryLiveData(getBooksQuery());
Somehow when Users updates, I need to get the data of Books as well and then merge them, but I also need this to happen if Books updates and then get the data of Users and merge them.
Any ideas would be greatly appreciated.
Additional Note/Observation
Okay, so I'm not completely ruling out a MediatorLiveData object. Certainly it allows me the listening of two different LiveData objects within the same method, however, I don't want to merge the two of them directly because I need to act on each liveData object individually. So as an example: usersLiveData fires because we create or modify a user, then I need to query books, get the results and merge users and books etc.
Below is my MediatorLiveData as it currently stands:
//MediatorLiveData merge two LiveData QuerySnapshot streams
private MediatorLiveData<QuerySnapshot> usersBooksLiveDataMerger() {
final MediatorLiveData<QuerySnapshot> mediatorLiveData = new MediatorLiveData<>();
mediatorLiveData.addSource(usersLiveData, new Observer<QuerySnapshot>() {
#Override
public void onChanged(#Nullable QuerySnapshot querySnapshot) {
mediatorLiveData.setValue(querySnapshot);
}
});
mediatorLiveData.addSource(booksLiveData, new Observer<QuerySnapshot>() {
#Override
public void onChanged(#Nullable QuerySnapshot querySnapshot) {
mediatorLiveData.setValue(querySnapshot);
}
});
return mediatorLiveData;
}
Right now it's returning null results of the other LiveData source. Instead I need to query then merge. Any ideas on how to do this? There isn't much out there on this very thing.
I tried putting a query inside a Function that is called using a Transformations.map() but because of it be an asynchronous call, the return statement is being called before the query finishes.
Here's my attempt at the Function:
private class ListenUsersGetBooks implements Function<QuerySnapshot, LinkedHashMap<User, List<Book>>> {
#Override
public LinkedHashMap<User, List<Book>> apply(final QuerySnapshot input) {
userBookList = new LinkedHashMap<>();
getBooksQuery().get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
List<User> users = input.toObjects(User.class);
List<Book> books = task.getResult().toObjects(Book.class);
Log.d(TAG, "USERLIST! " + users);
Log.d(TAG, "BOOKLIST! " + books);
for (User user : users) {
bookList = new ArrayList<>();
for (Book book : books) {
if (user.getUserID().equals(book.getUserID())
&& book.getBookAssigned()) {
bookList.add(book);
}
else if (user.getAllBookID().equals(book.getBookID())) {
bookList.add(book);
}
}
userBookList.put(user, bookList);
}
Log.d(TAG,"OBSERVE userBookList: " + userBookList);
}
});
return userBookList;
}
}
Here's a simple version of what you could do, I hope it makes sense.
You're close with the MediatorLiveData. Instead of MediatorLiveData<QuerySnapshot> you probably want to use a custom object like this:
class MyResult {
public QuerySnapshot usersSnapshot;
public QuerySnapshot booksSnapshot;
public MyResult() {}
boolean isComplete() {
return (usersSnapshot != null && booksSnapshot != null);
}
}
Then in your observers, do something like this:
private MediatorLiveData<MyResult> usersBooksLiveDataMerger() {
final MediatorLiveData<MyResult> mediatorLiveData = new MediatorLiveData<>();
mediatorLiveData.addSource(usersLiveData, new Observer<QuerySnapshot>() {
#Override
public void onChanged(#Nullable QuerySnapshot querySnapshot) {
MyResult current = mediatorLiveData.getValue();
current.usersSnapshot = querySnapshot;
mediatorLiveData.setValue(current);
}
});
mediatorLiveData.addSource(booksLiveData, new Observer<QuerySnapshot>() {
#Override
public void onChanged(#Nullable QuerySnapshot querySnapshot) {
MyResult current = mediatorLiveData.getValue();
current.booksSnapshot = querySnapshot;
mediatorLiveData.setValue(current);
}
});
return mediatorLiveData;
}
Then when you observe the combined live data:
usersBooksLiveDataMerger().observe(new Observer<MyResult>() {
#Override
public void onChanged(#Nullable MyResult result) {
if (result == null || !result.isComplete()) {
// Ignore, this means only one of the queries has fininshed
return;
}
// If you get to here, you know all the queries are ready!
// ...
}
});
I think I solved it. We were declaring a new MyResult object in each mediatorLiveData.addSource() method. Which meant that we were getting a new object for each QuerySnapshot so we would never get them to merge with each other.
Here's the update to MediatorLiveData:
private MediatorLiveData<MyResult> usersBooksLiveDataMerger() {
final MediatorLiveData<MyResult> mediatorLiveData = new MediatorLiveData<>();
final MyResult current = new MyResult();
mediatorLiveData.addSource(usersLiveData, new Observer<QuerySnapshot>() {
#Override
public void onChanged(#Nullable QuerySnapshot querySnapshot) {
current.setUsersSnapshot(querySnapshot);
mediatorLiveData.setValue(current);
}
});
mediatorLiveData.addSource(booksLiveData, new Observer<QuerySnapshot>() {
#Override
public void onChanged(#Nullable QuerySnapshot querySnapshot) {
current.setBooksSnapshot(querySnapshot);
mediatorLiveData.setValue(current);
}
});
return mediatorLiveData;
}
Now I'm getting users and books in the observer in Activity! Now the only thing I need to do is transform (merge the data) into a LinkedHashMap, but I think I got that figured out. Thanks Sam!
So this is where I am with your suggestions Sam.
I added getter and setter methods to the MyResult class as it wasn't giving me access to the member variables in the observer otherwise:
public class MyResult {
QuerySnapshot usersSnapshot;
QuerySnapshot booksSnapshot;
//default constructor
public MyResult() {
}
public QuerySnapshot getUsersSnapshot() {
return usersSnapshot;
}
public void setUsersSnapshot(QuerySnapshot usersSnapshot) {
this.usersSnapshot = usersSnapshot;
}
public QuerySnapshot getBooksSnapshot() {
return booksSnapshot;
}
public void setBooksSnapshot(QuerySnapshot booksSnapshot) {
this.booksSnapshot = booksSnapshot;
}
public boolean isComplete() {
return (usersSnapshot != null && booksSnapshot != null);
}
}
Here's the MediatorLiveData and get method. I changed the MyResult class initialization to = new MyResult(); thinking there was an issue with using mediatorLiveData.getValue(); as the initialization and get method.
private MediatorLiveData<MyResult> usersBooksLiveDataMerger() {
final MediatorLiveData<MyResult> mediatorLiveData = new MediatorLiveData<>();
mediatorLiveData.addSource(usersLiveData, new Observer<QuerySnapshot>() {
#Override
public void onChanged(#Nullable QuerySnapshot querySnapshot) {
MyResult current = new MyResult();
current.setUsersSnapshot(querySnapshot);
mediatorLiveData.setValue(current);
}
});
mediatorLiveData.addSource(booksLiveData, new Observer<QuerySnapshot>() {
#Override
public void onChanged(#Nullable QuerySnapshot querySnapshot) {
MyResult current = new MyResult();
current.setBooksSnapshot(querySnapshot);
mediatorLiveData.setValue(current);
}
});
return mediatorLiveData;
}
public MediatorLiveData<MyResult> getUsersBooksLiveDataMerger() {
return usersBooksLiveDataMerger();
}
And finally the observer:
mainViewModel.getUsersBooksLiveDataMerger().observe(this, new Observer<MainViewModel.MyResult>() {
#Override
public void onChanged(#Nullable MainViewModel.MyResult myResult) {
if (myResult == null || !myResult.isComplete()) {
// Ignore, this means only one of the queries has fininshed
Log.d(TAG, "OBSERVE BLAH!!!!");
return;
}
// If you get to here, you know all the queries are ready!
// ...
List<Book> books;
List<User> users;
books = myResult.getBooksSnapshot().toObjects(Book.class);
users = myResult.getUsersSnapshot().toObjects(User.class);
Log.d(TAG, "OBSERVE MERGE users: " + users);
Log.d(TAG, "OBSERVE MERGE books: " + books);
}
});
Please note: I did do a null check in the mediatorLiveData, just took it out for testing purposes.
Somehow I need to trigger my books query if just my users is triggered AND I need to trigger my users query if just my books is triggered...I feel like there is a step before the MediatorLiveData that needs to happen so we can make sure one liveData triggers the other query. Does that make sense?
You can greatly simplify the usage by using my LiveDataZipExtensions https://gist.github.com/Benjiko99/d2e5406aab0a4a775ea747956ae16624
With them, you don't have to create an object to hold your combined result.
Example usage
val firstNameLD = MutableLiveData<String>().apply { value = "John" }
val lastNameLD = MutableLiveData<String>().apply { value = "Smith" }
// The map function will get called once all zipped LiveData are present
val fullNameLD = zip(firstNameLD, lastNameLD).map { (firstName, lastName) ->
"$firstName $lastName"
}
I am trying to make a method called sendRegisterUserMessage() that sends a User object to the Firebase db if the User does not already exist within the database. For some reason the method always returns false despite finding the object in the database with the listener.
Anyways I invoke this method in my MainActiviy in onCreate():
MessageSenderHandler.getInstance().sendRegisterUserMessage();
In my MessageSenderHandler class:
//Checks if user is Registered- if not it sends a RegisterUser message
public void sendRegisterUserMessage() {
System.out.println("registered: " +DatabaseManager.getInstance().isCurrentUserRegistered()); //This prints false no matter what
if (DatabaseManager.getInstance().isCurrentUserRegistered() == false) {
FirebaseDatabase.getInstance().getReference().child(MessageTypes.REGISTER_USER_MESSAGE).child(
CurrentUser.getInstance().getUserId())
.setValue(
new RegisterUserMessage()
);
}
}
Then here is my isCurrentUserRegistered() method in my DatabaseManager class
public boolean isCurrentUserRegistered() {
Query query = databaseReference.child("/" + MessageTypes.REGISTER_USER_MESSAGE + "/" + CurrentUser.getInstance().getUserId());
query.addValueEventListener(isUserRegisteredDataListener);
System.out.println("REGISTERED:" + isUserRegisteredDataListener.isUserRegisterd()); //This also prints false no matter what
return isUserRegisteredDataListener.isUserRegisterd();
}
Here is my IsUerRegisteredDataListener
public class IsUserRegisteredDataListener implements ValueEventListener {
static boolean exists;
private static IsUserRegisteredDataListener isUserRegisteredDataListener;
public IsUserRegisteredDataListener() {
isUserRegisteredDataListener = this;
}
public static IsUserRegisteredDataListener getInstance() {
if (isUserRegisteredDataListener == null) {
isUserRegisteredDataListener = new IsUserRegisteredDataListener();
}
return isUserRegisteredDataListener;
}
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
this.exists = dataSnapshot.exists();
System.out.println("EXISTS" + exists)); //This prints true
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
public boolean isUserRegisterd() {
return exists;
}
}
I don't understand why the first two methods print FALSE but the one with the actual data listener prints TRUE inside the listener
public boolean isCurrentUserRegistered() {
Query query = databaseReference.child("/" + MessageTypes.REGISTER_USER_MESSAGE + "/" + CurrentUser.getInstance().getUserId());
query.addValueEventListener(isUserRegisteredDataListener);
System.out.println("REGISTERED:" + isUserRegisteredDataListener.isUserRegisterd()); //This also prints false no matter what
return isUserRegisteredDataListener.isUserRegisterd();
}
In this method, you have to wait for firebase result to get isUserRegistered. So your IsUserRegisteredDataListener class can not work what you wish.
Can you show where you initialize isUserRegisteredDataListener field?
I want to have a Splash screen that has an inderteminate ProgressDialog and its progress gets updated by async calls from within a Presenter class (from MVP architecture).
I have a number of API calls to make to my BaaS server and for every successfull call, I would like to update the progress bar.
What's the best way to accomplish this?
I have been trying using EventBus to send notifications to my SplashActivity but it seems that all the API calls are first completed and only then the bus notifications are getting consumed and updating the UI.
What I have done so far is:
SplashActivity:
#Subscribe(threadMode = ThreadMode.MAIN)
public void onProgressBar(String event) {
Timber.d("onProgressBar");
if(event.contains("Done")) {
roundCornerProgressBar.setProgress(100);
} else {
roundCornerProgressBar.setProgress(roundCornerProgressBar.getProgress() + 10);
}
textViewTips.setText(event);
}
Presenter:
InstanceID iid = InstanceID.getInstance(ctx);
String id = iid.getId();
mDataManager.getPreferencesHelper().putInstanceId(id);
GSUtil.instance().deviceAuthentication(id, "android", mDataManager);
GSUtil.instance().getPropertySetRequest("PRTSET", mDataManager);
GSUtil:
public void deviceAuthentication(String deviceId, String deviceOS, final DataManager mDataManager) {
gs.getRequestBuilder().createDeviceAuthenticationRequest()
.setDeviceId(deviceId)
.setDeviceOS(deviceOS)
.send(new GSEventConsumer<GSResponseBuilder.AuthenticationResponse>() {
#Override
public void onEvent(GSResponseBuilder.AuthenticationResponse authenticationResponse) {
if(mDataManager != null) {
mDataManager.getPreferencesHelper().putGameSparksUserId(authenticationResponse.getUserId());
}
EventBus.getDefault().post("Reading player data");
}
});
}
public void getPropertySetRequest(String propertySetShortCode, final DataManager mDataManager) {
gs.getRequestBuilder().createGetPropertySetRequest()
.setPropertySetShortCode(propertySetShortCode)
.send(new GSEventConsumer<GSResponseBuilder.GetPropertySetResponse>() {
#Override
public void onEvent(GSResponseBuilder.GetPropertySetResponse getPropertySetResponse) {
GSData propertySet = getPropertySetResponse.getPropertySet();
GSData scriptData = getPropertySetResponse.getScriptData();
try {
JSONObject jObject = new JSONObject(propertySet.getAttribute("max_tickets").toString());
mDataManager.getPreferencesHelper().putGameDataMaxTickets(jObject.getInt("max_tickets"));
jObject = new JSONObject(propertySet.getAttribute("tickets_refresh_time").toString());
mDataManager.getPreferencesHelper().putGameDataTicketsRefreshTime(jObject.getLong("refresh_time"));
} catch (JSONException e) {
e.printStackTrace();
}
EventBus.getDefault().post("Game data ready");
EventBus.getDefault().post("Done!");
}
});
}
Right now I am just showing you 2 API calls, but I will need another 2.
Thank you
I found the answer! It's easier that I thought, which is unfortunate as I spend about 4 hours on this:
First, I created two new methods on my MVPView interface:
public interface SplashMvpView extends MvpView {
void updateProgressBarWithTips(float prog, String tip);
void gameDataLoaded();
}
Then, in the presenter itself, I call every API call and for every call, I update the View with the updateProgressBarWithTips method and when everything is completed, I finalise it so I can move from Splash screen to Main screen:
private void doGSData(String id) {
getMvpView().updateProgressBarWithTips(10, "Synced player data");
GSAndroidPlatform.gs().getRequestBuilder().createDeviceAuthenticationRequest()
.setDeviceId(id)
.setDeviceOS("android")
.send(new GSEventConsumer<GSResponseBuilder.AuthenticationResponse>() {
#Override
public void onEvent(GSResponseBuilder.AuthenticationResponse authenticationResponse) {
if(mDataManager != null) {
mDataManager.getPreferencesHelper().putGameSparksUserId(authenticationResponse.getUserId());
}
getMvpView().updateProgressBarWithTips(10, "Synced game data");
GSAndroidPlatform.gs().getRequestBuilder().createGetPropertySetRequest()
.setPropertySetShortCode("PRTSET")
.send(new GSEventConsumer<GSResponseBuilder.GetPropertySetResponse>() {
#Override
public void onEvent(GSResponseBuilder.GetPropertySetResponse getPropertySetResponse) {
GSData propertySet = getPropertySetResponse.getPropertySet();
GSData scriptData = getPropertySetResponse.getScriptData();
try {
JSONObject jObject = new JSONObject(propertySet.getAttribute("max_tickets").toString());
mDataManager.getPreferencesHelper().putGameDataMaxTickets(jObject.getInt("max_tickets"));
jObject = new JSONObject(propertySet.getAttribute("tickets_refresh_time").toString());
mDataManager.getPreferencesHelper().putGameDataTicketsRefreshTime(jObject.getLong("refresh_time"));
} catch (JSONException e) {
e.printStackTrace();
}
getMvpView().gameDataLoaded();
}
});
}
});
}
I hope this helps someone, if you're using MVP architecture.
Cheers