Flutter: UI: Widget Types

 17th December 2020 at 4:20pm

Flutter 主要有这几种 widget:

  • Stateless
  • Stateful
  • Inherited

Flutter 的思路大量借鉴了 React。Stateless widget 类似于没有本地 state 的 React 组件(pure component);Stateful 则是有本地 state 的。Inherited 则类似于 React 的 context(不过我没有深入过)。

Flutter 的 widget 用来描述 app 的 UI 结构。渲染 UI 时,Flutter 会递归地解析 widget 嵌套,然后构建一棵 element tree。

Stateless

Stateless widget 是一个 const 类,它的所有属性都是 final 的。如:

定义

class DogName extends StatelessWidget {
    final String name;
    
    const DogName(this.name);
    
    @override
    Widget build(BuildContext context) {
        return Text(name);
    }
}

使用

class DogApp extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
        return MaterialApp(
            title: 'My Dog App',
            home: Column(
                children: [
                    DogName('Rocky'),
                    DogName('Jimmy'),
                ]
            )
        );
    }
}

Stateful Widget

例子:

class ItemCounter extends StatefulWidget {
  final String name;

  ItemCounter({this.name});

  @override
  _ItemCounterState createState() => _ItemCounterState();
}

class _ItemCounterState extends State<ItemCounter> {
  int count = 0;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
        onTap: () {
          setState(() {
            count++;
          });
        },
        child: Text('${widget.name}: $count'));
  }
}

Stateful Widget 分为两部分:

  • Widget object 负责:
    • 保存不会变的值,比如上面的 name
    • 创建 state object
  • State object 负责:
    • 保存会变化的值,比如上面的 count
    • 构建 child widgets(通过 build 方法)

为什么 stateful widget 和 state 是分开的两个类?

You might wonder why StatefulWidget and State are separate objects. In Flutter, these two types of objects have different life cycles. Widgets are temporary objects, used to construct a presentation of the application in its current state. State objects, on the other hand, are persistent between calls to build(), allowing them to remember information.

State 是要被存进 element tree 的,而 widget 不被存入 element tree。

那为什么 build() 函数是在 state 中而不是 widget 中呢?不靠谱猜测 是 Flutter 运行时发现 state 中的属性有变化时,想要能快速地找到相应的 build() 函数来构建新的展示。

Flutter 的 UI 重绘机制

对于 stateful widget,当调用 setState() 时,Flutter 会把该 widget 标记为 dirty 进行重新渲染。

但假如某个 stateful widget 是一个 widget 列表构成的:

[DogName('Rocky'), DogName('Jimmy')]

如果使用代码对两个 DogName widget 位置改变,Jimmy 跑到前头,那 Flutter 是否能正确绘制出来呢?答案是否定的。因为 Flutter 为了绘制效率,并不会销毁原先的两个 DogName widget 去重绘,而是判断变化后的 widget 的类型和 key 是否相同。由于这个场景中没有使用 key,而 widget 类型又不变,因此没有被正确绘制。你需要指定 key:

class DogName extends StatelessWidget {
    final String name;
    
    // 留意这里加了 key 参数
    const DogName({Key key, this.name}) : super(key: key);
    
    @override
    Widget build(BuildContext context) {
        return Text(name);
    }
}

[DogName(name: 'Rocky', key: 1), DogName(name: 'Jimmy', key: 2)]

Widget Lifecycle

对于 StatefulWidget,当框架把 state object 加进 element tree 时,会调其 initState() 函数;当 state object 不再被需要时,框架会调用它的 dispose()initState() 需要先调用 super.initState()dispose() 一般会在结尾调 super.dispose()

Inherited Widget

这种 widget 可以方便地将数据从 widget tree 上端传至下端:

class FrogColor extends InheritedWidget {
  const FrogColor({
    Key key,
    @required this.color,
    @required Widget child,
  }) : assert(color != null),
       assert(child != null),
       super(key: key, child: child);

  final Color color;

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

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

class MyPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: FrogColor(
        color: Colors.green,
        child: Builder(
          builder: (BuildContext innerContext) {
            return Text(
              'Hello Frog',
              style: TextStyle(color: FrogColor.of(innerContext).color),
            );
          },
        ),
      ),
    );
  }
}

比如上面的 FrogColor

  • 它并没有 build() 方法,不需要描述具体的子 widget 的展示,只需要提供 children 变量让使用者定义子 widget
  • 它虽然在 widget tree 中作为一个节点,但作用仅限于存储一个 color 变量,供子节点使用
  • 子节点一旦使用了 FrogColor.of()(底层是 context.dependOnInheritedWidgetOfExactType())后,一旦 FrogColor 中的数据发生变化(updateShouldNotify() 为 true 时),子节点就会被重绘

参考