Android 架构模式之 MVVM

  1. Android 架构模式之 MVC
  2. Android 架构模式之 MVP
  3. Android 架构模式之 MVVM

大家好!

作为 Android 程序猿,你熟悉 MVVM 架构吗。学过了 MVC 架构MVP 架构,为什么还要继续 MVVM 架构?又是什么原因导致它让人又爱又恨?

架构设计的目的

通过设计使程序模块化,模块内 高内聚、模块间 低耦合,提高开发效率,便于复用及后续维护。

对 MVVM 的理解

MVVM 架构图

上图是 MVVM 的架构图,我们都知道,MVVM架构中 M 代表 Model(模型)、V 代表 View(视图)、VM 代表 ViewModel(视图模型)。它们的职责分别是:

  1. View 负责接收用户的输入事件,然后将事件传递给 ViewModel;
  2. ViewModel 收到事件后,会进行业务分发,通知 Model 获取数据;
  3. Model 区分数据来源,进而通过不同渠道获取数据,拿到数据后返回给 ViewModel;
  4. ViewModel 进行后续处理,或者通知 View 更新 UI。

如果有看过 MVP 架构,会感觉这两个是一样的,不用怀疑,就是一样的,还有 MVC 也是一样的,因为这些都是从 MVC 演变过来的,只是每次演变都是为了解决特定的问题。
区别是实现方式不一样了,MVVM 架构是基于框架实现,变成了基于数据驱动。MVVM 是基于 DataBinding 框架进行数据 单向/双向 绑定,实现了 View 和 Model 的数据同步,这种方式增强了 xml 的能力,使得 Activity/Fragment 可以专职维护 View 的初始化,同时也减少了不少编码任务,这也体现了框架的强大之处。但是这里我们仅引入 DataBinding 库,以最少的引入,来了解 MVVM 架构的思路,至于那些常用的开发库,他们只是在 MVVM 架构的基础之上帮我们大大提高了开发效率、规避可能存在的问题风险。

代码实现

Model

BaseModel.java

public abstract class BaseModel {
    
    

}

View

BaseActivity.java

public abstract class BaseActivity<B extends ViewDataBinding, VM extends BaseViewModel> extends AppCompatActivity {
    
    

    protected B mBinding;
    protected VM mViewModel;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        this.mViewModel = createViewModel();
        setVariable();
    }

    /**
     * 初始化 ViewModel
     * @return
     */
    public abstract VM createViewModel();

    /**
     * 初始化 xml 中定义的变量
     */
    public abstract void setVariable();

    @Override
    protected void onDestroy() {
    
    
        super.onDestroy();

        if (mViewModel != null) {
    
    
            mViewModel.onDestroy();
            mViewModel = null;
        }
    }
}

ViewModel

BaseViewModel.java

public abstract class BaseViewModel<M extends BaseModel> {
    
    

    protected M mModel;

    public BaseViewModel() {
    
    
        this.mModel = createModel();
    }

    protected abstract M createModel();

    public void onDestroy() {
    
    
        if (mModel != null) {
    
    
            mModel = null;
        }
    }
}

上述代码中可以看到,View 中持有 ViewModel 引用,ViewModel 中持有 Model 引用,持有顺序为正向顺序,然后通过 setVariable 函数将 View 和 ViewModel 进行关联,关联后就会通过 DataBinding 在框架层进行数据绑定,代码很简洁,职责分配的也很清楚。

Android 中 MVVM 的问题

不幸的是,在 MVVM 架构中一旦出现了问题,会是噩梦般的存在,很难发现原因,甚至没有提示,所以我们在编写代码的时候务必勤于调试,完成一个小功能点就看一下效果,免得写了很多功能,最后一路报错,那样在排错的时候会无从下手、毫无思绪。
ViewModel 中会定义大量的数据绑定对象,以及 getter/setter 方法,会导致 ViewModel 越来越臃肿,可以考虑进一步提取操作。

Google 建议

  • 编写数据驱动型 UI
  • xml 与 Activity/Fragment 通过 databinding 进行关联
  • Activity/Fragment 尽可能保持精简
  • ViewModel 充当连接器
  • 通过 协程 处理耗时操作
  • 尽可能使用 依赖项注入 最佳实践,主要是构造函数注入。

注意事项

  • 避免 ViewModel 持有 任何与生命周期相关的类型的引用。如,Activity, Fragment, Context 或 Resources。如果某元素需要在 ViewModel 中使用 Context,则应重新严格评估其是否位于正确的层级中。
  • AndroidViewModel 中持有 applicationContext,但官方 建议 使用 ViewModel,而非 AndroidViewModel,不应在 ViewModel 中使用 Application。正确做法是将依赖项移至界面层或数据层。

try a demo

点击按钮,请求 wanandroid 网站的 banner 接口数据,将最后一条数据展示到UI
将 显示控件/输入控件 绑定到同一个 Bean 上,查看数据绑定的效果

代码结构

MVVM 架构的 Demo 是从 MVP 架构那套代码变更过来的,只涉及上述几个文件的变动,文件数/代码量都大大减少了,这里多了一个 IUpdateListener,主要用于定义数据更新的接口,Bean 中会实现更新接口,也可以不带它。

Bean

继承自 BaseObservable,是被观察者角色,View 充当观察者。
在需要关注的属性的 getter/setter 上通过 @Bindable 和 notifyPropertyChanged(BR.xx) 进行绑定

IUpdateListener.java

public interface IUpdateListener<T> {
    
    
    /**
     * 获取到新数据后,用于更新与 xml 绑定的实体类的属性值
     * @param t
     */
    void update(T t);
}

Banner.java

public class Banner extends BaseObservable implements IUpdateListener<Banner> {
    
    

    private String desc;
    private int id;
    private String imagePath;
    private int isVisible;
    private int order;
    private String title;
    private int type;
    private String url;

    public String getDesc() {
    
    
        return desc;
    }

    public void setDesc(String desc) {
    
    
        this.desc = desc;
    }

    public int getId() {
    
    
        return id;
    }

    public void setId(int id) {
    
    
        this.id = id;
    }

    public String getImagePath() {
    
    
        return imagePath;
    }

    public void setImagePath(String imagePath) {
    
    
        this.imagePath = imagePath;
    }

    public int getIsVisible() {
    
    
        return isVisible;
    }

    public void setIsVisible(int isVisible) {
    
    
        this.isVisible = isVisible;
    }

    public int getOrder() {
    
    
        return order;
    }

    public void setOrder(int order) {
    
    
        this.order = order;
    }

    @Bindable
    public String getTitle() {
    
    
        return title;
    }

    public void setTitle(String title) {
    
    
        this.title = title;
        notifyPropertyChanged(BR.title);
    }

    public int getType() {
    
    
        return type;
    }

    public void setType(int type) {
    
    
        this.type = type;
    }

    public String getUrl() {
    
    
        return url;
    }

    public void setUrl(String url) {
    
    
        this.url = url;
    }

    @Override
    public void update(Banner banner) {
    
    
        setDesc(banner.desc);
        setId(banner.id);
        setImagePath(banner.imagePath);
        setIsVisible(banner.isVisible);
        setOrder(banner.order);
        setTitle(banner.title);
        setType(banner.type);
        setUrl(banner.url);
    }

    @Override
    public String toString() {
    
    
        return "Banner{" +
                "desc='" + desc + '\'' +
                ", id=" + id +
                ", imagePath='" + imagePath + '\'' +
                ", isVisible=" + isVisible +
                ", order=" + order +
                ", title='" + title + '\'' +
                ", type=" + type +
                ", url='" + url + '\'' +
                '}';
    }
}

Model

请求接口,获取 banner 数据

MainModel.java

public class MainModel extends BaseModel {
    
    

    public void getNetworkBanner(ResponseCallback<List<Banner>> callback) {
    
    
        // 收到需求,请求接口数据
        NetworkRepository.getInstance().requestBanners(callback);
    }
}

View

包含一个 Button,点击请求接口数据
包含一个 TextView,用于回显数据
包含一个 EditText,用于查看数据绑定 UI 效果
注:xml 的变动是很重要的部分,它的功能增强了很多

activity_main.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"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <import type="com.villen.mvvm.MainViewModel" />

        <import type="com.villen.mvvm.bean.Banner" />

        <variable
            name="vm"
            type="MainViewModel" />

        <variable
            name="banner"
            type="Banner" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical"
        tools:context=".MainActivity">

        <Button
            android:id="@+id/btn_banner_info"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{(view) -> vm.getNetworkBanner()}"
            android:text="@string/get_network_info" />

        <EditText
            android:id="@+id/et_banner_info"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:hint="@string/hint_change_data"
            android:text="@={banner.title}" />

        <TextView
            android:id="@+id/tv_banner_info"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{banner.title}" />

    </LinearLayout>
</layout>

MainActivity.java

public class MainActivity extends BaseActivity<ActivityMainBinding, MainViewModel> {
    
    

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        mBinding = ActivityMainBinding.inflate(LayoutInflater.from(this));
        setContentView(mBinding.getRoot());
        super.onCreate(savedInstanceState);
    }

    @Override
    public MainViewModel createViewModel() {
    
    
        return new MainViewModel();
    }

    @Override
    public void setVariable() {
    
    
        mBinding.setVm(mViewModel);
        mBinding.setBanner(mViewModel.getBanner());
    }
}

ViewModel

通过 model 获取数据
通知 UI 更新

MainViewModel.java

public class MainViewModel extends BaseViewModel<MainModel> {
    
    

    private Banner mBanner;
    /**
     * 获取实体类对象,用于 xml 中数据绑定
     */
    public Banner getBanner() {
    
    
        if (mBanner == null) {
    
    
            mBanner = new Banner();
        }
        return mBanner;
    }

    @Override
    protected MainModel createModel() {
    
    
        return new MainModel();
    }

    /**
     * 获取 banner 数据
     */
    public void getNetworkBanner() {
    
    
        if (mModel == null) {
    
    
            return;
        }
        // 收到新需求,分发给 model 处理
        mModel.getNetworkBanner(new ResponseCallback<List<Banner>>() {
    
    
            @Override
            public void onSuccess(List<Banner> banners) {
    
    
                if (banners != null && banners.size() > 0) {
    
    
                    mBanner.update(banners.get(2));
                }
            }

            @Override
            public void onFail(String msg) {
    
    
                Log.e("network", msg);
            }
        });
    }
}

效果展示

效果展示

附上源码链接

致谢:
感谢 wanandroid 提供的开放API

参考:
Android DataBinding 从入门到进阶,看这一篇就够

猜你喜欢

转载自blog.csdn.net/qq_34801506/article/details/141472846