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;
}

The problem is still there until now, you can followup it here, so I solved the problem temporarily as follows:
1- Define
FocusNodeas list like this:2- pass
FocusNodeto inputs like this:3- Call
_unFocusNodes()before callvalidate()like this (here is the trick):