FocusNode does not auto scroll to TextFormField when item already selected - flutter

1.5k Views Asked by At

Scenario: this is a big form, the user selects the TextFormField and then manually scrolls down to the save button. if the _formKey.currentState.validate() detects validation issues we can to get the focus back the the TextFormField with issues.

first, we call _myFocusNode.unfocus(); // this works because the keyword is automatically close. next, we call FocusScope.of(context).requestFocus(_myFocusNode) //this also works because we can start typing right after. but, the FocusScope.of(context).requestFocus is not automatically scrolling back to the TextFormField.

If we try to another TextFormField (Tfield2) other than the last selected, the Tfield2 gets focus and the scroll also makes it visible.

NOTE: if we start typing again, the scroll is executed and the TextFormField becomes visible.

https://github.com/flutter/flutter/issues/58877

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Material App',
      theme: ThemeData.dark(),
      home: Home(),
    );
  }
}

class Home extends StatefulWidget {
  @override
  _HomeState createState() => _HomeState();
}

class _HomeState extends State<Home> {
  final GlobalKey<FormState> _formKey = new GlobalKey<FormState>();
  bool show = false;
  TextEditingController cnt1 = new TextEditingController();
  TextEditingController cnt2 = new TextEditingController();
  TextEditingController cnt3 = new TextEditingController();
  TextEditingController cnt4 = new TextEditingController();
  TextEditingController cnt5 = new TextEditingController();
  TextEditingController cnt6 = new TextEditingController();
  TextEditingController cnt7 = new TextEditingController();
  TextEditingController cnt8 = new TextEditingController();

  FocusNode _focuserr;
  FocusNode _focus1;
  FocusNode _focus2;
  FocusNode _focus3;
  FocusNode _focus4;
  FocusNode _focus5;
  FocusNode _focus6;
  FocusNode _focus7;
  FocusNode _focus8;

 @override
  void dispose() {
    // Clean up the focus node when the Form is disposed.
    _focuserr.dispose();
    _focus1.dispose();
    _focus2.dispose();
    _focus3.dispose();
    _focus4.dispose();
    _focus5.dispose();
    _focus6.dispose();
    _focus7.dispose();
    _focus8.dispose();

    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    _focus1 = new FocusNode();
    _focus2 = new FocusNode();
    _focus3 = new FocusNode();
    _focus4 = new FocusNode();
    _focus5 = new FocusNode();
    _focus6 = new FocusNode();
    _focus7 = new FocusNode();
    _focus8 = new FocusNode();

    return Scaffold(
      appBar: AppBar(
        title: Text('Material App Bar'),
      ),
      body: Center(
        child: Container(
          child: Text('Hello World'),
        ),
      ),
      floatingActionButton: FloatingActionButton.extended(
        onPressed: () {
          showModalBottomSheet(
            context: context,
            isScrollControlled: true,
            builder: (context) => StatefulBuilder(
              builder: (context, setState) {
                return Padding(
                  padding: EdgeInsets.only(left:10, right: 10, bottom: MediaQuery.of(context).viewInsets.bottom + 5),
                  child: Container(
                    height: 300,
                    child: SingleChildScrollView(
                      child: Form(
                        key: _formKey,
                        child: Column(
                          mainAxisSize: MainAxisSize.min,
                          children: [
                            createTextField(cnt1, _focus1, 1),
                            createTextField(cnt2, _focus2, 2),
                            createTextField(cnt3, _focus3, 3),
                            createTextField(cnt4, _focus4, 4),
                            createTextField(cnt5, _focus5, 5),
                            createTextField(cnt6, _focus6, 6),
                            createTextField(cnt7, _focus7, 7),
                            createTextField(cnt8, _focus8, 8),
                            RaisedButton(
                                child: Text('Validate'),
                                onPressed: () {
                                  _validateInputs(context);
                                })
                          ],
                        ),
                      ),
                    ),
                  ),
                );
              },
            ),
          );
        },
        icon: Icon(Icons.add),
        label: Text('bottomsheet'),
      ),
    );
  }

  Widget createTextField(TextEditingController c, FocusNode f, int id){
  return Padding(
      padding: const EdgeInsets.only(
          top: 5, bottom: 5, left: 10, right: 10),
      child: Container(
        color: Colors.lightBlue.withOpacity(0.3),
        child:
        TextFormField(
          controller: c,
          focusNode: f,
          validator: (val) {
            var result = isNotNull(val);
            if(result != null && _focuserr == null)
              _focuserr = f;
              
            return result;
          },
          decoration: InputDecoration(
            counterText : "",
            hintStyle: TextStyle(fontSize: 17),
            hintText: 'TXT$id',
            border: InputBorder.none,
            focusedBorder: UnderlineInputBorder(
                borderSide: BorderSide(color: Colors.blue[300], width: 0.3)),
            enabledBorder: InputBorder.none,
            errorBorder: InputBorder.none,
            disabledBorder: InputBorder.none,
            contentPadding: const EdgeInsets.only(left: 10, top: 0),
          ),
        ),
      )
    );
  }

  void _validateInputs(BuildContext cnt) {
    final form = _formKey.currentState;
    _focuserr = null;
    
    if (form.validate()) {
      form.save();
    }
    else
    {
      setState(() {
        FocusManager.instance.primaryFocus.unfocus();
        FocusScope.of(cnt).requestFocus(_focuserr);
      });
    }
  }

  String isNotNull(String val) =>
      (val.length == 0) ? 'Cannot be empty' : null;
}

Demo:

2

There are 2 best solutions below

0
On

The problem is still there until now, you can followup it here, so I solved the problem temporarily as follows:

1- Define FocusNode as list like this:

  final _formKey = GlobalKey<FormState>();
  final List<Map<String, FocusNode>> _focusNodes = [];

  @override
  void initState() {
    super.initState();
    
    _focusNodes.add({'nickname' : FocusNode()});
    _focusNodes.add({'firstName' : FocusNode()});
    _focusNodes.add({'lastName' : FocusNode()});
    _focusNodes.add({'email' : FocusNode()});
    // ... to map your inputs with nodes
    
  }

  void _unFocusNodes(){
    for (var element in _focusNodes) {
      element.values.first.unfocus();
    }
  }

2- pass FocusNode to inputs like this:

TextFormField(
  focusNode: _focusNodes.firstWhere((element) => element.keys.first == 'email').values.first;
)

3- Call _unFocusNodes() before call validate() like this (here is the trick):

    setState(() {
      _unFocusNodes();
      _formKey.currentState?.validate();
    });
0
On

happened to be a confirmed issue on stable v17 as well as newer dev channel