You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android Jetpack Compose中如何获取闲置/无活动时长?最佳实践咨询

Compose风格闲置计时器(自动登出)最佳实践

在Compose中实现闲置计时器,核心是结合声明式状态管理、生命周期感知和全局用户事件监听,以下是几种贴近Compose设计理念的实践方案:

一、核心思路

利用ViewModel管理全局闲置状态与计时器逻辑,通过DisposableEffect绑定全局触摸/键盘事件,结合StateFlow实现响应式的闲置时长追踪,同时监听App生命周期避免后台误触发。

二、具体实现方案

1. 基础版:ViewModel + 全局触摸监听

步骤1:创建IdleTimerViewModel

负责管理闲置时长、计时器逻辑和登出触发:

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch

class IdleTimerViewModel(
    private val idleThreshold: Long = 1800000L, // 默认30分钟
    private val updateInterval: Long = 1000L // 每秒更新一次
) : ViewModel() {
    private val _idleDuration = MutableStateFlow(0L)
    val idleDuration: StateFlow<Long> = _idleDuration

    private var isTimerActive = true

    init {
        startTimerLoop()
    }

    // 重置闲置计时器
    fun resetIdleState() {
        _idleDuration.value = 0L
    }

    // 暂停/恢复计时器(用于后台/前台切换)
    fun setTimerActive(active: Boolean) {
        isTimerActive = active
    }

    private fun startTimerLoop() {
        viewModelScope.launch {
            while (true) {
                delay(updateInterval)
                if (isTimerActive) {
                    val newDuration = _idleDuration.value + updateInterval
                    _idleDuration.value = newDuration
                    if (newDuration >= idleThreshold) {
                        triggerAutoLogout()
                    }
                }
            }
        }
    }

    private fun triggerAutoLogout() {
        // 这里实现你的登出逻辑:清除Token、导航到登录页等
        // 建议通过外部回调传递,避免ViewModel依赖UI组件
    }
}

步骤2:封装根Composable绑定全局事件

在App的根Composable中注册全局触摸/键盘事件,并绑定ViewModel的生命周期:

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.viewmodel.compose.viewModel
import android.view.MotionEvent
import android.view.View

@Composable
fun AppRoot(
    onAutoLogout: () -> Unit,
    content: @Composable () -> Unit
) {
    val idleViewModel: IdleTimerViewModel = viewModel()
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current

    // 监听闲置时长,触发登出回调
    LaunchedEffect(idleViewModel.idleDuration) {
        idleViewModel.idleDuration.collect { duration ->
            if (duration >= idleViewModel.idleThreshold) {
                onAutoLogout()
            }
        }
    }

    // 绑定App生命周期,后台暂停计时器
    DisposableEffect(lifecycleOwner) {
        val observer = object : androidx.lifecycle.LifecycleObserver {
            @androidx.lifecycle.OnLifecycleEvent(androidx.lifecycle.Lifecycle.Event.ON_STOP)
            fun onBackground() {
                idleViewModel.setTimerActive(false)
            }

            @androidx.lifecycle.OnLifecycleEvent(androidx.lifecycle.Lifecycle.Event.ON_START)
            fun onForeground() {
                idleViewModel.setTimerActive(true)
            }
        }
        lifecycleOwner.lifecycle.addObserver(observer)

        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }

    // 注册全局触摸+键盘事件,重置闲置状态
    val rootView = remember { (context as androidx.appcompat.app.AppCompatActivity).window.decorView }
    DisposableEffect(rootView) {
        val touchListener = View.OnTouchListener { _, event ->
            if (event.action == MotionEvent.ACTION_DOWN) {
                idleViewModel.resetIdleState()
            }
            false // 不拦截事件,保证其他视图正常响应
        }

        val keyListener = View.OnKeyListener { _, _, _ ->
            idleViewModel.resetIdleState()
            false
        }

        rootView.setOnTouchListener(touchListener)
        rootView.setOnKeyListener(keyListener)

        onDispose {
            rootView.setOnTouchListener(null)
            rootView.setOnKeyListener(null)
        }
    }

    // 包裹业务内容
    Surface(
        modifier = Modifier.fillMaxSize()
    ) {
        content()
    }
}

步骤3:在App中使用根Composable

@Composable
fun MyApp() {
    AppRoot(
        onAutoLogout = {
            // 执行登出逻辑:清除本地缓存、导航到登录页等
        }
    ) {
        // 你的App主内容,比如NavHost、HomeScreen等
        HomeScreen()
    }
}

2. 进阶版:纯Compose内部事件监听

如果App完全基于Compose开发,无原生View,可使用Modifier.pointerInput监听所有触摸动作(滑动、长按等),替代系统View监听:

import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.pointerInput

Surface(
    modifier = Modifier
        .fillMaxSize()
        .pointerInput(Unit) {
            awaitPointerEventScope {
                while (true) {
                    val event = awaitPointerEvent()
                    // 捕获按压、移动等所有触摸动作
                    if (event.type == PointerEventType.Press || event.type == PointerEventType.Move) {
                        idleViewModel.resetIdleState()
                    }
                }
            }
        }
) {
    content()
}

这种方式更贴合Compose声明式风格,无需依赖系统View API。

三、关键注意事项

  • 生命周期适配:必须监听App前台/后台状态,避免后台运行时触发误登出。
  • 事件覆盖全面:除触摸事件,还要考虑硬件键盘输入、物理按键(如返回键)的触发。
  • 可配置性:将闲置阈值、更新间隔做成可参数化,方便不同场景复用。
  • 解耦设计:登出逻辑通过外部回调传递,ViewModel只负责状态管理,避免依赖UI组件(如NavController)。

内容的提问来源于stack exchange,提问作者ssantos

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 13:48:11