LoginSignup
7

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-08-05

やりたいこと

  • BLoC中でAPIを呼んで、返ってきた値をStreamに流す
  • そのStreamの値に応じてWidget側で何らかの処理を実行したい
    • UIの小さな変更であれば、StreamBuilderを使えば良いが、処理(ロジック)を実行したい場合は?

例:決済APIから完了通知が来たらモーダルを表示

Simulator Screen Shot - iPhone 8 - 2019-08-05 at 14.29.18.png

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

利用するパッケージ:
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)); // 通信時間をイメージして、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

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
7