用 “餐厅故事” 读懂 Android VIPER 架构

67 阅读6分钟

把 VIPER 的 5 个组件类比成餐厅里的岗位,一下子就能把 “各司其职” 的核心逻辑讲透。咱们就从 “你去餐厅点一份番茄炒蛋” 这个场景入手,一步步拆透 VIPER 的实现原理。

一、先搞懂:VIPER = 餐厅里的 5 个核心角色

VIPER 是5 个组件的分工协作模式,每个组件只干自己的活,就像餐厅里没人既当服务员又当厨师。先记住这 5 个 “岗位” 对应的职责:

VIPER 组件餐厅角色核心职责(Android 场景)
View服务员只和用户互动:显示界面、接收点击(比如 “点单按钮”),不做任何逻辑处理
Presenter领班中间调度员:接收 View 的请求,转发给 Interactor,再把结果回传给 View
Interactor后厨厨师干脏活累活:处理业务逻辑(比如 “查番茄炒蛋的菜谱”“算价格”)、操作数据
Entity食材 / 菜品纯数据模型:只存数据(比如番茄炒蛋的名字、食材、价格),没有任何逻辑
Router门童 / 引路员负责跳转:处理页面间的导航(比如 “从点餐页跳转到菜品详情页”)

二、实战:用 “点番茄炒蛋” 拆透调用流程 + 代码

咱们以 “用户在 APP 点餐页点击‘点番茄炒蛋’,最终显示菜品详情” 为例,一步步看每个组件怎么配合。

1. 第一步:定义 “食材”——Entity(纯数据类)

Entity 是最单纯的,只存数据,像餐厅里的 “番茄炒蛋” 这个菜品本身,只包含名字、食材、价格这些属性。

// Entity:纯数据模型,无任何逻辑
data class DishEntity(
    val name: String,       // 菜名:番茄炒蛋
    val ingredients: String,// 食材:番茄2个+鸡蛋3个
    val price: Float        // 价格:28元
)

2. 第二步:服务员待命 ——View(Activity/Fragment)

View 就像服务员,只做两件事:显示界面把用户的需求传给领班(Presenter) 。它不知道 “番茄炒蛋怎么做”,也不知道 “价格怎么算”。

// View:只负责UI交互,不做逻辑
class OrderActivity : AppCompatActivity(), OrderContract.View {
    // View持有Presenter的引用(服务员找得到领班)
    private lateinit var presenter: OrderContract.Presenter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_order)

        // 初始化Presenter(服务员上岗前找到领班)
        presenter = OrderPresenter(this)

        // 1. 用户点击“点番茄炒蛋”按钮(接收用户操作)
        btn_order_egg_tomato.setOnClickListener {
            // 2. 把需求传给Presenter,自己啥也不干
            presenter.handleOrderDish("番茄炒蛋")
        }
    }

    // 4. 接收Presenter的结果,更新UI(领班让服务员给用户看菜单)
    override fun showDishDetail(dish: DishEntity) {
        tv_dish_name.text = dish.name
        tv_dish_ingredients.text = "食材:${dish.ingredients}"
        tv_dish_price.text = "价格:${dish.price}元"
    }

    // 跳转页面(由Router处理,View只负责触发)
    override fun jumpToDetailPage(dish: DishEntity) {
        OrderRouter.jumpToDetail(this, dish)
    }
}

// (关键)View和Presenter的协议:定义View能做的事(避免直接依赖)
interface OrderContract.View {
    fun showDishDetail(dish: DishEntity) // 显示菜品详情
    fun jumpToDetailPage(dish: DishEntity) // 跳转到详情页
}

3. 第三步:领班调度 ——Presenter(中间协调者)

Presenter 是 “总指挥”,它既懂 View 的需求,也知道该找哪个 Interactor 干活。它不自己做具体逻辑,只负责 “转发” 和 “回调”。

// Presenter:中间调度,不做具体逻辑
class OrderPresenter(
    private val view: OrderContract.View // 持有View引用,方便回调
) : OrderContract.Presenter, DishInteractor.Callback {

    // Presenter持有Interactor引用(领班找得到后厨)
    private val dishInteractor = DishInteractor()

    // 2. 接收View的请求(服务员告诉领班:用户要点番茄炒蛋)
    override fun handleOrderDish(dishName: String) {
        // 3. 转发给Interactor,让后厨查这个菜的信息
        dishInteractor.getDishInfo(dishName, this)
    }

    // 5. 接收Interactor的结果(后厨把菜谱给领班)
    override fun onDishFound(dish: DishEntity) {
        // 6. 把结果回传给View,让服务员展示给用户
        view.showDishDetail(dish)
        // 同时让Router跳转详情页(领班让门童引路)
        view.jumpToDetailPage(dish)
    }

    // 处理“菜没找到”的情况
    override fun onDishNotFound() {
        view.showError("不好意思,这个菜卖完了~")
    }
}

// Presenter的协议:定义Presenter能处理的请求
interface OrderContract.Presenter {
    fun handleOrderDish(dishName: String) // 处理点单请求
}

4. 第四步:后厨干活 ——Interactor(业务逻辑处理)

Interactor 是 “打工人”,负责所有脏活累活:查数据、算价格、调用接口等。它不知道 View 的存在,只通过回调把结果告诉 Presenter。

// Interactor:处理业务逻辑,操作数据
class DishInteractor {
    // 3. 接收Presenter的请求(领班让后厨查番茄炒蛋的信息)
    fun getDishInfo(dishName: String, callback: Callback) {
        // 模拟“从服务器/数据库查数据”的耗时操作(后厨备菜需要时间)
        Thread {
            Thread.sleep(1000) // 模拟网络延迟

            // 业务逻辑:判断是不是有这个菜
            if (dishName == "番茄炒蛋") {
                // 4. 查到数据,通过回调告诉Presenter(后厨把菜给领班)
                val tomatoEgg = DishEntity(
                    name = "番茄炒蛋",
                    ingredients = "番茄2个+鸡蛋3个+盐1勺",
                    price = 28.0f
                )
                callback.onDishFound(tomatoEgg)
            } else {
                callback.onDishNotFound()
            }
        }.start()
    }

    // Interactor和Presenter的回调协议:定义结果怎么传
    interface Callback {
        fun onDishFound(dish: DishEntity) // 找到菜品
        fun onDishNotFound()             // 没找到菜品
    }
}

5. 第五步:引路 ——Router(页面跳转)

Router 只负责 “跳转”,像门童一样,知道从哪个页面到哪个页面该走哪条路。它不处理任何业务逻辑。

// Router:只负责页面跳转
object OrderRouter {
    // 7. 接收View的跳转请求(门童接到引路需求)
    fun jumpToDetail(context: Context, dish: DishEntity) {
        // 跳转到详情页,并传递数据
        val intent = Intent(context, DishDetailActivity::class.java)
        intent.putExtra("DISH_INFO", dish) // 传递DishEntity(需实现Parcelable)
        context.startActivity(intent)
    }
}

// 注意:DishEntity需要实现Parcelable才能传递,修改一下Entity:
data class DishEntity(
    val name: String,
    val ingredients: String,
    val price: Float
) : Parcelable { // 新增Parcelable实现,用于页面间传值
    // (自动生成的Parcelable代码,Android Studio可自动生成)
    constructor(parcel: Parcel) : this(
        parcel.readString() ?: "",
        parcel.readString() ?: "",
        parcel.readFloat()
    )

    override fun writeToParcel(parcel: Parcel, flags: Int) {
        parcel.writeString(name)
        parcel.writeString(ingredients)
        parcel.writeFloat(price)
    }

    override fun describeContents(): Int = 0

    companion object CREATOR : Parcelable.Creator<DishEntity> {
        override fun createFromParcel(parcel: Parcel): DishEntity = DishEntity(parcel)
        override fun newArray(size: Int): Array<DishEntity?> = arrayOfNulls(size)
    }
}

三、时序图:一眼看清整个调用流程

用 mermaid 时序图把上面的 7 步串起来,谁先谁后一目了然:

VIPER.png

Router(OrderRouter)Entity(DishEntity)Interactor(DishInteractor)Presenter(OrderPresenter)View(OrderActivity)Router(OrderRouter)Entity(DishEntity)Interactor(DishInteractor)Presenter(OrderPresenter)View(OrderActivity)用户点击“点番茄炒蛋”按钮调用handleOrderDish("番茄炒蛋")调用getDishInfo("番茄炒蛋", 回调)模拟网络请求查菜品信息构造DishEntity(番茄炒蛋数据)调用onDishFound(dish)调用showDishDetail(dish)调用jumpToDetail(dish)跳转到DishDetailActivity

四、VIPER 的核心优势:为什么要这么麻烦?

可能你会问:“直接在 Activity 里查数据、更 UI 不更简单吗?” 这就像餐厅里 “服务员自己做菜”—— 短期简单,但人多了就乱套了。VIPER 的优势在于:

  1. 完全解耦:改 UI(换服务员)不影响业务逻辑(后厨),改逻辑不影响 UI;
  2. 易测试:比如想测试 “没找到菜” 的场景,直接调用 Interactor 的onDishNotFound,不用点手机;
  3. 易维护:出问题了能快速定位 ——UI 不显示找 View,逻辑错了找 Interactor,跳转错了找 Router。

不过缺点也很明显:

  1. 样板代码多 (Boilerplate Code) :需要创建很多类和接口,对于小功能来说显得臃肿。
  2. 学习曲线陡峭 (Steep Learning Curve) :对于新手来说,理解所有组件的职责和通信方式需要一些时间。