📱

[Flutter] 認証コード入力画面を自作する

2022/01/30に公開

背景

業務でSMS認証コード入力画面を実装するタスクがあり、その実装に少し手間取ったので共有します。
pub.devにpin_code_fields等もありましたが、如何せんサードパーティー製はカスタム性に乏しく業務のデザインにフィットしなかったので自作することにしました。

完成形のGIFは以下になります。(DartPadよりGIFの動作を確認できます。)

設計

当初の想定では、TextField, TextEditingController, FocusNodeを入力文字数分だけ用意し、FocusNode#requestFocus, FocusNode#unFocusを利用することで簡単に実装できるだろうと思っていました。

しかし、実際に実装してみるとわかるのですが、それだけでは文字をクリアするときの挙動を表現することができませんでした。というのも、未入力状態のTextFieldに対してクリアを押してもTextField#onChangedコールバックが発火しないので、FocusNode#requestFocusで一つ前のTextFieldにフォーカスすることができなかったのです。

そこでゼロ幅スペースを各TextFieldの1文字目に設定することで、TextField#onChangedのイベントを発火させるように設計しました。以下の図はイメージです。

実装

設計を元に、TextFieldの1文字目にゼロ幅スペースを入れ、適宜TextSelectionで入力時のカーソルを調整します。
以下は、TextField#onChangedに指定する関数になります。

void _onChanged(String str, int index) {
  if (str.length == 1 && str != space) {
    for (final controller in controllerList) {
      controller.text = space;
    }

    if (int.tryParse(str) == null) {
      return;
    }

    controllerList[index].value = TextEditingValue(
        text: space + str,
        selection: const TextSelection.collapsed(offset: 2));

    focusNodeList[index + 1].requestFocus();
    return;
  }

  if (str.length == charCount) {
    str.split('').asMap().forEach((index, value) {
      controllerList[index].value = TextEditingValue(
          text: space + value,
          selection: const TextSelection.collapsed(offset: 2));
    });

    focusNodeList[index].unfocus();
    return;
  }

  if (str.length >= 2) {
    if (int.tryParse(str.substring(str.length - 1, str.length)) == null) {
      controllerList[index].value = TextEditingValue(
          text: space, selection: const TextSelection.collapsed(offset: 1));
      return;
    }

    if (str.length > 2) {
      final newStr = space + str.substring(str.length - 1, str.length);

      controllerList[index].value = TextEditingValue(
          text: newStr, selection: const TextSelection.collapsed(offset: 2));
    }

    if (index < charCount - 1) {
      focusNodeList[index + 1].requestFocus();
    } else if (index == charCount - 1) {
      focusNodeList[index].unfocus();
    }

    return;
  }

  if (str.length == 1 && str == space && index != 0) {
    focusNodeList[index - 1].requestFocus();
    return;
  }

  if (str.isEmpty) {
    controllerList[index].value = TextEditingValue(
        text: space, selection: const TextSelection.collapsed(offset: 1));

    if (index != 0) {
      focusNodeList[index - 1].requestFocus();
    }
  }
}

全コードはgistDartPadを参照ください。

参考

StackOverFlowを参考にした覚えがありますが実装したのが半年ほど前で、現在そのページを見つけることができませんでした。見つかり次第、リンクを貼ります。

おわりに

200行程度で自作できたので、カスタム性を考えるとパッケージを利用するよりコスパはいい気がしてます。
もっといい方法があればコメントにて教えていただけると幸いです。

最後まで見ていただき、ありがとうございました。

Discussion