1. 程式人生 > >Android Retrofit2.0-post&get請求&檔案上傳&結合Rxjava

Android Retrofit2.0-post&get請求&檔案上傳&結合Rxjava

  1. 可以配置不同HTTP client來實現網路請求,如okhttp、httpclient等
  2. 請求的方法引數註解都可以定製
  3. 支援同步、非同步和RxJava
  4. 超級解耦
  5. 可以配置不同的反序列化工具來解析資料,如json、xml等
  6. 使用非常方便靈活
  7. 框架使用了很多設計模式(感興趣的可以看看原始碼學習學習)

缺點:

  1. 不能接觸序列化實體和響應資料
  2. 執行的機制太嚴格
  3. 使用轉換器比較低效
  4. 只能支援簡單自定義引數型別

相關學習資料的網址

環境配置

在builde.gradle裡面新增上

compile 'com.squareup.retrofit2:retrofit:2.1.0'
compile 'com.squareup.retrofit2:converter-gson:2.1.0'
compile 'com.squareup.okhttp3:okhttp:3.4.1'

在AndroidManifest.xml新增所需許可權

<uses-permission android:name="android.permission.INTERNET" />

基本使用

  • get非同步請求

public interface GitHubService {
  @GET("users/{user}/repos")
  Call<ResponseBody> listRepos(@Path("user") String user);
}
Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.github.com/")
    .addConverterFactory(GsonConverterFactory.create()) 
    .build();
GitHubService service = retrofit.create(GitHubService.class);
Call<ResponseBody> repos = service.listRepos("octocat");
repos.enqueue(new Callback<ResponseBody>() {
            @Override
            public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
                Log.e("APP",response.body().source().toString());
            }
            @Override
            public void onFailure(Call<ResponseBody> call, Throwable t) {
                t.printStackTrace();
            }
        });

.baseUrl設定最基本url,也就是http請求的url字首,可以把專案中重複的字首用這個來設定

.addConverterFactory(GsonConverterFactory.create())是新增Gson資料解析ConverterFactory,後面會專門介紹下這個,這裡就不做過多解釋

ResponseBody 這個是okhttp裡面的物件,可以直接返回整個字串,也可以獲取流的形式

  • post非同步請求

POST與GET實現基本上是一樣的,只是把註解GET換成POST就OK.為了測試POST,專門去網上找了個介面測試,下面就分享給大家,既可以用GET也可以用POST請求

http://www.kuaidi100.com/query?type=快遞公司代號&postid=快遞單號 
ps:快遞公司編碼:申通="shentong" EMS="ems" 順豐="shunfeng" 圓通="yuantong" 中通="zhongtong" 韻達="yunda" 天天="tiantian" 匯通="huitongkuaidi" 全峰="quanfengkuaidi" 德邦="debangwuliu" 宅急送="zhaijisong"

拿著這個介面來實現一下POST非同步請求

public interface GitHubService {
    @POST("query")
    Call<PostQueryInfo> search(@Query("type") String type, @Query("postid") String postid);
}
public class PostQueryInfo {
    private String message;
    private String nu;
    private String ischeck;
    private String com;
    private String status;
    private String condition;
    private String state;
    private List<DataBean> data;
    public static class DataBean {
        private String time;
        private String context;
        private String ftime;
    }
}
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("http://www.kuaidi100.com/")
                .addConverterFactory(GsonConverterFactory.create()) 
                .build();
        GitHubService apiService = retrofit.create(GitHubService.class);
        Call<PostQueryInfo> call = apiService.search("yuantong","500379523313");
        call.enqueue(new Callback<PostQueryInfo>(){
            @Override
            public void onResponse(Call<PostQueryInfo> call,Response<PostQueryInfo> response){
                 Log.e("APP",response.body().getNu());
            }
            @Override
            public void onFailure(Call<PostQueryInfo> call,Throwable t){
                t.printStackTrace();
            }
        });

PostQueryInfo實體類省略了get和set方法,大家可以自行快捷鍵,最終列印返回回來的快遞單號,實現POST非同步請求就是這麼簡單

http://www.bejson.com/knownjson/webInterface/這網站裡面還有一些其它免費介面,感興趣的可以去看看

  • 常用註解的使用介紹

上面GitHubService裡面的註解大家應該都能猜它的作用了吧,下面就給大家介紹下

@GET@POST分別是get和post請求。括號裡面的value值與上面.baseUrl組成完整的路徑

@Path動態的URL訪問。像上面get請求中的{user}可以把它當做一個佔位符,通過@Path("user")標註的引數進行替換

@Query請求引數。無論是GET或POST的引數都可以用它來實現

@QueryMap請求引數使用Map集合。可以傳遞一個map集合物件

@Body實體請求引數。顧名思義可以傳遞一個實體物件來作為請求的引數,不過實體屬性要與引數名一一致

@FormUrlEncoded@Field簡單的表單鍵值對。兩個需要結合使用,使用如下:

@FormUrlEncoded
@POST("user/edit")
Call<User> updateUser(@Field("first_name") String first, @Field("last_name") String last);

@Multipart@PartPOST表單的方式上傳檔案可以攜帶引數。兩個需要結合使用,使用方式檢視下面檔案上傳中介紹。

@PartMap@PartPOST表單上傳多個檔案攜帶引數。兩個結合使用,使用方式檢視下面檔案上傳中介紹。

這裡只介紹了一些常用的,大家如果想了解更多可以檢視相關文件

  • 檔案上傳

    1、單檔案上傳攜帶引數(使用註解@Multipart@Part),需要在手機SD卡目錄下的Pictures資料夾下新增xuezhiqian.png圖片
@Multipart
@POST("UploadServlet")
Call<ResponseBody> uploadfile(@Part MultipartBody.Part photo, @Part("username") RequestBody username, @Part("password") RequestBody password);
Retrofit retrofitUpload = new Retrofit.Builder()
                .baseUrl("http://192.168.1.8:8080/UploadFile/")
                .addConverterFactory(GsonConverterFactory.create())
                .build();
GitHubService service = retrofitUpload.create(GitHubService.class);
File file = new File(Environment.getExternalStorageDirectory()+"/Pictures", "xuezhiqian.png");
//設定Content-Type:application/octet-stream
RequestBody photoRequestBody = RequestBody.create(MediaType.parse("application/octet-stream"), file);
//設定Content-Disposition:form-data; name="photo"; filename="xuezhiqian.png"
MultipartBody.Part photo = MultipartBody.Part.createFormData("photo", file.getName(), photoRequestBody);
//新增引數使用者名稱和密碼,並且是文字型別
RequestBody userName = RequestBody.create(MediaType.parse("text/plain"), "abc");
RequestBody passWord = RequestBody.create(MediaType.parse("text/plain"), "123"); 
Call<ResponseBody> loadCall = service.uploadfile(photo, userName,passWord);
loadCall.enqueue(new Callback<ResponseBody>() {
    @Override
    public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
        Log.e("APP", response.body().source().toString());
    }
    @Override
    public void onFailure(Call<ResponseBody> call, Throwable t) {
        t.printStackTrace();
    }
});

2、多檔案上傳攜帶引數(使用註解@PartMap@Part),需要再在手機SD卡目錄下的Pictures資料夾下新增xuezhiqian2.png圖片

@Multipart
@POST("UploadServlet")
Call<ResponseBody> uploadfile(@PartMap Map<String, RequestBody> params,  @Part("password") RequestBody password);
Retrofit retrofitUpload = new Retrofit.Builder()
                .baseUrl("http://192.168.1.8:8080/UploadFile/")
                .addConverterFactory(GsonConverterFactory.create())
                .build();
GitHubService service = retrofitUpload.create(GitHubService.class);
File file = new File(Environment.getExternalStorageDirectory()+"/Pictures", "xuezhiqian.png");
File file2 = new File(Environment.getExternalStorageDirectory()+"/Pictures", "xuezhiqian2.png");
RequestBody photoRequestBody = RequestBody.create(MediaType.parse("application/octet-stream"), file);
RequestBody photoRequestBody2 = RequestBody.create(MediaType.parse("application/octet-stream"), file2);  
RequestBody userName = RequestBody.create(MediaType.parse("text/plain"), "abc");
RequestBody passWord = RequestBody.create(MediaType.parse("text/plain"), "123"); 
Map<String,RequestBody> photos = new HashMap<>();
//新增檔案一
photos.put("photos\"; filename=\""+file.getName(), photoRequestBody);
//新增檔案二
photos.put("photos\"; filename=\""+file2.getName(), photoRequestBody2);
//新增使用者名稱引數
photos.put("username",  userName);
Call<ResponseBody> loadCall = service.uploadfile(photos, passWord);
loadCall.enqueue(new Callback<ResponseBody>() {
    @Override
    public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
        Log.e("APP", response.body().source().toString());
    }
    @Override
    public void onFailure(Call<ResponseBody> call, Throwable t) {
        t.printStackTrace();
    }
});

要說明的是,多個檔案上傳不能像單個檔案上傳使用MultipartBody.Part物件,而是使用註解@PartMap新增多個RequestBody拼接filename來實現,@part("?")@Multipart註解已經幫我們設定好成Content-Disposition:form-data; name="?"這樣子了,@part裡的問號對應name後面的問號,而我們要新增多個檔案,則需要在name裡面作文章。所以就有了上面MAP集合中的KEY的拼接字串,我們想設定成Content-Disposition:form-data; name="photo"; filename="xuezhiqian.png",MAP集合KEY的值設定為photo"; filename="xuezhiqian.png替換name後面的問號就OK了,裡面的引號在使用的時候需要加上反斜槓轉義巢狀使用。

一直找不到免費上傳檔案的介面來做測試,我就花了點時間自己做了一個,程式碼也是參考網上的,這裡也給大家貼一下後臺核心接收檔案儲存的程式碼,希望對大家有所幫助,自己動手豐衣足食。

    /**
     * 上傳檔案
     * @param request
     * @param response
     * @throws IOException
     */
    @SuppressWarnings("unchecked")
    private void uploadFile(HttpServletRequest request, HttpServletResponse response) throws IOException {
        response.setContentType("text/html;charset=utf-8");// 設定響應編碼
        request.setCharacterEncoding("utf-8");
        PrintWriter writer = response.getWriter();// 獲取響應輸出流
        //ServletInputStream inputStream = request.getInputStream();// 獲取請求輸入流
        /*
         * 1、建立DiskFileItemFactory物件,設定緩衝區大小和臨時檔案目錄 該類有兩個構造方法一個是無參的構造方法,
         * 另一個是帶兩個引數的構造方法
         * 
         * @param int sizeThreshold,該引數設定記憶體緩衝區的大小,預設值為10K。當上傳檔案大於緩衝區大小時,
         * fileupload元件將使用臨時檔案快取上傳檔案
         * 
         * @param java.io.File
         * repository,該引數指定臨時檔案目錄,預設值為System.getProperty("java.io.tmpdir");
         * 
         * 如果使用了無參的構造方法,則使用setSizeThreshold(int
         * sizeThreshold),setRepository(java.io.File repository) 方法手動進行設定
         */
        DiskFileItemFactory factory = new DiskFileItemFactory();
        int sizeThreshold = 1024 * 1024;
        factory.setSizeThreshold(sizeThreshold);
        File repository = new File(request.getSession().getServletContext().getRealPath("temp"));
        // System.out.println(request.getSession().getServletContext().getRealPath("temp"));
        // System.out.println(request.getRealPath("temp"));
        factory.setRepository(repository);
        /*
         * 2、使用DiskFileItemFactory物件建立ServletFileUpload物件,並設定上傳檔案的大小
         * 
         * ServletFileUpload物件負責處理上傳的檔案資料,並將表單中每個輸入項封裝成一個FileItem 該物件的常用方法有:
         * boolean isMultipartContent(request);判斷上傳表單是否為multipart/form-data型別
         * List parseRequest(request);解析request物件,並把表單中的每一個輸入項包裝成一個fileItem
         * 物件,並返回一個儲存了所有FileItem的list集合 void setFileSizeMax(long
         * filesizeMax);設定單個上傳檔案的最大值 void setSizeMax(long sizeMax);設定上傳溫江總量的最大值
         * void setHeaderEncoding();設定編碼格式,解決上傳檔名亂碼問題
         */
        ServletFileUpload upload = new ServletFileUpload(factory);
        upload.setHeaderEncoding("utf-8");// 設定編碼格式,解決上傳檔名亂碼問題
        /*
         * 3、呼叫ServletFileUpload.parseRequest方法解析request物件,得到一個儲存了所有上傳內容的List物件
         */
        List<FileItem> parseRequest = null;
        try {
            parseRequest = upload.parseRequest(request);
        } catch (FileUploadException e) {
            e.printStackTrace();
        }
        /*
         * 4、對list進行迭代,每迭代一個FileItem物件,呼叫其isFormField方法判斷是否是檔案上傳
         * true表示是普通表單欄位,則呼叫getFieldName、getString方法得到欄位名和欄位值
         * false為上傳檔案,則呼叫getInputStream方法得到資料輸入流,從而讀取上傳資料
         * FileItem用來表示檔案上傳表單中的一個上傳檔案物件或者普通的表單物件 該物件常用方法有: boolean
         * isFormField();判斷FileItem是一個檔案上傳物件還是普通表單物件 true表示是普通表單欄位,
         * 則呼叫getFieldName、getString方法得到欄位名和欄位值 false為上傳檔案,
         * 則呼叫getName()獲得上傳檔案的檔名,注意:有些瀏覽器會攜帶客戶端路徑,需要自己減除
         * 呼叫getInputStream()方法得到資料輸入流,從而讀取上傳資料 
         * delete();表示在關閉FileItem輸入流後,刪除臨時檔案。
         */
        for (FileItem fileItem : parseRequest) {
            if (fileItem.isFormField()) {// 表示普通欄位
                if ("username".equals(fileItem.getFieldName())) {
                    String username = fileItem.getString();
                    writer.write("您的使用者名稱:" + username + "<br>");
                }
                if ("password".equals(fileItem.getFieldName())) {
                    String userpass = fileItem.getString();
                    writer.write("您的密碼:" + userpass + "<br>");
                }
            } else {// 表示是上傳的檔案
                    // 不同瀏覽器上傳的檔案可能帶有路徑名,需要自己切割
                String clientName = fileItem.getName();
                String filename = "";
                if (clientName.contains("\\\\\\\\")) {// 如果包含"\\\\"表示是一個帶路徑的名字,則擷取最後的檔名
                    filename = clientName.substring(clientName.lastIndexOf("\\\\\\\\")).substring(1);
                } else {
                    filename = clientName;
                }
                UUID randomUUID = UUID.randomUUID();// 生成一個128位長的全球唯一標識
                filename = randomUUID.toString() + filename;
                /*
                 * 設計一個目錄生成演算法,如果所用使用者上傳的檔案總數是億數量級的或更多,放在同一個目錄下回導致檔案索引非常慢,
                 * 所以,設計一個目錄結構來分散存放檔案是非常有必要,且合理的 將UUID取雜湊演算法,雜湊到更小的範圍,
                 * 將UUID的hashcode轉換為一個8位的8進位制字串,
                 * 從這個字串的第一位開始,每一個字元代表一級目錄,這樣就構建了一個八級目錄,每一級目錄中最多有16個子目錄
                 * 這無論對於伺服器還是作業系統都是非常高效的目錄結構
                 */
                int hashUUID = randomUUID.hashCode();
                String hexUUID = Integer.toHexString(hashUUID);
                // System.out.println(hexUUID);
                // 獲取將上傳的檔案存儲存在哪個資料夾下的絕對路徑
                String filepath = request.getSession().getServletContext().getRealPath("upload");
                System.out.println("filePath="+filepath);
                for (char c : hexUUID.toCharArray()) {
                    filepath = filepath + "/" + c;
                }
                // 如果目錄不存在就生成八級目錄
                File filepathFile = new File(filepath);
                if (!filepathFile.exists()) {
                    filepathFile.mkdirs();
                }
                // 從Request輸入流中讀取檔案,並寫入到伺服器
                InputStream inputStream2 = fileItem.getInputStream();
                // 在伺服器端建立檔案
                File file = new File(filepath + "/" + filename);
                BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file));
                byte[] buffer = new byte[10 * 1024];
                int len = 0;
                while ((len = inputStream2.read(buffer, 0, 10 * 1024)) != -1) {
                    bos.write(buffer, 0, len);
                }
                writer.write("您上傳檔案" + clientName + "成功<br>");
                // 關閉資源
                bos.close();
                inputStream2.close();
            }
        }
        // 注意Eclipse的上傳的檔案是儲存在專案的執行目錄,而不是workspace中的工程目錄裡。
    }

其實很簡單就是一個servelt,利用了commons-fileupload.jar和commons-io.jar兩個庫來實現,兩個庫網上都可以找到

  • 檔案下載

可以採用OKHTTP下載檔案的方式,利用ResponseBody物件,呼叫response.body().byteStream()方法獲取InputStream輸入流,通過寫檔案操作來實現。

  • 同步請求和結合RxJava的使用

1、同步請求

Call.execute()同步請求網路,要注意的是Android4.0以後不能在主執行緒裡呼叫,要開一個非同步執行緒來使用,
Call.enqueue()非同步請求網路,加入一個回撥,同步非同步需要可按照不同的場景來使用。
Call.cancel()取消此次請求,有一些場景還是會用到該方法的。

2、結合RxJava使用

新增RxJava環境,在builde.gradle裡面新增上

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

我們就拿上面post非同步請求改成RxJava模式

@POST("query")
Observable<PostQueryInfo> searchRx(@Query("type") String type, @Query("postid") String postid);
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("http://www.kuaidi100.com/")
                 //新增資料解析ConverterFactory
                .addConverterFactory(GsonConverterFactory.create()) 
                 //新增RxJava
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())   
                .build();
        GitHubService apiService = retrofit.create(GitHubService.class);
        apiService.searchRx("yuantong","500379523313")
                //訪問網路切換非同步執行緒
                .subscribeOn(Schedulers.io())
                //響應結果處理切換成主執行緒
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Subscriber<PostQueryInfo>() {
                    @Override
                    public void onCompleted() {
                         //請求結束回撥
                    }
                    @Override
                    public void onError(Throwable e) {
                         //錯誤回撥
                        e.printStackTrace();
                    }
                    @Override
                    public void onNext(PostQueryInfo postQueryInfo) {
                         //成功結果返回
                        Log.e("APP",postQueryInfo.getNu());
                    }
                });

可能有些童鞋沒接觸過RxJava,這裡瞭解一下就可以,後面我會單獨寫一篇關於RxJava的文章,到時可以再回來看看。

配置設定

  • 配置OKHttp

HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient client = new OkHttpClient.Builder()
                .connectTimeout(10000L,TimeUnit.MILLISECONDS)       //設定連線超時
                .readTimeout(10000L,TimeUnit.MILLISECONDS)          //設定讀取超時
                .writeTimeout(10000L,TimeUnit.MILLISECONDS)         //設定寫入超時
                .cache(new Cache(getCacheDir(),10 * 1024 * 1024))   //設定快取目錄和10M快取
                .addInterceptor(interceptor)    //新增日誌攔截器(該方法也可以設定公共引數,頭資訊)
                .build();
Retrofit retrofit = new Retrofit.Builder()
                .client(client)        //設定OkHttp
                .baseUrl("http://www.kuaidi100.com/")
                .addConverterFactory(GsonConverterFactory.create()) //  新增資料解析ConverterFactory
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())   //新增RxJava
                .build();
...省略後面的程式碼

日誌攔截器需要新增OkHttp對應庫,okhttp的庫是3.4.1,這裡也需要設成3.4.1

compile 'com.squareup.okhttp3:logging-interceptor:3.4.1'
  • 多種ConverterFactory設定

常見的ConverterFactory有

Gson: com.squareup.retrofit2:converter-gson:2.1.0
Jackson: com.squareup.retrofit2:converter-jackson:2.1.0
Moshi: com.squareup.retrofit2:converter-moshi:2.1.0
Protobuf: com.squareup.retrofit2:converter-protobuf:2.1.0
Wire: com.squareup.retrofit2:converter-wire:2.1.0
Simple XML: com.squareup.retrofit2:converter-simplexml:2.1.0
Scalars (primitives, boxed, and String): com.squareup.retrofit2:converter-scalars:2.1.0

新增對應得ConverterFactory只需要先在builde.gradle裡面配置好上面對應的庫,然後通過.addConverterFactory該方法新增即可

  • 新增混淆

# Platform calls Class.forName on types which do not exist on Android to determine platform.
-dontnote retrofit2.Platform
# Platform used when running on RoboVM on iOS. Will not be used at runtime.
-dontnote retrofit2.Platform$IOS$MainThreadExecutor
# Platform used when running on Java 8 VMs. Will not be used at runtime.
-dontwarn retrofit2.Platform$Java8
# Retain generic type information for use by reflection by converters and adapters.
-keepattributes Signature
# Retain declared checked exceptions for use by a Proxy instance.
-keepattributes Exceptions

介紹兩個好用好玩的AS外掛

1、GsonFormat JSON解析成物件
2、Sexy Editor 工作區間背景設定
都可以在Preferences下Plugins裡搜尋到

寫在最後的話:
這篇文章還是花了蠻長時間的,包括期間有一些其它事情耽擱了,加上還是邊學邊寫變測試,雖然花的時間有點長但是我覺得還是學到了蠻多東西的,我相信我能一直堅持學下去,一直寫下去。篇幅有點長謝謝大家能夠看到最後!

成功源於不斷的學習和積累