217 lines
15 KiB
JSON
217 lines
15 KiB
JSON
{
|
||
"name": "Flutter 开发实战",
|
||
"lvNames": ["Widget 基础", "布局与组件", "状态管理", "路由与导航", "数据与网络", "实战综合"],
|
||
"questions": [
|
||
{
|
||
"lv": 1,
|
||
"q": "新建 Flutter 项目后,把应用真正跑起来的入口代码是?",
|
||
"opts": ["<code>void main() => runApp(MyApp());</code>", "<code>void main() { MyApp(); }</code>", "<code>runApp() { MyApp }</code>", "<code>main() { MaterialApp(); }</code>"],
|
||
"ans": 0,
|
||
"exp": "Dart 程序入口是 <code>main()</code>,Flutter 通过 <code>runApp(Widget)</code> 把根组件挂载到引擎上。注意 <code>MyApp()</code> 是创建 Widget 实例,不能少了括号。"
|
||
},
|
||
{
|
||
"lv": 1,
|
||
"q": "实现一个\"3 秒倒计时\"按钮,每秒刷新显示剩余秒数,应使用?",
|
||
"opts": ["StatelessWidget", "StatefulWidget", "Container", "Scaffold"],
|
||
"ans": 1,
|
||
"exp": "有<b>动态变化的数据</b>(倒计时数字)就需要 StatefulWidget:数据保存在 State 里,变化时调 <code>setState()</code> 刷新。没有可变状态时应优先用 StatelessWidget。"
|
||
},
|
||
{
|
||
"lv": 1,
|
||
"q": "界面上要显示一行固定文案\"登录成功\",最合适的组件是?",
|
||
"opts": ["Text", "Label", "Paragraph", "RichText"],
|
||
"ans": 0,
|
||
"exp": "<code>Text</code> 是最常用的文本组件,可通过 style 配置字号、颜色、字重等。RichText 用于一段文字中混排多种样式的富文本场景。"
|
||
},
|
||
{
|
||
"lv": 1,
|
||
"q": "点击\"确认\"按钮后分数要 +1 并立即显示到界面,正确流程是?",
|
||
"opts": ["修改 State 字段后调用 <code>setState()</code>", "直接修改字段就行", "重启整个 App", "修改 Widget 的构造参数"],
|
||
"ans": 0,
|
||
"exp": "State 里的字段只是普通数据,必须调用 <code>setState()</code> 通知框架\"数据变了\",框架才会重新执行 build 刷新 UI。注意 setState 只重建<b>当前 State 的子树</b>,不是整棵树。"
|
||
},
|
||
{
|
||
"lv": 1,
|
||
"q": "要展示 10000 条商品记录,性能最优的做法是?",
|
||
"opts": ["<code>ListView.builder</code>(按需构建可见项)", "Column 里循环放 10000 个组件", "SingleChildScrollView 包 Column", "GridView.count 全部构建"],
|
||
"ans": 0,
|
||
"exp": "<code>ListView.builder</code> 只构建<b>当前可见</b>的列表项(懒加载),滚动时回收复用;Column + for 会一次性构建全部组件,万条数据直接卡死。数据量大优先 builder 系列。"
|
||
},
|
||
{
|
||
"lv": 2,
|
||
"q": "登录页要垂直排列\"输入框、输入框、登录按钮\",用哪个布局组件?",
|
||
"opts": ["Column", "Row", "Stack", "Wrap"],
|
||
"ans": 0,
|
||
"exp": "<code>Column</code> 垂直方向排列子组件,<code>Row</code> 水平方向,<code>Stack</code> 叠加并支持定位,<code>Wrap</code> 在空间不足时自动换行。"
|
||
},
|
||
{
|
||
"lv": 2,
|
||
"q": "Row 里想让一个按钮<b>占满剩余宽度</b>(剩余空间全给它),按钮外层套?",
|
||
"opts": ["<code>Expanded</code>", "<code>Padding</code>", "<code>Center</code>", "<code>Spacer</code>"],
|
||
"ans": 0,
|
||
"exp": "<code>Expanded</code> 让子组件占据主轴的剩余空间(Row 的主轴是水平方向)。多个 Expanded 可通过 flex 参数按比例分配空间。"
|
||
},
|
||
{
|
||
"lv": 2,
|
||
"q": "Column 里有几个按钮,想让它们在<b>水平方向居中</b>,应该设置?",
|
||
"opts": ["<code>crossAxisAlignment: CrossAxisAlignment.center</code>", "<code>mainAxisAlignment: MainAxisAlignment.center</code>", "<code>alignment: Alignment.center</code>", "<code>textAlign: TextAlign.center</code>"],
|
||
"ans": 0,
|
||
"exp": "Column 的<b>主轴是垂直</b>方向、<b>交叉轴是水平</b>方向。水平居中=沿交叉轴居中=crossAxisAlignment.center;mainAxisAlignment 控制的是垂直方向。主/交叉轴搞反是新手最常踩的坑。"
|
||
},
|
||
{
|
||
"lv": 2,
|
||
"q": "封面图要<b>铺满整个区域且不拉伸变形</b>(多出的部分裁掉),fit 参数应该用?",
|
||
"opts": ["<code>BoxFit.cover</code>", "<code>BoxFit.fill</code>", "<code>BoxFit.none</code>", "<code>BoxFit.scaleDown</code>"],
|
||
"ans": 0,
|
||
"exp": "<b>cover</b>:等比缩放填满、超出裁剪,常用于头像、封面;<b>fill</b> 会拉伸变形;<b>contain</b> 完整显示但可能留白;<b>none</b> 不缩放。"
|
||
},
|
||
{
|
||
"lv": 2,
|
||
"q": "商品名过长时希望<b>单行显示,超出部分显示省略号</b>,Text 的配置是?",
|
||
"opts": ["<code>maxLines: 1, overflow: TextOverflow.ellipsis</code>", "只设 overflow: TextOverflow.clip", "只设 maxLines: 3", "<code>softWrap: false</code> 就够了"],
|
||
"ans": 0,
|
||
"exp": "<code>maxLines</code> 限制最大行数 + <code>overflow: TextOverflow.ellipsis</code> 指定超出的表现(省略号/裁剪/fade),两者配合才能出现\"…\"效果。"
|
||
},
|
||
{
|
||
"lv": 3,
|
||
"q": "输入框需要\"预填默认值 + 按钮点击后读取内容 + 随时清空\",推荐用?",
|
||
"opts": ["<code>TextEditingController</code>", "每次读 onChanged 参数", "GlobalKey 临时查找", "无法实现"],
|
||
"ans": 0,
|
||
"exp": "<code>TextEditingController</code> 是输入框的\"数据源\":<code>TextEditingController(text: \"预填\")</code> 初始化,<code>controller.text</code> 读取,<code>controller.clear()</code> 清空。用完后在 dispose 里释放。"
|
||
},
|
||
{
|
||
"lv": 3,
|
||
"q": "页面销毁时,取消监听、释放 Controller 的代码应该写在哪里?",
|
||
"opts": ["<code>dispose()</code>", "<code>initState()</code>", "<code>build()</code>", "<code>didChangeDependencies()</code>"],
|
||
"ans": 0,
|
||
"exp": "<code>dispose()</code> 是 State 被移除时的清理钩子:取消 Timer、Stream 订阅、释放 Controller。不清理会造成内存泄漏和 \"setState called after dispose\" 报错。"
|
||
},
|
||
{
|
||
"lv": 3,
|
||
"q": "登录状态、用户信息要被<b>多个页面共享</b>,又不想层层传构造参数,项目常用方案是?",
|
||
"opts": ["Provider / InheritedWidget", "定义全局变量直接读写", "每个页面重新请求接口", "把状态塞进路由参数"],
|
||
"ans": 0,
|
||
"exp": "Flutter 官方的底层方案是 <b>InheritedWidget</b>,<b>Provider</b> 是对它的优雅封装:共享数据放顶层,子组件 <code>context.watch()</code> 取用并自动刷新。全局变量没有通知机制,页面不会自动更新。"
|
||
},
|
||
{
|
||
"lv": 3,
|
||
"q": "商品列表页要支持<b>下拉刷新</b>,应该把 ListView 包进哪个组件?",
|
||
"opts": ["<code>RefreshIndicator</code>", "<code>GestureDetector</code>", "<code>AnimatedList</code>", "<code>Scrollbar</code>"],
|
||
"ans": 0,
|
||
"exp": "<code>RefreshIndicator</code> 包住可滚动组件,配合 <code>onRefresh</code> 返回的 Future 实现下拉刷新动画。这是 Material 规范自带的下拉刷新方案。"
|
||
},
|
||
{
|
||
"lv": 3,
|
||
"q": "App 切到后台、再回到前台时,需要暂停/恢复计时器,通过什么监听?",
|
||
"opts": ["<code>WidgetsBindingObserver</code>", "<code>RouteObserver</code>", "<code>NavigatorObserver</code>", "<code>MediaQuery</code>"],
|
||
"ans": 0,
|
||
"exp": "给 State 混入 <code>with WidgetsBindingObserver</code>,在 <code>didChangeAppLifecycleState()</code> 中监听 AppLifecycleState(paused / resumed / inactive),即可感知前后台切换。"
|
||
},
|
||
{
|
||
"lv": 4,
|
||
"q": "列表页点击商品跳转详情页,标准写法是?",
|
||
"opts": ["<code>Navigator.push(context, MaterialPageRoute(builder: (_) => DetailPage(id: id)))</code>", "<code>Navigator.go(DetailPage(id: id))</code>", "<code>Route.open(DetailPage())</code>", "<code>DetailPage(id: id).show()</code>"],
|
||
"ans": 0,
|
||
"exp": "<code>Navigator.push</code> 把新路由压入导航栈,MaterialPageRoute 提供转场动画,返回上一页用 <code>Navigator.pop(context)</code>。"
|
||
},
|
||
{
|
||
"lv": 4,
|
||
"q": "详情页编辑完要把结果<b>回传给列表页</b>,详情页返回时应该写?",
|
||
"opts": ["<code>Navigator.pop(context, result)</code>", "<code>Navigator.back(result)</code>", "<code>Navigator.pushResult(result)</code>", "<code>context.popAndPush(result)</code>"],
|
||
"ans": 0,
|
||
"exp": "<code>pop(context, 返回值)</code> 的第二个参数就是回传的数据,列表页 <code>await push(...)</code> 的返回值即可收到。注意 pop 的参数是\"返回给上一页的数据\",不是新路由。"
|
||
},
|
||
{
|
||
"lv": 4,
|
||
"q": "跳转时需要传递商品 id、名称等参数,团队协作中<b>最推荐</b>的传参方式是?",
|
||
"opts": ["<code>DetailPage(id: 1, name: \"商品\")</code> 构造参数传递", "全局变量临时存", "static 静态字段存", "URL 字符串拼接"],
|
||
"ans": 0,
|
||
"exp": "构造参数显式、类型安全、可读性好——跳转处一眼看到传了什么。全局变量/静态字段是隐藏依赖,页面多起来后非常难维护。"
|
||
},
|
||
{
|
||
"lv": 4,
|
||
"q": "在 App 里注册命名路由(如 <code>/login</code>、<code>/home</code>),配置位置是?",
|
||
"opts": ["<code>MaterialApp(routes: {...})</code>", "<code>main()</code> 函数里", "<code>runApp()</code> 参数", "每个页面里各自声明"],
|
||
"ans": 0,
|
||
"exp": "命名路由集中配置在 <code>MaterialApp</code> 的 <code>routes</code> 表,之后用 <code>Navigator.pushNamed(context, \"/login\")</code> 跳转;需要传参时配合 <code>arguments</code> 参数。"
|
||
},
|
||
{
|
||
"lv": 4,
|
||
"q": "跳到选择页,用户选完返回后要根据<b>选择结果</b>继续处理,列表页怎么写?",
|
||
"opts": ["<code>final r = await Navigator.push(...);</code> 拿到返回值再处理", "无法等待结果", "用 setState 轮询", "跳转时把回调函数传过去"],
|
||
"ans": 0,
|
||
"exp": "<code>push</code> 返回 <code>Future<T></code>,await 后拿到 pop 回传的值,再执行后续逻辑(如刷新选中项)。这是\"页面间取结果\"的标准姿势。"
|
||
},
|
||
{
|
||
"lv": 5,
|
||
"q": "接口返回 JSON 字符串,执行 <code>var data = jsonDecode(text);</code> 后 data 的类型是?",
|
||
"opts": ["<code>dynamic</code>(实际可能是 Map 或 List)", "<code>String</code>", "<code>Map<String, dynamic></code>", "<code>Object</code>"],
|
||
"ans": 0,
|
||
"exp": "<code>jsonDecode</code> 的返回类型是 <code>dynamic</code>:JSON 对象变 Map<String, dynamic>、数组变 List<dynamic>、数字/布尔原样返回。通常需要手动转换:<code>jsonDecode(text) as Map<String, dynamic></code>。"
|
||
},
|
||
{
|
||
"lv": 5,
|
||
"q": "要把用户对象 <code>User(id: 1, name: \"张三\")</code> 转成 JSON 字符串发给服务器,正确流程是?",
|
||
"opts": ["给 User 写 <code>toJson()</code>,再 <code>jsonEncode(user.toJson())</code>", "<code>jsonEncode(user)</code> 直接序列化", "<code>user.toString()</code>", "<code>JSON.stringify(user)</code>"],
|
||
"ans": 0,
|
||
"exp": "Dart 默认不会自动序列化任意对象,惯例是模型类自己实现 <code>Map<String, dynamic> toJson()</code>,再用 <code>jsonEncode</code> 转字符串;反序列化则对应 <code>User.fromJson(Map)</code> 工厂构造。"
|
||
},
|
||
{
|
||
"lv": 5,
|
||
"q": "网络请求可能超时/断网,用户点\"登录\"时抛出了异常,正确的处理是?",
|
||
"opts": ["用 try-catch 包裹 await 请求,捕获异常并提示用户", "不做处理", "打印日志后忽略", "延迟 3 秒自动重试"],
|
||
"ans": 0,
|
||
"exp": "<code>try { await api.login(...) } catch (e) { 提示\"网络异常,请重试\" }</code> 是最基础的错误处理;生产环境还会区分 SocketException(断网)、TimeoutException(超时)等做分类提示。"
|
||
},
|
||
{
|
||
"lv": 5,
|
||
"q": "请求进行中需要展示\"加载中\"状态,最合适用?",
|
||
"opts": ["<code>CircularProgressIndicator</code>", "Text(\"加载中...\")", "Opacity 半透明遮罩", "AnimatedContainer 闪烁"],
|
||
"ans": 0,
|
||
"exp": "Material 标准的加载指示器是 <code>CircularProgressIndicator</code>(转圈)和 <code>LinearProgressIndicator</code>(条),配合 loading 状态变量做条件渲染。"
|
||
},
|
||
{
|
||
"lv": 5,
|
||
"q": "http 请求要设置 10 秒超时,正确写法是?",
|
||
"opts": ["<code>await http.get(url).timeout(Duration(seconds: 10))</code>", "<code>http.get(url, timeout: 10)</code>", "<code>Timer(Duration(seconds: 10), ...)</code>", "<code>Future.any</code> 自动超时"],
|
||
"ans": 0,
|
||
"exp": "Future 自带 <code>.timeout()</code>:超时抛 TimeoutException,可再接 <code>onTimeout</code> 参数兜底。http 包本身没有 timeout 参数,这是常见的误解。"
|
||
},
|
||
{
|
||
"lv": 6,
|
||
"q": "首页同时请求\"轮播图、公告、商品列表\"三个接口,全部完成再渲染页面,用?",
|
||
"opts": ["<code>Future.wait([...])</code> 并发等待", "三个 await 串行", "依次 catch 分别显示", "用 <code>Future.any</code>"],
|
||
"ans": 0,
|
||
"exp": "并发场景用 <code>Future.wait</code>,总耗时约等于最慢的请求而不是三个相加。生产上可配合记录单个请求失败做局部降级(某块失败不影响整页)。"
|
||
},
|
||
{
|
||
"lv": 6,
|
||
"q": "商品列表滑到底部要自动加载下一页,监听滚动的方案是?",
|
||
"opts": ["<code>ScrollController</code> 监听 pixels 接近 maxScrollExtent 时触发", "用 Timer 每 3 秒加载", "下拉刷新代替", "GestureDetector 监听手势"],
|
||
"ans": 0,
|
||
"exp": "给 ListView 挂 <code>ScrollController</code>,在 <code>addListener</code> 里判断 <code>position.pixels >= position.maxScrollExtent - 200</code>(预留阈值)就加载下一页,配合 loading 状态防止重复触发。"
|
||
},
|
||
{
|
||
"lv": 6,
|
||
"q": "商品标题中要<b>红色高亮\"热卖\"二字</b>、其余部分灰色,用?",
|
||
"opts": ["<code>RichText</code> + <code>TextSpan</code> 组合不同样式", "拆成多个 Text 排一排", "用换行符拼接字符串", "TextStyle 做不到,只能整段同色"],
|
||
"ans": 0,
|
||
"exp": "<code>RichText</code> 的 <code>TextSpan</code> 支持树状嵌套:每个 span 可以有自己的 style,是\"一段文字多种样式\"的标准方案。"
|
||
},
|
||
{
|
||
"lv": 6,
|
||
"q": "界面出现<b>黄黑条纹</b>溢出警告(RenderFlex overflowed),说明?",
|
||
"opts": ["子组件超出了父组件给定的约束空间", "主题颜色配置错误", "内存泄漏", "字体加载失败"],
|
||
"ans": 0,
|
||
"exp": "Row/Column 空间不够时子组件溢出,debug 模式显示黄黑条纹并打印 \"RenderFlex overflowed by XX pixels\"。解决思路:Expanded/Flexible 让出空间、SingleChildScrollView 滚动、FittedBox 缩放。"
|
||
},
|
||
{
|
||
"lv": 6,
|
||
"q": "瀑布流商品图列表很卡、图片解码占内存高,优先优化手段是?",
|
||
"opts": ["<code>cacheWidth</code> / <code>ResizeImage</code> 限制解码尺寸", "给图片加圆角", "提高图片的清晰度", "减少列表项数量"],
|
||
"ans": 0,
|
||
"exp": "Image.network 默认按原图尺寸解码,一张 4000px 大图在手机上就是内存杀手。用 <code>cacheWidth: 300</code> 或 <code>ResizeImage</code> 让引擎按需解码成小尺寸位图,内存占用和滚动性能立竿见影。"
|
||
}
|
||
]
|
||
}
|