上一節簡單回顧
在上一篇文章裡面,主要是講的ValueListenableBuilder的基本使用,我個人認為ValueNotifier與ValueListenableBuilder在一對一的繫結(一個變數對於一個Widget)中,簡單易用,容易理解,而如果在多對一的繫結中,使用ValueListenableBuilder可能會陷入巢狀的地獄之中,所以這個時候就是MultiProvider登場了。
往期文章:
Flutter:從ValueListenableBuilder到Provider(一)|技術點評
MultiProvider
說到MultiProvider,它其實是provider庫中,針對多狀態繫結一個Widget的一個元件。 說到provider庫我們總會與全域性狀態管理聯絡在一起,孰不知,其實provider在單頁面管理上面也非常好的,甚至可以說,理解了provider在單頁面管理,那麼全域性管理不在話下。
那麼我們還是先接著上一節的文末最後的例子,接著寫程式碼。 需求如下:兩個輸入框,第一個輸入框輸入11位手機號並且第二個輸入框輸入6位驗證碼時,按鈕變色且響應點選事件。
在寫元件程式碼之前,我們先定義下面兩個模型:
class IsRightPhoneNumber extends ChangeNotifier {
bool _isOK = false;
bool get isOK => _isOK;
set isOK(bool newValue) {
_isOK = newValue;
notifyListeners();
}
}
class IsRightCode extends ChangeNotifier {
bool _isOK = false;
bool get isOK => _isOK;
set isOK(bool newValue) {
_isOK = newValue;
notifyListeners();
}
}
複製程式碼
也許你會好奇,為啥要寫這兩個模型,這個兩個模型又有何用?上一節裡面,我們並沒有深入說ValueNotifier的原始碼,那麼我們現在去看看ValueNotifier的原始碼:
class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> {
/// Creates a [ChangeNotifier] that wraps this value.
ValueNotifier(this._value);
/// The current value stored in this notifier.
///
/// When the value is replaced with something that is not equal to the old
/// value as evaluated by the equality operator ==, this class notifies its
/// listeners.
@override
T get value => _value;
T _value;
set value(T newValue) {
if (_value == newValue)
return;
_value = newValue;
notifyListeners();
}
@override
String toString() => '${describeIdentity(this)}($value)';
}
複製程式碼
如果你仔細看,就會發現,我們定義的模型和ValueNotifier用異曲同工之妙,都繼承了ChangeNotifier,而ChangeNotifier中最最重要的方法就是notifyListeners()
,它幹嘛呢?它就是通知告訴介面,值變化了,要重新整理介面了。
可以說我們自定義的模型是ValueNotifier的簡化與特化版,簡化是因為我們並沒有遵守ValueListenable
協議,特化是因為我們將ValueNotifier中泛型指定成為我們定義的型別。
當然自定義的模型直接繼承ValueNotifier來進行編寫也是可以的。
下面進入介面的主要程式碼:
和上一節一樣,其他的非主題邏輯的程式碼我會附在最後。
/// 定義兩個需要監聽的全域性變數
final _isRightPhoneNumber = IsRightPhoneNumber();
final _isRightCode = IsRightCode();
@override
Widget build(BuildContext context) {
/// 構建頁面的頂層我們使用了MultiProvider元件,並註冊監聽_isRightPhoneNumber與_isRightCode
return MultiProvider(
providers: [
ChangeNotifierProvider(
create: (context) => _isRightPhoneNumber,
),
ChangeNotifierProvider(
create: (context) => _isRightCode,
),
],
child: GestureDetector(
child: Scaffold(
appBar: AppBar(
title: Text("MultiProvider兩個輸入繫結一個按鈕"),
),
body: Container(
margin: const EdgeInsets.fromLTRB(16, 40, 16, 0),
child: ListView(
children: [
_textField(
title: "輸入11位字串",
limit: 11,
onChanged: (inputString) {
_isRightPhoneNumber.isOK = inputString.length == 11;
}),
_textField(
title: "輸入6位字串",
limit: 6,
onChanged: (inputString) {
_isRightCode.isOK = inputString.length == 6;
}),
/// 使用provider庫中的Selector元件
/// 首先不要被<IsRightPhoneNumber, IsRightCode, bool>這三個泛型瞎壞了,
/// 它實際是定義兩個入參分別為IsRightPhoneNumber和IsRightCode型別,返回bool型別,在Selector2中的selecto進行使用
Selector2<IsRightPhoneNumber, IsRightCode, bool>(
selector: (context, value1, value2) =>
value1.isOK && value2.isOK,
builder: (context, isAllRight, child) {
return Container(
margin: const EdgeInsets.fromLTRB(0, 30, 0, 0),
child: RaisedButton(
color: _buttonColor(isAllRight),
shape: BeveledRectangleBorder(
borderRadius: BorderRadius.circular(0),
),
child: Container(
child: Center(
child: Text(
"Selector2+自定義模型 確認修改",
style: TextStyle(
fontSize: 16,
color: isAllRight
? Colors.white
: DSColor.colorA3A4A4,
),
),
),
height: 48,
),
onPressed: () {
if (isAllRight) {}
},
),
);
},
),
],
),
),
),
onTap: () => print("鍵盤收起方法"),
),
);
}
複製程式碼
程式碼解讀:
一個關鍵點是構建頁面的頂層我們使用了MultiProvider元件,並註冊監聽_isRightPhoneNumber與_isRightCode;
另一個關鍵是Selector2元件的使用,首先不要被<IsRightPhoneNumber, IsRightCode, bool>這三個泛型嚇壞了,它實際是定義兩個入參分別為IsRightPhoneNumber和IsRightCode型別,返回bool型別,在Selector2中的selector進行使用,而Selector2中的builder其實和ValueListenableBuilder的builder非常相似,就是通過變數去建立Widget,並且在變數變化的時候去改變Widget,而這個變數實際是由selector進行控制,selector的在本例子中本質型別是一個bool Function(IsRightPhoneNumber, IsRightCode)
的函式而已。
Selector與Consumer
Selector2是專門處理有兩個入參,合併為一個新的出參的元件,另還有Selector3、Selector4、Selector5、Selector6這樣的元件可以使用,分別對應的意思通過類名最後追加的數字應該就可以理解吧。
除了Selector系列元件,還有一個Consumer元件,我理解它其實就是Selector簡化版本,入參少了selector和shouldRebuild引數,它的builder方法中會直接上按順序定義好的泛型引數,進行Widget的建立與更新。這裡我們的按鈕也完全可以使用Consumer2來進行構建,其程式碼如下:
Consumer2<IsRightPhoneNumber, IsRightCode>(
builder: (context, IsRightPhoneNumber value1, IsRightCode value2, child) {
final isAllRight = value1.isOK && value2.isOK;
return Container(
margin: const EdgeInsets.fromLTRB(0, 30, 0, 0),
child: RaisedButton(
color: _buttonColor(isAllRight),
shape: BeveledRectangleBorder(
borderRadius: BorderRadius.circular(0),
),
child: Container(
child: Center(
child: Text(
"Consumer2+自定義模型 確認修改",
style: TextStyle(
fontSize: 16,
color: isAllRight
? Colors.white
: DSColor.colorA3A4A4,
),
),
),
height: 48,
),
onPressed: () {
if (isAllRight) {}
},
),
);
},
),
複製程式碼
整體而言,不管是Selector還是Consumer,在使用方法上和ValueListenableBuilder基本一致,而Selector的精細程度上更上一層樓,除了入參有selector方法,用來展平多入參統一到唯一變數進行Widget的更新外,另外還有shouldRebuild引數來判斷新值與舊值是否真的需要進行Widget更新,整體而言,對於單頁面的多引數控制Widget,無論是從編碼與易用性上看,對於開發者都是十分友好的。
抽出來的非業務程式碼
將下面這些程式碼CV到每個例子的類中就可以了。
Widget _textField({
String title,
int limit,
ValueChanged<String> onChanged,
}) {
return Column(
children: [
TextField(
inputFormatters: [LengthLimitingTextInputFormatter(limit)],
decoration: InputDecoration(
hintText: title,
hintStyle: TextStyle(
color: Color(0xFFA3A4A4),
fontSize: 14,
),
enabledBorder: UnderlineInputBorder(
// 不是焦點的時候顏色
borderSide: BorderSide(color: Color(0xFF303131)),
),
focusedBorder: UnderlineInputBorder(
// 焦點集中的時候顏色
borderSide: BorderSide(color: Color(0xFFC3B5AB)),
),
),
keyboardType: TextInputType.number,
onChanged: onChanged,
),
_spacer23(),
],
);
}
Widget _spacer23() {
return SizedBox(
height: 23,
);
}
Color _buttonColor(bool value) {
if (value) {
return Color(0xFFC3B5AB);
} else {
return Color(0xFF303131);
}
}
複製程式碼
下一節,聊一下我對全域性管理的理解吧,嗯,之前掉坑裡去了,現在也沒爬起來。。。
參考文件:
Flutter 元件 | ValueListenableBuilder 區域性重新整理小能手
【Flutter 技能篇】你不得不會的狀態管理 Provider
本文正在參與「掘金 2021 春招闖關活動」, 點選檢視活動詳情