102

I have a listView in my screen. I have attached a controller to it. I am able to call my Endpoint, receive response, parse it and insert in row. ListView supposed to Scroll automatically. It does, but not in perfect way. I am always an item behind. This is my code:

@override
  Widget build(BuildContext context) {
    // Scroll to the most recent item
    if (equationList.length > 0) {
      _toEnd();
    }

    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: EquList(equationList, _scrollController),
      floatingActionButton: new FloatingActionButton(
        onPressed: onFabClick,
        tooltip: 'Fetch Post',
        child: new Icon(isLoading ? Icons.pause : Icons.play_arrow),
      ),
    );
  }

  void _toEnd() {
    _scrollController.animateTo(
      _scrollController.position.maxScrollExtent,
      duration: const Duration(milliseconds: 250),
      curve: Curves.ease,
    );
  }

The problem is, I am calling _toEnd() function before the last item inserts in to the list. So, I am looking for a callback (if there is any) that tells me build() is done. Then I call my _toEnd() function.

What is the best practice in this case?

halfer
  • 19,824
  • 17
  • 99
  • 186
Hesam
  • 52,260
  • 74
  • 224
  • 365
  • 1
    Note we prefer a technical style of writing here. We gently discourage greetings, hope-you-can-helps, thanks, advance thanks, notes of appreciation, regards, kind regards, signatures, please-can-you-helps, chatty material and abbreviated txtspk, pleading, how long you've been stuck, voting advice, meta commentary, etc. Just explain your problem, and show what you've tried, what you expected, and what actually happened. – halfer Jul 07 '18 at 11:06
  • A better way would be to set `reverse: true` in the `ListView` or scroll view if you use one and pass the content in reverse order. – Günter Zöchbauer Jul 09 '18 at 17:47
  • @GünterZöchbauer, I am doing so. But the thing is, I still need a callback to tell me the build is done. – Hesam Jul 09 '18 at 17:53
  • 6
    What do you need it for? `build()` is sync, so if you execute something async in `build()` it will be executed after `build()` is done. – Günter Zöchbauer Jul 10 '18 at 02:49
  • Thanks @GünterZöchbauer, Is that possible to put your suggestion as answer with an example? I am very new in Flutter and I afraid I did not get you well. Thanks. – Hesam Jul 10 '18 at 04:25
  • https://pub.dartlang.org/packages/after_layout might also be of interest – Günter Zöchbauer Jul 12 '18 at 17:18

3 Answers3

187

General solution

Just to clear things up, I did not expect this question to attract so much attention. Hence, I only answered for this very specific case.
As explained in another answer WidgetsBinding offers a way to add a one time post frame callback.

WidgetsBinding.instance!.addPostFrameCallback((_) {
  // executes after build
})

As this callback will only be called a single time, you will want to add it every time you build:

@override
Widget build(BuildContext context) {
  WidgetsBinding.instance!.addPostFrameCallback((_) => afterBuild);
  return Container(); // widget tree
}

void afterBuild() {
  // executes after build is done
}

Specific (async)

Elaborating on Günter's comment:

@override
Widget build(BuildContext context) {
  executeAfterBuild();
  return Container();
}

Future<void> executeAfterBuild() async {
  await Future.delayed(Duration.zero);
  // this code will get executed after the build method
  // because of the way async functions are scheduled
}

There is a nice example illustrating that effect here.
Extensive information about scheduling in Dart can be found here.

Red Sun
  • 131
  • 10
creativecreatorormaybenot
  • 114,516
  • 58
  • 291
  • 402
  • 6
    I didn't know async will be run after sync function is done. It works like charme. Thanks for your sample code. – Hesam Jul 10 '18 at 21:59
  • 1
    @creativecreatorormaybenot I don't think that just by marking a function asynchronous (using `async` keyword), the code inside it will be scheduled on event queue. You should rather use `Future(() {})` or some other constructor form of it. – CopsOnRoad Sep 26 '20 at 10:57
  • 1
    The code inside the async function cannot be just any random code. We have to make to sure that we return an Future object and then write the code which we want to actually execute. For example: Future executeAfterBuild() async {await Future.delayed(Duration(milliseconds:10)); )// code which to execute }. When this async function will be called, we will get a Future object in incomplete state since we called this async function without doing await. – Sumit Trehan Jan 26 '21 at 11:52
60

The async way from @creativecreatorormaybenot is enough to answer the question for most situations.

But if you want to setState() or do something that will change widgets in the tree right after building the widget, you cannot use the async way. Because the callback will be fired during the build process of the widget tree. It will throw an exception:

Dart Error: Unhandled exception:
E/flutter (25259): setState() or markNeedsBuild() called during build.

For this situation, you can register a post frame callback to modify the widget:

@override
Widget build(BuildContext context) {
  WidgetsBinding.instance
    .addPostFrameCallback((_) => executeAfterWholeBuildProcess(context));
My Car
  • 4,198
  • 5
  • 17
  • 50
RockingDice
  • 1,909
  • 2
  • 14
  • 22
14

If you don't want to use WidgetsBinding:

  • Use Future or Timer

    @override
    Widget build(BuildContext context) {
      Timer(_runsAfterBuild); // <-- Just add this line 
      return Container();
    }
    
    Future<void> _runsAfterBuild() async {
      // This code runs after build ...
    }
    
  • Add a dummy wait (fixes @creativecreatureormaybenot problem)

    @override
    Widget build(BuildContext context) {
      _runsAfterBuild();
      return Container();
    }
    
    Future<void> _runsAfterBuild() async {
      await Future.delayed(Duration.zero); // <-- Add a 0 dummy waiting time
      // This code runs after build ...
    }
    
CopsOnRoad
  • 237,138
  • 77
  • 654
  • 440