Awesome Open Source
Awesome Open Source
Sponsorship

Sample 简介

一个简单的结合 Retrofit 和 RxJava 框架实现 MVVM 架构的例子。

最近在研究 Kotlin for Android,做了一个基于 Clean 架构以及 Retrofit , RxKotlin , Dagger 框架实现的 Kotlin for Android App ,更多详情请戳这里

效果预览

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. 各层之间的通信,都是双向的。
  2. View 与 Model 不直接发生联系,都通过 Presenter 进行间接通信。
  3. Model 层与 Presenter 层,Presenter 层与 View 层之间通过接口建立联系。

采用 MVP 设计架构,Activity 与 Fragment 只位于 View 层。

MVP 的缺陷在于:由于我们使用了接口的方式去连接 View 层和 Presenter 层,这样就导致了一个问题,当你的页面逻辑很复杂的时候,你的接口会有很多,如果你的 app 中有很多个这样复杂的页面,维护接口的成本就会变的非常的大。

MVVM

MVVM

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

RxJava

RxJava 在 GitHub 主页上的自我介绍是 "a library for composing asynchronous and event-based programs using observable sequences for the Java VM"(一个在 Java VM 上使用可观测的序列来组成异步的、基于事件的程序的库)。

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

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

Retrofit

retrofit

一个 Android 和 Java 上 HTTP 库(利用注解和 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+).

实践

嫌代码不够高亮?请移步博客http://haohaochang.cn

直接上代码。

依赖的第三方类库

    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

android {
    ......

    dataBinding {
        enabled = true
    }
}

工程目录结构

目录

MVVM 之 View

MainActivity.java

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

MovieFragment.java

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

<?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

<?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

<?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

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**

public interface CompletedListener {
    void onCompleted();
}

MVVM 之 ViewModel

MainViewModel.java

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

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

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

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 类,这里就不贴出来了。


Get A Weekly Email With Trending Projects For These Topics
No Spam. Unsubscribe easily at any time.
java (29,497
rxjava (324
mvvm (292
sample (99
retrofit (87

Find Open Source By Browsing 7,000 Topics Across 59 Categories