How to keep the keyboard on screen while temporarily disabling a text field?
CupertinoTextField dismisses the keyboard when enabled=false or readOnly=true. I need to keep the keyboard on screen.
I searched for about four hours and finally came up with a solution: Have the text field's onChanged function focus a hidden widget that accepts keyboard input. Once the processing is complete, focus the text field again.
Working example:
import 'package:flutter/cupertino.dart'
show
CupertinoApp,
CupertinoButton,
CupertinoPageScaffold,
CupertinoTextField;
import 'package:flutter/widgets.dart'
show
BuildContext,
Center,
ClipRect,
Column,
Container,
FocusNode,
FocusScope,
MainAxisSize,
runApp,
State,
StatefulWidget,
Text,
TextAlign,
TextEditingController,
Widget;
import 'package:meta/meta.dart' show required;
class KeepKeyboardOnScreen extends StatefulWidget {
final FocusNode focusNode;
const KeepKeyboardOnScreen({#required this.focusNode});
#override
State createState() => KeepKeyboardOnScreenState();
}
class KeepKeyboardOnScreenState extends State<KeepKeyboardOnScreen> {
TextEditingController _controller;
#override
void initState() {
super.initState();
_controller = new TextEditingController();
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) => Container(
height: 0,
child: ClipRect(
child: CupertinoTextField(
controller: _controller,
focusNode: widget.focusNode,
onChanged: (_) => _controller.clear(),
),
),
);
}
class Page extends StatefulWidget {
#override
State createState() => PageState();
}
class PageState extends State<Page> {
TextEditingController _controller;
FocusNode _focusNode;
FocusNode _keepKeyboardOnScreenFocusNode;
bool enabled = true;
#override
void initState() {
super.initState();
_controller = new TextEditingController();
_focusNode = new FocusNode();
_keepKeyboardOnScreenFocusNode = new FocusNode();
}
#override
void dispose() {
_controller.dispose();
_focusNode.dispose();
_keepKeyboardOnScreenFocusNode.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) => CupertinoApp(
home: CupertinoPageScaffold(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
CupertinoTextField(
controller: _controller,
focusNode: _focusNode,
enabled: enabled,
),
KeepKeyboardOnScreen(focusNode: _keepKeyboardOnScreenFocusNode),
CupertinoButton(
onPressed: () {
setState(() {
enabled = true;
});
FocusScope.of(context).requestFocus(_focusNode);
},
child: Text("Enable", textAlign: TextAlign.center)),
CupertinoButton(
onPressed: () {
setState(() {
enabled = false;
});
FocusScope.of(context)
.requestFocus(_keepKeyboardOnScreenFocusNode);
},
child: Text("Disable", textAlign: TextAlign.center)),
],
),
),
),
);
}
void main() async {
runApp(Page());
}
See also: Flutter Issue #45076 Add high-level documentation and examples on managing keyboard focus.
Related
I have a stateful widget like this:
class _CustomTextField extends StatefulWidget {
const _CustomTextField();
#override
State<ConsumerStatefulWidget> createState() =>
__CustomTextFieldState();
}
class __CustomTextFieldState extends State<_CustomTextField> {
late final FocusNode focusNode;
#override
void initState() {
super.initState();
focusNode = FocusNode()..addListener(() {
setState(() {});
});
}
#override
void dispose() {
focusNode.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Row(
color: focusNode.hasFocus ? const Color(0xFFF5F5F5) : null,
children: [
if(focusNode.hasFocus)
Icon(Icons.edit),
TextField(
focusNode: focusNode,
),
]
);
}
}
where I want to change the widget when the textfield gets selected.
My problem is, that with the current implementation, the whole widget gets rebuilt, causing the Keyboard to disappear immediately after selecting the textfield.
What is the best way to handle this?
I'd like to create a screen in which many TextFIelds lined up vertically, and when the Enter key is pressed while editing the bottom TextField, a new TextField is added below it moving the focus too. I created a demo app referring to the example in the docs of FocusNode and it works basically but the keyboard bounces when moving the focus to a newly created TextField (see the gif below). How can I fix this unwanted behavior?
The gif of the demo app
The code of the demo app is here:
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
static const String _title = 'Flutter Code Sample';
#override
Widget build(BuildContext context) {
return const MaterialApp(
title: _title,
home: MyStatefulWidget(),
);
}
}
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({super.key});
#override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
int focusedChild = 0;
List<Widget> children = <Widget>[];
List<FocusNode> childFocusNodes = <FocusNode>[];
#override
void initState() {
super.initState();
// Add the first child.
_addChild();
}
#override
void dispose() {
for (final FocusNode node in childFocusNodes) {
node.dispose();
}
super.dispose();
}
void _addChild() {
// Calling requestFocus here creates a deferred request for focus, since the
// node is not yet part of the focus tree.
childFocusNodes
.add(FocusNode(debugLabel: 'Child ${children.length}')..requestFocus());
children.add(
TextField(
focusNode: childFocusNodes.last,
textInputAction: TextInputAction.unspecified,
minLines: 1,
onSubmitted: (value) {
setState(() {
focusedChild = children.length;
_addChild();
});
},
),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: SingleChildScrollView(
child: Column(
children: children,
),
),
),
);
}
}
Screenshot
You need to use TextInputAction
TextInputAction.next: Moves the focus to the next focusable item.
TextInputAction.done: To close the keyboard.
class TestWidget extends StatelessWidget {
const TestWidget({super.key});
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: const [
TextField(
decoration: InputDecoration(hintText: 'TextField #1 with next'),
textInputAction: TextInputAction.next, // Moves the focus to the next focusable item.
),
TextField(
decoration: InputDecoration(hintText: 'TextField #2 with next'),
textInputAction: TextInputAction.next, // Moves the focus to the next focusable item.
),
TextField(
decoration: InputDecoration(hintText: 'TextField #3 with done'),
textInputAction: TextInputAction.done, // Close the keyboard.
),
],
),
),
);
}
}
Update
Create a new text field on editing complete and change focus to the new one.
You need to use onEditingComplete function, instead of onSubmitted. Because onEditingComplete will not close (dismiss) the keyboard.
I rewrite your code, and remove generating a list of widgets. And replaced it with TextEditingController because it's a bad experience to keep UI (widgets) in a variable. So text fields generates by TextEditingController length.
I remove a list of FocusNode, and use FocusScope.of(context).nextFocus() to change the focus.
After creating a new TextEditingController function call delayed nextFocus() with delay, to give time to recreate the new UI.
here is the full code:
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return const MaterialApp(home: MyStatefulWidget());
}
}
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({super.key});
#override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
final List<TextEditingController> _controllers = [
TextEditingController()
]; // with first controller
_addController() {
setState(() {
_controllers.add(TextEditingController());
});
Future.delayed(const Duration(milliseconds: 100), () {
// Add delay for recreate UI after setState
FocusScope.of(context).nextFocus();
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: SingleChildScrollView(
child: Column(
children: _controllers
.map((e) => TextField(
textInputAction: TextInputAction.next,
controller: e,
onEditingComplete: _addController,
))
.toList(),
),
),
),
);
}
}
I am building a chat screen and trying to get messages from textEditController instead of getting them from the message model
Code below:
TextEditingController _controller =
new TextEditingController();
String _text = '';
....
IconButton(
icon: Icon(Icons.send),
iconSize: 25.0,
color: Theme.of(context).primaryColor,
onPressed: () async {
DataModel? data = await submitData(_controller);
_dataModel = data;
setState(
() {
_text = _controller.text.toString();
},
);
},
),
....
child: TextField(
controller: _controller,
textCapitalization: TextCapitalization.sentences,
onChanged: (value) {},
decoration: InputDecoration(hintText: 'Send a message...'),
),
What you need to do is to invoke the addListener function on your TextEditingController and do your actions on it. Here is an example:
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
#override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
late final TextEditingController _controller;
#override
void initState() {
_controller = TextEditingController();
_controller.addListener(() {
final text = _controller.text;
// do something with text here
});
super.initState();
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home Page'),
),
body: TextField(
controller: _controller,
),
);
}
}
I want the focus the focus on the material button so I can press enter or click the button an create an item
final FocusNode _createButtonFocusNode = new FocusNode();
#override
void initState() {
FocusScope.of(context).requestFocus(_createButtonFocusNode);
super.initState();
}
RawKeyboardListener(
focusNode: _createButtonFocusNode,
onKey: (RawKeyEvent event) {
if (event.logicalKey == LogicalKeyboardKey.enter) {
_createItem();
}
},
child:RaisedButton(focusNode: _createButtonFocusNode,
onPressed: () {
_createItem();
},
child: Text("Create"))))
Assume also a cancel material button exists with a _cancelItem event that should be able to accept an enter key on focus
You can copy paste run full code below
You can use _node.requestFocus() to request focus and list keyboard event with FocusAttachment and attach
In demo code, when receive Enter will change button color, see working demo below
code snippet
_node.requestFocus();
...
FocusAttachment _nodeAttachment;
_nodeAttachment = _node.attach(context, onKey: _handleKeyPress);
...
bool _handleKeyPress(FocusNode node, RawKeyEvent event) {
if (event is RawKeyDownEvent) {
print('Focus node ${node.debugLabel} got key event: ${event.logicalKey}');
if (event.logicalKey == LogicalKeyboardKey.enter) {
print('clicked enter');
setState(() {
_color = Colors.deepPurple;
});
return true;
}
}
return false;
}
working demo
full code
// Flutter code sample for FocusNode
// This example shows how a FocusNode should be managed if not using the
// [Focus] or [FocusScope] widgets. See the [Focus] widget for a similar
// example using [Focus] and [FocusScope] widgets.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(MyApp());
/// This Widget is the main application widget.
class MyApp extends StatelessWidget {
static const String _title = 'Flutter Code Sample';
#override
Widget build(BuildContext context) {
return MaterialApp(
title: _title,
home: Scaffold(
appBar: AppBar(title: const Text(_title)),
body: MyStatelessWidget(),
),
);
}
}
class CustomButton extends StatefulWidget {
FocusNode focusNode;
CustomButton({Key key, this.focusNode}) : super(key: key);
#override
_CustomButtonState createState() => _CustomButtonState();
}
class _CustomButtonState extends State<CustomButton> {
bool _focused = false;
FocusAttachment _nodeAttachment;
Color _color = Colors.white;
#override
void initState() {
super.initState();
//widget.focusNode = FocusNode(debugLabel: 'Button');
widget.focusNode.addListener(_handleFocusChange);
_nodeAttachment = widget.focusNode.attach(context, onKey: _handleKeyPress);
}
void _handleFocusChange() {
print(widget.focusNode.hasFocus);
if (widget.focusNode.hasFocus != _focused) {
setState(() {
_focused = widget.focusNode.hasFocus;
_color = Colors.white;
});
}
}
bool _handleKeyPress(FocusNode node, RawKeyEvent event) {
if (event is RawKeyDownEvent) {
print('Focus node ${node.debugLabel} got key event: ${event.logicalKey}');
if (event.logicalKey == LogicalKeyboardKey.enter) {
print('clicked enter');
setState(() {
_color = Colors.deepPurple;
});
return true;
}
}
return false;
}
#override
void dispose() {
widget.focusNode.removeListener(_handleFocusChange);
// The attachment will automatically be detached in dispose().
widget.focusNode.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
_nodeAttachment.reparent();
return Center(
child: RaisedButton(
focusNode: widget.focusNode,
color: _focused ? _color : Colors.white,
child: Text(_focused ? "focused" : 'Not focus'),
onPressed: () {
print("create item");
},
),
);
}
}
class MyStatelessWidget extends StatefulWidget {
MyStatelessWidget({Key key}) : super(key: key);
#override
_MyStatelessWidgetState createState() => _MyStatelessWidgetState();
}
class _MyStatelessWidgetState extends State<MyStatelessWidget> {
FocusNode _node1 = FocusNode();
FocusNode _node2 = FocusNode();
#override
Widget build(BuildContext context) {
final TextTheme textTheme = Theme.of(context).textTheme;
return DefaultTextStyle(
style: textTheme.headline4,
child: Column(
children: [
CustomButton(
focusNode: _node1,
),
CustomButton(
focusNode: _node2,
),
RaisedButton(
onPressed: () {
_node1.requestFocus();
setState(() {});
},
child: Text("request focus button 1")),
RaisedButton(
onPressed: () {
_node2.requestFocus();
setState(() {});
},
child: Text("request focus button 2")),
],
),
);
}
}
If all you want is for the button to be focused by default, you can do that by just specifying autofocus:true on the button, and you don't even need to create a FocusNode:
class MyCustomWidget extends StatelessWidget {
const MyCustomWidget({Key? key}) : super(key: key);
void _createItem() {
print('Item created');
}
#override
Widget build(BuildContext context) {
return TextButton(
autofocus: true,
child: const Text('CREATE'),
onPressed: _createItem,
);
}
}
This will automatically focus the widget when first built, as long as something else doesn't have the focus already.
If you need to set the focus from another control, you can do that with a focus node, but you don't need to use a FocusAttachment (you rarely, if ever, need to use one of those), you can just pass it to the button and call requestFocus() on it.
class MyCustomWidget extends StatefulWidget {
const MyCustomWidget({Key? key}) : super(key: key);
#override
State<MyCustomWidget> createState() => _MyCustomWidgetState();
}
class _MyCustomWidgetState extends State<MyCustomWidget> {
late FocusNode _createButtonFocusNode;
#override
void initState() {
super.initState();
_createButtonFocusNode = FocusNode();
}
#override
void dispose() {
_createButtonFocusNode.dispose();
super.dispose();
}
void _createItem() {
print('Item created');
}
#override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextButton(
child: const Text('FOCUS OTHER BUTTON'),
onPressed: () => _createButtonFocusNode.requestFocus(),
),
TextButton(
focusNode: _createButtonFocusNode,
child: const Text('CREATE'),
onPressed: _createItem,
),
],
),
);
}
}
(When you do create a FocusNode, be sure to dispose of it properly.)
I don't need to do many things with TextEditingController but want to show the initial text. And I feel like creating StatefulWidget is too much for that.
Here's what I want my code looks like
// In StatelessWidget
TextField(
controller: TextEditingController(),
)
But every tutorials and blog posts I've seen use TextEditingController in StatefulWidget and dispose them in the dispose method. But I can't dispose them if I use like the above
If you want to use TextEditingController, there is no way around except to use a StatefulWidget if you want to avoid memory leaks.
However, if you see alot of boilerplate in this approach, you can use HookWidget (flutter_hooks) which gives you access to TextEditingController in a simple way and disposes it for you,here is a comparison:
using StatefulWidget:
class Test extends StatefulWidget {
#override
_TestState createState() => _TestState();
}
class _TestState extends State<Test> {
TextEditingController controller;
FocusNode focusNode;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
width: 200,
height: 200,
color: Colors.red,
child: TextField(
focusNode: focusNode,
controller: controller,
),
),
),
);
}
#override
void initState() {
controller = TextEditingController();
focusNode = FocusNode();
super.initState();
}
#override
void dispose() {
controller.dispose();
focusNode.dispose();
super.dispose();
}
}
using HookWidget:
class Test extends HookWidget {
#override
Widget build(BuildContext context) {
final focusNode = useFocusNode();
final controller = useTextEditingController();
return Scaffold(
body: Center(
child: Container(
width: 200,
height: 200,
color: Colors.red,
child: TextField(
focusNode: focusNode,
controller: controller,
),
),
),
);
}
}