skip to Main Content

I’m having an issue with my widget running its FutureBuilder code multiple times with an already resolved Future. Unlike the other questions on SO about this, my build() method isn’t being called multiple times.

My future is being called outside of build() in initState() – it’s also wrapped in an AsyncMemoizer.

Relevant code:

class _HomeScreenState extends State<HomeScreen> {
  late final Future myFuture;
  final AsyncMemoizer _memoizer = AsyncMemoizer();

  @override
  void initState() {
    super.initState();

    /// provider package
    final homeService = context.read<HomeService>();
    myFuture = _memoizer.runOnce(homeService.getMyData);
  }

  @override
  Widget build(BuildContext context) {
    print("[HOME] BUILDING OUR HOME SCREEN");

    return FutureBuilder(
      future: myFuture,
      builder: ((context, snapshot) {
        print("[HOME] BUILDER CALLED WITH SNAPSHOT: $snapshot - connection state: ${snapshot.connectionState}");

When I run the code, and trigger the bug (a soft keyboard being shown manages to trigger it 50% of the time, but not all the time), my logs are:

I/flutter (29283): [HOME] BUILDING OUR HOME SCREEN
I/flutter (29283): [HOME] BUILDER CALLED WITH SNAPSHOT: AsyncSnapshot<dynamic>(ConnectionState.waiting, null, null, null) - connection state: ConnectionState.waiting
I/flutter (29283): [HOME] BUILDER CALLED WITH SNAPSHOT: AsyncSnapshot<dynamic>(ConnectionState.done, Instance of 'HomeData', null, null) - connection state: ConnectionState.done
...
/// bug triggered
...
I/flutter (29283): [HOME] BUILDER CALLED WITH SNAPSHOT: AsyncSnapshot<dynamic>(ConnectionState.done, Instance of 'HomeData', null, null) - connection state: ConnectionState.done

The initial call with ConnectionState.waiting is normal, then we get the first build with ConnectionState.done.

After the bug is triggered, I end up with another FutureBuilder resolve without the build() method being called.

Am I missing something here?

Edit with full example

This shows the bug in question – if you click in and out of the TextField, the FutureBuilder is called again.

It seems related to how the keyboard is hidden. If I use the FocusScopeNode method, it will rebuild, whereas if I use FocusManager, it won’t, so I’m not sure if this is a bug or not.

import 'package:flutter/material.dart';

void main() async {
  runApp(const TestApp());
}

class TestApp extends StatelessWidget {
  const TestApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Testapp',
      home: Scaffold(
        body: TestAppHomeScreen(),
      ),
    );
  }
}

class TestAppHomeScreen extends StatefulWidget {
  const TestAppHomeScreen({super.key});

  @override
  State<TestAppHomeScreen> createState() => _TestAppHomeScreenState();
}

class _TestAppHomeScreenState extends State<TestAppHomeScreen> {
  late final Future myFuture;

  @override
  void initState() {
    super.initState();

    myFuture = Future.delayed(const Duration(milliseconds: 500), () => true);

    print("[HOME] HOME SCREEN INIT STATE CALLED: $hashCode");
  }

  @override
  Widget build(BuildContext context) {
    print("[HOME] HOME SCREEN BUILD CALLED: $hashCode");
    return FutureBuilder(
      future: myFuture,
      builder: (context, snapshot) {
        print("[HOME] HOME SCREEN FUTURE BUILDER CALLED WITH STATE ${snapshot.connectionState}: $hashCode");
        if (snapshot.connectionState == ConnectionState.waiting) {
          return const Scaffold(
            body: Center(
              child: CircularProgressIndicator(),
            ),
          );
        }

        return GestureDetector(
          onTapUp: (details) {
            // hide the keyboard if it's showing
            FocusScopeNode currentFocus = FocusScope.of(context);
            if (!currentFocus.hasPrimaryFocus) {
              currentFocus.unfocus();
            }
            // FocusManager.instance.primaryFocus?.unfocus();
          },
          child: const Scaffold(
            body: Center(
              child: Padding(
                padding: EdgeInsets.symmetric(horizontal: 32.0),
                child: TextField(),
              ),
            ),
          ),
        );
      },
    );
  }
}

5

Answers


  1. Please try this solution /// provider package up super.initState();

    your code will be like this

      @override
      void initState() {
        /// provider package
        final homeService = context.read<HomeService>();
        myFuture = _memoizer.runOnce(homeService.getMyData);
        super.initState();
      }
    

    please after trying it tell me the result

    Login or Signup to reply.
  2. pass descendant context to FocusScope.of will not trigger the build(), i think because focus manager remove child for this parent (FutureBuilder), and reassign it based on current context, in this case build() context, so futurebuilder need to rebuild.

    Widget build(BuildContext context) {
        print("[HOME] HOME SCREEN BUILD CALLED: $hashCode");
        return FutureBuilder(
          future: myFuture,
          builder: (context, snapshot) {
            print("[HOME] HOME SCREEN FUTURE BUILDER CALLED WITH STATE ${snapshot.connectionState}: $hashCode");
            if (snapshot.connectionState == ConnectionState.waiting) {
              return const Scaffold(
                body: Center(
                  child: CircularProgressIndicator(),
                ),
              );
            }
            //make StatefulBuilder as parent will prevent it
            return StatefulBuilder(
              builder: (context, setState) {
                return GestureDetector(
                  onTapUp: (details) {
                    // hide the keyboard if it's showing
                    FocusScopeNode currentFocus = FocusScope.of(context);
                    if (!currentFocus.hasPrimaryFocus) {
                      currentFocus.unfocus();
                    }
                    // FocusManager.instance.primaryFocus?.unfocus();
                  },
                  child: const Scaffold(
                    body: Center(
                      child: Padding(
                        padding: EdgeInsets.symmetric(horizontal: 32.0),
                        child: TextField(),
                      ),
                    ),
                  ),
                );
              }
            );
          },
        );
      }
    

    to prove it , i try to warp it parent (FutureBuilder) with another builder :

    return LayoutBuilder(
          builder: (context, box) {
            print('Rebuild');
            return FutureBuilder(
              future: myFuture,
              builder: (context, snapshot) {
                print("[HOME] HOME SCREEN FUTURE BUILDER CALLED WITH STATE ${snapshot.connectionState}: $hashCode");
                if (snapshot.connectionState == ConnectionState.waiting) {
                  return const Scaffold(
                    body: Center(
                      child: CircularProgressIndicator(),
                    ),
                  );
                }
    
                return GestureDetector(
                  onTapUp: (details) {
                    // hide the keyboard if it's showing
                    FocusScopeNode currentFocus = FocusScope.of(context);
                    if (!currentFocus.hasPrimaryFocus) {
                      currentFocus.unfocus();
                    }
                    // FocusManager.instance.primaryFocus?.unfocus();
                  },
                  child: const Scaffold(
                    body: Center(
                      child: Padding(
                        padding: EdgeInsets.symmetric(horizontal: 32.0),
                        child: TextField(),
                      ),
                    ),
                  ),
                );
    
    
              },
            );
          }
        );
    

    build() method not reinvoked because focusScope manager only rebuild context from FutureBuilder (Parent)

    Login or Signup to reply.
  3. Thank you for the full, reproducible example.

    print statements inside the builder method of your FutureBuilder are likely misleading you towards the incorrect "culprit".

    The key "problem" arises from this line:

    FocusScopeNode currentFocus = FocusScope.of(context);
    

    In case you didn’t know, Flutter’s .of static methods expose InheritedWidget APIs of some kind. By convention, in a .of method you can usually find a call to dependOnInheritedWidgetOfExactType, which is meant to register the caller, i.e. the children Widget, as a dependency, i.e. a Widget that depends and react to changes of a InheritedWidget of that type.

    Shortly, putting a .of inside a build method is meant to trigger rebuilds on your Widget: it’s actively registered for listening to changes!

    In your code, FutureBuilder‘s builder method is being registered as dependant of FocusScope.of and will be rebuilt if FocusScope changes. And yes, that does happen whenever we change focus. Indeed, you can even move up those few lines (outside GestureDetector, directly in the builder scope), and you’d obtain even more rebuilds (4: one for the first focus change, then others subsequent caused by the focus shift caused by such rebuilds).

    One quick fix would be to directly look for the associated InheritedWidget these API expose, and then, instead of a simple .of, you’d call:

    context.getElementForInheritedWidgetOfExactType<T>();
    

    EDIT. I just looked for T in your use case. Unluckily, it turns out it is a _FocusMarker extends InheritedWidget class, which is a private class, and therefore it cannot be used outside of its file / package. I’m not sure why they designed the API like that, but I am not familiar with FocusNodes.

    An alternative approach would be to simply isolate the children for your FutureBuilder, like so:

    builder: (context, snapshot) {
      print("[HOME] HOME SCREEN FUTURE BUILDER CALLED WITH STATE ${snapshot.connectionState}: $hashCode");
      // ...
      return Something();
    }
    

    Where Something is just the refactored StatelessWidget that contains the UI you’ve shown there. This would rebuild just Something and not the whole builder method, if that’s your concern.

    You want to deepen the "how" and the "whys" of InheritedWidgets, make sure you first watch this video to correctly understand what InheritedWidgets are. Then, if you wish to understand how to exploit didChangeDependencies, watch this other video and you’ll be good to go.

    Login or Signup to reply.
  4. You need to understand the role of BuildContext.

    Example-1:

    I’m using context passed to the Widget.build() method, and doing

    FocusScope.of(context).unfocus();
    

    will invoke both build() and builder() method because you’re telling Flutter to take the focus away from any widget within the context and therefore the Widget.build() gets called, which further calls the Builder.builder() method.

    // Example-1
    @override
    Widget build(BuildContext context) {
      print("Widget.build()");
    
      return Builder(builder: (context2) {
        print('Builder.builder()');
        return GestureDetector(
          onTap: () => FocusScope.of(context).unfocus(), // <-- Using `context`
          child: Scaffold(
            body: Center(
              child: TextField(),
            ),
          ),
        );
      });
    }
    

    Example-2:

    I’m using context2 passed to the Builder.builder() method, and doing

    FocusScope.of(context2).unfocus();
    

    will invoke only the builder() method because you’re telling Flutter to take the focus away from any widget within the context2 and thus the Builder.builder() gets called.

    // Example-2
    @override
    Widget build(BuildContext context) {
      print("Widget.build()");
    
      return Builder(builder: (context2) {
        print('Builder.builder()');
        return GestureDetector(
          onTap: () => FocusScope.of(context2).unfocus(), // <-- Using `context2`
          child: Scaffold(
            body: Center(
              child: TextField(),
            ),
          ),
        );
      });
    }
    

    To answer your question, if you replace

    builder: (context, snapshot) { ...}
    

    with

    builder: (_, snapshot) { }
    

    then your build() will also get called.

    Login or Signup to reply.
    • The difference was happen because the context you use is parent
      context (from future builder method).
      Just wrap GestureDetector with Builder then the result is same as 2nd way.

           return Builder(builder: (_context) {
             return GestureDetector(
               onTapUp: () {
                 // hide the keyboard if it's showing
                 final currentFocus = FocusScope.of(_context);
                 if (!currentFocus.hasPrimaryFocus) {
                   currentFocus.unfocus();
                 },
               } ...
      
    • When attempting to dismiss keyboard we should use second way FocusManager.instance.primaryFocus?.unfocus(); as discussion in official issue here:
      https://github.com/flutter/flutter/issues/20227#issuecomment-512860882
      https://github.com/flutter/flutter/issues/19552

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search