이 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 레이아웃의 뷰는 UI 컨트롤러 (프래그먼트)를 사용하여 정보를 중계함으로써
ViewModel
객체와 간접적으로 통신합니다. 이 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 컨트롤러가 있으며, 이는 뷰와 ViewModel
사이의 릴레이 역할을 합니다.
예를 들면 다음과 같습니다.
- 확인 버튼은
game_fragment.xml
레이아웃 파일에서Button
뷰로 정의됩니다. - 사용자가 확인 버튼을 탭하면
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
을 데이터 바인딩에 전달합니다.
이렇게 하려면 이전 단계에서 선언한binding.gameViewModel
변수에viewModel
을 할당합니다.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
에서skip_button
에onClick
속성을 추가합니다. 바인딩 표현식을 정의하고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()}"
... />
GameViewModel
에서end_game_button
의 클릭 이벤트를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
에서play_again_button
에onClick
속성을 추가합니다. 리스너 바인딩을 정의하고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
관찰자 메서드를 사용하지 않고 LiveData
를 데이터 결합 소스로 사용하여 데이터 변경을 UI에 알리도록 GuessTheWord 앱을 변경합니다.
1단계: game_fragment.xml 파일에 단어 LiveData 추가
이 단계에서는 현재 단어 텍스트 뷰를 ViewModel
의 LiveData
객체에 직접 바인딩합니다.
game_fragment.xml
에서android:text
속성을word_text
텍스트 뷰에 추가합니다.
결합 변수 gameViewModel
를 사용하여 GameViewModel
의 LiveData
객체 word
로 설정합니다.
<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
속성을 점수 텍스트 뷰에 추가합니다.scoreViewModel.score
을text
속성에 할당합니다.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)}"
... />
word_text
와 유사하게score
텍스트 뷰에 서식을 지정합니다.game_fragment.xml
에서text
속성을score_text
텍스트 뷰에 추가합니다.%d
자리표시자로 표시되는 숫자 인수를 하나 사용하는 문자열 리소스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 방문 페이지를 참고하세요.