Bloc provider above OverlayEntry flutter - flutter

I am having some problems with my flutter app. I am trying to add an overlay like this in the photo below:
And it works just fine, I am able to open it on long press and close it on tap everywhere else on the screen.
The problem is that those two buttons - delete and edit - should call a bloc method that then do all the logic, but I do not have a bloc provider above the OverlayEntry. This is the error:
Error: Could not find the correct Provider<BrowseBloc> above this _OverlayEntryWidget Widget
This happens because you used a `BuildContext` that does not include the provider
of your choice. There are a few common scenarios:
- You added a new provider in your `main.dart` and performed a hot-reload.
To fix, perform a hot-restart.
- The provider you are trying to read is in a different route.
Providers are "scoped". So if you insert of provider inside a route, then
other routes will not be able to access that provider.
- You used a `BuildContext` that is an ancestor of the provider you are trying to read.
Make sure that _OverlayEntryWidget is under your MultiProvider/Provider<BrowseBloc>.
This usually happens when you are creating a provider and trying to read it immediately.
For example, instead of:
```
Widget build(BuildContext context) {
return Provider<Example>(
create: (_) => Example(),
// Will throw a ProviderNotFoundError, because `context` is associated
// to the widget that is the parent of `Provider<Example>`
child: Text(context.watch<Example>().toString()),
);
}
```
consider using `builder` like so:
```
Widget build(BuildContext context) {
return Provider<Example>(
create: (_) => Example(),
// we use `builder` to obtain a new `BuildContext` that has access to the provider
builder: (context, child) {
// No longer throws
return Text(context.watch<Example>().toString());
}
);
}
```
If none of these solutions work, consider asking for help on StackOverflow:
https://stackoverflow.com/questions/tagged/flutter
I've already encountered this error but this time I'm in a bit of trouble because I'm working with an overlay and not a widget.
This is my code:
late OverlayEntry _popupDialog;
class ExpenseCard extends StatelessWidget {
const ExpenseCard({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return BlocConsumer<AppBloc, AppState>(
listener: (context, state) {},
buildWhen: (previous, current) => previous.theme != current.theme,
builder: (context, state) {
return Column(
children: [
GestureDetector(
onLongPress: () {
_popupDialog = _createOverlay(expense);
Overlay.of(context)?.insert(_popupDialog);
},
child: Card(
...some widgets
),
),
const Divider(height: 0),
],
);
},
);
}
}
OverlayEntry _createOverlay(Expenses e) {
return OverlayEntry(
builder: (context) => GestureDetector(
onTap: () => _popupDialog.remove(),
child: AnimatedDialog(
child: _createPopupContent(context, e),
),
),
);
}
Widget _createPopupContent(BuildContext context, Expenses e) {
return GestureDetector(
onTap: () {},
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: MediaQuery.of(context).size.width * 0.9,
decoration: BoxDecoration(
color: LocalCache.getActiveTheme() == ThemeMode.dark ? darkColorScheme.surface : lightColorScheme.surface,
borderRadius: const BorderRadius.all(Radius.circular(16)),
),
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...some other widgets
],
),
),
SizedBox(
width: 256,
child: Card(
child: Column(
children: [
InkWell(
onTap: () {
_popupDialog.remove();
// This is where the error is been thrown
context.read<BrowseBloc>().add(SetTransactionToEdit(e));
showBottomModalSheet(
context,
dateExpense: e.dateExpense,
total: e.total,
transactionToEdit: e,
);
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
child: Row(
children: [Text(AppLocalizations.of(context).edit), const Spacer(), const Icon(Icons.edit)],
),
),
),
const Divider(height: 0),
InkWell(
onTap: () {
_popupDialog.remove();
// This is where the error is been thrown
context.read<BrowseBloc>().add(DeleteExpense(e.id!, e.isExpense));
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
child: Row(
children: [Text(AppLocalizations.of(context).delete), const Spacer(), const Icon(Unicons.delete)],
),
),
),
],
),
),
),
],
),
);
}
How can I add the bloc provider above my OverlayEntry? Is this the best course of action?
Thank you to everyone that can help!

Wrap your widget that you use in OverlayEntry in BlocProvider.value constructor and pass the needed bloc as an argument to it, like so
OverlayEntry _createOverlay(Expenses e, ExampleBloc exampleBloc) {
return OverlayEntry(
builder: (context) => GestureDetector(
onTap: () => _popupDialog.remove(),
child: BlocProvider<ExampleBloc>.value(
value: exampleBloc,
child: AnimatedDialog(
child: _createPopupContent(context, e),
),
),
),
);
}

I have found a solution starting from the answer of Olga P, but changing one thing. I use the BlocProvider.value but I am passing as an argument to the method the context and not the bloc itself. This is the code:
OverlayEntry _createOverlay(Expenses e, BuildContext context) {
return OverlayEntry(
builder: (_) => GestureDetector(
onTap: () => _popupDialog.remove(),
child: BlocProvider<BrowseBloc>.value(
value: BlocProvider.of(context),
child: AnimatedDialog(
child: _createPopupContent(context, e),
),
),
),
);
}
With this change the two methods - edit and delete - work perfectly. Thanks to everyone who replied, I learned something today too!

The problem is that you are using a function and not a widget. So you can either modify _createOverlay to be stateless or stateful widget, or you can pass the bloc as an argument to the function.
In the latter case this would be _createOverlay(expense, context.read<AppBloc>())

Related

Can I trigger grandparent state changes without an external state management library?

I cannot find a satisfactory way for a grandchild widget to trigger a grandparent state change. My app saves and sources its data all from an on-device database.
Ive tried to proceed this far without using a state management library as I thought this was overkill - the app is not complex.
Ive got a ListView (grandparent), which in turn has children that are my own version of ListTiles. There are two icon buttons on each ListTile, one to edit and one to delete - both of which trigger a different alertdialog (grandchild) popup. When I perform an update or delete on the data, it is written to the db and a Future is returned - and then I need the grandparent ListView state to refresh. StatefulBuilders will only give me a way to refresh state on the grandchild (separately from the child), not a way to trigger 'multi level' state change.
Is it time for a state management solution such as BLOC or Riverpod, or is there any other solution?
ListView Grandparent Widget
#override
Widget build(BuildContext context) {
return Scaffold(
body: Builder(
builder: (BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// other children here
Expanded(
flex: 11,
child: FutureBuilder<List<MyCustomObject>>(
future: _getQuotes(), // queries the db
builder: (context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.waiting
&& !snapshot.hasData) {
return const Center(
child: SizedBox(
height: AppDims.smallSizedBoxLoadingProgress,
width: AppDims.smallSizedBoxLoadingProgress,
child: CircularProgressIndicator()
),
);
} else if (snapshot.hasError) {
log(snapshot.error.toString());
log(snapshot.stackTrace.toString());
return Center(child: Text(snapshot.error.toString()));
} else {
// no point using StatefulBuilder here, as i need
// to potentially trigger _getQuotes() again to rebuild the entire ListView
return ListView.builder(
padding: const EdgeInsets.symmetric(
horizontal: AppDims.textHorizontalPadding,
vertical: AppDims.textVerticalPadding
),
itemCount: snapshot.data!.length,
itemBuilder: (context, int index) {
return MyCustomTile(
// tile data mapping from snapshot for MyCustomObject
);
},
);
}
},
)
)
]
);
}
)
);
}
MyCustomTile Child Widget
#override
Widget build(BuildContext context) {
return Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppDims.tileBorderRadius),
side: const BorderSide(
color: Colors.green,
width: 1.5,
)
),
child: ListTile(
// other omitted ListTile params here
trailing: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.edit),
onPressed: () => showDialog(
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
return EditDialog();
}
).then((_) => setState(() {})), // will only setState on the dialog!
),
IconButton(
icon: const Icon(Icons.delete),
onPressed: () => showDialog(
context: context,
barrierDismissible: true,
builder: (BuildContext context) => DeleteWarningDialog(
widget.id,
AppStrings.price.toLowerCase(),
true
),
),
),
]
),
),
);
}
DeleteWarningDialog Grandchild Widget
#override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(_buildFinalWarningString()),
actions: [
TextButton(
child: const Text(AppStrings.cancel),
onPressed: () => Navigator.pop(context),
),
TextButton(
child: const Text(AppStrings.delete),
onPressed: () {
_appDatabase.deleteFoo(widget.objectIdToDelete);
Navigator.pop(context);
},
)
],
);
}
you will have to declare a function in the grandParent which is the listView in your case and pass it to parent and children's. but it will be so complicated and not really efficient, using state management would make it a lot easer and clean

Multiple navigators in Flutter causes problem with pop

I'm struggling in a Flutter situation with multiple navigators. Concept here is a page which triggers a modal with his own flow of multiple pages. Inside the modal everything is going swiftly (navigation pushes/pops are working), but if the modal is dismissed it removes every page of the lowest navigator. I've looked at the example of https://stackoverflow.com/a/51589338, but I'm probably missing something here.
There's a wrapper Widget inside a page which is the root of the application.
class Wrapper extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
child: Padding(
padding: EdgeInsets.only(top: 10, bottom: 20),
child: FlatButton(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 5),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'Open modal',
),
],
),
),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
settings: RouteSettings(name: '/modal'),
builder: (context) => Modal(),
),
);
},
),
),
);
}
}
The modal initiates a Scaffold with his own navigator to handle its own flow.
class Modal extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: new Navigator(
initialRoute: FirstModalPage.route().toString(),
onGenerateRoute: (routeSettings) {
final path = routeSettings.name;
if (path == '/secondmodalpage') {
return MaterialPageRoute(
settings: routeSettings,
builder: (_) => SecondModalPage(),
);
}
return new MaterialPageRoute(
settings: routeSettings,
builder: (_) => FirstModalPage(),
);
},
),
);
}
}
The pages inside the modal are below with a close button which should remove the modal from the root navigator. But for some reason if I call pop() it removes all pages from the widget tree. And popUntil((route) => route.isFirst) doesn't do anything at all. If I'm looking at the widget via the Widget Inspector it does say that the Wrapper and Modal are on the same level.
class FirstModalPage extends StatelessWidget {
static Route route() {
return MaterialPageRoute<void>(builder: (_) => FirstModalPage());
}
#override
Widget build(BuildContext context) {
return Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Padding(
padding: EdgeInsets.only(
top: 30,
right: 20,
bottom: 10,
left: 20,
),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
},
child: Text('Back'),
),
Text(
'First modal page title'
),
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
Navigator.of(context, rootNavigator: true).popUntil(
(route) => route.isFirst,
);
},
child: Text('Close'),
),
],
),
),
FlatButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => SecondModalPage()),
);
},
child: Text(
'Second modal page'
),
),
],
),
);
}
}
You'd need to use a Navigator key to keep tab on which Navigator you need to do an action with.
final _mainNavigatorKey = GlobalKey<NavigatorState>();
Navigator(
key: _mainNavigatorKey,
...
),
To use the Navigator Key, you can call _mainNavigatorKey.currentState.pop();

Flutter / BLoC - BlocProvider.of() called with a context that does not contain a Bloc of type ArticlesBloc

I'm developing a new Flutter Mobile app using the BLoC pattern. But I've got a problem and I don't find the solution yet.
I've got three widgets :
The first one is my home page with a drawer
Thanks to the drawer, I can navigate to my second widget, an article list
And one last widget : article details (I can reach it with a tap on an article in the list)
My problem is between the second and the third. When I tap on an article, I've an error : BlocProvider.of() called with a context that does not contain a Bloc of type ArticlesBloc.
I've got this in my MaterialApp routes attribute
MyRoutes.articleList: (context) => BlocProvider<ArticlesBloc>(
create: (context) => ArticlesBloc()..add(ArticlesLoaded()),
child: ArticlesScreen(),
),
This is my article list body :
body: BlocBuilder<ArticlesBloc, ArticlesState>(
builder: (BuildContext buildContext, state) {
if (state is ArticlesLoadSuccess) {
final articles = state.articles;
return ListView.builder(
itemCount: articles.length,
itemBuilder: (BuildContext context, int index) {
final article = articles[index];
return ArticleItem(
onTap: () {
Navigator.of(buildContext).push(
MaterialPageRoute(builder: (_) {
return ArticleDetailScreen(id: article.id);
}),
);
},
article: article);
},
);
}
},
),
And my article details page :
#override
Widget build(BuildContext context) {
return BlocBuilder<ArticlesBloc, ArticlesState>(builder: (context, state) {
final Article article = (state as ArticlesLoadSuccess)
.articles
.firstWhere((article) => article.id == id, orElse: () => null);
return Scaffold(
appBar: AppBar(
title: Text(article.reference + ' - Article details'),
),
body: id == null
? Container()
: Padding(
padding: EdgeInsets.all(16.0),
child: ListView(
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Hero(
tag: '${article.id}__heroTag',
child: Container(
width: MediaQuery.of(context).size.width,
child: Text(
article.designation,
),
),
),
],
),
),
],
),
],
),
),
);
});
}
Please heeeelp ^^
Edit 1 : I find a solution but I don't know if it's the right way. Instead of only pass the id to the details screen, I pass the complete article so I can directly return the Scaffold without the BlocBuilder
You need to provide your bloc to your new route with ArticleDetailScreen.
Like this:
MaterialPageRoute(builder: (_) {
return BlocProvider.value(
value: BlocProvider.of<ArticlesBloc>(context),
child: ArticleDetailScreen(id: article.id),
);
})

Why Bloc not in the context?

I have BlocProvider widget above the widget where I'm trying to dispatch event, but I still getting BlocProvider.of() called with a context that does not contain a Bloc of type RenderBloc.
This is what my build method returns:
return BlocProvider<RenderBloc>(
builder: (BuildContext context) => RenderBloc(),
child: Column(
children: <Widget>[
FlatButton(
child: Text('Render'),
onPressed: () {
BlocProvider.of<RenderBloc>(context).add(RenderProjectEvent(project));
},
)
],
),
);
I also tried with MultiBlocProvider, got the same.
You need to create another inner context (using Builder, for example) to access InheritedWidget (Provider):
return BlocProvider<RenderBloc>(
builder: (BuildContext context) => RenderBloc(),
child: Builder(
builder: (cxt) {
return Column(
children: <Widget>[
FlatButton(
child: Text('Render'),
onPressed: () {
BlocProvider.of<RenderBloc>(cxt).add(RenderProjectEvent(project));
},
)
],
),
),
}
);

Use nested Navigator with WillPopScope in Flutter

In my application, I want to have a custom Navigation (only change part of the screen and keep an history of what I'm doing inside it).
For that purpose, I am using a Navigator and it's working fine for simple navigation.
However, I want to handle the back button of Android.
There is a problem with it in Flutter apparently which forces me to handle the backbutton in the parent widget of the Navigator :
https://github.com/flutter/flutter/issues/14083
Due to that, I need to retrieve the instance of my Navigator in the children and call pop() on it. I am trying to use a GlobalKey for this.
I am trying to make it work for a while now and made a sample project just to test this.
Here is my code :
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(title: 'Navigation Basics', home: MainWidget()));
}
class MainWidget extends StatelessWidget {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey();
#override
Widget build(BuildContext context) {
return SafeArea(
child: WillPopScope(
onWillPop: () => navigatorKey.currentState.maybePop(),
child: Scaffold(
body: Padding(
child: Column(
children: <Widget>[
Text("Toto"),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Expanded(
child: RaisedButton(
child: Text('First'),
onPressed: () {
navigatorKey.currentState.pushNamed('/first');
// Navigator.push(
// context,
// MaterialPageRoute(builder: (context) => SecondRoute()),
// );
},
)),
Expanded(
child: RaisedButton(
child: Text('Second'),
onPressed: () {
navigatorKey.currentState.pushNamed('/second');
},
))
],
),
Expanded(
child: Stack(
children: <Widget>[
Container(
decoration: BoxDecoration(color: Colors.red),
),
ConstrainedBox(
constraints: BoxConstraints.expand(),
child: _getNavigator()),
],
)),
],
),
padding: EdgeInsets.only(bottom: 50),
))));
}
Navigator _getNavigator() {
return Navigator(
key: navigatorKey,
initialRoute: '/',
onGenerateRoute: (RouteSettings settings) {
WidgetBuilder builder;
switch (settings.name) {
case '/':
builder = (BuildContext _) => FirstRoute();
break;
case '/second':
builder = (BuildContext _) => SecondRoute();
break;
default:
throw new Exception('Invalid route: ${settings.name}');
}
return new MaterialPageRoute(builder: builder, settings: settings);
});
}
}
class FirstRoute extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
child: Column(
children: <Widget>[
RaisedButton(
child: Text("GO TO FRAGMENT TWO"),
onPressed: () => Navigator.of(context).pushNamed("/second"),
)
],
),
decoration: BoxDecoration(color: Colors.green),
);
}
}
class SecondRoute extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
child: Column(
children: <Widget>[
RaisedButton(
child: Text("GO TO FRAGMENT ONE"),
onPressed: () => Navigator.of(context).pop(),
)
],
),
decoration: BoxDecoration(color: Colors.blue),
);
}
}
This is however not working as I would like. The default Navigator seem to still be used : after opening the SecondRoute and pressing the Android back button, it just leaves the app instead of just going back to the first route.
How can I achieve what I want?
Following the documentation of onWillPop:
/// Called to veto attempts by the user to dismiss the enclosing [ModalRoute].
///
/// If the callback returns a Future that resolves to false, the enclosing
/// route will not be popped.
final WillPopCallback onWillPop;
your handler should indicate that the enclosing route should not be closed, hence returning false will resolve your issue.
changing your handler to this works:
onWillPop: () async {
navigatorKey.currentState.maybePop();
return false;
},