淺談Android開發(fā)系列網(wǎng)絡篇之Retrofit
Retrofit是一個不錯的網(wǎng)絡請求庫,用官方自己的介紹就是:
A type-safe REST client for Android and Java
看官網(wǎng)的介紹用起來很省事,不過如果不了解它是怎么實現(xiàn)的也不太敢用,不然出問題了就不知道怎么辦了。這幾天比較閑就下下來看了一下,了解一下大概實現(xiàn)方法,細節(jié)就不追究了。先來看一個官網(wǎng)的例子,詳細說明去網(wǎng)官看
簡單示例
首先定義請求接口,即程序中都需要什么請求操作
public interface GitHubService {
@GET("/users/{user}/repos")
List<Repo> listRepos(@Path("user") String user);
}
然后通過RestAdapter生成一個剛才定義的接口的實現(xiàn)類,使用的是動態(tài)代理。
RestAdapter restAdapter = new RestAdapter.Builder()
.setEndpoint("https://api.github.com")
.build();
GitHubService service = restAdapter.create(GitHubService.class);
現(xiàn)在就可以調用接口進行請求了
List<Repo> repos = service.listRepos("octocat");
使用就是這么簡單,請求時直接調用接口就行了,甚至不用封裝參數(shù),因為參數(shù)的信息已經(jīng)在定義接口時通過Annotation定義好了。
從上面的例子可以看到接口直接返回了需要的Java類型,而不是byte[]或String,解析數(shù)據(jù)的地方就是Converter,這個是可以自定義的,默認是用Gson解析,也就是說默認認為服務器返回的是Json數(shù)據(jù),可以通過指定不同的Convert使用不同的解析方法,如用Jackson解析Json,或自定義XmlConvert解析xml數(shù)據(jù)。
Retrofit的使用就是以下幾步:
1.定義接口,參數(shù)聲明,Url都通過Annotation指定
2.通過RestAdapter生成一個接口的實現(xiàn)類(動態(tài)代理)
3.調用接口請求數(shù)據(jù)
接口的定義要用用Rtrofit定義的一些Annotation,所以先看一下Annotation的。
Annotation
以上面的示例中的接口來看
@GET("/group/{id}/users")
List<User> groupList(@Path("id") int groupId);
先看@GET
/** Make a GET request to a REST path relative to base URL. */
@Documented
@Target(METHOD)
@Retention(RUNTIME)
@RestMethod("GET")
public @interface GET {
String value();
}
@GET本身也被幾個Anotation注解,@Target表示@GET注解是用于方法的,value方法就返回這個注解的value值,在上例中就是/group/{id}/users,然后就是@RestMethod
@Documented
@Target(ANNOTATION_TYPE)
@Retention(RUNTIME)
public @interface RestMethod {
String value();
boolean hasBody() default false;
}
RestMethod是一個用于Annotation的Annotation,比如上面的例子中用來注解的@GET,value方法就返回GET,hasBody表示是否有Body,對于POST這個方法就返回true
@Documented
@Target(METHOD)
@Retention(RUNTIME)
@RestMethod(value = "POST", hasBody = true)
public @interface POST {
String value();
}
Retrofit的Annotation包含請求方法相關的@GET、@POST、@HEAD、@PUT、@DELETA、@PATCH,和參數(shù)相關的@Path、@Field、@Multipart等。
定義了Annotation要就有解析它的方法,在Retrofit中解析的位置就是RestMethodInfo,但在這之前需要先看哪里使用了RestMethodInfo,前面說了Retrofit使用了動態(tài)代理生成了我們定義的接口的實現(xiàn)類,而這個實現(xiàn)類是通過RestAdapter.create返回的,所以使用動態(tài)代理的位置就是RestAdapter,接下來就看一下RestAdapter。
RestAdapter
RestAdapter restAdapter = new RestAdapter.Builder()
.setEndpoint("https://api.github.com")
.build();
GitHubService service = restAdapter.create(GitHubService.class);
public RestAdapter build() {
if (endpoint == null) {
throw new IllegalArgumentException("Endpoint may not be null.");
}
ensureSaneDefaults();
return new RestAdapter(endpoint, clientProvider, httpExecutor, callbackExecutor,
requestInterceptor, converter, profiler, errorHandler, log, logLevel);
}
setEndPoint就不說了,接口中定義的都是相對Url,EndPoint就是域名,build方法調用ensureSaneDefaults()方法,然后就構造了一個RestAdapter對象,構造函數(shù)的參數(shù)中傳入了EndPoint外的幾個對象,這幾個對象就是在ensureSaneDefaults()中初始化的。
private void ensureSaneDefaults() {
if (converter == null) { converter = Platform.get().defaultConverter(); }
if (clientProvider == null) { clientProvider = Platform.get().defaultClient(); }
if (httpExecutor == null) { httpExecutor = Platform.get().defaultHttpExecutor(); }
if (callbackExecutor == null) { callbackExecutor = Platform.get().defaultCallbackExecutor(); }
if (errorHandler == null) { errorHandler = ErrorHandler.DEFAULT; }
if (log == null) { log = Platform.get().defaultLog(); }
if (requestInterceptor == null) { requestInterceptor = RequestInterceptor.NONE; }
}
ensureSaneDefaults()中初始化了很多成員,errorHandler、log就不看了,其他的除了requestInterceptor都是通過Platform對象獲得的,所以要先看下Platform
Platform
private static final Platform PLATFORM = findPlatform();
static final boolean HAS_RX_JAVA = hasRxJavaOnClasspath();
static Platform get() {
return PLATFORM;
}
private static Platform findPlatform() {
try {
Class.forName("android.os.Build");
if (Build.VERSION.SDK_INT != 0) {
return new Android();
}
} catch (ClassNotFoundException ignored) {
}
if (System.getProperty("com.google.appengine.runtime.version") != null) {
return new AppEngine();
}
return new Base();
}
使用了單例的PLATFORM,通過findPlatform()初始化實例,如果是Android平臺就使用Platform.Android,如果是Google AppEngine就使用Platform.AppEngine,否則使用Platform.Base,這些都是Platform的子類,其中AppEngine又是Base的子類。
Platform是一個抽象類,定義了以下幾個抽象方法,這幾個方法的作用就是返回一些RestAdapter中需要要用到成員的默認實現(xiàn)
abstract Converter defaultConverter(); // 默認的Converter,用于將請求結果轉化成需要的數(shù)據(jù),如GsonConverter將JSON請求結果用Gson解析成Java對象 abstract Client.Provider defaultClient(); // Http請求類,如果是AppEngine就使用`UrlFetchClient`,否則如果有OKHttp就使用OKHttp,如果是Android,2.3以后使用HttpURLConnection,2.3以前使用HttpClient abstract Executor defaultHttpExecutor(); // 用于執(zhí)行Http請求的Executor abstract Executor defaultCallbackExecutor(); // Callback調用中用于執(zhí)行Callback的Executor(可能是同步的) abstract RestAdapter.Log defaultLog(); // Log接口,用于輸出Log
看完Platform的接口再看ensureSaneDefaults就清楚了,初始化轉化數(shù)據(jù)的Converter、執(zhí)行請求的Client、執(zhí)行請求的Executor、執(zhí)行Callback的Executor、Log輸出類、錯誤處理類和用于在請求前添加額外處理的攔截請求的Interceptor。
Converter默認都是用的GsonConverter,就不看了,defaultClient返回執(zhí)行網(wǎng)絡請求的Client
Platform.Android
@Override Client.Provider defaultClient() {
final Client client;
if (hasOkHttpOnClasspath()) {
client = OkClientInstantiator.instantiate();
} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) {
client = new AndroidApacheClient();
} else {
client = new UrlConnectionClient();
}
return new Client.Provider() {
@Override public Client get() {
return client;
}
};
}
Platform.Base
@Override Client.Provider defaultClient() {
final Client client;
if (hasOkHttpOnClasspath()) {
client = OkClientInstantiator.instantiate();
} else {
client = new UrlConnectionClient();
}
return new Client.Provider() {
@Override public Client get() {
return client;
}
};
}
Platform.AppEngine
@Override Client.Provider defaultClient() {
final UrlFetchClient client = new UrlFetchClient();
return new Client.Provider() {
@Override public Client get() {
return client;
}
};
}
對于Android,優(yōu)先使用OKHttp,否則2.3以后使用HttpUrlConnection,2.3以前使用HttpClient
defaultHttpExecutor就是返回一個Executor,執(zhí)行請求的線程在這個Executor中執(zhí)行,就做了一件事,把線程設置為后臺線程
defaultCallbackExecutor用于執(zhí)行Callback類型的請求時,提供一個Executor執(zhí)行Callback的Runnable
Platform.Base
@Override Executor defaultCallbackExecutor() {
return new Utils.SynchronousExecutor();
}
Platform.Android
@Override Executor defaultCallbackExecutor() {
return new MainThreadExecutor();
}
SynchronousExecutor
static class SynchronousExecutor implements Executor {
@Override public void execute(Runnable runnable) {
runnable.run();
}
}
MainThreadExecutor
public final class MainThreadExecutor implements Executor {
private final Handler handler = new Handler(Looper.getMainLooper());
@Override public void execute(Runnable r) {
handler.post(r);
}
}
如果是Android,通過Handler將回調發(fā)送到主線程執(zhí)行,如果非Android,直接同步執(zhí)行。
Platform看完了,RestAdapter的成員初始化完成,就要看怎么通過RestAdapter.create生成我們定義的接口的實現(xiàn)類了
RestAdapter.create
public <T> T create(Class<T> service) {
Utils.validateServiceClass(service);
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
new RestHandler(getMethodInfoCache(service)));
}
Map<Method, RestMethodInfo> getMethodInfoCache(Class<?> service) {
synchronized (serviceMethodInfoCache) {
Map<Method, RestMethodInfo> methodInfoCache = serviceMethodInfoCache.get(service);
if (methodInfoCache == null) {
methodInfoCache = new LinkedHashMap<Method, RestMethodInfo>();
serviceMethodInfoCache.put(service, methodInfoCache);
}
return methodInfoCache;
}
}
使用了動態(tài)代理,InvocationHandler是RestHandler,RestHandler有一個參數(shù),是Method->RestMethodInfo的映射,初始化時這個映射是空的。重點就是這兩個了:RestHandler,RestMethodInfo,
@Override public Object invoke(Object proxy, Method method, final Object[] args)
throws Throwable {
// If the method is a method from Object then defer to normal invocation.
if (method.getDeclaringClass() == Object.class) { // 1
return method.invoke(this, args);
}
// Load or create the details cache for the current method.
final RestMethodInfo methodInfo = getMethodInfo(methodDetailsCache, method); // 2
if (methodInfo.isSynchronous) { // 3
try {
return invokeRequest(requestInterceptor, methodInfo, args);
} catch (RetrofitError error) {
Throwable newError = errorHandler.handleError(error);
if (newError == null) {
throw new IllegalStateException("Error handler returned null for wrapped exception.",
error);
}
throw newError;
}
}
if (httpExecutor == null || callbackExecutor == null) {
throw new IllegalStateException("Asynchronous invocation requires calling setExecutors.");
}
// Apply the interceptor synchronously, recording the interception so we can replay it later.
// This way we still defer argument serialization to the background thread.
final RequestInterceptorTape interceptorTape = new RequestInterceptorTape();
requestInterceptor.intercept(interceptorTape); // 4
if (methodInfo.isObservable) { // 5
if (rxSupport == null) {
if (Platform.HAS_RX_JAVA) {
rxSupport = new RxSupport(httpExecutor, errorHandler);
} else {
throw new IllegalStateException("Observable method found but no RxJava on classpath");
}
}
return rxSupport.createRequestObservable(new Callable<ResponseWrapper>() {
@Override public ResponseWrapper call() throws Exception {
return (ResponseWrapper) invokeRequest(interceptorTape, methodInfo, args);
}
});
}
Callback<?> callback = (Callback<?>) args[args.length - 1]; // 6
httpExecutor.execute(new CallbackRunnable(callback, callbackExecutor, errorHandler) {
@Override public ResponseWrapper obtainResponse() {
return (ResponseWrapper) invokeRequest(interceptorTape, methodInfo, args);
}
});
return null; // Asynchronous methods should have return type of void.
}
執(zhí)行請求時會調用RestHandler的invoke方法,如上所示,主要是上面代碼中標注有6點
1.如果調用的是Object的方法,不做處理直接調用。
2.通過getMethodInfo獲取調用的Method對應的RestMethodInfo,前面說了,構造RestHandler對象時傳進來了一個Method->RestMethodInfo的映射,初始時是空的。
static RestMethodInfo getMethodInfo(Map<Method, RestMethodInfo> cache, Method method) {
synchronized (cache) {
RestMethodInfo methodInfo = cache.get(method);
if (methodInfo == null) {
methodInfo = new RestMethodInfo(method);
cache.put(method, methodInfo);
}
return methodInfo;
}
在getMethodInfo中判斷如果相應的映射不存在,就建立這個映射,并如名字所示緩存起來
3. 如果是同步調用(接口中直接返回數(shù)據(jù),不通過Callback或Observe),直接調用invokeRequest
4. 如果是非同步調用,先通過RequestInterceptorTape記錄攔截請求,記錄后在后臺線程做實際攔截,后面會提到。
5. 如果是Observe請求(RxJava),執(zhí)行第5步,對RxJava不了解,略過
6. 如果是Callback形式,交由線程池執(zhí)行
接口中的每一個Method有一個對應的RestMethodInfo,關于接口中Annotation信息的處理就都在這里了
RestMethodInfo
private enum ResponseType {
VOID,
OBSERVABLE,
OBJECT
}
RestMethodInfo(Method method) {
this.method = method;
responseType = parseResponseType();
isSynchronous = (responseType == ResponseType.OBJECT);
isObservable = (responseType == ResponseType.OBSERVABLE);
}
在構造函數(shù)中調用了parseResponseType,parseResponseType解析了方法簽名,根據(jù)方法的返回值類型及最后一個參數(shù)的類型判斷方法的類型是哪種ResponseType
無論是哪種ResponseType,最終都是調用invokeRequest執(zhí)行實際的請求,接下來依次看下invokeRequest的執(zhí)行步驟
RestAdapter.invokeRequest
第一步是調用methodInfo.init()解析調用的方法,方法里有做判斷,只在第一次調用時解析,因為處一次解析后這個對象就被緩存起來了,下次調同一個方法時可以直接使用
synchronized void init() {
if (loaded) return;
parseMethodAnnotations();
parseParameters();
loaded = true;
}
在RestMethodInfo.init中分別調用
- parseMethodAnnotations():解析所有方法的Annotation
- parseParameters():解析所有參數(shù)的Annotation
for (Annotation methodAnnotation : method.getAnnotations()) {
Class<? extends Annotation> annotationType = methodAnnotation.annotationType();
RestMethod methodInfo = null;
// Look for a @RestMethod annotation on the parameter annotation indicating request method.
for (Annotation innerAnnotation : annotationType.getAnnotations()) {
if (RestMethod.class == innerAnnotation.annotationType()) {
methodInfo = (RestMethod) innerAnnotation;
break;
}
}
...
}
在parseMethodAnnotations中,會獲取方法所有的Annotation并遍歷:
- 對于每一個Annotation,也會獲取它的Annotation,看它是否是被RestMethod注解的Annotation,如果是,說明是@GET,@POST類型的注解,就調用parsePath解析請求的Url,requestParam(URL中問號后的內容)及Url中需要替換的參數(shù)名(Url中大括號括起來的部分)
- 尋找Headers Annotation解析Header參數(shù)
- 解析RequestType:SIMPLE,MULTIPART,F(xiàn)ORM_URL_ENCODED
parseParameters解析請求參數(shù),即參數(shù)的Annotation,@PATH、@HEADER、@FIELD等
第二步是RequestBuilder和Interceptor,這兩個是有關聯(lián)的,所以一起看。
RequestBuilder requestBuilder = new RequestBuilder(serverUrl, methodInfo, converter); requestBuilder.setArguments(args); requestInterceptor.intercept(requestBuilder); Request request = requestBuilder.build();
先說RequestInterceptor,作用很明顯,當執(zhí)行請求時攔截請求以做一些特殊處理,比如添加一些額外的請求參數(shù)。
/** Intercept every request before it is executed in order to add additional data. */
public interface RequestInterceptor {
/** Called for every request. Add data using methods on the supplied {@link RequestFacade}. */
void intercept(RequestFacade request);
interface RequestFacade {
void addHeader(String name, String value);
void addPathParam(String name, String value);
void addEncodedPathParam(String name, String value);
void addQueryParam(String name, String value);
void addEncodedQueryParam(String name, String value);
}
/** A {@link RequestInterceptor} which does no modification of requests. */
RequestInterceptor NONE = new RequestInterceptor() {
@Override public void intercept(RequestFacade request) {
// Do nothing.
}
};
}
RequestInterceptor只有一個方法intercept,接收一個RequestFacade參數(shù),RequestFacade是RequestInterceptor內部的一個接口,這個接口的方法就是添加請求參數(shù),Query、Header什么的。大概可以看出RequestInterceptor的作用了,如果RequestFacade表示一個請求相關的數(shù)據(jù),RequestInteceptor.intercept的作用就是向這個RequestFacade中添加額外Header,Param等參數(shù)。
RequestFacade的一個子類叫RequestBuilder,用來處理Request請求參數(shù),在invokeRequest中會對RequestBuilder調用intercept方法向RequestBuilder添加額外的參數(shù)。
有一個叫RequestInterceptorTape的類,同時實現(xiàn)了RequestFacade與RequestInterceptor,它的作用是:
- 當作為RequestFacade使用時作為參數(shù)傳給一個RequestInteceptor,這個RequestInterceptor調用它的addHeader等方法時,它把這些調用及參數(shù)記錄下來
- 然后作為RequestInterceptor使用時,將之前記錄的方法調用及參數(shù)重新應用到它的intercept參數(shù)RequestFacade中
在RestHandler.invoke中,如果判斷方法的調用不是同步調用,就通過下面的兩行代碼將用戶設置的interceptor需要添加的參數(shù)記錄到RequestInterceptorTape,然后在invokeRequest中再實際執(zhí)行參數(shù)的添加。
// Apply the interceptor synchronously, recording the interception so we can replay it later. // This way we still defer argument serialization to the background thread. final RequestInterceptorTape interceptorTape = new RequestInterceptorTape(); requestInterceptor.intercept(interceptorTape);
RequestBuilder.setArguments()解析調用接口時的實際參數(shù)。然后通過build()方法生成一個Request對象
第三步執(zhí)行請求,Response response = clientProvider.get().execute(request);
第四步就是解析并分發(fā)請求結果了,成功請求時返回結果,解析失敗調用ErrorHandler給用戶一個自定義異常的機會,但最終都是通過異常拋出到invoke()中的,如果是同步調用,直接拋異常,如果是Callback調用,會回調Callback.failure
CallbackRunnable
請求類型有同步請求,Callback請求,Observable請求,來看下Callback請求:
Callback<?> callback = (Callback<?>) args[args.length - 1];
httpExecutor.execute(new CallbackRunnable(callback, callbackExecutor, errorHandler) {
@Override public ResponseWrapper obtainResponse() {
return (ResponseWrapper) invokeRequest(interceptorTape, methodInfo, args);
}
});
Callback請求中函數(shù)最后一個參數(shù)是一個Callback的實例,httpExecutor是一個Executor,用于執(zhí)行Runnable請求,我們看到,這里new了一個CallbackRunnable執(zhí)行,并實現(xiàn)了它的obtainResponse方法,看實現(xiàn):
abstract class CallbackRunnable<T> implements Runnable {
private final Callback<T> callback;
private final Executor callbackExecutor;
private final ErrorHandler errorHandler;
CallbackRunnable(Callback<T> callback, Executor callbackExecutor, ErrorHandler errorHandler) {
this.callback = callback;
this.callbackExecutor = callbackExecutor;
this.errorHandler = errorHandler;
}
@SuppressWarnings("unchecked")
@Override public final void run() {
try {
final ResponseWrapper wrapper = obtainResponse();
callbackExecutor.execute(new Runnable() {
@Override public void run() {
callback.success((T) wrapper.responseBody, wrapper.response);
}
});
} catch (RetrofitError e) {
Throwable cause = errorHandler.handleError(e);
final RetrofitError handled = cause == e ? e : unexpectedError(e.getUrl(), cause);
callbackExecutor.execute(new Runnable() {
@Override public void run() {
callback.failure(handled);
}
});
}
}
public abstract ResponseWrapper obtainResponse();
}
就是一個普通的Runnable,在run方法中首先執(zhí)行obtailResponse,從名字可以看到是執(zhí)行請求返回Response,這個從前面可以看到執(zhí)行了invokeRequest,和同步調用中一樣執(zhí)行請求。
緊接著就提交了一個Runnable至callbackExecutor,在看Platform時看到了callbackExecotor是通過Platform.get().defaultCallbackExecutor()返回的,Android中是向主線程的一個Handler發(fā)消息
值得注意的事,對于同步調用,如果遇到錯誤是直接拋異常,而對于異步調用,是調用Callback.failure()
Mime
執(zhí)行網(wǎng)絡請求,需要向服務端發(fā)送請求參數(shù),如表單數(shù)據(jù),上傳的文件等,同樣需要解析服務端返回的數(shù)據(jù),在Retrofit中對這些做了封裝,位于Mime包中,也只有封裝了,才好統(tǒng)一由指定的Converter執(zhí)行數(shù)據(jù)的轉換
TypedInput和TypedOutput表示輸入輸出的數(shù)據(jù),都包含mimeType,并分別支持讀入一個InputStream或寫到一個OutputStrem
/**
* Binary data with an associated mime type.
*
* @author Jake Wharton (jw@squareup.com)
*/
public interface TypedInput {
/** Returns the mime type. */
String mimeType();
/** Length in bytes. Returns {@code -1} if length is unknown. */
long length();
/**
* Read bytes as stream. Unless otherwise specified, this method may only be called once. It is
* the responsibility of the caller to close the stream.
*/
InputStream in() throws IOException;
}
/**
* Binary data with an associated mime type.
*
* @author Bob Lee (bob@squareup.com)
*/
public interface TypedOutput {
/** Original filename.
*
* Used only for multipart requests, may be null. */
String fileName();
/** Returns the mime type. */
String mimeType();
/** Length in bytes or -1 if unknown. */
long length();
/** Writes these bytes to the given output stream. */
void writeTo(OutputStream out) throws IOException;
}
TypedByteArray,內部數(shù)據(jù)是一個Byte數(shù)組
private final byte[] bytes;
@Override public long length() {
return bytes.length;
}
@Override public void writeTo(OutputStream out) throws IOException {
out.write(bytes);
}
@Override public InputStream in() throws IOException {
return new ByteArrayInputStream(bytes);
}
TypedString,繼承自TypedByteArray,內部表示是一樣的
public TypedString(String string) {
super("text/plain; charset=UTF-8", convertToBytes(string));
}
private static byte[] convertToBytes(String string) {
try {
return string.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
其他的也一樣,從名字很好理解:TypedFile,MultipartTypedOutput,F(xiàn)ormEncodedTypedOutput。
其他
Retrofit對輸入和輸出做了封裝,通過TypedOutput向服務器發(fā)送數(shù)據(jù),通過TypedInput讀取服務器返回的數(shù)據(jù)。
通過MultipartTypedOutput支持文件上傳,讀取服務器數(shù)據(jù)時,如果要求直接返回未解析的Response,Restonse會被轉換為TypedByteArray,所以不能是大文件類的
Retrofit支持不同的Log等級,當為LogLevel.Full時會把Request及Response的Body打印出來,所以如果包含文件就不行了。
Retrofit默認使用GsonConverter,所以要想獲取原始數(shù)據(jù)不要Retrofit解析,要么自定義Conveter,要么直接返回Response了,返回Response也比較麻煩
總體來說Retrofit看起來很好用,不過要求服務端返回數(shù)據(jù)最好要規(guī)范,不然如果請求成功返回一種數(shù)據(jù)結構,請求失敗返回另一種數(shù)據(jù)結構,不好用Converter解析,接口的定義也不好定義,除非都返回Response,或自定義Converter所有接口都返回String
在Twitter上JakeWharton這么說:
Gearing up towards a Retrofit 1.6.0 release and then branching 1.x so we can push master towards a 2.0 and fix long-standing design issues.
要出2.0了,內部API會改,接口應該不怎么變
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
- Android使用Retrofit2.0技術仿微信發(fā)說說
- Android使用Retrofit仿微信多張圖片拍照上傳
- Android中okhttp3.4.1+retrofit2.1.0實現(xiàn)離線緩存
- Android中Retrofit+OkHttp進行HTTP網(wǎng)絡編程的使用指南
- Android中的Retrofit+OkHttp+RxJava緩存架構使用
- Android Retrofit 2.0框架上傳圖片解決方案
- 簡略分析Android的Retrofit應用開發(fā)框架源碼
- Android app開發(fā)中Retrofit框架的初步上手使用
- Android Retrofit文件下載進度顯示問題的解決方法
相關文章
Android FrameWork之Zygote啟動示例詳解
這篇文章主要為大家介紹了Android FrameWork之Zygote啟動示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-07-07
Android ListView實現(xiàn)無限循環(huán)滾動
這篇文章主要為大家詳細介紹了Android ListView實現(xiàn)無限循環(huán)滾動,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2020-06-06
Android自定義Chronometer實現(xiàn)短信驗證碼秒表倒計時功能
這篇文章主要介紹了Android自定義ChronometerView實現(xiàn)類似秒表倒計時,短信驗證碼倒計時功能,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-11-11
Flutter?LinearProgressIndicator使用指南分析
這篇文章主要為大家介紹了Flutter?LinearProgressIndicator使用指南分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-03-03

