RecyclerView点击弹出BottomSheetDialog报BadTokenException异常
异常产生原因
这个WindowManager$BadTokenException的核心触发原因有两个:
- 你传入Dialog的
mContext不具备有效的窗口Token:如果mContext是Application级别的上下文,或者是Adapter持有的已经被销毁的旧Activity实例,本身没有可挂载弹窗的活跃窗口,调用show()时就会报token无效的错误。 - 缺少生命周期校验:点击事件触发、Dialog执行show方法时,宿主Activity可能已经被销毁(比如用户切后台、退出页面、屏幕旋转重建),此时窗口已经被系统回收,不允许再添加新的弹窗。
另外你当前用原生Dialog手动设置Gravity模拟底部弹窗的写法不是官方推荐方案,存在样式不统一、手势交互缺失、兼容性差的问题,同时Firebase异步请求没有绑定生命周期,回调时如果页面销毁还会触发View更新的内存泄漏/崩溃问题。
正确实现方案
前置准备
确保项目已经引入Material组件依赖,直接使用官方封装好的BottomSheetDialog,不需要手动修改Window属性实现底部弹出效果,默认自带底部对齐、手势拖拽关闭、圆角等标准交互。
实现步骤
- 不要在Adapter中直接处理弹窗逻辑:Adapter只负责绑定列表数据,点击事件通过回调抛给宿主Activity/Fragment处理,避免Adapter长期持有Context导致的内存泄漏、生命周期不一致问题。
Adapter中点击逻辑修改参考:class PostAdapter(...) : RecyclerView.Adapter<PostAdapter.ViewHolder>() { // 定义点击回调 private var onPublisherClick: ((publisherId: String) -> Unit)? = null fun setPublisherClickListener(listener: (String) -> Unit) { onPublisherClick = listener } override fun onBindViewHolder(holder: ViewHolder, position: Int) { val post = currentList[position] // 其他绑定逻辑... holder.publisher.setOnClickListener { onPublisherClick?.invoke(post.publisher) } } // 其他Adapter逻辑... } - 在宿主Activity/Fragment中设置Adapter点击监听,实现弹窗逻辑,增加生命周期校验,避免在页面销毁时操作UI:
正确的// 在Activity中设置监听 postAdapter.setPublisherClickListener { publisherId -> openBottomSheet(publisherId) }openBottomSheet实现:private fun openBottomSheet(publisher: String) { // 第一步先校验Activity状态,已销毁直接返回不弹弹窗 if (isFinishing || isDestroyed) return // 初始化官方BottomSheetDialog,传入当前Activity作为Context val bottomSheetDialog = BottomSheetDialog(this) val contentView = LayoutInflater.from(this).inflate(R.layout.bottom_sheet_dialog, null) // 初始化弹窗内的View val uImage = contentView.findViewById<ImageView>(R.id.sheetIvProfileImage) val uNumber = contentView.findViewById<TextView>(R.id.sheetTvOwnerNumber) val uEmail = contentView.findViewById<TextView>(R.id.tvOwnerEmail) val uAbout = contentView.findViewById<TextView>(R.id.tvOwnerAbout) val uTitle = contentView.findViewById<TextView>(R.id.sheetTvOwnerTitle) // 配置弹窗属性并显示 bottomSheetDialog.apply { setContentView(contentView) setCancelable(true) // 弹窗消失时清理图片加载请求,避免内存泄漏 setOnDismissListener { Glide.with(this@YourActivityName).clear(uImage) } show() } // 发起Firebase数据请求 val ownersRef = FirebaseFirestore.getInstance().collection("owners").document(publisher) ownersRef.get() .addOnSuccessListener { document -> // 异步回调先校验页面、弹窗状态,已销毁就不更新UI if (isFinishing || isDestroyed || !bottomSheetDialog.isShowing) return@addOnSuccessListener val ownerModel = document.toObject(Owner::class.java) ?: return@addOnSuccessListener Glide.with(this) .load(ownerModel.profileImage) .into(uImage) uTitle.text = ownerModel.username uNumber.text = ownerModel.phoneNumber uEmail.text = ownerModel.email uAbout.text = ownerModel.about } .addOnFailureListener { // 请求失败关闭弹窗给出提示 if (bottomSheetDialog.isShowing) bottomSheetDialog.dismiss() Toast.makeText(this, "获取用户信息失败,请重试", Toast.LENGTH_SHORT).show() } }
关键注意点
- 永远不要用Application级别的Context弹Dialog/弹窗,必须传入当前活跃的Activity实例,只有Activity持有有效的窗口Token。
- 所有异步回调(网络请求、数据库查询、延迟任务)执行UI操作前,必须校验页面/组件的生命周期状态,避免操作已销毁的组件。
- 优先使用官方封装的
BottomSheetDialog实现底部弹窗,不要手动修改原生Dialog的Window属性模拟,减少兼容性问题。
内容的提问来源于stack exchange,提问作者Sam Kabiru
相关产品推荐
相关产品推荐

