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