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 3 of 5 of this article. Click here to view the first page.

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:

  1. Takes the name of searched People as function parameter.
  2. Performs the search using peopleRepository.findPeople(name) and sets the resulting LiveData as a source of peopleList.
  3. 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?

Search for people

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 uidetails 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.

People Details

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)
    
    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:

App Architecture

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:

Single-Activity Architecture