BLoCパターン(ProviderとStreamController/StreamBuilder)を用いる方法
続いて、BLoCパターンに基づいた実装方法の一例を紹介します。BLoCとは、Business Logic Componentの略になります。BLoCとはここでは深く説明はしませんが、FlutterにおいてはStreamを用いてビジネスロジックと画面間のデータ連携を行う手法になります。
全体の構造
BLoCパターンを用いた場合の構造について示したのが図4です。
前回の変更(イベント)通知と大きく違うポイントは、画面処理とロジック以外にデータ処理のレイヤーが追加されたことです。この処理が追加されたことでデータのみを定義する構造クラス(DateModel)が必要になります。そして、そのデータがStreamを通じてビジネスロジックから画面へ提供される仕組みになります。
今回のような単純なサンプルでは変更(イベント)通知との違いがよくわかりにくいですが、データ処理レイヤーを別途もうけることで以下のようなメリットも生じます。
- 1つの処理から異なるデータ形式に分割して変更通知を伝えることができる。
- 複数の処理からデータを統合をしてから、画面ウィジェットに変更通知を伝えることができる。
- 変更のタイミングと画面表示のタイミングを実装者が意識する必要がない。
(1)(2)は複数のStreamを利用することでデータ処理レイヤーだけで対応できます。例えば、処理が共通でも必要なデータ形式が違う画面がある場合などに有用です。
また、(3)は少々わかりにくいですが、先ほどのイベント処理でnotifyListeners()を現在の画面Frame処理の後に予約するような問題回避をしましたが、そのようなことを考える必要性がなくなります。なぜなら、処理タイミングと画面更新のタイミングが同期していなくてもStreamによって矛盾がないデータが保証されているためです。
コード例
先ほどの構造を実装したコード例がリスト4になります。コード自体はイベント通知と比べて大きな違いがないと思います。
// (1) データ定義
class DateModel {
final List<DateItem> items;
final bool loading;
DateModel(this.items, this.loading);
}
// (2) ロジック定義
class WeekBloc{
// (3) Streamの準備
final _controller = StreamController<DateModel>();
Stream<DateModel> get stream => _controller.stream;
List<DateItem> _last = [];
void load() async{
// (4) データの追加(ローディング中)
_controller.sink.add(DateModel(_last, true));
var res = await http.get(Uri.parse("http://192.168.1.1/list.json"),headers: {
'Accept' : 'application/json'
});
var json = jsonDecode(utf8.decode(res.bodyBytes));
var week = json['items'].map<DateItem>((e) => DateItem.fromJson(e)).toList();
_last = week;
// (5) データの追加(データ取得)
_controller.sink.add(DateModel(week, false));
}
void dispose(){
_controller.close();
}
}
class StreamScreen extends StatelessWidget {
const StreamScreen({super.key});
@override
Widget build(BuildContext context) {
// (6) Streamを提供する親ウィジェット
return Provider<WeekBloc>(
create: (context) => WeekBloc(),
dispose: (_,bloc) => bloc.dispose(),
child: const DateListScreen(),
);
}
}
class _DateListScreen extends State<DateListScreen>{
@override
void initState() {
super.initState();
final bloc = Provider.of<WeekBloc>(context, listen: false);
bloc.load();
}
@override
Widget build(BuildContext context) {
final bloc = Provider.of<WeekBloc>(context);
// (7) 画面処理
return StreamBuilder<DateModel>(
stream: bloc.stream,
initialData: null,
builder: (context, snapshot) {
final DateModel? block = snapshot.data;
return Scaffold(
appBar: AppBar(
//: (省略)
),
body: Stack(
children: [
// : (省略)
],
)
);
},
);
}
// : (省略)
}
(1)はStream上に流れるデータの定義です。そして、ビジネスロジックを定義したのが(2)で、Streamをあつかうため(3)StreamControllerを用意します。load()メソッドが実際のデータを取得する処理で有ることが先ほどと変わりませんが、ローディング中を示すデータを(4)のように追加します。ここで_lastという変数を用意している理由は、リロード処理において前回データが必要なためです。
また、データ取得後でも再度(5)のようにStreamにデータを追加します。(6)のProviderにてビジネスロジックのインスタンスを管理します。そして、(7)のStreamBuilderがデータを受け取る画面ウィジェットになります。
最後に
アーキテクチャを採用する際に1つの方法に統一しようと思う方もいるかも知れません。しかし、今回紹介した3つのパターンでもわかるように、実際には違う原理で実現していることが分かると思います。
実際に開発するアプリでは1つの方法だけでは問題を無理に回避するための分かりにくいコードができたり、必要以上に複雑さを生じさせているケースをよく見ます。そのため、無理に統一する必要はなく、いくつからパターンから採用できる方がよいと筆者は考えます。
また、今回紹介できませんでしたが、Reactなどで普及しているReduxパターンというのもあるので、興味がある方はいろいろと調べてみるとよいと思います。
次回はFlutter特有のことについていくつか紹介します。例えば、今回のコードでもBuildContextを通じてインスタンスの連携などを行いましたが、その辺りにもふれていきたいと思います。
