LiveData is not updating its value after first call
Asked Answered
C

4

27

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(); 
}
Catacomb answered 25/8, 2017 at 21:26 Comment(10)
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.Orogeny
@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.Catacomb
@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...Catacomb
Worth noting that I use all this new crap with RXJava2 as well… tho I highly doubt this has anything to do with anything.Orogeny
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)Orogeny
btw, the min reputation for chat is 20 (afaik) I’m sure you can get +2 ;) Go answer a couple of questions! :)))Orogeny
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.Exactitude
Hi @ianhanniballake, yes it is. I am following the example given here developer.android.com/topic/libraries/architecture/guide.html. 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.Catacomb
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?Catacomb
You can find solution in here. https://mcmap.net/q/534923/-livedata-list-doesn-39-t-update-when-updating-database. It worked for meKalagher
C
19

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

Thanks to @MartinMarconcini for all his help in 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<List>

@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();
}
Catacomb answered 27/8, 2017 at 19:30 Comment(8)
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!Orogeny
The allowMainThreadQueries is not mandatory, but for the time I will leave it, just for self-keep :)Catacomb
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). :)Orogeny
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(simpleSQLiteQuery), 6).build();Mateya
@AnushkaKhare I haven't tried to use PagedList with LiveData. It's in my to do list :)Catacomb
@Catacomb Please help me check this similar but different problem #54442285Peephole
@Catacomb Why did you change the DAO to List instead of LiveData?Bertabertasi
@Bertabertasi 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.Catacomb
U
4

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

Uphill answered 9/5, 2018 at 22:11 Comment(1)
thank you sire. was scratching my head around for quite sometime.Lashley
C
4

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
Center answered 16/8, 2018 at 10:3 Comment(0)
O
2

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.

Orogeny answered 25/8, 2017 at 21:44 Comment(1)
Let us continue this discussion in chat. (Moderator note: all prior comments have been archived in chat.)Catacomb

© 2022 - 2024 — McMap. All rights reserved.