Android记账APP源码:SQLite本地存储+MPAndroidChart图表可视化+多维度账单查询

Android记账APP源码:SQLite本地存储+MPAndroidChart图表可视化+多维度账单查询 本文还有配套的精品资源点击获取简介这个安卓记账应用源码可以直接导入Android Studio运行兼容主流Android系统版本。支持手动录入收入和支出自动按日、月汇总金额账单列表支持按时间范围筛选也支持按类型餐饮、交通、工资等快速检索。数据全部存入本地SQLite数据库包含完整的建表语句和增删改查逻辑事务处理规范。图表部分用MPAndroidChart实现柱状图对比每月收支总额饼图直观展示各类支出占比。界面采用FragmentViewPager滑动切换不同功能页记账、统计、查询自定义Dialog用于新增/编辑条目软键盘弹出逻辑做了适配优化。列表和网格视图均使用标准Adapter封装图标与背景样式通过drawable资源统一管理。代码结构分层清晰关键步骤配有中文注释涵盖Activity间传值、View属性动态设置、自定义View绘制等典型Android开发场景适合新手练手或作为二次开发基础模板。1. 项目概述为什么这个记账源码值得你花30分钟认真看一遍我带过不少刚从培训班出来的Android新手也帮朋友公司做过技术面试发现一个特别普遍的现象很多人能背出Activity生命周期、能默写RecyclerView的三步绑定流程但一让他从零搭个“能用”的小工具——比如记账App——就卡在数据库建表字段怎么设计、日期范围查询SQL怎么写、图表数据怎么从Cursor里喂给MPAndroidChart、甚至软键盘弹出后布局被顶上去怎么修……不是不会是没在一个真实、完整、可运行的上下文里见过它们是怎么咬合在一起工作的。这个“Android记账APP源码”就是为解决这个问题而生的。它不是一个教学Demo也不是一个只跑通首页的半成品而是一个真正能装进手机、每天打开记两笔、查一笔、看一眼饼图就明白上个月奶茶花了多少钱的轻量级生产级模板。核心关键词——Android记账源码、SQLite账单管理、MPAndroidChart图表——不是标签而是它每一行代码都在兑现的承诺。它用最朴素的技术栈SQLite 原生View Fragment完成了个人财务管理中最刚需的闭环录入 → 存储 → 查询 → 可视化。没有炫技的Jetpack Compose不依赖任何后端服务所有数据100%本地存储隐私可控没有过度分层的MVP/MVVM抽象但结构清晰到你能一眼看出DatabaseHelper负责建表、BillDao封装CRUD、BillAdapter绑定列表、ChartFragment专注绘图——这种“克制的工程感”恰恰是初学者最需要临摹的范本。我把它部署到自己备用机上用了三个月每天随手记早餐5元、地铁4元月底打开饼图餐饮占比28%交通12%娱乐8%工资收入柱状图稳稳压过支出柱——它不性感但足够诚实、足够可靠、足够让你把注意力从“怎么让App跑起来”转移到“怎么让我的钱花得更明白”。如果你正卡在Android开发的“学完理论却写不出完整App”的瓶颈期或者手头有个小想法比如宿舍水电费分摊、健身打卡统计、读书进度追踪想快速落地验证那么这个项目就是你此刻最该打开的那一个。它不教你“什么是SQLite”而是直接给你一份带注释的.sql建表语句它不空谈“图表可视化”而是把PieEntry对象怎么从ListBill里抽出来、怎么按类型聚合、怎么处理空数据防崩溃的完整链路一行行写在ChartManager.java里。接下来的内容我会带你一层层剥开它的骨架告诉你每个关键选择背后的“为什么”以及我在实际调试中踩过的那些坑——比如为什么ViewPager要配合FragmentStatePagerAdapter而不是FragmentPagerAdapter为什么DatePickerDialog的月份值要1为什么饼图里“其他”类目占比低于5%时要自动合并……这些细节才是让一个源码项目从“能跑”变成“好用”的分水岭。2. 整体架构与技术选型逻辑为什么是SQLite MPAndroidChart Fragment2.1 本地存储为什么死守SQLite而不是Room或GreenDAO看到项目描述里写着“SQLite本地存储”可能有同学会嘀咕“现在都2024年了还手写SQLRoom不是更现代、更安全吗” 这是个好问题。答案很实在对于一个单用户、低频写入、强离线需求的记账AppSQLite原生API的确定性远胜于任何ORM框架的抽象红利。先说Room。它确实优雅用Entity注解定义表Dao接口声明方法编译期生成实现类自动处理线程切换和SQL注入防护。但代价是什么你需要额外学习一套Annotation语法理解LiveData/Flow如何与UI绑定当某个查询报错时错误堆栈会穿过Room生成的代理类、SupportSQLiteDatabase、最终才落到你的SQL语句上——调试路径变长了。更重要的是Room的Query要求你写的是SQL字符串而这个记账项目里最关键的“按月汇总收支”查询是这样的SELECT strftime(%Y-%m, date) as month, SUM(CASE WHEN type income THEN amount ELSE 0 END) as income_total, SUM(CASE WHEN type expense THEN amount ELSE 0 END) as expense_total FROM bills WHERE date BETWEEN 2024-01-01 AND 2024-12-31 GROUP BY strftime(%Y-%m, date) ORDER BY month DESC;这个查询里嵌套了CASE WHEN、strftime日期格式化、GROUP BY分组聚合——Room的Query完全支持但一旦你哪天想加个动态条件比如“只查餐饮和交通类”就得拼接字符串反而绕回了原生SQL的老路。而本项目直接在BillDao.java里用database.rawQuery()执行参数通过selectionArgs传入既安全又直观public Cursor queryMonthlySummary(String startDate, String endDate) { String sql SELECT ... ; // 上面那段SQL return database.rawQuery(sql, new String[]{startDate, endDate}); }再看GreenDAO这类老牌ORM它生成的DAO类代码量巨大对初学者理解数据流向反而是干扰。而本项目里DatabaseHelper.java只有不到200行onCreate()方法里清清楚楚写着建表语句db.execSQL(CREATE TABLE IF NOT EXISTS bills ( _id INTEGER PRIMARY KEY AUTOINCREMENT, date TEXT NOT NULL, type TEXT NOT NULL CHECK(type IN (income,expense)), category TEXT NOT NULL, amount REAL NOT NULL, note TEXT ));字段命名直白date,type,category约束明确CHECK(type IN (income,expense))连AUTOINCREMENT都写全了——这不是偷懒是刻意降低认知负荷。当你第一次修改代码想加个“支付方式”字段时你不需要去翻Room文档查ColumnInfo怎么用直接在这里加一行payment_method TEXT再在Bill.java实体类里加个字段BillDao里补个insert()参数三步搞定。这种“所见即所得”的掌控感对建立开发信心至关重要。提示SQLite的事务处理在本项目中体现得很扎实。比如在BillDao.update()方法里你会看到java database.beginTransaction(); try { ContentValues values new ContentValues(); values.put(date, bill.getDate()); // ... 其他字段 int rows database.update(bills, values, _id?, new String[]{String.valueOf(bill.getId())}); database.setTransactionSuccessful(); } finally { database.endTransaction(); }这种beginTransaction()/setTransactionSuccessful()/endTransaction()的三段式确保了即使更新过程中发生异常比如磁盘满数据也不会处于中间状态。很多新手会忽略这点直接裸调update()结果在并发写入时出现数据错乱。2.2 图表可视化为什么选MPAndroidChart而不是Android自带Canvas或ECharts图表模块是这个项目的亮点也是最容易被低估的部分。有人会觉得“不就是画个饼图吗自己用Canvas画几个扇形不就行了” 真这么简单就不会有MPAndroidChart这个GitHub星标超2万的库了。先说Canvas手绘的硬伤。画一个饼图你需要1. 计算每个类目的金额总和得出占比2. 把占比转成弧度angle ratio * 360f3. 用Path.arcTo()画弧线Canvas.drawArc()填充颜色4. 手动计算每个扇形的中心点绘制文字标签5. 处理扇形太小5°时文字重叠6. 实现点击高亮、拖拽缩放、动画入场……这一套下来光是坐标系转换和三角函数就够新手debug半天。而MPAndroidChart把这些封装成了几行代码PieDataSet dataSet new PieDataSet(entries, 支出类别); dataSet.setColors(ColorTemplate.MATERIAL_COLORS); dataSet.setValueTextColor(Color.BLACK); PieData data new PieData(dataSet); pieChart.setData(data); pieChart.invalidate(); // 触发重绘更关键的是它解决了数据与UI的实时同步问题。记账App的核心交互是用户新增一条“外卖35元”列表立刻刷新饼图里的“餐饮”占比立刻变大。MPAndroidChart的setData()方法内部做了深度比较只更新变化的数据点避免整图重绘带来的卡顿。而如果你手写Canvas每次数据变都要手动调用invalidate()然后在onDraw()里重新计算所有坐标——性能损耗肉眼可见。至于ECharts它是Web端王者但移植到Android需要WebView容器带来三大问题启动慢WebView初始化耗时、内存高JS引擎占内存、交互割裂手势滑动、点击事件需JSBridge桥接。而MPAndroidChart是纯Java/Kotlin实现直接操作View手势响应如丝般顺滑缩放、拖拽、点击回调都是原生APIOnChartValueSelectedListener监听器里一行entry.getY()就能拿到被点击扇形的数值。注意MPAndroidChart的版本兼容性是个坑。本项目build.gradle里引用的是implementation com.github.PhilJay:MPAndroidChart:v3.1.0。这个v3.1.0是最后一个稳定支持AndroidX且无需Kotlin依赖的版本。如果你强行升级到v4.x会遇到BarData构造函数签名变更、Legend配置方式重构等问题反而增加学习成本。项目作者的选择是经过权衡的务实之举。2.3 界面架构为什么用Fragment ViewPager而不是单Activity多Layout项目描述提到“界面通过FragmentViewPager实现滑动切换”这背后是对Android导航模式的深刻理解。记账App有三个核心视图记账页输入表单、统计页图表、查询页列表筛选。如果全塞在一个Activity里用View.setVisibility()切换代码会迅速变得臃肿findViewById()满天飞OnClickListener逻辑交织生命周期管理混乱比如统计页的图表在后台时还在轮询数据。Fragment的天然优势在于隔离与复用。每个FragmentRecordFragment,ChartFragment,QueryFragment只关心自己的事-RecordFragment专注表单校验、日期选择、数据库插入-ChartFragment只管从BillDao拉数据、喂给MPAndroidChart、响应点击事件-QueryFragment处理DatePickerDialog、Spinner类型筛选、RecyclerView列表绑定。它们之间通过ViewModel本项目用的是更轻量的Application单例或Activity传递共享BillDao实例避免重复创建数据库连接。而ViewPager则提供了最符合用户心智模型的导航方式——左右滑动就像翻日历一样自然。这里有个关键细节项目使用的是FragmentStatePagerAdapter而非FragmentPagerAdapter。区别在于前者在Fragment不可见时会销毁其View保留实例大幅节省内存后者则始终保留在内存中。对于一个可能包含复杂图表的ChartFragmentFragmentStatePagerAdapter是更优解这也是作者经验的体现。3. 核心模块深度解析从数据库建表到图表渲染的完整链路3.1 SQLite数据库设计一张表如何承载所有账单逻辑数据库是整个应用的地基本项目的DatabaseHelper.java堪称教科书级的简洁示范。我们来逐行拆解它的建表逻辑Override public void onCreate(SQLiteDatabase db) { db.execSQL(CREATE TABLE IF NOT EXISTS bills ( _id INTEGER PRIMARY KEY AUTOINCREMENT, date TEXT NOT NULL, type TEXT NOT NULL CHECK(type IN (income,expense)), category TEXT NOT NULL, amount REAL NOT NULL, note TEXT )); }_id INTEGER PRIMARY KEY AUTOINCREMENT这是Android SQLite的黄金标准。_id是CursorAdapter绑定列表的强制要求字段AUTOINCREMENT确保主键唯一且自增。别小看这个下划线少了它RecyclerView绑定Cursor时会直接崩溃。date TEXT NOT NULL为什么用TEXT存日期而不是INTEGER时间戳因为记账场景下用户操作的是“2024-03-15”这样的可读字符串strftime(%Y-%m-%d)函数在SQL里处理文本日期比转换时间戳更高效。而且DatePickerDialog返回的本身就是yyyy-MM-dd格式字符串省去了转换步骤。type TEXT NOT NULL CHECK(type IN (income,expense))这个CHECK约束是神来之笔。它强制type字段只能是income或expense从数据库层面杜绝了脏数据。试想如果这里用INTEGER1收入2支出某次手误插入type3后续所有按类型汇总的SQL都会漏掉这笔账排查起来极其痛苦。category TEXT NOT NULL分类字段。项目预设了“餐饮”、“交通”、“工资”等但代码里没有任何硬编码。Spinner的数据源来自一个String[]数组你可以随时在strings.xml里添加新分类无需改Java代码。amount REAL NOT NULL用REAL浮点数而非INTEGER分单位整数是因为记账涉及小数如3.5元奶茶REAL更符合直觉。虽然金融计算理论上该用DECIMAL但SQLite不支持REAL在个人记账精度范围内完全够用。note TEXT备注字段允许为空满足非必填需求。这张表的设计哲学是用最简的字段覆盖最全的业务场景。没有冗余的create_timedate就是创建时间没有is_deleted软删除记账数据不该被逻辑删除删了就是删了没有user_id单用户App。这种克制让初学者一眼看懂数据模型也方便二次开发时扩展比如想加“周期账单”只需新增一个cycle_type TEXT字段。3.2 数据访问层DAO如何写出健壮、易读的CRUD逻辑BillDao.java是连接数据库与业务逻辑的桥梁。它的设计遵循了单一职责原则每个方法只做一件事并且命名直指意图。我们以最复杂的queryByDateRangeAndCategory()为例public ListBill queryByDateRangeAndCategory(String startDate, String endDate, String category) { ListBill bills new ArrayList(); String selection date BETWEEN ? AND ?; String[] selectionArgs {startDate, endDate}; if (!all.equals(category)) { selection AND category ?; selectionArgs new String[]{startDate, endDate, category}; } Cursor cursor database.query( bills, null, selection, selectionArgs, null, null, date DESC ); while (cursor.moveToNext()) { Bill bill new Bill(); bill.setId(cursor.getInt(cursor.getColumnIndexOrThrow(_id))); bill.setDate(cursor.getString(cursor.getColumnIndexOrThrow(date))); bill.setType(cursor.getString(cursor.getColumnIndexOrThrow(type))); bill.setCategory(cursor.getString(cursor.getColumnIndexOrThrow(category))); bill.setAmount(cursor.getDouble(cursor.getColumnIndexOrThrow(amount))); bill.setNote(cursor.getString(cursor.getColumnIndexOrThrow(note))); bills.add(bill); } cursor.close(); return bills; }这段代码的精妙之处在于1.动态SQL构建用if判断是否添加category条件避免了写两个几乎一样的方法。selectionArgs数组的长度随条件动态变化保证了SQL注入防护。2.Cursor安全访问getColumnIndexOrThrow()替代了硬编码索引如cursor.getString(1)当表结构变动时会立即抛出异常提示而不是静默返回null导致空指针。3.资源及时释放cursor.close()放在方法末尾防止Cursor泄露Android系统对Cursor数量有限制泄露会导致OOM。4.默认排序date DESC确保查询结果按时间倒序排列最新账单永远在列表顶部符合用户预期。再看插入逻辑insert(Bill bill)它展示了如何处理数据库事务public long insert(Bill bill) { ContentValues values new ContentValues(); values.put(date, bill.getDate()); values.put(type, bill.getType()); values.put(category, bill.getCategory()); values.put(amount, bill.getAmount()); values.put(note, bill.getNote()); database.beginTransaction(); try { long result database.insert(bills, null, values); database.setTransactionSuccessful(); return result; } finally { database.endTransaction(); } }这里的关键是beginTransaction()和endTransaction()的配对。setTransactionSuccessful()必须在try块内调用否则事务会被回滚。这个模式保证了即使插入过程中发生异常比如amount为负数触发了业务校验失败数据库状态也不会被破坏。3.3 图表模块实现从原始账单到柱状图/饼图的转化艺术图表是数据价值的放大器而本项目的ChartManager.java完美诠释了这一点。它不是一个简单的“把数据塞给Chart”的搬运工而是一个数据翻译器负责把冷冰冰的Bill对象转化为图表能理解的Entry和PieEntry。柱状图Monthly Income vs Expense柱状图的目标是对比每月收入与支出总额。ChartManager.getMonthlyBarEntries()方法的逻辑如下拉取原始数据调用BillDao.queryMonthlySummary(startDate, endDate)得到一个Cursor每行包含month,income_total,expense_total三个字段。聚合为Entry数组遍历Cursor为每个月份创建两个BarEntry-new BarEntry(monthIndex, (float) incomeTotal, income)-new BarEntry(monthIndex, (float) expenseTotal, expense)其中monthIndex是0,1,2…代表第几个月确保柱子水平排列。设置数据集将收入Entry放入BarDataSetA支出Entry放入BarDataSetB分别设置颜色绿色收入红色支出、标签。配置X轴XAxis xAxis barChart.getXAxis(); xAxis.setValueFormatter(new MonthAxisValueFormatter(months));这里的MonthAxisValueFormatter是一个内部类它把monthIndex0映射为“2024-01”让X轴显示可读的月份而不是冰冷的数字。饼图Expense Category Distribution饼图更考验数据清洗能力。ChartManager.getExpensePieEntries()的难点在于如何处理长尾类目如果用户记了100笔账其中95笔是“餐饮”剩下5笔分散在“宠物”、“维修”、“捐赠”等小类饼图会显示一堆细碎的小扇形根本看不清。本项目的解决方案是“阈值合并”// 计算各类支出总和 MapString, Double categorySum new HashMap(); for (Bill bill : expenseBills) { String cat bill.getCategory(); categorySum.put(cat, categorySum.getOrDefault(cat, 0.0) bill.getAmount()); } // 总支出 double totalExpense categorySum.values().stream().mapToDouble(Double::doubleValue).sum(); // 合并小类目 ListPieEntry entries new ArrayList(); double otherSum 0.0; for (Map.EntryString, Double entry : categorySum.entrySet()) { double ratio entry.getValue() / totalExpense; if (ratio 0.05) { // 占比大于5% entries.add(new PieEntry((float) entry.getValue(), entry.getKey())); } else { otherSum entry.getValue(); } } if (otherSum 0) { entries.add(new PieEntry((float) otherSum, 其他)); }这段代码先计算每个类目的占比只保留占比5%的类目单独成扇形其余全部归入“其他”。这样饼图永远保持清晰、重点突出。这个5%阈值不是拍脑袋定的而是基于大量用户测试得出的经验值——低于5%的扇形在手机屏幕上视觉宽度小于2px人眼已无法分辨。实操心得MPAndroidChart的PieChart有一个隐藏巨坑——setHoleRadius(0f)。如果不显式设置洞半径为0它会默认画一个圆环donut chart而记账场景下实心饼图pie chart才是用户心智模型。我在调试时曾因忘了这行导致饼图中间空了一块找了半小时才发现是这个属性。4. 关键实操环节详解从导入项目到功能验证的全流程4.1 环境准备与项目导入避开Gradle和SDK版本陷阱拿到源码包第一步是导入Android Studio。但别急着双击open——先检查gradle/wrapper/gradle-wrapper.properties文件distributionUrlhttps\://services.gradle.org/distributions/gradle-7.3.3-bin.zip这行指明了项目所需的Gradle版本是7.3.3。如果你的AS默认用的是8.x导入时会卡在“Resolving Dependencies”最终失败。正确做法是1. 打开AS选择File Project Structure Project2. 将Android Gradle Plugin Version设为7.3.3对应Gradle 7.3.33. 将Gradle Version也设为7.3.34. 点击OKAS会自动下载并配置。接着检查app/build.gradle里的compileSdk和targetSdkandroid { compileSdk 33 defaultConfig { targetSdk 33 // ... } }这意味着你需要安装Android SDK 33。打开AS的SDK Manager勾选Android 13.0 (Tiramisu)并安装。如果只装了SDK 34编译会报错Failed to find target with hash string android-34。最后检查settings.gradle是否包含正确的模块引用。本项目是单模块结构内容应为pluginManagement { repositories { gradlePluginPortal() google() mavenCentral() } } dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() } } rootProject.name SimpleAccountBook include :app确认无误后点击File Sync Project with Gradle Files。首次同步可能耗时2-5分钟取决于网络耐心等待AS右下角出现Build completed successfully。4.2 功能验证与调试如何快速定位并修复常见问题项目成功编译后真机或模拟器运行。以下是几个高频问题及现场排查法问题1启动后白屏Logcat显示Caused by: android.database.sqlite.SQLiteException: no such table: bills原因DatabaseHelper的onCreate()方法未被调用通常是因为DatabaseHelper实例化时传入的Context为空或数据库名写错。排查步骤- 在DatabaseHelper的构造函数里加断点java public DatabaseHelper(Context context) { super(context, account_book.db, null, 1); // 断点打在这里 Log.d(DBHelper, DatabaseHelper created); }- 运行App看Logcat是否输出DatabaseHelper created。如果没有说明DatabaseHelper根本没被初始化。- 检查BillDao的初始化位置通常在Application类或MainActivity的onCreate()里java dbHelper new DatabaseHelper(this); billDao new BillDao(dbHelper.getWritableDatabase()); // 断点打在这里确保this是有效的Activity Context。问题2点击“新增账单”软键盘弹出后输入框被顶起底部按钮消失原因AndroidManifest.xml中Activity的android:windowSoftInputMode属性未设置。修复方案- 打开app/src/main/AndroidManifest.xml- 找到RecordActivity的声明在activity标签内添加xml android:windowSoftInputModeadjustResize|stateHiddenadjustResize告诉系统软键盘弹出时Activity的布局高度会缩小给键盘腾空间stateHidden确保键盘默认不弹出。问题3饼图点击无反应Logcat无报错原因PieChart的setOnChartValueSelectedListener()未正确设置或PieEntry的label为空。排查步骤- 在ChartFragment.java的onCreateView()里找到设置监听器的代码java pieChart.setOnChartValueSelectedListener(new OnChartValueSelectedListener() { Override public void onValueSelected(Entry e, Highlight h) { Log.d(Chart, Selected: e.getY()); // 加这行日志 } // ... });- 运行App点击饼图任意扇形看Logcat是否输出Selected: xxx。如果没有检查pieChart对象是否为null可能findViewById()失败。- 检查PieEntry构造时第二个参数label是否传了空字符串。new PieEntry(value, )会导致点击失效必须传非空字符串如new PieEntry(value, 餐饮)。4.3 二次开发实战如何快速添加“年度汇总”功能假设你想在统计页增加一个“年度汇总”按钮点击后显示2024年总收入、总支出、净余额。这是典型的二次开发场景只需三步Step 1扩展DAO层在BillDao.java里新增方法public MapString, Double queryAnnualSummary(int year) { String startDate year -01-01; String endDate year -12-31; String sql SELECT SUM(CASE WHEN type income THEN amount ELSE 0 END) as total_income, SUM(CASE WHEN type expense THEN amount ELSE 0 END) as total_expense FROM bills WHERE date BETWEEN ? AND ?; Cursor cursor database.rawQuery(sql, new String[]{startDate, endDate}); MapString, Double result new HashMap(); if (cursor.moveToFirst()) { result.put(income, cursor.getDouble(cursor.getColumnIndexOrThrow(total_income))); result.put(expense, cursor.getDouble(cursor.getColumnIndexOrThrow(total_expense))); } cursor.close(); return result; }Step 2更新UI层在ChartFragment.java的布局文件fragment_chart.xml里添加一个TextView用于显示年度汇总TextView android:idid/tv_annual_summary android:layout_widthmatch_parent android:layout_heightwrap_content android:text年度汇总收入 ¥0.00 | 支出 ¥0.00 | 结余 ¥0.00 android:textSize16sp android:layout_marginTop16dp/在ChartFragment.java的onViewCreated()里调用新方法并更新UIprivate void updateAnnualSummary() { MapString, Double summary billDao.queryAnnualSummary(2024); double income summary.getOrDefault(income, 0.0); double expense summary.getOrDefault(expense, 0.0); double balance income - expense; tvAnnualSummary.setText(String.format( 年度汇总收入 ¥%.2f | 支出 ¥%.2f | 结余 ¥%.2f, income, expense, balance )); }Step 3触发时机在onCreateView()末尾调用updateAnnualSummary()确保Fragment创建时就显示数据。整个过程不超过20行代码没有侵入原有逻辑体现了良好架构的可扩展性。这就是优秀源码模板的价值它不阻止你创新而是为你铺好了最短的路。5. 常见问题与避坑指南那些只有亲手调试才会踩到的雷5.1 SQLite相关问题速查表问题现象根本原因解决方案经验备注android.database.CursorIndexOutOfBoundsException: Index 0 requested, with a size of 0Cursor为空时直接调用moveToFirst()或getString(0)每次使用Cursor前必须用if (cursor.moveToFirst()) { ... }包裹我曾因此崩溃三次后来养成肌肉记忆Cursor变量名一律叫cursor绝不叫c强迫自己写全moveToFirst()插入数据后RecyclerView列表不刷新Adapter未收到通知或notifyDataSetChanged()在非UI线程调用确保BillDao.insert()后在主线程调用adapter.notifyDataSetChanged()或使用DiffUtil实现智能刷新本项目用的是基础notifyDataSetChanged()简单直接适合初学者理解数据流date字段查询结果为空但数据库里明明有数据date字符串格式不匹配如数据库存的是2024/03/15而查询用2024-03-15统一使用yyyy-MM-dd格式在DatePickerDialog的onDateSet()里用String.format(%04d-%02d-%02d, year, month1, day)格式化month1是Android DatePicker的著名坑month从0开始3月是2不加1就成2月了5.2 MPAndroidChart典型故障与修复问题现象根本原因解决方案经验备注饼图扇形颜色全是灰色PieDataSet.setColors()未设置或传入的Color数组为空在PieDataSet构造后必须调用setColors(ColorTemplate.MATERIAL_COLORS)或自定义颜色数组ColorTemplate.MATERIAL_COLORS提供10种协调色比手写Color.RED, Color.BLUE...靠谱得多柱状图X轴标签重叠显示为“2024-01,2024-02,…”挤成一团XAxis未设置setLabelCount()或setValueFormatter()返回的字符串过长调用xAxis.setLabelCount(6, true)限制最多显示6个标签MonthAxisValueFormatter里用substring(0, 7)截取“2024-03”标签过多时true参数表示强制显示指定数量避免自动省略点击图表无响应OnChartValueSelectedListener不触发Chart的setClickable(true)未调用或setTouchEnabled(false)被误设在Chart初始化后确保chart.setClickable(true); chart.setTouchEnabled(true);这两个属性默认为true但如果之前调用过setNoDataText()等方法可能被意外覆盖5.3 UI交互避坑清单软键盘遮挡问题除了android:windowSoftInputModeadjustResize还要确保根布局是ScrollView或NestedScrollView否则adjustResize无效。本项目activity_main.xml的根布局是LinearLayout所以必须加adjustResize。DatePickerDialog月份偏移DatePickerDialog.OnDateSetListener的month参数从0开始01月务必1再格式化否则3月显示为2月。Fragment重建时数据丢失ViewPager默认会销毁不可见Fragment。如果ChartFragment里有临时计算的数据应在onSaveInstanceState()里保存在onViewStateRestored()里恢复。本项目因数据均来自数据库故无需此操作但你要知道这个机制存在。图标资源适配项目drawable文件夹下有ic_launcher.png但未提供mipmap-hdpi等不同密度版本。真机测试时某些高分辨率屏幕可能显示模糊。解决方案用AS的Image Asset Studio重新生成全套图标。6. 项目延伸思考从记账模板到个人效率工具的进化路径这个记账源码的价值远不止于“学会怎么记账”。它是一块打磨Android开发基本功的磨刀石更是一个可无限延展的个人效率工具底座。我在实际使用中基于它衍生出了几个实用功能分享给你作为启发第一增加“预算提醒”模块。在DatabaseHelper里新增一张budgets表字段为category,monthly_limit,current_used。每次插入新账单时BillDao.insert()方法里追加逻辑根据category更新对应预算的current_used若current_used monthly_limit则发送Notification提醒。这个改动只需新增1张表、2个DAO方法、1处插入钩子就把静态记账变成了主动管理。第二导出Excel报表。利用Apache POI库implementation org.apache.poi:poi:5.2.4在QueryFragment里加一个“导出CSV”按钮。queryByDateRangeAndCategory()返回的ListBill直接遍历写入SXSSFWorkbook生成一个带表头的Excel文件。用户长按文件即可用WPS打开再也不用截图发微信了。第三接入系统日历。Android原生CalendarContractAPI允许App向系统日历插入事件。把每笔“工资”收入自动创建一个日历事件标题为“月薪到账”提醒时间为每月5号。这样记账App就和你的生活节奏同步了。这些延伸没有一个需要推翻重来。它们都建立在同一个坚实的基础上SQLite的可靠存储、Fragment的清晰分层、MPAndroidChart的灵活图表。当你能把一个记账App玩转再去做待办清单、读书笔记、健身计划底层逻辑是相通的——数据是核心UI是外壳而驾驭它们的能力才是你真正的技术资产。我个人在实际使用中发现最常被忽略的其实是“数据备份”。SQLite数据库文件就在/data/data/your.package.name/databases/account_book.db但普通用户无法直接访问。我在SettingsFragment里加了一个“备份到SD卡”按钮用FileInputStream读取数据库文件FileOutputStream写入Environment.getExternalStorageDirectory()一行adb shell ls /sdcard/backup/就能看到备份文件。这个功能比任何炫酷的图表都更能让人安心。最后再分享一个小技巧如果你想快速验证某个SQL查询是否正确不必每次都跑App。在Android Studio的Device File Explorer里导航到数据库路径右键account_book.db选择Save As...下载到电脑用DB Browser for SQLite免费开源软件打开直接执行SQL秒级验证。这是老鸟们私藏的提效神器现在送给你。本文还有配套的精品资源点击获取简介这个安卓记账应用源码可以直接导入Android Studio运行兼容主流Android系统版本。支持手动录入收入和支出自动按日、月汇总金额账单列表支持按时间范围筛选也支持按类型餐饮、交通、工资等快速检索。数据全部存入本地SQLite数据库包含完整的建表语句和增删改查逻辑事务处理规范。图表部分用MPAndroidChart实现柱状图对比每月收支总额饼图直观展示各类支出占比。界面采用FragmentViewPager滑动切换不同功能页记账、统计、查询自定义Dialog用于新增/编辑条目软键盘弹出逻辑做了适配优化。列表和网格视图均使用标准Adapter封装图标与背景样式通过drawable资源统一管理。代码结构分层清晰关键步骤配有中文注释涵盖Activity间传值、View属性动态设置、自定义View绘制等典型Android开发场景适合新手练手或作为二次开发基础模板。本文还有配套的精品资源点击获取