likes
comments
collection
share

深入理解Flutter UI系统

作者站长头像
站长
· 阅读数 4

Flutter UI系统

它提供了一套Dart API,然后在底层通过OpenGL这种跨平台的绘制库(内部会调用操作系统API)实现了一套代码跨多端。由于Dart API也是调用操作系统API,所以它的性能接近原生。

Flutter中,一切都是Widget,当UI要发生变化时,我们不去直接修改DOM,而是通过更新状态,让Flutter UI系统来根据新的状态来重新构建UI。

Widget与Element

Widget只是UI元素的一个配置数据,并且一个Widget可以对应多个Element

@immutable
abstract class Widget extends DiagnosticableTree {
  const Widget({ this.key });
  final Key key;

  @protected
  Element createElement();

  @override
  String toStringShort() {
    return key == null ? '$runtimeType' : '$runtimeType-$key';
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense;
  }

  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }
}
  • canUpdate(...)是一个静态方法,它主要用于在Widget树重新build时复用旧的widget,其实具体来说,应该是:是否用新的Widget对象去更新旧UI树上所对应的Element对象的配置;通过其源码我们可以看到,只要newWidgetoldWidgetruntimeTypekey同时相等时就会用newWidget去更新Element对象的配置,否则就会创建新的Element

另外Widget类本身是一个抽象类,其中最核心的就是定义了createElement()接口,在Flutter开发中,我们一般都不用直接继承Widget类来实现一个新组件,相反,我们通常会通过继承StatelessWidgetStatefulWidget来间接继承Widget类来实现。StatelessWidgetStatefulWidget都是直接继承自Widget类,而这两个类也正是Flutter中非常重要的两个抽象类,它们引入了两种Widget模型,接下来我们将重点介绍一下这两个类。

3.1.4 StatelessWidget

StatelessWidget用于不需要维护状态的场景,它通常在build方法中通过嵌套其它Widget来构建UI,在构建过程中会递归的构建其嵌套的Widget。我们看一个简单的例子:

Context

build方法有一个context参数,它是BuildContext类的一个实例,表示当前widget在widget树中的上下文,每一个widget都会对应一个context对象(因为每一个widget都是widget树上的一个节点)。实际上,context是当前widget在widget树中位置中执行”相关操作“的一个句柄,比如它提供了从当前widget开始向上遍历widget树以及按照widget类型查找父级widget的方法。下面是在子树中获取父级widget的一个示例:

lass ContextRoute extends StatelessWidget {
  @override
 
 Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Context测试"),
      ),
      body: Container(
        child: Builder(builder: (context) {
          // 在Widget树中向上查找最近的父级`Scaffold` widget
          Scaffold scaffold = context.findAncestorWidgetOfExactType<Scaffold>();
          // 直接返回 AppBar的title, 此处实际上是Text("Context测试")
          return (scaffold.appBar as AppBar).title;
        }),
      ),
    );
  }
}

3.1.5 StatefulWidget

StatelessWidget一样,StatefulWidget也是继承自Widget类,并重写了createElement()方法,不同的是返回的Element 对象并不相同;另外StatefulWidget类中添加了一个新的接口createState()

abstract class StatefulWidget extends Widget {
  const StatefulWidget({ Key key }) : super(key: key);

  @override
  StatefulElement createElement() => new StatefulElement(this);

  @protected
  State createState();
}

  • StatefulElement 间接继承自Element类,与StatefulWidget相对应(作为其配置数据)。StatefulElement中可能会多次调用createState()来创建状态(State)对象。
  • createState() 用于创建和Stateful widget相关的状态,它在Stateful widget的生命周期中可能会被多次调用。例如,当一个Stateful widget同时插入到widget树的多个位置时,Flutter framework就会调用该方法为每一个位置生成一个独立的State实例,其实,本质上就是一个StatefulElement对应一个State实例。

3.1.6 State

在widget生命周期中可以被改变,当State被改变时,可以手动调用其setState()方法通知Flutter framework状态发生改变,Flutter framework在收到消息后,会重新调用其build方法重新构建widget树,从而达到更新UI的目的。

State生命周期

  void initState()
  Widget build(BuildContext context) 
  void didUpdateWidget(CounterWidget oldWidget) 
  void deactivate() 
  void dispose() 
  void didChangeDependencies() 

我们运行应用并打开该路由页面,在新路由页打开后

I/flutter ( 5436): initState
I/flutter ( 5436): didChangeDependencies
I/flutter ( 5436): build

可以看到,在StatefulWidget插入到Widget树时首先initState方法会被调用。

然后我们点击⚡️按钮热重载,控制台输出日志如下:

I/flutter ( 5436): reassemble
I/flutter ( 5436): didUpdateWidget
I/flutter ( 5436): build

可以看到此时initStatedidChangeDependencies都没有被调用,而此时didUpdateWidget被调用。

  • initState:当Widget第一次插入到Widget树时会被调用,对于每一个State对象,Flutter framework只会调用一次该回调,所以,通常在该回调中做一些一次性的操作,如状态初始化、订阅子树的事件通知等。不能在该回调中调用BuildContext.dependOnInheritedWidgetOfExactType(该方法用于在Widget树上获取离当前widget最近的一个父级InheritFromWidget,关于InheritedWidget我们将在后面章节介绍),原因是在初始化完成后,Widget树中的InheritFromWidget也可能会发生变化,所以正确的做法应该在在build()方法或didChangeDependencies()中调用它。
  • didChangeDependencies():当State对象的依赖发生变化时会被调用;例如:在之前build() 中包含了一个InheritedWidget,然后在之后的build()InheritedWidget发生了变化,那么此时InheritedWidget的子widget的didChangeDependencies()回调都会被调用。典型的场景是当系统语言Locale或应用主题改变时,Flutter framework会通知widget调用此回调。
  • build():此回调读者现在应该已经相当熟悉了,它主要是用于构建Widget子树的,会在如下场景被调用:
    1. 在调用initState()之后。
    2. 在调用didUpdateWidget()之后。
    3. 在调用setState()之后。
    4. 在调用didChangeDependencies()之后。
    5. 在State对象从树中一个位置移除后(会调用deactivate)又重新插入到树的其它位置之后。
  • reassemble():此回调是专门为了开发调试而提供的,在热重载(hot reload)时会被调用,此回调在Release模式下永远不会被调用。
  • didUpdateWidget():在widget重新构建时,Flutter framework会调用Widget.canUpdate来检测Widget树中同一位置的新旧节点,然后决定是否需要更新,如果Widget.canUpdate返回true则会调用此回调。正如之前所述,Widget.canUpdate会在新旧widget的key和runtimeType同时相等时会返回true,也就是说在在新旧widget的key和runtimeType同时相等时didUpdateWidget()就会被调用。
  • deactivate():当State对象从树中被移除时,会调用此回调。在一些场景下,Flutter framework会将State对象重新插到树中,如包含此State对象的子树在树的一个位置移动到另一个位置时(可以通过GlobalKey来实现)。如果移除后没有重新插入到树中则紧接着会调用dispose()方法。
  • dispose():当State对象从树中被永久移除时调用;通常在此回调中释放资源。

StatefulWidget生命周期如图3-2所示:

深入理解Flutter UI系统

3.1.8 Flutter SDK内置组件库介绍

Flutter提供了一套丰富、强大的基础组件,在基础组件库之上Flutter又提供了一套Material风格(Android默认的视觉风格)和一套Cupertino风格(iOS视觉风格)的组件库。要使用基础组件库,需要先导入:

import 'package:flutter/widgets.dart';

下面我们介绍一下常用的组件。

基础组件

  • Text:该组件可让您创建一个带格式的文本。
  • RowColumn: 这些具有弹性空间的布局类Widget可让您在水平(Row)和垂直(Column)方向上创建灵活的布局。其设计是基于Web开发中的Flexbox布局模型。
  • Stack: 取代线性布局 (译者语:和Android中的FrameLayout相似),Stack允许子 widget 堆叠, 你可以使用 Positioned 来定位他们相对于Stack的上下左右四条边的位置。Stacks是基于Web开发中的绝对定位(absolute positioning )布局模型设计的。
  • ContainerContainer 可让您创建矩形视觉元素。container 可以装饰一个BoxDecoration, 如 background、一个边框、或者一个阴影。 Container 也可以具有边距(margins)、填充(padding)和应用于其大小的约束(constraints)。另外, Container可以使用矩阵在三维空间中对其进行变换。

Material组件

Flutter提供了一套丰富的Material组件,它可以帮助我们构建遵循Material Design设计规范的应用程序。Material应用程序以MaterialApp 组件开始, 该组件在应用程序的根部创建了一些必要的组件,比如Theme组件,它用于配置应用的主题。 是否使用MaterialApp完全是可选的,但是使用它是一个很好的做法。在之前的示例中,我们已经使用过多个Material 组件了,如:ScaffoldAppBarFlatButton等。要使用Material 组件,需要先引入它:

import 'package:flutter/material.dart';

Cupertino组件

Flutter也提供了一套丰富的Cupertino风格的组件,尽管目前还没有Material 组件那么丰富,但是它仍在不断的完善中。值得一提的是在Material 组件库中有一些组件可以根据实际运行平台来切换表现风格,比如MaterialPageRoute,在路由切换时,如果是Android系统,它将会使用Android系统默认的页面切换动画(从底向上);如果是iOS系统,它会使用iOS系统默认的页面切换动画(从右向左)。由于在前面的示例中还没有Cupertino组件的示例,下面我们实现一个简单的Cupertino组件风格的页面:

//导入cupertino widget库
import 'package:flutter/cupertino.dart';

class CupertinoTestRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      navigationBar: CupertinoNavigationBar(
        middle: Text("Cupertino Demo"),
      ),
      child: Center(
        child: CupertinoButton(
            color: CupertinoColors.activeBlue,
            child: Text("Press"),
            onPressed: () {}
        ),
      ),
    );
  }
}

其他

  1. 深入理解StatelessWidget

  2. abstract class StatelessWidget extends Widget Containing class: StatelessWidget A widget that does not require mutable state. A stateless widget is a widget that describes part of the user interface by building a constellation of other widgets that describe the user interface more concretely. The building process continues recursively until the description of the user interface is fully concrete (e.g., consists entirely of RenderObjectWidgets, which describe concrete RenderObjects). www.youtube.com/watch?v=wE7… Stateless widget are useful when the part of the user interface you are describing does not depend on anything other than the configuration information in the object itself and the BuildContext in which the widget is inflated. For compositions that can change dynamically, e.g. due to having an internal clock-driven state, or depending on some system state, consider using StatefulWidget. Performance considerations The build method of a stateless widget is typically only called in three situations: the first time the widget is inserted in the tree, when the widget's parent changes its configuration, and when an InheritedWidget it depends on changes. If a widget's parent will regularly change the widget's configuration, or if it depends on inherited widgets that frequently change, then it is important to optimize the performance of the build method to maintain a fluid rendering performance. There are several techniques one can use to minimize the impact of rebuilding a stateless widget: Minimize the number of nodes transitively created by the build method and any widgets it creates. For example, instead of an elaborate arrangement of Rows, Columns, Paddings, and SizedBoxes to position a single child in a particularly fancy manner, consider using just an Align or a CustomSingleChildLayout. Instead of an intricate layering of multiple Containers and with Decorations to draw just the right graphical effect, consider a single CustomPaint widget. Use const widgets where possible, and provide a const constructor for the widget so that users of the widget can also do so. Consider refactoring the stateless widget into a stateful widget so that it can use some of the techniques described at StatefulWidget, such as caching common parts of subtrees and using GlobalKeys when changing the tree structure. If the widget is likely to get rebuilt frequently due to the use of InheritedWidgets, consider refactoring the stateless widget into multiple widgets, with the parts of the tree that change being pushed to the leaves. For example instead of building a tree with four widgets, the inner-most widget depending on the Theme, consider factoring out the part of the build function that builds the inner-most widget into its own widget, so that only the inner-most widget needs to be rebuilt when the theme changes. {@tool snippet} The following is a skeleton of a stateless widget subclass called GreenFrog. Normally, widgets have more constructor arguments, each of which corresponds to a final property. class GreenFrog extends StatelessWidget { const GreenFrog({ Key key }) : super(key: key);

    @override Widget build(BuildContext context) { return Container(color: const Color(0xFF2DBD3A)); } } {@end-tool} {@tool snippet} This next example shows the more generic widget Frog which can be given a color and a child: class Frog extends StatelessWidget { const Frog({ Key key, this.color = const Color(0xFF2DBD3A), this.child, }) : super(key: key);

    final Color color; final Widget child;

    @override Widget build(BuildContext context) { return Container(color: color, child: child); } } {@end-tool} By convention, widget constructors only use named arguments. Named arguments can be marked as required using @required. Also by convention, the first argument is key, and the last argument is child, children, or the equivalent. See also: StatefulWidget and State, for widgets that can build differently several times over their lifetime. InheritedWidget, for widgets that introduce ambient state that can be read by descendant widgets.

    1. 不需要可变状态的小部件。
    2. 非常具体描述某一个事情的时候使用
    3. 参考: www.youtube.com/watch?v=wE7…
    4. 当您所描述的用户界面部分不依赖于对象本身中的配置信息和小部件膨胀的构建上下文之外的任何东西时,无状态小部件非常有用。对于可以动态更改的组合,例如,由于具有内部时钟驱动状态,或取决于某些系统状态,请考虑使用StatefulWidget。
    5. 性能注意事项
      1. 无状态小部件的构建方法通常只在三种情况下调用:
      2. 第一次将小部件插入树中时,
      3. 当小部件的父级更改其配置时,
      4. 以及当继承的小部件依赖于更改时。
    6. 如果小部件的父级将定期更改小部件的配置,或者如果它依赖于频繁更改的继承小部件,那么优化构建方法的性能以保持流体渲染性能非常重要。
      1. 最小化由build方法及其创建的任何小部件可传递地创建的节点数。例如,为了以一种特别奇特的方式定位单个子级,而不是对行、列、填充和大小框进行精心安排,而是考虑使用Align或CustomSingleChildLayout。考虑一个单独的CustomPaint小部件,而不是用多个容器和装饰来绘制正确的图形效果的复杂分层。
      2. 尽可能使用const widgets,并为小部件提供一个const构造函数,这样小部件的用户也可以这样做。
      3. 考虑将无状态小部件重构为有状态小部件,以便它可以使用StatefulWidget中描述的一些技术,例如缓存子树的公共部分,以及在更改树结构时使用globalkey。
      4. 如果小部件可能由于使用继承的小部件而频繁地重新构建,那么考虑将无状态的小部件重构为多个小部件,并将树中更改的部分推到叶子上。例如,与其构建一个包含四个小部件(最内部的小部件取决于主题)的树,不如考虑将构建最内部小部件的构建函数部分分解到自己的小部件中,这样当主题发生变化时,只需要重新构建最内部的小部件。
  3. StatelessWidget

  4. InheritedWidget 数据共享组件

    1. 提供了一种数据在widget树中从上到下传递、共享的方式,比如我们在应用的根widget中通过InheritedWidget共享了一个数据,那么我们便可以在任意子widget中来获取该共享的数据!如Flutter SDK中正是通过InheritedWidget来共享应用主题(Theme)和Locale (当前语言环境)信息的。

    2. 特性 didChangeDependencies

    3. state中didChangeDependencies的回调方法,再依赖发生变化的时候会进行被调用;依赖的行为只指子的widget是否使用了父widget中InheritedWidget 的数据;如果使用了就代表依赖了,依赖其的子widget的didChangeDependencies方法将会被调用。

    4. context.getElementForInheritedWidgetOfExactType<ShareDataWidget>().widget
      
      
    5. 如果是通过 'getElementForInheritedWidgetOfExactType'

把dependOnInheritedWidgetOfExactType()方法换成了context.getElementForInheritedWidgetOfExactType<ShareDataWidget>().widget,那么他们到底有什么区别呢,我们看一下这两个方法的源码(实现代码在Element类中,Context和Element的关系我们将在后面专门介绍):

@override
InheritedElement getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
  assert(_debugCheckStateIsActiveForAncestorLookup());
  final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
  return ancestor;
}
@override
InheritedWidget dependOnInheritedWidgetOfExactType({ Object aspect }) {
  assert(_debugCheckStateIsActiveForAncestorLookup());
  final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
  //多出的部分
  if (ancestor != null) {
    assert(ancestor is InheritedElement);
    return dependOnInheritedElement(ancestor, aspect: aspect) as T;
  }
  _hadUnsatisfiedDependencies = true;
  return null;
}

我们可以看到,dependOnInheritedWidgetOfExactType() 比 getElementForInheritedWidgetOfExactType()多调了dependOnInheritedElement方法,dependOnInheritedElement源码如下:

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

可以看到dependOnInheritedElement方法中主要是注册了依赖关系!看到这里也就清晰了,调用dependOnInheritedWidgetOfExactType()getElementForInheritedWidgetOfExactType()的区别就是前者会注册依赖关系,而后者不会,所以在调用dependOnInheritedWidgetOfExactType()时,InheritedWidget和依赖它的子孙组件关系便完成了注册,之后当InheritedWidget发生变化时,就会更新依赖它的子孙组件,也就是会调这些子孙组件的didChangeDependencies()方法和build()方法。而当调用的是 getElementForInheritedWidgetOfExactType()时,由于没有注册依赖关系,所以之后当InheritedWidget发生变化时,就不会更新相应的子孙Widget。

参考: book.flutterchina.club/chapter7/in…

转载自:https://juejin.cn/post/6850418121762455566
评论
请登录