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

Hilt中如何向ViewModel提供Activity/Application上下文解决弹窗报错

问题根因

你遇到的Unable to add window -- token null is not valid; is your activity running?报错是两个核心问题导致的:

  • 架构违反设计规范:ViewModel的定位是持有界面数据、处理业务逻辑,绝对不能持有任何UI层引用(Activity、Dialog、View等),更不能直接执行弹出Dialog、操作控件这类UI操作,否则不仅会造成内存泄漏,还会出现生命周期不匹配的崩溃。
  • Context类型使用错误:你通过@ApplicationContext注入的是全局Application级上下文,这个上下文没有关联当前前台活跃的Activity窗口,而Dialog必须挂载到Activity的窗口上才能显示,Application上下文没有对应的窗口token,自然会报无有效token的错误。
正确实现方案

核心原则是:所有UI操作(弹Dialog、弹Toast、操作View)全部放在Activity/Fragment层处理,ViewModel只负责发送UI事件、处理回调后的业务逻辑,和UI层完全解耦。

  1. 修改ViewModel代码,移除Context注入和Dialog相关逻辑,改用事件流通知UI层:
@HiltViewModel
class SetGoalsViewModel @Inject constructor() : ViewModel() {
    // 用SharedFlow发送一次性UI事件,避免屏幕旋转等场景下事件重复触发
    private val _showQuestionDialog = MutableSharedFlow<Int>(extraBufferCapacity = 1)
    val showQuestionDialog: SharedFlow<Int> = _showQuestionDialog

    fun onItemButtonQuestionClicked(id: Int) {
        // 仅发送事件,不做任何UI操作
        _showQuestionDialog.tryEmit(id)
    }

    // 处理Dialog点击后的业务逻辑,和UI完全解耦
    fun onDialogAction(id: Int, response: CustomSetGoalsDialog.ResponseType) {
        // 在这里写点击YES/NO/CANCEL对应的业务逻辑即可
    }
}
  1. 优化自定义Dialog代码,移除冗余的Builder创建逻辑和多余的业务参数耦合:
class CustomSetGoalsDialog(context: Context) : AlertDialog.Builder(context) {
    enum class ResponseType {
        YES, NO, CANCEL
    }

    fun show(
        title: String,
        message: String,
        resultListener: (ResponseType) -> Unit
    ): AlertDialog {
        setTitle(title)
        setMessage(message)
        setIcon(android.R.drawable.ic_dialog_alert)

        setPositiveButton("Yes") { _, _ -> resultListener(ResponseType.YES) }
        setNegativeButton("No") { _, _ -> resultListener(ResponseType.NO) }
        setNeutralButton("Cancel") { _, _ -> resultListener(ResponseType.CANCEL) }

        return create().apply {
            setCancelable(false)
            show()
        }
    }
}
  1. 在承载RecyclerView的Activity/Fragment中收集事件,使用当前Activity实例作为Context弹出Dialog:
@AndroidEntryPoint
class SetGoalsActivity : AppCompatActivity() {
    private val viewModel: SetGoalsViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 省略setContentView、RecyclerView初始化等常规逻辑

        // 初始化Adapter,点击事件直接调用ViewModel方法
        val goalsAdapter = SetGoalsAdapter(
            onItemDeleteClicked = { /* 对应删除逻辑 */ },
            onItemRootClicked = { /* 对应条目点击逻辑 */ },
            onButtonClickedListener = { itemId -> viewModel.onItemButtonQuestionClicked(itemId) }
        )

        // 监听ViewModel的UI事件,在生命周期活跃状态下执行UI操作
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.showQuestionDialog.collect { itemId ->
                    // 用当前Activity作为Context,属于活跃的窗口载体,不会出现token错误
                    CustomSetGoalsDialog(this@SetGoalsActivity).show(
                        title = "TESTTITLE",
                        message = "ARE YOU SURE ABOUT IT?"
                    ) { response ->
                        Toast.makeText(this@SetGoalsActivity, response.toString(), Toast.LENGTH_LONG).show()
                        // 把用户选择传回ViewModel处理业务
                        viewModel.onDialogAction(itemId, response)
                    }
                }
            }
        }
    }
}
Hilt中Context注入的正确规则
  • Application上下文:需要全局单例上下文的场景(比如数据库初始化、全局资源获取、不依赖窗口的工具类初始化),直接在构造函数中用@ApplicationContext标注Context参数即可注入,这个上下文生命周期和应用一致,不会造成泄漏,但绝对不能用来弹出Dialog、直接启动Activity。
  • Activity上下文:永远不要把Activity上下文注入到ViewModel中。ViewModel生命周期长于Activity,持有Activity引用会导致Activity销毁后无法被回收,引发内存泄漏。需要使用Activity上下文的UI操作,逻辑必须写在Activity/Fragment本身,通过ViewModel的事件驱动执行。
  • 特殊场景下非UI类需要Activity上下文时,由UI层调用方法时作为参数传入即可,不要直接注入到长生命周期类中。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 23:45:45