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
Please try this solution
/// provider package
upsuper.initState();
your code will be like this
please after trying it tell me the result
pass descendant
context
toFocusScope.of
will not trigger thebuild()
, i think because focus manager remove child for this parent (FutureBuilder), and reassign it based on current context, in this casebuild()
context, so futurebuilder need to rebuild.to prove it , i try to warp it parent (
FutureBuilder
) with another builder :build()
method not reinvoked because focusScope manager only rebuild context fromFutureBuilder
(Parent)Thank you for the full, reproducible example.
print
statements inside thebuilder
method of yourFutureBuilder
are likely misleading you towards the incorrect "culprit".The key "problem" arises from this line:
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 todependOnInheritedWidgetOfExactType
, which is meant to register the caller, i.e. the childrenWidget
, as a dependency, i.e. aWidget
that depends and react to changes of aInheritedWidget
of that type.Shortly, putting a
.of
inside abuild
method is meant to trigger rebuilds on yourWidget
: it’s actively registered for listening to changes!In your code,
FutureBuilder
‘sbuilder
method is being registered as dependant ofFocusScope.of
and will be rebuilt ifFocusScope
changes. And yes, that does happen whenever we change focus. Indeed, you can even move up those few lines (outsideGestureDetector
, directly in thebuilder
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: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 withFocusNode
s.An alternative approach would be to simply isolate the children for your
FutureBuilder
, like so:Where
Something
is just the refactoredStatelessWidget
that contains the UI you’ve shown there. This would rebuild justSomething
and not the wholebuilder
method, if that’s your concern.You want to deepen the "how" and the "whys" of
InheritedWidget
s, make sure you first watch this video to correctly understand whatInheritedWidget
s are. Then, if you wish to understand how to exploitdidChangeDependencies
, watch this other video and you’ll be good to go.You need to understand the role of
BuildContext
.Example-1:
I’m using
context
passed to theWidget.build()
method, and doingwill invoke both
build()
andbuilder()
method because you’re telling Flutter to take the focus away from any widget within thecontext
and therefore theWidget.build()
gets called, which further calls theBuilder.builder()
method.Example-2:
I’m using
context2
passed to theBuilder.builder()
method, and doingwill invoke only the
builder()
method because you’re telling Flutter to take the focus away from any widget within thecontext2
and thus theBuilder.builder()
gets called.To answer your question, if you replace
with
then your
build()
will also get called.The difference was happen because the
context
you use is parentcontext (from future builder method).
Just wrap GestureDetector with Builder then the result is same as 2nd way.
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