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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Android Jetpack Architecture Components: Getting Started
35 mins
- Getting Started
- Adding Dependencies for Architecture Components
- Creating ROOM for Your Contacts
- Creating Entities
- Creating a Data Access Object (DAO)
- Creating the Database
- Pre-populating a Room Database
- Live Updates With LiveData
- Introducing ViewModel
- Mastering ViewModel and LiveData
- Implementing Search
- Mapping With LiveData Transformations
- ViewModels Everywhere
- Architecture Layers
- Exploring Navigation Components
- Preparing for Navigation
- Navigating to the Next Fragment
- Navigation With Additional Data
- Where to Go From Here?
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:
- Similar to before, with the
@Database
annotation, you’ve declared PeopleDatabase as your Database class, which extends the abstract class RoomDatabase. By usingentities = [People::class]
along with the annotation, you’ve defined the list of Entities for this database. The only entity is thePeople
class for this app.version = 1
is the version number for your database. - You’ve created a
companion object
in this class for static access, defining alock
to synchronize the database access from different threads, declaring aDB_NAME
variable for the database name and anINSTANCE
variable of its own type. ThisINSTANCE
will be used as a Singleton object for your database throughout the app. - The
getInstance(application: Application)
function returns the sameINSTANCE
ofPeopleDatabase
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.
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!
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>>
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!!)
})
}
Build and run. This time, you’ll see the people list immediately, because LiveData notifies the observer whenever the data is available.
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 ui ▸ list 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:
-
getPeopleList()
returns an observable LiveData version of thepeopleList
, making it accessible to the attached Activity or Fragment. -
getAllPeople()
sets the data source ofpeopleList
from PeopleRepository. It fetches the list of people by executingpeopleRepository.getAllPeople()
and posting the value topeopleList
.
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!