Android Kotlin Fundamentals 05.2 #5 : Task: Add a game-finished event

현재 앱은 End Game 버튼을 눌렀을 때, score screent으로 이동 합니다. 모든 단어를 다 끝마쳤을 때 또한 score screen으로 이동하도록 구현 해 보겠습니다. 사용자가 마지막 단어에 대한 문제를 끝마치면, End Game 버튼을 누르지 않더라도 자동으로 score screen으로 이동 해보도록 하겠습니다.

 

해당 기능을 구현하기 위해 모든 단어가 표시되었을 경우 ViewModel로 부터 Fragment에게 전달 해주는 이벤트가 있어야 합니다. 이렇게 구현 하기 위해서는 LiveData 옵져버 패턴을 이용하여 game-finished 이벤트를 모델링 해야 합니다.

 

The Observer pattern


옵져버 패턴이란 소프트웨어 디자인 패턴 중 하나 입니다. 관찰할 수 있는 대상과 관차자 사이의 커뮤니케이션을 정의 합니다. 관측 가능 한 것은 관찰자에게 그 상태의 변화에 대해 통지 합니다.

해당 앱의 경우 관찰할 수 있는 대상은 LiveData이고, 관찰자는 UI controller의 메소드들 입니다. LiveData에 감싸진 데이터의 변화가 일어날 때 마다 관찰자는 이를 감지 합니다. LiveData는 뷰모델과 프레그먼트가 통신하는데 매우 중요한 역할을 합니다.

 

Step1. Use LiveData to detect a game-finished event


이번 태스크에서는 game-finished 이벤트를 모델링 하기 위해 LiveData의 옵저버 패턴을 사용해 볼 것입니다.

 

1. GameViewModel에서 _eventGameFinished 라는 Boolean MutableLiveData를 만듭니다. 이것은 game-finished 이벤트를 가지고 있습니다.

 

2. _eventGameFinished를 초기화 한 후, eventGameFinish라는 backing property 생성하고 초기화 해주세요.

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

 

3. GameViewModel 안에서 onGameFinish() 메소드를 추가 해주세요. 이 메소드 안에서 game-finished event를 셋팅 할 것입니다. eventGameFinish의 변수 값을 true로 바꿔주어서 말이죠.

 

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

 

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

 

5. GameFragment의 onCreateView()안에서 viewModel을 초기화 한 코드 다음에 observer()를 사용하여 eventGameFinish에 observer를 붙힙니다. 람다 함수 안에서 gameFinished()를 호출 해 줍니다.

 

// Observer for the Game finished event
viewModel.eventGameFinish.observe(this, Observer<Boolean> { hasFinished ->
   if (hasFinished) gameFinished()
})

 

6. 앱을 한번 실행 시켜 보세요. 그리고 모든 단어를 진행 해 보세요. 그러면 점수를 나타내는 화면으로 자동으로 이동 할 것 입니다. 예전에는 End Game 버튼을 눌러야만 게임이 끝났는데 말이죠.

 

word list가 비어있게 되면 eventGameFinish가 셋팅 됩니다. 그리고 이와 관련된 observer 메소드가 호출되게 됩니다. 그리고 app은 score fragment로 이동하게 되죠.

 

7. 우리가 추가했던 코드들은 라이프사이클 이슈를 야기 할 수 있습니다. 해당 이슈를 이해 하기 위해 GameFragment 안에서 navigation 관련 코드를 주석처리 하고, 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)
   }

 

8. 그런 다음 앱을 한번 실행 해 보세요. 모든 단어들을 다 진행 해보세요. 그러면 토스트 메시지가 나타날 것 입니다. 이것은 우리가 예상 할 수 있는 결과 입니다.

 

그러나 이 때 화면 회전을 시켜보세요. toast가 한번 더 나타날 것입니다. 이후에 화면 회전을 몇 번 더 해보면 토스트 메시지가 화면 회전을 할 때마다 뜰 것입니다. 이것은 버그입니다. 왜냐하면 토스트는 게임이 끝났을 때, 한번 만 보여야 하기 때문입니다. 토스트는 프래그먼트가 재 생성 될 때 여러번 보여선 안 됩니다. 다음 태스크에서 해당 이슈를 해결 해 보도록 하겠습니다.

 

Step 2: Reset the game-finished event


일반적으로, LiveData는 데이터가 변경되었을 때만 옵저버에게 업데이트를 제공합니다. 이 동작의 예외는 옵저버가 비활성 상태에서 활성 상태로 변경될 때에도 옵져버 업데이트를 받는 다는 것입니다.

 

이것이 위에서 게임이 끝난 후 나타나는 토스트 메시지가 화면 회전을 할 때마다 반복적으로 나타는 이유 입니다. 화면 회전 후 GameFragment가 다시 만들어 지면서 비활성 상태에서 활성 상태로 변경됩니다. fragment의 옵져버는 기존 ViewModel과 다시 연결이 되고 현재의 데이터를 받습니다. 이 때 gameFinished()가 다시 트리거 되고, 토스트가 나타나게 되는 것 입니다.

 

이번 태스크에서는 해당 이슈를 고쳐 토스트 메시지가 한번만 나오도록 해 볼 것입니다. 다시 eventGameFinish 플래그를 셋팅하는 방식으로 해결 할 것 입니다.

 

1. GameViewModel에서 onGameFinishComplete() 함수를 추가해 주세요. 해당 함수는 _eventGameFinish를 리셋 시켜주는 함수 입니다.

 

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

fun onGameFinishComplete() {
   _eventGameFinish.value = false
}

 

2. GameFragmentdml gameFinished() 끝에, onGaemFinishComplete()를 호출 해 줍니다. (저번에 주석으로 변경한 navigation 관련 코드들은 아직 그대로 두세요!)

 

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

 

3. 다시 앱을 실행 시키고 게임을 해 보세요. 모든 단어들을 다 진행 하였을 때, 화면 회전을 한번 해 보세요. 이번에는 화면 회전을 몇번 하더라도 토스트가 한번만 뜰 것입니다.

 

4. GameFragment의 gameFinish()에서 주석처리 했던 navigation 관련 코드들을 풀어 주세요.

 

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. 앱을 실행하고 게임을 진행 해 보세요. 모든 단어들이 다 진행 되었을 때, 앱은 자동으로 스코어 스크킨으로 네비게이션 됩니다.

 

 

잘하셨습니다. 이번 태스크에서는 game-finished event를 처리하기 위해 GameViewModel에서 LiveData를 이용해 보았습니다. 이제 word list가 비어져 있다면, 앱은 자동으로 score fragment로 네비게이션 합니다.

댓글



Designed by JB FACTORY