flutter_for_openharmony逆向思维训练app实战+数列推理实现
本文介绍了数列推理模块的实现,包含以下核心内容: 模块导航设计 通过PatternRecognitionPage统一管理训练项入口 使用导航卡片构建方法保持样式一致性 采用const构造器优化性能 数据结构设计 使用二维列表存储题目、答案和规律说明 平行列表确保数据关联性 新增3的幂次序列丰富题库 页面实现要点 响应式布局适配不同屏幕 圆形卡片可视化展示数列 灰色问号容器标识待求解项 状态管理控制

目标
- 从“模式识别”模块进入
数列推理 - 用一个固定的
sequences列表提供题目 - 用
TextField接收用户输入 - 用
showResult控制解析区展示 - 用“上一题/下一题”切换序列
本文涉及文件
lib/feature_pages.dartlib/app.dartlib/main.dart
1. 入口:数列推理挂在“模式识别”模块里
项目的根页面在 lib/app.dart,四个 Tab 里第二个是 PatternRecognitionPage。PatternRecognitionPage 会把 8 个训练项组织成一个列表,其中第一项就是 数列推理,这是功能模块的核心导航逻辑:
- 模块化拆分:主页仅承担导航职责,功能页专注业务逻辑,符合“单一职责”设计原则
- 入口统一管理:所有训练项都在
PatternRecognitionPage中注册,便于后续统一维护和扩展 - 视觉与功能绑定:通过图标(Icons.trending_up)和文字(数列推理)形成直观的功能识别
对应入口行(位于 lib/feature_pages.dart):
_buildFeatureCard(context, '数列推理', Icons.trending_up, const SequenceReasoningPage()),
- 该代码行是典型的“导航卡片构建”写法,参数依次为上下文、标题、图标、目标页面
- 常量构造器(const SequenceReasoningPage())减少不必要的对象重建,提升性能
- 统一的
_buildFeatureCard方法保证所有训练项入口样式、交互逻辑一致
3. SequenceReasoningPage 的真实代码
下面这段实现来自你项目 lib/feature_pages.dart,我们按模块拆解分析:
3.1 页面入口定义
class SequenceReasoningPage extends StatefulWidget {
const SequenceReasoningPage({super.key});
State<SequenceReasoningPage> createState() => _SequenceReasoningPageState();
}
- 类命名规范:遵循“大驼峰+功能描述+Page”的命名方式,符合 Flutter 开发规范
- 常量构造器:const 关键字减少对象重复创建,提升页面初始化性能
- 状态类关联:createState 方法明确绑定对应的状态类,是 StatefulWidget 的核心语法
- 键值传递:super.key 保证 Widget 树中节点的唯一性,避免重建时的异常
3.2 状态类与基础数据定义
class _SequenceReasoningPageState extends State<SequenceReasoningPage> {
final List<List<int>> sequences = [
[1, 4, 9, 16, 25], // 平方数
[2, 3, 5, 7, 11], // 质数
[1, 1, 2, 3, 5], // 斐波那契
[2, 6, 12, 20, 30], // n*(n+1)
[3, 9, 27, 81, 243], // 新增:3的幂次序列
];
- 数据结构设计:二维列表存储多组数列,外层列表对应“题目列表”,内层列表对应“单题数列”
- 不可变声明:final 关键字保证数列数据初始化后不可修改,避免业务逻辑中误操作
- 注释规范:每个数列后标注规律,提升代码可读性,便于后续扩展和维护
- 扩展性设计:新增 3 的幂次序列,丰富题库类型,且保持数据结构兼容性
final List<String> answers = ['36', '13', '8', '42', '729'];
final List<String> patterns = [
'平方数序列:n²(n从1开始)',
'质数序列:只能被1和自身整除的数',
'斐波那契数列:前两项之和等于后一项',
'n*(n+1)序列:相邻两个自然数的乘积',
'3的幂次序列:3ⁿ(n从1开始)'
];
- 平行列表设计:answers、patterns 与 sequences 按索引一一对应,数据关联清晰
- 答案类型选择:字符串类型存储答案,兼容未来非数字答案(如字母、表达式)的扩展
- 规律描述优化:新增详细的规律说明,相比原注释更易理解,提升用户学习效果
- 数据一致性:新增题目后同步扩展答案和规律列表,保证三者长度一致
int currentSequence = 0;
String? userAnswer;
bool showResult = false;
- 状态初始化:
- currentSequence 初始化为 0,默认展示第一题,符合用户操作习惯
- userAnswer 声明为可空字符串,避免初始值干扰输入判断
- showResult 初始化为 false,保证页面加载时不显示解析区
- 状态粒度控制:仅保留核心状态,无冗余字段,降低状态管理复杂度
- 类型安全:明确的类型声明(int、String?、bool)避免运行时类型异常
3.3 页面构建核心逻辑
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('数列推理')),
body: Padding(
padding: EdgeInsets.all(16.w),
child: Column(
children: [
Text('找出下一个数字', style: TextStyle(fontSize: 20.sp, fontWeight: FontWeight.bold)),
- 页面结构规范:Scaffold + AppBar + Body 是 Flutter 页面的标准结构,保证兼容性
- 响应式布局:使用 .w/.h/.sp 单位(需依赖 flutter_screenutil),适配不同屏幕尺寸
- 样式分层:标题文字设置加粗和字号,突出核心任务,符合视觉层级设计
- 内边距设置:Padding 包裹整个 body,避免内容贴边,提升页面美观度
SizedBox(height: 24.h),
Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Colors.blue[50],
borderRadius: BorderRadius.circular(12.r),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
- 间距控制:SizedBox 明确设置垂直间距,相比 Padding 更适合独立组件间的间距控制
- 容器样式:
- 浅蓝背景色(Colors.blue[50])营造轻量的题目卡片视觉效果
- 圆角设计(12.r)弱化直角的生硬感,符合现代 UI 设计趋势
- 内边距保证内容与容器边界的距离,提升可读性
- 布局对齐:Row 设置 spaceEvenly,使数列项均匀分布,视觉上更平衡
children: sequences[currentSequence].map((num) =>
Container(
width: 40.w,
height: 40.w,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(20.r),
),
- 动态渲染:map 方法遍历当前数列,将每个数字转换为可视化的圆形卡片
- 圆形卡片实现:
- 宽高均为 40.w,保证正方形容器
- borderRadius 设为宽高的一半(20.r),完美实现圆形效果
- 蓝色背景+白色文字,高对比度提升可读性
- 响应式适配:统一使用 .w 单位,保证不同屏幕下圆形比例不变形
child: Center(child: Text(num.toString(),
style: TextStyle(color: Colors.white, fontSize: 16.sp))),
),
).toList() + [
Container(
width: 40.w,
height: 40.w,
decoration: BoxDecoration(
color: Colors.grey,
borderRadius: BorderRadius.circular(20.r),
border: Border.all(color: Colors.blue, width: 2),
),
- 类型转换:map 遍历返回的 Iterable 转换为 List,符合 Row 子组件的类型要求
- 未知项设计:
- 灰色背景区分已知项和未知项
- 蓝色边框保持视觉关联,提示用户此处为需要填写的位置
- “?”符号直观表达“待求解”的含义
- 列表拼接:通过 + 运算符拼接未知项容器,实现“数列+问号”的完整展示逻辑
child: Center(child: Text('?',
style: TextStyle(color: Colors.blue, fontSize: 16.sp))),
),
],
),
),
- 文字居中:Center 组件保证“?”和数字在圆形容器中居中显示,视觉对齐
- 样式统一:未知项文字字号、颜色与已知项呼应,保持视觉一致性
- 布局闭合:Row 组件的 children 列表完整闭合,避免布局异常
SizedBox(height: 32.h),
TextField(
decoration: const InputDecoration(
labelText: '你的答案',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
onChanged: (value) => userAnswer = value.trim(),
),
- 输入优化:onChanged 中增加 trim() 方法,去除用户输入的首尾空格,避免因空格导致的答案判断错误
- 键盘适配:keyboardType 设置为数字键盘,降低用户输入成本,提升交互体验
- 输入框样式:OutlineInputBorder 提供清晰的边框样式,labelText 明确提示输入用途
- 状态更新:输入过程中仅更新 userAnswer,不触发 setState,减少不必要的 UI 重建
SizedBox(height: 24.h),
ElevatedButton(
onPressed: userAnswer?.isNotEmpty == true
? () => setState(() => showResult = true)
: null,
child: const Text('确认答案'),
),
- 按钮禁用逻辑优化:
- 原逻辑仅判断 userAnswer != null,优化后增加 isNotEmpty 判断,避免空字符串触发确认
- onPressed 设为 null 时,按钮自动禁用并变灰,符合 Material Design 交互规范
- 状态触发:点击确认后通过 setState 更新 showResult,触发解析区展示,实现“输入-确认-展示”的交互闭环
- 代码简洁性:使用箭头函数简化回调逻辑,代码更紧凑易读
if (showResult) ...[
SizedBox(height: 16.h),
Container(
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: userAnswer == answers[currentSequence]
? Colors.green[100]
: Colors.red[100],
borderRadius: BorderRadius.circular(8.r),
),
- 条件渲染:if (showResult) …[] 语法实现解析区的条件展示,相比 Visibility 组件更轻量
- 反馈设计:
- 正确答案:浅绿色背景(Colors.green[100]),传递“正确”的视觉信号
- 错误答案:浅红色背景(Colors.red[100]),传递“错误”的视觉信号
- 圆角设计保持与题目卡片的视觉一致性
- 内边距设置:12.w 的内边距保证解析内容不贴边,提升可读性
child: Column(
children: [
Text('正确答案:${answers[currentSequence]}',
style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w500)),
SizedBox(height: 8.h),
Text('规律:${patterns[currentSequence]}',
style: TextStyle(fontSize: 14.sp, color: Colors.grey[700])),
],
),
),
],
- 样式分层:
- 正确答案:字号 16.sp + 半粗体,突出核心信息
- 规律说明:字号 14.sp + 灰色文字,作为辅助信息,视觉层级清晰
- 间距控制:8.h 的垂直间距区分答案和规律,避免内容拥挤
- 字符串插值:直接使用 ${} 拼接变量,代码简洁且易维护
Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: currentSequence > 0 ? () => setState(() {
currentSequence--;
userAnswer = null;
showResult = false;
}) : null,
child: const Text('上一题'),
),
- 布局推挤:Spacer 组件占据剩余空间,将按钮行推至页面底部,符合“操作区在下”的交互习惯
- 边界判断:currentSequence > 0 保证“上一题”按钮仅在非第一题时可用,避免索引越界
- 状态重置:切题时重置 userAnswer 和 showResult,避免上一题的输入和解析残留,保证每题的独立性
ElevatedButton(
onPressed: currentSequence < sequences.length - 1 ? () => setState(() {
currentSequence++;
userAnswer = null;
showResult = false;
// 新增:切题后自动收起键盘
FocusScope.of(context).unfocus();
}) : null,
child: const Text('下一题'),
),
],
),
],
),
),
);
}
}
- 新增交互优化:FocusScope.of(context).unfocus() 切题时收起键盘,避免键盘遮挡新题目内容
- 边界判断:currentSequence < sequences.length - 1 保证“下一题”按钮仅在非最后一题时可用
- 状态一致性:切题时同步重置三个核心状态,保证新题目加载时的初始状态干净
- 按钮布局:Row + spaceEvenly 保证两个按钮水平均匀分布,视觉平衡
4. 题目数据的组织:sequences + answers + patterns 三个列表
你把题目拆成了三个平行列表,这种设计在小体量静态题库中优势明显:
4.1 设计优势
- 直观易理解:三个列表按索引一一对应,开发和维护时可快速定位某一题的完整信息
- 示例:currentSequence = 2 时,可快速找到 sequences[2](斐波那契数列)、answers[2](8)、patterns[2](斐波那契数列规律)
- 开发成本低:无需定义额外的模型类,直接通过列表索引访问数据,代码量少
- 扩展简单:新增题目时,只需在三个列表末尾同步添加对应内容,无需修改核心逻辑
- 兼容性好:适配 Flutter 所有版本,无第三方依赖,运行稳定
4.2 约束与优化建议
- 核心约束:必须保证
sequences.length == answers.length == patterns.length,否则会触发索引越界异常- 优化:可在初始化时增加断言校验,提前发现数据不一致问题
assert(sequences.length == answers.length && answers.length == patterns.length, "数列、答案、规律列表长度必须一致"); - 扩展性局限:当题目数量超过20道时,平行列表的维护成本会显著增加
- 优化建议:定义统一的模型类,将三个字段封装为对象
class SequencePuzzle { final List<int> sequence; final String answer; final String pattern; const SequencePuzzle({ required this.sequence, required this.answer, required this.pattern, }); } // 使用示例 final List<SequencePuzzle> puzzles = [ SequencePuzzle( sequence: [1,4,9,16,25], answer: '36', pattern: '平方数序列:n²(n从1开始)' ), ]; - 数据不可变性:final 关键字保证列表引用不可变,但列表内的元素仍可修改
- 优化:使用 List.unmodifiable 包装列表,禁止修改内部元素
final List<List<int>> sequences = List.unmodifiable([ [1,4,9,16,25], // 其他数列 ]);
这种结构的特点是:
- 写起来简单
- 适合题量小、静态内置题库
同时它也要求你保持一个不变的约束:
sequences.length == answers.length == patterns.length
在当前代码里你天然满足。
后续如果要动态加载题目,建议把这三项合并成一个模型对象,例如
SequencePuzzle { List<int> seq; String answer; String pattern; }
但在当前项目里,平行列表是足够清晰的。
5. 数列展示的实现:map + toList 再拼接一个问号格
你在 UI 上用一排圆形数字格展示数列,核心写法的设计细节如下:
5.1 核心写法拆解
children: sequences[currentSequence].map((num) => Container(...)).toList() + [ Container(...'?') ],
- 动态渲染逻辑:
- map 方法遍历当前数列的每个数字,转换为对应的圆形卡片组件
- toList() 将 Iterable 转换为 List,满足 Row 组件对 children 类型的要求
-
- 运算符拼接问号容器,实现“已知数列 + 未知项”的完整展示
- 数据驱动 UI:数列数据直接决定展示内容,数据更新时 UI 自动同步,符合“数据驱动”的开发思想
- 组件复用性:每个数字卡片的样式、尺寸完全一致,保证视觉统一性
- 扩展性:若需修改数列展示样式,只需调整 map 中的 Container 配置,所有数字卡片会同步更新
5.2 视觉设计细节
- 色彩区分:已知项使用纯蓝色背景,未知项使用灰色背景+蓝色边框,既区分又关联
- 尺寸统一:所有卡片宽高均为 40.w,保证视觉上的等距分布
- 文字居中:Center 组件保证数字和“?”在卡片中垂直水平居中,提升可读性
- 响应式适配:使用 .w 单位,在不同屏幕尺寸下保持卡片比例不变
这比直接写一段文本更有训练感。
6. TextField 的输入:为什么 onChanged 不 setState
你写的核心代码:
onChanged: (value) => userAnswer = value.trim(),
6.1 设计思路
- 性能优化:输入过程中不触发 setState,避免每次输入字符都重建整个页面,提升流畅度
- 示例:用户输入“36”时,会触发两次 onChanged(输入3、输入6),若每次都 setState,会导致页面两次重建
- 状态暂存:仅将输入值暂存到 userAnswer 变量中,不立即更新 UI,符合“输入-确认”的交互逻辑
- 输入清洗:新增 trim() 方法,自动去除首尾空格,避免用户误输入空格导致答案判断错误
- 类型安全:value 为字符串类型,直接赋值给 userAnswer,无需类型转换,代码简洁
6.2 交互配合
- 按钮禁用逻辑:确认按钮仅在 userAnswer 非空时可用,引导用户先输入再确认
- 键盘适配:数字键盘减少非数字字符的输入,降低输入错误率
- 输入校验:后续可扩展输入过滤(如仅保留数字),进一步提升输入准确性
与之配合的是按钮的禁用逻辑。
7. 按钮禁用:用 onPressed == null 做引导
你对“确认答案”按钮使用的核心代码(优化后):
onPressed: userAnswer?.isNotEmpty == true ? () => setState(() => showResult = true) : null,
7.1 交互引导价值(分点说明)
- 自然引导流程:按钮禁用状态直观告诉用户“当前不可操作”,引导用户先完成输入
- 对比弹窗提示:无需额外的文字提示,通过视觉状态引导,更符合直觉
- 避免无效操作:禁止空输入触发确认,减少无意义的状态更新和逻辑判断
- 符合 Material Design 规范:onPressed 为 null 时,ElevatedButton 自动应用禁用样式(灰底+浅文字),无需手动设置样式
- 代码简洁:三元运算符直接判断状态,无需额外的 if-else 语句,代码更紧凑
7.2 边界情况处理
- 空字符串处理:userAnswer?.isNotEmpty 同时判断“非空”和“非空字符串”,覆盖 userAnswer = “” 的情况
- 空值处理:userAnswer? 空安全运算符,避免 userAnswer 为 null 时触发空指针异常
- 状态触发:确认后仅更新 showResult 状态,聚焦核心逻辑,无冗余操作
它让页面的流程自然变成
- 先输入
- 再确认
而不是把所有操作都暴露出来让用户猜。
8. 结果展示:颜色反馈 + 文本解释
你在展示结果时的核心代码:
color: userAnswer == answers[currentSequence] ? Colors.green[100] : Colors.red[100]
Text('正确答案:${answers[currentSequence]}')
Text('规律:${patterns[currentSequence]}')
8.1 视觉反馈设计
- 色彩心理学应用:
- 绿色:代表“正确、成功”,给用户积极的反馈,增强学习成就感
- 红色:代表“错误、需要修正”,提示用户核对答案,强化记忆
- 浅色调(100)避免颜色过艳,提升页面美观度
- 样式一致性:解析区容器使用圆角设计,与题目卡片保持视觉风格统一
- 内边距控制:12.w 的内边距保证文字与容器边界有足够距离,提升可读性
8.2 信息展示设计
- 信息分层:
- 核心信息:正确答案(加粗+大字号),优先展示
- 辅助信息:规律说明(常规字号+灰色),补充解释
- 内容完整:同时展示答案和规律,不仅告诉用户“答案是什么”,还解释“为什么”,符合训练类应用的核心目标
- 动态适配:解析内容随当前题目自动切换,保证信息与题目匹配
这种组合能同时满足
- 快速判断对错
- 立刻知道“为什么”
这比只弹一个对/错更适合训练类应用。
9. 为什么切题必须重置 userAnswer 与 showResult
你在“上一题/下一题”的回调里核心代码:
userAnswer = null;
showResult = false;
// 新增:收起键盘
FocusScope.of(context).unfocus();
9.1 避免答案串题
- 场景还原:若不重置 userAnswer,用户切到下一题时,输入框仍显示上一题的答案(如从第一题切到第二题,输入框仍显示36)
- 交互误导:用户可能误以为当前输入的答案适用于新题目,直接点击确认,导致错误的答题结果
- 状态干净:重置为 null 后,输入框清空,按钮恢复禁用状态,保证新题的初始状态正确
9.2 避免解析串题
- 视觉干扰:若不重置 showResult,新题目页面会显示上一题的解析内容(如切到第二题仍显示第一题的平方数规律)
- 逻辑混乱:用户会混淆“当前题目”和“解析内容”的对应关系,降低学习效果
- 流程重置:恢复到“未输入、未确认”的初始状态,保证每题的交互流程独立
9.3 新增交互优化
- 收起键盘:FocusScope.of(context).unfocus() 切题时自动收起键盘,避免键盘遮挡新题目内容
- 操作流畅性:无需用户手动点击空白处收起键盘,提升交互的连贯性
- 兼容性:适配不同输入法和屏幕尺寸,保证操作一致性
所以把这两句放到切题逻辑里,是最稳的处理。
10. 题目切换如何避免越界
你在按钮的 onPressed 上做的核心代码:
上一题:
onPressed: currentSequence > 0 ? () => setState(() { ... }) : null,
下一题:
onPressed: currentSequence < sequences.length - 1 ? () => setState(() { ... }) : null,
10.1 边界保护设计(分点说明)
- 前置判断:将边界判断写在 onPressed 赋值阶段,而非回调内部,有两大优势:
- 按钮禁用:不符合条件时按钮自动禁用,用户无法点击,从源头避免越界
- 代码简洁:无需在回调内写 if 判断,减少嵌套,提升代码可读性
- 索引计算:
- 上一题:currentSequence > 0 保证索引不小于0,避免负数索引
- 下一题:currentSequence < sequences.length - 1 保证索引不超过列表最后一位(如列表长度为5,最大索引为4)
- 动态适配:边界判断基于 sequences.length,新增题目后无需修改判断逻辑,自动适配
10.2 交互体验
- 视觉提示:禁用状态的按钮直观告诉用户“已到第一题/最后一题”,无需额外提示
- 操作安全:避免用户点击无效按钮导致的无响应或异常,提升交互稳定性
- 代码可维护:判断逻辑与数据列表长度绑定,后续扩展题目时无需修改边界判断代码
这种写法的好处是:
- 边界条件被“写进按钮可用性”
- 不需要在回调里额外 if
可读性很强:
- 什么时候能点,什么时候不能点,一眼就能看出来
11. keyboardType: TextInputType.number 的真实意义
你给 TextField 设置的核心代码:
keyboardType: TextInputType.number
11.1 核心作用
- 键盘适配:触发系统数字键盘(移动端),用户无需切换键盘即可输入数字,降低输入成本
- 对比普通键盘:减少字母、符号等无关按键的干扰,提升输入效率
- 输入引导:暗示用户需要输入数字,符合“数列推理”的题目类型
- 兼容性:适配不同平台(Android/iOS/OpenHarmony)的数字键盘样式,保证跨平台体验一致
11.2 局限性与优化
- 非强制限制:仅改变键盘类型,无法阻止用户输入非数字字符(如符号、字母)
- 示例:用户仍可通过粘贴、第三方输入法输入非数字内容
- 优化方案:可在 onChanged 中增加输入过滤,仅保留数字字符
onChanged: (value) { // 过滤非数字字符 final filtered = value.replaceAll(RegExp(r'[^0-9]'), ''); userAnswer = filtered.trim(); }, - 空值处理:需配合按钮禁用逻辑,避免空输入或全空格输入触发确认
它的意义不是“强制输入一定是数字”,而是
- 让输入法优先弹出数字键盘
这会显著减少用户输入成本。
但是它不等于验证。
例如用户仍然可能输入
- 空字符串
- 带空格的内容
- 一些输入法特殊字符
你当前的代码采用的是“最小实现”:
- 不做复杂校验
- 用字符串直接比对答案
在训练 Demo 场景下是可以接受的。
如果后续要更严谨,你可以做两步增强:
value.trim()去掉空白- 只保留数字字符或转 int 再比较
12. 为什么答案用 String 存,而不是 int
你现在的核心代码:
final List<String> answers = ['36', '13', '8', '42', '729'];
12.1 字符串存储的优势(
- 扩展兼容性:
- 支持非数字答案:未来若扩展题目类型(如字母序列、表达式),无需修改数据类型
- 示例:若新增“字母序列:A,B,C,D,?”,答案可直接存为“E”,无需类型转换
- 展示便捷性:直接通过字符串插值展示,无需调用 toString() 方法,代码更简洁
- 对比 int 存储:若存为 int,展示时需写
answers[currentSequence].toString()
- 对比 int 存储:若存为 int,展示时需写
- 格式兼容性:支持带格式的答案(如“100,000”),int 类型无法存储带分隔符的数字
- 空安全:字符串可直接存储空值(“”),int 类型不能为空,灵活性更高
12.2 潜在问题与解决方案
- 字符串比较的严格性:
- 问题:用户输入“036”(前导零)、“36 ”(尾部空格)会被判定为错误
- 解决方案:比较前统一处理(如转 int 比较、去除前导零、trim 空格)
// 优化后的答案比较逻辑 bool isAnswerCorrect() { if (userAnswer == null) return false; // 去除空格并转数字比较 final userNum = int.tryParse(userAnswer!.trim()); final correctNum = int.tryParse(answers[currentSequence]); return userNum == correctNum; } - 类型转换成本:需手动转换为数字进行计算(如验证答案范围),但在当前场景下无此需求
当然它也带来一个缺点:
- 比较时是字符串比较
这会导致
- 用户输入
036会被认为错误 - 用户输入
36(尾部空格)会被认为错误
因此是否要 trim,取决于你希望训练规则多严格。
13. 数列展示的布局:为什么用一排“圆形数字格”
你核心代码:
width: 40.w,
height: 40.w,
borderRadius: BorderRadius.circular(20.r)
13.1 视觉设计价值
- 题目卡片化:圆形数字格+浅蓝背景,营造“题目卡片”的视觉效果,区别于普通文本展示
- 焦点集中:数字在圆形容器中居中显示,用户视线更易聚焦,提升阅读效率
- 训练场景适配:相比纯文本,可视化的数字格更符合“逆向思维训练”的场景定位,增强互动感
- 视觉层次:已知项(蓝色)与未知项(灰色+边框)形成视觉对比,突出解题目标
13.2 响应式设计
- 比例一致性:宽高均使用 .w 单位,保证在不同屏幕尺寸下圆形不变形(如手机、平板)
- 间距均匀:Row 设置 spaceEvenly,数字格之间的间距自动适配屏幕宽度
- 尺寸适配:40.w 的尺寸在小屏手机上不会过小,在大屏平板上不会过大,兼顾可读性和美观度
13.3 未知项设计细节
- 色彩区分:灰色背景与已知项的蓝色形成对比,明确提示“待求解”
- 边框关联:蓝色边框与已知项的背景色呼应,保持视觉统一性
- 符号提示:“?”符号直观表达未知项,无需额外文字说明
未知项 ? 用 Colors.grey 区分,并加了蓝色描边
border: Border.all(color: Colors.blue, width: 2)
这让用户知道“这里缺一项”。
14. showResult 的定位:它是流程控制,不是数据
你用 showResult 控制解析区出现的核心逻辑:
if (showResult) ...[
// 解析区UI
]
14.1 流程控制价值
- 交互闭环:实现“输入→确认→展示解析”的三步式交互,符合用户的答题习惯
- 第一步:输入答案(用户思考)
- 第二步:确认提交(用户决策)
- 第三步:查看解析(用户学习)
- 注意力引导:确认前隐藏解析,避免用户直接查看答案,保证训练效果
- 状态轻量化:仅用一个布尔值控制解析区显示,无冗余状态,易于维护
- UI 简洁性:解析区直接嵌入页面,无需弹窗,保持交互的连续性
14.2 状态管理设计
- 触发时机:仅在“确认答案”和“切题”时修改 showResult,状态变更可控
- 重置逻辑:切题时同步重置 showResult,保证新题页面无残留解析
- 无副作用:showResult 仅控制 UI 展示,不影响其他业务逻辑,状态纯净
它的价值是把流程变成两步:
- 输入
- 确认
在训练类应用里,确认动作非常关键。
它让用户把“猜测”变成“提交”。
这样解析出现时,用户的注意力会更集中。
你没有引入额外弹窗,而是把解析放在页面里。
这会让交互更连续。
15. 小结:数列推理实现的核心点
- 题目数据驱动:
sequences[currentSequence] - 输入与结果分离:输入时不 rebuild,确认时才展示
- 强引导:按钮禁用 + 颜色反馈
- 切题必重置:避免答案与解析串题
到这里,文章已经覆盖了数列推理的实现结构与关键细节。
下一步你可以按同样的模式扩展更多题目类型:
- 增加更多序列
- 增加更多规律说明
- 或把平行列表合并成一个模型对象
16. 常见踩坑点:TextField 的 onChanged 与 setState
新手常在 TextField 的 onChanged 里写 setState,导致频繁 rebuild。
你的写法是正确的示范:只在需要 UI 变化时才触发 setState。
输入过程不需要实时 UI 更新,直接赋值即可。
16.1 踩坑示例
- 错误写法:
onChanged: (value) { userAnswer = value; setState(() {}); // 不必要的重建 }, - 问题后果:
- 输入每个字符都触发页面重建,性能下降
- 可能导致输入框焦点丢失,影响交互体验
- 正确写法:
- 仅暂存输入值,确认时再触发 setState
- 新增 trim() 清洗输入,提升答案判断准确性
17. 常见踩坑点:字符串比较忽略空格
用户输入 36 (带空格)会被认为错误。
如果希望更宽松,可以用 userAnswer?.trim()。
但训练场景下,严格比较有助于培养精确输入习惯。
17.1 踩坑场景
- 场景1:用户输入时误触空格,输入“36 ”,字符串比较时“36 ” != “36”,判定为错误
- 场景2:用户复制粘贴答案时带入空格,导致答案错误
- 场景3:输入全角空格( ),trim() 无法去除,需额外处理
17.2 优化方案
- 基础优化:onChanged 中使用 trim() 去除首尾空格
- 进阶优化:过滤所有空格(包括全角)
userAnswer = value.replaceAll(RegExp(r'\s'), ''); - 严格训练:保留原始字符串比较,培养用户精确输入的习惯
18. 常见踩坑点:答案用 int 存导致展示麻烦
如果把答案存为 int,展示时需要 toString()。
你直接存为 String,输出时直接插值,代码更简洁。
这是“展示友好”的设计。
18.1 踩坑示例
- 错误写法:
final List<int> answers = [36, 13, 8, 42]; // 展示时需转换 Text('正确答案:${answers[currentSequence].toString()}') - 问题后果:
- 代码冗余:每次展示都需调用 toString()
- 空安全风险:int 类型不能为空,扩展非数字答案时需修改类型
- 正确写法:
- 直接存储字符串,展示时无需转换
- 兼容非数字答案,扩展性更好
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐
所有评论(0)