Android Kotlin Fundamentals 노트 3

2020년 10월 15일

Android Kotlin Fundamentals Course 코드랩 하면서 노트. Android Kotlin Fundamentals: ViewModel 부터.

Useful Shortcuts

  • Override Methods: Choose Code > Override Methods, or CTRL+o
  • Comment: Choose Code > Comment with Line Comment or CTRL+/ (Command+/ on a Mac)

App architecture

Follow Android app architecture guidlines and Android Architecture Components. Similarly MVVM.

  • UI-controller: Activity or Fragment. Display data and capture OS/user events.
  • ViewModel: Hold all of the data needed for the UI and prepare it for display
  • ViewModelFactory: instantiates ViewModel


ViewModel class to store and manage UI-releated data in a lifecycle-conscious way. (safe for device-configuration changes via ViewModelFactory)

Add dependencies

// build.gradle (app-level)
dependencies {
  // ...

  implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'

Then, sync.

Create GameViewModel in screens/game/ folder.

class GameViewModel : ViewModel() {
  init {
    Log.i("GameViewModel", "GameViewModel created!")

  override fun onCleared() {
    Log.i("GameViewModel", "GameViewModel destoryed!")

Associate GameViewModel with the game fragment.

// in GameFragment.kt
private lateinit var viewModel: GameViewModel

ViewModelProvider create the ViewModel instance so that the view model survives during the configuration changes. The provider manages the lifecycle of the view model until the UI is destroyed.

Initialize the viewModel using ViewModelProvider.get().

Log.i("GameFragment", "Called ViewModelProvider.get")
viewModel = ViewModelProvider(this).get(


  • Fragment: Display fragment, capture user events, do not survive config changes
  • ViewModel: Hold data for fragment, should never contain references to any fragments/activities

Move all states from Fragment to ViewModel.

Pass data to next fragments.

private fun gameFinished() {
  Toast.makeText(activity, "Game has just finished", Toast.LENGTH_SHORT).show()
  val action = GameFragmentDirections.actionGameToScore()
  action.score = viewModel.score

However, ViewModel is destoryed during the transition. It needs ViewModelFactory for that.


Create ScoreViewModel.

class ScoreViewModel(finalScore: Int): ViewModel() {
  var score = finalScore
  init {
    Log.i("ScoreViewModel", "Final score is $finalScore")

Create ScoreViewFactory and override the create() method.

class ScoreViewModelFactory(private val finalScore: Int) : ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom( {
            return ScoreViewModel(finalScore) as T
        throw IllegalArgumentException("Unknown viewModel class")

Add members in the fragment.

private lateinit var viewModel: ScoreViewModel
private lateinit var viewModelFactory: ScoreViewModelFactory

Initialize viewModel using the factory from onCreateView().

// in `onCreateView()`
// init factory with given arguments via navigation
viewModelFactory = ScoreViewModelFactory(ScoreFragmentArgs.fromBundle(requireArguments()).score)
// get view model from the provider via factory
viewModel = ViewModelProvider(this, viewModelFactory)

Then, the fragment uses data from the viewModel.

binding.scoreText.text = viewModel.score.toString()

Parameterized constructor of viewModel is useful when the data should be right during initialization.


Data objects that notify views when the underlying database changes. Set up "observers" to the activities or fragments. Also it is lifecycle-aware and watch only active ones.

  • LiveData is observable, which means that an observer is notified when the data held by the LiveData object changes.
  • LiveData holds data; LiveData is a wrapper that can be used with any data
  • LiveData is lifecycle-ware. For this, the observer is associated with LifecycleOwner so that is working with active state only.
// GameViewModel.kt
val word = MutableLiveData<String>()
val score = MutableLiveData<Int>()

init {
  word.value = ""
  score.value = 0

Update object references. minus() and plus() are null-safety manipulation.

// score--
score.value = (score.value)?.minus(1)

// score++
score.value = (score.value)?.plus(1)

// word = wordList.removeAt(0)
word.value = wordList.removeAt(0)

References in the fragment needs to update too.

// binding.wordText.text = viewModel.word
binding.wordText.text = viewModel.word.value

// action.score = viewModel.score
action.score = viewModel.score.value?:0

Attach observers to the LiveData objects

Note: fragment's lifecycle and fragment view's lifecycle is a bit different. Always set the observers in onCreateView() and pass viewLifecycleOwner to observers.

// in onCreateView()

viewModel.score.observe(viewLifecycleOwner, Observer { newScore ->
  binding.scoreText.text = newScore.toString()
viewModel.word.observe(viewLifecycleOwner, Observer { newWord ->
  binding.wordText.text = newWord.toString()

Then, remove updateWordText() and updateScoreText().

Use MutableLiveData for encapsulation

Using backing property, ViewModel uses MutableLiveData internally and exposes LiveData to public access.

// from
val score = MutableLiveData<Int>()

// to
private val _score = MutableLiveData<Int>()
val score: LiveData<Int>
  get() = _score

Check Getters and Setters in Kotlin.

Internally, viewModel should use _score now.

// in ViewModel
_score.value = (score.value)?.minus(1)

// for reading
binding.scoreText.text = score.value.toString()

Example: game-finish event

private val _eventGameFinish = MutableLiveData<Boolean>()
val eventGameFinish: LiveData<Boolean>
  get() = _eventGameFinish

fun nextWord() {
  if (wordList.isNotEmpty()) {
    //Select and remove a word from the list
    _word.value = wordList.removeAt(0)
  } else {

fun onGameFinish() {
  _eventGameFinish.value = true

fun onGameFinishComplete() {
  _eventGameFinish.value = false

in the fragment,

// onCreateView()
viewModel.eventGameFinish.observe(viewLifecycleOwner, Observer<Boolean> { hasFinished ->
  if (hasFinished) gameFinished()

private fun gameFinished() {
  // ...

Example: transition event

// in ViewModel
private val _eventPlayAgain = MutableLiveData<Boolean>()
val eventPlayAgain: LiveData<Boolean>
    get() = _eventPlayAgain

fun onPlayAgain() {
    _eventPlayAgain.value = true
fun onPlayAgainComplete() {
    _eventPlayAgain.value = false
// onCreateView() in Fragment
binding.playAgainButton.setOnClickListener { viewModel.onPlayAgain() }

viewModel.eventPlayAgain.observe(viewLifecycleOwner, Observer<Boolean> { playAgain ->
  if (playAgain) {

Data binding with ViewModel and LiveData


Views        <- UI Controller          <- ViewModel
(XML layout)    (activity/fragment        (LiveData)
                 with click listeners)

ViewModel passed into the data binding:

Views        <- ViewModel
(XML layout)    (LiveData)

Data binding with ViewModel

Add ViewModel into fragment xml file.

<layout ...>
      type="" />


Add data binding at onCreateView() in the Fragment.

binding.gameViewModel = viewModel

Use listener bindings for event handling via lamda expression.

   android:onClick="@{() -> gameViewModel.onSkip()}"
   ... />

Then, remove all onClickListener() in the acitivities and fragments.

Note: data-binding error messages are quite hard to understand. Read the error and check the spell carefully.

Data binding with LiveData

  ... />
  ... />
binding.gameViewModel = viewModel
binding.lifecycleOwner = viewLifecycleOwner

Then, remove all observe() for updating the content.


in string.xml:

<string name="quote_format">\"%s\"</string>
<string name="score_format">Current Score: %d</string>

in the fragment xml file,

  ... />

Check Layouts and binding expressions.

As a result, each fragments only observes the navigation related LiveData.

Transformations for LiveData

Transformations manipulates the data in LiveData for sanitizing, deducting, subsetting, etc. e.g., Transformations.switchMap.

Add a timer

// in ViewModel
companion object {
  private const val DONE = 0L
  private const val ONE_SECOND = 1000L
  private const val COUNTDOWN_TIME = 60000L

private val _currentTime = MutableLiveData<Long>()
val currentTime: LiveData<Long>
  get() = _currentTime

private val timer: CountDownTimer

init {
  timer = object : CountDownTimer(COUNTDOWN_TIME, ONE_SECOND) {
    override fun onTick(millisUntilFinished: Long) {
      _currentTime.value = millisUntilFinished / ONE_SECOND
    override fun onFinish() {
      _currentTime.value = DONE


override fun onCleared() {

Now, the time should display "MM:SS" format rather than just a number. DataUtils.formatElaspedTime() is utility method takes a long number of milliseconds and format the number to the time format.

// in ViewModel
val currentTimeString = { time ->
// it's like a computed property in Swift and/or Vue.js.

Then, update fragment xml file.

  ... />

다음 챕터: Android Kotlin Fundamentals: 06.1 Create a Room database