HomeLearnArticle

Building an Android Emoji Garden on Jetpacks! (Compose) with Realm

Published: Mar 04, 2021

  • Realm
  • MongoDB
  • Mobile
  • ...

By Aniket Kadam

Share

As an Android developer, have you wanted to get acquainted with Jetpack Compose and mobile architecture? Or maybe you have wanted to build an app end to end, with a hosted database? If yes, then this post is for you!

We'll be building an app that shows data from a central shared database: MongoDB Realm. The app will reflect changes in the database in real-time on all devices that use it.

A diagram of a mobile and a MongoDB Realm icon. Two boxes are in the mobile. One for 'App' and one for a database with the Realm logo on it. Depicts a local Realm database and a remote MongoDB Realm Sync server.

Imagine you're at a conference and you'd like to engage with the other attendees in a creative way. How about with emojis? ๐Ÿ˜‹ What if the conference had an app with a field of emojis where each emoji represents an attendee? Together, they create a beautiful garden. I call this app _Emoji Garden. I'll be showing you how to build such an app in this post.

This article is Part 1 of a two-parter where we'll just be building the core app structure and establishing the connection to Realm and sharing our emojis between the database and the app. Adding and changing emojis from the app will be in Part 2.

Opens with a login screen. It's clicked and a single brown tile emoji appears onscreen. Then the screen fills with brown tile emojis. Random tiles begin to be replaced by trees. Then, one by one, tiles turn to animal emojis. Animal emojis keep popping up until all brown tile emojis turn to animals.

Here we see the app at first run. We'll be creating two screens:

  1. A Login Screen.
  2. An Emoji Garden Screen updated with emojis directly from the server. It displays all the attendees of the conference as emojis.

Looks like a lot of asynchronous code, doesn't it? As we know, asynchronous code is the bane of Android development. However, you generally can't avoid it for database and network operations. In our app, we store emojis in the local Realm database. The local database seamlessly syncs with a MongoDB Realm Sync server instance in the background. Are we going to need other libraries like RxJava or Coroutines? Nope, we won't. In this article, we'll see how to get Realm to do this all for you!

If you prefer Kotlin Flows with Coroutines, then don't worry. The Realm SDK can generate them for you. I'll show you how to do that too. Let's begin!

Let me tempt you with the tech for Emoji Garden!

  • Using Jetpack Compose to put together the UI.
  • Using ViewModels and MVVM effectively with Compose.
  • Using Coroutines and Realm functions to keep your UI updated.
  • Using anonymous logins in Realm.
  • Setting up a globally accessible MongoDB Atlas instance to sync to your app's Realm database.

#Prerequisites

Remember that all of the code for the final app is available in the GitHub repo. If you'd like to build Emoji Garden๐ŸŒฒ with me, you'll need the following:

  1. Android Studio Canary, version "Arctic Fox Canary 3 (2020.3.1.3)" or later.
  2. A basic understanding of building Android apps, like knowing what an Activity is and having tried a bit of Java or Kotlin coding.

Emoji Garden shouldn't be the first Android app you've ever tried to build. However, it is a great intro into Realm and Jetpack Compose.

๐Ÿ’ก There's one prerequisite you'd need for anything you're doing and that's a growth mindset ๐ŸŒฑ. It means you believe you can learn anything. I believe in you!

Estimated time to complete: 2.5-3 hours

#Create a New Compose Project

Once you've got the Android Studio Canary, you can fire up the New Project menu and select Empty Compose Activity. Name your app "Emoji Garden" if you want the same name as mine.

Screenshot of Android Studio Canary Preview's New Project screen. Alongside the standard Basic Activity and Bottom Navigation Activity it has an option for Empty Compose Activity with a "preview" stamped across it.

#Project Imports

We will be adding imports into two files:

  1. Into the app level build.gradle.
  2. Into the project level build.gradle.

At times, I may refer to functions, classes, or variables by putting their names in italics, like EmojiClass, so you can tell what's a variable/constant/class and what isn't.

#App Level build.gradle Imports

First, the app level build.gradle. To open the app's build.gradle file, double-tap Shift in Android Studio and type "build.gradle." Select the one with "app" at the end and hit enter. Check out how build.gradle looks in the finished sample app. Yours doesn't need to look exactly like this yet. I'll tell you what to add.

A search field with build.gradle typed into it. The drop-down under it shows "build.gradle app" highlighted.

In the app level build.gradle, we are going to add a few dependencies, shown below. They go into the dependencies block:

1// For the viewModel function that imports them into activities
2implementation 'androidx.activity:activity-ktx:1.2.0-rc01'
3
4// For the ViewModelScope if using Coroutines in the ViewModel
5implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
6implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha06'

After adding them, your dependencies block should look like this. You could copy and replace the entire block in your app.

1dependencies {
2
3 implementation 'androidx.core:core-ktx:1.3.2'
4 implementation 'androidx.appcompat:appcompat:1.2.0'
5 implementation 'com.google.android.material:material:1.2.1'
6 testImplementation 'junit:junit:4.+'
7 androidTestImplementation 'androidx.test.ext:junit:1.1.2'
8 androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
9 implementation "androidx.compose.ui:ui:$compose_version"
10 implementation "androidx.compose.material:material:$compose_version"
11 implementation "androidx.compose.ui:ui-tooling:$compose_version"
12
13 // For the viewModel function that imports them into activities
14 implementation 'androidx.activity:activity-ktx:1.2.0-rc01'
15 // For the ViewModelScope if using Coroutines in the ViewModel
16 implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
17 implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha06'
18
19}

In the same file under android in the app level build.gradle, you should have the composeOptions already. Make sure the kotlinCompilerVersion is at least 1.4.21. Compose alpha9 needs this to function correctly.

1composeOptions {
2 kotlinCompilerExtensionVersion compose_version
3 kotlinCompilerVersion '1.4.21'
4}

#Project Level build.gradle Imports

Open the project level build.gradle file. Double-tap Shift in Android Studio -> type "build.gradle" and look for the one with a dot at the end. This is how it looks in the sample app. Follow along for steps.

A search field with build.gradle typed into it. The drop-down under it shows "build.gradle ." highlighted.

Make sure the compose version under buildscript is alpha09.

1buildscript {
2 ext {
3 compose_version = '1.0.0-alpha09'
4 }

Great! We're all done with imports. Remember to hit "Sync Now" at the top right.

A highlighted bar that says "Sync Now" and "Ignore these changes"

#Overview of the Emoji Garden App

#Folder Structure

com.example.emojigarden is the directory where all the code for the Emoji Garden app resides. This directory is auto-generated from the app name when you create a project. The image shown below is an overview of all the classes in the finished app. It's what we'll have when we're done with this article.

The folder structure with the folded titled com.example.emojigarden highlighted. It's in the Android view. Under app -> java -> com.example.emojigarden

#Building the Android App

The Emoji Garden app is divided into two parts: the UI and the logic.

  1. The UI displays the emoji garden.
  2. The logic (classes and functions) will update the emoji garden from the server. This will keep the app in sync for all attendees.

#Creating a New Source File

Let's create a file named EmojiTile inside a source folder. If you're not sure where the source folder is, here's how to find it. Hit the project tab (โŒ˜+1 on mac or Ctrl+1 on Windows/Linux).

Open the app folder -> java -> com.example.emojigarden or your package name. Right click on com.example.emojigarden to create new files for source code. For this project, we will create all source files here. To see other strategies to organize code, see package-by-feature.

Right clicked on the com.example.emojigarden folder in the project tab. Under New is the Kotlin Class/File option.

Type in the name of the class you want to makeโ€”EmojiTile, for instance. Then hit Enter.

EmojiTile is typed into the file name field. File is the option selected in the dropdown menu.

#Write the Emoji Tile Class

Since the garden is full of emojis, we need a class to represent the emojis. Let's make the EmojiTile class for this. Paste this in.

1class EmojiTile {
2 var emoji : String = ""
3}

#Let's Start with the Garden Screen

Here's what the screen will look like. When the UI is ready, the Garden Screen will display a grid of beautiful emojis. We still have some work to do in setting everything up.

Phone screen with a grid of different emojis.

#The Garden UI Code

Let's get started making that screen. We're going to throw away nearly everything in MainActivity.kt and write this code in its place.

Reach MainActivity.kt by double-tapping Shift and typing "mainactivity." Any of those three results in the image below will take you there.

Search with "mainac" typed in. The drop down shows "MainActivity.kt" in recent files and also in classes.

Here's what the file looks like before we've made any changes.

MainActivity.kt exist with a function of DefaultPreview and Greeting. MainActivity uses Greeting inside *setContent*.

Now, leave only the code below in MainActivity.kt apart from the imports. Notice how we've removed everything inside the setContent function except the MainActivityUi function. We haven't created it yet, so I've left it commented out. It's the last of the three sectioned UI below. The extra annotation (@ExperimentalFoundationLayout) will be explained shortly.

1@ExperimentalFoundationApi
2@ExperimentalLayout
3class MainActivity : AppCompatActivity() {
4
5 override fun onCreate(savedInstanceState: Bundle?) {
6 super.onCreate(savedInstanceState)
7 setContent {
8 // MainActivityUi(emptyList())
9
10 }
11 }
12}
#Missing Import Errors

If you see an error like this...

The *@ExperimentalLayout* annotation is underlined as an error. Android Studio is offering to import the class from *android.compose.foundation.layout.ExperimentalLayout*

...click on the line that has the error. Hit the shortcut that the error message is telling you about. On Mac, it says to hit โŒฅโ†ต. On Windows/Linux, it'll be Alt+Enter. This will add the imports for the @ExperimentalLayout annotation. Same for the other imports.

The UI code for the garden will be built up in three functions. Each represents one "view."

๐Ÿ’ก We'll be using a handful of functions for UI instead of defining it in the Android XML file. Compose uses only regular functions marked @Composeable to define how the UI should look. Compose also features interactive Previews without even deploying to an emulator or device. "Functions as UI" make UIs designed in Jetpack Compose incredibly modular.

The functions are:

  1. EmojiHolder
  2. EmojiGrid
  3. MainActivityUi

I'll show how to do previews right after the first function EmojiHolder. Each of the three functions will be written at the end of the MainActivity.kt file. That will put the functions outside the MainActivity class. Compose functions are independent of classes. They'll be composed together like this:

A box representing the EmojiHolder. Then another box representing the EmojiGrid. Inside this box is the EmojiHolder. Another box representing the MainActivityUi which contains box for the EmojiGrid.

๐Ÿ’ก Composing just means using inside something elseโ€”like calling one Jetpack Compose function inside another Jetpack Compose function.

#Single Emoji Holder

Let's start from the smallest bit of UI, the holder for a single emoji.

1@Composable
2fun EmojiHolder(emoji: EmojiTile) {
3 Text(emoji.emoji)
4}

The EmojiHolder function draws the emoji in a text box. The text function is part of Jetpack Compose. It's the equivalent of a TextView in the XML way making UI. It just needs to have some text handed to it. In this case, the text comes from the EmojiTile class.

#Previewing Your Code

A great thing about Compose functions is that they can be previewed right inside Android Studio. Drop this function into MainActivity.kt at the end.

1@Preview
2@Composable
3fun EmojiPreview() {
4 EmojiHolder(EmojiTile().apply { emoji = "๐Ÿ˜ผ" })
5}

You'll see the image below! If the preview is too small, click it and hit Ctrl+ or โŒ˜+ to increase the size. If it's not, choose the "Split View" (the larger arrow below). It splits the screen between code and previews. Previews are only generated once you've changed the code and hit the build icon. To rebuild the code, hit the refresh icon (the smaller green arrow below).

Composable functions of EmojiHolder and the preview for it. A giant cat emoji on the right. Above the preview to its left corner is a pair of refresh icons being pointed to with a smaller arrow. To its right are three icons for "Code," "Design," and "Split." A large green arrow points to "Split." It is also the currently selected choice, which is why both the preview and code are on-screen.

#The EmojiGrid

To make the garden, we'll be using the LazyVerticalGrid, which is like RecyclerView in Compose. It only renders items that are visible, as opposed to those that scroll offscreen. LazyVerticalGrid is a new class in Jetpack Compose version alpha9. Since it's experimental, it requires the @ExperimentalFoundationApi annotation. It's fun to play with though! Copy this into your project.

1@ExperimentalFoundationApi
2@ExperimentalLayout
3@Composable
4fun EmojiGrid(emojiList: List<EmojiTile>) {
5
6 LazyVerticalGrid(cells = GridCells.Adaptive(20.dp)) {
7 items(emojiList) { emojiTile ->
8 EmojiHolder(emojiTile)
9 }
10 }
11}

Note: You might have some missing import warnings like before.

#Garden Screen Container: MainActivityUI

Finally, the EmojiGrid is centered in a full-width Box. Box itself is a compose function.

๐Ÿ’ก Since my app was named "Emoji Garden," the auto-generated theme for it is EmojiGardenTheme. The theme name may be different for you. Type it in, if so.

Since the MainActivityUi is composed of EmojiGrid, which uses the @ExperimentalFoundationApi annotation, MainActivityUi now has to use the same annotation.

1@ExperimentalFoundationApi
2@ExperimentalLayout
3@Composable
4fun MainActivityUi(emojiList: List<EmojiTile>) {
5 EmojiGardenTheme {
6 Box(
7 Modifier.fillMaxWidth().padding(16.dp),
8 contentAlignment = Alignment.Center
9 ) {
10 EmojiGrid(emojiList)
11 }
12 }
13}

#Previews

Try previews for any of these! Here's a preview function for MainActivityUI. Preview functions should be in the same file as the functions they're trying to preview.

1@ExperimentalFoundationApi
2@ExperimentalLayout
3@Preview(showBackground = true)
4@Composable
5fun DefaultPreview() {
6 MainActivityUi(List(102){ i -> EmojiTile().apply { emoji = emojis[i] }})
7}
8
9val emojis = listOf("๐Ÿค","๐Ÿฆ","๐Ÿ”","๐Ÿฆค","๐Ÿ•Š","๏ธ","๐Ÿฆ†","๐Ÿฆ…","๐Ÿชถ","๐Ÿฆฉ","๐Ÿฅ","-","๐Ÿฃ","๐Ÿฆ‰","๐Ÿฆœ","๐Ÿฆš","๐Ÿง","๐Ÿ“","๐Ÿฆข","๐Ÿฆƒ","๐Ÿฆก","๐Ÿฆ‡","๐Ÿป","๐Ÿฆซ","๐Ÿฆฌ","๐Ÿˆ","โ€","โฌ›","๐Ÿ—","๐Ÿช","๐Ÿˆ","๐Ÿฑ","๐Ÿฟ","๏ธ","๐Ÿ„","๐Ÿฎ","๐ŸฆŒ","๐Ÿ•","๐Ÿถ","๐Ÿ˜","๐Ÿ‘","๐ŸฆŠ","๐Ÿฆ’","๐Ÿ","๐Ÿฆ","๐Ÿฆฎ","๐Ÿน","๐Ÿฆ”","๐Ÿฆ›","๐ŸŽ","๐Ÿด","๐Ÿฆ˜","๐Ÿจ","๐Ÿ†","๐Ÿฆ","๐Ÿฆ™","๐Ÿฆฃ","๐Ÿ’","๐Ÿต","๐Ÿ","๐Ÿญ","๐Ÿฆง","๐Ÿฆฆ","๐Ÿ‚","๐Ÿผ","๐Ÿพ","๐Ÿ–","๐Ÿท","๐Ÿฝ","๐Ÿป","โ€","โ„","๏ธ","๐Ÿฉ","๐Ÿ‡","๐Ÿฐ","๐Ÿฆ","๐Ÿ","๐Ÿ€","๐Ÿฆ","๐Ÿ•","โ€","๐Ÿฆบ","๐Ÿฆจ","๐Ÿฆฅ","๐Ÿ…","๐Ÿฏ","๐Ÿซ","-","๐Ÿฆ„","๐Ÿƒ","๐Ÿบ","๐Ÿฆ“","๐Ÿณ","๐Ÿก","๐Ÿฌ","๐ŸŸ","๐Ÿ™","๐Ÿฆญ","๐Ÿฆˆ","๐Ÿš","๐Ÿณ","๐Ÿ ","๐Ÿ‹","๐ŸŒฑ","๐ŸŒต","๐ŸŒณ","๐ŸŒฒ","๐Ÿ‚","๐Ÿ€","๐ŸŒฟ","๐Ÿƒ","๐Ÿ","๐ŸŒด","๐Ÿชด","๐ŸŒฑ","โ˜˜","๏ธ","๐ŸŒพ","๐ŸŠ","๐ŸŠ","๐Ÿ‰","๐Ÿฒ","๐ŸฆŽ","๐Ÿฆ•","๐Ÿ","๐Ÿฆ–","-","๐Ÿข")

Here's a preview generated by the code above. Remember to hit the build arrows if it doesn't show up.

A screen inside Android Studio that shows a grid of the first 102 of the emojis above.

You might notice that some of the emojis aren't showing up. That's because we haven't begun to use EmojiCompat yet. We'll get to that in the next article.

#Login Screen

You can use a Realm database locally without logging in. Syncing data requires a user account. Let's take a look at the UI for login since we'll need it soon. If you're following along, drop this into the MainActivity.kt, at the end of the file. The login screen is going to be all of one button. Notice that the actual login function is passed into the View. Later, we'll make a ViewModel named LoginVm. It will provide the login function.

1@Composable
2fun LoginView(login : () -> Unit) {
3 Column(modifier = Modifier.fillMaxWidth().padding(16.dp),
4 verticalArrangement = Arrangement.Center,
5 horizontalAlignment = Alignment.CenterHorizontally){
6
7 Button(login){
8 Text("Login")
9 }
10 }
11}

#Set Up Realm Sync

We've built as much of the app as we can without Realm. Now it's time to enable storing our emojis locally. Then we can begin syncing them to your own managed Realm instance in the cloud.

Now we need to:

  1. Create a free MongoDB Atlas account

    • Follow the link above to host your data in the cloud. The emojis in the garden will be synced to this database so they can be sent to all connecting mobile devices. Configure your Atlas account with the following steps:
    • Add your connection IP, so only someone with your IP can access the database.
    • Create a database user, so you have an admin user to run commands with. Note down the username and password you create here.
  2. Create a Realm App on the cloud account

    • Hit the Realm tab
    • An arrow pointing out the Realm tab on the http://cloud.mongodb.com next to the Atlas tab.
    • You're building a Mobile app for Android from scratch. How cool! Hit Start a New realm App.
    • A form where you specify your primary platform as mobile, mobile platform as Android and that you're starting from scratch. It's what you see under the Realm tab.
    • You can name your application anything you want. Even the default "Application 0" is fine.
  3. Turn on Anonymous authentication - We don't want to make people wait around to authenticate with a username and password. So, we'll just hand them a login button that will perform an anonymous authentication. Follow the link in the title to turn it on.
  4. Enable Realm Sync

    • This will allow real-time data synchronization between mobile clients.
    • Go to https://cloud.mongodb.com and hit the Realm tab.
    • Click your application. It might have a different name.
    • An arrow pointing to the first application under the Realm tab. Might be named Application-0 or whatever you chose to name it.
    • As in the image below, hit Sync (on the left) in the Realm tab. Then "Define Data Models" on the page that opens.
    • An arrow pointing to Define Data Models under the Sync tab.
    • Choose the default cluster. For the partition key, type in "event" and select a type of "string" for it. Under "Define a database name," type in "gardens." Hit "Turn Dev Mode On" at the bottom.
    • Partition key: event. Field type "string." "Required" is checked. Define a database name says "gardens."

๐Ÿ’ก For this use case, the "partition key" should be named "event" and be of type "String." We'll see why when we add the partition key to our EmojiTile later. The partition key is a way to separate data within the collection by when it's going to be used.

Fill in those details and hit "Turn Dev Mode On." Now click "Review and Deploy."

#Integrating Realm into the App

#Install the SDK

Install the Realm Android SDK

Follow the link above to install the SDK. This provides Realm authentication and database methods within the app. When they talk about adding "apply plugin:" just replace that with "id," like in the image below:

The plugins section where instead of *apply plugin: "kotlin kapt"* it says *id "kotlin-kapt"*.

#Add Internet Permissions

Open the AndroidManifest.xml file by double-tapping Shift in Android Studio and typing in "manifest."

A search bar with "manife" typed and the drop down shows "AndroidManifest.xml app/src/main"

Add the Internet permission to your Android Manifest above the application tag.

1<uses-permission android:name="android.permission.INTERNET" />

The file should start off like this after adding it:

1<?xml version="1.0" encoding="utf-8"?>
2<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.emojigarden">
3
4 <uses-permission android:name="android.permission.INTERNET"/>
5
6 <application

Since Realm will be synchronizing data to the MongoDB database over the internet, the app needs internet access.

#Updating the Emoji Class

It's time to add Realm to that EmojiTile class we made earlier. This is required to store it into Realm. Objects (emojis) that we intend to store in Realm and sync into MongoDB have to follow certain rules:

  1. They must either extend RealmObject or implement the RealmClass interface.
  2. They require an _id field that's unique to their collectionโ€”e.g., ObjectId.
  3. They need a partition key.
  4. Their classes must remain open to extension.
  5. They have to be initialized with default values for all variables.

So, EmojiTile has to be changed to this:

1@RealmClass
2open class EmojiTile : RealmModel {
3 @PrimaryKey
4 var _id : ObjectId = ObjectId.get()
5 var index = 0
6 var emoji : String = ""
7 var event : String = "default" // partition key
8}

An emoji garden will be composed of many such "tiles."

๐Ÿ’ก "event" might seem a strange name for a field for an emoji. Here, it's the partition key. Emojis for a single garden will be assigned the same partition key. Each instance of Realm on mobile can only be configured to retrieve objects with one partition key.

#Separating Your Concerns

We're going to need objects from the Realm Mobile SDK that give access to login and data functions. These will be abstracted into their own class, called RealmModule.

Later, I'll create a custom application class EmojiGardenApplication to instantiate RealmModule. This will make it easy to pass into the ViewModels.

#RealmModule

Grab a copy of the RealmModule from the sample repo. This will handle Realm App initialization and connecting to a synced instance for you. It also contains a method to log in. Copy/paste it into the source folder. You might end up with duplicate package declarations. Delete the extra one, if so. Let's take a look at what's in RealmModule. Skip to the next section if you want to get right to using it.

#The Constructor and Class Variables

The init{ } block is like a Kotlin constructor. It'll run as soon as an instance of the class is created. Realm.init is required for local or remote Realms. Then, a configuration is created from your appId as part of initialization, as seen here. To get a synced realm, we need to log in.

We'll need to hold onto the Realm App object for logins later, so it's a class variable.

1private var syncedRealm: Realm? = null
2private val app : App
3private val TAG = RealmModule::class.java.simpleName
4
5init {
6 Realm.init(application)
7 app = App(AppConfiguration.Builder(appId).build())
8
9 // Login anonymously because a logged in user is required to open a synced realm.
10 loginAnonSyncedRealm(
11 onSuccess = {Log.d(TAG, "Login successful") },
12 onFailure = {Log.d(TAG, "Login Unsuccessful, are you connected to the net?")}
13 )
14}
#The Login Function

Before you can add data to a synced Realm, you need to be logged in. You only need to be online the first time you log in. Your credentials are preserved and data can be inserted offline after that.

Note the partition key. Only objects with the same value for the partition key as specified here will be synced by this Realm instance. To sync objects with different keys, you would need to create another instance of Realm. Once login succeeds, the logged-in user object is used to instantiate the synced Realm.

1fun loginAnonSyncedRealm(partitionKey : String = "default", onSuccess : () -> Unit, onFailure : () -> Unit ) {
2
3 val credentials = Credentials.anonymous()
4
5 app.loginAsync(credentials) { loginResult ->
6 Log.d("RealmModule", "logged in: $loginResult, error? : ${loginResult.error}")
7 if (loginResult.isSuccess) {
8 instantiateSyncedRealm(loginResult.get(), partitionKey)
9 onSuccess()
10 } else {
11 onFailure()
12 }
13 }
14
15}
16
17private fun instantiateSyncedRealm(user: User?, partition : String) {
18 val config: SyncConfiguration = SyncConfiguration.defaultConfig(user, partition)
19 syncedRealm = Realm.getInstance(config)
20}
#Initialize the Realm Schema

Part of the setup of Realm is telling the server a little about the data types it can expect. This is only important for statically typed programming languages like Kotlin, which would refuse to sync objects that it can't cast into expected types.

๐Ÿ’ก There are a few ways to do this:

  1. Manually code the schema as a JSON schema document.
  2. Let Realm generate the schema from what's stored in the database already.
  3. Let Realm figure out the schema from the documents at the time they're pushed into the db from the mobile app.

We'll be doing #3.

If you're wondering where the single soil emoji comes from when you log in, it's from this function. It will be called behind the scenes (in LoginVm) to set up the schema for the EmojiTile collection. Later, when we add emojis from the server, it'll have stronger guarantees about what types it contains.

1fun initializeCollectionIfEmpty() {
2 syncedRealm?.executeTransactionAsync { realm ->
3 if (realm.where(EmojiTile::class.java).count() == 0L) {
4 realm.insert(EmojiTile().apply {
5 emoji = "๐ŸŸซ"
6 })
7 }
8 }
9}
#Minor Functions

getSyncedRealm Required to work around the fact that syncedRealm must be nullable internally. The internal nullability is used to figure out whether it's initialized. When it's retrieved externally, we'd always expect it to be available and so we throw an exception if it isn't.

1fun isInitialized() = syncedRealm != null
2
3fun getSyncedRealm() : Realm = syncedRealm ?: throw IllegalStateException("loginAnonSyncedRealm has to return onSuccess first")

#EmojiGarden Custom Application

Create a custom application class for the Emoji Garden app which will instantiate the RealmModule.

Remember to add your appId to the appId variable. You could name the new class EmojiGardenApplication.

1class EmojiGardenApplication : Application() {
2 lateinit var realmModule : RealmModule
3
4 override fun onCreate() {
5 super.onCreate()
6
7 // Get your appId from https://realm.mongodb.com/ for the database you created under there.
8 val appId = "your appId here"
9 realmModule = RealmModule(this, appId)
10 }
11}

#ViewModels

ViewModels hold the logic and data for the UI. There will be one ViewModel each for the Login and Garden UIs.

#Login ViewModel

What the LoginVm does:

  1. An anonymous login.
  2. Initializing the MongoDB Realm Schema.

Copy LoginVm's complete code from here.

Here's how the LoginVm works:

  1. Retrieve an instance of the RealmModule from the custom application.
  2. Once login succeeds, it adds initial data (like a ๐ŸŸซ emoji) to the database to initialize the Realm schema.

๐Ÿ’ก Initializing the Realm schema is only required right now because the app doesn't provide a way to choose and insert your emojis. At least one inserted emoji is required for Realm Sync to figure out what kind of data will be synced. When the app is written to handle inserts by itself, this can be removed.

showGarden will be used to "switch" between whether the Login screen or the Garden screen should be shown. This will be covered in more detail later. It is marked "private set" so that it can't be modified from outside LoginVm.

1var showGarden : Boolean by mutableStateOf(getApplication<EmojiGardenApplication>().realmModule.isInitialized())
2 private set

initializeData will insert a sample emoji into Realm Sync. When it's done, it will signal for the garden to be shown. We're going to call this after login.

1private fun initializeData() {
2 getApplication<EmojiGardenApplication>().realmModule.initializeCollectionIfEmpty()
3 showGarden = true
4}

login calls the equivalent function in RealmModule as seen earlier. If it succeeds, it initializes the data. Failures are only logged, but you could do anything with them.

1fun login() = getApplication<EmojiGardenApplication>().realmModule.loginAnonSyncedRealm(
2 onSuccess = ::initializeData,
3 onFailure = { Log.d(TAG, "Failed to login") }
4 )

You can now modify MainActivity.kt to display and use Login. You might need to import the viewModel function. Android Studio will give you that option.

1@ExperimentalFoundationApi
2@ExperimentalLayout
3class MainActivity : AppCompatActivity() {
4
5 override fun onCreate(savedInstanceState: Bundle?) {
6 super.onCreate(savedInstanceState)
7 setContent {
8 val loginVm : LoginVm = viewModel()
9 if(!loginVm.showGarden) {
10 LoginView(loginVm::login)
11 }
12 }
13 }
14}

Once you've hit login, the button will disappear, leaving you a blank screen. Let's understand what happened and get to work on the garden screen, which should appear instead.

๐Ÿ’ก If you get an error like "Caused by: java.lang.ClassCastException: android.app.Application cannot be cast to EmojiGardenApplication at com.example.emojigarden.LoginVm.<init>(LoginVm.kt:20)," then you might have forgotten to add the EmojiGardenApplication to the name attribute in the manifest.

#What Initialization Did

Here's how you can verify what happened because of the initialization. Before logging in and sending the first EmojiTile, you could go look up your data's schema by going to https://cloud.mongodb.com in the Realm tab. Click Schema on the options on the left and you'd see this:

An arrow pointing to Schema under Data Access.

MongoDB Realm Sync has inferred the data types in EmojiTile when the first EmojiTile was pushed up. Here's what that section says now instead:

The gardens.EmojiTile now has a schema defined in json under the schema tab.

If we had inserted data on the server side prior to this, it would've defaulted the index field type to Double instead. The Realm SDK would not have been able to coerce it on mobile, and sync would've failed.

#The Garden ViewModel

The UI code is only going to render data that is given to them by the ViewModels, which is why if you run the app without previews, everything has been blank so far.

As a refresher, we're using the MVVM architecture, and we'll be using Android ViewModels. The ViewModels that we'll be using are custom classes that extend the ViewModel class. They implement their own methods to retrieve and hold onto data that UI should render. In this case, that's the EmojiTile objects that we'll be loading from the MongoDB Realm Sync server.

I'm going to demonstrate two ways to do this:

  1. With Realm alone handling the asynchronous data retrieval via Realm SDK functions. In the class EmojiVmRealm.
  2. With Kotlin Coroutines Flow handling the data being updated asynchronously, but with Realm still providing the data. In the class EmojiVmFlow.

Either way is fine. You can pick whichever way suits you. You could even swap between them by changing a single line of code. If you would like to avoid any asynchronous handling of data by yourself, use EmojiVmRealm and let Realm do all the heavy lifting! If you are already using Kotlin Flows, and would like to use that model of handling asynchronous operations, use EmojiVmFlow.

Here's what's common to both ViewModels.

Take a look at the code of EmojiVmRealm and EmojiVmFlow side by side.

Here's how they work:

  1. The emojiState variable is observed by Compose since it's created via the mutableStateOf. It allows Jetpack Compose to observe and react to values when they change to redraw the UI. Both ViewModels will get data from the Realm database and update the emojiState variable with it. This separates the code for how the UI is rendered from how the data for it is retrieved.
  2. The ViewModel is set up as an AndroidViewModel to allow it to receive an Application object.
  3. Since Application is accessible from it, the RealmModule can be pulled in.
  4. RealmModule was instantiated in the custom application so that it could be passed to any ViewModel in the app.

    • We get the Realm database instance from the RealmModule via getSyncedRealm.
    • Searching for EmojiTile objects is as simple as calling where(EmojiTile::class.java).
    • Calling .sort on the results of where sorts them by their index in ascending order.
    • They're requested asynchronously with findAllAsync, so the entire operation runs in a background thread.

#EmojiVmRealm

EmojiVmRealm is a class that extends ViewModel. Take a look at the complete code and copy it into your source folder. It provides logic operations and updates data to the Jetpack Compose UI. It uses standard Realm SDK functionality to asynchronously load up the emojis and order them for display.

Apart from what the two ViewModels have in common, here's how this class works:

#Realm Change Listeners

A change listener watches for changes in the database. These changes might come from other people setting their emojis in their own apps.

1private val emojiTilesResults : RealmResults<EmojiTile> = getApplication<EmojiGardenApplication>().realmModule
2 .getSyncedRealm()
3 .where(EmojiTile::class.java)
4 .sort(EmojiTile::index.name)
5 .findAllAsync()
6 .apply {
7 addChangeListener(emojiTilesChangeListener)
8 }

๐Ÿ’ก The Realm change listener is at the heart of reactive programming with Realm.

1private val emojiTilesChangeListener =
2 OrderedRealmCollectionChangeListener<RealmResults<EmojiTile>> { updatedResults, _ ->
3 emojiState = updatedResults.freeze()
4 }

The change listener function defines what happens when a change is detected in the database. Here, the listener operates on any collection of EmojiTiles as can be seen from its type parameter of RealmResults<EmojiTile>. In this case, when changes are detected, the emojiState variable is reassigned with "frozen" results.

The freeze function is part of the Realm SDK and makes the object immutable. It's being used here to avoid issues when items are deleted from the server. A delete would invalidate the Realm object, and if that object was providing data to the UI at the time, it could lead to crashes if it wasn't frozen.

#MutableState: emojiState

1import androidx.compose.runtime.getValue
2import androidx.compose.runtime.neverEqualPolicy
3import androidx.compose.runtime.setValue
4
5 var emojiState : List<EmojiTile> by mutableStateOf(listOf(), neverEqualPolicy())
6 private set

emojiState is a mutableStateOf which Compose can observe for changes. It's been assigned a private set, which means that its value can only be set from inside EmojiVmRealm for code separation. When a change is detected, the emojiState variable is updated with results. The changeset isn't required so it's marked "_".

neverEqualPolicy needs to be specified since Mutable State's default structural equality check doesn't see a difference between updated RealmResults. neverEqualPolicy is then required to make it update. I specify the imports here because sometimes you'd get an error if you didn't specifically import them.

1private val emojiTilesChangeListener =
2 OrderedRealmCollectionChangeListener<RealmResults<EmojiTile>> { updatedResults, _ ->
3 emojiState = updatedResults.freeze()
4}

Change listeners have to be released when the ViewModel is being disposed. Any resources in a ViewModel that are meant to be released when it's being disposed should be in onCleared.

1override fun onCleared() {
2 super.onCleared()
3 emojiTilesResults.removeAllChangeListeners()
4}

#EmojiVmFlow

EmojiVmFlow offloads some asynchronous operations to Kotlin Flows while still retrieving data from Realm. Take a look at it in the sample repo here, and copy it to your app.

Apart from what the two ViewModels have in common, here's what this VM does:

The toFlow operator from the Realm SDK automatically retrieves the list of emojis when they're updated on the server.

1private val _emojiTiles : Flow<RealmResults<EmojiTile>> = getApplication<EmojiGardenApplication>().realmModule
2 .getSyncedRealm()
3 .where(EmojiTile::class.java)
4 .sort(EmojiTile::index.name)
5 .findAllAsync()
6 .toFlow()

The flow is launched in viewModelScope to tie it to the ViewModel lifecycle. Once collected, each emitted list is stored in the emojiState variable.

1init {
2 viewModelScope.launch {
3 _emojiTiles.collect {
4 emojiState = it
5 }
6 }
7}

Since viewModelScope is a built-in library scope that's cleared when the ViewModel is shut down, we don't need to bother with disposing of it.

#Switching UI Between Login and Gardens

As we put both the screens together in the view for the actual Activity, here's what we're trying to do:

First, connect the LoginVm to the view and check if the user is authenticated. Then:

  • If authenticated, show the garden.
  • If not authenticated, show the login view.
  • This is done via if(loginVm.showGarden).

Take a look at the entire activity in the repo. The only change we'll be making is in the onCreate function. In fact, only the setContent function is modified to selectively show either the Login or the Garden Screen (MainActivityUi). It also connects the ViewModels to the Garden Screen now.

The LoginVm internally maintains whether to showGarden or not based on whether the login succeeded. If this succeeds, the garden screen MainActivityUI is instantiated with its own ViewModel, supplying the emojis it gathers from Realm. If the login hasn't happened, it shows the login view.

๐Ÿ’ก The code below uses EmojiVmRealm. If you were using EmojiVmFlow, just type in EmojiVmFlow instead. Everything will just work.

1@ExperimentalFoundationApi
2@ExperimentalLayout
3class MainActivity : AppCompatActivity() {
4
5 override fun onCreate(savedInstanceState: Bundle?) {
6 super.onCreate(savedInstanceState)
7 setContent {
8 val loginVm : LoginVm = viewModel()
9
10 if(loginVm.showGarden){
11 val model : EmojiVmRealm = viewModel()
12 MainActivityUi(model.emojiState)
13 } else
14 {
15 LoginView(loginVm::login)
16 }
17
18 }
19 }
20}

#Tending the Garden Remotely

Here's what you'll have on your app once you're all logged in and the garden screen is hooked up too: a lone ๐ŸŸซ emoji on a vast, empty screen.

The phone screen with a ๐ŸŸซ at the top left.

Let's move to the server to add some more emojis and let the server handle sending them back to the app! Every user of the app will see the same list of emojis. I'll show how to insert the emojis from the web console.

Open up https://cloud.mongodb.com again. Hit collections. Insert document will appear at the middle right. Then hit insert document.

Arrow pointing to collections on the clusters tab. Then "insert document" when the collections tab opens.

Hit the curly braces so you can copy/paste this huge pile of emojis into it.

Arrow pointing to curly braces icon on the insert object dialog. The text of the emojis linked `here <https://github.com/mongodb-developer/EmojiGarden/blob/main/EmojisForDatabaseInsert.txt>`__ has been pasted into the field.

You'll have all these emojis we just added to the server pop up on the device. Enjoy your critters!

Phone screen with various animal, tree, and brown tile emojis.

Feel free to play around with the console. Change the emojis in collections by double-clicking them.

#Summary

This has been a walkthrough for how to build an Android app that effectively uses Compose and Realm together with the latest techniques to build reactive apps with very little code.

In this article, we've covered:

  • Using the MVVM architectural pattern with Jetpack Compose.
  • Setting up MongoDB Realm.
  • Using Realm in ViewModels.
  • Using Realm to Kotlin Flows in ViewModels.
  • Using anonymous authentication in Realm.
  • Building Conditional UIs with Jetpack Compose.

There's a lot here to add to any of your projects. Feel free to use any parts of this walkthrough or use the whole thing! I hope you've gotten to see what MongoDB Realm can do for your mobile apps!

#What's Next?

In Part 2, I'll get to best practises for dealing with emojis using EmojiCompat. I'll also get into how to change the emojis from the device itself and add some personalization that will enhance the app's functionality. In addition, we'll also have to add some "rules" to handle use casesโ€”for example, users can only alter unclaimed "soil" tiles and handle conflict resolution when two users try to claim the same tile simultaneously. What happens when two people pick the same tiles at nearly the same time? Who gets to keep it? How can we avoid pranksters changing our own emojis? These questions and more will be answered in Part 2.

#References

Here's some additional reading if you'd like to learn more about what we did in this article.

  1. Also, thanks to Monica Dinculescu for coming up with the idea for the garden on the web. This is an adaptation of her ideas.

If you have questions, please head to our developer community website where the MongoDB engineers and the MongoDB community will help you build your next big idea with MongoDB.

MongoDB Icon
  • Developer Hub
  • Documentation
  • University
  • Community Forums

ยฉ MongoDB, Inc.