LoginSignup
2
1
記事投稿キャンペーン 「2024年!初アウトプットをしよう」

Flutter でスナックバーが何度も表示されてしまう

Last updated at Posted at 2024-01-11

こんにちは、普段は Android のアプリエンジニアをしているものです。
最近は Flutter とスマートコントラクトの開発がマイブームです。

ところで突然ですが、皆さんは rebuild に悩んだことはありませんか。
例えば rebuild が走るたびに複数回エラーのスナックバーを表示してしまったり、です。
StateNotifier を経由して画面遷移をするときにも、 State に遷移先の情報を保持しておくと複数回走ってしまったときにおかしな挙動をしてしまうことがありますね。
Jetpack Compose だったら LaunchedEffect などを使って recompose を凌げますが、 Flutter にはそれがなくて困りました。
なぜこんなことに・・・

そもそも何が問題なのでしょうか。

問題点

問題点はシンプルで
「 Widget 側で、同じイベントを意図せず複数回受け取ってしまうこと」です。
同じイベントってなんでしょうね。
ここでは Event の インスタンス が同じなら同じイベント、異なるなら異なるイベントと定義しておくと狙い通りの挙動になります。

解決策

解決策は、「同じイベントを複数回流さないこと」です。
SingleEvent を作りました。

single_event.dart
class SingleEvent<T> {
  var _done = false;
  final T value;

  SingleEvent(this.value);

  T? get() {
    if (_done) {
      return null;
    }
    _done = true;
    return value;
  }
}

SingleEvent のインスタンスが同じとき、get() を二度目以降は null が返ります。
新しい SingleEvent のインスタンスを作れば get() すると最初の一回は non-null が返ります。

使い方は下記です。
軽くシチュエーションを説明すると、クレデンシャルがあれば Register 画面に進めて、なければ Exception をセットしています(スナックバーを表示するため)

home_state.dart
enum HomeAction {
  toRegister;
}

@freezed
class HomeState with _$HomeState {
  const factory HomeState({
    @Default(null) SingleEvent<HomeAction>? action,
    @Default(null) SingleEvent<Exception>? exception,
  }) = _HomeState;
}
home_notifier.dart

  Future<void> onTappedRegister() async {
    final result = await fetchCredentialUseCase(null);
    result.when(
      success: (data) {
        if (data != null) {
          state = state.copyWith(action: SingleEvent(HomeAction.toRegister));
        } else {
          state = state.copyWith(
            exception: SingleEvent(NoCredentialException()),
          );
        }
      },
      error: (error) {},
      exception: (exception) {
        state = state.copyWith(exception: SingleEvent(exception));
      },
    );
  }
  

こうすれば rebuild がいくら走っても大丈夫です。
Widget 側が意図せず複数回、同じ値を受け取ってしまうことはありません。

※奇妙な解決策に思われるかもしれませんが、ネイティブの Android アプリ開発だと珍しくない方針だと思います。

こちらの回答にも同じようなものがあります。↓

/*Used as a wrapper for data that is exposed via a LiveData that represents an 
 event.*/

open class Event<out T>(private val content: T) {

  var hasBeenHandled = false
    private set // Allow external read but not write

  /**
   * Returns the content and prevents its use again.
  */
  fun getContentIfNotHandled(): T? {
    return if (hasBeenHandled) {
      null
    } else {
      hasBeenHandled = true
      content
    }
  }

  /**
    * Returns the content, even if it's already been handled.
  */
  fun peekContent(): T = content
}

LiveData でも同じ問題がありましたね・・・

とはいえなんだかトリッキーな気がして、これを初めて見たときから気持ちが煮え切らないです。
もっとベストプラクティスがあるよ!という方はぜひコメントで教えてください!

それでは。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1