Files
2026-08-10 00:15:44 +08:00

374 lines
19 KiB
JSON
Raw Permalink Blame History

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