【Android】MVVMについて、開発中のアプリを例にまとめてみました

# はじめに
本記事では、ソフトウェアアーキテクチャの一種であるMVVMについて自分用にまとめておきます。また、私が現在開発中のアプリにこれを当てはめ、アプリ内のどの部分がMVVMのどの要素に対応しているかをまとめてみたいと思います。投稿者はAndroid開発初学者であるため、内容に誤りや不足がある可能性もあります。コメントでご指南、ご指摘いただければ幸いです。

# MVVMとは何か
MVVMとは、UIを持つソフトウェアに適用されるソフトウェアアーキテクチャの一種です。MVVMでは、ソフトウェアを**Model-View-ViewModel**の3要素に分割してアプリを設計します。<br>

<div style="text-align: center;">

```mermaid
graph TD
    View -->|イベント（クリック等）| ViewModel
    ViewModel -->|UI描画に必要なデータ| View
    ViewModel -->|データ取得、Modelの更新等| Model
    Model -->|データ更新| ViewModel

```
</div>

**View**はViewModelからデータを受け取り、UIを表示する役割があります。また、ユーザーがボタンのクリックなどのイベントを起こした際にはそれをViewModelに伝える役割があります。<br><br>
**ViewModel**はViewとModelの仲介役となります。Viewから送られてきたイベントの情報をもとにModelに対してデータの取得や更新などの処理を行います。また、ModelのデータをUI描画に必要な形でViewに送る役割も持っています。<br><br>
**Model**はアプリケーションのデータとビジネスロジックを管理します。データの取得や保存などを行い、ViewModelからのリクエストに応じて必要なデータを提供します。

# アプリの概要
私が現在開発中のアプリは、読書の情報を一元管理し、ユーザーの読書をサポートするアプリです。ユーザーはタイトルを検索して書籍を登録し、登録した書籍に対しては現在の状態(未読、読書中、読了の3種類)の管理や読了ページ数の変更、感想の記入などを行うことができます。詳細は以下のリポジトリをご覧ください。
[ReadTrack](https://github.com/suzukibelltree/ReadTrack)

## View
### 役割
- UIの表示・更新を行う
- ユーザーの操作の検知
    Viewはユーザーにデータを表示し、ユーザーの操作を検知してViewModelに渡します。
### 具体例
Jetpack Composeにおいては、Composable関数がこのViewにあたります。
ここでは、例として登録した書籍の一覧を表示する画面を描画するコンポーザブル関数を示します。
<details><summary>LibraryScreen.kt</summary>

```Kotlin:LibraryScreen.kt
@Composable
fun LibraryScreen(
    navController: NavController,
    savedBooksViewModel: SavedBooksViewModel
) {
    val configuration = LocalConfiguration.current
    val screenWidthDp = configuration.screenWidthDp.dp
    val savedBooks by savedBooksViewModel.savedBooks.collectAsState()
    var selectedTabIndex by remember { mutableStateOf(0) }
    Column {
        TabRow(
            selectedTabIndex = selectedTabIndex,
            contentColor = Color.Blue,
            modifier = Modifier.fillMaxWidth()
        ) {
            Tab(
                selected = selectedTabIndex == 0,
                onClick = { selectedTabIndex = 0 },
                text = { Text("未読") }
            )
            Tab(
                selected = selectedTabIndex == 1,
                onClick = { selectedTabIndex = 1 },
                text = { Text("読書中") }
            )
            Tab(
                selected = selectedTabIndex == 2,
                onClick = { selectedTabIndex = 2 },
                text = { Text("読了") }
            )
        }
        LazyVerticalGrid(columns = GridCells.Adaptive(screenWidthDp / 3)) {
            // ここでselectedTabIndexに応じて表示する本のリストを変える
            val filteredBooks = when (selectedTabIndex) {
                0 -> savedBooks.filter { it.progress == 0 }
                1 -> savedBooks.filter { it.progress == 1 }
                2 -> savedBooks.filter { it.progress == 2 }
                else -> savedBooks
            }
            items(filteredBooks) { book ->
                Column {
                    AsyncImage(
                        model = book.thumbnail,
                        contentDescription = null,
                        modifier = Modifier
                            .fillMaxWidth()
                            .padding(16.dp)
                            .clickable {
                                savedBooksViewModel.selectBook(book.id)
                                navController.navigate("${ReadTrackScreen.MyBook.name}/${book.id}")
                            }
                    )
                    LinearProgressIndicator(
                        progress = book.readpage!!.toFloat() / book.pageCount!!.toFloat(),
                        modifier = Modifier.fillMaxWidth().padding(16.dp)
                    )
                }
            }
        }
    }
}
```

</details>
LibraryScreen関数は登録した書籍の一覧を表示する画面を描画するコンポーザブル関数です。LazyVerticalGrid()関数により、savedBooksViewModelという登録された本の情報を管理するViewModelからデータを受け取り、登録された書籍の情報をユーザーに表示します。<br>
 また、AsyncImage()関数内で本の画像がクリックされたときに、savedBooksViewModelに対してクリックされた本のidの情報を渡すようにしています。これにより、本の画像をクリックしたときにその本の詳細な情報を表示する画面に遷移することができます。

## ViewModel
### 役割
- Viewを描画するための状態を保持する
- Viewから送られてきた値を保存するためのメソッドを呼ぶ
ViewModelは、Modelからデータを取得し、Viewに必要な形式で提供します。また、Viewからイベントを受け取り、Modelに指示を送る役割も担当しています。
### 具体例

<details><summary>SavedBooksViewModel.kt</summary>

```Kotlin:SavedBooksViewModel.kt
class SavedBooksViewModel(private val booksRepository: BooksRepository) : ViewModel() {
    val savedBooks: StateFlow<List<BookData>> = booksRepository.getAllBooksFlow()
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
    private val _selectedBook = MutableStateFlow<BookData?>(null)
    val selectedBook: StateFlow<BookData?> = _selectedBook

    fun selectBook(savedbookId: String) {
        _selectedBook.value = savedBooks.value.find { it.id == savedbookId }
    }
    fun fetchBookDetails(bookId: String) {
        viewModelScope.launch {
            try {
                val book = booksRepository.getBookById(bookId) // Fetch book from repository
                _selectedBook.value = book
            } catch (e: Exception) {
                // Handle error
            }
        }
    }
    fun updateBook(book: BookData) {
        viewModelScope.launch {
            booksRepository.updateBook(book)
        }
    }
    fun deleteBook(book: BookData) {
        viewModelScope.launch {
            booksRepository.deleteBook(book)
        }
    }
}
```

</details>
SavedBooksViewModelは登録した書籍の情報をRoomデータベースに格納する際のデータの型であるBookDataのインスタンスのリストをメンバとして持っています。
そしてローカルのデータベースから引数として受け取ったIDと一致するインスタンスを持ってくるselectBook()関数、リポジトリを介してリモートの書籍データを取得するfetchBookDetails()関数、リポジトリを介してRoomデータベースに保存されているデータの更新、削除を行うupdateBook()関数、deleteBook()関数が存在しています。

## Model
### 役割
- データの永続化
- ネットワーク、APIなどを利用したデータの取得

Modelはアプリケーションのデータやビジネスロジックを担っています。データベースやネットワーク通信などの処理を含み、純粋にデータ管理を担当します。

### 具体例
ここではデータクラスを記述するBook.ktを例示します。
<details><summary>Book.kt</summary>

```Kotlin:Book.kt
// BookItemのリストを格納するデータクラス
data class BookLists(
    val items: List<BookItem>?
)

// APIから取得したデータを格納するデータクラス
data class BookItem(
    val id: String,
    val selfLink: String,
    val volumeInfo: VolumeInfo
)

data class VolumeInfo(
    val title: String,
    val authors: List<String>?,
    val publisher: String?,
    val publishedDate: String?,
    val description: String?,
    val imageLinks: ImageLinks = ImageLinks("", ""),
    val pageCount: Int?,
    val categories: List<String>?,
)

data class ImageLinks(
    val smallThumbnail: String,
    val thumbnail: String
)

// データベースに保存するためのデータクラス
// BookItemはネストされたデータクラスであるため、Roomでのエラーを回避するために分離する
@Entity(tableName = "BookData")
data class BookData(
    @PrimaryKey val id: String,
    val title: String,
    val author:String,
    val publisher: String?,
    val publishedDate: String?,
    val description: String?,
    val thumbnail: String,
    val pageCount: Int?,
    var readpage: Int? = 0, //読んだページ数
    var comment: String? = "", //本に対するコメント、感想
    var progress: Int = 0, //登録された本の状態(0:未読,1:読書中,2:読了)
    var registeredDate: String = "", //本が登録された日付
    var updatedDate: String = "" //本の情報が更新された日付
)
```

</details>
BookList,BookItem,VolumeInfo,ImageLinksはGoogle Books APIからデータを格納するのに用いるデータクラスであり、BookDataはRoomデータベースで管理する型となるデータクラスです。
異なる責務を持つデータクラスを別々に設計することで、ソフトウェア設計の原則の1つである「責任の分離」を満たした設計となっています。


</details>

## まとめ
本記事では、ざっくりとMVVMの概要とそのコード例について示しました。
引き続きアーキテクチャについて勉強し、よい設計ができるよう努力したいと思います。
誤っている点などありましたらご指摘いただけますと幸いです。