Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

yourginieus

ViewModel : ViewModelFactory 본문

Android/Android Kotlin 기초 실습 정리

ViewModel : ViewModelFactory

EOJIN 2022. 10. 26. 23:13

App architecture : 애플리케이션 아키텍처

  • 앱 아키텍쳐란, 앱의 class들과 그 사이의 관계를 설명하는 방법으로, 특정 시나리오에서 잘 수행되며 작업하기 쉬움
  • Android app architecture는 MVVM(model-view-viewmodel) architectural pattern과 유사함
  • 실습에서 사용할 앱은 separation of concerns 디자인 원칙을 따르며, 클래스로 나뉘어짐과 동시에 각  클래스는 개별 관심사를 해결함
  • 우리는 UI controller로 ViewModel과 ViewModelFactory를 이용할 것!

UI controller

  • UI controller는 Activity와 Fragment 같은 UI-based class를 말함
  • UI 컨트롤러는 UI 및 OS 상호작용(view를 보여주거나 user input을 캡처하는 것)을 다룰 수 있는 logic만 포함해야 함
  • 표시한 텍스트를 결정하는 것과 같은 의사 결정에 대한 logic은 UI 컨트롤러에 포함되면 안 됨!
  • 본 실습에서는 UI 컨트롤러로 GameFragment, ScoreFragment, TitleFragment 총 3개가 존재함
    • "separation of concerns" 디자인 원칙에 따라, GameFragment는 게임 요소를 화면에 그리고 유저가 버튼을 언제 탭하는지에 대해서만 맡고 있음
    • 사용자가 버튼을 탭하면 그 정보가 GameViewModel에 전달됨

ViewModel

  • ViewModel은 fragment 또는 activity에 표시되는 데이터를 가지고 있음
  • ViewModel은 데이터에 대해 간단한 계산과 변환을 수행하여 UI 컨트롤러에 의해 보여질 데이터를 준비함
  • 이 architecture에서 ViewModel은 의사 결정(decision-making)을 수행함

ViewModelFactory

  • ViewModelFactory는 ViewModel object를 생성자 파라미터에 관계업이 인스턴스화 함



1. GameViewModel 만들기

  • ViewModel 클래스는 UI와 관련된 데이터를 저장하고 관리하기 위해 설계됨
  • 이 실습 앱에서는 각각의 ViewModel은 1개의 fragment와 관련되어 있음
  • 지금은 GameFragment를 위해 GameViewModel을 추가할 것

STEP 1 : GameViewModel 클래스 추가

  • build.gradle(module:app)을 열고, dependencies 안에 ViewModel을 위한 Gradle 종속성을 추가하기
//ViewModel
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
  • screens/game 폴더 안에 새로운 Kotlin 클래스 만들기 - GameViewModel
  • GameViewModel 클래스가 ViewModel이라는 abstract class를 상속받기
  • ViewModel이 lifecycle-aware라는 것을 이해하기 쉽도록, init 블록을 만들고 log 생성
class GameViewModel : ViewModel() {
   init {
       Log.i("GameViewModel", "GameViewModel created!")
   }
}

STEP 2 : Override onCleared() and add logging

  • ViewModel은 관련된 fragment가 detached되거나 activity가 finish되면 destroyed됨
  • ViewModel이 파괴되기 직전에, onCleared() 콜백이 불려 리소스를 청소함
    • GameViewModel클래스에서 onCleared() 오버라이드 하기
    • 그 안에 로그문을 추가하여 GameViewModel lifecycle 추적
override fun onCleared() {
   super.onCleared()
   Log.i("GameViewModel", "GameViewModel destroyed!")
}

 

STEP 3 : Associate GameViewModel with the game fragment

  • ViewModel은 UI 컨트롤러와 연결되어야 함
  • 이 둘을 연결짓기 위해서는, UI 컨트롤러 안에서 ViewModel에 대한 reference를 생성해야 함
  • UI컨트롤러인 GameFragment 안에서 GameViewModel에 대한 레퍼런스를 만들 것!
    • GameFragment 안에서, class 변수로 GameViewModel 유형의 field 추가
private lateinit var viewModel: GameViewModel

STEP 4 : Initialize the ViewModel

  • 화면 회전 등으로 구성이 변경되는 동안, fragment와 같은 re-created 됨
  • 하지만 ViewModel 인스턴스는 살아있음!
  • ViewModel 클래스를 이용하여 ViewModel 인트턴스를 생성하면, fragment가 re-created될 때마다 새로운 객체가 생성됨
  • 위의 방법 대신 ViewModelProvider를 이용하여 ViewModel 인스턴스 만들기!

  • ViewModelProvider가 작동하는 방법
    • ViewModelProvider는 ViewModel이 존재하는 경우 그걸 반환하고, 그렇지 않다면 새로 생성하여 반환함
    • ViewModelProvider는 주어진 scope(activity 또는 fragment)와 관련된 ViewModel 인스턴스를 생성함
    • 생성된 ViewModel은 해당 scope가 살아있는 동안 유지됨
      • 만약 스코프가 fragment일 경우, ViewModel은 fragment가 detached될 때까지 유지됨
  • ViewModelProvider를 만들어주는 ViewModelProvider.get() 메소드를 사용하여 ViewModel 초기화하기
    • GameFragment클래스에서 viewModel 변수 초기화하기
      • onCreateView() 안에, binding 변수가 정의된 후에 이 코드 집어넣기
      • ViewModelProvider.get() 메소드를 사용할 때, 관련된 GameFragment context와 GameViewModel 클래스를 파라미터로 전달하기
      • 위의 ViewModel object에 대한 초기화를 하기 전에 ViewModelProvider.get() 메소드를 호출했음을 알 수 있는 록 추가하기
Log.i("GameFragment", "Called ViewModelProvider.get")
viewModel = ViewModelProvider(this).get(GameViewModel::class.java)
  • 로그를 확인하면, GameFragment의 onCreateView() 메소드는 GameViewModel을 생성하기 위해 ViewModelProvider.get()을 호출함을 알 수 있음
I/GameFragment: Called ViewModelProvider.get
I/GameViewModel: GameViewModel created!
  • 디바이스에서 자동회전을 활성화한 후 화면 방향을 여러 번 변경하면, GameFragment가 매번 destroyed되고 re-created됨을 알 수 있음 -> 매번 ViewModelProvider.get()이 호출됨
  • 하지만 GameViewModel은 오직 한 번만 생성되며, 매번 re-created되거나 destroyed되지 않음
I/GameFragment: Called ViewModelProvider.get
I/GameViewModel: GameViewModel created!
I/GameFragment: Called ViewModelProvider.get
I/GameFragment: Called ViewModelProvider.get
I/GameFragment: Called ViewModelProvider.get
  • 게임을 종료하거나 game fragment 밖으로 이동하면 GameFragment는 destroyed 됨.
  • 연결된 GameViewModel 도 파괴되어, onCleared() 를 호출함
I/GameFragment: Called ViewModelProvider.get
I/GameViewModel: GameViewModel created!
I/GameFragment: Called ViewModelProvider.get
I/GameFragment: Called ViewModelProvider.get
I/GameFragment: Called ViewModelProvider.get
I/GameViewModel: GameViewModel destroyed!

2. Populate the GameViewModel

  • ViewModel은 구성 변경에도 대응할 수 있기 때문에, 구성 변경에도 보존되어야 하는 데이터를 저장하기에 좋음
    • 화면에 표시될 데이터와 그 데이터를 다룰 코드를 ViewModel에 입력하기
    • ViewModel은 fragments, avtivities, views에 대한 레퍼런스는 포함하면 안 됨!! 걔네는 구성 변경 때 못 살아남음!!

  • ViewModel을 추가하기 전 GameFragment UI 데이터가 다뤄지는 방법
    • 화면 회전 등 구성 변경 시 game fragment는 파괴되고 재생성 됨
  • ViewModel을 추가하고 game fragment's UI 데이터를 ViewModel로 옮긴 후
    • fragment가 표시할 필요가 있는 모든 데이터는 이제 ViewModel임
    • 구성 변경이 일어나도 ViewModel은 살아남아 데이터가 보존됨

  • 이번에는 앱의 UI data와 그걸 처리하는 코드를 GameViewModel 클래스로 옮길 것! 구성 변경 중에도 데이터를 유지하기 위해!

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

  • GameFragment에서 GameViewModel로 data fields and methods 옮기기
    • word, score, wordList 데이터 필드를 옮기고 word와 score를 private이 아니게 하기
  • 바인딩 변수는 옮기면 안 됨! view에 대한 레퍼런스가 포함되어 있기 때문에!
    • 이 변수는 layout을 확장하고 클릭 리스너를 설정, 데이터를 화면에 보여주는 데 사용됨
  • resetList(), nextWord() 메소드는 옮기기
    • 화면에 표시할 단어를 결정함
  • onCreateView() 안에서 resetList()와 nextWord()를 호출하는 부분을 GameViewModel의 init 블록 안으로 옮기기
    • 이 메소드들은 반드시 init 블록 안에 있어야 함! fragment가 변하는 순간마다 초기화되는 것이 아닌 ViewModel이 생성되었을 때 wordList를 초기화해야 하기 때문에!
    • GameViewModel 안의 init 안에 있는 log 문은 삭제해도 됨
  • GameFragment 안의 onSkip() 과 onCorrect() 클릭핸들러는 데이터를 처리하고 UI를 업데이트하는 코드를 포함하고 있음
    • UI를 업데이트하기 위한 코드는 fragment에 남아 있어야 하지만 데이터 처리를 위한 코드는 ViewModel로 이동해야 함
  • 이제는 두 곳에 동일한 메소드를 둘 것
    • GameFragment의 onSkip() 과 onCorrect()를 복사하여 GameViewModel()에 붙이기
    • GameViewModel에서, onSkip과 onCorrect가 private이 아니도록 하기 -> 이 메소드들을 fragment에서 참조해야 하기 때문에!
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() {
       score--
       nextWord()
   }

   fun onCorrect() {
       score++
       nextWord()
   }

   override fun onCleared() {
       super.onCleared()
       Log.i("GameViewModel", "GameViewModel destroyed!")
   }
}
/**
* 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 ViewModelProvider.get")
       viewModel = ViewModelProvider(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() {
       score--
       nextWord()
   }

   private fun onCorrect() {
       score++
       nextWord()
   }


   /** Methods for updating the UI **/

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

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

STEP 2 : Update references to click handlers and data fields in GameFragment

  • GameFragment 안에서, onSkip()과 onCorrect() 메소드 업데이트하기
    • score를 업데이트하는 코드를 삭제한 후 ViewModel의 onSkip()과 onCorrect()를 부르는 것으로 대체
    • nextWord()를 ViewModel로 이동했기 때문에, game fragment는 더 이상 그것에 액세스할 수 없음
  • GameFragment의 onSkip과 onCorrect 안에서, nextWord()를 부르는 부분을 updateScoreText()와 updateWordText()를 부르는 것으로 대체
    • 화면에 데이터를 표시하기 위한 메소드
private fun onSkip() {
   viewModel.onSkip()
   updateWordText()
   updateScoreText()
}
private fun onCorrect() {
   viewModel.onCorrect()
   updateScoreText()
   updateWordText()
}
  • GameFragment에서, score과 word 변수를 GameViewModel의 변수를 사용하도록 업데이트 -> 걔네가 이제 GameViewModel에 있으니까!
private fun updateWordText() {
   binding.wordText.text = viewModel.word
}

private fun updateScoreText() {
   binding.scoreText.text = viewModel.score.toString()
}
  • GameViewModel의 nextWord() 안에서, updateWordText()와 updateScoreText() 호출 부분 삭제
    • 얘네는 GameFragment에서 호출되고 있음
  • 그리고 실행해서 오류 없는지 확인! 이제 앱의 모든 데이터가 ViewModel에 저장되며 따라서 구성 변경 중에도 유지 됨!

3. Implement click listener for the End Game button

게임 종료 버튼의 클릭리스너 구현하기

  • GameFragment 안에 onEndGame() 메소드 추가
    • EndGame 버튼을 누르면 이 메소드가 호출될 것
private fun onEndGame() {
   }
  • GameFragment의 onCreateView() 메소드 안에서, Got it 버튼과 Skip 버튼의 클릭리스너를 설정하는 코드 찾기
  • 그 두 줄 바로 아래에 End Game 버튼의 클릭리스너 설정 -> 바인딩 변수 사용하기
binding.endGameButton.setOnClickListener { onEndGame() }
  • GameFragment 안에 gameFinished() 메소드 추가
    • 앱을 score 화면으로 이동시키기 위해
    • Safe Args를 사용하여 score를 argument로 전달
/**
* Called when the game is finished
*/
private fun gameFinished() {
   Toast.makeText(activity, "Game has just finished", Toast.LENGTH_SHORT).show()
   val action = GameFragmentDirections.actionGameToScore()
   action.score = viewModel.score
   NavHostFragment.findNavController(this).navigate(action)
}
  • onEndGame() 메소드에서 gameFinished() 메소드 콜하기
private fun onEndGame() {
   gameFinished()
}
  • 앱 실행하면? 게임종료 버튼 누르면 앱이 점수 화면으로 이동하지만 최종 점수가 표시되지는 않음!

4. Use a ViewModelFactory

  • 사용자가 게임을 종료해도 ScoreFragment는 점수를 표시하지 않음
  • 우리는 ViewModel이 ScoreFragment에 표시하기 위한 score를 가지고 있기를 원함
  • 우리는 factory method pattern을 사용하여 ViewModel이 초기화되는 동안 score value를 전달할 수 있음
  • The factory method pattern?
    • objects를 생성하기 위해 factory 메소드를 사용하는 creational design pattern
    • factory method? 같은 클래스의 인스턴스를 반환하는 메소드
  • 여기서는 score fragment를 위한 파라미터화 된 생성자와 ViewModel을 설명하기 위한 factory method를 이용해 ViewModel을 만들 거임
    • score 패키지 아래에 새로운 코틀린 클래스 만들기 - ScoreViewModel
      • score fragment에 연결된 ViewModel
    • ScoreViewModel은 ViewModel을 상속받음
    • 최종 점수에 대한 파라미터 추가하고 init 안에 로그문 작성하기
    • ScoreViewModel 클래스 안에 score라는  변수 추가하기
      • 최종 점수 저장 위함
class ScoreViewModel(finalScore: Int) : ViewModel() {
   // The final score
   var score = finalScore
   init {
       Log.i("ScoreViewModel", "Final score is $finalScore")
   }
}
  • score 패키지 아래에, ScoreViewModelFactory 라는 코틀린 클래스 만들기
    • ScoreViewModel object에 대한 인스턴스화 담당
  • ScoreViewModelFactory는 ViewModelProvider.Factory 를 상속 받음
  • 최종 점수에 대한 파라미터 추가
class ScoreViewModelFactory(private val finalScore: Int) : ViewModelProvider.Factory {
}
  • ScoreViewModelFactory 안에서, Android Studio는 아직 구현되지 않은 추상 멤버에 대한 에러를 보여 줌
    • create() 메소드를 override하면 에러 해결 됨
    • create() 메소드는 새로 만들어진 ScoreViewModel object를 반환함
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
   if (modelClass.isAssignableFrom(ScoreViewModel::class.java)) {
       return ScoreViewModel(finalScore) as T
   }
   throw IllegalArgumentException("Unknown ViewModel class")
}
  • ScoreFragment 안에 ScoreViewModel과 ScoreViewModelFactory 클래스를 위한 변수 만들기
private lateinit var viewModel: ScoreViewModel
private lateinit var viewModelFactory: ScoreViewModelFactory
  • ScoreFragment 안의 onCreateView() 안에서, binding 변수를 초기화한 후 viewModelFactory 초기화하기
    • ScoreViewModelFactory 이용하기
    • bundle의 argument에서 얻은 final score를 ScoreViewModelFactory()의 파라미터로 전달하기
viewModelFactory = ScoreViewModelFactory(ScoreFragmentArgs.fromBundle(requireArguments()).score)
  • onCreateView() 안에서 viewModelFactory를 초기화 한 후, viewModel 객체 초기화하기
    • ViewModelProvider.get() 메소드 부르고 관련된 fragment인 score fragment의 context와 viewModelFactory를 파라미터로 전달하기
    • viewModelFactory클래스에 정의된 factory method를 사용하여 ScoreViewModel 오브젝트가 생성될 거임!
viewModel = ViewModelProvider(this, viewModelFactory)
       .get(ScoreViewModel::class.java)
  • onCreateView() 안에서 viewModel을 초기화 한 후, scoreText view의 텍스트를 ScoreViewModel에서 정의된 final score로 설정하기
binding.scoreText.text = viewModel.score.toString()
  • 앱 실행하고 확인하기!

 

https://codelabs.developers.google.com/codelabs/kotlin-android-training-view-model/index.html?index=..%2F..android-kotlin-fundamentals&hl=ko#0 

 

Comments