I want to mark the ListTile of the current page as selected but 2 days ago I'm looking for a general way to do it.
I saw examples like this one where you hardcode the tile ID and use a case to know which is the current Tile. My question is, what if I have, to exaggerate, 100 ListTiles? How do I change the selected attribute programmatically to the selected Tile? Or a more real case: I have a Drawer that changes shape in each release, so keeping a code with the hardcoded IDs is not useful. I hope you understand the idea.
I've been trying different solutions for days but none seems general enough to me.
Simple create enum class like below.
enum DrawerSelection { home, favorites, settings}
Create enum object and pass pre-defined value if you want, in my case i pass home as selected ListTile item. Like below code.
class _MyHomePage extends State<MyHomePage> {
DrawerSelection _drawerSelection = DrawerSelection.home;
Then in ListTile use selected property and change enum onTap() like below code.
ListTile(
selected: _drawerSelection == DrawerSelection.home,
title: Text('Home'),
leading: Icon(Icons.home),
onTap: () {
Navigator.pop(context);
setState(() {
_drawerSelection = DrawerSelection.home;
_currentWidget = MainWidget();
_appBarTitle = Text("Home");
});
},
),
ListTile(
selected: _drawerSelection == DrawerSelection.favorites,
title: Text('Favorites'),
leading: Icon(Icons.favorite),
onTap: () {
Navigator.pop(context);
setState(() {
_drawerSelection = DrawerSelection.favorites;
_currentWidget = FavoritesWidget();
_appBarTitle = Text("Favorites");
});
},
),
ListTile(
selected: _drawerSelection == DrawerSelection.settings,
title: Text('Settings'),
leading: Icon(Icons.settings),
onTap: () {
Navigator.pop(context);
setState(() {
_drawerSelection = DrawerSelection.settings;
_currentWidget = SettingsWidget();
_appBarTitle = Text("Settings");
});
},
),
i think it is just simple you can create a new class that have your data and the bool selected
class Post {
final bool selected;
var data;
Post({
this.selected,
this.data
});
}
now when you use LIstView.builder in the itemBuilder if list[index].selected is true then set the color to blue if not then let it white or whatever in the on tap or onpressed whatever you are you using save the last clicked index in a global variable(called savedIndex)-initialize it with (-1)- and change selected to true at the this list index,then if savedIndex wasnt equal -1 change list[savedIndex].selected=false.
global variable
int selectedIndex =-1;
and itemBuilder.
itemBuilder: (BuildContext _context, int i) {
return GestureDetector(
child:
Container(
decoration: new BoxDecoration(
borderRadius: new BorderRadius.circular(16.0),
color:_list[index].selected? Colors.blue:colors.white,
),
child: _buildRow(_list[index]),) ,
onTap: () {
setState(){
if(savedIndex!=-1){
list[savedIndex].selected=false
}
_list[index].selected=true;
savedIndex=index;
}
}
);
}
Related
Hi I am new to flutter and have been going through flutter's udacity course building their unit converter app to try to learn about the framework. I was attempting to architecture the app using bloc but have ran into an issue with my dropdown menu. Every time when I change the item in the dropdown it resets back to the default value when focusing on the text input field. It looks like the widget tree i rebuilt when focusing on a textfield. The default units are the reset because in my bloc constructor I have a method to set default units. I am at a loss for where I would move my default units method so that it does not conflict. What should I do in my bloc to set default units only when a distinct category is set, and when it is first being built.
I tried using _currentCatController.stream.distinct method to only update the stream when distinct data is passed but that did not seem to work either. I tried to wrap the default units method in various conditional statements that did not give me the result I wanted.
you can find all the source here https://github.com/Renzo-Olivares/Units_Flutter
class _ConverterScreenState extends State<ConverterScreen> {
///function that creates dropdown widget
Widget _buildDropdown(
bool selectionType, ValueChanged<dynamic> changeFunction) {
print("build dropdown");
return Padding(
padding: const EdgeInsets.symmetric(vertical: 15.0),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.black, style: BorderStyle.solid),
borderRadius: BorderRadius.circular(4.0)),
child: DropdownButtonHideUnderline(
child: ButtonTheme(
alignedDropdown: true,
child: StreamBuilder<Unit>(
stream: _conversionBloc.inputUnit,
initialData: widget._category.units[0],
builder: (context, snapshotIn) {
return StreamBuilder<Unit>(
stream: _conversionBloc.outputUnit,
initialData: widget._category.units[1],
builder: (context, snapshotOut) {
return StreamBuilder<Category>(
stream: _conversionBloc.currentCategory,
initialData: widget._category,
builder: (context, snapshotDropdown) {
return DropdownButton(
items: snapshotDropdown.data.units
.map(_buildDropdownItem)
.toList(),
value: selectionType
? snapshotIn.data.name
: snapshotOut.data.name,
onChanged: changeFunction,
isExpanded: true,
hint: Text("Select Units",
style: TextStyle(
color: Colors.black,
)),
);
});
});
})),
),
),
);
}
}
class ConversionBloc {
//input
final _currentCatController = StreamController<Category>();
Sink<Category> get currentCat => _currentCatController.sink;
final _currentCatSubject = BehaviorSubject<Category>();
Stream<Category> get currentCategory => _currentCatSubject.stream;
ConversionBloc() {
print("conversion bloc");
//category
_currentCatController.stream.listen((category) {
print("setting category ${category.name}");
_category = category;
_currentCatSubject.sink.add(_category);
//units default
setDefaultUnits(_category);
});
}
void setDefaultUnits(Category category) {
print("setting default units for ${category.name}");
_inputUnits = category.units[0];
_outputUnits = category.units[1];
_inputUnitSubject.sink.add(_inputUnits);
_outputUnitSubject.add(_outputUnits);
}
}
The issue here is that the DropdownButton value wasn't updated on onChanged. What you can do here is handle the value passed from onChanged and update the DropdownButton value. Also, focusing on a Widget displayed on screen shouldn't rebuild Widget build().
I'm making a project with 4 different pages. I use a "BottomNavigationBar" widget to navigate to each page. When an icon in the "BottomNavigationBar" is pressed, I display a different page in the Scaffold body. I don't use any routing so when a user presses back on Android, the app closes. Something I don't want happening.
All the guides I have found reload the whole "Scaffold" when navigating, but I want to only update the "body" property of the "Scaffold" widget. When a Navigation.pop() occurs I again only want the "Scaffold" body to change.
I have found a post around the same issue, but the answer didn't work for me.
Another workaround I can try is making a custom history list, that I then update when pages are changed. Catching OnWillPop event to update the pages when the back button is pressed. I haven't tried this because I feel like there has to be a better way.
The Scaffold widget that displays the page.
Widget createScaffold() {
return Scaffold(
backgroundColor: Colors.white,
appBar: EmptyAppBar(),
body: _displayedPage,
bottomNavigationBar: createBottomNavigationbar(),
);
}
The BottomNavigationBar widget.
Widget createBottomNavigationbar() {
return BottomNavigationBar(
type: BottomNavigationBarType.fixed,
currentIndex: _selectedIndex,
onTap: _onItemTapped,
items: [
BottomNavigationBarItem(
icon: new Icon(Icons.home,
color: _selectedIndex == 0 ? selectedColor : unselectedColor),
title: new Text('Home',
style: new TextStyle(
color:
_selectedIndex == 0 ? selectedColor : unselectedColor)),
),
BottomNavigationBarItem(
icon: new Icon(Icons.show_chart,
color: _selectedIndex == 1 ? selectedColor : unselectedColor),
title: new Text('Month',
style: new TextStyle(
color:
_selectedIndex == 1 ? selectedColor : unselectedColor)),
),
BottomNavigationBarItem(
icon: Icon(Icons.history,
color: _selectedIndex == 2 ? selectedColor : unselectedColor),
title: Text('History',
style: new TextStyle(
color: _selectedIndex == 2
? selectedColor
: unselectedColor))),
BottomNavigationBarItem(
icon: Icon(Icons.settings,
color: _selectedIndex == 3 ? selectedColor : unselectedColor),
title: Text('Settings',
style: new TextStyle(
color: _selectedIndex == 3
? selectedColor
: unselectedColor)))
],
);
}
Methods that update the state of the displayed page.
void _onItemTapped(int index) {
_changeDisplayedScreen(index);
setState(() {
_selectedIndex = index;
});
}
void _changeDisplayedScreen(int index) {
switch (index) {
case 0:
setState(() {
_displayedPage = new LatestReadingPage();
});
break;
case 1:
setState(() {
_displayedPage = new HomeScreen();
//Placeholder
});
break;
case 2:
setState(() {
_displayedPage = new HomeScreen();
//Placeholder
});
break;
case 3:
setState(() {
_displayedPage = new HomeScreen();
//Placeholder
});
break;
default:
setState(() {
_displayedPage = new LatestReadingPage();
});
break;
}
}
}
What I want is to be able to use the Flutter Navigation infrastructure, but only update the body property of the Scaffold widget when changing pages. Instead of the whole screen.
A lot like the Youtube app or Google news app.
I have added an answer to the post that you linked: https://stackoverflow.com/a/59133502/6064621
The answer below is similar to the one above, but I've also added unknown routes here:
What you want can be achieved by using a custom Navigator.
The Flutter team did a video on this, and the article they followed is here: https://medium.com/flutter/getting-to-the-bottom-of-navigation-in-flutter-b3e440b9386
Basically, you will need to wrap the body of your Scaffold in a custom Navigator:
class _MainScreenState extends State<MainScreen> {
final _navigatorKey = GlobalKey<NavigatorState>();
// ...
#override
Widget build(BuildContext context) {
return Scaffold(
body: Navigator(
key: _navigatorKey,
initialRoute: '/',
onGenerateRoute: (RouteSettings settings) {
WidgetBuilder builder;
// Manage your route names here
switch (settings.name) {
case '/':
builder = (BuildContext context) => HomePage();
break;
case '/page1':
builder = (BuildContext context) => Page1();
break;
case '/page2':
builder = (BuildContext context) => Page2();
break;
default:
throw Exception('Invalid route: ${settings.name}');
}
// You can also return a PageRouteBuilder and
// define custom transitions between pages
return MaterialPageRoute(
builder: builder,
settings: settings,
);
},
),
bottomNavigationBar: _yourBottomNavigationBar,
);
}
}
Within your bottom navigation bar, to navigate to a new screen in the new custom Navigator, you just have to call this:
_navigatorKey.currentState.pushNamed('/yourRouteName');
If you don't used named routes, then here is what you should do for your custom Navigator, and for navigating to new screens:
// Replace the above onGenerateRoute function with this one
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute(
builder: (BuildContext context) => YourHomePage(),
settings: settings,
);
},
_navigatorKey.currentState.push(MaterialPageRoute(
builder: (BuildContext context) => YourNextPage(),
));
To let Navigator.pop take you to the previous view, you will need to wrap the custom Navigator with a WillPopScope:
#override
Widget build(BuildContext context) {
return Scaffold(
body: WillPopScope(
onWillPop: () async {
if (_navigatorKey.currentState.canPop()) {
_navigatorKey.currentState.pop();
return false;
}
return true;
},
child: Navigator(
// ...
),
),
bottomNavigationBar: _yourBottomNavigationBar,
);
}
And that should be it! No need to manually handle pop too much or manage a custom history list.
wrap your body inside the IndexedStack widget. Like this:
body: IndexedStack(
index: selectedIndex,
children: _children, //define a widget children list
),
Here's a link https://medium.com/flutter/getting-to-the-bottom-of-navigation-in-flutter-b3e440b9386
Have you tried this?
return Scaffold(
backgroundColor: Colors.white,
appBar: EmptyAppBar(),
body: _displayedPage[_selectedIndex],
bottomNavigationBar: _createBottomNavigationbar,
);
Then you make a widget list of all the Screens you want:
List<Widget> _displayedPage = [LatestReadingPage(), HomeScreen(), ExampleScreen()];
The full page code is very long but my DropdownButton widget code like this.
The problems are,
first: I can't update my selectedCity, it doesn't get an update. Also, the print function calls null, since my cityList data is like [new york, paris, london] etc...
second: flutter doesn't change focus from any TextField to DropdownButton fully. I mean, clicked TextField, then DropdownButton but focus reverts to that TextField after the button click. It is default action of Flutter?
List<dynamic> _cityList;
String _selectedCity;
#override
Widget build(BuildContext context) {
return DropdownButton(
value: _selectedCity,
style: TextStyle(
fontSize: 11,
color: textColor,
),
items: _cityList.map((city) {
return DropdownMenuItem<String>(
child: Padding(
padding: const EdgeInsets.only(left: 4),
child: Text(city),
),
);
}).toList(),
onChanged: (String value) {
setState(() {
_selectedCity = value;
print(_selectedCity);
});
},
isExpanded: true,
);
}
Edit: The solution of resetting FocusNode after selecting an item from DropdownMenuItem is adding this line inside of setstate like this:
this: FocusScope.of(context).requestFocus(new FocusNode());
to here: onChanged:(){setSate((){here}}
I hope it will help you. I have modified your code a little bit
List<dynamic> _cityList;
String _selectedCity;
It will show the Dropdown Button and when you click on it and select any value showing in the print
#override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
body: ListView(
children: [
Column(
children: <Widget>[
DropdownButton<String>(
items: _cityList.map((dynamic value) {
return DropdownMenuItem<String>(
value: value,
child: new Text(value),
);
}).toList(),
onChanged: (value) {
setState(() {
_selectedCity = value;
print(_selectedCity);
});
},
),
],
),
],
),
);
}
for the focus problem you should use focusNodes one with the drop down list and another with the text field https://docs.flutter.io/flutter/widgets/FocusNode-class.html.
I have a code, that uses dismissible in the listview (showes items from database). After dismissing an item it is supposed to show snackbar but it is not showing it and it seems that the dismissible is still part of the tree. Can you help me with that?
return ListView.builder(
itemCount: count,
itemBuilder: (BuildContext context, int position) {
final ThemeData theme = Theme.of(context);
return Dismissible(
key: Key(this.objs[position].id.toString()),
onDismissed: (direction) {
setState(() async {
int result = await helper.delete(this.objs[position].id);
});
Scaffold.of(context)
.showSnackBar(SnackBar(
content: Text(this.objs[position].title + "dismissed")));
},
background: Container(
color: Colors.red,
child: const ListTile(
leading: Icon(Icons.delete, color: Colors.white, size: 36.0)
)
),
child: ListTile(
leading: CircleAvatar(
backgroundColor: getColor(this.objs[position].priority),
child: Text(this.objs[position].id.toString()),
),
title: Text(obj[position].title),
subtitle: Text(objs[position].date),
onTap: () {
debugPrint("Tapped on " + objs[position].id.toString());
navigateToDetail(this.objs[position]);
},
),
);
},
);
this is called inside a Scaffold. And objs is a list that contains all my objects from the database.
Here is my delete code that is called inside onDismissed:
Future<int> delete(int id) async {
Database db = await this.db;
var result = await db.rawDelete("DELETE FROM $tblT WHERE $colId=$id");
return result;
}
I've noticed if I delete one item, and immediately try to create another one (I have an option to insert to DB):
It sometimes throws the error: A dismissed Dismissible widget is still part of the tree
Update:
Moved the delete part, before setState and I am getting the error: A dismissed Dismissible widget is still part of the tree every time I swipe to dismiss
You could try the following code for the onDismissed: property.
The problem is the future inside the onDismissed function. We need to reorder the async and await keywords.
Anyway, take care with the timings when removing successive items.
onDismissed: (direction) async {
String title = this.obj[position].title;
await helper.delete(this.obj[position].id);
setState(() {});
Scaffold.of(context)
.showSnackBar(SnackBar(content: Text("$title dismissed")));
},
It also moves the async out of the setState() and stores the title to be used later by the SnackBar.
Inside content in SnackBar you can try :
Text(this.obj[position].title.toString() + "dismissed")
I doing a AlertDialog, so when I tried to insert Slider widget inside the state of value sound realy stranger, and this doesn't happens if Slider is outside of AlertDialog
new Slider(
onChanged: (double value) {
setState(() {
sliderValue = value;
});
},
label: 'Oi',
divisions: 10,
min: 0.0,
max: 10.0,
value: sliderValue,
)
The complete widget code of AlertDialog
Future<Null> _showDialog() async {
await showDialog<Null>(
context: context,
builder: (BuildContext context) {
return new AlertDialog(
title: const Text('Criar novo cartão'),
actions: <Widget>[
new FlatButton(onPressed: () {
Navigator.of(context).pop(null);
}, child: new Text('Hello'))
],
content: new Container(
child: new Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
new Text('Deseja iniciar um novo cartão com quantos pedidos ja marcados?'),
new Slider(
onChanged: (double value) {
setState(() {
sliderValue = value;
});
},
label: 'Oi',
divisions: 10,
min: 0.0,
max: 10.0,
value: sliderValue,
)
],
),
),
);
}
);
}
and everything is under State class of StatefullWidget.
Its look like doesn't update the value and when try to change the value keep in same position.
Update 1
The problem is there are 2 required parameters in Slider (onChanged, value), So I shoud update this or UI keep quite, see the video how the aplication is running
Video on Youtube
Update 2
I've also opened a issue to get help with this at Github repository, if someone wants to get more information can go to issue #19323
The problem is that it's not your dialog that holds the state. It's the widget that called showDialog. Same goes for when you call setState, you are calling in on the dialog creator.
The problem is, dialogs are not built inside build method. They are on a different widget tree. So when the dialog creator updates, the dialog won't.
Instead, you should make your dialog stateful. Hold the data inside that dialog. And then use Navigator.pop(context, sliderValue) to send the slider value back to the dialog creator.
The equivalent in your dialog would be
FlatButton(
onPressed: () => Navigator.of(context).pop(sliderValue),
child: Text("Hello"),
)
Which you can then catch inside the showDialog result :
final sliderValue = await showDialog<double>(
context: context,
builder: (context) => MyDialog(),
)
I've come up with the same issue with a checkbox and that's my solution, even if it's not the best approach. (see the comment in the code)
Future<Null>_showDialog() async {
return showDialog < Null > (
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
return new AlertDialog(
title: Text("title"),
content: Container(
height: 150.0,
child: Checkbox(
value: globalSearch,
onChanged: (bool b) {
print(b);
globalSearch = b;
Navigator.of(context).pop(); // here I pop to avoid multiple Dialogs
_showDialog(); //here i call the same function
},
)),
);
},
);
}
Easiest and least amount of lines:
Use StatefulBuilder as top widget of Content in the AlertDialog.
StatefulBuilder(
builder: (context, state) => CupertinoSlider(
value: brightness,
onChanged: (val) {
state(() {
brightness = val;
});
},
),
));
I had similar issue and resolved by putting everything under AlertDialog in to a StatefullWidget.
class <your dialog widget> extends StatefulWidget {
#override
_FilterDialogState createState() => _FilterDialogState();
}
class _<your dialog widget> extends State<FilterDialog> {
#override
Widget build(BuildContext context) {
return AlertDialog(
//your alert dialog content here
);
}
}
create a statefull class with the slider at the return time and the double value should declare inside the statefull class thus the setstate func will work.
here is an example i done this for my slider popup its same for alert dialog use can declare the variable as global thus it can be accessed by other classes
class _PopupMenuState extends State<PopupMenu> {
double _fontSize=15.0;
#override
Widget build(BuildContext context) {
return Container(
child: Slider(
value: _fontSize,
min: 10,
max: 100,
onChanged: (value) {
setState(() {
print(value);
_fontSize = value;
});
},
),
);
}
}