Tatsuro のテックブログ

アプリ開発の技術情報を発信します。

【Android × Kotlin】ViewModel の基本的な使い方

今回は、Android Jetpack の ViewModel の基本的な使い方について解説します。

なお、ここに掲載しているソースコードは以下の環境で動作確認しています。

  • Android Studio Bumblebee | 2021.1.1 Patch 2
  • JDK 11.0.11
  • Android Gradle Plugin 7.1.2
  • Kotlin 1.6.10
  • Gradle 7.4
  • androidx.lifecycle 2.4.0

ViewModel とは?

Android アプリの場合、Activity や Fragment があり、これらに View があったり、ロジックがあったり、一時的なデータを保持したりしています。

Activity や Fragment は、リソース確保や画面回転などにより、 Android フレームワークによって破棄されることがあります。そのときに、Activity や Fragment の内部で保持していたデータもいっしょに破棄されてしまいます。

しかし、アプリ内部の一時的なデータを Activity や Fragment ではなくて ViewModel に持たせることで、Activity や Fragment が一時的に破棄されてしまっても、一時的なデータを保持し続けることができるようになります。

ライブラリのインポート

ViewModel を使えるようにするために、アプリの build.gradle ファイルに次の依存関係を追加します。

dependencies {

    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0"
    implementation "androidx.activity:activity-ktx:1.4.0"
    implementation "androidx.fragment:fragment-ktx:1.4.1"
}

まず、ViewModel を使うために viewmodel-ktx をインポートします。

そして、Activity や Fragment にて ViewModel を使いやすくするために、activity-ktxfragment-ktx をインポートします。

activity-ktxfragment-ktx は、by で ViewModel のインスタンスを取得するために必要なライブラリです。なお、ViewModelProvider を使っても、ViewModel を取得でき、そちらを使うときは activity-ktxfragment-ktx をインポートする必要はありません。

// by で ViewModel インスタンスを取得する。
val viewModel: MainViewModel by viewModels()

// ViewModelProvider で ViewModel インスタンスを取得する。
val viewModel = ViewModelProvider(this)[MainViewModel::class.java]

また、activity-ktxfragment-ktx に含まれているため、fragment-ktx がインポート済みの場合、activity-ktx をインポートする必要はありません。

ViewModel を使う

実際に ViewModel を使ってみます。

今回は、Button をクリックした回数を ViewModel に保持し、その回数を TextView に表示してみます。

まず、ViewModel を実装します。

class MainViewModel : ViewModel() {

    /** Button のクリック回数 */
    var count = 0
        private set

    /** クリック回数をカウントアップする。 */
    fun countUp() {
        count++
    }
}

ViewModel を実装する場合は、ViewModel を継承します。そして、実装した ViewModel に Button のクリック回数を保持する変数とクリック回数をカウントアップするメソッドを追加します。

そして、Activity を実装します。

class MainActivity : AppCompatActivity(R.layout.main_activity) {

    private val viewModel: MainViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // ボタンがクリックされるたびに、クリック回数をカウントアップして、表示を更新する。
        findViewById<Button>(R.id.button).setOnClickListener {
            viewModel.countUp()
            showCount()
        }
    }

    override fun onResume() {
        super.onResume()
        showCount()
    }

    /** クリック回数を TextView に表示する。 */
    private fun showCount() {
        findViewById<TextView>(R.id.text).text = viewModel.count.toString()
    }
}

Activity では MainViewModelインスタンスを取得します。

通常、インスタンスを取得するときは、直接コンストラクタを使ってインスタンスを生成し、そのインスタンスを Activity で保持します。しかし、その方法で ViewModel を取得してしまうと、Activity が再生成されるたびに ViewModel も再生成され、ViewModel 内部で保持するデータが破棄されてしまいます。

そこで、by viewModels() を使って ViewModel を取得します。by viewModels() を使うと、ViewModel を Activity ではなくて ViewModelStore というところで保持するようになります。そして、Activity が再生成されても ViewModel は破棄されずに保持され続けるようになります。

by viewModels() は ViewModel を要求されるたびに ViewModelStore に格納済みか確認します。まだ格納されていない場合、ViewModel を生成します。すでに格納済みの場合、その格納済みの ViewModel を返します。よって、Activity が再生成されても、Activity は以前に生成した ViewModel を再度取得できます。

なお、ViewModelProvider にも by viewModels() と同様の効果があります。

// NG
val viewModel = MainViewModel()

// OK
val viewModel: MainViewModel by viewModels()

// OK
val viewModel = ViewModelProvider(this)[MainViewModel::class.java]

また、Fragment でも、by viewModels()ViewModelProvider を使って ViewModel を取得することができます。

ViewModel で Context を使う

リソース文字列を取得するなどで Context を使うことがあります。Activity や Fragment では、Context を取得する方法がありますが、ViewModel の場合、ViewModel を継承するだけでは Context を取得することができません。

ViewModel で Context を使う場合、以下のように ViewModel ではなくて AndroidViewModel を継承することで Context を使えるようになります。

class MainViewModel(app: Application) : AndroidViewModel(app) {

    val str = getApplication<Application>().getString(R.string.app_name)
}

まず、ViewModel コンストラクタに Application の引数を追加します。次に、その Application を AndroidViewModel コンストラクタの引数に渡します。こうすることで、AndroidViewModel を継承する ViewModel は内部に Application を保持するようになります。そして、この Application を取得するときは、AndroidViewModel#getApplication を使います。Application は Context を継承しているので、これで Context を取得できるようになります。

なお、ViewModel の継承元を ViewModel から AndroidViewModel に変更しても、ViewModel 使用側を変更する必要はありません。これは、by viewModels()ViewModelProvider が、ViewModel の継承元の違いを吸収してくれるからです。

class MainActivity : AppCompatActivity(R.layout.main_activity) {

    // 継承元が ViewModel または AndroidViewModel のどちらでも問題ない。
    private val viewModel: MainViewModel by viewModels()
    ︙

参考

ViewModel の概要

関連記事

LiveData で値を監視する方法について、以下で解説しています。ViewModel と LiveData の組み合わせは、Android アプリ開発でよく使用されています。

tatsurotech.hatenablog.com

コンストラクタ経由で ViewModel に値を渡す方法について、以下で解説しています。

tatsurotech.hatenablog.com