Navigation

Android Kotlin Tutorial

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.

Check Out the Quick Start

If you prefer to explore on your own rather than follow a guided tutorial, check out the Android 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:

Set up the Mobile App

1

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 https://github.com/mongodb-university/realm-tutorial-android-kotlin.git

Important

The realm-tutorial-android-kotlin repository contains two branches: final and start. The final branch is a finished version of the app as it should look after you complete this tutorial. To walk through this tutorial, please check out the start branch:

git checkout start
2

Open the Project in Android Studio

Once the installation is complete, use Android Studio to open the project:

  1. Open Android Studio.
  2. In the “Welcome to Android Studio” window, click the “Open an existing Android Studio project” option.
  3. 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.
  4. Select the realm-tutorial-android-kotlin folder.
  5. Click “Open”.
  6. 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:

The login screen of the Task Tracker Android app.
3

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. The relevant files are as follows:

File Purpose
TaskTracker

A subclass of the Android Application class. Responsible for:

  • initializing the connection to MongoDB Realm
  • creating the Realm App object used by all other classes to communicate with the Realm backend
  • defining a Kotlin extension method called TAG() used in logging methods throughout the app, which dynamically fetches class names for logging tags.
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.

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/models directory.

4

Connect to Your MongoDB Realm App

Tasktracker uses the Gradle dependency management system to manage the Realm Android 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.

5

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.

6

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>.

Note

To learn more about how Realm Object models are used in Android applications, see Realm Objects in our Android client guide.

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(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:

val changeListener = OrderedRealmCollectionChangeListener<RealmResults<User>> { results, changeSet ->
    Log.i(TAG(), "User object initialized, displaying project list.")
    setUpRecyclerView(realm)
}
syncedUsers.addChangeListener(changeListener)

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 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"

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 finishes or stops:

override fun onDestroy() {
    super.onDestroy()
    userRealm?.close()
    recyclerView.adapter = null
}
override fun onStop() {
    super.onStop()
    user.run {
        userRealm?.close()
    }
}
7

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()
    }
}
8

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:

  1. Connects to the project realm using the partition member variable of the adapter.
  2. Queries the realm for the Task with the specified _id value.
  3. 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 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 (not async) because changeStatus should execute on a background thread
realm.executeTransaction {
    // 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 (not async) because remoteAt should execute on a background thread
realm.executeTransaction {
    // 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()
9

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 ProjectActivity, 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()
    }
}
10

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()
        }
    }
}
11

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

Reminder

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?

Leave Feedback

How did it go? Please let us know if this tutorial was helpful or if you had any issues by using the feedback widget on the bottom right of the page.