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
Related
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>())
I am creating the List of Cards according to the number of toDoId.
toDoController.toDo() is like
toDo = [q1, r4, g4, d4].obs;
And, this is my ListView.builder()
Obx(() {
List _todo = toDoController.toDo();
return ListView.builder(
shrinkWrap: true,
scrollDirection: Axis.horizontal,
itemCount: _todo.length,
itemBuilder: (BuildContext context, int i) {
var _loading = true;
var _title = 'loading';
getTodoInfo() async {
_title = await toDoController
.getTodoInfo(
_todo[i]
);
_loading = false;
print(_title); // 'Clean!' <--- returns correct title
}
getTodoInfo();
return Container(
height: 150,
width: 150,
child: _loading
? Text(
_title,
)
: Text(
_title,
),
);
},
);
})
I am trying to make each Container calls the http requests to get the title from my database. Get the title and then update to the Text() widget below. However, it doesn't get updated after the value has been returned from the server.
I could make them wait for the request to get the title by using FutureBuilder. I tried with FutureBuilder too. However, FutureBuilder was not also reactive to the variable changes. So, I am trying to do this here. I kinda get the problem. After, the widget is returned, it is not changeable? Is there any way that I can do it with GetX?
Here's an example of using GetX with a Listview.builder.
This example uses a GetBuilder rather than Obx, as I'm not sure using a stream adds anything of benefit. If for some reason observables/streams are needed, numbers can be updated to be an .obs and the update() calls should be removed and GetBuilder replaced by GetX or Obx. If someone asks, I'll add that as an alternate example.
The GetBuilder wraps the ListView.builder and only the ListView will be rebuilt, not the entire widget tree / page.
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class ListDataX extends GetxController {
List<int> numbers = List<int>.from([0,1,2,3]);
void httpCall() async {
await Future.delayed(Duration(seconds: 1),
() => numbers.add(numbers.last + 1)
);
update();
}
void reset() {
numbers = numbers.sublist(0, 3);
update();
}
}
class GetXListviewPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
ListDataX dx = Get.put(ListDataX());
print('Page ** rebuilt');
return Scaffold(
body: SafeArea(
child: Column(
children: [
Expanded(
flex: 8,
child: GetBuilder<ListDataX>(
builder: (_dx) => ListView.builder(
itemCount: _dx.numbers.length,
itemBuilder: (context, index) {
return ListTile(
title: Text('Number: ${_dx.numbers[index]}'),
);
}),
),
),
Expanded(
flex: 1,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
RaisedButton(
child: Text('Http Request'),
onPressed: dx.httpCall,
),
RaisedButton(
child: Text('Reset'),
onPressed: dx.reset,
)
],
)
)
],
),
),
);
}
}
Obx / Streams version
Here's the above solution using Rx streams & Obx widget.
class ListDataX2 extends GetxController {
RxList<int> numbers = List<int>.from([0,1,2,3]).obs;
void httpCall() async {
await Future.delayed(Duration(seconds: 1),
() => numbers.add(numbers.last + 1)
);
//update();
}
void reset() {
numbers = numbers.sublist(0, 3);
//update();
}
}
class GetXListviewPage2 extends StatelessWidget {
#override
Widget build(BuildContext context) {
ListDataX2 dx = Get.put(ListDataX2());
print('Page ** rebuilt');
return Scaffold(
body: SafeArea(
child: Column(
children: [
Expanded(
flex: 8,
child: Obx(
() => ListView.builder(
itemCount: dx.numbers.length,
itemBuilder: (context, index) {
return ListTile(
title: Text('Number: ${dx.numbers[index]}'),
);
}),
),
),
Expanded(
flex: 1,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
RaisedButton(
child: Text('Http Request'),
onPressed: dx.httpCall,
),
RaisedButton(
child: Text('Reset'),
onPressed: dx.reset,
)
],
)
)
],
),
),
);
}
}
I've not tested it due to the fact that I don't have a complete sample but I think this is what you are looking for:
FutureBuilder<String>(
future: toDoController.getTodoInfo(_todo[i]),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.hasData) {
return Container(
height: 150,
width: 150,
child: Text(snapshot.data),
);
} else if (snapshot.hasError) {
return Text('Error');
} else {
return CircularProgressIndicator();
}
},
),
This is the code you need to return for every item of list builder.
I'm new to flutter and I'm trying to understand How to use provider state management in an application which users Moor to save some data into sqlite table. My application is a task recording application. I'm getting the above error in my widget tree when I open my bottom sheet add a task. I'm using provider: ^4.3.1
class TaskView extends StatelessWidget {
DateTime selectedDate = DateTime.now();
#override
Widget build(BuildContext context) {
return Provider<TaskViewModel>(
create: (_) => TaskViewModel(),
child: Scaffold(
appBar: AppBar(
title: Text('Tasks'),
),
body: Text("Temporary body!"),
floatingActionButton: FloatingActionButton(
onPressed: () {
showMaterialModalBottomSheet(
context: context,
builder: (context, scrollController) => Container(
child: bottomSheet(context),
),
);
},
child: Icon(
Icons.add,
color: Colors.white,
),
backgroundColor: Colors.blueAccent,
)
)
);
}
Widget bottomSheet(BuildContext context) {
return Padding(
padding: EdgeInsets.only(
left: 16.0,
top: 16.0,
right: 16.0,
bottom: 16.0 + MediaQuery.of(context).viewInsets.bottom),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
TextField(
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Task',
),
),
SizedBox(height: 8),
TextField(
readOnly: true,
decoration: InputDecoration(
border: OutlineInputBorder(),
suffixIcon: IconButton(
icon: Icon(Icons.date_range, color: Colors.grey),
onPressed: () => _selectDate(context),
),
labelText: 'Date',
),
),
SizedBox(height: 8),
Align(
alignment: Alignment.topRight,
child: context.watch<TaskViewModel>().state == ViewState.IDLE
? FlatButton(
child: Text("Save"),
color: Colors.blueAccent,
textColor: Colors.white,
onPressed: () => _onClickInsertTask(context))
: _loadingButtonChild(context))
],
),
);
}
Widget _loadingButtonChild(BuildContext context) {
return Container(
height: 20,
width: 20,
margin: EdgeInsets.all(5),
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white)),
);
}
/// This function is responsible for displaying the date picker when user click
/// on task due date inputFiled
Future<Null> _selectDate(BuildContext context) async {
final DateTime picked = await showDatePicker(
context: context,
initialDate: selectedDate,
firstDate: DateTime(2015, 8),
lastDate: DateTime(2101));
if (picked != null && picked != selectedDate) {
print("Date selected ${selectedDate.toString()}");
}
}
/// This function is responsible for triggering insert task block event
void _onClickInsertTask(BuildContext context) {
var insertTask = TaskData(task: "task", dueDate: selectedDate);
context.read<TaskViewModel>().insertTask(insertTask);
}
}
The error suggested checking.
- The provider you are trying to read is in a different route.
I have not given the provider to s route but as the direct parent view.
- You used a BuildContext that is an ancestor of the provider you are trying to read.
I didn't understand what it means but I made the suggested fix in the error. like below
#override
Widget build(BuildContext context) {
return Provider<TaskViewModel>(
create: (_) => TaskViewModel(),
builder: (context, child) => Scaffold(
appBar: AppBar(
title: Text('Tasks'),
),
body: Text("Temporary body!"),
floatingActionButton: FloatingActionButton(
onPressed: () {
showMaterialModalBottomSheet(
context: context,
builder: (context, scrollController) => Container(
child: bottomSheet(context),
),
);
},
child: Icon(
Icons.add,
color: Colors.white,
),
backgroundColor: Colors.blueAccent,
)));
}
Still get the same error. Another thing to note here is error suggested the below.
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) {
// No longer throws
return Text(context.watch<Example>()),
}
),
}
But I could not find a builder: (context) so I used the builder: (context, child). Please let me know what I should change to get this working. Thanks.
Edit:
BaseModel
class BaseViewModel extends ChangeNotifier {
ViewState _state = ViewState.IDLE;
ViewState get state => _state;
void setState(ViewState viewState) {
_state = viewState;
notifyListeners();
}
}
TaskViewModel
class TaskViewModel extends BaseViewModel{
final TaskRepository _repository = TaskRepository();
Resource<int> insertTaskStatus;
Future<void> insertTask(TaskData task) async {
setState(ViewState.PROCESSING);
var tasksCompanion = TasksCompanion(task: Value(task.task),dueDate: Value(task.dueDate));
insertTaskStatus = await _repository.insertTask(tasksCompanion);
setState(ViewState.COMPLETED);
}
}
Although you call showMaterialModalBottomSheet in the Scaffold wrapped by the provider, the provider is not above both TaskView's Scaffold and the modalBottomSheet. Why?
The provider you are trying to read is in a different route.
So, it seems that the modalBottomSheet is on a different route that doesn't have a provider above. If you take a look at the implementation of showModalBottomSheet you'll see:
return Navigator.of(context, rootNavigator: useRootNavigator).push(_ModalBottomSheetRoute<T>(....);
Clearly, it's a new route. So, to access the provider it should be above both routes. Since, the modalBottomSheet route is managed by the MaterialApp, you have to place the provider above the MaterialApp.
Provider uses lazy loading by default. So, objects are created when they are required and not on app start. However, if you don't want this behavior you can set lazy: false individually. For more info check the offical docs.
Another Easier option, for example if you are creating a package, that is supposed to inherit to be used in Another materialApp and you wish to use showModalBottomSheet, to read context of the parent provider, you have to disable the context of the showModalBottomSheet. This will force it to use the tree that has context for the provider in the parent widget.
#override
Widget build(BuildContext context) {
return Provider<TaskViewModel>(
create: (_) => TaskViewModel(),
builder: (context, child) => Scaffold(
appBar: AppBar(
title: Text('Tasks'),
),
body: Text("Temporary body!"),
floatingActionButton: FloatingActionButton(
onPressed: () {
showMaterialModalBottomSheet(
context: context,
builder: (_) => Container(
child: bottomSheet(context),
),
);
},
child: Icon(
Icons.add,
color: Colors.white,
),
backgroundColor: Colors.blueAccent,
)));
}
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),
);
})
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));
},
)
],
),
),
}
);