Introduction
The Realm Mobile Database is a cross-platform database solution that can be used as an alternative to SQLite and Core Data. Compared to these two options, Realm is easier to set up and use. To perform the same operation in Realm, you usually end up writing fewer lines of code than you would with SQLite or Core Data. On performance, Realm is said to be faster and it also offers other modern features such as encryption, JSON support and data change notifications.
Unlike a traditional database, objects in Realm are native objects. You don’t have to copy objects out of the database, modify them, and save them back—you’re always working with the “live,” real object. If one thread or process modifies an object, other threads and processes can be immediately notified. Objects always stay in sync.
If your application needs to store user data to the cloud and have it synced on all devices used by the user, you can use the Realm Mobile Database together with the Realm Object Server for this. In this article though, we are going to focus on the Realm Mobile Database. We are going to see how to integrate it into an Android app and perform the usual CRUD operations on it. We'll create a To Do application which will enable the user to create, edit and delete tasks from a list.
Getting Started
To get started, first create an Android project (I named mine Tasky). You can use an IDE of your choice, but the tutorial will give instructions specific to Android Studio.
Select a Basic Activity template for it and on the last window of the project creation wizard, change the Activity Name to TaskListActivity
.
To add the Realm library to the project, first add the classpath dependency to the project level build.gradle
file.
buildscript {
repositories {
jcenter()
}
dependencies {
classpath "io.realm:realm-gradle-plugin:3.1.4"
}
}
Then apply the realm-android plugin to the top of the application level build.gradle
file.
apply plugin: 'realm-android'
And add the following dependency in the same file. This isn't a requirement when working with Realm, but if your project is going to use Realm adapters, then you have to include the library. We'll use this later when we set up the ListView
compile 'io.realm:android-adapters:2.0.0'
Sync the project's gradle files.
Before you can use Realm in your app, you must initialize a Realm. Realms are the equivalent of a database. They map to one file on disk and contain different kinds of objects. Initializing a Realm is done once in the app's lifecycle. A good place to do this is in an Application
subclass.
Create a class named TaskListApplication
and modify it as shown.
package com.echessa.tasky;
import android.app.Application;
import io.realm.Realm;
import io.realm.RealmConfiguration;
public class TaskListApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
Realm.init(this);
RealmConfiguration realmConfig = new RealmConfiguration.Builder()
.name("tasky.realm")
.schemaVersion(0)
.build();
Realm.setDefaultConfiguration(realmConfig);
}
}
In the above, we first initialize the Realm and then configure it with a RealmConfiguration
object. The RealmConfiguration
controls all aspects of how a Realm is created. The minimal configuration usable by Realm is RealmConfiguration config = new RealmConfiguration.Builder().build();
which will create a file called default.realm
located in Context.getFilesDir()
.
In our configuration, we name the Realm file tasky.realm
and set a version number on it.
In the manifest file, set this class as the name of the application tag.
<application
android:name=".TaskListApplication"
Next open the content_task_list.xml
layout file and replace the TextView with a ListView.
<ListView
android:id="@+id/task_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
In the activity_task_list.xml
file, change the icon on the FloatingActionButton by modifying its srcCompat
attribute as shown.
app:srcCompat="@android:drawable/ic_input_add"
Add a layout file named task_list_row.xml
to the res/layout
folder that will specify the format of each row of the previously added ListView. Modify it as shown:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dp"
android:layout_gravity="center_vertical">
<TextView
android:id="@+id/task_item_name"
android:textSize="20sp"
android:layout_weight="100"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<CheckBox
android:id="@+id/task_item_done"
android:focusable="false"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
Next we'll create a model that will hold the data for each task.
Creating a Realm Model
Inside your main package, add a package named models
and create a class named Task
inside that package. Add the following to the file.
package com.echessa.tasky.models;
import io.realm.RealmObject;
import io.realm.annotations.PrimaryKey;
import io.realm.annotations.Required;
public class Task extends RealmObject {
@Required
@PrimaryKey
private String id;
@Required
private String name;
private boolean done;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isDone() {
return done;
}
public void setDone(boolean done) {
this.done = done;
}
}
In the above, we create a Realm model classe by extending the RealmObject
base class. Realm supports the following field types: boolean
, byte
, short
, int
, long
, float
, double
, String
, Date
and byte[]
as well as the boxed types Boolean
, Byte
, Short
, Integer
, Long
, Float
and Double
. Subclasses of RealmObject
and RealmList<? extends RealmObject>
are used to model relationships.
Each Task in our app will have a name
which will be the description set for the task, a boolean done
field that will indicate whether the task has been completed or not and a unique ID that will be used to identify the task.
The id
and name
fields are marked with a @Required
annotation. This is used to tell Realm to enforce checks on these fields and disallow null
values. Only Boolean
, Byte
, Short
, Integer
, Long
, Float
, Double
, String
, byte[]
and Date
can be annotated with Required. Fields with primitive types and the RealmList
type are required implicitly while ones with RealmObject
type are always nullable.
The id
field is annotated with @PrimaryKey
thus marking it as the primary key. Supported field types can be either string (String
) or integer (byte
, short
, int
, or long
) and its boxed variants (Byte
, Short
, Integer
, and Long
). Using a string field as a primary key implies that the field is indexed (i.e. it will implicitly be marked with the annotation @Index
). Indexing a field makes querying it faster, but it slows down the creation and updating of the object, so you should be careful about the number of fields in your object that you @Index
.
String (String
) and boxed integer (Byte
, Short
, Integer
, and Long
) Primary keys can have null
values and so we also mark id
with @Required
to enforce a check.
Working with RealmBaseAdapter
Our application will display the Task items in a ListView, and to work with this, we need an adapter that will manage the data model and adapt it to individual rows in the ListView.
Realm makes two adapters available that can be used to bind its data to UI widgets, in particular data coming from OrderedRealmCollection
(RealmResults
and RealmList
, which we'll look at shortly, implement this interface). There is the RealmBaseAdapter
for working with ListViews and RealmRecyclerViewAdapter
for working with RecyclerViews.
To use any one of these adapters, you have to add the io.realm:android-adapters:2.0.0
dependency in the application level Gradle file, which we've done.
To create an adapter, create a class named TaskAdapter
and modify its contents as shown.
package com.echessa.tasky;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.ListAdapter;
import android.widget.TextView;
import com.echessa.tasky.models.Task;
import io.realm.OrderedRealmCollection;
import io.realm.RealmBaseAdapter;
public class TaskAdapter extends RealmBaseAdapter<Task> implements ListAdapter {
private TaskListActivity activity;
private static class ViewHolder {
TextView taskName;
CheckBox isTaskDone;
}
TaskAdapter(TaskListActivity activity, OrderedRealmCollection<Task> data) {
super(data);
this.activity = activity;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
final ViewHolder viewHolder;
if (convertView == null) {
convertView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.task_list_row, parent, false);
viewHolder = new ViewHolder();
viewHolder.taskName = (TextView) convertView.findViewById(R.id.task_item_name);
viewHolder.isTaskDone = (CheckBox) convertView.findViewById(R.id.task_item_done);
viewHolder.isTaskDone.setOnClickListener(listener);
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
}
if (adapterData != null) {
Task task = adapterData.get(position);
viewHolder.taskName.setText(task.getName());
viewHolder.isTaskDone.setChecked(task.isDone());
viewHolder.isTaskDone.setTag(position);
}
return convertView;
}
private View.OnClickListener listener = new View.OnClickListener() {
@Override
public void onClick(View view) {
int position = (Integer) view.getTag();
if (adapterData != null) {
Task task = adapterData.get(position);
activity.changeTaskDone(task.getId());
}
}
};
}
The above creates the View of row items. Each row will contain a TextView that will display the Task name and a Checkbox that will indicate whether a task has been completed or not. We use the View holder pattern to optimize performance by ensuring reuse of existing views.
We also add an OnClick listener to the view's checkbox that will be used to change the status of Task represented by that view.
In TaskListActivity
add the following variable to the class.
private Realm realm;
Then modify onCreate()
as shown.
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_task_list);
realm = Realm.getDefaultInstance();
// RealmResults are "live" views, that are automatically kept up to date, even when changes happen
// on a background thread. The RealmBaseAdapter will automatically keep track of changes and will
// automatically refresh when a change is detected.
RealmResults<Task> tasks = realm.where(Task.class).findAll();
final TaskAdapter adapter = new TaskAdapter(this, tasks);
ListView listView = (ListView) findViewById(R.id.task_list);
listView.setAdapter(adapter);
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
final Task task = (Task) adapterView.getAdapter().getItem(i);
final EditText taskEditText = new EditText(TaskListActivity.this);
taskEditText.setText(task.getName());
AlertDialog dialog = new AlertDialog.Builder(TaskListActivity.this)
.setTitle("Edit Task")
.setView(taskEditText)
.setPositiveButton("Save", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
// TODO: 5/4/17 Save Edited Task
}
})
.setNegativeButton("Delete", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
// TODO: 5/4/17 Delete Task
}
})
.create();
dialog.show();
}
});
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
final EditText taskEditText = new EditText(TaskListActivity.this);
AlertDialog dialog = new AlertDialog.Builder(TaskListActivity.this)
.setTitle("Add Task")
.setView(taskEditText)
.setPositiveButton("Add", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
realm.executeTransactionAsync(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
realm.createObject(Task.class, UUID.randomUUID().toString())
.setName(String.valueOf(taskEditText.getText()));
}
});
}
})
.setNegativeButton("Cancel", null)
.create();
dialog.show();
}
});
}
In the above, we first get a Realm instance for the thread which will be used in all interactions with the database. When finished with a Realm instance, it is important that you close it with a call to close()
to deallocate memory and release any other used resource. For a UI thread the easiest way to close a Realm instance is in the onDestroy()
method. Realm instances are reference counted, which means each call to getInstance()
must have a corresponding call to close()
.
After getting the instance, we query the database with realm.where(Task.class).findAll()
to get all Task objects saved and assign them to a RealmResults
object. RealmResults
(and RealmObject
) are live objects that are automatically kept up to date when changes happen to their underlying data. The RealmBaseAdapter
also automatically keeps track of changes to its data model and updates when a change is detected.
We then create an instance of a ListView, set its adapter and add an OnItemClickListener on it. When a list item is tapped, we display an AlertDialog that the user can use to either edit the task or delete it. We'll implement these later.
We then create an instance of a FloatingActionButton and set an OnClick listener on it which displays an AlertDialog that can be used to create a task.
All write operations to Realm (create, update and delete) must be wrapped in write transactions. A write transaction can either be committed or cancelled. During a commit, all changes are written to disk, and a commit is only successful if all changes are persisted. By cancelling a write transaction, all changes will be discarded. With write transactions, your data will always be in a consistent state.
Write operations can be made using the following format:
realm.beginTransaction();
//... add or update objects here ...
realm.commitTransaction();
Or they can be made using transaction blocks that use the realm.executeTransaction()
or realm.executeTransactionAsync()
methods which we use in the application. By default, write transactions block each other. It is recommended that you use asynchronous transactions on the UI thread that will run on a background thread and avoid blocking the UI. This is why we use executeTransactionAsync()
as opposed to the other function. You can pass a callback function to executeTransactionAsync()
that will get called when the transaction completes.
We create a Task with realm.createObject()
, set a random UUID value as its id
and set the text entered by the user as its name
. To keep the code simple, we omit any check of the text entered by the user. In a real app, you would ensure that the user entered text before saving it and also display a message to them letting them know that input is required. As the app is right now, when a user doesn't input any text, the value of the Task name
will be an empty String.
Next, add the following method to the class which will close the Realm instance when the Activity exits.
@Override
protected void onDestroy() {
super.onDestroy();
realm.close();
}
Add the following to the class. This method is called from the OnClick listener we created in the adapter. It gets an id
of the Task whose checkbox was clicked and updates its done
field.
public void changeTaskDone(final String taskId) {
realm.executeTransactionAsync(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
Task task = realm.where(Task.class).equalTo("id", taskId).findFirst();
task.setDone(!task.isDone());
}
});
}
Run the app and you should be able to create tasks, mark them as completed, exit the application and still have your data when you launch it again.
Editing Tasks
To enable editing the task's title, first add the following to TaskListActivity
. This queries the database for a Task with a given id
then sets its name
with the passed in text.
private void changeTaskName(final String taskId, final String name) {
realm.executeTransactionAsync(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
Task task = realm.where(Task.class).equalTo("id", taskId).findFirst();
task.setName(name);
}
});
}
Call it by replacing the // TODO: 5/4/17 Save Edited Task
comment in the ListViews's OnItemClickListener with:
changeTaskName(task.getId(), String.valueOf(taskEditText.getText()));
Run the app and you should be able to change a task's title.
Deleting Tasks
Add the following to the TaskListActivity
class.
private void deleteTask(final String taskId) {
realm.executeTransactionAsync(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
realm.where(Task.class).equalTo("id", taskId)
.findFirst()
.deleteFromRealm();
}
});
}
private void deleteAllDone() {
realm.executeTransactionAsync(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
realm.where(Task.class).equalTo("done", true)
.findAll()
.deleteAllFromRealm();
}
});
}
The first function deletes a specific Task given an id
while the second deletes all completed tasks.
Call the first function by replacing the // TODO: 5/4/17 Delete Task
comment in the ListViews's OnItemClickListener with:
deleteTask(task.getId());
Next, we'll add a menu button that will be used to delete all done tasks.
Open menu_task_list.xml
in the res/menu
folder and add an item
to it:
<item
android:id="@+id/action_delete"
android:title="@string/action_delete"
app:showAsAction="always"/>
Add the following to the strings.xml
file in res/values
.
<string name="action_delete">Clear Done</string>
Then modify onOptionsItemSelected()
in TaskListActivity
as shown.
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
return true;
} else if (id == R.id.action_delete) {
deleteAllDone();
return true;
}
return super.onOptionsItemSelected(item);
}
Run the app. You should now be able to delete a single task or all completed tasks.
Working with Migrations
You might have noticed that if you delete a task that isn't the last item in the list, it will be deleted and the last item in the list will move up to take its place. This reordering isn't a big issue, but some users might want their list of item to remain in the same order. To achieve this, we are going to sort the RealmResults
tasks before passing them to the adapter.
We want the list to keep the order in which items were added. For this, we'll add a timestamp to each item and sort the list using this field. This means that we'll be changing the Realm model and thus the database's schema.
If you have data saved to the disk, you can't just change the database schema and have it work with the saved .realm
file. You have to perform a migration from the old schema to the new definition. If there is no file on disk when Realm launches, no migration is needed. Realm will just create a new .realm
file & schema based on the latest models defined in your code.
If your application is in development and you are okay with losing saved data, then you can just delete the .realm
file on disk instead of having to write a migration. You can delete the Realm file with the following code in the TaskListApplication
. Remember to remove the Realm.deleteRealm(realmConfig)
statements on subsequent runs otherwise your database will be deleted each time the app is launched.
RealmConfiguration realmConfig = new RealmConfiguration.Builder()
.name("tasky.realm")
.schemaVersion(0)
.build();
Realm.deleteRealm(realmConfig);
Realm.setDefaultConfiguration(realmConfig);
A better way might be to delete the Realm file only when the schema changes:
RealmConfiguration realmConfig = new RealmConfiguration.Builder()
.name("tasky.realm")
.schemaVersion(0)
.deleteRealmIfMigrationNeeded()
.build()
Realm.setDefaultConfiguration(realmConfig);
The above two solutions are only ideal in development. If you have an app in production and want to release an update that uses a different schema, you wouldn't want your user's to lose their data when they update the app. For this, you'll have to create a migration.
First, add a timestamp
field to the Task model and add setter and getter functions for it.
private long timestamp;
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
Then create a class named Migration
inside the models
package and modify it as shown below.
package com.echessa.tasky.models;
import io.realm.DynamicRealm;
import io.realm.DynamicRealmObject;
import io.realm.RealmMigration;
import io.realm.RealmObjectSchema;
import io.realm.RealmSchema;
/**
* Example of migrating a Realm file from version 0 (initial version) to its latest version (version 1).
*/
public class Migration implements RealmMigration {
@Override
public void migrate(final DynamicRealm realm, long oldVersion, long newVersion) {
// During a migration, a DynamicRealm is exposed. A DynamicRealm is an untyped variant of a normal Realm, but
// with the same object creation and query capabilities.
// A DynamicRealm uses Strings instead of Class references because the Classes might not even exist or have been
// renamed.
// Access the Realm schema in order to create, modify or delete classes and their fields.
RealmSchema schema = realm.getSchema();
/************************************************
// Version 0
class Task
@Required
@PrimaryKey
private String id;
@Required
private String name;
private boolean done;
// Version 1
class Task
@Required
@PrimaryKey
private String id;
@Required
private String name;
private boolean done;
private long timestamp;
************************************************/
// Migrate from version 0 to version 1
if (oldVersion == 0) {
RealmObjectSchema taskSchema = schema.get("Task");
taskSchema.addField("timestamp", long.class)
.transform(new RealmObjectSchema.Function() {
@Override
public void apply(DynamicRealmObject obj) {
obj.set("timestamp", 0);
}
});
oldVersion++;
}
}
}
The above specifies a migration of the schema from version 0 (which is the version number we set during its creation) to 1. We use addField()
to add a new field to the RealmObject model class. The transform()
block isn't necessary, I include it here to show how you would go about setting a value for the field in already existing objects. transform()
runs a transformation function on each RealmObject instance of the current class. In the above, the existing objects will have a timestamp
value of 0
.
We then increment the value of oldVersion
. This isn't necessary here since we only have one migration to perform, but I prefer to end each block that checks for a version number with an increment of oldVersion
so that I won't have to remember to do so when I add another migration. If I was to migrate from version 1 to 2, then I would add another if
block if (oldVersion == 1)
and if I had forgotten to increment the oldVersion
, then the app would be buggy. For a detailed example of a migration check out this example and you'll get what I'm trying to say here.
Finally in your TaskListApplication
class, change the schemaVersion
to the version you are upgrading to and add the migration to the configuration code with migration()
.
RealmConfiguration realmConfig = new RealmConfiguration.Builder()
.name("tasky.realm")
.schemaVersion(1)
.migration(new Migration())
.build();
Realm.setDefaultConfiguration(realmConfig);
Before we run the app, let us complete the original task we were working on, which was to sort the items by their timestamp
.
Sorting Data
To sorts the tasks by their timestamps, first make sure that new tasks are created with a timestamp value. Modify the AlertDialog's button OnClick listener (inside the FloatingActionButton code) as shown. This sets the device's current time as the value of the task's timestamp.
@Override
public void onClick(DialogInterface dialogInterface, int i) {
realm.executeTransactionAsync(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
Task task = realm.createObject(Task.class, UUID.randomUUID().toString());
task.setName(String.valueOf(taskEditText.getText()));
task.setTimestamp(System.currentTimeMillis());
}
});
}
Then add the following right after RealmResults<Task> tasks
is initialized. Before the tasks
object is passed to the adapter.
tasks = tasks.sort("timestamp");
Run the app and you should now be able to delete tasks and have them keep their original order.
Pat yourself on the back because you have just created an application that performs all CRUD operations on a Realm database.
Aside: Adding Auth0 Authentication to the Application
Before we conclude, we'll look at how authentication can be added to an Android app using Auth0.
To get started, first sign up for an Auth0 account, then navigate to the Dashboard. Click on the New Client button and fill in the name of the client (or leave it at its default. Select Native from the Client type list. After the client has been created, you will see a page with a quickstart guide. Select the Settings tab where the client ID, client Secret and Domain can be retrieved. Add the following to the Allowed Callback URLs and save the changes with the button at the bottom of the page.
Replace YOUR_AUTH0_DOMAIN
and YOUR_APP_PACKAGE_NAME
with your specific values.
https://YOUR_AUTH0_DOMAIN/android/YOUR_APP_PACKAGE_NAME/callback
You'll require the Client ID and Domain for your client application. You can find these values on your Auth0 dashboard. We'll add them to the app's strings.xml
file so that they are accessible to the rest of the app.
<string name="com_auth0_client_id">YOUR_AUTH0_CLIENT_ID</string>
<string name="com_auth0_domain">YOUR_AUTH0_DOMAIN</string>
To add a login/register screen to the application, we'll use the Auth0.Android library which will allow us to display a Centralized Login page for our Android apps with support for social connections and the usual username/password form.
To add Auth0.Android to your project, first add the following dependency and sync your gradle files.
compile 'com.auth0.android:auth0:1.12.0'
You will also need to add the manifestPlaceholders
options to the defaultConfig
section:
android {
// (...)
defaultConfig {
// (...)
manifestPlaceholders = [auth0Domain: "@string/com_auth0_domain", auth0Scheme: "https"]
}
// (...)
}
Then, add the following permissions to the manifest file.
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
In your main package, create a class named LoginActivity
and add the following to it.
package com.echessa.tasky;
import android.app.Activity;
import android.app.Dialog;
import android.content.Intent;
import android.os.Bundle;
import android.widget.Toast;
import com.auth0.android.Auth0;
import com.auth0.android.authentication.AuthenticationAPIClient;
import com.auth0.android.authentication.AuthenticationException;
import com.auth0.android.callback.BaseCallback;
import com.auth0.android.provider.AuthCallback;
import com.auth0.android.provider.WebAuthProvider;
import com.auth0.android.result.Credentials;
import com.auth0.android.result.UserProfile;
import com.echessa.tasky.utils.CredentialsManager;
public class LoginActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
protected void onDestroy() {
super.onDestroy();
// Your own Activity code
}
@Override
protected void onResume() {
super.onResume();
final Auth0 account = new Auth0(this);
WebAuthProvider.init(account)
.withConnectionScope("openid", "offline_access")
.start(LoginActivity.this, mCallback);
}
private void showToastText(final String text) {
runOnUiThread(new Runnable() {
public void run() {
Toast.makeText(LoginActivity.this, text, Toast.LENGTH_SHORT).show();
}
});
}
private final AuthCallback mCallback = new AuthCallback() {
@Override
public void onSuccess(Credentials credentials) {
showToastText("Log In - Success");
startActivity(new Intent(LoginActivity.this, TaskListActivity.class));
finish();
}
@Override
public void onFailure(Dialog dialog) {
showToastText("Log In - Cancelled");
}
@Override
public void onFailure(AuthenticationException exception) {
showToastText("Log In - Error Occurred");
}
};
}
In the above code, we make the added class into an Activity. We'll set this Activity as the first activity that gets called on app launch.
In the activity's onResume()
method, we instantiate an Auth0
object. This object reads the values of the Auth0 domain and client ID from the strings.xml
file. We can then instantiate a WebAuthProvider
object that will display a login page for us.
We then include some callback functions that will be called after the authentication call to Auth0. We have the onSuccess()
that is called on successful authentication. Here, we display a message in a Toast and redirect the user to the TaskListActivity
activity. We also include the onFailure()
callbacks that are called when authentication is cancelled or in case of an authentication error.
Add the LoginActivity to the manifest file as shown below and also remove the intent-filter
tag from the TaskListActivity
specification. We've instead added it to LoginActivity and made this activity the Launcher activity.
<activity
android:name=".TaskListActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar"/>
<activity android:name=".LoginActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
Run the app. If everything went well, you should see the Auth0 login screen and you should be able to create an account and be logged in.
In this app, we won't associate the To Do tasks to a particular user. Any logged in user will be able to see the tasks saved to a device. Later on we'll see how you can get the user's Auth0 details, incase you want to associate the data shown to a particular user.
We've added authentication to the app and now our user's list has some protection against prying eyes. That's great and we could call it a day, but let's look at an improvement we could make on the app. Right now, each time the user exits the app and launches it, a login is required. This might be annoying to some users. In the next section, we'll see how we can save the user's session on the device that will be used to automatically log them on subsequent launches.
Working with Sessions
To keep a user's session, we'll be working with the following key objects.
- idToken: Identity Token that proves the identity of the user.
- accessToken: Access Token used by the Auth0 API.
refreshToken: Refresh Token that can be used to request new tokens without signing in again.
These objects are the keys needed to keep the user connected, as they will be used in all the API calls.
First we'll add some utility classes to the project. One class will be used to store some constants and the other will be user to save the Auth0 user credentials to disk.
In the main package, add another package named
utils
. Inside this package, add two classes namedConstants
andCredentialsManager
. Modify them as shown.Constants.java
```java package com.echessa.tasky.utils;
class Constants { final static String REFRESHTOKEN = "refreshtoken"; final static String ACCESSTOKEN = "accesstoken"; final static String IDTOKEN = "idtoken"; final static String TOKENTYPE = "tokentype"; final static String EXPIRESIN = "expiresin"; } ```
CredentialsManager.java
package com.echessa.tasky.utils;
import android.content.Context;
import android.content.SharedPreferences;
import com.auth0.android.result.Credentials;
import com.echessa.tasky.R;
import static com.echessa.tasky.utils.Constants.ACCESS_TOKEN;
import static com.echessa.tasky.utils.Constants.EXPIRES_IN;
import static com.echessa.tasky.utils.Constants.ID_TOKEN;
import static com.echessa.tasky.utils.Constants.REFRESH_TOKEN;
import static com.echessa.tasky.utils.Constants.TOKEN_TYPE;
public class CredentialsManager {
public static void saveCredentials(Context context, Credentials credentials) {
SharedPreferences sharedPref = context.getSharedPreferences(
context.getString(R.string.auth0_preferences), Context.MODE_PRIVATE);
sharedPref.edit()
.putString(ID_TOKEN, credentials.getIdToken())
.putString(REFRESH_TOKEN, credentials.getRefreshToken())
.putString(ACCESS_TOKEN, credentials.getAccessToken())
.putString(TOKEN_TYPE, credentials.getType())
.putLong(EXPIRES_IN, credentials.getExpiresIn())
.apply();
}
public static Credentials getCredentials(Context context) {
SharedPreferences sharedPref = context.getSharedPreferences(
context.getString(R.string.auth0_preferences), Context.MODE_PRIVATE);
return new Credentials(
sharedPref.getString(ID_TOKEN, null),
sharedPref.getString(ACCESS_TOKEN, null),
sharedPref.getString(TOKEN_TYPE, null),
sharedPref.getString(REFRESH_TOKEN, null),
sharedPref.getLong(EXPIRES_IN, 0));
}
public static void deleteCredentials(Context context) {
SharedPreferences sharedPref = context.getSharedPreferences(
context.getString(R.string.auth0_preferences), Context.MODE_PRIVATE);
sharedPref.edit()
.putString(ID_TOKEN, null)
.putString(REFRESH_TOKEN, null)
.putString(ACCESS_TOKEN, null)
.putString(TOKEN_TYPE, null)
.putLong(EXPIRES_IN, 0)
.apply();
}
}
The above class has methods that will be used to save, retrieve and delete user credentials from the device's shared preferences.
Add the following to the strings.xml
file.
<string name="auth0_preferences">AUTH0_SHARED_PREFERENCES_KEY</string>
Modify LoginActivity
as shown.
package com.echessa.tasky;
import android.app.Activity;
import android.app.Dialog;
import android.content.Intent;
import android.os.Bundle;
import android.widget.Toast;
import com.auth0.android.Auth0;
import com.auth0.android.authentication.AuthenticationAPIClient;
import com.auth0.android.authentication.AuthenticationException;
import com.auth0.android.callback.BaseCallback;
import com.auth0.android.provider.AuthCallback;
import com.auth0.android.provider.WebAuthProvider;
import com.auth0.android.result.Credentials;
import com.auth0.android.result.UserProfile;
import com.echessa.tasky.utils.CredentialsManager;
public class LoginActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
protected void onDestroy() {
super.onDestroy();
// Your own Activity code
}
@Override
protected void onResume() {
super.onResume();
final Auth0 account = new Auth0(this);
String accessToken = CredentialsManager.getCredentials(this).getAccessToken();
if (accessToken == null) {
WebAuthProvider.init(account)
.withConnectionScope("openid", "offline_access")
.start(LoginActivity.this, mCallback);
return;
}
AuthenticationAPIClient aClient = new AuthenticationAPIClient(account);
aClient.userInfo(accessToken)
.start(new BaseCallback<UserProfile, AuthenticationException>() {
@Override
public void onSuccess(final UserProfile payload) {
showToastText("Automatic Login Success");
startActivity(new Intent(LoginActivity.this, TaskListActivity.class));
finish();
}
@Override
public void onFailure(AuthenticationException error) {
showToastText("Session Expired, please Log In");
CredentialsManager.deleteCredentials(LoginActivity.this);
WebAuthProvider.init(account)
.withConnectionScope("openid", "offline_access")
.start(LoginActivity.this, mCallback);
}
});
}
private void showToastText(final String text) {
runOnUiThread(new Runnable() {
public void run() {
Toast.makeText(LoginActivity.this, text, Toast.LENGTH_SHORT).show();
}
});
}
private final AuthCallback mCallback = new AuthCallback() {
@Override
public void onSuccess(Credentials credentials) {
showToastText("Log In - Success");
CredentialsManager.saveCredentials(LoginActivity.this, credentials);
startActivity(new Intent(LoginActivity.this, TaskListActivity.class));
finish();
}
@Override
public void onFailure(Dialog dialog) {
showToastText("Log In - Cancelled");
}
@Override
public void onFailure(AuthenticationException exception) {
showToastText("Log In - Error Occurred");
}
};
}
Before showing the login page, we need to first ask for the offline_access
scope in order to get a valid refresh_token
in the response.
We then check for a saved accessToken
. If there is none, the login page is shown and the user can log in. When they log in, the onSuccess()
callback will be called and their credentials saved.
If an accessToken
is found on application launch, we check whether it’s still valid. To do this, we fetch the user profile with the AuthenticationAPI
. If this call succeeds, we launch the TaskListActivity
.
How you deal with a non-valid token is up to you. You will normally choose between two scenarios. You can either ask users to re-enter their credentials or use the refreshToken
to get a new valid one. In our app, we ask the user to log in again (in the onFailure()
callback).
Run the app and you'll be able to remain logged in when you exit the app.
User Profile
If your app needs to work with the logged in user's details, you can grab them from the UserProfile
object that is passed into the onSuccess()
callback when making a call to the AuthenticationAPI
. In our code, we have the following parameter UserProfile payload
. From this payload
object you can get details about the user depending on how your application is set up and what details are saved to Auth0. Examples of what you can get are shown.
payload.getName();
payload.getEmail();
payload.getPictureURL();
payload.getUserMetadata().get("country").toString();
User Logout
Next we'll add a Logout option to the app's menu. Add the following item to the menu/menu_task_list
file.
<item
android:id="@+id/logout"
android:title="@string/logout"
app:showAsAction="never"/>
Add the following to the strings.xml
file.
<string name="logout">Logout</string>
In TaskListActivity
add the following. Here we delete the saved user credentials and navigate to the LoginActivity
.
private void logout() {
CredentialsManager.deleteCredentials(TaskListActivity.this);
startActivity(new Intent(this, LoginActivity.class));
}
Call this function in onOptionsItemSelected()
by modifying the if_else block as shown.
if (id == R.id.action_settings) {
return true;
} else if (id == R.id.action_delete) {
deleteAllDone();
return true;
} else if (id == R.id.logout) {
logout();
}
Run the app and you should be able to logout via the Logout menu option
Conclusion
In this article, we've looked at how to use the Realm Mobile Database in an Android app. If you are interested in finding out more on this, be sure to check out its documentation. You can download the completed project files from here. The folder contains two subfolders labelled completed
and completed_with_auth0
. The former contains the completed project without the added authentication code, while the latter contains the Auth0 code. For the latter, remember to place in your Auth0 client ID and domain in the strings.xml
file.