3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

(初学者向け)Android開発におけるViewModelの役割

3
Last updated at Posted at 2025-12-07

はじめに

近年のAndroid開発におけるアーキテクチャのトレンドとして、MVVMというものがあると思います。
MVVMとは、Model-View-ViewModelの略で、UIの表示と、ビジネスロジックやデータ処理を分離するためのアーキテクチャパターンの1つです。
今回はその中のViewModelについて焦点を絞って紹介していこうと思います。

ViewModelの役割

簡潔に言うと、ViewModelの役割とは、「壊れやすいUI(View)に代わって情報をキープしておいてあげる」ことです。また、「UIから頼まれた情報を(Modelから)取ってくる」という役割もあります。
つまり、ViewとModelの中継ぎ的存在なのです。

「壊れやすいUI」って?

実はAndroidアプリのUIは画面回転をすることで簡単に壊れてしまいます。
これはどういうことかというと、画面回転をすることでUIが壊れて再生成されるため、保持している情報(クリックされた回数など)が失われてしまうのです。
試しに以下のようなコードを書いてみました。

@Composable
fun MainScreen(modifier: Modifier = Modifier) {
    Column(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        var count by remember { mutableIntStateOf(0) } // countの値が変わると再描画する
        Button(onClick = { count++ }) {
            Text(text = "Count: $count")
        }
    }
}
コード全体
package com.websarva.wings.roleofviewmodel

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.websarva.wings.roleofviewmodel.ui.theme.RoleOfViewModelTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            RoleOfViewModelTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    MainScreen(modifier = Modifier.padding(innerPadding))
                }
            }
        }
    }
}

@Composable
fun MainScreen(modifier: Modifier = Modifier) {
    Column(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        var count by remember { mutableIntStateOf(0) } // countの値が変わると再描画する
        Button(onClick = { count++ }) {
            Text(text = "Count: $count")
        }
    }
}
こうして動く様子が以下の通りです。

screen-20251117-2355092-ezgif.com-video-to-gif-converter.gif

画面回転するとカウントした値がリセットされていることが分かると思います。
これは画面回転の際にUIが壊れて再生成されて、保持していた情報(countの値)が失われて初期状態に戻ってしまったことを示しています。
これではマズいので、Composable関数内にあるcountをViewModelに逃がしてあげましょう。
以下のように書き換えてみます。(ViewModelを使わなくてよいrememberSaveableを使う方法もありますが今回は割愛)

@Composable
fun MainScreen(
    modifier: Modifier = Modifier,
    viewModel: SampleViewModel
) {
    Column(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Button(onClick = { viewModel.count++ }) {
            Text(text = "Count: ${viewModel.count}")
        }
    }
}

class SampleViewModel : ViewModel() { // ViewModelクラスを継承する
    var count by mutableIntStateOf(0) // ViewModel内でcountの値を管理
}
コード全体
package com.websarva.wings.roleofviewmodel

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.lifecycle.ViewModel
import com.websarva.wings.roleofviewmodel.ui.theme.RoleOfViewModelTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        // by viewModels()でActivityが破棄されてもViewModelを保持する
        val viewModel: SampleViewModel by viewModels() 
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            RoleOfViewModelTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    MainScreen(
                        modifier = Modifier.padding(innerPadding),
                        viewModel = viewModel
                    )
                }
            }
        }
    }
}

@Composable
fun MainScreen(
    modifier: Modifier = Modifier,
    viewModel: SampleViewModel
) {
    Column(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Button(onClick = { viewModel.count++ }) {
            Text(text = "Count: ${viewModel.count}")
        }
    }
}

class SampleViewModel : ViewModel() { // ViewModelクラスを継承する
    var count by mutableIntStateOf(0) // ViewModel内でcountの値を管理
}

こうすると以下のような動きになります。

screen-20251118-0017492-ezgif.com-video-to-gif-converter.gif

画面回転をしてもcountの値がリセットされず、無事カウントアップできましたね。
このように、Composable関数内(View)ではなくViewModelに値を保持することで、UIが壊れても情報をキープできるようになります。

ViewModelの立ち位置

ここで、公式ドキュメントが出しているアプリの推奨アーキテクチャの図を見てみましょう。(今回はDomain Layerについては割愛します)
image.png
この図でいう「UI elements」がComposable関数たち(View)、「State holders」がViewModel、「Data Layer」が必要なデータが保存されている場所orそのデータにアクセスするロジックが格納されている層(Model)になります。
アーキテクチャは基本的にそれぞれが各々の役割に集中できるような設計になるように定められています。
つまり、ViewはUIの表示ViewModelは状態保持、データ取得依頼Modelはデータの提供、管理に集中すべきなのです。
なので、最初に紹介したUIがcountの値を保持しているパターンは、UIが表示と状態保持を同時に行っているため、アーキテクチャに反した設計となっています。
ViewModelを導入したパターンは、UIがcountの値の表示のみを担当し、ViewModelがUIにcountの値を提供している状態なので、これはアーキテクチャに即した設計となることが分かります。

まとめ

ViewとViewModelの関係性を理解することで、きちんと責務を分離させたアーキテクチャを構築することができます。実はこの話題はとある会社のインターンシップの面接でも聞かれたことなので、しっかりと理解したうえで言語化できるようにしておきましょう!

補足(脱初学者向け)

今回はあえて「壊れやすいUI」という表現を用いましたが、本当の表現は、「画面回転をするとActivityが破棄されて再構築される」というものです。
ここで巷では親の顔より見るとされるアクティビティのライフサイクルに関する簡略な図を見てみましょう
image.png

Androidアプリにはアクティビティのライフサイクルというものが存在し、普段は緑色の"Activity running"にいるのですが、画面回転が始まるとonPause()→onStop()→onDestroy()と順に呼ばれ、Activityが破棄(Activity shut down)されます。そして画面回転が終わると即座にActivityが再構築(Activity launched)され、再びActivity runningに帰ってきます。この目まぐるしく状態が変化するActivityから、値をViewModelに逃がしてあげている、という訳なんです。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?