Java SDK Tutorial
On this page
- Overview
- Prerequisites
- Set up the Mobile App
- Clone the Client App Repository
- Open the Project in Android Studio
- Explore the App Structure
- Connect to Your MongoDB Realm App
- Create the Realm App
- Enable Authentication
- Implement the Projects List
- Implement the Tasks List
- Add Logic to Update and Delete Tasks to the TaskAdapter
- Implement the Manage Team View
- Handle Team Member Removals in MemberAdapter
- Run and Test
- What's Next?
Overview
In this tutorial, you will create a task tracker app that allows users to:
- Register themselves with email and password.
- Sign in to their account with their email and password and sign out later.
- View a list of projects they are a member of.
- View, create, modify, and delete tasks in projects.
- View a list of team members in their project.
- Add and remove team members to their project.
This tutorial should take around 30 minutes.
If you prefer to explore on your own rather than follow a guided tutorial, check out the Java Quick Start. It includes copyable code examples and the essential information that you need to set up a MongoDB Realm application.
Prerequisites
Before you begin, ensure you have:
- Android Studio version 4.0 or higher.
- An Android emulator for testing.
- Set up the backend.
This tutorial uses Realm Sync. As a result, you must set up the backend before proceeding with the tutorial.
Set up the Mobile App
Clone the Client App Repository
We've already put together a task tracker Android application that has most of the code you'll need. You can clone the client application repository directly from GitHub:
git clone --branch start https://github.com/mongodb-university/realm-tutorial-android-kotlin.git
The start
branch is an incomplete version of the app that we will
complete in this tutorial. To view the finished app, check out the
final
branch and update the app's build.gradle
with your
Realm app ID (see "Connect to Your MongoDB Realm
App" below).
Open the Project in Android Studio
Once the installation is complete, use Android Studio to open the project:
- Open Android Studio.
- In the "Welcome to Android Studio" window, click the "Open an existing Android Studio project" option.
- In the file navigator opened by Android Studio, navigate to the
directory where, in the previous step, you cloned the
realm-tutorial-android-kotlin
repository. - Select the
realm-tutorial-android-kotlin
folder. - Click "Open".
- Run the project using the
Run
button.
This should run the gradle build automatically and launch a (nonfunctional) login screen with the text "Task Tracker" at the top of the screen:

Explore the App Structure
In the Android Studio Project view, you can see the source files of the task
tracker application in the app
folder and its subfolders.
TaskTracker stores files that describe the UI logic for Android
views in the app/java/com/mongodb/tasktracker
directory. Files
that describe data models or how those data models relate to the UI
belong in the app/java/com/mongodb/tasktracker/model
directory.
The relevant files are as follows:
File | Purpose |
---|---|
TaskTracker | A subclass of the Android Application class. Responsible for:
|
LoginActivity | View launched whenever a user is not currently logged in.
Allows users to create email/password accounts in the connected
MongoDB Realm application and login to existing accounts. |
ProjectActivity | Displays the list of projects that the currently logged in user
has access to. |
MemberActivity | Displays the list of users that have access to a project, and
allows the owner of that project to edit the list of users. |
TaskActivity | Displays the list of tasks in a project and allows and project
member to create new tasks as well as edit and delete existing
tasks. |
Project | Model describing in-app projects. Subclass of
the RealmObject class, which allows you to store objects of
this class in a realm. |
ProjectAdapter | Adapter that allows the display of a list of projects in an
Android RecyclerView. |
Member | Model describing in-app members of a project. Does not extend
the RealmObject class, since we will fetch members of a
project using a Realm Function rather than a Realm Database
query. |
MemberAdapter | Adapter that allows the display of a list of members in an
Android RecyclerView. |
Task | Model describing in-app tasks that make up the contents of a
project. Subclass of the RealmObject class, which allows
you to store objects of this class in a realm. |
TaskStatus | Enum class capturing the various possible states of an in-app
task. Provides a single place to define the strings that Realm
stores in Realm Database to persist task status state between
devices and app runs. |
TaskAdapter | Adapter that allows the display of a list of tasks in an
Android RecyclerView. |
User | Model describing in-app user data, including the list of
projects to which that user belongs. |
Connect to Your MongoDB Realm App
Tasktracker uses the Gradle dependency management system to manage the Realm Java SDK. The Maven and Ant build systems are not currently supported.
Android Studio projects contain two build.gradle
files by default:
- a top-level project
build.gradle
file which defines build configurations for all project modules - a module-level app
build.gradle
file which allows you to configure build settings for that module (your app) only
You can find the project build.gradle
at the root of the
TaskTracker project, and the app build.gradle
in the app
directory of your project. See the location of these files
in the directory graphic below:
. |-- build.gradle // project gradle file |-- app | |-- build.gradle // app gradle file | |-- src |-- gradle | |-- wrapper |-- gradle.properties |-- gradlew |-- gradlew.bat |-- local.properties |-- settings.gradle |-- Task Tracker.iml
Open the app build.gradle
file, where we'll configure your
app's connection to MongoDB Realm. In the android.buildTypes
section, define your App ID, which TaskTracker
uses to
instantiate a connection to your Realm app whenever this Android app
runs:
buildTypes { def appId = "<your app ID here>" // Replace with proper Application ID debug { buildConfigField "String", "MONGODB_REALM_APP_ID", "\"${appId}\"" } release { buildConfigField "String", "MONGODB_REALM_APP_ID", "\"${appId}\"" minifyEnabled false signingConfig signingConfigs.debug } }
Replace <your app ID here>
with your Realm app ID, which you can
find in the Realm UI.
Create the Realm App
Navigate to TaskTracker
. Android automatically runs the
onCreate()
lifecycle method of this class when you launch
the Task Tracker app. This happens because TaskTracker
is
configured as the global Application
reference in
AndroidManifest.xml
. This allows us to run certain logic
once each time you run the app, such as:
- initializing Realm
- defining the global
App
reference using theappId
variable you just configured in your app-levelbuild.gradle
file - setting the Realm log level
taskApp = App( AppConfiguration.Builder(BuildConfig.MONGODB_REALM_APP_ID) .defaultSyncErrorHandler { session, error -> Log.e(TAG(), "Sync error: ${error.errorMessage}") } .build())
You don't need to make any changes to this file right now, but it is useful to know where to configure these global settings in case you need to change the log level or debug issues with your App connection.
Enable Authentication
Navigate to LoginActivity
, which contains all login and user
registration logic. The UI for LoginActivity
contains text entry
fields for email and password entry, as well as buttons to either
register a user account or login to an existing account. We need to
implement the logic to handle user login and user account creation.
You'll find this logic in the login()
method, where a boolean
value called createuser
controls where the method submits user
credentials to create a new account or to login to an existing
account.
First, let's implement the logic that registers a new user:
taskApp.emailPassword.registerUserAsync(username, password) { // re-enable the buttons after user registration returns a result createUserButton.isEnabled = true loginButton.isEnabled = true if (!it.isSuccess) { onLoginFailed("Could not register user.") Log.e(TAG(), "Error: ${it.error}") } else { Log.i(TAG(), "Successfully registered user.") // when the account has been created successfully, log in to the account login(false) } }
Now, implement the logic to log in with an existing user. Once
logged in successfully, call the onLoginSuccess()
method, which
closes the LoginActivity
and resumes the calling activity
(typically ProjectActivity
):
val creds = Credentials.emailPassword(username, password) taskApp.loginAsync(creds) { // re-enable the buttons after user login returns a result loginButton.isEnabled = true createUserButton.isEnabled = true if (!it.isSuccess) { onLoginFailed(it.error.message ?: "An error occurred.") } else { onLoginSuccess() } }
Don't forget to call the onLoginFailed()
method in the event of a
login or account creation failure with a message describing the error.
Implement the Projects List
ProjectActivity
displays a list of projects that the current user
can access. For each user, TaskTracker stores the list of accessible
projects in the User
object. Tasktracker stores each User
object in a special realm with a value of
the following format: user=<user-id>
.
The backend you imported makes exactly one custom user data object for each user upon signup. This custom user data object contains a list of partitions a user can read and a list of partitions a user can write to.
The backend is set up so that every user has read-only access to their own custom user data object. The backend also has functions to add and remove access to projects, which we will use later when we add the Manage Team view.
By managing the custom user data object entirely on the backend and only providing read-only access on the client side, we prevent a malicious client from granting themselves arbitrary permissions.
To access the user's list of projects, we need to open a connection to
that realm and access the User
object. To get started, open
the user's realm in the else
block of the onStart()
method
of ProjectActivity
:
// configure realm to use the current user and the partition corresponding to the user's project val config = SyncConfiguration.Builder(user!!, "user=${user!!.id}") .build() // Sync all realm changes via a new instance, and when that instance has been successfully created connect it to an on-screen list (a recycler view) Realm.getInstanceAsync(config, object: Realm.Callback() { override fun onSuccess(realm: Realm) { // since this realm should live exactly as long as this activity, assign the realm to a member variable this@ProjectActivity.userRealm = realm setUpRecyclerView(getProjects(realm)) } })
Next, we need to query the realm to get a copy of the User
object containing the user's list of projects. Because each user
should only ever be able to access their own user object, this realm
only contains one object: the custom data belonging to the currently
logged in user. An authentication trigger automatically creates and initializes this
object when the user creates an account. Add the code that queries for
the user object in getProjects()
:
val syncedUsers : RealmResults<User> = realm.where<User>().sort("id").findAll() val syncedUser : User? = syncedUsers.getOrNull(0) // since there might be no user objects in the results, default to "null"
Because it can take a few seconds for the trigger to create this object after a login, we should handle the case where the user object doesn't yet exist immediately after account creation. To accomplish this, in the event that our query doesn't return a user object, we'll watch the realm for changes and only set up the project's Recycler View once the trigger runs:
val changeListener = OrderedRealmCollectionChangeListener<RealmResults<User>> { results, changeSet -> Log.i(TAG(), "User object initialized, displaying project list.") setUpRecyclerView(getProjects(realm)) } syncedUsers.addChangeListener(changeListener)
Users shouldn't have to wait for a Trigger to complete just to write tasks to their personal project. But Task Tracker doesn't display any projects until the user's custom user data object has been initialized by a Trigger. To work around this, we automatically create a temporary fake custom user data object whenever custom user data isn't yet available, so the Recycler View has something to display. Since tasks are written to a separate realm, using an in-memory realm for the fake custom user data doesn't impact creating and syncing tasks.
Finally, we need to guarantee that ProjectActivity
always closes
the user realm when the app closes or the user logs out. To
accomplish this, add logic that calls the realm.close()
method
when ProjectActivity
stops or finishes:
override fun onStop() { super.onStop() user.run { userRealm?.close() } }
override fun onDestroy() { super.onDestroy() userRealm?.close() recyclerView.adapter = null }
Implement the Tasks List
Navigate to the TaskActivity
file, where we'll display the list
of tasks that belong to a particular project. TaskTracker stores the
list of tasks belonging to each project in a special realm with a value of the following format:
project=<user-id>
(where <user-id>
is equal to the user ID of
the user who owns the project). We'll begin by initializing a
connection to this realm when the activity starts:
val config = SyncConfiguration.Builder(user!!, partition) .build() // Sync all realm changes via a new instance, and when that instance has been successfully created connect it to an on-screen list (a recycler view) Realm.getInstanceAsync(config, object: Realm.Callback() { override fun onSuccess(realm: Realm) { // since this realm should live exactly as long as this activity, assign the realm to a member variable this@TaskActivity.projectRealm = realm setUpRecyclerView(realm, user, partition) } })
Next, we'll query the realm for the list of tasks belonging to this
project. Fortunately the query isn't too complicated: since every task
within this realm belongs to this project, there's no need to
filter the query at all. However, we do want to make sure that tasks
always appear in the same order on the page, so users don't have to
hunt through the full list every time they reload this activity. To
accomplish this, we'll add a sort()
to the Realm Database query
that organizes the tasks by _id
. Once you've queried for the
list of tasks, pass the RealmResult
to the TaskAdapter
and set
that adapter as the RecyclerView's
adapter:
adapter = TaskAdapter(realm.where<Task>().sort("id").findAll(), user!!, partition)
TaskActivity
needs to allow users to create a new task in the project. To
handle this, write logic in the floating action button's
setPositiveButton()
callback that creates a new task based on the user's
input in inputText
and adds that task to the realm:
val task = Task(input.text.toString()) // all realm writes need to occur inside of a transaction projectRealm.executeTransactionAsync { realm -> realm.insert(task) }
Finally, we need to guarantee that TaskActivity
always closes
the user realm when the app closes or the user logs out. To
accomplish this, add logic that calls the realm.close()
method
when TaskActivity
finishes or stops:
override fun onDestroy() { super.onDestroy() recyclerView.adapter = null // if a user hasn't logged out when the activity exits, still need to explicitly close the realm projectRealm.close() }
override fun onStop() { super.onStop() user.run { projectRealm.close() } }
Add Logic to Update and Delete Tasks to the TaskAdapter
The TaskAdapter
extends the RealmRecyclerViewAdapter
to
automatically display RealmResults
, as well as synced changes to
the items in a RealmResults
collection, in a Recycler View. While
RealmRecyclerViewAdapter
gives you a lot of functionality by
default, it does not define the UI for displaying items in a
RealmResults
collection or any user interaction with those items.
Fortunately, TaskAdapter
already includes a layout and most
boilerplate code for you. You'll just have to implement two methods:
changeStatus
, which updates the status of a task, and
removeAt
, which deletes a task from the realm.
We'll begin by implementing the logic for changeStatus
, which:
- Connects to the project realm using the
partition
member variable of the adapter. - Queries the realm for the Task with the specified
_id
value. - Sets the
statusEnum
property of the Task to the specified status value.
Don't forget to read and write from the realm within a transaction!
// need to create a separate instance of realm to issue an update // since realm instances cannot be shared across threads val config = SyncConfiguration.Builder(user, partition) .build() // Sync all realm changes via a new instance, and when that instance has been successfully // created connect it to an on-screen list (a recycler view) val realm: Realm = Realm.getInstance(config) // execute Transaction asynchronously to avoid blocking the UI thread realm.executeTransactionAsync { // using our thread-local new realm instance, query for and update the task status val item = it.where<Task>().equalTo("id", id).findFirst() item?.statusEnum = status } // always close realms when you are done with them! realm.close()
The logic that deletes a task is similar to the logic that updates a task, but it removes the task from the realm instead of updating any properties:
// need to create a separate instance of realm to issue an update, since this event is // handled by a background thread and realm instances cannot be shared across threads val config = SyncConfiguration.Builder(user, partition) .build() // Sync all realm changes via a new instance, and when that instance has been successfully created connect it to an on-screen list (a recycler view) val realm: Realm = Realm.getInstance(config) // execute Transaction asynchronously to avoid blocking the UI thread realm.executeTransactionAsync { // using our thread-local new realm instance, query for and delete the task val item = it.where<Task>().equalTo("id", id).findFirst() item?.deleteFromRealm() } // always close realms when you are done with them! realm.close()
Implement the Manage Team View
A user can add and remove team members to their own Project using the Manage Team view. Since this logic edits the custom user data objects of other users (which the currently logged in user cannot edit), we need to call out to our Realm functions we defined earlier, which handle edits to other user's custom user data objects securely:
Navigate to MemberActivity
, which defines the view
that pops up when a user clicks the "options" action on their project
in ProjectActivity
. Just like ProjectActivity
and
TaskActivity
, MemberActivity
fetches the data for the
Recycler View in the setUpRecyclerView
method. However, instead of
querying against a realm, we'll instead use a call to the
getMyTeamMembers
function. This function returns a list of team
members as objects of the Document
type, so we'll use Kotlin's
built-in map
function to transform the objects in the list
returned by the function from type Document
to type Member
,
which is the type of data that MemberAdapter
expects. You can
access Realm Functions through the function manager
found in your project-global Realm app:
val functionsManager: Functions = taskApp.getFunctions(user) // get team members by calling a Realm Function which returns a list of members functionsManager.callFunctionAsync("getMyTeamMembers", ArrayList<String>(), ArrayList::class.java) { result -> if (result.isSuccess) { Log.v(TAG(), "successfully fetched team members. Number of team members: ${result.get().size}") // The `getMyTeamMembers` function returns team members as Document objects. Convert them into Member objects so the MemberAdapter can display them. this.members = ArrayList(result.get().map { item -> Member(item as Document) }) adapter = MemberAdapter(members, user!!) recyclerView.layoutManager = LinearLayoutManager(this) recyclerView.adapter = adapter recyclerView.setHasFixedSize(true) recyclerView.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL)) } else { Log.e(TAG(), "failed to get team members with: " + result.error) } }
Similar to TaskActivity
, we'll use the floating action button in
MemberActivity
to add users to the project. To handle this, write
logic in the floating action button's setPositiveButton()
callback
that adds a new user to the project using the addTeamMember()
Realm function. Because the team members displayed in this
view aren't live Realm Objects, this
RecyclerView's adapter isn't a subclass of
RealmRecyclerViewAdapter
, and we'll have to manually update our
local copy of the list of team members to reflect the success of this
method when we add a team member. To do this, call
setUpRecyclerView()
to reload the list of team members from the
backend when the addTeamMember()
Realm Function
successfully adds a team member:
val functionsManager: Functions = taskApp.getFunctions(user) functionsManager.callFunctionAsync( "addTeamMember", listOf(input.text.toString()), Document::class.java ) { result -> if (result.isSuccess) { Log.v(TAG(), "Attempted to add team member. Result: ${result.get()}") // rebuild the list of members to display the newly-added member setUpRecyclerView() } else { Log.e(TAG(), "failed to add team member with: " + result.error) Toast.makeText(this, result.error.errorMessage, Toast.LENGTH_LONG).show() } }
Handle Team Member Removals in MemberAdapter
MemberAdapter
handles the display of a list of team members
in a RecyclerView. All of the logic to handle this display is already
provided for you except for the logic that handles removing a team
member from a project when the user selects "Remove User". To
implement this logic, we'll have to call the removeTeamMember()
Realm Function. When the removeTeamMember()
Realm Function successfully removes a team member, we'll
have to manually remove that team member from the list of team members
with data.removeAt()
. Once you've removed the object from the
underlying dataset, just call notifyItemRemoved()
with the item's
position in the dataset and the UI should automatically stop
displaying the removed team member:
val functionsManager: Functions = taskApp.getFunctions(user) functionsManager.callFunctionAsync("removeTeamMember", listOf(obj.name), Document::class.java) { result -> run { dialog.dismiss() if (result.isSuccess) { Log.v(TAG(), "removed team member: ${result.get()}") data.removeAt(position) notifyItemRemoved(position) } else { Log.e(TAG(), "failed to remove team member with: " + result.error) Toast.makeText(parent.context, result.error.toString(), Toast.LENGTH_LONG).show() } } }
Run and Test
Once you have completed the code, you can run the app and check functionality.
Click the Run button in Android Studio. If the app builds successfully, here are some things you can try in the app:
- Create a user with email first@example.com.
- Explore the app, then log out or launch a second instance of the app on another virtual device.
- Create another user with email second@example.com.
- Navigate to second@example.com's project.
- Add, update, and remove some tasks.
- Navigate back to the list of projects in
ProjectActivity
with the back button. - Click the three dots "options" menu on "My Project".
- Add first@example.com to your team by clicking the floating action button and entering "first@example.com".
- Log out and log in as first@example.com.
- You should see two projects in the projects list, one of them labeled "second@example.com's project".
- Navigate to second@example.com's project.
- Collaborate by adding, updating, and removing some new tasks.
If something isn't working for you, you can check out the final
branch of
this repo to compare your code with our finished solution.
What's Next?
- Read our Java SDK documentation.
- Try the MongoDB Realm Backend tutorial.
- Find developer-oriented blog posts and integration tutorials on the MongoDB Developer Hub.
- Join the MongoDB Community forum to learn from other MongoDB developers and technical experts.
How did it go? Use the Give Feedback tab at the bottom right of the page to let us know if this tutorial was helpful or if you had any issues.