はじめに
こんにちは!
前回の基本設計の記事でアプリの「骨格」を固めたので、今回はその骨格に「神経」と「筋肉」を通わせていく詳細設計のログを公開します。
基本設計が「家の間取り図」だとしたら、詳細設計は**「壁の中の配線図」や「使うネジの種類まで書かれた施工図」**のようなものです。これを完成させれば、あとはもう何も考えず、ただただコードを書くことに集中できる、という状態を目指します(笑)。
1. データ層の設計 (dataパッケージ)
まずはアプリの血液となるデータの部分から。基本設計で決めたデータ構造を、そのままコードの形に落とし込みます。
Memo.kt - メモの設計図
一つのメモが持つべき情報を定義します。
package com.takahashi.kazuya.simplememoapp.data.model
import java.time.LocalDateTime
import java.util.UUID
data class Memo(
val id: UUID = UUID.randomUUID(),
val text: String,
val createdAt: LocalDateTime = LocalDateTime.now()
)
設計のポイント:
- idにUUIDを使うことで、将来DBに保存することになっても、まず重複しない一意なIDを保証できます。
- createdAtを持たせることで、「新しい順」といったソートが簡単にできるようになります。
MemoUiState.kt - UI状態の設計図
UIが表示すべき全パターンを、型安全に定義します。今回の設計の肝となる部分です。
package com.takahashi.kazuya.simplememoapp.ui.state
import com.takahashi.kazuya.simplememoapp.data.model.Memo
sealed class MemoUiState {
object Loading : MemoUiState()
data class Success(val memos: List<Memo>) : MemoUiState()
object Empty : MemoUiState()
data class Error(val message: String) : MemoUiState()
}
設計のポイント:
sealed classを使うことで、when式でUIの状態を網羅的に扱うことができ、「ローディング画面の実装を忘れてた!」みたいなバグをコンパイラレベルで防げます。未来の自分を助けるための、賢い選択ですね w
2. UI層の設計 (uiパッケージ)
次に、ユーザーが直接触れる画面の部分を設計していきます。
MemoViewModel.kt - アプリの頭脳
UIに表示する状態を作り出し、ユーザーからの操作を受け付ける、まさにアプリの頭脳です。
package com.takahashi.kazuya.simplememoapp.ui.viewmodel
import androidx.lifecycle.ViewModel
import com.takahashi.kazuya.simplememoapp.data.model.Memo
import com.takahashi.kazuya.simplememoapp.ui.state.MemoUiState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
class MemoViewModel : ViewModel() {
// ViewModelの内部でのみ変更可能なUI状態
private val _uiState = MutableStateFlow<MemoUiState>(MemoUiState.Empty)
// UIへは、変更不可能な読み取り専用のStateFlowとして公開する
val uiState: StateFlow<MemoUiState> = _uiState.asStateFlow()
/**
* 新しいメモを追加する
* @param text 入力されたメモの本文
*/
fun addMemo(text: String) {
// 入力チェック:空白文字のみの場合は何もしない
if (text.isBlank()) {
return
}
val newMemo = Memo(
text = text.trim() // 前後の空白は除去
)
// 現在の状態を「不変的」に更新する
_uiState.update { currentState ->
val currentMemos = if (currentState is MemoUiState.Success) {
currentState.memos
} else {
emptyList()
}
val newMemos = listOf(newMemo) + currentMemos
MemoUiState.Success(newMemos)
}
}
}
設計のポイント:
-
カプセル化: _uiStateをprivateにして、UI側から勝手に状態を書き換えられないようにしています。状態の変更は必ずaddMemoのようなViewModelの関数を通して行われる、というルールを徹底することで、データフローが予測可能になります。
-
責務の分離: 入力された文字が空かどうかのチェック(isBlank)はViewModelの責務です。UIはただ「ボタンが押されたよ」と通知するだけで、ビジネスロジックには関与しません。
MemoScreen.kt - 状態をUIに映す鏡
ViewModelが作った「状態」を受け取って、それを画面に描画することに専念します。
package com.takahashi.kazuya.simplememoapp.ui.screen
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.viewmodel.compose.viewModel
import com.takahashi.kazuya.simplememoapp.ui.state.MemoUiState
import com.takahashi.kazuya.simplememoapp.ui.viewmodel.MemoViewModel
@Composable
fun MemoScreen(viewModel: MemoViewModel = viewModel()) {
// ViewModelのUiStateを監視
val uiState by viewModel.uiState.collectAsState()
Scaffold(
topBar = { /* TopAppBarの実装 */ }
) { paddingValues ->
Column(modifier = Modifier.padding(paddingValues)) {
// 1. メモ入力エリア
MemoInputSection(onAddClick = { text ->
viewModel.addMemo(text)
})
// 2. UiStateに応じて表示を切り替える
when (val state = uiState) {
is MemoUiState.Empty -> { /* 「メモはありません」という表示 */ }
is MemoUiState.Error -> { /* エラーメッセージ表示 */ }
is MemoUiState.Loading -> { /* ローディングインジケータ表示 */ }
is MemoUiState.Success -> {
// 3. メモリスト表示
MemoListSection(memos = state.memos)
}
}
}
}
}
@Composable
private fun MemoInputSection(onAddClick: (String) -> Unit) {
// このComposable内でのみ使うテキストの状態
var text by remember { mutableStateOf("") }
// Row { TextField, Button } の実装
// ButtonのonClickで onAddClick(text) を呼び出し、 text = "" で入力欄をクリア
}
@Composable
private fun MemoListSection(memos: List<Memo>) {
// LazyColumn { ... } の実装
}
設計のポイント:
-
State Hoisting (状態の巻き上げ): MemoInputSectionに注目してください。入力中のテキスト(text)という状態はMemoInputSection自身が持ちますが、「追加ボタンが押された」というイベントは、onAddClickという形で親のMemoScreenに通知しています。これにより、MemoInputScreenは「テキストを入力して、ボタンが押されたら知らせる」という単純な役割に専念でき、再利用性が格段に上がります。これはComposeを使いこなす上で非常に重要な考え方です。
-
Composableの分割: MemoScreenという一つの大きな関数に全てを書くのではなく、MemoInputSectionやMemoListSectionといった、意味のある単位で関数を分割しています。こうすることで、コードの見通しが良くなり、修正も楽になります。
まとめと次のステップ
以上が、実装に着手するための最終設計図となります。
全てのクラス、全ての関数に「なぜそうするのか」という意図が込められており、これに従えばモダンで堅牢なアプリが完成するはずです。
ここまで来たら、もう迷うことは何もありません。
いよいよ次回から、この設計書を元に実装フェーズに入っていきます!
ここまで読んでいただきありがとうございました!