Retrofit你想知道的一切之基礎(chǔ)使用

Retrofit--相信大家都或多或少的聽過和用過了蒿囤,不知道是什么的證明你已經(jīng)out了~我使用和研究Retrofit也有一段時間了守谓,所以準(zhǔn)備記錄一下自己使用的一些成果吕世。這篇將會介紹Retrofit的簡單配置和使用翻翩。
注:本系列Retrofit版本為2.1.0檐盟,OkHttp版本為3.4.1褂萧;本系列的接口均為非RESTful接口

Retrofit初識

1.HTTP請求方法和簡單的RESTful

Retrofit支持RESTful ,先來簡單的說一下RESTful接口(由于我自己也對RESTful一臉懵逼葵萎,所以請小伙伴們自行Google了解更多)导犹。首先在這里先來說一下HTTP的請求方法,HTTP請求方法包含get羡忘、post谎痢、delete、put卷雕、head节猿、patch、trace、options總共8種滨嘱。除get外峰鄙,其他6種都是基于post方法衍生的,最常見的是get和post太雨,而put吟榴、delete、post囊扳、get這四種最重要吩翻,分別對應(yīng)數(shù)據(jù)庫的增刪改查。

我們先來看一下比較常見的接口地址:
對學(xué)生信息進(jìn)行操作:1.查詢學(xué)生數(shù)量 2.創(chuàng)建新學(xué)生 3.修改學(xué)生信息 4.刪除學(xué)生锥咸。

請求方法 接口地址 接口說明
get /api/student/index 查詢接口
post /api/student/ + 參數(shù) 創(chuàng)建接口
post /api/student/update + 參數(shù) 修改接口
post /api/student/delete + 參數(shù) 刪除接口

而如果我們用了RESTful API就會變成這樣:

請求方法 接口地址 接口說明
get /api/student/index 查詢接口
post /api/student/ + 參數(shù) 創(chuàng)建接口
put /api/student/ + 參數(shù) 修改接口
delete /api/student/ + 參數(shù) 刪除接口

大家是不是也看出兩者的區(qū)別了狭瞎?修改跟刪除直接用HTTP的請求方法(Method)來指定,雖然url 一樣搏予,但仍然可以知道你的動作脚作。這就是所謂的RESTful 概念(這部分出自百萬講師stormzhang,感謝張哥帶我們飛~)

2.開啟Retrofit旅程

導(dǎo)入Retrofit:

compile 'com.squareup.retrofit2:retrofit:2.1.0'

Retrofit通過接口來管理HTTP API缔刹,那么首先我們先定義一個API的接口:

public interface RetrofitApi{
    @GET("public")
    Call<BaseResult<User>>  getUser();
}

然后通過Retrofit.Builder獲取到Retrofit實(shí)例,并通過create(clazz)方法獲取到我們剛才創(chuàng)建的RetrofitApi接口實(shí)例:

Retrofit retrofit=new Retrofit.Builder()
        .baseUrl("http://192.168.1.79:8080/")
        .build();

RetrofitApi retrofitApi=retrofit.create(RetrofitApi.class);

有了RetrofitApi實(shí)例之后劣针,我們就可以在需要網(wǎng)絡(luò)請求的地方調(diào)用了:

Call<BaseResult<User>> userCall=retrofitApi.getUser();

以上是利用Retrofit向http://192.168.1.79:8080/public接口發(fā)送一個get請求校镐,獲取用戶信息。

注:baseUrl("")中的url必須以'/'結(jié)尾捺典,否則會報(bào)異常鸟廓;@GET("public")中的url如果是需要拼接在baseUrl之后的則不要以‘/’開頭

Retrofit的注解

Retrofit中有很多注解,這些注解總共分三類:HTTP請求方法襟己、標(biāo)記類引谜、參數(shù)類

1.HTTP請求方法注解

Retrofit支持八種HTTP請求方法注解,分別是:GET擎浴,POST员咽,PUT,DELETE贮预,HEAD贝室,PATCH,OPTIONS仿吞,HTTP滑频,其中前7種分別對應(yīng)HTTP請求方法(見Retrofit初識小節(jié)1),而HTTP注解可自定義請求方法唤冈,也就是說可以替換前面七種方法峡迷。

  • GET:對應(yīng)HTTP的get請求方法

    寫法:

    @GET("public")
    Call<BaseResult<List<User>>> getUser();
    
  • POST:對應(yīng)HTTP的post請求方法

    寫法:

       @POST("User")
       Call<BaseResult<String>> addUser();
    
  • PUT:對應(yīng)HTTP的put請求方法

    寫法:

       @PUT("User")
       Call<BaseResult<String>> updateUser();
    
  • DELETE:對應(yīng)HTTP的delete請求方法

    寫法:

       @DELETE("User")
       Call<BaseResult<String>> deleteUser();
    
  • HEAD:對應(yīng)HTTP的head請求方法

  • PATCH:對應(yīng)HTTP的patch請求方法

  • OPTIONS:對應(yīng)HTTP的options請求方法

  • HTTP:可替換以上七種,也可以擴(kuò)展請求方法

    寫法:

       /**
    * method 表示請的方法你虹,不區(qū)分大小寫
    * path表示路徑
    * hasBody表示是否有請求體
    */
    @HTTP(method = "get", path = "public", hasBody = false)
    Call<BaseResult<List<User>>> getUser();
    

2.標(biāo)記類注解

Retrofit支持三種標(biāo)記類注解绘搞,分別是:FormUrlEncoded彤避、Multipart、Streaming看杭。

  • FormUrlEncoded:指請求體是一個Form表單忠藤,Content-Type=application/x-www-form-urlencoded,需要和參數(shù)類注解@Field楼雹,@FieldMap搭配使用(詳見下節(jié))

    寫法:

    @FormUrlEncoded
    @POST("public")
    Call<BaseResult> addUser(@Field("userName") String userName);
    
  • Multipart:指請求體是一個支持文件上傳的Form表單模孩,Content-Type=multipart/form-data,需要和參數(shù)類注解@Part贮缅,@PartMap搭配使用(詳見下節(jié))

    寫法:

    @Multipart
    @POST("public")
    Call<BaseResult> uploadFile(@Part MultipartBody.Part file);
    
  • Streaming:指響應(yīng)體的數(shù)據(jù)以流的形式返回榨咐,如果不使用默認(rèn)會把數(shù)據(jù)全部加載到內(nèi)存,所以下載文件時需要加上這個注解

    寫法:

    @Streaming
    @GET("download")
    Call<ResponseBody> downloadFile();
    

3.參數(shù)類注解

  • Headers:添加請求頭谴供,作用于方法

    寫法:

    @Headers("Cache-Control: max-age=640000")
    @GET("public")
    Call<BaseResult<List<User>>> getUser();
    

    @Headers({
        "Cache-Control: max-age=640000"
        "User-Agent: Retrofit-Sample-App"
    })
    @GET("public")
    Call<BaseResult<List<User>>> getUser();
    

    ?

  • Header:用于動態(tài)添加頭部块茁,作用于方法參數(shù)

    寫法:

    @GET("public")
    Call<BaseResult<List<User>>> getUser(@Header("Token") String token);
    
  • Body:用于非表單請求體,作用于方法參數(shù)

    寫法:

    @POST("user")
    Call<BaseResult<String>> addUser(@Body User user);
    
  • Url:用于動態(tài)改變Url桂肌,作用于方法參數(shù)

    寫法:

    @GET("public")
    Call<BaseResult<List<User>>> getUser(@Url String url);
    

    請求的時候数焊,url會替換掉public

  • Path:用于替換請求地址,作用于方法參數(shù)

    寫法:

    @GET("{path}")
    Call<BaseResult<List<User>>> getUser(@Path("path") String path);
    
  • Field:用于表單字段參數(shù)崎场,(需要配合FormUrlEncoded使用)作用于方法參數(shù)

    寫法:

    @FormUrlEncoded
    @POST("public")
    Call<BaseResult> addUser(@Field("userName") String userName);
    
  • FieldMap:用于表單字段參數(shù)佩耳,接收Map實(shí)現(xiàn)多個參數(shù),(需要配合FormUrlEncoded使用)作用于方法參數(shù)

    寫法:

    @FormUrlEncoded
    @POST("public")
    Call<BaseResult> addUser(@FieldMap Map<String,String> fieldMap);
    
  • Part:用于表單字段參數(shù)谭跨,適用于文件上傳干厚,(需要配合Multipart使用)作用于方法參數(shù)

    寫法:

    @Multipart
    @POST("public")
    Call<BaseResult> uploadFile(@Part MultipartBody.Part file);
    
  • PartMap:用于表單字段參數(shù),適用于文件上傳螃宙,(需要配合Multipart使用)作用于方法參數(shù)

    寫法:

    @Multipart
    @POST("public")
    Call<BaseResult> uploadFile(@PartMap Map<String,RequestBody> RequestBodyMap);
    
  • Query:用于條件字段參數(shù)蛮瞄,作用于方法參數(shù)

    寫法:

    @GET("public")
    Call<BaseResult<List<User>>> getUser(@Query("userId") String userId);
    
  • QueryMap:用于條件字段參數(shù),作用于方法參數(shù)

    寫法:

    @GET("public")
    Call<BaseResult<List<User>>> getUser(@QueryMap Map<String,String> map);
    

注:如果使用Post請求方式谆扎,建議使用Field或FieldMap+FormUrlEncoded傳遞參數(shù)挂捅,雖然Query或QueryMap也可以實(shí)現(xiàn),但是Query或QueryMap都是將參數(shù)拼接在url后面的堂湖,而@Field或@FieldMap傳遞的參數(shù)時放在請求體的

Retrofit的配置

1.為Retrofit添加Converter

Retrofit中提供了Converter的概念籍凝,直譯為轉(zhuǎn)換器,Retrofit正常請求下來后苗缩,響應(yīng)體為ResponseBody類型饵蒂,我們需要將ResponseBody解析后才能得到我們想要的數(shù)據(jù),那么如果我們想要直接在響應(yīng)的時候拿到我們想要的數(shù)據(jù)怎么辦呢酱讶?這時候我們就需要Converter來幫我們進(jìn)行轉(zhuǎn)換了退盯。Retrofit提供了幾個轉(zhuǎn)換器,如下表:

依賴庫 Gradle引用 來源
Gson com.squareup.retrofit2:converter-gson 官方
Jackson com.squareup.retrofit2:converter-jackson 官方
Moshi com.squareup.retrofit2:converter-moshi 官方
Protobuf com.squareup.retrofit2:converter-protobuf 官方
Wire com.squareup.retrofit2:converter-wire 官方
Simple Framework com.squareup.retrofit2:converter-simpleframework 官方
Scalars com.squareup.retrofit2:converter-scalars 官方
LoganSquare com.github.aurae.retrofit2:converter-logansquare 第三方
FastJson org.ligboy.retrofit2:converter-fastjson 或org.ligboy.retrofit2:converter-fastjson-android 第三方

我們用Gson來介紹一下如何使用,首先引入Gson的Converter:

 com.squareup.retrofit2:converter-gson:2.1.0

然后通過Retrofit.Builder()配置添加Converter:

Retrofit retrofit=new Retrofit.Builder()
        .addConverterFactory(GsonConverterFactory.create())
        .baseUrl("http://192.168.1.79:8080/")
        .build();

RetrofitApi retrofitApi=retrofit.create(RetrofitApi.class);

接著讓我們來測試一下吧:

接口返回?cái)?shù)據(jù)格式為:

{
    "status": "1",
    "message": "success",
    "data": {
        "name": "zyyoona7",
        "age": "18",
        "height": "180cm"
    }
}

兩個實(shí)體類分別為BaseResult和User:

//BaseResult類
public class BaseResult<T> {

    public static final int FAILURE = 0; // 失敗
    public static final int SUCCESS = 1; // 成功

    private int status;  // 返回狀態(tài):0 失敗   1 成功
    private String message;  // 返回信息
    private T data;  // 包裝的對象
    //...省略getter setter
}

//User類
public class User implements Parcelable {

    private String name;
    private int age;
    private String height;
  
    //省略getter setter
}

請求接口:

@GET("public")
Call<BaseResult<User>> getUser();

代碼調(diào)用:

RetrofitApi retrofitApi=retrofit.create(RetrofitApi.class);

Call<BaseResult<User>> userCall=retrofitApi.getUser();
userCall.enqueue(new Callback<BaseResult<User>>() {
            @Override
            public void onResponse(Call<BaseResult<User>> call, Response<BaseResult<User>> response) {
                //直接解析出我們想要的數(shù)據(jù)
            }

            @Override
            public void onFailure(Call<BaseResult<User>> call, Throwable t) {

            }
        });

2.為Retrofit添加RxJava支持

支持RxJava簡直就是Retrofit的大招哇渊迁,用起來超級酷慰照,如果你對RxJava還不了解請移步RxJava學(xué)習(xí)資料鏈接,是時候用起RxJava了琉朽。我們來配置一下毒租,使Retrofit支持RxJava:

首先導(dǎo)入:

compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0'
compile 'io.reactivex:rxandroid:1.2.1'
compile 'io.reactivex:rxjava:1.1.6'

然后通過Retrofit.Builder配置:

Retrofit retrofit=new Retrofit.Builder()
        .addConverterFactory(GsonConverterFactory.create())
       //RxJava支持
      .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
      .baseUrl("http://192.168.1.79:8080/")
        .build();

RetrofitApi retrofitApi=retrofit.create(RetrofitApi.class);

接著接口的返回值需要轉(zhuǎn)變一下:

@GET("public")
Observable<BaseResult<User>> getUser();

這時候就可以開車了:

retrofitApi.getUser()
        .flatMap(new Func1<BaseResult<User>, Observable<User>>() {
            @Override
            public Observable<User> call(BaseResult<User> userBaseResult) {
                return Observable.just(userBaseResult.getData());
            }
        })
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(new Subscriber<User>() {
            @Override
            public void onCompleted() {

            }

            @Override
            public void onError(Throwable e) {

            }

            @Override
            public void onNext(User user) {
                Log.e(TAG, "onNext: " + user);
            }
        });

3.為Retrofit添加日志攔截器

Retrofit是很強(qiáng)大,但是調(diào)試的時候想要看到請求和響應(yīng)的信息怎么辦呢箱叁?Retrofit是一個封裝墅垮,它依賴了OkHttp作為客戶端,從源碼中可以看出:

 public Retrofit build() {
      if (baseUrl == null) {
        throw new IllegalStateException("Base URL required.");
      }

      okhttp3.Call.Factory callFactory = this.callFactory;
      if (callFactory == null) {
        callFactory = new OkHttpClient();
      }
   //...省略
 }

從方法中看出耕漱,如果callFactory為null則會創(chuàng)建新的OkHttpClient算色,而且在Builder中提供了client()方法,可以讓我們重新設(shè)置Client螟够。這樣我們可以通過給OkHttpClient添加攔截器來實(shí)現(xiàn)打印日志灾梦。

官方給我們提供了一個攔截器:

compile 'com.squareup.okhttp3:logging-interceptor:3.4.1'

在Retrofit2.1.0的版本中,如果我們的項(xiàng)目不導(dǎo)入OkHttp妓笙,默認(rèn)將會導(dǎo)入OkHttp-3.3.0和okio-1.8.0若河,我們可以導(dǎo)入最新的OkHttp:

compile 'com.squareup.okhttp3:okhttp:3.4.1'

創(chuàng)建HttpLoggingInterceptor對象:

HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);

打印日志有四個級別:

  • Level.BODY:打印請求頭,請求體和響應(yīng)頭寞宫,響應(yīng)體的所有內(nèi)容
  • Level.HEADERS:打印請求和響應(yīng)的頭部信息
  • Level.BASIC:打印基本信息
  • Level.NONE:無打印

配置到Retrofit中:

Retrofit retrofit=new Retrofit.Builder()
        .addConverterFactory(GsonConverterFactory.create())
        .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
        //配置OkHttpClient
        .client(new OkHttpClient().newBuilder().addInterceptor(loggingInterceptor).build())
        .baseUrl("http://192.168.1.79:8080/")
        .build();

RetrofitApi retrofitApi=retrofit.create(RetrofitApi.class);

這樣我們就可以暢爽的看日志信息了~~~

4.為Retrofit添加請求頭

在項(xiàng)目中或多或少都會用到請求頭牡肉,比如登錄的token之類的,具體看服務(wù)器端如何規(guī)定淆九,上面再介紹注解的時候我們已經(jīng)看到了可以通過@Headers或者@Header來添加頭部信息(具體寫法請看Retrofit的注解第3小節(jié))。這兩種寫法只是給特定的接口添加毛俏,如果接口多的話那手指就得抽筋了炭庙,有沒有統(tǒng)一添加頭部的方法呢?答案是肯定的煌寇。依然通過給OkHttp添加攔截器的方式焕蹄。

自定義HeaderInterceptor:

public class HeaderInterceptor implements Interceptor {

    private Map<String, String> headers = new HashMap<>();

    public HeaderInterceptor(Map<String, String> headers) {
        this.headers = headers;
    }

    @Override
    public Response intercept(Chain chain) throws IOException {

        Request.Builder builder = chain.request().newBuilder();
        for (String key : headers.keySet()) {
            builder.addHeader(key, headers.get(key));
        }
        Request request = builder.build();
        return chain.proceed(request);
    }

然后就可以愉快的使用了:

Map<String,String> headerMap=new HashMap<>();
headerMap.put("token","123456");
//...more
HeaderInterceptor headerInterceptor=new HeaderInterceptor();

Retrofit retrofit=new Retrofit.Builder()
        .addConverterFactory(GsonConverterFactory.create())
        .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
        //配置OkHttpClient
        .client(new OkHttpClient().newBuilder()
                .addInterceptor(loggingInterceptor)
                .addInterceptor(headerInterceptor)
                .build())
        .baseUrl("http://192.168.1.79:8080/")
        .build();

RetrofitApi retrofitApi=retrofit.create(RetrofitApi.class);

更多使用可以猛戳:https://github.com/square/retrofit/wiki/Retrofit-Tutorials

Demo努力趕制中...其他篇幅努力編寫中...

參考

如何使用Retrofit請求非Restful API
你真的會用Retrofit2嗎?Retrofit2完全教程
Retrofit官網(wǎng)
Retrofit Wiki

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市阀溶,隨后出現(xiàn)的幾起案子腻脏,更是在濱河造成了極大的恐慌,老刑警劉巖银锻,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件永品,死亡現(xiàn)場離奇詭異,居然都是意外死亡击纬,警方通過查閱死者的電腦和手機(jī)鼎姐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人炕桨,你說我怎么就攤上這事饭尝。” “怎么了献宫?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵钥平,是天一觀的道長。 經(jīng)常有香客問我姊途,道長涉瘾,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任吭净,我火速辦了婚禮睡汹,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘寂殉。我一直安慰自己囚巴,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布友扰。 她就那樣靜靜地躺著彤叉,像睡著了一般。 火紅的嫁衣襯著肌膚如雪村怪。 梳的紋絲不亂的頭發(fā)上秽浇,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天,我揣著相機(jī)與錄音甚负,去河邊找鬼柬焕。 笑死,一個胖子當(dāng)著我的面吹牛梭域,可吹牛的內(nèi)容都是我干的斑举。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼病涨,長吁一口氣:“原來是場噩夢啊……” “哼富玷!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起既穆,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤赎懦,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后幻工,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體励两,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年囊颅,在試婚紗的時候發(fā)現(xiàn)自己被綠了伐蒋。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片工三。...
    茶點(diǎn)故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖先鱼,靈堂內(nèi)的尸體忽然破棺而出俭正,到底是詐尸還是另有隱情,我是刑警寧澤焙畔,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布掸读,位于F島的核電站,受9級特大地震影響宏多,放射性物質(zhì)發(fā)生泄漏儿惫。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一伸但、第九天 我趴在偏房一處隱蔽的房頂上張望肾请。 院中可真熱鬧,春花似錦更胖、人聲如沸铛铁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽饵逐。三九已至,卻和暖如春彪标,著一層夾襖步出監(jiān)牢的瞬間倍权,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工捞烟, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留薄声,地道東北人。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓题画,卻偏偏與公主長得像默辨,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子婴程,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)抱婉,斷路器档叔,智...
    卡卡羅2017閱讀 134,599評論 18 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,499評論 25 707
  • 整體Retrofit內(nèi)容如下: 1、Retrofit解析1之前哨站——理解RESTful2蒸绩、Retrofit解析2...
    隔壁老李頭閱讀 3,972評論 8 19
  • 音級 樂音體系中的各個音衙四,如1234567 就是各個音級。 全音和半音 把一個八度的距離等分為12等份患亿,每一份就是...
    debugman007閱讀 1,774評論 0 2
  • 十來年沒有看過電視传蹈,對電影卻一直非常喜歡押逼。喜歡電影,第一是節(jié)省時間惦界,看兩部電視劇或一個綜藝節(jié)目的時間挑格,就足以去看一...
    劉建強(qiáng)閱讀 208評論 0 0