
JetpackCompose日記(連続タップ対策を真剣に考える)

IT技術

はじめに
JetpackComposeでの連続タップ対策を真面目に考たい。
遷移ボタンの連続タップ対策として
よくdropUnlessResumed()が使われている記事があったりしますが。
中身を見ると、連続タップ対策というよりは
「特定のライフサイクル内かどうか?」を見ているだけです。
押下と同時に遷移するのであれば2重遷移対策として使えますが、
「押下時に何かしら処理をして、その処理を待ってから遷移する」みたいな実装では使えません。
これでは使い勝手が悪いです。
汎用的な連続タップ対策を考えるなら自分で実装する必要がありそうです。
「操作を受け付けるかどうか」の状態を保持する
連続タップ対策では
「操作を受け付けるかどうか」という状態をBooleanで保持することになると思います。
なので、「UiState」や「UiStateを作成するための状態」と同じように保持するのがよいです。
保持場所については以下の動画が参考になります。
「操作を受け付けるかどうか」のBooleanをどこで保持するのか?
大きく分けると2種類
Booleanの保持場所 | 利用するケース |
---|---|
UiState or ViewModel | 通信や重い処理など待つ時間が一定ではない場合 |
Composable関数内 | 簡易的な連続タップ対策 ダイアログ表示ボタンの連続タップなど 待つ時間が一定の場合 |
UiState or ViewModelで保持する
通信や重い処理が絡む場合は、基本的このやり方で実装するべきだと思います。
1class HogeViewModel {
2 // 今回はローディング中という状態として保持
3 val isLoading = MutableStateFlow(false)
4
5 val uiState: StateFlow = combine(/*省略*/).stateIn(/*省略*/)
6
7 fun onClickButton() {
8 if (isLoading.value) {
9 // ローディング中は無視する
10 return
11 }
12 // ローディング中にする
13 isLoading.update { true }
14 // 通信
15 try {
16 fetchHoge()
17 } finally {
18 isLoading.update { false }
19 }
20 }
21}
もし、「ローディング中はボタンを非活性にする」などUIにも影響を与えたい場合は。
以下のように、UiState作成のcombineに、isLoadingを追加すればよさそう。
1 // UiState生成要素の一つとしている
2val uiState: StateFlow = combine(
3 repository.sourceFlow,
4 isLoading
5 ) { source, isLoading ->
6 HogeUiState(
7 text = source.text,
8 isEnableButton = !isLoading // isLoadingをUiStateの生成に利用する
9 )
10 }.stateIn(/*省略*/)
Composable関数内で保持する
ビジネスロジックが絡まない場合や、とにかく簡易的に連続タップ対策をしたい場合はこちらの方法がよいです。
「Compose関数内のrememberでBooleanを保持して、一定時間経ったらBooleanをもとに戻す」
みたいな実装です。
ちょうど参考になりそうな記事を書いてる方がいたのでリンク貼ります。
Jetpack Compose での多重タップを防ぐコードを考えた
https://zenn.dev/t2low/articles/4f96f32c919f27
上記の記事を参考に自分が使いやすいようにカスタムしてみました。
1/*
2 * 二重タップや同時押しなど
3 * 連続で発生するイベントを抑制することを主目的としたDelay状態
4 *
5 * 参考:https://zenn.dev/t2low/articles/4f96f32c919f27
6 */
7data class DelayState(
8 val delayTimeMillis: Long = 500,
9) {
10 var isDelayed: Boolean by mutableStateOf(true)
11}
12
13@Composable
14fun rememberDelayState(
15 intervalTimeMillis: Long = 500,
16): DelayState {
17 val state = remember { DelayState(delayTimeMillis = intervalTimeMillis) }
18 LaunchedEffect(state.isDelayed) {
19 if (state.isDelayed) return@LaunchedEffect
20 delay(state.delayTimeMillis)
21 state.isDelayed = true
22 }
23 return state
24}
25
26/**
27 * Composable関数で使わない場合
28 */
29fun dropRedundantEventWith(
30 delayState: DelayState,
31 block: () -> Unit,
32) {
33 if (delayState.isDelayed) {
34 delayState.isDelayed = false
35 block()
36 }
37}
38
39/**
40 * Composable関数で直接使う場合
41 */
42@Composable
43fun dropRedundantEvent(
44 intervalTimeMillis: Long = 500L,
45 delayState: DelayState = rememberDelayState(intervalTimeMillis = intervalTimeMillis),
46 block: () -> Unit,
47): () -> Unit {
48 return {
49 if (delayState.isDelayed) {
50 delayState.isDelayed = false
51 block()
52 }
53 }
54}
以下のように使います。
1// Composable関数内
2Button(
3 onClick = dropRedundantEvent {
4 // 0.5秒以内のイベントは無視する
5 viewModel.onClickbutton()
6 }
7)
DelayStateを使えば、少し複雑な制御もできます。
1// Composable関数内
2val delayState = rememberDelayState()
3// Button1
4Button(
5 onClick = dropRedundantEvent(delayState = delayState) {
6 // Button1の押下直後は、押下イベントを無視する
7 // ただし、Button2の押下直後はイベントを無視しない
8 viewModel.onClickbutton1()
9 }
10)
11// Button2
12Button(
13 onClick = {
14 // Button1の押下直後はButton2の押下イベントも無視する
15 // ただし、Button2の押下直後はイベントを無視しない
16 if (delayState.isDelayed) {
17 viewModel.onClickbutton2()
18 }
19 }
20)
dropRedundantEventWith()は
Composable関数を使わない場合用です
1// Composable関数内
2val delayState = rememberDelayState()
3checkBoxUiStates.forEach {
4 CheckBoxRow(
5 // 引数があるラムダなど、Composable関数外から呼びたくなることもあるので
6 onClick = { id ->
7 dropRedundantEventWith(delayState = delayState) {
8 viewModel.onClickCheckButton(id)
9 }
10 }
11 )
12}
ライトコードでは、エンジニアを積極採用中!
ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。
採用情報へ