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.
Related
I am getting User from Room Database by returning Single<UserMinimal>. Once I get the data I want to show it through LiveData observing, and I am using LiveDataReactiveStreams.fromPublisher() method, but looks like when .setValue() is called, all attributes of the received user are null. Don't know if needs .subscribe() or whatever.
LiveData<UserMinimal> userLiveData = LiveDataReactiveStreams
.fromPublisher(mRepository.getUser().toFlowable());
user = new MediatorLiveData<>();
user.addSource(userLiveData, minimalUser -> {
user.postValue(minimalUser);
user.removeSource(userLiveData);
});
public Single<MenuRepository.UserMinimal> getUser(){
return dataSource.getUser()
.onErrorReturnItem(new MenuRepository.UserMinimal())
.doOnError(t -> Timber.e(t))
.subscribeOn(Schedulers.io());
}
Looks like the access to database is successfully working because if I use MutableLiveData instead of MeadiatorLiveData, it does work:
user = new MutableLiveData<>();
disposable.add(mRepository.getUser().subscribe(new Consumer<UserMinimal>() {
#Override
public void accept(UserMinimal userMinimal) throws Throwable {
user.postValue(userMinimal);
}
}));
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
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 am unable to get a LiveData ArrayList from a Room database but I am able to retrieve a standard ArrayList and cannot figure out why.
I have run this code in debug mode and the ArrayList returns a size of 4, which it should. The LiveData ArrayList, when get value is used returns null. I have run the LiveData query both within an executor and outside of the executor and it returns null.
Declarations
public LiveData<List<CourseEntity>> courseEntities;
private List<CourseEntity> courseData = new ArrayList<>();
Code outside of executor
public void loadData(final int termId) {
courseEntities = courseRepository.getCourseByTermId(termId);
courseData = courseEntities.getValue();
}
Code inside executor
public void loadData(final int termId) {
executor.execute(new Runnable() {
#Override
public void run() {
courseEntities = courseRepository.getCourseByTermId(termId);
courseData = courseEntities.getValue();
}
});
}
Code using just an ArrayList
public void loadData(final int termId) {
executor.execute(new Runnable() {
#Override
public void run() {
courseData = courseRepository.getCourseByTerm(termId);
}
});
}
Queries from Dao
#Query("SELECT * FROM course " +
"WHERE term_id = :termIdSelected ORDER BY course_start" )
LiveData<List<CourseEntity>> getCourseByTermId(int termIdSelected);
#Query("SELECT * FROM course WHERE term_id = :termIdSelected ORDER BY course_start")
List<CourseEntity> getCourseByTerm(int termIdSelected);
This produces a null value for the LiveData instead of a value of 4 like the plain ArrayList produces. The only difference being the LiveData wrapper for the result. Any wisdom someone can share would be most appreciated.
When you have a Room #Dao return a LiveData (or an RxJava type like Observable or Single), the generated implementation will do the actual work on a background thread. So, when getCourseByTermId() returns, the work will not yet have begun, so the LiveData will not have results yet.
Reactive types, like LiveData, are meant to be observed. So, your activity/fragment/whatever would observe() the LiveData and react to the result when it is delivered.
I am setting a room query in onCreate() which returns a live data instance that I observe in the following.
viewModel.setmQueryMediaList(null, MediaUtils.DATE_TAKEN_KEY, MediaUtils.DATE_TAKEN_KEY);
viewModel.getmQueryMediaList().observe(MainActivity.this, new Observer<List<MediaStoreData>>() {
#Override
public void onChanged(#Nullable List<MediaStoreData> mediaStoreDataList) {
List<MediaStoreData> sectionedMediaStoreDataList = MediaUtils.getSectionedList(getBaseContext(), mediaStoreDataList, MediaUtils.ORDER_BY_MONTH );
mRecyclerViewAdapter.submitList(sectionedMediaStoreDataList);
Log.e("MediaDatabase", "something changed! Size:" + (mediaStoreDataList != null ? mediaStoreDataList.size() : 0));
}
});
onClick() I want to change the room query and I assumed that the observer triggers on that change but it doesn't.
#Override
public void onAlbumClicked(AlbumData albumData, TextView titleView) {
viewModel.setmQueryMediaList(albumData.getDirectory_path(), null, MediaUtils.DATE_TAKEN_KEY);
mSlidingUpPanelLayout.setPanelState(SlidingUpPanelLayout.PanelState.EXPANDED);
}
This is in my ViewModel class
public void setmQueryMediaList(String directory_path, String sectionOrder, String sortOrder) {
if(directory_path != null){
this.mQueryMediaList = mediaDatabase.mediaDao().getAllByName();
} else {
this.mQueryMediaList = mediaDatabase.mediaDao().getAllByDate();
}
}
public LiveData<List<MediaStoreData>> getmQueryMediaList(){
return mQueryMediaList;
}
Any ideas what I am doing wrong?
Your problem is that you replace the Object which has the Observer attached.
That means that you Obserers are not attached to your new QueryMediaList, so you would need to reset them every time you change the Query.
To do that you could extract your Observer into its own variable, and then reatach that variable to the list, after you changed the query.
The correct way to do this would be to put the directoryPath in a MutableLiveData<String>, then do a Transformations.switchMap by it to update the LiveData that you're actually observing from your Activity/Fragment.
public void setmQueryMediaList(String directoryPath, String sectionOrder, String sortOrder) {
directoryPathLiveData.setValue(directoryPath);
}
mQueryMediaList = Transformations.switchMap(directoryPathLiveData, (directoryPath) -> {
if(directory_path != null){
return mediaDatabase.mediaDao().getAllByName();
} else {
return mediaDatabase.mediaDao().getAllByDate();
}
});