Ruqus (pronounced "Ruckus") is an Android library which provides a number of components which allow your users to construct their own Realm queries. Users simply choose one of your model object classes, then they add any conditions, operators, and sort fields they wish. All of this information is stored in RealmUserQuery
objects, which can then be turned into a RealmResults
object using a single method call, just as if you had created the query at design-time yourself.
Sound awesome yet? What if I told you that Ruqus makes all of this possible with a minimal amount of reflection at run-time? This is possible because Ruqus uses an annotation processor to generate information about your model object classes at compile-time.
If you just want to know what's new, the changelog is here.
If you want to see how I use Ruqus in one of my apps, check out Minerva.
Adding/Editing a Condition 1 | Adding/Editing a Condition 2 | Adding an Operator |
---|---|---|
Editing Sort Fields | Filled RealmQueryView | Results |
---|---|---|
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
And then add this to your app's build.gradle
file:
apply plugin: 'com.neenbedankt.android-apt'
dependencies {
apt 'com.bkromhout.ruqus:ruqus-compiler:{latest version}'
compile ('com.bkromhout.ruqus:ruqus-core:{latest version}@aar') {
transitive = true
}
}
Please note that at this time, Ruqus has been tested and is verified to work with Realm 2.0.2. Don't be afraid to try a newer version of Realm, just be sure to open an issue if you run into problems.
Ruqus is compatible with Android API Levels >= 14.
## The Basics Ruqus relies on a number of annotations to help it generate information at compile-time. At a high level, the processor uses Realm's `@RealmClass` annotation to figure out which classes in your project are `RealmModel`s. Since `RealmObject` is annotated with `@RealmClass`, all of your model objects which extend it will automatically be picked up by the annotation processor. Any classes which implement `RealmModel` directly will also be picked up as long as they're annotated with `@RealmClass` (which they must be anyway for Realm to see them).Ruqus also provides a few annotations which you should use to help the processor generate extra information about your model object classes. These are discussed a little further down.
Before that, however, there are a few things which must be done to make Ruqus work.
### Initializing Ruqus You **must** call `Ruqus.init(Context)` sometime before you handle `RealmUserQuery` objects or the user has the ability to interact with `RealmQueryView`s. I personally feel that the best place for this call to be made is in the `onCreate()` method of a custom Application class, as can be seen in the sample app's [`SampleApplication`][SampleApplication Class] class: ```java public class SampleApplication extends Application { ... @Override public void onCreate() { super.onCreate(); ... Ruqus.init(this); ... } ... } ``` ### Adding a `RealmQueryView` For users to build a query, they obviously need some sort of UI control to interact with; Ruqus provides such a thing in the form of the `RealmQueryView`. Before we get into how to add one to a layout and hook it up in code, there are a couple of things you should know:- A
RealmQueryView
is a fairly space-hungry control; for the best user experience, I recommend having nothing else on the screen at the same time (for phones), or at the very least ensuring that there are no other views above or below it (tablets) RealmQueryView
will save/restore its view state when configuration changes occur all by itself (unless you're doing something funky and interrupting Android'sonSaveInstanceState()
/onRestoreInstanceState()
call hierarchy)
Keeping that in mind, I'd recommend creating a new empty activity to use as a query building activity and adding a RealmQueryView
to it. You can see the sample app's activity_edit_query.xml file if you want, but this is what it boils down to:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.bkromhout.ruqus.RealmQueryView
android:id="@+id/rqv"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</RelativeLayout>
Once you've done this, I highly recommend that you have your corresponding activity implement RealmQueryView.ModeListener
so that it is notified when RealmQueryView
's mode changes. Again, you can look at the sample app's EditQueryActivity.java
file if you'd like, but here's what it boils down to:
class EditQueryActivity implements RealmQueryView.ModeListener {
RealmQueryView rqv;
// Have this set to MAIN by default.
RealmQueryView.Mode rqvMode = RealmQueryView.Mode.MAIN;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
// Bind the RealmQueryView.
rqv = (RealmQueryView) findViewById(R.id.rqv);
// Ensure your save button is visible (or if it's a toolbar icon,
// do it in onPrepareOptionsMenu() instead).
}
@Override
protected void onResume() {
super.onResume();
// Register this activity with the view to be notified of mode changes.
rqv.setModeListener(this);
}
@Override
protected void onPause() {
super.onPause();
// Make sure the view doesn't hold onto a reference to the activity!
rqv.clearModeListener();
}
@Override
public void rqvModeChanged(RealmQueryView.Mode newMode) {
rqvMode = newMode;
// Do something to show your save trigger view if newMode is MAIN,
// or to hide it if not.
}
}
The reason for this is that RealmQueryView
can be in main mode, or a builder mode; it logically makes sense have a button/toolbar action/etc visible to let the user save their query while in main mode, but not while in a builder mode. See the screenshots at the top and notice how the sample app shows/hides the check button in the toolbar based on the state of the RealmQueryView
.
Another way you can integrate RealmQueryView
to provide good UX is by overriding your activity's onBackPressed()
method to have it call and check the return value of RealmQueryView.leaveBuilderMode()
.
This method will return true
if calling it caused your RealmQueryView
to return from one of the builder modes to the main mode, or false
if it was already in the main mode. I recommend only following through with the default system behavior when false
is returned, which will provide better UX:
@Override
public void onBackPressed() {
if (!rqv.leaveBuilderMode()) super.onBackPressed();
}
While I won't discuss these in detail here since the sample code is fairly straight-forward, there are a couple more key functionalities I recommend implementing:
- Structure your app so that your query builder activity is started using
startActivityForResult()
; then, when the query is saved, have it return aRealmUserQuery
by putting it in the extras of anIntent
. - Similarly, if you want users to be able to edit an existing
RealmUserQuery
s, you can put it into the extras of theIntent
used to start the query builder activity, and then pass it to theRealmQueryView
using itssetRealmUserQuery()
method sometime duringonCreate()
.
Both of these are possible because RealmUserQuery
implements Android's Parcelable
interface.
Ruqus includes a few annotations which you apply to your model objects to help it:
- Generate class/field data at compile-time
- Allow users to build queries against only the model objects which you include using only the fields which you don't exclude
- Specify "nice", human-readable names for model objects and fields
The @Queryable
annotation takes one required parameter called name
, which you should consider to be a human-readable name for your model object that users will see.
Here's an example from the Person
class from the sample app:
@Queryable(name = "Person")
public class Person extends RealmObject {
...
private Dog dog;
...
}
This will allow users to build queries which return Person
objects.
But notice that there's a Dog
-typed field in Person
. If we look at the Dog
class from the sample app, notice that it does not have the @Queryable
annotation:
public class Dog extends RealmObject {
...
}
However, Ruqus will still generate information for the Dog
class in order to support Realm's link queries.
So in our sample app, users cannot build a query which returns Dog
objects; but since our queryable class Person
references the Dog
class, they can build queries which return Person
objects based on the linked Dog
objects' fields. If this is still confusing, I'd recommend reading up on Realm's link query functionality.
Here's an example from the Person
class from the sample app:
@Queryable(name = "Person")
public class Person extends RealmObject {
...
@Ignore
private int tempReference;
@Hide
private long id;
...
}
From the Ruqus annotation processor's point of view, the meaning of both @Ignore
and @Hide
are the same: Skip the field. So, users won't be able to use the tempReference
or the id
fields when building Person
queries.
While @Hide
may seem quite trivial in theory, thoughtful use of it is essential for guarding against OOM errors causing your app to crash.
Just one last thing; Ruqus will always skip all static
fields and all byte
/Byte
/byte[]
/Byte[]
fields (because how would a user possibly add a condition against such fields?), so you needn't bother annotating those with @Hide
.
Here's an example from the Cat
class from the sample app:
@RealmClass
@Queryable(name = "Cat")
public class Cat implements RealmModel {
public String name;
@VisibleAs(string = "Least Favorite Dog")
public Dog leastFavorite;
...
}
When Ruqus generates data for the Cat
class, the name
field's visible name will be "Name", and the leastFavorite
field's visible name will be "Least Favorite Dog" (If we hadn't added the @VisibleAs
annotation to it, it would have been "Least Favorite").
I personally feel that the quickest way to grok what a transformer is and how it functions is to look at a few of them, so go ahead, look some of the transformers which come with Ruqus.
Here are some general guidelines which apply to all transformers:
- They all must extend the abstract
RUQTransformer
class - They all must be annotated with the
@Transformer
annotation - When Ruqus creates an instance of them, it uses their no-argument constructors to do so
- When Ruqus creates a human-readable version of the whole query string, it relies upon each transformer's
makeReadableString(...)
method to do so
Also, you'll need some more information about the @Transformer
annotation's parameters:
String name
: This is the string which will be displayed to the user describing what the transformer does. All transformers must specify this- You can see what I've used for the built-in transformers in
Names.java
- You can see what I've used for the built-in transformers in
Class[] validArgTypes
: This is an array ofClass
objects which represent the types which a transformer's arguments can be. All transformers must specify this- You really shouldn't pass anything other than
Boolean.class
,Date.class
,Double.class
,Float.class
,Integer.class
,Long.class
,Short.class
, andString.class
in this array, since those are the types which Ruqus supports andRealmQuery
's various methods will work with - It is important to note that while a transformer may accept multiple types in general, Ruqus will expect all of the arguments to be of the same type each time it calls a transformer's
transform(...)
method - i.e., if a transformer with two arguments accepts both
Integer
andLong
, you can pass it twoInteger
s, or twoLong
s, but not one of each
- You really shouldn't pass anything other than
int numArgs
: This determines how many arguments a transformer accepts. By default this is set to1
, since most methods onRealmQuery
take afieldName
plus one more argument- If you're thinking in terms of the methods on
RealmQuery
, note that this number does not include the extremely-commonfieldName
argument on those methods, only the other arguments
- If you're thinking in terms of the methods on
boolean isNoArgs
: Whether or not this is a no-arguments transformer. By default this is set tofalse
, since in most cases you'll have no reason to set it otherwise- This may seem redundant since it appears at first glance that you could achieve the same effect by setting
numArgs
to0
, but it isn't. Consider the methodsRealmQuery.or()
andRealmQuery.isNull(String)
; the former legitimately has no arguments, while the latter takes the usualfieldName
argument (which we don't count fornumArgs
!) - If a transformer has this set to true, Ruqus considers it to be an "Operator" rather than a "Condition".As you can see in the screenshots at the top, this affects where in a
RealmQueryView
it will show up
- This may seem redundant since it appears at first glance that you could achieve the same effect by setting
Beyond this information, I recommend you take a look at EqualTo.java
, Between.java
, and Or.java
, since those provide good examples of differently structured transformers. You can also look at the NoOp transformer included in the sample app (spoiler alert, it does nothing at all to the RealmQuery
when its transform(...)
method is called π).
Transformers are very powerful things! If you haven't realized why I'm spending so much time discussing them yet, allow me to clue you in: Ruqus lets you build your own transformers, because just like your model object classes, Ruqus generates information for transformer classes at compile-time.
If you create a good, general-purpose transformer which I don't already have and you think others would benefit from its inclusion in the core Ruqus library, don't hesitate to open a pull request!
## Customizing You can override certain resources that used by Ruqus in order to customize it. The following resources can be overridden; I've provided their default values, as well as paired them so that you can see which ones go together (this is only really important for cards however): ```xml @android:color/white @color/cardview_light_background #DE000000 @color/ruqus_blue800 @android:color/white@color/ruqus_grey700
@color/cardview_dark_background @android:color/white
@color/ruqus_blueGrey800 @android:color/white
(Any of the default values which aren't present here can be seen in the actual file, but I'll save you some time and tell you that they correspond to Material Design colors.)
<a name="troubleshooting"/>
## Troubleshooting
<a name="ts_oom"/>
#### My app is crashing due to OOM errors!
This is, sadly, a fairly easy thing to cause depending on what relationships you have set up between your various model classes.
The root of the problem is that you have a cycle of relationships, whether it be something like `A-->A`, `A-->B-->A`, etc; and that at least one of the classes in the cycle is annotated with `@Queryable` (or is referenced by a class which is).
To better illustrate how and why this issue occurs, let's use a simple example involving these classes:
```java
@Queryable(name = "A")
class A extends RealmObject {
...
private B b;
...
}
class B extends RealmObject {
...
private A a;
...
}
Say the user wants to build a query which returns A
objects. What happens is that once they choose the kind of model they wish to build a query for (in this case, A
), RealmQueryView
makes a call to Ruqus.visibleFlatFieldsForClass(String)
, passing it the real name of the chosen model object class. That method, simply put, returns a list containing the following (in this case):
- Visible names of all fields on
A
, except those which were skipped (@Ignore
/@Hide
) and those which define a relationship (fields whose type is either some model object or aRealmList
of model objects; in our example, the fieldb
in classA
falls into this category) - For fields which define a relationship, such as field
b
, we traverse the relationship (A-->B
) and:- The first rule is applied again, so we'd add all of
B
's fields except for fielda
- Then the second rule is applied again, which causes us traverse the relationship
B-->A
to addA
's fields, thus creating a cycle
- The first rule is applied again, so we'd add all of
This should hopefully make it clear how cycles can cause issues. It also exposes the solution, which is to break cycles using the @Hide
annotation.
Ideally, one of the model classes involved in the cycle isn't annotated with @Queryable
, in which case I'd recommend annotating the offending field in that class with @Hide
:
class B extends RealmObject {
...
@Hide
private A a;
...
}
However, you know your models' relationships better than I do, so you add the @Hide
annotation where it will serve you best.
My hope is that this will become a non-issue once the Android Realm library implements support for backlinks, since the current lack of support for them is what usually prompts us to build relationship cycles in the first place. In lieu of that, if someone knows of a clever way to defeat this issue I'd love to hear it; open an issue or a pull request!