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