Navigation 3 時代の「二重遷移」を防ぐ正しい方法 - dropUnlessResumed は debounce ではない

Android アプリでボタンを素早く連打すると、同じ画面が何度も積み重なってしまうことがあります。


Home
  ↓
Detail
  ↓
Detail
  ↓
Detail

この問題を防ぐために debouncethrottle を使うケースは少なくありません。

しかし、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 の方が自然です。

 

🤔 まとめ

dropUnlessResumeddebounce の代替ではありません。

見る対象が時間ではなく Lifecycle だからです。

Navigation では「今この画面は操作可能か」が最も重要になります。

そのため Navigation 3 では、時間ベースの制御ではなく Lifecycle ベースの制御で二重遷移を防ぐ設計になっています。

一度仕組みを理解すると、「なぜ Navigation 用に専用 API が用意されているのか」がよく分かるはずです。

 

🤔 参考


Related Categories :  AndroidDevelopmemtJetpackComposeKMPKotlinKotlin Multiplatform MobileNewbieRecommended