はじめに
何番煎じか分かりませんが、BlocパターンをFlutter Demoに適応してみました。
なので、あまり目新しい情報はありません。
blocパターンのサンプルコードでは、blocオブジェクトをコンストラクタ経由で渡すパターンが多いですが、ここではgetElementForInheritedWidgetOfExactTypeで先祖Widgetから取得する方式で作成しています。
この方式のBlocパターンを試してみたいのであれば、この記事が役に立つかもしれません。
勉強の動機
前回の記事でFlutterで作成したアプリを作成しましたが、このとき状態管理などのパターンを知らなかったため、ゴリ押しUIになりパフォーマンス的にも、メンテナンス面からも効率の悪い構成になっていました。
そのため、主流なパターンを勉強してみることにしました。
現状で主流となっているのは、自作のBlocパターン、flutter_blocパッケージ、riverpodパッケージを使ったパターンと認識しています。
今回は、自作のBlocパターンを勉強してStreamに関しても知識を深めようと考えました。
作成したコード
前述の通り、blocオブジェクトをコンストラクタで引き回すのはメンテナンス面から避けたいと考えたので、getElementForInheritedWidgetOfExactTypeで先祖Widgetから取得する方式にしました。
こちらの記事を参考にblocオブジェクトのHolder(Providerと言う方が適切?)などの必要なクラス群を作成しました。
参考にしたコードからの変更点として、ジェネリクスを使ってBlocクラスのインターフェイス(BlocBase)を継承したクラスを扱えるようにしました。
また、BlocBaseを使ってFlutter DemoのカウントアプリをBlocパターンで組んだものを以下に記載しています。
(長いので折りたたんでいます。)
bloc_base.dart
import 'package:flutter/cupertino.dart';
class BlocBase {
void dispose() {}
}
class BlocInherited<T extends BlocBase> extends InheritedWidget {
final T bloc;
const BlocInherited({Key? key, required this.bloc, required Widget child})
: super(key: key, child: child);
@override
bool updateShouldNotify(InheritedWidget oldWidget) {
return oldWidget != this;
}
}
class BlocHolder<T extends BlocBase> extends StatefulWidget {
final Widget child;
final T Function() blocBuilder;
const BlocHolder({
Key? key,
required this.child,
required this.blocBuilder,
}) : super(key: key);
@override
_BlocHolderState createState() => _BlocHolderState<T>();
static T? blocOf<T extends BlocBase>(BuildContext context) {
return (context.getElementForInheritedWidgetOfExactType<BlocInherited<T>>()?.widget
as BlocInherited<T>).bloc;
}
}
class _BlocHolderState<T extends BlocBase> extends State<BlocHolder<T>> {
late T _bloc;
_BlocHolderState();
@override
void initState() {
super.initState();
_bloc = widget.blocBuilder();
}
@override
Widget build(BuildContext context) {
return BlocInherited<T>(bloc: _bloc, child: widget.child);
}
@override
void dispose() {
_bloc.dispose();
super.dispose();
}
}
main.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'bloc_base.dart';
void main() {
runApp(const MyApp());
}
// Blocクラス
class BlocMain extends BlocBase {
final onCountChange = StreamController<int>();
int count = 0;
BlocMain() {
onCountChange.add(count);
}
void countUp() {
++count;
onCountChange.add(count);
}
@override
void dispose() {
onCountChange.close();
}
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Bloc base demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: const MyHomePage(title: 'Bloc base demo'),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
Widget build(BuildContext context) {
return BlocHolder(
// Blocオブジェクト生成処理登録
// (BlocHolderのinitStateで呼ばれる。build毎Blocオブジェトが生成されるわけではない。)
blocBuilder: () => BlocMain(),
// build関数で渡されたcontextはBlocHolderの親のものなので、
// このcontextからはBlocオブジェクトを取得することはできない。
// そのため、Builderを挟んでBlocHolderの子のcontextを生成する。
child: Builder(builder: (BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(title)),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('You have pushed the button this many times:'),
StreamBuilder(
// BlocオブジェクトのStreamを設定する。
stream: BlocHolder.blocOf<BlocMain>(context)!
.onCountChange
.stream,
builder: (BuildContext context,
AsyncSnapshot<int> snapShot) {
return Text(
'${snapShot.data}',
style: Theme.of(context).textTheme.headline4,
);
})
])),
floatingActionButton: FloatingActionButton(
// Blocオブジェクトのイベント関数を設定する。
onPressed: BlocHolder.blocOf<BlocMain>(context)!.countUp,
tooltip: 'Increment',
child: const Icon(Icons.add),
));
}));
}
}
試してみて
余分なbuildが走らない点に関して
以前作成したアプリではユーザの操作などイベントを上位のStatefulWidgetで受け取り、setStateを呼んで画面全体を更新していたので大量の余分なbuildが走っていたのだと思います。
このパターンでは必要なWidgetをピンポイントで更新できるので、処理負荷的にも精神衛生上もとても良いと思いました。
反面、StreamBuilderを挟まないといけないのは若干、面倒くさいなと感じました。
同時に、Streamで配信するデータの粒度を細かくしすぎると、このパターンで達成したいメンテナンス性を損なうと感じました。
業務レベルでの実装では、そのあたりの定石があるのか気になりました。
ステート管理オブジェクトの引き回しについて
今回はblocオブジェクトをコンストラクタでの引き回しではなく、getElementForInheritedWidgetOfExactTypeで先祖Widgetから取得する方式で作成しました。
この方式はオブジェクトをコンストラクタで引き回さないで済むのは楽なのですが、子孫のWidgetから制限なくアクセスできてしまうので、グローバル変数に似た危うさを感じました。
Blocクラスの作り方を工夫することで制限できるのか、flutter_blocパッケージ、riverpodパッケージではそのあたりの懸念が解消されているのか調査していきたいと思います。
以上