Retrofit使用详解(二)

OKHttp(一)之Calls

OKHttp(二)之Connections

OkHttp(三)之使用方法

OkHttp(四)之拦截器

OkHttp(五)之HTTPS

Retrofit使用详解(一)

Retrofit使用详解(二)

Retrofit使用详解(三)

Retrofit使用详解(四)

Retrofit使用详解(五)

Retrofit使用详解(六)

同步请求和异步请求

Retrofit支持同步请求和异步请求。

同步请求

使用的同步请求通过定义返回类型来声明。 下面的示例期望执行getTasks方法时返回Task列表。

public interface TaskService {  
    @GET("/tasks")
    Call<List<Task>> getTasks();
}

在Retrofit 2中,每个请求都被包装到一个Call对象中。 实际的同步或异步请求将在稍后创建的调用对象上的所需方法执行。 但是,同步和异步请求的接口类在Retrofit 2中是相同的。

同步方法在主线程上执行。 这意味阻塞UI,并且在此期间不可能进行交互。

Note: 警告:同步请求会触发Android 4.0或更高版本上的应用崩溃。 你会遇到NetworkOnMainThreadException错误。

同步方法能够直接返回值,因为操作在网络请求期间阻止其他任何操作。

对于非阻塞UI,你必须在一个单独的线程中的请求执行。 这意味着,你仍然可以在等待响应时与应用程序本身进行交互。

Get Results from Synchronous Requests

以下代码片段说明了使用Retrofit执行的同步请求:

TaskService taskService = ServiceGenerator.createService(TaskService.class);  
Call<List<Task>> call = taskService.getTasks();  
List<Task>> tasks = call.execute().body();  

在Call对象上使用.execute()方法,在Retrofit2中执行同步请求。反序列化响应主体可通过响应对象上的.body()方法获得。

Asynchronous Requests

public interface TaskService {  
    @GET("/tasks")
    Call<List<Task>> getTasks();
}

Retrofit在单独的线程中执行。Callback类是通用的,并映射您定义的返回类型。 我们的示例返回一个任务列表,Callback在内部进行映射。

如上所述:Retrofit 2中的接口定义对于同步和异步请求是相同的。 所需的返回类型被封装到一个Call对象中,实际的请求执行定义它的类型(同步/异步)。

Get Results from Asynchronous Requests

使用异步请求必须实现两个回调方法:成功和失败。 当从服务类调用异步getTasks()方法时,必须实现回调,并定义一旦请求完成应该做什么:

TaskService taskService = ServiceGenerator.createService(TaskService.class);  
Call<List<Task>> call = taskService.getTasks();  
call.enqueue(new Callback<List<Task>>() {  
    @Override
    public void onResponse(Call<List<Task>> call, Response<List<Task>> response) {
        if (response.isSuccessful()) {
            // tasks available
        } else {
            // error response, no access to resource?
        }
    }

    @Override
    public void onFailure(Call<List<Task>> call, Throwable t) {
        // something went completely south (like no internet connection)
        Log.d("Error", t.getMessage());
    }
}

Get Raw HTTP Response

如果你需要原始的HTTP响应对象,只需要定义返回类型为Response.
你可以接收Retrofit 2中原始响应主体与定义请求类型(sync / async)的方式相同。 不需要将Response类定义为返回类型,但可以在onResponse()回调方法中捕获它。 让我们看看下面的代码片段来说明如何获得原始响应:

call.enqueue(new Callback<List<Task>>() {  
    @Override
    public void onResponse(Call<List<Task>> call, Response<List<Task>> response) {
        // get raw response
        Response raw = response.raw();
    }

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

在请求体中发送对象

Send Objects as Request Body

Retrofit提供了在请求体内发送对象的能力。 通过使用@Body注释,可以指定对象用作HTTP请求主体。

public interface TaskService {  
    @POST("/tasks")
    Call<Task> createTask(@Body Task task);
}

定义的RestAdapter的转换器(如Gson)会将对象映射到JSON,它将最终作为请求的主体发送到服务器。

Example

public class Task {  
    private long id;
    private String text;

    public Task(long id, String text) {
        this.id = id;
        this.text = text;
    }
}

创建一个新的Task对象。

Task task = new Task(1, "my task title");  
Call<Task> call = taskService.createTask(task);  
call.enqueue(new Callback<Task>() {});  

调用方法createTask会将Task转换为JSON。Task的JSON将如下所示:

{
    "id": 1,
    "text": "my task title"
}

添加自定义头信息

Retrofit提供了两个定义HTTP请求标头字段的选项:静态和动态。 静态请求头不能更改。请求头的的键和值是固定的,并与应用程序同时启动。

相反,动态请求头必须每次都设置。

静态请求头

将API方法的头和相应的值定义为注解。 对于使用此方法的每个请求,头信息会自动通过Retrofit添加。 注解必须是键值对,可以有一个或者多个:

public interface UserService {  
    @Headers("Cache-Control: max-age=640000")
    @GET("/tasks")
    Call<List<Task>> getTasks();
}

上面的示例显示了静态头的键值定义。 此外,您可以将多个键值字符串作为封装在大括号{}中的列表传递到@Headers注释。

public interface UserService {  
    @Headers({
        "Accept: application/vnd.yourapi.v1.full+json",
        "User-Agent: Your-App-Name"
    })
    @GET("/tasks/{task_id}")
    Call<Task> getTask(@Path("task_id") long taskId);
}

此外,您可以通过Retrofit的RequestInterceptor的拦截方法(在Retrofit 2中自定义实现Interceptor接口)来定义静态头。
在Retrofit中必须在OkHttp中添加拦截器。

OkHttpClient.Builder httpClient = new OkHttpClient.Builder();  
httpClient.addInterceptor(new Interceptor() {  
    @Override
    public Response intercept(Interceptor.Chain chain) throws IOException {
        Request original = chain.request();

        Request request = original.newBuilder()
            .header("User-Agent", "Your-App-Name")
            .header("Accept", "application/vnd.yourapi.v1.full+json")
            .method(original.method(), original.body())
            .build();

        return chain.proceed(request);
    }
}

OkHttpClient client = httpClient.build();  
Retrofit retrofit = new Retrofit.Builder()  
    .baseUrl(API_BASE_URL)
    .addConverterFactory(GsonConverterFactory.create())
    .client(client)
    .build();

上面的示例将User-Agent和Accept头字段设置为相应的值。 这些值与使用RestAdapter(Retrofit 2中的Retrofit)和集成的RequestInterceptor(Retrofit 2中的Interceptor)执行的请求一起传递。

Dynamic Header

动态header是作为参数传进方法中的。 在执行请求之前,通过Retrofit映射提供的参数值:

public interface UserService {  
    @GET("/tasks")
    Call<List<Task>> getTasks(@Header("Content-Range") String contentRange);
}

动态请求头允许你为每个请求设置不同的值。

复写Retrofit2中的已经存在的请求头

  • .header(key,value):如果已经存在由键标识的现有头,则用值覆盖相应的值
  • .addHeader(key,value):添加相应的标题键和值,即使存在具有相同键的现有标题字段

在拦截器中管理请求头

添加请求头

一个常见的例子是使用授权头字段。如果几乎每个请求都需要包含授权的头字段,则可以使用拦截器来添加这条信息。 这样,就不需要为每个端点声明添加@Header注释。

类似于前边介绍的,可以直接添加头信息,也可以覆盖原有的头信息。

How to Override Headers

使用OkHttp拦截器允许你修改实际请求。请求构建器具有一个.header(key,val)方法,它会将定义的头添加到请求中。 如果已经存在具有相同键标识符的现有头,则此方法将覆盖先前定义的值。

OkHttpClient.Builder httpClient = new OkHttpClient.Builder();  
httpClient.addInterceptor(new Interceptor() {  
    @Override
    public Response intercept(Interceptor.Chain chain) throws IOException {
        Request original = chain.request();

        // Request customization: add request headers
        Request.Builder requestBuilder = original.newBuilder()
                .header("Authorization", "auth-value"); // <-- this is the important line

        Request request = requestBuilder.build();
        return chain.proceed(request);
    }
});

OkHttpClient client = httpClient.build();  

Retrofit和OkHttp允许你添加多个具有相同key的头信息,这将会覆盖原有的头信息。

How to Not Override Headers

有时会使用具有相同名称的多个头。实际上,我们只知道一个具体的实例:Cache-Control头。 HTTP RFC2616指定允许具有相同名称的多个标头值,如果它们可以表示为逗号分隔列表。

Cache-Control: no-cache  
Cache-Control: no-store  

等同于

Cache-Control: no-cache, no-store  

此时可以调用.addHeader()方才来添加头,而不是覆盖。

OkHttpClient.Builder httpClient = new OkHttpClient.Builder();  
httpClient.addInterceptor(new Interceptor() {  
    @Override
    public Response intercept(Interceptor.Chain chain) throws IOException {
        Request original = chain.request();

        // Request customization: add request headers
        Request.Builder requestBuilder = original.newBuilder()
                    .addHeader("Cache-Control", "no-cache")
                    .addHeader("Cache-Control", "no-store");

        Request request = requestBuilder.build();
        return chain.proceed(request);
    }
});

OkHttpClient client = httpClient.build();  
  • .header(key,value):如果已经存在由键标识的现有头,则用值覆盖相应的值
  • .addHeader(key,value):添加相应的标题键和值,即使存在具有相同键的现有标题字段

使用@HeaderMap添加请求头

Dynamic Request Headers

前边文章中显示的方法都是静态的。 虽然你可以更改请求头的值,但无法动态选择实际发送的请求头。 ,@HeaderMap可以让你在运行时决定哪些头被添加到你的请求。

与@Header注释类似,需要将@HeaderMap声明为接口参数之一。 参数的类型需要实现Java Map接口:

public interface TaskService {  
    @GET("/tasks")
    Call<List<Task>> getTasks(
        @HeaderMap Map<String, String> headers
    );
}

使用我们上面声明的接口非常简单。 您可以创建一个Map实例,并根据您的需要使用值填充它。 Retrofit将@HeaderMap的每个非空元素添加为请求标头。

TaskService taskService = ServiceGenerator.createService(TaskService.class);

Map<String, String> map = new HashMap<>();  
map.put("Page", String.valueOf(page));

if (BuildConfig.DEBUG) {  
    map.put("Accept", "application/vnd.yourapi.v1.full+json");
    map.put("User-Agent", "Future Studio Debug");
}
else {  
    map.put("Accept", "application/json");
    map.put("Accept-Charset", "utf-8");
    map.put("User-Agent", "Future Studio Release");
}

Call<List<Task>> call = taskService.getTasks(map);  
// Use it like any other Retrofit call

多个具有相同名称的查询参数

Query Parameters

Query parameters 是从客户端传递数据到服务器的最常见的方式。

https://api.example.com/tasks?id=123  

上面的例子是通过路由"tasks"传递数据"id=123"到服务器。

在Retrofit中可以如下写

public interface TaskService {  
    @GET("/tasks")
    Call<Task> getTask(@Query("id") long taskId);
}

在getTask方法中需要传入参数"taskId",Retrofit会自动转换为"/task?id=<taskid>"形式。

Multiple Query Parameters

一些情况下需要传递多个相同名字的参数到服务器,类似于下面的样子:

https://api.example.com/tasks?id=123&id=124&id=125  

此时期望服务器的返回值应该是一个任务列表对应的id是ids=[123,124,125]。

在Retrofit中可以很简单的做到,只需要传递一个List即可。

public interface TaskService {  
    @GET("/tasks")
    Call<List<Task>> getTask(@Query("id") List<Long> taskIds);
}

可选查询参数

根据API的设计,有时候我们需要传递可选的参数到服务器。如果你不想传递参数,就传递null即可。
service.getTasks(null);
Retrofit在重新编译请求时会自动忽略null的参数。记住,你不能使用原始数据类型来传递null,例如:int,float,long等,你必须使用它们的包装类:Integer,Float,Long等,这样编译器不会报错:

public interface TaskService {  
    @GET("/tasks")
    Call<List<Task>> getTasks(
        @Query("sort") String order,
        @Query("page") Integer page);
}

现在你可以为getTasks方法传递null参数了。

service.getTasks(null, null);  

传递URL表单数据

表单请求

在Retrofit添加表单请求只需要添加另外一个注解将会直接将你的请求类型转换为"application/x-www-form-urlencoded".如下边的例子:

public interface TaskService {  
    @FormUrlEncoded
    @POST("tasks")
    Call<Task> createTask(@Field("title") String title);
}

重要的部分是@FormUrlEncoded这个注解。你不能使用get方法时候添加这个注解,表单的目的是传递数据到服务器。
此外,必须使用@Field注释来与您的请求一起发送的参数。 将所需的键放在@Field(“key”)注释中以定义参数名称。 此外,将您的值的类型添加为方法参数。 如果不使用String,Retrofit将使用Java的String.valueOf(yourObject)方法创建一个字符串值。

service.createTask("Research Retrofit form encoded requests");  

生成的结果是

title=Research+Retrofit+form+encoded+requests  

假如需要传递多个参数,你只需要继续添加@Field即可。

Form Encoded Requests Using an Array of Values

在上边的例子中使用@Field注解可以添加字符串数据。但是如果你想要使用对象而不是字符串类型,Retrofit将会把你的对象转换为字符串。你也可以使用同一个key传递一个字符串数组。

public interface TaskService {  
    @FormUrlEncoded
    @POST("tasks")
    Call<List<Task>> createTasks(@Field("title") List<String> titles);
}

现在来看一下如何使用

List<String> titles = new ArrayList<>();  
titles.add("Research Retrofit");  
titles.add("Retrofit Form Encoded")

service.createTasks(titles);  

生成的结果如下:
title=Research+Retrofit&title=Retrofit+Form+Encoded

每个条目都被转换成了类似于map的键值对形式,键值对与键值对之间用&连接,键值对内部用=连接。

Field Options

@Filed注解有一个编码选项encoded,是布尔值。默认是false。

这个encoded定义的内容是你是否已经为键值对编码为url:
@Field(value = "title", encoded = true) String title

Form-Urlencoded vs. Query Parameter

这两种传递数据的方式有什么不同:
form-urlencoded: POST
query parameter: GET
使用form-urlencoded方式传递数据的时候数据存放在请求体内,不会再url中显示,使用query parameter方式传递参数时会在url中显示,多用于筛选或指定区域数据查询。

使用FieldMap发送表单数据

What Is @Fieldmap in Retrofit?

假如有多个注解,例如添加查询参数或者路径参数,使用给定对象请求数据,创建已经编码好的请求体。举一个简单的例子,你现在想更新程序中用户的数据,你需要请求一个接口要求是键值对形式上传的。接口接受一个JSON字段,你可以这样写:

public interface UserService {  
    @FormUrlEncoded
    @PUT("user")
    Call<User> update(
            @Field("username") String username,
            @Field("name") String name,
            @Field("email") String email,
            @Field("homepage") String homepage,
            @Field("location") String location
    );
}

PUT方法需要多个参数,例如usernam,email,homepage等。

缺点:每次我们使用新的数据发送更新的时候,我们必须要提供每一个参数的值,即使他们没有更改,这样做很繁琐。

Retrofit提供了一个解决上述问题的方案:@FieldMap。

Form Encoded Requests Using FieldMap

有时候你只需要为某个用户更新指定字段,你可以使用Retrofit的@FieldMao。你可以使用java标准的Map格式来添加你的键值对。

public interface UserService {  
    @FormUrlEncoded
    @PUT("user")
    Call<User> update(@FieldMap Map<String, String> fields);
}

Note:假如你只需要更新你的username字段,那就没有必要添加username以外的字段,你的请求只包含单个字段。

@FiledMap在应用程序中使用的非常广泛,但是又一点不好的地方:你不知道哪些字段是允许添加的,你也不知道字段名称,这就需要额外的文档来说明。

FieldMap Options

同@Field一样,@FieldMap也包含一个布尔值encoded,默认是false。

使用方法如下:
@FieldMap(encoded = true) Map<String, String> fields

encode定义了每个键值对是否已经被编码。

来看一个简单的例子。要更新username字段的值为marcus-poehls,默认情况下会变成username=marcus-poehls,使用编码后会变成username=marcus%2Dpoehls。

为每个请求添加Query Parameters

你可以通过向OkHttpClient添加一个新的请求拦截器来实现。 拦截实际请求并获取HttpUrl。 http url是添加查询参数所必需的,因为它将通过附加查询参数名称及其值来更改先前生成的请求网址。

OkHttpClient.Builder httpClient =  
    new OkHttpClient.Builder();
httpClient.addInterceptor(new Interceptor() {  
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request original = chain.request();
        HttpUrl originalHttpUrl = original.url();

        HttpUrl url = originalHttpUrl.newBuilder()
                .addQueryParameter("apikey", "your-actual-api-key")
                .build();

        // Request customization: add request headers
        Request.Builder requestBuilder = original.newBuilder()
                .url(url);

        Request request = requestBuilder.build();
        return chain.proceed(request);
    }
});

一旦您拥有HttpUrl对象,就可以基于原始的http url对象创建一个新的构建器。 该构建器将允许您使用.addQueryParameter(key,val)方法添加其他查询参数。 添加查询参数后,使用.build()方法创建新的HttpUrl对象,该对象通过使用Request.Builder添加到请求中。 上面的代码使用基于原始请求的附加参数构建新请求,并且只是将网址与新创建的网址进行交换。

使用@QueryMap添加多个查询参数

Retrofit允许使用@Query注解来添加查询参数。假如有多个参数需要传递,类似于下面:

public interface NewsService() {  
    @GET("/news")
    Call<List<News>> getNews(
            @Query("page") int page,
            @Query("order") String order,
            @Query("author") String author,
            @Query("published_at") Date date,
            …
    );
}

你可以使用null来传给getNews方法。但是Retrofit提供了一个更好的方法。

How to Use QueryMap

@QueryMap注解使用Map<String,String>来作为参数,每个非空的key的value都会被添加进去。

public interface NewsService() {  
    @GET("/news")
    Call<List<News>> getNews(
        @QueryMap Map<String, String> options
    );
}

如果你请求作者是Marcus第二页的新闻,你可以只添加page和author字段,而没必要再去添加其他字段。

private void fetchNews() {  
    Map<String, String> data = new HashMap<>();
    data.put("author", "Marcus");
    data.put("page", String.valueOf(2));

    // simplified call to request the news with already initialized service
    Call<List<News>> call = newsService.getNews(data);
    call.enqueue(…);
}

最后的结果是

http://your.api.url/news?page=2&author=Marcus

QueryMap Options

同前面的一样,它也拥有布尔值encoded,默认是false。此处不讲解详细用法了。

怎样在请求中使用动态URL

Use-Case Scenarios

说明一下这个动态URl是不使用BaseURL的网址。

  1. 上传文件:假如你的APP允许用户上传自己的图片,你可能回把它们保存到自己的服务器上。
  2. 下载文件:文件能在不同于BaseURL的网址保存

How to Use Dynamic Urls

你可使用@Url注解为请求方法添加动态网址:

public interface UserService {  
    @GET
    public Call<ResponseBody> profilePicture(@Url String url);
}

上边的@GET注解后边并没有添加路由参数,它会自己添加@Url注解的参数作为请求地址。

How Urls Resolve Against Your Base Url

Retrofit2使用OkHttp的httpurl解析站点。
来看第一个例子:

Retrofit retrofit = Retrofit.Builder()  
    .baseUrl("https://your.api.url/");
    .build();

UserService service = retrofit.create(UserService.class);  
service.profilePicture("https://s3.amazon.com/profile-picture/path");

// request url results in:
// https://s3.amazon.com/profile-picture/path

因为你使用了一个完全的地址(https://s3.amazon.com/profile-picture/path),Retrofit会替换掉BaseUrl然后使用你输入的地址。

来看第二个例子

Retrofit retrofit = Retrofit.Builder()  
    .baseUrl("https://your.api.url/");
    .build();

UserService service = retrofit.create(UserService.class);  
service.profilePicture("profile-picture/path");

// request url results in:
// https://your.api.url/profile-picture/path

这个例子中你添加的参数被拼接到BaseURL后边。

来看第三个例子

Retrofit retrofit = Retrofit.Builder()  
    .baseUrl("https://your.api.url/v2/");
    .build();

UserService service = retrofit.create(UserService.class);  
service.profilePicture("/profile-picture/path");

// request url results in:
// https://your.api.url/profile-picture/path

第二个和第三个例子之间的区别是:我们添加了v2 /到BaseURl,并使用/在路径前面。 实际上,这将导致相同的最终请求url,因为以开头的斜杠开头的端点url将仅附加到基本url的主机url。 当对端点网址使用前导斜杠时,将忽略主机网址后面的所有内容。 您可以通过从您的端点删除前导/来解决您的问题。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,633评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,811评论 25 707
  • 整体Retrofit内容如下: 1、Retrofit解析1之前哨站——理解RESTful2、Retrofit解析2...
    隔壁老李头阅读 15,060评论 4 39
  • 〖各种绕〗☞「简化」 一些看起来复杂的图样 只要按解构步骤 一笔一划画出来 过程其实几简单
    肥鸽子麻麻阅读 299评论 4 2
  • 《今天的花店没有丁香》 金色的阳光穿过头顶灰色的天空,落在这一座钢筋水泥的森林,镂空他的掌心,铺在他的眼睛上,熠熠...
    临江渚雨阅读 222评论 0 0