Realm Custom Sort throws UnsupportedOperationException - android

Using Realm for Android and I have a custom sort which throws a java.lang.UnsupportedOperationException: Replacing and element is not supported
So, I have a piece of data that I need to sort after I have retreived it from a Realm database, so it is sitting in a List collection like this:
Collections.sort(stores, new DistanceComparator(currentLocation));
The DistanceComparator looks like this:
public class DistanceComparator implements Comparator<Restaurant> {
private Location currentLocation;
public DistanceComparator(Location location) {
currentLocation = location;
}
public int compare(RealmObject c1, RealmObject c2) {
if ((LocationUtil.calculateSomeValue(c1.getLocation()))
== (LocationUtil.calculateSomeValue(c2.getLocation()))) {
return 0;
}
else if ((LocationUtil.calculateSomeValue(c1.getLocation()))
< (LocationUtil.calculateSomeValue(c2.getLocation()))) {
return -1;
}
else {
return 1;
}
}
}
However, when I execute the sort, I am getting a:
java.lang.UnsupportedOperationException: Replacing and element is not supported.
at io.realm.RealmResults$RealmResultsListIterator.set(RealmResults.java:868)
at io.realm.RealmResults$RealmResultsListIterator.set(RealmResults.java:799)
at java.util.Collections.sort(Collections.java:247)
I cannot perform the sort within Realm due to the transient nature of the data.
Can you not perform a basic sort against a collection that happen to have Realm data or do I need to add some sort of annotation in the class that is a RealmObject in order to perform this type of sort?
I did take a look at this question and that is my fallback if this will not work: How do you sort a RealmList or RealmResult by distance?

You can't perform custom sorting on RealmResult. You'll have to copy your results to an unmanaged List and work with it instead of the realm results.
List<ModelMyFlirtsItem> storesList = realm.copyFromRealm(stores);
Collections.sort(storesList, new DistanceComparator(currentLocation));

You can consider moving the value evaluated by LocationUtil to be part of the RealmObject itself, and do the sort by Realm.
public int compare(RealmObject c1, RealmObject c2) {
if ((LocationUtil.calculateSomeValue(c1.getLocation()))
== (LocationUtil.calculateSomeValue(c2.getLocation()))) {
return 0;
}
else if ((LocationUtil.calculateSomeValue(c1.getLocation()))
< (LocationUtil.calculateSomeValue(c2.getLocation()))) {
return -1;
}
else {
return 1;
}
}
could be
public class RealmObject extends RealmObject {
private Integer distance;
private Location location;
public void setLocation(Location location) {
if(location == null) {
this.distance = null;
} else {
this.distance = LocationUtil.calculateSomeValue(location);
}
this.location = location;
}
}
Then
RealmResults<RealmObject> results = realm.where(RealmObject.class).findAllSorted("distance", Sort.ASCENDING);

I have also faced this problem for using sorting in RealmResults. I resolved it just using the Realm's native .sort() function. For example -
RealmResult<Location> stores......;
.......
.......
stores.sort(new DistanceComparator(currentLocation));

Related

Android sorting array of custom objects

I am trying to sort a static array of Ticket objects into two different arrays based on their dates and Class. These two arrays will be used as data to populate two different list views. I only want to add one Ticket per Class object to each array. I get an error
ArrayList<Ticket> myUpcomingTickets = new ArrayList<>();
ArrayList<Ticket> myPastTickets = new ArrayList<>();
for (Ticket t : Ticket.getTickets()) {
if (t.getClass().getDate().after(date)) {
if(myUpcomingTickets.isEmpty()){
myUpcomingTickets.add(t);
}else {
for(Ticket t1: myUpcomingTickets) {
if (!t.getClass().getId().equals(t1.getClass().getId())) {
myUpcomingTickets.add(t);
}
}
}
} else {
if(myPastTickets.isEmpty()){
myPastTickets.add(t);
} else {
for(Ticket t2: myPastTickets) {
if (!t.getClass().getId().equals(t2.getClass().getId())) { //Add if not already there
myPastTickets.add(t);
}
}
}
}
}
Error Show in Logcat java.util.ConcurrentModificationException
There are a few things wrong with this. The error is caused by this
for(Ticket t2: myPastTickets) {
if (!t.getClass().getId().equals(t2.getClass().getId())) { //Add if not already there
myPastTickets.add(t);
}
}
you are not allowed to add or remove from a list you are currently looping through.
In addition this method would be very slow because of the nested loops you have.
My suggesting would be
1) extend your objects hash and equals to make your object easier to work with
public class Ticket {
#Override
public int hashCode() {
return getId().hashCode();
}
#Override
public boolean equals(final Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final Ticket other = (Ticket ) obj;
return other.getId() == this.getId();
}
}
and then you can do this:
ArrayList<Ticket> myUpcomingTickets = new ArrayList<>();
ArrayList<Ticket> myPastTickets = new ArrayList<>();
for (Ticket t : Ticket.getTickets()) {
if (t.getClass().getDate().after(date)) {
if(!myUpcomingTickets.contains(t)){
myUpcomingTickets.add(t);
}
} else {
if(!myPastTickets.contains(t)){
myPastTickets.add(t);
}
}
}
and if you dont need the lists to be in an order i would suggest using a hashmap instead of arraylist to speed this up
You are modifying the lists (myUpcomingTickets, myPastTickets) while iterating them!
What you could do:
1. If not implemented yet, implement the equals and hashcode for Ticket object
2. Use list.contains(Obj) instead of using the for loop
E.g:
if(!myUpcomingTickets.contains(t)){
myUpcomingTickets.add(t);
}

Is there a way to conditional sort in two fields in Realm?

I have a Rating Realm objects list as Follows:
public class RatingRealmObject extends RealmObject {
#SerializedName("average")
private float average;
#SerializedName("raters")
private int raters;
public float getAverage() {
return average;
}
public void setAverage(float average) {
this.average = average;
}
public int getRaters() {
return raters;
}
public void setRaters(int raters) {
this.raters = raters;
}
}
I want to sort the RealmResultList by descending order of average but the number of raters should be greater than or equal to 5.
e.g. If we have two recipes as A and B.
A has average rating 4, and no. of raters are 8 and,
B has average rating 5 and no. of raters are 4
then after sort, A should come first in the list.
Any help how to make the condition in realm query.
One possible solution would be to concatenate two RealmResults together to get the sort you want, see here.
Another solution would be that if you extend your schema with an additional field
public class RatingRealmObject extends RealmObject {
#Inedx
private float average;
private int raters;
#Index
private int hasOverFiveRaters; // 0 or 1 like a TINYINT in SQLite
public float getAverage() {
return average;
}
public void setAverage(float average) {
this.average = average;
}
public int getRaters() {
return raters;
}
public void setRaters(int raters) {
this.raters = raters;
this.hasOverFiveRaters = raters >= 5 ? 1 : 0;
}
}
Because now you can do
realm.where(RatingRealmObject.class)
.findAllSortedAsync(
new String[] {"hasOverFiveRaters", "average"},
new Sort[] { Sort.DESCENDING, Sort.DESCENDING });
P.S.: realm.copyFromRealm() copies every single element from the Realm (in your case, on the UI thread) and therefore removes lazy evaluation, it is generally not needed nor recommended unless you are trying to create a detached modifiable object or want to send the RealmObject through GSON.
You could define a weight field in your RatingRealmObject, and compute the weight whenever you set average or raters. Then the solution will be just as simple as sorting by the weight.
Some example:
public class RatingRealmObject extends RealmObject {
private float average;
private int raters;
#Index
private float weight;
private void computeWeight() {
// This is for showing the idea, you need to change how to calculate the weight depends on your requirement.
weight = raters / 5 + average;
}
public float getAverage() {
return average;
}
public void setAverage(float average) {
this.average = average;
computeWeight();
}
public int getRaters() {
return raters;
}
public void setRaters(int raters) {
this.raters = raters;
computeWeight();
}
}
Then you could just have realm.where(RatingRealmObject.class).findAllSortedAsync("weight");
#EpicPandaForce and #beeender Thank you guys, You both helped me alot to solve this problem.
As I am using GSON library for the deserialization so as per GSON guidelines setter method does not call. So additional field computation in Rating object didn't work for me.
I have solved my problem by the first solution provided by #EpicPandaForce.
List<RecipeRealmObject> ratingMoreThan5List = recipeRealmResult.where().greaterThanOrEqualTo("rating.raters", 5).findAllSorted("rating.average", Sort.DESCENDING);
List<RecipeRealmObject> ratingLessThan5List = recipeRealmResult.where().lessThan("rating.raters", 5).findAllSorted("rating.average", Sort.DESCENDING);
List<RecipeRealmObject> ratingNullList = recipeRealmResult.where().isNull("rating").findAll();
List<RecipeRealmObject> recipeList = new ArrayList<>();
recipeList.addAll(ratingMoreThan5List);
recipeList.addAll(ratingLessThan5List);
recipeList.addAll(ratingNullList);
onCompleteListener.onSearchComplete(recipeList);

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.

Query nested Realm objects encapsulated in RealmList into RealmResults

I have the following RealmObject:
public class City extends RealmObject {
private String cityId;
private RealmList<Street> streets;
public String getId() {
return cityId;
}
public void setCityId(String cityId) {
this.cityId = cityId;
}
public RealmList<Street> getStreets() {
return streets;
}
public void setStreets(RealmList<Street> streets) {
this.streets = streets;
}
}
Now having a cityId I need to query streets of particular city. How to do that? What I did try was:
Realm.getInstance(context).where(City.class).equalTo("cityId", someCityId, false)
.findFirst().getStreets().where().findAll()
But this leads into an Exception. I need to display streets in a ListView implementing filtering so I need streets to be RealmResults to use RealmBaseAdapter<Street>.
The proper way would be to have an open Realm instance either opened in your Activity in onCreate() and closed in onDestroy(), or in your custom application class.
Then you can use this realm instance to query the realm
City city = realm.where(City.class).equalTo("cityId", cityId).findFirst();
Then you can access the RealmList<T> like any other list
RealmList<Street> streets = city.getStreets();
Then you can use a recyclerview to get the view for a given index within your streets list.

return an object Android

I want to return an object with some things in them.
Here is the declaration;
Object user_det = get_user_det();
Here is the function code:
private Object get_user_det() {
Firebase f_user = new Firebase("https://myapp.firebaseio.com/User/");
f_user.addListenerForSingleValueEvent(new ValueEventListener(){
#Override
public void onDataChange(DataSnapshot snap_user) {
// TODO Auto-generated method stub
Iterable<DataSnapshot> rs = snap_user.getChildren();
Iterator<DataSnapshot> irs = rs.iterator();
long allNum2 = snap_user.getChildrenCount();
int maxNum2 = (int)allNum2;
int count_user = 1;
while(irs.hasNext())
{
if(count_user <= maxNum2)
{
Firebase user_data = new Firebase("https://myapp.firebaseio.com/");
AuthData authData = user_data.getAuth();
Map<String, Object> nPost = (Map<String, Object>) irs.next().getValue();
String db_email = nPost.get("email_addr").toString();
if (authData != null) {
String usr_email = authData.getProviderData().get("email").toString();
if(usr_email.equals(db_email))
{
//NB: I WANT TO ADD THE FOLLOWING INTO THE OBJECT
String disp_name = nPost.get("disp_name").toString();
String real_name = nPost.get("real_name").toString();
}
} else {
System.out.println("Failed");
}
}
count_user++;
}
}
#Override
public void onCancelled(FirebaseError arg0) {
// TODO Auto-generated method stub
}
});
return null; //NB: I NEED TO RETURN THE OBJECT HERE.
}
I want to return the string disp_name and real_name but they are inside the addListenerForSingleValueEvent, so how do I get them out and return it to the function.
I have wrote "NB" in the code where I need help with.
Thanks for your time.
If you want to return an object from your method in java, do it like this:
The Object class:
This contains the structure of your Object, and defines what data will be in it. Also includes methods to easily get the data.
private class myObject {
private String name;
private String realName;
//The constructor, so you can set the data when creating the Object.
public myObject (String disp_name, String real_name) {
name = disp_name;
realName = real_name;
}
//Getter methods, to get the data.
public String getRealName() {return realName;}
public String getDisplayName() {return name;}
}
Your code:
private Object get_user_det() {
myObject o; //Declare it, so it can be returned.
...
String disp_name = nPost.get("disp_name").toString();
String real_name = nPost.get("real_name").toString();
o = new myObject(disp_name, real_name); //create it and set the data.
...
return myobject; //return the new Object with the data.
}
To get the data from the Object:
myObject o = get_user_det(); //Call the metod which return our Object.
String realName = o.getRealName(); //Get the data from the Object.
String displayName = o.getDisplayName;
In your case, it would be much easier to use a String array.
Hope this helps.
It's probably easiest to see what's going on, if you add some printlns to your code:
private Object get_user_det() {
Firebase f_user = new Firebase("https://myapp.firebaseio.com/User/");
System.out.println("Adding listener");
f_user.addListenerForSingleValueEvent(new ValueEventListener(){
#Override
public void onDataChange(DataSnapshot snap_user) {
System.out.println("Data received");
}
#Override
public void onCancelled(FirebaseError arg0) {
// TODO Auto-generated method stub
}
});
System.out.println("Returning");
return null; //NB: I NEED TO RETURN THE OBJECT HERE.
}
If you execute this code, you will see that it logs:
Adding listener
Returning
Data received
Most likely, this is not what you expected. But hopefully, it makes sense if you read my explanation below.
Asynchronous loading
When you register your listener:
f_user.addListenerForSingleValueEvent(new ValueEventListener(){
You tell Firebase to start listening for events. It goes off and starts retrieving the data from the server.
Since retrieving the data may take some time, it does this retrieval asynchronously so that your thread isn't blocked. Once the data is completely retrieved, Firebase calls the onDataChange method in your listener.
Between the time you start listening and the time onDataChange is called, your code continues executing. So there is no way to return data that is loaded asynchronously, because by the time your function returns, the data isn't loaded yet.
Solutions
Disclaimer: I am not an expert at solving this problem in Java, so there may be problems with my solutions. If^H^HWhen you find any, please report them in the comments.
I know of three possible solutions to the problem:
force the code to wait for the data to be returned
return a Future that at some point will contain the data
pass a callback into get_user_det and call that function once the data is available
You will probably be tempted to selected option 1, since it matches most closely with your mental modal of loading data. While this is not necessarily wrong, keep in mind that there is a good reason that the loading is done asynchronously. It might be worth taking the "learning how to deal with asynchronicity" penalty now.
Instead of writing up examples for all solutions, I'll instead refer to some relevant questions:
Retrieving data from firebase returning NULL (an answer that uses approach 3)
Is waiting for return, ok?
Java wait() & notify() vs Android wait() & notify() (a question from a user taking approach 1)
How it works:
Firebase uses reflection to build a JSON tree object to save to the database. When you retrieve this JSON tree, you can cast it back to your original object. Just like serializing and deserializing. This means you do not need to handle the keys and values when trying to "rebuild" your object like you are. It can all be done like so:
YourObject object = (YourObject) dataSnapshot.getValue(YourObject.class);
Notice the YourObject.class in the getValue(). This tells firebase to reflect through this class and find the appropriate accessors with the dataSnapshot.
How to do it
Be sure that your object has:
Accessors Appropriate getters and setters for ALL fields - (or annotated with #JsonIgnore if you wish to not save a particular field)
Empty constructor. Your object must provide a constructor that does not modify itself at all.
What your object should look like:
public class YourObject {
private String displayName;
private String realName;
public YourObject() { /*Empty constructor needed for Firebase */ }
// Accessors
public void setRealName(String realName){
this.realName = realName;
}
public String getRealName(){
return this.realName;
}
public String getDisplayName(){
return this.displayName;
}
public void setDisplayName(String displayName){
this.displayName = displayName;
}
}
Then, in any of the firebase callbacks, you can just cast your DataSnapshot in to your object:
public void onDataChange(DataSnapshot snap_user) {
YourObject object = new Object;
if(snap_user.getValue() != null) {
try {
object = (YourObject) snap_user.getValue(YourObject.class); <-- Improtant!!!
} catch(ClassCastException ex) {
ex.printStackTrace();
}
}
return object;
}
Also
It seems you are retrieving many objects. When doing this, I find it best to use the onChildEventListener then for each of the YourObjects in that node, onChildAdded(DataSnapshot ds, String previousChild); will be called.

Categories

Resources