With android Paging library it is really easy to load data from Database in chunks and ViewModel provides automatic UI update and data survival. All these frameworks modules help us create a great app in android platform.
A typical android app has to show a list of items and allows user to search that list. And this what I want to achieve with my app. So I have done an implementation by reading many documentations, tutorials and even stackoverflow answers. But I am not so sure whether I am doing it correctly or how I supposed to do it. So below, I have shown my way of implementing paging library with ViewModel and RecyclerView.
Please, review my implementation and correct me where I am wrong or show me how I supposed to do it. I think there are many new android developers like me are still confused how to do it correctly as there is no single source to have answers to all your questions on such implementation.
I am only showing what I think is important to show. I am using Room. Here is my Entity that I am working with.
#Entity(tableName = "event")
public class Event {
#PrimaryKey(autoGenerate = true)
public int id;
public String title;
}
Here is DAO for Event entity.
#Dao
public interface EventDao {
#Query("SELECT * FROM event WHERE event.title LIKE :searchTerm")
DataSource.Factory<Integer, Event> getFilteredEvent(String searchTerm);
}
Here is ViewModel extends AndroidViewModel which allows reading and searching by providing LiveData< PagedList< Event>> of either all events or filtered event according to search text. I am really struggling with the idea that every time when there is a change in filterEvent, I'm creating new LiveData which can be redundant or bad.
private MutableLiveData<Event> filterEvent = new MutableLiveData<>();
private LiveData<PagedList<Event>> data;
private MeDB meDB;
public EventViewModel(Application application) {
super(application);
meDB = MeDB.getInstance(application);
data = Transformations.switchMap(filterEvent, new Function<Event, LiveData<PagedList<Event>>>() {
#Override
public LiveData<PagedList<Event>> apply(Event event) {
if (event == null) {
// get all the events
return new LivePagedListBuilder<>(meDB.getEventDao().getAllEvent(), 5).build();
} else {
// get events that match the title
return new LivePagedListBuilder<>(meDB.getEventDao()
.getFilteredEvent("%" + event.title + "%"), 5).build();
}
}
});
}
public LiveData<PagedList<Event>> getEvent(Event event) {
filterEvent.setValue(event);
return data;
}
For searching event, I am using SearchView. In onQueryTextChange, I wrote the following code to search or to show all the events when no search terms is supplied meaning searching is done or canceled.
Event dumpEvent;
#Override
public boolean onQueryTextChange(String newText) {
if (newText.equals("") || newText.length() == 0) {
// show all the events
viewModel.getEvent(null).observe(this, events -> adapter.submitList(events));
}
// don't create more than one object of event; reuse it every time this methods gets called
if (dumpEvent == null) {
dumpEvent = new Event(newText, "", -1, -1);
}
dumpEvent.title = newText;
// get event that match search terms
viewModel.getEvent(dumpEvent).observe(this, events -> adapter.submitList(events));
return true;
}
Thanks to George Machibya for his great answer. But I prefer to do some modifications on it as bellow:
There is a trade off between keeping none filtered data in memory to make it faster or load them every time to optimize memory. I prefer to keep them in memory, so I changed part of code as bellow:
listAllFood = Transformations.switchMap(filterFoodName), input -> {
if (input == null || input.equals("") || input.equals("%%")) {
//check if the current value is empty load all data else search
synchronized (this) {
//check data is loaded before or not
if (listAllFoodsInDb == null)
listAllFoodsInDb = new LivePagedListBuilder<>(
foodDao.loadAllFood(), config)
.build();
}
return listAllFoodsInDb;
} else {
return new LivePagedListBuilder<>(
foodDao.loadAllFoodFromSearch("%" + input + "%"), config)
.build();
}
});
Having a debouncer helps to reduce number of queries to database and improves performance. So I developed DebouncedLiveData class as bellow and make a debounced livedata from filterFoodName.
public class DebouncedLiveData<T> extends MediatorLiveData<T> {
private LiveData<T> mSource;
private int mDuration;
private Runnable debounceRunnable = new Runnable() {
#Override
public void run() {
DebouncedLiveData.this.postValue(mSource.getValue());
}
};
private Handler handler = new Handler();
public DebouncedLiveData(LiveData<T> source, int duration) {
this.mSource = source;
this.mDuration = duration;
this.addSource(mSource, new Observer<T>() {
#Override
public void onChanged(T t) {
handler.removeCallbacks(debounceRunnable);
handler.postDelayed(debounceRunnable, mDuration);
}
});
}
}
And then used it like bellow:
listAllFood = Transformations.switchMap(new DebouncedLiveData<>(filterFoodName, 400), input -> {
...
});
I usually prefer to use DataBiding in android. By using two way Data Binding you don't need to use TextWatcher any more and you can bind your TextView to the viewModel directly.
BTW, I modified George Machibya solution and pushed it in my Github. For more details you can see it here.
I will strong advice to start using RxJava and you it can simplify the entire problem of looking on the search logic.
I recommend in the Dao Room Class you implement two method, one to query all the data when the search is empty and the other one is to query for the searched item as follows. Datasource is used to load data in the pagelist
#Query("SELECT * FROM food order by food_name")
DataSource.Factory<Integer, Food> loadAllFood();
#Query("SELECT * FROM food where food_name LIKE :name order by food_name")
DataSource.Factory<Integer, Food> loadAllFoodFromSearch(String name);
In the ViewModel Class we need to two parameter that one will be used to observed searched text and that we use MutableLiveData that will notify the Views during OnChange. And then LiveData to observe the list of Items and update the UI.
SwitchMap apply the function that accept the input LiveData and generate the corresponding LiveData output. Please find the below Code
public LiveData<PagedList<Food>> listAllFood;
public MutableLiveData<String> filterFoodName = new MutableLiveData<>();
public void initialFood(final FoodDao foodDao) {
this.foodDao = foodDao;
PagedList.Config config = (new PagedList.Config.Builder())
.setPageSize(10)
.build();
listAllFood = Transformations.switchMap(filterFoodName, outputLive -> {
if (outputLive == null || outputLive.equals("") || input.equals("%%")) {
//check if the current value is empty load all data else search
return new LivePagedListBuilder<>(
foodDao.loadAllFood(), config)
.build();
} else {
return new LivePagedListBuilder<>(
foodDao.loadAllFoodFromSearch(input),config)
.build();
}
});
}
The viewModel will then propagate the LiveData to the Views and observe the data onchange. In the MainActivity then we call the method initialFood that will utilize our SwitchMap function.
viewModel = ViewModelProviders.of(this).get(FoodViewModel.class);
viewModel.initialFood(FoodDatabase.getINSTANCE(this).foodDao());
viewModel.listAllFood.observe(this, foodlistPaging -> {
try {
Log.d(LOG_TAG, "list of all page number " + foodlistPaging.size());
foodsactivity = foodlistPaging;
adapter.submitList(foodlistPaging);
} catch (Exception e) {
}
});
recyclerView.setAdapter(adapter);
For the first onCreate initiate filterFoodName as Null so that to retrieve all items.
viewModel.filterFoodName.setValue("");
Then apply TextChangeListener to the EditText and call the MutableLiveData that will observe the Change and update the UI with the searched Item.
searchFood.addTextChangedListener(new TextWatcher() {
#Override
public void beforeTextChanged(CharSequence charSequence, int i,
int i1, int i2) {
}
#Override
public void onTextChanged(CharSequence charSequence, int i, int
i1, int i2) {
}
#Override
public void afterTextChanged(Editable editable) {
//just set the current value to search.
viewModel.filterFoodName.
setValue("%" + editable.toString() + "%");
}
});
}
Below is my github repo of full code.
https://github.com/muchbeer/PagingSearchFood
Hope that help
Related
I am using android studios to build a dictionary app. Everything has went fairly well with help from the SO community and now I have another question.
I have setup several custom dictionaries so that the user can store and view words in different dictionaries. (e.g. literature, programming, general, etc.)
These will be a FK id in the word entity and filled when the user adds new words.
In the MainActivity, I have an Options menu item for 'Change Dictionary'. This brings up an AlertDialog where the user can change the dictionary and theoretically see a new set of words.
Here is the problem. No changes to the database are happening when the user selects a particular dictionary so the onChange attached to the WordViewModel's getWordsByDictionaryId() method does not trigger. Hence, the LiveData> will not refill.
Code in onCreate() method:
// get all words from WordDao
mWordViewModel = ViewModelProviders.of(MainActivity.this).get(WordViewModel.class);
mWordViewModel.getWordByDictionaryId(dictionaryId).observe(MainActivity.this, new Observer<List<Word>>() {
#Override
public void onChanged(#Nullable List<Word> words) {
mainAdapter.setWords(words);
mAllWords = words;
mainAdapter.notifyDataSetChanged();
}
});
Code in OnOptionsItemSelected() method:
AlertDialog.Builder changeDictionaryDialog = new AlertDialog.Builder(MainActivity.this);
changeDictionaryDialog.setTitle("Select Dictionary:");
LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View view = inflater.inflate(R.layout.alert_dialog_spinner_view, null);
mDictionaryStringList = new ArrayList<>();
mDictionaryStringList = convertToList(mDictionaryList);
final Spinner dictionarySpinner = (Spinner) view.findViewById(R.id.alert_dialog_spinner);
ArrayAdapter<String> spinnerAdapter = new ArrayAdapter(MainActivity.this, android.R.layout.simple_spinner_dropdown_item, mDictionaryStringList);
spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
dictionarySpinner.setAdapter(spinnerAdapter);
dictionarySpinner.setSelection(0);
changeDictionaryDialog.setView(view);
changeDictionaryDialog.setPositiveButton("OK", new DialogInterface.OnClickListener() {
#Override
public void onClick(DialogInterface dialog, int which) {
final String dictionaryValue = dictionarySpinner.getSelectedItem().toString();
for (Dictionary dictionary : mDictionaryList) {
if (dictionary.getDictionaryName().trim().equals(dictionaryValue)) {
dictionaryId = dictionary.getDid();
LiveData<List<Word>> myNewList = mWordViewModel.getWordByDictionaryId(dictionaryId);
List<Word> testList = myNewList.
}
}
}
});
changeDictionaryDialog.setNegativeButton("Cancel", null);
changeDictionaryDialog.create();
changeDictionaryDialog.show();
Code in the WordViewModel:
public LiveData<List<Word>> getWordByDictionaryId(long dictionaryId) {
return mWordRepository.getWordByDictionaryId(dictionaryId);
}
Code in WordDao:
#Query("SELECT * FROM word_table WHERE dictionary_id =:dictionaryId")
public LiveData<List<Word>> getWordsByDictionaryID(long dictionaryId);
Finally, the Word entity:
#Entity(tableName = "word_table",
indices = { #Index(value = "word", unique = true),
#Index("dictionary_id") })
public class Word {
#PrimaryKey(autoGenerate = true)
private long id;
private String word;
#ColumnInfo(name = "dictionary_id")
private long dictionaryId;
#Ignore
public Word(String word, long dictionaryId) {
this.word = word;
this.dictionaryId = dictionaryId;
}
public Word(long id, String word, long dictionaryId) {
this.id = id;
this.word = word;
this.dictionaryId = dictionaryId;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getWord() {
return word;
}
public void setWord(String word) {
this.word = word;
}
public long getDictionaryId() {
return dictionaryId;
}
public void setDictionaryId(long dictionaryId) {
this.dictionaryId = dictionaryId;
}
}
I have tried to setup a background thread (AsyncTask), but the one time that I did, the results came after I needed them so the return value was null. Some SO developers have said don't use Asynctask, but I think the issue would still be timing...
I have also read a little about generating a callback. Would that actually work in this situation? It seems to me like it might put me in the same boat as the background thread.
I have been really struggling trying to learn how to get data out of this "wonderfully simple way to create databases" when it comes to getting data out of the SQLite database via Room persistence via View Model via LiveData via ???whatever comes next???. Is there a "Google" way to do this? Or is this where the RxJava and other libraries come into play?
So my question is just this "exactly how can I get a List of entity objects out of the database if there is no onChange event through live data?" Is there a best practices or right way to call the query without the LiveData wrapper? or a way to access the query directly within the LiveData wrapper without needing the data to change to get things done?
LiveData solution
#Zohaib Amir's comment is correct. You can call LiveData#observe() on anywhere you want as long as you remember to clear the observers. One drawback with this approach is that you have to keep the reference to these observers and LiveData already has Transformations.switchMap() so that you can avoid that.
ViewModel
// Instance variable that stores the current dictionaryId that user wants to see.
private final MutableLiveData<Long> currentDictionaryId = new MutableLiveData<>();
// Instance variable that stores the current list of words. This will automatically change when currentDictionaryId value changes.
private final LiveData<List<Word>> words = Transformations.switchMap(currentDictionaryId, dictionaryId ->
mWordRepository.getWordByDictionaryId(dictionaryId));
// Set new dictionaryId
public void setDictionaryId(long dictionaryId) {
currentDictionaryId.postValue(dictionaryId);
}
// Get LiveData to listen to
public LiveData<List<Word>> getWords() {
return words;
}
Activity
// onCreate()
// get all words from WordDao
mWordViewModel = ViewModelProviders.of(MainActivity.this).get(WordViewModel.class);
mWordViewModel.setDictionaryId(dictionaryId);
mWordViewModel.getWords().observe(MainActivity.this, new Observer<List<Word>>() {
#Override
public void onChanged(#Nullable List<Word> words) {
mainAdapter.setWords(words);
mAllWords = words;
mainAdapter.notifyDataSetChanged();
}
});
// note that the order of calling setDictionaryId and getWords doesn't matter. setDictionaryId is supposed to call at anytime.
// OnOptionsItemSelected
...
changeDictionaryDialog.setView(view);
changeDictionaryDialog.setPositiveButton("OK", new DialogInterface.OnClickListener() {
#Override
public void onClick(DialogInterface dialog, int which) {
final String dictionaryValue = dictionarySpinner.getSelectedItem().toString();
for (Dictionary dictionary : mDictionaryList) {
if (dictionary.getDictionaryName().trim().equals(dictionaryValue)) {
dictionaryId = dictionary.getDid();
mWordViewModel.setDictionaryId(dictionaryId);
break;
}
}
}
});
...
This time I prepared other solutions so that you can compare. Other solutions (almost) do the exact same thing.
Non-reactive solution
You can implement above feature based on callbacks, and this means that you have to manually handle lifecycle changes, notifications, and threading. Also, there is a functional difference that LiveData will automatically notify the observer when there is a change in the db, while this callback design is just one-time look up only.
In this example, we will use Executor to execute tasks in the background thread.
WordDao
#Query("SELECT * FROM word_table WHERE dictionary_id =:dictionaryId")
public List<Word> getWordsByDictionaryID(long dictionaryId); // notice that livedata is gone.
WordRepository
Database access must be done in a background thread. So when we access db, we need to switch to a background thread at some point. In this example, we will switch to the background
thread in this layer.
// This executor is needed to run Runnables on a background thread. In real application, you may
// create this executor outside of this repository and later inject it to this repository.
private final Executor ioExecutor = Executors.newSingleThreadExecutor();
private void getWordByDictionaryId(long dictionaryId, Consumer<List<Word>> callback) {
ioExecutor.execute(() -> {
List<Word> wordList = wordDao.getWordsByDictionaryId(dictionaryId);
callback.accept(wordList);
});
}
WordViewModel
There isn't much ViewModel does in this example. Just pass the parameters to the repository.
public void getWordByDictionaryId(long dictionaryId, Consumer<List<Word>> callback) {
mWordRepository.getWordByDictionaryId(dictionaryId, callback);
}
Activity
Note that Consumer#accept will run on a background thread. Therefore before you do anything with the ui, you need to switch back to the ui thread.
// onCreate
mWordViewModel = ViewModelProviders.of(MainActivity.this).get(WordViewModel.class);
mWordViewModel.getWordByDictionaryId(dictionaryId, words -> {
runOnUiThread(() -> {
if (isFinishing() || isDestroyed()) return; // if the activity is finishing, don't do anything.
mainAdapter.setWords(words);
mAllWords = words;
mainAdapter.notifyDataSetChanged();
});
});
// onOptionsItemSelected
changeDictionaryDialog.setPositiveButton("OK", new DialogInterface.OnClickListener() {
#Override
public void onClick(DialogInterface dialog, int which) {
final String dictionaryValue = dictionarySpinner.getSelectedItem().toString();
for (Dictionary dictionary : mDictionaryList) {
if (dictionary.getDictionaryName().trim().equals(dictionaryValue)) {
dictionaryId = dictionary.getDid();
mWordViewModel.getWordByDictionaryId(dictionaryId, words -> {
runOnUiThread(() -> {
if (isFinishing() || isDestroyed()) return; // if the activity is finishing, don't do anything.
mainAdapter.setWords(words);
mAllWords = words;
mainAdapter.notifyDataSetChanged();
});
});
break;
}
}
}
});
RxJava
RxJava solution will look much like the LiveData solution. It has many advantages over LiveData: it has lots of observables and operators to work with. These advantages are more apparent in more complex applications, where you need to do conditional or delayed async tasks, periodic tasks, multiple requests chained one after the other, error handlings, etc.
WordDao
#Query("SELECT * FROM word_table WHERE dictionary_id =:dictionaryId")
public Flowable<List<Word>> getWordsByDictionaryID(long dictionaryId);
ViewModel
private final BehaviorProcessor<Long> currentDictionaryId = BehaviorProcessor.create();
// Set new dictionaryId
public void setDictionaryId(long dictionaryId) {
currentDictionaryId.onNext(dictionaryId);
}
// Get Flowable to listen to
public Flowable<List<Word>> getWords() {
return currentDictionaryId.switchMap(dictionaryId -> mWordRepository
.getWordByDictionaryId(dictionaryId)
// add ".take(1)" if you need one-time look up.
.subscribeOn(Schedulers.io()));
}
Activity
private fianl CompositeDisposable disposables = new CompositeDisposable();
// onCreate()
mWordViewModel = ViewModelProviders.of(MainActivity.this).get(WordViewModel.class);
mWordViewModel.setDictionaryId(dictionaryId);
disposables.add(mWordViewModel.getWords()
.observeOn(AndroidSchedulers.mainThread()))
.subscribe(words -> {
mainAdapter.setWords(words);
mAllWords = words;
mainAdapter.notifyDataSetChanged();
});
// onOptionsItemSelected()
changeDictionaryDialog.setPositiveButton("OK", new DialogInterface.OnClickListener() {
#Override
public void onClick(DialogInterface dialog, int which) {
final String dictionaryValue = dictionarySpinner.getSelectedItem().toString();
for (Dictionary dictionary : mDictionaryList) {
if (dictionary.getDictionaryName().trim().equals(dictionaryValue)) {
dictionaryId = dictionary.getDid();
mWordViewModel.setDictionaryId(dictionaryId);
break;
}
}
}
});
// onDestroy()
disposables.clear();
I have a Dictionary app where I want to assign existing synonyms to a word in the dictionary. To accomplish this I am using is using a M:N relationship between the word and synonym tables.
Entities:
#Entity(tableName = "word_table",
indices = #Index(value = "word", unique = true))
public class Word {
#PrimaryKey(autoGenerate = true)
private long id;
private String word;
#Ignore
public Word(String word) {
this.word = word;
}
public Word(long id, String word) {
this.id = id;
this.word = word;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getWord() {
return word;
}
public void setWord(String word) {
this.word = word;
}
}
#Entity(tableName = "synonym_table")
public class Synonym {
#PrimaryKey(autoGenerate = true)
private long sid;
private String synonym;
#Ignore
public Synonym(String synonym) {
this.synonym = synonym;
}
public Synonym(long sid, String synonym) {
this.sid = sid;
this.synonym = synonym;
}
public long getSid() {
return sid;
}
public void setSid(long id) {
this.sid = sid;
}
public String getSynonym() {
return synonym;
}
public void setSynonym(String synonym) {
this.synonym = synonym;
}
}
#Entity(tableName = "word_synonym_join_table",
primaryKeys= {"word_id" , "synonym_id"},
foreignKeys = {#ForeignKey(entity = Word.class, parentColumns = "id", childColumns = "word_id"),
#ForeignKey(entity = Synonym.class, parentColumns = "sid", childColumns = "synonym_id")})
public class WordSynonymJoin {
#ColumnInfo(name = "word_id")
private long wordId;
#ColumnInfo(name = "synonym_id")
private long synonymId;
public WordSynonymJoin(long wordId, long synonymId) {
this.wordId = wordId;
this.synonymId = synonymId;
}
public long getWordId() {
return wordId;
}
public void setWordId(long wordId) {
this.wordId = wordId;
}
public long getSynonymId() {
return synonymId;
}
public void setSynonymId(long synonymId) {
this.synonymId = synonymId;
}
}
To retrieve the data for the Word and associated Synonyms, I created a POJO called WordWithSynonyms.
public class WordWithSynonyms {
#Embedded
public Word word;
#Embedded
public WordSynonymJoin wordSynonymJoin;
}
The Daos are as follows:
#Dao
public interface WordDao {
#Query("SELECT * FROM word_table")
public LiveData<List<Word>> getAllWords();
#Query("SELECT * FROM word_table WHERE id =:wordId")
public LiveData<List<Word>> getWordById(long wordId);
#Query("SELECT * from word_table WHERE word =:value")
public LiveData<List<Word>> getWordByValue(String value);
#Insert
public long insert(Word word);
#Delete
public void delete(Word word);
#Update
public void update(Word word);
#Query("DELETE FROM word_table")
public void deleteAll();
}
#Dao
public interface SynonymDao {
#Query("SELECT * FROM synonym_table")
public LiveData<List<Synonym>> getAllSynonyms();
#Query("SELECT * FROM synonym_table WHERE synonym =:value")
public LiveData<List<Synonym>> getSynonymByValue(String value);
#Insert
public void insert(Synonym synonym);
#Delete
public void delete(Synonym synonym);
#Query("DELETE FROM synonym_table")
public void deleteAll();
}
#Dao
public interface WordSynonymJoinDao {
#Query("SELECT * FROM word_table INNER JOIN word_synonym_join_table " +
"ON word_table.id = word_synonym_join_table.word_id " +
"WHERE word_synonym_join_table.synonym_id =:synonymId")
public LiveData<List<WordWithSynonyms>> getWordsBySynonym(long synonymId);
#Query("SELECT * FROM synonym_table INNER JOIN word_synonym_join_table " +
"ON synonym_table.sid = word_synonym_join_table.synonym_id " +
"WHERE word_synonym_join_table.word_id =:wordId")
public LiveData<List<SynonymWithWords>> getSynonymsByWord(long wordId);
#Query("SELECT * FROM synonym_table INNER JOIN word_synonym_join_table " +
"ON synonym_table.sid = word_synonym_join_table.synonym_id " +
"WHERE word_synonym_join_table.word_id !=:wordId")
public LiveData<List<SynonymWithWords>> getSynonymsByNotWord(long wordId);
#Insert
public void insert(WordSynonymJoin wordSynonymJoin);
#Delete
public void delete(WordSynonymJoin wordSynonymJoin);
#Query("DELETE FROM word_synonym_join_table")
public void deleteAll();
}
When I arrive on the Synonyms Activity, i pass the wordId to retrieve the current synonyms for that word through a ViewModel observer.
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_synonym);
Intent intent = getIntent();
wordId = Long.parseLong(intent.getStringExtra(EXTRA_WORD_ID));
//SynonymViewModel synonymViewModel = ViewModelProviders.of(this).get(SynonymViewModel.class);
WordSynonymJoinViewModel wordSynonymJoinViewModel = ViewModelProviders.of(this).get(WordSynonymJoinViewModel.class);
//synonymAdapter = new SynonymListAdapter(this);
synonymAdapter = new SynonymWithWordListAdapter(this);
synonynRecyclerView = findViewById(R.id.recycler_view_syonym);
if (wordId != 0) {
wordSynonymJoinViewModel.getSynonymsByWord(wordId).observe(SynonymActivity.this, new Observer<List<SynonymWithWords>>() {
#Override
public void onChanged(#Nullable List<SynonymWithWords> synonymWithWords) {
synonymAdapter.setSynonyms(synonymWithWords);
synonymAdapter.notifyDataSetChanged();
}
});
}
synonynRecyclerView.setAdapter(synonymAdapter);
synonynRecyclerView.setLayoutManager(new LinearLayoutManager(SynonymActivity.this));
}
I then give the user the opportunity to associate an existing, unassigned synonym from the Synonym table to the Word table.
I retrieve the unused and available Synonyms through a separate ViewModel observer inside of an AlertDialog which uses a spinner to display them via the WordSynonymJoin table using another ViewModel observer.
Finally, inside of that ViewModel observer when the user clicks the OK button on the AlertDialog, a third VieModel observer is ran to do the actual insertion into the WordSynonymJoin table.
case R.id.synonym_assign_synonym:
final WordSynonymJoinViewModel wordSynonymJoinViewModel = ViewModelProviders.of(SynonymActivity.this).get(WordSynonymJoinViewModel.class);
wordSynonymJoinViewModel.getSynonymsByNotWord(wordId).observe(SynonymActivity.this, new Observer<List<SynonymWithWords>>() {
#Override
public void onChanged(#Nullable List<SynonymWithWords> synonymWithWords) {
List<String> synonymsNotAssignList = new ArrayList<>();
for (SynonymWithWords sww : synonymWithWords)
synonymsNotAssignList.add(sww.synonym.getSynonym());
AlertDialog.Builder assignSynonymDialog = new AlertDialog.Builder(SynonymActivity.this);
assignSynonymDialog.setTitle("Select New Category:");
LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View view = inflater.inflate(R.layout.alert_dialog_spinner_view, null);
final Spinner synonymSpinner = (Spinner) view.findViewById(R.id.alert_dialog_spinner);
final SynonymViewModel synonymViewModel = ViewModelProviders.of(SynonymActivity.this).get(SynonymViewModel.class);
ArrayAdapter<String> spinnerAdapter = new ArrayAdapter(SynonymActivity.this, android.R.layout.simple_spinner_dropdown_item, synonymsNotAssignList);
spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
synonymSpinner.setAdapter(spinnerAdapter);
synonymSpinner.setSelection(synonymId);
assignSynonymDialog.setView(view);
assignSynonymDialog.setPositiveButton("OK", new DialogInterface.OnClickListener() {
#Override
public void onClick(DialogInterface dialog, int which) {
final String synonymValue = synonymSpinner.getSelectedItem().toString();
// get new synonym id
synonymViewModel.getSynonymByValue(synonymValue).observe(SynonymActivity.this, new Observer<List<Synonym>>() {
#Override
public void onChanged(#Nullable List<Synonym> synonyms) {
long id = 0;
if (!synonyms.get(0).getSynonym().equals(synonymValue)) {
if (synonyms.size() > 1)
Toast.makeText(SynonymActivity.this, "Query found " + synonyms.size() + " which is more than the one expected.", Toast.LENGTH_SHORT).show();
} else {
id = synonyms.get(0).getSid();
}
WordSynonymJoinViewModel wordSynonymJoinViewModel = ViewModelProviders.of(SynonymActivity.this).get(WordSynonymJoinViewModel.class);
wordSynonymJoinViewModel.insert(new WordSynonymJoin(wordId, id));
}
});
}
});
assignSynonymDialog.setNegativeButton("Cancel", null);
assignSynonymDialog.create();
assignSynonymDialog.show();
}
});
return true;
On the first pass, all seems to work well. However, on successive passes where the user continues to add new synonyms to the word, it takes that many clicks on the cancel button of the AlertDialog to exit after each synonym added. 2 synonyms added, 2 click on the cancel to get back to main Activity. 3 synonyms added, 3 clicks on the cancel to remove the AlertDialog.
I am very new to this whole concept of MVVM and Room persistence so I know there will be issues. Here is the code for the AlertDialog for adding existing, unassigned synonyms to the current word.
I don't like how much code is being used for this, but i have not been able to word my searches so that I can find ways around it.
My questions are:
Why is the code cycling +1 every time I enter associate new synonym to the word? Am I suppose to be clearing something out.
Is this coding even remotely right?
This seems like an awful lot of work to accomplish something so seemingly small. I think I have missed something. Have I made this abnormally complicated?
What am I missing that this code looks so cumbersome and unwieldy?
It seems a very cumbersome way to retrieve values and I don't really think i need to observe every query that I ran above. Maybe I am wrong.
Is there a direction of study that will help me understand this better?
Could this be where the Rx Java comes in?
I can certainly provide more code as needed.
Any help would be appreciated.
TRDL: Don't call .observe outside of ON_CREATE state.
You made a LiveData mistake... but you are not alone! That mistake is the most common LiveData mistake on StackOverflow: calling .observe outside of Activity#onCreate(). This includes calling .observe in a click listener, on onResume, broadcast receiver, etc.
The problem I see in most people who uses LivedData for the first time is that they treat LiveData just like a call back, when they are not. LiveData is a stream. LiveData does not notify just one time. The Observers attached to the LiveData will continue to be notified until they are unsubscribed. Also, It is meant to be subscribed at the beginning of the life-cycle, (e.g. Activity#onCreate or Fragment#onViewCreated) and unsubscribed at the end of the life-cycle. LiveData automatically handles the unsubscription part, so all you need to make sure is to subscribe in onCreate.
The fundamental reason you are keep getting +1 Dialog is that the previous observer is not dead and you are keep adding a new subscription to the database each time you repeat the same thing. Try rotating the phone and see if the number of dialog resets back to 1. That's because all of the previous observers are unsubscribed when you rotate the screen and activity is recreated.
Maybe you could call isShowing() and see if any dialog is open, as suggested in another answer. However, doing so is just a work around. What if it was a Toast or something else that you can't check? Besides, you are lucky that you could easily spot this bug. You might be having this duplicate observer bug some place else that is not visually noticeable.
So I think you already know how to use LiveData, but it is just that you need to know how to implement reactive pattern correctly. It would be too much to explain in one writing but let me give you a simple example:
Lets say you have a button that when you press, you fetch some data from DB. In a callback-like design you often call some function in ViewModel and pass a callback instance. For example you have this:
//ViewModel
void getSynonymsByNotWord(WordSynonymJoin word, Callback callback) { ... }
//Activity
void onClick(View v) {
wordSynonymJoinViewModel.changeCurrentSysnonymsByNotWord(wordId, callback);
}
You perform an action to ViewModel and you receive the response through callback. This is perfectly fine for callback. However, you can't do the same with LiveData. When using LiveData, View layer don't expect that there will be a response for each of the action. Instead, View layer should always blindly listen to the response, even before the button is clicked.
//ViewModel
private MutableLiveData wordQuery;
private Livedata synonymsByNotWord = Transformations.switchMap(wordQuery, word -> {
return repository.getSynonymsByWord(word);
});
LiveData getCurrentSynonymsByNotWord() {
return synonymsByNotWord;
}
void changeCurrentSynonymsByNotWord(WordSynonymJoin word) {
wordQuery.postValue(word);
}
//Activity
void onCreate() {
wordSynonymJoinViewModel.getCurrentSynonymsByNotWord().observe(...);
}
void onClick(View v) {
wordSynonymJoinViewModel.changeCurrentSynonymsByNotWord(wordId);
}
And also it is okay to, but you normally don't get ViewModel from ViewModelProviders every time you need a view model. You should just get one view model at onCreate, save it as an activity instance variable, and use the same instance in the rest of the activity.
Here:
wordSynonymJoinViewModel.getSynonymsByNotWord(wordId).observe(SynonymActivity.this, new Observer<List<SynonymWithWords>>() {
you are monitoring for synonyms, but inside of the observing, you show a dialog and allow more synonyms to be added. Everytime a new synonym is added, it creates a new AlertDialog.
So this is why you have to press cancel on each dialog.
To fix that, you can assign your AlertDialog to a field and use the isShowing() method to decide if you want to show another dialog (i.e. don't show another one if one is already showing.
https://developer.android.com/reference/android/app/Dialog.html#isShowing()
As for all your other questions, I'm sorry it's a bit too much for me to unpack.
I can share my thoughts on how I would do this though:
I want to assign existing synonyms to a word in the dictionary.
Perhaps forget the database to start with and create an in memory solution.
Then later you can change this to be persisted.
In memory the structure looks like a Hashtable of Dictionary words and Synonym lookups Map<String, List<String>>.
This Map would be in a class called Repository that exposes someway for you to observe and update it (RxJava Observable) or LiveData like you have already.
Your Fragment would observe this Map displaying it in a RecyclerView using MVVM or MVP whatever you want.
You have a clicklistener on each row of the RecyclerView to add a new synonym. On click opens the dialog (or a new activity/fragment). After the user types the synonym you will save this through the repository to your Map - and therefore the original observer will update the RecyclerView.
You should not get in a loop state of opening multiple dialogs :/
Hope that helps, tbh it sounds like you are on the right track and just need to work at it a bit more.
I have LiveData for Books in ViewModel's constructor:
LiveData<List<Book>> books;
public MyViewModel(#NonNull Application application) {
super(application);
books = bookRepository.getBooks();
}
When user creates new book from UI, I want attribute book_order to be filled with incremented maximum of book_order of other books. To better describe what I want, see following preudocode:
book.book_order = max(books.book_order) + 1;
So when there are three books with book_order 1, 2, 3 respectively, new book would have this attribute set to 4.
Question is, how can I do this with LiveData in ViewModel? I tried using Transformations.map to the new LiveData, but this approach is not working at all, bookMax seems to be null.
public void insertBook(Book book) {
LiveData<Integer> bookMax = Transformations.map(books,
list -> {
int value = 0;
for(Book b: list) {
if (value < b.getBookOrder()) {
value = b.getBookOrder();
}
}
}
);
book.setBookOrder(bookMax + 1)
bookRepository.update(book);
}
Any ideas how to set incremented maximum to the new book? It can be another approach than the one described here. ViewModel was created to separate app logic from UI. However it does not seem to do that in this case, because if I want to observe value, I need to be in Activity. Also, I did not find any alternative how to do this kind of getting one value from DB. Any help appreciated.
Note that your books are livedata, thus may change its value from time to time.
Whereis your bookMax is a single value that should be calculated at the moment of insertion.
To insert you need:
get the current books list
then calculate bookMax
then actually insert.
val bookList: List<Book> = books.value // current value. may be null!
val bookMax: Int = bookList.maxBy { it.order }.order // find max order
// insert
val newBooks = arrayListOf(bookList)
newBooks.add(newBook)
books.value = newBooks // update your livedata
EDIT Here is Java code
// get current value. may be null!
List<Book> bookList = books.getValue();
// so we better handle it early
if (bookList == null) {
bookList = new ArrayList<>();
}
// calculate max order
int maxOrder = -1;
for (Book book : bookList) {
if (maxOrder < book.order) {
maxOrder = book.order;
}
}
// create new book
Book newBook = new Book();
newBook.order = maxOrder + 1;
// add book to the list
bookList.add(newBook);
// do with new list whatever you want
// for example, you can update live data (if it is a MutableLiveData)
books.setValue(bookList);
Using LiveData will not help in your scenario.
LiveData in DAO gets executed on a different thread and the new value is posted in observer code or in your case Transformation.map() callback. So you need to access book.id inside Transformation.map().
However, if you insert book in Transformation.map(), it would trigger an infinite loop since on every table entry update Transformation.map() would be called as LiveData> would change.
So, for your case:
Expose a method which exposes last book id
Insert a new entry.
Add a LiveData> to receive an update and display in UI.
Instead of taking all books for finding max book order, you should make a method in your repository that will provide you max number of book_order from db.
Something like below pseudo code :
int maxOrder = bookRepository.getMaxBookOrder();
Now, all you need to do is while inserting new book, you can use that maxOrder variable to incremental purpose.
So, your insert method will be like :
public void insertBook(Book book) {
int maxOrder = bookRepository.getMaxBookOrder();
book.setBookOrder(maxOrder + 1)
bookRepository.update(book);
}
Here, assuming that you're using ROOM for persisting database, this is the query that can help you get your maximum book_order:
SELECT MAX(book_order) FROM Book
If ROOM isn't your case then, you can do with another approach :
We first retrieve list using repository method and then find maximum from it like below pseudo :
List<Book> books = bookRepository.getBooks().getValue(); // Assuming getBooks() returns LiveData
Then find max from it and then increment it by one :
public void insertBook(Book book) {
List<Book> books = bookRepository.getBooks().getValue();
// calculating max order using loop
int maxOrder = -1;
for (Book book : books) {
if (maxOrder < book.order) {
maxOrder = book.order;
}
}
book.setBookOrder(maxOrder + 1)
bookRepository.update(book);
}
Even you can move this code of finding maximum to repository method as mentioned earlier like :
public int getMaxBookOrder() {
List<Book> books = getBooks().getValue();
// calculating max order using loop
int maxOrder = -1;
for (Book book : books) {
if (maxOrder < book.order) {
maxOrder = book.order;
}
}
return maxOrder;
}
If you really want to do it with a LiveData you can create a custom one:
class BooksLiveData(list: List<Book>) : LiveData<List<Book>>() {
val initialList: MutableList<Book> = list.toMutableList()
fun addBook(book: Book) {
with(initialList) {
// Assuming your list is ordered
add(book.copy(last().bookOrder + 1))
}
postValue(initialList)
}
}
Then you can just create it and use:
val data = bookRepository.getBooks() // Make getBooks() return BooksLiveData now
data.addBook(userCreatedBook) // This'll trigger observers as well
You can still observe this live data, since it's posting initialList when a book is added, it'll notify observers. You can change it more, for example, to return the book that's added etc.
Side note: It might be better to extend from MutableLiveData instead, since LiveData is not supposed to update its value but internally you're posting something so it might be confusing.
Approach 1:
You can use an Auto Increment field or the PrimaryKey such as an id for the book_order's functionality. You can even name it book_order if you want to. Make your Model or Entity class Like:
public class Book{
#PrimaryKey(autoGenerate = true)
private int book_order;
//other data members, constructors and methods
}
So that, the the book_order gets incremented on each Book added to the database.
Next you can have your ViewModel classs like:
public class MyViewModel extends AndroidViewModel {
private BookRepository bookRepository;
LiveData<List<Book>> books;
public MyViewModel(#NonNull Application application) {
super(application);
bookRepository = AppRepository.getInstance(application.getApplicationContext());
books = bookRepository.getBooks();
}
}
Now you can subscribe your list Activity to this ViewModel (ie. make your activity observe the ViewModel) by putting the call to following method in your activity's onCreate():
private void initViewModel() {
final Observer<List<Book>> bookObserver= new Observer<List<Book>>() {
#Override
public void onChanged(#Nullable List<Book> books) {
bookList.clear();
bookList.addAll(books);
if (mAdapter == null) {
mAdapter = new BooksAdapter(bookList, YourActivity.this);
mRecyclerView.setAdapter(mAdapter);
} else {
mAdapter.notifyDataSetChanged();
}
}
};
mViewModel = ViewModelProviders.of(this)
.get(MyViewModel.class);
mViewModel.mBooks.observe(this, notesObserver); //mBooks is member variable in ViewModel class
}
Doing these things, you will be able to receive updates, ie. whenever a Book is added to your database by the user, the List/ Recycler view should automatically display the newly added Book.
Approach 2:
If this is not what you have wanted at all and you only want to find the latest added book's order, you can skip the third code block completely and use the following in your Dao:
#Query("SELECT COUNT(*) FROM books")
int getCount();
which gives the total number of books ie. rows in the books table, which you can then call from your repository, which in turn can be called from your ViewModel which in turn can be called from the Activity.
Approach 3:
If you want the book_order which I think is the latest number of books in the database after a new book is added, you can use Approach 1 which gives you the List of Book in the ViewModel. You can then get the number of books from the booklist count.
Important!
either way you would want to edit your insertBook() method in your editor or newBook ViewModel and make it something like:
public void insertBook(Book book) {
Book book= mLiveBook.getValue(); //declare mLiveBook as MutableLiveData<Book> in this ViewModel
//maybe put some validation here or some other logic
mRepository.insertBook(book);
}
and in your Repository corresponding insert would look like:
public void insertBook(final Book book) {
executor.execute(new Runnable() {
#Override
public void run() {
mDb.bookDao().insertBook(book);
}
});
}
and corresponding Dao method:
#Insert(onConflict = OnConflictStrategy.REPLACE)
void insertBook(Book book);
In my app I am trying to use MediatorLiveData to listen to the changes to a livedata. Since DB operations are involved I use an executor service like this.
MediatorLiveData<Content> mediatorLiveData = new MediatorLiveData<>();
appExecutors.diskIO().execute(() -> {
long id = contentDao.insert(content);
Log.i("LIVE", id + "");
LiveData<Content> content = contentDao.getContentById(id);
mediatorLiveData.addSource(content, new Observer<Content>() {
#Override
public void onChanged(#Nullable Content content) {
Log.i("LIVE", "FIRED");
}
});
});
First I try to insert a new content object into the db. I get the id of the inserted object which I log in the next line. I get some id which is good. After this, I use the id to query for the same object. The query returns a LiveData. (If I use content.getValue() at this time, I get null.)
Then I listen to changes in this liveData using a MediatorLiveData. Unfortunately, the onChange method of the mediatorLiveData is never fired. Thus the Log is not printed too.
This is my content dao class
#Dao
public interface ContentDao {
#Insert
long insert(Content content);
#Query("SELECT * FROM my_table WHERE id = :id")
LiveData<Content> getContentById(long id);
}
I can't understand what I am doing wrong. Can someone please help. Thanks!!
Edit: To clarify, this is how the code looks.
return new NetworkBoundResource<Content, CreateContent>(appExecutors) {
#Override
protected void saveCallResult(#NonNull CreateContent item) {
//Something
}
#Override
protected boolean shouldCall(#Nullable Content data) {
//Something;
}
#Override
protected LiveData<Content> createDbCall() {
MediatorLiveData<Content> mediatorLiveData = new MediatorLiveData<>();
appExecutors.diskIO().execute(() -> {
long id = contentDao.insert(content);
Log.i("LIVE", id + "");
LiveData<Content> content = contentDao.getContentById(id);
mediatorLiveData.addSource(content, new Observer<Content>() {
#Override
public void onChanged(#Nullable Content c) {
Log.i("LIVE", "FIRED");
mediatorLiveData.removeSource(content);
mediatorLiveData.postValue(c);
}
});
});
return mediatorLiveData;
}
#NonNull
#Override
protected LiveData<ApiResponse<CreateContent>> createCall() {
//Something
}
}.asLiveData();
The value is returned to the constructor.
#MainThread
public NetworkBoundResource(AppExecutors appExecutors) {
this.appExecutors = appExecutors;
result.setValue(Resource.loading(null));
//TODO:: Add method to check if data should be saved. This should apply for search data.
LiveData<ResultType> dbSource = createDbCall();
result.addSource(dbSource, data -> {
result.removeSource(dbSource);
if (shouldCall(data)) {
fetchFromNetwork(dbSource);
} else {
result.addSource(dbSource, newData -> setValue(Resource.success(newData)));
}
});
}
As discussed you need to make sure the mediatorLiveData has an active observer attached.
If you take a look at the addSource method it checks whether any active observers are attached before subscribing to the source.
https://github.com/aosp-mirror/platform_frameworks_support/blob/d79202da157cdd94c2d0c0b6ee57170a97d12c93/lifecycle/livedata/src/main/java/androidx/lifecycle/MediatorLiveData.java#L95
In case anyone is re initializing a mediator live data, the old object only will be observed, new object will not be observed.
That is , dont do this:
Observe
myViewModel.observe(....)
Trying to allocate new memory to mediator
myMediatorObj = new MediatorLiveData<>(); //this can be the issue. Try removing if you have any lines like this.
//after this point,anything set to the object myMediatorObj will not be observed
In case you are trying to reset the data, pass in some data that signals null/empty/rest.
I'm currently refactoring legacy code to use Android Architecture Components and set up a room db and volley requests within a kind of repository pattern.
So the presentation/domain layer asks the repository to get LiveData-Objects to observe or tell him to synchronize with the server, after which old db entries are deleted and all current ones refetched from the server.
I've written tests for the synchronization part, so I'm sure, that the objects get fetched and inserted to the database correctly. But when writing a test to observe the entries of that db table (and test if the objects were saved correctly with everything there needs to be done before putting them into db) the LiveData> I'm observing, doesn't get triggered.
In the following snippet you can assume, that the synchronizeFormsWithServer(...) method does work correctly and is performing database operations asynchronously. It contains operations which deletes all Form-Objects from the db which are not present in the list of Forms fetched from the server and inserts all new ones. Since at the start of the test the database is empty this shouldn't matter that much
The test in which the observer doesn't get triggered:
#Test
public void shouldSaveFormsFromServerIntoDb() throws Exception
{
Lifecycle lifecycle = Mockito.mock(Lifecycle.class);
when(lifecycle.getCurrentState()).thenReturn(Lifecycle.State.RESUMED);
LifecycleOwner owner = Mockito.mock(LifecycleOwner.class);
when(owner.getLifecycle()).thenReturn(lifecycle);
final CountDownLatch l = new CountDownLatch(19);
formRepository.allForms().observe(owner, formList ->
{
if (formList != null && formList.isEmpty())
{
for (Form form : formList)
{
testForm(form);
l.countDown();
}
}
});
formRepository.synchronizeFormsWithServer(owner);
l.await(2, TimeUnit.MINUTES);
assertEquals(0, l.getCount());
}
The FormRepository code:
#Override
public LiveData<List<Form>> allForms()
{
return formDatastore.getAllForms();
}
The datastore:
#Override
public LiveData<List<Form>> getAllForms()
{
return database.formDao().getAllForms();
}
The formDao code (database is implemented how you'd expect it from room):
#Query("SELECT * FROM form")
LiveData<List<Form>> getAllForms();
It may very well be, that I didn't understand something about the LiveData-Components, because this is my first time using them, so maybe I got something fundamentally wrong.
Every bit of help is very much appreciated :)
PS: I stumbled across THIS post, which discusses a similar issue, but since I'm currently not using DI at all and just use a single instance of the formrepository (which has only one instance of formDao associated) I don't think it's the same problem.
Ok, so I found the solution, although I don't know, why it behaves that way.
Remember when I said "don't worry about the synchronize method"? Well... turns out there were a couple of things wrong with it, which delayed the solution further.
I think the most important error there was the method to update the objects in the database when the network response came in.
I used to call
#Update
void update(Form form)
in the dao, which for unknown reasons doesn't trigger the LiveData-Observer. So I changed it to
#Insert(onConflict = OnConflictStrategy.REPLACE)
void insert(Form form);
After doing this I could get the Form-LiveData from my repository as easy as
LiveData<List<Form>> liveData = formRepository.allForms();
Then subscribe to it as usual.
The previously failed test looks like this now:
#Test
public void shouldSaveFormsFromServerIntoDb() throws Exception
{
Lifecycle lifecycle = Mockito.mock(Lifecycle.class);
when(lifecycle.getCurrentState()).thenReturn(Lifecycle.State.RESUMED);
LifecycleOwner owner = Mockito.mock(LifecycleOwner.class);
when(owner.getLifecycle()).thenReturn(lifecycle);
final CountDownLatch l = new CountDownLatch(19);
final SortedList<Form> sortedForms = new SortedList<Form>(Form.class, new SortedList.Callback<Form>()
{
#Override
public int compare(Form o1, Form o2)
{
return o1.getUniqueId().compareTo(o2.getUniqueId());
}
#Override
public void onChanged(int position, int count)
{
Log.d(LOG_TAG, "onChanged: Form at position " + position + " has changed. Count is " + count);
for (int i = 0; i < count; i++)
{
l.countDown();
}
}
#Override
public boolean areContentsTheSame(Form oldItem, Form newItem)
{
return (oldItem.getContent() != null && newItem.getContent() != null && oldItem.getContent().equals(newItem.getContent())) || oldItem.getContent() == null && newItem.getContent() == null;
}
#Override
public boolean areItemsTheSame(Form item1, Form item2)
{
return item1.getUniqueId().equals(item2.getUniqueId());
}
#Override
public void onInserted(int position, int count)
{
}
#Override
public void onRemoved(int position, int count)
{
}
#Override
public void onMoved(int fromPosition, int toPosition)
{
}
});
LiveData<List<Form>> ld = formRepository.allForms();
ld.observe(owner, formList ->
{
if (formList != null && !formList.isEmpty())
{
Log.d(LOG_TAG, "shouldSaveFormsFromServerIntoDb: List contains " + sortedForms.size() + " Forms");
sortedForms.addAll(formList);
}
});
formRepository.synchronizeFormsWithServer(owner);
l.await(2, TimeUnit.MINUTES);
assertEquals(0, l.getCount());
}
I know that exactly 19 Forms will get fetched from the server and then every Form will get changed once (first time I load a list containing all Forms with reduced data, and the second time I load every item from the server again replacing the old value in the db with the new value with more data).
I don't know if this will help you #joao86 but maybe you have a similar issue. If so, please make sure to comment here :)
You have to use the same database instance at all places.
=> Use a singleton for that
I had a similar issue with yours --> LiveData is not updating its value after first call
Instead of using LiveData use MutableLiveData and pass the MutableLiveData<List<Form>> object to the Repository and do setValue or postValue of the new content of the list.
From my experience with this, which is not much, apparently the observer is connected to object you first assign it too, and every change must be done to that object.