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?
Mastering ViewModel and LiveData
Implementing Search
Now, sharpen your skills with ViewModel and LiveData even more! Next, you’ll implement the ability to search for people by name. Start with updating PeopleDao. Add following function to query People by name from the database:
@Query("SELECT * FROM People WHERE name LIKE '%' || :name || '%'")
fun findBy(name: String): LiveData<List<People>>
This function takes a name
string as a parameter. The query selects People with matching name from the database and returns a list of People. This query lists People even if the name
is only partially matched!
You need to update PeopleRepository as well. Add the following function at the end of the PeopleRepository class:
fun findPeople(name: String): LiveData<List<People>> {
return peopleDao.findBy(name)
}
This one is fairly simple. It just executes the findBy(name)
method using the peopleDao
instance and returns the list of People with a matched name to those who require the data (preferably, your ViewModel).
You’re going to use the search feature in PeoplesListFragment. PeoplesListViewModel has taken the responsibility of handling data for PeoplesListFragment, so you’ll update it first. Add the following function inside PeoplesListViewModel:
// 1
fun searchPeople(name: String) {
// 2
peopleList.addSource(peopleRepository.findPeople(name)) { peoples ->
// 3
peopleList.postValue(peoples)
}
}
This function performs three things:
- Takes the
name
of searched People as function parameter. - Performs the search using
peopleRepository.findPeople(name)
and sets the resulting LiveData as a source ofpeopleList
. - Posts the value of the resulting LiveData to the observer of
peopleList
. As a result, your people list will show with the name of searched people (if found) instead of showing all People.
Now, allow PeoplesListFragment to interact with the search. Add the following line to onQueryTextSubmit()
before the return:
viewModel.searchPeople(query!!)
The above method simply delegates the search operation to the attached ViewModel, passing the search query (in this case, people’s names). The rest is handled by your Observer for the people list and the ViewModel.
You may want to show the initial list of people again when the search is closed. Add the following code to onClose()
:
viewModel.getAllPeople()
searchView.onActionViewCollapsed()
onClose()
is fired when you close the search bar. The code above informs your ViewModel to fetch the all-people list again and notifies searchView
that the search is done, which then collapses it.
Build and run. Try the search feature – isn’t it awesome?
Mapping With LiveData Transformations
There’s still more to explore with ViewModel and LiveData. This time, you’ll improve PeopleDetailsFragment using Architecture Components. Select the ui ▸ details package in your starter project and create a new Kotlin Class named PeopleDetailsViewModel. Then code PeopleDetailsViewModel as following:
package com.raywenderlich.android.imet.ui.details
import android.app.Application
import android.arch.lifecycle.AndroidViewModel
import android.arch.lifecycle.LiveData
import android.arch.lifecycle.MutableLiveData
import android.arch.lifecycle.Transformations
import com.raywenderlich.android.imet.IMetApp
import com.raywenderlich.android.imet.data.model.People
class PeopleDetailsViewModel(application: Application) : AndroidViewModel(application) {
private val peopleRepository = getApplication<IMetApp>().getPeopleRepository()
private val peopleId = MutableLiveData<Int>()
// Maps people id to people details
fun getPeopleDetails(id: Int): LiveData<People> {
peopleId.value = id
val peopleDetails =
Transformations.switchMap<Int, People>(peopleId) { id ->
peopleRepository.findPeople(id)
}
return peopleDetails
}
}
Here, you used MutableLiveData for peopleId
because this data will change for different people. The interesting part is this:
val peopleDetails = Transformations.switchMap<Int, People>(peopleId) { id ->
peopleRepository.findPeople(id)
}
This triggers a peopleRepository.findPeople(id)
method whenever peopleId.value
is set. So, basically, it’s acting like a converter — it takes input from people id
as an argument and returns a People object by searching into PeopleRepository
. It returns the LiveData of People
with that specific id
to the observer through peopleDetails
.
You also need to change the findPeople(id: Int)
method in PeopleRepository so that it returns LiveData:
fun findPeople(id: Int): LiveData<People> {
return peopleDao.find(id)
}
Again, you need to update the return type of the find(id: Int)
function in PeopleDao to avoid the compilation error. Open PeopleDao and update the find()
method to return LiveData:
@Query("SELECT * FROM People WHERE id = :id")
fun find(id: Int): LiveData<People>
Now, you’re ready to use the PeopleDetailsViewModel in PeopleDetailsFragment. Add the following code above onCreateView()
inside PeopleDetailsFragment:
private lateinit var viewModel: PeopleDetailsViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel =
ViewModelProviders.of(this).get(PeopleDetailsViewModel::class.java)
}
Here, you are declaring the instance of PeopleDetailsViewModel and initializing it once PeopleDetailsFragment is created.
Now, use the ViewModel to map people’s id
to corresponding details information. Update onViewCreated()
to the following:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Get people details with provided id
val peopleId = activity?.intent?.getIntExtra(getString(R.string.people_id), 0)
peopleId?.let {
viewModel.getPeopleDetails(peopleId).observe(this, Observer { peopleDetails ->
populatePeopleDetails(peopleDetails)
})
}
}
Now, build and run to see the output yourself.
ViewModels Everywhere
As a challenge inside a challenge, try refactoring AddPeopleFragment to use ViewModel. Below is a list of key steps you’ll need to take to accomplish this task:
With:
- Create a new class named AddPeopleViewModel with a property of type PeopleRepository and a method named
addPeople(people: People)
. - Add a property of type AddPeopleViewModel to AddPeopleFragment and initialize it in
onCreate()
- Replace the following code in AddPeopleFragment
(activity?.application as IMetApp).getPeopleRepository().insertPeople(people)
With:
viewModel.addPeople(people)
(activity?.application as IMetApp).getPeopleRepository().insertPeople(people)
viewModel.addPeople(people)
If you run into problems, check out the final project to see how its done.
Architecture Layers
If you review your app’s current architecture at this point, you’ll see that using the ViewModels added an additional layer in your app’s structure:
From the source code, you can actually see that, in the current structure, Activities do nothing more than host the Fragments. According to Google‘s Principles of Navigation, it’s convenient to use a Single-Activity Architecture for providing a constant and seamless navigation experience throughout your app.
So here’s your final challenge: Implement Proper Navigation and move towards a Single-Activity Architecture. You’ll do this using Navigation Components
Exploring Navigation Components
Navigation Architecture Components are meant to simplify navigation within different parts of your app. They also help you visualize the navigation flow by generating a navigation graph. The navigation graph actually consists of a set of navigation points, which can be an Activity, Fragment or even a Custom Navigation Type. Navigation points are usually called destinations.
Your goal is to update the app structure to match the following diagram, eliminating the unnecessary Activity Layer in your current architecture: