Android Jetpack Architecture Components: Getting Started

In this tutorial, you will learn how to create a contacts app using Architecture Components from Android Jetpack like Room, LiveData and ViewModel. By Zahidur Rahman Faisal.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 5 of this article. Click here to view the first page.

Creating the Database

Now, you need to implement the third and most central component: the Database class. In order to do that, add a new file inside the db package and name it PeopleDatabase. Implement PeopleDatabase like the following:

package com.raywenderlich.android.imet.data.db

import android.app.Application
import android.arch.persistence.room.Database
import android.arch.persistence.room.Room
import android.arch.persistence.room.RoomDatabase
import com.raywenderlich.android.imet.data.model.People

// 1
@Database(entities = [People::class], version = 1)
abstract class PeopleDatabase : RoomDatabase() {

  abstract fun peopleDao(): PeopleDao

  // 2
  companion object {
    private val lock = Any()
    private const val DB_NAME = "People.db"
    private var INSTANCE: PeopleDatabase? = null

    // 3
    fun getInstance(application: Application): PeopleDatabase {
      synchronized(lock) {
        if (INSTANCE == null) {
          INSTANCE = 
              Room.databaseBuilder(application,
                  PeopleDatabase::class.java, DB_NAME)
              .allowMainThreadQueries()
              .build()
        }
        return INSTANCE!!
      }
    }


  }
}

Take a moment to understand each segment:

  1. Similar to before, with the @Database annotation, you’ve declared PeopleDatabase as your Database class, which extends the abstract class RoomDatabase. By using entities = [People::class] along with the annotation, you’ve defined the list of Entities for this database. The only entity is the People class for this app. version = 1 is the version number for your database.
  2. You’ve created a companion object in this class for static access, defining a lock to synchronize the database access from different threads, declaring a DB_NAME variable for the database name and an INSTANCE variable of its own type. This INSTANCE will be used as a Singleton object for your database throughout the app.
  3. The getInstance(application: Application) function returns the same INSTANCE of PeopleDatabase whenever it needs to be accessed in your app. It also ensures thread safety and prevents creating a new database every time you try to access it.

Now, it’s time to update PeopleRepository so that it can interact with PeopleDatabase. Replace everything in PeopleRepository with the following code:

package com.raywenderlich.android.imet.data

import android.app.Application
import com.raywenderlich.android.imet.data.db.PeopleDao
import com.raywenderlich.android.imet.data.db.PeopleDatabase
import com.raywenderlich.android.imet.data.model.People

class PeopleRepository(application: Application) {

  private val peopleDao: PeopleDao

  init {
    val peopleDatabase = PeopleDatabase.getInstance(application)
    peopleDao = peopleDatabase.peopleDao()
  }

  fun getAllPeople(): List<People> {
    return peopleDao.getAll()
  }

  fun insertPeople(people: People) {
    peopleDao.insert(people)
  }

  fun findPeople(id: Int): People {
    return peopleDao.find(id)
  }

}

This class is pretty much self-explainatory. It acts as the only access point to your data. It passes your request for People data through PeopleDao to PeopleDatabase and returns the data to the requested view (Activity or Fragment).

You have one more step: pre-populating the database with existing data from PeopleInfoProvider.

Pre-populating a Room Database

You’re almost there to complete the persistence challenge, so don’t quit yet – add the function below inside the companion object block in PeopleDatabase class:

fun prePopulate(database: PeopleDatabase, peopleList: List<People>) {
  for (people in peopleList) {
    AsyncTask.execute { database.peopleDao().insert(people) }
  }
}

This function adds People from a provided people list and inserts them into the PeopleDatabase asynchronously.

Note: You should always perform long-running or batch operations in Room database asynchronously.

Now, modify getInstance(application: Application) function:

fun getInstance(application: Application): PeopleDatabase {
  synchronized(PeopleDatabase.lock) {
    if (PeopleDatabase.INSTANCE == null) {
      PeopleDatabase.INSTANCE =
          Room.databaseBuilder(application, PeopleDatabase::class.java, PeopleDatabase.DB_NAME)
              .allowMainThreadQueries()
              .addCallback(object : RoomDatabase.Callback() {
                override fun onCreate(db: SupportSQLiteDatabase) {
                  super.onCreate(db)
                  PeopleDatabase.INSTANCE?.let {
                    PeopleDatabase.prePopulate(it, PeopleInfoProvider.peopleList)
                  }
                }
              })
              .build()
    }
    return PeopleDatabase.INSTANCE!!
  }
}

Notice that you’ve added a callback function before calling build() on the Room database builder. That callback function will notify once the database is created for the first time, so you apply the prePopulate() function on the PeopleDatabase instance with the existing peopleList from PeopleInfoProvider class.

Uninstall the app from your development device or emulator, the build and run again. Now, you’ll see an empty screen, but don’t worry — tap the ADD floating action button at the bottom-right corner to navigate to AddPeopleFragment, then press Back. You’ll see the good ol’ people list again!

Adding People

Can you guess what’s happening here? This happened because you don’t have the right data at the right moment! The database creation and insertion are asynchronous operations, so the database was not ready to provide the requested data when PeoplesListFragment loaded. This is where LiveData comes in…

Live Updates With LiveData

The fundamental property of LiveData is that its observable and a LiveData always alerts the observer (it can be a View, Activity or Fragment) when there’s something new to offer.

To update your app with this exciting Architecture Component, start with the PeopleDao class. Wrap the people list returned by the getAll() function with LiveData like this:

fun getAll(): LiveData<List<People>>
Note: You only need to wrap the return type from the database with LiveData and Room will do all the heavy lifting for you! Now your app can observe changes in the model with very little effort!

Next, update the getAllPeople() method in the PeopleRepository class:

fun getAllPeople(): LiveData<List<People>> {
  return peopleDao.getAll()
}

Now, modify the onResume() method in the PeoplesListFragment class to observe the people list like below:

override fun onResume() {
  super.onResume()

  // Observe people list
  val peopleRepository = (activity?.application as IMetApp).getPeopleRepository()
  peopleRepository.getAllPeople().observe(this, Observer { peopleList ->
    populatePeopleList(peopleList!!)
  })
}
Note: If you’re asked to resolve what class Observer is, be sure to pick android.arch.lifecycle.Observer.

Build and run. This time, you’ll see the people list immediately, because LiveData notifies the observer whenever the data is available.

Brilliant!!!

Now that you’re done with the Persistence challenge, time to face the next challenge: Effectively Handling Data, which includes releasing the observer when the view is no longer in use to ensure optimal data consumption and minimizing memory leaks.

Introducing ViewModel

ViewModels offer a number of benefits:

  • ViewModel‘s are lifecycle-aware, which means they know when the attached Activity or Fragment is destroyed and can immediately release data observers and other resources.
  • They survive configuration changes, so if your data is observed or fetched through a ViewModel, it’s still available after your Activity or Fragment is re-created. This means you can re-use the data without fetching it again.
  • ViewModel takes the responsibility of holding and managing data. It acts as a bridge between your Repository and the View. Freeing up your Activity or Fragment from managing data allows you to write more concise and unit-testable code.

These are enough to solve your effective data-management challenge! Now, implement your first ViewModel. Open the uilist package in your starter project and create a new Kotlin Class named PeoplesListViewModel. Add the code below inside PeoplesListViewModel:

package com.raywenderlich.android.imet.ui.list

import android.app.Application
import android.arch.lifecycle.AndroidViewModel
import android.arch.lifecycle.LiveData
import android.arch.lifecycle.MediatorLiveData
import com.raywenderlich.android.imet.IMetApp
import com.raywenderlich.android.imet.data.model.People

class PeoplesListViewModel(application: Application) : AndroidViewModel(application) {

  private val peopleRepository = getApplication<IMetApp>().getPeopleRepository()
  private val peopleList = MediatorLiveData<List<People>>()

  init {
    getAllPeople()
  }

  // 1
  fun getPeopleList(): LiveData<List<People>> {
    return peopleList
  }

  // 2
  fun getAllPeople() {
    peopleList.addSource(peopleRepository.getAllPeople()) { peoples ->
      peopleList.postValue(peoples)
    }
  }

}

This class fetches the people list from PeopleRepository when initialized. You may have already noticed that it has a peopleList variable of MediatorLiveData type. MediatorLiveData is a special kind of LiveData, which can hold data from multiple data sources.

PeoplesListViewModel has two important methods:

  1. getPeopleList() returns an observable LiveData version of the peopleList, making it accessible to the attached Activity or Fragment.
  2. getAllPeople() sets the data source of peopleList from PeopleRepository. It fetches the list of people by executing peopleRepository.getAllPeople() and posting the value to peopleList.

Add the following property to PeoplesListFragment to use this ViewModel:

private lateinit var viewModel: PeoplesListViewModel

Add the following code inside the onCreate() method to initialize the ViewModel:

viewModel = ViewModelProviders.of(this).get(PeoplesListViewModel::class.java)

Finally, you need to get the people list from the ViewModel and render it to the view. Add the following lines to the end of onViewCreated():

// Start observing people list
viewModel.getPeopleList().observe(this, Observer<List<People>> { peoples ->
  peoples?.let {
    populatePeopleList(peoples)
  }
})

Now, you don’t need to fetch data every time the Fragment resumes; ViewModel will take care of that. Delete the onResume() method from PeoplesListFragment completely.

Build and run. You’ll see that the people list is doing just fine. Go ahead and add new People; if you see the added person immediately on top of the list, everything is in sync!