Flutter BLoC+Providerとマルチプロバイダーのサンプル(戻るボタンのフックで苦労した話)

皆さん、StayHomeしていますか？
例年のGWなら、実家に行ったり飲みに行ったり旅行に出掛けたりして案外まとまった時間でコーディングできなかったりしますが、今年はどっぷりコーディングに憑かれそうですね(変換ミスだけど面白いのでそのままにしようw)

# 環境など
下記環境でFlutterアプリを開発中です。

|ツールなど|バージョンなど|
|:--|:--|
| MacBook Air Early2015|macOS Mojave 10.14.5|
|Android Studio |3.6.1|
|Java |1.8.0_131|
|Flutter|1.12.13+hotfix.5|
|Dart |2.7.0|


上記のサイトなどを参考に、`ChangeNotifierProvider`+`Selector/Consumer`でAndroidの`LiveData`っぽく使っていましたが、どうにもそれだけでは解決できないことがあって、その解決策として普通の`Provider`を使う方法を試したので覚え書きです。

# 簡単な用語解説

概要というか私の乱暴な理解を書いておきます。厳密には違うことはあるでしょうが、この理解で一応実装上は困っていません。

## 1.BLoC

「Business Logic Component」の略。GoogleがFlutterアプリの設計アーキテクチャとして推奨していた。
開発するときは、以下の点を気をつける感じ。

- デバイス依存をBLoCクラスに入れない
- Stream/Sinkで描画データの更新/監視をする
  - だからウィジェットは`StreamBuilder`を使う必要がある 

私の場合は、まだ本格的な業務では携わったことが無いのもありますが、上記以外のことはあまり気にしないで使っています。


## 2.Provider

`ChangeNotifierProvider`使っている人なら今更という所でしょうが、一応。

- [providerパッケージ](https://pub.dev/packages/provider)を使う
- 基本的には、DI的な発想で、下位層にあるウィジェットに値を伝搬するコードの削減をするためのもの
- 色々種類がある
- ノーマルな`Provier`はインスタンスの生成と破棄を自動的にやってもらえて(※それぞれのメソッドを指定する)、下位Widgetで値を利用することが出来る。`Selector/Consumer`である必要が無い
  - ただし、画面のルートウィジェット直下ではビルドメソッドでProviderが解決できないため、1つ子ウィジェットを挟む必要がある。詳細は[Providerを使用できないタイミング](#providerを使用できないタイミング)を参照
- `StatefulWidget`を使うのをやめられる
  - 無駄なウィジェットリビルドを避けられる(=アプリ高速化,省メモリ化)

ノーマルな`Provider`の使い方は、`ChangeNotifierProvider`と全く同じです。

ウィジェットを`Provider<MyBloc>`でくくり、下位のウィジェットからは、`final bloc = Provider.of<Blocクラス>(context, listen: false);`でアクセスし、値の受け渡しなどが可能になります。

```dart
class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Provider<MyBloc>(
        create: (context) => MyBloc(),
        dispose: (context, bloc) => bloc.dispose(),
        child: MyHomePage(title: 'Flutter Demo Home Page'),
      ),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        // Center is a layout widget. It takes a single child and positions it
        // in the middle of the parent.
        child: Center(
          child: Text("a"),
        ),
      ),
    );
  }
}

// MyHomePage内で使っているウィジェット
class SomeChildWidget extends StatelessWidget {
    final bloc = Provider.of<MyBloc>(context, listen: false);
}
```

`MyBloc`は、何かのクラスの派生である必要はありません。ここは`ChangeNotifierProvider`との違いですね。

```dart
class MyBloc {
 ...
}
```

# やりたかったこと

戻るボタンの処理で、**「編集していたら」値を返す**というのを実現するため、Providerにアクセスしたい。

## 1. 戻るボタンのフック

まずは、戻るボタンのフック方法です。
`WillPopScope`というのを使います。

```dart
class SampleScaffold extends StatelessWidget {
  SampleScaffold({Key key, @required this.title}) : super(key: key);
  final String title;

  /// ページ全体
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: WillPopScope(
        onWillPop: () {
          return Future.value(true); // trueで戻る。falseで戻らない
        },
        child: ...,
    );
  }
}
```

これで、AppBarの戻るボタン、端末の戻るボタン両方拾えます。
以下でも大丈夫です。

```dart
  @override
  Widget build(BuildContext context) {
   return WillPopScope(
      onWillPop: () {
        return Future.value(true);
      },
      child: Scaffold(
        appBar: AppBar(
          title: Text(title),
        ),
        body: ...
    ),
  );
```

## 2. ChangeNotifierProvider & Selector? Consumer?

それまでは、`ChangeNotifierProvider`のみでこうしようとしていました。

```dart
class MyPage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<MyViewModel>(
      create: (context) => MyViewModel(),
      child: SubPage(),
    );
  }
}

class SubPage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Selector<MyViewModel, bool>(
      selector: (context, model) => model.isModified,
      builder: (context, date, child) =>
          WillPopScope(
            onWillPop: () {
              _onBackPressed(context);
              return Future.value(false);
            },
            child: Scaffold(
              appBar: AppBar(
                title: Text(title),
              ),
              body: ,
            ),
          ),
    );
  }

  /// 画面戻り時の処理
  void _onBackPressed(BuildContext context) {
    final viewModel = Provider.of<MyViewModel>(context, listen: false);
    Navigator.pop(context, viewModel.isModified);
  }
}

class MyViewModel with ChangeNotifier {
  Data data;
  bool _isModified = false;

  bool get isModified => _isModified;
  
  void onDataUpdate(Data data){
    // dataの更新処理
    notifyListeners();
    _isModified = true;
  }
}
```

これだと、`Scaffold`以下が、`isModified`の値が変わるたびにリビルドされてしまいます。
`Consumer`だろうと`Selector`だろうと同じです。
よくありません。
全体のリビルドを避けたいから使っているのに・・・本末転倒です。

## 3. BLoC用のもう1つ別のProviderを作った

書いていて正直これはBLoCと言えないような気もしてきましたが、実装していたときは`BLoC + Provider`にすればいいんじゃね？(ﾜｸﾃｶ)と思ってやっていたのでそのままにします。

`_isModified`に関わる部分を移植しました。

```dart
class MyBloc{
  bool _isModified = false;

  bool get isModified => _isModified;
  
  void onUpdate(){
    _isModified = true;
  }
}
```

さて、Providerが二つになりました。`ChangeNotifierProvider`はそのまま残すからです。
なんでかって、StreamBuilderに書き変えるのが面倒だからです。
ChangeNotifierを使わず、BLoCパターンでウィジェットにデータ変更を流すためには、`StreamBuilder`に変更が必要なんですが、今更変更箇所が多くて嫌です(ｷﾘｯ)

なので、この二つのプロバイダーを同時に使いたい！
そうだ、**マルチプロバイダー**の出番だ！！

## 4. マルチプロバイダーにする

マルチプロバイダーは次のように設定します。

```dart
class MyPage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        Provider<MyBloc>(
            create: (context) => MyBloc(),
            dispose: (context, bloc) => bloc.dispose()),
        ChangeNotifierProvider<MyViewModel>(
            create: (context) => MyViewModel(),
      ],
      child: SubPage(),
    );
  }
}
```

これで、`Consumer`や`Selector`を使用する必要がなくなります。

```dart
class SubPage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: () {
        _onBackPressed(context);
        return Future.value(false); // 上の関数で手動でpopしているのでfalseを返す
      },
      child: Scaffold(
      ...
     ),
  }
}
```

`_onBackPressed`では、`Provider<MyBloc>`にアクセス出来ます。

```dart
void _onBackPressed(context){
    final bloc = Provider.of<MyBloc>(context, listen: false);
    Navigator.pop(context, bloc.isModified);
}
```

やった！
これで、遷移元にデータを変更したかどうかの結果を返すことが出来るようになりました。

ここで時間と**やる気**があれば、`ChangeNotifier`な`MyViewModel`の機能を全部`MyBloc`に移して、`Selector`や`Consumer`で監視していたところを`StreamBuilder`使うようにすれば、立派なBLoCパターンの出来上がるところですが、前述の通り、このままにします。

多分、設計としてはダメダメなんでしょうが、~~面倒だし~~マルチプロバイダーの例にもなるし、いいよね？

# ウィジェットクラスのメンバー変数じゃダメなの？

と思ったあなた。
ダメなんです。
`StatelessWidget`を使いたいから。

```dart
class SubPage extends StatelessWidget {
  bool isModified = false
}
```

とすると、`ステートレスなウィジェットが変更可能な値を持つのはよろしくないよ`と警告してくれます。
ビルドは一応通ったりもしますが、ダメと言われていることは回避するのが大人ってもんです。
だからといって、`StatefulWidget`にするのは、やはりリビルド処理を減らしたいのに本末転倒です。
だから、Providerパターンが必要なんです。

# Providerを使用できないタイミング

例)NGパターン

- Providerを宣言した`StatelessWidget/StatefulWidget`の`build`メソッド内でProviderにアクセスしようとするのはNG

```dart
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Provider<MyBloc>(
        create: (context) => MyBloc(),
        dispose: (context, bloc) => bloc.dispose(),
        child: _buildPage(context),
      ),
    );
  }

  Widget _buildPage(BuildContext context){
    // このタイミングではまだProviderが作成されておらず、blocはnullになる
    final bloc = Provider.of<PermissionBloc>(context, listen: false);

    return Scaffold(
      appBar: AppBar(
        title: Text("title"),
      ),
      body: Center(
        child: Text("Hello!"),
      ),
    );
  }
}
```

例)OKパターン

- Providerを宣言した`StatelessWidget/StatefulWidget`から、一つ下位以降の`StatelessWidget/StatefulWidget`の`build`メソッドでは、Providerにアクセス可能。

```dart
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Provider<MyBloc>(
        create: (context) => MyBloc(),
        dispose: (context, bloc) => bloc.dispose(),
        child: MyHomePage(), // 下位のStatelessWidgetを挟む
      ),
    );
  }
}

class MyHomePage extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    // このタイミングではProviderが作成済みでblocを利用できる
    final bloc = Provider.of<MyBloc>(context, listen: false);
  }
}
```

理由の想像ですが、多分、Providerは`InheritedWidget`を使いやすくラップしたものなので、一度親(Providerを宣言したウィジェット)のbuildを抜け(下位層のビルドが呼ばれ)ないと、Providerがもつ`InheritedWidget`が作られないんでしょうね。(NGパターンの時は、そのウィジェットが持つ`InheritedWidget`がnullになっており、Providerが見つけられないというエラーが起こります)


# 参考サイト

長めだけどたぶんわかりやすいBLoCパターンの解説
https://qiita.com/kabochapo/items/8738223894fb74f952d3

Flutterでバックキーイベントを検知するには
https://kwmt27.net/2018/07/18/flutter-onbackpressed/