🏞️
【Flutter】色んな状態管理で作ってみよう ④RxDart
※こちらの記事は【Flutter】色んな状態管理手法でカウンターアプリを作ってみるの一部として作成された記事です
ReactiveプログラミングをDartに! RxDart
今回はリアクティブ・プログラミングを様々な言語で実現させているプロジェクトReactiveXのDart版RxDartでカウンターアプリを作っていきます
ReactiveXについての詳細には触れないので興味のある方はこちらの記事などを参考にしてみてください
使用するPackage:
rxdart v0.27.2
概要
-
rxdart自体は状態管理パッケージではなく、Streamの拡張版のようなパッケージ -
Streamの拡張版なので、streamを使った状態管理手法であるBLoCパターンに活用する事が出来る - 様々な言語でライブラリが提供されている
ReactiveXのdart版 - 初版リリースは2015年10月
全体像
- 状態変数の変更を通知する
Streamを定義し、それをStreamBuilderで受け取る -
RxDartはあくまでもStreamの拡張版であり、依存関係の注入をする為のクラスなどは存在しないので、ProviderやRiverpodなどを使って適宜注入する - 状態の変更を発火するメソッドは状態管理クラスに直接アクセスして実行する
- 構成としては
blocパッケージのcubitを使ったBLoCパターンの構成に近い
キーとなるクラスやメソッド
Streamクラス
- flutterで用意されている
Streamクラスと同じですが、RxDartでは様々なコンストラクタが用意されており、多種多様なstreamインスタンスを生成する事が出来ます。
Subjectクラス
-
RxDartでは複数のSubjectクラスというStreamControllerに様々な機能を拡張したクラスが用意されています。 - これらは全てブロードキャスト状態の
StreamController+αの様なイメージです - その中でも代表的なのが
PublishSubject、BeghaviorSubject、ReplaySubjectクラスです。
PublishSubjectクラス:
- ブロードキャスト状態のStreamControllerとほぼ同じです。違いは
streamとsink、両方を兼ね備えている事。PublishSubjectクラスは最もシンプルな形のSubjectクラスです。
final streamSubject = PublishSubject<String>();
streamSubject.listen((addData){
print(addData);
});
streamSubject.sink.add('1st State');
BehaviorSubjectクラス:
-
PublishSubjectの機能に加え、「初期値の設定」と「最後にsink.addしたオブジェクトをキャッシュする」機能が付いたSubjectクラスです。 -
BehaviorSubject<T>.seededコンストラクタを使う事でstreamの初期値を設定できます。 - これにより下記の①で定義したメソッドは初期値の
"1st state"をまず実行します。
final streamSubject = BehaviorSubject<String>.seeded("1st state");
void main()async{
streamSubject.listen((addData){
print(addData); // ①
})
streamSubject.sink.add("2nd state");
streamSubject.sink.add("3rd state");
streamSubject.listen((addData){
print(addData); // ②
})
}
- また
BehaviorSubjectでは最後にsink.addされたデータをキャッシュしている為、②で定義したメソッドで最後にsink.addされた"3rd state"が実行されます。 - 上記プログラムを実行すると下記の様に結果が表示されます。
# 実行結果
$ 1st state # ①の実行結果。初期値がまず実行されます。
$ 2nd state # ①の実行結果
$ 3rd state # ①の実行結果
$ 3rd state # ②の実行結果
ReplaySubject クラス:
-
BehaviorSubjectが最後にsink.addされたデータをキャッシュしているのに対して、ReplaySubjectは「sink.addした全てのデータのキャッシュを保持」します。 - その為、どのタイミングで
Subjectに対してlistenし始めても、sink.addされた全てのデータを受け取る事になります。
final streamSubject = ReplaySubject<String>();
void main()async{
streamSubject.listen((addData){
print(addData); // ①
})
streamSubject.sink.add("1st state");
streamSubject.sink.add("2nd state");
streamSubject.sink.add("3rd state");
streamSubject.listen((addData){
print(addData); // ②
})
}
- 上記では①でも②でも
sink.addした3つのイベントを全て取得する事になります
# 実行結果
$ 1st state # ①
$ 1st state # ②
$ 2nd state # ①
$ 2nd state # ②
$ 3rd state # ①
$ 3rd state # ②
- また
maxSizeフィールドで最後からいくつまでのイベントを受け取るか制限する事も出来ます。
final streamSubject = ReplaySubject<String>(maxSize:2);
void main()async{
streamSubject.sink.add("1st state");
streamSubject.sink.add("2nd state");
streamSubject.sink.add("3rd state");
streamSubject.listen((addData){
print(addData);
})
}
- 上記では
maxSize:2としているので最後から2個まで遡ったイベントを受け取る事が出来ます。
# 実行結果
$ 2nd state
$ 3rd state
準備
具体的にカウンターアプリを例に使い方を見ていきましょう。
サンプルコードはこちら
1. Stateクラス
- 今回は
countフィールドを持つCounterStateクラスを定義します。
class CounterState {
CounterState(this.count);
int count;
}
2. 状態管理(BLoC)クラス
-
RxDartを用いてBLoCパターンを実現するので、状態管理クラスはRxCounterBlocクラスと命名しました。 -
stateとcounterSubject(stream)のフィールドを持たせます。 - 今回は
BehaviorSubjectを使用し、初期値が設定出来る様にしています。 - コンストラクタの引数に渡したオブジェクトを
counterSubjectのseededに渡し、初期値を設定しています。
class RxCounterBloc {
CounterState state;
BehaviorSubject<CounterState> _counterSubject;
RxCounterBloc({this.state}) {
_counterSubject = BehaviorSubject<CounterState>.seeded(this.state);
}
Stream get counterStream => _counterSubject.stream;
}
- コンストラクタ以下には状態変更を発火するメソッド
increment,decrement,clearを用意しました。 - これらのメソッドでは新しい状態値を
sink.addする事でUI側に状態の変更を通知します。 - またStreamが用済みになった際に破棄する為の
disposeメソッドも定義しておきます。
void increment() {
state = CounterState(state.count + 1);
_counterSubject.sink.add(state);
}
void decrement() {
state = CounterState(state.count - 1);
_counterSubject.sink.add(state);
}
void clear() {
state = CounterState(0);
_counterSubject.sink.add(state);
}
void dispose() {
_counterSubject.close();
}
UIへ注入
- 前述の通り、
rxdartパッケージには依存性注入のクラスは用意されていないので、好みの方法でUIへ注入します。 - 本サンプルでは
providerパッケージのProviderクラスを使って注入していきます。
Widget build(BuildContext context) {
return Provider<RxCounterBloc>(
create: (_) => RxCounterBloc(state: CounterState(0)),
child: _RxdartCounterPage(),
);
}
状態へのアクセス
- 本サンプルでは
providerで依存関係を注入しているので、状態管理クラスへのアクセスはProviderを通してアクセスします。
Widget build(BuildContext context) {
final RxCounterBloc counter = Provider.of<RxCounterBloc>(context);
状態値の監視
- 状態値の監視は
StreamBulderを通して状態管理クラスのBehaviorSubjectをlistenする事で行います。 - これにより
BehaviorSubjectに新しいオブジェクトがsink.addされる度にbuilder内のwidgetが際描画されます。
StreamBuilder<CounterState>(
initialData: CounterState(0),
stream: counter.counterStream,
builder: (BuildContext context,
AsyncSnapshot<CounterState> snapshot) =>
Text(
snapshot.data.count.toString(),
style: Theme.of(context).textTheme.headline4,
),
),
メソッドへのアクセス
- シンプルに
providerを通してアクセスします
FloatingActionButton(
onPressed: () => counter.increment(),
tooltip: 'Increment',
heroTag: 'Increment',
child: Icon(Icons.add),
),
全体
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:rxdart/rxdart.dart';
import 'package:state_management_examples/widgets/main_appbar.dart';
// 状態クラス
class CounterState {
CounterState(this.count);
int count;
}
// 状態管理クラス
class RxCounterBloc {
CounterState state;
BehaviorSubject<CounterState> _counterSubject;
RxCounterBloc({this.state}) {
_counterSubject = BehaviorSubject<CounterState>.seeded(this.state);
}
Stream get counterStream => _counterSubject.stream;
void increment() {
state = CounterState(state.count + 1);
_counterSubject.sink.add(state);
}
void decrement() {
state = CounterState(state.count - 1);
_counterSubject.sink.add(state);
}
void clear() {
state = CounterState(0);
_counterSubject.sink.add(state);
}
void dispose() {
_counterSubject.close();
}
}
// 依存関係を注入
class RxdartCounterPage extends StatelessWidget {
const RxdartCounterPage({Key key}) : super(key: key);
Widget build(BuildContext context) {
return Provider<RxCounterBloc>(
create: (_) => RxCounterBloc(state: CounterState(0)),
child: _RxdartCounterPage(),
);
}
}
// カウンター本体
class _RxdartCounterPage extends StatelessWidget {
const _RxdartCounterPage({Key key}) : super(key: key);
Widget build(BuildContext context) {
final RxCounterBloc counter = Provider.of<RxCounterBloc>(context);
print('rebuild!');
return Scaffold(
appBar: MainAppBar(
title: 'RxDart x Provider',
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
StreamBuilder<CounterState>(
initialData: CounterState(0),
stream: counter.counterStream,
builder: (BuildContext context,
AsyncSnapshot<CounterState> snapshot) =>
Text(
snapshot.data.count.toString(),
style: Theme.of(context).textTheme.headline4,
),
),
],
),
),
floatingActionButton: FittedBox(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
FloatingActionButton(
onPressed: () => counter.increment(),
tooltip: 'Increment',
heroTag: 'Increment',
child: Icon(Icons.add),
),
const SizedBox(width: 16),
FloatingActionButton(
onPressed: () => counter.decrement(),
tooltip: 'Decrement',
heroTag: 'Decrement',
child: Icon(Icons.remove),
),
const SizedBox(width: 16),
FloatingActionButton.extended(
onPressed: () => counter.clear(),
tooltip: 'Clear',
heroTag: 'Clear',
label: Text('CLEAR'),
),
],
),
),
);
}
}
以上でした
いかがだったでしょうか?RxDartはStreamを強化するだけでも便利なので、状態管理と関係ない場所でも活躍してくれ、個人的には好きなパッケージです。ただこれでRxdartでBLoCパターンを実装するならblocパッケージでいいのかなぁと思ったりもします。
Discussion