Android开发丶一步步教你实现okhttp带进度的列表下载文件功能

你们好,我又回来了!html

标题好像又起的不知所云,可是貌似也想不起更好的标题,看看效果图android

如今有个文件列表,每一个列表标签都有一个下载的按钮,点击如下载对应的文件,若是已下载则显示“已下载”,反之显示“点击下载”。git

首先咱们使用okhttp框架下载文件,而且使用progressDialog显示下载进度,至于界面主列表,则是高端大气上档次的RecyclerView,啥?你还告诉我你用listView?好了不说废话,下来就一步步实现该功能吧。github

1、首先新建应用,打开app的build.gradle添加经常使用框架的依赖web

1.RecyerView(v7包默认不带,因此须要咱们手动添加)浏览器

2.BaseQuickAdapter(一个搭配RecyerView很强大简洁易用的万能适配器)缓存

3.okhttp(最经常使用的okhttp网络框架之一,无人不知无人不晓)tomcat

//RecycerView列表控件
implementation 'com.android.support:recyclerview-v7:28.0.0'
//okhttp网络下载框架
implementation 'com.squareup.okhttp3:okhttp:3.6.0'
//BaseQuickAdapter适配器
implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:2.9.22'

以后打开project的build.gradle,在allpreject下的repositories节点下添加如下代码,供BaseQuickAdapter依赖所用服务器

maven { url "https://jitpack.io" }
allprojects {
    repositories {
        google()
        jcenter()
        maven { url "https://jitpack.io" }
    }
}

添加完毕点击Sync Project图标网络

等待加载完成便可。

2、完成环境的搭建,接下来咱们就能够画界面啦。

首先天然是主界面了,没什么好说的,直接整个RecycerView怼上去就可。

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/main_recyclerview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</RelativeLayout>

而后是列表item界面,也没什么太复杂的东西,这里是直接整个左文字显示标题,再来个右按钮启动下载方法,由于recyclerview默认没有分割线,咱们再给底部怼一个view便可,能够根据需求进行更改。

item_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="55dp"
    >

    <TextView
        android:layout_width="wrap_content"
        android:id="@+id/tv"
        android:padding="15dp"
        android:layout_centerVertical="true"
        android:text="11111"
        android:layout_height="wrap_content" />

    <Button
        android:id="@+id/item_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_marginRight="15dp"
        android:padding="10dp"
        android:layout_centerVertical="true"/>

    <View
        android:layout_width="match_parent"
        android:background="@color/colorPrimary"
        android:layout_alignParentBottom="true"
        android:layout_height="1dp"/>

</RelativeLayout>

3、画完了布局,咱们加个列表内容bean,这个没什么难度,界面上有两个属性,textview的文字属性,button的下载状态属性。

MainBean.class

public class MainBean implements Serializable{
    private String title;
    private boolean isDownload;

    @Override
    public String toString() {
        return "MainBean{" +
                "title='" + title + '\'' +
                ", isDownload=" + isDownload +
                '}';
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public boolean isDownload() {
        return isDownload;
    }

    public void setDownload(boolean download) {
        isDownload = download;
    }

    public MainBean(String title, boolean isDownload) {
        this.title = title;
        this.isDownload = isDownload;
    }
}

值得一提的是,在实际开发过程当中,isDownload方法服务器是不会给咱们返回的,因此这里为咱们手动添加,用来判断boolean值来实现button按钮上的文字显示,具体逻辑咱们接下来会谈到。

4、接下来开整adapter

新建一个MainAdapter继承BaseQuickAdapter,生成convert方法,再新建一个构造函数以供MainActivity引用,同时把item布局文件塞进super里面的layoutResId参数里。

public class MainAdapter extends BaseQuickAdapter<MainBean, BaseViewHolder> {

    public MainAdapter(int layoutResId, @Nullable List<MainBean> data) {
        super(R.layout.item_main, data);
    }

    @Override
    protected void convert(BaseViewHolder helper, MainBean item) {
    }
}

重写convert方法,先给item的textview设置title。

helper.setText(R.id.item_tv, item.getTitle());

而后给item的button设置文字,这里根据isDownload值,为true则已下载显示“已下载”,为false则未下载显示“点击下载”。

helper.setText(R.id.item_btn, item.isDownload()? "已下载": "未下载");

五.准备工做都已作好,如今能够编辑Activity了。

打开MainActivity

在onCreate()方法中

1.初始化数据

private void initData() {
    MainBean bean= new MainBean("高祖提剑入咸阳,炎炎红日升扶桑", "http://10.48.78.196:8080/pdf/test.zip",false);
    datalist.add(bean);
    MainBean bean1= new MainBean("光武中兴续大统,金乌飞上天中央", "http://10.48.78.196:8080/pdf/test.zip",false);
    datalist.add(bean1);
    MainBean bean2= new MainBean("哀哉献帝绍海宇,红轮西坠咸池榜", "http://10.48.78.196:8080/pdf/test.zip",false);
    datalist.add(bean2);
    MainBean bean3= new MainBean("何进无谋中贵乱,凉州董卓居朝堂", "http://10.48.78.196:8080/pdf/test.zip",false);
    datalist.add(bean3);
    MainBean bean4= new MainBean("王允定计诛逆党,李榷郭汜兴刀枪", "http://10.48.78.196:8080/pdf/test.zip",false);
    datalist.add(bean4);
    MainBean bean5= new MainBean("四方盗贼如蚁聚,六合奸雄皆鹰扬", "http://10.48.78.196:8080/pdf/test.zip",false);
    datalist.add(bean5);
    MainBean bean6= new MainBean("孙坚孙策起江左,袁绍袁术兴河梁", "http://10.48.78.196:8080/pdf/test.zip",false);
    datalist.add(bean6);
    MainBean bean7= new MainBean("刘焉父子居巴蜀,刘表羁旅屯荆襄", "http://10.48.78.196:8080/pdf/test.zip",false);
    datalist.add(bean7);
    MainBean bean8= new MainBean("张燕张鲁霸南郑,马腾韩遂守西凉", "http://10.48.78.196:8080/pdf/test.zip",false);
    datalist.add(bean8);
    MainBean bean9= new MainBean("陶谦张绣公孙瓒,各逞雄才占一方", "http://10.48.78.196:8080/pdf/test.zip",false);
    datalist.add(bean9);
}

2.初始化控件

/**
 * 初始化控件
 */
private void initView() {
    recyclerView= findViewById(R.id.main_recyclerview);
    LinearLayoutManager manager= new LinearLayoutManager(this);
    recyclerView.setLayoutManager(manager);

}

3.初始化适配器

/**
 * 初始化适配器
 */
private void initAdapter() {
    MainAdapter adapter= new MainAdapter(datalist);
    recyclerView.setAdapter(adapter);
}

这时候跑起程序,界面已成功呈现。

六.遗憾的是,RecycerView并无给咱们提供item和各控件的点击监听,因此这里咱们须要经过接口回调的方式完成button下载按钮的点击下载监听。

回到MainAdapter,设置自定义监听

public interface downloadClickListener {
    void downloadClick(int position);

}

public void setDownloadClickListener(downloadClickListener listener){
    this.listener= listener;
}

在convert()方法中设置button的点击事件,把position参数传进去以供测试。

helper.getView(R.id.item_btn).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        if (listener!= null){
            listener.downloadClick(helper.getAdapterPosition());
        }
    }
});

回到MainActivity,接口回调触发button的点击监听

//接口回调完成item上button下载按钮的点击事件
adapter.setDownloadClickListener(new MainAdapter.downloadClickListener() {
    @Override
    public void downloadClick(int position) {
        Toast.makeText(MainActivity.this, ""+ position, Toast.LENGTH_SHORT).show();
    }
});

运行程序,这里咱们用Toast测试,是否position参数顺利传了进来。点击第一个item的button

点击最后一个,咱们一共自定义了10条数据,因此应该Toast 9

Perfect!!!接下来咱们就能够设置每一个item的button的下载监听方法了。

七.下载文件方法

1.首先咱们分析一下实现方法以及原理,实际开发过程当中,后台通常会把文件下载路径提供给咱们,测试时咱们能够把文件放在tomcat服务器上以供测试,下载是经过okhttp网络框架由IO流的方式将文件下载到手机内置存储中,在手机文件管理器下的Android/data/应用包名xxxx 路径下有两个文件夹cache和file,前者cache顾名思义就是缓存文件夹,通常放置一些微型不长存的数据,若是手机由于内存不足等状况下,该文件夹常常被清除,因此不适合咱们存放长时间保存的下载文件,于是咱们通常在后者file文件夹下新建一个文件夹用来保存下载文件,并且由于是在包名目录下,当应用卸载时,下载的文件也会随之清除,避免了垃圾文件的残余。

原理ok了,咱们来讲说实现步骤吧

首先咱们把要下载的文件放置在tomcat服务器上,具体环境搭建继承不详细叙述,有问题可百度。

打开tomcat的文件夹,咱们会看到webapps文件夹

点击进入,新建一个文件夹放咱们要下载的测试文件,我这里是新建了一个pdf文件夹,里面放了一个pdf文件

启动tomcat,获取下载路径,好比我这里

打开网络设置,获取本机IP地址 http://10.48.78.196

由于咱们把须要下载的测试文件pdf_test.pdf放进了tomcat文件夹下的webapps文件夹下的pdf文件夹下,这样咱们的下载路径就是  http://10.48.78.196:8080/pdf/pdf_test.pdf

在浏览器中打开如上连接,能正常打开说明咱们部署成功。

perfect!这样咱们经过请求该连接就能下载该pdf文件了。

2.接下来,咱们决定使用okhttp下载该文件,先编写工具类。

咱们以前已经添加过okhttp的依赖,因此直接引用。

主要讲几个核心方法,完整代码随后附录。

A、download()

/**
 * @param url 下载链接
 * @param saveDir 储存下载文件的SDCard目录
 * @param listener 下载监听
 */
public void download(Context context, String fileId, final String url, final String saveDir, final OnDownloadListener listener) {
    this.context= context;
    Request request = new Request.Builder().url(url).build();
    okHttpClient.newCall(request).enqueue(new Callback() {
        @Override
        public void onFailure(Call call, IOException e) {
            // 下载失败
            listener.onDownloadFailed();
        }
        @Override
        public void onResponse(Call call, Response response) throws IOException {
            InputStream is = null;
            byte[] buf = new byte[2048];
            int len = 0;
            FileOutputStream fos = null;
            // 储存下载文件的目录
            String savePath = isExistDir(saveDir);
            try {
                is = response.body().byteStream();
                long total = response.body().contentLength();
                File file = new File(savePath, getNameFromUrl(url, fileId));
                fos = new FileOutputStream(file);
                long sum = 0;
                while ((len = is.read(buf)) != -1) {
                    fos.write(buf, 0, len);
                    sum += len;
                    int progress = (int) (sum * 1.0f / total * 100);
                    // 下载中
                    listener.onDownloading(progress);
                }
                fos.flush();
                // 下载完成
                listener.onDownloadSuccess();
            } catch (Exception e) {
                listener.onDownloadFailed();
            } finally {
                try {
                    if (is != null)
                        is.close();
                } catch (IOException e) {
                }
                try {
                    if (fos != null)
                        fos.close();
                } catch (IOException e) {
                }
            }
        }
    });
}

在该方法会产生两个回调,顾名思义,一个成功onResponse()、一个失败onFailure()

咱们先看看onResponse(),当下载成功后,咱们把下载的文件经过IO流的方式写入指定的手机内存路径中,由于下载和写入的进度同步进行,因此咱们须要把该进度progress传递,以便咱们的progressDialog显示,提高用户体验。当下载成功后,一样完成相应的处理并记得关闭IO流。

2.在如上方法中,有个isExistDir()方法。

/**
 * @param saveDir
 * @return
 * @throws IOException
 * 判断下载目录是否存在
 */
private String isExistDir(String saveDir) throws IOException {
    // 下载位置
    File downloadFile = new File(context.getExternalFilesDir(null), saveDir);
    if (!downloadFile.mkdirs()) {
        downloadFile.createNewFile();
    }
    String savePath = downloadFile.getAbsolutePath();
    return savePath;
}

顾名思义,主要判断咱们指定的手机内存路径是否存在

若是尚不存在则create一个新的文件夹目录,

if (!downloadFile.mkdirs()) {
    downloadFile.createNewFile();
}

若是存在则直接返回

String savePath = downloadFile.getAbsolutePath();
return savePath;

3.在onResponse()方法中,咱们注意到有一个getNameFromUrl()方法

顾名思义,这是获取下载文件的原始名称以对该文件进行下载后的命名。

由于下载文件路径都是这样的

xxxxxxxxxxxxx/我是某某某文件.xxx

因此咱们把该路径最后一个/号后面的文字所有截取,就能够获得该文件的原始名称了。

return url.substring(url.lastIndexOf("/") + 1);

4.写一下相应的回调接口。以便进行相应的处理。

public interface OnDownloadListener {
    /**
     * 下载成功
     */
    void onDownloadSuccess();

    /**
     * @param progress
     * 下载进度
     */
    void onDownloading(int progress);

    /**
     * 下载失败
     */
    void onDownloadFailed();
}

8、准备工做基本完成,接下来咱们就在列表界面Activity进行调取了。

回到Activity的列表item上的button点击事件上。去掉以前的测试toast,增长下载方法。

//接口回调完成item上button下载按钮的点击事件
adapter.setDownloadClickListener(new MainAdapter.downloadClickListener() {
    @Override
    public void downloadClick(int position) {
        //文件下载路径
        String url= "http://10.48.78.196:8080/pdf/pdf_test.pdf";
        //文件在手机内存存储的路径
        String saveurl= getExternalFilesDir(null)+ "/pdffile/";
        //启动下载方法
        DownloadUtil.get().download(MainActivity.this, url, saveurl, new DownloadUtil.OnDownloadListener() {
            @Override
            public void onDownloadSuccess() {
                Toast.makeText(MainActivity.this, "下载成功", Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onDownloading(int progress) {

            }

            @Override
            public void onDownloadFailed() {
                Toast.makeText(MainActivity.this, "下载失败", Toast.LENGTH_SHORT).show();
            }
        });
    }
});

在AndroidManifest.xml清单文件中添加权限。

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

如今咱们能够先把程序跑起来,点击下载按钮

显示下载成功,咱们打开相应包名下的file文件夹,看有没有该文件。

点击能够正常打开,说明咱们已经成功地把服务器上的文件下载下来了。

接下来,咱们给下载过程加上进度弹窗,当下载比较大和耗时的文件时显示进度,提高用户体验。

//配置progressDialog
final ProgressDialog dialog= new ProgressDialog(MainActivity.this);
dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
dialog.setCanceledOnTouchOutside(false);
dialog.setCancelable(true);
dialog.setTitle("正在下载中");
dialog.setMessage("请稍后...");
dialog.setProgress(0);
dialog.setMax(100);
dialog.show();

在下载的onDownloading()方法中设置进度。

@Override
public void onDownloading(int progress) {
    dialog.setProgress(progress);
}

下载完成或者下载失败时隐藏掉弹窗。

dialog.dismiss();

咱们如今下载个比较大的文件测试(为了进度条存在时间更长)

http://10.48.78.196:8080/pdf/test.zip

打开目录,查看是否存在

咋一看下载都成功了,忽然问题来了

刚点击了好几个不一样的下载按钮,为何只有这两个文件?

原来是被同名覆盖了,这样咱们要在下载命名中作点功夫了

这下就避免同名覆盖了

而后下载成功时,咱们局部刷新item,将item上的下载按钮文字改成“已下载”

打开Adapter,判断文件是否存在,存在即为”已下载“,反之为“未下载”

String filePath= context.getExternalFilesDir(null)+ "/pdffile/"+ helper.getLayoutPosition()+ "_"+ item.getUrl().substring(item.getUrl().lastIndexOf("/") + 1);
if(isFileExist(filePath)){
    item.setDownload(true);
}else {
    item.setDownload(false);
}
helper.setText(R.id.item_btn, item.isDownload()? "已下载": "未下载");

下载成功后,局部刷新item上的button

adapter.notifyItemChanged(position);

 

至此所有完成,demo附上

资源下载