Flutter:從ValueListenableBuilder到Provider(二)|技術點評

season_zhu發表於2021-03-10

上一節簡單回顧

在上一篇文章裡面,主要是講的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 春招闖關活動」, 點選檢視活動詳情

相關文章