FlutterでBLoCの値の変化に応じて処理を行う（モーダル表示）

## やりたいこと
- BLoC中でAPIを呼んで、返ってきた値をStreamに流す
- そのStreamの値に応じてWidget側で何らかの処理を実行したい
  - **UIの小さな変更であれば、StreamBuilderを使えば良いが、処理（ロジック）を実行したい場合は？**


## 例：決済APIから完了通知が来たらモーダルを表示
![Simulator Screen Shot - iPhone 8 - 2019-08-05 at 14.29.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164020/2372a7fd-07ba-7a92-a680-a3a49cc3fffd.png)


1. Widget側でBLoCのStreamをlisten()し、値の変更に応じた処理を設定
2. ボタンを押して、BLoC経由で決済APIを実行
3. 決済APIから完了通知が返ってくる
4. 完了通知をBLoCのStreamに流す
5. listen()で設定しておいた処理が実行され、モーダルが表示される


利用するパッケージ: [
Providerパッケージ](https://pub.dev/packages/provider)

### BLoC

まずは決済機能を管理する `PaymentBloc` を作ります。

- `Sink<void> tryPayment` をWidgetに公開し、`tryPayment.add(null)` で値を流せる
- `tryPayment` 経由で流れてくるStream値をlisten()し、値が流れてきたら決済APIを叩く
- 決済APIの結果を `_paymentController` に流す
- Widget側では、決済APIの結果を `_paymentController` のStreamから受け取る

```payment_bloc.dart
import 'dart:async';

class PaymentBloc {
  PaymentBloc() {
    _paymentApiController.stream.listen((_) async {
      final result = await paymentApi.tryToPay();
      _changePaymentState.add(result);
    });
  }

  StreamController<void> _paymentApiController = StreamController<void>();
  Sink<void> get tryPayment => _paymentApiController.sink;

  StreamController<bool> _paymentController = StreamController<bool>();
  Sink<bool> get _changePaymentState => _paymentController.sink;
  Stream<bool> get paymentState => _paymentController.stream;

  final paymentApi = PaymentApiMock();

  void dispose() {
    _paymentApiController.close();
    _paymentController.close();
  }
}

class PaymentApiMock {
  Future<bool> tryToPay() async {
    await Future.delayed(Duration(seconds: 1)); // 通信時間をイメージして、１秒のdelayを設定
    return true;
  }
}
```


### Widget
決済処理をユーザが行うためのWidgetとして `PaymentPage` を作成します

- **blocでの決済状態の変更をlisten()で監視し**、trueが流れてきたら決済完了のモーダルを表示
- 「支払う」ボタンを押すとblocに値を送る


```payment_page.dart
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import 'payment_bloc.dart';

class PaymentPage extends StatefulWidget {
  @override
  _PaymentPageState createState() => _PaymentPageState();
}

class _PaymentPageState extends State<PaymentPage> {
  PaymentBloc bloc;
  StreamSubscription _paymentStateStreamSubscription;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    bloc = Provider.of<PaymentBloc>(context);
    _paymentStateStreamSubscription?.cancel();
    _paymentStateStreamSubscription = bloc.paymentState.listen((state) {
      if (state) {
        showDialog(
            context: context,
            builder: (_) => AlertDialog(
                  title: Text('支払いが完了しました'),
                  content: FlatButton(
                    onPressed: () {
                      Navigator.pop(context);
                    },
                    child: Text('確認', style: TextStyle(color: Colors.white)),
                    color: Colors.green,
                  ),
                ));
      }
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('お支払い'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            FlatButton(
              onPressed: () {
                bloc.tryPayment.add(null);
              },
              child: const Text(
                '支払う',
                style: TextStyle(color: Colors.white),
              ),
              color: Colors.red,
            ), // This trailing comma makes auto-formatting nicer for build methods.
          ],
        ),
      ),
    );
  }
}

```

一応 `PaymentPage` を作成しているmain.dartも載せておきます。pubspec.yamlを除くとこれでコードは全てになります。

```main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import 'payment_bloc.dart';
import 'payment_page.dart';

void main() => runApp(Provider<PaymentBloc>(
    builder: (_) => PaymentBloc(),
    dispose: (_, bloc) => bloc.dispose(),
    child: MyApp()));

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: PaymentPage(),
    );
  }
}
```


## まとめ
- blocのstreamのlisten()を使えば、値の変更に応じた処理をWidget側で設定することができる
- contextを使ってBLoCを取得する際は、didChangeDependencies()を使うと良い


## 参考
Flutter - BLoC pattern - How to use streams to invoke a method of another widget, i.e. an animation?
https://stackoverflow.com/questions/53443827/flutter-bloc-pattern-how-to-use-streams-to-invoke-a-method-of-another-widget?rq=1



## 過去記事

Stream/RxDart初心者のためのBLoC入門
https://qiita.com/tetsufe/items/521014ddc59f8d1df581

Stream/Sinkを使いこなす！ Stream/RxDart初心者のためのBLoC入門 part2
https://qiita.com/tetsufe/items/7b2f8592f5161104d1cd

RxDartのBehaviorSubjectとPublishSubjectの違いと使い分け
https://qiita.com/tetsufe/items/28ea0a07410efd6d6a9f