Android View Model

69582 ワード

ActivityはonCreateポイントでdevice configurationを参照します.デバイス構成が変更された場合はonCreateを再度呼び出し、これらの理由で他の操作がない場合は携帯電話を横モードとしてviewを初期化します.
このときBundleを使用することができますが、ActivityとPregmentのコードはアプリケーションの複雑さが増すにつれて複雑になりますので、Bundleを処理すると非常に面倒になります.

view model


ビューモデルは、画面上のすべてのデータとビジネスロジックを担当するオブジェクトです.画面を更新する必要がある場合は、アクティビティとプレゼンテーションでビューモデルが尋ねられ、ビジネスロジック関数もビューモデルから呼び出されます.
これらのビューモデルを使用すると、アクティブおよびレンダリングはレイアウト処理に集中できます.また、デバイスconfigurationが変更されても、ビューモデルオブジェクトは安全であるため、Bundleの追加処理は必要ありません.

前の例


単語推測ゲームをして、ビューモデルの適用前後を比較します.
メインアクティブデバイスは、ナビゲータのフロントエンドとしてのみ機能し、GameFragmentとResultFragmentを作成します.

framgent_game

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical"
    android:padding="16dp"
    tools:context=".GameFragment">

    <TextView
        android:id="@+id/word"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:textSize="36sp"
        android:letterSpacing="0.1"/>

    <TextView
        android:id="@+id/lives"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="16sp"/>

    <TextView
        android:id="@+id/incorrect_guesses"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="16sp"/>

    <EditText
        android:id="@+id/guess"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="16sp"
        android:hint="Guess a letter"
        android:inputType="text"
        android:maxLength="1"/>

    <Button
        android:id="@+id/guessButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="Guess!"/>
</LinearLayout>

framgent_result

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical"
    android:gravity="center"
    tools:context=".ResultFragment">

    <TextView
        android:id="@+id/won_lost"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:textSize="28sp"
        android:layout_margin="18dp"/>

    <Button
        android:id="@+id/new_game_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="Start new game"/>

</LinearLayout>

nav_graph

<?xml version="1.0" encoding="utf-8"?>
<navigation 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:id="@+id/nav_graph"
    app:startDestination="@id/gameFragment">

    <fragment
        android:id="@+id/gameFragment"
        android:name="com.example.guessinggame.GameFragment"
        android:label="Game"
        tools:layout="@layout/fragment_game">
        <action
            android:id="@+id/toResultFragment"
            app:destination="@id/resultFragment"
            app:popUpTo="@id/gameFragment"
            app:popUpToInclusive="true"/>
    </fragment>
    <fragment
        android:id="@+id/resultFragment"
        android:name="com.example.guessinggame.ResultFragment"
        android:label="Result"
        tools:layout="@layout/fragment_game">
        <argument
            android:name="result"
            app:argType="string" />
        <action
            android:id="@+id/toGameFragment"
            app:destination="@id/gameFragment" />
    </fragment>
</navigation>

activity_main

<?xml version="1.0" encoding="utf-8"?>
<androidx.fragment.app.FragmentContainerView 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:id="@+id/navHost"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:name="androidx.navigation.fragment.NavHostFragment"
    app:navGraph="@navigation/nav_graph"
    app:defaultNavHost="true"
    tools:context=".MainActivity">

</androidx.fragment.app.FragmentContainerView>

GameFragment.kt

class GameFragment: Fragment() {
    private var _binding: FragmentGameBinding? = null
    private val binding get() = _binding!!

    val words = listOf("Android", "Activity", "Fragment")
    val secretWord = words.random().uppercase()
    var secretWordDisplay = ""
    var correctGuess = ""
    var incorrectGuess = ""
    var livesLeft = 8

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = FragmentGameBinding.inflate(inflater, container, false)
        val view = binding.root
        secretWordDisplay = deriveSecretWordDisplay()
        updateScreen()

        binding.guessButton.setOnClickListener {
            makeGuess(binding.guess.text.toString().uppercase())
            binding.guess.text = null
            updateScreen()
            if (isWon() || isLost()) {
                val action = GameFragmentDirections
                    .toResultFragment(wonListMessage())
                view.findNavController().navigate(action)
            }
        }
        return view
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }

    fun updateScreen() {
        binding.word.text = secretWordDisplay
        binding.lives.text = "You have $livesLeft lives left"
        binding.incorrectGuesses.text = "Incorrect guesses: $incorrectGuess"
    }

    fun deriveSecretWordDisplay() : String {
        var display = ""
        secretWord.forEach {
            display += checkLetter(it.toString())
        }
        return display
    }

    fun checkLetter(str: String) = when (correctGuess.contains(str)) {
        true -> str
        false -> "_"
    }

    fun makeGuess(guess: String) {
        if (guess.length == 1) {
            if (secretWord.contains(guess)) {
                correctGuess += guess
                secretWordDisplay = deriveSecretWordDisplay()
            } else {
                incorrectGuess += "$guess"
                livesLeft--
            }
        }
    }
    fun isWon() = secretWord.equals(secretWordDisplay, true)
    fun isLost() = livesLeft <= 0

    fun wonListMessage() : String  {
        var message = ""
        if (isWon()) message = "You Won!"

        else if (isLost()) message = "You Lost!"

        message += " The word wat $secretWord"
        return message
    }
}

ResultFragment.kt

class ResultFragment: Fragment() {
    private var _binding: FragmentResultBinding? = null
    private val binding get() = _binding!!


    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = FragmentResultBinding.inflate(inflater, container, false)
        val view = binding.root
        binding.wonLost.text = ResultFragmentArgs.fromBundle(requireArguments()).result

        binding.newGameButton.setOnClickListener {
            view.findNavController().navigate(R.id.toGameFragment)
        }
        return view
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}
まず、ゲーム宣伝のコードが長いです.しかし、この状態では、携帯電話が横画面モードになると、既存のデータはすべて消えてしまいます.Bundleを処理するコードを再加入するのは負担です.次に、ビューモデルを適用します.

ビューモデルの適用


ビューモデルは、画面から出力されるデータとビジネスロジックを担当するオブジェクトです.従って、game fragmentのprefertieおよび関数は、ビューモデルに移行されるべきである.残りのコードはUIを制御するコードだけです.
ビューモデルを適用することで、注目点分離の概念を適用できます.Activity、pregment、viewmodelの仕事は完全に分かれています.

ビューモデル依存性の追加

implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'

ビューモデルオブジェクトの追加


ゲームプレゼンテーションからビジネスロジックとビューに関するデータを取得します.そしてAndroid xlifecycle.ViewModelを拡張する必要があります.
class GameViewModel: ViewModel() {
    val words = listOf("Android", "Activity", "Fragment")
    val secretWord = words.random().uppercase()
    var secretWordDisplay = ""
    var correctGuess = ""
    var incorrectGuess = ""
    var livesLeft = 8

    init {
        secretWordDisplay = deriveSecretWordDisplay()
    }


    fun deriveSecretWordDisplay() : String {
        var display = ""
        secretWord.forEach {
            display += checkLetter(it.toString())
        }
        return display
    }

    fun checkLetter(str: String) = when (correctGuess.contains(str)) {
        true -> str
        false -> "_"
    }

    fun makeGuess(guess: String) {
        if (guess.length == 1) {
            if (secretWord.contains(guess)) {
                correctGuess += guess
                secretWordDisplay = deriveSecretWordDisplay()
            } else {
                incorrectGuess += "$guess"
                livesLeft--
            }
        }
    }
    fun isWon() = secretWord.equals(secretWordDisplay, true)
    fun isLost() = livesLeft <= 0

    fun wonListMessage() : String  {
        var message = ""
        if (isWon()) message = "You Won!"

        else if (isLost()) message = "You Lost!"

        message += " The word wat $secretWord"
        return message
    }
}

Link ViewModel & Fragment


ゲームプレゼンテーションから直接オブジェクトを作成し、ViewModelProviderオブジェクトから作成します.このオブジェクトから新しいインスタンスを作成し、ビューモデルオブジェクトが以前に作成されていない場合にのみ新しいインスタンスを作成します.デバイス構成が変更されても、すでに生成されたビューモデルがあるため、再生成されません.
レンダリングのライフサイクルによってレンダリングが破壊されると、ビューモデルも消えます.次に、レイヤーを再作成すると、ビューモデルが再作成されます.
class GameFragment: Fragment() {
    private var _binding: FragmentGameBinding? = null
    private val binding get() = _binding!!
    lateinit var viewModel: GameViewModel

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = FragmentGameBinding.inflate(inflater, container, false)
        val view = binding.root
        viewModel = ViewModelProvider(this).get(GameViewModel::class.java)

        updateScreen()

        binding.guessButton.setOnClickListener {
            viewModel.makeGuess(binding.guess.text.toString().uppercase())
            binding.guess.text = null
            updateScreen()
            if (viewModel.isWon() || viewModel.isLost()) {
                val action = GameFragmentDirections
                    .toResultFragment(viewModel.wonListMessage())
                view.findNavController().navigate(action)
            }
        }
        return view
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }

    fun updateScreen() {
        binding.word.text = viewModel.secretWordDisplay
        binding.lives.text = "You have ${viewModel.livesLeft} lives left"
        binding.incorrectGuesses.text = "Incorrect guesses: ${viewModel.incorrectGuess}"
    }
}

ResultViewModel


結果を出せばStringでいい
class ResultViewModel(finalResult: String): ViewModel() {
    val result = finalResult
}
しかし、ジェネレータでパラメータを受信するビューモデルをどのように作成しますか?上記では、GameViewModelはジェネレータを単独で考慮していません.
ViewModelFactoryを使用できます.このオブジェクトの目的は、ビューモデルの作成と初期化です.
以下に作成します.
// 이 인터페이스를 구현함으로써 클래스를 뷰모델 팩토리로 바꿔놓음.
class ResultViewModelFactory(private val finalResult: String)
    :ViewModelProvider.Factory{

    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        // 제대로된 타입으로 만들려고 하는지 체크
        if (modelClass.isAssignableFrom(ResultViewModel::class.java))
            return ResultViewModel(finalResult) as T
        // 그렇지 않다면 예외 처리
        throw IllegalArgumentException("Unknown ViewModel")
    }
}
ResultFragmentコードを変更するには
class ResultFragment: Fragment() {
    private var _binding: FragmentResultBinding? = null
    private val binding get() = _binding!!
    lateinit var viewModelFactory: ResultViewModelFactory
    lateinit var viewModel: ResultViewModel


    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = FragmentResultBinding.inflate(inflater, container, false)
        val view = binding.root
        val result = ResultFragmentArgs.fromBundle(requireArguments()).result
        viewModelFactory = ResultViewModelFactory(result)
        viewModel = ViewModelProvider(this, viewModelFactory).get(ResultViewModel::class.java)
        
        binding.wonLost.text = viewModel.result

        binding.newGameButton.setOnClickListener {
            view.findNavController().navigate(R.id.toGameFragment)
        }
        return view
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

onCleared()


onCleared()は、ビューモデルが消去される前に呼び出されます.ビューモデルに関連するアクティブまたはバージョンのライフサイクルでビューモデルを使用して、ビューモデルが消える前にリソースをクリーンアップする場合に便利です.ビューモデルのライフサイクルと考えられますか?