Observed LiveData doesn't really update after first fetch - android

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.

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.

How to avoid downloading same data from firebase realtime database?

I'm creating an Android app with an activity with a bottom navigation control that lets the user navigate between different fragments. In these fragments i have lists of data coming from a firebase backend that i show with a RecyclerView.
The problem is that every time i navigate between these fragments all the data is downloaded again, while i would want to use cached data and just listen for changes.
What i have done so far is to use ViewModel and LiveData and they work fine. Moreover if i disconnect the phone from the Internet the data is showed (and of course is not downloaded), even if i navigate between the fragments.
In the fragment that shows the data i have:
LiveData<List<UncompletedTask>> taskLiveData = viewModel.getTaskLiveData();
taskLiveData.observe(this, new Observer<List<UncompletedTask>>() {
#Override
public void onChanged(List<UncompletedTask> uncompletedTasks) {
myAdapter.submitList(uncompletedTasks);
listener.onTodoListElementsLoaded(uncompletedTasks.size());
}
});
In the viewmodel i have:
private TodoTaskRepository repository;
#NonNull
public LiveData<List<UncompletedTask>> getTaskLiveData() {
return repository.getTaskLiveData();
}
In the TodoTaskRepository i initialize FirebaseQueryLiveData in the contructor and return it in getTaskLiveData().
Finally FirebaseQueryLiveData is like this:
public class FirebaseQueryLiveData extends LiveData<DataSnapshot> {
private static final String LOG_TAG = "FirebaseQueryLiveData";
private final Query query;
private final MyValueEventListener listener = new MyValueEventListener();
public FirebaseQueryLiveData(Query query) {
this.query = query;
}
#Override
protected void onActive() {
query.addValueEventListener(listener);
}
#Override
protected void onInactive() {
query.removeEventListener(listener);
}
private class MyValueEventListener implements ValueEventListener {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
setValue(dataSnapshot);
}
#Override
public void onCancelled(DatabaseError databaseError) {
Log.e(LOG_TAG, "Can't listen to query " + query, databaseError.toException());
}
}
}
How can i download all the data the first time but then just listen for changes and don't download the same data while navigating between fragments if nothing is changed?
If you have enabled disk persistence then data will not be download again unless data has changed
FirebaseDatabase.getInstance().setPersistenceEnabled(true);
When you run your ValueEventListener the first time data is downloaded alright, the second time the same ValueEventListener runs then data is coming from local cache persistent
Moreover if disconnect the phone from the Internet the data is indeed coming from the same local cache.

How to modify LiveData from a database and forward it to the view

I'm getting a LiveData> from a database in my view model. But I have to add some Foo-objects to the list, before I can forward them to the view.
I'm using the Room API to get access to a database. I'm using the recommended encapsulation with a Dao, a repository and the view model. The repository just forwards the LiveData from the Dao.
In the view model, I call the method from the repository and store the result in a variable. Because I can't use the observe-method of the LiveData-object, I tried it with the Transformations.map-method. But the map-method isn't called at any time.
public class FooViewModel extends AndroidViewModel {
private LiveData<List<Foo>> fromDatabase;
private MutableLiveData<List<Foo>> forView;
public FooViewModel(/*...*/) {
//...
forView = new MutableLiveData<>();
}
//Returns the LiveData<List> for the view, that should be observed
public LiveData<List<Foo>> getViewList() {
return forView;
}
//Loads the data from the database, modifies it and maps it to the LiveData for the view
public void loadFromDatabase(/*Some conditions for query*/) {
fromDatabase = repository.getData(/*Some conditions*/);
Transformations.map(fromDatabase, (foos) -> {
forView.setValue(fillList(foos));
return forView;
}
}
//Fills the list with some other foos
private static List<Foo> fillList(List<Foo> foos) {
//Fill the list
}
}
And in the view I observe the list in a way like this:
public class FooActivity {
protected void onCreate(/*Some inputs*/) {
viewModel.getViewList().observe(this, (foos) -> /*Display the list*/);
viewModel.loadFromDatabase(/*with some conditions*/);
}
}
And then nothing happens...
I tried also to forward the LiveData got from the repository and observe it. That observation works fine. But not the modified one.
//Loads the data from the database, modifies it and maps it to the LiveData for the view
public void loadFromDatabase(/*Some conditions for query*/) {
fromDatabase = repository.getData(/*Some conditions*/);
Transformations.map(fromDatabase, (foos) -> {
forView.setValue(fillList(foos));
return forView;
}
}
This will never work. fromDatabase is replaced but the transformation is done against the previous fromDatabase instance.
You need to set the query conditions into a MutableLiveData to which you do Transformations.switchMap to return the LiveData<List<T>> with the correct filters applied through the DAO.
Then if you modify the conditions live data, the DAO will re-evaluate the new list with the new conditions.

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() {
}
});
}

How to make use of the new system architecture ViewModel/LiveData with cursorAdapter?

While learning the new android Architecture component’s ViewModel and LiveData, having a little confusion when observe the LiveData changing from database source change, and how this would work with Cursor adapter.
in https://developer.android.com/reference/android/widget/CursorAdapter.html, it says
int FLAG_REGISTER_CONTENT_OBSERVER
If set the adapter will register a content observer on the cursor
and will call onContentChanged() when a notification comes in. Be
careful when using this flag: you will need to unset the current
Cursor from the adapter to avoid leaks due to its registered
observers. This flag is not needed when using a CursorAdapter
with a CursorLoader.
so the with cursorAdaptor it has a way to get the ‘live update’ when the database data is updated.
is there a way to use the LiveData (to observe the database data update) with the cursorAdaptor?
trying to show the question of where to use the liveData updating the cursor in snippet below:
(with the sample of https://codelabs.developers.google.com/codelabs/android-persistence)
the Book:
#Entity
public class Book {
public #PrimaryKey String id;
public String title;
}
The ViewModel:
public class BooksBorrowedByUserViewModel extends AndroidViewModel {
public final LiveData<List<Book>> books;
private AppDatabase mDb;
public BooksBorrowedByUserViewModel(Application application) {
super(application);
createDb();
// Books is a LiveData object so updates are observed.
books = mDb.bookModel().findBooksBorrowedByName("Mike"); //<=== this ViewModel specific to one type query statement
}
public void createDb() {
mDb = AppDatabase.getInMemoryDatabase(this.getApplication());
// Populate it with initial data
DatabaseInitializer.populateAsync(mDb);
}
}
is this the way to use LiveData observer to force reload cursor?
private CursorAdapter listAdapter;
private BooksBorrowedByUserViewModel mViewModel;
private void subscribeUiBooks() {
mViewModel.books.observe(this, new Observer<List<Book>>() {
#Override
public void onChanged(#NonNull final List<Book> books) {
showBooksInUi(books, mBooksTextView); //<== the sample’s code
// if would like to update the cursorAdaptor
//
// ??? to requery the database and swap cursor here?
// Cursor data = queryData(buildSqlStatement()); // build the same sql statement as used in the BooksBorrowedByUserViewModel
// listAdapter.swapCursor(data)
}
});
}
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//having a list using CursorAdaptor
ListView list = getListView();
listAdapter = new CursorAdapter(getActivity(), null, 0)
list.setAdapter(listAdapter);
// Get a reference to the ViewModel for this screen.
mViewModel = ViewModelProviders.of(this).get(BooksBorrowedByUserViewModel.class);
subscribeUiBooks();
}
CursorAdapter is an old stuff, you should use Room + LiveData + RecyclerView.
Your data layer:
public LiveData<List<UserEntity>> getUsers() {
return userDao.getUsers();
}
Your activity:
viewModel.getUsers().observe(this, new Observer<List<UserEntity>>() {
#Override
public void onChanged(#Nullable List<UserEntity> users) {
if (users != null) {
adapter.setUsers(users);
}
}
});
In adapter:
private List<UserEntity> users = new ArrayList<>();
public void setUsers(List<UserEntity> users) {
this.users.clear();
this.users.addAll(users);
notifyDataSetChanged();
}
So when your activity lunch you should get live data from Room and subscribe to it. After that when you add something to that table, room automatically update observers, so you should just setup new data to the adapter and notify it.
Guide to App Architecture
LiveData
Room

Categories

Resources