📝

Flutterでフォーカス制御とキーボード制御を完璧にする方法

に公開

Flutterアプリでテキストフィールドを使うとき、キーボードの表示・非表示を適切に制御したいことがありますよね。
今回は、フォーカス制御とキーボード制御について3つの方法を紹介します。


🚨 問題:キーボードが勝手に表示・非表示される

Flutterアプリでテキストフィールドを使うと、以下の問題が発生することがあります。

  • テキストフィールドをタップすると自動的にキーボードが表示される
  • キーボードを閉じたいのに閉じない
  • 複数のテキストフィールドでフォーカスが混乱する

📱 方法1:基本的なフォーカス制御

最もシンプルな方法です。FocusNodeを使ってフォーカスを制御します。

class BasicFocusControl extends StatefulWidget {
  const BasicFocusControl({super.key});

  
  State<BasicFocusControl> createState() => _BasicFocusControlState();
}

class _BasicFocusControlState extends State<BasicFocusControl> {
  // テキスト入力の制御用
  final TextEditingController _controller = TextEditingController();
  // フォーカス管理用
  final FocusNode _focusNode = FocusNode();

  
  void dispose() {
    // リソースを解放
    _controller.dispose();
    _focusNode.dispose();
    super.dispose();
  }

  // キーボードを閉じるメソッド
  void _dismissKeyboard() {
    _focusNode.unfocus();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('基本フォーカス制御')),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            TextField(
              // フォーカス管理
              focusNode: _focusNode,
              // テキスト制御
              controller: _controller,
              // 入力フィールドの装飾
              decoration: const InputDecoration(
                border: OutlineInputBorder(),
                labelText: 'テキストを入力',
              ),
            ),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: _dismissKeyboard,
              child: const Text('キーボードを閉じる'),
            ),
          ],
        ),
      ),
    );
  }
}

メリット

  • 実装が簡単
  • 基本的なキーボード制御ができる
  • 理解しやすい

デメリット

  • 複数フィールドには対応していない
  • タップで閉じる機能がない
  • 機能が限定的

🔄 方法2:タップでキーボードを閉じる

画面をタップしたときにキーボードを閉じる方法です。

class TapToDismissControl extends StatefulWidget {
  const TapToDismissControl({super.key});

  
  State<TapToDismissControl> createState() => _TapToDismissControlState();
}

class _TapToDismissControlState extends State<TapToDismissControl> {
  // テキスト入力の制御用
  final TextEditingController _controller = TextEditingController();

  
  void dispose() {
    // リソースを解放
    _controller.dispose();
    super.dispose();
  }

  // キーボードを閉じるメソッド
  void _dismissKeyboard() {
    // 現在のフォーカスを外してキーボードを閉じる
    FocusScope.of(context).unfocus();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('タップでキーボード閉じる')),
      body: GestureDetector(
        // 画面をタップしたときにキーボードを閉じる
        onTap: _dismissKeyboard,
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Column(
            children: [
              TextField(
                // テキスト制御
                controller: _controller,
                // 入力フィールドの装飾
                decoration: const InputDecoration(
                  border: OutlineInputBorder(),
                  labelText: 'テキストを入力',
                ),
              ),
              const SizedBox(height: 16),
              const Text('画面のどこかをタップするとキーボードが閉じます'),
            ],
          ),
        ),
      ),
    );
  }
}

メリット

  • 直感的な操作
  • 実装が簡単
  • ユーザビリティが良い

デメリット

  • 複数フィールドの制御ができない
  • フォーカス移動ができない
  • 細かい制御ができない

🎯 方法3:複数フィールドの制御

複数のテキストフィールドを適切に制御する方法です。

実装のポイント

  1. 複数のFocusNodeを管理
// 各テキストフィールド用のフォーカスノード
final FocusNode _nameFocusNode = FocusNode();
final FocusNode _emailFocusNode = FocusNode();
final FocusNode _messageFocusNode = FocusNode();
  1. すべてのキーボードを閉じる
void _dismissAllKeyboards() {
  _nameFocusNode.unfocus();
  _emailFocusNode.unfocus();
  _messageFocusNode.unfocus();
}
  1. 次のフィールドに移動
void _focusNext() {
  if (_nameFocusNode.hasFocus) {
    _emailFocusNode.requestFocus();
  } else if (_emailFocusNode.hasFocus) {
    _messageFocusNode.requestFocus();
  }
}

メリット

  • 複数フィールドを適切に制御
  • フォーカス移動ができる
  • 細かい制御が可能

デメリット

  • 実装が複雑
  • コードが長くなる
  • 管理が大変

✅ 3つの方法を比較した完全版コード

import 'package:flutter/material.dart';

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

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'フォーカス制御デモ',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const FocusControlComparisonDemo(),
    );
  }
}

class FocusControlComparisonDemo extends StatefulWidget {
  const FocusControlComparisonDemo({super.key});

  
  State<FocusControlComparisonDemo> createState() => _FocusControlComparisonDemoState();
}

class _FocusControlComparisonDemoState extends State<FocusControlComparisonDemo> {
  // 方法1用のコントローラーとフォーカスノード
  final TextEditingController _basicController = TextEditingController();
  final FocusNode _basicFocusNode = FocusNode();
  
  // 方法2用のコントローラー
  final TextEditingController _tapController = TextEditingController();
  
  // 方法3用のコントローラーとフォーカスノード
  final TextEditingController _nameController = TextEditingController();
  final TextEditingController _emailController = TextEditingController();
  final TextEditingController _messageController = TextEditingController();
  final FocusNode _nameFocusNode = FocusNode();
  final FocusNode _emailFocusNode = FocusNode();
  final FocusNode _messageFocusNode = FocusNode();

  
  void dispose() {
    // すべてのリソースを解放
    _basicController.dispose();
    _basicFocusNode.dispose();
    _tapController.dispose();
    _nameController.dispose();
    _emailController.dispose();
    _messageController.dispose();
    _nameFocusNode.dispose();
    _emailFocusNode.dispose();
    _messageFocusNode.dispose();
    super.dispose();
  }

  // 方法1: 基本的なキーボード制御
  void _dismissBasicKeyboard() {
    _basicFocusNode.unfocus();
  }

  // 方法2: タップでキーボードを閉じる
  void _dismissTapKeyboard() {
    FocusScope.of(context).unfocus();
  }

  // 方法3: すべてのキーボードを閉じる
  void _dismissAllKeyboards() {
    _nameFocusNode.unfocus();
    _emailFocusNode.unfocus();
    _messageFocusNode.unfocus();
  }

  // 方法3: 次のフィールドに移動
  void _focusNext() {
    if (_nameFocusNode.hasFocus) {
      _emailFocusNode.requestFocus();
    } else if (_emailFocusNode.hasFocus) {
      _messageFocusNode.requestFocus();
    }
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('フォーカス制御比較'),
        actions: [
          IconButton(
            icon: const Icon(Icons.keyboard_hide),
            onPressed: _dismissAllKeyboards,
          ),
        ],
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 方法1: 基本的なフォーカス制御
            _buildSectionTitle('方法1: 基本的なフォーカス制御'),
            _buildBasicFocusControl(),
            const SizedBox(height: 24),
            
            // 方法2: タップでキーボードを閉じる
            _buildSectionTitle('方法2: タップでキーボードを閉じる'),
            _buildTapToDismissControl(),
            const SizedBox(height: 24),
            
            // 方法3: 複数フィールドの制御
            _buildSectionTitle('方法3: 複数フィールドの制御'),
            _buildMultiFieldControl(),
            const SizedBox(height: 24),
            
            // 比較説明
            _buildComparisonInfo(),
          ],
        ),
      ),
    );
  }

  // セクションタイトルを表示するウィジェット
  Widget _buildSectionTitle(String title) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 8),
      child: Text(
        title,
        style: const TextStyle(
          fontSize: 16,
          fontWeight: FontWeight.bold,
          color: Colors.blue,
        ),
      ),
    );
  }

  // 方法1: 基本的なフォーカス制御
  Widget _buildBasicFocusControl() {
    return Column(
      children: [
        TextField(
          focusNode: _basicFocusNode,
          controller: _basicController,
          decoration: const InputDecoration(
            border: OutlineInputBorder(),
            labelText: 'テキストを入力',
          ),
        ),
        const SizedBox(height: 8),
        ElevatedButton(
          onPressed: _dismissBasicKeyboard,
          child: const Text('キーボードを閉じる'),
        ),
      ],
    );
  }

  // 方法2: タップでキーボードを閉じる
  Widget _buildTapToDismissControl() {
    return GestureDetector(
      onTap: _dismissTapKeyboard,
      child: Column(
        children: [
          TextField(
            controller: _tapController,
            decoration: const InputDecoration(
              border: OutlineInputBorder(),
              labelText: 'テキストを入力',
            ),
          ),
          const SizedBox(height: 8),
          const Text(
            '画面のどこかをタップするとキーボードが閉じます',
            style: TextStyle(fontSize: 12, color: Colors.grey),
          ),
        ],
      ),
    );
  }

  // 方法3: 複数フィールドの制御
  Widget _buildMultiFieldControl() {
    return Column(
      children: [
        TextField(
          focusNode: _nameFocusNode,
          controller: _nameController,
          decoration: const InputDecoration(
            border: OutlineInputBorder(),
            labelText: '名前',
          ),
          onSubmitted: (_) => _focusNext(),
        ),
        const SizedBox(height: 8),
        TextField(
          focusNode: _emailFocusNode,
          controller: _emailController,
          decoration: const InputDecoration(
            border: OutlineInputBorder(),
            labelText: 'メールアドレス',
          ),
          onSubmitted: (_) => _focusNext(),
        ),
        const SizedBox(height: 8),
        TextField(
          focusNode: _messageFocusNode,
          controller: _messageController,
          maxLines: 3,
          decoration: const InputDecoration(
            border: OutlineInputBorder(),
            labelText: 'メッセージ',
          ),
        ),
        const SizedBox(height: 8),
        ElevatedButton(
          onPressed: _dismissAllKeyboards,
          child: const Text('すべてのキーボードを閉じる'),
        ),
      ],
    );
  }

  // 比較情報を表示するウィジェット
  Widget _buildComparisonInfo() {
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Colors.blue.shade50,
        borderRadius: BorderRadius.circular(8),
        border: Border.all(color: Colors.blue.shade200),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: const [
          Text(
            '📊 比較表',
            style: TextStyle(
              fontSize: 16,
              fontWeight: FontWeight.bold,
            ),
          ),
          SizedBox(height: 8),
          Text('• 方法1: 基本制御 - 実装簡単、機能限定'),
          Text('• 方法2: タップ制御 - 直感的、単一フィールド向け'),
          Text('• 方法3: 複数制御 - 高機能、実装複雑'),
        ],
      ),
    );
  }
}

🎯 使い分けのポイント

方法1(基本制御)を使う場合

  • 単一のテキストフィールドの場合
  • 実装を簡単に済ませたい場合
  • 基本的なキーボード制御で十分な場合

方法2(タップ制御)を使う場合

  • 直感的な操作を重視する場合
  • 単一のテキストフィールドの場合
  • ユーザビリティを重視する場合

方法3(複数制御)を使う場合

  • 複数のテキストフィールドがある場合
  • フォーカス移動が必要な場合
  • 細かい制御が必要な場合

🧭 おわりに

Flutterでフォーカス制御とキーボード制御を適切に行うことで、ユーザーエクスペリエンスを大幅に向上させることができます。

今回紹介した3つの方法を参考に、自分のアプリに最適なフォーカス制御を実装してみてください!

Discussion