9.
Using Room with Google's Architecture Components
Written by Aldo Olivares
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 lifecycle of the app and configuration changes. This is where Google’s architecture components such as the ViewModel and LiveData come to the rescue!
In this chapter, you will learn:
- What
LiveDataandViewModelcomponents are, and how to use them. - How to make your DAOs return
LiveDatas 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.
Dive in!
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 with the starter project attached to this chapter and open it using Android Studio 3.4, or greater, by going to File ▸ New ▸ Import Project and selecting the build.gradle file, or by using the File ▸ Open Existing Project, 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
QuizDatabaseand your DAOs. The model package contains your entities:QuestionandAnswer. - The view package contains all the activities for your app:
MainActivity,QuestionActivityandResultActivity.
Once the starter project finishes loading and building, 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, let’s talk about LiveData.
Using LiveData with a repository
To use LiveData, you first need to learn what a LiveData 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 LiveDatas is 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
LiveDataimplements the observer pattern, you can be sure that your UI widgets such asButtons,ListViews,RecyclerViews orTextViews will always be updated with the latest information. -
No memory leaks: Since
LiveDataobservers 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
LiveDatacomponents are 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.
LiveData sounds awesome, right? Well, you are going to use it to wrap the results of your DAO queries, and as such, activities and fragments 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 LiveDatas.
Adding LiveData to the project
If you check out your app’s build.gradle file, you can find the following code:
// architecture components
implementation "androidx.arch.core:core-common:$androidx_common"
implementation
"androidx.lifecycle:lifecycle-common:$lifecycle_components"
implementation
"androidx.lifecycle:lifecycle-extensions:$lifecycle_components"
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 under 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 actually 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>>
Note: Don’t forget to import all missing dependencies using Alt + Enter on Windows or Option + Enter on Mac.
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 lists of 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, 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 will create a Repository that acts as a bridge between your DAOs and the ViewModels, which you will define later.
Creating a QuizRepository
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>>
fun saveQuestion(question: Question)
fun saveAnswer(answer: Answer)
fun getQuestionAndAllAnswers(): LiveData<List<QuestionAndAllAnswers>>
fun deleteQuestions()
}
The above interface defines the methods that you will 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 the following properties at the top of your class:
private val quizDao: QuizDao by lazy { QuizApplication.database.questionsDao() }
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() like this:
override fun saveQuestion(question: Question) {
AsyncTask.execute { quizDao.insert(question) }
}
override fun saveAnswer(answer: Answer) {
AsyncTask.execute { quizDao.insert(answer) }
}
The above code uses insert() of your quizDao to create new Question and Answer records in your database. You are using an AsyncTask because you don’t want to execute long-running write operations on the main thread.
Note: You can also choose different mechanisms to schedule the insert operations in the background, such as Kotlin Coroutines or creating your own threads.
Next, change the deleteQuestions() to the following code:
override fun deleteQuestions() {
AsyncTask.execute { quizDao.clearQuestions() }
}
Just like saveAnswer() and saveQuestion(), this method uses an AsyncTask 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 will make your code much easier to maintain and test.
All right, that’s enough about LiveData. Let’s talk about ViewModels!
Creating ViewModels
The ViewModel is a part of the Google’s 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:d 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 is better to separate
Views from the business logic by delegating this task to a more appropriate class such as theViewModels.
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 the 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 ViewModel.
The ViewModel sounds like the perfect choice to manage the questions and answers for your DroidQuiz app, right?
You should find a package under drodquiz with the name viewmodel. Create a new Kotlin class inside this package and name it MainViewModel.
Make MainViewModel extend the ViewModel 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 ViewModels, 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() {
for (question in QuestionInfoProvider.questionList) {
repository.saveQuestion(question)
}
for (answer in QuestionInfoProvider.answerList) {
repository.saveAnswer(answer)
}
}
fun clearQuestions() = repository.deleteQuestions()
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.
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 will 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 for your MainActivity is ready! Now, it is time to create the ViewModel for your QuestionActivity.
Next, you need to create the ViewModel for your QuestionActivity, This will be slightly more complex, but don’t worry, you’ll be guided every step of the way.
The QuestionActivity can have three different states at any given point:
- Loading State: Displayed when the list of questions are being loaded from your database and it is 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 the
ViewModelwhen there are no more questions to be displayed. This state is only used by theQuestionActivityto know when to navigate to theResultActivity.
To represent the above states you are going to create a sealed class that your ViewModel will update and the QuestionActivity will observe using LiveData. In the end, you will 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. The 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 Data has a contains the question that you will display to your users, within data. The Finish 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 - Loading and Empty are simple objects, which represent events for the two cases.
Now, it is time to create your ViewModel.
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 your ViewModel and observed by yourQuestionActivity. -
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.Finish(currentQuestionNumber, score))
}
}
currentState.addSource(allQuestionAndAllAnswers) { allQuestionsAndAnswers -> // 2
if (allQuestionsAndAnswers.isEmpty()) {
currentState.postValue(QuizState.Empty)
}
}
currentState.addSource(questionAndAnswers) { questionAndAnswers -> // 3
currentState.postValue(QuizState.Data(questionAndAnswers))
}
}
You will 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 will change the value ofQuizStatetoFinish. - If the list of questions retrieved from the database is empty you will change the value of
QuizStatetoEmpty. - This is the
questionAndAnswersthat you will send to theQuestionActivityinData.
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.
Now, add the following code:
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 the NEXT button and will call
verifyAnswer()andchangeCurrentQuestion(). -
verifyAnswer()checks if the answer selected by the user is correct and will increase thescoreaccordingly.
Finally, add the following init block, under your class properties, to set up your ViewModel:
init {
currentState.postValue(QuizState.Loading)
addStateSources()
addQuestionSources()
currentQuestion.postValue(0)
}
Here, you are initializing the QuizState as Loading 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.
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 = ViewModelProviders.of(this).get(MainViewModel::class.java)
The only problem with the above approach is that the ViewModelProviders is responsible for creating our ViewModels and, as such, it can’t call their custom constructors. By default, ViewModelProviders 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 ViewModelProviders. 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:
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("error")
}
return super.onOptionsItemSelected(item)
}
Step by step:
-
prepopulateQuestions()uses yourviewModelto prepopulate your Room database with sample question and answer records. -
clearQuestions()uses yourviewModelto 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 will callclearQuestions().
And that is all you need to do in your MainActivity! Now, you need to set up the ViewModel for the 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 the QuestionActivity is first created.
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.Empty -> renderEmptyState()
is QuizState.Data -> renderDataState(state)
is QuizState.Finish -> goToResultActivity(state.numberOfQuestions, state.score)
is QuizState.Loading -> 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
progressBar.visibility = View.GONE
displayQuestionsView()
questionsRadioGroup.clearCheck()
questionTextView.text = quizState.data.question?.text
questionsRadioGroup.forEachIndexed { index, view ->
if (index < quizState.data.answers.size)
(view as RadioButton).text = quizState.data.answers[index].text
}
}
private fun renderLoadingState() { // 2
progressBar.visibility = View.VISIBLE
}
private fun renderEmptyState() { // 3
progressBar.visibility = View.GONE
emptyDroid.visibility = View.VISIBLE
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(view: View) { // 1
val radioButton = findViewById<RadioButton>(questionsRadioGroup.checkedRadioButtonId)
val selectedOption = questionsRadioGroup.indexOfChild(radioButton)
viewModel.nextQuestion(selectedOption)
}
private fun displayQuestionsView() { // 2
questionsRadioGroup.visibility = View.VISIBLE
questionTextView.visibility = View.VISIBLE
button.visibility = View.VISIBLE
}
private fun goToResultActivity(numberOfQuestions: Int, score: Int) { //. 3
startActivity(
intentFor<ResultActivity>(
SCORE to score,
NUMBER_OF_QUESTIONS to numberOfQuestions
).newTask().clearTask()
)
}
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 the NEXT button. TheOnClickListeneris bound within the XML tonextQuestion(). -
displayQuestionsView()makes yourquestionsRadioGroup,questionTextViewandbuttonwidgets visible, so you can display a question to your user. -
goToResultActivity()creates anIntentto start theResultActivityand passes thescoreandnumberOfQuestionsas extras.
Add the following code and import the androidx.lifecycle.Observer:
private fun getQuestionsAndAnswers() {
viewModel.getCurrentState().observe(this, Observer {
render(it)
})
}
This method simply calls the getCurrentState() method to get the QuizState of your ViewModel. Since QuizState is a LiveData you can observe it using observe() and each time its value changes you will call render() passing the new QuizState as a parameter.
Finally, add the following line inside onCreate():
getQuestionsAndAnswers()
Since you immediately want to start observing the QuizState, onCreate() is the best place to call this method.
The last step to finish building your app is to display the quiz results to your user.
Open the ResultActivity.kt file under the view package. Add the following code inside onCreate(), importing the scoreTextView from activity_result XML:
val score = intent.extras?.getInt(QuestionActivity.SCORE)
val numberOfQuestions = intent.extras?.getInt(QuestionActivity.NUMBER_OF_QUESTIONS)
scoreTextView.text = String.format(getString(R.string.score_message), score, numberOfQuestions)
That’s it! build and run your app to see it in action:
Now, try clicking the START button and you should see the empty screen layout:
Now, go back to the main screen and click the prepopulate button in your action bar:
Click START and you should now 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, like 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. - The
ViewModelis part of the Google’s 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 troubles following along, you can always download the final project attached to this chapter.
So far, you have only learned about three of the Google 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 will 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!