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

大家好,我又回来了!

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

现在有个文件列表,每个列表标签都有一个下载的按钮,点击以下载对应的文件,如果已下载则显示“已下载”,反之显示“点击下载”。

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

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

1.RecyerView(v7包默认不带,所以需要我们手动添加)

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

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

//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图标

等待加载完成即可。

二、完成环境的搭建,接下来我们就可以画界面啦。

首先自然是主界面了,没什么好说的,直接整个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>

三、画完了布局,我们加个列表内容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按钮上的文字显示,具体逻辑我们接下来会谈到。

四、接下来开整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();
}

八、准备工作基本完成,接下来我们就在列表界面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附上

资源下载

  • 8
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
要在 Android 中使用 OkHttp 实现进度文件上传,可以使用 OkHttp 提供的 Interceptor 和 RequestBody 类。下面是一个简单的示例代码: ```java public class ProgressRequestBody extends RequestBody { private static final int DEFAULT_BUFFER_SIZE = 2048; private final File file; private final String contentType; private final ProgressListener listener; public ProgressRequestBody(File file, String contentType, ProgressListener listener) { this.file = file; this.contentType = contentType; this.listener = listener; } @Override public MediaType contentType() { return MediaType.parse(contentType); } @Override public long contentLength() { return file.length(); } @Override public void writeTo(BufferedSink sink) throws IOException { long fileLength = file.length(); byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; long uploaded = 0; try (InputStream in = new FileInputStream(file)) { int read; while ((read = in.read(buffer)) != -1) { uploaded += read; sink.write(buffer, 0, read); if (listener != null) { listener.onProgress(uploaded, fileLength); } } } } public interface ProgressListener { void onProgress(long uploaded, long total); } } ``` 这个类继承自 OkHttp 的 RequestBody 类,并实现了上传文件进度监听。在 writeTo() 方法中使用 InputStream 从文件中读取数据,并将数据写入 BufferedSink 中。同时,每次写入数据都会调用 onProgress() 方法通知进度监听器。 然后,创建一个 OkHttpClient,添加一个 Interceptor,该 Interceptor 使用 ProgressRequestBody 替换 RequestBody,从而实现进度监听: ```java OkHttpClient client = new OkHttpClient.Builder() .addInterceptor(new Interceptor() { @Override public Response intercept(Chain chain) throws IOException { Request original = chain.request(); // 使用 ProgressRequestBody 替换 RequestBody ProgressRequestBody requestBody = new ProgressRequestBody(file, contentType, listener); Request request = original.newBuilder() .method(original.method(), requestBody) .build(); return chain.proceed(request); } }) .build(); ``` 其中,file 是要上传的文件,contentType 是文件的 MIME 类型,listener 是进度监听器。最后,使用 OkHttpClient 发起一个上传文件的 Request: ```java Request request = new Request.Builder() .url(uploadUrl) .post(requestBody) .build(); Call call = client.newCall(request); call.enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { // 上传失败 } @Override public void onResponse(Call call, Response response) throws IOException { // 上传成功 } }); ``` 在 onResponse() 方法中处理上传结果即可。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值