
Android アプリでボタンを素早く連打すると、同じ画面が何度も積み重なってしまうことがあります。
Home
↓
Detail
↓
Detail
↓
Detail
この問題を防ぐために debounce や throttle を使うケースは少なくありません。
しかし、Navigation 3 が提供する dropUnlessResumed は考え方がまったく異なります。
これは「一定時間タップを無視する」のではなく、画面が遷移できる状態かどうかを見てイベントを受け付ける API です。
🤔 debounce との違い
一見すると似ていますが、見ているものが違います。

つまり、
debounce
↓
「まだ500ms経ってないから無視」
dropUnlessResumed
↓
「この画面はもう操作できないから無視」
という違いがあります。
🤔 Navigationでは「時間」より「状態」が重要
画面遷移が始まると、現在の画面はすぐに RESUMED ではなくなります。
RESUMED
│
ボタン押下
│
navigate()
│
STARTED
│
STOPPED
この間にもう一度クリックされても、
dropUnlessResumed {
navController.navigate(...)
}
であれば実行されません。
つまり、
クリック1回目
↓
navigate()
クリック2回目
↓
画面はもう RESUMED じゃない
↓
無視
となります。
🤔 なぜ debounce より自然なのか
例えば画面遷移アニメーションが長くなったとします。
debounce は
500ms待つ
という固定時間なので、
- アニメーションが300ms
- アニメーションが700ms
どちらにも最適とは限りません。
一方 dropUnlessResumed は
画面が操作可能
↓
受け付ける
画面遷移中
↓
受け付けない
戻ってきた
↓
再び受け付ける
と、Lifecycle に合わせて自動的に動作します。
🤔 内部では何をしているの?
実装は驚くほどシンプルです。
概念的には次のような処理です。
if (lifecycle.currentState == Lifecycle.State.RESUMED) {
block()
}
つまり、
- 現在の Lifecycle を確認する
- RESUMED のときだけラムダを実行する
それだけです。
タイマーも、Coroutine も、待ち時間もありません。
🤔 Navigation 3 での使い方
Compose ではクリックイベントをそのまま包むだけです。
val onClick = dropUnlessResumed {
navController.navigate(Detail)
}
Button(
onClick = onClick
) {
Text("Open")
}
これだけで、
- 二重 Push
- 二重画面生成
- 連打による BackStack の重複
を簡単に防げます。
🤔 debounce を使うべき場面
もちろん debounce が不要になったわけではありません。
例えば
- 検索ボックス
- API リクエスト
- テキスト入力
- リアルタイム検索
のように「連続イベントを間引く」目的なら debounce が適しています。
一方、
- Navigation
- Dialog を開く
- BottomSheet を表示する
など Lifecycle に依存する UI 操作では dropUnlessResumed の方が自然です。
🤔 まとめ
dropUnlessResumed は debounce の代替ではありません。
見る対象が時間ではなく Lifecycle だからです。
Navigation では「今この画面は操作可能か」が最も重要になります。
そのため Navigation 3 では、時間ベースの制御ではなく Lifecycle ベースの制御で二重遷移を防ぐ設計になっています。
一度仕組みを理解すると、「なぜ Navigation 用に専用 API が用意されているのか」がよく分かるはずです。
🤔 参考
Related Categories : Android・Developmemt・JetpackCompose・KMP・Kotlin・Kotlin Multiplatform Mobile・Newbie・Recommended