Chapters

Hide chapters

Saving Data on Android

First Edition · Android 10 · Kotlin 1.3 · AS 3.5

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Using Firebase

Section 3: 11 chapters
Show chapters Hide chapters

9. Using Room with Google's Architecture Components
Written by Aldo Olivares

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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 LiveData and ViewModel components 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 QuizDatabase and your DAOs. The model package contains your entities: Question and Answer.
  • The view package contains all the activities for your app: MainActivity, QuestionActivity and ResultActivity.

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.

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"
@Query("SELECT * FROM question ORDER BY question_id")
fun getAllQuestions(): List<Question>

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

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

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()
}
class Repository : QuizRepository {

}
private val quizDao: QuizDao by lazy { QuizApplication.database.questionsDao() }
private val allQuestions by lazy { quizDao.getAllQuestions() }
private val allQuestionsAndAllAnswers by lazy { quizDao.getQuestionAndAllAnswers() }
override fun saveQuestion(question: Question) {
  AsyncTask.execute { quizDao.insert(question) }
}

override fun saveAnswer(answer: Answer) {
  AsyncTask.execute { quizDao.insert(answer) }
}
override fun deleteQuestions() {
  AsyncTask.execute { quizDao.clearQuestions() }
}
override fun getSavedQuestions() = allQuestions

override fun getQuestionAndAllAnswers() = allQuestionsAndAllAnswers

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:

class MainViewModel() : ViewModel() {
    
}
class MainViewModel(private val repository: QuizRepository) : ViewModel() {
    
}
fun prepopulateQuestions() {
  for (question in QuestionInfoProvider.questionList) {
    repository.saveQuestion(question)
  }
  for (answer in QuestionInfoProvider.answerList) {
    repository.saveAnswer(answer)
  }
}

fun clearQuestions() = repository.deleteQuestions()

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()
}
class QuizViewModel(repository: QuizRepository) : ViewModel() {
}
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
fun getCurrentState(): LiveData<QuizState> = currentState

private fun changeCurrentQuestion() {
  currentQuestion.postValue(currentQuestion.value?.inc())
}
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))
  }
}
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])
    }
  }
}
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++
  }
}
init {
  currentState.postValue(QuizState.Loading)
  addStateSources()
  addQuestionSources()
  currentQuestion.postValue(0)
}

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.

viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
private val viewModel by lazy { getViewModel { MainViewModel(Repository()) } }
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)
}
private val viewModel by lazy { getViewModel { QuizViewModel(Repository()) } }
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()
  }
}
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
}
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()
  )
}
private fun getQuestionsAndAnswers() {
  viewModel.getCurrentState().observe(this, Observer {
    render(it)
  })
}
getQuestionsAndAnswers()
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)

Key points

  • LiveData is a data holder class, like 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.
  • The ViewModel is 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 different sources, 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.

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.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now