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

LiveData 및 LiveData Observer 본문

Android/Android Kotlin 기초 실습 정리

LiveData 및 LiveData Observer

EOJIN 2022. 10. 27. 01:32

Live Data

  • LiveData는 lifecycle을 판단해서 관찰할 수 있는 data holder class
  • 특징
    • LiveData는 관찰할 수 있음 : LiveData에 있는 data가 변경되었을 때 observer가 통지될 수 있음 
    • data를 저장함 : LiveData는 모든 데이터와 함께 사용할 수 있는 wrapper
    • 생명주기를 인식함 : observer를 LiveData에 접속시키는 경우, 옵저버는 LifecycleOwner(보통 activity나 fragment)와 연결됨
      • LiveData는 오직 STRTED나 RESUMED와 같은 active lifecycle 상태에 있는 옵저버만 업데이트 함

1. Add LiveData to the GameViewModel

  • 여기서는 GameViewModel에 있는 current score와 current word data를 LiveData로 변환하여 모든 데이터 유형을 LiveData object로 wrap하는 방법을 배울 것
  • 다음으로는 이 LiveData object에 observer를 추가하여 LiveData를 관찰하는 방법을 배울 것

STEP 1 : Change the score and word to use LiveData

  • screens/game 아래의 GameViewModel 파일 열기
  • score와 word의 변수 유형을 MutableLiveData 로 변경
  • MutableLiveData
    • 값이 변경될 수 있는 LiveData
    • generic class이므로 보유할 데티어 타입을 지정해야 함
// The current word
val word = MutableLiveData<String>()
// The current score
val score = MutableLiveData<Int>()
  • GameViewModel의 init 블록 안에서, score와 word 초기화하기
  • LiveData 변수의 값을 변경하려면, setValue() 메소드를 사용해야 함
  • 코틀린에서는 value 속성을 이용하여 setValue()를 호출할 수 있음
init {

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

STEP 2 : Update the LiveData object reference

  • 변수 score와 word는 현재 LiveData 타입임
  • 여기서는 value 속성을 사용하여 이 변수들에 대한 레퍼런스를 변경할 것

  • GameViewModel 안의 onSkip()메소드에서, score를 score.value 로 바꾸기 -> score이 null일 수도 있는 에러에 주의
  • 오류를 해결하려면 null check를 onSkip의 score.value에 추가해야 함
  • 그리고 score에서 minus() 함수 호출하기
    • null-safety인 변수에 뺄셈을 수행하는 함수
fun onSkip() {
   score.value = (score.value)?.minus(1)
   nextWord()
}
  • onCorrect() 메소드도 같은 방법으로 업데이트 : score에 null-check 추가하고 plus() 함수 호출
fun onCorrect() {
   score.value = (score.value)?.plus(1)
   nextWord()
}
  • GameViewModel의 nextWord() 안에서, word 에 대한 참조를 word.value로 변경
private fun nextWord() {
   if (!wordList.isEmpty()) {
       //Select and remove a word from the list
       word.value = wordList.removeAt(0)
   }
}
  • GameFragment의 updateWordText() 메소드 안에서 word에 대한 참조를 word.value로 변경
/** Methods for updating the UI **/
private fun updateWordText() {
   binding.wordText.text = viewModel.word.value
}
  • GameFragment 안의 updateScoreText() 메소드 안에서, score에 대한 참조를 score.value 로 변경
private fun updateScoreText() {
   binding.scoreText.text = viewModel.score.value.toString()
}
  • GameFragment 안의 gameFinished() 메소드 안에서, score에 대한 참조를 score.value로 추가하고 필요하다면 null-safety check도 추가
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)
}

실행해보고 오류 없는지 확인


2. Attach observers to the LiveData objects

  • 이 작업은 score와 word data를 LiveData objects로 변환한 이전 작업과 밀접한 관련이 있음
  • 여기서는 그 LiveData objects에 Observer objects를 연결할 거임
  • fragment view(viewLifecycleOwner)를 LifecycleOwner 로 사용할 것!
viewLifecycleOwner를 사용하는 이유 :
fragment view는 fragment 자체를 파괴하지 않아도 사용자가 fragment에서 벗어나면 파괴됨.
이는 두 개의 lifecycle을 생성함 - fragment의 lifecycle과 fragment's view의 lifecycle
fragment's view의 생명주기가 아닌 fragment의 생명주기를 참조하려면 fragment's view를 업데이트할 때 미묘한 에러가 발생할 수 있음
따라서 fragment's view에 영향을 주는 옵저버를 설정할 때는 
1. onCreateView() 에서 옵저버 설정하기
2. viewLifecycleOwner를 옵저버에게 전달하기
를 수행해야 함!

  • GameFragment의 onCreateView() 안에서, Observer object를 current score를 위한 LiceData object 인 viewModel.score에 attach하기
    • observe() 메소드를 사용하며, viewModel을 초기화하는 코드 이후에 집어넣기
  • lambda expression을 사용하여 코드 간결하게 하기
    • lambda expression은 선언되진 않았지만 식으로서 즉시 전달될 수 있는 익명함수임
viewModel.score.observe(viewLifecycleOwner, Observer { newScore ->
})
  • 방금 생성한 옵저버는 관찰되는 LiveData object가 지닌 데이터가 변경될 때 이벤트를 수신함
  • 옵저버 안에서는 score textview를 새로운 점수로 업데이트하기
/** Setting up LiveData observation relationship **/
viewModel.score.observe(viewLifecycleOwner, Observer { newScore ->
   binding.scoreText.text = newScore.toString()
})
  • 위와 같은 방법으로 Observer object를 current word LiveData object에 attach하기
/** Setting up LiveData observation relationship **/
viewModel.word.observe(viewLifecycleOwner, Observer { newWord ->
   binding.wordText.text = newWord
})
  • score나 word의 값이 변경되면, 이제 자동으로 업데이트되어 화면에 표시될 것!
    • GameFragment에서 updateWordText() 와 updateScoreText(), 그리고 이들에 대한 모든 레퍼런스 삭제하기
    • 텍스트뷰가 LiveData observer 메소드에 의해 갱신되기 때문에 더 이상 필요 없음!
  • 이제 앱은 이전과 똑같이 작동하지만 LiveData와 LiveData Observers를 사용 중인 것

3. Encapsulate the LiveData

  • Encapsulation : 캡슐화
    • object의 일부 field에 대한 direct access를 제한하는 방법
    • object를 캡슐화할 때private internal 필드를 변경하는 set of public method가 노출됨
    • 캡슐화를 사용하면 다른 클래스가 이러한 내부 필드를 조작하는 방법을 제어할 수 있음

  • 현재 코드에서는 모든 외부 클래스가 value 속성을 사용하여 score와 word 변수를 수정할 수 있음
  • 개발 중인 앱에는 문제 없을 수 있지만 실제 운영하는 앱에서는 데이터를 ViewModel object에서의 데이터 제어가 필요함
  • 오직 ViewModel이 앱의 데이터를 편집해야 함
  • 하지만 UI controller가 데이터를 읽어야 하므로, 데이터 필드를 private으로 바꿀 수는 없음
    • 앱의 데이터를 캡슐화하려면 MutableLiveData와 LiveData objects를 사용해야 함

  • MutableLiveData vs LiveData
    • MutableLiveData object의 data는 이름에서 알 수 있듯이 변경될 수 있음
      • ViewModel 안의 데이터는 편집 가능해야 함! 그래서 MutableLiveData 사용함
    • LiveData object의 데이터는 read는 가능하지만 changed는 불가능 함
      • ViewModel의 바깥에서는, 데이터는 readable하지만 not editable해야 함
      • 따라서 데이터는 LiveData로서 노출되어야 함
  • 이 전략을 수행하려면 Kotlin backing property 를 사용해야 함
    • backing property를 사용하면 정확한 object 외의 getter에서 무언가를 반환할 수 있음
  • 여기서는 score와 word를 위한 backing property를 구현할 것

STEP 1 : Add a backing property to score and word

  • GameViewModel 안에서, 최근 점수를 위한 private 변수 만들기
  • backing property의 naming 규칙을 따르기 위해 score를 _score로 변경
    • _score 는 내부적으로 사용되는 game score를 위한 Mutable version이 되는 것!
  • score라는 이름의 public 타입의 LiveData 만들기
// The current score
private val _score = MutableLiveData<Int>()
val score: LiveData<Int>
  • initialization error : GameFragment 안에서 score가 LiveData 레퍼런스이고 더 이상 score의 setter에 액세스할 수 없게 되어서 발생한 에러
    • 에러를 해결하려면 GameViewModel의 score object에서 get() 메소드를 override하고 backing property인 _score를 반환하기
val score: LiveData<Int>
   get() = _score
  • GameViewModel에서 score에 대한 참조를 score의 내부 가변 버전인 _score 로 변경
init {
   ...
   _score.value = 0
   ...
}

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

fun onCorrect() {
   _score.value = (score.value)?.plus(1)
   ...
}
  • word object에 대해서도 이름을 _word 로 변경하고 backing property 추가하기
// 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 object인 word와 score를 캡슐화한 것!

4. Add a game-finished event

  • 사용자가 게임 종료 버튼을 누르면 현재 앱이 점수화면으로 이동
  • 플레이어가 모든 단어를 마치면 앱이 점수화면으로 디동
    • 플레이어가 마지막 단어를 마치면 사용자가 버튼을 누르지 않아도 되도록 자동으로 게임이 종료되는 것
  • 이 기능을 구현하려면 모든 단어가 다 나왔을 때 이벤트를 발생시키고 ViewModel에서 fragment로 전달해야 함
  • 그러기 위해서는 게임 종료 이벤트를 모델링하기 위해 LiveData observer pattern을 사용해야 함

The observer pattern

  • 옵저버 패턴은 SW 디자인 패턴임
  • 관찰 가능한(관찰의 대상인) objects 와 관찰자(observer)간의 통신을 지정함
  • An observable(관찰 가능한)? : 상태의 변화를 옵저버에게 알리는 object

  • 이 앱의 LiveData의 경우 관찰 대상은 LiveData object임
  • observer는 UI controller(프래그먼트 등)의 메소드임
  • LiveData 안의 wrapped 된 데이터가 변화할 때마다 상태 변화가 발생함
  • LiveData클래스는 ViewModel에서 fragment까지의 커뮤니케이션에 있어서 아주 중요함

STEP 1 : Use LiveData to detect a game-finished event

LiveData observer pattern를 이용하여 게임 종료 이벤트를 모델링할 것

  • GameViewModel에서, Boolean MutableLiveData object를 만듦 - _eventGameFinish
    • 게임 종료 이벤트를 가질 object
  • _eventGameFinish object를 초기화한 후, eventGameFinish 라는 backing poperty를 만들고 초기화하기
// Event which triggers the end of the game
private val _eventGameFinish = MutableLiveData<Boolean>()
val eventGameFinish: LiveData<Boolean>
   get() = _eventGameFinish
  • GameViewModel에서, onGameFinish() 메소드 추가하기
    • game-finished event인 eventGameFinish를 true로 설정할 것
/** Method for the game completed event **/
fun onGameFinish() {
   _eventGameFinish.value = true
}
  • GameViewModel의 nextWord() 안에서, word list가 비어 있으면 게임 종료하기
private fun nextWord() {
   if (wordList.isEmpty()) {
       onGameFinish()
   } else {
       //Select and remove a _word from the list
       _word.value = wordList.removeAt(0)
   }
}
  • GameFragment 의 onCreateView() 안에서, viewModel을 초기화한 다음에, observer를 eventGameFinish에 연결
    • observe() method 이용하기
    • 람다 함수 안에서 gameFinished() 호출하기
// Observer for the Game finished event
viewModel.eventGameFinish.observe(viewLifecycleOwner, Observer<Boolean> { hasFinished ->
   if (hasFinished) gameFinished()
})
  • 앱 실행하면? 모든 단어가 나왔을 때 사용자가 게임 종료를 누를 때까지 기다리지 않고 자동으로 점수 화면으로 넘어감
  • wordList가 비게 되면, eventGameFinish가 설정되고, game fragment에서 연결된 observer method가 호출된 후 app은 screen fragment로 navigate 함
  • 근데 지금 추가한 코드들은 lifecycle issue가 있음 : 일단 GameFragment 클래스 안에서, gameFinished() 안의 네비게이션 코드를 주석처리 한 후 Toast 메세지 띄우기
    • 앱 실행하면 모든 단어가 나왔을 때 화면 이동이 아닌 토스트 메세지 나옴
  • 근데 디바이스를 회전시키면 토스트 메세지가 또 뜸! 회전할 때마다 뜸! 근데 토스트는 게임 끝났을 때 딱 한 번만 표시되어야 함
    • 다음 작업에서 해결

STEP 2 : Reset the game-finished event

  • 보통 LiveData는 데이터가 변경되었을 경우에만 옵저버에 업데이트 전달함
  • 이 동작의 예외는, 옵저버가 inactive 상태에서 active 상태로 변화되었을 경우에도 업데이트를 수신한다는 거임!
    • 토스트가 자꾸 나오는 이유
    • 화면 회전 후 game fragment가 re-created 되면 inactive 상태에서 active 상태로 변화함
    • fragment 내의 옵저버가 기존의 ViewModel에 재접속하여 최근 데이터를 받음
    • gameFinished() 메소드가 다시 발생되고 토스트가 표시되는 것!
  • 이 작업에서는 이를 수정하여 토스트를 한 번만 표시할 것
    • GameViewModel에서 eventGameFinish flat 리셋하기

 

  • GameViewModel 안에 onGameFinishComplete() 추가하기
    • game finished method인 _eventGameFinish 를 리셋하는 메소드
/** Method for the game completed event **/

fun onGameFinishComplete() {
   _eventGameFinish.value = false
}
  • GameFragment의 gameFinished() 맨 마지막에서, viewModel object에서 onGameFinishComplete() 호출하기
    • gameFinished()의 네비게이션 코드는 일단 주석 상태로 그냥 둠
private fun gameFinished() {
   ...
   viewModel.onGameFinishComplete()
}
  • 앱을 실행하면 모든 단어가 나온 후 화면을 회전해도 토스트는 한 번만 표시됨
    • 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()
}


5. Add LiveData to the ScoreViewModel

  • score를 ScoreViewModel의 LiveData object로 바꾸고 거기에 observer 연결할 것
  • GameViewModel에 LiveData 추가할 때와 비슷
  • 앱의 모든 데이터가 LiveData를 사용함으로써 완성도를 높일 수 있음

  • ScrollViewModel에서, score를 MutableLiveData 타입으로 바꾸기
  • _score로 이름 변경한 후(규칙에 따라) 백업 속성 추가
private val _score = MutableLiveData<Int>()
val score: LiveData<Int>
   get() = _score
  • ScrollViewModel의 init 안에서, _score를 초기화하기
    • init 안의 로그는 지우거나 말거나 맘대로!
init {
   _score.value = finalScore
}
  • ScoreFragment의 onCreateView() 안에서, viewModel을 초기화한 다음에, score LiveData object에 observer 연결
    • 람다식 내에서 score value를 score text view로 설정하고
    • ViewModel 에서 직접적으로 textview에 score 값을 할당하는 코드는 지우기
// Add observer for score
viewModel.score.observe(viewLifecycleOwner, Observer { newScore ->
   binding.scoreText.text = newScore.toString()
})

//지워야 할 코드
binding.scoreText.text = viewModel.score.toString()

6. Add the Play Again button

  • play again 버튼을 score screen에 추가하고 LiveData event를 이용해 클릭리스너 생성하기
  • 버튼을 누르면 점수 화면에서 게임 화면으로 이동하는 이벤트 발생

  • 스타터 코드에는 Play Again 버튼이 포함되어 있지만 숨겨져 있으므로, score fragment에서 보이게 바꾸기
<Button
   android:id="@+id/play_again_button"
...
   android:visibility="visible"
 />
  • ScoreViewModel에 Boolean 값을 저장하기 위한 LiveData object 추가하기 - _eventPlayAgain()
    • 이 오브젝트는 score 화면에서 game 화면으로 이동하는 이벤트를 저장하기 위해 사용됨
private val _eventPlayAgain = MutableLiveData<Boolean>()
val eventPlayAgain: LiveData<Boolean>
   get() = _eventPlayAgain
  • ScoreViewModel에서 event를 설정하고 리셋하는 _eventPlayAgain 를 위한 메소드 추가
fun onPlayAgain() {
   _eventPlayAgain.value = true
}
fun onPlayAgainComplete() {
   _eventPlayAgain.value = false
}
  • ScoreFragment에 eventPlayAgain을 위한 옵저버 추가
    • onCreateView()의 맨 아래(return보다는 전)에 코드 추가하기
    • 람다식에서 game screen으로 돌아가도록 설정한 후 eventPlayAgain 리셋하기
// Navigates back to game when button is pressed
viewModel.eventPlayAgain.observe(viewLifecycleOwner, Observer { playAgain ->
   if (playAgain) {
      findNavController().navigate(ScoreFragmentDirections.actionRestart())
       viewModel.onPlayAgainComplete()
   }
})
  • ScoreFragment 안의 onCreateView() 안에서, PlayAgain 버튼을 위한 클릭리스너를 추가한 후 viewModel.onPlayAgain() 호출하기
binding.playAgainButton.setOnClickListener {  viewModel.onPlayAgain()  }

 

 

https://developer.android.com/codelabs/kotlin-android-training-live-data?index=..%2F..android-kotlin-fundamentals&hl=ko#0 

 

Comments