Flutterをある程度触ってくると、次のような状態になりやすいです。
- 画面は作れる
- API通信もできる
- 状態管理も一通り触った
- FirebaseやSupabaseも何となく使える
- でもコードがだんだんつらくなってくる
初学者を抜けたあと、多くの人がぶつかるのは、書けないことではなく、増やせないことです。
機能追加をするたびに既存コードが重くなる。
画面ごとの差分は小さいのに、修正範囲は広い。
状態管理ライブラリを導入しても、なぜか保守性が上がった実感がない。
このあたりで悩み始めたなら、次に意識すべきなのは設計パターンの名前よりも、責務分離です。
この記事では、Flutter中級者が次のステップに進むために重要だと思うポイントを整理します。
中級者の壁は、技術不足というより整理不足
Flutter中級者が苦しくなる原因は、Dartの文法やWidget知識の不足ではないことが多いです。
本当にきつくなるのは、1つのファイルや1つのクラスに役割が集まりすぎることです。
たとえば、こんな画面はかなりよくあります。
- 画面描画
- ローディング管理
- API呼び出し
- JSON変換
- バリデーション
- エラーハンドリング
- 成功時のナビゲーション
- Snackbar表示
これらを全部StatefulWidgetやNotifierの中にまとめてしまうと、動いてはいても、だんだん直しにくくなります。
つまり問題は、Flutterが難しいことではなく、責務の境界が曖昧なことです。
状態管理を変えても楽にならない理由
Flutter界隈では、状態管理の話題がとても多いです。
- Provider
- Riverpod
- Bloc
- Cubit
- GetX
- ValueNotifier
どれを使うかは確かに重要です。
ただ、中級者の段階では、ライブラリの選定よりも、何を状態として持つかのほうが重要です。
たとえば、次の2つは別問題です。
- どのライブラリで状態を持つか
- その状態に何を持たせるか
ここが混ざると、状態管理ライブラリを入れ替えても何も改善しません。
よくあるのは、NotifierやControllerに全部載せるパターンです。
- API通信
- 画面イベント
- 表示文言の分岐
- 例外文字列の整形
- フォーム状態
- 画面遷移トリガー
これでは、Widgetからロジックが移っただけで、責務分離はできていません。
状態管理は整理を助ける道具ですが、整理そのものを自動でやってくれるわけではないです。
まず分けるべきは UI、状態、データ取得
中級者が最初に意識すると効果が高いのは、以下の3つを分けることです。
- UI
- 状態
- データ取得
かなりシンプルですが、これだけでも保守性はだいぶ変わります。
UIの責務
- 表示する
- ユーザー操作を受け取る
- 状態に応じて見た目を変える
状態の責務
- ローディング中かどうかを持つ
- 成功・失敗・空データなどの状態を持つ
- UIに必要な整形済みデータを持つ
データ取得の責務
- APIやDBから情報を取ってくる
- 成功時はデータを返す
- 失敗時は例外や結果型で返す
この切り分けをするだけで、Widgetがかなり薄くなります。
画面が太る人は、イベントの置き場所を見直したほうがいい
中級者になると、onPressedやinitStateの中に処理を書き続けてしまう問題が出てきます。
たとえばこういうコードです。
ElevatedButton(
onPressed: () async {
setState(() {
isLoading = true;
});
try {
final response = await api.login(email, password);
if (response.success) {
Navigator.pushReplacementNamed(context, '/home');
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(response.message)),
);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('エラーが発生しました')),
);
} finally {
setState(() {
isLoading = false;
});
}
},
child: const Text('ログイン'),
)
動きます。
でも、この形のまま機能が増えると、画面がどんどんイベント処理の置き場になります。
中級者以降は、Widgetの中に書くべき処理と、外に出すべき処理を意識したほうがいいです。
たとえば、最低でもこう考えるだけで変わります。
- ボタン押下はUIの責務
- ログイン処理はアプリ側の責務
- 成功失敗の状態反映は状態管理側の責務
Repositoryを入れる意味は、テストしやすさより依存の向きを整えること
Flutter学習でRepositoryパターンが出てくると、少し抽象的に感じる人も多いと思います。
でも実務や中規模開発で効いてくるのは、抽象化のかっこよさではなく、依存の整理です。
たとえば、画面やNotifierが直接 Dio や Firebase を触り始めると、上のレイヤーがインフラ実装に強く依存します。
すると次のような問題が出ます。
- APIクライアント変更の影響が大きい
- モックしにくい
- 画面側が通信仕様を知りすぎる
- データ取得方法の差し替えがしづらい
Repositoryを挟むと、上位レイヤーは何を取得したいかだけを意識しやすくなります。
abstract class UserRepository {
Future<User> fetchCurrentUser();
}
これだけでも、画面や状態管理側は、どこから取るかではなく、何を得るかに集中できます。
中級者にとって大事なのは、Repositoryという単語を使うことではなく、依存を下に閉じ込める感覚を持つことです。
Freezedやsealed classを使うと状態がかなり扱いやすくなる
中級者以降でかなり差が出るのが、状態表現です。
Flutterでは、画面状態を bool の組み合わせで管理してしまうケースがよくあります。
bool isLoading = false;
bool hasError = false;
bool isEmpty = false;
List<Item> items = [];
この形はすぐ書けますが、状態の矛盾が起きやすいです。
- isLoading = true なのに items が入っている
- hasError = true なのに isEmpty も true
- 初期状態と空状態の区別が曖昧
こういうときは、状態を列挙的に表したほうが強いです。
sealed class UserState {
const UserState();
}
class Initial extends UserState {
const Initial();
}
class Loading extends UserState {
const Loading();
}
class Success extends UserState {
final User user;
const Success(this.user);
}
class Error extends UserState {
final String message;
const Error(this.message);
}
この形にすると、状態のあり方そのものが整理されます。
中級者が次の段階に進むときは、処理を書く力より、状態を雑にしない力が大事です。
UIモデルを別に持つと画面がかなり読みやすくなる
APIレスポンスのモデルをそのままUIに流すのは、中級者がやりがちな落とし穴です。
たとえばAPIモデルにこういう情報があるとします。
class UserResponse {
final String firstName;
final String lastName;
final String createdAt;
final bool isPremium;
}
これを画面で直接扱うと、UI側で毎回こういう変換が発生します。
- 姓名を結合する
- 日付文字列を整形する
- isPremium を表示文言に変える
この変換が各画面に散ると、画面の責務が増えます。
なので中級者以降は、UIに渡すためのモデルを1段作るだけでもかなり違います。
class UserViewData {
final String displayName;
final String joinedDateText;
final String planLabel;
const UserViewData({
required this.displayName,
required this.joinedDateText,
required this.planLabel,
});
}
これを挟むと、Widgetは表示に集中しやすくなります。
画面ごとに全部最適化しない
Flutterに慣れてくると、設計を頑張りすぎるフェーズにも入りやすいです。
- 全画面に共通抽象化を入れる
- 汎用Widgetを作りすぎる
- 将来使うかもしれないRepositoryを先に作る
- hooksやmixinを多用する
- extensionを増やしすぎる
これはこれで別のつらさを生みます。
中級者に必要なのは、全部を美しくすることではなく、変更頻度の高いところを壊れにくくすることです。
特に意識するといいのは次の3つです。
- 画面単位で責務を切る
- 再利用が3回以上出てから共通化する
- 抽象化で説明コストが増えるならやりすぎを疑う
FlutterはUI中心の開発になることが多いので、抽象化より見通しの良さが勝つ場面がかなり多いです。
フォルダ構成は正解を探すより、責務が伝わることを優先したほうがいい
中級者になると、フォルダ構成も気になり始めます。
- feature-first
- layer-first
- clean architecture
- MVVM風
- domain drivenっぽい構成
このあたりは確かに重要ですが、最初から完璧な構成を選ぶ必要はないです。
むしろ大事なのは、チームや未来の自分が見たときに責務がわかることです。
たとえば feature-first なら、こんな形はかなり扱いやすいです。
lib/
features/
auth/
data/
domain/
presentation/
home/
data/
domain/
presentation/
この形の良さは、機能ごとの関心事がまとまりやすいことです。
中級者の段階では、どの思想が正しいかを議論するより、影響範囲が追いやすい構造を選ぶほうが実践的です。
非同期処理で崩れやすい人は、成功時しか考えていないことが多い
Flutter中級者がアプリを作っていて急に苦しくなるのが、非同期処理の増加です。
たとえば、API通信ひとつでも実際には状態がいくつもあります。
- 未実行
- 実行中
- 成功
- 空データ
- リトライ可能な失敗
- ログイン切れ
- タイムアウト
ここを成功パス中心で組んでいると、あとから例外分岐が大量に増えて崩れます。
なので、中級者以降は次の観点を入れると強いです。
- ローディング中の重複タップを防ぐ
- dispose後の反映を避ける
- エラーの粒度を分ける
- リトライ導線を最初から置く
- 画面に出すエラーとログ用のエラーを分ける
見た目は地味ですが、このあたりがアプリの品質をかなり左右します。
中級者が一段上がるには、便利ライブラリよりレビュー観点を増やすほうが効く
中級者の成長で意外と大事なのが、コードレビューの観点です。
自分のコードを見返すときに、次のような視点を持てるだけでだいぶ変わります。
- このWidgetは表示に専念できているか
- 状態は矛盾しない形で表現できているか
- APIレスポンスをそのままUIに流していないか
- エラー時の動きが曖昧ではないか
- 同じ整形処理が複数画面に散っていないか
- 変更時にどこまで影響するか想像できるか
ライブラリを増やすと一時的に前進した感じは出ます。
でも本質的に効くのは、コードの見方が変わることです。
個人的に、中級者が最初に意識すると効果が高い順番
もし今、Flutterをある程度書けるけどコードが散らかり始めているなら、次の順番をおすすめしたいです。
- 画面からAPI呼び出しを直接減らす
- 画面状態を bool 群ではなく状態クラスで表す
- APIモデルとUIモデルを分ける
- エラーとローディングの扱いを整理する
- フォルダ構成を機能単位で見直す
- 状態管理ライブラリの使い方を整理する
- 必要な範囲だけテストを書く
この順番なら、学習コストに対して保守性の改善が大きいです。
まとめ
Flutter中級者が次に伸ばすべきポイントは、難しい設計用語を覚えることよりも、責務分離を体で理解することだと思っています。
- UIは表示に寄せる
- 状態は状態として整理する
- データ取得は外に逃がす
- APIモデルをそのまま画面に渡さない
- 変更されやすい場所を薄くする
このあたりを意識し始めると、同じFlutterでもコードのしんどさがかなり変わってきます。
初学者の頃は、とにかく動かすことが大切です。
でも中級者以降は、動くコードを増やせる形で書くことが重要になります。
Flutterは書けるようになってからが本番です。
だからこそ、次の一歩として、状態管理ライブラリの比較より先に、責務の境界を見直してみるのがおすすめです。
さいごに
Flutterを基礎から実務で困りやすいポイントまで体系的に学びたい方向けに、Udemyで講座も出しています。
以下のリンクは、30日間使える半額クーポン付きです。