Android随笔-MVVM

一、MVVM 是什么

MVVM (Model-View-ViewModel) 是一种架构模式,核心思想是将 UI 逻辑与业务逻辑彻底分离

  • View 只负责展示,不持有业务逻辑
  • ViewModel 持有 UI 数据,处理业务逻辑,不依赖 View
  • Model 负责数据获取与持久化

Android 官方通过 Jetpack 提供了原生支持:ViewModel、LiveData、DataBinding。

二、完整 Demo:用户信息显示

实现一个简单的用户信息展示页面,支持加载、更新用户数据。

1. 依赖配置(build.gradle)

groovy 复制代码
android {
    // 开启 DataBinding
    dataBinding {
        enabled = true
    }
}

dependencies {
    implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
    annotationProcessor 'androidx.lifecycle:lifecycle-compiler:2.2.0'
}

2. Model 层

User.java ------ 实体类

java 复制代码
package com.example.mvvmdemo.model;

public class User {
    private String name;
    private int age;
    private String bio;

    public User(String name, int age, String bio) {
        this.name = name;
        this.age = age;
        this.bio = bio;
    }

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
    public String getBio() { return bio; }
    public void setBio(String bio) { this.bio = bio; }
}

UserRepository.java ------ 数据仓库

java 复制代码
package com.example.mvvmdemo.repository;

import android.os.Handler;
import android.os.Looper;
import androidx.lifecycle.MutableLiveData;
import com.example.mvvmdemo.model.User;

public class UserRepository {
    
    // 模拟从网络/数据库获取数据
    public void getUserFromServer(String userId, MutableLiveData<User> liveData) {
        // 模拟异步网络请求
        new Thread(() -> {
            try {
                Thread.sleep(1500); // 模拟网络延迟
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            // 构造假数据
            User user = new User("张三", 25, "Android 开发工程师,热爱技术");
            
            // 切回主线程更新数据
            new Handler(Looper.getMainLooper()).post(() -> {
                liveData.setValue(user);
            });
        }).start();
    }
}

3. ViewModel 层

UserViewModel.java

java 复制代码
package com.example.mvvmdemo.viewmodel;

import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.mvvmdemo.model.User;
import com.example.mvvmdemo.repository.UserRepository;

public class UserViewModel extends ViewModel {
    
    // 对外暴露不可变的 LiveData,对内用 MutableLiveData 修改
    private MutableLiveData<User> userLiveData;
    private UserRepository userRepository;
    
    // 加载状态
    private MutableLiveData<Boolean> loadingLiveData;
    
    public UserViewModel() {
        userLiveData = new MutableLiveData<>();
        userRepository = new UserRepository();
        loadingLiveData = new MutableLiveData<>(false);
    }
    
    public LiveData<User> getUser() {
        return userLiveData;
    }
    
    public LiveData<Boolean> getLoading() {
        return loadingLiveData;
    }
    
    // 业务逻辑:加载用户数据
    public void loadUser(String userId) {
        loadingLiveData.setValue(true);
        
        userRepository.getUserFromServer(userId, new androidx.lifecycle.Observer<User>() {
            @Override
            public void onChanged(User user) {
                userLiveData.setValue(user);
                loadingLiveData.setValue(false);
            }
        });
    }
    
    // 业务逻辑:更新用户名
    public void updateUserName(String newName) {
        User current = userLiveData.getValue();
        if (current != null) {
            current.setName(newName);
            // 触发观察者更新 UI
            userLiveData.setValue(current);
        }
    }
    
    @Override
    protected void onCleared() {
        super.onCleared();
        // ViewModel 销毁时的清理工作
    }
}

4. View 层

activity_main.xml(DataBinding 布局)

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    
    <data>
        <variable
            name="viewModel"
            type="com.example.mvvmdemo.viewmodel.UserViewModel" />
    </data>
    
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:padding="16dp">
        
        <ProgressBar
            android:id="@+id/progressBar"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:visibility="gone" />
        
        <TextView
            android:id="@+id/tvName"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{viewModel.user.name}"
            android:textSize="24sp"
            android:textStyle="bold" />
        
        <TextView
            android:id="@+id/tvAge"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{String.valueOf(viewModel.user.age) + ` 岁`}"
            android:textSize="18sp"
            android:layout_marginTop="8dp" />
        
        <TextView
            android:id="@+id/tvBio"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{viewModel.user.bio}"
            android:textSize="16sp"
            android:layout_marginTop="8dp" />
        
        <Button
            android:id="@+id/btnLoad"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="加载用户"
            android:layout_marginTop="16dp" />
        
        <Button
            android:id="@+id/btnUpdate"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="修改名字为李四"
            android:layout_marginTop="8dp" />
    </LinearLayout>
</layout>

MainActivity.java

java 复制代码
package com.example.mvvmdemo;

import android.os.Bundle;
import android.view.View;
import android.widget.ProgressBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.databinding.DataBindingUtil;
import androidx.lifecycle.ViewModelProvider;
import com.example.mvvmdemo.databinding.ActivityMainBinding;
import com.example.mvvmdemo.viewmodel.UserViewModel;

public class MainActivity extends AppCompatActivity {
    
    private UserViewModel viewModel;
    private ActivityMainBinding binding;
    private ProgressBar progressBar;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        // 使用 DataBinding 加载布局
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        
        // 获取 ViewModel(配置变更时不会重新创建)
        viewModel = new ViewModelProvider(this).get(UserViewModel.class);
        
        // 绑定 ViewModel 到布局
        binding.setViewModel(viewModel);
        binding.setLifecycleOwner(this); // 让 LiveData 感知生命周期
        
        progressBar = findViewById(R.id.progressBar);
        
        // 观察用户数据变化
        viewModel.getUser().observe(this, user -> {
            // 数据变化时自动更新 UI(DataBinding 已自动处理,这里可额外处理)
            if (user != null) {
                binding.tvName.setText(user.getName());
                binding.tvAge.setText(user.getAge() + " 岁");
                binding.tvBio.setText(user.getBio());
            }
        });
        
        // 观察加载状态
        viewModel.getLoading().observe(this, isLoading -> {
            progressBar.setVisibility(isLoading ? View.VISIBLE : View.GONE);
        });
        
        // 按钮点击:加载用户
        binding.btnLoad.setOnClickListener(v -> {
            viewModel.loadUser("user_001");
        });
        
        // 按钮点击:更新名字
        binding.btnUpdate.setOnClickListener(v -> {
            viewModel.updateUserName("李四");
        });
    }
}

三、每个模块作用详解

模块 职责 为什么这样设计
ModelUser + UserRepository 定义数据结构,负责数据的获取、存储、缓存。Repository 屏蔽了数据来源(网络/本地/内存),对上层提供统一接口 实现数据与业务解耦,方便切换数据源(如从网络切到本地数据库)
ViewModelUserViewModel 持有 UI 相关的数据(通过 LiveData),处理业务逻辑。不持有 View 的引用,只通过数据驱动 UI 生命周期比 Activity 长,配置变更(如旋转屏幕)时数据不丢失
ViewActivity + XML 只负责展示数据和响应用户操作。所有逻辑都委托给 ViewModel 单元测试友好,Activity 代码大幅减少
LiveData 可感知生命周期的数据容器。自动在合适的生命周期状态分发数据 避免内存泄漏,避免空指针,自动处理生命周期
DataBinding 在 XML 中直接绑定 ViewModel 的数据,减少 findViewById 和手动设值代码 声明式 UI,减少样板代码,实现真正的数据驱动

四、MVVM 的优势与缺点

✅ 优势

1. 生命周期感知

ViewModel 在配置变更时存活,LiveData 自动处理生命周期,避免内存泄漏

2. 彻底解耦

View 不依赖 ViewModel 的具体实现,只观察数据;ViewModel 不依赖 View

3. 可测试性强

ViewModel 是纯 Java 类,不依赖 Android 框架,可用 JUnit 直接单元测试

4. 数据驱动 UI

数据变化自动刷新 UI,减少手动同步状态的代码,避免遗漏更新

5. 职责清晰

每个模块只干一件事,代码结构一目了然,便于团队协作

❌ 缺点

1. 学习成本高

需要理解 LiveData、ViewModel、DataBinding、Lifecycle 等多个组件

2. DataBinding 调试困难

XML 中的表达式错误编译期提示不明确,运行时难以定位问题

3. 不适合简单页面

对于只有一个按钮和一个 TextView 的页面,引入 MVVM 显得过重

4. LiveData 的"粘性"问题

新注册的 Observer 会立即收到上次的数据,在某些场景下不符合预期(如事件通知)

5. 包体积增加

DataBinding 和 Lifecycle 库会增加一定的包体积

五、常见问题

1. MVVM 与 MVP 的区别是什么?

MVP 中 Presenter 通过接口与 View 通信,持有 View 的引用;MVVM 中 ViewModel 不持有 View 引用,通过数据绑定或观察驱动 UI。MVVM 更解耦,且天然支持生命周期管理。

2. ViewModel 为什么能在旋转屏幕后存活?

因为 ViewModel 不是由 Activity 直接创建,而是由 ViewModelProvider 管理,实际存储在 ActivityThread 的 NonConfigurationInstances 中,与 Activity 生命周期解耦。

3. LiveData 的 setValue() 和 postValue() 有什么区别?

setValue() 必须在主线程调用;postValue() 可在子线程调用,内部通过 Handler 切到主线程。连续调用 postValue() 可能只保留最后一次的值。

4. MVVM 中必须使用 DataBinding 吗?

不是必须的。可以通过在 Activity 中 observe(LiveData) 手动更新 UI,但 DataBinding 能让代码更简洁,实现真正的声明式 UI。

5. Repository 模式的作用是什么?

作为数据层的统一入口,对 ViewModel 屏蔽数据来源(网络、数据库、SP、内存缓存),实现数据与业务逻辑解耦,便于测试和维护。

6. 为什么 LiveData 能避免内存泄漏?

LiveData 内部绑定了 LifecycleOwner,当 Activity/Fragment 销毁时(DESTROYED 状态),会自动移除 Observer,不再分发数据。

7. ViewModel 中可以持有 Activity 的 Context 吗?

不可以。ViewModel 生命周期比 Activity 长,若持有 Activity 的 Context 会导致内存泄漏。如需使用 Context,可继承 AndroidViewModel,它持有 Application Context。

8. LiveData 的粘性事件是什么?如何解决?

粘性事件指新注册的 Observer 会立即收到上次分发的值。解决方案:使用 SingleLiveEvent(官方推荐方案),或通过自定义 LiveData 包装器控制分发逻辑。

9. ViewModel 如何进行跨页面通信?

通常不推荐直接跨页面通信。可通过共享的 Repository、EventBus、或单例的 LiveData 实现,但需注意生命周期和内存泄漏问题。

10. DataBinding 的双向绑定原理是什么?

通过生成的 Binding 类,在数据变化时调用 setText() 更新 View;同时给 View 设置 Listener,在 View 变化时(如 EditText 输入)回调修改数据,实现双向同步。

11. LiveData 的 setValue() 为什么必须在主线程?

因为 LiveData 设计为直接更新 UI,而 Android 的 UI 操作必须在主线程。setValue() 内部会遍历 Observer 并直接回调,如果在子线程调用会导致 UI 更新异常。

12. postValue() 连续调用会丢值吗?

。postValue() 将值放入一个临时字段 mPendingData,然后 post 一个 Runnable 到主线程执行 setValue()。如果主线程还没执行,新的 postValue() 会覆盖 mPendingData,导致中间值丢失。

13. ViewModel 中可以直接更新 View 吗?

绝对不可以。ViewModel 不能持有 View 的引用(如 Activity、TextView),否则会造成内存泄漏,且破坏了 MVVM 的解耦原则。所有 UI 更新必须通过 LiveData 驱动。

14. ViewModel 如何更新数据的?

ViewModel 更新数据的核心就是 "通过 LiveData 发布状态变更":

  1. 内部持有 MutableLiveData(可写)
  2. 对外暴露 LiveData(只读)
  3. 业务方法 中调用 setValue() 或 postValue()
  4. View 层 通过 observe() 自动响应变化

这样实现了数据驱动 UI,ViewModel 不关心 View 是谁,只管更新数据;View 不关心数据从哪来,只管观察变化。

六、总结

一句话:ViewModel 持有数据,LiveData 传递数据,View 观察数据,数据变则 UI 变

相关推荐
安卓与AI研习社2 小时前
Android ANR 到底怎么定位?从触发原理到日志判断的完整方法
android
阿pin4 小时前
Android随笔-MVI
android·mvi
leobertlan4 小时前
【好玩系列】训练一个神经网络指导小孩玩游戏
android·机器学习·程序员
Android-Flutter5 小时前
android LeakCanary 工作原理 详解
android·kotlin
newcih5 小时前
从原理到实战:LSPosed 模块开发完全指南(2026 修订版)
android·xposed·lsposed
AFinalStone6 小时前
Android 7系统无障碍服务(一)全景图与架构概览
android·无障碍服务
Android-Flutter6 小时前
android 自定义view 详解
android·kotlin
WAsbry7 小时前
协程任务的失败控制:取消、异常传播与Supervisor
android
Android打工仔7 小时前
从 finally 理解程序的控制流:它为什么不是 catch 后面的代码?
android·kotlin
随遇丿而安7 小时前
第15周:Service 全功能 + 后台优化
android