Flutter - child widgets not rebuilding in tabBarView - flutter

I am unable to build the children Operations and Logistics widget in the following code. I have tried setState and valuelistenableBuilder, but nothing is working. Operations and Logistics store their own list (and some other data), when they are first built (init is called) they fetch the data from API.
final GlobalKey<OperationsState> operationsKey = GlobalKey();
final GlobalKey<LogisticsState> logisticsKey = GlobalKey();
Widget build(BuildContext context) {
_tabsList = [
Tab(
child: Text(
'Operations',
style: CustomAppTheme.tabHeading,
overflow: TextOverflow.ellipsis,
),
),
Tab(
child: Text(
'Logistics',
style: CustomAppTheme.tabHeading,
overflow: TextOverflow.ellipsis,
),
),
];
// Operations and Logistics are stateful widget with their own state/data
_tabBarViewList = [
Tab(
child: Operations(
operationsKey: operationsKey,
logisticsKey: logisticsKey,
),
),
Tab(
child: Logistics(),
),
];
return DefaultTabController(
length: 2,
initialIndex: 0,
child: Scaffold(
key: scaffoldKey,
floatingActionButton: ValueListenableBuilder(
valueListenable: _showFloatingActionButton,
builder: (_, showButton, child) {
return showButton
? FloatingActionButton(
onPressed: () {
Navigator.of(context)
.push(MaterialPageRoute(builder: (context) {
return CreateRequest();
})).then((val) {
// on successfull request creation I am passing 'reload' to refresh the tabs
if (val == 'reload') {
// _refreshLeaves.value++;
setState(() {
});
}
});
},
child: Icon(Icons.add),
backgroundColor: CustomAppTheme.primaryColor,
)
: Container();
},
),
appBar: AppBar(), // code omitted as not related to the question
body: ValueListenableBuilder(
valueListenable: _refreshLeaves,
builder: (_, refresh, child) {
return TabBarView(
// controller: _tabController,
children: _tabBarViewList);
},
),
),
);
}```

I finally solved it... instead of passing already initialized GlobalKey() I passed UniqueKey(). This updated the children correctly whenever setState() is called.

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

How to use Defaultabcontroller with Provider in flutter

Condition don't work with StatelessWidget and Provider in Flutter,
Hopefully, I need that FloatingActionButton show up in specific tab but current code does not work properly. FloatingActionButton work in all tab.
I tried to debug and found index value is reflected but the Button does not disappear when changing tab.
return DefaultTabController(
length: 3,
initialIndex: 0,
child: ChangeNotifierProvider<MainModel>(
create: (_) => MainModel()..getWorkoutListRealtime(),
child: Scaffold(
appBar: AppBar(
title: Text("Workout at home"),
actions: [
Consumer<MainModel>(builder: (context, model, child) {
final isActive = model.checkShouldActiveCompleteButton();
return FlatButton(
onPressed: isActive
? () async {
await model.deleteCheckedItems();
}
: null,
child: Text(
'削除',
style: TextStyle(
color:
isActive ? Colors.white : Colors.white.withOpacity(0.5),
),
),
);
})
],
bottom: TabBar(
tabs: [
Tab(icon: Icon(Icons.directions_car)),
Tab(icon: Icon(Icons.directions_transit)),
Tab(icon: Icon(Icons.directions_bike)),
],
),
),
body: TabBarView(
children: [
Consumer<MainModel>(builder: (context, model, child) {
------------------------------------------------------------------
Consumer<MainModel>(builder: (context, model, child) {
return SimpleDatumLegend.withSampleData(model);
}),
CountdownTimer(),
],
),
floatingActionButton:
Consumer<MainModel>(builder: (context, model, child) {
final index = DefaultTabController.of(context).index;
return index == 1
? FloatingActionButton(
onPressed: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => AddPage(model),
fullscreenDialog: true,
),
);
},
child: Icon(Icons.touch_app),
)
: SizedBox.shrink();
}),
I use DefaultTabController because I use Stateless widget and Provider function.

Data From multiple FutureBuilders in flutter

I'm fetching data from an api source , the data is fetched properly , then i store the data in sqflite , so basically after doing both , i need to check if there is connection so that i show data from internet other than that i get data back from database , now since i'm using futurebuilder which return internet async operation result , how would i be also to get list of data from database , any help is appreciated guys and thank you in advance.
This is what i have tried so far
#override
void initState() {
super.initState();
dbHelper = DbHelper();
}
#override
Widget build(BuildContext context) {
return Scaffold (
appBar: AppBar(
title: Text("News Application"),
centerTitle: true,
backgroundColor: Colors.black,
titleTextStyle: TextStyle(color: Colors.white),
),
body: FutureBuilder (
future: Future.wait([getEverything(),dbHelper.getAllNews()]),
builder: (BuildContext context, AsyncSnapshot<List<dynamic>> snapshot) {
if(snapshot.hasError) {
// So basically here if there is an error , i woul like to show data from database
// i tried to get data from snapshot like this : snapshot.data[0]...and snapshot.data[1]
// but no data is returned..
return new Center(
child: new CircularProgressIndicator(
backgroundColor: Colors.black,
),
);
} else {
if(snapshot.connectionState == ConnectionState.done){
return new Container(
color: Colors.black,
child: GridView.count(
padding: const EdgeInsets.all(20),
crossAxisCount: 2,
children: List.generate(snapshot.data.articles.length, (index) {
return new GestureDetector(
onTap: (){
Navigator.push(
context,
MaterialPageRoute(builder: (context) => DetailsScreen(
image: snapshot.data.articles[index].urlToImage,
author: snapshot.data.articles[index].author,
title: snapshot.data.articles[index].title,
description: snapshot.data.articles[index].description,
publishedAt: snapshot.data.articles[index].publishedAt,
content: snapshot.data.articles[index].content,
))
);
},
child: Card(
elevation: 12,
child: new Column(
children: [
Image.network(snapshot.data.articles[index].urlToImage,
width: 250,),
Text(snapshot.data.articles[index].description)
],
),
),
);
}
)));
}
}
return new Center(
child: Visibility(
visible: true,
child: CircularProgressIndicator(
backgroundColor: Colors.black,
),
),
);
},
),
);
}

Flutter StreamProvider used a `BuildContext` that is an ancestor of the provider

I'm working on an app in Flutter (which I'm still kinda new too) and I'm stuck with the following error:
Error: Could not find the correct Provider<List<Category>> above this Exercises Widget
This likely happens because you used a `BuildContext` that does not include the provider
of your choice. There are a few common scenarios:
- 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 Exercises is under your MultiProvider/Provider<List<Category>>.
This usually happen 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>()),
),
}
```
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) {
// No longer throws
return Text(context.watch<Example>()),
}
),
I've been looking online and it clearly has to do with me not being able to get the correct 'context' when calling Provider.of<List<Category>>(context) in exercises_add.dart, and I don't really understand why. Because as you can see in my exercises.dart I have 'body: ExerciseList()', in which I am able to get the categories from the StreamProvider, but when I try to access it by clicking on my 'floatingActionButton' and then attempting to open my ExerciseAdd() page, it throws that error.
I would really appreciate a solution (+ explanation) on how to fix my code and why it isn't working.
exercises.dart
Widget build(BuildContext context) {
return _isLoading
? Loading()
: MultiProvider(
providers: [
StreamProvider<List<Exercise>>(
create: (context) => DatabaseService().exercises,
),
StreamProvider<List<Category>>(
create: (context) => DatabaseService().categories,
),
ChangeNotifierProvider<ExerciseFilter>(
create: (context) => ExerciseFilter(isActive: true),
)
],
child: Scaffold(
appBar: AppBar(
title: Text('Exercises'),
elevation: 0.0,
actions: _buildActions(),
),
body: ExerciseList(),
floatingActionButton: FloatingActionButton(
backgroundColor: Colors.black,
child: Icon(Icons.add, color: Colors.white),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ExercisesAdd(),
),
);
},
),
),
);
}
}
exercises_add.dart
#override
Widget build(BuildContext context) {
final cats = Provider.of<List<Category>>(context);
print(cats.length);
return Scaffold(
appBar: AppBar(
title: Text('Add Exercise'),
elevation: 0.0,
),
body: SingleChildScrollView(
child: Container(
padding: EdgeInsets.symmetric(vertical: 20.0, horizontal: 50.0),
child: Form(
key: _formKey,
child: Column(
children: <Widget>[
SizedBox(height: 20.0),
TextFormField(
decoration:
textInputDecoration.copyWith(hintText: 'Exercise Name'),
validator: (value) {
if (value.isEmpty) {
return 'Exercise name is required';
}
return null;
},
onChanged: (value) {
setState(() {
exerciseName = value;
});
},
),
SizedBox(height: 20.0),
Theme(
data: Theme.of(context).copyWith(canvasColor: Colors.white),
child: DropdownButtonFormField<String>(
decoration: dropdownDecoration,
value: exerciseCategory,
onChanged: (value) {
setState(() {
exerciseCategory = value;
});
},
items: categories.map<DropdownMenuItem<String>>((value) {
return DropdownMenuItem<String>(
value: value.name,
child: Text(value.name),
);
}).toList(),
),
),
SizedBox(height: 20.0),
RaisedButton(
elevation: 0,
color: Colors.black,
child: Text(
'Add Exercise',
style: TextStyle(color: Colors.white),
),
onPressed: () async {
if (_formKey.currentState.validate()) {
bool failed = false;
String uid = await _auth.getCurrentUser();
if (uid != null) {
dynamic result = DatabaseService(uid: uid)
.addExercise(exerciseName, exerciseCategory);
if (result != null) {
Navigator.pop(context);
} else {
failed = true;
}
} else {
failed = true;
}
if (failed) {
setState(() {
error = 'Failed to add exercise. Please try again';
});
}
}
},
),
SizedBox(height: 12.0),
Text(
error,
style: TextStyle(color: Colors.red, fontSize: 14.0),
),
],
),
),
),
),
);
DatabaseService().exercises
List<Category> _categoryListFromSnapshot(QuerySnapshot snapshot) {
return snapshot.documents.map((doc) {
return Category(name: doc.data['category'] ?? '');
}).toList();
}
Stream<List<Category>> get categories {
return categoryCollection
.orderBy('category')
.snapshots()
.map(_categoryListFromSnapshot);
}
NOTE: the StreamProvider & MultiProvider etc.. are all part of the 'provider' package (I use the most recent version)
The error you received describes the scenario you are currently in.
- 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're navigating to a different route and trying to access the provider, but it's no longer in the widget tree.
You simply need to move your MultiProvider above whatever navigator you're using in your widget tree. You're likely using a MaterialApp to do this, so move MultiProvider and make MaterialApp it's child.

Flutter Provider - rebuild list item instead of list view

I'm using the Provider package to manage my apps business logic but I've encountered a problem where my entire ListView is rebuilding instead of an individual ListTile. Here's the UI to give you a better understanding:
Currently if I scroll to the bottom of the list, tap the checkbox of the last item, I see no animation for the checkbox toggle and the scroll jumps to the top of the screen because the entire widget has rebuilt. How do I use Provider so that only the single ListTile rebuilds and not every item in the List?
Here's some of the relevant code:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Checklist',
theme: ThemeData(
brightness: Brightness.light,
primaryColor: Colors.indigo[500],
accentColor: Colors.amber[500],
),
home: ChecklistHomeScreen(),
),
providers: [
ChangeNotifierProvider(
create: (ctx) => ChecklistsProvider(),
),
],
);
}
}
class ChecklistHomeScreen extends StatefulWidget {
#override
_ChecklistHomeScreenState createState() => _ChecklistHomeScreenState();
}
class _ChecklistHomeScreenState extends State<ChecklistHomeScreen> {
void createList(BuildContext context, String listName) {
if (listName.isNotEmpty) {
Provider.of<ChecklistsProvider>(context).addChecklist(listName);
}
}
#override
Widget build(BuildContext context) {
final _checklists = Provider.of<ChecklistsProvider>(context).checklists;
final _scaffoldKey = GlobalKey<ScaffoldState>();
ScrollController _scrollController =
PrimaryScrollController.of(context) ?? ScrollController();
return Scaffold(
key: _scaffoldKey,
body: CustomScrollView(
controller: _scrollController,
slivers: <Widget>[
SliverAppBar(
floating: true,
pinned: false,
title: Text('Your Lists'),
centerTitle: true,
actions: <Widget>[
PopupMenuButton(
itemBuilder: (ctx) => null,
),
],
),
ReorderableSliverList(
delegate: ReorderableSliverChildBuilderDelegate(
(ctx, i) => _buildListItem(_checklists[i], i),
childCount: _checklists.length,
),
onReorder: (int oldIndex, int newIndex) {
setState(() {
final checklist = _checklists.removeAt(oldIndex);
_checklists.insert(newIndex, checklist);
});
},
),
],
),
drawer: Drawer(
child: null,
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: null,
),
);
}
Widget _buildListItem(Checklist list, int listIndex) {
return Dismissible(
key: ObjectKey(list.id),
direction: DismissDirection.endToStart,
background: Card(
elevation: 0,
child: Container(
alignment: AlignmentDirectional.centerEnd,
color: Theme.of(context).accentColor,
child: Padding(
padding: EdgeInsets.fromLTRB(0.0, 0.0, 10.0, 0.0),
child: Icon(
Icons.delete,
color: Colors.white,
),
),
),
),
child: Card(
child: ListTile(
onTap: null,
title: Text(list.name),
leading: Checkbox(
value: list.completed,
onChanged: (value) {
Provider.of<ChecklistsProvider>(context)
.toggleCompletedStatus(list.id, list.completed);
},
),
trailing: IconButton(
icon: Icon(Icons.more_vert),
onPressed: null,
),
),
),
onDismissed: (direction) {
_onDeleteList(list, listIndex);
},
);
}
void _onDeleteList(Checklist list, int listIndex) {
Scaffold.of(context).removeCurrentSnackBar();
Scaffold.of(context).showSnackBar(
SnackBar(
action: SnackBarAction(
label: 'UNDO',
onPressed: () {
Provider.of<ChecklistsProvider>(context)
.undoDeleteChecklist(list, listIndex);
},
),
content: Text(
'List deleted',
style: TextStyle(color: Theme.of(context).accentColor),
),
),
);
}
}
class ChecklistsProvider with ChangeNotifier {
final ChecklistRepository _repository = ChecklistRepository(); //singleton
UnmodifiableListView<Checklist> get checklists => UnmodifiableListView(_repository.getChecklists());
void addChecklist(String name) {
_repository.addChecklist(name);
notifyListeners();
}
void deleteChecklist(int id) {
_repository.deleteChecklist(id);
notifyListeners();
}
void toggleCompletedStatus(int id, bool completed) {
final list = checklists.firstWhere((c) => c.id == id);
if(list != null) {
list.completed = completed;
_repository.updateChecklist(list);
notifyListeners();
}
}
}
I should say I understand why this is the current behavior, I'm just not sure of the correct approach to ensure only the list item I want to update gets rebuilt instead of the whole screen.
I've also read about Consumer but I'm not sure how I'd fit it into my implementation.
A Consumer will essentially allow you to consume any changes made to your change notifier. It's best practice to embed the Consumer as deep down as possible in your build method. This way only the wrapped widget will get re-built. This document explains it well: https://flutter.dev/docs/development/data-and-backend/state-mgmt/simple
Try wrapping your CheckBox widget in a Consumer widget. Only the checkbox should be rebuilt.
Consumer<ChecklistsProvider>(
builder: (context, provider, _) {
return Checkbox(
value: list.completed,
onChanged: (value) {
provider.toggleCompletedStatus(list.id, list.completed);
},
);
},
),
If you'd rather have the ListTile AND the CheckBox be re-built, just wrap the ListTile in the Consumer instead