Why LiveData setValue or PostValue triggers onChange just once in the view?
Asked Answered
U

3

6

LiveData setValue should have triggered the onChanged method in the Activity, however it calls only at the first time, after when I try to make paging, it breaks and doesn't call onChanged anymore, though my response is does successful and I see it in the log. What's wrong with setValue/postValue? Is it a bug? Should I implement observer pattern on my own? Whats the point of using LiveData then? My paging doesn't work merely for this already 2-3 days.....

  1. MainActivity class

     public class MainActivity extends AppCompatActivity 
     private MutableLiveData<List<Photo>> mLivePhotos;
     // some code...
    
     @Override
     protected void onCreate(Bundle savedInstanceState) {
        mLivePhotos = loadData();
        mLivePhotos.observe(this, photos -> {
            Log.d(TAG, "onChanged!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
            mProgressBar.setVisibility(View.GONE);
            mPhotos = photos;
            if (mIsInitialCall) {
                initiateAdapter();
                mIsInitialCall = false;
            } else {
                mAdapter.updateList(mPhotos.subList(mPageNumber, mPageNumber + 10));
            }
        });
    
        mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                int lastPosition = 
    mLayoutManager.findLastCompletelyVisibleItemPosition();
                Log.d(TAG, "onScrolled - lastPosition: " + lastPosition);
                if (lastPosition == mLayoutManager.getItemCount() - 1) {
                    Log.d(TAG, "onScrolled - End of list?");
                    loadData();
                }
            }
        });
    }
    
    private MutableLiveData<List<Photo>> loadData() {
        Log.d(TAG, "loadData");
        if (mArticleViewModel == null) return null;
        mPageNumber += 10;
        mProgressBar.setVisibility(View.VISIBLE);
        return mArticleViewModel.loadPhotos();
    }
    
  2. ViewModel

    public class ArticleViewModel extends ViewModel {
        private MutableLiveData<List<Photo>> photos;
        private ArticleRepository articleRepository;
    
        public MutableLiveData<List<Photo>> loadPhotos() {
            Log.d(TAG, "getArticleList");
            //TODO; add Dagger 2
            articleRepository = new ArticleRepository();
            photos = articleRepository.getPhotos();
            return photos;
        }
    
  3. Repository

    public class ArticleRepository {
    
        public MutableLiveData<List<Photo>> getPhotos() {
            final MutableLiveData<List<Photo>> result = new MutableLiveData<>();
            Log.d(TAG, "getResults");
            ApiService.getService().getPhotos().enqueue(new Callback<List<Photo>>() {
            @Override
            public void onResponse(Call<List<Photo>> call, Response<List<Photo>> response) {
                Log.d(TAG, "onResponse");
                if (response.isSuccessful()) {
                    Log.d(TAG, "isSuccessful");
                    result.postValue(response.body());
                }
            }
    
            @Override
            public void onFailure(Call<List<Photo>> call, Throwable t) {
                Log.d(TAG, "onFailure: " + t.getMessage() + "\n" +  t.getStackTrace());
            }
        });
        return result;
    }
    
Unweave answered 21/6, 2018 at 6:56 Comment(0)
A
7

The Activity shouldn't have any MutablieLiveData member variables, that should be inside the ViewModel.

The reason for why it only works the first time, is because the first time you observe something it notifes as changed, however because your arrangement is incorrect it never updates again. That is, because ArticleRepository is recreated again inside your ViewModel with a new set of MutableLiveData, the previous one you subscribed to is no longer relevant - and you only subscribe once onCreate().

You should separate bind from async-tasks such as loadData() they are not the same thing. Binding is what you do at the beginning to gather the MutableLiveData (what you are doing in loadData), but after you've done that once you shouldn't do it again.

I also noted that you are actually having LiveData inside the model, it's not recommended to do it this way as it breaks the pattern and can bring other issues. It's the ViewModel that is supposed to prepare the presentation, not the Repository. As you currently have configured things your repository might as well be called the ViewModel. Instead what you should do is use observables to notify the ViewModel of a new batch to post or handle possible errors that occurred.

Study this example: https://developer.android.com/topic/libraries/architecture/viewmodel

Note that loadUsers() is done once when getUsers() is called. This is what binds the Activity to the ViewModel. But loadUsers() can be done again later and should post the changes to the LiveData inside the ViewModel.

Adjure answered 21/6, 2018 at 7:29 Comment(8)
Thanks for great explanation, I will rewrite the structure and see what happensUnweave
I fell to another trap: the first time getting data from the viewModel to call observe on it returns null because of asynchronous call,I need to know in the MainActivity when exactly the data is fetched in order to call observe on it, on the other hand I should observe data in order to know when it's updated. Dead circle. mLivePhotos = loadData() data is not loaded yet and I call mLivePhotos observe on a null object. LiveData doesn't serve its purpose?In order to start observe I need to pass MainActivity instance to ViewModel, which shouldn't hold reference to a viewUnweave
For observing the model you can either use android services, such as AsyncTask or something like RXJava. I have hard time commenting on your specific problem as I don't have your code in front of me. You might even get away with using simple java Observable, but then you must be mindful of the UI being on a different thread.Adjure
"it returns null because of asynchronous call," it returns null since you called postValue(null) / setValue(null) somewhereAllisan
@Allisan you are right, saved me! Bot now I wonder how the asynchronous call manages to fit in time? What if response comes later than observe is called on data?Makes me think...Unweave
it is a normal case: you observe something which can be already set (but does not have to) - in both cases the change can be triggered multiple times and the observer is notified each time the observed value changesAllisan
I continued with this project and stuck in other place could anyone please help? This is my question #51029528 and I will provide github link if neededUnweave
@tiago Redaelli I have a similar problem here: stackoverflow.com/questions/57650511/…. Would using MutableLiveData in my Repository and loadUsers() example and postValue() help? I would appreciate any thoughts you could offer...Hostetter
M
5

You should not new MutableLiveData() each time you load more photos, instead call postValue on the single MutableLiveData object. Example code below:

ViewModel:

public class ArticleViewModel extends ViewModel {
    private MutableLiveData<List<Photo>> photos;

    public MutableLiveData<List<Photo>> getPhotoList() {
        Log.d(TAG, "loadData");
        if (photos == null) {
            photos = new MutableLiveData<>();
            loadPhotos();
        }
        return photos;
    }

    public void loadPhotos() {
        Log.d(TAG, "getArticleList");

        ApiService.getService().getPhotos().enqueue(new Callback<List<Photo>>() {
            @Override
            public void onResponse(Call<List<Photo>> call, Response<List<Photo>> response) {
                Log.d(TAG, "onResponse");
                if (response.isSuccessful()) {
                    Log.d(TAG, "isSuccessful");
                    List<Photo> morePhotos = response.body();
                    List<Photo> allPhotos = new ArrayList<>();
                    allPhotos.addAll(photos.getValue());
                    allPhotos.addAll(morePhotos);
                    photos.postValue(allPhotos);   // <--- post change here
                }
            }

            @Override
            public void onFailure(Call<List<Photo>> call, Throwable t) {
                Log.d(TAG, "onFailure: " + t.getMessage() + "\n" +  t.getStackTrace());
            }
        });
    }
}

MainActivity:

public class MainActivity extends AppCompatActivity 
    private ArticleViewModel mArticalViewModel;

    // some code...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        mArticalViewModel = ViewModelProviders.of(this).get(ArticleViewModel.class);
        mArticleViewModel.getPhotoList().observe(this, photos -> {
            Log.d(TAG, "onChanged!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
        mProgressBar.setVisibility(View.GONE);
        mPhotos = photos;
        if (mIsInitialCall) {
            initiateAdapter();
            mIsInitialCall = false;
        } else {
            mAdapter.updateList(mPhotos.subList(mPageNumber, mPageNumber + 10));
        }
    });

    mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            int lastPosition = 
mLayoutManager.findLastCompletelyVisibleItemPosition();
            Log.d(TAG, "onScrolled - lastPosition: " + lastPosition);
            if (lastPosition == mLayoutManager.getItemCount() - 1) {
                Log.d(TAG, "onScrolled - End of list?");
                mArticleViewModel.loadPhotos(); // <--- changed here
            }
        }
    });
}
Mila answered 21/6, 2018 at 7:47 Comment(3)
This looks like it would solve the problem, but I'm not a fan of turning the data layer into a presentation layer as well. :)Adjure
Thanks for pointing out :), have moved MutableLiveData out from the Activity.Mila
@Hong Duan I have a similar problem here: #57651011. Would using MutableLiveData in my Repository, loadPhotos() and postValue() help? I would appreciate any thoughts you could offer...Hostetter
A
0

is the "mAdapter.updateList" function in your recycler adapter "arraylist.addAll or list.addAll" and is the "loadPhotos" function in viewModel called onCreate? if so, try changing "arraylist.addAll or list.addAll" to "arraylist = data or list = data". and separate the loadmore function with the firstCallApi (the viewmodel function you put in onCreate).

so the loadmore function is for updating data while firstCall is for initial data. on observe for loadmore insert code "arraylist.addAll or list.addAll" and for firstCall insert "list = data".

example : https://gist.github.com/kazuiains/c52f9fc597b66c05b7ec0439a0bb4ccb

Adalie answered 6/6, 2021 at 16:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.