1
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アプリの「神経」を作る - シンプルなメモアプリの詳細設計ログ

Posted at

はじめに

こんにちは!

前回の基本設計の記事でアプリの「骨格」を固めたので、今回はその骨格に「神経」と「筋肉」を通わせていく詳細設計のログを公開します。

基本設計が「家の間取り図」だとしたら、詳細設計は**「壁の中の配線図」や「使うネジの種類まで書かれた施工図」**のようなものです。これを完成させれば、あとはもう何も考えず、ただただコードを書くことに集中できる、という状態を目指します(笑)。


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といった、意味のある単位で関数を分割しています。こうすることで、コードの見通しが良くなり、修正も楽になります。


まとめと次のステップ

以上が、実装に着手するための最終設計図となります。
全てのクラス、全ての関数に「なぜそうするのか」という意図が込められており、これに従えばモダンで堅牢なアプリが完成するはずです。

ここまで来たら、もう迷うことは何もありません。
いよいよ次回から、この設計書を元に実装フェーズに入っていきます!
ここまで読んでいただきありがとうございました!

1
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
1
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?