Flutter change state of an item in a ListView - flutter

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

Related

Flutter when ListView.builder is refreshed it goes back from the beggining and not from the maxScrollExtent point

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,
//),
),

How to create a parent listview that handles selecting items etc.?

I'm trying to create a base listview, that takes care of clicking on a listtile and selecting items.
It then shows a checkbox per item and keeps track of how many items have been selected.
All my lists are going to need this feature so it makes sense to put this inside some base class.
But how would I do this using flutter?
The following code contains part of the logic:
Widget build(BuildContext context) {
return ListView.separated(
padding: const EdgeInsets.all(8),
separatorBuilder: (context, index) => Divider(
color: Colors.grey,
),
itemCount: widget.models.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(onLongPress: () {
selectMode = true;
widget.models[index].selected = true;
selectCount++;
setState(() {});
}, onTap: () {
if (selectMode) {
if (widget.models[index].selected) {
widget.models[index].selected = false;
selectCount--;
} else {
widget.models[index].selected = true;
selectCount++;
}
if (selectCount == 0) {
selectMode = false;
}
setState(() {});
return;
}
});
},
);
}
I have no idea how to continue from here. This obviously works perfectly fine when putting it all inside one specific list. But it makes much more sense to have it in a base class and simply inherit this behaviour.
Is it possible to move this logic into a base class and still be able to define different ListTiles for other lists?
I read that you are not supposed to inherit widgets in flutter at all, so I really don't know how to proceed, but there certainly must be a better way than having to copy/paste this logic into every new list.
Edit:
ListTile showing a checkbox based on selectMode:
ListTile(
onLongPress: () {
},
leading: CircleAvatar(
radius: 30,
backgroundColor: Color.fromARGB(255, 33, 84, 158),
child: Icon(Icons.location_on, color: Colors.white, size: 40),
),
title: Text('Title $index'),
subtitle: Text('${models[index]}'),
trailing: Visibility(
visible: selectMode,
child: models[index].selected == true
? Icon(Icons.check_box, color: Color.fromARGB(255, 33, 84, 158))
: Icon(Icons.check_box_outline_blank,
color: Color.fromARGB(255, 33, 84, 158))),
onTap: () {
});
This is another thing that should be part of the "base", else you would have to paste this in every listview you create. So when I have some custom listview widget, which already has a listTile built in to handle to clicking and showing the checkbox, how would a new listview be able to have a different styled listTile, possibly without a leading icon etc.?
I have created a little DartPad to show how it works:
https://dartpad.dev/98d3f9b01c5b048d90aad3467aa3954e
Now image you want the same behaviour in every list you ever create, without having to write the same code again and still be able to use different kind of listTiles, or even add actions to onTap event etc.
I'm not sure I completely understand what you're trying to achieve, but from what I do understand, it seems you are trying to reuse a particular widget.
To do this, all you have to do is create a new Stateful widget that contains only the code for the ListView. Then to make sure it is "dynamic" (i.e, it can use different lists of Objects/Models), you'll have to create a parameter in its constructor.
Also, if you want to access the total from another widget, you'll need to use a callback.
class MyCustomSelectableListView extends StatefulWidget {
// Variable for your list of models. Replace Object with your Type
final List<Object> models;
// Callback to get the value of selectCount from another widget.
final Function(int) getSelectCount;
// Passing the variables through a constructor.
const MyCustomSelectableListView({this.models, this.getSelectCount, Key key})
: super(key: key);
#override
_MyCustomSelectableListView createState() => _MyCustomSelectableListView();
}
class _MyCustomSelectableListView extends State<MyCustomSelectableListView> {
// Your variables
bool selectMode = false;
int selectCount = 0;
#override
Widget build(BuildContext context) {
return ListView.separated(
padding: const EdgeInsets.all(8),
separatorBuilder: (context, index) => Divider(
color: Colors.grey,
),
itemCount: widget.models.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(onLongPress: () {
selectMode = true;
widget.models[index].selected = true;
selectCount++;
setState(() {});
}, onTap: () {
if (selectMode) {
if (widget.models[index].selected) {
widget.models[index].selected = false;
selectCount--;
// Using the callback
widget.getSelectCount(selectCount);
} else {
widget.models[index].selected = true;
selectCount++;
// Using the callback
widget.getSelectCount(selectCount);
}
if (selectCount == 0) {
selectMode = false;
}
setState(() {});
return;
}
});
},
);
}
}
Reusing this piece of code:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
List<Object> myListOfModels = [Object(), Object()];
return Scaffold(
body: MyCustomSelectableListView(
models: myListOfModels,
getSelectCount: (count) {
print(count);
}));
}
}

How to properly initialize a Future in Flutter Provider

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.

Flutter Navigation stack with Drawer

I'm building a multi-page application using Flutter, and it would appear that I'm handling the navigation incorrectly. I've noticed that, as I navigate in between screens, it seems to just keep pushing pages onto my stack, and "evaluates" the whole stack each time. I have an app drawer widget that I include on all of my pages that looks like this:
#override
Widget build(BuildContext context) {
AuthenticationContext authenticationContext =
AuthenticationContext.of(context);
auth = authenticationContext.auth;
var drawerOptions = <Widget>[];
for (var i = 0; i < drawerItems.length; i++) {
var d = drawerItems[i];
drawerOptions.add(new ListTile(
leading: new Icon(d.icon),
title: new Text(d.title),
selected: i == _selectedDrawerIndex,
onTap: () => _onSelectItem(i),
));
}
return new Drawer(
child: new Column(
children: <Widget>[
UserAccountsDrawerHeader(
accountName: Text(widget.name != null ? widget.name : ""),
accountEmail: Text(widget.email),
currentAccountPicture: CircleAvatar(
child: new Text(
widget.photoUrl == null ? widget.email[0].toUpperCase() : ""),
),
),
new Column(children: drawerOptions)
],
),
);
}
_getDrawerItemWidget(int pos) {
switch (pos) {
case 0:
return new HomePage();
case 1:
return new UserPantryPage();
case 2:
return new ShoppingListPage();
case 3:
return new FavoritesPage();
case 4:
auth.signOut();
return new RootPage();
default:
return new Text("Error");
}
}
_onSelectItem(int index) {
Navigator.of(context).pop();
Navigator.of(context)
.push(MaterialPageRoute<Null>(builder: (BuildContext context) {
return _getDrawerItemWidget(index);
}));
}
I have debug print statements in each of my page Widgets, and if I start clicking around on the drawer to navigate in between pages I notice that it'll start printing debug statements from, for example, the HomePage, even when I'm navigating to a completely separate page. I initially noticed it because one of my pages calls a few APIs to get data in its build method, and the more I used the app, the more the APIs were getting called. I traced it down to the fact that it appears that the build method of all of the page Widgets is getting called even when I'm going to another page Is there something I'm doing very wrong here in terms of navigating in a multi-page app?
Try the following pseudo code. Problem is that build method can be called multiple times during the apps life cycle, so to avoid your code being executed multiple times you move it into the initial state which is only ever called once, either using the override method or as the example below shows. more info here - https://flutterbyexample.com/stateful-widget-lifecycle/
class NavigationDraw extends StatefulWidget {
NavigationDraw({Key key, this.email, this.name}) : super(key:key);
final String email;
final String name;
#override
_NavigationDrawState createState() =>
new _NavigationDrawState();
}
class _NavigationDrawState extends State<NavigationDraw> {
BuildContext _ctx;
var navi;
_NavigationDrawState () {
navi = sideNavi();
}
#override
Widget build(BuildContext context) {
return navi;
}
sideNavi(){
_getDrawerItemWidget(int pos) {
switch (pos) {
case 0:
return new HomePage();
case 1:
return new UserPantryPage();
case 2:
return new ShoppingListPage();
case 3:
return new FavoritesPage();
case 4:
auth.signOut();
return new RootPage();
default:
return new Text("Error");
}
}
_onSelectItem(int index) {
Navigator.of(context).pop();
Navigator.of(context)
.push(MaterialPageRoute<Null>(builder: (BuildContext context) {
return _getDrawerItemWidget(index);
}));
}
AuthenticationContext authenticationContext = AuthenticationContext.of(context);
auth = authenticationContext.auth;
var drawerOptions = <Widget>[];
for (var i = 0; i < drawerItems.length; i++) {
var d = drawerItems[i];
drawerOptions.add(
new ListTile(
leading: new Icon(d.icon),
title: new Text(d.title),
selected: i == _selectedDrawerIndex,
onTap: () => _onSelectItem(i),
));
}
return new Drawer(
child: new Column(
children: <Widget>[
UserAccountsDrawerHeader(
accountName: Text(widget.name != null ? widget.name : ""),
accountEmail: Text(widget.email),
currentAccountPicture: CircleAvatar(
child: new Text(
widget.photoUrl == null ? widget.email[0].toUpperCase() : ""),
),
),
new Column(children: drawerOptions)
],
),
);
}
When you navigate around, Flutter plays the animation of the transition of screens. Because of this, the widgets being animated keep rebuilding as they might change visually during the animation. With the default fade & slide animation, you tend to get 3 screens building at the same time - the old one fading away, the new one fading in, and the one below the old one which is slightly visible because of the other two being partially transparent while they fade.
The solution to your problem (and the way to work with Flutter in general) is to move everything that doesn't have to do with the UI out of the build function. If you need to fetch some data when a widget is initialized, move that code to the initState function of your StatefulWidget.

Using provider package StreamProvider with declarative UI

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.