真實案例出發,再談retrofit封裝

nk2670 8年前發布 | 49K 次閱讀 Retrofit Java Android開發 移動開發

前言

在使用了一段時間的Retrofit之后,今天終于在這里講解到了網絡的部分。目前開源的HTTP 框架有很多,Volley,Android Async Http,以及OkHttp +Retrofit等。而我在自己的使用中選擇了Retrofit,這里就從基礎到原理,再到實例的方式,講解我對Retrofit做出的一些封裝和使用。來讓你進一步的了解和掌握Retrofit的使用。

基礎

Retrofit一個基于OkHttp的RESTFUL API請求工具。它是 Square 推出的 HTTP 框架,主要用于 Android 和 Java。Retrofit 將網絡請求變成方法的調用,使用起來非常簡潔方便。

A type-safe HTTP client for Android and Java

如果你還對Retrofit不了解,那么我建議你去 官方文檔 了解一下。

Retrofit使用大體分為三個步驟

(1)Retrofit將HTTP API 轉化成了Java接口的形式,所以首先我們會提供一個接口類GitHubService 。

public interface GitHubService {
  @GET("users/{user}/repos")
  Call<List<Repo>> listRepos(@Path("user") String user);
}

(2)Retrofit類可以針對之前定義的GitHubService 接口生成一個具體實現。

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("

GitHubService service = retrofit.create(GitHubService.class);</code></pre>

(3)然后就可以對GitHubService 的方法進行同步或者異步的調用來進行網絡的訪問,也就是說可以通過call對象獲得數據:(可以使用enqueue 或者 execute來執行發起請求,enqueue是是異步執行,而 execute是同步執行。)

Call<List<Repo>> repos = service.listRepos("octocat");

通過上面三個步驟,我們會發現Retrofit給人眼前一亮的當然是它的注解調用和優雅的API轉化為方法。每一個方法都會對應著一個Http的注解,總共有GET, POST, PUT, DELETE,HEAD五個內嵌的注解。我們也會在注解上指定相應的相對地址信息。比如上方的 @GET("users/{user}/repos")

這里本來想將官網所有內容翻譯一遍的,返現很多詞不達意 。然后今天又湊巧看到了郭神公眾號推薦的一篇文章 Android網絡請求庫 - Say hello to retrofit .對官網對的內容講解得非常的詳細易懂,繼續閱讀下面章節之前,一定要去看看這篇文章。

于是我們完整的Retrofit使用流程:

Retrofit retrofit = new Retrofit.Builder()
.baseUrl("

GitHubService service = retrofit.create(GitHubService.class); Call<List<Repo>> repos = service.listRepos("octocat"); repos.enqueue(new Callback<List<Repo>>() { @Override public void onResponse(Call<List<Repo>> call, Response<List<Repo>> response) {

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

}

});</code></pre>

Retrofit原理解析

在進一步了解和使用Retrofit之前,不妨先來了解Retrofit的原理,看看Retrofit的源碼是了解原理的一個有效途徑。

(1) 源碼結構

Retrofit包含一個http包,里面全部是定義HTTP請求的Java注解,比如GET、POST、PUT、DELETE、Headers、Path、Query等等。

余下的retrofit包中幾個類和接口就是全部retrofit的代碼了,代碼很少因為retrofit把網絡請求這部分功能全部交給了OkHttp了。

(2) 整體流程

繼續回到官網的例子。

首先關注的是我們通過 new Retrofit.Builder()...build() 進行Retrofit的構建,可以了解的是這里使用的是 Builder 模式。

在Android源碼中,經常用到Builder模式的可能就是AlerDialog 了。Builder模式用于將一個復雜的對象的構建和它的表示分離,使得同樣的構建過程可以創建不同的表示。這里Retrofit使用Builder模式支持了支持不同的轉換(就是將HTTP返回的數據解析成Java對象,主要有Xml、Gson等)和返回(主要作用就是將Call對象轉換成另一個對象,比如RxJava)。這里也就真正的達到了構建復雜對象和它的部件進行解耦。

這里通過build方法來了解Retrofit創建,需要6個參數。如下方代碼注解:

public Retrofit build() {
      //1 baseUrl   基地址
      if (baseUrl == null) {
        throw new IllegalStateException("Base URL required.");
      }
      //2 callFactory  默認創建一個 OkHttpClient
      okhttp3.Call.Factory callFactory = this.callFactory;
      if (callFactory == null) {
        callFactory = new OkHttpClient();
      }

  Executor callbackExecutor = this.callbackExecutor;
  if (callbackExecutor == null) {
    //3 callbackExecutor  Android 中返回的是 MainThreadExecutor
    callbackExecutor = platform.defaultCallbackExecutor();
  }

 //4 adapterFactories(比如RxJavaCallAdapterFactory 用于將Call返回支持Rxjava)  把Call對象轉換成其它類型
  List<CallAdapter.Factory> adapterFactories = new ArrayList<>(this.adapterFactories);
  adapterFactories.add(platform.defaultCallAdapterFactory(callbackExecutor));

  //5  converterFactories(例如GsonConverterFactory 用于Gson轉換)  請求網絡得到的response的轉換器的集合 默認會加入 BuiltInConverters ,
  List<Converter.Factory> converterFactories = new ArrayList<>(this.converterFactories);

  //6 private boolean validateEagerly;  validateEagerly 是否需要立即解析接口中的方法

  return new Retrofit(callFactory, baseUrl, converterFactories, adapterFactories,
      callbackExecutor, validateEagerly);
}

}</code></pre>

所以我們會看到我們通過Builder模式創建Retrofit訪問對象都必須指定基地址url。如果還需要支持Gson轉換,我們就需要添加 .addConverterFactory(GsonConverterFactory.create()) ,如果需要支持Rxjava,那么還需要添加 .addCallAdapterFactory (RxJavaCallAdapterFactory.create()) 。

接著我們通過 GitHubService service = retrofit.create(GitHubService.class); create方法創建網絡請求接口類GitHubService 的實例。我們正是使用該對象的listRepos方法完成了 Call<List<Repo>> repos = service.listRepos("octocat"); 獲取到了數據。下面看看create方法的源碼:

public <T> T create(final Class<T> service) {
    Utils.validateServiceInterface(service);
    if (validateEagerly) {
      eagerlyValidateMethods(service);
    }
    return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
        new InvocationHandler() {
          private final Platform platform = Platform.get();

      @Override public Object invoke(Object proxy, Method method, Object... args)
          throws Throwable {
        // If the method is a method from Object then defer to normal invocation.
        if (method.getDeclaringClass() == Object.class) {
          return method.invoke(this, args);
        }
        //為了兼容 Java8 平臺,Android 中不會執行
        if (platform.isDefaultMethod(method)) {
          return platform.invokeDefaultMethod(method, service, proxy, args);
        }
        ServiceMethod serviceMethod = loadServiceMethod(method);
        OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
        return serviceMethod.callAdapter.adapt(okHttpCall);
      }
    });

}</code></pre>

create方法接受一個 Class 對象,也就是我們編寫的接口,里面含有通過注解標識的請求網絡的方法。注意 return

語句部分,這里調用了 Proxy.newProxyInstance方法,這個很重要,因為用了 動態代理模式 。關于動態代理模式,可以參考這篇文章: 公共技術點之 Java 動態代理 。簡單的描述就是,Proxy.newProxyInstance根據傳進來的 Class 對象生成了一個實例 A,也就是代理類。每當這個代理類 A 執行某個方法時,總是會調用 InvocationHandler(Proxy.newProxyInstance中的第三個參數) 的invoke方法,在這個方法中可以執行一些操作(這里是解析方法的注解參數等),通過這個方法真正的執行我們編寫的接口中的網絡請求。

也就是概括一句話:通過動態代理的方式把 Java 接口中的解析為響應的網絡請求,然后交給 OkHttp 去執行。并且可以適配不同的 CallAdapter

可以方便與 RxJava 結合使用。

封裝和使用

之前有網友評論,說網絡上的很多開源庫已經封裝的很完美了 ,我們就不需要再次做出多余的封裝了 。這個觀點實在是不敢茍同,開源庫固然已經做了很多事情,但是我們還是要根據不同的業務邏輯封裝自己的使用呢 。比如同樣的圖片加載,我們不可能每次都要調用Glide的一些初始化操作。同樣的網絡請求,我們也不可能每次都寫一大堆初始化代碼 。每個app的邏輯業務操作都是相同的,當然可以封裝起來,讓代碼更加清爽。

下面講講我這里的封裝邏輯。并提供Github我的關注列表以及百度天氣接口的訪問,兩個真實案例進行講解使用,項目代碼將會在 CameloeAnthony / Ant 中提供更新:

(1) Github 關注列表

來看看Github訪問頁面,這里只需要下面幾行代碼就完成了GithubUser 數據的返回。

mSubscription = mDataManager.loadUserFollowingList("CameloeAnthony")
                .subscribe(new HttpSubscriber<List<GithubUser>>() {
                    @Override
                    public void onNext(List<GithubUser> users) {
                        ......Github用戶數據加載完成
                    }
                });

這里使用 loadUserFollowingList 方法通過Rxjava的Observable返回 Observable<List<GithubUser>> 對象,DataManager是一個數據的入口,我們不將所有的數據訪問放在DataManager中。這種創建方式在之前的文章 淺析MVP中model層設計 中有過提及,類似于通常使用的 Respository

接下來看看DataManager中的 loadUserFollowingList 方法。

/**

 * load  following list of github users
 * @return Observable<List<GithubUser>>
 */
public Observable<List<GithubUser>> loadUserFollowingList(String userName){
    return mHttpHelper.getService(GithubApi.class)
            .loadUserFollowingList(userName)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread());
}</code></pre> 

這里傳入了接口GithubApi.class 然后調用了HttpHelper的 loadUserFollowingList 方法。

在此架構中,Model層被劃分為兩個部分:許多helpers類和一個 DataManager

.helpers類的數量在不同的工程中不盡相同,但是每個都有自己的功能。比如:通過SharedPreferences與數據進行交互的PreferHelper,通過SqlBrite提供與數據庫交互的DatabaseHelper,DataManager結合并且轉化不同的Helpers類為Rx操作符,向Presenter層提供Observables類型的數(provide meaningful data to the Presenter),并且同時處理數據的并發操作(group actions that will always happen together.)。這一層也包含實際的model類,用于定義當前數據架構。

GithubApi 接口,你可以直接訪問

https://api.github.com/users/CameloeAnthony/following 獲取到這些列表數據。

public interface GithubApi {
    String end_point = "https://api.github.com/";
    @GET("/users/{user}/following")
    Observable<List<GithubUser>> loadUserFollowingList(@Path(value = "user") String user);
}

GithubUser 是與Github API對應的Github用戶信息的實體類,API 和實體類的轉化可以去網站 http://www.jsonschema2pojo.org/ 快捷完成:

public class GithubUser {

    @SerializedName("login")
    private String login;
    @SerializedName("id")
    private Integer id;
......

    public String getLogin() {
        return login;
    }
......

(2) 天氣信息的加載

這里加載百度API提供的天氣信息

首先還是加載的頁面的方法,非常簡單的Rxjava操作完成了數據的讀取

mSubscription = mDataManager.loadWeatherData(“成都”).subscribe(new HttpSubscriber<WeatherData>() {
            @Override
            public void onNext(WeatherData weatherData) {
             .......天氣數據加載完成
            }

            @Override
            public void onError(Throwable e) {
                super.onError(e);
                toastUtils.showToast("加載天氣信息失敗");
            }
        });

接著看看DataManager提供的方法 loadWeatherData

public Observable<WeatherData> loadWeatherData(String location) {
        Map<String, String> params = new HashMap<>();
        params.put("location", location);
        params.put("language", "zh-Hans");
        params.put("unit", "c");
        params.put("start", "0");
        params.put("days", "3");
        return mHttpHelper.getService(WeatherApi.class)
                .loadWeatherData(params)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread());
    }

這里同樣是調用了HttpHelper的loadWeatherData方法。遵從的是上面一樣的Model層訪問原則。所有數據都是先訪問DataManager再訪問相應的類,比如這里的HttpHelper。

public interface WeatherApi {
    String end_point = “http://apis.baidu.com/”;

    //example , remember to add a apikey to your header
// "http://apis.baidu.com/thinkpage/weather_api/suggestion?location=beijing&language=zh-Hans&unit=c&start=0&days=3";

    @Headers("apikey: 87f4cacc3ffe1f1025ebf1ea415ff112")
    @GET("/thinkpage/weather_api/suggestion")
    Observable<WeatherData> loadWeatherData(@QueryMap Map<String,String> params);
}

WeatherData同樣是根據百度天氣API編寫的實體類 。這個實體類也是有點復雜。所以同樣是通過 http://www.jsonschema2pojo.org/ 把API json放到輸入框,然后寫好名字,快速的完成了實體類的創建。

所以這里就講解完了兩個接口的調用 。但是客官,你肯定要說 ,這不對呀 ,你這Retrofit蹤跡都沒看到。你就完成了各種API調用,你逗我呢吧?哈哈,接著往下看。

(3)retrofit封裝

這里還是要回到最初的那一段代碼,retrofit的創建分為三個步驟。回到基礎部分再看一下吧。雖然上面的兩次API調用都沒有“使用Retrofit”,但是都是使用了HttpHelper類。將 GithubApi 和 WeatherApi 分別傳遞到了HttpHelper對象的 getService 方法中,所以貓膩就在這里 。看下面的代碼:

/**
 * Created by Anthony on 2016/7/8.
 * Class Note:
 * entrance class to access network with {@link Retrofit}
 * used only by{@link DataManager} is recommended
 * <p>
 * 使用retrofit進行網絡訪問的入口類,推薦只在{@link DataManager}中使用
 */
public class HttpHelper {
    private static final int DEFAULT_TIMEOUT = 30;
    private HashMap<String, Object> mServiceMap;
    private Context mContext;
//    private Gson gson = new GsonBuilder().setLenient().create();

    @Inject
    public HttpHelper(@ApplicationContext Context context) {
        //Map used to store RetrofitService
        mServiceMap = new HashMap<>();
        this.mContext = context;
    }


    @SuppressWarnings("unchecked")
    public <S> S getService(Class<S> serviceClass) {
        if (mServiceMap.containsKey(serviceClass.getName())) {
            return (S) mServiceMap.get(serviceClass.getName());
        } else {
            Object obj = createService(serviceClass);
            mServiceMap.put(serviceClass.getName(), obj);
            return (S) obj;
        }
    }

    @SuppressWarnings("unchecked")
    public <S> S getService(Class<S> serviceClass, OkHttpClient client) {
        if (mServiceMap.containsKey(serviceClass.getName())) {
            return (S) mServiceMap.get(serviceClass.getName());
        } else {
            Object obj = createService(serviceClass, client);
            mServiceMap.put(serviceClass.getName(), obj);
            return (S) obj;
        }
    }

    private <S> S createService(Class<S> serviceClass) {
        //custom OkHttp
        OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
        //time our
        httpClient.connectTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS);
        httpClient.writeTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS);
        httpClient.readTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS);
        //cache
        File httpCacheDirectory = new File(mContext.getCacheDir(), "OkHttpCache");
        httpClient.cache(new Cache(httpCacheDirectory, 10 * 1024 * 1024));
        //Interceptor
        httpClient.addNetworkInterceptor(new LogInterceptor());
        httpClient.addInterceptor(new CacheControlInterceptor());

        return createService(serviceClass, httpClient.build());
    }

    private <S> S createService(Class<S> serviceClass, OkHttpClient client) {
        String end_point = "";
        try {
            Field field1 = serviceClass.getField("end_point");
            end_point = (String) field1.get(serviceClass);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.getMessage();
            e.printStackTrace();
        }

        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(end_point)
                .addConverterFactory(GsonConverterFactory.create())
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .client(client)
                .build();

        return retrofit.create(serviceClass);
    }

    private class LogInterceptor implements Interceptor {
        @Override
        public Response intercept(Chain chain) throws IOException {
            Request request = chain.request();

            long t1 = System.nanoTime();
            Timber.i("HttpHelper" + String.format("Sending request %s on %s%n%s",
                    request.url(), chain.connection(), request.headers()));

            Response response = chain.proceed(request);
            long t2 = System.nanoTime();

            Timber.i("HttpHelper" + String.format("Received response for %s in %.1fms%n%s",
                    response.request().url(), (t2 - t1) / 1e6d, response.headers()));
            return response;

            // log Response Body
//            if(BuildConfig.DEBUG) {
//                String responseBody = response.body().string();
//                Log.v("HttpHelper", String.format("Received response for %s in %.1fms%n%s%n%s",
//                        response.request().url(), (t2 - t1) / 1e6d, response.headers(), responseBody));
//                return response.newBuilder()
//                        .body(ResponseBody.create(response.body().contentType(), responseBody))
//                        .build();
//            } else {
//                Log.v("HttpHelper", String.format("Received response for %s in %.1fms%n%s",
//                        response.request().url(), (t2 - t1) / 1e6d, response.headers()));
//                return response;
//            }
        }
    }

    private class CacheControlInterceptor implements Interceptor {
        @Override
        public Response intercept(Chain chain) throws IOException {
            Request request = chain.request();
            if (!AppUtils.isNetworkConnected(mContext)) {
                request = request.newBuilder()
                        .cacheControl(CacheControl.FORCE_CACHE)
                        .build();
            }

            Response response = chain.proceed(request);

            if (AppUtils.isNetworkConnected(mContext)) {
                int maxAge = 60 * 60; // read from cache for 1 minute
                response.newBuilder()
                        .removeHeader("Pragma")
                        .header("Cache-Control", "public, max-age=" + maxAge)
                        .build();
            } else {
                int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale
                response.newBuilder()
                        .removeHeader("Pragma")
                        .header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale)
                        .build();
            }
            return response;
        }
    }
}

這里getService方法將會獲取緩存中的是否有傳遞進來的Class對象。有則使用,沒有則創建。

這里則調用了 createService(Class<S> serviceClass) 進行了OkHttpClient的初始化操作,并添加了兩個LogInterceptor,CacheControlInterceptor 分別用于打印相關的請求信息。最終調用的方法是 createService(Class<S> serviceClass, OkHttpClient client) 我們通過反射達到了end_point字段的基地址。

然后代碼也就回到了

Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(end_point)
                .addConverterFactory(GsonConverterFactory.create())
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .client(client)
                .build();

        return retrofit.create(serviceClass);

可以看到了這里同樣是進行了Retrofit的Builder創建以及create操作。到這里我想你就 .addConverterFactory(GsonConverterFactory.create()) 完成了添加Gson的轉換器的操作。 RxJavaCallAdapterFactory.create() 完成了RxJava結果返回的支持。所以我們才可以支持了返回Observable的對象。

(4) 思路梳理

這里所有的代碼也就完成了我們的操作。所以能夠看到網絡的訪問變得更加簡潔。這里對優點和缺點進行總結,需要在開發使用中注意:

優點:

1 通過DataManager作為數據入口的形式,屏蔽底層細節,讓網絡的訪問更加清晰。

2 使用 快速的通過API Json數據完成了實體類的創建

3 支持Rxjava的流式操作,是Retrofit的使用更加得心應手。當我們執行異步操作的時候,java提供了Thread, Future,FutureTask, CompletableFuture 去解決這個問題,但是隨著任務的復雜程度的增加,代碼也變得難于維護,他們也不能實現Rxjava一樣的鏈式處理操作。Rxjava相比具有更高的靈活性,可以鏈式調用,可以對單個事件以及序列進行操作。

4 支持Gson轉換器和Retrofit的配合,省去了Gson fromJson的操作。更加便捷。

5 HttpHelper對Retrofit的封裝省去了Retrofit初始化的創建,并且添加了攔截器進行日志打印方便查看。

缺點:

1 Api接口類中的基地址,必須按照”end_point”的形式提供。

2 DataManager隨著項目的增大作為唯一的數據入口將會變得越來越臃腫。

當然本項目也引入了Dagger2和ButterKnife,讓代碼更加的整潔易用。

當然本篇文章沒有對官網和源碼細節進行進一步解析。都可以在上面和下方提供的參考鏈接中進行查看。

example project make your architecting of android apps quicker and smoother ,long-term maintenance by me,blog updating at the same time.Used in real project in my development 由作者長期維護的架構以及示例代碼,用于本人的各種真實項目。博客更新,希望對你的安卓架構提供指導性的意義

參考文章

Retrofit官網

Android網絡請求庫 - Say hello to retrofit

RxJava 與 Retrofit 結合的最佳實踐 「Android技術匯」Retrofit2 源碼解析和案例說明 Android Retrofit源碼解析 Rxjava+ReTrofit+okHttp深入淺出-終極封裝

 

來自:http://blog.csdn.net/u014315849/article/details/52809011

 

 本文由用戶 nk2670 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!