I am trying to create this input field for the user to register its pin, however I don't want to call another context screen. So, the point is simple, the user types the pin, the field clears, the text in the screen changes and the user types again , confirming the pin.
I ran into two problems tho. The field never clears, no matter what I do and when I click the field to type again the state resets back to the first pin input. here is the code for the widgets.
//Form fields
StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Column(
children: [
Container(
width: 330,
child: pinCodeType == 1
? Text("Choose a Pincode",
style: TextStyle(color: brandBlue, fontSize: 18))
: Text("Repeat your Pincode",
style: TextStyle(color: brandBlue, fontSize: 18)),
),
Container(
height: 90,
width: 330,
child: pinCodeType == 1
? new PinCodeTextField(
enablePinAutofill: false,
keyboardAppearance: Brightness.dark,
length: 5,
obscureText: true,
textStyle: TextStyle(color: brandBlue),
animationType: AnimationType.scale,
keyboardType: TextInputType.numberWithOptions(),
autoDisposeControllers: true,
pinTheme: PinTheme(
shape: PinCodeFieldShape.underline,
//borderRadius: BorderRadius.circular(5),
fieldHeight: 80,
fieldWidth: 60,
activeColor: brandBlue,
inactiveFillColor: Colors.transparent,
inactiveColor: brandDarkGrey,
activeFillColor: brandWhite,
selectedColor: brandLightBlue,
selectedFillColor: Colors.transparent),
animationDuration: Duration(milliseconds: 300),
backgroundColor: brandWhite,
enableActiveFill: true,
errorAnimationController: errorController,
controller: textEditingController,
onCompleted: (v) {
if (pinCodeType == 1) {
pincode1 = v;
print("PINCODE 1 $pincode1");
print("PINCODE TYPE $pinCodeType");
setState(() {
textEditingController.clear();
pinCodeType = 2;
v = '';
});
}
},
onChanged: (value) {
print(value);
setState(() {});
},
beforeTextPaste: (text) {
print("Allowing to paste $text");
//if you return true then it will show the paste confirmation dialog. Otherwise if false, then nothing will happen.
//but you can show anything you want here, like your pop up saying wrong paste format or etc
return true;
},
appContext: context,
)
: new PinCodeTextField(
enablePinAutofill: false,
keyboardAppearance: Brightness.dark,
length: 5,
obscureText: true,
textStyle: TextStyle(color: brandBlue),
animationType: AnimationType.scale,
keyboardType: TextInputType.numberWithOptions(),
pinTheme: PinTheme(
shape: PinCodeFieldShape.underline,
//borderRadius: BorderRadius.circular(5),
fieldHeight: 80,
fieldWidth: 60,
activeColor: brandBlue,
inactiveFillColor: Colors.transparent,
inactiveColor: brandDarkGrey,
activeFillColor: brandWhite,
selectedColor: brandLightBlue,
selectedFillColor: Colors.transparent),
animationDuration: Duration(milliseconds: 300),
backgroundColor: brandWhite,
autoDisposeControllers: true,
enableActiveFill: true,
errorAnimationController: errorController2,
controller: textEditingController2,
onCompleted: (v) {
if (pinCodeType == 2) {
pincode1 = v;
print("PINCODE 2 $pincode2");
print("PINCODE TYPE $pinCodeType");
setState(() {
v = '';
pinCodeType++;
});
}
},
onChanged: (value) {
print(value);
setState(() {});
},
beforeTextPaste: (text) {
print("Allowing to paste $text");
//if you return true then it will show the paste confirmation dialog. Otherwise if false, then nothing will happen.
//but you can show anything you want here, like your pop up saying wrong paste format or etc
return true;
},
appContext: context,
)),
Container(
width: MediaQuery.of(context).size.width * 0.9,
child: ListTile(
title: Text(
"Use [biometric]",
style: TextStyle(color: brandBlue, fontSize: 18),
),
trailing: FlutterSwitch(
width: 70.0,
height: 30.0,
valueFontSize: 12.0,
toggleSize: 20.0,
value: biometricStatus,
activeText: "Yes",
activeTextColor: brandBlue,
inactiveText: "No",
activeColor: brandOrange,
borderRadius: 30.0,
padding: 8.0,
showOnOff: true,
onToggle: (val) {
setState(() {
biometricStatus = val;
});
},
),
)),
],
);
}),
And here is the code for the initial states
TextEditingController textEditingController = TextEditingController();
TextEditingController textEditingController2 = TextEditingController();
StreamController<ErrorAnimationType> errorController;
StreamController<ErrorAnimationType> errorController2;
bool hasError = false;
const brandBlue = const Color(0xff243665);
const brandWhite = const Color(0xffF8F8FA);
const brandLightGrey = const Color(0xffE7E9ED);
const brandMediumGrey = const Color(0xffAAB0B9);
const brandDarkGrey = const Color(0xff868E94);
const brandLightBlue = const Color(0xff0FAFDA);
const brandOrange = const Color(0xffEB684F);
bool biometricStatus = true;
String pincode1;
String pincode2;
var pinCodeType = 1;
#override
void initState() {
errorController = StreamController<ErrorAnimationType>();
textEditingController = TextEditingController();
textEditingController2 = TextEditingController();
pinCodeType = 1;
super.initState();
}
#override
void dispose() {
errorController.close();
super.dispose();
}
How do I get this right:?
You don't need to call textEditingController.clear(); inside setState() to clear the value set on the TextEditingController. Move the function outside setState and this should fix the issue.
Related
I have four textfields, a title field, a details field, a date field, and a time field. Both the date and time fields are wrapped within a gesture detector, and onTap calls a pickDateAndTime method. The problem is that when I click on the date field and try to manually change the time through the input rather than the dial way, the focus goes to the title field and when I am still on the time picker and type something in the time picker, the title field gets changed with the new input. The weird part is that this error just appeared out of nowhere, and there are no errors reported in the console.
class TodoScreen extends StatefulWidget {
final int? todoIndex;
final int? arrayIndex;
const TodoScreen({Key? key, this.todoIndex, this.arrayIndex})
: super(key: key);
#override
State<TodoScreen> createState() => _TodoScreenState();
}
class _TodoScreenState extends State<TodoScreen> {
final ArrayController arrayController = Get.find();
final AuthController authController = Get.find();
final String uid = Get.find<AuthController>().user!.uid;
late TextEditingController _dateController;
late TextEditingController _timeController;
late TextEditingController titleEditingController;
late TextEditingController detailEditingController;
late String _setTime, _setDate;
late String _hour, _minute, _time;
late String dateTime;
late bool done;
#override
void initState() {
super.initState();
String title = '';
String detail = '';
String date = '';
String? time = '';
if (widget.todoIndex != null) {
title = arrayController
.arrays[widget.arrayIndex!].todos![widget.todoIndex!].title ??
'';
detail = arrayController
.arrays[widget.arrayIndex!].todos![widget.todoIndex!].details ??
'';
date = arrayController
.arrays[widget.arrayIndex!].todos![widget.todoIndex!].date!;
time = arrayController
.arrays[widget.arrayIndex!].todos![widget.todoIndex!].time;
}
_dateController = TextEditingController(text: date);
_timeController = TextEditingController(text: time);
titleEditingController = TextEditingController(text: title);
detailEditingController = TextEditingController(text: detail);
done = (widget.todoIndex == null)
? false
: arrayController
.arrays[widget.arrayIndex!].todos![widget.todoIndex!].done!;
}
DateTime selectedDate = DateTime.now();
TimeOfDay selectedTime = TimeOfDay(
hour: (TimeOfDay.now().minute > 55)
? TimeOfDay.now().hour + 1
: TimeOfDay.now().hour,
minute: (TimeOfDay.now().minute > 55) ? 0 : TimeOfDay.now().minute + 5);
Future<DateTime?> _selectDate() => showDatePicker(
builder: (context, child) {
return datePickerTheme(child);
},
initialEntryMode: DatePickerEntryMode.calendarOnly,
context: context,
initialDate: selectedDate,
initialDatePickerMode: DatePickerMode.day,
firstDate: DateTime.now(),
lastDate: DateTime(DateTime.now().year + 5));
Future<TimeOfDay?> _selectTime() => showTimePicker(
builder: (context, child) {
return timePickerTheme(child);
},
context: context,
initialTime: selectedTime,
initialEntryMode: TimePickerEntryMode.input);
Future _pickDateTime() async {
DateTime? date = await _selectDate();
if (date == null) return;
if (date != null) {
selectedDate = date;
_dateController.text = DateFormat("MM/dd/yyyy").format(selectedDate);
}
TimeOfDay? time = await _selectTime();
if (time == null) {
_timeController.text = formatDate(
DateTime(
DateTime.now().year,
DateTime.now().day,
DateTime.now().month,
DateTime.now().hour,
DateTime.now().minute + 5),
[hh, ':', nn, " ", am]).toString();
}
if (time != null) {
selectedTime = time;
_hour = selectedTime.hour.toString();
_minute = selectedTime.minute.toString();
_time = '$_hour : $_minute';
_timeController.text = _time;
_timeController.text = formatDate(
DateTime(2019, 08, 1, selectedTime.hour, selectedTime.minute),
[hh, ':', nn, " ", am]).toString();
}
}
#override
Widget build(BuildContext context) {
bool visible =
(_dateController.text.isEmpty && _timeController.text.isEmpty)
? false
: true;
final formKey = GlobalKey<FormState>();
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
title: Text((widget.todoIndex == null) ? 'New Task' : 'Edit Task',
style: menuTextStyle),
leadingWidth: (MediaQuery.of(context).size.width < 768) ? 90.0 : 100.0,
leading: Center(
child: Padding(
padding: (MediaQuery.of(context).size.width < 768)
? const EdgeInsets.only(left: 0)
: const EdgeInsets.only(left: 21.0),
child: TextButton(
style: const ButtonStyle(
splashFactory: NoSplash.splashFactory,
),
onPressed: () {
Get.back();
},
child: Text(
"Cancel",
style: paragraphPrimary,
),
),
),
),
centerTitle: true,
actions: [
Center(
child: Padding(
padding: (MediaQuery.of(context).size.width < 768)
? const EdgeInsets.only(left: 0)
: const EdgeInsets.only(right: 21.0),
child: TextButton(
style: const ButtonStyle(
splashFactory: NoSplash.splashFactory,
),
onPressed: () async {
},
child: Text((widget.todoIndex == null) ? 'Add' : 'Update',
style: paragraphPrimary),
),
),
)
],
),
body: SafeArea(
child: Container(
width: double.infinity,
padding: (MediaQuery.of(context).size.width < 768)
? const EdgeInsets.symmetric(horizontal: 15.0, vertical: 20.0)
: const EdgeInsets.symmetric(horizontal: 35.0, vertical: 15.0),
child: Column(
children: [
Form(
key: formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
validator: Validator.titleValidator,
controller: titleEditingController,
autofocus: true, // problem here
autocorrect: false,
cursorColor: Colors.grey,
maxLines: 1,
maxLength: 25,
textInputAction: TextInputAction.next,
decoration: InputDecoration(
counterStyle: counterTextStyle,
hintStyle: hintTextStyle,
hintText: "Title",
border: InputBorder.none),
style: todoScreenStyle),
primaryDivider,
TextField(
controller: detailEditingController,
maxLines: null,
autocorrect: false,
cursorColor: Colors.grey,
textInputAction: TextInputAction.done,
decoration: InputDecoration(
counterStyle: counterTextStyle,
hintStyle: hintTextStyle,
hintText: "Notes",
border: InputBorder.none),
style: todoScreenDetailsStyle),
],
),
),
Visibility(
visible: (widget.todoIndex != null) ? true : false,
child: GestureDetector(
onTap: () {},
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Completed",
style: todoScreenStyle,
),
Transform.scale(
scale: 1.3,
child: Theme(
data: ThemeData(
unselectedWidgetColor: const Color.fromARGB(
255, 187, 187, 187)),
child: Checkbox(
shape: const CircleBorder(),
checkColor: Colors.white,
activeColor: primaryColor,
value: done,
side: Theme.of(context).checkboxTheme.side,
onChanged: (value) {
setState(() {
done = value!;
});
})),
)
],
),
),
),
GestureDetector(
onTap: () async {
await _pickDateTime();
setState(() {
visible = true;
});
},
child: Column(
children: [
Row(
children: [
Flexible(
child: TextField(
enabled: false,
controller: _dateController,
onChanged: (String val) {
_setDate = val;
},
decoration: InputDecoration(
hintText: "Date",
hintStyle: hintTextStyle,
border: InputBorder.none),
style: todoScreenStyle,
),
),
visible
? IconButton(
onPressed: () {
_dateController.clear();
_timeController.clear();
setState(() {});
},
icon: const Icon(
Icons.close,
color: Colors.white,
))
: Container()
],
),
primaryDivider,
TextField(
onChanged: (String val) {
_setTime = val;
},
enabled: false,
controller: _timeController,
decoration: InputDecoration(
hintText: "Time",
hintStyle: hintTextStyle,
border: InputBorder.none),
style: todoScreenStyle,
)
],
),
),
],
),
),
),
);
}
}
Should I open an issue on Github, as I had not made any changes to the code for it behave this way and also because there were no errors in the console
Here is the full code on Github
Update
Here is a reproducible example:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const TodoScreen(),
);
}
}
class TodoScreen extends StatefulWidget {
const TodoScreen({Key? key}) : super(key: key);
#override
State<TodoScreen> createState() => _TodoScreenState();
}
class _TodoScreenState extends State<TodoScreen> {
late TextEditingController _dateController;
late TextEditingController _timeController;
late TextEditingController titleEditingController;
late TextEditingController detailEditingController;
late String _setTime, _setDate;
late String _hour, _minute, _time;
late String dateTime;
#override
void initState() {
super.initState();
String title = '';
String detail = '';
String date = '';
String? time = '';
_dateController = TextEditingController(text: date);
_timeController = TextEditingController(text: time);
titleEditingController = TextEditingController(text: title);
detailEditingController = TextEditingController(text: detail);
}
#override
void dispose() {
super.dispose();
titleEditingController.dispose();
detailEditingController.dispose();
_timeController.dispose();
_dateController.dispose();
}
Theme timePickerTheme(child) => Theme(
data: ThemeData.dark().copyWith(
timePickerTheme: TimePickerThemeData(
backgroundColor: const Color.fromARGB(255, 70, 70, 70),
dayPeriodTextColor: Colors.green,
hourMinuteTextColor: MaterialStateColor.resolveWith((states) =>
states.contains(MaterialState.selected)
? Colors.white
: Colors.white),
dialHandColor: Colors.green,
helpTextStyle: TextStyle(
fontSize: 12, fontWeight: FontWeight.bold, color: Colors.green),
dialTextColor: MaterialStateColor.resolveWith((states) =>
states.contains(MaterialState.selected)
? Colors.white
: Colors.white),
entryModeIconColor: Colors.green,
),
textButtonTheme: TextButtonThemeData(
style: ButtonStyle(
foregroundColor:
MaterialStateColor.resolveWith((states) => Colors.green)),
),
),
child: child!,
);
Theme datePickerTheme(child) => Theme(
data: ThemeData.dark().copyWith(
colorScheme: ColorScheme.dark(
surface: Colors.green,
secondary: Colors.green,
onPrimary: Colors.white,
onSurface: Colors.white,
primary: Colors.green,
)),
child: child!,
);
DateTime selectedDate = DateTime.now();
TimeOfDay selectedTime = TimeOfDay(
hour: (TimeOfDay.now().minute > 55)
? TimeOfDay.now().hour + 1
: TimeOfDay.now().hour,
minute: (TimeOfDay.now().minute > 55) ? 0 : TimeOfDay.now().minute + 5);
Future<DateTime?> _selectDate() => showDatePicker(
builder: (context, child) {
return datePickerTheme(child);
},
initialEntryMode: DatePickerEntryMode.calendarOnly,
context: context,
initialDate: selectedDate,
initialDatePickerMode: DatePickerMode.day,
firstDate: DateTime.now(),
lastDate: DateTime(DateTime.now().year + 5));
Future<TimeOfDay?> _selectTime() => showTimePicker(
builder: (context, child) {
return timePickerTheme(child);
},
context: context,
initialTime: selectedTime,
initialEntryMode: TimePickerEntryMode.input);
Future _pickDateTime() async {
DateTime? date = await _selectDate();
if (date == null) return;
if (date != null) {
selectedDate = date;
_dateController.text = selectedDate.toString();
}
TimeOfDay? time = await _selectTime();
if (time != null) {
selectedTime = time;
_hour = selectedTime.hour.toString();
_minute = selectedTime.minute.toString();
_time = '$_hour : $_minute';
_timeController.text = _time;
_timeController.text =
DateTime(2019, 08, 1, selectedTime.hour, selectedTime.minute)
.toString();
}
}
#override
Widget build(BuildContext context) {
bool visible =
(_dateController.text.isEmpty && _timeController.text.isEmpty)
? false
: true;
final formKey = GlobalKey<FormState>();
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
centerTitle: true,
),
body: SafeArea(
child: Container(
width: double.infinity,
padding: (MediaQuery.of(context).size.width < 768)
? const EdgeInsets.symmetric(horizontal: 15.0, vertical: 20.0)
: const EdgeInsets.symmetric(horizontal: 35.0, vertical: 15.0),
child: Column(
children: [
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(14.0)),
padding: const EdgeInsets.symmetric(
horizontal: 24.0, vertical: 15.0),
child: Form(
key: formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
controller: titleEditingController,
autofocus: true,
autocorrect: false,
cursorColor: Colors.grey,
maxLines: 1,
maxLength: 25,
textInputAction: TextInputAction.next,
decoration: InputDecoration(
hintText: "Title", border: InputBorder.none),
),
Divider(color: Colors.black),
TextField(
controller: detailEditingController,
maxLines: null,
autocorrect: false,
cursorColor: Colors.grey,
textInputAction: TextInputAction.done,
decoration: InputDecoration(
hintText: "Notes", border: InputBorder.none),
),
],
),
)),
GestureDetector(
onTap: () async {
await _pickDateTime();
setState(() {
visible = true;
});
},
child: Container(
margin: const EdgeInsets.only(top: 20.0),
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: 24.0, vertical: 15.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(14.0)),
child: Column(
children: [
Row(
children: [
Flexible(
child: TextField(
enabled: false,
controller: _dateController,
onChanged: (String val) {
_setDate = val;
},
decoration: InputDecoration(
hintText: "Date", border: InputBorder.none),
),
),
visible
? IconButton(
onPressed: () {
_dateController.clear();
_timeController.clear();
setState(() {});
},
icon: const Icon(
Icons.close,
color: Colors.white,
))
: Container()
],
),
Divider(
color: Colors.blue,
),
TextField(
onChanged: (String val) {
_setTime = val;
},
enabled: false,
controller: _timeController,
decoration: InputDecoration(
hintText: "Enter", border: InputBorder.none),
)
],
)),
),
],
),
),
),
);
}
}
In your main.dart file, you should return something like this:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
// This allows closing keyboard when tapping outside of a text field
FocusScopeNode currentFocus = FocusScope.of(context);
if (!currentFocus.hasPrimaryFocus &&
currentFocus.focusedChild != null) {
FocusManager.instance.primaryFocus!.unfocus();
}
},
child: // your app's entry point,
);
}
}
Add a focusNode in your textField:
FocusNode focusNode = FocusNode();
TextField(
focusNode: focusNode,
);
And then in the gesture detector, add that following code to unselect the textfield input:
FocusScope.of(context).requestFocus(FocusNode());
simply wrap your Scaffold widget GestureDetector and add FocusScope.of(context).requestFocus(FocusNode()); it will automatically unfocused text field when you click anywhere on your screen
GestureDetector(
onTap: () {
FocusScope.of(context).requestFocus(FocusNode());
},
child: Scaffold()
)
You can use below code to remove focus in gesture detector event
FocusScopeNode currentFocus = FocusScope.of(context);
if (!currentFocus.hasPrimaryFocus && currentFocus.focusedChild != null) {
currentFocus.unfocus();
}
I am trying to animate a TextField's width for a search bar based on a bool isSearching. I initially used AnimatedContainer with the width set to 350 if isSearching is false and 300 if isSearching is true. I need to have a "cancel" button appear to the right of the TextField when searching. Using 350 and 300 works well when on a bigger phone size (iPhone 13), but when testing on a smaller screen size (iPhone 13 mini) there is a render overflow.
Here is my class setup:
class _ExplorePageState extends ConsumerState<ExplorePage> {
bool _isSearching = false;
bool _cancelVisibility = false;
late TextEditingController _searchController;
late FocusNode _focusNode;
double _searchFieldSize = 350;
Future<void> toggleCancelButton() async {
await Future.delayed(
const Duration(milliseconds: 400),
() {
setState(() {
_cancelVisibility = true;
});
},
);
}
#override
void initState() {
_searchController = TextEditingController();
_focusNode = FocusNode();
_focusNode.addListener(() {
if (_focusNode.hasFocus || _isSearching) {
_searchFieldSize = 300;
setState(() {
_isSearching = true;
toggleCancelButton();
});
} else {
_searchFieldSize = 350;
setState(() {
_isSearching = false;
_cancelVisibility = false;
_focusNode.unfocus();
});
}
});
super.initState();
}
#override
void dispose() {
super.dispose();
_focusNode.dispose();
_searchController.dispose();
}
And here is the Row where the AnimatedContainer in which I want to animate is located:
SizedBox(
width: MediaQuery.of(context).size.width,
child: Row(
children: [
const SizedBox(width: defaultMargin),
AnimatedContainer(
duration: const Duration(milliseconds: 350),
width: _searchFieldSize,
child: TextField(
focusNode: _focusNode,
controller: _searchController,
autofocus: false,
autocorrect: false,
style:
Theme.of(context).textTheme.bodySmall!.copyWith(
color: isDark
? darkThemeDefaultTextColor
: lightThemeDefaultTextColor,
),
decoration: InputDecoration(
prefixIcon: Icon(
Ionicons.search_outline,
color: isDark
? darkThemeSubTextColor
: lightThemeSubTextColor,
size: 16,
),
suffixIcon: _cancelVisibility
? IconButton(
icon: Icon(
Ionicons.close_circle_outline,
size: 18,
color: isDark
? darkThemeSubTextColor
: lightThemeSubTextColor,
),
onPressed: () {
_searchController.clear();
ref
.read(explorePageControllerProvider
.notifier)
.clearAll();
setState(() {});
},
)
: null,
hintStyle: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(
color: isDark
? darkThemeSubTextColor
: lightThemeSubTextColor,
),
hintText: 'Search (character, village)',
contentPadding: const EdgeInsets.only(
left: 14.0, bottom: 8.0, top: 8.0),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: isDark
? darkThemeSubTextColor
: lightThemeSubTextColor,
),
borderRadius: BorderRadius.circular(15),
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: isDark
? darkThemeSubTextColor
: lightThemeSubTextColor,
),
borderRadius: BorderRadius.circular(15),
),
),
onChanged: (value) async {
if (_searchController.value.text.isEmpty) {
ref
.read(
explorePageControllerProvider.notifier)
.clearAll();
} else {
await ref
.read(
explorePageControllerProvider.notifier)
.getSearchResults(value);
}
setState(() {});
},
),
),
Visibility(
visible: _cancelVisibility,
child: TextButton(
style: ButtonStyle(
overlayColor:
MaterialStateProperty.all(Colors.transparent),
),
onPressed: () {
_searchController.clear();
_focusNode.unfocus();
setState(() {
_isSearching = false;
_cancelVisibility = false;
_searchFieldSize = 350;
});
},
child: Text(
'cancel',
style: Theme.of(context).textTheme.bodySmall,
),
),
),
],
),
),
To accommodate smaller phone sizes I would usually use MediaQuery to set the width but I need to deal with the width inside initState before build is run.
Is there a way to do this with AnimatedContainer or should I use a different approach for animating the search bar?
I am not sure this will work but could you try this:
// You should try to estimate the width of the cancel button (Space it will take)
//As i think it is constant across mobile devices.
Say the width of your Cancel button is 50,
var initialWidth = MediaQuery.of(context).size.width; // without cancel button
AnimatedContainer(
duration: const Duration(milliseconds: 350),
width: !isSearching ? initialWidth: initialWidth - 50 ,
child: TextField()), TextButton(child:Text("Cancel"),onPressed(){})
I have an issue with DatePicker in my application. Here's a simple TextFormField that I've created in my app which will open up DatePicker whenever the user taps on it.
This widget is a part of a form where I also have specified the GlobalKey and TextController for it. The rightmost calendar icon uses the suffixIcon property of InputDecoration and it changes to a clear icon whenever the user selects a date.
Here's the code for the above widget.
TextFormField(
onTap: () => _selectStartDate(context),
controller: _startDateTextController,
keyboardType: TextInputType.datetime,
readOnly: true,
decoration: InputDecoration(
suffixIcon: showClear ? IconButton(
icon: Icon(Icons.clear),
onPressed: _clearStartDate,
) : Icon(Icons.date_range),
labelText: 'Start Date',
labelStyle: TextStyle(
fontSize: AppDimensions.font26,
color: AppColors.paraColor
),
),
validator: (value){
if(value!.isEmpty) {
return 'Please enter a date';
}
},
),
My goal is to let the user pick on a date, and clear it should they choose to do so.
Here's the code for the _selectStartDate and _clearStartDate functions as well as the necessary controller and key. I'm using the intl package to format the date.
final _formKey = GlobalKey<FormState>();
TextEditingController _startDateTextController = TextEditingController();
DateTime selectedStartDate = DateTime.now();
bool showClear = false;
_selectStartDate(BuildContext context) async {
final DateTime? newStartDate = await showDatePicker(
context: context,
initialDate: selectedStartDate,
firstDate: DateTime(1900),
lastDate: DateTime(2100),
helpText: 'STARTING DATE'
);
if(newStartDate != null && newStartDate != selectedStartDate) {
setState(() {
selectedStartDate = newStartDate;
_startDateTextController.text = DateFormat.yMMMd().format(selectedStartDate);
showClear = true;
});
}
}
_clearStartDate() {
_startDateTextController.clear();
setState(() {
showClear = !showClear;
});
}
When i run the app, the DatePicker pops up and I'm able to select a date. The date is then shown on the TextFormField like the image below.
As you can see the clear icon is displayed. However, when i clicked on it, the DatePicker still popped up. And when i clicked on cancel on the DatePicker window, the TextFormField is cleared as expected.
Here's the complete code.
class BookingForm extends StatefulWidget {
const BookingForm({Key? key}) : super(key: key);
#override
_BookingFormState createState() => _BookingFormState();
}
class _BookingFormState extends State<BookingForm> {
final _formKey = GlobalKey<FormState>();
TextEditingController _startDateTextController = TextEditingController();
DateTime selectedStartDate = DateTime.now();
bool showClear = false;
_selectStartDate(BuildContext context) async {
final DateTime? newStartDate = await showDatePicker(
context: context,
initialDate: selectedStartDate,
firstDate: DateTime(1900),
lastDate: DateTime(2100),
helpText: 'STARTING DATE'
);
if(newStartDate != null && newStartDate != selectedStartDate) {
setState(() {
selectedStartDate = newStartDate;
_startDateTextController.text = DateFormat.yMMMd().format(selectedStartDate);
showClear = true;
});
}
}
_clearStartDate() {
_startDateTextController.clear();
setState(() {
showClear = !showClear;
});
}
#override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Container(
margin: EdgeInsets.only(left: AppDimensions.width20, right: AppDimensions.width20),
padding: EdgeInsets.all(AppDimensions.height20),
decoration: BoxDecoration(
color: Colors.white70,
borderRadius: BorderRadius.circular(AppDimensions.radius20),
boxShadow: [
BoxShadow(
color: Color(0xFFe8e8e8),
blurRadius: 5.0,
spreadRadius: 1.0,
offset: Offset(2,2)
),
]
),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
TextFormField(
onTap: () => _selectStartDate(context),
controller: _startDateTextController,
keyboardType: TextInputType.datetime,
readOnly: true,
decoration: InputDecoration(
suffixIcon: showClear ? IconButton(
icon: Icon(Icons.clear),
onPressed: _clearStartDate,
) : Icon(Icons.date_range),
labelText: 'Start Date',
labelStyle: TextStyle(
fontSize: AppDimensions.font26,
color: AppColors.paraColor
),
),
validator: (value){
if(value!.isEmpty) {
return 'Please enter a date';
}
},
),
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: EdgeInsets.only(
top: AppDimensions.height10,
bottom: AppDimensions.height10,
left: AppDimensions.width45,
right: AppDimensions.width45
),
primary: AppColors.mainColor2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppDimensions.radius20)
)
),
child: SmallText(
text: 'Search',
size: AppDimensions.font26,
color: Colors.white,
),
onPressed: (){
if(_formKey.currentState!.validate()) {
_formKey.currentState!.save();
}
}
),
],
),
),
);
}
}
I'm not sure why this happens. Any help is greatly appreciated. Thank you
I made a custom form and when I want to use the validation. My issue is that the error message is not appearing as I wanted. Here's the screenshot of it.
So I want to change the position of the error message to be below my container. Does anyone have any idea how to do it ?
Here's the code of the form:
class AuthForm extends StatefulWidget {
final bool isPassword;
final IconData prefixIcon;
final String hintText;
late bool isPasswordVisible = isPassword;
final bool isCalendar;
final TextEditingController controller;
final bool isDropDown;
final bool isPhone;
final String? Function(String?)? validator;
AuthForm({Key? key, this.isPassword = false, required this.prefixIcon, required this.hintText,
this.isCalendar = false, required this.controller, this.isDropDown = false, this.isPhone = false, required this.validator}) : super(key: key);
#override
State<AuthForm> createState() => _AuthFormState();
}
class _AuthFormState extends State<AuthForm> {
#override
void initState() {
super.initState();
if (widget.isPhone){
getCountryCode();
}
}
start () async {
await CountryCodes.init();
}
Locale? getCountryCode () {
start();
final Locale? deviceLocale = CountryCodes.getDeviceLocale();
final CountryDetails details = CountryCodes.detailsForLocale();
return deviceLocale;
}
DateTime selectedDate = DateTime(2000,1);
Future<void> _selectDate(BuildContext context) async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: selectedDate,
firstDate: DateTime(1950, 1),
lastDate: DateTime.now());
if (picked != null && picked != selectedDate) {
setState(() {
selectedDate = picked;
});
}
}
#override
Widget build(BuildContext context) {
return Container(
width: 70.w,
height: 5.h,
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
width: 0.13.w,
color: Theme.of(context).splashColor,
),
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(
widget.prefixIcon,
color: Theme.of(context).primaryColor,
size: 6.w,
),
widget.isDropDown ? const DropDownBar() :
Expanded(
child: TextFormField(
//autovalidateMode: AutovalidateMode.onUserInteraction,
validator: widget.validator,
keyboardType: widget.isPhone ? TextInputType.phone : TextInputType.text,
inputFormatters: [DialCodeFormatter()],
controller: widget.controller,
textAlign: TextAlign.center,
obscureText: widget.isPasswordVisible,
style: Theme.of(context).textTheme.bodyText2,
decoration: InputDecoration(
hintText : widget.hintText,
hintStyle: Theme.of(context).textTheme.bodyText1,
border: InputBorder.none,
),
onTap: () async {
if (widget.isCalendar){
//Dismiss the keyboard
FocusScope.of(context).requestFocus(FocusNode());
//Call the calendar
await _selectDate(context);
widget.controller.text = DateFormat('dd-MM-yyyy').format(selectedDate);
}
}
),
),
//Show Icon only if the form is for a Password
Visibility(
visible: widget.isPassword,
//Maintain the space where the widget is even if it is hid
maintainAnimation: true,
maintainState: true,
maintainSize: true,
child: InkWell(
highlightColor : Colors.transparent,
splashColor: Colors.transparent,
child: Icon(
widget.isPasswordVisible ? Icons.visibility : Icons.visibility_off,
color: Theme.of(context).primaryColor,
size: 6.w,
),
onTap: () {
setState(() {
widget.isPasswordVisible = !widget.isPasswordVisible;
});
},
),
),
],
),
);
}
}
Thanks for your suggestion,
Chris
You should try below piece of code for TextFormField
TextFormField(
textInputAction: TextInputAction.next,
style: TextStyle(fontSize: 16, height: 1),
inputFormatters: <TextInputFormatter>[FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(10)],
keyboardType: TextInputType.phone,
validator: (val) {
if (val!.isEmpty) {
return 'Please enter your mobile number';
} else if (val.length < 10) {
return 'Mobile number should be 10 digits only';
}
},
controller: bloc.mobileNumberLoginController,
decoration: InputDecoration(
filled: true,
border: OutlineInputBorder(
borderSide: BorderSide(
color: code != null ? HexColor(code!) : Colors.pink,
)),
contentPadding: EdgeInsets.only(top: 5, bottom: 5, left: 10, right: 10),
),
),
I'm using Flutter Typeahead to use a google maps places autocomplete. Everything is working fine except that I can't get the textField to show the selected suggestion text. I am setting the TextEditingController.text to some valid text and it just doesn't display anything in the TextField:
viewModel.typeAheadController.text =
suggestion.structuredFormatting.mainText;
Why will the selected suggestion text not display in the TextField, when I have set the text value on the TextEditingController upon selecting the option?
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:google_maps_webservice/places.dart';
import 'package:reactive_forms/reactive_forms.dart';
import 'package:flutter/widgets.dart';
import 'package:vepo/fonts.gen.dart';
import 'package:vepo/src/domain/entities/establishment/establishment_domain_entity.dart';
import 'package:vepo/src/presentation/widgets/purely_display_widgets/text/caption_widget.dart';
import 'establishments_autocomplete_field_view_model.dart';
class VpEstablishmentsAutocompleteField
extends ReactiveFormField<EstablishmentDomainEntity> {
VpEstablishmentsAutocompleteField(
{#required String formControlName,
this.context,
this.label,
this.hint,
bool hasFocus = false,
this.onFocusChange,
Map<String, String> Function(AbstractControl<dynamic>) validationMessages,
this.key})
: super(
key: key,
formControlName: formControlName,
validationMessages: validationMessages,
showErrors: (control) => control.invalid,
builder: (ReactiveFormFieldState<EstablishmentDomainEntity> field) {
final viewModel = ProviderContainer()
.read<EstablishmentsAutocompleteFieldViewModel>(
establishmentsAutocompleteViewModelProvider);
return FocusScope(
child: Focus(
onFocusChange: (isFocused) {
hasFocus = isFocused;
if (onFocusChange != null) {
onFocusChange(isFocused);
}
field.setState(() {});
},
child: TypeAheadFormField<Prediction>(
textFieldConfiguration: TextFieldConfiguration(
controller: viewModel.typeAheadController,
autofocus: true,
decoration: InputDecoration(
errorMaxLines: 3,
suffixIconConstraints: const BoxConstraints(
minWidth: 2,
minHeight: 2,
),
suffixIcon: ReactiveStatusListenableBuilder(
formControlName: formControlName,
builder: (context, control, child) {
return control.pending
? Container(
width: 90,
height: 60,
child: Stack(children: [
Positioned(
top: 10,
right: 15,
child: CircularProgressIndicator(
backgroundColor:
Theme.of(context)
.primaryColorDark),
)
]))
: Container(width: 0);
},
),
alignLabelWithHint: true,
labelStyle: TextStyle(
height: 0,
fontSize: hasFocus ? 24 : 18.0,
color: Theme.of(context).primaryColor),
hintText: hint,
labelText: label,
counterText: ''),
),
suggestionsCallback: (pattern) async {
return await viewModel.fetchSuggestions(
pattern, context);
},
suggestionsBoxDecoration: SuggestionsBoxDecoration(
elevation: 20,
color: Theme.of(context).scaffoldBackgroundColor,
borderRadius: const BorderRadius.all(
Radius.circular(10.0),
),
),
itemBuilder: (context, suggestion) {
return ListTile(
leading: FaIcon(FontAwesomeIcons.storeAlt,
size: 25,
color: Theme.of(context).primaryColor),
title: RichText(
text: TextSpan(
text: viewModel.getBoldText(
suggestion.structuredFormatting.mainText,
suggestion.structuredFormatting
.mainTextMatchedSubstrings.first),
style: TextStyle(
fontSize: 20,
fontFamily: FontFamily.hind,
fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColorDark),
children: <TextSpan>[
TextSpan(
text: suggestion
.structuredFormatting.mainText
.substring(suggestion
.structuredFormatting
.mainTextMatchedSubstrings
.first
.length as int),
style: const TextStyle(
fontWeight: FontWeight.w200,
color: Colors.black54))
],
),
),
subtitle: VpCaption(
text:
suggestion.structuredFormatting.secondaryText,
color: Colors.black54,
fontSize: 17,
overflow: TextOverflow.visible,
),
);
},
onSuggestionSelected: (suggestion) {
viewModel.typeAheadController.text =
suggestion.structuredFormatting.mainText;
viewModel.typeAheadController.value =
TextEditingValue(
text:
suggestion.structuredFormatting.mainText);
final x = EstablishmentDomainEntity(
placeId: suggestion.placeId);
field.didChange(x);
field.control.markAsTouched();
field.control.markAsDirty();
},
)));
});
final Key key;
final BuildContext context;
final String label;
final String hint;
bool hasFocus;
final void Function(bool) onFocusChange;
#override
ReactiveFormFieldState<EstablishmentDomainEntity> createState() =>
ReactiveFormFieldState<EstablishmentDomainEntity>();
}
Is your TextEditingController initialized? This code works fine for me and updates upon a option selected.
final TextEditingController _typeAheadController = TextEditingController();
...
Container(
width: constraints.maxWidth * 0.80,
height: 50,
decoration: BoxDecoration(
color: Color.fromRGBO(245, 245, 245, 1),
borderRadius: BorderRadius.all(Radius.circular(50))),
child: Padding(
padding: EdgeInsets.only(left: 20),
child: TypeAheadField(
hideOnEmpty: true,
textFieldConfiguration: TextFieldConfiguration(
autofocus: false,
controller: _typeAheadController,
decoration: InputDecoration(
border: InputBorder.none,
hintText: 'Enter your city',
hintStyle: TextStyle(
fontFamily: 'Metropolis-SemiBold',
color: Color.fromRGBO(197, 197, 197, 1)),
),
),
suggestionsCallback: (pattern) async {
var x = await FunctionsService()
.getRegionSuggestion(pattern);
locationCodes = x[1];
suggestionList = x[0];
return x[0];
},
itemBuilder: (context, suggestion) {
return ListTile(
title: Text(suggestion),
);
},
onSuggestionSelected: (suggestion) {
setState(() {
_typeAheadController.text = suggestion;
publicLocation = suggestion.toString();
locationSelected = true;
});
}),
),
),
This one was quite a specific scenario as I'm using so many packages.
I needed to create a custom control value accessor for reactive_forms because my form value was a complex object (EstablishmentDomainEntity), not a string:
import 'package:reactive_forms/reactive_forms.dart';
import 'package:vepo/src/domain/entities/establishment/establishment_domain_entity.dart';
/// Represents a control value accessor that convert between data types
/// [EstablishmentDomainEntity] and [String].
class EstablishmentValueAccessor
extends ControlValueAccessor<EstablishmentDomainEntity, String> {
#override
String modelToViewValue(EstablishmentDomainEntity modelValue) {
return modelValue == null ? '' : modelValue.name;
}
#override
EstablishmentDomainEntity viewToModelValue(String viewValue) {
return (viewValue == '' || viewValue == null)
? null
: EstablishmentDomainEntity(name: viewValue);
}
}
Then in my custom widget constructor i had to pass in then value accessor:
class VpEstablishmentsAutocompleteField
extends ReactiveFormField<EstablishmentDomainEntity> {
VpEstablishmentsAutocompleteField(
{#required String formControlName,
this.key})
: super(
key: key,
formControlName: formControlName,
valueAccessor: EstablishmentValueAccessor(),
Then in my TypeAheadFormField onSuggestionSelected, call field.didChange and also manually set the TypeAheadFormField TextEditingController.text:
onSuggestionSelected: (suggestion) {
viewModel.typeAheadController.text =
suggestion.structuredFormatting.mainText;
final x = EstablishmentDomainEntity(
placeId: suggestion.placeId,
name: suggestion.structuredFormatting.mainText);
field.didChange(x);
},