安卓开发使用POI导出Excel表格
关键词【安卓开发】【导出EXCEL表格】【XLS】【XLSX】
在开始之前我先介绍一下在安卓上导出Excel的多种方案
1.JXL
JXL(Java Excel API)则专注于处理Excel文件,它是Apache POI的一个轻量级替代方案。JXL库的API更为简洁,对于只涉及Excel操作的应用程序来说,它可能更受青睐。
但是我使用下来有几个痛点不得不放弃JXL
优点上JXL使用简单,可以设计出样式丰富的xls表格,满足只需要少量数据导出表格的需求
缺点只支持XLS导出(单sheet最多65535行)不满足客户几十万条数据单表导出的需求,导出性能较差占用内存过高
总体:如果你需要导出的Excel数据量不大,需要快速接入可以选择JXL进行开发,这里我放一个可以参考的链接
作者:motosheep 链接:关于安卓jxl的excel操作(一)_安卓开发处理excel-CSDN博客
2.POI for android
这是我最终使用的,在github上有个老外大神基于Apache POI 修改最终用于Android 5+的Excel导出库,这里我先放个链接:
作者:andruhon 仓库链接:andruhon/android5xlsx: Reading and Writing XLSX and XLS on Android 5+ with Apache POI
使用 Apache POI 在 Android 5 上读取和写入 XLSX 和 XLS
在 Android 上使用 Apache POI 和 Dalvik VM 是一项相当具有挑战性的任务。在 Android 5+ 上使用 Apache POI 与 ART VM 和 Build Tools 21+ 相比,要容易得多。
基于这个库我实现了在Android5.1、Android7.1、Android11上运行并导出Excel,并且性能由于采用了SXSSFWorkbook
SXSSFWorkbook是Apache POI库的一部分,专门用于处理大数据量的Excel导出。它采用了流式处理的方式,只会将最新的Excel行保存在内存中,而之前的行会被写入到硬盘的临时文件中。这种方式通过牺牲部分功能(如公式求值、Sheet克隆等)来换取内存的高效使用,避免在处理大数据量时发生内存溢出。
SXSSFWorkbook:
属于Apache POI库(3.8+版本),专为大数据量导出(.xlsx格式) 设计。通过流式处理将数据分批写入磁盘,仅保留少量行在内存中,避免OOM。适合百万级数据导出136。
WritableWorkbook:
属于JExcelAPI(JXL库),主要处理旧版.xls格式(Excel 97-2003)。全内存操作,数据量超过65536行会报错,且性能在大数据量下显著下降(如20万行卡顿)
内存占用也相较于JXL有大幅度降低
实测POI导出162000条数据
内存占用30MB浮动
Excel生成时间37978 ms
应该能满足大部分开发者的需求
- 选
SXSSFWorkbook如果:
✅ 需导出 >1万行数据(尤其是.xlsx);
✅ 内存有限,避免OOM;
✅ 接受牺牲部分功能(如公式计算)。 - 选
WritableWorkbook如果:
✅ 处理 <6.5万行的小型.xls文件;
✅ 依赖旧版JXL库且不能导出XLSX
开始使用POI
1.引入jar
implementation files('libs\\poi-3.12-android-20240110-fixed.jar')
implementation files('libs\\poi-ooxml-schemas-3.12-20150511.jar')
2.添加ExcelUtils工具类
这个类是我写在自己项目上的,格式、调用方式只能当作参考,代码逻辑或许不是很好,仅供参考:
/**
* Excel导出工具
*
* @author Ganfandw
* @date : 2025/05/29 修改
*/
public class ExcelUtils {
//Excel文件存储路径,可以自行修改方法 实现动态路径
private static String FILE_PATH = Environment.getExternalStorageDirectory() + File.separator + "Demo" + "/Excel/";
//数据Bean 里面存放导出的数据,这里我会添加一个内部类用于演示
private DataBean bean;
private Cell cell;
private static String fileName = "";
private static String sheetName = "";
private static SXSSFWorkbook wb;
private static Sheet sheet;
private static String[] title = {"用户id", "时间(年/月/日/时间)", "数据1", "数据2"};
private static File excelFile;
private static final int CELL_WIDTH = 512;
private static int lastRowNum = 2;
private static FileOutputStream fileOutputStream = null;
private static long startTime;
private static ExcelUtils instance = null;
private ProgressInterface mProgressBar = null;
private static CellStyle cellStyle_cell = null;
private boolean cancelTask = false;
private String TAG = "ExcelUtils";
private Context mContext;
// 私有构造方法,防止外部直接实例化
private ExcelUtils(Context context) {
// 初始化操作
mContext = context;
}
// 获取单例实例的方法
public static ExcelUtils getInstance(Context context) {
if (instance == null) {
synchronized (ExcelUtils.class) {
if (instance == null) {
instance = new ExcelUtils(context);
}
}
}
return instance;
}
/**
* 初始化Excel表格文件名、sheet名、第一列
*/
public ExcelUtils build(String fileName, String sheetName, String medicalCode, String userName, String userGender, String doctorName) {
try {
startTime = System.currentTimeMillis(); //起始时间
cancelTask = false;
//初始化Excel文件目录
initExcelPath();
setFileName(fileName);
setSheetName(sheetName);
File dir = new File(FILE_PATH);
if (!dir.exists()) dir.mkdirs();
excelFile = new File(FILE_PATH + fileName + ".xlsx");
// 创建excel xlsx格式
wb = new SXSSFWorkbook(100);
cellStyle_cell = wb.createCellStyle();
cellStyle_cell.setBorderTop(CellStyle.BORDER_THIN);
cellStyle_cell.setBorderBottom(CellStyle.BORDER_THIN);
cellStyle_cell.setBorderLeft(CellStyle.BORDER_THIN);
cellStyle_cell.setBorderRight(CellStyle.BORDER_THIN);
cellStyle_cell.setAlignment(CellStyle.ALIGN_CENTER);
cellStyle_cell.setVerticalAlignment(CellStyle.VERTICAL_CENTER);
// 创建工作表
sheet = wb.createSheet(sheetName);
//创建行对象
Row row = sheet.createRow(0);
row.setHeight((short) (3 * 256));
// 创建单元格并应用样式
Cell cell0 = row.createCell(0);
cell0.setCellValue("病例号");
cell0.setCellStyle(cellStyle_cell);
Cell cell1 = row.createCell(1);
cell1.setCellValue(medicalCode);
cell1.setCellStyle(cellStyle_cell);
Cell cell2 = row.createCell(2);
cell2.setCellValue("姓名");
cell2.setCellStyle(cellStyle_cell);
Cell cell3 = row.createCell(3);
cell3.setCellValue(userName);
cell3.setCellStyle(cellStyle_cell);
Cell cell4 = row.createCell(4);
cell4.setCellValue("性别");
cell4.setCellStyle(cellStyle_cell);
Cell cell5 = row.createCell(5);
cell5.setCellValue(userGender);
cell5.setCellStyle(cellStyle_cell);
Cell cell6 = row.createCell(6);
cell6.setCellValue("医生");
cell6.setCellStyle(cellStyle_cell);
Cell cell7 = row.createCell(7);
cell7.setCellValue(doctorName);
cell7.setCellStyle(cellStyle_cell);
//创建行对象
Row row1 = sheet.createRow(1);
row1.setHeight((short) (3 * 256));
// 设置有效数据的行数和列数
int colNum = title.length;
for (int i = 0; i < colNum; i++) {
sheet.setColumnWidth(i, CELL_WIDTH * title[i].length()); // 显示8个字符的宽度
cell = row1.createCell(i);
cell.setCellStyle(cellStyle_cell);
cell.setCellValue(title[i]);//第一行
}
lastRowNum = 2;
fileOutputStream = new FileOutputStream(excelFile);
} catch (Exception e) {
}
return instance;
}
//同步锁 synchronization lock
private static ReentrantLock reentrantLock = new ReentrantLock(true);
/**
* 写数据到Excel
*
* @param listData
* @return
*/
public ExcelUtils write(List<DataBean> listData) {
try {
reentrantLock.lock();
Row row = null;
// 导入数据
Log.d(TAG, "开始写入数据,当前总行数" + sheet.getLastRowNum() + " / " + lastRowNum);
for (int rowNum = 0; rowNum < listData.size(); rowNum++, lastRowNum++) {
if (cancelTask) {
Log.d(TAG, "取消任务");
return instance;
}
// 之所以rowNum + 1 是因为要设置第二行单元格
row = sheet.createRow(lastRowNum);
// 设置单元格显示宽度
row.setHeightInPoints(26f);
bean = listData.get(rowNum);
for (int j = 0; j < title.length; j++) {
cell = row.createCell(j);
cell.setCellStyle(cellStyle_cell);
//要和title[]一一对应
switch (j) {
case 0:
//实时时间
cell.setCellValue(bean.getUserId());
break;
case 1:
//实时压力
cell.setCellValue(bean.getData1());
break;
case 2:
//累计高压时长
cell.setCellValue(bean.getData2());
break;
case 3:
//实时时间
cell.setCellValue(bean.getRealTime());
break;
}
}
}
} catch (Exception e) {
Log.e(TAG, "WRITE EXCEL" + e.getMessage() + e.getStackTrace());
} finally {
long endTime = System.currentTimeMillis(); //结束时间
long runTime = endTime - startTime;
Log.e("Excel", String.format("生成Excel使用时间 %d ms", runTime));
reentrantLock.unlock();
}
return instance;
}
public void cancelTask() {
cancelTask = true;
}
/**
* 初始化Excel表格文件名、sheet名、第一列
*/
public ExcelUtils progress(ProgressInterface progressInterface) {
this.mProgressBar = progressInterface;
return instance;
}
/**
* 这里我设置的-1是导出成功,-2是导出报错
* 如果要添加导出进度,你可以在activity里添加自定义弹窗,根据数据库里面的数据进行分页,给progress设置max值
* 每执行一次write方法,进度+1 实时设置上去 直到读取到-1 就导出完成
* */
public interface ProgressInterface {
void OnProgress(int value);
}
private static boolean commitSuccess = false;
/**
* 提交数据到文件中
*
* @return boolean true 写入成功
*/
@SuppressLint("WrongConstant")
public boolean commit() {
commitSuccess = false;
if (cancelTask) {
Log.d(TAG, "取消任务");
return false;
}
//动态获取权限
//初始化软件的动态权限 Initializes dynamic permissions for software
PermissionX.init((FragmentActivity) mContext)
.permissions(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
.request(new RequestCallback() {
@Override
public void onResult(boolean allGranted, @NonNull List<String> grantedList, @NonNull List<String> deniedList) {
if (allGranted) {
try {
wb.write(fileOutputStream);
wb.close();
long endTime = System.currentTimeMillis(); //结束时间
long runTime = endTime - startTime;
commitSuccess = true;
if (mProgressBar != null) mProgressBar.OnProgress(-1);
Log.e(TAG, String.format("Excel制作完成使用时间 %d ms", runTime));
} catch (IOException e) {
if (e.toString().contains("EPERM")) {
commitSuccess = true;
if (mProgressBar != null) mProgressBar.OnProgress(-1);
} else {
if (mProgressBar != null) mProgressBar.OnProgress(-2);
Log.e(TAG, e.toString());
commitSuccess = false;
}
} finally {
if (fileOutputStream != null) {
try {
fileOutputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
fileOutputStream = null;
}
}
} else {
Log.e(TAG, "未赋予存储权限,Excel导出失败");
if (mProgressBar != null) mProgressBar.OnProgress(-2);
}
}
});
return commitSuccess;
}
/**
* 初始化Excel文件路径
*/
private static void initExcelPath() {
Log.e("Excel", "检测文件路径");
File PATH = new File(FILE_PATH);
// 目录不存在则自动创建目录
if (!PATH.exists()) {
PATH.mkdirs();
}
}
public static void setFileName(String fileName) {
ExcelUtils.fileName = fileName;
}
public static void setSheetName(String sheetName) {
ExcelUtils.sheetName = sheetName;
}
public static class DataBean {
private Long id;
//配置名称 用户自定义id
private Long userId;
//配置名称 演示数据1
private String data1;
//配置名称 演示数据2
private String data2;
//实时时间
private long realTime;
public DataBean(Long id, Long userId, String data1, String data2,
long realTime) {
this.id = id;
this.userId = userId;
this.data1 = data1;
this.data2 = data2;
this.realTime = realTime;
}
public DataBean() {
}
public Long getId() {
return this.id;
}
public void setId(Long id) {
this.id = id;
}
public Long getUserId() {
return this.userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public String getData1() {
return data1;
}
public String getData2() {
return data2;
}
public void setData1(String data1) {
this.data1 = data1;
}
public void setData2(String data2) {
this.data2 = data2;
}
public long getRealTime() {
return this.realTime;
}
public void setRealTime(long realTime) {
this.realTime = realTime;
}
}
}
3.调用工具类并导出Excel
new Thread(new Runnable() {
@Override
public void run() {
excelUtils = ExcelUtils.getInstance(PwmControl.this);
excelUtils.build("演示参考", "Record", "SOS", "囧.史密斯", "18", "凉宫").progress(new ExcelUtils.ProgressInterface() {
@Override
public void OnProgress(int value) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (value == -1) {
Log.e("ExcelUtils", "导出成功");
//这里做延迟是因为安卓开发板读写性能很差,需要一段时间才能彻底写入,如果是手机可以取消掉handler
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
//这个自己去设计逻辑 在哪里取消
if (!cancelTask) {
// progressDialog.setText(getString(R.string.text_export_success) + fileName);
// progressDialog.hideProgress();
// progressDialog.setCancelVisible(false);
// progressDialog.setConfirmVisible(true);
}
}
}, 5 * 1000);
} else if (value == -2) {
runOnUiThread(new Runnable() {
@Override
public void run() {
// cancelTask = true;
// progressDialog.setText("Excel导出失败,请检查系统日志。");
// progressDialog.hideProgress();
}
});
}
}
});
}
});
//progress = 0;
List<ExcelUtils.DataBean> chunk = new ArrayList<>();
// chunk = daoSession.getDataBeanDao().queryBuilder()
// .where(DataBeanDao.Properties.UserId.eq(userBean.getId()))
// .offset(offset)
// .limit(pageSize)
// .list();
for (int i = 0; i < 1000; i++) {
chunk.add(new ExcelUtils.DataBean(1L, (long) i + 1000, "演示数据1" + i, "演示数据2" + i, 1));
}
// if (chunk.isEmpty()) {
// break;
// }
if (cancelTask) {
excelUtils.cancelTask();
return;
}
excelUtils.write(chunk);
//progress++;
// runOnUiThread(new Runnable() {
// @Override
// public void run() {
// progressDialog.setProgress(progress);
// }
// });
chunk.clear();
chunk = null;
// offset += pageSize;
excelUtils.commit();
}
}).start();
}
});
这些代码只是用于参考,并不是实际生产环境的代码,如果要监听进度可以自行实现progress 弹窗,建议导出Excel时 不要做其他操作
执行完成后导出Excel,在Windows上打开的效果,具体格式自行调整

如果你有更好的建议欢迎评论



2169

被折叠的 条评论
为什么被折叠?



