안드로이드 5주차 정리(2)

김성준·2022년 2월 25일
0

안드로이드

목록 보기
7/16

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

LiveData

LiveData는 수명 주기를 인식하는 관찰 가능한 데이터 홀더 클래스입니다. 예를 들어, GuessTheWord 앱에서 현재 점수 주위에 LiveData를 래핑할 수 있습니다. 여기서는 LiveData의 몇 가지 특성에 대해 알아봅니다.

  • LiveData는 관찰 가능합니다. 즉, LiveData 개체가 보유한 데이터가 변경될 때 Observer에게 알림이 전송됩니다.

  • LiveData는 데이터를 보유합니다. LiveData는 모든 데이터와 함께 사용할 수 있는 래퍼입니다.

  • LiveData는 수명 주기를 인식합니다. LiveData에 Observer를 연결하면 Observer는 LifecycleOwner(일반적으로 Activity 또는 Fragment)와 연결됩니다. LiveData는 STARTED 또는 RESUMED와 같은 활성 수명 주기 상태에 있는 Observer만 업데이트합니다. 여기에서 LiveData 및 Observe에 대해 자세히 알아볼 수 있습니다.

이 작업에서는 GameViewModel의 현재 점수 및 현재 단어 데이터를 LiveData로 변환하여 모든 데이터 유형을 LiveData 개체로 래핑하는 방법을 배웁니다. 이후 작업에서 이러한 LiveData 개체에 Observer를 추가하고 LiveData를 관찰하는 방법을 배웁니다.

1.Add LiveData to the GameViewModel

1.1. Change the score and word to use LiveData

1.1.1. screens/game 패키지에서 GameViewModel 파일을 엽니다.

1.1.2. 변수 score 및 word의 유형을 MutableLiveData로 변경하십시오.

var word = MutableLiveData<String>()
var score = MutableLiveData<Int>()

1.1.3. GameViewModel에서 init 블록 내에서 점수와 단어를 초기화합니다. LiveData 변수의 값을 변경하려면 변수에 대해 setValue() 메서드를 사용합니다. Kotlin에서는 value 속성을 사용하여 setValue()를 호출할 수 있습니다.

init {

   word.value = ""
   score.value = 0
  ...
}

1.2. Update the LiveData object reference

score 및 word 변수는 이제 LiveData 유형입니다. 이 단계에서는 value 속성을 사용하여 이러한 변수에 대한 참조를 변경합니다.

1.2.1. GameViewModel의 onSkip() 메서드에서 score를 score.value로 변경합니다. 점수가 null일 수 있다는 오류에 유의하십시오. 다음에 이 오류를 수정합니다.

1.2.2. 오류를 해결하려면 onSkip()의 score.value에 null 검사를 추가하십시오. 그런 다음 null 안전으로 빼기를 수행하는 점수에 대해 마이너스() 함수를 호출합니다.

fun onSkip() {
   score.value = (score.value)?.minus(1)
   nextWord()
}

1.2.3. 같은 방식으로 onCorrect() 메서드를 업데이트합니다. 점수 변수에 null 검사를 추가하고 plus() 함수를 사용합니다.

fun onCorrect() {
   score.value = (score.value)?.plus(1)
   nextWord()
}

1.2.4. GameViewModel의 nextWord() 메서드 내에서 단어 참조를 word.value로 변경합니다.

private fun nextWord() {
   if (!wordList.isEmpty()) {
       //Select and remove a word from the list
       word.value = wordList.removeAt(0)
   }
}

1.2.5. GameFragment의 updateWordText() 메서드 내에서 viewModel.word에 대한 참조를 viewModel.word.value로 변경합니다.

/** Methods for updating the UI **/
private fun updateWordText() {
   binding.wordText.text = viewModel.word.value
}

1.2.6. GameFragment의 updateScoreText() 메서드 내에서 viewModel.score에 대한 참조를 viewModel.score.value로 변경합니다.

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

1.2.7. GameFragment의 gameFinished() 메서드 내에서 viewModel.score에 대한 참조를 viewModel.score.value로 변경합니다. 필수 널 안전 검사를 추가하십시오.

private fun gameFinished() {
   Toast.makeText(activity, "Game has just finished", Toast.LENGTH_SHORT).show()
   val action = GameFragmentDirections.actionGameToScore()
   action.score = viewModel.score.value?:0
   NavHostFragment.findNavController(this).navigate(action)
}

1.2.8. 코드에 오류가 없는지 확인하십시오. 앱을 컴파일하고 실행합니다. 앱의 기능은 이전과 동일해야 합니다.

2. Attach observers to the LiveData objects

이 작업은 score와 word 데이터를 LiveData 개체로 변환한 이전 작업과 밀접하게 관련되어 있습니다. 이 작업에서는 Observer 개체를 해당 LiveData 개체에 연결합니다. fragment view(viewLifecycleOwner)를 LifecycleOwner로 사용합니다.

viewLifecycleOwner를 사용하는 이유는 무엇입니까?

프래그먼트 자체가 파괴되지 않더라도 사용자가 프래그먼트에서 멀어지면 프래그먼트 뷰가 파괴됩니다. 이것은 본질적으로 프래그먼트의 라이프사이클과 프래그먼트 뷰의 라이프사이클이라는 두 가지 라이프사이클을 생성합니다. 프래그먼트 뷰의 수명 주기 대신 프래그먼트의 수명 주기를 참조하면 프래그먼트 뷰를 업데이트할 때 미묘한 버그가 발생할 수 있습니다.

2.1. GameFragment의 onCreateView() 메서드 내에서 현재 점수인 viewModel.score에 대한 LiveData 개체에 Observer 개체를 연결합니다. observe() 메서드를 사용하고 viewModel 초기화 후 코드를 넣습니다. 람다 식을 사용하여 코드를 단순화합니다. (람다 식은 선언되지 않았지만 즉시 식으로 전달되는 익명 함수입니다.)

viewModel.score.observe(viewLifecycleOwner, Observer { newScore ->
})

2.2. 방금 만든 Observer는 관찰된 LiveData 개체가 보유한 데이터가 변경될 때 이벤트를 수신합니다. Observer 내에서 새 점수로 TextView 점수를 업데이트합니다.

viewModel.score.observe(viewLifecycleOwner, Observer { newScore -> binding.scoreText.text = newScore.toString()
})

2.3. Observer 개체를 현재 단어 LiveData 개체에 연결합니다. Observer 개체를 현재 점수에 연결한 것과 같은 방식으로 수행합니다.

/** Setting up LiveData observation relationship **/
viewModel.word.observe(viewLifecycleOwner, Observer { newWord ->
   binding.wordText.text = newWord
})

점수 또는 단어의 값이 변경되면 화면에 표시되는 점수 또는 단어가 자동으로 업데이트됩니다.

2.4. GameFragment에서 updateWordText() 및 updateScoreText() 메서드와 이에 대한 모든 참조를 삭제합니다. TextView가 LiveData 관찰자 메서드에 의해 업데이트되기 때문에 더 이상 필요하지 않습니다.

2.5. 앱을 실행합니다. 게임 앱은 이전과 동일하게 작동해야 하지만 이제 LiveData 및 LiveData 관찰자를 사용합니다.

3. Encapsulate the LiveData

캡슐화는 일부 개체 필드에 대한 직접 액세스를 제한하는 방법입니다. 개체를 캡슐화하면 개인 내부 필드를 수정하는 공용 메서드 집합이 노출됩니다. 캡슐화를 사용하여 다른 클래스가 이러한 내부 필드를 조작하는 방법을 제어합니다.

현재 코드에서 모든 외부 클래스는 value 속성을 사용하여 점수 및 단어 변수를 수정할 수 있습니다(예: viewModel.score.value 사용). 이 코드랩에서 개발 중인 앱에서는 중요하지 않을 수 있지만 프로덕션 앱에서는 ViewModel 개체의 데이터를 제어하려고 합니다.

ViewModel만 앱의 데이터를 편집해야 합니다. 그러나 UI 컨트롤러는 데이터를 읽어야 하므로 데이터 필드가 완전히 비공개일 수는 없습니다. 앱의 데이터를 캡슐화하려면 MutableLiveData 및 LiveData 개체를 모두 사용합니다.

MutableLiveData vs. LiveData:

  • MutableLiveData 개체의 데이터는 이름에서 알 수 있듯이 변경할 수 있습니다. ViewModel 내부에서 데이터는 편집 가능해야 하므로 MutableLiveData를 사용합니다.
  • LiveData 개체의 데이터는 읽을 수 있지만 변경할 수는 없습니다. ViewModel 외부에서 데이터는 읽을 수 있지만 편집할 수 없어야 하므로 데이터가 LiveData로 노출되어야 합니다.

이 전략을 수행하려면 Kotlin 지원 속성을 사용합니다. 지원 속성을 사용하면 정확한 객체가 아닌 다른 getter에서 무언가를 반환할 수 있습니다. 이 작업에서 당신은 guessTheWord 앱에서 점수와 단어 개체에 대한 지원 속성을 구현합니다.

3.1. Add a backing property to score and word

3.1.1. GameViewModel에서 현재 점수 개체를 비공개로 만듭니다.

3.1.2. 지원 속성에 사용된 명명 규칙을 따르려면 score를 _score로 변경합니다. _score 속성은 이제 내부적으로 사용되는 게임 점수의 변경 가능한 버전입니다.

3.1.3. score라는 LiveData 유형의 공개 버전을 작성하십시오.

// The current score
private val _score = MutableLiveData<Int>()
val score: LiveData<Int>

3.1.4. 초기화 오류가 표시됩니다. 이 오류는 GameFragment 내부에서 점수가 LiveData 참조이고 점수가 더 이상 해당 setter에 액세스할 수 없기 때문에 발생합니다. Kotlin의 getter 및 setter에 대해 자세히 알아보려면 Getter 및 Setter를 참조하세요.

오류를 해결하려면 GameViewModel에서 점수 개체에 대한 get() 메서드를 재정의하고 지원 속성인 _score를 반환합니다.

val score: LiveData<Int>
   get() = _score

3.1.5. GameViewModel에서 score 참조를 내부 변경 가능한 버전인 _score로 변경합니다.

init {
   ...
   _score.value = 0
   ...
}

...
fun onSkip() {
   _score.value = (score.value)?.minus(1)
  ...
}

fun onCorrect() {
   _score.value = (score.value)?.plus(1)
   ...
}

3.1.6. word 개체의 이름을 _word로 바꾸고 score 개체에 대해 수행한 것처럼 백업 속성을 추가합니다.

// The current word
private val _word = MutableLiveData<String>()
val word: LiveData<String>
   get() = _word
...
init {
   _word.value = ""
   ...
}
...
private fun nextWord() {
   if (!wordList.isEmpty()) {
       //Select and remove a word from the list
       _word.value = wordList.removeAt(0)
   }
}

LiveData 개체 word와 score를 캡슐화했습니다.

4. Add a game-finished event

사용자가 게임 종료 버튼을 탭하면 현재 앱이 점수 화면으로 이동합니다. 또한 플레이어가 모든 단어를 순환했을 때 앱이 점수 화면으로 이동하기를 원합니다. 플레이어가 마지막 단어를 끝낸 후 사용자가 버튼을 탭할 필요가 없도록 게임이 자동으로 종료되기를 원합니다.

이 기능을 구현하려면 모든 단어가 표시되었을 때 이벤트가 트리거되고 ViewModel에서 Fragment로 전달되어야 합니다. 이를 위해 LiveData Observer 패턴을 사용하여 게임 종료 이벤트를 모델링합니다.

The observer pattern

observer pattern은 소프트웨어 디자인 패턴입니다. 관찰 대상(관찰의 "주제")과 관찰자 간의 통신을 지정합니다. 옵저버블은 옵저버에게 상태 변화를 알리는 객체입니다.

이 앱의 LiveData의 경우 옵저버블(subject)은 LiveData 객체이고 옵저버는 프래그먼트와 같은 UI 컨트롤러의 메서드입니다. 상태 변경은 LiveData 내부에 래핑된 데이터가 변경될 때마다 발생합니다. LiveData 클래스는 ViewModel에서 Fragment로 통신하는 데 중요합니다.

4.1. LiveData를 사용하여 게임 종료 이벤트 감지

이 작업에서는 LiveData 관찰자 패턴을 사용하여 게임 종료 이벤트를 모델링합니다.

4.1.1. GameViewModel에서 _eventGameFinish라는 부울 MutableLiveData 개체를 만듭니다. 이 개체는 게임 종료 이벤트를 보유합니다.

4.1.2. _eventGameFinish 개체를 초기화한 후 eventGameFinish라는 지원 속성을 만들고 초기화합니다.

// Event which triggers the end of the game
private val _eventGameFinish = MutableLiveData<Boolean>()
val eventGameFinish: LiveData<Boolean>
   get() = _eventGameFinish

4.1.3. GameViewModel에서 onGameFinish() 메서드를 추가합니다. 메서드에서 게임 종료 이벤트인 eventGameFinish를 true로 설정합니다.

/** Method for the game completed event **/
fun onGameFinish() {
   _eventGameFinish.value = true
}

4.1.4. GameViewModel에서 nextWord() 메서드 내에서 단어 목록이 비어 있으면 게임을 종료합니다.

private fun nextWord() {
   if (wordList.isEmpty()) {
       onGameFinish()
   } else {
       //Select and remove a _word from the list
       _word.value = wordList.removeAt(0)
   }
}

4.1.5. GameFragment의 onCreateView() 내부에서 viewModel을 초기화한 후 eventGameFinish에 옵저버를 연결합니다. 관찰() 메서드를 사용합니다. 람다 함수 내에서 gameFinished() 메서드를 호출합니다.

4.1.6. 앱을 실행하고 게임을 플레이하고 모든 단어를 살펴보세요. 앱은 게임 종료를 탭할 때까지 GameFragment에 머무르지 않고 자동으로 점수 화면으로 이동합니다.

단어 목록이 비어 있으면 eventGameFinish가 설정되고 GameFragment의 관련 관찰자 메서드가 호출되며 앱은 TitleFragment로 이동합니다.

4.1.7. 추가한 코드로 인해 수명 주기 문제가 발생했습니다. 이 문제를 이해하려면 GameFragment 클래스에서 gameFinished() 메서드의 탐색 코드를 주석 처리하십시오. 메서드에서 Toast 메시지를 유지해야 합니다.

private fun gameFinished() {
       Toast.makeText(activity, "Game has just finished", Toast.LENGTH_SHORT).show()
//        val action = GameFragmentDirections.actionGameToScore()
//        action.score = viewModel.score.value?:0
//        NavHostFragment.findNavController(this).navigate(action)
   }

4.1.8. 앱을 실행하고 게임을 플레이하고 모든 단어를 살펴보세요. "게임이 막 종료되었습니다"라는 토스트 메시지가 게임 화면 하단에 잠시 나타납니다. 이는 예상된 동작입니다.

이제 장치 또는 에뮬레이터를 회전합니다. 토스트가 다시 등장! 기기를 몇 번 더 돌리면 매번 토스트가 표시될 것입니다. 토스트는 게임이 끝나면 한 번만 표시되어야 하기 때문에 이것은 버그입니다. 프래그먼트가 다시 생성될 때마다 알림이 표시되지 않아야 합니다. 다음 작업에서 이 문제를 해결합니다.

4.2. Reset the game-finished event

일반적으로 LiveData는 데이터가 변경될 때만 관찰자에게 업데이트를 전달합니다. 이 동작의 예외는 관찰자가 비활성 상태에서 활성 상태로 변경될 때 관찰자도 업데이트를 수신한다는 것입니다.

이것이 게임 종료 토스트가 앱에서 반복적으로 실행되는 이유입니다. 게임 프래그먼트가 화면 회전 후 다시 생성되면 비활성 상태에서 활성 상태로 이동합니다. 프래그먼트의 옵저버는 기존 ViewModel에 다시 연결되어 현재 데이터를 수신합니다. gameFinished() 메서드가 다시 트리거되고 알림이 표시됩니다.

이 작업에서는 GameViewModel에서 eventGameFinish 플래그를 재설정하여 이 문제를 수정하고 토스트를 한 번만 표시합니다.

4.2.1. GameViewModel에서 onGameFinishComplete() 메서드를 추가하여 게임 종료 이벤트인 _eventGameFinish를 재설정합니다.

/** Method for the game completed event **/

fun onGameFinishComplete() {
   _eventGameFinish.value = false
}

4.2.2. GameFragment에서 gameFinished()가 끝날 때 viewModel 개체에서 onGameFinishComplete()를 호출합니다. (지금은 gameFinished()의 탐색 코드를 주석 처리한 상태로 두십시오.)

private fun gameFinished() {
   ...
   viewModel.onGameFinishComplete()
}

4.2.3. 앱을 실행하고 게임을 플레이하세요. 모든 단어를 살펴본 다음 장치의 화면 방향을 변경합니다. 토스트는 한 번만 표시됩니다.

4.2.4. GameFragment의 gameFinished() 메서드 내에서 탐색 코드의 주석 처리를 제거합니다.

private fun gameFinished() {
   Toast.makeText(activity, "Game has just finished", Toast.LENGTH_SHORT).show()
   val action = GameFragmentDirections.actionGameToScore()
   action.score = viewModel.score.value?:0
   findNavController(this).navigate(action)
   viewModel.onGameFinishComplete()
}

4.2.5. 앱을 실행하고 게임을 플레이하세요. 모든 단어를 통과한 후 앱이 자동으로 최종 점수 화면으로 이동하는지 확인합니다.

앱은 LiveData를 사용하여 게임 종료 이벤트를 트리거하여 GameViewModel에서 단어 목록이 비어 있음을 GameFragment으로 전달합니다. 그런 다음 GameFragment은 ScoreFragment으로 이동합니다.

5. Add LiveData to the ScoreViewModel

이 작업에서는 ScoreViewModel에서 score를 LiveData 개체로 변경하고 관찰자를 연결합니다. 이 작업은 LiveData를 GameViewModel에 추가할 때 수행한 작업과 유사합니다.

앱의 모든 데이터가 LiveData를 사용하도록 하기 위해 ScoreViewModel을 변경합니다.

5.1. ScoreViewModel에서 점수 변수 유형을 MutableLiveData로 변경합니다. 규칙에 따라 이름을 _score로 바꾸고 지원 속성을 추가합니다.

private val _score = MutableLiveData<Int>()
val score: LiveData<Int>
   get() = _score

5.2. ScoreViewModel의 블록 내에서 _score를 초기화합니다. init 블록에서 원하는 대로 솜씨를 발휘하세요.

init {
   _score.value = finalScore
}

5.3. ScoreFragment의 onCreateView() 내부에서 viewModel을 초기화한 후 스코어 LiveData 객체에 대한 옵저버를 연결합니다. 람다 식 내에서 점수 값을 점수 텍스트 보기로 설정합니다. ViewModel에서 점수 값으로 텍스트 보기를 직접 할당하는 코드를 제거합니다.

// Add observer for score
viewModel.score.observe(viewLifecycleOwner, Observer { newScore ->
   binding.scoreText.text = newScore.toString()
})
// remove code
binding.scoreText.text = viewModel.score.toString()

5.4. 앱을 실행하고 게임을 플레이하세요. 앱은 이전과 같이 작동해야 하지만 이제 LiveData와 관찰자를 사용하여 점수를 업데이트합니다.

6. Add the Play Again button

이 작업에서는 점수 화면에 Play Again 버튼을 추가하고 LiveData 이벤트를 사용하여 clickListener를 구현합니다. 버튼은 점수 화면에서 게임 화면으로 이동하는 이벤트를 트리거합니다.

6.1. res/layout/score_fragment.xml에서 play_again_button 버튼의 가시성 속성 값을 visible로 변경합니다.

<Button
   android:id="@+id/play_again_button"
...
   android:visibility="visible"
 />

6.2. ScoreViewModel에서 LiveData 개체를 추가하여 _eventPlayAgain이라는 부울을 보유합니다. 이 개체는 스코어 화면에서 게임 화면으로 이동하기 위해 LiveData 이벤트를 저장하는 데 사용됩니다.

private val _eventPlayAgain = MutableLiveData<Boolean>()
val eventPlayAgain: LiveData<Boolean>
   get() = _eventPlayAgain

6.3. ScoreViewModel에서 _eventPlayAgain 이벤트를 설정하고 재설정하는 메서드를 정의합니다.

fun onPlayAgain() {
   _eventPlayAgain.value = true
}
fun onPlayAgainComplete() {
   _eventPlayAgain.value = false
}

6.4. ScoreFragment에서 eventPlayAgain에 대한 관찰자를 추가합니다. return 문 앞의 onCreateView() 끝에 코드를 넣습니다. 람다 식 내에서 게임 화면으로 돌아가서 eventPlayAgain을 재설정합니다.

// Navigates back to game when button is pressed
viewModel.eventPlayAgain.observe(viewLifecycleOwner, Observer { playAgain ->
   if (playAgain) {
      findNavController().navigate(ScoreFragmentDirections.actionRestart())
       viewModel.onPlayAgainComplete()
   }
})

6.5. ScoreFragment의 onCreateView() 내부에서 PlayAgain 버튼에 클릭 리스너를 추가하고 viewModel.onPlayAgain()을 호출합니다.

binding.playAgainButton.setOnClickListener {  viewModel.onPlayAgain()  }

6.6. 앱을 실행하고 게임을 플레이하세요. 게임이 끝나면 점수 화면에 최종 점수와 PlayAgain 버튼이 표시됩니다. PlayAgain 버튼을 탭하면 앱이 GameFragment으로 이동하여 게임을 다시 플레이할 수 있습니다.

Summary

LiveData

  • LiveData는 Android 아키텍처 구성 요소 중 하나인 수명 주기를 인식하는 관찰 가능한 데이터 홀더 클래스입니다.

  • LiveData를 사용하여 데이터가 업데이트될 때 UI가 자동으로 업데이트되도록 할 수 있습니다.

  • LiveData는 관찰 가능합니다. 즉, LiveData 개체가 보유하고 있는 데이터가 변경될 때 Activity나 Fragment와 같은 관찰자에게 알릴 수 있습니다.

  • LiveData는 데이터를 보유합니다. 모든 데이터와 함께 사용할 수 있는 래퍼입니다.

  • LiveData는 수명 주기를 인식하므로 STARTED 또는 RESUMED와 같은 활성 수명 주기 상태에 있는 관찰자만 업데이트합니다.

To add LiveData

  • ViewModel의 데이터 변수 유형을 LiveData 또는 MutableLiveData로 변경합니다.

    • MutableLiveData는 값을 변경할 수 있는 LiveData 객체입니다. MutableLiveData는 일반 클래스이므로 보유하는 데이터 유형을 지정해야 합니다.
  • LiveData가 보유한 데이터의 값을 변경하려면 LiveData 변수에서 setValue() 메소드를 사용하십시오.

To encapsulate LiveData

  • ViewModel 내부의 LiveData는 편집 가능해야 합니다. ViewModel 외부에서 LiveData를 읽을 수 있어야 합니다. 이것은 Kotlin 지원 속성을 사용하여 구현할 수 있습니다.

  • Kotlin 지원 속성을 사용하면 정확한 객체가 아닌 getter에서 무언가를 반환할 수 있습니다.

  • LiveData를 캡슐화하려면 ViewModel 내부에서 비공개 MutableLiveData를 사용하고 ViewModel 외부에서 LiveData 지원 속성을 반환합니다.

Observable LiveData

  • LiveData는 관찰자 패턴을 따릅니다. "Observable"은 LiveData 개체이고 관찰자는 Fragment 같은 UI 컨트롤러의 메서드입니다. LiveData 내부에 래핑된 데이터가 변경될 때마다 UI 컨트롤러의 관찰자 메서드에 알림이 전송됩니다.

  • LiveData를 Observable하게 만들려면 관찰자(예: 액티비티 및 프래그먼트) 메서드를 사용하여 관찰자의 LiveData 참조에 관찰자 객체를 연결합니다.

  • 이 LiveData 관찰자 패턴은 ViewModel에서 UI 컨트롤러로 통신하는 데 사용할 수 있습니다.

profile
수신제가치국평천하

0개의 댓글