본문 바로가기
android/android kotlin fundamentals

05-3. Databinding with ViewModel and LiveData

by 유저혀 2021. 2. 18.
반응형

Android Kotlin Fundamentals는 codelab에 올라와있는 강의를 한글로 번역한 내용입니다.

 

Codelabs for Android Kotlin Fundamentals  |  Training Courses

Content and code samples on this page are subject to the licenses described in the Content License. Java is a registered trademark of Oracle and/or its affiliates. Last updated 2020-09-14 UTC.

developer.android.com

코드는 github를 참고해주세요

 

 

05-3. Databinding with ViewModel and LiveData


 

1. Add ViewModel data binding

 

  • 이전 단계에서 뷰에 액세스하는 방법으로 type-safe한 데이터 바인딩을 사용했다.
  • 그러나 data binding의 진정한 힘은 이름이 의미하는 대로 데이터를 앱의 뷰 객체에 직접 바인딩하는 것이다.

 

지금까지의 app architecture

앱에서 뷰는 XML 레이아웃으로 정의되며 해당 뷰의 데이터는 ViewModel 객체에 보관된다. 각 뷰와 해당 viewModel 사이에는 UI 컨트롤러가 있으며 이들 사이에 릴레이 역할을 한다.

 

 

  • Got It 버튼은 game_fragment.xml 레이아웃 파일에 Button view로 정의되어 있다
  • Got it 버튼을 사용자가 탭하면 GameFragment의 클릭 리스너가 GameViewModel의 해당 클릭 리스너를 호출한다
  • score는 GameViewModel에서 업데이트 된다.

Button 뷰와 GameViewModel은 직접적으로 커뮤니케이션 하지 않는다. 단지 GameFragment에 있는 클릭 리스너만 필요하다.

 

 

지금부터 사용 할 app architecture - ViewModel passed into the data binding

UI 컨트롤러를 매개체로 사용하지 않고 레이아웃의 뷰가 viewModel 객체의 데이터와 직접 통신하는 것이 더 간단하다.

 

ViewModel 객체는 앱의 모든 UI 데이터를 보유한다. ViewModel 객체를 데이터 바인딩으로 전달하면 view와 ViewModel 객체 간의 일부 통신을 자동화 할 수 있다.

 

이번 단계에서는 GameViewModel 및 ScoreViewModel 클래스를 해당 xml 레이아웃과 연결하고 클릭 이벤트를 처리할 리스너 바인딩도 설정한다.

 

 


 

Step1: Add data binding for the GameViewModel

Step 1에서는 GameViewModel과 그에 해당하는 layout 파일인 game_fragment.xml을 연결한다.

 

 

1) game_fragment.xml에 GameViewModel 타입 data-binding의 변수를 추가한다.

 

<layout ...>

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

 

 

2) GameFragment 파일에서 GameViewModel을 data binding에 넘긴다.

 

먼저 viewModel에 binding.gameViewModel 변수를 할당한다. viewModel 초기화 코드 이후에 이 코드를 onCreateView() 메소드 안에 넣는다.

 

// Set the viewmodel for databinding - this allows the bound layout access 
// to all the data in the ViewModel

binding.gameViewModel = viewModel

 

 

 

Step2: Use listener bindings for event handling

  • Listener binding은 onClick(), onZoomIn(), onZoomOut()과 같은 이벤트가 트리거 될 때 실행되는 바인딩 표현식이다
  • Listener binding은 람다 식으로 작성된다
  • 데이터 바인딩은 리스너를 작성하고 뷰에서 리스너를 설정한다
  • Listener binding은 그래들 플러그인 버전 2.0 이상부터 작용한다

이번 단계에서는 GameFragment에 있는 click listener를 game_fragment.xml 파일의 listener binding으로 변경한다.

 

 

1) game_fragment.xml을 열어서 skip_button에 onClick 속성을 추가한다.

 

binding 표현식을 정의하고 GameViewModel의 onSkip() 메소드를 호출한다. 이 바인딩 표현식을 리스너 바인딩이라고 한다.

 

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

 

 

2) 유사하게 correct_button에도 GameViewModel의 onCorrect() 이벤트를 연결한다.

 

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

 

 

3) end_game_button 역시 GameViewModel의 onGameFinish()와 연결한다.

 

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

 

 

4) GameFragment에서 setOnClickListner를 제거하고 클릭 리스너를 호출하는 기능을 제거한다.

 

기존 UI 컨트롤러에서 하던 작업은 뷰에서 data binding을 통해 처리하므로 아래 코드는 모두 제거한다.

 

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()
}

 

 

 

Step3: Add data binding for the ScoreViewModel

 

이번 단계에서는 ScoreViewModel을 score_fragment.xml과 연결시킨다.

 

1) score_fragment.xml 안에 ScoreViewModel 타입의 바인딩 변수를 추가한다. 이 단계는 위에서 진행했던 GameViewModel과 동일하다

 

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

 

 

2) core_fragment.xml에서 play_again_button에 onClick 속성을 추가한다. 리스너 바인딩은 ScoreViewModel의 onPlayAgain()으로 정의한다.

 

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

 

 

3) ScoreFragment의 onCreateView()에서 viewModel을 초기화하고 binding.scoreViewModel에 초기화한 viewModel을 넣는다.

 

viewModel = ...
binding.scoreViewModel = viewModel

 

 

4) 아래 코드를 삭제한다.

 

binding.playAgainButton.setOnClickListener { viewModel.onPlayAgain() }

 

 

5) 앱을 실행하면 앱은 이전과 같이 작동하지만 버튼 뷰가 직접 viewModel 객체와 통신한다.

 

더이상 ScoreFragment에서 버튼 클릭 핸들러를 통해 뷰와 통신하지 않는다.

 

 

Troubleshooting data-binding error message

  • 앱에서 데이터 바인딩을 사용할 때 컴파일 프로세스는 데이터 바인딩에 사용되는 중간 클래스를 생성한다.
  • 앱에서 에러가 발생할 수도 있는데, 이 에러는 안드로이드 스튜디오에서 앱 컴파일 시 감지할 수 없어 code 상 warning이나 빨간색으로 밑줄이 생기지 않는다.
  • 그러나 컴파일 시 생성된 중간 클래스에서는 에러가 발생한다.


만약 이런 에러가 발생하면

  • 안드로이드 스튜디오의 Build 패널을 유심히 살펴보면 data binding에러가 어떤 databinding에서 발생했는지 알 수 있다
  • 레이아웃 XML 파일에서 데이터 바인딩을 사용하는 onClick 속성의 오류를 확인해보고 람다식이 호출하는 함수가 실제로 존재하는지 살펴본다
  • <data> 영역에서 data-binding 변수의 스펠링을 체크해본다.

 

예를 들어 아래 리스너에서 onCorrect() 함수의 스펠링이 틀린 것을 확인해볼 수 있다.

 

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

 

 

또한 XML 파일에서 영역 중 gameViewModel의 스펠링 철자 틀림으로 인해 오류가 발생할 수도 있다.

 

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

 

 

안드로이드 스튜디오는 앱을 컴파일 할 때까지 에러를 발견할 수 없으며, 컴파일 중에 아래와 같은 에러 메세지를 확인할 수 있다.

 

error: cannot find symbol
import com.example.android.guesstheword.databinding.GameFragmentBindingImpl"

symbol:   class GameFragmentBindingImpl
location: package com.example.android.guesstheword.databinding

 

 

 


 

2. Add LiveData to data binding

  • 데이터 바인딩은 viewModel 객체와 함께 사용되는 LiveData와도 잘 동작한다.
  • 이번 단계에서는 LiveData의 observer 메소드를 사용하지 않고 LiveData를 데이터 바인딩 소스로 사용하여 UI에 데이터 바인딩 변경 사항을 알리도록 수정한다.

 

Step1: Add word LiveData to the game_fragment.xml file

Step1에서는 word 텍스트 뷰를 viewModel에 있는 LiveData 객체에 직접 바인딩한다.

 

 

1) game_fragment.xml 열어서 word_text 텍스트 뷰에 android:text 속성을 추가한다.

 

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

 

word.value를 사용할 필요는 없고 대신 실제 LiveData 객체를 사용한다. LiveData 객체는 word의 실제 값을 표시하고 만약 word가 null이면 LiveData 개체는 빈 문자열을 표시한다.

 

 

 

2) GameFragment의 onCreateView()에서 gameViewModel 초기화 코드 이후에 binding 변수의 lifecycle owner를 현재 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

 

 

3) GameFragment에서 LiveData word의 observer를 제거한다.

삭제해야 할 코드는 아래와 같다.

 

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

 

 

4) 앱을 실행시켜 게임을 실행한다. UI 컨트롤러 내에 observer 메소드 없이도 current word가 업데이트 되는 것을 확인할 수 있다

 

 

 

Step2: Add score LiveData to the score_fragment.xml file

 

이번 단계에서는 score fragment의 text view에 LiveData score를 바인딩한다.

 

 

1) score_fragment.xml의 score text view에 android:text 속성을 추가한다. ScoreViewModel.score를 text 속성에 할당한다. score는 integer이므로 String.valueOf()를 써서 string으로 변환한다.

 

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

 

 

2) scoreFragment에서 ScoreViewModel을 초기화 한 코드 다음에 현재 activity를 바인딩 변수의 lifeCycleOwner로 설정한다.

 

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

 

 

3) ScoreFragment에서 score 값을 observer하는 코드를 제거한다

// Add observer for score
viewModel.score.observe(this, Observer { newScore ->
   binding.scoreText.text = newScore.toString()
})

 

 

4) 앱을 실행시켜서 ScoreFragment의 score가 나오는지 확인한다.

 

 

 

 

Step3: Add string formatting with data binding

레이아웃에서 data binding과 함께 string formatter도 추가할 수 있다. 이 단계에서는 current word를 큰 따옴표를 붙인 형식으로 변경해보고 점수에 'current score'라는 접두사를 추가해본다.

 

 

 

1) string.xml에 아래와 같이 문자열을 추가한다.

 

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

 

 

2) game_fragment.xml에서 word_text 텍스트 뷰의 text 속성을 string.xml의 quote_format을 사용하도록 변경한다. gameModelView.word를 인자로 넘기면 string formatting도 진행된다.

 

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

 

 

3) score_text 텍스트 뷰의 text도 score_format formatting을 적용한 score 값으로 변경한다

 

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

 

 

4) GameFragment의 onCreateView() 에서 score를 observe하는 코드를 지운다.

 

viewModel.score.observe(this, Observer { newScore ->
   binding.scoreText.text = newScore.toString()
})

 

 

5) 앱을 실행시켜서 formatting이 잘 적용되었는지 확인한다.

728x90

'android > android kotlin fundamentals' 카테고리의 다른 글

06-1. Room Database  (0) 2021.02.21
05-4. LiveData Transformations  (0) 2021.02.21
05-2. LiveData and LiveDataObservers  (0) 2021.02.18
05-1. ViewModel and ViewModelFactory  (0) 2021.02.17
04-2. Complex LIfecycle  (0) 2021.02.17

댓글