尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

儿童成长APP开发实战:SQLite、状态机与自定义日历View

儿童成长APP开发实战:SQLite、状态机与自定义日历View 简介面向幼教场景的Android儿童成长APP项目源码是一套覆盖任务打卡、成长记录、育儿知识、家庭互动与奖励兑换的完整移动端解决方案适合毕业设计、课程实训及Android开发者进阶参考。资源包共1151个文件压缩包仅15.23MB包含大量Java源码、XML界面布局、图片素材、jar依赖库以及少量JSP与配置文件其中Java与XML是功能改造的核心图片便于替换视觉风格class与jar支撑项目快速运行。已有171人学习下载源码模块划分清晰便于逐块拆解。通过源码可理解任务日历汇总与编辑、每日任务删除与执行、专家文章阅读计时、家庭分享奖励小红花、奖励兑换等业务逻辑同时从包内类文件可看出项目还涉及论坛、新闻、宠物等扩展功能能够帮助读者掌握数据库设计、图片缩放控件、网络请求、列表刷新等通用技术点适合需要快速搭建同类型应用、深入学习项目架构或准备毕业答辩的读者。1. 从“打卡奖励”到“成长档案”儿童成长APP的模块拆解儿童成长类APP常死在“打开一次就流失”上核心原因是任务体系太轻缺少持续正反馈。这个项目把任务日历、每日任务、专家推荐、家庭分享、奖励兑换五个模块串成闭环家长记录任务孩子完成任务领小红花小红花兑换礼物专家内容和家庭分享又拉长使用时长。最值得看的不是界面布局而是它用SQLite存任务流、用状态机管奖励、用自定义View做日历以及如何处理图片缩放、阅读时长统计这些真实业务细节。对有一两年Android基础、想完整走一遍业务型APP开发的人来说这是很好的完整样例对做毕业设计或作品集的开发者这套模块划分也足够撑起一个完整项目。2. 数据层设计任务日历与小红花奖励的SQLite实现2.1 任务表与奖励记录的ER设计任务日历要支持“对过去所有任务进行汇总、编辑修改”意味着不能只存当天待办而是把每次任务作为独立记录带上日期、状态、奖励值。常见做法是设计两张表task表存任务实例flower_record表存小红花流水。CREATE TABLE task ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, task_date TEXT NOT NULL, -- 格式 yyyy-MM-dd status INTEGER DEFAULT 0, -- 0未完成 1已完成 2已过期 reward_flower INTEGER DEFAULT 1, category_id INTEGER, -- 关联任务分类 created_at TEXT DEFAULT (datetime(now,localtime)) ); CREATE TABLE flower_record ( id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER, change_type INTEGER, -- 1获得 -1消费 change_value INTEGER NOT NULL, remain INTEGER NOT NULL, -- 变动后的余额 create_time TEXT DEFAULT (datetime(now,localtime)) );task_date用 TEXT 而不是 DATETIME是因为日历模块按天聚合时TEXT 的等值比较和索引都比带时分秒的 DATETIME 简单。status用整数而不是字符串后续状态机切换时可以直接按数值比较也便于扩展“已过期”这类中间态。reward_flower放到任务表里允许不同任务配置不同的奖励数兑换类任务甚至可以把奖励设为 0。flower_record里的remain字段是冗余设计但避免了每次算余额都 SUM 一遍流水。对本地单机应用来说用空间换时间完全值得。change_type用正负数区分获得和消费后面做统计报表时只需要对change_value做 SUM无需额外判断方向。2.2 日历聚合查询的SQL写法日历模块要“对过去所有任务进行汇总”典型需求是给定一个月份返回每一天完成任务数、获得小红花数。一次性把整月数据拉到内存再循环也可以但数据量大了会有卡顿。更合适的写法是直接按日期分组查询SELECT task_date, COUNT(*) AS task_count, SUM(CASE WHEN status 1 THEN 1 ELSE 0 END) AS done_count, SUM(CASE WHEN status 1 THEN reward_flower ELSE 0 END) AS got_flower FROM task WHERE task_date BETWEEN 2025-04-01 AND 2025-04-30 GROUP BY task_date;这里用 CASE WHEN 而不是两次 WHERE 查询是为了在同一条 SQL 里同时拿到总数和完成数减少一次数据库往返。如果你只需要已完成任务可以追加HAVING done_count 0。Android 的SQLiteDatabase.rawQuery完全支持这种语法。遍历 Cursor 时要注意不要用硬编码列索引先通过cursor.getColumnIndex(task_date)获取索引避免字段顺序变化后崩溃。月度日历展示时UI 层通常需要 42 个格子6 周但数据库只有若干条记录。我一般会先在内存中构造一个当月日期数组把查询结果转成MapString, TaskSummary用task_date做 key然后遍历数组去 map 中取值。这样 UI 渲染和数据库解耦以后改用 Room 或网络数据源时只需要替换数据获取层。2.3 DAO层的封装与事务处理任务编辑和奖励发放必须在一个事务里完成否则用户完成任务后小红花没有变化反馈会非常奇怪。典型场景是完成任务 - 更新任务状态 - 插入小红花流水 - 刷新用户总花数。用beginTransaction包裹public void completeTask(int taskId) { SQLiteDatabase db helper.getWritableDatabase(); db.beginTransaction(); try { Task task queryTaskById(taskId); int newRemain queryTotalFlower() task.rewardFlower; ContentValues taskValues new ContentValues(); taskValues.put(status, 1); db.update(task, taskValues, id?, new String[]{String.valueOf(taskId)}); ContentValues recordValues new ContentValues(); recordValues.put(task_id, taskId); recordValues.put(change_type, 1); recordValues.put(change_value, task.rewardFlower); recordValues.put(remain, newRemain); db.insert(flower_record, null, recordValues); db.setTransactionSuccessful(); } finally { db.endTransaction(); } }注意setTransactionSuccessful必须在endTransaction之前否则事务会回滚。finally里不要嵌套 try-catch 吞异常否则数据库连接状态会混乱。如果是批量完成任务比如日历上勾选多天应该放在同一个事务里减少磁盘 IO 次数。这个 DAO 层如果后续要支持多进程或跨应用访问可以改成 ContentProvider 暴露要支持响应式编程可以换成 Room LiveData但表结构不需要大改因为字段设计已经覆盖了业务主流程。3. 每日任务与奖励兑换状态机驱动的前端交互3.1 任务状态流转与WheelView选数器每日任务模块强调“操作便捷”删除和完成都要一步到位。在实现上任务状态不是一个简单布尔值而是未完成 - 已完成 - 已领取奖励三步。有的孩子会先完成任务但不急着领花所以状态需要能回退。定义状态机public static final int STATUS_TODO 0; public static final int STATUS_DONE 1; public static final int STATUS_REWARDED 2; public static boolean canTransit(int from, int to) { if (from STATUS_TODO to STATUS_DONE) return true; if (from STATUS_TODO to STATUS_REWARDED) return false; // 必须先完成 if (from STATUS_DONE to STATUS_REWARDED) return true; if (from STATUS_DONE to STATUS_TODO) return true; // 允许撤销 return false; }界面上的按钮文案根据状态切换0 显示“去完成”1 显示“领红花”2 显示“已领取”。点击事件里先判断状态再决定调哪个接口。这里有个容易踩的坑快速连点会触发两次状态变更导致事务冲突或流水重复。解决方法是点击后立即把按钮置为不可点击或者用 Handler 做个 300ms 的防抖。对于“去完成”这个动作如果任务类型是户外活动可以跳转到计时页面如果是阅读任务可以跳转到阅读时长统计页。WheelView 是项目里用来选择任务数量或奖励兑换数量的滚轮控件。它本质是一个自定义 View继承 View 后重写onDraw绘制每一行文本再用onTouchEvent处理滚动和惯性。网上开源实现很多但要特别注意高版本 Android 兼容性尤其是setOnItemSelectedListener的触发时机——在 fling 停止后要重新计算选中项否则松手后 listener 不会触发用户看到的选中值和实际结果不一致。3.2 奖励兑换的并发扣减奖励兑换模块虽然只是从余额里扣减但一致性要求很高。儿童 APP 虽然是单机使用但可能存在家长在后台管理、孩子在平板前端同时操作的情况。Android 里同一个进程的多线程写 SQLite 不会冲突因为SQLiteDatabase内部有锁但如果两个线程各自拿了不同连接就可能出现脏读。防止办法是使用单例的SQLiteOpenHelper并保证所有写操作通过同一个数据库实例。兑换流程里最核心的三步是“查询余额 - 判断是否充足 - 扣减”必须打包成事务public boolean exchange(int flowerCost) { SQLiteDatabase db helper.getWritableDatabase(); db.beginTransaction(); try { int totalFlower queryTotalFlower(); if (totalFlower flowerCost) { return false; } int newRemain totalFlower - flowerCost; ContentValues values new ContentValues(); values.put(change_type, -1); values.put(change_value, flowerCost); values.put(remain, newRemain); db.insert(flower_record, null, values); db.setTransactionSuccessful(); return true; } finally { db.endTransaction(); } }这里不能先在事务外把余额读出来判断后再回来更新否则两个并发任务可能同时通过检查。SQLite 的事务是串行化的beginTransaction之后其他写操作会等待所以这个写法是安全的。界面提示要区分“余额不足”和“兑换失败”后者的原因可能是数据库插入异常需要记录日志并给出可重试按钮。3.3 小红花余额的本地持久化与跨页面同步余额变化后首页、任务列表、兑换中心三处都要刷新。如果每个页面各自查询数据库会导致短暂的数据不一致。常见做法是维护一个单例的内存级仓库用 AndroidX 的 LiveData 包装余额数据层在事务成功后更新 LiveDataUI 层自动刷新public class FlowerRepository { private static FlowerRepository instance; private MutableLiveDataInteger totalFlower new MutableLiveData(); public static FlowerRepository getInstance() { if (instance null) { instance new FlowerRepository(); } return instance; } public LiveDataInteger observeTotalFlower() { return totalFlower; } public void refreshFromDb() { int total queryTotalFlower(); totalFlower.postValue(total); } }所有写操作完成后调用refreshFromDb()页面只需要订阅一次。注意postValue是异步安全的如果从子线程调用UI 层会收到回调如果是主线程可以用setValue减少一次线程切换。如果项目还没引入 AndroidX也可以用 Handler Message 广播但消息容易泄漏订阅者要在onDestroy中移除。这个仓库模式同样适用于任务列表刷新任务完成后把当前任务 ID 和状态放进一个共享的MutableLiveDataTaskEvent列表页观察后做局部刷新。4. 家庭分享与专家推荐网络层与图片缩放优化4.1 家庭分享模块的图片上传与压缩分享宝宝成长点滴时照片动辄几 MB如果不压缩直接上传会消耗大量流量。Android 端必须在本地先做采样压缩。最常见的做法是用BitmapFactory.Options的inSampleSize降采样public static Bitmap getCompressedBitmap(String path, int targetWidth, int targetHeight) { BitmapFactory.Options options new BitmapFactory.Options(); options.inJustDecodeBounds true; BitmapFactory.decodeFile(path, options); int sampleSize 1; while (options.outWidth / sampleSize targetWidth || options.outHeight / sampleSize targetHeight) { sampleSize * 2; } options.inSampleSize sampleSize; options.inJustDecodeBounds false; return BitmapFactory.decodeFile(path, options); }先用inJustDecodeBounds读取图片宽高而不加载像素再根据目标尺寸计算sampleSize。sampleSize必须是 2 的幂否则系统会向下取整导致压缩后仍然超出目标大小。目标宽高一般取屏幕宽度的两倍比如手机屏宽 1080px目标宽设为 1920保证清晰度和体积平衡。压缩后的 Bitmap 上传时还要考虑格式。在 Android 9 以下 WebP 支持不完整所以统一压缩成 JPEG 并设置quality为 80 更稳妥。注意Bitmap.compress返回的是 boolean某些设备上可能失败最好把压缩结果写到缓存文件再通过文件流上传。如果分享内容还有视频那就要走另一条分片上传的流程这里不展开。4.2 PhotoViewAttacher查看大图的缩放细节分享模块里点击图片查看大图项目用到了 PhotoViewAttacher 这个库。它的原理是在 ImageView 外层包装一个可缩放的 ViewGroup核心是GestureDetector处理双击和单指拖动ScaleGestureDetector处理双指缩放。源码里值得学习的是它如何维护Matrix状态以及缩放超出边界后如何回弹。实际使用中有一个容易踩的坑PhotoViewAttacher 必须在 ImageView 绑定好图片之后再 attach。如果先 attach 再setImageResource缩放矩阵会基于错误的尺寸计算表现为图片错位。正确顺序是imageView.setImageBitmap(bitmap); PhotoViewAttacher attacher new PhotoViewAttacher(imageView); attacher.setScaleType(ImageView.ScaleType.FIT_CENTER); attacher.setOnPhotoTapListener((view, x, y) - finish());在 Android Studio 里接入这个库时如果项目是离线环境可以直接把源码里的PhotoViewAttacher.java和Util.java拷进工程但要注意依赖版本统一。另外如果列表复用了 ImageView一定要在RecyclerView的onViewRecycled中调用attacher.cleanup()否则会内存泄漏。这个库的实现思路也可以迁移到签名图片、头像裁剪等场景核心就是 Matrix 的 setScale/postTranslate 组合。4.3 专家推荐阅读时长统计与防作弊专家推荐模块要“统计阅读时长按时长奖励小红花”。这个统计不能只在页面onDestroy时算一个开始结束时间因为用户可能中途切到后台或者页面被系统回收。一个稳妥做法是记录生命周期事件onResume时记录startTimeonPause时计算当前段时长累加到总时长并写入数据库如果页面重新可见再记录新的startTime同时要防止用户把手机放那儿不动靠屏幕常亮也能混时长。很多 APP 会加“是否处于前台且内容可见”的判定但最简单的是加一个交互时间戳每次用户点击或滑动时更新lastInteractTime在onPause统计时如果最后交互时间距离当前时间超过 5 分钟就只统计到最后一次交互Override protected void onPause() { super.onPause(); long now System.currentTimeMillis(); if (lastInteractTime 5 * 60 * 1000 now) { long readTime now - startTime; addReadDuration(readTime); } startTime 0; }注意startTime为 0 时说明会话已经结算过不要再重复累加。阅读时长累计到一定阈值就发放小红花阈值可以做成 SharedPreferences 配置比如“每满 10 分钟奖励 1 朵”。把配置和逻辑分离后面调整奖励规则不用改代码。如果要做更准确的埋点还可以上报阅读文章 ID、滑动深度和停留时长用于后续推荐算法的调整。5. 自定义日历 View 与性能调优的落地技巧任务日历控件如果只用 GridView 或 RecyclerView 嵌套月切换时容易闪烁。这个项目多半是直接继承 View 自己绘制核心难点有两个计算每个日期所在的单元格位置以及处理不同月份的天数和第一周的偏移。常见做法是先算出当月第一天是周几再根据偏移量决定星期头后面的占位。绘制时用Canvas.drawText画日期数字用drawRoundRect画背景标记比如完成任务画一个绿色圆角矩形。注意绘制时必须启用抗锯齿否则文字边缘发虚Paint paint new Paint(Paint.ANTI_ALIAS_FLAG); paint.setTextSize(dp2px(14)); paint.setTextAlign(Paint.Align.CENTER);每格宽度在onSizeChanged里用宽度除以 7 得到不要在onDraw里重复计算。星期栏固定 40dp 高度剩余空间平均分给 6 行。性能上onDraw里不要创建新的 Paint、Rect 或 Path所有绘制资源在初始化时准备好。月切换时只需要调用invalidate()系统会自动重绘。如果要显示连续多个月的任务点可以在 onDraw 之前把数据整理成SetString判断当前日期包含在集合中就画圆点这个 Set 尽量在数据层就构建好不要在绘制时遍历数据库。除了绘制性能还要关注内存。小红花图标如果以 Bitmap 形式存在在onDraw里每次drawBitmap会频繁调用 native 层操作。更好的做法是把图标缩放到具体 px 后缓存为全局变量或者直接用 Canvas.drawCircle 画一个花形避免加载图片资源。另外不要在onDraw里做日期格式化“yyyy-MM-dd” 转成“今天”这类逻辑放到数据层UI 层只负责按位置绘制。最后分享一个验证任务闭环的小技巧用 Android Studio 的 Device Explorer 打开应用的数据库目录通过adb shell run-as 包名 cat databases/app.db查看实际落库数据。检查点包括任务状态是否为 1flower_record是否多了一条正向流水余额是否等于历史累加。如果用脚本自动化可以先用adb shell input tap模拟点击任务完成按钮再查询数据库变化确保 UI 和事务层真的对得上。这样跑通一遍儿童成长 APP 的核心循环就完全闭环了。后续想扩展成长档案、勋章体系也只需要在flower_record基础上加事件类型不需要改表结构这是整个项目设计里最划算的决策。本文还有配套的精品资源点击获取
返回列表