ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java安卓班费管理系统:SQLite持久化与业务层设计实战

Java安卓班费管理系统:SQLite持久化与业务层设计实战 简介这是一份基于Java开发的安卓班费管理系统设计源码面向课程设计、毕业设计或班级管理应用开发场景适合有Java基础、想学习Android项目结构的初中级开发者。项目围绕班费透明化管理构建完整业务闭环开支记录与全员查询、一周内限时质疑与班委答复、质疑和应答全员可见半数以上成员确认且质疑处理完毕后可标记为有效体现了多人协作场景下的状态流转与权限控制。源码包共200个文件压缩后约7.17MB以125个XML布局与配置、24个Java逻辑源码、WebP与PNG图片资源以及Gradle、Properties工程配置为主文件组织清晰便于按功能模块阅读和二次开发。目前已有330人学习下载可作为Android客户端开发、数据存储和简单审批流程设计的参考模板。适用于班级日常开支管理、班费公示等场景也适合作为Android期末项目或毕业设计的原型参考。1. 从班级收款对账到 Java 安卓班费管理系统落地班级里收班费最让人头疼的不是收钱而是流动账目和期末余额没法定性。今天只记了一笔买扫把明天又有个活动退费月底群里翻记录翻到怀疑人生。真正需要的不是记账本而是一个到账、支出、余款、成员欠费都能随时查清的班费管理系统。用 Java 写安卓端是很顺手的选择Java 的生态成熟、Android Studio 对 Java 支持完整跑起来少折腾面向对象写起来也直观。“设计源码”这一说法容易让人误解以为拿到源码就直接用。实际上班费管理系统在安卓里的核心点是三层结构数据模型、数据持久化、业务管理和界面展示。本文会把常用的分层方法、表结构、关键代码、参数设置讲清楚后面可以直接照着在自己的项目里跑起来。适合 Java 基础还算稳、想用安卓把生活场景做成工具的开发者。系统不复杂但数据一致性、防误操作的设计值得认真对待。2. 班费管理系统的数据模型设计与选型2.1 为什么先用普通 Java 类定义模型而不是直接建表班费管理系统的业务对象很明确成员学生、班费流水收入/支出、收支分类、结余。如果一上来就写建表语句很容易犯一个错误把界面逻辑、业务逻辑和存储逻辑全部混在一起。常见做法是先用 Java POJO 定义模型类再将对象映射到 SQLite 表结构。这样后续改界面或加功能时不会牵扯到底层存储。底层存储选 SQLite 是因为它适合移动端单机数据场景。班费系统通常是一台手机管理整个班的数据并发量极低但要求数据可持久化、查询响应快。SQLite 不需要额外服务进程文件即数据库先天适合这种轻量管理场景。2.2 成员、流水、分类三张基础表的结构定义对于一个班费管理系统最常用的表结构是三张成员表、流水表、分类表。看看每一张表的字段安排成员表member字段类型说明idINTEGER主键自增nameTEXT成员姓名student_noTEXT学号可选is_activeINTEGER是否还在该班1是0否created_atINTEGER加入时间毫秒时间戳流水表bill字段类型说明idINTEGER主键自增member_idINTEGER关联成员表 id可空表示班集体收支typeINTEGER1收入0支出amountREAL金额单位元category_idINTEGER关联分类表 idnoteTEXT备注如“购买篮球”“春游门票”bill_dateINTEGER发生日期毫秒时间戳created_byTEXT记录人默认当前操作者分类表category字段类型说明idINTEGER主键自增nameTEXT分类名称如“学习用品”“活动费”typeINTEGER适用收支类型1收入0支出2通用为什么需要单独的分类表而不是在流水表里写死分类文本主要原因是分类可维护。学期初只有“买资料”一个支出分类期中又加了“班服”“活动租车”直接在代码里写死就要改源码重新打包用分类表则可以在界面里动态维护。2.3 日期为什么要用毫秒时间戳而不是字符串新人容易把日期字段设成字符串类似2025-03-10。看起来直观但会出现两个问题第一字符串排序是按照字典序排的如果格式不统一有的写2025-3-10排序直接乱掉。第二按月份分组统计时要先做字符串截取SQL 写起来很别扭。更稳的思路是把日期存成INTEGER值为System.currentTimeMillis()。SQLite 内置函数datetime()能随时把毫秒转成可读格式例如按月份统计支出SELECT strftime(%Y-%m, bill_date / 1000, unixepoch, localtime) as month, SUM(amount) as total FROM bill WHERE type 0 GROUP BY month ORDER BY month DESC;逻辑说明bill_date / 1000把毫秒转成秒unixepoch告诉 SQLite 这个字段是 Unix 时间戳localtime让它按本地时区显示月份。strftime(%Y-%m, ...)是日期格式化函数返回如2025-03的月份字符串。SUM(amount)汇总当月支出GROUP BY month按月分组。这样做的好处是后续按天、按月、按年统计只用改strftime的格式参数而不需要迁移表结构。2.4 模型类与表结构的对应映射Java 模型类做的是把 SQL 查询结果和 UI 显示解耦。典型的模型写法public class Bill { private long id; private Long memberId; // 使用 Longnull 表示班级集体收支 private int type; // 1 收入0 支出 private double amount; private long categoryId; private String note; private long billDate; private String createdAt; public boolean isIncome() { return type 1; } public String getDisplayAmount() { return (type 1 ? : -) String.format(%.2f, amount); } // 省略 getter/setter }这段代码中的memberId用Long而不是long是因为成员 id 可能为空班费里购买的班级公共物资不一定挂到某个学生的头上。getDisplayAmount()负责把金额加工成带正负符号的展示字符串这样列表适配器直接调用方法而不必重复写格式化逻辑。参数说明String.format(%.2f, amount)确保金额始终保留两位小数避免出现 13.0 这种不协调的展示。isIncome()方法把 type 的魔法数字语义化界面里判断收入支出时直接调用方法可读性好很多。3. Java 安卓班费管理系统的持久层实现与建库参数3.1 用 SQLiteOpenHelper 管理建库建表与版本升级安卓里操作 SQLite 最常规的方式是继承SQLiteOpenHelper。这个方法有两个关键要理清楚onCreate()只在数据库文件第一次创建时调用onUpgrade()在数据库版本号升高时触发。班费管理系统的下载源码里如果这两个方法写错极易出现“升级后旧表数据全部丢失”的严重问题。典型实现如下public class DbHelper extends SQLiteOpenHelper { private static final String DB_NAME class_fee.db; private static final int DB_VERSION 2; public DbHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } Override public void onCreate(SQLiteDatabase db) { db.execSQL(CREATE TABLE member ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, student_no TEXT, is_active INTEGER DEFAULT 1, created_at INTEGER)); db.execSQL(CREATE TABLE category ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, type INTEGER DEFAULT 2)); db.execSQL(CREATE TABLE bill ( id INTEGER PRIMARY KEY AUTOINCREMENT, member_id INTEGER, type INTEGER NOT NULL, amount REAL NOT NULL, category_id INTEGER, note TEXT, bill_date INTEGER, created_by TEXT, FOREIGN KEY(member_id) REFERENCES member(id), FOREIGN KEY(category_id) REFERENCES category(id))); // 预置收支分类数据 db.execSQL(INSERT INTO category(name, type) VALUES(班费收取, 1)); db.execSQL(INSERT INTO category(name, type) VALUES(资料打印, 0)); db.execSQL(INSERT INTO category(name, type) VALUES(活动支出, 0)); } Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion 2) { db.execSQL(ALTER TABLE bill ADD COLUMN bill_date INTEGER DEFAULT 0); } } }建表 SQL 里的AUTOINCREMENT是自增主键的声明方式。FOREIGN KEY保证了成员和流水的引用完整性避免删掉一个成员后流水中残留无效指向。onUpgrade中用增量迁移策略而不是DROP TABLE这样用户升级版本后账目数据不会丢。if (oldVersion 2)的写法很关键后续版本大于 2 时只会执行新增迁移逻辑不会重复加列。这个版本的onCreate预置了三个分类班费收取、资料打印、活动支出。尾部注释说明这些是种子数据用户进入系统后可以自行增删所以类别表设计成可动态维护而非硬编码。3.2 封装一个单一入口的 DAO避免各处散落 SQL直接 Fragment 里写查询 SQL三个页面就要写三遍“查所有流水”的代码后期改动表结构时得一个个找。常见做法是建一个BillDao把所有读写、统计操作收敛到一个类中。一个班费管理系统的 DAO 往往长这样public class BillDao { private final DbHelper helper; public BillDao(Context context) { helper new DbHelper(context); } public ListBill getRecentBills(int limit) { SQLiteDatabase db helper.getReadableDatabase(); ListBill list new ArrayList(); Cursor cursor db.rawQuery( SELECT * FROM bill ORDER BY bill_date DESC, id DESC LIMIT ?, new String[]{String.valueOf(limit)}); while (cursor.moveToNext()) { list.add(fromCursor(cursor)); } cursor.close(); return list; } public double getBalance() { SQLiteDatabase db helper.getReadableDatabase(); Cursor cursor db.rawQuery( SELECT SUM(CASE WHEN type 1 THEN amount ELSE -amount END) FROM bill, null); double balance 0; if (cursor.moveToFirst()) { balance cursor.getDouble(0); } cursor.close(); return balance; } private Bill fromCursor(Cursor cursor) { Bill bill new Bill(); bill.setId(cursor.getLong(cursor.getColumnIndexOrThrow(id))); bill.setMemberId(cursor.isNull(cursor.getColumnIndexOrThrow(member_id)) ? null : cursor.getLong(cursor.getColumnIndexOrThrow(member_id))); bill.setType(cursor.getInt(cursor.getColumnIndexOrThrow(type))); bill.setAmount(cursor.getDouble(cursor.getColumnIndexOrThrow(amount))); bill.setCategoryId(cursor.getLong(cursor.getColumnIndexOrThrow(category_id))); bill.setNote(cursor.getString(cursor.getColumnIndexOrThrow(note))); bill.setBillDate(cursor.getLong(cursor.getColumnIndexOrThrow(bill_date))); return bill; } }getBalance()里用的SUM(CASE WHEN type 1 THEN amount ELSE -amount END)是一行精妙的 SQL把收入记为正、支出记为负后整体求和免去在 Java 里遍历累加的代码。CASE WHEN是 SQL 的条件表达式type1时取正数否则取负数SUM对结果求和即得当前结余。isNull()用于判断member_id是否为空空值赋null给Long类型避免了getLong在 null 上返回 0 造成的假集体收入。DAO 用getReadableDatabase()还是getWritableDatabase()在 SQLite 中实际差别不大多数场景都会回落成同一个库连接。我在读操作里用前者写操作用后者这是语义区分实际上getReadableDatabase在磁盘满时可能拿到只读连接但它不会真的建出两个数据库文件。3.3 事务处理班费收支连续写入时怎么保证不丢数据一个典型的场景是批量导入班费记录比如学期初一次性录入 40 个成员的缴费。如果逐条 insert任何一条中途失败都会造成数据半途而废一部分记录了另一部分没记录总金额对不上。此时必须开启事务。public void batchInsert(ListBill bills) { SQLiteDatabase db helper.getWritableDatabase(); db.beginTransaction(); try { for (Bill b : bills) { ContentValues values new ContentValues(); values.put(member_id, b.getMemberId()); values.put(type, b.getType()); values.put(amount, b.getAmount()); values.put(category_id, b.getCategoryId()); values.put(note, b.getNote()); values.put(bill_date, b.getBillDate()); db.insert(bill, null, values); } db.setTransactionSuccessful(); } finally { db.endTransaction(); } }beginTransaction()开始事务setTransactionSuccessful()标记成功endTransaction()在 finally 中无论成败都会结束事务。如果循环中某条插入抛出异常程序不会执行到setTransactionSuccessful()endTransaction()会自动回滚整批数据。这样批量写入时要么全部成功要么全部不写。参数说明insert第二个参数传null表示不给空行如果表中某列没有值但不允许为 null这个参数的意义才会显现出来。建议先自查member_id和分类是否合法再做操作因为 SQLite 默认没有启用外键约束。4. 班费管理系统的业务层设计余额计算与防呆校验4.1 集体收支和个人收支分开处理的业务边界班费系统里最容易被写乱的逻辑是“这个钱是谁的”。买羽毛球拍是集体支出某学生补交的是个人收入期末余额却是汇总所有收支。业务层里应当定义一个明确的服务类FeeService所有对外暴露的用例都收敛在它内部。个人收支与集体收支的区别主要影响统计口径个人未交列表要按 member 分组找该成员累计收入是否达到应缴标准而集体活动花费则挂在 category 上分析的是费用去向。在源码实现上常见做法是把这两种概念做成两个方法而不是一个多参数方法避免在上层使用时忘了传关键参数public class FeeService { private final BillDao billDao; public FeeService(BillDao billDao) { this.billDao billDao; } public double getMemberPaidAmount(long memberId) { return billDao.getMemberTotalByType(memberId, 1); } public double getMemberUnpaidAmount(long memberId, double standardFee) { double paid getMemberPaidAmount(memberId); return Math.max(0, standardFee - paid); } }Math.max(0, standardFee - paid)把负值截断成 0防止未交金额出现负数。这个处理在展示层很有用界面直接取数渲染不额外判断。4.2 删除流水时的防误删设计和撤销操作班费数据删错了基本救不回来SQLite 没有自带的回收站。生产经验不足的人往往直接执行db.delete(bill, id?, args)一个手滑把整月账目清空。稳妥的做法是软删除给bill表增加一个deleted字段删除操作变成UPDATEALTER TABLE bill ADD COLUMN deleted INTEGER NOT NULL DEFAULT 0;然后业务层的删除方法执行public void softDeleteBill(long billId) { SQLiteDatabase db helper.getWritableDatabase(); ContentValues values new ContentValues(); values.put(deleted, 1); db.update(bill, values, id ?, new String[]{String.valueOf(billId)}); }更新而不是删除意味着账单还真实存在于数据库中只是界面查询时统一加条件AND deleted 0。撤销删除也只需要把deleted改回 0。收益很直接就算误操作了还能恢复同时保留了完整审计链路这对班费这种多人关注的财务数据非常重要。界面侧删除后弹出轻量提示做一个“撤销”按钮回调recoverBill(id)即可。4.3 每月结余和成员欠费统计的 SQL 设计结余不只是当前总额还需要按月份维度看开销趋势。一个实用的统计方法是按月拿上月末结余作为月初结余再通过当月流水计算期末结余。不过这种滚动计算在法理上有点绕更直观的做法是只算累计结余按时间正序排列public ListBalancePoint getCumulativeBalanceTrend() { SQLiteDatabase db helper.getReadableDatabase(); ListBalancePoint points new ArrayList(); Cursor cursor db.rawQuery( SELECT bill_date, SUM(CASE WHEN type 1 THEN amount ELSE -amount END) OVER (ORDER BY bill_date) as cumulative FROM bill WHERE deleted 0 GROUP BY bill_date, type, amount, id ORDER BY bill_date, null); while (cursor.moveToNext()) { BalancePoint p new BalancePoint(); p.date cursor.getLong(0); p.cumulative cursor.getDouble(1); points.add(p); } cursor.close(); return points; }这里用到了窗口函数OVER (ORDER BY bill_date)它对每一条记录计算截止到当前行的累计总和。与普通的GROUP BY不同窗口函数不会合并行每一行都能得到独立的累计值。这样画折线图时横轴是时间纵轴是累计结余一条连续曲线就出来了。需要注意一点SQLite 的窗口函数要求 Android 系统版本提供的 SQLite 库在 3.25 以上。Android 7 以下可能不支持。如果目标机型偏老兜底方案是在 Java 层遍历流水手动累加。我一般这么处理先判断数据库版本支持情况不支持时用 Java 循环逻辑替代代码不复杂但能显著提升兼容性。4.4 金额字段的精度处理策略浮点数double直接存储金额常识上似乎没什么问题但严格来说有精度隐患0.1 0.2 在二进制浮点表示下不等于 0.3。班费金额不会涉及小数太多但也可能因为数学计算出现19.999999这种结果。两种处理方案一种是用整数存“分”一种是在读写边界统一做四舍五入。比较折中的做法是存储仍用 REAL但在每次写库前用BigDecimal做一位换算并设置舍入模式private long convertToCents(double yuan) { return BigDecimal.valueOf(yuan) .setScale(2, RoundingMode.HALF_UP) .movePointRight(2) .longValue(); }BigDecimal.valueOf(yuan)直接接收 double 不丢失二进制精度setScale(2, HALF_UP)保留两位小数并四舍五入movePointRight(2)把元变成分最终longValue()输出整数。这套方案的代价是数据库内部统一以分存储展示时再除以 100但换来的是任何金额运算都不会出现二进制浮点误差。如果不想改表结构至少保证所有从 UI 拿到的金额字符串在入库前都经过BigDecimal不要直接拿Double.parseDouble的结果写进 SQLite。5. 安卓界面层接入与列表展示的关键参数5.1 RecyclerView 适配器绑定 Java 模型的常用写法界面部分比较重要的是列表的响应性和数据绑定。班费流水列表用RecyclerViewListAdapter是很常用的方式。ListAdapter内部用DiffUtil比较新旧列表差异只刷新有变化的部分比直接notifyDataSetChanged()成本小很多。看代码public class BillAdapter extends ListAdapterBill, BillAdapter.BillViewHolder { protected BillAdapter() { super(new DiffUtil.ItemCallbackBill() { Override public boolean areItemsTheSame(Bill oldItem, Bill newItem) { return oldItem.getId() newItem.getId(); } Override public boolean areContentsTheSame(Bill oldItem, Bill newItem) { return oldItem.getAmount() newItem.getAmount() oldItem.getBillDate() newItem.getBillDate() Objects.equals(oldItem.getNote(), newItem.getNote()); } }); } Override public void onBindViewHolder(BillViewHolder holder, int position) { Bill bill getItem(position); holder.amountText.setText(bill.getDisplayAmount()); holder.noteText.setText(bill.getNote()); holder.dateText.setText(DateFormat.format(yyyy-MM-dd, bill.getBillDate())); if (bill.isIncome()) { holder.amountText.setTextColor(0xFF2E7D32); } else { holder.amountText.setTextColor(0xFFC62828); } } }areItemsTheSame判断是不是同一条数据主键 id 相等即认为是同一条areContentsTheSame判断数据内容是否变化参与比较的字段至少有金额、日期、备注。收入显示绿色0xFF2E7D32支出显示红色0xFFC62828这两个十六进制颜色值是 Material Design 的深绿和深红。如果源码里只在 adapter 这样写没有设置 ViewHolder 里的setIsRecyclable(false)那么复用列表项时颜色会错乱上滑后收入支出颜色混乱就是这里出的问题。5.2 收支录入表单的输入校验与参数约束录入页面是班费系统最容易产生脏数据的入口。金额为空、负数、分类没选各种情况都要拦截。常用做法是把校验集中在保存按钮的点击逻辑里避免散落在 XML 或自定义控件中public void onSaveClick(View view) { String amountStr amountInput.getText().toString().trim(); String noteStr noteInput.getText().toString().trim(); if (amountStr.isEmpty()) { amountInput.setError(金额不能为空); return; } double amount; try { amount Double.parseDouble(amountStr); } catch (NumberFormatException e) { amountInput.setError(金额格式不正确); return; } if (amount 0) { amountInput.setError(金额必须大于 0); return; } if (selectedCategoryId 0) { Toast.makeText(this, 请选择收支分类, Toast.LENGTH_SHORT).show(); return; } Bill bill new Bill(); bill.setType(incomeRadio.isChecked() ? 1 : 0); bill.setAmount(amount); bill.setCategoryId(selectedCategoryId); bill.setNote(noteStr); bill.setBillDate(System.currentTimeMillis()); billDao.insert(bill); finish(); }setError()是 Android 原生EditText的表单错误提示方法会在输入框底部显示红字并有图标提示。金额用try-catch包住Double.parseDouble防止用户输入非数字字符导致崩溃。System.currentTimeMillis()直接作为记账时间不做手工录入是为了减少脏数据如果需求要求支持补录历史日期再改为日期控件获取毫秒值。5.3 可视化“本月支出占比”统计图表的 Java 接入文本列表之外分类占比是一种直观的视觉补充。常见的做法是引入MPAndroidChart的PieChart。库的体积稍大但班费系统的界面数据量小用起来很稳妥。核心是构建PieEntry列表private void loadExpensePieChart() { ListCategoryExpense expenses feeDao.getExpenseGroupByCategory(); ArrayListPieEntry entries new ArrayList(); for (CategoryExpense e : expenses) { entries.add(new PieEntry((float) e.totalAmount, e.categoryName)); } PieDataSet dataSet new PieDataSet(entries, 支出分类); dataSet.setColors(new int[]{0xFF42A5F5, 0xFF66BB6A, 0xFFFFA726, 0xFFEF5350}, 255); PieData data new PieData(dataSet); data.setValueFormatter(new PercentFormatter(pieChart)); pieChart.setData(data); pieChart.setUsePercentValues(true); pieChart.invalidate(); }PieEntry构造函数的第一个参数是值第二个是标签文本对应分类名。setColors(int[])里传的是颜色数组饼图的每个扇区按数组顺序取色数组较短时循环使用。PercentFormatter(pieChart)让扇区上显示百分比而不是原始金额setUsePercentValues(true)是配套设置。invalidate()方法是强制重绘的关键改了数据后可别忘了调用否则界面不会刷新。6. 按月份筛选和把源码跑起来前要过的三道关卡6.1 月份筛选列表的快速实现班费查询最常用的操作是“看这个月花在哪”。与其在 SQL 里拼字符串找日期不如把日历控件的选择结果转成毫秒范围public ListBill getBillsBetween(long startMillis, long endMillis) { SQLiteDatabase db helper.getReadableDatabase(); ListBill list new ArrayList(); Cursor cursor db.rawQuery( SELECT * FROM bill WHERE deleted 0 AND bill_date ? AND bill_date ? ORDER BY bill_date DESC, new String[]{String.valueOf(startMillis), String.valueOf(endMillis)}); while (cursor.moveToNext()) { list.add(fromCursor(cursor)); } cursor.close(); return list; }参数含义startMillis是当月 1 日 0 点的毫秒数endMillis是下个月 1 日 0 点的毫秒数。用右开区间[start, end)是为了天然避开月底最后一天 23:59:59 到凌晨 0 点的边界问题不需要额外加一天再截断。Calendar 生成这两个值的方式有多种我习惯在 Java 里这么构造Calendar c Calendar.getInstance(); c.set(Calendar.DAY_OF_MONTH, 1); c.set(Calendar.HOUR_OF_DAY, 0); c.set(Calendar.MINUTE, 0); c.set(Calendar.SECOND, 0); c.set(Calendar.MILLISECOND, 0); long start c.getTimeInMillis(); c.add(Calendar.MONTH, 1); long end c.getTimeInMillis();只要把Calendar配置一次SQL 里就能稳定地拿到当月数据比strftime(%Y-%m, ...) ?这类写法更不容易踩时区坑。6.2 第一道关卡手机存储权限和数据库路径错误把源码工程导入 Android Studio 后最容易发生在早期的报错与数据库的读写路径有关。SQLiteOpenHelper 默认把数据库文件创建在/data/data/包名/databases/目录下应用卸载或清除数据会一并删除。如果你的代码试图直接读写外部存储的某个.db文件必须确保Context.getExternalFilesDir()不为 null且运行在 Android 6 以上时要处理动态权限。代码排查思路看到SQLiteCantOpenDatabaseException时先看完整路径是否存在于应用的私有目录排查IOException: unable to open database file时删除应用数据重装通常能恢复说明 SD 卡路径只读或目录尚未创建。6.3 第二道关卡列表数据重复与 DiffUtil 更新无效RecyclerView 列表刷新后出现重复数据基本原因是新列表传入submitList()前没有经过去重或者把原列表 addAll 后没清理旧数据。用ListAdapter时要注意submitList内部是异步计算差异如果连续调用两次相同列表第二次 diff 结果为空界面不会变化。想要强制刷新时调用submitList(null)再submitList(currentList)是以新数据触发重绘的常见手段但这不是常用路径不要养成习惯。更稳妥的做法是确保每次数据变更都从 DAO 重新查询保证新列表对象确实包含变化结果。6.4 第三道关卡老机型不显示图标或图表空白图表不显示这件事常见原因是布局中PieChart控件没有设置宽高或者数据里金额都为 0 导致没有扇区。其次检查MPAndroidChart的依赖是否与当前 AndroidX 版本冲突如果日志出现NoClassDefFoundError,优先尝试把依赖升级到与 targetSdk 匹配的版本。另外老手机上PieChart的图例较多时可关闭setDrawEntryLabels(true)的默认显示把文字和百分比放到饼图内部或图例中避免横屏时文字挤压重叠。这样源码拿到的第一屏就能看到分类占比而不是空白。本文还有配套的精品资源点击获取
返回列表