Below there is a minimal code fragment from a password-change widget reproducing the issue. I'm using flutter 3.3.10 on windows 10 and a real (not emulated) ZTE Blade A5 device with Android 9.
A form with TextFormField
for password and password-confirmation.
TextFormField both have textInputAction: TextInputAction.next
and an InputDecoration
with a show/hide button.
show/hide buttons have an onPressed() function to change field content visibility status.
On input action , pressing "next" on android keyboard button on the first TextFormField, the focus doesn't move to next field if espectedthe onPressed function is not null;
If I set onPressed: null in the first or both TextFormField, textInputAction "next" works as expected. I tried focusNode
too without success.
Is there a way to get it working ? Thanks!
void main() async {
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 MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final _formKey = GlobalKey<FormState>();
final TextEditingController passwordController = TextEditingController();
final TextEditingController confirmPasswordController = TextEditingController();
bool _obscurePassword = true;
bool _obscureConfirmPassword = true;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Test"),
),
body: Center(
child: Form(
key: _formKey,
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: Column(
children: [
TextFormField(
obscureText: _obscurePassword,
controller: passwordController,
keyboardType: TextInputType.text,
textInputAction: TextInputAction.next,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.password),
suffixIcon: MaterialButton(
padding: EdgeInsets.zero,
minWidth: 0,
child: Icon(_obscurePassword ? Icons.visibility_off : Icons.visibility),
onPressed: () => setState(() => _obscurePassword = !_obscurePassword),
),
labelText: "Password",
),
),
TextFormField(
obscureText: _obscureConfirmPassword,
controller: confirmPasswordController,
keyboardType: TextInputType.text,
textInputAction: TextInputAction.next,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.password),
suffixIcon: MaterialButton(
padding: EdgeInsets.zero,
minWidth: 0,
child: Icon(_obscureConfirmPassword ? Icons.visibility_off : Icons.visibility),
onPressed: () => setState(() => _obscureConfirmPassword = !_obscureConfirmPassword),
),
labelText: "Password confirmation",
),
),
],
),
),
),
),
);
}
}
I ran into the same problem with textInputAction: TextInputAction.next
not working when using password/confirm password fields with obscure password input decorations.
I was able to get it working correctly by adding an onEditingComplete
handler for the password's TextFormField
that does a requestFocus()
to the move to the confirm password's FocusNode
.
I got the idea from @gspencergoog's comment on https://github.com/flutter/flutter/issues/63575
Modified code (see //ADDED
comments)
void main() async {
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 MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final _formKey = GlobalKey<FormState>();
final TextEditingController passwordController = TextEditingController();
final TextEditingController confirmPasswordController = TextEditingController();
late final FocusNode confirmPasswordFocusNode; //ADDED
bool _obscurePassword = true;
bool _obscureConfirmPassword = true;
@override
void initState() { //ADDED
super.initState();
confirmPasswordFocusNode = FocusNode();
}
@override
void dispose() { //ADDED
confirmPasswordFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Test"),
),
body: Center(
child: Form(
key: _formKey,
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: Column(
children: [
TextFormField(
obscureText: _obscurePassword,
controller: passwordController,
keyboardType: TextInputType.text,
textInputAction: TextInputAction.next,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.password),
suffixIcon: MaterialButton(
padding: EdgeInsets.zero,
minWidth: 0,
child: Icon(_obscurePassword ? Icons.visibility_off : Icons.visibility),
onPressed: () => setState(() => _obscurePassword = !_obscurePassword),
),
labelText: "Password",
),
onEditingComplete: () { //ADDED
// Move the focus to the next node explicitly.
confirmPasswordFocusNode.requestFocus();
},
),
TextFormField(
obscureText: _obscureConfirmPassword,
controller: confirmPasswordController,
focusNode: confirmPasswordFocusNode, //ADDED
keyboardType: TextInputType.text,
textInputAction: TextInputAction.next,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.password),
suffixIcon: MaterialButton(
padding: EdgeInsets.zero,
minWidth: 0,
child: Icon(_obscureConfirmPassword ? Icons.visibility_off : Icons.visibility),
onPressed: () => setState(() => _obscureConfirmPassword = !_obscureConfirmPassword),
),
labelText: "Password confirmation",
),
),
],
),
),
),
),
);
}
}