Android Kotlin Fundamentals 05.1: ViewModel and ViewModelFactory 번역 #6

뷰모델은 configuration change 시에도 살아남아 있습니다. 그래서 뷰모델은 configuration change시 데이터를 보관하기에 좋습니다.

 

  • 화면에 보여져야 하는 데이터와 이 데이터를 처리하는 코드를 뷰모델에 추가합니다.
  • 뷰모델은 프레그먼트, 액티비티, 그리고 뷰와 같은 UI Controller의 참조를 가지지 않습니다.

뷰모델을 추가하기 전의 staret app과 추가한 후의 app을 비교 해 봅시다.

 

  • ViewModel을 추가하기 전 : 화면 회전과 같은 Configuration Change가 일어나면 game fragment는 destroyed되고 re-created 됩니다. 이때 데이터는 사라집니다.
  • ViewModel을 추가하고 game fragment의 데이터를 뷰모델에 옮겼을 때 : game fragment의 UI를 그리기 위한 모든 데이터는 뷰모델 안에 있습니다. 앱에서 Configuration Change가 일어 날 때, 뷰모델은 살아 있으며 데이터 또한 보존 됩니다.

이번 태스크에서는 앱의 UI 관련 데이터와 데이터를 처리하는 메소드를 GameViewModel로 옮길것 입니다. 이렇게 함으로써 configuration change가 일어나도 데이터를 보존 할 수 있게 됩니다.

 

Step 1: Move data fields and data processing to the ViewModel 

 

GameFragment의 데이터와 메소드들을 GameViewModel로 옮기세요.

 

1. word, score, wordList 데이터를 옮기세요. word와 score 데이터는 private가 아님을 주의하세요.

 

binding 변수 (GameFragmentBinding)는 옮기지 마세요. 왜냐하면 해당 변수는 view에 대한 참조를 가지고 있습니다. 이 변수는 layout을 inflate하고, click listener들을 셋팅하고 데이터들을 화면에 보여줄때 사용합니다. 이는 fragment 즉, UI controller의 업무입니다.

 

2. 다음으로 resetList() nextWord() 메소드를 옮겨 주세요. 이 메소드들은 어떤 단어가 화면에 보여야 하는지 결정 합니다.

 

3. resetList()와 nextWord()를 GameViewModel의 init 블록에 넣어주세요.

이 메소드들은 꼭 init 블록안에 있어야 합니다. 왜냐하면 프레그먼트가 생성되었을 때가 아닌, ViewModel이 생성되었을 때 resetList()가 호출되어야 하기 때문입니다. 

 

onSkip()과 onCorrect()는 데이터처리와 ui를 업데이트하는 코드를 가지고 있습니다. ui를 업데이트 하는 코드는 fragment에 남기고, 데이터를 처리하는 코드만 ViewModel로 옮깁시다.

 

1. onSkip()과 onCorrect()를 복사해서 GameViewModel에 붙여 넣어 줍니다.

2. GameViewModel에서 onSkip()과 onCorrect()는 private가 아닙니다. 왜냐하면 이 함수들에 대한 참조를 GameFragemnt에서 할 것이기 때문입니다.

 

현재 GameViewModel에 대한 코드는 아래와 같습니다.

class GameViewModel : ViewModel() {
   // The current word
   var word = ""
   // The current score
   var score = 0
   // The list of words - the front of the list is the next word to guess
   private lateinit var wordList: MutableList<String>

   /**
    * Resets the list of words and randomizes the order
    */
   private fun resetList() {
       wordList = mutableListOf(
               "queen",
               "hospital",
               "basketball",
               "cat",
               "change",
               "snail",
               "soup",
               "calendar",
               "sad",
               "desk",
               "guitar",
               "home",
               "railway",
               "zebra",
               "jelly",
               "car",
               "crow",
               "trade",
               "bag",
               "roll",
               "bubble"
       )
       wordList.shuffle()
   }

   init {
       resetList()
       nextWord()
       Log.i("GameViewModel", "GameViewModel created!")
   }
   /**
    * Moves to the next word in the list
    */
   private fun nextWord() {
       if (!wordList.isEmpty()) {
           //Select and remove a word from the list
           word = wordList.removeAt(0)
       }
       updateWordText()
       updateScoreText()
   }
 /** Methods for buttons presses **/
   fun onSkip() {
       if (!wordList.isEmpty()) {
           score--
       }
       nextWord()
   }

   fun onCorrect() {
       if (!wordList.isEmpty()) {
           score++
       }
       nextWord()
   }

   override fun onCleared() {
       super.onCleared()
       Log.i("GameViewModel", "GameViewModel destroyed!")
   }
}

 

GameFragment에 대한 코드는 아래와 같습니다.

/**
* Fragment where the game is played
*/
class GameFragment : Fragment() {


   private lateinit var binding: GameFragmentBinding


   private lateinit var viewModel: GameViewModel


   override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                             savedInstanceState: Bundle?): View? {

       // Inflate view and obtain an instance of the binding class
       binding = DataBindingUtil.inflate(
               inflater,
               R.layout.game_fragment,
               container,
               false
       )

       Log.i("GameFragment", "Called ViewModelProviders.of")
       viewModel = ViewModelProviders.of(this).get(GameViewModel::class.java)

       binding.correctButton.setOnClickListener { onCorrect() }
       binding.skipButton.setOnClickListener { onSkip() }
       updateScoreText()
       updateWordText()
       return binding.root

   }


   /** Methods for button click handlers **/

   private fun onSkip() {
       if (!wordList.isEmpty()) {
           score--
       }
       nextWord()
   }

   private fun onCorrect() {
       if (!wordList.isEmpty()) {
           score++
       }
       nextWord()
   }


   /** Methods for updating the UI **/

   private fun updateWordText() {
       binding.wordText.text = word
   }

   private fun updateScoreText() {
       binding.scoreText.text = score.toString()
   }
}

 

Step2 : Update reference to click handlers and data fields in GameFragment

 

1. GameFragment에서 onSkip()과 onCorrect()를 업데이트 해줍니다. score를 업데이트하는 코드들을 제거 해줍니다. 그리고 뷰모델의 onSkip()과 onCorrect()를 호출 해 줍니다.

 

2. 왜냐하면 nextWord()를 뷰모델로 옮겼기 때문에 더 이상 GameFragment에서 해당 메소드를 호출 할 수 없습니다.

 

GameFragment의 onSkip()과 onCorrect()안에서 nextWord()를 updateScoreText()와 updateWordText()로 변경 해 줍니다. 이 메소드 들은 데이터를 화면에 보여주는 역할을 합니다.

 

 

3. GameFragment에서 GameViewModel 데이터를 사용하여 score와 wrod 변수를 업데이트 합니다. 왜냐하면 이제 데이터 관련 변수들은 GameViewModel에 있기 때문입니다.

 

 

Reminder : 앱의 액티비티, 프레그먼트 , 뷰들이 configuration change가 발생하면 살아 있지 않기 때문에, ViewModel은 UI Controller에 대한 참조를 가져선 안됩니다. 

 

4. GameViewMdoel의 nextWord() 메소드 안에서 updateWordText()와 updateScoreText()를 지웁니다. 이 메소드들은 GameFragment에서 사용 될 것 입니다. (데이터를 화면에 보여주는 용도) 

 

5. 앱을 빌드 시켜보세요. 에러가 발생한다면 clean Project를 한 뒤 rebuild 해 보세요.

 

6. 앱을 실행 해 보세요. 그리고 게임을 진행 하면서 화면 회전을 해 보세요. 화면 회전 시 current score와 current word가 유지 되는 것을 확인 할 수 있습니다.

 

지금까지 ViewModel에 데이터를 저장했고, 이것이 configuration change시 에도 유지된 다는 것을 확인 해 봤습니다.

댓글



Designed by JB FACTORY