Dependency Injectionとアーキテクチャパターン(MVC、MVP、MVVM、Clean Architecture)の相性
FlutterでのDependency Injection(DI)は、様々なアーキテクチャパターンと組み合わせることで、アプリケーションの設計をより効率的に、またメンテナンスしやすくすることができます。
FlutterにおけるDI(依存性注入)について詳しく知りたい方は、以下を参考にしてください!
ここでは、Riverpodを使ったDIの具体例を交えながら、各アーキテクチャパターンとの相性について説明します。
MVC(Model-View-Controller)
MVCは、アプリケーションをModel(データやビジネスロジック)、View(ユーザーインターフェース)、Controller(ModelとViewの間の仲介役)の3つのコンポーネントに分けるアーキテクチャパターンです。
DIの役割とメリット
MVCパターンでは、Model、View、Controllerがそれぞれ独立して役割を持ちます。DIを使用することで、ControllerがModelに依存する部分を外部から注入し、テストやメンテナンスを容易にします。
例
以下は、Riverpodを使用してMVCパターンを実装する例です。
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Model
class ApiService {
Future<String> fetchData() async {
await Future.delayed(Duration(seconds: 1));
return 'Hello from API';
}
}
// Controller
class HomeController {
final ApiService apiService;
HomeController(this.apiService);
Future<String> loadData() {
return apiService.fetchData();
}
}
// Provider
final apiServiceProvider = Provider((ref) => ApiService());
final homeControllerProvider = Provider((ref) => HomeController(ref.read(apiServiceProvider)));
void main() {
runApp(ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('MVC with Riverpod')),
body: HomeView(),
),
);
}
}
// View
class HomeView extends ConsumerWidget {
@override
Widget build(BuildContext context, ScopedReader watch) {
final controller = watch(homeControllerProvider);
return Center(
child: FutureBuilder<String>(
future: controller.loadData(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
} else if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else {
return Text('Data: ${snapshot.data}');
}
},
),
);
}
}
解説
- Model: ApiServiceがデータの取得を担当しています。
- Controller: HomeControllerがApiServiceのインスタンスを受け取り、loadDataメソッドを通じてデータを取得します。依存性はコンストラクタを通じて注入されています。
- Provider: RiverpodのProviderを使ってApiServiceとHomeControllerのインスタンスを管理し、依存性注入を行っています。
- View: HomeViewがユーザーインターフェースを提供し、HomeControllerのデータを表示します。ConsumerWidgetを使ってプロバイダーからHomeControllerを取得しています。
MVP(Model-View-Presenter)
MVPは、Model、View、Presenterの3つのコンポーネントで構成されるアーキテクチャパターンです。PresenterがModelとViewの間のロジックを担当し、ViewはUIの表示のみを担当します。
DIの役割とメリット
MVPパターンでは、PresenterがViewとModelの仲介役を担います。DIを使用することで、Presenterに必要なModelや他の依存関係を注入し、テストしやすい構造にします。
例
以下は、Riverpodを使用してMVPパターンを実装する例です。
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Model
class ApiService {
Future<String> fetchData() async {
await Future.delayed(Duration(seconds: 1));
return 'Hello from API';
}
}
// View
abstract class HomeView {
void showData(String data);
void showError(String message);
}
// Presenter
class HomePresenter {
final HomeView view;
final ApiService apiService;
HomePresenter(this.view, this.apiService);
void loadData() async {
try {
final data = await apiService.fetchData();
view.showData(data);
} catch (e) {
view.showError(e.toString());
}
}
}
// Providers
final apiServiceProvider = Provider((ref) => ApiService());
final homePresenterProvider = Provider.family<HomePresenter, HomeView>((ref, view) {
return HomePresenter(view, ref.read(apiServiceProvider));
});
void main() {
runApp(ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomeViewImpl(),
);
}
}
class HomeViewImpl extends StatefulWidget implements HomeView {
@override
_HomeViewImplState createState() => _HomeViewImplState();
}
class _HomeViewImplState extends State<HomeViewImpl> {
String _data = '';
String _error = '';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('MVP with Riverpod')),
body: Center(
child: _data.isNotEmpty
? Text('Data: $_data')
: _error.isNotEmpty
? Text('Error: $_error')
: ElevatedButton(
onPressed: () => context.read(homePresenterProvider(this)).loadData(),
child: Text('Load Data'),
),
),
);
}
@override
void showData(String data) {
setState(() {
_data = data;
_error = '';
});
}
@override
void showError(String message) {
setState(() {
_error = message;
_data = '';
});
}
}
解説
- Model: ApiServiceがデータの取得を担当しています。
- View: HomeViewがインターフェースとして定義され、HomeViewImplが具体的な実装を行っています。データ表示とエラーメッセージの表示を担当します。
- Presenter: HomePresenterがHomeViewとApiServiceを受け取り、データ取得のロジックを実行します。依存性はコンストラクタを通じて注入されています。
- Provider: RiverpodのProviderとProvider.familyを使って依存性注入を行い、HomePresenterのインスタンスを管理しています。
MVVM(Model-View-ViewModel)
MVVMは、Model、View、ViewModelの3つのコンポーネントで構成されるアーキテクチャパターンです。ViewModelがViewとModelのデータバインディングを担当し、ViewはUIの表示のみを担当します。
DIの役割とメリット
MVVMパターンでは、ViewModelがViewとModelのデータバインディングを担当します。DIを使用することで、ViewModelに必要なModelや他の依存関係を注入し、シンプルでテストしやすいコードを実現します。
例
以下は、Riverpodを使用してMVVMパターンを実装する例です。
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Model
class ApiService {
Future<String> fetchData() async {
await Future.delayed(Duration(seconds: 1));
return 'Hello from API';
}
}
// ViewModel
class HomeViewModel extends StateNotifier<AsyncValue<String>> {
final ApiService apiService;
HomeViewModel(this.apiService) : super(const AsyncValue.loading()) {
loadData();
}
Future<void> loadData() async {
try {
final data = await apiService.fetchData();
state = AsyncValue.data(data);
} catch (e) {
state = AsyncValue.error(e);
}
}
}
// Providers
final apiServiceProvider = Provider((ref) => ApiService());
final homeViewModelProvider = StateNotifierProvider<HomeViewModel, AsyncValue<String>>((ref) {
return HomeViewModel(ref.read(apiServiceProvider));
});
void main() {
runApp(ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomeView(),
);
}
}
// View
class HomeView extends ConsumerWidget {
@override
Widget build(BuildContext context, ScopedReader watch) {
final state = watch(homeViewModelProvider);
return Scaffold(
appBar: AppBar(title: Text('MVVM with Riverpod')),
body: Center(
child: state.when(
data: (data) => Text('Data: $data'),
loading: () => CircularProgressIndicator(),
error: (e, _) => Text('Error: $e'),
),
),
);
}
}
解説
- Model: ApiServiceがデータの取得を担当しています。
- ViewModel: HomeViewModelが状態管理を行い、ApiServiceを使用してデータを取得します。依存性はコンストラクタを通じて注入されています。
- Provider: RiverpodのProviderとStateNotifierProviderを使って依存性注入を行い、HomeViewModelのインスタンスを管理しています。
- View: HomeViewがユーザーインターフェースを提供し、HomeViewModelの状態を監視してデータを表示します。ConsumerWidgetを使ってプロバイダーからHomeViewModelを取得しています。
Clean Architecture
Clean Architectureは、層ごとに責任を分けて設計することを重視します。典型的には、データ、ドメイン、プレゼンテーションの3層または4層で構成され、各層が独立して開発・テストできるようにします。
DIの役割とメリット
Clean Architectureは、層ごとに責任を分けて設計することを重視します。DIを使用することで、各層間の依存関係を明確にし、テスト可能で拡張性の高いコードを実現します。
例
以下は、Riverpodを使用してClean Architectureパターンを実装する例です。
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Data Layer
class ApiService {
Future<String> fetchData() async {
await Future.delayed(Duration(seconds: 1));
return 'Hello from API';
}
}
// Domain Layer
class FetchDataUseCase {
final ApiService apiService;
FetchDataUseCase(this.apiService);
Future<String> execute() {
return apiService.fetchData();
}
}
// Presentation Layer
class HomeViewModel extends StateNotifier<AsyncValue<String>> {
final FetchDataUseCase fetchDataUseCase;
HomeViewModel(this.fetchDataUseCase) : super(const AsyncValue.loading()) {
loadData();
}
Future<void> loadData() async {
try {
final data = await fetchDataUseCase.execute();
state = AsyncValue.data(data);
} catch (e) {
state = AsyncValue.error(e);
}
}
}
// Providers
final apiServiceProvider = Provider((ref) => ApiService());
final fetchDataUseCaseProvider = Provider((ref) => FetchDataUseCase(ref.read(apiServiceProvider)));
final homeViewModelProvider = StateNotifierProvider<HomeViewModel, AsyncValue<String>>((ref) {
return HomeViewModel(ref.read(fetchDataUseCaseProvider));
});
void main() {
runApp(ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context)
解説
- Data Layer: ApiServiceがデータの取得を担当しています。
- Domain Layer: FetchDataUseCaseがビジネスロジックを担当し、ApiServiceからデータを取得します。依存性はコンストラクタを通じて注入されています。
- Presentation Layer: HomeViewModelが状態管理を行い、FetchDataUseCaseを使用してデータを取得します。依存性はコンストラクタを通じて注入されています。
- Provider: RiverpodのProviderとStateNotifierProviderを使って依存性注入を行い、各層のインスタンスを管理しています。
- View: HomeViewがユーザーインターフェースを提供し、HomeViewModelの状態を監視してデータを表示します。ConsumerWidgetを使ってプロバイダーからHomeViewModelを取得しています。
まとめ
各アーキテクチャパターンにおいて、Dependency Injectionは重要な役割を果たし、コードの再利用性やテストのしやすさ、メンテナンス性を向上させます。特に、Riverpodを使用することで、依存関係の管理が容易になり、クリーンでスケーラブルなアプリケーションを構築することができます。
各パターンの選定に少しでも参考になりましたら幸いです。
Discussion