Flutter组件(Widget)的局部刷新方式

作者: 好_好先生 | 来源:发表于2021-10-19 14:42 被阅读0次

    Flutter中有两个常用的状态Widget分为StatefulWidget和StatelessWidget,分别为动态视图和静态视图,视图的更新需要调用StatefulWidget的setState方法,这会遍历调用子Widget的build方法。如果一个页面内容比较复杂时,会包含多个widget,如果直接调用setState,会遍历所有子Widget的build,这样会造成很多不必要的开销,所以非常有必要了解Flutter中局部刷新的方式:

    通过GlobalKey局部刷新

    globalkey唯一定义了某个element,它使你能够访问与element相关联的其他对象,例如buildContext、state等。应用场景:跨widget访问状态。
    例如:可以通过key.currentState拿到它的状态对象,然后就可以调用其中的onPressed方法。

    class _MyHomePageState extends State<MyHomePage> {
      int _counter = 0;
    
      GlobalKey<_TextWidgetState> textKey = GlobalKey();
    
      @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:',
                ),
                TextWidget(textKey),// 需要更新的Text
              ],
            ),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: (){
              _counter ++;// 这里我们只给他值变动,状态刷新交给下面的key事件
              textKey.currentState.onPressed(_counter);//这个counter的值已经改变了,但是没有重绘所以我们看到的知识我们定义的初始值
            },
            tooltip: 'Increment',
            child: Icon(Icons.add),
          ), // This trailing comma makes auto-formatting nicer for build methods.
        );
      }
    }
    
    class TextWidget extends StatefulWidget {
      final Key key;
      const TextWidget(this.key);
      @override
      _TextWidgetState createState() => _TextWidgetState();
    }
    
    class _TextWidgetState extends State<TextWidget> {
      String _text = "0";
      @override
      Widget build(BuildContext context) {
        return Center(child: Text(_text, style: TextStyle(fontSize: 20),),);
      }
    
      void onPressed(int count) {
        setState(() {
          _text = count.toString();
        });
      }
    }
    

    ValueNotifier和ValueListenableBuilder

    Flutter框架内部提供了一个非常小巧精致的组件,专门用于局部组件的刷新。适用于值改动的刷新。

    class ValueNotifierTestPage extends StatefulWidget {
      @override
      _ValueNotifierTestPageState createState() => _ValueNotifierTestPageState();
    }
    
    class _ValueNotifierTestPageState extends State<ValueNotifierTestPage> {
    
      // 定义ValueNotifier<int> 对象 _counter
      final ValueNotifier<int> _counter = ValueNotifier<int>(0);
    
      void _incrementCounter(){
        _counter.value += 1;
      }
    
      @override
      Widget build(BuildContext context) {
        return Center(
          child:  Scaffold(
            appBar: AppBar(
              title: Text('ValueNotifierTestPage'),
            ),
            body: Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  WidgetA(),
                  Text( 'You have pushed the button this many times:'),
                  ValueListenableBuilder<int>(
                    builder: _buildWithValue,
                    valueListenable: _counter,
                  )
                ],
              ),
            ),
            floatingActionButton: FloatingActionButton(
              onPressed: _incrementCounter,
              child: Icon(Icons.add),
            ),
          ),
        );
      }
    
      Widget _buildWithValue(BuildContext context, int value, Widget child) {
        return Text(
          '$value',
          style: Theme.of(context).textTheme.headline4,
        );
      }
    
      @override
      void dispose() {
        _counter.dispose();
        super.dispose();
      }
    }
    
    class WidgetA extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Text('I am a widget that will not be rebuilt.');
      }
    }
    

    实现原理:在 initState 中对传入的可监听对象进行监听,执行 _valueChanged 方法,_valueChanged 中进行了 setState 来触发当前状态的刷新。触发 build 方法,从而触发 widget.builder 回调,这样就实现了局部刷新。可以看到这里回调的 child 是组件传入的 child,所以直接使用,这就是对 child 的优化的根源。

    可以看到 ValueListenableBuilder 实现局部刷新的本质,也是进行组件的抽离,让组件状态的改变框定在状态内部,并通过 builder 回调控制局部刷新,暴露给用户使用。

    StatefulBuilder局部刷新

    通过这个可以创建一个支持局部刷新的widget树,比如你可以在StatelessWidget里面刷新某个布局,但是不需要改变成StatefulWidget;也可以在StatefulWidget中使用做部分刷新而不需要刷新整个页面,这个刷新是不会调用Widget build(BuildContext context)刷新整个布局树的。

    class _MyHomePageState extends State<MyHomePage> {
      int _counter = 0;
    
      StateSetter _reloadTextSetter;
    
      @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:',
                ),
                StatefulBuilder(builder: (BuildContext context, StateSetter stateSetter){
                  _reloadTextSetter = stateSetter;
                  return Text(_counter.toString());
                })// 需要更新的Text
              ],
            ),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: (){
              _counter ++;
              _reloadTextSetter((){
              });
            },
            tooltip: 'Increment',
            child: Icon(Icons.add),
          ), // This trailing comma makes auto-formatting nicer for build methods.
        );
      }
    }
    

    FutureBuilder & StreamBuilder

    异步UI更新:
    很多时候我们会依赖一些异步数据来动态更新UI,比如在打开一个页面时我们需要先从互联网上获取数据,在获取数据的过程中显示一个加载框,等获取到数据时我们再渲染页面;又比如我们想展示Stream(比如文件流、互联网数据接收流)的进度。当然StatefulWidget我们完全可以实现以上功能。但由于在实际开发中依赖异步数据更新UI的这种场景非常常见,并且当StatefulWidget中控件树较大时,更新一个属性导致整个树重建,消耗性能,因此Flutter专门提供了FutureBuilder和SteamBuilder两个组件来快速实现这种功能。

    class _MyHomePageState extends State<MyHomePage> {
    
      Future<String> mockNetworkData() async {
        return Future.delayed(Duration(seconds: 2), () => "我是从互联网上获取的数据");
      }
    
      @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:',
                ),
                FutureBuilder(
                  future: mockNetworkData(),
                    builder: (BuildContext context, AsyncSnapshot snapshot){
                  if(snapshot.connectionState == ConnectionState.done){
                    if(snapshot.hasError){
                      // 请求失败,显示错误
                      return Text("Error: ${snapshot.error}");
                    }else {
                      // 请求成功,显示数据
                      return Text("Contents: ${snapshot.data}");
                    }
                  }else {
                    return CircularProgressIndicator();
                  }
                }),
              ],
            ),
          ), // This trailing comma makes auto-formatting nicer for build methods.
        );
      }
    }
    
    class _MyHomePageState extends State<MyHomePage> {
    
      Stream<int> counter(){
        return Stream.periodic(Duration(seconds: 1), (i){
          return i;
        });
      }
    
      @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:',
                ),
                StreamBuilder(
                    stream: counter(),
                    builder: (BuildContext context, AsyncSnapshot<int> snapshot){
                      if(snapshot.hasError){
                        return Text("Error: ${snapshot.error}");
                      }
    
                      switch (snapshot.connectionState){
                        case ConnectionState.none:
                          return Text("没有Stream");
                        case ConnectionState.waiting:
                          return Text("等待数据、、、");
                        case ConnectionState.active:
                          return Text("active: ${snapshot.data}");
                        case ConnectionState.done:
                          return Text("Stream已关闭");
                      }
                      return null;
                    }),
              ],
            ),
          ), // This trailing comma makes auto-formatting nicer for build methods.
        );
      }
    }
    

    InheritedWidget

    通常情况下,子Widget无法单独感知父Widget的变化,当父state变化时,通过其build重建所有子widget;
    InheriteddWidget可以避免这种全局创建,实现局部子Widget更新。InheritedWidget提供了一种在Widget树中从上到下传递、共享数据的方式。Flutter SDK正是通过InheritedWidget来共享应用主题和Locale等信息。
    InheritedWidgetData

    class InheritedWidgetData<T> extends InheritedWidget {
      InheritedWidgetData({
        Key key,
        @required Widget child,
        @required this.data,
      }) : super(key: key, child: child);
    
      final T data;
      @override
      bool updateShouldNotify(InheritedWidgetData<T> oldWidget) {
        return true;
      }
    }
    

    TestData

    class TestData extends StatefulWidget {
      TestData({
        Key key,
        this.child,
      }) : super(key: key);
    
      final Widget child;
    
      @override
      _TestDataState createState() => _TestDataState();
    
    
      static _TestDataState of(BuildContext context, {bool rebuild = true}) {
        if (rebuild) {
          return context.dependOnInheritedWidgetOfExactType<InheritedWidgetData<_TestDataState>>().data;
        }
        return context.findAncestorWidgetOfExactType<InheritedWidgetData<_TestDataState>>().data;
      }
    
    
    }
    
    class _TestDataState extends State<TestData> {
      int counter = 0;
    
      void _incrementCounter() {
        setState(() {
          counter++;
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return InheritedWidgetData(
          data: this,
          child: widget.child,
        );
      }
    }
    

    InheritedTest1Page

    import 'package:flutter/material.dart';
    
    class InheritedTest1Page extends StatefulWidget {
      @override
      _InheritedTest1PageState createState() => _InheritedTest1PageState();
    }
    
    class _InheritedTest1PageState extends State<InheritedTest1Page> {
      @override
      Widget build(BuildContext context) {
        return Center(
          child:  Scaffold(
            appBar: AppBar(
              title: Text('InheritedWidgetTest'),
            ),
            body: TestData(
              child: Column(
                children: <Widget>[
                  Padding(
                    padding: const EdgeInsets.only(left: 10.0, top: 10.0, right: 10.0),
                    child: Text('我们常使用的\nTheme.of(context).textTheme\nMediaQuery.of(context).size等\n就是通过InheritedWidget实现的',
                      style: Theme.of(context).textTheme.title,),
                  ),
                  WidgetA(),
                  WidgetB(),
                  WidgetC(),
                ],
              ),
            ),
          ),
        );
      }
    }
    
    class WidgetA extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
      final _TestDataState state = TestData.of(context);
    
        return Center(
          child: Text(
            '${state.counter}',
            style: Theme.of(context).textTheme.display1,
          ),
        );
      }
    }
    
    class WidgetB extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Text('I am a widget that will not be rebuilt.');
      }
    }
    
    class WidgetC extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
      final _TestDataState state = TestData.of(context, rebuild: false);
        return RaisedButton(
          onPressed: () {
            state._incrementCounter();
          },
          child: Icon(Icons.add),
        );
      }
    }
    

    ChangeNotifierProvider(Provider)

    provider是Google I/O 2019大会上宣布的现在官方推荐的管理方式,而ChangeNotifierProvider可以说是Provider的一种:
    yaml文件需要引入provider: ^3.1.0
    顶层嵌套ChangeNotifierProvider

    void main(){
      runApp(ChangeNotifierProvider(builder: (context) => DataInfo(), child: MyApp(),));
    }
    

    创建共享数据类DataInfo:
    数据类需要with ChangeNotifier 以使用 notifyListeners()函数通知监听者更新界面。

    class DataInfo with ChangeNotifier {
      int _count = 0;
    
      get count => _count;
    
      addCount(){
        _count ++;
        notifyListeners();
      }
    
      subCount(){
        _count --;
        notifyListeners();
      }
    }
    

    使用Provider.of(context)获取DataInfo

    @override
      Widget build(BuildContext context) {
        var dataInfo = Provider.of<DataInfo>(context);
        return Scaffold(
          appBar: AppBar(
            title: Text(widget.title),
          ),
          body: Center(
            child: Column(
    
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                FloatingActionButton(
                  heroTag: "add",
                  child: Icon(Icons.add),
                    onPressed: (){
                    dataInfo.addCount();
                    }
                    ),
                Text(
                  '${dataInfo.count}',
                ),
                FloatingActionButton(
                  heroTag: "sub",
                  child: Icon(Icons.add),
                    onPressed: (){
                    dataInfo.subCount();
                    })
              ],
            ),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: (){
              Navigator.of(context).push(CupertinoPageRoute(builder: (con){
                return NextPage();
              }));
    
            },
            tooltip: 'Increment',
            child: Icon(Icons.add),
          ), // This trailing comma makes auto-formatting nicer for build methods.
        );
      }
    

    nextPage:
    使用Consumer包住需要使用共享数据的Widget

    @override
    Widget build(BuildContext context) {
      return Consumer<DataInfo>(
        builder: (context, dataInfo, _){
          return Scaffold(
            appBar: AppBar(
              title: Text("next"),
            ),
            body: Center(
              child: Column(
    
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  FloatingActionButton(
                      heroTag: "add",
                      child: Icon(Icons.add),
                      onPressed: (){
                    dataInfo.addCount();
                      }
                  ),
                  Text(
                '${dataInfo.count}',
                  ),
                  FloatingActionButton(
                      heroTag: "sub",
                      child: Icon(Icons.add),
                      onPressed: (){
                    dataInfo.subCount();
                      })
                ],
              ),
            ),
            floatingActionButton: FloatingActionButton(
              onPressed: (){
                Navigator.of(context).pop();
              },
              tooltip: 'Increment',
              child: Icon(Icons.settings_input_composite),
            ), // This trailing comma makes auto-formatting nicer for build methods.
          );
        },
      );
    }
    

    RepaintBoundary

    RepaintBoundary就是重绘边界,用于重绘时独立于父视图。页面需要更新的页面结构可以用 RepaintBoundary组件嵌套,flutter 会将包含的组件独立出一层"画布",去绘制。官方很多组件 外层也包了层 RepaintBoundary 标签。如果你的自定义view比较复杂,应该尽可能的避免重绘。

    void markNeedsPaint() {
        if (_needsPaint)
          return;
        _needsPaint = true;
        if (isRepaintBoundary) { // 为true时,直接合成视图,避免重绘
          if (owner != null) {
            owner._nodesNeedingPaint.add(this);
            owner.requestVisualUpdate();
          }
        } else if (parent is RenderObject) {
          final RenderObject parent = this.parent;
          parent.markNeedsPaint();
          assert(parent == this.parent);
        } else {
          if (owner != null)
            owner.requestVisualUpdate();
        }
      }
    
    class RepainBoundaryPage extends StatefulWidget {
      @override
      _RepainBoundaryPageState createState() => _RepainBoundaryPageState();
    }
    
    class _RepainBoundaryPageState extends State<RepainBoundaryPage> {
      @override
      Widget build(BuildContext context) {
        return Center(
          child:  Scaffold(
            appBar: AppBar(
              title: Text('RepainBoundaryPage'),
            ),
            body: Column(
              children: <Widget>[
                WidgetA(),
                WidgetB(),
                WidgetC(),
              ],
            ),
          ),
        );
      }
    }
    
    class WidgetC extends StatefulWidget {
      @override
      _WidgetCState createState() => _WidgetCState();
    }
    
    class _WidgetCState extends State<WidgetC> {
      int counter = 0;
      @override
      Widget build(BuildContext context) {
        return RepaintBoundary(child: Column(
          children: [
            Text('~~~${counter}'),
            RaisedButton(
              onPressed: () {
                setState(() {
                  counter++;
                });
              },
              child: Icon(Icons.add),
            )
          ],
        ),);
      }
    }
    

    以上总结了几种Flutter的局部刷新的方式,可根据实际需要使用不同的方式,最适合的才是最好的。

    相关文章

      网友评论

        本文标题:Flutter组件(Widget)的局部刷新方式

        本文链接:https://www.haomeiwen.com/subject/xqtqgltx.html