ARTS Tips:Flutter动态替换Widget的练习

956 阅读2分钟

前面了解完Flutter的三个核心对象以及布局的文章,对于Flutter在UI上的设计有一定深入了解,可以尝试做些练习来加深理解。

这里我们做一个动态替换Widget的练习,题目灵感来自重磅! flutter视图局部更新 - 林二鹿 - 博客园 ,主要的目的是在用户点击某个按钮的时候替换一个文本Widget组件。

做作业之前回顾一下组件树的展示过程:build->mount->layout->paint,这里用标准demo举例,初始代码如下:

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '动态替换叶子节点的Widget',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: '动态替换叶子节点的Widget'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('You have pushed the button this many times:',),
            Text('$_counter', style: Theme.of(context).textTheme.headline4,),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), 
    );
  }
}

我们试试改造一下,首先去掉_incrementCounter的调用,不使用setState的机制,直接在点击相应事件中,生成新的title widget,然后通过Element来更新组件树。Widget title的可见范围是个点,常见的方式放到_MyHomePageState类级别,也可以放到Widget build(BuildContext context)函数里,通过匿名函数FloatingActionButton(onPressed: () {})来保留引用,因为Widget title在这个过程一直是变化的,所以基本达到要求。

这里需要感谢林二鹿做的尝试,帮助排除了不少坑,最终代码如下:

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(new MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '动态替换叶子节点的Widget',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: '动态替换叶子节点的Widget'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  Widget title = new Text('another times: 0',);

  static Element findChild(Element e, Widget w) {
    Element child;
    void visit(Element element) {
      if (w == element.widget)
        child = element;
      else
        element.visitChildren(visit);
    }
    visit(e);
    return child;
  }

  @override
  Widget build(BuildContext context) {
    print('Enter _MyHomePageState.build()');
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
            title,
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          print('Enter floatingActionButton.onPressed()');
          _counter++;
          Element e = findChild(context as Element, title);
          if (e != null) {
            title = new Text('another times: $_counter',);
            e.owner.lockState(() {
              e.update(title);
            });
          }
        },
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

回头总结一下,这个练习还是很必要,有以下收获:

  • 不通过setState也可以直接更新,setState会自动触发更新,以后如果需要运行时更新,也可以通过在Element上执行update方法来达到更新效果
  • 这次练习用的是叶子节点,如果是非叶子节点且子树的复杂度低,全部重来也没事,如果子树复杂度高,则可以考虑替换当前节点和复用子树的方法,这时需要留有子树的引用,方便用来重新构建当前节点
  • 实际的情况中准确找到Element很重要,这里有性能的考虑也有计算复杂度的考虑,这里可以简化
  • Element树的引用在Widget build(BuildContext context)中可以访问到,其实就是参数BuildContext context,因为Element是可变对象,会尽量被复用,所以在一个UI树,这个对象会不变
  • 这里还有一个点,State里面其实有一个Element的引用,和build方法里的参数是同一个对象
  • 这里动态替换的widget要求必须是同一个 runtimeType
  • 除了owner.lockState(),还有一个owner.buildScope看样子也比较重要需要琢磨

Reference