讲清楚 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);
  }
}
相关推荐
江上清风山间明月6 小时前
flutter bottomSheet 控件详解
android·flutter·底部导航·bottomsheet
yuanlaile21 小时前
纯Dart Flutter库适配HarmonyOS
flutter·华为·harmonyos·flutter开发鸿蒙·harmonyos教程
yuanlaile21 小时前
Flutter开发HarmonyOS 鸿蒙App的好处、能力以及把Flutter项目打包成鸿蒙应用
flutter·华为·harmonyos·flutter开发鸿蒙
zacksleo1 天前
鸿蒙原生开发手记:04-一个完整元服务案例
flutter
jcLee952 天前
Flutter/Dart:使用日志模块Logger Easier
flutter·log4j·dart·logger
tmacfrank2 天前
Flutter 异步编程简述
flutter
tmacfrank2 天前
Flutter 基础知识总结
flutter
叫我菜菜就好2 天前
【Flutter_Web】Flutter编译Web第三篇(网络请求篇):dio如何改造方法,变成web之后数据如何处理
前端·网络·flutter
AiFlutter2 天前
Flutter-底部分享弹窗(showModalBottomSheet)
java·前端·flutter
m0_748247803 天前
Flutter Intl包使用指南:实现国际化和本地化
前端·javascript·flutter