Using multiple BlocBuilder of same Bloc/Cubit, each for different events - flutter

Like this below official code sample, I used two BlocBuilder of CounterCubit for two different events increment and decrement.
It's running without any error, but both BlocBuilders are calling on each event.
I want one Builder should call on increment and one Builder should call on decrement.
class CounterView extends StatelessWidget {
#override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
return Scaffold(
appBar: AppBar(title: const Text('Counter')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
BlocBuilder<CounterCubit, int>(
builder: (context, state) {
return Text('Increment $state', style: textTheme.headline2);
},
),
BlocBuilder<CounterCubit, int>(
builder: (context, state) {
return Text('Decrement $state', style: textTheme.headline2);
},
),
])),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
FloatingActionButton(
key: const Key('counterView_increment_floatingActionButton'),
child: const Icon(Icons.add),
onPressed: () => context.read<CounterCubit>().increment(),
),
const SizedBox(height: 8),
FloatingActionButton(
key: const Key('counterView_decrement_floatingActionButton'),
child: const Icon(Icons.remove),
onPressed: () => context.read<CounterCubit>().decrement(),
),
],
),
);
}
}
Can I achieve this using a single CounterCubit?
Or I need to create two different Cubit classes like IncrementCubit and DecrementCubit.

Your counter cubit should emit both an increment and a decrement state.
One BlocBuilder is all you should use to keep it simple.
Please reference the bloc documents here

In your cubit state, instead of just an integer, you can wrap it with a denote of your intended action (increment or decrement). Within you BlocBuilder, check the intended action and update the view. Something like this:
BlocBuilder<CounterCubit, CounterCubitState>(
builder: (context, state) {
if (state.intendedOperation == increment)
return Text('Increment $state', style: textTheme.headline2);
},
),
You can also create 2 cubits as you mentioned. That's fine, but then you will have to manage the counter value in a common store (such as a repository).

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

Bloc provider above OverlayEntry 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>())

How to access index of Future<List<T>> data in another page Flutter

I am getting a List of data from third-party API on one page and using the same data on another page but I don't know how to make the list iterable to access indexes in the Widget Build() method.
On this page, I am getting List.
class DetailsPage extends StatefulWidget {
List<MarkersOnMap> propertydetails;
DetailsPage({Key key, this.propertydetails})
: super(
key: key,
);
#override
_DetailsPageState createState() => _DetailsPageState();
}
This is a Build() method and I want to access index as variable on each call, currently I'm just using index [0]
Widget build(BuildContext context) {
double width = MediaQuery.of(context).size.width;
return Scaffold(
appBar: AppBar(
title: Text(widget.propertydetails[0].ePropertiesCardsList.name,
style: appBarTextStyle),
),
bottomNavigationBar: Material(
child: Container(
child: Row(
children: <Widget>[
Row(
children: [
Text(
widget.propertydetails[0].ePropertiesCardsList.price
.toString(),
style: blackBigBoldTextStyle,
),
Text(
widget.propertydetails[0].ePropertiesCardsList.status,
style: blackSmallTextStyle,
),
],
),
child: Container(
child: Text(
'Contact'),
),
),
],
),
),
),
body: ListView(
children: [
Hero(
tag: widget.propertydetails[0].ePropertiesCardsList.name,
child: slider(),
),
titleRating(),
],
),
);
}
Sending data from the page like following
_propertyFinalList(index) {
return Container(
child: Center(
child: InkWell(
onTap: () async {
var get = await future;
if (get.isNotEmpty) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailsPage(
propertydetails: get,
)),
);
}
},
Not 100 percent sure I got the question right but to keep it simple you could use a global static class to store the data and access it from different parts of your application.
class GlobalState {
static List<MarkersOnMap> propertydetails = [];
static int selectedIndex = 0;
}
Then you would be able to write and read this data from different Pages. Keep in mind that your view will not rerender if you change data in this GlobalState, for that you could take a look at: https://pub.dev/packages/get
I just solved the problem by changing the following code from this
MaterialPageRoute(
builder: (context) => DetailsPage(
propertydetails: get,
)),
);
to this code. The full code is available above in the question.
MaterialPageRoute(
builder: (context) => DetailsPage(
propertydetails: [get[index]],
)),
);

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));
},
)
],
),
),
}
);