美文网首页
我理解的MVVM架构及实战Demo

我理解的MVVM架构及实战Demo

作者: TokyoZ | 来源:发表于2019-11-14 09:32 被阅读0次

    我理解的MVVM架构及实战Demo

    简介

    MVVM架构模式,即Model-View-ViewModel三个层级,MVVM模式出来的时间已经很长了,网上关于MVVM模式的解析也有很多,我这里只说一下我自己的理解,基本上是和MVP模式相比较的一个差异。

    先说一下MVVM中,我理解的各层的作用:

    • Model: 数据处理层,用来存储数据和处理数据,这一层的作用和MVP中的Model层类似;

    • View:UI显示更新和处理与用户的交互。这一层和MVP中的View层有一点区别,在MVP模式中,View的界面更新是由Presenter层驱动的,而在MVVM模式中,View的界面更新是采用观察者模式去动态监听数据变化,数据改变后由自己来改变,变被动为主动,这样View层与其他层的耦合度进一步降低了。并且若采用了DataBinding的形式后,View的UI数据更改完全由程序自动化更改,我们少去很多初始化工作和setText()等繁复操作。

    • ViewModel:处理逻辑中转任务的媒介,很类似于MVP模式中的Presenter层,但是它相比Presenter层的优点在于,它不用去通知View层去更新UI界面,降低了和View的耦合度,并且同一个ViewModel可以同时绑定多个View层,达到共同更新数据的目的。

    MVVM模式中的一些相关知识点

    Demo中会涉及一下一些关于MVVM模式的知识,不清楚的同学可以先了解一下,加深印象。

    Lifecycle

    Lifecycle是Google官方提供的方便管理生命周期事件的方式,可以更方便的监听生命周期变化事件,它以注解的方式作用在方法上,当生命周期方法调用时,它也会被跟随调用。

    LiveData

    LiveData是一种用来持有数据的对象,并且当数据改变时,去通知处于active状态的观察者去做出相应的改变。在Android中,处于STARTED或者RESUME状态下的观察者被认为是active状态。LiveData基于Lifecycle。

    也就是说,当多个View层共享同一组数据时(一个Activity和多个Fragment),这组数据可以通过LiveData的形式持有,因为LiveData持有的数据是根据View的当前状态去通知的,因此,它不会产生一些类似于View被销毁后还去更改UI的一些奇怪的crash现象。

    并且,LiveData通过Lifecycle的方式,已经监听了View的生命周期,它会跟随View层在View被销毁的时候销毁(旋转屏等重建行为不会受影响)。

    DataBinding

    Databing是Google发布的一个用来支持MVVM模式的框架,它的主要作用是用来降低布局和逻辑的耦合度。
    以前,我们都是在布局xml文件中写好控件,然后通过findViewById的形式去手动更新UI,DataBindind的出现,让我们省去了查找控件和手动更新UI的操作,我们仅需要把布局中的控件和Model层的数据关联起来,当数据改变时,UI也会跟着改变,达到双向绑定的目的。

    实战演练

    理论知识就介绍到这里,网上有很多相关的资料介绍,大家可以自行查找。这里,我通过实战Demo来告诉一下大家我理解的MVVM模式是什么样的,若有不正确,还请指正。

    准备工作

    这次的Demo也是参考网上其他人的博客来写的,功能是获取Bing网站首页的图片并显示,Bing网站首页图片的获取地址是:

    https://cn.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1
    

    其中,idx的数字表示不同的图片,获取到的Json文件格式如下:

    {
        "images":Array[1],
        "tooltips":{
            "loading":"正在加载...",
            "previous":"上一个图像",
            "next":"下一个图像",
            "walle":"此图片不能下载用作壁纸。",
            "walls":"下载今日美图。仅限用作桌面壁纸。"
        }
    }
    

    其中,"images":Array[1]中包含图片的URL和描述,展开后是这样的:

    {
        "startdate":"20190901",
        "fullstartdate":"201909011600",
        "enddate":"20190902",
        "url":"/th?id=OHR.RamsauWimbachklamm_ZH-CN1602837695_1920x1080.jpg&rf=LaDigue_1920x1080.jpg&pid=hp",
        "urlbase":"/th?id=OHR.RamsauWimbachklamm_ZH-CN1602837695",
        "copyright":"拉姆绍的Wimbachklamm峡谷,德国巴伐利亚 (© Westend61 GmbH/Alamy)",
        "copyrightlink":"https://www.bing.com/search?q=Wimbachklamm%E5%B3%A1%E8%B0%B7&form=hpcapt&mkt=zh-cn",
        "title":"",
        "quiz":"/search?q=Bing+homepage+quiz&filters=WQOskey:%22HPQuiz_20190901_RamsauWimbachklamm%22&FORM=HPQUIZ",
        "wp":true,
        "hsh":"9f822f5607e3cec381408303a73c1986",
        "drk":1,
        "top":1,
        "bot":1,
        "hs":[
    
        ]
    }
    

    通过Json文件我们得到,真正的图片地址是https://www.bing.com/开头,接上Json中Array中的url,即为图片地址。

    下面不多说,直接开撸代码。

    因为本Demo使用了DataBinding + LiveData的形式,因此需要首先在module的build.gradle文件中加上:

    android {
        ...
        
        dataBinding {
            enabled = true
        }
    }
    

    然后,依赖如下框架:

    dependencies {
        ...
    
        implementation 'com.squareup.retrofit2:retrofit:2.4.0'
        implementation 'com.squareup.retrofit2:adapter-rxjava2:2.4.0'
        implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
        implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
        implementation 'io.reactivex.rxjava2:rxjava:2.1.12'
        implementation 'com.github.bumptech.glide:glide:4.6.1'
        annotationProcessor 'com.github.bumptech.glide:compiler:4.6.1'
    }
    

    当然,还要记得在AndroidManifest文件上加上访问网络的权限:

    <uses-permission android:name="android.permission.INTERNET" />
    

    1、Model层

    Model层的职责就是用来处理数据。

    1.1 Bean文件

    由于我们只关心Json数据中的url地址和图片描述,因此,我们的Bean文件极为简单:

    获取Url和描述的Bean文件ImageUrlBean

    public class ImageUrlBean {
    
        private List<UrlBean> images;
    
        public List<UrlBean> getImages() {
            return images;
        }
    
        public void setImages(List<UrlBean> images) {
            this.images = images;
        }
    
        public static class UrlBean {
    
            public static final String BASE_IMAGE_ADDRESS_URL = "https://cn.bing.com/";
            
            private String url;
            
            private String copyright;
    
            public String getUrl() {
                return url;
            }
    
            public void setUrl(String url) {
                this.url = url;
            }
    
            public String getCopyright() {
                return copyright;
            }
    
            public void setCopyright(String copyright) {
                this.copyright = copyright;
            }
        }
    
    }
    

    由于网络等其他原因会存在获取数据失败的风险,,因此在ImageUrlBean的基础上,加封了一层数据层UrlData,用来判断是否获取到了真实数据:

    public class UrlData<T> {
    
        private T data;
        private String errorMsg;
    
        public UrlData(){}
    
        public UrlData(T data, String errorMsg) {
            this.data = data;
            this.errorMsg = errorMsg;
        }
    
        public T getData() {
            return data;
        }
    
        public void setData(T data) {
            this.data = data;
        }
    
        public String getErrorMsg() {
            return errorMsg;
        }
    
        public void setErrorMsg(String errorMsg) {
            this.errorMsg = errorMsg;
        }
    }
    

    1.2 封装网络

    本次Demo的网络框架用的是Retrofit2 + RxJava2,不在本次的研究范围内,不了解的朋友请自行学习。

    获取网络的接口IGetUrlService

    public interface IGetUrlService {
    
        @GET("HPImageArchive.aspx")
        Observable<ImageUrlBean> getUrl(@Query("format") String format,
                                        @Query("idx") int idx,
                                        @Query("n") int n);
    }
    

    获取到Url数据后的回调GetUrlCallback

    public interface GetUrlCallback {
    
        void handleUrl(UrlData<ImageUrlBean.UrlBean> data);
    
    }
    

    网络工具类HttpUtil:

    public class HttpUtil {
    
        private Retrofit mRetrifit;
    
        private HttpUtil(){
            mRetrifit = new Retrofit.Builder()
                    .baseUrl(ImageUrlBean.UrlBean.BASE_IMAGE_ADDRESS_URL)
                    .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                    .addConverterFactory(GsonConverterFactory.create())
                    .build();
        }
    
        public Observable<ImageUrlBean> getImageUrl(String format, int idx, int n) {
            return mRetrifit.create(IGetUrlService.class)
                    .getUrl(format, idx, n);
        }
    
        public static HttpUtil getInstance() {
            return Holder.mInstance;
        }
    
        private static class Holder {
            private static final HttpUtil mInstance = new HttpUtil();
        }
    }
    

    1.3 获取数据的Model

    用来获取数据的ImageUrlModel

    public class ImageUrlModel {
    
        private HttpUtil mHttpUtil;
    
        public ImageUrlModel() {
            mHttpUtil = HttpUtil.getInstance();
        }
    
        public void getImageUrl(String format, int idx, int n, final GetUrlCallback callback) {
            mHttpUtil.getImageUrl(format, idx, n)
                    .subscribeOn(Schedulers.io())
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribe(new Observer<ImageUrlBean>() {
                        @Override
                        public void onSubscribe(Disposable d) {
    
                        }
    
                        @Override
                        public void onNext(ImageUrlBean imageUrlBean) {
                            UrlData<ImageUrlBean.UrlBean> data =
                                    new UrlData<>(imageUrlBean.getImages().get(0), null);
                            callback.handleUrl(data);
                        }
    
                        @Override
                        public void onError(Throwable e) {
                            UrlData<ImageUrlBean.UrlBean> data = new UrlData<>(null, e.getMessage());
                            callback.handleUrl(data);
                        }
    
                        @Override
                        public void onComplete() {
    
                        }
                    });
        }
    }
    

    2、ViewModel层

    ViewModel层的职责是用来处理中转逻辑,将真正获取数据的操作交给Model去执行,然后将获取到的数据更改到自身的LiveData中去。LiveData中的数据改变后,会去通知监听它改变的、当前状态是可见的View们去更改UI。

    ImageUrlViewModel

    public class ImageUrlViewModel extends ViewModel {
    
        public static final int STATE_NO_PRE_IMAGE = 0; // 没有前一张图片时的状态
        public static final int STATE_NORMAL_IMAGE = 1; // 图片正常获取的状态
    
        /**
         * 放在LiveData中的数据,会被View监听,
         * 当数据改变且View层属于可见状态,则会去通知View层更新数据
         */
        private MutableLiveData<UrlData<ImageUrlBean.UrlBean>> mData;
    
        // 用来当特殊情况下更新UI,比如没有前一张图片时,提醒View层弹出Toast提醒用户
        private MutableLiveData<Integer> mState;
    
        /**
         * 本例没有用到Context,
         * 若需要,则必须传Application的Context,达到与View层独立的目的。
         */
        private Context mContext;
    
        private ImageUrlModel model;
    
        private GetUrlCallback callback;
    
        private int index; // 当前获取的哪一张图片的索引
    
        public ImageUrlViewModel(){
            this(null);
        }
    
        public ImageUrlViewModel(Context context) {
            this.mContext = context != null ?
                    context.getApplicationContext() : null;
            mData = new MutableLiveData<>();
            mState = new MutableLiveData<>();
            model = new ImageUrlModel();
            callback = new GetUrlCallback() {
                @Override
                public void handleUrl(UrlData<ImageUrlBean.UrlBean> data) {
                    // ViewModel层仅仅处理数据,界面的改变逻辑由Activity去执行
                    mData.setValue(data);
                }
            };
        }
    
        public MutableLiveData<UrlData<ImageUrlBean.UrlBean>> getImageUrl() {
            return mData;
        }
    
        public MutableLiveData<Integer> getState() {
            return mState;
        }
    
        public void loadImage() {
            model.getImageUrl("js", index, 1, callback);
        }
    
        public void loadNextImage() {
            index++;
            model.getImageUrl("js", index, 1, callback);
        }
    
        public void loadPreImage() {
            if(index == 0) {
                // 没有数据时,通过更改state的值,去通知View层更改数据
                mState.setValue(STATE_NO_PRE_IMAGE);
                mState.setValue(STATE_NORMAL_IMAGE);
                return;
            }
            index--;
            model.getImageUrl("js", index, 1, callback);
        }
    
    }
    

    3、View层

    首先编写xml布局,由于本例采用DataBinding框架,因此布局可能有所不同:

    <?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>
            <variable
                name="image"
                type="com.zw.mvvmdemo.bean.ImageUrlBean.UrlBean"/>
    
            <!-- 用来处理点击事件 -->
            <variable
                name="clicker"
                type="com.zw.mvvmdemo.BingImageActivity.Clicker"/>
    
            <!-- 用来处理一些特殊UI更新变化 -->
            <variable
                name="uiChanger"
                type="Integer"/>
    
        </data>
    
    
        <android.support.constraint.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">
    
            <ImageView
                android:id="@+id/iv"
                url="@{image.BASE_IMAGE_ADDRESS_URL + image.url}"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                android:layout_width="match_parent"
                android:layout_height="200dp" />
    
            <TextView
                android:id="@+id/tv"
                android:text="@{image.copyright}"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toBottomOf="@id/iv"
                android:layout_marginTop="20dp"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center"
                android:textSize="20sp"/>
    
            <Button
                android:id="@+id/btn_pre"
                android:onClick="@{clicker.onClick}"
                android:layout_width="0dp"
                android:layout_height="50dp"
                android:text="上一张"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toBottomOf="@id/tv"
                android:layout_marginTop="20dp"
                app:layout_constraintWidth_percent="0.5"/>
    
            <Button
                android:id="@+id/btn"
                android:onClick="@{clicker.onClick}"
                android:layout_width="0dp"
                android:layout_height="50dp"
                android:text="加载"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toBottomOf="@id/btn_pre"
                android:layout_marginTop="20dp"
                app:layout_constraintWidth_percent="0.5"/>
    
            <Button
                android:id="@+id/btn_next"
                android:onClick="@{clicker.onClick}"
                android:layout_width="0dp"
                android:layout_height="50dp"
                android:text="下一张"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toBottomOf="@id/btn"
                android:layout_marginTop="20dp"
                app:layout_constraintWidth_percent="0.5"/>
    
    
        </android.support.constraint.ConstraintLayout>
    
    </layout>
    

    布局和我们常见的布局不一样,根标签是<layout>,然后是两个平级的标签<data>和真正的布局文件,布局文件比较好理解,就不过多介绍了,介绍一下<data>

    当你需要与布局文件中的某个控件进行双向绑定的时候,可以将你的某个对象与控件绑定。比如上述文件中的

    <variable
        name="image"
        type="com.zw.mvvmdemo.bean.ImageUrlBean.UrlBean"
    />
    

    它与布局文件中的

    <ImageView
        android:id="@+id/iv"
        url="@{image.BASE_IMAGE_ADDRESS_URL + image.url}"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:layout_width="match_parent"
        android:layout_height="200dp" 
    />
    

    对应,其中variable标签中:

    • name:需要绑定数据的名称,可按照自己的命名习惯命名;
    • type:需要绑定数据的类;

    在此例中,当需要用到UrlBean时,可以通过@{}操作符并通过name来填写符合需求的表达式。比如在此例中,我们给ImageViewurl属性绑定了UrlBean这个实例,当UrlBeanimage.url变化时,ImageViewurl也会跟着改变。

    url是我们自定义的一个属性,这个属性并不是在style.xml文件中定义的,而是通过DataBinding的注解BindingAdapter来完成的,我们可以通过自定义属性,来完成我们想要进行处理的逻辑操作。

    GetBingImageAdapter

    public class GetBingImageAdapter {
    
        @BindingAdapter("url")
        public static void setImage(ImageView iv, String url) {
            Glide.with(iv)
                    .load(url)
                    .into(iv);
        }
    }
    

    ImageView中的url改变时,会回调此Adapter,然后回调setImage()

    然后,我们看看BingImageActivity

    public class BingImageActivity extends AppCompatActivity {
    
        private ActivityBingImageBinding binding;
        private ImageUrlViewModel mViewModel;
        private ProgressDialog mDialog;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
    
            binding = DataBindingUtil
                    .setContentView(this, R.layout.activity_bing_image);
    
             // 建立ViewModel,并将Activity的生命周期绑定到ViewModel上
            mViewModel = new ViewModelProvider(
                    this,
                    ViewModelProvider.AndroidViewModelFactory.getInstance(getApplication()))
                    .get(ImageUrlViewModel.class);
    
            // 为ViewModel的UrlData建立数据监听,并监听数据变化,根据数据更新UI
            mViewModel.getImageUrl().observe(this, new Observer<UrlData<ImageUrlBean.UrlBean>>() {
                @Override
                public void onChanged(@Nullable UrlData<ImageUrlBean.UrlBean> data) {
                    mDialog.dismiss();
                    if(data.getErrorMsg() != null) {
                        Toast.makeText(BingImageActivity.this, data.getErrorMsg(),
                                Toast.LENGTH_LONG).show();
                    }
                    // 监听到数据变化后,通过databinding更改布局UI,若未使用databinding,则需要自己写相关UI更新逻辑
                    binding.setImage(data.getData());
            }
            });
    
            // 监听特殊状态下更新UI的操作
            mViewModel.getState().observe(this, new Observer<Integer>() {
                @Override
                public void onChanged(@Nullable Integer integer) {
                    mDialog.dismiss();
                    if(integer == ImageUrlViewModel.STATE_NO_PRE_IMAGE) {
                        Toast.makeText(BingImageActivity.this, "没有前一张图片了!",
                                Toast.LENGTH_LONG).show();
                    }
                }
            });
    
            binding.setClicker(new Clicker());
    
            mDialog = new ProgressDialog(this);
            mDialog.setTitle("加载中");
            mDialog.show();
            mViewModel.loadImage();
        }
    
        public class Clicker {
    
            public void onClick(View view) {
                mDialog.show();
                switch (view.getId()) {
                    case R.id.btn_pre:
                        mViewModel.loadPreImage();
                        break;
                    case R.id.btn:
                        mViewModel.loadImage();
                        break;
                    case R.id.btn_next:
                        mViewModel.loadNextImage();
                        break;
                }
            }
        }
    }
    

    实战总结

    这次编写的MVVM模式的Demo,让我对MVVM模式有了一点理解,它的优点和缺点都同样明显。

    优点

    • 数据和布局文件实现双向绑定,我们仅关心数据的改变即可,View层UI的改变会通过数据的改变而改变。省去写findViewByIdsetText()等繁复的代码的工作。
    • View层更新UI的操作完全由自己决定,而无需其他层级的通知,变被动为主动,同时也减轻了其他层级的负担。
    • View层和ViewModel层完全解耦,并且ViewModel层可同时绑定多个View层,去同时更新数据的变化。

    缺点

    • View层对ViewModel层数据变化采用监听的方式,若数据种类过多,则需要写多个监听器观测。
    • 若ViewModel层数据未改变,而需要View层去改变UI时,则非常不方便,需要使用其他方式实现(比如增加额外监听器)。
    • Android Studio对MVVM模式的支持还不是很完善,Binding类的生成可能需要多次Rebuild的操作,若Binding类过多,则编译工作会耗时很大;
    • MVVM模式下,因为View层和ViewModel层完全独立的关系,程序如果报错则很难定位具体问题,排查问题困难。

    总的来说,MVVM模式我认为还是一个非常优秀的模式,它省去了MVP模式中编写巨量接口的麻烦,同时将View层和ViewModel层完全解耦,若可以接受上述总结中的缺点项,我认为项目完全可以采用MVVM模式去进行开发。

    Demo下载

    本Demo地址

    相关文章

      网友评论

          本文标题:我理解的MVVM架构及实战Demo

          本文链接:https://www.haomeiwen.com/subject/prxsyctx.html