1. はじめに
1.1. 目的
本ドキュメントは、先日作成した基本設計書に基づき、シンプルメモアプリにメモの削除機能を実装するための詳細設計を定義します。
このドキュメントのゴールは、実装作業に着手する際に、各コンポーネントが何をすべきかを明確にし、MVVMアーキテクチャの原則に則った、保守性の高いコードを実装することにあります。
1.2. 設計思想
-
単方向データフロー (UDF): UIからのイベントは
ViewModel
へ、ViewModel
からの状態はUIへという一方向の流れを徹底します。 - 関心の分離 (SoC): 各層(UI, ViewModel, Repository, DAO)が自身の責務に集中できるように、役割を明確に分離します。
-
リアクティブな更新:
Room
のFlow
を活用し、データベースからデータが削除されたら、UIが自動的に更新される仕組みを維持します。
2. アーキテクチャと情報の流れ
削除機能における、情報の流れは以下の通りです。これはMVVMの基本となる重要なフローなので、面談でも説明できるようにしておきましょう。
[ ユーザー操作 (削除ボタンTap) ]
↓ (Event)
+-----------------------------+
| UI層 (MemoScreen.kt) |
| └─ MemoItem.kt |
| (onDeleteClickを通知) |
+--------------|--------------+
↓ (deleteMemoを呼び出し)
+--------------|--------------+
| ViewModel層 (頭脳) |
| (MemoViewModel.kt) |
| (ロジック実行を指示) |
+--------------|--------------+
↓ (deleteを依頼)
+--------------|--------------+
| Repository層 (データの司書) |
| (MemoRepository.kt) |
| (データ操作を抽象化) |
+--------------|--------------+
↓ (DB操作を命令)
+--------------|--------------+
| データ層 (Room) |
| └─ MemoDao.kt |
| (DBからレコードを削除) |
+-----------------------------+
Roomでデータが削除されると、Repositoryが公開しているallMemosのFlowが自動的に新しいリストを発行し、それがViewModelを経由してUIに伝わり、画面が再コンポーズ(再描画)されます。
3. Step by Step 詳細設計
3.1. Step 1: データ層の拡張 (DAO)
まず、データベースの「操作マニュアル」に、「削除」という新しい操作を追加します。
ファイル: data/local/dao/MemoDao.kt
package com.example.simplememoapp_android.data.local.dao
import androidx.room.Dao
import androidx.room.Delete // ← Deleteアノテーションをインポート
import androidx.room.Insert
import androidx.room.Query
import com.example.simplememoapp_android.data.model.Memo
import kotlinx.coroutines.flow.Flow
@Dao
interface MemoDao {
// ... (getAllMemos, insertMemo は既存) ...
/**
* 指定されたメモをデータベースから削除します。
* @param memo 削除対象のMemoオブジェクト。主キー(id)が参照されます。
*/
@Delete
suspend fun deleteMemo(memo: Memo) // ← これを追加
}
【💡MVVM学習ポイント】
@Deleteアノテーションを付けるだけで、Roomが賢く「このMemoオブジェクトの主キー(id)と同じ行をテーブルから削除する」というSQLを自動生成してくれます。DAOは、あくまで「どんな操作ができるか」を定義するだけの、クリーンなインターフェースであり続けます。
3.2. Step 2: Repository層の拡張
次に、「データの司書」に、ViewModelからの削除依頼を受け付ける新しい窓口を作ります。
ファイル: data/repository/MemoRepository.kt
package com.example.simplememoapp_android.data.repository
import com.example.simplememoapp_android.data.local.dao.MemoDao
import com.example.simplememoapp_android.data.model.Memo
import kotlinx.coroutines.flow.Flow
import java.time.LocalDateTime
class MemoRepository(private val memoDao: MemoDao) {
// ... (allMemos, insert は既存) ...
/**
* 指定されたメモをデータベースから削除します。
* ViewModelからの要求をDAOに仲介します。
* @param memo 削除対象のMemoオブジェクト。
*/
suspend fun delete(memo: Memo) { // ← これを追加
try {
memoDao.deleteMemo(memo)
} catch (e: Exception) {
// エラーハンドリング(今回はログ出力のみ)
println("メモの削除に失敗しました: ${e.message}")
}
}
}
【💡MVVM学習ポイント】
Repositoryの役割は、ViewModelがデータベース(DAO)の具体的な実装を知らなくてもいいように、データ操作を**抽象化(隠蔽)**することです。ViewModelは、ただこのdelete関数を呼ぶだけで、中で何が起きているかを知る必要はありません。
3.3. Step 3: ViewModel層の拡張
「シェフ」に、ウェイターからの「この料理を下げてくれ」という指示を処理する新しいレシピを教えます。
ファイル: ui/viewmodel/MemoViewModel.kt
package com.example.simplememoapp_android.ui.viewmodel
// ... (既存のimport) ...
import com.example.simplememoapp_android.data.model.Memo // Memoをインポート
class MemoViewModel(private val repository: MemoRepository) : ViewModel() {
// ... (uiState, addMemo は既存) ...
/**
* 指定されたメモを削除するようRepositoryに依頼します。
* DB操作はUIスレッドをブロックしないよう、viewModelScopeで実行します。
* @param memo 削除対象のMemoオブジェクト。
*/
fun deleteMemo(memo: Memo) { // ← これを追加
viewModelScope.launch {
repository.delete(memo)
}
}
}
【💡MVVM学習ポイント】
ViewModelの責務は、UIからのイベント(「このメモを消して!」)を受け取り、それをビジネスロジック(Repositoryに削除を依頼する)に変換することです。UIの状態は、repository.allMemosのFlowが自動的に更新してくれるため、ViewModelは削除後に手動でuiStateを更新する必要がありません。これぞリアクティブプログラミングの力です。
3.4. Step 4: UI層の改修
最後に、ユーザーが実際に触る「ウェイター」たちに、削除ボタンの設置と、上司への報告ルートを教えます。
3.4.1. MemoItemの改造 (現場担当者)
ファイル: ui/screen/MemoScreen.kt
// ... (必要なimportを追加) ...
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
// MemoItemの引数に、削除イベントを通知するためのラムダ(関数)を追加
@Composable
private fun MemoItem(
memo: Memo,
onDeleteClick: (Memo) -> Unit // ← これを追加!
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 4.dp)
) {
Row( // Cardの中身をRowにして、テキストとボタンを横並びにする
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = memo.text,
modifier = Modifier
.weight(1f) // テキストが残りのスペースを全て使う
.padding(16.dp)
)
// ゴミ箱アイコンのボタンを追加
IconButton(onClick = { onDeleteClick(memo) }) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "メモを削除"
)
}
}
}
}
3.4.2. MemoListSectionの修正 (課長)
ファイル: ui/screen/MemoScreen.kt
// MemoListSectionは、onDeleteClickをMemoItemに渡す役割を担う
@Composable
private fun MemoListSection(
memos: List<Memo>,
onDeleteClick: (Memo) -> Unit // ← これを追加!
) {
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
items(memos) { memo ->
MemoItem(
memo = memo,
onDeleteClick = onDeleteClick // ← そのまま部下(MemoItem)に渡す
)
}
}
}
3.4.3. MemoScreenの修正 (部長)
ファイル: ui/screen/MemoScreen.kt
// MemoScreenが、ViewModel(CEO)の関数を呼び出す最終的な責任者
@Composable
fun MemoScreen(...) { // 引数は前回のDI実装のまま
// ... (viewModelの取得など) ...
// ...
when (val state = uiState) {
is MemoUiState.Success -> {
MemoListSection(
memos = state.memos,
onDeleteClick = { memo -> // ← 課長(MemoListSection)からの報告をここで処理
viewModel.deleteMemo(memo) // CEO(ViewModel)に指示を仰ぐ
}
)
}
// ... (Empty, Loadingなどの他の状態) ...
}
// ...
}
【💡MVVM学習ポイント】
onDeleteClickというイベントを、MemoItem → MemoListSection → MemoScreenへと、バケツリレーのように渡しています。これをイベントのコールバックと言います。UI部品は「ボタンが押されたよ」と上に報告するだけで、具体的な処理(ViewModelを呼ぶ)は、ViewModelを直接知っているMemoScreenだけが責任を持つ。この役割分担が、再利用性の高いUIを作るためのState Hoistingの考え方そのものです。
4. まとめ
以上が、削除機能実装のための全ステップです。
各層に少しずつ変更を加えるだけで、一つの機能が完成していく様子が分かったかと思います。
この**「変更が各層にきれいに分離されている」**状態こそ、MVVMアーキテクチャがもたらす最大の恩恵です。