fix(base): 修复Loading弹窗管理和超时机制

- 添加了Loading超时自动关闭功能,默认15秒超时
- 实现了Activity销毁时只关闭自己的Loading,防止误关其他页面
- 优化了Loading重复显示时的节流处理,避免ANR问题
- 在BaseActivity中增加了showLoading方法的超时参数支持
- 为图片上传等长时间操作添加了60秒超时兜底机制
- 增加了相关日志便于调试Loading的显示和关闭流程
This commit is contained in:
2026-05-18 18:11:26 +08:00
parent e570707216
commit aa539d87d0
4 changed files with 85 additions and 18 deletions
@@ -5,6 +5,7 @@ import android.content.pm.ActivityInfo
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.View
import android.widget.FrameLayout
import android.widget.ImageView
@@ -37,6 +38,7 @@ import kotlin.getValue
open class BaseActivity : AppCompatActivity() {
companion object {
const val TAG = "BaseActivity"
/** 日期时间格式化器,复用避免每秒重复创建 */
private val DATE_FORMATTER =
DateTimeFormatter.ofPattern("yyyy年MM月dd日 EEEE***HH:mm:ss", Locale.CHINA)
@@ -71,7 +73,8 @@ open class BaseActivity : AppCompatActivity() {
override fun onDestroy() {
handler.removeCallbacksAndMessages(null)
dismissLoading()
// 只关闭自己弹出的 Loading,防止误关其他 Activity 的 Loading
Loading.dismissIfOwnedBy(this)
super.onDestroy()
ActivityManager.removeActivity(this)
}
@@ -150,22 +153,25 @@ open class BaseActivity : AppCompatActivity() {
handler.removeCallbacks(updateTask)
}
fun showLoading(msg: String = "加载中……") {
fun showLoading(msg: String = "加载中……", timeoutMs: Long = Loading.DEFAULT_TIMEOUT_MS) {
handler.post {
Loading.show(
context = this,
message = msg
message = msg,
timeoutMs = timeoutMs
)
}
}
fun dismissLoading() {
Log.d(TAG, "dismissLoading, ${this.javaClass.simpleName}")
handler.post {
Loading.dismiss()
}
}
fun delayDismissLoading() {
Log.d(TAG, "delayDismissLoading, ${this.javaClass.simpleName}")
handler.postDelayed({
Loading.dismiss()
}, 500)
@@ -1,17 +1,28 @@
package com.shuwei.dish.match.dialog
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.Context
import android.graphics.Color
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.WindowManager
import androidx.core.graphics.drawable.toDrawable
import com.shuwei.dish.match.R
import com.shuwei.dish.match.databinding.DialogLoadingBinding
@SuppressLint("StaticFieldLeak")
object Loading {
/** 默认超时兜底时长(毫秒):业务方未传值时使用 */
const val DEFAULT_TIMEOUT_MS = 15_000L
private var dialog: LoadingDialog? = null
/** 当前持有 Loading 的 context,用于隔离 Activity 销毁时的误关 */
private var owner: Context? = null
private val timeoutHandler = Handler(Looper.getMainLooper())
private val timeoutRunnable = Runnable { dismiss() }
fun isShowing(): Boolean {
return dialog != null && dialog!!.isShowing
@@ -22,30 +33,71 @@ object Loading {
dialog!!.updateContent(content)
}
fun show(context: Context, message: String = "加载中……", onDismiss: () -> Unit = {}) {
/**
* 显示 Loading 弹窗
*
* @param context 上下文
* @param message 提示文案
* @param timeoutMs 超时兜底时长(毫秒):到期自动 dismiss;传 0 关闭兜底,由业务方自己负责
* @param onDismiss 关闭回调
*/
fun show(
context: Context,
message: String = "加载中……",
timeoutMs: Long = DEFAULT_TIMEOUT_MS,
onDismiss: () -> Unit = {}
) {
try {
// 节流:已显示中只更新文案 + 重置超时,不重新 attach 窗口
// 避免反复触发 FocusEvent 在主线程繁忙时引发 ANR
if (dialog?.isShowing == true) {
dialog?.dismiss()
}
if (dialog == null) {
dialog = LoadingDialog(context = context, message = message, onDismiss = onDismiss)
dialog?.updateContent(message)
owner = context // 同一弹窗最后一次 show 决定 owner
resetTimeout(timeoutMs)
return
}
dialog = LoadingDialog(context = context, message = message, onDismiss = onDismiss)
owner = context
dialog?.show()
resetTimeout(timeoutMs)
} catch (e: Exception) {
e.printStackTrace()
}
}
fun dismiss() {
timeoutHandler.removeCallbacks(timeoutRunnable)
try {
dialog?.let {
if (it.isShowing) {
it.dismiss()
}
}
dialog = null
} catch (e: Exception) {
e.printStackTrace()
} finally {
dialog = null
owner = null
}
}
/**
* Activity 销毁专用:只关闭自己持有的 Loading,防止跨 Activity 误关
* 单例 Loading 在多 Activity 共享时,若每个 Activity 的 onDestroy 都无脑 dismiss
* 会误关后启动 Activity 弹出的 Loading;由本方法做 owner 校验后才允许关闭
*/
fun dismissIfOwnedBy(context: Context) {
if (owner === context) dismiss()
}
/**
* 重置超时兜底定时器
* @param timeoutMs <=0 表示不安排定时器(关闭兜底)
*/
private fun resetTimeout(timeoutMs: Long) {
timeoutHandler.removeCallbacks(timeoutRunnable)
if (timeoutMs > 0) {
timeoutHandler.postDelayed(timeoutRunnable, timeoutMs)
}
}
@@ -145,6 +145,8 @@ class FoodListFragment : BaseFragment<FragmentFoodListBinding>() {
override fun onResume() {
super.onResume()
Log.d(TAG, "onResume, dinnerType=$dinnerType, isVisible=${isVisible}, isHidden=${isHidden}")
if (isHidden) return // 过滤 Activity 回前台时隐藏 tab 的无效触发
runCatching {
pageNo = 1
getDishList(currentFoodName)
@@ -226,16 +228,19 @@ class FoodListFragment : BaseFragment<FragmentFoodListBinding>() {
val localList = if (pageNo == 1) pendingLocalList else null
netViewModel.searchFoodListWithCallback(
param = param,
onLoading = { activity.showLoading() }
onLoading = { activity.showLoading(timeoutMs = 35_000L) }
) { result ->
when (result) {
is UiState.Success -> {
if (isAdded.not()) return@searchFoodListWithCallback
Log.d(TAG, "getDishList, UiState.Success")
activity.delayDismissLoading()
finishRefresh()
loadAndMergeDishList(result.data, localList)
}
is UiState.Error -> {
if (isAdded.not()) return@searchFoodListWithCallback
Log.d(TAG, "getDishList, UiState.Error")
toast(result.msg)
finishRefresh()
activity.delayDismissLoading()
@@ -334,7 +334,9 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
private fun upload() {
lifecycleScope.launch {
val totalFileCount = vectorList.count { it.imageFile != null }
showWaitingDialog("图片上传中0/$totalFileCount")
// 上传场景单批可能耗时较长(含图片传输 + 网络往返),用 60s 超时兜底
// 每次 onProgress 都会重新调用 showWaitingDialog,会重置定时器,正常进度不会触发兜底
showWaitingDialog("图片上传中0/$totalFileCount", timeoutMs = 60_000L)
val params = mutableMapOf<String, RequestBody>()
params["goodsId"] = checkedItem!!.goodsId.toRequestBody()
params["goodsName"] = checkedItem!!.goodsName!!.toRequestBody()
@@ -358,7 +360,7 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
log("uploadImage返回:result=${result.toJsonString()}")
if (result is UiState.Success) result.data else null
}, onProgress = { count, batch, idList ->
showWaitingDialog("图片上传中$count/$totalFileCount")
showWaitingDialog("图片上传中$count/$totalFileCount", timeoutMs = 60_000L)
val foodList = batch.mapIndexed { index, it ->
val name = checkedItem!!.goodsName + "&&" + checkedItem!!.rawMaterialsType
Food(
@@ -507,12 +509,14 @@ class VectorCollectionFragment : BaseFragment<FragmentVectorCollectionBinding>()
currentActivity.dismissLoading()
}
private fun showWaitingDialog(msg: String) {
if (Loading.isShowing()) {
Loading.updateContent(msg)
return
}
currentActivity.showLoading(msg)
/**
* 显示等待弹窗
* @param msg 提示文案
* @param timeoutMs 超时兜底时长(毫秒):到期自动关闭,传 0 关闭兜底
* Loading 内部已做节流,重复调用只会更新文案 + 重置超时定时器
*/
private fun showWaitingDialog(msg: String, timeoutMs: Long = Loading.DEFAULT_TIMEOUT_MS) {
currentActivity.showLoading(msg, timeoutMs)
}
private lateinit var previewView: PreviewView