Android開發網路通訊一開始的時候使用的是AsyncTask封裝HttpClient,沒有使用原生的HttpURLConnection就跳到了Volley,隨著OkHttp的流行又開始遷移到OkHttp上面,隨著Rxjava的流行又瞭解了Retrofit,隨著Retrofit的發展又從1.x到了2.x......。好吧,暫時到這裡。
那麼的多的使用工具有時候有點眼花繚亂,今天來總結一下現在比較流行的基於OkHttp 和 Retrofit 的網路通訊API設計方法。有些同學可能要想,既然都有那麼好用的Volley和Okhttp了,在需要用到的地方建立一個Request然後交給RequestQueue(Volley的方式)或者 Call(Okhttp的方式)就行了嗎,為什麼還那麼麻煩? 但是我認為這種野生的網路庫的用法還是是有很多弊端(弊端就不說了,畢竟是總結新東西),在好的Android架構中都不會出現這樣的程式碼。
網路通訊都是非同步完成,設計網路API我覺得首先需要考慮非同步結果的返回機制。基於Okhttp或Retrofit,我們考慮如何返回非同步的返回結果,有幾種方式:
1. 直接返回:
OkHttp 的返回方式:
OkHttpClient : OkHttpClient client = new OkHttpClient(); Request : Request request = new Request.Builder() .url("https://api.github.com/repos/square/okhttp/issues") .header("User-Agent", "OkHttp Headers.java") .addHeader("Accept", "application/json; q=0.5") .addHeader("Accept", "application/vnd.github.v3+json") .build(); //第一種 Response response = client.newCall(request).execute(); // 第二種 client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Request request, Throwable throwable) { } @Override public void onResponse(Response response) throws IOException { } }
Retrofit 的方式:
interface GitHubService { @GET("/repos/{owner}/{repo}/contributors") Call<List<Contributor>> repoContributors( @Path("owner") String owner, @Path("repo") String repo); } Call<List<Contributor>> call = gitHubService.repoContributors("square", "retrofit"); response = call.execute();
上面的方式適用於野生的返回網路請求的內容。
2. 使用事件匯流排(Otto,EventBus,RxBus(自己使用PublishSubject封裝))
程式碼來源:https://github.com/saulmm/Material-Movies
public interface MovieDatabaseAPI { /************Retrofit 1.x ,使用非同步的方式返回 ****************/ @GET("/movie/popular") void getPopularMovies( @Query("api_key") String apiKey, Callback<MoviesWrapper> callback); @GET("/movie/{id}") void getMovieDetail ( @Query("api_key") String apiKey, @Path("id") String id, Callback<MovieDetail> callback ); @GET("/movie/popular") void getPopularMoviesByPage( @Query("api_key") String apiKey, @Query("page") String page, Callback<MoviesWrapper> callback ); @GET("/configuration") void getConfiguration ( @Query("api_key") String apiKey, Callback<ConfigurationResponse> response ); @GET("/movie/{id}/reviews") void getReviews ( @Query("api_key") String apiKey, @Path("id") String id, Callback<ReviewsWrapper> response ); @GET("/movie/{id}/images") void getImages ( @Query("api_key") String apiKey, @Path("id") String movieId, Callback<ImagesWrapper> response ); }
public class RestMovieSource implements RestDataSource { private final MovieDatabaseAPI moviesDBApi; private final Bus bus; /***********使用了Otto**************/ public RestMovieSource(Bus bus) { RestAdapter movieAPIRest = new RestAdapter.Builder() /*** Retrofit 1.x ***/ .setEndpoint(Constants.MOVIE_DB_HOST) .setLogLevel(RestAdapter.LogLevel.HEADERS_AND_ARGS) .build(); moviesDBApi = movieAPIRest.create(MovieDatabaseAPI.class); this.bus = bus; } @Override public void getMovies() { moviesDBApi.getPopularMovies(Constants.API_KEY, retrofitCallback); } @Override public void getDetailMovie(String id) { moviesDBApi.getMovieDetail(Constants.API_KEY, id, retrofitCallback); } @Override public void getReviews(String id) { moviesDBApi.getReviews(Constants.API_KEY, id, retrofitCallback); } @Override public void getConfiguration() { moviesDBApi.getConfiguration(Constants.API_KEY, retrofitCallback); } @Override public void getImages(String movieId) { moviesDBApi.getImages(Constants.API_KEY, movieId, retrofitCallback); } public Callback retrofitCallback = new Callback() { /******************這裡統一的Callback,根據不同的返回值使用事件匯流排進行返回**************************/ @Override public void success(Object o, Response response) { if (o instanceof MovieDetail) { MovieDetail detailResponse = (MovieDetail) o; bus.post(detailResponse); } else if (o instanceof MoviesWrapper) { MoviesWrapper moviesApiResponse = (MoviesWrapper) o; bus.post(moviesApiResponse); } else if (o instanceof ConfigurationResponse) { ConfigurationResponse configurationResponse = (ConfigurationResponse) o; bus.post(configurationResponse); } else if (o instanceof ReviewsWrapper) { ReviewsWrapper reviewsWrapper = (ReviewsWrapper) o; bus.post(reviewsWrapper); } else if (o instanceof ImagesWrapper) { ImagesWrapper imagesWrapper = (ImagesWrapper) o; bus.post(imagesWrapper); } } @Override public void failure(RetrofitError error) { System.out.printf("[DEBUG] RestMovieSource failure - " + error.getMessage()); } }; @Override public void getMoviesByPage(int page) { moviesDBApi.getPopularMoviesByPage( Constants.API_KEY, page + "", retrofitCallback ); } }
3. 返回Observable(這裡也可以考慮直接返回Observable 和間接返回Observable)
直接的返回 Observable,在建立 apiService 的時候使用 Retrofit.create(MovieDatabaseAPI)就行了(見下面程式碼)
public interface MovieDatabaseAPI { @GET("/movie/popular") Observable<MovieWrapper> getPopularMovies( @Query("api_key") String apiKey, ); @GET("/movie/{id}") Observable<MovideDetail> getMovieDetail ( @Query("api_key") String apiKey, @Path("id") String id, ); }
間接返回Observable,這裡參考了AndroidCleanArchitecture:
public interface RestApi { /************定義API介面*****************/ String API_BASE_URL = "http://www.android10.org/myapi/"; /** Api url for getting all users */ String API_URL_GET_USER_LIST = API_BASE_URL + "users.json"; /** Api url for getting a user profile: Remember to concatenate id + 'json' */ String API_URL_GET_USER_DETAILS = API_BASE_URL + "user_"; /** * Retrieves an {@link rx.Observable} which will emit a List of {@link UserEntity}. */ Observable<List<UserEntity>> userEntityList(); /** * Retrieves an {@link rx.Observable} which will emit a {@link UserEntity}. * * @param userId The user id used to get user data. */ Observable<UserEntity> userEntityById(final int userId); }
/**** 使用Rx Observable 實現 RestApi 介面,實際呼叫的是 ApiConnection 裡面的方法 ****/ public class RestApiImpl implements RestApi { /***注意這裡沒有使用Retrofit,而是對上面介面的實現***/ private final Context context; private final UserEntityJsonMapper userEntityJsonMapper; /** * Constructor of the class * * @param context {@link android.content.Context}. * @param userEntityJsonMapper {@link UserEntityJsonMapper}. */ public RestApiImpl(Context context, UserEntityJsonMapper userEntityJsonMapper) { if (context == null || userEntityJsonMapper == null) { throw new IllegalArgumentException("The constructor parameters cannot be null!!!"); } this.context = context.getApplicationContext(); this.userEntityJsonMapper = userEntityJsonMapper; } @RxLogObservable(SCHEDULERS) @Override public Observable<List<UserEntity>> userEntityList() { return Observable.create(subscriber -> { if (isThereInternetConnection()) { try { String responseUserEntities = getUserEntitiesFromApi(); if (responseUserEntities != null) { subscriber.onNext(userEntityJsonMapper.transformUserEntityCollection( responseUserEntities)); subscriber.onCompleted(); } else { subscriber.onError(new NetworkConnectionException()); } } catch (Exception e) { subscriber.onError(new NetworkConnectionException(e.getCause())); } } else { subscriber.onError(new NetworkConnectionException()); } }); } @RxLogObservable(SCHEDULERS) @Override public Observable<UserEntity> userEntityById(final int userId) { return Observable.create(subscriber -> { if (isThereInternetConnection()) { try { String responseUserDetails = getUserDetailsFromApi(userId); if (responseUserDetails != null) { subscriber.onNext(userEntityJsonMapper.transformUserEntity(responseUserDetails)); subscriber.onCompleted(); } else { subscriber.onError(new NetworkConnectionException()); } } catch (Exception e) { subscriber.onError(new NetworkConnectionException(e.getCause())); } } else { subscriber.onError(new NetworkConnectionException()); } }); } private String getUserEntitiesFromApi() throws MalformedURLException { return ApiConnection.createGET(RestApi.API_URL_GET_USER_LIST).requestSyncCall(); } private String getUserDetailsFromApi(int userId) throws MalformedURLException { String apiUrl = RestApi.API_URL_GET_USER_DETAILS + userId + ".json"; return ApiConnection.createGET(apiUrl).requestSyncCall(); } /** * Checks if the device has any active internet connection. * * @return true device with internet connection, otherwise false. */ private boolean isThereInternetConnection() { boolean isConnected; ConnectivityManager connectivityManager = (ConnectivityManager) this.context.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); isConnected = (networkInfo != null && networkInfo.isConnectedOrConnecting()); return isConnected; } }
public class ApiConnection implements Callable<String> { /***********************網路介面的實際實現********************************/ private static final String CONTENT_TYPE_LABEL = "Content-Type"; private static final String CONTENT_TYPE_VALUE_JSON = "application/json; charset=utf-8"; private URL url; private String response; private ApiConnection(String url) throws MalformedURLException { this.url = new URL(url); } public static ApiConnection createGET(String url) throws MalformedURLException { return new ApiConnection(url); } /** * Do a request to an api synchronously. * It should not be executed in the main thread of the application. * * @return A string response */ @Nullable public String requestSyncCall() { connectToApi(); return response; } private void connectToApi() { OkHttpClient okHttpClient = this.createClient(); /*******************使用OKhttp的實現*******************/ final Request request = new Request.Builder() .url(this.url) .addHeader(CONTENT_TYPE_LABEL, CONTENT_TYPE_VALUE_JSON) .get() .build(); try { this.response = okHttpClient.newCall(request).execute().body().string(); } catch (IOException e) { e.printStackTrace(); } } private OkHttpClient createClient() { final OkHttpClient okHttpClient = new OkHttpClient(); okHttpClient.setReadTimeout(10000, TimeUnit.MILLISECONDS); okHttpClient.setConnectTimeout(15000, TimeUnit.MILLISECONDS); return okHttpClient; } @Override public String call() throws Exception { return requestSyncCall(); } }
這裡簡單總結了一下OkHttp和Retrofit該如何封裝,這樣的封裝放在整個大的程式碼框架中具有很好的模組化效果。對於使用MVP架構或者類似架構的APP,良好的網路介面模組封裝是非常重要的。