4

I am new to using LiveData and ViewModels and I don't fully understand how the data is updated.

According to the Android Dev site:

Instead of updating the UI every time the app data changes, your observer can update the UI every time there's a change.

That sentence doesn't make sense to me. I'm using a Sqlite database (without Room) and I'm asynchronously getting the data in the getItems() method of the ViewModel. Does that mean when data is added or updated in the database, the LiveData will automatically detect it and onChange is called?

According to the Android dev site, it says to begin observing a LiveData object in onCreate. Before using LiveData, I was refreshing the data in onResume by querying the database then calling notifyDataSetChanged().

This code I have now displays the data when the Fragment loads, but if I delete or add new data to the database, the UI does not reflect the change. I just think I don't have a basic understanding of how LiveData works and I can't find a good explanation anywhere.

Help me StackOverflow, you're my only hope.

public class MyViewModel extends AndroidViewModel {
    private MutableLiveData<List<String>> items;
    private Application application;

    public MyViewModel(@NonNull Application application) {
        super(application);
        this.application = application;
    }

    public MutableLiveData<List<String>> getItems() {
        if (items == null) {
            items = new MutableLiveData<>();
            getItems();
        }
        return items;
    }

    private void getItems() {
        List<String> items = GetItems(); //async process

        this.items.setValue(items)
    }
}

public class ListFragment extends Fragment {

    @Override
    public void onCreate(final Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        MyViewModel model = ViewModelProviders.of(getActivity()).get(MyViewModel.class);

        final Observer<List<String>> observer = new Observer<List<String>>() {
            @Override
            public void onChanged(List<String> items) {
                //update UI
            }
        };

        model.getItems().observe(this, observer);
    }

}
Kris B
  • 3,436
  • 9
  • 64
  • 106
  • The problem is that you're only getting the items once, in your `getItems()` method. For it to work as you're expecting, you'll need to use a Room DAO and observe the DAO in the ViewModel. When the DAO observer in the ViewModel detects changes to the underlying data, then update the LiveData, and then the Fragment will get the updated data. – Daniel Nugent Nov 12 '19 at 22:21
  • I see. So if I'm not using Room does it makes sense to even use `LiveData`? – Kris B Nov 12 '19 at 22:41
  • Are you able to call again `private void getItems()` function once you have deleted or added elements to your database? If yes, call it again. – Julio Lemus Nov 12 '19 at 22:45
  • Can I just make `getItems()` public? – Kris B Nov 13 '19 at 00:42
  • I had the same problem as the OP, but my issue was that my **Fragment** was not obtaining the same instance of the ViewModel that my **Activity** was. More here: https://stackoverflow.com/questions/61373456/what-viewmodelstoreowner-to-use-for-viewmodelprovider-in-fragment . – ban-geoengineering Apr 22 '20 at 22:28

3 Answers3

4

You're retrieving items only once if private MutableLiveData<List<String>> items hasn't been initialised yet:

public MutableLiveData<List<String>> getItems() {
    if (items == null) {
        items = new MutableLiveData<>();
        getItems();// it's called only once 
    }
    return items;
}

One way to overcome this issue could be to use ContentObserver for your data:

...
private final ContentObserver contentObserver = new ContentObserver(new Handler()) {
    @Override
    public void onChange(boolean selfChange) {
        // if there're any changes then perform the request and update the UI
        getItems(); 
    }
};
...

Don't forget to register it to get notified of changes in the DB:

...
contentResolver.registerContentObserver(<your data content uri>, true, contentObserver);
...

I would also suggest to create a separate Repository layer responsible for data retrieval that could look as follows:

...
private final ContentObserver contentObserver = new ContentObserver(new Handler()) {
    @Override
    public void onChange(boolean selfChange) {
        // if there're any changes then perform the request and update the UI
        getItems(); 
    }
};

public LiveData<List<String>> getItems() {
    return new MutableLiveData<String>() {
        @Override
        public void onActive() {
           // your async operation here
           new Thread({
               List<String> items = db.getItems();
               postValue(items);
           }).start();

           contentResolver.registerContentObserver(<your data content uri>, true, contentObserver); 
        }

        @Override
        public void onInactive() {
            contentResolver.unregisterContentObserver(contentObserver); 
        }
    }
    return liveData;
}   
... 

Then, your ViewModel would need to inject the repository object and hence would require ViewModelFactory to build an instance of your ViewModel but the overall usage would look similar to this:

public class MyViewModel extends AndroidViewModel {   
    ... 
    public MutableLiveData<List<String>> getItems() {
        dataRepository.getItems();
    }
    ...
}

To get more information please read Guide to app architecture

Anatolii
  • 14,139
  • 4
  • 35
  • 65
2

Your code seems correct. You are setting up everything correctly and you will get a callback on your Observer in the ListFragment each time the LiveData value changes.

However, with that code, the LiveData value will never change, hence your LiveData's Observer will get called only once. You are calling MyViewModel.getItems only once, in the onCreate and this method will do two things: return the LiveData AND fetch the item list.

A first approach for you would be to divide your ViewModel getItems method into two: one for getting the LiveData and one for refreshing the actual list of items. In the Fragment, you would then refresh your data in the onResume(), as you used to.

It would look something like this:

public class MyViewModel extends ViewModel {
    private final MutableLiveData<List<String>> items = new MutableLiveData<>();

    public LiveData<List<String>> getItemsLiveData() {
        return items;
    }

    public void refreshItems() {
        List<String> items = GetItems(); //async process

        this.items.setValue(items)
    }
}

public class ListFragment extends Fragment {

    private MyViewModel model;

    @Override
    public void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        model = ViewModelProviders.of(getActivity()).get(MyViewModel.class);

        final Observer<List<String>> observer = new Observer<List<String>>() {
            @Override
            public void onChanged(List<String> items) {
                //update UI
            }
        };

        model.getItemsLiveData().observe(this, observer);
    }

    @Override
    public void onResume() {
        model.refreshItems()
    }
}

To go further, you will need to change the way you actually refresh items. If you want to be notified in your ViewModel when the database is modified, you will also need to use an Observer pattern in the communication between ViewModel and Database (Room with LiveData, RxJava's Observable or the new coroutine Flow are good places to start)

Here is a pseudo-algorithm example using RxJava (but that's up to you to choose which Observer library to use):

public class MyViewModel extends ViewModel {
    private final MutableLiveData<List<String>> items = new MutableLiveData<>();

    public MyViewModel() {
        // In actual code, don't forget to dispose that subscription in ViewModel.onCleared()
        myDatabase.getDatabaseObservable()
            .subscribe(new Subscriber<List<String>>() {
                @Override
                public void onCompleted() {
                }

                @Override
                public void onNext(List<String> newItems) {
                    items.setValue(newItems)
                }

                @Override
                public void onError() {
                }
            })
    }

    public LiveData<List<String>> getItemsLiveData() {
        return items;
    }
}

The last part in there would be to create that Observable in your Database class (either with a Library that does it automatically like Room or by creating your own PublishSubject).

Julien Arzul
  • 961
  • 9
  • 7
0

Your code seems to be set up correctly. But your Observers's onChanged() method will be called once since there is no part of your code notifying it of the change you are making to the underlying data.

I would suggest you explicitly notify your observer of changes in the data by using the MutableLiveData's setValue() or postValue() method depending on whether they are called from the main thread or background thread respectively. Or better still implement ROOM database with its Data Access Object (DAO) objects.

Here is a code snippet of where I did something similar if you are not willing to use ROOM just yet:

class HomeViewModel : ViewModel() {

private val _budgetList = MutableLiveData<ArrayList<Budget>>()
val budgetList: LiveData<ArrayList<Budget>> = _budgetList

private val _billList = MutableLiveData<ArrayList<Bill>>()
val billList: LiveData<ArrayList<Bill>> = _billList

init {
    _budgetList.value = getBudgetList()
    _billList.value = getBills()
}

fun deleteBudget(pos: Int) {
    _budgetList.apply {
        val newValue = value?.apply { removeAt(pos) }  header position
        postValue(newValue)
    }
}

}

After removing a value from the list, the new list is sent to the Livedata for an update via the postValue() method. Simply removing this value without posting it will not trigger the onchange() method of the Livedata's Observer. I hope this help someone.