How to change a live data instance in view model? - android

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();
}
});

Related

Cannot read country code selected in a spinner Android

I am working on an app in which users have to select a country code, i was successful in creating a spinner for the said purpose as shown in this link:
Creating a spinner for choosing country code
But i am getting problem in reading the value selected in the spinner.
{
String abc = onCountryPickerClick();//abc is always null
}
public String onCountryPickerClick (){
ccp.setOnCountryChangeListener(new CountryCodePicker.OnCountryChangeListener() {
#Override
public void onCountrySelected() {
selected_country_code = ccp.getSelectedCountryCodeWithPlus();
}
});
return selected_country_code;
}
When String abc = onCountryPickerClick(); is being invoked, the selected_country_code value will be assigned to abc.
When your CountryCodePicker.OnCountryChangeListener's onCountrySelected() method is being invoked, the ccp.getSelectedCountryCodeWithPlus();'s value gets assigned to selected_country_code. Since String is immutable, changing selected_country_code's value won't change the value of abc, nor the return selected_country_code; will be invoked.
One of possible solutions would be to change your CountryCodePicker.OnCountryChangeListener anonymous implementation to assign the selected country value to abc e.g.
#Override
public void onCountrySelected() {
selected_country_code = ccp.getSelectedCountryCodeWithPlus();
abc = selected_country_code
}
Callbacks are not synchronous. Unfortunately, you cannot simply do String abc = onCountryPickerClick(); because what you are returning is something that is not yet set. Let's go through your code:
ccp.setOnCountryChangeListener(
new CountryCodePicker.OnCountryChangeListener() {
#Override
public void onCountrySelected() {
selected_country_code = ccp.getSelectedCountryCodeWithPlus();
}
});
The code seems to say that when the country is selected in the spinner, you assign the value of selected_country_code. Assuming this is an action triggered by the user, when you call String abc = onCountryPickerClick();, how can you be sure the user has selected anything? This is the issue. You cannot be sure that the user has already selected the option and returning the value is not enough.
You can solve this in many ways. You can for example keep propagating the callback:
public void onCountryPickerClick(OnCountryChangeListener listener){
ccp.setOnCountryChangeListener(listener);
}
// Anywhere you call this
onCountryPickerClick(new CountryCodePicker.OnCountryChangeListener() {
#Override
public void onCountrySelected() {
// Here do whatever you want with the selected country
}
});
The above approach is not very different than what you have now. There are other options. You could use java observables i.e.:
class CountryCodeObservable extends Observable {
private String value;
public CountryCodeObservable(String value) {
this.value = value;
}
public void setCountryCode(String countryCode) {
value = countryCode;
setChanged();
notifyObservers(value);
}
}
public CountryCodeObservable onCountryPickerClick(){
CountryCodeObservable retValue = new CountryCodeObservable("");
ccp.setOnCountryChangeListener(
new CountryCodePicker.OnCountryChangeListener() {
#Override
public void onCountrySelected() {
retValue.setCountryCode(ccp.getSelectedCountryCodeWithPlus());
}
});
return retValue;
}
// Then when calling this method you can do something like:
CountryCodeObservable observable = onCountryPickerClick();
observable.addObserver((obj, arg) -> {
// arg is the value that changed. You'll probably need to cast it to
// a string
});
The above example lets you add more than one observable. It might be too much for your use case, I just thought it illustrates another approach and also the asynchronicity of this situation.
Again, there are even more ways to solve this, the key is that you can't simply return a string and hope it changes when the user selects anything.

MediatorLiveData onChanged not getting called

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.

Realm not fully saved

is realm have something like listener for commiting data?
my code is
...
RealmDatabase.submitNewUser(mainActivity.getMyRealm(), userModel);
mainActivity.closeKeyboard();
mainActivity.onBackPressed();
...
public static void submitNewUser(Realm myRealm, UserModel user) {
myRealm.beginTransaction();
UserDb userDb = myRealm.createObject(UserDb.class);
userDb.setUserid(getNextUserId(myRealm));
userDb.setName(user.name);
....
myRealm.commitTransaction();
}
private static int getNextUserId(Realm myRealm) {
try {
return myRealm.where(UserDb.class).max("userid").intValue() + 1;
} catch (ArrayIndexOutOfBoundsException e) {
return 1;
}
}
after save data, i closed fragment and back to previous fragment.
on start function, checking if have data
#Override
public void onStart() {
super.onStart();
if (loading.isShowing()) loading.dismiss();
if (reloadList && checkContent()) {
....
}
reloadList = true;
}
private boolean checkContent() {
users = RealmDatabase.loadUserList(mainActivity.getMyRealm());
if (users.size() > 0 && users.get(0).userid > 0) {
// note:
// user id is auto increament while saved
// if no data, return dataset with userid = 0 for other purpose
return true;
} else {
....
return false;
}
}
public static List<UserModel> loadUserList(Realm myRealm) {
List<UserModel> list = new ArrayList<>();
RealmResults<UserDb> results = myRealm.where(UserDb.class).findAll();
results = results.sort("userid", Sort.DESCENDING);
for (UserDb result : results) {
UserModel userModel = new UserModel();
userModel.userid = result.getUserid();
....
userModel.note = result.getNote();
list.add(userModel);
}
if (list.size() == 0) {
UserModel userModel = new UserModel();
userModel.userid = 0;
userModel.note = "You still have no user at this time";
list.add(userModel);
}
return list;
}
checkContent(), user.size detected as 1 (new data is added) but userid still 0.
am i miss something in this logic? because everything is working well if i reopen app after add new user.
update
after using listener, i got my dataset but still not showing my content. after some trial i found that my list view is not showing the data even after i re-input data and do notifydataset on adapter.
users = RealmDatabase.loadUserList(mainActivity.getMyRealm());
homeAdapter.reloadList(users);
....
public void reloadList(List<UserModel> users) {
this.users = users;
notifyDataSetChanged();
}
update 2
everything going well for the 2nd, 3rd, and later item except the first one
is realm have something like listener for commiting data?
Yes
realmResults.addChangeListener(realmChangeListener);
One must keep a field reference to the RealmResults.
everything is working well if i reopen app after add new user.
Probably the ArrayList you build from the RealmResults is not updated.
as suggested by #epicPandaForce answer, i use listener to my code.
and to solved my problem as i mentioned in the last comment in #epicPandaForce answer, i change my code like this
getMyRealm().addChangeListener(new RealmChangeListener<Realm>() {
#Override
public void onChange(Realm element) {
// previous code in my activity
// getFragmentManager().popBackStackImmediate();
// new code in my activity
fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.replace(R.id.container, fragment);
}
});
those code not really inside the listener instead calling function on my activity. those placement just for where its called.

How to query in CouchDB by docType?

I'm trying to use the below query to get my documents by passing the value of docType [template or audit]; however, it is not showing the correct results.
Data.getData(database, "audit").run();
public static Query getData(Database database, final String type) {
View view = database.getView("data");
if (view.getMap() == null) {
view.setMap(new Mapper() {
#Override
public void map(Map<String, Object> document, Emitter emitter) {
if(document.get("docType").equals(type)){
emitter.emit(document.get("_id"), null);
}
}
}, "4");
}
return view.createQuery();
}
As far as I know, you might want to creat an index like this:
function (doc) {
if (doc.type) {
emit(doc.type, doc._id);
}
}
The above function creates an index for you sorted according to doc.type. Then you can do your queries faster.

Is it possible to reuse the same RealmObject instance with different IDs to save/update multiple objects in a write transaction?

Currently, we are creating a new instance for every RealmObject that we want to save in our Mapper class.
#Override
public Person toRealmObject(Realm realm, PersonXML businessObject) {
Person person = new Person();
person.setId(businessObject.getId());
person.setName(businessObject.getName());
return person;
}
When we create a new one, we collect it into a list.
#Override
public void populateRealmListWithMappedModel(Realm realm, RealmList<Person> realmList, PersonsXML personXML) {
for(PersonXML personXML : personXML.getPersons()) {
realmList.add(personMapper.toRealmObject(realm, personXML));
}
}
/*next the following happens:*/
//realm.beginTransaction();
//personRepository.saveOrUpdate(realm, list);
//realm.commitTransaction();
Then we save the list.
#Override
public RealmList<T> saveOrUpdate(Realm realm, RealmList<T> list) {
RealmList<T> realmList = new RealmList<T>();
for(T t : realm.copyToRealmOrUpdate(list)) {
realmList.add(t);
}
return realmList;
}
The question is, is the following possible, can I re-use the same Person object instead and change its values to specify to Realm the objects that I want to have saved, but not have a whole object associated with it?
As in, something like this:
#Override
public Person toRealmObject(Realm realm, PersonXML businessObject, Person person) {
person.setId(businessObject.getId());
person.setName(businessObject.getName());
return person;
}
Then
#Override
public void writeObjectsToRealm(Realm realm, PersonsXML personXML) {
realm.beginTransaction();
Person person = new Person();
for(PersonXML personXML : personXML.getPersons()) {
person = personMapper.toRealmObject(realm, personXML, person));
personRepository.saveOrUpdate(person);
}
realm.closeTransaction();
}
Where this method is
#Override
public T saveOrUpdate(Realm realm, T t) {
return realm.copyToRealmOrUpdate(t);
}
I'm asking this because rewriting the architecture to use the following would require rewriting every populateRealmListWithMappedModel() methods I have, and that would be a bit concerning if it doesn't work. So I'm curious if it theoretically works.
Basically the short question is, if I call copyToRealmOrUpdate(t) on a realmObject, and alter its id and data, and save the same object again and again, will the write transaction succeed?
Yes you can re-use the object used as input to copyToRealm. We are creating a copy of your input object but not altering the original in any way, so reusing the object like you are doing should work and will also reduce the amount work the GC has to do.
Person javaPerson = new Person();
Person realmPerson = realm.copyToRealm(javaPerson);
// The following is true
assertTrue(javaPerson != realmPerson)
assertFalse(javaPerson.equals(realmPerson))
assertFalse(realmPerson.equals(javaPerson))
Yep, it worked.
#Override
public void persist(Realm realm, SchedulesXML schedulesXML) {
Schedule defaultSchedule = new Schedule();
if(schedulesXML.getSchedules() != null) {
realm.beginTransaction();
for(SchedulesByChannelXML schedulesByChannelXML : schedulesXML.getSchedules()) {
Channel channel = channelRepository.findOne(realm, schedulesByChannelXML.getChannelId());
if(channel == null) {
Log.w(TAG,
"The channel [" + schedulesByChannelXML.getChannelId() + "] could not be found in Realm!");
}
for(ScheduleForChannelXML scheduleForChannelXML : schedulesByChannelXML.getSchedules()) {
defaultSchedule = scheduleMapper.toRealmObject(realm, scheduleForChannelXML, defaultSchedule);
defaultSchedule.setChannel(channel);
defaultSchedule.setChannelId(schedulesByChannelXML.getChannelId());
boolean isInRealm = scheduleRepository.findOne(realm,
scheduleForChannelXML.getScheduleId()) != null;
Schedule savedSchedule = scheduleRepository.saveOrUpdate(realm, defaultSchedule);
if(!isInRealm) {
if(channel != null) {
channel.getSchedules().add(savedSchedule);
}
}
}
}
realm.commitTransaction();
}
}
Instead of creating 150 Schedule each time I save the batch of schedules, now I only create one per batch, and it works flawlessly.

Categories

Resources