讲清楚 ProxyWidget

在Flutter中,ProxyWidget 是一个特殊的widget,它不创建Widget,而是关联子组件和父组件,为它们之间的交流提供渠道,在适当的时候提供额外的功能。比如可以使用ProxyWidget来监视或更改子widget的约束、样式或属性,而无需直接修改子widget本身。它就是提供了一种机制,可以在不更改子widget本身的情况下,对其进行干预或者在其周围构建其他逻辑。

对应的,ProxyWidget 的 element 是 ProxyElement。

ProxyWidget 有三个直接子类:

1、ParentDataWidget:在实际开发中,常用于视图数据的传递

2、InheritedWidget:在实际开发中,常用于业务数据的传递

3、NotificationListener:通知冒泡,在实际开发中,常用于滚动监听

ParentDataWidget

引用注释的一句话:

css 复制代码
A [ParentDataWidget] is specific to a particular kind of [ParentData].

父组件会用存储在子组件的 RenderObject 的 ParentData 来对子组件进行一些处理,比如布局、绘制。

ParentDataWidget 正是用来配置子组件的 RenderObject 的 ParentData 的。

可以类比 Android 中的 ViewGroup.LayoutParams 。

那么可以从读、写两个角度分析这里的数据流转:

写:即设置 ParentData:

组件在 mount 之后,会依次执行 attachRenderObject 、_updateParentData 方法,最后执行到 applyParentData:

arduino 复制代码
@protected
void applyParentData(RenderObject renderObject);

这里以 Positioned 组件为例,它是一个 StackParentData 类型的 ParentDataWidget

scala 复制代码
sdk:
class Positioned extends ParentDataWidget<StackParentData>

demo:
@override
Widget build(BuildContext context) {
  return Scaffold(
    body: SafeArea(
      child: Stack(
        children: [
          Positioned(
            top: 50,
            left: 50,
            child: _box(),
          ),
        ],
      ),
    ),
  );
}

对于 Positioned 组件的 applyParentData 方法:

ini 复制代码
@override
void applyParentData(RenderObject renderObject) {
  assert(renderObject.parentData is StackParentData);
  final StackParentData parentData = renderObject.parentData! as StackParentData;
  bool needsLayout = false;

  if (parentData.left != left) {
    parentData.left = left;
    needsLayout = true;
  }
  
  // ... 省略其它属性
  
  if (needsLayout) {
    final AbstractNode? targetParent = renderObject.parent;
    if (targetParent is RenderObject) {
      targetParent.markNeedsLayout();
    }
  }
}

当第一次执行时,子组件的 parentData.left 为 null,那么给它设置新数据 50,标记 needsLayout 为 true,最后 markNeedsLayout() ,等待更新ui。

后续触发更新时,会触发 ProxyElement 的 notifyClients 方法,最后还是走到 applyParentData 方法,如果新旧数据不一致,再次触发 markNeedsLayout() 。

读:即获取 ParentData:

向上寻找 Positioned 的父组件 Stack 的 performLayout 方法:

ini 复制代码
@override
void performLayout() {
  final BoxConstraints constraints = this.constraints;
  _hasVisualOverflow = false;

  size = _computeSize(
    constraints: constraints,
    layoutChild: ChildLayoutHelper.layoutChild,
  );

  assert(_resolvedAlignment != null);
  RenderBox? child = firstChild;
  while (child != null) {
 
    /// 获取到 StackParentData
    final StackParentData childParentData = child.parentData! as StackParentData;

    if (!childParentData.isPositioned) {
      childParentData.offset = _resolvedAlignment!.alongOffset(size - child.size as Offset);
    } else {
      _hasVisualOverflow = layoutPositionedChild(child, childParentData, size, _resolvedAlignment!) || _hasVisualOverflow;
    }

    assert(child.parentData == childParentData);
    child = childParentData.nextSibling;
  }
}

在 RenderStack 的 performLayout 中,容易找到,通过 child 的 parentData 属性,获取到了 StackParentData。如果子组件被 Positioned 组件包裹,那么它会执行

layoutPositionedChild,

源码这里不贴了,逻辑就是通过 StackParentData 携带来的 上、下、左、右、宽、高等数据计算出约束和偏移,通过 layout 方法进行布局。

由此,这里可以得到自定义 ParentDataWidget 的一点思路:

dart 复制代码
demo:
class MyPosition extends ParentDataWidget<StackParentData> {
  const MyPosition({
    super.key,
    required super.child,
    required this.top,
  });

  final double top;

  // 仅提供简略逻辑。这里将top设置为输入值的2倍
  @override
  void applyParentData(RenderObject renderObject) {
    final StackParentData parentData =
        renderObject.parentData! as StackParentData;
    parentData.top = 2 * top;
    (renderObject.parent as RenderObject).markNeedsLayout();
  }

  // 给哪个组件提供parentData信息。仅作为debug信息。
  @override
  Type get debugTypicalAncestorWidgetClass => Stack;
}

实际结果影响了布局时的数据。

更进一步:自定义 ProxyWidget 、ProxyElement

scala 复制代码
class MyProxyWidget extends ProxyWidget {
  const MyProxyWidget({
    super.key,
    required this.width,
    required super.child,
  });

  final int width;

  @override
  Element createElement() {
    return MyProxyElement(this);
  }
}

class MyProxyElement extends ProxyElement {
  MyProxyElement(super.widget);

  @override
  void notifyClients(MyProxyWidget oldWidget) {
    if ((widget as MyProxyWidget).width != oldWidget.width) {
      print("======> 数据变更,去做事");
    }
  }
}

InheritedWidget

最早接触 InheritedWidget 是在学习状态管理的一些框架中,比如 Provider、Bloc等,都是对于 InheritedWidget 的封装。子组件可以通过 context 向上搜索父组件,找到对应InheritedWidget 提供的数据。

典型的实现是:继承 InheritedWidget 组件后,实现maybeOf 和 of 方法

scala 复制代码
demo:
class FrogColor extends InheritedWidget {
  const FrogColor({
    super.key,
    required this.color,
    required super.child,
  });

  final Color color;

  static FrogColor? maybeOf(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<FrogColor>();
  }

  static FrogColor of(BuildContext context) {
    final FrogColor? result = maybeOf(context);
    assert(result != null, 'No FrogColor found in context');
    return result!;
  }

  @override
  bool updateShouldNotify(FrogColor oldWidget) => color != oldWidget.color;
}

典型使用是:在子组件中调用

less 复制代码
demo:
@override
Widget build(BuildContext context) {
  return Scaffold(
    body: FrogColor(
      color: Colors.green,
      child: Builder(
        builder: (BuildContext innerContext) {
          // 1、必须在子组件中调用
          // 2、这里通过 Builder 组件构造了一个子组件环境
          final textColor = FrogColor.of(innerContext).color;
          return Text(
            'Hello Frog',
            style: TextStyle(color: textColor),
          );
        },
      ),
    ),
  );
}

InheritedWidget 是一个典型的观察者模式的实现,那么可以从以下两方面分析:

1、注册监听

以上面的 dependOnInheritedWidgetOfExactType 方法为切入口:

dart 复制代码
@override
T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object? aspect}) {
  assert(_debugCheckStateIsActiveForAncestorLookup());
  final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
  if (ancestor != null) {
    return dependOnInheritedElement(ancestor, aspect: aspect) as T;
  }
  _hadUnsatisfiedDependencies = true;
  return null;
}

容易观察到,InheritedWidget 是从 _inheritedWidgets 成员中获取的,

ini 复制代码
PersistentHashMap<Type, InheritedElement>? _inheritedWidgets;

_inheritedWidgets 是 Element 的一个成员,widget 在执行 mount 方法中,会执行 _updateInheritance() 方法,这里给 _inheritedWidgets 赋值,

ini 复制代码
@override
void _updateInheritance() {
  assert(_lifecycleState == _ElementLifecycle.active);
  final PersistentHashMap<Type, InheritedElement> incomingWidgets =
      _parent?._inheritedWidgets ?? const PersistentHashMap<Type, InheritedElement>.empty();
  _inheritedWidgets = incomingWidgets.put(widget.runtimeType, this);
}

map 以 widget.runtimeType 为 Key,所以在多层嵌套的时候,获取的数据只会来自离当前组件最近的组件。

此时,如果找到了对应类型的 InheritedElement ,那么通过 dependOnInheritedElement 方法向它注册监听,将自己(this)加入到 _dependents 中:

dart 复制代码
@override
InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object? aspect }) {
  assert(ancestor != null);
  _dependencies ??= HashSet<InheritedElement>();
  _dependencies!.add(ancestor);
  ancestor.updateDependencies(this, aspect);
  return ancestor.widget as InheritedWidget;
}


final Map<Element, Object?> _dependents = HashMap<Element, Object?>();

到这里,完成了注册监听的操作。额外的,观察到:

java 复制代码
@override
InheritedElement? getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
  assert(_debugCheckStateIsActiveForAncestorLookup());
  final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
  return ancestor;
}

getElementForInheritedWidgetOfExactType 与 dependOnInheritedWidgetOfExactType 的区别:前者没有注册监听。

2、获取数据改变,通知观察者

数据改变,界面发生更新,唯一的方式就是去执行 Element 的 performRebuild() 方法,重新创建一个新的Widget。这里有必要贴出部分注释,讲一下 didChangeDependencies方法。

在 StatefulWidget 中, State 对象有个 didChangeDependencies 方法,在多次重建时,它会在数据发生变化时执行,以下方法依次执行:

typescript 复制代码
StatefulWidget 部分注释:
/// The second category is widgets that use [State.setState] or depend on
/// [InheritedWidget]s. These will typically rebuild many times during the
/// application's lifetime, and it is therefore important to minimize the impact
/// of rebuilding such a widget. (They may also use [State.initState] or
/// [State.didChangeDependencies] and allocate resources, but the important part
/// is that they rebuild.)

InheritedElement 的 notifyClients 方法部分注释:
/// Notifies all dependent elements that this inherited widget has changed, by
/// calling [Element.didChangeDependencies].
///
/// This method must only be called during the build phase. Usually this
/// method is called automatically when an inherited widget is rebuilt, e.g.
/// as a result of calling [State.setState] above the inherited widget.

InheritedElement.updated // 数据更新后的执行
@override
void updated(InheritedWidget oldWidget) {
  if ((widget as InheritedWidget).updateShouldNotify(oldWidget)) { // 重建方法的判断
    super.updated(oldWidget); // 执行父类的updated
  }
}

ProxyElement.updated
@protected
void updated(covariant ProxyWidget oldWidget) {
  notifyClients(oldWidget); // 父类实现
}

InheritedElement.notifyClients
/// 忽略了 assert
@override
void notifyClients(InheritedWidget oldWidget) {
  for (final Element dependent in _dependents.keys) {
    notifyDependent(oldWidget, dependent); // 遍历上述 注册监听 时的 _dependents
  }
}

InheritedElement.notifyDependent
@protected
void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {
  dependent.didChangeDependencies(); // 这里执行原组件的 didChangeDependencies
}

StatefulElement.didChangeDependencies
@override
void didChangeDependencies() {
  super.didChangeDependencies(); // 调用了 super.
  _didChangeDependencies = true; // 这里标识没有影响下面的 super.performRebuild();
}

Element.didChangeDependencies
/// 忽略了 assert
@mustCallSuper
void didChangeDependencies() {
  markNeedsBuild(); // 标记需要重建,等待下一帧
}

StatefulElement.performRebuild
@override
void performRebuild() {
  if (_didChangeDependencies) {
    state.didChangeDependencies(); // 调用了state的didChangeDependencies方法,回到了业务里
    _didChangeDependencies = false;
  }
  super.performRebuild(); // 重建
}

以上描述了两个逻辑:

a、向父组件 InheritedWidget 注册了的子组件且满足 updateShouldNotify 的条件,子组件的 didChangeDependencies 才会被执行。

b、如果使用 setState() 方法, build 方法不管注册与否,都会调用,如果要减少 build 方法执行,需要实现缓存,涉及到状态管理逻辑的处理。

scala 复制代码
demo:
class OneChild extends StatefulWidget {
  const OneChild({
    super.key,
    required this.color,
  });

  final Color color;

  @override
  State<OneChild> createState() => _OneChildState();
}

class _OneChildState extends State<OneChild> {
  @override
  Widget build(BuildContext context) {
    // 如果不注册 InheritedWidget ,
    // 即使通过 setState 改变了数据,didChangeDependencies也不会执行。
    // final textColor = FrogColor.of(context).color;
    return Text(
      'Hello Frog',
      style: TextStyle(color: widget.color),
    );
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    print("======> didChangeDependencies");
  }
}

接下来继续讲 InheritedWidget 实现局部刷新的原理。这里可以先对比一下:

csharp 复制代码
ProxyElement:
@override
Widget build() => (widget as ProxyWidget).child;

StatefulElement:
@override
Widget build() => state.build(this);

StatelessElement:
@override
Widget build() => (widget as StatelessWidget).build(this);

ProxyElement 的 build 方法直接返回了 child。

ini 复制代码
省略部分逻辑
@protected
@pragma('vm:prefer-inline')
Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {
  final Element newChild;
  if (child != null) {
    bool hasSameSuperclass = true;
    if (hasSameSuperclass && child.widget == newWidget) {
      newChild = child; // 1 直接赋值
    } else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
      child.update(newWidget); // 2 更新重建
    } else {
      deactivateChild(child);
      newChild = inflateWidget(newWidget, newSlot); // 3 新建
    }
  } else {
    newChild = inflateWidget(newWidget, newSlot);
  }
  return newChild;
}

上述说到:InheritedElement.updated 触发了后续的更新重建,它的调用时从:

Element.updateChild -> ProxyElement.update -> InheritedElement.updated 来的。

这里,通过源码的 if-else 判断,至少 child.widget != newWidget 时,才会执行 child.update(newWidget);。

而 ProxyElement 并没有提供 newWidget,没有新建,只是提供了子 widget。

NotificationListener

典型用法:

scala 复制代码
// 1.定义一个通知
class NumNotification extends Notification {
  int notifyNum;

  NumNotification({
    required this.notifyNum,
  });
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: NotificationListener<NumNotification>(
          onNotification: (notification) { // 2.设置监听回调
            print("======> ${notification.notifyNum}");
            return true;
          },
          child: SizedBox(
            width: 100,
            height: 100,
            child: ColoredBox(
              color: Colors.red,
              child: Builder(
                builder: (context) {
                  return GestureDetector(
                    onTap: () {
                    // 3.dispatch 分发通知
                      NumNotification(notifyNum: 1).dispatch(context);
                    },
                  );
                },
              ),
            ),
          ),
        ),
      ),
    );
  }
}

相对于ParentDataWidget和InheritedWidget的原理来讲,NotificationListener比较简单,这里直接贴出_NotificationElement、NotifiableElementMixin、_NotificationNode的源码,并增加了关键注释:

scala 复制代码
class _NotificationElement<T extends Notification> extends ProxyElement 
    with NotifiableElementMixin {  // 混入 NotifiableElementMixin
  _NotificationElement(NotificationListener<T> super.widget);

  @override
  bool onNotification(Notification notification) {
    final NotificationListener<T> listener = widget as NotificationListener<T>;
    if (listener.onNotification != null && notification is T) { // 有设置对应类型的监听回调
      return listener.onNotification!(notification); // 处理收到的通知
    }
    return false;
  }

  @override
  void notifyClients(covariant ProxyWidget oldWidget) {
    // sdk:
    // Notification tree does not need to notify clients.
    // custom:
    // sdk注释说在通知里,notifyClients不需要有实际操作,
    // 这也是NotificationListener与ParentDataWidget、InheritedWidget的区别之一,
    // 后两者都需要依赖notifyClients方法做数据更新。
  }
}

mixin NotifiableElementMixin on Element {
  /// Called when a notification of the appropriate type arrives at this
  /// location in the tree.
  ///
  /// Return true to cancel the notification bubbling. Return false to
  /// allow the notification to continue to be dispatched to further ancestors.
  bool onNotification(Notification notification);

  // Notification tree 是由一个个 _NotificationNode 组成的,
  // attachNotificationTree() 方法会在widget 在执行 mount 方法中 执行,
  //(与InheritedWidget 的 _updateInheritance();方法执行时机一致。)
  @override
  void attachNotificationTree() {
    _notificationTree = _NotificationNode(_parent?._notificationTree, this);
  }
}

class _NotificationNode {
  _NotificationNode(this.parent, this.current);

  NotifiableElementMixin? current;
  // 当前的 _NotificationNode 可以拿到父节点的 _NotificationNode
  _NotificationNode? parent; 

  // 处理通知
  void dispatchNotification(Notification notification) {
  // 这里的if判断执行了两步操作
  // 1 处理 onNotification 的业务回调
  // 2 根据回调结果,如返回 true,不再向上冒泡,如返回 false,继续冒泡给父组件处理
    if (current?.onNotification(notification) ?? true) {
      return;
    }
    parent?.dispatchNotification(notification);
  }
}
相关推荐
钛态2 小时前
Flutter for OpenHarmony:mockito 单元测试的替身演员,轻松模拟复杂依赖(测试驱动开发必备) 深度解析与鸿蒙适配指南
服务器·驱动开发·安全·flutter·华为·单元测试·harmonyos
念格5 小时前
Flutter 弹窗 UI 不刷新?用 StatefulBuilder 解决
flutter
程序员老刘7 小时前
2026春招Flutter岗位为何变少?我看到的3个招聘逻辑变化
flutter·ai编程·客户端
念格7 小时前
Flutter 实现点击任意位置收起键盘的最佳实践
flutter
念格7 小时前
Flutter ListView Physics 滚动物理效果详解
flutter
国医中兴7 小时前
ClickHouse的数据模型设计:从理论到实践
flutter·harmonyos·鸿蒙·openharmony
国医中兴10 小时前
ClickHouse数据导入导出最佳实践:从性能到可靠性
flutter·harmonyos·鸿蒙·openharmony
国医中兴11 小时前
大数据处理的性能优化技巧:从理论到实践
flutter·harmonyos·鸿蒙·openharmony
●VON12 小时前
Flutter 入门指南:从基础组件到状态管理核心机制
前端·学习·flutter·von
西西学代码12 小时前
Flutter---SingleChildScrollView
前端·javascript·flutter