Flutterで機能を開発する順序を整理しようと思います。
機能定義と要件分析の後にはおおまかな画面ができている必要があり、UIおよびUX設計の前までには画面デザインが出来上がっていないと画面開発ができないと思います。
開発の順序#
1. 機能定義と要件分析#
- まず、特定の機能(一つの流れ)がどのように動作すべきかを明確に定義します。
- 例えば、予定作成であればユーザーが予定のタイトル、日付、時間、場所などを入力して保存できる機能を作ります。
2. ドメインモデルの定義 (Domain Model)#
- 機能を実行するためのおおまかなドメインモデルを定義します。予定に関連する場合、入力値が保存されるモデルやUIで処理される必要があるデータのモデルなど、複数のモデルが一つの機能のために作られることがあります。
- モデルはドメインレイヤーでアプリケーションのコアビジネスロジックとデータをカプセル化します。
class Appointment {
final String title;
final DateTime date;
final String location;
Appointment({
required this.title,
required this.date,
required this.location,
});
}3. リポジトリパターンの実装 (Repository Pattern)#
- ドメインモデルを保存・取得する処理のためにRepositoryを設計します。
- 例えば、予定データをローカルDBまたはリモートサーバーに保存・取得するロジックを実装します。
abstract class AppointmentRepository {
Future<void> saveAppointment(Appointment appointment);
Future<List<Appointment>> getAppointments();
}- この際、実際のデータソースを扱う
LocalAppointmentRepositoryやRemoteAppointmentRepositoryのような具体的な実装を作成します。
class LocalAppointmentRepository implements AppointmentRepository {
@override
Future<void> saveAppointment(Appointment appointment) async {
// Local storage logic here
}
@override
Future<List<Appointment>> getAppointments() async {
// Retrieve data from local storage
}
}4. アプリケーションサービスレイヤーの定義 (Application Layer)#
- ドメインモデルとリポジトリを使用してビジネスロジックを実行するサービスクラスを定義します。
- このレイヤーは予定作成などのビジネスロジックをカプセル化し、Presentation Layerから簡単に呼び出せるようにします。必ずしもそうとは限りませんが、ここでのみドメインが生成されるべきという制約を設けるのも良いでしょう。データを参照する場合はすでにドメインを持っているため、該当しない場合もあります。
class CreateAppointmentService {
final AppointmentRepository repository;
CreateAppointmentService(this.repository);
Future<void> createAppointment(String title, DateTime date, String location) async {
final appointment = Appointment(title: title, date: date, location: location);
await repository.saveAppointment(appointment);
}
}5. プレゼンテーションレイヤーの設計 (Presentation Layer)#
- プレゼンテーションレイヤーはUIとアプリケーションの状態を管理する部分です。
Riverpodを活用して状態を管理し、UIをユーザーとインタラクションするように設計します。- repository、serviceもriverpodを利用します。
part 'appointment_controller.g.dart';
@riverpod
class AppointmentController extends _$AppointmentController {
@override
Appointment build() async {
return Appointment.empty();
}
Future<Appointment> createAppointment(String title, DateTime date, String location) {
state = state.copyWith(title: title, date: date, location: location);
final service = ref.read(createAppointmentServiceProvider);
final appointment = await service.createAppointment(title, date, location);
return appointment;
}
}- UIでこの
appointmentControllerProviderを使って状態を読み取り、予定を作成できるようにします。
class CreateAppointmentScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final appointment = ref.watch(appointmentControllerProvider);
return Scaffold(
appBar: AppBar(title: Text('Create Appointment')),
body: Column(
children: [
// Input fields for title, date, location...
ElevatedButton(
onPressed: () {
// Trigger createAppointment
},
child: Text('Save Appointment'),
),
ListTile(
title: Text(appointment.title),
subtitle: Text('${appointment.date} at ${appointment.location}'),
),
],
),
);
}
}6. UIおよびUX設計#
- 上記のプレゼンテーションレイヤーに基づいてUIを設計し、必要なフォームとウィジェットを実装します。
- 画面遷移、入力検証、ユーザビリティテストなどを通じて最終UIを完成させます。
- UIのウィジェットにはnormal、loading、success、error、emptyといった状態があるため、それぞれの状態でどう処理し、どう表示するかを決定する必要があります。
7. テストの作成#
- 作成した各レイヤー(ドメインモデル、リポジトリ、サービス、プレゼンテーション)に対してユニットテストと統合テストを作成します。
- TDD方式で各段階でテストを行うのも良い方法です。
- テストを通じてアプリケーションが意図した通りに動作するかを検証します。
8. 結合と最適化#
- すべてのコンポーネントを結合して機能全体をテストし、必要に応じて最適化作業を行います。
- mainブランチとマージしながらデプロイ自動化を通じて新しいバージョンをリリースします。
まとめ#
- 機能定義とドメインモデル設計
- リポジトリパターンによるデータ保存・管理
- アプリケーションサービスレイヤーでビジネスロジックの実装
- プレゼンテーションレイヤーをRiverpodで状態管理およびUI実装
- UI設計とUX最適化
- テスト作成と検証
開発の順序と合わせて知っておくと役立つこと:
アジャイル手法の導入:スプリント設定 -> 一定期間中に特定機能に集中開発 -> 大きなプロジェクトなら小さな単位に分けて開発、優先順位の設定。
TDDの活用:コード作成前にテストケースをまず作成 -> テストをパスするコードを作成
DevOpsの導入:コードレビュー、テスト自動化 & CI/CD
状態管理#
状態管理とは:UIとデータ(状態)間の一貫性を維持する方法です。データ(状態)が変更されるたびにUIがそれに応じて更新され、ユーザーに最新情報を表示できるようにすることです。状態管理ライブラリを使う理由は、コードの利便性とレンダリング効率のためです。
非同期リクエスト、キャッシュ、エラーおよびローディング状態の処理は以下の機能に影響します。
- プルして更新
- 無限リスト / スクロール時のフェッチ
- タイピング中の検索
- 非同期リクエストのデバウンシング(Debouncing)
- 不要になった非同期リクエストのキャンセル
- オプティミスティック(Optimistic) UI
- オフラインモード
- ...
単純な状態管理ではRiverpodの使用を避けましょう。Riverpodでは、Riverpodの使用に適した状態管理のケースとしてビジネスロジックの場合にのみ使用することを推奨しています。RiverpodでUIロジックと状態管理を分離する場合に使用してください。
Riverpodはエラー、ローディング状態が必要なデータを処理するために使用すると考えれば、限定的に使うことができます。
ローカルウィジェットの状態にproviderを使用しないでください
Providerは共有ビジネス状態(shared business state)用に設計されています。
ローカルウィジェットの状態のような用途には適していません:
- フォーム(form)の状態保存
- 現在選択されている項目
- アニメーション
- 一般的にFlutterが「controller」(例:
TextEditingController)で処理するすべてのもの
ローカルウィジェットの状態を処理する方法を探しているなら、代わりにflutter_hooksを使用することをお勧めします。
これを推奨しない理由の一つは、このような状態がルートにスコープされる(scoped to a route)場合が多いためです。そうしないと、新しいページが前のページの状態を上書きしてしまうため、アプリの戻るボタンが壊れる可能性があります。
You have to give up some of the old so that you can make room for the new.
— Yanni