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

FlutterでMVVMアーキテクチャ + Riverpodを採用した構成例

Posted at

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アーキテクチャになります。

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