Flutter ValueNotifier 組件原理剖析

在flutter的狀態(tài)管理中,ValueNotifier會被經(jīng)常使用,一般會和ValueListenableBuilder一起使用.ValueNotifier負(fù)責(zé)狀態(tài),ValueListenableBuilder負(fù)責(zé)嵌套組件來刷新

ValueNotifier<int> type = ValueNotifier(0);

ValueListenableBuilder(
  valueListenable: type,
  builder: (context, type, child) {
    return Text(type.toString());
  },
);

這篇文章就來深度剖析一下這個組件內(nèi)部的具體實現(xiàn)原理
首先我們從ValueNotifier入手,它的繼承關(guān)系

class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T>

mixin class ChangeNotifier implements Listenable

abstract class ValueListenable<T> extends Listenable

Listenable

最終根源是節(jié)點是Listenable,它提供了addListener,removeListener,用來添加監(jiān)聽和移除監(jiān)聽

ChangeNotifier

再來看ChangeNotifier,這個類在我們使用Provider的時候也是會經(jīng)常用到,并且很多的controller會是它的子類,比如常用的ScrollController,TabController.ChangeNotifier要實現(xiàn)Listenable的方法

void addListener(VoidCallback listener) {
   assert(ChangeNotifier.debugAssertNotDisposed(this));
   if (kFlutterMemoryAllocationsEnabled) {
     maybeDispatchObjectCreation(this);
   }
   if (_count == _listeners.length) {
     if (_count == 0) {
       _listeners = List<VoidCallback?>.filled(1, null);
     } else {
       final List<VoidCallback?> newListeners =
           List<VoidCallback?>.filled(_listeners.length * 2, null);
       for (int i = 0; i < _count; i++) {
         newListeners[i] = _listeners[i];
       }
       _listeners = newListeners;
     }
   }
   _listeners[_count++] = listener;
}

@override
  void removeListener(VoidCallback listener) {
    for (int i = 0; i < _count; i++) {
      final VoidCallback? listenerAtIndex = _listeners[i];
      if (listenerAtIndex == listener) {
        if (_notificationCallStackDepth > 0) {
          _listeners[i] = null;
          _reentrantlyRemovedListeners++;
        } else {
          _removeAt(i);
        }
        break;
      }
    }
  }

ChangeNotifier內(nèi)部提供_listeners,用來存放監(jiān)聽的回調(diào),來實現(xiàn)上面兩個函數(shù),主要作用是來操作_listeners添加刪除回調(diào)函數(shù)
ChangeNotifier特色函數(shù)notifyListeners

void notifyListeners() {
    assert(ChangeNotifier.debugAssertNotDisposed(this));
    if (_count == 0) {
      return;
    }
    _notificationCallStackDepth++;

    final int end = _count;
    for (int i = 0; i < end; i++) {
      try {
        _listeners[i]?.call();
      } catch (exception, stack) {
        FlutterError.reportError(FlutterErrorDetails(
          exception: exception,
          stack: stack,
          library: 'foundation library',
          context: ErrorDescription('while dispatching notifications for $runtimeType'),
          informationCollector: () => <DiagnosticsNode>[
            DiagnosticsProperty<ChangeNotifier>(
              'The $runtimeType sending notification was',
              this,
              style: DiagnosticsTreeStyle.errorProperty,
            ),
          ],
        ));
      }
    }

    _notificationCallStackDepth--;

    if (_notificationCallStackDepth == 0 && _reentrantlyRemovedListeners > 0) {
      final int newLength = _count - _reentrantlyRemovedListeners;
      if (newLength * 2 <= _listeners.length) {
        final List<VoidCallback?> newListeners = List<VoidCallback?>.filled(newLength, null);

        int newIndex = 0;
        for (int i = 0; i < _count; i++) {
          final VoidCallback? listener = _listeners[i];
          if (listener != null) {
            newListeners[newIndex++] = listener;
          }
        }

        _listeners = newListeners;
      } else {
        // Otherwise we put all the null references at the end.
        for (int i = 0; i < newLength; i += 1) {
          if (_listeners[i] == null) {
            // We swap this item with the next not null item.
            int swapIndex = i + 1;
            while (_listeners[swapIndex] == null) {
              swapIndex += 1;
            }
            _listeners[i] = _listeners[swapIndex];
            _listeners[swapIndex] = null;
          }
        }
      }

      _reentrantlyRemovedListeners = 0;
      _count = newLength;
    }
  }

大段代碼不想看,其實可以只關(guān)注

final int end = _count;
for (int i = 0; i < end; i++) {
  _listeners[i]?.call();
}

主要是調(diào)用監(jiān)聽的回調(diào)函數(shù),剩下的部分是對_listeners的優(yōu)化,在執(zhí)行完所有的回調(diào)函數(shù)后,才對_listeners進行長度變更,對應(yīng)removeListener,在_notificationCallStackDepth > 0的時候,并沒有對數(shù)組長度進行優(yōu)化,而是在回調(diào)結(jié)束后,才進行的優(yōu)化
比如只需要監(jiān)聽一次結(jié)果,然后在回調(diào)中移除了監(jiān)聽,這里如果正好在_listeners長度變化的節(jié)點上,需要重新開辟內(nèi)存,此時可能是出于性能的優(yōu)化,并不會在回調(diào)執(zhí)行過程中做數(shù)組的內(nèi)存變更,統(tǒng)一在所有回調(diào)結(jié)束后去操作,還有一種情況是我的回調(diào)函數(shù)很多,移除的也很多,可能會出現(xiàn)多次開辟內(nèi)存的情況,而最后統(tǒng)一處理,最多只需要開辟一次內(nèi)存,這個就屬于很細節(jié)的操作了
到這里,我們結(jié)束了對ChangeNotifier剖析

ValueListenable

abstract class ValueListenable<T> extends Listenable {
  /// Abstract const constructor. This constructor enables subclasses to provide
  /// const constructors so that they can be used in const expressions.
  const ValueListenable();

  /// The current value of the object. When the value changes, the callbacks
  /// registered with [addListener] will be invoked.
  T get value;
}

ValueListenable就是在Listenable基礎(chǔ)上添加了get value也是一個基類

ValueNotifier

在講解完上面的部分后,再來說ValueNotifier就相對容易些了

class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> {
  ValueNotifier(this._value);

  @override
  T get value => _value;

  T _value;

  set value(T newValue) {
    if (_value == newValue) {
      return;
    }
    _value = newValue;
    notifyListeners();
  }

  @override
  String toString() => '${describeIdentity(this)}($value)';
}

接口類ValueListenable中的添加刪除監(jiān)聽的實現(xiàn),被ValueNotifier的父類ChangeNotifier實現(xiàn)了,ValueListenable增加了成員變量_value,并且實現(xiàn)了ValueListenableget value,重點是在set value
調(diào)用了父類notifyListeners(),用來調(diào)用監(jiān)聽回調(diào)函數(shù),所以我們在最開始的例子中,直接執(zhí)行type.value++,就可以直接通知到被監(jiān)聽對象了

ValueListenableBuilder

這個組件是Widget組件,用來包裹需要刷新的組件,是一個StatefulWidget

typedef ValueWidgetBuilder<T> = Widget Function(BuildContext context, T value, Widget? child);
class ValueListenableBuilder<T> extends StatefulWidget {
  final ValueListenable<T> valueListenable;
  final ValueWidgetBuilder<T> builder;
  final Widget? child;
}

valueListenable抽象成了ValueListenable,所以所有實現(xiàn)了ValueListenable方法的類都可以使用這個組件來進行刷新
具體看下它的 State _ValueListenableBuilderState

class _ValueListenableBuilderState<T> extends State<ValueListenableBuilder<T>> {
  late T value;

  @override
  void initState() {
    super.initState();
    value = widget.valueListenable.value;
    widget.valueListenable.addListener(_valueChanged);
  }

  @override
  void didUpdateWidget(ValueListenableBuilder<T> oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.valueListenable != widget.valueListenable) {
      oldWidget.valueListenable.removeListener(_valueChanged);
      value = widget.valueListenable.value;
      widget.valueListenable.addListener(_valueChanged);
    }
  }

  @override
  void dispose() {
    widget.valueListenable.removeListener(_valueChanged);
    super.dispose();
  }

  void _valueChanged() {
    setState(() { value = widget.valueListenable.value; });
  }

  @override
  Widget build(BuildContext context) {
    return widget.builder(context, value, widget.child);
  }
}
  • initState中添加監(jiān)聽addListener -> _valueChanged
  • dispose中移除監(jiān)聽removeListener -> _valueChanged
  • _valueChanged,就是簡單的setState刷新組件
  • didUpdateWidget是在widget重新構(gòu)建后調(diào)用,如果監(jiān)聽對象變了,需要移除之前的監(jiān)聽,并且添加新的監(jiān)聽
    到這里,關(guān)于ValueNotifier的內(nèi)容就結(jié)束了,但是我們試想一下,如果一個widget組件需要監(jiān)聽多個valueListenable,如何做到,一種方式是ValueListenableBuilder嵌套,能解決問題,但是不是很優(yōu)雅
    完全可以根據(jù)上邊的原理,我們實現(xiàn)一套多監(jiān)聽的組件
final ValueListenable<T> valueListenable 變?yōu)?final List<ValueListenable> valueListenables;

將之前的單個valueListenable添加回調(diào)變成數(shù)組遍歷添加回調(diào),移除亦然,關(guān)于didUpdateWidget中對數(shù)組中的每個元素作比較這個方法很多,可以選擇

void initState() {
    super.initState();
    value = widget.valueListenables;
    _addListener(widget);
}

@override
  void dispose() {
    _removeListener(widget);
    super.dispose();
  }

  _addListener(ValueListenableListBuilder widget) {
    for (var element in widget.valueListenables) {
      element.addListener(_valueChanged);
    }
  }

  _removeListener(ValueListenableListBuilder widget) {
    for (var element in widget.valueListenables) {
      element.removeListener(_valueChanged);
    }
  }

 @override
  void didUpdateWidget(ValueListenableListBuilder<T> oldWidget) {
    if (!const DeepCollectionEquality().equals(oldWidget.valueListenables, widget.valueListenables)) {
      _removeListener(oldWidget);
      value = widget.valueListenables;
      _addListener(widget);
    }
    super.didUpdateWidget(oldWidget);
  }

  void _valueChanged() {
    setState(() {
      value = List.of(widget.valueListenables);
    });
  }

  @override
  Widget build(BuildContext context) {
    final result = value.map((e) => e.value).toList();
    return widget.builder(context, result, widget.child);
  }

再進一步優(yōu)化,上邊返回的result是List,元素沒有具體的類型,在使用的時候會有一些不便利,所以我們可以再上層,添加一層嵌套,以兩個為例,創(chuàng)建一個類來接收這兩個ValueListenable

class Tuple<T1, T2> {
  T1 value1;
  T2 value2;

  Tuple(this.value1, this.value2);

  List<R> toList<R>() => List.from([value1, value2]);

  factory Tuple.fromList(List list) {
    return Tuple(list[0], list[1]);
  }
}
typedef ValueTupleWidgetBuilder<T1, T2> = Widget Function(
  BuildContext context,
  Tuple<T1, T2> value,
  Widget? child,
);
class ValueListenableList2Builder<T1, T2> extends ValueListenableListBuilder {
  ValueListenableList2Builder({
    super.key,
    required Tuple<ValueListenable<T1>, ValueListenable<T2>> valueListenables,
    required ValueTupleWidgetBuilder<T1, T2> builder,
    super.child,
  }) : super(
          valueListenables: valueListenables.toList(),
          builder: (context, value, child) => builder(
            context,
            Tuple.fromList(value),
            child,
          ),
        );
}

這樣一來,我們在使用Tuple的時候就不用再考慮類型問題了

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末章钾,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌剩燥,老刑警劉巖人弓,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件外遇,死亡現(xiàn)場離奇詭異洛搀,居然都是意外死亡扳抽,警方通過查閱死者的電腦和手機斤贰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進店門智哀,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人荧恍,你說我怎么就攤上這事瓷叫⊥偷酰” “怎么了?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵摹菠,是天一觀的道長盒卸。 經(jīng)常有香客問我,道長次氨,這世上最難降的妖魔是什么蔽介? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮煮寡,結(jié)果婚禮上虹蓄,老公的妹妹穿的比我還像新娘。我一直安慰自己幸撕,他們只是感情好薇组,可當(dāng)我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著坐儿,像睡著了一般律胀。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上挑童,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天累铅,我揣著相機與錄音,去河邊找鬼站叼。 笑死娃兽,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的尽楔。 我是一名探鬼主播投储,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼阔馋!你這毒婦竟也來了玛荞?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤勋眯,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后下梢,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體客蹋,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年孽江,在試婚紗的時候發(fā)現(xiàn)自己被綠了讶坯。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡岗屏,死狀恐怖辆琅,靈堂內(nèi)的尸體忽然破棺而出漱办,到底是詐尸還是另有隱情,我是刑警寧澤婉烟,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布娩井,位于F島的核電站,受9級特大地震影響隅很,放射性物質(zhì)發(fā)生泄漏撞牢。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一叔营、第九天 我趴在偏房一處隱蔽的房頂上張望屋彪。 院中可真熱鬧,春花似錦绒尊、人聲如沸畜挥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蟹但。三九已至,卻和暖如春谭羔,著一層夾襖步出監(jiān)牢的瞬間华糖,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工瘟裸, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留客叉,地道東北人。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓话告,卻偏偏與公主長得像兼搏,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子沙郭,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內(nèi)容