やりたいこと
- BLoC中でAPIを呼んで、返ってきた値をStreamに流す
- そのStreamの値に応じてWidget側で何らかの処理を実行したい
- UIの小さな変更であれば、StreamBuilderを使えば良いが、処理(ロジック)を実行したい場合は?
例:決済APIから完了通知が来たらモーダルを表示
- Widget側でBLoCのStreamをlisten()し、値の変更に応じた処理を設定
- ボタンを押して、BLoC経由で決済APIを実行
- 決済APIから完了通知が返ってくる
- 完了通知をBLoCのStreamに流す
- listen()で設定しておいた処理が実行され、モーダルが表示される
利用するパッケージ:
Providerパッケージ
BLoC
まずは決済機能を管理する PaymentBloc
を作ります。
-
Sink<void> tryPayment
をWidgetに公開し、tryPayment.add(null)
で値を流せる -
tryPayment
経由で流れてくるStream値をlisten()し、値が流れてきたら決済APIを叩く - 決済APIの結果を
_paymentController
に流す - Widget側では、決済APIの結果を
_paymentController
のStreamから受け取る
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に値を送る
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を除くとこれでコードは全てになります。
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