Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flutter setState() doesn't always call my build method

Tags:

flutter

I'm trying out Flutter, but I'm having trouble getting the UI to update consistently. I'd like to show a status message while a long-running async method is called, but the setState() call I make just before calling the long-running method doesn't seem to cause my build() method to get invoked.

I've created a simple example that calculates the Fibonacci number for a randomly selected number between 25 and 30. In my sample code/app, hitting the "calc" button calls _calc(). _calc() picks a random number, sets a status message "Calculating Fib of $num..." tied to a text widget (_status) and updates it with setState(); then calls the async _fib() routine to calculate the number; then updates _status with the result using setState(). Additionally, the build() method prints the value of _status to the console, which can be used to see when build() is invoked.

In practice, when the button is pressed, the first status message does not appear either in the debug console, or on the UI. Doing a bit of experimentation, I added a pseudo sleep function that I call just prior to calling _fib(). This sometimes causes the first setState() call to work properly - invoking build(). The longer I make the sleep, the more often it works. (I'm using values from a few milliseconds up to a full second).

So my question are: What am I doing wrong? and What's the right way to do this? Using the pseudo sleep is obviously not the correct solution.

Other, probably not too relevant info: My dev environment is Android Studio 3.1.2 on a Win10 machine. Using Android SDK 27.0.3, with Flutter beta 0.3.2. My target device is the emulator for a pixel2 running Android 8.1. Also, sorry if my lack of 'new' keywords is off-putting, but from what I read in Dart 2 release notes, it's not usually necessary now.

import 'package:flutter/material.dart';
import "dart:async";
import "dart:math";

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Debug Toy',
      home: MyWidget(),
    );
  }
}

class MyWidget extends StatefulWidget {
  @override
  MyWidgetState createState() => MyWidgetState();
}

class MyWidgetState extends State<MyWidget> {
  String _status = "Initialized";
  final rand = Random();

  Future sleep1() async {
    return new Future.delayed(const Duration(milliseconds: 100),() => "1");
  }

  Future<Null> _resetState() async {
    setState(() { _status = "State Reset"; });
  }

  Future<Null> _calc() async {
    // calculate something that takes a while
    int num = 25 + rand.nextInt(5);
    setState(() { _status = "Calculating Fib of $num..."; });

    //await sleep1(); // without this, the status above does not appear
    int fn = await _fib(num);

    // update the display
    setState(() { _status = "Fib($num) = $fn"; });
  }

  Future<int> _fib(int n) async {
    if (n<=0) return 0;
    if ((n==1) || (n==2)) return 1;
    return await _fib(n-1) + await _fib(n-2);
  }

  @override
  Widget build(BuildContext context) {
    print("Build called with status: $_status");
    return Scaffold(
      appBar: AppBar(title: Text('Flutter Debug Toy')),
      body: Column(
        children: <Widget>[
          Container(
            child: Row(children: <Widget>[
              RaisedButton( child: Text("Reset"), onPressed: _resetState, ),
              RaisedButton( child: Text("Calc"), onPressed: _calc, )
            ]),
          ),
          Text(_status),
        ],
      ),
    );
  }
}
like image 802
Rich K Avatar asked May 17 '18 19:05

Rich K


Video Answer


1 Answers

Let's start by going to one extreme and rewriting fib as fibSync

  int fibSync(int n) {
    if (n <= 0) return 0;
    if (n == 1 || n == 2) return 1;
    return fibSync(n - 1) + fibSync(n - 2);
  }

and calling that

  Future<Null> _calc() async {
    // calculate something that takes a while
    int num = 25 + rand.nextInt(5);
    setState(() {
      _status = "Calculating Fib of $num...";
    });

    //await Future.delayed(Duration(milliseconds: 100));

    int fn = fibSync(num);

    // update the display
    setState(() {
      _status = "Fib($num) = $fn";
    });
  }

The first setState just marks the Widget as needing to be rebuilt and (without the 'sleep') continues straight into the calculation, never giving the framework the chance to rebuild the Widget, so the 'Calculating' message isn't displayed. The second setState is called after the calculation and once again (redundantly) marks the Widget as needing to be rebuilt.

So, the execution order is:

  1. Set status to Calculating, mark Widget as dirty
  2. Perform the synchronous calculation
  3. Set status to Result, mark Widget as dirty (redundantly)
  4. Framework finally gets chance to rebuild; build method is called

When we uncomment the 'sleep', the execution order changes to

  1. Set status to Calculating, mark Widget as dirty
  2. 'Sleep', allowing the framework to call build
  3. Perform the synchronous calculation
  4. Set status to Result, mark Widget as dirty (again)
  5. Framework calls build

(As an aside, note how the synchronous fib calculation is an order of magnitude faster because it doesn't have to do all the microtask scheduling.)

Let's re-consider the async calculation. What's the motivation of making it async? So that the UI remains responsive during the calculation? As you've seen, that doesn't achieve the desired effect. You still only have one thread of execution, and you aren't allowing any gaps in execution for callbacks and rendering to occur. Sleeping for 100ms is not compute bound, so drawing etc can occur.

We use async functions to wait for external events, like replies from web servers, where we don't have anything to do until the reply arrives, and we can use that time to keep rendering the display, reacting to gestures, etc.

For compute bound stuff, you need a second thread of execution which is achieved with an Isolate. An isolate has its own heap, so you have to pass it its data, it works away in its own space, then passes back some results. You can also stop it, if it's taking too long, or the user cancels, etc.

(There are much less computationally expensive ways to calculate fibs, but I guess we're using the recursive version as a good example of an O(n^2) function, right?)

like image 99
Richard Heap Avatar answered Nov 04 '22 23:11

Richard Heap