軽量DIライブラリ lite_ref で小中規模Flutterアプリを整理する
はじめに
Flutter開発では、画面間でサービスやデータを共有する場面が多くあります。インスタンス生成のコードが散らばると、重複やバグが発生しやすくなります。特に複雑なアプリでは、オブジェクトの生成と寿命の管理が難しくなります。
依存注入(DI)ライブラリはこの問題を解決します。サービスやリポジトリを一元管理することで、コードの見通しと保守性が向上します。しかし、多機能なフレームワークは小規模アプリには過剰な場合があります。
本記事では軽量DIライブラリ「lite_ref」を紹介します。Flutter標準機能を活用し、最小限のコードでDIを実現します。「小中規模アプリで設計を簡潔にしたい」「依存関係を整理したいが複雑な状態管理は不要」というニーズに最適です。
lite_ref
とは何か
lite_refは「軽量な参照」を扱うパッケージです。状態管理フレームワークと異なり、画面の再ビルド機能はほとんどありません。代わりに、サービスやViewModelをアプリ全体または画面単位で管理する機能に特化しています。
LiteRefScope
ウィジェットでアプリを包み、その中で「Ref」を使って依存を登録します。登録されたオブジェクトはBuildContextを通じてどこからでも取得できます。Riverpodと違って状態変化の自動監視はありませんが、APIがシンプルで学習コストが低いです。
画面ごとに異なるサービスを使い分けたり、テスト時にモックを注入したりする場合は、Ref.scoped
やoverrideWith
を使います。「必要最小限のDI」と「BuildContextの直接活用」を重視する設計です。
小中規模アプリになぜおすすめ?
アプリが成長すると、無計画なコード追加では限界があります。RiverpodやReduxなどの複雑なフレームワークは、小規模アプリでは学習コストや設定の煩雑さが目立ちます。
lite_refは「どこでどのオブジェクトを使うか」を明確にする点に注力しています。依存関係の整理さえできていれば、状態管理はFlutterの標準機能で十分です。シンプルなAPIでサービス管理ができ、コード全体がすっきりします。
また、scoped
な依存定義時にBuildContextを受け取れるため、GoRouterなどのUI依存オブジェクトを直接参照できます。これによりViewModelからのナビゲーション呼び出しが簡単になります。UIとロジックの完全分離を求めない場合、実用的な選択肢です。
lite_refではLiteRefScope
と数行のDI定義だけで、画面間の依存共有が簡単になります。複雑な設計なしでアプリの可読性と保守性が向上します。
基本的な使い方
lite_refを使うには、まず依存関係を追加します。
dependencies:
flutter:
sdk: flutter
lite_ref: ^1.0.0 # バージョンは例示
次にアプリをLiteRefScopeで包みます。
import 'package:flutter/material.dart';
import 'package:lite_ref/lite_ref.dart';
void main() {
runApp(
LiteRefScope(
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
title: 'lite_ref Demo',
home: HomePage(),
);
}
}
これでRef経由の依存取得が可能になります。オブジェクト注入は次のように行います。
// シングルトンの場合
final settingsServiceRef = Ref.singleton(() => SettingsService());
// スコープド(コンテキスト依存)の場合
final userServiceRef = Ref.scoped((context) => UserService());
依存の取得方法は、作成方法によって異なります:
// シングルトンはコンテキストなしで取得可能
final settings = settingsServiceRef();
// スコープドな依存はコンテキストが必要
final userService = userServiceRef.of(context); // または userServiceRef(context)
lite_refは非同期処理にも対応しています。Ref.asyncSingleton
やRef.asyncScoped
を使用すると、Futureを返す依存オブジェクトも簡単に管理できます。これにより、APIクライアントやデータベース接続など、初期化に時間がかかるサービスも効率的に扱えます。
応用例:GoRouter を注入して遷移を行う
lite_refの強みは、Ref.scopedのコールバック関数がBuildContextを受け取れる点です。これを使えばナビゲーションやテーマなどのUIオブジェクトもDIで扱えます。
final routerRef = Ref.scoped(
(context) => GoRouter.of(context),
);
これを活用してViewModelにルーターを注入できます。
final myViewModelRef = Ref.scoped(
(context) {
final router = routerRef.of(context);
return MyViewModel(router: router);
},
);
ViewModelではコンストラクタで受け取ったGoRouterを使うだけで画面遷移が可能です。コンテキスト管理の負担を減らしたい小中規模アプリに最適です。
他のライブラリとの比較
Flutterの依存注入/状態管理では、Riverpod、Provider、GetItなどが有名です。
Riverpodは「状態管理+DI」を包括的に扱い、UI再ビルドや非同期状態を完全サポートします。Providerは標準InheritedWidgetのラッパーでUI再ビルド機能を提供。GetItはシングルトン管理に特化したサービスロケータです。
lite_refは「必要最小限のDI」に特化しています。UI状態管理は自動化しませんが、BuildContextに密着したスコープ管理を柔軟に行えます。Riverpodではコンテキストをビジネスロジックに持ち込まないよう推奨されますが、lite_refはあえてその設計を許容し、UIとの連携を簡単にします。
「Riverpodは複雑すぎる」「Providerでも再ビルド管理が面倒」と感じる場合、lite_refは良い選択肢です。後でRiverpodに乗り換えることも可能です。
ベストプラクティス & Tips
lite_refは使いやすい反面、使い方次第で依存関係が分かりづらくなるリスクがあります。
まず、スコープの粒度を意識しましょう。アプリ全体で共有するサービスにはRef.singleton
を使います。画面遷移ごとに再作成するViewModelにはRef.scoped
やRef.scopedFamily
を使うと、リソースが自動解放されます。
多すぎるシングルトンはかえって依存関係を不明瞭にします。何でもグローバル化せず、「どのスコープで必要か」を判断することが重要です。
ViewModelパターンの実装例
小中規模アプリでは、ChangeNotifierベースのViewModelとFlutter Hooksを組み合わせる方法が効率的です。この方法では、ビジネスロジックとUIの分離が適度に保たれます。
- まず、ViewModelをChangeNotifierで実装します:
class TodoViewModel extends ChangeNotifier {
final TodoRepository _repository;
List<Todo> _todos = [];
List<Todo> get todos => _todos;
bool _isLoading = false;
bool get isLoading => _isLoading;
TodoViewModel(this._repository) {
_loadTodos();
}
Future<void> _loadTodos() async {
_isLoading = true;
notifyListeners();
_todos = await _repository.fetchTodos();
_isLoading = false;
notifyListeners();
}
// その他のメソッド...
}
- lite_refでViewModelをscopedで提供します:
final todoRepositoryRef = Ref.singleton(() => TodoRepository());
final todoViewModelRef = Ref.scoped((context) {
final repository = todoRepositoryRef();
return TodoViewModel(repository);
});
- Widgetでは、Flutter Hooksを使って状態変化を監視します:
class TodoScreen extends HookWidget {
Widget build(BuildContext context) {
// ViewModelを取得し、状態変化を監視
final viewModel = useListenable(todoViewModelRef.of(context));
return Scaffold(
appBar: AppBar(title: Text('Todos')),
body: viewModel.isLoading
? Center(child: CircularProgressIndicator())
: ListView.builder(
itemCount: viewModel.todos.length,
itemBuilder: (context, index) => TodoItem(viewModel.todos[index]),
),
);
}
}
このパターンの利点は以下の通りです:
- ChangeNotifierは Flutter 標準機能のため、余分なライブラリが不要
- lite_ref の scoped 機能によりWidgetツリーに連動したライフサイクル管理が可能
- Flutter Hooks の useListenable() で簡潔に状態変化を監視できる
- 必要に応じて、他のリポジトリや依存も ViewModel に注入できる
テストではoverrideWith
が便利です。通信処理を行うサービスをモックに差し替えられます。テストを前提にしたDI設計を最初から行えば、後のリファクタリングが不要になります。
状態管理の再ビルドはFlutter標準機能(ChangeNotifier等)に依存します。必要に応じてValueListenableBuilderなどと組み合わせられます。lite_refは「依存オブジェクトのスコープ管理」に集中し、UIの再ビルド方法は自由に設計できます。
まとめ
小中規模Flutterアプリでは、複雑なフレームワークはかえって負担になります。lite_refはDIに特化したシンプルな設計で、必要十分な機能を提供します。
GoRouterなどのコンテキスト依存オブジェクトの注入方法やテストでのモック差し替えを紹介しました。UI再ビルド制御をほぼ行わない分、学習コストが低く、コードの見通しが良いことが特徴です。
将来的に複雑化したら他のフレームワークへの移行も検討できますが、小中規模アプリではlite_refでDIレイヤーを整備するだけで十分な品質を確保できます。
「使いやすさ」と「保守性」を両立させたいプロジェクトにlite_refはおすすめです。必要な依存を整理し、アプリをシンプルに保ちましょう。
Discussion