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

김성준·2022년 2월 25일
0

안드로이드

목록 보기
8/16

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

1. Add ViewModel data binding

현재의 앱 아키텍처

앱에서 뷰는 XML 레이아웃에 정의되고 해당 뷰의 데이터는 ViewModel 개체에 보관됩니다. 각 뷰와 해당 ViewModel 사이에는 UI 컨트롤러가 있으며, 이는 그들 사이에서 중계기 역할을 합니다.

For example:

  • Got It 버튼은 game_fragment.xml 레이아웃 파일에 Button 보기로 정의되어 있습니다.
  • 사용자가 Get It 버튼을 탭하면 GameFragment 프래그먼트의 클릭 리스너가 GameViewModel의 해당 클릭 리스너를 호출합니다.
  • 점수는 GameViewModel에서 업데이트됩니다.

Button 뷰와 GameViewModel은 직접 통신하지 않습니다. GameFragment에 있는 clickListener가 이를 대신합니다.

데이터 바인딩에 전달된 ViewModel

레이아웃의 뷰가 중개자로서 UI 컨트롤러에 의존하지 않고 ViewModel 객체의 데이터와 직접 통신한다면 더 간단할 것입니다.

ViewModel 개체는 guessTheWord 앱의 모든 UI 데이터를 보유합니다. ViewModel 개체를 데이터 바인딩에 전달하면 보기와 ViewModel 개체 간의 일부 통신을 자동화할 수 있습니다.

이 작업에서는 GameViewModel 및 ScoreViewModel 클래스를 해당 XML 레이아웃과 연결합니다. 또한 클릭 이벤트를 처리하도록 리스너 바인딩을 설정합니다.

1.1. Add data binding for the GameViewModel

이 단계에서는 GameViewModel을 해당 레이아웃 파일인 game_fragment.xml과 연결합니다.

1.1.1 game_fragment.xml 파일에서 GameViewModel 유형의 데이터 바인딩 변수를 추가합니다. Android Studio에 오류가 있는 경우 프로젝트를 정리하고 다시 빌드하세요.

<layout ...>

   <data>

       <variable
           name="gameViewModel"
           type="com.example.android.guesstheword.screens.game.GameViewModel" />
   </data>
  
   <androidx.constraintlayout...

1.1.2. GameFragment 파일에서 GameViewModel을 데이터 바인딩에 전달합니다.

이를 위해 이전 단계에서 선언한 binding.gameViewModel 변수에 viewModel을 할당합니다. viewModel이 초기화된 후 이 코드를 onCreateView() 안에 넣습니다. Android Studio에 오류가 있는 경우 프로젝트를 정리하고 다시 빌드하세요.

// Set the viewmodel for databinding - this allows the bound layout access 
// to all the data in the ViewModel
binding.gameViewModel = viewModel

1.2. Use listener bindings for event handling

리스너 바인딩은 onClick(), onZoomIn() 또는 onZoomOut()과 같은 이벤트가 트리거될 때 실행되는 바인딩 표현식입니다. 리스너 바인딩은 람다 식으로 작성됩니다.

데이터 바인딩은 리스너를 생성하고 뷰에 리스너를 설정합니다. listened-for 이벤트가 발생하면 리스너는 람다 식을 평가합니다. 리스너 바인딩은 Android Gradle 플러그인 버전 2.0 이상에서 작동합니다. 자세한 내용은 레이아웃 및 바인딩 표현식을 참조하세요.

1.2.1. game_fragment.xml에서 skip_button에 onClick 속성을 추가합니다. 바인딩 표현식을 정의하고 GameViewModel에서 onSkip() 메서드를 호출합니다. 이 바인딩 표현식을 리스너 바인딩이라고 합니다.

<Button
   android:id="@+id/skip_button"
   ...
   android:onClick="@{() -> gameViewModel.onSkip()}"
   ... />

1.2.2. 마찬가지로, correct_button의 클릭 이벤트를 GameViewModel의 onCorrect() 메서드에 바인딩합니다.

<Button
   android:id="@+id/correct_button"
   ...
   android:onClick="@{() -> gameViewModel.onSkip()}"
   ... />

1.2.3.end_game_button의 클릭 이벤트를 GameViewModel의 onGameFinish() 메서드에 바인딩합니다.

<Button
   android:id="@+id/end_game_button"
   ...
   android:onClick="@{() -> gameViewModel.onGameFinish()}"
   ... />

1.2.4. GameFragment에서 클릭 수신기를 설정하는 문을 제거하고 클릭 수신기가 호출하는 함수를 제거합니다. 더 이상 필요하지 않습니다.

1.3. Add data binding for the ScoreViewModel

1.3.1. score_fragment.xml 파일에서 ScoreViewModel 유형의 바인딩 변수를 추가하십시오. 이 단계는 위의 GameViewModel에 대해 수행한 작업과 유사합니다.

<layout ...>
   <data>
       <variable
           name="scoreViewModel"
           type="com.example.android.guesstheword.screens.score.ScoreViewModel" />
   </data>
   <androidx.constraintlayout.widget.ConstraintLayout

1.3.2. score_fragment.xml에서 play_again_button에 onClick 속성을 추가합니다. 리스너 바인딩을 정의하고 ScoreViewModel에서 onPlayAgain() 메서드를 호출합니다.

<Button
   android:id="@+id/play_again_button"
   ...
   android:onClick="@{() -> scoreViewModel.onPlayAgain()}"
   ... />

1.3.3. ScoreFragment의 onCreateView() 내부에서 viewModel을 초기화합니다. 그런 다음 binding.scoreViewModel 바인딩 변수를 초기화합니다.

viewModel = ...
binding.scoreViewModel = viewModel

1.3.4. ScoreFragment에서 playAgainButton에 대한 클릭 수신기를 설정하는 코드를 제거합니다. Android Studio에 오류가 표시되면 프로젝트를 정리하고 다시 빌드합니다.

//remove code
binding.playAgainButton.setOnClickListener {  viewModel.onPlayAgain()  }

1.3.5. 앱을 실행합니다. 앱은 이전과 같이 작동해야 하지만 이제 버튼 보기는 ViewModel 개체와 직접 통신합니다. 뷰는 더 이상 ScoreFragment의 버튼 클릭 핸들러를 통해 통신하지 않습니다.

Troubleshooting data-binding error messages

앱이 데이터 바인딩을 사용하는 경우 컴파일 프로세스는 데이터 바인딩에 사용되는 중간 클래스를 생성합니다. 앱에는 앱을 컴파일할 때까지 Android Studio가 감지하지 못하는 오류가 있을 수 있으므로 코드를 작성하는 동안 경고나 빨간색 코드가 표시되지 않습니다. 그러나 컴파일 타임에 생성된 중간 클래스에서 발생하는 수수께끼 같은 오류가 발생합니다.

알 수 없는 오류 메시지가 표시되는 경우

  • Android Studio Build 창의 메시지를 주의 깊게 살펴보십시오. 데이터 바인딩으로 끝나는 위치가 보이면 데이터 바인딩에 오류가 있는 것입니다.
  • 레이아웃 XML 파일에서 데이터 바인딩을 사용하는 onClick 속성의 오류를 확인하십시오. 람다 식이 호출하는 함수를 찾아 존재하는지 확인합니다.
  • XML의 섹션에서 데이터 바인딩 변수의 철자를 확인합니다.

2. Add LiveData to data binding

데이터 바인딩은 ViewModel 개체와 함께 사용되는 LiveData와 잘 작동합니다. ViewModel 개체에 데이터 바인딩을 추가했으므로 이제 LiveData를 통합할 준비가 되었습니다.
이 작업에서는 LiveData 관찰자 메서드를 사용하지 않고 데이터 변경 사항에 대해 UI에 알리기 위해 LiveData를 데이터 바인딩 소스로 사용하도록guessTheWord 앱을 변경합니다.

2.1. game_fragment.xml 파일에 LiveData라는 단어 추가

2.1.1. game_fragment.xml에서 android:text 속성을word_text 텍스트 보기에 추가합니다.

바인딩 변수 gameViewModel을 사용하여 GameViewModel의 단어인 LiveData 개체로 설정합니다.

<TextView
   android:id="@+id/word_text"
   ...
   android:text="@{gameViewModel.word}"
   ... />

word.value를 사용할 필요는 없습니다. 대신 실제 LiveData 개체를 사용할 수 있습니다. LiveData 개체는 단어의 현재 값을 표시합니다. word 값이 null이면 LiveData 객체는 빈 문자열을 표시합니다.

2.1.2. GameFragment의 onCreateView()에서 gameViewModel을 초기화한 후 Fragment View를 바인딩 변수의 수명 주기 소유자로 설정합니다. 이것은 위의 LiveData 개체의 범위를 정의하여 개체가 game_fragment.xml 레이아웃의 보기를 자동으로 업데이트할 수 있도록 합니다.

binding.gameViewModel = ...
// Specify the fragment view as the lifecycle owner of the binding.
// This is used so that the binding can observe LiveData updates
binding.lifecycleOwner = viewLifecycleOwner

2.1.3. GameFragment에서 LiveData 단어에 대한 관찰자를 제거합니다.

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

2.1.4. 앱을 실행하고 게임을 플레이하세요. 이제 현재 단어는 UI 컨트롤러에서 관찰자 메서드 없이 업데이트됩니다.

2.2. score_fragment.xml 파일에 스코어 LiveData 추가

2.2.1. score_fragment.xml에서 android:text 속성을 Score TextView에 추가합니다. 텍스트 속성에 scoreViewModel.score를 할당합니다. 점수는 정수이므로 String.valueOf()를 사용하여 문자열로 변환합니다.

<TextView
   android:id="@+id/score_text"
   ...
   android:text="@{String.valueOf(scoreViewModel.score)}"
   ... />

2.2.2. ScoreFragment에서 scoreViewModel을 초기화한 후 현재 활동을 바인딩 변수의 수명 주기 소유자로 설정합니다.

binding.scoreViewModel = ...
// Specify the fragment view as the lifecycle owner of the binding.
// This is used so that the binding can observe LiveData updates
binding.lifecycleOwner = viewLifecycleOwner

2.2.3. ScoreFragment에서 점수 개체에 대한 관찰자를 제거합니다.

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

2.2.4. 앱을 실행하고 게임을 플레이하세요. 점수는 score LiveData의 관찰자 없이 올바르게 표시됩니다.

2.3. 데이터 바인딩으로 문자열 서식 추가

레이아웃에서 데이터 바인딩과 함께 문자열 서식을 추가할 수 있습니다. 이 작업에서는 현재 단어의 서식을 지정하여 주위에 따옴표를 추가합니다. 또한 다음 이미지와 같이 현재 점수에 접두사를 붙이도록 점수 문자열의 형식을 지정합니다.

2.3.1. string.xml에 다음 문자열을 추가합니다. 이 문자열을 사용하여 단어 형식을 지정하고 TextView에 점수를 매깁니다. %s 및 %d는 현재 단어 및 현재 점수에 대한 자리 표시자입니다.

<string name="quote_format">\"%s\"</string>
<string name="score_format">Current Score: %d</string>

2.3.2. game_fragment.xml에서 quote_format 문자열 리소스를 사용하도록 word_text 텍스트 보기의 텍스트 속성을 업데이트합니다. gameViewModel.word를 전달합니다. 이것은 현재 단어를 형식화 문자열에 대한 인수로 전달합니다.

<TextView
   android:id="@+id/word_text"
   ...
   android:text="@{@string/quote_format(gameViewModel.word)}"
   ... />

2.3.3. word_text와 유사한 점수 텍스트 보기의 형식을 지정합니다. game_fragment.xml에서 텍스트 속성을 score_text 텍스트 보기에 추가합니다. %d 자리 표시자로 표시되는 하나의 숫자 인수를 사용하는 문자열 리소스 score_format을 사용합니다. 이 형식화 문자열에 대한 인수로 LiveData 개체, score를 전달합니다.

<TextView
   android:id="@+id/score_text"
   ...
   android:text="@{@string/score_format(gameViewModel.score)}"
   ... />

2.3.4. GameFragment 클래스의 onCreateView() 메서드 내에서 점수 관찰자 코드를 제거합니다.

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

2.3.5. 앱을 정리, 다시 빌드 및 실행한 다음 게임을 플레이하세요. 현재 단어와 점수는 게임 화면에서 형식이 지정됩니다.

3. Add a timer

이 작업에서는 앱에 카운트다운 타이머를 추가합니다. 단어 목록이 비어 있을 때 게임이 종료되는 대신 타이머가 완료되면 게임이 종료됩니다. Android는 타이머를 구현하는 데 사용하는 CountDownTimer라는 유틸리티 클래스를 제공합니다.

타이머가 구성 변경 중에 소멸되지 않도록 GameViewModel에 타이머에 대한 논리를 추가합니다. 프래그먼트에는 타이머가 틱할 때 타이머 TextView를 업데이트하는 코드가 포함되어 있습니다.

3.1. GameViewModel클래스에 타이머 상수를 보관할 컴패니언 개체를 만듭니다.

companion object {

   // Time when the game is over
   private const val DONE = 0L

   // Countdown time interval
   private const val ONE_SECOND = 1000L

   // Total time for the game
   private const val COUNTDOWN_TIME = 60000L

}

3.2. 타이머의 카운트다운 시간을 저장하려면 _currentTime이라는 MutableLiveData 멤버 변수와 지원 속성인 currentTime을 추가합니다.

// Countdown time
private val _currentTime = MutableLiveData<Long>()
val currentTime: LiveData<Long>
   get() = _currentTime

3.3. CountDownTimer 유형의 timer라는 비공개 멤버 변수를 추가합니다. 다음 단계에서 초기화 오류를 해결합니다.

private val timer: CountDownTimer

3.4. init 블록 내에서 타이머를 초기화하고 시작합니다. 총 시간 COUNTDOWN_TIME을(를) 경과하십시오. 시간 간격으로 ONE_SECOND를 사용합니다. 콜백 메서드 onTick() 및 onFinish()를 재정의하고 타이머를 시작합니다.

// Creates a timer which triggers the end of the game when it finishes
timer = object : CountDownTimer(COUNTDOWN_TIME, ONE_SECOND) {

   override fun onTick(millisUntilFinished: Long) {
       
   }

   override fun onFinish() {
       
   }
}

timer.start()

3.5. 모든 간격 또는 모든 틱마다 호출되는 onTick() 콜백 메서드를 구현합니다. 전달된 매개변수 millisUntilFinished를 사용하여 _currentTime을 업데이트합니다. millisUntilFinished는 타이머가 완료될 때까지의 시간(밀리초)입니다. millisUntilFinished를 초로 변환하고 _currentTime에 할당합니다.

override fun onTick(millisUntilFinished: Long)
{
   _currentTime.value = millisUntilFinished/ONE_SECOND
}

3.6. onFinish() 콜백 메소드는 타이머가 완료되면 호출됩니다. onFinish()를 구현하여 _currentTime을 업데이트하고 게임 종료 이벤트를 트리거합니다.

override fun onFinish() {
   _currentTime.value = DONE
   onGameFinish()
}

3.7. 게임을 끝내는 대신 목록이 비어 있을 때 단어 목록을 재설정하도록 nextWord() 메서드를 업데이트하십시오.

private fun nextWord() {
   // Shuffle the word list, if the list is empty 
   if (wordList.isEmpty()) {
       resetList()
   } else {
   // Remove a word from the list
   _word.value = wordList.removeAt(0)
   }
}

3.8. onCleared() 메서드 내에서 메모리 누수를 방지하기 위해 타이머를 취소합니다. 더 이상 필요하지 않으므로 로그 문을 제거할 수 있습니다. onCleared() 메서드는 ViewModel이 소멸되기 전에 호출됩니다.

override fun onCleared() {
   super.onCleared()
   // Cancel the timer
   timer.cancel()
}

3.9. 앱을 실행하고 게임을 플레이하세요. 60초를 기다리면 게임이 자동으로 종료됩니다. 그러나 타이머 텍스트는 화면에 표시되지 않습니다.

4. Add transformation for the LiveData

Transformations.map() 메서드는 소스 LiveData에서 데이터 조작을 수행하고 결과 LiveData 객체를 반환하는 방법을 제공합니다. 관찰자가 반환된 LiveData 개체를 관찰하지 않는 한 이러한 변환은 계산되지 않습니다.

이 메서드는 소스 LiveData와 함수를 매개변수로 사용합니다. 이 함수는 소스 LiveData를 조작합니다.

참고: Transformation.map()에 전달된 람다 함수는 기본 스레드에서 실행되므로 장기 실행 작업을 포함하지 마십시오.

이 작업에서는 경과 시간 LiveData 개체를 "MM:SS" 형식의 새 문자열 LiveData 개체로 포맷합니다. 또한 화면에 포맷된 경과 시간을 표시합니다.

4.1. GameViewModel 클래스에서 currentTime을 인스턴스화한 후 currentTimeString이라는 새 LiveData 개체를 만듭니다. 이 개체는 currentTime의 형식이 지정된 문자열 버전을 위한 것입니다.

4.2. Transformations.map()을 사용하여 currentTimeString을 정의합니다. currentTime 및 람다 함수를 전달하여 시간 형식을 지정합니다. DateUtils.formatElapsedTime() 유틸리티 메서드를 사용하여 람다 함수를 구현할 수 있습니다. 이 메서드는 "MM:SS" 문자열 형식으로 형식을 지정합니다.

// The String version of the current time
val currentTimeString = Transformations.map(currentTime) { time ->
   DateUtils.formatElapsedTime(time)
}
  
#### 4.3. game_fragment.xml 파일의 타이머 텍스트 보기에서 text 속성을 gameViewModel의 currentTimeString에 바인딩합니다.

<TextView
android:id="@+id/timer_text"
...
android:text="@{gameViewModel.currentTimeString}"
... />

#### 4.4. 앱을 실행하고 게임을 플레이하세요. 타이머 텍스트는 1초에 한 번 업데이트됩니다. 모든 단어를 순환해도 게임이 끝나지 않습니다. 이제 타이머가 종료되면 게임이 종료됩니다.

Summary

  • 데이터 바인딩 라이브러리는 ViewModel 및 LiveData와 같은 Android 아키텍처 구성 요소와 원활하게 작동합니다.
  • 앱의 레이아웃은 UI 컨트롤러의 수명 주기를 관리하고 데이터 변경 사항을 알리는 데 도움이 되는 아키텍처 구성 요소의 데이터에 바인딩할 수 있습니다.

ViewModel data binding

  • 데이터 바인딩을 사용하여 ViewModel을 레이아웃과 연결할 수 있습니다.

  • ViewModel 개체는 UI 데이터를 보유합니다. ViewModel 개체를 데이터 바인딩에 전달하면 View와 ViewModel 개체 간의 일부 통신을 자동화할 수 있습니다.

How to associate a ViewModel with a layout

  • 레이아웃 파일에서 ViewModel 유형의 데이터 바인딩 변수를 추가합니다.
<data>
     <variable
           name="gameViewModel"
           type="com.example.android.guesstheword.screens.game.GameViewModel" />
 </data>
  • GameFragment 파일에서 GameViewModel을 데이터 바인딩에 전달합니다.
binding.gameViewModel = viewModel

Listener bindings

  • 리스너 바인딩은 onClick()과 같은 클릭 이벤트가 트리거될 때 실행되는 레이아웃의 바인딩 표현식입니다.

  • 리스너 바인딩은 람다 식으로 작성됩니다.

  • 리스너 바인딩을 사용하여 UI 컨트롤러의 클릭 리스너를 레이아웃 파일의 리스너 바인딩으로 바꿉니다.

  • 데이터 바인딩은 리스너를 생성하고 뷰에 리스너를 설정합니다.

 android:onClick="@{() -> gameViewModel.onSkip()}"

데이터 바인딩에 LiveData 추가

  • LiveData 개체를 데이터 바인딩 소스로 사용하여 데이터 변경 사항을 UI에 자동으로 알릴 수 있습니다.

  • ViewModel의 LiveData 개체에 직접 뷰를 바인딩할 수 있습니다. ViewModel의 LiveData가 변경되면 UI 컨트롤러의 관찰자 메서드 없이 레이아웃의 뷰가 자동으로 업데이트될 수 있습니다.

android:text="@{gameViewModel.word}"
  • LiveData 데이터 바인딩이 작동하도록 하려면 현재 액티비티(UI 컨트롤러)를 UI 컨트롤러에서 바인딩 변수의 수명 주기 소유자로 설정합니다.
binding.lifecycleOwner = this

String formatting with data binding

  • 데이터 바인딩을 사용하면 문자열의 경우 %s, 정수의 경우 %d와 같은 자리 표시자로 문자열 리소스의 형식을 지정할 수 있습니다.

  • 뷰의 텍스트 속성을 업데이트하려면 LiveData 개체를 형식화 문자열에 대한 인수로 전달하십시오.

 android:text="@{@string/quote_format(gameViewModel.word)}"

Transforming LiveData

  • 때로는 LiveData의 결과를 변환하고 싶을 때가 있습니다. 예를 들어 날짜 문자열을 "시간:분:초"로 형식 지정하거나 목록 자체를 반환하는 대신 목록의 항목 수를 반환할 수 있습니다. LiveData에서 변환을 수행하려면 Transformations 클래스의 유틸 메서드를 사용합니다.

  • Transformations.map() 메서드는 LiveData에서 데이터 조작을 수행하고 다른 LiveData 객체를 반환하는 쉬운 방법을 제공합니다. 권장되는 방법은 UI 데이터와 함께 ViewModel의 Transformations 클래스를 사용하는 데이터 형식 지정 논리를 넣는 것입니다.

Displaying the result of a transformation in a TextView

  • 소스 데이터가 ViewModel에서 LiveData로 정의되어 있는지 확인하십시오.

  • 변수를 정의하십시오(예: newResult). Transformation.map()을 사용하여 변환을 수행하고 결과를 변수에 반환합니다.

val newResult = Transformations.map(someLiveData) { input ->
   // Do some transformation on the input live data
   // and return the new value
}
  • TextView가 포함된 레이아웃 파일이 ViewModel에 대한 변수를 선언하는지 확인하십시오.
<data>
   <variable
       name="MyViewModel"
       type="com.example.android.something.MyViewModel" />
</data>
  • 레이아웃 파일에서 TextView의 text 속성을 ViewModel의 newResult 바인딩으로 설정합니다. 예를 들어:
android:text="@{SomeViewModel.newResult}"

Formatting dates

  • DateUtils.formatElapsedTime() 유틸리티 메서드는 밀리초가 MM:SS 문자열 형식을 사용하도록 숫자 형식을 지정합니다.
profile
수신제가치국평천하

0개의 댓글