🏞️
【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