【Flutter】FocusNodeを使いこなすための基本ガイド

  • 2024年6月26日
  • 2024年6月26日
  • Widget

対象者

  • Flutterを使用しているアプリケーション開発者
  • フォーム入力やフォーカス管理に課題を感じている方
  • ユーザーエクスペリエンスを向上させたいと考えている方

はじめに

Flutterを使いこなすためには、ただコードを書くことだけでなく、ユーザーの視点に立ったUI/UXの設計も重要です。FocusNodeをマスターすることで、ユーザーがスムーズに操作できる直感的なインターフェースを提供し、満足度を高めることができます。

この記事を読むことで、FocusNodeの作成・初期化から、TextFieldやボタンとの連携、requestFocusやunfocusといった具体的なメソッドの使い方など、すべてを網羅的に理解できます。
この記事は、Flutterを使用するすべてのアプリケーション開発者、特にフォーム入力やフォーカス管理に課題を感じている方、そしてユーザーエクスペリエンスを向上させたいと考えている方に最適です。

FocusNodeとは

FocusNodeの基本概要

FocusNodeは、Flutterにおいてウィジェットがフォーカスを受けるかどうかを制御するためのクラスです。ユーザーがテキスト入力を行う際や、キーボード操作を管理する際に重要な役割を果たします。

Flutterアプリケーションでは、ユーザーがフォームに入力する際に複数のTextFieldを使用することが一般的です。このような場合、どのTextFieldが現在フォーカスを持っているかを管理する必要があります。FocusNodeは、このフォーカスの状態を管理するための中心的なコンポーネントです。各TextFieldにFocusNodeを関連付けることで、フォーカスの移動やキーボードの表示・非表示を制御できます。

FocusNodeの役割と用途

FocusNodeは、主に以下のような役割と用途があります。

  1. フォーカスの管理:
    FocusNodeは、ウィジェットが現在フォーカスを受けているかどうかを管理します。これにより、ユーザーの入力に応じて動的にUIを変更できます。

  2. フォーカスの移動:
    複数の入力フィールド間でフォーカスを移動する際に使用します。例えば、ユーザーがあるTextFieldで入力を完了した後、次のTextFieldに自動的にフォーカスを移動させることができます。

  3. キーボード操作の管理:
    FocusNodeを使用することで、キーボードの表示・非表示を制御できます。これにより、ユーザーの操作に応じて適切にキーボードを表示したり隠したりできます。

FocusNodeの使い方

FocusNodeの作成と初期化、廃棄

FocusNodeの作成と初期化は非常にシンプルです。
FocusNodeを利用することで、ウィジェットがフォーカスを受けるかどうかを管理できます。

FocusNodeを使用するためには、まずFocusNodeオブジェクトを作成し、必要に応じて初期化します。これは主にStatefulWidget内で行われます。作成されたFocusNodeは、ウィジェットのライフサイクルに従って適切に破棄することが重要です。

FocusNode myFocusNode = FocusNode();

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

このコードスニペットでは、FocusNodeを定義・初期化し、disposeメソッド内で破棄しています。これにより、メモリリークを防ぐことができます。FocusNodeを適切に管理することで、アプリケーションのパフォーマンスと安定性を向上させることができます。

FocusNodeのアタッチ方法

FocusNodeを作成したら、それを特定のウィジェットにアタッチする必要があります。最も一般的なケースは、TextFieldなどの入力フィールドにアタッチすることです。FocusNodeをTextFieldにアタッチすることで、そのTextFieldがフォーカスを受けたときに特定の動作を実行できます。

@override
Widget build(BuildContext context) {
  return TextField(
    focusNode: myFocusNode,
  );
}

このコードでは、TextFieldのfocusNodeプロパティに先ほど作成したmyFocusNodeを設定しています。これにより、TextFieldがフォーカスを受けると、FocusNodeがその状態を管理し始めます。フォーカスの移動やキーボードの表示・非表示の制御を簡単に行えるようになります。

FocusNodeを利用したフォーカスの管理

FocusNodeを使用すると、フォーカスの管理が容易になります。詳しく見ていきましょう。

requestFocusの使い方

FocusNodeのrequestFocusメソッドは、特定のウィジェットにフォーカスを設定します。これにより、ユーザーが特定の入力フィールドに迅速にアクセスできるようになります。

このメソッドは、特定の条件が満たされた場合にウィジェットにフォーカスを設定するために使用されます。例えば、ユーザーが特定のボタンをクリックしたときに、次の入力フィールドに自動的にフォーカスを移すことができます。

実例として、以下のコードを見てみましょう。

FocusNode firstFocusNode = FocusNode();
FocusNode secondFocusNode = FocusNode();


@override
Widget build(BuildContext context) {
  return Column(
    children: [
      TextField(
        focusNode: firstFocusNode,
      ),
      ElevatedButton(
        onPressed: () {
          firstFocusNode.requestFocus();
        },
        child: Text('Focus First Field'),
      ),
    ],
  );
}

この例では、ボタンをクリックすると最初のTextFieldにフォーカスが移動します。requestFocusメソッドを使用することで、ユーザーが効率的に入力を行えるように設計されています。

unfocusの使い方

unfocusメソッドは、現在フォーカスされているウィジェットからフォーカスを外すために使用されます。特に、ユーザーの入力が完了した後に自動的にフォーカスを解除する場合や、特定の条件が満たされた場合にフォーカスをクリアする場合に有用です。

実例として、以下のコードを見てみましょう。

FocusNode myFocusNode = FocusNode();

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

@override
Widget build(BuildContext context) {
  return Column(
    children: [
      TextField(
        focusNode: myFocusNode,
      ),
      ElevatedButton(
        onPressed: () {
          myFocusNode.unfocus();
        },
        child: Text('Unfocus TextField'),
      ),
    ],
  );
}

この例では、ボタンをクリックするとTextFieldからフォーカスが外れます。unfocusメソッドを使用することで、ユーザーインターフェースの制御がより柔軟になります。

addListenerによるフォーカス変更の監視

addListenerメソッドは、FocusNodeの状態が変更されたときに通知を受け取るためのリスナーを追加するために使用されます。これにより、フォーカスの変更を監視し、適切なアクションを実行することが可能になります。

このメソッドは、フォーカスの変更を検出して特定の処理を実行する場合に非常に有用です。例えば、フォーカスが特定のフィールドに移動したときに特定のアニメーションを開始するなどの用途があります。

実例として、以下のコードを見てみましょう。

FocusNode myFocusNode = FocusNode();

@override
void initState() {
  super.initState();
  myFocusNode.addListener(() {
    if (myFocusNode.hasFocus) {
      print('TextField has focus');
    } else {
      print('TextField lost focus');
    }
  });
}

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

@override
Widget build(BuildContext context) {
  return TextField(
    focusNode: myFocusNode,
  );
}

この例では、TextFieldのフォーカスが変更されるたびにコンソールにメッセージが表示されます。addListenerメソッドを使用することで、フォーカスの状態をリアルタイムで監視し、ユーザーエクスペリエンスを向上させることができます。

FocusNodeのrequestFocus、unfocus、addListenerメソッドを活用することで、ユーザーインターフェースのフォーカス管理がより柔軟かつ効率的になります。これにより、ユーザーはアプリケーションを直感的に操作できるようになり、全体的なユーザビリティが向上します。

TextFieldのautoFocusや「next」ボタンを使ったフォーカスの移動

TextFieldのautoFocus

TextFieldのautoFocusプロパティを利用することで、ユーザーが画面を開いたときに自動的に特定の入力フィールドにフォーカスを設定できます。これにより、ユーザーは手動でフィールドをタップする必要がなくなり、すぐに入力を開始することができます。

以下のコードは、TextFieldにAutoFocusを設定する簡単な例です。

    child: TextField(
      autofocus: true,
      decoration: const InputDecoration(labelText: 'AutoFocus Field'),
    ),

このコードは、画面が表示されたときにTextFieldに自動的にフォーカスが設定される例です。これにより、ユーザーはすぐに入力を開始できます。

フォーカスを次に移動

フォーム入力中にキーボードの「next」ボタンを使用してフォーカスを次のフィールドに移動させることで、ユーザーの入力作業をスムーズに進めることができます。これは特に複数の入力フィールドがあるフォームで有用です。

以下のコードは、「next」ボタンを使用してフォーカスを移動させる例です。

    TextField(
      focusNode: firstFocusNode,
      decoration: const InputDecoration(labelText: 'First Field'),
      textInputAction: TextInputAction.next,
    ),
    TextField(
      focusNode: secondFocusNode,
      decoration: const InputDecoration(labelText: 'Second Field'),
      onEditingComplete: () =>
          FocusScope.of(context).requestFocus(thirdFocusNode),
    ),

このコードは、ユーザーが「next」ボタンを押すと次のTextFieldにフォーカスが移動する例です。これにより、ユーザーはフォームをスムーズに入力できます。

TextFieldのtextInputAction

textInputActionにはいくつかの主要なアクションがあります。その中でも特に重要なものを以下に示します。

  • TextInputAction.next: 次の入力フィールドにフォーカスを移動します。
  • TextInputAction.done: 入力を完了します(一般的には、フォームの送信などに使われます)。
  • TextInputAction.go: 次のステップに進みます(例:検索を開始する)。
  • TextInputAction.search: 検索を実行します。
  • TextInputAction.send: メッセージを送信します。

以下は、上記の4つの方法を使用してフォーカスを外すFlutterアプリのサンプルコードです。TextFieldが一つあり、4つのボタンでフォーカスが外れるように設定されています。

フォーカスを外す方法

フォーカスを外す方法がいくつかありますので紹介します。

  1. FocusNode().unfocus():

    • _textFieldFocusNodeというFocusNodeを作成し、それをTextFieldに関連付けています。
    • ボタンを押すと_textFieldFocusNode.unfocus()が呼び出され、キーボードが閉じます。
  2. FocusManager.instance.primaryFocus?.unfocus():

    • FocusManager.instance.primaryFocus?.unfocus()を使って、現在フォーカスされている要素のフォーカスを外し、キーボードを閉じます。
    • primaryFocus?.unfocus()という形でも書けます。contextが不要のため、どこからでも呼べるので、すべてのフォーカスを外したいときに便利
  3. SystemChannels.textInput.invokeMethod('TextInput.hide'):

    • SystemChannels.textInput.invokeMethod('TextInput.hide')を使って、プラットフォーム固有の方法でキーボードを閉じます。
  4. FocusScope.of(context).unfocus():

    • FocusScope.of(context).unfocus()を使って、指定されたコンテキスト内のすべてのウィジェットのフォーカスを外し、キーボードを閉じます。

このコードを使うことで、TextFieldのキーボードを4つの異なる方法で閉じることができます。

hasFocusとhasPrimaryFocus の違い

hasFocushasPrimaryFocusは、FlutterのFocusNodeクラスで使用されるプロパティで、フォーカスの状態を示します。これらのプロパティは、ウィジェットが現在フォーカスされているかどうかを確認するために使用されますが、それぞれが異なる意味を持っています。

hasFocus

hasFocusは、そのウィジェットまたはその子孫のどれかがフォーカスされているかどうかを示します。つまり、そのウィジェット自体がフォーカスを持っている場合や、そのウィジェットの子孫がフォーカスを持っている場合にtrueを返します。

hasPrimaryFocus

hasPrimaryFocusは、そのウィジェット自体がフォーカスを持っている場合のみtrueを返します。これは、フォーカスの末端(ツリーの最も深い部分)であることを意味します。子孫ウィジェットがフォーカスを持っている場合、hasPrimaryFocusfalseを返します。

Q&A

Q1: FocusNodeとは何ですか?

A1: FocusNodeは、Flutterアプリケーションでウィジェットのフォーカス状態を管理するためのクラスです。これを使用することで、テキスト入力フィールドのフォーカスを簡単に制御でき、複雑なフォームでもユーザーが効率的にデータを入力できるようになります。初期化や破棄を適切に行うことで、メモリリークを防ぎ、アプリケーションのパフォーマンスを維持できます。

Q2: FocusNodeのrequestFocusメソッドの使い方は?

A2: requestFocusメソッドは、特定のウィジェットにフォーカスを設定するために使用されます。例えば、ユーザーがボタンをクリックした際に次の入力フィールドにフォーカスを移動させることが可能です。これにより、ユーザーがフォームをスムーズに入力できるようになります。以下のコード例では、ボタンをクリックすると次のTextFieldにフォーカスが移ります。

ElevatedButton(
  onPressed: () {
    FocusScope.of(context).requestFocus(secondFocusNode);
  },
  child: Text('Next Field'),
);

Q3: FocusNodeを使用する際の注意点は何ですか?

A3: FocusNodeを使用する際には、メモリ管理とフォーカスの適切な制御が重要です。特に、ウィジェットのライフサイクルに合わせてFocusNodeを初期化し、破棄することが重要です。これを怠ると、メモリリークが発生し、アプリケーションのパフォーマンスが低下する可能性があります。また、自動フォーカスの使用には注意が必要で、ユーザーが意図しないタイミングでキーボードが表示されないように設定することが大切です。

Q4: フォーカスを外すにはどうすれば良いですか

A4: primaryFocus?.unfocus()でフォーカスを外せると思います。

まとめ

この記事を通じて、FocusNodeの基本概要や役割、使い方について学びました。FocusNodeの初期化や破棄の重要性、requestFocusやunfocusの使用方法、addListenerによるフォーカス変更の監視など、具体的なメソッドの活用方法を理解しました。また、TextFieldとの連携やボタンによるフォーカス切り替えの実例を通じて、実践的なフォーカス管理の手法を勉強しました。これにより、複雑なフォームの管理やパフォーマンスの最適化に必要な知識を身につけ、Flutterアプリケーションのユーザーエクスペリエンスを向上させる方法を理解しました。

参考

ソース(main.dartにコピペして動作確認用)

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:rive/rive.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter FocusNode Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  FocusNode firstFocusNode = FocusNode();
  FocusNode secondFocusNode = FocusNode();
  FocusNode thirdFocusNode = FocusNode();

  final formNodeNode = FocusNode();
  final textFieldFocusNode = FocusNode();

  var nodeInfo = '';

  @override
  void initState() {
    super.initState();

    // Add listeners to FocusNodes
    firstFocusNode.addListener(() {
      if (firstFocusNode.hasFocus) {
        print('First TextField has focus');
      } else {
        print('First TextField lost focus');
      }
    });
    secondFocusNode.addListener(() {
      if (secondFocusNode.hasFocus) {
        print('Second TextField has focus');
      } else {
        print('Second TextField lost focus');
      }
    });
    thirdFocusNode.addListener(() {
      if (thirdFocusNode.hasFocus) {
        print('Third TextField has focus');
      } else {
        print('Third TextField lost focus');
      }
    });
  }

  @override
  void dispose() {
    firstFocusNode.dispose();
    secondFocusNode.dispose();
    thirdFocusNode.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter FocusNode Demo'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            TextField(
              focusNode: firstFocusNode,
              decoration: const InputDecoration(labelText: 'First Field'),
              textInputAction: TextInputAction.next,
            ),
            TextField(
              focusNode: secondFocusNode,
              decoration: const InputDecoration(labelText: 'Second Field'),
              onEditingComplete: () =>
                  FocusScope.of(context).requestFocus(thirdFocusNode),
            ),
            TextField(
              focusNode: thirdFocusNode,
              decoration: const InputDecoration(labelText: 'Third Field'),
              textInputAction: TextInputAction.done,
            ),
            const SizedBox(height: 20),
            FilledButton(
              onPressed: () =>
                  FocusScope.of(context).requestFocus(firstFocusNode),
              child: const Text('Focus First Field'),
            ),
            const SizedBox(width: 10),
            FilledButton(
              onPressed: () =>
                  FocusScope.of(context).requestFocus(secondFocusNode),
              child: const Text('Focus Second Field'),
            ),
            const SizedBox(width: 10),
            FilledButton(
              onPressed: () =>
                  FocusScope.of(context).requestFocus(thirdFocusNode),
              child: const Text('Focus Third Field'),
            ),
            const SizedBox(height: 20),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                Text('Unfocus all'),
                OutlinedButton(
                  onPressed: () {
                    firstFocusNode.unfocus();
                    secondFocusNode.unfocus();
                    thirdFocusNode.unfocus();
                  },
                  child: const Text('1'),
                ),
                OutlinedButton(
                  onPressed: () {
                    SystemChannels.textInput.invokeMethod('TextInput.hide');
                  },
                  child: const Text('2'),
                ),
                OutlinedButton(
                  onPressed: () {
                    // FocusManager.instance.primaryFocus?.unfocus();
                    primaryFocus?.unfocus();
                  },
                  child: const Text('3'),
                ),
                OutlinedButton(
                  onPressed: () {
                    FocusScope.of(context).unfocus();
                  },
                  child: const Text('4'),
                ),
                const SizedBox(height: 16),
              ],
            ),
            Focus(
              focusNode: formNodeNode,
              child: Form(
                child: Column(
                  children: [
                    TextField(focusNode: textFieldFocusNode),
                    FilledButton(
                      onPressed: () {
                        final parentStatus =
                            'Form hasFocus: ${formNodeNode.hasFocus} hasPrimaryFocus: ${formNodeNode.hasPrimaryFocus}';
                        final childStatus =
                            'TextField hasFocus: ${textFieldFocusNode.hasFocus} hasPrimaryFocus: ${textFieldFocusNode.hasPrimaryFocus}';
                        setState(() {
                          nodeInfo = '$parentStatus\n$childStatus';
                        });
                      },
                      child: const Text('Test'),
                    ),
                    Text(nodeInfo),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}