Written by Adrian Kremski
Android Developer
Published February 6, 2017

Realm Mobile Platform: Offline-first TaskManager app – basics of Realm

In this part of the series we are going to focus on implementing basic Realm support to the TaskManager app by using Realm Mobile Database.

Part 1: REALM MOBILE PLATFORM: OFFLINE-FIRST TASKMANAGER APP – INTRO
Part 3: REALM MOBILE PLATFORM: ANDROID “TASKMANAGER” – REALM OBJECT SERVER
Part 4: REALM MOBILE PLATFORM: OFFLINE-FIRST TASKMANAGER APP – AUTHENTICATION
Part 5: Coming soon!

Here you can read the opening article of the series, devoted to defining the technology stack and the UI of the app. At the end of the series you will know how to build offline-first TaskManager app with realtime synchronization.

1. Setup

Firstly, we will need to update our gradle build to fetch Realm library.

Project build.gradle

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

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

And apply it to the app’s build.gradle

App’s build.gradle

apply plugin: 'com.android.application'
apply plugin: 'realm-android'

Read Installing Realm for Android: Why is it Changing? if you are wondering why Realm is provided as a plugin, and not as a standard dependency.

Now we can proceed and initialize our Realm with init(Context) method.
According to the documentation, the best place to do that is the onCreate method of Application class.

public class RealmTaskManagerApplication extends Application {

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

We also need to update the AndroidManifest.xml file

<application
       android:name=".RealmTaskManagerApplication"
       ...>

2. Mocking Tasks

Before adding anything to Realm, we need to set its configuration. In this case we will simply use the default option. By doing that Realm will create a file called “default.realm” saved in Context.getFilesDir().

public class RealmTaskManagerApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        Realm.init(this);
        RealmConfiguration config = new RealmConfiguration.Builder().build();
        Realm.setDefaultConfiguration(config);
    }
}

Realm is now set and ready. We can define our mock tasks.

public class RealmTaskManagerApplication extends Application {
    @Override
    public void onCreate() {
        ... 
        mockTasks();
    }

    private void mockTasks() {
        final List<Task> testTasks = new LinkedList<>();
        testTasks.add(new Task("Have a coffee"));
        testTasks.add(new Task("Check Android Weekly"));
        testTasks.add(new Task("Check Your mail"));
        testTasks.add(new Task("Standup"));
        testTasks.add(new Task("Pretend that You are working on something"));
        testTasks.add(new Task("Dinner"));
        testTasks.add(new Task("Go home"));

        Realm realm = Realm.getDefaultInstance();
    
        realm.executeTransaction(new Realm.Transaction() {
            @Override
            public void execute(Realm realm) {
                for (Task task : testTasks) {
                    Task realmTask = realm.createObject(Task.class); // this line creates a new object saved in Realm
                    realmTask.setCompleted(false);
                    realmTask.setText(task.getText());
                }
            }
        });
        realm.close(); // Always remember to close the realm instance
    }
}

One thing, however, is still missing here. In order for this code to work, our Task model must extend RealmObject class (all models managed by Realm have to do that) and have a default constructor.

public class Task extends RealmObject{
    ...

    public Task() {
    }
}

3. Loading tasks

To load the tasks we will use a where query.

public class MainActivity extends AppCompatActivity {
     
    private Realm realm;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
   
        Realm realm = Realm.getDefaultInstance();
        RealmResults realmTasks = realm.where(Task.class).findAll();
        recyclerView.setAdapter(taskAdapter = new TaskAdapter(extractTasksFromRealm(realmTasks)));
    }

    private List<Task> extractTasksFromRealm(RealmResults<Task> realmResults) {
        List<Task> tasks = new LinkedList<>();
        for (Task realmTask : realmResults) {
            tasks.add(realmTask);
        }
        return tasks;
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        realm.close(); // Always remember to close the realm instance
    }

    ...
}

Please note that this could also be achieved with rxjava (yes, realm supports it!), however, for the sake of simplicity we will stick to the easiest solution.

public class MainActivity extends AppCompatActivity {

    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        subscription = realm.where(Task.class).findAllAsync().asObservable().observeOn(AndroidSchedulers.mainThread())
                .map(new Func1<RealmResults, List>() {
                    @Override
                    public List call(RealmResults realmResults) {
                        return extractTasksFromRealm(realmResults);
                    }
                }).subscribe(new Action1<List>() {
                    @Override
                    public void call(List tasks) {
                        recyclerView.setAdapter(taskAdapter = new TaskAdapter(tasks));
                    }
                });
    }
}

Result

ezgif.com-resize

4. Adding & Removing tasks

In this part, we are going to add the feature that adds and then removes tasks from the TaskManager app (removal will be done by swipe gesture).

The implementation of the gesture listener (SwipeToDismissTouchListener) can be found in this github project DynamicRecyclerView

public class MainActivity extends AppCompatActivity {
  @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...

        SwipeToDismissTouchListener swipeToDismissTouchListener = new SwipeToDismissTouchListener(recyclerView, new SwipeToDismissTouchListener.DismissCallbacks() {
            @Override
            public SwipeToDismissTouchListener.SwipeDirection canDismiss(int position) {
                return SwipeToDismissTouchListener.SwipeDirection.RIGHT;
            }

            @Override
            public void onDismiss(RecyclerView view, List<SwipeToDismissTouchListener.PendingDismissData> dismissData) {
                for (SwipeToDismissTouchListener.PendingDismissData data : dismissData) {
                    removeTaskFromRealm(taskAdapter.getTaskAtPosition(data.position));
                    taskAdapter.removeTask(data.position);
                }
            }
        });
        recyclerView.addOnItemTouchListener(swipeToDismissTouchListener);
    }

    private void removeTaskFromRealm(final Task taskToRemove) {
        realm.executeTransaction(new Realm.Transaction() {
            @Override
            public void execute(Realm realm) {
                taskToRemove.deleteFromRealm();
            }
        });
    }

    @OnClick(R.id.add_task)
    public void addTask() {
        if (materialSheetFab.isSheetVisible()) {
            materialSheetFab.hideSheet();
        }
        addTaskToRealm(newTask);
    }

    private void addTaskToRealm() {
        final String newTaskText = textInputField.getText().toString();
        textInputField.setText("");
        realm.executeTransaction(new Realm.Transaction() {
            @Override
            public void execute(Realm realm) {
                final Task realmTask = realm.createObject(Task.class);
                realmTask.setText(newTaskText);
                taskAdapter.addTask(realmTask);
            }
        });
    }
}

public class TaskAdapter extends RecyclerView.Adapter {
    public Task getTaskAtPosition(int position) {
        return tasks.get(position);
    }
}

As you can see these two operations are really straightforward.

Result

ezgif.com-resize

5. Updating tasks

Implementation of tasks updates will be kept as simple as possible.
To do that let’s define TaskCompletedListener interface in TaskRowHolder class which will be implemented by the MainActivity.

public class TaskRowHolder extends RecyclerView.ViewHolder {
    ...

    private Task task;
    private TaskCompletedListener taskCompletedListener;

    public interface TaskCompletedListener {
        void taskCompleted(Task task, boolean isCompleted);
    }

    public TaskRowHolder(View itemView, final TaskCompletedListener taskCompletedListener) {
        ...
        this.taskCompletedListener = taskCompletedListener;
        ButterKnife.bind(this, itemView);
        checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                taskCompletedListener.taskCompleted(task, isChecked);
            }
        });
    }
}

Next, we need to update the TaskAdapter class.

public class TaskAdapter extends RecyclerView.Adapter {

    ...

    private TaskRowHolder.TaskCompletedListener taskCompletedListener;

    public TaskAdapter(List<Task> tasks, TaskRowHolder.TaskCompletedListener taskCompletedListener) {
        this.tasks = tasks;
        this.taskCompletedListener = taskCompletedListener;
    }

    ...

    @Override
    public TaskRowHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.view_task_row, parent, false);
        return new TaskRowHolder(itemView, taskCompletedListener);
    }
}

Finally, handling the update by MainActivity.

public class MainActivity extends AppCompatActivity implements TaskRowHolder.TaskCompletedListener {

    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        RealmResults realmTasks = realm.where(Task.class).findAll();
        recyclerView.setAdapter(taskAdapter = new TaskAdapter(extractTasksFromRealm(realmTasks), MainActivity.this));
    }

    @Override
    public void taskCompleted(final Task task, final boolean isCompleted) {
        realm.executeTransaction(new Realm.Transaction() {
            @Override
            public void execute(Realm realm) {
                task.setCompleted(isCompleted);
            }
        });
    } 
}

As you can see,  we update tasks the same way as we run any other operation in this tutorial (by invoking Realm logic within the Transaction body).

Result

ezgif.com-resize

6. Tasks order

To make sure that our tasks are ordered properly we are going to introduce a new field createdAt

public class Task extends RealmObject{

    ...

    private long createdAt;

    public Task(String text) {
        ...
        this.createdAt = new Date().getTime();
    }

    public void setCreatedAt(long createdAt) {
        this.createdAt = createdAt;
    }

    public long getCreatedAt() {
        return createdAt;
    }
}

Update the addTaskToRealm method of MainActivity.

public class MainActivity {

    private void addTaskToRealm(final Task newTask) {
        realm.executeTransaction(new Realm.Transaction() {
            @Override
            public void execute(Realm realm) {
                ...
                realmTask.setCreatedAt(newTask.getCreatedAt());
            }
        });
    }
}

And finally use it with Realm sort method to get a properly sorted list of tasks!

public class MainActivity {
    @Override
    protected void onStart() {
        super.onStart();
        RealmResults realmTasks = realm.where(Task.class).findAll().sort("createdAt");
        recyclerView.setAdapter(taskAdapter = new TaskAdapter(extractTasksFromRealm(realmTasks), MainActivity.this));
    }
}

Final Result

ezgif.com-resize

Now that everything is in place, we are free to remove the code mocking our tasks.

7. Things worth remembering from the documentation

  • Every model which is stored in Realm Mobile database must extend the RealmObject class. An alternative to extending the RealmObject base class is implementing the RealmModel interface and adding the @RealmClass annotation.
  • Realm supports the following types: boolean, byte, short, int, long, float, double, String, Date, byte[], Boolean, Byte, Short, Integer, Long, Float and Double (boxed types can be set to null). Types byte, short, int, and long are all mapped to long within Realm.
  • Annotations:
    • @Ignore – field annotated with @Ignore won’t be persisted to disk
    • @Index – the annotation adding a search index to the field. This one makes inserts slow but gets queries faster
    • @PrimaryKey – adding a primary key to an object. Compound keys (with multiple fields) are not supported
  • Realm supports many-to-one and many-to-many relationships
  • All write operations (adding, modifying, and removing objects) must be done within transaction blocks
  • Reading data from realm is not blocked by write transactions
  • It’s impossible to update one element of a string or byte arrays. If you want to do that, you will have to alter the element in the array, and then assign this array again to chosen field
    bytes[] bytes = realmObject.binary;
    bytes[4] = 'a';
    realmObject.binary = bytes;
    
  • Supported query conditions:
    • between(), greaterThan(), lessThan(), greaterThanOrEqualTo() & lessThanOrEqualTo()
    • equalTo() & notEqualTo()
    • contains(), beginsWith() & endsWith()
    • isNull() & isNotNull()
    • isEmpty() & isNotEmpty()
  • You can only use asynchronous queries on a Looper thread. The asynchronous query needs to use the Realm’s Handler in order to deliver results consistently. Trying to call an asynchronous query using Realm opened inside a thread without a Looper will throw an IllegalStateException.
  • RealmConfiguration object is used to control all aspects of how a Realm is created.
  • It is important to remember to close your Realm instances when you are done with them.
  • Finding your Realm file
  • You cannot randomly pass Realm objects between threads. If you need the same data on another thread you just need to query for that data on the other thread.

The next part of the series is already available!
Part 3: REALM MOBILE PLATFORM: ANDROID “TASKMANAGER” – REALM OBJECT SERVER

Written by Adrian Kremski
Android Developer
Published February 6, 2017