How to properly initialize a Future in Flutter Provider - flutter

so I am trying to build up a list in my provider from a Future Call.
So far, I have the following ChangeNotifier class below:
class MainProvider extends ChangeNotifier {
List<dynamic> _list = <dynamic>[];
List<dynamic> get list => _list;
int count = 0;
MainProvider() {
initList();
}
initList() async {
var db = new DatabaseHelper();
addToList(Consumer<MainProvider>(
builder: (_, provider, __) => Text(provider.count.toString())));
await db.readFromDatabase(1).then((result) {
result.forEach((item) {
ModeItem _modelItem= ModeItem.map(item);
addToList(_modelItem);
});
});
}
addToList(Object object) {
_list.add(object);
notifyListeners();
}
addCount() {
count += 1;
notifyListeners();
}
}
However, this is what happens whenever I use the list value:
I can confirm that my initList function is executing properly
The initial content from the list value that is available is the
Text() widget that I firstly inserted through the addToList function, meaning it appears that there is only one item in the list at this point
When I perform Hot Reload, the rest of the contents of the list seems to appear now
Notes:
I use the value of list in a AnimatedList widget, so I am
supposed to show the contents of list
What appears initially is that the content of my list value is only one item
My list value doesn't seem to automatically update during the
execution of my Future call
However, when I try to call the addCount function, it normally
updates the value of count without needing to perform Hot Reload -
this one seems to function properly
It appears that the Future call is not properly updating the
contents of my list value
My actual concern is that on initial loading, my list value doesn't
properly initialize all it's values as intended
Hoping you guys can help me on this one. Thank you.
UPDATE: Below shows how I use the ChangeNotifierClass above
class ParentProvider extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<MainProvider>(
create: (context) => MainProvider(),
),
],
child: ParentWidget(),
);
}
}
class ParentWidget extends StatelessWidget {
final GlobalKey<AnimatedListState> listKey = GlobalKey<AnimatedListState>();
#override
Widget build(BuildContext context) {
var mainProvider = Provider.of<MainProvider>(context);
buildItem(BuildContext context, int index, Animation animation) {
print('buildItem');
var _object = mainProvider.list[index];
var _widget;
if (_object is Widget) {
_widget = _object;
} else if (_object is ModelItem) {
_widget = Text(_object.unitNumber.toString());
}
return SizeTransition(
key: ValueKey<int>(index),
axis: Axis.vertical,
sizeFactor: animation,
child: InkWell(
onTap: () {
listKey.currentState.removeItem(index,
(context, animation) => buildItem(context, index, animation),
duration: const Duration(milliseconds: 300));
mainProvider.list.removeAt(index);
mainProvider.addCount();
},
child: Card(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: _widget,
),
),
),
);
}
return Scaffold(
appBar: AppBar(),
body: Container(
color: Colors.white,
child: Padding(
padding: const EdgeInsets.all(32.0),
child: mainProvider.list == null
? Container()
: AnimatedList(
key: listKey,
initialItemCount: mainProvider.list.length,
itemBuilder:
(BuildContext context, int index, Animation animation) =>
buildItem(context, index, animation),
),
),
),
);
}
}

You are retrieving your provider from a StatelessWidget. As such, the ChangeNotifier can't trigger your widget to rebuild because there is no state to rebuild. You have to either convert ParentWidget to be a StatefulWidget or you need to get your provider using Consumer instead of Provider.of:
class ParentWidget extends StatelessWidget {
final GlobalKey<AnimatedListState> listKey = GlobalKey<AnimatedListState>();
#override
Widget build(BuildContext context) {
return Consumer<MainProvider>(
builder: (BuildContext context, MainProvider mainProvider, _) {
...
}
);
}
As an aside, the way you are using provider is to add the MainProvider to its provider and then retrieve it from within its immediate child. If this is the only place you are retrieving the MainProvider, this makes the provider pattern redundant as you can easily just declare it within ParentWidget, or even just get your list of images using a FutureBuilder. Using provider is a good step toward proper state management, but also be careful of over-engineering your app.

Related

Switching from ReorderableListView to ListView breaks Interaction of rows

I have a view which switches between a ListView and a ReorderableListView.
Widget _buildList(
BuildContext context,
List<ExerciseTemplate> exerciseTemplates,
EditWorkoutModel dao,
) {
if (_isInEditingMode) {
return ReorderableListView(
key: ObjectKey('reordeableListView'),
onReorder: ((oldIndex, newIndex) {
dao.reorderIndexes(
oldIndex,
(oldIndex < newIndex) ? newIndex - 1 : newIndex,
);
}),
padding: const EdgeInsets.only(bottom: 120),
children: [
for (var exerciseTemplate in exerciseTemplates)
Provider(
key: ObjectKey('${exerciseTemplate.id}_compactExerciseTemplateRow_provider'),
create: (context) => EditExerciseModel(exerciseTemplate),
child: CompactExerciseTemplateRow(
key: ObjectKey('${exerciseTemplate.id}_compactExerciseTemplateRow'),
),
),
],
);
} else {
return ListView.builder(
key: ObjectKey('listView'),
itemCount: exerciseTemplates.length,
padding: const EdgeInsets.only(bottom: 120),
itemBuilder: (BuildContext context, int index) {
final exerciseTemplate = exerciseTemplates[index];
return Provider(
// Key is needed here to properly handle deleted rows in the ui.
// Without the key, deleted rows are being shown.
key: ObjectKey(
'${exerciseTemplate.id}_exerciseTemplateRow_provider'),
create: (context) => EditExerciseModel(exerciseTemplate),
child: ExerciseTemplateRow(
key: ObjectKey('${exerciseTemplate.id}_exerciseTemplateRow'),
onDelete: () async {
await dao.deleteExercise(exerciseTemplate);
return true;
},
),
);
},
);
}
}
Both lists show the same data, but tapping a button, switches to a ReorderableListView which shows the data with different widgets. Tapping the button again switches back to the ListView.
However, switching forth and back results that I am not able to interact with elements within the row of the ListView. This issue appeared after I added a globalKey for each element in the ListView. I need this key, to properly handle deleting rows, so I can not just remove the key again.
How can I make it work, that I can interact with widgets within the row after I switched to the ReorderableListView and back to the ListView?
Copied from Provider document:
https://pub.dev/packages/provider
DON'T create your object from variables that can change over time.
In such a situation, your object would never update when the value changes.
int count;
Provider(
create: (_) => MyModel(count),
child: ...
)
If you want to pass variables that can change over time to your object, consider using ProxyProvider:
int count;
ProxyProvider0(
update: (_, __) => MyModel(count),
child: ...
)
It's ok to use Global key and switch between ListView and ReorderableListView, see example below:
https://dartpad.dev/?id=fd39a89b67448d86e682dd2c5ec77453
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool reOrder = false;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(reOrder ? "ReoderableListView" : "ListView"),
),
floatingActionButton: FloatingActionButton(onPressed: () {
setState(() {
reOrder = !reOrder;
});
}),
body: MyListView(reOrder));
}
}
final data = List.generate(10, (index) => {"title": 'item $index', "value": false});
class MyListView extends StatefulWidget {
final bool reOrder;
const MyListView(this.reOrder, {super.key});
#override
State<MyListView> createState() => _MyListViewState();
}
class _MyListViewState extends State<MyListView> {
#override
Widget build(BuildContext context) {
if (widget.reOrder) {
return ReorderableListView(
key: const ObjectKey('reordeableListView'),
onReorder: (int oldIndex, int newIndex) {
setState(() {
if (oldIndex < newIndex) {
newIndex -= 1;
}
final item = data.removeAt(oldIndex);
data.insert(newIndex, item);
});
},
children: [
for (var item in data)
ListTile(
key: ObjectKey('${item["title"]}_compactExerciseTemplateRow_provider'),
title: Text(item["title"] as String),
trailing: Text((item["value"] as bool).toString()),
),
],
);
} else {
return ListView.builder(
key: const ObjectKey('listView'),
itemCount: data.length,
padding: const EdgeInsets.only(bottom: 120),
itemBuilder: (BuildContext context, int index) {
return CheckboxListTile(
key: ObjectKey('${data[index]["title"]}_exerciseTemplateRow_provider'),
title: Text(data[index]["title"] as String),
value: (data[index]["value"] as bool),
onChanged: (bool? value) {
setState(() => data[index]["value"] = value!);
},
);
},
);
}
}
}
So the issue was that I was using ObjectKey instead of ValueKey.
The difference between those two is that ObjectKey checks if the identity (the instance) is the same. ValueKey checks the underlying value with the == operator.
My guess here is that by using ObjectKey in my case, flutter is not able to properly replace the old widget with the new one, since the new widget always have a different key. By using ValueKey flutter can distinguish between old and new widgets. Widgets will be in my case replaced after I switch between the lists, because the row widget won't be visible and therefor disposed.
Because the widgets were not properly replaced, somehow the old widgets are still being rendered, but all gesture listeners were already disposed. Therefor no interaction was possible anymore.
These are just my assumption, let me know if I am completely wrong here.

GetX and ListView issues

I have quite the problem wrapping my head around the whole state management of GetX so naturally I'm facing some issues.
I'm getting a collection from firebase which I put into a listview populated with Card widgets (CustomCard()), each document into a Card widget. In this Card widget I have a boolean that controls whether the card should be expanded (simply by Adding a Row()) or not if Card is tapped. The issue I'm facing is that if use GetX for this boolean, all the cards will trigger and not each individual card. In a way this seems logical because I only have one controller that manages this boolean.
So to clarify, bool isCardExpanded seem to be global for all Card widgets meaning that if I tap one card, ALL cards will expand, which is not what I want. I need them to act individually.
Do I need one separate controller for every Card in the list view or is this solvable in another way?
Controller
class Controller extends GetxController {
RxBool isCardExpanded = false.obs;
void changeExpanded() {
isCardExpanded.value = !isCardExpanded.value;
update();
}
}
ListView:
class CustomScreen extends State<CustomScreen>
implements ItemScreenInterface {
Controller ctrl = Get.find();
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: FirebaseFirestore.instance
.collection(someCol)
.doc(SomeDoc)
.collection(anotherCol).snapshots(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
return Column(
children: [
Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: snapshot.data.docs.length,
itemBuilder: (_, i) {
return CustomCard( // <------------ Card widget
document: snapshot.data.docs[i]);
}),
)
],
);
},
);
}
Card class
class CustomCard extends StatefulWidget {
CustomCard({required this.doc});
var doc;
#override
_CustomCardState createState() => _CustomCardState();
}
class _CustomCardState extends State<CustomCard> {
Controller ctrl = Get.find();
RxBool _isCardExpanded = false.obs;
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Card(
child: GestureDetector(
onTap: () {
ctrl.changeExpanded(); // <-- change bool
},
child: Padding(
padding: EdgeInsets.all(2),
child: Column(
children: <Widget>[
Row(
children: <Widget>[
Text('Top Part')
]),
if (_isCardExpanded.value) Divider(thickness: 2),
if (_isCardExpanded.value) // Controlled by _isCardExpanded. Add Row if true
Row(
children: <Widget>[
Text('EXPANDED'),
],
),
],
),
),
),
);
}
}
Desired outcome:
Actual outcome:
Because your controller instance initializes only once.
To solve your problem, you need to make a List that has the status of the cards in the Controller.
Or don't use the State management tool, but separately place the _isExpanded value in the _CustomCardState class. And use the setState() function.
EDIT
There is another way by using the tag argument when creating the controller like Get.create(someController, tag: TAG_NAME);

How to avoid streambuilder executes unnecessarytimes

I'm trying to display a list of documents which works, but I read that one good practice is to manage states (which I'm trying currently to understand too). In this case every time I change of screen using the bottomNavigationBar the streamBuilder executes (I always see the CircularProgressIndicator).
I tried call the collection reference in the intState but still the same issue, my code:
class Deparments extends StatefulWidget {
Deparments({Key? key, required this.auth}) : super(key: key);
final AuthBase auth;
#override
_DeparmentsState createState() => _DeparmentsState();
}
class _DeparmentsState extends State<Deparments> {
late final Stream<QuerySnapshot<Object?>> _widget;
Stream<QuerySnapshot<Object?>> getProds(){
CollectionReference ref = FirebaseFirestore.instance.collection("Departamentos");
return ref.snapshots();
}
#override
void initState() {
super.initState();
_widget = getProds();
}
#override
Widget build(BuildContext context) {
return Scaffold(
drawer: SideMenu(auth: widget.auth),
appBar: AppBar(
title: Text("Departamentos"),
centerTitle: true,
backgroundColor: Colors.green,
),
body: Container(
child: StreamBuilder<QuerySnapshot> (
stream: _widget,
builder: (BuildContext context, snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
} else {
List deparments =
snapshot.data!.docs.map((doc) => doc.id).toList();
return Column(
children: [
Expanded(
child: ListView.builder(
padding: EdgeInsets.only(top: 10),
scrollDirection: Axis.vertical,
itemCount: deparments.length,
itemBuilder: (context, index) {
return SingleChildScrollView(
child: Card(
child: Text(deparments[index]),
),
);
}),
)
],
);
}
}),
),
);
}
}
Update: for those who are facing the same issue Tayan provides a useful solution and he has a video showing the solution https://stackoverflow.com/a/64057210/9429407
Init state will not help you to avoid rebuilds because on changing tabs Flutter rebuilds your Screen. So we need some way to keep our screen alive, so here comes AutomaticKeepAliveClientMixin.
class _HomeState extends State<Home> with AutomaticKeepAliveClientMixin<Home> {
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
//Make sure to include the below method
super.build(context);
return SomeWidget();
}
}
The above implementation keeps all of your tab state persists and does not rebuilds the tabs again. Well this may serve your purpose but it may not be idle because this loads all the tabs at once even if the user actually didnt visited a tab, so to avoid the build unless a tab is clicked, use the above method in combination with pageview.
Check out pageView implementation
Also, if you want a better way to manage state and save some of your read calls to Firestore, then you should store data locally and fetch only those needed and/or use paginations.
Initialize your stream in initState just like this answer:
StreamBuilder being called numerous times when in build

How to pass a Firestore document from a stream to next page in Flutter?

I have a listview where each item is one document from a firestore collection. I would like to tap the item and pass the document information to a details page.
This is how I am retrieving document information within the first stream:
child: Text(streamSnapshot.data.docs[index]['event_title'],
This is how I'm attempting to send the data to the next page:
child: GestureDetector(
onTap: () {
Navigator.pushNamed(context, EventPage.id, arguments: streamSnapshot.data.docs[index]);
},
I'm lost as to how to receive the passed data:
class _EventPageState extends State<EventPage> {
#override
final db = FirebaseFirestore.instance;
Widget build(BuildContext context) {
final args = ModalRoute.of(context)!.settings.arguments;
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text('event_title'),
),
child: Column(
I know I need a StreamBuilder on the next page, but do you have any insight on how to make that stream show only the passed in document?
Why don't you use Provider instead? It'll help you to avoid boiler plate and as you're using streams it'll let you handle information in a better way. Check about it here
I have worked out an answer to this question. I'm sure there are several ways to do this, but here's mine:
The key is to pass the firestore document ID to the next page. In this example code, I pass streamSnapshot.data.docs[index].id.toString() as a parameter to a custom widget. I've located my named route within that widget.
StreamBuilder(
stream: FirebaseFirestore.instance
.collection('events')
.where('start_date', isGreaterThanOrEqualTo: DateTime.now())
.snapshots(),
builder: (context, AsyncSnapshot streamSnapshot) {
if (!streamSnapshot.hasData) {
return SizedBox(
height: 250,
child: Center(
child: CircularProgressIndicator(),
),
);
} else
return SizedBox(
height: 250,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: streamSnapshot.data.docs.length,
itemBuilder: (ctx, index) =>
EventListHorizontalTile(
//passes the document ID as a string down to the horizontally scrollable tile,
//where we push a named route with the docID string as an argument
firestoreDocID: streamSnapshot.data.docs[index].id.toString(),
image: streamSnapshot.data.docs[index]['main_image'],
name: streamSnapshot.data.docs[index]['name'],
),
),
);
}),
I then created a class to pass as an argument through a named route.
class Events {
final String firestoreDocID;
Events({
required this.firestoreDocID,
});
}
Now, within my EventListHorizontalTile widget:
class EventListHorizontalTile extends StatelessWidget {
const EventListHorizontalTile({
Key? key,
required this.name,
this.firestoreDocID = '',
}) : super(key: key);
final String name;
final String firestoreDocID;
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
//Here I am pushing a named route with an argument, using that Events class I made earlier.
Navigator.pushNamed(context, EventPage.id, arguments: Events(firestoreDocID: firestoreDocID));
},
//child: The rest of the list tile widget
),
Now we have to write a bit of code in the EventPage to receive the argument.
class EventPage extends StatefulWidget {
const EventPage({
Key? key,
}) : super(key: key);
static String id = 'EventPage';
#override
_EventPageState createState() => _EventPageState();
}
class _EventPageState extends State<EventPage> {
#override
Widget build(BuildContext context) {
//This is how we receive the argument.
final args = ModalRoute.of(context)!.settings.arguments as Events;
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
//Some text to see if the string made it.
Text(args.firestoreDocID),
]),
),
);
}
}
And that's it! Once you have that document ID in your new page, you can call a Streambuilder like this:
StreamBuilder(
stream: FirebaseFirestore.instance
.collection('events')
.doc(args.firestoreDocID)
.snapshots(),

How to update content of a ModalBottomSheet from parent while active

I have a custom button which takes in a list of items. When it is pressed, it opens a modal bottom sheet and passes that list to the bottom sheet.
However, When the buttons items change it doesn't update the items in the bottom sheet.
How can I achieve this effect.
Simple example
ButtonsPage
|
Button(items: initialItems)
|
BottomSheet(items: initialItems)
** After a delay, setState is called in ButtonsPage with newItems, thereby sending newItems to the button
ButtonsPage
|
Button(items: newItems)
| ## Here, the bottom sheet is open. I want it to update initialItems to be newItems in the bottom sheet
BottomSheet(items: initialItems -- should be newItems)
Code Sample
This is my select field which, as shown, receives a list items and when it is pressed it opens a bottom sheet and sends the items received to the bottom sheet.
class PaperSelect extends StatefulWidget {
final List<dynamic> items;
PaperSelect({
this.items,
}) : super(key: key);
#override
_PaperSelectState createState() => _PaperSelectState();
}
class _PaperSelectState extends State<PaperSelect> {
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: widget.disabled ? null : () => _showBottomSheet(context),
child: Container(
),
);
}
void _showBottomSheet(BuildContext context) {
showModalBottomSheet(
context: context,
builder: (BuildContext context) => BottomSheet(
items: widget.items,
),
)
);
}
}
After some time (a network call), items is updated in the parent Widget of PaperSelect. PaperSelect then updates and receives the new items.
class BottomSheet extends StatefulWidget {
final dynamic items;
BottomSheet({
this.items,
}) : super(key: key);
#override
State<StatefulWidget> createState() {
return _BottomSheetState();
}
}
class _BottomSheetState extends State<BottomSheet> {
dynamic items;
#override
void initState() {
print(widget.items);
items = widget.items;
super.initState();
}
#override
Widget build(BuildContext context) {
if(items==null) return Center(
child: SizedBox(
width: 140.0,
height: 140.0,
child: PaperLoader()
),
);
if(items==-1) return Text("Network Error");
return Column(
children: <Widget>
Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: items.length,
itemBuilder: (BuildContext context, int i) => ListTile(
onTap: () => Navigator.pop(context),
title: Text('')
)
),
),
],
);
}
}
Now, I want to send the updated data to the bottom sheet. However, it doesn't work because the ModalBottomSheet is already open.
How can I get around this?
I assume here that items is a List<String>, since you did not specify that at all. (You should generally not use dynamic in most cases, because it does not do any type checking at all). Anyway,
One thing you could do (beside countless others) is pass in a ValueNotifier<List<String>> instead of a List<String> and then user that with a ValueListenableBuilder in your bottom sheet. like:
// in the caller
final itemsNotifier = ValueNotifier(items);
// in your bottom sheet
ValueListenableBuilder<List<String>>(
valueListenable: itemsNotifier,
builder: (context, snapshot, child) {
final items = snapshot.data;
// build your list
},
)
then every time you would change itemsNotifier.value = [...] your items would rebuild.
Note when updating itemsNotifier.value: It must not be done inside build/didUpdateWidget/etc. so if this is the case you might use WidgetsBinding.instance.addPostFrameCallback(() => itemsNotifier.value = ... ) to postpone updating the value until after the current build phase.