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

Useful Shortcuts

App architecture

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


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(


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.

// 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

웹사이트 설정

웹페이지 색상을 선택하세요