[Flutter performance] It's 2021, are you still using setState in your animation?

Author: Zhang Jie Telie crazy

1. Pre-knowledge

For each UI frame, Animate, Build, Layout, Compositing bits, Paint, Compositing are mainly executed in sequence. Whenever the interface changes, it is triggered by a frame and the result will be updated. The following two grids represent the UI time (left) and Raster time (right) of a frame. When the left side is high, it means there is a problem with your interface. Looking at the following two UI frames, you can see that Build occupies a large part, which means that the UI may have some low efficiency.


You can look down at the depth of the entire Build traversal. If the tree is too deep, there may be a problem. At this time, you should check whether unnecessary parts have been updated.


However, it should be noted that for global topics, text, etc., updates will inevitably be traversed from the top node. This is unavoidable. Although it will cause a certain delay, these are visually insensitive operations and the number of operations is not very frequent. But in terms of animation, it is different. After a few frames are dropped, it will feel stuck and not smooth. On the other hand, the animation will continue to be rendered continuously for a period of time, so pay special attention to performance issues. In addition, don't look at performance in debug mode, don't look at performance in debug mode, don't look at performance in debug mode! Use profile mode.


2. Negative teaching materials! ! !

The animation is as follows, the circular gradient in the middle expands the animation, and the upper and lower squares do not move.

Program entry

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: HomePage());
  }
}

Mix _HomePageState into SingleTickerProviderStateMixin, create an animator controller, monitor animators, and call the setState method of _HomePageState every time it is triggered to update the Element held in _HomePageState. The animation is triggered when the middle is clicked.

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin {
  AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
        lowerBound: 0.3,
        upperBound: 1.0,
        vsync: this,
        duration: const Duration(milliseconds: 500));
    controller.addListener(() {
      setState(() {});
    });
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    print('---------_HomePageState#build------');
    return Scaffold(
        appBar: AppBar(
          title: Text("动画测试"),
        ),
        body: Column(
          children: [
            Expanded(
              child: Padding( padding: EdgeInsets.only(top: 20),
                child: buildBoxes(),
              ),
            ),
            Expanded(
              child: Center(
                child: buildCenter(),
              ),
            ),
            Expanded(
              child: Padding( padding: EdgeInsets.only(bottom: 20),
                child: buildBoxes(),
              ),
            ),
          ],
        ));
  }

  Widget buildCenter() => GestureDetector(
                onTap: () {
                  controller.forward(from: 0.3);
                },
                child: Transform.scale(
                  scale: controller.value,
                  child: Opacity(opacity: controller.value, child: Shower()),
                ),
              );

  Widget buildBoxes() => Wrap(
        spacing: 20,
        runSpacing: 20,
        children: List.generate( 24,
            (index) => Container(
                  alignment: Alignment.center,
                  width: 40,
                  height: 40,
                  color: Colors.orange,
                  child: Text('$index',style: TextStyle(color: Colors.white),),
                )),
      );
}

In order to facilitate the test, the intermediate components are separated into Shower. Use StatefulWidget to test the _ShowerState callback function during animation execution.

class Shower extends StatefulWidget {
  @override
  _ShowerState createState() => _ShowerState();
}

class _ShowerState extends State<Shower> {
  @override
  void initState() {
    super.initState();
    print('-----Shower#initState----------');
  }

  @override
  Widget build(BuildContext context) {
    print('-----Shower#build----------');
    return Container(
      width: 150,
      height: 150,
      alignment: Alignment.center,
      decoration: BoxDecoration(color: Colors.orange, shape: BoxShape.circle),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              Container(
                height: 30,
                width: 30,
                decoration:
                    BoxDecoration(color: Colors.white, shape: BoxShape.circle),
              ),
              Container(
                height: 30,
                width: 30,
                decoration:
                    BoxDecoration(color: Colors.white, shape: BoxShape.circle),
              )
            ],
          ),
          Text(
            'Toly',
            style: TextStyle(fontSize: 40, color: Colors.white),
          ),
        ],
      ),
    );
  }
}

Then you will find that _HomePageState#build and Shower#build will continue to trigger. The root cause is that setState is performed at a higher level, which causes the tree to be traversed. In this case, it is not advisable to perform animation. What we need to do is to lower the level of update element nodes. Flutter provides us with AnimatedBuilder.


3. Positive textbook AnimatedBuilder

Changes that need to be made: 1. Remove the monitor animator 2. Use AnimatedBuilder

@override
void initState() {
  super.initState();
  controller = AnimationController(
      vsync: this,
      lowerBound: 0.3,
      upperBound: 1.0,
      duration: const Duration(milliseconds: 500)); // 1、移除监听动画器
}

Widget buildCenter() => GestureDetector(
  onTap: () {
    controller.forward(from: 0);
  },
  child: AnimatedBuilder( //  2、使用 AnimatedBuilder
      animation: controller,
      builder: (ctx, child) {
        return Transform.scale(
          scale: controller.value,
          child: Opacity(opacity: controller.value, child: child),
        );
      },
      child: Shower()),
);

That's it, let's take a look at the effect, the animation is executed normally

There is nothing on the console. Is that too much? Isn't this slapping my setState in the face?

As can be seen from the UI frame below, in the same scenario, using AnimatedBuilder for animation can effectively shorten the Build process.


4. AnimatedBuilder source code analysis

First of all, AnimatedBuilder inherits from AnimatedWidget, and its members include builder builder and child component. The listenable object animation is also required when the object is created.

class AnimatedBuilder extends AnimatedWidget {
  const AnimatedBuilder({
    Key key,
    @required Listenable animation,
    @required this.builder,
    this.child,
  }) : assert(animation != null),
       assert(builder != null),
       super(key: key, listenable: animation);

  final TransitionBuilder builder;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    return builder(context, child);
  }
}

typedef TransitionBuilder = Widget Function(BuildContext context, Widget child);

AnimatedBuilder is very simple, the core of use should be in AnimatedWidget. It can be seen that AnimatedWidget is a StatefulWidget that needs to change state.

abstract class AnimatedWidget extends StatefulWidget {
  const AnimatedWidget({
    Key key,
    @required this.listenable,
  }) : assert(listenable != null),
       super(key: key);

  final Listenable listenable;

  @protected
  Widget build(BuildContext context);

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

The processing in _AnimatedState is also very simple, monitor the incoming listenable, execute _handleChange, and _handleChange executes..., yes: your uncle is still your uncle after all. Update still depends on setState. But compared to the setState above, the impact of setState here is much smaller.

class _AnimatedState extends State<AnimatedWidget> {
  @override
  void initState() {
    super.initState();
    widget.listenable.addListener(_handleChange);
  }

  @override
  void didUpdateWidget(AnimatedWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.listenable != oldWidget.listenable) {
      oldWidget.listenable.removeListener(_handleChange);
      widget.listenable.addListener(_handleChange);
    }
  }

  @override
  void dispose() {
    widget.listenable.removeListener(_handleChange);
    super.dispose();
  }

  void _handleChange() {
    setState(() {
      // The listenable's state is our build state, and it changed already.
    });
  }

  @override
  Widget build(BuildContext context) => widget.build(context);
}

When building is executed, widget.build(context) is executed, that is, the current context is called back to the widget.build method, and the widget.build method executes: builder (context, child), which is the builder we wrote (Figure below), it can be seen that the child callback is still the incoming child, so that a new Shower component will not be built, nor will it trigger the build method of the Shower component corresponding to the State. All animation needs are performed in the builder method , The refreshed things are also partially wrapped by AnimatedBuilder. In this way, the years are quiet and calm.

@override
Widget build(BuildContext context) {
  return builder(context, child);
}


From this point of view, AnimatedBuilder does not seem to be mysterious. After you understand these, and then look at the various animation components encapsulated in the Flutter framework, you will suddenly be enlightened. This is what you know.

To sum up, it's not that setState is bad, but the timing is right. AnimatedBuilder essentially uses setState to trigger updates, so don't look at the problem one-sidedly and aggressively. For the application interface UI, what we need to pay attention to is how to minimize the consumption of the Build process, especially for scenes that continuously update rendering such as animation and sliding.


5. Finally

Finally, here I also share a piece of dry goods, the Android learning PDF + architecture video + source notes collected by the big guys , as well as advanced architecture technology advanced brain maps, Android development interview special materials, advanced advanced architecture materials to help you learn Improve the advanced level, and save everyone's time to search for information on the Internet to learn, and you can also share with friends around you to learn together.

Guess you like

Origin blog.csdn.net/ajsliu1233/article/details/111396263