MVVM简单例子

概述
说到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. 针对适配器编写的ViewModel

public 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.下载地址
点击去下载

猜你喜欢

转载自blog.csdn.net/rjgcszlc/article/details/78314322