I'm at the beginning of learning Getx and I'm having the following problem: I have a ListView.builder with the CodeLine() widget, which is a widget where you can select it with an onLongPress. The idea here is to select a row but when I give onLongPress, all the rows are selected together.
class CodeEditor extends GetView<CodeEditorController> {
const CodeEditor({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
ScrollController scrollCode = ScrollController();
return Padding(
padding: const EdgeInsets.fromLTRB(0.0, 0.0, 10.0, 120.0),
child: ListView.builder(
controller: scrollCode,
itemCount: 10,
itemBuilder: (BuildContext context, int index){
return CodeLine();
}
)
);
}
}
In CodeLine I have isSelected variable to show on screen when it is selected or not and I change its value in CodeLineController controller
class CodeLine extends GetView<CodeLineController> {
CodeLine({Key? key}) : super(key: key);
#override
final controller = Get.put(CodeLineController());
#override
Widget build(BuildContext context) {
return GestureDetector(
child: GetX<CodeLineController>(
builder: (_){
return !_.isHidden ? Container(
color: _.isSelected ? const Color(0x327a7a7a) : Colors.transparent,
height: 35.0,
child: Row(
children: <Widget>[
SizedBox(
width: 25.0,
child: Text(
"1",
textAlign: TextAlign.end,
style: TextStyle(
color: codeLineTheme.hintColor,
fontSize: 15.0,
fontWeight: FontWeight.bold
)
)
),
const SizedBox(width: 7.5),
const Expanded(
child: SizedBox()
)
]
)
) : const SizedBox();
}
),
onLongPress: (){
controller.isSelected = !controller.isSelected;
}
);
}
}
In CodeLineController I have the isSelected variable as observable but when I change its boolean value, this value of all CodeLine instances is also changed, how can I change only the isSelected variable of a specific CodeLine?
class CodeLineController extends GetxController{
CodeLineController();
final _isHidden = false.obs;
get isHidden => _isHidden.value;
set isHidden(value) => _isHidden.value = value;
final _isSelected = false.obs;
get isSelected => _isSelected.value;
set isSelected(value) => _isSelected.value = value;
}
When you call a class that you extend with GetxController with Get.put, only one of that object is found in the project. And you always define the same object in the CodeLine widget. So a change affects all of its widgets. You can solve your problem like this:
Add Map inside CodeLineController. Save all CodeLine widgets you reproduced with ListView to the map with a special key.
class CodeLineController extends GetxController {
final RxMap<Key, bool> _map = <Key, bool>{}.obs;
void setKey(Key key, bool value) {
_map[key] = value;
}
void changeValue(Key key) {
if (_map[key] != null) {
_map[key] = !_map[key]!;
}
}
bool getValue(Key key) {
if (_map[key] != null) {
return _map[key]!;
} else {
return false;
}
}
...
}
It can be an int "index" value instead of the "key" in the Map or a unique data you want.
You can pass "key" as a parameter to the CodeLine class.
class CodeLine extends GetView<CodeLineController> {
final Key myKey;
CodeLine({
Key? key,
required this.myKey,
}) : super(key: key);
...
}
When creating the CodeLine class, pass the key as a parameter inside the ListView.
...
Key myKey;
return Padding(
...
itemBuilder: (BuildContext context, int index) {
myKey = GlobalKey();
controller.setKey(myKey, false);
return CodeLine(myKey: myKey);
},
...
onLongPress
...
onLongPress: () {
controller.changeValue(myKey);
},
...
This is how you can check
...
Container(
color: controller.getValue(myKey)
? const Color(0x327a7a7a)
: Colors.transparent,
...
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 several audio files to be played, so I used ListView to represent every audio file as an item of ListView, each one with its own controllers (play/pause button and duration slider). The code is as follows (I have used one audio file for all of the items for simplicity sake):
import 'package:audioplayers/audioplayers.dart';
class AudioTestScreen extends StatelessWidget {
const AudioTestScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Songs")),
body: ListView.builder(
itemCount: 10,
itemBuilder: (ctx, index) => const AudioItem(),
),
);
}
}
class AudioItem extends StatefulWidget {
const AudioItem({Key? key}) : super(key: key);
#override
State<AudioItem> createState() => _AudioItemState();
}
class _AudioItemState extends State<AudioItem> {
final audioPlayer = AudioPlayer();
bool isPlaying = false;
Duration duration = Duration.zero; // For total duration
Duration position = Duration.zero; // For the current position
#override
void initState() {
super.initState();
setAudioPlayer();
audioPlayer.onDurationChanged.listen((newDuration) {
setState(() {
duration = newDuration;
});
});
audioPlayer.onPositionChanged.listen((newPosition) {
if (mounted) {
setState(() {
position = newPosition;
});
}
});
audioPlayer.onPlayerStateChanged.listen((state) {
if (mounted) {
setState(() {
isPlaying = state == PlayerState.playing;
});
}
});
}
Future<void> setAudioPlayer() async {
final player = AudioCache(prefix: "assets/audios/");
final url = await player.load("song.mp3");
audioPlayer.setSourceUrl(url.path);
audioPlayer.setReleaseMode(ReleaseMode.stop);
}
#override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
decoration: BoxDecoration(
color: const Color(0xFFF4F2FF),
borderRadius: BorderRadius.circular(12),
border: Border.all(width: 1, color: Colors.grey)
),
child: Column(
children: [
Slider(
value: position.inMilliseconds.toDouble(),
max: duration.inMilliseconds.toDouble(),
onChanged: (value) {
setState(() {
position = Duration(milliseconds: value.toInt());
});
audioPlayer.seek(position);
},
),
GestureDetector(
onTap: () async {
isPlaying
? await audioPlayer.pause()
: await audioPlayer.resume();
},
child: CircleAvatar(
child: Icon(isPlaying ? Icons.pause : Icons.play_arrow),
),
)
],
),
);
}
}
And here is how it looks like:
Now when I play a music file, and later tap on another item to play it, both of them plays at the same time, but I want the previous one to pause and only the current one to play.
How can I achieve this behavior? Thanks in advance.
create the audio player in the parent class and pass it to the children. Then before you play stop the player and then play it with new url
widget.player.stop()
Use this to stop the player
EDIT
class AudioItem extends StatefulWidget {
final AudioPlayer audioPlayer;
final int currentIndex;
final int index;
final VoidCallback setIndex;
const AudioItem({Key? key, required this.audioPlayer, required required this.currentIndex, required this.index, required this.setIndex}) : super(key: key);
Add these 3 variables to the Audio item. When you add these Widgets in the tree pass the values
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Songs")),
body: ListView.builder(
itemCount: 10,
itemBuilder: (ctx, index) => const AudioItem(
audioPlayer: audioPlayer,
currentIndex:currentIndex, <--this is the variable in which we know which item is playing.
index: index,
setIndex: (){
currentIndex = index;
setState((){});
}
),
),
);
}
Now when the play button is clicked call this setIndex method that will update the parent.
I made question yesterday but is unclear. my unclear question :(
I have made changes for my question here. I got some trouble getting the value from Rx object. When the first time page load, it does perfectly listen the value change, but when I add some category amount inside DateClass, the getX not working. But it working after I changing the first amount from textfield then the second textfield is listen the value change. Did I miss something?
here my code and I attach some screenshot below
First page, I adding the value then total is listening
then I add new category to insert new amount, the total not listening
I made change to first textfield value, total is listening
after I change the first value, total is listening to second textfield value
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:collection/collection.dart';
class MainClass extends StatefulWidget {
Function callback;
MainClass({Key key, this.callback}) : super(key: key);
#override
MainClassState createState() => MainClassState();
}
class MainClassState extends State<MainClass> {
RxList<DateClass> dynamicPerDate = <DateClass>[].obs;
GlobalKey<MainClassState> mKey = GlobalKey();
String stringTotal = "";
addPerDate() async {
dynamicPerDate.add(DateClass());
setState(() {});
}
calculate() {
int total = 0;
List<int> listAmount = [];
for (int i = 0; i < dynamicPerDate.value.length; i++) {
for (int j = 0; dynamicPerDate.value[i].listCategory.length > j; j++) {
var a = dynamicPerDate[i].listCategory[j].amountCategory;
print(a);
if (a != null) {
listAmount.add(a.value);
}
total = listAmount.sum;
}
}
return total.toString();
}
#override
void initState() {
// TODO: implement initState
super.initState();
addPerDate();
}
#override
Widget build(BuildContext context) {
return Scaffold(
key: mKey,
body: Container(
child: ListView(
children: [
ListView.builder(
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: dynamicPerDate.length,
itemBuilder: (_, index) => dynamicPerDate.isNotEmpty
? dynamicPerDate[index]
: Container()),
FlatButton(
onPressed: () {
addPerDate();
// calculate2();
},
child: Text('Add Date')),
SizedBox(height: 20),
Text('Total All Amount From Category Class'),
Obx(() => Text(calculate())),
],
),
),
);
}
}
class DateClass extends StatefulWidget {
RxList<CategoryClass> listCategory = <CategoryClass>[].obs;
DateClass({Key key, this.listCategory}) : super(key: key);
#override
_DateClassState createState() => _DateClassState();
}
class _DateClassState extends State<DateClass> {
addCategoryPerDate() {
widget.listCategory.add(CategoryClass());
setState(() {});
}
#override
void initState() {
// TODO: implement initState
super.initState();
widget.listCategory = <CategoryClass>[].obs;
addCategoryPerDate();
}
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Card(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text('Date', style: TextStyle(color: Colors.blue)),
ListView.builder(
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: widget.listCategory.length,
itemBuilder: (_, index) => widget.listCategory.isNotEmpty
? widget.listCategory[index]
: Container()),
FlatButton(
onPressed: () {
addCategoryPerDate();
},
child: Text('Add Category'))
],
),
),
);
}
}
class CategoryClass extends StatefulWidget {
RxInt amountCategory = 0.obs;
TextEditingController amountController;
CategoryClass({Key key, this.amountCategory}) : super(key: key);
#override
_CategoryClassState createState() => _CategoryClassState();
}
class _CategoryClassState extends State<CategoryClass> {
MainClass mainClass = MainClass();
#override
void initState() {
// TODO: implement initState
super.initState();
widget.amountCategory = 0.obs;
}
#override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text('Amount'),
TextFormField(
controller: widget.amountController,
onChanged: (value) {
setState(() {
widget.amountCategory.value = int.parse(value);
widget.amountCategory.obs;
// MainClass().method();
});
},
keyboardType: TextInputType.number,
)
],
);
}
}
I'm trying to create a Textbutton widget with a disabled property like this:
class AppTextButton extends StatelessWidget {
final String title;
final void Function(BuildContext context) onPress;
final EdgeInsetsGeometry margin;
final EdgeInsetsGeometry padding;
final double borderRadius;
final Color backgroundColor;
final Image? leadingIcon;
final Image? trailingIcon;
final TextStyle? textStyle;
final bool disabled;
AppTextButton(this.title, this.onPress,
{this.margin = const EdgeInsets.all(0),
this.padding = const EdgeInsets.all(12),
this.borderRadius = 0,
this.leadingIcon,
this.trailingIcon,
this.textStyle,
this.disabled = false,
this.backgroundColor = const Color(0xFFFFFFFF)});
#override
Widget build(BuildContext context) {
return Container(
padding: margin,
child: TextButton(
style: ButtonStyle(
shape: MaterialStateProperty.all<RoundedRectangleBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(borderRadius))),
backgroundColor: MaterialStateProperty.all(backgroundColor)),
child: Row(
children: [
if (this.leadingIcon != null) ...[this.leadingIcon!],
Expanded(
child: Padding(
padding: padding,
child:
Text(title, textAlign: TextAlign.center, style: textStyle),
),
),
if (this.trailingIcon != null) ...[this.trailingIcon!]
],
),
onPressed: () => !disabled ? onPress(context) : null,
),
);
}
}
And in my screen, I declare my formKey and my form as following:
class LoginScreen extends AppBaseScreen {
LoginScreen({Key? key}) : super(key: key);
final _formKey = GlobalKey<FormState>();
#override
Widget build(BuildContext context) {
Form(
key: _formKey,
child: Obx(
() => AppTextInput(
"Please input passcode",
_passwordController,
borderRadius: 8,
fillColor: Color(0xFFF6F4F5),
keyboardType: TextInputType.number,
errorMessage: _c.errorLoginConfirm.value,
isObscure: true,
onChange: _onInputChange,
maxLength: 6,
margin: EdgeInsets.only(top: 12, left: 20, right: 20),
validator: (text) {
if (text != null && text.length > 0) {
if (text.length < 6) {
return "Passcode must have at least 6 digits";
}
}
},
),
)),
And I will have a button at the bottom of the screen, which I pass the !_formKey.currentState!.validate() in the disabled field
AppTextButton("Login", _onLogin,
margin: EdgeInsets.fromLTRB(24, 24, 24, 8),
backgroundColor: Color(0xFFFF353C),
disabled: !_formKey.currentState!.validate(),
textStyle: TextStyle(color: Colors.white),
borderRadius: 8),
However, the formKey.currentState is null and throw the following error everytime the screen is opened.
Null check operator used on a null value
What I am doing wrong here? Thank you in advance!
You need to save the form state before passing,
final FormState formState = _formKey.currentState;
formState.save();
onPressed: () {
FocusScope.of(context).requestFocus(FocusNode());
final FormState formState = _formKey.currentState;
if (formState.validate()) {
formState.save();
onPress(context);
}
},
I think the problem is caused because all the widgets are created at the same time, so the _formKey.currentState is still null when the AppTextButton calls it.
You need to create a separate controller to control the state of the button and add it to the validator like this:
validator: (text) {
if (text != null && text.length > 0) {
if (text.length < 6) {
buttonDisableController = true;
return "Passcode must have at least 6 digits";
}
}
buttonDisableController = false;
return null;
},
In your case, you should know how the widgets building process (Assume you have Botton widget and Input widget):
Botton and Input are building initial state. both states are not yet ready to be read and used
Botton and Input are built. States are ready to read.
User interact to Input. Input must call Button to rebuild its state if the value passes the validator
Botton rebuild.
For the process, you should change your code like:
Get and modify the state of Button inside Input
Notify Button to rebuild
There are many ways to handle the state management between widgets. I simply change the AppTextButton into Statefultwidget to achieve it.
...
final _buttonKey = GlobalKey<_AppTextButtonState>();
...
AppTextButton(key: _buttonKey)
...
class AppTextButton extends StatefulWidget {
final bool initDisable;
AppTextButton({
this.initDisable = false,
Key? key,
}) : super(key: key);
#override
_AppTextButtonState createState() => _AppTextButtonState();
}
class _AppTextButtonState extends State<AppTextButton> {
var disable;
#override
void initState() {
disable = widget.initDisable;
super.initState();
}
#override
Widget build(BuildContext context) {
return TextButton(child: Text('Button'), onPressed: disable ? null : () {});
}
void enableButton() {
setState(() {
disable = false;
});
}
void disableButton() {
setState(() {
disable = true;
});
}
}
class LoginScreen extends StatelessWidget {
LoginScreen({Key? key}) : super(key: key);
final _formKey = GlobalKey<FormState>();
#override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: TextFormField(
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: (text) {
if (text != null && text.length > 0) {
if (text.length < 6) {
return "Passcode must have at least 6 digits";
}
}
},
onChanged: (v) {
if (_formKey.currentState?.validate() ?? false) {
_buttonKey.currentState?.enableButton();
} else {
_buttonKey.currentState?.disableButton();
}
},
),
);
}
}
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;
});
},
);
},
);
}
}