이 Codelab은 Android Kotlin 기초 교육 과정의 일부입니다. Codelab을 순서대로 진행한다면 이 과정을 통해 최대한의 가치를 얻을 수 있을 것입니다. 모든 과정 Codelab은 Android Kotlin 기초 Codelab 방문 페이지에 나열되어 있습니다.
소개
이 과정의 이전 Codelab에서는 GuessTheWord 앱의 코드를 개선했습니다. 이제 앱이 ViewModel
객체를 사용하므로 화면 회전 및 키보드 가용성 변경과 같은 기기 설정 변경 후에도 앱 데이터가 유지됩니다. 또한 관찰 가능한 LiveData
를 추가했으므로 관찰된 데이터가 변경되면 뷰에 자동으로 알림이 전송됩니다.
이 Codelab에서는 GuessTheWord 앱을 계속 사용합니다. 앱의 ViewModel
클래스에 뷰를 바인딩하여 레이아웃의 뷰가 ViewModel
객체와 직접 통신하도록 합니다. (이제는 앱에서 뷰까지 앱의 프래그먼트를 통해 ViewModel
와 간접적으로 통신했습니다.) 데이터 결합을 ViewModel
객체와 통합한 후에는 더 이상 앱 프래그먼트에 클릭 핸들러가 필요하지 않으므로 이를 삭제합니다.
또한 LiveData
관찰자 메서드를 사용하지 않고 데이터 변경사항을 소스로 사용하여 LiveData
에 데이터 변경사항을 UI에 알리도록 GuessTheWord 앱을 변경합니다.
기본 요건
- Kotlin으로 기본 Android 앱을 만드는 방법
- 활동 및 프래그먼트 수명 주기 작동 방식
- 앱에서
ViewModel
객체를 사용하는 방법 ViewModel
에LiveData
를 사용하여 데이터를 저장하는 방법LiveData
데이터의 변경사항을 관찰하는 관찰자 메서드를 추가하는 방법
학습할 내용
- 데이터 결합 라이브러리의 요소를 사용하는 방법
ViewModel
를 데이터 결합과 통합하는 방법LiveData
를 데이터 결합과 통합하는 방법- 리스너 결합을 사용하여 프래그먼트의 클릭 리스너를 교체하는 방법
- 데이터 결합 표현식에 문자열 서식을 추가하는 방법
실습할 내용
- GuessTheWord 레이아웃의 뷰는
ViewModel
컨트롤러와 간접적으로 통신하며, UI 컨트롤러 (프래그먼트)를 사용하여 정보를 전달합니다. 이 Codelab에서는 앱의 뷰를ViewModel
객체에 바인딩하여 뷰가ViewModel
객체와 직접 통신하도록 합니다. LiveData
를 데이터 결합 소스로 사용하도록 앱을 변경합니다. 이 변경 후LiveData
객체는 데이터 변경사항에 관해 UI에 알리며LiveData
관찰자 메서드는 더 이상 필요하지 않습니다.
과정 5 Codelab에서는 시작 코드로 시작하여 GuessTheWord 앱을 개발합니다. GuessTheWord는 플레이어들이 가능한 최고 점수를 달성하기 위해 협력하는 2인용 차데드 스타일의 게임입니다.
첫 번째 플레이어는 앱의 단어를 보고 순서대로 하나씩 플레이하며 두 번째 플레이어에게 단어를 표시하지 않도록 합니다. 두 번째 플레이어는 단어를 추측하려고 합니다.
게임을 플레이하려면 첫 번째 플레이어가 기기에서 앱을 열면 아래 스크린샷과 같이 단어가 표시됩니다.
첫 번째 플레이어는 단어를 소리 내서 말하지만, 실제로 그 단어를 말하지는 않습니다.
- 두 번째 플레이어가 단어를 올바르게 추측하면 첫 번째 플레이어가 확인 버튼을 눌러 숫자를 1씩 늘리고 다음 단어를 표시합니다.
- 두 번째 플레이어가 단어를 추측할 수 없으면 첫 번째 플레이어가 건너뛰기 버튼을 누릅니다. 이 경우 숫자가 1씩 감소하고 다음 단어로 건너뜁니다.
- 게임을 종료하려면 게임 종료 버튼을 누릅니다. (이 기능은 시리즈의 첫 번째 Codelab 시작 코드에는 없습니다.)
이 Codelab에서는 ViewModel
객체의 데이터 결합을 LiveData
와 통합하여 GuessTheWord 앱을 개선합니다. 레이아웃의 뷰와 ViewModel
객체 간의 통신을 자동화하며, LiveData
를 사용하여 코드를 단순화할 수 있습니다.
타이틀 스크린 | 게임 화면 | 점수 화면 |
이 작업에서는 이 Codelab의 시작 코드를 찾아 실행합니다. 이전 Codelab에서 빌드한 GuessTheWord 앱을 시작 코드로 사용하거나 시작 앱을 다운로드할 수 있습니다.
- (선택사항) 이전 Codelab의 코드를 사용하지 않는 경우 이 Codelab의 시작 코드를 다운로드하세요. 코드의 압축을 푼 후 Android 스튜디오에서 프로젝트를 엽니다.
- 앱을 실행하고 게임을 플레이합니다.
- 확인 버튼을 누르면 다음 단어가 표시되고 점수가 1씩 증가하는 반면 건너뛰기 버튼은 다음 단어를 표시하고 점수를 1씩 줄입니다. 게임 종료 버튼을 누르면 게임이 종료됩니다.
- 모든 단어를 순환하여 앱에서 점수 화면으로 자동으로 이동하는 것을 알 수 있습니다.
이전 Codelab에서는 데이터 결합을 GuessTheWord 앱에서 뷰에 액세스하는 유형의 안전한 방법으로 사용했습니다. 그러나 데이터 결합의 진정한 강점은 이름에서 알 수 있듯이 앱의 뷰 객체에 직접 데이터를 결합하는 것입니다.
현재 앱 아키텍처
앱에서 뷰는 XML 레이아웃에 정의되며 해당 뷰의 데이터는 ViewModel
객체에 보관됩니다. 각 뷰와 이에 대응하는 ViewModel
사이에는 뷰 간 릴레이 역할을 하는 UI 컨트롤러가 있습니다.
예를 들면 다음과 같습니다.
- Got It 버튼은
game_fragment.xml
레이아웃 파일에서Button
뷰로 정의됩니다. - 사용자가 Got It 버튼을 탭하면
GameFragment
프래그먼트의 클릭 리스너가GameViewModel
에서 해당 클릭 리스너를 호출합니다. - 점수가
GameViewModel
에 업데이트됩니다.
Button
뷰와 GameViewModel
는 직접 통신하지 않으므로 GameFragment
에 클릭 리스너가 필요합니다.
데이터 결합에 전달된 ViewModel
UI 컨트롤러가 중개자로 의존하지 않고 레이아웃의 뷰가 ViewModel
객체의 데이터와 직접 통신하면 더 간단합니다.
ViewModel
객체는 GuessTheWord 앱의 모든 UI 데이터를 보유합니다. ViewModel
객체를 데이터 결합에 전달하면 뷰와 ViewModel
객체 간의 일부 커뮤니케이션을 자동화할 수 있습니다.
이 작업에서는 GameViewModel
및 ScoreViewModel
클래스를 해당 XML 레이아웃과 연결합니다. 리스너 이벤트를 설정하여 클릭 이벤트를 처리할 수도 있습니다.
1단계: GameViewModel의 데이터 결합 추가
이 단계에서는 GameViewModel
를 상응하는 레이아웃 파일 game_fragment.xml
와 연결합니다.
game_fragment.xml
파일에서GameViewModel
유형의 데이터 결합 변수를 추가합니다. Android 스튜디오에서 오류가 발생하면 프로젝트를 정리하고 다시 빌드합니다.
<layout ...>
<data>
<variable
name="gameViewModel"
type="com.example.android.guesstheword.screens.game.GameViewModel" />
</data>
<androidx.constraintlayout...
GameFragment
파일에서GameViewModel
를 데이터 결합에 전달합니다.
이렇게 하려면viewModel
를 이전 단계에서 선언한binding.gameViewModel
변수에 할당합니다.viewModel
이 초기화된 후 이 코드를onCreateView()
내부에 배치합니다. Android 스튜디오에서 오류가 발생하면 프로젝트를 정리하고 다시 빌드합니다.
// Set the viewmodel for databinding - this allows the bound layout access
// to all the data in the ViewModel
binding.gameViewModel = viewModel
2단계: 이벤트 처리에 리스너 결합 사용
리스너 결합은 onClick()
, onZoomIn()
, onZoomOut()
와 같은 이벤트가 트리거될 때 실행되는 결합 표현식입니다. 리스너 결합은 람다 표현식으로 작성됩니다.
데이터 결합은 리스너를 생성하고 뷰에 리스너를 설정합니다. 수신 대기 이벤트가 발생하면 리스너는 람다 표현식을 평가합니다. 리스너 결합은 Android Gradle 플러그인 버전 2.0 이상에서 작동합니다. 자세한 내용은 레이아웃 및 결합 표현식을 참고하세요.
이 단계에서는 GameFragment
의 클릭 리스너를 game_fragment.xml
파일의 리스너 결합으로 바꿉니다.
game_fragment.xml
에서onClick
속성을skip_button
에 추가합니다. 결합 표현식을 정의하고GameViewModel
에서onSkip()
메서드를 호출합니다. 이러한 결합 표현식을 리스너 결합이라고 합니다.
<Button
android:id="@+id/skip_button"
...
android:onClick="@{() -> gameViewModel.onSkip()}"
... />
- 마찬가지로
correct_button
의 클릭 이벤트를GameViewModel
의onCorrect
()
메서드에 바인딩합니다.
<Button
android:id="@+id/correct_button"
...
android:onClick="@{() -> gameViewModel.onCorrect()}"
... />
end_game_button
의 클릭 이벤트를GameViewModel
의onGameFinish
()
메서드에 결합합니다.
<Button
android:id="@+id/end_game_button"
...
android:onClick="@{() -> gameViewModel.onGameFinish()}"
... />
GameFragment
에서 클릭 리스너를 설정하는 문을 삭제하고 클릭 리스너가 호출하는 함수를 삭제합니다. 더 이상 필요하지 않습니다.
삭제할 코드:
binding.correctButton.setOnClickListener { onCorrect() }
binding.skipButton.setOnClickListener { onSkip() }
binding.endGameButton.setOnClickListener { onEndGame() }
/** Methods for buttons presses **/
private fun onSkip() {
viewModel.onSkip()
}
private fun onCorrect() {
viewModel.onCorrect()
}
private fun onEndGame() {
gameFinished()
}
3단계: ScoreViewModel의 데이터 결합 추가
이 단계에서는 ScoreViewModel
를 상응하는 레이아웃 파일 score_fragment.xml
와 연결합니다.
score_fragment.xml
파일에서ScoreViewModel
유형의 결합 변수를 추가합니다. 이 단계는 위의GameViewModel
에 실행한 과정과 유사합니다.
<layout ...>
<data>
<variable
name="scoreViewModel"
type="com.example.android.guesstheword.screens.score.ScoreViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
score_fragment.xml
에서onClick
속성을play_again_button
에 추가합니다. 리스너 결합을 정의하고ScoreViewModel
에서onPlayAgain()
메서드를 호출합니다.
<Button
android:id="@+id/play_again_button"
...
android:onClick="@{() -> scoreViewModel.onPlayAgain()}"
... />
ScoreFragment
의onCreateView()
내에서viewModel
를 초기화합니다. 그런 다음binding.scoreViewModel
결합 변수를 초기화합니다.
viewModel = ...
binding.scoreViewModel = viewModel
ScoreFragment
에서playAgainButton
의 클릭 리스너를 설정하는 코드를 삭제합니다. Android 스튜디오에 오류가 표시되면 프로젝트를 정리하고 다시 빌드합니다.
삭제할 코드:
binding.playAgainButton.setOnClickListener { viewModel.onPlayAgain() }
- 앱을 실행합니다. 앱이 이전과 동일하게 작동하지만 이제 버튼 뷰가
ViewModel
객체와 직접 통신합니다. 뷰가 더 이상ScoreFragment
의 버튼 클릭 핸들러를 통해 통신하지 않습니다.
데이터 결합 오류 메시지 문제 해결
앱에서 데이터 결합을 사용하면 컴파일 프로세스에서 데이터 결합에 사용되는 중간 클래스를 생성합니다. 앱을 컴파일하려고 시도할 때까지 Android 스튜디오가 감지할 수 없는 오류가 있을 수 있으므로, 개발자가 코드를 작성하는 동안에는 경고 또는 빨간색 코드가 표시되지 않습니다. 그러나 컴파일 시 생성된 중간 클래스에서 발생하는 암호화된 오류가 발생합니다.
암호화 오류 메시지가 표시되는 경우 다음 안내를 따르세요.
- Android 스튜디오 Build 창의 메시지를 자세히 살펴보세요.
databinding
로 끝나는 위치가 표시되면 데이터 결합에 오류가 있는 것입니다. - 레이아웃 XML 파일에서 데이터 결합을 사용하는
onClick
속성의 오류가 있는지 확인합니다. 람다 표현식이 호출하는 함수를 찾아 존재하는지 확인합니다. - XML의
<data>
섹션에서 데이터 결합 변수의 철자를 확인합니다.
예를 들어 다음 속성 값에서 함수 이름 onCorrect()
의 철자 오류에 유의하세요.
android:onClick="@{() -> gameViewModel.onCorrectx()}"
XML 파일의 <data>
섹션에서도 gameViewModel
철자가 틀렸습니다.
<data>
<variable
name="gameViewModelx"
type="com.example.android.guesstheword.screens.game.GameViewModel" />
</data>
Android 스튜디오는 앱을 컴파일할 때까지 이러한 오류를 감지하지 않으며 컴파일러는 다음과 같은 오류 메시지를 표시합니다.
error: cannot find symbol import com.example.android.guesstheword.databinding.GameFragmentBindingImpl" symbol: class GameFragmentBindingImpl location: package com.example.android.guesstheword.databinding
데이터 결합은 ViewModel
객체와 함께 사용되는 LiveData
에서 잘 작동합니다. 이제 ViewModel
객체에 데이터 결합을 추가했으므로 LiveData
를 통합할 준비가 되었습니다.
이 작업에서는 LiveData
관찰자 메서드를 사용하지 않고 UI에 데이터 변경사항을 알리기 위해 LiveData
를 데이터 결합 소스로 사용하도록 GuessTheWord 앱을 변경합니다.
1단계: game_fragment.xml 파일에 단어 LiveData 추가하기
이 단계에서는 현재 단어 텍스트 뷰를 ViewModel
의 LiveData
객체에 직접 바인딩합니다.
game_fragment.xml
에서word_text
텍스트 뷰에android:text
속성을 추가합니다.
결합 변수 gameViewModel
을 사용하여 GameViewModel
에서 word
로 설정된 LiveData
객체로 설정합니다.
<TextView
android:id="@+id/word_text"
...
android:text="@{gameViewModel.word}"
... />
word.value
를 사용할 필요는 없습니다. 대신 실제 LiveData
객체를 사용하면 됩니다. LiveData
객체는 word
의 현재 값을 표시합니다. word
값이 null이면 LiveData
객체는 빈 문자열을 표시합니다.
GameFragment
의onCreateView()
에서gameViewModel
를 초기화한 후 현재 활동을binding
변수의 수명 주기 소유자로 설정합니다. 이 메서드는 위의LiveData
객체의 범위를 정의하여 객체가game_fragment.xml
레이아웃에서 뷰를 자동으로 업데이트할 수 있도록 합니다.
binding.gameViewModel = ...
// Specify the current activity as the lifecycle owner of the binding.
// This is used so that the binding can observe LiveData updates
binding.lifecycleOwner = this
GameFragment
에서LiveData
word
의 관찰자를 삭제합니다.
삭제할 코드:
/** Setting up LiveData observation relationship **/
viewModel.word.observe(this, Observer { newWord ->
binding.wordText.text = newWord
})
- 앱을 실행하고 게임을 플레이합니다. 이제 현재 단어가 UI 컨트롤러에 관찰자 메서드 없이 업데이트됩니다.
2단계: Score_fragment.xml 파일에 점수 LiveData 추가하기
이 단계에서는 LiveData
score
를 점수 프래그먼트의 점수 텍스트 뷰에 바인딩합니다.
score_fragment.xml
에서 점수 텍스트 뷰에android:text
속성을 추가합니다.text
속성에scoreViewModel.score
를 할당합니다.score
은 정수이므로String.valueOf()
를 사용해 문자열로 변환합니다.
<TextView
android:id="@+id/score_text"
...
android:text="@{String.valueOf(scoreViewModel.score)}"
... />
ScoreFragment
에서scoreViewModel
를 초기화한 후 현재 활동을binding
변수의 수명 주기 소유자로 설정합니다.
binding.scoreViewModel = ...
// Specify the current activity as the lifecycle owner of the binding.
// This is used so that the binding can observe LiveData updates
binding.lifecycleOwner = this
ScoreFragment
에서score
객체의 관찰자를 삭제합니다.
삭제할 코드:
// Add observer for score
viewModel.score.observe(this, Observer { newScore ->
binding.scoreText.text = newScore.toString()
})
- 앱을 실행하고 게임을 플레이합니다. 점수 프래그먼트의 점수는 점수 프래그먼트에서 관찰자 없이 올바르게 표시됩니다.
3단계: 데이터 결합으로 문자열 형식 추가
레이아웃에서 데이터 결합과 함께 문자열 형식을 추가할 수 있습니다. 이 작업에서는 현재 단어의 형식을 지정하여 주위에 따옴표를 추가합니다. 또한 다음 이미지에 표시된 것처럼 점수 문자열을 현재 점수라는 프리픽스로 지정합니다.
string.xml
에서word
및score
텍스트 뷰의 형식을 지정하는 데 사용할 다음 문자열을 추가합니다.%s
및%d
는 현재 단어와 현재 점수의 자리표시자입니다.
<string name="quote_format">\"%s\"</string>
<string name="score_format">Current Score: %d</string>
game_fragment.xml
에서quote_format
문자열 리소스를 사용하도록word_text
텍스트 뷰의text
속성을 업데이트합니다.gameViewModel.word
을 전달합니다. 그러면 현재 단어가 서식 문자열에 인수로 전달됩니다.
<TextView
android:id="@+id/word_text"
...
android:text="@{@string/quote_format(gameViewModel.word)}"
... />
score
텍스트 뷰의 형식을word_text
와 비슷하게 지정합니다.game_fragment.xml
에서text
속성을score_text
텍스트 뷰에 추가합니다.%d
자리표시자로 표시되는 숫자 인수 1개를 사용하는 문자열 리소스score_format
를 사용합니다.LiveData
객체score
을 이 형식 지정 문자열의 인수로 전달합니다.
<TextView
android:id="@+id/score_text"
...
android:text="@{@string/score_format(gameViewModel.score)}"
... />
GameFragment
클래스의onCreateView()
메서드 내에서score
관찰자 코드를 삭제합니다.
삭제할 코드:
viewModel.score.observe(this, Observer { newScore ->
binding.scoreText.text = newScore.toString()
})
- 앱을 정리, 재구성, 실행한 다음 게임을 플레이합니다. 현재 단어와 점수의 형식이 게임 화면에 표시됩니다.
수고하셨습니다. LiveData
및 ViewModel
를 앱에 데이터 결합과 통합했습니다. 이렇게 하면 레이아웃의 뷰가 프래그먼트에서 클릭 핸들러를 사용하지 않고도 ViewModel
와 직접 통신할 수 있습니다. 또한 LiveData
객체를 데이터 결합 소스로 사용하여 LiveData
관찰자 메서드 없이 데이터 변경사항을 UI에 자동으로 알립니다.
Android 스튜디오 프로젝트: GuessTheWord
- 데이터 결합 라이브러리는
ViewModel
,LiveData
등의 Android 아키텍처 구성요소와 원활하게 연동됩니다. - 앱의 레이아웃은 이미 UI 컨트롤러의 수명 주기를 관리하고 데이터 변경사항에 관해 알리는 데 도움이 되는 아키텍처 구성요소의 데이터에 바인딩될 수 있습니다.
ViewModel 데이터 결합
- 데이터 결합을 사용하여
ViewModel
을 레이아웃과 연결할 수 있습니다. ViewModel
객체에는 UI 데이터가 있습니다.ViewModel
객체를 데이터 결합에 전달하면 뷰와ViewModel
객체 간의 일부 커뮤니케이션을 자동화할 수 있습니다.
ViewModel
를 레이아웃과 연결하는 방법은 다음과 같습니다.
- 레이아웃 파일에서
ViewModel
유형의 데이터 결합 변수를 추가합니다.
<data>
<variable
name="gameViewModel"
type="com.example.android.guesstheword.screens.game.GameViewModel" />
</data>
GameFragment
파일에서GameViewModel
를 데이터 결합에 전달합니다.
binding.gameViewModel = viewModel
리스너 결합
- 리스너 결합은
onClick()
와 같은 클릭 이벤트가 트리거될 때 실행되는 레이아웃의 결합 표현식입니다. - 리스너 결합은 람다 표현식으로 작성됩니다.
- 리스너 결합을 사용하여 UI 컨트롤러의 클릭 리스너를 레이아웃 파일의 리스너 결합으로 대체합니다.
- 데이터 결합은 리스너를 생성하고 뷰에 리스너를 설정합니다.
android:onClick="@{() -> gameViewModel.onSkip()}"
데이터 결합에 LiveData 추가하기
LiveData
객체를 데이터 결합 소스로 사용하여 데이터 변경을 UI에 자동으로 알릴 수 있습니다.- 뷰를
ViewModel
의LiveData
객체에 직접 결합할 수 있습니다.ViewModel
의LiveData
가 변경되면 UI 컨트롤러의 관찰자 메서드 없이 레이아웃의 뷰를 자동으로 업데이트할 수 있습니다.
android:text="@{gameViewModel.word}"
LiveData
데이터 결합이 작동하도록 하려면 현재 활동 (UI 컨트롤러)을 UI 컨트롤러에서binding
변수의 수명 주기 소유자로 설정합니다.
binding.lifecycleOwner = this
데이터 결합을 사용한 문자열 형식 지정
- 데이터 결합을 사용하면 문자열의 경우
%s
, 정수의 경우%d
같은 자리표시자로 문자열 리소스의 형식을 지정할 수 있습니다. - 뷰의
text
속성을 업데이트하려면LiveData
객체를 서식 문자열의 인수로 전달합니다.
android:text="@{@string/quote_format(gameViewModel.word)}"
Udacity 과정:
Android 개발자 문서:
이 섹션에는 강사가 진행하는 과정의 일부로 이 Codelab을 통해 작업하는 학생들의 숙제 과제가 나와 있습니다. 강사는 다음을 처리합니다.
- 필요한 경우 과제를 할당합니다.
- 학생에게 과제 과제를 제출하는 방법을 알려주세요.
- 과제 과제를 채점합니다.
강사는 이러한 추천을 원하는 만큼 사용할 수 있으며 다른 적절한 숙제를 할당해도 좋습니다.
이 Codelab을 직접 학습하고 있다면 언제든지 숙제를 통해 지식을 확인해 보세요.
답변
질문 1
리스너 결합과 관련하여 다음 설명 중 참이 아닌 것은 무엇인가요?
- 리스너 결합은 이벤트가 발생할 때 실행되는 결합 표현식입니다.
- 리스너 결합은 모든 버전의 Android Gradle 플러그인에서 작동합니다.
- 리스너 결합은 람다 표현식으로 작성됩니다.
- 리스너 결합은 메서드 참조와 비슷하지만, 리스너 결합을 사용하면 임의의 데이터 결합 표현식을 실행할 수 있습니다.
질문 2
앱에 다음 문자열 리소스가 포함되어 있다고 가정합니다. <string name="generic_name">Hello %s</string>
다음 중 데이터 결합 표현식을 사용하여 문자열의 형식을 지정하기 위한 올바른 구문은 무엇인가요?
android:text= "@{@string/generic_name(user.name)}"
android:text= "@{string/generic_name(user.name)}"
android:text= "@{@generic_name(user.name)}"
android:text= "@{@string/generic_name,user.name}"
질문 3
리스너 결합 표현식은 언제 평가되고 실행되나요?
LiveData
으로 보유한 데이터가 변경된 경우- 구성 변경으로 인해 활동이 다시 생성될 때
onClick()
와 같은 이벤트가 발생할 때- 활동이 백그라운드로 전환될 때
다음 강의 시작:
이 과정의 다른 Codelab 링크는 Android Kotlin 기초 Codelab 방문 페이지를 참고하세요.