I've been trying to wrap my head around data flow and state management in flutter particularly using the provider package. I tried querying from firestore using Future but turns out that doesn't fully utilise the capabilities of firestore since Futures only expect one value.
So I switched to using Streams instead, and using StreamProvider to get the data to my widgets. Problem is I can't figure out at what point/how to tell the UI to rebuild. Sometimes the stream has data, sometimes it doesn't and I can't really tell what's going on during those times. Say I wanted to show a circular progress bar when the view is busy, how do I achieve that when using StreamProvider. Does it even make sense to do that?
So with the filledstacks approach, one is able to switch model state between Busy and Idle, when making a network call using futures. For instance when we call a getPosts method, we first set the state to Busy so the UI can react and probably show a loading indicator. However we can set the state back to Idle once data has been received async. A Stream on the other hand, at least from my understanding, is just a pipe that's constantly passing data and I can't come up with a way to achieve the Busy/Idle state or even to tell if this Stream has new data/any data. Any ideas?
This is part of my database_service.dart that should return a stream of data from firestore.
Stream<List<Expense>> getExpenses(int month, int year) {
var ref = _db
.collection("userData")
.document(fireUser.uid)
.collection("expenses")
.where("month", isEqualTo: month)
.where("year", isEqualTo: year)
.orderBy("date", descending: true);
return ref.snapshots().map((list) =>
list.documents.map((snap) => Expense.fromFirestore(snap)).toList());
}
This is part of my expenses.dart view file that uses StreamProvider.
class ExpensesPage extends StatefulWidget {
#override
_ExpensesPageState createState() => _ExpensesPageState();
}
class _ExpensesPageState extends State<ExpensesPage> {
int _month;
int _year;
#override
void initState() {
super.initState();
_month = DateTime.now().month;
_year = DateTime.now().year;
}
#override
Widget build(BuildContext context) {
return StreamProvider<List<Expense>>.value(
stream: locator<ExpensesProvider>().getMonthExpenses(_month, _year),
initialData: [],
child: StreamProvider<List<Category>>.value(
stream: locator<ExpensesProvider>().getCategories(),
initialData: [],
child: Scaffold(
body: Container(
alignment: Alignment.center,
color: Theme.of(context).primaryColor,
padding: EdgeInsets.all(10.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
InkWell(
child: DateHeader("$_month $_year"),
onTap: () => _selectDate(),
),
Expanded(
child: ExpenseList(
month: _month,
year: _year,
),
)
],
),
),
),
),
);
}
and expense_list.dart with the ExpenseList widget that consumes data from the StreamProvider.
class ExpenseList extends StatelessWidget {
final _dateFormat = DateFormat.yMMMEd();
final _dbService = locator<DatabaseService>();
final int month;
final int year;
ExpenseList({#required this.month, #required this.year});
String getMonthName(int month) {
return _dbService.monthNames[month - 1];
}
#override
Widget build(BuildContext context) {
var expenses = Provider.of<List<Expense>>(context);
var categories = Provider.of<List<Category>>(context);
return expenses.length < 1
? Center(
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Text(
"You do not have any expenses for ${getMonthName(month)}, $year",
style: Theme.of(context).textTheme.subhead,
textAlign: TextAlign.center,
),
),
)
: ListView.builder(
itemBuilder: (BuildContext context, int index) {
var cat = categories.firstWhere(
(category) => category.id == expenses[index].category,
orElse: () => Category.initial(),
);
var sub = cat.subs[expenses[index].subcategory];
var amount = expenses[index].amount;
var date = expenses[index].date;
return ExpenseCard(
expenseCategory: cat.name,
expenseSubCategory: sub,
expenseAmount: amount,
expenseDate: _dateFormat.format(date),
cardColor: Theme.of(context).cardColor);
},
itemCount: expenses.length,
);
}
}
My UI is constantly at this
,even though I expect it to update with the data from the StreamProvider.
Related
I'm trying to use a ListTile widget inside a ListView that will change depending on some information received elsewhere in the app. In order to practice, I tried to make a little Dartpad example.
The problem is as follows: I have been able to change the booleans and data behind the items, but they don't update within the ListView.builder.
I have ListTile widgets inside the ListView that I want to have four different states as follows: idle, wait, requested, and speaking.
The different states look like this, as an example:
I am trying to change the individual state of one of these items, but I haven't been able to get them to properly update within the ListView.
The ListTile code looks like this. Most of this code is responsible for just handling what the UI should look like for each state:
class UserItem extends StatefulWidget {
String name;
UserTileState userTileState;
UserItem(this.name, this.userTileState);
#override
UserItemState createState() => UserItemState();
}
class UserItemState extends State<UserItem> {
String _getCorrectTextState(UserTileState userTileState) {
switch (userTileState) {
case UserTileState.speaking:
return "Currently Speaking";
case UserTileState.requested:
return "Speak Request";
case UserTileState.wait:
return "Wait - Someone Speaking";
case UserTileState.idle:
return "Idle";
}
}
Widget _getCorrectTrailingWidget(UserTileState userTileState) {
switch (userTileState) {
case UserTileState.speaking:
return const CircleAvatar(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
child: Icon(Icons.volume_up));
case UserTileState.requested:
return CircleAvatar(
backgroundColor: Colors.green.shade100,
foregroundColor: Colors.grey[700],
child: const Icon(Icons.volume_up),
);
case UserTileState.wait:
return CircleAvatar(
backgroundColor: Colors.green.shade100,
foregroundColor: Colors.grey[700],
child: const Icon(Icons.volume_off),
);
case UserTileState.idle:
return CircleAvatar(
backgroundColor: Colors.green.shade100,
foregroundColor: Colors.grey[700],
child: const Icon(Icons.volume_off),
);
}
}
void kickUser(String name) {
print("Kick $name");
}
#override
Widget build(BuildContext context) {
return ListTile(
onLongPress: () {
kickUser(widget.name);
},
title: Text(widget.name,
style: TextStyle(
fontWeight: widget.userTileState == UserTileState.speaking ||
widget.userTileState == UserTileState.requested
? FontWeight.bold
: FontWeight.normal)),
subtitle: Text(_getCorrectTextState(widget.userTileState),
style: const TextStyle(fontStyle: FontStyle.italic)),
trailing: _getCorrectTrailingWidget(widget.userTileState));
}
}
enum UserTileState { speaking, requested, idle, wait }
To try and trigger a change in one of these UserTile items, I wrote a function as follows. This one should make an idle tile become a requested tile.
void userRequest(String name) {
// send a speak request to a tile
int index = users.indexWhere((element) => element.name == name);
users[index].userTileState = UserTileState.requested;
}
I will then run that userRequest inside my main build function inside a button, as follows:
class MyApp extends StatefulWidget {
#override
MyAppState createState() => MyAppState();
}
class MyAppState extends State<MyApp> {
void userRequest(String name) {
// send a speak request to a tile
int index = users.indexWhere((element) => element.name == name);
users[index].userTileState = UserTileState.requested;
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(children: [
Expanded(
flex: 8,
child: ListView.separated(
separatorBuilder: (context, index) {
return const Divider();
},
itemCount: users.length,
itemBuilder: (context, index) {
return users[index];
})),
Expanded(
flex: 2,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
TextButton(
onPressed: () {
setState(() {
userRequest("Test1");
});
},
child: const Text("Send Request")),
]))
]));
}
}
When I tap the button to set the value within the first UserTile, nothing happens.
I don't know where to put setState, and the state of the object within the ListView isn't being updated. What is the most simple solution to this problem? Provider? I've used Provider in this same situation and can't get it to work. Is there another simpler solution to this?
How can I change update the state of a specific element within a ListView?
The code that I posted has had mixed results for some. My main project actually is actually a bit more complex and involves Provider, but I tried my best to strip the code down to its simplest parts when I posted this question. I'm wondering if something else is getting in the way of this working. (maybe it's because I threw this example together using dartpad?)
In the mean time, I've discovered that adding or removing an element to the list seems to change the entire state of the list. Using this technique, instead of trying to change an individual value, I get its index, remove it, and re-add it at that index and it works like a charm. So instead of this:
void userRequest(String name) {
// send a speak request to a tile
int index = users.indexWhere((element) => element.name == name);
users[index].userTileState = UserTileState.requested;
}
followed by setState(() => userRequest("name");, the following seems to work instead:
void userRequest(String name) {
// send a speak request to a tile
int index = users.indexWhere((element) => element.name == name);
users.removeAt(index);
users.insert(index, UserItem(name, UserTileStates.requested));
}
I believe the problem lies here in _getCorrectTextState()
Create a variable, String _status;
And do something like this;
void _getCorrectTextState(UserTileState userTileState) {
String tmpState;
switch (userTileState) {
case UserTileState.speaking:
tmpState = "Currently Speaking";
case UserTileState.requested:
tmpState = "Speak Request";
case UserTileState.wait:
tmpState = "Wait - Someone Speaking";
case UserTileState.idle:
tmpState = "Idle";
}
setState(() => {_state = tmpState});
}
then in your build:
subtitle: _state;
You can use StatefulBuilder() - by using this you will get a separate state for each item in your list, that can be updated
like this:
ListView.separated(
separatorBuilder: (context, index) {
return const Divider();
},
itemCount: users.length,
itemBuilder: (context, index) {
return StatefulBuilder(
builder: ((context, setStateItem){
//return_your_listTile_widget_here
//use setStateItem (as setState) this will update the state of selected item
setStateItem(() {
//code for update the listView item
});
})
);
})),
Am a completely new flutter dev. I am trying to save a document from a queried firestore list on another saved documents page like an add to cart functionality. Am passing doc id as arguments to another page from firestore so that I get data based on the previous selection. Now how can I send the firestore reference and save it to the other screen without navigating to it so that users are able to save their favorite docs on another page and access them? Here is my Assignment page that lists the docs based on the previous selection.
class Assignments extends StatelessWidget {
final String programId;
final String yearId;
final String semesterId;
final String courseId;
const Assignments(
{Key key, this.programId, this.yearId, this.semesterId,
this.courseId})
: super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomAppBar2(title: 'Assigment'.toUpperCase(), ),
body: Column(
children: [
Expanded(
child: ContentArea(
addPadding: false,
child: StreamBuilder(
stream:
getAssignment(programId, yearId, semesterId, courseId),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator(
color: kOnSurfaceTextColorYellow),
);
}
return ListView.separated(
padding: UIParameters.screenPadding,
shrinkWrap: true,
itemCount: snapshot.data.docs.length,
itemBuilder: (BuildContext context, int index) {
final data = snapshot.data.docs[index];
return DisplayCard(
title: data['nameOfAssignment'],
icon: Icons.add,
// Here is the action i want that should save the documment to
// the SavedPage empty list without navigating to it
onTapIconSave: (){}
onTap: () => Get.to(Pdf(
nameOfAssignment: data['nameOfAssignment'],
pdfUrl: data['pdfUrl'],
)),
);
},
separatorBuilder: (BuildContext context, int index) {
return const SizedBox(
height: 10,
);
},
);
})),
),
],
),
);
}
}
Here is the SavedPage which may be similar to the cart page. Am not sure what to do in order to save the Document from the Assignment Page in a Dynamic growable list
class Saved extends StatefulWidget {
const Saved({ Key key }) : super(key: key);
#override
State<Saved> createState() => _SavedState();
}
class _SavedState extends State<Saved> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: const CustomAppBar2(title: 'Saved'),
body: Column(
children: [],
),
);
}
}
You can add a state management package like Provider or Bloc, also you could save your data in your local database and access them from there. I recommend Provider, easy to use, and its what you need.
I am new to Flutter Bloc and must be missing how State changes are processed by the UI widgets. At the top level I have a BlocConsumer and under that I have nested BlocBuilder widgets with buildWhen methods to indicate when and how the Bloc widget should be rebuilt. Based on print statements,it looks like the Bloc state is consumed in the top level BlocConsumer widget and never makes it down to the lower level BlocBuilder widgets.
The code below should
Display circular progress bar on startup - this works ok
Call a bunch of APIs - This is happening
In the meantime display the initial screen with default text values in various widgets - this happens
As API returns and Bloc passes states on the stream, the appropriate UI widget should be rebuilt replacing default text with the data in the stream object. -- this doesn't happen.
Code snippets:
RaspDataStates issued by Bloc (Just showing for reference. Not showing all subclasses of RaspDataState):
#immutable
abstract class RaspDataState {}
class RaspInitialState extends RaspDataState {
#override
String toString() => "RaspInitialState";
}
class RaspForecastModels extends RaspDataState {
final List<String> modelNames;
final String selectedModelName;
RaspForecastModels(this.modelNames, this.selectedModelName);
}
...
Bloc just to show how initialized. Code all seems to work fine and isn't shown.
class RaspDataBloc extends Bloc<RaspDataEvent, RaspDataState> {
RaspDataBloc({required this.repository}) : super(RaspInitialState());
#override
RaspDataState get initialState => RaspInitialState();
...
Now to the UI widget.
class SoaringForecast extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocProvider<RaspDataBloc>(
create: (BuildContext context) =>
RaspDataBloc(repository: RepositoryProvider.of<Repository>(context)),
child: RaspScreen(repositoryContext: context),
);
}
}
class RaspScreen extends StatefulWidget {
final BuildContext repositoryContext;
RaspScreen({Key? key, required this.repositoryContext}) : super(key: key);
#override
_RaspScreenState createState() => _RaspScreenState();
}
class _RaspScreenState extends State<RaspScreen>
with SingleTickerProviderStateMixin, AfterLayoutMixin<RaspScreen> {
// Executed only when class created
#override
void initState() {
super.initState();
_firstLayoutComplete = false;
print('Calling series of APIs');
BlocProvider.of<RaspDataBloc>(context).add(GetInitialRaspSelections());
_mapController = MapController();
}
#override
void afterFirstLayout(BuildContext context) {
_firstLayoutComplete = true;
print(
"First layout complete. mapcontroller is set ${_mapController != null}");
_setMapLatLngBounds();
}
#override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
drawer: AppDrawer.getDrawer(context),
appBar: AppBar(
title: Text('RASP'),
actions: <Widget>[
IconButton(icon: Icon(Icons.list), onPressed: null),
],
),
body: BlocConsumer<RaspDataBloc, RaspDataState>(
listener: (context, state) {
print('In forecastLayout State: $state'); << Can see all streamed states here
if (state is RaspDataLoadErrorState) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Colors.green,
content: Text(state.error),
),
);
}
}, builder: (context, state) {
print('state is $state'); << Only see last streamed state here
if (state is RaspInitialState || state is RaspDataLoadErrorState) {
print('returning CircularProgressIndicator');
return Center(
child: CircularProgressIndicator(),
);
}
print('creating main screen'); << Only see this when all streams complete
return Padding(
padding: EdgeInsets.all(8.0),
child:
Column(mainAxisAlignment: MainAxisAlignment.start, children: [
getForecastModelsAndDates(),
getForecastTypes(),
displayForecastTimes(),
returnMap()
]));
}));
}
Widget getForecastModelsAndDates() {
return Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Expanded(
flex: 3,
child: forecastModelDropDownList(), // ForecastModelsWidget()
),
Expanded(
flex: 7,
child: Padding(
padding: EdgeInsets.only(left: 16.0),
child: forecastDatesDropDownList(),
)),
],
);
}
// Display GFS, NAM, ....
Widget forecastModelDropDownList() {
return BlocBuilder<RaspDataBloc, RaspDataState>(
buildWhen: (previous, current) {
return current is RaspInitialState || current is RaspForecastModels;
}, builder: (context, state) {
if (state is RaspInitialState || !(state is RaspForecastModels)) {
return Text("Getting Forecast Models");
}
var raspForecastModels = state;
print('Creating dropdown for models');
return DropdownButton<String>(
value: (raspForecastModels.selectedModelName),
isExpanded: true,
iconSize: 24,
elevation: 16,
onChanged: (String? newValue) {
BlocProvider.of<RaspDataBloc>(context)
.add(SelectedRaspModel(newValue!));
},
items: raspForecastModels.modelNames
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value.toUpperCase()),
);
}).toList(),
);
});
}
... more BlocBuilder child widgets similar to the one above
The print statements in the console are:
Calling series of APIs
state is RaspInitialState
returning CircularProgressIndicator
First layout complete. mapcontroller is set true
... (First of bunch of API output displays - all successful)
state is RaspInitialState << Not sure why this occurs again
returning CircularProgressIndicator
... (More API output displays - all successful)
streamed RaspForecastModels
In forecastLayout State: Instance of 'RaspForecastModels' << Doesn't cause widget to be rebuild
streamed RaspForecastDates << Other states being produced by Bloc
In forecastLayout State: Instance of 'RaspForecastDates'
streamed RaspForecasts
In forecastLayout State: Instance of 'RaspForecasts'
In forecastLayout State: Instance of 'RaspForecastTime'
streamed RaspMapLatLngBounds
In forecastLayout State: Instance of 'RaspMapLatLngBounds'
state is Instance of 'RaspMapLatLngBounds'
creating main screen
Any words of wisdom on the errors of my way would be appreciated.
I added this earlier as a comment but then found Stackoverflow didn't initially show my comment (I needed to click on show more). So here it is in better readable form.
Problem solved. I needed to move the line:
BlocProvider.of<RaspDataBloc>(context).add(GetInitialRaspSelections());
from the initState() method to afterFirstLayout().
All blocbuilders then executed and the UI was built appropriately . And to answer my title question, the bloc states are broadcast and can be picked up by different BlocBuilders.
I am new in Flutter and I am facing a problem.
What I have so far is that I am getting the data from a web service asynchronously (Json). So in the build method I use a FutureBuilder widget for Scaffold's body argument. And its 'builder' argument (of FutureBuilder widget) returns a ListView.builder to show the data.
Also I use ScrollController for scrolling.
Its time I reach the maximum scroll point I call once again the service to get the next page of data and so on...
The problem is that when I "change page" by scrolling it brings the data correctly, but it starts from the very first line of data and not from the point where i ve reached the maxScrollExtent point.
So if the first 'snapshot' has 25 rows and the second has 15, when I go to the second it starts from row 1 and not from row 26 although the total amount of data row is 40 correctly .
As a result i don't have a smooth scrolling. I have been stuck for enough time to this point and i do not know what I am missing. Do I have to use Page Storage keys (i saw video from flutter team but i haven't found an edge to my problem). Any hints? A sample of code follows.
class _MyAppState extends State<MyApp> {
final ScrollController _controller = ScrollController();
#override
void initState() {
super.initState();
_controller.addListener(_scrollListener);
}
void _scrollListener() {
if (_controller.offset >= _controller.position.maxScrollExtent &&
!_controller.position.outOfRange) {
if (nextDataSet != null) {
print('nextDataSet = ' + nextDataSet);
setState(() {
inDataSet = nextDataSet;
});
} else {
print('nextDataSet = NULL');
}
}
Future<Post> fetchPost([String dataSet]) async {
...
return Post.fromJson(json.decode(utf8.decode(response.bodyBytes)));
}
Widget build(BuildContext context) {
return MaterialApp(
home: Builder(
builder: (context) => Scaffold(
...
body: FutureBuilder<Post>(
future: fetchPost(inDataSet),
builder: _buildList,
),
),
),
);
}
Widget _buildList(BuildContext context, AsyncSnapshot snapshot) {
... /*code that eventually fills the lists of strings
progressivePartyListSec, progressivePartyListThird*/
...
return ListData(controller: _controller, listEidosPerigr:
progressivePartyListSec, listPerigr: progressivePartyListThird );
}
}
class ListData extends StatelessWidget {
final ScrollController controller;
final List<String> listEidosPerigr;
final List<String> listPerigr;
ListData({this.controller,
this.listEidosPerigr,
this.listPerigr});
#override
Widget build(BuildContext context) {
return ListView.builder(
controller: controller,
itemCount: rowsSelected,
itemBuilder: (context, i) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
removeAllHtmlTags(listEidosPerigr[i]),
style: TextStyle(
fontWeight: FontWeight.bold, color: Colors.blue),
),
new RichText(
text: new TextSpan(
text: listPerigr[i].replaceAllMapped('<BR>', (Match m) => '').replaceAllMapped('<b>', (Match m) => '').replaceAllMapped('</b>', (Match m) => ''),
style: DefaultTextStyle.of(context).style,
),
),
]);
});
}
}
after some research i found a solution in the following link: Flutter: Creating a ListView that loads one page at a time
Thanks a lot AbdulRahman AlHamali.
Basically, on the above code that i have posted, i used another argument for the ListView.builder and it is the following key: PageStorageKey('offset'),
Finally as AbdulRahman writes on his article i used the KeepAliveFutureBuilder as a wrapper of FutureBuilder in other worlds in my build method i did...
body: KeepAliveFutureBuilder(
//child: FutureBuilder<Post>(
future: fetchPost(inDataSet),
builder: _buildList,
//),
),
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.