前言:DAY8的进度定位、目标拆解与适用人群
做到第8天,智能备忘录项目已经跑通了基础的笔记增删改查、搜索和列表展示,数据层和路由层也已经有了雏形。但把APP用起来之后,很多人会开始觉得"功能都有了,哪里不太对劲"——列表永远是新增顺序,晚上用手机屏幕白得刺眼,设置改起来得进代码改常量。DAY8这一天的目标就是把这些问题一次性收拾干净:笔记时间排序、深浅主题切换、设置页面搭建、全局风格统一优化。这四件事单拆开都不难,难的是把它们串起来的时候对项目结构的影响——主题状态怎么做全局共享、排序逻辑应该放在数据层还是UI层、设置页的配置项怎么和业务逻辑解耦,这些才是今天真正要处理的点。
这篇内容适合两类人看。一类是刚跟着实战系列把备忘录写到能用的朋友,可以照着代码把DAY8的功能完整补齐;另一类是已经在写Flutter应用、但一直觉得自己的项目"代码也能跑,就是很乱"的开发者,这篇里的全局统一和主题架构思路能直接搬到你自己的项目里。所有示例代码都基于Flutter开发OpenHarmony应用的场景,涉及平台的差异点我会单独标注。
我习惯先讲清楚每块功能的定位再给代码,不然只照着敲一遍,遇到异常情况还是不会排查。
1. 笔记时间排序:先理清"什么时候排、按什么排、排完放哪"
1.1 排序触发链路:在数据层统一处理,而不是UI层反复sort
DAY7之前我一直在列表页的build方法里直接调notes.sort((a, b) => b.createTime.compareTo(a.createTime)),当时图省事。但到了DAY8我决定把排序逻辑收拢到一个地方,因为随着功能变多,"笔记列表"出现了好几种来源:主页全部笔记、搜索结果的过滤列表、某个标签下的筛选列表。如果三种列表分别在各自的build里写sort,早晚会出现两个页面排序规则不一致的情况——比如搜索页忘了写二级排序,结果时间相同的时候就乱序。
我当时在项目里新增了一个统一入口,叫NoteRepository,所有笔记列表的获取都经过它:
class NoteRepository { Future<List<NoteEntity>> fetchNotes({String? keyword, String? tagId}) async { var notes = await localDb.queryNotes( keyword: keyword, tagId: tagId, ); return NoteSortUtil.applyDefaultSort(notes); } }这样所有页面拿到的列表天然就是排好序的,UI层不需要关心排序逻辑。NoteSortUtil.applyDefaultSort是一个纯静态方法,便于单独测试。
1.2 复合排序:时间相同的时候不能靠"运气"排
备忘录的场景里,用户能感知到的排序维度有三个:置顶状态(最优先)、编辑时间(最近改的在上面)、创建时间(完全同时创建时的兜底)。这里有一个非常隐蔽的坑:Dart的List.sort在标准库文档里标记为不稳定排序,在OpenHarmony设备上和其它平台上跑同样的比较器,结果可能不一致。如果你的比较器遇到"两本时间完全一致"时返回0,最终谁前谁后就不再可控。
我第一次发现这个问题是两个测试账本在同一天里加了多条笔记,用毫秒级时间戳依然有撞车的,列表顺序在两次冷启动后会变。排查下来的根因就是比较器没有构成全序关系。
解决思路很朴素:让任意两条笔记的比较结果都不为0,比较器按优先级依次比较多个字段。
class NoteSortUtil { static List<NoteEntity> applyDefaultSort(List<NoteEntity> notes) { final sorted = List<NoteEntity>.from(notes); sorted.sort((a, b) { // 第一优先级:置顶 if (a.isPinned != b.isPinned) { return a.isPinned ? -1 : 1; } // 第二优先级:编辑时间倒序 int byUpdated = b.updatedAt.compareTo(a.updatedAt); if (byUpdated != 0) return byUpdated; // 第三优先级:创建时间倒序 int byCreated = b.createdAt.compareTo(a.createdAt); if (byCreated != 0) return byCreated; // 兜底:用id保证排序结果确定 return a.id.compareTo(b.id); }); return sorted; } }注意这里我是先List<NoteEntity>.from(notes)复制了一份,避免直接修改上层传入的原始列表。很多人在Repository里直接对返回的列表原地sort,一旦调用方还持有同一个List对象的引用,排序就会产生外部副作用,查起来非常费劲。
1.3 编辑时间和创建时间到底哪个做主排序字段
大多数笔记应用其实应该用"更新时间"做主排序,因为用户对"最近动过的笔记"出现在顶部有强烈预期——尤其是搜索场景里,你搜到一条旧笔记然后改了内容,回到列表却发现它还在原来的位置,这个反馈是很糟糕的。创建时间作为二级排序就够了。
但有一个例外:如果你做的是"每日待办清单"或者"按照日期归档的日记本",那创建时间应该排在前面,因为这种场景的时间锚点是"哪天记的",不是"哪天改的"。我在DAY8给NoteSortUtil留了一个枚举参数,默认走编辑时间优先,日记模式传SortMode.byCreatedAt。设置为"默认排序方式"之后,把它暴露到设置页面里,这也是后面设置页能接进来的关键抓手。
另一个要注意的点是置顶。置顶不能只在排序比较器里加一个布尔值判断,还要考虑置顶内部的顺序——用户把A置顶后又把B置顶,通常期望B在更上面,所以置顶内部还是得按时间倒序。用上面这段代码就已经覆盖了,但得留意:如果做"拖动手动排序"功能,置顶组和普通组之间的拖拽边界判断要单独设计,DAY8不做那么深,先给一个可扩展的比较器结构。
1.4 排序后的列表更新策略:setState前先确认引用变了没
在列表页拿到返回的排序结果后,我当时在第一版代码里直接执行了setState(() => _notes = sorted)。这没问题,但隐藏着一个性能隐患:假如后续要支持"排序方案切换"(比如按标题字母排),每次切换都重建整个列表倒也合理;但如果列表本身已经几十条配合缩略图,整页rebuild会产生肉眼可见的闪烁。
这里的优化手法是给ListView加itemExtent或者使用ListView.builder的addAutomaticKeepAlives配合RepaintBoundary,这些可以在后面的全局优化部分展开。DAY8这一步先把排序逻辑做干净,列表性能优化跟着全局统一一起做,避免"一天改十处"。
2. 深浅色主题切换:从"能换色"到"换得彻底、换得顺畅"
2.1 主题状态管理选型:ValueNotifier能解决的事不用上重量级方案
做深浅主题,首先要回答一个问题:主题切换这个状态放在哪里。它不像笔记数据那样需要跨模块查询,只需要在切换的瞬间通知所有订阅者重建UI,同时把用户的选择持久化到本地存储。这个需求用Flutter自带的ValueNotifier<ThemeMode>就够了,不需要为了主题单独引入Provider或Riverpod。
我是这样设计的:
class ThemeController { ThemeController(this._prefs) { _loadThemeMode(); } final SharedPreferences _prefs; final ValueNotifier<ThemeMode> modeNotifier = ValueNotifier(ThemeMode.system); static const _themeModeKey = 'theme_mode_index'; Future<void> _loadThemeMode() async { final index = _prefs.getInt(_themeModeKey) ?? ThemeMode.system.index; modeNotifier.value = ThemeMode.values[index]; } void setThemeMode(ThemeMode mode) { modeNotifier.value = mode; _prefs.setInt(_themeModeKey, mode.index); } }然后在MaterialApp顶层监听:
ValueListenableBuilder<ThemeMode>( valueListenable: themeController.modeNotifier, builder: (context, mode, _) { return MaterialApp( theme: AppTheme.light(), darkTheme: AppTheme.dark(), themeMode: mode, ); }, )这里要特别说明一下:GlobalKey需要放在ValueListenableBuilder的外面,或者MaterialApp内部的NavigatorKey不能跟着builder重建。我第一次写的时候把scaffoldMessengerKey直接定义在了builder的局部作用域里,结果切换主题后,所有SnackBar全部失效,调试了很久才发现是key被重建导致的。正确做法是把这些Key声明成顶层全局变量。
2.2 深浅色不是简单把background换黑,要设计一套"语义色板"
很多人做深色模式就是在build里写Theme.of(context).brightness == Brightness.dark ? Colors.black : Colors.white,这种三元表达式看得人头皮发麻,而且没法覆盖"成功绿""警告橙""次要文字"这些场景。我建议把颜色做成语义化的AppColors扩展,而不是直接散落各种Color。
我建了一个AppColors抽象类,浅色深色各自实现一份:
abstract class AppColors { Color get background; Color get card; Color get primary; Color get textPrimary; Color get textSecondary; Color get divider; Color get surfaceVariant; } class LightColors extends AppColors { @override Color get background => const Color(0xFFF7F8FA); @override Color get card => Colors.white; @override Color get primary => const Color(0xFF5B6AF0); @override Color get textPrimary => const Color(0xFF1A1D24); @override Color get textSecondary => const Color(0xFF6E7380); @override Color get divider => const Color(0xFFE8EAF0); @override Color get surfaceVariant => const Color(0xFFF0F1F5); } class DarkColors extends AppColors { @override Color get background => const Color(0xFF121316); @override Color get card => const Color(0xFF1E2026); @override Color get primary => const Color(0xFF8B97FF); @override Color get textPrimary => const Color(0xFFF2F3F7); @override Color get textSecondary => const Color(0xFF9A9FAD); @override Color get divider => const Color(0xFF2C2E36); @override Color get surfaceVariant => const Color(0xFF2A2D35); }然后再通过一个BuildContext扩展取用:
extension AppThemeContext on BuildContext { AppColors get colors { return Theme.of(this).brightness == Brightness.dark ? DarkColors() : LightColors(); } }业务代码里就变成了context.colors.card,所有页面都不再直接出现Color数值。这个改动的收益在全局统一阶段会被放大很多。
2.3 三态主题选择:跟随系统、浅色、深色,设置页必须给全
这里要先聊一个产品层面的判断:深浅色切换在移动端上最合理的交互不是"浅色/深色"二选一,而是三选一,包含"跟随系统"。因为很多用户不希望APP跟系统强绑定(夜间想在被窝里用白底应用,或者白天想用深色让OLED省电),但也不可能每次都手动切,跟随系统就是那个"免打扰"的中间态。
对应的,设置页里的"外观"区域我选择了三个单选条目而不是一个Switch。如果用Switch表达"深色模式开/关",就没法承载"跟随系统"这个选项——除非再加一个"跟随系统"的开关,那交互会变得很啰嗦。
三态存储到SharedPreferences时的取法在上面已经写了。这里补一个启动顺序的坑:SharedPreferences.getInstance()是异步的,如果MaterialApp在首帧就依赖主题值,你会看到一帧白屏闪一下,然后才切到正确的深色或浅色。解决办法是让根Widget在拿到prefs之前先显示一个启动画面。我在DAY8的实际处理是:
Future<void> main() async { WidgetsFlutterBinding.ensureInitialized(); final prefs = await SharedPreferences.getInstance(); final themeController = ThemeController(prefs); runApp(MemoApp(themeController: themeController)); }也就是先异步拿prefs再runApp,避免首帧主题闪烁。这种做法需要你在启动流程里接受"等待SharedPreferences初始化"的时间,实测几乎无感,但能彻底省掉首帧闪白问题。
2.4 在OpenHarmony上的深层适配:状态栏颜色、字体权重、滚动回弹差异
只看标题的人可能以为"深浅主题切换"就是MaterialApp传两个ThemeData,但真跑到OpenHarmony设备上,还有几个容易忽略的细节。
第一个是系统状态栏图标颜色。浅色模式下状态栏背景是浅色,那状态栏上的时间、信号图标必须是深色;切到深色模式后图标要跟着反白。在OpenHarmony上需要用到SystemChrome.setSystemUIOverlayStyle,并且不能只在App启动时执行一次,必须在主题切换监听里同步调用。我当时在ValueListenableBuilder的builder里根据mode动态设置:
SystemChrome.setSystemUIOverlayStyle( Theme.of(context).brightness == Brightness.dark ? SystemUiOverlayStyle.light // 白色图标,适合深色背景 : SystemUiOverlayStyle.dark, // 深色图标,适合浅色背景 );第二个是字体渲染。OpenHarmony平台上某些字重的渲染效果和标准Flutter预期不同,尤其是FontWeight.w500这个中间档,有些设备会直接渲染成w400的效果,深浅色模式下观感差异更明显。我在项目里的兜底方案是:正文用w400,标题用w600,少用w500,避免出现"看上去该是重点标题却和正文一样粗"的尴尬。
第三个是SafeArea和刘海屏状态栏高度。OpenHarmony机型的系统状态栏高度在深浅色模式切换时不会变,但如果你用了AppBar,切换主题时AppBar的颜色会变,状态栏背景色也要跟着AppBar走。我的项目里统一用AnnotatedRegion<SystemUiOverlayStyle>包住Scaffold,确保状态栏逻辑和AppBar颜色是同一个数据源驱动的,而不是各写各的。
这些都是实际跑测试机时陆续发现的,如果你只在模拟器上看效果,前两条大概率会漏掉。
3. 设置页面搭建:不是堆一堆ListTile就完事,要处理好数据持久化和异步反馈
3.1 设置页的页面结构与导航方式
备忘录的设置页我按"分区"组织:外观(主题、排序方式)、通知(开关提醒、提醒时间)、数据管理(导出、清空缓存、清除全部数据)、关于(版本号、开源许可)。分区用ListView包若干个顺序排列的SectionCard实现,每个SectionCard内部是一个Column包几个ListTile,这样比起直接在一个大ListView里用section header,视觉上更清爽,也方便整体控制卡片圆角和间距。
页面本身用Navigator.push推入,而不是Tab切换。因为设置页打开频率低、信息密度高,独立页面更符合用户直觉。在OpenHarmony上如果用户开启了侧滑返回手势,Navigator.push的默认动画能够正常工作,不需要额外配置。
3.2 设置项的核心结构:Switch、RadioListTile、普通跳转项
设置页里最常见的三种交互形态我分别处理:
- 开关类,例如"开启每日提醒"。对应
SwitchListTile,value从配置读取,onChanged里写入配置并同步重置本地通知。注意:开启通知需要动态申请权限,切换开关后应该先请求权限,如果被拒绝要把Switch拨回原地并给用户Toast提示,不能假装开启成功。 - 单选项,例如主题模式、首页默认排序方式。对应
RadioListTile或者ListTile + 选中标记,点击后立即持久化并反馈(高亮选中项、关闭弹层/弹窗)。 - 跳转类,例如"数据导出""版本信息"。对应
ListTile带trailing: Icon(Icons.chevron_right),跳到对应子页面或弹窗。
其中单选项我专门做了一个小组件封装:参数是当前值、可选值列表、标题,点击事件回调,UI统一用"右侧圆点选中"的形式,和系统设置的视觉习惯保持一致。
class _RadioGroupItem<T> extends StatelessWidget { final String title; final List<T> values; final T selected; final ValueChanged<T> onChanged; const _RadioGroupItem({ required this.title, required this.values, required this.selected, required this.onChanged, }); @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.fromLTRB(16, 14, 16, 8), child: Text(title, style: Theme.of(context).textTheme.titleSmall), ), ...values.map((value) { final isSelected = value == selected; return RadioListTile<T>( value: value, groupValue: selected, title: Text(_labelFor(value)), onChanged: (next) { if (next != null && next != selected) onChanged(next); }, selected: isSelected, ); }), ], ); } }这里有个小细节:RadioListTile在被选中时,只有把selected设为true,才会渲染出"选中态"的勾选视觉,否则只是圆点变化,不明显。很多新手在这里漏掉,会导致"点了但看起来没选中"的错觉。
3.3 设置项的持久化与即时生效
设置页面里牵涉到的变更大体分两种:一种是马上会改变全局状态的(主题、排序方式),一种是只影响后续行为的(通知开关、提醒时间)。统一的做法是变更后立刻写SharedPreferences,并通过各Controller的ValueNotifier通知全局刷新,而不是等用户退出设置页再批量保存。
我在项目里专门封装了一个SettingsRepository,负责以下几类数据的读取和写入:
themeModeIndex:主题当前值,影响MaterialApp级defaultSortMode:默认排序,影响笔记列表数据获取dailyReminderEnabled+reminderTime:通知,影响本地通知调度cacheSize:数据管理页展示用,每次进入页面实时计算
这个Repository和ThemeController、NoteRepository三者之间的依赖方向必须控制好:SettingsRepository只做读写,不持有业务状态;ThemeController从SettingsRepository读主题;NoteRepository从SettingsRepository读排序方式。如果反了,比如Repository直接调用ThemeController,就会出现初始化顺序问题,冷启动时容易空指针。
时效性上有个反直觉的体验点:普通开关类变更不需要全局rebuild,但你需要在对应业务模块里能感知到变化。比如用户关闭了"每日提醒",正在运行中的定时器并不会因为SharedPreferences写入就自己停掉。我是在设置页的onChanged里直接调用ReminderService.cancelDailyReminder()的,配置持久化只是"下一次启动时读取的底账",真正让功能当下生效的是业务服务方法的显式调用。这一点最容易漏。
3.4 数据管理区:导出与清空的二次确认
"清除全部数据"这种高危操作,一定不能只用一个Switch或者一个ListTile点一下就直接执行。我在点击后弹了一个BottomSheet,显示"将删除所有笔记且不可恢复",需要输入"DELETE"(或者长按确认按钮)才能执行。这个交互不是故作高深,而是真实项目里误触之后找不回数据的代价太大了。
导出功能我用的是最简单的兜底方案:把所有笔记序列化为JSON字符串,写入到应用文档目录下的导出文件,然后用系统分享面板发出去。实现核心是:
final jsonList = notes.map((note) => note.toJson()).toList(); final file = await File('${appDocDir.path}/memo_export_${timestamp}.json') .writeAsString(jsonEncode(jsonList));DAY8的导出属于"先跑通"级别,只保证文件能生成、能读回,后续DAY9如果做云同步或备份恢复,可以在这个基础上再扩展导入校验和冲突合并。
3.5 关于页的细节:版本号显示别写死
设置页底部的版本号,我一开始写的是Text('v1.0.0'),后来发现每次打包发布都要改,而且发测试包时容易忘了改。更好的做法是从包信息里读取版本号。在Flutter里可以用PackageInfo插件实现:
PackageInfo.fromPlatform().then((info) { setState(() => _versionText = 'v${info.version} (${info.buildNumber})'); });这个改动虽小,但是能杜绝"测试包和线上包版本号一样"这种尴尬问题。
4. 全局风格统一优化:把散落的硬编码收进设计系统,一次改到位
4.1 一次"颜色普查":全局搜索Color(和EdgeInsets后触目惊心
做风格统一之前,我先干了一件事:全局搜索Color(和EdgeInsets.only,看看项目里有多少处硬编码。结果在自己的项目里找到了七十多处颜色常量、二十多种间距值。白色有时候用Colors.white,有时候用Color(0xFFFFFFFF);边距有4、6、8、10、12、16浮动,圆角有6、8、10、12。这种程度的混乱,后期想改一个主题几乎等于把所有页面重写一遍。
风格统一的核心不是"把所有页面写得一模一样",而是把"设计决策"收敛到尽可能少的文件里,以后想调整就只改一个地方。所以这一阶段的任务是:
- 定义Spacing和Radius标准
- 定义文字排版标准
- 定义通用组件标准
- 逐个页面替换硬编码
4.2 轻量级设计常量:间距、圆角、文字样式
我把间距固定为4的整数倍,基础单位是4:
class AppSpacing { static const double xs = 4; static const double sm = 8; static const double md = 12; static const double lg = 16; static const double xl = 24; static const double xxl = 32; } class AppRadius { static const double sm = 6; static const double md = 10; static const double lg = 14; static const double xl = 20; }在替换过程中会遇到一个现实问题:有的页面里间距是"视觉上看起来差不多"的6和7并存,你会忍不住想"要不要顺手统一",但我劝你不要顺手改。只做"机械替换"——把EdgeInsets.all(6)替换成EdgeInsets.all(AppSpacing.sm)——而不改变原本的视觉间距。因为一旦改变了视觉布局,你需要重新检查所有页面,在DAY8这个阶段会拖慢进度。真实项目里,间距微调和设计系统落地是两件不同的事。
文字样式我建了AppTextTheme,里面统一了标题、正文、辅助文字的size和weight:
class AppTextStyle { static const TextStyle pageTitle = TextStyle(fontSize: 20, fontWeight: FontWeight.w600); static const TextStyle sectionTitle = TextStyle(fontSize: 15, fontWeight: FontWeight.w600); static const TextStyle body = TextStyle(fontSize: 14, fontWeight: FontWeight.w400); static const TextStyle caption = TextStyle(fontSize: 12, fontWeight: FontWeight.w400); }注意这些样式类没有绑定颜色,颜色一律从context.colors取。如果绑定到ThemeData的textTheme里,深浅色模式下可以自动切换,但代价是所有页面都要走Theme的textTheme,在存量代码里一下子改不过来;用独立样式类的方案更渐进,新代码用AppTextStyle.body,旧代码逐渐替换。
4.3 公共组件收敛:空状态、加载态、提示条不再各写各的
在备忘录这种工具型应用里,最容易出现风格分裂的是空状态、加载中和保存成功的提示。
空状态我统一做成了EmptyPlaceholder组件:一个图标、一行主文案、一行可选副文案、一个可选操作按钮。页面里再也不用写:
Center( child: Column( children: [Icon(Icons.inbox), Text('还没有笔记'), Text('点击右下角新建')], ), )而是直接:
EmptyPlaceholder( icon: Icons.inbox_outlined, title: '还没有笔记', message: '点击右下角新建你的第一条备忘录', actionLabel: '快速新建', onAction: _createNote, )加载状态我用了一个全局通用的LoadingIndicator,并且区分全屏加载和局部加载。保存成功的反馈统一从ScaffoldMessenger.of(context).showSnackBar收敛到一个AppToast.show(context, '已保存')包装函数里,这个包装函数内部负责处理SnackBar的时长、背景色、主题变化后的样式。
SnackBar这里有个坑:如果你在主题切换后还持有旧的ScaffoldMessengerState,弹出的SnackBar背景色可能是旧的浅色主题。解决方式是别缓存SnackBar状态,每次show的时候从当前context重新取ScaffoldMessenger.of(context)。这也是为什么前面强调scaffoldMessengerKey要在顶层稳定持有的另一个原因。
4.4 列表性能与整体观感:itemExtent、RepaintBoundary、SafeArea、滚动弹簧
风格统一不只是颜色和字体,还包括交互反馈的一致。DAY8里我顺手处理了三个影响整体质感的问题:
第一,列表项高度混乱的问题。备忘录列表里每条笔记的高度会随标题两行/一行、摘要有无而变化,滚动时会出现明显的跳动。我给列表项外层套了RepaintBoundary,让每条卡片独立绘制,滚动时不会把整列表都重绘。同时在ListView.builder里设置了itemExtent,虽然不能完全固定两行摘要的高度,但固定了卡片的最小高度,实测滚动流畅度提升非常明显。
第二,手势与滚动冲突。在OpenHarmony设备上,某些系统版本的下拉刷新手势和CustomScrollView的physics有冲突,表现为"下拉刷新偶尔拉不动,或者松手后列表回弹到异常位置"。我最终在ScrollConfiguration里设置了全局的physics: ClampingScrollPhysics(),关闭了部分平台默认的弹性效果,这虽然牺牲了一点iOS式的回弹手感,但换来的是OpenHarmony上行为的一致性。
第三,深浅色模式下SafeArea区域的处理。设置页最底部如果贴着导航条,深色模式下会有一条浅色留白,非常难看。解决方案是给设置页的根Container设置color: context.colors.background,让底部的安全区域也被背景色覆盖:
Scaffold( backgroundColor: context.colors.background, body: SafeArea( child: ListView(...), ), )干净利落,一劳永逸。
4.5 样式替换过程中可以顺带建立的"自查清单"
全局统一是一个漫长的替换过程,容易改着改着就漏掉角落。我在项目里整理了一份简单自查清单,每次替换完一个页面就对照检查一遍:
| 检查项 | 问题现象 | 处理方式 |
|---|---|---|
| 硬编码颜色 | 页面里有Color(0xFFxxxx) | 换成context.colors.xxx |
| 硬编码间距 | EdgeInsets.all(7)这类非标值 | 换成最近的AppSpacing档位 |
| 状态栏对比度 | 深色模式下状态栏图标看不见 | 用AnnotatedRegion包裹并动态设置样式 |
| 空状态/加载态样式 | 每个页面长得不一样 | 统一使用EmptyPlaceholder和LoadingIndicator |
| SnackBar频繁闪烁 | 切换主题后提示条颜色异常 | 改用AppToast.show统一入口 |
| 阴影溢出 | 卡片阴影在深色模式下过深 | 统一阴影颜色按主题区分,浅色shadow更轻、深色shadow更透明 |
这个清单不是一次性任务,它是给整个DAYS阶段兜底的。DAY8把它建立起来,后面继续加功能时,新页面照着清单走,就不会再产生新的风格债。
收尾:DAY8做完之后我对这个项目的架构新感受
写到DAY8,最明显的变化不是功能列表多了三四个,而是项目的改造成本开始显著下降。排序逻辑收进Repository之后,我在搜索页里排查一个"时间相同但顺序不稳定"的bug只用了十分钟;主题颜色收进context.colors之后,想调整主色调只需要改两个类文件。这些都是前期没有专门规划、但做到一半时"顺手"获得的收益。
如果让我给正在做同样事情的朋友一个建议:DAY8这一天尽量把"设计系统"当成一个正式任务来做,而不是"顺便优化"。即便一开始只定义间距、颜色和三个公共组件,也让后续每一个新页面的开发成本低一个量级。下一阶段我计划做云同步和跨设备冲突合并,到时候设置页里"数据管理"这一块就能真正发挥作用了——不过那是后话了,先把今天的改动多跑几台OpenHarmony设备,确认深浅色切换和状态栏适配没有边界问题,再往下走也不迟。