Android Kotlin 기초의 내용을 번역하며 정리한 자료입니다.
앱에서 뷰는 XML 레이아웃에 정의되고 해당 뷰의 데이터는 ViewModel 개체에 보관됩니다. 각 뷰와 해당 ViewModel 사이에는 UI 컨트롤러가 있으며, 이는 그들 사이에서 중계기 역할을 합니다.
For example:
Button 뷰와 GameViewModel은 직접 통신하지 않습니다. GameFragment에 있는 clickListener가 이를 대신합니다.
레이아웃의 뷰가 중개자로서 UI 컨트롤러에 의존하지 않고 ViewModel 객체의 데이터와 직접 통신한다면 더 간단할 것입니다.
ViewModel 개체는 guessTheWord 앱의 모든 UI 데이터를 보유합니다. ViewModel 개체를 데이터 바인딩에 전달하면 보기와 ViewModel 개체 간의 일부 통신을 자동화할 수 있습니다.
이 작업에서는 GameViewModel 및 ScoreViewModel 클래스를 해당 XML 레이아웃과 연결합니다. 또한 클릭 이벤트를 처리하도록 리스너 바인딩을 설정합니다.
이 단계에서는 GameViewModel을 해당 레이아웃 파일인 game_fragment.xml과 연결합니다.
<layout ...>
<data>
<variable
name="gameViewModel"
type="com.example.android.guesstheword.screens.game.GameViewModel" />
</data>
<androidx.constraintlayout...
이를 위해 이전 단계에서 선언한 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
리스너 바인딩은 onClick(), onZoomIn() 또는 onZoomOut()과 같은 이벤트가 트리거될 때 실행되는 바인딩 표현식입니다. 리스너 바인딩은 람다 식으로 작성됩니다.
데이터 바인딩은 리스너를 생성하고 뷰에 리스너를 설정합니다. listened-for 이벤트가 발생하면 리스너는 람다 식을 평가합니다. 리스너 바인딩은 Android Gradle 플러그인 버전 2.0 이상에서 작동합니다. 자세한 내용은 레이아웃 및 바인딩 표현식을 참조하세요.
<Button
android:id="@+id/skip_button"
...
android:onClick="@{() -> gameViewModel.onSkip()}"
... />
<Button
android:id="@+id/correct_button"
...
android:onClick="@{() -> gameViewModel.onSkip()}"
... />
<Button
android:id="@+id/end_game_button"
...
android:onClick="@{() -> gameViewModel.onGameFinish()}"
... />
<layout ...>
<data>
<variable
name="scoreViewModel"
type="com.example.android.guesstheword.screens.score.ScoreViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
<Button
android:id="@+id/play_again_button"
...
android:onClick="@{() -> scoreViewModel.onPlayAgain()}"
... />
viewModel = ...
binding.scoreViewModel = viewModel
//remove code
binding.playAgainButton.setOnClickListener { viewModel.onPlayAgain() }
앱이 데이터 바인딩을 사용하는 경우 컴파일 프로세스는 데이터 바인딩에 사용되는 중간 클래스를 생성합니다. 앱에는 앱을 컴파일할 때까지 Android Studio가 감지하지 못하는 오류가 있을 수 있으므로 코드를 작성하는 동안 경고나 빨간색 코드가 표시되지 않습니다. 그러나 컴파일 타임에 생성된 중간 클래스에서 발생하는 수수께끼 같은 오류가 발생합니다.
데이터 바인딩은 ViewModel 개체와 함께 사용되는 LiveData와 잘 작동합니다. ViewModel 개체에 데이터 바인딩을 추가했으므로 이제 LiveData를 통합할 준비가 되었습니다.
이 작업에서는 LiveData 관찰자 메서드를 사용하지 않고 데이터 변경 사항에 대해 UI에 알리기 위해 LiveData를 데이터 바인딩 소스로 사용하도록guessTheWord 앱을 변경합니다.
바인딩 변수 gameViewModel을 사용하여 GameViewModel의 단어인 LiveData 개체로 설정합니다.
<TextView
android:id="@+id/word_text"
...
android:text="@{gameViewModel.word}"
... />
word.value를 사용할 필요는 없습니다. 대신 실제 LiveData 개체를 사용할 수 있습니다. LiveData 개체는 단어의 현재 값을 표시합니다. word 값이 null이면 LiveData 객체는 빈 문자열을 표시합니다.
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
// remove code
/** Setting up LiveData observation relationship **/
viewModel.word.observe(viewLifecycleOwner, Observer { newWord ->
binding.wordText.text = newWord
})
<TextView
android:id="@+id/score_text"
...
android:text="@{String.valueOf(scoreViewModel.score)}"
... />
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
//remove code
viewModel.score.observe(viewLifecycleOwner, Observer { newScore ->
binding.scoreText.text = newScore.toString()
})
레이아웃에서 데이터 바인딩과 함께 문자열 서식을 추가할 수 있습니다. 이 작업에서는 현재 단어의 서식을 지정하여 주위에 따옴표를 추가합니다. 또한 다음 이미지와 같이 현재 점수에 접두사를 붙이도록 점수 문자열의 형식을 지정합니다.
<string name="quote_format">\"%s\"</string>
<string name="score_format">Current Score: %d</string>
<TextView
android:id="@+id/word_text"
...
android:text="@{@string/quote_format(gameViewModel.word)}"
... />
<TextView
android:id="@+id/score_text"
...
android:text="@{@string/score_format(gameViewModel.score)}"
... />
//remove code
viewModel.score.observe(viewLifecycleOwner, Observer { newScore ->
binding.scoreText.text = newScore.toString()
})
이 작업에서는 앱에 카운트다운 타이머를 추가합니다. 단어 목록이 비어 있을 때 게임이 종료되는 대신 타이머가 완료되면 게임이 종료됩니다. Android는 타이머를 구현하는 데 사용하는 CountDownTimer라는 유틸리티 클래스를 제공합니다.
타이머가 구성 변경 중에 소멸되지 않도록 GameViewModel에 타이머에 대한 논리를 추가합니다. 프래그먼트에는 타이머가 틱할 때 타이머 TextView를 업데이트하는 코드가 포함되어 있습니다.
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
}
// Countdown time
private val _currentTime = MutableLiveData<Long>()
val currentTime: LiveData<Long>
get() = _currentTime
private val timer: CountDownTimer
// 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()
override fun onTick(millisUntilFinished: Long)
{
_currentTime.value = millisUntilFinished/ONE_SECOND
}
override fun onFinish() {
_currentTime.value = DONE
onGameFinish()
}
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)
}
}
override fun onCleared() {
super.onCleared()
// Cancel the timer
timer.cancel()
}
Transformations.map() 메서드는 소스 LiveData에서 데이터 조작을 수행하고 결과 LiveData 객체를 반환하는 방법을 제공합니다. 관찰자가 반환된 LiveData 개체를 관찰하지 않는 한 이러한 변환은 계산되지 않습니다.
이 메서드는 소스 LiveData와 함수를 매개변수로 사용합니다. 이 함수는 소스 LiveData를 조작합니다.
참고: Transformation.map()에 전달된 람다 함수는 기본 스레드에서 실행되므로 장기 실행 작업을 포함하지 마십시오.
이 작업에서는 경과 시간 LiveData 개체를 "MM:SS" 형식의 새 문자열 LiveData 개체로 포맷합니다. 또한 화면에 포맷된 경과 시간을 표시합니다.
// 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초에 한 번 업데이트됩니다. 모든 단어를 순환해도 게임이 끝나지 않습니다. 이제 타이머가 종료되면 게임이 종료됩니다.
ViewModel data binding
데이터 바인딩을 사용하여 ViewModel을 레이아웃과 연결할 수 있습니다.
ViewModel 개체는 UI 데이터를 보유합니다. ViewModel 개체를 데이터 바인딩에 전달하면 View와 ViewModel 개체 간의 일부 통신을 자동화할 수 있습니다.
How to associate a ViewModel with a layout
<data>
<variable
name="gameViewModel"
type="com.example.android.guesstheword.screens.game.GameViewModel" />
</data>
binding.gameViewModel = viewModel
Listener bindings
리스너 바인딩은 onClick()과 같은 클릭 이벤트가 트리거될 때 실행되는 레이아웃의 바인딩 표현식입니다.
리스너 바인딩은 람다 식으로 작성됩니다.
리스너 바인딩을 사용하여 UI 컨트롤러의 클릭 리스너를 레이아웃 파일의 리스너 바인딩으로 바꿉니다.
데이터 바인딩은 리스너를 생성하고 뷰에 리스너를 설정합니다.
android:onClick="@{() -> gameViewModel.onSkip()}"
데이터 바인딩에 LiveData 추가
LiveData 개체를 데이터 바인딩 소스로 사용하여 데이터 변경 사항을 UI에 자동으로 알릴 수 있습니다.
ViewModel의 LiveData 개체에 직접 뷰를 바인딩할 수 있습니다. ViewModel의 LiveData가 변경되면 UI 컨트롤러의 관찰자 메서드 없이 레이아웃의 뷰가 자동으로 업데이트될 수 있습니다.
android:text="@{gameViewModel.word}"
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
}
<data>
<variable
name="MyViewModel"
type="com.example.android.something.MyViewModel" />
</data>
android:text="@{SomeViewModel.newResult}"
Formatting dates