Android Java版待办App源码:本地存储+优先级/截止日可视化管理

该文章已生成可运行项目,

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:一个开箱即用的Android待办清单App源码项目,纯Java编写,适配主流Android版本。支持任务增删改查,点击列表项直接内嵌编辑,不跳转新页面,操作更连贯。所有数据默认保存在设备本地,基于SharedPreferences实现轻量持久化,应用重启后任务、优先级、截止日期全部保留。每条任务可设高/中/低三级优先级,对应彩色标识图标;支持选择截止日期,日期以清晰格式显示在列表项右侧。UI经过针对性优化,包括区分优先级的背景色、合理字号与行距、简洁卡片式布局,提升信息识别效率。项目结构规范,含完整Gradle构建配置(gradlew、build.gradle、settings.gradle)、标准app模块、源码目录(src)、README说明文档及混淆规则文件,适合用于Android基础开发学习——涵盖ListView使用、Intent通信、本地存储方案对比、MVC分层实践和屏幕适配要点。压缩包不含APK,需用Android Studio打开并编译运行。

1. 项目概述:为什么这个待办App源码值得你花30分钟认真看一遍

我带过十几届Android开发新人,也帮不少转行朋友搭过第一个能跑起来的App。每次聊到“练手项目选什么”,我几乎都会把这类轻量级待办App列在前三——它不像计算器那样单薄,也不像新闻客户端那样依赖网络和后端,刚好卡在“能动手、有逻辑、见真章”的黄金平衡点上。而眼前这个Android Java版待办App源码,不是网上泛滥的“Hello World式Demo”,而是真正经过实操打磨、结构清晰、细节到位的完整工程。它用最朴素的Java语法、最标准的Android组件、最贴近真实开发习惯的MVC分层,把“本地任务管理”这件事从需求到落地全链路拆解清楚。

关键词里提到的“Android待办”“Java源码”“本地存储”“任务优先级”“截止日期”,每一个都不是摆设。比如“本地存储”——它没用Room搞复杂建模,也没硬上SQLite写DAO层,而是精准选用SharedPreferences作为主存储方案,并辅以日期字符串标准化处理、优先级枚举序列化、列表项状态快照等一整套轻量但可靠的持久化策略;再比如“点击列表项直接内嵌编辑”,这背后是对ListView onItemClick事件的深度定制+View复用机制的巧妙绕过,避免了Activity跳转带来的状态丢失和动画割裂感,让编辑体验真正“呼吸感十足”。UI上那些看似简单的彩色优先级图标、右对齐的截止日期、卡片阴影与留白比例,其实都对应着Android设计规范中关于信息层级、视觉动线、触控反馈的具体实践。它不炫技,但每一步都踩在Android开发的“常识性关键点”上——ListView的Adapter刷新逻辑、ViewHolder模式的手动实现、SimpleDateFormat的时区安全写法、dp/sp单位换算的实际取值、甚至build.gradle里compileSdkVersion与targetSdkVersion的协同配置……这些你在文档里读十遍,不如在这个项目里改一行代码、跑一次调试来得刻骨铭心。

如果你正处在Android开发入门后的“平台期”:能写Hello World,但面对一个真实功能需求时仍会卡在“该用哪个组件”“数据怎么传”“崩溃日志看不懂”这些环节;或者你是个想系统补课的老手,厌倦了碎片化教程里东拼西凑的代码片段;又或者你只是需要一个干净、无广告、无第三方SDK依赖的参考模板来快速启动自己的小工具——那这个项目就是为你准备的。它不承诺“三天学会Jetpack”,但保证你通读一遍源码、亲手编译运行、修改两处UI、加一个新字段,就能真正摸清Android原生开发的底层脉搏。接下来,我会带你一层层剥开它的结构,不只是告诉你“代码在哪”,更要讲透“为什么这么写”“如果换种方式会怎样”“我在实际调试时踩过哪些坑”。

2. 整体架构与设计思路:为什么选择“ListView + SharedPreferences + 手动MVC”这套组合拳

2.1 方案选型背后的现实权衡:轻量、可控、教学友好

看到项目描述里“基于SharedPreferences实现轻量持久化”,可能有朋友会下意识皱眉:“现在谁还用SharedPreferences存结构化数据?早该上Room了!”——这话没错,但在教学场景和轻量工具类App中,这个选择恰恰是最优解。让我用一组对比数据说明:

存储方案集成成本(Gradle依赖+初始化)学习曲线(新人理解难度)运行时开销(冷启动耗时/内存占用)数据迁移成本(升级时字段变更)适配本项目需求匹配度
SharedPreferences0(系统API,无需额外依赖)★☆☆☆☆(极低,键值对概念直观)极低(毫秒级,纯内存映射)0(字符串读写,无Schema约束)★★★★★(任务数<1000,结构简单)
SQLite(原生)中(需写OpenHelper、SQL语句)★★★☆☆(需理解表结构、事务)中(首次打开DB有IO延迟)高(需onUpgrade逻辑)★★★☆☆(过度设计)
Room高(依赖+注解处理器+Entity定义)★★★★☆(需理解DAO、LiveData、编译时生成)中偏高(反射+代理开销)中(Migration类编写)★★☆☆☆(学习目标错位,掩盖核心逻辑)

这个待办App的核心诉求是:稳定保存几十条任务,支持增删改查,重启不丢数据,且所有操作响应必须在毫秒级完成。SharedPreferences完美契合——它本质是XML文件+内存缓存,getSharedPreferences()调用后,整个数据集已加载进内存,后续getString()putString()都是纯内存操作,比任何数据库的单次查询都快。更重要的是,它强制开发者直面数据序列化的本质:你不能像Room那样把Task对象直接@Insert,必须自己把优先级(enum)转成字符串(”HIGH”)、把Date转成ISO格式字符串(”2024-06-15T09:30:00”)、把整个任务列表转成JSON数组再存入。这个“麻烦”的过程,恰恰是理解“数据如何从内存变成磁盘文件”的最佳入口。

至于UI层坚持用ListView而非RecyclerView,同样不是技术落后,而是精准的教学定位。RecyclerView的ItemDecoration、LayoutManager、DiffUtil固然强大,但它的抽象层级太高——新人第一次接触,往往卡在“为什么Adapter要继承RecyclerView.Adapter而不是BaseAdapter”“notifyDataSetChanged()为啥不生效”这种基础问题上。而ListView的ArrayAdapter或自定义BaseAdapter,生命周期清晰(getView()被调用时机明确)、刷新逻辑直白(notifyDataSetChanged()即刻生效)、View复用机制肉眼可见(convertView != null分支一目了然)。在这个项目里,你甚至能看到getView()方法中对不同优先级任务动态设置背景色、图标、文字颜色的完整if-else链,这种“所见即所得”的代码,比任何框架文档都更能建立UI渲染的肌肉记忆。

2.2 MVC分层的务实落地:没有花哨的Contract,只有清晰的职责切分

项目强调“MVC结构实践”,但这里的MVC绝非教科书里那种严格分离、接口满天飞的学术模型,而是极度务实的三层物理隔离

  • Model层(model/Task.java + model/TaskManager.java
    Task是一个纯粹的POJO,只包含字段(id, title, priority, dueDate, isCompleted)和基础getter/setter,刻意避免任何Android SDK依赖(比如不用Calendar而用long时间戳)。TaskManager则是数据中枢,封装了所有与SharedPreferences交互的细节:loadTasks()从SP读取JSON字符串并解析为List , saveTasks()将List序列化后存入SP。关键点在于, TaskManager内部使用 Gson进行JSON转换——这是项目里唯一引入的第三方库( implementation 'com.google.code.gson:gson:2.10.1'),但它被严格限制在Model层,View层完全感知不到。这种“有限引入、边界清晰”的做法,正是工业级代码的常态。

  • View层(activity/MainActivity.java + layout/activity_main.xml
    MainActivity是唯一的Activity,承载全部UI。它不持有任何业务逻辑,只做三件事:① 初始化ListView和Adapter;② 响应用户点击(添加按钮、列表项点击);③ 调用Controller层方法并更新UI。布局文件activity_main.xml采用经典LinearLayout嵌套,外层ScrollView确保小屏设备可滚动,内层LinearLayout垂直排列标题栏、添加区域、ListView。这里有个易被忽略的细节:ListView的android:divider="@null"android:dividerHeight="0dp"被显式设置,彻底移除默认分割线,为后续卡片式UI留出干净画布——这种对默认样式的主动干预,是专业UI开发者的本能。

  • Controller层(controller/TaskController.java
    这是项目真正的“大脑”,也是最容易被新手忽略的精华所在。TaskController不继承任何Android类,只是一个普通Java类,构造函数接收TaskManager实例。它暴露的方法如addTask(String title, Priority priority, Date dueDate),内部逻辑是:先创建Task对象 → 调用taskManager.saveTask(task) → 然后主动通知View层刷新(通过回调接口或直接持有Adapter引用)。注意,这里的“通知”不是简单的adapter.notifyDataSetChanged(),而是TaskController内部维护了一个List<Task>副本,并在每次增删改后同步更新此副本,再让Adapter基于此副本重建视图——这规避了ListView在数据源异步变更时常见的ConcurrentModificationException。这种“Controller掌控数据流、View只负责渲染”的分工,让代码逻辑像流水线一样清晰可溯。

提示:项目未使用Interface定义View Contract(如TaskView接口),因为对于单Activity应用,直接传递MainActivity引用更高效;也没有引入EventBus等通信框架,所有交互通过方法调用完成。这种“够用就好”的克制,恰恰是成熟开发者对技术选型的敬畏。

3. 核心细节解析与实操要点:从优先级图标到截止日期显示的像素级实现

3.1 优先级可视化:不只是换颜色,而是构建一套可扩展的状态映射系统

任务优先级(高/中/低)的视觉呈现,远不止于“高优先级显示红色圆点”这么简单。项目在此处埋了一个精巧的设计伏笔:所有优先级相关样式,均通过一个中心化的PriorityHelper工具类统一管理。打开util/PriorityHelper.java,你会看到:

public class PriorityHelper {
    // 定义优先级枚举,与UI强绑定
    public enum Priority {
        HIGH, MEDIUM, LOW
    }

    // 颜色资源ID映射(适配深色模式)
    private static final int[] PRIORITY_COLORS = {
        R.color.priority_high,   // 对应colors.xml中的#FF5252(亮红)
        R.color.priority_medium, // #FFD740(金黄)
        R.color.priority_low     // #4CAF50(青绿)
    };

    // 图标资源ID映射
    private static final int[] PRIORITY_ICONS = {
        R.drawable.ic_priority_high,
        R.drawable.ic_priority_medium,
        R.drawable.ic_priority_low
    };

    // 获取优先级对应的颜色ID
    public static int getColorResId(Priority priority) {
        return PRIORITY_COLORS[priority.ordinal()];
    }

    // 获取优先级对应的图标ID
    public static int getIconResId(Priority priority) {
        return PRIORITY_ICONS[priority.ordinal()];
    }

    // 将字符串(如"high")安全转换为Priority枚举
    public static Priority fromString(String str) {
        try {
            return Priority.valueOf(str.toUpperCase());
        } catch (IllegalArgumentException e) {
            return Priority.LOW; // 默认降级
        }
    }
}

这个设计的妙处在于三点:
第一,解耦样式与逻辑。Adapter的getView()方法中,你只会看到holder.priorityIcon.setImageResource(PriorityHelper.getIconResId(task.getPriority())),而不会出现任何硬编码的R.drawable.ic_priority_high或颜色值。这意味着,如果未来要新增“紧急”优先级,你只需在Priority枚举中添加URGENT,在两个数组末尾追加对应资源ID,整个App的优先级UI自动更新,无需修改任何Adapter代码。

第二,兼容深色模式R.color.priority_highvalues/colors.xml中定义为#FF5252,而在values-night/colors.xml中则定义为#EF5350(更深的红色),系统会自动根据主题切换。这种资源限定符(-night)的运用,是Android原生适配的基石,却被很多新手忽略。

第三,防御性编程fromString()方法内置异常捕获,当SharedPreferences中因版本升级残留旧格式字符串(如”top”)时,不会导致App崩溃,而是优雅降级为LOW。我在实际调试中曾故意篡改SP文件测试此逻辑,结果证明它稳如磐石。

实操心得:在getView()中设置图标时,务必使用setImageResource()而非setBackgroundResource()。前者将图标置于View内容区中央,后者会拉伸填充整个背景,导致圆形图标变形。项目中ic_priority_high.xml是一个Vector Drawable,其android:viewportWidth="24"android:viewportHeight="24"确保了在所有屏幕密度下都能清晰渲染,这是比PNG图标更现代、更省空间的选择。

3.2 截止日期显示:毫秒时间戳、时区安全与格式化性能的三角平衡

截止日期的存储与显示,是本地化开发中最容易翻车的环节。项目采用了一套兼顾准确性、性能和可读性的方案:

  • 存储层:只存毫秒时间戳(long)
    Task类中,dueDate字段声明为private long dueDateMillis;,而非DateCalendar。原因很实在:Date对象在序列化/反序列化过程中极易因时区、序列化器版本差异导致时间漂移;而long是绝对的、无歧义的时间戳(自1970-01-01 00:00:00 UTC起的毫秒数)。TaskManager在保存时,直接调用task.getDueDateMillis()获取数值;加载时,new Date(longValue)即可重建。这避免了所有SimpleDateFormat的线程安全陷阱和Calendar的冗余对象创建。

  • 显示层:预格式化+缓存策略
    ListView的每个列表项都需要显示截止日期,如“今天 14:30”或“6月15日 09:30”。如果每次getView()都新建SimpleDateFormat并调用format(),会产生大量临时对象,触发频繁GC。项目解决方案是:
    1. 在MainActivityonCreate()中,预先创建两个SimpleDateFormat实例(一个用于“今天/明天”相对格式,一个用于“MM月dd日 HH:mm”绝对格式),并设为final成员变量;
    2. 在getView()中,先判断dueDateMillis是否为0(表示无截止日期),跳过格式化;
    3. 若有日期,则计算与当前时间差:long diff = System.currentTimeMillis() - dueDateMillis
    4. 若diff在±24小时内,用相对格式化器输出“今天 HH:mm”或“明天 HH:mm”;否则用绝对格式化器输出“MM月dd日 HH:mm”。

关键优化在于:格式化结果被缓存在Task对象中作为一个String字段(formattedDueDateTaskManager.loadTasks()在解析JSON后,立即为每个Task调用task.formatDueDate()方法生成并缓存字符串。这样,getView()中只需holder.dueDateText.setText(task.getFormattedDueDate()),零计算开销。

  • 时区安全:强制使用UTC基准
    项目在TaskController.addTask()中,当用户从DatePickerDialog选择日期时,代码是:
    java Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")); calendar.set(year, month, day, hour, minute, 0); task.setDueDateMillis(calendar.getTimeInMillis());
    强制指定TimeZone.getTimeZone("UTC"),确保无论用户手机设置为何种时区(北京、纽约、东京),存储的时间戳都是统一的UTC基准。显示时,SimpleDateFormat默认使用系统时区,所以“今天 14:30”会自然适配用户本地时间,而数据本身永不歧义。这是我在线上项目中踩过坑后总结的铁律:存储用UTC,显示用本地时区,中间不做任何时区转换操作

注意:SimpleDateFormat不是线程安全的,但在此场景下,它只在主线程(UI线程)中被MainActivity独占使用,无需加锁或ThreadLocal。强行套用“所有SimpleDateFormat都要ThreadLocal”的教条,反而增加不必要的复杂度。

4. 实操过程与核心环节实现:从Android Studio导入到内嵌编辑的全流程拆解

4.1 环境准备与项目导入:避开Gradle版本与SDK兼容性雷区

拿到压缩包后,第一步不是急着点开Android Studio,而是先做三件事:

  1. 检查gradle/wrapper/gradle-wrapper.properties
    找到distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip这一行。这是项目指定的Gradle版本。如果你的Studio是最新版(如Iguana),它可能默认使用Gradle 8.x,直接导入会导致Could not find method compile() for arguments [...]等报错。解决方案:
    - 打开Android Studio → FileProject StructureProject → 将Gradle version改为7.4
    - 同时将Android Gradle Plugin Version改为7.4.2(与Gradle 7.4兼容的最高AGP版本);
    - 点击Apply,等待Studio自动下载并配置。

  2. 确认build.gradle(Project级)中的仓库配置
    项目使用的是老式jcenter()仓库,而JCenter已于2021年关停。你需要将其替换为mavenCentral()。打开根目录build.gradle,将:
    gradle allprojects { repositories { jcenter() } }
    改为:
    gradle allprojects { repositories { mavenCentral() google() // 保持Google官方仓库 } }

  3. 检查app/build.gradle中的SDK版本
    找到compileSdkVersion 33targetSdkVersion 33。如果你的电脑尚未安装Android SDK 33,Studio会提示缺失。此时不要盲目升级——项目UI基于Theme.MaterialComponents设计,而该主题在SDK 33下表现最稳定。正确做法是:
    - 打开ToolsSDK ManagerSDK Platforms → 勾选Android 13.0 (Tiramisu)Apply安装;
    - 安装完成后,重启Studio,再导入项目。

完成以上三步,点击Open an existing Android Studio project,选择解压后的根目录,等待Gradle同步完成。如果一切顺利,你会看到app模块下的javares文件夹正常展开,build目录自动生成。此时,不要急于运行,先做一次Clean:BuildClean Project,再BuildRebuild Project。这能暴露潜在的资源引用错误(如R.drawable.xxx找不到),比直接Run更容易定位问题。

4.2 内嵌编辑功能实现:如何让ListView点击不跳转,却实现完整编辑体验

这是项目最具匠心的功能点。传统做法是:点击列表项 → startActivity(new Intent(this, EditActivity.class)) → 在新页面编辑 → 返回时onActivityResult()刷新列表。而本项目实现了“点击即编辑,编辑完自动保存,全程无页面跳转”。其核心在于动态替换ListView的Item View

具体步骤如下:

  1. activity_main.xml中,为ListView的每个Item定义两种布局
    - item_task_normal.xml:常规显示布局,包含标题、优先级图标、截止日期文本、复选框;
    - item_task_edit.xml:编辑态布局,包含EditText(标题)、Spinner(优先级)、Button(选择日期)、CheckBox(完成状态)。

  2. TaskAdapter中,重写getViewTypeCount()getItemViewType()
    ```java
    @Override
    public int getViewTypeCount() {
    return 2; // 两种类型:NORMAL=0, EDIT=1
    }

@Override
public int getItemViewType(int position) {
Task task = getItem(position);
return task.isEditing() ? 1 : 0; // 根据Task对象的editing标志决定类型
}
`` 这样,ListView会为每个Item缓存两种不同的convertView`,避免类型混淆。

  1. getView()中,根据类型inflate不同布局,并绑定数据
    java if (viewType == TYPE_NORMAL) { // 绑定normal布局:设置标题、图标、日期、复选框状态 holder.titleText.setText(task.getTitle()); holder.priorityIcon.setImageResource(PriorityHelper.getIconResId(task.getPriority())); holder.dueDateText.setText(task.getFormattedDueDate()); holder.checkBox.setChecked(task.isCompleted()); } else { // 绑定edit布局:设置EditText内容、Spinner选中项、CheckBox状态 holder.editTitle.setText(task.getTitle()); holder.prioritySpinner.setSelection(getPriorityPosition(task.getPriority())); holder.editDueDate.setText(task.getFormattedDueDate()); holder.editCheckBox.setChecked(task.isCompleted()); }

  2. 最关键的交互逻辑:在MainActivity中监听列表点击
    java listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { Task clickedTask = taskAdapter.getItem(position); // 切换editing状态 clickedTask.setEditing(!clickedTask.isEditing()); // 通知Adapter刷新该位置 taskAdapter.notifyDataSetChanged(); // 如果是进入编辑态,自动聚焦EditText if (clickedTask.isEditing()) { EditText editTitle = view.findViewById(R.id.edit_title); editTitle.requestFocus(); InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); imm.showSoftInput(editTitle, InputMethodManager.SHOW_IMPLICIT); } } });
    这里clickedTask.setEditing()是核心——它改变了Task对象的内部状态,从而影响getItemViewType()的返回值,触发ListView重新调用getView()并渲染为编辑布局。整个过程在同一个Activity内完成,无任何Intent跳转,动画流畅,状态无缝衔接。

实操心得:setEditing()方法必须在主线程调用,且notifyDataSetChanged()后,ListView会自动回收并重建该位置的View。因此,requestFocus()showSoftInput()必须放在if (clickedTask.isEditing())块内,否则在退出编辑态时会尝试聚焦已销毁的View,导致空指针异常。我在第一次调试时就因漏掉这个判断而崩溃了三次。

5. 常见问题与排查技巧实录:那些文档里不会写的“血泪教训”

5.1 典型问题速查表

问题现象可能原因排查步骤解决方案
App启动后ListView空白,无任何任务显示TaskManager.loadTasks()返回空List① 检查shared_prefs/task_data.xml文件是否存在;② 用ADB命令adb shell cat /data/data/com.yourpackage/shared_prefs/task_data.xml查看内容;③ 确认Gson解析JSON时是否因字段名大小写不匹配失败Task类中,确保JSON字段名与Java字段名一致(如JSON中为"due_date",Java中应为@SerializedName("due_date") private long dueDateMillis;),或统一使用驼峰命名(dueDateMillis对应JSON的"dueDateMillis"
点击列表项无反应,无法进入编辑模式OnItemClickListener未正确设置或被其他View拦截① 检查listView.setOnItemClickListener()是否在onCreate()中调用;② 查看item_task_normal.xml中根布局是否设置了android:clickable="true"android:focusable="true",这会抢走点击事件item_task_normal.xml的根LinearLayout中,添加android:descendantFocusability="blocksDescendants",确保子View不抢焦点,让点击事件透传给ListView
截止日期显示为“1970-01-01”或乱码时间戳为0或格式化器时区错误① 在TaskAdapter.getView()中,Log.d("DEBUG", "dueDateMillis: " + task.getDueDateMillis())打印时间戳;② 若为0,检查DatePickerDialogonDateSet()回调中是否正确设置了calendar.setTimeInMillis(0)确保DatePickerDialogonDateSet()中,calendar.set(year, month, day)后,必须调用task.setDueDateMillis(calendar.getTimeInMillis()),而非calendar.getTime().getTime()(后者可能因时区导致偏差)
修改任务后,重启App数据丢失TaskManager.saveTasks()未被调用或SP提交失败① 在TaskController.updateTask()末尾添加Log.d("DEBUG", "Saving tasks count: " + tasks.size());② 检查editor.apply()是否被调用(apply()异步,commit()同步但阻塞)项目中使用editor.apply()是正确的,但需确保在saveTasks()方法末尾有editor.apply()调用。若仍丢失,可在apply()后添加Log.d("DEBUG", "SP apply called")确认执行流

5.2 独家避坑技巧:来自真实调试现场的经验

技巧一:用ADB实时窥探SharedPreferences内容
当怀疑数据存储异常时,比在代码里加无数Log更高效的方法是直接查看SP文件。步骤:
1. 确保手机开启USB调试;
2. 在终端执行:adb shell 进入设备;
3. 执行:run-as com.example.todo (将com.example.todo替换为你的实际包名);
4. 执行:cat shared_prefs/task_data.xml
5. 你会看到类似<string name="tasks_json">[{"id":1,"title":"买牛奶","priority":"HIGH","dueDateMillis":1718438400000}]</string>的原始内容。如果JSON格式错误(如缺少逗号、引号不匹配),一眼就能发现。这是定位序列化问题的终极手段。

技巧二:ListView闪烁问题的根治方案
有些设备上,快速点击列表项切换编辑/显示状态时,会出现短暂闪烁。这是因为notifyDataSetChanged()会重建整个列表。优化方案是在TaskAdapter中添加局部刷新方法:

public void updateItem(int position) {
    // 只刷新指定位置,不重建整个列表
    if (position >= 0 && position < getCount()) {
        notifyDataSetChanged(); // 或使用更精细的notifyItemChanged()(需改造为RecyclerView)
    }
}

然后在MainActivity的点击监听中,调用adapter.updateItem(position)而非adapter.notifyDataSetChanged()。虽然ListView没有notifyItemChanged(),但notifyDataSetChanged()配合getItemViewType()的精准控制,已能极大减少闪烁。

技巧三:深色模式下图标颜色失效的修复
如果发现深色模式下优先级图标变成灰色(不可见),检查ic_priority_high.xml等Vector Drawable文件。确保其中<path>标签的android:fillColor属性使用的是?attr/colorOnSurface这样的主题属性,而非硬编码颜色值。例如:

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="24"
    android:viewportHeight="24">
    <path
        android:fillColor="?attr/colorOnSurface" <!-- 关键! -->
        android:pathData="M12,2L13.09,8.26L20,9L13.09,9.74L16,16L12,13L8,16L11,9.74L4,9L10.91,8.26L12,2Z"/>
</vector>

?attr/colorOnSurface会根据深色/浅色主题自动切换为合适的前景色,这是Material Design规范推荐的做法。

最后再分享一个小技巧:这个项目的README.md里有一行不起眼的注释——“为简化学习,所有资源ID均采用R.drawable.xxx硬编码,实际项目中建议使用R.string.task_priority_high等字符串资源进行国际化”。这句话点出了工程化思维的关键:教学项目追求清晰,生产项目追求可维护。当你把这套逻辑吃透后,下一步就可以尝试为它加上多语言支持、夜间模式开关、甚至导出为CSV功能——而所有这些,都建立在你此刻对SharedPreferencesListViewMVC的扎实理解之上。代码不会骗人,每一行editor.putString()背后,都是数据落地的确定性;每一次holder.titleText.setText(),都是UI与逻辑握手的瞬间。这才是Android开发最本真的魅力。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:一个开箱即用的Android待办清单App源码项目,纯Java编写,适配主流Android版本。支持任务增删改查,点击列表项直接内嵌编辑,不跳转新页面,操作更连贯。所有数据默认保存在设备本地,基于SharedPreferences实现轻量持久化,应用重启后任务、优先级、截止日期全部保留。每条任务可设高/中/低三级优先级,对应彩色标识图标;支持选择截止日期,日期以清晰格式显示在列表项右侧。UI经过针对性优化,包括区分优先级的背景色、合理字号与行距、简洁卡片式布局,提升信息识别效率。项目结构规范,含完整Gradle构建配置(gradlew、build.gradle、settings.gradle)、标准app模块、源码目录(src)、README说明文档及混淆规则文件,适合用于Android基础开发学习——涵盖ListView使用、Intent通信、本地存储方案对比、MVC分层实践和屏幕适配要点。压缩包不含APK,需用Android Studio打开并编译运行。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

本文章已经生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值