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(),
),
),
),
);
},
);
}
}
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.
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.
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
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
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)
I need to make the modal route animation come from the bottom when clicked, however the only animation that I managed to do is the fade away and the spin animation.
It is an overlay that is called when the user clicks a button to explain it.
Here is the build page
#override
Widget buildPage(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
return Material(
type: MaterialType.transparency,
// make sure that the overlay content is not cut off
child: SafeArea(
child: _buildOverlayContent(context),
),
);
}
and here is the build transitions:
#override
Widget buildTransitions(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
// You can add your own animations for the overlay content
return FadeTransition(
opacity: animation,
child: ScaleTransition(
scale: animation,
child: child,
),
);
}
I have tried to use slide transitions from several ways but it doesnt seem to work any time I try. It either leads to an error or simply doesnt work.
(One of my tries)
Animation<Offset> animated() {
Animation<Offset> anime;
return anime;
}
#override
Widget buildTransitions(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
//You can add your own animations for the overlay content
return SlideTransition(
position: animated(),
child: child,
);
}
What should I do?
You can use the page_transition package:
https://pub.dev/packages/page_transition
You can use the bottomToTop transition to achieve this.
Navigator.push(
context,
PageTransition(
type: PageTransitionType.bottomToTop,
child: DetailScreen()
)
);
This should achieve what you're looking for:
#override
Widget buildTransitions(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
return SlideTransition(
position: Tween(
begin: const Offset(0.0, 1.0),
end: Offset.zero,
).animate(animation),
child: child,
);
}
I have several register forms with next buttons and I would like to switch between the different screens with
onTap: () => { Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SomeOtherScaffold()))
},
but without the default animation that comes with it.
If you want to use other animation, you can use transitionBuilder, in your PageRoute. How you can use this represented for you below:
import 'package:flutter/material.dart';
main() {
runApp(MaterialApp(
home: Page1(),
));
}
class Page1 extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: RaisedButton(
child: Text('Go!'),
onPressed: () {
Navigator.of(context).push(_createRoute());
},
),
),
);
}
}
Route _createRoute() {
return PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => Page2(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return child;
},
);
}
And if you want to transition instantly, you can do this:
transitionDuration: Duration(seconds: 0)
This will decrease down the transition time to 0 second, which will eventually results to quick transition speed.
To know more about Page Animation Transition follow these articles:
Page Route Animation
Everything you need for Flutter Page Transition
You need to define something for your transition, if you don't want the default one, and this answer will help you achieve this.
I'm using OpenContainer animation to open a screen that could display alert dialog upon the opening of the screen - the case of the item the screen is trying to display is no longer valid or deleted.
Because OpenContainer renders the screen during the animation, the alert dialog is displayed several times.
My attempt to address the issue was to modify the OpenContainer buildPage method to return animation status to openBuilder callback. Is there better way to do without modifying OpenContainer code?
child: AnimatedBuilder(
animation: animation,
builder: (BuildContext context, Widget child) {
if (animation.isCompleted) {
return SizedBox.expand(
child: Material(
color: openColor,
elevation: openElevation,
shape: openShape,
child: Builder(
key: _openBuilderKey,
builder: (BuildContext context) {
return openBuilder(context, closeContainer, false); // added false
},
),
),
);
}
Code to reproduce the issue - https://gist.github.com/MartinJLee/0992a986ad641ef5b4f477fb1ce69249
You can add a listener to your AnimationController something like this
Consider you have an AnimationController like this -
AnimationController _animationController = AnimationController(
vsync: this,
duration: Duration(seconds: 5),
);
Then you can add a Status Listener to this _animationController using the addStatusListener method, something like this -
_animationController.addStatusListener((status) {
if(status == AnimationStatus.completed){
//Do something here
}
});
This listener will be called everytime the animation state changes.
Hope this helps!
I was able to do using the following code on the container for openBuilder.
void initState() {
super.initState();
WidgetsBinding.instance
.addPostFrameCallback((_) => yourFunction(context));
}
see original answers - Flutter: Run method on Widget build complete
Project
Hi,
I recently discovered flutter bloc and now I'm trying to understand how exactly this works. My goal is to separate logic from widget classes in order to easily manage my projects.
Problem
I'm stuck with something that is very simple using classic setState, but I was trying to achieve this with bloc.
Here is an old widget of mine
Widget build(BuildContext context) {
return AnimatedOpacity(
duration: Duration(milliseconds: 200),
opacity: _opacity,
curve: Curves.easeInOut,
child: Text(
_currentTitle,
style: TitleTextStyle,
),
);
}
Is it possible to control _opacity and _currentTitle from a bloc? Something like this:
List<String> titles = ['title1', 'title2', ....];
int myIndex;
#override
Stream<SomeBlocState> mapEventToState(SomeEvent event,)
async* {
....
if (event is SomeSpecificEvent)
setWidgetTitle(titles[myIndex]);
....
}
I am trying to avoid buiding different state for each possible title, that would be a mess
Thanks
You can try and make the title widget build in a StreamBuilder instead. That will give you a clean separation of responsibilities. So that the BLoC doesn't know of any state changes or widget specific things.
Widget build(BuildContext context) {
return AnimatedOpacity(
duration: Duration(milliseconds: 200),
opacity: _opacity,
curve: Curves.easeInOut,
child: StreamBuilder<String>(
stream: myBloc.title,
builder: (_, snap) {
return Text(
snap?.data ?? 'Some default',
style: TitleTextStyle,
);
}
),
);
}
And you create some logic in your BLoC that emits the title as needed
List<String> titles = ['title1', 'title2', ....];
int myIndex;
final _myObservableTitles = PublishSubject();
Stream<String> get title => _myObservableTitles.stream;
....
if (event is SomeSpecificEvent)
_myObservableTitles.add(titles[myIndex]);
....