更好的Android多线程下载框架

Android FileDownloader框架多线程下载 在android 多线程下载框架中, FileDownloader开源库是个比较好的下载库非常方便快捷 地址如下: [https://github.com/lingochamp/FileDownloader](https://github.com/lingochamp/FileDownloader) 特点 简单易用 单任务多线程/多连接/分块下载(并支持通过ConnectionCountAdap... 阅读详情
/**
 * 作者:Pich
 * 原文链接:http://me.woblog.cn/
 * QQ群:129961195
 * Github:https://github.com/lifengsofts
 */

概述

为什么是更好的Android多线程下载框架呢,原因你懂的,广告法嘛!

本篇我们我们就来聊聊多线程下载框架,先聊聊我们框架的特点:

  1. 多线程
  2. 多任务
  3. 断点续传
  4. 支持大文件
  5. 可以自定义下载数据库
  6. 高度可配置,像超时时间这类
  7. 业务数据和下载数据分离

下面我们在说下该框架能实现那些的应用场景:

  1. 该框架可以很方便的下载单个文件,并且显示各种状态,包括开始下载,下载中,下载失败,删除等状态。
  2. 也可以实现常见的需要下载功能应用,比如:某某手机助手,在该应用内可以说是下载是核心功能,所以对框架的稳定性,代码可靠性,框架扩展性依赖很大,所以该框架真是从这种出发点而生的。通常这类应用的表示形式分三个页面需要用到下载功能,一个列表用来显示来自业务数据的列表,在该列表右边可以点击单个条目,或者多选实现下载,点击每个条目进入详情,同时还有个一个下载管理,包括大概两个界面,正在下载,下载完成的,在这几个界面都需要一个核心的功能就是都可以暂停,恢复,删除并且能显示下载进度。在列表一个最重要的问题就是界面刷新,如果每次更新都刷新整个列表,那么这将是异常灾难,而我们这个框架正好解决了该问题,采用了回调单个条目并更新该条目的进度和状态。

该项目状态

该项目的雏形始于14年的公司项目需要用到多线程下载,但当时实现的单线程多任务断点续传,后面不断完善,在这之间遇到过很多坑,也对一个下载框架有了更深的认识,所以在16年又重写了该框架。

项目的Github地址:https://github.com/lifengsofts/AndroidDownloader

项目的官网地址:http://i.woblog.cn/AndroidDownloader

项目还处于发展状态,但已经趋于稳定,并且有一定的编码规范,同时采用了多个开源项目的质量控制方案以保证每次代码提交的可靠性。

下面上几张框架Demo的截图,这样用户在心中有一个自己的概念,但是推荐各位还是讲Demo下载到本地亲自,运行一下。

截图

AndroidDownloader Sample Screenshots AndroidDownloader Sample Screenshots

AndroidDownloader Sample Screenshots AndroidDownloader Sample Screenshots

第一个界面是单独下载一个文件。

第二个界面是应用中最常用的一个界面,该界面来自业务数据。

第三个页面是离线管理中的下载中的界面。

第四个页面是离线管理中的下载完成的界面。

可以看到他们在每个界面都能暂停下载,继续下载,以及删除,并且都能拿到进度,状态等信息。

下面就来看看这么强大的下载框架那该如何来使用呢?

添加权限

我相信这一步任何一个项目都已经添加了,但是还是不得不提一下。

该框架需要网络访问权限,如果你是讲文件下载到存储卡,那相应的需要添加存储卡访问权限。

<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" />

配置Service

因为该框架采用才service中下载一个文件的。这样做的目的是下载任务一般都需要在后头下载,如果在Activity中来做这类任务,我想任何一个新手都知道这样不行。

<service android:name="cn.woblog.android.downloader.DownloadService">
  <intent-filter>
    <action android:name="cn.woblog.android.downloader.DOWNLOAD_SERVICE" />
  </intent-filter>
</service>

添加依赖

我们提供了多种集成方式,比如:gradle,maven,jar。选择适合你自己的就行了。

Gradle

在module目录下面的build.gradle文件中添加如下内容:

compile 'cn.woblog.android:downloader:1.0.1'

Maven

或者你使用的Maven依赖管理工具。那道理其实是一样的,在pom文件中添加:

<dependency>
  <groupId>cn.woblog.android</groupId>
  <artifactId>downloader</artifactId>
  <version>1.0.0</version>
</dependency>

或者你也可以参考该链接使用Snapshots版本。

混淆配置

如果你的项目使用了混淆规则,那么一定要加上。

-keep public class * implements cn.woblog.android.downloader.db.DownloadDBController
-keep class cn.woblog.android.downloader.domain.** { *; }

创建下载管理器

现在万事俱备只欠东风了,接下来只需要创建一个下载管理器,该框架所有的操作都是通过该来实现的:

downloadManager = DownloadService.getDownloadManager(context.getApplicationContext());

或者你可以使用更详细的来配置该框架:

Config config = new Config();
//set database path.
//    config.setDatabaseName("/sdcard/a/d.db");
//      config.setDownloadDBController(dbController);

//set download quantity at the same time.
config.setDownloadThread(3);

//set each download info thread number
config.setEachDownloadThread(2);

// set connect timeout,unit millisecond
config.setConnectTimeout(10000);

// set read data timeout,unit millisecond
config.setReadTimeout(10000);
downloadManager = DownloadService.getDownloadManager(this.getApplicationContext(), config);

下载一个文件

//create download info set download uri and save path.
final DownloadInfo downloadInfo = new DownloadInfo.Builder().setUrl("http://example.com/a.apk")
    .setPath("/sdcard/a.apk")
    .build();

//set download callback.
downloadInfo.setDownloadListener(new DownloadListener() {

  @Override
  public void onStart() {
    tv_download_info.setText("Prepare downloading");
  }

  @Override
  public void onWaited() {
    tv_download_info.setText("Waiting");
    bt_download_button.setText("Pause");
  }

  @Override
  public void onPaused() {
    bt_download_button.setText("Continue");
    tv_download_info.setText("Paused");
  }

  @Override
  public void onDownloading(long progress, long size) {
    tv_download_info
        .setText(FileUtil.formatFileSize(progress) + "/" + FileUtil
            .formatFileSize(size));
    bt_download_button.setText("Pause");
  }

  @Override
  public void onRemoved() {
    bt_download_button.setText("Download");
    tv_download_info.setText("");
    downloadInfo = null;
  }

  @Override
  public void onDownloadSuccess() {
    bt_download_button.setText("Delete");
    tv_download_info.setText("Download success");
  }

  @Override
  public void onDownloadFailed(DownloadException e) {
    e.printStackTrace();
    tv_download_info.setText("Download fail:" + e.getMessage());
  }
});

//submit download info to download manager.
downloadManager.download(downloadInfo);

下载一个文件时直接创建一个DownloadInfo,然后设置下载链接和下载路径。再添加一个监听。就可以提交到下载框架了。

通过下载监听器我们可以获取到很多状态。开始下载,等待中,暂停完成,下载中,删除成功,下载成功,下载失败等状态。

在列表控件使用

我们这里演示如何在RecyclerView这类列表控件使用。当然如果你用的是ListView那道理是一样的。

class ViewHolder extends RecyclerView.ViewHolder {

  private final ImageView iv_icon;
  private final TextView tv_size;
  private final TextView tv_status;
  private final ProgressBar pb;
  private final TextView tv_name;
  private final Button bt_action;
  private DownloadInfo downloadInfo;

  public ViewHolder(View view) {
    super(view);

    iv_icon = (ImageView) view.findViewById(R.id.iv_icon);
    tv_size = (TextView) view.findViewById(R.id.tv_size);
    tv_status = (TextView) view.findViewById(R.id.tv_status);
    pb = (ProgressBar) view.findViewById(R.id.pb);
    tv_name = (TextView) view.findViewById(R.id.tv_name);
    bt_action = (Button) view.findViewById(R.id.bt_action);
  }

  @SuppressWarnings("unchecked")
  public void bindData(final MyDownloadInfo data, int position, final Context context) {
    Glide.with(context).load(data.getIcon()).into(iv_icon);
    tv_name.setText(data.getName());

    // Get download task status
    downloadInfo = downloadManager.getDownloadById(data.getUrl().hashCode());

    // Set a download listener
    if (downloadInfo != null) {
      downloadInfo
          .setDownloadListener(new MyDownloadListener(new SoftReference(ViewHolder.this)) {
            //  Call interval about one second
            @Override
            public void onRefresh() {
              if (getUserTag() != null && getUserTag().get() != null) {
                ViewHolder viewHolder = (ViewHolder) getUserTag().get();
                viewHolder.refresh();
              }
            }
          });

    }

    refresh();

//      Download button
    bt_action.setOnClickListener(new OnClickListener() {
      @Override
      public void onClick(View v) {
        if (downloadInfo != null) {

          switch (downloadInfo.getStatus()) {
            case DownloadInfo.STATUS_NONE:
            case DownloadInfo.STATUS_PAUSED:
            case DownloadInfo.STATUS_ERROR:

              //resume downloadInfo
              downloadManager.resume(downloadInfo);
              break;

            case DownloadInfo.STATUS_DOWNLOADING:
            case DownloadInfo.STATUS_PREPARE_DOWNLOAD:
            case STATUS_WAIT:
              //pause downloadInfo
              downloadManager.pause(downloadInfo);
              break;
            case DownloadInfo.STATUS_COMPLETED:
              downloadManager.remove(downloadInfo);
              break;
          }
        } else {
//            Create new download task
          File d = new File(Environment.getExternalStorageDirectory().getAbsolutePath(), "d");
          if (!d.exists()) {
            d.mkdirs();
          }
          String path = d.getAbsolutePath().concat("/").concat(data.getName());
          downloadInfo = new Builder().setUrl(data.getUrl())
              .setPath(path)
              .build();
          downloadInfo
              .setDownloadListener(new MyDownloadListener(new SoftReference(ViewHolder.this)) {

                @Override
                public void onRefresh() {
                  if (getUserTag() != null && getUserTag().get() != null) {
                    ViewHolder viewHolder = (ViewHolder) getUserTag().get();
                    viewHolder.refresh();
                  }
                }
              });
          downloadManager.download(downloadInfo);
        }
      }
    });

  }

  private void refresh() {
    if (downloadInfo == null) {
      tv_size.setText("");
      pb.setProgress(0);
      bt_action.setText("Download");
      tv_status.setText("not downloadInfo");
    } else {
      switch (downloadInfo.getStatus()) {
        case DownloadInfo.STATUS_NONE:
          bt_action.setText("Download");
          tv_status.setText("not downloadInfo");
          break;
        case DownloadInfo.STATUS_PAUSED:
        case DownloadInfo.STATUS_ERROR:
          bt_action.setText("Continue");
          tv_status.setText("paused");
          try {
            pb.setProgress((int) (downloadInfo.getProgress() * 100.0 / downloadInfo.getSize()));
          } catch (Exception e) {
            e.printStackTrace();
          }
          tv_size.setText(FileUtil.formatFileSize(downloadInfo.getProgress()) + "/" + FileUtil
              .formatFileSize(downloadInfo.getSize()));
          break;

        case DownloadInfo.STATUS_DOWNLOADING:
        case DownloadInfo.STATUS_PREPARE_DOWNLOAD:
          bt_action.setText("Pause");
          try {
            pb.setProgress((int) (downloadInfo.getProgress() * 100.0 / downloadInfo.getSize()));
          } catch (Exception e) {
            e.printStackTrace();
          }
          tv_size.setText(FileUtil.formatFileSize(downloadInfo.getProgress()) + "/" + FileUtil
              .formatFileSize(downloadInfo.getSize()));
          tv_status.setText("downloading");
          break;
        case STATUS_COMPLETED:
          bt_action.setText("Delete");
          try {
            pb.setProgress((int) (downloadInfo.getProgress() * 100.0 / downloadInfo.getSize()));
          } catch (Exception e) {
            e.printStackTrace();
          }
          tv_size.setText(FileUtil.formatFileSize(downloadInfo.getProgress()) + "/" + FileUtil
              .formatFileSize(downloadInfo.getSize()));
          tv_status.setText("success");
          break;
        case STATUS_REMOVED:
          tv_size.setText("");
          pb.setProgress(0);
          bt_action.setText("Download");
          tv_status.setText("not downloadInfo");
        case STATUS_WAIT:
          tv_size.setText("");
          pb.setProgress(0);
          bt_action.setText("Pause");
          tv_status.setText("Waiting");
          break;
      }

    }
  }
}

关键代码就是bindData方法中先通过业务的id,我们这里使用的url来获取该业务数据是否有对应的下载任务。如果有,则从新绑定监听器,也就是这段代码

downloadInfo
  .setDownloadListener(new MyDownloadListener(new SoftReference(ViewHolder.this)) {
    //  Call interval about one second
    @Override
    public void onRefresh() {
      if (getUserTag() != null && getUserTag().get() != null) {
        ViewHolder viewHolder = (ViewHolder) getUserTag().get();
        viewHolder.refresh();
      }
    }
  });

其中要注意到的是缓存每个条目我们使用了SoftReference,这样做的目的内容在吃紧的情况下而已及时的是否这些条目。

接下来又一个重要的点是,设置按钮的点击事件,通常在这样的列表中有一个或多个按钮控制下载状态。

if (downloadInfo != null) {

  switch (downloadInfo.getStatus()) {
    case DownloadInfo.STATUS_NONE:
    case DownloadInfo.STATUS_PAUSED:
    case DownloadInfo.STATUS_ERROR:

      //resume downloadInfo
      downloadManager.resume(downloadInfo);
      break;

    case DownloadInfo.STATUS_DOWNLOADING:
    case DownloadInfo.STATUS_PREPARE_DOWNLOAD:
    case STATUS_WAIT:
      //pause downloadInfo
      downloadManager.pause(downloadInfo);
      break;
    case DownloadInfo.STATUS_COMPLETED:
      downloadManager.remove(downloadInfo);
      break;
  }
} else {
//            Create new download task
  File d = new File(Environment.getExternalStorageDirectory().getAbsolutePath(), "d");
  if (!d.exists()) {
    d.mkdirs();
  }
  String path = d.getAbsolutePath().concat("/").concat(data.getName());
  downloadInfo = new Builder().setUrl(data.getUrl())
      .setPath(path)
      .build();
  downloadInfo
      .setDownloadListener(new MyDownloadListener(new SoftReference(ViewHolder.this)) {

        @Override
        public void onRefresh() {
          if (getUserTag() != null && getUserTag().get() != null) {
            ViewHolder viewHolder = (ViewHolder) getUserTag().get();
            viewHolder.refresh();
          }
        }
      });
  downloadManager.download(downloadInfo);
}

关键点就是如果没有下载任务就创建一个下载任务,如果已有下载任务就根据任务现在的状态执行相应的操作,比如当前是没有下载,点击就是创建一个下载任务。

接下还有一个重点就是,我们在回调监听中调用了refresh方法,在该方法中根据状态显示进度和相应的操作按钮。这样做的好处上面已经提到了。

private void refresh() {
  if (downloadInfo == null) {
    tv_size.setText("");
    pb.setProgress(0);
    bt_action.setText("Download");
    tv_status.setText("not downloadInfo");
  } else {
    switch (downloadInfo.getStatus()) {
      case DownloadInfo.STATUS_NONE:
        bt_action.setText("Download");
        tv_status.setText("not downloadInfo");
        break;
      case DownloadInfo.STATUS_PAUSED:
      case DownloadInfo.STATUS_ERROR:
        bt_action.setText("Continue");
        tv_status.setText("paused");
        try {
          pb.setProgress((int) (downloadInfo.getProgress() * 100.0 / downloadInfo.getSize()));
        } catch (Exception e) {
          e.printStackTrace();
        }
        tv_size.setText(FileUtil.formatFileSize(downloadInfo.getProgress()) + "/" + FileUtil
            .formatFileSize(downloadInfo.getSize()));
        break;

      case DownloadInfo.STATUS_DOWNLOADING:
      case DownloadInfo.STATUS_PREPARE_DOWNLOAD:
        bt_action.setText("Pause");
        try {
          pb.setProgress((int) (downloadInfo.getProgress() * 100.0 / downloadInfo.getSize()));
        } catch (Exception e) {
          e.printStackTrace();
        }
        tv_size.setText(FileUtil.formatFileSize(downloadInfo.getProgress()) + "/" + FileUtil
            .formatFileSize(downloadInfo.getSize()));
        tv_status.setText("downloading");
        break;
      case STATUS_COMPLETED:
        bt_action.setText("Delete");
        try {
          pb.setProgress((int) (downloadInfo.getProgress() * 100.0 / downloadInfo.getSize()));
        } catch (Exception e) {
          e.printStackTrace();
        }
        tv_size.setText(FileUtil.formatFileSize(downloadInfo.getProgress()) + "/" + FileUtil
            .formatFileSize(downloadInfo.getSize()));
        tv_status.setText("success");
        break;
      case STATUS_REMOVED:
        tv_size.setText("");
        pb.setProgress(0);
        bt_action.setText("Download");
        tv_status.setText("not downloadInfo");
      case STATUS_WAIT:
        tv_size.setText("");
        pb.setProgress(0);
        bt_action.setText("Pause");
        tv_status.setText("Waiting");
        break;
    }

  }
}

到这里改下框架的核心使用方法就介绍完了。

支持

如有任何问题可以在加我们的QQ群或者在Github上提Issue,另外请提Issue或者的PR的一定要看下项目的贡献代码的方法以及一要求,因为如果要保证一个开源项目的质量就必须在各方面都规范化。

OkDownload框架源码分析 OkDownload是一个android下载框架,FileDownloader的升级版本,也称FileDownloader2;是一个支持多线程,多任务,断点续传,可靠,灵活,高性能以及强大的下载引擎,由于版本之间有代码差异,我们基于1.0.2版本做介绍。 1. 对比FileDownloader的优势 单元测试覆盖率很高,从而保证框架的可靠性。 简单... 阅读详情

相关推荐

【Android -- 多线程】IntentService

Android 里的一个封装类,继承四大组件之一的 Service.

Kevin Learn Android 1万+

FileDownlaoder下载框架

此处引用自android自定义实现断点续传。

qq_45444058的博客 1158

高效Android多线程下载框架:Afinal+Gson源码解析

本文还有配套的精品资源,点击获取 简介:在Android应用开发中,多线程下载框架是提升资源下载效率的关键。本框架利用Afinal库简化数据库操作,结合Gson进行数据序列化,实现高效且易用的多线程下载功能。详细分析框架中的主要技术点,包括线程同步、下载管理器设计、断点续传机制,以及用户界面的设计,以提高用户体验和开发效率。 1. Afinal库的数据库操作优势 ...

weixin_36487018的博客 1211

android-downloader:一款开源的多线程多任务下载框架

android-downloader:一款开源的多线程多任务下载框架 在Android开发过程中,文件下载是常见的需求。今天,我要为大家推荐一款开源的多线程多任务下载框架——android-downloader,它可以帮助我们轻松实现稳定、高效的文件下载功能。 项目介绍 android-downloader 是一个适用于 Android 平台的开源多线程多任务下载框架。它支持多线程下载,能够提高下...

gitblog_01184的博客 1061

Android高效多线程文件下载实现

本文还有配套的精品资源,点击获取 简介:本项目为Android平台上的"down_ANDROID",重点讲解如何使用Download Service在后台高效下载大文件,以及多线程技术加速下载过程。通过该示例项目,开发者可以了解如何创建自定义下载管理器,实现文件的下载、暂停、恢复和取消等操作。实现步骤包括初始化下载任务、设置请求头、创建多线程、下载部分文件、保存本地文件、合...

weixin_36123300的博客 1417

Android网络多线程断点续传下载实战教程

在了解多线程下载之前,我们先来了解单线程下载的基本原理。单线程下载是指用户设备上的下载客户端与服务器之间建立一个HTTP连接,通过这个连接从服务器下载数据,直到文件下载完成。然而,这种方式受到网络带宽和服务器处理能力的限制,下载速度有限。

weixin_31185473的博客 888

Andoid 更好的Android多线程下载框架

概述 为什么是更好的Android多线程下载框架呢,原因你懂的,广告法嘛! 本篇我们我们就来聊聊多线程下载框架,先聊聊我们框架的特点: 多线程 多任务 断点续传 支持大文件 可以自定义下载数据库 高度可配置,像超时时间这类 业务数据和下载数据分离 下面我们在说下该框架能实现那些的应用场景: 该框架可以很方便的下载单个文件,并且显示各种状态,包括开始下载,下载中,下...

weixin_30703911的博客 119

实现Android多线程文件下载的核心技术

本文还有配套的精品资源,点击获取 简介:在Android开发中,多线程下载技术对提升大文件下载速度和优化用户体验至关重要。本文详细介绍了多线程下载的基本原理以及如何利用Android技术如AsyncTask、Thread和Handler、IntentService、下载管理库以及存储合并技术等实现多线程下载。同时,文章提供了代码示例和源码说明,帮助开发者通过实践更好地理解和...

weixin_33750664的博客 1409

Android Downloader:一款强大的多线程下载框架

Android Downloader:一款强大的多线程下载框架 1. 项目基础介绍及主要编程语言 Android Downloader 是一个开源的多线程下载框架,旨在为 Android 应用程序提供稳定、高效的下载功能。该项目主要使用 Java 语言开发,适用于需要下载大文件或多个文件的 Android 应用场景。 2. 核心功能 多线程下载:通过多线程技术,提高下载速度和效率。 断点续传:支...

gitblog_00101的博客 707

XDownloader_v1.9.0:安卓高效下载工具详解

用户应被赋予更多控制权,以根据自身的网络状况和需求自定义断点续传的策略。这可能包括设置重试次数、自定义重连间隔、以及是否启用某些特殊模式,如节能模式等。

weixin_42311427的博客 1245

ReinaDownloader:Android项目的多线程多任务下载库

雷纳下载器 适用于 Android 项目的多线程和多任务下载库。 待办事项清单: 多线程下载。 √ 断点续传支持。 √ 回调接口。 √ 处理异常。 √ 多任务管理。 √ 线程之间的同步通信。 × v1.1.0 有什么新变化? 多任务管理器:帮助您高效管理任务。 优化的代码结构。 如何更新到 v1.1.0? 由于代码结构已更改,您必须更新代码才能使用新版本库。 更改导入的包名:sumimakito.android.reina.lib.downloader → sumimakito.android.reina.downloader 更改监听器:DownloadProgressListener → DownloadCallbacks DownloadCallbacks 在每个回调方法中返回一个 String taskId。 如果您仍想使用单任务下载器,DownloadT

android没有adm_这可能是安卓平台上最好的下载器:ADM

下载一直是安卓用户难以磨灭的痛苦,PC上的P2P神器迅雷在安卓平台上的表现也是差强人意。其实除了P2P下载之外,多线程下载也是一种不错的解决方案。他既没有P2P对于上传的要求,也不会占用过多的CPU功耗,可以说是安卓平台上最好的下载解决方案。Advanced Download Manager(简称ADM)就是一款多线程下载器,Pro版的Play售价在12块人民币左右。多线程下载我们可以把线程理解成...

weixin_30465829的博客 1609

Android快速框架+多线程下载框架的技术

Android快速框架+多线程下载框架的技术(Android + Afinal+gson)

android多线程下载原理,安卓多线程断点续传下载功能(靠谱第三方组件,原理demo)...

一,原生的DownloadManager从Android 2.3(API level 9)开始,Android以Service的方式提供了全局的DownloadManager来系统级地优化处理长时间的下载操作。DownloadManager支持失败重试、Notification通知等基本特性。但是DownloadManager提供的接口很有限,虽然有暂停状态却没有提供主动暂停的接口。所以需手动实现...

weixin_35572715的博客 486

Android多线程断点下载框架

支持断点下线,支持多线程下载,支持列表下载管理、支持通知栏下载。

Android快速框架+多线程下载

结合自己项目整合了一套快速框架, Demo代码已实现多线程下载,希望对初学者和需要的开发者有所帮助,也欢迎大家交流。 框架的技术(Android + Afinal+gson)

Aria多线程下载:10个提升下载速度的关键技巧

Aria多线程下载框架是Android平台上最强大的下载解决方案之一,它能显著提升文件下载速度。对于需要快速下载大文件的用户来说,掌握Aria多线程下载的关键技巧至关重要。本文将为您揭示10个实用的Aria下载优化方法,让您的下载体验更加流畅高效。🚀 ## 为什么选择Aria多线程下载? Aria是一个功能强大的Android下载框架,支持多线程下载、断点续传、任务队列管理等核心功能。相比传

gitblog_00936的博客 477

Android快速框架+多线程下载框架的技术(Android + Afinal+gson).zip

Android 应用开发源码 参考与学习使用

上一篇: 2.RecyclerView基本使用
下一篇: 7.如何给RecyclerView添加Click和LongClick事件
爱上学习啊
博客等级 码龄16年 99粉丝 76原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值