小菜前兩天剛學習了基本的 Bloc 狀态管理,其中 UI 通過 setState() 方式更新資料,今天進一步了解進階版的 FlutterBloc 狀态管理;
FlutterBloc
FlutterBloc 可以更便利的實作 Bloc,主要是為了與 Bloc 共同使用而建構的;同樣需要提前了解幾個概念;小菜繼續以上一節中的 Demo 進行擴充,添加了 Number 的遞增和遞減;
BlocBuilder
BlocBuilder 小菜了解為 Bloc 構造器,主要用于建構 Widget 以響應新的狀态,相較于 StreamBuilder 更便捷;可替代小菜上一節使用的 **setState()**;
const BlocBuilder({
Key key,
@required this.builder,
B bloc,
BlocBuilderCondition<S> condition,
})
分析源碼可知,builder 用于相應狀态的 Widget,bloc 為目前提供的範圍僅限于單個 Widget 且無法通過父級 BlocProvider 和目前級通路的 Bloc 時才使用;而 condition 為可選的過度細粒度,包括兩個參數,之前的狀态和目前的狀态,傳回值為 Boolean 類型,true 為更新狀态重建 Widget,false 時不重新建構;
@override
Widget build(BuildContext context) {
return BlocBuilder<NumberBloc, int>(
bloc: _numBloc,
condition: (previousState, state) {
print('BlocPage.condition->$previousState==$state');
return state <= 30 ? true : false;
},
builder: (context, count) {
return Scaffold(
appBar: AppBar(title: Text('Bloc Page')),
body: Center(
child: Column( mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('當 Number > 30 時,不進行變更', style: TextStyle(fontSize: 20.0, color: Colors.blue)),
SizedBox(height: 20.0),
Text('目前 Number = ${_numBloc.state}', style: TextStyle(fontSize: 20.0, color: Colors.blue))
])),
floatingActionButton: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
FloatingActionButton(
heroTag: 'addTag', child: Icon(Icons.add),
onPressed: () => _numBloc.add(NumberEvent.addEvent)),
SizedBox(height: 20.0),
FloatingActionButton(
heroTag: 'removeTag', child: Icon(Icons.remove),
onPressed: () => _numBloc.add(NumberEvent.removeEvent))
]));
});
}
BlocProvider
BlocProvider 為 Bloc 的供應者,建立 Bloc 并供應給其子控件樹;
BlocProvider({
Key key,
@required Create<T> create,
Widget child,
bool lazy,
})
簡單了解源碼可知,BlocProvider 通過 create 建立一個 Bloc;通過 child 設定用來響應狀态的變更的 Widget;lazy 為是否懶建立(延遲建立),小菜了解的為是否在使用時再進行建立,預設為 true;
class _BlocPageState extends State<BlocPage> {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (BuildContext context) => NumberBloc(),
child: BlocBuilder<NumberBloc, int>(
condition: (previousState, state) {
print('BlocPage.condition->$previousState==$state');
return state <= 30 ? true : false;
}, builder: (context, count) {
return Scaffold(
appBar: AppBar(title: Text('Bloc Page')),
body: Center(
child: Column( mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('當 Number > 30 時,不進行變更', style: TextStyle(fontSize: 20.0, color: Colors.blue)),
SizedBox(height: 20.0),
Text('目前 Number = ${BlocProvider.of<NumberBloc>(context).state}', style: TextStyle(fontSize: 20.0, color: Colors.blue))
])),
floatingActionButton: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
FloatingActionButton(
heroTag: 'addTag', child: Icon(Icons.add),
onPressed: () => BlocProvider.of<NumberBloc>(context).add(NumberEvent.addEvent)),
SizedBox(height: 20.0),
FloatingActionButton(
heroTag: 'removeTag', child: Icon(Icons.remove),
onPressed: () => BlocProvider.of<NumberBloc>(context).add(NumberEvent.removeEvent))
]));
}));
}
}
BlocListener
BlocListener 與 BlocBuilder 應用有相似之處;其中 listener 用于監聽狀态變更,可在此做出相應的業務處理;
class BlocListener<B extends Bloc<dynamic, S>, S> extends BlocListenerBase<B, S> with BlocListenerSingleChildWidget {
final Widget child;
const BlocListener({
Key key,
@required BlocWidgetListener<S> listener,
B bloc,
BlocListenerCondition<S> condition,
this.child,
})
}
簡單分析源碼可得:
- child 為 BlocListener 提供的 Widget 用來響應狀态的變更;
- bloc 與 BlocBuilder 對應的 bloc 用法相同,如果省略了 bloc 參數,BlocListener 将使用 BlocProvider 和目前函數自動執行查找 BuildContext;
- condition 為可選的過度細粒度,包括兩個參數,之前的狀态和目前的狀态,傳回值為 Boolean 類型,true 為進行 listener 的監聽,false 時過濾掉 listener 的監聽;此時的過濾與 BlocBuilder 中的 condition 過濾無關;
- listener 在每次狀态變更時調用,其中包括上下文環境和目前狀态兩個參數;
@override
Widget build(BuildContext context) {
return BlocListener<NumberBloc, int>(
bloc: _numBloc,
listener: (context, count) {
print('BlocPage.listene->$count');
},
condition: (previousState, state) {
print('BlocPage.BlocListener.condition->$previousState==$state');
return state <= 20 ? true : false;
},
child: BlocBuilder<NumberBloc, int>(
bloc: _numBloc,
condition: (previousState, state) {
print('BlocPage.condition->$previousState==$state');
return state <= 30 ? true : false;
},
builder: (context, count) {
return Scaffold(
appBar: AppBar(title: Text('Bloc Page')),
body: Center(
child: Column( mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('當 Number > 20 時,BlocListener 過濾 listener 監聽,與 BlocBuilder 中過濾的狀态無關', style: TextStyle(fontSize: 20.0, color: Colors.red)),
SizedBox(height: 20.0),
Text('當 Number > 30 時,Number 不進行變更', style: TextStyle(fontSize: 20.0, color: Colors.green)),
SizedBox(height: 20.0),
Text('目前 Number = ${_numBloc.state}', style: TextStyle(fontSize: 20.0, color: Colors.blue))
])),
floatingActionButton: Column( crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
FloatingActionButton(
heroTag: 'addTag', child: Icon(Icons.add),
onPressed: () => _numBloc.add(NumberEvent.addEvent)),
SizedBox(height: 20.0),
FloatingActionButton(
heroTag: 'removeTag', child: Icon(Icons.remove),
onPressed: () => _numBloc.add(NumberEvent.removeEvent))
]));
}));
}
TestCode
小菜在測試過程中遇到一些小問題,僅簡單記錄一下,以防忘記;
Q1: There are multiple heroes that share the same tag within a subtree.
小菜在擴充上一節的 Demo 時,點選進入頁面時會黑屏,提示如下錯誤;
A1: 在 FloatingActionButton 中添加 heroTag 區分
以前在學習 Hero Animation 時,在同一個 Page 頁面不能用兩個相同的 heroTag,小菜這次忽略了 FloatingActionButton 中也應用了 Hero 動畫,需要區分一下即可;
floatingActionButton: Column(
crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
FloatingActionButton(
heroTag: 'addTag', child: Icon(Icons.add),
onPressed: () => _numBloc.add(NumberEvent.addEvent)),
FloatingActionButton(
heroTag: 'removeTag', child: Icon(Icons.remove),
onPressed: () => _numBloc.add(NumberEvent.removeEvent))
]));
Q2: BlocProvider.of() called with a context that does not contain a Bloc of type ...
A2: 在 build() 外建立或通過如下方式建立,并建議與 BlocBuilder 成對設定
// build() 方法外建立
NumberBloc _numBloc;
@override
void initState() {
super.initState();
_numBloc = NumberBloc();
}
// BlocProvider create() 建立
BlocProvider(
create: (BuildContext context) => NumberBloc(),
child: ....
);