FlutterでMVVM + Riverpod + Freezed + Retrofitを採用した実践アーキテクチャ
アプリが成長すると
- UIとロジックの密結合
- APIコードの肥大化
- 状態管理の複雑化
が起きがちです。
それを防ぐために本記事では
MVVM + Riverpod + Repository + Freezed + Retrofit
を組み合わせた、スケールしても崩れにくい構成を紹介します。
🗂 ディレクトリ構成
lib/
├─ view/
├─ viewmodel/
├─ repository/
├─ datasource/api/
└─ model/
🧊 Model層(Freezed)
役割
アプリ内で扱うデータ構造の定義。
この層のメリット
| メリット | 内容 |
|---|---|
| 不変オブジェクト | 意図しない変更防止 |
| copyWith生成 | 状態更新が安全 |
| 等価比較自動 | Riverpod再描画最適化 |
| JSON変換対応 | API連携が簡単 |
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user.freezed.dart';
part 'user.g.dart';
@freezed
class User with _$User {
const factory User({
required String id,
required String name,
String? email,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) =>
_$UserFromJson(json);
}
🌐 DataSource層(Retrofit API)
役割
外部APIとの通信を担当する層。
この層のメリット
| メリット | 内容 |
|---|---|
| HTTPコード削減 | APIクライアント自動生成 |
| 型安全 | レスポンスがモデル直結 |
| 保守性向上 | エンドポイント管理が集中 |
import 'package:retrofit/retrofit.dart';
import 'package:dio/dio.dart';
import '../../model/user.dart';
part 'user_api.g.dart';
@RestApi(baseUrl: "https://example.com")
abstract class UserApi {
factory UserApi(Dio dio, {String baseUrl}) = _UserApi;
@GET("/user")
Future<User> fetchUser();
}
🗃 Repository層
役割
データ取得の契約を定義し、DataSourceとViewModelの仲介をする層。
この層のメリット
| メリット | 内容 |
|---|---|
| API変更に強い | 実装差し替え可能 |
| テスト容易 | モック差し替え可能 |
| 依存逆転 | UIがインフラに依存しない |
abstract class UserRepository {
Future<User> fetchUser();
}
class UserRepositoryImpl implements UserRepository {
final UserApi api;
UserRepositoryImpl(this.api);
@override
Future<User> fetchUser() {
return api.fetchUser();
}
}
🧠 ViewModel層(Riverpod)
役割
UI状態管理とビジネスロジックを担当。
この層のメリット
| メリット | 内容 |
|---|---|
| 状態管理が明確 | AsyncValueで簡潔 |
| 依存注入 | Provider経由で注入 |
| テスト容易 | override可能 |
final dioProvider = Provider<Dio>((ref) => Dio());
final userApiProvider = Provider<UserApi>((ref) {
return UserApi(ref.read(dioProvider));
});
final userRepositoryProvider = Provider<UserRepository>((ref) {
return UserRepositoryImpl(ref.read(userApiProvider));
});
final userProvider =
StateNotifierProvider<UserViewModel, AsyncValue<User?>>((ref) {
return UserViewModel(ref.read(userRepositoryProvider));
});
class UserViewModel extends StateNotifier<AsyncValue<User?>> {
final UserRepository repo;
UserViewModel(this.repo) : super(const AsyncValue.loading());
Future<void> fetchUser() async {
try {
final user = await repo.fetchUser();
state = AsyncValue.data(user);
} catch (e, st) {
state = AsyncValue.error(e, st);
}
}
}
🖥 View層(UI)
役割
データ表示のみ担当。ロジックを持たない。
この層のメリット
| メリット | 内容 |
|---|---|
| UIとロジック分離 | 変更が安全 |
| テスト容易 | Widget単体テスト可 |
| 再利用可能 | UIパーツの独立性 |
class UserPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final userState = ref.watch(userProvider);
return Scaffold(
appBar: AppBar(title: Text("User")),
body: userState.when(
data: (user) => user == null
? Text("No user")
: Center(child: Text(user.name)),
loading: () => Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text("Error: $e")),
),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(userProvider.notifier).fetchUser(),
child: Icon(Icons.download),
),
);
}
}
🏁 まとめ
この構成では責務が明確に分離されます。
| 層 | 担当 |
|---|---|
| Model | データ定義 |
| DataSource | API通信 |
| Repository | データ取得抽象化 |
| ViewModel | 状態管理 |
| View | 表示 |
結果として
- スケールしても崩れにくい
- テストしやすい
- API変更に強い
実務向けの堅牢なFlutterアーキテクチャになります。