I'm trying to create a Textbutton widget with a disabled property like this:
class AppTextButton extends StatelessWidget {
final String title;
final void Function(BuildContext context) onPress;
final EdgeInsetsGeometry margin;
final EdgeInsetsGeometry padding;
final double borderRadius;
final Color backgroundColor;
final Image? leadingIcon;
final Image? trailingIcon;
final TextStyle? textStyle;
final bool disabled;
AppTextButton(this.title, this.onPress,
{this.margin = const EdgeInsets.all(0),
this.padding = const EdgeInsets.all(12),
this.borderRadius = 0,
this.leadingIcon,
this.trailingIcon,
this.textStyle,
this.disabled = false,
this.backgroundColor = const Color(0xFFFFFFFF)});
#override
Widget build(BuildContext context) {
return Container(
padding: margin,
child: TextButton(
style: ButtonStyle(
shape: MaterialStateProperty.all<RoundedRectangleBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(borderRadius))),
backgroundColor: MaterialStateProperty.all(backgroundColor)),
child: Row(
children: [
if (this.leadingIcon != null) ...[this.leadingIcon!],
Expanded(
child: Padding(
padding: padding,
child:
Text(title, textAlign: TextAlign.center, style: textStyle),
),
),
if (this.trailingIcon != null) ...[this.trailingIcon!]
],
),
onPressed: () => !disabled ? onPress(context) : null,
),
);
}
}
And in my screen, I declare my formKey and my form as following:
class LoginScreen extends AppBaseScreen {
LoginScreen({Key? key}) : super(key: key);
final _formKey = GlobalKey<FormState>();
#override
Widget build(BuildContext context) {
Form(
key: _formKey,
child: Obx(
() => AppTextInput(
"Please input passcode",
_passwordController,
borderRadius: 8,
fillColor: Color(0xFFF6F4F5),
keyboardType: TextInputType.number,
errorMessage: _c.errorLoginConfirm.value,
isObscure: true,
onChange: _onInputChange,
maxLength: 6,
margin: EdgeInsets.only(top: 12, left: 20, right: 20),
validator: (text) {
if (text != null && text.length > 0) {
if (text.length < 6) {
return "Passcode must have at least 6 digits";
}
}
},
),
)),
And I will have a button at the bottom of the screen, which I pass the !_formKey.currentState!.validate() in the disabled field
AppTextButton("Login", _onLogin,
margin: EdgeInsets.fromLTRB(24, 24, 24, 8),
backgroundColor: Color(0xFFFF353C),
disabled: !_formKey.currentState!.validate(),
textStyle: TextStyle(color: Colors.white),
borderRadius: 8),
However, the formKey.currentState is null and throw the following error everytime the screen is opened.
Null check operator used on a null value
What I am doing wrong here? Thank you in advance!
You need to save the form state before passing,
final FormState formState = _formKey.currentState;
formState.save();
onPressed: () {
FocusScope.of(context).requestFocus(FocusNode());
final FormState formState = _formKey.currentState;
if (formState.validate()) {
formState.save();
onPress(context);
}
},
I think the problem is caused because all the widgets are created at the same time, so the _formKey.currentState is still null when the AppTextButton calls it.
You need to create a separate controller to control the state of the button and add it to the validator like this:
validator: (text) {
if (text != null && text.length > 0) {
if (text.length < 6) {
buttonDisableController = true;
return "Passcode must have at least 6 digits";
}
}
buttonDisableController = false;
return null;
},
In your case, you should know how the widgets building process (Assume you have Botton widget and Input widget):
Botton and Input are building initial state. both states are not yet ready to be read and used
Botton and Input are built. States are ready to read.
User interact to Input. Input must call Button to rebuild its state if the value passes the validator
Botton rebuild.
For the process, you should change your code like:
Get and modify the state of Button inside Input
Notify Button to rebuild
There are many ways to handle the state management between widgets. I simply change the AppTextButton into Statefultwidget to achieve it.
...
final _buttonKey = GlobalKey<_AppTextButtonState>();
...
AppTextButton(key: _buttonKey)
...
class AppTextButton extends StatefulWidget {
final bool initDisable;
AppTextButton({
this.initDisable = false,
Key? key,
}) : super(key: key);
#override
_AppTextButtonState createState() => _AppTextButtonState();
}
class _AppTextButtonState extends State<AppTextButton> {
var disable;
#override
void initState() {
disable = widget.initDisable;
super.initState();
}
#override
Widget build(BuildContext context) {
return TextButton(child: Text('Button'), onPressed: disable ? null : () {});
}
void enableButton() {
setState(() {
disable = false;
});
}
void disableButton() {
setState(() {
disable = true;
});
}
}
class LoginScreen extends StatelessWidget {
LoginScreen({Key? key}) : super(key: key);
final _formKey = GlobalKey<FormState>();
#override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: TextFormField(
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: (text) {
if (text != null && text.length > 0) {
if (text.length < 6) {
return "Passcode must have at least 6 digits";
}
}
},
onChanged: (v) {
if (_formKey.currentState?.validate() ?? false) {
_buttonKey.currentState?.enableButton();
} else {
_buttonKey.currentState?.disableButton();
}
},
),
);
}
}
Related
I have a list of dynamic forms where I need to add and remove form fields between two fields dynamically. I am able to add/remove form fields from the bottom of the list properly.
However, when I try to add a form field in between two form fields the data for the field does not update correctly.
How can I correctly add a field in between the two fields and populate the data correctly?
import 'package:flutter/material.dart';
class DynamicFormWidget extends StatefulWidget {
const DynamicFormWidget({Key? key}) : super(key: key);
#override
State<DynamicFormWidget> createState() => _DynamicFormWidgetState();
}
class _DynamicFormWidgetState extends State<DynamicFormWidget> {
List<String?> names = [null];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Dynamic Forms'),
),
body: ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
itemBuilder: (builderContext, index) => Row(
children: [
Flexible(
child: TextFormField(
initialValue: names[index],
onChanged: (name) {
names[index] = name;
debugPrint(names.toString());
},
decoration: InputDecoration(
hintText: 'Enter your name',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8))),
),
),
Padding(
padding: const EdgeInsets.all(8),
child: IconButton(
onPressed: () {
setState(() {
if(index + 1 == names.length){
names.add( null); debugPrint('Added: $names');
} else {
names.insert(index + 1, null); debugPrint('Added [${index+1}]: $names');
}
});
},
color: Colors.green,
iconSize: 32,
icon: const Icon(Icons.add_circle)),
),
Padding(
padding: const EdgeInsets.all(8),
child: IconButton(
onPressed: (index == 0&& names.length == 1)
? null
: () {
setState(() {
names.removeAt(index);
});
debugPrint('Removed [$index]: $names');
},
color: Colors.red,
iconSize: 32,
icon: const Icon(Icons.remove_circle)),
),
],
),
separatorBuilder: (separatorContext, index) => const SizedBox(
height: 16,
),
itemCount: names.length,
),
);
}
}
Basically the problem is that Flutter is confused about who is who in your TextFormField list.
To fix this issue simply add a key to your TextFormField, so that it can be uniquely identified by Flutter:
...
child: TextFormField(
initialValue: names[index],
key: UniqueKey(), // add this line
onChanged: (name) {
...
If you want to learn more about keys and its correct use take a look at this.
The widget AnimatedList solves this problem, it keep track of the widgets as a list would do and uses a build function so it is really easy to sync elements with another list. If you end up having a wide range of forms you can make use of the InheritedWidget to simplify the code.
In this sample i'm making use of the TextEditingController to abstract from the form code part and to initialize with value (the widget inherits from the ChangeNotifier so changing the value will update the text in the form widget), for simplicity it only adds (with the generic text) and removes at an index.
To make every CustomLineForm react the others (as in: disable remove if it only remains one) use a StreamBuilder or a ListModel to notify changes and make each entry evaluate if needs to update instead of rebuilding everything.
class App extends StatelessWidget {
final print_all = ChangeNotifier();
App({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: FormList(print_notifier: print_all),
floatingActionButton: IconButton(
onPressed: print_all.notifyListeners,
icon: Icon(Icons.checklist),
),
),
);
}
}
class FormList extends StatefulWidget {
final ChangeNotifier print_notifier;
FormList({required this.print_notifier, super.key});
#override
_FormList createState() => _FormList();
}
class _FormList extends State<FormList> {
final _controllers = <TextEditingController>[];
final _list_key = GlobalKey<AnimatedListState>();
void print_all() {
for (var controller in _controllers) print(controller.text);
}
#override
void initState() {
super.initState();
widget.print_notifier.addListener(print_all);
_controllers.add(TextEditingController(text: 'Inital entrie'));
}
#override
void dispose() {
widget.print_notifier.removeListener(print_all);
for (var controller in _controllers) controller.dispose();
super.dispose();
}
void _insert(int index) {
final int at = index.clamp(0, _controllers.length - 1);
_controllers.insert(at, TextEditingController(text: 'Insert at $at'));
// AnimatedList will take what is placed in [at] so the controller
// needs to exist before adding the widget
_list_key.currentState!.insertItem(at);
}
void _remove(int index) {
final int at = index.clamp(0, _controllers.length - 1);
// The widget is replacing the original, it is used to animate the
// disposal of the widget, ex: size.y -= delta * amount
_list_key.currentState!.removeItem(at, (_, __) => Container());
_controllers[at].dispose();
_controllers.removeAt(at);
}
#override
Widget build(BuildContext context) {
return AnimatedList(
key: _list_key,
initialItemCount: _controllers.length,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
itemBuilder: (ctx, index, _) {
return CustomLineForm(
index: index,
controler: _controllers[index],
on_insert: _insert,
on_remove: _remove,
);
},
);
}
}
class CustomLineForm extends StatelessWidget {
final int index;
final void Function(int) on_insert;
final void Function(int) on_remove;
final TextEditingController controler;
const CustomLineForm({
super.key,
required this.index,
required this.controler,
required this.on_insert,
required this.on_remove,
});
#override
Widget build(BuildContext context) {
return Row(
children: [
Flexible(
child: TextFormField(
controller: controler,
),
),
IconButton(
icon: Icon(Icons.add_circle),
onPressed: () => on_insert(index),
),
IconButton(
icon: Icon(Icons.remove_circle),
onPressed: () => on_remove(index),
)
],
);
}
}
I'm at the beginning of learning Getx and I'm having the following problem: I have a ListView.builder with the CodeLine() widget, which is a widget where you can select it with an onLongPress. The idea here is to select a row but when I give onLongPress, all the rows are selected together.
class CodeEditor extends GetView<CodeEditorController> {
const CodeEditor({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
ScrollController scrollCode = ScrollController();
return Padding(
padding: const EdgeInsets.fromLTRB(0.0, 0.0, 10.0, 120.0),
child: ListView.builder(
controller: scrollCode,
itemCount: 10,
itemBuilder: (BuildContext context, int index){
return CodeLine();
}
)
);
}
}
In CodeLine I have isSelected variable to show on screen when it is selected or not and I change its value in CodeLineController controller
class CodeLine extends GetView<CodeLineController> {
CodeLine({Key? key}) : super(key: key);
#override
final controller = Get.put(CodeLineController());
#override
Widget build(BuildContext context) {
return GestureDetector(
child: GetX<CodeLineController>(
builder: (_){
return !_.isHidden ? Container(
color: _.isSelected ? const Color(0x327a7a7a) : Colors.transparent,
height: 35.0,
child: Row(
children: <Widget>[
SizedBox(
width: 25.0,
child: Text(
"1",
textAlign: TextAlign.end,
style: TextStyle(
color: codeLineTheme.hintColor,
fontSize: 15.0,
fontWeight: FontWeight.bold
)
)
),
const SizedBox(width: 7.5),
const Expanded(
child: SizedBox()
)
]
)
) : const SizedBox();
}
),
onLongPress: (){
controller.isSelected = !controller.isSelected;
}
);
}
}
In CodeLineController I have the isSelected variable as observable but when I change its boolean value, this value of all CodeLine instances is also changed, how can I change only the isSelected variable of a specific CodeLine?
class CodeLineController extends GetxController{
CodeLineController();
final _isHidden = false.obs;
get isHidden => _isHidden.value;
set isHidden(value) => _isHidden.value = value;
final _isSelected = false.obs;
get isSelected => _isSelected.value;
set isSelected(value) => _isSelected.value = value;
}
When you call a class that you extend with GetxController with Get.put, only one of that object is found in the project. And you always define the same object in the CodeLine widget. So a change affects all of its widgets. You can solve your problem like this:
Add Map inside CodeLineController. Save all CodeLine widgets you reproduced with ListView to the map with a special key.
class CodeLineController extends GetxController {
final RxMap<Key, bool> _map = <Key, bool>{}.obs;
void setKey(Key key, bool value) {
_map[key] = value;
}
void changeValue(Key key) {
if (_map[key] != null) {
_map[key] = !_map[key]!;
}
}
bool getValue(Key key) {
if (_map[key] != null) {
return _map[key]!;
} else {
return false;
}
}
...
}
It can be an int "index" value instead of the "key" in the Map or a unique data you want.
You can pass "key" as a parameter to the CodeLine class.
class CodeLine extends GetView<CodeLineController> {
final Key myKey;
CodeLine({
Key? key,
required this.myKey,
}) : super(key: key);
...
}
When creating the CodeLine class, pass the key as a parameter inside the ListView.
...
Key myKey;
return Padding(
...
itemBuilder: (BuildContext context, int index) {
myKey = GlobalKey();
controller.setKey(myKey, false);
return CodeLine(myKey: myKey);
},
...
onLongPress
...
onLongPress: () {
controller.changeValue(myKey);
},
...
This is how you can check
...
Container(
color: controller.getValue(myKey)
? const Color(0x327a7a7a)
: Colors.transparent,
...
What I want to do is disable the elevated button until the text form field is valid. And then once the data is valid the elevated button should be enabled. I have reviewed several SO threads and a few articles on Google about how to disable a button until the text form field is validated. They all focused on whether or not the text form field was empty or not which is not what I am asking about here. I'm using regex to determine if the user has entered a valid email address. Only when the data entered is a valid email is the data considered valid. That is when I want the button to become enabled. If I try to call setState with a boolean in the validateEmail method I get the error:
setState() or markNeedsBuild() called during build.
Any help will be appreciated. Thank you.
class ResetPasswordForm extends StatefulWidget {
const ResetPasswordForm({Key? key}) : super(key: key);
#override
_ResetPasswordFormState createState() => _ResetPasswordFormState();
}
class _ResetPasswordFormState extends State<ResetPasswordForm> {
final _formKey = GlobalKey<FormState>();
final TextEditingController _emailController = TextEditingController();
String? validateEmail(String? value) {
String pattern = ValidatorRegex.emailAddress;
RegExp regex = RegExp(pattern);
if (value == null || value.isEmpty || !regex.hasMatch(value)) {
return ValidatorString.enterValidEmail;
} else {
return null;
}
}
#override
void dispose() {
_emailController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Column(
children: [
Form(
key: _formKey,
child: TextFormField(
controller: _emailController,
validator: (value) => validateEmail(value),
),
),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
Auth().resetPassword(
context,
_emailController.text.trim(),
);
}
},
child: const Text('Reset Password'),
),
],
);
}
}
you can do somthing like that:
class ResetPasswordForm extends StatefulWidget {
const ResetPasswordForm({Key? key}) : super(key: key);
#override
_ResetPasswordFormState createState() => _ResetPasswordFormState();
}
class _ResetPasswordFormState extends State<ResetPasswordForm> {
final _formKey = GlobalKey<FormState>();
final TextEditingController _emailController = TextEditingController();
final bool _isValidated = false;
String? validateEmail(String? value) {
String pattern = ValidatorRegex.emailAddress;
RegExp regex = RegExp(pattern);
if (value == null || value.isEmpty || !regex.hasMatch(value)) {
return ValidatorString.enterValidEmail;
} else {
setState(){
_isValidated = true;
}
return null;
}
}
#override
void dispose() {
_emailController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Column(
children: [
Form(
key: _formKey,
child: TextFormField(
controller: _emailController,
validator: (value) => validateEmail(value),
),
),
ElevatedButton(
onPressed:_isValidated
? () {
//do stuff
}
: null,,
child: const Text('Reset Password'),
),
],
);
}
}
if onPressed be null, the button is disabled.
Enabling and disabling functionality is the same for most widgets
set onPressed property as shown below
onPressed : null returns a disabled widget while
onPressed: (){} or onPressed: _functionName returns enabled widget
in this case it'll be this way:
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
Auth().resetPassword(
context,
_emailController.text.trim(),
);
} else {
print('disabled');
}
},
child: const Text('Reset Password'),
),
First, move the logic into a named function
void _sendData (){
if (_formKey.currentState!.validate()) {
Auth().resetPassword( context,
_emailController.text.trim(), );
}
Now in onpressed
onpressed: _emailController.text.trim.isNotEmpty?_sendData : null;
Best for this is to just create a form key for this
final formGlobalKey = GlobalKey <FormState> ();
Assign it to form like:
Form(
key: formGlobalKey,
And now you just have to check the validation for this like:
ElevatedButton(
style: style,
onPressed: formGlobalKey.currentState==null?null: formGlobalKey.currentState!.validate()? () {
This is body of button} : null,
child: const Text('Enabled'),
),
**If you didn't use first condition (formGlobalKey.currentState==null?) it will take you towards null exception **
I have a list of objects that I can display in a ListView. Now I wanted to implement a search feature and only display the search result. When I try to do it using onChanged on TextField(or even Controller) it doesn't work. I tried to debug and he gets the list updated correctly but he doesn't update the Widget. But when I removed the onChanged and added a button and then called the same method that I was calling on onChanged everything worked.
The goal is to update the widget as the user writes in the text field.
I would be happy to get some help
My full code :
import 'package:flutter/material.dart';
import 'package:hello_fridge/single_ingredient_icon.dart';
import 'package:string_similarity/string_similarity.dart';
import 'entities/ingredient.dart';
class IngredientsContainer extends StatefulWidget {
const IngredientsContainer({Key? key}) : super(key: key);
#override
_IngredientsContainerState createState() => _IngredientsContainerState();
}
class _IngredientsContainerState extends State<IngredientsContainer> {
late List<Ingredient> ingredients;
final searchController = TextEditingController();
#override
void dispose() {
// Clean up the controller when the widget is disposed.
searchController.dispose();
super.dispose();
}
void updateResults(String newValue) {
if (newValue.isEmpty) {
ingredients = Ingredient.getDummyIngredients();
} else {
print("new Value = $newValue");
ingredients = this.ingredients.where((ing) {
double similarity =
StringSimilarity.compareTwoStrings(ing.name, newValue);
print("$similarity for ${ing.name}");
return similarity > 0.2;
}).toList();
ingredients.forEach((element) {
print("found ${element.name}");
});
}
setState(() {});
}
Widget _searchBar(List<Ingredient> ingredients) {
return Row(
children: <Widget>[
IconButton(
splashColor: Colors.grey,
icon: Icon(Icons.restaurant),
onPressed: null,
),
Expanded(
child: TextField(
controller: searchController,
onChanged: (newValue) {
updateResults(newValue);
},
cursorColor: Colors.black,
keyboardType: TextInputType.text,
textInputAction: TextInputAction.go,
decoration: InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 15),
hintText: "Search..."),
),
),
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: IconButton(
icon: Icon(
Icons.search,
color: Color(0xff9ccc65),
),
onPressed: () {
updateResults(searchController.text);
},
),
),
],
);
}
#override
void initState() {
this.ingredients = Ingredient.getDummyIngredients();
super.initState();
}
#override
Widget build(BuildContext context) {
return Material(
child: Column(children: [
Expanded(flex: 1, child: _searchBar(this.ingredients)),
Expanded(flex: 4, child: IngredientsGrid(this.ingredients))
]),
);
}
}
class IngredientsGrid extends StatelessWidget {
List<Ingredient> ingredients;
IngredientsGrid(this.ingredients);
List<Widget> _buildIngredients() {
return this.ingredients.map((ing) => SingleIngredientIcon(ing)).toList();
}
// const IngredientsGrid({
// Key? key,
// }) : super(key: key);
#override
Widget build(BuildContext context) {
this.ingredients.forEach((ing) => print(ing.name! + ","));
return ListView(
children: <Widget>[
GridView.count(
crossAxisCount: 4,
// physics: NeverScrollableScrollPhysics(),
// to disable GridView's scrolling
shrinkWrap: true,
// You won't see infinite size error
children: _buildIngredients()),
// ...... other list children.
],
);
}
}
Moreover, I keep getting this Warning :
"Changing the content within the composing region may cause the input method to behave strangely, and is therefore discouraged. See https://github.com/flutter/flutter/issues/78827 for more details".
Visiting the linked GitHub page wasn't helpful
The problem is that while you are correctly filtering the list but your TextController is not getting assigned any value.
So, no value is getting assigned to your TextField as the initial value and hence the list again filters to have the entire list.
To solve this just assign the TextController the newValue like this.
void updateResults(String newValue) {
if (newValue.isEmpty) {
ingredients = Ingredient.getDummyIngredients();
} else {
print("new Value = $newValue");
ingredients = this.ingredients.where((ing) {
double similarity =
StringSimilarity.compareTwoStrings(ing.name, newValue);
print("$similarity for ${ing.name}");
return similarity > 0.2;
}).toList();
ingredients.forEach((element) {
print("found ${element.name}");
});
}
// change
searchController = TextEditingController.fromValue(
TextEditingValue(
text: newValue,
),
);
setState(() {});
}
If it throws an error then remove final from the variable declaration, like this :
var searchController = TextEditingController();
I need to fully customize the rendering of input fields and especially the way error messages are rendered. I need them to be displayed below the field without any other modification on the field.
Right now either the field is resized (default) or fixed when I set helperText: ' ' as stated in the doc, but then the field height becomes huge to keep space for the error message.
Of course I can add a Text widget below my field for the error (that's what I did actually) but then I can't use form validation as it relies on the built-in error management of the input widget to display error messages... So this would require to re-create my own form validation.
One thing I've been thinking of would be to prevent the form validation to display the error message but I couldn't find a way to do it.
Any idea?
Edit : some code bellow :
class CustomTextFormField extends StatefulWidget {
final FormFieldSetter<String> onSaved;
final ValueChanged<String> onFieldSubmitted;
final TextInputAction textInputAction;
final String initialValue;
final bool autofocus;
final bool obscureText;
final String label;
final bool withDivider;
final FocusNode focusNode;
final bool required;
CustomTextFormField(
{Key key,
this.onSaved,
this.onFieldSubmitted,
this.textInputAction,
this.initialValue,
this.autofocus = false,
this.obscureText = false,
this.label,
this.withDivider = false,
this.focusNode,
this.required})
: super(key: key);
CustomTextFormFieldState createState() => CustomTextFormFieldState();
}
class CustomTextFormFieldState extends State<CustomTextFormField> with CustomFormFieldState {
bool _isMasked = false;
String _showObscureIcon;
FocusNode _focusNode;
TextEditingController _controller;
String _errorText;
/// True if this field has any validation errors.
bool get hasError => _errorText != null;
#override
void initState() {
// Add a listener to the focusNode to detect when we loose focus
_focusNode = widget.focusNode != null ? widget.focusNode : FocusNode();
_focusNode.addListener(lostFocusListener);
// Create the TextEditingController for the field with optional initial value
_controller = TextEditingController(text: widget.initialValue);
_controller.addListener(() {
CustomForm.of(context).fieldDidChange(); // CustomForm is basically a simplified and slightly adapted version of the built-in Form
});
// Set obscure properties
if (widget.obscureText) {
_isMasked = true;
_showObscureIcon = 'assets/img/icon_displaypassword.png';
}
super.initState();
}
/// Displays the field label
Widget _inputLabel() {
return Container(
child: Text(widget.label,
style: hasError ? TextStyle(color: themeErrorColor) : null),
margin: const EdgeInsets.only(bottom: 8.0),
);
}
/// Displays the field
Widget _inputField() {
return Expanded(
child: TextFormField(
decoration: InputDecoration(
enabledBorder: themeInputBorder,
focusedBorder: themeInputBorder,
contentPadding: EdgeInsets.all(14.0),
),
textInputAction: widget.textInputAction,
autofocus: widget.autofocus,
onSaved: widget.onSaved,
onFieldSubmitted: widget.onFieldSubmitted,
obscureText: _isMasked,
focusNode: _focusNode,
controller: _controller,
));
}
/// Displays the obscured toggle button
Widget _obscureButton() {
return IconButton(
icon: Image.asset(_showObscureIcon),
onPressed: _toggleObscureText,
);
}
/// Displays the decorated field
Widget _decoratedField() {
// Row that contains the input field
List<Widget> rowChildren = <Widget>[_inputField()];
// If the field must be oscured, we add the toggle button to the row
if (widget.obscureText) {
rowChildren.add(_obscureButton());
}
return Container(
decoration: hasError ? themeInputErrorDecoration : themeInputDecoration, // themeInputErrorDecoration and themeInputDecoration are LinearGradient defined elsewhere
padding: const EdgeInsets.all(1.0),
child: Container(
decoration: BoxDecoration(
color: Colors.white, borderRadius: BorderRadius.circular(2.0)),
child: Row(
children: rowChildren,
),
));
}
/// Displays the error message if any
Widget _errorLabel() {
return Offstage(
offstage: !hasError,
child: Container(
margin: EdgeInsets.only(top: 10.0),
child: Text(_errorText == null ? '' : _errorText,
style: TextStyle(color: themeErrorColor))));
}
/// Toggles beetween obscured/clear text and action icon
void _toggleObscureText() {
setState(() {
_isMasked = !_isMasked;
_showObscureIcon = _isMasked
? 'assets/img/icon_displaypassword.png'
: 'assets/img/icon_dontdisplaypassword.png';
});
}
/// Callback called when the focus has changed. Validates the input text
void lostFocusListener() {
if (!_focusNode.hasFocus) {
if (!touched) {
touched = true;
}
}
}
/// Saves the field
void save() {
widget.onSaved(_controller.text);
}
/// Resets the field to its initial value.
void reset() {
setState(() {
_controller.text = widget.initialValue;
_errorText = null;
});
}
/// Validates the field and set the [_errorText]. Returns true if there
/// were no errors.
bool validate() {
// If the field has not been touched yet, the field is validated
if(!touched) {
return true;
}
setState(() {
_validate();
});
return !hasError;
}
void _validate() {
// TODO: write a real validator
if (_controller.text.isEmpty && widget.required) {
_errorText = 'Field is required';
} else {
_errorText = null;
}
}
#override
Widget build(BuildContext context) {
register(context);
// Widgets for the column
List<Widget> children = <Widget>[
_inputLabel(),
_decoratedField(),
_errorLabel()
];
// Adds a divider if neeed to the column
if (widget.withDivider) {
children.add(Divider(height: 40.0));
}
// Renders the column
return Column(
crossAxisAlignment: CrossAxisAlignment.start, children: children);
}
#override
void dispose() {
// Clean up the controller when the widget is disposed.
_controller.dispose();
super.dispose();
}
#override
void deactivate() {
unregister(context);
super.deactivate();
}
}
abstract class CustomFormFieldState {
String errorText;
/// True if this field has any validation errors.
bool get hasError => errorText != null;
bool touched = false;
void register(BuildContext context) {
CustomForm.of(context)?.register(this);
}
void unregister(BuildContext context) {
CustomForm.of(context)?.unregister(this);
}
void save();
void reset();
bool validate();
}