Android MVVM

本文介绍了Android中的MVVM架构模式,对比了MVC和MVP的优劣,重点探讨了MVVM的低耦合特性。同时,文章提到了Android DataBinding框架,它是Google为了解决界面逻辑问题而提出的。文中还给出了一个简单的MVVM实践DEMO,展示了如何在Activity中绑定View与ViewModel,以及如何处理数据刷新和界面更新。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Android MVVM

Android框架

如今,Android框架日益发展,从MVC到MVP、MVVM

  • MVC(Model-View-Controller)自不必说,大家应该都早已知道,在Android中,由于Activity层即承担View的职责,又有Controller职责,导致Activity异常臃肿,测试难,维护难。

  • MVP(Model-View-Presenter)是从MVC演化而来,将View层与Model层解耦,Activity层作为View,只与Presenter层进行交互,Presenter通过接口对View进行操作,而Model层也只与Presenter交互,View层和Model层不进行直接交互,Presenter作为两者的桥梁。如此一来三者各司其职,降低耦合,又便于测试,便于维护。

  • MVVM(Model-View-ViewModel)最早是由微软提出的,在Android中,MVVM和MVP比较相似,但它的核心在于DataBinding,View的变化可以自动的反映在ViewModel,ViewModel的数据变化也会自动反应到View上。这样我们就不用处理接收时间和View更新的工作,框架已经做好了。

MVC、MVP、MVVM优劣对比

  • MVC:其实Android本身还是符合MVC架构的,但是Activity又是View又是Controller,导致Activity臃肿,高度耦合,动辄千行代码,维护测试困难。

  • MVP:

    优势:MVP对于MVC做了优化,V层与P层通过接口交互,Activity只负责UI变化,业务逻辑放在P层之中,M层与V层不能直接访问,M与V层解耦。它的结构也更清晰了,每层都有它对应的职责,这样的结构便于维护与单元测试。

    劣势:以为V层与P层通过接口交互,定义接口就成为了一个问题,粒度太小,就会导致接口过多,对应Activity代码也会很多,粒度太大,解耦效果就差。且其本身虽然通过接口访问进行解耦,但是如果控件变更,对应的逻辑也要做相应的变更

  • MVVM:万事万物皆有利弊

    优势:

    1. 低耦合,在MVVM中,数据是独立于UI的,数据和业务逻辑是在独立的ViewModel中,不涉及任何UI相关的事,不持有UI控件的引用,因此逻辑只需要关心数据即可。
    2. MVVM同样是便于维护就单元测试的,但是由于MVVM的ViewModel并不像MVP的Presenter一样是纯java代码,因此测试框架选择不同。

    劣势:

    1. 结构相对复杂
    2. 数据绑定是的bug很难被调试。当我们看到界面异常的时候,可能是View的代码有bug,也可能是Model的代码有问题。数据绑定让一个位置的bug快速传递到其他位置,要定义到原始出问题的地方就不那么容易了。
    3. 对于过大的项目,数据绑定要话费更大的内存。
Android DataBinding

DataBinding是google搞出来的数据绑定框架,是解决界面逻辑的一个黑科技。

DataBinding已经有很多大牛写过相关的文档,优美的文笔,严谨的逻辑,我是自愧不如,就不再多做更多的介绍,只在此推荐如下一篇文章。结合DataBinding官方文档,相信大家可以快速入门、精通。

DataBinding入门:Android MVVM到底是啥?看完就明白了

DataBinding官方文档:https://developer.android.com/topic/libraries/data-binding/index.html#build_environment

MVVM实践

Activity中绑定View与ViewModel

ViewModel中持有Model的引用,并且通过DataBinding绑定在View上,当data刷新时,view同步更新

PODEMO:登陆然后设置HOME界面List

包括两个页面,登陆页面和主页面

登陆页面分为LoginModel,LoginBean,LoginActivity,LoginViewModel

LoginModel中完成与服务器的交互,校验用户名密码的准确性

/**
 * 模拟网络请求验证登录信息,实际应有回调
 * @param name 用户名
 * @param pwd 密码
 * @return 校验结果
 */
public boolean checkLoginInfo(String name, String pwd) {
    return "vicky".equals(name) && "123".equals(pwd);
}

LoginBean中包含用户名和密码

private String loginName;
private String loginPwd;

public String getLoginName() {
    return loginName;
}

public void setLoginName(String loginName) {
    this.loginName = loginName;
}

public String getLoginPwd() {
    return loginPwd;
}

public void setLoginPwd(String loginPwd) {
    this.loginPwd = loginPwd;
}

LoginActivity中绑定xml布局,并设置EditText的TextWatcher

// DataBinding根据xml名称设置类,用以绑定xml
private ActivityLoginBinding mBinding;
// 持有ViewModel对象可以调用其中的方法
private LoginViewModel mViewModel;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
	// DataBinding绑定方式
    mBinding = DataBindingUtil.setContentView(this, R.layout.activity_login);
    mViewModel = new LoginViewModel(new LoginBean());
	// 绑定完成后要设置ViewModel
    mBinding.setLogin(mViewModel);
    attachListener();
}

private void attachListener() {
    mBinding.loginNameEdit.addTextChangedListener(mTextWatcher);
    mBinding.loginPwdEdit.addTextChangedListener(mTextWatcher);
}

private TextWatcher mTextWatcher = new TextWatcher() {
    @Override
    public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {

    }

    @Override
    public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {

    }

    @Override
    public void afterTextChanged(Editable editable) {
        if (mBinding.loginNameEdit.getText().length() > 0
                && mBinding.loginPwdEdit.getText().length() > 0) {
            mViewModel.setBtnVisible(true);
        } else {
            mViewModel.setBtnVisible(false);
        }
    }
};

Login xml布局

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

<data>

    <variable
        name="login"
        type="com.daydayup.mvvm.viewmodel.LoginViewModel" />
</data>

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

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:orientation="horizontal">

        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginEnd="10dp"
            android:layout_marginStart="10dp"
            android:layout_weight="1"
            android:gravity="end"
            android:paddingBottom="5dp"
            android:paddingTop="5dp"
            android:text="@string/login_name"
            android:textSize="15sp" />

        <EditText
            android:id="@+id/login_name_edit"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginEnd="10dp"
            android:layout_marginStart="10dp"
            android:layout_weight="3"
            android:background="@null"
            android:hint="@string/input_login_name"
            android:inputType="text"
            android:paddingBottom="5dp"
            android:paddingTop="5dp"
            android:text="@={login.loginName}"
            android:textSize="15sp"/>

    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginEnd="10dp"
            android:layout_marginStart="10dp"
            android:layout_weight="1"
            android:gravity="end"
            android:paddingBottom="5dp"
            android:paddingTop="5dp"
            android:text="@string/login_pwd"
            android:textSize="15sp" />

        <EditText
            android:id="@+id/login_pwd_edit"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginEnd="10dp"
            android:layout_marginStart="10dp"
            android:layout_weight="3"
            android:background="@null"
            android:hint="@string/input_login_pwd"
            android:inputType="numberPassword"
            android:paddingBottom="5dp"
            android:paddingTop="5dp"
            android:text="@={login.loginPwd}"
            android:textSize="15sp" />

    </LinearLayout>

    <Button
        android:id="@+id/login_btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginEnd="10dp"
        android:layout_marginStart="10dp"
        android:layout_marginTop="20dp"
        android:enabled="@{login.btnVisible}"
        android:onClick="@{login.handleLogin}"
        android:text="@string/login_btn_txt" />

</LinearLayout>
</layout>

LoginViewModel中设置获取用户名和密码,通过View的绑定显示这些信息在界面上,并制定登陆按钮是否可点击及点击时间

// 持有Model对象,调用接口等
private LoginModel mMode;
// 显示并更新界面数据
private LoginBean mBean;
// 双向绑定,实时控制btn的Enable
private boolean btnVisible;

// 参数按照需要添加
public LoginViewModel(LoginBean bean) {
    this.mBean = bean;
    mMode = new LoginModel();
}

public String getLoginName() {
    return mBean.getLoginName();
}

public void setLoginName(String loginName) {
    mBean.setLoginName(loginName);
}

public String getLoginPwd() {
    return mBean.getLoginPwd();
}

public void setLoginPwd(String loginPwd) {
    mBean.setLoginPwd(loginPwd);
}

// 双向绑定的写法 @Bindable及notifyPropertyChanged
@Bindable
public boolean isBtnVisible() {
    return btnVisible;
}

public void setBtnVisible(boolean btnVisible) {
    this.btnVisible = btnVisible;
    notifyPropertyChanged(BR.btnVisible);
}

// 处理btn的点击事件 
public void handleLogin(View view) {
    Context context = view.getContext();
    if (mMode.checkLoginInfo(mBean.getLoginName(), mBean.getLoginPwd())) {
        context.startActivity(new Intent(context, HomeActivity.class));
    } else {
        Toast.makeText(context, "error name or pwd", Toast.LENGTH_SHORT).show();
    }
}

主页面结构登录界面相同

HomeModel获取主页面列表

// 模拟返回数据,此处应有回调
public ArrayList<HomeBean> getList() {
    ArrayList<HomeBean> list = new ArrayList<>();
    for (int i = 0; i < 3; i++) {
        HomeBean homeBean = new HomeBean();
        homeBean.setDescription("Description:" + i);
        homeBean.setKeyWords("KeyWords:" + i);
        homeBean.setSummary("Summary:" + i);
        homeBean.setImg("http://img.bizhi.sogou.com/images/2012/03/14/140763.jpg");
        list.add(homeBean);
    }
    return list;
}

HomeBean主页列表item(本demo只有列表,如有其它,可以定义第二个bean,如Login一样定义到ViewModel中)

private String description;
private String img;
private String keyWords;
private String summary;

public String getDescription() {
    return description;
}

public void setDescription(String description) {
    this.description = description;
}

public String getImg() {
    return img;
}

public void setImg(String img) {
    this.img = img;
}

public String getKeyWords() {
    return keyWords;
}

public void setKeyWords(String keyWords) {
    this.keyWords = keyWords;
}

public String getSummary() {
    return summary;
}

public void setSummary(String summary) {
    this.summary = summary;
}

// ImageView绑定方法(PS:经测试"img"全工程共享)
@BindingAdapter("img")
public static void loadImg(ImageView imgView, String url) {
    Glide.with(imgView.getContext()).load(url).into(imgView);
}

HomeActivity绑定xml,在onResume时刷新列表数据

// 持有Model对象,调用接口等
private HomeViewModel mViewModel;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ActivityHomeBinding mBinding = DataBindingUtil.setContentView(this, R.layout.activity_home);
	mViewModel = new HomeViewModel();
    mBinding.setHome(mViewModel);
}

@Override
protected void onResume() {
    super.onResume();
    mViewModel.refreshList();
}

Home xml布局

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto">

<data>

    <variable
        name="home"
        type="com.daydayup.mvvm.viewmodel.HomeViewModel" />
</data>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ListView
        android:id="@+id/home_list_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:onItemClick="@{home.onItemClick}"
        app:itemView="@{home.itemView}"
        app:items="@{home.items}" />

</LinearLayout>

</layout>

Home item xml布局

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">

<data>

    <variable
        name="homeItem"
        type="com.daydayup.mvvm.model.HomeBean" />
</data>

<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="96dp">

    <ImageView
        android:id="@+id/iv"
        android:layout_width="96dp"
        android:layout_height="96dp"
        android:contentDescription="@null"
        android:padding="6dp"
        app:img="@{homeItem.img}" />

    <TextView
        android:id="@+id/list_view_description"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="8dp"
        android:layout_marginStart="8dp"
        android:layout_toEndOf="@id/iv"
        android:layout_toRightOf="@id/iv"
        android:ellipsize="end"
        android:text="@{homeItem.description}" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_marginBottom="2dp"
        android:layout_marginLeft="8dp"
        android:layout_marginStart="8dp"
        android:layout_toEndOf="@id/iv"
        android:layout_toRightOf="@id/iv"
        android:text="@{homeItem.keyWords}" />
</RelativeLayout>

</layout>

HomeViewModel显示在界面上的数据定义及item点击事件

private HomeModel mModel;

// list item(列表的bean对象如此加载)
public final ObservableList<HomeBean> items = new ObservableArrayList<>();
// item view
public final ItemView itemView = ItemView.of(BR.homeItem, R.layout.item_list_view);

public HomeViewModel() {
    mModel = new HomeModel();
}

public void refreshList() {
    items.clear();
    items.addAll(mModel.getList());
}

// item点击事件
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
    Context context = view.getContext();
    Toast.makeText(context, "It's " + position + " item.", Toast.LENGTH_SHORT).show();
}

简易DEMO,仅供参考!

Over.

参考:

  1. 认清Android框架 MVC,MVP和MVVM
  2. Android MVVM到底是啥?看完就明白了
  3. Android数据绑定框架DataBinding
  4. 如何构建Android MVVM 应用框架
  5. HTML特殊转义字符对照表
<think>嗯,用户想了解Android中的MVVM架构模式和实现方法。首先,我需要回忆一下MVVM的基本概念和组成部分。根据引用[3],MVVM有三个主要组件:Model、View和ViewModel。Model负责处理数据和业务逻辑,View负责显示数据和用户交互,ViewModel则处理请求并为View提供所需的数据。 接下来,我应该分步骤解释MVVM的实现。首先可能需要介绍如何分层结构,比如Model层通常包含数据源,比如数据库或网络请求。然后ViewModel层如何连接Model和View,使用LiveData或StateFlow这样的可观察数据持有者。View层通常是Activity或Fragment,负责观察ViewModel中的数据变化并更新UI。 用户可能对具体的代码实现感兴趣,比如如何创建ViewModel,如何在View中观察数据变化。引用[5]提到了使用导航组件和safe args插件进行Fragment之间的导航,这可能也是一个相关点,可以结合MVVM的结构来说明如何实现界面跳转。 还需要考虑数据绑定的方式,比如使用Data Binding库来减少样板代码,或者使用ViewBinding。同时,可能需要提到Repository模式,作为Model层的一部分,用于管理多个数据源的协调,比如本地数据库和远程API的结合。引用[1]中的Pokedex项目就展示了MVVM和Repository模式的结合,这可能是一个很好的例子。 另外,用户可能想知道MVVM的优势,比如分离关注点,便于测试,以及如何避免内存泄漏,比如在ViewModel中使用生命周期感知的组件。同时,可以提到常见的库,如Jetpack中的ViewModel、LiveData,以及协程或RxJava用于异步操作。 在实现步骤中,可能需要指导用户如何添加必要的依赖项,例如在build.gradle中引入Lifecycle组件、Data Binding等。然后创建各个层级的类,比如Entity、Repository、ViewModel,并在Activity/Fragment中初始化ViewModel,观察数据变化。 最后,用户可能会有疑问,比如如何测试ViewModel,如何处理网络请求的错误情况,或者如何结合其他架构组件如Room、Retrofit等。这些问题可以作为相关问题的部分,引导用户进一步学习。</think>### Android MVVM 架构模式实现方法分步解析 #### 一、MVVM 核心组件(三要素) 1. **Model 层** 负责数据获取和业务逻辑,包含: - 数据实体类(如`User`) - 数据源(如`LocalDataSource`本地数据库、`RemoteDataSource`网络请求) - 仓库类(`Repository`协调多数据源)[^1][^3] 2. **ViewModel 层** 将数据转换为可观察状态,通过`LiveData`/`StateFlow`向View暴露数据: ```kotlin class UserViewModel : ViewModel() { private val _users = MutableLiveData<List<User>>() val users: LiveData<List<User>> = _users fun loadUsers() { viewModelScope.launch { _users.value = repository.getUsers() // 从仓库获取数据 } } } ``` 3. **View 层** Activity/Fragment负责: - 初始化数据绑定 - 监听ViewModel数据变化 - 处理UI交互事件 示例观察数据: ```kotlin viewModel.users.observe(viewLifecycleOwner) { userList -> adapter.submitList(userList) // 更新RecyclerView } ``` #### 二、关键技术实现 1. **数据绑定(Data Binding)** 在`build.gradle`启用: ```gradle android { buildFeatures { dataBinding true } } ``` XML布局绑定ViewModel: ```xml <layout> <data> <variable name="viewModel" type="com.example.UserViewModel"/> </data> <TextView android:text="@{viewModel.userName}"/> </layout> ``` 2. **依赖注入** 使用Hilt实现依赖管理: ```kotlin @HiltViewModel class UserViewModel @Inject constructor( private val repository: UserRepository ) : ViewModel() ``` 3. **导航组件集成** 使用`Navigation Component`进行Fragment跳转: ```kotlin findNavController().navigate( HomeFragmentDirections.actionToDetailFragment(userId) ) ``` 通过`safe args`插件保证类型安全[^5] #### 三、典型代码结构 ``` app/ ├── model/ │ ├── User.kt # 数据实体 │ ├── UserRepository.kt # 仓库类 ├── view/ │ ├── UserFragment.kt │ └── UserAdapter.kt └── viewmodel/ └── UserViewModel.kt ``` #### 四、优势与注意事项 1. **核心优势** - 关注点分离(数据逻辑与UI解耦) - 自动化的生命周期管理 - 方便单元测试(ViewModel可独立测试) 2. **常见问题处理** - **内存泄漏预防**:使用`viewLifecycleOwner`替代Activity生命周期 - **状态管理**:通过`SavedStateHandle`保存配置变更时的临时数据 - **错误处理**:在ViewModel中封装错误状态: ```kotlin sealed class UiState { object Loading : UiState() data class Success(val data: T) : UiState() data class Error(val message: String) : UiState() } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值