My Android Query-Based Sync ToDo App

Query-based Sync is not recommended. For applications using Realm Sync, we recommend Full Sync. Learn more about our plans for the future of Realm Sync here.

Prerequisites

  • Android Studio version 3.0 or higher

  • JDK version 7.0 or higher

  • Android API Level 21 or higher (Android 5.0 and above, typically included as part of the Android Studio installation).

  • You will need your Realm Cloud instance URL that was generated when you created your instance (it can be found by logging in to the cloud portal, and clicking the Copy Instance URL link)

  • Basic knowledge about Android development and Android Studio.

  • Realm Studio (optional) -- throughout the tutorial we will test components of the sync process where directly viewing and editing the Realm Object Server is helpful.

  • Enabled nickname authentication provider: this project uses the deprecated nickname authentication provider, which you must opt-in to use. To opt in,

    • Select your instance.

    • Go to the "Settings" tab.

    • Under "Authentication", click to expand "Nickname".

    • Click "Enable" and accept the prompt.

    If you forget to enable nickname authentication and run the app anyway, you will see the following fatal error: "Your request parameters did not validate. provider: Invalid parameter 'provider'!".

    Please note that the nickname authentication provider is only intended for development purposes. If you intend to use the same instance for production, you should disable the nickname authentication provider after you are done with this sample app.

Quick Start

Want to get started right away? Follow these quick steps.

Clone repository from GitHub

git clone https://github.com/realm/my-first-realm-app

Open in Android Studio

Use Android Studio version 3.0 or higher, to open the existing Android project under

<root>/my-first-realm-app/android/todo-query-based-sync

Set the URL

Edit Constants.java and set INSTANCE_ADDRESS to the URL of your Cloud instance . Be sure to paste in only the host name part ("your-app-name.cloud.realm.io").

Build the application

Now build and run the application. Login with any username and add projects and tasks and observe how they sync on your Realm Cloud instance using Realm Studio :

Collaborate!

To see sync in practice, attach another device/emulator; start the app for each; choose the same nickname for each user and observe the two simultaneously editing the same ToDo Lists at the same time!

How the ToDo application was built

Now that you have seen the ToDo app in action, the rest of this tutorial will walk you through how we took a basic ToDo app and added persistence and synchronization in just a few steps. It is assumed that you have cloned the ToDo repository.

Step 1 Add Realm Java Plugin

First you need to add Realm to the project.

  • Locate and open the project level build.gradle file in the project file navigator as shown here:

  • Add the realm-gradle-plugin to the class path dependency in the project level build.gradle file. The default file may contain additional named repositories; you should edit the file to mirror the settings shown here:

buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
classpath 'io.realm:realm-gradle-plugin:5.8.0'
}
}

Next we will change the app-level build.gradle which is shown here:

Step 1a: Apply the realm-android plugin under the Android one (com.android.application) in the build.gradle file.

apply plugin: 'realm-android'

Step 1b: Add the following plugin configuration to enable the sync APIs:

realm {
syncEnabled = true
}

Step 1c:

We're going to use Realm Android Adapter to build the list of tasks, so we need to add it's dependency.

implementation 'io.realm:android-adapters:3.1.0'

The final build.gradle will look something like this:

apply plugin: 'com.android.application'
apply plugin: 'realm-android'
android {
compileSdkVersion 28
defaultConfig {
applicationId "io.realm.todo"
minSdkVersion 21
targetSdkVersion 28
versionCode 1
versionName "1.0"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
realm {
syncEnabled = true
}
dependencies {
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
implementation 'com.android.support:design:28.0.0'
implementation 'io.realm:android-adapters:3.1.0'
}

Before we can use any Realm functionality, we have to initialize the Realm library. This is done in the onCreate method of the ToDoApplication class.

public void onCreate() {
super.onCreate();
Realm.init(this);
}

Step 2 Add Realm model classes

Realm models database entities using normal Java classes. For the purpose of this app we need two classes: Project and Item. They look like this.

package io.realm.todo.model;
import java.util.Date;
import io.realm.RealmList;
import io.realm.RealmObject;
import io.realm.annotations.PrimaryKey;
import io.realm.annotations.Required;
public class Project extends RealmObject {
@PrimaryKey
@Required
private String id;
@Required
private String owner;
@Required
private String name;
@Required
private Date timestamp;
private RealmList<Item> items;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getOwner() {
return owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Date getTimestamp() {
return timestamp;
}
public void setTimestamp(Date timestamp) {
this.timestamp = timestamp;
}
public RealmList<Item> getTasks() {
return items;
}
public void setTasks(RealmList<Item> items) {
this.items = items;
}
}
package io.realm.todo.model;
import java.util.Date;
import io.realm.annotations.PrimaryKey;
import io.realm.annotations.Required;
public class Item extends RealmObject {
@PrimaryKey
@Required
private String itemId;
@Required
private String body;
@Required
private Boolean isDone;
@Required
private Date timestamp;
}

All properties are required, which means they cannot be null . We will use the itemId property as primary key and the timestamp property to sort the collection of Items. We are indicating this by annotations.

The owner property in Project represents the id of the currently connected user. This is a way to filter our projects compared to others.

You can read more about how to create model classes in Realm here.

Step 3 Logging in to the Realm Object Server Instance

Locate the Java class Constants which will hold the URLs to your Realm Cloud instance, as follow

package io.realm.todo;
final class Constants {
private static final String INSTANCE_ADDRESS = "YOUR_INSTANCE.cloud.realm.io";
static final String AUTH_URL = "https://" + INSTANCE_ADDRESS + "/auth";
}

Assign to INSTANCE_ADDRESSthe actual instance address. It can be found on the 'Getting started' tab in Realm Studio.

Self-Hosted: The code snippet above is optimized for cloud. When using a self-hosted legacy version of Realm Object Server, directly set the AUTH_URL variable. It is likely you won't initially have SSL/TLS setup, so you may need to adjust https to http and realms to realm.

Now locate the WelcomeActivity class. This activity is responsible of authenticating a user in order to access your Realm Object Server instance. This is done in the attemptLogin() method by using the provided "nickname". Realm's "nickname" authentication provider is an excellent development credential for you to quickly get started with Realm Sync without the need to remember any credentials.

In production you will most likely be using a password based provider like usernamePassword or OAuth based like facebook or Google.

Logging in to the Realm Object Server looks like this:

SyncCredentials credentials = SyncCredentials.nickname(nickname, false);
SyncUser.logInAsync(credentials, Constants.AUTH_URL, new SyncUser.Callback<SyncUser>() {
@Override
public void onSuccess(SyncUser user) {
showProgress(false);
setUpRealmAndGoToTaskListActivity();
}
@Override
public void onError(ObjectServerError error) {
showProgress(false);
nicknameView.setError("Uh oh something went wrong! (check your logcat please)");
nicknameView.requestFocus();
Log.e("Login error", error.toString());
}
});

After a successful login, the SyncUser is persisted internally, there's no need to login again on the next app startup. We do check in onCreate if there's already a SyncUser. If we have one we do not attempt to login a user, instead we navigate directly to the next Activity. Add the following right before setting up the login form:

// onCreate
if (SyncUser.current() != null) {
setUpRealmAndGoToTaskListActivity();
}

Try now to run the application and log into your server using some random name, eg. "CoolJoe". You should now see the new user in the Realm Studio user list:

We can also implement the option to log out in the action bar. Locate the TasksActivity class and the method should look like this.

public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.action_logout) {
SyncUser syncUser = SyncUser.current();
if (syncUser != null) {
syncUser.logOut();
Intent intent = new Intent(this, WelcomeActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
}
return true;
}
return super.onOptionsItemSelected(item);
}

As you can see we will logout the user before jumping back to the login screen. You may now try to log out and log in using another name.

Step 4 Adding list of projects

After logging in we setup a default configuration that defines the Realm being used going forward. For Query-based Realms a minimal setup looks like this:

private void setUpRealmAndGoToTaskListActivity() {
SyncConfiguration configuration = SyncUser.current().getDefaultConfiguration();
Realm.setDefaultConfiguration(configuration);
Intent intent = new Intent(WelcomeActivity.this, ProjectsActivity.class);
startActivity(intent);
}

After this the user is sent to ProjectsActivity. This activity is responsible for displaying the list of projects belonging to the current user and select a project to add tasks for. It uses a RecyclerView and a Floating Action Button with a dialog to create new projects.

For Query-based Realms all users save their data to a single shared/default Realm. This way it is easy to share data between users and access can later be restricted using fine-grained permissions. This way of working with Realm is very flexible but slower than working with fully synchronized Realms.

  • When creating a new Project we set the owner to the id of the current user. This way we can easily find all projects that belong to a given user.

String userId = SyncUser.currentUser().getIdentity();
String name = taskText.getText().toString();
Project project = new Project();
project.setId(UUID.randomUUID().toString());
project.setOwner(userId);
project.setName(name);
project.setTimestamp(new Date());
realm.insert(project);
  • The list of projects are fetched using a Realm query. When creating a asynchronous query, we are at the same time creating a Subscription which transparently download all matching data from the server and keep them in sync. This means that if we attach a RealmChangeListener to the query result we will get notified as objects are downloaded or updated. In case we are offline we will just see the objects that were already downloaded.

Realm realm = Realm.getDefaultInstance();
RealmResults<Project> projects = realm
.where(Project.class)
.equalTo("owner", SyncUser.current().getIdentity())
.sort("timestamp", Sort.DESCENDING)
.findAllAsync();
  • The Recycler view is implemented in ProjectsRecyclerAdapter which uses the RealmRecyclerViewAdapter as the base class. This base class will automatically register a RealmChangeListener on the query result. This enables live updates and fine-grained animations out of the box.

public class ProjectsRecyclerAdapter extends RealmRecyclerViewAdapter<Project, ProjectsRecyclerAdapter.MyViewHolder> {
private final Context context;
public ProjectsRecyclerAdapter(Context context, OrderedRealmCollection<Project> data) {
super(data, true);
this.context = context;
}
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext())
.inflate(android.R.layout.simple_list_item_1, parent, false);
return new MyViewHolder(itemView);
}
@Override
public void onBindViewHolder(MyViewHolder holder, int position) {
final Project project = getItem(position);
if (project != null) {
holder.textView.setText(project.getName());
holder.textView.setOnClickListener(v -> {
Intent intent = new Intent(context, ItemsActivity.class);
intent.putExtra(ItemsActivity.INTENT_EXTRA_PROJECT_ID, project.getId());
context.startActivity(intent);
});
}
}
class MyViewHolder extends RecyclerView.ViewHolder {
TextView textView;
MyViewHolder(View itemView) {
super(itemView);
textView = itemView.findViewById(android.R.id.text1);
}
}
}
  • Note that for simplicity we attach an onClickListener when binding the ViewHolder . This will navigate to the ItemsAcivity by passing in argument the projectiD This way the ItemsActivity knows which list of Item it needs to display

holder.textView.setOnClickListener(v -> {
Intent intent = new Intent(context, ItemsActivity.class);
intent.putExtra(ItemsActivity.INTENT_EXTRA_PROJECT_ID, project.getId());
context.startActivity(intent);
});

Step 5 Adding list of tasks

The TasksActivity is used to display the list of tasks part of a project. Just like the ProjectsActivity it consists of a RecyclerView and a floating action button.

  • The project Id was sent in an Intent which means we are able to query for the project we want to show.

public static final String INTENT_EXTRA_PROJECT_ID = "TasksActivity.projectId";
private Realm realm;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//...
String projectId = getIntent().getStringExtra(INTENT_EXTRA_PROJECT_ID);
realm = Realm.getDefaultInstance();
Project project = realm.where(Project.class).equalTo("projectId", projectId).findFirst();
//...
}
  • Just like for showing projects we use a subclass of the RealmRecyclerViewAdapter to enable live updates and animations.

public class TasksRecyclerAdapter extends RealmRecyclerViewAdapter<Item, TasksRecyclerAdapter.MyViewHolder> {
public TasksRecyclerAdapter(OrderedRealmCollection<Item> data) {
super(data, true);
}
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_layout, parent, false);
return new MyViewHolder(itemView);
}
@Override
public void onBindViewHolder(MyViewHolder holder, int position) {
final Item item = getItem(position);
holder.setItem(item);
}
class MyViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
TextView textView;
CheckBox checkBox;
Item item;
MyViewHolder(View itemView) {
super(itemView);
textView = itemView.findViewById(R.id.body);
checkBox = itemView.findViewById(R.id.checkbox);
checkBox.setOnClickListener(this);
}
void setItem(Item item){
this.item = item;
this.textView.setText(item.getBody());
this.checkBox.setChecked(item.getIsDone());
}
@Override
public void onClick(View v) {
String itemId = item.getItemId();
boolean isDone = this.item.getIsDone();
this.item.getRealm().executeTransactionAsync(realm -> {
Item item = realm.where(Item.class).equalTo("itemId", itemId).findFirst();
if (item != null) {
item.setIsDone(!isDone);
}
});
}
}
}
  • Note that changes to the Realm are done inside a background write transactions. Due to Realm's notifications background changes they will automatically propagate back to UI thread which will then update to reflect the latest state.

// TasksRecyclerAdapter - Marking a task done
this.item.getRealm().executeTransactionAsync(realm -> {
Item item = realm.where(Item.class).equalTo("itemId", itemId).findFirst();
if (item != null) {
item.setIsDone(!isDone);
}
});
// TasksActivity - Deleting a tasks by swiping it away
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {
int position = viewHolder.getAdapterPosition();
String id = itemsRecyclerAdapter.getItem(position).getItemId();
realm.executeTransactionAsync(realm -> {
Item item = realm.where(Item.class).equalTo("itemId", id).findFirst();
if (item != null) {
item.deleteFromRealm();
}
});
}

Step 6 Compile and Run

Now you can use two different users on two different devices, you should only see each user's project/tasks. If you log the same user into both devices, you will see changes seamlessly synchronize between them.

You can also pull the Realm file from the device then open it using Realm Studio, you'll see effectively that your local Realm contains only Item and Project related to your user.

Step 7 Using Realm Studio to update values

Changes are stored in the server realm called /default.

If you run you application now and create new items through the Realm Studio interface, you should see the changes reflected on the device. Also, if you click a checkbox, the 'done' property should be changed on your server.

If you create multiple items, be sure to use unique strings for the itemId.

Congrats on creating your first synchronized app with Realm Platform!

Not what you were looking for? Leave Feedback