LiveData is not updating its value after first call

37,456

Solution 1

Well, I have reached a solution for this issue and found out how this LiveData things works.

Thanks to @MartinMarconcini for all his help is debugging ;)

So apparently, the observers are linked to the object you first set it up to. You cannot replace the object (by attribution) or otherwise it will not work. Also, if the value of your variable is going to change then you should use MutableLiveData

So the change necessary were:

1. Change from LiveData to MutableLiveData and pass that MutableLiveData to the repository when you need to update it

public class StatesViewModel extends ViewModel {

private MutableLiveData<List<State>> states; ;;CHANGED
private StatesRepository repo;

@Inject
public StatesViewModel(StatesRepository repository){
    this.repo = repository;
}


public void init(String token){

    states = repo.getStates(token);
}

public void getStatesFromCountry(String countryID){

    repo.getStatesFromCountry(this.states, countryID); ;;CHANGED
}

public LiveData<List<State>> getStates(){

    return this.states;
}
}

2. In the repository, update the MutableLiveData using setValue

@Singleton
public class StatesRepository {

private final WebServices services;
private final StateDao stateDao;
private final Executor executor;

@Inject
public StatesRepository(Executor executor, StateDao stateDao, WebServices services) {
    this.services = services;
    this.stateDao = stateDao;
    this.executor = executor;
}


public MutableLiveData<List<State>> getStates(String token){
    refreshStates(token);

    final MutableLiveData<List<State>> data = new MutableLiveData<>();

    data.setValue(stateDao.getAllStates());

    return data;

}

;; CHANGED
public void getStatesFromCountry(MutableLiveData states, final String countryID){

    states.setValue(stateDao.getStatesFromCountry(countryID));

}

private void refreshStates(final String token){

    executor.execute(() -> {

        if(stateDao.getNrStates() == 0){

            try {
                Response<List<State>> response = services.getStates("Bearer "+token).execute();

                stateDao.insertAll(response.body());

            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    });
}
}

3. Changed the DAO to return List instead of LiveData>

@Dao
public interface StateDao {

@Query("SELECT * FROM states")
List<State> getAllStates();

@Query("SELECT * FROM states WHERE ctrId = :countryID")
List<State> getStatesFromCountry(String countryID);

@Query("SELECT COUNT(*) FROM states")
int getNrStates();

@Query("SELECT COUNT(*) FROM states WHERE ctrId = :countryID")
int getNrStatesByCountry(String countryID);

@Insert(onConflict = IGNORE)
void insertAll(List<State> states);

@Delete
void delete(State state);
}

4.Finally allow to perform queries in the main thread

AppModule.java

@Singleton @Provides
AppDatabase provideDb(Application app) {
    return Room.databaseBuilder(app, AppDatabase.class,"unitail.db")
            .allowMainThreadQueries()
            .fallbackToDestructiveMigration()
            .build();
}

Solution 2

Dao must be same across all operations. You use different Dao instance for insert and observe

Solution 3

You should not update the livedata reference once you set it and start observing it.Instead to update the live data with repository you should use the MediatorLiveData.

In your case do the following changes:

private MediatorLiveData<List<State>> states;  // change
.....
.....
states.addSource(repo.getStatesFromCountry(countryID), newData -> states.setValue(newData)); //change

Solution 4

Writing an answer for better discussion.

So I have (in Kotlin, sry) a model that is a list of notes (it’s just a sandbox app to play w/all this) and here’s my architecture: I don’t have a Repo, but I have Activity -> ViewModel -> Dao.

So Dao exposes a LiveData<MutableList<Note>>

@Query("SELECT * FROM notes")
fun loadAll(): LiveData<MutableList<Note>>

My ViewModel… exposes it through:

val notesList = database.notesDao().loadAll()

and my Activity (onCreate) does…

    viewModel.notesList.observe(this,
            Observer<MutableList<Note>> { notes ->
                if (notes != null) {
                    progressBar?.hide()
                    adapter.setNotesList(notes)
                }
            })

This works. The adapter is a RecyclerView adapter that does literally nothing but:

 fun setNotesList(newList: MutableList<Note>) {
        if (notes.isEmpty()) {
            notes = newList
            notifyItemRangeInserted(0, newList.size)
        } else {
            val result = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
                override fun getOldListSize(): Int {
                    return notes.size
                }

                override fun getNewListSize(): Int {
                    return newList.size
                }

                override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
                    return notes[oldItemPosition].id == newList[newItemPosition].id
                }

                override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
                    val (id, title, _, priority) = newList[newItemPosition]
                    val (id1, title1, _, priority1) = notes[oldItemPosition]
                    return id == id1
                            && priority == priority1
                            && title == title1
                }
            })
            notes = newList
            result.dispatchUpdatesTo(this)
        }
    }

If ANY other part of the app modifies that list of notes, the adapter updates automagically. I hope this gives you a playground to try a simple(r?) approach.

Share:
37,456

Related videos on Youtube

joao86
Author by

joao86

I am person who likes his job, that likes to have problems to solve and having to find solutions for it. I am experienced in communicating with demanding clients and I am very easily adaptable to different environments and situations. Over the last few years, I have been developing my knowledge regarding mobile programming, specially Android.

Updated on July 13, 2022

Comments

  • joao86
    joao86 almost 2 years

    I have been beating my head against the wall and I cannot understand why this is happening. I am working with the new Architectural Components for Android and I am having problems updating a LiveData with a List of Objects. I have two spinners. When i change the option in the first one, The second one must have its content changed. But this last part is not happening. Can anyone help me?

    State.java

    @Entity(tableName = "states")
    public class State{
    
    @PrimaryKey(autoGenerate = false)
    private int id;
    
    private String name;
    
    @ColumnInfo(name = "countryId")
    private String CountryId;
    
    @Ignore
    private Object geoCenter, geoLimit;
    
    public State(){
    
    }
    
    public int getId() {
        return id;
    }
    
    public void setId(int id) {
        this.id = id;
    }
    
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
    
    public String getCountryId() {
        return CountryId;
    }
    
    public void setCountryId(String countryId) {
        CountryId = countryId;
    }
    }
    

    StateDAO

    @Dao
    public interface StateDao {
    
    @Query("SELECT * FROM states")
    LiveData<List<State>> getAllStates();
    
    @Query("SELECT * FROM states WHERE countryId = :countryID")
    LiveData<List<State>> getStatesFromCountry(String countryID);
    
    @Query("SELECT COUNT(*) FROM states")
    int getNrStates();
    
    @Query("SELECT COUNT(*) FROM states WHERE countryId = :countryID")
    int getNrStatesByCountry(String countryID);
    
    @Insert(onConflict = IGNORE)
    void insertAll(List<State> states);
    
    @Delete
    void delete(State state);
    }
    

    StateRepository

    @Singleton
    public class StatesRepository {
    
    private final WebServices services;
    private final StateDao stateDao;
    private final Executor executor;
    
    @Inject
    public StatesRepository(Executor executor, StateDao stateDao, WebServices services) {
        this.services = services;
        this.stateDao = stateDao;
        this.executor = executor;
    }
    
    
    public LiveData<List<State>> getStates(String token){
        refreshStates(token);
    
        return stateDao.getAllStates();
    }
    
    public LiveData<List<State>> getStatesFromCountry(String countryID){
    
        return stateDao.getStatesFromCountry(countryID);
    }
    
    private void refreshStates(final String token){
    
        executor.execute(() -> {
    
            Log.d("oooooo", stateDao.getNrStates() + "");
            if(stateDao.getNrStates() == 0){
    
                try {
                    Response<List<State>> response = services.getStates("Bearer "+token).execute();
    
                    stateDao.insertAll(response.body());
    
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
    }
    }
    

    StateViewModel

    public class StatesViewModel extends ViewModel {
    
    private LiveData<List<State>> states;
    private StatesRepository repo;
    
    @Inject
    public StatesViewModel(StatesRepository repository){
    
        this.repo = repository;
    }
    
    public void init(String token){
    
        states = repo.getStates(token);
    }
    
    public void getStatesFromCountry(String countryID){
    
        states = repo.getStatesFromCountry(countryID);
    
    }
    
    public LiveData<List<State>> getStates(){
    
        return this.states;
    }
    
    }
    

    Fragment

    public class EditAddressFragment extends LifecycleFragment implements View.OnClickListener, Injectable{
    
    
    private Spinner country, city, state, zip_code;
    private String token;
    private List<Country> countries;
    private List<City> cities;
    private List<State> states;
    @Inject ViewModelFactory viewModelFactory;
    
    
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.addresses_edit_layout, container, false);
    
        city = view.findViewById(R.id.city);
        state = view.findViewById(R.id.state);
        country = view.findViewById(R.id.country);
        ...
    
        countries = new ArrayList<>();
        cities = new ArrayList<>();
        states = new ArrayList<>();
    
        return view;
    }
    
    
    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
    
    
        CountrySpinnerAdapter adapter = new CountrySpinnerAdapter(getActivity(), android.R.layout.simple_spinner_item, countries);
        country.setAdapter(adapter);
    
        CitySpinnerAdapter cityAdapter = new CitySpinnerAdapter(getActivity(), android.R.layout.simple_spinner_item, cities);
        city.setAdapter(cityAdapter);
        StateSpinnerAdapter stateAdapter = new StateSpinnerAdapter(getActivity(), android.R.layout.simple_spinner_item, states);
        state.setAdapter(stateAdapter);
    
    
        CountriesViewModel countriesViewModel = ViewModelProviders.of(this, viewModelFactory).get(CountriesViewModel.class);
        countriesViewModel.init(token);
        countriesViewModel.getCountries().observe(this, adapter::setValues);
    
        CityViewModel cityViewModel = ViewModelProviders.of(this, viewModelFactory).get(CityViewModel.class);
        cityViewModel.init(token);
        cityViewModel.getCities().observe(this, cityAdapter::setValues);
    
        StatesViewModel statesViewModel = ViewModelProviders.of(this, viewModelFactory).get(StatesViewModel.class);
        statesViewModel.init(token);
        statesViewModel.getStates().observe(this, states -> { 
          Log.d("called", states.toString()); 
          stateAdapter.setValues(states); } );
    
    
        country.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
            @Override
            public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
    
                Country c = (Country) adapterView.getItemAtPosition(i);
    
                Log.d("cd", c.getId());
    
                //states = new ArrayList<State>();
    
                statesViewModel.getStatesFromCountry(c.getId());
    
            }
    
            @Override
            public void onNothingSelected(AdapterView<?> adapterView) {
    
            }
        });
    

    ....

    Adapter

    public void setValues(List<State> states)
    { 
    this.states = states; 
    Log.d("s", states.isEmpty()+" "+states.toString()); 
    notifyDataSetChanged(); 
    }
    
    • Martin Marconcini
      Martin Marconcini almost 7 years
      It’s hard to see the whole picture… (I initially though you had no LiveData) but you do. However, I suggest you check your threading / concurrency, since you’re using an executor in your repo… The approach I’d take (I think) is each spinner is observing LiveData (different) and the ViewModel would ensure to update it when the UI/Spinner1 changes, so the LiveData observed by the UI/Spinner2 changes. If this is what you’re doing atm, it’s hard to see in the code.
    • joao86
      joao86 almost 7 years
      @MartinMarconcini I do it in the repository because that is where I have the DAO instance. MY Repository is where I make the calls either to the API or the database. In this case, I only need to go to the local DB because previously I have retrieved all the states. From all my tests, he can apparently get the value from the DB but the onchange method of the observer is not called.
    • joao86
      joao86 almost 7 years
      @MartinMarconcini that is the case. I have two different spinners observing two different LiveData. I also saw someone talking about the threading issue, but I am using the same instance of the DAO so there should not be any problem with that I think...
    • Martin Marconcini
      Martin Marconcini almost 7 years
      Worth noting that I use all this new crap with RXJava2 as well… tho I highly doubt this has anything to do with anything.
    • Martin Marconcini
      Martin Marconcini almost 7 years
      So the Log.d("called", states.toString()); stateAdapter.setValues(states); }); callback in the Observable is never called even tho in theory the statesViewModel.getStates() live data was updated? hmmm… Another thing I had trouble with was the LifecycleFragment/Activity thing. It wouldn’t correctly work (at least when I tried) so I ended up using/implementing LifecycleRegistryOwner interface in a simple AppCompatActivity (and providing the LifecycleRegistry(this)
    • Martin Marconcini
      Martin Marconcini almost 7 years
      btw, the min reputation for chat is 20 (afaik) I’m sure you can get +2 ;) Go answer a couple of questions! :)))
    • ianhanniballake
      ianhanniballake almost 7 years
      Is your StatesRepository a singleton? If you have two instances of your database, events from one won't propagate to observers set on the other instance.
    • joao86
      joao86 almost 7 years
      Hi @ianhanniballake, yes it is. I am following the example given here developer.android.com/topic/libraries/architecture/guide.htm‌​l. Also when I call the getStates(token) method, the Observer is updated, so why isn't it updating the other times? My database also has one instance as it also a Singleton.
    • joao86
      joao86 almost 7 years
      See the continuation of my conversation with @MartinMarconcini, I think the problem is with the observer, since the list values are updated. I thought that the observer would be called every time the list content would be changed, or am I wrong?
    • Cecil Paul
      Cecil Paul over 5 years
      You can find solution in here. stackoverflow.com/a/46059300/5756743. It worked for me
  • joao86
    joao86 almost 7 years
    Let us continue this discussion in chat. (Moderator note: all prior comments have been archived in chat.)
  • Martin Marconcini
    Martin Marconcini almost 7 years
    Interesting. Tho the allowMainThreadQueries() shouldn’t really be used for Prod… I’m still puzzled why I have LiveData being returned directly from the DAO and you had to change that… hmmm. Oh well, glad to hear it works. Good luck!
  • joao86
    joao86 almost 7 years
    The allowMainThreadQueries is not mandatory, but for the time I will leave it, just for self-keep :)
  • Martin Marconcini
    Martin Marconcini almost 7 years
    Consider using RXJava2 for those queries, it’s really useful once you get used to it (and you wouldn’t need to use an Executor, with all the problems that has). :)
  • Anushka Khare
    Anushka Khare about 6 years
    I am using paged list livedata. its builder class returns only livedata . I am not able to replace livedata with mutable livedata , then how can I resolve this issue . LiveData<PagedList<RecipeListPojo>> liveData= new LivePagedListBuilder(recipeDao.getAllRecipesList(simpleSQLit‌​eQuery), 6).build();
  • joao86
    joao86 about 6 years
    @AnushkaKhare I haven't tried to use PagedList with LiveData. It's in my to do list :)
  • EdgeDev
    EdgeDev over 5 years
    @joao86 Please help me check this similar but different problem stackoverflow.com/questions/54441785/…
  • Bink
    Bink almost 4 years
    @joao86 Why did you change the DAO to List instead of LiveData?
  • joao86
    joao86 almost 4 years
    @Bink at the time LiveData was only for data that wasn't going to change constantly. If the data was going to change constantly then you needed to use MutableLiveData and therefore couldn't use LiveData. I don't if that has changed in the meantime.
  • Gowtham
    Gowtham over 3 years
    thank you sire. was scratching my head around for quite sometime.