yourginieus
LiveData 및 LiveData Observer 본문
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로서 노출되어야 함
- MutableLiveData object의 data는 이름에서 알 수 있듯이 변경될 수 있음
- 이 전략을 수행하려면 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() }
'Android > Android Kotlin 기초 실습 정리' 카테고리의 다른 글
LiveData transformations (0) | 2022.10.27 |
---|---|
ViewModel 및 LiveData를 통한 데이터 바인딩 (0) | 2022.10.27 |
ViewModel : ViewModelFactory (0) | 2022.10.26 |
LifecycleObserver, onSaveInstanceState() (0) | 2022.10.26 |
Life Cycle 및 Logging - Timber (0) | 2022.10.26 |