Detect tap with pressed keyboard key - flutter

I am trying to implement a ListView for Desktop applications, which is able to multiselect the items. On Desktop we do this by either clicking an item, or clicking and holding the control key. To select an item, you simply can add an Inkwell or GestureRecognizer, but how do I detect on click that there is also the control key pressed? I couldn't find any suggestions

You can play with this widget. Make sure to run as desktop mode.
we need to listen keyboard event. For that I am using RawKeyboardListener.
keep track ctrl event
single selection happen on normal tap by clearing previous selected item, but while _isCTRLPressed don't clear the selected items
onTap: () {
if (!_isCTRLPressed) _selectedIndex.clear();
_onTap(index);
}
Demo widget
class ItemSelection extends StatefulWidget {
const ItemSelection({Key? key}) : super(key: key);
#override
State<ItemSelection> createState() => _ItemSelectionState();
}
class _ItemSelectionState extends State<ItemSelection> {
List<int> _selectedIndex = [];
void _onTap(index) {
if (_selectedIndex.contains(index)) {
_selectedIndex.remove(index);
} else {
_selectedIndex.add(index);
}
setState(() {});
}
final fc = FocusNode();
// you can use list for multi-purpose
bool _isCTRLPressed = false;
#override
Widget build(BuildContext context) {
return Scaffold(
body: RawKeyboardListener(
focusNode: fc,
autofocus: true,
onKey: (event) {
if (event.isKeyPressed(LogicalKeyboardKey.controlLeft)) {
if (event is RawKeyDownEvent) {
_isCTRLPressed = true;
}
} else {
_isCTRLPressed = false;
}
},
child: GridView.count(
crossAxisCount: 6,
mainAxisSpacing: 2,
crossAxisSpacing: 2,
children: List.generate(
55,
(index) => GestureDetector(
onTap: () {
if (!_isCTRLPressed) _selectedIndex.clear();
_onTap(index);
debugPrint("ctrlPressed $_isCTRLPressed");
},
child: Container(
color: _selectedIndex.contains(index)
? Colors.cyanAccent
: Colors.grey,
alignment: Alignment.center,
child: Text(index.toString()),
),
),
),
),
),
);
}
}

Related

Flutter keyboard stays open after changing screen

I have several TextFormField on a screen. If I tap one of the fields the keyboard opens as expected however if I then select a new screen from the Drawer menu the keyboard closes and as soon as the new screen finishes loading the keyboard automatically opens again. More than that if I type something the text field is updated in the background if I return to the screen with the TextFormField it shows the correct input.
I would expect the screen/widget to be disposed of when navigating to another screen(widget) from the navigation menu, and I definitely should not be able to update the content of a widget's text field while in another widget.
// Form Field
Form(key: _constructionFormKey,
child: Column(children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const SizedBox(
width: 100,
child: Text(
'Homes',
style: regularBoldText,
),
),
SizedBox(
width: 75,
child: Text(
'${widget.tribe.homes} (${calculatePercent(widget.tribe.land, widget.tribe.homes)}%)',
style: regularText,
),
),
SizedBox(
height: 18,
width: MediaQuery.of(context).size.width / 3,
child: TextFormField(
autovalidateMode:
AutovalidateMode.onUserInteraction,
onChanged: (String? newValue) {
if (newValue != null && isNumber(newValue)) {
setState(() {
buildHomes = int.parse(newValue);
});
// Requiered or variable will not clear properly
// when the user deletes input content
} else if (newValue == null || newValue.isEmpty) {
setState(() {
buildHomes = 0;
});
}
},
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.digitsOnly
],
style: const TextStyle(fontSize: 10),
decoration: const InputDecoration(
border: OutlineInputBorder()),
keyboardType: TextInputType.number,
),
),
],
),));
// Home Screen where I have the navigation logic.
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
#override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
//! Default to tribe overview screen - 1 -, 0 is mail screen
int _drawerNavIndex = 3;
/// [setSelectedTab] will update the current screen based on the tapped option
/// from [DrawerContentWidget]
void setSelectedTab(index) {
// if the [_drawerNavIndex] is not the same as [index] update it to [index]
// value
if (_drawerNavIndex != index) {
setState(() {
_drawerNavIndex = index;
});
}
}
/// [selectedTabContent] will return the screen selected from the
/// [DrawerContentWidget] based on [_drawerNavIndex]
Widget selectedTabContent() {
List<Widget> pages = [
// Tribe Screens
const TribeMailScreen(),
const TribeHomeScreen(),
const TribeAdvisorScreen(),
const ConstructionScreen()
// Alliance
];
return IndexedStack(
index: _drawerNavIndex,
children: pages,
);
}
#override
Widget build(BuildContext context) {
TribeSummary tribe = Provider.of<TribeSummary>(context, listen: true);
// If the tribe uid value is `placeHolderTribe` assume that there is no
// existing or active tribe for this account
if (tribe.uid == 'placeHolderTribe') {
return Scaffold(
/// TODO: create a proper drawer or appBar for the [StartTribeWidget]
appBar: AppBar(
title: const Text('Orkfia'),
),
body: const StartTribeWidget(),
);
// If the tribe `uid` value is `placeHolderTribe` assume that an error
// occurred while trying to get the tribe stream or while the tribe stream
// is parsed to [TribeSummary], log should give more information
} else if (tribe.uid == 'placeHolderErrorTribe') {
// TODO: create a bettter error screen for this situation
return const Center(
child: Text('Unable to retrieve tribe data'),
);
}
// This Scaffold wraps the entire app, anything here will be avilable
// globally
return Scaffold(
// App Bar
appBar: const AppBarContent(),
// [DrawerContentWidget] holds all the drawer content, it requires
// [selectedTab] function to handle the navigation between screens
drawer: DrawerContentWidget(
setSelectedTab: setSelectedTab,
selectedTabIndex: _drawerNavIndex,
),
// Display the contents of the selected screen
body: selectedTabContent(),
// Reserved
bottomNavigationBar: SizedBox(
height: 50,
child: Container(
color: Colors.red[100],
child: const Center(child: Text('Reserved space')),
)),
);
}
}
Use TextEditingController for every TextFormField to solve this problem.
A controller for an editable text field.
First Whenever the user modifies a text field with an associated TextEditingController, the text field updates value and the controller notifies its listeners. Listeners can then read the text and selection properties to learn what the user has typed or how the selection has been updated.
Second, remember to dispose of the TextEditingController inside dispose() when it is no longer needed. This will ensure we discard any resources used by the object.
To close keyboard from screen
you can use GesterDetector widget.
FocusManager.instance.primaryFocus?.unfocus();
or use can below for hot fix
FocusScope.of(context).unfocus();
Example is given below
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({super.key});
#override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
final TextEditingController _controller = TextEditingController();
#override
void initState() {
super.initState();
_controller.addListener(() {
final String text = _controller.text.toLowerCase();
_controller.value = _controller.value.copyWith(
text: text,
selection:
TextSelection(baseOffset: text.length, extentOffset: text.length),
composing: TextRange.empty,
);
});
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
alignment: Alignment.center,
padding: const EdgeInsets.all(6),
child: TextFormField(
controller: _controller,
decoration: const InputDecoration(border: OutlineInputBorder()),
),
),
);
}
}
When you navigate to a new page, you are not really disposing of the previous page. The new page is simply added on top of the previous page. You could try wrapping the entire scaffold in a GestureDetector with the following onTap function:
FocusScope.of(context).unfocus();
This will make sure the keyboard is dismissed when you push a new page with user taps.

How can I fix the focus on a ListView item in Flutter?

I have a listview that I want to enable shortcuts like Ctrl+c, Enter, etc this improves user experience.
The issue is after I click/tap on an item, it loses focus and the shortcut keys no longer work.
Is there a fix or a workaround for this?
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
void main() {
runApp(const MyApp());
}
class SomeIntent extends Intent {}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.orange,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return GetBuilder<Controller>(
init: Get.put(Controller()),
builder: (controller) {
final List<MyItemModel> myItemModelList = controller.myItemModelList;
return Scaffold(
appBar: AppBar(
title: RawKeyboardListener(
focusNode: FocusNode(),
onKey: (event) {
if (event.logicalKey.keyLabel == 'Arrow Down') {
FocusScope.of(context).nextFocus();
}
},
child: const TextField(
autofocus: true,
),
),
),
body: myItemModelList.isEmpty
? const Center(child: CircularProgressIndicator())
: ListView.builder(
itemBuilder: (context, index) {
final MyItemModel item = myItemModelList[index];
return Shortcuts(
shortcuts: {
LogicalKeySet(LogicalKeyboardKey.enter): SomeIntent(),
},
child: Actions(
actions: {
SomeIntent: CallbackAction<SomeIntent>(
// this will not launch if I manually focus on the item and press enter
onInvoke: (intent) => print(
'SomeIntent action was launched for item ${item.name}'),
)
},
child: InkWell(
focusColor: Colors.blue,
onTap: () {
print('clicked item $index');
controller.toggleIsSelected(item);
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
color: myItemModelList[index].isSelected
? Colors.green
: null,
height: 50,
child: ListTile(
title: Text(myItemModelList[index].name),
subtitle: Text(myItemModelList[index].detail),
),
),
),
),
),
);
},
itemCount: myItemModelList.length,
),
);
},
);
}
}
class Controller extends GetxController {
List<MyItemModel> myItemModelList = [];
#override
void onReady() {
myItemModelList = buildMyItemModelList(100);
update();
super.onReady();
}
List<MyItemModel> buildMyItemModelList(int count) {
return Iterable<MyItemModel>.generate(
count,
(index) {
return MyItemModel('$index - check debug console after pressing Enter.',
'$index - click me & press Enter... nothing happens\nfocus by pressing TAB/Arrow Keys and press Enter.');
},
).toList();
}
toggleIsSelected(MyItemModel item) {
for (var e in myItemModelList) {
if (e == item) {
e.isSelected = !e.isSelected;
}
}
update();
}
}
class MyItemModel {
final String name;
final String detail;
bool isSelected = false;
MyItemModel(this.name, this.detail);
}
Tested with Windows 10 and flutter 3.0.1
Using Get State manager.
In Flutter, a ListView or GridView containing a number of ListTile widgets, you may notice that the selection and the focus are separate. We also have the issue of tap() which ideally sets both the selection and the focus - but by default tap does nothing to affect focus or selection.
The the official demo of ListTile selected property https://api.flutter.dev/flutter/material/ListTile/selected.html
shows how we can manually implement a selected ListTile and get tap() to change the selected ListTile. But this does nothing for us in terms of synchronising focus.
Note: As that demo shows, tracking the selected ListTile needs to
be done manualy, by having e.g. a selectedIndex variable, then setting the
selected property of a ListTile to true if the index matches the
selectedIndex.
Here are a couple of solutions to the problem of to the syncronising focus, selected and tap in a listview.
Solution 1 (deprecated, not recommended):
The main problem is accessing focus behaviour - by default we have no access
to each ListTile's FocusNode.
UPDATE: Actually it turns out that there is a way to access a focusnode, and thus allocating our own focusnodes is not necessary - see Solution 2 below. You use the Focus widget with a child: Builder(builder: (BuildContext context) then you can access the focusnode with FocusScope.of(context).focusedChild. I am leaving this first solution here for study, but recommend solution 2 instead.
But by allocating a focus node for each ListTile item in the
ListView, we then do. You see, normally a ListTile item allocates its own focus
node, but that's bad for us because we want to access each focus node from
the outside. So we allocate the focus nodes ourselves and pass them to the
ListTile items as we build them, which means a ListTile no longer has to
allocate a FocusNode itself - note: this is not a hack - supplying custom
FocusNodes is supported in the ListTile API. We now get access to the
FocusNode object for each ListTile item, and
invoke its requestFocus()
method whenever selection changes.
we also listen in the FocusNode
objects for changes in focus, and update the selection whenever focus
changes.
The benefits of custom focus node which we supply ourselves to each ListTile
are:
We can access the focus node from outside the ListTile widget.
We can use the focus node to request focus.
We can listen to changes in focus.
BONUS: We can wire shortcuts directly into the focus node without the usual Flutter shortcut complexity.
This code synchronises selection, focus and tap behaviour, as well as supporting up and down arrow changing the selection.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
// Enhancements to the official ListTile 'selection' demo
// https://api.flutter.dev/flutter/material/ListTile/selected.html to
// incorporate Andy's enhancements to sync tap, focus and selected.
// This version includes up/down arrow key support.
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
static const String _title =
'Synchronising ListTile selection, focus and tap - with up/down arrow key support';
#override
Widget build(BuildContext context) {
return MaterialApp(
title: _title,
home: Scaffold(
appBar: AppBar(title: const Text(_title)),
body: const MyStatefulWidget(),
),
);
}
}
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({super.key});
#override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
int _selectedIndex = 0;
late List _focusNodes; // our custom focus nodes
void changeSelected(int index) {
setState(() {
_selectedIndex = index;
});
}
void changeFocus(int index) {
_focusNodes[index].requestFocus(); // this works!
}
// initstate
#override
void initState() {
super.initState();
_focusNodes = List.generate(
10,
(index) => FocusNode(onKeyEvent: (node, event) {
print(
'focusnode detected: ${event.logicalKey.keyLabel} ${event.runtimeType} $index ');
// The focus change that happens when the user presses TAB,
// SHIFT+TAB, UP and DOWN arrow keys happens on KeyDownEvent (not
// on the KeyUpEvent), so we ignore the KeyDownEvent and let
// Flutter do the focus change. That way we don't need to worry
// about programming manual focus change ourselves, say, via
// methods on the focus nodes, which would be an unecessary
// duplication.
//
// Once the focus change has happened naturally, all we need to do
// is to change our selected state variable (which we are manually
// managing) to the new item position (where the focus is now) -
// we can do this in the KeyUpEvent. The index of the KeyUpEvent
// event will be item we just moved focus to (the KeyDownEvent
// supplies the old item index and luckily the corresponding
// KeyUpEvent supplies the new item index - where the focus has
// just moved to), so we simply set the selected state value to
// that index.
if (event.runtimeType == KeyUpEvent &&
(event.logicalKey == LogicalKeyboardKey.arrowUp ||
event.logicalKey == LogicalKeyboardKey.arrowDown ||
event.logicalKey == LogicalKeyboardKey.tab)) {
changeSelected(index);
}
return KeyEventResult.ignored;
}));
}
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: 10,
itemBuilder: (BuildContext context, int index) {
return ListTile(
focusNode: _focusNodes[
index], // allocate our custom focus node for each item
title: Text('Item $index'),
selected: index == _selectedIndex,
onTap: () {
changeSelected(index);
changeFocus(index);
},
);
},
);
}
}
Important Note: The above solution doesn't work when changing the number of items, because all the focusnodes are allocated during initState which only gets called once. For example if the number of items increases then there are not enough focusnodes to go around and the build step will crash.
The next solution (below) does not explicitly allocate focusnodes and is a more robust solution which supports rebuilding and adding and removing items dynamically.
Solution 2 (allows rebuilds, recommended)
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'dart:developer' as developer;
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
static const String _title = 'Flutter selectable listview - solution 2';
#override
Widget build(BuildContext context) {
return const MaterialApp(
title: _title,
home: HomeWidget(),
);
}
}
// ╦ ╦┌─┐┌┬┐┌─┐╦ ╦┬┌┬┐┌─┐┌─┐┌┬┐
// ╠═╣│ ││││├┤ ║║║│ │││ ┬├┤ │
// ╩ ╩└─┘┴ ┴└─┘╚╩╝┴─┴┘└─┘└─┘ ┴
class HomeWidget extends StatefulWidget {
const HomeWidget({super.key});
#override
State<HomeWidget> createState() => _HomeWidgetState();
}
class _HomeWidgetState extends State<HomeWidget> {
// generate a list of 10 string items
List<String> _items = List<String>.generate(10, (int index) => 'Item $index');
String currentItem = '';
int currentIndex = 0;
int redrawTrigger = 0;
// clear items method inside setstate
void _clearItems() {
setState(() {
currentItem = '';
_items.clear();
});
}
// add items method inside setstate
void _rebuildItems() {
setState(() {
currentItem = '';
_items.clear();
_items.addAll(List<String>.generate(5, (int index) => 'Item $index'));
});
}
// set currentItem method inside setstate
void _setCurrentItem(String item) {
setState(() {
currentItem = item;
currentIndex = _items.indexOf(item);
});
}
// set currentindex method inside setstate
void _setCurrentIndex(int index) {
setState(() {
currentIndex = index;
if (index < 0 || index >= _items.length) {
currentItem = '';
} else {
currentItem = _items[index];
}
});
}
// delete current index method inside setstate
void _deleteCurrentIndex() {
// ensure that the index is valid
if (currentIndex >= 0 && currentIndex < _items.length) {
setState(() {
String removedValue = _items.removeAt(currentIndex);
if (removedValue.isNotEmpty) {
print('Item index $currentIndex deleted, which was $removedValue');
// calculate new focused index, if have deleted the last item
int newFocusedIndex = currentIndex;
if (newFocusedIndex >= _items.length) {
newFocusedIndex = _items.length - 1;
}
_setCurrentIndex(newFocusedIndex);
print('setting new newFocusedIndex to $newFocusedIndex');
} else {
print('Failed to remove $currentIndex');
}
});
} else {
print('Index $currentIndex is out of range');
}
}
#override
Widget build(BuildContext context) {
// print the current time
print('HomeView build at ${DateTime.now()} $_items');
return Scaffold(
body: Column(
children: [
// display currentItem
Text(currentItem),
Text(currentIndex.toString()),
ElevatedButton(
child: Text("Force Draw"),
onPressed: () => setState(() {
redrawTrigger = redrawTrigger + 1;
}),
),
ElevatedButton(
onPressed: () {
_setCurrentItem('Item 0');
redrawTrigger = redrawTrigger + 1;
},
child: const Text('Set to Item 0'),
),
ElevatedButton(
onPressed: () {
_setCurrentIndex(1);
redrawTrigger = redrawTrigger + 1;
},
child: const Text('Set to index 1'),
),
// button to clear items
ElevatedButton(
onPressed: _clearItems,
child: const Text('Clear Items'),
),
// button to add items
ElevatedButton(
onPressed: _rebuildItems,
child: const Text('Rebuild Items'),
),
// button to delete current item
ElevatedButton(
onPressed: _deleteCurrentIndex,
child: const Text('Delete Current Item'),
),
Expanded(
key: ValueKey('${_items.length} $redrawTrigger'),
child: ListView.builder(
itemBuilder: (BuildContext context, int index) {
// print(' building listview index $index');
return FocusableText(
_items[index],
autofocus: index == currentIndex,
updateCurrentItemParentCallback: _setCurrentItem,
deleteCurrentItemParentCallback: _deleteCurrentIndex,
);
},
itemCount: _items.length,
),
),
],
),
);
}
}
// ╔═╗┌─┐┌─┐┬ ┬┌─┐┌─┐┌┐ ┬ ┌─┐╔╦╗┌─┐─┐ ┬┌┬┐
// ╠╣ │ ││ │ │└─┐├─┤├┴┐│ ├┤ ║ ├┤ ┌┴┬┘ │
// ╚ └─┘└─┘└─┘└─┘┴ ┴└─┘┴─┘└─┘ ╩ └─┘┴ └─ ┴
class FocusableText extends StatelessWidget {
const FocusableText(
this.data, {
super.key,
required this.autofocus,
required this.updateCurrentItemParentCallback,
required this.deleteCurrentItemParentCallback,
});
/// The string to display as the text for this widget.
final String data;
/// Whether or not to focus this widget initially if nothing else is focused.
final bool autofocus;
final updateCurrentItemParentCallback;
final deleteCurrentItemParentCallback;
#override
Widget build(BuildContext context) {
return CallbackShortcuts(
bindings: {
const SingleActivator(LogicalKeyboardKey.keyX): () {
print('X pressed - attempting to delete $data');
deleteCurrentItemParentCallback();
},
},
child: Focus(
autofocus: autofocus,
onFocusChange: (value) {
print(
'$data onFocusChange ${FocusScope.of(context).focusedChild}: $value');
if (value) {
updateCurrentItemParentCallback(data);
}
},
child: Builder(builder: (BuildContext context) {
// The contents of this Builder are being made focusable. It is inside
// of a Builder because the builder provides the correct context
// variable for Focus.of() to be able to find the Focus widget that is
// the Builder's parent. Without the builder, the context variable used
// would be the one given the FocusableText build function, and that
// would start looking for a Focus widget ancestor of the FocusableText
// instead of finding the one inside of its build function.
developer.log('build $data', name: '${Focus.of(context)}');
return GestureDetector(
onTap: () {
Focus.of(context).requestFocus();
// don't call updateParentCallback('data') here, it will be called by onFocusChange
},
child: ListTile(
leading: Icon(Icons.map),
selectedColor: Colors.red,
selected: Focus.of(context).hasPrimaryFocus,
title: Text(data),
),
);
}),
),
);
}
}
Edit:
this works to regain focus, however, the focus starts again from the top widget and not from the widget that was clicked on. I hope this answer still helps
Edit 2 I found a solution, you'll have to create a separate FocusNode() for each element on your listview() and requestFocus() on that in your inkwell. Complete updated working example (use this one, not the one in the original answer):
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class SomeIntent extends Intent {}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.orange,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
MyHomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final myItemModelList = List.generate(10, (index) => Text('${index + 1}'));
final _focusNodes = List.generate(myItemModelList.length, (index) => FocusNode());
return Scaffold(
appBar: AppBar(),
body: myItemModelList.isEmpty
? const Center(child: CircularProgressIndicator())
: ListView.builder(
itemBuilder: (context, index) {
final item = myItemModelList[index];
return RawKeyboardListener(
focusNode: _focusNodes[index],
onKey: (event) {
if (event.logicalKey.keyLabel == 'Arrow Down') {
FocusScope.of(context).nextFocus();
}
},
child: Actions(
actions: {
SomeIntent: CallbackAction<SomeIntent>(
// this will not launch if I manually focus on the item and press enter
onInvoke: (intent) => print(
'SomeIntent action was launched for item ${item}'),
)
},
child: InkWell(
focusColor: Colors.blue,
onTap: () {
_focusNodes[index].requestFocus();
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
color: Colors.blue,
height: 50,
child: ListTile(
title: myItemModelList[index],
subtitle: myItemModelList[index]),
),
),
),
),
);
},
itemCount: myItemModelList.length,
),
);
}
}
Edit 3:
To also detect the up key you can try:
onKey: (event) {
if (event.isKeyPressed(LogicalKeyboardKey.arrowDown)) {
FocusScope.of(context).nextFocus();
} else if (event.isKeyPressed(LogicalKeyboardKey.arrowUp)) {
FocusScope.of(context).previousFocus();
}
},
Original answer (you should still read to understand the complete answer).
First of all, your adding RawKeyboardListener() within your appBar() don't do that, instead add it to the Scaffold().
Now, create a FocusNode() outside of your Build method:
class MyHomePage extends StatelessWidget {
MyHomePage({Key? key}) : super(key: key);
final _focusNode = FocusNode();
#override
Widget build(BuildContext context) {}
...
...
And assing the _focusNode to the RawKeyboardListener():
RawKeyboardListener(focusNode: _focusNode,
...
And here's the key point. Since you don't want to lose focus in the ListView(), in the onTap of your inkWell you'll have to request focus again:
InkWell(
focusColor: Colors.blue,
onTap: () {
_focusNode.requestFocus();
print('clicked item $index');
},
...
That's it.
Here is a complete working example based on your code. (I needed to modify some things, since I don't have all your data):
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
runApp(const MyApp());
}
class SomeIntent extends Intent {}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.orange,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
MyHomePage({Key? key}) : super(key: key);
final _focusNode = FocusNode();
#override
Widget build(BuildContext context) {
final myItemModelList = List.generate(10, (index) => Text('${index + 1}'));
return Scaffold(
appBar: AppBar(),
body: myItemModelList.isEmpty
? const Center(child: CircularProgressIndicator())
: RawKeyboardListener(
focusNode: _focusNode,
onKey: (event) {
if (event.logicalKey.keyLabel == 'Arrow Down') {
FocusScope.of(context).nextFocus();
}
},
child: ListView.builder(
itemBuilder: (context, index) {
final item = myItemModelList[index];
return Shortcuts(
shortcuts: {
LogicalKeySet(LogicalKeyboardKey.enter): SomeIntent(),
},
child: Actions(
actions: {
SomeIntent: CallbackAction<SomeIntent>(
// this will not launch if I manually focus on the item and press enter
onInvoke: (intent) => print(
'SomeIntent action was launched for item ${item}'),
)
},
child: InkWell(
focusColor: Colors.blue,
onTap: () {
_focusNode.requestFocus();
print('clicked item $index');
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
color: Colors.blue,
height: 50,
child: ListTile(
title: myItemModelList[index],
subtitle: myItemModelList[index]),
),
),
),
),
);
},
itemCount: myItemModelList.length,
),
),
);
}
}
Demo:

Flutter, ListView.builder onTap strange behavior

On press key 1, ListView adds 1 tile, on press key 2 ListView removes one tile, though after clicking with mouse outside of ListView or Text() widget, keyboard keys stop responding without any error being shown in terminal.
I thought, that maybe FocusNode was disposed after clicking outside of ListView, though, after testing, this seems not to be the case
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class OnTapWidgetIssue extends StatefulWidget {
OnTapWidgetIssue({Key? key}) : super(key: key);
String testOnTap = '';
int nOfList = 1;
#override
_OnTapWidgetIssueState createState() => _OnTapWidgetIssueState();
}
class _OnTapWidgetIssueState extends State<OnTapWidgetIssue> {
final FocusNode _focusNode = FocusNode();
#override
void dispose() {
_focusNode.dispose();
print('_focusNode.dispose()');
super.dispose();
}
void _handleKeyEvent(RawKeyEvent event) {
if (event is RawKeyDownEvent &&
event.data.logicalKey == LogicalKeyboardKey.digit1) {
widget.nOfList += 1;
setState(() {});
}
if (event is RawKeyDownEvent &&
event.data.logicalKey == LogicalKeyboardKey.digit2) {
if (widget.nOfList > 1) {
widget.nOfList--;
setState(() {});
} else {}
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
drawer: MenuDrawer(),
appBar: AppBar(title: Text('OnTap-widget.Issue')),
body: RawKeyboardListener(
autofocus: true,
focusNode: _focusNode, // <-- more magic
onKey: _handleKeyEvent,
child: Column(children: [
Text(widget.testOnTap, style: TextStyle(fontSize: 52.0)),
Text('''
press 1 to add ListTile
press 2 to remove ListTile
'''),
Expanded(
child: Row(
children: [
Expanded(
flex: 2,
child: SizedBox(),
),
Expanded(
flex: 1,
// child: SizedBox(),
// // ),
child: ListView.builder(
itemCount: widget.nOfList,
// itemCount: widget.testOnTap.length,
itemBuilder: (_, i) {
return ListTile(
title: Text('$i'),
onTap: () {
widget.testOnTap = widget.testOnTap + i.toString();
setState(() {});
},
// Handle your onTap here.
);
},
),
),
Expanded(
flex: 2,
child: SizedBox(),
),
],
),
),
]),
),
);
}
}
Also Im getting error when clicking to go to new page in the app
Error: A FocusNode was used after being disposed.
Once you have called dispose() on a FocusNode, it can no longer be used.
at Object.throw_ [as throw] (http://localhost:49535/dart_sdk.js:5061:11)
at http://localhost:49535/packages/flutter/src/foundation/change_notifier.dart.lib.js:66:21
at focus_manager.FocusNode.new.[_debugAssertNotDisposed] (http://localhost:49535/packages/flutter/src/foundation/change_notifier.dart.lib.js:69:25)
at focus_manager.FocusNode.new.notifyListeners (http://localhost:49535/packages/flutter/src/foundation/change_notifier.dart.lib.js:131:41)
at focus_manager.FocusNode.new.[_notify] (http://localhost:49535/packages/flutter/src/widgets/widget_inspector.dart.lib.js:42893:12)
at focus_manager.FocusManager.new.[_applyFocusChange] (http://localhost:49535/packages/flutter/src/widgets/widget_inspector.dart.lib.js:43665:26)
at Object._microtaskLoop (http://localhost:49535/dart_sdk.js:38778:13)
at _startMicrotaskLoop (http://localhost:49535/dart_sdk.js:38784:13)
at http://localhost:49535/dart_sdk.js:34519:9
How ever, I don't get this error when selecting exercise page in drawer menu, only when going to this new page from home page. Exercise and Home pages are kinda similar, but still different in some aspects.
Thank
Technically, you are not adding the onTap to the ListView.builder, you're adding it to every single ListTile added by the builder. :)
Declare your two state variables:
String testOnTap = '';
int nOfList = 1;
inside the _OnTapWidgetIssueState class, not the OnTapWidgetIssue class. The convention is to name them _testOnTap and _nOfList respectively since they are private to the class.
And update the two variables INSIDE the setState call, not outside it.

Can't update list with setState() in Flutter

I have a list of objects that I can display in a ListView. Now I wanted to implement a search feature and only display the search result. When I try to do it using onChanged on TextField(or even Controller) it doesn't work. I tried to debug and he gets the list updated correctly but he doesn't update the Widget. But when I removed the onChanged and added a button and then called the same method that I was calling on onChanged everything worked.
The goal is to update the widget as the user writes in the text field.
I would be happy to get some help
My full code :
import 'package:flutter/material.dart';
import 'package:hello_fridge/single_ingredient_icon.dart';
import 'package:string_similarity/string_similarity.dart';
import 'entities/ingredient.dart';
class IngredientsContainer extends StatefulWidget {
const IngredientsContainer({Key? key}) : super(key: key);
#override
_IngredientsContainerState createState() => _IngredientsContainerState();
}
class _IngredientsContainerState extends State<IngredientsContainer> {
late List<Ingredient> ingredients;
final searchController = TextEditingController();
#override
void dispose() {
// Clean up the controller when the widget is disposed.
searchController.dispose();
super.dispose();
}
void updateResults(String newValue) {
if (newValue.isEmpty) {
ingredients = Ingredient.getDummyIngredients();
} else {
print("new Value = $newValue");
ingredients = this.ingredients.where((ing) {
double similarity =
StringSimilarity.compareTwoStrings(ing.name, newValue);
print("$similarity for ${ing.name}");
return similarity > 0.2;
}).toList();
ingredients.forEach((element) {
print("found ${element.name}");
});
}
setState(() {});
}
Widget _searchBar(List<Ingredient> ingredients) {
return Row(
children: <Widget>[
IconButton(
splashColor: Colors.grey,
icon: Icon(Icons.restaurant),
onPressed: null,
),
Expanded(
child: TextField(
controller: searchController,
onChanged: (newValue) {
updateResults(newValue);
},
cursorColor: Colors.black,
keyboardType: TextInputType.text,
textInputAction: TextInputAction.go,
decoration: InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 15),
hintText: "Search..."),
),
),
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: IconButton(
icon: Icon(
Icons.search,
color: Color(0xff9ccc65),
),
onPressed: () {
updateResults(searchController.text);
},
),
),
],
);
}
#override
void initState() {
this.ingredients = Ingredient.getDummyIngredients();
super.initState();
}
#override
Widget build(BuildContext context) {
return Material(
child: Column(children: [
Expanded(flex: 1, child: _searchBar(this.ingredients)),
Expanded(flex: 4, child: IngredientsGrid(this.ingredients))
]),
);
}
}
class IngredientsGrid extends StatelessWidget {
List<Ingredient> ingredients;
IngredientsGrid(this.ingredients);
List<Widget> _buildIngredients() {
return this.ingredients.map((ing) => SingleIngredientIcon(ing)).toList();
}
// const IngredientsGrid({
// Key? key,
// }) : super(key: key);
#override
Widget build(BuildContext context) {
this.ingredients.forEach((ing) => print(ing.name! + ","));
return ListView(
children: <Widget>[
GridView.count(
crossAxisCount: 4,
// physics: NeverScrollableScrollPhysics(),
// to disable GridView's scrolling
shrinkWrap: true,
// You won't see infinite size error
children: _buildIngredients()),
// ...... other list children.
],
);
}
}
Moreover, I keep getting this Warning :
"Changing the content within the composing region may cause the input method to behave strangely, and is therefore discouraged. See https://github.com/flutter/flutter/issues/78827 for more details".
Visiting the linked GitHub page wasn't helpful
The problem is that while you are correctly filtering the list but your TextController is not getting assigned any value.
So, no value is getting assigned to your TextField as the initial value and hence the list again filters to have the entire list.
To solve this just assign the TextController the newValue like this.
void updateResults(String newValue) {
if (newValue.isEmpty) {
ingredients = Ingredient.getDummyIngredients();
} else {
print("new Value = $newValue");
ingredients = this.ingredients.where((ing) {
double similarity =
StringSimilarity.compareTwoStrings(ing.name, newValue);
print("$similarity for ${ing.name}");
return similarity > 0.2;
}).toList();
ingredients.forEach((element) {
print("found ${element.name}");
});
}
// change
searchController = TextEditingController.fromValue(
TextEditingValue(
text: newValue,
),
);
setState(() {});
}
If it throws an error then remove final from the variable declaration, like this :
var searchController = TextEditingController();

How to prevent multiple click on a gesture?

I have a GestureDetector that need to launch a url. But if the gesture gets multiple taps, then launch is called multiple times.
In the current code im trying to use a state _isButtonTapped to control the tap. But the .whenComplete is somehow call before the launch is preformed?
_isButtonTapped = false
Widget _buildButton(String key, Text title, String url) {
_onTapped() async {
if (await canLaunch(url)) {
launch(url).whenComplete(
() => setState(() {
_isButtonTapped = false;
}),
);
}
}
return GestureDetector(
onTap: () {
_isButtonTapped ? null : _onTapped();
setState(() {
_isButtonTapped = true;
});
},
child: Container(
child: Padding(
padding: EdgeInsets.all(6.0),
child: Center(child: title),
),
),
);
}
Try this:
class _HomePageState extends State<HomePage> {
bool _isButtonTapped = false;
String _url = "https://google.ca";
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
color: Colors.blue,
width: 100,
height: 100,
child: GestureDetector(
onTap: () async {
if (!_isButtonTapped) { // only allow click if it is false
_isButtonTapped = true; // make it true when clicked
if (await canLaunch(_url)) {
await launch(_url);
_isButtonTapped = false; // once url is launched successfully, we again make it false, allowing tapping again
}
}
},
),
),
),
);
}
}
Try this? It should solve your problem.
class SafeOnTap extends StatefulWidget {
SafeOnTap({
Key? key,
required this.child,
required this.onSafeTap,
this.intervalMs = 500,
}) : super(key: key);
final Widget child;
final GestureTapCallback onSafeTap;
final int intervalMs;
#override
_SafeOnTapState createState() => _SafeOnTapState();
}
class _SafeOnTapState extends State<SafeOnTap> {
int lastTimeClicked = 0;
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
final now = DateTime.now().millisecondsSinceEpoch;
if (now - lastTimeClicked < widget.intervalMs) {
return;
}
lastTimeClicked = now;
widget.onSafeTap();
},
child: widget.child,
);
}
}
You can wrap any kind of widget if you want.
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Center(
child: Column(
children: [
// every click need to wait for 500ms
SafeOnTap(
onSafeTap: () => log('500ms'),
child: Container(
width: double.infinity,
height: 200,
child: Center(child: Text('500ms click me')),
),
),
// every click need to wait for 2000ms
SafeOnTap(
intervalMs: 2000,
onSafeTap: () => log('2000ms'),
child: Container(
width: double.infinity,
height: 200,
child: Center(child: Text('2000ms click me')),
),
),
],
),
),
),
);
}
}
the easiest way is in inkWell widget put doubleTap: () {},
it will do nothing, when user click multiple time
You have a bug in your code.
You are setting _isButtonTapped to true everytime you press it.
Correct you onTap function:
return GestureDetector(
onTap: () {
if (_isButtonTapped == false){
_onTapped();
setState(() {
_isButtonTapped = true;
});
},
}
//...
Regarding why the whenComplete is not beign called when you expected, that's another problem. I never used it but tacking a quick look into the docs (https://api.flutter.dev/flutter/scheduler/TickerFuture/whenComplete.html) show us that are multiple ways of achiving this, including wraping the function in an try block and use thr finally cloused as the whenCompleted. You should take a look at he docs and tried it out. Can't help more with that detail.
Hope it helps you.