안드로이드 5주차 정리

김성준·2022년 2월 25일
0

안드로이드

목록 보기
6/16

Android Kotlin 기초의 내용을 번역하며 정리한 자료입니다.

App Architecture

앱 아키텍처는 코드가 구성되고 특정 시나리오에서 잘 수행되며 작업하기 쉽도록 앱의 클래스와 이들 간의 관계를 디자인하는 방법입니다. Android 앱 아키텍처는 MVVM(model-view-viewmodel) 아키텍처 패턴과 유사합니다.

GuessTheWord 앱은 관심사 분리 설계 원칙에 따라 클래스로 나뉘며 각 클래스는 별도의 관심사를 처리합니다. 이 강의의 첫 번째 코드랩에서 작업하는 클래스는 UI 컨트롤러, ViewModel 및 ViewModelFactory입니다.

UI controller

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

GuessTheWord 시작 코드에서 UI 컨트롤러는 GameFragment, ScoreFragment 및 TitleFragment의 세 조각입니다. "관심 분리" 디자인 원칙에 따라 GameFragment는 게임 요소를 화면에 그리고 사용자가 버튼을 탭할 때를 아는 것 외에는 아무 것도 하지 않습니다. 사용자가 버튼을 탭하면 이 정보가 GameViewModel에 전달됩니다.

ViewModel

ViewModel은 ViewModel과 관련된 Fragment 또는 Activity에 표시할 데이터를 보유합니다. ViewModel은 데이터에 대한 간단한 계산 및 변환을 수행하여 UI 컨트롤러에서 표시할 데이터를 준비할 수 있습니다. 이 아키텍처에서 ViewModel은 의사 결정을 수행합니다.

GameViewModel은 화면에 표시할 데이터이기 때문에 점수 값, 단어 목록 및 현재 단어와 같은 데이터를 보유합니다. GameViewModel에는 데이터의 현재 상태를 결정하기 위해 간단한 계산을 수행하는 비즈니스 로직도 포함되어 있습니다.

ViewModelFactory

ViewModelFactory는 생성자 매개변수가 있거나 없는 ViewModel 객체를 인스턴스화합니다.

1. Create GameViewModel

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

이 작업에서는 앱에 첫 번째 ViewModel인 GameFragment를 위한 GameViewModel을 추가합니다. 또한 ViewModel이 수명 주기를 인식한다는 것이 무엇을 의미하는지 배웁니다.

1.1. Add the GameViewModel class

1.1.1. build.gradle(module:app) 파일을 엽니다. dependency 블록 내에서 ViewModel에 대한 Gradle 종속성을 추가합니다.

implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'

1.1.2. 패키지 screens/game/ 폴더에서 GameViewModel이라는 새로운 Kotlin 클래스를 생성합니다.

1.1.3. GameViewModel 클래스가 추상 클래스 ViewModel을 확장하도록 합니다.

1.1.4. ViewModel이 수명 주기를 인식하는 방식을 더 잘 이해하는 데 도움이 되도록 log 문과 함께 init 블록을 추가하십시오.

class GameViewModel : ViewModel() {
   init {
       Log.i("GameViewModel", "GameViewModel created!")
   }
}

1.2. Override onCleared() and add logging

ViewModel은 연결된 Fragment가 분리되거나 Activity가 완료되면 소멸됩니다. ViewModel이 소멸되기 직전에 리소스를 정리하기 위해 onCleared() 콜백이 호출됩니다.

1.2.1. GameViewModel 클래스에서 onCleared() 메서드를 재정의합니다.

1.2.2. onCleared() 내부에 로그 문을 추가하여 GameViewModel 수명 주기를 추적합니다.

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

1.3. Associate GameViewModel with the game fragment

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

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

1.3.1. GameFragment 클래스에서 최상위 수준의 GameViewModel 유형 필드를 클래스 변수로 추가합니다.

private lateinit var viewModel: GameViewModel

1.4. Initialize the ViewModel

화면 회전과 같은 구성 변경 시 Fragment와 같은 UI 컨트롤러가 다시 생성됩니다. 그러나 ViewModel 인스턴스는 살아남습니다. ViewModel 클래스를 사용하여 ViewModel 인스턴스를 생성하면 Fragment가 다시 생성될 때마다 새 객체가 생성됩니다. 대신 ViewModelProvider를 사용하여 ViewModel 인스턴스를 만듭니다.

중요: ViewModel 인스턴스를 직접 인스턴스화하는 대신 항상 ViewModelProvider를 사용하여 ViewModel 개체를 생성하십시오.

ViewModelProvider 작동 방식:

  • ViewModelProvider는 존재하는 경우 기존 ViewModel을 반환하고, 이미 존재하지 않는 경우 새 ViewModel을 만듭니다.

  • ViewModelProvider는 주어진 범위(액티비티 또는 프래그먼트)와 관련된 ViewModel 인스턴스를 생성합니다.

  • 생성된 ViewModel은 액티비티 또는 프래그먼트가 살아있는 한 유지됩니다. 예를 들어 범위가 프래그먼트인 경우 ViewModel은 프래그먼트가 분리될 때까지 유지됩니다.

ViewModelProvider.get() 메서드를 사용하여 ViewModelProvider를 생성하여 ViewModel을 초기화합니다.

1.4.1. GameFragment 클래스에서 viewModel 변수를 초기화합니다. 바인딩 변수를 정의한 후 이 코드를 onCreateView() 안에 넣습니다. ViewModelProvider.get() 메서드를 사용하고 연결된 GameFragment 컨텍스트 및 GameViewModel 클래스를 전달합니다.

1.4.2. ViewModel 개체의 초기화 위에 ViewModelProvider.get() 메서드 호출을 기록하는 log 문을 추가합니다.

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

1.4.3. 앱을 실행합니다. Android Studio에서 Logcat 창을 열고 Game을 필터링합니다. 기기 또는 에뮬레이터에서 재생 버튼을 탭합니다. 게임 화면이 열립니다.

Logcat에서 볼 수 있듯이 GameFragment의 onCreateView() 메서드는 ViewModelProvider.get() 메서드를 호출하여 GameViewModel을 생성합니다. GameFragment 및 GameViewModel에 추가한 로깅 문은 Logcat에 표시됩니다.

I/GameFragment: Called ViewModelProvider.get
I/GameViewModel: GameViewModel created!

1.4.4. 장치 또는 에뮬레이터에서 자동 회전 설정을 활성화하고 화면 방향을 몇 번 변경합니다. GameFragment는 매번 소멸되고 재생성되므로 ViewModelProvider.get()이 매번 호출됩니다. 그러나 GameViewModel은 한 번만 생성되며 호출할 때마다 다시 생성되거나 소멸되지 않습니다.

I/GameFragment: Called ViewModelProvider.get
I/GameViewModel: GameViewModel created!
I/GameFragment: Called ViewModelProvider.get
I/GameFragment: Called ViewModelProvider.get
I/GameFragment: Called ViewModelProvider.get

1.4.5. 게임을 종료하거나 GameFragment 밖으로 탐색합니다. GameFragment가 파괴되었습니다. 연결된 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 GameViewModel

ViewModel은 구성 변경 사항을 유지하므로 구성 변경 사항을 유지해야 하는 데이터에 적합합니다.

  • ViewModel에 화면에 표시할 데이터와 해당 데이터를 처리할 코드를 넣습니다.
  • ViewModel은 프래그먼트, 액티비티 또는 뷰에 대한 참조를 포함해서는 안 됩니다. 액티비티, 프래그먼트 및 뷰는 구성 변경 후에도 유지되지 않기 때문입니다.

비교를 위해 다음은 ViewModel을 추가하기 전과 ViewModel을 추가한 후 앱에서 GameFragment UI 데이터를 처리하는 방법입니다.

  • ViewModel을 추가하기 전: 앱이 화면 회전과 같은 구성 변경을 겪을 때 GameFragment가 파괴되고 다시 생성됩니다. 데이터가 손실됩니다.

  • ViewModel을 추가하고 GameFragment의 UI 데이터를 ViewModel로 이동한 후: Fragment가 표시해야 하는 모든 데이터는 이제 ViewModel입니다. 앱이 구성 변경을 거치면 ViewModel이 유지되고 데이터가 유지됩니다.

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

2.1. Move data fields and data processing to the ViewModel

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

2.1.1. word, score, wordList 데이터 필드를 이동합니다. 단어와 점수가 private이 아닌지 확인하십시오.

경고 !
바인딩 변수 GameFragmentBinding에는 View에 대한 참조가 포함되어 있으므로 이동하지 마십시오. 이 변수는 레이아웃을 확장하고, 클릭 리스너를 설정하고, 화면에 데이터를 표시하는 데 사용됩니다(프래그먼트의 책임).

2.1.2. resetList() 및 nextWord() 메서드를 이동합니다. 이 메서드는 화면에 표시할 단어를 결정합니다.

2.1.3. onCreateView() 메서드 내부에서 resetList() 및 nextWord()에 대한 메서드 호출을 GameViewModel의 init 블록으로 이동합니다.

이러한 메서드는 init 블록에 있어야 합니다. 왜냐하면 Fragment가 생성될 때마다가 아니라 ViewModel이 생성될 때 단어 목록을 재설정해야 하기 때문입니다. GameViewModel의 init 블록에서 log 문을 삭제할 수 있습니다.

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

지금은 동일한 방법을 두 위치에 모두 배치합니다.

2.1.4. GameFragment에서 GameViewModel로 onSkip() 및 onCorrect() 메서드를 복사합니다.

2.1.5. GameViewModel에서 onSkip() 및 onCorrect() 메서드가 private이 아닌지 확인합니다. Fragment에서 이러한 메서드를 참조하기 때문입니다.

2.2. Update references to click handlers and data fields in GameFragment

2.2.1. GameFragment에서 onSkip() 및 onCorrect() 메서드를 업데이트합니다. 코드를 제거하여 점수를 업데이트하고 대신 viewModel에서 해당 onSkip() 및 onCorrect() 메서드를 호출합니다.

2.2.2. nextWord() 메서드를 ViewModel로 이동했기 때문에 게임 조각에서 더 이상 액세스할 수 없습니다.

GameFragment의 onSkip() 및 onCorrect() 메서드에서 nextWord()에 대한 호출을 updateScoreText() 및 updateWordText()로 바꿉니다. 이러한 메서드는 화면에 데이터를 표시합니다.

private fun onSkip() {
   viewModel.onSkip()
   updateWordText()
   updateScoreText()
}
private fun onCorrect() {
   viewModel.onCorrect()
   updateScoreText()
   updateWordText()
}

2.2.3. GameFragment에서 score 및 word 변수를 업데이트하여 GameViewModel 변수를 사용하십시오. 이러한 변수는 이제 GameViewModel에 있기 때문입니다.

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

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

[알림]

앱의 Activity, Fragment 및 View는 구성 변경 후에도 유지되지 않으므로 ViewModel에는 앱의 Activity, Fragment 또는 View에 대한 참조가 포함되어서는 안 됩니다.

2.2.4. GameViewModel의 nextWord() 메서드 내에서 updateWordText() 및 updateScoreText() 메서드에 대한 호출을 제거합니다. 이러한 메서드는 이제 GameFragment에서 호출됩니다.

2.2.5. 앱을 빌드하고 오류가 없는지 확인합니다. 오류가 있으면 프로젝트를 정리하고 다시 빌드하십시오.

2.2.6. 앱을 실행하고 단어를 통해 게임을 플레이하세요. 게임 화면에 있는 동안 장치를 회전합니다. 현재 점수와 현재 단어는 방향 변경 후에도 유지됩니다.

이제 앱의 모든 데이터가 ViewModel에 저장되므로 구성 변경 중에도 유지됩니다.

3. Implement click listener for the End Game button

이번 과제에서는 End Game button에 대한 클릭 리스너를 구현합니다.

3.1. GameFragment에서 onEndGame()이라는 메서드를 추가합니다. 사용자가 게임 종료 버튼을 탭하면 onEndGame() 메서드가 호출됩니다.

3.2. GameFragment의 onCreateView() 메서드 내에서 Got It 및 Skip 버튼에 대한 클릭 리스너를 설정하는 코드를 찾습니다. 이 두 줄 바로 아래에서 End Game 버튼에 대한 클릭 리스너를 설정합니다. 바인딩 변수 바인딩을 사용합니다. 클릭 리스너 내에서 onEndGame() 메서드를 호출합니다.

binding.endGameButton.setOnClickListener { onEndGame() }

3.3. 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)
}

3.4. onEndGame 메서드에서 gameFinished메서드를 호출합니다.

private fun onEndGame() {
   gameFinished()
}

3.5. 앱을 실행하고, 게임을 플레이하고, 몇 가지 단어를 순환합니다. 게임 종료 버튼을 탭합니다. 앱이 점수 화면으로 이동하지만 최종 점수는 표시되지 않습니다. 다음 작업에서 이 문제를 수정합니다.

4. Use a ViewModelFactory

사용자가 게임을 종료하면 ScoreFragment에 점수가 표시되지 않습니다. ScoreFragment에 의해 표시될 점수를 ViewModel이 보유해야합니다. 팩토리 메서드 패턴을 사용하여 ViewModel 초기화 중에 점수 값을 전달합니다.

팩토리 메소드 패턴은 팩토리 메소드를 사용하여 객체를 생성하는 생성 디자인 패턴입니다. 팩토리 메소드는 동일한 클래스의 인스턴스를 리턴하는 메소드입니다.

이 작업에서는 ScoreFragment에 대한 매개 변수화된 생성자와 ViewModel을 인스턴스화하는 팩토리 메서드를 사용하여 ViewModel을 만듭니다.

4.1. Score디렉토리 아래에서 ScoreViewModel이라는 새 Kotlin 클래스를 만듭니다. 이 클래스는 점수 조각에 대한 ViewModel이 됩니다.

4.2. ViewModel에서 ScoreViewModel 클래스를 확장합니다. 최종 점수에 대한 생성자 매개변수를 추가합니다. 로그 문으로 초기화 블록을 추가합니다.

4.3. ScoreViewModel 클래스에 score라는 변수를 추가하여 최종 점수를 저장합니다.

class ScoreViewModel(finalScore: Int) : ViewModel() {
   // The final score
   var score = finalScore
   init {
       Log.i("ScoreViewModel", "Final score is $finalScore")
   }
}

4.4. Score디렉토리 아래에 ScoreViewModelFactory라는 다른 Kotlin 클래스를 만듭니다. 이 클래스는 ScoreViewModel 개체의 인스턴스화를 담당합니다.

4.5. ViewModelProvider.Factory에서 ScoreViewModelFactory 클래스를 확장합니다. 최종 점수에 대한 생성자 매개변수를 추가합니다.

class ScoreViewModelFactory(private val finalScore: Int) : ViewModelProvider.Factory {
}

4.6. ScoreViewModelFactory에서 Android Studio는 구현되지 않은 추상 멤버에 대한 오류를 표시합니다. 오류를 해결하려면 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")
}

4.7. ScoreFragment에서 ScoreViewModel 및 ScoreViewModelFactory에 대한 클래스 변수를 만듭니다.

private lateinit var viewModel: ScoreViewModel
private lateinit var viewModelFactory: ScoreViewModelFactory

4.8. ScoreFragment에서 onCreateView() 내에서 바인딩 변수를 초기화한 후 viewModelFactory를 초기화합니다. ScoreViewModelFactory를 사용합니다. ScoreViewModelFactory()에 대한 생성자 매개변수로 인수 번들의 최종 점수를 전달합니다.

private lateinit var viewModel: ScoreViewModel
    private lateinit var viewModelFactory: ScoreViewModelFactory

4.9. onCreateView()에서 viewModelFactory 초기화 후 viewModel 객체를 초기화 합니다. ViewModelProvider.get() 메서드를 호출하고 연결된 점수 조각 컨텍스트 및 viewModelFactory를 전달합니다. viewModelFactory 클래스에 정의된 팩토리 메소드를 사용하여 ScoreViewModel 객체를 생성합니다.

viewModel = ViewModelProvider(this, viewModelFactory)
       .get(ScoreViewModel::class.java)

4.10. onCreateView() 메소드에서 viewModel을 초기화한 후 ScoreText 뷰의 텍스트를 ScoreViewModel에 정의된 최종 스코어로 설정한다.

binding.scoreText.text = viewModel.score.toString()

4.11. 앱을 실행하고 게임을 플레이하세요. 일부 또는 전체 단어를 순환하고 게임 종료를 탭합니다. 이제 ScoreFragment에 최종 점수가 표시됩니다.

4.12. ScoreViewModel에서 필터링하여 Logcat에서 ScoreViewModel 로그를 확인합니다. 점수 값이 표시되어야 합니다.

[참고]

이 앱에서는 점수를 viewModel.score 변수에 직접 할당할 수 있으므로 ScoreViewModel에 대한 ViewModelFactory를 추가할 필요가 없습니다. 그러나 때때로 viewModel이 초기화될 때 바로 데이터가 필요합니다.

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

Summary

  • Android 앱 아키텍처 가이드라인은 책임이 다른 클래스를 분리할 것을 권장합니다.

  • UI 컨트롤러는 Activity 또는 Fragment와 같은 UI 기반 클래스입니다. UI 컨트롤러에는 UI 및 운영 체제 상호 작용을 처리하는 논리만 포함되어야 합니다. UI에 표시할 데이터를 포함해서는 안 됩니다. 해당 데이터를 ViewModel에 넣습니다.

  • ViewModel 클래스는 UI 관련 데이터를 저장하고 관리합니다. ViewModel 클래스를 사용하면 화면 회전과 같은 구성 변경 후에도 데이터를 유지할 수 있습니다.

  • ViewModel은 권장되는 Android 아키텍처 구성 요소 중 하나입니다.

  • ViewModelProvider.Factory는 ViewModel 개체를 만드는 데 사용할 수 있는 인터페이스입니다.

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

UI controllerViewModel
UI 컨트롤러의 예는 ScoreFragment입니다.ViewModel의 예는 ScoreViewModel입니다.
UI에 표시할 데이터가 없습니다.UI 컨트롤러가 UI에 표시하는 데이터를 포함합니다.
데이터를 표시하기 위한 코드와 클릭 리스너와 같은 사용자 이벤트 코드를 포함합니다.데이터 처리를 위한 코드를 포함합니다.
구성이 변경될 때마다 삭제 및 재생성됩니다.연결된 UI 컨트롤러가 영구적으로 사라지는 경우에만 소멸됩니다. 액티비티의 경우 액티비티가 완료될 때 또는 프래그먼트의 경우 프래그먼트가 분리될 때 소멸됩니다.
View를 포함합니다.액티비티, 프래그먼트 또는 뷰에 대한 참조를 포함해서는 안 됩니다. 왜냐하면 액티비티, 프래그먼트, 뷰는 구성 변경에서 살아남지 못하지만 ViewModel은 유지하기 때문입니다.
연결된 ViewModel에 대한 참조를 포함합니다.연결된 UI 컨트롤러에 대한 참조를 포함하지 않습니다.
profile
수신제가치국평천하

0개의 댓글