Search code examples
androidfluttermobile

Flutter TextInputAction.next doesn't move to the next input field when pressing next on Android keyboard


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",
                  ),
                ),
               ],
            ),
          ),
        ),
      ),
     );
  }
}

Solution

  • 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",
                      ),
                    ),
                   ],
                ),
              ),
            ),
          ),
         );
      }
    }