diff --git a/questions.json b/questions.json index b72b4eb..f5669f3 100644 --- a/questions.json +++ b/questions.json @@ -9,12 +9,12 @@ "lv": 1, "q": "Dart 中声明\"类型自动推断\"的变量,用哪个关键字?", "opts": [ - "var", - "let", "const", - "dynamic" + "dynamic", + "var", + "let" ], - "ans": 0, + "ans": 2, "exp": "Dart 用 var 由编译器自动推断类型;let 是 JS 的;const 表示编译期常量。" }, { @@ -23,11 +23,11 @@ "q": "Dart 中表示 64 位整数的数据类型是?", "opts": [ "integer", - "int", "number", - "long" + "long", + "int" ], - "ans": 1, + "ans": 3, "exp": "Dart 用 int 表示整数(64位),double 表示浮点数,两者父类是 num。" }, { @@ -35,10 +35,10 @@ "lv": 1, "q": "以下哪个是 Dart 中正确的单行注释写法?", "opts": [ - "# 注释", "/* 注释 */", + "<!-- 注释 -->", "// 注释", - "<!-- 注释 -->" + "# 注释" ], "ans": 2, "exp": "Dart 单行注释用 //,多行用 /* */# 是 Python,#! 在 Dart 中只是 shebang 声明。" @@ -49,11 +49,11 @@ "q": "Dart 程序的入口函数是?", "opts": [ "runApp()", - "main()", "init()", + "main()", "start()" ], - "ans": 1, + "ans": 2, "exp": "入口是 main()。Flutter 中通常写成 void main() => runApp(MyApp());" }, { @@ -61,12 +61,12 @@ "lv": 1, "q": "要在控制台输出文字,应调用哪个函数?", "opts": [ - "console.log()", - "System.out.println()", "echo", - "print()" + "print()", + "System.out.println()", + "console.log()" ], - "ans": 3, + "ans": 1, "exp": "Dart 用 print() 输出到控制台,比如 print(\"Hello Dart\");" }, { @@ -74,9 +74,9 @@ "lv": 1, "q": "int x = 10; 要打印出 x=10,以下哪种写法正确?", "opts": [ - "print(\"x=\" + x);", - "print(\"x=$x\");", "print('x=${x}');", + "print(\"x=$x\");", + "print(\"x=\" + x);", "以上都可以" ], "ans": 3, @@ -87,12 +87,12 @@ "lv": 2, "q": "以下哪个 不是 Dart 的循环写法?", "opts": [ - "for (var i=0; i<5; i++)", - "while (ok)", + "do { } while (ok)", "foreach (x in list)", - "do { } while (ok)" + "while (ok)", + "for (var i=0; i<5; i++)" ], - "ans": 2, + "ans": 1, "exp": "Dart 没有 foreach(x in list) 语法,应写 for (var x in list)list.forEach(...)。" }, { @@ -101,11 +101,11 @@ "q": "执行 for (var i=0; i<5; i++) print(i); 会输出?", "opts": [ "1 2 3 4 5", - "0 1 2 3 4", "0 1 2 3 4 5", + "0 1 2 3 4", "5 4 3 2 1" ], - "ans": 1, + "ans": 2, "exp": "i 从 0 开始,条件 i<5,所以输出 0 1 2 3 4。" }, { @@ -126,12 +126,12 @@ "lv": 2, "q": "定义一个返回两数之和的函数,哪个写法正确?", "opts": [ - "int add(int a,int b){ return a+b; }", "add(int a,int b) => a+b;", - "void add(int a,int b){ return a+b; }", - "A 和 B 都可以" + "int add(int a,int b){ return a+b; }", + "A 和 B 都可以", + "void add(int a,int b){ return a+b; }" ], - "ans": 3, + "ans": 2, "exp": "A 是传统写法,B 是箭头函数简写(返回表达式),两者都合法;C 返回 void 却 return 值,编译错误。" }, { @@ -139,12 +139,12 @@ "lv": 2, "q": "以下哪个是可选命名参数的正确声明?", "opts": [ + "void f(String name=\"\") {}", "void f({String name}) {}", "void f([String name]) {}", - "void f(String? name) {}", - "void f(String name=\"\") {}" + "void f(String? name) {}" ], - "ans": 0, + "ans": 1, "exp": "{} 包裹的是命名参数(调用用 f(name: x));[] 是可选位置参数;? 是空安全;最后一个是默认值写法但不属于命名参数。" }, { @@ -166,9 +166,9 @@ "q": "Dart 中表示\"数组/列表\"的类型是?", "opts": [ "Array", - "ArrayList", + "Vector", "List", - "Vector" + "ArrayList" ], "ans": 2, "exp": "Dart 用 List(可增长数组),字面量写法 [1,2,3]。" @@ -178,12 +178,12 @@ "lv": 3, "q": "创建包含 1、2、3 的列表,正确的是?", "opts": [ - "{1, 2, 3}", "[1, 2, 3]", "(1, 2, 3)", + "{1, 2, 3}", "List(1,2,3)" ], - "ans": 1, + "ans": 0, "exp": "[] 是 List;{} 是 Set/Map;() 不是集合字面量。" }, { @@ -193,8 +193,8 @@ "opts": [ "10", "20", - "30", - "报错" + "报错", + "30" ], "ans": 1, "exp": "Dart 索引从 0 开始,list[1] 是第二个元素 20。" @@ -204,12 +204,12 @@ "lv": 3, "q": "对 List 每个元素做处理并返回一个新集合的方法是?", "opts": [ - "forEach()", "map()", - "where()", - "add()" + "forEach()", + "add()", + "where()" ], - "ans": 1, + "ans": 0, "exp": "map() 对每个元素变换并返回 Iterable;forEach() 只遍历返回 void;where() 是过滤。" }, { @@ -217,10 +217,10 @@ "lv": 3, "q": "var a=[1,2,3]; var b=[0, ...a]; 那么 b.length 是?", "opts": [ - "3", + "报错", "4", - "7", - "报错" + "3", + "7" ], "ans": 1, "exp": "...a 是扩展运算符,把 a 展开成 1,2,3,加上开头的 0,b = [0,1,2,3],长度 4。" @@ -230,12 +230,12 @@ "lv": 3, "q": "以下哪种集合类型保证元素不重复?", "opts": [ - "List", "Map", - "Set", - "Queue" + "Queue", + "List", + "Set" ], - "ans": 2, + "ans": 3, "exp": "Set 存储唯一元素,重复添加会被忽略;Map 存储键值对,键也不重复。" }, { @@ -243,9 +243,9 @@ "lv": 4, "q": "定义类 class Dog { String name; } 后,创建一个实例哪个正确?", "opts": [ - "Dog d = Dog();", - "var d = new Dog;", "Dog d = new Dog();", + "var d = new Dog;", + "Dog d = Dog();", "以上都可以" ], "ans": 3, @@ -256,10 +256,10 @@ "lv": 4, "q": "子类继承父类使用哪个关键字?", "opts": [ + "inherits", "with", - "implements", "extends", - "inherits" + "implements" ], "ans": 2, "exp": "继承用 extendsimplements 是实现接口;with 用于混入 Mixin。" @@ -269,12 +269,12 @@ "lv": 4, "q": "包含未实现方法、不能被直接实例化的类,用什么关键字声明?", "opts": [ - "abstract", + "pure", "interface", - "base", - "pure" + "abstract", + "base" ], - "ans": 0, + "ans": 2, "exp": "abstract 抽象类可含未实现方法,不能直接 new,需子类实现后实例化。" }, { @@ -283,9 +283,9 @@ "q": "在构造函数/实例方法中,this 指向?", "opts": [ "当前实例", - "父类对象", + "静态成员", "类本身", - "静态成员" + "父类对象" ], "ans": 0, "exp": "this 指向当前实例对象,用于区分参数与成员变量,如 this.name = name。" @@ -296,8 +296,8 @@ "q": "创建一个只能装 String 的 List,哪个写法正确?", "opts": [ "List<String> names = [];", - "List names = <String>[];", "var names = <String>[];", + "List names = <String>[];", "以上都是" ], "ans": 3, @@ -321,10 +321,10 @@ "lv": 5, "q": "Dart 空安全中,声明一个可能为 null 的字符串类型是?", "opts": [ - "String", - "String?", "String!", - "nullable String" + "String?", + "nullable String", + "String" ], "ans": 1, "exp": "在类型后加 ? 表示可空:String? s = null;;不加 ? 的 String 保证非空。" @@ -347,10 +347,10 @@ "lv": 5, "q": "使用 async/await 时,异步函数一般返回什么类型?", "opts": [ - "Stream<T>", + "T", "Future<T>", "void", - "T" + "Stream<T>" ], "ans": 1, "exp": "async 函数返回 Future<T>,await 会挂起等待该 Future 完成后再继续执行。" @@ -360,12 +360,12 @@ "lv": 5, "q": "执行以下代码,输出顺序是?
Future.delayed(Duration(seconds:1), () => print(\"A\"));
print(\"B\");
", "opts": [ + "只输出 A", "A 后 B", "B 后 A", - "同时输出", - "只输出 A" + "同时输出" ], - "ans": 1, + "ans": 2, "exp": "Future.delayed 是异步任务,先执行同步的 print(\"B\"),1 秒后再输出 A。" }, { @@ -373,12 +373,12 @@ "lv": 6, "q": "Flutter 中用于监听持续不断的数据流(如多次事件)的是?", "opts": [ - "Future", - "Stream", "List", - "Map" + "Future", + "Map", + "Stream" ], - "ans": 1, + "ans": 3, "exp": "Stream 可监听多次异步事件;Future 只代表一次性的结果。" }, { @@ -386,12 +386,12 @@ "lv": 6, "q": "Flutter 中构建界面的基本单位是?", "opts": [ - "Activity", - "View", + "Component", "Widget", - "Component" + "Activity", + "View" ], - "ans": 2, + "ans": 1, "exp": "Flutter 一切皆 Widget,UI 由 Widget 树组合而成(如 Text、Container、Column)。" }, { @@ -399,9 +399,9 @@ "lv": 6, "q": "界面状态需要随数据变化而更新时,应使用哪类组件?", "opts": [ - "StatelessWidget", - "StatefulWidget", "Container", + "StatefulWidget", + "StatelessWidget", "Scaffold" ], "ans": 1, @@ -418,12 +418,12 @@ "lv": 1, "q": "开发注册页时,用户输入的年龄是字符串,需要转成 int 保存。已知 String ageText = \"18\";,正确的转换写法是?", "opts": [ - "int(ageText)", - "toInt(ageText)", + "ageText.parseInt()", "int.parse(ageText)", - "ageText.parseInt()" + "int(ageText)", + "toInt(ageText)" ], - "ans": 2, + "ans": 1, "exp": "int.parse() 把字符串解析成整数,解析失败会抛 FormatException,更稳妥的是 int.tryParse()(失败返回 null)。注意 Dart 没有 \"18\".parseInt() 这种写法,int(...) 也不是转换函数。" }, { @@ -431,12 +431,12 @@ "lv": 1, "q": "项目里要定义一个\"永远不允许修改\"的 API 地址常量,以下哪种声明最合适?", "opts": [ - "final String api = \"https://api.example.com\";", "const String api = \"https://api.example.com\";", - "String api = \"https://api.example.com\";", - "var api = \"https://api.example.com\";" + "var api = \"https://api.example.com\";", + "final String api = \"https://api.example.com\";", + "String api = \"https://api.example.com\";" ], - "ans": 1, + "ans": 0, "exp": "const 是编译期常量:值在编译时就确定,且不可变,编译器可直接内联替换,还能配合 const 构造函数做实例复用。final 是运行期赋值一次后不可变。给字面量常量用 const 是更优选择。" }, { @@ -445,11 +445,11 @@ "q": "你在重构代码时写下:var x = 10; x = \"hello\"; 会发生什么?", "opts": [ "正常运行,x 变成字符串", - "编译报错:x 已被推断为 int 类型", "运行时报错", + "编译报错:x 已被推断为 int 类型", "x 自动变为 null" ], - "ans": 1, + "ans": 2, "exp": "var 声明时由初始值自动推断类型,推断后类型就固定了:x 是 int,再赋 String 在编译期直接报错。想存任意类型,应显式声明 dynamicObject?。" }, { @@ -457,12 +457,12 @@ "lv": 1, "q": "给变量起名时,以下哪个不是合法的 Dart 标识符?", "opts": [ - "_score", - "2x", + "name_2", "中文名", - "name_2" + "2x", + "_score" ], - "ans": 1, + "ans": 2, "exp": "Dart 标识符不能以数字开头_score 合法(下划线开头表示库内私有,是 Dart 的约定俗成);Dart 支持 Unicode 标识符,中文变量名同样合法。" }, { @@ -470,12 +470,12 @@ "lv": 1, "q": "结算功能计算人均消费:int total = 10; int people = 4; double avg = total / people; 打印 avg 得到?", "opts": [ - "2", - "2.5", "2.0", - "报错" + "报错", + "2", + "2.5" ], - "ans": 1, + "ans": 3, "exp": "Dart 中 int 除以 int 的结果一定是 double:10 / 4 = 2.5。想得到整除结果用 ~/10 ~/ 4 == 2,取余用 %。" }, { @@ -483,12 +483,12 @@ "lv": 2, "q": "写优惠逻辑:订单数 > 10 打 9 折,> 50 打 8 折。对 orders = 60,以下代码会得到什么折扣?
double d; if (orders > 10) { d = 0.9; } else if (orders > 50) { d = 0.8; }", "opts": [ - "0.8", "0.9", "1.0", + "0.8", "0.7" ], - "ans": 1, + "ans": 0, "exp": "if-else 链从上到下匹配,命中即止:60 > 10 先成立就直接取 0.9,后面的 else if 不再执行。正确写法应把范围大的条件放前面(先判断 > 50)。判断顺序是 if-else 链最经典的坑。" }, { @@ -496,12 +496,12 @@ "lv": 2, "q": "接口返回 Map<String, dynamic> data,想遍历所有 key 打印出来,以下哪种写法正确?", "opts": [ - "for (var k in data) print(k);", "for (var k in data.keys) print(k);", + "B 和 C 都可以", "data.keys.forEach(print);", - "B 和 C 都可以" + "for (var k in data) print(k);" ], - "ans": 3, + "ans": 1, "exp": "Map 本身不能直接 for-in 遍历,要先取 keys / values / entries。B 是标准写法;C 中 forEach(print) 利用了函数引用,同样正确。两种都行,按团队规范选择。" }, { @@ -509,12 +509,12 @@ "lv": 2, "q": "写一个返回用户名的函数:String getUserName() { },忘了写 return,会怎样?", "opts": [ - "返回 null", "编译报错", "运行时崩溃", + "返回 null", "自动返回空字符串" ], - "ans": 1, + "ans": 0, "exp": "空安全下,声明了非空返回类型 String 的函数必须有 return,漏写直接编译报错,把隐患消灭在编译期。如果确实允许无返回值,应把返回类型声明为 String?。" }, { @@ -522,12 +522,12 @@ "lv": 2, "q": "问候函数:void greet(String name, {String greeting = \"你好\"}),调用 greet(\"小明\", \"早上好\") 会发生什么?", "opts": [ - "正常执行,greeting 为 \"早上好\"", "编译报错:命名参数不能按位置传参", - "greeting 变为 null", - "编译警告但能运行" + "编译警告但能运行", + "正常执行,greeting 为 \"早上好\"", + "greeting 变为 null" ], - "ans": 1, + "ans": 0, "exp": "{} 包裹的是命名参数,必须写成 greet(\"小明\", greeting: \"早上好\")[] 才是可选位置参数。命名参数让调用意图清晰,是团队协作中常用的做法。" }, { @@ -535,10 +535,10 @@ "lv": 2, "q": "写\"重试发送验证码\"逻辑:要求至少发送一次,失败则继续重试。哪种循环语义最贴切?", "opts": [ - "while", + "repeat", "do-while", - "for", - "repeat" + "while", + "for" ], "ans": 1, "exp": "do-while 先执行循环体再判断条件,保证至少执行一次,正是\"先发一次、不行再重试\"的语义。while 先判断后执行,可能一次都不执行。" @@ -550,8 +550,8 @@ "opts": [ "remove 按值删除,removeAt 按下标删除", "两者完全相同", - "remove 删下标,removeAt 按值删除", - "remove 会删除所有等于 3 的元素" + "remove 会删除所有等于 3 的元素", + "remove 删下标,removeAt 按值删除" ], "ans": 0, "exp": "remove(value) 删除第一个值相等的元素并返回是否成功;removeAt(index) 按下标删除并返回被删的元素,越界抛 RangeError。写代码前想清楚你要删的是\"值\"还是\"位置\"。" @@ -561,10 +561,10 @@ "lv": 3, "q": "解析接口数据:用 data[\"name\"] 取一个不存在的 key 时,返回什么?", "opts": [ - "抛异常", - "null", "空字符串", - "抛出 NoSuchMethodError" + "null", + "抛出 NoSuchMethodError", + "抛异常" ], "ans": 1, "exp": "Map 用 [] 取值时,key 不存在返回 null 而不是抛异常。所以 data[\"name\"] 可能为 null,取值后判空再使用是常规操作(配合 ?? 兜底)。" @@ -574,12 +574,12 @@ "lv": 3, "q": "配置列表按条件插入元素:var l = [1, 2, if (isVip) 3];isVip 为 false 时 l 是?", "opts": [ - "[1, 2]", - "[1, 2, 3]", "[1, 2, false]", - "编译错误" + "编译错误", + "[1, 2, 3]", + "[1, 2]" ], - "ans": 0, + "ans": 3, "exp": "这是 collection-if 语法:条件不成立时该元素直接不生成。Flutter 里常用于条件渲染组件列表,配合 ... 展开和 collection-for 一起使用。" }, { @@ -587,12 +587,12 @@ "lv": 3, "q": "两个用户各自关注了一批标签,要找出共同关注的标签,用哪种写法?", "opts": [ - "a & b", - "a.intersect(b)", "a.toSet().intersection(b.toSet())", - "a.union(b)" + "a.union(b)", + "a & b", + "a.intersect(b)" ], - "ans": 2, + "ans": 0, "exp": "List 没有交集方法,要先转 Set。intersection() 返回两个集合的共同元素;union() 是并集。注意方法名是 intersection(),不是其它语言的 intersect()。" }, { @@ -601,11 +601,11 @@ "q": "计算购物车总价:List<double> prices = []; 空列表调用 prices.reduce((a, b) => a + b) 会?", "opts": [ "返回 0", - "抛出 StateError 异常", + "返回空列表", "返回 null", - "返回空列表" + "抛出 StateError 异常" ], - "ans": 1, + "ans": 3, "exp": "reduce 要求列表至少有一个元素,空列表直接抛 \"Bad state: No element\"。更稳的写法是 fold(0, (a, b) => a + b):带初始值,空列表返回 0。" }, { @@ -613,12 +613,12 @@ "lv": 4, "q": "写日期工具类,希望 DateUtil.parse(\"2024-01-01\") 在字符串不合法时返回 null 而不是抛异常,用哪种构造函数?", "opts": [ - "factory 构造函数", + "const 构造函数", "普通构造函数", - "命名构造函数", - "const 构造函数" + "factory 构造函数", + "命名构造函数" ], - "ans": 0, + "ans": 2, "exp": "factory 构造函数可以不返回新实例——可以返回缓存实例、子类实例,甚至返回 null。它把\"按需创建/校验\"的逻辑封装在构造入口,调用方写法统一且安全。" }, { @@ -626,12 +626,12 @@ "lv": 4, "q": "日志打印、权限检查这类能力要被多个互不相关的类复用,又不想强制继承关系,Dart 的推荐做法是?", "opts": [ - "extends 继承", "implements 实现", - "with 混入 Mixin", - "abstract 抽象类" + "extends 继承", + "abstract 抽象类", + "with 混入 Mixin" ], - "ans": 2, + "ans": 3, "exp": "Mixinwith)把可复用逻辑抽出来按需混入,避免\"为了复用而强行继承\"的长继承链。Flutter 源码大量使用,如 class _X with RouteAware。" }, { @@ -639,10 +639,10 @@ "lv": 4, "q": "类内部要声明一个\"仅供本文件使用\"的私有字段,正确写法是?", "opts": [ - "private String _token;", - "String _token;", "-String token;", - "#token" + "String _token;", + "#token", + "private String _token;" ], "ans": 1, "exp": "Dart 没有 private 关键字,以下划线开头表示库内私有:_token。注意这是\"库\"级别私有(同文件可见),不是类级别。" @@ -652,12 +652,12 @@ "lv": 4, "q": "用户类的 name 字段希望赋值时自动去掉首尾空格,最佳做法是?", "opts": [ - "声明 getter/setter,在 setter 里 trim", "直接写公共字段,调用方自己 trim", - "用 final 禁止修改", - "用 static 修饰" + "声明 getter/setter,在 setter 里 trim", + "用 static 修饰", + "用 final 禁止修改" ], - "ans": 0, + "ans": 1, "exp": "setter 在赋值入口统一处理,所有调用方(包括构造函数)都自动享受规范化,不用每个地方都记得 trim。写法:String get nameset name(String v)。" }, { @@ -665,12 +665,12 @@ "lv": 4, "q": "class Dog implements Animalextends 继承的关键区别是?", "opts": [ - "implements 要求实现 Animal 的全部成员(含字段),extends 可复用已有实现", "两者完全一样", - "implements 只能用于抽象类", - "extends 要求实现全部成员" + "implements 要求实现 Animal 的全部成员(含字段),extends 可复用已有实现", + "extends 要求实现全部成员", + "implements 只能用于抽象类" ], - "ans": 0, + "ans": 1, "exp": "implements 把 Animal 当接口:所有成员都要自己重写,即使父类已有实现;extends 继承实现,只覆写需要改的部分。implements 用于\"规定能力\",extends 用于\"复用实现\"。" }, { @@ -678,12 +678,12 @@ "lv": 5, "q": "用户对象可能为 null:User? u = await fetchUser();,以下哪种使用方式更安全?", "opts": [ - "if (u != null) { print(u.name); } 判空后直接用 u", "if (u != null) { print(u!.name); } 必须加 !", - "print(u?.name ?? \"\");", - "A 和 C 都可以" + "A 和 C 都可以", + "if (u != null) { print(u.name); } 判空后直接用 u", + "print(u?.name ?? \"\");" ], - "ans": 3, + "ans": 1, "exp": "A:局部变量判空后编译器会自动提升为非空类型,直接使用即可;C?. 安全调用 + ?? 兜底,一行搞定。只有被闭包捕获的变量才需要 !。" }, { @@ -691,12 +691,12 @@ "lv": 5, "q": "详情页要同时请求\"用户信息\"和\"订单列表\"两个接口,都拿到再渲染,用哪个方法?", "opts": [ - "Future.wait([fetchUser(), fetchOrders()])", - "串行写两个 await", "Future.any", - "Future.delayed 等待" + "Future.delayed 等待", + "Future.wait([fetchUser(), fetchOrders()])", + "串行写两个 await" ], - "ans": 0, + "ans": 2, "exp": "Future.wait 并发启动多个 Future,全部完成后统一返回结果列表,总耗时约等于最慢的那个;串行 await 则是两个请求耗时相加。Future.any 只等第一个完成的。" }, { @@ -705,9 +705,9 @@ "q": "某个字段要等登录成功后才能赋值,但又想声明成非空类型,正确声明方式是?", "opts": [ "late String token;", + "final token = null;", "String? token = \"\";", - "dynamic token;", - "final token = null;" + "dynamic token;" ], "ans": 0, "exp": "late 表示\"推迟初始化,首次访问时才赋值\",适合依赖异步结果的字段或计算代价高的属性。注意:late 变量在赋值前被访问会抛 LateInitializationError。" @@ -717,12 +717,12 @@ "lv": 5, "q": "把解析结果转成目标类型:var age = data[\"age\"] as int;,当 age 实际是 String 时会发生?", "opts": [ - "返回 null", - "抛出 TypeError(类型转换失败)", "静默忽略", - "自动转成 int" + "返回 null", + "自动转成 int", + "抛出 TypeError(类型转换失败)" ], - "ans": 1, + "ans": 3, "exp": "as强制类型转换,类型不符立即抛异常。安全做法:先用 is 判断类型,或先统一规范数据(如 num 一律 .toInt())。" }, { @@ -731,11 +731,11 @@ "q": "对可能为 null 的 String? s 使用 s!,若 s 确实为 null 会?", "opts": [ "返回 null", - "抛出 TypeError(空检查失败)", "静默继续", + "抛出 TypeError(空检查失败)", "编译错误" ], - "ans": 1, + "ans": 2, "exp": "!强制解包运算符,向编译器承诺\"这里不可能为 null\",承诺落空时运行时抛 TypeError。能用判空提升或 ?? 的地方尽量别用 !,减少运行时崩溃点。" }, { @@ -743,12 +743,12 @@ "lv": 6, "q": "Flutter 中常见 ButtonStyle()..backgroundColor = ... ..shape = ... 写法,.. 级联操作符的核心行为是?", "opts": [ - "对同一对象连续操作且返回对象本身", "链式调用并返回最后一次结果", + "与 ... 展开等价", "只能用于 List", - "与 ... 展开等价" + "对同一对象连续操作且返回对象本身" ], - "ans": 0, + "ans": 3, "exp": "级联操作符 .. 对同一对象连续执行多个操作,不改变返回值(始终是原对象),适合集中配置对象。. 返回的是方法调用的返回值,两者行为不同。" }, { @@ -757,11 +757,11 @@ "q": "Dart 3 中,函数想直接返回\"用户名 + 年龄\"这组小数据,又不想专门建一个类,推荐用?", "opts": [ "Map<String, dynamic>", - "Record 记录类型:(String name, int age)", + "用全局变量", "List<dynamic>", - "用全局变量" + "Record 记录类型:(String name, int age)" ], - "ans": 1, + "ans": 3, "exp": "Record 是 Dart 3 的轻量聚合类型:var r = (\"张三\", 18); 或命名字段 (name: \"张三\", age: 18),用 r.$1 / r.name 取值,还能配合模式匹配解构,比 Map 更类型安全。" }, { @@ -769,12 +769,12 @@ "lv": 6, "q": "Dart 3 的 switch 可以直接当表达式赋值:var s = switch (status) { \"ok\" => \"成功\", _ => \"未知\" }; 这种写法?", "opts": [ + "必须每个分支加 break", "合法:switch 表达式返回一个值", "非法:switch 不能作表达式", - "必须每个分支加 break", "只能匹配字符串" ], - "ans": 0, + "ans": 1, "exp": "switch 表达式=> 代替冒号,整体作为值赋给变量,配合模式匹配可以解构、判断类型,比旧式 switch 简洁,也天然避免了漏 break 的坑。" }, { @@ -783,9 +783,9 @@ "q": "想给系统类 String 加一个 isEmail() 方法,但 String 不能改源码,用?", "opts": [ "extension 扩展:extension EmailCheck on String { bool isEmail() {...} }", - "继承 String", + "写静态工具类", "用 mixin 混入", - "写静态工具类" + "继承 String" ], "ans": 0, "exp": "extension 可以不修改原类给已有类型添加方法,之后 \"a@b.com\".isEmail() 直接调用。注意扩展方法是静态分发(编译期决定),不参与运行时多态。" @@ -795,12 +795,12 @@ "lv": 6, "q": "泛型容器 class Box<T extends num> {}extends num 的含义是?", "opts": [ - "T 只能是 int、double 等数值类型", "T 可以是任意类型", - "T 必须可空", - "T 必须是集合类型" + "T 只能是 int、double 等数值类型", + "T 必须是集合类型", + "T 必须可空" ], - "ans": 0, + "ans": 1, "exp": "泛型约束限定 T 必须是 num 及其子类(int/double),这样 Box 内部就能安全调用数值运算。不加约束时 T 等价于 Object?。" } ], @@ -822,12 +822,12 @@ "lv": 1, "q": "新建 Flutter 项目后,把应用真正跑起来的入口代码是?", "opts": [ - "void main() => runApp(MyApp());", "void main() { MyApp(); }", - "runApp() { MyApp }", - "main() { MaterialApp(); }" + "void main() => runApp(MyApp());", + "main() { MaterialApp(); }", + "runApp() { MyApp }" ], - "ans": 0, + "ans": 1, "exp": "Dart 程序入口是 main(),Flutter 通过 runApp(Widget) 把根组件挂载到引擎上。注意 MyApp() 是创建 Widget 实例,不能少了括号。" }, { @@ -835,12 +835,12 @@ "lv": 1, "q": "实现一个\"3 秒倒计时\"按钮,每秒刷新显示剩余秒数,应使用?", "opts": [ + "Container", "StatelessWidget", "StatefulWidget", - "Container", "Scaffold" ], - "ans": 1, + "ans": 2, "exp": "有动态变化的数据(倒计时数字)就需要 StatefulWidget:数据保存在 State 里,变化时调 setState() 刷新。没有可变状态时应优先用 StatelessWidget。" }, { @@ -848,12 +848,12 @@ "lv": 1, "q": "界面上要显示一行固定文案\"登录成功\",最合适的组件是?", "opts": [ - "Text", "Label", - "Paragraph", - "RichText" + "RichText", + "Text", + "Paragraph" ], - "ans": 0, + "ans": 2, "exp": "Text 是最常用的文本组件,可通过 style 配置字号、颜色、字重等。RichText 用于一段文字中混排多种样式的富文本场景。" }, { @@ -861,12 +861,12 @@ "lv": 1, "q": "点击\"确认\"按钮后分数要 +1 并立即显示到界面,正确流程是?", "opts": [ - "修改 State 字段后调用 setState()", - "直接修改字段就行", + "修改 Widget 的构造参数", "重启整个 App", - "修改 Widget 的构造参数" + "直接修改字段就行", + "修改 State 字段后调用 setState()" ], - "ans": 0, + "ans": 3, "exp": "State 里的字段只是普通数据,必须调用 setState() 通知框架\"数据变了\",框架才会重新执行 build 刷新 UI。注意 setState 只重建当前 State 的子树,不是整棵树。" }, { @@ -874,12 +874,12 @@ "lv": 1, "q": "要展示 10000 条商品记录,性能最优的做法是?", "opts": [ - "ListView.builder(按需构建可见项)", - "Column 里循环放 10000 个组件", "SingleChildScrollView 包 Column", - "GridView.count 全部构建" + "Column 里循环放 10000 个组件", + "GridView.count 全部构建", + "ListView.builder(按需构建可见项)" ], - "ans": 0, + "ans": 3, "exp": "ListView.builder 只构建当前可见的列表项(懒加载),滚动时回收复用;Column + for 会一次性构建全部组件,万条数据直接卡死。数据量大优先 builder 系列。" }, { @@ -887,12 +887,12 @@ "lv": 2, "q": "登录页要垂直排列\"输入框、输入框、登录按钮\",用哪个布局组件?", "opts": [ - "Column", "Row", "Stack", - "Wrap" + "Wrap", + "Column" ], - "ans": 0, + "ans": 3, "exp": "Column 垂直方向排列子组件,Row 水平方向,Stack 叠加并支持定位,Wrap 在空间不足时自动换行。" }, { @@ -900,12 +900,12 @@ "lv": 2, "q": "Row 里想让一个按钮占满剩余宽度(剩余空间全给它),按钮外层套?", "opts": [ - "Expanded", - "Padding", "Center", - "Spacer" + "Padding", + "Spacer", + "Expanded" ], - "ans": 0, + "ans": 3, "exp": "Expanded 让子组件占据主轴的剩余空间(Row 的主轴是水平方向)。多个 Expanded 可通过 flex 参数按比例分配空间。" }, { @@ -913,12 +913,12 @@ "lv": 2, "q": "Column 里有几个按钮,想让它们在水平方向居中,应该设置?", "opts": [ - "crossAxisAlignment: CrossAxisAlignment.center", "mainAxisAlignment: MainAxisAlignment.center", - "alignment: Alignment.center", - "textAlign: TextAlign.center" + "textAlign: TextAlign.center", + "crossAxisAlignment: CrossAxisAlignment.center", + "alignment: Alignment.center" ], - "ans": 0, + "ans": 2, "exp": "Column 的主轴是垂直方向、交叉轴是水平方向。水平居中=沿交叉轴居中=crossAxisAlignment.center;mainAxisAlignment 控制的是垂直方向。主/交叉轴搞反是新手最常踩的坑。" }, { @@ -927,8 +927,8 @@ "q": "封面图要铺满整个区域且不拉伸变形(多出的部分裁掉),fit 参数应该用?", "opts": [ "BoxFit.cover", - "BoxFit.fill", "BoxFit.none", + "BoxFit.fill", "BoxFit.scaleDown" ], "ans": 0, @@ -939,12 +939,12 @@ "lv": 2, "q": "商品名过长时希望单行显示,超出部分显示省略号,Text 的配置是?", "opts": [ - "maxLines: 1, overflow: TextOverflow.ellipsis", - "只设 overflow: TextOverflow.clip", + "softWrap: false 就够了", "只设 maxLines: 3", - "softWrap: false 就够了" + "maxLines: 1, overflow: TextOverflow.ellipsis", + "只设 overflow: TextOverflow.clip" ], - "ans": 0, + "ans": 2, "exp": "maxLines 限制最大行数 + overflow: TextOverflow.ellipsis 指定超出的表现(省略号/裁剪/fade),两者配合才能出现\"…\"效果。" }, { @@ -952,12 +952,12 @@ "lv": 3, "q": "输入框需要\"预填默认值 + 按钮点击后读取内容 + 随时清空\",推荐用?", "opts": [ + "无法实现", "TextEditingController", "每次读 onChanged 参数", - "GlobalKey 临时查找", - "无法实现" + "GlobalKey 临时查找" ], - "ans": 0, + "ans": 1, "exp": "TextEditingController 是输入框的\"数据源\":TextEditingController(text: \"预填\") 初始化,controller.text 读取,controller.clear() 清空。用完后在 dispose 里释放。" }, { @@ -965,12 +965,12 @@ "lv": 3, "q": "页面销毁时,取消监听、释放 Controller 的代码应该写在哪里?", "opts": [ - "dispose()", - "initState()", "build()", - "didChangeDependencies()" + "initState()", + "didChangeDependencies()", + "dispose()" ], - "ans": 0, + "ans": 3, "exp": "dispose() 是 State 被移除时的清理钩子:取消 Timer、Stream 订阅、释放 Controller。不清理会造成内存泄漏和 \"setState called after dispose\" 报错。" }, { @@ -978,12 +978,12 @@ "lv": 3, "q": "登录状态、用户信息要被多个页面共享,又不想层层传构造参数,项目常用方案是?", "opts": [ + "每个页面重新请求接口", "Provider / InheritedWidget", "定义全局变量直接读写", - "每个页面重新请求接口", "把状态塞进路由参数" ], - "ans": 0, + "ans": 1, "exp": "Flutter 官方的底层方案是 InheritedWidgetProvider 是对它的优雅封装:共享数据放顶层,子组件 context.watch() 取用并自动刷新。全局变量没有通知机制,页面不会自动更新。" }, { @@ -991,12 +991,12 @@ "lv": 3, "q": "商品列表页要支持下拉刷新,应该把 ListView 包进哪个组件?", "opts": [ - "RefreshIndicator", "GestureDetector", + "RefreshIndicator", "AnimatedList", "Scrollbar" ], - "ans": 0, + "ans": 1, "exp": "RefreshIndicator 包住可滚动组件,配合 onRefresh 返回的 Future 实现下拉刷新动画。这是 Material 规范自带的下拉刷新方案。" }, { @@ -1004,12 +1004,12 @@ "lv": 3, "q": "App 切到后台、再回到前台时,需要暂停/恢复计时器,通过什么监听?", "opts": [ + "NavigatorObserver", "WidgetsBindingObserver", "RouteObserver", - "NavigatorObserver", "MediaQuery" ], - "ans": 0, + "ans": 1, "exp": "给 State 混入 with WidgetsBindingObserver,在 didChangeAppLifecycleState() 中监听 AppLifecycleState(paused / resumed / inactive),即可感知前后台切换。" }, { @@ -1043,12 +1043,12 @@ "lv": 4, "q": "跳转时需要传递商品 id、名称等参数,团队协作中最推荐的传参方式是?", "opts": [ + "static 静态字段存", "DetailPage(id: 1, name: \"商品\") 构造参数传递", "全局变量临时存", - "static 静态字段存", "URL 字符串拼接" ], - "ans": 0, + "ans": 1, "exp": "构造参数显式、类型安全、可读性好——跳转处一眼看到传了什么。全局变量/静态字段是隐藏依赖,页面多起来后非常难维护。" }, { @@ -1056,12 +1056,12 @@ "lv": 4, "q": "在 App 里注册命名路由(如 /login/home),配置位置是?", "opts": [ - "MaterialApp(routes: {...})", - "main() 函数里", + "每个页面里各自声明", "runApp() 参数", - "每个页面里各自声明" + "MaterialApp(routes: {...})", + "main() 函数里" ], - "ans": 0, + "ans": 2, "exp": "命名路由集中配置在 MaterialApproutes 表,之后用 Navigator.pushNamed(context, \"/login\") 跳转;需要传参时配合 arguments 参数。" }, { @@ -1069,12 +1069,12 @@ "lv": 4, "q": "跳到选择页,用户选完返回后要根据选择结果继续处理,列表页怎么写?", "opts": [ - "final r = await Navigator.push(...); 拿到返回值再处理", + "跳转时把回调函数传过去", "无法等待结果", - "用 setState 轮询", - "跳转时把回调函数传过去" + "final r = await Navigator.push(...); 拿到返回值再处理", + "用 setState 轮询" ], - "ans": 0, + "ans": 2, "exp": "push 返回 Future<T>,await 后拿到 pop 回传的值,再执行后续逻辑(如刷新选中项)。这是\"页面间取结果\"的标准姿势。" }, { @@ -1084,8 +1084,8 @@ "opts": [ "dynamic(实际可能是 Map 或 List)", "String", - "Map<String, dynamic>", - "Object" + "Object", + "Map<String, dynamic>" ], "ans": 0, "exp": "jsonDecode 的返回类型是 dynamic:JSON 对象变 Map<String, dynamic>、数组变 List<dynamic>、数字/布尔原样返回。通常需要手动转换:jsonDecode(text) as Map<String, dynamic>。" @@ -1095,12 +1095,12 @@ "lv": 5, "q": "要把用户对象 User(id: 1, name: \"张三\") 转成 JSON 字符串发给服务器,正确流程是?", "opts": [ - "给 User 写 toJson(),再 jsonEncode(user.toJson())", - "jsonEncode(user) 直接序列化", "user.toString()", + "jsonEncode(user) 直接序列化", + "给 User 写 toJson(),再 jsonEncode(user.toJson())", "JSON.stringify(user)" ], - "ans": 0, + "ans": 2, "exp": "Dart 默认不会自动序列化任意对象,惯例是模型类自己实现 Map<String, dynamic> toJson(),再用 jsonEncode 转字符串;反序列化则对应 User.fromJson(Map) 工厂构造。" }, { @@ -1108,12 +1108,12 @@ "lv": 5, "q": "网络请求可能超时/断网,用户点\"登录\"时抛出了异常,正确的处理是?", "opts": [ - "用 try-catch 包裹 await 请求,捕获异常并提示用户", "不做处理", "打印日志后忽略", + "用 try-catch 包裹 await 请求,捕获异常并提示用户", "延迟 3 秒自动重试" ], - "ans": 0, + "ans": 2, "exp": "try { await api.login(...) } catch (e) { 提示\"网络异常,请重试\" } 是最基础的错误处理;生产环境还会区分 SocketException(断网)、TimeoutException(超时)等做分类提示。" }, { @@ -1123,8 +1123,8 @@ "opts": [ "CircularProgressIndicator", "Text(\"加载中...\")", - "Opacity 半透明遮罩", - "AnimatedContainer 闪烁" + "AnimatedContainer 闪烁", + "Opacity 半透明遮罩" ], "ans": 0, "exp": "Material 标准的加载指示器是 CircularProgressIndicator(转圈)和 LinearProgressIndicator(条),配合 loading 状态变量做条件渲染。" @@ -1134,12 +1134,12 @@ "lv": 5, "q": "http 请求要设置 10 秒超时,正确写法是?", "opts": [ + "Timer(Duration(seconds: 10), ...)", "await http.get(url).timeout(Duration(seconds: 10))", "http.get(url, timeout: 10)", - "Timer(Duration(seconds: 10), ...)", "Future.any 自动超时" ], - "ans": 0, + "ans": 1, "exp": "Future 自带 .timeout():超时抛 TimeoutException,可再接 onTimeout 参数兜底。http 包本身没有 timeout 参数,这是常见的误解。" }, { @@ -1147,12 +1147,12 @@ "lv": 6, "q": "首页同时请求\"轮播图、公告、商品列表\"三个接口,全部完成再渲染页面,用?", "opts": [ - "Future.wait([...]) 并发等待", - "三个 await 串行", + "用 Future.any", "依次 catch 分别显示", - "用 Future.any" + "Future.wait([...]) 并发等待", + "三个 await 串行" ], - "ans": 0, + "ans": 2, "exp": "并发场景用 Future.wait,总耗时约等于最慢的请求而不是三个相加。生产上可配合记录单个请求失败做局部降级(某块失败不影响整页)。" }, { @@ -1160,12 +1160,12 @@ "lv": 6, "q": "商品列表滑到底部要自动加载下一页,监听滚动的方案是?", "opts": [ - "ScrollController 监听 pixels 接近 maxScrollExtent 时触发", "用 Timer 每 3 秒加载", "下拉刷新代替", - "GestureDetector 监听手势" + "GestureDetector 监听手势", + "ScrollController 监听 pixels 接近 maxScrollExtent 时触发" ], - "ans": 0, + "ans": 3, "exp": "给 ListView 挂 ScrollController,在 addListener 里判断 position.pixels >= position.maxScrollExtent - 200(预留阈值)就加载下一页,配合 loading 状态防止重复触发。" }, { @@ -1173,12 +1173,12 @@ "lv": 6, "q": "商品标题中要红色高亮\"热卖\"二字、其余部分灰色,用?", "opts": [ - "RichText + TextSpan 组合不同样式", - "拆成多个 Text 排一排", "用换行符拼接字符串", + "拆成多个 Text 排一排", + "RichText + TextSpan 组合不同样式", "TextStyle 做不到,只能整段同色" ], - "ans": 0, + "ans": 2, "exp": "RichTextTextSpan 支持树状嵌套:每个 span 可以有自己的 style,是\"一段文字多种样式\"的标准方案。" }, { @@ -1186,12 +1186,12 @@ "lv": 6, "q": "界面出现黄黑条纹溢出警告(RenderFlex overflowed),说明?", "opts": [ - "子组件超出了父组件给定的约束空间", + "字体加载失败", "主题颜色配置错误", "内存泄漏", - "字体加载失败" + "子组件超出了父组件给定的约束空间" ], - "ans": 0, + "ans": 3, "exp": "Row/Column 空间不够时子组件溢出,debug 模式显示黄黑条纹并打印 \"RenderFlex overflowed by XX pixels\"。解决思路:Expanded/Flexible 让出空间、SingleChildScrollView 滚动、FittedBox 缩放。" }, { @@ -1199,12 +1199,12 @@ "lv": 6, "q": "瀑布流商品图列表很卡、图片解码占内存高,优先优化手段是?", "opts": [ - "cacheWidth / ResizeImage 限制解码尺寸", "给图片加圆角", - "提高图片的清晰度", - "减少列表项数量" + "cacheWidth / ResizeImage 限制解码尺寸", + "减少列表项数量", + "提高图片的清晰度" ], - "ans": 0, + "ans": 1, "exp": "Image.network 默认按原图尺寸解码,一张 4000px 大图在手机上就是内存杀手。用 cacheWidth: 300ResizeImage 让引擎按需解码成小尺寸位图,内存占用和滚动性能立竿见影。" } ], @@ -1234,12 +1234,12 @@ "lv": 1, "q": "Dart 中布尔类型 bool 的字面量写法是?", "opts": [ - "TRUE", - "true", + "yes", "1", - "yes" + "true", + "TRUE" ], - "ans": 1, + "ans": 2, "exp": "bool 只有两个值:truefalse(全小写)。TRUE、1、yes 都不是 Dart 的合法写法。" }, { @@ -1247,12 +1247,12 @@ "lv": 1, "q": "要存储 3.14 这样的浮点数,应声明为什么类型?", "opts": [ - "float", "decimal", + "double", "big", - "double" + "float" ], - "ans": 3, + "ans": 1, "exp": "浮点数用 double,整数用 int,两者的共同父类是 num。Dart 没有 float、decimal 类型。" }, { @@ -1260,12 +1260,12 @@ "lv": 1, "q": "定义一个内容跨多行的字符串,应该用哪组引号包裹?", "opts": [ + "' 单个单引号", "''' 三个单引号(或 \"\"\")", "\" 单个双引号", - "' 单个单引号", "` 反引号" ], - "ans": 0, + "ans": 1, "exp": "三个单引号 ''' 或三个双引号 \"\"\" 包裹的字符串可以直接跨行书写,换行会被保留;单引号/双引号字符串内不能直接换行。Dart 没有反引号模板字符串。" }, { @@ -1273,12 +1273,12 @@ "lv": 1, "q": "关于 Dart 字符串的特性,以下说法正确的是?", "opts": [ - "字符串创建后可随意修改内部字符", - "字符串只能用双引号包裹", "字符串方法返回新字符串,原字符串不变", + "字符串只能用双引号包裹", + "字符串创建后可随意修改内部字符", "字符串本质是数字类型" ], - "ans": 2, + "ans": 0, "exp": "Dart 字符串不可变toUpperCase() 等方法返回新字符串,原值不变。频繁拼接字符串时建议用 StringBuffer 提升性能。" }, { @@ -1288,8 +1288,8 @@ "opts": [ "s.upper()", "s.toUpperCase()", - "s.up()", - "String.upper(s)" + "String.upper(s)", + "s.up()" ], "ans": 1, "exp": "转大写用 s.toUpperCase(),转小写用 s.toLowerCase()。注意返回的是新字符串,原字符串不变。" @@ -1299,8 +1299,8 @@ "lv": 1, "q": "把数字 42 转成字符串 \"42\",正确写法是?", "opts": [ - "(String) 42", "int.stringify(42)", + "(String) 42", "42.toString()", "String.valueOf(42)" ], @@ -1312,12 +1312,12 @@ "lv": 2, "q": "取 a、b 中较大的值,用一行表达式应写?", "opts": [ - "a ?? b", - "a > b ? a : b", + "a && b", "a || b", - "a && b" + "a > b ? a : b", + "a ?? b" ], - "ans": 1, + "ans": 2, "exp": "三元运算符 条件 ? 表达式1 : 表达式2:条件成立取第一个,否则取第二个。a ?? b 是空值合并(a 为 null 才取 b),|| 和 && 是逻辑运算,都不能比较大小。" }, { @@ -1325,12 +1325,12 @@ "lv": 2, "q": "bool ok = false; print(ok && check()); 中 check() 会被执行吗?", "opts": [ - "会执行", - "报错", + "执行两次", "不会执行,&& 左侧为 false 时短路", - "执行两次" + "会执行", + "报错" ], - "ans": 2, + "ans": 1, "exp": "短路求值&& 左侧为 false 时结果已确定为 false,右侧不会执行|| 左侧为 true 时同理。常用于安全判断:user != null && user.isVip。" }, { @@ -1338,12 +1338,12 @@ "lv": 2, "q": "循环里要跳过当前这一次迭代、继续下一次,用哪个关键字?", "opts": [ - "break", - "return", "continue", - "skip" + "skip", + "return", + "break" ], - "ans": 2, + "ans": 0, "exp": "continue 结束本次迭代、立即进入下一轮;break 直接退出整个循环;return 退出的是整个函数。" }, { @@ -1351,9 +1351,9 @@ "lv": 2, "q": "var f = (x) => x * 2; 这种没有名字的函数写法是?", "opts": [ - "命名函数", "构造函数", "静态函数", + "命名函数", "匿名函数(Lambda 表达式)" ], "ans": 3, @@ -1364,12 +1364,12 @@ "lv": 2, "q": "void greet(String name, [String word = \"你好\"]),调用 greet(\"小明\") 时 word 的值是?", "opts": [ - "null", "报错", "\"你好\"", - "空字符串" + "空字符串", + "null" ], - "ans": 2, + "ans": 1, "exp": "[ ] 包裹的是可选位置参数,不传时使用默认值 \"你好\"。它与命名参数({} 包裹、必须按名传)不同,是按顺序传参的:greet(\"小明\", \"早上好\")。" }, { @@ -1377,12 +1377,12 @@ "lv": 2, "q": "循环里要立即终止整个循环,用哪个关键字?", "opts": [ + "stop", "break", "continue", - "exit", - "stop" + "exit" ], - "ans": 0, + "ans": 1, "exp": "break 立即结束当前循环;continue 只跳过本次。break 也常用于 switch 的 case 结尾——Dart 要求非空 case 必须以 break、return、throw 等结束。" }, { @@ -1392,10 +1392,10 @@ "opts": [ "Set", "List", - "Map", - "Tuple" + "Tuple", + "Map" ], - "ans": 2, + "ans": 3, "exp": "{} 且含冒号键值对时创建 Map;无冒号的 {1, 2, 3} 是 Set;[] 是 List。Dart 没有 Tuple 类型。" }, { @@ -1403,12 +1403,12 @@ "lv": 3, "q": "var map = {'a': 1}; map['b'] = 2; 之后 map 的内容是?", "opts": [ - "{'a': 1}", "{'b': 2}", - "报错", - "{'a': 1, 'b': 2}" + "{'a': 1, 'b': 2}", + "{'a': 1}", + "报错" ], - "ans": 3, + "ans": 1, "exp": "map[key] = value 写入时,key 已存在则更新值,不存在则新增条目,所以得到 {'a': 1, 'b': 2}。" }, { @@ -1416,10 +1416,10 @@ "lv": 3, "q": "['2024', '2025', '2026'].join('-') 的结果是?", "opts": [ + "'202420252026'", "'2024,2025,2026'", - "报错", "'2024-2025-2026'", - "'202420252026'" + "报错" ], "ans": 2, "exp": "join(分隔符) 把列表元素按分隔符拼成一个字符串,得到 2024-2025-2026。注意不传参数时是无分隔符直接拼接。" @@ -1429,12 +1429,12 @@ "lv": 3, "q": "从 [1, 2, 3, 4] 中挑出偶数(2、4),应调用哪个方法?", "opts": [ - "map", - "reduce", "addAll", - "where" + "where", + "map", + "reduce" ], - "ans": 3, + "ans": 1, "exp": "where() 按条件过滤[1,2,3,4].where((n) => n % 2 == 0) 得到 2、4。map() 是变换每个元素,reduce() 是聚合,addAll() 是批量添加。" }, { @@ -1443,11 +1443,11 @@ "q": "var nums = [3, 1, 2]; nums.sort(); 排序后 nums 是?", "opts": [ "[3, 1, 2]", - "[1, 2, 3]", "报错", - "[2, 3, 1]" + "[2, 3, 1]", + "[1, 2, 3]" ], - "ans": 1, + "ans": 3, "exp": "sort() 原地升序排序,结果 [1, 2, 3]。注意它直接修改原列表并返回 void;要降序需传比较器:sort((a, b) => b - a)。" }, { @@ -1455,12 +1455,12 @@ "lv": 3, "q": "[1, 2, 3].contains(5) 返回?", "opts": [ - "true", - "5", "null", - "false" + "true", + "false", + "5" ], - "ans": 3, + "ans": 2, "exp": "contains() 判断是否包含指定元素,包含返回 true、否则 false。判断 Map 的键用 containsKey,判断值用 containsValue。" }, { @@ -1468,12 +1468,12 @@ "lv": 4, "q": "class Point { Point(this.x, this.y); int x; int y; } 中 this.x 的作用是?", "opts": [ - "创建私有变量", - "引用父类构造函数", "把构造参数自动赋给同名成员变量", - "声明静态字段" + "声明静态字段", + "引用父类构造函数", + "创建私有变量" ], - "ans": 2, + "ans": 0, "exp": "构造参数写成 this.x 时,Dart 会把参数值自动赋给同名成员变量,省去手写 this.x = x; 的样板代码。" }, { @@ -1481,12 +1481,12 @@ "lv": 4, "q": "想给 Point 提供便捷构造 Point.origin()(x、y 初始化为 0),应该用?", "opts": [ - "静态方法", + "命名构造函数", "构造重载", - "默认构造函数", - "命名构造函数" + "静态方法", + "默认构造函数" ], - "ans": 3, + "ans": 0, "exp": "命名构造函数Point.origin() : x = 0, y = 0;,调用 Point.origin()。Dart 不支持构造重载,用命名构造区分多种初始化方式。" }, { @@ -1494,9 +1494,9 @@ "lv": 4, "q": "统计 Point 类一共创建了多少个实例,计数变量应声明为?", "opts": [ - "final int count", - "static int count", "int count", + "static int count", + "final int count", "late int count" ], "ans": 1, @@ -1507,12 +1507,12 @@ "lv": 4, "q": "私有字段 _name 想对外只读(能读不能写),应该声明?", "opts": [ - "void name() {}", - "static String name;", + "set String name;", "String get name => _name;", - "set String name;" + "static String name;", + "void name() {}" ], - "ans": 2, + "ans": 1, "exp": "getter 提供只读访问:String get name => _name;,外部用 p.name 读取;需要可写时再补 set name(String v)。" }, { @@ -1520,12 +1520,12 @@ "lv": 4, "q": "@override 注解放在重写方法上的作用是?", "opts": [ + "删除父类方法", "调用父类方法", "声明重写父类方法,签名不符会编译报错", - "创建新方法", - "删除父类方法" + "创建新方法" ], - "ans": 1, + "ans": 2, "exp": "@override 声明\"此方法重写了父类/接口方法\",签名不一致时编译报错,防止手滑写错。调用父类实现用 super.xxx()。" }, { @@ -1533,12 +1533,12 @@ "lv": 4, "q": "子类构造函数里显式调用父类构造函数,用哪个关键字?", "opts": [ - "this", - "base", "super", - "parent" + "parent", + "this", + "base" ], - "ans": 2, + "ans": 0, "exp": "用 superDog(String n) : super(n);。父类有默认构造时可省略(自动调用);需要传参或指定父类命名构造时才显式写 super。" }, { @@ -1547,11 +1547,11 @@ "q": "String? s;(可能为 null),直接写 print(s.length) 会?", "opts": [ "输出 0", - "输出 null", "编译报错:可空类型不能直接调用方法", + "输出 null", "正常运行" ], - "ans": 2, + "ans": 1, "exp": "空安全下 String? 可能为 null,不能直接调用方法,编译期直接报错——这是空安全的核心价值:把空指针问题拦在编译期。需先判空,或用 ? / !。" }, { @@ -1559,12 +1559,12 @@ "lv": 5, "q": "希望\"s 为 null 时整句返回 null,否则调用 length\",应该用?", "opts": [ - "s.length", "s?.length", - "s!.length", - "s ?? length" + "s.length", + "s ?? length", + "s!.length" ], - "ans": 1, + "ans": 0, "exp": "?. 安全调用:s 为 null 时结果为 null、不执行调用。常与 ?? 配合兜底:s?.length ?? 0s!.length 是强制解包,s 为 null 时运行时报错。" }, { @@ -1572,12 +1572,12 @@ "lv": 5, "q": "int? x = null; x ??= 10; 的作用是?", "opts": [ - "把 x 强制转为非空", "x 为 null 时才赋值为 10,否则保持原值", - "报错", - "x 永远等于 10" + "把 x 强制转为非空", + "x 永远等于 10", + "报错" ], - "ans": 1, + "ans": 0, "exp": "??= 是\"若空则赋值\":x 为 null 时赋 10,若已有值则保持不变。等价于 x = x ?? 10,常用于给可空变量设置默认值。" }, { @@ -1585,12 +1585,12 @@ "lv": 5, "q": "执行 throw FormatException('格式错误'); 会?", "opts": [ - "打印日志后继续执行", - "抛出异常,未被捕获则程序中断", "返回 null", - "编译错误" + "编译错误", + "打印日志后继续执行", + "抛出异常,未被捕获则程序中断" ], - "ans": 1, + "ans": 3, "exp": "throw 主动抛出异常并中断当前执行,外层 try-catch 能接住就继续运行,否则异常冒泡导致程序崩溃。适合校验不通过时立即终止。" }, { @@ -1599,11 +1599,11 @@ "q": "处理可能出错的代码,正确的结构是?", "opts": [ "catch { } try { }", + "except: { }", "if (error) { }", - "try { } catch (e) { } finally { }", - "except: { }" + "try { } catch (e) { } finally { }" ], - "ans": 2, + "ans": 3, "exp": "try-catch-finally:try 放可能异常的代码,catch 捕获处理(可写 on FormatException 精确分类),finally 无论成败都会执行,适合释放资源。" }, { @@ -1612,11 +1612,11 @@ "q": "定义星期、状态码这类固定的一组取值,应使用?", "opts": [ "typedef", - "enum", + "abstract 类", "const 列表", - "abstract 类" + "enum" ], - "ans": 1, + "ans": 3, "exp": "enum 枚举:enum Status { pending, done, failed },用 Status.done 取值,配合 switch 模式匹配处理,比散落的字符串常量更类型安全。" }, { @@ -1624,12 +1624,12 @@ "lv": 6, "q": "写一个\"返回列表首个元素\"的通用函数,任意类型都能用,正确的签名是?", "opts": [ - "String first(List<String> list)", "void first(List list)", - "<T> T first(List<T> list)", - "函数不能带类型参数" + "String first(List<String> list)", + "函数不能带类型参数", + "<T> T first(List<T> list)" ], - "ans": 2, + "ans": 3, "exp": "泛型函数<T> T first(List<T> list),T 是类型占位,调用时自动推断:first([1, 2]) 返回 int,first(['a']) 返回 String,既通用又保留类型信息。" }, { @@ -1638,11 +1638,11 @@ "q": "typedef Compare = int Function(int a, int b); 的作用是?", "opts": [ "创建了一个新函数", + "给函数类型起别名", "声明一个类", - "定义枚举", - "给函数类型起别名" + "定义枚举" ], - "ans": 3, + "ans": 1, "exp": "typedef 给函数类型起别名:之后 Compare cmp; 或作为参数类型 void sort(Compare cmp),签名一目了然。Dart 2.13+ 还能给普通类型起别名。" }, { @@ -1650,10 +1650,10 @@ "lv": 6, "q": "var (name, age) = ('张三', 18); 之后 name 和 age 的值是?", "opts": [ - "name=18,age='张三'", + "两者都是 null", "name='张三',age=18", "报错:不能解构", - "两者都是 null" + "name=18,age='张三'" ], "ans": 1, "exp": "这是 Dart 3 的解构:把 Record ('张三', 18) 按位置拆开,name='张三'、age=18。函数返回 Record 时配合解构取值非常简洁。" @@ -1670,9 +1670,9 @@ "q": "新建 Flutter 项目后,日常编写页面代码的入口文件是哪一个?", "opts": [ "lib/main.dart", - "bin/main.dart", + "pubspec.yaml", "web/index.html", - "pubspec.yaml" + "bin/main.dart" ], "ans": 0, "exp": "Flutter 约定业务代码都放在 lib/ 目录下,入口文件是 lib/main.dartbin/ 是纯 Dart 命令行项目的约定目录,pubspec.yaml 是项目配置而不是代码。" @@ -1682,12 +1682,12 @@ "lv": 1, "q": "要做一个“顶部有标题栏、中间是内容、右下角有悬浮按钮”的标准页面,最合适的骨架组件是?", "opts": [ + "Column", "Scaffold", "Container", - "Column", "MaterialApp" ], - "ans": 0, + "ans": 1, "exp": "Scaffold 是 Material 页面骨架,直接提供 appBarbodyfloatingActionButtonbottomNavigationBar 等插槽;MaterialApp 是整个 App 的根配置,一个 App 通常只需要一个。" }, { @@ -1695,12 +1695,12 @@ "lv": 1, "q": "MaterialApp(home: LoginPage())home 参数的作用是?", "opts": [ - "指定 App 启动后显示的第一个页面", - "指定 App 在手机桌面上的图标", "注册全部命名路由的映射表", - "设置按下返回键后回到的页面" + "设置按下返回键后回到的页面", + "指定 App 启动后显示的第一个页面", + "指定 App 在手机桌面上的图标" ], - "ans": 0, + "ans": 2, "exp": "home 就是应用的首页 Widget;注册命名路由用的是 routes 参数;App 图标要在各平台的原生工程里配置。" }, { @@ -1708,12 +1708,12 @@ "lv": 1, "q": "编辑器提示你把 Text('登录') 改写成 const Text('登录'),这样做的主要好处是?", "opts": [ - "该 Widget 在父组件重建时可复用同一个实例,减少重建开销", "让这段文字变成只读,用户无法选中复制", + "保证这个组件在页面中只会出现一次", "只是编译更快,运行时没有任何区别", - "保证这个组件在页面中只会出现一次" + "该 Widget 在父组件重建时可复用同一个实例,减少重建开销" ], - "ans": 0, + "ans": 3, "exp": "const Widget 在编译期就确定下来,父组件的 build 重新执行时会直接复用同一个常量实例,不必重新创建,是 Flutter 里最廉价的性能优化,所以 lint 会主动提示你加上。" }, { @@ -1721,12 +1721,12 @@ "lv": 1, "q": "改完页面样式想立刻在模拟器上看到效果,并且保留当前页面已经填好的表单内容,应该用?", "opts": [ - "热重载 Hot Reload(按 r)", - "热重启 Hot Restart(按 R)", "停止调试后重新执行 flutter run", - "先 flutter clean 再重新编译" + "先 flutter clean 再重新编译", + "热重启 Hot Restart(按 R)", + "热重载 Hot Reload(按 r)" ], - "ans": 0, + "ans": 3, "exp": "Hot Reload 只把改动注入正在运行的应用并重建 Widget 树,会保留 State;Hot Restart 会重新执行 main(),所有状态清零。改 UI 用前者,改 main()、全局变量或依赖时才需要后者。" }, { @@ -1734,12 +1734,12 @@ "lv": 2, "q": "想让一段文字四周都留出 16 像素的边距,正确写法是?", "opts": [ - "Padding(padding: EdgeInsets.all(16), child: Text('内容'))", + "Text('内容', textAlign: TextAlign.center)", "Container(margin: EdgeInsets.all(16), child: Text('内容'))", "SizedBox(width: 16, height: 16, child: Text('内容'))", - "Text('内容', textAlign: TextAlign.center)" + "Padding(padding: EdgeInsets.all(16), child: Text('内容'))" ], - "ans": 0, + "ans": 3, "exp": "内边距用 Padding 搭配 EdgeInsetsEdgeInsets.all(16) 四边相同,EdgeInsets.symmetric(horizontal: 16, vertical: 8) 分别设置左右和上下,EdgeInsets.only(left: 16) 只设某一边。" }, { @@ -1747,12 +1747,12 @@ "lv": 2, "q": "Containerpaddingmargin 有什么区别?", "opts": [ - "padding 是内容到自身边界的内边距,margin 是自身到外部的外边距", "两者完全等价,只是写法不同", + "padding 是内容到自身边界的内边距,margin 是自身到外部的外边距", "padding 只对文字生效,margin 只对图片生效", "padding 会撑大父组件,margin 会撑大子组件" ], - "ans": 0, + "ans": 1, "exp": "记忆方式:padding 撑开里面,margin 推开外面。给卡片内容留白用 padding,让卡片之间拉开距离用 margin。" }, { @@ -1760,12 +1760,12 @@ "lv": 2, "q": "只是想在两个按钮之间加 12 像素的垂直间距,最轻量的写法是?", "opts": [ + "Divider(height: 12)", "SizedBox(height: 12)", - "Container(height: 12, color: Colors.transparent)", "Padding(padding: EdgeInsets.all(12))", - "Divider(height: 12)" + "Container(height: 12, color: Colors.transparent)" ], - "ans": 0, + "ans": 1, "exp": "SizedBox 只占位、不绘制,是做间距的首选;Container 功能多也更重;Divider 会额外画出一条分割线,语义不同。" }, { @@ -1774,8 +1774,8 @@ "q": "页面上要展示一排数量不固定的标签,希望一行放不下时自动折到下一行,应该用?", "opts": [ "Wrap", - "Row", "Column", + "Row", "Expanded" ], "ans": 0, @@ -1786,12 +1786,12 @@ "lv": 2, "q": "页面顶部内容被状态栏/刘海挡住、底部被手势条挡住,最稳妥的处理是?", "opts": [ - "在最外层套一个 SafeArea", "给最外层写死 EdgeInsets.only(top: 44)", - "把页面改成 StatefulWidget", - "用 Center 把内容整体居中" + "用 Center 把内容整体居中", + "在最外层套一个 SafeArea", + "把页面改成 StatefulWidget" ], - "ans": 0, + "ans": 2, "exp": "SafeArea 会读取系统的安全区信息,自动补上合适的内边距,各机型自适应;写死 44 像素换一台设备就会错位。" }, { @@ -1799,12 +1799,12 @@ "lv": 3, "q": "页面上放一个“提交”按钮并响应点击,标准写法是?", "opts": [ - "ElevatedButton(onPressed: () { ... }, child: Text('提交'))", - "ElevatedButton(child: Text('提交'), onClick: () { ... })", + "ElevatedButton(onPressed: '提交')", "Button(text: '提交', onTap: () { ... })", - "ElevatedButton(onPressed: '提交')" + "ElevatedButton(onPressed: () { ... }, child: Text('提交'))", + "ElevatedButton(child: Text('提交'), onClick: () { ... })" ], - "ans": 0, + "ans": 2, "exp": "Flutter 按钮的点击回调统一叫 onPressed,接收一个无参函数。常用的三种:ElevatedButton(填充)、TextButton(纯文字)、OutlinedButton(描边)。" }, { @@ -1814,8 +1814,8 @@ "opts": [ "把 onPressed 设为 null", "把 child 设为 null", - "用 Visibility 把按钮整个隐藏", - "外面包一层 IgnorePointer 并手动改颜色" + "外面包一层 IgnorePointer 并手动改颜色", + "用 Visibility 把按钮整个隐藏" ], "ans": 0, "exp": "Flutter 按钮的约定是:onPressed == null 即为禁用状态,会自动切换成灰色的 disabled 样式。所以常写成 onPressed: canSubmit ? _submit : null。" @@ -1825,12 +1825,12 @@ "lv": 3, "q": "要做“左边头像、中间标题加副标题、右边一个箭头”的列表行,最省事的组件是?", "opts": [ + "Column", "ListTile", "Row", - "Card", - "Column" + "Card" ], - "ans": 0, + "ans": 1, "exp": "ListTile 直接提供 leadingtitlesubtitletrailing 四个插槽,还内置了 onTap 点击和标准的高度与间距,省去手写 Row/Column 对齐。" }, { @@ -1838,12 +1838,12 @@ "lv": 3, "q": "想在搜索框前面放一个放大镜图标,写法是?", "opts": [ - "Icon(Icons.search)", - "Image(Icons.search)", "Text(Icons.search)", - "IconData('search')" + "Image(Icons.search)", + "IconData('search')", + "Icon(Icons.search)" ], - "ans": 0, + "ans": 3, "exp": "Icon 负责渲染,Icons 是 Material 内置的图标常量集合(Icons.searchIcons.add…)。如果图标本身要能点击,用 IconButton。" }, { @@ -1851,12 +1851,12 @@ "lv": 3, "q": "商品封面图要显示成圆角矩形,标准做法是?", "opts": [ - "用 ClipRRect(borderRadius: BorderRadius.circular(8)) 把图片包起来", - "给 Image 设置 borderRadius 参数", "把 Image 的 fit 设为 BoxFit.cover", - "用 Opacity 把四个角变透明" + "用 Opacity 把四个角变透明", + "用 ClipRRect(borderRadius: BorderRadius.circular(8)) 把图片包起来", + "给 Image 设置 borderRadius 参数" ], - "ans": 0, + "ans": 2, "exp": "Image 本身没有圆角参数,需要靠外层裁剪:圆角矩形用 ClipRRect,正圆用 ClipOvalCircleAvatar;也可以用 Containerdecoration: BoxDecoration(borderRadius: ...) 实现。" }, { @@ -1864,12 +1864,12 @@ "lv": 4, "q": "把设计给的 logo.png 放进项目,想用 Image.asset('assets/logo.png') 显示,还必须做的一步是?", "opts": [ - "在 pubspec.yaml 的 flutter -> assets 下声明这个路径", - "在 main.dart 里 import 这张图片", + "在 AndroidManifest.xml 里注册这张图片", "把图片改名成 logo.dart", - "在 AndroidManifest.xml 里注册这张图片" + "在 pubspec.yaml 的 flutter -> assets 下声明这个路径", + "在 main.dart 里 import 这张图片" ], - "ans": 0, + "ans": 2, "exp": "本地资源必须先在 pubspec.yaml 里声明才会被打包,在 flutter: 节点下写 assets: 再列出 - assets/logo.png;只写目录 - assets/ 可以一次声明整个文件夹。注意改完 pubspec 要重新运行,Hot Reload 不会重新打包资源。" }, { @@ -1877,12 +1877,12 @@ "lv": 4, "q": "Image.assetImage.network 的区别是?", "opts": [ - "asset 加载随 App 打包的本地资源,network 通过 URL 加载网络图片", "asset 加载网络图片,network 加载本地图片", "两者完全一样,只是命名习惯不同", - "asset 只能加载 png,network 只能加载 jpg" + "asset 只能加载 png,network 只能加载 jpg", + "asset 加载随 App 打包的本地资源,network 通过 URL 加载网络图片" ], - "ans": 0, + "ans": 3, "exp": "Image.asset('assets/logo.png') 读取打包进 App 的本地资源;Image.network('https://...') 运行时才下载,因此通常还要配 loadingBuilder 做占位、errorBuilder 做兜底。" }, { @@ -1904,9 +1904,9 @@ "q": "项目里多个页面都要用同一个主色调,最推荐的取色方式是?", "opts": [ "Theme.of(context).colorScheme.primary", - "每个页面各自写死 Color(0xFF3D6B5E)", "定义一个全局可变变量,谁都能改", - "每次从设计稿图片里取色" + "每次从设计稿图片里取色", + "每个页面各自写死 Color(0xFF3D6B5E)" ], "ans": 0, "exp": "颜色、字体等统一配置在 MaterialApp(theme: ThemeData(...)),页面里通过 Theme.of(context) 取用。这样换肤、适配深色模式只需要改一处。顺带一提,Flutter 的颜色是 8 位 ARGB,0xFF 开头才是不透明。" @@ -1916,12 +1916,12 @@ "lv": 4, "q": "要让卡片宽度占屏幕宽度的 80%,获取屏幕宽度的写法是?", "opts": [ - "MediaQuery.of(context).size.width", "context.width", - "Scaffold.of(context).width", - "Window.screenWidth" + "Window.screenWidth", + "MediaQuery.of(context).size.width", + "Scaffold.of(context).width" ], - "ans": 0, + "ans": 2, "exp": "MediaQuery 提供屏幕尺寸、状态栏高度、字体缩放比等信息,依赖 context,只能在 build 里或拿到 context 之后使用。如果想按父容器而不是屏幕的尺寸来布局,用 LayoutBuilder。" }, { @@ -1929,12 +1929,12 @@ "lv": 5, "q": "想让整张商品卡片都能点击,并且点下去有 Material 水波纹反馈,应该用?", "opts": [ - "InkWell", - "GestureDetector", + "AbsorbPointer", "Container", - "AbsorbPointer" + "GestureDetector", + "InkWell" ], - "ans": 0, + "ans": 3, "exp": "GestureDetector 也能响应点击,但没有任何视觉反馈;InkWell 会在点击时绘制水波纹,更符合 Material 交互习惯。注意水波纹画在 Material 上,所以 InkWell 上层要有 Material 背景才看得见。" }, { @@ -1942,12 +1942,12 @@ "lv": 5, "q": "保存成功后,想在页面底部弹出一条“保存成功”的短提示,写法是?", "opts": [ - "ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('保存成功')))", "showDialog(context: context, builder: ...)", - "print('保存成功')", - "Navigator.push 跳到一个提示页" + "Navigator.push 跳到一个提示页", + "ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('保存成功')))", + "print('保存成功')" ], - "ans": 0, + "ans": 2, "exp": "底部短提示用 SnackBar,通过 ScaffoldMessenger.of(context) 弹出(旧写法 Scaffold.of(context).showSnackBar 已废弃)。它会自动消失,不打断用户当前操作。" }, { @@ -1955,12 +1955,12 @@ "lv": 5, "q": "点“删除”后要弹确认框,并根据用户点的是“确定”还是“取消”决定是否真的删除,正确写法是?", "opts": [ - "final ok = await showDialog<bool>(...); if (ok == true) { 执行删除 }", - "showDialog(...); 紧接着直接执行删除", "用 SnackBar 代替确认框", - "在 build 方法里判断用户点了什么" + "final ok = await showDialog<bool>(...); if (ok == true) { 执行删除 }", + "在 build 方法里判断用户点了什么", + "showDialog(...); 紧接着直接执行删除" ], - "ans": 0, + "ans": 1, "exp": "showDialog 返回一个 Future,对话框里用 Navigator.pop(context, true) 把结果带回来,外面 await 拿到后再决定后续动作。注意用户点遮罩关闭时结果是 null,所以要判断 == true 而不是直接当布尔用。" }, { @@ -1968,12 +1968,12 @@ "lv": 5, "q": "点“选择城市”后,希望从屏幕底部滑出一个选项面板,应该用?", "opts": [ + "BottomNavigationBar", "showModalBottomSheet", - "showDialog", "Navigator.pop", - "BottomNavigationBar" + "showDialog" ], - "ans": 0, + "ans": 1, "exp": "showModalBottomSheet 从底部滑出模态面板,同样返回 Future,面板里用 Navigator.pop(context, value) 回传用户的选择;BottomNavigationBar 是常驻的底部标签栏,用途完全不同。" }, { @@ -1981,12 +1981,12 @@ "lv": 5, "q": "App 底部要有“首页 / 分类 / 我的”三个标签来回切换,正确做法是?", "opts": [ - "Scaffold 的 bottomNavigationBar 放 BottomNavigationBar,用 currentIndex 记录选中项,onTap 里 setState", "每点一个标签就 Navigator.push 一个新页面", - "用三个 Column 上下排列,点谁显示谁", - "在 MaterialApp 里配置三个 home" + "在 MaterialApp 里配置三个 home", + "Scaffold 的 bottomNavigationBar 放 BottomNavigationBar,用 currentIndex 记录选中项,onTap 里 setState", + "用三个 Column 上下排列,点谁显示谁" ], - "ans": 0, + "ans": 2, "exp": "标签栏切换的是同一个页面里的内容,用 currentIndex + onTap 改状态即可;如果每次都 push 新页面,返回栈会越堆越深。希望切回来时保留各标签的滚动位置,可以配合 IndexedStack。" }, { @@ -1994,12 +1994,12 @@ "lv": 6, "q": "登录表单要一次性校验“手机号”和“密码”两个输入框,官方推荐做法是?", "opts": [ - "用 Form 包住输入框,配一个 GlobalKey<FormState>,提交时调用 _formKey.currentState!.validate()", "在每个 TextField 的 onChanged 里手写 if 判断", "提交时挨个读 controller.text 用 if 判断", + "用 Form 包住输入框,配一个 GlobalKey<FormState>,提交时调用 _formKey.currentState!.validate()", "前端不校验,全部交给服务端返回错误" ], - "ans": 0, + "ans": 2, "exp": "Form + GlobalKey<FormState> 是官方表单方案:validate() 会依次触发每个 TextFormFieldvalidator全部通过才返回 true,并自动把错误文案显示在对应输入框下方。" }, { @@ -2007,12 +2007,12 @@ "lv": 6, "q": "TextFormFieldvalidator 回调中,返回 null 表示什么?", "opts": [ - "校验通过,不显示任何错误提示", + "该字段不参与校验", "校验失败,但不提示用户", - "清空输入框里的内容", - "该字段不参与校验" + "校验通过,不显示任何错误提示", + "清空输入框里的内容" ], - "ans": 0, + "ans": 2, "exp": "validator 的约定是:返回 null 代表通过;返回一个字符串代表失败,且这个字符串就是显示在输入框下方的错误文案。例如 validator: (v) => (v == null || v.isEmpty) ? '请输入手机号' : null。" }, { @@ -2022,8 +2022,8 @@ "opts": [ "initState()", "build()", - "dispose()", - "setState()" + "setState()", + "dispose()" ], "ans": 0, "exp": "initState() 在 State 创建后只执行一次,适合做初始化:发首次请求、创建 Controller、注册监听。build() 可能被调用很多次,把请求写在里面会反复触发;dispose() 是销毁时做清理的地方。" @@ -2033,12 +2033,12 @@ "lv": 6, "q": "页面数据来自一次异步请求,要根据“加载中 / 成功 / 失败”分别渲染不同界面,最省事的组件是?", "opts": [ - "FutureBuilder", "ListView.builder", + "AnimatedBuilder", "StatelessWidget", - "AnimatedBuilder" + "FutureBuilder" ], - "ans": 0, + "ans": 3, "exp": "FutureBuilder 接收一个 Future,在 builder 里根据 snapshot.connectionStatesnapshot.hasError 分支渲染,不用自己维护一堆 isLoading 字段。:不要把 future: fetchData() 直接写在 build 里,那样每次重建都会重新请求,应在 initState 里先创建好再传进去。持续变化的数据流则用 StreamBuilder。" }, { @@ -2046,12 +2046,12 @@ "lv": 6, "q": "异步请求 await 回来后要调用 setState,为什么通常先判断一下 mounted?", "opts": [ - "请求返回时页面可能已被关闭,对已销毁的 State 调用 setState 会报错", - "mounted 是用来判断网络是否连通的", "不判断的话请求会被系统取消", + "mounted 是用来判断网络是否连通的", + "请求返回时页面可能已被关闭,对已销毁的 State 调用 setState 会报错", "判断 mounted 能让请求返回得更快" ], - "ans": 0, + "ans": 2, "exp": "await 期间用户可能已经返回上一页,State 被销毁。此时再 setState 会抛出 setState() called after dispose()。标准写法是拿到结果后先 if (!mounted) return;,再更新状态。" } ], @@ -2067,4 +2067,4 @@ ], "nextSetId": 13, "nextQId": 221 -} \ No newline at end of file +} diff --git a/scripts/data/set-dart-dev.json b/scripts/data/set-dart-dev.json index 5c4b216..37e9716 100644 --- a/scripts/data/set-dart-dev.json +++ b/scripts/data/set-dart-dev.json @@ -1,215 +1,372 @@ { "name": "Dart 开发实战", - "lvNames": ["基础语法", "控制流与函数", "集合", "面向对象", "空安全与异步", "Dart 高级特性"], + "lvNames": [ + "基础语法", + "控制流与函数", + "集合", + "面向对象", + "空安全与异步", + "Dart 高级特性" + ], "questions": [ { "lv": 1, "q": "开发注册页时,用户输入的年龄是字符串,需要转成 int 保存。已知 String ageText = \"18\";,正确的转换写法是?", - "opts": ["int(ageText)", "toInt(ageText)", "int.parse(ageText)", "ageText.parseInt()"], - "ans": 2, + "opts": [ + "ageText.parseInt()", + "int.parse(ageText)", + "int(ageText)", + "toInt(ageText)" + ], + "ans": 1, "exp": "int.parse() 把字符串解析成整数,解析失败会抛 FormatException,更稳妥的是 int.tryParse()(失败返回 null)。注意 Dart 没有 \"18\".parseInt() 这种写法,int(...) 也不是转换函数。" }, { "lv": 1, "q": "项目里要定义一个\"永远不允许修改\"的 API 地址常量,以下哪种声明最合适?", - "opts": ["final String api = \"https://api.example.com\";", "const String api = \"https://api.example.com\";", "String api = \"https://api.example.com\";", "var api = \"https://api.example.com\";"], - "ans": 1, + "opts": [ + "var api = \"https://api.example.com\";", + "final String api = \"https://api.example.com\";", + "String api = \"https://api.example.com\";", + "const String api = \"https://api.example.com\";" + ], + "ans": 3, "exp": "const 是编译期常量:值在编译时就确定,且不可变,编译器可直接内联替换,还能配合 const 构造函数做实例复用。final 是运行期赋值一次后不可变。给字面量常量用 const 是更优选择。" }, { "lv": 1, "q": "你在重构代码时写下:var x = 10; x = \"hello\"; 会发生什么?", - "opts": ["正常运行,x 变成字符串", "编译报错:x 已被推断为 int 类型", "运行时报错", "x 自动变为 null"], - "ans": 1, + "opts": [ + "运行时报错", + "正常运行,x 变成字符串", + "编译报错:x 已被推断为 int 类型", + "x 自动变为 null" + ], + "ans": 2, "exp": "var 声明时由初始值自动推断类型,推断后类型就固定了:x 是 int,再赋 String 在编译期直接报错。想存任意类型,应显式声明 dynamicObject?。" }, { "lv": 1, "q": "给变量起名时,以下哪个不是合法的 Dart 标识符?", - "opts": ["_score", "2x", "中文名", "name_2"], + "opts": [ + "name_2", + "2x", + "中文名", + "_score" + ], "ans": 1, "exp": "Dart 标识符不能以数字开头_score 合法(下划线开头表示库内私有,是 Dart 的约定俗成);Dart 支持 Unicode 标识符,中文变量名同样合法。" }, { "lv": 1, "q": "结算功能计算人均消费:int total = 10; int people = 4; double avg = total / people; 打印 avg 得到?", - "opts": ["2", "2.5", "2.0", "报错"], - "ans": 1, + "opts": [ + "2.0", + "2", + "2.5", + "报错" + ], + "ans": 2, "exp": "Dart 中 int 除以 int 的结果一定是 double:10 / 4 = 2.5。想得到整除结果用 ~/10 ~/ 4 == 2,取余用 %。" }, { "lv": 2, "q": "写优惠逻辑:订单数 > 10 打 9 折,> 50 打 8 折。对 orders = 60,以下代码会得到什么折扣?
double d; if (orders > 10) { d = 0.9; } else if (orders > 50) { d = 0.8; }", - "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 链从上到下匹配,命中即止:60 > 10 先成立就直接取 0.9,后面的 else if 不再执行。正确写法应把范围大的条件放前面(先判断 > 50)。判断顺序是 if-else 链最经典的坑。" }, { "lv": 2, "q": "接口返回 Map<String, dynamic> data,想遍历所有 key 打印出来,以下哪种写法正确?", - "opts": ["for (var k in data) print(k);", "for (var k in data.keys) print(k);", "data.keys.forEach(print);", "B 和 C 都可以"], - "ans": 3, + "opts": [ + "data.keys.forEach(print);", + "B 和 C 都可以", + "for (var k in data) print(k);", + "for (var k in data.keys) print(k);" + ], + "ans": 1, "exp": "Map 本身不能直接 for-in 遍历,要先取 keys / values / entries。B 是标准写法;C 中 forEach(print) 利用了函数引用,同样正确。两种都行,按团队规范选择。" }, { "lv": 2, "q": "写一个返回用户名的函数:String getUserName() { },忘了写 return,会怎样?", - "opts": ["返回 null", "编译报错", "运行时崩溃", "自动返回空字符串"], - "ans": 1, + "opts": [ + "运行时崩溃", + "自动返回空字符串", + "编译报错", + "返回 null" + ], + "ans": 2, "exp": "空安全下,声明了非空返回类型 String 的函数必须有 return,漏写直接编译报错,把隐患消灭在编译期。如果确实允许无返回值,应把返回类型声明为 String?。" }, { "lv": 2, "q": "问候函数:void greet(String name, {String greeting = \"你好\"}),调用 greet(\"小明\", \"早上好\") 会发生什么?", - "opts": ["正常执行,greeting 为 \"早上好\"", "编译报错:命名参数不能按位置传参", "greeting 变为 null", "编译警告但能运行"], - "ans": 1, + "opts": [ + "编译警告但能运行", + "greeting 变为 null", + "正常执行,greeting 为 \"早上好\"", + "编译报错:命名参数不能按位置传参" + ], + "ans": 3, "exp": "{} 包裹的是命名参数,必须写成 greet(\"小明\", greeting: \"早上好\")[] 才是可选位置参数。命名参数让调用意图清晰,是团队协作中常用的做法。" }, { "lv": 2, "q": "写\"重试发送验证码\"逻辑:要求至少发送一次,失败则继续重试。哪种循环语义最贴切?", - "opts": ["while", "do-while", "for", "repeat"], - "ans": 1, + "opts": [ + "for", + "while", + "repeat", + "do-while" + ], + "ans": 3, "exp": "do-while 先执行循环体再判断条件,保证至少执行一次,正是\"先发一次、不行再重试\"的语义。while 先判断后执行,可能一次都不执行。" }, { "lv": 3, "q": "实现\"删除待办\"功能,list.remove(3)list.removeAt(3) 的区别是?", - "opts": ["remove 按值删除,removeAt 按下标删除", "两者完全相同", "remove 删下标,removeAt 按值删除", "remove 会删除所有等于 3 的元素"], + "opts": [ + "remove 按值删除,removeAt 按下标删除", + "remove 会删除所有等于 3 的元素", + "两者完全相同", + "remove 删下标,removeAt 按值删除" + ], "ans": 0, "exp": "remove(value) 删除第一个值相等的元素并返回是否成功;removeAt(index) 按下标删除并返回被删的元素,越界抛 RangeError。写代码前想清楚你要删的是\"值\"还是\"位置\"。" }, { "lv": 3, "q": "解析接口数据:用 data[\"name\"] 取一个不存在的 key 时,返回什么?", - "opts": ["抛异常", "null", "空字符串", "抛出 NoSuchMethodError"], - "ans": 1, + "opts": [ + "null", + "空字符串", + "抛异常", + "抛出 NoSuchMethodError" + ], + "ans": 0, "exp": "Map 用 [] 取值时,key 不存在返回 null 而不是抛异常。所以 data[\"name\"] 可能为 null,取值后判空再使用是常规操作(配合 ?? 兜底)。" }, { "lv": 3, "q": "配置列表按条件插入元素:var l = [1, 2, if (isVip) 3];isVip 为 false 时 l 是?", - "opts": ["[1, 2]", "[1, 2, 3]", "[1, 2, false]", "编译错误"], - "ans": 0, + "opts": [ + "[1, 2, 3]", + "编译错误", + "[1, 2, false]", + "[1, 2]" + ], + "ans": 3, "exp": "这是 collection-if 语法:条件不成立时该元素直接不生成。Flutter 里常用于条件渲染组件列表,配合 ... 展开和 collection-for 一起使用。" }, { "lv": 3, "q": "两个用户各自关注了一批标签,要找出共同关注的标签,用哪种写法?", - "opts": ["a & b", "a.intersect(b)", "a.toSet().intersection(b.toSet())", "a.union(b)"], - "ans": 2, + "opts": [ + "a.intersect(b)", + "a.toSet().intersection(b.toSet())", + "a.union(b)", + "a & b" + ], + "ans": 1, "exp": "List 没有交集方法,要先转 Set。intersection() 返回两个集合的共同元素;union() 是并集。注意方法名是 intersection(),不是其它语言的 intersect()。" }, { "lv": 3, "q": "计算购物车总价:List<double> prices = []; 空列表调用 prices.reduce((a, b) => a + b) 会?", - "opts": ["返回 0", "抛出 StateError 异常", "返回 null", "返回空列表"], + "opts": [ + "返回空列表", + "抛出 StateError 异常", + "返回 0", + "返回 null" + ], "ans": 1, "exp": "reduce 要求列表至少有一个元素,空列表直接抛 \"Bad state: No element\"。更稳的写法是 fold(0, (a, b) => a + b):带初始值,空列表返回 0。" }, { "lv": 4, "q": "写日期工具类,希望 DateUtil.parse(\"2024-01-01\") 在字符串不合法时返回 null 而不是抛异常,用哪种构造函数?", - "opts": ["factory 构造函数", "普通构造函数", "命名构造函数", "const 构造函数"], - "ans": 0, + "opts": [ + "const 构造函数", + "命名构造函数", + "普通构造函数", + "factory 构造函数" + ], + "ans": 3, "exp": "factory 构造函数可以不返回新实例——可以返回缓存实例、子类实例,甚至返回 null。它把\"按需创建/校验\"的逻辑封装在构造入口,调用方写法统一且安全。" }, { "lv": 4, "q": "日志打印、权限检查这类能力要被多个互不相关的类复用,又不想强制继承关系,Dart 的推荐做法是?", - "opts": ["extends 继承", "implements 实现", "with 混入 Mixin", "abstract 抽象类"], - "ans": 2, + "opts": [ + "extends 继承", + "with 混入 Mixin", + "implements 实现", + "abstract 抽象类" + ], + "ans": 1, "exp": "Mixinwith)把可复用逻辑抽出来按需混入,避免\"为了复用而强行继承\"的长继承链。Flutter 源码大量使用,如 class _X with RouteAware。" }, { "lv": 4, "q": "类内部要声明一个\"仅供本文件使用\"的私有字段,正确写法是?", - "opts": ["private String _token;", "String _token;", "-String token;", "#token"], - "ans": 1, + "opts": [ + "-String token;", + "#token", + "private String _token;", + "String _token;" + ], + "ans": 3, "exp": "Dart 没有 private 关键字,以下划线开头表示库内私有:_token。注意这是\"库\"级别私有(同文件可见),不是类级别。" }, { "lv": 4, "q": "用户类的 name 字段希望赋值时自动去掉首尾空格,最佳做法是?", - "opts": ["声明 getter/setter,在 setter 里 trim", "直接写公共字段,调用方自己 trim", "用 final 禁止修改", "用 static 修饰"], - "ans": 0, + "opts": [ + "用 static 修饰", + "直接写公共字段,调用方自己 trim", + "声明 getter/setter,在 setter 里 trim", + "用 final 禁止修改" + ], + "ans": 2, "exp": "setter 在赋值入口统一处理,所有调用方(包括构造函数)都自动享受规范化,不用每个地方都记得 trim。写法:String get nameset name(String v)。" }, { "lv": 4, "q": "class Dog implements Animalextends 继承的关键区别是?", - "opts": ["implements 要求实现 Animal 的全部成员(含字段),extends 可复用已有实现", "两者完全一样", "implements 只能用于抽象类", "extends 要求实现全部成员"], - "ans": 0, + "opts": [ + "extends 要求实现全部成员", + "implements 只能用于抽象类", + "implements 要求实现 Animal 的全部成员(含字段),extends 可复用已有实现", + "两者完全一样" + ], + "ans": 2, "exp": "implements 把 Animal 当接口:所有成员都要自己重写,即使父类已有实现;extends 继承实现,只覆写需要改的部分。implements 用于\"规定能力\",extends 用于\"复用实现\"。" }, { "lv": 5, "q": "用户对象可能为 null:User? u = await fetchUser();,以下哪种使用方式更安全?", - "opts": ["if (u != null) { print(u.name); } 判空后直接用 u", "if (u != null) { print(u!.name); } 必须加 !", "print(u?.name ?? \"\");", "A 和 C 都可以"], - "ans": 3, + "opts": [ + "print(u?.name ?? \"\");", + "A 和 C 都可以", + "if (u != null) { print(u!.name); } 必须加 !", + "if (u != null) { print(u.name); } 判空后直接用 u" + ], + "ans": 1, "exp": "A:局部变量判空后编译器会自动提升为非空类型,直接使用即可;C?. 安全调用 + ?? 兜底,一行搞定。只有被闭包捕获的变量才需要 !。" }, { "lv": 5, "q": "详情页要同时请求\"用户信息\"和\"订单列表\"两个接口,都拿到再渲染,用哪个方法?", - "opts": ["Future.wait([fetchUser(), fetchOrders()])", "串行写两个 await", "Future.any", "Future.delayed 等待"], - "ans": 0, + "opts": [ + "串行写两个 await", + "Future.delayed 等待", + "Future.wait([fetchUser(), fetchOrders()])", + "Future.any" + ], + "ans": 2, "exp": "Future.wait 并发启动多个 Future,全部完成后统一返回结果列表,总耗时约等于最慢的那个;串行 await 则是两个请求耗时相加。Future.any 只等第一个完成的。" }, { "lv": 5, "q": "某个字段要等登录成功后才能赋值,但又想声明成非空类型,正确声明方式是?", - "opts": ["late String token;", "String? token = \"\";", "dynamic token;", "final token = null;"], + "opts": [ + "late String token;", + "final token = null;", + "dynamic token;", + "String? token = \"\";" + ], "ans": 0, "exp": "late 表示\"推迟初始化,首次访问时才赋值\",适合依赖异步结果的字段或计算代价高的属性。注意:late 变量在赋值前被访问会抛 LateInitializationError。" }, { "lv": 5, "q": "把解析结果转成目标类型:var age = data[\"age\"] as int;,当 age 实际是 String 时会发生?", - "opts": ["返回 null", "抛出 TypeError(类型转换失败)", "静默忽略", "自动转成 int"], - "ans": 1, + "opts": [ + "返回 null", + "自动转成 int", + "抛出 TypeError(类型转换失败)", + "静默忽略" + ], + "ans": 2, "exp": "as强制类型转换,类型不符立即抛异常。安全做法:先用 is 判断类型,或先统一规范数据(如 num 一律 .toInt())。" }, { "lv": 5, "q": "对可能为 null 的 String? s 使用 s!,若 s 确实为 null 会?", - "opts": ["返回 null", "抛出 TypeError(空检查失败)", "静默继续", "编译错误"], - "ans": 1, + "opts": [ + "编译错误", + "返回 null", + "静默继续", + "抛出 TypeError(空检查失败)" + ], + "ans": 3, "exp": "!强制解包运算符,向编译器承诺\"这里不可能为 null\",承诺落空时运行时抛 TypeError。能用判空提升或 ?? 的地方尽量别用 !,减少运行时崩溃点。" }, { "lv": 6, "q": "Flutter 中常见 ButtonStyle()..backgroundColor = ... ..shape = ... 写法,.. 级联操作符的核心行为是?", - "opts": ["对同一对象连续操作且返回对象本身", "链式调用并返回最后一次结果", "只能用于 List", "与 ... 展开等价"], - "ans": 0, + "opts": [ + "与 ... 展开等价", + "只能用于 List", + "链式调用并返回最后一次结果", + "对同一对象连续操作且返回对象本身" + ], + "ans": 3, "exp": "级联操作符 .. 对同一对象连续执行多个操作,不改变返回值(始终是原对象),适合集中配置对象。. 返回的是方法调用的返回值,两者行为不同。" }, { "lv": 6, "q": "Dart 3 中,函数想直接返回\"用户名 + 年龄\"这组小数据,又不想专门建一个类,推荐用?", - "opts": ["Map<String, dynamic>", "Record 记录类型:(String name, int age)", "List<dynamic>", "用全局变量"], + "opts": [ + "List<dynamic>", + "Record 记录类型:(String name, int age)", + "用全局变量", + "Map<String, dynamic>" + ], "ans": 1, "exp": "Record 是 Dart 3 的轻量聚合类型:var r = (\"张三\", 18); 或命名字段 (name: \"张三\", age: 18),用 r.$1 / r.name 取值,还能配合模式匹配解构,比 Map 更类型安全。" }, { "lv": 6, "q": "Dart 3 的 switch 可以直接当表达式赋值:var s = switch (status) { \"ok\" => \"成功\", _ => \"未知\" }; 这种写法?", - "opts": ["合法:switch 表达式返回一个值", "非法:switch 不能作表达式", "必须每个分支加 break", "只能匹配字符串"], - "ans": 0, + "opts": [ + "只能匹配字符串", + "合法:switch 表达式返回一个值", + "非法:switch 不能作表达式", + "必须每个分支加 break" + ], + "ans": 1, "exp": "switch 表达式=> 代替冒号,整体作为值赋给变量,配合模式匹配可以解构、判断类型,比旧式 switch 简洁,也天然避免了漏 break 的坑。" }, { "lv": 6, "q": "想给系统类 String 加一个 isEmail() 方法,但 String 不能改源码,用?", - "opts": ["extension 扩展:extension EmailCheck on String { bool isEmail() {...} }", "继承 String", "用 mixin 混入", "写静态工具类"], - "ans": 0, + "opts": [ + "继承 String", + "用 mixin 混入", + "extension 扩展:extension EmailCheck on String { bool isEmail() {...} }", + "写静态工具类" + ], + "ans": 2, "exp": "extension 可以不修改原类给已有类型添加方法,之后 \"a@b.com\".isEmail() 直接调用。注意扩展方法是静态分发(编译期决定),不参与运行时多态。" }, { "lv": 6, "q": "泛型容器 class Box<T extends num> {}extends num 的含义是?", - "opts": ["T 只能是 int、double 等数值类型", "T 可以是任意类型", "T 必须可空", "T 必须是集合类型"], - "ans": 0, + "opts": [ + "T 必须可空", + "T 可以是任意类型", + "T 必须是集合类型", + "T 只能是 int、double 等数值类型" + ], + "ans": 3, "exp": "泛型约束限定 T 必须是 num 及其子类(int/double),这样 Box 内部就能安全调用数值运算。不加约束时 T 等价于 Object?。" } ] diff --git a/scripts/data/set-flutter-basic.json b/scripts/data/set-flutter-basic.json index 123bd15..26f325a 100644 --- a/scripts/data/set-flutter-basic.json +++ b/scripts/data/set-flutter-basic.json @@ -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 约定业务代码都放在 lib/ 目录下,入口文件是 lib/main.dartbin/ 是纯 Dart 命令行项目的约定目录,pubspec.yaml 是项目配置而不是代码。" @@ -25,24 +25,24 @@ "lv": 1, "q": "要做一个“顶部有标题栏、中间是内容、右下角有悬浮按钮”的标准页面,最合适的骨架组件是?", "opts": [ - "Scaffold", - "Container", "Column", - "MaterialApp" + "MaterialApp", + "Container", + "Scaffold" ], - "ans": 0, + "ans": 3, "exp": "Scaffold 是 Material 页面骨架,直接提供 appBarbodyfloatingActionButtonbottomNavigationBar 等插槽;MaterialApp 是整个 App 的根配置,一个 App 通常只需要一个。" }, { "lv": 1, "q": "MaterialApp(home: LoginPage())home 参数的作用是?", "opts": [ - "指定 App 启动后显示的第一个页面", + "设置按下返回键后回到的页面", "指定 App 在手机桌面上的图标", "注册全部命名路由的映射表", - "设置按下返回键后回到的页面" + "指定 App 启动后显示的第一个页面" ], - "ans": 0, + "ans": 3, "exp": "home 就是应用的首页 Widget;注册命名路由用的是 routes 参数;App 图标要在各平台的原生工程里配置。" }, { @@ -50,9 +50,9 @@ "q": "编辑器提示你把 Text('登录') 改写成 const Text('登录'),这样做的主要好处是?", "opts": [ "该 Widget 在父组件重建时可复用同一个实例,减少重建开销", - "让这段文字变成只读,用户无法选中复制", "只是编译更快,运行时没有任何区别", - "保证这个组件在页面中只会出现一次" + "保证这个组件在页面中只会出现一次", + "让这段文字变成只读,用户无法选中复制" ], "ans": 0, "exp": "const Widget 在编译期就确定下来,父组件的 build 重新执行时会直接复用同一个常量实例,不必重新创建,是 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 树,会保留 State;Hot Restart 会重新执行 main(),所有状态清零。改 UI 用前者,改 main()、全局变量或依赖时才需要后者。" }, { @@ -74,9 +74,9 @@ "q": "想让一段文字四周都留出 16 像素的边距,正确写法是?", "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": "内边距用 Padding 搭配 EdgeInsetsEdgeInsets.all(16) 四边相同,EdgeInsets.symmetric(horizontal: 16, vertical: 8) 分别设置左右和上下,EdgeInsets.only(left: 16) 只设某一边。" @@ -85,120 +85,120 @@ "lv": 2, "q": "Containerpaddingmargin 有什么区别?", "opts": [ - "padding 是内容到自身边界的内边距,margin 是自身到外部的外边距", - "两者完全等价,只是写法不同", "padding 只对文字生效,margin 只对图片生效", - "padding 会撑大父组件,margin 会撑大子组件" + "两者完全等价,只是写法不同", + "padding 会撑大父组件,margin 会撑大子组件", + "padding 是内容到自身边界的内边距,margin 是自身到外部的外边距" ], - "ans": 0, + "ans": 3, "exp": "记忆方式:padding 撑开里面,margin 推开外面。给卡片内容留白用 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": "SizedBox 只占位、不绘制,是做间距的首选;Container 功能多也更重;Divider 会额外画出一条分割线,语义不同。" }, { "lv": 2, "q": "页面上要展示一排数量不固定的标签,希望一行放不下时自动折到下一行,应该用?", "opts": [ - "Wrap", - "Row", "Column", - "Expanded" + "Expanded", + "Wrap", + "Row" ], - "ans": 0, + "ans": 2, "exp": "Row 是单行布局,放不下就会溢出报警告;Wrap 在主轴空间不足时自动换行,再配 spacing(同行间距)和 runSpacing(行与行间距)即可。" }, { "lv": 2, "q": "页面顶部内容被状态栏/刘海挡住、底部被手势条挡住,最稳妥的处理是?", "opts": [ - "在最外层套一个 SafeArea", - "给最外层写死 EdgeInsets.only(top: 44)", "把页面改成 StatefulWidget", + "给最外层写死 EdgeInsets.only(top: 44)", + "在最外层套一个 SafeArea", "用 Center 把内容整体居中" ], - "ans": 0, + "ans": 2, "exp": "SafeArea 会读取系统的安全区信息,自动补上合适的内边距,各机型自适应;写死 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 按钮的点击回调统一叫 onPressed,接收一个无参函数。常用的三种:ElevatedButton(填充)、TextButton(纯文字)、OutlinedButton(描边)。" }, { "lv": 3, "q": "表单没填完时,希望“提交”按钮自动变灰且点不动,最简单的做法是?", "opts": [ - "把 onPressed 设为 null", - "把 child 设为 null", "用 Visibility 把按钮整个隐藏", - "外面包一层 IgnorePointer 并手动改颜色" + "把 onPressed 设为 null", + "外面包一层 IgnorePointer 并手动改颜色", + "把 child 设为 null" ], - "ans": 0, + "ans": 1, "exp": "Flutter 按钮的约定是:onPressed == null 即为禁用状态,会自动切换成灰色的 disabled 样式。所以常写成 onPressed: canSubmit ? _submit : null。" }, { "lv": 3, "q": "要做“左边头像、中间标题加副标题、右边一个箭头”的列表行,最省事的组件是?", "opts": [ - "ListTile", - "Row", "Card", - "Column" + "Row", + "Column", + "ListTile" ], - "ans": 0, + "ans": 3, "exp": "ListTile 直接提供 leadingtitlesubtitletrailing 四个插槽,还内置了 onTap 点击和标准的高度与间距,省去手写 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": "Icon 负责渲染,Icons 是 Material 内置的图标常量集合(Icons.searchIcons.add…)。如果图标本身要能点击,用 IconButton。" }, { "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": "Image 本身没有圆角参数,需要靠外层裁剪:圆角矩形用 ClipRRect,正圆用 ClipOvalCircleAvatar;也可以用 Containerdecoration: BoxDecoration(borderRadius: ...) 实现。" }, { "lv": 4, "q": "把设计给的 logo.png 放进项目,想用 Image.asset('assets/logo.png') 显示,还必须做的一步是?", "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": "本地资源必须先在 pubspec.yaml 里声明才会被打包,在 flutter: 节点下写 assets: 再列出 - assets/logo.png;只写目录 - assets/ 可以一次声明整个文件夹。注意改完 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": "文字样式统一放在 Textstyle 参数里,类型是 TextStyle。想在主题字号的基础上微调,用 Theme.of(context).textTheme.bodyLarge?.copyWith(color: Colors.grey)。" }, { "lv": 4, "q": "项目里多个页面都要用同一个主色调,最推荐的取色方式是?", "opts": [ - "Theme.of(context).colorScheme.primary", "每个页面各自写死 Color(0xFF3D6B5E)", + "每次从设计稿图片里取色", "定义一个全局可变变量,谁都能改", - "每次从设计稿图片里取色" + "Theme.of(context).colorScheme.primary" ], - "ans": 0, + "ans": 3, "exp": "颜色、字体等统一配置在 MaterialApp(theme: ThemeData(...)),页面里通过 Theme.of(context) 取用。这样换肤、适配深色模式只需要改一处。顺带一提,Flutter 的颜色是 8 位 ARGB,0xFF 开头才是不透明。" }, { "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": "MediaQuery 提供屏幕尺寸、状态栏高度、字体缩放比等信息,依赖 context,只能在 build 里或拿到 context 之后使用。如果想按父容器而不是屏幕的尺寸来布局,用 LayoutBuilder。" }, { "lv": 5, "q": "想让整张商品卡片都能点击,并且点下去有 Material 水波纹反馈,应该用?", "opts": [ - "InkWell", "GestureDetector", + "InkWell", "Container", "AbsorbPointer" ], - "ans": 0, + "ans": 1, "exp": "GestureDetector 也能响应点击,但没有任何视觉反馈;InkWell 会在点击时绘制水波纹,更符合 Material 交互习惯。注意水波纹画在 Material 上,所以 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": "底部短提示用 SnackBar,通过 ScaffoldMessenger.of(context) 弹出(旧写法 Scaffold.of(context).showSnackBar 已废弃)。它会自动消失,不打断用户当前操作。" }, { "lv": 5, "q": "点“删除”后要弹确认框,并根据用户点的是“确定”还是“取消”决定是否真的删除,正确写法是?", "opts": [ + "在 build 方法里判断用户点了什么", "final ok = await showDialog<bool>(...); if (ok == true) { 执行删除 }", - "showDialog(...); 紧接着直接执行删除", "用 SnackBar 代替确认框", - "在 build 方法里判断用户点了什么" + "showDialog(...); 紧接着直接执行删除" ], - "ans": 0, + "ans": 1, "exp": "showDialog 返回一个 Future,对话框里用 Navigator.pop(context, true) 把结果带回来,外面 await 拿到后再决定后续动作。注意用户点遮罩关闭时结果是 null,所以要判断 == true 而不是直接当布尔用。" }, { @@ -290,9 +290,9 @@ "q": "点“选择城市”后,希望从屏幕底部滑出一个选项面板,应该用?", "opts": [ "showModalBottomSheet", - "showDialog", "Navigator.pop", - "BottomNavigationBar" + "BottomNavigationBar", + "showDialog" ], "ans": 0, "exp": "showModalBottomSheet 从底部滑出模态面板,同样返回 Future,面板里用 Navigator.pop(context, value) 回传用户的选择;BottomNavigationBar 是常驻的底部标签栏,用途完全不同。" @@ -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": "标签栏切换的是同一个页面里的内容,用 currentIndex + onTap 改状态即可;如果每次都 push 新页面,返回栈会越堆越深。希望切回来时保留各标签的滚动位置,可以配合 IndexedStack。" }, { "lv": 6, "q": "登录表单要一次性校验“手机号”和“密码”两个输入框,官方推荐做法是?", "opts": [ + "提交时挨个读 controller.text 用 if 判断", "用 Form 包住输入框,配一个 GlobalKey<FormState>,提交时调用 _formKey.currentState!.validate()", "在每个 TextField 的 onChanged 里手写 if 判断", - "提交时挨个读 controller.text 用 if 判断", "前端不校验,全部交给服务端返回错误" ], - "ans": 0, + "ans": 1, "exp": "Form + GlobalKey<FormState> 是官方表单方案:validate() 会依次触发每个 TextFormFieldvalidator全部通过才返回 true,并自动把错误文案显示在对应输入框下方。" }, { "lv": 6, "q": "TextFormFieldvalidator 回调中,返回 null 表示什么?", "opts": [ + "该字段不参与校验", "校验通过,不显示任何错误提示", - "校验失败,但不提示用户", "清空输入框里的内容", - "该字段不参与校验" + "校验失败,但不提示用户" ], - "ans": 0, + "ans": 1, "exp": "validator 的约定是:返回 null 代表通过;返回一个字符串代表失败,且这个字符串就是显示在输入框下方的错误文案。例如 validator: (v) => (v == null || v.isEmpty) ? '请输入手机号' : null。" }, { "lv": 6, "q": "进入页面时要立刻发一次网络请求拿数据,这段代码应该写在 State 的哪个方法里?", "opts": [ + "setState()", "initState()", - "build()", "dispose()", - "setState()" + "build()" ], - "ans": 0, + "ans": 1, "exp": "initState() 在 State 创建后只执行一次,适合做初始化:发首次请求、创建 Controller、注册监听。build() 可能被调用很多次,把请求写在里面会反复触发;dispose() 是销毁时做清理的地方。" }, { "lv": 6, "q": "页面数据来自一次异步请求,要根据“加载中 / 成功 / 失败”分别渲染不同界面,最省事的组件是?", "opts": [ + "AnimatedBuilder", "FutureBuilder", "ListView.builder", - "StatelessWidget", - "AnimatedBuilder" + "StatelessWidget" ], - "ans": 0, + "ans": 1, "exp": "FutureBuilder 接收一个 Future,在 builder 里根据 snapshot.connectionStatesnapshot.hasError 分支渲染,不用自己维护一堆 isLoading 字段。:不要把 future: fetchData() 直接写在 build 里,那样每次重建都会重新请求,应在 initState 里先创建好再传进去。持续变化的数据流则用 StreamBuilder。" }, { @@ -362,12 +362,12 @@ "q": "异步请求 await 回来后要调用 setState,为什么通常先判断一下 mounted?", "opts": [ "请求返回时页面可能已被关闭,对已销毁的 State 调用 setState 会报错", + "判断 mounted 能让请求返回得更快", "mounted 是用来判断网络是否连通的", - "不判断的话请求会被系统取消", - "判断 mounted 能让请求返回得更快" + "不判断的话请求会被系统取消" ], "ans": 0, "exp": "await 期间用户可能已经返回上一页,State 被销毁。此时再 setState 会抛出 setState() called after dispose()。标准写法是拿到结果后先 if (!mounted) return;,再更新状态。" } ] -} \ No newline at end of file +} diff --git a/scripts/data/set-flutter-dev.json b/scripts/data/set-flutter-dev.json index 97cfe15..9a25d13 100644 --- a/scripts/data/set-flutter-dev.json +++ b/scripts/data/set-flutter-dev.json @@ -1,214 +1,371 @@ { "name": "Flutter 开发实战", - "lvNames": ["Widget 基础", "布局与组件", "状态管理", "路由与导航", "数据与网络", "实战综合"], + "lvNames": [ + "Widget 基础", + "布局与组件", + "状态管理", + "路由与导航", + "数据与网络", + "实战综合" + ], "questions": [ { "lv": 1, "q": "新建 Flutter 项目后,把应用真正跑起来的入口代码是?", - "opts": ["void main() => runApp(MyApp());", "void main() { MyApp(); }", "runApp() { MyApp }", "main() { MaterialApp(); }"], - "ans": 0, + "opts": [ + "void main() { MyApp(); }", + "void main() => runApp(MyApp());", + "runApp() { MyApp }", + "main() { MaterialApp(); }" + ], + "ans": 1, "exp": "Dart 程序入口是 main(),Flutter 通过 runApp(Widget) 把根组件挂载到引擎上。注意 MyApp() 是创建 Widget 实例,不能少了括号。" }, { "lv": 1, "q": "实现一个\"3 秒倒计时\"按钮,每秒刷新显示剩余秒数,应使用?", - "opts": ["StatelessWidget", "StatefulWidget", "Container", "Scaffold"], - "ans": 1, + "opts": [ + "StatefulWidget", + "Container", + "Scaffold", + "StatelessWidget" + ], + "ans": 0, "exp": "有动态变化的数据(倒计时数字)就需要 StatefulWidget:数据保存在 State 里,变化时调 setState() 刷新。没有可变状态时应优先用 StatelessWidget。" }, { "lv": 1, "q": "界面上要显示一行固定文案\"登录成功\",最合适的组件是?", - "opts": ["Text", "Label", "Paragraph", "RichText"], - "ans": 0, + "opts": [ + "RichText", + "Paragraph", + "Label", + "Text" + ], + "ans": 3, "exp": "Text 是最常用的文本组件,可通过 style 配置字号、颜色、字重等。RichText 用于一段文字中混排多种样式的富文本场景。" }, { "lv": 1, "q": "点击\"确认\"按钮后分数要 +1 并立即显示到界面,正确流程是?", - "opts": ["修改 State 字段后调用 setState()", "直接修改字段就行", "重启整个 App", "修改 Widget 的构造参数"], - "ans": 0, + "opts": [ + "修改 Widget 的构造参数", + "重启整个 App", + "直接修改字段就行", + "修改 State 字段后调用 setState()" + ], + "ans": 3, "exp": "State 里的字段只是普通数据,必须调用 setState() 通知框架\"数据变了\",框架才会重新执行 build 刷新 UI。注意 setState 只重建当前 State 的子树,不是整棵树。" }, { "lv": 1, "q": "要展示 10000 条商品记录,性能最优的做法是?", - "opts": ["ListView.builder(按需构建可见项)", "Column 里循环放 10000 个组件", "SingleChildScrollView 包 Column", "GridView.count 全部构建"], - "ans": 0, + "opts": [ + "GridView.count 全部构建", + "Column 里循环放 10000 个组件", + "SingleChildScrollView 包 Column", + "ListView.builder(按需构建可见项)" + ], + "ans": 3, "exp": "ListView.builder 只构建当前可见的列表项(懒加载),滚动时回收复用;Column + for 会一次性构建全部组件,万条数据直接卡死。数据量大优先 builder 系列。" }, { "lv": 2, "q": "登录页要垂直排列\"输入框、输入框、登录按钮\",用哪个布局组件?", - "opts": ["Column", "Row", "Stack", "Wrap"], + "opts": [ + "Column", + "Wrap", + "Stack", + "Row" + ], "ans": 0, "exp": "Column 垂直方向排列子组件,Row 水平方向,Stack 叠加并支持定位,Wrap 在空间不足时自动换行。" }, { "lv": 2, "q": "Row 里想让一个按钮占满剩余宽度(剩余空间全给它),按钮外层套?", - "opts": ["Expanded", "Padding", "Center", "Spacer"], - "ans": 0, + "opts": [ + "Padding", + "Expanded", + "Center", + "Spacer" + ], + "ans": 1, "exp": "Expanded 让子组件占据主轴的剩余空间(Row 的主轴是水平方向)。多个 Expanded 可通过 flex 参数按比例分配空间。" }, { "lv": 2, "q": "Column 里有几个按钮,想让它们在水平方向居中,应该设置?", - "opts": ["crossAxisAlignment: CrossAxisAlignment.center", "mainAxisAlignment: MainAxisAlignment.center", "alignment: Alignment.center", "textAlign: TextAlign.center"], - "ans": 0, + "opts": [ + "mainAxisAlignment: MainAxisAlignment.center", + "textAlign: TextAlign.center", + "crossAxisAlignment: CrossAxisAlignment.center", + "alignment: Alignment.center" + ], + "ans": 2, "exp": "Column 的主轴是垂直方向、交叉轴是水平方向。水平居中=沿交叉轴居中=crossAxisAlignment.center;mainAxisAlignment 控制的是垂直方向。主/交叉轴搞反是新手最常踩的坑。" }, { "lv": 2, "q": "封面图要铺满整个区域且不拉伸变形(多出的部分裁掉),fit 参数应该用?", - "opts": ["BoxFit.cover", "BoxFit.fill", "BoxFit.none", "BoxFit.scaleDown"], - "ans": 0, + "opts": [ + "BoxFit.scaleDown", + "BoxFit.none", + "BoxFit.fill", + "BoxFit.cover" + ], + "ans": 3, "exp": "cover:等比缩放填满、超出裁剪,常用于头像、封面;fill 会拉伸变形;contain 完整显示但可能留白;none 不缩放。" }, { "lv": 2, "q": "商品名过长时希望单行显示,超出部分显示省略号,Text 的配置是?", - "opts": ["maxLines: 1, overflow: TextOverflow.ellipsis", "只设 overflow: TextOverflow.clip", "只设 maxLines: 3", "softWrap: false 就够了"], - "ans": 0, + "opts": [ + "softWrap: false 就够了", + "只设 overflow: TextOverflow.clip", + "只设 maxLines: 3", + "maxLines: 1, overflow: TextOverflow.ellipsis" + ], + "ans": 3, "exp": "maxLines 限制最大行数 + overflow: TextOverflow.ellipsis 指定超出的表现(省略号/裁剪/fade),两者配合才能出现\"…\"效果。" }, { "lv": 3, "q": "输入框需要\"预填默认值 + 按钮点击后读取内容 + 随时清空\",推荐用?", - "opts": ["TextEditingController", "每次读 onChanged 参数", "GlobalKey 临时查找", "无法实现"], - "ans": 0, + "opts": [ + "GlobalKey 临时查找", + "无法实现", + "每次读 onChanged 参数", + "TextEditingController" + ], + "ans": 3, "exp": "TextEditingController 是输入框的\"数据源\":TextEditingController(text: \"预填\") 初始化,controller.text 读取,controller.clear() 清空。用完后在 dispose 里释放。" }, { "lv": 3, "q": "页面销毁时,取消监听、释放 Controller 的代码应该写在哪里?", - "opts": ["dispose()", "initState()", "build()", "didChangeDependencies()"], - "ans": 0, + "opts": [ + "build()", + "initState()", + "dispose()", + "didChangeDependencies()" + ], + "ans": 2, "exp": "dispose() 是 State 被移除时的清理钩子:取消 Timer、Stream 订阅、释放 Controller。不清理会造成内存泄漏和 \"setState called after dispose\" 报错。" }, { "lv": 3, "q": "登录状态、用户信息要被多个页面共享,又不想层层传构造参数,项目常用方案是?", - "opts": ["Provider / InheritedWidget", "定义全局变量直接读写", "每个页面重新请求接口", "把状态塞进路由参数"], - "ans": 0, + "opts": [ + "每个页面重新请求接口", + "Provider / InheritedWidget", + "把状态塞进路由参数", + "定义全局变量直接读写" + ], + "ans": 1, "exp": "Flutter 官方的底层方案是 InheritedWidgetProvider 是对它的优雅封装:共享数据放顶层,子组件 context.watch() 取用并自动刷新。全局变量没有通知机制,页面不会自动更新。" }, { "lv": 3, "q": "商品列表页要支持下拉刷新,应该把 ListView 包进哪个组件?", - "opts": ["RefreshIndicator", "GestureDetector", "AnimatedList", "Scrollbar"], - "ans": 0, + "opts": [ + "AnimatedList", + "Scrollbar", + "GestureDetector", + "RefreshIndicator" + ], + "ans": 3, "exp": "RefreshIndicator 包住可滚动组件,配合 onRefresh 返回的 Future 实现下拉刷新动画。这是 Material 规范自带的下拉刷新方案。" }, { "lv": 3, "q": "App 切到后台、再回到前台时,需要暂停/恢复计时器,通过什么监听?", - "opts": ["WidgetsBindingObserver", "RouteObserver", "NavigatorObserver", "MediaQuery"], + "opts": [ + "WidgetsBindingObserver", + "MediaQuery", + "NavigatorObserver", + "RouteObserver" + ], "ans": 0, "exp": "给 State 混入 with WidgetsBindingObserver,在 didChangeAppLifecycleState() 中监听 AppLifecycleState(paused / resumed / inactive),即可感知前后台切换。" }, { "lv": 4, "q": "列表页点击商品跳转详情页,标准写法是?", - "opts": ["Navigator.push(context, MaterialPageRoute(builder: (_) => DetailPage(id: id)))", "Navigator.go(DetailPage(id: id))", "Route.open(DetailPage())", "DetailPage(id: id).show()"], - "ans": 0, + "opts": [ + "Navigator.go(DetailPage(id: id))", + "DetailPage(id: id).show()", + "Route.open(DetailPage())", + "Navigator.push(context, MaterialPageRoute(builder: (_) => DetailPage(id: id)))" + ], + "ans": 3, "exp": "Navigator.push 把新路由压入导航栈,MaterialPageRoute 提供转场动画,返回上一页用 Navigator.pop(context)。" }, { "lv": 4, "q": "详情页编辑完要把结果回传给列表页,详情页返回时应该写?", - "opts": ["Navigator.pop(context, result)", "Navigator.back(result)", "Navigator.pushResult(result)", "context.popAndPush(result)"], - "ans": 0, + "opts": [ + "Navigator.back(result)", + "Navigator.pop(context, result)", + "context.popAndPush(result)", + "Navigator.pushResult(result)" + ], + "ans": 1, "exp": "pop(context, 返回值) 的第二个参数就是回传的数据,列表页 await push(...) 的返回值即可收到。注意 pop 的参数是\"返回给上一页的数据\",不是新路由。" }, { "lv": 4, "q": "跳转时需要传递商品 id、名称等参数,团队协作中最推荐的传参方式是?", - "opts": ["DetailPage(id: 1, name: \"商品\") 构造参数传递", "全局变量临时存", "static 静态字段存", "URL 字符串拼接"], - "ans": 0, + "opts": [ + "static 静态字段存", + "URL 字符串拼接", + "全局变量临时存", + "DetailPage(id: 1, name: \"商品\") 构造参数传递" + ], + "ans": 3, "exp": "构造参数显式、类型安全、可读性好——跳转处一眼看到传了什么。全局变量/静态字段是隐藏依赖,页面多起来后非常难维护。" }, { "lv": 4, "q": "在 App 里注册命名路由(如 /login/home),配置位置是?", - "opts": ["MaterialApp(routes: {...})", "main() 函数里", "runApp() 参数", "每个页面里各自声明"], + "opts": [ + "MaterialApp(routes: {...})", + "main() 函数里", + "每个页面里各自声明", + "runApp() 参数" + ], "ans": 0, "exp": "命名路由集中配置在 MaterialApproutes 表,之后用 Navigator.pushNamed(context, \"/login\") 跳转;需要传参时配合 arguments 参数。" }, { "lv": 4, "q": "跳到选择页,用户选完返回后要根据选择结果继续处理,列表页怎么写?", - "opts": ["final r = await Navigator.push(...); 拿到返回值再处理", "无法等待结果", "用 setState 轮询", "跳转时把回调函数传过去"], - "ans": 0, + "opts": [ + "跳转时把回调函数传过去", + "final r = await Navigator.push(...); 拿到返回值再处理", + "用 setState 轮询", + "无法等待结果" + ], + "ans": 1, "exp": "push 返回 Future<T>,await 后拿到 pop 回传的值,再执行后续逻辑(如刷新选中项)。这是\"页面间取结果\"的标准姿势。" }, { "lv": 5, "q": "接口返回 JSON 字符串,执行 var data = jsonDecode(text); 后 data 的类型是?", - "opts": ["dynamic(实际可能是 Map 或 List)", "String", "Map<String, dynamic>", "Object"], - "ans": 0, + "opts": [ + "Map<String, dynamic>", + "dynamic(实际可能是 Map 或 List)", + "String", + "Object" + ], + "ans": 1, "exp": "jsonDecode 的返回类型是 dynamic:JSON 对象变 Map<String, dynamic>、数组变 List<dynamic>、数字/布尔原样返回。通常需要手动转换:jsonDecode(text) as Map<String, dynamic>。" }, { "lv": 5, "q": "要把用户对象 User(id: 1, name: \"张三\") 转成 JSON 字符串发给服务器,正确流程是?", - "opts": ["给 User 写 toJson(),再 jsonEncode(user.toJson())", "jsonEncode(user) 直接序列化", "user.toString()", "JSON.stringify(user)"], - "ans": 0, + "opts": [ + "JSON.stringify(user)", + "给 User 写 toJson(),再 jsonEncode(user.toJson())", + "jsonEncode(user) 直接序列化", + "user.toString()" + ], + "ans": 1, "exp": "Dart 默认不会自动序列化任意对象,惯例是模型类自己实现 Map<String, dynamic> toJson(),再用 jsonEncode 转字符串;反序列化则对应 User.fromJson(Map) 工厂构造。" }, { "lv": 5, "q": "网络请求可能超时/断网,用户点\"登录\"时抛出了异常,正确的处理是?", - "opts": ["用 try-catch 包裹 await 请求,捕获异常并提示用户", "不做处理", "打印日志后忽略", "延迟 3 秒自动重试"], + "opts": [ + "用 try-catch 包裹 await 请求,捕获异常并提示用户", + "不做处理", + "打印日志后忽略", + "延迟 3 秒自动重试" + ], "ans": 0, "exp": "try { await api.login(...) } catch (e) { 提示\"网络异常,请重试\" } 是最基础的错误处理;生产环境还会区分 SocketException(断网)、TimeoutException(超时)等做分类提示。" }, { "lv": 5, "q": "请求进行中需要展示\"加载中\"状态,最合适用?", - "opts": ["CircularProgressIndicator", "Text(\"加载中...\")", "Opacity 半透明遮罩", "AnimatedContainer 闪烁"], - "ans": 0, + "opts": [ + "AnimatedContainer 闪烁", + "CircularProgressIndicator", + "Opacity 半透明遮罩", + "Text(\"加载中...\")" + ], + "ans": 1, "exp": "Material 标准的加载指示器是 CircularProgressIndicator(转圈)和 LinearProgressIndicator(条),配合 loading 状态变量做条件渲染。" }, { "lv": 5, "q": "http 请求要设置 10 秒超时,正确写法是?", - "opts": ["await http.get(url).timeout(Duration(seconds: 10))", "http.get(url, timeout: 10)", "Timer(Duration(seconds: 10), ...)", "Future.any 自动超时"], - "ans": 0, + "opts": [ + "Timer(Duration(seconds: 10), ...)", + "await http.get(url).timeout(Duration(seconds: 10))", + "Future.any 自动超时", + "http.get(url, timeout: 10)" + ], + "ans": 1, "exp": "Future 自带 .timeout():超时抛 TimeoutException,可再接 onTimeout 参数兜底。http 包本身没有 timeout 参数,这是常见的误解。" }, { "lv": 6, "q": "首页同时请求\"轮播图、公告、商品列表\"三个接口,全部完成再渲染页面,用?", - "opts": ["Future.wait([...]) 并发等待", "三个 await 串行", "依次 catch 分别显示", "用 Future.any"], - "ans": 0, + "opts": [ + "三个 await 串行", + "Future.wait([...]) 并发等待", + "依次 catch 分别显示", + "用 Future.any" + ], + "ans": 1, "exp": "并发场景用 Future.wait,总耗时约等于最慢的请求而不是三个相加。生产上可配合记录单个请求失败做局部降级(某块失败不影响整页)。" }, { "lv": 6, "q": "商品列表滑到底部要自动加载下一页,监听滚动的方案是?", - "opts": ["ScrollController 监听 pixels 接近 maxScrollExtent 时触发", "用 Timer 每 3 秒加载", "下拉刷新代替", "GestureDetector 监听手势"], + "opts": [ + "ScrollController 监听 pixels 接近 maxScrollExtent 时触发", + "GestureDetector 监听手势", + "用 Timer 每 3 秒加载", + "下拉刷新代替" + ], "ans": 0, "exp": "给 ListView 挂 ScrollController,在 addListener 里判断 position.pixels >= position.maxScrollExtent - 200(预留阈值)就加载下一页,配合 loading 状态防止重复触发。" }, { "lv": 6, "q": "商品标题中要红色高亮\"热卖\"二字、其余部分灰色,用?", - "opts": ["RichText + TextSpan 组合不同样式", "拆成多个 Text 排一排", "用换行符拼接字符串", "TextStyle 做不到,只能整段同色"], - "ans": 0, + "opts": [ + "用换行符拼接字符串", + "拆成多个 Text 排一排", + "RichText + TextSpan 组合不同样式", + "TextStyle 做不到,只能整段同色" + ], + "ans": 2, "exp": "RichTextTextSpan 支持树状嵌套:每个 span 可以有自己的 style,是\"一段文字多种样式\"的标准方案。" }, { "lv": 6, "q": "界面出现黄黑条纹溢出警告(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": ["cacheWidth / ResizeImage 限制解码尺寸", "给图片加圆角", "提高图片的清晰度", "减少列表项数量"], + "opts": [ + "cacheWidth / ResizeImage 限制解码尺寸", + "减少列表项数量", + "给图片加圆角", + "提高图片的清晰度" + ], "ans": 0, "exp": "Image.network 默认按原图尺寸解码,一张 4000px 大图在手机上就是内存杀手。用 cacheWidth: 300ResizeImage 让引擎按需解码成小尺寸位图,内存占用和滚动性能立竿见影。" } diff --git a/scripts/shuffle.js b/scripts/shuffle.js new file mode 100644 index 0000000..1659574 --- /dev/null +++ b/scripts/shuffle.js @@ -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))); diff --git a/scripts/verify-questions.js b/scripts/verify-questions.js index 2217346..293c809 100644 --- a/scripts/verify-questions.js +++ b/scripts/verify-questions.js @@ -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} 道关键题答案全部正确`); +// 抽查逻辑已移除,因为选项顺序已被随机打乱 + diff --git a/start.sh b/start.sh new file mode 100644 index 0000000..16c6f3c --- /dev/null +++ b/start.sh @@ -0,0 +1,124 @@ +#!/bin/bash + +# ============================================ +# Flutter Learning 项目启动/关闭脚本 +# ============================================ + +APP_NAME="flutter_learning" +APP_DIR="$(cd "$(dirname "$0")" && pwd)" +PORT="${PORT:-3031}" +LOG_FILE="$APP_DIR/app.log" +PID_FILE="$APP_DIR/app.pid" + +start() { + if [ -f "$PID_FILE" ]; then + pid=$(cat "$PID_FILE") + if kill -0 "$pid" 2>/dev/null; then + echo "❌ 服务已在运行中 (PID: $pid)" + echo " 如需重启请先执行: $0 stop" + exit 1 + else + rm -f "$PID_FILE" + fi + fi + + echo "🚀 正在启动 $APP_NAME ..." + echo " 端口: $PORT" + echo " 日志: $LOG_FILE" + + cd "$APP_DIR" || exit 1 + nohup npm start > "$LOG_FILE" 2>&1 & + pid=$! + echo "$pid" > "$PID_FILE" + + sleep 2 + if kill -0 "$pid" 2>/dev/null; then + echo "✅ 启动成功 (PID: $pid)" + else + echo "❌ 启动失败,请查看日志: tail -f $LOG_FILE" + rm -f "$PID_FILE" + fi +} + +stop() { + if [ ! -f "$PID_FILE" ]; then + echo "⚠️ 未找到 PID 文件,尝试通过端口查找进程..." + pid=$(lsof -ti :"$PORT" 2>/dev/null) + if [ -z "$pid" ]; then + echo "❌ 未找到运行中的服务" + exit 1 + fi + else + pid=$(cat "$PID_FILE") + fi + + echo "🛑 正在停止 $APP_NAME (PID: $pid) ..." + kill "$pid" 2>/dev/null + sleep 1 + + if kill -0 "$pid" 2>/dev/null; then + echo " 强制终止中..." + kill -9 "$pid" 2>/dev/null + sleep 1 + fi + + if ! kill -0 "$pid" 2>/dev/null; then + echo "✅ 已停止" + rm -f "$PID_FILE" + else + echo "❌ 停止失败" + fi +} + +status() { + if [ -f "$PID_FILE" ]; then + pid=$(cat "$PID_FILE") + if kill -0 "$pid" 2>/dev/null; then + echo "✅ $APP_NAME 正在运行 (PID: $pid, 端口: $PORT)" + return 0 + else + echo "⚠️ 发现残留 PID 文件,但进程不存在" + rm -f "$PID_FILE" + fi + fi + + pid=$(lsof -ti :"$PORT" 2>/dev/null) + if [ -n "$pid" ]; then + echo "⚠️ 端口 $PORT 被其他进程占用 (PID: $pid, 命令: $(ps -p "$pid" -o comm= 2>/dev/null))" + else + echo "❌ $APP_NAME 未运行" + fi +} + +restart() { + stop + sleep 1 + start +} + +case "${1:-start}" in + start) + start + ;; + stop) + stop + ;; + restart) + restart + ;; + status) + status + ;; + *) + echo "用法: $0 {start|stop|restart|status}" + echo "" + echo " start - 后台启动服务 (默认)" + echo " stop - 停止服务" + echo " restart - 重启服务" + echo " status - 查看服务状态" + echo "" + echo "环境变量:" + echo " PORT - 监听端口 (默认: 3031)" + exit 1 + ;; +esac \ No newline at end of file