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

Viewed 267

I have a listview that I want to enable shortcuts like Ctrl+c, Enter, etc this improves user experience.

enter image description here

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.
1 Answers

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:

enter image description here

Related