Flutter - Handle key event only if there is no text entry event - flutter

I'm trying to implement some shortcuts on a desktop app. I've been looking into these links:
Understanding Flutter's focus system
Focus and text fields
Using Actions and Shortcuts
I would like to do an action when the user presses on the key a (for example).
class MyWidget extends StatefulWidget {
const MyWidget({Key? key}) : super(key: key);
#override
_TestState createState() => _TestState();
}
class _TestState extends State<Test> {
int count = 0;
KeyEventResult onKey(FocusNode node, RawKeyEvent event) {
if (event is RawKeyDownEvent && event.logicalKey == LogicalKeyboardKey.keyA) {
setState(() {
count++;
});
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Focus(
onKey: onKey,
child: Column(
children: [
Text('count: $count'),
const TextField(),
TextButton(
child: Text('button'),
onPressed: () {},
),
],
),
),
),
);
}
}
When the user clicks on a, the count increments.
The issue with this code is the user being unable to type a in the text field anymore because it is being handled by the focus node.
The first link states:
Key events start at the focus node with primary focus. If that node doesn’t return KeyEventResult.handled from its onKey handler, then its parent focus node is given the event. If the parent doesn’t handle it, it goes to its parent, and so on, until it reaches the root of the focus tree.
and
Focus key events are processed before text entry events, so handling a key event when the focus widget surrounds a text field prevents that key from being entered into the text field.
I would like my Focus widget to handle the key event only if the subtree didn't handle it itself including TextFields (and therefore text entry events).
I tried to always return KeyEventResult.ignored in the onKey method, but the OS triggers a sound meaning there is no action available every time the user clicks on a.
Is there a way to implement what I am trying to do? If yes, how?

You could wrap your text field in a focus that returns KeyEventResult.skipRemainingHandlers in onKeyEvent. This will prevent it affecting the other handlers while the text field is focused. Although this is inelegant and I'd love a correction with a better solution.
const unhandledKeys = [
LogicalKeyboardKey.delete,
LogicalKeyboardKey.backspace,
LogicalKeyboardKey.arrowUp,
LogicalKeyboardKey.arrowDown,
LogicalKeyboardKey.arrowLeft,
LogicalKeyboardKey.arrowRight
];
final focusNode = useFocusNode(onKeyEvent: (_a, _b) {
if (unhandledKeys.contains(event.logicalKey)) {
return KeyEventResult.ignored;
}
return KeyEventResult.skipRemainingHandlers;
});
return Focus(
focusNode: focusNode,
child: TextField()
);

Related

Flutter FocusNode onKeyEvent detect multiple keys

Using this for Flutter Desktop. I used to use onKey of FocusNode. The doc says it will be deprecated:
/// To receive key events that focuses on this node, pass a listener to `onKeyEvent`.
/// The `onKey` is a legacy API based on [RawKeyEvent] and will be deprecated
/// in the future.
This is how I used onKey for detecting shift + enter:
FocusNode(
onKey: (node, event)
{
if (event.isKeyPressed(LogicalKeyboardKey.enter))
{
if (event.isShiftPressed)
return KeyEventResult.ignored;
sendAction();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
},
How do you detect multiple keys being pressed at the same time with onKeyEvent?
Edit: I'm looking for a way to detect multiple keys being pressed WITHOUT the legacy approach. The event parameter in onKeyEvent is not
RawKeyEvent type, therefore isKeyPressed() is not available there. The only thing available is event.logicalKey.keyLabel which can't be used for multiple key detection.
To handle the shortcuts Flutter has Shortcut widget. To start using it you have to specify:
Intent - describes that shortcut intent triggered
Action - describes action on triggered Intent
Let's create an intent class
class NewLineIntent extends Intent {
const NewLineIntent();
}
Add it to your widget and specify the conditions:
static const newLine = SingleActivator(LogicalKeyboardKey.enter, shift: true);
Finally, build a widget:
#override
Widget build(BuildContext context) {
return Shortcuts(
shortcuts: const {
newLine: NewLineIntent()
},
child: Actions(
actions: {
NewLineIntent: CallbackAction<NewLineIntent>(
onInvoke: (NewLineIntent intent) {
print('New line');
},
)
},
child: Focus(
autofocus: true,
child: ... ,
),
),
);
}
And when your widget will appear on the screen and you press Shift + Enter your shortcut will be triggered.

Is there any way to generate the same key for the "same" widget in Flutter without having any values to be based on

I have a widget which can have multiple input sections from the same type. When I delete the first child then it behaves weirdly like showing still the old value from that first widget. I figured out I need to use keys for the children but then my UX gets broken. Let me show you some code snippet, please:
class _ParentState extends State<ParentWidget> {
...
#override
Widget build(BuildContext context) {
return Column(
children: stateList
.mapIndexed((index, element) => ChildWidget(key: UniqueKey(), input: element, onChanged: (text) {
setState(() {
stateList[index].text = text;
});
}))
.toList(),
);
}
}
class _ChildState extends State<ChildWidget> {
final ctrl = TextEditingController();
#override
void initState() {
super.initState();
ctrl.text = widget.input.text;
}
#override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
controller: ctrl,
onChanged: (value) {
widget.onChanged(value);
}
),
...
]
);
}
}
When I wrote the user experience is broken, I was meaning that whenever the user starts to type into the text field then the focus gets loosen character-by-character since the callback is invoked and then generates a new child widget from the state because it has now "different" key... How can I refactor this? I just need a key mechanism which results the same for a child while it is not disposed yet!?
TL;DR
Solution:
remove key
by default, flutter is smart enough to determine which widget is need to rebuild or not. Thats why, key is very rarely used.
ChildWidget(input: element,
onChanged: (text) {
setState(() {
....
When you set UniquieKey() you widget will have different key every time build method called.
which is: everytime you call SetState() your apps will build NEW widget.
if you want to set key, you can use index value
ChildWidget(
key: ValueKey('$index'),
input: element,
onChanged: (text) {
what caused your "user experience broken".
it because, in your onChange function, you call setState() and also you set UniqueKey to your widget. So.. when setState() has called, it will re-execute build method, and since your ChildWidget has Unique key, it will rebuild the widget.
every time user typing, ChildWidget is rebuild. that caused lost focus

Flutter : my custom Switch widget does not respond to state change

I made a custom SwitchTile widget to avoid code duplication, which is based on a StatefulWidget with a boolean value so this is quiet simple :
class SwitchTile extends StatefulWidget
{
final String title;
final bool value;
final ValueChanged<bool> onChanged;
const SwitchTile({Key? key, required this.title, required this.value, required this.onChanged}) : super(key: key);
#override
State<StatefulWidget> createState() => _SwitchTileState();
}
class _SwitchTileState extends State<SwitchTile>
{
late bool value;
#override
void initState()
{
super.initState();
this.value = widget.value;
}
#override
Widget build(BuildContext context)
{
return ListTile(
title: Text(widget.title),
trailing: Switch.adaptive(
value: this.value,
onChanged: (newValue) {
widget.onChanged(newValue);
this.setState(() => this.value = newValue);
}
)
);
}
#override
void dispose()
{
super.dispose();
}
}
Now I want to synchronize some SwitchTile widget with another boolean value, but when the value changes it does not rebuild as I expected. I use flutter_bloc, the bloc/events/states work very well. See what I tried to do :
// See my Widget build() method ; bloc variable is declared above
BlocBuilder<ChadsVascBloc, ChadsVascState>(
builder: (BuildContext context, ChadsVascState state)
{
return Column(
children: [
// this works very well
ListTile(
title: Text("Switch : ageOver65 is ${state.ageOver65.toString()}"),
trailing: Switch.adaptive(
value: state.ageOver65,
onChanged: (value) => bloc.add(AgeOver65Toggled(ageOver65: value))
)
),
// this is does not
SwitchTile(
// the change is well triggered in the title property !!!
title: "SwitchTile : ageOver65 is ${state.ageOver65.toString()}",
value: state.ageOver65, // but this does not change the button's status
onChanged: (value) => bloc.add(AgeOver65Toggled(ageOver65: value))
)
]
);
}
),
SwitchTile(
title: AppLocalizations.of(context)!.ageOver65,
value: bloc.state.ageOver65,
onChanged: (value) => bloc.add(AgeOver65Toggled(ageOver65: value)),
)
When I push the last button, the first one work very well ("base" Switch widget) but the second one (custom SwitchTile widget) does not : the title changes but not the button's position.
What did I do wrong ?
So basically, you use the same event twice, that's why the bloc doesn't recognize any changes because you add the same event even it has the different value.
And i think you use the Bloc in the wrong way. Unlike provider and getx, main objective of Bloc is deliver the context with state. It was never meant to save some object inside it for so long.
For your case,
I assume you are trying to edit some kind of object. Use Bloc event to transfer the related object into another screen, in edit screen, Bloc will listen the state and parsing it to the local variabel that was assigned to the Switch (use BlocConsumer, listener to set state)
if u want to change the value of the variabel, go for it, and confirm it by some trigger like button then add another event to finish the edit
The problem seems to be : how to update properly a custom Switch widget ?
It does not seem to be about Bloc, because when I do that :
BlocBuilder<ChadsVascBloc, ChadsVascState>(
builder: (context, state) => SwitchTile(
title: "AgeOver65 is ${state.ageOver65.toString()}",
value: state.ageOver65,
onChanged: null
),
)
If I trigger a state change, ${state.ageOver65.toString()} changes but not the value of the Switch.
Solved : my error was not using BLoC but with bad state management structure. As a Switch widget does not have to manage its own state, my custom Switch widget should not try to changes its own state.
So it can be a simple StatelessWidget but used inside a parent StatefulWidget, or - I my case - in a BlocBuilder. I finally use a simple context.watch() as the value of my custom widget.

How to add Zero Width Character

After reviewing this article from this Flutter issue (regarding to identify when back key is pressed to work with a pin alike widget)
I've been trying to add the \u200b special character as suggested in the article, but it is always taking it as a String with of length 6.
How can I work with it to place a \u200b Unicode character in the TextEditingController and make it work?
This is what the article suggests:
Take whatever content you want in your TextEditingController and place a \u200b unicode character at the front of that text. When the user deletes all of your content, the text field looks empty, but there is single remaining \u200b character. When the user presses “delete” one more time, your TextEditingController receives a change event, and the text in your TextEditingController goes from a length of 1 to a length of 0. Now you know that the user pressed “delete” in an “empty” text field and you can take whatever action you want as a developer.
For adding the zero-width-space to the TextEditingController it was enough to provide "\u200b" in the initializer and when updating the value of the controller.
As for how to make this detect delete presses, you can try the code below, it have a callback onEmptyTextField which triggers when the TextEditingController's text turns empty or already is empty.
import 'package:flutter/material.dart';
const zeroWidthSpace = "\u200b";
class DetectDeleteTextField extends StatelessWidget {
const DetectDeleteTextField({
Key? key,
required this.onEmptyTextField,
required this.controller,
}) : super(key: key);
/// Triggered when the TextField turns/is empty, such as when the zero-width-space ("\u200b")
/// or another character is deleted, or delete is pressed repeatedly.
final Function() onEmptyTextField;
final TextEditingController controller;
#override
Widget build(BuildContext context) {
return TextField(
controller: controller,
onChanged: (text) {
print("onChanged - Length: ${text.length}, Text runes:${text.runes.toString()}");
// Remove the zero-width-space character, handle cursor position
// correctly, and ensure no zero-width-space is present if text is pasted.
if (text.length >= 2 && text.contains(zeroWidthSpace)) {
String trimmedText = text.replaceAll(zeroWidthSpace, "");
controller.value = TextEditingValue(
text: trimmedText,
selection: TextSelection.fromPosition(TextPosition(offset: trimmedText.length)),
);
print("Trimming - Length: ${controller.text.length}, Text runes:${controller.text.runes.toString()}");
}
// Trigger the callback and reset the text to the zero-width-space character.
// The selection is needed for detecting backspace when only the
// zero-width-space exist in the text.
if (text.isEmpty) {
onEmptyTextField();
controller.value = const TextEditingValue(text: zeroWidthSpace);
controller.selection = const TextSelection(baseOffset: 0, extentOffset: 1);
print("After delete - Length: ${controller.text.length}, Text runes:${controller.text.runes.toString()}");
}
},
);
}
}
class App extends StatelessWidget {
const App({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
DetectDeleteTextField(
controller: TextEditingController(text: zeroWidthSpace),
onEmptyTextField: () => print("Delete triggered!")
),
],
),
),
);
}
}
void main() => runApp(const App());
Tested on Android and Windows.

How to listen to keyboard on screen Flutter?

I am building a mobile app, I want to remove a widget when the keyboard appears on the screen, i.e when the input text field is on focus.
I have tried to use RawKeyboardListener but that doesn't seem to work, my code is as below:
new Container(
child: new RawKeyboardListener(
focusNode: new FocusNode(),
onKey: (input) => debugPrint("*****KEY PRESSED"),
child: new TextField(
controller: new TextEditingController(),
),
),
);
You can use this simple check:
MediaQuery.of(context).viewInsets.bottom == 0
The keyboard is closed when this returns true, otherwise it's open.
Be aware to take the context of the whole screen (Scaffold for example) and not only from one widget.
This is how you integrate that check to your code:
Visibility(
child: Icon(Icons.add),
visible: MediaQuery.of(context).viewInsets.bottom == 0,
)
The keyboard will automatically appear when the text field is focused. So you can add a listner to the focusnode to listen the focus change and hide respective widget.
Example:
void _listener(){
if(_myNode.hasFocus){
// keyboard appeared
}else{
// keyboard dismissed
}
}
FocusNode _myNode = new FocusNode()..addListener(_listner);
TextField _myTextField = new TextField(
focusNode: _mynNode,
...
...
);
new Container(
child: _myTextField
);
I used the package keyboard_visibility
Then I wrapped my TextField with a KeyboardListener implemented as follows:
class KeyboardListener extends StatefulWidget {
final Widget child;
final void Function(bool) onChange;
KeyboardListener({#required this.child, #required this.onChange});
#override
_KeyboardListenerState createState() => _KeyboardListenerState();
}
class _KeyboardListenerState extends State<KeyboardListener> {
int _sId;
KeyboardVisibilityNotification _kvn;
#override
void initState() {
super.initState();
_kvn = KeyboardVisibilityNotification();
_sId = _kvn.addNewListener(
onChange: widget.onChange,
);
}
#override
Widget build(BuildContext context) {
return widget.child;
}
#override
void dispose() {
_kvn.removeListener(_sId);
super.dispose();
}
}
You can use this library keyboard_visibility: ^0.5.6 at :
https://pub.dev/packages/keyboard_visibility
For execute your code, insert this in the initState()
KeyboardVisibilityNotification.addNewListener(
onChange: (bool visible) {
print(visible);
this.setState(() {
keyboardIsOpen = visible;
});
},
);
Whenever keyboard is open or closed, the library calls onChange method with the visibility boolean.
A widget that calls a callback whenever the user presses or releases a key on a keyboard.
A RawKeyboardListener is useful for listening to raw key events and hardware buttons that are represented as keys. Typically used by games and other apps that use keyboards for purposes other than text entry.
For text entry, consider using a EditableText, which integrates with on-screen keyboards and input method editors (IMEs).
const RawKeyboardListener({
Key key,
#required FocusNode focusNode,
#required ValueChanged<RawKeyEvent> onKey,
#required Widget child
})
Creates a widget that receives raw keyboard events.
For text entry, consider using a EditableText, which integrates with on-screen keyboards and input method editors (IMEs).
Implementation
const RawKeyboardListener({
Key key,
#required this.focusNode,
#required this.onKey,
#required this.child,
}) : assert(focusNode != null),
assert(child != null),
super(key: key);