はじめに
こんにちは!
前回の要件定義の記事に引き続き、今回はアプリの基本設計を行っていきます。要件定義が「何を作るか」を決める地図だとしたら、基本設計は「どう作るか」という具体的な航路を描く、いわばアプリの「骨格」作りです。
いきなりコーディングを始めるのではなく、一度立ち止まって設計を考えることで、手戻りが少なく、見通しの良いコードを書くことができるようになります。今回は、その思考プロセスをログとして残していこうと思います。
設計の全体像:MVVM + UiState
今回のアーキテクチャは、Google推奨の MVVM (Model-View-ViewModel) を採用します。
- View: ユーザーが見る画面 (Jetpack Compose)
- ViewModel: アプリの頭脳。ロジックや状態を管理
- Model: データそのもの
というシンプルな役割分担ですね。そして、今回の設計で一番のこだわりポイントが、**UiState
**という考え方の導入です。
ViewModelは、単に「メモのリスト(List<Memo>
)」をViewに渡すのではありません。「ローディング中」「成功(データあり)」「データが空」「エラー発生」といった、UIが取りうる全ての状態をUiState
として定義し、それごとViewに渡します。
正直、ただのメモアプリにUiState
を導入するのは少し大げさかな?とも悩みました(笑)。でも、今後の拡張性を考えたときや、非同期処理が入ってきたときのことを考えると、最初からこの「型」で設計しておくことが、未来の自分を絶対に助けてくれると信じて、この構成に踏み切りました。
画面設計
画面はシンプルに一つです。ごちゃごちゃさせず、メモの追加と閲覧に集中できるように設計します。
-
画面名: メモ一覧・追加画面 (
MemoScreen.kt
) -
主な構成コンポーネント:
-
MemoInputSection
: テキスト入力欄と「追加」ボタンをまとめた、画面上部の領域。 -
MemoListSection
: メモのリストを表示する領域。UiState
に応じて、ローディング表示や「メモはありません」といった表示を切り替える賢い子です。
-
機能の振る舞い
ユーザー操作によって、各機能がどのように動くのかを定義します。
メモが追加されるまでの流れ
- ユーザーが文字を入力し、「追加」ボタンをタップします。
- (入力チェック): もし入力が空っぽだったら? -> ViewModel側で「空のメモは許可しない」というロジックを入れて、処理を中断させます。ユーザーには何も起こりませんが、不正なデータは登録させません w
-
(正常な流れ): ViewはViewModelの
addMemo()
関数を呼び出します。 - ViewModelは新しいメモデータを作り、リストの先頭に追加して、
UiState
を「成功(Success
)」状態で更新します。 -
UiState
の変化を検知したViewが自動的に再描画され、ユーザーは新しいメモがリストの一番上に追加されたのを確認できます。
データの形を決める(データ設計)
アプリの心臓部とも言える、データの「形」をここで定義しておきます。
Memo.kt
一つのメモがどんな情報を持つのか、という定義です。
// packages/data/model/Memo.kt
import java.time.LocalDateTime
import java.util.UUID
// 一つのメモを表すデータクラス
data class Memo(
// 重複しないようにUUIDで一意なIDを付与
val id: UUID = UUID.randomUUID(),
// メモの本文
val text: String,
// 並び替えに使う作成日時
val createdAt: LocalDateTime = LocalDateTime.now()
)
MemoUiState.kt
今回の設計の肝となる、UIの状態を定義したものです。
// packages/ui/state/MemoUiState.kt
// UIの状態を型安全に表現するためのsealed class
sealed class MemoUiState {
// ローディング中
object Loading : MemoUiState()
// 成功(メモリストを持つ)
data class Success(val memos: List<Memo>) : MemoUiState()
// データが空
object Empty : MemoUiState()
// エラー(エラーメッセージを持つ)
data class Error(val message: String) : MemoUiState()
}
これを使うことで、when式で全ての状態を網羅しないとコンパイルエラーになるので、実装漏れを防げるというメリットもあります。
コードの住所録(パッケージ構成)
最初が肝心、ということで、コードをしまう棚(パッケージ)も最初に決めておきます。
今回は、data
(データ関連)とui
(画面関連)という大きな棚に分けて整理します。
com.takahashi.kazuya.simplememoapp
│
├── data
│ └─ model
│ └─ Memo.kt // データクラス
│
├── ui
│ ├── screen
│ │ └─ MemoScreen.kt // 画面のCompose実装
│ ├── state
│ │ └─ MemoUiState.kt // UI状態の定義
│ └─ viewmodel
│ └─ MemoViewModel.kt// ViewModel
│
└─ MainActivity.kt // アプリのエントリポイント
data
とui
がしっかり分かれているのがポイントですね。
このように役割ごとにパッケージを分けることで、将来アプリが大きくなっても、どこに何があるか迷子になりにくくなります。
まとめ
以上が、今回のメモアプリの基本設計(骨格)になります。
-
MVVMアーキテクチャをベースにする
-
UiStateでUIの状態を厳密に管理する
-
機能やデータの「形」を事前に定義しておく
この骨格に沿って、次回からはいよいよ実装(詳細設計)という肉付け作業に入っていこうと思います。
ここまで読んでいただきありがとうございました!(サボらないように頑張ります w)