1. 程式人生 > >Android 教你一步步搭建MVP+Retrofit+RxJava網路請求框架

Android 教你一步步搭建MVP+Retrofit+RxJava網路請求框架

之前公司的專案用到了MVP+Retrofit+RxJava的框架進行網路請求,所以今天特此寫一篇文章以做總結。相信很多人都聽說過MVP、Retrofit、以及RxJava,有的人已經開始用了,有的人可能還不知道這是什麼,以及到底怎麼用。不過沒關係,接下來我將為你一一揭開他們的神祕面紗,然後利用這三個傢伙搭建一個網路請求框架

1.什麼是MVP?

MVP(Model View Presenter)其實就是一種專案的整體框架,能讓你的程式碼變得更加簡潔,說起框架大家可能還會想到MVC、MVVM。由於篇幅原因,這裡我們先不講MVVM,先來看一下MVC。其實Android本身就採用的是MVC(Model View Controllor)模式、其中Model指的是資料邏輯和實體模型;View指的是佈局檔案、Controllor指的是Activity。對於很多Android初學者可能會有這樣的經歷,寫程式碼的時候,不管三七二十一都往Activity中寫,當然我當初也是這麼幹的,根本就沒有什麼框架的概念,只要能實現某一個功能就很開心了,沒有管這麼多。當然專案比較小還好,一旦專案比較大,你會發現,Activity所承擔的任務其實是很重的,它既要負責頁面的展示和互動,還得負責資料的請求和業務邏輯之類的工作,相當於既要打理家庭,又要教育自己調皮的孩子,真是又當爹又當媽。。。那該怎麼辦呢?這時候Presenter這個繼父來到了這個家庭。Presenter對Activity說,我來了,以後你就別這麼辛苦了,你就好好打理好View這個家,我專門來負責教育Model這孩子,有什麼情況我會向你反映的。這時Activity流下了幸福的眼淚,從此,Model、View(Activity)、Presenter一家三口過上了幸福的生活。。。好了磕個藥繼續,由於Presenter(我們自己建的類)的出現,可以使View(Activity)不用直接和Model打交道,View(Activity)只用負責頁面的顯示和互動,剩下的和Model互動的事情都交給Presenter做,比如一些網路請求、資料的獲取等,當Presenter獲取到資料後再交給View(Activity)進行展示,這樣,Activity的任務就大大減小了。這便是MVP(Model 還是指的資料邏輯和實體模型,View指的是Activity,P就是Presenter)框架的工作方式。

2.什麼是Retrofit?

接下來我們看一下什麼是Retrofit。在官網對Retrofit的描述是這樣的
A type-safe HTTP client for Android and Java說人話就是“一個型別安全的用於Android和Java網路請求的客戶端”,其實就是一個封裝好的網路請求庫。接下來就來看一下這個庫該怎麼用。首先我在網上找了一個API介面用於測試:https://api.douban.com/v2/book/search?q=金瓶梅&tag=&start=0&count=1這是一個用於查詢一本書詳細資訊的一個請求介面。如果直接用瀏覽器開啟的話會返回以下內容:


瀏覽器中返回內容

接下來我們來看看如何用Retrofit將上面的請求下來。為了在Android Studio中新增Retrofit庫,我們需要新增如下依賴:

compile 'com.squareup.retrofit2:retrofit:2.1.0'

好了,新增完該庫,我們再來看看如何使用,首先我們來建一個實體類Book,用於裝網路請求後返回的資料。這裡順帶說一下,有的人建一個實體類時可能會根據瀏覽器中返回中的資料一行一行敲,其實這樣非常麻煩,這裡教大家一個簡單的方法,瞬間生成一個實體類。沒錯有的人可能用過,我們需要一個外掛GsonFormat。它的使用也很簡單,首先需要在Android Studio中下載,點選左上角選單欄中的File

,然後點選Settings,在彈窗中選擇Plugins,然後點選下方的Browse repositories...


然後在新開啟的視窗中搜索GsonFormat,點選右側綠色按鈕就可以下載安裝了,安裝完需要重啟下studio,就可以用了。


它的用法也很簡單,比如你先建立一個新的空類取名Book,然後在裡面按Alt+insert,會有個小彈窗選擇GsonFormat,之後在彈出的編輯框中拷入在瀏覽器中請求下來的那一坨東西,然後一直點ok就會自動生成欄位,以及set和get方法,一會兒我們用Retrofit請求下來的資料都會儲存在這個實體類中,還是挺方便的。最後我們裡面新增一個toString()方法,用於後面顯示方便。

接下來,回到我們的Retrofit中上,實體類已經建好了,我們來看看這個Retrofit如何進行網路請求,其實程式碼也很簡單。首先我們需要定義一個介面,取名RetrofitService :

public interface RetrofitService {
    @GET("book/search")
    Call<Book> getSearchBook(@Query("q") String name, 
                             @Query("tag") String tag, 
                             @Query("start") int start, 
                             @Query("count") int count);
}

額。。想必有人要問了,這是什麼玩意?跟我們平時定義的介面類很像,但又不一樣。別心急,我來一一解釋下,和別的介面類一樣,我們在其中定義了一個方法getSearchBook,那麼這個方法是做什麼的呢?其實它乾的事很簡單,就是拼接一個URL然後進行網路請求。這裡我們拼接的URL就是上文提到的測試URL:https://api.douban.com/v2/book/search?q=金瓶梅&tag=&start=0&count=1。聰明的你一定看出來了,在這個URL中book/search就是GET後的值,而?後的q、tag、start、count等入參就是這個方法的入參。有的朋友可能要問了,https://api.douban.com/v2/這麼一大串跑哪去了?其實我們在進行網路請求時,在URL中前一部分是相對不變的。什麼意思呢,比如你開啟間書網站,在間書中你開啟不同的網頁,雖然它的URL不同,但你會發現,每個URL前面都是以http://www.jianshu.com/開頭,我們把這個不變的部分,也叫做baseUrl提出來,放到另一個地方,在下面我們會提到。這樣我們一個完整的URL就拼接好了。在方法的開頭我們可以看到有個GET的註釋,說明這個請求是GET方法,當然你也可以根據具體需要用POST、PUT、DELETE以及HEAD。他們的區別如下:

  • GET ----------查詢資源(查)
  • POST --------修改資源(改)
  • PUT ----------上傳檔案(增)
  • DELETE ----刪除檔案(刪)
  • HEAD--------只請求頁面的首部

然後我們來看一下這個方法的返回值,它返回Call實體,一會我們要用它進行具體的網路請求,我們需要為它指定泛型為Book也就是我們資料的實體類。接下來,你會發現這個方法的入參和我們平時方法的入參還不大一樣。在每個入參前還多了一個註解。比如第一個入參@Query("q") String nameQuery表示把你傳入的欄位拼接起來,比如在測試url中我們可以看到q=金瓶梅的入參,那麼Query後面的值必須是q,要和url中保持不變,然後我們定義了String型別的name,當呼叫這個方法是,用於傳入字串,比如可以傳入“金瓶梅”。那麼這個方法就會自動在q後面拼上這個字串進行網路請求。以此類推,這個url需要幾個入參你就在這個方法中定義幾個入參,每個入參前都要加上Query註解。當然Retrofit除了Query這個註解外,還有其他幾個比如:@QueryMap、@Path、@Body、@FormUrlEncoded/@Field、@Header/@Headers。我們來看一下他們的區別:

@Query(GET請求):

用於在url後拼接上引數,例如:

@GET("book/search")
Call<Book> getSearchBook(@Query("q") String name);//name由呼叫者傳入

相當於:

@GET("book/search?q=name")
Call<Book> getSearchBook();

@QueryMap(GET請求):

當然如果入參比較多,就可以把它們都放在Map中,例如:

@GET("book/search")
Call<Book> getSearchBook(@QueryMap Map<String, String> options);

@Path(GET請求):

用於替換url中某個欄位,例如:

@GET("group/{id}/users")
Call<Book> groupList(@Path("id") int groupId);

像這種請求介面,在group和user之間有個不確定的id值需要傳入,就可以這種方法。我們把待定的值欄位用{}括起來,當然 {}裡的名字不一定就是id,可以任取,但需和@Path後括號裡的名字一樣。如果在user後面還需要傳入引數的話,就可以用Query拼接上,比如:

@GET("group/{id}/users")
Call<Book> groupList(@Path("id") int groupId,@Query("sort") String sort);

當我們呼叫這個方法時,假設我們groupId傳入1,sort傳入“2”,那麼它拼接成的url就是group/1/users?sort=2,當然最後請求的話還會加上前面的baseUrl。

@Body(POST請求):

可以指定一個物件作為HTTP請求體,比如:

@POST("users/new")
Call<User> createUser(@Body User user);

它會把我們傳入的User實體類轉換為用於傳輸的HTTP請求體,進行網路請求。

@Field(POST請求):

用於傳送表單資料:

@FormUrlEncoded
@POST("user/edit")
Call<User> updateUser(@Field("first_name") String first, @Field("last_name") String last);

注意開頭必須多加上@FormUrlEncoded這句註釋,不然會報錯。表單自然是有多組鍵值對組成,這裡的first_name就是鍵,而具體傳入的first就是值啦。

@Header/@Headers(POST請求):

用於新增請求頭部:

@GET("user")
Call<User> getUser(@Header("Authorization") String authorization)

表示將頭部Authorization屬性設定為你傳入的authorization;當然你還可以用@Headers表示,作用是一樣的比如:

@Headers("Cache-Control: max-age=640000")
@GET("user")
Call<User> getUser()

當然你可以多個設定:

@Headers({
    "Accept: application/vnd.github.v3.full+json",
    "User-Agent: Retrofit-Sample-App"
})
@GET("user")
Call<User> getUser()

好了,這樣我們就把上面這個RetrofitService 介面類解釋的差不多了。我覺得,Retrofit最主要的也就是這個介面類的定義了。好了,有了這個介面類,我們來看一下,到底如何使用這個我們定義的介面來進行網路請求。程式碼如下:

Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("https://api.douban.com/v2/")
        .addConverterFactory(GsonConverterFactory.create(new GsonBuilder().create()))
        .build();
RetrofitService service = retrofit.create(RetrofitService.class);
Call<Book> call =  service.getSearchBook("金瓶梅", null, 0, 1);
call.enqueue(new Callback<Book>() {
    @Override
    public void onResponse(Call<Book> call, Response<Book> response) {
        text.setText(response.body()+"");
    }
    @Override
    public void onFailure(Call<Book> call, Throwable t) {
    }
});

這裡我們可以看到,先新建了一個Retrofit物件,然後給它設定一個我們前面說的baseUrlhttps://api.douban.com/v2/.因為介面返回的資料不是我們需要的實體類,我們需要呼叫addConverterFactory方法進行轉換。由於返回的資料為json型別,所以在這個方法中傳入Gson轉換工廠GsonConverterFactory.create(new GsonBuilder().create()),這裡我們需要在studio中新增Gson的依賴:

compile 'com.squareup.retrofit2:converter-gson:2.1.0'

然後我們呼叫retrofit的create方法並傳入上面我們定義的介面的檔名RetrofitService.class,就可以得到RetrofitService 的實體物件。有了這個物件,我們就可以呼叫裡面之前定義好的請求方法了。比如:

Call<Book> call =  service.getSearchBook("金瓶梅", null, 0, 1);

它會返回一個Call實體類,然後就可以呼叫Call的enqueue方法進行非同步請求,在enqueue方法中傳入一個回撥CallBack,重寫裡面的onResponse和
onFailure方法,也就是請求成功和失敗的回撥方法。當成功時,它會返回Response,裡邊封裝了請求結果的所有資訊,包括報頭,返回碼,還有主體等。比如呼叫它的body()方法就可獲得Book物件,也就是我們需要的資料。這裡我們就把返回的Book,顯示螢幕上。如下圖:


Book中的資料

好了,到這裡我們就基本瞭解了Retrofit的整個工作流程。

3.RxJava

我們這篇文章主要介紹搭建整體網路請求框架,所以關於RxJava的基礎知識,我這就不再詳細介紹了,網上也有很多文章,對RxJava還不是很瞭解的同學,推薦你看一下拋物線的這篇文章給 Android 開發者的 RxJava 詳解

下面我們來看一下RxJava和retrofit的結合使用,為了使Rxjava與retrofit結合,我們需要在Retrofit物件建立的時候新增一句程式碼addCallAdapterFactory(RxJavaCallAdapterFactory.create()),當然你還需要在build.gradle檔案中新增如下依賴:

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

完整的程式碼如下:

Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("https://api.douban.com/v2/")
        .addConverterFactory(GsonConverterFactory.create(new GsonBuilder().create()))
        .addCallAdapterFactory(RxJavaCallAdapterFactory.create())//支援RxJava
        .build();

然後我們還需要修改RetrofitService 中的程式碼:

public interface RetrofitService {
    @GET("book/search")
    Observable<Book> getSearchBook(@Query("q") String name,
                                    @Query("tag") String tag, @Query("start") int start,
                                    @Query("count") int count);

可以看到,在原來的RetrofitService 中我們把getSearchBook方法返回的型別Call改為了Observable,也就是被觀察者。其他都沒變。然後就是建立RetrofitService 實體類:

RetrofitService service = retrofit.create(RetrofitService.class);

和上面一樣,建立完RetrofitService ,就可以呼叫裡面的方法了:

Observable<Book> observable =  service.getSearchBook("金瓶梅", null, 0, 1);

其實這一步,就是建立了一個rxjava中observable,即被觀察者,有了被觀察者,就需要一個觀察者,且訂閱它:

observable.subscribeOn(Schedulers.io())//請求資料的事件發生在io執行緒
          .observeOn(AndroidSchedulers.mainThread())//請求完成後在主執行緒更顯UI
          .subscribe(new Observer<Book>() {//訂閱
              @Override
              public void onCompleted() {
                  //所有事件都完成,可以做些操作。。。
              }
              @Override
              public void onError(Throwable e) {
                  e.printStackTrace(); //請求過程中發生錯誤
              }
              @Override
              public void onNext(Book book) {//這裡的book就是我們請求介面返回的實體類    
              }
           }

在上面中我們可以看到,事件的消費在Android主執行緒,所以我們還要在build.gradle中新增如下依賴:

compile 'io.reactivex:rxandroid:1.2.0'

這樣我們就引入了RxAndroid,RxAndroid其實就是對RxJava的擴充套件。比如上面這個Android主執行緒在RxJava中就沒有,因此要使用的話就必須得引用RxAndroid。

4.實踐

接下來我們就看看,在一個專案中上面三者是如何配合的。我們開啟Android Studio,新建一個專案取名為MVPDemo。這個demo的功能也很簡單,就是點選按鈕呼叫上面的那個測試介面,將請求下來書的資訊顯示在螢幕上。首先我們來看一下這個工程的目錄結構:


工程目錄


我們可以看到,在專案的包名下,我們建了三個主要的資料夾:app、service、ui。當然根據專案的需要你也可以新增更多其他的資料夾,比如一些工具類等。其中app資料夾中可以建一個Application類,用於設定應用全域性的一些屬性,這裡為了使專案更加簡單就沒有新增;然後,我們再來看看ui資料夾下,這個資料夾下主要放一些關於介面的東西。在裡面我們又建了三個資料夾:activity、adapter、fragment,我想看名字你就清楚裡面要放什麼了。最後我們在重點看看service資料夾中的東西。首先我們來看看裡面重要的兩個類:RetrofitHelper和RetrofitService。RetrofitHelper主要用於Retrofit的初始化:

public class RetrofitHelper {

    private Context mCntext;

    OkHttpClient client = new OkHttpClient();
    GsonConverterFactory factory = GsonConverterFactory.create(new GsonBuilder().create());
    private static RetrofitHelper instance = null;
    private Retrofit mRetrofit = null;
    public static RetrofitHelper getInstance(Context context){
        if (instance == null){
            instance = new RetrofitHelper(context);
        }
        return instance;
    }
    private RetrofitHelper(Context mContext){
        mCntext = mContext;
        init();
    }

    private void init() {
        resetApp();
    }

    private void resetApp() {
        mRetrofit = new Retrofit.Builder()
                .baseUrl("https://api.douban.com/v2/")
                .client(client)
                .addConverterFactory(factory)
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .build();
    }
    public RetrofitService getServer(){
        return mRetrofit.create(RetrofitService.class);
    }
}

程式碼並不複雜,其中resetApp方法,就是前面介紹的Retrofit的建立,getServer方法就是為了獲取RetrofitService介面類的例項化。然後定義了一個靜態方法getInstance用於獲取自身RetrofitHelper的例項化,並且只會例項化一次。

接下來,看一下RetrofitService,其中程式碼還是上面一樣:

public interface RetrofitService {
    @GET("book/search")
    Observable<Book> getSearchBooks(@Query("q") String name,
                                    @Query("tag") String tag, @Query("start") int start,
                                    @Query("count") int count);
}

然後我們依次來看一下service資料夾下的四個資料夾:entity、manager、presenter和view。其中entity下放我們請求的實體類,這裡就是Book。接下來我們來看一下manager中DataManager。這個類其實就是為了讓你更方便的呼叫RetrofitService 中定義的方法:

public class DataManager {
    private RetrofitService mRetrofitService;
    public DataManager(Context context){
        this.mRetrofitService = RetrofitHelper.getInstance(context).getServer();
    }
    public  Observable<Book> getSearchBooks(String name,String tag,int start,int count){
        return mRetrofitService.getSearchBooks(name,tag,start,count);
    }
}

可以看到,在它的構造方法中,我們得到了RetrofitService 的例項化,然後定義了一個和RetrofitService 中同名的方法,裡面其實就是呼叫RetrofitService 中的這個方法。這樣,把RetrofitService 中定義的方法都封裝到DataManager 中,以後無論在哪個要呼叫方法時直接在DataManager 中呼叫就可以了,而不是重複建立RetrofitService 的例項化,再呼叫其中的方法。

好了,我們再來看一下presenter和view,我們在前面說過,presenter主要用於網路的請求以及資料的獲取,view就是將presenter獲取到的資料進行展示。首先我們先來看view,我們看到我們建了兩個介面類View和BookView,其中View是空的,主要用於和Android中的View區別開來:

public interface View {
}

然後讓BookView繼承自我們自己定義的View :

public interface BookView extends View {
    void onSuccess(Book mBook);
    void onError(String result);
}

可以看到在裡面定義兩個方法,一個onSuccess,如果presenter請求成功,將向該方法傳入請求下來的實體類,也就是Book,view拿到這個資料實體類後,就可以進行關於這個資料的展示或其他的一些操作。如果請求失敗,就會向這個view傳入失敗資訊,你可以彈個Toast來提示請求失敗。通常這兩個方法比較常用,當然你可以根據專案需要來定義一些其他的方法。接下來我們看看presenter是如何進行網路請求的 。我們也定義了一個基礎Presenter:

public interface Presenter {
    void onCreate();

    void onStart();//暫時沒用到

    void onStop();

    void pause();//暫時沒用到

    void attachView(View view);

    void attachIncomingIntent(Intent intent);//暫時沒用到
}

裡面我們可以看到,定義了一些方法,前面幾個onCreate、onStart等方法對應著Activity中生命週期的方法,當然沒必要寫上Activity生命週期中所有回撥方法,通常也就用到了onCreate和onStop,除非需求很複雜,在Activity不同生命週期請求的情況不同。接著我們定義了一個attachView方法,用於繫結我們定義的View。也就是,你想把請求下來的資料實體類給哪個View就傳入哪個View。下面這個attachIncomingIntent暫且沒用到,就不說了。好了,我們來看一下BookPresenter具體是怎麼實現的:

public class BookPresenter implements Presenter {
    private DataManager manager;
    private CompositeSubscription mCompositeSubscription;
    private Context mContext;
    private BookView mBookView;
    private Book mBook;
    public BookPresenter (Context mContext){
        this.mContext = mContext;
    }
    @Override
    public void onCreate() {
        manager = new DataManager(mContext);
        mCompositeSubscription = new CompositeSubscription();
    }

    @Override
    public void onStart() {

    }

    @Override
    public void onStop() {
        if (mCompositeSubscription.hasSubscriptions()){
            mCompositeSubscription.unsubscribe();
        }
    }

    @Override
    public void pause() {

    }

    @Override
    public void attachView(View view) {
        mBookView = (BookView)view;
    }

    @Override
    public void attachIncomingIntent(Intent intent) {
    }
    public void getSearchBooks(String name,String tag,int start,int count){
        mCompositeSubscription.add(manager.getSearchBooks(name,tag,start,count)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Observer<Book>() {
                    @Override
                    public void onCompleted() {
                        if (mBook != null){
                            mBookView.onSuccess(mBook);
                        }
                    }

                    @Override
                    public void onError(Throwable e) {
                        e.printStackTrace();
                        mBookView.onError("請求失敗!!");
                    }

                    @Override
                    public void onNext(Book book) {
                        mBook = book;
                    }
                })
        );
    }
}

BookPresenter實現了我們定義的基礎Presenter,在onCreate中我們建立了DataManager的實體類,便於呼叫RetrofitService中的方法,還新建了一個CompositeSubscription物件,CompositeSubscription是用來存放RxJava中的訂閱關係的。注意請求完資料要及時清掉這個訂閱關係,不然會發生記憶體洩漏。可在onStop中通過呼叫CompositeSubscription的unsubscribe方法來取消這個訂閱關係,不過一旦呼叫這個方法,那麼這個CompositeSubscription也就無法再用了,要想再用只能重新new一個。然後我們可以看到在attachView中,我們把BookView傳進去。也就是說我們要把請求下來的實體類交給BookView來處理。接下來我們定義了一個方法getSearchBooks,名字和入參都和請求介面RetrofitService中的方法相同。這裡的這個方法也就是請求的具體實現過程。其實也很簡單,就是向CompositeSubscription新增一個訂閱關係。上面我們已經說過manager.getSearchBooks就是呼叫RetrofitService的getSearchBooks方法,而這個方法返回的是一個泛型為Book的Observable,即被觀察者,然後通過subscribeOn(Schedulers.io())來定義請求事件發生在io執行緒,然後通過observeOn(AndroidSchedulers.mainThread())來定義事件在主執行緒消費,即在主執行緒進行資料的處理,最後通過subscribe使觀察者訂閱它。在觀察者中有三個方法:onNext、onCompleted、onError。當請求成功話,就會呼叫onNext,並傳入請求返回的Book實體類,我們在onNext中,把請求下來的Book實體類存到記憶體中,當請求結束後會呼叫onCompleted,我們把請求下來的Book實體類交給BookView處理就可以了,如果請求失敗,那麼不會呼叫onCompleted而呼叫onError,這樣我們可以向BookView傳遞錯誤訊息。

好了,這樣我們我們就可以呼叫這個介面方法來進行網路的請求了,我們先寫一下頁面的佈局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:orientation="vertical"
    android:paddingTop="@dimen/activity_vertical_margin">

    <TextView
        android:id="@+id/text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!" />
    <Button
        android:id="@+id/button"
        android:onClick="getFollowers"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text=