3
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/DartのDIコンテナ(auto_injector)を使ってみる

Posted at

Dart/Flutter用のDIコンテナには色々ありますが、その中の1つ、auto_injectorを使ってみたいと思います。

auto_injectorの特徴

auto_injectorは使い方のシンプルさが特徴です。

実行時に依存性を解決する

実行時に依存性オブジェクトをコンテナへ登録していくため、build_runnerを使いません。開発規模がそこそこ大きくなってくるとbuild_runnerの実行時間が増加しがちなので助かります。
また、コンストラクタ経由で依存性を注入するため、サービスロケーターのようにならないのが良いです。

final injector = AutoInjector();
injector.addSingleton<IServiceA>(ServiceAImpl.new);
injector.addSingleton<IServiceB>(ServiceBImpl.new);
injector.commit();

AutoInjectorインスタンスしか使わない

目的のオブジェクトにアクセスするにはAutoInjectorインスタンスを参照できればよいので簡単です。

final useCase = autoInjector.get<UseCase>();

十分な機能がある

使い方はシンプルですが、DIコンテナとして十分な機能を持っています。

  • シングルトンクラス、生成済みインスタンス、ファクトリ経由のインスタンス取得
  • 遅延生成。シングルトンインスタンスが必要になったタイミングで生成できる
  • シングルトンインスタンスを破棄できる。また、disposeの際に処理を挟み込める
  • モジュール化できる
// module_sample
final subInjector1 = AutoInjector(
  tag: 'sub',
  on: (injector) {
    injector.add('ABC');
  },
);

final subInjector2 = AutoInjector(
  tag: 'sub',
  on: (injector) {
    injector.add(123);
  },
);

final rootInjector = AutoInjector(
  tag: 'root',
  on: (injector) {
    injector.addInjector(subInjector1);
    injector.addInjector(subInjector2);
  },
);

何か作ってみる

DIコンテナをテーマに見た目が面白そうなものを作るのは難しいです。
ですので、とりあえず FirebaseのCloud Firestoreをデータソースとして、値の読み書きを行ういかにもサンプルっぽいアプリを作りたいと思います。

やることは以下の通りです。

  • FirebaseのCloud Firestoreのカウンター値を読み出す・更新する
    • これをデータソースにする
    • Firebaseがテーマではないので、楽観ロックは実装しない
  • DataSource → Repository → UseCaseという依存関係を持つようにする

ソースコード

実行可能なソースコードです。

簡単な解説

INumberDataSource、INumberRepository、IStringRepository、UseCaseの実装をDIコンテナで管理します。
Repositoryの実装にはコンストラクタにprintを入れてあります。LazySingletonのインスタンス生成タイミングが分かるかと思います。

Firestoreのスキーマは下記のようにしています。
Firestoreのスキーマ

ソースコード

Firebase関連はコンソールで設定済みという前提になっています。また、状態の管理にはRiverpodを使います。

$ flutter pub add auto_injector flutter_riverpod firebase_core cloud_firestore
import 'package:auto_injector/auto_injector.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

/// DataSourceインタフェイス
abstract class INumberDataSource {
  /// 値を取得する
  Future<int> fetch();

  /// 値を保存する
  Future<void> store(int data);
}

/// DataSource実装
class NumberDataSource implements INumberDataSource {

  /// 値を読み出す
  @override
  Future<int> fetch() async {
    final query = await FirebaseFirestore.instance
        .collection('numbers')
        .limit(1)
        .get();

    final row = query.docs.firstOrNull;
    return row?['counter'] as int? ?? 0;
  }

  /// 値を保存する
  @override
  Future<void> store(int data) async {
    final query = await FirebaseFirestore.instance
        .collection('numbers')
        .limit(1)
        .get();

    final row = query.docs.firstOrNull;
    if (row == null) {
      return;
    }

    return FirebaseFirestore.instance.collection('numbers').doc(row.id).update({
      'counter': data,
    });
  }
}

/// 整数データレポジトリ
abstract class INumberRepository {
  Future<int?> findData();
  Future<void> saveData(int data);
}

class NumberRepository implements INumberRepository {
  NumberRepository(this.dataSource) {
    print('NumberRepository created');
  }

  final INumberDataSource dataSource;

  @override
  Future<int?> findData() {
    return dataSource.fetch();
  }

  @override
  Future<void> saveData(int data) {
    return dataSource.store(data);
  }
}

/// 文字列データレポジトリ
abstract class IStringRepository {
  Future<String?> findData();
}

class StringRepository implements IStringRepository {
  StringRepository(this._value) {
    print('StringRepository created');
  }

  final String _value;

  @override
  Future<String?> findData() async {
    return _value;
  }
}

/// ユースケース
class UseCase {
  const UseCase(this.numberRepository, this.stringRepository);

  final INumberRepository numberRepository;
  final IStringRepository stringRepository;

  /// データ取得
  Future<String?> getData() async {
    final data = await Future.wait([
      numberRepository.findData(),
      stringRepository.findData(),
    ]);

    final value1 = data[0] as int?;
    final value2 = data[1] as String?;

    return value1 == null || value2 == null ? null : '$value1 , $value2';
  }

  /// インクリメント
  Future<void> increment() async {
    final currentNumber = await numberRepository.findData() ?? 0;
    return numberRepository.saveData(currentNumber + 1);
  }
}

/// DIコンテナ
final autoInjector = AutoInjector();

/// ユースケースプロバイダ
final useCaseProvider = Provider.autoDispose<UseCase>((ref) {
  return autoInjector.get<UseCase>();
});

/// 値プロバイダー
final valueProvider = FutureProvider.autoDispose<String>((ref) async {
  final useCase = ref.watch(useCaseProvider);
  return await useCase.getData() ?? 'No Data';
});

/// エントリーポイント
void main() async {
  /// 依存関係の登録
  autoInjector.addSingleton<INumberDataSource>(NumberDataSource.new);
  autoInjector.addSingleton<INumberRepository>(NumberRepository.new);
  autoInjector.addLazySingleton<IStringRepository>(
    () => StringRepository('テスト'),
  );
  autoInjector.add<UseCase>(UseCase.new);
  autoInjector.commit();

  print('commit DI objects completed');

  WidgetsFlutterBinding.ensureInitialized();

  /// Firebase初期化
  await Firebase.initializeApp();
  FirebaseFirestore.instance.settings = const Settings(
    persistenceEnabled: true,
  );

  runApp(const MyApp());
}

/// アプリ本体
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(colorScheme: .fromSeed(seedColor: Colors.deepPurple)),
      home: ProviderScope(
        child: Scaffold(body: SafeArea(child: _Content())),
      ),
    );
  }
}

/// コンテンツ
class _Content extends ConsumerWidget {
  const _Content();

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final asyncValue = ref.watch(valueProvider);

    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Column(
        children: [
          // 取得した値を表示する
          Expanded(
            child: asyncValue.when(
              data: (data) => Center(child: Text('Data: $data')),
              loading: () => const Center(child: CircularProgressIndicator()),
              error: (e, st) => Center(child: Text('Error: $e')),
            ),
          ),
          // 値を更新する
          ElevatedButton(
            onPressed: () async {
              await ref.read(useCaseProvider).increment();
              ref.invalidate(valueProvider);
            },
            child: const Text('Update'),
          ),
        ],
      ),
    );
  }
}

最後に

株式会社ボトルキューブではFlutterを使ったお仕事を募集中です。
お問い合わせは下記リンク先のフォームからご連絡ください。

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