I have a page that consists of a ListView, which contains TextFormFields. The user can add or remove items from that ListView.
I use the bloc pattern, and bind the number of Items and their content inside the ListView to a list saved in the bloc state. When I want to remove the items, I remove the corresponding text from this list and yield the new state. However, this will always remove the last item, instead of the item that's supposed to be removed. While debugging, I can clearly see that the Item I want removed is in fact removed from the state's list. Still, the ListView removes the last item instead.
I've read that using keys solves this problem and it does. However, if I use keys there is a new problem.
Now, the TextFormField will go out of focus every time a character is written. I guess this is to do with the fact that the ListView is redrawing its items everytime a character is typed, and somehow having a key makes the focus behave differently.
Any ideas how to solve this?
The page code (The ListView is at the bottom):
class GiveBeneftis extends StatelessWidget {
#override
Widget build(BuildContext context) {
var bloc = BlocProvider.of<CreateChallengeBloc>(context);
return BlocBuilder<CreateChallengeBloc, CreateChallengeState>(
builder: (context, state) {
return CreatePageTemplate(
progress: state.progressOfCreation,
buttonBar: NavigationButtons(
onPressPrevious: () {
bloc.add(ProgressOfCreationChanged(nav_direction: -1));
Navigator.of(context).pop();
},
onPressNext: () {
bloc.add(ProgressOfCreationChanged(nav_direction: 1));
Navigator.of(context).pushNamed("create_challenge/add_pictures");
},
previous: 'Details',
next: 'Picture',
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text(
'List the benefits of you Challenge',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
),
SizedBox(height: 30),
Text(
'Optionally: Make a list of physical and mental benefits the participants can expect. ',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.grey,
fontSize: 14,
fontWeight: FontWeight.w400),
),
SizedBox(height: 50),
Container(
margin: EdgeInsets.all(8.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.yellow[600]),
child: FlatButton(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
onPressed: () => bloc.add(ChallengeBenefitAdded()),
child: Text('Add a benefit',
style: TextStyle(
color: Colors.white, fontWeight: FontWeight.bold)),
),
),
Expanded(
child: new ListView.builder(
itemCount: state.benefits.length,
itemBuilder: (BuildContext context, int i) {
final item = state.benefits[i];
return Padding(
padding: EdgeInsets.symmetric(horizontal: 25),
child: TextFieldTile(
//key: UniqueKey(),
labelText: 'Benefit ${i + 1}',
validator: null,
initialText: state.benefits[i],
onTextChanged: (value) => bloc.add(
ChallengeBenefitChanged(
number: i, text: value)),
onCancelIconClicked: () {
bloc.add(ChallengeBenefitRemoved(number: i));
},
));
})),
],
),
);
});
}
}
The Code of the TextfieldTile:
class TextFieldTile extends StatelessWidget {
final Function onTextChanged;
final Function onCancelIconClicked;
final Function validator;
final String labelText;
final String initialText;
const TextFieldTile(
{Key key,
this.onTextChanged,
this.onCancelIconClicked,
this.labelText,
this.initialText,
this.validator})
: super(key: key);
#override
Widget build(BuildContext context) {
return Stack(children: <Widget>[
TextFormField(
textCapitalization: TextCapitalization.sentences,
initialValue: initialText,
validator: validator,
onChanged: onTextChanged,
maxLines: null,
decoration: InputDecoration(
labelText: labelText,
)),
Align(
alignment: Alignment.topRight,
child: IconButton(
icon: Icon(Icons.cancel), onPressed: onCancelIconClicked),
),
]);
}
}
The relevant portion of the Bloc:
if (event is ChallengeBenefitAdded) {
var newBenefitsList = List<String>.from(state.benefits);
newBenefitsList.add("");
yield state.copyWith(benefits: newBenefitsList);
}
else if (event is ChallengeBenefitChanged) {
var newBenefitsList = List<String>.from(state.benefits);
newBenefitsList[event.number] = event.text;
yield state.copyWith(benefits: newBenefitsList);
}
else if (event is ChallengeBenefitRemoved) {
var newBenefitsList = List<String>.from(state.benefits);
newBenefitsList.removeAt(event.number);
yield state.copyWith(benefits: newBenefitsList);
}
I can think of two things you can do here.
Create a different bloc for processing the changes in the text field, that will avoid having to actually update the state of the entire list if no needed.
Have a conditional to avoid rebuilding the list when your bloc change to a state that is relevant only to the keyboard actions.
Example:
BlocBuilder<CreateChallengeBloc, CreateChallengeState>(
buildWhen: (previousState, currentState) {
return (currentState is YourNonKeyboardStates);
}
...
);
Related
See for the invoice page I have BlocBuilder wrapped in a scaffold of stateful page, inside that body under several widgets is a call to future void in separate file call to create a dialog widget. And inside the dialog method is a call to create an invoice form which is in a separate file and is stateful class displayed to be displayed on the dialog screen. In this form the user will be able to add and delete UI elements from a list view what I need to do is rebuild the widget either dialog screen/form or the list view/ to reflect the changes made by the user
import 'package:flutter/material.dart';
import 'dart:developer' as dev;
import 'package:track/src/features/invoices/application/bloc.dart';
import 'package:track/src/features/invoices/application/events.dart';
import 'package:track/src/features/invoices/application/pdf_invoice_api.dart';
class InvoiceForm extends StatefulWidget {
final InvoiceBlocController blocController;
const InvoiceForm(this.blocController, {Key? key}) : super(key: key);
#override
State<InvoiceForm> createState() => _InvoiceFormState();
}
class _InvoiceFormState extends State<InvoiceForm> {
final _formKey = GlobalKey<FormState>();
#override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
controller: TextEditingController()
..text = widget.blocController.invoice.client,
validator: (value) {
value!.isEmpty ? 'Enter a value for client' : null;
},
style: Theme.of(context).textTheme.labelMedium,
decoration: InputDecoration(
focusedBorder: const UnderlineInputBorder(
borderSide: BorderSide(
color: Colors.white,
),
),
enabledBorder: const UnderlineInputBorder(
borderSide: BorderSide(
color: Colors.white,
),
),
labelText: 'Client:',
labelStyle: Theme.of(context).textTheme.labelMedium),
),
TextFormField(
controller: TextEditingController()
..text =
'${widget.blocController.invoice.projectNumber}-${widget.blocController.invoice.invoiceNumber}',
validator: (value) {
value!.isEmpty ? 'Enter a valid project number' : null;
},
style: Theme.of(context).textTheme.labelMedium,
decoration: InputDecoration(
focusedBorder: const UnderlineInputBorder(
borderSide: BorderSide(
color: Colors.white,
),
),
enabledBorder: const UnderlineInputBorder(
borderSide: BorderSide(
color: Colors.white,
),
),
labelText: 'Client:',
labelStyle: Theme.of(context).textTheme.labelMedium),
),
ListView.builder(
shrinkWrap: true,
itemCount: widget.blocController.invoice.items.length,
itemBuilder: (context, index) {
final item = widget.blocController.invoice.items[index];
return ListTile(
contentPadding: EdgeInsets.zero,
trailing: IconButton(
onPressed: () {
widget.blocController.add(DeleteItemFromInvoice(index));
},
icon: Icon(Icons.delete)),
title: Column(
children: [
Row(
children: [
itemTextFormField(
initialValue: item.name ?? '',
labelText: 'name',
index: index),
SizedBox(width: 20),
itemTextFormField(
initialValue: item.description ?? '',
labelText: 'description',
index: index),
],
),
Row(
children: [
itemTextFormField(
initialValue: item.quantity.toString(),
labelText: 'quantity',
index: index),
SizedBox(width: 20),
itemTextFormField(
initialValue: item.costBeforeVAT.toString(),
labelText: 'Cost Before VAT',
index: index),
],
),
SizedBox(height: 30),
Divider(
thickness: 2,
color: Colors.black,
)
],
),
);
},
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton(
onPressed: () {
dev.log('button clicked to add new item');
widget.blocController.add(AddNewItemToInvoice());
},
icon: Icon(Icons.add)),
IconButton(
onPressed: () async {
_formKey.currentState!.save();
Navigator.pop(context);
await PdfInvoiceApi.generate(widget.blocController.invoice);
},
icon: Icon(Icons.send))
],
)
],
),
);
}
Expanded itemTextFormField({
required String initialValue,
required String labelText,
required int index,
}) {
return Expanded(
child: TextFormField(
controller: TextEditingController()..text = initialValue,
onSaved: (newValue) {
widget.blocController.add(UpdateInvoiceDetails(index));
},
style: Theme.of(context).textTheme.labelMedium,
decoration: InputDecoration(
focusedBorder: const UnderlineInputBorder(
borderSide: BorderSide(
color: Colors.white,
),
),
enabledBorder: const UnderlineInputBorder(
borderSide: BorderSide(
color: Colors.white,
),
),
labelText: labelText,
labelStyle: Theme.of(context).textTheme.labelMedium,
),
),
);
}
}
InvoiceDialog Source code: https://pastebin.com/PCjmCWsk
InvoiceDialog Source code: https://pastebin.com/VS5CG22D
Edit 2: Made the follwoing changes to bloc per Mostafa answer as best I could, getting pressed against a deadline here so really need some help:
These changes were to main page calling the show dialog passing bloc.
showDialog(
context: context,
builder: (context) => BlocProvider.value(
value: blocController,
child: InvoiceDetailsDialog(
screenWidth: screenWidth,
screenHeight: screenHeight),
),
);
This file was the original place where showdialog was called and was custom Future return showDialog.
Results: showDialog takes enitre screen. Rendering Invoice form reulsts in error being displayed in place of the form:
No Material widget found.
Edit 3: fixed previous error but back where i started bloc is still being called succesfully but no changes to the ui:
Widget build(BuildContext context) {
final blocController = BlocProvider.of<InvoiceBlocController>(context);
return Center(
child: Material(color: Colors.red,
borderRadius: BorderRadius.circular(50),
child: SizedBox(
width: screenWidth / 2, height: screenHeight / 2,
child: Padding(padding: const EdgeInsets.all(20),
child: Column(children: [
Expanded(child: ListView(children: [
Text('Invoices',
style: Theme.of(context)
.textTheme.bodyLarge?.copyWith(color: Colors.white)),
InvoiceForm()
]))])))));
}
As form nothing changed except instead of passing the blocController through a method I am now calling it like:
class _InvoiceFormState extends State<InvoiceForm> {
final _formKey = GlobalKey<FormState>();
late final InvoiceBlocController blocController;
#override
void initState() {
blocController = BlocProvider.of<InvoiceBlocController>(context);
super.initState();
}
Still nothing changes.
Edit 4: Set state does work and leaving in bloc code was executing and if I clicked add two items would be added or delete would remove two items. But with setstate commented out it went back to not rebuilding. Using setstate for now but not preferred.
Edit 5: Don't if this is still being paid attention to hopefully is. Can I keep add add events like: add(NewItem), add(deleteItem),add(GeneratePDF). Without changing state. currently I have done that once so far. Is this bad practice
You can pass the main bloc to the dialog widget and call the bloc function that you want and it will reflect on the main screen
How can you do this? by injecting the MainBloc value to DialogWidget with BlocProvider.value
MainWidget
class MainWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocProvider(
create: (BuildContext context) => MainBloc(),
child: BlocConsumer<MainBloc, MainStates>(
listener: (BuildContext context, MainStates state) {},
builder: (BuildContext context, MainStates state) {
final bloc = MainBloc.get(context);
return GestureDetector(
onTap: () {
showDialog(
context,
builder: (context) => BlocProvider.value(
value: bloc,
child: WidgetTwoDialog(),
),
);
},
child: Item(),
);
},
),
);
}
}
DialogWidget
class DialogWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
final bloc = MainBloc.get(context);
return GestureDetector(
onTap: () {
bloc.addToList();
},
child: Text('Remove form the main screen'),
);
}
}
Also, this answer might help you to get my point well here
you can wrap your dialog within the stateful builder and then you will get the method to set your dialog state.
i want to change the indexvalue (pictogramindex) of one page when we click nextbutton on another screen.I will explain briefly , I have 2 screens in my scenario the first screen contains an image and it's name , a textfield and nextbutton (i have provided a dummy data contains a list of image and it's names) the logic behind this is , when we complete the textfield box and click next button(after validate) the textfield value checks with the correctvalue which i was given in the dummy data and show it's synonym which also provided. when we click the next button we will go to another page which contains the correct answer(passed from first page) and a textfield in this the user can write about the correct answer ( validated) when click next button in this page (till this my applicationworks perfectly) i want to load the first page with it's index updated (+1) which i initialised as 0 (var pictogramindex=0). But in my case when coming back to first page the index is not updating it will automatically stores the initialised value. what i want is i want to update index on the first page when i click next button in the Second page .
my source code of first screen is shown here
class Pictogramscreen extends StatefulWidget {
final int length;
const Pictogramscreen({Key key, this.length}) : super(key: key);
#override
_PictogramscreenState createState() => _PictogramscreenState();
}
class _PictogramscreenState extends State<Pictogramscreen> {
#override
final _Key = GlobalKey<FormState>();
Color defaultcolor = Colors.blue[50];
Color trueColor = Colors.green;
Color falseColor = Colors.red;
Widget defcorrect = Text('');
var pictogramindex = 0;
TextEditingController usertitleInput = TextEditingController();
nextPictogram() {
setState(() {
pictogramindex++;
});
}
fillColor() {
setState(() {
usertitleInput.text == pictdata[pictogramindex]['pictcorrectword']
? defaultcolor = trueColor
: defaultcolor = falseColor;
});
}
correctText() {
setState(() {
usertitleInput.text == pictdata[pictogramindex]['pictcorrectword']
? defcorrect = Text(pictdata[pictogramindex]['pictsynonym'])
: defcorrect = Text(pictdata[pictogramindex]['pictcorrectword']);
});
}
reset() {
setState(() {
defaultcolor = Colors.blue[50];
defcorrect = Text('');
usertitleInput.clear();
});
}
void description(BuildContext ctx) {
Navigator.of(context).pushNamed('/user-description', arguments: {
'id': pictdata[pictogramindex]['pictid'],
'word': pictdata[pictogramindex]['pictcorrectword']
});
}
Widget build(BuildContext context) {
int length = pictdata.length;
return Scaffold(
body: pictogramindex < pictdata.length
? ListView(
children: <Widget>[
Container(
margin: EdgeInsets.only(top: 20),
padding: EdgeInsets.all(15),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Card(
margin: EdgeInsets.only(top: 20),
child: Image.network(
pictdata[pictogramindex]['pictimg']),
),
SizedBox(
height: 10,
),
Text(
pictdata[pictogramindex]['pictword'],
style: TextStyle(
fontSize: 25,
),
),
SizedBox(
height: 10,
),
//Card(
//color: Colors.blue,
// child: TextField(
// decoration: InputDecoration.collapsed(
// hintText: 'type here'),
//textAlign: TextAlign.center,
// onSubmitted: (value) {
// usertitleInput = value;
// print(usertitleInput);
// },
// ),
//),
Form(
key: _Key,
child: TextFormField(
controller: usertitleInput,
validator: (usertitleInput) {
if (usertitleInput.isEmpty) {
return 'Answer cannot be empty';
} else {
return null;
}
},
textAlign: TextAlign.center,
decoration: InputDecoration(
enabledBorder: OutlineInputBorder(
borderSide:
BorderSide(color: Colors.blueAccent),
borderRadius: BorderRadius.all(
Radius.circular(15),
)),
labelText: 'Type your Answer',
filled: true,
fillColor: defaultcolor,
),
onFieldSubmitted: (value) {
usertitleInput.text = value;
fillColor();
correctText();
print(usertitleInput.text);
}),
),
SizedBox(
height: 10,
),
defcorrect,
SizedBox(
height: 10,
),
RaisedButton(
onPressed: () {
if (_Key.currentState.validate()) {
description(context);
// nextPictogram();
reset();
}
//
//if (_Key.currentState.validate() == correctText()) {
// nextPictogram;
// }
},
child: Text('Next'),
)
],
),
),
],
)
: Center(
child: Text('completed'),
));
}
}
my source code of the second screen is show here
class Userinputscreen extends StatefulWidget {
final String id;
final String word;
const Userinputscreen({Key key, this.id, this.word}) : super(key: key);
#override
_UserinputscreenState createState() => _UserinputscreenState();
}
class _UserinputscreenState extends State<Userinputscreen> {
final _Keey = GlobalKey<FormState>();
TextEditingController userdescription = TextEditingController();
var pictogramindex;
void nextpict(BuildContext context) {
Navigator.of(context).pushNamed('/main-screen');
}
// void nextpict(BuildContext context, int index) {
// Navigator.push(
// context,
// MaterialPageRoute(
// builder: (ctx) => Pictogramscreen(
// index: i = 0,
// )));
// }
#override
Widget build(BuildContext context) {
final routeArgs =
ModalRoute.of(context).settings.arguments as Map<String, String>;
final correctWord = routeArgs['word'];
return MaterialApp(
home: Scaffold(
body: ListView(children: <Widget>[
Padding(
padding: EdgeInsets.only(top: 50),
child: Center(
child: Container(
padding: EdgeInsets.all(20),
margin: EdgeInsets.only(top: 100),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
correctWord,
style: TextStyle(fontSize: 26),
),
SizedBox(
height: 10,
),
Form(
key: _Keey,
child: TextFormField(
controller: userdescription,
validator: (userdescription) {
if (userdescription.isEmpty) {
return 'Answer cannot be empty';
} else {
return null;
}
},
textAlign: TextAlign.center,
decoration: InputDecoration(
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.blueAccent),
borderRadius: BorderRadius.all(
Radius.circular(15),
)),
labelText: 'Type your Answer',
filled: true,
),
onFieldSubmitted: (value) {
userdescription.text = value;
print(userdescription.text);
}),
),
SizedBox(
height: 10,
),
RaisedButton(
onPressed: () {
if (_Keey.currentState.validate()) {
nextpict(context);
}
},
child: Text('Next'),
)
],
),
),
),
),
])),
);
}
}
If I get it right, you basically want to tell the initial page that it's state is updated(the index) elsewhere. You basically need to make your app "reactive".
As is said in Google Developers Tutorial:
One of the advantages of Flutter is that it uses reactive views, which you can take to the next level by also applying reactive principles to your app’s data model.
Use some sort of state management. You need to choose from and use either Bloc, InheritedWidget and InheritedModel, Provider(ScopedModel), or the like.
Check this article on flutter about state management, or this for a complete list of approaches
I have a Dialog class in which I want to show different designations that could be assigned to an employee.
In the beginning, I tried to use only a RaisedButton to select the desired designations. Within the App, the Button should change Colors. This part is found within a StatefulWidget.
I also tried a modified version, where I created a new StatefulWidget only for the Dialog part but this part did not have any effect, thus I thought to implement a SwitchListTile to do the same thing.
The SwitchListTile gets activated and deactivated although only the true value gets registered. This means that when I deactivate (swipe to left) the code does not go within the following setState:
setState(() { hEnabled[hDesignations[index].designation] = value; });
Also when the hEnabled Map gets changed within the setState method the following code does not re-run to change the color of the container:
color: hEnabled[hDesignations[index].designation] ? Colors.green : Colors.grey,
Part with the Dialog:
Widget buildChooseDesignations(
BuildContext context, List<Designation> hDesignations) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadiusDirectional.circular(8.0),
),
child: _buildDialogChild(context, hDesignations),
);
}
_buildDialogChild(BuildContext context, List<Designation> hDesignations) {
//todo: when editing an employee I need the chosen designations (have to pass a list)
Map<String, bool> hEnabled = new Map<String, bool>();
for (var i = 0; i < hDesignations.length; i++) {
hEnabled[hDesignations[i].designation] = false;
}
return Container(
height: 200.0,
//todo: width not working properly
width: 50,
child: Column(
children: <Widget>[
Expanded(
child: ListView.builder(
itemCount: hDesignations.length,
itemBuilder: (context, index) {
return Row(
children: <Widget>[
Expanded(
child: Container(
width: 10,
color: hEnabled[hDesignations[index].designation]
? Colors.green
: Colors.grey,
padding: EdgeInsets.only(left: 80),
child: Text(hDesignations[index].designation,
style: TextStyle(fontWeight: FontWeight.bold),),
),
),
Expanded(
child: SwitchListTile(
value: hEnabled[hDesignations[index].designation],
onChanged: (bool value) {
setState(() {
hEnabled[hDesignations[index].designation] =
value;
});
}),
)
],
);
}),
),
SizedBox(
height: 15.0,
),
RaisedButton(
color: Colors.blueGrey,
child: Text(
'set',
style: TextStyle(color: Colors.white),
),
onPressed: () {
//todo: save the 'newly' selected designations in a list on set click
},
)
],
),
);
}
The Dialog is called when I click on the Add + FlatButton and looks like this:
ButtonTheme(
height: 30.0,
// child: Container(),
child: FlatButton(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
color: Colors.blueGrey.shade200,
onPressed: () {
//todo add Dialog
// List<Designation> hList = state.designations;
showDialog(
context: context,
builder: (context) => buildChooseDesignations(
context, state.designations));
// DesignationDialog(
// designations:state.designations));
},
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0)),
child: Text(
'Add +',
style: TextStyle(color: Colors.black),
),
),
),
Found the problem :)
First I did re-write everything into a new StatefulWidget. This I needed since I want that my widget gets re-build after I click on the SwitchListTile to re-color my Container.
Then I had to move my hEnabled (re-named hChecked) map outside the state. The reason was that the widget would re-build all the everything including the initialization of this map, making the user's input useless.
The same applies to the RaisedButton Widget.
Here is my code:
class DesignationDialog extends StatefulWidget {
final List<Designation> designations;
final Map<String, bool> hChecked;
DesignationDialog({Key key, this.designations, this.hChecked}) : super(key: key);
#override
_DesignationDialogState createState() => _DesignationDialogState();
}
class _DesignationDialogState extends State<DesignationDialog> {
_buildDialogChild(BuildContext context, List<Designation> hDesignations) {
//todo: when editing an employee I need the chosen designations (have to pass a list)
// for (var i = 0; i < hDesignations.length; i++) {
// hChecked[hDesignations[i].designation] = false;
// }
return Container(
height: 200.0,
child: Column(
children: <Widget>[
Expanded(
child: ListView.builder(
itemCount: hDesignations.length,
itemBuilder: (context, index) {
// return ButtonTheme(
// //todo: fix the width of the buttons is not working
// minWidth: 20,
// child: RaisedButton(
// color: widget.hChecked[hDesignations[index].designation]
// ? Colors.green
// : Colors.grey,
// child: Text(hDesignations[index].designation),
// onPressed: () {
// //todo mark designation and add to an array
// setState(() {
// widget.hChecked[hDesignations[index].designation] =
// !widget
// .hChecked[hDesignations[index].designation];
// });
// },
// ),
// );
// -- With Switch
return Row(
children: <Widget>[
Expanded(
child: Container(
child: Text(hDesignations[index].designation),
width: 10,
color: widget.hChecked[hDesignations[index].designation]
? Colors.green
: Colors.grey,
)),
Expanded(
child: SwitchListTile(
value: widget.hChecked[hDesignations[index].designation],
onChanged: (bool value) {
setState(() {
widget.hChecked[hDesignations[index].designation] =
value;
});
}),
)
],
);
// -- end
}),
),
SizedBox(
height: 15.0,
),
RaisedButton(
color: Colors.blueGrey,
child: Text(
'set',
style: TextStyle(color: Colors.white),
),
onPressed: () {
//todo: save the 'newly' selected designations in a list on set click
},
)
],
),
);
}
#override
Widget build(BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadiusDirectional.circular(8.0),
),
child: _buildDialogChild(context, widget.designations),
);
}
I'm trying to insert TextFormField on a click to take the name of the student. This thing is working fine. But the problem is when I integrate remove functionality than it's not working as expected.
I did take List<Student> to insert and remove items and converted this List into Map to plot items to UI and update user input name value to a specific indexed Student object value.
If we try adding items and removing them serially than it'll work fine but the only issue is when I remove a single item from in-between it will only update my List and Map but UI will not get updated. This is my code
import 'package:dynamic_input_add_flutter/student.dart';
import 'package:flutter/material.dart';
class SingleListUse extends StatefulWidget {
static final String tag = 'single-list-use';
#override
_SingleListUseState createState() => _SingleListUseState();
}
class Student1 {
String _name;
int _sessionId;
Student1(this._name, this._sessionId);
String get name => _name;
set name(String value) {
_name = value;
}
int get sessionId => _sessionId;
set sessionId(int value) {
_sessionId = value;
}
#override
String toString() {
return 'Student $_name from session $_sessionId';
}
}
class _SingleListUseState extends State<SingleListUse> {
List<Student1> _studentList = [];
Map<int, Student1> _studentMap = {};
void _addNewStudent() {
setState(() {
_studentList.add(Student1('', 1));
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
child: Icon(
Icons.done,
color: Colors.white,
),
onPressed: () {
if (_studentList.length != 0) {
_studentList.forEach((student) => print(student.toString()));
} else {
print('map list empty');
}
},
),
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
floatingActionButtonAnimator: FloatingActionButtonAnimator.scaling,
appBar: AppBar(
title: Text('Single Map Use'),
actions: <Widget>[
FlatButton(
onPressed: () {
_addNewStudent();
},
child: Icon(
Icons.add,
color: Colors.white,
),
)
],
),
body: Container(
padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 20.0),
child: Builder(
builder: (context) {
print("List : ${_studentList.toString()}");
_studentMap = _studentList.asMap();
print("MAP : ${_studentMap.toString()}");
return ListView.builder(
itemCount: _studentMap.length,
itemBuilder: (context, position) {
print('Item Position $position');
return Padding(
padding: EdgeInsets.only(top: 5.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Expanded(
child: TextFormField(
initialValue: _studentMap[position].name.length != 0
? _studentMap[position].name
: '',
onFieldSubmitted: (name) {
setState(() {
_studentList[position].name = name;
});
},
decoration: InputDecoration(
hintText: 'enter student name',
hintStyle: TextStyle(
fontSize: 16.0,
color: Colors.black26,
),
border: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.black12,
),
borderRadius: BorderRadius.all(
Radius.circular(15.0),
),
),
),
),
),
IconButton(
icon: Icon(
Icons.delete,
color: Colors.red,
),
onPressed: () {
setState(() {
_studentList.removeAt(position);
});
},
)
],
),
);
},
);
},
),
),
);
}
}
The first image is when we add a Student name in plus icon click.(every item in List is a TextFormField. When I remove the second item from UI it will remove 3rd one while technically from data structure that I've used (List & Map) it's removing 2nd (and that's ok). I have an issue of displayed UI after we perform any delete from between.
Since this is state-full widget make a variable bool showTextFormField = false in state class
now in widget use if(showTextFormField) <Widget>
now on button click
setState(){
showTextFormField = true;
}
Hi In my App I have something like this.
where I have a dropdown which displaying 3 options, but is there any way I can select multiple options inside the dropdown in flutter? and to store the result of selected options inside the list?
or is it possible to do something like below in flutter?
Thanks.
Code:-
class CustomMultiselectDropDown extends StatefulWidget {
final Function(List<String>) selectedList;
final List<String> listOFStrings;
CustomMultiselectDropDown(
{required this.selectedList, required this.listOFStrings});
#override
createState() {
return new _CustomMultiselectDropDownState();
}
}
class _CustomMultiselectDropDownState extends State<CustomMultiselectDropDown> {
List<String> listOFSelectedItem = [];
String selectedText = "";
#override
Widget build(BuildContext context) {
var size = MediaQuery.of(context).size;
return Container(
margin: EdgeInsets.only(top: 10.0),
decoration:
BoxDecoration(border: Border.all(color: PrimeDentalColors.grey1)),
child: ExpansionTile(
iconColor: PrimeDentalColors.grey,
title: Text(
listOFSelectedItem.isEmpty ? "Select" : listOFSelectedItem[0],
style: GoogleFonts.poppins(
textStyle: TextStyle(
color: PrimeDentalColors.grey,
fontWeight: FontWeight.w400,
fontSize: 15.0,
),
),
),
children: <Widget>[
new ListView.builder(
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: widget.listOFStrings.length,
itemBuilder: (BuildContext context, int index) {
return Container(
margin: EdgeInsets.only(bottom: 8.0),
child: _ViewItem(
item: widget.listOFStrings[index],
selected: (val) {
selectedText = val;
if (listOFSelectedItem.contains(val)) {
listOFSelectedItem.remove(val);
} else {
listOFSelectedItem.add(val);
}
widget.selectedList(listOFSelectedItem);
setState(() {});
},
itemSelected: listOFSelectedItem
.contains(widget.listOFStrings[index])),
);
},
),
],
),
);
}
}
class _ViewItem extends StatelessWidget {
String item;
bool itemSelected;
final Function(String) selected;
_ViewItem(
{required this.item, required this.itemSelected, required this.selected});
#override
Widget build(BuildContext context) {
var size = MediaQuery.of(context).size;
return Padding(
padding:
EdgeInsets.only(left: size.width * .032, right: size.width * .098),
child: Row(
children: [
SizedBox(
height: 24.0,
width: 24.0,
child: Checkbox(
value: itemSelected,
onChanged: (val) {
selected(item);
},
activeColor: PrimeDentalColors.blue,
),
),
SizedBox(
width: size.width * .025,
),
Text(
item,
style: GoogleFonts.poppins(
textStyle: TextStyle(
color: PrimeDentalColors.grey,
fontWeight: FontWeight.w400,
fontSize: 17.0,
),
),
),
],
),
);
}
}
You could achieve that by using a custom widget as a child of the DropdownMenuItem, where the custom widget would need to be stateful so it can handle it's own state to show a check mark or something. And it should have it's own onTap method, so the DropdownMenuItem onTap won't trigger and select the option, dismissing the dropdown. You will also need to have an option to finalize the selection.
But I reccommend you to look another approach for this case for a better usability, like a dialog where you can select multiple options: Is there an equivalent widget in flutter to the "select multiple" element in HTML
You can use the following package
https://pub.dev/packages/multiselect
it has a dropdown based implementation instead of Dialog to show options.
PS: I needed this feature in a recent project and had to create my own widget. this is my implementation.