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?
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 the zip file containing the starter project for this tutorial using the Download materials button at the top or bottom of this tutorial.
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.
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!
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?!”
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.
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:
- Room Components: Add local persistence in your app. Room is an object-mapping library wrapping SQLite to make your database management concise and painless.
- 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.
- 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:
- Entity: A model class that represents a table in a Room database.
- Data Access Object (DAO): A helper class to access and query the database.
- 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. AddingautoGenerate = true
ensuresid
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:
- It saves you from writing direct queries, which are more error-prone and harder to debug.
- 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.
Name it db inside the New Package dialog and click OK.
Create a new Kotlin Interface inside the db package and name it PeopleDao.
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:
-
getAll() returns a list of People entities. The
@Query
annotation on top performs a selection on your database and returns allPeople
as a list. In this query,SELECT * FROM People
, actually does the job andORDER BY id DESC
prepares the list with a descending order of people’sid
. -
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 addonConflict = OnConflictStrategy.REPLACE
for cases wherein your new People entity has the sameid
of an existing one in your database. In that case, it’ll replace (or update) the existing entity. - 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).
-
find(id: Int) is to find a People entity with specific
id
. It executes a SELECT query with a condition where the People‘sid
is matched to the suppliedid
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:
- 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!
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:
- 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:
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:
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 New ▸ Android 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.
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:
- You are declaring the
navigationController
instance for MainActivity. - The NavController is actually a part of a NavigationHostFragment attached to this Activity. This section initializes the
navigationController
instance usingfindNavController()
. Then, the NavigationUI helper class ties thenavigationController
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. - 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!
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:
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.
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:
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:
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:
Build and run. It’ll show iMet as 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!
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:
- More ROOM in Raywenderlich
- Detailed Lifecycle and LiveData
- Deep-diving with Navigation Components
- Official Guide to App Architecure
I hope you enjoyed flying high with Android Jetpack. If you have any questions or comments, please join the forum discussion and comment below!