add flutter quiz project code

This commit is contained in:
吴璨
2026-08-04 18:07:40 +08:00
parent 14a28cccc6
commit 22b5d75d2d
22 changed files with 6416 additions and 0 deletions
+216
View File
@@ -0,0 +1,216 @@
{
"name": "Dart 开发实战",
"lvNames": ["基础语法", "控制流与函数", "集合", "面向对象", "空安全与异步", "Dart 高级特性"],
"questions": [
{
"lv": 1,
"q": "开发注册页时,用户输入的年龄是字符串,需要转成 <code>int</code> 保存。已知 <code>String ageText = \"18\";</code>,正确的转换写法是?",
"opts": ["<code>int(ageText)</code>", "<code>toInt(ageText)</code>", "<code>int.parse(ageText)</code>", "<code>ageText.parseInt()</code>"],
"ans": 2,
"exp": "<code>int.parse()</code> 把字符串解析成整数,解析失败会抛 FormatException,更稳妥的是 <code>int.tryParse()</code>(失败返回 null)。注意 Dart 没有 <code>\"18\".parseInt()</code> 这种写法,<code>int(...)</code> 也不是转换函数。"
},
{
"lv": 1,
"q": "项目里要定义一个\"永远不允许修改\"的 API 地址常量,以下哪种声明最合适?",
"opts": ["<code>final String api = \"https://api.example.com\";</code>", "<code>const String api = \"https://api.example.com\";</code>", "<code>String api = \"https://api.example.com\";</code>", "<code>var api = \"https://api.example.com\";</code>"],
"ans": 1,
"exp": "<b>const</b> 是编译期常量:值在编译时就确定,且不可变,编译器可直接内联替换,还能配合 const 构造函数做实例复用。<b>final</b> 是运行期赋值一次后不可变。给字面量常量用 const 是更优选择。"
},
{
"lv": 1,
"q": "你在重构代码时写下:<code>var x = 10; x = \"hello\";</code> 会发生什么?",
"opts": ["正常运行,x 变成字符串", "编译报错:x 已被推断为 int 类型", "运行时报错", "x 自动变为 null"],
"ans": 1,
"exp": "<b>var</b> 声明时由初始值自动推断类型,推断后类型就<b>固定</b>了:x 是 int,再赋 String 在编译期直接报错。想存任意类型,应显式声明 <code>dynamic</code> 或 <code>Object?</code>。"
},
{
"lv": 1,
"q": "给变量起名时,以下哪个<b>不是</b>合法的 Dart 标识符?",
"opts": ["<code>_score</code>", "<code>2x</code>", "<code>中文名</code>", "<code>name_2</code>"],
"ans": 1,
"exp": "Dart 标识符<b>不能以数字开头</b>。<code>_score</code> 合法(下划线开头表示库内私有,是 Dart 的约定俗成);Dart 支持 Unicode 标识符,中文变量名同样合法。"
},
{
"lv": 1,
"q": "结算功能计算人均消费:<code>int total = 10; int people = 4; double avg = total / people;</code> 打印 <code>avg</code> 得到?",
"opts": ["2", "2.5", "2.0", "报错"],
"ans": 1,
"exp": "Dart 中 <b>int 除以 int 的结果一定是 double</b>10 / 4 = 2.5。想得到整除结果用 <code>~/</code><code>10 ~/ 4 == 2</code>,取余用 <code>%</code>。"
},
{
"lv": 2,
"q": "写优惠逻辑:订单数 &gt; 10 打 9 折,&gt; 50 打 8 折。对 <code>orders = 60</code>,以下代码会得到什么折扣?<br><code>double d; if (orders > 10) { d = 0.9; } else if (orders > 50) { d = 0.8; }</code>",
"opts": ["0.8", "0.9", "1.0", "0.7"],
"ans": 1,
"exp": "if-else 链<b>从上到下匹配,命中即止</b>60 &gt; 10 先成立就直接取 0.9,后面的 else if 不再执行。正确写法应把范围大的条件放前面(先判断 &gt; 50)。判断顺序是 if-else 链最经典的坑。"
},
{
"lv": 2,
"q": "接口返回 <code>Map&lt;String, dynamic&gt; data</code>,想遍历所有 key 打印出来,以下哪种写法正确?",
"opts": ["<code>for (var k in data) print(k);</code>", "<code>for (var k in data.keys) print(k);</code>", "<code>data.keys.forEach(print);</code>", "B 和 C 都可以"],
"ans": 3,
"exp": "Map 本身<b>不能</b>直接 for-in 遍历,要先取 <code>keys</code> / <code>values</code> / <code>entries</code>。B 是标准写法;C 中 <code>forEach(print)</code> 利用了函数引用,同样正确。两种都行,按团队规范选择。"
},
{
"lv": 2,
"q": "写一个返回用户名的函数:<code>String getUserName() { }</code>,忘了写 <code>return</code>,会怎样?",
"opts": ["返回 null", "编译报错", "运行时崩溃", "自动返回空字符串"],
"ans": 1,
"exp": "空安全下,声明了非空返回类型 <code>String</code> 的函数<b>必须有 return</b>,漏写直接编译报错,把隐患消灭在编译期。如果确实允许无返回值,应把返回类型声明为 <code>String?</code>。"
},
{
"lv": 2,
"q": "问候函数:<code>void greet(String name, {String greeting = \"你好\"})</code>,调用 <code>greet(\"小明\", \"早上好\")</code> 会发生什么?",
"opts": ["正常执行,greeting 为 \"早上好\"", "编译报错:命名参数不能按位置传参", "greeting 变为 null", "编译警告但能运行"],
"ans": 1,
"exp": "<code>{}</code> 包裹的是<b>命名参数</b>,必须写成 <code>greet(\"小明\", greeting: \"早上好\")</code><code>[]</code> 才是可选位置参数。命名参数让调用意图清晰,是团队协作中常用的做法。"
},
{
"lv": 2,
"q": "写\"重试发送验证码\"逻辑:要求<b>至少发送一次</b>,失败则继续重试。哪种循环语义最贴切?",
"opts": ["<code>while</code>", "<code>do-while</code>", "<code>for</code>", "<code>repeat</code>"],
"ans": 1,
"exp": "<b>do-while</b> 先执行循环体再判断条件,保证至少执行一次,正是\"先发一次、不行再重试\"的语义。while 先判断后执行,可能一次都不执行。"
},
{
"lv": 3,
"q": "实现\"删除待办\"功能,<code>list.remove(3)</code> 和 <code>list.removeAt(3)</code> 的区别是?",
"opts": ["remove 按值删除,removeAt 按下标删除", "两者完全相同", "remove 删下标,removeAt 按值删除", "remove 会删除所有等于 3 的元素"],
"ans": 0,
"exp": "<code>remove(value)</code> 删除<b>第一个</b>值相等的元素并返回是否成功;<code>removeAt(index)</code> 按下标删除并返回被删的元素,越界抛 RangeError。写代码前想清楚你要删的是\"值\"还是\"位置\"。"
},
{
"lv": 3,
"q": "解析接口数据:用 <code>data[\"name\"]</code> 取一个<b>不存在的 key</b> 时,返回什么?",
"opts": ["抛异常", "null", "空字符串", "抛出 NoSuchMethodError"],
"ans": 1,
"exp": "Map 用 <code>[]</code> 取值时,key 不存在<b>返回 null</b> 而不是抛异常。所以 <code>data[\"name\"]</code> 可能为 null,取值后判空再使用是常规操作(配合 <code>??</code> 兜底)。"
},
{
"lv": 3,
"q": "配置列表按条件插入元素:<code>var l = [1, 2, if (isVip) 3];</code> 当 <code>isVip</code> 为 false 时 l 是?",
"opts": ["<code>[1, 2]</code>", "<code>[1, 2, 3]</code>", "<code>[1, 2, false]</code>", "编译错误"],
"ans": 0,
"exp": "这是 <b>collection-if</b> 语法:条件不成立时该元素直接不生成。Flutter 里常用于条件渲染组件列表,配合 <code>...</code> 展开和 collection-for 一起使用。"
},
{
"lv": 3,
"q": "两个用户各自关注了一批标签,要找出<b>共同关注</b>的标签,用哪种写法?",
"opts": ["<code>a & b</code>", "<code>a.intersect(b)</code>", "<code>a.toSet().intersection(b.toSet())</code>", "<code>a.union(b)</code>"],
"ans": 2,
"exp": "List 没有交集方法,要先转 Set。<code>intersection()</code> 返回两个集合的共同元素;<code>union()</code> 是并集。注意方法名是 <code>intersection()</code>,不是其它语言的 <code>intersect()</code>。"
},
{
"lv": 3,
"q": "计算购物车总价:<code>List&lt;double&gt; prices = [];</code> 空列表调用 <code>prices.reduce((a, b) => a + b)</code> 会?",
"opts": ["返回 0", "抛出 StateError 异常", "返回 null", "返回空列表"],
"ans": 1,
"exp": "<code>reduce</code> 要求列表<b>至少有一个元素</b>,空列表直接抛 \"Bad state: No element\"。更稳的写法是 <code>fold(0, (a, b) => a + b)</code>:带初始值,空列表返回 0。"
},
{
"lv": 4,
"q": "写日期工具类,希望 <code>DateUtil.parse(\"2024-01-01\")</code> 在字符串不合法时返回 null 而不是抛异常,用哪种构造函数?",
"opts": ["factory 构造函数", "普通构造函数", "命名构造函数", "const 构造函数"],
"ans": 0,
"exp": "<b>factory</b> 构造函数可以不返回新实例——可以返回缓存实例、子类实例,甚至返回 null。它把\"按需创建/校验\"的逻辑封装在构造入口,调用方写法统一且安全。"
},
{
"lv": 4,
"q": "日志打印、权限检查这类能力要被多个<b>互不相关</b>的类复用,又不想强制继承关系,Dart 的推荐做法是?",
"opts": ["<code>extends</code> 继承", "<code>implements</code> 实现", "<code>with</code> 混入 Mixin", "<code>abstract</code> 抽象类"],
"ans": 2,
"exp": "<b>Mixin</b><code>with</code>)把可复用逻辑抽出来按需混入,避免\"为了复用而强行继承\"的长继承链。Flutter 源码大量使用,如 <code>class _X with RouteAware</code>。"
},
{
"lv": 4,
"q": "类内部要声明一个\"仅供本文件使用\"的私有字段,正确写法是?",
"opts": ["<code>private String _token;</code>", "<code>String _token;</code>", "<code>-String token;</code>", "<code>#token</code>"],
"ans": 1,
"exp": "Dart 没有 <code>private</code> 关键字,以<b>下划线开头</b>表示库内私有:<code>_token</code>。注意这是\"库\"级别私有(同文件可见),不是类级别。"
},
{
"lv": 4,
"q": "用户类的 <code>name</code> 字段希望<b>赋值时自动去掉首尾空格</b>,最佳做法是?",
"opts": ["声明 getter/setter,在 setter 里 trim", "直接写公共字段,调用方自己 trim", "用 final 禁止修改", "用 static 修饰"],
"ans": 0,
"exp": "<b>setter</b> 在赋值入口统一处理,所有调用方(包括构造函数)都自动享受规范化,不用每个地方都记得 trim。写法:<code>String get name</code> 与 <code>set name(String v)</code>。"
},
{
"lv": 4,
"q": "<code>class Dog implements Animal</code> 与 <code>extends</code> 继承的关键区别是?",
"opts": ["implements 要求实现 Animal 的<b>全部</b>成员(含字段),extends 可复用已有实现", "两者完全一样", "implements 只能用于抽象类", "extends 要求实现全部成员"],
"ans": 0,
"exp": "<code>implements</code> 把 Animal 当<b>接口</b>:所有成员都要自己重写,即使父类已有实现;<code>extends</code> 继承实现,只覆写需要改的部分。implements 用于\"规定能力\"extends 用于\"复用实现\"。"
},
{
"lv": 5,
"q": "用户对象可能为 null<code>User? u = await fetchUser();</code>,以下哪种使用方式更安全?",
"opts": ["<code>if (u != null) { print(u.name); }</code> 判空后直接用 u", "<code>if (u != null) { print(u!.name); }</code> 必须加 !", "<code>print(u?.name ?? \"\");</code>", "A 和 C 都可以"],
"ans": 3,
"exp": "<b>A</b>:局部变量判空后编译器会<b>自动提升</b>为非空类型,直接使用即可;<b>C</b><code>?.</code> 安全调用 + <code>??</code> 兜底,一行搞定。只有被闭包捕获的变量才需要 <code>!</code>。"
},
{
"lv": 5,
"q": "详情页要同时请求\"用户信息\"和\"订单列表\"两个接口,都拿到再渲染,用哪个方法?",
"opts": ["<code>Future.wait([fetchUser(), fetchOrders()])</code>", "串行写两个 await", "<code>Future.any</code>", "<code>Future.delayed</code> 等待"],
"ans": 0,
"exp": "<b>Future.wait</b> 并发启动多个 Future,全部完成后统一返回结果列表,总耗时约等于最慢的那个;串行 await 则是两个请求耗时相加。<code>Future.any</code> 只等第一个完成的。"
},
{
"lv": 5,
"q": "某个字段要等登录成功后才能赋值,但又想声明成非空类型,正确声明方式是?",
"opts": ["<code>late String token;</code>", "<code>String? token = \"\";</code>", "<code>dynamic token;</code>", "<code>final token = null;</code>"],
"ans": 0,
"exp": "<b>late</b> 表示\"推迟初始化,首次访问时才赋值\",适合依赖异步结果的字段或计算代价高的属性。注意:late 变量在赋值前被访问会抛 LateInitializationError。"
},
{
"lv": 5,
"q": "把解析结果转成目标类型:<code>var age = data[\"age\"] as int;</code>,当 age 实际是 String 时会发生?",
"opts": ["返回 null", "抛出 TypeError(类型转换失败)", "静默忽略", "自动转成 int"],
"ans": 1,
"exp": "<code>as</code> 是<b>强制类型转换</b>,类型不符立即抛异常。安全做法:先用 <code>is</code> 判断类型,或先统一规范数据(如 num 一律 <code>.toInt()</code>)。"
},
{
"lv": 5,
"q": "对可能为 null 的 <code>String? s</code> 使用 <code>s!</code>,若 s 确实为 null 会?",
"opts": ["返回 null", "抛出 TypeError(空检查失败)", "静默继续", "编译错误"],
"ans": 1,
"exp": "<code>!</code> 是<b>强制解包</b>运算符,向编译器承诺\"这里不可能为 null\",承诺落空时运行时抛 TypeError。能用判空提升或 <code>??</code> 的地方尽量别用 <code>!</code>,减少运行时崩溃点。"
},
{
"lv": 6,
"q": "Flutter 中常见 <code>ButtonStyle()..backgroundColor = ... ..shape = ...</code> 写法,<code>..</code> 级联操作符的核心行为是?",
"opts": ["对同一对象连续操作且返回对象本身", "链式调用并返回最后一次结果", "只能用于 List", "与 <code>...</code> 展开等价"],
"ans": 0,
"exp": "<b>级联操作符</b> <code>..</code> 对同一对象连续执行多个操作,<b>不改变返回值</b>(始终是原对象),适合集中配置对象。<code>.</code> 返回的是方法调用的返回值,两者行为不同。"
},
{
"lv": 6,
"q": "Dart 3 中,函数想直接返回\"用户名 + 年龄\"这组小数据,又不想专门建一个类,推荐用?",
"opts": ["<code>Map&lt;String, dynamic&gt;</code>", "Record 记录类型:<code>(String name, int age)</code>", "<code>List&lt;dynamic&gt;</code>", "用全局变量"],
"ans": 1,
"exp": "<b>Record</b> 是 Dart 3 的轻量聚合类型:<code>var r = (\"张三\", 18);</code> 或命名字段 <code>(name: \"张三\", age: 18)</code>,用 <code>r.$1</code> / <code>r.name</code> 取值,还能配合模式匹配解构,比 Map 更类型安全。"
},
{
"lv": 6,
"q": "Dart 3 的 switch 可以直接当表达式赋值:<code>var s = switch (status) { \"ok\" =&gt; \"成功\", _ =&gt; \"未知\" };</code> 这种写法?",
"opts": ["合法:switch 表达式返回一个值", "非法:switch 不能作表达式", "必须每个分支加 break", "只能匹配字符串"],
"ans": 0,
"exp": "<b>switch 表达式</b>用 <code>=&gt;</code> 代替冒号,整体作为值赋给变量,配合模式匹配可以解构、判断类型,比旧式 switch 简洁,也天然避免了漏 break 的坑。"
},
{
"lv": 6,
"q": "想给系统类 <code>String</code> 加一个 <code>isEmail()</code> 方法,但 String 不能改源码,用?",
"opts": ["<b>extension</b> 扩展:<code>extension EmailCheck on String { bool isEmail() {...} }</code>", "继承 String", "用 mixin 混入", "写静态工具类"],
"ans": 0,
"exp": "<b>extension</b> 可以不修改原类给已有类型添加方法,之后 <code>\"a@b.com\".isEmail()</code> 直接调用。注意扩展方法是静态分发(编译期决定),不参与运行时多态。"
},
{
"lv": 6,
"q": "泛型容器 <code>class Box&lt;T extends num&gt; {}</code> 中 <code>extends num</code> 的含义是?",
"opts": ["T 只能是 int、double 等数值类型", "T 可以是任意类型", "T 必须可空", "T 必须是集合类型"],
"ans": 0,
"exp": "<b>泛型约束</b>限定 T 必须是 num 及其子类(int/double),这样 Box 内部就能安全调用数值运算。不加约束时 T 等价于 <code>Object?</code>。"
}
]
}
+216
View File
@@ -0,0 +1,216 @@
{
"name": "Flutter 开发实战",
"lvNames": ["Widget 基础", "布局与组件", "状态管理", "路由与导航", "数据与网络", "实战综合"],
"questions": [
{
"lv": 1,
"q": "新建 Flutter 项目后,把应用真正跑起来的入口代码是?",
"opts": ["<code>void main() =&gt; runApp(MyApp());</code>", "<code>void main() { MyApp(); }</code>", "<code>runApp() { MyApp }</code>", "<code>main() { MaterialApp(); }</code>"],
"ans": 0,
"exp": "Dart 程序入口是 <code>main()</code>Flutter 通过 <code>runApp(Widget)</code> 把根组件挂载到引擎上。注意 <code>MyApp()</code> 是创建 Widget 实例,不能少了括号。"
},
{
"lv": 1,
"q": "实现一个\"3 秒倒计时\"按钮,每秒刷新显示剩余秒数,应使用?",
"opts": ["StatelessWidget", "StatefulWidget", "Container", "Scaffold"],
"ans": 1,
"exp": "有<b>动态变化的数据</b>(倒计时数字)就需要 StatefulWidget:数据保存在 State 里,变化时调 <code>setState()</code> 刷新。没有可变状态时应优先用 StatelessWidget。"
},
{
"lv": 1,
"q": "界面上要显示一行固定文案\"登录成功\",最合适的组件是?",
"opts": ["Text", "Label", "Paragraph", "RichText"],
"ans": 0,
"exp": "<code>Text</code> 是最常用的文本组件,可通过 style 配置字号、颜色、字重等。RichText 用于一段文字中混排多种样式的富文本场景。"
},
{
"lv": 1,
"q": "点击\"确认\"按钮后分数要 +1 并立即显示到界面,正确流程是?",
"opts": ["修改 State 字段后调用 <code>setState()</code>", "直接修改字段就行", "重启整个 App", "修改 Widget 的构造参数"],
"ans": 0,
"exp": "State 里的字段只是普通数据,必须调用 <code>setState()</code> 通知框架\"数据变了\",框架才会重新执行 build 刷新 UI。注意 setState 只重建<b>当前 State 的子树</b>,不是整棵树。"
},
{
"lv": 1,
"q": "要展示 10000 条商品记录,性能最优的做法是?",
"opts": ["<code>ListView.builder</code>(按需构建可见项)", "Column 里循环放 10000 个组件", "SingleChildScrollView 包 Column", "GridView.count 全部构建"],
"ans": 0,
"exp": "<code>ListView.builder</code> 只构建<b>当前可见</b>的列表项(懒加载),滚动时回收复用;Column + for 会一次性构建全部组件,万条数据直接卡死。数据量大优先 builder 系列。"
},
{
"lv": 2,
"q": "登录页要垂直排列\"输入框、输入框、登录按钮\",用哪个布局组件?",
"opts": ["Column", "Row", "Stack", "Wrap"],
"ans": 0,
"exp": "<code>Column</code> 垂直方向排列子组件,<code>Row</code> 水平方向,<code>Stack</code> 叠加并支持定位,<code>Wrap</code> 在空间不足时自动换行。"
},
{
"lv": 2,
"q": "Row 里想让一个按钮<b>占满剩余宽度</b>(剩余空间全给它),按钮外层套?",
"opts": ["<code>Expanded</code>", "<code>Padding</code>", "<code>Center</code>", "<code>Spacer</code>"],
"ans": 0,
"exp": "<code>Expanded</code> 让子组件占据主轴的剩余空间(Row 的主轴是水平方向)。多个 Expanded 可通过 flex 参数按比例分配空间。"
},
{
"lv": 2,
"q": "Column 里有几个按钮,想让它们在<b>水平方向居中</b>,应该设置?",
"opts": ["<code>crossAxisAlignment: CrossAxisAlignment.center</code>", "<code>mainAxisAlignment: MainAxisAlignment.center</code>", "<code>alignment: Alignment.center</code>", "<code>textAlign: TextAlign.center</code>"],
"ans": 0,
"exp": "Column 的<b>主轴是垂直</b>方向、<b>交叉轴是水平</b>方向。水平居中=沿交叉轴居中=crossAxisAlignment.centermainAxisAlignment 控制的是垂直方向。主/交叉轴搞反是新手最常踩的坑。"
},
{
"lv": 2,
"q": "封面图要<b>铺满整个区域且不拉伸变形</b>(多出的部分裁掉),fit 参数应该用?",
"opts": ["<code>BoxFit.cover</code>", "<code>BoxFit.fill</code>", "<code>BoxFit.none</code>", "<code>BoxFit.scaleDown</code>"],
"ans": 0,
"exp": "<b>cover</b>:等比缩放填满、超出裁剪,常用于头像、封面;<b>fill</b> 会拉伸变形;<b>contain</b> 完整显示但可能留白;<b>none</b> 不缩放。"
},
{
"lv": 2,
"q": "商品名过长时希望<b>单行显示,超出部分显示省略号</b>Text 的配置是?",
"opts": ["<code>maxLines: 1, overflow: TextOverflow.ellipsis</code>", "只设 overflow: TextOverflow.clip", "只设 maxLines: 3", "<code>softWrap: false</code> 就够了"],
"ans": 0,
"exp": "<code>maxLines</code> 限制最大行数 + <code>overflow: TextOverflow.ellipsis</code> 指定超出的表现(省略号/裁剪/fade),两者配合才能出现\"…\"效果。"
},
{
"lv": 3,
"q": "输入框需要\"预填默认值 + 按钮点击后读取内容 + 随时清空\",推荐用?",
"opts": ["<code>TextEditingController</code>", "每次读 onChanged 参数", "GlobalKey 临时查找", "无法实现"],
"ans": 0,
"exp": "<code>TextEditingController</code> 是输入框的\"数据源\"<code>TextEditingController(text: \"预填\")</code> 初始化,<code>controller.text</code> 读取,<code>controller.clear()</code> 清空。用完后在 dispose 里释放。"
},
{
"lv": 3,
"q": "页面销毁时,取消监听、释放 Controller 的代码应该写在哪里?",
"opts": ["<code>dispose()</code>", "<code>initState()</code>", "<code>build()</code>", "<code>didChangeDependencies()</code>"],
"ans": 0,
"exp": "<code>dispose()</code> 是 State 被移除时的清理钩子:取消 Timer、Stream 订阅、释放 Controller。不清理会造成内存泄漏和 \"setState called after dispose\" 报错。"
},
{
"lv": 3,
"q": "登录状态、用户信息要被<b>多个页面共享</b>,又不想层层传构造参数,项目常用方案是?",
"opts": ["Provider / InheritedWidget", "定义全局变量直接读写", "每个页面重新请求接口", "把状态塞进路由参数"],
"ans": 0,
"exp": "Flutter 官方的底层方案是 <b>InheritedWidget</b><b>Provider</b> 是对它的优雅封装:共享数据放顶层,子组件 <code>context.watch()</code> 取用并自动刷新。全局变量没有通知机制,页面不会自动更新。"
},
{
"lv": 3,
"q": "商品列表页要支持<b>下拉刷新</b>,应该把 ListView 包进哪个组件?",
"opts": ["<code>RefreshIndicator</code>", "<code>GestureDetector</code>", "<code>AnimatedList</code>", "<code>Scrollbar</code>"],
"ans": 0,
"exp": "<code>RefreshIndicator</code> 包住可滚动组件,配合 <code>onRefresh</code> 返回的 Future 实现下拉刷新动画。这是 Material 规范自带的下拉刷新方案。"
},
{
"lv": 3,
"q": "App 切到后台、再回到前台时,需要暂停/恢复计时器,通过什么监听?",
"opts": ["<code>WidgetsBindingObserver</code>", "<code>RouteObserver</code>", "<code>NavigatorObserver</code>", "<code>MediaQuery</code>"],
"ans": 0,
"exp": "给 State 混入 <code>with WidgetsBindingObserver</code>,在 <code>didChangeAppLifecycleState()</code> 中监听 AppLifecycleStatepaused / resumed / inactive),即可感知前后台切换。"
},
{
"lv": 4,
"q": "列表页点击商品跳转详情页,标准写法是?",
"opts": ["<code>Navigator.push(context, MaterialPageRoute(builder: (_) =&gt; DetailPage(id: id)))</code>", "<code>Navigator.go(DetailPage(id: id))</code>", "<code>Route.open(DetailPage())</code>", "<code>DetailPage(id: id).show()</code>"],
"ans": 0,
"exp": "<code>Navigator.push</code> 把新路由压入导航栈,MaterialPageRoute 提供转场动画,返回上一页用 <code>Navigator.pop(context)</code>。"
},
{
"lv": 4,
"q": "详情页编辑完要把结果<b>回传给列表页</b>,详情页返回时应该写?",
"opts": ["<code>Navigator.pop(context, result)</code>", "<code>Navigator.back(result)</code>", "<code>Navigator.pushResult(result)</code>", "<code>context.popAndPush(result)</code>"],
"ans": 0,
"exp": "<code>pop(context, 返回值)</code> 的第二个参数就是回传的数据,列表页 <code>await push(...)</code> 的返回值即可收到。注意 pop 的参数是\"返回给上一页的数据\",不是新路由。"
},
{
"lv": 4,
"q": "跳转时需要传递商品 id、名称等参数,团队协作中<b>最推荐</b>的传参方式是?",
"opts": ["<code>DetailPage(id: 1, name: \"商品\")</code> 构造参数传递", "全局变量临时存", "static 静态字段存", "URL 字符串拼接"],
"ans": 0,
"exp": "构造参数显式、类型安全、可读性好——跳转处一眼看到传了什么。全局变量/静态字段是隐藏依赖,页面多起来后非常难维护。"
},
{
"lv": 4,
"q": "在 App 里注册命名路由(如 <code>/login</code>、<code>/home</code>),配置位置是?",
"opts": ["<code>MaterialApp(routes: {...})</code>", "<code>main()</code> 函数里", "<code>runApp()</code> 参数", "每个页面里各自声明"],
"ans": 0,
"exp": "命名路由集中配置在 <code>MaterialApp</code> 的 <code>routes</code> 表,之后用 <code>Navigator.pushNamed(context, \"/login\")</code> 跳转;需要传参时配合 <code>arguments</code> 参数。"
},
{
"lv": 4,
"q": "跳到选择页,用户选完返回后要根据<b>选择结果</b>继续处理,列表页怎么写?",
"opts": ["<code>final r = await Navigator.push(...);</code> 拿到返回值再处理", "无法等待结果", "用 setState 轮询", "跳转时把回调函数传过去"],
"ans": 0,
"exp": "<code>push</code> 返回 <code>Future&lt;T&gt;</code>await 后拿到 pop 回传的值,再执行后续逻辑(如刷新选中项)。这是\"页面间取结果\"的标准姿势。"
},
{
"lv": 5,
"q": "接口返回 JSON 字符串,执行 <code>var data = jsonDecode(text);</code> 后 data 的类型是?",
"opts": ["<code>dynamic</code>(实际可能是 Map 或 List", "<code>String</code>", "<code>Map&lt;String, dynamic&gt;</code>", "<code>Object</code>"],
"ans": 0,
"exp": "<code>jsonDecode</code> 的返回类型是 <code>dynamic</code>JSON 对象变 Map&lt;String, dynamic&gt;、数组变 List&lt;dynamic&gt;、数字/布尔原样返回。通常需要手动转换:<code>jsonDecode(text) as Map&lt;String, dynamic&gt;</code>。"
},
{
"lv": 5,
"q": "要把用户对象 <code>User(id: 1, name: \"张三\")</code> 转成 JSON 字符串发给服务器,正确流程是?",
"opts": ["给 User 写 <code>toJson()</code>,再 <code>jsonEncode(user.toJson())</code>", "<code>jsonEncode(user)</code> 直接序列化", "<code>user.toString()</code>", "<code>JSON.stringify(user)</code>"],
"ans": 0,
"exp": "Dart 默认不会自动序列化任意对象,惯例是模型类自己实现 <code>Map&lt;String, dynamic&gt; toJson()</code>,再用 <code>jsonEncode</code> 转字符串;反序列化则对应 <code>User.fromJson(Map)</code> 工厂构造。"
},
{
"lv": 5,
"q": "网络请求可能超时/断网,用户点\"登录\"时抛出了异常,正确的处理是?",
"opts": ["用 try-catch 包裹 await 请求,捕获异常并提示用户", "不做处理", "打印日志后忽略", "延迟 3 秒自动重试"],
"ans": 0,
"exp": "<code>try { await api.login(...) } catch (e) { 提示\"网络异常,请重试\" }</code> 是最基础的错误处理;生产环境还会区分 SocketException(断网)、TimeoutException(超时)等做分类提示。"
},
{
"lv": 5,
"q": "请求进行中需要展示\"加载中\"状态,最合适用?",
"opts": ["<code>CircularProgressIndicator</code>", "Text(\"加载中...\")", "Opacity 半透明遮罩", "AnimatedContainer 闪烁"],
"ans": 0,
"exp": "Material 标准的加载指示器是 <code>CircularProgressIndicator</code>(转圈)和 <code>LinearProgressIndicator</code>(条),配合 loading 状态变量做条件渲染。"
},
{
"lv": 5,
"q": "http 请求要设置 10 秒超时,正确写法是?",
"opts": ["<code>await http.get(url).timeout(Duration(seconds: 10))</code>", "<code>http.get(url, timeout: 10)</code>", "<code>Timer(Duration(seconds: 10), ...)</code>", "<code>Future.any</code> 自动超时"],
"ans": 0,
"exp": "Future 自带 <code>.timeout()</code>:超时抛 TimeoutException,可再接 <code>onTimeout</code> 参数兜底。http 包本身没有 timeout 参数,这是常见的误解。"
},
{
"lv": 6,
"q": "首页同时请求\"轮播图、公告、商品列表\"三个接口,全部完成再渲染页面,用?",
"opts": ["<code>Future.wait([...])</code> 并发等待", "三个 await 串行", "依次 catch 分别显示", "用 <code>Future.any</code>"],
"ans": 0,
"exp": "并发场景用 <code>Future.wait</code>,总耗时约等于最慢的请求而不是三个相加。生产上可配合记录单个请求失败做局部降级(某块失败不影响整页)。"
},
{
"lv": 6,
"q": "商品列表滑到底部要自动加载下一页,监听滚动的方案是?",
"opts": ["<code>ScrollController</code> 监听 pixels 接近 maxScrollExtent 时触发", "用 Timer 每 3 秒加载", "下拉刷新代替", "GestureDetector 监听手势"],
"ans": 0,
"exp": "给 ListView 挂 <code>ScrollController</code>,在 <code>addListener</code> 里判断 <code>position.pixels &gt;= position.maxScrollExtent - 200</code>(预留阈值)就加载下一页,配合 loading 状态防止重复触发。"
},
{
"lv": 6,
"q": "商品标题中要<b>红色高亮\"热卖\"二字</b>、其余部分灰色,用?",
"opts": ["<code>RichText</code> + <code>TextSpan</code> 组合不同样式", "拆成多个 Text 排一排", "用换行符拼接字符串", "TextStyle 做不到,只能整段同色"],
"ans": 0,
"exp": "<code>RichText</code> 的 <code>TextSpan</code> 支持树状嵌套:每个 span 可以有自己的 style,是\"一段文字多种样式\"的标准方案。"
},
{
"lv": 6,
"q": "界面出现<b>黄黑条纹</b>溢出警告(RenderFlex overflowed),说明?",
"opts": ["子组件超出了父组件给定的约束空间", "主题颜色配置错误", "内存泄漏", "字体加载失败"],
"ans": 0,
"exp": "Row/Column 空间不够时子组件溢出,debug 模式显示黄黑条纹并打印 \"RenderFlex overflowed by XX pixels\"。解决思路:Expanded/Flexible 让出空间、SingleChildScrollView 滚动、FittedBox 缩放。"
},
{
"lv": 6,
"q": "瀑布流商品图列表很卡、图片解码占内存高,优先优化手段是?",
"opts": ["<code>cacheWidth</code> / <code>ResizeImage</code> 限制解码尺寸", "给图片加圆角", "提高图片的清晰度", "减少列表项数量"],
"ans": 0,
"exp": "Image.network 默认按原图尺寸解码,一张 4000px 大图在手机上就是内存杀手。用 <code>cacheWidth: 300</code> 或 <code>ResizeImage</code> 让引擎按需解码成小尺寸位图,内存占用和滚动性能立竿见影。"
}
]
}
+31
View File
@@ -0,0 +1,31 @@
const fs = require('fs');
const vm = require('vm');
const html = fs.readFileSync('index.html', 'utf8');
const start = html.indexOf('const questions');
const arrStart = html.indexOf('[', start);
let depth = 0, inStr = false, esc = false, end = -1;
for (let i = arrStart; i < html.length; i++) {
const c = html[i];
if (inStr) {
if (esc) { esc = false; continue; }
if (c === '\\') { esc = true; continue; }
if (c === "'") inStr = false;
continue;
}
if (c === "'") { inStr = true; continue; }
if (c === '[') depth++;
else if (c === ']') { depth--; if (depth === 0) { end = i; break; } }
}
let src = html.slice(arrStart, end + 1);
src = src.replace(/\/\/\s*=====[^']*?=====\s*/g, ' ');
const questions = vm.runInNewContext(src);
const withIds = questions.map((q, i) => ({ id: i + 1, ...q }));
const data = {
sets: [{ id: 1, name: 'Dart & Flutter 学习测验', questions: withIds }],
nextSetId: 2,
nextQId: withIds.length + 1
};
fs.writeFileSync('questions.json', JSON.stringify(data, null, 2), 'utf8');
console.log('已提取', withIds.length, '道题 -> questions.json');
+34
View File
@@ -0,0 +1,34 @@
const fs = require('fs');
const path = require('path');
(async () => {
const { escapeSmartHtml, decodeEntities } = await import('../web/src/htmlUtil.mjs');
const file = path.join(__dirname, '..', 'questions.json');
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
let fixed = 0;
for (const set of data.sets) {
for (const q of set.questions) {
const nq = escapeSmartHtml(decodeEntities(q.q));
const nx = escapeSmartHtml(decodeEntities(q.exp || ''));
if (nq !== q.q) { q.q = nq; fixed++; }
if (nx !== q.exp) { q.exp = nx; fixed++; }
const no = q.opts.map(o => escapeSmartHtml(decodeEntities(o)));
if (JSON.stringify(no) !== JSON.stringify(q.opts)) { q.opts = no; fixed++; }
}
}
fs.writeFileSync(file, JSON.stringify(data, null, 2), 'utf8');
console.log('清洗完成,修正字段数:', fixed);
console.log('幂等检查(再跑一遍应无变化)...');
let again = 0;
for (const set of data.sets) {
for (const q of set.questions) {
if (escapeSmartHtml(decodeEntities(q.q)) !== q.q) again++;
if (escapeSmartHtml(decodeEntities(q.exp || '')) !== q.exp) again++;
if (JSON.stringify(q.opts.map(o => escapeSmartHtml(decodeEntities(o)))) !== JSON.stringify(q.opts)) again++;
}
}
console.log(again === 0 ? '幂等 ✓' : `仍会变化: ${again} 处 ✗`);
})();
+38
View File
@@ -0,0 +1,38 @@
const fs = require('fs');
const path = require('path');
const BASE = 'http://localhost:3030';
const FILES = ['set-dart-dev.json', 'set-flutter-dev.json'];
async function main() {
const loginRes = await fetch(BASE + '/api/admin/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: 'admin', password: 'admin123' })
});
const login = await loginRes.json();
if (!loginRes.ok) throw new Error('登录失败: ' + login.error);
for (const file of FILES) {
const data = JSON.parse(fs.readFileSync(path.join(__dirname, 'data', file), 'utf8'));
const res = await fetch(BASE + '/api/admin/import', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + login.token
},
body: JSON.stringify(data)
});
const result = await res.json();
if (!res.ok) {
console.log(`${data.name} 导入失败: ${result.error}`);
} else {
console.log(`${data.name}: 导入 ${result.imported} 题, 套题 id=${result.set.id}`);
}
}
}
main().catch(e => {
console.error(e.message);
process.exit(1);
});
+48
View File
@@ -0,0 +1,48 @@
const d = require('../questions.json');
let total = 0, bad = [];
for (const set of d.sets) {
for (const q of set.questions) {
total++;
if (!q.q || !Array.isArray(q.opts) || q.opts.length < 2) { bad.push(`[${set.name} #${q.id}] 选项不足`); continue; }
if (!Number.isInteger(q.ans) || q.ans < 0 || q.ans >= q.opts.length) { bad.push(`[${set.name} #${q.id}] ans 越界`); continue; }
if (!q.exp) { bad.push(`[${set.name} #${q.id}] 缺解析`); continue; }
if (!Number.isInteger(q.lv) || q.lv < 1 || q.lv > 6) { bad.push(`[${set.name} #${q.id}] lv 非法`); continue; }
const dup = q.opts.findIndex((o, i) => q.opts.indexOf(o) !== i);
if (dup !== -1) { bad.push(`[${set.name} #${q.id}] 选项重复: "${q.opts[dup]}"`); }
}
}
console.log('总题数:', total);
if (bad.length) { console.log('问题:'); bad.forEach(b => console.log(' -', b)); }
else console.log('结构校验全部通过');
const checks = [
['Dart 开发实战', 'var x = 10', 1],
['Dart 开发实战', 'orders = 60', 1],
['Dart 开发实战', 'remove(3)', 0],
['Dart 开发实战', '不存在的 key', 1],
['Dart 开发实战', 'if (isVip) 3', 0],
['Dart 开发实战', 'reduce((a, b)', 1],
['Dart 开发实战', 'factory 构造函数', 0],
['Dart 开发实战', 'mixin', 2],
['Dart 开发实战', 's!', 1],
['Dart 开发实战', 'late String token', 0],
['Flutter 开发实战', 'runApp(MyApp())', 0],
['Flutter 开发实战', 'crossAxisAlignment', 0],
['Flutter 开发实战', 'TextOverflow.ellipsis', 0],
['Flutter 开发实战', 'RefreshIndicator', 0],
['Flutter 开发实战', 'Navigator.pop(context, result)', 0],
['Flutter 开发实战', 'jsonDecode(text)', 0],
['Flutter 开发实战', '.timeout(Duration', 0],
['Flutter 开发实战', 'cacheWidth', 0],
['Flutter 开发实战', 'RichText', 0],
['Flutter 开发实战', 'dispose()', 0]
];
let fail = 0;
for (const [setName, keyword, expect] of checks) {
const set = d.sets.find(s => s.name === setName);
const q = set.questions.find(x => (x.q + x.opts.join('') + x.exp).includes(keyword));
if (!q) { console.log('✗ 未找到题:', setName, keyword); fail++; continue; }
const ok = q.ans === expect;
if (!ok) { console.log(`✗ [${setName}] ${keyword} → ans=${q.ans} 期望 ${expect},正确选项内容: "${q.opts[q.ans]}"`); fail++; }
}
console.log(fail ? `抽查 ${checks.length} 题,${fail} 题不符` : `抽查 ${checks.length} 道关键题答案全部正确`);