[TOC]
# 概述
* Retrofit 對網絡請求接口進行了封裝,實際執行網絡請求的依然是 OkHttp
* Retrofit 接口層實際是對 OkHttp 中 Request 的封裝,采用注解的形式來描述網絡請求參數
* Retrofit采用動態代理模式創建請求接口對象,請求執行調用接口方法時,Retrofit會根據注解創建相應的Call對象,接下來使用OkHttp發起請求
# Retrofit 使用
在學習 Retrofit 使用之前我們先回顧下 OKHttp 的工作方式,一個標準的 OkHttp 請求格式為 `mClient.newCall(request).enqueue(callback);`。可以看到,首先構造出 Request 對象(經過封裝的 http 請求信息)和 OkHttpClient 對象,然后調用 OkHttpRequest 的 newCall 方法,傳入封裝的 request 對象后,就得到了一個 RealCall 對象,然后就可以操作 RealCall 對象執行請求、取消等一系列操作。
而 Retrofit 對于 http 請求信息并不是直接構造成 Request 對象,而是聲明為接口的形式,相關請求信息(地址、方法、請求頭等)使用注解和參數的形式傳入。同樣會有一個 Retrofit 客戶端對象,調用其 create 方法會幫我們創建出請求接口的實例對象,接著調用請求接口實例對象的方法發起請求。流程如下:
* 創建請求接口
* 構建 Retrofit 實例
* 創建網絡請求接口實例
* 發起網絡請求
可以看到,Retrofit 對于 OkHttp 中的 Request 進行了抽象化為接口,在使用時首先構造出請求對象。一個標準的 Retrofit 請求格式為 `mRetrofit.create(RequestService.class).doGet("params").enqueue(callback)`,其中 doGet 方法返回的是一個 Call 對象。
# Retrofit 注解使用
來源:[Retrofit2.0中注解使用套路](https://blog.csdn.net/stven_king/article/details/52372172)、[Retrofit網絡請求參數注解,@Path、@Query、@QueryMap](https://www.jianshu.com/p/7687365aa946)
## 靜態 url 請求
### GET 請求
```java
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.build();
public interface GitHubService {
//無參數
// https://api.github.com/users/xcy396/repos
@GET("users/xcy396/repos")
Call<List<Repo>> listRepos();
//少數參數
//https://api.github.com/users/xcy396/repos?time={time}
@GET("users/xcy396/repos")
Call<List<Repo>> listRepos(@Query("time") long time);
//參數較多
//https://api.github.com/users/xcy396/repos?time={time}&author={author}...
@GET("users/xcy396/repos")
Call<List<Repo>> listRepos(@QueryMap Map<String, String> params);
}
```
### POST 請求
```java
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.build();
public interface GitHubService {
//無參數
@POST("users/xcy396/repos")
Call<List<Repo>> listRepos();
//少數參數(參數拼接在url后面)
//https://api.github.com/users/xcy396/repos?time={time}
@POST("users/xcy396/repos")
Call<List<Repo>> listRepos(@Query("time") long time);
//少數參數(使用表單形式提交,參數在請求體)
@FormUrlEncoded
@POST("users/xcy396/repos")
Call<List<Repo>> listRepos(@Field("time") long time);
//參數較多(使用表單形式提交,參數在請求體)
@FormUrlEncoded
@POST("users/xcy396/repos")
Call<List<Repo>> listRepos(@FieldMap Map<String, String> params);
//參數較多(使用Plain形式提交,參數在請求體)
@POST("users/xcy396/repos")
Call<List<Repo>> listRepos(@Body Map<String, String> params);
}
```
### DELETE請求
```java
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.build();
public interface GitHubService {
//無參數
// https://api.github.com/users/xcy396/repos
@DELETE("users/xcy396/repos")
Call<List<Repo>> listRepos();
//少數參數(參數拼接在url后面)
//https://api.github.com/users/xcy396/repos?time={time}
@DELETE("users/xcy396/repos")
Call<List<Repo>> listRepos(@Query("time") long time);
//參數較多(使用Plain形式提交,參數在請求體)
//https://api.github.com/users/xcy396/repos
@HTTP(method = "DELETE", path = "users/xcy396/repos", hasBody = true)
Call<List<Repo>> listRepos(@Body Map<String, String> params);
}
```
注意:
* 當@GET或@POST注解的url為全路徑時(可能和baseUrl不是一個域),會直接使用注解的url的域
* 如果請求為 Post 實現,那么最好傳遞參數時使用@Field、@FieldMap和@FormUrlEncoded。因為@Query和@QueryMap都是將參數拼接在url后面的,而@Field或@FieldMap傳遞的參數時放在請求體的
* 使用@Path時,path對應的路徑不能包含”/”,否則會將其轉化為%2F。在遇到想動態的拼接多節url時,還是使用@Url
## 半靜態 url 請求
https://api.github.com/users/{user}/repos
```java
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.build()
public interface GitHubService {
@GET("users/{user}/repos")
Call<List<Repo>> listRepos(@Path("user") String user);
}
```
## 動態 url 請求
```java
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.build()
public interface GitHubService {
@GET
Call<List<Repo>> listRepos(@Url String user);
}
```
# 注解說明
## 網絡請求方法
網絡請求方法注解對應于 Http 請求方式,包括:
注解名稱|說明
---|---
@GET|從服務器取出資源(一項或多項)
@POST|在服務器新建一個資源
@PUT|在服務器更新資源(客戶端提供改變后的完整資源)
@DELETE|從服務器刪除資源
@HEAD|獲取資源的元數據
@OPTIONS|獲取信息,關于資源的哪些屬性是客戶端可以改變的
## 標記類
標記類注解,作用于網絡請求接口的方法,包括:
注解名稱|說明
---|---
@FormUrlEncoded|表示請求體是一個 Form 表單
@Multipart|表示請求體是一個 Multipart 表單
@Streaming|表示返回的數據以流的形式返回
### 提交表單
代碼示例:
```java
public interface Add2GankApi {
@POST("add2gank")
@FormUrlEncoded
Call<ResponseBody> add(@Field("url") String url, @Field("desc") String desc, @Field("who") String who,
@Field("type") String type, @Field("debug") boolean debug);
}
```
### 提交 Multipart
代碼示例:
```java
public interface Add2GankApi {
@POST("add2gank")
@Multipart
Call<ResponseBody> addFile(@Part("name") RequestBody name, @Part("age") RequestBody age,
@Part MultipartBody.Part file);
}
```
其中 @Part 后面括號內的字符用于生成每個 Part 請求頭 Content-Disposition 字段的信息,接收的參數則用于生成請求體。生成的 Content-Disposition 字段示例如下:
```plain
Content-Disposition: form-data; name=”name”
或
Content-Disposition: form-data; name=”test”; filename=”test.txt”
```
調用:
```java
RequestBody name = RequestBody.create(mediaType, "Tom");
RequestBody age = RequestBody.create(mediaType, "12");
MultipartBody.Part file= MultipartBody.Part.createFormData("test", "test.txt",
RequestBody.create(mediaType, new File("test.txt")));
RetrofitUtil.getInstance()
.createAdd2GankApiRequest()
.addFile(name, age, file);
```
## 網絡請求參數注解
網絡請求參數注解包括:
注解名稱|說明
---|---
@Headers|添加固定的請求頭
@Header|添加不固定值的 Header
示例:
```java
@GET("user")
Call<ResponseBody> getUser(@Header("Authorization") String authorization);
@GET("user")
@Headers("Authorization: authorization")
Call<ResponseBody> getUser();
```
注解名稱|說明
---|---
@Body|發送自定義數據類型(非表單數據)給服務器
注解名稱|說明
---|---
@Field|表單字段
@FieldMap|表單字段
示例:
```java
@POST("add2gank")
@FormUrlEncoded
Call<ResponseBody> add(@Field("username") String name, @Field("age") int age);
@POST("add2gank")
@FormUrlEncoded
Call<ResponseBody> add(@FieldMap Map<String, Object> map);
```
使用:
```java
RetrofitUtil.getInstance()
.createAdd2GankApiRequest()
.add("Tom", 18);
Map<String, Object> map = new HashMap<>();
map.put("username", "Tom");
map.put("age", 18);
RetrofitUtil.getInstance()
.createAdd2GankApiRequest()
.add(map);
```
注解名稱|說明
---|---
@Part|Multipart 表單字段
@PartMap|Multipart 表單字段
示例:
```java
public interface Add2GankApi {
@POST("add2gank")
@Multipart
Call<ResponseBody> addFile(@Part("name") RequestBody name, @Part("age") RequestBody age,
@Part MultipartBody.Part file);
@POST("add2gank")
@Multipart
Call<ResponseBody> addFile(@PartMap Map<String, RequestBody> args, @Part MultipartBody.Part file);
}
```
使用
```java
// Part 使用
RequestBody name = RequestBody.create(mediaType, "Tom");
RequestBody age = RequestBody.create(mediaType, "12");
MultipartBody.Part file= MultipartBody.Part.createFormData("test", "test.txt",
RequestBody.create(mediaType, new File("test.txt")));
RetrofitUtil.getInstance()
.createAdd2GankApiRequest()
.addFile(name, age, file);
// PartMap 使用
Map<String, RequestBody> map = new HashMap<>();
map.put("name", name);
map.put("age", age);
RetrofitUtil.getInstance()
.createAdd2GankApiRequest()
.addFile(map, file);
```
注解名稱|說明
---|---
@Query|作用于 GET 方法的查詢參數,作用于 Url
@QueryMap|作用于 GET 方法的查詢參數,作用于 Url
示例:
```java
@GET("user")
Call<ResponseBody> getUser(@Query("age") int age, @Query("gender") int gender);
```
在 baseUrl = "http://gank.io/api/",調用 getUser 方法傳參為(18, 0)時,請求 url 為 "http://gank.io/api/?age=18&gender=0"
注解名稱|說明
---|---
@Path|URL 地址的缺省值
@Url|直接設置 Url 變量
示例:
```java
@GET("users/{user}/photos")
Call<ResponseBody> getUserPhotos(@Path("user") String user);
```
當傳參為 "tom" 時,請求 url 為 "http://gank.io/api/users/tom/photos"。
示例:
```java
@GET
Call<ResponseBody> getUSer(@Url String url, @Query("age") int age);
```
當有 @Url 注解時,@GET 傳入的 Url 可以忽略。
# 源碼分析
## 具體分析
### Retrofit 的構造
我們首先來從 Retrofit 對象的構建說起。和 OkHttpClient 一樣,Retrofit 對象我們也建議項目中只維護一個。一個標準的 Retrofit 構造代碼如下:
```java
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
```
可以看到 Retrofit 也使用了建造者模式,
### Request對象的封裝與Call的生成
一個標準的 Retrofit 請求格式為 `mRetrofit.create(RequestService.class).doGet("params").enqueue(callback)`。
在OkHttp中,我們手動創建了Request對象,并創建Call對象。而在Retrofit中,則是首先聲明請求接口(通過注解聲明請求地址、方法、請求體等),Retrofit的create方法會為我們創建一個請求接口對象。先來看看create方法:
```java
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, @Nullable 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);
}
if (platform.isDefaultMethod(method)) {
return platform.invokeDefaultMethod(method, service, proxy, args);
}
// 創建 ServiceMethod
ServiceMethod<Object, Object> serviceMethod = (ServiceMethod<Object, Object>) loadServiceMethod(method);
// 創建 OkHttpCall
OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);
// 返回
return serviceMethod.adapt(okHttpCall);
}
});
}
```
Retrofit使用了動態代理模式(可參考[代理模式]([http://wiki.xuchongyang.com/1091154#2\_\_57](http://wiki.xuchongyang.com/1091154#2__57))),create方法返回了請求接口的代理對象。當我們調用代理對象的方法時,會被攔截,執行最后三行關鍵代碼,下面我們先看看ServiceMethod類,再依次看最后三行代碼。
1、ServiceMethod類就是我們定義的請求接口到Call的轉換適配器。
```java
public class ServiceMethod {
Builder(Retrofit retrofit, Method method) {
this.retrofit = retrofit;
this.method = method;
this.methodAnnotations = method.getAnnotations();
this.parameterTypes = method.getGenericParameterTypes();
this.parameterAnnotationsArray = method.getParameterAnnotations();
}
public ServiceMethod build() {
//...
// 解析方法注解
for (Annotation annotation : methodAnnotations) {
parseMethodAnnotation(annotation);
}
//...
int parameterCount = parameterAnnotationsArray.length;
parameterHandlers = new ParameterHandler<?>[parameterCount];
for (int p = 0; p < parameterCount; p++) {
Type parameterType = parameterTypes[p];
if (Utils.hasUnresolvableType(parameterType)) {
throw parameterError(p, "Parameter type must not include a type variable or wildcard: %s",
parameterType);
}
Annotation[] parameterAnnotations = parameterAnnotationsArray[p];
if (parameterAnnotations == null) {
throw parameterError(p, "No Retrofit annotation found.");
}
// 解析參數
parameterHandlers[p] = parseParameter(p, parameterType, parameterAnnotations);
}
// ...
return new ServiceMethod<>(this);
}
private void parseMethodAnnotation(Annotation annotation) {
if (annotation instanceof DELETE) {
parseHttpMethodAndPath("DELETE", ((DELETE) annotation).value(), false);
} else if (annotation instanceof GET) {
parseHttpMethodAndPath("GET", ((GET) annotation).value(), false);
} else if (annotation instanceof HEAD) {
parseHttpMethodAndPath("HEAD", ((HEAD) annotation).value(), false);
if (!Void.class.equals(responseType)) {
throw methodError("HEAD method must use Void as response type.");
}
} else if (annotation instanceof PATCH) {
parseHttpMethodAndPath("PATCH", ((PATCH) annotation).value(), true);
} else if (annotation instanceof POST) {
parseHttpMethodAndPath("POST", ((POST) annotation).value(), true);
} else if (annotation instanceof PUT) {
parseHttpMethodAndPath("PUT", ((PUT) annotation).value(), true);
} else if (annotation instanceof OPTIONS) {
parseHttpMethodAndPath("OPTIONS", ((OPTIONS) annotation).value(), false);
} else if (annotation instanceof HTTP) {
HTTP http = (HTTP) annotation;
parseHttpMethodAndPath(http.method(), http.path(), http.hasBody());
} else if (annotation instanceof retrofit2.http.Headers) {
String[] headersToParse = ((retrofit2.http.Headers) annotation).value();
if (headersToParse.length == 0) {
throw methodError("@Headers annotation is empty.");
}
headers = parseHeaders(headersToParse);
} else if (annotation instanceof Multipart) {
if (isFormEncoded) {
throw methodError("Only one encoding annotation is allowed.");
}
isMultipart = true;
} else if (annotation instanceof FormUrlEncoded) {
if (isMultipart) {
throw methodError("Only one encoding annotation is allowed.");
}
isFormEncoded = true;
}
}
}
```
從代碼中可以看到,我們使用注解定義的所有信息,在構造ServiceMethod時進行了解析并存儲。
2、接下來看看剛剛create方法的倒數第三行代碼`ServiceMethod<Object, Object> serviceMethod = (ServiceMethod<Object, Object>) loadServiceMethod(method);`。
其中loadServiceMethod方法源碼如下:
```java
ServiceMethod<?, ?> loadServiceMethod(Method method) {
ServiceMethod<?, ?> result = serviceMethodCache.get(method);
if (result != null) return result;
synchronized (serviceMethodCache) {
result = serviceMethodCache.get(method);
if (result == null) {
result = new ServiceMethod.Builder<>(this, method).build();
serviceMethodCache.put(method, result);
}
}
return result;
}
```
可以看到Retrofit對ServiceMethod實例進行了緩存,來提高請求時的解析效率。
3、看下倒數第二行代碼`OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);`。這行代碼根據serviceMethod對象構造了一個OkHttpCall對象。
OkHttpCall類源碼如下:
```java
final class OkHttpCall<T> implements Call<T> {
private final ServiceMethod<T, ?> serviceMethod;
private final @Nullable Object[] args;
OkHttpCall(ServiceMethod<T, ?> serviceMethod, @Nullable Object[] args) {
this.serviceMethod = serviceMethod;
this.args = args;
}
private okhttp3.Call createRawCall() throws IOException {
okhttp3.Call call = serviceMethod.toCall(args);
if (call == null) {
throw new NullPointerException("Call.Factory returned null.");
}
return call;
}
}
```
可以看到,OkHttpCall實現了Call接口,可以直接作為Call執行一系列操作,并且有一個createRawCall方法,會在調用request、enqueue、execute方法時進行調用。
4、得到OkHttpCall對象后,來看看最后一行代碼`return serviceMethod.adapt(okHttpCall);`
來看下ServiceMethod的adapt方法:
```java
T adapt(Call<R> call) {
return callAdapter.adapt(call);
}
```
下面我們按兩條線來走,一是CallAdapter是怎么來的呢,二是CallAdapter的adapt方法做了什么。
a、首先來看看CallAdapter是怎么來的呢?
```java
private CallAdapter<T, R> createCallAdapter() {
Type returnType = method.getGenericReturnType();
Annotation[] annotations = method.getAnnotations();
//...
return (CallAdapter<T, R>) retrofit.callAdapter(returnType, annotations);
}
// Retrofit類的callAdapter方法如下:
public CallAdapter<?, ?> callAdapter(Type returnType, Annotation[] annotations) {
return nextCallAdapter(null, returnType, annotations);
}
public CallAdapter<?, ?> nextCallAdapter(@Nullable CallAdapter.Factory skipPast, Type returnType,
Annotation[] annotations) {
int start = callAdapterFactories.indexOf(skipPast) + 1;
for (int i = start, count = callAdapterFactories.size(); i < count; i++) {
CallAdapter<?, ?> adapter = callAdapterFactories.get(i).get(returnType, annotations, this);
if (adapter != null) {
return adapter;
}
}
}
```
可以看到,從CallAdapterFactories中遍歷查詢,看是否有返回類型相匹配的CallAdpater。下面再看看CallAdapterFactories是怎么來的。
```java
public Retrofit build() {
//...
// 添加默認的CallAdapterFactory
List<CallAdapter.Factory> callAdapterFactories = new ArrayList<>(this.callAdapterFactories);
callAdapterFactories.add(platform.defaultCallAdapterFactory(callbackExecutor));
//...
}
```
在Retrofit的構造方法中,會添加平臺的默認CallAdapterFactory。CallAdapterFactory有兩個子類:DefaultCallAdapterFactory和ExecutorCallAdapterFactory。默認的CallAdapterFactory是ExecutorCallAdapterFactory:
```java
final class ExecutorCallAdapterFactory extends Factory {
public CallAdapter<?, ?> get(Type returnType, Annotation[] annotations, Retrofit retrofit) {
//...
return new CallAdapter<Object, Call<?>>() {
public Type responseType() {
return responseType;
}
public Call<Object> adapt(Call<Object> call) {
return new ExecutorCallAdapterFactory.ExecutorCallbackCall(ExecutorCallAdapterFactory.this.callbackExecutor, call);
}
};
}
static final class ExecutorCallbackCall<T> implements Call<T> {
final Executor callbackExecutor;
final Call<T> delegate;
ExecutorCallbackCall(Executor callbackExecutor, Call<T> delegate) {
this.callbackExecutor = callbackExecutor;
this.delegate = delegate;
}
public void enqueue(final Callback<T> callback) {
//...
}
public Response<T> execute() throws IOException {
//...
}
}
}
```
b、下面看看第二個問題,CallAdapter的adapt方法到底做了什么呢?由上面的ExecutorCallAdapterFactory可看到,adapt方法返回了一個Call接口的實現類。
c、最后,我們再多來看一下CallAdapter的聲明
```java
public interface CallAdapter<R, T> {
Type responseType();
T adapt(Call<R> call);
abstract class Factory {
public abstract @Nullable CallAdapter<?, ?> get(Type returnType, Annotation[] annotations,
Retrofit retrofit);
protected static Type getParameterUpperBound(int index, ParameterizedType type) {
return Utils.getParameterUpperBound(index, type);
}
protected static Class<?> getRawType(Type type) {
return Utils.getRawType(type);
}
}
}
```
# 參考
[這是一份很詳細的 Retrofit 2.0 使用教程(含實例講解)](http://blog.csdn.net/carson_ho/article/details/73732076)
[RESTful API 設計指南](http://www.ruanyifeng.com/blog/2014/05/restful_api.html)
[Android:手把手帶你 深入讀懂 Retrofit 2.0 源碼]([https://www.jianshu.com/p/0c055ad46b6c](https://www.jianshu.com/p/0c055ad46b6c))
- 導讀
- Java知識
- Java基本程序設計結構
- 【基礎知識】Java基礎
- 【源碼分析】Okio
- 【源碼分析】深入理解i++和++i
- 【專題分析】JVM與GC
- 【面試清單】Java基本程序設計結構
- 對象與類
- 【基礎知識】對象與類
- 【專題分析】Java類加載過程
- 【面試清單】對象與類
- 泛型
- 【基礎知識】泛型
- 【面試清單】泛型
- 集合
- 【基礎知識】集合
- 【源碼分析】SparseArray
- 【面試清單】集合
- 多線程
- 【基礎知識】多線程
- 【源碼分析】ThreadPoolExecutor源碼分析
- 【專題分析】volatile關鍵字
- 【面試清單】多線程
- Java新特性
- 【專題分析】Lambda表達式
- 【專題分析】注解
- 【面試清單】Java新特性
- Effective Java筆記
- Android知識
- Activity
- 【基礎知識】Activity
- 【專題分析】運行時權限
- 【專題分析】使用Intent打開三方應用
- 【源碼分析】Activity的工作過程
- 【面試清單】Activity
- 架構組件
- 【專題分析】MVC、MVP與MVVM
- 【專題分析】數據綁定
- 【面試清單】架構組件
- 界面
- 【專題分析】自定義View
- 【專題分析】ImageView的ScaleType屬性
- 【專題分析】ConstraintLayout 使用
- 【專題分析】搞懂點九圖
- 【專題分析】Adapter
- 【源碼分析】LayoutInflater
- 【源碼分析】ViewStub
- 【源碼分析】View三大流程
- 【源碼分析】觸摸事件分發機制
- 【源碼分析】按鍵事件分發機制
- 【源碼分析】Android窗口機制
- 【面試清單】界面
- 動畫和過渡
- 【基礎知識】動畫和過渡
- 【面試清單】動畫和過渡
- 圖片和圖形
- 【專題分析】圖片加載
- 【面試清單】圖片和圖形
- 后臺任務
- 應用數據和文件
- 基于網絡的內容
- 多線程與多進程
- 【基礎知識】多線程與多進程
- 【源碼分析】Handler
- 【源碼分析】AsyncTask
- 【專題分析】Service
- 【源碼分析】Parcelable
- 【專題分析】Binder
- 【源碼分析】Messenger
- 【面試清單】多線程與多進程
- 應用優化
- 【專題分析】布局優化
- 【專題分析】繪制優化
- 【專題分析】內存優化
- 【專題分析】啟動優化
- 【專題分析】電池優化
- 【專題分析】包大小優化
- 【面試清單】應用優化
- Android新特性
- 【專題分析】狀態欄、ActionBar和導航欄
- 【專題分析】應用圖標、通知欄適配
- 【專題分析】Android新版本重要變更
- 【專題分析】唯一標識符的最佳做法
- 開源庫源碼分析
- 【源碼分析】BaseRecyclerViewAdapterHelper
- 【源碼分析】ButterKnife
- 【源碼分析】Dagger2
- 【源碼分析】EventBus3(一)
- 【源碼分析】EventBus3(二)
- 【源碼分析】Glide
- 【源碼分析】OkHttp
- 【源碼分析】Retrofit
- 其他知識
- Flutter
- 原生開發與跨平臺開發
- 整體歸納
- 狀態及狀態管理
- 零碎知識點
- 添加Flutter到現有應用
- Git知識
- Git命令
- .gitignore文件
- 設計模式
- 創建型模式
- 結構型模式
- 行為型模式
- RxJava
- 基礎
- Linux知識
- 環境變量
- Linux命令
- ADB命令
- 算法
- 常見數據結構及實現
- 數組
- 排序算法
- 鏈表
- 二叉樹
- 棧和隊列
- 算法時間復雜度
- 常見算法思想
- 其他技術
- 正則表達式
- 編碼格式
- HTTP與HTTPS
- 【面試清單】其他知識
- 開發歸納
- Android零碎問題
- 其他零碎問題
- 開發思路