Retrofit+Rxjava實現(xiàn)文件下載進度

前言

最近在學習Retrofit忘巧,雖然Retrofit沒有提供文件下載進度的回調(diào)紫皇,但是Retrofit底層依賴的是OkHttp,實際上所需要的實現(xiàn)OkHttp對下載進度的監(jiān)聽,在OkHttp的官方Demo中祝懂,有一個Progress.java的文件,顧名思義拘鞋。點我查看砚蓬。

準備工作

本文采用Dagger2,Retrofit盆色,RxJava灰蛙。

compile'com.squareup.retrofit2:retrofit:2.0.2'
compile'com.squareup.retrofit2:converter-gson:2.0.2'
compile'com.squareup.retrofit2:adapter-rxjava:2.0.2'
//dagger2
compile'com.google.dagger:dagger:2.6'
apt'com.google.dagger:dagger-compiler:2.6'
//RxJava
compile'io.reactivex:rxandroid:1.2.0'
compile'io.reactivex:rxjava:1.1.5'
compile'com.jakewharton.rxbinding:rxbinding:0.4.0'

改造ResponseBody

okHttp3默認的ResponseBody因為不知道進度的相關信息,所以需要對其進行改造隔躲∧ξ啵可以使用接口監(jiān)聽進度信息。這里采用的是RxBus發(fā)送FileLoadEvent對象實現(xiàn)對下載進度的實時更新蹭越。這里先講改造的ProgressResponseBody障本。

public class ProgressResponseBody extends ResponseBody {
    private ResponseBody responseBody;

    private BufferedSource bufferedSource;
    public ProgressResponseBody(ResponseBody responseBody) {
        this.responseBody = responseBody;
    }

    @Override
    public MediaType contentType() {
        return responseBody.contentType();
    }

    @Override
    public long contentLength() {
        return responseBody.contentLength();
    }

    @Override
    public BufferedSource source() {
        if (bufferedSource == null) {
            bufferedSource = Okio.buffer(source(responseBody.source()));
        }
        return bufferedSource;
    }

    private Source source(Source source) {
        return new ForwardingSource(source) {
            long bytesReaded = 0;
            @Override
            public long read(Buffer sink, long byteCount) throws IOException {
                long bytesRead = super.read(sink, byteCount);
                bytesReaded += bytesRead == -1 ? 0 : bytesRead;
               //實時發(fā)送當前已讀取的字節(jié)和總字節(jié)
                RxBus.getInstance().post(new FileLoadEvent(contentLength(), bytesReaded));
                return bytesRead;
            }
        };
    }
}

呃,OKIO相關知識我也正在學响鹃,這個是從官方Demo中copy的代碼驾霜,只不過中間使用了RxBus實時發(fā)送FileLoadEvent對象。

FileLoadEvent

FileLoadEvent很簡單买置,包含了當前已加載進度和文件總大小粪糙。

public class FileLoadEvent {

    long total;
    long bytesLoaded;

    public long getBytesLoaded() {
        return bytesLoaded;
    }

    public long getTotal() {
        return total;
    }

    public FileLoadEvent(long total, long bytesLoaded) {
        this.total = total;
        this.bytesLoaded = bytesLoaded;
    }
}

RxBus

RxBus 名字看起來像一個庫,但它并不是一個庫忿项,而是一種模式蓉冈,它的思想是使用 RxJava 來實現(xiàn)了 EventBus ,而讓你不再需要使用OTTO或者 EventBus轩触。點我查看詳情寞酿。

public class RxBus {

    private static volatile RxBus mInstance;
    private SerializedSubject<Object, Object> mSubject;
    private HashMap<String, CompositeSubscription> mSubscriptionMap;

    /**
     *  PublishSubject只會把在訂閱發(fā)生的時間點之后來自原始Observable的數(shù)據(jù)發(fā)射給觀察者
     *  Subject同時充當了Observer和Observable的角色,Subject是非線程安全的脱柱,要避免該問題伐弹,
     *  需要將 Subject轉(zhuǎn)換為一個 SerializedSubject ,上述RxBus類中把線程非安全的PublishSubject包裝成線程安全的Subject榨为。
     */
    private RxBus() {
        mSubject = new SerializedSubject<>(PublishSubject.create());
    }

    /**
     * 單例 雙重鎖
     * @return
     */
    public static RxBus getInstance() {
        if (mInstance == null) {
            synchronized (RxBus.class) {
                if (mInstance == null) {
                    mInstance = new RxBus();
                }
            }
        }
        return mInstance;
    }

    /**
     * 發(fā)送一個新的事件
     * @param o
     */
    public void post(Object o) {
        mSubject.onNext(o);
    }

    /**
     * 根據(jù)傳遞的 eventType 類型返回特定類型(eventType)的 被觀察者
     * @param type
     * @param <T>
     * @return
     */
    public <T> Observable<T> tObservable(final Class<T> type) {
        //ofType操作符只發(fā)射指定類型的數(shù)據(jù)惨好,其內(nèi)部就是filter+cast
        return mSubject.ofType(type);
    }

    public <T> Subscription doSubscribe(Class<T> type, Action1<T> next, Action1<Throwable> error) {
        return tObservable(type)
                .onBackpressureBuffer()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(next, error);
    }

    public void addSubscription(Object o, Subscription subscription) {

        if (mSubscriptionMap == null) {
            mSubscriptionMap = new HashMap<>();
        }
        String key = o.getClass().getName();
        if (mSubscriptionMap.get(key) != null) {
            mSubscriptionMap.get(key).add(subscription);
        } else {
            CompositeSubscription compositeSubscription = new CompositeSubscription();
            compositeSubscription.add(subscription);
            mSubscriptionMap.put(key, compositeSubscription);
          //  Log.e("air", "addSubscription:訂閱成功 " );
        }
    }

    public void unSubscribe(Object o) {
        if (mSubscriptionMap == null) {
            return;
        }
        String key = o.getClass().getName();
        if (!mSubscriptionMap.containsKey(key)) {
            return;
        }
        if (mSubscriptionMap.get(key) != null) {
            mSubscriptionMap.get(key).unsubscribe();
        }
        mSubscriptionMap.remove(key);
        //Log.e("air", "unSubscribe: 取消訂閱" );
    }
}

FileCallBack

那么,重點來了随闺。代碼其實有5個方法需要重寫日川,好吧,其實這些方法可以精簡一下矩乐。其中progress()方法有兩個參數(shù)龄句,progress和total,分別表示文件已下載的大小和總大小,我們將這兩個參數(shù)不斷更新到UI上就行了。

public abstract class FileCallBack<T> {

    private String destFileDir;
    private String destFileName;

    public FileCallBack(String destFileDir, String destFileName) {
        this.destFileDir = destFileDir;
        this.destFileName = destFileName;
        subscribeLoadProgress();
    }

    public abstract void onSuccess(T t);

    public abstract void progress(long progress, long total);

    public abstract void onStart();

    public abstract void onCompleted();

    public abstract void onError(Throwable e);

    public void saveFile(ResponseBody body) {
        InputStream is = null;
        byte[] buf = new byte[2048];
        int len;
        FileOutputStream fos = null;
        try {
            is = body.byteStream();
            File dir = new File(destFileDir);
            if (!dir.exists()) {
                dir.mkdirs();
            }
            File file = new File(dir, destFileName);
            fos = new FileOutputStream(file);
            while ((len = is.read(buf)) != -1) {
                fos.write(buf, 0, len);
            }
            fos.flush();
            unsubscribe();
            //onCompleted();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (is != null) is.close();
                if (fos != null) fos.close();
            } catch (IOException e) {
                Log.e("saveFile", e.getMessage());
            }
        }
    }

    /**
     * 訂閱加載的進度條
     */
    public void subscribeLoadProgress() {
        Subscription subscription = RxBus.getInstance().doSubscribe(FileLoadEvent.class, new Action1<FileLoadEvent>() {
            @Override
            public void call(FileLoadEvent fileLoadEvent) {
                progress(fileLoadEvent.getBytesLoaded(),fileLoadEvent.getTotal());
            }
        }, new Action1<Throwable>() {
            @Override
            public void call(Throwable throwable) {
                //TODO 對異常的處理
            }
        });
        RxBus.getInstance().addSubscription(this, subscription);
    }

    /**
     * 取消訂閱撒璧,防止內(nèi)存泄漏
     */
    public void unsubscribe() {
        RxBus.getInstance().unSubscribe(this);
    }

}

開始下載

使用自己的ProgressResponseBody

通過OkHttpClient的攔截器去攔截Response透葛,并將我們的ProgressReponseBody設置進去監(jiān)聽進度。

public class ProgressInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Response originalResponse = chain.proceed(chain.request());
        return originalResponse.newBuilder()
                .body(new ProgressResponseBody(originalResponse.body()))
                .build();
    }
}

構(gòu)建Retrofit

@Module
public class ApiModule {

    @Provides
    @Singleton
    public OkHttpClient provideClient() {
        OkHttpClient client = new OkHttpClient.Builder()
                .addInterceptor(new ProgressInterceptor())
                .build();
        return client;
    }

    @Provides
    @Singleton
    public Retrofit provideRetrofit(OkHttpClient client){
        Retrofit retrofit = new Retrofit.Builder()
                .client(client)
                .baseUrl(Constant.HOST)
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .addConverterFactory(GsonConverterFactory.create())
                .build();
        return retrofit;
    }

    @Provides
    @Singleton
    public ApiInfo provideApiInfo(Retrofit retrofit){
        return retrofit.create(ApiInfo.class);
    }

    @Provides
    @Singleton
    public ApiManager provideApiManager(Application application, ApiInfo apiInfo){
        return new ApiManager(application,apiInfo);
    }

}

請求接口

public interface ApiInfo {
    @Streaming
    @GET
    Observable<ResponseBody> download(@Url String url);
}

執(zhí)行請求

public void load(String url, final FileCallBack<ResponseBody> callBack){
        apiInfo.download(url)
                .subscribeOn(Schedulers.io())//請求網(wǎng)絡 在調(diào)度者的io線程
                .observeOn(Schedulers.io()) //指定線程保存文件
                .doOnNext(new Action1<ResponseBody>() {
                    @Override
                    public void call(ResponseBody body) {
                        callBack.saveFile(body);
                    }
                })
                .observeOn(AndroidSchedulers.mainThread()) //在主線程中更新ui
                .subscribe(new FileSubscriber<ResponseBody>(application,callBack));
    }

在presenter層中執(zhí)行網(wǎng)絡請求卿樱。

通過V層依賴注入的presenter對象調(diào)用請求網(wǎng)絡,請求網(wǎng)絡后調(diào)用V層更新UI的操作硫椰。

public void load(String url){
        String fileName = "app.apk";
        String fileStoreDir = Environment.getExternalStorageDirectory().getAbsolutePath();
        Log.e(TAG, "load: "+fileStoreDir.toString() );
        FileCallBack<ResponseBody> callBack = new FileCallBack<ResponseBody>(fileStoreDir,fileName) {

            @Override
            public void onSuccess(final ResponseBody responseBody) {
                Toast.makeText(App.getInstance(),"下載文件成功",Toast.LENGTH_SHORT).show();
            }

            @Override
            public void progress(long progress, long total) {
                iHomeView.update(total,progress);
            }

            @Override
            public void onStart() {
                iHomeView.showLoading();
            }

            @Override
            public void onCompleted() {
                iHomeView.hideLoading();
            }

            @Override
            public void onError(Throwable e) {
                //TODO: 對異常的一些處理
                e.printStackTrace();
            }
        };
        apiManager.load(url, callBack);
    }

踩到的坑繁调。

  • 依賴的Retrofit版本一定要保持一致!0胁荨蹄胰!說多了都是淚啊。
  • 保存文件時要使用RxJava的doOnNext操作符奕翔,后續(xù)更新UI的操作切換到UI線程裕寨。

總結(jié)

看似代碼很多,其實過程并不復雜:

  • 在保存文件時派继,調(diào)用ForwardingSource的read方法宾袜,通過RxBus發(fā)送實時的FileLoadEvent對象。
  • FileCallBack訂閱RxBus發(fā)送的FileLoadEvent驾窟。通過接收到FileLoadEvent中的下載進度和文件總大小對UI進行更新庆猫。
  • 在下載保存文件完成后,取消訂閱绅络,防止內(nèi)存泄漏月培。

Demo地址:https://github.com/AirMiya/DownloadDemo

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市恩急,隨后出現(xiàn)的幾起案子杉畜,更是在濱河造成了極大的恐慌,老刑警劉巖衷恭,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件此叠,死亡現(xiàn)場離奇詭異,居然都是意外死亡匾荆,警方通過查閱死者的電腦和手機拌蜘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來牙丽,“玉大人简卧,你說我怎么就攤上這事】韭” “怎么了举娩?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我铜涉,道長智玻,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任芙代,我火速辦了婚禮吊奢,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘纹烹。我一直安慰自己页滚,他們只是感情好,可當我...
    茶點故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布铺呵。 她就那樣靜靜地躺著裹驰,像睡著了一般。 火紅的嫁衣襯著肌膚如雪片挂。 梳的紋絲不亂的頭發(fā)上幻林,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天,我揣著相機與錄音音念,去河邊找鬼沪饺。 笑死,一個胖子當著我的面吹牛症昏,可吹牛的內(nèi)容都是我干的随闽。 我是一名探鬼主播,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼肝谭,長吁一口氣:“原來是場噩夢啊……” “哼掘宪!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起攘烛,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤魏滚,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后坟漱,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鼠次,經(jīng)...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年芋齿,在試婚紗的時候發(fā)現(xiàn)自己被綠了腥寇。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,675評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡觅捆,死狀恐怖赦役,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情栅炒,我是刑警寧澤掂摔,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布术羔,位于F島的核電站,受9級特大地震影響乙漓,放射性物質(zhì)發(fā)生泄漏级历。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一叭披、第九天 我趴在偏房一處隱蔽的房頂上張望寥殖。 院中可真熱鬧,春花似錦涩蜘、人聲如沸扛禽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至豆巨,卻和暖如春剩辟,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背往扔。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工贩猎, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人萍膛。 一個月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓吭服,卻偏偏與公主長得像,于是被迫代替她去往敵國和親蝗罗。 傳聞我的和親對象是個殘疾皇子艇棕,可洞房花燭夜當晚...
    茶點故事閱讀 45,685評論 2 360

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