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