Chapters

Hide chapters

Saving Data on Android

Second Edition · Android 11 · Kotlin 1.5 · Android Studio 4.2

Using Firebase

Section 3: 11 chapters
Show chapters Hide chapters

10. Using Room with Android Architecture Components
Written by Subhrajyoti Sen

In the previous chapters, you learned how to create the most important components of a Room Database: your Data Access Objects (DAOs) and your Entities.

While having your DAOs and entities is usually enough to interact with your database, you still need a way to display all the information to the user, all the while handling the lifecycle of the app and configuration changes. This is where Android Architecture Components such as ViewModel and LiveData come to the rescue!

In this chapter, you’ll learn:

  • What LiveData and ViewModel are, and how to use them.
  • How to make your DAOs return LiveData instead of simple data objects.
  • How to create ViewModels that are lifecycle-aware and observe them in your activities.
  • How to create a Repository that acts as a bridge between your ViewModels and your DAOs.
  • How to prepopulate your database using a provider class.

Note: This chapter assumes you have basic knowledge of Kotlin and Android. If you’re new to Android, check out our Android tutorials here: https://www.raywenderlich.com/category/android. If you know Android but are unfamiliar with Kotlin, take a look at, “Kotlin For Android: An Introduction,” here: https://www.raywenderlich.com/174395/kotlin-for-android-an-introduction-2.

Getting started

Start by opening the starter project using Android Studio 4.2, or greater, by going to File ▸ Open and selecting the starter project directory.

If you have been following along until this point, you should already be familiar with the code since it is the same as the final project from the last chapter. But, if you are just getting started, here is a quick recap:

  • The data package contains two packages: db and model. db contains the QuizDatabase class and your DAOs. model contains your entities: Question and Answer.
  • The view package contains all the activities for your app: SplashActivity, MainActivity, QuestionActivity and ResultActivity.

Build and run the app on a device or emulator.

The Main Screen.
The Main Screen.

Looks like everything is working as expected. You’re ready to start working on connecting Room to your app. But, first, you need to learn about LiveData.

Using LiveData with a Repository

To use LiveData, you first need to learn what it is. To put it simply, LiveData is an observable piece of data, which is aware of the Android lifecycle. You could, for simplicity’s sake, think of an Observable from Reactive Extensions, but which also listens to the Android lifecycle. As such, you can listen to its updates, by adding Observers.

Furthermore, by knowing the lifecycle state at all times, it has smart internal mechanisms, which stop potential observers from being updated, unless the lifecycle is active — your app and the screen with LiveData objects are visible. Because of this, you can easily avoid updating the UI, when the app is not active — e.g. when it’s in the background.

According to the documentation, there are six main advantages of using LiveData vs other similar libraries:

  • Ensures your UI matches your data state: Since LiveData implements the observer pattern, you can be sure that your UI widgets such as Button s, ListView s, RecyclerView s or TextView s will always be updated with the latest information.
  • No memory leaks: Since LiveData observers are bound to the lifecycle of other components, they will be destroyed as soon as the associated component is destroyed.
  • No crashes due to stopped activities: When an Activity becomes paused or stopped it won’t receive any new LiveData notifications.
  • No more manual lifecycle handling: Google made sure that LiveData is lifecycle-aware by default, so you don’t have to manually control them.
  • Proper configuration changes: Since LiveData is lifecycle-aware, and it caches the latest piece of data emitted, your Android components will receive the last emitted state, after configuration changes.
  • Sharing resources: LiveData can be extended to wrap system services, so they can be shared in your app.

It sounds awesome, right? Well, you’re going to use it to wrap the results of your DAO queries, and as such, every Activity and Fragment you use will automatically be notified of any changes. But, to do this, you need to add the LiveData dependency to your project, and change the DAO definitions, to return LiveData s.

Adding LiveData to the project

Add the following dependencies to the app build.gradle file:

// architecture components
implementation "androidx.arch.core:core-common:2.1.0"
implementation "androidx.lifecycle:lifecycle-common:2.3.1"
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1"

By adding the core and lifecycle dependencies, you add some of Android Jetpack’s fundamental components, which are related to the Android lifecycle. This also includes the LiveData component, so you can use it, without changing anything.

Now, open QuizDao.kt inside the data > db package. Take a look at getAllQuestions() and getQuestionAndAllAnswers():

@Query("SELECT * FROM question ORDER BY question_id")
fun getAllQuestions(): List<Question>

@Transaction
@Query("SELECT * FROM question")
fun getQuestionAndAllAnswers(): List<QuestionAndAllAnswers>

Right now, these methods are just returning a simple List but how do you make them return a LiveData object instead? Well, it’s very easy! You just need to change your method signatures to this:

@Query("SELECT * FROM question ORDER BY question_id")
fun getAllQuestions(): LiveData<List<Question>>

@Transaction
@Query("SELECT * FROM question")
fun getQuestionAndAllAnswers(): LiveData<List<QuestionAndAllAnswers>>

As previously mentioned, LiveData is an observable wrapper around a piece of data. Because of this, you can hold anything within it, like a list, array or a list of nested lists, you get the point!

So, to use LiveData, you need to wrap the object that you want to observe for changes, which in this case are the results of getAllQuestions() and getQuestionsAndAllAnswers(). Now, every time the data in Room changes, for example by adding a new Question object, your LiveData and its observers will be notified, with the new information. But, if you keep calling to the DAO, you’ll always get a new LiveData, and this defeats the purpose of Room being observable.

Additionally, you should never talk to the DAOs directly, because it’s easier to modify the code for internal database communication if you wrap it around another layer of abstraction. For this reason. you’ll create a Repository that acts as a bridge between your DAOs and the ViewModels, which you’ll define later.

Creating a quiz repository

Create a new Kotlin interface under the data package and name it QuizRepository. Add the following code, also importing the missing classes:

interface QuizRepository {

  fun getSavedQuestions(): LiveData<List<Question>>

  suspend fun saveQuestion(question: Question)

  suspend fun saveAnswer(answer: Answer)

  fun getQuestionAndAllAnswers(): LiveData<List<QuestionAndAllAnswers>>

  suspend fun deleteQuestions()
}

The above interface defines the methods that you’ll need in your repository. By using an interface you make it much easier to change the implementation of your code if you later decide that you need another database implementation rather than Room.

Now, create a new Kotlin class under the data package and name it Repository. Make your class implement the QuizRepository interface:

class Repository: QuizRepository {
}

Override all the functions in the interface to implement them. You can press Ctrl + I and Android Studio should display a list of all the missing members. Select all of them and press OK.

Next, add the following properties at the top of your class:

private val quizDao: QuizDao by lazy { 
  QuizApplication.database.quizDao() 
}
private val allQuestions by lazy { 
  quizDao.getAllQuestions()
}
private val allQuestionsAndAllAnswers by lazy { 
  quizDao.getQuestionAndAllAnswers()
}

You create two LiveData values, to observe and react to the data within the database, and all the changes. allQuestions will hold a reference to all the questions currently stored in your database.

Once again, since this is LiveData, your observers will be notified each time a new record is added or updated in the questions table. allQuestionsAndAllAnswers holds a LiveData list of QuestionAndAllAnswers. This property will be useful when displaying a question and its answers to your users.

Now, implement saveQuestion() and saveAnswer() as follows:

override suspend fun saveQuestion(question: Question) {
  quizDao.insert(question)
}

override suspend fun saveAnswer(answer: Answer) {
  quizDao.insert(answer)
}

The above code uses insert() of your quizDao to create new Question and Answer records in your database. You are using a suspend function because you don’t want to block the main thread by executing long-running operations on it.

Note: If you are new to suspend functions, check out the Kotlin Coroutines Tutorial for Android: Getting Started at https://www.raywenderlich.com/1423941-kotlin-coroutines-tutorial-for-android-getting-started.

Next, add this line to deleteQuestions() :

quizDao.clearQuestions()

Just like saveAnswer() and saveQuestion(), this method uses a suspend function to execute one of your DAO’s methods: clearQuestions(). As the name implies, deleteQuestions() will delete all the questions in your database.

Finally, implement the remainder of the code like this:

override fun getSavedQuestions() = allQuestions

override fun getQuestionAndAllAnswers() = allQuestionsAndAllAnswers

Once again, because you’re using LiveData, if you ever need to access fresh information from the database, you simply have to observe the preloaded LiveDatas in your repository. And that is all you need to create a repository that interacts with your database using your Data Access Objects.

At this point, you might be wondering: Why do I need to define a repository that interacts with my DAOs? Can’t I simply use my DAOs inside my ViewModels?

Well, you certainly can use your DAOs directly inside your ViewModels, but you also need to remember a very important principle in programming: The Single Responsibility Principle. The Single Responsibility Principle states that each class in your code should have a single responsibility or a single reason to change and that it should do it well. Just like in a company, you wouldn’t want developers doing accounting stuff and you wouldn’t want your accountants touching your code.

Also, by having each class focusing on a single task you’ll make your code much easier to maintain and test.

All right, that’s enough about LiveData. Next, you’ll learn about ViewModel!

Creating ViewModels

The ViewModel is a part of the Android Architecture Components and it’s designed to solve two common issues that developers often face when developing Android apps:

  • When an activity or fragment is destroyed, data is lost: Most developers can restore simple data by saving it using onSaveInstanceState() and retrieving it from the Bundle in onCreate(). However, this is not always a good approach since Bundles are only suitable for small amounts of data that can be serialized and de-serialized. You shouldn’t store all the data you’re displaying and working with.
  • Separation of Concerns: Activities and fragments usually have to execute different operations such as database transactions, HTTP requests, UI updates and more. It’s better to separate Views from the business logic by delegating this task to a more appropriate class such as ViewModels.

But how does the ViewModel help solve these issues? Well, ViewModel is specifically designed to hold and manage data related to your user interface. Just like LiveData, ViewModel is lifecycle aware, which means that it can react to the state of your Android components. Furthermore, using factories, and dependency management, it can survive configuration changes such as screen rotations. Therefore you won’t need to manually restore data using methods such as onSaveInstanceState(), you can simply retrieve everything you stored within the view model.

ViewModel sounds like the perfect choice to manage the questions and answers for your DroidQuiz app, right?

Create a new package named viewmodel under drodquiz. Create a new Kotlin class inside this package and name it MainViewModel.

Make MainViewModel extend the ViewModel abstract class by modifying your code like this:

class MainViewModel() : ViewModel() {
    
}

Now, since you are going to need your repository to interact with your database from your ViewModel classes, add the following parameter to the primary constructor of your class:

class MainViewModel(private val repository: QuizRepository) : ViewModel() {
    
}

Finally, add the following code:

fun prepopulateQuestions() {
  viewModelScope.launch(Dispatchers.IO) {
    for (question in QuestionInfoProvider.questionList) {
      repository.saveQuestion(question)
    }
    for (answer in QuestionInfoProvider.answerList) {
      repository.saveAnswer(answer)
    }
  }
}

fun clearQuestions() {
  viewModelScope.launch(Dispatchers.IO) {
    repository.deleteQuestions()
  }
}

In the code above, prepopulateQuestions() creates new question and answer records in your database by using the sample data included in your QuestionInfoProvider, while clearQuestions() deletes all the questions in your database using deleteQuestions() on your QuizRepository. viewModelScope is a CoroutineScope that is tied to the lifecycle of the ViewModel , meaning that whenever ViewModel is destroyed, all work launched from viewModelScope will be automatically canceled. Dispatchers.IO specifies that the work should be performed on a background thread.

Note: Here you are using a method in your ViewModel to prepopulate your database since it is very useful to learn how to perform delete and insert operations at any time. However, if you want to prepopulate your database with some default data, most of the time you’ll probably use addCallback() on your database builder, which gives you a callback, for when the database is ready. Then, within the callback you can call your prepopulated code.

The ViewModel class for MainActivity is ready! Now, it’s time to create one for your QuestionActivity. This will be slightly more complex, but don’t worry, you’ll be guided every step of the way.

Representing the state

QuestionActivity can have four different states at any given point:

  • Loading State: Displayed when the list of questions is being loaded from your database and it’s represented by a progress bar.

The Loading State.
The Loading State.

  • Data State: Displayed when you are ready to display a question to your user. This state is represented by a text and a group of radio buttons for the options.

The Data State.
The Data State.

  • Empty State: Displayed when there are no questions in your database. This state is represented by an image and a text saying that there are no questions.

The Empty State.
The Empty State.

  • Finish State: A special state, sent from ViewModel when there are no more questions to be displayed. This state is only used by QuestionActivity to know when to navigate to ResultActivity.

To represent the above states you are going to create a sealed class that your ViewModel will update and QuestionActivity will observe using LiveData. In the end, you’ll have an architecture like this.

The MVVM Architecture.
The MVVM Architecture.

Your Views will only be in charge of communicating the actions from the user to your ViewModel and rendering the data received. ViewModel will be in charge of communicating with the repository, handling the logic and sending UI-ready data to your View. Interesting huh? :]

Create a new Kotlin class under the model package and name it QuizState. Modify your class like below:

sealed class QuizState {
  object LoadingState : QuizState()
  data class DataState(val data: QuestionAndAllAnswers) : QuizState()
  object EmptyState : QuizState()
  data class FinishState(val numberOfQuestions: Int, val score: Int) : QuizState()
}

As you can see each of the aforementioned states is represented here. The DataState has a data attribute that contains a question and its associated answers. The FinishState class holds the number of questions in the quiz, within numberOfQuestions and the number of correct answers from the user within score. The other two states - LoadingState and EmptyState are simple objects, which represent events for the two cases.

Now, it’s time to create your ViewModel.

Changing the state

Create a new class under the viewmodel package and name it QuizViewModel. Modify your class like below:

class QuizViewModel(repository: QuizRepository) : ViewModel() {
}

Just like before, you are making the QuizViewModel class extend from ViewModel() and have the repository property in the primary constructor.

Next, add the following properties to the top of your class:

private val questionAndAnswers = MediatorLiveData<QuestionAndAllAnswers>() // 1
private val currentQuestion = MutableLiveData<Int>() // 2
private val currentState = MediatorLiveData<QuizState>() // 3
private val allQuestionAndAllAnswers = repository.getQuestionAndAllAnswers() // 4
private var score: Int = 0 // 5

Step by step:

  1. Represents the current QuestionAndAnswers that is going to be sent to QuestionActivity and displayed to the user.
  2. currentQuestion is a helper property that helps you keep track of which question has to be displayed from the list of questions retrieved from the repository. For example, if currentQuestion is equal to 0 you are going to display the question with question_id = 0.
  3. currentState contains the current QuizState that is going to be updated by your ViewModel and observed by QuestionActivity.
  4. allQuestionAndAllAnswers contains a LiveData list of all the questions in your database.
  5. score is another helper that holds the score of your user which is updated each time your user answers a question correctly.

Next, add the following methods:

fun getCurrentState(): LiveData<QuizState> = currentState

private fun changeCurrentQuestion() {
  currentQuestion.postValue(currentQuestion.value?.inc())
}

getCurrentState() will be used by your MainActivity to retrieve and observe the current QuizState. changeCurrentQuestion() simply adds one to the value of currentQuestion.

Next, add the following method:

private fun addStateSources() {
  currentState.addSource(currentQuestion) { currentQuestionNumber -> // 1
    if (currentQuestionNumber == allQuestionAndAllAnswers.value?.size) {
      currentState.postValue(QuizState.FinishState(currentQuestionNumber, score))
    }
  }
  currentState.addSource(allQuestionAndAllAnswers) { allQuestionsAndAnswers -> 
    // 2
    if (allQuestionsAndAnswers.isEmpty()) {
      currentState.postValue(QuizState.EmptyState)
    }
  }
  currentState.addSource(questionAndAnswers) { questionAndAnswers -> // 3
    currentState.postValue(QuizState.DataState(questionAndAnswers))
  }
}

You’ll call addStateSources() to add the sources which your currentState needs to observe: currentQuestion, allQuestionAndAllAnswers and questionAndAnswers. Taking each commented section in turn:

  1. If currentQuestion is equal to the number of questions in your database it means that your user has finished answering the questions in your quiz, so you’ll change the value of QuizState to FinishState.
  2. If the list of questions retrieved from the database is empty you’ll change the value of QuizState to EmptyState.
  3. This is the questionAndAnswers that you’ll send to the QuestionActivity in DataState.

Now, add the following method:

 private fun addQuestionSources() {
  questionAndAnswers.addSource(currentQuestion) { currentQuestionNumber ->
    val questions = allQuestionAndAllAnswers.value
      
    if (questions != null && currentQuestionNumber < questions.size) {
      questionAndAnswers.postValue(questions[currentQuestionNumber])
    }
  }
    
  questionAndAnswers.addSource(allQuestionAndAllAnswers) { questionsAndAnswers ->
    val currentQuestionNumber = currentQuestion.value 
      
    if (currentQuestionNumber != null && questionsAndAnswers.isNotEmpty()) { 
      questionAndAnswers.postValue(questionsAndAnswers[currentQuestionNumber])
    }
  }
}

This method adds two different sources to your questionAndAnswers: currentQuestion and allQuestionAndAllAnswers. Observing currentQuestion will help you update the current allQuestionAndAllAnswers that will be sent to the QuestionActivity using a Data. Observing allQuestionAndAllAnswers will tell your questionAndAnswers when the list of questions has been properly retrieved from your database.

Next, add the following methods:

fun nextQuestion(choice: Int) { // 1
  verifyAnswer(choice)
  changeCurrentQuestion()
}

private fun verifyAnswer(choice: Int) { // 2
  val currentQuestion = questionAndAnswers.value

  if (currentQuestion != null && currentQuestion.answers[choice].isCorrect) {
    score++
  }
}

Step by step:

  1. This method will be called when the user presses NEXT and will call verifyAnswer() and changeCurrentQuestion().
  2. verifyAnswer() checks if the answer selected by the user is correct and will increase the score value accordingly.

Finally, add the following init block, under your class properties, to set up your ViewModel:

init {
  currentState.postValue(QuizState.LoadingState)
  addStateSources()
  addQuestionSources()
  currentQuestion.postValue(0)
}

Here, you are initializing the QuizState as LoadingLoadingState and currentQuestion with a value of zero (0). You are also calling addStateSources() and addQuestionSources() as soon as your ViewModel is created.

And that’s it! Your ViewModels are ready. Now, you need to create your Views.

Defining your Views

As mentioned at the beginning of this chapter, the ViewModel is scoped to the lifecycle of an Activity or Fragment which means that it will live as long as its scope is still alive.

Getting access to a ViewModel

To create a ViewModel you usually call the ViewModelProviders.of(Scope).get(Type) which contains several utility methods that help you attach a ViewModel to a certain lifecycle and keep track of its state. This is how the code would look:

viewModel = ViewModelProvider(this).get(MainViewModel::class.java)

The only problem with the above approach is that the ViewModelProvider is responsible for creating our ViewModels and, as such, it can’t call their custom constructors. By default, ViewModelProvider will always call the empty constructor using get(). This is a problem because you need to pass the repository as a parameter.

There are several approaches to solve the above problem but the usual way of doing it is to create a factory for the ViewModel and to pass it to ViewModelProvider. To keep things short and to the point, a couple of extension functions for the Activity and Fragment classes have already been prepared for you that automatically take care of doing all of this for you. If you want to take a look at the source code just open Utils.kt under the root package.

Open MainActivity.kt and add the following property at the top of your class:

private val viewModel by lazy { getViewModel { MainViewModel(Repository()) } }

The above code uses lazy initialization to create your MainViewModel using getViewModel() which automatically takes care of the initialization and creation logic of the ViewModels using a ViewModelFactory.

Next, you need to set up menu actions for the user.

Interacting with the ViewModel

Add the following methods to MainActivity:

private fun prepopulateQuestions() = viewModel.prepopulateQuestions() // 1

private fun clearQuestions() = viewModel.clearQuestions() // 2

override fun onOptionsItemSelected(item: MenuItem): Boolean { // 3
  when (item.itemId) {
    R.id.prepopulate -> prepopulateQuestions()
    R.id.clear -> clearQuestions()
    else -> Toast.makeText(this, "error", Toast.LENGTH_SHORT).show()
  }
  return super.onOptionsItemSelected(item)
}

Step by step:

  1. prepopulateQuestions() uses your viewMode instance to prepopulate your Room database with sample question and answer records.
  2. clearQuestions() uses your viewModel instance to clear all the rows in your database.
  3. Here, you are adding the appropriate actions to your Activity‘s action bar. If the user taps on the prepopulate button, you’re going to call prepopulateQuestions(). If the user taps the clear button, you’ll call clearQuestions().

And that is all you need to do in your MainActivity! Now, you need to set up the ViewModel for QuestionActivity.

Open QuestionActivity.kt and add the following property for your QuizViewModel:

private val viewModel by lazy { getViewModel { QuizViewModel(Repository()) } }

Just like MainActivity, you are using lazy initialization and getViewModel() to create an instance of your QuizViewModel when QuestionActivity is created for the first time.

Now that your ViewModel is set up, you only need to create some methods that handle the different states provided by your QuizState and render them to the screen. Since all the logic is handled in your ViewModel, this should be pretty easy.

Add the following code:

private fun render(state: QuizState) {
  when (state) {
    is QuizState.EmptyState -> renderEmptyState()
    is QuizState.DataState -> renderDataState(state)
    is QuizState.FinishState -> goToResultActivity(state.numberOfQuestions, state.score)
    is QuizState.LoadingState -> renderLoadingState()
  }
}

render() will be in charge of calling the appropriate methods depending on QuizState using a when expression.

Now, add the following code to handle your states:

private fun renderDataState(quizState: QuizState.DataState) { //. 1
  binding.progressBar.visibility = View.GONE
  displayQuestionsView()
  binding.questionsRadioGroup.clearCheck()
  binding.questionTextView.text = quizState.data.question?.text
  binding.questionsRadioGroup.forEachIndexed { index, view ->
    if (index < quizState.data.answers.size)
      (view as RadioButton).text = quizState.data.answers[index].text
  }
}

private fun renderLoadingState() { // 2
  binding.progressBar.visibility = View.VISIBLE
}

private fun renderEmptyState() { // 3
  binding.progressBar.visibility = View.GONE
  binding.emptyDroid.visibility = View.VISIBLE
  binding.emptyTextView.visibility = View.VISIBLE
}

These are all the methods that render() will use to display different kinds of screens to the user. Taking each commented section in turn:

  1. renderDataState() displays a question to your user usingdata of your QuizState.Data.
  2. renderLoadingState() displays a progress bar.
  3. renderEmptyState() displays an image and text saying that there are no questions in the database.

Now, add the following methods:

fun nextQuestion() { // 1
  val radioButton = findViewById<RadioButton>(binding.questionsRadioGroup.checkedRadioButtonId)
  val selectedOption = binding.questionsRadioGroup.indexOfChild(radioButton)
  if (selectedOption != -1) {
    viewModel.nextQuestion(selectedOption)
  } else {
    Toast.makeText(this, getString(R.string.please_select_an_option), Toast.LENGTH_SHORT).show()
  }
}

private fun displayQuestionsView() { // 2
  binding.questionsRadioGroup.visibility = View.VISIBLE
  binding.questionTextView.visibility = View.VISIBLE
  binding.button.visibility = View.VISIBLE
}

private fun goToResultActivity(numberOfQuestions: Int, score: Int) { // 3
  val intent = Intent(this, ResultActivity::class.java).apply {
    addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
    addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
    putExtra(SCORE, score)
    putExtra(NUMBER_OF_QUESTIONS, numberOfQuestions)
  }

  startActivity(intent)
}

Briefly:

  1. nextQuestion() calls your ViewModel’s nextQuestion() passing the index of the selected radio button as a parameter. It will be called every time the user taps NEXT. The OnClickListener is bound within the XML to nextQuestion().
  2. displayQuestionsView() makes your questionsRadioGroup, questionTextView and button widgets visible, so you can display a question to your user.
  3. goToResultActivity() creates an Intentobject to start ResultActivity and passes the score and numberOfQuestions values as extras.

To define SCORE and NUMBER_OF_QUESTIONS, add the following code to the bottom of your class:

companion object {
  const val SCORE = "SCORE"
  const val NUMBER_OF_QUESTIONS = "NUMBER_OF_QUESTIONS"
}

The code above defines two string constants that will act as keys in Bundle being passed to ResultActivity.

Next, add the following code:

private fun getQuestionsAndAnswers() {
  viewModel.getCurrentState().observe(this) {
    render(it)
  }
}

This method simply calls getCurrentState() to get the QuizState value of your ViewModel. Since QuizState is a LiveData value you can observe it using observe() and each time its value changes you’ll call render() passing a new QuizState as a parameter.

Finally, add the following line inside onCreate():

binding.button.setOnClickListener { nextQuestion() }
getQuestionsAndAnswers()

In the code above, you set a click listener on the NEXT button which invokes nextQuestion() when clicked. Since you immediately want to start observing the QuizState value, onCreate() is the best place to call getQuestionsAndAnswers().

The last step to finish building your app is to display the quiz results to your user.

Open ResultActivity.kt under the view package. Add the following code inside onCreate(), importing scoreTextView from activity_result.xml:

val score = intent.extras?.getInt(QuestionActivity.SCORE)
val numberOfQuestions = intent.extras?.getInt(QuestionActivity.NUMBER_OF_QUESTIONS)
binding.scoreTextView.text = String.format(getString(R.string.score_message), score, numberOfQuestions)

That’s it! Build and run your app to see it in action.

The Start Screen shows properly.
The Start Screen shows properly.

Click START to see the empty screen layout.

The Empty Screen.
The Empty Screen.

Now, go back to the main screen and click prepopulate in your action bar:

The Prepopulate Menu Option.
The Prepopulate Menu Option.

Click START and you’ll see your question and answers displayed.

The Question & Answers Screen.
The Question & Answers Screen.

Answer the questions and take a look at the final screen to see your score!

The Results Screen.
The Results Screen.

Sweet! Your app is now working and displaying your Question and Answers!

Key points

  • LiveData is a data holder class, as a List, that can be observed for changes by an Observer.
  • LiveData is lifecycle-aware, meaning it can observe the lifecycle of Android components like the Activity or Fragment. It will only keep updating observers if its component is still active.
  • ViewModel is part of the Android Architecture Components and it’s specifically designed to manage data related to your user interface.
  • A Repository helps you separate concerns to have a single entry point for your app’s data.
  • You can combine LiveDatas and add different sources, to take action if something changes.

Where to go from here?

I hope you enjoyed this chapter! If you had trouble following along, you can always download the final project attached to this chapter.

So far, you have only learned about three of the Android Architecture Components classes: LiveData, ViewModel and Room. However, there are many other classes that Google provides. If you want to learn about them, you can use the following resources:

See you in the next chapter!

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.