1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

個人開発で「過剰設計」に挫折した話:1人で完結させるためのFlutter + Firebase最小構成

1
Posted at

はじめに

「1人でアプリを開発する時、アーキテクチャはどこまで作り込むべきか?」

個人開発を始めた当初、私は「保守性の高い綺麗なコードを書こう」と意気込み、厳密な Clean Architecture を忠実に再現しようとしました。しかし、結果は大失敗。1つの小さな機能を追加するたびに Domain、Data、Presentation の各レイヤーでファイルを何枚も作成・往復することになり、完全に開発スピードが停滞してしまったのです。

「個人開発の本質は、ユーザーに価値を早く届け、運用しながら改善し続けること」。
そう痛感した私は、過剰な設計(Over-engineering)を削ぎ落とし、1人でストレスなく開発・運用できる「最小限の構成」へリアーキテクチャしました。

本記事では、現在個人アプリを開発・運用する中で行き着いた、**「開発スピードと保守性を両立させるためのリアルな技術選定と設計」**を共有します。


技術選定の軸:なぜ「引き算」をしたのか

1人でUI、バックエンド、インフラ、さらにはプロダクトの運用まで回す必要があったため、技術選定では以下の3つを徹底しました。

  1. ボイラープレート(定型コード)を極限まで減らす
    • 綺麗な設計よりも、コード量が少なく直感的に書けることを最優先。
  2. iOS / Android の差分吸収コストを最小化する
    • OSごとのレイアウト調整に時間を取られないフレームワークを選ぶ。
  3. 「車輪の再発明」をしない
    • 認証やデータベース運用など、BaaSで解決できる部分は自作しない。

採用した技術スタックと「不採用」にした理由

領域 採用技術 検討・不採用にした技術 採用・方向転換の理由
フロント Flutter React Native UI再現性が高く、OS間の表示崩れの修正コストが圧倒的に低いため
バックエンド Firebase 自作API (Go/Node) インフラ構築・保守コストをゼロにし、アプリ本体の開発に集中するため
状態管理 Riverpod Bloc Blocはコード量が多すぎて1人開発では疲弊したため、よりシンプルなRiverpodへ移行
ローカル保持 Hive SQLite 複雑なリレーションが不要なため、キーバリュー型で高速かつ扱いやすいHiveを選択

アーキテクチャ:3レイヤーの「Feature-First」構成

過剰な階層化をやめ、機能単位(Feature-First)でフォルダを分けるシンプルな3レイヤー構成に落ち着きました。

lib/
  ├── src/
  │    ├── features/         # 機能ごとのモジュール
  │    │    ├── auth/
  │    │    └── item/
  │    │         ├── domain/      # モデルクラス (Freezed)
  │    │         ├── data/        # Firestoreとの通信・リポジトリ
  │    │         └── presentation/# UI (Screens, Widgets, Notifier)
  │    └── common/           # 共通UI・ユーティリティ
  └── main.dart
// リポジトリのProvider定義
final itemRepositoryProvider = Provider<ItemRepository>((ref) {
  return FirestoreItemRepository(FirebaseFirestore.instance);
});

// AsyncNotifierを用いた状態管理
final itemListProvider = AsyncNotifierProvider<ItemListNotifier, List<Item>>(() {
  return ItemListNotifier();
});

class ItemListNotifier extends AsyncNotifier<List<Item>> {
  @override
  Future<List<Item>> build() async {
    final repository = ref.watch(itemRepositoryProvider);
    return repository.fetchItems();
  }

  Future<void> addItem(Item newItem) async {
    state = const AsyncValue.loading();
    state = await AsyncValue.guard(() async {
      final repository = ref.read(itemRepositoryProvider);
      await repository.addItem(newItem);
      return repository.fetchItems();
    });
  }
}

開発中に泥臭く解決したハマりポイント(Tips)

  1. キーボード表示時の Bottom Overflowed エラー
    画面下部に固定アクションボタンを置いた際、キーボードを立ち上げると定番のレイアウト崩れが発生しました。

対応:
resizeToAvoidBottomInset: true の指定に加え、SingleChildScrollView + LayoutBuilder でキーボード表示時の高さを動的に計算させるガード処理を入れて回避しました。

  1. FirestoreのRead回数(コスト)跳ね上がり事件
    開発初期、画面遷移のたびに Firestore からデータを直接取得していたため、Console 上の Read 数が勢いよく増えてヒヤッとしました。

対応:
アプリ起動時にローカル(Hive)のキャッシュを参照し、Firestore からは「最終更新日時以降の差分データのみ」を取得するビルドロジックに修正。通信量とFirebaseコストの双方を大幅に抑えられました。

おわりに
個人開発を続けていて痛感するのは、「ベストなアーキテクチャは、プロダクトのフェーズやチーム人数(1人か複数人か)によって常に変わる」ということです。

最初から100点満点の綺麗なコードを目指して開発が止まってしまうくらいなら、まずは60点でも動くものを手早く作り、運用しながらリファクタリングしていく方が、結果としてプロダクトもコードも健やかに育つと感じています。

今後もアプリの改善やパフォーマンスチューニングの過程で得た知見を定期的に発信していきます。

同じく個人開発に取り組まれている方や、同様の構成で開発されている方の参考になれば幸いです。「自分はこんな構成でやっている」といったアイデアがあれば、ぜひコメント欄で教えてください!

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?