Flutter raw autocomplete suggestions get hidden under soft keyboard - flutter

I'm creating a raw auto complete widget.
The issue is if the widget is at the center or around the bottom of the screen, when I start typing the auto suggestions shown gets hidden under the soft keyboard. How to build the optionsViewBuilder to overcome the hiding of the options under the keyboard?
Sample source code:
class AutoCompleteWidget extends StatefulWidget {
const AutoCompleteWidget(
Key key,
) : super(key: key);
#override
_AutoCompleteWidgetState createState() => _AutoCompleteWidgetState();
}
class _AutoCompleteWidgetState extends State<AutoCompleteWidget> {
late TextEditingController _textEditingController;
String? _errorText;
final FocusNode _focusNode = FocusNode();
final GlobalKey _autocompleteKey = GlobalKey();
List<String> _autoSuggestions = ['abc', 'def', 'hij', 'aub', 'bted' 'donfr', 'xyz'];
#override
void initState() {
super.initState();
_textEditingController = TextEditingController();
}
#override
void dispose() {
_textEditingController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return RawAutocomplete<String>(
key: _autocompleteKey,
focusNode: _focusNode,
textEditingController: _textEditingController,
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text == '') {
return _autoSuggestions;
}
return _autoSuggestions.where((dynamic option) {
return option
.toString()
.toLowerCase()
.startsWith(textEditingValue.text.toLowerCase());
});
},
optionsViewBuilder: (BuildContext context,
AutocompleteOnSelected<String> onSelected, Iterable<String> options) {
return Material(
elevation: 4.0,
child: ListView(
children: options
.map((String option) => GestureDetector(
onTap: () {
onSelected(option);
},
child: ListTile(
title: Text(option),
),
))
.toList(),
),
);
},
fieldViewBuilder: (
BuildContext context,
TextEditingController textEditingController,
FocusNode focusNode,
VoidCallback onSubmitted,
) {
return Card(
elevation: (null == _errorText ? 8 : 0),
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
child: TextField(
controller: textEditingController,
focusNode: focusNode,
),
);
},
);
}
}

A solution I came up with was using was building my own version of a simple autocomplete widget using a TextFormField and setting scrollPadding on it. I'm showing the results in a container with a set height that works with that padding.
#override
Widget build(BuildContext context) {
return ListView(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
children: [
// THE AUTOCOMPLETE INPUT FIELD
TextFormField(
focusNode: _focusNode,
scrollPadding: const EdgeInsets.only(bottom: 300),
maxLines: null,
key: const ValueKey('company_address'),
autocorrect: false,
enableSuggestions: false,
controller: widget.textEditingController,
validator: (value) {
if (value!.isEmpty) {
return _i10n.enterAName;
}
return null;
},
decoration: InputDecoration(
labelText: widget.labelText,
),
textInputAction: TextInputAction.next,
onChanged: (_) {
_handleChange();
widget.onChange();
},
onTap: () {
setState(() {
_showAutocompleteSuggestions = true;
});
},
),
const SizedBox(
height: 5.0,
),
// THE AUTOCOMPLETE RESULTS
if (_showAutocompleteSuggestions)
Container(
// height: _autocompleteHeight,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
boxShadow: const [
BoxShadow(blurRadius: 10.0, color: Colors.black12)
],
color: Colors.white,
),
constraints: const BoxConstraints(maxHeight: 200.0),
child: Scrollbar(
child: SingleChildScrollView(
child: Column(children: [
if (_autocompleteSuggestions.isEmpty)
const ListTile(
title: Text('No results'),
)
else
..._autocompleteSuggestions.map((_autocompleteSuggestion) =>
Material(
child: InkWell(
onTap: () {
_handleSelectSuggestion(_autocompleteSuggestion);
},
child: ListTile(
leading: const Icon(Icons.location_on_outlined),
title: Text(_autocompleteSuggestion.description),
),
),
))
]),
),
),
),
],
);
}
Forgive the quick code dump. 😁

You should use SingleChildScrollView on your screen where RawAutocomplete places with reverse: true property.
Just like beneath:
child: Center(
child: SingleChildScrollView(
reverse: true,
child: Column()

You could use some constraints to achieve the behavior you want.
First of all, place the root widget as a child of LayoutBuilder to get the layout constraints (I also used a Align top place the options view better).
After that, you can use a ConstrainedBox as the parent of your options view.
You can customize these constraints as you want. The example below is set to have half screen height as the max height of the options view minus the bottom view inset (dynamic as the state of the soft keyboard).
The code you gave on your example would be something like this:
#override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.topLeft,
child: LayoutBuilder(
builder: (context, constraints) => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
),
child: RawAutocomplete<String>(
key: _autocompleteKey,
focusNode: _focusNode,
textEditingController: _textEditingController,
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text == '') {
return _autoSuggestions;
}
return _autoSuggestions.where((dynamic option) {
return option
.toString()
.toLowerCase()
.startsWith(textEditingValue.text.toLowerCase());
});
},
optionsViewBuilder: (BuildContext context,
AutocompleteOnSelected<String> onSelected,
Iterable<String> options) {
return Container(
margin: EdgeInsets.symmetric(horizontal: 16),
child: Material(
elevation: 4.0,
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: constraints.biggest.width,
maxHeight: (MediaQuery.of(context).size.height / 2) -
(MediaQuery.of(context).viewInsets.bottom / 4),
),
child: ListView(
children: options
.map((String option) => GestureDetector(
onTap: () {
onSelected(option);
},
child: ListTile(
title: Text(option),
),
))
.toList(),
),
),
),
);
},
fieldViewBuilder: (
BuildContext context,
TextEditingController textEditingController,
FocusNode focusNode,
VoidCallback onSubmitted,
) {
return Card(
elevation: (null == _errorText ? 8 : 0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0)),
child: TextField(
controller: textEditingController,
focusNode: focusNode,
),
);
},
),
),
),
);
}

Related

Changing Text regarding to the TextField Simultaneously

I have a custom Text widget Named Dynamic_Game_Preview , and also a TextField.
I want the Dynamic_Game_Preview to be changed with the change of the TextField.
I used onChanged method for the TextField but all the letters are shown separately in the Dynamic_Game_Preview. How can I handle this changes to be applied in the same Dynamic_Game_Preview simultaneously with changing the TextField?
Here is my code:
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:pet_store/widgets/dynamic_game_preview.dart';
import 'main.dart';
class Dynamic_Game extends StatefulWidget {
const Dynamic_Game({Key? key}) : super(key: key);
#override
State<Dynamic_Game> createState() => _Dynamic_GameState();
}
class _Dynamic_GameState extends State<Dynamic_Game> {
TextEditingController nameController = TextEditingController();
List<String> names = [];
bool isLoading = false;
List<Dynamic_Game_Preview> dynamicList = [];
void initState() {
super.initState();
dynamicList = [];
names = [];
}
void addNames() {
if (names.length == 1) {
names = [];
}
names.add(nameController.text);
nameController.clear();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.indigo,
title: const Text('Dynamic Game'),
leading: GestureDetector(
child: const Icon(
Icons.arrow_back_ios,
color: Colors.white,
),
onTap: () {
// Navigator.pop(context);
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(
builder: (BuildContext context) => const HomePage(),
),
(route) => false,
);
},
),
),
body: GestureDetector(
onTap: () {
FocusScope.of(context).requestFocus(FocusNode());
},
child: Center(
child: Column(
children: <Widget>[
SizedBox(height: 20),
Container(
margin: const EdgeInsets.symmetric(horizontal: 30),
child: TextField(
controller: nameController,
onChanged: (value) {
setState(() {
addNames();
});
},
decoration: const InputDecoration(
labelText: 'Enter a Pet Name',
),
),
),
SizedBox(height: 10),
Flexible(
fit: FlexFit.loose,
child: ListView.builder(
shrinkWrap: true,
itemCount: names.length,
itemBuilder: (_, index) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 5.0, vertical: 3.0),
child: Dynamic_Game_Preview(nameController.text),
);
},
),
),
],
),
),
),
);
}
}
Problem
Your code clears the nameController:
void addNames() {
...
nameController.clear();
}
Then the code is trying to display nameController.text, which just got cleared:
Dynamic_Game_Preview(nameController.text)
Solution
Something along these lines should work:
itemBuilder: (_, index) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 5.0, vertical: 3.0),
child: Dynamic_Game_Preview(names[index]),
);
},
Last but not least
This probably is not needed:
onTap: () {
FocusScope.of(context).requestFocus(FocusNode());
},

How to make just one ExpansionPanel, in an ExpansionPanelList different to the others? flutter

As the question suggests I have an ExpansionPanelList, one ExpansionPanel (the last one or the 7th one) should have 2 additional buttons, but how can I add them just in this one last panel & not in all the others as well?
This is the code of my whole Expansion panel, as Im not sure where you have to add the behaviour, but guessing in the body of the ExpansionPanel (close to line 40):
class ExpansionList extends StatefulWidget {
final Info info;
const ExpansionList({
Key key,
this.info,
}) : super(key: key);
#override
_ExpansionListState createState() => _ExpansionListState();
}
class _ExpansionListState extends State<ExpansionList> {
Widget _buildListPanel() {
return Container(
child: Theme(
data: Theme.of(context).copyWith(
cardColor: Color(0xffDDBEA9),
),
child: ExpansionPanelList(
dividerColor: Colors.transparent,
elevation: 0,
expansionCallback: (int index, bool isExpanded) {
setState(() {
infos[index].isExpanded = !isExpanded;
});
},
children: infos.map<ExpansionPanel>((Info info) {
return ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return ListTile(
title: !isExpanded
? Text(
info.headerValue,
) //code if above statement is true
: Text(
info.headerValue,
textScaleFactor: 1.3,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
);
},
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
decoration: BoxDecoration(
color: Color(0xffFFE8D6),
borderRadius: BorderRadius.circular(25)),
child: Column(
children: [
ListView.separated(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
padding: EdgeInsets.only(left: 40.0,),
itemCount: info.expandedValueData.length,
itemBuilder: (context, index) {
return CheckboxListTile(
title: Text(info.expandedValueData[index].title,
style: TextStyle(
decoration: info.expandedValueData[index]
.completed
? TextDecoration.lineThrough
: null)),
value: info.expandedValueData[index].completed,
onChanged: (value) {
setState(() {
// Here you toggle the checked item state
infos.firstWhere(
(currentInfo) => info == currentInfo)
..expandedValueData[index].completed =
value;
});
});
},
separatorBuilder: (BuildContext context, int index) {
return SizedBox(
height: 20,
);
},
),
Row(children: [
SizedBox(
width: MediaQuery.of(context).size.width * 0.16),
Text("Abschnitt bis zum Neustart löschen"),
SizedBox(
width: MediaQuery.of(context).size.width * 0.11),
IconButton(
icon: Icon(Icons.delete),
onPressed: () {
setState(() {
infos.removeWhere(
(currentInfo) => info == currentInfo);
});
},
)
]),
],
),
),
),
isExpanded: info.isExpanded);
}).toList(),
),
),
);
}
#override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Container(
child: _buildListPanel(),
),
);
}
}
Thanks for suggestions!
Hi Just add a field (if you already do not have one) in the info object that will allow you to change the widget that is inflated based on that field.
For example
...
children: infos.map<ExpansionPanel>((Info info) {
return ExpansionPanel(
headerBuilder: (BuildContext context, bool isExpanded) {
return info.type == TYPE_A ? TypeAWidgetHeader(info) : TypeBWidgetHeader(info);
body: info.type == TYPE_A ? TypeAWidgetBody(info) : TypeBWidgetBody(info);
...

how to use TextEditingController from Autocomplete widget Flutter

I need to use TexteditingController of the widget "autocomplete".
is to use the clear function when a stepper changes stage
I need to do that since if I go back a stage the text entered previously remains
this is the autocomplete code:
Autocomplete<Profesional>(
optionsViewBuilder: (BuildContext context,
AutocompleteOnSelected<Profesional> onSelected,
Iterable<Profesional> options) {
return Align(
alignment: Alignment.topLeft,
child: Material(
elevation: 4.0,
child: SizedBox(
height: 200.0,
child: ListView.builder(
padding: const EdgeInsets.all(8.0),
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
final Profesional option =
options.elementAt(index);
return GestureDetector(
onTap: () {
onSelected(option);
},
child: ListTile(
title: Text(option.cod),
),
);
},
),
),
),
);
},
optionsBuilder: (TextEditingValue query) {
return viewModel.efectores.where((efector) {
return efector.cod
.toLowerCase()
.contains(query.text.toLowerCase()) ||
efector.nombre
.toLowerCase()
.contains(query.text.toLowerCase());
});
},
fieldViewBuilder: (BuildContext context,
TextEditingController textEditingController,
FocusNode focusNode,
VoidCallback onFieldSubmitted) {
return TextFormField(
controller: textEditingController,
decoration: const InputDecoration(
hintText: 'Seleccione Efector',
),
autofocus: true,
focusNode: focusNode,
onFieldSubmitted: (String value) {
onFieldSubmitted();
},
);
},
displayStringForOption: (efector) {
return efector.cod + ' - ' + efector.nombre;
},
onSelected: (efector) {
viewModel.efector = efector;
}),
You can use RawAutocomplete instead of Autocomplete.
In this case, you can pass your own TextEditingController and FocusNode. Then use the TextEditingController clear method to clear text if when needed.
If you need to clear the autocomplete view from the parent widget user global key.
See sample code here:
class CustomAutocomplete extends StatelessWidget {
final TextEditingController _textEditingController = TextEditingController();
final FocusNode _focusNode = FocusNode();
final GlobalKey _autocompleteKey = GlobalKey();
final List<String> _options = <String>[
'aardvark',
'bobcat',
'chameleon',
];
CustomAutocomplete({Key? key}) : super(key: key);
void clear() {
_textEditingController.clear();
}
#override
Widget build(BuildContext context) {
return RawAutocomplete<String>(
key: _autocompleteKey,
focusNode: _focusNode,
textEditingController: _textEditingController,
optionsBuilder: (TextEditingValue textEditingValue) {
return _options.where((String option) {
return option.contains(textEditingValue.text.toLowerCase());
}).toList();
},
optionsViewBuilder: (BuildContext context,
AutocompleteOnSelected<String> onSelected, Iterable<String> options) {
return Material(
elevation: 4.0,
child: ListView(
children: options
.map((String option) => GestureDetector(
onTap: () {
onSelected(option);
},
child: ListTile(
title: Text(option),
),
))
.toList(),
),
);
},
);
}
}

Passing value to previous widget

I have simple form , inside it have CircularAvatar when this is pressed show ModalBottomSheet to choose between take picture from gallery or camera. To make my widget more compact , i separated it to some file.
FormDosenScreen (It's main screen)
DosenImagePicker (It's only CircularAvatar)
ModalBottomSheetPickImage (It's to show ModalBottomSheet)
The problem is , i don't know how to passing value from ModalBottomSheetPickImage to FormDosenScreen. Because value from ModalBottomSheetPickImage i will use to insert operation.
I only success passing from third Widget to second Widget , but when i passing again from second Widget to first widget the value is null, and i think the problem is passing from Second widget to first widget.
How can i passing from third Widget to first Widget ?
First Widget
class FormDosenScreen extends StatefulWidget {
static const routeNamed = '/formdosen-screen';
#override
_FormDosenScreenState createState() => _FormDosenScreenState();
}
class _FormDosenScreenState extends State<FormDosenScreen> {
String selectedFile;
#override
Widget build(BuildContext context) {
final detectKeyboardOpen = MediaQuery.of(context).viewInsets.bottom;
print('trigger');
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text('Tambah Dosen'),
actions: <Widget>[
PopupMenuButton(
itemBuilder: (_) => [
PopupMenuItem(
child: Text('Tambah Pelajaran'),
value: 'add_pelajaran',
),
],
onSelected: (String value) {
switch (value) {
case 'add_pelajaran':
Navigator.of(context).pushNamed(FormPelajaranScreen.routeNamed);
break;
default:
}
},
)
],
),
body: Stack(
fit: StackFit.expand,
children: <Widget>[
SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
SizedBox(height: 20),
DosenImagePicker(onPickedImage: (file) => selectedFile = file),
SizedBox(height: 20),
Card(
margin: const EdgeInsets.symmetric(horizontal: 15, vertical: 10),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
TextFormFieldCustom(
onSaved: (value) {},
labelText: 'Nama Dosen',
),
SizedBox(height: 20),
TextFormFieldCustom(
onSaved: (value) {},
prefixIcon: Icon(Icons.email),
labelText: 'Email Dosen',
keyboardType: TextInputType.emailAddress,
),
SizedBox(height: 20),
TextFormFieldCustom(
onSaved: (value) {},
keyboardType: TextInputType.number,
inputFormatter: [
// InputNumberFormat(),
WhitelistingTextInputFormatter.digitsOnly
],
prefixIcon: Icon(Icons.local_phone),
labelText: 'Telepon Dosen',
),
],
),
),
),
SizedBox(height: kToolbarHeight),
],
),
),
Positioned(
child: Visibility(
visible: detectKeyboardOpen > 0 ? false : true,
child: RaisedButton(
onPressed: () {
print(selectedFile);
},
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
color: colorPallete.primaryColor,
child: Text(
'SIMPAN',
style: TextStyle(fontWeight: FontWeight.bold, fontFamily: AppConfig.headerFont),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
textTheme: ButtonTextTheme.primary,
),
),
bottom: kToolbarHeight / 2,
left: sizes.width(context) / 15,
right: sizes.width(context) / 15,
)
],
),
);
}
}
Second Widget
class DosenImagePicker extends StatefulWidget {
final Function(String file) onPickedImage;
DosenImagePicker({#required this.onPickedImage});
#override
DosenImagePickerState createState() => DosenImagePickerState();
}
class DosenImagePickerState extends State<DosenImagePicker> {
String selectedImage;
#override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.center,
child: InkWell(
onTap: () async {
await showModalBottomSheet(
context: context,
builder: (context) => ModalBottomSheetPickImage(
onPickedImage: (file) {
setState(() {
selectedImage = file;
widget.onPickedImage(selectedImage);
print('Hellooo dosen image picker $selectedImage');
});
},
),
);
},
child: CircleAvatar(
foregroundColor: colorPallete.black,
backgroundImage: selectedImage == null ? null : MemoryImage(base64.decode(selectedImage)),
radius: sizes.width(context) / 6,
backgroundColor: colorPallete.accentColor,
child: selectedImage == null ? Text('Pilih Gambar') : SizedBox(),
),
),
);
}
}
Third Widget
class ModalBottomSheetPickImage extends StatelessWidget {
final Function(String file) onPickedImage;
ModalBottomSheetPickImage({#required this.onPickedImage});
#override
Widget build(BuildContext context) {
return SizedBox(
child: Padding(
padding: const EdgeInsets.all(15.0),
child: Wrap(
alignment: WrapAlignment.spaceEvenly,
children: <Widget>[
InkWell(
onTap: () async {
final String resultBase64 =
await commonFunction.pickImage(quality: 80, returnFile: ReturnFile.BASE64);
onPickedImage(resultBase64);
},
child: CircleAvatar(
foregroundColor: colorPallete.white,
backgroundColor: colorPallete.green,
child: Icon(Icons.camera_alt),
),
),
InkWell(
onTap: () async {
final String resultBase64 =
await commonFunction.pickImage(returnFile: ReturnFile.BASE64, isCamera: false);
onPickedImage(resultBase64);
},
child: CircleAvatar(
foregroundColor: colorPallete.white,
backgroundColor: colorPallete.blue,
child: Icon(Icons.photo_library),
),
),
],
),
),
);
}
}
The cleanest and easiest way to do this is through Provider. It is one of the state management solutions you can use to pass values around the app as well as rebuild only the widgets that changed. (Ex: When the value of the Text widget changes). Here is how you can use Provider in your scenario:
This is how your model should look like:
class ImageModel extends ChangeNotifier {
String _base64Image;
get base64Image => _base64Image;
set base64Image(String base64Image) {
_base64Image = base64Image;
notifyListeners();
}
}
Don't forget to add getters and setters so that you can use notifyListeners() if you have any ui that depends on it.
Here is how you can access the values of ImageModel in your UI:
final model=Provider.of<ImageModel>(context,listen:false);
String image=model.base64Image; //get data
model.base64Image=resultBase64; //set your image data after you used ImagePicker
Here is how you can display your data in a Text Widget (Ideally, you should use Selector instead of Consumer so that the widget only rebuilds if the value its listening to changes):
#override
Widget build(BuildContext context) {
//other widgets
Selector<ImageModel, String>(
selector: (_, model) => model.base64Image,
builder: (_, image, __) {
return Text(image);
},
);
}
)
}
You could achieve this easily. If you are using Blocs.

Deleting widget from list from a press within the widget

I have a list of custom TextFormField's that i added to them a delete icon
all I am trying to do is when I press the delete button it will be deleted from the list and the view
i tried adding a function to my form field with no success
I think my approach isn't the best way to implement what i want, I am open to any idea
here is the code
import 'package:flutter/material.dart';
typedef DeleteCallback = void Function(Key key);
class DynamicFormField extends FormField<String>{
DynamicFormField({
Key key,
FormFieldSetter<String> onSaved,
FormFieldValidator<String> validator,
String initialValue = "",
bool autovalidate = false,
DeleteCallback onDelete(Key key),
}) : super(
onSaved: onSaved,
validator: validator,
initialValue: initialValue,
autovalidate: autovalidate,
builder: (FormFieldState<String> state) {
return Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Expanded(
flex: 5,
child: TextFormField(
decoration: const InputDecoration(
hintText: 'Enter Player Name',
),
onSaved: onSaved,
validator: validator,
initialValue: initialValue,
autovalidate: autovalidate,
),
),
IconButton(
icon: Icon(Icons.delete_outline),
onPressed: onDelete(key)
),
],
);
}
);
}
DynamicFormField(
key: UniqueKey(),
validator: (value) {
if (value.isEmpty) {
return 'Please enter some text';
}
return null;
},
onSaved: (input) => {players.add(Player(input))},
onDelete: f,
),
);
}
void f(Key key){
fields.removeWhere((item) => item.key == key);
}
tnx
I solved it using ListView builder
import 'package:flutter/material.dart';
import 'package:rummy/models/player.dart';
import 'package:rummy/screens/game_screen.dart';
class NewGame extends StatefulWidget {
NewGame({Key key}) : super(key: key);
#override
_NewGameState createState() => _NewGameState();
}
class _NewGameState extends State<NewGame> {
final _formKey = GlobalKey<FormState>();
List<Widget> fields;
List<Player> players;
_NewGameState() {
players = new List<Player>();
fields = new List();
print(players);
fields.add(generateField());
}
Widget generateField() {
return TextFormField(
decoration: const InputDecoration(
hintText: 'Enter Player Name',
),
onSaved: (input) => {players.add(Player(input))},
validator: (value) {
if (value.isEmpty) {
return 'Please enter some text';
}
return null;
},
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SizedBox.expand(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
children: <Widget>[
Form(
key: _formKey,
child: Expanded(
child: ListView(
children: <Widget>[
SizedBox(
height: MediaQuery.of(context).size.height,
child: Builder(
builder: (BuildContext context) {
return ListView.builder(
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemCount: fields.length,
itemBuilder:
(BuildContext context, int postion) {
return Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Expanded(
child: fields[postion],
),
IconButton(
icon: Icon(Icons.delete_outline),
onPressed: () => {
setState(() {
print(postion);
fields.removeAt(postion);
})
}),
],
);
},
);
},
),
)
],
),
)),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
onPressed: () {
print("asdasd");
if (_formKey.currentState.validate()) {
players.clear();
_formKey.currentState.save();
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => GameScreen(players),
));
} else
print(_formKey.currentState.validate());
},
child: Text('Submit'),
),
RaisedButton(
onPressed: () {
setState(() {
fields.add(generateField());
});
},
child: Text('Add New Player'),
),
],
),
],
mainAxisAlignment: MainAxisAlignment.center,
),
),
),
);
}
}
I used this
https://github.com/MobMaxime/Flutter-To-Do-App/blob/master/lib/screens/todo_list.dart