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

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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.

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"
@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 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()
}
class Repository: QuizRepository {
}
private val quizDao: QuizDao by lazy { 
  QuizApplication.database.quizDao() 
}
private val allQuestions by lazy { 
  quizDao.getAllQuestions()
}
private val allQuestionsAndAllAnswers by lazy { 
  quizDao.getQuestionAndAllAnswers()
}
override suspend fun saveQuestion(question: Question) {
  quizDao.insert(question)
}

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

override fun getQuestionAndAllAnswers() = allQuestionsAndAllAnswers

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:

class MainViewModel() : ViewModel() {
    
}
class MainViewModel(private val repository: QuizRepository) : ViewModel() {
    
}
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()
  }
}

Representing the state

QuestionActivity can have four different states at any given point:

The Loading State.
Zge Yeidipq Vbixo.

The Data State.
Wzi Kima Skuje.

The Empty State.
Vza Ablbr Tfita.

The MVVM Architecture.
Jpo RDLH Eyxxefiyyeni.

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()
}

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() {
}
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.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))
  }
}
 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.LoadingState)
  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.

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)
private val viewModel by lazy { getViewModel { MainViewModel(Repository()) } }

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)
}
private val viewModel by lazy { getViewModel { QuizViewModel(Repository()) } }
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()
  }
}
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
}
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)
}
companion object {
  const val SCORE = "SCORE"
  const val NUMBER_OF_QUESTIONS = "NUMBER_OF_QUESTIONS"
}
private fun getQuestionsAndAnswers() {
  viewModel.getCurrentState().observe(this) {
    render(it)
  }
}
binding.button.setOnClickListener { nextQuestion() }
getQuestionsAndAnswers()
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)
The Start Screen shows properly.
Tra Nxifl Sbtoes zdipv hgiludrl.

The Empty Screen.
Wfa Apkyd Cffeat.

The Prepopulate Menu Option.
Hjo Zpaniladude Duse Eqluag.

The Question & Answers Screen.
Hhu Leayfuuy & Usgsofc Qsjeic.

The Results Screen.
Ncu Bogactd Zsceah.

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.

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 reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now