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.

Version

  • Kotlin 1.2, Android 4.2, Android Studio 3

Developing Android apps is just like playing video games — it’s challenging and rewarding at the same time! While enjoying your journey through the Android world, you must have faced many obstacles and wished for some kind of Jetpack to jump through those obstacles, or that you had a nitro boost to kickstart your development engine for the sprint…

Google heard you – now Android literally offers you a Jetpack!

Android Jetpack is a set of libraries that helps you to deal with the challenges you eventually face as a Android Developer — writing boilerplate code, managing activity lifecycles, surviving configuration changes or preventing memory leaks.

In this tutorial, you’ll create a contact list app called iMet using Android Jetpack, which stores contact information about the people you have met.

In the process, you’ll learn:

  • To create a Room database to store and retrieve data.
  • To use ViewModel to isolate use cases from the View.
  • To view data and observe changes using LiveData.
  • To simplify navigation and data passing within the app using Navigation Components.
  • And More!

Getting Started

Get started by downloading and installing the preview release of Android Studio from here.

Download Android Studio Canary

Note: You need to have Android Studio Canary Build 3.3 to use Navigation Components in your project. The Navigation Components are an experimental feature in Android Studio 3.2. You can use Android Studio Stable for your ongoing projects and Android Studio Preview to explore new features at the same time. If you already have Android Studio Canary Build 3.3, then you can continue reading!

Download the zip file containing the starter project for this tutorial using the Download materials button at the top or bottom of this tutorial.

Launch Android Studio 3.3 Preview

Now, on macOS launch Android Studio 3.3 Preview from your Applications folder as shown above and select Open an existing Android Studio project to import the starter project that you just downloaded. On Linux and Windows, follow the steps for your OS to start Android Studio 3.3 Preview and open the starter project.

Note: You may be asked to update your gradle plugin after opening the app. Go ahead and update!

Build and run using keyboard shortcut Shift + F10 (or Control + R if you’re on macOS). If you see a list of awesome people like following, you’re all set to dive in!

Starter project

Start exploring the PeopleRepository class inside the data package in the starter project. PeopleRepository is the gatekeeper of the data layer in your app. It holds all data regarding People and provides information to different segments in the app — for example: PeoplesListFragment.

You must be thinking, “What’s the source of data for PeopleRepository?!”

What is the source?

Well, assume PeopleInfoProvider, inside the net package within data, is your helper class that fetches People information from some web-service and stores it in PeopleRepository.

But how about adding the People that you’ve just met today into the PeopleRepository so that you can contact them later?

To achieve that, you need to update PeopleRepository to store data from all your sources (local or remote), making it a single source of truth for managing the People information.

Note: To learn more about Repository Pattern, you may follow the official App Architecture guide.

So, here’s your first challenge – adding persistence!

Adding Dependencies for Architecture Components

Jetpack depends on a few external libraries, so you need to add those dependencies first. Open the build.gradle file from your app module and append the following lines just above the closing brackets of the dependencies section:

// 1: Room Components
def roomVersion = "1.1.1"
implementation "android.arch.persistence.room:runtime:$roomVersion"
kapt "android.arch.persistence.room:compiler:$roomVersion"

// 2: Lifecycle Components
def lifecycleVersion = "1.1.1"
implementation "android.arch.lifecycle:extensions:$lifecycleVersion"

// 3: Navigation Components
def navigationVersion = "1.0.0-alpha04"
implementation "android.arch.navigation:navigation-fragment-ktx:$navigationVersion"
implementation "android.arch.navigation:navigation-ui-ktx:$navigationVersion"

These dependencies provide components with the following capabilities:

  1. Room Components: Add local persistence in your app. Room is an object-mapping library wrapping SQLite to make your database management concise and painless.
  2. Lifecycle Components: Lift the responsibility of managing your app’s lifecycle with ease. They add lifecycle-aware components like ViewModel and LiveData that allows you to forget about writing code for handling configuration changes or loading data into your UI when there’s an update.
  3. Navigation Components: Add helper classes like NavController to simplify navigation and passing of data throughout your app.

Creating ROOM for Your Contacts

To pass your first challenge, persistence, you need to implement three major components from Room:

  1. Entity: A model class that represents a table in a Room database.
  2. Data Access Object (DAO): A helper class to access and query the database.
  3. Database: An abstract class that directly extends RoomDatabase. It’s main responsibility is creating the database and exposing entities through Data Access Objects (DAO).

Creating Entities

Start with the simplest component first. You already have a People class inside the model which is nested in the data package; now, modify it to match the following to declare it as an Entity for your Room database:

package com.raywenderlich.android.imet.data.model

import android.arch.persistence.room.Entity
import android.arch.persistence.room.PrimaryKey

@Entity
data class People(
    var name: String = "",
    var metAt: String = "",
    var contact: String = "",
    var email: String = "",
    var facebook: String = "",
    var twitter: String = "",
    @PrimaryKey(autoGenerate = true) var id: Int = 0
)

Here, you’re using two annotations from Room:

  • @Entity: Declares that you’re going to use this model as an Entity.
  • @PrimaryKey: Defines id as the Primary Key for the Entity. Adding autoGenerate = true ensures id will be automatically generated whenever you create a new record in the database with this Entity.

Piece of cake! Moving on to the next one…

Creating a Data Access Object (DAO)

A DAO is basically an interface to access required data from your database. It has two sole purposes:

  1. It saves you from writing direct queries, which are more error-prone and harder to debug.
  2. It isolates query logic from database creation and migration code for better manageability.

Create a new package for the DAO and Database classes. Right-click on the data package, then select New ▸ Package.

New package

Name it db inside the New Package dialog and click OK.

New Package dialog

Create a new Kotlin Interface inside the db package and name it PeopleDao.

New Interface

As you’re going to use this class for database queries, implement the most common queries — Select All, Insert, Delete and Select by ID. Replace everything inside the PeopleDao file with following:

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

import android.arch.persistence.room.Dao
import android.arch.persistence.room.Insert
import android.arch.persistence.room.OnConflictStrategy
import android.arch.persistence.room.Query
import com.raywenderlich.android.imet.data.model.People

@Dao
interface PeopleDao {

  // 1: Select All
  @Query("SELECT * FROM People ORDER BY id DESC")
  fun getAll(): List<People>

  // 2: Insert
  @Insert(onConflict = OnConflictStrategy.REPLACE)
  fun insert(people: People)

  // 3: Delete
  @Query("DELETE FROM People")
  fun deleteAll()

  // 4: Select by id
  @Query("SELECT * FROM People WHERE id = :id")
  fun find(id: Int): People

}

That’s pretty straight forward! Similar to the @Entity annotation, here, you use @Dao on top of the interface to declare it as a DAO for your Room database.

Querying is fairly simple in Room. Reviewing the methods in PeopleDao one by one:

  1. getAll() returns a list of People entities. The @Query annotation on top performs a selection on your database and returns all People as a list. In this query, SELECT * FROM People, actually does the job and ORDER BY id DESC prepares the list with a descending order of people’s id.
  2. insert(people: People) inserts new People entities in your database. While using the @Insert annotation is enough to perform the insertion for you, you can additionally add onConflict = OnConflictStrategy.REPLACE for cases wherein your new People entity has the same id of an existing one in your database. In that case, it’ll replace (or update) the existing entity.
  3. deleteAll() does exactly as its name implies — you perform a DELETE operation on the People entity in your database (remember, an Entity is like a Table in Room).
  4. find(id: Int) is to find a People entity with specific id. It executes a SELECT query with a condition where the People‘s id is matched to the supplied id parameter in this function.

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!

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:

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

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

Preparing for Navigation

You’ve already added the necessary dependencies for using Navigation Components, so you’ll now focus on the implementation.

Right-click on the res directory and select NewAndroid Resource File. In the New Resource File dialog, input navigation_graph as File name and select Navigation in the Resource type drop-down list. Then click OK.

New navigation graph

Now, you need to modify the activity_main.xml layout to define it as the single entry point in your navigation graph. To do that, update activity_main.xml to the following:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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=".ui.MainActivity">

  <fragment
    android:id="@+id/navigationHostFragment"
    android:name="androidx.navigation.fragment.NavHostFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:defaultNavHost="true"
    app:navGraph="@navigation/navigation_graph" />

</android.support.constraint.ConstraintLayout>

Here, you are attaching a NavHostFragment inside MainActivity as the default navigation host (or default entry point) for all other Fragments. The NavHostFragment is a part of the Navigation Architecture Components library. app:navGraph="@navigation/navigation_graph" is referencing the navigation_graph.xml file you created in order to know about possible destinations from NavHostFragment.

You’ll need to update MainActivity so that it handles Back or Up navigation, utilizing Navigation Architecture Components, and so you won’t need to bother about managing the Fragment Back-Stack later. Replace everything inside MainActivity to be as follows:

package com.raywenderlich.android.imet.ui

import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import androidx.navigation.NavController
import androidx.navigation.findNavController
import androidx.navigation.ui.NavigationUI
import com.raywenderlich.android.imet.R

class MainActivity : AppCompatActivity() {

  //1
  private lateinit var navigationController: NavController

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    //2
    navigationController = findNavController(R.id.navigationHostFragment)
    NavigationUI.setupActionBarWithNavController(this, navigationController)
  }

  //3
  override fun onSupportNavigateUp() = navigationController.navigateUp()
}

Take some time to understand each segment of the above snippet as numbered:

  1. You are declaring the navigationController instance for MainActivity.
  2. The NavController is actually a part of a NavigationHostFragment attached to this Activity. This section initializes the navigationController instance using findNavController(). Then, the NavigationUI helper class ties the navigationController with the ActionBar in this Activity. This is necessary to allow the ActionBar to show a Back button whenever a child fragment is attached to this Activity.
  3. This override lets the navigationController handle the Fragment Back-Stack internally when a user performs a Back or Up action.

Now, open navigation_graph.xml to set PeoplesListFragment as your initial destination. Click on the New Destination button on top. Then, select the fragment_people_list layout from the dropdown — easy!

Add to nav graph

So, now, PeoplesListFragment will be loaded in the NavigationHostFragment by default when you launch the app.

Navigating to the Next Fragment

Next we’ll add the ability to navigate to the AddPeopleFragment. Click on the New Destination button on top and select the fragment_add_people layout from the dropdown menu. This will add another destination, AddPeopleFragment, in the Navigation Graph. Now, select PeoplesListFragment again — you’ll see a small circle appear. Drag the circle to the AddPeopleFragment. A connector arrow will appear in between those destinations:

Adding an action

Now, open the PeoplesListFragment class and replace the code inside addFab.setOnClickListener with the following code:

view.findNavController().navigate(
    R.id.action_peoplesListFragment_to_addPeopleFragment)

Here, you’re using the NavController from the attached view to perform the navigation instead of using a new intent.

Note: R.id.action_peoplesListFragment_to_addPeopleFragment is a automatically generated unique identifier by Android Studio as a result of connecting PeoplesListFragment and AddPeopleFragment in the Navigation Graph.

Now, open the AddPeopleFragment class and replace this line inside savePeopleInfo(),

activity?.finish()

with this:

Navigation.findNavController(view!!).navigateUp()

The above line navigates users back using NavController instead of finishing the Activity when new People are added.

Build and run. Navigate to AddPeopleFragment and try adding new people to check that the Navigation Components are working properly.

If everything works as expected, AddPeopleActivity is not necessary anymore. You can delete the AddPeopleActivity class and activity_add_people.xml from the project. Remember to remove the following lines from AndroidManifest.xml:

<activity
  android:name=".ui.add.AddPeopleActivity"
  android:label="@string/add_people"
  android:parentActivityName=".ui.MainActivity" />

Also, remove the reference to AddPeopleActivity inside PeopleListFragment, if it’s still there.

You’re just one step away from completing the Navigation challenge! Next, you’ll eliminate PeopleDetailsActivity to complete it…

Navigation With Additional Data

Open navigation_graph.xml again and add the PeopleDetailsFragment by clicking the New Destination button. Then, connect PeoplesListFragment to PeopleDetailsFragment just like the previous step. Your navigation_graph.xml will now look like this:

Full navigation graph

Now, open PeoplesListFragment and replace the code inside onItemClick() with the following:

val peopleBundle = Bundle().apply {
  putInt(getString(R.string.people_id), people.id)
}
view?.findNavController()
    ?.navigate(R.id.action_peoplesListFragment_to_peopleDetailsFragment, peopleBundle)

Again, this function uses the NavController from the attached view to perform the navigation instead of creating a new intent, but, this time, with an additional parameter, peopleBundle, which carries the id of the selected People object to the destination PeopleDetailsFragment.

Next, open the PeopleDetailsFragment class and replace this line inside onViewCreated(),

val peopleId = activity?.intent?.getIntExtra(getString(R.string.people_id), 0)

with this one:

val peopleId = arguments?.getInt(getString(R.string.people_id))

Here, the arguments variable of this Fragment returns the bundle passed with NavController during navigation.

Now, build and run again. Select an item from the people list to navigate to the PeopleDetailsFragment. Check if the Navigation Components are carrying your data properly and displaying the correct item in PeopleDetailsFragment.

You can now remove the PeopleDetailsActivity class along with the activity_peoples_details.xml layout! Also, remove the following lines from AndroidManifest.xml:

<activity
  android:name=".ui.details.PeopleDetailsActivity"
  android:label="@string/people_details"
  android:parentActivityName=".ui.MainActivity" />

Remember to remove the import of PeopleDetailsActivity at the top of your PeopleListFragment, if it’s still there.

Finally, all seems complete. Build and run. You should see that everything works fine with your Single-Activity Architecture:

App with Single-Activity Architecture

But wait — did you notice the title on top of the screen? Who changed that?

Well, the title is auto-generated for all your Fragments when you imported them into navigation_graph.xml. You can change the title easily from your IDE.

Open navigation_graph.xml again and select PeoplesListFragment. Change the Label from the Attributes panel to iMet like below:

Changing a Label

Build and run. It’ll show iMet as title:

iMet title

Now, select AddPeopleFragment in the navigation graph and change the title to Add People. Similarly, change the title of PeopleDetailsFragment to People Details.

Build and run once again. You should see the correct title on each screen.

You completed the final challenge: Implementing Proper Navigation!

All Complete!

Congratulations!

In this quest, you’ve learned how to:

  • Add a local database with Room.
  • Use advanced features of LiveData.
  • Deal with data using ViewModel.
  • Use Navigation Architecture Components for easy navigation.
  • Implement a Single-Activity Architecture.

Where to Go From Here?

You can download the final project using the Download materials button at the top or bottom of this tutorial.

Keep exploring the beautiful world of Android with Jetpack. Here are some additional resources to explore:

I hope you enjoyed flying high with Android Jetpack. If you have any questions or comments, please join the forum discussion and comment below!

Contributors

Comments