MVVM简单例子

来源:互联网 发布:鉴知往来的意思 编辑:程序博客网 时间:2024/06/05 04:02

概述
说到Android MVVM,相信大家都会想到Google 2015年推出的DataBinding框架。然而两者的概念是不一样的,不能混为一谈。MVVM是一种架构模式,而DataBinding是一个实现数据和UI绑定的框架,是构建MVVM模式的一个工具。

之前看过很多关于Android MVVM的博客,但大多数提到的都是DataBinding的基本用法,很少有文章仔细讲解在Android中是如何通过DataBinding去构建MVVM的应用框架的。View、ViewModel、Model每一层的职责如何?它们之间联系怎样、分工如何、代码应该如何设计?这是我写这篇文章的初衷。

接下来,我们先来看看什么是MVVM,然后再一步一步来设计整个MVVM框架。

MVC、MVP、MVVM
首先,我们先大致了解下Android开发中常见的模式。

MVC

  • View:XML布局文件。
  • Model:实体模型(数据的获取、存储、数据状态变化)。
  • Controller:对应于Activity,处理数据、业务和UI。

从上面这个结构来看,Android本身的设计还是符合MVC架构的,但是Android中纯粹作为View的XML视图功能太弱,我们大量处理View的逻辑只能写在Activity中,这样Activity就充当了View和Controller两个角色,直接导致Activity中的代码大爆炸。相信大多数Android开发者都遇到过一个Acitivty数以千行的代码情况吧!所以,更贴切的说法是,这个MVC结构最终其实只是一个Model-View(Activity:View&Controller)的结构。

MVP

  • View: 对应于Activity和XML,负责View的绘制以及与用户的交互。
  • Model: 依然是实体模型。
  • Presenter:负责完成View与Model间的交互和业务逻辑。

前面我们说,Activity充当了View和Controller两个角色,MVP就能很好地解决这个问题,其核心理念是通过一个抽象的View接口(不是真正的View层)将Presenter与真正的View层进行解耦。Persenter持有该View接口,对该接口进行操作,而不是直接操作View层。这样就可以把视图操作和业务逻辑解耦,从而让Activity成为真正的View层。

但MVP也存在一些弊端:

  • Presenter(以下简称P)层与View(以下简称V)层是通过接口进行交互的,接口粒度不好控制。粒度太小,就会存在大量接口的情况,使代码太过碎版化;粒度太大,解耦效果不好。同时对于UI的输入和数据的变化,需要手动调用V层或者P层相关的接口,相对来说缺乏自动性、监听性。如果数据的变化能自动响应到UI、UI的输入能自动更新到数据,那该多好!
  • MVP是以UI为驱动的模型,更新UI都需要保证能获取到控件的引用,同时更新UI的时候要考虑当前是否是UI线程,也要考虑Activity的生命周期(是否已经销毁等)。
  • MVP是以UI和事件为驱动的传统模型,数据都是被动地通过UI控件做展示,但是由于数据的时变性,我们更希望数据能转被动为主动,希望数据能更有活性,由数据来驱动UI。
  • V层与P层还是有一定的耦合度。一旦V层某个UI元素更改,那么对应的接口就必须得改,数据如何映射到UI上、事件监听接口这些都需要转变,牵一发而动全身。如果这一层也能解耦就更好了。
  • 复杂的业务同时也可能会导致P层太大,代码臃肿的问题依然不能解决。

MVVM

  • View: 对应于Activity和XML,负责View的绘制以及与用户交互。
  • Model: 实体模型。
  • ViewModel:
    负责完成View与Model间的交互,负责业务逻辑。

MVVM的目标和思想与MVP类似,利用数据绑定(Data Binding)、依赖属性(Dependency Property)、命令(Command)、路由事件(Routed Event)等新特性,打造了一个更加灵活高效的架构。

1. 导包

 compile 'com.android.support:appcompat-v7:25.0.1' compile 'com.android.support:design:25.0.1' compile 'com.android.support:cardview-v7:25.0.1' compile 'io.reactivex:rxjava:1.1.0' compile 'io.reactivex:rxandroid:1.1.0' compile 'com.squareup.retrofit2:retrofit:2.0.0-beta4' compile 'com.squareup.retrofit2:converter-gson:2.0.0-beta4' compile 'com.squareup.retrofit2:adapter-rxjava:2.0.0-beta4' compile 'com.github.bumptech.glide:glide:3.7.0'

2. Model层

1.网络帮助类-----    public class RetrofitHelper {    private static final int DEFAULT_TIMEOUT = 10;    private Retrofit retrofit;    private HttpMovieService movieService;    OkHttpClient.Builder builder;    /**     * 获取RetrofitHelper对象的单例     * */    private static class Singleton {        private static final RetrofitHelper INSTANCE = new RetrofitHelper();    }    public static RetrofitHelper getInstance() {        return Singleton.INSTANCE;    }    private RetrofitHelper() {        builder = new OkHttpClient.Builder();        builder.connectTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS);        retrofit = new Retrofit.Builder()                .client(builder.build())                .addConverterFactory(GsonConverterFactory.create())                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())                .baseUrl(HttpMovieService.BASE_URL)                .build();        movieService = retrofit.create(HttpMovieService.class);    }    public void getMovies(Subscriber<Response> subscriber, int start, int count) {        movieService.getMovies(start, count)                .subscribeOn(Schedulers.io())                .observeOn(AndroidSchedulers.mainThread())                .subscribe(subscriber);    }}//网络接口public interface HttpMovieService {    String BASE_URL = "https://api.douban.com/v2/movie/";    @GET("top250")    Observable<Response<Movie>> getMovies(@Query("start") int start, @Query("count") int count);}2.实体对象------    public class Movie {    public String id;    public String alt;    public String year;    public String title;    public String original_title;    public List<String> genres;    public List<Cast> casts;    public List<Cast> directors;    public Avatars images;    public Rating rating;    public static class Rating {        public float average;    }    public static class Cast{        public String id;        public String name;        public String alt;        public Avatars avatars;    }    public static class Avatars{        public String small;        public String medium;        public String large;    }}/** * Created by zlc on 2017/10/22. */public class BaseInfo<T> {    public List<T> subjects;    public int count;    public int start;    public int total;}

3. View层

1.fragment设计public class MovieFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener,CompletedListener {    private MovieFragmentBinding movieFragmentBinding;    private MovieAdapter movieAdapter;    private MainViewModel mainViewModel;    @Nullable    @Override    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {        movieFragmentBinding = DataBindingUtil.inflate(inflater, R.layout.movie_fragment, container, false);        return movieFragmentBinding.getRoot();    }    @Override    public void onActivityCreated(@Nullable Bundle savedInstanceState) {        super.onActivityCreated(savedInstanceState);        initData();    }    private void initData() {        movieAdapter = new MovieAdapter(getActivity());        movieFragmentBinding.recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));        movieFragmentBinding.recyclerView.setItemAnimator(new DefaultItemAnimator());        movieFragmentBinding.recyclerView.setAdapter(movieAdapter);        mainViewModel = new MainViewModel(movieAdapter,this);        movieFragmentBinding.setViewModel(mainViewModel);        movieFragmentBinding.swipeRefreshLayout.setOnRefreshListener(this);    }    public static Fragment getInstance() {        return new MovieFragment();    }    @Override    public void onRefresh() {        mainViewModel.refreshData();    }    @Override    public void onCompleted() {        if(movieFragmentBinding.swipeRefreshLayout.isRefreshing()){            movieFragmentBinding.swipeRefreshLayout.setRefreshing(false);        }    }}2. RecycleView适配器编写/** * Created by Administrator on 2017/10/22. */public class MovieAdapter extends RecyclerView.Adapter<MovieAdapter.MovieViewHolder>{    private List<Movie> mDatas;    private Context mContext;    public MovieAdapter(Context context){        mDatas = new ArrayList<>();        this.mContext = context;    }    @Override    public MovieAdapter.MovieViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {        MovieItemBinding binding = DataBindingUtil.inflate(LayoutInflater.from(mContext), R.layout.movie_item, parent, false);        MovieViewHolder viewHolder = new MovieViewHolder(binding.getRoot());        viewHolder.setBinding(binding);        return viewHolder;    }    @Override    public void onBindViewHolder(MovieAdapter.MovieViewHolder holder, int position) {        Movie movie = mDatas.get(position);        MovieViewModel movieViewModel = new MovieViewModel(movie);        holder.getBinding().setViewModel(movieViewModel);    }    @Override    public int getItemCount() {        return mDatas.size();    }    public void setMovies(List<Movie> movies) {        mDatas = movies;        notifyDataSetChanged();    }    public void clearAll() {        mDatas.clear();    }    public static class MovieViewHolder extends RecyclerView.ViewHolder{        private MovieItemBinding binding;        public MovieViewHolder(View itemView) {            super(itemView);        }        public void setBinding(MovieItemBinding binding) {            this.binding = binding;        }        public MovieItemBinding getBinding() {            return binding;        }    }}

4. ViewModel

1. 针对fragment编写的ViewModel/** * Created by zlc on 2017/10/22. */public class MovieFragmentViewModel {    public ObservableField<Integer> contentViewVisibility;    public ObservableField<Integer> progressBarVisibility;    public ObservableField<Integer> errorInfoLayoutVisibility;    private MovieAdapter movieAdapter;    private CompletedListener completedListener;    public MovieFragmentViewModel(MovieAdapter movieAdapter, CompletedListener completedListener) {        this.movieAdapter = movieAdapter;        this.completedListener = completedListener;        initData();        getMovieInfo();    }    private void getMovieInfo() {        RetrofitHelper.getInstance().getMovies(new Subscriber<BaseInfo>() {            @Override            public void onCompleted() {                Log.e("MovieFragmentViewModel", "onCompleted");                hideAll();                contentViewVisibility.set(View.VISIBLE);                completedListener.onCompleted();            }            @Override            public void onError(Throwable e) {                Log.e("MovieFragmentViewModel", "onError="+e.getMessage());                hideAll();                errorInfoLayoutVisibility.set(View.VISIBLE);                completedListener.onCompleted();            }            @Override            public void onNext(BaseInfo response) {                if(response!=null)                    movieAdapter.setMovies(response.subjects);            }        },0,20);    }    private void initData() {        contentViewVisibility = new ObservableField<>();        progressBarVisibility = new ObservableField<>();        errorInfoLayoutVisibility = new ObservableField<>();        show(View.GONE,contentViewVisibility,errorInfoLayoutVisibility);        progressBarVisibility.set(View.VISIBLE);    }    public void refreshData() {        movieAdapter.clearAll();        getMovieInfo();    }    private void hideAll(){        show(View.GONE,contentViewVisibility,errorInfoLayoutVisibility,progressBarVisibility);    }    private void show(int visable,ObservableField ...fields){        for (int i = 0; i < fields.length; i++) {            fields[i].set(visable);        }    }}2. 针对适配器编写的ViewModelpublic class MovieAdapterViewModel extends BaseObservable{    private Movie movie;    public MovieAdapterViewModel(Movie movie) {        this.movie = movie;    }    public String getTitle() {        return movie.title;    }    public float getRating() {        return movie.rating.average;    }    public String getRatingText() {        return String.valueOf(movie.rating.average);    }    public String getMovieType() {        StringBuilder builder = new StringBuilder();        for (String s : movie.genres) {            builder.append(s + " ");        }        return builder.toString();    }    public String getYear(){        return movie.year;    }    public String getImageUrl() {        return movie.images.small;    }    @BindingAdapter({"app:imageUrl"})    public static void loadImage(ImageView imageView,String url) {        Glide.with(imageView.getContext())                .load(url)                .placeholder(R.drawable.cover)                .error(R.drawable.cover)                .into(imageView);    }}

5. 布局

1. fragment布局<layout xmlns:android="http://schemas.android.com/apk/res/android">    <data>        <variable            name="viewModel"            type="com.zlc.mvvmsample.viewModel.MovieFragmentViewModel"/>    </data>    <RelativeLayout        android:layout_width="match_parent"        android:layout_height="match_parent">        <android.support.v4.widget.SwipeRefreshLayout            android:visibility="@{viewModel.contentViewVisibility}"            android:id="@+id/swipe_refresh_layout"            android:layout_width="match_parent"            android:layout_height="match_parent">            <android.support.v7.widget.RecyclerView                android:id="@+id/recycler_view"                android:background="#ddd"                android:layout_width="match_parent"                android:layout_height="match_parent"                android:padding="8dp">            </android.support.v7.widget.RecyclerView>        </android.support.v4.widget.SwipeRefreshLayout>        <ProgressBar            style="?android:attr/progressBarStyleLarge"            android:id="@+id/progress_bar"            android:visibility="@{viewModel.progressBarVisibility}"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:layout_centerInParent="true"/>        <LinearLayout            android:layout_width="match_parent"            android:id="@+id/error_info_layout"            android:visibility="@{viewModel.errorInfoLayoutVisibility}"            android:orientation="vertical"            android:layout_height="match_parent">            <TextView                android:layout_gravity="center"                android:layout_width="wrap_content"                android:layout_height="wrap_content"                android:text=""/>        </LinearLayout>    </RelativeLayout></layout>2. RecycleView子条目布局<?xml version="1.0" encoding="utf-8"?><layout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:app="http://schemas.android.com/tools">    <data>        <variable            name="viewModel"            type="com.zlc.mvvmsample.viewModel.MovieAdapterViewModel"/>    </data>    <android.support.v7.widget.CardView        xmlns:card_view="http://schemas.android.com/apk/res-auto"        android:id="@+id/card_view"        android:layout_width="match_parent"        android:layout_height="wrap_content"        card_view:cardCornerRadius="4dp"        card_view:cardBackgroundColor="@color/background"        card_view:cardUseCompatPadding="true">        <LinearLayout            android:layout_width="match_parent"            android:layout_height="wrap_content"            android:orientation="horizontal">            <ImageView                android:layout_margin="8dp"                android:layout_width="60dp"                android:layout_height="100dp"                android:src="@drawable/cover"                app:imageUrl="@{viewModel.imageUrl}"                android:id="@+id/cover"/>            <LinearLayout                android:layout_width="wrap_content"                android:layout_height="match_parent"                android:layout_margin="8dp"                android:orientation="vertical">                <TextView                    android:textColor="@android:color/black"                    android:layout_width="wrap_content"                    android:layout_height="wrap_content"                    android:text="@{viewModel.title}"                    android:textSize="12sp"/>                <LinearLayout                    android:layout_width="wrap_content"                    android:layout_height="wrap_content"                    android:layout_marginTop="4dp"                    android:orientation="horizontal">                    <android.support.v7.widget.AppCompatRatingBar                        android:id="@+id/ratingBar"                        style="?android:attr/ratingBarStyleSmall"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:layout_gravity="center_vertical"                        android:isIndicator="true"                        android:max="10"                        android:numStars="5"                        android:rating="@{viewModel.rating}" />                    <TextView                        android:id="@+id/rating_text"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:layout_gravity="center_vertical"                        android:layout_marginLeft="6dp"                        android:text="@{viewModel.ratingText}"                        android:textColor="?android:attr/textColorSecondary"                        android:textSize="10sp" />                </LinearLayout>                <TextView                    android:layout_width="wrap_content"                    android:layout_height="wrap_content"                    android:textColor="?android:attr/textColorSecondary"                    android:textSize="10sp"                    android:text="@{viewModel.movieType}"                    android:id="@+id/movie_type_text"                    android:layout_marginTop="6dp"                    />                <TextView                    android:layout_width="wrap_content"                    android:layout_height="wrap_content"                    android:textColor="?android:attr/textColorSecondary"                    android:textSize="10sp"                    android:text="@{viewModel.year}"                    android:id="@+id/year_text"                    android:layout_marginTop="6dp"                    />            </LinearLayout>        </LinearLayout>    </android.support.v7.widget.CardView></layout>

6. 联系方式

qq:1509815887@qq.com email : zlc921022@163.com phone : 18684732678

7.下载地址
点击去下载