My Android Full-Sync ToDo App

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.

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-full-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; log in as the same user for each device 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 an Item class. It looks like this:

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.

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

Step 3 Log 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";
public static final String AUTH_URL = "http://" + INSTANCE_ADDRESS + "/auth";
public static final String REALM_URL = "realm://" + INSTANCE_ADDRESS;
}

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 legacy self-hosted 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 for authenticating a user in order to access your Realm Object Server instance. This is done in the attemptLogin() method by using the provided username and password. Realm's username/password authentication provider is an excellent credential for you to quickly get started with Realm Sync.

In production you will most likely be using an OAuth-based provider like facebook or Google.

Logging in to the Realm Object Server looks like this:

SyncCredentials credentials = SyncCredentials.usernamePassword(username, password, createUser);
SyncUser.logInAsync(credentials, AUTH_URL, new SyncUser.Callback<SyncUser>() {
@Override
public void onSuccess(SyncUser user) {
showProgress(false);
gotoListActivity();
}
@Override
public void onError(ObjectServerError error) {
showProgress(false);
usernameView.setError("Uh oh something went wrong! (check your logcat please)");
usernameView.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) {
gotoListActivity();
}

Try now to run the application and sign up 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 Add the list of tasks

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

The Recycler view is implemented in TasksRecyclerAdapter 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 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 mItem;
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.mItem = item;
this.textView.setText(item.getBody());
this.checkBox.setChecked(item.getIsDone());
}
@Override
public void onClick(View v) {
String itemId = mItem.getItemId();
boolean isDone = this.mItem.getIsDone();
this.mItem.getRealm().executeTransactionAsync(realm -> {
Item item = realm.where(Item.class).equalTo("itemId", itemId).findFirst();
if (item != null) {
item.setIsDone(!isDone);
}
});
}
}
}

Back to TasksActivity: the realm url is sent in an Intent. In this app, a realm represents a ToDo list.

For now, the realm url is hardcoded to a single user-specific realm. All of a user's ToDo items go into this one realm. If you wanted to have multiple ToDo lists for each user, you would not hardcode the realm url. Instead, for example, you could programmatically generate the url with a ToDo list ID in the realm url and allow users to specify which ToDo list they want to work on. However, that's out of scope for this tutorial.

public static final String INTENT_EXTRA_PROJECT_URL = "TasksActivity.projectId";
private Realm realm;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//...
String projectUrl = getIntent().getStringExtra(INTENT_EXTRA_PROJECT_URL);
SyncConfiguration config = SyncUser.current()
.createConfiguration(projectUrl)
.fullSynchronization()
.waitForInitialRemoteData(30, TimeUnit.SECONDS)
.build();
Realm.getInstanceAsync(config, new Realm.Callback() {
@Override
public void onSuccess(Realm realm) {
TasksActivity.this.realm = realm;
Project project = realm.where(Project.class).findFirst();
if (project != null) {
setTitle(project.getName());
RealmResults<Item> tasks = project.getTasks().where().sort("timestamp", Sort.ASCENDING).findAllAsync();
tasks.addChangeListener(list -> {
if (list.isEmpty()) {
setStatus("Press + to add a new task");
} else {
setStatus("");
}
});
tasksRecyclerAdapter = new TasksRecyclerAdapter(tasks);
recyclerView.setAdapter(tasksRecyclerAdapter);
} else {
setStatus("Could not load project");
}
}
});
//...
}
  • 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 5 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.

Alternatively, you can pull the Realm file from the device and open it using Realm Studio. You'll see that your local Realm contains only Items related to your user.

Step 6 Use Realm Studio to update values

Changes are stored in the server realm called /~/project, where ~ is replaced by the identity of the user.

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!

Further reading

Not what you were looking for? Leave Feedback