Flutter FocusNode

## What's this
FlutterのFocusNodeについてまとめてみるます。
下記の `FocusNode` のドキュメントをまとめたものです。

https://api.flutter.dev/flutter/widgets/FocusNode-class.html

FocusNodeをより理解するために、TextFormFieldでの使われ方についても触れます。

## FocusNode
キーボードのフォーカを取得したり、キーボードのイベントを処理するために使用されるオブジェクト。
FocusNode は ChangeNotifier であるため、フォーカスの変更を受け取ることができるListnerを登録できる。


### FocusTree
FocusNodesは、フォーカスに関する階層を表現した `FocusTree` を形成する。
`debugDumpFocusTree()` を使うことで、デバッグコンソールに FocusTree を表示することができます。また、`debugDescribeFocusTree()` で FocusTreeを文字列として取得することもlできる。このFocusTreeは次のfocusScopeNodeを理解する上での理解につながります。

```
I/flutter (13953): FocusManager#dcb9b
I/flutter (13953):  │ primaryFocus: FocusNode#6804e([PRIMARY FOCUS])
I/flutter (13953):  │
I/flutter (13953):  └─rootScope: FocusScopeNode#86af5(Root Focus Scope [IN FOCUS PATH])
I/flutter (13953):    │ IN FOCUS PATH
I/flutter (13953):    │ focusedChildren: FocusScopeNode#44017(Navigator Scope [IN FOCUS
I/flutter (13953):    │   PATH])
I/flutter (13953):    │
I/flutter (13953):    └─Child 1: FocusNode#bd472([IN FOCUS PATH])
I/flutter (13953):      │ context: Focus
I/flutter (13953):      │ NOT FOCUSABLE
I/flutter (13953):      │ IN FOCUS PATH
I/flutter (13953):      │
I/flutter (13953):      └─Child 1: FocusNode#e16bc(Shortcuts [IN FOCUS PATH])
I/flutter (13953):        │ context: Focus
I/flutter (13953):        │ NOT FOCUSABLE
I/flutter (13953):        │
```

https://api.flutter.dev/flutter/widgets/debugDumpFocusTree.html

https://api.flutter.dev/flutter/widgets/debugDescribeFocusTree.html

### FocusScopeNode
FocusNode は、FocusScopeNode によってまとめられて管理されています。 FocusScopeNodeは、ノードのサブツリーを形成し、探索をノードのグループに制限します。スコープ内では、直近にフォーカスされた FocusNode が記憶されており、 ある FocusNode がフォーカスされた後にフォーカスが解除されると、前の FocusNode が再びフォーカスされます。


### Lifecycle
StatefullWidget での FocusNode の生成と破棄に関してはFlutterのCookbookのドキュメントの例がわかりやすいので、リンクを下記に載せておきます。

https://docs.flutter.dev/cookbook/forms/focus#1-create-a-focusnode

### Focus Traversal
Focus Traversal とは、特定の順序で、あるウィジェットから次のウィジェットにフォーカスを移すこと。
前のウィジェットにフォーカスを与えるには、 `nextFocus` メソッドまたは `previousFocus` メソッドを呼び出します。また、特定の方向にフォーカスを与えるには、 `focusInDirection` メソッドを呼び出します。


## TextFormFieldのFocusの方法
4つの方法を紹介します。

### 1. ユーザがTextFormFiledをタップする
コードでは制御ではないですが、描画されているTextFormFiledはユーザがタップすることで、タップしたTextFormFieldにフォーカスが当たります。


### 2. autoFocusをTrueにする
テキストフィールドが表示されたらすぐにフォーカスを行う。（他にフォーカスがされていない場合）

`ex`

```dart
return TextFormField(autofocus: true);
```

`autoFocus bool`

https://api.flutter.dev/flutter/material/TextField/autofocus.html

TextFormFieldの内部で行われている、 `EditableText` Widgetのコードを見ることでautofocusの値がどう使われているかを見てみます。

`flutter/lib/src/widgets/editable_text.dart`

```dart
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();

    ....

    if (!_didAutoFocus && widget.autofocus) {
      _didAutoFocus = true;
      SchedulerBinding.instance!.addPostFrameCallback((_) {
        if (mounted) {
          FocusScope.of(context).autofocus(widget.focusNode);
        }
      });
    }
```

EditableText の didChangeDependencies 内で autofocus の値に応じて`FocusScope.of(context).autofocus(widget.focusNode);` が呼ばれている。
`FocusScope.of(context)` で context における FocusScope を取得して、FocusScope の autofocus メソッドを実行することで、 引数で指定した focusNode にフォーカスを当てるのと、FocusTree への追加が行われている。

https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/widgets/editable_text.dart#L1726-L1733

https://api.flutter.dev/flutter/widgets/FocusScopeNode/autofocus.html


### 3. FocusNode の requestFocus メソッド

`FocusNode` を生成して、TextFormFieldに渡して、フォーカスしたい箇所で `requestFocus` メソッドを実行する

```dart
class _MyHomePageState extends State<MyHomePage> {
  late FocusNode myFocusNode;

  @override
  void initState() {
    super.initState();
    myFocusNode = FocusNode();
  }

  @override
  void dispose() {
    myFocusNode.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Form Test'),
      ),
      body: Center(
        child: TextFormField(
          focusNode: myFocusNode,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          myFocusNode.requestFocus();
        },
      ),
    );
  }
}
```

### 4. FocusScopeNode の nextFocus メソッド
context の FocusScopeNode を取得して、FocusScope 内で Focus Traversal に基づいて、直近のFocusNodeに対してFocusを行う。

```dart
class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Form Test'),
      ),
      body: Center(
        child: TextFormField(),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          final focusScopeNode = FocusScope.of(context);
          focusScopeNode.nextFocus();
        },
      ),
    );
  }
}
```

ちなみにTextFormFieldにFocusNodeを指定しなくても、TextFieldの内部でFocusNodeが生成されます。


```dart
class _TextFieldState extends State<TextField> with RestorationMixin implements TextSelectionGestureDetectorBuilderDelegate {
  ...

  FocusNode? _focusNode;
  FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode());
  ...
```

https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/material/text_field.dart#L850

## TextFormFieldのUnfocusの方法
Focusと同じ方法で可能です。
1. ユーザがフォーカスされているフォームをタップする
2. フォカスされている focusNode の unfocus メソッドを呼ぶ
3. focusScopeNode の unfocus メソッドを呼ぶ

## キーイベントの取得
Focus Widget で TextFormField をラップすることで、 Focus Widget はもっとも近いFocusNodeを取得して、FocusNode の keyEventの取得と制御を行うことができます。
制御するには、 FocusOnKeyCallback の 返り値の KeyEventResult の値を適切に変化させることで入力を拒否したすることができます。

```dart
Focus(
  onKey: (FocusNode node, RawKeyEvent event) {
    print(node);
    print(event);
    return KeyEventResult.ignored;
  },
  child: TextFormField(),
),
```

https://api.flutter.dev/flutter/widgets/Focus-class.html

また、直接 focusNode の onKey にコールバック関数を登録することで、 keyEvent を取得することもできます。

```dart
focusNode.onKey = (FocusNode node, RawKeyEvent event) {
  print('$node, $key');
  return KeyEventResult.ignored;
};
```

https://api.flutter.dev/flutter/widgets/FocusNode/onKey.html

### FlutterのIssue
Flutter の iOS で DeleteKey のイベントを取得することができません。

https://github.com/flutter/flutter/issues/14809

SMSの暗証番号の入力などで、複数のフォームを並べてからの1つ前のフォームに戻るときに DeleteKey のイベントのハンドリングが必要になると思いますが、ハンドリングすることができません。

解決策としてIssue内でいろいろな方法が提案されていますが、個人的には zero-width unicode character の `\u200b` を先頭に入れてハンドリングする方法がしっくり来ました。

https://medium.com/super-declarative/why-you-cant-detect-a-delete-action-in-an-empty-flutter-text-field-3cf53e47b631