ローソンデジタルイノベーション テックブログ

ローソンデジタルイノベーション(LDI)の技術ブログです

失敗から学んだ、Flutterでテキストボックスの状態に合わせてボタンの活性状態を制御する方法

はじめに

こんにちは、Android開発エンジニアの岡田です。

普段はAndroid開発を行っていますが、今回はFlutterで在宅勤務報告システムを開発した時の出来事を記事にしていきます。
テキストボックスの状態によって、ボタンの活性/非活性を制御する方法についてです。

本記事はRiverpodやMVVMに関する記事となっています。
以下の記事で紹介しているので、こちらもぜひご覧ください! techblog.ldi.co.jp

開発環境

  • Flutter:バージョン 3.7.7
  • flutter_riverpod:バージョン 2.3.1
  • hooks_riverpod:バージョン 2.3.1

対象読者

本記事は以下のような方々に向けた記事です。

  • Riverpodを使用して開発しているが、まだ慣れていない人

今回の内容はRiverpodを使いこなしている人からすれば、ただの失敗談でしょう...
ただ自分はこの問題でつまずいてしまったので、戒めとして記していきます...

やりたいこと

以下の画像のような画面があるとします。

このときテキストボックスに文字が入力されていない場合は、タップされないようボタンを非活性に変化させる、ということが今回追加したい機能です。

修正箇所のコード

修正対象のViewModelとViewのコードについて、注目箇所に絞って記載します。

ViewModel

以下がViewModelのコードです。

class TestViewModelNotifier
    extends StateNotifier<TestPageState> {
  
  ~~~省略~~~

  // テキストボックスを管理
  final testMessageController = TextEditingController();

  void execute() {
    // 処理を実行
    // testMessageController.text でテキストボックスの文字列を取得している
  }

  ~~~省略~~~
}

@immutable
class TestPageState {
  // ここでViewModelの状態を管理
}

TextEditingControllerが代入されている「testMessageController」でテキストボックスの文字列が取得できます。

View

次にViewのコードです。

class TestPage extends HookConsumerWidget {
  // ViewModelのProviderを代入
  final viewModelProvider = TestViewModelProvider;

  TestPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final TestViewModelNotifier viewModelNotifier =
        ref.read(viewModelProvider.notifier);

    return Scaffold(
        ~~~省略、ページに表示するUIを記載~~~
    );
  }
}

上記のコードで省略されているUI部分の記載から、テキストボックスとボタンのコードを抜粋します。
まずはテキストボックスです。

TextField(
  maxLines: null,
  expands: false,
  keyboardType: TextInputType.multiline,
  controller: viewModelNotifier.testMessageController,
  decoration: const InputDecoration(
    filled: true, hintText: '入力してください')
  );                      

テキストボックスの文字列を取得するため、ViewModelで定義されている「testMessageController」を使用します。
またボタンのコードは以下のとおりです。

ElevatedButton(
  onPressed: () {
    viewModelNotifier.execute();
  },
  child: const Text("実行"),
)

タップ時にViewModelで定義されている「execute()」が実行されます。

失敗した修正案

では自分が最初に思いついた修正方法を記載します。
まずElevatedButtonについて、onPressedにnullを渡すことでボタンを非活性にさせることができます。
そのためテキストボックスの内容によってonPressedに渡すものを変えることができれば目的を達成できます。 以上より、ElevatedButtonのonPressedに渡すものを以下のロジックで切り替える、というのが自分が最初に思いついた修正案です。

  • 「testMessageController.text」が空であれば、nullを渡す
  • 「testMessageController.text」が空でなければ、viewModelNotifier.execute()を実行する関数を渡す
ElevatedButton(
  // ここに条件文を追加
  onPressed: viewModelNotifier.testMessageController.text.isEmpty
    ? null
    : () {
      viewModelNotifier.execute();
    },
  child: const Text("実行"),
)

しかし、テキストボックスが空になっているのにボタンは活性化されたまま...
「testMessageController」にはテキストが入っていない?しかし「execute()」実行時にはテキストを使用して処理が行われている.....

失敗理由

失敗した理由は単純でした。
テキストボックス内の文字が変化した際、ViewModelの状態を更新していなかったためです。
状態が変更されなければ、画面の更新も行われないためボタンは非活性にならないという流れです。 「testMessageController」がViewModelのコードに記載されていたため、状態として管理されていると思い込んでいました.....

解決方法

解決方法として、まずテキストボックス内の文字が変化した際にViewModelの状態を変更させるよう「addListener」を行います。

class TestPage extends HookConsumerWidget {
  // ViewModelのProviderを代入
  final viewModelProvider = TestViewModelProvider;

  TestPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final TestViewModelNotifier viewModelNotifier =
        ref.read(viewModelProvider.notifier);
    final testPageState = ref.watch(viewModelProvider);

    // 初回読み込み時に実行
    useEffect(() {
      // テキストボックス内の文字変更時にステートを更新するようリスナを追加
      viewModelNotifier.testMessageController.addListener(() {
        viewModelNotifier.updateDescription();
      });

      return null;
    }, []);

    return Scaffold(
        ~~~省略、ページに表示するUIを記載~~~
    );
  }
}

そしてボタンはViewModelの状態として管理しているテキストボックス内の文字列を参照して条件分岐を行うようにします。

ElevatedButton(
  // 条件文を変更
  onPressed: testPageState.description.isEmpty
    ? null
    : () {
      viewModelNotifier.execute();
    },
  child: const Text("実行"),
)

最後にViewModel側のコードにテキストボックス内文字列の状態を更新するメソッドを追加すれば完成です。

class TestViewModelNotifier
    extends StateNotifier<TestPageState> {
  
  ~~~省略~~~

  // テキストボックスを管理
  final testMessageController = TextEditingController();

  /// 理由・業務内容を更新
  void updateDescription() {
    // TestPageStateで管理しているdescriptionをtestMessageController.textの値に更新
  }
}

@immutable
class TestPageState {
  // descriptionを管理するように修正
}

まとめ

  • Riverpodを使用するときは、状態を変化させなければUIはしない、ということを念頭において開発することが大事

  • TextEditingControllerが管理しているテキストボックス内の文字列が変化した際にUIを変更したいなら、addListenerメソッドを使用して状態を変化させる

最後に

今後も開発メンバーが技術ブログを更新していく予定ですので、興味がある方は是非「読者になる」をお願いします!