Viewmodel Livedata doesn't update data observers - android

For example, I have 12 UpcomingGamesFragment and each fragment has a different set of game data releasing a month, for example the first fragment of 12 will have video games releasing on January 2019.
In my app, there's a navigation drawer with a list of platforms (ps4, xbox, pc, etc.) and when the user picks his consoles by clicking on the check boxes and then closes the drawer layout, I want all the fragments to update accordingly. Like only show games releasing on these platforms. Retrieve and filter successfully works through a method in UpcomingGamesFragment called loadReleasesData()
Now what I want is all the fragments to update when the navigation drawer gets closed, because my implementation doesn't work, please tell me what's wrong.
Here's my ViewModel class:
public class ReleasesViewModel extends ViewModel {
private MutableLiveData<List<_Release>> upcomingReleases = new MutableLiveData<>();
public ReleasesViewModel() { }
public MutableLiveData<List<_Release>> getUpcomingReleases() {
return upcomingReleases;
}
}
And in my Filter drawer layout is in my MainActivity, and also I declare my ViewModel in my MainActivity:
OnCreate (A lot of code omitted for clarity)
mReleasesViewModel = ViewModelProviders.of(this).get(ReleasesViewModel.class);
My drawer layout on close:
#Override
public void onDrawerClosed(#NonNull View drawerView) {
// If change detected refresh!
if (!mCopyOfUserPlatforms.equals(SharedPrefManager.read(SharedPrefManager.PLATFORM_IDS, mDefaultPlatformsSet))) {
mReleasesViewModel.getUpcomingReleases().setValue(new ArrayList<_Release>());
}
}
And I pass the viewmodel livedata object to my 12 fragments when I initialize them in another fragment called the ViewPagerFragment here's how:
onCreateView:
// Get the ViewModel
ReleasesViewModel releasesViewModel = ((MainActivity)getActivity()).mReleasesViewModel;
for (int i = 0; i < 14; i++) {
UpcomingGamesFragment upcomingGamesFragment = new UpcomingGamesFragment();
upcomingGamesFragment.setLiveData(releasesViewModel); // HERE
mSectionsPagerAdapter.addFragment(upcomingGamesFragment, title, queryId);
}
This is my setLiveData() method in UpcomingGamesFragment:
public void setLiveData(ReleasesViewModel releasesViewModel) {
releasesViewModel.getUpcomingReleases().observe(this, new android.arch.lifecycle.Observer<List<_Release>>() {
#Override
public void onChanged(#Nullable List<_Release> releases) {
Log.d(TAG, "Refreshing this baby boy...");
loadReleaseData(0);
}
});
}
How I know the livedata doesn't update all my fragments? It is because I have a log in my loadReleasesData method and it doesn't get printed in the Logcat and not to mention the fact it doesn't update the fragment(s). Have a good day and bye! :)

Your issue is not with LiveData but with ViewModel instance you're getting in ViewPagerFragment :
use this in your onCreateView() of fragment:
ReleasesViewModel releasesViewModel = ViewModelProviders.of(getActivity()).get(ReleasesViewModel.class);
instead of your code:
// Get the ViewModel
ReleasesViewModel releasesViewModel = ((MainActivity)getActivity()).mReleasesViewModel;

Related

How to cache PagingData in a way that it does not cause its data to reflow

I have a simple setup of 2 fragments: ConversationFragment and DetailsFragment
I am using Room with Paging 3 library and to populate the ConversationFragment I am using a PagingLiveData implementation together with a AndroidViewModel belonging to the ConversationFragment.
I am not using the Navigation Components here, just a common fragment navigation as per Android documentation.
From that fragment I can open the DetailsFragment and then return back to the fragment again. Everything is working well, until I open said fragment and return, then the observer that was tied in the ConversationFragment is lost since that fragment is being destroyed when opening the DetailsFragment.
So far this is not a big issue, I can restart the observer again and it does work when I do that.
However, when I attach the observer again the entire list reflows, this causes the items in the RecyclerView to go wild, the position the list was on is lost and the scrollbar changes sizes which confirms pages are being loaded/reloaded.
I could withstand the weird behavior to a degree, but to have the position lost on top of that is not acceptable.
I looked into caching the results in the view model, but the examples I could find in the available documentation are basic and do not show how the same could be achieved using a LiveData<PagingData<...> object.
Currently this is what I have:
ConversationFragment
#Override
public void onViewCreated(
#NonNull View view,
#Nullable Bundle savedInstanceState
) {
if (viewModel == null) {
viewModel = new ViewModelProvider(this).get(ConversationViewModel.class);
}
if (savedInstanceState == null) {
// adapter is initialized in onCreateView
viewModel
.getList(getViewLifecycleOwner())
.observe(getViewLifecycleOwner(), pagingData -> adapter.submitData(lifecycleOwner.getLifecycle(), pagingData));
}
super.onViewCreated(view, savedInstanceState);
}
ConversationViewModel
public class ConversationViewModel extends AndroidViewModel {
final PagingConfig pagingConfig = new PagingConfig(10, 10, false, 20);
private final Repository repository;
private final MutableLiveData<PagingData<ItemView>> messageList;
public ConversationFragmentVM(#NonNull Application application) {
super(application);
messageList = new MutableLiveData<>();
repository = new Repository(application);
}
public LiveData<PagingData<ItemView>> getList(#NonNull LifecycleOwner lifecycleOwner) {
// at first I tried only setting the value if it was null
// but since the observer is lost on destroy and the value
// is not it would never be able to "restart" the observer
// again
// if (messageList.getValue() == null) {
PagingLiveData.cachedIn(
PagingLiveData.getLiveData(new Pager<>(pagingConfig, () -> repository.getMessageList())),
lifecycleOwner.getLifecycle()
).observe(lifecycleOwner, messageList::setValue);
// }
return messageList;
}
}
As it is, even if I return the result of the PagingLiveData.cachedIn the behavior is the same when I return to the fragment; the items show an erratic behavior in the recyclerview list and the position it was on is totally lost.
This is what I was trying to achieve to see if it fixed my issue:
This is a code lab available here: https://developer.android.com/codelabs/android-training-livedata-viewmodel#8
As you can see the mAllWords are cached and they are only initialized when the view model is constructed for the first time, any subsequent changes are simply updates and would only require new observers to be attached when the fragment is destroyed and created again while still in the back stack.
This is what I was trying to do, but it does not work the way I thought it did, at least it is not as straight forward as I thought.
How can this be achieved?
There's quite a lot to unpack here but my best guess would be your getList method in ConversationViewModel. You're on the right track with using ViewModels and LiveData to persist data across navigation but here you're recreating the LiveData every time this method is called, meaning when you resume ConversationFragment and onViewCreated is called, it creates a new Pager which fetches new data.
The solution would be to create the pager when ConversationViewModel is first created and then accessing the LiveData object itself, rather than the method. You can see this in the Codelab example, they assign the LiveData in the constructor and simply return the already created LiveData in the getAllWords() method.
I'm using this as an example, change ConversationViewModel to something like this and change it to use your config and repository.
private final LiveData<PagingData<ItemView>> messageList;
public ConversationFragmentVM(#NonNull Application application) {
super(application);
repository = new Repository(application);
// CoroutineScope helper provided by the lifecycle-viewmodel-ktx artifact.
CoroutineScope viewModelScope = ViewModelKt.getViewModelScope(viewModel);
Pager<Integer, User> pager = Pager<>(
new PagingConfig(/* pageSize = */ 20),
() -> ExamplePagingSource(backend, query));
messageList = PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), viewModelScope);
}
public LiveData<PagingData<ItemView>> getList(){
return messageList;
}
Then in your fragment, you simply observe getList() like usual, except this time it's returning a prexisting version.
viewModel.getList().observe(getViewLifecycleOwner(), pagingData ->
adapter.submitData(lifecycleOwner.getLifecycle(), pagingData));
}
I haven't been able to test that this compiles or works so let me know if it doesn't and I'll update this answer.

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.

Trigger update for a LiveData member when another LiveData is updated in the view model

In a word game app I share a model between an activity and a fragment:
public class MainViewModel extends AndroidViewModel {
private LiveData<List<Game>> mGames;
private final MutableLiveData<Game> mDisplayedGame = new MutableLiveData<>();
(please excuse the non-english text in the screenshot)
The activity observes mGames being currently played by the user and updates the navigational drawer menu (see the left side of the above screenshot).
The fragment observes mDisplayedGame and displays it in a custom view (see the right side of the above screenshot).
My problem is that when the list of the games is updated at the server (and the activity receives new list of games via Websocket and stores it in the Room), I need to post an update to the fragment: "Hey, the game you are displaying was updated, redraw it!"
Is it possible to do that from within the shared view model?
I know that I could observe mGames in the fragment too and add a code there iterating through them and then finding out if the displayed game was updated at the server.
But I would prefer to do it in the MainViewModel because I have a feeling that the fragment should only observe the one game it is displaying and that's it.
TL;DR
Whenever mGames is updated in the view model via Room, I need to notify the mDisplayedGame observers too!
You should use a MediatorLiveData for this.
The way it works is that
public class MainViewModel extends AndroidViewModel {
private final LiveData<List<Game>> mGames;
private final MutableLiveData<Game> mSelectedGame = new MutableLiveData<>();
private final MediatorLiveData<Game> mDisplayedGame = new MediatorLiveData<>();
{
mDisplayedGame.addSource(mGames, (data) -> {
// find the new value of the selected game in the list
mSelectedGame.setValue(newSelectedGame);
});
mDisplayedGame.addSource(mSelectedGame, (data) -> {
mDisplayedGame.setValue(data);
});
}
And then you expose mDisplayedGame as a LiveData<Game> and it should just work.
Use callback bro
-add a callback interface in your viewmodel and a setCallback method
-make your fragment implement it then call viewmodel.setCallback(fragment)
-call the callback in your obsever

Update all fragments data at the same time with ViewModel

I have 12 fragments UpcomingGamesFragment repeated twelve times for each month of the year, the respective fragment shows the game releases of the month. For example, the first month will show the games releasing in January 2019, the next fragment will have February 2019 releases, etc.
What I'm trying to build is an architecture that uses a ViewModel. A ViewModel which will be shared across all my 12 fragments and would trigger the data change (through LiveData) to all fragments gracefully, but I have no idea how to use this ViewModel class to accomplish the update to all visible fragments, here's my UpcomingGamesFragment class with the request month data method:
public class UpcomingGamesFragment extends Fragment {
public void loadReleaseData(final int refresh) {
if (mDatabaseLoading == null) {
Log.d(TAG, "Fragment filter " + mFilter + " [fragment is null]");
return;
} else {
Log.d(TAG, "Updating fragment: " + mFilter);
}
if (AppUtil.doesInternetWork(getActivity())) {
// update, data fetched from firebase
}
}
The 12 fragments are initialized in another fragment which gets shown in MainActivity, the fragment is called UpcomingViewPagerFragment which creates 12 UpcomingGamesFragment in a loop.
And here's my ViewModel class:
public class ReleasesViewModel extends ViewModel {
private MutableLiveData<List<_Release>> upcomingFragmentLiveData =
new MutableLiveData<>();
public ReleasesViewModel() {
}
public LiveData<List<_Release>> getUpcomingFragmentList() {
return upcomingFragmentLiveData;
}
}
So how can I update the 12 fragments with the loadReleaseData method taking in count the lifecycle of each?
Assuming all fragment contents are values held somewhere else in your code and assuming you are correctly updating those values. The easiest way to update all fragments accordingly is by calling:
notifyDataSetChanged();
You need to retrieve your ViewModel class object in fragment and register LiveData from it like,
ReleasesViewModel obj = ViewModelProviders.of(getActivity()).get(ReleasesViewModel.class);
in your fragment's onCreate() method, then use that obj of view model for observing your live data like,
(obj of your view model).getUpcomingFragmentList().observe(this, `your observer for that fragment`);
For your instance:
This will get called once any data change happen for your live data inside ViewModel
obj.getUpcomingFragmentList().observe(this, new Observer<List<_Release>>() >{
#Override
public void onChanged(#Nullable List<_Release> list) {
}
});
If you can't find ViewModelProviders class then you need to add dependecies for your ViewModel & livedata from here,
link to add view model & live data to your project

What is the use of return statement when using with LiveData

I have followed this example, an integrated ViewModel and LiveData.
I used LiveData for ViewModel to Repository communication, and activity to ViewModel communication
I have few confusions that I want to clear this question.
This is working fine, and display Toast Message after 5 seconds on MainActivity.
MainActivity
MainViewModel homeViewModel = ViewModelProviders.of(this).get(MainViewModel.class);
homeViewModel.getResponseval().observe(this, new Observer<String>() {
#Override
public void onChanged(#Nullable String s) {
Toast.makeText(getApplicationContext(), "MainActivityObserverCalled", Toast.LENGTH_LONG).show();
}
});
MainViewModel
public class MainViewModel extends ViewModel {
public MutableLiveData<String> responseval;
private LoginRepositry loginRepositry;
public MainViewModel(){
loginRepositry = new LoginRepositry();
responseval = loginRepositry.getData("username","password");
}
public MutableLiveData<String> getResponseval() {
return responseval;
}
LoginRepositry
public class LoginRepositry {
private MutableLiveData<String> data = new MutableLiveData<>();
public MutableLiveData<String> getData(String username , String userpass) {
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
data.setValue("Login Repo Set Value");
}
}, 5000);
return data;
}
These are my 2 questions.
Now with each method, I am returning some data of type LiveData, but
at the time of returning the data, the value is not set. I set it after 5
seconds data.setValue("SomeValue"), So what is the use of return
here, is it only because of method return type, and does nothing in
case of LiveData
In MainActivity, i used homeViewModel.getResponseval().observe
to observer data, but in ViewModel, I didn't use observe, but
ViewModel is still observing the Repository and after 5 seconds
MainActivity gets a result. I am unable to understand whats happening here
.
So let's do this by parts:
Now with each method i am returning some data of type LiveData, but at
the time of returning the data, the value is not set. I set it after 5
seconds data.setValue("SomeValue"), So what is the use of return here,
is it only because of method return type, and does nothing in case of
LiveData
If you check the docs you'll see that LiveData "is an observable data holder class", so it holds data and you can observer it. This is very important to understand why you're returning this LiveData object here. By returning it you're telling the next layer of your architecture (the ViewModel) "hey, here is this holder, I will put some data here at some point, so observer it if you want to receive the data".
The ViewModel doesn't observe it, but simply pass it to the next entity interested in this holder of data, the UI (LifeCycleOwner). So in the owner you start to observe this "holder of data" and will be warned when new data arrives.
In MainActivity, i used homeViewModel.getResponseval().observe to
observer data, but in ViewModel, I didn't use observe, but ViewModel
is still observing the Repository, and after 5 seconds MainActivity
gets result. I am unable to understand whats happening here.
You need a LifeCycleOwner in order to observe updates from a LiveData, also from the docs: "LiveData considers an observer, which is represented by the Observer class, to be in an active state if its lifecycle is in the STARTED or RESUMED state. LiveData only notifies active observers about updates." In order to detect the state, it needs a LifeCycleOwner, that's why when you have the observe method you pass this as a parameter. And that's why you didn't use observe in the ViewModel.

Categories

Resources