rxjava+retrofit

Retrofit結合RxJava使用指南

Retrofit是一個當前很流行的網絡請求庫, 官網的介紹是: "Type-safe HTTP client for Android and Java". 本文介紹Retrofit的使用.
先介紹單獨使用Retrofit進行網絡請求, 後面主要介紹和RxJava結合的請求, 有實例代碼.

http://www.cnblogs.com/mengdd/p/6047948.html

Retrofit單獨使用

Setup

首先在manifest中加上網絡權限:

<uses-permission android:name="android.permission.INTERNET" />

然後在app/build.gradle中加上依賴:

compile 'com.squareup.retrofit2:retrofit:2.1.0'
compile 'com.google.code.gson:gson:2.8.0'
compile 'com.squareup.retrofit2:converter-gson:2.1.0'

準備API和model類

本例子中使用Github API做請求.

以Github的Root Endpoint爲例:
https://api.github.com.
首先, 我們在命令行發送:

curl https://api.github.com

或者在Postman發送這個請求, 兩種方法都可以得到結果.

這個請求返回的是一個json.

利用這個網站: jsonschema2pojo, 可以用json生成一個java類, 比如上面這個, 我們給它起名字叫Endpoints.java.

之後例子中的API都是這種方式, 先發送請求得到json, 然後轉成java的model類.

利用Retrofit發送請求並得到結果

首先寫一個ServiceGenerator類, 用於生成service:

public class ServiceGenerator {
    public static final String API_BASE_URL = "https://api.github.com";

    private static OkHttpClient.Builder httpClient = new OkHttpClient.Builder();

    private static Retrofit.Builder builder =
            new Retrofit.Builder()
                    .baseUrl(API_BASE_URL)
                    .addConverterFactory(GsonConverterFactory.create());

    public static <S> S createService(Class<S> serviceClass) {
        Retrofit retrofit = builder.client(httpClient.build()).build();
        return retrofit.create(serviceClass);
    }
}

這裏指定了我們的base url.
createService()方法返回的是一個泛型.

然後我們創建GithubService, 注意這是一個接口:

import com.ddmeng.helloretrofit.data.models.Endpoints;

import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Url;

public interface GitHubService {
    @GET
    Call<Endpoints> getAllEndpoints(@Url String url);
}

這裏@GET指定了是一個GET請求, 因爲我們請求的就是base url, 所以是這樣寫的.
Endpoints類是這個請求所返回的json轉化的java類.

好了, 準備工作做完了, 現在就可以請求並得到結果:
請求github api的root url, 得到所有的endpoints:

GitHubService gitHubService = ServiceGenerator.createService(GitHubService.class);
Call<Endpoints> endpointsCall = gitHubService.getAllEndpoints("");
endpointsCall.enqueue(new Callback<Endpoints>() {
    @Override
    public void onResponse(Call<Endpoints> call, Response<Endpoints> response) {
        Endpoints endpoints = response.body();
        String repositoryUrl = endpoints.getRepositoryUrl();
        LogUtils.i(repositoryUrl);
        Toast.makeText(MainActivity.this, "repository url: " + repositoryUrl, Toast.LENGTH_LONG).show();
    }

    @Override
    public void onFailure(Call<Endpoints> call, Throwable t) {

    }
});

說明:
首先利用前面的ServiceGenerator來創建Service, 然後調用接口中定義的getAllEndpoints()方法, 此處傳入了空字符串, 因爲我請求的就是base url.

同步和異步

這裏注意用Retrofit請求的返回值是Call<T> (後面我們還會介紹用RxJava的情形), 泛型T是model類型, 它有兩個方法:

  • execute()是同步方法, 返回Response<T>;
  • enqueue()是異步方法, 在上面的例子中用的就是這個, 在回調onResponse()中返回了Response<T>.

Converter

Converter的作用: 如果不指定Converter, 默認情況下Retrofit只能返回ResponseBody類型, 加了Converter之後就可以返回我們定義的Model類型了.
所以Converter替我們做了json -> model的工作.

本例子中ConverterFactory指定的是GsonConverterFactory. 這裏我們選的是Gson Converter, 所以依賴的是com.squareup.retrofit2:converter-gson.

Retrofit支持多種converters:

  • Gson: com.squareup.retrofit2:converter-gson
  • Jackson: com.squareup.retrofit2:converter-jackson
  • Moshi: com.squareup.retrofit2:converter-moshi
  • Protobuf: com.squareup.retrofit2:converter-protobuf
  • Wire: com.squareup.retrofit2:converter-wire
  • Simple XML: com.squareup.retrofit2:converter-simplexml
  • Scalars (primitives, boxed, and String): com.squareup.retrofit2:converter-scalars

Path和參數

從上面返回的endpoints可以看到, user_url是: https://api.github.com/users/{user}
這是一個帶path參數的url, 我們發請求的時候在{user}處寫一個github用戶名, 即可得到該用戶的信息, 比如:
https://api.github.com/users/mengdd.

那麼用Retrofit如何處理呢?
只需要在GithubService中增加一個方法, 這樣寫:

public interface GitHubService {
    @GET
    Call<Endpoints> getAllEndpoints(@Url String url);

    @GET("users/{user}")
    Call<User> getUser(@Path("user") String user);
}

使用時的方法完全一樣, 不再贅述, 同理, 如果要在後面加參數, 可以用@Query.
更多註解的例子見官方網站: Retrofit

Retrofit + RxJava

RxJava近年來很流行, 主要優勢是流式操作, 可以處理並行發送請求, 使用靈活, 線程切換容易.
當你要處理的邏輯比較複雜時, 就會發現使用RxJava的優勢.

以我們的例子來說, 當前我們利用一個請求可以得到一個用戶的信息並顯示出來.
如果我們想得到這個用戶的所有repo的所有者或者其他信息, 所有他follow的人的信息, 以及他們的repo的信息呢?

這就需要發很多個請求, 並且其中有些請求是並行發送的, 如果按照前面的方法, 不斷地在callback裏面嵌套, 那就太難看了.

Setup with RxJava

添加RxJava依賴

首先, 添加RxJava和RxAndroid的依賴:

compile 'io.reactivex:rxjava:1.2.2'
compile 'io.reactivex:rxandroid:1.2.1'

注: 雖然在我寫這篇文章的時候(2016.11.4)RxJava2.0剛剛release, 但是我們還是先用RxJava1來寫這個demo.

然後添加retrofit的adapter-rxjava:

compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0'

所以現在我們的依賴總的看起來是這樣:

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile "com.android.support:appcompat-v7:${supportLibVersion}"
    compile "com.android.support:design:${supportLibVersion}"
    compile "com.jakewharton:butterknife:${butterKnifeVersion}"
    apt "com.jakewharton:butterknife-compiler:${butterKnifeVersion}"
    compile 'com.squareup.retrofit2:retrofit:2.1.0'
    compile 'com.google.code.gson:gson:2.8.0'
    compile 'com.squareup.retrofit2:converter-gson:2.1.0'
    compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0'
    compile 'io.reactivex:rxjava:1.2.2'
    compile 'io.reactivex:rxandroid:1.2.1'
    testCompile 'junit:junit:4.12'
}

Retrofit結合RxJava

Retrofit.Builder()中加入這一行:
.addCallAdapterFactory(RxJavaCallAdapterFactory.create());

ServiceGenerator變成這樣:

public class ServiceGenerator {
    public static final String API_BASE_URL = "https://api.github.com";

    private static OkHttpClient.Builder httpClient = new OkHttpClient.Builder();

    private static Retrofit.Builder builder =
            new Retrofit.Builder()
                    .baseUrl(API_BASE_URL)
                    .addConverterFactory(GsonConverterFactory.create())
                    .addCallAdapterFactory(RxJavaCallAdapterFactory.create());

    public static <S> S createService(Class<S> serviceClass) {
        Retrofit retrofit = builder.client(httpClient.build()).build();
        return retrofit.create(serviceClass);
    }
}

這樣我們在GithubService中定義的接口方法, 既可以像原來一樣返回Call, 也可以返回Observable.

Retrofit + RxJava請求實例

以單個請求爲例,
不用RxJava的時候:

@GET("users/{user}/following")
Call<List<User>> getUserFollowing(@Path("user") String user);

請求的時候是這樣的:
請求指定用戶follow的所有人:

GitHubService service = ServiceGenerator.createService(GitHubService.class);
Call<List<User>> userFollowing = service.getUserFollowing(inputUserNameView.getText().toString());
userFollowing.enqueue(new Callback<List<User>>() {
    @Override
    public void onResponse(Call<List<User>> call, Response<List<User>> response) {
        List<User> followingUsers = response.body();
        peopleListAdapter.setUsers(followingUsers);
        peopleListAdapter.notifyDataSetChanged();
    }

    @Override
    public void onFailure(Call<List<User>> call, Throwable t) {

    }
});

現在改用RxJava了, 返回的不是Call而是Observable:

@GET("users/{user}/following")
Observable<List<User>> getUserFollowingObservable(@Path("user") String user);

結合RxJava請求的時候變爲這樣:
還是請求用戶follow的所有人:

GitHubService service = ServiceGenerator.createService(GitHubService.class);
String username = inputUserNameView.getText().toString();
service.getUserFollowingObservable(username)
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(new Subscriber<List<User>>() {
            @Override
            public void onCompleted() {

            }

            @Override
            public void onError(Throwable e) {

            }

            @Override
            public void onNext(List<User> users) {
                LogUtils.i("onNext: " + users.size());
                peopleListAdapter.setUsers(users);
                peopleListAdapter.notifyDataSetChanged();
            }
        });

用RxJava實現後, 請求返回的是一個Observable, 用subscribe()添加一個訂閱者, 即它的觀察者.
當請求返回後, 回到主線程, 更新UI.
這是單個請求的例子, 所以RxJava的優勢不是很明顯, 如果我們有多個請求, 用RxJava進行變換組合顯然就是更好的選擇.

用RxJava進行線程切換

上個例子中.subscribeOn(Schedulers.io())指定Observable的工作, 在我們的例子中Observable的工作即發送請求, 在io線程做, 指定了被觀察者的處理線程;
.observeOn(AndroidSchedulers.mainThread())指定最後onNext()回調在主線程, 即指定了通知後續觀察者的線程.
關於這兩個操作符的更多說明請看官方文檔: subscribeOnobserveOn.

RxJava處理多個請求的例子

設計這樣一個場景, 我們現在取到了一個用戶follow的所有人, 但是取回的信息中並不包含每個人擁有的repo個數, 只有一個url可用戶查看所有repo.

接下來我們要取其中每一個人的詳細信息, 就要查詢另一個API, 重新查詢這個人的完整信息.
查詢用戶follow的所有人, 然後查詢每一個人的詳細信息:

subscription = service.getUserFollowingObservable(username)
        .flatMap(new Func1<List<User>, Observable<User>>() {
            @Override
            public Observable<User> call(List<User> users) {
                return Observable.from(users);
            }
        })
        .flatMap(new Func1<User, Observable<User>>() {
            @Override
            public Observable<User> call(User user) {
                return service.getUserObservable(user.getLogin());
            }
        })
        .toList()
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(new Subscriber<List<User>>() {
            @Override
            public void onCompleted() {

            }

            @Override
            public void onError(Throwable e) {

            }

            @Override
            public void onNext(List<User> users) {
                peopleListAdapter.setUsers(users);
                peopleListAdapter.notifyDataSetChanged();
            }
        });

可以看到我們加了兩個flatMap()和一個toList()來做這個事情.

首先, 第一步我們用getUserFollowingObservable()得到的是一個Observable<List<User>>;
我們之後用.flatMap(), 它的輸入是List<User>, 返回的是Observable<User>. 我們在其中用了一個.from()來生成一個發射一組User的Observable;

之後第二個.flatMap()裏, 輸入是前一個Observable的輸出, 即User, 調用了getUserObservable(), 返回的結果是Observable<User>, 之後加一個.toList(), 把輸出的結果從單個的User變爲List, 即和我們最初的例子一樣.

只不過此時得到的用戶信息是更詳細的用戶信息, 包含了他的repo數據和follow數據. 因爲它們是通過單獨查詢每一個人得到的.

運行, 雖然可以得到我們想要的結果, 但是這個例子仍然是有問題的.

線程問題處理

上面多個請求的例子, 發現雖然實現了我們的需求, 但是結果回來得很慢.
我們加上一個.map操作符來加上log:
(這裏省略了一些前後的代碼, 只是在.flatMap()里加了一個.map())

...
subscription = service.getUserFollowingObservable(username)
        .flatMap(new Func1<List<User>, Observable<User>>() {
            @Override
            public Observable<User> call(List<User> users) {
                return Observable.from(users);
            }
        })
        .flatMap(new Func1<User, Observable<User>>() {
            @Override
            public Observable<User> call(User user) {
                return service.getUserObservable(user.getLogin())
                        .map(new Func1<User, User>() {
                            @Override
                            public User call(User user) {
                                // this .map is used to output log information to check the threads
                                LogUtils.i("getUserObservable: " + user.getLogin());
                                return user;
                            }
                        });
            }
        })
        .toList()
        ...

由Log可以發現(log中的線程號是一樣的)單獨取每一個用戶詳細信息的請求都發生在同一個線程, 是順次進行的.

查看代碼:
Demo地址: https://github.com/mengdd/HelloRetrofit.
git checkout multiple-requests-in-single-thread

回頭梳理一下我們的需求, 請求一個所有follow的人, 返回一個follow的人的List, 然後對List中的每一個人, 單獨請求詳細信息.

那麼按理來說, 第二個批量的請求是可以同時發送, 並行進行的.
所以我們想要的行爲其實是平行發送多個請求, 然後最後統一結果到UI線程.

改動如下:

subscription = service.getUserFollowingObservable(username)
        .subscribeOn(Schedulers.io()) // 從io線程開始, 取用戶follow的所有人
        .flatMap(new Func1<List<User>, Observable<User>>() {
            @Override
            public Observable<User> call(List<User> users) {
                LogUtils.i("from");
                return Observable.from(users);
            }
        })
        .flatMap(new Func1<User, Observable<User>>() {
            @Override
            public Observable<User> call(User user) {
                return service.getUserObservable(user.getLogin()) // 取每個人的詳細信息
                        .subscribeOn(Schedulers.io()) // 指定取每個人詳細信息的工作都在單獨的io線程
                        .map(new Func1<User, User>() {
                            @Override
                            public User call(User user) {
                                // this map operation is just used for showing log
                                LogUtils.i("getUserObservable: " + user.getLogin());
                                return user;
                            }
                        });
            }
        })
        .toList()
        .observeOn(AndroidSchedulers.mainThread()) // 最後返回到主線程
        .subscribe(new Subscriber<List<User>>() {
            @Override
            public void onCompleted() {

            }

            @Override
            public void onError(Throwable e) {

            }

            @Override
            public void onNext(List<User> users) {
                LogUtils.i("onNext: " + users.size());
                peopleListAdapter.setUsers(users);
                peopleListAdapter.notifyDataSetChanged();
            }
        })

給改動的部分加上了註釋, 這樣更清楚一些.

注意subscribeOn()指定的是當前的這個Observable的工作在什麼線程進行.
所以在本例子中, subscribeOn(Schedulers.io())的位置放在.flatMap()裏面纔會產生多個請求並行的效果.

這樣一改, 我們的顯示時間不再是所有請求時間之和, 而是隻取決於最慢的那個請求時間.

查看代碼:
Demo地址: https://github.com/mengdd/HelloRetrofit
git checkout multiple-requests-in-multiple-threads

取消訂閱

正常情況下, 行爲結束之後, 到達onComplete()或者onError(), RxJava的訂閱會自動取消.
但是在處理網絡請求的時候, 很可能會出現請求還沒有返回, 界面就已經結束了的情況.

上面的代碼中已經出現了, 訂閱方法subscribe()的返回值是一個Subscription對象, 我們保存了這個對象的引用, 然後在onPause()的時候取消了請求, 防止內存泄露.

@Override
public void onPause() {
    super.onPause();
    if (subscription != null && subscription.isUnsubscribed()) {
        subscription.unsubscribe();
    }
}

當然也可以選別的生命週期回調, 比如onDestroyView()或者onDestroy().

如果有多個請求, 可以用:

private CompositeSubscription compositeSubscription = new CompositeSubscription();

...
// 在發請求的地方, 返回subscription
compositeSubscription.add(subscription);
...

// 選一個生命週期註銷所有請求
@Override
public void onPause() {
    super.onPause();
    compositeSubscription.unsubscribe();
}

Demo說明

Demo地址: https://github.com/mengdd/HelloRetrofit

本Demo只用於展示Retrofit和RxJava結合的使用, 爲了清晰起見所以沒有采用MVP構架, 也沒有用Dagger進行依賴注入, 有的請求也沒有在生命週期結束時取消, 也沒有UI的loading效果和沒網情況的處理等, 大家使用時請根據實際需要做一些處理.

這些沒有的東西會在我最近在做一個應用repo中出現: https://github.com/mengdd/GithubClient, 還在開發中, 可以關注一下.

另, Demo使用有時候用着用着請求就返回了:

{"message":"API rate limit exceeded for xxx ip...

這是因爲沒授權的用戶每小時最多隻能發60個請求:https://developer.github.com/v3/#rate-limiting
解決辦法就是..查following的人時, 不要查那種follow了很多人的賬號. orz.

References

歡迎關注我的微信公衆號, 定期更新Android開發資訊或文章:
wechat qrcode

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章