Building an Android Emoji Garden on Jetpack Compose with Realm
Rate this tutorial
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: . The app will reflect changes in the database in real-time on all devices that use it.
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.
Here we see the app at first run. We'll be creating two screens:
- A Login Screen.
- 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 anonymous logins in Realm.
- Setting up a globally accessible MongoDB Atlas instance to sync to your app's Realm database.
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.
Estimated time to complete: 2.5-3 hours
We will be adding imports into two files:
- Into the app level build.gradle.
- 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.
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 app. Yours doesn't need to look exactly like this yet. I'll tell you what to add.
In the app level build.gradle, we are going to add a few dependencies, shown below. They go into the dependencies block:
After adding them, your dependencies block should look like this. You could copy and replace the entire block in your app.
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.5.10. Compose needs this to function correctly.
Great! We're all done with imports. Remember to hit "Sync Now" at the top right.
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 Emoji Garden app is divided into two parts: the UI and the logic.
- The UI displays the emoji garden.
- The logic (classes and functions) will update the emoji garden from the server. This will keep the app in sync for all attendees.
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 .
Type in the name of the class you want to make— EmojiTile, for instance. Then hit Enter.
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.
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.
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.
Here's what the file looks like before we've made any changes.
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 (@ExperimentalFoundationApi) will be explained shortly.
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 without even deploying to an emulator or device. "Functions as UI" make UIs designed in Jetpack Compose incredibly modular.
The functions are:
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:
💡 Composing just means using inside something else—like calling one Jetpack Compose function inside another Jetpack Compose function.
Let's start from the smallest bit of UI, the holder for a single emoji.
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.
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.
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).
To make the garden, we'll be using the LazyVerticalGrid, which is like 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.
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.
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.
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.
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:
- 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:
- Create a Realm App on the cloud account
- Hit the Realm tab
- You're building a Mobile app for Android from scratch. How cool! Hit Start a New realm App.
- You can name your application anything you want. Even the default "Application 0" is fine.
- Enable Realm Sync
- This will allow real-time data synchronization between mobile clients.
- Click your application. It might have a different name.
- As in the image below, hit Sync (on the left) in the Realm tab. Then "Define Data Models" on the page that opens.
- 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.
💡 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."
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:
Open the AndroidManifest.xml file by double-tapping Shift in Android Studio and typing in "manifest."
Add the Internet permission to your Android Manifest above the application tag.
The file should start off like this after adding it:
Since Realm will be synchronizing data to the MongoDB database over the internet, the app needs internet access.
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:
- They must either extend RealmObject or implement the RealmClass interface.
- They require an _id field that's unique to their collection—e.g., ObjectId.
- They need a partition key.
- Their classes must remain open to extension.
- They have to be initialized with default values for all variables.
So, EmojiTile has to be changed to this:
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.
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.
Grab a copy of the RealmModule from the 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 section if you want to get right to using it.
We'll need to hold onto the Realm App object for logins later, so it's a class variable.
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.
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:
- Manually code the schema as a JSON schema document.
- Let Realm generate the schema from what's stored in the database already.
- 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.
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.
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.
ViewModels hold the logic and data for the UI. There will be one ViewModel each for the Login and Garden UIs.
What the LoginVm does:
- An anonymous login.
- Initializing the MongoDB Realm Schema.
Here's how the LoginVm works:
- 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.
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.
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.
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.
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.
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 in the Realm tab. Click Schema on the options on the left and you'd see this:
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:
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 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 architecture, and we'll be using . 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:
- With Realm alone handling the asynchronous data retrieval via Realm SDK functions. In the class EmojiVmRealm.
- 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 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 .
Here's how they work:
- 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.
- The ViewModel is set up as an AndroidViewModel to allow it to receive an Application object.
- Since Application is accessible from it, the RealmModule can be pulled in.
- 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.
A change listener watches for changes in the database. These changes might come from other people setting their emojis in their own apps.
💡 The Realm change listener is at the heart of reactive programming with Realm.
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.
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.
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.
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.
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).
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.
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.
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 again. Hit collections. Insert document will appear at the middle right. Then hit insert document.
Hit the curly braces so you can copy/paste huge pile of emojis into it.
You'll have all these emojis we just added to the server pop up on the device. Enjoy your critters!
Feel free to play around with the console. Change the emojis in collections by double-clicking them.
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!
In Part 2, I'll get to best practises for dealing with emojis using . 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.
Here's some additional reading if you'd like to learn more about what we did in this article.