chore: update project
This commit is contained in:
+406
-2
@@ -1659,8 +1659,412 @@
|
||||
"exp": "这是 Dart 3 的<b>解构</b>:把 Record <code>('张三', 18)</code> 按位置拆开,name='张三'、age=18。函数返回 Record 时配合解构取值非常简洁。"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"name": "Flutter 基础巩固",
|
||||
"questions": [
|
||||
{
|
||||
"id": 191,
|
||||
"lv": 1,
|
||||
"q": "新建 Flutter 项目后,日常编写页面代码的入口文件是哪一个?",
|
||||
"opts": [
|
||||
"lib/main.dart",
|
||||
"bin/main.dart",
|
||||
"web/index.html",
|
||||
"pubspec.yaml"
|
||||
],
|
||||
"ans": 0,
|
||||
"exp": "Flutter 约定业务代码都放在 <code>lib/</code> 目录下,入口文件是 <code>lib/main.dart</code>;<code>bin/</code> 是纯 Dart 命令行项目的约定目录,<code>pubspec.yaml</code> 是项目配置而不是代码。"
|
||||
},
|
||||
{
|
||||
"id": 192,
|
||||
"lv": 1,
|
||||
"q": "要做一个“顶部有标题栏、中间是内容、右下角有悬浮按钮”的标准页面,最合适的骨架组件是?",
|
||||
"opts": [
|
||||
"Scaffold",
|
||||
"Container",
|
||||
"Column",
|
||||
"MaterialApp"
|
||||
],
|
||||
"ans": 0,
|
||||
"exp": "<code>Scaffold</code> 是 Material 页面骨架,直接提供 <code>appBar</code>、<code>body</code>、<code>floatingActionButton</code>、<code>bottomNavigationBar</code> 等插槽;<code>MaterialApp</code> 是整个 App 的根配置,一个 App 通常只需要一个。"
|
||||
},
|
||||
{
|
||||
"id": 193,
|
||||
"lv": 1,
|
||||
"q": "<code>MaterialApp(home: LoginPage())</code> 中 <b>home</b> 参数的作用是?",
|
||||
"opts": [
|
||||
"指定 App 启动后显示的第一个页面",
|
||||
"指定 App 在手机桌面上的图标",
|
||||
"注册全部命名路由的映射表",
|
||||
"设置按下返回键后回到的页面"
|
||||
],
|
||||
"ans": 0,
|
||||
"exp": "<code>home</code> 就是应用的首页 Widget;注册命名路由用的是 <code>routes</code> 参数;App 图标要在各平台的原生工程里配置。"
|
||||
},
|
||||
{
|
||||
"id": 194,
|
||||
"lv": 1,
|
||||
"q": "编辑器提示你把 <code>Text('登录')</code> 改写成 <code>const Text('登录')</code>,这样做的主要好处是?",
|
||||
"opts": [
|
||||
"该 Widget 在父组件重建时可复用同一个实例,减少重建开销",
|
||||
"让这段文字变成只读,用户无法选中复制",
|
||||
"只是编译更快,运行时没有任何区别",
|
||||
"保证这个组件在页面中只会出现一次"
|
||||
],
|
||||
"ans": 0,
|
||||
"exp": "<code>const</code> Widget 在编译期就确定下来,父组件的 <code>build</code> 重新执行时会直接复用同一个常量实例,不必重新创建,是 Flutter 里最廉价的性能优化,所以 lint 会主动提示你加上。"
|
||||
},
|
||||
{
|
||||
"id": 195,
|
||||
"lv": 1,
|
||||
"q": "改完页面样式想立刻在模拟器上看到效果,并且保留当前页面已经填好的表单内容,应该用?",
|
||||
"opts": [
|
||||
"热重载 Hot Reload(按 r)",
|
||||
"热重启 Hot Restart(按 R)",
|
||||
"停止调试后重新执行 flutter run",
|
||||
"先 flutter clean 再重新编译"
|
||||
],
|
||||
"ans": 0,
|
||||
"exp": "Hot Reload 只把改动注入正在运行的应用并重建 Widget 树,<b>会保留 State</b>;Hot Restart 会重新执行 <code>main()</code>,所有状态清零。改 UI 用前者,改 <code>main()</code>、全局变量或依赖时才需要后者。"
|
||||
},
|
||||
{
|
||||
"id": 196,
|
||||
"lv": 2,
|
||||
"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)"
|
||||
],
|
||||
"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> 只设某一边。"
|
||||
},
|
||||
{
|
||||
"id": 197,
|
||||
"lv": 2,
|
||||
"q": "<code>Container</code> 的 <b>padding</b> 和 <b>margin</b> 有什么区别?",
|
||||
"opts": [
|
||||
"padding 是内容到自身边界的内边距,margin 是自身到外部的外边距",
|
||||
"两者完全等价,只是写法不同",
|
||||
"padding 只对文字生效,margin 只对图片生效",
|
||||
"padding 会撑大父组件,margin 会撑大子组件"
|
||||
],
|
||||
"ans": 0,
|
||||
"exp": "记忆方式:<b>padding 撑开里面,margin 推开外面</b>。给卡片内容留白用 padding,让卡片之间拉开距离用 margin。"
|
||||
},
|
||||
{
|
||||
"id": 198,
|
||||
"lv": 2,
|
||||
"q": "只是想在两个按钮之间加 12 像素的垂直间距,最轻量的写法是?",
|
||||
"opts": [
|
||||
"SizedBox(height: 12)",
|
||||
"Container(height: 12, color: Colors.transparent)",
|
||||
"Padding(padding: EdgeInsets.all(12))",
|
||||
"Divider(height: 12)"
|
||||
],
|
||||
"ans": 0,
|
||||
"exp": "<code>SizedBox</code> 只占位、不绘制,是做间距的首选;<code>Container</code> 功能多也更重;<code>Divider</code> 会额外画出一条分割线,语义不同。"
|
||||
},
|
||||
{
|
||||
"id": 199,
|
||||
"lv": 2,
|
||||
"q": "页面上要展示一排数量不固定的标签,希望一行放不下时自动折到下一行,应该用?",
|
||||
"opts": [
|
||||
"Wrap",
|
||||
"Row",
|
||||
"Column",
|
||||
"Expanded"
|
||||
],
|
||||
"ans": 0,
|
||||
"exp": "<code>Row</code> 是单行布局,放不下就会溢出报警告;<code>Wrap</code> 在主轴空间不足时自动换行,再配 <code>spacing</code>(同行间距)和 <code>runSpacing</code>(行与行间距)即可。"
|
||||
},
|
||||
{
|
||||
"id": 200,
|
||||
"lv": 2,
|
||||
"q": "页面顶部内容被状态栏/刘海挡住、底部被手势条挡住,最稳妥的处理是?",
|
||||
"opts": [
|
||||
"在最外层套一个 SafeArea",
|
||||
"给最外层写死 EdgeInsets.only(top: 44)",
|
||||
"把页面改成 StatefulWidget",
|
||||
"用 Center 把内容整体居中"
|
||||
],
|
||||
"ans": 0,
|
||||
"exp": "<code>SafeArea</code> 会读取系统的安全区信息,自动补上合适的内边距,各机型自适应;写死 44 像素换一台设备就会错位。"
|
||||
},
|
||||
{
|
||||
"id": 201,
|
||||
"lv": 3,
|
||||
"q": "页面上放一个“提交”按钮并响应点击,标准写法是?",
|
||||
"opts": [
|
||||
"ElevatedButton(onPressed: () { ... }, child: Text('提交'))",
|
||||
"ElevatedButton(child: Text('提交'), onClick: () { ... })",
|
||||
"Button(text: '提交', onTap: () { ... })",
|
||||
"ElevatedButton(onPressed: '提交')"
|
||||
],
|
||||
"ans": 0,
|
||||
"exp": "Flutter 按钮的点击回调统一叫 <code>onPressed</code>,接收一个无参函数。常用的三种:<code>ElevatedButton</code>(填充)、<code>TextButton</code>(纯文字)、<code>OutlinedButton</code>(描边)。"
|
||||
},
|
||||
{
|
||||
"id": 202,
|
||||
"lv": 3,
|
||||
"q": "表单没填完时,希望“提交”按钮自动变灰且点不动,最简单的做法是?",
|
||||
"opts": [
|
||||
"把 onPressed 设为 null",
|
||||
"把 child 设为 null",
|
||||
"用 Visibility 把按钮整个隐藏",
|
||||
"外面包一层 IgnorePointer 并手动改颜色"
|
||||
],
|
||||
"ans": 0,
|
||||
"exp": "Flutter 按钮的约定是:<code>onPressed == null</code> 即为禁用状态,会自动切换成灰色的 disabled 样式。所以常写成 <code>onPressed: canSubmit ? _submit : null</code>。"
|
||||
},
|
||||
{
|
||||
"id": 203,
|
||||
"lv": 3,
|
||||
"q": "要做“左边头像、中间标题加副标题、右边一个箭头”的列表行,最省事的组件是?",
|
||||
"opts": [
|
||||
"ListTile",
|
||||
"Row",
|
||||
"Card",
|
||||
"Column"
|
||||
],
|
||||
"ans": 0,
|
||||
"exp": "<code>ListTile</code> 直接提供 <code>leading</code>、<code>title</code>、<code>subtitle</code>、<code>trailing</code> 四个插槽,还内置了 <code>onTap</code> 点击和标准的高度与间距,省去手写 Row/Column 对齐。"
|
||||
},
|
||||
{
|
||||
"id": 204,
|
||||
"lv": 3,
|
||||
"q": "想在搜索框前面放一个放大镜图标,写法是?",
|
||||
"opts": [
|
||||
"Icon(Icons.search)",
|
||||
"Image(Icons.search)",
|
||||
"Text(Icons.search)",
|
||||
"IconData('search')"
|
||||
],
|
||||
"ans": 0,
|
||||
"exp": "<code>Icon</code> 负责渲染,<code>Icons</code> 是 Material 内置的图标常量集合(<code>Icons.search</code>、<code>Icons.add</code>…)。如果图标本身要能点击,用 <code>IconButton</code>。"
|
||||
},
|
||||
{
|
||||
"id": 205,
|
||||
"lv": 3,
|
||||
"q": "商品封面图要显示成圆角矩形,标准做法是?",
|
||||
"opts": [
|
||||
"用 ClipRRect(borderRadius: BorderRadius.circular(8)) 把图片包起来",
|
||||
"给 Image 设置 borderRadius 参数",
|
||||
"把 Image 的 fit 设为 BoxFit.cover",
|
||||
"用 Opacity 把四个角变透明"
|
||||
],
|
||||
"ans": 0,
|
||||
"exp": "<code>Image</code> 本身没有圆角参数,需要靠外层裁剪:圆角矩形用 <code>ClipRRect</code>,正圆用 <code>ClipOval</code> 或 <code>CircleAvatar</code>;也可以用 <code>Container</code> 的 <code>decoration: BoxDecoration(borderRadius: ...)</code> 实现。"
|
||||
},
|
||||
{
|
||||
"id": 206,
|
||||
"lv": 4,
|
||||
"q": "把设计给的 logo.png 放进项目,想用 <code>Image.asset('assets/logo.png')</code> 显示,还必须做的一步是?",
|
||||
"opts": [
|
||||
"在 pubspec.yaml 的 flutter -> assets 下声明这个路径",
|
||||
"在 main.dart 里 import 这张图片",
|
||||
"把图片改名成 logo.dart",
|
||||
"在 AndroidManifest.xml 里注册这张图片"
|
||||
],
|
||||
"ans": 0,
|
||||
"exp": "本地资源必须先在 <code>pubspec.yaml</code> 里声明才会被打包,在 <code>flutter:</code> 节点下写 <code>assets:</code> 再列出 <code>- assets/logo.png</code>;只写目录 <code>- assets/</code> 可以一次声明整个文件夹。注意改完 pubspec 要重新运行,Hot Reload 不会重新打包资源。"
|
||||
},
|
||||
{
|
||||
"id": 207,
|
||||
"lv": 4,
|
||||
"q": "<code>Image.asset</code> 和 <code>Image.network</code> 的区别是?",
|
||||
"opts": [
|
||||
"asset 加载随 App 打包的本地资源,network 通过 URL 加载网络图片",
|
||||
"asset 加载网络图片,network 加载本地图片",
|
||||
"两者完全一样,只是命名习惯不同",
|
||||
"asset 只能加载 png,network 只能加载 jpg"
|
||||
],
|
||||
"ans": 0,
|
||||
"exp": "<code>Image.asset('assets/logo.png')</code> 读取打包进 App 的本地资源;<code>Image.network('https://...')</code> 运行时才下载,因此通常还要配 <code>loadingBuilder</code> 做占位、<code>errorBuilder</code> 做兜底。"
|
||||
},
|
||||
{
|
||||
"id": 208,
|
||||
"lv": 4,
|
||||
"q": "要把一段文字设置成 18 号、加粗、灰色,正确写法是?",
|
||||
"opts": [
|
||||
"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,
|
||||
"exp": "文字样式统一放在 <code>Text</code> 的 <code>style</code> 参数里,类型是 <code>TextStyle</code>。想在主题字号的基础上微调,用 <code>Theme.of(context).textTheme.bodyLarge?.copyWith(color: Colors.grey)</code>。"
|
||||
},
|
||||
{
|
||||
"id": 209,
|
||||
"lv": 4,
|
||||
"q": "项目里多个页面都要用同一个主色调,最推荐的取色方式是?",
|
||||
"opts": [
|
||||
"Theme.of(context).colorScheme.primary",
|
||||
"每个页面各自写死 Color(0xFF3D6B5E)",
|
||||
"定义一个全局可变变量,谁都能改",
|
||||
"每次从设计稿图片里取色"
|
||||
],
|
||||
"ans": 0,
|
||||
"exp": "颜色、字体等统一配置在 <code>MaterialApp(theme: ThemeData(...))</code>,页面里通过 <code>Theme.of(context)</code> 取用。这样换肤、适配深色模式只需要改一处。顺带一提,Flutter 的颜色是 8 位 ARGB,<code>0xFF</code> 开头才是不透明。"
|
||||
},
|
||||
{
|
||||
"id": 210,
|
||||
"lv": 4,
|
||||
"q": "要让卡片宽度占屏幕宽度的 80%,获取屏幕宽度的写法是?",
|
||||
"opts": [
|
||||
"MediaQuery.of(context).size.width",
|
||||
"context.width",
|
||||
"Scaffold.of(context).width",
|
||||
"Window.screenWidth"
|
||||
],
|
||||
"ans": 0,
|
||||
"exp": "<code>MediaQuery</code> 提供屏幕尺寸、状态栏高度、字体缩放比等信息,依赖 <code>context</code>,只能在 <code>build</code> 里或拿到 context 之后使用。如果想按<b>父容器</b>而不是屏幕的尺寸来布局,用 <code>LayoutBuilder</code>。"
|
||||
},
|
||||
{
|
||||
"id": 211,
|
||||
"lv": 5,
|
||||
"q": "想让整张商品卡片都能点击,并且点下去有 Material 水波纹反馈,应该用?",
|
||||
"opts": [
|
||||
"InkWell",
|
||||
"GestureDetector",
|
||||
"Container",
|
||||
"AbsorbPointer"
|
||||
],
|
||||
"ans": 0,
|
||||
"exp": "<code>GestureDetector</code> 也能响应点击,但没有任何视觉反馈;<code>InkWell</code> 会在点击时绘制水波纹,更符合 Material 交互习惯。注意水波纹画在 <code>Material</code> 上,所以 InkWell 上层要有 Material 背景才看得见。"
|
||||
},
|
||||
{
|
||||
"id": 212,
|
||||
"lv": 5,
|
||||
"q": "保存成功后,想在页面底部弹出一条“保存成功”的短提示,写法是?",
|
||||
"opts": [
|
||||
"ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('保存成功')))",
|
||||
"showDialog(context: context, builder: ...)",
|
||||
"print('保存成功')",
|
||||
"Navigator.push 跳到一个提示页"
|
||||
],
|
||||
"ans": 0,
|
||||
"exp": "底部短提示用 <code>SnackBar</code>,通过 <code>ScaffoldMessenger.of(context)</code> 弹出(旧写法 <code>Scaffold.of(context).showSnackBar</code> 已废弃)。它会自动消失,不打断用户当前操作。"
|
||||
},
|
||||
{
|
||||
"id": 213,
|
||||
"lv": 5,
|
||||
"q": "点“删除”后要弹确认框,并根据用户点的是“确定”还是“取消”决定是否真的删除,正确写法是?",
|
||||
"opts": [
|
||||
"final ok = await showDialog<bool>(...); if (ok == true) { 执行删除 }",
|
||||
"showDialog(...); 紧接着直接执行删除",
|
||||
"用 SnackBar 代替确认框",
|
||||
"在 build 方法里判断用户点了什么"
|
||||
],
|
||||
"ans": 0,
|
||||
"exp": "<code>showDialog</code> 返回一个 <code>Future</code>,对话框里用 <code>Navigator.pop(context, true)</code> 把结果带回来,外面 <code>await</code> 拿到后再决定后续动作。注意用户点遮罩关闭时结果是 <code>null</code>,所以要判断 <code>== true</code> 而不是直接当布尔用。"
|
||||
},
|
||||
{
|
||||
"id": 214,
|
||||
"lv": 5,
|
||||
"q": "点“选择城市”后,希望从屏幕底部滑出一个选项面板,应该用?",
|
||||
"opts": [
|
||||
"showModalBottomSheet",
|
||||
"showDialog",
|
||||
"Navigator.pop",
|
||||
"BottomNavigationBar"
|
||||
],
|
||||
"ans": 0,
|
||||
"exp": "<code>showModalBottomSheet</code> 从底部滑出模态面板,同样返回 <code>Future</code>,面板里用 <code>Navigator.pop(context, value)</code> 回传用户的选择;<code>BottomNavigationBar</code> 是常驻的底部标签栏,用途完全不同。"
|
||||
},
|
||||
{
|
||||
"id": 215,
|
||||
"lv": 5,
|
||||
"q": "App 底部要有“首页 / 分类 / 我的”三个标签来回切换,正确做法是?",
|
||||
"opts": [
|
||||
"Scaffold 的 bottomNavigationBar 放 BottomNavigationBar,用 currentIndex 记录选中项,onTap 里 setState",
|
||||
"每点一个标签就 Navigator.push 一个新页面",
|
||||
"用三个 Column 上下排列,点谁显示谁",
|
||||
"在 MaterialApp 里配置三个 home"
|
||||
],
|
||||
"ans": 0,
|
||||
"exp": "标签栏切换的是同一个页面里的内容,用 <code>currentIndex</code> + <code>onTap</code> 改状态即可;如果每次都 <code>push</code> 新页面,返回栈会越堆越深。希望切回来时保留各标签的滚动位置,可以配合 <code>IndexedStack</code>。"
|
||||
},
|
||||
{
|
||||
"id": 216,
|
||||
"lv": 6,
|
||||
"q": "登录表单要一次性校验“手机号”和“密码”两个输入框,官方推荐做法是?",
|
||||
"opts": [
|
||||
"用 Form 包住输入框,配一个 GlobalKey<FormState>,提交时调用 _formKey.currentState!.validate()",
|
||||
"在每个 TextField 的 onChanged 里手写 if 判断",
|
||||
"提交时挨个读 controller.text 用 if 判断",
|
||||
"前端不校验,全部交给服务端返回错误"
|
||||
],
|
||||
"ans": 0,
|
||||
"exp": "<code>Form</code> + <code>GlobalKey<FormState></code> 是官方表单方案:<code>validate()</code> 会依次触发每个 <code>TextFormField</code> 的 <code>validator</code>,<b>全部通过才返回 true</b>,并自动把错误文案显示在对应输入框下方。"
|
||||
},
|
||||
{
|
||||
"id": 217,
|
||||
"lv": 6,
|
||||
"q": "<code>TextFormField</code> 的 <code>validator</code> 回调中,返回 <code>null</code> 表示什么?",
|
||||
"opts": [
|
||||
"校验通过,不显示任何错误提示",
|
||||
"校验失败,但不提示用户",
|
||||
"清空输入框里的内容",
|
||||
"该字段不参与校验"
|
||||
],
|
||||
"ans": 0,
|
||||
"exp": "<code>validator</code> 的约定是:返回 <code>null</code> 代表通过;返回一个字符串代表失败,且这个字符串就是显示在输入框下方的错误文案。例如 <code>validator: (v) => (v == null || v.isEmpty) ? '请输入手机号' : null</code>。"
|
||||
},
|
||||
{
|
||||
"id": 218,
|
||||
"lv": 6,
|
||||
"q": "进入页面时要立刻发一次网络请求拿数据,这段代码应该写在 State 的哪个方法里?",
|
||||
"opts": [
|
||||
"initState()",
|
||||
"build()",
|
||||
"dispose()",
|
||||
"setState()"
|
||||
],
|
||||
"ans": 0,
|
||||
"exp": "<code>initState()</code> 在 State 创建后只执行一次,适合做初始化:发首次请求、创建 Controller、注册监听。<code>build()</code> 可能被调用很多次,把请求写在里面会反复触发;<code>dispose()</code> 是销毁时做清理的地方。"
|
||||
},
|
||||
{
|
||||
"id": 219,
|
||||
"lv": 6,
|
||||
"q": "页面数据来自一次异步请求,要根据“加载中 / 成功 / 失败”分别渲染不同界面,最省事的组件是?",
|
||||
"opts": [
|
||||
"FutureBuilder",
|
||||
"ListView.builder",
|
||||
"StatelessWidget",
|
||||
"AnimatedBuilder"
|
||||
],
|
||||
"ans": 0,
|
||||
"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>。"
|
||||
},
|
||||
{
|
||||
"id": 220,
|
||||
"lv": 6,
|
||||
"q": "异步请求 <code>await</code> 回来后要调用 <code>setState</code>,为什么通常先判断一下 <code>mounted</code>?",
|
||||
"opts": [
|
||||
"请求返回时页面可能已被关闭,对已销毁的 State 调用 setState 会报错",
|
||||
"mounted 是用来判断网络是否连通的",
|
||||
"不判断的话请求会被系统取消",
|
||||
"判断 mounted 能让请求返回得更快"
|
||||
],
|
||||
"ans": 0,
|
||||
"exp": "<code>await</code> 期间用户可能已经返回上一页,State 被销毁。此时再 <code>setState</code> 会抛出 setState() called after dispose()。标准写法是拿到结果后先 <code>if (!mounted) return;</code>,再更新状态。"
|
||||
}
|
||||
],
|
||||
"lvNames": [
|
||||
"项目与骨架",
|
||||
"布局与间距",
|
||||
"常用组件",
|
||||
"资源与样式",
|
||||
"交互与反馈",
|
||||
"表单与异步"
|
||||
]
|
||||
}
|
||||
],
|
||||
"nextSetId": 12,
|
||||
"nextQId": 191
|
||||
"nextSetId": 13,
|
||||
"nextQId": 221
|
||||
}
|
||||
Reference in New Issue
Block a user