Docs Menu

Quick Start with LiveData - Android SDK

On this page

This page contains instructions to quickly get Realm Database integrated into an example Android application that uses LiveData. This example application allows a user to increment a counter using a button.

This quick start guide uses Realm Sync to synchronize data changes between clients. Before you begin, ensure you have:

Note
Using LiveData without Realm Sync

To use this quick start without Realm Sync, disable the sync features in the SDK. You can do this by removing the following lines from your app-level build.gradle file:

realm {
syncEnabled = true
}

After removing the lines, re-synchronize the Gradle configuration to reload the Android SDK in an offline-only state. Remove the lines related to importing and using Sync Configuration, user login, and partition values from the CounterModel file to use the Android SDK without Realm Sync.

To get started, copy the example repo into your local environment.

We've already put together an 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-android-livedata.git
Important

The realm-quickstart-android-livedata 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

Now that you've cloned the repo, you need to add the dependencies you'll need to run the Realm Android SDK and Android LiveData. Begin by adding the Realm Android SDK dependency to the buildscript.dependencies block of your project level build.gradle file:

buildscript {
ext.kotlin_version = "1.4.10"
repositories {
google()
jcenter()
}
dependencies {
classpath "com.android.tools.build:gradle:4.0.2"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "io.realm:realm-gradle-plugin:10.2.0"
}
}

You'll also have to add the Android LiveData Dependency to the dependencies block of your app level build.gradle file:

dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.2.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.1'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.1'
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}

Next, configure the Realm Android SDK to enable Realm Sync by creating the following top-level block in your app level build.gradle file:

realm {
syncEnabled = true
}

Then, enable DataBinding by creating the following block in the android block of your app level build.gradle file:

android {
compileSdkVersion 30
buildToolsVersion "30.0.2"
defaultConfig {
applicationId "com.mongodb.realm.livedataquickstart"
minSdkVersion 16
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildFeatures {
dataBinding true
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}

Finally, click the "Sync" button or select Build > Rebuild Project in the application menu to reconfigure your Gradle configuration with these changes and fetch the dependencies.

With all of the dependencies in place, it's time to create a LiveData-compatible interface for our Realm objects. To do so, we'll have to handle a few events:

  • The onActive() method enables an observer to subscribe to changes to the underlying Realm object by adding a change listener.

    override fun onActive() {
    super.onActive()
    val obj = value
    if (obj != null && RealmObject.isValid(obj)) {
    RealmObject.addChangeListener(obj, listener)
    }
    }
  • The onInactive() method enables an observer to unsubscribe to changes to the underlying Realm object by removing the change listener.

    override fun onInactive() {
    super.onInactive()
    val obj = value
    if (obj != null && RealmObject.isValid(obj)) {
    RealmObject.removeChangeListener(obj, listener)
    }
    }
  • When a change occurs, the listener member uses the setValue() method of the LiveData parent class to pass the Realm object's value to the UI unless the object was deleted, in which case the change listener passes a value of null instead of passing along a reference to an invalid, deleted object.

    private val listener =
    RealmObjectChangeListener<T> { obj, objectChangeSet ->
    if (!objectChangeSet!!.isDeleted) {
    setValue(obj)
    } else { // Because invalidated objects are unsafe to set in LiveData, pass null instead.
    setValue(null)
    }
    }
Note
Using LiveData with Collections of RealmResults

Even though this example project doesn't use LiveData to display RealmResults, we've also provided an example implementation that follows these same principles to encapsulate a collection of data contained in a RealmResults object, called LiveRealmResults.

This application stores all of its logic and core data within a ViewModel called CounterModel. When the application runs, it creates an instance of CounterModel that is used until the application closes. That instance contains the LiveData that displays on the UI of the application. To create an instance of LiveData, we need to access a Counter object stored in a realm and pass it to the LiveRealmObject constructor. To accomplish this:

  1. Connect to your Realm app.
  2. Authenticate a user.
  3. Connect to a specific realm using Realm Sync.
  4. Query the realm for a Counter, inserting a new Counter if one hasn't already been created in this realm.
  5. Instantiate a LiveRealmObject using the Counter instance and store it in the counter member of CounterModel.

The following code snippet implements this behavior:

init {
val appID = "YOUR APP ID HERE" // TODO: replace this with your App ID
// 1. connect to the MongoDB Realm app backend
val app = App(
AppConfiguration.Builder(appID)
.build()
)
// 2. authenticate a user
app.loginAsync(Credentials.anonymous()) {
if(it.isSuccess) {
Log.v("QUICKSTART", "Successfully logged in anonymously.")
// 3. connect to a realm with Realm Sync
val user: User? = app.currentUser()
val partitionValue = "example partition"
val config = SyncConfiguration.Builder(user!!, partitionValue)
// because this application only reads/writes small amounts of data, it's OK to read/write from the UI thread
.allowWritesOnUiThread(true)
.allowQueriesOnUiThread(true)
.build()
// open the realm
realm = Realm.getInstance(config)
// 4. Query the realm for a Counter, creating a new Counter if one doesn't already exist
// access all counters stored in this realm
val counterQuery = realm!!.where<Counter>()
val counters = counterQuery.findAll()
// if we haven't created the one counter for this app before (as on first launch), create it now
if (counters.size == 0) {
realm?.executeTransaction { transactionRealm ->
val counter = Counter()
transactionRealm.insert(counter)
}
}
// 5. Instantiate a LiveRealmObject using the Counter and store it in a member variable
// the counters query is life, so we can just grab the 0th index to get a guaranteed counter
this._counter.postValue(counters[0]!!)
} else {
Log.e("QUICKSTART", "Failed to log in anonymously. Error: ${it.error.message}")
}
}
}
Important
Fill in Your App ID to connect to your Realm App with the Android SDK

To connect to your backend instance of Realm Sync, replace the App ID placeholder with your Realm app's App ID.

Note
Reads and Writes on the UI Thread

Reads and writes are expensive, frequently involving context switches and disk I/O operations. As a result, reads and writes are disabled by default on the UI thread. While you can use the allowWritesOnUiThread() and allowQueriesOnUiThread() config builder methods to enable writes and reads, respectively, on the UI thread, you should almost always defer reads and writes to a background thread using asynchronous patterns.

To display the data stored in the CounterModel on the application UI, we'll need to access the CounterModel singleton using the viewModels() method when the application creates CounterFragment. Once we've instantiated the model, we can use the Android Data Binding library to display the model's data in UI elements.

To access the CounterModel singleton when the application creates CounterFragment, place the following code in the onCreateView() method of CounterFragment:

val model: CounterModel by viewModels()

Next, set up the Data Binding hooks in the UI for the counter fragment:

<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<variable
name="counterModel"
type="com.mongodb.realm.livedataquickstart.model.CounterModel"
/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".CounterFragment">
<TextView
android:id="@+id/textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{counterModel.counter.value.get().toString()}"
android:textSize="58pt"
app:layout_constraintBottom_toTopOf="@id/button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/add"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textview" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Finally, connect the model to the binding so that the UI can display the counter and iterate the counter on button press with the following code in the onCreateView() method of CounterFragment:

val binding = CounterFragmentBinding.inflate(inflater, container, false).apply {
lifecycleOwner = viewLifecycleOwner
counterModel = model
}
binding.root.button.setOnClickListener {
Log.v("QUICKSTART", "Clicked increment button. Current value: ${model.counter.value?.value?.get()}")
model.incrementCounter()
}
return binding.root

Now you should be able to run the sample application. You should see an interface that looks something like this:

The LiveData QuickStart Counter app.

Clicking the "ADD" button should add one to the value of your counter. With Realm Sync, you can view your Realm app logs to see individual increment events. Android LiveData is lifecycle-aware, so rotating the screen or freeing the application's state by clearing your device's RAM should have no effect on the application state, which should seamlessly resume and automatically resubscribe to events on resume using the state stored in the model singleton and the encapsulated LiveData instance.

  • Use the the LiveRealmObject and LiveRealmResults classes as a template for encapsulating live Realm data in Android LiveData.
  • Use a ViewModel to separate underlying data from the UI elements that display that data.
  • DataBinding lets you declare relationships between model data and UI elements without explicitly setting values in an Activity or Fragment.

Did you find this quick start guide helpful? Please let us know with the feedback form on the right side of the page!

Give Feedback
© 2021 MongoDB, Inc.

About

  • Careers
  • Legal Notices
  • Privacy Notices
  • Security Information
  • Trust Center
© 2021 MongoDB, Inc.