background

As we all know, Flutter is developed based on the idea of front-end framework React. There are many similarities and some invisible differences. What I feel most is the ubiquitous rebuild of flutter, so is there any way to prevent rebuild? There are!

Prefixes the widget with const

This works, once and for all, but once you add const, your widget will never update, and unless you are writing static pages, you should probably not use it

Write your component as “leaf” component

Refer to flutter the document is to define your components as the leaves, the bottom of the tree, and then you change status inside the components in the leaves, so that mutual influence between the leaves, emm, it seems to me like this with the state of the react the ideas instead of ascension to each other because of you, you can’t put the state to the root node, on the root node, As soon as I call setState, all the custom values will be rebuilt. I always use this idea to solve the problems of rebuild, such as using StreamBuilder, which can wrap your components, I then use the stream to trigger the internal rebuild of the StreamBuilder, and use the StreamBuilder to isolate the external components. There is a slight drawback to this. I have to write an extra stream and close the stream.

Use other libraries, such as providers

You can see that the authors of the Provider library provide some widgets to reduce rebuild, but I feel that they are not very simple to use. The implementation of these libraries is similar to that of StreamBuilder, where one Widget isolates other widgets and updates are limited internally. But one thing they all have in common is that you need extra external variables to trigger internal updates

The ultimate way to

As anyone who has used React knows, the React class component has a very important lifecycle called shouldComponentUpdate. We can override this declaration cycle inside the component to optimize performance.

If the properties of the component are the same as those of the old and new props, then the component does not need to be updated. Does flutter have a similar life cycle? No!

The Flutter team thinks that the flutter rendering is fast enough, and flutter actually has a react diff algorithm to compare whether elements need to be updated. They do optimization and caching, because updating the flutter element is an expensive operation. The Rebuild Widget just re-creates an instance of the Widget as if it were just executing the DART code. There are no UI layer changes involved, and they also diff the old and new widgets to reduce changes to the Element layer. However, as long as it doesn’t cause Element destruction or rebuilding, it generally doesn’t affect performance.

But the fact that you can still find people searching for ways to prevent rebuild on Google and Baidu shows that there is a market for it. In my opinion, this is not called excessive optimization. In fact, there is a scenario that needs to be optimized. For example, the state management library Provider recommended by Google provides methods to reduce unnecessary rebuild

I don’t want to talk too much:

library should_rebuild_widget;

import 'package:flutter/material.dart';

typedef ShouldRebuildFunction<T> = bool Function(T oldWidget, T newWidget);

class ShouldRebuild<T extends Widget> extends StatefulWidget {
  final T child;
  final ShouldRebuildFunction<T> shouldRebuild;
  ShouldRebuild({@required this.child, this.shouldRebuild}):assert(() {if(child == null) {throw FlutterError.fromParts(
          <DiagnosticsNode>[
            ErrorSummary('ShouldRebuild widget: builder must be not null')]); }return true; } ());@override
  _ShouldRebuildState createState() => _ShouldRebuildState<T>();
}

class _ShouldRebuildState<T extends Widget> extends State<ShouldRebuild> {
  @override
  ShouldRebuild<T> get widget => super.widget;
  T oldWidget;
  @override
  Widget build(BuildContext context) {
    final T newWidget = widget.child;
    if (this.oldWidget == null || (widget.shouldRebuild == null ? true : widget.shouldRebuild(oldWidget, newWidget))) {
      this.oldWidget = newWidget;
    }
    returnoldWidget; }}Copy the code

Here are a few lines of code, less than 40 lines, for the test code:

import 'dart:math';

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

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: Test(), ); }}class Test extends StatefulWidget {
  @override
  _TestState createState() => _TestState();
}

class _TestState extends State<Test> {
  int productNum = 0;
  int counter = 0;

  _incrementCounter(){
    setState(() {
      ++counter;
    });
  }
  _incrementProduct(){
    setState(() {
      ++productNum;
    });
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Container(
          constraints: BoxConstraints.expand(),
          child: Column(
            children: <Widget>[
              ShouldRebuild<Counter>(
                shouldRebuild: (oldWidget, newWidget) => oldWidget.counter != newWidget.counter,
                child: Counter(counter: counter,onClick: _incrementCounter,title: 'I'm Counter optimized',) ,
              ),
              Counter(
                counter: counter,onClick: _incrementCounter,title: 'I'm Counter that's not optimized',
              ),
              Text('productNum = $productNum',style: TextStyle(fontSize: 22,color: Colors.deepOrange),),
              RaisedButton(
                onPressed: _incrementProduct,
                child: Text('increment Product'() [() (() (() (() (() () (() () () () ( }}class Counter extends StatelessWidget {
  final VoidCallback onClick;
  final int counter;
  final String title;
  Counter({this.counter,this.onClick,this.title});
  @override
  Widget build(BuildContext context) {
    Color color = Color.fromRGBO(Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1);
    return AnimatedContainer(
      duration: Duration(milliseconds: 500),
      color:color,
      height: 150,
      child:Column(
        children: <Widget>[
          Text(title,style: TextStyle(fontSize: 30),),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text('counter = The ${this.counter}',style: TextStyle(fontSize: 43,color: Colors.white),),
            ],
          ),
          RaisedButton(
            color: color,
            textColor: Colors.white,
            elevation: 20,
            onPressed: onClick,
            child: Text('increment Counter'() [() [() [() }}Copy the code

Layout renderings:

  • We define a Counter component that will change its background color during build. Every time the build is executed, the background color will be randomly generated so that we can observe whether the component is built or not. In addition, Counter receives the value Counter from the parent and displays it, as well as a title to distinguish between different Counter names
  • Look at the code here
           Column(
            children: <Widget>[
              ShouldRebuild<Counter>(
                shouldRebuild: (oldWidget, newWidget) => oldWidget.counter != newWidget.counter,
                child:  Counter(counter: counter,onClick: _incrementCounter,title: 'I'm Counter optimized',),
              ),
              Counter(
                counter: counter,onClick: _incrementCounter,title: 'I'm Counter that's not optimized',
              ),
              Text('productNum = $productNum',style: TextStyle(fontSize: 22,color: Colors.deepOrange),),
              RaisedButton(
                onPressed: _incrementProduct,
                child: Text('increment Product'),) ",Copy the code

Our top Counter is wrapped by ShouldRebuild, At the same time, the shouldRebuild parameter is passed with a custom condition. If the Counter received is inconsistent, the rebuild will not be done if the old and new Counter are found to be the same, and the following Counter is not optimized.

  • Let’s click on the add Product buttonincrement ProductSo the counter wrapped with ShouldRebuild doesn’t have rebuild, and the counter that doesn’t have a package below is rebuilt. Let’s see GIF:

The principle of revelation

The principle of flutter is the same as that of const widgets

Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
...
      if (child.widget == newWidget) {
        if(child.slot ! = newSlot) updateSlotForChild(child, newSlot);return child;
      }
      if (Widget.canUpdate(child.widget, newWidget)) {
        if(child.slot ! = newSlot) updateSlotForChild(child, newSlot); child.update(newWidget);assert(child.widget == newWidget);
        assert(() {
          child.owner._debugElementWasRebuilt(child);
          return true; } ());returnchild; }... }Copy the code

Excerpt some of them. Number one

if (child.widget == newWidget) {
        if(child.slot ! = newSlot) updateSlotForChild(child, newSlot);return child;
   }
Copy the code

This is the key point. Flutter finds the child.widget that the old widget is the same as the new widget, and returns the child directly if the reference is the same

If you find any inconsistency, go here

if (Widget.canUpdate(child.widget, newWidget)) {
        if(child.slot ! = newSlot) updateSlotForChild(child, newSlot); child.update(newWidget);assert(child.widget == newWidget);
        assert(() {
          child.owner._debugElementWasRebuilt(child);
          return true; } ());return child;
      }
Copy the code

If it can be updated, it will go child.update(), and if it does, the build method will definitely execute. See what it did

@override
  void update(StatelessWidget newWidget) {
    super.update(newWidget);
    assert(widget == newWidget);
    _dirty = true;
    rebuild();
  }
Copy the code

See rebuild() and you know that you must execute build.

If (child.widget == newWidget) const Text() does not repeat build, because constants do not change

conclusion

With this Widget we can put all the states in the root component, split the page into sub-components, wrap the sub-components with ShouldRebuild and set the rebuild condition to prevent unnecessary rerendering. You can setState as much as you like, of course, if your state is used by multiple components, then you need state management. However, some people may think whether to optimize excessively. Personally, I think whether to optimize depends on your own situation. If users feedback that your page is stuck one day, then you need to optimize, or you feel that the rebuild affects your function, for example, the animation is repeated, then you need to stop the rebuild.

Making:shouldRebuild

If you think it helps you, please star