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

MVP模式下传递View引用至Presenter是否为不良实践?求测试优化方案

优化MVP Presenter单元测试的方案选择

Great question—let's break this down to make your Presenter unit tests cleaner while keeping your MVP architecture aligned with best practices.

先分析你的两个思路

思路1:从Presenter获取View引用

  • 这个方案不推荐,原因有几点:
    • 违反MVP核心原则:Presenter应该依赖抽象的View接口,而非具体的Android控件(比如AppCompatTextView)。持有具体控件引用会让Presenter和Android框架深度耦合,测试时还是需要Mock这些控件,根本问题没解决。
    • 违背单一职责:Presenter本应专注业务逻辑,现在还要管理View控件的持有,职责边界变得模糊。

思路2:Presenter仅返回业务逻辑结果(推荐)

这是更符合MVP设计思想的做法,Presenter专注于业务逻辑计算,把UI渲染的工作完全交给View层。

针对你提到的「多View间逻辑处理」的问题,可以用状态驱动UI的方式解决:

  1. 定义一个数据类封装UI状态,把需要操作的多View信息都包含进去:
    data class NumberUiState(
        val displayText: String,
        val shouldShowWarning: Boolean = false // 示例:控制警告View的显示状态
    )
    
  2. Presenter只负责计算这个状态,不碰任何具体控件:
    class MainActivityPresenter(private val view: MainActivityView) {
        fun calculateNumberDisplay(number: Int): NumberUiState {
            val displayText = when {
                number < 0 -> "Not compatible"
                number < 10 -> number.toString()
                else -> "9+"
            }
            return NumberUiState(displayText, number < 0)
        }
    
        fun onCreate() {
            view.showNumber()
        }
    }
    
  3. View层拿到状态后,负责更新对应的所有控件:
    override fun showNumber() {
        val uiState = mainActivityPresenter.calculateNumberDisplay(22)
        numberTV.text = uiState.displayText
        // 同步处理其他View的逻辑
        warningView.visibility = if (uiState.shouldShowWarning) View.VISIBLE else View.GONE
    }
    
  • 这样单元测试Presenter时,只需要验证返回的NumberUiState是否符合预期,完全不需要Mock任何Android控件,测试简洁且独立。

处理异步场景(比如Glide加载图片)

对于Glide这类依赖Android上下文和异步操作的场景,核心原则还是把UI执行逻辑放到View层,Presenter只负责传递业务决策:

  1. 在View接口中定义抽象方法,避免Presenter直接操作ImageView:
    interface MainActivityView {
        fun showNumber()
        fun loadCachedSplashImage(uri: String?)
        fun showLogo()
    }
    
  2. Presenter只做业务判断,调用View接口方法即可:
    class MainActivityPresenter(private val view: MainActivityView) {
        fun showImageAccordingToCache(cachedSplashScreenUri: String?) {
            view.loadCachedSplashImage(cachedSplashScreenUri)
        }
    
        fun onSplashImageLoadFailed() {
            view.showLogo()
            // 这里处理缓存预加载的业务逻辑,比如通知数据层去缓存图片
        }
    }
    
  3. View层(Activity)实现loadCachedSplashImage,写Glide的具体逻辑:
    override fun loadCachedSplashImage(uri: String?) {
        Glide.with(this)
            .setDefaultRequestOptions(defaultDiskStrategy().onlyRetrieveFromCache(true))
            .load(uri)
            .listener(object : RequestListener<Drawable> {
                override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?, isFirstResource: Boolean): Boolean {
                    mainActivityPresenter.onSplashImageLoadFailed()
                    return true
                }
                override fun onResourceReady(resource: Drawable?, model: Any?, target: Target<Drawable>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
                    return false
                }
            })
            .into(mainImageView)
    }
    
  • 单元测试Presenter时,只需要验证:
    • 传入cachedSplashScreenUri时,是否调用了view.loadCachedSplashImage(uri)
    • 图片加载失败逻辑触发时,是否调用了view.showLogo()
      完全不需要处理Glide的异步逻辑和Android上下文依赖。

总结

  • 优先选择思路2,让Presenter专注业务逻辑,View负责UI渲染,通过状态类处理多View逻辑。
  • 异步/Android框架相关操作(如Glide),通过View接口抽象,Presenter只做业务决策,不直接操作控件。
  • 这样的设计既符合MVP架构原则,又能让Presenter的单元测试变得简单、独立,完全脱离Android环境运行。

内容的提问来源于stack exchange,提问作者Rafael Ruiz Muñoz

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.29 06:45:59