ビューモデルとデータバインディング

  • 投稿者:
  • 投稿カテゴリー:その他

設定

Moduleのbuild.gradle

buildFeatures {
viewBinding = true
}

ビューバインディングからデータバインディングに変更する場合は、上記を次のように書換

buildFeatures {
dataBinding = true
}

pluginsのkotlin-kaptを確認

plugins {
id ‘com.android.application’
id ‘kotlin-android’
id ‘kotlin-kapt’ ←無い場合は追加
}

ViewModel依存関係
implementation ‘androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0’

ビューモデルとビューバインディングを使ったサンプル

<resources>
    <string name="app_name">Unscramble</string>
    <string name="instructions">Unscramble the word using all the letters.</string>
    <string name="skip">Skip</string>
    <string name="submit">Submit</string>
    <string name="score">Score: %d</string>
    <string name="word_count">%d of %d words</string>
    <string name="enter_your_word">Enter your word</string>
    <string name="congratulations">Congratulations!</string>
    <string name="you_scored">"You scored: %d"</string>
    <string name="exit">Exit</string>
    <string name="play_again">Play Again</string>
    <string name="try_again">Try again!</string>
</resources>

レイアウト

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/game_fragment"
        android:name="com.example.android.unscramble.ui.game.GameFragment"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

フラグメントレイアウトファイル

・データバインディングレイアウトへの変更
 ルート要素のルートにやタグを追加する
 現在のルート要素にて右クリック
 [Show Context Actions] > [Convert to data binding layout] を選択

・Fragmentや変数の定義する
 タグにという名前の子タグを作成しnameとtypeを設定  name:レイアウトファイル内で使用する名称  type:Fragmentや変数の型などのクラス名

<data>
    <variable
        name="gameViewModel"
        type="com.example.android.unscramble.ui.game.GameViewModel" />
    <variable
        name="maxNoOfWords"
        type="int" />
</data>

・dataタグに設定した名前を使ってtext等をセット
 これにより値を代入するソースコードは不要となる
android:text=”@{gameViewModel.currentScrambledWord}”

<?xml version="1.0" encoding="utf-8"?>

<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>
        <variable
            name="gameViewModel"
            type="com.example.android.unscramble.ui.game.GameViewModel" />
        <variable
            name="maxNoOfWords"
            type="int" />
    </data>

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

        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="@dimen/default_padding"
            tools:context=".ui.game.GameFragment">

            <Button
                android:id="@+id/skip"
                style="?attr/materialButtonOutlinedStyle"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginStart="@dimen/default_padding"
                android:layout_marginEnd="@dimen/default_padding"
                android:text="@string/skip"
                app:layout_constraintBaseline_toBaselineOf="@+id/submit"
                app:layout_constraintEnd_toStartOf="@+id/submit"
                app:layout_constraintStart_toStartOf="parent" />

            <Button
                android:id="@+id/submit"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginTop="@dimen/default_margin"
                android:text="@string/submit"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toEndOf="@+id/skip"
                app:layout_constraintTop_toBottomOf="@+id/textField" />

            <TextView
                android:id="@+id/textView_instructions"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/instructions"
                android:textSize="17sp"
                app:layout_constraintBottom_toTopOf="@+id/textField"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/textView_unscrambled_word" />

            <TextView
                android:id="@+id/textView_unscrambled_word"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="@dimen/default_margin"
                android:layout_marginBottom="@dimen/default_margin"
                android:textAppearance="@style/TextAppearance.MaterialComponents.Headline3"
                android:text="@{gameViewModel.currentScrambledWord}"
                app:layout_constraintBottom_toTopOf="@+id/textView_instructions"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/word_count"
                tools:text="Scramble word" />

            <TextView
                android:id="@+id/word_count"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@{@string/word_count(gameViewModel.currentWordCount, maxNoOfWords)}"
                android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
                app:layout_constraintBottom_toTopOf="@+id/textView_unscrambled_word"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                tools:text="3 of 10 words" />

            <TextView
                android:id="@+id/score"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@{@string/score(gameViewModel.score)}"
                android:textAllCaps="true"
                android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                tools:text="Score: 20" />

            <com.google.android.material.textfield.TextInputLayout
                android:id="@+id/textField"
                style="@style/Widget.Unscramble.TextInputLayout.OutlinedBox"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginTop="@dimen/default_margin"
                android:hint="@string/enter_your_word"
                app:errorIconDrawable="@drawable/ic_error"
                app:helperTextTextAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
                app:layout_constraintBottom_toTopOf="@+id/submit"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/textView_instructions">

                <com.google.android.material.textfield.TextInputEditText
                    android:id="@+id/text_input_edit_text"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:inputType="textPersonName|textNoSuggestions"
                    android:maxLines="1" />
            </com.google.android.material.textfield.TextInputLayout>

        </androidx.constraintlayout.widget.ConstraintLayout>
    </ScrollView>
</layout>

フラグメント

・前提としてコードは画面表示機能に限定する。ロジックや値の保持はビューモデルクラスにて行う
・クラス変数でViewModelを定義。by句で委譲する事でデバイスの回転時でもデータを保持
・クラス変数でBindingオブジェクトをlateinitで取得(型はフラグメント名+Binding)
・onCreateViewにてデータバインディングをインフレートしrootオブジェクトをリターン
・onViewCreatedにてxmlレイアウトで定義した タグのname属性にインスタンスを当てはめてライフサイクルを設定する
binding.gameViewModel = viewModel
binding.maxNoOfWords = MAX_NO_OF_WORDS
binding.lifecycleOwner = viewLifecycleOwner
・ビューバインディングからの値取得
方法はビューバインディングとデータバインディング共通
val playerWord = binding.textInputEditText.text.toString()
・データバインディングでのリスナー登録
方法はビューバインディングとデータバインディング共通
binding.submit.setOnClickListener { onSubmitWord() }

class GameFragment : Fragment() {
    // viewModelsに委譲する事でデータを保持 (by 委譲クラス)
    private val viewModel: GameViewModel by viewModels()

    // Binding object instance with access to the views in the game_fragment.xml layout
    private lateinit var binding: GameFragmentBinding

    override fun onCreateView(
            inflater: LayoutInflater, container: ViewGroup?,
            savedInstanceState: Bundle?
    ): View {
        // ViewBindingの場合
        //binding = GameFragmentBinding.inflate(inflater, container, false)

        // DataBindingの場合
        binding = DataBindingUtil.inflate(inflater, R.layout.game_fragment, container, false)

        Log.d("GameFragment", "GameFragment created/re-created!")
        Log.d("GameFragment", "Word: ${viewModel.currentScrambledWord} " +
                "Score: ${viewModel.score} WordCount: ${viewModel.currentWordCount}")
        // rootオブジェクトを戻す
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // xmlレイアウトで定義した <data>タグのname属性にインスタンスを当てはめる
        binding.gameViewModel = viewModel
        binding.maxNoOfWords = MAX_NO_OF_WORDS

        // ライフサイクルオーナーの設定
        binding.lifecycleOwner = viewLifecycleOwner

        // リスナー登録(ビューバインディングとデータバインディング共通)
        binding.submit.setOnClickListener { onSubmitWord() }
        binding.skip.setOnClickListener { onSkipWord() }
    }

    // 対応するアクティビティとフラグメントが破棄されたときに呼び出される onDetach()
    override fun onDetach() {
        super.onDetach()
        Log.d("GameFragment", "GameFragment destroyed!")
    }

    // submitボタン押下時
    private fun onSubmitWord() {
        // バインディングデータの取得(ビューバインディングとデータバインディング共通)
        val playerWord = binding.textInputEditText.text.toString()
        if (viewModel.isUserWordCorrect(playerWord)) {
            // 正解した場合
            setErrorTextField(false)    // エラークリア
            if (!viewModel.nextWord()) {
                // 終了ダイアログを表示
                showFinalScoreDialog()
            }
        } else {
            // 不正解の場合(エラーを表示する)
            setErrorTextField(true)
        }
    }

    // スキップボタン押下時
    private fun onSkipWord() {
        if (viewModel.nextWord()) {
            setErrorTextField(false)    // エラークリア
        } else {
            // 終了ダイアログを表示
            showFinalScoreDialog()
        }
    }

    // リスタート
    private fun restartGame() {
        viewModel.reinitializeData()
        setErrorTextField(false)
    }

    /*
     * Exits the game.
     */
    private fun exitGame() {
        activity?.finish()
    }

    /*
    * Sets and resets the text field error status.
    */
    private fun setErrorTextField(error: Boolean) {
        if (error) {
            binding.textField.isErrorEnabled = true
            binding.textField.error = getString(R.string.try_again)
        } else {
            binding.textField.isErrorEnabled = false
            binding.textInputEditText.text = null
        }
    }

    // ゲーム終了時のダイアログ
    private fun showFinalScoreDialog() {
        MaterialAlertDialogBuilder(requireContext())
            .setTitle(getString(R.string.congratulations))
            .setMessage(getString(R.string.you_scored, viewModel.score.value))
            .setCancelable(false)   // キャンセルボタンを表示しない
            .setNegativeButton(getString(R.string.exit)) { _, _ ->
                exitGame()  // exitの時
            }
            .setPositiveButton(getString(R.string.play_again)) { _, _ ->
                restartGame()   // play_againの時
            }
            .show()
    }
}

ビューモデル

・ViewModelクラスを継承
・デバイス回転時等の値の保持やロジック等を記載
・LiveDataをクラス変数に定義し画面情報を保存する
・LiveDataは可変のprivate変数と外部クラスからの読取専用を用意
private var _currentWordCount = MutableLiveData(0)
val currentWordCount: LiveData get() = _currentWordCount

private val _currentScrambledWord = MutableLiveData<String>()
val currentScrambledWord: LiveData<String> get() = _currentScrambledWord

・LiveDataが変更されるとオブザーバーに通知される
・LiveDataの値取得は.valueを使う
currentWordCount.value!! < MAX_NO_OF_WORDS
・LiveDataの値セットは.valueを使う
_score.value = 0
_score.value = (_score.value)?.plus(SCORE_INCREASE)
_currentScrambledWord.value = String(tempWord)
_currentWordCount.value = (_currentWordCount.value)?.inc()
・クラス初期化時は init { }を実行する

class GameViewModel : ViewModel() {

    // ViewModelで保存する変数とゲッターを定義
    // MutableLiveDataの定義と読取専用のバッキングプロパティ
    // LiveDataが変更されるとオブザーバーに通知される
    private var _score = MutableLiveData(0)  // 得点
    val score: LiveData<Int>
        get() = _score          // 得点のバッキングプロパティ

    private var _currentWordCount = MutableLiveData(0)        // 出題数
    val currentWordCount: LiveData<Int>
        get() = _currentWordCount  // 出題数のバッキングプロパティ

    // クイズの問題(シャッフルされたアルファベット)
    // MutableLiveDataの定義と読取専用のバッキングプロパティ
    private val _currentScrambledWord = MutableLiveData<String>()
    val currentScrambledWord: LiveData<String>
        get() = _currentScrambledWord

    // 既に出題した単語list
    private var wordsList: MutableList<String> = mutableListOf()
    // 問題の答え
    private lateinit var currentWord: String


    // init処理
    init {
        Log.d("GameFragment", "GameViewModel created!")
        getNextWord()   // クイズのお題を取得する
    }

    // activityとfragmentが終了した時にonClearedがコールされる
    override fun onCleared() {
        super.onCleared()
        Log.d("GameFragment", "GameViewModel destroyed!")
    }

    // クイズ正解時に点数を加算する
    private fun increaseScore() {
        _score.value = (_score.value)?.plus(SCORE_INCREASE)
    }

    //
    fun isUserWordCorrect(playerWord: String): Boolean {
        if (playerWord.equals(currentWord, true)) {
            increaseScore()
            return true
        }
        return false
    }

    // クイズのお題を生成する
    private fun getNextWord() {
        // ランダムで単語を選択
        currentWord = allWordsList.random()

        // 単語をアルファベットの配列に変換
        val tempWord = currentWord.toCharArray()
        // アルファベットをシャッフル
        tempWord.shuffle()

        // シャフルした単語が元のアルファベットと同じ場合はもう一度シャッフルする
        while (String(tempWord).equals(currentWord, false)) {
            tempWord.shuffle()
        }

        // すでに出題済みの単語のチェック
        if (wordsList.contains(currentWord)) {
            // すでに出題済みの単語の場合はもう一度関数を実行
            getNextWord()
        } else {
            // お題の文字列を取得
            // (MutableLiveDataにデータをセットする場合は.valueを使用)
            _currentScrambledWord.value = String(tempWord)

            // 出題数をインクリメント
            _currentWordCount.value = (_currentWordCount.value)?.inc()

            // 出題済みリストに追加
            wordsList.add(currentWord)
        }

    }

    // 指定回数未満の場合に次のクイズを作成
    fun nextWord(): Boolean {
        return if (currentWordCount.value!! < MAX_NO_OF_WORDS) {
            getNextWord()
            true
        } else false
    }

    // 再チャレンジ
    fun reinitializeData() {
        _score.value = 0
        _currentWordCount.value = 0
        wordsList.clear()
        getNextWord()
    }
}

トレーニング > KOTLIN を用いた ANDROID の基本 > ナビゲーション > ナビゲーションの概要 > ViewModelでLiveDataを使用する > 3.LiveDataについて