haohao

MVVM,RxJava 和 Retrofit 的一次实践

Cover

春节后的第一篇博客

本文已授权微信公众号:鸿洋(hongyangAndroid)原创首发。
原创文章,转载请注明出处:haohaochang.cn


效果预览

result

Demo 下载

准备知识

MVC

mvc

  • 视图层(View):用户界面。
  • 控制器层(Controller):业务逻辑
  • 模型层(Model):数据保存

  1. View 层传送指令到 Controller 层
  2. Controller 层完成业务逻辑后,要求 Model 层改变状态
  3. Model 层将新的数据发送到 View层,使用户得到反馈

缺陷:View 层和 Model 层是相互可知,耦合性大,像 Activity 或者 Fragment 既在 Controller 层,又在 View 层,造成工程的可扩展性可维护性非常差。

MVP

mvp

在 MVP 架构模式中,Controller 层变成了 Presenter 层。

  1. MVP 模式各层之间的通信,都是双向的。
  2. View 层与 Model 层不直接发生联系,都通过 Presenter 层进行间接通信。
  3. Model 层与 Presenter 层,Presenter 层与 View 层之间通过接口建立联系。

采用 MVP 架构,Activity,Fragment 以及自定义 View 只位于 View 层。
View 层:负责数据展示和用户交互;
Presenter 层:充当中间人的角色,用来隔离 Model 层和 View 层,负责将 View 层的请求分发给 Model 处理,将 Model 层的反馈传递给 View 层;
Model 层:封装数据来源,如网络数据,本地数据库数据。

MVP 的优势:

  1. 易于维护,测试,功能扩展;
  2. 易于分工协作编程;

MVP 的缺陷:

  1. 由于我们使用了接口的方式去连接 View 层和 Presenter 层,这样就导致了特定场景下的一些问题,当你的页面逻辑很复杂的时候,你的 View 层实现的接口会有很多,如果你的 App 中有很多个这样复杂的页面,维护接口的成本就会变的非常的大。
  2. 增加代码类的数量,可读性差。

MVVM

MVVM

MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。
区别在于: View 层与 ViewModel 层通过 DataBinding 相互绑定,View 层的变动,自动反映在 ViewModel 层,反之亦然。

Clean

本篇只着重讲基于 RxJava 和 Retrofit 框架实现 MVVM 架构设计,想要了解 Clean 架构的同学,移步Android Clean 架构浅析

另外这里有一个基于 Kotlin 实现的 Android 架构的例子 – 一个基于 Clean 架构以及 Retrofit , RxKotlin , Dagger 框架实现的 Kotlin for Android App

RxJava

RxJava

Rx 是微软 .Net 的一个响应式扩展,Rx 借助可观测的序列提供一种简单的方式来创建异步的,基于事件驱动的程序。2012 年 Netflix 为了应对不断增长的业务需求开始将 .NET Rx 迁移到 JVM 上面。并于 13 年二月份正式向外展示了 RxJava 。

从语义的角度来看, RxJava 就是 .NET Rx 。从语法的角度来看, Netflix 考虑到了对应每个 Rx 方法,保留了 Java 代码规范和基本的模式。

RxJava 在 GitHub 主页的介绍是:

RxJava is a Java VM implementation of Reactive Extensions: a library for composing asynchronous and event-based programs by using observable sequences.

一个在 Java VM 上使用可观测的序列来组成异步的、基于事件程序的库。

RxJava 本质上是一个异步操作库,是一个能让你用极其简洁的逻辑去处理繁琐复杂任务的异步事件库。

简而言之,RxJava 可以用几个关键字概括:简洁,队列化,异步

Retrofit

retrofit

一个 Android 和 Java 上 HTTP 库(利用注解和 OKHttp 来实现和服务器的数据交互)。

Retrofit 基于 OKHttp 并引入注解,使用简单,易扩展,易维护。

Retrofit 官方文档:http://square.github.io/retrofit/

DataBinding

data-binding

在Google IO 2015 中,Google 在 support-v7 中新增了 Data Binding,使用 Data Binding可以直接在布局的 xml 中绑定布局与数据,从而简化代码,Android Data Binding是Android 的 MVVM 框架。因为 Data Binding 是包含在 support-v7 包里面的,所以可以向下兼容到最低 Android 2.1 (API level 7+).

Data Binding 是实现 xml 文件与 java 代码交互的工具类库,通过观察者模式 Observable 变量集实现 ViewModel 层和 View 层的相互监听,自动交互。

实践

直接上代码。

依赖的第三方类库

1
2
3
4
5
6
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'

API

https://api.douban.com/v2/movie/top250?start=0&count=20

引入DataBinding

1
2
3
4
5
6
7
android {
......
dataBinding {
enabled = true
}
}

工程目录结构

目录

详细源码:https://github.com/githubhaohao/MVVMRxJavaRetrofitSample

MVVM 之 View

MainActivity.java

1
getFragmentManager().beginTransaction().add(R.id.movie_fragment, MovieFragment.getInstance()).commit();

MovieFragment.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class MovieFragment extends Fragment implements CompletedListener,SwipeRefreshLayout.OnRefreshListener{
private static String TAG = MovieFragment.class.getSimpleName();
private MainViewModel viewModel;
private MovieFragmentBinding movieFragmentBinding;
private MovieAdapter movieAdapter;
public static MovieFragment getInstance() {
return new MovieFragment();
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View contentView = inflater.inflate(R.layout.movie_fragment, container, false);
movieFragmentBinding = MovieFragmentBinding.bind(contentView);
initData();
return contentView;
}
private void initData() {
movieAdapter = new MovieAdapter();
movieFragmentBinding.recyclerView.setLayoutManager(new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false));
movieFragmentBinding.recyclerView.setItemAnimator(new DefaultItemAnimator());
movieFragmentBinding.recyclerView.setAdapter(movieAdapter);
movieFragmentBinding.swipeRefreshLayout.setColorSchemeResources(R.color.colorAccent, R.color.colorPrimary, R.color.colorPrimaryDark);
movieFragmentBinding.swipeRefreshLayout.setOnRefreshListener(this);
viewModel = new MainViewModel(movieAdapter,this);
movieFragmentBinding.setViewModel(viewModel);
}
@Override
public void onRefresh() {
movieAdapter.clearItems();
viewModel.refreshData();
}
@Override
public void onCompleted() {
if (movieFragmentBinding.swipeRefreshLayout.isRefreshing()) {
movieFragmentBinding.swipeRefreshLayout.setRefreshing(false);
}
}
}

activity_main.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".view.MainActivity">
<!-- ... -->
<FrameLayout
android:layout_marginTop="?attr/actionBarSize"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/movie_fragment"/>
<!-- ... -->
</android.support.design.widget.CoordinatorLayout>

movie_fragment.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="viewModel"
type="com.jc.mvvmrxjavaretrofitsample.viewModel.MainViewModel"/>
</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="@{viewModel.exception}"/>
</LinearLayout>
</RelativeLayout>
</layout>

movie_item.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
<?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.jc.mvvmrxjavaretrofitsample.viewModel.MovieViewModel"/>
</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>

MovieAdapter.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class MovieAdapter extends RecyclerView.Adapter<MovieAdapter.BindingHolder> {
private List<Movie> movies;
public MovieAdapter() {
movies = new ArrayList<>();
}
@Override
public BindingHolder onCreateViewHolder(ViewGroup parent, int viewType) {
MovieItemBinding itemBinding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.movie_item, parent, false);
return new BindingHolder(itemBinding);
}
@Override
public void onBindViewHolder(BindingHolder holder, int position) {
MovieViewModel movieViewModel = new MovieViewModel(movies.get(position));
holder.itemBinding.setViewModel(movieViewModel);
}
@Override
public int getItemCount() {
return movies.size();
}
public void addItem(Movie movie) {
movies.add(movie);
notifyItemInserted(movies.size() - 1);
}
public void clearItems() {
movies.clear();
notifyDataSetChanged();
}
public static class BindingHolder extends RecyclerView.ViewHolder {
private MovieItemBinding itemBinding;
public BindingHolder(MovieItemBinding itemBinding) {
super(itemBinding.cardView);
this.itemBinding = itemBinding;
}
}
}

回调接口 CompletedListener.java

1
2
3
public interface CompletedListener {
void onCompleted();
}

MVVM 之 ViewModel

MainViewModel.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public class MainViewModel {
public ObservableField<Integer> contentViewVisibility;
public ObservableField<Integer> progressBarVisibility;
public ObservableField<Integer> errorInfoLayoutVisibility;
public ObservableField<String> exception;
private Subscriber<Movie> subscriber;
private MovieAdapter movieAdapter;
private CompletedListener completedListener;
public MainViewModel(MovieAdapter movieAdapter,CompletedListener completedListener) {
this.movieAdapter = movieAdapter;
this.completedListener = completedListener;
initData();
getMovies();
}
private void getMovies() {
subscriber = new Subscriber<Movie>() {
@Override
public void onCompleted() {
Log.d("[MainViewModel]", "onCompleted");
hideAll();
contentViewVisibility.set(View.VISIBLE);
completedListener.onCompleted();
}
@Override
public void onError(Throwable e) {
hideAll();
errorInfoLayoutVisibility.set(View.VISIBLE);
exception.set(e.getMessage());
}
@Override
public void onNext(Movie movie) {
movieAdapter.addItem(movie);
}
};
RetrofitHelper.getInstance().getMovies(subscriber, 0, 20);
}
public void refreshData() {
getMovies();
}
private void initData() {
contentViewVisibility = new ObservableField<>();
progressBarVisibility = new ObservableField<>();
errorInfoLayoutVisibility = new ObservableField<>();
exception = new ObservableField<>();
contentViewVisibility.set(View.GONE);
errorInfoLayoutVisibility.set(View.GONE);
progressBarVisibility.set(View.VISIBLE);
}
private void hideAll(){
contentViewVisibility.set(View.GONE);
errorInfoLayoutVisibility.set(View.GONE);
progressBarVisibility.set(View.GONE);
}
}

MovieViewModel.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class MovieViewModel extends BaseObservable {
private Movie movie;
public MovieViewModel(Movie movie) {
this.movie = movie;
}
public String getCoverUrl() {
return movie.getImages().getSmall();
}
public String getTitle() {
return movie.getTitle();
}
public float getRating() {
return movie.getRating().getAverage();
}
public String getRatingText(){
return String.valueOf(movie.getRating().getAverage());
}
public String getYear() {
return movie.getYear();
}
public String getMovieType() {
StringBuilder builder = new StringBuilder();
for (String s : movie.getGenres()) {
builder.append(s + " ");
}
return builder.toString();
}
public String getImageUrl() {
return movie.getImages().getSmall();
}
@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);
}
}

MVVM 之 Model

DouBanMovieService.java

1
2
3
4
5
6
public interface DouBanMovieService {
String BASE_URL = "https://api.douban.com/v2/movie/";
@GET("top250")
Observable<Response<List<Movie>>> getMovies(@Query("start") int start, @Query("count") int count);
}

RetrofitHelper.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class RetrofitHelper {
private static final int DEFAULT_TIMEOUT = 10;
private Retrofit retrofit;
private DouBanMovieService movieService;
OkHttpClient.Builder builder;
/**
* 获取RetrofitHelper对象的单例
* */
private static class Singleton {
private static final RetrofitHelper INSTANCE = new RetrofitHelper();
}
public static RetrofitHelper getInstance() {
return Singleton.INSTANCE;
}
public 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(DouBanMovieService.BASE_URL)
.build();
movieService = retrofit.create(DouBanMovieService.class);
}
public void getMovies(Subscriber<Movie> subscriber, int start, int count) {
movieService.getMovies(start, count)
.map(new Func1<Response<List<Movie>>, List<Movie>>() {
@Override
public List<Movie> call(Response<List<Movie>> listResponse) {
return listResponse.getSubjects();
}
})
.flatMap(new Func1<List<Movie>, Observable<Movie>>() {
@Override
public Observable<Movie> call(List<Movie> movies) {
return Observable.from(movies);
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(subscriber);
}
}

还有 entity 类,这里就不贴出来了。

详细源码:https://github.com/githubhaohao/MVVMRxJavaRetrofitSample



联系我

Wechat ID

公众号

生活不止于眼前的苟且, 还有诗和远方的田野