【Flutter】テキストフィールドのフォーカス解除と条件付きの場合のコード理解

## やりたいこと
テキストフィールドでのフォーカス解除をやりたい

## フォーカスの解除
フォーカス解除の一般的な方法は以下になります。
```dart
primaryFocus?.unfocus();
```
このコードは、現在のプライマリフォーカスを持つウィジェットからフォーカスを外す処理を行います。

:::note info
プライマリフォーカスを持つウィジェットとは？
その時点でユーザーの入力（キーボード入力など）を直接受け取るウィジェットのことです。
:::

`primaryFocus?.unfocus();`を呼び出すことで、テキストフィールドからフォーカスを外すことが出来ます。

## 条件付きフォーカス解除
`primaryFocus?.unfocus();`でもフォーカスを解除できますが、ググっていると以下のような条件でフォーカス解除している例を見ます。

これが具体的に何をしているのかを私自身が理解するために、使用例をもとに解説していきます。

```dart
final currentScope = FocusScope.of(context);
if (!currentScope.hasPrimaryFocus && currentScope.hasFocus) {
  primaryFocus?.unfocus();
}
```

## 使用例
ユーザーがメイン画面のテキストフィールドにフォーカスを当てている状態から、ダイアログを表示し、ダイアログ内の「閉じるボタン」をタップすることで、メイン画面のテキストフィールドからフォーカスを外す動作で検証します。

### 例①：条件なしの場合
`Closeボタン`をタップした時に以下のメソッドを呼び出しています。

```dart
void _unFocusFirstVersion() {
    primaryFocus?.unfocus();
}
```

挙動
![focus_first.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2658262/402d4631-2e03-55eb-658d-acf101067acb.gif)

`Closeボタン`をタップすると、ダイアログを閉じた後テキストフィールドのフォーカスが解除されていることがわかります。


では次の例を見てみましょう

### 例②：条件ありの場合
`Closeボタン`をタップした時に以下のメソッドを呼び出しています。

```dart
  void _unFocusSecondVersion(BuildContext context) {
    final currentScope = FocusScope.of(context);
    if (!currentScope.hasPrimaryFocus && currentScope.hasFocus) {
      primaryFocus?.unfocus();
    }
  }
```

挙動
![focus_second.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2658262/5b9ad4a0-620c-6655-9f7d-9be4f2bfdd19.gif)


「Closeボタン」をタップすると、ダイアログを閉じた後テキストフィールドにフォーカスが当たっています。

## 条件付きフォーカス解除の解説
それでは以下のコードが何をしているのかを解説していきます。
```dart
final currentScope = FocusScope.of(context);
if (!currentScope.hasPrimaryFocus && currentScope.hasFocus) {
  primaryFocus?.unfocus();
}
```
### ①現在のフォーカススコープの取得

```dart
final currentScope = FocusScope.of(context); 
```
この行は、現在のウィジェットツリーのコンテキストにおけるフォーカススコープを取得します。

:::note
現在のウィジェットツリーのコンテキストとは？
`context`というパラメータは、現在のウィジェットの位置や状態に関する情報を持っています。FocusScope.of(context) を呼び出すことで、そのウィジェットが属するフォーカススコープを取得します。
:::


:::note
フォーカススコープとは？
フォーカススコープは、フォーカスを受け取ることができるウィジェットのグループを管理する仕組みです。

Flutterでは、ウィジェットツリー内の各部分が独自のフォーカススコープを持つことができ、これによりアプリケーション内の異なるUI部分（例えば、画面全体、ダイアログ、フォームなど）でフォーカスを個別に管理できます。
:::


### ②フォーカスの条件チェック
```dart
if (!currentScope.hasPrimaryFocus && currentScope.hasFocus) { ... }
```
現在のフォーカススコープがプライマリフォーカスを持っていない場合にのみフォーカスを解除します。

`!currentScope.hasPrimaryFocus`：現在のフォーカススコープにプライマリフォーカス（ユーザーの入力を受け取るアクティブなウィジェット）が存在しない。

`currentScope.hasFocus`：現在のフォーカススコープ内のいずれかのウィジェットがフォーカスを持っている。

### ③フォーカスの解除

```dart
primaryFocus?.unfocus();
```
現在のプライマリフォーカス（アクティブなフォーカス）を持つウィジェットからフォーカスを外します。

## 全体の流れ
### ①ダイアログのフォーカススコープ
ダイアログが表示されると、新しいフォーカススコープが作成されます。
このスコープ内には、ダイアログ内の要素（例えば、テキストフィールドやボタン）が含まれます。これにより、ダイアログ内のフォーカス管理が可能になります。

### ②プライマリフォーカスの位置
ダイアログが開かれると、通常はプライマリフォーカスがダイアログの最初のフォーカス可能な要素に移動します。
したがって、プライマリフォーカスはメイン画面の要素からダイアログ内の要素に移行します。

### ③フォーカス解除の条件
`_unFocusSecondVersion`メソッドの条件は、ダイアログ（現在のフォーカススコープ）がプライマリフォーカスを持っていない場合にのみ`true`となります。
これは、ダイアログ内の要素がフォーカスを持っているが、それがプライマリフォーカスではない場合にフォーカスを解除することができることを意味します。

しかし、メイン画面のテキストフィールドは、ダイアログのフォーカススコープとは独立しているため、このメソッドでは影響を受けません。そのためフォーカスは当たったままとなります。

## 条件付きフォーカス解除の使用例について
あくまで条件の比較のため上記の使用例で説明していましたが、
`_unFocusSecondVersion` の本来の使い方としては、ダイアログ内の要素がフォーカスを受けた後（例えばユーザーがダイアログ内のテキストフィールドをタップした後）にそのフォーカスを解除するために適しています。
このメソッドは、特にダイアログやモーダルウィンドウ内でのフォーカス管理に有効かと思います。

## 全コード
今回使用したコードになります。

```dart
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  void _showDialog(BuildContext context, bool useFirstVersion) {
    showDialog(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: const Text('Dialog'),
          content: const TextField(),
          actions: [
            ElevatedButton(
              child: const Text('Close'),
              onPressed: () {
                if (useFirstVersion) {
                  _unFocusFirstVersion();
                } else {
                  _unFocusSecondVersion(context);
                }
                Navigator.of(context).pop();
              },
            ),
          ],
        );
      },
    );
  }

  void _unFocusFirstVersion() {
    primaryFocus?.unfocus();
  }

  void _unFocusSecondVersion(BuildContext context) {
    final currentScope = FocusScope.of(context);
    if (!currentScope.hasPrimaryFocus && currentScope.hasFocus) {
      primaryFocus?.unfocus();
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Focus Demo'),
      ),
      body: Center(
        child: Column(
          children: [
            const TextField(
              autofocus: true,
            ),
            const SizedBox(height: 12),
            ElevatedButton(
              onPressed: () => _showDialog(context, true),
              child: const Text('Open Dialog (条件なし)'),
            ),
            const SizedBox(height: 12),
            ElevatedButton(
              onPressed: () => _showDialog(context, false),
              child: const Text('Open Dialog (条件あり)'),
            ),
          ],
        ),
      ),
    );
  }
}

```

## 参考記事

https://github.com/flutter/flutter/pull/43859

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

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