Wait for async call to finish execution with Android LiveData - android

I'm developing an Android app using the MVVM architecture. This is the function that returns the MutableLiveData from my repository, to be observed from the ViewModel.
public MutableLiveData<ArrayList<CoffeeShop>> getCoffeeShops(){
setCoffeeShops();
MutableLiveData<ArrayList<CoffeeShop>> data = new MutableLiveData<>();
data.setValue(coffeeShops);
return data;
}
public void setCoffeeShops() {
CollectionReference coffeeShopsRef = database.collection("coffee_shops");
coffeeShopsRef.get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>(){
#Override
public void onComplete(#NonNull Task< QuerySnapshot > task) {
if (task.isSuccessful()) {
for (QueryDocumentSnapshot document : task.getResult()) {
CoffeeShop coffeeShop = new CoffeeShop();
coffeeShop.setName((String) document.get("name"));
coffeeShop.setLocation((GeoPoint) document.get("location"));
coffeeShop.setOffers((ArrayList<String>) document.get("offers"));
coffeeShop.setRating((Long) document.get("rating"));
coffeeShop.setReviews((ArrayList<Review>) document.get("reviews"));
coffeeShops.add(coffeeShop);
}
}
}
});
}
The setCoffeeShops() function performs an async call to the database and sets the value for the coffeeShops variable. I need to find a way to wait for the async call to be resolved, and only then assign the value to the MutableLiveData object. Is this the right way to approach the issue, and if yes, what would you recommend?
Thank you!

Try this:
Take a callback param to setCoffeeShops()
eg: setCoffeeShops(Runnable callback)
And when your set coffee shops is complete, invoke the callback:
callback.run()
And when you call setCoffeeShops, do it like this:
MutableLiveData<ArrayList<CoffeeShop>> data = new MutableLiveData<>();
setCoffeeShops(()->{
data.setValue(coffeeShops);
});
return data;
OR to keep your code a bit cleaner use an interface instead of keeping coffeeShops as a global variable like this:
interface CoffeeShopLoader {
void shopsLoaded(List<CoffeeShop> shops);
}
void setCoffeeShops(CoffeeShopLoader loader) {
...
loader.shopsLoaded(coffeeShops);
}
... // and call it like this
setCoffeeShops((shops)->{
data.setValue(shops)
});
...

I want to recommend Room database framework. It supports Livedata and also has great ORMapper. So by simple object model you can reach the goal and reduces code to reach the goal.
Following link shows you great way to do so:
https://codelabs.developers.google.com/codelabs/android-training-livedata-viewmodel/#6
However, if you don't have any plan to migrate to Room, simply change your like as follow:
public MutableLiveData<ArrayList<CoffeeShop>> getCoffeeShops(){
MutableLiveData<ArrayList<CoffeeShop>> data = new MutableLiveData<>();
setCoffeeShops(data);
return data;
}
public void setCoffeeShops(MutableLiveData<ArrayList<CoffeeShop>> data) {
CollectionReference coffeeShopsRef = database.collection("coffee_shops");
coffeeShopsRef.get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>(){
#Override
public void onComplete(#NonNull Task< QuerySnapshot > task) {
if (task.isSuccessful()) {
for (QueryDocumentSnapshot document : task.getResult()) {
CoffeeShop coffeeShop = new CoffeeShop();
/* data conversion code*/
coffeeShops.add(coffeeShop);
}
//If runs on main thread
data.setValue(coffeeShops)
//Otherwise
data.postValue(coffeeShops)
//Use one of top lines base on the situation
}
}
});
}
Now you do not need to wait for data be gathered, the changes will be projected as soon as data is ready in listener method.

Related

How to display data in Fragment and Activity (both independent) from same Snapshot Listener (Firebase)

Would like to have your help on my weird problem that currently I am facing. I tried for couple of days but no luck and finally decided to post here to take help.
I created a Snapshot Listener attached to a Collection in Firebase defined as follows :-
public class FirebaseTypingStatusLiveData extends LiveData<List<documentSnapshot>> {
// Logging constant
private static final String TAG = "FirebaseQueryLiveData";
// Document Reference
private final DocumentReference documentReference;
// Listener
private final MyDocumentListener listener = new MyDocumentListener();
// Handler
private final Handler handler = new Handler();
private ListenerRegistration listenerRegistration;
// Flag to remove listener
private boolean listenerRemovePending = false;
private MutableLiveData <List<documentSnapshot> mutableLiveData = new MutableLiveData<>();
// Constructor
public FirebaseTypingStatusLiveData(DocumentReference documentReference) {
this.documentReference = documentReference;
}
public LiveData<List<documentSnapshot>> checknow(){
// Add listener
if (!Listeners.LIVESAMPLE.containsKey(documentReference)) {
listenerRegistration = documentReference.addSnapshotListener(listener);
Listeners.LIVESAMPLE.put(documentReference, listenerRegistration);
} else {
listenerRegistration = Listeners.LIVETYPINGSTATUSSAMPLE.get(documentReference);
}
return mutableLiveData;
}
// Listener definition
private class MyDocumentListener implements EventListener<DocumentSnapshot> {
#Override
public void onEvent(#Nullable DocumentSnapshot documentSnapshot, #Nullable
FirebaseFirestoreException e) {
Log.d(TAG, "onEvent");
// Check for error
if (e != null) {
// Log
Log.d(TAG, "Can't listen to query snapshots: " + documentSnapshot
+ ":::" + e.getMessage());
return;
}
setValue(documentSnapshot);
mutableLiveData.setValue(documentSnapshot);
}
}
}
}
The snapshot reads the data perfectly and advised as and when data is available.
The snapshot data is getting displayed 1. in Fragment (not part of Activity that i am talking about) 2. Activity through two view models that have the same code as follows :
#NonNull
public LiveData<List<documentSnapshot>> getDataSnapshotLiveData() {
Firestore_dB db = new Firestore_dB();
DocumentReference docref = db.get_document_firestore("Sample/"+docID);
FirebaseTypingStatusLiveData firebaseTypingStatusLiveData = new
FirebaseTypingStatusLiveData(docref);
return firebaseTypingStatusLiveData.checknow();
}
The Fragment & Activity code is also same except changing owner which are as follows :-
LiveData<List<documentSnapshot>> liveData = viewmodel.getDataSnapshotLiveData();
liveData.observe(this, new Observer<List<documentSnapshot>>() {
#Override
public void onChanged(DocumentReference docreef) {
String name = docreef.get("name");
stringname.setText(name); // The text is displaying either in Fragment or in Activity but not in both.
});
My problem is i need data in both i.e. Fragment & Activity whereas I am getting data either in Fragment or in Activity depending upon the code which I commented.
Kindly advise where I am making mistake. Thanks in Advance
Honestly, I am not sure that my answer wouldn't lead you away to the false way, but you can try.
My guess is that your problem could be somehow connected with ViewModel sharing.
There is a well-known task How to share Viewmodel between fragments.
But in your case, that can't help, because you have to share ViewModel between activities (now you have two separate ViewModels and that could be problem with Firestore EventListeners).
Technically you can share ViewModel between activities (I haven't try since usually I use Single activity pattern). For that as a owner parameter in ViewModelProvider constructor you can set instance of your custom Application class (but you have implement interface ViewModelStoreOwner for it). After that both in your activity and in your fragment you can get the same ViewModel with the Application class-instance:
val sharedViewModel = ViewModelProvider(mainApplication, viewModelFactory).get(SharedViewModel::class.java)
I made LiveData static that listens to changes in source data and provide updated content were ever required in different Activity.

Android studio Firebase query - retrieve value from Callback function and assign it to a variable

I am a beginner so apologies for a possible silly question.
I am trying to retrieve data from a Firebase database. This works but I cannot assign the value to a string variable for use later on.
This is the asynchronous call to the database which returns the right result. (The data its querying from is static so I don't really need an asynchronous call but as far as I am aware, I don't have another option).
public static void getAnimalDetails(String strColl, final String strQueryField, final String strQueryValue,
final MyCallBack myCallback){
mfirebaseDb.collection(strColl)
.whereEqualTo(strQueryField, strQueryValue)
.get()
.addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
if(task.isSuccessful()) {
for (QueryDocumentSnapshot document : task.getResult()) {
String strResult = document.get("animal_class").toString();
Log.d(TAG, "SSSS:" + strResult );
myCallback.onCallback(strResult);
}
}
}
});
}
This is the callback function passed to the method above.
public interface MyCallBack {
void onCallback(String strValFromAsyncTask);
}
Then this is where I call the asynch task and try and access the data from the callback.
This method fires on a button click
I can see via the log that the right value is populated in strAnimalClass (which is a global variable)
But when I try to use strAnimalClass outside of the call back it is null.
getAnimalDetails("animals", "animal_common_name", animal, new MyCallBack() {
#Override
public void onCallback(String strValFromAsyncTask) {
strAnimalClass = strValFromAsyncTask;
Log.d(TAG, "mmmmm:" + strAnimalClass );
}
});
Can anyone help with how to get a value like this out of the async / callback environment for use later on?
Thank you
You can't use the value outside of the callback. Or more specifically, you can use the value outside of the callback, but not before the callback has been called. This same rule applies to Firebase's onComplete and to your custom onCallback method.
You can verify that with a few log lines:
Log.d(TAG, "Before calling getAnimalDetails")
getAnimalDetails("animals", "animal_common_name", animal, new MyCallBack() {
#Override
public void onCallback(String strValFromAsyncTask) {
Log.d(TAG, "Inside onCallback");
}
});
Log.d(TAG, "After calling getAnimalDetails")
When you run this code, it logs:
Before calling getAnimalDetails
After calling getAnimalDetails
Inside getAnimalDetails
So while you can access strAnimalClass after the code that calls getAnimalDetails, it won't have the expected value yet, because onCallback wasn't called yet.
For this reason any code that needs the value(s) from the database will need to be inside onComplete or inside onCallback, or be called from within there.
Also see:
How to check a certain data already exists in firestore or not
getContactsFromFirebase() method return an empty list
Setting Singleton property value in Firebase Listener

Observed LiveData doesn't really update after first fetch

I'm building an offline-first app with the database setup as the single source of truth. I am using Room to simplify the database handling, and LiveData to simplify observable data patterns.
I am also using Retrofit to make any network calls required to populate the database with new data.
I have set up an observer in my Fragment as follows:
private void setUpObserver() {
tfViewModel = ViewModelProviders.of(getActivity()).get(TFViewModel.class);
tfViewModel.getAllPosts().observe(getActivity(),
newPosts -> {
if (newPosts != null && newPosts.size() > 0) {
lottieAnimationView.setVisibility(View.INVISIBLE);
mPostsAdapter.updateItems(newPosts);
}
});
tfViewModel.fetchNextData(currentPage);
}
When my app first starts, I'm deleberately truncating each table in my database using Room callbacks so that new data is fetched every time. (For testing. This beats the offline-first experience and must not be done in production.)
Anyway, so when it first starts, it calls the fetchNextData method of the viewmodel which in turn asks the Repository to fetch the data.
Here's my ViewModel:
public class TFViewModel extends AndroidViewModel {
private TFRepository mRepository;
private LiveData<List<Post>> mPostList;
public TFViewModel(Application application) {
super(application);
mRepository = new TFRepository(application);
mPostList = mRepository.getAllPosts();
}
public LiveData<List<Post>> getAllPosts() {
return mPostList;
}
public void fetchNextData(int page) {
mRepository.fetchNextPosts(page);
}
}
In the repository, I use my DAOs to insert posts into the database. To fetch new data, I use a Service Class to fetch new data for me. When the fetch call returns, I use an AsyncTask to insert the new posts to my database. (Details omitted for brevity):
public class TFRepository {
private PostDao postDao;
private LiveData<List<Post>> postList;
private RetrofitSingleton retrofitSingleton;
public TFRepository(Application application) {
TFRoomDatabase db = TFRoomDatabase.getDatabase(application);
postDao = db.postDao();
retrofitSingleton = RetrofitSingleton.getInstance(application.getApplicationContext());
postList = postDao.getAllPosts();
}
public LiveData<List<Post>> getAllPosts() {
return postList;
}
public void fetchNextPosts(int page) {
getPostList(page);
}
private void getPostList(int page) {
APICaller.getInstance(retrofitSingleton).getFeed(page,
new NetworkResponseListener<BaseResponse<FeedResponse>>() {
#Override
public void onResponseReceived(BaseResponse<FeedResponse> feedResponseBaseResponse) {
if (feedResponseBaseResponse == null) return;
List<Post> posts = feedResponseBaseResponse.getData().getPosts();
new insertAllPostsAsyncTask(postDao).execute(posts);
}
#Override
public void onError(String errorMessage) {
}
});
}
}
The OBSERVER I had setup in my fragment gets an empty list the first time around. The API call returns with the first page of posts and it receives 10 posts the second time. The view is popualted. Everything is good.
Problem: As the user scrolls down, the Fragment asks the ViewModel to fetch more data. The ViewModel asks the Repository to fetch new data. The Retrofit call goes and comes back with the new data. It is inserted in the database. BUT THE OBSERVER IS NOT NOTIFIED. What am I missing?
NOTE: I do not want to use a MutableLiveData as I want to maintain the DB as the single source of truth. Also, as the docs state that LiveData is notified whenever the underlying DB changes, my implementation should work with LiveData.

How to pass data from server/manager to viewmodel RxJava Android

I am very new to RxJava and I'm working on an Android app with it. I am making a network request and I'd like my Fragment to update the UI based on the network returned data, and I'm looking for a good 'rx' way to do this. Basically I have my Fragment immediately firing to my viewmodel that it should make the server call. I need to make the server call and notify/send that data to the viewModel so that I can update it to the fragment. Normally (without rx), I'd just pass all of this data with variables, but how can I achieve this data flow using rx and observables?
Thanks everyone.
Create a separate Repository layer, access it only from your viewModels.
In this way you will have view/databinding triggering requests.
Next, have some State management inside Repository or store some data there(use LiveData)
In your ViewModel assign value to the ref of LiveData from repository. So anytime you update it inside Repository - viewModel will have the same object.
Finally, you can observe that viewModel's LiveData.
val someData = MutableLiveData<SomeObject>() - this one inside repository, now you will be able to save any network call result inside repository.
Have your ViewModel contain next one: val someData= Repository.instance.someData
And from fragment/activity use : viewModel.someData.observe(this, {...})
Going to show simple example with code. Another way of doing this using concept single source of truth (SSOT).
Activity-->ViewModel--->Repository--->Insert On Room DB
Step 01: Get all data from room database with Live Data query. And set adapter.
Step 02: Call from Activity/Fragment to remote database/repository to get data.
Step 03: After getting data from remote repository insert it to room database.
Step 04: You have already observing data with Live Query on step 01 so as soon as you
insert data on room database your live observe query will fire again and update
your listview.
Now following example is not complete. But to get a rough idea.
To call & update List using LiveData.
Activity/ Fragment:
RouteViewModel mViewModel = ViewModelProviders.of(this).get(RouteViewModel.class);
mViewModel.getAllRoutes().observe(this, new Observer<List<Route>>() {
#Override
public void onChanged(#Nullable final List<Route> items) {
// will call automatic as soon as room database update
adapter.setItems(items);
}
});
//init/write a remote call here (like you called on room database)
--View Model
public LiveData<List<Route>> getAllRoutes()
{
//call here reposatory
return mAllRoutes;
}
//also write another method here to call repo to init a remote call
---Repository
public LiveData<List<Route>> getRoutes() {
//call on Dao
return mRouteDao.getRoutes();
}
//init a remote call
public Observable<Route> getRoutesFromNetwork(int routeID) {
return new NetworkService().GetChannel().subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<String>() {
#Override
public void onSubscribe(Disposable d) {
}
#Override
public void onNext(String result) {
List<Route> items = new Gson().fromJson(result, new TypeToken<List<Route>>() {
}.getType());
Completable.fromRunnable(() -> {
//insert routes
//if routes is Live data it will update ui automatic
}).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new CompletableObserver() {
#Override
public void onSubscribe(Disposable d) {
}
#Override
public void onComplete() {
Log.v("Completed", "DONE");
Toasty.info(context,"DONE", Toast.LENGTH_SHORT,true).show();
}
#Override
public void onError(Throwable e) {
Log.v("Error", "Error");
}
});
}
#Override
public void onError(Throwable e) {
}
#Override
public void onComplete() {
}
});
}

What's the correct way to deal with multithreading and Realm?

On my Android application I have a data access layer where I have the following property on each data store class
Realm realm = Realm.getDefaultInstance();
My problema is when I try to call any data store method from a different thread. Then I get
Realm objects can only be accessed on the thread they were created.
I've read that I should be creating a new Realm instance on the new thread. Problem is that data access layer is no thread aware, it doesn't know if it's called from main or separated and it seems to me it would smell adding logic to check that.
I was checking different questions here and issues filled on github but it's not clear for me what's the oficial way to handle a situation like this. I don't think I'm dealing with a strange scenario...
EDIT
My arquitecture is Fragment containing a Presenter whic contains a DataStore.
I detected a very long running process so I moved it to a separated thread like this on presenter
Thread thread = new Thread(new Runnable() {
#Override
public void run() {
// Events is a collection of DTOs, they are not Realm objects (converted from Realm object to DTO inside dataStore)
events = dataStore.getEvents(filterTerm);
view.getActivity().runOnUiThread(new Runnable() {
#Override
public void run() {
if (events.size() > 0) {
view.showEvents(events);
}
}
});
}
});
thread.start();
EDIT
Adding 3 queries executed in a row. They are called from 3 different methods
Query 1
final RealmResults<Event> results = query.findAllAsync();
results.addChangeListener(new RealmChangeListener() {
#Override
public void onChange() {
....
}
});
Query 2
final RealmResults<T> result = realm.where(type).findAllAsync();
result.addChangeListener(new RealmChangeListener() {
#Override
public void onChange() {
...
}
});
Query 3
final RealmResults<PresentationSpeaker> results = query.findAllAsync();
results.addChangeListener(new RealmChangeListener() {
#Override
public void onChange() {
...
}
});
It is normally custom to let your DataStore be asynchronous to avoid exactly the problems you run into now: That the operation are taking to long, and you have to create custom threads and use postRunnable. This should be something the DataStore is concerned about, not your Presenter and UI which should run on the UI thread. One solution is to let your Datastore be asynchronous instead, either by implementing some observer pattern yourself or use RxJava. Then you could use Realms async API's (https://realm.io/docs/java/latest/#asynchronous-queries) to do the following:
// DataStore
public class DataStore {
private final Realm realm;
public DataStore() {
realm = Realm.getDefaultInstance(); // Called on the UI thread
}
public void getEvents(EventsLoadedListener listener) {
RealmResults<Event> results = realm.where(Events.class).findAllAsync();
results.addChangeListener(new RealmChangeListener() {
public void onChange() {
if (listener != null) {
listener.eventsLoaded(results);
}
}
});
}
public interface EventsLoadedListener {
public void eventsLoaded(List<Event> events);
}
}
// UI code
datastore.getEvents(new DataStore.EventsLoadedListener() {
public void eventsLoaded(List<Event> events) {
view.showEvents(events);
}
})
The above code can be massively simplified using RxJava, so I would really encourage you to look into that. Realm also supports RxJava natively: https://realm.io/docs/java/latest/#rxjava

Categories

Resources