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
QuizDatabaseclass and your DAOs. model contains your entities:QuestionandAnswer. - The view package contains all the activities for your app:
SplashActivity,MainActivity,QuestionActivityandResultActivity.
Build and run the app on a device or emulator.
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
Buttons,ListViews,RecyclerViews orTextViews 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
Activitybecomes paused or stopped it won’t receive any newLiveDatanotifications. -
No more manual lifecycle handling: Google made sure that
LiveDatais lifecycle-aware by default, so you don’t have to manually control them. -
Proper configuration changes: Since
LiveDatais 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:
LiveDatacan 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 theBundleinonCreate(). However, this is not always a good approach sinceBundles 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 asViewModels.
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
ViewModelto 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 useaddCallback()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.
- 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.
- 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.
-
Finish State: A special state, sent from
ViewModelwhen there are no more questions to be displayed. This state is only used byQuestionActivityto know when to navigate toResultActivity.
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.
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:
- Represents the current
QuestionAndAnswersthat is going to be sent toQuestionActivityand displayed to the user. -
currentQuestionis 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, ifcurrentQuestionis equal to 0 you are going to display the question withquestion_id = 0. -
currentStatecontains the currentQuizStatethat is going to be updated by yourViewModeland observed byQuestionActivity. -
allQuestionAndAllAnswerscontains aLiveDatalist of all the questions in your database. -
scoreis 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:
- If
currentQuestionis 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 ofQuizStatetoFinishState. - If the list of questions retrieved from the database is empty you’ll change the value of
QuizStatetoEmptyState. - This is the
questionAndAnswersthat you’ll send to theQuestionActivityinDataState.
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:
- This method will be called when the user presses NEXT and will call
verifyAnswer()andchangeCurrentQuestion(). -
verifyAnswer()checks if the answer selected by the user is correct and will increase thescorevalue 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:
-
prepopulateQuestions()uses yourviewModeinstance to prepopulate your Room database with sample question and answer records. -
clearQuestions()uses yourviewModelinstance to clear all the rows in your database. - 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 callprepopulateQuestions(). If the user taps the clear button, you’ll callclearQuestions().
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:
-
renderDataState()displays a question to your user usingdataof yourQuizState.Data. -
renderLoadingState()displays a progress bar. -
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:
-
nextQuestion()calls yourViewModel’snextQuestion()passing the index of the selected radio button as a parameter. It will be called every time the user taps NEXT. TheOnClickListeneris bound within the XML tonextQuestion(). -
displayQuestionsView()makes yourquestionsRadioGroup,questionTextViewandbuttonwidgets visible, so you can display a question to your user. -
goToResultActivity()creates anIntentobject to startResultActivityand passes thescoreandnumberOfQuestionsvalues 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.
Click START to see the empty screen layout.
Now, go back to the main screen and click prepopulate in your action bar:
Click START and you’ll see your question and answers displayed.
Answer the questions and take a look at the final screen to see your score!
Sweet! Your app is now working and displaying your Question and Answers!
Key points
-
LiveDatais a data holder class, as aList, that can be observed for changes by anObserver. -
LiveDatais lifecycle-aware, meaning it can observe the lifecycle of Android components like theActivityorFragment. It will only keep updating observers if its component is still active. -
ViewModelis 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 differentsources, 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:
- This tutorial about MVVM and Databinding with Android Design Patterns: https://www.raywenderlich.com/636803-mvvm-and-databinding-android-design-patterns.
- This tutorial about the Paging Library which helps create Lists on Android: https://www.raywenderlich.com/6948-paging-library-for-android-with-kotlin-creating-infinite-lists.
- This tutorial about the WorkManager architecture component in which you’ll learn how to create and manage background tasks: https://www.raywenderlich.com/6040-workmanager-tutorial-for-android-getting-started.
See you in the next chapter!