一、MVP 架构概述 1.1 Android MVC 的困境 在 Android 开发的早期,开发者通常将 Activity/Fragment 既当作 View(处理 UI 渲染)又当作 Controller(处理业务逻辑),这种模式被称作”上帝对象”(God Object)。以下是一段典型的早期 Android 代码:
public class LoginActivity extends AppCompatActivity { private EditText etUsername, etPassword; private Button btnLogin; private ProgressBar progressBar; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_login); etUsername = findViewById(R.id.et_username); etPassword = findViewById(R.id.et_password); btnLogin = findViewById(R.id.btn_login); progressBar = findViewById(R.id.progress); btnLogin.setOnClickListener(v -> { String username = etUsername.getText().toString(); String password = etPassword.getText().toString(); if (TextUtils.isEmpty(username)) { etUsername.setError("用户名不能为空" ); return ; } progressBar.setVisibility(View.VISIBLE); new Thread (() -> { try { String result = ApiClient.login(username, password); runOnUiThread(() -> { progressBar.setVisibility(View.GONE); if (result != null ) { startActivity(new Intent (this , HomeActivity.class)); } else { Toast.makeText(this , "登录失败" , Toast.LENGTH_SHORT).show(); } }); } catch (Exception e) { runOnUiThread(() -> { progressBar.setVisibility(View.GONE); Toast.makeText(this , "网络异常" , Toast.LENGTH_SHORT).show(); }); } }).start(); }); } }
这种写法的核心问题:
View 和业务逻辑耦合 :UI 代码和业务逻辑混在一起,修改任一都牵动全局
无法单元测试 :所有代码依赖 Android Framework(Activity、View),必须用 Instrumentation Test
代码难以复用 :同样的登录逻辑在其他页面无法重用
违反单一职责原则 :Activity 同时处理 UI 渲染、输入验证、网络请求、异常处理
1.2 MVP 的核心思想 MVP(Model-View-Presenter)将这三部分职责严格分离:
Model :数据层,负责数据的获取、存储和业务逻辑。包括网络请求、数据库操作、文件读写。Model 对 View 和 Presenter 完全无感知。
View :纯 UI 层(Passive View),只负责显示数据和转发用户操作给 Presenter。View 永远不直接访问 Model。
Presenter :中间人(Mediator),从 Model 获取数据,处理后调用 View 的接口更新 UI。Presenter 持有 View 引用和 Model 引用。
┌──────────┐ ┌──────────────┐ ┌──────────┐ │ View │ ───────> │ Presenter │ ───────> │ Model │ │(Activity)│ <─────── │ │ <─────── │(Repository) └──────────┘ └──────────────┘ └──────────┘ 用户交互 业务逻辑 数据获取 只调用 Presenter 调用 View 接口更新 UI 网络/数据库
1.3 Contract 接口契约模式 MVP 最经典的实现方式是通过 Contract 接口 定义 View 和 Presenter 的通信协议:
public interface LoginContract { interface View { void showLoading () ; void hideLoading () ; void showError (String message) ; void showLoginSuccess () ; void navigateToHome () ; void setLoginButtonEnabled (boolean enabled) ; } interface Presenter { void attach (View view) ; void detach () ; void onUsernameChanged (String username) ; void onPasswordChanged (String password) ; void onLoginClicked () ; } }
Contract 的作用:
明确通信协议 :一眼看清 View 和 Presenter 之间可以互相调用哪些方法
编译时安全 :接口定义后,任何实现偏差都会在编译阶段暴露
方便协作 :团队成员根据 Contract 各自开发 View 实现和 Presenter 实现
二、MVP 的完整实现 2.1 Model 层实现 Model 层负责数据获取,不感知 UI 层的任何存在:
interface UserRepository { suspend fun login (username: String , password: String ) : Result<User> suspend fun getUserProfile (userId: String ) : Result<UserProfile> suspend fun updateProfile (profile: UserProfile ) : Result<Unit > } class UserRepositoryImpl ( private val apiService: ApiService, private val userDao: UserDao, private val cacheManager: CacheManager ) : UserRepository { override suspend fun login (username: String , password: String ) : Result<User> { return try { val response = apiService.login(LoginRequest(username, password)) if (response.isSuccessful && response.body() != null ) { val user = response.body()!! cacheManager.saveToken(user.token) userDao.insertUser(user.toEntity()) Result.success(user) } else { Result.failure(HttpException(response)) } } catch (e: IOException) { Result.failure(e) } } override suspend fun getUserProfile (userId: String ) : Result<UserProfile> { return try { val cached = cacheManager.getProfile(userId) if (cached != null ) { return Result.success(cached) } val response = apiService.getProfile(userId) if (response.isSuccessful && response.body() != null ) { val profile = response.body()!! cacheManager.putProfile(userId, profile) Result.success(profile) } else { Result.failure(HttpException(response)) } } catch (e: Exception) { Result.failure(e) } } }
2.2 Presenter 层实现 Presenter 持有 View 的弱引用(避免内存泄漏),并通过 attach/detach 管理生命周期:
class LoginPresenter ( private val userRepository: UserRepository ) : LoginContract.Presenter { private var viewRef: WeakReference<LoginContract.View>? = null private var job: Job? = null private var currentUsername = "" private var currentPassword = "" override fun attach (view: LoginContract .View ) { viewRef = WeakReference(view) if (currentUsername.isNotEmpty()) { view.onUsernameChanged(currentUsername) } if (currentPassword.isNotEmpty()) { view.onPasswordChanged(currentPassword) } } override fun detach () { viewRef?.clear() viewRef = null job?.cancel() } override fun onUsernameChanged (username: String ) { currentUsername = username validateForm() } override fun onPasswordChanged (password: String ) { currentPassword = password validateForm() } private fun validateForm () { val isUsernameValid = currentUsername.length >= 3 val isPasswordValid = currentPassword.length >= 6 viewRef?.get ()?.setLoginButtonEnabled(isUsernameValid && isPasswordValid) } override fun onLoginClicked () { val view = viewRef?.get () ?: return if (currentUsername.length < 3 ) { view.showError("用户名至少3个字符" ) return } if (currentPassword.length < 6 ) { view.showError("密码至少6个字符" ) return } view.showLoading() job = CoroutineScope(Dispatchers.Main).launch { val result = withContext(Dispatchers.IO) { userRepository.login(currentUsername, currentPassword) } result.fold( onSuccess = { user -> view.hideLoading() view.showLoginSuccess() view.navigateToHome() }, onFailure = { error -> view.hideLoading() val message = when (error) { is IOException -> "网络连接失败,请检查网络" is HttpException -> when (error.code()) { 401 -> "用户名或密码错误" 429 -> "请求过于频繁,请稍后再试" 500 -> "服务器内部错误" else -> "登录失败 (${error.code()} )" } else -> "未知错误: ${error.message} " } view.showError(message) } ) } } }
2.3 View 层实现 —— Passive View View 是 完全被动的 (Passive View):不做任何业务判断,只负责展示和转发事件:
class LoginActivity : AppCompatActivity (), LoginContract.View { private lateinit var presenter: LoginContract.Presenter private lateinit var binding: ActivityLoginBinding override fun onCreate (savedInstanceState: Bundle ?) { super .onCreate(savedInstanceState) binding = ActivityLoginBinding.inflate(layoutInflater) setContentView(binding.root) val repository = (application as MyApp).serviceLocator.userRepository presenter = LoginPresenter(repository) binding.btnLogin.setOnClickListener { presenter.onLoginClicked() } binding.etUsername.doAfterTextChanged { text -> presenter.onUsernameChanged(text?.toString() ?: "" ) } binding.etPassword.doAfterTextChanged { text -> presenter.onPasswordChanged(text?.toString() ?: "" ) } } override fun onStart () { super .onStart() presenter.attach(this ) } override fun onStop () { super .onStop() presenter.detach() } override fun showLoading () { binding.progressBar.isVisible = true binding.btnLogin.isEnabled = false } override fun hideLoading () { binding.progressBar.isVisible = false binding.btnLogin.isEnabled = true } override fun showError (message: String ) { binding.tvError.isVisible = true binding.tvError.text = message } override fun showLoginSuccess () { Toast.makeText(this , "登录成功" , Toast.LENGTH_SHORT).show() } override fun navigateToHome () { startActivity(Intent(this , HomeActivity::class .java)) finish() } override fun setLoginButtonEnabled (enabled: Boolean ) { binding.btnLogin.isEnabled = enabled } }
Passive View 的关键原则:
View 中没有任何 if-else 业务判断
View 不做数据转换、验证、排序
View 的每个方法只对应一个 UI 操作(show/hide/set text)
View 收到用户事件后,立即调用 Presenter,不做任何前置处理
三、MVP 的生命周期与内存管理 3.1 为什么需要 attach/detach Android 的 Activity 生命周期由系统控制,可能随时被销毁重建(屏幕旋转、语言切换、内存不足)。如果 Presenter 持有 Activity 的强引用,当 Activity 被销毁后 Presenter 仍存活(例如正在执行异步任务),会导致:
内存泄漏 :Activity 无法被 GC 回收
空指针崩溃 :异步任务完成后尝试更新已销毁的 Activity
3.2 三种引用管理方案 方案一:弱引用(WeakReference)
class LoginPresenter (repository: UserRepository) { private var viewRef: WeakReference<LoginContract.View>? = null fun attach (view: LoginContract .View ) { viewRef = WeakReference(view) } fun detach () { viewRef?.clear() viewRef = null } private fun getView () : LoginContract.View? = viewRef?.get () } private fun onLoginResult (result: Result <User >) { val view = getView() ?: return view.hideLoading() result.fold( onSuccess = { view.navigateToHome() }, onFailure = { view.showError(it.message ?: "错误" ) } ) }
方案二:取消协程
Kotlin 协程提供了更优雅的方式 —— 配合 viewModelScope 或在 detach 时取消 Job:
class LoginPresenter (repository: UserRepository) { private var job: Job? = null private var view: LoginContract.View? = null fun attach (view: LoginContract .View ) { this .view = view } fun detach () { this .view = null job?.cancel() } fun onLoginClicked () { val v = view ?: return v.showLoading() job = CoroutineScope(Dispatchers.Main).launch { val result = withContext(Dispatchers.IO) { repository.login(v.getUsername(), v.getPassword()) } view?.hideLoading() view?.let { handleResult(it, result) } } } }
方案三:使用 Android Architecture Components —— ViewModel
将 Presenter 的逻辑放入 ViewModel,利用 ViewModel 的生命周期长于 Activity 的特性:
class LoginViewModel ( private val userRepository: UserRepository ) : ViewModel() { private val _viewState = MutableLiveData<LoginViewState>() val viewState: LiveData<LoginViewState> = _viewState fun login (username: String , password: String ) { _viewState.value = LoginViewState.Loading viewModelScope.launch { val result = withContext(Dispatchers.IO) { userRepository.login(username, password) } result.fold( onSuccess = { _viewState.value = LoginViewState.Success }, onFailure = { _viewState.value = LoginViewState.Error(it.message) } ) } } } sealed class LoginViewState { object Loading : LoginViewState() object Success : LoginViewState() data class Error (val message: String?) : LoginViewState() }
这是从 MVP 到 MVVM 的自然过渡 —— Presenter 演变为 ViewModel,直接调用 View 的方法演变为观察 LiveData/StateFlow。
3.3 Process Death 处理 class LoginPresenter ( private val savedState: Bundle?, private var view: LoginContract.View? ) { private var username: String private var password: String init { username = savedState?.getString("username" ) ?: "" password = savedState?.getString("password" ) ?: "" } fun saveState (outState: Bundle ) { outState.putString("username" , username) outState.putString("password" , password) } fun attach (view: LoginContract .View ) { this .view = view if (username.isNotEmpty()) view.onUsernameChanged(username) if (password.isNotEmpty()) view.onPasswordChanged(password) } }
四、MVP 的测试策略 4.1 MVP 测试的核心优势 MVP 最大的测试优势在于:Presenter 不依赖 Android Framework 。测试 Presenter 不需要启动模拟器,不需要 Robolectric,是纯 JVM 单元测试:
class LoginPresenterTest { private lateinit var repository: UserRepository private lateinit var view: LoginContract.View private lateinit var presenter: LoginPresenter @Before fun setup () { repository = mock() view = mock() presenter = LoginPresenter(repository) presenter.attach(view) } @Test fun `valid input enables login button`() { presenter.onUsernameChanged("testuser" ) presenter.onPasswordChanged("password123" ) verify(view).setLoginButtonEnabled(true ) } @Test fun `empty username disables login button`() { presenter.onUsernameChanged("" ) presenter.onPasswordChanged("password123" ) verify(view).setLoginButtonEnabled(false ) } @Test fun `short password disables login button`() { presenter.onUsernameChanged("testuser" ) presenter.onPasswordChanged("123" ) verify(view).setLoginButtonEnabled(false ) } @Test fun `login success navigates to home`() = runTest { whenever(repository.login("testuser" , "password123" )) .thenReturn(Result.success(User("1" , "testuser" ))) presenter.onUsernameChanged("testuser" ) presenter.onPasswordChanged("password123" ) presenter.onLoginClicked() verify(view).showLoading() advanceUntilIdle() verify(view).hideLoading() verify(view).showLoginSuccess() verify(view).navigateToHome() } @Test fun `network error shows error message`() = runTest { whenever(repository.login(any(), any())) .thenReturn(Result.failure(IOException("Network failure" ))) presenter.onUsernameChanged("test" ) presenter.onPasswordChanged("password123" ) presenter.onLoginClicked() verify(view).showLoading() advanceUntilIdle() verify(view).hideLoading() verify(view).showError(contains("网络连接失败" )) } @Test fun `login with 401 shows credential error`() = runTest { val response = mock<Response<User>> { on { code() } doReturn 401 on { isSuccessful } doReturn false } whenever(repository.login(any(), any())) .thenReturn(Result.failure(HttpException(response))) presenter.onUsernameChanged("bad" ) presenter.onPasswordChanged("credentials" ) presenter.onLoginClicked() advanceUntilIdle() verify(view).showError(contains("用户名或密码错误" )) } @Test fun `View detached during request prevents update`() = runTest { whenever(repository.login(any(), any())).thenReturn(Result.success(User("1" , "u" ))) presenter.onUsernameChanged("u" ) presenter.onPasswordChanged("password123" ) presenter.onLoginClicked() presenter.detach() advanceUntilIdle() verify(view, never()).navigateToHome() verify(view, never()).showLoginSuccess() } }
五、MVP 完整实战 —— 用户资料页面 5.1 功能需求 一个完整的用户资料页面,包含:
从网络获取用户资料
头像加载(Glide)
下拉刷新
本地缓存(先显示缓存,再刷新网络)
错误重试
编辑资料
5.2 Contract 定义 interface ProfileContract { data class ProfileViewData ( val avatarUrl: String, val nickname: String, val bio: String, val followerCount: String, val followingCount: String, val postCount: String ) interface View { fun showLoading (isRefresh: Boolean = false ) fun showContent (data : ProfileViewData ) fun showError (message: String , canRetry: Boolean ) fun showNetworkErrorWithCache (cachedData: ProfileViewData ) fun showEditSuccess () fun navigateToEditProfile () fun enableSwipeRefresh (enable: Boolean ) } interface Presenter { fun attach (view: View ) fun detach () fun loadProfile (isRefresh: Boolean = false ) fun onEditClicked () fun onRetryClicked () } }
5.3 Presenter 实现 class ProfilePresenter ( private val userRepository: UserRepository, private val analytics: AnalyticsTracker ) : ProfileContract.Presenter { private var view: ProfileContract.View? = null private var loadJob: Job? = null private var cachedData: ProfileContract.ProfileViewData? = null override fun attach (view: ProfileContract .View ) { this .view = view loadProfile() } override fun detach () { view = null loadJob?.cancel() } override fun loadProfile (isRefresh: Boolean ) { val v = view ?: return if (!isRefresh && cachedData != null ) { v.showContent(cachedData!!) return } v.showLoading(isRefresh) v.enableSwipeRefresh(true ) loadJob = CoroutineScope(Dispatchers.Main).launch { val result = withContext(Dispatchers.IO) { userRepository.getUserProfile("self" ) } val currentView = view ?: return @launch result.fold( onSuccess = { profile -> val viewData = profile.toViewData() cachedData = viewData currentView.showContent(viewData) analytics.logEvent("profile_loaded_success" ) }, onFailure = { error -> if (cachedData != null ) { currentView.showNetworkErrorWithCache(cachedData!!) } else { val message = mapErrorToMessage(error) currentView.showError(message, canRetry = true ) } analytics.logEvent("profile_load_failed" , mapOf("error" to error.message)) } ) currentView.enableSwipeRefresh(false ) } } override fun onEditClicked () { view?.navigateToEditProfile() } override fun onRetryClicked () { loadProfile(isRefresh = true ) } private fun UserProfile.toViewData () = ProfileContract.ProfileViewData( avatarUrl = this .avatar, nickname = this .nickname, bio = this .bio ?: "这个人很懒,什么都没写" , followerCount = formatCount(this .followerCount), followingCount = formatCount(this .followingCount), postCount = formatCount(this .postCount) ) private fun formatCount (count: Long ) : String = when { count >= 10000 -> "${count / 10000 } 万" count >= 1000 -> "${count / 1000 } k" else -> count.toString() } private fun mapErrorToMessage (error: Throwable ) : String = when (error) { is IOException -> "网络连接失败,请检查网络后重试" is HttpException -> when (error.code()) { 404 -> "用户不存在" 401 -> "登录已过期,请重新登录" in 500. .599 -> "服务器繁忙,请稍后重试" else -> "请求失败 (${error.code()} )" } is CancellationException -> "请求已取消" else -> "未知错误: ${error.message} " } }
5.4 View 实现 class ProfileFragment : Fragment (), ProfileContract.View { private lateinit var presenter: ProfileContract.Presenter private var binding: FragmentProfileBinding? = null override fun onCreateView ( inflater: LayoutInflater , container: ViewGroup ?, savedInstanceState: Bundle ? ) : View { binding = FragmentProfileBinding.inflate(inflater, container, false ) return binding!!.root } override fun onViewCreated (view: View , savedInstanceState: Bundle ?) { super .onViewCreated(view, savedInstanceState) val repository = (requireActivity().application as MyApp) .serviceLocator.userRepository val analytics = (requireActivity().application as MyApp) .serviceLocator.analytics presenter = ProfilePresenter(repository, analytics) binding?.apply { swipeRefresh.setOnRefreshListener { presenter.loadProfile(isRefresh = true ) } btnEdit.setOnClickListener { presenter.onEditClicked() } btnRetry.setOnClickListener { presenter.onRetryClicked() } } } override fun onStart () { super .onStart() presenter.attach(this ) } override fun onStop () { super .onStop() presenter.detach() } override fun showLoading (isRefresh: Boolean ) { if (!isRefresh) { binding?.progressBar?.isVisible = true binding?.contentGroup?.isVisible = false } } override fun showContent (data : ProfileContract .ProfileViewData ) { binding?.apply { progressBar.isVisible = false contentGroup.isVisible = true errorGroup.isVisible = false swipeRefresh.isRefreshing = false Glide.with(root) .load(data .avatarUrl) .circleCrop() .into(ivAvatar) tvNickname.text = data .nickname tvBio.text = data .bio tvFollowerCount.text = data .followerCount tvFollowingCount.text = data .followingCount tvPostCount.text = data .postCount } } override fun showError (message: String , canRetry: Boolean ) { binding?.apply { progressBar.isVisible = false contentGroup.isVisible = false errorGroup.isVisible = true swipeRefresh.isRefreshing = false tvError.text = message btnRetry.isVisible = canRetry } } override fun showNetworkErrorWithCache (cachedData: ProfileContract .ProfileViewData ) { showContent(cachedData) Snackbar.make( requireView(), "网络不可用,当前显示缓存数据" , Snackbar.LENGTH_LONG ).show() } override fun showEditSuccess () { Toast.makeText(requireContext(), "资料已更新" , Toast.LENGTH_SHORT).show() presenter.loadProfile(isRefresh = true ) } override fun navigateToEditProfile () { findNavController().navigate(R.id.action_profile_to_edit) } override fun enableSwipeRefresh (enable: Boolean ) { binding?.swipeRefresh?.isEnabled = enable } override fun onDestroyView () { super .onDestroyView() binding = null } }
六、MVP vs MVC vs MVVM 深度对比 6.1 MVC 在 Android 上的问题 在经典 Web MVC 中,Controller 接收 HTTP 请求,调用 Model 获取数据,选择 View 渲染。但在 Android 中:
Activity 既是 Controller 又是 View :onCreate 中既 findViewById(View 职责)又调用 API(Controller 职责)
不能独立测试 :所有代码耦合在 Activity 中,必须用 Instrumentation Test
Fragment 的引入更糟 :Controller 逻辑分散在 Activity 和多个 Fragment 之间
6.2 MVP 解决了什么
问题
MVC 现状
MVP 解决方式
测试
必须 Instrumentation
Presenter 是纯 JVM 测试
复用
逻辑粘贴复制
Presenter 可被不同 View 复用
职责
Activity 做所有事
View 只渲染,Presenter 只逻辑
并行开发
UI 和逻辑绑定
根据 Contract 并行开发
代码审查
难以区分 UI 和逻辑
Contract 清晰分界
6.3 MVP vs MVVM
维度
MVP
MVVM
View 更新方式
Presenter 主动调用 View 接口方法
View 观察 ViewModel 的 LiveData/StateFlow
View 与 Presenter 关系
双向引用(View 持有 Presenter,Presenter 持有 View)
单向引用(View 持有 ViewModel,ViewModel 不感知 View)
数据绑定
手动 textView.setText()
DataBinding 或 ViewBinding 自动绑定
模板代码
需要写 View 接口方法
DataBinding 减少模板,但 XML 变复杂
内存泄漏风险
需要手动 attach/detach 管理
ViewModel 与 LifecycleOwner 绑定,自动清理
测试
Presenter 单元测试容易
ViewModel 单元测试也容易(mock Repository)
6.4 从 MVP 迁移到 MVVM 的路径 步骤一:提取 ViewModel
MVP: View <-> Presenter (双向引用) MVVM: View -> ViewModel (单向引用)
将 Presenter 持有的 View 弱引用移除,改为使用 LiveData/StateFlow 暴露状态:
class OldPresenter { private var view: LoginContract.View? = null fun onLoginClicked () { view?.showLoading() view?.hideLoading() view?.navigateToHome() } } class NewViewModel : ViewModel () { private val _state = MutableLiveData<LoginViewState>() val state: LiveData<LoginViewState> = _state fun login (username: String , password: String ) { _state.value = LoginViewState.Loading viewModelScope.launch { _state.value = LoginViewState.Success } } }
步骤二:View 从主动调用变为被动观察
presenter.onLoginClicked() viewModel.state.observe(viewLifecycleOwner) { state -> when (state) { is LoginViewState.Loading -> showLoading() is LoginViewState.Success -> navigateToHome() is LoginViewState.Error -> showError(state.message) } } viewModel.login(username, password)
步骤三:逐步替换 DataBinding
不需要一步到位,可以渐进引入 DataBinding:
<TextView android:text ="@{viewModel.nickname}" ... />
七、MVP 的常见反模式与最佳实践 7.1 反模式一:万能 Presenter class GodPresenter : Presenter { fun onLogin () {} fun onRegister () {} fun onForgotPassword () {} fun onVerifyCode () {} fun onResetPassword () {} fun onBindPhone () {} }
修正 :按功能拆分 Presenter,每个 Presenter 只负责一个屏幕或一个功能:
class LoginPresenter : LoginContract.Presenter class RegisterPresenter : RegisterContract.Presenter class ForgotPasswordPresenter : ForgotPasswordContract.Presenter
7.2 反模式二:View 接口爆炸 interface View { fun showLoading () fun hideLoading () fun setUsername (username: String ) fun setAvatar (url: String ) fun setNickname (nickname: String ) fun setBio (bio: String ) fun setFollowerCount (count: Int ) }
修正 :使用数据类批量传递 ViewData:
interface View { fun showLoading () fun hideLoading () fun showContent (data : ProfileViewData ) fun showError (message: String ) } data class ProfileViewData ( val username: String, val avatar: String, val nickname: String, val bio: String, val followerCount: Int , val followingCount: Int )
7.3 反模式三:View 包含业务逻辑 binding.btnLogin.setOnClickListener { if (etUsername.text.length < 3 ) { etUsername.error = "用户名太短" } else if (etPassword.text.length < 6 ) { etPassword.error = "密码太短" } else { presenter.onLoginClicked() } }
修正 :View 不做任何判断,直接转发:
binding.btnLogin.setOnClickListener { presenter.onLoginClicked( etUsername.text.toString(), etPassword.text.toString() ) }
7.4 反模式四:在 Presenter 中操作 View class LeakyPresenter (private val activity: Activity) { fun showDialog () { AlertDialog.Builder(activity) .setTitle("提示" ) .show() } }
修正 :通过 View 接口隔离:
interface View { fun showAlertDialog (title: String , message: String ) } class SafePresenter (private var view: View?) { fun showDialog () { view?.showAlertDialog("提示" , "确认删除?" ) } }
八、总结 MVP 架构通过 Contract 接口契约 和 Passive View 模式,将 Android 中混乱的 Activity 代码拆分为清晰的三个层次:
Model :纯数据层,与 UI 完全解耦
View :纯展示层,只响应 Presenter 的指令
Presenter :中间人,所有业务逻辑的集中地
MVP 最大贡献不是让代码”看起来漂亮”,而是让 单元测试覆盖业务逻辑成为可能 。一个完全在 JVM 上运行、秒级通过的测试套件,是 MVP 相比于 MVC 最根本的价值。
随着 Jetpack ViewModel + LiveData/StateFlow 的成熟,MVVM 在很多场景下是 MVP 的自然演进方向,但 MVP 的核心思想——职责分离、面向接口编程、被动视图 ——仍然是所有 Android 架构的基石。
参考资源