How do I switch focus groups (via keyboard) immediately?

235

Preface: I'm not an expert on Focus and find it quite convoluted myself.

But the below works for what you're after, I believe, traversing from one column to the next, without focusing "invisible" items. At least on mobile devices. Web platform... that's a whole different ballgame (and I doubt it works the same).

I got rid of the FocusableActionDetector (which acts as a FocusNode itself) and wrapped each Column in a FocusTraversalGroup. I believe Flutter tries to go from TraversalGroup to TraversalGroup when it can.

The FocusScope wrapping the TraversalGroups prevents the Back button and any other clickable items from getting focus (once the FocusScope has gained focus).

class MyHomePage2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: SizedBox(
          width: double.infinity,
          child: FocusScope( // LIMIT FOCUS TO DESCENDANTS
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                for (var i = 0; i < 2; i++)
                  FocusTraversalGroup( // CREATE GROUPS HERE
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        for (var i = 0; i < 3; i++)
                          SizedBox(
                            width: 150,
                            child: TextField(),
                          ),
                      ],
                    ),
                  ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

Summary

(to the best of my knowledge, which is fuzzy)

  • FocusScope limits node traversal to its direct descendant nodes
    • focus will not traverse from one FocusScope to another FocusScope
    • to go to another FocusScope the user will need to either manually/click-focus the other FocusScope, or its descendant nodes must requestFocus
    • using FocusScope instead of FocusTraversalGroup above, would limit node traversal to one Column, whichever got focus. It would not jump from one Column to the next when reaching last TextField
  • FocusTraversalGroup collects descendants into a group for traversal
    • but focus will leave this group for another group
    • jumps when at final child node and another FocusTraversalGroup is available
    • can adjust traversal order within its group

Debug

Dumping the focus tree using debugDumpFocusTree (a static function available everywhere) can be helpful in debugging.

I sometimes add it to the AppBar for easy, on-demand access:

    return Scaffold(
      appBar: AppBar(
        title: Text('Focus Tab Page'),
        actions: [
          IconButton(icon: Icon(Icons.info_outline), onPressed: debugDumpFocusTree)
        ],
      ),

I've copied to relevant part below.

└─Child 2: FocusScopeNode#c83f0(_ModalScopeState<dynamic> Focus Scope [IN FOCUS PATH])
  │ context: FocusScope
  │ IN FOCUS PATH
  │ focusedChildren: FocusScopeNode#7d536([IN FOCUS PATH])
  │
  ├─Child 1: FocusScopeNode#7d536([IN FOCUS PATH])
  │ │ context: FocusScope
  │ │ IN FOCUS PATH
  │ │ focusedChildren: FocusNode#1b886([PRIMARY FOCUS]),
  │ │   FocusNode#72c3b, FocusNode#34b25, FocusNode#3b410,
  │ │   FocusNode#02fac, FocusNode#61cd5
  │ │
  │ ├─Child 1: FocusNode#1d51e(FocusTraversalGroup)
  │ │ │ context: Focus
  │ │ │ NOT FOCUSABLE
  │ │ │
  │ │ ├─Child 1: FocusNode#3b410
  │ │ │   context: EditableText-[LabeledGlobalKey<EditableTextState>#70699]
  │ │ │
  │ │ ├─Child 2: FocusNode#34b25
  │ │ │   context: EditableText-[LabeledGlobalKey<EditableTextState>#7c822]
  │ │ │
  │ │ └─Child 3: FocusNode#72c3b
  │ │     context: EditableText-[LabeledGlobalKey<EditableTextState>#f00bc]
  │ │
  │ └─Child 2: FocusNode#bdb17(FocusTraversalGroup [IN FOCUS PATH])
  │   │ context: Focus
  │   │ NOT FOCUSABLE
  │   │ IN FOCUS PATH
  │   │
  │   ├─Child 1: FocusNode#1b886([PRIMARY FOCUS])
  │   │   context: EditableText-[LabeledGlobalKey<EditableTextState>#c5a56]
  │   │   PRIMARY FOCUS
  │   │
  │   ├─Child 2: FocusNode#61cd5
  │   │   context: EditableText-[LabeledGlobalKey<EditableTextState>#a4dd8]
  │   │
  │   └─Child 3: FocusNode#02fac
  │       context: EditableText-[LabeledGlobalKey<EditableTextState>#fe12d]
  │
  ├─Child 2: FocusNode#4886e
  │   context: Focus
  │
  └─Child 3: FocusNode#9241b
      context: Focus
Share:
235
Merritt
Author by

Merritt

Updated on December 30, 2022

Comments

  • Merritt
    Merritt over 1 year

    When traversing from one focus group to the next (with tab on keyboard) I expect the focus to move to the first field in the next group, but it seems to focus on nothing - and then another tab moves into that group.

    class MyHomePage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Center(
            child: SizedBox(
              width: double.infinity,
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                children: [
                  for (var i = 0; i < 2; i++)
                    FocusableActionDetector(
                      onFocusChange: (focused) {
                        if (!focused) {
                          print('Have left focus group');
                        }
                      },
                      child: Column(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          for (var i = 0; i < 3; i++)
                            SizedBox(
                              width: 150,
                              child: TextField(),
                            ),
                        ],
                      ),
                    ),
                ],
              ),
            ),
          ),
        );
      }
    }
    

    Dartpad example

    I expect that after the last field in the first group a press of Tab should move focus immediately to the first item in the next focus group.

    I have tried all manner of FocusNode, FocusScope, FocusScope.of(context).___, however I am finding the focus management in Flutter a bit confusing.

  • Merritt
    Merritt almost 3 years
    Perfect answer, nice bonus about using debugDumpFocusTree! FocusTraversalGroup was primarily what I needed, and you helped me understand how it works much better. In addition for my use case I need to set the selection of the TextField when focused, so I wrap the TextFields with a Focus with skipTraversal: true and set the controller selection inside onFocusChange. Thank you for the excellent answer, much appreciated! <3