ViewModel과 함께 LiveData 사용하기

1. 시작하기 전에

이전 Codelab에서는 ViewModel을 사용하여 앱 데이터를 저장하는 방법을 알아봤습니다. ViewModel을 사용하면 구성 변경 후에도 앱의 데이터가 유지됩니다. 이 Codelab에서는 LiveDataViewModel의 데이터와 통합하는 방법을 알아봅니다.

LiveData 클래스는 Android 아키텍처 구성요소의 일부이기도 하며 관찰 가능한 데이터 홀더 클래스입니다.

기본 요건

  • GitHub에서 소스 코드를 다운로드하여 Android 스튜디오에서 여는 방법
  • Kotlin에서 활동과 프래그먼트를 사용하여 기본 Android 앱을 만들고 실행하는 방법
  • 활동 및 프래그먼트 수명 주기 작동 방식
  • ViewModel을 사용하여 기기 구성 변경 시에도 UI 데이터를 유지하는 방법
  • 람다 표현식을 작성하는 방법

학습할 내용

  • 앱에서 LiveDataMutableLiveData를 사용하는 방법
  • ViewModel에 저장된 데이터를 LiveData로 캡슐화하는 방법
  • LiveData.에서 변경사항을 관찰하는 관찰자 메서드를 추가하는 방법
  • 레이아웃 파일에서 결합 표현식을 작성하는 방법

빌드할 항목

  • Unscramble 앱에서 LiveData를 앱의 데이터(단어, 단어 수, 점수)로 사용합니다.
  • 데이터가 변경되면 알림을 받는 관찰자 메서드를 추가하고 글자가 뒤섞인 단어 텍스트 뷰를 자동으로 업데이트합니다.
  • 기본 LiveData가 변경되면 트리거되는 결합 표현식을 레이아웃 파일에 작성합니다. 점수, 단어 수, 글자가 뒤섞인 단어의 텍스트 뷰가 자동으로 업데이트됩니다.

필요한 항목

  • Android 스튜디오가 설치된 컴퓨터
  • 이전 Codelab의 솔루션 코드(ViewModel을 사용한 Unscramble 앱)

이 Codelab용 시작 코드 다운로드하기

이 Codelab에서는 이전 Codelab에서 빌드한 Unscramble 앱(ViewModel에 데이터 저장)을 시작 코드로 사용합니다.

2. 시작 앱 개요

이 Codelab은 이전 Codelab에서 다룬 바 있는 Unscramble 솔루션 코드를 사용합니다. 이 앱은 글자가 섞인 단어를 표시하여 플레이어가 단어를 추측하도록 합니다. 플레이어는 횟수에 제한 없이 시도하여 올바른 단어를 추측할 수 있습니다. 현재 단어, 플레이어의 점수, 단어 수와 같은 앱 데이터는 ViewModel에 저장됩니다. 하지만 새로운 점수 및 단어 수 값이 앱의 UI에 반영되지 않습니다. 이 Codelab에서는 LiveData를 사용하여 누락된 기능을 구현합니다.

a20e6e45e0d5dc6f.png

3. Livedata란?

LiveData는 수명 주기를 인식하는 관찰 가능한 데이터 홀더 클래스입니다.

LiveData의 특성은 다음과 같습니다.

  • LiveData는 데이터를 보유합니다. LiveData는 모든 유형의 데이터에 사용할 수 있는 래퍼입니다.
  • LiveData는 관찰 가능합니다. 즉, LiveData 객체에서 보유한 데이터가 변경되면 관찰자에 알림이 제공됩니다.
  • LiveData는 수명 주기를 인식합니다. LiveData에 관찰자를 연결하면 관찰자는 LifecycleOwner(일반적으로 활동 또는 프래그먼트)와 연결됩니다. LiveDataSTARTED 또는 RESUMED 같은 활성 수명 주기 상태인 관찰자만 업데이트합니다. LiveData 및 관찰에 관한 자세한 내용은 여기에서 알아볼 수 있습니다.

시작 코드의 UI 업데이트

UI에 글자가 뒤섞인 새로운 단어를 표시하려고 할 때마다 시작 코드에서 updateNextWordOnScreen() 메서드가 명시적으로 호출됩니다. 게임이 초기화되는 동안이나 플레이어가 Submit 또는 Skip 버튼을 눌렀을 때 이 메서드를 호출합니다. 이 메서드는 onViewCreated(), restartGame(), onSkipWord(), onSubmitWord() 메서드에서 호출됩니다. Livedata를 사용하면 UI를 업데이트하기 위해 여러 위치에서 이 메서드를 호출할 필요 없이 관찰자에서 한 번만 호출하면 됩니다.

4. 글자가 뒤섞인 현재 단어에 LiveData 추가하기

이 작업에서는 GameViewModel의 현재 단어를 LiveData로 변환하여 LiveData,로 데이터를 래핑하는 방법을 알아봅니다. 이후 작업에서는 이 LiveData 객체에 관찰자를 추가하고 LiveData를 관찰하는 방법을 알아봅니다.

MutableLiveData

MutableLiveData는 변경 가능한 버전의 LiveData입니다. 즉, 내부에 저장된 데이터의 값을 변경할 수 있습니다.

  1. GameViewModel에서 변수 _currentScrambledWord의 유형을 MutableLiveData<String>으로 변경합니다. LiveDataMutableLiveData는 일반 클래스이므로 이러한 클래스에 보유되는 데이터의 유형을 지정해야 합니다.
  2. LiveData/MutableLiveData 객체의 값은 동일하게 유지되고 이 객체에 저장된 데이터만 변경되기 때문에 _currentScrambledWord의 변수 유형을 val로 변경합니다.
private val _currentScrambledWord = MutableLiveData<String>()
  1. 지원 필드 currentScrambledWord를 변경할 수 없기 때문에 그 유형을 LiveData<String>으로 변경합니다. Android 스튜디오에서 몇 가지 오류가 표시되며, 이 오류를 다음 단계에서 수정합니다.
val currentScrambledWord: LiveData<String>
   get() = _currentScrambledWord
  1. LiveData 객체 내의 데이터에 액세스하려면 value 속성을 사용합니다. getNextWord() 메서드의 GameViewModel에서 else 블록 내에 있는 _currentScrambledWord의 참조를 _currentScrambledWord.value로 변경합니다.
private fun getNextWord() {
 ...
   } else {
       _currentScrambledWord.value = String(tempWord)
       ...
   }
}

5. LiveData 객체에 관찰자 연결하기

이 작업에서는 앱 구성요소 GameFragment에서 관찰자를 설정합니다. 추가할 관찰자는 앱 데이터 currentScrambledWord의 변경사항을 관찰합니다. LiveData는 수명 주기를 인식합니다. 즉, 활성 수명 주기 상태인 관찰자만 업데이트합니다. 따라서 GameFragment의 관찰자는 GameFragmentSTARTED 또는 RESUMED 상태인 경우에만 알림을 받습니다.

  1. GameFragment에서 메서드 updateNextWordOnScreen() 및 이 메서드의 모든 호출을 삭제합니다. 여기서는 LiveData에 관찰자를 연결하므로 이 메서드가 필요하지 않습니다.
  2. onSubmitWord()에서 빈 if-else 블록을 다음과 같이 수정합니다. 완성된 메서드는 다음과 같습니다.
private fun onSubmitWord() {
    val playerWord = binding.textInputEditText.text.toString()

    if (viewModel.isUserWordCorrect(playerWord)) {
        setErrorTextField(false)
        if (!viewModel.nextWord()) {
            showFinalScoreDialog()
        }
    } else {
        setErrorTextField(true)
    }
}
  1. currentScrambledWord LiveData의 관찰자를 연결합니다. GameFragmentonViewCreated() 콜백 끝에서 currentScrambledWord에 관해 observe() 메서드를 호출합니다.
// Observe the currentScrambledWord LiveData.
viewModel.currentScrambledWord.observe()

Android 스튜디오에 누락된 매개변수에 관한 오류가 표시됩니다. 다음 단계에서 오류를 수정합니다.

  1. viewLifecycleOwner를 첫 번째 매개변수로 observe() 메서드에 전달합니다. viewLifecycleOwner프래그먼트의 뷰 수명 주기를 나타냅니다. 이 매개변수를 사용하면 LiveDataGameFragment 수명 주기를 인식하고 GameFragment가 활성 상태(STARTED 또는 RESUMED)일 때만 관찰자에 알릴 수 있습니다.
  2. newWord를 함수 매개변수로 사용하여 두 번째 매개변수로 람다를 추가합니다. newWord에는 글자가 뒤섞인 새 단어 값이 포함됩니다.
// Observe the scrambledCharArray LiveData, passing in the LifecycleOwner and the observer.
viewModel.currentScrambledWord.observe(viewLifecycleOwner,
   { newWord ->
   })

람다 표현식은 선언되지 않지만 즉시 표현식으로 전달되는 익명 함수입니다. 람다 표현식은 항상 중괄호 { }로 묶습니다.

  1. 람다 표현식의 함수 본문에서 newWord를 글자가 뒤섞인 단어 텍스트 뷰에 할당합니다.
// Observe the scrambledCharArray LiveData, passing in the LifecycleOwner and the observer.
viewModel.currentScrambledWord.observe(viewLifecycleOwner,
   { newWord ->
       binding.textViewUnscrambledWord.text = newWord
   })
  1. 앱을 컴파일하고 실행합니다. 게임 앱이 이전과 똑같이 작동하지만 이제는 글자가 뒤섞인 단어 텍스트 뷰가 updateNextWordOnScreen() 메서드가 아닌 LiveData 관찰자에서 자동으로 업데이트됩니다.

6. 점수 및 단어 수에 관찰자 연결하기

앞의 작업과 마찬가지로 이 작업에서는 LiveData를 앱의 다른 데이터, 점수, 단어 수에 추가하여 게임 중에 UI에 점수와 단어 수의 값이 올바르게 업데이트되도록 합니다.

1단계: LiveData로 점수 및 단어 수 래핑하기

  1. GameViewModel에서 _score_currentWordCount 클래스 변수의 유형을 val로 변경합니다.
  2. _score 변수와 _currentWordCount 변수의 데이터 유형을 MutableLiveData로 변경하고 변수를 0으로 초기화합니다.
  3. 지원 필드 유형을 LiveData<Int>로 변경합니다.
private val _score = MutableLiveData(0)
val score: LiveData<Int>
   get() = _score

private val _currentWordCount = MutableLiveData(0)
val currentWordCount: LiveData<Int>
   get() = _currentWordCount
  1. GameViewModelreinitializeData() 메서드 시작 부분에서 _score_currentWordCount의 참조를 각각 _score.value_currentWordCount.value로 변경합니다.
fun reinitializeData() {
   _score.value = 0
   _currentWordCount.value = 0
   wordsList.clear()
   getNextWord()
}
  1. GameViewModelnextWord() 메서드 내에서 _currentWordCount의 참조를 _currentWordCount.value!!로 변경합니다.
fun nextWord(): Boolean {
    return if (_currentWordCount.value!! < MAX_NO_OF_WORDS) {
           getNextWord()
           true
       } else false
   }
  1. GameViewModelincreaseScore() 메서드와 getNextWord() 메서드 내에서 _score_currentWordCount의 참조를 각각 _score.value_currentWordCount.value로 변경합니다. Android 스튜디오에서 오류가 표시됩니다. _score가 더 이상 정수가 아니고 LiveData이기 때문입니다. 다음 단계에서 이 오류를 수정합니다.
  2. Kotlin 함수 plus()를 사용하여 _score 값을 늘립니다. 그러면 null에 안전하게 덧셈이 처리됩니다.
private fun increaseScore() {
    _score.value = (_score.value)?.plus(SCORE_INCREASE)
}
  1. 마찬가지로 Kotlin 함수 inc()를 사용하여 null에 안전하게 값을 1씩 증분합니다.
private fun getNextWord() {
   ...
    } else {
        _currentScrambledWord.value = String(tempWord)
        _currentWordCount.value = (_currentWordCount.value)?.inc()
        wordsList.add(currentWord)
       }
   }
  1. GameFragment에서 value 속성을 사용하여 score의 값에 액세스합니다. showFinalScoreDialog() 메서드 내에서 viewModel.scoreviewModel.score.value로 변경합니다.
private fun showFinalScoreDialog() {
   MaterialAlertDialogBuilder(requireContext())
       .setTitle(getString(R.string.congratulations))
       .setMessage(getString(R.string.you_scored, viewModel.score.value))
       ...
       .show()
}

2단계: 관찰자를 점수 및 단어 수에 연결하기

앱에서 점수와 단어 수가 업데이트되지 않습니다. 이 작업에서 LiveData 관찰자를 사용하여 업데이트합니다.

  1. GameFragmentonViewCreated() 메서드 내에서 점수 및 단어 수 텍스트 뷰를 업데이트하는 코드를 삭제합니다.

다음을 삭제합니다.

binding.score.text = getString(R.string.score, 0)
binding.wordCount.text = getString(R.string.word_count, 0, MAX_NO_OF_WORDS)
  1. GameFragmentonViewCreated() 메서드 끝부분에 score의 관찰자를 연결합니다. viewLifecycleOwner를 첫 번째 매개변수로 관찰자에 전달하고 두 번째 매개변수용으로 람다 표현식을 전달합니다. 람다 표현식 내에서 새 점수를 매개변수로 전달하고 함수 본문 내에서 새 점수를 텍스트 뷰로 설정합니다.
viewModel.score.observe(viewLifecycleOwner,
   { newScore ->
       binding.score.text = getString(R.string.score, newScore)
   })
  1. onViewCreated() 메서드 끝부분에 currentWordCount LiveData의 관찰자를 연결합니다. viewLifecycleOwner를 첫 번째 매개변수로 관찰자에 전달하고 두 번째 매개변수용으로 람다 표현식을 전달합니다. 람다 표현식 내에서 새 단어 수를 매개변수로 전달하고 함수 본문에서 MAX_NO_OF_WORDS와 함께 새 단어 수를 텍스트 뷰로 설정합니다.
viewModel.currentWordCount.observe(viewLifecycleOwner,
   { newWordCount ->
       binding.wordCount.text =
           getString(R.string.word_count, newWordCount, MAX_NO_OF_WORDS)
   })

ViewModel 내에서 수명 주기 소유자의 전체 기간(즉, GameFragment) 중에 점수와 단어 수의 값이 변경되면 새 관찰자가 트리거됩니다.

  1. 앱을 실행하여 결과를 확인합니다. 단어 몇 개로 게임을 플레이해봅니다. 점수와 단어 수도 화면에 올바르게 업데이트됩니다. 코드의 일부 조건에 따라 이러한 텍스트 뷰가 업데이트되지 않는지 확인합니다. scorecurrentWordCountLiveData이며 기본 값이 변경되면 상응하는 관찰자가 자동으로 호출됩니다.

80e118245bdde6df.png

7. 데이터 결합과 함께 LiveData 사용하기

이전 작업에서는 앱이 코드의 데이터 변경사항을 수신합니다. 마찬가지로, 앱은 레이아웃에서 데이터 변경사항을 수신할 수 있습니다. 데이터 결합을 사용하면 관찰 가능한 LiveData 값이 변경될 때 바인딩된 레이아웃의 UI 요소에도 알림이 전송되며 레이아웃 내에서 UI를 업데이트할 수 있습니다.

개념: 데이터 결합

이전 Codelab에서는 단방향 결합인 뷰 결합을 확인했습니다. 뷰를 코드에 바인딩할 수 있지만 코드를 뷰에 바인딩할 수는 없습니다.

뷰 결합에 관한 복습:

뷰 결합은 코드에서 뷰에 더 쉽게 액세스할 수 있는 기능으로, 각 XML 레이아웃 파일의 결합 클래스를 생성합니다. 결합 클래스의 인스턴스에는 상응하는 레이아웃에 ID가 있는 모든 뷰의 직접 참조가 포함됩니다. 예를 들어 Unscramble 앱은 현재 뷰 결합을 사용하므로 생성된 결합 클래스를 사용하여 코드에서 뷰를 참조할 수 있습니다.

예:

binding.textViewUnscrambledWord.text = newWord
binding.score.text = getString(R.string.score, newScore)
binding.wordCount.text =
                  getString(R.string.word_count, newWordCount, MAX_NO_OF_WORDS)

뷰 결합을 사용하면 뷰(레이아웃 파일)에서 앱 데이터를 참조할 수 없습니다. 이 작업은 데이터 결합을 사용하면 됩니다.

데이터 결합

데이터 결합 라이브러리는 Android Jetpack 라이브러리의 일부입니다. 데이터 결합은 선언적 형식을 사용하여 레이아웃의 UI 구성요소를 앱의 데이터 소스에 결합합니다. 이에 관해서는 Codelab의 후반에서 알아봅니다.

간단히 말해서 데이터 결합은 코드에서 데이터를 뷰 + 뷰 결합에 결합(뷰를 코드에 결합)하는 것입니다.

UI 컨트롤러에서 뷰 결합 사용의 예

binding.textViewUnscrambledWord.text = viewModel.currentScrambledWord

레이아웃 파일에서 데이터 결합 사용의 예

android:text="@{gameViewModel.currentScrambledWord}"

위의 예에서는 데이터 결합 라이브러리를 사용하여 레이아웃 파일에서 뷰/위젯에 직접 앱 데이터를 할당하는 방법을 보여줍니다. 할당 표현식에 사용되는 @{} 구문에 유의하세요.

데이터 결합을 사용할 때 주요 이점은 활동에서 많은 UI 프레임워크 호출을 삭제할 수 있어 파일이 더욱 단순해지고 더 손쉬운 유지관리가 가능하다는 점입니다. 또한 앱 성능이 향상되며 메모리 누수 및 null 포인터 예외를 방지할 수 있습니다.

1단계: 뷰 결합을 데이터 결합으로 변경하기

  1. build.gradle(Module) 파일의 buildFeatures 섹션에서 dataBinding 속성을 사용 설정합니다.

다음을

buildFeatures {
   viewBinding = true
}

다음으로 바꿉니다.

buildFeatures {
   dataBinding = true
}

Android 스튜디오에서 메시지가 표시되 Gradle 동기화를 실행합니다.

  1. Kotlin 프로젝트에서 데이터 결합을 사용하려면 kotlin-kapt 플러그인을 적용해야 합니다. 이 단계는 build.gradle(Module) 파일에서 이미 완료되어 있습니다.
plugins {
   id 'com.android.application'
   id 'kotlin-android'
   id 'kotlin-kapt'
}

위의 단계는 앱의 모든 레이아웃 XML 파일용 결합 클래스를 자동으로 생성합니다. 레이아웃 파일 이름이 activity_main.xml인 경우 자동 생성 클래스의 이름은 ActivityMainBinding이 됩니다.

2단계: 레이아웃 파일을 데이터 결합 레이아웃으로 변환하기

데이터 결합 레이아웃 파일은 약간 차이가 있으며 <layout>의 루트 태그로 시작하고 선택적 <data> 요소 및 view 루트 요소가 뒤따릅니다. 이 view 요소는 루트가 결합 레이아웃 파일이 아닌 파일에 있는 요소입니다.

  1. game_fragment.xml을 열고 code 탭을 선택합니다.
  2. 레이아웃을 데이터 결합 레이아웃으로 변환하려면 루트 요소를 <layout> 태그로 래핑합니다. 또한 네임스페이스 정의(xmlns:로 시작하는 속성)를 새 루트 요소로 이동해야 합니다. 루트 요소 위에 있는 <layout> 태그 내부에 <data></data> 태그를 추가합니다. Android 스튜디오에서는 루트 요소(ScrollView)를 마우스 오른쪽 버튼으로 클릭하고 Show Context Actions > Convert to data binding layout을 선택하면 자동으로 간편하게 추가할 수 있습니다.

8d48f58c2bdccb52.png

  1. 레이아웃은 다음과 같이 표시됩니다.
<layout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools">

   <data>

   </data>

   <ScrollView
       android:layout_width="match_parent"
       android:layout_height="match_parent">

       <androidx.constraintlayout.widget.ConstraintLayout
         ...
       </androidx.constraintlayout.widget.ConstraintLayout>
   </ScrollView>
</layout>
  1. GameFragmentonCreateView() 메서드 시작 부분에서 데이터 결합을 사용하도록 binding 변수의 인스턴스화를 변경합니다.

바꾸기

binding = GameFragmentBinding.inflate(inflater, container, false)

아래와 같이 바꿉니다.

binding = DataBindingUtil.inflate(inflater, R.layout.game_fragment, container, false)
  1. 코드를 컴파일합니다. 문제없이 컴파일할 수 있습니다. 이제 앱이 데이터 결합을 사용하고 레이아웃의 뷰가 앱 데이터에 액세스할 수 있습니다.

8. 데이터 결합 변수 추가하기

이 작업에서는 레이아웃 파일에서 viewModel의 앱 데이터에 액세스하도록 속성을 추가합니다. 코드에서 레이아웃 변수를 초기화합니다.

  1. game_fragment.xml<data> 태그 내에 <variable>이라는 하위 태그를 추가하고 GameViewModel 유형의 gameViewModel이라는 속성을 선언합니다. 이 속성을 사용하여 ViewModel의 데이터를 레이아웃에 결합할 수 있습니다.
<data>
   <variable
       name="gameViewModel"
       type="com.example.android.unscramble.ui.game.GameViewModel" />
</data>

gameViewModel 유형에 패키지 이름이 포함되어 있습니다. 이 패키지 이름이 앱의 패키지 이름과 일치하는지 확인하세요.

  1. gameViewModel 선언 아래에서 <data> 태그 내부에 Integer 유형의 다른 변수를 추가하고 이 변수의 이름을 maxNoOfWords로 지정합니다. 이 변수는 ViewModel의 변수에 바인딩하여 게임별 단어 수를 저장하는 데 사용합니다.
<data>
   ...
   <variable
       name="maxNoOfWords"
       type="int" />
</data>
  1. GameFragmentonViewCreated() 메서드 시작 부분에서 레이아웃 변수 gameViewModelmaxNoOfWords를 초기화합니다.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   super.onViewCreated(view, savedInstanceState)

   binding.gameViewModel = viewModel

   binding.maxNoOfWords = MAX_NO_OF_WORDS
...
}
  1. LiveData는 수명 주기를 인식하며 관찰 가능합니다. 따라서 레이아웃에 수명 주기 소유자를 전달해야 합니다. GameFragmentonViewCreated() 메서드 내에서 결합 변수 초기화 아래에 다음 코드를 추가합니다.
   // 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

LiveData 관찰자를 구현할 때 유사한 기능을 구현했습니다. viewLifecycleOwner를 매개변수 중 하나로 LiveData 관찰자에 전달했습니다.

9. 결합 표현식 사용하기

결합 표현식은 레이아웃 내에서 레이아웃 속성을 참조하는 속성(attribute properties)(예: android:text)에서 작성됩니다. 레이아웃 속성은 <variable> 태그를 통해 데이터 결합 레이아웃 파일의 상단에서 선언됩니다. 종속 변수 중 하나라도 변경되면 'DB 라이브러리'가 결합 표현식을 실행하고 이에 따라 뷰를 업데이트합니다. 이러한 변경 감지는 데이터 결합 라이브러리를 사용할 때 무료로 제공되는 훌륭한 최적화 기능입니다.

결합 표현식의 구문

결합 표현식은 @ 기호로 시작하고 중괄호 {}로 래핑됩니다. 다음 예에서 TextView 텍스트는 user 변수의 firstName 속성으로 설정됩니다.

예:

<TextView android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:text="@{user.firstName}" />

1단계: 현재 단어에 결합 표현식 추가하기

이 단계에서는 현재 단어 텍스트 뷰를 ViewModelLiveData 객체에 바인딩합니다.

  1. game_fragment.xml에서 text 속성을 textView_unscrambled_word 텍스트 뷰에 추가합니다. 새 레이아웃 변수 gameViewModel을 사용하고 text 속성에 @{gameViewModel.currentScrambledWord}를 할당합니다.
<TextView
   android:id="@+id/textView_unscrambled_word"
   ...
   android:text="@{gameViewModel.currentScrambledWord}"
   .../>
  1. GameFragment에서 currentScrambledWordLiveData 관찰자 코드를 삭제합니다. 프래그먼트에 더 이상 관찰자 코드가 필요하지 않습니다. LiveData 변경사항 업데이트가 레이아웃에 직접 수신됩니다.

삭제:

viewModel.currentScrambledWord.observe(viewLifecycleOwner,
   { newWord ->
       binding.textViewUnscrambledWord.text = newWord
   })
  1. 앱을 실행하면 앱이 전과 동일하게 작동합니다. 하지만 이제 글자가 뒤섞인 단어 텍스트 뷰는 LiveData 관찰자가 아닌 결합 표현식을 사용하여 UI를 업데이트합니다.

2단계: 점수 및 단어 수에 결합 표현식 추가하기

데이터 결합 표현식의 리소스

데이터 결합 표현식은 다음 구문을 사용하여 앱 리소스를 참조할 수 있습니다.

예:

android:padding="@{@dimen/largePadding}"

위의 예에서는 padding 속성에 dimen.xml 리소스 파일의 largePadding 값이 할당됩니다.

레이아웃 속성을 리소스 매개변수로 전달할 수도 있습니다.

예:

android:text="@{@string/example_resource(user.lastName)}"

strings.xml

<string name="example_resource">Last Name: %s</string>

위의 예에서 example_resource%s 자리표시자가 있는 문자열 리소스입니다. user.lastName을 결합 표현식의 리소스 매개변수로 전달합니다. 여기서 user는 레이아웃 변수입니다.

이 단계에서는 점수 및 단어 수 텍스트 뷰에 결합 표현식을 추가하고 리소스 매개변수를 전달합니다. 이 단계는 위의 textView_unscrambled_word에 실행한 과정과 유사합니다.

  1. game_fragment.xml에서 다음 결합 표현식을 사용하여 word_count 텍스트 뷰의 text 속성을 업데이트합니다. word_count 문자열 리소스를 사용하고 gameViewModel.currentWordCountmaxNoOfWords를 리소스 매개변수로 사용합니다.
<TextView
   android:id="@+id/word_count"
   ...
   android:text="@{@string/word_count(gameViewModel.currentWordCount, maxNoOfWords)}"
   .../>
  1. 다음 결합 표현식을 사용하여 score 텍스트 뷰의 text 속성을 업데이트합니다. score 문자열 리소스를 사용하고 gameViewModel.score를 리소스 매개변수로 전달합니다.
<TextView
   android:id="@+id/score"
   ...
   android:text="@{@string/score(gameViewModel.score)}"
   ... />
  1. GameFragment에서 LiveData 관찰자를 삭제합니다. 상응하는 LiveData가 변경되면 결합 표현식이 UI를 업데이트하므로 이 관찰자는 더 이상 필요하지 않습니다.

다음을 삭제합니다.

viewModel.score.observe(viewLifecycleOwner,
   { newScore ->
       binding.score.text = getString(R.string.score, newScore)
   })

viewModel.currentWordCount.observe(viewLifecycleOwner,
   { newWordCount ->
       binding.wordCount.text =
           getString(R.string.word_count, newWordCount, MAX_NO_OF_WORDS)
   })
  1. 앱을 실행하고 몇 단어를 플레이해봅니다. 이제 코드는 LiveData와 결합 표현식을 사용하여 UI를 업데이트합니다.

7880e60dc0a6f95c.png 9ef2fdf21ffa5c99.png

축하합니다. LiveData 관찰자와 함께 LiveData를 사용하고 및 결합 표현식과 함께 LiveData를 사용하는 방법을 알아보았습니다.

10. TalkBack을 사용 설정한 상태에서 Unscramble 앱 테스트하기

이 과정을 전체적으로 진행하면서 가능한 한 많은 사용자가 액세스할 수 있는 앱을 빌드하려고 합니다. 일부 사용자는 TalkBack을 사용하여 앱에 액세스하고 탐색할 수도 있습니다. TalkBack은 Android 기기에 포함된 Google 스크린 리더입니다. TalkBack에서 음성 피드백을 제공하므로 화면을 보지 않고 기기를 사용할 수 있습니다.

TalkBack을 사용 설정한 상태에서 플레이어가 게임을 플레이할 수 있는지 확인합니다.

  1. 안내에 따라 기기에서 TalkBack을 사용 설정합니다.
  2. Unscramble 앱으로 돌아갑니다.
  3. 안내에 따라 TalkBack을 통해 앱을 탐색합니다. 오른쪽으로 스와이프하여 화면 요소를 순서대로 탐색하고 왼쪽으로 스와이프하여 반대 방향으로 이동합니다. 아무 곳이나 두 번 탭하여 선택합니다. 스와이프 동작으로 앱의 모든 요소에 도달할 수 있는지 확인합니다.
  4. TalkBack 사용자가 화면의 각 항목으로 이동할 수 있는지 확인합니다.
  5. TalkBack에서 글자가 뒤섞인 단어를 단어로 읽으려고 시도합니다. 실제 단어가 아니기 때문에 플레이어에게 혼란스러울 수도 있습니다.
  6. 사용자 경험을 향상하려면 TalkBack에서 글자가 뒤섞인 단어의 개별 문자를 소리 내서 읽도록 합니다. GameViewModel 내에서 글자가 뒤섞인 단어 StringSpannable 문자열로 변환합니다. spannable 문자열은 추가 정보가 연결된 문자열입니다. 이 경우에는 이 문자열을 TYPE_VERBATIMTtsSpan에 연결하여 글자가 뒤섞인 verbatim이라는 단어를 텍스트 음성 변환 엔진이 한 글자씩 소리 내서 읽도록 합니다.
  7. GameViewModel에서 다음 코드를 사용하여 currentScrambledWord 변수가 선언되는 방식을 수정합니다.
val currentScrambledWord: LiveData<Spannable> = Transformations.map(_currentScrambledWord) {
    if (it == null) {
        SpannableString("")
    } else {
        val scrambledWord = it.toString()
        val spannable: Spannable = SpannableString(scrambledWord)
        spannable.setSpan(
            TtsSpan.VerbatimBuilder(scrambledWord).build(),
            0,
            scrambledWord.length,
            Spannable.SPAN_INCLUSIVE_INCLUSIVE
        )
        spannable
    }
}

이 변수는 이제 LiveData<String>이 아닌 LiveData<Spannable>입니다. 이 변수가 작동하는 방식을 세부적으로 모두 이해할 필요는 없습니다. 간단히 설명하면, LiveData 변환을 사용하여 접근성 서비스에서 글자가 뒤섞인 현재 단어 String을 적절하게 처리할 수 있는 Spannable 문자열로 변환합니다. 다음 Codelab에서는 LiveData 변환에 관해 자세히 알아봅니다. 이 변환에서는 상응하는 LiveData의 값에 따라 다른 LiveData 인스턴스를 반환할 수 있습니다.

  1. Unscramble 앱을 실행하고 TalkBack을 사용하여 앱을 탐색합니다. 이제 TalkBack에서 글자가 뒤섞인 단어의 개별 글자를 읽습니다.

앱의 접근성을 높이는 방법을 자세히 알아보려면 이 원칙을 확인하세요.

11. 사용하지 않는 코드 삭제하기

솔루션 코드에서 불량 코드, 미사용 코드, 원치 않는 코드를 삭제하는 것이 좋습니다. 그러면 코드를 쉽게 유지관리할 수 있고 신규 팀원이 코드를 더 쉽게 이해할 수도 있습니다.

  1. GameFragment에서 getNextScrambledWord() 메서드와 onDetach() 메서드를 삭제합니다.
  2. GameViewModel에서 onCleared() 메서드를 삭제합니다.
  3. 소스 파일 맨 위에서 사용되지 않는 가져오기를 삭제합니다. 회색으로 표시됩니다.

로그 구문이 더 이상 필요하지 않으며 원하는 경우 코드에서 삭제할 수 있습니다.

  1. (선택사항) 소스 파일(GameFragment.ktGameViewModel.kt)에서 이전 Codelab에서 ViewModel 수명 주기를 이해하기 위해 추가한 Log 문을 삭제합니다.

12. 솔루션 코드

이 Codelab의 솔루션 코드는 아래 표시된 프로젝트에 있습니다.

  1. 프로젝트에 제공된 GitHub 저장소 페이지로 이동합니다.
  2. 브랜치 이름이 Codelab에 지정된 브랜치 이름과 일치하는지 확인합니다. 예를 들어 다음 스크린샷에서 브랜치 이름은 main입니다.

1e4c0d2c081a8fd2.png

  1. 프로젝트의 GitHub 페이지에서 Code 버튼을 클릭하여 팝업을 엽니다.

1debcf330fd04c7b.png

  1. 팝업에서 Download ZIP 버튼을 클릭하여 컴퓨터에 프로젝트를 저장합니다. 다운로드가 완료될 때까지 기다립니다.
  2. 컴퓨터에서 파일을 찾습니다(예: Downloads 폴더).
  3. ZIP 파일을 더블클릭하여 압축을 해제합니다. 프로젝트 파일이 포함된 새 폴더가 만들어집니다.

Android 스튜디오에서 프로젝트 열기

  1. Android 스튜디오를 시작합니다.
  2. Welcome to Android Studio 창에서 Open을 클릭합니다.

d8e9dbdeafe9038a.png

참고: Android 스튜디오가 이미 열려 있는 경우 File > Open 메뉴 옵션을 대신 선택합니다.

8d1fda7396afe8e5.png

  1. 파일 브라우저에서 압축 해제된 프로젝트 폴더가 있는 위치로 이동합니다(예: Downloads 폴더).
  2. 프로젝트 폴더를 더블클릭합니다.
  3. Android 스튜디오가 프로젝트를 열 때까지 기다립니다.
  4. Run 버튼 8de56cba7583251f.png을 클릭하여 앱을 빌드하고 실행합니다. 예상대로 작동하는지 확인합니다.

13. 요약

  • LiveData는 데이터를 보유합니다. LiveData는 모든 데이터에 사용할 수 있는 래퍼입니다.
  • LiveData는 관찰 가능합니다. 즉, LiveData 객체에서 보유한 데이터가 변경되면 관찰자에 알림이 제공됩니다.
  • LiveData는 수명 주기를 인식합니다. LiveData에 관찰자를 연결하면 관찰자는 LifecycleOwner(일반적으로 활동 또는 프래그먼트)와 연결됩니다. LiveData는 STARTED 또는 RESUMED 같은 활성 수명 주기 상태인 관찰자만 업데이트합니다. LiveData 및 관찰에 관한 자세한 내용은 여기에서 알아볼 수 있습니다.
  • 앱은 데이터 결합 및 결합 표현식을 사용하여 레이아웃에서 LiveData 변경사항을 수신할 수 있습니다.
  • 결합 표현식은 레이아웃 내에서 레이아웃 속성을 참조하는 속성(attribute properties)(예: android:text)에서 작성됩니다.

14. 자세히 알아보기

블로그 게시물