FlutterでDIならget_itもいいじゃないか

この記事は@sensuikan1973さんの[【BLoC/RxDart入門】 Flutterの公式チュートリアルを書き換える](https://qiita.com/sensuikan1973/items/64f1a6235bd8ecaf9067)をもとにしています。

今回上記のサンプルを写経しつつ、自分なりに更新してFlutterにおけるDIを考えてみました。

# きっかけ: Widgetツリーを考えずにインスタンス取得したい
以前からFlutter開発でアプリ内で任意の場所でインスタンスを使いまわす設計、つまりDIに悩んでおり、調べてみると`provider`パッケージや`InheritedWidget`での解決方法が主流のようでした。いくつかのQiitaやブログをみながら色々試したのですが、どうにも違和感がありました。

というのも、自分の場合は「データストアのインスタンスにアクセスしたいだけ」という状況だったので、「UI関係ないインスタンスにアクセスしたいだけのときに、`BuildContext`使って取得したり、`Provider`とか`Consumer `とかで囲わないと伝播できないのなんとかならんかなぁ」と感じていました。

Singletonで同じインスタンス取得できるようにするのもやれるけど他にもないかと探していたら`get_it`パッケージというのを見つけたので、今回、`InheritedWidget`を使っている前述の記事を写経しつつ、そこから`get_it`を使った方法にアップデートしてみて、理解を深めようと考えました。

＊正確には`get_it`はDIを実現しているのではなく、Service Locatorパターンを実装したライブラリです。が、そこは今回のきっかけに対する解決策としては十分使えると判断しました。Service LocatorとDIの違いはMartin FowlerのInversion of Control Containers and the Dependency Injection patternを読むとわかりやすいです。
https://www.martinfowler.com/articles/injection.html (原文)
https://kakutani.com/trans/fowler/injection.html (日本語訳)


# ソース
https://github.com/gki/bloc_getit_sample

# アプリのUI
繰り返しになりますが、このアプリは@sensuikan1973さんの記事のアプリを写経した後に、変更を加えたものです。

## Startup Name Generatorページ
- 初期画面。ランダムに表示されている単語を「お気に入り」にすることができる。
- お気に入りの総数はナビゲーションバーに表示され、お気に入りの追加・削除で更新される。
- ナビゲーションバーのボタンから次の画面、Your Favoriteページへ移動できる。

<img width=30% src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60457/715c73c2-735a-499f-7a68-b42c85cd6b42.png"/>

## Your Favoriteページ
- 自分の「お気に入り」をリスト表示してレビューすることができる。
- デフォルトでは、リストの先頭にダミーのサジェスト項目が表示される。
- また、ダミーの広告表示もリストのいち項目として表示される。
- ユーザーはナビゲーションバーのスイッチで、ダミーのサジェスト項目と広告表示を無効にすることができる。
- リストの言葉を気に入らなくなった場合は、各お気に入り項目の削除アイコンボタンからお気に入りを削除することができる。

<img width=30% src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60457/b48b96fe-4a7c-4e36-fce6-fd49f866f858.png >

# ざっくりやったこと
## `main.dart`で初期化
```dart

void main() {
  _setup(); // 初期化呼び出し。
  runApp(new MyApp());
}

// 中略

_setup() {
  GetIt.I.registerSingleton<WordBloc>(WordBloc()); //これ
}
```

## `Provider.of<WordBloc>(context)`を`GetIt.I<WordBloc>()`に変更
変更にあわせて`build()`で取得した`WordBloc`をメソッド引数で持ち回っていた部分は引数削除。メソッド内でインスタンスを取得。

```dart
Widget _buildRow(WordPair pair) { // 引数にあったWordBlocを消して
  return new StreamBuilder<List<WordItem>>(
      stream: GetIt.I<WordBloc>().items,  //ここで直接取得
      builder: (_, snapshot) {
        if (snapshot.data == null || snapshot.data.isEmpty) {
```

# get_it良いじゃないか
所感としてはかなり自分の満足する形でWidgetツリーのことを全く考えないでインスタンスの取得ができました。特に、`Context`を必要とせずにインスタンスを生成できる＋`GetIt.I<Foo>()`がstaticに使えることで`build()`を待たずしてインスタンスを取得し、そのまま`initState()`で諸々の準備にBlocを使えるようになりました。

```dart
class _BlocFavoritePageState extends State<BlocFavoritePage> {
  final WordBloc wordBloc = GetIt.I<WordBloc>();   // これ！

  List<Item> wordItemList;
  bool withInfo = true;
  StreamSubscription<List<Item>> _subscription;

  @override
  void initState() {
    super.initState();
    _setupSubscription();
  }

  _setupSubscription() {
    if (_subscription != null) {
      _subscription.cancel();
    }
    if (withInfo) {
      //initState()から呼ばれてもwordBlocを使える
      _subscription = wordBloc.itemsWithInfo.listen((items) {
        setState(() {
          wordItemList = items;
        });
      });
    } else {
      _subscription = wordBloc.items.listen((items) {
        setState(() {
          wordItemList = items;
        });
      });
    }
  }

  @override
  void dispose() {
    _subscription.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(...
// 以下略
```
上記の様に、インスタンスフィールドに直接セットできる他にも、コンストラクタ引数にしておいて、`MaterialApp`の`routes`の中でコンストラクタに渡すといった設計も可能になります。コンストラクタインジェクションと言われるパターンですね。こちらはテスタビリティを上げる戦略として使えるやり方です。

## get_itのデメリットもある
`get_it`はwidgetツリーとは関係ない場所でインスタンスの取得を行えるため、言い換えればWidgetのrebuildについて何も考慮してくれません。`provider`の`listen`や`InheritedWidget`の`updateShouldNotify()`はインスタンスの取得と配下のWidgetツリーの更新をまとめて制御してくれる点が1つのメリットという理解ですが、`get_it`はそんなことはしてくれません。

なので、`get_it`を使ってWidgetを更新するためには、Widgetを`StatefullWidget`にして`setState()`でデータを更新していったり、`StreamBuilder`を間に挟むなど、どうやってrebuildをさせるか自分で考える必要があります。

一方で、`provider`や`InheritedWidget`のその辺りの挙動をもう理解している人は`get_it`の旨味はあまりないかもしれません。

# まとめ
上記の通りデメリットもありますが、私の場合は「やってることがシンプル」「デメリットのrebuildの制御は見える様になるのが嬉しい」ので、`provider`、`InheritedWidget`、`get_it`どれでも実現できるロジックなら`get_it`をメインで使おうと思います。

なお、念のためですが、`provider`と`get_it`は目的が違うライブラリなので優劣はないと考えています。（もし目的が完全一致するものであれば比較できますが。）
ツールは優劣ではなく、どの様な設計をしたいかに合わせて適切な道具を選べる、選択権を持っていることが重要です。そういう意味ではFlutterでDIを考えたときにこのような選択肢があることは素晴らしいことですし、自分も`get_it`を使いつつ、`get_it`偏重にならないように`provider`も使える様になっていかないとなぁと思う次第です。


### おまけ
DaggerにインスパイアされたInject.dartっていうのも見つけましたが今回は未チェックです。
https://github.com/google/inject.dart

2019年前半から1年くらい更新ない。。。どうなるんだろう？