Reputation: 8033
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(),
),
),
),
);
},
);
}
}
Upvotes: 0
Views: 901
Reputation: 2087
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 FocusNode
s.
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 InheritedWidget
s, make sure you first watch this video to correctly understand what InheritedWidget
s are. Then, if you wish to understand how to exploit didChangeDependencies
, watch this other video and you'll be good to go.
Upvotes: 1
Reputation: 1720
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
Upvotes: 1
Reputation: 267404
You need to understand the role of BuildContext
.
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(),
),
),
);
});
}
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.
Upvotes: 1
Reputation: 1551
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)
Upvotes: 0
Reputation: 247
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
Upvotes: 0