【Android】从早期 MVC 到现代 MVVM 的架构变迁

Android 架构的演进是一个不断解耦、提升可维护性和可测试性的过程,其核心围绕 UI 与逻辑分离、状态管理优化展开。以下结合技术演进路径和典型案例,解析从早期 MVC 到现代 MVVM 的架构变迁:

一、早期 MVC:代码混杂与 "全能 Activity"

架构特点

  • 模型(Model) :负责数据存储(如 SQLite)和业务逻辑
  • 视图(View) :XML 布局文件定义 UI
  • 控制器(Controller) :Activity 同时承担视图渲染和逻辑处理
java 复制代码
public class MainActivity extends AppCompatActivity {

    private int count = 0;
    private TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);

        textView = findViewById(R.id.mvc_text);
        textView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                count++;
                textView.setText("点击了" + count + "次");
            }
        });
    }
}
  • 紧密耦合:Activity 同时处理网络请求、数据库操作和 UI 更新
  • 状态丢失:屏幕旋转导致数据重置
  • 测试困难:依赖 Android 框架难以进行单元测试

二、经典 MVC:引入独立控制器

架构改进

  • Model:数据实体和业务逻辑
  • View:Activity 作为视图层
  • Controller:独立的逻辑处理类
java 复制代码
public class MainActivity extends AppCompatActivity implements CounterView{

    private CounterController controller;

    private TextView countText;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);


        countText = findViewById(R.id.count_text);
        Button button = findViewById(R.id.increment_btn);

        CounterModel model = new CounterModel();
        controller = new CounterController(model);
        controller.attachView(this);

        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                controller.increment();
            }
        });
    }


    @Override
    public void updateView(int count) {
        countText.setText(String.valueOf(count));
    }
}
java 复制代码
public class CounterController {

    private CounterModel model;

    private CounterView view;


    public CounterController(CounterModel model) {
        this.model = model;
    }

    public void attachView(CounterView view){
        this.view = view;
        updateView();
    }

    public void increment() {
        model.increment();
        updateView();
    }

    private void updateView() {
        if(view != null){
            view.updateView(model.getCount());
        }
    }

}
java 复制代码
public class CounterModel {
    private int count = 0;

    public int getCount() {
        return count;
    }

    public void increment() {
        count++;
    }
}
csharp 复制代码
public interface CounterView {

    void updateView(int count);
}
xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center">

    <TextView
        android:id="@+id/count_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="0"
        android:textSize="30sp" />

    <Button
        android:id="@+id/increment_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Increment" />

</LinearLayout>

三、MVP:通过接口彻底解耦

架构核心

  • Model:数据层(网络 / 数据库)
  • View:UI 接口(Activity/Fragment 实现)
  • Presenter:逻辑核心,持有 Model 和 View 接口引用
java 复制代码
public class LoginActivity extends AppCompatActivity implements LoginContract.View {
    private EditText etUsername, etPassword;
    private Button btnLogin;

    private LoginPresenter loginPresenter;

    private ProgressBar progressBar;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        initView();
        setupPresenter();
    }



    private void initView() {
        etUsername = findViewById(R.id.et_username);
        etPassword = findViewById(R.id.et_password);
        btnLogin = findViewById(R.id.btn_login);
        progressBar = findViewById(R.id.progress_bar);

        btnLogin.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                User user = getUserInput();
                loginPresenter.login(user);
            }
        });
    }

    private void setupPresenter() {
        loginPresenter = new LoginPresenter(this);
    }

    @Override
    public void showLoading() {
        progressBar.setVisibility(View.VISIBLE);
        btnLogin.setEnabled(false);
    }

    @Override
    public void hideLoading() {
        progressBar.setVisibility(View.INVISIBLE);
        btnLogin.setEnabled(true);
    }

    @Override
    public void onLoginSuccess(User user) {
        Toast.makeText(this, "欢迎 " + user.getUsername(), Toast.LENGTH_SHORT).show();
        // 跳转到主页面,传递用户信息
        Intent intent = new Intent(this, MainActivity.class);
        intent.putExtra("username", user.getUsername());
        startActivity(intent);
        finish();
    }

    @Override
    public void onLoginFailed(String error) {
        Toast.makeText(this, error, Toast.LENGTH_SHORT).show();
    }

    @Override
    public User getUserInput() {
        String username = etUsername.getText().toString().trim();
        String password = etPassword.getText().toString();
        return new User(username, password);
    }
}
java 复制代码
public class LoginPresenter implements LoginContract.Presenter{

    private LoginContract.View view;

    public LoginPresenter(LoginContract.View view) {
        this.view = view;
    }


    @Override
    public void login(User user) {
        if(!user.isValid()){
            view.onLoginFailed("用户名和密码不能为空");
            return;
        }

        view.showLoading();

        // 模拟网络请求
        new android.os.Handler().postDelayed(() -> {
            view.hideLoading();

            // 验证登录凭据
            if (user.validateCredentials()) {
                view.onLoginSuccess(user);
            } else {
                view.onLoginFailed("用户名或密码错误");
            }
        }, 1500);
    }
}
java 复制代码
public interface LoginContract {

    interface View {
        void showLoading();

        void hideLoading();

        void onLoginSuccess(User user);

        void onLoginFailed(String error);

        User getUserInput();
    }


    interface Presenter {
        void login(User user);
    }
}
java 复制代码
public class User {

    private String username;

    private String password;

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    // 验证用户信息
    public boolean isValid() {
        return username != null && !username.isEmpty() &&
                password != null && !password.isEmpty();
    }

    // 验证登录凭据
    public boolean validateCredentials() {
        return "admin".equals(username) && "123456".equals(password);
    }
}
ini 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="24dp"
    android:gravity="center">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="用户登录"
        android:textSize="24sp"
        android:textStyle="bold"
        android:layout_marginBottom="32dp" />

    <EditText
        android:id="@+id/et_username"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="用户名"
        android:layout_marginBottom="16dp" />

    <EditText
        android:id="@+id/et_password"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="密码"
        android:inputType="textPassword"
        android:layout_marginBottom="24dp" />

    <ProgressBar
        android:id="@+id/progress_bar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:visibility="gone" />

    <Button
        android:id="@+id/btn_login"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="登录" />

</LinearLayout>

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

架构革新

  • Model:数据层(Repository 模式)
  • View:XML 布局 + DataBinding
  • ViewModel:持有 LiveData,与 View 双向绑定
java 复制代码
public class LoginActivity extends AppCompatActivity {
    private EditText etUsername, etPassword;
    private Button btnLogin;
    private ProgressBar progressBar;
    private LoginViewModel loginViewModel;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);

        initView();
        setupViewModel();
        setupObservers();
    }

    private void initView() {
        etUsername = findViewById(R.id.et_username);
        etPassword = findViewById(R.id.et_password);
        btnLogin = findViewById(R.id.btn_login);
        progressBar = findViewById(R.id.progress_bar);

        btnLogin.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                loginViewModel.login(new User(etUsername.getText().toString(), etPassword.getText().toString()));
            }
        });
    }

    private void setupViewModel() {
        loginViewModel = new ViewModelProvider(this).get(LoginViewModel.class);
    }


    private void setupObservers() {
        loginViewModel.getIsloading().observe(this,isLoading  -> {
            if(isLoading ){
                progressBar.setVisibility(View.VISIBLE);
                btnLogin.setEnabled(false);
            } else {
                progressBar.setVisibility(View.GONE);
                btnLogin.setEnabled(true);
            }
        });

        loginViewModel.getLoginResult().observe(this,loginResult -> {
            if(loginResult){
                // 登录成功
                Toast.makeText(this, "登录成功", Toast.LENGTH_SHORT).show();
                startActivity(new Intent(this, MainActivity.class));
                finish();
            }
        });

        loginViewModel.getErrorMessage().observe(this,errorMessage -> {
            if(errorMessage != null){
                Toast.makeText(this, errorMessage, Toast.LENGTH_SHORT).show();
            }
        });
    }
    
}
java 复制代码
public class LoginViewModel extends ViewModel {

    private LoginRepository loginRepository;
    private MutableLiveData<Boolean> isloading = new MutableLiveData<>();

    private MutableLiveData<Boolean> loginResult = new MutableLiveData<>();

    private MutableLiveData<String> errorMessage = new MutableLiveData<>();

    public LoginViewModel(){
        loginRepository = new LoginRepository();
        isloading.setValue(false);
    }
    public void login(User user){
        if(!user.isValid()){
            errorMessage.setValue("用户名或密码不能为空");
            return;
        }

        isloading.setValue(true);
        loginRepository.login(user).observeForever(new Observer<Boolean>() {
            @Override
            public void onChanged(Boolean success) {
                isloading.setValue(false);

                if(success){
                    loginResult.setValue(true);
                } else {
                    errorMessage.setValue("用户名或密码错误");
                }
                loginRepository.login(user).removeObserver(this);
            }
        });
    }



    public LiveData<Boolean> getIsloading() {
        return isloading;
    }

    public LiveData<Boolean> getLoginResult() {
        return loginResult;
    }

    public LiveData<String> getErrorMessage() {
        return errorMessage;
    }
}
java 复制代码
public class LoginRepository {

    public LiveData<Boolean> login(User user) {
        MutableLiveData<Boolean> result = new MutableLiveData<>();

        // 模拟网络请求
        new Handler().postDelayed(() -> {
            // 简单验证逻辑
            boolean success = "admin".equals(user.getUsername()) &&
                    "123456".equals(user.getPassword());
            result.setValue(success);
        }, 1500);

        return result;
    }
}
java 复制代码
public class User {
    private String username;
    private String password;

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    public String getUsername() {
        return username;
    }

    public String getPassword() {
        return password;
    }

    public boolean isValid() {
        return username != null && !username.isEmpty() &&
                password != null && !password.isEmpty();
    }
}
xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="24dp"
    android:gravity="center">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="用户登录"
        android:textSize="24sp"
        android:textStyle="bold"
        android:layout_marginBottom="32dp" />

    <EditText
        android:id="@+id/et_username"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="用户名"
        android:layout_marginBottom="16dp" />

    <EditText
        android:id="@+id/et_password"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="密码"
        android:inputType="textPassword"
        android:layout_marginBottom="24dp" />

    <ProgressBar
        android:id="@+id/progress_bar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:visibility="gone" />

    <Button
        android:id="@+id/btn_login"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="登录" />

</LinearLayout>
  1. Model (User)

    • 封装用户数据
    • 提供数据验证方法
  2. Repository

    • 数据获取层,负责从网络、数据库等获取数据
    • 返回 LiveData 供 ViewModel 观察
  3. ViewModel

    • 管理 UI 相关的数据
    • 通过 LiveData 暴露状态给 View
    • 处理业务逻辑,但不直接引用 View
  4. View (Activity)

    • 只负责 UI 显示和用户交互
    • 观察 ViewModel 的 LiveData 并更新 UI
    • 不包含任何业务逻辑

MVVM 优势:

  1. 数据驱动 UI

    • UI 自动响应数据变化,无需手动更新
    • 通过 LiveData 实现数据绑定
  2. 更好的生命周期管理

    • ViewModel 独立于 Activity 生命周期
    • 屏幕旋转等配置变化时数据不会丢失
  3. 更清晰的职责分离

    • View 只关心显示,ViewModel 只关心数据,Model 只关心数据结构
  4. 易于测试

    • ViewModel 可以独立于 Android 组件进行测试
    • Repository 可以模拟数据进行测试
  5. 减少内存泄漏

    • LiveData 具有生命周期感知能力,自动管理观察者
相关推荐
lbr3 小时前
uniapp封装图片上传组件,使用v-model双向绑定
前端
拉不动的猪3 小时前
回顾前端项目打包时--脚本引入时机与环境类型的判断问题
前端·vue.js·面试
比特记忆3 小时前
Android开发中用到的反射机制
前端
fox_3 小时前
一次搞懂柯里化:从最简单代码到支持任意函数,这篇让你不再踩参数传递的坑
前端·javascript
Keepreal4963 小时前
实现一个简单的hello world vs-code插件
前端·javascript·visual studio code
用户458203153173 小时前
CSS 层叠层 (@layer) 详解:控制样式优先级新方式
前端·css
月弦笙音3 小时前
【Vue组件】封装组件该考虑的核心点
前端·javascript·vue.js
清风细雨_林木木3 小时前
HttpOnly 是怎么防止 XSS 攻击的?
前端·xss
用户2519162427113 小时前
Node之单表基本查询
前端·javascript·node.js