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(); 
}
joao86
  • 2,056
  • 1
  • 21
  • 23
  • 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. – Martin Marconcini Aug 25 '17 at 21:37
  • @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 Aug 25 '17 at 21:38
  • @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... – joao86 Aug 25 '17 at 21:41
  • 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 Aug 25 '17 at 21:55
  • 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 Aug 25 '17 at 22:06
  • btw, the min reputation for chat is 20 (afaik) I’m sure you can get +2 ;) Go answer a couple of questions! :))) – Martin Marconcini Aug 25 '17 at 23:44
  • 1
    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. – ianhanniballake Aug 26 '17 at 18:50
  • Hi @ianhanniballake, yes it is. I am following the example given here https://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. – joao86 Aug 26 '17 at 18:55
  • 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? – joao86 Aug 26 '17 at 18:58
  • You can find solution in here. https://stackoverflow.com/a/46059300/5756743. It worked for me – Cecil Paul Mar 01 '19 at 13:29

4 Answers4

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();
}
joao86
  • 2,056
  • 1
  • 21
  • 23
  • 4
    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! – Martin Marconcini Aug 28 '17 at 18:44
  • 1
    The `allowMainThreadQueries` is not mandatory, but for the time I will leave it, just for self-keep :) – joao86 Aug 28 '17 at 21:04
  • 1
    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). :) – Martin Marconcini Aug 28 '17 at 23:12
  • 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> liveData= new LivePagedListBuilder(recipeDao.getAllRecipesList(simpleSQLiteQuery), 6).build(); – Anushka Khare May 04 '18 at 09:10
  • @AnushkaKhare I haven't tried to use PagedList with LiveData. It's in my to do list :) – joao86 May 05 '18 at 12:02
  • @joao86 Please help me check this similar but different problem https://stackoverflow.com/questions/54441785/livedata-not-observing-after-first-call – EdgeDev Jan 30 '19 at 16:16
  • @joao86 Why did you change the DAO to List instead of LiveData? – Bink Sep 17 '20 at 22:39
  • @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. – joao86 Sep 18 '20 at 13:17
4

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

Sergey Buzin
  • 255
  • 2
  • 6
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
Harish Sharma
  • 360
  • 1
  • 2
  • 7
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.

Martin Marconcini
  • 26,875
  • 19
  • 106
  • 144
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/152913/discussion-between-joao86-and-martin-marconcini). (Moderator note: all prior comments have been archived in chat.) – joao86 Aug 26 '17 at 18:55