I created a method that returns a list of Widget using this following code and would like to handle onPressed events individually! (i.e change background color of clicked button)
I'm new to Flutter and can't find a way to do this!
List<Widget> workingHoursButtons() {
List<Widget> timeButtons = [];
for (var i = 8; i <= 17; i++) {
timeButtons.add(
Padding(
padding: const EdgeInsets.all(8.0),
child: SizedBox(
width: 59.0,
height: 50.0,
child: FlatButton(
color: i == currentHour ? Color(0xff425660) : null,
shape: RoundedRectangleBorder(
side: BorderSide(),
borderRadius: BorderRadius.circular(3.0),
),
child: Text(
"$i",
style: TextStyle(
color: i == currentHour ? Colors.white : null,
fontSize: 16.0,
fontWeight: FontWeight.bold,
),
),
onPressed: i < currentHour
? null
: () {
print(i);
},
),
),
),
);
}
return timeButtons;
}
So, based on Igor’s comment, yes, you’ll need a customizable StatefulWidget that can hold a unique id per button so that it can be checked against your current hour. A simplified example:
First, setting up your button list:
workingHoursButtons() {
buttonList = new List();
for (var i = 0; i <= 5; i++) {
buttonList.add(MyButton(index: i, whatHour: _currentHour,));
}
}
What your main build Widget might look like, using a ListView.builder:
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: ...,
body: ListView.builder(
itemCount: buttonList.length,
itemBuilder: (BuildContext context, int index) {
return buttonList[index];
},
),
);
}
And your custom button widget, passing it an index and your currentHour:
class MyButton extends StatefulWidget {
MyButton({Key key, this.index, this.whatHour}) : super(key: key);
final int index;
final int whatHour;
#override
_MyButtonState createState() => _MyButtonState();
}
class _MyButtonState extends State<MyButton> {
Color _btnColor;
#override
void initState() {
super.initState();
_btnColor = _setInitColor();
}
Color _setInitColor() {
return widget.index == widget.whatHour ? Color(0xff425660) : null;
}
MaterialColor _changeColor() {
// just testing with blue/red colors
return widget.index < widget.whatHour ? Colors.blue : Colors.red;
}
#override
Widget build(BuildContext context) {
return FlatButton(
color: _btnColor,
child: Text('button' + widget.index.toString()),
onPressed: () {
setState(() {
_btnColor = _changeColor();
});
},
);
}
}
Modify as needed.
You need to use StatefulWidget and store colors for each button in a state of your widget. Then, using onPressed event, you can change color for this button (by passing its index) by calling setState method and your changed button will repaint.
I want to thank Igor and TWL for their answers but one thing i've noticed regarding this kind of issue is that there's no "correct" answer. For anyone who'll face this similar issue in the future, i would summarize it in this way.
You'll need a variable that will change for every button press(I had to create 4 in my case)
Call setState() for every change (i.e button press)
Of course depending on your dynamic buttons logic this can get messy but that seems to be the only solution.
On a side note: I think there should a better way to handle button press(using this.?) or maybe it's just me expecting flutter to work like JS
Related
I have a list of dynamic forms where I need to add and remove form fields between two fields dynamically. I am able to add/remove form fields from the bottom of the list properly.
However, when I try to add a form field in between two form fields the data for the field does not update correctly.
How can I correctly add a field in between the two fields and populate the data correctly?
import 'package:flutter/material.dart';
class DynamicFormWidget extends StatefulWidget {
const DynamicFormWidget({Key? key}) : super(key: key);
#override
State<DynamicFormWidget> createState() => _DynamicFormWidgetState();
}
class _DynamicFormWidgetState extends State<DynamicFormWidget> {
List<String?> names = [null];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Dynamic Forms'),
),
body: ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
itemBuilder: (builderContext, index) => Row(
children: [
Flexible(
child: TextFormField(
initialValue: names[index],
onChanged: (name) {
names[index] = name;
debugPrint(names.toString());
},
decoration: InputDecoration(
hintText: 'Enter your name',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8))),
),
),
Padding(
padding: const EdgeInsets.all(8),
child: IconButton(
onPressed: () {
setState(() {
if(index + 1 == names.length){
names.add( null); debugPrint('Added: $names');
} else {
names.insert(index + 1, null); debugPrint('Added [${index+1}]: $names');
}
});
},
color: Colors.green,
iconSize: 32,
icon: const Icon(Icons.add_circle)),
),
Padding(
padding: const EdgeInsets.all(8),
child: IconButton(
onPressed: (index == 0&& names.length == 1)
? null
: () {
setState(() {
names.removeAt(index);
});
debugPrint('Removed [$index]: $names');
},
color: Colors.red,
iconSize: 32,
icon: const Icon(Icons.remove_circle)),
),
],
),
separatorBuilder: (separatorContext, index) => const SizedBox(
height: 16,
),
itemCount: names.length,
),
);
}
}
Basically the problem is that Flutter is confused about who is who in your TextFormField list.
To fix this issue simply add a key to your TextFormField, so that it can be uniquely identified by Flutter:
...
child: TextFormField(
initialValue: names[index],
key: UniqueKey(), // add this line
onChanged: (name) {
...
If you want to learn more about keys and its correct use take a look at this.
The widget AnimatedList solves this problem, it keep track of the widgets as a list would do and uses a build function so it is really easy to sync elements with another list. If you end up having a wide range of forms you can make use of the InheritedWidget to simplify the code.
In this sample i'm making use of the TextEditingController to abstract from the form code part and to initialize with value (the widget inherits from the ChangeNotifier so changing the value will update the text in the form widget), for simplicity it only adds (with the generic text) and removes at an index.
To make every CustomLineForm react the others (as in: disable remove if it only remains one) use a StreamBuilder or a ListModel to notify changes and make each entry evaluate if needs to update instead of rebuilding everything.
class App extends StatelessWidget {
final print_all = ChangeNotifier();
App({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: FormList(print_notifier: print_all),
floatingActionButton: IconButton(
onPressed: print_all.notifyListeners,
icon: Icon(Icons.checklist),
),
),
);
}
}
class FormList extends StatefulWidget {
final ChangeNotifier print_notifier;
FormList({required this.print_notifier, super.key});
#override
_FormList createState() => _FormList();
}
class _FormList extends State<FormList> {
final _controllers = <TextEditingController>[];
final _list_key = GlobalKey<AnimatedListState>();
void print_all() {
for (var controller in _controllers) print(controller.text);
}
#override
void initState() {
super.initState();
widget.print_notifier.addListener(print_all);
_controllers.add(TextEditingController(text: 'Inital entrie'));
}
#override
void dispose() {
widget.print_notifier.removeListener(print_all);
for (var controller in _controllers) controller.dispose();
super.dispose();
}
void _insert(int index) {
final int at = index.clamp(0, _controllers.length - 1);
_controllers.insert(at, TextEditingController(text: 'Insert at $at'));
// AnimatedList will take what is placed in [at] so the controller
// needs to exist before adding the widget
_list_key.currentState!.insertItem(at);
}
void _remove(int index) {
final int at = index.clamp(0, _controllers.length - 1);
// The widget is replacing the original, it is used to animate the
// disposal of the widget, ex: size.y -= delta * amount
_list_key.currentState!.removeItem(at, (_, __) => Container());
_controllers[at].dispose();
_controllers.removeAt(at);
}
#override
Widget build(BuildContext context) {
return AnimatedList(
key: _list_key,
initialItemCount: _controllers.length,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
itemBuilder: (ctx, index, _) {
return CustomLineForm(
index: index,
controler: _controllers[index],
on_insert: _insert,
on_remove: _remove,
);
},
);
}
}
class CustomLineForm extends StatelessWidget {
final int index;
final void Function(int) on_insert;
final void Function(int) on_remove;
final TextEditingController controler;
const CustomLineForm({
super.key,
required this.index,
required this.controler,
required this.on_insert,
required this.on_remove,
});
#override
Widget build(BuildContext context) {
return Row(
children: [
Flexible(
child: TextFormField(
controller: controler,
),
),
IconButton(
icon: Icon(Icons.add_circle),
onPressed: () => on_insert(index),
),
IconButton(
icon: Icon(Icons.remove_circle),
onPressed: () => on_remove(index),
)
],
);
}
}
I have this layout :
I want when click on Select Time to change this blue container to another container
this is the code of radio buttons :
class TimeRadioButtonsClass extends StatefulWidget {
#override
_TimeRadioButtonsClassState createState() => _TimeRadioButtonsClassState();
List<String> labels;
String picked;
Function function;
TimeRadioButtonsClass({this.picked , this.labels , this.function});
}
class _TimeRadioButtonsClassState extends State<TimeRadioButtonsClass> {
#override
Widget build(BuildContext context) {
return RadioButtonGroup(
orientation: GroupedButtonsOrientation.VERTICAL,
margin: const EdgeInsets.only(left: 12.0),
onSelected: (String selected) => setState((){
widget.picked = selected;
}),
labels: widget.labels,
picked: widget.picked,
activeColor: Color(0xffFFD243),
onChange: (String label, int index) {
print("label: $label index: $index");
widget.function(label,index);
},
);
}
}
and here I call this class in CheckoutClass :
TimeRadioButtonsClass(picked: picked , labels: when),
NOTE : I used setState in CheckoutClass by access it as parameter in TimeRadioButtonsClass it changes container for just one time.
I don't know the reason why setState doesn't work !
Edit : I pass function as parameter to TimeRadioButtonsClass (I edit TimeRadioButtonsClass code above) and this is the code for calling:
TimeRadioButtonsClass(picked: picked , labels: when , function: (String label , int index){
setState(() {
if(index == 1){
indexRadio = 1;
print("indexRadio 1 : "+indexRadio.toString());
}
else{
indexRadio = 0;
print("indexRadio 2 : "+indexRadio.toString());
}
});
},),
indexRadio == 1 ? Container(
height: 50.0,
color: Colors.blue,
):
Container(
height: 50.0,
color: Colors.red,
),
If your container is outside the class, then you can make a callback on the selected button of the radiobutton.
widget.onTap(selected);
Add this line of code for the onTap method, to make a callback to the class from where it is called.
class TimeRadioButtonsClass extends StatefulWidget {
#override
_TimeRadioButtonsClassState createState() => _TimeRadioButtonsClassState();
List<String> labels;
String picked;
Function onTap;
TimeRadioButtonsClass({this.picked, this.labels, this.onTap});
}
class _TimeRadioButtonsClassState extends State<TimeRadioButtonsClass> {
#override
Widget build(BuildContext context) {
return RadioButtonGroup(
// orientation: GroupedButtonsOrientation.VERTICAL,
margin: const EdgeInsets.only(left: 12.0),
onSelected: (String selected) {
setState(() {
widget.picked = selected;
});
widget.onTap(selected); //This will make a callback to your class and send the selected value in the onTap method of that class.
},
labels: widget.labels,
picked: widget.picked,
activeColor: Color(0xffFFD243),
onChange: (String label, int index) =>
print("label: $label index: $index"),
);
}
}
Now to get the onTap value from the class, Call your widget as below
NOTE: Here value of 1 and 2 is your radio button picked string 1 is the ASAP and 2 is the Select timer
TimeRadioButtonsClass(
picked: "picked",
onTap: (value) {
if (value == 1) {
//do change for one
containerColor = Colors.blue;
setState(() {
});
} else if (value == 2) {
//do change for two
containerColor = Colors.red;
setState(() {
});
}
},
)
Define a Color for container as Default so to change the color on the onTap method of the class
Color containerColor = Colors.red;
Now Show The container As Follows
Container(
height: 100,
width: 100,
color: containerColor,
);
EDITED 2:- (This code changes the color for container and Radio button also changes now everytime)
String picked = "ASAP"; // Add this line at the top
Replace the Code of Calling TimeRadioButtonsClass Class with this
TimeRadioButtonsClass(
picked: picked,
labels: [
"ASAP",
"Select Timer",
],
onTap: (
int index,
) {
indexRadio = index;
if (index == 0) {
picked = "ASAP";
} else {
picked = "Select Timer";
}
setState(() {});
},
),
indexRadio == 0
? Container(
height: 50.0,
color: Colors.blue,
)
: Container(
height: 50.0,
color: Colors.red,
),
And Also Replace the TimeRadioButtonClass Itself with this code
import 'package:flutter/material.dart';
import 'package:grouped_buttons/grouped_buttons.dart';
class TimeRadioButtonsClass extends StatefulWidget {
#override
_TimeRadioButtonsClassState createState() => _TimeRadioButtonsClassState();
List<String> labels;
String picked;
Function(int) onTap;
TimeRadioButtonsClass({this.picked, this.labels, this.onTap});
}
class _TimeRadioButtonsClassState extends State<TimeRadioButtonsClass> {
#override
Widget build(BuildContext context) {
return RadioButtonGroup(
orientation: GroupedButtonsOrientation.VERTICAL,
margin: const EdgeInsets.only(left: 12.0),
labels: widget.labels,
picked: widget.picked,
activeColor: Colors.red,
onChange: (String label, int index) {
print(index);
widget.onTap(index);
});
}
}
What I have done is that Removed the onSelected (maybe it may come use later), and Made the onTap function to pass the int value
setState() only works within a particular State class.
Whenever you change the internal state of a State object, make the change in a function that you pass to setState
Calling setState notifies the framework that the internal state of this object has changed in a way that might impact the user interface in this subtree, which causes the framework to schedule a build for this State object.
setState() docs
Here, setState() is working, but your container isn't in its scope.
When the date is selected, _TimeRadioButtonsClassState.build will be called which doesn't affect your container since your container isn't in that widget (or class, if you prefer)
If possible, try shifting the container inside the widget where you'll be calling setState or try using a different state management approach
A more complex approach would be using GlobalKey and passing it as a parameter to your StatefulWidget and calling setState or using context.findAncestorStateOfType
Try this :
class _TimeRadioButtonsClassState extends State<TimeRadioButtonsClass> {
String _picked;
initState() {
_picked = widget.picked;
super.initState();
}
#override
Widget build(BuildContext context) {
return RadioButtonGroup(
orientation: GroupedButtonsOrientation.VERTICAL,
margin: const EdgeInsets.only(left: 12.0),
onSelected: (String selected) => setState((){
_picked = selected;
}),
labels: widget.labels,
picked: _picked,
activeColor: Color(0xffFFD243),
onChange: (String label, int index) => print("label: $label index: $index"),
);
}
}
In my app I am generating a ListView and items can be highlighted by tapping on them. That works fine and I also have a callback function that gives me the key for the just selected item. I can currently manually deselect the item by tapping on it again, but will ultimately take that functionality out.
My problem is that I want one and only one item to be selected at a time. In order to create the list I currently take some initial content in the form of a list, generate the tiles and add them to another list. I then use that list to create the ListView. My plan was on the callback from a new selection, run through the list of tiles and deselect them before highlighting the new chosen tile and carrying out the other functions. I have tried various methods to tell each tile to deselect itself but have not found any way to address each of the tiles. Currently I get the error:
Class 'OutlineTile' has no instance method 'deselect'.
Receiver: Instance of 'OutlineTile'
Tried calling: deselect()
I have tried to access a method within the tile class and to use a setter but neither worked so far. I am quite new to flutter so it could be something simple I am missing. My previous experience was with Actionscript where this system would have worked fine and I could access a method of an object (in this case the tile) easily as long s it is a public method.
I'd be happy to have another way to unselect the old item or to find a way to access a method within the tile. The challenge is to make the tiles show not highlighted without them being tapped themselves but when a different tile is tapped.
The code in my parent class is as follows:
class WorkingDraft extends StatefulWidget {
final String startType;
final String name;
final String currentContent;
final String currentID;
final List startContent;
WorkingDraft(
{this.startType,
this.name,
this.currentContent,
this.currentID,
this.startContent});
#override
_WorkingDraftState createState() => _WorkingDraftState();
}
class _WorkingDraftState extends State<WorkingDraft> {
final _formKey = GlobalKey<FormState>();
final myController = TextEditingController();
//String _startType;
String _currentContent = "";
String _name = "Draft";
List _startContent = [];
List _outLineTiles = [];
int _counter = 0;
#override
void dispose() {
// Clean up the controller when the widget is disposed.
myController.dispose();
super.dispose();
}
void initState() {
super.initState();
_currentContent = widget.currentContent;
_name = widget.name;
_startContent = widget.startContent;
_counter = 0;
_startContent.forEach((element) {
_outLineTiles.add(OutlineTile(
key: Key("myKey$_counter"),
outlineName: element[0],
myContent: element[1],
onTileSelected: clearHilights,
));
_counter++;
});
}
dynamic clearHilights(Key myKey) {
_outLineTiles.forEach((element) {
element.deselect(); // this throws an error Class 'OutlineTile' has no instance method 'deselect'.
Key _foundKey = element.key;
print("Element Key $_foundKey");
});
}
.......
and further down within the widget build scaffold:
child: ListView.builder(
itemCount: _startContent.length,
itemBuilder: (context, index) {
return _outLineTiles[index];
},
),
Then the tile class is as follows:
class OutlineTile extends StatefulWidget {
final Key key;
final String outlineName;
final Icon myIcon;
final String myContent;
final Function(Key) onTileSelected;
OutlineTile(
{this.key,
this.outlineName,
this.myIcon,
this.myContent,
this.onTileSelected});
#override
_OutlineTileState createState() => _OutlineTileState();
}
class _OutlineTileState extends State<OutlineTile> {
Color color;
Key _myKey;
#override
void initState() {
super.initState();
color = Colors.transparent;
}
bool _isSelected = false;
set isSelected(bool value) {
_isSelected = value;
print("set is selected to $_isSelected");
}
void changeSelection() {
setState(() {
_myKey = widget.key;
_isSelected = !_isSelected;
if (_isSelected) {
color = Colors.lightBlueAccent;
} else {
color = Colors.transparent;
}
});
}
void deselect() {
setState(() {
isSelected = false;
color = Colors.transparent;
});
}
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Row(
children: [
Card(
elevation: 10,
margin: EdgeInsets.fromLTRB(10.0, 6.0, 5.0, 0.0),
child: SizedBox(
width: 180,
child: Container(
color: color,
child: ListTile(
title: Text(widget.outlineName),
onTap: () {
if (widget.outlineName == "Heading") {
Text("Called Heading");
} else (widget.outlineName == "Paragraph") {
Text("Called Paragraph");
widget.onTileSelected(_myKey);
changeSelection();
},
),
........
Thanks for any help.
Amended Code sample and explanation, that builds to a complete project, from here:
Following the advice from phimath I have created a full buildable sample of the relevant part of my project.
The problem is that the tiles in my listview are more complex with several elements, many of which are buttons in their own right so whilst phimath's solution works for simple text tiles I have not been able to get it working inside my own project. My approach is trying to fundamentally do the same thing as phimath's but when I include these more complex tiles it fails to work.
This sample project is made up of three files. main.dart which simply calls the project and passes in some dummy data in the way my main project does. working_draft.dart which is the core of this issue. And outline_tile.dart which is the object that forms the tiles.
Within working draft I have a function that returns an updated list of the tiles which should show which tile is selected (and later any other changes from the other buttons). This gets called when first going to the screen. When the tile is tapped it uses a callback function to redraw the working_draft class but this seems to not redraw the list as I would expect it to. Any further guidance would be much appreciated.
The classes are:
first class is main.dart:
import 'package:flutter/material.dart';
import 'package:listexp/working_draft.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
home: WorkingDraft(
startType: "Basic",
name: "Draft",
currentID: "anID",
startContent: [
["Heading", "New Heading"],
["Paragraph", "New Text"],
["Image", "placeholder"],
["Signature", "placeholder"]
],
));
}
}
Next file is working_draft.dart:
import 'package:flutter/material.dart';
import 'package:listexp/outline_tile.dart';
class WorkingDraft extends StatefulWidget {
final String startType;
final String name;
final String currentContent;
final String currentID;
final List startContent;
final int selectedIndex;
WorkingDraft(
{this.startType,
this.name,
this.currentContent,
this.currentID,
this.startContent,
this.selectedIndex});
#override
_WorkingDraftState createState() => _WorkingDraftState();
}
class _WorkingDraftState extends State<WorkingDraft> {
int selectedIndex;
String _currentContent = "";
String _name = "Draft";
List _startContent = [];
var _outLineTiles = [];
int _counter = 0;
int _selectedIndex;
bool _isSelected;
dynamic clearHilights(int currentIndex) {
setState(() {
_selectedIndex = currentIndex;
});
}
updatedTiles() {
if (_selectedIndex == null) {
_selectedIndex = 0;
}
_currentContent = widget.currentContent;
_name = widget.name;
_startContent = widget.startContent;
_counter = 0;
_outLineTiles = [];
_startContent.forEach((element) {
_isSelected = _selectedIndex == _counter ? true : false;
_outLineTiles.add(OutlineTile(
key: Key("myKey$_counter"),
outlineName: element[0],
myContent: element[1],
myIndex: _counter,
onTileSelected: clearHilights,
isSelected: _isSelected,
));
_counter++;
});
}
#override
Widget build(BuildContext context) {
updatedTiles();
return Scaffold(
body: Center(
child: Column(children: [
SizedBox(height: 100),
Text("Outline", style: new TextStyle(fontSize: 15)),
Container(
height: 215,
width: 300,
decoration: BoxDecoration(
border: Border.all(
color: Colors.lightGreenAccent,
width: 2,
),
borderRadius: BorderRadius.circular(2),
),
child: ListView.builder(
itemCount: _startContent.length,
itemBuilder: (context, index) {
return _outLineTiles[index];
},
),
),
]),
));
}
}
and finally is outline_tile.dart
import 'package:flutter/material.dart';
class OutlineTile extends StatefulWidget {
final Key key;
final String outlineName;
final Icon myIcon;
final String myContent;
final int myIndex;
final Function(int) onTileSelected;
final bool isSelected;
OutlineTile(
{this.key,
this.outlineName,
this.myIcon,
this.myContent,
this.myIndex,
this.onTileSelected,
this.isSelected});
#override
_OutlineTileState createState() => _OutlineTileState();
}
class _OutlineTileState extends State<OutlineTile> {
Color color;
// Key _myKey;
bool _isSelected;
#override
void initState() {
super.initState();
_isSelected = widget.isSelected;
if (_isSelected == true) {
color = Colors.lightBlueAccent;
} else {
color = Colors.transparent;
}
}
void deselect() {
setState(() {
_isSelected = widget.isSelected;
if (_isSelected == true) {
color = Colors.lightBlueAccent;
} else {
color = Colors.transparent;
}
});
}
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Row(
children: [
Card(
elevation: 10,
margin: EdgeInsets.fromLTRB(10.0, 6.0, 5.0, 0.0),
child: SizedBox(
width: 180,
child: Container(
color: color,
child: ListTile(
title: Text(widget.outlineName),
onTap: () {
if (widget.outlineName == "Heading") {
Text("Called Heading");
} else if (widget.outlineName == "Paragraph") {
Text("Called Paragraph");
} else if (widget.outlineName == "Signature") {
Text("Called Signature");
} else {
Text("Called Image");
}
var _myIndex = widget.myIndex;
widget.onTileSelected(_myIndex);
deselect();
},
),
),
),
),
SizedBox(
height: 60,
child: Column(
children: [
SizedBox(
height: 20,
child: IconButton(
iconSize: 30,
icon: Icon(Icons.arrow_drop_up),
onPressed: () {
print("Move Up");
}),
),
SizedBox(height: 5),
SizedBox(
height: 20,
child: IconButton(
iconSize: 30,
icon: Icon(Icons.arrow_drop_down),
onPressed: () {
print("Move Down");
}),
),
],
),
),
SizedBox(
height: 60,
child: Column(
children: [
SizedBox(
height: 20,
child: IconButton(
iconSize: 20,
icon: Icon(Icons.add_box),
onPressed: () {
print("Add another");
}),
),
SizedBox(
height: 10,
),
SizedBox(
height: 20,
child: IconButton(
iconSize: 20,
icon: Icon(Icons.delete),
onPressed: () {
print("Delete");
}),
),
],
),
),
],
),
);
}
}
Thanks again
Instead of manually deselecting tiles, just keep track of which tile is currently selected.
I've made a simple example for you. When we click a tile, we just set the selected index to the index we clicked, and each tile looks at that to see if its the currently selected tile.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(body: Home()),
);
}
}
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
int selectedIndex;
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: 10,
itemBuilder: (context, index) {
return ListTile(
title: Text('Item: $index'),
tileColor: selectedIndex == index ? Colors.blue : null,
onTap: () {
setState(() {
selectedIndex = index;
});
},
);
},
);
}
}
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);
}));
}
}
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();
}