Docs Menu
Docs Home
/ /
Atlas Device SDKs

Quick Start with LiveData - Java SDK

On this page

  • Prerequisites
  • Clone the LiveData Quick Start Repository
  • Import Dependencies
  • LiveRealmObject
  • Instantiating LiveData in the ViewModel
  • Connecting the ViewModel to the UI
  • Run the Application
  • Summary
  • Feedback

This page contains instructions to quickly get Realm 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 Sync to synchronize data changes between clients. Before you begin, ensure you have:

Note

Using LiveData without Sync

To use this quick start without 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 Java 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 Java SDK without 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

The 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 Java SDK and Android LiveData. Begin by adding the Java 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, enable Sync in the SDK 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)
    }
    }

Tip

See also: Using LiveData with RealmResults

This example only uses LiveData to display RealmObjects in the UI. For a sample implementation displaying RealmResults, see 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 App with your App ID.

  2. Authenticate a user.

  3. Connect to a specific realm using 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

Don't Read or Write on the UI Thread

Database reads and writes are computationally expensive, so the SDK disables reads and writes by default on the UI thread. For simplicity, this example enables UI thread reads and writes with the allowWritesOnUiThread() and allowQueriesOnUiThread() config builder methods. In production applications, you should almost always defer reads and writes to a background thread using asynchronous methods.

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 Sync, you can view your 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!

Next

Welcome to the Atlas Device SDK Docs