Android Kotlin 기초 05.1: ViewModel 및 ViewModelFactory

이 Codelab은 Android Kotlin 기초 교육 과정의 일부입니다. Codelab을 순서대로 진행한다면 이 과정을 통해 최대한의 가치를 얻을 수 있을 것입니다. 모든 과정 Codelab은 Android Kotlin 기초 Codelab 방문 페이지에 나열되어 있습니다.

타이틀 스크린

게임 화면

점수 화면

소개

이 Codelab에서는 Android 아키텍처 구성요소 중 하나인 ViewModel에 관해 알아봅니다.

  • ViewModel 클래스를 사용하여 수명 주기를 고려하여 UI 관련 데이터를 저장하고 관리합니다. ViewModel 클래스를 사용하면 화면 회전 및 키보드 가용성 변경과 같은 기기 설정 변경 시에도 데이터를 유지할 수 있습니다.
  • ViewModelFactory 클래스를 사용하여 구성 변경 후에도 유지되는 ViewModel 객체를 인스턴스화하고 반환합니다.

기본 요건

  • Kotlin으로 기본 Android 앱을 만드는 방법
  • 탐색 그래프를 사용하여 앱에서 탐색을 구현하는 방법
  • 앱의 대상 간을 이동하는 코드를 추가하고 탐색 대상 간에 데이터를 전달하는 방법
  • 활동 및 프래그먼트 수명 주기 작동 방식
  • Android 스튜디오에서 Logcat을 사용하여 앱에 로깅 정보를 추가하고 로그를 읽는 방법

학습할 내용

  • 권장되는 Android 앱 아키텍처 사용 방법
  • 앱에서 Lifecycle, ViewModel, ViewModelFactory 클래스를 사용하는 방법
  • 기기 설정 변경을 통해 UI 데이터를 유지하는 방법
  • 팩토리 디자인 패턴의 정의와 사용 방법입니다.
  • ViewModelProvider.Factory 인터페이스를 사용하여 ViewModel 객체를 만드는 방법

실습할 내용

  • ViewModel를 앱에 추가하여 앱 변경 시에도 데이터가 유지되도록 앱을 저장합니다.
  • ViewModelFactory 및 팩토리 메서드 디자인 패턴을 사용하여 생성자 매개변수로 ViewModel 객체를 인스턴스화합니다.

과정 5 Codelab에서는 시작 코드로 시작하여 GuessTheWord 앱을 개발합니다. GuessTheWord는 플레이어들이 가능한 최고 점수를 달성하기 위해 협력하는 2인용 차데드 스타일의 게임입니다.

첫 번째 플레이어는 앱의 단어를 보고 순서대로 하나씩 플레이하며 두 번째 플레이어에게 단어를 표시하지 않도록 합니다. 두 번째 플레이어는 단어를 추측하려고 합니다.

게임을 플레이하려면 첫 번째 플레이어가 기기에서 앱을 열면 아래 스크린샷과 같이 단어가 표시됩니다.

첫 번째 플레이어는 단어를 소리 내서 말하지만, 실제로 그 단어를 말하지는 않습니다.

  • 두 번째 플레이어가 단어를 올바르게 추측하면 첫 번째 플레이어가 확인 버튼을 눌러 숫자를 1씩 늘리고 다음 단어를 표시합니다.
  • 두 번째 플레이어가 단어를 추측할 수 없으면 첫 번째 플레이어가 건너뛰기 버튼을 누릅니다. 이 경우 숫자가 1씩 감소하고 다음 단어로 건너뜁니다.
  • 게임을 종료하려면 게임 종료 버튼을 누릅니다. (이 기능은 시리즈의 첫 번째 Codelab 시작 코드에는 없습니다.)

이 작업에서는 시작 앱을 다운로드하고 실행한 후 코드를 검사합니다.

1단계: 시작하기

  1. GessTheWord 시작 코드를 다운로드하고 Android 스튜디오에서 프로젝트를 엽니다.
  2. Android 지원 기기 또는 에뮬레이터에서 앱을 실행합니다.
  3. 버튼을 탭하세요. Skip 버튼을 누르면 다음 단어가 표시되고 점수가 1씩 줄어드는 경우, Got It 버튼을 누르면 다음 단어가 표시되고 점수가 1씩 증가합니다. 게임 종료 버튼은 구현되지 않으므로 버튼을 탭해도 아무 일도 일어나지 않습니다.

2단계: 코드 둘러보기

  1. Android 스튜디오에서 코드를 살펴보고 앱의 작동 방식을 알아봅니다.
  2. 아래에 설명된 파일, 특히 중요한 파일을 살펴보세요.

MainActivity.kt

이 파일에는 템플릿으로 생성된 기본 코드만 포함됩니다.

res/layout/main_activity.xml

이 파일에는 앱의 기본 레이아웃이 포함되어 있습니다. NavHostFragment는 사용자가 앱을 탐색할 때 다른 프래그먼트를 호스팅합니다.

UI 프래그먼트

시작 코드에는 com.example.android.guesstheword.screens 패키지 아래의 세 가지 패키지에 있는 세 개의 프래그먼트가 있습니다.

  • 제목 화면의 title/TitleFragment
  • 게임 화면: game/GameFragment
  • 점수 화면 score/ScoreFragment

screen/title/TitleFragment.kt

제목 프래그먼트는 앱이 실행될 때 표시되는 첫 번째 화면입니다. 클릭 핸들러를 Play 버튼으로 설정하여 게임 화면으로 이동합니다.

screen/game/GameFragment.kt

이 작업은 대부분의 게임 작업이 발생하는 기본 프래그먼트입니다.

  • 변수는 현재 단어와 현재 점수에 대해 정의됩니다.
  • resetList() 메서드 내에서 정의된 wordList는 게임에 사용할 단어의 샘플 목록입니다.
  • onSkip() 메서드는 Skip 버튼의 클릭 핸들러입니다. 점수가 1씩 낮아진 후 nextWord() 메서드를 사용하여 다음 단어를 표시합니다.
  • onCorrect() 메서드는 Got It 버튼의 클릭 핸들러입니다. 이 메서드는 onSkip() 메서드와 유사하게 구현됩니다. 유일한 차이점은 이 메서드가 감산 대신 1을 점수에 추가한다는 것입니다.

screen/score/ScoreFragment.kt

ScoreFragment는 게임의 최종 화면이며 플레이어의 최종 점수를 표시합니다. 이 Codelab에서는 이 화면을 표시하고 최종 점수를 표시하는 구현을 추가합니다.

res/navigation/main_navigation.xml

탐색 그래프는 프래그먼트가 탐색을 통해 연결되는 방식을 보여줍니다.

  • 제목 프래그먼트에서 사용자가 게임 프래그먼트로 이동할 수 있습니다.
  • 게임 프래그먼트에서 사용자가 점수 프래그먼트로 이동할 수 있습니다.
  • 점수 프래그먼트에서 사용자는 게임 프래그먼트로 다시 이동할 수 있습니다.

이 작업에서는 GuessTheWord 시작 앱에 문제가 있습니다.

  1. 시작 코드를 실행하고 몇 단어로 게임을 플레이하고 각 단어 뒤에 Skip 또는 Got It을 탭합니다.
  2. 이제 게임 화면에 단어와 현재 점수가 표시됩니다. 기기나 에뮬레이터를 회전하여 화면 방향을 변경하면 현재 점수가 소멸됩니다.
  3. 게임을 더 많이 말해 보세요. 게임 화면이 점수와 함께 표시되면 앱을 닫았다가 다시 엽니다. 앱 상태가 저장되지 않으므로 게임이 처음부터 다시 시작됩니다.
  4. 단어 몇 개로 게임을 플레이한 다음 게임 종료 버튼을 탭합니다. 아무 일도 일어나지 않습니다.

앱 문제:

  • 시작 시 앱은 기기 방향이 변경되거나 앱이 종료되었다가 다시 시작될 때와 같이 구성이 변경되는 동안 앱 상태를 저장하고 복원하지 않습니다.
    onSaveInstanceState() 콜백을 사용하여 이 문제를 해결할 수 있습니다. 하지만 onSaveInstanceState() 메서드를 사용하려면 번들에 상태를 저장하는 추가 코드를 작성하고 이 상태를 검색하는 로직을 구현해야 합니다. 또한 저장할 수 있는 데이터의 양은 최소한입니다.
  • 사용자가 게임 종료 버튼을 탭해도 게임 화면이 점수 화면으로 이동하지 않습니다.

이 Codelab에서 알아보는 앱 아키텍처 구성요소를 사용하여 이 문제를 해결할 수 있습니다.

앱 아키텍처

앱 아키텍처는 코드를 구성하고, 특정 시나리오에서 잘 작동하고, 작업하기 쉽도록 앱과 클래스 간의 관계를 설계하는 방법입니다. 네 가지 Codelab 조합에서 GuessTheWord 앱의 개선사항은 Android 앱 아키텍처 가이드라인을 따르며 Android 아키텍처 구성요소를 사용합니다. Android 앱 아키텍처는 MVVM(모델 뷰 보기 모델) 아키텍처 패턴과 유사합니다.

GuessTheWord 앱은 관심사 분리 디자인 원칙을 준수하며 클래스로 나뉘어 있으며, 각 클래스에서 별도의 우려사항을 다룹니다. 이 과정의 첫 번째 Codelab에서 사용하는 클래스는 UI 컨트롤러, ViewModel, ViewModelFactory입니다.

UI 컨트롤러

UI 컨트롤러Activity 또는 Fragment와 같은 UI 기반 클래스입니다. UI 컨트롤러에는 뷰 표시 및 사용자 입력 캡처와 같은 UI 및 운영체제 상호작용을 처리하는 로직만 포함해야 합니다. 표시할 텍스트를 결정하는 로직과 같은 의사 결정 로직을 UI 컨트롤러에 배치하지 마세요.

GessTheWord 시작 코드에서 UI 컨트롤러는 세 가지 프래그먼트(GameFragment, ScoreFragment,, TitleFragment)입니다. 관심사 분리 디자인 원칙에 따라 GameFragment은 화면에 게임 요소를 그리고 사용자가 버튼을 탭하는 시점만 인지하고 다른 것에는 책임을 지지 않습니다. 사용자가 버튼을 탭하면 이 정보가 GameViewModel에 전달됩니다.

ViewModel

ViewModel에는 ViewModel와 연결된 프래그먼트 또는 활동에 표시할 데이터가 포함됩니다. ViewModel는 간단한 데이터 계산 및 변환을 실행하여 UI 컨트롤러에서 표시할 데이터를 준비할 수 있습니다. 이 아키텍처에서 ViewModel는 의사 결정을 실행합니다.

GameViewModel는 화면에 표시되는 데이터이므로 점수 값, 단어 목록, 현재 단어와 같은 데이터를 보유합니다. GameViewModel에는 간단한 계산을 실행하여 데이터의 현재 상태를 확인하는 비즈니스 로직도 포함되어 있습니다.

ViewModelFactory

ViewModelFactory는 생성자 매개변수의 유무와 상관없이 ViewModel 객체를 인스턴스화합니다.

이후 Codelab에서는 UI 컨트롤러 및 ViewModel와 관련된 다른 Android 아키텍처 구성요소에 관해 알아봅니다.

ViewModel 클래스는 UI 관련 데이터를 저장하고 관리하도록 설계되었습니다. 이 앱에서 각 ViewModel는 하나의 프래그먼트와 연결됩니다.

이 작업에서는 앱에 첫 번째 ViewModelGameFragmentGameViewModel를 추가합니다. 또한 ViewModel가 수명 주기를 인식한다는 것이 어떤 의미인지 알아봅니다.

1단계: GameViewModel 클래스 추가

  1. build.gradle(module:app) 파일을 엽니다. dependencies 블록 내부에 ViewModel의 Gradle 종속 항목을 추가합니다.

    라이브러리의 최신 버전을 사용하는 경우 솔루션 앱이 예상대로 컴파일됩니다. 문제가 해결되지 않으면 문제 해결을 시도하거나 아래에 표시된 버전으로 되돌리세요.
//ViewModel
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
  1. 패키지 screens/game/ 폴더에서 GameViewModel라는 새 Kotlin 클래스를 만듭니다.
  2. GameViewModel 클래스가 추상 클래스 ViewModel를 확장하도록 합니다.
  3. ViewModel가 수명 주기를 인식하는 방법을 더 잘 이해하려면 log 문으로 init 블록을 추가하세요.
class GameViewModel : ViewModel() {
   init {
       Log.i("GameViewModel", "GameViewModel created!")
   }
}

2단계: onCleared() 재정의 및 로깅 추가

ViewModel은 연결된 프래그먼트가 분리되거나 활동이 완료되면 소멸됩니다. ViewModel이 소멸되기 직전에 onCleared() 콜백이 호출되어 리소스를 정리합니다.

  1. GameViewModel 클래스에서 onCleared() 메서드를 재정의합니다.
  2. GameViewModel 수명 주기를 추적하도록 onCleared() 내에 로그 구문을 추가합니다.
override fun onCleared() {
   super.onCleared()
   Log.i("GameViewModel", "GameViewModel destroyed!")
}

3단계: GameViewModel을 게임 프래그먼트와 연결

ViewModel를 UI 컨트롤러와 연결해야 합니다. 이 둘을 연결하려면 UI 컨트롤러 내에 ViewModel에 대한 참조를 만듭니다.

이 단계에서는 해당하는 UI 컨트롤러(GameFragment) 내에 GameViewModel의 참조를 만듭니다.

  1. GameFragment 클래스의 최상위 수준에서 GameViewModel 유형의 필드를 클래스 변수로 추가합니다.
private lateinit var viewModel: GameViewModel

4단계: ViewModel 초기화

화면 회전과 같은 구성 변경 중에 프래그먼트와 같은 UI 컨트롤러가 다시 생성됩니다. 하지만 ViewModel 인스턴스는 유지됩니다. ViewModel 클래스를 사용하여 ViewModel 인스턴스를 만들면 프래그먼트가 다시 생성될 때마다 새 객체가 생성됩니다. 대신 ViewModelProvider를 사용하여 ViewModel 인스턴스를 만드세요.

ViewModelProvider의 작동 원리

  • ViewModelProvider은 기존 ViewModel가 있으면 이를 반환하거나 기존 ViewModel이 없으면 새 ViewModel을 생성합니다.
  • ViewModelProvider는 지정된 범위 (활동 또는 프래그먼트)와 관련하여 ViewModel 인스턴스를 생성합니다.
  • 생성된 ViewModel는 범위가 활성화되어 있는 동안 유지됩니다. 예를 들어 범위가 프래그먼트이면 프래그먼트가 분리될 때까지 ViewModel가 유지됩니다.

ViewModelProviders.of() 메서드를 사용하여 ViewModelProvider를 만드는 ViewModel를 초기화합니다.

  1. GameFragment 클래스에서 viewModel 변수를 초기화합니다. 결합 변수를 정의한 후 이 코드를 onCreateView() 내부에 배치합니다. ViewModelProviders.of() 메서드를 사용하여 연결된 GameFragment 컨텍스트 및 GameViewModel 클래스를 전달합니다.
  2. ViewModel 객체의 초기화 위에 ViewModelProviders.of() 메서드 호출을 로깅하는 로그 구문을 추가합니다.
Log.i("GameFragment", "Called ViewModelProviders.of")
viewModel = ViewModelProviders.of(this).get(GameViewModel::class.java)
  1. 앱을 실행합니다. Android 스튜디오에서 Logcat 창을 열고 Game로 필터링합니다. 기기 또는 에뮬레이터에서 Play 버튼을 탭합니다. 게임 화면이 열립니다.

    Logcat에서 보여주는 것처럼 GameFragmentonCreateView() 메서드는 ViewModelProviders.of() 메서드를 호출하여 GameViewModel를 만듭니다. GameFragmentGameViewModel에 추가한 로깅 문이 Logcat에 표시됩니다.

  1. 기기나 에뮬레이터에서 자동 회전 설정을 켜고 화면 방향을 몇 번 바꿉니다. GameFragment는 매번 소멸되고 다시 생성되므로 매번 ViewModelProviders.of()가 호출됩니다. 하지만 GameViewModel는 한 번만 생성되며 매번 호출별로 다시 생성되거나 소멸되지 않습니다.
I/GameFragment: Called ViewModelProviders.of
I/GameViewModel: GameViewModel created!
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
  1. 게임을 종료하거나 게임 프래그먼트 외부로 이동합니다. GameFragment가 소멸됩니다. 연결된 GameViewModel도 함께 소멸되며 콜백 onCleared()가 호출됩니다.
I/GameFragment: Called ViewModelProviders.of
I/GameViewModel: GameViewModel created!
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
I/GameViewModel: GameViewModel destroyed!

ViewModel는 구성 변경 후에도 유지되므로 구성 변경 이후에도 유지되어야 하는 데이터에 적합합니다.

  • 화면에 표시할 데이터와 데이터를 처리하는 코드를 ViewModel에 배치합니다.
  • ViewModel은 프래그먼트, 활동 또는 뷰에 대한 참조를 포함해서는 안 됩니다. 활동, 프래그먼트 및 뷰가 구성 변경에도 유지되지 않기 때문입니다.

참고로 ViewModel를 추가하기 전과 ViewModel를 추가한 후 시작 앱에서 GameFragment UI 데이터가 처리되는 방식은 다음과 같습니다.

  • ViewModel 추가 전:
    앱이 화면 회전과 같은 구성 변경을 진행하면 게임 프래그먼트가 소멸되고 다시 생성됩니다. 데이터가 사라집니다.
  • ViewModel을 추가하고 게임 프래그먼트의 UI 데이터를 ViewModel로 옮긴 후에는
    프래그먼트가 표시해야 하는 모든 데이터가 이제 ViewModel이 됩니다. 앱에서 구성 변경을 거치면 ViewModel는 유지되며 데이터가 유지됩니다.

이 작업에서는 앱의 UI 데이터와 데이터를 처리하는 메서드를 GameViewModel 클래스로 이동합니다. 이렇게 하면 구성 변경 중에 데이터가 보존됩니다.

1단계: 데이터 필드 및 데이터 처리를 ViewModel로 이동

다음 데이터 필드와 메서드를 GameFragment에서 GameViewModel로 이동합니다.

  1. word, score, wordList 데이터 필드를 이동합니다. wordscoreprivate이 아니어야 합니다.

    결합 변수 GameFragmentBinding는 뷰에 관한 참조를 포함하고 있으므로 이동하지 마세요. 이 변수는 레이아웃을 확장하고 클릭 리스너를 설정하며 화면에 프래그먼트의 책임을 표시하는 데이터를 표시하는 데 사용됩니다.
  2. resetList()nextWord() 메서드를 이동합니다. 이러한 메서드는 화면에 표시할 단어를 결정합니다.
  3. onCreateView() 메서드 내에서 resetList()nextWord()의 메서드 호출을 GameViewModelinit 블록으로 이동합니다.

    이러한 메서드는 프래그먼트가 생성될 때마다가 아니라 ViewModel가 생성될 때 단어 목록을 재설정해야 하므로 init 블록에 있어야 합니다. GameFragmentinit 블록에서 로그 구문을 삭제할 수 있습니다.

GameFragmentonSkip()onCorrect() 클릭 핸들러에는 데이터를 처리하고 UI를 업데이트하는 코드가 포함되어 있습니다. UI를 업데이트하는 코드는 프래그먼트에 남아 있어야 하지만, 데이터 처리를 위한 코드는 ViewModel로 이동해야 합니다.

지금은 두 위치에 동일한 메서드를 배치합니다.

  1. onSkip()onCorrect() 메서드를 GameFragment에서 GameViewModel로 복사합니다.
  2. GameViewModel에서 onSkip()onCorrect() 메서드가 private가 아닌지 확인합니다. 이러한 메서드가 프래그먼트에서 참조되기 때문입니다.

다음은 리팩터링 후 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()
   }
}

2단계: GameFragment의 클릭 핸들러와 데이터 필드에 대한 참조 업데이트

  1. GameFragment에서 onSkip()onCorrect() 메서드를 업데이트합니다. 코드를 삭제하여 점수를 업데이트하고 대신 viewModel에서 상응하는 onSkip()onCorrect() 메서드를 호출하세요.
  2. nextWord() 메서드를 ViewModel로 이동했으므로 게임 프래그먼트에서 더 이상 액세스할 수 없습니다.

    GameFragmentonSkip()onCorrect() 메서드에서 nextWord() 호출을 updateScoreText()updateWordText()로 바꿉니다. 이러한 메서드는 화면에 데이터를 표시합니다.
private fun onSkip() {
   viewModel.onSkip()
   updateWordText()
   updateScoreText()
}
private fun onCorrect() {
   viewModel.onCorrect()
   updateScoreText()
   updateWordText()
}
  1. scoreword 변수가 GameViewModel에 있으므로 GameViewModel 변수를 사용하도록 GameFragment 변수를 업데이트합니다.
private fun updateWordText() {
   binding.wordText.text = viewModel.word
}

private fun updateScoreText() {
   binding.scoreText.text = viewModel.score.toString()
}
  1. GameViewModelnextWord() 메서드 내에서 updateWordText()updateScoreText() 메서드 호출을 삭제합니다. 이제 이러한 메서드는 GameFragment에서 호출됩니다.
  2. 앱을 빌드하고 오류가 없는지 확인합니다. 오류가 있으면 프로젝트를 정리하고 다시 빌드합니다.
  3. 앱을 실행하고 몇 단어를 플레이해 게임을 플레이합니다. 게임 화면에서 기기를 회전합니다. 방향을 변경한 후에도 현재 점수와 현재 단어가 그대로 유지됩니다.

잘하셨습니다. 이제 모든 앱 데이터가 ViewModel에 저장되므로 구성이 변경되는 동안 보관됩니다.

이 작업에서는 End Game 버튼의 클릭 리스너를 구현합니다.

  1. GameFragment에서 onEndGame()라는 메서드를 추가합니다. 사용자가 게임 종료 버튼을 탭하면 onEndGame() 메서드가 호출됩니다.
private fun onEndGame() {
   }
  1. GameFragmentonCreateView() 메서드 내에서 Got ItSkip 버튼의 클릭 리스너를 설정하는 코드를 찾습니다. 이 두 줄 바로 아래에 End Game 버튼의 클릭 리스너를 설정합니다. 결합 변수 binding을 사용합니다. 클릭 리스너 내에서 onEndGame() 메서드를 호출합니다.
binding.endGameButton.setOnClickListener { onEndGame() }
  1. GameFragment에서 앱을 점수 화면으로 이동하는 gameFinished()라는 메서드를 추가합니다. Safe Args를 사용하여 점수를 인수로 전달합니다.
/**
* 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)
}
  1. onEndGame() 메서드에서 gameFinished() 메서드를 호출합니다.
private fun onEndGame() {
   gameFinished()
}
  1. 앱을 실행하고 게임을 플레이하며 단어를 넘겨봅니다. 게임 종료 버튼을 탭합니다. 앱이 점수 화면으로 이동하지만 최종 점수는 표시되지 않습니다. 다음 작업에서 이 문제를 해결합니다.

사용자가 게임을 종료하면 ScoreFragment에 점수가 표시되지 않습니다. ScoreFragment가 표시할 점수를 ViewModel에 유지하려고 합니다. ViewModel 초기화 중에 팩토리 패턴 패턴을 사용하여 점수 값을 전달합니다.

팩토리 메서드 패턴은 팩토리 메서드를 사용하여 객체를 만드는 재생성 설계 패턴입니다. 팩토리 메서드는 동일한 클래스의 인스턴스를 반환하는 메서드입니다.

이 작업에서는 점수 프래그먼트의 매개변수화된 생성자와 ViewModel를 인스턴스화하는 팩토리 메서드를 사용하여 ViewModel를 만듭니다.

  1. score 패키지에서 ScoreViewModel라는 새 Kotlin 클래스를 만듭니다. 이 클래스는 점수 프래그먼트의 ViewModel이 됩니다.
  2. ViewModel.에서 ScoreViewModel 클래스를 확장합니다. 최종 점수의 생성자 매개변수를 추가합니다. 로그 구문이 포함된 init 블록을 추가합니다.
  3. ScoreViewModel 클래스에서 최종 점수를 저장할 score이라는 변수를 추가합니다.
class ScoreViewModel(finalScore: Int) : ViewModel() {
   // The final score
   var score = finalScore
   init {
       Log.i("ScoreViewModel", "Final score is $finalScore")
   }
}
  1. score 패키지에서 ScoreViewModelFactory라는 또 다른 Kotlin 클래스를 만듭니다. 이 클래스는 ScoreViewModel 객체를 인스턴스화합니다.
  2. ViewModelProvider.Factory에서 ScoreViewModelFactory 클래스를 확장합니다. 최종 점수의 생성자 매개변수를 추가합니다.
class ScoreViewModelFactory(private val finalScore: Int) : ViewModelProvider.Factory {
}
  1. ScoreViewModelFactory에서 Android 스튜디오가 구현되지 않은 추상 멤버에 관한 오류를 표시합니다. 오류를 해결하려면 create() 메서드를 재정의합니다. create() 메서드에서 새로 구성된 ScoreViewModel 객체를 반환합니다.
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")
}
  1. ScoreFragment에서 ScoreViewModelScoreViewModelFactory의 클래스 변수를 만듭니다.
private lateinit var viewModel: ScoreViewModel
private lateinit var viewModelFactory: ScoreViewModelFactory
  1. ScoreFragmentonCreateView() 내에서 binding 변수를 초기화한 후 viewModelFactory을 초기화합니다. ScoreViewModelFactory. 인수 번들의 최종 점수를 ScoreViewModelFactory()에 생성자 매개변수로 전달합니다.
viewModelFactory = ScoreViewModelFactory(ScoreFragmentArgs.fromBundle(arguments!!).score)
  1. onCreateView(에서 viewModelFactory를 초기화한 후 viewModel 객체를 초기화합니다. ViewModelProviders.of() 메서드를 호출하고 연결된 점수 프래그먼트 컨텍스트와 viewModelFactory을 전달합니다. 그러면 viewModelFactory 클래스에 정의된 팩토리 메서드를 사용하여 ScoreViewModel 객체가 생성됩니다..
viewModel = ViewModelProviders.of(this, viewModelFactory)
       .get(ScoreViewModel::class.java)
  1. onCreateView() 메서드에서 viewModel를 초기화한 후 scoreText 뷰의 텍스트를 ScoreViewModel에 정의된 최종 점수로 설정합니다.
binding.scoreText.text = viewModel.score.toString()
  1. 앱을 실행하고 게임을 플레이합니다. 전체 단어 중 일부 또는 전체를 순환하고 게임 종료를 탭합니다. 이제 점수 프래그먼트에 최종 점수가 표시됩니다.

  1. 선택사항: ScoreViewModel에서 필터링하여 Logcat의 ScoreViewModel 로그를 확인합니다. 점수 값이 표시됩니다.
2019-02-07 10:50:18.328 com.example.android.guesstheword I/ScoreViewModel: Final score is 15

이 작업에서는 ViewModel를 사용하도록 ScoreFragment를 구현했습니다. 또한 ViewModelFactory 인터페이스를 사용하여 ViewModel의 매개변수화된 생성자를 만드는 방법도 배웠습니다.

수고하셨습니다. Android 아키텍처 구성요소 중 하나인 ViewModel를 사용하도록 앱의 아키텍처를 변경했습니다. 앱의 수명 주기 문제를 해결했으며 이제 구성 변경 후에도 게임의 데이터가 유지됩니다. 또한 ViewModelFactory 인터페이스를 사용하여 ViewModel를 만들기 위한 매개변수화된 생성자를 만드는 방법도 배웠습니다.

Android 스튜디오 프로젝트: GuessTheWord

  • Android 앱 아키텍처 가이드라인에서는 책임이 서로 다른 클래스를 분리하도록 권장합니다.
  • UI 컨트롤러Activity 또는 Fragment와 같은 UI 기반 클래스입니다. UI 컨트롤러에는 UI 및 운영체제 상호작용을 처리하는 로직만 포함해야 합니다. UI에 표시할 데이터는 포함되지 않아야 합니다. 이 데이터를 ViewModel에 넣습니다.
  • ViewModel 클래스는 UI 관련 데이터를 저장하고 관리합니다. ViewModel 클래스를 사용하면 화면 회전과 같이 구성을 변경할 때도 데이터를 유지할 수 있습니다.
  • ViewModel은 권장되는 Android 아키텍처 구성요소 중 하나입니다.
  • ViewModelProvider.FactoryViewModel 객체를 만드는 데 사용할 수 있는 인터페이스입니다.

아래 표는 UI 컨트롤러와 데이터를 보유하고 있는 ViewModel 인스턴스를 비교합니다.

UI 컨트롤러

ViewModel

UI 컨트롤러의 예는 이 Codelab에서 만든 ScoreFragment입니다.

ViewModel의 예는 이 Codelab에서 만든 ScoreViewModel입니다.

UI에 표시할 데이터를 포함하지 않습니다.

UI 컨트롤러에 UI에 표시되는 데이터가 포함됩니다.

데이터 표시를 위한 코드 및 클릭 리스너와 같은 사용자 이벤트 코드를 포함합니다.

데이터 처리를 위한 코드가 포함되어 있습니다.

구성을 변경할 때마다 소멸되고 다시 생성됩니다.

관련 UI 컨트롤러가 영구적으로 사라지는 경우에만(활동, 활동이 완료될 때 또는 프래그먼트에서 프래그먼트가 분리될 때) 폐기됩니다.

뷰가 포함되어 있습니다.

구성 변경에도 유지되지 않으므로 활동, 프래그먼트 또는 뷰에 대한 참조를 포함해서는 안 되지만 ViewModel는 참조하기 때문입니다.

연결된 ViewModel의 참조를 포함합니다.

연결된 UI 컨트롤러에 대한 참조를 포함하지 않습니다.

Udacity 과정:

Android 개발자 문서:

기타:

이 섹션에는 강사가 진행하는 과정의 일부로 이 Codelab을 통해 작업하는 학생들의 숙제 과제가 나와 있습니다. 강사는 다음을 처리합니다.

  • 필요한 경우 과제를 할당합니다.
  • 학생에게 과제 과제를 제출하는 방법을 알려주세요.
  • 과제 과제를 채점합니다.

강사는 이러한 추천을 원하는 만큼 사용할 수 있으며 다른 적절한 숙제를 할당해도 좋습니다.

이 Codelab을 직접 학습하고 있다면 언제든지 숙제를 통해 지식을 확인해 보세요.

답변

질문 1

기기 설정 변경 중에 데이터가 손실되지 않도록 하려면 어떤 클래스에 앱 데이터를 저장해야 하나요?

  • ViewModel
  • LiveData
  • Fragment
  • Activity

질문 2

ViewModel는 프래그먼트, 활동 또는 뷰에 대한 참조를 포함해서는 안 됩니다. 참 또는 거짓 중 선택하세요.

  • 거짓

질문 3

ViewModel는 언제 소멸되나요?

  • 기기 방향이 변경되는 동안 관련 UI 컨트롤러가 소멸되고 다시 생성될 때
  • 방향이 변경됩니다.
  • 관련 UI 컨트롤러가 완료되거나(활동인 경우) 분리될 때(프래그먼트인 경우)
  • 사용자가 뒤로 버튼을 누를 때

질문 4

ViewModelFactory 인터페이스의 용도

  • ViewModel 객체를 인스턴스화합니다.
  • 방향이 변경되는 동안 데이터를 보존합니다.
  • 화면에 표시되는 데이터를 새로고침하는 중입니다.
  • 앱 데이터 변경 시 알림 수신

다음 강의 시작: 5.2: LiveData 및 LiveData 관찰자

이 과정의 다른 Codelab 링크는 Android Kotlin 기초 Codelab 방문 페이지를 참고하세요.