Flutter Navigation stack with Drawer - flutter

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.

Related

Flutter change state of an item in a ListView

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

Moving from Provider to GetX and cannot Get.find the correct Controller

I'm trying to move from Provider to GetX and am stuck on how GetX.find() works.
We have a restaurant app with a few tables. Below is the narrowed down view of my old Provider code showing just the staff table and VIP table. You click on the "Seat" button to mark the table as seated.:
Widget build(BuildContext context) {
return MaterialApp(
title: 'Restaurant',
home: Column(children: [
ChangeNotifierProvider<Table>( //Provider
create: (_) => Table("Staff"), //create controller
child: Builder(builder: (context) {
return Column(children: [
Text("Staff seated: ${context.watch<Table>().seated}"), //consume changes
ElevatedButton(
child: const Text("Seat Staff"),
onPressed: () {
context.read<Table>().toggleSeated(); //call controller
}),
]);
}),
),
ChangeNotifierProvider<Table>(. //Provider
create: (_) => Table("VIP"), //create controller
child: Builder(
builder: (context) {
return Column(children: [
Text("VIP seated: ${context.watch<Table>().seated}"), //consume changes
ElevatedButton(
child: const Text("Seat VIP"),
onPressed: () {
context.read<Table>().toggleSeated(); //call controller
}),
]);
},
)),
]),
);
}
}
and associated narrowed down controller:
class Table extends ChangeNotifier {
final String name;
int chairs = 0;
bool seated = false;
Table(this.name);
toggleSeated() {
seated = !seated;
notifyListeners(); //notify providers
}
}
Now here is the GetX code (which compiles fine) but I tried t.toggle and Get.find<Table>().toggle and both functions toggle both tables at the same time. In the old code they toggled the tables individually:
Widget build(BuildContext context) {
return MaterialApp(
title: 'Restaurant',
home: Column(
children: [
GetBuilder( //GetX
init: Table("Staff"), //create controller
builder: (Table t) {
return Column(children: [
Text("Staff seated: ${t.seated}"), //consume changes
ElevatedButton(
child: const Text("Seat Staff"),
onPressed: () {
//I've tried Get.find here
Get.find<Table>().toggleSeated(); //call controller
},
),
]);
},
),
GetBuilder(. //GetX
init: Table("VIP"), //create controller
builder: (Table t) {
return Column(children: [
Text("VIP seated: ${t.seated}"), //consume changes
ElevatedButton(
child: const Text("Seat VIP"),
onPressed: () {
//I've also tried directly using the controller
t.toggleSeated(); //call controller
},
),
]);
},
),
],
));
}
and associated narrowed down controller:
class Table extends GetxController {
final String name;
int chairs = 0;
bool seated = false;
Table(this.name);
toggleSeated() {
seated = !seated;
update(); //notify GetX
}
}
In the Provider version I can interact with the tables individually but in the GetX version the button seems to act on all tables regardless of whether I use Get.find<Table>() or directly using the controller's t.toggle.
Controllers are created per type, but you can add 'tag' to create separate controllers. Also you need to add the controller to Get to have proper lifecycle:
Get.put(Table())
Like you have 'Staff' and 'VIP' you can pass that as a tag:
Get.put(Table(), tag: 'Staff')
GetX Controllers automatically released when the caller disposed so in your case when you leave the given page.
You can find the 'Staff' Controller downstream with Get.find(tag:'Staff');
You need to use an instance of Table just like package example. you can't directly use it on your screen.
Controller controller = Get.put(Table());
// ↑ declare controller inside build method
You can check Baker's answer too.

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

Passing data between pages in Flutter. Empty error

good day to everyone.
I'm developing an app, and I need to pass a String type variable that is within a list, this String is the name of a file that I use on the next page. I tried to pass this variable but I could not. Before sending it I print it and there it is, but when I receive it on the other page it is empty, and this causes me an error. Could anyone help me?
This is the part of the code where I pass the previously declared variable:
_verifyPurchase(var id,bool i, String book) {
if (user_item.contains(id) & i) {
return "Comprado";
}else if(!user_item.contains(id) & i){
return "No comprado";
}
if (user_item.contains(id) & !i) {
return new FlatButton(
child: new Text("Leer"),
onPressed: () {
print(book);
Navigator.push(
context,
new MaterialPageRoute(
builder: (context) => new ReadBook(bookName: book,)));
},
);
//return IconButton(icon: Icon(Icons.library_books),color: Colors.green, onPressed: (){print(id);});
}else if(!user_item.contains(id) & !i){
return new FlatButton(child: new Text("Comprar"),onPressed: () {print(id);},);
//return IconButton(icon: Icon(Icons.shopping_cart), color: Colors.black, onPressed: (){print(id);});
}
}
In this part, I call the function:
content: new Text("Book"),
actions: <Widget>[
// usually buttons at the bottom of the dialog
new FlatButton(
child: new Text("Ok"),
onPressed: () {
Navigator.of(context).pop();
},
),
_verifyPurchase(id_book[pos], false, file_book[pos])
],
)
This is the second-page code where I receive the variable:
class ReadBook extends StatefulWidget {
ReadBook({this.bookName});
final String bookName;
#override
_ReadBookState createState() => _ReadBookState();
}
These pages are not part of the Main, they are other pages, so I have not been able to use routes. Although it is the first time that I program in Dart and Flutter.
Thanks for your help.
To get the value of bookName from within the State which I assume you have as you have a Stateful widget above. Add widget.bookName within the build method
#override
Widget build(BuildContext context) {
var book = widget.bookName;
return Scaffold(
body: Container(child: Text(book));
)
}

How to change the background color of a button dynamically in onPressed()

I have a list of Raised buttons, I want the background color of the selected button to change in its onPressed()
I tried changing the color in setState but it doesn't do anything.
This is the function that generates the list of Buttons
List<Widget> _makeZoneList(List<Zone> zones) {
List<Widget>Buttons = new List();
for (int i = 0; i < zones.length; i++) {
Buttons.add(RaisedButton(
color: zones[i].isSelected ? AppColors.primaryColor : AppColors.white,
onPressed: () {
setState(() {
if (zones[i].isSelected){
zones[i].isSelected = false;
}
else{
zones[i].isSelected = true;
}
print(zones[i].isSelected.toString());
});
},
child: Text(zones.elementAt(i).text)
));
}
return Buttons;
}
This is where I call the function
Widget _zoneBody() {
return Padding(
padding: EdgeInsets.all(32),
child: StreamBuilder<List<Zone>>(
stream: GetterBloc.zonesStream,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return new Container();
} else {
if (snapshot.hasData) {
return Wrap(
spacing: 6.0, // gap between adjacent chips
children: _makeZoneList(snapshot.data));
} else {
return new Container();
}
}
}));
}
When I press any button, its isSelected value changes but the background doesn't change accordingly
Updated answer (ElevatedButton)
Since RaisedButton is now deprecated, use ElevatedButton:
Code:
class _MyState extends State<MyPage> {
bool _flag = true;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () => setState(() => _flag = !_flag),
child: Text(_flag ? 'Red' : 'Green'),
style: ElevatedButton.styleFrom(
backgroundColor: _flag ? Colors.red : Colors.teal, // This is what you need!
),
),
),
);
}
}
Old answer
It was difficult to implement your code because of undefined classes and variable, however I created a small example which will help you what you are looking for.
List<bool> _list = [true, false, true, false];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Title")),
body: ListView(children: _buildButtons()),
);
}
List<Widget> _buildButtons() {
List<Widget> listButtons = List.generate(_list.length, (i) {
return RaisedButton(
color: _list[i] ? Colors.green : Colors.red,
onPressed: () {
setState(() {
_list[i] = !_list[i];
});
},
child: Text("Button #${i}"),
);
});
return listButtons;
}
Output:
Unless I'm mistaken, what you're trying to do is already handled by flutter. I think all you have to do is set the hightlightColor of the button and when it is pressed it will change to that color. And you could set this into the theme for your entire application so that all buttons behave the same rather then setting it for each individual button.
However, there's also a reason why what you're doing isn't working. You haven't included quite enough code for me to tell, but I believe the reason why what you're doing isn't working is that you have a List of data that you're mutating when the button is pressed (i.e. zones[i].isSelected = false;). You're doing that in a setState, but the way that flutter checks whether something needs rebuilding is by doing an equality compare on the State's members (i.e. it will check whether zones == zones).
Because 'zones' is just a list, and is actually the same list in for the old state and the new state, flutter will assume nothing has changed and won't bother rebuilding.
There's two easy ways to get around this. One would be to make a copy of the list each time it is modified and set the zones member to that, so that when flutter does the compare old.zones != new.zones. The other way would be to keep a separate object that you change each time the list is modified (I tend to use an integer called changeCounter that I increment each time the list changes) as that way you can 'fool' flutter into rebuilding.
Here is a very simple approach.
bool isButtonPressed = false;
RaisedButton(
color: isButtonPressed ? Colors.green : Colors.red,
onPressed: () {
setState(() {
isButtonPressed =!isButtonPressed;
});
},
),
What you can do is create a "color" property (not required to set any value for it) in Zone class. Then set the color property of RaisedButton to zone.color ??= AppColors.primaryColor.
And now, inside onPressed function you can check if !zone.isSelected then set zone.color = Colors.white.
Below is the implementation for creating RaisedButton. You can also check full implementation here
List<Widget> _createButton(BuildContext context, List<Zone> zones) {
return zones.map((Zone zone) {
return RaisedButton(
onPressed: () {
setState(() {
if (!zone.isSelected) {
zone.color = AppColors.white;
}
});
},
color: zone.color ??= Theme.of(context).primaryColor,
child: Text(zone.text),
);
}).toList();
}