概述
说到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>