2
votes

So I was trying this code in flutter:

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

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  int _i = 1;
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: MaterialButton(
            child: Text('You Pressed Me $_i'),
            onPressed: () {
              setState(() {
                _i++;
                print('inside i = $_i');
              });
              sleep(Duration(seconds: 10));
              _i++;
              print('outside i = $_i');
            }
          ),
        ),
      ),
    );
  }
}

Expected behavior(after running and pressing the button once): the button shows text "You Pressed Me 2",

and the variable _i then gets incremented to 3 without affecting the visual result.

Actual behavior: setState() is execued and then _i is incremented again and no visual change happens i.e. the text on screen doesn't update, and when onPressed() returns, setState() causes the widget to rebuild and the screen updates and this is what appears on screen after 10 seconds: "You Pressed Me 3".

There is this quote from the docs about setState():

The provided callback is immediately called synchronously. It must not return a future (the callback cannot be async), since then it would be unclear when the state was actually being set.

I understand that the call is synchronous (so it's blocking, according to this answer) so it should return first (and this already happens) and then update the screen (or schedule that for some time in the future) and then return control to the following line (the latter 2 things don't happen).

I even tried it without the sleep but same result.

so what am I missing or misunderstanding?

2
So you are saying that lastly 3 is printed on the visual screen?Vidor Vistrom
yes but the problem is that "2" should appear on the screen because when _i becomes 3 the UI should have been updatedHaidar Mehsen

2 Answers

5
votes

Event Loop

  • There is something called Event Loop

  • Event Loop process Events in order

You have two events in order

Event A => Click => by the User

    0.onPressed: () {
        1.setState(() {
            3. i++
            4. Mark as widget dirty
            5. Add to the global dirty widgets list
        });
        6.i++
    });

Event B => Vsync signal => provided by the OS

    7. check dirty widgets list
    8. repaint

More

Ref.:

  1. setState method - State class - widgets library - Dart API

  2. markNeedsBuild method - Element class - widgets library - Dart API

  3. scheduleBuildFor method - BuildOwner class - widgets library - Dart API

  4. drawFrame method - WidgetsBinding class - widgets library - Dart API

  5. handleDrawFrame method - SchedulerBinding class - scheduler library - Dart API

  6. buildScope method - BuildOwner class - widgets library - Dart API

  7. dart engine loop - Google Search

  8. Dart Programming - Loops - Tutorialspoint

  9. optimization - What is the optimal render loop in Dart 2? - Stack Overflow

  10. Understanding Flutter Render Engine - Stack Overflow

  11. Technical overview - Flutter

  12. Flutter - Dart API docs

  13. flutter/spinning_square.dart at master · flutter/flutter

14 .dart engine - Google Search

  1. scheduler library - Dart API

  2. flutter/binding.dart at master · flutter/flutter

  3. scheduler library - Dart API

  4. frame scheduling flutter - Google Search

  5. scheduleFrame method - SchedulerBinding class - scheduler library - Dart API

  6. scheduler library - Dart API

  7. packages/flutter/lib/scheduler.dart - external/github.com/flutter/flutter - Git at Google

  8. flutter/spinning_square.dart at master · flutter/flutter

  9. dart engine - Google Search

  10. threading | Dart Package

  11. isolate flutter - Google Search

enter image description here

1
votes

If you want to run code after the widget is rebuild use:

WidgetsBinding.instance.addPostFrameCallback((_) {
//doStuffAfterNextBuild
});

The function passed to setState is run directly.

"The provided callback is immediately called synchronously"

setState only marks the widget as dirty.

"Calling setState notifies the framework that the internal state of this object has changed"

setState does not itself manipulate your state in any way.

(Quotes from https://api.flutter.dev/flutter/widgets/State/setState.html)


See for example the following:

class _MyWidgetState extends State<MyWidget> {
  String s = ""

  // ... Somewhere in the tree
  onPressed: () {
    // State is updated immediately with this call
    setState(() {
      this.s = "1";
      // Implementation of setState calls
      // _element.markNeedsBuild();
    });

    // State is updated again
    this.s = "2";
  }
}

This will result in the states s beeing "2". Although the closure passed to setState sets s to "1", it is again modified after that in the closure of onPressed.

You can actually update the state outside of setStates closure entirely:

class _MyWidgetState extends State<MyWidget> {
  String s = ""

  // ... Somewhere in the tree
  onPressed: () {
    // State is updated immediately
    this.s = "1";
    setState(() {
      // Implementation of setState calls
      // _element.markNeedsBuild();
    });
  }
}


If you look at the implementation of setState you can see that it does not actually do anything with the callback besides of executing it and making sure you did not give it an asynchronous function and that the widget still exists:

@protected
void setState(VoidCallback fn) {
  assert(fn != null);
  assert(() {
    if (_debugLifecycleState == _StateLifecycle.defunct) {
      throw FlutterError.fromParts(<DiagnosticsNode>[
        ErrorSummary('setState() called after dispose(): $this'),
        ErrorDescription(
          'This error happens if you call setState() on a State object for a widget that '
          'no longer appears in the widget tree (e.g., whose parent widget no longer '
          'includes the widget in its build). This error can occur when code calls '
          'setState() from a timer or an animation callback.'
        ),
        ErrorHint(
          'The preferred solution is '
          'to cancel the timer or stop listening to the animation in the dispose() '
          'callback. Another solution is to check the "mounted" property of this '
          'object before calling setState() to ensure the object is still in the '
          'tree.'
        ),
        ErrorHint(
          'This error might indicate a memory leak if setState() is being called '
          'because another object is retaining a reference to this State object '
          'after it has been removed from the tree. To avoid memory leaks, '
          'consider breaking the reference to this object during dispose().'
        ),
      ]);
    }
    if (_debugLifecycleState == _StateLifecycle.created && !mounted) {
      throw FlutterError.fromParts(<DiagnosticsNode>[
        ErrorSummary('setState() called in constructor: $this'),
        ErrorHint(
          'This happens when you call setState() on a State object for a widget that '
          "hasn't been inserted into the widget tree yet. It is not necessary to call "
          'setState() in the constructor, since the state is already assumed to be dirty '
          'when it is initially created.'
        ),
      ]);
    }
    return true;
  }());
  final dynamic result = fn() as dynamic;
  assert(() {
    if (result is Future) {
      throw FlutterError.fromParts(<DiagnosticsNode>[
        ErrorSummary('setState() callback argument returned a Future.'),
        ErrorDescription(
          'The setState() method on $this was called with a closure or method that '
          'returned a Future. Maybe it is marked as "async".'
        ),
        ErrorHint(
          'Instead of performing asynchronous work inside a call to setState(), first '
          'execute the work (without updating the widget state), and then synchronously '
         'update the state inside a call to setState().'
        ),
      ]);
    }
    // We ignore other types of return values so that you can do things like:
    //   setState(() => x = 3);
    return true;
  }());
  _element.markNeedsBuild();
}

(Also from https://api.flutter.dev/flutter/widgets/State/setState.html)


Check out this great video for more insight into how State works in Flutter: https://www.youtube.com/watch?v=dkyY9WCGMi0





P.S.

Just for fun you could also handle the creation of the element for the StatefulWidget your self and mark it manually. Although you definitely should not

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage();

  late final StatefulElement element;

  @override
  _MyHomePageState createState() => _MyHomePageState();
  
  @override
  StatefulElement createElement() { 
    this.element = StatefulElement(this); 
    return element;
  }
}

class _MyHomePageState extends State<MyHomePage> {
  String s = "";


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text(
          '$s',
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          this.s = "2";
          widget.element.markNeedsBuild();
        },
        child: Icon(Icons.add),
      ),
    );
  }
}