hibix's blog

明日の自分への備忘録

【Flutter】キーボードの表示状態を検知するカスタムフックを作ってみた

先日、キーボードの表示状態を検知しなくてはならない状況に遭遇しました。

検知は flutter_keyboard_visibility を用いて簡単に行うことができます。いくつかの実装が用意されていますが、ここでは Direct query and subscription の実装を使用します。

import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
import 'dart:async';

late StreamSubscription<bool> keyboardSubscription;

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

  var keyboardVisibilityController = KeyboardVisibilityController();
  // Query
  print('Keyboard visibility direct query: ${keyboardVisibilityController.isVisible}');

  // Subscribe
  keyboardSubscription = keyboardVisibilityController.onChange.listen((bool visible) {
    print('Keyboard visibility update. Is visible: $visible');
  });
}

@override
void dispose() {
  keyboardSubscription.cancel();
  super.dispose();
}

ただ、このサンプルを見て、「これは カスタムフックにできるのでは?」と思い実装してみました。

こちらがそのカスタムフックの実装です。

/// キーボードの表示状態を取得する Hook
bool useKeyboardVisibility() {
  return use(const _KeyboardVisibility());
}

class _KeyboardVisibility extends Hook<bool> {
  const _KeyboardVisibility();

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

class _KeyboardVisibilityState extends HookState<bool, _KeyboardVisibility> {
  late final KeyboardVisibilityController _controller;
  late final StreamSubscription<bool> _subscription;

  @override
  void initHook() {
    super.initHook();
    _controller = KeyboardVisibilityController();
    _subscription = _controller.onChange.listen((_) {
      // build を発火させる
      setState(() {});
    });
  }

  @override
  bool build(BuildContext context) {
    return _controller.isVisible;
  }

  @override
  void dispose() {
    _subscription.cancel();
    super.dispose();
  }
}

また、これをもっと使いやすく、キーボードを閉じた場合に限定したものも作ってみました。

/// キーボードが非表示になった時にコールバックを呼び出す Hook
void useOnKeyboardHidden(VoidCallback callback) {
  final keyboardVisibility = useKeyboardVisibility();
  useEffect(
    () {
      if (!keyboardVisibility) {
        callback();
      }
      return null;
    },
    [keyboardVisibility],
  );
}

今回は StreamSubscription を用いる形のものからカスタムフックを作成してみました。これまで flutter_hooks 組み込みのフックを組み合わせた関数のカスタムフックは何度か作成したことがありますが、クラスのカスタムフックは初めてだったので最初は戸惑いました。いい具合に作成できた気がします。

もっといい実装あるよ!これじゃあ、〇〇の場合に動かないよ!といったご意見、ご指摘ありましたら、是非ともコメントください。

テストダブルの整理

テストダブルを整理する

テスト専用に用意される偽りの依存の総称をテストダブルと呼ぶ。 テストダブルにはモック、スパイ、スタブ、ダミー、フェイクがある。 これらは、大きく分けるとモックとスタブの2種類に分類できる。

  • モック
    • テスト対象から依存に向かって行われる外部へのコミュニケーション
    • モック
    • スパイ
      • 手書き
  • スタブ
    • モックの逆。依存からテスト対象に向かって行われる内部へのコミュニケーション
    • スタブ
      • シナリオごとに結果を変えられる
    • ダミー
      • シグネチャを満たすためだけのハードコーディングされる固定値
    • フェイク
      • 実装が存在しない依存のためのスタブ。UnimplementErrorを返すのが一般的

検証の必要性

検証(verify)はモックに対してのみ行う。(道具としてのモックを指す。すなわちモックとスパイの両方を指す)

スタブを検証しない理由はテストの最終的な結果を生み出すための一過程でしかなく、必要なデータを提供しているに過ぎないためである。 スタブを検証することは過剰検証であり、アンチ・パターンである。

これに対してモックの検証が必要である理由は、モックの検証はテストの最終的な結果に直結するためである。 例として、メールアドレスで会員登録をテストする場合を考える。

  1. 認証基盤との連携をモック(具体的にはモックorスパイ)して認証の初期状態を設定しておく
  2. 会員登録用のメールアドレスを入力して次のステップに進める
  3. 認証コードが送信されることを検証する
  4. 認証状態が認証コード入力待ちになることを確認する

1.で取り組んだことは前提条件の設定であり、最終的な結果を生み出す一過程でしかない。 これに対して3.で取り組んだことは期待する動作の検証、すなわち最終的な結果のテストになるため、検証が必要である。

おまけ

Flutterの状態管理パッケージとして有名なRiverpodのテストはProviderContainer#listenを用いて行われる。 この場合も依存に向かう外部へのコミュニケーションであるために、verifyで検証するのかなと勝手に思った(完全に個人的な想像ですが)

riverpod.dev

参考

Vladimir Khorikov著、須田智之訳、マイナビ出版単体テストの考え方/使い方 プロジェクトの持続可能な成長を実現するための戦略」