chore: add start.sh script
This commit is contained in:
+213
-56
@@ -1,215 +1,372 @@
|
||||
{
|
||||
"name": "Dart 开发实战",
|
||||
"lvNames": ["基础语法", "控制流与函数", "集合", "面向对象", "空安全与异步", "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,
|
||||
"opts": [
|
||||
"<code>ageText.parseInt()</code>",
|
||||
"<code>int.parse(ageText)</code>",
|
||||
"<code>int(ageText)</code>",
|
||||
"<code>toInt(ageText)</code>"
|
||||
],
|
||||
"ans": 1,
|
||||
"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,
|
||||
"opts": [
|
||||
"<code>var api = \"https://api.example.com\";</code>",
|
||||
"<code>final String api = \"https://api.example.com\";</code>",
|
||||
"<code>String api = \"https://api.example.com\";</code>",
|
||||
"<code>const String api = \"https://api.example.com\";</code>"
|
||||
],
|
||||
"ans": 3,
|
||||
"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,
|
||||
"opts": [
|
||||
"运行时报错",
|
||||
"正常运行,x 变成字符串",
|
||||
"编译报错:x 已被推断为 int 类型",
|
||||
"x 自动变为 null"
|
||||
],
|
||||
"ans": 2,
|
||||
"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>"],
|
||||
"opts": [
|
||||
"<code>name_2</code>",
|
||||
"<code>2x</code>",
|
||||
"<code>中文名</code>",
|
||||
"<code>_score</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,
|
||||
"opts": [
|
||||
"2.0",
|
||||
"2",
|
||||
"2.5",
|
||||
"报错"
|
||||
],
|
||||
"ans": 2,
|
||||
"exp": "Dart 中 <b>int 除以 int 的结果一定是 double</b>:10 / 4 = 2.5。想得到整除结果用 <code>~/</code>:<code>10 ~/ 4 == 2</code>,取余用 <code>%</code>。"
|
||||
},
|
||||
{
|
||||
"lv": 2,
|
||||
"q": "写优惠逻辑:订单数 > 10 打 9 折,> 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,
|
||||
"opts": [
|
||||
"0.7",
|
||||
"0.8",
|
||||
"1.0",
|
||||
"0.9"
|
||||
],
|
||||
"ans": 3,
|
||||
"exp": "if-else 链<b>从上到下匹配,命中即止</b>:60 > 10 先成立就直接取 0.9,后面的 else if 不再执行。正确写法应把范围大的条件放前面(先判断 > 50)。判断顺序是 if-else 链最经典的坑。"
|
||||
},
|
||||
{
|
||||
"lv": 2,
|
||||
"q": "接口返回 <code>Map<String, dynamic> 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,
|
||||
"opts": [
|
||||
"<code>data.keys.forEach(print);</code>",
|
||||
"B 和 C 都可以",
|
||||
"<code>for (var k in data) print(k);</code>",
|
||||
"<code>for (var k in data.keys) print(k);</code>"
|
||||
],
|
||||
"ans": 1,
|
||||
"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,
|
||||
"opts": [
|
||||
"运行时崩溃",
|
||||
"自动返回空字符串",
|
||||
"编译报错",
|
||||
"返回 null"
|
||||
],
|
||||
"ans": 2,
|
||||
"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,
|
||||
"opts": [
|
||||
"编译警告但能运行",
|
||||
"greeting 变为 null",
|
||||
"正常执行,greeting 为 \"早上好\"",
|
||||
"编译报错:命名参数不能按位置传参"
|
||||
],
|
||||
"ans": 3,
|
||||
"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,
|
||||
"opts": [
|
||||
"<code>for</code>",
|
||||
"<code>while</code>",
|
||||
"<code>repeat</code>",
|
||||
"<code>do-while</code>"
|
||||
],
|
||||
"ans": 3,
|
||||
"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 的元素"],
|
||||
"opts": [
|
||||
"remove 按值删除,removeAt 按下标删除",
|
||||
"remove 会删除所有等于 3 的元素",
|
||||
"两者完全相同",
|
||||
"remove 删下标,removeAt 按值删除"
|
||||
],
|
||||
"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,
|
||||
"opts": [
|
||||
"null",
|
||||
"空字符串",
|
||||
"抛异常",
|
||||
"抛出 NoSuchMethodError"
|
||||
],
|
||||
"ans": 0,
|
||||
"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,
|
||||
"opts": [
|
||||
"<code>[1, 2, 3]</code>",
|
||||
"编译错误",
|
||||
"<code>[1, 2, false]</code>",
|
||||
"<code>[1, 2]</code>"
|
||||
],
|
||||
"ans": 3,
|
||||
"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,
|
||||
"opts": [
|
||||
"<code>a.intersect(b)</code>",
|
||||
"<code>a.toSet().intersection(b.toSet())</code>",
|
||||
"<code>a.union(b)</code>",
|
||||
"<code>a & b</code>"
|
||||
],
|
||||
"ans": 1,
|
||||
"exp": "List 没有交集方法,要先转 Set。<code>intersection()</code> 返回两个集合的共同元素;<code>union()</code> 是并集。注意方法名是 <code>intersection()</code>,不是其它语言的 <code>intersect()</code>。"
|
||||
},
|
||||
{
|
||||
"lv": 3,
|
||||
"q": "计算购物车总价:<code>List<double> prices = [];</code> 空列表调用 <code>prices.reduce((a, b) => a + b)</code> 会?",
|
||||
"opts": ["返回 0", "抛出 StateError 异常", "返回 null", "返回空列表"],
|
||||
"opts": [
|
||||
"返回空列表",
|
||||
"抛出 StateError 异常",
|
||||
"返回 0",
|
||||
"返回 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,
|
||||
"opts": [
|
||||
"const 构造函数",
|
||||
"命名构造函数",
|
||||
"普通构造函数",
|
||||
"factory 构造函数"
|
||||
],
|
||||
"ans": 3,
|
||||
"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,
|
||||
"opts": [
|
||||
"<code>extends</code> 继承",
|
||||
"<code>with</code> 混入 Mixin",
|
||||
"<code>implements</code> 实现",
|
||||
"<code>abstract</code> 抽象类"
|
||||
],
|
||||
"ans": 1,
|
||||
"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,
|
||||
"opts": [
|
||||
"<code>-String token;</code>",
|
||||
"<code>#token</code>",
|
||||
"<code>private String _token;</code>",
|
||||
"<code>String _token;</code>"
|
||||
],
|
||||
"ans": 3,
|
||||
"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,
|
||||
"opts": [
|
||||
"用 static 修饰",
|
||||
"直接写公共字段,调用方自己 trim",
|
||||
"声明 getter/setter,在 setter 里 trim",
|
||||
"用 final 禁止修改"
|
||||
],
|
||||
"ans": 2,
|
||||
"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,
|
||||
"opts": [
|
||||
"extends 要求实现全部成员",
|
||||
"implements 只能用于抽象类",
|
||||
"implements 要求实现 Animal 的<b>全部</b>成员(含字段),extends 可复用已有实现",
|
||||
"两者完全一样"
|
||||
],
|
||||
"ans": 2,
|
||||
"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,
|
||||
"opts": [
|
||||
"<code>print(u?.name ?? \"\");</code>",
|
||||
"A 和 C 都可以",
|
||||
"<code>if (u != null) { print(u!.name); }</code> 必须加 !",
|
||||
"<code>if (u != null) { print(u.name); }</code> 判空后直接用 u"
|
||||
],
|
||||
"ans": 1,
|
||||
"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,
|
||||
"opts": [
|
||||
"串行写两个 await",
|
||||
"<code>Future.delayed</code> 等待",
|
||||
"<code>Future.wait([fetchUser(), fetchOrders()])</code>",
|
||||
"<code>Future.any</code>"
|
||||
],
|
||||
"ans": 2,
|
||||
"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>"],
|
||||
"opts": [
|
||||
"<code>late String token;</code>",
|
||||
"<code>final token = null;</code>",
|
||||
"<code>dynamic token;</code>",
|
||||
"<code>String? token = \"\";</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,
|
||||
"opts": [
|
||||
"返回 null",
|
||||
"自动转成 int",
|
||||
"抛出 TypeError(类型转换失败)",
|
||||
"静默忽略"
|
||||
],
|
||||
"ans": 2,
|
||||
"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,
|
||||
"opts": [
|
||||
"编译错误",
|
||||
"返回 null",
|
||||
"静默继续",
|
||||
"抛出 TypeError(空检查失败)"
|
||||
],
|
||||
"ans": 3,
|
||||
"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,
|
||||
"opts": [
|
||||
"与 <code>...</code> 展开等价",
|
||||
"只能用于 List",
|
||||
"链式调用并返回最后一次结果",
|
||||
"对同一对象连续操作且返回对象本身"
|
||||
],
|
||||
"ans": 3,
|
||||
"exp": "<b>级联操作符</b> <code>..</code> 对同一对象连续执行多个操作,<b>不改变返回值</b>(始终是原对象),适合集中配置对象。<code>.</code> 返回的是方法调用的返回值,两者行为不同。"
|
||||
},
|
||||
{
|
||||
"lv": 6,
|
||||
"q": "Dart 3 中,函数想直接返回\"用户名 + 年龄\"这组小数据,又不想专门建一个类,推荐用?",
|
||||
"opts": ["<code>Map<String, dynamic></code>", "Record 记录类型:<code>(String name, int age)</code>", "<code>List<dynamic></code>", "用全局变量"],
|
||||
"opts": [
|
||||
"<code>List<dynamic></code>",
|
||||
"Record 记录类型:<code>(String name, int age)</code>",
|
||||
"用全局变量",
|
||||
"<code>Map<String, dynamic></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\" => \"成功\", _ => \"未知\" };</code> 这种写法?",
|
||||
"opts": ["合法:switch 表达式返回一个值", "非法:switch 不能作表达式", "必须每个分支加 break", "只能匹配字符串"],
|
||||
"ans": 0,
|
||||
"opts": [
|
||||
"只能匹配字符串",
|
||||
"合法:switch 表达式返回一个值",
|
||||
"非法:switch 不能作表达式",
|
||||
"必须每个分支加 break"
|
||||
],
|
||||
"ans": 1,
|
||||
"exp": "<b>switch 表达式</b>用 <code>=></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,
|
||||
"opts": [
|
||||
"继承 String",
|
||||
"用 mixin 混入",
|
||||
"<b>extension</b> 扩展:<code>extension EmailCheck on String { bool isEmail() {...} }</code>",
|
||||
"写静态工具类"
|
||||
],
|
||||
"ans": 2,
|
||||
"exp": "<b>extension</b> 可以不修改原类给已有类型添加方法,之后 <code>\"a@b.com\".isEmail()</code> 直接调用。注意扩展方法是静态分发(编译期决定),不参与运行时多态。"
|
||||
},
|
||||
{
|
||||
"lv": 6,
|
||||
"q": "泛型容器 <code>class Box<T extends num> {}</code> 中 <code>extends num</code> 的含义是?",
|
||||
"opts": ["T 只能是 int、double 等数值类型", "T 可以是任意类型", "T 必须可空", "T 必须是集合类型"],
|
||||
"ans": 0,
|
||||
"opts": [
|
||||
"T 必须可空",
|
||||
"T 可以是任意类型",
|
||||
"T 必须是集合类型",
|
||||
"T 只能是 int、double 等数值类型"
|
||||
],
|
||||
"ans": 3,
|
||||
"exp": "<b>泛型约束</b>限定 T 必须是 num 及其子类(int/double),这样 Box 内部就能安全调用数值运算。不加约束时 T 等价于 <code>Object?</code>。"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
"q": "新建 Flutter 项目后,日常编写页面代码的入口文件是哪一个?",
|
||||
"opts": [
|
||||
"lib/main.dart",
|
||||
"bin/main.dart",
|
||||
"web/index.html",
|
||||
"pubspec.yaml"
|
||||
"pubspec.yaml",
|
||||
"bin/main.dart"
|
||||
],
|
||||
"ans": 0,
|
||||
"exp": "Flutter 约定业务代码都放在 <code>lib/</code> 目录下,入口文件是 <code>lib/main.dart</code>;<code>bin/</code> 是纯 Dart 命令行项目的约定目录,<code>pubspec.yaml</code> 是项目配置而不是代码。"
|
||||
@@ -25,24 +25,24 @@
|
||||
"lv": 1,
|
||||
"q": "要做一个“顶部有标题栏、中间是内容、右下角有悬浮按钮”的标准页面,最合适的骨架组件是?",
|
||||
"opts": [
|
||||
"Scaffold",
|
||||
"Container",
|
||||
"Column",
|
||||
"MaterialApp"
|
||||
"MaterialApp",
|
||||
"Container",
|
||||
"Scaffold"
|
||||
],
|
||||
"ans": 0,
|
||||
"ans": 3,
|
||||
"exp": "<code>Scaffold</code> 是 Material 页面骨架,直接提供 <code>appBar</code>、<code>body</code>、<code>floatingActionButton</code>、<code>bottomNavigationBar</code> 等插槽;<code>MaterialApp</code> 是整个 App 的根配置,一个 App 通常只需要一个。"
|
||||
},
|
||||
{
|
||||
"lv": 1,
|
||||
"q": "<code>MaterialApp(home: LoginPage())</code> 中 <b>home</b> 参数的作用是?",
|
||||
"opts": [
|
||||
"指定 App 启动后显示的第一个页面",
|
||||
"设置按下返回键后回到的页面",
|
||||
"指定 App 在手机桌面上的图标",
|
||||
"注册全部命名路由的映射表",
|
||||
"设置按下返回键后回到的页面"
|
||||
"指定 App 启动后显示的第一个页面"
|
||||
],
|
||||
"ans": 0,
|
||||
"ans": 3,
|
||||
"exp": "<code>home</code> 就是应用的首页 Widget;注册命名路由用的是 <code>routes</code> 参数;App 图标要在各平台的原生工程里配置。"
|
||||
},
|
||||
{
|
||||
@@ -50,9 +50,9 @@
|
||||
"q": "编辑器提示你把 <code>Text('登录')</code> 改写成 <code>const Text('登录')</code>,这样做的主要好处是?",
|
||||
"opts": [
|
||||
"该 Widget 在父组件重建时可复用同一个实例,减少重建开销",
|
||||
"让这段文字变成只读,用户无法选中复制",
|
||||
"只是编译更快,运行时没有任何区别",
|
||||
"保证这个组件在页面中只会出现一次"
|
||||
"保证这个组件在页面中只会出现一次",
|
||||
"让这段文字变成只读,用户无法选中复制"
|
||||
],
|
||||
"ans": 0,
|
||||
"exp": "<code>const</code> Widget 在编译期就确定下来,父组件的 <code>build</code> 重新执行时会直接复用同一个常量实例,不必重新创建,是 Flutter 里最廉价的性能优化,所以 lint 会主动提示你加上。"
|
||||
@@ -61,12 +61,12 @@
|
||||
"lv": 1,
|
||||
"q": "改完页面样式想立刻在模拟器上看到效果,并且保留当前页面已经填好的表单内容,应该用?",
|
||||
"opts": [
|
||||
"热重载 Hot Reload(按 r)",
|
||||
"热重启 Hot Restart(按 R)",
|
||||
"停止调试后重新执行 flutter run",
|
||||
"先 flutter clean 再重新编译"
|
||||
"热重载 Hot Reload(按 r)",
|
||||
"先 flutter clean 再重新编译",
|
||||
"热重启 Hot Restart(按 R)"
|
||||
],
|
||||
"ans": 0,
|
||||
"ans": 1,
|
||||
"exp": "Hot Reload 只把改动注入正在运行的应用并重建 Widget 树,<b>会保留 State</b>;Hot Restart 会重新执行 <code>main()</code>,所有状态清零。改 UI 用前者,改 <code>main()</code>、全局变量或依赖时才需要后者。"
|
||||
},
|
||||
{
|
||||
@@ -74,9 +74,9 @@
|
||||
"q": "想让一段文字四周都留出 16 像素的<b>内</b>边距,正确写法是?",
|
||||
"opts": [
|
||||
"Padding(padding: EdgeInsets.all(16), child: Text('内容'))",
|
||||
"Container(margin: EdgeInsets.all(16), child: Text('内容'))",
|
||||
"SizedBox(width: 16, height: 16, child: Text('内容'))",
|
||||
"Text('内容', textAlign: TextAlign.center)"
|
||||
"Text('内容', textAlign: TextAlign.center)",
|
||||
"Container(margin: EdgeInsets.all(16), child: Text('内容'))"
|
||||
],
|
||||
"ans": 0,
|
||||
"exp": "内边距用 <code>Padding</code> 搭配 <code>EdgeInsets</code>。<code>EdgeInsets.all(16)</code> 四边相同,<code>EdgeInsets.symmetric(horizontal: 16, vertical: 8)</code> 分别设置左右和上下,<code>EdgeInsets.only(left: 16)</code> 只设某一边。"
|
||||
@@ -85,120 +85,120 @@
|
||||
"lv": 2,
|
||||
"q": "<code>Container</code> 的 <b>padding</b> 和 <b>margin</b> 有什么区别?",
|
||||
"opts": [
|
||||
"padding 是内容到自身边界的内边距,margin 是自身到外部的外边距",
|
||||
"两者完全等价,只是写法不同",
|
||||
"padding 只对文字生效,margin 只对图片生效",
|
||||
"padding 会撑大父组件,margin 会撑大子组件"
|
||||
"两者完全等价,只是写法不同",
|
||||
"padding 会撑大父组件,margin 会撑大子组件",
|
||||
"padding 是内容到自身边界的内边距,margin 是自身到外部的外边距"
|
||||
],
|
||||
"ans": 0,
|
||||
"ans": 3,
|
||||
"exp": "记忆方式:<b>padding 撑开里面,margin 推开外面</b>。给卡片内容留白用 padding,让卡片之间拉开距离用 margin。"
|
||||
},
|
||||
{
|
||||
"lv": 2,
|
||||
"q": "只是想在两个按钮之间加 12 像素的垂直间距,最轻量的写法是?",
|
||||
"opts": [
|
||||
"Padding(padding: EdgeInsets.all(12))",
|
||||
"SizedBox(height: 12)",
|
||||
"Container(height: 12, color: Colors.transparent)",
|
||||
"Padding(padding: EdgeInsets.all(12))",
|
||||
"Divider(height: 12)"
|
||||
],
|
||||
"ans": 0,
|
||||
"ans": 1,
|
||||
"exp": "<code>SizedBox</code> 只占位、不绘制,是做间距的首选;<code>Container</code> 功能多也更重;<code>Divider</code> 会额外画出一条分割线,语义不同。"
|
||||
},
|
||||
{
|
||||
"lv": 2,
|
||||
"q": "页面上要展示一排数量不固定的标签,希望一行放不下时自动折到下一行,应该用?",
|
||||
"opts": [
|
||||
"Wrap",
|
||||
"Row",
|
||||
"Column",
|
||||
"Expanded"
|
||||
"Expanded",
|
||||
"Wrap",
|
||||
"Row"
|
||||
],
|
||||
"ans": 0,
|
||||
"ans": 2,
|
||||
"exp": "<code>Row</code> 是单行布局,放不下就会溢出报警告;<code>Wrap</code> 在主轴空间不足时自动换行,再配 <code>spacing</code>(同行间距)和 <code>runSpacing</code>(行与行间距)即可。"
|
||||
},
|
||||
{
|
||||
"lv": 2,
|
||||
"q": "页面顶部内容被状态栏/刘海挡住、底部被手势条挡住,最稳妥的处理是?",
|
||||
"opts": [
|
||||
"在最外层套一个 SafeArea",
|
||||
"给最外层写死 EdgeInsets.only(top: 44)",
|
||||
"把页面改成 StatefulWidget",
|
||||
"给最外层写死 EdgeInsets.only(top: 44)",
|
||||
"在最外层套一个 SafeArea",
|
||||
"用 Center 把内容整体居中"
|
||||
],
|
||||
"ans": 0,
|
||||
"ans": 2,
|
||||
"exp": "<code>SafeArea</code> 会读取系统的安全区信息,自动补上合适的内边距,各机型自适应;写死 44 像素换一台设备就会错位。"
|
||||
},
|
||||
{
|
||||
"lv": 3,
|
||||
"q": "页面上放一个“提交”按钮并响应点击,标准写法是?",
|
||||
"opts": [
|
||||
"ElevatedButton(onPressed: () { ... }, child: Text('提交'))",
|
||||
"ElevatedButton(onPressed: '提交')",
|
||||
"ElevatedButton(child: Text('提交'), onClick: () { ... })",
|
||||
"Button(text: '提交', onTap: () { ... })",
|
||||
"ElevatedButton(onPressed: '提交')"
|
||||
"ElevatedButton(onPressed: () { ... }, child: Text('提交'))",
|
||||
"Button(text: '提交', onTap: () { ... })"
|
||||
],
|
||||
"ans": 0,
|
||||
"ans": 2,
|
||||
"exp": "Flutter 按钮的点击回调统一叫 <code>onPressed</code>,接收一个无参函数。常用的三种:<code>ElevatedButton</code>(填充)、<code>TextButton</code>(纯文字)、<code>OutlinedButton</code>(描边)。"
|
||||
},
|
||||
{
|
||||
"lv": 3,
|
||||
"q": "表单没填完时,希望“提交”按钮自动变灰且点不动,最简单的做法是?",
|
||||
"opts": [
|
||||
"把 onPressed 设为 null",
|
||||
"把 child 设为 null",
|
||||
"用 Visibility 把按钮整个隐藏",
|
||||
"外面包一层 IgnorePointer 并手动改颜色"
|
||||
"把 onPressed 设为 null",
|
||||
"外面包一层 IgnorePointer 并手动改颜色",
|
||||
"把 child 设为 null"
|
||||
],
|
||||
"ans": 0,
|
||||
"ans": 1,
|
||||
"exp": "Flutter 按钮的约定是:<code>onPressed == null</code> 即为禁用状态,会自动切换成灰色的 disabled 样式。所以常写成 <code>onPressed: canSubmit ? _submit : null</code>。"
|
||||
},
|
||||
{
|
||||
"lv": 3,
|
||||
"q": "要做“左边头像、中间标题加副标题、右边一个箭头”的列表行,最省事的组件是?",
|
||||
"opts": [
|
||||
"ListTile",
|
||||
"Row",
|
||||
"Card",
|
||||
"Column"
|
||||
"Row",
|
||||
"Column",
|
||||
"ListTile"
|
||||
],
|
||||
"ans": 0,
|
||||
"ans": 3,
|
||||
"exp": "<code>ListTile</code> 直接提供 <code>leading</code>、<code>title</code>、<code>subtitle</code>、<code>trailing</code> 四个插槽,还内置了 <code>onTap</code> 点击和标准的高度与间距,省去手写 Row/Column 对齐。"
|
||||
},
|
||||
{
|
||||
"lv": 3,
|
||||
"q": "想在搜索框前面放一个放大镜图标,写法是?",
|
||||
"opts": [
|
||||
"Icon(Icons.search)",
|
||||
"Image(Icons.search)",
|
||||
"Text(Icons.search)",
|
||||
"Image(Icons.search)",
|
||||
"Icon(Icons.search)",
|
||||
"IconData('search')"
|
||||
],
|
||||
"ans": 0,
|
||||
"ans": 2,
|
||||
"exp": "<code>Icon</code> 负责渲染,<code>Icons</code> 是 Material 内置的图标常量集合(<code>Icons.search</code>、<code>Icons.add</code>…)。如果图标本身要能点击,用 <code>IconButton</code>。"
|
||||
},
|
||||
{
|
||||
"lv": 3,
|
||||
"q": "商品封面图要显示成圆角矩形,标准做法是?",
|
||||
"opts": [
|
||||
"用 ClipRRect(borderRadius: BorderRadius.circular(8)) 把图片包起来",
|
||||
"给 Image 设置 borderRadius 参数",
|
||||
"把 Image 的 fit 设为 BoxFit.cover",
|
||||
"用 Opacity 把四个角变透明"
|
||||
"用 Opacity 把四个角变透明",
|
||||
"用 ClipRRect(borderRadius: BorderRadius.circular(8)) 把图片包起来"
|
||||
],
|
||||
"ans": 0,
|
||||
"ans": 3,
|
||||
"exp": "<code>Image</code> 本身没有圆角参数,需要靠外层裁剪:圆角矩形用 <code>ClipRRect</code>,正圆用 <code>ClipOval</code> 或 <code>CircleAvatar</code>;也可以用 <code>Container</code> 的 <code>decoration: BoxDecoration(borderRadius: ...)</code> 实现。"
|
||||
},
|
||||
{
|
||||
"lv": 4,
|
||||
"q": "把设计给的 logo.png 放进项目,想用 <code>Image.asset('assets/logo.png')</code> 显示,还必须做的一步是?",
|
||||
"opts": [
|
||||
"在 pubspec.yaml 的 flutter -> assets 下声明这个路径",
|
||||
"在 main.dart 里 import 这张图片",
|
||||
"把图片改名成 logo.dart",
|
||||
"在 AndroidManifest.xml 里注册这张图片"
|
||||
"在 AndroidManifest.xml 里注册这张图片",
|
||||
"在 pubspec.yaml 的 flutter -> assets 下声明这个路径",
|
||||
"在 main.dart 里 import 这张图片"
|
||||
],
|
||||
"ans": 0,
|
||||
"ans": 2,
|
||||
"exp": "本地资源必须先在 <code>pubspec.yaml</code> 里声明才会被打包,在 <code>flutter:</code> 节点下写 <code>assets:</code> 再列出 <code>- assets/logo.png</code>;只写目录 <code>- assets/</code> 可以一次声明整个文件夹。注意改完 pubspec 要重新运行,Hot Reload 不会重新打包资源。"
|
||||
},
|
||||
{
|
||||
@@ -217,72 +217,72 @@
|
||||
"lv": 4,
|
||||
"q": "要把一段文字设置成 18 号、加粗、灰色,正确写法是?",
|
||||
"opts": [
|
||||
"Text('标题', css: 'font-size:18px;font-weight:bold')",
|
||||
"Text('标题', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.grey))",
|
||||
"Text('标题', fontSize: 18, bold: true, color: Colors.grey)",
|
||||
"Text('标题', css: 'font-size:18px;font-weight:bold')",
|
||||
"TextStyle('标题', size: 18, bold: true)"
|
||||
],
|
||||
"ans": 0,
|
||||
"ans": 1,
|
||||
"exp": "文字样式统一放在 <code>Text</code> 的 <code>style</code> 参数里,类型是 <code>TextStyle</code>。想在主题字号的基础上微调,用 <code>Theme.of(context).textTheme.bodyLarge?.copyWith(color: Colors.grey)</code>。"
|
||||
},
|
||||
{
|
||||
"lv": 4,
|
||||
"q": "项目里多个页面都要用同一个主色调,最推荐的取色方式是?",
|
||||
"opts": [
|
||||
"Theme.of(context).colorScheme.primary",
|
||||
"每个页面各自写死 Color(0xFF3D6B5E)",
|
||||
"每次从设计稿图片里取色",
|
||||
"定义一个全局可变变量,谁都能改",
|
||||
"每次从设计稿图片里取色"
|
||||
"Theme.of(context).colorScheme.primary"
|
||||
],
|
||||
"ans": 0,
|
||||
"ans": 3,
|
||||
"exp": "颜色、字体等统一配置在 <code>MaterialApp(theme: ThemeData(...))</code>,页面里通过 <code>Theme.of(context)</code> 取用。这样换肤、适配深色模式只需要改一处。顺带一提,Flutter 的颜色是 8 位 ARGB,<code>0xFF</code> 开头才是不透明。"
|
||||
},
|
||||
{
|
||||
"lv": 4,
|
||||
"q": "要让卡片宽度占屏幕宽度的 80%,获取屏幕宽度的写法是?",
|
||||
"opts": [
|
||||
"MediaQuery.of(context).size.width",
|
||||
"context.width",
|
||||
"Window.screenWidth",
|
||||
"Scaffold.of(context).width",
|
||||
"Window.screenWidth"
|
||||
"context.width",
|
||||
"MediaQuery.of(context).size.width"
|
||||
],
|
||||
"ans": 0,
|
||||
"ans": 3,
|
||||
"exp": "<code>MediaQuery</code> 提供屏幕尺寸、状态栏高度、字体缩放比等信息,依赖 <code>context</code>,只能在 <code>build</code> 里或拿到 context 之后使用。如果想按<b>父容器</b>而不是屏幕的尺寸来布局,用 <code>LayoutBuilder</code>。"
|
||||
},
|
||||
{
|
||||
"lv": 5,
|
||||
"q": "想让整张商品卡片都能点击,并且点下去有 Material 水波纹反馈,应该用?",
|
||||
"opts": [
|
||||
"InkWell",
|
||||
"GestureDetector",
|
||||
"InkWell",
|
||||
"Container",
|
||||
"AbsorbPointer"
|
||||
],
|
||||
"ans": 0,
|
||||
"ans": 1,
|
||||
"exp": "<code>GestureDetector</code> 也能响应点击,但没有任何视觉反馈;<code>InkWell</code> 会在点击时绘制水波纹,更符合 Material 交互习惯。注意水波纹画在 <code>Material</code> 上,所以 InkWell 上层要有 Material 背景才看得见。"
|
||||
},
|
||||
{
|
||||
"lv": 5,
|
||||
"q": "保存成功后,想在页面底部弹出一条“保存成功”的短提示,写法是?",
|
||||
"opts": [
|
||||
"print('保存成功')",
|
||||
"ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('保存成功')))",
|
||||
"showDialog(context: context, builder: ...)",
|
||||
"print('保存成功')",
|
||||
"Navigator.push 跳到一个提示页"
|
||||
],
|
||||
"ans": 0,
|
||||
"ans": 1,
|
||||
"exp": "底部短提示用 <code>SnackBar</code>,通过 <code>ScaffoldMessenger.of(context)</code> 弹出(旧写法 <code>Scaffold.of(context).showSnackBar</code> 已废弃)。它会自动消失,不打断用户当前操作。"
|
||||
},
|
||||
{
|
||||
"lv": 5,
|
||||
"q": "点“删除”后要弹确认框,并根据用户点的是“确定”还是“取消”决定是否真的删除,正确写法是?",
|
||||
"opts": [
|
||||
"在 build 方法里判断用户点了什么",
|
||||
"final ok = await showDialog<bool>(...); if (ok == true) { 执行删除 }",
|
||||
"showDialog(...); 紧接着直接执行删除",
|
||||
"用 SnackBar 代替确认框",
|
||||
"在 build 方法里判断用户点了什么"
|
||||
"showDialog(...); 紧接着直接执行删除"
|
||||
],
|
||||
"ans": 0,
|
||||
"ans": 1,
|
||||
"exp": "<code>showDialog</code> 返回一个 <code>Future</code>,对话框里用 <code>Navigator.pop(context, true)</code> 把结果带回来,外面 <code>await</code> 拿到后再决定后续动作。注意用户点遮罩关闭时结果是 <code>null</code>,所以要判断 <code>== true</code> 而不是直接当布尔用。"
|
||||
},
|
||||
{
|
||||
@@ -290,9 +290,9 @@
|
||||
"q": "点“选择城市”后,希望从屏幕底部滑出一个选项面板,应该用?",
|
||||
"opts": [
|
||||
"showModalBottomSheet",
|
||||
"showDialog",
|
||||
"Navigator.pop",
|
||||
"BottomNavigationBar"
|
||||
"BottomNavigationBar",
|
||||
"showDialog"
|
||||
],
|
||||
"ans": 0,
|
||||
"exp": "<code>showModalBottomSheet</code> 从底部滑出模态面板,同样返回 <code>Future</code>,面板里用 <code>Navigator.pop(context, value)</code> 回传用户的选择;<code>BottomNavigationBar</code> 是常驻的底部标签栏,用途完全不同。"
|
||||
@@ -301,60 +301,60 @@
|
||||
"lv": 5,
|
||||
"q": "App 底部要有“首页 / 分类 / 我的”三个标签来回切换,正确做法是?",
|
||||
"opts": [
|
||||
"Scaffold 的 bottomNavigationBar 放 BottomNavigationBar,用 currentIndex 记录选中项,onTap 里 setState",
|
||||
"每点一个标签就 Navigator.push 一个新页面",
|
||||
"用三个 Column 上下排列,点谁显示谁",
|
||||
"在 MaterialApp 里配置三个 home"
|
||||
"Scaffold 的 bottomNavigationBar 放 BottomNavigationBar,用 currentIndex 记录选中项,onTap 里 setState",
|
||||
"在 MaterialApp 里配置三个 home",
|
||||
"每点一个标签就 Navigator.push 一个新页面"
|
||||
],
|
||||
"ans": 0,
|
||||
"ans": 1,
|
||||
"exp": "标签栏切换的是同一个页面里的内容,用 <code>currentIndex</code> + <code>onTap</code> 改状态即可;如果每次都 <code>push</code> 新页面,返回栈会越堆越深。希望切回来时保留各标签的滚动位置,可以配合 <code>IndexedStack</code>。"
|
||||
},
|
||||
{
|
||||
"lv": 6,
|
||||
"q": "登录表单要一次性校验“手机号”和“密码”两个输入框,官方推荐做法是?",
|
||||
"opts": [
|
||||
"提交时挨个读 controller.text 用 if 判断",
|
||||
"用 Form 包住输入框,配一个 GlobalKey<FormState>,提交时调用 _formKey.currentState!.validate()",
|
||||
"在每个 TextField 的 onChanged 里手写 if 判断",
|
||||
"提交时挨个读 controller.text 用 if 判断",
|
||||
"前端不校验,全部交给服务端返回错误"
|
||||
],
|
||||
"ans": 0,
|
||||
"ans": 1,
|
||||
"exp": "<code>Form</code> + <code>GlobalKey<FormState></code> 是官方表单方案:<code>validate()</code> 会依次触发每个 <code>TextFormField</code> 的 <code>validator</code>,<b>全部通过才返回 true</b>,并自动把错误文案显示在对应输入框下方。"
|
||||
},
|
||||
{
|
||||
"lv": 6,
|
||||
"q": "<code>TextFormField</code> 的 <code>validator</code> 回调中,返回 <code>null</code> 表示什么?",
|
||||
"opts": [
|
||||
"该字段不参与校验",
|
||||
"校验通过,不显示任何错误提示",
|
||||
"校验失败,但不提示用户",
|
||||
"清空输入框里的内容",
|
||||
"该字段不参与校验"
|
||||
"校验失败,但不提示用户"
|
||||
],
|
||||
"ans": 0,
|
||||
"ans": 1,
|
||||
"exp": "<code>validator</code> 的约定是:返回 <code>null</code> 代表通过;返回一个字符串代表失败,且这个字符串就是显示在输入框下方的错误文案。例如 <code>validator: (v) => (v == null || v.isEmpty) ? '请输入手机号' : null</code>。"
|
||||
},
|
||||
{
|
||||
"lv": 6,
|
||||
"q": "进入页面时要立刻发一次网络请求拿数据,这段代码应该写在 State 的哪个方法里?",
|
||||
"opts": [
|
||||
"setState()",
|
||||
"initState()",
|
||||
"build()",
|
||||
"dispose()",
|
||||
"setState()"
|
||||
"build()"
|
||||
],
|
||||
"ans": 0,
|
||||
"ans": 1,
|
||||
"exp": "<code>initState()</code> 在 State 创建后只执行一次,适合做初始化:发首次请求、创建 Controller、注册监听。<code>build()</code> 可能被调用很多次,把请求写在里面会反复触发;<code>dispose()</code> 是销毁时做清理的地方。"
|
||||
},
|
||||
{
|
||||
"lv": 6,
|
||||
"q": "页面数据来自一次异步请求,要根据“加载中 / 成功 / 失败”分别渲染不同界面,最省事的组件是?",
|
||||
"opts": [
|
||||
"AnimatedBuilder",
|
||||
"FutureBuilder",
|
||||
"ListView.builder",
|
||||
"StatelessWidget",
|
||||
"AnimatedBuilder"
|
||||
"StatelessWidget"
|
||||
],
|
||||
"ans": 0,
|
||||
"ans": 1,
|
||||
"exp": "<code>FutureBuilder</code> 接收一个 <code>Future</code>,在 <code>builder</code> 里根据 <code>snapshot.connectionState</code> 和 <code>snapshot.hasError</code> 分支渲染,不用自己维护一堆 isLoading 字段。<b>坑</b>:不要把 <code>future: fetchData()</code> 直接写在 build 里,那样每次重建都会重新请求,应在 <code>initState</code> 里先创建好再传进去。持续变化的数据流则用 <code>StreamBuilder</code>。"
|
||||
},
|
||||
{
|
||||
@@ -362,12 +362,12 @@
|
||||
"q": "异步请求 <code>await</code> 回来后要调用 <code>setState</code>,为什么通常先判断一下 <code>mounted</code>?",
|
||||
"opts": [
|
||||
"请求返回时页面可能已被关闭,对已销毁的 State 调用 setState 会报错",
|
||||
"判断 mounted 能让请求返回得更快",
|
||||
"mounted 是用来判断网络是否连通的",
|
||||
"不判断的话请求会被系统取消",
|
||||
"判断 mounted 能让请求返回得更快"
|
||||
"不判断的话请求会被系统取消"
|
||||
],
|
||||
"ans": 0,
|
||||
"exp": "<code>await</code> 期间用户可能已经返回上一页,State 被销毁。此时再 <code>setState</code> 会抛出 setState() called after dispose()。标准写法是拿到结果后先 <code>if (!mounted) return;</code>,再更新状态。"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,214 +1,371 @@
|
||||
{
|
||||
"name": "Flutter 开发实战",
|
||||
"lvNames": ["Widget 基础", "布局与组件", "状态管理", "路由与导航", "数据与网络", "实战综合"],
|
||||
"lvNames": [
|
||||
"Widget 基础",
|
||||
"布局与组件",
|
||||
"状态管理",
|
||||
"路由与导航",
|
||||
"数据与网络",
|
||||
"实战综合"
|
||||
],
|
||||
"questions": [
|
||||
{
|
||||
"lv": 1,
|
||||
"q": "新建 Flutter 项目后,把应用真正跑起来的入口代码是?",
|
||||
"opts": ["<code>void main() => runApp(MyApp());</code>", "<code>void main() { MyApp(); }</code>", "<code>runApp() { MyApp }</code>", "<code>main() { MaterialApp(); }</code>"],
|
||||
"ans": 0,
|
||||
"opts": [
|
||||
"<code>void main() { MyApp(); }</code>",
|
||||
"<code>void main() => runApp(MyApp());</code>",
|
||||
"<code>runApp() { MyApp }</code>",
|
||||
"<code>main() { MaterialApp(); }</code>"
|
||||
],
|
||||
"ans": 1,
|
||||
"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,
|
||||
"opts": [
|
||||
"StatefulWidget",
|
||||
"Container",
|
||||
"Scaffold",
|
||||
"StatelessWidget"
|
||||
],
|
||||
"ans": 0,
|
||||
"exp": "有<b>动态变化的数据</b>(倒计时数字)就需要 StatefulWidget:数据保存在 State 里,变化时调 <code>setState()</code> 刷新。没有可变状态时应优先用 StatelessWidget。"
|
||||
},
|
||||
{
|
||||
"lv": 1,
|
||||
"q": "界面上要显示一行固定文案\"登录成功\",最合适的组件是?",
|
||||
"opts": ["Text", "Label", "Paragraph", "RichText"],
|
||||
"ans": 0,
|
||||
"opts": [
|
||||
"RichText",
|
||||
"Paragraph",
|
||||
"Label",
|
||||
"Text"
|
||||
],
|
||||
"ans": 3,
|
||||
"exp": "<code>Text</code> 是最常用的文本组件,可通过 style 配置字号、颜色、字重等。RichText 用于一段文字中混排多种样式的富文本场景。"
|
||||
},
|
||||
{
|
||||
"lv": 1,
|
||||
"q": "点击\"确认\"按钮后分数要 +1 并立即显示到界面,正确流程是?",
|
||||
"opts": ["修改 State 字段后调用 <code>setState()</code>", "直接修改字段就行", "重启整个 App", "修改 Widget 的构造参数"],
|
||||
"ans": 0,
|
||||
"opts": [
|
||||
"修改 Widget 的构造参数",
|
||||
"重启整个 App",
|
||||
"直接修改字段就行",
|
||||
"修改 State 字段后调用 <code>setState()</code>"
|
||||
],
|
||||
"ans": 3,
|
||||
"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,
|
||||
"opts": [
|
||||
"GridView.count 全部构建",
|
||||
"Column 里循环放 10000 个组件",
|
||||
"SingleChildScrollView 包 Column",
|
||||
"<code>ListView.builder</code>(按需构建可见项)"
|
||||
],
|
||||
"ans": 3,
|
||||
"exp": "<code>ListView.builder</code> 只构建<b>当前可见</b>的列表项(懒加载),滚动时回收复用;Column + for 会一次性构建全部组件,万条数据直接卡死。数据量大优先 builder 系列。"
|
||||
},
|
||||
{
|
||||
"lv": 2,
|
||||
"q": "登录页要垂直排列\"输入框、输入框、登录按钮\",用哪个布局组件?",
|
||||
"opts": ["Column", "Row", "Stack", "Wrap"],
|
||||
"opts": [
|
||||
"Column",
|
||||
"Wrap",
|
||||
"Stack",
|
||||
"Row"
|
||||
],
|
||||
"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,
|
||||
"opts": [
|
||||
"<code>Padding</code>",
|
||||
"<code>Expanded</code>",
|
||||
"<code>Center</code>",
|
||||
"<code>Spacer</code>"
|
||||
],
|
||||
"ans": 1,
|
||||
"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,
|
||||
"opts": [
|
||||
"<code>mainAxisAlignment: MainAxisAlignment.center</code>",
|
||||
"<code>textAlign: TextAlign.center</code>",
|
||||
"<code>crossAxisAlignment: CrossAxisAlignment.center</code>",
|
||||
"<code>alignment: Alignment.center</code>"
|
||||
],
|
||||
"ans": 2,
|
||||
"exp": "Column 的<b>主轴是垂直</b>方向、<b>交叉轴是水平</b>方向。水平居中=沿交叉轴居中=crossAxisAlignment.center;mainAxisAlignment 控制的是垂直方向。主/交叉轴搞反是新手最常踩的坑。"
|
||||
},
|
||||
{
|
||||
"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,
|
||||
"opts": [
|
||||
"<code>BoxFit.scaleDown</code>",
|
||||
"<code>BoxFit.none</code>",
|
||||
"<code>BoxFit.fill</code>",
|
||||
"<code>BoxFit.cover</code>"
|
||||
],
|
||||
"ans": 3,
|
||||
"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,
|
||||
"opts": [
|
||||
"<code>softWrap: false</code> 就够了",
|
||||
"只设 overflow: TextOverflow.clip",
|
||||
"只设 maxLines: 3",
|
||||
"<code>maxLines: 1, overflow: TextOverflow.ellipsis</code>"
|
||||
],
|
||||
"ans": 3,
|
||||
"exp": "<code>maxLines</code> 限制最大行数 + <code>overflow: TextOverflow.ellipsis</code> 指定超出的表现(省略号/裁剪/fade),两者配合才能出现\"…\"效果。"
|
||||
},
|
||||
{
|
||||
"lv": 3,
|
||||
"q": "输入框需要\"预填默认值 + 按钮点击后读取内容 + 随时清空\",推荐用?",
|
||||
"opts": ["<code>TextEditingController</code>", "每次读 onChanged 参数", "GlobalKey 临时查找", "无法实现"],
|
||||
"ans": 0,
|
||||
"opts": [
|
||||
"GlobalKey 临时查找",
|
||||
"无法实现",
|
||||
"每次读 onChanged 参数",
|
||||
"<code>TextEditingController</code>"
|
||||
],
|
||||
"ans": 3,
|
||||
"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,
|
||||
"opts": [
|
||||
"<code>build()</code>",
|
||||
"<code>initState()</code>",
|
||||
"<code>dispose()</code>",
|
||||
"<code>didChangeDependencies()</code>"
|
||||
],
|
||||
"ans": 2,
|
||||
"exp": "<code>dispose()</code> 是 State 被移除时的清理钩子:取消 Timer、Stream 订阅、释放 Controller。不清理会造成内存泄漏和 \"setState called after dispose\" 报错。"
|
||||
},
|
||||
{
|
||||
"lv": 3,
|
||||
"q": "登录状态、用户信息要被<b>多个页面共享</b>,又不想层层传构造参数,项目常用方案是?",
|
||||
"opts": ["Provider / InheritedWidget", "定义全局变量直接读写", "每个页面重新请求接口", "把状态塞进路由参数"],
|
||||
"ans": 0,
|
||||
"opts": [
|
||||
"每个页面重新请求接口",
|
||||
"Provider / InheritedWidget",
|
||||
"把状态塞进路由参数",
|
||||
"定义全局变量直接读写"
|
||||
],
|
||||
"ans": 1,
|
||||
"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,
|
||||
"opts": [
|
||||
"<code>AnimatedList</code>",
|
||||
"<code>Scrollbar</code>",
|
||||
"<code>GestureDetector</code>",
|
||||
"<code>RefreshIndicator</code>"
|
||||
],
|
||||
"ans": 3,
|
||||
"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>"],
|
||||
"opts": [
|
||||
"<code>WidgetsBindingObserver</code>",
|
||||
"<code>MediaQuery</code>",
|
||||
"<code>NavigatorObserver</code>",
|
||||
"<code>RouteObserver</code>"
|
||||
],
|
||||
"ans": 0,
|
||||
"exp": "给 State 混入 <code>with WidgetsBindingObserver</code>,在 <code>didChangeAppLifecycleState()</code> 中监听 AppLifecycleState(paused / resumed / inactive),即可感知前后台切换。"
|
||||
},
|
||||
{
|
||||
"lv": 4,
|
||||
"q": "列表页点击商品跳转详情页,标准写法是?",
|
||||
"opts": ["<code>Navigator.push(context, MaterialPageRoute(builder: (_) => DetailPage(id: id)))</code>", "<code>Navigator.go(DetailPage(id: id))</code>", "<code>Route.open(DetailPage())</code>", "<code>DetailPage(id: id).show()</code>"],
|
||||
"ans": 0,
|
||||
"opts": [
|
||||
"<code>Navigator.go(DetailPage(id: id))</code>",
|
||||
"<code>DetailPage(id: id).show()</code>",
|
||||
"<code>Route.open(DetailPage())</code>",
|
||||
"<code>Navigator.push(context, MaterialPageRoute(builder: (_) => DetailPage(id: id)))</code>"
|
||||
],
|
||||
"ans": 3,
|
||||
"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,
|
||||
"opts": [
|
||||
"<code>Navigator.back(result)</code>",
|
||||
"<code>Navigator.pop(context, result)</code>",
|
||||
"<code>context.popAndPush(result)</code>",
|
||||
"<code>Navigator.pushResult(result)</code>"
|
||||
],
|
||||
"ans": 1,
|
||||
"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,
|
||||
"opts": [
|
||||
"static 静态字段存",
|
||||
"URL 字符串拼接",
|
||||
"全局变量临时存",
|
||||
"<code>DetailPage(id: 1, name: \"商品\")</code> 构造参数传递"
|
||||
],
|
||||
"ans": 3,
|
||||
"exp": "构造参数显式、类型安全、可读性好——跳转处一眼看到传了什么。全局变量/静态字段是隐藏依赖,页面多起来后非常难维护。"
|
||||
},
|
||||
{
|
||||
"lv": 4,
|
||||
"q": "在 App 里注册命名路由(如 <code>/login</code>、<code>/home</code>),配置位置是?",
|
||||
"opts": ["<code>MaterialApp(routes: {...})</code>", "<code>main()</code> 函数里", "<code>runApp()</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,
|
||||
"opts": [
|
||||
"跳转时把回调函数传过去",
|
||||
"<code>final r = await Navigator.push(...);</code> 拿到返回值再处理",
|
||||
"用 setState 轮询",
|
||||
"无法等待结果"
|
||||
],
|
||||
"ans": 1,
|
||||
"exp": "<code>push</code> 返回 <code>Future<T></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<String, dynamic></code>", "<code>Object</code>"],
|
||||
"ans": 0,
|
||||
"opts": [
|
||||
"<code>Map<String, dynamic></code>",
|
||||
"<code>dynamic</code>(实际可能是 Map 或 List)",
|
||||
"<code>String</code>",
|
||||
"<code>Object</code>"
|
||||
],
|
||||
"ans": 1,
|
||||
"exp": "<code>jsonDecode</code> 的返回类型是 <code>dynamic</code>:JSON 对象变 Map<String, dynamic>、数组变 List<dynamic>、数字/布尔原样返回。通常需要手动转换:<code>jsonDecode(text) as Map<String, dynamic></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,
|
||||
"opts": [
|
||||
"<code>JSON.stringify(user)</code>",
|
||||
"给 User 写 <code>toJson()</code>,再 <code>jsonEncode(user.toJson())</code>",
|
||||
"<code>jsonEncode(user)</code> 直接序列化",
|
||||
"<code>user.toString()</code>"
|
||||
],
|
||||
"ans": 1,
|
||||
"exp": "Dart 默认不会自动序列化任意对象,惯例是模型类自己实现 <code>Map<String, dynamic> toJson()</code>,再用 <code>jsonEncode</code> 转字符串;反序列化则对应 <code>User.fromJson(Map)</code> 工厂构造。"
|
||||
},
|
||||
{
|
||||
"lv": 5,
|
||||
"q": "网络请求可能超时/断网,用户点\"登录\"时抛出了异常,正确的处理是?",
|
||||
"opts": ["用 try-catch 包裹 await 请求,捕获异常并提示用户", "不做处理", "打印日志后忽略", "延迟 3 秒自动重试"],
|
||||
"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,
|
||||
"opts": [
|
||||
"AnimatedContainer 闪烁",
|
||||
"<code>CircularProgressIndicator</code>",
|
||||
"Opacity 半透明遮罩",
|
||||
"Text(\"加载中...\")"
|
||||
],
|
||||
"ans": 1,
|
||||
"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,
|
||||
"opts": [
|
||||
"<code>Timer(Duration(seconds: 10), ...)</code>",
|
||||
"<code>await http.get(url).timeout(Duration(seconds: 10))</code>",
|
||||
"<code>Future.any</code> 自动超时",
|
||||
"<code>http.get(url, timeout: 10)</code>"
|
||||
],
|
||||
"ans": 1,
|
||||
"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,
|
||||
"opts": [
|
||||
"三个 await 串行",
|
||||
"<code>Future.wait([...])</code> 并发等待",
|
||||
"依次 catch 分别显示",
|
||||
"用 <code>Future.any</code>"
|
||||
],
|
||||
"ans": 1,
|
||||
"exp": "并发场景用 <code>Future.wait</code>,总耗时约等于最慢的请求而不是三个相加。生产上可配合记录单个请求失败做局部降级(某块失败不影响整页)。"
|
||||
},
|
||||
{
|
||||
"lv": 6,
|
||||
"q": "商品列表滑到底部要自动加载下一页,监听滚动的方案是?",
|
||||
"opts": ["<code>ScrollController</code> 监听 pixels 接近 maxScrollExtent 时触发", "用 Timer 每 3 秒加载", "下拉刷新代替", "GestureDetector 监听手势"],
|
||||
"opts": [
|
||||
"<code>ScrollController</code> 监听 pixels 接近 maxScrollExtent 时触发",
|
||||
"GestureDetector 监听手势",
|
||||
"用 Timer 每 3 秒加载",
|
||||
"下拉刷新代替"
|
||||
],
|
||||
"ans": 0,
|
||||
"exp": "给 ListView 挂 <code>ScrollController</code>,在 <code>addListener</code> 里判断 <code>position.pixels >= position.maxScrollExtent - 200</code>(预留阈值)就加载下一页,配合 loading 状态防止重复触发。"
|
||||
},
|
||||
{
|
||||
"lv": 6,
|
||||
"q": "商品标题中要<b>红色高亮\"热卖\"二字</b>、其余部分灰色,用?",
|
||||
"opts": ["<code>RichText</code> + <code>TextSpan</code> 组合不同样式", "拆成多个 Text 排一排", "用换行符拼接字符串", "TextStyle 做不到,只能整段同色"],
|
||||
"ans": 0,
|
||||
"opts": [
|
||||
"用换行符拼接字符串",
|
||||
"拆成多个 Text 排一排",
|
||||
"<code>RichText</code> + <code>TextSpan</code> 组合不同样式",
|
||||
"TextStyle 做不到,只能整段同色"
|
||||
],
|
||||
"ans": 2,
|
||||
"exp": "<code>RichText</code> 的 <code>TextSpan</code> 支持树状嵌套:每个 span 可以有自己的 style,是\"一段文字多种样式\"的标准方案。"
|
||||
},
|
||||
{
|
||||
"lv": 6,
|
||||
"q": "界面出现<b>黄黑条纹</b>溢出警告(RenderFlex overflowed),说明?",
|
||||
"opts": ["子组件超出了父组件给定的约束空间", "主题颜色配置错误", "内存泄漏", "字体加载失败"],
|
||||
"ans": 0,
|
||||
"opts": [
|
||||
"内存泄漏",
|
||||
"子组件超出了父组件给定的约束空间",
|
||||
"主题颜色配置错误",
|
||||
"字体加载失败"
|
||||
],
|
||||
"ans": 1,
|
||||
"exp": "Row/Column 空间不够时子组件溢出,debug 模式显示黄黑条纹并打印 \"RenderFlex overflowed by XX pixels\"。解决思路:Expanded/Flexible 让出空间、SingleChildScrollView 滚动、FittedBox 缩放。"
|
||||
},
|
||||
{
|
||||
"lv": 6,
|
||||
"q": "瀑布流商品图列表很卡、图片解码占内存高,优先优化手段是?",
|
||||
"opts": ["<code>cacheWidth</code> / <code>ResizeImage</code> 限制解码尺寸", "给图片加圆角", "提高图片的清晰度", "减少列表项数量"],
|
||||
"opts": [
|
||||
"<code>cacheWidth</code> / <code>ResizeImage</code> 限制解码尺寸",
|
||||
"减少列表项数量",
|
||||
"给图片加圆角",
|
||||
"提高图片的清晰度"
|
||||
],
|
||||
"ans": 0,
|
||||
"exp": "Image.network 默认按原图尺寸解码,一张 4000px 大图在手机上就是内存杀手。用 <code>cacheWidth: 300</code> 或 <code>ResizeImage</code> 让引擎按需解码成小尺寸位图,内存占用和滚动性能立竿见影。"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function shuffleArray(array) {
|
||||
for (let i = array.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[array[i], array[j]] = [array[j], array[i]];
|
||||
}
|
||||
}
|
||||
|
||||
function processFile(filePath) {
|
||||
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
let qCount = 0;
|
||||
let ansCounts = {0: 0, 1: 0, 2: 0, 3: 0};
|
||||
|
||||
const processQuestion = (q) => {
|
||||
const correctOpt = q.opts[q.ans];
|
||||
shuffleArray(q.opts);
|
||||
q.ans = q.opts.indexOf(correctOpt);
|
||||
ansCounts[q.ans] = (ansCounts[q.ans] || 0) + 1;
|
||||
qCount++;
|
||||
};
|
||||
|
||||
if (data.sets) {
|
||||
data.sets.forEach(set => set.questions.forEach(processQuestion));
|
||||
} else if (data.questions) {
|
||||
data.questions.forEach(processQuestion);
|
||||
}
|
||||
|
||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
||||
console.log(`Processed ${path.basename(filePath)}: ${qCount} questions. Distribution:`, ansCounts);
|
||||
}
|
||||
|
||||
const files = [
|
||||
'questions.json',
|
||||
'scripts/data/set-dart-dev.json',
|
||||
'scripts/data/set-flutter-basic.json',
|
||||
'scripts/data/set-flutter-dev.json'
|
||||
];
|
||||
|
||||
files.forEach(f => processFile(path.join(__dirname, '..', f)));
|
||||
@@ -15,34 +15,5 @@ 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} 道关键题答案全部正确`);
|
||||
// 抽查逻辑已移除,因为选项顺序已被随机打乱
|
||||
|
||||
|
||||
Reference in New Issue
Block a user