こんにちは、普段は Android のアプリエンジニアをしているものです。
最近は Flutter とスマートコントラクトの開発がマイブームです。
ところで突然ですが、皆さんは rebuild に悩んだことはありませんか。
例えば rebuild が走るたびに複数回エラーのスナックバーを表示してしまったり、です。
StateNotifier を経由して画面遷移をするときにも、 State に遷移先の情報を保持しておくと複数回走ってしまったときにおかしな挙動をしてしまうことがありますね。
Jetpack Compose だったら LaunchedEffect
などを使って recompose を凌げますが、 Flutter にはそれがなくて困りました。
なぜこんなことに・・・
そもそも何が問題なのでしょうか。
問題点
問題点はシンプルで
「 Widget 側で、同じイベントを意図せず複数回受け取ってしまうこと」です。
同じイベントってなんでしょうね。
ここでは Event の インスタンス
が同じなら同じイベント、異なるなら異なるイベントと定義しておくと狙い通りの挙動になります。
解決策
解決策は、「同じイベントを複数回流さないこと」です。
SingleEvent
を作りました。
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 をセットしています(スナックバーを表示するため)
enum HomeAction {
toRegister;
}
@freezed
class HomeState with _$HomeState {
const factory HomeState({
@Default(null) SingleEvent<HomeAction>? action,
@Default(null) SingleEvent<Exception>? exception,
}) = _HomeState;
}
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 でも同じ問題がありましたね・・・
とはいえなんだかトリッキーな気がして、これを初めて見たときから気持ちが煮え切らないです。
もっとベストプラクティスがあるよ!という方はぜひコメントで教えてください!
それでは。