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)
Related
I implemented the alert dialog in the initstate() method but Init state is only called once. In my case I want the alert to appear automatically every time a variable value changes for exemple. ( I need it to suddenly pop up during using the app)
You could use a ValueNotifier and a ValueListenableBuilder so that every time the value in the ValueNotifier changes, the ValueListenableBuilder rebuilds and shows a dialog, like so:
class MyWidget extends StatelessWidget {
ValueNotifier<int> dialogTrigger = ValueNotifier(0);
#override
Widget build(BuildContext context) {
return Column(
children: [
TextButton(
onPressed: () {
var random = Random();
dialogTrigger.value = random.nextInt(100);
},
child: const Text('Click me and change a value')
),
Expanded(
child: ValueListenableBuilder(
valueListenable: dialogTrigger,
builder: (ctx, value, child) {
Future.delayed(const Duration(seconds: 0), () {
showDialog(
context: ctx,
builder: (ctx) {
return AlertDialog(
title: const Text('Dialog'),
content: Text('Hey! I got a $value'),
);
});
});
return const SizedBox();
})
)
]
);
}
}
(Again, I'm only adding a button to change the value, not to launch the dialog. That way you can programmatically change the value, which eventually launches the dialog). See if that works for your purposes.
Hello I am new to flutter and I have a problem to update the list after executing a deleting an item from the database. Some said to use setState, but I still don't know how to implement it in my code. Tried to call seState right after the delete action, but still nothing happened. Still have some trouble to understand which component to update in Flutter. Thank you.
class ProfileView extends StatefulWidget {
#override
State<StatefulWidget> createState() {
// TODO: implement createState
return _ProfileViewState();
}
}
class _ProfileViewState extends State<ProfileView> {
late Future<List<Patient>> _patients;
late PatientService patientService;
#override
void initState() {
super.initState();
patientService = PatientService();
_patients = patientService.getPatient();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Profile')),
body: Column(
children: <Widget>[
Flexible(
child: SizedBox(
child: FutureBuilder<List<Patient>>(
future: _patients,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if(snapshot.hasError) {
print(snapshot);
return Center(
child: Text("Error"),
);
} else if (snapshot.hasData){
List<Patient> patients = snapshot.data;
return _buildListView(patients);
} else {
return Center(
child: Container(),
);
}
},
),
),
)
],
),
);
}
Widget _buildListView(List<Patient> patients) {
return ListView.separated(
separatorBuilder: (BuildContext context, int i) => Divider(color: Colors.grey[400]),
itemCount: patients.length,
itemBuilder: (context, index) {
Patient patient = patients[index];
return Row(
children: <Widget>[
Flexible(
child: SizedBox(
child: ListTile(
title: Text(patient.firstName),
subtitle: Text(patient.phone),
trailing: IconButton(
icon: new Icon(const IconData(0xf4c4, fontFamily: 'Roboto'), size: 48.0, color: Colors.red),
onPressed: () {
patientService.deletePatient(patient.id.toString());
}),
),
)
)
],
);
}
);
}
}
You can achieve that by removing the initialization of the future from the initState and simply give the future of patientService.getPatient() to the FutureBuilder, like this:
future: patientService.getPatient()
And call setState after making sure the patient have been successfully deleted.
The explanation behind doing that is when you delete your patient from the DB, yes it is removed from there, but the UI didn't get an update about the list of patients after the delete. And the reason why calling setState in your case doesn't make a change is because you are assigning the future in initState which is called once and only once when the widget is initialized. So when you call setState the future won't be called again hence no new data is fetched.
So what I did is just remove the initialization of the future from initState and give it to the FutureBuilder, which will be rebuild whenever you call setState.
Even though this works, it isn't the ideal solution. Because you are rebuilding your whole widget every time a delete is made which is kinda of heavy considering the FutureBuilder, so what I suggest is checking out some state mangement solutions like Bloc or Mobx or even the Provider package (which isn't a state mangement according to its creator).
Hope that makes clear !
Happy coding !
call setState() inside the onPressed method.
onPressed: () {
patientService.deletePatient(patient.id.toString());
setState((){});
}),
If you are not saving a local copy of the list from which you are deleting an item then this works
Although if the delete method deletes on from wherever you are fetching the items then you will need to call
_patients = patientService.getPatient();
before calling setState()
I think your deletePatient is asynchronous method. And you are calling this method without await and after this function setState is called thus widget is getting updated before delete is completed.
If deletePatient is asynchronous then add await before calling it and add setState after it.
onPressed: () async {
await patientService.deletePatient(patient.id.toString());
setState((){});
})
I would like to know when to use a FutureBuilder and how is it usefull, given the fact that a widget can be built multiple times during its life time and not only when we setState or update it, so instead of using below code:
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool done = false;
#override
void initState() {
wait();
super.initState();
}
Future<void> wait() async {
await Future.delayed(Duration(seconds: 2));
setState(() {
done = true;
});
}
#override
Widget build(BuildContext context) {
print('is built');
return done ? Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
tooltip: 'Increment',
child: Icon(Icons.add),
),
) : Scaffold(body: CircularProgressIndicator(),);
}
}
In which cases would a FutureBuilder work for me instead of the above set up, given the fact that I would also want to optimize my app for less backend reads (in FutureBuilder I would read more than once). I am looking for a case where FutureBuilder would be more usefull and correct than the above setup.
FutureBuilder is used to handle "asynchronous" calls that return the data you want to display.
For example, let's say we have to get a List<String> from the server.
The API call :
Future<List<String>> getStringList() async {
try {
return Future.delayed(Duration(seconds: 1)).then(
(value) => ['data1', 'data2', 'data3', 'data4'],
);
} catch (e) {
throw Exception(e);
}
}
How can we handle the above API call status (load, data, error...etc) using FutureBuilder:
FutureBuilder<List<String>?>(
future: getStringList(),
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return Center(
child: CircularProgressIndicator(),
);
case ConnectionState.done:
if (snapshot.hasError)
return Text(snapshot.error.toString());
else
return ListView(
children: snapshot.data!.map((e) => Text(e)).toList(),
);
default:
return Text('Unhandle State');
}
},
),
As we can see, we don't have to create a state class with variable isLoading bool and String for error...etc. FutureBuilder saves us time.
BUT
Since FutureBuilder is a widget and lives in the widget tree, rebuilding can make FutureBuilder run again.
So I would say you can use FutureBuilder when you know there won't be a rebuild, however there are many ways (in the internet) to prevent FutureBuilder from being called again when the rebuild happens but it didn't work for me and leads to unexpected behavior.
Honestly I prefer handling the state in a different class with any state management solution than using FutureBuilder because it would be safer (rebuild wont effect it), more usable and easier to read (spreating business logic from UI).
FutureBuilder
Widget that builds itself based on the latest snapshot of interaction with a Future.
The future must have been obtained earlier, e.g. during State.initState, State.didUpdateWidget, or State.didChangeDependencies. It must not be created during the State.build or StatelessWidget.build method call when constructing the FutureBuilder.
If the future is created at the same time as the FutureBuilder, then every time the FutureBuilder's parent is rebuilt, the asynchronous task will be restarted.A general guideline is to assume that every build method could get called every frame, and to treat omitted calls as an optimization.
Documentation is very great way to get started and understand what widget does what in what condition...
https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html
Actually, you will never need to use FutureBuilder Widget if you don't want to. Your logic in your code do exactly what FutureBuilder Widget does if you optimise FutureBuilder Widget correctly.
This code is exactly same with yours:
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool done = false;
late Future myFuture;
#override
void initState() {
myFuture = wait();
super.initState();
}
Future<bool> wait() async {
await Future.delayed(const Duration(seconds: 2));
return true;
}
#override
Widget build(BuildContext context) {
print('is built');
return FutureBuilder(
future: myFuture,
builder: (BuildContext context, snapshot) {
if(snapshot.connectionState == ConnectionState.waiting) {
return const Scaffold(body: CircularProgressIndicator(),);
} else {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
);
}
}
FutureBuilder is just a StatefulWidget whose state variable is _snapshot
Initial state is _snapshot = AsyncSnapshot<T>.withData(ConnectionState.none, widget.initialData);
FutureBuilder is generally used to remove boilerplate code.
consider you want to fetch some data from the backend on page launch and show a loader until data comes.
It is subscribing to future which we send via the constructor and update the state based on that.
Tasks for FutureBuilder:
Give the async task in future of Future Builder
Based on connectionState, show message (loading, active(streams), done)
Based on data(snapshot.hasError), show view
Benefits of FutureBuilder
Does not use the two state variables and setState
Reactive programming (FutureBuilder will take care of updating the view on data arrival)
Example:
FutureBuilder<String>(
future: _fetchNetworkCall, // async work
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting: return Text('Loading....');
default:
if (snapshot.hasError)
return Text('Error: ${snapshot.error}');
else
return Text('Result: ${snapshot.data}');
}
},
)
hence to conclude Future builder is wrapper/boilerplate of what we do typically, thus there should not be any performance impact.
FutureBuilder is a widget by Flutter which lets you easily determine the current state of the Future and choose what to show during that state.
you can refer this: https://walkingtree.tech/futurebuilder-flutter-widget/
One of the use cases is FutureBuilder update on a particular section inside the widget tree, not the full widget tree like you implement in the sample.
Future<void> wait() async {
return Future.delayed(Duration(seconds: 2));
}
Here setState() => update widget tree.
So same code in FutureBuilder you can change the UI value in a particular position in the widget tree.
I have implemented the multiple Offstage Navigators for the bottomNavigationBar, further details can be seen here. The problem is that when using this method, each time we select a bottom navigation item, the FutureBuilder runs the future method and rebuilds the entire widget, each Offstage widget and all their children are also rebuilt.
For each Offstage widget, I'm loading data via html request and that means each time I switch a tab, 5 requests will be made.
This is my main Scaffold which holds the bottomNavigationBar.
#override
Widget build(BuildContext context) {
TabProvider tabProvider = Provider.of<TabProvider>(context);
return FutureBuilder(
future: initProvider(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return WillPopScope(
onWillPop: _onWillPop,
child: Scaffold(
appBar: AppBar(
title: Text(tabName[tabProvider.currentTab],
),
body: Stack(children: <Widget>[
_buildOffstageNavigator(TabItem.feed, tabProvider.currentTab),
_buildOffstageNavigator(TabItem.explore, tabProvider.currentTab),
_buildOffstageNavigator(TabItem.guide, tabProvider.currentTab),
_buildOffstageNavigator(TabItem.map, tabProvider.currentTab),
_buildOffstageNavigator(TabItem.profile, tabProvider.currentTab),
]),
bottomNavigationBar: BottomNavigation(
currentTab: tabProvider.currentTab,
onSelectTab: tabProvider.selectTab,
),
),
);
} else {
return Text('Loading');
}
},
);
}
The FutureBuilder will initialize the values in my provider so each tab can access the cached data.
The _buildOffstageNavigator will return the below
return Offstage(
offstage: currentTab != tabItem,
child: TabNavigator(
navigatorKey: navigatorKeys[tabItem],
tabItem: tabItem,
),
);
Below is the Widget which is built inside the Scaffold body and hence inside the Offstage Navigator from above.
#override
Widget build(BuildContext context) {
TabProvider tabProvider = Provider.of<TabProvider>(context);
States stateData = tabProvider.exploreStateCache;
return Container(
child: ListView(
children: <Widget>[
Text(stateData.stateName),
Text(stateData.stateDescription),
],
),
);
}
I have followed this articles advice for using futures with the provider but something else is missing
Instead of creating futures in a build method, which as you have noticed, may be called several times, create them in a place that is invoked only once. For example, a StatefulWidget's initState:
class Foo extends StatefulWidget {
#override
_FooState createState() => _FooState();
}
class _FooState extends State<Foo> {
Future<MyData> _dataFuture;
#override
void initState() {
super.initState();
_dataFuture = getData();
}
#override
Widget build(BuildContext context) => FutureBuilder<MyData>(
future: _dataFuture,
builder: (context, snapshot) => ...,
);
}
A second thing you can improve is reduce the scope of what gets rebuild when the provider provides a new value for TabProvider. The context that you call Provider.of<Data>(context) gets rebuild when there's a new value for Data. That is done most conveniently with the various other widgets offered by the provider package, like Consumer and Selector.
So remove the Provider<TabProvder>.of(context) calls and use Consumers and Selectors. For example, to only rebuild the title when a tab is switched:
AppBar(
title: Selector<TabProvider, String>(
selector: (context, tabProvider) => tabName[tabProvider.currentTab],
builder: (context, title) => Text(title),
),
)
Selector only rebuilds the Text(title) widget, when the result of its selector callback is different from the previous value. Similarly for _buildOffstageNavigator:
Widget _buildOffstageNavigator(BuildContext context, TabItem tabItem) {
return Selector<TabProvider, bool>(
selector: (context, tabProvider) => tabProvider.currentTab != tabItem,
builder: (context, isCurrent) => Offstage(
offstage: isCurrent,
child: Selector<TabProvider, Key>(
selector: (context, tabProvider) => tabProvider.navigatorKeys[tabItem],
builder: (context, tabKey) => TabNavigator(
navigatorKey: tabKey,
tabItem: tabItem,
),
),
);
}
(Beware: All code is untested and contains typos)
I have a FloatingActionButton inside a widget tree which has a BlocProvider from flutter_bloc. Something like this:
BlocProvider(
builder: (context) {
SomeBloc someBloc = SomeBloc();
someBloc.dispatch(SomeEvent());
return someBloc;
},
child: Scaffold(
body: ...
floatingActionButton: FloatingActionButton(
onPressed: _openFilterSchedule,
child: Icon(Icons.filter_list),
),
)
);
Which opens a modal bottom sheet:
void _openFilterSchedule() {
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) {
return TheBottomSheet();
},
);
}
I am trying to access SomeBloc using BlocProvider.of<SomeBloc>(context) inside TheBottomSheet but I get the following error:
BlocProvider.of() called with a context that does not contain a Bloc of type SomeBloc.
I have tried to use the solution described in https://stackoverflow.com/a/56533611/2457045 but only works for BottomSheet and not ModalBottomSheet.
Note: This is not restricted to BlocProvider or flutter_bloc. Any Provider from the provider package has the same behaviour.
How can I access BlocProvider.of<SomeBloc>(context) inside the showModalBottomSheet?
In case it's not possible to do that, how to adapt https://stackoverflow.com/a/56533611/2457045 solution to Modal Bottom Sheet?
InheritedWidgets, and therefore Providers, are scoped to the widget tree. They cannot be accessed outside of that tree.
The thing is, using showDialog and similar functions, the dialog is located in a different widget tree – which may not have access to the desired provider.
It is therefore necessary to add the desired providers in that new widget tree:
void myShowDialog() {
final myModel = Provider.of<MyModel>(context, listen: false);
showDialog(
context: context,
builder: (_) {
return Provider.value(value: myModel, child: SomeDialog());
},
);
}
Provider in showModalBottomSheet (Bottom-Sheet)
void myBottomSheet() {
final myModel = Provider.of<MyModel>(context, listen: false);
showModalBottomShee(
context: context,
builder: (_) {
return ListenableProvider.value(
value: myModel,
child: Text(myModel.txtValue),
);
},
);
}
You need move Provider to top layer(MaterialApp)
According to picture, Dialog widget is under MaterialApp, so this is why you using wrong context
wrap your whole child widget inside the consumer.
void myShowDialog() {
showDialog(
context: context,
builder: Consumer<MyModel>(
builder: (context, value, builder) {
retuen widget();
}
);
}
You should split Scaffold widget and its children, to another StatefulWidget
From single Widget
class MainScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocProvider(
builder: (context) {
SomeBloc someBloc = SomeBloc();
someBloc.dispatch(SomeEvent());
return someBloc;
},
child: Scaffold(
body: ...
floatingActionButton: FloatingActionButton(
onPressed: _openFilterSchedule,
child: Icon(Icons.filter_list),
),
)
);
}
}
Splitted into these two widget
class MainScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocProvider(
builder: (context) {
SomeBloc someBloc = SomeBloc();
someBloc.dispatch(SomeEvent());
return someBloc;
},
child: Screen(),
);
}
}
and ..
class Screen extends StatelessWidget {
void _openFilterSchedule() {
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) {
return TheBottomSheet();
},
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: ...
floatingActionButton: FloatingActionButton(
onPressed: _openFilterSchedule,
child: Icon(Icons.filter_list),
),
);
}
}
I found a solution, Just return your showModalBottomSheet with a StatefulBuilder and use the context of your modalsheet builder to pass to your provider. a snippet of my code below:
Future<Widget> showModal(int qty, Product product) async {
return await showModalBottomSheet(
isScrollControlled: true,
backgroundColor: Colors.transparent,
context: context,
builder: (BuildContext ctx) {
return StatefulBuilder(builder: (ctx, state) {
return Container(
child: RaisedButton(
onPressed: () {
Product prod = Product(product.id,
product.sku, product.name, qty);
Provider.of<CartProvider>(ctx, listen:
false).addCart(prod);}),);
}
}
);
}
TLDR: Make sure your import statement's casings match your project's folder casings.
I came across one other quirk while debugging this same error. I had several providers that were all working, including in showModalBottomSheets, however one was not working. After combing through the entire widget tree, without finding any discrepancies, I found that I had capitalized the first letter of a folder on one of the import statements of my problem-child notifier. I think this confused the compiler and caused it to throw the Could not find the correct Provider above this widget error.
After ensuring the import statement casing matched the folder name, my provider problems were resolved. Hopefully this will save someone a headache.
Not finding a clear explanation of adding multiple provided values, I thought I'd share here for reference.
await showMobileModals(
isDismissible: false,
context: context,
child: MultiProvider(
providers: [
Provider.value(
value: provided_one,
),
Provider.value(
value: provided_two,
),
Provider.value(
value: provided_three,
),
],
child: Container(),
),
);
Faced the same issue while dealing with showModelBottomSheet, since it happens to work in a different (context)widget tree I had to level up my state to that of the app so that I could access my provider using the context.