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. GuessTheWord 시작 코드를 다운로드하고 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

screens/title/TitleFragment.kt

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

screens/game/GameFragment.kt

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

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

screens/score/ScoreFragment.kt

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

res/navigation/main_navigation.xml

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

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

이 작업에서는 GuessTheWord 시작 앱의 문제를 찾습니다.

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

앱의 문제:

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

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

앱 아키텍처

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

GuessTheWord 앱은 관심사 분리 디자인 원칙을 따르며, 각 클래스가 별도의 문제를 처리하는 클래스로 나뉩니다. 이 강의의 첫 번째 Codelab에서는 UI 컨트롤러, ViewModel, ViewModelFactory 클래스를 사용합니다.

UI 컨트롤러

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

GuessTheWord 시작 코드에서 UI 컨트롤러는 GameFragment, ScoreFragment,, TitleFragment의 세 프래그먼트입니다. '관심사 분리' 설계 원칙에 따라 GameFragment는 게임 요소를 화면에 그리고 사용자가 버튼을 탭하는 시점을 아는 것만 담당합니다. 사용자가 버튼을 탭하면 이 정보가 GameViewModel에 전달됩니다.

ViewModel

ViewModelViewModel과 연결된 프래그먼트 또는 활동에 표시할 데이터를 보유하고, 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로 필터링합니다. 기기 또는 에뮬레이터에서 재생 버튼을 탭합니다. 게임 화면이 열립니다.

    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를 추가하기 전과 후에 스타터 앱에서 GameFragment UI 데이터가 처리되는 방식은 다음과 같습니다.ViewModel

  • 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 블록으로 이동합니다.

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

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

지금은 동일한 메서드를 두 위치에 모두 넣습니다.

  1. GameFragment에서 GameViewModelonSkip()onCorrect() 메서드를 복사합니다.
  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. GameFragment에서 scoreword 변수를 GameViewModel 변수를 사용하도록 업데이트합니다. 이러한 변수는 이제 GameViewModel에 있기 때문입니다.
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 컨트롤러와 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 방문 페이지를 참고하세요.