為什么使用Retrofit?
我在《如何使用Retrofit請求非Restful API》 前言 提到過HttpClient、OkHttp、Retrofit歷史。Retrofit無縫銜接RxJava,請求結果解析都不需要手動寫,用Gson處理json->Object
。
總的來說,就是
- 更清晰的結構
- 更簡潔的代碼
拒絕Retrofit的心態
- 舊代碼不好改
- 擔心Retrofit不能滿足需求
- 自己寫的代碼比Retrofit屌
1)
我猜大部分人都是第一種情況。項目做到一定程度,http層邏輯跟別的類耦合了,換Retrofit要大動干戈,老板不管你代碼重構有的沒的,項目催得緊......
2)
第二種情況,也是很頭疼的問題。很多公司不一定遵循Restful Api準則寫接口,新手對如何修改Retrofit代碼無從入手。嘆息一下,還是用回舊代碼吧。
對于這種情況,希望《如何使用Retrofit請求非Restful API》 對你有幫助!
3)
不用猶豫,拿磚頭拍死那個同事......
Retrofit官網例子:
http://square.github.io/retrofit/
Retrofit turns your HTTP API into a Java interface.
public interface GitHubService {
@GET("users/{user}/repos")
Call<List<Repo>> listRepos(@Path("user") String user);
}
The Retrofit class generates an implementation of the GitHubService interface.
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.build();
GitHubService service = retrofit.create(GitHubService.class);
Each Call from the created GitHubService can make a synchronous or asynchronous HTTP request to the remote webserver.
Call<List<Repo>> repos = service.listRepos("octocat");
即使從來未接觸Retrofit的人,一看GitHubService
listRepos
方法,就了解這個api大概意思,url是什么,傳什么參數。
思路:代理舊http代碼
我們目的是寫到好像Retrofit那樣。Retrofit本身就運用了代理模式隱藏了請求的細節,因此,我們就模仿它寫一個自己的代理層。
舊http使用方式
public loadUser(String value, Callback callback) {
new Thread(new Runnerable(){
@Override
public void run(){
MyHttpClient httpClient = new MyHttpClient();
Map<String, Object> params = new HashMap<>();
params.put("key", value);
String json = httpClient.get("user/kkmike999.json", params);
User user = new Gson().fromJson(json, User.class);
if (callback != null) {
callback.onLoaded(user);
}
}
}).start();
}
MyHttpClient
public class MyHttpClient {
public String get(String path, Map<String, Object> params) throws IOException {
OkHttpClient client = new OkHttpClient();
// "http://kkmike999-file.b0.upaiyun.com/" + path
HttpUrl.Builder urlBuilder = new HttpUrl.Builder().scheme("http")
.host("kkmike999-file.b0.upaiyun.com")
.addPathSegment(path);
for (String key : params.keySet()) {
urlBuilder.addQueryParameter(key, params.get(key)
.toString());
}
HttpUrl httpUrl = urlBuilder.build();
Request request = new Request.Builder().url(httpUrl)
.build();
Response response = client.newCall(request)
.execute();
return response.body()
.string();
}
}
寫代理層
大概就是這幾個類
MyRetrofit
public class MyRetrofit {
public <T> T create(Class<T> clazz) {
if (!clazz.isInterface()) {
throw new RuntimeException("Service not interface");
}
return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, new ServiceProxy());
}
}
ServiceProxy
public class ServiceProxy implements InvocationHandler {
@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);
}
ServiceMethod serviceMethod = new ServiceMethod(method);
Converter converter = new Converter(new Gson(), serviceMethod.returnType);
HttpCall call = new HttpCall(serviceMethod, converter, args);
return call.request();
}
}
HttpCall
public class HttpCall<T> {
MyHttpClient client;
Converter converter;
ServiceMethod serviceMethod;
Object[] args;
public HttpCall(ServiceMethod serviceMethod, Converter converter, Object[] args) {
this.client = new MyHttpClient();
this.converter = converter;
this.serviceMethod = serviceMethod;
this.args = args;
}
public T request() {
try {
// 參數count與注釋count不一致, 拋錯
int argumentCount = args != null ? args.length : 0;
if (argumentCount != serviceMethod.argumentTypes.length) {
throw new IllegalArgumentException("Argument count (" + argumentCount + ") doesn't match expected count (" + serviceMethod.argumentTypes.length + ")");
}
// 參數
Map<String, Object> params = new HashMap<>();
for (int p = 0; p < argumentCount; p++) {
params.put(serviceMethod.getQueryKey(p), args[p].toString());
}
String url = serviceMethod.url();
String json = client.get(url, params);
return (T) converter.convert(json);
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e.getMessage());// 請求失敗
}
}
}
ServiceMethod:
protected Method method;
protected Annotation[] methodAnnotations;
protected Annotation[][] argumentAnnotations;
protected Class[] argumentTypes;
protected Type returnType;
public ServiceMethod(Method method) {
this.method = method;
methodAnnotations = method.getDeclaredAnnotations();
argumentAnnotations = method.getParameterAnnotations();
argumentTypes = method.getParameterTypes();
returnType = method.getGenericReturnType();
}
/**
* 從@Query注釋,獲取請求參數的key
*/
public String getQueryKey(int index) {
for (Annotation annotation : argumentAnnotations[index]) {
if (annotation instanceof Query) {
return ((Query) annotation).value();
}
}
return "";
}
/**
* 從@Get注釋中,獲取url
*/
public String url() {
for (Annotation annotation : methodAnnotations) {
if (annotation instanceof Get) {
return ((Get) annotation).value();
}
}
throw new RuntimeException("no GET or POST annotation");
}
Converter
public class Converter<T> {
TypeAdapter adapter;
public Converter(Gson gson, Type type) {
adapter = gson.getAdapter(TypeToken.get(type));
}
T convert(String json) throws IOException {
return (T) adapter.fromJson(json);
}
}
最好Converter就抽象成一個接口,你不喜歡用Gson,用fastjson,可以隨時切換。
@GET
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Get {
String value() default "";
}
@Query
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Query {
String value() default "";
}
新Api使用方式
json數據:
{
"uid":1,
"name":"kkmike999"
}
UserApi
public interface UserApi {
@Get("user/kkmike999.json")
User loadUser(@Query("key") String value);
}
public void loadUser(String value, Callback callback) {
new Thread(new Runnerable(){
@Override
public void run(){
UserApi userApi = new MyRetrofit().create(UserApi.class);
User user = userApi.loadUser("test");
if (callback != null) {
callback.onLoaded(user);
}
}
}).start();
}
調用方式,99%像Retrofit,實際底層http,還是舊代碼。
配合RxJava
現在很多開發者都離不開RxJava了。“Retrofit+RxJava簡直無敵” 這種想法恐怕不僅僅是我有吧?
你的UserApi
應該是這樣的:
public interface UserApi {
@Get("user/kkmike999.json")
Observable<User> loadUserO(@Query("key") String value);
}
然后,只需要:
新增接口CallAdapter
public interface CallAdapter<T> {
T adapt(HttpCall call);
}
新增RxCallAdapter
public class RxCallAdapter<T> implements CallAdapter<Observable<T>>{
@Override
public Observable<T> adapt(final HttpCall call) {
return Observable
.create(new Observable.OnSubscribe<T>() {
@Override
public void call(Subscriber<? super T> subscriber) {
try {
subscriber.onNext((T) call.request());
subscriber.onCompleted();
} catch (Exception e) {
subscriber.onError(e);
}
}
})
.subscribeOn(Schedulers.io());
}
}
修改ServiceProxy
public class ServiceProxy implements InvocationHandler {
@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);
}
ServiceMethod serviceMethod = new ServiceMethod(method);
Converter converter = new Converter(new Gson(), serviceMethod.returnType);
CallAdapter adapter = new RxCallAdapter();
HttpCall call = new HttpCall(serviceMethod, converter, args);
return adapter.adapt(call);
}
}
修改ServiceMethod
public class ServiceMethod{
......
public ServiceMethod(Method method) {
...
returnType = method.getGenericReturnType();
if (returnType instanceof ParameterizedType) {
// Obserable<?> 里面的type
ParameterizedType parameterizedType = (ParameterizedType) returnType;
returnType = parameterizedType.getActualTypeArguments()[0];
}
}
......
}
RxJava Api調用方式
public void loadUser(String value, Callback callback){
userApi.loadUserO("test")
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Action1<User>() {
@Override
public void call(User user) {
if (callback != null) {
callback.onLoaded(user);
}
}
});
}
跟Retrofit使用方式一模一樣了!
依賴關系:
單元測試
使用Retrofit最終目的,就是讓我們更好地單元測試!
上面的代碼,都沒提到UserPresenter
,這是擔心代碼量太多。懂MVP的同學應該一看就明白。
public class UserPresenter {
UserApi userApi;
UserView userView;
public void loadUser(String value){...}
}
public class UserPresenterTest {
@Mock
UserApi userApi;
@Mock
UserView userView;
@Captor
ArgumentCaptor<User> captor;
UserPresenter userPresenter;
@Before
public void setUp() throws Exception {
RxJavaPlugins.getInstance()
.registerSchedulersHook(new RxJavaSchedulersHook() {
@Override
public Scheduler getIOScheduler() {
// io scheduler會新建線程,把io scheduler->immediate scheduler, 異步->同步
return Schedulers.immediate();
}
});
MockitoAnnotations.initMocks(this);
userPresenter = new UserPresenter(userApi, userView);
}
@Test
public void loadUser() throws InterruptedException {
User user = new User();
user.uid = 1;
user.name = "kkmike999";
when(userApi.loadUser("test")).thenReturn(Observable.just(user));
userPresenter.loadUser("test");
verify(userView).onUserLoaded(captor.capture());
User result = captor.getValue();
Assert.assertEquals(result.uid, 1);
Assert.assertEquals(result.name, "kkmike999");
}
}
改進
其實ServiceProxy
可以寫得更解耦,例如Converter
、CallAdapter
都從外面傳進來。
public class MyRetrofit {
......
public Converter converter(Type returnType) {
return new GsonConverter(new Gson(), returnType);
}
public CallAdapter callAdapter() {
return new RxCallAdapter();
}
}
可以參考Retrofit,MyRetrofit
用Builder模式構建會更好,此處就不累贅了。
public class ServiceProxy implements InvocationHandler {
MyRetrofit retrofit;
public ServiceProxy(MyRetrofit retrofit) {
this.retrofit = retrofit;
}
@Override
public Object invoke(Object proxy, Method method, Object... args) throws Throwable {
....
Converter converter = retrofit.converter(serviceMethod.actualType);
CallAdapter adapter = retrofit.callAdapter();
HttpCall call = new HttpCall(client, serviceMethod, converter, args);
return adapter.adapt(call);
}
}
Java Demo:https://git.oschina.net/kkmike999/ImitationRetrofit.git
結語
無論你的同事出于什么理由,不想使用Retrofit,遷就一下也無妨,除非原來的代碼真的一團糟。也許他只是未研究Retrofit,不想冒無謂的風險。
放下磚頭,立地成佛
當你自己寫一套代理,就可以享受Retrofit的美妙設計模式,同時也不違背原來代碼(如果同事連你寫代理都不贊同,拿磚頭拍死他)。這樣我們就能更好地單元測試。(可能你同事還沒認識到單元測試重要性)當他看到你行云流水的代碼,或許某天他就用你寫的這個代理了。
關于MPV、Retrofit、單元測試,請參考《(MVP+RxJava+Retrofit)解耦+Mockito單元測試 經驗分享》
關于作者
我是鍵盤男。
在廣州生活,在創業公司上班,猥瑣文藝碼農。喜歡科學、歷史,玩玩投資,偶爾獨自旅行。希望成為獨當一面的工程師。