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层完全解耦。
- 修改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对应的业务逻辑即可 } }
- 优化自定义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() } } }
- 在承载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
相关产品推荐
相关产品推荐

