Android 架构演进:从 MVC 到 MVVM 的设计之道

发布于:2025-07-24 ⋅ 阅读:(31) ⋅ 点赞:(0)

在 Android 开发初期,很多开发者会把所有逻辑塞进 Activity—— 网络请求、数据处理、UI 更新全堆在一起,导致代码超过数千行,改一个按钮点击都要翻半天。这种 “面条式代码” 的根源是缺乏架构设计。随着应用复杂度提升,MVC、MVP、MVVM 三种架构逐渐成为主流,它们通过 “分层设计” 解决代码耦合问题。本文将从核心思想、代码实现到适用场景,全面解析这三种架构的设计逻辑,帮你找到适合项目的架构方案。

一、架构设计的核心目标

无论哪种架构,最终目的都是解决三个核心问题:

  • 解耦:分离 UI、业务逻辑、数据处理,避免 “改一处动全身”;
  • 可测试:业务逻辑可独立于 UI 测试(如无需启动 Activity 就能测试登录逻辑);
  • 可维护:分层清晰,新人能快速定位代码位置(如 “UI 相关找 View 层,网络请求找 Model 层”)。

形象比喻:架构就像 “餐厅分工”—— 厨师(Model)负责做菜(数据处理),服务员(Presenter/ViewModel)负责传递需求(业务逻辑),顾客(View)只负责点餐(UI 交互),各司其职才高效。

二、MVC 架构:最基础的分层思想

MVC(Model-View-Controller)是最早普及的分层架构,核心是 “将数据、UI、逻辑分离”。在 Android 中,MVC 的实现有其特殊性 —— 因 Activity 同时承担部分 View 和 Controller 职责,与传统 MVC 略有差异。

2.1 MVC 核心结构与职责

层级

核心职责

Android 中的载体

示例操作

Model

数据管理(网络请求、数据库、实体)

JavaBean、Repository、Dao

调用登录接口、从数据库查用户信息

View

展示 UI、接收用户输入

XML 布局、Activity(部分)、View

显示登录按钮、输入用户名密码

Controller

处理业务逻辑、协调 Model 和 View

Activity(主要)、Fragment

点击登录后调用 Model 校验,通知 View 显示结果

2.2 Android MVC 的实现(登录案例)

以 “登录功能” 为例,MVC 的代码结构如下:

(1)Model 层:数据与数据处理
// 1. 数据实体(User.java)
public class User {
    private String username;
    private String password;
    // 构造方法、getter、setter
}

// 2. 数据处理(登录接口调用,LoginModel.java)
public class LoginModel {
    // 模拟网络请求
    public void login(User user, LoginCallback callback) {
        new Thread(() -> {
            try {
                // 模拟网络延迟
                Thread.sleep(1000);
                // 简单校验逻辑
                if ("admin".equals(user.getUsername()) && "123456".equals(user.getPassword())) {
                    callback.onSuccess("登录成功");
                } else {
                    callback.onFail("用户名或密码错误");
                }
            } catch (InterruptedException e) {
                callback.onFail("网络异常");
            }
        }).start();
    }

    // 回调接口(Model通知Controller结果)
    public interface LoginCallback {
        void onSuccess(String msg);
        void onFail(String msg);
    }
}
(2)View 层:UI 展示(XML 布局)
<!-- activity_login.xml -->
<LinearLayout>
    <EditText
        android:id="@+id/et_username"
        hint="用户名"/>
    <EditText
        android:id="@+id/et_password"
        hint="密码"
        inputType="textPassword"/>
    <Button
        android:id="@+id/btn_login"
        text="登录"/>
    <TextView
        android:id="@+id/tv_result"/>
</LinearLayout>
(3)Controller 层:逻辑协调(Activity)
public class LoginActivity extends AppCompatActivity implements LoginModel.LoginCallback {
    private EditText etUsername;
    private EditText etPassword;
    private TextView tvResult;
    private LoginModel loginModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        // 初始化View
        etUsername = findViewById(R.id.et_username);
        etPassword = findViewById(R.id.et_password);
        tvResult = findViewById(R.id.tv_result);
        loginModel = new LoginModel();

        // 登录按钮点击(用户输入触发Controller)
        findViewById(R.id.btn_login).setOnClickListener(v -> {
            String username = etUsername.getText().toString();
            String password = etPassword.getText().toString();
            // 调用Model处理数据
            loginModel.login(new User(username, password), this);
        });
    }

    // Model回调:更新UI(Controller通知View)
    @Override
    public void onSuccess(String msg) {
        runOnUiThread(() -> tvResult.setText(msg));
    }

    @Override
    public void onFail(String msg) {
        runOnUiThread(() -> tvResult.setText(msg));
    }
}

2.3 MVC 的优缺点与适用场景

优势:
  • 简单直观:无需额外类和接口,新手易上手;
  • 开发快速:适合小型项目(如工具类 APP),无需复杂设计。
缺陷:
  • Activity 职责过重:既做 Controller(逻辑)又做 View(UI),代码易膨胀(上千行很常见);
  • 耦合度较高:View 和 Controller 通过 Activity 强耦合,难以单独测试(测登录逻辑需启动 Activity);
  • 复用性差:逻辑与 Activity 绑定,换个界面(如从 Activity 换成 Dialog)需重写逻辑。
适用场景:
  • 小型项目(如单个 Activity 的工具 APP);
  • 快速原型开发(需快速验证功能,不考虑长期维护)。

三、MVP 架构:解耦 View 与逻辑的中间层

MVP(Model-View-Presenter)是为解决 MVC 中 “Activity 职责过重” 而诞生的架构。其核心是引入Presenter 作为中间层,彻底分离 View(UI)和业务逻辑,让 Activity 只专注于 UI 展示。

3.1 MVP 核心结构与职责

MVP 在 MVC 基础上拆分出 Presenter,各层职责更清晰:

层级

核心职责

Android 中的载体

核心交互

Model

数据管理(与 MVC 一致)

JavaBean、Repository

登录接口调用、数据校验

View

纯 UI 层(展示、用户输入)

Activity、Fragment、XML 布局

显示加载框、暴露更新 UI 的方法

Presenter

业务逻辑核心、协调 Model 和 View

Presenter 类(独立于 Android 框架)

接收 View 的登录请求→调用 Model→通知 View 更新

核心改进

  • View 与 Presenter 通过接口交互(View 只暴露 UI 方法,不包含逻辑);
  • Presenter 完全独立于 Android 框架(不持有 Activity 上下文),可单独测试。

3.2 Android MVP 的实现(登录案例)

同样以登录功能为例,MVP 通过 “接口定义交互” 实现解耦:

(1)Model 层:与 MVC 一致(复用 LoginModel)
// 复用MVC中的LoginModel和User,无需修改
public class LoginModel {
    public void login(User user, LoginCallback callback) { ... }
    // 回调接口
    public interface LoginCallback { ... }
}
(2)View 层:定义 UI 接口 + 实现
// 1. View接口(定义UI操作,与Presenter交互)
public interface LoginView {
    // 显示加载状态
    void showLoading();
    // 隐藏加载状态
    void hideLoading();
    // 更新登录结果
    void showResult(String msg);
    // 获取用户输入
    String getUsername();
    String getPassword();
}

// 2. View实现(Activity只做UI,不处理逻辑)
public class LoginActivity extends AppCompatActivity implements LoginView {
    private EditText etUsername;
    private EditText etPassword;
    private TextView tvResult;
    private ProgressDialog loadingDialog;
    private LoginPresenter presenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        // 初始化UI
        etUsername = findViewById(R.id.et_username);
        etPassword = findViewById(R.id.et_password);
        tvResult = findViewById(R.id.tv_result);
        loadingDialog = new ProgressDialog(this);
        loadingDialog.setMessage("登录中...");

        // 创建Presenter,传入View接口
        presenter = new LoginPresenter(this);

        // 登录按钮点击(View只通知Presenter,不处理逻辑)
        findViewById(R.id.btn_login).setOnClickListener(v -> presenter.login());
    }

    // 实现LoginView接口的UI方法
    @Override
    public void showLoading() {
        loadingDialog.show();
    }

    @Override
    public void hideLoading() {
        loadingDialog.dismiss();
    }

    @Override
    public void showResult(String msg) {
        tvResult.setText(msg);
    }

    @Override
    public String getUsername() {
        return etUsername.getText().toString();
    }

    @Override
    public String getPassword() {
        return etPassword.getText().toString();
    }

    // 生命周期管理:避免内存泄漏
    @Override
    protected void onDestroy() {
        super.onDestroy();
        presenter.detachView(); // 断开Presenter与View的引用
    }
}
(3)Presenter 层:逻辑核心
public class LoginPresenter {
    // 持有View接口(而非具体Activity)和Model
    private LoginView loginView;
    private LoginModel loginModel;
    // 弱引用(避免Presenter持有Activity导致内存泄漏)
    private WeakReference<LoginView> viewRef;

    // 构造方法:关联View和Model
    public LoginPresenter(LoginView view) {
        this.viewRef = new WeakReference<>(view);
        this.loginModel = new LoginModel();
    }

    // 登录逻辑(核心)
    public void login() {
        LoginView view = viewRef.get();
        if (view == null) return;

        // 1. 通知View显示加载
        view.showLoading();

        // 2. 获取View的输入数据
        String username = view.getUsername();
        String password = view.getPassword();

        // 3. 调用Model处理登录
        loginModel.login(new User(username, password), new LoginModel.LoginCallback() {
            @Override
            public void onSuccess(String msg) {
                // 4. 通知View更新结果
                if (viewRef.get() != null) {
                    viewRef.get().hideLoading();
                    viewRef.get().showResult(msg);
                }
            }

            @Override
            public void onFail(String msg) {
                if (viewRef.get() != null) {
                    viewRef.get().hideLoading();
                    viewRef.get().showResult(msg);
                }
            }
        });
    }

    // 断开View引用(避免内存泄漏)
    public void detachView() {
        if (viewRef != null) {
            viewRef.clear();
            viewRef = null;
        }
    }
}

3.3 MVP 的核心改进与优缺点

核心改进:
  • 完全解耦:View 只做 UI,Presenter 只做逻辑,修改 UI 不影响逻辑;
  • 可测试性:Presenter 不依赖 Android 框架,可通过 JUnit 直接测试(无需启动 APP);
    // 测试Presenter(纯Java测试,不依赖Android)
    public class LoginPresenterTest {
        @Test
        public void testLoginSuccess() {
            // 模拟View
            LoginView mockView = Mockito.mock(LoginView.class);
            // 模拟输入
            Mockito.when(mockView.getUsername()).thenReturn("admin");
            Mockito.when(mockView.getPassword()).thenReturn("123456");
    
            LoginPresenter presenter = new LoginPresenter(mockView);
            presenter.login();
    
            // 验证逻辑:是否调用了显示加载和隐藏加载
            Mockito.verify(mockView).showLoading();
            Mockito.verify(mockView).hideLoading();
            Mockito.verify(mockView).showResult("登录成功");
        }
    }

  • 复用性提升:Presenter 可搭配不同 View(如用同一 LoginPresenter 支持 Activity 和 Fragment)。
缺陷:
  • 代码量增加:需定义大量接口(View 接口、回调),简单功能也需多个类;
  • 生命周期管理复杂:Presenter 需手动处理 View 的生命周期(如detachView),否则易内存泄漏;
  • 接口冗余:View 接口可能定义大量方法(如 10 个 UI 更新方法),维护成本高。
适用场景:
  • 中型项目(如多模块应用,需团队协作);
  • 需频繁迭代的项目(逻辑与 UI 分离,便于维护);
  • 对测试有要求的项目(需单元测试覆盖核心逻辑)。

四、MVVM 架构:数据驱动 UI 的响应式设计

MVVM(Model-View-ViewModel)是当前 Android 主流架构,借助 “数据绑定(DataBinding)” 和 “响应式数据(如 LiveData)” 实现 “数据驱动 UI”——UI 自动响应数据变化,无需手动调用更新方法。

4.1 MVVM 核心结构与职责

MVVM 的核心是ViewModel 与 View 的数据绑定,各层职责如下:

层级

核心职责

Android 中的载体

核心交互

Model

数据管理(与前两种架构一致)

JavaBean、Repository、Room

登录接口、数据库操作

View

UI 层(自动响应数据变化)

Activity、Fragment、XML+DataBinding

声明式绑定数据,无需手动更新

ViewModel

持有可观察数据、处理业务逻辑

ViewModel(Jetpack 组件)

调用 Model 获取数据→更新 LiveData→View 自动刷新

核心优势

  • 数据与 UI 通过 DataBinding 绑定,省去大量setText等更新代码;
  • ViewModel 生命周期独立于 Activity(屏幕旋转时不重建),数据自动保留;
  • 响应式编程(LiveData 自动通知数据变化),逻辑更清晰。

4.2 Android MVVM 的实现(登录案例)

结合 Jetpack 组件(ViewModel、LiveData、DataBinding)实现登录功能:

(1)Model 层:数据与仓库(引入 Repository 模式)
// 1. 数据实体(User.java)
public class User { ... }

// 2. 数据源(登录接口,LoginDataSource.java)
public class LoginDataSource {
    public void login(User user, LoginCallback callback) {
        // 模拟网络请求(与MVP的Model一致)
        new Thread(() -> {
            try {
                Thread.sleep(1000);
                if ("admin".equals(user.getUsername()) && "123456".equals(user.getPassword())) {
                    callback.onSuccess("登录成功");
                } else {
                    callback.onFail("用户名或密码错误");
                }
            } catch (InterruptedException e) {
                callback.onFail("网络异常");
            }
        }).start();
    }

    public interface LoginCallback { ... }
}

// 3. 仓库(统一管理数据源,LoginRepository.java)
public class LoginRepository {
    private static LoginRepository instance;
    private LoginDataSource dataSource;

    // 单例仓库(可同时管理网络和本地数据源)
    public static LoginRepository getInstance() {
        if (instance == null) {
            instance = new LoginRepository(new LoginDataSource());
        }
        return instance;
    }

    private LoginRepository(LoginDataSource dataSource) {
        this.dataSource = dataSource;
    }

    // 暴露登录接口给ViewModel
    public void login(User user, LoginDataSource.LoginCallback callback) {
        dataSource.login(user, callback);
    }
}
(2)ViewModel 层:持有 LiveData 与逻辑
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;

public class LoginViewModel extends ViewModel {
    // 可观察数据(登录结果,View会自动监听)
    private MutableLiveData<String> loginResult = new MutableLiveData<>();
    // 加载状态
    private MutableLiveData<Boolean> isLoading = new MutableLiveData<>();
    // 仓库引用
    private LoginRepository repository;

    public LoginViewModel() {
        repository = LoginRepository.getInstance();
    }

    // 暴露给View的只读LiveData(防止View直接修改)
    public LiveData<String> getLoginResult() {
        return loginResult;
    }

    public LiveData<Boolean> getIsLoading() {
        return isLoading;
    }

    // 登录逻辑
    public void login(String username, String password) {
        isLoading.setValue(true); // 通知加载开始
        User user = new User(username, password);
        repository.login(user, new LoginDataSource.LoginCallback() {
            @Override
            public void onSuccess(String msg) {
                isLoading.postValue(false); // 子线程用postValue
                loginResult.postValue(msg);
            }

            @Override
            public void onFail(String msg) {
                isLoading.postValue(false);
                loginResult.postValue(msg);
            }
        });
    }
}
(3)View 层:DataBinding 绑定数据
<!-- activity_login.xml(启用DataBinding) -->
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 数据变量定义 -->
    <data>
        <variable
            name="viewModel"
            type="com.example.mvvm.LoginViewModel" />
        <variable
            name="activity"
            type="com.example.mvvm.LoginActivity" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <EditText
            android:id="@+id/et_username"
            android:hint="用户名"/>

        <EditText
            android:id="@+id/et_password"
            android:hint="密码"
            android:inputType="textPassword"/>

        <Button
            android:text="登录"
            android:onClick="@{() -> activity.login()}"/>

        <TextView
            android:text="@{viewModel.loginResult}" /> <!-- 自动绑定结果 -->

        <ProgressBar
            android:visibility="@{viewModel.isLoading ? View.VISIBLE : View.GONE}" /> <!-- 自动绑定加载状态 -->
    </LinearLayout>
</layout>
(4)Activity:关联 ViewModel 与 DataBinding
public class LoginActivity extends AppCompatActivity {
    private ActivityLoginBinding binding; // DataBinding自动生成的类
    private LoginViewModel loginViewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // 1. 初始化DataBinding
        binding = DataBindingUtil.setContentView(this, R.layout.activity_login);

        // 2. 获取ViewModel(通过ViewModelProvider,确保生命周期正确)
        loginViewModel = new ViewModelProvider(this).get(LoginViewModel.class);

        // 3. 绑定ViewModel到布局
        binding.setViewModel(loginViewModel);
        binding.setActivity(this);
        // 绑定生命周期所有者(让LiveData感知Activity生命周期)
        binding.setLifecycleOwner(this);
    }

    // 登录按钮点击(调用ViewModel的登录方法)
    public void login() {
        String username = binding.etUsername.getText().toString();
        String password = binding.etPassword.getText().toString();
        loginViewModel.login(username, password);
    }
}

4.3 MVVM 的核心优势与优缺点

核心优势:
  • 数据驱动 UI:通过 LiveData+DataBinding,数据变化自动更新 UI,省去runOnUiThread和setText;
  • 生命周期安全:ViewModel 在屏幕旋转时不重建(数据保留),避免重复请求网络;
  • 低耦合:View 只绑定数据,ViewModel 只处理逻辑,Model 只管数据,修改 UI 不影响逻辑;
  • 可测试性:ViewModel 独立于 Android 框架,可直接测试(如验证登录逻辑是否正确更新 LiveData)。
缺陷:
  • 学习成本高:需掌握 DataBinding、LiveData、ViewModel 等 Jetpack 组件;
  • 调试难度增加:数据绑定是黑盒操作,UI 异常时需排查绑定关系;
  • 简单功能冗余:小功能(如单个按钮)用 MVVM 显得繁琐。
适用场景:
  • 大型项目(如电商 APP、社交 APP,需长期维护);
  • 频繁更新 UI 的场景(如列表刷新、实时数据展示);
  • 团队协作项目(架构规范统一,新人易接手)。

五、三种架构对比与选择指南

维度

MVC

MVP

MVVM

核心思想

分层但 View 与 Controller 耦合

Presenter 中间层解耦

数据绑定 + 响应式数据驱动

代码量

少(无额外接口)

中(需定义 View 接口)

多(需 ViewModel 和绑定)

耦合度

高(Activity 承担多重职责)

中(接口交互,需手动管理)

低(数据绑定,自动响应)

可测试性

低(需依赖 Activity)

高(Presenter 可独立测试)

高(ViewModel 独立测试)

维护成本

高(后期改不动)

中(接口清晰但需维护)

低(分层明确,数据驱动)

Android 适配

原生支持(简单但粗糙)

需手动实现接口和生命周期管理

依赖 Jetpack(官方推荐)

5.1 架构选择建议

  1. 按项目规模选择
  • 小型项目(<5 个 Activity)→ MVC(快速开发);
  • 中型项目(5-20 个页面)→ MVP(平衡开发效率和维护性);
  • 大型项目(>20 个页面)→ MVVM(长期维护,团队协作)。
  1. 按团队情况选择
  • 新手团队→ MVC(降低学习成本);
  • 有经验团队→ MVVM(利用 Jetpack 提升效率)。
  1. 按功能复杂度选择
  • 简单功能(如设置页面)→ MVC 或 MVP;
  • 复杂功能(如首页列表 + 购物车 + 实时消息)→ MVVM。

六、架构设计的本质:灵活应变

无论 MVC、MVP 还是 MVVM,都不是 “银弹”。实际开发中不必严格遵守某一种架构,可根据需求混合使用:

  • 小型项目用 MVC,但抽取工具类减少 Activity 代码;
  • MVP 中引入 DataBinding 简化 View 更新;
  • MVVM 中保留 Presenter 的部分逻辑(如复杂表单校验)。

架构的本质是 “解决当前问题”—— 能让团队高效开发、代码易于维护的就是好架构。随着项目演进,架构也可逐步升级(如从 MVC 重构为 MVVM),关键是保持 “分层清晰、职责单一” 的核心原则。

掌握这三种架构后,你会发现:优秀的 Android 代码不是 “堆功能”,而是通过合理设计让每一行代码都有明确的位置和职责 —— 这也是从 “会写代码” 到 “能设计系统” 的关键一步。


网站公告

今日签到

点亮在社区的每一天
去签到