Retrofit2.0使用姊妹篇——帶進度下載文件

Retrofit是目前最主流的網絡框架了,它對網絡請求幾近完美的封裝,大大降低了我們開發者的研發難度,縮短了研發週期。最近項目中遇到了下載視頻和圖片文件的需求(還有上傳視頻和圖片的需求,請移步這篇博客),我第一反應是用retrofit做呀,so easy!產品接着說,要帶下載進度條哦!我一想,retrofit好像並沒有給我們提供顯示下載進度的接口,哎呀,看來還是得自己個兒整整嘍!接下來,我把自己實現Retrofit帶進度下載文件的流程分享給大家。

想看源碼的請移步github:https://github.com/kb18519142009/DownloadFile
大家喜歡的話,就給個star^_^,有問題或者建議,可以直接提issues,也可以在博客下面給我留言。謝謝~

先看看效果圖:

效果圖

在demo中我分別實現了視頻和圖片的下載,並附帶有下載進度顯示,視頻下載完成後運用exo播放器直接播放的,圖片只是用Glide簡單展示了一下。好了,我們步入正題吧!

一、添加依賴

在app的build.gradle的dependencies節點中添加以下代碼:

implementation 'com.squareup.retrofit2:retrofit:2.3.0'
implementation 'com.google.android.exoplayer:exoplayer:r2.5.4'
implementation 'com.github.bumptech.glide:glide:4.3.1'

俗話說的好,工欲善其事必先利器!我們分別添加Retrofit、exoplayer和glide的依賴,可能有朋友要問了,“implementation ”這是什麼玩意呀?添依賴不是用compile嗎?ok!兄弟不要急,如果你有這個疑問,很明顯你平日裏吃飯的傢伙什兒已經out了,趕緊去升級Android Studio3.0吧!http://www.android-studio.org/

二、添加權限和動態權限處理

在清單文件AndroidManifest中的manifest節點中添加以下代碼:

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

要實現將文件下載到本地,那必然需要網絡權限和內存的讀寫權限啦!

注意:由於我們用到了寫入內存的權限,所以千萬要注意6.0以上動態權限的申請!我在demo裏用的是自己簡單封裝的權限申請工具類,有興趣的可以直接去看demo源碼,代碼如下:

if (KbPermissionUtils.needRequestPermission()) { //判斷是否需要動態申請權限
            KbPermission.with(this)
                    .requestCode(100)
                    .permission(Manifest.permission.WRITE_EXTERNAL_STORAGE) //需要申請的權限(支持不定長參數)
                    .callBack(new KbPermissionListener() {
                        @Override
                        public void onPermit(int requestCode, String... permission) { //允許權限的回調
                            downloadVideo(); //處理具體下載過程
                        }

                        @Override
                        public void onCancel(int requestCode, String... permission) { //拒絕權限的回調
                            KbPermissionUtils.goSetting(mContext); //跳轉至當前app的權限設置界面
                        }
                    })
                    .send();
        } else {
            downloadVideo(); //處理具體下載過程
        }

三、設計回調

/**
 * Description:
 * Created by kang on 2018/3/9.
 */

public interface DownloadListener {
    void onStart();

    void onProgress(int currentLength);

    void onFinish(String localPath);

    void onFailure();
}

回調中包括下載開始下載進度下載結束下載失敗等四個方法。其中我們在下載進度的回調中返回進度的百分比,在此可以將進度顯示在控件上;在下載結束的回調中返回下載至本地的文件路徑,在此可直接對下載完成的文件進行操作。如果你還有一些個性化的需求,可以自行添加。

四、網絡工具類準備

/**
 * ApiHelper
 * Created by kang on 2018/3/9.
 */
public class ApiHelper {

    private static final String TAG = "ApiHelper";

    private static ApiHelper mInstance;
    private Retrofit mRetrofit;
    private OkHttpClient mHttpClient;

    private ApiHelper() {
        this( 30, 30, 30);
    }

    public ApiHelper( int connTimeout, int readTimeout, int writeTimeout) {
        OkHttpClient.Builder builder = new OkHttpClient.Builder()
                .connectTimeout(connTimeout, TimeUnit.SECONDS)
                .readTimeout(readTimeout, TimeUnit.SECONDS)
                .writeTimeout(writeTimeout, TimeUnit.SECONDS);

        mHttpClient = builder.build();
    }

    public static ApiHelper getInstance() {
        if (mInstance == null) {
            mInstance = new ApiHelper();
        }

        return mInstance;
    }

    public ApiHelper buildRetrofit(String baseUrl) {
        mRetrofit = new Retrofit.Builder()
                .baseUrl(baseUrl)
                .client(mHttpClient)
                .build();
        return this;
    }

    public <T> T createService(Class<T> serviceClass) {
        return mRetrofit.create(serviceClass);
    }

}

這裏我對Retrofit進行了簡單封裝。

/**
 * Description:
 * Created by kang on 2018/3/9.
 */

public interface ApiInterface {
    /**
     * 下載視頻
     *
     * @param fileUrl
     * @return
     */
    @Streaming //大文件時要加不然會OOM
    @GET
    Call<ResponseBody> downloadFile(@Url String fileUrl);
}

注意:對於大文件的操作一定要加@Streaming,否則會出現OOM

五、文件下載工具類準備

/**
 * Description:下載文件工具類
 * Created by kang on 2018/3/9.
 */

public class DownloadUtil {
    private static final String TAG = "DownloadUtil";
    private static final String PATH_CHALLENGE_VIDEO = Environment.getExternalStorageDirectory() + "/DownloadFile";
    //視頻下載相關
    protected ApiInterface mApi;
    private Call<ResponseBody> mCall;
    private File mFile;
    private Thread mThread;
    private String mVideoPath; //下載到本地的視頻路徑

    public DownloadUtil() {
        if (mApi == null) {
        //初始化網絡請求接口
            mApi = ApiHelper.getInstance().buildRetrofit("https://sapi.daishumovie.com/")
                    .createService(ApiInterface.class);
        }
    }

    public void downloadFile(String url, final DownloadListener downloadListener) {
        String name = url;
        //通過Url得到文件並創建本地文件
        if (FileUtils.createOrExistsDir(PATH_CHALLENGE_VIDEO)) {
            int i = name.lastIndexOf('/');//一定是找最後一個'/'出現的位置
            if (i != -1) {
                name = name.substring(i);
                mVideoPath = PATH_CHALLENGE_VIDEO +
                        name;
            }
        }
        if (TextUtils.isEmpty(mVideoPath)) {
            Log.e(TAG, "downloadVideo: 存儲路徑爲空了");
            return;
        }
        //建立一個文件
        mFile = new File(mVideoPath);
        if (!FileUtils.isFileExists(mFile) && FileUtils.createOrExistsFile(mFile)) {
            if (mApi == null) {
                Log.e(TAG, "downloadVideo: 下載接口爲空了");
                return;
            }
            mCall = mApi.downloadFile(url);
            mCall.enqueue(new Callback<ResponseBody>() {
                @Override
                public void onResponse(@NonNull Call<ResponseBody> call, @NonNull final Response<ResponseBody> response) {
                    //下載文件放在子線程
                    mThread = new Thread() {
                        @Override
                        public void run() {
                            super.run();
                            //保存到本地
                            writeFile2Disk(response, mFile, downloadListener);
                        }
                    };
                    mThread.start();
                }

                @Override
                public void onFailure(Call<ResponseBody> call, Throwable t) {
                    downloadListener.onFailure(); //下載失敗
                }
            });
        } else {
            downloadListener.onFinish(mVideoPath); //下載完成
        }
    }
    //將下載的文件寫入本地存儲
    private void writeFile2Disk(Response<ResponseBody> response, File file, DownloadListener downloadListener) {
        downloadListener.onStart();
        long currentLength = 0;
        OutputStream os = null;

        InputStream is = response.body().byteStream(); //獲取下載輸入流
        long totalLength = response.body().contentLength();

        try {
            os = new FileOutputStream(file); //輸出流
            int len;
            byte[] buff = new byte[1024];
            while ((len = is.read(buff)) != -1) {
                os.write(buff, 0, len);
                currentLength += len;
                Log.e(TAG, "當前進度: " + currentLength);
                //計算當前下載百分比,並經由回調傳出
                downloadListener.onProgress((int) (100 * currentLength / totalLength));
                //當百分比爲100時下載結束,調用結束回調,並傳出下載後的本地路徑
                if ((int) (100 * currentLength / totalLength) == 100) {
                    downloadListener.onFinish(mVideoPath); //下載完成
                }
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (os != null) {
                try {
                    os.close(); //關閉輸出流
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (is != null) {
                try {
                    is.close(); //關閉輸入流
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

這一段時我們下載文件的核心代碼,我們來簡單分析一下。首先我在DownloadUtil這個類的構造函數中初始化了網絡請求接口,然後提供了兩個方法,downloadFile和writeFile2Disk,顧名思義第一個是下載文件的方法,第二個是將文件寫入SDCard的方法。

方法一:downloadFile(String url, final DownloadListener downloadListener)

兩個參數:url和downloadListener
url是我們要下載的地址,downloadListener是第三步我們設計的下載回調

先截取url最後一個’/’之後的內容,得到我們將要存儲到本地的文件名,然後創建該文件,通過網絡請求得到Response對象,接着開啓子線程,調用writeFile2Disk方法。

方法二:writeFile2Disk(Response response, File file, DownloadListener downloadListener)

三個參數:Response對象,file和downloadListener
通過Response對象我們可以獲取到InputStream輸入流,file是之前創建好的本地文件夾,downloadListener是第三步我們設計的下載回調

ok!到此我們要開始計算下載百分比了!
通過InputStream is = response.body().byteStream()可以獲取到下載的InputStream輸入流,通過long totalLength = response.body().contentLength()獲取到下載的總長度;再通過file創建輸出流os = new FileOutputStream(file); 此時通過輸入流的read(buff)方法每次讀取固定大小的buff(一般1024即可),再調用輸出流的write方法將buff寫入文件,這是一個while循環,直到將輸入流的字節全部讀取完畢,而正好在每次循環裏,我們可以將讀取的字節數累加,得到當前已下載的字節長度currentLength,(100*currentLength/totalLength)就是當前下載百分比啦,這個時候我們用downloadListener.onProgress回調將進度傳出即可;當進度達到100時,將本地文件地址通過downloadListener.onFinish回調傳出!
最後別忘記在finally中關閉輸入輸出流!

到此,我的Retrofit帶進度下載文件的核心代碼已經介紹完畢了!有木有迫不及待的想要用用試試呢!

六、最後來看看使用

private void downloadPicture() {
        mDownloadUtil = new DownloadUtil();
        mDownloadUtil.downloadFile(PICTURE_URL, new DownloadListener() {
            @Override
            public void onStart() {
                Log.e(TAG, "onStart: ");
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        fl_circle_progress.setVisibility(View.VISIBLE);
                    }
                });

            }

            @Override
            public void onProgress(final int currentLength) {
                Log.e(TAG, "onLoading: " + currentLength);
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        circle_progress.setProgress(currentLength);
                    }
                });

            }

            @Override
            public void onFinish(final String localPath) {
                Log.e(TAG, "onFinish: " + localPath);
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        fl_circle_progress.setVisibility(View.GONE);
                        Glide.with(mContext).load(localPath).into(iv_picture);
                    }
                });
            }

            @Override
            public void onFailure() {
                Log.e(TAG, "onFailure: ");
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        fl_circle_progress.setVisibility(View.GONE);
                    }
                });
            }
        });
    }

這裏我放上的是下載圖片的代碼(視頻、文件等都類似),fl_circle_progress是進度條的父佈局,circle_progress是環形進度條,在onStart中將fl_circle_progress顯示,onProgress中給circle_progress設置進度,onFinish中將fl_circle_progress隱藏,並利用Glide將下載完成的圖片顯示在iv_picture中,iv_picture就是一個imageView,如果下載過程中出錯那就在onFailure中將fl_circle_progress隱藏。因爲對UI的處理需要在UI線程中進行,所以這些處理需要通過runOnUiThread切換線程

到這裏整個下載過程就結束了,是不是很簡單呀!歡迎到github下載源碼:

https://github.com/kb18519142009/DownloadFile

大家喜歡的話,就給個star^_^,有問題或者建議,可以直接提issues,也可以在博客下面給我留言。謝謝~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章