I'm trying to make an auto suggest input which fetches the results from a backend API. Here is my code:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:hello_world/api.dart';
import 'package:hello_world/autocomplete_item.dart';
class Debouncer {
final int milliseconds;
VoidCallback? action;
Timer? _timer;
Debouncer({this.milliseconds = 250});
run(VoidCallback action) {
_timer?.cancel();
_timer = Timer(Duration(milliseconds: milliseconds), action);
}
}
class AutoCompleteInput extends StatefulWidget {
const AutoCompleteInput({
Key? key,
this.label = 'Suggest',
this.textInputAction,
this.validator,
this.errorMessage,
}) : super(key: key);
final String label;
final TextInputAction? textInputAction;
final FormFieldValidator<String>? validator;
final String? errorMessage;
#override
_AutoCompleteInputState createState() => _AutoCompleteInputState();
}
class _AutoCompleteInputState extends State<AutoCompleteInput> {
final _debouncer = Debouncer(milliseconds: 500);
List<AutoCompleteItem> _options = [];
bool _isLoading = false;
#override
Widget build(BuildContext context) {
return Autocomplete<AutoCompleteItem>(
displayStringForOption: (AutoCompleteItem item) => item.value,
optionsBuilder: _optionsBuilder,
fieldViewBuilder: (
BuildContext context,
TextEditingController fieldTextEditingController,
FocusNode fieldFocusNode,
VoidCallback onFieldSubmitted,
) {
return TextFormField(
controller: fieldTextEditingController,
focusNode: fieldFocusNode,
autocorrect: false,
maxLength: 50,
maxLines: 1,
keyboardType: TextInputType.text,
textInputAction: widget.textInputAction,
decoration: InputDecoration(
labelText: widget.label,
contentPadding: EdgeInsets.zero,
errorText: widget.errorMessage,
counterText: '',
suffix: _isLoading
? Padding(
padding: const EdgeInsets.only(right: 1),
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
)
: null,
),
validator: widget.validator,
onFieldSubmitted: (String value) {
onFieldSubmitted();
},
onChanged: (text) {
_debouncer.run(() async {
setState(() => _isLoading = true);
await _fetchResults(text);
setState(() => _isLoading = false);
});
},
);
},
optionsViewBuilder: (
BuildContext context,
AutocompleteOnSelected<AutoCompleteItem> onSelected,
Iterable<AutoCompleteItem> options,
) {
return Align(
alignment: Alignment.topLeft,
child: Material(
elevation: 2,
child: Container(
width: 300,
constraints: BoxConstraints(maxHeight: 250),
child: ListView.builder(
padding: EdgeInsets.all(10.0),
itemCount: options.length,
shrinkWrap: true,
itemBuilder: (BuildContext context, int index) {
final AutoCompleteItem option = options.elementAt(index);
return GestureDetector(
onTap: () {
onSelected(option);
},
child: ListTile(
dense: true,
title: Text(option.value),
),
);
},
),
),
),
);
},
onSelected: (AutoCompleteItem selection) {
print('${selection.type} => ${selection.value}');
},
);
}
Future<void> _fetchResults(String text) async {
// WE PERFORM THE HTTP REQUEST HERE AND THEN ASSIGN THE RESPONSE TO `_options`
setState(() async {
_options = await Api.fetchSuggestions(text);
});
}
Iterable<AutoCompleteItem> _optionsBuilder(
TextEditingValue textEditingValue) {
if (textEditingValue.text == '') {
return const Iterable<AutoCompleteItem>.empty();
}
return _options;
}
}
As you can see, I call _fetchResults method (which fetches the data from the API) inside the onChanged method of TextFormField. But when the data is fetched and the state is updated, there is no overlay of suggestions! I checked the length of _options and it has all of the results but still setState did not forced Autocomplete widget to rebuild it's overlay. Although when I remove the last character of the TextFormField, it immediately shows the overlay. How can I fix this issue?
In the TextField's onChanged method calls the textEditingController.notifyListeners() and ignores the warnings
example
TextField(
onChanged: (text)async{
timer?.cancel();
timer = Timer(const Duration(milliseconds: 700), () async {
await getData(textEditingController).whenComplete((){
setState(() {
// ignore: invalid_use_of_protected_member
//invalid_use_of_visible_for_testing_member
textEditingController.notifyListeners();
});
});
});
},
)
Related
I've recently asked a question on how to create a group of form dynamically. and i've got an answer. but the problem was when removed an index of the group it removes the last added form. but the value is correct.
for example if i add 3 dynamic group formfields and removed the second index index[1] the ui update will remove the last index but the removed value is only the selected index. why is the ui not working as expected?
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart';
class Purchased extends StatefulWidget {
const Purchased({Key? key}) : super(key: key);
#override
State<Purchased> createState() => _PurchasedState();
}
class _PurchasedState extends State<Purchased> {
List<UserInfo> list = [];
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
/// every time you add new Userinfo, it will generate new FORM in the UI
list.add(UserInfo());
setState(() {}); // dont forget to call setState to update UI
},
child: const Icon(Icons.add),
),
body: Column(
children: [
Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: list.length,
itemBuilder: ((context, index) {
return Column(
children: [
Text('phone'),
Text(list[index].phone),
Text('email'),
Text(list[index].email),
Text('category'),
Text(list[index].category)
],
);
})),
),
Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: list.length,
itemBuilder: ((context, index) {
return MyForm(
// dont forget use the key, to make sure every MyForm is has identity. to avoid missed build
key: ValueKey(index),
//pass init value so the widget always update with current value
initInfo: list[index],
// every changes here will update your current list value
onChangePhone: (phoneVal) {
if (phoneVal != null) {
list[index].setPhone(phoneVal);
setState(() {});
}
},
// every changes here will update your current list value
onchangeEmail: (emailVal) {
if (emailVal != null) {
list[index].setEmail(emailVal);
setState(() {});
}
},
onchangeCategory: (categoryVal) {
if (categoryVal != null) {
list[index].setCategory(categoryVal);
setState(() {});
}
},
onremove: () {
list.removeAt(index);
setState(() {});
});
})),
)
],
),
);
}
}
class MyForm extends StatefulWidget {
final UserInfo initInfo;
final Function(String?) onChangePhone;
final Function(String?) onchangeEmail;
final Function(String?) onchangeCategory;
final VoidCallback? onremove;
const MyForm({
key,
required this.initInfo,
required this.onChangePhone,
required this.onchangeEmail,
required this.onchangeCategory,
required this.onremove,
});
#override
State<MyForm> createState() => _MyFormState();
}
class _MyFormState extends State<MyForm> {
TextEditingController _phoneCtrl = TextEditingController();
TextEditingController _emailCtrl = TextEditingController();
String? selected;
final List<String> category = [
'Manager',
'Reception',
'Sales',
'Service',
];
#override
void initState() {
super.initState();
// set init value
_phoneCtrl = TextEditingController(text: widget.initInfo.phone);
_emailCtrl = TextEditingController(text: widget.initInfo.email);
selected = widget.initInfo.category;
}
#override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(12),
child: Column(
children: [
IconButton(onPressed: widget.onremove, icon: Icon(Icons.remove)),
TextFormField(
controller: _phoneCtrl,
onChanged: widget.onChangePhone,
),
TextFormField(
controller: _emailCtrl,
onChanged: widget.onchangeEmail,
),
DropdownButtonFormField2(
//key: _key,
decoration: InputDecoration(
isDense: true,
contentPadding: EdgeInsets.zero,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
),
),
isExpanded: true,
hint: const Text(
'Select Category',
style: TextStyle(fontSize: 14),
),
icon: const Icon(
Icons.arrow_drop_down,
color: Colors.black45,
),
iconSize: 30,
buttonHeight: 60,
buttonPadding: const EdgeInsets.only(left: 20, right: 10),
items: category
.map((item) => DropdownMenuItem<String>(
value: item,
child: Text(
item,
style: const TextStyle(
fontSize: 14,
),
),
))
.toList(),
validator: (value) {
if (value == null) {
return 'Please select Catagory.';
}
},
onChanged: widget.onchangeCategory,
onSaved: widget.onchangeCategory)
/// same like TextFormField, you can create new widget below
/// for dropdown, you have to 2 required value
/// the initValue and the onchage function
],
),
);
}
}
class UserInfo {
///define
String _phone = '';
String _email = '';
String _category = '';
/// getter
String get phone => _phone;
String get email => _email;
String get category => _category;
///setter
void setPhone(String phone) {
_phone = phone;
}
void setEmail(String email) {
_email = email;
}
void setCategory(String category) {
_category = category;
}
}
any help is appreciated.
I new to Flutter and i was trying to find a solution for the below issue for several hours. I have searched and every solution provided does not work form me.
I have page where one of the widgets is the autocomplete text input. I have created this autocomplete widget on different class. I have added this widget as StatefulBuilder within my main widget. it is working fine however, i am not able to access its value so I can store it with other fields.
My code look like
class ItemDetails extends StatefulWidget {
const ItemDetails({Key? key}) : super(key: key);
static const routeName = '/item_details';
#override
State<ItemDetails> createState() => _ItemDetails();
}
class _ItemDetails extends State<ItemDetails> {
late TextEditingController labelController;
late TextEditingController valueController;
late TextEditingController notesController;
bool _submitted = false;
late var args;
String _itemLabel2 = "";
// var labelAutoComp = LabelSugg();
#override
void initState() {
super.initState();
labelController = TextEditingController();
valueController = TextEditingController();
notesController = TextEditingController();
}
#override
void dispose() {
labelController.dispose();
valueController.dispose();
notesController.dispose();
// Hive.close();
super.dispose();
}
String? _labelErrorText(context) {
final text = labelController.value.text;
if (text.isEmpty) {
// return 'Can\'t be empty';
return AppLocalizations.of(context)!.noEmpty;
}
}
String? _valueErrorText(context) {
final text = valueController.value.text;
if (text.isEmpty) {
// return 'Can\'t be empty';
return AppLocalizations.of(context)!.noEmpty;
}
}
#override
Widget build(BuildContext context) {
try {
args = ModalRoute.of(context)!.settings.arguments as Map;
} on Exception catch (e) {
// print(e);
}
// print(args);
return Scaffold(
appBar: AppBar(
title: Text(args['title']),
),
body: Container(
padding: const EdgeInsets.all(20),
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(20),
child: Column(
// mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
LabelSugg(getLabelText: (String val) {
print(val);
_itemLabel2 = val;
}),
TextField(
autofocus: true,
decoration: InputDecoration(
labelText: AppLocalizations.of(context)!.label,
hintText: AppLocalizations.of(context)!.labelHint,
errorText:
_submitted ? _labelErrorText(context) : null,
),
controller: labelController,
onChanged: (_) => setState(() {}),
),
const SizedBox(height: 5),
TextField(
autofocus: false,
decoration: InputDecoration(
labelText: AppLocalizations.of(context)!.value,
hintText: AppLocalizations.of(context)!.valueHint,
errorText:
_submitted ? _valueErrorText(context) : null,
),
controller: valueController,
keyboardType: const TextInputType.numberWithOptions(
decimal: true, signed: false),
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.allow(
RegExp(r"[0-9.]")),
TextInputFormatter.withFunction(
(oldValue, newValue) {
try {
final text = newValue.text;
if (text.isNotEmpty) double.parse(text);
return newValue;
} catch (e) {}
return oldValue;
}),
], // Only numbers can be entered
onChanged: (_) => setState(() {}),
),
const SizedBox(height: 5),
TextField(
autofocus: true,
decoration: InputDecoration(
labelText: AppLocalizations.of(context)!.notes,
hintText: AppLocalizations.of(context)!.noteHint,
),
controller: notesController,
onChanged: (_) => setState(() {}),
),
]),
// ],
),
Expanded(
child: Align(
alignment: FractionalOffset.bottomCenter,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton.icon(
onPressed: () {
setState(() => _submitted = true);
if (_labelErrorText(context) == null &&
_valueErrorText(context) == null) {
//insert
var localLabel = labelController.value.text;
var _localValue = 0.0;
if (valueController.value.text != '') {
_localValue =
double.parse(valueController.value.text);
} else {
_localValue = 0.0;
}
var localNotes = notesController.value.text;
addItemToList(
localLabel, _localValue, localNotes);
Navigator.of(context).pop();
labelController.clear();
valueController.clear();
notesController.clear();
}
},
label: Text(AppLocalizations.of(context)!.add),
icon: const Icon(Icons.save, size: 18),
),
const SizedBox(width: 10),
ElevatedButton.icon(
onPressed: () => {Navigator.pop(context)},
label: Text(AppLocalizations.of(context)!.cancel),
icon: const Icon(Icons.cancel, size: 18),
),
],
)),
),
// )
],
)));
}
void addItemToList(String localLabel, double localValue, String localNotes) {
var _item = YearItems()..yearID = args['year'];
_item.itemLabel = localLabel;
_item.itemValue = localValue;
_item.itemNote = localNotes;
print(_itemLabel2);
final itemsBox = ItemsBoxes.getTransactions();
itemsBox.add(_item);
}
}
my labelAutoComp widget code look like
class LabelSugg extends StatefulWidget {
final ValueChanged<String> getLabelText;
const LabelSugg({Key? key, required this.getLabelText}) : super(key: key);
#override
State<LabelSugg> createState() => _LabelSugg();
}
class _LabelSugg extends State<LabelSugg> {
late TextEditingController fieldTextEditingController2;
#override
void initState() {
super.initState();
}
#override
void dispose() {
super.dispose();
}
getLabel() {
return widget.getLabelText(fieldTextEditingController2.text);
}
#override
Widget build(BuildContext context) {
List<LabelsAc> labelOptions = <LabelsAc>[
LabelsAc(label: AppLocalizations.of(context)!.labelClothes),
LabelsAc(label: AppLocalizations.of(context)!.labelFood),
LabelsAc(label: AppLocalizations.of(context)!.labelPerfumes),
LabelsAc(label: AppLocalizations.of(context)!.labelCapital),
];
return Autocomplete<LabelsAc>(
optionsBuilder: (TextEditingValue textEditingValue) {
return labelOptions
.where((LabelsAc _label) => _label.label
.toLowerCase()
.startsWith(textEditingValue.text.toLowerCase()))
.toList();
},
displayStringForOption: (LabelsAc option) => option.label,
fieldViewBuilder: (BuildContext context,
TextEditingController fieldTextEditingController,
// fieldTextEditingController,
FocusNode fieldFocusNode,
VoidCallback onFieldSubmitted) {
return TextField(
controller: fieldTextEditingController,
focusNode: fieldFocusNode,
style: const TextStyle(fontWeight: FontWeight.bold),
// onChanged: getLabel(),
onChanged: (String val) {
fieldTextEditingController2 = fieldTextEditingController;
getLabel();
});
},
onSelected: (LabelsAc selection) {
fieldTextEditingController2 =
TextEditingController(text: selection.label);
getLabel();
},
optionsViewBuilder: (BuildContext context,
AutocompleteOnSelected<LabelsAc> onSelected,
Iterable<LabelsAc> options) {
return Align(
alignment: Alignment.topLeft,
child: Material(
child: Container(
// width: 350,
// color: Colors.cyan,
child: ListView.builder(
padding: const EdgeInsets.all(10.0),
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
final LabelsAc option = options.elementAt(index);
return GestureDetector(
onTap: () {
onSelected(option);
},
child: ListTile(
title: Text(option.label,
style: const TextStyle(color: Colors.black)),
),
);
},
),
),
),
);
},
);
// ),
// );
}
}
class LabelsAc {
LabelsAc({required this.label});
String label;
}
first is redundant when you wrap your class that extend StatefullWidget with StatefullBuilder. LabelSugg is a component Widget. you can use it like other widget.
benefit to separate widget with StatefullWidget class is, we can update the value inside the class without re-build the current page. which is good for performance. that's why developer recomend to separete with class insted compared to make local method.
as you see, when you create LabelSugg extend StatefullWidget class , we will have _LabelSugg . underscore means that: all variable only accessible on current file.
thats why we can't call getLabel() or other variable from different file.
its used for handle the State in 'LabelSugg` widget.
now how to pass the value from LabelSugg is by created variable outside the state. here you are:
class LabelSugg extends StatefulWidget {
// use this to pass any changes when we use LabelSugg
final ValueChanged<String> getLabelText;
const LabelSugg({Key? key, required this.getLabelText}) : super(key: key);
#override
State<LabelSugg> createState() => _LabelSugg();
}
then we can call the onChaged inside _LabelSugg state. because its Statefull widget, we can acces by : widget.getLabelText()
class _LabelSugg extends State<LabelSugg> {
late TextEditingController fieldTextEditingController;
.....
getLabel() {
return widget.getLabelText(fieldTextEditingController.text);
}
then in other class we call LabelSugg like common widget
import 'package:../labelsug.dart';
class ItemDetails extends StatefulWidget {
.....
return Scaffold(
appBar: AppBar(
title: Text(args['title']),
),
body: Container(
padding: const EdgeInsets.all(20),
child: Column(
children: <Widget>[
// now use it like a widget
LabelSug(
getLabelText: (String val){
print(val);
}
:)
So I am implementing a search function that has a text form field and an overlay entry to display the results. Whenever the text form changes, the filter function is called which should theoretically update the listview in the overlay entry. Strangely, when I filter the list source data, I don't even need to call setstate in order for it to update, however, if I want to introduce an asynchronous method call before I update the data, the list will update properly the next time I press on the keyboard (as if its one step behind on filtering every time). However, if I hot reload everything appears fine.
I want the overlay entry to update on time but I can't seem to get it.
import 'package:flutter/material.dart';
import 'package:geocoding/geocoding.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:app/Databases/college_data.dart';
import 'package:app/Utilities/point_network_helper.dart';
import 'package:app/Widgets/Search_location_container.dart';
import 'package:app/Utilities/app_text_style.dart';
class LocationOverlay extends StatefulWidget {
final Function setLocation;
final LatLng? initialLocationCoordinates;
final String? initalLocation;
LocationOverlay(
{Key? key,
required this.setLocation,
this.initalLocation,
this.initialLocationCoordinates})
: super(key: key);
#override
_LocationOverlayState createState() => _LocationOverlayState();
}
class _LocationOverlayState extends State<LocationOverlay> {
final FocusNode _focusNode = FocusNode();
OverlayEntry? _overlayEntry;
final LayerLink _layerLink = LayerLink();
List<SearchLocationContainer> _allLocations = [];
List<SearchLocationContainer> _selectedLocations = [];
late TextEditingController fieldController;
LatLng? _currentLocationCoordinate;
#override
void initState() {
super.initState();
_focusNode.addListener(() {
if (_focusNode.hasFocus) {
this._overlayEntry = this._createOverlayEntry();
Overlay.of(context)!.insert(this._overlayEntry!);
} else {
this._overlayEntry!.remove();
}
});
fieldController = TextEditingController(text: widget.initalLocation ?? "");
formLocationContainers();
}
void selectLocation(String location, LatLng coordinate) {
_focusNode.unfocus();
fieldController.text = location;
_currentLocationCoordinate = coordinate;
widget.setLocation(coordinate, fieldController.text);
_runFilter(location);
}
void formLocationContainers() {
StaticDataMap.forEach((location, coordinate) {
_allLocations.add(SearchLocationContainer(
location: location,
coordinate: coordinate,
selectLocation: selectLocation));
});
_selectedLocations = _allLocations;
}
void _runFilter(String enteredKeyword) async {
if (enteredKeyword.isEmpty) {
// I want it to use this
setState(() {
_selectedLocations = _allLocations;
}
// but this also works for some reason
_selectedLocations = _allLocations;
} else {
List<SearchLocationContainer> results = [];
results = _allLocations
.where((location) => location.getLocation
.toLowerCase()
.contains(enteredKeyword.toLowerCase()))
.toList();
print("update");
_getOtherLocations(enteredKeyword, results);
}
}
Future<void> _getOtherLocations(
String keyword, List<SearchLocationContainer> initialVals) async {
List<Location> locations =
await PointNetworkHelper().getPointFromAddress(keyword);
List<SearchLocationContainer> newContainers = [];
for (int i = 0; i < locations.length; i++) {
newContainers.add(SearchLocationContainer(
location: locations[i].timestamp.toString(),
coordinate: LatLng(locations[i].latitude, locations[i].longitude),
selectLocation: selectLocation));
}
// same issue here, setstate does nothing to change the behavior
setState(() {
_selectedLocations = initialVals;
_selectedLocations.addAll(newContainers);
}
// this will do the same thing
_selectedLocations = initialVals;
_selectedLocations.addAll(newContainers);
}
OverlayEntry _createOverlayEntry() {
RenderObject? renderBox = context.findRenderObject();
var size = renderBox!.paintBounds;
return OverlayEntry(
builder: (context) => Positioned(
width: size.width,
child: CompositedTransformFollower(
key: UniqueKey(),
link: _layerLink,
showWhenUnlinked: false,
offset: Offset(0.0, size.height - 15),
child: Container(
padding: const EdgeInsets.only(top: 15),
constraints: const BoxConstraints(maxHeight: 200),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(15),
bottomRight: Radius.circular(15)),
),
child: Material(
color: Colors.transparent,
child: ListView.separated(
padding: const EdgeInsets.all(8),
itemBuilder: (context, index) {
return SizedBox(
child: _selectedLocations[index],
width: MediaQuery.of(context).size.width * .8,
);
},
separatorBuilder: (context, index) {
return SizedBox(
height: 8,
);
},
itemCount: _selectedLocations.length,
),
),
),
),
));
}
#override
Widget build(BuildContext context) {
return CompositedTransformTarget(
link: _layerLink,
child: TextFormField(
controller: fieldController,
focusNode: _focusNode,
autocorrect: false,
enableSuggestions: false,
onTap: () {
if (fieldController.text == "") {
_runFilter("");
}
},
keyboardType: TextInputType.streetAddress,
cursorColor: Colors.black,
onChanged: (value) {
_runFilter(value);
},
onFieldSubmitted: (value) {
_runFilter(value);
_focusNode.unfocus();
},
),
);
}
}
Just for example, in a package available on FlutterDev, I did some editing. Below is the code. How can I get my BottomButton to show up as soon as T tap on textfield?
(P.S. BottomButton is already defined in another file, don't bother about it. I tried to do that - if you want, see at the bottom - but didn't get the job done.)
Here is the code:
//library international_phone_input;
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:international_phone_input/src/phone_service.dart';
import 'country.dart';
import 'package:byteseal/components/bottom_button.dart';
import 'package:byteseal/screens/otp_screen1.dart';
class InternationalPhoneInput extends StatefulWidget {
final void Function(String phoneNumber, String internationalizedPhoneNumber,
String isoCode) onPhoneNumberChange;
final String initialPhoneNumber;
final String initialSelection;
final String errorText;
final String hintText;
final String labelText;
final TextStyle errorStyle;
final TextStyle hintStyle;
final TextStyle labelStyle;
final int errorMaxLines;
final List<String> enabledCountries;
final InputDecoration decoration;
final bool showCountryCodes;
final bool showCountryFlags;
final Widget dropdownIcon;
final InputBorder border;
InternationalPhoneInput(
{this.onPhoneNumberChange,
this.initialPhoneNumber,
this.initialSelection,
this.errorText,
this.hintText,
this.labelText,
this.errorStyle,
this.hintStyle,
this.labelStyle,
this.enabledCountries = const [],
this.errorMaxLines,
this.decoration,
this.showCountryCodes = true,
this.showCountryFlags = true,
this.dropdownIcon,
this.border});
static Future<String> internationalizeNumber(String number, String iso) {
return PhoneService.getNormalizedPhoneNumber(number, iso);
}
#override
_InternationalPhoneInputState createState() =>
_InternationalPhoneInputState();
}
class _InternationalPhoneInputState extends State<InternationalPhoneInput> {
Country selectedItem;
List<Country> itemList = [];
String errorText;
String hintText;
String labelText;
TextStyle errorStyle;
TextStyle hintStyle;
TextStyle labelStyle;
int errorMaxLines;
bool hasError = false;
bool showCountryCodes;
bool showCountryFlags;
InputDecoration decoration;
Widget dropdownIcon;
InputBorder border;
_InternationalPhoneInputState();
final phoneTextController = TextEditingController();
#override
void initState() {
errorText = widget.errorText ?? 'Please enter a valid phone number';
hintText = widget.hintText ?? 'eg. 244056345';
labelText = widget.labelText;
errorStyle = widget.errorStyle;
hintStyle = widget.hintStyle;
labelStyle = widget.labelStyle;
errorMaxLines = widget.errorMaxLines;
decoration = widget.decoration;
showCountryCodes = widget.showCountryCodes;
showCountryFlags = widget.showCountryFlags;
dropdownIcon = widget.dropdownIcon;
phoneTextController.addListener(_validatePhoneNumber);
phoneTextController.text = widget.initialPhoneNumber;
_fetchCountryData().then((list) {
Country preSelectedItem;
if (widget.initialSelection != null) {
preSelectedItem = list.firstWhere(
(e) =>
(e.code.toUpperCase() ==
widget.initialSelection.toUpperCase()) ||
(e.dialCode == widget.initialSelection.toString()),
orElse: () => list[0]);
} else {
preSelectedItem = list[0];
}
setState(() {
itemList = list;
selectedItem = preSelectedItem;
});
});
super.initState();
}
_validatePhoneNumber() {
String phoneText = phoneTextController.text;
if (phoneText != null && phoneText.isNotEmpty) {
PhoneService.parsePhoneNumber(phoneText, selectedItem.code)
.then((isValid) {
setState(() {
hasError = !isValid;
});
if (widget.onPhoneNumberChange != null) {
if (isValid) {
PhoneService.getNormalizedPhoneNumber(phoneText, selectedItem.code)
.then((number) {
widget.onPhoneNumberChange(phoneText, number, selectedItem.code);
});
} else {
widget.onPhoneNumberChange('', '', selectedItem.code);
}
}
});
}
}
Future<List<Country>> _fetchCountryData() async {
var list = await DefaultAssetBundle.of(context)
.loadString('packages/international_phone_input/assets/countries.json');
List<dynamic> jsonList = json.decode(list);
List<Country> countries = List<Country>.generate(jsonList.length, (index) {
Map<String, String> elem = Map<String, String>.from(jsonList[index]);
if (widget.enabledCountries.isEmpty) {
return Country(
name: elem['en_short_name'],
code: elem['alpha_2_code'],
dialCode: elem['dial_code'],
flagUri: 'assets/flags/${elem['alpha_2_code'].toLowerCase()}.png');
} else if (widget.enabledCountries.contains(elem['alpha_2_code']) ||
widget.enabledCountries.contains(elem['dial_code'])) {
return Country(
name: elem['en_short_name'],
code: elem['alpha_2_code'],
dialCode: elem['dial_code'],
flagUri: 'assets/flags/${elem['alpha_2_code'].toLowerCase()}.png');
} else {
return null;
}
});
countries.removeWhere((value) => value == null);
return countries;
}
#override
Widget build(BuildContext context) {
return Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
DropdownButtonHideUnderline(
child: Padding(
padding: EdgeInsets.only(top: 8),
child: DropdownButton<Country>(
value: selectedItem,
icon: Padding(
padding:
EdgeInsets.only(bottom: (decoration != null) ? 6 : 0),
child: dropdownIcon ?? Icon(Icons.arrow_drop_down),
),
onChanged: (Country newValue) {
setState(() {
selectedItem = newValue;
});
_validatePhoneNumber();
},
items: itemList.map<DropdownMenuItem<Country>>((Country value) {
return DropdownMenuItem<Country>(
value: value,
child: Container(
padding: const EdgeInsets.only(bottom: 5.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
if (showCountryFlags) ...[
Image.asset(
value.flagUri,
width: 32.0,
package: 'international_phone_input',
)
],
if (showCountryCodes) ...[
SizedBox(width: 4),
Text(value.dialCode, style: TextStyle(color: Colors.white),)
]
],
),
),
);
}).toList(),
),
),
),
Flexible(
child: TextField(
keyboardType: TextInputType.phone,
style: TextStyle(
color: Colors.white,
),
controller: phoneTextController,
decoration: decoration ??
InputDecoration(
hintText: hintText,
labelText: labelText,
errorText: hasError ? errorText : null,
hintStyle: hintStyle ?? null,
errorStyle: errorStyle ?? null,
labelStyle: labelStyle,
errorMaxLines: errorMaxLines ?? 3,
border: border ?? null,
),
onTap: () {
setState(() {
BottomButton(
buttonTitle: 'Next',
onTap: (info) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => OTPScreen(number: phoneTextController.text, iso: info),
)
);
});
});
},
// onTap: BottomButton(
// buttonTitle: 'Next',
// onTap: () {
// Navigator.push(
// context,
// MaterialPageRoute(
// builder: (context) => OTPScreen(number: phoneTextController.value, iso: selectedItem),
// )
// );
// }),
))
],
),
);
}
}
The text field should be wrapped with GestureDetector(child: TextField, onTap: setState() {buttonShown = true}) and the button should be wrapped in Visibility() with parameter Visibility(visibility: buttonShown, child: BottomButton)
Use a stateful widget with a FocusNode, and a Visiblity Widget.
Create a boolean to hold visible status and a FocusNode object. Set the listener of the FocusNode to a method where you can change the state of your button visibility boolean.
bool buttonVisible;
FocusNode _focus = new FocusNode();
#override
void initState() {
super.initState();
buttonVisible = false;
_focus.addListener(_changeVisibility);
}
void _changeVisibility() {
setState(() {
buttonVisible = true;
});
}
Then use a Column with the TextField and a Visibility Widget that has the button as its child. Set the visible attribute of the visibility widget to be the buttonVisibility boolean you defined earlier.
Column(
children: <Widget>[
TextField(
focusNode: _focus,
),
Visibility(
visible: buttonVisible,
child: RaisedButton(
onPressed: () { },
child: Text("My Button")
),
),
]
)
After validating a form and sending a request from flutter to the server backend: I want to set any potential error message from the server to be displayed in the original form. Preferably exactly like a validation error.
For instance:
Widget build(BuildContext context) {
...
TextFormField(
onFieldSubmitted: (value) => _signIn(),
validator: (input) {
if (input.length < 6)
return 'Your password is too short';
return null;
},
onSaved: (input) => _password = input,
decoration: InputDecoration(
labelText: 'Password',
),
obscureText: true,
)
...
}
Future<void> _signIn() async {
final formState = _formKey.currentState;
if (!formState.validate()) return;
formState.save();
try {
... // do fancy request stuff
} catch (e) {
// this is where I want to set the "validation" error
}
}
It's actually super simple and the validation error still works aswell.
String? _errorMsg;
Widget build(BuildContext context) {
...
TextFormField(
onFieldSubmitted: (value) => _signIn(),
validator: (input) {
if (input.length < 6)
// will set the errorText directly, no need for a variable here
return 'Your password is too short';
return null;
},
onSaved: (input) => _password = input,
decoration: InputDecoration(
labelText: 'Password',
errorText: _errorMsg,
),
obscureText: true,
)
...
}
Future<void> _signIn() async {
setState(() {
_errorMsg = null; // clear any existing errors
});
final formState = _formKey.currentState;
if (!formState.validate()) return;
formState.save();
try {
... // do fancy request stuff
} catch (e) {
setState(() {
_errorMsg = 'Wrong password.';
});
}
}
I suppose, I could think of a solution, but I think it's kind of ugly.
I could have an "error" variable, that is set when the request fails.
I would then call formState.validate() a second time, in there: check the error variable and return it if it's not null.
You can use flutter_form_bloc and use addError method of TextFieldBloc.
usernameField.addError('That username is taken. Try another.');
Keep in mind that you can also use asynchronous validators.
This is a complete example:
dependencies:
flutter:
sdk: flutter
flutter_bloc: ^0.21.0
form_bloc: ^0.5.0
flutter_form_bloc: ^0.4.1+1
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_bloc/flutter_form_bloc.dart';
import 'package:form_bloc/form_bloc.dart';
void main() {
runApp(MaterialApp(home: SignUpForm()));
}
class SignUpFormBloc extends FormBloc<String, String> {
final usernameField = TextFieldBloc();
final passwordField =
TextFieldBloc(validators: [Validators.passwordMin6Chars]);
#override
List<FieldBloc> get fieldBlocs => [usernameField, passwordField];
#override
Stream<FormBlocState<String, String>> onSubmitting() async* {
// Form logic...
try {
await _signUp(
throwException: true,
username: usernameField.value,
password: passwordField.value,
);
yield currentState.toSuccess();
} catch (e) {
// When get the error from the backend you can
// add the error to the field:
usernameField.addError('That username is taken. Try another.');
yield currentState
.toFailure('The error was added to the username field.');
}
}
Future<void> _signUp({
#required bool throwException,
#required String username,
#required String password,
}) async {
print(username);
print(password);
await Future<void>.delayed(Duration(seconds: 2));
if (throwException) throw Exception();
}
}
class SignUpForm extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocProvider<SignUpFormBloc>(
builder: (context) => SignUpFormBloc(),
child: Builder(
builder: (context) {
final formBloc = BlocProvider.of<SignUpFormBloc>(context);
return Scaffold(
appBar: AppBar(title: Text('Sign Up Form')),
body: FormBlocListener<SignUpFormBloc, String, String>(
onSubmitting: (context, state) {
// Show the progress dialog
showDialog(
context: context,
barrierDismissible: false,
builder: (_) => WillPopScope(
onWillPop: () async => false,
child: Center(
child: Card(
child: Container(
width: 80,
height: 80,
padding: EdgeInsets.all(12.0),
child: CircularProgressIndicator(),
),
),
),
),
);
},
onSuccess: (context, state) {
// Hide the progress dialog
Navigator.of(context).pop();
// Navigate to success screen
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => SuccessScreen()));
},
onFailure: (context, state) {
// Hide the progress dialog
Navigator.of(context).pop();
// Show snackbar with the error
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text(state.failureResponse),
backgroundColor: Colors.red[300],
),
);
},
child: ListView(
children: <Widget>[
TextFieldBlocBuilder(
textFieldBloc: formBloc.usernameField,
decoration: InputDecoration(labelText: 'Username'),
),
TextFieldBlocBuilder(
textFieldBloc: formBloc.passwordField,
decoration: InputDecoration(labelText: 'Password'),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: RaisedButton(
onPressed: formBloc.submit,
child: Center(child: Text('SUBMIT')),
),
),
],
),
),
);
},
),
);
}
}
class SuccessScreen extends StatelessWidget {
const SuccessScreen({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.green[300],
body: Center(
child: SingleChildScrollView(
child: Column(
children: <Widget>[
Icon(
Icons.sentiment_satisfied,
size: 100,
),
RaisedButton(
color: Colors.green[100],
child: Text('Sign out'),
onPressed: () => Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => SignUpForm())),
)
],
),
),
),
);
}
}
A simple solution:
Make a key for the widgets state:
GlobalKey<_CustomErrorTextField> _passwordTextFieldState = GlobalKey();
Set the Error message using the key:
_passwordTextFieldState.currentState.updateError(errorMsg);
Reset the error after 2 seconds:
Future.delayed(Duration(seconds: 2), () {
// Runs after duration sec
_passwordTextFieldState.currentState.updateError(null);
});
Set up the widget (be sure to set the key):
CustomErrorTextField(
key: _passwordTextFieldState,
label: "Password",
currentPassword: password,
validator: yourValidator,
callback: passwordCallback,
obscureText: hidePassword.value - a bool value show/hide password
)
Here is the Widget:
class CustomErrorTextField extends StatefulWidget {
CustomErrorTextField({
Key key,
this.label,
this.currentPassword,
this.validator,
this.callback,
this.obscureText = false
}): super(key: key);
final String label;
final String currentPassword;
final FormFieldValidator<String> validator;
final Function callback;
final obscureText;
#override
_CustomErrorTextField createState() => _CustomErrorTextField();
}
class _CustomErrorTextField extends State<CustomErrorTextField> {
String errorMsg;
updateError(String errorMsg){
setState(() {
this.errorMsg = errorMsg;
});
}
#override
Widget build(BuildContext context) {
return TextFormField(
decoration: InputDecoration(
labelText: widget.label,
errorText: errorMsg
),
initialValue: widget.currentPassword,
keyboardType: TextInputType.visiblePassword,
validator: widget.validator,
onSaved: (String val) {
widget.callback(val);
},
obscureText: widget.obscureText,
);
}
}