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:
MongoDB Realm. 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.
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:
- 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
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. 

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 sample
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.
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.
Make sure the compose version under buildscript is 1.x.x or greater.

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 package-by-feature.
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 Previews without even deploying to an emulator or device. "Functions as UI" make UIs designed in Jetpack Compose incredibly modular.
The functions are:
- EmojiHolder
- EmojiGrid
- 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: 

💡 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
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.
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.
Here's a preview generated by the code above. Remember to hit the build arrows if it doesn't show up.
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.

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:
- 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.
- 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.
- 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.
- 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 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 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.
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.
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.
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.
💡 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.
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:
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 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:
- 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
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 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.
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.
The toFlow operator from the Realm SDK automatically retrieves the list of emojis when they're updated on the server.
The flow is launched in
viewModelScope
to tie it to the ViewModel lifecycle. Once collected, each emitted list
is stored in the emojiState variable.
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).
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.
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 https://cloud.mongodb.com again. Hit collections. Insert
document will appear at the middle right. Then hit insert document.
Hit the curly braces so you can copy/paste
this
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
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.
Here's some additional reading if you'd like to learn more about what we
did in this article.
- 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.