[{"data":1,"prerenderedAt":37389},["ShallowReactive",2],{"all-notes":3},[4,764,1040,1647,1884,15439,19703,21373,28609,36373,36645,37151],{"id":5,"title":6,"body":7,"category":752,"date":753,"description":754,"extension":755,"meta":756,"navigation":757,"order":752,"path":758,"seo":759,"seoDescription":754,"seoTitle":760,"slug":761,"stem":762,"__hash__":763},"notes\u002Fnotes\u002F2026-05-08-immersive-reading-design.md","沉浸式阅读网站设计实践",{"type":8,"value":9,"toc":736},"minimark",[10,17,21,24,28,31,35,119,122,128,154,159,179,182,185,320,323,326,372,375,378,381,437,478,481,484,586,590,612,673,677,680,699,702,705,732],[11,12,13],"blockquote",{},[14,15,16],"p",{},"记录时间：2026-05-08",[18,19,20],"h2",{"id":20},"问题",[14,22,23],{},"为一个基于 Nuxt 3 + Vue 3 的中文内容网站（多元思维模型）设计沉浸式阅读体验。网站以 Markdown 长文为主要内容，需要在排版、配色、交互上全面优化阅读舒适度。",[18,25,27],{"id":26},"设计方向墨韵ink-charm","设计方向：墨韵（Ink Charm）",[14,29,30],{},"整体走温润的书卷气路线，不是冰冷的科技感，而是像翻开一本好书的感觉。",[32,33,34],"h3",{"id":34},"核心排版规范",[36,37,38,51],"table",{},[39,40,41],"thead",{},[42,43,44,48],"tr",{},[45,46,47],"th",{},"项目",[45,49,50],{},"规范",[52,53,54,63,71,79,87,95,103,111],"tbody",{},[42,55,56,60],{},[57,58,59],"td",{},"中文字体",[57,61,62],{},"霞鹜文楷（LXGW WenKai TC）—— 温润不刺眼",[42,64,65,68],{},[57,66,67],{},"英文\u002F数字",[57,69,70],{},"Inter",[42,72,73,76],{},[57,74,75],{},"正文字重",[57,77,78],{},"400（常规），拒绝粗黑生硬",[42,80,81,84],{},[57,82,83],{},"PC 正文字号",[57,85,86],{},"17-18px",[42,88,89,92],{},[57,90,91],{},"移动端正文字号",[57,93,94],{},"16px",[42,96,97,100],{},[57,98,99],{},"行高",[57,101,102],{},"1.7-1.8（长文不累眼）",[42,104,105,108],{},[57,106,107],{},"段间距",[57,109,110],{},"上下 1em，留白透气",[42,112,113,116],{},[57,114,115],{},"首行缩进",[57,117,118],{},"全局禁止（网文阅读更舒服）",[32,120,121],{"id":121},"配色方案",[14,123,124],{},[125,126,127],"strong",{},"浅色模式：",[129,130,131,140,147],"ul",{},[132,133,134,135,139],"li",{},"文字 ",[136,137,138],"code",{},"#2c2c2c","（非纯黑，护眼）",[132,141,142,143,146],{},"背景 ",[136,144,145],{},"#fafafa","（低饱和灰白）",[132,148,149,150,153],{},"链接 ",[136,151,152],{},"#4a7cad","（低饱和蓝，不浮夸）",[14,155,156],{},[125,157,158],{},"深色模式：",[129,160,161,166,172],{},[132,162,134,163],{},[136,164,165],{},"#e5e5e5",[132,167,142,168,171],{},[136,169,170],{},"#1a1a1a","（低饱和灰，不刺眼）",[132,173,174,175,178],{},"代码块 ",[136,176,177],{},"#282c34"," 柔和暗色背景",[32,180,181],{"id":181},"标题层级设计",[14,183,184],{},"h1 → h6 逐级缩小，不超大加粗，font-weight 统一用 600：",[186,187,192],"pre",{"className":188,"code":189,"language":190,"meta":191,"style":191},"language-css shiki shiki-themes github-light github-dark",".prose h1 { font-size: 24px; }\n.prose h2 { font-size: 20px; }\n.prose h3 { font-size: 17px; }\n.prose h4 { font-size: 15px; }\n.prose h5, .prose h6 { font-size: 14px; }\n","css","",[136,193,194,228,249,270,291],{"__ignoreMap":191},[195,196,199,203,207,211,215,218,221,225],"span",{"class":197,"line":198},"line",1,[195,200,202],{"class":201},"sScJk",".prose",[195,204,206],{"class":205},"s9eBZ"," h1",[195,208,210],{"class":209},"sVt8B"," { ",[195,212,214],{"class":213},"sj4cs","font-size",[195,216,217],{"class":209},": ",[195,219,220],{"class":213},"24",[195,222,224],{"class":223},"szBVR","px",[195,226,227],{"class":209},"; }\n",[195,229,231,233,236,238,240,242,245,247],{"class":197,"line":230},2,[195,232,202],{"class":201},[195,234,235],{"class":205}," h2",[195,237,210],{"class":209},[195,239,214],{"class":213},[195,241,217],{"class":209},[195,243,244],{"class":213},"20",[195,246,224],{"class":223},[195,248,227],{"class":209},[195,250,252,254,257,259,261,263,266,268],{"class":197,"line":251},3,[195,253,202],{"class":201},[195,255,256],{"class":205}," h3",[195,258,210],{"class":209},[195,260,214],{"class":213},[195,262,217],{"class":209},[195,264,265],{"class":213},"17",[195,267,224],{"class":223},[195,269,227],{"class":209},[195,271,273,275,278,280,282,284,287,289],{"class":197,"line":272},4,[195,274,202],{"class":201},[195,276,277],{"class":205}," h4",[195,279,210],{"class":209},[195,281,214],{"class":213},[195,283,217],{"class":209},[195,285,286],{"class":213},"15",[195,288,224],{"class":223},[195,290,227],{"class":209},[195,292,294,296,299,302,304,307,309,311,313,316,318],{"class":197,"line":293},5,[195,295,202],{"class":201},[195,297,298],{"class":205}," h5",[195,300,301],{"class":209},", ",[195,303,202],{"class":201},[195,305,306],{"class":205}," h6",[195,308,210],{"class":209},[195,310,214],{"class":213},[195,312,217],{"class":209},[195,314,315],{"class":213},"14",[195,317,224],{"class":223},[195,319,227],{"class":209},[14,321,322],{},"标题和正文之间间距拉开，层级清晰。",[32,324,325],{"id":325},"细节处理",[129,327,328,342,352,366],{},[132,329,330,333,334,337,338,341],{},[125,331,332],{},"图片","：",[136,335,336],{},"width: 100%"," 自适应 + ",[136,339,340],{},"border-radius: 6px"," 圆角",[132,343,344,347,348,351],{},[125,345,346],{},"引用块","：浅底色 ",[136,349,350],{},"var(--surface)"," + 左边 3px 细线，不抢视觉",[132,353,354,357,358,361,362,365],{},[125,355,356],{},"代码块","：柔和暗色背景，",[136,359,360],{},"white-space: pre-wrap"," 自动换行 + ",[136,363,364],{},"overflow-x: auto"," 横向滚动",[132,367,368,371],{},[125,369,370],{},"表格\u002F列表","：紧凑但不拥挤",[18,373,374],{"id":374},"交互增强",[32,376,377],{"id":377},"阅读进度条",[14,379,380],{},"页面顶部 2px 细条，accent 色，跟随滚动进度缩放：",[186,382,386],{"className":383,"code":384,"language":385,"meta":191,"style":191},"language-typescript shiki shiki-themes github-light github-dark","const readingProgress = ref(0)\n\u002F\u002F 在 scroll 事件中\nreadingProgress.value = scrollY \u002F (docHeight) \u002F\u002F 0~1\n","typescript",[136,387,388,411,417],{"__ignoreMap":191},[195,389,390,393,396,399,402,405,408],{"class":197,"line":198},[195,391,392],{"class":223},"const",[195,394,395],{"class":213}," readingProgress",[195,397,398],{"class":223}," =",[195,400,401],{"class":201}," ref",[195,403,404],{"class":209},"(",[195,406,407],{"class":213},"0",[195,409,410],{"class":209},")\n",[195,412,413],{"class":197,"line":230},[195,414,416],{"class":415},"sJ8bj","\u002F\u002F 在 scroll 事件中\n",[195,418,419,422,425,428,431,434],{"class":197,"line":251},[195,420,421],{"class":209},"readingProgress.value ",[195,423,424],{"class":223},"=",[195,426,427],{"class":209}," scrollY ",[195,429,430],{"class":223},"\u002F",[195,432,433],{"class":209}," (docHeight) ",[195,435,436],{"class":415},"\u002F\u002F 0~1\n",[186,438,442],{"className":439,"code":440,"language":441,"meta":191,"style":191},"language-html shiki shiki-themes github-light github-dark","\u003Cdiv class=\"reading-progress-bar\"\n  :style=\"{ transform: `scaleX(${readingProgress})` }\" \u002F>\n","html",[136,443,444,461],{"__ignoreMap":191},[195,445,446,449,452,455,457],{"class":197,"line":198},[195,447,448],{"class":209},"\u003C",[195,450,451],{"class":205},"div",[195,453,454],{"class":201}," class",[195,456,424],{"class":209},[195,458,460],{"class":459},"sZZnC","\"reading-progress-bar\"\n",[195,462,463,466,468,471,475],{"class":197,"line":230},[195,464,465],{"class":201},"  :style",[195,467,424],{"class":209},[195,469,470],{"class":459},"\"{ transform: `scaleX(${readingProgress})` }\"",[195,472,474],{"class":473},"s7hpK"," \u002F",[195,476,477],{"class":209},">\n",[32,479,480],{"id":480},"自动隐藏导航栏",[14,482,483],{},"向下滚动超过 200px 后隐藏 header，向上滚动立即恢复。配合毛玻璃效果：",[186,485,487],{"className":188,"code":486,"language":190,"meta":191,"style":191},".header {\n  backdrop-filter: blur(12px) saturate(1.2);\n  transition: transform 0.35s ease;\n}\n.header--hidden {\n  transform: translateY(-100%);\n}\n",[136,488,489,497,528,548,553,560,581],{"__ignoreMap":191},[195,490,491,494],{"class":197,"line":198},[195,492,493],{"class":201},".header",[195,495,496],{"class":209}," {\n",[195,498,499,502,504,507,509,512,514,517,520,522,525],{"class":197,"line":230},[195,500,501],{"class":213},"  backdrop-filter",[195,503,217],{"class":209},[195,505,506],{"class":213},"blur",[195,508,404],{"class":209},[195,510,511],{"class":213},"12",[195,513,224],{"class":223},[195,515,516],{"class":209},") ",[195,518,519],{"class":213},"saturate",[195,521,404],{"class":209},[195,523,524],{"class":213},"1.2",[195,526,527],{"class":209},");\n",[195,529,530,533,536,539,542,545],{"class":197,"line":251},[195,531,532],{"class":213},"  transition",[195,534,535],{"class":209},": transform ",[195,537,538],{"class":213},"0.35",[195,540,541],{"class":223},"s",[195,543,544],{"class":213}," ease",[195,546,547],{"class":209},";\n",[195,549,550],{"class":197,"line":272},[195,551,552],{"class":209},"}\n",[195,554,555,558],{"class":197,"line":293},[195,556,557],{"class":201},".header--hidden",[195,559,496],{"class":209},[195,561,563,566,568,571,573,576,579],{"class":197,"line":562},6,[195,564,565],{"class":213},"  transform",[195,567,217],{"class":209},[195,569,570],{"class":213},"translateY",[195,572,404],{"class":209},[195,574,575],{"class":213},"-100",[195,577,578],{"class":223},"%",[195,580,527],{"class":209},[195,582,584],{"class":197,"line":583},7,[195,585,552],{"class":209},[32,587,589],{"id":588},"亮色暗色切换","亮色\u002F暗色切换",[129,591,592,603,606,609],{},[132,593,594,595,598,599,602],{},"使用 ",[136,596,597],{},"html.dark"," class 控制，替代 ",[136,600,601],{},"prefers-color-scheme"," media query",[132,604,605],{},"localStorage 持久化用户偏好",[132,607,608],{},"nuxt.config 注入内联脚本防止页面闪烁（FOUC）",[132,610,611],{},"按钮放在导航栏右侧，日\u002F月图标带旋转过渡动画",[186,613,615],{"className":383,"code":614,"language":385,"meta":191,"style":191},"\u002F\u002F composables\u002FuseTheme.ts\nfunction applyTheme(t: Theme) {\n  document.documentElement.classList.toggle('dark', t === 'dark')\n}\n",[136,616,617,622,645,669],{"__ignoreMap":191},[195,618,619],{"class":197,"line":198},[195,620,621],{"class":415},"\u002F\u002F composables\u002FuseTheme.ts\n",[195,623,624,627,630,632,636,639,642],{"class":197,"line":230},[195,625,626],{"class":223},"function",[195,628,629],{"class":201}," applyTheme",[195,631,404],{"class":209},[195,633,635],{"class":634},"s4XuR","t",[195,637,638],{"class":223},":",[195,640,641],{"class":201}," Theme",[195,643,644],{"class":209},") {\n",[195,646,647,650,653,655,658,661,664,667],{"class":197,"line":251},[195,648,649],{"class":209},"  document.documentElement.classList.",[195,651,652],{"class":201},"toggle",[195,654,404],{"class":209},[195,656,657],{"class":459},"'dark'",[195,659,660],{"class":209},", t ",[195,662,663],{"class":223},"===",[195,665,666],{"class":459}," 'dark'",[195,668,410],{"class":209},[195,670,671],{"class":197,"line":272},[195,672,552],{"class":209},[32,674,676],{"id":675},"文章目录toc布局","文章目录（TOC）布局",[14,678,679],{},"TOC 放在文章右侧比左侧更合理：",[129,681,682,685,688],{},[132,683,684],{},"主内容左对齐符合阅读视线起点",[132,686,687],{},"目录作为辅助导航放右侧不干扰阅读流",[132,689,690,691,694,695,698],{},"hover 时右边线高亮（",[136,692,693],{},"border-right"," 而非 ",[136,696,697],{},"border-left","）",[18,700,701],{"id":701},"小结",[14,703,704],{},"沉浸式阅读的核心不是花哨的动效，而是让读者忘记界面的存在。关键原则：",[706,707,708,714,720,726],"ol",{},[132,709,710,713],{},[125,711,712],{},"字体温润","：霞鹜文楷给中文长文最舒适的阅读感",[132,715,716,719],{},[125,717,718],{},"配色克制","：非纯黑文字、低饱和背景、低调链接色",[132,721,722,725],{},[125,723,724],{},"间距透气","：1em 段间距、1.8 行高，长文不累",[132,727,728,731],{},[125,729,730],{},"交互隐身","：导航自动隐藏、进度条极细、主题切换不突兀",[733,734,735],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s7hpK, html code.shiki .s7hpK{--shiki-default:#B31D28;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":191,"searchDepth":230,"depth":230,"links":737},[738,739,745,751],{"id":20,"depth":230,"text":20},{"id":26,"depth":230,"text":27,"children":740},[741,742,743,744],{"id":34,"depth":251,"text":34},{"id":121,"depth":251,"text":121},{"id":181,"depth":251,"text":181},{"id":325,"depth":251,"text":325},{"id":374,"depth":230,"text":374,"children":746},[747,748,749,750],{"id":377,"depth":251,"text":377},{"id":480,"depth":251,"text":480},{"id":588,"depth":251,"text":589},{"id":675,"depth":251,"text":676},{"id":701,"depth":230,"text":701},null,"2026-05-08 00:00:00 CST","为思维模型网站设计沉浸式阅读体验，涵盖排版规范、主题切换、自动隐藏导航栏等完整方案。","md",{},true,"\u002Fnotes\u002F2026-05-08-immersive-reading-design",{"title":6,"description":754},"沉浸式阅读网站设计实践｜个人笔记","immersive-reading-design","notes\u002F2026-05-08-immersive-reading-design","ZwFFq1v_Cuo_nPMc9S0wzR1KNBqGARx0kOD4-i_oNA4",{"id":765,"title":766,"body":767,"category":752,"date":1030,"description":1031,"extension":755,"meta":1032,"navigation":757,"order":752,"path":1033,"seo":1034,"seoDescription":1035,"seoTitle":1036,"slug":1037,"stem":1038,"__hash__":1039},"notes\u002Fnotes\u002F2026-05-07-claude-code-auto-approve.md","Claude Code 自动跳过权限确认的三种方式",{"type":8,"value":768,"toc":1023},[769,774,776,779,783,798,801,805,815,841,844,888,896,900,910,971,1001,1004,1007,1020],[11,770,771],{},[14,772,773],{},"记录时间：2026-05-07",[18,775,20],{"id":20},[14,777,778],{},"在使用 Claude Code 执行任务时，经常遇到权限确认提示（yes\u002Fno），需要手动选择，打断工作流。如何让它自动选择 yes、跳过确认？",[18,780,782],{"id":781},"方法一启动时加标志最简单粗暴","方法一：启动时加标志（最简单粗暴）",[186,784,788],{"className":785,"code":786,"language":787,"meta":191,"style":191},"language-bash shiki shiki-themes github-light github-dark","claude --dangerously-skip-permissions\n","bash",[136,789,790],{"__ignoreMap":191},[195,791,792,795],{"class":197,"line":198},[195,793,794],{"class":201},"claude",[195,796,797],{"class":213}," --dangerously-skip-permissions\n",[14,799,800],{},"直接跳过所有权限提示。适合在安全、可控的环境中使用，不建议在生产环境或敏感项目中开启。",[18,802,804],{"id":803},"方法二设置默认权限模式","方法二：设置默认权限模式",[14,806,807,808,811,812,333],{},"在 ",[136,809,810],{},".claude\u002Fsettings.json"," 中配置 ",[136,813,814],{},"defaultMode",[186,816,820],{"className":817,"code":818,"language":819,"meta":191,"style":191},"language-json shiki shiki-themes github-light github-dark","{\n  \"defaultMode\": \"acceptEdits\"\n}\n","json",[136,821,822,827,837],{"__ignoreMap":191},[195,823,824],{"class":197,"line":198},[195,825,826],{"class":209},"{\n",[195,828,829,832,834],{"class":197,"line":230},[195,830,831],{"class":213},"  \"defaultMode\"",[195,833,217],{"class":209},[195,835,836],{"class":459},"\"acceptEdits\"\n",[195,838,839],{"class":197,"line":251},[195,840,552],{"class":209},[14,842,843],{},"可选模式：",[36,845,846,856],{},[39,847,848],{},[42,849,850,853],{},[45,851,852],{},"模式",[45,854,855],{},"行为",[52,857,858,868,878],{},[42,859,860,865],{},[57,861,862],{},[136,863,864],{},"\"plan\"",[57,866,867],{},"只读模式，只能读文件和执行只读命令",[42,869,870,875],{},[57,871,872],{},[136,873,874],{},"\"acceptEdits\"",[57,876,877],{},"自动批准文件编辑和常用文件操作（mkdir、touch、mv、cp），危险命令仍会提示",[42,879,880,885],{},[57,881,882],{},[136,883,884],{},"\"bypassPermissions\"",[57,886,887],{},"跳过所有权限提示",[14,889,890,895],{},[125,891,892,893],{},"推荐日常开发使用 ",[136,894,874],{},"，兼顾效率和安全。",[18,897,899],{"id":898},"方法三针对特定工具预授权最精细","方法三：针对特定工具预授权（最精细）",[14,901,807,902,905,906,909],{},[136,903,904],{},"settings.json"," 的 ",[136,907,908],{},"permissions.allow"," 数组中添加规则，只对匹配的操作自动放行：",[186,911,913],{"className":817,"code":912,"language":819,"meta":191,"style":191},"{\n  \"permissions\": {\n    \"allow\": [\n      \"Bash(npm run *)\",\n      \"Bash(git *)\",\n      \"Edit\"\n    ]\n  }\n}\n",[136,914,915,919,927,935,943,950,955,960,966],{"__ignoreMap":191},[195,916,917],{"class":197,"line":198},[195,918,826],{"class":209},[195,920,921,924],{"class":197,"line":230},[195,922,923],{"class":213},"  \"permissions\"",[195,925,926],{"class":209},": {\n",[195,928,929,932],{"class":197,"line":251},[195,930,931],{"class":213},"    \"allow\"",[195,933,934],{"class":209},": [\n",[195,936,937,940],{"class":197,"line":272},[195,938,939],{"class":459},"      \"Bash(npm run *)\"",[195,941,942],{"class":209},",\n",[195,944,945,948],{"class":197,"line":293},[195,946,947],{"class":459},"      \"Bash(git *)\"",[195,949,942],{"class":209},[195,951,952],{"class":197,"line":562},[195,953,954],{"class":459},"      \"Edit\"\n",[195,956,957],{"class":197,"line":583},[195,958,959],{"class":209},"    ]\n",[195,961,963],{"class":197,"line":962},8,[195,964,965],{"class":209},"  }\n",[195,967,969],{"class":197,"line":968},9,[195,970,552],{"class":209},[129,972,973,979,985,991],{},[132,974,975,978],{},[136,976,977],{},"Bash(npm run *)"," — 自动批准所有 npm 脚本",[132,980,981,984],{},[136,982,983],{},"Bash(git *)"," — 自动批准所有 git 命令",[132,986,987,990],{},[136,988,989],{},"Edit"," — 自动批准所有文件编辑",[132,992,993,994,997,998],{},"也支持路径限定，如 ",[136,995,996],{},"Edit(\u002Fsrc\u002F**)","、",[136,999,1000],{},"WebFetch(domain:github.com)",[14,1002,1003],{},"不匹配的操作仍然会弹出确认提示。",[18,1005,1006],{"id":1006},"建议",[14,1008,1009,1010,1019],{},"日常开发最佳实践：",[125,1011,1012,1014,1015,1018],{},[136,1013,874],{}," 模式 + 常用命令的 ",[136,1016,1017],{},"allow"," 规则","。既不用频繁点确认，又能在执行危险操作时收到提醒。",[733,1021,1022],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}",{"title":191,"searchDepth":230,"depth":230,"links":1024},[1025,1026,1027,1028,1029],{"id":20,"depth":230,"text":20},{"id":781,"depth":230,"text":782},{"id":803,"depth":230,"text":804},{"id":898,"depth":230,"text":899},{"id":1006,"depth":230,"text":1006},"2026-05-07 00:00:00 CST","总结 Claude Code 执行任务时遇到 yes\u002Fno 权限提示的自动跳过方法，包括启动标志、默认模式和针对性预授权规则。",{},"\u002Fnotes\u002F2026-05-07-claude-code-auto-approve",{"title":766,"description":1031},"如何让 Claude Code 在执行任务时自动跳过 yes\u002Fno 权限确认：dangerously-skip-permissions 标志、defaultMode 模式设置、permissions.allow 预授权规则。","Claude Code 自动跳过权限确认的三种方式｜个人笔记","claude-code-auto-approve","notes\u002F2026-05-07-claude-code-auto-approve","gp2lKyLVEj7NhegaOEswTxhtOZ5lqbB59ExZfDy3t4c",{"id":1041,"title":1042,"body":1043,"category":752,"date":1637,"description":1638,"extension":755,"meta":1639,"navigation":757,"order":752,"path":1640,"seo":1641,"seoDescription":1642,"seoTitle":1643,"slug":1644,"stem":1645,"__hash__":1646},"notes\u002Fnotes\u002F2026-05-06-buffett-sidebar-layout.md","巴菲特模块侧边栏布局重构笔记",{"type":8,"value":1044,"toc":1620},[1045,1050,1053,1060,1063,1066,1069,1077,1080,1138,1141,1149,1155,1168,1174,1312,1320,1325,1341,1349,1379,1387,1395,1429,1448,1455,1534,1538,1543,1556,1569,1572,1617],[11,1046,1047],{},[14,1048,1049],{},"记录时间：2026-05-06",[18,1051,1052],{"id":1052},"背景",[14,1054,1055,1056,1059],{},"巴菲特模块有 284 篇文章（致股东信 100 篇、访谈与文章 151 篇、股东大会 33 篇），原本全部以卡片网格平铺在 ",[136,1057,1058],{},"\u002Fbuffett"," 页面上，页面极长，浏览体验差。",[14,1061,1062],{},"目标：改为类似文档站的双栏布局——左侧固定侧边栏显示分类目录，右侧显示文章详情，两侧独立滚动。",[18,1064,1065],{"id":1065},"实现方案",[32,1067,1068],{"id":1068},"布局结构",[186,1070,1075],{"className":1071,"code":1073,"language":1074},[1072],"language-text","┌──────────────────────────────────────────┐\n│  Header (sticky)                         │\n├──────────┬───────────────────────────────┤\n│ 侧边栏    │  文章内容                      │\n│ (260px)  │  (overflow-y: auto)           │\n│          │                               │\n│ ▼ 致股东信 │  标题 \u002F 描述                   │\n│   1956…  │  ─────────                    │\n│   1957…  │  正文 (ContentRenderer)        │\n│          │                               │\n│ ▶ 访谈    │                               │\n│ ▶ 股东大会 │                               │\n│          │                               │\n│ (独立滚动) │  (独立滚动)                    │\n└──────────┴───────────────────────────────┘\n","text",[136,1076,1073],{"__ignoreMap":191},[32,1078,1079],{"id":1079},"关键文件",[36,1081,1082,1092],{},[39,1083,1084],{},[42,1085,1086,1089],{},[45,1087,1088],{},"文件",[45,1090,1091],{},"作用",[52,1093,1094,1104,1114,1124],{},[42,1095,1096,1101],{},[57,1097,1098],{},[136,1099,1100],{},"app\u002Fcomponents\u002FBuffettSidebar.vue",[57,1102,1103],{},"新建，侧边栏组件",[42,1105,1106,1111],{},[57,1107,1108],{},[136,1109,1110],{},"app\u002Fpages\u002Fbuffett\u002F[slug].vue",[57,1112,1113],{},"重构为双栏布局",[42,1115,1116,1121],{},[57,1117,1118],{},[136,1119,1120],{},"app\u002Fpages\u002Fbuffett\u002Findex.vue",[57,1122,1123],{},"改为重定向到第一篇文章",[42,1125,1126,1131],{},[57,1127,1128],{},[136,1129,1130],{},"app\u002Fassets\u002Fcss\u002Fmain.css",[57,1132,1133,1134,1137],{},"新增 ",[136,1135,1136],{},"--header-h"," 变量",[18,1139,1140],{"id":1140},"踩过的三个坑",[32,1142,1144,1145,1148],{"id":1143},"坑-1position-sticky-劫持滚动","坑 1：",[136,1146,1147],{},"position: sticky"," 劫持滚动",[14,1150,1151,1154],{},[125,1152,1153],{},"现象："," 侧边栏内容超出屏幕后无法滚动，鼠标滚轮只滚动右侧文章区。",[14,1156,1157,1160,1161,1163,1164,1167],{},[125,1158,1159],{},"原因："," 最初侧边栏使用 ",[136,1162,1147],{},"，它仍然处于页面的滚动流中。浏览器滚动事件优先作用于页面（即右侧内容区），而不是侧边栏内部的 ",[136,1165,1166],{},"overflow-y: auto"," 子元素。",[14,1169,1170,1173],{},[125,1171,1172],{},"修复："," 放弃 sticky 方案，改为双面板独立滚动模型：",[186,1175,1177],{"className":188,"code":1176,"language":190,"meta":191,"style":191},".buffett-layout {\n  height: calc(100vh - var(--header-h));\n  overflow: hidden;  \u002F* 父容器不滚动 *\u002F\n}\n\n.buffett-content {\n  overflow-y: auto;  \u002F* 右侧独立滚动 *\u002F\n}\n\n.sidebar-nav {\n  flex: 1;\n  overflow-y: auto;  \u002F* 左侧独立滚动 *\u002F\n}\n",[136,1178,1179,1186,1217,1233,1237,1242,1249,1264,1268,1272,1280,1293,1307],{"__ignoreMap":191},[195,1180,1181,1184],{"class":197,"line":198},[195,1182,1183],{"class":201},".buffett-layout",[195,1185,496],{"class":209},[195,1187,1188,1191,1193,1196,1198,1201,1204,1207,1210,1212,1214],{"class":197,"line":230},[195,1189,1190],{"class":213},"  height",[195,1192,217],{"class":209},[195,1194,1195],{"class":213},"calc",[195,1197,404],{"class":209},[195,1199,1200],{"class":213},"100",[195,1202,1203],{"class":223},"vh",[195,1205,1206],{"class":223}," -",[195,1208,1209],{"class":213}," var",[195,1211,404],{"class":209},[195,1213,1136],{"class":634},[195,1215,1216],{"class":209},"));\n",[195,1218,1219,1222,1224,1227,1230],{"class":197,"line":251},[195,1220,1221],{"class":213},"  overflow",[195,1223,217],{"class":209},[195,1225,1226],{"class":213},"hidden",[195,1228,1229],{"class":209},";  ",[195,1231,1232],{"class":415},"\u002F* 父容器不滚动 *\u002F\n",[195,1234,1235],{"class":197,"line":272},[195,1236,552],{"class":209},[195,1238,1239],{"class":197,"line":293},[195,1240,1241],{"emptyLinePlaceholder":757},"\n",[195,1243,1244,1247],{"class":197,"line":562},[195,1245,1246],{"class":201},".buffett-content",[195,1248,496],{"class":209},[195,1250,1251,1254,1256,1259,1261],{"class":197,"line":583},[195,1252,1253],{"class":213},"  overflow-y",[195,1255,217],{"class":209},[195,1257,1258],{"class":213},"auto",[195,1260,1229],{"class":209},[195,1262,1263],{"class":415},"\u002F* 右侧独立滚动 *\u002F\n",[195,1265,1266],{"class":197,"line":962},[195,1267,552],{"class":209},[195,1269,1270],{"class":197,"line":968},[195,1271,1241],{"emptyLinePlaceholder":757},[195,1273,1275,1278],{"class":197,"line":1274},10,[195,1276,1277],{"class":201},".sidebar-nav",[195,1279,496],{"class":209},[195,1281,1283,1286,1288,1291],{"class":197,"line":1282},11,[195,1284,1285],{"class":213},"  flex",[195,1287,217],{"class":209},[195,1289,1290],{"class":213},"1",[195,1292,547],{"class":209},[195,1294,1296,1298,1300,1302,1304],{"class":197,"line":1295},12,[195,1297,1253],{"class":213},[195,1299,217],{"class":209},[195,1301,1258],{"class":213},[195,1303,1229],{"class":209},[195,1305,1306],{"class":415},"\u002F* 左侧独立滚动 *\u002F\n",[195,1308,1310],{"class":197,"line":1309},13,[195,1311,552],{"class":209},[32,1313,1315,1316,1319],{"id":1314},"坑-2grid-子项-min-height-auto-撑破容器","坑 2：Grid 子项 ",[136,1317,1318],{},"min-height: auto"," 撑破容器",[14,1321,1322,1324],{},[125,1323,1153],{}," 改为双面板后侧边栏依然不滚动。",[14,1326,1327,1329,1330,1333,1334,1336,1337,1340],{},[125,1328,1159],{}," CSS Grid 子项的默认 ",[136,1331,1332],{},"min-height"," 是 ",[136,1335,1258],{},"（由内容撑开），即使父容器有固定高度，子项也会被内容撑出去，",[136,1338,1339],{},"overflow"," 无法生效。",[14,1342,1343,1345,1346,333],{},[125,1344,1172],{}," 给 grid 子项加 ",[136,1347,1348],{},"min-height: 0",[186,1350,1352],{"className":188,"code":1351,"language":190,"meta":191,"style":191},".desktop-sidebar {\n  min-height: 0;  \u002F* 允许 Grid 子项被约束 *\u002F\n}\n",[136,1353,1354,1361,1375],{"__ignoreMap":191},[195,1355,1356,1359],{"class":197,"line":198},[195,1357,1358],{"class":201},".desktop-sidebar",[195,1360,496],{"class":209},[195,1362,1363,1366,1368,1370,1372],{"class":197,"line":230},[195,1364,1365],{"class":213},"  min-height",[195,1367,217],{"class":209},[195,1369,407],{"class":213},[195,1371,1229],{"class":209},[195,1373,1374],{"class":415},"\u002F* 允许 Grid 子项被约束 *\u002F\n",[195,1376,1377],{"class":197,"line":251},[195,1378,552],{"class":209},[32,1380,1382,1383,1386],{"id":1381},"坑-3vue-scoped-css-display-属性覆盖","坑 3：Vue scoped CSS ",[136,1384,1385],{},"display"," 属性覆盖",[14,1388,1389,1391,1392,1394],{},[125,1390,1153],{}," 加了 ",[136,1393,1348],{}," 后还是不滚动。移动端抽屉正常，桌面端不行。",[14,1396,1397,1399,1400,1403,1404,1406,1407,1410,1411,1414,1415,1406,1418,1421,1422,1425,1426,1428],{},[125,1398,1159],{}," 父组件 ",[136,1401,1402],{},"[slug].vue"," 的 scoped CSS 给 ",[136,1405,1358],{}," 设了 ",[136,1408,1409],{},"display: block","，子组件 ",[136,1412,1413],{},"BuffettSidebar.vue"," 给 ",[136,1416,1417],{},".sidebar",[136,1419,1420],{},"display: flex","。两者同级优先级（都是一个 class + 一个 ",[136,1423,1424],{},"[data-v-xxx]"," 属性选择器），源码加载顺序决定了 ",[136,1427,1409],{}," 胜出。",[14,1430,1431,1433,1434,1437,1438,1441,1442,1444,1445,1447],{},[136,1432,1409],{}," 下，",[136,1435,1436],{},"flex: 1"," 和 ",[136,1439,1440],{},"flex-direction: column"," 全部失效，",[136,1443,1277],{}," 没有被约束高度，",[136,1446,1166],{}," 无法触发滚动。",[14,1449,1450,1452,1453,333],{},[125,1451,1172],{}," 移除父组件中多余的 ",[136,1454,1409],{},[186,1456,1458],{"className":188,"code":1457,"language":190,"meta":191,"style":191},"\u002F* 之前 *\u002F\n.desktop-sidebar {\n  display: block;    \u002F* 覆盖了子组件的 display: flex *\u002F\n  min-height: 0;\n}\n\n\u002F* 修复后 *\u002F\n.desktop-sidebar {\n  min-height: 0;     \u002F* 只保留必要属性 *\u002F\n}\n",[136,1459,1460,1465,1471,1487,1497,1501,1505,1510,1516,1530],{"__ignoreMap":191},[195,1461,1462],{"class":197,"line":198},[195,1463,1464],{"class":415},"\u002F* 之前 *\u002F\n",[195,1466,1467,1469],{"class":197,"line":230},[195,1468,1358],{"class":201},[195,1470,496],{"class":209},[195,1472,1473,1476,1478,1481,1484],{"class":197,"line":251},[195,1474,1475],{"class":213},"  display",[195,1477,217],{"class":209},[195,1479,1480],{"class":213},"block",[195,1482,1483],{"class":209},";    ",[195,1485,1486],{"class":415},"\u002F* 覆盖了子组件的 display: flex *\u002F\n",[195,1488,1489,1491,1493,1495],{"class":197,"line":272},[195,1490,1365],{"class":213},[195,1492,217],{"class":209},[195,1494,407],{"class":213},[195,1496,547],{"class":209},[195,1498,1499],{"class":197,"line":293},[195,1500,552],{"class":209},[195,1502,1503],{"class":197,"line":562},[195,1504,1241],{"emptyLinePlaceholder":757},[195,1506,1507],{"class":197,"line":583},[195,1508,1509],{"class":415},"\u002F* 修复后 *\u002F\n",[195,1511,1512,1514],{"class":197,"line":962},[195,1513,1358],{"class":201},[195,1515,496],{"class":209},[195,1517,1518,1520,1522,1524,1527],{"class":197,"line":968},[195,1519,1365],{"class":213},[195,1521,217],{"class":209},[195,1523,407],{"class":213},[195,1525,1526],{"class":209},";     ",[195,1528,1529],{"class":415},"\u002F* 只保留必要属性 *\u002F\n",[195,1531,1532],{"class":197,"line":1274},[195,1533,552],{"class":209},[32,1535,1537],{"id":1536},"额外分类折叠状态重置","额外：分类折叠状态重置",[14,1539,1540,1542],{},[125,1541,1153],{}," 点击文章链接后，所有分类都自动展开。",[14,1544,1545,1547,1548,1551,1552,1555],{},[125,1546,1159],{}," ",[136,1549,1550],{},"\u003Cdetails open>"," 是硬编码属性，NuxtLink 导航导致组件重新渲染，所有 ",[136,1553,1554],{},"open"," 重置。",[14,1557,1558,1560,1561,1564,1565,1568],{},[125,1559,1172],{}," 用 Vue ref（",[136,1562,1563],{},"Set\u003Cstring>","）管理每个分类的展开状态，初始只展开当前文章所在分类，通过 ",[136,1566,1567],{},"@toggle"," 事件同步状态。",[18,1570,1571],{"id":1571},"经验",[129,1573,1574,1590,1598,1611],{},[132,1575,1576,1579,1580,1582,1583,1586,1587,1589],{},[125,1577,1578],{},"双栏独立滚动","不要用 ",[136,1581,1147],{},"，应该让父容器固定高度 + ",[136,1584,1585],{},"overflow: hidden","，两个子面板各自 ",[136,1588,1166],{},"。",[132,1591,1592,1593,1595,1596,1589],{},"Grid 布局中如果子项需要被约束高度（内部有 ",[136,1594,1339],{}," 滚动），必须给子项加 ",[136,1597,1348],{},[132,1599,1600,1601,997,1604,1607,1608,1610],{},"Vue scoped CSS 中，父组件和子组件对同一元素设置同一属性时会产生优先级竞争。最佳实践：父组件只设置布局相关属性（",[136,1602,1603],{},"margin",[136,1605,1606],{},"grid-area","），不要覆盖子组件的 ",[136,1609,1385],{}," 等核心属性。",[132,1612,1613,1614,1616],{},"调试 CSS 滚动问题时，从最内层的滚动容器开始检查：它是否有固定高度？它的每一层祖先是否都正确约束了高度？任何一层\"漏掉\"都会导致 ",[136,1615,1339],{}," 失效。",[733,1618,1619],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":191,"searchDepth":230,"depth":230,"links":1621},[1622,1623,1627,1636],{"id":1052,"depth":230,"text":1052},{"id":1065,"depth":230,"text":1065,"children":1624},[1625,1626],{"id":1068,"depth":251,"text":1068},{"id":1079,"depth":251,"text":1079},{"id":1140,"depth":230,"text":1140,"children":1628},[1629,1631,1633,1635],{"id":1143,"depth":251,"text":1630},"坑 1：position: sticky 劫持滚动",{"id":1314,"depth":251,"text":1632},"坑 2：Grid 子项 min-height: auto 撑破容器",{"id":1381,"depth":251,"text":1634},"坑 3：Vue scoped CSS display 属性覆盖",{"id":1536,"depth":251,"text":1537},{"id":1571,"depth":230,"text":1571},"2026-05-06 00:00:00 CST","记录巴菲特模块从卡片平铺改为侧边栏+文章详情双栏布局的过程，以及修复侧边栏无法滚动的三个 CSS 坑。",{},"\u002Fnotes\u002F2026-05-06-buffett-sidebar-layout",{"title":1042,"description":1638},"Nuxt 3 双栏布局中侧边栏滚动失效的三个 CSS 陷阱：sticky 滚动劫持、Grid min-height 默认值、Vue scoped CSS display 覆盖。","巴菲特模块侧边栏布局重构笔记｜个人笔记","buffett-sidebar-layout","notes\u002F2026-05-06-buffett-sidebar-layout","xTB8ZbYiFyx1gO5lKlLRw-Hdn2XsAgehVvvObcPnhtg",{"id":1648,"title":1649,"body":1650,"category":752,"date":1637,"description":1875,"extension":755,"meta":1876,"navigation":757,"order":752,"path":1877,"seo":1878,"seoDescription":1879,"seoTitle":1880,"slug":1881,"stem":1882,"__hash__":1883},"notes\u002Fnotes\u002F2026-05-06-fix-buffett-prerender-500.md","修复巴菲特模块 SSG 预渲染 500 错误",{"type":8,"value":1651,"toc":1867},[1652,1656,1658,1673,1679,1682,1685,1690,1697,1700,1769,1785,1788,1809,1812,1818,1842,1848,1850],[11,1653,1654],{},[14,1655,1049],{},[18,1657,20],{"id":20},[14,1659,1660,1661,1664,1665,1668,1669,1672],{},"运行 ",[136,1662,1663],{},".\u002Fdeploy.sh","（内部执行 ",[136,1666,1667],{},"npm run generate","）时，巴菲特模块全部 284 个页面预渲染失败，返回 ",[136,1670,1671],{},"[500]"," 错误，导致构建中断：",[186,1674,1677],{"className":1675,"code":1676,"language":1074},[1072],"[nitro]   ├─ \u002Fbuffett\u002F2005年伯克希尔股东大会 (507ms)\n  │ ├── [500]\n  │ └── Linked from \u002Fbuffett\n",[136,1678,1676],{"__ignoreMap":191},[14,1680,1681],{},"其他模块（思维模型、大道总纲、个人笔记）均正常生成。",[18,1683,1684],{"id":1684},"原因",[14,1686,1687],{},[125,1688,1689],{},"根本原因：Markdown 文件的 slug 使用了中文字符。",[14,1691,1692,1693,1696],{},"巴菲特模块的 slug 形如 ",[136,1694,1695],{},"2005年伯克希尔股东大会","，URL 路径中包含非 ASCII 字符。Nuxt\u002FNitro 在 SSG 预渲染阶段处理这类 URL 时失败，返回 500。",[14,1698,1699],{},"对比其他正常的模块：",[36,1701,1702,1715],{},[39,1703,1704],{},[42,1705,1706,1709,1712],{},[45,1707,1708],{},"模块",[45,1710,1711],{},"slug 示例",[45,1713,1714],{},"预渲染结果",[52,1716,1717,1733,1745,1757],{},[42,1718,1719,1722,1730],{},[57,1720,1721],{},"大道总纲",[57,1723,1724,997,1727],{},[136,1725,1726],{},"benfen",[136,1728,1729],{},"anquanbianji",[57,1731,1732],{},"正常",[42,1734,1735,1738,1743],{},[57,1736,1737],{},"个人笔记",[57,1739,1740],{},[136,1741,1742],{},"flutter-interview",[57,1744,1732],{},[42,1746,1747,1750,1755],{},[57,1748,1749],{},"思维模型",[57,1751,1752],{},[136,1753,1754],{},"fail-safe",[57,1756,1732],{},[42,1758,1759,1762,1766],{},[57,1760,1761],{},"巴菲特",[57,1763,1764],{},[136,1765,1695],{},[57,1767,1768],{},"500",[14,1770,1771,1772,1775,1776,1589,1779,1781,1782,1589],{},"此外，有 2 个文件的 slug 包含 ",[136,1773,1774],{},"--","（双连字符），如 ",[136,1777,1778],{},"巴菲特：2023年初学者如何投资--3 条简单的规则",[136,1780,1774],{}," 是 SQL 注释语法，触发了 Nuxt Content 的安全检查，报错 ",[136,1783,1784],{},"Invalid query: SQL comments are not allowed",[32,1786,1787],{"id":1787},"排查过程中的干扰项",[129,1789,1790,1797,1803],{},[132,1791,1792,1793,1796],{},"开发模式（",[136,1794,1795],{},"npm run dev","）下页面正常返回 200，问题仅在预渲染阶段出现。",[132,1798,1799,1800,1802],{},"Nitro 的预渲染日志只显示 ",[136,1801,1671],{},"，不输出具体错误信息，需要手动添加 server plugin 捕获错误。",[132,1804,1805,1806,1808],{},"即使把 ",[136,1807,1402],{}," 页面组件清空为空模板，500 依然存在，说明问题不在页面逻辑而在 URL 层面。",[18,1810,1811],{"id":1811},"修复",[14,1813,594,1814,1817],{},[136,1815,1816],{},"transliteration"," 库将 284 个文件批量处理：",[706,1819,1820,1836],{},[132,1821,1822,1829,1830,1832,1833,698],{},[125,1823,1824,1825,1828],{},"frontmatter ",[136,1826,1827],{},"slug"," 字段","：中文转拼音（如 ",[136,1831,1695],{}," → ",[136,1834,1835],{},"2005nian-bo-ke-xi-er-gu-dong-da-hui",[132,1837,1838,1841],{},[125,1839,1840],{},"文件名","：同步重命名为对应拉丁化名称",[14,1843,1844,1845,1847],{},"修复后 ",[136,1846,1667],{}," 成功预渲染 936 个路由，零错误。",[18,1849,1571],{"id":1571},[129,1851,1852,1855,1861],{},[132,1853,1854],{},"Nuxt Content v3 的 SSG 预渲染对 URL 中的非 ASCII 字符支持有问题，slug 应始终使用拉丁字母。",[132,1856,1857,1858,1860],{},"slug 中避免使用 ",[136,1859,1774],{},"，它会被 Nuxt Content 的 SQL 层识别为注释语法并拒绝执行。",[132,1862,1863,1864,1866],{},"开发模式正常不代表生产构建正常，涉及新模块时应尽早跑一次 ",[136,1865,1667],{}," 验证。",{"title":191,"searchDepth":230,"depth":230,"links":1868},[1869,1870,1873,1874],{"id":20,"depth":230,"text":20},{"id":1684,"depth":230,"text":1684,"children":1871},[1872],{"id":1787,"depth":251,"text":1787},{"id":1811,"depth":230,"text":1811},{"id":1571,"depth":230,"text":1571},"记录巴菲特模块全部页面在 npm run generate 时返回 500 的排查过程、根本原因和修复方案。",{},"\u002Fnotes\u002F2026-05-06-fix-buffett-prerender-500",{"title":1649,"description":1875},"Nuxt Content v3 预渲染时中文 slug 导致 500 错误的排查与修复记录。","修复巴菲特模块 SSG 预渲染 500 错误｜个人笔记","fix-buffett-prerender-500","notes\u002F2026-05-06-fix-buffett-prerender-500","BRd38khq82HEpGb-Z2pAjXxMSbJExa4BaTre3WxXT-g",{"id":1885,"title":1886,"body":1887,"category":752,"date":15429,"description":15430,"extension":755,"meta":15431,"navigation":757,"order":752,"path":15432,"seo":15433,"seoDescription":15434,"seoTitle":15435,"slug":15436,"stem":15437,"__hash__":15438},"notes\u002Fnotes\u002F2026-05-03-mobile-engineer-interview.md","移动端全栈工程师面试题与详解",{"type":8,"value":1888,"toc":15316},[1889,1892,1895,1899,1944,1948,1990,1994,2027,2031,2052,2054,2058,2062,2073,2078,2175,2288,2293,2310,2316,2336,2338,2349,2353,2356,2476,2490,2495,2576,2581,2643,2645,2653,2657,2660,2966,2968,2975,2979,2982,3095,3100,3194,3196,3207,3211,3282,3358,3438,3440,3444,3448,3452,3463,3468,3558,3569,3620,3687,3692,3771,3773,3777,3781,3784,3854,3859,3870,3876,3878,3882,3886,3890,3895,3957,3962,4022,4027,4033,4038,4120,4122,4135,4139,4303,4308,4396,4401,4457,4459,4463,4467,4708,4713,4739,4741,4745,4749,4753,4758,4834,4839,4972,4977,5122,5127,5184,5186,5190,5194,5356,5361,5453,5455,5459,5463,5467,5472,5478,5481,5486,5643,5648,5852,5939,5941,5945,5949,5953,6035,6040,6240,6242,6246,6250,6254,6260,6265,6306,6396,6398,6402,6406,6409,6415,6420,6463,6506,6511,6525,6527,6531,6535,6541,6718,6723,6806,6808,6811,6815,6832,6836,7026,7028,7032,7036,7041,7107,7112,7216,7358,7360,7364,7368,7599,7601,7605,7609,7776,7778,7782,7786,7790,7796,7802,7947,7949,7953,7957,7963,7968,8164,8166,8170,8174,8179,8289,8294,8370,8375,8449,8451,8455,8459,8463,8524,8529,8761,8763,8767,8771,9033,9038,9112,9114,9118,9122,9399,9401,9405,9409,9413,9521,9526,9674,9676,9680,9684,9803,9808,9872,9877,9951,9953,9957,9961,9965,9971,9976,10160,10162,10166,10170,10367,10369,10373,10377,10381,10386,10498,10503,10592,10597,10668,10670,10674,10678,10747,10857,10859,10863,10867,10871,10877,10882,10936,10966,10968,10972,10976,11143,11145,11148,11152,11156,11160,11320,11322,11326,11330,11546,11548,11552,11556,11560,11566,11571,11616,11621,11627,11714,11716,11720,11724,11728,11733,11968,11973,12139,12221,12223,12227,12231,12235,12340,12398,12441,12446,12494,12499,12533,12577,12579,12583,12587,12591,12858,12860,12863,12867,12871,12875,12881,12886,12988,13026,13088,13093,13142,13199,13201,13205,13209,13286,13320,13349,13351,13355,13359,13363,13762,13767,13814,13816,13820,13824,13828,13834,14009,14014,14046,14048,14052,14056,14062,14223,14410,14489,14491,14495,14499,14505,14510,14556,14630,14635,14673,14675,14679,14683,14689,15081,15083,15087,15091,15150,15154,15213,15217,15267,15270,15313],[1890,1891],"hr",{},[18,1893,1894],{"id":1894},"目录",[32,1896,1898],{"id":1897},"第一部分ios-开发","第一部分：iOS 开发",[706,1900,1901,1908,1914,1920,1926,1932,1938],{},[132,1902,1903],{},[1904,1905,1907],"a",{"href":1906},"#1-swift-%E8%AF%AD%E8%A8%80%E5%9F%BA%E7%A1%80","Swift 语言基础",[132,1909,1910],{},[1904,1911,1913],{"href":1912},"#2-%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86","内存管理",[132,1915,1916],{},[1904,1917,1919],{"href":1918},"#3-uikit-%E4%B8%8E-swiftui","UIKit 与 SwiftUI",[132,1921,1922],{},[1904,1923,1925],{"href":1924},"#4-%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B","并发编程",[132,1927,1928],{},[1904,1929,1931],{"href":1930},"#5-%E6%9E%B6%E6%9E%84%E4%B8%8E%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8Fios","架构与设计模式",[132,1933,1934],{},[1904,1935,1937],{"href":1936},"#6-%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E4%B8%8E%E5%B7%A5%E5%85%B7ios","性能优化与工具",[132,1939,1940],{},[1904,1941,1943],{"href":1942},"#7-%E7%B3%BB%E7%BB%9F%E6%9C%BA%E5%88%B6%E4%B8%8E%E6%A1%86%E6%9E%B6","系统机制与框架",[32,1945,1947],{"id":1946},"第二部分android-开发","第二部分：Android 开发",[706,1949,1950,1956,1962,1968,1974,1979,1984],{"start":962},[132,1951,1952],{},[1904,1953,1955],{"href":1954},"#8-kotlin-%E8%AF%AD%E8%A8%80%E5%9F%BA%E7%A1%80","Kotlin 语言基础",[132,1957,1958],{},[1904,1959,1961],{"href":1960},"#9-%E5%9B%9B%E5%A4%A7%E7%BB%84%E4%BB%B6%E4%B8%8E%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F","四大组件与生命周期",[132,1963,1964],{},[1904,1965,1967],{"href":1966},"#10-jetpack-%E7%BB%84%E4%BB%B6","Jetpack 组件",[132,1969,1970],{},[1904,1971,1973],{"href":1972},"#11-%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8Bcoroutines","并发编程（Coroutines）",[132,1975,1976],{},[1904,1977,1931],{"href":1978},"#12-%E6%9E%B6%E6%9E%84%E4%B8%8E%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8Fandroid",[132,1980,1981],{},[1904,1982,1937],{"href":1983},"#13-%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E4%B8%8E%E5%B7%A5%E5%85%B7android",[132,1985,1986],{},[1904,1987,1989],{"href":1988},"#14-%E7%B3%BB%E7%BB%9F%E6%9C%BA%E5%88%B6","系统机制",[32,1991,1993],{"id":1992},"第三部分flutter-开发","第三部分：Flutter 开发",[706,1995,1997,2003,2009,2015,2021],{"start":1996},15,[132,1998,1999],{},[1904,2000,2002],{"href":2001},"#15-dart-%E8%AF%AD%E8%A8%80%E4%B8%8E%E6%A0%B8%E5%BF%83%E6%9C%BA%E5%88%B6","Dart 语言与核心机制",[132,2004,2005],{},[1904,2006,2008],{"href":2007},"#16-%E6%B8%B2%E6%9F%93%E4%B8%8E%E4%B8%89%E6%A3%B5%E6%A0%91","渲染与三棵树",[132,2010,2011],{},[1904,2012,2014],{"href":2013},"#17-%E7%8A%B6%E6%80%81%E7%AE%A1%E7%90%86","状态管理",[132,2016,2017],{},[1904,2018,2020],{"href":2019},"#18-%E5%B9%B3%E5%8F%B0%E9%80%9A%E4%BF%A1%E4%B8%8E%E6%B7%B7%E5%90%88%E5%BC%80%E5%8F%91","平台通信与混合开发",[132,2022,2023],{},[1904,2024,2026],{"href":2025},"#19-%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96flutter","性能优化",[32,2028,2030],{"id":2029},"第四部分跨平台通用","第四部分：跨平台通用",[706,2032,2034,2040,2046],{"start":2033},20,[132,2035,2036],{},[1904,2037,2039],{"href":2038},"#20-%E7%BD%91%E7%BB%9C%E4%B8%8E%E5%AE%89%E5%85%A8","网络与安全",[132,2041,2042],{},[1904,2043,2045],{"href":2044},"#21-cicd-%E4%B8%8E%E5%8F%91%E5%B8%83","CI\u002FCD 与发布",[132,2047,2048],{},[1904,2049,2051],{"href":2050},"#22-%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%E4%B8%8E%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1","架构设计与系统设计",[1890,2053],{},[2055,2056,1898],"h1",{"id":2057},"第一部分ios-开发-1",[18,2059,2061],{"id":2060},"_1-swift-语言基础","1. Swift 语言基础",[32,2063,2065,2066,1437,2069,2072],{"id":2064},"q11-struct-和-class-的区别什么时候用哪个","Q1.1: ",[136,2067,2068],{},"struct",[136,2070,2071],{},"class"," 的区别？什么时候用哪个？",[14,2074,2075],{},[125,2076,2077],{},"答：",[36,2079,2080,2093],{},[39,2081,2082],{},[42,2083,2084,2087,2090],{},[45,2085,2086],{},"特性",[45,2088,2089],{},"Struct（值类型）",[45,2091,2092],{},"Class（引用类型）",[52,2094,2095,2106,2117,2128,2139,2150,2163],{},[42,2096,2097,2100,2103],{},[57,2098,2099],{},"存储位置",[57,2101,2102],{},"通常在栈上（小对象）",[57,2104,2105],{},"堆上",[42,2107,2108,2111,2114],{},[57,2109,2110],{},"赋值行为",[57,2112,2113],{},"拷贝（Copy-on-Write）",[57,2115,2116],{},"共享引用",[42,2118,2119,2122,2125],{},[57,2120,2121],{},"继承",[57,2123,2124],{},"不支持",[57,2126,2127],{},"支持",[42,2129,2130,2133,2136],{},[57,2131,2132],{},"引用计数",[57,2134,2135],{},"无",[57,2137,2138],{},"ARC",[42,2140,2141,2144,2147],{},[57,2142,2143],{},"线程安全",[57,2145,2146],{},"天然安全（值拷贝）",[57,2148,2149],{},"需要同步",[42,2151,2152,2157,2160],{},[57,2153,2154],{},[136,2155,2156],{},"mutating",[57,2158,2159],{},"需要标记修改方法",[57,2161,2162],{},"不需要",[42,2164,2165,2170,2172],{},[57,2166,2167],{},[136,2168,2169],{},"deinit",[57,2171,2135],{},[57,2173,2174],{},"有析构器",[186,2176,2180],{"className":2177,"code":2178,"language":2179,"meta":191,"style":191},"language-swift shiki shiki-themes github-light github-dark","\u002F\u002F Struct：值语义\nstruct Point {\n    var x: Double\n    var y: Double\n}\n\nvar a = Point(x: 1, y: 2)\nvar b = a       \u002F\u002F 拷贝\nb.x = 10\nprint(a.x)     \u002F\u002F 1，a 不受影响\n\n\u002F\u002F Class：引用语义\nclass Person {\n    var name: String\n    init(name: String) { self.name = name }\n}\n\nlet p1 = Person(name: \"Alice\")\nlet p2 = p1     \u002F\u002F 共享引用\np2.name = \"Bob\"\nprint(p1.name)  \u002F\u002F \"Bob\"，p1 也被修改了\n","swift",[136,2181,2182,2187,2192,2197,2202,2206,2210,2215,2220,2225,2230,2234,2239,2244,2250,2255,2260,2265,2271,2277,2282],{"__ignoreMap":191},[195,2183,2184],{"class":197,"line":198},[195,2185,2186],{},"\u002F\u002F Struct：值语义\n",[195,2188,2189],{"class":197,"line":230},[195,2190,2191],{},"struct Point {\n",[195,2193,2194],{"class":197,"line":251},[195,2195,2196],{},"    var x: Double\n",[195,2198,2199],{"class":197,"line":272},[195,2200,2201],{},"    var y: Double\n",[195,2203,2204],{"class":197,"line":293},[195,2205,552],{},[195,2207,2208],{"class":197,"line":562},[195,2209,1241],{"emptyLinePlaceholder":757},[195,2211,2212],{"class":197,"line":583},[195,2213,2214],{},"var a = Point(x: 1, y: 2)\n",[195,2216,2217],{"class":197,"line":962},[195,2218,2219],{},"var b = a       \u002F\u002F 拷贝\n",[195,2221,2222],{"class":197,"line":968},[195,2223,2224],{},"b.x = 10\n",[195,2226,2227],{"class":197,"line":1274},[195,2228,2229],{},"print(a.x)     \u002F\u002F 1，a 不受影响\n",[195,2231,2232],{"class":197,"line":1282},[195,2233,1241],{"emptyLinePlaceholder":757},[195,2235,2236],{"class":197,"line":1295},[195,2237,2238],{},"\u002F\u002F Class：引用语义\n",[195,2240,2241],{"class":197,"line":1309},[195,2242,2243],{},"class Person {\n",[195,2245,2247],{"class":197,"line":2246},14,[195,2248,2249],{},"    var name: String\n",[195,2251,2252],{"class":197,"line":1996},[195,2253,2254],{},"    init(name: String) { self.name = name }\n",[195,2256,2258],{"class":197,"line":2257},16,[195,2259,552],{},[195,2261,2263],{"class":197,"line":2262},17,[195,2264,1241],{"emptyLinePlaceholder":757},[195,2266,2268],{"class":197,"line":2267},18,[195,2269,2270],{},"let p1 = Person(name: \"Alice\")\n",[195,2272,2274],{"class":197,"line":2273},19,[195,2275,2276],{},"let p2 = p1     \u002F\u002F 共享引用\n",[195,2278,2279],{"class":197,"line":2033},[195,2280,2281],{},"p2.name = \"Bob\"\n",[195,2283,2285],{"class":197,"line":2284},21,[195,2286,2287],{},"print(p1.name)  \u002F\u002F \"Bob\"，p1 也被修改了\n",[14,2289,2290],{},[125,2291,2292],{},"选择指南（Apple 官方建议）：",[129,2294,2295,2301],{},[132,2296,2297,2300],{},[125,2298,2299],{},"默认用 Struct","：数据模型、坐标、配置等",[132,2302,2303,2306,2307,2309],{},[125,2304,2305],{},"用 Class","：需要继承、需要引用语义（身份标识）、需要 ",[136,2308,2169],{},"、需要与 Objective-C 互操作",[14,2311,2312,2315],{},[125,2313,2314],{},"Copy-on-Write (COW)："," Swift 标准库的集合类型（Array、Dictionary、Set）使用 COW 优化：只在真正修改且有多个引用时才执行拷贝。",[186,2317,2319],{"className":2177,"code":2318,"language":2179,"meta":191,"style":191},"var arr1 = [1, 2, 3]\nvar arr2 = arr1      \u002F\u002F 此时共享底层存储，未拷贝\narr2.append(4)       \u002F\u002F 修改时才触发拷贝\n",[136,2320,2321,2326,2331],{"__ignoreMap":191},[195,2322,2323],{"class":197,"line":198},[195,2324,2325],{},"var arr1 = [1, 2, 3]\n",[195,2327,2328],{"class":197,"line":230},[195,2329,2330],{},"var arr2 = arr1      \u002F\u002F 此时共享底层存储，未拷贝\n",[195,2332,2333],{"class":197,"line":251},[195,2334,2335],{},"arr2.append(4)       \u002F\u002F 修改时才触发拷贝\n",[1890,2337],{},[32,2339,2341,2342,1437,2345,2348],{"id":2340},"q12-swift-中的-protocol-和-protocol-oriented-programming-是什么","Q1.2: Swift 中的 ",[136,2343,2344],{},"Protocol",[136,2346,2347],{},"Protocol-Oriented Programming"," 是什么？",[14,2350,2351],{},[125,2352,2077],{},[14,2354,2355],{},"Protocol 定义行为契约，是 Swift 实现多态和代码复用的核心机制：",[186,2357,2359],{"className":2177,"code":2358,"language":2179,"meta":191,"style":191},"protocol Drawable {\n    func draw()\n}\n\n\u002F\u002F Protocol Extension：提供默认实现\nextension Drawable {\n    func draw() {\n        print(\"Default drawing\")\n    }\n\n    \u002F\u002F 非协议要求的方法：静态派发\n    func description() -> String {\n        return \"A drawable object\"\n    }\n}\n\nstruct Circle: Drawable {\n    func draw() { print(\"Drawing circle\") }         \u002F\u002F 动态派发\n    func description() -> String { return \"Circle\" } \u002F\u002F ⚠️ 静态派发！\n}\n\nlet shape: Drawable = Circle()\nshape.draw()          \u002F\u002F \"Drawing circle\" ✅ 动态派发\nshape.description()   \u002F\u002F \"A drawable object\" ⚠️ 调用的是 extension 的默认实现！\n",[136,2360,2361,2366,2371,2375,2379,2384,2389,2394,2399,2404,2408,2413,2418,2423,2427,2431,2435,2440,2445,2450,2454,2458,2464,2470],{"__ignoreMap":191},[195,2362,2363],{"class":197,"line":198},[195,2364,2365],{},"protocol Drawable {\n",[195,2367,2368],{"class":197,"line":230},[195,2369,2370],{},"    func draw()\n",[195,2372,2373],{"class":197,"line":251},[195,2374,552],{},[195,2376,2377],{"class":197,"line":272},[195,2378,1241],{"emptyLinePlaceholder":757},[195,2380,2381],{"class":197,"line":293},[195,2382,2383],{},"\u002F\u002F Protocol Extension：提供默认实现\n",[195,2385,2386],{"class":197,"line":562},[195,2387,2388],{},"extension Drawable {\n",[195,2390,2391],{"class":197,"line":583},[195,2392,2393],{},"    func draw() {\n",[195,2395,2396],{"class":197,"line":962},[195,2397,2398],{},"        print(\"Default drawing\")\n",[195,2400,2401],{"class":197,"line":968},[195,2402,2403],{},"    }\n",[195,2405,2406],{"class":197,"line":1274},[195,2407,1241],{"emptyLinePlaceholder":757},[195,2409,2410],{"class":197,"line":1282},[195,2411,2412],{},"    \u002F\u002F 非协议要求的方法：静态派发\n",[195,2414,2415],{"class":197,"line":1295},[195,2416,2417],{},"    func description() -> String {\n",[195,2419,2420],{"class":197,"line":1309},[195,2421,2422],{},"        return \"A drawable object\"\n",[195,2424,2425],{"class":197,"line":2246},[195,2426,2403],{},[195,2428,2429],{"class":197,"line":1996},[195,2430,552],{},[195,2432,2433],{"class":197,"line":2257},[195,2434,1241],{"emptyLinePlaceholder":757},[195,2436,2437],{"class":197,"line":2262},[195,2438,2439],{},"struct Circle: Drawable {\n",[195,2441,2442],{"class":197,"line":2267},[195,2443,2444],{},"    func draw() { print(\"Drawing circle\") }         \u002F\u002F 动态派发\n",[195,2446,2447],{"class":197,"line":2273},[195,2448,2449],{},"    func description() -> String { return \"Circle\" } \u002F\u002F ⚠️ 静态派发！\n",[195,2451,2452],{"class":197,"line":2033},[195,2453,552],{},[195,2455,2456],{"class":197,"line":2284},[195,2457,1241],{"emptyLinePlaceholder":757},[195,2459,2461],{"class":197,"line":2460},22,[195,2462,2463],{},"let shape: Drawable = Circle()\n",[195,2465,2467],{"class":197,"line":2466},23,[195,2468,2469],{},"shape.draw()          \u002F\u002F \"Drawing circle\" ✅ 动态派发\n",[195,2471,2473],{"class":197,"line":2472},24,[195,2474,2475],{},"shape.description()   \u002F\u002F \"A drawable object\" ⚠️ 调用的是 extension 的默认实现！\n",[14,2477,2478,2481,2482,2485,2486,2489],{},[125,2479,2480],{},"关键陷阱："," Protocol Extension 中定义的方法如果",[125,2483,2484],{},"不在协议声明中","，则使用",[125,2487,2488],{},"静态派发","，根据编译时类型决定调用哪个实现。",[14,2491,2492],{},[125,2493,2494],{},"Protocol 与泛型约束：",[186,2496,2498],{"className":2177,"code":2497,"language":2179,"meta":191,"style":191},"\u002F\u002F 关联类型（Associated Type）\nprotocol Container {\n    associatedtype Item\n    var count: Int { get }\n    mutating func append(_ item: Item)\n    subscript(i: Int) -> Item { get }\n}\n\n\u002F\u002F 泛型约束\nfunc findIndex\u003CT: Equatable>(of value: T, in array: [T]) -> Int? {\n    return array.firstIndex(of: value)\n}\n\n\u002F\u002F any vs some\nfunc draw(shape: any Drawable) { }  \u002F\u002F 存在类型（运行时多态，有开销）\nfunc draw(shape: some Drawable) { } \u002F\u002F 不透明类型（编译时确定，零开销）\n",[136,2499,2500,2505,2510,2515,2520,2525,2530,2534,2538,2543,2548,2553,2557,2561,2566,2571],{"__ignoreMap":191},[195,2501,2502],{"class":197,"line":198},[195,2503,2504],{},"\u002F\u002F 关联类型（Associated Type）\n",[195,2506,2507],{"class":197,"line":230},[195,2508,2509],{},"protocol Container {\n",[195,2511,2512],{"class":197,"line":251},[195,2513,2514],{},"    associatedtype Item\n",[195,2516,2517],{"class":197,"line":272},[195,2518,2519],{},"    var count: Int { get }\n",[195,2521,2522],{"class":197,"line":293},[195,2523,2524],{},"    mutating func append(_ item: Item)\n",[195,2526,2527],{"class":197,"line":562},[195,2528,2529],{},"    subscript(i: Int) -> Item { get }\n",[195,2531,2532],{"class":197,"line":583},[195,2533,552],{},[195,2535,2536],{"class":197,"line":962},[195,2537,1241],{"emptyLinePlaceholder":757},[195,2539,2540],{"class":197,"line":968},[195,2541,2542],{},"\u002F\u002F 泛型约束\n",[195,2544,2545],{"class":197,"line":1274},[195,2546,2547],{},"func findIndex\u003CT: Equatable>(of value: T, in array: [T]) -> Int? {\n",[195,2549,2550],{"class":197,"line":1282},[195,2551,2552],{},"    return array.firstIndex(of: value)\n",[195,2554,2555],{"class":197,"line":1295},[195,2556,552],{},[195,2558,2559],{"class":197,"line":1309},[195,2560,1241],{"emptyLinePlaceholder":757},[195,2562,2563],{"class":197,"line":2246},[195,2564,2565],{},"\u002F\u002F any vs some\n",[195,2567,2568],{"class":197,"line":1996},[195,2569,2570],{},"func draw(shape: any Drawable) { }  \u002F\u002F 存在类型（运行时多态，有开销）\n",[195,2572,2573],{"class":197,"line":2257},[195,2574,2575],{},"func draw(shape: some Drawable) { } \u002F\u002F 不透明类型（编译时确定，零开销）\n",[14,2577,2578],{},[125,2579,2580],{},"POP vs OOP：",[186,2582,2584],{"className":2177,"code":2583,"language":2179,"meta":191,"style":191},"\u002F\u002F OOP：通过继承复用（单继承限制、紧耦合）\nclass Animal { func eat() { } }\nclass Dog: Animal { func bark() { } }\n\n\u002F\u002F POP：通过组合复用（灵活、可用于值类型）\nprotocol Eating { func eat() }\nprotocol Barking { func bark() }\n\nextension Eating { func eat() { print(\"Eating\") } }\nextension Barking { func bark() { print(\"Barking\") } }\n\nstruct Dog: Eating, Barking { }  \u002F\u002F 组合多个能力\n",[136,2585,2586,2591,2596,2601,2605,2610,2615,2620,2624,2629,2634,2638],{"__ignoreMap":191},[195,2587,2588],{"class":197,"line":198},[195,2589,2590],{},"\u002F\u002F OOP：通过继承复用（单继承限制、紧耦合）\n",[195,2592,2593],{"class":197,"line":230},[195,2594,2595],{},"class Animal { func eat() { } }\n",[195,2597,2598],{"class":197,"line":251},[195,2599,2600],{},"class Dog: Animal { func bark() { } }\n",[195,2602,2603],{"class":197,"line":272},[195,2604,1241],{"emptyLinePlaceholder":757},[195,2606,2607],{"class":197,"line":293},[195,2608,2609],{},"\u002F\u002F POP：通过组合复用（灵活、可用于值类型）\n",[195,2611,2612],{"class":197,"line":562},[195,2613,2614],{},"protocol Eating { func eat() }\n",[195,2616,2617],{"class":197,"line":583},[195,2618,2619],{},"protocol Barking { func bark() }\n",[195,2621,2622],{"class":197,"line":962},[195,2623,1241],{"emptyLinePlaceholder":757},[195,2625,2626],{"class":197,"line":968},[195,2627,2628],{},"extension Eating { func eat() { print(\"Eating\") } }\n",[195,2630,2631],{"class":197,"line":1274},[195,2632,2633],{},"extension Barking { func bark() { print(\"Barking\") } }\n",[195,2635,2636],{"class":197,"line":1282},[195,2637,1241],{"emptyLinePlaceholder":757},[195,2639,2640],{"class":197,"line":1295},[195,2641,2642],{},"struct Dog: Eating, Barking { }  \u002F\u002F 组合多个能力\n",[1890,2644],{},[32,2646,2648,2649,2652],{"id":2647},"q13-swift-中的-enum-有哪些高级用法","Q1.3: Swift 中的 ",[136,2650,2651],{},"enum"," 有哪些高级用法？",[14,2654,2655],{},[125,2656,2077],{},[14,2658,2659],{},"Swift 的 enum 是一等类型，远比 C\u002FJava 的枚举强大：",[186,2661,2663],{"className":2177,"code":2662,"language":2179,"meta":191,"style":191},"\u002F\u002F 1. 关联值（Associated Values）\nenum NetworkResult {\n    case success(data: Data, statusCode: Int)\n    case failure(error: Error)\n}\n\nlet result: NetworkResult = .success(data: data, statusCode: 200)\n\nswitch result {\ncase .success(let data, let statusCode) where statusCode == 200:\n    process(data)\ncase .success(_, let statusCode):\n    print(\"Unexpected status: \\(statusCode)\")\ncase .failure(let error):\n    print(\"Error: \\(error)\")\n}\n\n\u002F\u002F 2. 递归枚举\nindirect enum ArithExpr {\n    case number(Int)\n    case add(ArithExpr, ArithExpr)\n    case multiply(ArithExpr, ArithExpr)\n}\n\nfunc evaluate(_ expr: ArithExpr) -> Int {\n    switch expr {\n    case .number(let n): return n\n    case .add(let a, let b): return evaluate(a) + evaluate(b)\n    case .multiply(let a, let b): return evaluate(a) * evaluate(b)\n    }\n}\n\n\u002F\u002F 3. 遵循协议 + 计算属性\nenum Planet: CaseIterable, Comparable {\n    case mercury, venus, earth, mars\n\n    var distanceFromSun: Double {\n        switch self {\n        case .mercury: return 57.9\n        case .venus: return 108.2\n        case .earth: return 149.6\n        case .mars: return 227.9\n        }\n    }\n}\n\n\u002F\u002F 4. 用 enum 做命名空间（无 case 的 enum 不能被实例化）\nenum Constants {\n    static let apiBaseURL = \"https:\u002F\u002Fapi.example.com\"\n    static let maxRetries = 3\n}\n\n\u002F\u002F 5. Result 类型（标准库）\nenum Result\u003CSuccess, Failure: Error> {\n    case success(Success)\n    case failure(Failure)\n}\n",[136,2664,2665,2670,2675,2680,2685,2689,2693,2698,2702,2707,2712,2717,2722,2727,2732,2737,2741,2745,2750,2755,2760,2765,2770,2774,2778,2784,2790,2796,2802,2808,2813,2818,2823,2829,2835,2841,2846,2852,2858,2864,2870,2876,2882,2888,2893,2898,2903,2909,2915,2921,2927,2932,2937,2943,2949,2955,2961],{"__ignoreMap":191},[195,2666,2667],{"class":197,"line":198},[195,2668,2669],{},"\u002F\u002F 1. 关联值（Associated Values）\n",[195,2671,2672],{"class":197,"line":230},[195,2673,2674],{},"enum NetworkResult {\n",[195,2676,2677],{"class":197,"line":251},[195,2678,2679],{},"    case success(data: Data, statusCode: Int)\n",[195,2681,2682],{"class":197,"line":272},[195,2683,2684],{},"    case failure(error: Error)\n",[195,2686,2687],{"class":197,"line":293},[195,2688,552],{},[195,2690,2691],{"class":197,"line":562},[195,2692,1241],{"emptyLinePlaceholder":757},[195,2694,2695],{"class":197,"line":583},[195,2696,2697],{},"let result: NetworkResult = .success(data: data, statusCode: 200)\n",[195,2699,2700],{"class":197,"line":962},[195,2701,1241],{"emptyLinePlaceholder":757},[195,2703,2704],{"class":197,"line":968},[195,2705,2706],{},"switch result {\n",[195,2708,2709],{"class":197,"line":1274},[195,2710,2711],{},"case .success(let data, let statusCode) where statusCode == 200:\n",[195,2713,2714],{"class":197,"line":1282},[195,2715,2716],{},"    process(data)\n",[195,2718,2719],{"class":197,"line":1295},[195,2720,2721],{},"case .success(_, let statusCode):\n",[195,2723,2724],{"class":197,"line":1309},[195,2725,2726],{},"    print(\"Unexpected status: \\(statusCode)\")\n",[195,2728,2729],{"class":197,"line":2246},[195,2730,2731],{},"case .failure(let error):\n",[195,2733,2734],{"class":197,"line":1996},[195,2735,2736],{},"    print(\"Error: \\(error)\")\n",[195,2738,2739],{"class":197,"line":2257},[195,2740,552],{},[195,2742,2743],{"class":197,"line":2262},[195,2744,1241],{"emptyLinePlaceholder":757},[195,2746,2747],{"class":197,"line":2267},[195,2748,2749],{},"\u002F\u002F 2. 递归枚举\n",[195,2751,2752],{"class":197,"line":2273},[195,2753,2754],{},"indirect enum ArithExpr {\n",[195,2756,2757],{"class":197,"line":2033},[195,2758,2759],{},"    case number(Int)\n",[195,2761,2762],{"class":197,"line":2284},[195,2763,2764],{},"    case add(ArithExpr, ArithExpr)\n",[195,2766,2767],{"class":197,"line":2460},[195,2768,2769],{},"    case multiply(ArithExpr, ArithExpr)\n",[195,2771,2772],{"class":197,"line":2466},[195,2773,552],{},[195,2775,2776],{"class":197,"line":2472},[195,2777,1241],{"emptyLinePlaceholder":757},[195,2779,2781],{"class":197,"line":2780},25,[195,2782,2783],{},"func evaluate(_ expr: ArithExpr) -> Int {\n",[195,2785,2787],{"class":197,"line":2786},26,[195,2788,2789],{},"    switch expr {\n",[195,2791,2793],{"class":197,"line":2792},27,[195,2794,2795],{},"    case .number(let n): return n\n",[195,2797,2799],{"class":197,"line":2798},28,[195,2800,2801],{},"    case .add(let a, let b): return evaluate(a) + evaluate(b)\n",[195,2803,2805],{"class":197,"line":2804},29,[195,2806,2807],{},"    case .multiply(let a, let b): return evaluate(a) * evaluate(b)\n",[195,2809,2811],{"class":197,"line":2810},30,[195,2812,2403],{},[195,2814,2816],{"class":197,"line":2815},31,[195,2817,552],{},[195,2819,2821],{"class":197,"line":2820},32,[195,2822,1241],{"emptyLinePlaceholder":757},[195,2824,2826],{"class":197,"line":2825},33,[195,2827,2828],{},"\u002F\u002F 3. 遵循协议 + 计算属性\n",[195,2830,2832],{"class":197,"line":2831},34,[195,2833,2834],{},"enum Planet: CaseIterable, Comparable {\n",[195,2836,2838],{"class":197,"line":2837},35,[195,2839,2840],{},"    case mercury, venus, earth, mars\n",[195,2842,2844],{"class":197,"line":2843},36,[195,2845,1241],{"emptyLinePlaceholder":757},[195,2847,2849],{"class":197,"line":2848},37,[195,2850,2851],{},"    var distanceFromSun: Double {\n",[195,2853,2855],{"class":197,"line":2854},38,[195,2856,2857],{},"        switch self {\n",[195,2859,2861],{"class":197,"line":2860},39,[195,2862,2863],{},"        case .mercury: return 57.9\n",[195,2865,2867],{"class":197,"line":2866},40,[195,2868,2869],{},"        case .venus: return 108.2\n",[195,2871,2873],{"class":197,"line":2872},41,[195,2874,2875],{},"        case .earth: return 149.6\n",[195,2877,2879],{"class":197,"line":2878},42,[195,2880,2881],{},"        case .mars: return 227.9\n",[195,2883,2885],{"class":197,"line":2884},43,[195,2886,2887],{},"        }\n",[195,2889,2891],{"class":197,"line":2890},44,[195,2892,2403],{},[195,2894,2896],{"class":197,"line":2895},45,[195,2897,552],{},[195,2899,2901],{"class":197,"line":2900},46,[195,2902,1241],{"emptyLinePlaceholder":757},[195,2904,2906],{"class":197,"line":2905},47,[195,2907,2908],{},"\u002F\u002F 4. 用 enum 做命名空间（无 case 的 enum 不能被实例化）\n",[195,2910,2912],{"class":197,"line":2911},48,[195,2913,2914],{},"enum Constants {\n",[195,2916,2918],{"class":197,"line":2917},49,[195,2919,2920],{},"    static let apiBaseURL = \"https:\u002F\u002Fapi.example.com\"\n",[195,2922,2924],{"class":197,"line":2923},50,[195,2925,2926],{},"    static let maxRetries = 3\n",[195,2928,2930],{"class":197,"line":2929},51,[195,2931,552],{},[195,2933,2935],{"class":197,"line":2934},52,[195,2936,1241],{"emptyLinePlaceholder":757},[195,2938,2940],{"class":197,"line":2939},53,[195,2941,2942],{},"\u002F\u002F 5. Result 类型（标准库）\n",[195,2944,2946],{"class":197,"line":2945},54,[195,2947,2948],{},"enum Result\u003CSuccess, Failure: Error> {\n",[195,2950,2952],{"class":197,"line":2951},55,[195,2953,2954],{},"    case success(Success)\n",[195,2956,2958],{"class":197,"line":2957},56,[195,2959,2960],{},"    case failure(Failure)\n",[195,2962,2964],{"class":197,"line":2963},57,[195,2965,552],{},[1890,2967],{},[32,2969,2971,2972],{"id":2970},"q14-解释-swift-的-propertywrapper","Q1.4: 解释 Swift 的 ",[136,2973,2974],{},"@propertyWrapper",[14,2976,2977],{},[125,2978,2077],{},[14,2980,2981],{},"Property Wrapper 封装属性的存取逻辑，避免重复的样板代码：",[186,2983,2985],{"className":2177,"code":2984,"language":2179,"meta":191,"style":191},"\u002F\u002F 定义：限制值在指定范围内\n@propertyWrapper\nstruct Clamped\u003CValue: Comparable> {\n    var wrappedValue: Value {\n        didSet { wrappedValue = min(max(wrappedValue, range.lowerBound), range.upperBound) }\n    }\n    let range: ClosedRange\u003CValue>\n\n    init(wrappedValue: Value, _ range: ClosedRange\u003CValue>) {\n        self.range = range\n        self.wrappedValue = min(max(wrappedValue, range.lowerBound), range.upperBound)\n    }\n}\n\n\u002F\u002F 使用\nstruct Player {\n    @Clamped(0...100) var health: Int = 100\n    @Clamped(0...999) var score: Int = 0\n}\n\nvar player = Player()\nplayer.health = 150  \u002F\u002F 被钳制为 100\nplayer.health = -10  \u002F\u002F 被钳制为 0\n",[136,2986,2987,2992,2997,3002,3007,3012,3016,3021,3025,3030,3035,3040,3044,3048,3052,3057,3062,3067,3072,3076,3080,3085,3090],{"__ignoreMap":191},[195,2988,2989],{"class":197,"line":198},[195,2990,2991],{},"\u002F\u002F 定义：限制值在指定范围内\n",[195,2993,2994],{"class":197,"line":230},[195,2995,2996],{},"@propertyWrapper\n",[195,2998,2999],{"class":197,"line":251},[195,3000,3001],{},"struct Clamped\u003CValue: Comparable> {\n",[195,3003,3004],{"class":197,"line":272},[195,3005,3006],{},"    var wrappedValue: Value {\n",[195,3008,3009],{"class":197,"line":293},[195,3010,3011],{},"        didSet { wrappedValue = min(max(wrappedValue, range.lowerBound), range.upperBound) }\n",[195,3013,3014],{"class":197,"line":562},[195,3015,2403],{},[195,3017,3018],{"class":197,"line":583},[195,3019,3020],{},"    let range: ClosedRange\u003CValue>\n",[195,3022,3023],{"class":197,"line":962},[195,3024,1241],{"emptyLinePlaceholder":757},[195,3026,3027],{"class":197,"line":968},[195,3028,3029],{},"    init(wrappedValue: Value, _ range: ClosedRange\u003CValue>) {\n",[195,3031,3032],{"class":197,"line":1274},[195,3033,3034],{},"        self.range = range\n",[195,3036,3037],{"class":197,"line":1282},[195,3038,3039],{},"        self.wrappedValue = min(max(wrappedValue, range.lowerBound), range.upperBound)\n",[195,3041,3042],{"class":197,"line":1295},[195,3043,2403],{},[195,3045,3046],{"class":197,"line":1309},[195,3047,552],{},[195,3049,3050],{"class":197,"line":2246},[195,3051,1241],{"emptyLinePlaceholder":757},[195,3053,3054],{"class":197,"line":1996},[195,3055,3056],{},"\u002F\u002F 使用\n",[195,3058,3059],{"class":197,"line":2257},[195,3060,3061],{},"struct Player {\n",[195,3063,3064],{"class":197,"line":2262},[195,3065,3066],{},"    @Clamped(0...100) var health: Int = 100\n",[195,3068,3069],{"class":197,"line":2267},[195,3070,3071],{},"    @Clamped(0...999) var score: Int = 0\n",[195,3073,3074],{"class":197,"line":2273},[195,3075,552],{},[195,3077,3078],{"class":197,"line":2033},[195,3079,1241],{"emptyLinePlaceholder":757},[195,3081,3082],{"class":197,"line":2284},[195,3083,3084],{},"var player = Player()\n",[195,3086,3087],{"class":197,"line":2460},[195,3088,3089],{},"player.health = 150  \u002F\u002F 被钳制为 100\n",[195,3091,3092],{"class":197,"line":2466},[195,3093,3094],{},"player.health = -10  \u002F\u002F 被钳制为 0\n",[14,3096,3097],{},[125,3098,3099],{},"SwiftUI 中的常见 Property Wrapper：",[36,3101,3102,3112],{},[39,3103,3104],{},[42,3105,3106,3109],{},[45,3107,3108],{},"Wrapper",[45,3110,3111],{},"用途",[52,3113,3114,3124,3134,3144,3154,3164,3174,3184],{},[42,3115,3116,3121],{},[57,3117,3118],{},[136,3119,3120],{},"@State",[57,3122,3123],{},"View 私有状态，值类型",[42,3125,3126,3131],{},[57,3127,3128],{},[136,3129,3130],{},"@Binding",[57,3132,3133],{},"父子 View 之间的双向绑定",[42,3135,3136,3141],{},[57,3137,3138],{},[136,3139,3140],{},"@ObservedObject",[57,3142,3143],{},"外部传入的可观察对象",[42,3145,3146,3151],{},[57,3147,3148],{},[136,3149,3150],{},"@StateObject",[57,3152,3153],{},"View 拥有的可观察对象（生命周期绑定）",[42,3155,3156,3161],{},[57,3157,3158],{},[136,3159,3160],{},"@EnvironmentObject",[57,3162,3163],{},"通过环境注入的共享对象",[42,3165,3166,3171],{},[57,3167,3168],{},[136,3169,3170],{},"@Environment",[57,3172,3173],{},"读取系统环境值（如 colorScheme）",[42,3175,3176,3181],{},[57,3177,3178],{},[136,3179,3180],{},"@Published",[57,3182,3183],{},"属性变更时自动通知订阅者",[42,3185,3186,3191],{},[57,3187,3188],{},[136,3189,3190],{},"@AppStorage",[57,3192,3193],{},"UserDefaults 的声明式封装",[1890,3195],{},[32,3197,3199,3200,1437,3203,3206],{"id":3198},"q15-some-和-any-关键字的区别是什么","Q1.5: ",[136,3201,3202],{},"some",[136,3204,3205],{},"any"," 关键字的区别是什么？",[14,3208,3209],{},[125,3210,2077],{},[186,3212,3214],{"className":2177,"code":3213,"language":2179,"meta":191,"style":191},"\u002F\u002F some：不透明类型（Opaque Type）\n\u002F\u002F 编译时确定具体类型，但对调用者隐藏\nfunc makeShape() -> some Shape {\n    return Circle(radius: 10)\n    \u002F\u002F 每次调用返回同一具体类型（编译器保证）\n}\n\n\u002F\u002F any：存在类型（Existential Type）\n\u002F\u002F 运行时可以是任意遵循协议的类型\nfunc draw(shapes: [any Shape]) {\n    for shape in shapes {\n        shape.draw() \u002F\u002F 运行时动态派发\n    }\n}\n",[136,3215,3216,3221,3226,3231,3236,3241,3245,3249,3254,3259,3264,3269,3274,3278],{"__ignoreMap":191},[195,3217,3218],{"class":197,"line":198},[195,3219,3220],{},"\u002F\u002F some：不透明类型（Opaque Type）\n",[195,3222,3223],{"class":197,"line":230},[195,3224,3225],{},"\u002F\u002F 编译时确定具体类型，但对调用者隐藏\n",[195,3227,3228],{"class":197,"line":251},[195,3229,3230],{},"func makeShape() -> some Shape {\n",[195,3232,3233],{"class":197,"line":272},[195,3234,3235],{},"    return Circle(radius: 10)\n",[195,3237,3238],{"class":197,"line":293},[195,3239,3240],{},"    \u002F\u002F 每次调用返回同一具体类型（编译器保证）\n",[195,3242,3243],{"class":197,"line":562},[195,3244,552],{},[195,3246,3247],{"class":197,"line":583},[195,3248,1241],{"emptyLinePlaceholder":757},[195,3250,3251],{"class":197,"line":962},[195,3252,3253],{},"\u002F\u002F any：存在类型（Existential Type）\n",[195,3255,3256],{"class":197,"line":968},[195,3257,3258],{},"\u002F\u002F 运行时可以是任意遵循协议的类型\n",[195,3260,3261],{"class":197,"line":1274},[195,3262,3263],{},"func draw(shapes: [any Shape]) {\n",[195,3265,3266],{"class":197,"line":1282},[195,3267,3268],{},"    for shape in shapes {\n",[195,3270,3271],{"class":197,"line":1295},[195,3272,3273],{},"        shape.draw() \u002F\u002F 运行时动态派发\n",[195,3275,3276],{"class":197,"line":1309},[195,3277,2403],{},[195,3279,3280],{"class":197,"line":2246},[195,3281,552],{},[36,3283,3284,3298],{},[39,3285,3286],{},[42,3287,3288,3290,3294],{},[45,3289],{},[45,3291,3292],{},[136,3293,3202],{},[45,3295,3296],{},[136,3297,3205],{},[52,3299,3300,3311,3322,3336,3347],{},[42,3301,3302,3305,3308],{},[57,3303,3304],{},"派发方式",[57,3306,3307],{},"静态派发（零开销）",[57,3309,3310],{},"动态派发（有开销）",[42,3312,3313,3316,3319],{},[57,3314,3315],{},"类型一致性",[57,3317,3318],{},"必须始终是同一具体类型",[57,3320,3321],{},"可以是不同类型",[42,3323,3324,3327,3330],{},[57,3325,3326],{},"放在集合中",[57,3328,3329],{},"不能（类型不统一）",[57,3331,3332,3333],{},"可以 ",[136,3334,3335],{},"[any Protocol]",[42,3337,3338,3341,3344],{},[57,3339,3340],{},"性能",[57,3342,3343],{},"更好",[57,3345,3346],{},"有装箱开销（Existential Container）",[42,3348,3349,3352,3355],{},[57,3350,3351],{},"使用场景",[57,3353,3354],{},"函数返回值、SwiftUI body",[57,3356,3357],{},"异构集合、需要运行时多态",[186,3359,3361],{"className":2177,"code":3360,"language":2179,"meta":191,"style":191},"\u002F\u002F SwiftUI 中 some 是必须的\nstruct ContentView: View {\n    var body: some View {  \u002F\u002F 编译器知道具体类型\n        VStack {\n            Text(\"Hello\")\n            Image(systemName: \"star\")\n        }\n    }\n}\n\n\u002F\u002F 需要 any 的场景\nprotocol Animal { func sound() -> String }\nstruct Dog: Animal { func sound() -> String { \"Woof\" } }\nstruct Cat: Animal { func sound() -> String { \"Meow\" } }\n\nlet pets: [any Animal] = [Dog(), Cat()] \u002F\u002F 异构集合必须用 any\n",[136,3362,3363,3368,3373,3378,3383,3388,3393,3397,3401,3405,3409,3414,3419,3424,3429,3433],{"__ignoreMap":191},[195,3364,3365],{"class":197,"line":198},[195,3366,3367],{},"\u002F\u002F SwiftUI 中 some 是必须的\n",[195,3369,3370],{"class":197,"line":230},[195,3371,3372],{},"struct ContentView: View {\n",[195,3374,3375],{"class":197,"line":251},[195,3376,3377],{},"    var body: some View {  \u002F\u002F 编译器知道具体类型\n",[195,3379,3380],{"class":197,"line":272},[195,3381,3382],{},"        VStack {\n",[195,3384,3385],{"class":197,"line":293},[195,3386,3387],{},"            Text(\"Hello\")\n",[195,3389,3390],{"class":197,"line":562},[195,3391,3392],{},"            Image(systemName: \"star\")\n",[195,3394,3395],{"class":197,"line":583},[195,3396,2887],{},[195,3398,3399],{"class":197,"line":962},[195,3400,2403],{},[195,3402,3403],{"class":197,"line":968},[195,3404,552],{},[195,3406,3407],{"class":197,"line":1274},[195,3408,1241],{"emptyLinePlaceholder":757},[195,3410,3411],{"class":197,"line":1282},[195,3412,3413],{},"\u002F\u002F 需要 any 的场景\n",[195,3415,3416],{"class":197,"line":1295},[195,3417,3418],{},"protocol Animal { func sound() -> String }\n",[195,3420,3421],{"class":197,"line":1309},[195,3422,3423],{},"struct Dog: Animal { func sound() -> String { \"Woof\" } }\n",[195,3425,3426],{"class":197,"line":2246},[195,3427,3428],{},"struct Cat: Animal { func sound() -> String { \"Meow\" } }\n",[195,3430,3431],{"class":197,"line":1996},[195,3432,1241],{"emptyLinePlaceholder":757},[195,3434,3435],{"class":197,"line":2257},[195,3436,3437],{},"let pets: [any Animal] = [Dog(), Cat()] \u002F\u002F 异构集合必须用 any\n",[1890,3439],{},[18,3441,3443],{"id":3442},"_2-内存管理","2. 内存管理",[32,3445,3447],{"id":3446},"q21-arcautomatic-reference-counting的工作原理如何解决循环引用","Q2.1: ARC（Automatic Reference Counting）的工作原理？如何解决循环引用？",[14,3449,3450],{},[125,3451,2077],{},[14,3453,3454,3455,3458,3459,3462],{},"ARC 在编译时自动插入 ",[136,3456,3457],{},"retain","（引用计数 +1）和 ",[136,3460,3461],{},"release","（引用计数 -1）调用。当引用计数归零时，对象被销毁。",[14,3464,3465],{},[125,3466,3467],{},"循环引用（Retain Cycle）：",[186,3469,3471],{"className":2177,"code":3470,"language":2179,"meta":191,"style":191},"\u002F\u002F ❌ 循环引用：两个对象互相强引用\nclass Person {\n    var apartment: Apartment?\n    deinit { print(\"Person deinit\") } \u002F\u002F 永远不会调用\n}\n\nclass Apartment {\n    var tenant: Person?   \u002F\u002F 强引用\n    deinit { print(\"Apartment deinit\") } \u002F\u002F 永远不会调用\n}\n\nvar john: Person? = Person()\nvar unit: Apartment? = Apartment()\njohn?.apartment = unit\nunit?.tenant = john\njohn = nil  \u002F\u002F 引用计数 1（Apartment 还持有）\nunit = nil  \u002F\u002F 引用计数 1（Person 还持有）\n\u002F\u002F 内存泄漏！\n",[136,3472,3473,3478,3482,3487,3492,3496,3500,3505,3510,3515,3519,3523,3528,3533,3538,3543,3548,3553],{"__ignoreMap":191},[195,3474,3475],{"class":197,"line":198},[195,3476,3477],{},"\u002F\u002F ❌ 循环引用：两个对象互相强引用\n",[195,3479,3480],{"class":197,"line":230},[195,3481,2243],{},[195,3483,3484],{"class":197,"line":251},[195,3485,3486],{},"    var apartment: Apartment?\n",[195,3488,3489],{"class":197,"line":272},[195,3490,3491],{},"    deinit { print(\"Person deinit\") } \u002F\u002F 永远不会调用\n",[195,3493,3494],{"class":197,"line":293},[195,3495,552],{},[195,3497,3498],{"class":197,"line":562},[195,3499,1241],{"emptyLinePlaceholder":757},[195,3501,3502],{"class":197,"line":583},[195,3503,3504],{},"class Apartment {\n",[195,3506,3507],{"class":197,"line":962},[195,3508,3509],{},"    var tenant: Person?   \u002F\u002F 强引用\n",[195,3511,3512],{"class":197,"line":968},[195,3513,3514],{},"    deinit { print(\"Apartment deinit\") } \u002F\u002F 永远不会调用\n",[195,3516,3517],{"class":197,"line":1274},[195,3518,552],{},[195,3520,3521],{"class":197,"line":1282},[195,3522,1241],{"emptyLinePlaceholder":757},[195,3524,3525],{"class":197,"line":1295},[195,3526,3527],{},"var john: Person? = Person()\n",[195,3529,3530],{"class":197,"line":1309},[195,3531,3532],{},"var unit: Apartment? = Apartment()\n",[195,3534,3535],{"class":197,"line":2246},[195,3536,3537],{},"john?.apartment = unit\n",[195,3539,3540],{"class":197,"line":1996},[195,3541,3542],{},"unit?.tenant = john\n",[195,3544,3545],{"class":197,"line":2257},[195,3546,3547],{},"john = nil  \u002F\u002F 引用计数 1（Apartment 还持有）\n",[195,3549,3550],{"class":197,"line":2262},[195,3551,3552],{},"unit = nil  \u002F\u002F 引用计数 1（Person 还持有）\n",[195,3554,3555],{"class":197,"line":2267},[195,3556,3557],{},"\u002F\u002F 内存泄漏！\n",[14,3559,3560],{},[125,3561,3562,3563,1437,3566],{},"解决方案：",[136,3564,3565],{},"weak",[136,3567,3568],{},"unowned",[186,3570,3572],{"className":2177,"code":3571,"language":2179,"meta":191,"style":191},"class Apartment {\n    weak var tenant: Person?  \u002F\u002F 弱引用，不增加引用计数\n    \u002F\u002F tenant 被销毁后自动置为 nil\n}\n\nclass CreditCard {\n    unowned let owner: Person  \u002F\u002F 无主引用，不增加引用计数\n    \u002F\u002F 假设 owner 生命周期 >= CreditCard\n    \u002F\u002F owner 被销毁后访问会崩溃（类似野指针）\n}\n",[136,3573,3574,3578,3583,3588,3592,3596,3601,3606,3611,3616],{"__ignoreMap":191},[195,3575,3576],{"class":197,"line":198},[195,3577,3504],{},[195,3579,3580],{"class":197,"line":230},[195,3581,3582],{},"    weak var tenant: Person?  \u002F\u002F 弱引用，不增加引用计数\n",[195,3584,3585],{"class":197,"line":251},[195,3586,3587],{},"    \u002F\u002F tenant 被销毁后自动置为 nil\n",[195,3589,3590],{"class":197,"line":272},[195,3591,552],{},[195,3593,3594],{"class":197,"line":293},[195,3595,1241],{"emptyLinePlaceholder":757},[195,3597,3598],{"class":197,"line":562},[195,3599,3600],{},"class CreditCard {\n",[195,3602,3603],{"class":197,"line":583},[195,3604,3605],{},"    unowned let owner: Person  \u002F\u002F 无主引用，不增加引用计数\n",[195,3607,3608],{"class":197,"line":962},[195,3609,3610],{},"    \u002F\u002F 假设 owner 生命周期 >= CreditCard\n",[195,3612,3613],{"class":197,"line":968},[195,3614,3615],{},"    \u002F\u002F owner 被销毁后访问会崩溃（类似野指针）\n",[195,3617,3618],{"class":197,"line":1274},[195,3619,552],{},[36,3621,3622,3636],{},[39,3623,3624],{},[42,3625,3626,3628,3632],{},[45,3627],{},[45,3629,3630],{},[136,3631,3565],{},[45,3633,3634],{},[136,3635,3568],{},[52,3637,3638,3652,3666,3676],{},[42,3639,3640,3643,3649],{},[57,3641,3642],{},"类型",[57,3644,3645,3648],{},[136,3646,3647],{},"Optional","（可选）",[57,3650,3651],{},"非可选",[42,3653,3654,3657,3663],{},[57,3655,3656],{},"对象销毁后",[57,3658,3659,3660],{},"自动设为 ",[136,3661,3662],{},"nil",[57,3664,3665],{},"悬垂引用（访问崩溃）",[42,3667,3668,3670,3673],{},[57,3669,3340],{},[57,3671,3672],{},"略有开销（side table）",[57,3674,3675],{},"更轻量",[42,3677,3678,3681,3684],{},[57,3679,3680],{},"适用场景",[57,3682,3683],{},"引用对象可能先销毁",[57,3685,3686],{},"确信引用对象生命周期更长",[14,3688,3689],{},[125,3690,3691],{},"闭包中的循环引用：",[186,3693,3695],{"className":2177,"code":3694,"language":2179,"meta":191,"style":191},"class ViewController: UIViewController {\n    var name = \"VC\"\n\n    func setupTimer() {\n        \u002F\u002F ❌ 闭包隐式捕获 self（强引用）\n        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in\n            self.updateUI()  \u002F\u002F self 引用计数 +1\n        }\n\n        \u002F\u002F ✅ 使用 capture list 打破循环\n        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in\n            guard let self else { return }\n            self.updateUI()\n        }\n    }\n}\n",[136,3696,3697,3702,3707,3711,3716,3721,3726,3731,3735,3739,3744,3749,3754,3759,3763,3767],{"__ignoreMap":191},[195,3698,3699],{"class":197,"line":198},[195,3700,3701],{},"class ViewController: UIViewController {\n",[195,3703,3704],{"class":197,"line":230},[195,3705,3706],{},"    var name = \"VC\"\n",[195,3708,3709],{"class":197,"line":251},[195,3710,1241],{"emptyLinePlaceholder":757},[195,3712,3713],{"class":197,"line":272},[195,3714,3715],{},"    func setupTimer() {\n",[195,3717,3718],{"class":197,"line":293},[195,3719,3720],{},"        \u002F\u002F ❌ 闭包隐式捕获 self（强引用）\n",[195,3722,3723],{"class":197,"line":562},[195,3724,3725],{},"        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in\n",[195,3727,3728],{"class":197,"line":583},[195,3729,3730],{},"            self.updateUI()  \u002F\u002F self 引用计数 +1\n",[195,3732,3733],{"class":197,"line":962},[195,3734,2887],{},[195,3736,3737],{"class":197,"line":968},[195,3738,1241],{"emptyLinePlaceholder":757},[195,3740,3741],{"class":197,"line":1274},[195,3742,3743],{},"        \u002F\u002F ✅ 使用 capture list 打破循环\n",[195,3745,3746],{"class":197,"line":1282},[195,3747,3748],{},"        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in\n",[195,3750,3751],{"class":197,"line":1295},[195,3752,3753],{},"            guard let self else { return }\n",[195,3755,3756],{"class":197,"line":1309},[195,3757,3758],{},"            self.updateUI()\n",[195,3760,3761],{"class":197,"line":2246},[195,3762,2887],{},[195,3764,3765],{"class":197,"line":1996},[195,3766,2403],{},[195,3768,3769],{"class":197,"line":2257},[195,3770,552],{},[1890,3772],{},[32,3774,3776],{"id":3775},"q22-什么是-autorelease-pool在-swift-中还需要关心它吗","Q2.2: 什么是 Autorelease Pool？在 Swift 中还需要关心它吗？",[14,3778,3779],{},[125,3780,2077],{},[14,3782,3783],{},"Autorelease Pool 是 Objective-C MRC 时代的遗留机制，用于延迟释放对象：",[186,3785,3787],{"className":2177,"code":3786,"language":2179,"meta":191,"style":191},"\u002F\u002F Swift 中仍然需要关心的场景：\n\u002F\u002F 循环中大量创建临时对象时\nfor i in 0..\u003C1_000_000 {\n    \u002F\u002F ❌ 临时对象积压到 RunLoop 结束才释放\n    let image = processImage(data[i])\n}\n\n\u002F\u002F ✅ 用 autoreleasepool 及时释放\nfor i in 0..\u003C1_000_000 {\n    autoreleasepool {\n        let image = processImage(data[i])\n        \u002F\u002F 每次循环结束时释放 pool 中的对象\n    }\n}\n",[136,3788,3789,3794,3799,3804,3809,3814,3818,3822,3827,3831,3836,3841,3846,3850],{"__ignoreMap":191},[195,3790,3791],{"class":197,"line":198},[195,3792,3793],{},"\u002F\u002F Swift 中仍然需要关心的场景：\n",[195,3795,3796],{"class":197,"line":230},[195,3797,3798],{},"\u002F\u002F 循环中大量创建临时对象时\n",[195,3800,3801],{"class":197,"line":251},[195,3802,3803],{},"for i in 0..\u003C1_000_000 {\n",[195,3805,3806],{"class":197,"line":272},[195,3807,3808],{},"    \u002F\u002F ❌ 临时对象积压到 RunLoop 结束才释放\n",[195,3810,3811],{"class":197,"line":293},[195,3812,3813],{},"    let image = processImage(data[i])\n",[195,3815,3816],{"class":197,"line":562},[195,3817,552],{},[195,3819,3820],{"class":197,"line":583},[195,3821,1241],{"emptyLinePlaceholder":757},[195,3823,3824],{"class":197,"line":962},[195,3825,3826],{},"\u002F\u002F ✅ 用 autoreleasepool 及时释放\n",[195,3828,3829],{"class":197,"line":968},[195,3830,3803],{},[195,3832,3833],{"class":197,"line":1274},[195,3834,3835],{},"    autoreleasepool {\n",[195,3837,3838],{"class":197,"line":1282},[195,3839,3840],{},"        let image = processImage(data[i])\n",[195,3842,3843],{"class":197,"line":1295},[195,3844,3845],{},"        \u002F\u002F 每次循环结束时释放 pool 中的对象\n",[195,3847,3848],{"class":197,"line":1309},[195,3849,2403],{},[195,3851,3852],{"class":197,"line":2246},[195,3853,552],{},[14,3855,3856],{},[125,3857,3858],{},"什么时候需要 autoreleasepool：",[129,3860,3861,3864,3867],{},[132,3862,3863],{},"循环中大量创建临时对象（特别是与 Objective-C 桥接的对象）",[132,3865,3866],{},"后台线程（没有自动的 RunLoop autorelease pool）",[132,3868,3869],{},"命令行工具（没有 UIApplication 管理的 pool）",[14,3871,3872,3875],{},[125,3873,3874],{},"主线程 RunLoop"," 每次迭代结束时会自动 drain autorelease pool，所以日常开发中大多不需要手动管理。",[1890,3877],{},[18,3879,3881],{"id":3880},"_3-uikit-与-swiftui","3. UIKit 与 SwiftUI",[32,3883,3885],{"id":3884},"q31-swiftui-的视图更新机制是怎样的与-uikit-有什么本质区别","Q3.1: SwiftUI 的视图更新机制是怎样的？与 UIKit 有什么本质区别？",[14,3887,3888],{},[125,3889,2077],{},[14,3891,3892],{},[125,3893,3894],{},"UIKit（命令式）：",[186,3896,3898],{"className":2177,"code":3897,"language":2179,"meta":191,"style":191},"\u002F\u002F 手动管理状态同步\nclass CounterVC: UIViewController {\n    var count = 0\n    let label = UILabel()\n\n    func increment() {\n        count += 1\n        label.text = \"\\(count)\"       \u002F\u002F 手动更新 UI\n        button.isEnabled = count \u003C 10  \u002F\u002F 手动更新 UI\n        \u002F\u002F 每个状态变化都要手动同步所有关联的 UI\n    }\n}\n",[136,3899,3900,3905,3910,3915,3920,3924,3929,3934,3939,3944,3949,3953],{"__ignoreMap":191},[195,3901,3902],{"class":197,"line":198},[195,3903,3904],{},"\u002F\u002F 手动管理状态同步\n",[195,3906,3907],{"class":197,"line":230},[195,3908,3909],{},"class CounterVC: UIViewController {\n",[195,3911,3912],{"class":197,"line":251},[195,3913,3914],{},"    var count = 0\n",[195,3916,3917],{"class":197,"line":272},[195,3918,3919],{},"    let label = UILabel()\n",[195,3921,3922],{"class":197,"line":293},[195,3923,1241],{"emptyLinePlaceholder":757},[195,3925,3926],{"class":197,"line":562},[195,3927,3928],{},"    func increment() {\n",[195,3930,3931],{"class":197,"line":583},[195,3932,3933],{},"        count += 1\n",[195,3935,3936],{"class":197,"line":962},[195,3937,3938],{},"        label.text = \"\\(count)\"       \u002F\u002F 手动更新 UI\n",[195,3940,3941],{"class":197,"line":968},[195,3942,3943],{},"        button.isEnabled = count \u003C 10  \u002F\u002F 手动更新 UI\n",[195,3945,3946],{"class":197,"line":1274},[195,3947,3948],{},"        \u002F\u002F 每个状态变化都要手动同步所有关联的 UI\n",[195,3950,3951],{"class":197,"line":1282},[195,3952,2403],{},[195,3954,3955],{"class":197,"line":1295},[195,3956,552],{},[14,3958,3959],{},[125,3960,3961],{},"SwiftUI（声明式）：",[186,3963,3965],{"className":2177,"code":3964,"language":2179,"meta":191,"style":191},"struct CounterView: View {\n    @State private var count = 0\n\n    var body: some View {\n        VStack {\n            Text(\"\\(count)\")\n            Button(\"Increment\") { count += 1 }\n                .disabled(count >= 10)\n        }\n        \u002F\u002F 状态变化 → 自动重新计算 body → 高效 diff 更新\n    }\n}\n",[136,3966,3967,3972,3977,3981,3986,3990,3995,4000,4005,4009,4014,4018],{"__ignoreMap":191},[195,3968,3969],{"class":197,"line":198},[195,3970,3971],{},"struct CounterView: View {\n",[195,3973,3974],{"class":197,"line":230},[195,3975,3976],{},"    @State private var count = 0\n",[195,3978,3979],{"class":197,"line":251},[195,3980,1241],{"emptyLinePlaceholder":757},[195,3982,3983],{"class":197,"line":272},[195,3984,3985],{},"    var body: some View {\n",[195,3987,3988],{"class":197,"line":293},[195,3989,3382],{},[195,3991,3992],{"class":197,"line":562},[195,3993,3994],{},"            Text(\"\\(count)\")\n",[195,3996,3997],{"class":197,"line":583},[195,3998,3999],{},"            Button(\"Increment\") { count += 1 }\n",[195,4001,4002],{"class":197,"line":962},[195,4003,4004],{},"                .disabled(count >= 10)\n",[195,4006,4007],{"class":197,"line":968},[195,4008,2887],{},[195,4010,4011],{"class":197,"line":1274},[195,4012,4013],{},"        \u002F\u002F 状态变化 → 自动重新计算 body → 高效 diff 更新\n",[195,4015,4016],{"class":197,"line":1282},[195,4017,2403],{},[195,4019,4020],{"class":197,"line":1295},[195,4021,552],{},[14,4023,4024],{},[125,4025,4026],{},"SwiftUI 更新机制：",[186,4028,4031],{"className":4029,"code":4030,"language":1074},[1072],"@State \u002F @ObservedObject 变化\n          ↓\n标记 View 为 invalid\n          ↓\n调用 body 属性（重新计算声明）\n          ↓\n与旧的 View 值进行结构对比（Diff）\n          ↓\n只更新变化的部分到底层 UIKit\u002FAppKit 视图\n",[136,4032,4030],{"__ignoreMap":191},[14,4034,4035],{},[125,4036,4037],{},"关键设计差异：",[36,4039,4040,4052],{},[39,4041,4042],{},[42,4043,4044,4046,4049],{},[45,4045],{},[45,4047,4048],{},"UIKit",[45,4050,4051],{},"SwiftUI",[52,4053,4054,4065,4076,4087,4098,4109],{},[42,4055,4056,4059,4062],{},[57,4057,4058],{},"View 本质",[57,4060,4061],{},"类（引用类型，长寿命对象）",[57,4063,4064],{},"结构体（值类型，轻量描述）",[42,4066,4067,4070,4073],{},[57,4068,4069],{},"更新方式",[57,4071,4072],{},"命令式（手动 set）",[57,4074,4075],{},"声明式（状态驱动）",[42,4077,4078,4081,4084],{},[57,4079,4080],{},"布局",[57,4082,4083],{},"Auto Layout（约束求解）",[57,4085,4086],{},"自带布局系统（Stack\u002FAlignment）",[42,4088,4089,4092,4095],{},[57,4090,4091],{},"数据流",[57,4093,4094],{},"Delegate\u002FKVO\u002FNotificationCenter",[57,4096,4097],{},"@State\u002F@Binding\u002FEnvironment",[42,4099,4100,4103,4106],{},[57,4101,4102],{},"动画",[57,4104,4105],{},"UIView.animate \u002F Core Animation",[57,4107,4108],{},"withAnimation \u002F .animation modifier",[42,4110,4111,4114,4117],{},[57,4112,4113],{},"生命周期",[57,4115,4116],{},"viewDidLoad\u002FviewWillAppear...",[57,4118,4119],{},"onAppear\u002FonDisappear\u002Ftask",[1890,4121],{},[32,4123,4125,4126,997,4128,997,4130,997,4132,4134],{"id":4124},"q32-swiftui-中-statestateobjectobservedobjectenvironmentobject-的区别","Q3.2: SwiftUI 中 ",[136,4127,3120],{},[136,4129,3150],{},[136,4131,3140],{},[136,4133,3160],{}," 的区别？",[14,4136,4137],{},[125,4138,2077],{},[186,4140,4142],{"className":2177,"code":4141,"language":2179,"meta":191,"style":191},"\u002F\u002F @State：View 私有的简单值类型状态\nstruct ToggleView: View {\n    @State private var isOn = false  \u002F\u002F View 拥有此状态\n    var body: some View {\n        Toggle(\"Switch\", isOn: $isOn)\n    }\n}\n\n\u002F\u002F @StateObject：View 拥有的引用类型状态（整个生命周期只创建一次）\nstruct ParentView: View {\n    @StateObject private var viewModel = MyViewModel()  \u002F\u002F 只创建一次\n    var body: some View {\n        ChildView(viewModel: viewModel)\n    }\n}\n\n\u002F\u002F @ObservedObject：外部传入的引用类型状态（不拥有，可能重建）\nstruct ChildView: View {\n    @ObservedObject var viewModel: MyViewModel  \u002F\u002F 由父传入\n    var body: some View {\n        Text(viewModel.title)\n    }\n}\n\n\u002F\u002F @EnvironmentObject：通过环境注入的共享状态\nstruct DeepChildView: View {\n    @EnvironmentObject var settings: AppSettings  \u002F\u002F 从祖先注入\n    var body: some View {\n        Text(settings.theme)\n    }\n}\n\n\u002F\u002F 注入方式\nContentView()\n    .environmentObject(AppSettings())\n",[136,4143,4144,4149,4154,4159,4163,4168,4172,4176,4180,4185,4190,4195,4199,4204,4208,4212,4216,4221,4226,4231,4235,4240,4244,4248,4252,4257,4262,4267,4271,4276,4280,4284,4288,4293,4298],{"__ignoreMap":191},[195,4145,4146],{"class":197,"line":198},[195,4147,4148],{},"\u002F\u002F @State：View 私有的简单值类型状态\n",[195,4150,4151],{"class":197,"line":230},[195,4152,4153],{},"struct ToggleView: View {\n",[195,4155,4156],{"class":197,"line":251},[195,4157,4158],{},"    @State private var isOn = false  \u002F\u002F View 拥有此状态\n",[195,4160,4161],{"class":197,"line":272},[195,4162,3985],{},[195,4164,4165],{"class":197,"line":293},[195,4166,4167],{},"        Toggle(\"Switch\", isOn: $isOn)\n",[195,4169,4170],{"class":197,"line":562},[195,4171,2403],{},[195,4173,4174],{"class":197,"line":583},[195,4175,552],{},[195,4177,4178],{"class":197,"line":962},[195,4179,1241],{"emptyLinePlaceholder":757},[195,4181,4182],{"class":197,"line":968},[195,4183,4184],{},"\u002F\u002F @StateObject：View 拥有的引用类型状态（整个生命周期只创建一次）\n",[195,4186,4187],{"class":197,"line":1274},[195,4188,4189],{},"struct ParentView: View {\n",[195,4191,4192],{"class":197,"line":1282},[195,4193,4194],{},"    @StateObject private var viewModel = MyViewModel()  \u002F\u002F 只创建一次\n",[195,4196,4197],{"class":197,"line":1295},[195,4198,3985],{},[195,4200,4201],{"class":197,"line":1309},[195,4202,4203],{},"        ChildView(viewModel: viewModel)\n",[195,4205,4206],{"class":197,"line":2246},[195,4207,2403],{},[195,4209,4210],{"class":197,"line":1996},[195,4211,552],{},[195,4213,4214],{"class":197,"line":2257},[195,4215,1241],{"emptyLinePlaceholder":757},[195,4217,4218],{"class":197,"line":2262},[195,4219,4220],{},"\u002F\u002F @ObservedObject：外部传入的引用类型状态（不拥有，可能重建）\n",[195,4222,4223],{"class":197,"line":2267},[195,4224,4225],{},"struct ChildView: View {\n",[195,4227,4228],{"class":197,"line":2273},[195,4229,4230],{},"    @ObservedObject var viewModel: MyViewModel  \u002F\u002F 由父传入\n",[195,4232,4233],{"class":197,"line":2033},[195,4234,3985],{},[195,4236,4237],{"class":197,"line":2284},[195,4238,4239],{},"        Text(viewModel.title)\n",[195,4241,4242],{"class":197,"line":2460},[195,4243,2403],{},[195,4245,4246],{"class":197,"line":2466},[195,4247,552],{},[195,4249,4250],{"class":197,"line":2472},[195,4251,1241],{"emptyLinePlaceholder":757},[195,4253,4254],{"class":197,"line":2780},[195,4255,4256],{},"\u002F\u002F @EnvironmentObject：通过环境注入的共享状态\n",[195,4258,4259],{"class":197,"line":2786},[195,4260,4261],{},"struct DeepChildView: View {\n",[195,4263,4264],{"class":197,"line":2792},[195,4265,4266],{},"    @EnvironmentObject var settings: AppSettings  \u002F\u002F 从祖先注入\n",[195,4268,4269],{"class":197,"line":2798},[195,4270,3985],{},[195,4272,4273],{"class":197,"line":2804},[195,4274,4275],{},"        Text(settings.theme)\n",[195,4277,4278],{"class":197,"line":2810},[195,4279,2403],{},[195,4281,4282],{"class":197,"line":2815},[195,4283,552],{},[195,4285,4286],{"class":197,"line":2820},[195,4287,1241],{"emptyLinePlaceholder":757},[195,4289,4290],{"class":197,"line":2825},[195,4291,4292],{},"\u002F\u002F 注入方式\n",[195,4294,4295],{"class":197,"line":2831},[195,4296,4297],{},"ContentView()\n",[195,4299,4300],{"class":197,"line":2837},[195,4301,4302],{},"    .environmentObject(AppSettings())\n",[14,4304,4305],{},[125,4306,4307],{},"核心区别总结：",[36,4309,4310,4326],{},[39,4311,4312],{},[42,4313,4314,4316,4319,4321,4324],{},[45,4315,3108],{},[45,4317,4318],{},"所有权",[45,4320,3642],{},[45,4322,4323],{},"创建时机",[45,4325,3680],{},[52,4327,4328,4346,4362,4379],{},[42,4329,4330,4334,4337,4340,4343],{},[57,4331,4332],{},[136,4333,3120],{},[57,4335,4336],{},"View 拥有",[57,4338,4339],{},"值类型",[57,4341,4342],{},"View 首次创建时",[57,4344,4345],{},"简单的局部状态",[42,4347,4348,4352,4354,4357,4359],{},[57,4349,4350],{},[136,4351,3150],{},[57,4353,4336],{},[57,4355,4356],{},"ObservableObject",[57,4358,4342],{},[57,4360,4361],{},"ViewModel、复杂对象",[42,4363,4364,4368,4371,4373,4376],{},[57,4365,4366],{},[136,4367,3140],{},[57,4369,4370],{},"外部传入",[57,4372,4356],{},[57,4374,4375],{},"外部创建",[57,4377,4378],{},"父传子的数据",[42,4380,4381,4385,4388,4390,4393],{},[57,4382,4383],{},[136,4384,3160],{},[57,4386,4387],{},"环境注入",[57,4389,4356],{},[57,4391,4392],{},"祖先提供",[57,4394,4395],{},"全局共享（主题\u002F认证）",[14,4397,4398],{},[125,4399,4400],{},"⚠️ 常见陷阱：",[186,4402,4404],{"className":2177,"code":4403,"language":2179,"meta":191,"style":191},"\u002F\u002F ❌ 用 @ObservedObject 创建对象 → 每次 body 重算都会重建\nstruct BuggyView: View {\n    @ObservedObject var vm = MyViewModel()  \u002F\u002F 每次父 View 重建都创建新实例！\n    var body: some View { Text(vm.data) }\n}\n\n\u002F\u002F ✅ 用 @StateObject → 只创建一次\nstruct CorrectView: View {\n    @StateObject var vm = MyViewModel()\n    var body: some View { Text(vm.data) }\n}\n",[136,4405,4406,4411,4416,4421,4426,4430,4434,4439,4444,4449,4453],{"__ignoreMap":191},[195,4407,4408],{"class":197,"line":198},[195,4409,4410],{},"\u002F\u002F ❌ 用 @ObservedObject 创建对象 → 每次 body 重算都会重建\n",[195,4412,4413],{"class":197,"line":230},[195,4414,4415],{},"struct BuggyView: View {\n",[195,4417,4418],{"class":197,"line":251},[195,4419,4420],{},"    @ObservedObject var vm = MyViewModel()  \u002F\u002F 每次父 View 重建都创建新实例！\n",[195,4422,4423],{"class":197,"line":272},[195,4424,4425],{},"    var body: some View { Text(vm.data) }\n",[195,4427,4428],{"class":197,"line":293},[195,4429,552],{},[195,4431,4432],{"class":197,"line":562},[195,4433,1241],{"emptyLinePlaceholder":757},[195,4435,4436],{"class":197,"line":583},[195,4437,4438],{},"\u002F\u002F ✅ 用 @StateObject → 只创建一次\n",[195,4440,4441],{"class":197,"line":962},[195,4442,4443],{},"struct CorrectView: View {\n",[195,4445,4446],{"class":197,"line":968},[195,4447,4448],{},"    @StateObject var vm = MyViewModel()\n",[195,4450,4451],{"class":197,"line":1274},[195,4452,4425],{},[195,4454,4455],{"class":197,"line":1282},[195,4456,552],{},[1890,4458],{},[32,4460,4462],{"id":4461},"q33-uikit-中-uitableview-uicollectionview-的性能优化要点","Q3.3: UIKit 中 UITableView \u002F UICollectionView 的性能优化要点？",[14,4464,4465],{},[125,4466,2077],{},[186,4468,4470],{"className":2177,"code":4469,"language":2179,"meta":191,"style":191},"\u002F\u002F 1. Cell 复用（最基础）\nfunc tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {\n    let cell = tableView.dequeueReusableCell(withIdentifier: \"Cell\", for: indexPath)\n    \u002F\u002F 配置 cell...\n    return cell\n}\n\n\u002F\u002F 2. 预估高度 + 自动计算\ntableView.estimatedRowHeight = 80\ntableView.rowHeight = UITableView.automaticDimension\n\n\u002F\u002F 3. 异步图片加载 + 取消\nfunc tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {\n    let cell = tableView.dequeueReusableCell(withIdentifier: \"Cell\", for: indexPath) as! ImageCell\n    cell.task?.cancel()  \u002F\u002F 取消上一次请求\n    cell.task = Task {\n        let image = try await ImageLoader.load(url: items[indexPath.row].imageURL)\n        if !Task.isCancelled {\n            cell.photoView.image = image\n        }\n    }\n    return cell\n}\n\n\u002F\u002F 4. 预加载（Prefetching）\nextension VC: UITableViewDataSourcePrefetching {\n    func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {\n        for indexPath in indexPaths {\n            ImageLoader.prefetch(url: items[indexPath.row].imageURL)\n        }\n    }\n\n    func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {\n        for indexPath in indexPaths {\n            ImageLoader.cancelPrefetch(url: items[indexPath.row].imageURL)\n        }\n    }\n}\n\n\u002F\u002F 5. Diffable Data Source（iOS 13+，避免 reloadData）\nvar snapshot = NSDiffableDataSourceSnapshot\u003CSection, Item>()\nsnapshot.appendSections([.main])\nsnapshot.appendItems(items, toSection: .main)\ndataSource.apply(snapshot, animatingDifferences: true)\n\n\u002F\u002F 6. Compositional Layout（iOS 13+，高性能复杂布局）\nlet layout = UICollectionViewCompositionalLayout { sectionIndex, env in\n    let item = NSCollectionLayoutItem(layoutSize: ...)\n    let group = NSCollectionLayoutGroup.horizontal(layoutSize: ..., subitems: [item])\n    return NSCollectionLayoutSection(group: group)\n}\n",[136,4471,4472,4477,4482,4487,4492,4497,4501,4505,4510,4515,4520,4524,4529,4533,4538,4543,4548,4553,4558,4563,4567,4571,4575,4579,4583,4588,4593,4598,4603,4608,4612,4616,4620,4625,4629,4634,4638,4642,4646,4650,4655,4660,4665,4670,4675,4679,4684,4689,4694,4699,4704],{"__ignoreMap":191},[195,4473,4474],{"class":197,"line":198},[195,4475,4476],{},"\u002F\u002F 1. Cell 复用（最基础）\n",[195,4478,4479],{"class":197,"line":230},[195,4480,4481],{},"func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {\n",[195,4483,4484],{"class":197,"line":251},[195,4485,4486],{},"    let cell = tableView.dequeueReusableCell(withIdentifier: \"Cell\", for: indexPath)\n",[195,4488,4489],{"class":197,"line":272},[195,4490,4491],{},"    \u002F\u002F 配置 cell...\n",[195,4493,4494],{"class":197,"line":293},[195,4495,4496],{},"    return cell\n",[195,4498,4499],{"class":197,"line":562},[195,4500,552],{},[195,4502,4503],{"class":197,"line":583},[195,4504,1241],{"emptyLinePlaceholder":757},[195,4506,4507],{"class":197,"line":962},[195,4508,4509],{},"\u002F\u002F 2. 预估高度 + 自动计算\n",[195,4511,4512],{"class":197,"line":968},[195,4513,4514],{},"tableView.estimatedRowHeight = 80\n",[195,4516,4517],{"class":197,"line":1274},[195,4518,4519],{},"tableView.rowHeight = UITableView.automaticDimension\n",[195,4521,4522],{"class":197,"line":1282},[195,4523,1241],{"emptyLinePlaceholder":757},[195,4525,4526],{"class":197,"line":1295},[195,4527,4528],{},"\u002F\u002F 3. 异步图片加载 + 取消\n",[195,4530,4531],{"class":197,"line":1309},[195,4532,4481],{},[195,4534,4535],{"class":197,"line":2246},[195,4536,4537],{},"    let cell = tableView.dequeueReusableCell(withIdentifier: \"Cell\", for: indexPath) as! ImageCell\n",[195,4539,4540],{"class":197,"line":1996},[195,4541,4542],{},"    cell.task?.cancel()  \u002F\u002F 取消上一次请求\n",[195,4544,4545],{"class":197,"line":2257},[195,4546,4547],{},"    cell.task = Task {\n",[195,4549,4550],{"class":197,"line":2262},[195,4551,4552],{},"        let image = try await ImageLoader.load(url: items[indexPath.row].imageURL)\n",[195,4554,4555],{"class":197,"line":2267},[195,4556,4557],{},"        if !Task.isCancelled {\n",[195,4559,4560],{"class":197,"line":2273},[195,4561,4562],{},"            cell.photoView.image = image\n",[195,4564,4565],{"class":197,"line":2033},[195,4566,2887],{},[195,4568,4569],{"class":197,"line":2284},[195,4570,2403],{},[195,4572,4573],{"class":197,"line":2460},[195,4574,4496],{},[195,4576,4577],{"class":197,"line":2466},[195,4578,552],{},[195,4580,4581],{"class":197,"line":2472},[195,4582,1241],{"emptyLinePlaceholder":757},[195,4584,4585],{"class":197,"line":2780},[195,4586,4587],{},"\u002F\u002F 4. 预加载（Prefetching）\n",[195,4589,4590],{"class":197,"line":2786},[195,4591,4592],{},"extension VC: UITableViewDataSourcePrefetching {\n",[195,4594,4595],{"class":197,"line":2792},[195,4596,4597],{},"    func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {\n",[195,4599,4600],{"class":197,"line":2798},[195,4601,4602],{},"        for indexPath in indexPaths {\n",[195,4604,4605],{"class":197,"line":2804},[195,4606,4607],{},"            ImageLoader.prefetch(url: items[indexPath.row].imageURL)\n",[195,4609,4610],{"class":197,"line":2810},[195,4611,2887],{},[195,4613,4614],{"class":197,"line":2815},[195,4615,2403],{},[195,4617,4618],{"class":197,"line":2820},[195,4619,1241],{"emptyLinePlaceholder":757},[195,4621,4622],{"class":197,"line":2825},[195,4623,4624],{},"    func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) {\n",[195,4626,4627],{"class":197,"line":2831},[195,4628,4602],{},[195,4630,4631],{"class":197,"line":2837},[195,4632,4633],{},"            ImageLoader.cancelPrefetch(url: items[indexPath.row].imageURL)\n",[195,4635,4636],{"class":197,"line":2843},[195,4637,2887],{},[195,4639,4640],{"class":197,"line":2848},[195,4641,2403],{},[195,4643,4644],{"class":197,"line":2854},[195,4645,552],{},[195,4647,4648],{"class":197,"line":2860},[195,4649,1241],{"emptyLinePlaceholder":757},[195,4651,4652],{"class":197,"line":2866},[195,4653,4654],{},"\u002F\u002F 5. Diffable Data Source（iOS 13+，避免 reloadData）\n",[195,4656,4657],{"class":197,"line":2872},[195,4658,4659],{},"var snapshot = NSDiffableDataSourceSnapshot\u003CSection, Item>()\n",[195,4661,4662],{"class":197,"line":2878},[195,4663,4664],{},"snapshot.appendSections([.main])\n",[195,4666,4667],{"class":197,"line":2884},[195,4668,4669],{},"snapshot.appendItems(items, toSection: .main)\n",[195,4671,4672],{"class":197,"line":2890},[195,4673,4674],{},"dataSource.apply(snapshot, animatingDifferences: true)\n",[195,4676,4677],{"class":197,"line":2895},[195,4678,1241],{"emptyLinePlaceholder":757},[195,4680,4681],{"class":197,"line":2900},[195,4682,4683],{},"\u002F\u002F 6. Compositional Layout（iOS 13+，高性能复杂布局）\n",[195,4685,4686],{"class":197,"line":2905},[195,4687,4688],{},"let layout = UICollectionViewCompositionalLayout { sectionIndex, env in\n",[195,4690,4691],{"class":197,"line":2911},[195,4692,4693],{},"    let item = NSCollectionLayoutItem(layoutSize: ...)\n",[195,4695,4696],{"class":197,"line":2917},[195,4697,4698],{},"    let group = NSCollectionLayoutGroup.horizontal(layoutSize: ..., subitems: [item])\n",[195,4700,4701],{"class":197,"line":2923},[195,4702,4703],{},"    return NSCollectionLayoutSection(group: group)\n",[195,4705,4706],{"class":197,"line":2929},[195,4707,552],{},[14,4709,4710],{},[125,4711,4712],{},"关键优化清单：",[129,4714,4715,4722,4733,4736],{},[132,4716,4717,4718,4721],{},"避免在 ",[136,4719,4720],{},"cellForRow"," 中做耗时操作",[132,4723,4724,4725,4728,4729,4732],{},"Cell 中避免离屏渲染（圆角用 ",[136,4726,4727],{},"cornerRadius"," + ",[136,4730,4731],{},"masksToBounds"," 会触发，可用贝塞尔路径裁剪）",[132,4734,4735],{},"图层扁平化（减少层级嵌套）",[132,4737,4738],{},"固定高度优于动态计算",[1890,4740],{},[18,4742,4744],{"id":4743},"_4-并发编程","4. 并发编程",[32,4746,4748],{"id":4747},"q41-swift-concurrencyasyncawaitactor如何工作","Q4.1: Swift Concurrency（async\u002Fawait、Actor）如何工作？",[14,4750,4751],{},[125,4752,2077],{},[14,4754,4755],{},[125,4756,4757],{},"async\u002Fawait（结构化并发）：",[186,4759,4761],{"className":2177,"code":4760,"language":2179,"meta":191,"style":191},"\u002F\u002F 定义异步函数\nfunc fetchUser(id: String) async throws -> User {\n    let (data, _) = try await URLSession.shared.data(from: url)\n    return try JSONDecoder().decode(User.self, from: data)\n}\n\n\u002F\u002F 调用\nTask {\n    do {\n        let user = try await fetchUser(id: \"123\")\n        \u002F\u002F 自动在合适的线程更新（如果是 @MainActor）\n    } catch {\n        print(error)\n    }\n}\n",[136,4762,4763,4768,4773,4778,4783,4787,4791,4796,4801,4806,4811,4816,4821,4826,4830],{"__ignoreMap":191},[195,4764,4765],{"class":197,"line":198},[195,4766,4767],{},"\u002F\u002F 定义异步函数\n",[195,4769,4770],{"class":197,"line":230},[195,4771,4772],{},"func fetchUser(id: String) async throws -> User {\n",[195,4774,4775],{"class":197,"line":251},[195,4776,4777],{},"    let (data, _) = try await URLSession.shared.data(from: url)\n",[195,4779,4780],{"class":197,"line":272},[195,4781,4782],{},"    return try JSONDecoder().decode(User.self, from: data)\n",[195,4784,4785],{"class":197,"line":293},[195,4786,552],{},[195,4788,4789],{"class":197,"line":562},[195,4790,1241],{"emptyLinePlaceholder":757},[195,4792,4793],{"class":197,"line":583},[195,4794,4795],{},"\u002F\u002F 调用\n",[195,4797,4798],{"class":197,"line":962},[195,4799,4800],{},"Task {\n",[195,4802,4803],{"class":197,"line":968},[195,4804,4805],{},"    do {\n",[195,4807,4808],{"class":197,"line":1274},[195,4809,4810],{},"        let user = try await fetchUser(id: \"123\")\n",[195,4812,4813],{"class":197,"line":1282},[195,4814,4815],{},"        \u002F\u002F 自动在合适的线程更新（如果是 @MainActor）\n",[195,4817,4818],{"class":197,"line":1295},[195,4819,4820],{},"    } catch {\n",[195,4822,4823],{"class":197,"line":1309},[195,4824,4825],{},"        print(error)\n",[195,4827,4828],{"class":197,"line":2246},[195,4829,2403],{},[195,4831,4832],{"class":197,"line":1996},[195,4833,552],{},[14,4835,4836],{},[125,4837,4838],{},"并行执行：",[186,4840,4842],{"className":2177,"code":4841,"language":2179,"meta":191,"style":191},"\u002F\u002F async let：并行启动多个任务\nfunc loadDashboard() async throws -> Dashboard {\n    async let user = fetchUser()\n    async let posts = fetchPosts()\n    async let notifications = fetchNotifications()\n\n    \u002F\u002F 三个请求并行执行，在这里等待全部完成\n    return try await Dashboard(\n        user: user,\n        posts: posts,\n        notifications: notifications\n    )\n}\n\n\u002F\u002F TaskGroup：动态数量的并行任务\nfunc fetchAllImages(urls: [URL]) async throws -> [UIImage] {\n    try await withThrowingTaskGroup(of: UIImage.self) { group in\n        for url in urls {\n            group.addTask { try await downloadImage(url) }\n        }\n        var images: [UIImage] = []\n        for try await image in group {\n            images.append(image)\n        }\n        return images\n    }\n}\n",[136,4843,4844,4849,4854,4859,4864,4869,4873,4878,4883,4888,4893,4898,4903,4907,4911,4916,4921,4926,4931,4936,4940,4945,4950,4955,4959,4964,4968],{"__ignoreMap":191},[195,4845,4846],{"class":197,"line":198},[195,4847,4848],{},"\u002F\u002F async let：并行启动多个任务\n",[195,4850,4851],{"class":197,"line":230},[195,4852,4853],{},"func loadDashboard() async throws -> Dashboard {\n",[195,4855,4856],{"class":197,"line":251},[195,4857,4858],{},"    async let user = fetchUser()\n",[195,4860,4861],{"class":197,"line":272},[195,4862,4863],{},"    async let posts = fetchPosts()\n",[195,4865,4866],{"class":197,"line":293},[195,4867,4868],{},"    async let notifications = fetchNotifications()\n",[195,4870,4871],{"class":197,"line":562},[195,4872,1241],{"emptyLinePlaceholder":757},[195,4874,4875],{"class":197,"line":583},[195,4876,4877],{},"    \u002F\u002F 三个请求并行执行，在这里等待全部完成\n",[195,4879,4880],{"class":197,"line":962},[195,4881,4882],{},"    return try await Dashboard(\n",[195,4884,4885],{"class":197,"line":968},[195,4886,4887],{},"        user: user,\n",[195,4889,4890],{"class":197,"line":1274},[195,4891,4892],{},"        posts: posts,\n",[195,4894,4895],{"class":197,"line":1282},[195,4896,4897],{},"        notifications: notifications\n",[195,4899,4900],{"class":197,"line":1295},[195,4901,4902],{},"    )\n",[195,4904,4905],{"class":197,"line":1309},[195,4906,552],{},[195,4908,4909],{"class":197,"line":2246},[195,4910,1241],{"emptyLinePlaceholder":757},[195,4912,4913],{"class":197,"line":1996},[195,4914,4915],{},"\u002F\u002F TaskGroup：动态数量的并行任务\n",[195,4917,4918],{"class":197,"line":2257},[195,4919,4920],{},"func fetchAllImages(urls: [URL]) async throws -> [UIImage] {\n",[195,4922,4923],{"class":197,"line":2262},[195,4924,4925],{},"    try await withThrowingTaskGroup(of: UIImage.self) { group in\n",[195,4927,4928],{"class":197,"line":2267},[195,4929,4930],{},"        for url in urls {\n",[195,4932,4933],{"class":197,"line":2273},[195,4934,4935],{},"            group.addTask { try await downloadImage(url) }\n",[195,4937,4938],{"class":197,"line":2033},[195,4939,2887],{},[195,4941,4942],{"class":197,"line":2284},[195,4943,4944],{},"        var images: [UIImage] = []\n",[195,4946,4947],{"class":197,"line":2460},[195,4948,4949],{},"        for try await image in group {\n",[195,4951,4952],{"class":197,"line":2466},[195,4953,4954],{},"            images.append(image)\n",[195,4956,4957],{"class":197,"line":2472},[195,4958,2887],{},[195,4960,4961],{"class":197,"line":2780},[195,4962,4963],{},"        return images\n",[195,4965,4966],{"class":197,"line":2786},[195,4967,2403],{},[195,4969,4970],{"class":197,"line":2792},[195,4971,552],{},[14,4973,4974],{},[125,4975,4976],{},"Actor（数据竞争保护）：",[186,4978,4980],{"className":2177,"code":4979,"language":2179,"meta":191,"style":191},"\u002F\u002F Actor：保证内部状态串行访问，消除数据竞争\nactor BankAccount {\n    private var balance: Double = 0\n\n    func deposit(_ amount: Double) {\n        balance += amount\n    }\n\n    func withdraw(_ amount: Double) throws -> Double {\n        guard balance >= amount else { throw InsufficientFundsError() }\n        balance -= amount\n        return amount\n    }\n}\n\n\u002F\u002F 外部访问 Actor 属性\u002F方法必须 await\nlet account = BankAccount()\nawait account.deposit(100)\nlet money = try await account.withdraw(50)\n\n\u002F\u002F @MainActor：保证在主线程执行\n@MainActor\nclass ViewModel: ObservableObject {\n    @Published var items: [Item] = []\n\n    func load() async {\n        let data = await fetchFromNetwork()  \u002F\u002F 可能在其他线程\n        items = data  \u002F\u002F 自动回到主线程，因为类标记了 @MainActor\n    }\n}\n",[136,4981,4982,4987,4992,4997,5001,5006,5011,5015,5019,5024,5029,5034,5039,5043,5047,5051,5056,5061,5066,5071,5075,5080,5085,5090,5095,5099,5104,5109,5114,5118],{"__ignoreMap":191},[195,4983,4984],{"class":197,"line":198},[195,4985,4986],{},"\u002F\u002F Actor：保证内部状态串行访问，消除数据竞争\n",[195,4988,4989],{"class":197,"line":230},[195,4990,4991],{},"actor BankAccount {\n",[195,4993,4994],{"class":197,"line":251},[195,4995,4996],{},"    private var balance: Double = 0\n",[195,4998,4999],{"class":197,"line":272},[195,5000,1241],{"emptyLinePlaceholder":757},[195,5002,5003],{"class":197,"line":293},[195,5004,5005],{},"    func deposit(_ amount: Double) {\n",[195,5007,5008],{"class":197,"line":562},[195,5009,5010],{},"        balance += amount\n",[195,5012,5013],{"class":197,"line":583},[195,5014,2403],{},[195,5016,5017],{"class":197,"line":962},[195,5018,1241],{"emptyLinePlaceholder":757},[195,5020,5021],{"class":197,"line":968},[195,5022,5023],{},"    func withdraw(_ amount: Double) throws -> Double {\n",[195,5025,5026],{"class":197,"line":1274},[195,5027,5028],{},"        guard balance >= amount else { throw InsufficientFundsError() }\n",[195,5030,5031],{"class":197,"line":1282},[195,5032,5033],{},"        balance -= amount\n",[195,5035,5036],{"class":197,"line":1295},[195,5037,5038],{},"        return amount\n",[195,5040,5041],{"class":197,"line":1309},[195,5042,2403],{},[195,5044,5045],{"class":197,"line":2246},[195,5046,552],{},[195,5048,5049],{"class":197,"line":1996},[195,5050,1241],{"emptyLinePlaceholder":757},[195,5052,5053],{"class":197,"line":2257},[195,5054,5055],{},"\u002F\u002F 外部访问 Actor 属性\u002F方法必须 await\n",[195,5057,5058],{"class":197,"line":2262},[195,5059,5060],{},"let account = BankAccount()\n",[195,5062,5063],{"class":197,"line":2267},[195,5064,5065],{},"await account.deposit(100)\n",[195,5067,5068],{"class":197,"line":2273},[195,5069,5070],{},"let money = try await account.withdraw(50)\n",[195,5072,5073],{"class":197,"line":2033},[195,5074,1241],{"emptyLinePlaceholder":757},[195,5076,5077],{"class":197,"line":2284},[195,5078,5079],{},"\u002F\u002F @MainActor：保证在主线程执行\n",[195,5081,5082],{"class":197,"line":2460},[195,5083,5084],{},"@MainActor\n",[195,5086,5087],{"class":197,"line":2466},[195,5088,5089],{},"class ViewModel: ObservableObject {\n",[195,5091,5092],{"class":197,"line":2472},[195,5093,5094],{},"    @Published var items: [Item] = []\n",[195,5096,5097],{"class":197,"line":2780},[195,5098,1241],{"emptyLinePlaceholder":757},[195,5100,5101],{"class":197,"line":2786},[195,5102,5103],{},"    func load() async {\n",[195,5105,5106],{"class":197,"line":2792},[195,5107,5108],{},"        let data = await fetchFromNetwork()  \u002F\u002F 可能在其他线程\n",[195,5110,5111],{"class":197,"line":2798},[195,5112,5113],{},"        items = data  \u002F\u002F 自动回到主线程，因为类标记了 @MainActor\n",[195,5115,5116],{"class":197,"line":2804},[195,5117,2403],{},[195,5119,5120],{"class":197,"line":2810},[195,5121,552],{},[14,5123,5124],{},[125,5125,5126],{},"Sendable：",[186,5128,5130],{"className":2177,"code":5129,"language":2179,"meta":191,"style":191},"\u002F\u002F Sendable 标记可以安全跨并发域传递的类型\nstruct UserData: Sendable {  \u002F\u002F 值类型通常自动 Sendable\n    let name: String\n    let age: Int\n}\n\n\u002F\u002F ⚠️ 类需要满足条件才能 Sendable\nfinal class Config: Sendable {  \u002F\u002F final + 所有属性不可变\n    let apiKey: String\n    init(apiKey: String) { self.apiKey = apiKey }\n}\n",[136,5131,5132,5137,5142,5147,5152,5156,5160,5165,5170,5175,5180],{"__ignoreMap":191},[195,5133,5134],{"class":197,"line":198},[195,5135,5136],{},"\u002F\u002F Sendable 标记可以安全跨并发域传递的类型\n",[195,5138,5139],{"class":197,"line":230},[195,5140,5141],{},"struct UserData: Sendable {  \u002F\u002F 值类型通常自动 Sendable\n",[195,5143,5144],{"class":197,"line":251},[195,5145,5146],{},"    let name: String\n",[195,5148,5149],{"class":197,"line":272},[195,5150,5151],{},"    let age: Int\n",[195,5153,5154],{"class":197,"line":293},[195,5155,552],{},[195,5157,5158],{"class":197,"line":562},[195,5159,1241],{"emptyLinePlaceholder":757},[195,5161,5162],{"class":197,"line":583},[195,5163,5164],{},"\u002F\u002F ⚠️ 类需要满足条件才能 Sendable\n",[195,5166,5167],{"class":197,"line":962},[195,5168,5169],{},"final class Config: Sendable {  \u002F\u002F final + 所有属性不可变\n",[195,5171,5172],{"class":197,"line":968},[195,5173,5174],{},"    let apiKey: String\n",[195,5176,5177],{"class":197,"line":1274},[195,5178,5179],{},"    init(apiKey: String) { self.apiKey = apiKey }\n",[195,5181,5182],{"class":197,"line":1282},[195,5183,552],{},[1890,5185],{},[32,5187,5189],{"id":5188},"q42-gcdgrand-central-dispatch的核心概念与-swift-concurrency-的对比","Q4.2: GCD（Grand Central Dispatch）的核心概念？与 Swift Concurrency 的对比？",[14,5191,5192],{},[125,5193,2077],{},[186,5195,5197],{"className":2177,"code":5196,"language":2179,"meta":191,"style":191},"\u002F\u002F GCD：基于队列的并发模型\n\u002F\u002F 串行队列\nlet serial = DispatchQueue(label: \"com.app.serial\")\nserial.async { \u002F* 任务按顺序执行 *\u002F }\n\n\u002F\u002F 并发队列\nlet concurrent = DispatchQueue(label: \"com.app.concurrent\", attributes: .concurrent)\nconcurrent.async { \u002F* 任务并行执行 *\u002F }\n\n\u002F\u002F 主队列\nDispatchQueue.main.async { \u002F* UI 更新 *\u002F }\n\n\u002F\u002F 全局队列（按优先级）\nDispatchQueue.global(qos: .userInitiated).async { \u002F* 高优先级后台任务 *\u002F }\nDispatchQueue.global(qos: .utility).async { \u002F* 低优先级任务 *\u002F }\n\n\u002F\u002F DispatchGroup：等待一组任务完成\nlet group = DispatchGroup()\ngroup.enter()\nfetchA { group.leave() }\ngroup.enter()\nfetchB { group.leave() }\ngroup.notify(queue: .main) { \u002F* 全部完成 *\u002F }\n\n\u002F\u002F Semaphore：限制并发数\nlet semaphore = DispatchSemaphore(value: 3) \u002F\u002F 最多 3 个并发\nfor url in urls {\n    semaphore.wait()\n    queue.async {\n        download(url)\n        semaphore.signal()\n    }\n}\n",[136,5198,5199,5204,5209,5214,5219,5223,5228,5233,5238,5242,5247,5252,5256,5261,5266,5271,5275,5280,5285,5290,5295,5299,5304,5309,5313,5318,5323,5328,5333,5338,5343,5348,5352],{"__ignoreMap":191},[195,5200,5201],{"class":197,"line":198},[195,5202,5203],{},"\u002F\u002F GCD：基于队列的并发模型\n",[195,5205,5206],{"class":197,"line":230},[195,5207,5208],{},"\u002F\u002F 串行队列\n",[195,5210,5211],{"class":197,"line":251},[195,5212,5213],{},"let serial = DispatchQueue(label: \"com.app.serial\")\n",[195,5215,5216],{"class":197,"line":272},[195,5217,5218],{},"serial.async { \u002F* 任务按顺序执行 *\u002F }\n",[195,5220,5221],{"class":197,"line":293},[195,5222,1241],{"emptyLinePlaceholder":757},[195,5224,5225],{"class":197,"line":562},[195,5226,5227],{},"\u002F\u002F 并发队列\n",[195,5229,5230],{"class":197,"line":583},[195,5231,5232],{},"let concurrent = DispatchQueue(label: \"com.app.concurrent\", attributes: .concurrent)\n",[195,5234,5235],{"class":197,"line":962},[195,5236,5237],{},"concurrent.async { \u002F* 任务并行执行 *\u002F }\n",[195,5239,5240],{"class":197,"line":968},[195,5241,1241],{"emptyLinePlaceholder":757},[195,5243,5244],{"class":197,"line":1274},[195,5245,5246],{},"\u002F\u002F 主队列\n",[195,5248,5249],{"class":197,"line":1282},[195,5250,5251],{},"DispatchQueue.main.async { \u002F* UI 更新 *\u002F }\n",[195,5253,5254],{"class":197,"line":1295},[195,5255,1241],{"emptyLinePlaceholder":757},[195,5257,5258],{"class":197,"line":1309},[195,5259,5260],{},"\u002F\u002F 全局队列（按优先级）\n",[195,5262,5263],{"class":197,"line":2246},[195,5264,5265],{},"DispatchQueue.global(qos: .userInitiated).async { \u002F* 高优先级后台任务 *\u002F }\n",[195,5267,5268],{"class":197,"line":1996},[195,5269,5270],{},"DispatchQueue.global(qos: .utility).async { \u002F* 低优先级任务 *\u002F }\n",[195,5272,5273],{"class":197,"line":2257},[195,5274,1241],{"emptyLinePlaceholder":757},[195,5276,5277],{"class":197,"line":2262},[195,5278,5279],{},"\u002F\u002F DispatchGroup：等待一组任务完成\n",[195,5281,5282],{"class":197,"line":2267},[195,5283,5284],{},"let group = DispatchGroup()\n",[195,5286,5287],{"class":197,"line":2273},[195,5288,5289],{},"group.enter()\n",[195,5291,5292],{"class":197,"line":2033},[195,5293,5294],{},"fetchA { group.leave() }\n",[195,5296,5297],{"class":197,"line":2284},[195,5298,5289],{},[195,5300,5301],{"class":197,"line":2460},[195,5302,5303],{},"fetchB { group.leave() }\n",[195,5305,5306],{"class":197,"line":2466},[195,5307,5308],{},"group.notify(queue: .main) { \u002F* 全部完成 *\u002F }\n",[195,5310,5311],{"class":197,"line":2472},[195,5312,1241],{"emptyLinePlaceholder":757},[195,5314,5315],{"class":197,"line":2780},[195,5316,5317],{},"\u002F\u002F Semaphore：限制并发数\n",[195,5319,5320],{"class":197,"line":2786},[195,5321,5322],{},"let semaphore = DispatchSemaphore(value: 3) \u002F\u002F 最多 3 个并发\n",[195,5324,5325],{"class":197,"line":2792},[195,5326,5327],{},"for url in urls {\n",[195,5329,5330],{"class":197,"line":2798},[195,5331,5332],{},"    semaphore.wait()\n",[195,5334,5335],{"class":197,"line":2804},[195,5336,5337],{},"    queue.async {\n",[195,5339,5340],{"class":197,"line":2810},[195,5341,5342],{},"        download(url)\n",[195,5344,5345],{"class":197,"line":2815},[195,5346,5347],{},"        semaphore.signal()\n",[195,5349,5350],{"class":197,"line":2820},[195,5351,2403],{},[195,5353,5354],{"class":197,"line":2825},[195,5355,552],{},[14,5357,5358],{},[125,5359,5360],{},"GCD vs Swift Concurrency：",[36,5362,5363,5375],{},[39,5364,5365],{},[42,5366,5367,5369,5372],{},[45,5368],{},[45,5370,5371],{},"GCD",[45,5373,5374],{},"Swift Concurrency",[52,5376,5377,5388,5399,5410,5421,5431,5442],{},[42,5378,5379,5382,5385],{},[57,5380,5381],{},"语法",[57,5383,5384],{},"闭包回调",[57,5386,5387],{},"async\u002Fawait",[42,5389,5390,5393,5396],{},[57,5391,5392],{},"线程管理",[57,5394,5395],{},"开发者控制队列",[57,5397,5398],{},"运行时自动管理",[42,5400,5401,5404,5407],{},[57,5402,5403],{},"数据安全",[57,5405,5406],{},"需手动加锁\u002F串行队列",[57,5408,5409],{},"Actor 自动保护",[42,5411,5412,5415,5418],{},[57,5413,5414],{},"取消支持",[57,5416,5417],{},"需手动实现",[57,5419,5420],{},"Task.cancel() 内建",[42,5422,5423,5426,5428],{},[57,5424,5425],{},"编译检查",[57,5427,2135],{},[57,5429,5430],{},"Sendable 编译检查",[42,5432,5433,5436,5439],{},[57,5434,5435],{},"回调地狱",[57,5437,5438],{},"容易出现",[57,5440,5441],{},"线性代码流",[42,5443,5444,5447,5450],{},[57,5445,5446],{},"优先级反转",[57,5448,5449],{},"可能发生",[57,5451,5452],{},"运行时自动处理",[1890,5454],{},[18,5456,5458],{"id":5457},"_5-架构与设计模式ios","5. 架构与设计模式（iOS）",[32,5460,5462],{"id":5461},"q51-对比-mvcmvvmvipertca-架构","Q5.1: 对比 MVC、MVVM、VIPER、TCA 架构",[14,5464,5465],{},[125,5466,2077],{},[14,5468,5469],{},[125,5470,5471],{},"MVC（Apple 传统）：",[186,5473,5476],{"className":5474,"code":5475,"language":1074},[1072],"View ← Controller → Model\n       (Massive)\n",[136,5477,5475],{"__ignoreMap":191},[14,5479,5480],{},"问题：Controller 承担过多职责（Massive View Controller）",[14,5482,5483],{},[125,5484,5485],{},"MVVM：",[186,5487,5489],{"className":2177,"code":5488,"language":2179,"meta":191,"style":191},"\u002F\u002F Model\nstruct User { let name: String; let email: String }\n\n\u002F\u002F ViewModel：不依赖 UIKit\nclass UserViewModel: ObservableObject {\n    @Published var displayName = \"\"\n    @Published var isLoading = false\n    private let repository: UserRepository\n\n    init(repository: UserRepository) {\n        self.repository = repository\n    }\n\n    func load() async {\n        isLoading = true\n        let user = try? await repository.getUser()\n        displayName = user?.name ?? \"Unknown\"\n        isLoading = false\n    }\n}\n\n\u002F\u002F View：只负责展示\nstruct UserView: View {\n    @StateObject var viewModel: UserViewModel\n\n    var body: some View {\n        if viewModel.isLoading {\n            ProgressView()\n        } else {\n            Text(viewModel.displayName)\n        }\n    }\n}\n",[136,5490,5491,5496,5501,5505,5510,5515,5520,5525,5530,5534,5539,5544,5548,5552,5556,5561,5566,5571,5576,5580,5584,5588,5593,5598,5603,5607,5611,5616,5621,5626,5631,5635,5639],{"__ignoreMap":191},[195,5492,5493],{"class":197,"line":198},[195,5494,5495],{},"\u002F\u002F Model\n",[195,5497,5498],{"class":197,"line":230},[195,5499,5500],{},"struct User { let name: String; let email: String }\n",[195,5502,5503],{"class":197,"line":251},[195,5504,1241],{"emptyLinePlaceholder":757},[195,5506,5507],{"class":197,"line":272},[195,5508,5509],{},"\u002F\u002F ViewModel：不依赖 UIKit\n",[195,5511,5512],{"class":197,"line":293},[195,5513,5514],{},"class UserViewModel: ObservableObject {\n",[195,5516,5517],{"class":197,"line":562},[195,5518,5519],{},"    @Published var displayName = \"\"\n",[195,5521,5522],{"class":197,"line":583},[195,5523,5524],{},"    @Published var isLoading = false\n",[195,5526,5527],{"class":197,"line":962},[195,5528,5529],{},"    private let repository: UserRepository\n",[195,5531,5532],{"class":197,"line":968},[195,5533,1241],{"emptyLinePlaceholder":757},[195,5535,5536],{"class":197,"line":1274},[195,5537,5538],{},"    init(repository: UserRepository) {\n",[195,5540,5541],{"class":197,"line":1282},[195,5542,5543],{},"        self.repository = repository\n",[195,5545,5546],{"class":197,"line":1295},[195,5547,2403],{},[195,5549,5550],{"class":197,"line":1309},[195,5551,1241],{"emptyLinePlaceholder":757},[195,5553,5554],{"class":197,"line":2246},[195,5555,5103],{},[195,5557,5558],{"class":197,"line":1996},[195,5559,5560],{},"        isLoading = true\n",[195,5562,5563],{"class":197,"line":2257},[195,5564,5565],{},"        let user = try? await repository.getUser()\n",[195,5567,5568],{"class":197,"line":2262},[195,5569,5570],{},"        displayName = user?.name ?? \"Unknown\"\n",[195,5572,5573],{"class":197,"line":2267},[195,5574,5575],{},"        isLoading = false\n",[195,5577,5578],{"class":197,"line":2273},[195,5579,2403],{},[195,5581,5582],{"class":197,"line":2033},[195,5583,552],{},[195,5585,5586],{"class":197,"line":2284},[195,5587,1241],{"emptyLinePlaceholder":757},[195,5589,5590],{"class":197,"line":2460},[195,5591,5592],{},"\u002F\u002F View：只负责展示\n",[195,5594,5595],{"class":197,"line":2466},[195,5596,5597],{},"struct UserView: View {\n",[195,5599,5600],{"class":197,"line":2472},[195,5601,5602],{},"    @StateObject var viewModel: UserViewModel\n",[195,5604,5605],{"class":197,"line":2780},[195,5606,1241],{"emptyLinePlaceholder":757},[195,5608,5609],{"class":197,"line":2786},[195,5610,3985],{},[195,5612,5613],{"class":197,"line":2792},[195,5614,5615],{},"        if viewModel.isLoading {\n",[195,5617,5618],{"class":197,"line":2798},[195,5619,5620],{},"            ProgressView()\n",[195,5622,5623],{"class":197,"line":2804},[195,5624,5625],{},"        } else {\n",[195,5627,5628],{"class":197,"line":2810},[195,5629,5630],{},"            Text(viewModel.displayName)\n",[195,5632,5633],{"class":197,"line":2815},[195,5634,2887],{},[195,5636,5637],{"class":197,"line":2820},[195,5638,2403],{},[195,5640,5641],{"class":197,"line":2825},[195,5642,552],{},[14,5644,5645],{},[125,5646,5647],{},"TCA（The Composable Architecture）：",[186,5649,5651],{"className":2177,"code":5650,"language":2179,"meta":191,"style":191},"\u002F\u002F 单向数据流，强调可测试性和可组合性\n@Reducer\nstruct Counter {\n    @ObservableState\n    struct State: Equatable {\n        var count = 0\n    }\n\n    enum Action {\n        case increment\n        case decrement\n        case fetchCompleted(Int)\n    }\n\n    var body: some ReducerOf\u003CSelf> {\n        Reduce { state, action in\n            switch action {\n            case .increment:\n                state.count += 1\n                return .none\n            case .decrement:\n                state.count -= 1\n                return .none\n            case .fetchCompleted(let value):\n                state.count = value\n                return .none\n            }\n        }\n    }\n}\n\n\u002F\u002F View\nstruct CounterView: View {\n    let store: StoreOf\u003CCounter>\n\n    var body: some View {\n        HStack {\n            Button(\"-\") { store.send(.decrement) }\n            Text(\"\\(store.count)\")\n            Button(\"+\") { store.send(.increment) }\n        }\n    }\n}\n",[136,5652,5653,5658,5663,5668,5673,5678,5683,5687,5691,5696,5701,5706,5711,5715,5719,5724,5729,5734,5739,5744,5749,5754,5759,5763,5768,5773,5777,5782,5786,5790,5794,5798,5803,5807,5812,5816,5820,5825,5830,5835,5840,5844,5848],{"__ignoreMap":191},[195,5654,5655],{"class":197,"line":198},[195,5656,5657],{},"\u002F\u002F 单向数据流，强调可测试性和可组合性\n",[195,5659,5660],{"class":197,"line":230},[195,5661,5662],{},"@Reducer\n",[195,5664,5665],{"class":197,"line":251},[195,5666,5667],{},"struct Counter {\n",[195,5669,5670],{"class":197,"line":272},[195,5671,5672],{},"    @ObservableState\n",[195,5674,5675],{"class":197,"line":293},[195,5676,5677],{},"    struct State: Equatable {\n",[195,5679,5680],{"class":197,"line":562},[195,5681,5682],{},"        var count = 0\n",[195,5684,5685],{"class":197,"line":583},[195,5686,2403],{},[195,5688,5689],{"class":197,"line":962},[195,5690,1241],{"emptyLinePlaceholder":757},[195,5692,5693],{"class":197,"line":968},[195,5694,5695],{},"    enum Action {\n",[195,5697,5698],{"class":197,"line":1274},[195,5699,5700],{},"        case increment\n",[195,5702,5703],{"class":197,"line":1282},[195,5704,5705],{},"        case decrement\n",[195,5707,5708],{"class":197,"line":1295},[195,5709,5710],{},"        case fetchCompleted(Int)\n",[195,5712,5713],{"class":197,"line":1309},[195,5714,2403],{},[195,5716,5717],{"class":197,"line":2246},[195,5718,1241],{"emptyLinePlaceholder":757},[195,5720,5721],{"class":197,"line":1996},[195,5722,5723],{},"    var body: some ReducerOf\u003CSelf> {\n",[195,5725,5726],{"class":197,"line":2257},[195,5727,5728],{},"        Reduce { state, action in\n",[195,5730,5731],{"class":197,"line":2262},[195,5732,5733],{},"            switch action {\n",[195,5735,5736],{"class":197,"line":2267},[195,5737,5738],{},"            case .increment:\n",[195,5740,5741],{"class":197,"line":2273},[195,5742,5743],{},"                state.count += 1\n",[195,5745,5746],{"class":197,"line":2033},[195,5747,5748],{},"                return .none\n",[195,5750,5751],{"class":197,"line":2284},[195,5752,5753],{},"            case .decrement:\n",[195,5755,5756],{"class":197,"line":2460},[195,5757,5758],{},"                state.count -= 1\n",[195,5760,5761],{"class":197,"line":2466},[195,5762,5748],{},[195,5764,5765],{"class":197,"line":2472},[195,5766,5767],{},"            case .fetchCompleted(let value):\n",[195,5769,5770],{"class":197,"line":2780},[195,5771,5772],{},"                state.count = value\n",[195,5774,5775],{"class":197,"line":2786},[195,5776,5748],{},[195,5778,5779],{"class":197,"line":2792},[195,5780,5781],{},"            }\n",[195,5783,5784],{"class":197,"line":2798},[195,5785,2887],{},[195,5787,5788],{"class":197,"line":2804},[195,5789,2403],{},[195,5791,5792],{"class":197,"line":2810},[195,5793,552],{},[195,5795,5796],{"class":197,"line":2815},[195,5797,1241],{"emptyLinePlaceholder":757},[195,5799,5800],{"class":197,"line":2820},[195,5801,5802],{},"\u002F\u002F View\n",[195,5804,5805],{"class":197,"line":2825},[195,5806,3971],{},[195,5808,5809],{"class":197,"line":2831},[195,5810,5811],{},"    let store: StoreOf\u003CCounter>\n",[195,5813,5814],{"class":197,"line":2837},[195,5815,1241],{"emptyLinePlaceholder":757},[195,5817,5818],{"class":197,"line":2843},[195,5819,3985],{},[195,5821,5822],{"class":197,"line":2848},[195,5823,5824],{},"        HStack {\n",[195,5826,5827],{"class":197,"line":2854},[195,5828,5829],{},"            Button(\"-\") { store.send(.decrement) }\n",[195,5831,5832],{"class":197,"line":2860},[195,5833,5834],{},"            Text(\"\\(store.count)\")\n",[195,5836,5837],{"class":197,"line":2866},[195,5838,5839],{},"            Button(\"+\") { store.send(.increment) }\n",[195,5841,5842],{"class":197,"line":2872},[195,5843,2887],{},[195,5845,5846],{"class":197,"line":2878},[195,5847,2403],{},[195,5849,5850],{"class":197,"line":2884},[195,5851,552],{},[36,5853,5854,5873],{},[39,5855,5856],{},[42,5857,5858,5861,5864,5867,5870],{},[45,5859,5860],{},"架构",[45,5862,5863],{},"复杂度",[45,5865,5866],{},"可测试性",[45,5868,5869],{},"适用规模",[45,5871,5872],{},"学习成本",[52,5874,5875,5891,5907,5923],{},[42,5876,5877,5880,5883,5886,5889],{},[57,5878,5879],{},"MVC",[57,5881,5882],{},"低",[57,5884,5885],{},"差",[57,5887,5888],{},"小型\u002F原型",[57,5890,5882],{},[42,5892,5893,5896,5899,5902,5905],{},[57,5894,5895],{},"MVVM",[57,5897,5898],{},"中",[57,5900,5901],{},"好",[57,5903,5904],{},"中型",[57,5906,5882],{},[42,5908,5909,5912,5915,5918,5921],{},[57,5910,5911],{},"VIPER",[57,5913,5914],{},"高",[57,5916,5917],{},"很好",[57,5919,5920],{},"大型团队",[57,5922,5914],{},[42,5924,5925,5928,5931,5934,5937],{},[57,5926,5927],{},"TCA",[57,5929,5930],{},"中-高",[57,5932,5933],{},"极好",[57,5935,5936],{},"中大型",[57,5938,5930],{},[1890,5940],{},[18,5942,5944],{"id":5943},"_6-性能优化与工具ios","6. 性能优化与工具（iOS）",[32,5946,5948],{"id":5947},"q61-如何使用-instruments-诊断-ios-性能问题","Q6.1: 如何使用 Instruments 诊断 iOS 性能问题？",[14,5950,5951],{},[125,5952,2077],{},[36,5954,5955,5967],{},[39,5956,5957],{},[42,5958,5959,5962,5964],{},[45,5960,5961],{},"Instrument",[45,5963,3111],{},[45,5965,5966],{},"关注指标",[52,5968,5969,5980,5991,6002,6013,6024],{},[42,5970,5971,5974,5977],{},[57,5972,5973],{},"Time Profiler",[57,5975,5976],{},"CPU 耗时分析",[57,5978,5979],{},"主线程占用、热点函数",[42,5981,5982,5985,5988],{},[57,5983,5984],{},"Allocations",[57,5986,5987],{},"内存分配跟踪",[57,5989,5990],{},"总内存、分配频率、泄漏",[42,5992,5993,5996,5999],{},[57,5994,5995],{},"Leaks",[57,5997,5998],{},"内存泄漏检测",[57,6000,6001],{},"泄漏对象、循环引用",[42,6003,6004,6007,6010],{},[57,6005,6006],{},"Core Animation",[57,6008,6009],{},"渲染性能",[57,6011,6012],{},"FPS、离屏渲染、混合图层",[42,6014,6015,6018,6021],{},[57,6016,6017],{},"Network",[57,6019,6020],{},"网络请求分析",[57,6022,6023],{},"请求数、响应时间、数据量",[42,6025,6026,6029,6032],{},[57,6027,6028],{},"Energy Log",[57,6030,6031],{},"电池消耗分析",[57,6033,6034],{},"CPU\u002FGPU\u002F网络唤醒、后台活动",[14,6036,6037],{},[125,6038,6039],{},"常见性能问题与解决方案：",[186,6041,6043],{"className":2177,"code":6042,"language":2179,"meta":191,"style":191},"\u002F\u002F 1. 主线程阻塞\n\u002F\u002F ❌ 主线程做 IO\u002F计算\nlet data = try! Data(contentsOf: largeFileURL) \u002F\u002F 主线程阻塞\n\n\u002F\u002F ✅ 移到后台\nTask.detached {\n    let data = try await loadData(from: largeFileURL)\n    await MainActor.run { self.process(data) }\n}\n\n\u002F\u002F 2. 离屏渲染\n\u002F\u002F ❌ 触发离屏渲染\nview.layer.cornerRadius = 10\nview.layer.masksToBounds = true  \u002F\u002F 配合圆角 → 离屏渲染\nview.layer.shadowOffset = CGSize(width: 0, height: 2)  \u002F\u002F 阴影 → 离屏渲染\n\n\u002F\u002F ✅ 优化\nview.layer.cornerRadius = 10\nview.layer.cornerCurve = .continuous\n\u002F\u002F 阴影加 path 避免离屏渲染\nview.layer.shadowPath = UIBezierPath(roundedRect: view.bounds, cornerRadius: 10).cgPath\n\n\u002F\u002F 3. 图片解码\n\u002F\u002F ❌ 加载原图（4000x3000 解码消耗大量内存）\nimageView.image = UIImage(named: \"huge_photo\")\n\n\u002F\u002F ✅ 降采样（Downsampling）\nfunc downsample(imageAt url: URL, to pointSize: CGSize, scale: CGFloat) -> UIImage? {\n    let options = [kCGImageSourceShouldCache: false] as CFDictionary\n    guard let source = CGImageSourceCreateWithURL(url as CFURL, options) else { return nil }\n\n    let maxDimension = max(pointSize.width, pointSize.height) * scale\n    let downsampleOptions = [\n        kCGImageSourceCreateThumbnailFromImageAlways: true,\n        kCGImageSourceShouldCacheImmediately: true,\n        kCGImageSourceThumbnailMaxPixelSize: maxDimension\n    ] as CFDictionary\n\n    guard let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, downsampleOptions) else { return nil }\n    return UIImage(cgImage: cgImage)\n}\n",[136,6044,6045,6050,6055,6060,6064,6069,6074,6079,6084,6088,6092,6097,6102,6107,6112,6117,6121,6126,6130,6135,6140,6145,6149,6154,6159,6164,6168,6173,6178,6183,6188,6192,6197,6202,6207,6212,6217,6222,6226,6231,6236],{"__ignoreMap":191},[195,6046,6047],{"class":197,"line":198},[195,6048,6049],{},"\u002F\u002F 1. 主线程阻塞\n",[195,6051,6052],{"class":197,"line":230},[195,6053,6054],{},"\u002F\u002F ❌ 主线程做 IO\u002F计算\n",[195,6056,6057],{"class":197,"line":251},[195,6058,6059],{},"let data = try! Data(contentsOf: largeFileURL) \u002F\u002F 主线程阻塞\n",[195,6061,6062],{"class":197,"line":272},[195,6063,1241],{"emptyLinePlaceholder":757},[195,6065,6066],{"class":197,"line":293},[195,6067,6068],{},"\u002F\u002F ✅ 移到后台\n",[195,6070,6071],{"class":197,"line":562},[195,6072,6073],{},"Task.detached {\n",[195,6075,6076],{"class":197,"line":583},[195,6077,6078],{},"    let data = try await loadData(from: largeFileURL)\n",[195,6080,6081],{"class":197,"line":962},[195,6082,6083],{},"    await MainActor.run { self.process(data) }\n",[195,6085,6086],{"class":197,"line":968},[195,6087,552],{},[195,6089,6090],{"class":197,"line":1274},[195,6091,1241],{"emptyLinePlaceholder":757},[195,6093,6094],{"class":197,"line":1282},[195,6095,6096],{},"\u002F\u002F 2. 离屏渲染\n",[195,6098,6099],{"class":197,"line":1295},[195,6100,6101],{},"\u002F\u002F ❌ 触发离屏渲染\n",[195,6103,6104],{"class":197,"line":1309},[195,6105,6106],{},"view.layer.cornerRadius = 10\n",[195,6108,6109],{"class":197,"line":2246},[195,6110,6111],{},"view.layer.masksToBounds = true  \u002F\u002F 配合圆角 → 离屏渲染\n",[195,6113,6114],{"class":197,"line":1996},[195,6115,6116],{},"view.layer.shadowOffset = CGSize(width: 0, height: 2)  \u002F\u002F 阴影 → 离屏渲染\n",[195,6118,6119],{"class":197,"line":2257},[195,6120,1241],{"emptyLinePlaceholder":757},[195,6122,6123],{"class":197,"line":2262},[195,6124,6125],{},"\u002F\u002F ✅ 优化\n",[195,6127,6128],{"class":197,"line":2267},[195,6129,6106],{},[195,6131,6132],{"class":197,"line":2273},[195,6133,6134],{},"view.layer.cornerCurve = .continuous\n",[195,6136,6137],{"class":197,"line":2033},[195,6138,6139],{},"\u002F\u002F 阴影加 path 避免离屏渲染\n",[195,6141,6142],{"class":197,"line":2284},[195,6143,6144],{},"view.layer.shadowPath = UIBezierPath(roundedRect: view.bounds, cornerRadius: 10).cgPath\n",[195,6146,6147],{"class":197,"line":2460},[195,6148,1241],{"emptyLinePlaceholder":757},[195,6150,6151],{"class":197,"line":2466},[195,6152,6153],{},"\u002F\u002F 3. 图片解码\n",[195,6155,6156],{"class":197,"line":2472},[195,6157,6158],{},"\u002F\u002F ❌ 加载原图（4000x3000 解码消耗大量内存）\n",[195,6160,6161],{"class":197,"line":2780},[195,6162,6163],{},"imageView.image = UIImage(named: \"huge_photo\")\n",[195,6165,6166],{"class":197,"line":2786},[195,6167,1241],{"emptyLinePlaceholder":757},[195,6169,6170],{"class":197,"line":2792},[195,6171,6172],{},"\u002F\u002F ✅ 降采样（Downsampling）\n",[195,6174,6175],{"class":197,"line":2798},[195,6176,6177],{},"func downsample(imageAt url: URL, to pointSize: CGSize, scale: CGFloat) -> UIImage? {\n",[195,6179,6180],{"class":197,"line":2804},[195,6181,6182],{},"    let options = [kCGImageSourceShouldCache: false] as CFDictionary\n",[195,6184,6185],{"class":197,"line":2810},[195,6186,6187],{},"    guard let source = CGImageSourceCreateWithURL(url as CFURL, options) else { return nil }\n",[195,6189,6190],{"class":197,"line":2815},[195,6191,1241],{"emptyLinePlaceholder":757},[195,6193,6194],{"class":197,"line":2820},[195,6195,6196],{},"    let maxDimension = max(pointSize.width, pointSize.height) * scale\n",[195,6198,6199],{"class":197,"line":2825},[195,6200,6201],{},"    let downsampleOptions = [\n",[195,6203,6204],{"class":197,"line":2831},[195,6205,6206],{},"        kCGImageSourceCreateThumbnailFromImageAlways: true,\n",[195,6208,6209],{"class":197,"line":2837},[195,6210,6211],{},"        kCGImageSourceShouldCacheImmediately: true,\n",[195,6213,6214],{"class":197,"line":2843},[195,6215,6216],{},"        kCGImageSourceThumbnailMaxPixelSize: maxDimension\n",[195,6218,6219],{"class":197,"line":2848},[195,6220,6221],{},"    ] as CFDictionary\n",[195,6223,6224],{"class":197,"line":2854},[195,6225,1241],{"emptyLinePlaceholder":757},[195,6227,6228],{"class":197,"line":2860},[195,6229,6230],{},"    guard let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, downsampleOptions) else { return nil }\n",[195,6232,6233],{"class":197,"line":2866},[195,6234,6235],{},"    return UIImage(cgImage: cgImage)\n",[195,6237,6238],{"class":197,"line":2872},[195,6239,552],{},[1890,6241],{},[18,6243,6245],{"id":6244},"_7-系统机制与框架","7. 系统机制与框架",[32,6247,6249],{"id":6248},"q71-ios-app-的启动流程是怎样的如何优化启动时间","Q7.1: iOS App 的启动流程是怎样的？如何优化启动时间？",[14,6251,6252],{},[125,6253,2077],{},[186,6255,6258],{"className":6256,"code":6257,"language":1074},[1072],"┌─────────────────────────────────────────────┐\n│  Pre-main（系统阶段）                        │\n│  1. 加载 dyld（动态链接器）                   │\n│  2. 加载动态库（系统 + 第三方）                │\n│  3. Rebase & Bind（指针修正）                 │\n│  4. ObjC Runtime Setup（类注册、category 加载）│\n│  5. +load 方法                               │\n│  6. C++ 静态初始化器                          │\n├─────────────────────────────────────────────┤\n│  Post-main（应用阶段）                       │\n│  7. main() → UIApplicationMain()            │\n│  8. application:didFinishLaunchingWithOptions│\n│  9. 首帧渲染完成                              │\n└─────────────────────────────────────────────┘\n",[136,6259,6257],{"__ignoreMap":191},[14,6261,6262],{},[125,6263,6264],{},"优化策略：",[36,6266,6267,6277],{},[39,6268,6269],{},[42,6270,6271,6274],{},[45,6272,6273],{},"阶段",[45,6275,6276],{},"优化方法",[52,6278,6279,6295],{},[42,6280,6281,6284],{},[57,6282,6283],{},"Pre-main",[57,6285,6286,6287,6290,6291,6294],{},"减少动态库数量（合并或改用静态库）、移除不用的 ObjC 类、避免 ",[136,6288,6289],{},"+load","（改用 ",[136,6292,6293],{},"+initialize","）、减少 C++ 静态初始化",[42,6296,6297,6300],{},[57,6298,6299],{},"Post-main",[57,6301,6302,6305],{},[136,6303,6304],{},"didFinishLaunching"," 中延迟非必要初始化、首屏只加载必要数据、懒加载 SDK、使用 Launch Storyboard 提升感知速度",[186,6307,6309],{"className":2177,"code":6308,"language":2179,"meta":191,"style":191},"\u002F\u002F 测量启动时间\n\u002F\u002F Scheme → Environment Variables → 添加 DYLD_PRINT_STATISTICS = 1\n\n\u002F\u002F 延迟初始化示例\nfunc application(_ application: UIApplication,\n                 didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {\n    \u002F\u002F 只做必要初始化\n    setupCrashReporting()\n    setupCoreData()\n\n    \u002F\u002F 延迟非关键初始化\n    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {\n        self.setupAnalytics()\n        self.setupPushNotifications()\n    }\n\n    return true\n}\n",[136,6310,6311,6316,6321,6325,6330,6335,6340,6345,6350,6355,6359,6364,6369,6374,6379,6383,6387,6392],{"__ignoreMap":191},[195,6312,6313],{"class":197,"line":198},[195,6314,6315],{},"\u002F\u002F 测量启动时间\n",[195,6317,6318],{"class":197,"line":230},[195,6319,6320],{},"\u002F\u002F Scheme → Environment Variables → 添加 DYLD_PRINT_STATISTICS = 1\n",[195,6322,6323],{"class":197,"line":251},[195,6324,1241],{"emptyLinePlaceholder":757},[195,6326,6327],{"class":197,"line":272},[195,6328,6329],{},"\u002F\u002F 延迟初始化示例\n",[195,6331,6332],{"class":197,"line":293},[195,6333,6334],{},"func application(_ application: UIApplication,\n",[195,6336,6337],{"class":197,"line":562},[195,6338,6339],{},"                 didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {\n",[195,6341,6342],{"class":197,"line":583},[195,6343,6344],{},"    \u002F\u002F 只做必要初始化\n",[195,6346,6347],{"class":197,"line":962},[195,6348,6349],{},"    setupCrashReporting()\n",[195,6351,6352],{"class":197,"line":968},[195,6353,6354],{},"    setupCoreData()\n",[195,6356,6357],{"class":197,"line":1274},[195,6358,1241],{"emptyLinePlaceholder":757},[195,6360,6361],{"class":197,"line":1282},[195,6362,6363],{},"    \u002F\u002F 延迟非关键初始化\n",[195,6365,6366],{"class":197,"line":1295},[195,6367,6368],{},"    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {\n",[195,6370,6371],{"class":197,"line":1309},[195,6372,6373],{},"        self.setupAnalytics()\n",[195,6375,6376],{"class":197,"line":2246},[195,6377,6378],{},"        self.setupPushNotifications()\n",[195,6380,6381],{"class":197,"line":1996},[195,6382,2403],{},[195,6384,6385],{"class":197,"line":2257},[195,6386,1241],{"emptyLinePlaceholder":757},[195,6388,6389],{"class":197,"line":2262},[195,6390,6391],{},"    return true\n",[195,6393,6394],{"class":197,"line":2267},[195,6395,552],{},[1890,6397],{},[32,6399,6401],{"id":6400},"q72-什么是-runloop它和-ios-事件处理的关系","Q7.2: 什么是 RunLoop？它和 iOS 事件处理的关系？",[14,6403,6404],{},[125,6405,2077],{},[14,6407,6408],{},"RunLoop 是线程上的事件处理循环，让线程在没有事件时休眠，有事件时唤醒处理：",[186,6410,6413],{"className":6411,"code":6412,"language":1074},[1072],"┌──────────────────────────────────────┐\n│              RunLoop 循环             │\n│                                      │\n│   1. 处理 Source0（非端口事件）         │\n│      ↓                               │\n│   2. 处理 Source1（端口\u002F系统事件）      │\n│      ↓                               │\n│   3. 处理 Timer                       │\n│      ↓                               │\n│   4. 处理 Observer 通知               │\n│      ↓                               │\n│   5. 处理 GCD dispatch 到主队列的 block│\n│      ↓                               │\n│   6. 没有事件 → 线程休眠              │\n│      ↓                               │\n│   7. 有事件 → 唤醒，回到 1            │\n└──────────────────────────────────────┘\n",[136,6414,6412],{"__ignoreMap":191},[14,6416,6417],{},[125,6418,6419],{},"RunLoop Mode：",[36,6421,6422,6431],{},[39,6423,6424],{},[42,6425,6426,6429],{},[45,6427,6428],{},"Mode",[45,6430,3111],{},[52,6432,6433,6443,6453],{},[42,6434,6435,6440],{},[57,6436,6437],{},[136,6438,6439],{},"default",[57,6441,6442],{},"默认模式，大部分事件",[42,6444,6445,6450],{},[57,6446,6447],{},[136,6448,6449],{},"tracking",[57,6451,6452],{},"ScrollView 滚动时的模式",[42,6454,6455,6460],{},[57,6456,6457],{},[136,6458,6459],{},"common",[57,6461,6462],{},"default + tracking 的集合",[186,6464,6466],{"className":2177,"code":6465,"language":2179,"meta":191,"style":191},"\u002F\u002F 常见问题：Timer 在滚动时停止\n\u002F\u002F ❌ 默认 mode，滚动时 RunLoop 切换到 tracking mode，Timer 不触发\nlet timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in\n    updateUI()\n}\n\n\u002F\u002F ✅ 添加到 common mode\nRunLoop.current.add(timer, forMode: .common)\n",[136,6467,6468,6473,6478,6483,6488,6492,6496,6501],{"__ignoreMap":191},[195,6469,6470],{"class":197,"line":198},[195,6471,6472],{},"\u002F\u002F 常见问题：Timer 在滚动时停止\n",[195,6474,6475],{"class":197,"line":230},[195,6476,6477],{},"\u002F\u002F ❌ 默认 mode，滚动时 RunLoop 切换到 tracking mode，Timer 不触发\n",[195,6479,6480],{"class":197,"line":251},[195,6481,6482],{},"let timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in\n",[195,6484,6485],{"class":197,"line":272},[195,6486,6487],{},"    updateUI()\n",[195,6489,6490],{"class":197,"line":293},[195,6491,552],{},[195,6493,6494],{"class":197,"line":562},[195,6495,1241],{"emptyLinePlaceholder":757},[195,6497,6498],{"class":197,"line":583},[195,6499,6500],{},"\u002F\u002F ✅ 添加到 common mode\n",[195,6502,6503],{"class":197,"line":962},[195,6504,6505],{},"RunLoop.current.add(timer, forMode: .common)\n",[14,6507,6508],{},[125,6509,6510],{},"RunLoop 的应用场景：",[129,6512,6513,6516,6519,6522],{},[132,6514,6515],{},"AutoreleasePool 的 drain 时机（每次 RunLoop 迭代结束）",[132,6517,6518],{},"手势识别",[132,6520,6521],{},"屏幕刷新（CADisplayLink）",[132,6523,6524],{},"延迟加载（在 RunLoop 空闲时加载图片）",[1890,6526],{},[32,6528,6530],{"id":6529},"q73-解释-ios-中的-app-lifecycle前后台切换和-scene-生命周期","Q7.3: 解释 iOS 中的 App Lifecycle（前后台切换）和 Scene 生命周期",[14,6532,6533],{},[125,6534,2077],{},[186,6536,6539],{"className":6537,"code":6538,"language":1074},[1072],"Not Running → Inactive → Active → (用户使用中)\n                 ↕\n              Background → Suspended → (系统可能终止)\n",[136,6540,6538],{"__ignoreMap":191},[186,6542,6544],{"className":2177,"code":6543,"language":2179,"meta":191,"style":191},"\u002F\u002F iOS 13+ Scene-based lifecycle\nclass SceneDelegate: UIResponder, UIWindowSceneDelegate {\n    func sceneDidBecomeActive(_ scene: UIScene) {\n        \u002F\u002F 前台活跃：恢复暂停的任务、刷新 UI\n    }\n\n    func sceneWillResignActive(_ scene: UIScene) {\n        \u002F\u002F 即将失去焦点：暂停游戏、保存草稿\n    }\n\n    func sceneDidEnterBackground(_ scene: UIScene) {\n        \u002F\u002F 进入后台：保存数据、释放共享资源\n        \u002F\u002F 约 5 秒执行时间\n    }\n\n    func sceneWillEnterForeground(_ scene: UIScene) {\n        \u002F\u002F 即将回到前台：撤销后台的更改\n    }\n}\n\n\u002F\u002F SwiftUI\n@main\nstruct MyApp: App {\n    @Environment(\\.scenePhase) var scenePhase\n\n    var body: some Scene {\n        WindowGroup { ContentView() }\n            .onChange(of: scenePhase) { _, phase in\n                switch phase {\n                case .active: break      \u002F\u002F 前台活跃\n                case .inactive: break    \u002F\u002F 临时不活跃\n                case .background: break  \u002F\u002F 后台\n                @unknown default: break\n                }\n            }\n    }\n}\n",[136,6545,6546,6551,6556,6561,6566,6570,6574,6579,6584,6588,6592,6597,6602,6607,6611,6615,6620,6625,6629,6633,6637,6642,6647,6652,6657,6661,6666,6671,6676,6681,6686,6691,6696,6701,6706,6710,6714],{"__ignoreMap":191},[195,6547,6548],{"class":197,"line":198},[195,6549,6550],{},"\u002F\u002F iOS 13+ Scene-based lifecycle\n",[195,6552,6553],{"class":197,"line":230},[195,6554,6555],{},"class SceneDelegate: UIResponder, UIWindowSceneDelegate {\n",[195,6557,6558],{"class":197,"line":251},[195,6559,6560],{},"    func sceneDidBecomeActive(_ scene: UIScene) {\n",[195,6562,6563],{"class":197,"line":272},[195,6564,6565],{},"        \u002F\u002F 前台活跃：恢复暂停的任务、刷新 UI\n",[195,6567,6568],{"class":197,"line":293},[195,6569,2403],{},[195,6571,6572],{"class":197,"line":562},[195,6573,1241],{"emptyLinePlaceholder":757},[195,6575,6576],{"class":197,"line":583},[195,6577,6578],{},"    func sceneWillResignActive(_ scene: UIScene) {\n",[195,6580,6581],{"class":197,"line":962},[195,6582,6583],{},"        \u002F\u002F 即将失去焦点：暂停游戏、保存草稿\n",[195,6585,6586],{"class":197,"line":968},[195,6587,2403],{},[195,6589,6590],{"class":197,"line":1274},[195,6591,1241],{"emptyLinePlaceholder":757},[195,6593,6594],{"class":197,"line":1282},[195,6595,6596],{},"    func sceneDidEnterBackground(_ scene: UIScene) {\n",[195,6598,6599],{"class":197,"line":1295},[195,6600,6601],{},"        \u002F\u002F 进入后台：保存数据、释放共享资源\n",[195,6603,6604],{"class":197,"line":1309},[195,6605,6606],{},"        \u002F\u002F 约 5 秒执行时间\n",[195,6608,6609],{"class":197,"line":2246},[195,6610,2403],{},[195,6612,6613],{"class":197,"line":1996},[195,6614,1241],{"emptyLinePlaceholder":757},[195,6616,6617],{"class":197,"line":2257},[195,6618,6619],{},"    func sceneWillEnterForeground(_ scene: UIScene) {\n",[195,6621,6622],{"class":197,"line":2262},[195,6623,6624],{},"        \u002F\u002F 即将回到前台：撤销后台的更改\n",[195,6626,6627],{"class":197,"line":2267},[195,6628,2403],{},[195,6630,6631],{"class":197,"line":2273},[195,6632,552],{},[195,6634,6635],{"class":197,"line":2033},[195,6636,1241],{"emptyLinePlaceholder":757},[195,6638,6639],{"class":197,"line":2284},[195,6640,6641],{},"\u002F\u002F SwiftUI\n",[195,6643,6644],{"class":197,"line":2460},[195,6645,6646],{},"@main\n",[195,6648,6649],{"class":197,"line":2466},[195,6650,6651],{},"struct MyApp: App {\n",[195,6653,6654],{"class":197,"line":2472},[195,6655,6656],{},"    @Environment(\\.scenePhase) var scenePhase\n",[195,6658,6659],{"class":197,"line":2780},[195,6660,1241],{"emptyLinePlaceholder":757},[195,6662,6663],{"class":197,"line":2786},[195,6664,6665],{},"    var body: some Scene {\n",[195,6667,6668],{"class":197,"line":2792},[195,6669,6670],{},"        WindowGroup { ContentView() }\n",[195,6672,6673],{"class":197,"line":2798},[195,6674,6675],{},"            .onChange(of: scenePhase) { _, phase in\n",[195,6677,6678],{"class":197,"line":2804},[195,6679,6680],{},"                switch phase {\n",[195,6682,6683],{"class":197,"line":2810},[195,6684,6685],{},"                case .active: break      \u002F\u002F 前台活跃\n",[195,6687,6688],{"class":197,"line":2815},[195,6689,6690],{},"                case .inactive: break    \u002F\u002F 临时不活跃\n",[195,6692,6693],{"class":197,"line":2820},[195,6694,6695],{},"                case .background: break  \u002F\u002F 后台\n",[195,6697,6698],{"class":197,"line":2825},[195,6699,6700],{},"                @unknown default: break\n",[195,6702,6703],{"class":197,"line":2831},[195,6704,6705],{},"                }\n",[195,6707,6708],{"class":197,"line":2837},[195,6709,5781],{},[195,6711,6712],{"class":197,"line":2843},[195,6713,2403],{},[195,6715,6716],{"class":197,"line":2848},[195,6717,552],{},[14,6719,6720],{},[125,6721,6722],{},"后台任务：",[186,6724,6726],{"className":2177,"code":6725,"language":2179,"meta":191,"style":191},"\u002F\u002F Background Task：申请额外执行时间\nfunc sceneDidEnterBackground(_ scene: UIScene) {\n    let taskID = UIApplication.shared.beginBackgroundTask {\n        \u002F\u002F 时间到了的清理回调\n        UIApplication.shared.endBackgroundTask(taskID)\n    }\n\n    Task {\n        await saveData()\n        UIApplication.shared.endBackgroundTask(taskID)\n    }\n}\n\n\u002F\u002F BGTaskScheduler（iOS 13+）：后台定时任务\nBGTaskScheduler.shared.register(forTaskWithIdentifier: \"com.app.refresh\", using: nil) { task in\n    handleBackgroundRefresh(task: task as! BGAppRefreshTask)\n}\n",[136,6727,6728,6733,6738,6743,6748,6753,6757,6761,6766,6771,6775,6779,6783,6787,6792,6797,6802],{"__ignoreMap":191},[195,6729,6730],{"class":197,"line":198},[195,6731,6732],{},"\u002F\u002F Background Task：申请额外执行时间\n",[195,6734,6735],{"class":197,"line":230},[195,6736,6737],{},"func sceneDidEnterBackground(_ scene: UIScene) {\n",[195,6739,6740],{"class":197,"line":251},[195,6741,6742],{},"    let taskID = UIApplication.shared.beginBackgroundTask {\n",[195,6744,6745],{"class":197,"line":272},[195,6746,6747],{},"        \u002F\u002F 时间到了的清理回调\n",[195,6749,6750],{"class":197,"line":293},[195,6751,6752],{},"        UIApplication.shared.endBackgroundTask(taskID)\n",[195,6754,6755],{"class":197,"line":562},[195,6756,2403],{},[195,6758,6759],{"class":197,"line":583},[195,6760,1241],{"emptyLinePlaceholder":757},[195,6762,6763],{"class":197,"line":962},[195,6764,6765],{},"    Task {\n",[195,6767,6768],{"class":197,"line":968},[195,6769,6770],{},"        await saveData()\n",[195,6772,6773],{"class":197,"line":1274},[195,6774,6752],{},[195,6776,6777],{"class":197,"line":1282},[195,6778,2403],{},[195,6780,6781],{"class":197,"line":1295},[195,6782,552],{},[195,6784,6785],{"class":197,"line":1309},[195,6786,1241],{"emptyLinePlaceholder":757},[195,6788,6789],{"class":197,"line":2246},[195,6790,6791],{},"\u002F\u002F BGTaskScheduler（iOS 13+）：后台定时任务\n",[195,6793,6794],{"class":197,"line":1996},[195,6795,6796],{},"BGTaskScheduler.shared.register(forTaskWithIdentifier: \"com.app.refresh\", using: nil) { task in\n",[195,6798,6799],{"class":197,"line":2257},[195,6800,6801],{},"    handleBackgroundRefresh(task: task as! BGAppRefreshTask)\n",[195,6803,6804],{"class":197,"line":2262},[195,6805,552],{},[1890,6807],{},[2055,6809,1947],{"id":6810},"第二部分android-开发-1",[18,6812,6814],{"id":6813},"_8-kotlin-语言基础","8. Kotlin 语言基础",[32,6816,6818,6819,997,6822,997,6825,997,6828,6831],{"id":6817},"q81-kotlin-中的-data-classsealed-classobjectcompanion-object-各有什么用","Q8.1: Kotlin 中的 ",[136,6820,6821],{},"data class",[136,6823,6824],{},"sealed class",[136,6826,6827],{},"object",[136,6829,6830],{},"companion object"," 各有什么用？",[14,6833,6834],{},[125,6835,2077],{},[186,6837,6841],{"className":6838,"code":6839,"language":6840,"meta":191,"style":191},"language-kotlin shiki shiki-themes github-light github-dark","\u002F\u002F 1. data class：自动生成 equals\u002FhashCode\u002FtoString\u002Fcopy\u002FcomponentN\ndata class User(val name: String, val age: Int)\n\nval user1 = User(\"Alice\", 25)\nval user2 = user1.copy(age = 26)       \u002F\u002F 浅拷贝并修改\nval (name, age) = user1                 \u002F\u002F 解构\nprintln(user1)                          \u002F\u002F User(name=Alice, age=25)\nprintln(user1 == User(\"Alice\", 25))     \u002F\u002F true（结构相等）\n\n\u002F\u002F 2. sealed class：受限的继承层次，编译器可穷举\nsealed class Result\u003Cout T> {\n    data class Success\u003CT>(val data: T) : Result\u003CT>()\n    data class Error(val message: String) : Result\u003CNothing>()\n    data object Loading : Result\u003CNothing>()\n}\n\nfun handle(result: Result\u003CString>) = when (result) {\n    is Result.Success -> println(result.data)\n    is Result.Error -> println(result.message)\n    Result.Loading -> println(\"Loading...\")\n    \u002F\u002F 不需要 else，编译器知道已穷举\n}\n\n\u002F\u002F 3. object：单例\nobject Database {\n    fun connect() { \u002F* ... *\u002F }\n}\nDatabase.connect()\n\n\u002F\u002F 4. companion object：类的静态成员（类似 Java static）\nclass MyFragment : Fragment() {\n    companion object {\n        private const val ARG_ID = \"id\"\n\n        fun newInstance(id: String) = MyFragment().apply {\n            arguments = bundleOf(ARG_ID to id)\n        }\n    }\n}\n","kotlin",[136,6842,6843,6848,6853,6857,6862,6867,6872,6877,6882,6886,6891,6896,6901,6906,6911,6915,6919,6924,6929,6934,6939,6944,6948,6952,6957,6962,6967,6971,6976,6980,6985,6990,6995,7000,7004,7009,7014,7018,7022],{"__ignoreMap":191},[195,6844,6845],{"class":197,"line":198},[195,6846,6847],{},"\u002F\u002F 1. data class：自动生成 equals\u002FhashCode\u002FtoString\u002Fcopy\u002FcomponentN\n",[195,6849,6850],{"class":197,"line":230},[195,6851,6852],{},"data class User(val name: String, val age: Int)\n",[195,6854,6855],{"class":197,"line":251},[195,6856,1241],{"emptyLinePlaceholder":757},[195,6858,6859],{"class":197,"line":272},[195,6860,6861],{},"val user1 = User(\"Alice\", 25)\n",[195,6863,6864],{"class":197,"line":293},[195,6865,6866],{},"val user2 = user1.copy(age = 26)       \u002F\u002F 浅拷贝并修改\n",[195,6868,6869],{"class":197,"line":562},[195,6870,6871],{},"val (name, age) = user1                 \u002F\u002F 解构\n",[195,6873,6874],{"class":197,"line":583},[195,6875,6876],{},"println(user1)                          \u002F\u002F User(name=Alice, age=25)\n",[195,6878,6879],{"class":197,"line":962},[195,6880,6881],{},"println(user1 == User(\"Alice\", 25))     \u002F\u002F true（结构相等）\n",[195,6883,6884],{"class":197,"line":968},[195,6885,1241],{"emptyLinePlaceholder":757},[195,6887,6888],{"class":197,"line":1274},[195,6889,6890],{},"\u002F\u002F 2. sealed class：受限的继承层次，编译器可穷举\n",[195,6892,6893],{"class":197,"line":1282},[195,6894,6895],{},"sealed class Result\u003Cout T> {\n",[195,6897,6898],{"class":197,"line":1295},[195,6899,6900],{},"    data class Success\u003CT>(val data: T) : Result\u003CT>()\n",[195,6902,6903],{"class":197,"line":1309},[195,6904,6905],{},"    data class Error(val message: String) : Result\u003CNothing>()\n",[195,6907,6908],{"class":197,"line":2246},[195,6909,6910],{},"    data object Loading : Result\u003CNothing>()\n",[195,6912,6913],{"class":197,"line":1996},[195,6914,552],{},[195,6916,6917],{"class":197,"line":2257},[195,6918,1241],{"emptyLinePlaceholder":757},[195,6920,6921],{"class":197,"line":2262},[195,6922,6923],{},"fun handle(result: Result\u003CString>) = when (result) {\n",[195,6925,6926],{"class":197,"line":2267},[195,6927,6928],{},"    is Result.Success -> println(result.data)\n",[195,6930,6931],{"class":197,"line":2273},[195,6932,6933],{},"    is Result.Error -> println(result.message)\n",[195,6935,6936],{"class":197,"line":2033},[195,6937,6938],{},"    Result.Loading -> println(\"Loading...\")\n",[195,6940,6941],{"class":197,"line":2284},[195,6942,6943],{},"    \u002F\u002F 不需要 else，编译器知道已穷举\n",[195,6945,6946],{"class":197,"line":2460},[195,6947,552],{},[195,6949,6950],{"class":197,"line":2466},[195,6951,1241],{"emptyLinePlaceholder":757},[195,6953,6954],{"class":197,"line":2472},[195,6955,6956],{},"\u002F\u002F 3. object：单例\n",[195,6958,6959],{"class":197,"line":2780},[195,6960,6961],{},"object Database {\n",[195,6963,6964],{"class":197,"line":2786},[195,6965,6966],{},"    fun connect() { \u002F* ... *\u002F }\n",[195,6968,6969],{"class":197,"line":2792},[195,6970,552],{},[195,6972,6973],{"class":197,"line":2798},[195,6974,6975],{},"Database.connect()\n",[195,6977,6978],{"class":197,"line":2804},[195,6979,1241],{"emptyLinePlaceholder":757},[195,6981,6982],{"class":197,"line":2810},[195,6983,6984],{},"\u002F\u002F 4. companion object：类的静态成员（类似 Java static）\n",[195,6986,6987],{"class":197,"line":2815},[195,6988,6989],{},"class MyFragment : Fragment() {\n",[195,6991,6992],{"class":197,"line":2820},[195,6993,6994],{},"    companion object {\n",[195,6996,6997],{"class":197,"line":2825},[195,6998,6999],{},"        private const val ARG_ID = \"id\"\n",[195,7001,7002],{"class":197,"line":2831},[195,7003,1241],{"emptyLinePlaceholder":757},[195,7005,7006],{"class":197,"line":2837},[195,7007,7008],{},"        fun newInstance(id: String) = MyFragment().apply {\n",[195,7010,7011],{"class":197,"line":2843},[195,7012,7013],{},"            arguments = bundleOf(ARG_ID to id)\n",[195,7015,7016],{"class":197,"line":2848},[195,7017,2887],{},[195,7019,7020],{"class":197,"line":2854},[195,7021,2403],{},[195,7023,7024],{"class":197,"line":2860},[195,7025,552],{},[1890,7027],{},[32,7029,7031],{"id":7030},"q82-kotlin-的空安全和作用域函数letrunwithapplyalso","Q8.2: Kotlin 的空安全和作用域函数（let\u002Frun\u002Fwith\u002Fapply\u002Falso）",[14,7033,7034],{},[125,7035,2077],{},[14,7037,7038],{},[125,7039,7040],{},"空安全：",[186,7042,7044],{"className":6838,"code":7043,"language":6840,"meta":191,"style":191},"var name: String = \"Hello\"   \u002F\u002F 非空\nvar name: String? = null     \u002F\u002F 可空\n\nname?.length                 \u002F\u002F 安全调用\nname ?: \"default\"            \u002F\u002F Elvis 操作符\nname!!.length                \u002F\u002F 强制解包（null 时抛 NPE）\n\n\u002F\u002F 智能转换（Smart Cast）\nfun process(value: Any) {\n    if (value is String) {\n        println(value.length)  \u002F\u002F 自动转换为 String，无需强转\n    }\n}\n",[136,7045,7046,7051,7056,7060,7065,7070,7075,7079,7084,7089,7094,7099,7103],{"__ignoreMap":191},[195,7047,7048],{"class":197,"line":198},[195,7049,7050],{},"var name: String = \"Hello\"   \u002F\u002F 非空\n",[195,7052,7053],{"class":197,"line":230},[195,7054,7055],{},"var name: String? = null     \u002F\u002F 可空\n",[195,7057,7058],{"class":197,"line":251},[195,7059,1241],{"emptyLinePlaceholder":757},[195,7061,7062],{"class":197,"line":272},[195,7063,7064],{},"name?.length                 \u002F\u002F 安全调用\n",[195,7066,7067],{"class":197,"line":293},[195,7068,7069],{},"name ?: \"default\"            \u002F\u002F Elvis 操作符\n",[195,7071,7072],{"class":197,"line":562},[195,7073,7074],{},"name!!.length                \u002F\u002F 强制解包（null 时抛 NPE）\n",[195,7076,7077],{"class":197,"line":583},[195,7078,1241],{"emptyLinePlaceholder":757},[195,7080,7081],{"class":197,"line":962},[195,7082,7083],{},"\u002F\u002F 智能转换（Smart Cast）\n",[195,7085,7086],{"class":197,"line":968},[195,7087,7088],{},"fun process(value: Any) {\n",[195,7090,7091],{"class":197,"line":1274},[195,7092,7093],{},"    if (value is String) {\n",[195,7095,7096],{"class":197,"line":1282},[195,7097,7098],{},"        println(value.length)  \u002F\u002F 自动转换为 String，无需强转\n",[195,7100,7101],{"class":197,"line":1295},[195,7102,2403],{},[195,7104,7105],{"class":197,"line":1309},[195,7106,552],{},[14,7108,7109],{},[125,7110,7111],{},"作用域函数：",[36,7113,7114,7130],{},[39,7115,7116],{},[42,7117,7118,7121,7124,7127],{},[45,7119,7120],{},"函数",[45,7122,7123],{},"对象引用",[45,7125,7126],{},"返回值",[45,7128,7129],{},"典型场景",[52,7131,7132,7150,7167,7183,7200],{},[42,7133,7134,7139,7144,7147],{},[57,7135,7136],{},[136,7137,7138],{},"let",[57,7140,7141],{},[136,7142,7143],{},"it",[57,7145,7146],{},"Lambda 结果",[57,7148,7149],{},"空安全调用、变量转换",[42,7151,7152,7157,7162,7164],{},[57,7153,7154],{},[136,7155,7156],{},"run",[57,7158,7159],{},[136,7160,7161],{},"this",[57,7163,7146],{},[57,7165,7166],{},"对象配置 + 计算结果",[42,7168,7169,7174,7178,7180],{},[57,7170,7171],{},[136,7172,7173],{},"with",[57,7175,7176],{},[136,7177,7161],{},[57,7179,7146],{},[57,7181,7182],{},"对已有对象调用多个方法",[42,7184,7185,7190,7194,7197],{},[57,7186,7187],{},[136,7188,7189],{},"apply",[57,7191,7192],{},[136,7193,7161],{},[57,7195,7196],{},"对象本身",[57,7198,7199],{},"对象初始化\u002F配置",[42,7201,7202,7207,7211,7213],{},[57,7203,7204],{},[136,7205,7206],{},"also",[57,7208,7209],{},[136,7210,7143],{},[57,7212,7196],{},[57,7214,7215],{},"额外的副作用（日志等）",[186,7217,7219],{"className":6838,"code":7218,"language":6840,"meta":191,"style":191},"\u002F\u002F let：空安全 + 转换\nval length = name?.let {\n    println(\"Name is $it\")\n    it.length  \u002F\u002F 返回长度\n}\n\n\u002F\u002F apply：对象初始化\nval textView = TextView(context).apply {\n    text = \"Hello\"\n    textSize = 16f\n    setTextColor(Color.BLACK)\n}\n\n\u002F\u002F also：链式操作中的副作用\nfun getUser() = repository.findUser(id)\n    .also { log(\"Found user: $it\") }  \u002F\u002F 日志，不影响返回值\n\n\u002F\u002F run：配置 + 计算\nval result = service.run {\n    port = 8080\n    query(\"SELECT * FROM users\")  \u002F\u002F 返回查询结果\n}\n\n\u002F\u002F with：已有对象的多个操作\nwith(binding) {\n    titleText.text = item.title\n    subtitleText.text = item.subtitle\n    imageView.load(item.imageUrl)\n}\n",[136,7220,7221,7226,7231,7236,7241,7245,7249,7254,7259,7264,7269,7274,7278,7282,7287,7292,7297,7301,7306,7311,7316,7321,7325,7329,7334,7339,7344,7349,7354],{"__ignoreMap":191},[195,7222,7223],{"class":197,"line":198},[195,7224,7225],{},"\u002F\u002F let：空安全 + 转换\n",[195,7227,7228],{"class":197,"line":230},[195,7229,7230],{},"val length = name?.let {\n",[195,7232,7233],{"class":197,"line":251},[195,7234,7235],{},"    println(\"Name is $it\")\n",[195,7237,7238],{"class":197,"line":272},[195,7239,7240],{},"    it.length  \u002F\u002F 返回长度\n",[195,7242,7243],{"class":197,"line":293},[195,7244,552],{},[195,7246,7247],{"class":197,"line":562},[195,7248,1241],{"emptyLinePlaceholder":757},[195,7250,7251],{"class":197,"line":583},[195,7252,7253],{},"\u002F\u002F apply：对象初始化\n",[195,7255,7256],{"class":197,"line":962},[195,7257,7258],{},"val textView = TextView(context).apply {\n",[195,7260,7261],{"class":197,"line":968},[195,7262,7263],{},"    text = \"Hello\"\n",[195,7265,7266],{"class":197,"line":1274},[195,7267,7268],{},"    textSize = 16f\n",[195,7270,7271],{"class":197,"line":1282},[195,7272,7273],{},"    setTextColor(Color.BLACK)\n",[195,7275,7276],{"class":197,"line":1295},[195,7277,552],{},[195,7279,7280],{"class":197,"line":1309},[195,7281,1241],{"emptyLinePlaceholder":757},[195,7283,7284],{"class":197,"line":2246},[195,7285,7286],{},"\u002F\u002F also：链式操作中的副作用\n",[195,7288,7289],{"class":197,"line":1996},[195,7290,7291],{},"fun getUser() = repository.findUser(id)\n",[195,7293,7294],{"class":197,"line":2257},[195,7295,7296],{},"    .also { log(\"Found user: $it\") }  \u002F\u002F 日志，不影响返回值\n",[195,7298,7299],{"class":197,"line":2262},[195,7300,1241],{"emptyLinePlaceholder":757},[195,7302,7303],{"class":197,"line":2267},[195,7304,7305],{},"\u002F\u002F run：配置 + 计算\n",[195,7307,7308],{"class":197,"line":2273},[195,7309,7310],{},"val result = service.run {\n",[195,7312,7313],{"class":197,"line":2033},[195,7314,7315],{},"    port = 8080\n",[195,7317,7318],{"class":197,"line":2284},[195,7319,7320],{},"    query(\"SELECT * FROM users\")  \u002F\u002F 返回查询结果\n",[195,7322,7323],{"class":197,"line":2460},[195,7324,552],{},[195,7326,7327],{"class":197,"line":2466},[195,7328,1241],{"emptyLinePlaceholder":757},[195,7330,7331],{"class":197,"line":2472},[195,7332,7333],{},"\u002F\u002F with：已有对象的多个操作\n",[195,7335,7336],{"class":197,"line":2780},[195,7337,7338],{},"with(binding) {\n",[195,7340,7341],{"class":197,"line":2786},[195,7342,7343],{},"    titleText.text = item.title\n",[195,7345,7346],{"class":197,"line":2792},[195,7347,7348],{},"    subtitleText.text = item.subtitle\n",[195,7350,7351],{"class":197,"line":2798},[195,7352,7353],{},"    imageView.load(item.imageUrl)\n",[195,7355,7356],{"class":197,"line":2804},[195,7357,552],{},[1890,7359],{},[32,7361,7363],{"id":7362},"q83-kotlin-中的委托delegation机制","Q8.3: Kotlin 中的委托（Delegation）机制",[14,7365,7366],{},[125,7367,2077],{},[186,7369,7371],{"className":6838,"code":7370,"language":6840,"meta":191,"style":191},"\u002F\u002F 1. 类委托（by）：用组合替代继承\ninterface Printer {\n    fun print(message: String)\n}\n\nclass ConsolePrinter : Printer {\n    override fun print(message: String) = println(message)\n}\n\n\u002F\u002F LoggingPrinter 自动委托 Printer 接口的实现给 printer\nclass LoggingPrinter(private val printer: Printer) : Printer by printer {\n    override fun print(message: String) {\n        println(\"[LOG] About to print\")\n        printer.print(message)  \u002F\u002F 委托给真正的实现\n    }\n}\n\n\u002F\u002F 2. 属性委托\n\u002F\u002F lazy：首次访问时初始化\nval heavyObject: HeavyObject by lazy {\n    HeavyObject()  \u002F\u002F 线程安全的懒初始化\n}\n\n\u002F\u002F observable：属性变化监听\nvar name: String by Delegates.observable(\"initial\") { prop, old, new ->\n    println(\"$old → $new\")\n}\n\n\u002F\u002F 自定义委托\nclass SharedPreferencesDelegate(\n    private val prefs: SharedPreferences,\n    private val key: String,\n    private val default: String\n) : ReadWriteProperty\u003CAny, String> {\n\n    override fun getValue(thisRef: Any, property: KProperty\u003C*>): String {\n        return prefs.getString(key, default) ?: default\n    }\n\n    override fun setValue(thisRef: Any, property: KProperty\u003C*>, value: String) {\n        prefs.edit().putString(key, value).apply()\n    }\n}\n\n\u002F\u002F 使用\nclass Settings(prefs: SharedPreferences) {\n    var username: String by SharedPreferencesDelegate(prefs, \"username\", \"\")\n    var theme: String by SharedPreferencesDelegate(prefs, \"theme\", \"light\")\n}\n",[136,7372,7373,7378,7383,7388,7392,7396,7401,7406,7410,7414,7419,7424,7429,7434,7439,7443,7447,7451,7456,7461,7466,7471,7475,7479,7484,7489,7494,7498,7502,7507,7512,7517,7522,7527,7532,7536,7541,7546,7550,7554,7559,7564,7568,7572,7576,7580,7585,7590,7595],{"__ignoreMap":191},[195,7374,7375],{"class":197,"line":198},[195,7376,7377],{},"\u002F\u002F 1. 类委托（by）：用组合替代继承\n",[195,7379,7380],{"class":197,"line":230},[195,7381,7382],{},"interface Printer {\n",[195,7384,7385],{"class":197,"line":251},[195,7386,7387],{},"    fun print(message: String)\n",[195,7389,7390],{"class":197,"line":272},[195,7391,552],{},[195,7393,7394],{"class":197,"line":293},[195,7395,1241],{"emptyLinePlaceholder":757},[195,7397,7398],{"class":197,"line":562},[195,7399,7400],{},"class ConsolePrinter : Printer {\n",[195,7402,7403],{"class":197,"line":583},[195,7404,7405],{},"    override fun print(message: String) = println(message)\n",[195,7407,7408],{"class":197,"line":962},[195,7409,552],{},[195,7411,7412],{"class":197,"line":968},[195,7413,1241],{"emptyLinePlaceholder":757},[195,7415,7416],{"class":197,"line":1274},[195,7417,7418],{},"\u002F\u002F LoggingPrinter 自动委托 Printer 接口的实现给 printer\n",[195,7420,7421],{"class":197,"line":1282},[195,7422,7423],{},"class LoggingPrinter(private val printer: Printer) : Printer by printer {\n",[195,7425,7426],{"class":197,"line":1295},[195,7427,7428],{},"    override fun print(message: String) {\n",[195,7430,7431],{"class":197,"line":1309},[195,7432,7433],{},"        println(\"[LOG] About to print\")\n",[195,7435,7436],{"class":197,"line":2246},[195,7437,7438],{},"        printer.print(message)  \u002F\u002F 委托给真正的实现\n",[195,7440,7441],{"class":197,"line":1996},[195,7442,2403],{},[195,7444,7445],{"class":197,"line":2257},[195,7446,552],{},[195,7448,7449],{"class":197,"line":2262},[195,7450,1241],{"emptyLinePlaceholder":757},[195,7452,7453],{"class":197,"line":2267},[195,7454,7455],{},"\u002F\u002F 2. 属性委托\n",[195,7457,7458],{"class":197,"line":2273},[195,7459,7460],{},"\u002F\u002F lazy：首次访问时初始化\n",[195,7462,7463],{"class":197,"line":2033},[195,7464,7465],{},"val heavyObject: HeavyObject by lazy {\n",[195,7467,7468],{"class":197,"line":2284},[195,7469,7470],{},"    HeavyObject()  \u002F\u002F 线程安全的懒初始化\n",[195,7472,7473],{"class":197,"line":2460},[195,7474,552],{},[195,7476,7477],{"class":197,"line":2466},[195,7478,1241],{"emptyLinePlaceholder":757},[195,7480,7481],{"class":197,"line":2472},[195,7482,7483],{},"\u002F\u002F observable：属性变化监听\n",[195,7485,7486],{"class":197,"line":2780},[195,7487,7488],{},"var name: String by Delegates.observable(\"initial\") { prop, old, new ->\n",[195,7490,7491],{"class":197,"line":2786},[195,7492,7493],{},"    println(\"$old → $new\")\n",[195,7495,7496],{"class":197,"line":2792},[195,7497,552],{},[195,7499,7500],{"class":197,"line":2798},[195,7501,1241],{"emptyLinePlaceholder":757},[195,7503,7504],{"class":197,"line":2804},[195,7505,7506],{},"\u002F\u002F 自定义委托\n",[195,7508,7509],{"class":197,"line":2810},[195,7510,7511],{},"class SharedPreferencesDelegate(\n",[195,7513,7514],{"class":197,"line":2815},[195,7515,7516],{},"    private val prefs: SharedPreferences,\n",[195,7518,7519],{"class":197,"line":2820},[195,7520,7521],{},"    private val key: String,\n",[195,7523,7524],{"class":197,"line":2825},[195,7525,7526],{},"    private val default: String\n",[195,7528,7529],{"class":197,"line":2831},[195,7530,7531],{},") : ReadWriteProperty\u003CAny, String> {\n",[195,7533,7534],{"class":197,"line":2837},[195,7535,1241],{"emptyLinePlaceholder":757},[195,7537,7538],{"class":197,"line":2843},[195,7539,7540],{},"    override fun getValue(thisRef: Any, property: KProperty\u003C*>): String {\n",[195,7542,7543],{"class":197,"line":2848},[195,7544,7545],{},"        return prefs.getString(key, default) ?: default\n",[195,7547,7548],{"class":197,"line":2854},[195,7549,2403],{},[195,7551,7552],{"class":197,"line":2860},[195,7553,1241],{"emptyLinePlaceholder":757},[195,7555,7556],{"class":197,"line":2866},[195,7557,7558],{},"    override fun setValue(thisRef: Any, property: KProperty\u003C*>, value: String) {\n",[195,7560,7561],{"class":197,"line":2872},[195,7562,7563],{},"        prefs.edit().putString(key, value).apply()\n",[195,7565,7566],{"class":197,"line":2878},[195,7567,2403],{},[195,7569,7570],{"class":197,"line":2884},[195,7571,552],{},[195,7573,7574],{"class":197,"line":2890},[195,7575,1241],{"emptyLinePlaceholder":757},[195,7577,7578],{"class":197,"line":2895},[195,7579,3056],{},[195,7581,7582],{"class":197,"line":2900},[195,7583,7584],{},"class Settings(prefs: SharedPreferences) {\n",[195,7586,7587],{"class":197,"line":2905},[195,7588,7589],{},"    var username: String by SharedPreferencesDelegate(prefs, \"username\", \"\")\n",[195,7591,7592],{"class":197,"line":2911},[195,7593,7594],{},"    var theme: String by SharedPreferencesDelegate(prefs, \"theme\", \"light\")\n",[195,7596,7597],{"class":197,"line":2917},[195,7598,552],{},[1890,7600],{},[32,7602,7604],{"id":7603},"q84-kotlin-的内联函数inlinereified-泛型和高阶函数","Q8.4: Kotlin 的内联函数（inline）、reified 泛型和高阶函数",[14,7606,7607],{},[125,7608,2077],{},[186,7610,7612],{"className":6838,"code":7611,"language":6840,"meta":191,"style":191},"\u002F\u002F inline：编译时将函数体内联到调用处，消除 Lambda 对象开销\ninline fun measureTime(block: () -> Unit): Long {\n    val start = System.currentTimeMillis()\n    block()  \u002F\u002F 不会创建 Function 对象\n    return System.currentTimeMillis() - start\n}\n\n\u002F\u002F reified：在 inline 函数中获取泛型的实际类型（运行时可用）\n\u002F\u002F 普通泛型在运行时被擦除，reified 保留类型信息\ninline fun \u003Creified T> Gson.fromJson(json: String): T {\n    return fromJson(json, T::class.java)  \u002F\u002F 可以直接使用 T::class\n}\n\n\u002F\u002F 使用\nval user = gson.fromJson\u003CUser>(jsonString) \u002F\u002F 不需要传 User::class.java\n\n\u002F\u002F crossinline \u002F noinline\ninline fun execute(\n    crossinline setup: () -> Unit,   \u002F\u002F 不允许非局部返回\n    noinline callback: () -> Unit    \u002F\u002F 不内联此参数（可以存储引用）\n) {\n    setup()\n    post(callback)  \u002F\u002F noinline 的 lambda 可以作为对象传递\n}\n\n\u002F\u002F 非局部返回\ninline fun List\u003CInt>.forEachInlined(action: (Int) -> Unit) {\n    for (item in this) action(item)\n}\n\nfun test() {\n    listOf(1, 2, 3).forEachInlined {\n        if (it == 2) return  \u002F\u002F 直接从 test() 返回（非局部返回）\n    }\n}\n",[136,7613,7614,7619,7624,7629,7634,7639,7643,7647,7652,7657,7662,7667,7671,7675,7679,7684,7688,7693,7698,7703,7708,7712,7717,7722,7726,7730,7735,7740,7745,7749,7753,7758,7763,7768,7772],{"__ignoreMap":191},[195,7615,7616],{"class":197,"line":198},[195,7617,7618],{},"\u002F\u002F inline：编译时将函数体内联到调用处，消除 Lambda 对象开销\n",[195,7620,7621],{"class":197,"line":230},[195,7622,7623],{},"inline fun measureTime(block: () -> Unit): Long {\n",[195,7625,7626],{"class":197,"line":251},[195,7627,7628],{},"    val start = System.currentTimeMillis()\n",[195,7630,7631],{"class":197,"line":272},[195,7632,7633],{},"    block()  \u002F\u002F 不会创建 Function 对象\n",[195,7635,7636],{"class":197,"line":293},[195,7637,7638],{},"    return System.currentTimeMillis() - start\n",[195,7640,7641],{"class":197,"line":562},[195,7642,552],{},[195,7644,7645],{"class":197,"line":583},[195,7646,1241],{"emptyLinePlaceholder":757},[195,7648,7649],{"class":197,"line":962},[195,7650,7651],{},"\u002F\u002F reified：在 inline 函数中获取泛型的实际类型（运行时可用）\n",[195,7653,7654],{"class":197,"line":968},[195,7655,7656],{},"\u002F\u002F 普通泛型在运行时被擦除，reified 保留类型信息\n",[195,7658,7659],{"class":197,"line":1274},[195,7660,7661],{},"inline fun \u003Creified T> Gson.fromJson(json: String): T {\n",[195,7663,7664],{"class":197,"line":1282},[195,7665,7666],{},"    return fromJson(json, T::class.java)  \u002F\u002F 可以直接使用 T::class\n",[195,7668,7669],{"class":197,"line":1295},[195,7670,552],{},[195,7672,7673],{"class":197,"line":1309},[195,7674,1241],{"emptyLinePlaceholder":757},[195,7676,7677],{"class":197,"line":2246},[195,7678,3056],{},[195,7680,7681],{"class":197,"line":1996},[195,7682,7683],{},"val user = gson.fromJson\u003CUser>(jsonString) \u002F\u002F 不需要传 User::class.java\n",[195,7685,7686],{"class":197,"line":2257},[195,7687,1241],{"emptyLinePlaceholder":757},[195,7689,7690],{"class":197,"line":2262},[195,7691,7692],{},"\u002F\u002F crossinline \u002F noinline\n",[195,7694,7695],{"class":197,"line":2267},[195,7696,7697],{},"inline fun execute(\n",[195,7699,7700],{"class":197,"line":2273},[195,7701,7702],{},"    crossinline setup: () -> Unit,   \u002F\u002F 不允许非局部返回\n",[195,7704,7705],{"class":197,"line":2033},[195,7706,7707],{},"    noinline callback: () -> Unit    \u002F\u002F 不内联此参数（可以存储引用）\n",[195,7709,7710],{"class":197,"line":2284},[195,7711,644],{},[195,7713,7714],{"class":197,"line":2460},[195,7715,7716],{},"    setup()\n",[195,7718,7719],{"class":197,"line":2466},[195,7720,7721],{},"    post(callback)  \u002F\u002F noinline 的 lambda 可以作为对象传递\n",[195,7723,7724],{"class":197,"line":2472},[195,7725,552],{},[195,7727,7728],{"class":197,"line":2780},[195,7729,1241],{"emptyLinePlaceholder":757},[195,7731,7732],{"class":197,"line":2786},[195,7733,7734],{},"\u002F\u002F 非局部返回\n",[195,7736,7737],{"class":197,"line":2792},[195,7738,7739],{},"inline fun List\u003CInt>.forEachInlined(action: (Int) -> Unit) {\n",[195,7741,7742],{"class":197,"line":2798},[195,7743,7744],{},"    for (item in this) action(item)\n",[195,7746,7747],{"class":197,"line":2804},[195,7748,552],{},[195,7750,7751],{"class":197,"line":2810},[195,7752,1241],{"emptyLinePlaceholder":757},[195,7754,7755],{"class":197,"line":2815},[195,7756,7757],{},"fun test() {\n",[195,7759,7760],{"class":197,"line":2820},[195,7761,7762],{},"    listOf(1, 2, 3).forEachInlined {\n",[195,7764,7765],{"class":197,"line":2825},[195,7766,7767],{},"        if (it == 2) return  \u002F\u002F 直接从 test() 返回（非局部返回）\n",[195,7769,7770],{"class":197,"line":2831},[195,7771,2403],{},[195,7773,7774],{"class":197,"line":2837},[195,7775,552],{},[1890,7777],{},[18,7779,7781],{"id":7780},"_9-四大组件与生命周期","9. 四大组件与生命周期",[32,7783,7785],{"id":7784},"q91-activity-的生命周期和配置变更处理","Q9.1: Activity 的生命周期和配置变更处理",[14,7787,7788],{},[125,7789,2077],{},[186,7791,7794],{"className":7792,"code":7793,"language":1074},[1072],"                  onCreate()\n                      ↓\n                  onStart()    ←── onRestart()\n                      ↓               ↑\n                  onResume()          │\n                      ↓               │\n               [Activity Running]     │\n                      ↓               │\n                  onPause()           │\n                      ↓               │\n                  onStop()  ──────────┘\n                      ↓\n                  onDestroy()\n",[136,7795,7793],{"__ignoreMap":191},[14,7797,7798,7801],{},[125,7799,7800],{},"配置变更（如旋转屏幕）默认行为："," 销毁 Activity → 重建 Activity",[186,7803,7805],{"className":6838,"code":7804,"language":6840,"meta":191,"style":191},"\u002F\u002F 方案 1：ViewModel（推荐，数据在配置变更中存活）\nclass MyViewModel : ViewModel() {\n    private val _data = MutableLiveData\u003CList\u003CItem>>()\n    val data: LiveData\u003CList\u003CItem>> = _data\n\n    fun loadData() {\n        viewModelScope.launch {\n            _data.value = repository.fetch()\n        }\n    }\n}\n\n\u002F\u002F 方案 2：onSaveInstanceState（少量简单数据）\noverride fun onSaveInstanceState(outState: Bundle) {\n    super.onSaveInstanceState(outState)\n    outState.putInt(\"scroll_position\", scrollPosition)\n}\n\noverride fun onCreate(savedInstanceState: Bundle?) {\n    super.onCreate(savedInstanceState)\n    savedInstanceState?.getInt(\"scroll_position\")?.let { scrollTo(it) }\n}\n\n\u002F\u002F 方案 3：自行处理配置变更（不推荐，除非有特殊需求）\n\u002F\u002F AndroidManifest.xml\n\u002F\u002F android:configChanges=\"orientation|screenSize|keyboardHidden\"\noverride fun onConfigurationChanged(newConfig: Configuration) {\n    super.onConfigurationChanged(newConfig)\n    \u002F\u002F 手动调整布局\n}\n",[136,7806,7807,7812,7817,7822,7827,7831,7836,7841,7846,7850,7854,7858,7862,7867,7872,7877,7882,7886,7890,7895,7900,7905,7909,7913,7918,7923,7928,7933,7938,7943],{"__ignoreMap":191},[195,7808,7809],{"class":197,"line":198},[195,7810,7811],{},"\u002F\u002F 方案 1：ViewModel（推荐，数据在配置变更中存活）\n",[195,7813,7814],{"class":197,"line":230},[195,7815,7816],{},"class MyViewModel : ViewModel() {\n",[195,7818,7819],{"class":197,"line":251},[195,7820,7821],{},"    private val _data = MutableLiveData\u003CList\u003CItem>>()\n",[195,7823,7824],{"class":197,"line":272},[195,7825,7826],{},"    val data: LiveData\u003CList\u003CItem>> = _data\n",[195,7828,7829],{"class":197,"line":293},[195,7830,1241],{"emptyLinePlaceholder":757},[195,7832,7833],{"class":197,"line":562},[195,7834,7835],{},"    fun loadData() {\n",[195,7837,7838],{"class":197,"line":583},[195,7839,7840],{},"        viewModelScope.launch {\n",[195,7842,7843],{"class":197,"line":962},[195,7844,7845],{},"            _data.value = repository.fetch()\n",[195,7847,7848],{"class":197,"line":968},[195,7849,2887],{},[195,7851,7852],{"class":197,"line":1274},[195,7853,2403],{},[195,7855,7856],{"class":197,"line":1282},[195,7857,552],{},[195,7859,7860],{"class":197,"line":1295},[195,7861,1241],{"emptyLinePlaceholder":757},[195,7863,7864],{"class":197,"line":1309},[195,7865,7866],{},"\u002F\u002F 方案 2：onSaveInstanceState（少量简单数据）\n",[195,7868,7869],{"class":197,"line":2246},[195,7870,7871],{},"override fun onSaveInstanceState(outState: Bundle) {\n",[195,7873,7874],{"class":197,"line":1996},[195,7875,7876],{},"    super.onSaveInstanceState(outState)\n",[195,7878,7879],{"class":197,"line":2257},[195,7880,7881],{},"    outState.putInt(\"scroll_position\", scrollPosition)\n",[195,7883,7884],{"class":197,"line":2262},[195,7885,552],{},[195,7887,7888],{"class":197,"line":2267},[195,7889,1241],{"emptyLinePlaceholder":757},[195,7891,7892],{"class":197,"line":2273},[195,7893,7894],{},"override fun onCreate(savedInstanceState: Bundle?) {\n",[195,7896,7897],{"class":197,"line":2033},[195,7898,7899],{},"    super.onCreate(savedInstanceState)\n",[195,7901,7902],{"class":197,"line":2284},[195,7903,7904],{},"    savedInstanceState?.getInt(\"scroll_position\")?.let { scrollTo(it) }\n",[195,7906,7907],{"class":197,"line":2460},[195,7908,552],{},[195,7910,7911],{"class":197,"line":2466},[195,7912,1241],{"emptyLinePlaceholder":757},[195,7914,7915],{"class":197,"line":2472},[195,7916,7917],{},"\u002F\u002F 方案 3：自行处理配置变更（不推荐，除非有特殊需求）\n",[195,7919,7920],{"class":197,"line":2780},[195,7921,7922],{},"\u002F\u002F AndroidManifest.xml\n",[195,7924,7925],{"class":197,"line":2786},[195,7926,7927],{},"\u002F\u002F android:configChanges=\"orientation|screenSize|keyboardHidden\"\n",[195,7929,7930],{"class":197,"line":2792},[195,7931,7932],{},"override fun onConfigurationChanged(newConfig: Configuration) {\n",[195,7934,7935],{"class":197,"line":2798},[195,7936,7937],{},"    super.onConfigurationChanged(newConfig)\n",[195,7939,7940],{"class":197,"line":2804},[195,7941,7942],{},"    \u002F\u002F 手动调整布局\n",[195,7944,7945],{"class":197,"line":2810},[195,7946,552],{},[1890,7948],{},[32,7950,7952],{"id":7951},"q92-fragment-的生命周期和常见问题","Q9.2: Fragment 的生命周期和常见问题",[14,7954,7955],{},[125,7956,2077],{},[186,7958,7961],{"className":7959,"code":7960,"language":1074},[1072],"onAttach() → onCreate() → onCreateView() → onViewCreated()\n    → onStart() → onResume()\n\nonPause() → onStop() → onDestroyView() → onDestroy() → onDetach()\n",[136,7962,7960],{"__ignoreMap":191},[14,7964,7965],{},[125,7966,7967],{},"常见问题与最佳实践：",[186,7969,7971],{"className":6838,"code":7970,"language":6840,"meta":191,"style":191},"class MyFragment : Fragment(R.layout.fragment_my) {\n\n    \u002F\u002F ✅ 使用 viewLifecycleOwner（而非 this）观察 LiveData\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n\n        \u002F\u002F ❌ 使用 this 可能导致多次观察（Fragment 可能多次 onCreateView）\n        \u002F\u002F viewModel.data.observe(this) { ... }\n\n        \u002F\u002F ✅ viewLifecycleOwner 跟随 View 生命周期\n        viewModel.data.observe(viewLifecycleOwner) { data ->\n            adapter.submitList(data)\n        }\n    }\n\n    \u002F\u002F ViewBinding 的正确用法\n    private var _binding: FragmentMyBinding? = null\n    private val binding get() = _binding!!\n\n    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {\n        _binding = FragmentMyBinding.inflate(inflater, container, false)\n        return binding.root\n    }\n\n    override fun onDestroyView() {\n        super.onDestroyView()\n        _binding = null  \u002F\u002F 避免内存泄漏\n    }\n\n    \u002F\u002F Fragment 之间通信：使用 ViewModel 或 FragmentResult API\n    \u002F\u002F ❌ 直接引用其他 Fragment\n    \u002F\u002F ✅ 共享 ViewModel\n    private val sharedViewModel: SharedViewModel by activityViewModels()\n\n    \u002F\u002F ✅ FragmentResult API\n    setFragmentResult(\"requestKey\", bundleOf(\"key\" to value))\n    \u002F\u002F 接收方\n    setFragmentResultListener(\"requestKey\") { _, bundle ->\n        val value = bundle.getString(\"key\")\n    }\n}\n",[136,7972,7973,7978,7982,7987,7992,7997,8001,8006,8011,8015,8020,8025,8030,8034,8038,8042,8047,8052,8057,8061,8066,8071,8076,8080,8084,8089,8094,8099,8103,8107,8112,8117,8122,8127,8131,8136,8141,8146,8151,8156,8160],{"__ignoreMap":191},[195,7974,7975],{"class":197,"line":198},[195,7976,7977],{},"class MyFragment : Fragment(R.layout.fragment_my) {\n",[195,7979,7980],{"class":197,"line":230},[195,7981,1241],{"emptyLinePlaceholder":757},[195,7983,7984],{"class":197,"line":251},[195,7985,7986],{},"    \u002F\u002F ✅ 使用 viewLifecycleOwner（而非 this）观察 LiveData\n",[195,7988,7989],{"class":197,"line":272},[195,7990,7991],{},"    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n",[195,7993,7994],{"class":197,"line":293},[195,7995,7996],{},"        super.onViewCreated(view, savedInstanceState)\n",[195,7998,7999],{"class":197,"line":562},[195,8000,1241],{"emptyLinePlaceholder":757},[195,8002,8003],{"class":197,"line":583},[195,8004,8005],{},"        \u002F\u002F ❌ 使用 this 可能导致多次观察（Fragment 可能多次 onCreateView）\n",[195,8007,8008],{"class":197,"line":962},[195,8009,8010],{},"        \u002F\u002F viewModel.data.observe(this) { ... }\n",[195,8012,8013],{"class":197,"line":968},[195,8014,1241],{"emptyLinePlaceholder":757},[195,8016,8017],{"class":197,"line":1274},[195,8018,8019],{},"        \u002F\u002F ✅ viewLifecycleOwner 跟随 View 生命周期\n",[195,8021,8022],{"class":197,"line":1282},[195,8023,8024],{},"        viewModel.data.observe(viewLifecycleOwner) { data ->\n",[195,8026,8027],{"class":197,"line":1295},[195,8028,8029],{},"            adapter.submitList(data)\n",[195,8031,8032],{"class":197,"line":1309},[195,8033,2887],{},[195,8035,8036],{"class":197,"line":2246},[195,8037,2403],{},[195,8039,8040],{"class":197,"line":1996},[195,8041,1241],{"emptyLinePlaceholder":757},[195,8043,8044],{"class":197,"line":2257},[195,8045,8046],{},"    \u002F\u002F ViewBinding 的正确用法\n",[195,8048,8049],{"class":197,"line":2262},[195,8050,8051],{},"    private var _binding: FragmentMyBinding? = null\n",[195,8053,8054],{"class":197,"line":2267},[195,8055,8056],{},"    private val binding get() = _binding!!\n",[195,8058,8059],{"class":197,"line":2273},[195,8060,1241],{"emptyLinePlaceholder":757},[195,8062,8063],{"class":197,"line":2033},[195,8064,8065],{},"    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {\n",[195,8067,8068],{"class":197,"line":2284},[195,8069,8070],{},"        _binding = FragmentMyBinding.inflate(inflater, container, false)\n",[195,8072,8073],{"class":197,"line":2460},[195,8074,8075],{},"        return binding.root\n",[195,8077,8078],{"class":197,"line":2466},[195,8079,2403],{},[195,8081,8082],{"class":197,"line":2472},[195,8083,1241],{"emptyLinePlaceholder":757},[195,8085,8086],{"class":197,"line":2780},[195,8087,8088],{},"    override fun onDestroyView() {\n",[195,8090,8091],{"class":197,"line":2786},[195,8092,8093],{},"        super.onDestroyView()\n",[195,8095,8096],{"class":197,"line":2792},[195,8097,8098],{},"        _binding = null  \u002F\u002F 避免内存泄漏\n",[195,8100,8101],{"class":197,"line":2798},[195,8102,2403],{},[195,8104,8105],{"class":197,"line":2804},[195,8106,1241],{"emptyLinePlaceholder":757},[195,8108,8109],{"class":197,"line":2810},[195,8110,8111],{},"    \u002F\u002F Fragment 之间通信：使用 ViewModel 或 FragmentResult API\n",[195,8113,8114],{"class":197,"line":2815},[195,8115,8116],{},"    \u002F\u002F ❌ 直接引用其他 Fragment\n",[195,8118,8119],{"class":197,"line":2820},[195,8120,8121],{},"    \u002F\u002F ✅ 共享 ViewModel\n",[195,8123,8124],{"class":197,"line":2825},[195,8125,8126],{},"    private val sharedViewModel: SharedViewModel by activityViewModels()\n",[195,8128,8129],{"class":197,"line":2831},[195,8130,1241],{"emptyLinePlaceholder":757},[195,8132,8133],{"class":197,"line":2837},[195,8134,8135],{},"    \u002F\u002F ✅ FragmentResult API\n",[195,8137,8138],{"class":197,"line":2843},[195,8139,8140],{},"    setFragmentResult(\"requestKey\", bundleOf(\"key\" to value))\n",[195,8142,8143],{"class":197,"line":2848},[195,8144,8145],{},"    \u002F\u002F 接收方\n",[195,8147,8148],{"class":197,"line":2854},[195,8149,8150],{},"    setFragmentResultListener(\"requestKey\") { _, bundle ->\n",[195,8152,8153],{"class":197,"line":2860},[195,8154,8155],{},"        val value = bundle.getString(\"key\")\n",[195,8157,8158],{"class":197,"line":2866},[195,8159,2403],{},[195,8161,8162],{"class":197,"line":2872},[195,8163,552],{},[1890,8165],{},[32,8167,8169],{"id":8168},"q93-servicebroadcastreceivercontentprovider-的使用场景","Q9.3: Service、BroadcastReceiver、ContentProvider 的使用场景",[14,8171,8172],{},[125,8173,2077],{},[14,8175,8176],{},[125,8177,8178],{},"Service：",[186,8180,8182],{"className":6838,"code":8181,"language":6840,"meta":191,"style":191},"\u002F\u002F Foreground Service（长时间后台任务，如音乐播放、GPS 导航）\nclass MusicService : Service() {\n    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {\n        val notification = createNotification()\n        startForeground(NOTIFICATION_ID, notification) \u002F\u002F 必须显示通知\n        playMusic()\n        return START_STICKY \u002F\u002F 被杀后自动重启\n    }\n\n    override fun onBind(intent: Intent?): IBinder? = null\n}\n\n\u002F\u002F WorkManager（推荐替代后台 Service）\nval request = OneTimeWorkRequestBuilder\u003CUploadWorker>()\n    .setConstraints(Constraints.Builder()\n        .setRequiredNetworkType(NetworkType.CONNECTED)\n        .setRequiresBatteryNotLow(true)\n        .build())\n    .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, TimeUnit.SECONDS)\n    .build()\n\nWorkManager.getInstance(context).enqueue(request)\n",[136,8183,8184,8189,8194,8199,8204,8209,8214,8219,8223,8227,8232,8236,8240,8245,8250,8255,8260,8265,8270,8275,8280,8284],{"__ignoreMap":191},[195,8185,8186],{"class":197,"line":198},[195,8187,8188],{},"\u002F\u002F Foreground Service（长时间后台任务，如音乐播放、GPS 导航）\n",[195,8190,8191],{"class":197,"line":230},[195,8192,8193],{},"class MusicService : Service() {\n",[195,8195,8196],{"class":197,"line":251},[195,8197,8198],{},"    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {\n",[195,8200,8201],{"class":197,"line":272},[195,8202,8203],{},"        val notification = createNotification()\n",[195,8205,8206],{"class":197,"line":293},[195,8207,8208],{},"        startForeground(NOTIFICATION_ID, notification) \u002F\u002F 必须显示通知\n",[195,8210,8211],{"class":197,"line":562},[195,8212,8213],{},"        playMusic()\n",[195,8215,8216],{"class":197,"line":583},[195,8217,8218],{},"        return START_STICKY \u002F\u002F 被杀后自动重启\n",[195,8220,8221],{"class":197,"line":962},[195,8222,2403],{},[195,8224,8225],{"class":197,"line":968},[195,8226,1241],{"emptyLinePlaceholder":757},[195,8228,8229],{"class":197,"line":1274},[195,8230,8231],{},"    override fun onBind(intent: Intent?): IBinder? = null\n",[195,8233,8234],{"class":197,"line":1282},[195,8235,552],{},[195,8237,8238],{"class":197,"line":1295},[195,8239,1241],{"emptyLinePlaceholder":757},[195,8241,8242],{"class":197,"line":1309},[195,8243,8244],{},"\u002F\u002F WorkManager（推荐替代后台 Service）\n",[195,8246,8247],{"class":197,"line":2246},[195,8248,8249],{},"val request = OneTimeWorkRequestBuilder\u003CUploadWorker>()\n",[195,8251,8252],{"class":197,"line":1996},[195,8253,8254],{},"    .setConstraints(Constraints.Builder()\n",[195,8256,8257],{"class":197,"line":2257},[195,8258,8259],{},"        .setRequiredNetworkType(NetworkType.CONNECTED)\n",[195,8261,8262],{"class":197,"line":2262},[195,8263,8264],{},"        .setRequiresBatteryNotLow(true)\n",[195,8266,8267],{"class":197,"line":2267},[195,8268,8269],{},"        .build())\n",[195,8271,8272],{"class":197,"line":2273},[195,8273,8274],{},"    .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, TimeUnit.SECONDS)\n",[195,8276,8277],{"class":197,"line":2033},[195,8278,8279],{},"    .build()\n",[195,8281,8282],{"class":197,"line":2284},[195,8283,1241],{"emptyLinePlaceholder":757},[195,8285,8286],{"class":197,"line":2460},[195,8287,8288],{},"WorkManager.getInstance(context).enqueue(request)\n",[14,8290,8291],{},[125,8292,8293],{},"BroadcastReceiver：",[186,8295,8297],{"className":6838,"code":8296,"language":6840,"meta":191,"style":191},"\u002F\u002F 静态注册（AndroidManifest.xml，受限）\n\u002F\u002F 动态注册（推荐）\nval receiver = object : BroadcastReceiver() {\n    override fun onReceive(context: Context, intent: Intent) {\n        when (intent.action) {\n            Intent.ACTION_BATTERY_LOW -> showWarning()\n            ConnectivityManager.CONNECTIVITY_ACTION -> checkNetwork()\n        }\n    }\n}\n\n\u002F\u002F 注册\nregisterReceiver(receiver, IntentFilter(Intent.ACTION_BATTERY_LOW))\n\u002F\u002F 注销（避免泄漏）\nunregisterReceiver(receiver)\n",[136,8298,8299,8304,8309,8314,8319,8324,8329,8334,8338,8342,8346,8350,8355,8360,8365],{"__ignoreMap":191},[195,8300,8301],{"class":197,"line":198},[195,8302,8303],{},"\u002F\u002F 静态注册（AndroidManifest.xml，受限）\n",[195,8305,8306],{"class":197,"line":230},[195,8307,8308],{},"\u002F\u002F 动态注册（推荐）\n",[195,8310,8311],{"class":197,"line":251},[195,8312,8313],{},"val receiver = object : BroadcastReceiver() {\n",[195,8315,8316],{"class":197,"line":272},[195,8317,8318],{},"    override fun onReceive(context: Context, intent: Intent) {\n",[195,8320,8321],{"class":197,"line":293},[195,8322,8323],{},"        when (intent.action) {\n",[195,8325,8326],{"class":197,"line":562},[195,8327,8328],{},"            Intent.ACTION_BATTERY_LOW -> showWarning()\n",[195,8330,8331],{"class":197,"line":583},[195,8332,8333],{},"            ConnectivityManager.CONNECTIVITY_ACTION -> checkNetwork()\n",[195,8335,8336],{"class":197,"line":962},[195,8337,2887],{},[195,8339,8340],{"class":197,"line":968},[195,8341,2403],{},[195,8343,8344],{"class":197,"line":1274},[195,8345,552],{},[195,8347,8348],{"class":197,"line":1282},[195,8349,1241],{"emptyLinePlaceholder":757},[195,8351,8352],{"class":197,"line":1295},[195,8353,8354],{},"\u002F\u002F 注册\n",[195,8356,8357],{"class":197,"line":1309},[195,8358,8359],{},"registerReceiver(receiver, IntentFilter(Intent.ACTION_BATTERY_LOW))\n",[195,8361,8362],{"class":197,"line":2246},[195,8363,8364],{},"\u002F\u002F 注销（避免泄漏）\n",[195,8366,8367],{"class":197,"line":1996},[195,8368,8369],{},"unregisterReceiver(receiver)\n",[14,8371,8372],{},[125,8373,8374],{},"ContentProvider：",[186,8376,8378],{"className":6838,"code":8377,"language":6840,"meta":191,"style":191},"\u002F\u002F 跨应用数据共享\nclass MyProvider : ContentProvider() {\n    override fun query(uri: Uri, ...): Cursor? {\n        return when (uriMatcher.match(uri)) {\n            ITEMS -> database.query(\"items\", ...)\n            ITEM_ID -> database.query(\"items\", ..., \"id=${uri.lastPathSegment}\")\n            else -> null\n        }\n    }\n}\n\n\u002F\u002F 使用\nval cursor = contentResolver.query(\n    ContactsContract.Contacts.CONTENT_URI, null, null, null, null\n)\n",[136,8379,8380,8385,8390,8395,8400,8405,8410,8415,8419,8423,8427,8431,8435,8440,8445],{"__ignoreMap":191},[195,8381,8382],{"class":197,"line":198},[195,8383,8384],{},"\u002F\u002F 跨应用数据共享\n",[195,8386,8387],{"class":197,"line":230},[195,8388,8389],{},"class MyProvider : ContentProvider() {\n",[195,8391,8392],{"class":197,"line":251},[195,8393,8394],{},"    override fun query(uri: Uri, ...): Cursor? {\n",[195,8396,8397],{"class":197,"line":272},[195,8398,8399],{},"        return when (uriMatcher.match(uri)) {\n",[195,8401,8402],{"class":197,"line":293},[195,8403,8404],{},"            ITEMS -> database.query(\"items\", ...)\n",[195,8406,8407],{"class":197,"line":562},[195,8408,8409],{},"            ITEM_ID -> database.query(\"items\", ..., \"id=${uri.lastPathSegment}\")\n",[195,8411,8412],{"class":197,"line":583},[195,8413,8414],{},"            else -> null\n",[195,8416,8417],{"class":197,"line":962},[195,8418,2887],{},[195,8420,8421],{"class":197,"line":968},[195,8422,2403],{},[195,8424,8425],{"class":197,"line":1274},[195,8426,552],{},[195,8428,8429],{"class":197,"line":1282},[195,8430,1241],{"emptyLinePlaceholder":757},[195,8432,8433],{"class":197,"line":1295},[195,8434,3056],{},[195,8436,8437],{"class":197,"line":1309},[195,8438,8439],{},"val cursor = contentResolver.query(\n",[195,8441,8442],{"class":197,"line":2246},[195,8443,8444],{},"    ContactsContract.Contacts.CONTENT_URI, null, null, null, null\n",[195,8446,8447],{"class":197,"line":1996},[195,8448,410],{},[1890,8450],{},[18,8452,8454],{"id":8453},"_10-jetpack-组件","10. Jetpack 组件",[32,8456,8458],{"id":8457},"q101-jetpack-compose-的重组recomposition机制","Q10.1: Jetpack Compose 的重组（Recomposition）机制",[14,8460,8461],{},[125,8462,2077],{},[186,8464,8466],{"className":6838,"code":8465,"language":6840,"meta":191,"style":191},"\u002F\u002F Compose 核心：声明式 UI，状态变化触发重组\n@Composable\nfun Counter() {\n    var count by remember { mutableStateOf(0) }\n\n    Column {\n        Text(\"Count: $count\")  \u002F\u002F count 变化时重组\n        Button(onClick = { count++ }) {\n            Text(\"Increment\")\n        }\n    }\n}\n",[136,8467,8468,8473,8478,8483,8488,8492,8497,8502,8507,8512,8516,8520],{"__ignoreMap":191},[195,8469,8470],{"class":197,"line":198},[195,8471,8472],{},"\u002F\u002F Compose 核心：声明式 UI，状态变化触发重组\n",[195,8474,8475],{"class":197,"line":230},[195,8476,8477],{},"@Composable\n",[195,8479,8480],{"class":197,"line":251},[195,8481,8482],{},"fun Counter() {\n",[195,8484,8485],{"class":197,"line":272},[195,8486,8487],{},"    var count by remember { mutableStateOf(0) }\n",[195,8489,8490],{"class":197,"line":293},[195,8491,1241],{"emptyLinePlaceholder":757},[195,8493,8494],{"class":197,"line":562},[195,8495,8496],{},"    Column {\n",[195,8498,8499],{"class":197,"line":583},[195,8500,8501],{},"        Text(\"Count: $count\")  \u002F\u002F count 变化时重组\n",[195,8503,8504],{"class":197,"line":962},[195,8505,8506],{},"        Button(onClick = { count++ }) {\n",[195,8508,8509],{"class":197,"line":968},[195,8510,8511],{},"            Text(\"Increment\")\n",[195,8513,8514],{"class":197,"line":1274},[195,8515,2887],{},[195,8517,8518],{"class":197,"line":1282},[195,8519,2403],{},[195,8521,8522],{"class":197,"line":1295},[195,8523,552],{},[14,8525,8526],{},[125,8527,8528],{},"重组优化原则：",[186,8530,8532],{"className":6838,"code":8531,"language":6840,"meta":191,"style":191},"\u002F\u002F 1. Compose 是智能的：只重组读取了变化状态的 Composable\n@Composable\nfun Parent() {\n    var name by remember { mutableStateOf(\"\") }\n    var age by remember { mutableStateOf(0) }\n\n    NameDisplay(name)  \u002F\u002F 只有 name 变化时重组\n    AgeDisplay(age)    \u002F\u002F 只有 age 变化时重组\n}\n\n\u002F\u002F 2. remember：在重组中保持值\n@Composable\nfun ExpensiveCalculation(input: List\u003CInt>) {\n    \u002F\u002F ❌ 每次重组都计算\n    val result = input.sorted().take(10)\n\n    \u002F\u002F ✅ 只有 input 变化时重新计算\n    val result = remember(input) { input.sorted().take(10) }\n}\n\n\u002F\u002F 3. derivedStateOf：减少不必要的重组\n@Composable\nfun ItemList(items: List\u003CItem>) {\n    val listState = rememberLazyListState()\n\n    \u002F\u002F ❌ 每次滚动都重组\n    val showButton = listState.firstVisibleItemIndex > 0\n\n    \u002F\u002F ✅ 只在条件变化时重组\n    val showButton by remember {\n        derivedStateOf { listState.firstVisibleItemIndex > 0 }\n    }\n\n    if (showButton) {\n        ScrollToTopButton()\n    }\n}\n\n\u002F\u002F 4. 稳定性（Stability）\n\u002F\u002F Compose 跳过重组的条件：参数是 Stable 且 equals 返回 true\n\u002F\u002F data class 默认 Stable（所有属性都是 val 且类型 Stable）\ndata class User(val name: String, val age: Int)  \u002F\u002F ✅ Stable\n\n\u002F\u002F List\u002FMap 不是 Stable（可能被外部修改）\ndata class State(val items: List\u003CItem>)  \u002F\u002F ⚠️ 不 Stable\n\n\u002F\u002F 解决方案：使用 @Immutable 或 kotlinx.collections.immutable\n@Immutable\ndata class State(val items: ImmutableList\u003CItem>)  \u002F\u002F ✅ Stable\n",[136,8533,8534,8539,8543,8548,8553,8558,8562,8567,8572,8576,8580,8585,8589,8594,8599,8604,8608,8613,8618,8622,8626,8631,8635,8640,8645,8649,8654,8659,8663,8668,8673,8678,8682,8686,8691,8696,8700,8704,8708,8713,8718,8723,8728,8732,8737,8742,8746,8751,8756],{"__ignoreMap":191},[195,8535,8536],{"class":197,"line":198},[195,8537,8538],{},"\u002F\u002F 1. Compose 是智能的：只重组读取了变化状态的 Composable\n",[195,8540,8541],{"class":197,"line":230},[195,8542,8477],{},[195,8544,8545],{"class":197,"line":251},[195,8546,8547],{},"fun Parent() {\n",[195,8549,8550],{"class":197,"line":272},[195,8551,8552],{},"    var name by remember { mutableStateOf(\"\") }\n",[195,8554,8555],{"class":197,"line":293},[195,8556,8557],{},"    var age by remember { mutableStateOf(0) }\n",[195,8559,8560],{"class":197,"line":562},[195,8561,1241],{"emptyLinePlaceholder":757},[195,8563,8564],{"class":197,"line":583},[195,8565,8566],{},"    NameDisplay(name)  \u002F\u002F 只有 name 变化时重组\n",[195,8568,8569],{"class":197,"line":962},[195,8570,8571],{},"    AgeDisplay(age)    \u002F\u002F 只有 age 变化时重组\n",[195,8573,8574],{"class":197,"line":968},[195,8575,552],{},[195,8577,8578],{"class":197,"line":1274},[195,8579,1241],{"emptyLinePlaceholder":757},[195,8581,8582],{"class":197,"line":1282},[195,8583,8584],{},"\u002F\u002F 2. remember：在重组中保持值\n",[195,8586,8587],{"class":197,"line":1295},[195,8588,8477],{},[195,8590,8591],{"class":197,"line":1309},[195,8592,8593],{},"fun ExpensiveCalculation(input: List\u003CInt>) {\n",[195,8595,8596],{"class":197,"line":2246},[195,8597,8598],{},"    \u002F\u002F ❌ 每次重组都计算\n",[195,8600,8601],{"class":197,"line":1996},[195,8602,8603],{},"    val result = input.sorted().take(10)\n",[195,8605,8606],{"class":197,"line":2257},[195,8607,1241],{"emptyLinePlaceholder":757},[195,8609,8610],{"class":197,"line":2262},[195,8611,8612],{},"    \u002F\u002F ✅ 只有 input 变化时重新计算\n",[195,8614,8615],{"class":197,"line":2267},[195,8616,8617],{},"    val result = remember(input) { input.sorted().take(10) }\n",[195,8619,8620],{"class":197,"line":2273},[195,8621,552],{},[195,8623,8624],{"class":197,"line":2033},[195,8625,1241],{"emptyLinePlaceholder":757},[195,8627,8628],{"class":197,"line":2284},[195,8629,8630],{},"\u002F\u002F 3. derivedStateOf：减少不必要的重组\n",[195,8632,8633],{"class":197,"line":2460},[195,8634,8477],{},[195,8636,8637],{"class":197,"line":2466},[195,8638,8639],{},"fun ItemList(items: List\u003CItem>) {\n",[195,8641,8642],{"class":197,"line":2472},[195,8643,8644],{},"    val listState = rememberLazyListState()\n",[195,8646,8647],{"class":197,"line":2780},[195,8648,1241],{"emptyLinePlaceholder":757},[195,8650,8651],{"class":197,"line":2786},[195,8652,8653],{},"    \u002F\u002F ❌ 每次滚动都重组\n",[195,8655,8656],{"class":197,"line":2792},[195,8657,8658],{},"    val showButton = listState.firstVisibleItemIndex > 0\n",[195,8660,8661],{"class":197,"line":2798},[195,8662,1241],{"emptyLinePlaceholder":757},[195,8664,8665],{"class":197,"line":2804},[195,8666,8667],{},"    \u002F\u002F ✅ 只在条件变化时重组\n",[195,8669,8670],{"class":197,"line":2810},[195,8671,8672],{},"    val showButton by remember {\n",[195,8674,8675],{"class":197,"line":2815},[195,8676,8677],{},"        derivedStateOf { listState.firstVisibleItemIndex > 0 }\n",[195,8679,8680],{"class":197,"line":2820},[195,8681,2403],{},[195,8683,8684],{"class":197,"line":2825},[195,8685,1241],{"emptyLinePlaceholder":757},[195,8687,8688],{"class":197,"line":2831},[195,8689,8690],{},"    if (showButton) {\n",[195,8692,8693],{"class":197,"line":2837},[195,8694,8695],{},"        ScrollToTopButton()\n",[195,8697,8698],{"class":197,"line":2843},[195,8699,2403],{},[195,8701,8702],{"class":197,"line":2848},[195,8703,552],{},[195,8705,8706],{"class":197,"line":2854},[195,8707,1241],{"emptyLinePlaceholder":757},[195,8709,8710],{"class":197,"line":2860},[195,8711,8712],{},"\u002F\u002F 4. 稳定性（Stability）\n",[195,8714,8715],{"class":197,"line":2866},[195,8716,8717],{},"\u002F\u002F Compose 跳过重组的条件：参数是 Stable 且 equals 返回 true\n",[195,8719,8720],{"class":197,"line":2872},[195,8721,8722],{},"\u002F\u002F data class 默认 Stable（所有属性都是 val 且类型 Stable）\n",[195,8724,8725],{"class":197,"line":2878},[195,8726,8727],{},"data class User(val name: String, val age: Int)  \u002F\u002F ✅ Stable\n",[195,8729,8730],{"class":197,"line":2884},[195,8731,1241],{"emptyLinePlaceholder":757},[195,8733,8734],{"class":197,"line":2890},[195,8735,8736],{},"\u002F\u002F List\u002FMap 不是 Stable（可能被外部修改）\n",[195,8738,8739],{"class":197,"line":2895},[195,8740,8741],{},"data class State(val items: List\u003CItem>)  \u002F\u002F ⚠️ 不 Stable\n",[195,8743,8744],{"class":197,"line":2900},[195,8745,1241],{"emptyLinePlaceholder":757},[195,8747,8748],{"class":197,"line":2905},[195,8749,8750],{},"\u002F\u002F 解决方案：使用 @Immutable 或 kotlinx.collections.immutable\n",[195,8752,8753],{"class":197,"line":2911},[195,8754,8755],{},"@Immutable\n",[195,8757,8758],{"class":197,"line":2917},[195,8759,8760],{},"data class State(val items: ImmutableList\u003CItem>)  \u002F\u002F ✅ Stable\n",[1890,8762],{},[32,8764,8766],{"id":8765},"q102-viewmodel-livedatastateflow-repository-模式","Q10.2: ViewModel + LiveData\u002FStateFlow + Repository 模式",[14,8768,8769],{},[125,8770,2077],{},[186,8772,8774],{"className":6838,"code":8773,"language":6840,"meta":191,"style":191},"\u002F\u002F Repository\nclass UserRepository(\n    private val api: UserApi,\n    private val dao: UserDao\n) {\n    fun getUsers(): Flow\u003CList\u003CUser>> = dao.observeAll()  \u002F\u002F Room 返回 Flow\n\n    suspend fun refresh() {\n        val users = api.fetchUsers()\n        dao.insertAll(users)\n    }\n}\n\n\u002F\u002F ViewModel（使用 StateFlow）\nclass UserViewModel(private val repository: UserRepository) : ViewModel() {\n\n    private val _uiState = MutableStateFlow\u003CUiState>(UiState.Loading)\n    val uiState: StateFlow\u003CUiState> = _uiState.asStateFlow()\n\n    init {\n        viewModelScope.launch {\n            repository.getUsers()\n                .catch { _uiState.value = UiState.Error(it.message ?: \"Unknown\") }\n                .collect { users -> _uiState.value = UiState.Success(users) }\n        }\n    }\n\n    fun refresh() {\n        viewModelScope.launch {\n            _uiState.value = UiState.Loading\n            try {\n                repository.refresh()\n            } catch (e: Exception) {\n                _uiState.value = UiState.Error(e.message ?: \"Unknown\")\n            }\n        }\n    }\n}\n\nsealed class UiState {\n    data object Loading : UiState()\n    data class Success(val users: List\u003CUser>) : UiState()\n    data class Error(val message: String) : UiState()\n}\n\n\u002F\u002F Compose UI\n@Composable\nfun UserScreen(viewModel: UserViewModel = viewModel()) {\n    val uiState by viewModel.uiState.collectAsStateWithLifecycle()\n\n    when (val state = uiState) {\n        is UiState.Loading -> CircularProgressIndicator()\n        is UiState.Success -> UserList(state.users)\n        is UiState.Error -> ErrorView(state.message, onRetry = { viewModel.refresh() })\n    }\n}\n",[136,8775,8776,8781,8786,8791,8796,8800,8805,8809,8814,8819,8824,8828,8832,8836,8841,8846,8850,8855,8860,8864,8869,8873,8878,8883,8888,8892,8896,8900,8905,8909,8914,8919,8924,8929,8934,8938,8942,8946,8950,8954,8959,8964,8969,8974,8978,8982,8987,8991,8996,9001,9005,9010,9015,9020,9025,9029],{"__ignoreMap":191},[195,8777,8778],{"class":197,"line":198},[195,8779,8780],{},"\u002F\u002F Repository\n",[195,8782,8783],{"class":197,"line":230},[195,8784,8785],{},"class UserRepository(\n",[195,8787,8788],{"class":197,"line":251},[195,8789,8790],{},"    private val api: UserApi,\n",[195,8792,8793],{"class":197,"line":272},[195,8794,8795],{},"    private val dao: UserDao\n",[195,8797,8798],{"class":197,"line":293},[195,8799,644],{},[195,8801,8802],{"class":197,"line":562},[195,8803,8804],{},"    fun getUsers(): Flow\u003CList\u003CUser>> = dao.observeAll()  \u002F\u002F Room 返回 Flow\n",[195,8806,8807],{"class":197,"line":583},[195,8808,1241],{"emptyLinePlaceholder":757},[195,8810,8811],{"class":197,"line":962},[195,8812,8813],{},"    suspend fun refresh() {\n",[195,8815,8816],{"class":197,"line":968},[195,8817,8818],{},"        val users = api.fetchUsers()\n",[195,8820,8821],{"class":197,"line":1274},[195,8822,8823],{},"        dao.insertAll(users)\n",[195,8825,8826],{"class":197,"line":1282},[195,8827,2403],{},[195,8829,8830],{"class":197,"line":1295},[195,8831,552],{},[195,8833,8834],{"class":197,"line":1309},[195,8835,1241],{"emptyLinePlaceholder":757},[195,8837,8838],{"class":197,"line":2246},[195,8839,8840],{},"\u002F\u002F ViewModel（使用 StateFlow）\n",[195,8842,8843],{"class":197,"line":1996},[195,8844,8845],{},"class UserViewModel(private val repository: UserRepository) : ViewModel() {\n",[195,8847,8848],{"class":197,"line":2257},[195,8849,1241],{"emptyLinePlaceholder":757},[195,8851,8852],{"class":197,"line":2262},[195,8853,8854],{},"    private val _uiState = MutableStateFlow\u003CUiState>(UiState.Loading)\n",[195,8856,8857],{"class":197,"line":2267},[195,8858,8859],{},"    val uiState: StateFlow\u003CUiState> = _uiState.asStateFlow()\n",[195,8861,8862],{"class":197,"line":2273},[195,8863,1241],{"emptyLinePlaceholder":757},[195,8865,8866],{"class":197,"line":2033},[195,8867,8868],{},"    init {\n",[195,8870,8871],{"class":197,"line":2284},[195,8872,7840],{},[195,8874,8875],{"class":197,"line":2460},[195,8876,8877],{},"            repository.getUsers()\n",[195,8879,8880],{"class":197,"line":2466},[195,8881,8882],{},"                .catch { _uiState.value = UiState.Error(it.message ?: \"Unknown\") }\n",[195,8884,8885],{"class":197,"line":2472},[195,8886,8887],{},"                .collect { users -> _uiState.value = UiState.Success(users) }\n",[195,8889,8890],{"class":197,"line":2780},[195,8891,2887],{},[195,8893,8894],{"class":197,"line":2786},[195,8895,2403],{},[195,8897,8898],{"class":197,"line":2792},[195,8899,1241],{"emptyLinePlaceholder":757},[195,8901,8902],{"class":197,"line":2798},[195,8903,8904],{},"    fun refresh() {\n",[195,8906,8907],{"class":197,"line":2804},[195,8908,7840],{},[195,8910,8911],{"class":197,"line":2810},[195,8912,8913],{},"            _uiState.value = UiState.Loading\n",[195,8915,8916],{"class":197,"line":2815},[195,8917,8918],{},"            try {\n",[195,8920,8921],{"class":197,"line":2820},[195,8922,8923],{},"                repository.refresh()\n",[195,8925,8926],{"class":197,"line":2825},[195,8927,8928],{},"            } catch (e: Exception) {\n",[195,8930,8931],{"class":197,"line":2831},[195,8932,8933],{},"                _uiState.value = UiState.Error(e.message ?: \"Unknown\")\n",[195,8935,8936],{"class":197,"line":2837},[195,8937,5781],{},[195,8939,8940],{"class":197,"line":2843},[195,8941,2887],{},[195,8943,8944],{"class":197,"line":2848},[195,8945,2403],{},[195,8947,8948],{"class":197,"line":2854},[195,8949,552],{},[195,8951,8952],{"class":197,"line":2860},[195,8953,1241],{"emptyLinePlaceholder":757},[195,8955,8956],{"class":197,"line":2866},[195,8957,8958],{},"sealed class UiState {\n",[195,8960,8961],{"class":197,"line":2872},[195,8962,8963],{},"    data object Loading : UiState()\n",[195,8965,8966],{"class":197,"line":2878},[195,8967,8968],{},"    data class Success(val users: List\u003CUser>) : UiState()\n",[195,8970,8971],{"class":197,"line":2884},[195,8972,8973],{},"    data class Error(val message: String) : UiState()\n",[195,8975,8976],{"class":197,"line":2890},[195,8977,552],{},[195,8979,8980],{"class":197,"line":2895},[195,8981,1241],{"emptyLinePlaceholder":757},[195,8983,8984],{"class":197,"line":2900},[195,8985,8986],{},"\u002F\u002F Compose UI\n",[195,8988,8989],{"class":197,"line":2905},[195,8990,8477],{},[195,8992,8993],{"class":197,"line":2911},[195,8994,8995],{},"fun UserScreen(viewModel: UserViewModel = viewModel()) {\n",[195,8997,8998],{"class":197,"line":2917},[195,8999,9000],{},"    val uiState by viewModel.uiState.collectAsStateWithLifecycle()\n",[195,9002,9003],{"class":197,"line":2923},[195,9004,1241],{"emptyLinePlaceholder":757},[195,9006,9007],{"class":197,"line":2929},[195,9008,9009],{},"    when (val state = uiState) {\n",[195,9011,9012],{"class":197,"line":2934},[195,9013,9014],{},"        is UiState.Loading -> CircularProgressIndicator()\n",[195,9016,9017],{"class":197,"line":2939},[195,9018,9019],{},"        is UiState.Success -> UserList(state.users)\n",[195,9021,9022],{"class":197,"line":2945},[195,9023,9024],{},"        is UiState.Error -> ErrorView(state.message, onRetry = { viewModel.refresh() })\n",[195,9026,9027],{"class":197,"line":2951},[195,9028,2403],{},[195,9030,9031],{"class":197,"line":2957},[195,9032,552],{},[14,9034,9035],{},[125,9036,9037],{},"LiveData vs StateFlow：",[36,9039,9040,9052],{},[39,9041,9042],{},[42,9043,9044,9046,9049],{},[45,9045],{},[45,9047,9048],{},"LiveData",[45,9050,9051],{},"StateFlow",[52,9053,9054,9068,9079,9090,9101],{},[42,9055,9056,9059,9062],{},[57,9057,9058],{},"生命周期感知",[57,9060,9061],{},"自动",[57,9063,9064,9065],{},"需要 ",[136,9066,9067],{},"collectAsStateWithLifecycle()",[42,9069,9070,9073,9076],{},[57,9071,9072],{},"初始值",[57,9074,9075],{},"可选",[57,9077,9078],{},"必须有初始值",[42,9080,9081,9084,9087],{},[57,9082,9083],{},"纯 Kotlin",[57,9085,9086],{},"需要 Android 依赖",[57,9088,9089],{},"纯 Kotlin，跨平台",[42,9091,9092,9095,9098],{},[57,9093,9094],{},"操作符",[57,9096,9097],{},"有限（map\u002FswitchMap）",[57,9099,9100],{},"完整 Flow 操作符",[42,9102,9103,9106,9109],{},[57,9104,9105],{},"推荐",[57,9107,9108],{},"旧项目维护",[57,9110,9111],{},"新项目首选",[1890,9113],{},[32,9115,9117],{"id":9116},"q103-room-数据库的使用和优化","Q10.3: Room 数据库的使用和优化",[14,9119,9120],{},[125,9121,2077],{},[186,9123,9125],{"className":6838,"code":9124,"language":6840,"meta":191,"style":191},"\u002F\u002F Entity\n@Entity(tableName = \"users\")\ndata class UserEntity(\n    @PrimaryKey val id: String,\n    @ColumnInfo(name = \"display_name\") val name: String,\n    val email: String,\n    @ColumnInfo(index = true) val createdAt: Long  \u002F\u002F 添加索引\n)\n\n\u002F\u002F DAO\n@Dao\ninterface UserDao {\n    @Query(\"SELECT * FROM users ORDER BY created_at DESC\")\n    fun observeAll(): Flow\u003CList\u003CUserEntity>>  \u002F\u002F 响应式查询\n\n    @Query(\"SELECT * FROM users WHERE id = :userId\")\n    suspend fun getById(userId: String): UserEntity?\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun insertAll(users: List\u003CUserEntity>)\n\n    @Transaction  \u002F\u002F 事务\n    suspend fun replaceAll(users: List\u003CUserEntity>) {\n        deleteAll()\n        insertAll(users)\n    }\n\n    @Query(\"DELETE FROM users\")\n    suspend fun deleteAll()\n}\n\n\u002F\u002F Database\n@Database(entities = [UserEntity::class], version = 2)\n@TypeConverters(Converters::class)\nabstract class AppDatabase : RoomDatabase() {\n    abstract fun userDao(): UserDao\n}\n\n\u002F\u002F 类型转换器\nclass Converters {\n    @TypeConverter\n    fun fromTimestamp(value: Long?): Date? = value?.let { Date(it) }\n\n    @TypeConverter\n    fun dateToTimestamp(date: Date?): Long? = date?.time\n}\n\n\u002F\u002F 数据库迁移\nval MIGRATION_1_2 = object : Migration(1, 2) {\n    override fun migrate(db: SupportSQLiteDatabase) {\n        db.execSQL(\"ALTER TABLE users ADD COLUMN email TEXT NOT NULL DEFAULT ''\")\n    }\n}\n\n\u002F\u002F 构建\nRoom.databaseBuilder(context, AppDatabase::class.java, \"app.db\")\n    .addMigrations(MIGRATION_1_2)\n    .build()\n",[136,9126,9127,9132,9137,9142,9147,9152,9157,9162,9166,9170,9175,9180,9185,9190,9195,9199,9204,9209,9213,9218,9223,9227,9232,9237,9242,9247,9251,9255,9260,9265,9269,9273,9278,9283,9288,9293,9298,9302,9306,9311,9316,9321,9326,9330,9334,9339,9343,9347,9352,9357,9362,9367,9371,9375,9379,9384,9389,9394],{"__ignoreMap":191},[195,9128,9129],{"class":197,"line":198},[195,9130,9131],{},"\u002F\u002F Entity\n",[195,9133,9134],{"class":197,"line":230},[195,9135,9136],{},"@Entity(tableName = \"users\")\n",[195,9138,9139],{"class":197,"line":251},[195,9140,9141],{},"data class UserEntity(\n",[195,9143,9144],{"class":197,"line":272},[195,9145,9146],{},"    @PrimaryKey val id: String,\n",[195,9148,9149],{"class":197,"line":293},[195,9150,9151],{},"    @ColumnInfo(name = \"display_name\") val name: String,\n",[195,9153,9154],{"class":197,"line":562},[195,9155,9156],{},"    val email: String,\n",[195,9158,9159],{"class":197,"line":583},[195,9160,9161],{},"    @ColumnInfo(index = true) val createdAt: Long  \u002F\u002F 添加索引\n",[195,9163,9164],{"class":197,"line":962},[195,9165,410],{},[195,9167,9168],{"class":197,"line":968},[195,9169,1241],{"emptyLinePlaceholder":757},[195,9171,9172],{"class":197,"line":1274},[195,9173,9174],{},"\u002F\u002F DAO\n",[195,9176,9177],{"class":197,"line":1282},[195,9178,9179],{},"@Dao\n",[195,9181,9182],{"class":197,"line":1295},[195,9183,9184],{},"interface UserDao {\n",[195,9186,9187],{"class":197,"line":1309},[195,9188,9189],{},"    @Query(\"SELECT * FROM users ORDER BY created_at DESC\")\n",[195,9191,9192],{"class":197,"line":2246},[195,9193,9194],{},"    fun observeAll(): Flow\u003CList\u003CUserEntity>>  \u002F\u002F 响应式查询\n",[195,9196,9197],{"class":197,"line":1996},[195,9198,1241],{"emptyLinePlaceholder":757},[195,9200,9201],{"class":197,"line":2257},[195,9202,9203],{},"    @Query(\"SELECT * FROM users WHERE id = :userId\")\n",[195,9205,9206],{"class":197,"line":2262},[195,9207,9208],{},"    suspend fun getById(userId: String): UserEntity?\n",[195,9210,9211],{"class":197,"line":2267},[195,9212,1241],{"emptyLinePlaceholder":757},[195,9214,9215],{"class":197,"line":2273},[195,9216,9217],{},"    @Insert(onConflict = OnConflictStrategy.REPLACE)\n",[195,9219,9220],{"class":197,"line":2033},[195,9221,9222],{},"    suspend fun insertAll(users: List\u003CUserEntity>)\n",[195,9224,9225],{"class":197,"line":2284},[195,9226,1241],{"emptyLinePlaceholder":757},[195,9228,9229],{"class":197,"line":2460},[195,9230,9231],{},"    @Transaction  \u002F\u002F 事务\n",[195,9233,9234],{"class":197,"line":2466},[195,9235,9236],{},"    suspend fun replaceAll(users: List\u003CUserEntity>) {\n",[195,9238,9239],{"class":197,"line":2472},[195,9240,9241],{},"        deleteAll()\n",[195,9243,9244],{"class":197,"line":2780},[195,9245,9246],{},"        insertAll(users)\n",[195,9248,9249],{"class":197,"line":2786},[195,9250,2403],{},[195,9252,9253],{"class":197,"line":2792},[195,9254,1241],{"emptyLinePlaceholder":757},[195,9256,9257],{"class":197,"line":2798},[195,9258,9259],{},"    @Query(\"DELETE FROM users\")\n",[195,9261,9262],{"class":197,"line":2804},[195,9263,9264],{},"    suspend fun deleteAll()\n",[195,9266,9267],{"class":197,"line":2810},[195,9268,552],{},[195,9270,9271],{"class":197,"line":2815},[195,9272,1241],{"emptyLinePlaceholder":757},[195,9274,9275],{"class":197,"line":2820},[195,9276,9277],{},"\u002F\u002F Database\n",[195,9279,9280],{"class":197,"line":2825},[195,9281,9282],{},"@Database(entities = [UserEntity::class], version = 2)\n",[195,9284,9285],{"class":197,"line":2831},[195,9286,9287],{},"@TypeConverters(Converters::class)\n",[195,9289,9290],{"class":197,"line":2837},[195,9291,9292],{},"abstract class AppDatabase : RoomDatabase() {\n",[195,9294,9295],{"class":197,"line":2843},[195,9296,9297],{},"    abstract fun userDao(): UserDao\n",[195,9299,9300],{"class":197,"line":2848},[195,9301,552],{},[195,9303,9304],{"class":197,"line":2854},[195,9305,1241],{"emptyLinePlaceholder":757},[195,9307,9308],{"class":197,"line":2860},[195,9309,9310],{},"\u002F\u002F 类型转换器\n",[195,9312,9313],{"class":197,"line":2866},[195,9314,9315],{},"class Converters {\n",[195,9317,9318],{"class":197,"line":2872},[195,9319,9320],{},"    @TypeConverter\n",[195,9322,9323],{"class":197,"line":2878},[195,9324,9325],{},"    fun fromTimestamp(value: Long?): Date? = value?.let { Date(it) }\n",[195,9327,9328],{"class":197,"line":2884},[195,9329,1241],{"emptyLinePlaceholder":757},[195,9331,9332],{"class":197,"line":2890},[195,9333,9320],{},[195,9335,9336],{"class":197,"line":2895},[195,9337,9338],{},"    fun dateToTimestamp(date: Date?): Long? = date?.time\n",[195,9340,9341],{"class":197,"line":2900},[195,9342,552],{},[195,9344,9345],{"class":197,"line":2905},[195,9346,1241],{"emptyLinePlaceholder":757},[195,9348,9349],{"class":197,"line":2911},[195,9350,9351],{},"\u002F\u002F 数据库迁移\n",[195,9353,9354],{"class":197,"line":2917},[195,9355,9356],{},"val MIGRATION_1_2 = object : Migration(1, 2) {\n",[195,9358,9359],{"class":197,"line":2923},[195,9360,9361],{},"    override fun migrate(db: SupportSQLiteDatabase) {\n",[195,9363,9364],{"class":197,"line":2929},[195,9365,9366],{},"        db.execSQL(\"ALTER TABLE users ADD COLUMN email TEXT NOT NULL DEFAULT ''\")\n",[195,9368,9369],{"class":197,"line":2934},[195,9370,2403],{},[195,9372,9373],{"class":197,"line":2939},[195,9374,552],{},[195,9376,9377],{"class":197,"line":2945},[195,9378,1241],{"emptyLinePlaceholder":757},[195,9380,9381],{"class":197,"line":2951},[195,9382,9383],{},"\u002F\u002F 构建\n",[195,9385,9386],{"class":197,"line":2957},[195,9387,9388],{},"Room.databaseBuilder(context, AppDatabase::class.java, \"app.db\")\n",[195,9390,9391],{"class":197,"line":2963},[195,9392,9393],{},"    .addMigrations(MIGRATION_1_2)\n",[195,9395,9397],{"class":197,"line":9396},58,[195,9398,8279],{},[1890,9400],{},[18,9402,9404],{"id":9403},"_11-并发编程coroutines","11. 并发编程（Coroutines）",[32,9406,9408],{"id":9407},"q111-kotlin-coroutines-的核心概念","Q11.1: Kotlin Coroutines 的核心概念",[14,9410,9411],{},[125,9412,2077],{},[186,9414,9416],{"className":6838,"code":9415,"language":6840,"meta":191,"style":191},"\u002F\u002F CoroutineScope：定义协程的生命周期\nclass MyViewModel : ViewModel() {\n    \u002F\u002F viewModelScope：ViewModel 销毁时自动取消\n    fun load() = viewModelScope.launch {\n        val data = fetchData()  \u002F\u002F 挂起，不阻塞线程\n        _state.value = data\n    }\n}\n\n\u002F\u002F Dispatcher：决定协程在哪个线程执行\nviewModelScope.launch(Dispatchers.Main) {      \u002F\u002F 主线程\n    val data = withContext(Dispatchers.IO) {    \u002F\u002F 切到 IO 线程\n        api.fetchData()                        \u002F\u002F 网络请求\n    }\n    updateUI(data)                             \u002F\u002F 回到主线程\n}\n\n\u002F\u002F Dispatchers 对比\n\u002F\u002F Main：主线程，UI 操作\n\u002F\u002F IO：IO 密集型（网络、文件），线程池较大\n\u002F\u002F Default：CPU 密集型（排序、解析），线程数 = CPU 核数\n\u002F\u002F Unconfined：不切换线程（不推荐常规使用）\n",[136,9417,9418,9423,9427,9432,9437,9442,9447,9451,9455,9459,9464,9469,9474,9479,9483,9488,9492,9496,9501,9506,9511,9516],{"__ignoreMap":191},[195,9419,9420],{"class":197,"line":198},[195,9421,9422],{},"\u002F\u002F CoroutineScope：定义协程的生命周期\n",[195,9424,9425],{"class":197,"line":230},[195,9426,7816],{},[195,9428,9429],{"class":197,"line":251},[195,9430,9431],{},"    \u002F\u002F viewModelScope：ViewModel 销毁时自动取消\n",[195,9433,9434],{"class":197,"line":272},[195,9435,9436],{},"    fun load() = viewModelScope.launch {\n",[195,9438,9439],{"class":197,"line":293},[195,9440,9441],{},"        val data = fetchData()  \u002F\u002F 挂起，不阻塞线程\n",[195,9443,9444],{"class":197,"line":562},[195,9445,9446],{},"        _state.value = data\n",[195,9448,9449],{"class":197,"line":583},[195,9450,2403],{},[195,9452,9453],{"class":197,"line":962},[195,9454,552],{},[195,9456,9457],{"class":197,"line":968},[195,9458,1241],{"emptyLinePlaceholder":757},[195,9460,9461],{"class":197,"line":1274},[195,9462,9463],{},"\u002F\u002F Dispatcher：决定协程在哪个线程执行\n",[195,9465,9466],{"class":197,"line":1282},[195,9467,9468],{},"viewModelScope.launch(Dispatchers.Main) {      \u002F\u002F 主线程\n",[195,9470,9471],{"class":197,"line":1295},[195,9472,9473],{},"    val data = withContext(Dispatchers.IO) {    \u002F\u002F 切到 IO 线程\n",[195,9475,9476],{"class":197,"line":1309},[195,9477,9478],{},"        api.fetchData()                        \u002F\u002F 网络请求\n",[195,9480,9481],{"class":197,"line":2246},[195,9482,2403],{},[195,9484,9485],{"class":197,"line":1996},[195,9486,9487],{},"    updateUI(data)                             \u002F\u002F 回到主线程\n",[195,9489,9490],{"class":197,"line":2257},[195,9491,552],{},[195,9493,9494],{"class":197,"line":2262},[195,9495,1241],{"emptyLinePlaceholder":757},[195,9497,9498],{"class":197,"line":2267},[195,9499,9500],{},"\u002F\u002F Dispatchers 对比\n",[195,9502,9503],{"class":197,"line":2273},[195,9504,9505],{},"\u002F\u002F Main：主线程，UI 操作\n",[195,9507,9508],{"class":197,"line":2033},[195,9509,9510],{},"\u002F\u002F IO：IO 密集型（网络、文件），线程池较大\n",[195,9512,9513],{"class":197,"line":2284},[195,9514,9515],{},"\u002F\u002F Default：CPU 密集型（排序、解析），线程数 = CPU 核数\n",[195,9517,9518],{"class":197,"line":2460},[195,9519,9520],{},"\u002F\u002F Unconfined：不切换线程（不推荐常规使用）\n",[14,9522,9523],{},[125,9524,9525],{},"结构化并发：",[186,9527,9529],{"className":6838,"code":9528,"language":6840,"meta":191,"style":191},"\u002F\u002F 并行请求\nsuspend fun loadDashboard(): Dashboard = coroutineScope {\n    val user = async { fetchUser() }\n    val posts = async { fetchPosts() }\n    val stats = async { fetchStats() }\n\n    Dashboard(\n        user = user.await(),\n        posts = posts.await(),\n        stats = stats.await()\n    )\n    \u002F\u002F 任何一个失败，其他自动取消\n}\n\n\u002F\u002F 异常处理\nviewModelScope.launch {\n    try {\n        val result = repository.fetchData()\n        _state.value = UiState.Success(result)\n    } catch (e: CancellationException) {\n        throw e  \u002F\u002F 不要吞掉 CancellationException！\n    } catch (e: Exception) {\n        _state.value = UiState.Error(e.message)\n    }\n}\n\n\u002F\u002F SupervisorJob：子协程失败不影响其他子协程\nval scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)\nscope.launch { task1() }  \u002F\u002F task1 失败不会取消 task2\nscope.launch { task2() }\n",[136,9530,9531,9536,9541,9546,9551,9556,9560,9565,9570,9575,9580,9584,9589,9593,9597,9602,9607,9612,9617,9622,9627,9632,9637,9642,9646,9650,9654,9659,9664,9669],{"__ignoreMap":191},[195,9532,9533],{"class":197,"line":198},[195,9534,9535],{},"\u002F\u002F 并行请求\n",[195,9537,9538],{"class":197,"line":230},[195,9539,9540],{},"suspend fun loadDashboard(): Dashboard = coroutineScope {\n",[195,9542,9543],{"class":197,"line":251},[195,9544,9545],{},"    val user = async { fetchUser() }\n",[195,9547,9548],{"class":197,"line":272},[195,9549,9550],{},"    val posts = async { fetchPosts() }\n",[195,9552,9553],{"class":197,"line":293},[195,9554,9555],{},"    val stats = async { fetchStats() }\n",[195,9557,9558],{"class":197,"line":562},[195,9559,1241],{"emptyLinePlaceholder":757},[195,9561,9562],{"class":197,"line":583},[195,9563,9564],{},"    Dashboard(\n",[195,9566,9567],{"class":197,"line":962},[195,9568,9569],{},"        user = user.await(),\n",[195,9571,9572],{"class":197,"line":968},[195,9573,9574],{},"        posts = posts.await(),\n",[195,9576,9577],{"class":197,"line":1274},[195,9578,9579],{},"        stats = stats.await()\n",[195,9581,9582],{"class":197,"line":1282},[195,9583,4902],{},[195,9585,9586],{"class":197,"line":1295},[195,9587,9588],{},"    \u002F\u002F 任何一个失败，其他自动取消\n",[195,9590,9591],{"class":197,"line":1309},[195,9592,552],{},[195,9594,9595],{"class":197,"line":2246},[195,9596,1241],{"emptyLinePlaceholder":757},[195,9598,9599],{"class":197,"line":1996},[195,9600,9601],{},"\u002F\u002F 异常处理\n",[195,9603,9604],{"class":197,"line":2257},[195,9605,9606],{},"viewModelScope.launch {\n",[195,9608,9609],{"class":197,"line":2262},[195,9610,9611],{},"    try {\n",[195,9613,9614],{"class":197,"line":2267},[195,9615,9616],{},"        val result = repository.fetchData()\n",[195,9618,9619],{"class":197,"line":2273},[195,9620,9621],{},"        _state.value = UiState.Success(result)\n",[195,9623,9624],{"class":197,"line":2033},[195,9625,9626],{},"    } catch (e: CancellationException) {\n",[195,9628,9629],{"class":197,"line":2284},[195,9630,9631],{},"        throw e  \u002F\u002F 不要吞掉 CancellationException！\n",[195,9633,9634],{"class":197,"line":2460},[195,9635,9636],{},"    } catch (e: Exception) {\n",[195,9638,9639],{"class":197,"line":2466},[195,9640,9641],{},"        _state.value = UiState.Error(e.message)\n",[195,9643,9644],{"class":197,"line":2472},[195,9645,2403],{},[195,9647,9648],{"class":197,"line":2780},[195,9649,552],{},[195,9651,9652],{"class":197,"line":2786},[195,9653,1241],{"emptyLinePlaceholder":757},[195,9655,9656],{"class":197,"line":2792},[195,9657,9658],{},"\u002F\u002F SupervisorJob：子协程失败不影响其他子协程\n",[195,9660,9661],{"class":197,"line":2798},[195,9662,9663],{},"val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)\n",[195,9665,9666],{"class":197,"line":2804},[195,9667,9668],{},"scope.launch { task1() }  \u002F\u002F task1 失败不会取消 task2\n",[195,9670,9671],{"class":197,"line":2810},[195,9672,9673],{},"scope.launch { task2() }\n",[1890,9675],{},[32,9677,9679],{"id":9678},"q112-flow-的冷流与热流以及-stateflow-sharedflow","Q11.2: Flow 的冷流与热流，以及 StateFlow \u002F SharedFlow",[14,9681,9682],{},[125,9683,2077],{},[186,9685,9687],{"className":6838,"code":9686,"language":6840,"meta":191,"style":191},"\u002F\u002F Cold Flow（冷流）：每个收集者独立执行\nfun fetchItems(): Flow\u003CItem> = flow {\n    for (item in api.getItems()) {\n        emit(item)\n        delay(100)\n    }\n}\n\n\u002F\u002F 每次 collect 都会重新执行 flow { } 块\nfetchItems().collect { item -> println(item) } \u002F\u002F 执行一次\nfetchItems().collect { item -> println(item) } \u002F\u002F 再执行一次\n\n\u002F\u002F Hot Flow（热流）：多个收集者共享\n\u002F\u002F StateFlow：始终持有当前值，新收集者立即获得最新值\nprivate val _state = MutableStateFlow(UiState.Loading)\nval state: StateFlow\u003CUiState> = _state.asStateFlow()\n\n\u002F\u002F SharedFlow：不持有值，可配置重放和缓冲\nprivate val _events = MutableSharedFlow\u003CEvent>(\n    replay = 0,              \u002F\u002F 新订阅者不接收历史事件\n    extraBufferCapacity = 1, \u002F\u002F 缓冲区\n    onBufferOverflow = BufferOverflow.DROP_OLDEST\n)\nval events: SharedFlow\u003CEvent> = _events.asSharedFlow()\n",[136,9688,9689,9694,9699,9704,9709,9714,9718,9722,9726,9731,9736,9741,9745,9750,9755,9760,9765,9769,9774,9779,9784,9789,9794,9798],{"__ignoreMap":191},[195,9690,9691],{"class":197,"line":198},[195,9692,9693],{},"\u002F\u002F Cold Flow（冷流）：每个收集者独立执行\n",[195,9695,9696],{"class":197,"line":230},[195,9697,9698],{},"fun fetchItems(): Flow\u003CItem> = flow {\n",[195,9700,9701],{"class":197,"line":251},[195,9702,9703],{},"    for (item in api.getItems()) {\n",[195,9705,9706],{"class":197,"line":272},[195,9707,9708],{},"        emit(item)\n",[195,9710,9711],{"class":197,"line":293},[195,9712,9713],{},"        delay(100)\n",[195,9715,9716],{"class":197,"line":562},[195,9717,2403],{},[195,9719,9720],{"class":197,"line":583},[195,9721,552],{},[195,9723,9724],{"class":197,"line":962},[195,9725,1241],{"emptyLinePlaceholder":757},[195,9727,9728],{"class":197,"line":968},[195,9729,9730],{},"\u002F\u002F 每次 collect 都会重新执行 flow { } 块\n",[195,9732,9733],{"class":197,"line":1274},[195,9734,9735],{},"fetchItems().collect { item -> println(item) } \u002F\u002F 执行一次\n",[195,9737,9738],{"class":197,"line":1282},[195,9739,9740],{},"fetchItems().collect { item -> println(item) } \u002F\u002F 再执行一次\n",[195,9742,9743],{"class":197,"line":1295},[195,9744,1241],{"emptyLinePlaceholder":757},[195,9746,9747],{"class":197,"line":1309},[195,9748,9749],{},"\u002F\u002F Hot Flow（热流）：多个收集者共享\n",[195,9751,9752],{"class":197,"line":2246},[195,9753,9754],{},"\u002F\u002F StateFlow：始终持有当前值，新收集者立即获得最新值\n",[195,9756,9757],{"class":197,"line":1996},[195,9758,9759],{},"private val _state = MutableStateFlow(UiState.Loading)\n",[195,9761,9762],{"class":197,"line":2257},[195,9763,9764],{},"val state: StateFlow\u003CUiState> = _state.asStateFlow()\n",[195,9766,9767],{"class":197,"line":2262},[195,9768,1241],{"emptyLinePlaceholder":757},[195,9770,9771],{"class":197,"line":2267},[195,9772,9773],{},"\u002F\u002F SharedFlow：不持有值，可配置重放和缓冲\n",[195,9775,9776],{"class":197,"line":2273},[195,9777,9778],{},"private val _events = MutableSharedFlow\u003CEvent>(\n",[195,9780,9781],{"class":197,"line":2033},[195,9782,9783],{},"    replay = 0,              \u002F\u002F 新订阅者不接收历史事件\n",[195,9785,9786],{"class":197,"line":2284},[195,9787,9788],{},"    extraBufferCapacity = 1, \u002F\u002F 缓冲区\n",[195,9790,9791],{"class":197,"line":2460},[195,9792,9793],{},"    onBufferOverflow = BufferOverflow.DROP_OLDEST\n",[195,9795,9796],{"class":197,"line":2466},[195,9797,410],{},[195,9799,9800],{"class":197,"line":2472},[195,9801,9802],{},"val events: SharedFlow\u003CEvent> = _events.asSharedFlow()\n",[14,9804,9805],{},[125,9806,9807],{},"Flow 操作符：",[186,9809,9811],{"className":6838,"code":9810,"language":6840,"meta":191,"style":191},"repository.getUsers()\n    .map { users -> users.filter { it.isActive } }          \u002F\u002F 转换\n    .distinctUntilChanged()                                  \u002F\u002F 去重\n    .debounce(300)                                           \u002F\u002F 防抖\n    .catch { e -> emit(emptyList()) }                        \u002F\u002F 异常处理\n    .flowOn(Dispatchers.IO)                                  \u002F\u002F 上游在 IO 线程\n    .onEach { users -> analytics.log(\"users: ${users.size}\") }\n    .stateIn(                                                 \u002F\u002F 转换为 StateFlow\n        scope = viewModelScope,\n        started = SharingStarted.WhileSubscribed(5000),      \u002F\u002F 无订阅者 5s 后停止\n        initialValue = emptyList()\n    )\n",[136,9812,9813,9818,9823,9828,9833,9838,9843,9848,9853,9858,9863,9868],{"__ignoreMap":191},[195,9814,9815],{"class":197,"line":198},[195,9816,9817],{},"repository.getUsers()\n",[195,9819,9820],{"class":197,"line":230},[195,9821,9822],{},"    .map { users -> users.filter { it.isActive } }          \u002F\u002F 转换\n",[195,9824,9825],{"class":197,"line":251},[195,9826,9827],{},"    .distinctUntilChanged()                                  \u002F\u002F 去重\n",[195,9829,9830],{"class":197,"line":272},[195,9831,9832],{},"    .debounce(300)                                           \u002F\u002F 防抖\n",[195,9834,9835],{"class":197,"line":293},[195,9836,9837],{},"    .catch { e -> emit(emptyList()) }                        \u002F\u002F 异常处理\n",[195,9839,9840],{"class":197,"line":562},[195,9841,9842],{},"    .flowOn(Dispatchers.IO)                                  \u002F\u002F 上游在 IO 线程\n",[195,9844,9845],{"class":197,"line":583},[195,9846,9847],{},"    .onEach { users -> analytics.log(\"users: ${users.size}\") }\n",[195,9849,9850],{"class":197,"line":962},[195,9851,9852],{},"    .stateIn(                                                 \u002F\u002F 转换为 StateFlow\n",[195,9854,9855],{"class":197,"line":968},[195,9856,9857],{},"        scope = viewModelScope,\n",[195,9859,9860],{"class":197,"line":1274},[195,9861,9862],{},"        started = SharingStarted.WhileSubscribed(5000),      \u002F\u002F 无订阅者 5s 后停止\n",[195,9864,9865],{"class":197,"line":1282},[195,9866,9867],{},"        initialValue = emptyList()\n",[195,9869,9870],{"class":197,"line":1295},[195,9871,4902],{},[14,9873,9874],{},[125,9875,9876],{},"在 UI 中安全收集：",[186,9878,9880],{"className":6838,"code":9879,"language":6840,"meta":191,"style":191},"\u002F\u002F Compose\n@Composable\nfun MyScreen(viewModel: MyViewModel) {\n    val state by viewModel.state.collectAsStateWithLifecycle()\n    \u002F\u002F 自动跟随 Lifecycle 暂停\u002F恢复\n}\n\n\u002F\u002F View 系统\nlifecycleScope.launch {\n    repeatOnLifecycle(Lifecycle.State.STARTED) {\n        viewModel.state.collect { state ->\n            updateUI(state)\n        }\n    }\n}\n",[136,9881,9882,9887,9891,9896,9901,9906,9910,9914,9919,9924,9929,9934,9939,9943,9947],{"__ignoreMap":191},[195,9883,9884],{"class":197,"line":198},[195,9885,9886],{},"\u002F\u002F Compose\n",[195,9888,9889],{"class":197,"line":230},[195,9890,8477],{},[195,9892,9893],{"class":197,"line":251},[195,9894,9895],{},"fun MyScreen(viewModel: MyViewModel) {\n",[195,9897,9898],{"class":197,"line":272},[195,9899,9900],{},"    val state by viewModel.state.collectAsStateWithLifecycle()\n",[195,9902,9903],{"class":197,"line":293},[195,9904,9905],{},"    \u002F\u002F 自动跟随 Lifecycle 暂停\u002F恢复\n",[195,9907,9908],{"class":197,"line":562},[195,9909,552],{},[195,9911,9912],{"class":197,"line":583},[195,9913,1241],{"emptyLinePlaceholder":757},[195,9915,9916],{"class":197,"line":962},[195,9917,9918],{},"\u002F\u002F View 系统\n",[195,9920,9921],{"class":197,"line":968},[195,9922,9923],{},"lifecycleScope.launch {\n",[195,9925,9926],{"class":197,"line":1274},[195,9927,9928],{},"    repeatOnLifecycle(Lifecycle.State.STARTED) {\n",[195,9930,9931],{"class":197,"line":1282},[195,9932,9933],{},"        viewModel.state.collect { state ->\n",[195,9935,9936],{"class":197,"line":1295},[195,9937,9938],{},"            updateUI(state)\n",[195,9940,9941],{"class":197,"line":1309},[195,9942,2887],{},[195,9944,9945],{"class":197,"line":2246},[195,9946,2403],{},[195,9948,9949],{"class":197,"line":1996},[195,9950,552],{},[1890,9952],{},[18,9954,9956],{"id":9955},"_12-架构与设计模式android","12. 架构与设计模式（Android）",[32,9958,9960],{"id":9959},"q121-android-推荐架构google-官方","Q12.1: Android 推荐架构（Google 官方）",[14,9962,9963],{},[125,9964,2077],{},[186,9966,9969],{"className":9967,"code":9968,"language":1074},[1072],"┌─────────────────────────────────────────┐\n│              UI Layer                    │\n│   Activity\u002FFragment\u002FCompose ← ViewModel │\n│         (观察 UiState)                   │\n├─────────────────────────────────────────┤\n│            Domain Layer (可选)           │\n│            UseCases                      │\n├─────────────────────────────────────────┤\n│             Data Layer                   │\n│   Repository → Remote DataSource (API)   │\n│              → Local DataSource (Room)   │\n└─────────────────────────────────────────┘\n",[136,9970,9968],{"__ignoreMap":191},[14,9972,9973],{},[125,9974,9975],{},"单向数据流（UDF）：",[186,9977,9979],{"className":6838,"code":9978,"language":6840,"meta":191,"style":191},"\u002F\u002F 事件从 UI → ViewModel → Repository（向下）\n\u002F\u002F 状态从 Repository → ViewModel → UI（向上）\n\n\u002F\u002F UiState\ndata class HomeUiState(\n    val items: List\u003CItem> = emptyList(),\n    val isLoading: Boolean = false,\n    val error: String? = null\n)\n\n\u002F\u002F ViewModel\nclass HomeViewModel(private val repository: ItemRepository) : ViewModel() {\n    private val _uiState = MutableStateFlow(HomeUiState())\n    val uiState: StateFlow\u003CHomeUiState> = _uiState.asStateFlow()\n\n    fun onEvent(event: HomeEvent) {\n        when (event) {\n            is HomeEvent.Refresh -> refresh()\n            is HomeEvent.Delete -> delete(event.itemId)\n        }\n    }\n\n    private fun refresh() {\n        viewModelScope.launch {\n            _uiState.update { it.copy(isLoading = true) }\n            try {\n                val items = repository.getItems()\n                _uiState.update { it.copy(items = items, isLoading = false) }\n            } catch (e: Exception) {\n                _uiState.update { it.copy(error = e.message, isLoading = false) }\n            }\n        }\n    }\n}\n\nsealed class HomeEvent {\n    data object Refresh : HomeEvent()\n    data class Delete(val itemId: String) : HomeEvent()\n}\n",[136,9980,9981,9986,9991,9995,10000,10005,10010,10015,10020,10024,10028,10033,10038,10043,10048,10052,10057,10062,10067,10072,10076,10080,10084,10089,10093,10098,10102,10107,10112,10116,10121,10125,10129,10133,10137,10141,10146,10151,10156],{"__ignoreMap":191},[195,9982,9983],{"class":197,"line":198},[195,9984,9985],{},"\u002F\u002F 事件从 UI → ViewModel → Repository（向下）\n",[195,9987,9988],{"class":197,"line":230},[195,9989,9990],{},"\u002F\u002F 状态从 Repository → ViewModel → UI（向上）\n",[195,9992,9993],{"class":197,"line":251},[195,9994,1241],{"emptyLinePlaceholder":757},[195,9996,9997],{"class":197,"line":272},[195,9998,9999],{},"\u002F\u002F UiState\n",[195,10001,10002],{"class":197,"line":293},[195,10003,10004],{},"data class HomeUiState(\n",[195,10006,10007],{"class":197,"line":562},[195,10008,10009],{},"    val items: List\u003CItem> = emptyList(),\n",[195,10011,10012],{"class":197,"line":583},[195,10013,10014],{},"    val isLoading: Boolean = false,\n",[195,10016,10017],{"class":197,"line":962},[195,10018,10019],{},"    val error: String? = null\n",[195,10021,10022],{"class":197,"line":968},[195,10023,410],{},[195,10025,10026],{"class":197,"line":1274},[195,10027,1241],{"emptyLinePlaceholder":757},[195,10029,10030],{"class":197,"line":1282},[195,10031,10032],{},"\u002F\u002F ViewModel\n",[195,10034,10035],{"class":197,"line":1295},[195,10036,10037],{},"class HomeViewModel(private val repository: ItemRepository) : ViewModel() {\n",[195,10039,10040],{"class":197,"line":1309},[195,10041,10042],{},"    private val _uiState = MutableStateFlow(HomeUiState())\n",[195,10044,10045],{"class":197,"line":2246},[195,10046,10047],{},"    val uiState: StateFlow\u003CHomeUiState> = _uiState.asStateFlow()\n",[195,10049,10050],{"class":197,"line":1996},[195,10051,1241],{"emptyLinePlaceholder":757},[195,10053,10054],{"class":197,"line":2257},[195,10055,10056],{},"    fun onEvent(event: HomeEvent) {\n",[195,10058,10059],{"class":197,"line":2262},[195,10060,10061],{},"        when (event) {\n",[195,10063,10064],{"class":197,"line":2267},[195,10065,10066],{},"            is HomeEvent.Refresh -> refresh()\n",[195,10068,10069],{"class":197,"line":2273},[195,10070,10071],{},"            is HomeEvent.Delete -> delete(event.itemId)\n",[195,10073,10074],{"class":197,"line":2033},[195,10075,2887],{},[195,10077,10078],{"class":197,"line":2284},[195,10079,2403],{},[195,10081,10082],{"class":197,"line":2460},[195,10083,1241],{"emptyLinePlaceholder":757},[195,10085,10086],{"class":197,"line":2466},[195,10087,10088],{},"    private fun refresh() {\n",[195,10090,10091],{"class":197,"line":2472},[195,10092,7840],{},[195,10094,10095],{"class":197,"line":2780},[195,10096,10097],{},"            _uiState.update { it.copy(isLoading = true) }\n",[195,10099,10100],{"class":197,"line":2786},[195,10101,8918],{},[195,10103,10104],{"class":197,"line":2792},[195,10105,10106],{},"                val items = repository.getItems()\n",[195,10108,10109],{"class":197,"line":2798},[195,10110,10111],{},"                _uiState.update { it.copy(items = items, isLoading = false) }\n",[195,10113,10114],{"class":197,"line":2804},[195,10115,8928],{},[195,10117,10118],{"class":197,"line":2810},[195,10119,10120],{},"                _uiState.update { it.copy(error = e.message, isLoading = false) }\n",[195,10122,10123],{"class":197,"line":2815},[195,10124,5781],{},[195,10126,10127],{"class":197,"line":2820},[195,10128,2887],{},[195,10130,10131],{"class":197,"line":2825},[195,10132,2403],{},[195,10134,10135],{"class":197,"line":2831},[195,10136,552],{},[195,10138,10139],{"class":197,"line":2837},[195,10140,1241],{"emptyLinePlaceholder":757},[195,10142,10143],{"class":197,"line":2843},[195,10144,10145],{},"sealed class HomeEvent {\n",[195,10147,10148],{"class":197,"line":2848},[195,10149,10150],{},"    data object Refresh : HomeEvent()\n",[195,10152,10153],{"class":197,"line":2854},[195,10154,10155],{},"    data class Delete(val itemId: String) : HomeEvent()\n",[195,10157,10158],{"class":197,"line":2860},[195,10159,552],{},[1890,10161],{},[32,10163,10165],{"id":10164},"q122-hilt-依赖注入","Q12.2: Hilt 依赖注入",[14,10167,10168],{},[125,10169,2077],{},[186,10171,10173],{"className":6838,"code":10172,"language":6840,"meta":191,"style":191},"\u002F\u002F 1. Module 定义\n@Module\n@InstallIn(SingletonComponent::class)\nobject NetworkModule {\n    @Provides\n    @Singleton\n    fun provideRetrofit(): Retrofit = Retrofit.Builder()\n        .baseUrl(\"https:\u002F\u002Fapi.example.com\")\n        .addConverterFactory(GsonConverterFactory.create())\n        .build()\n\n    @Provides\n    @Singleton\n    fun provideUserApi(retrofit: Retrofit): UserApi =\n        retrofit.create(UserApi::class.java)\n}\n\n@Module\n@InstallIn(SingletonComponent::class)\nabstract class RepositoryModule {\n    @Binds\n    @Singleton\n    abstract fun bindUserRepository(impl: UserRepositoryImpl): UserRepository\n}\n\n\u002F\u002F 2. 注入\n@HiltViewModel\nclass UserViewModel @Inject constructor(\n    private val repository: UserRepository\n) : ViewModel() { ... }\n\n@AndroidEntryPoint\nclass UserActivity : AppCompatActivity() {\n    private val viewModel: UserViewModel by viewModels()\n}\n\n\u002F\u002F 3. Scope 对比\n\u002F\u002F @Singleton → SingletonComponent（应用级）\n\u002F\u002F @ActivityScoped → ActivityComponent（Activity 级）\n\u002F\u002F @ViewModelScoped → ViewModelComponent（ViewModel 级）\n\u002F\u002F @FragmentScoped → FragmentComponent（Fragment 级）\n",[136,10174,10175,10180,10185,10190,10195,10200,10205,10210,10215,10220,10225,10229,10233,10237,10242,10247,10251,10255,10259,10263,10268,10273,10277,10282,10286,10290,10295,10300,10305,10310,10315,10319,10324,10329,10334,10338,10342,10347,10352,10357,10362],{"__ignoreMap":191},[195,10176,10177],{"class":197,"line":198},[195,10178,10179],{},"\u002F\u002F 1. Module 定义\n",[195,10181,10182],{"class":197,"line":230},[195,10183,10184],{},"@Module\n",[195,10186,10187],{"class":197,"line":251},[195,10188,10189],{},"@InstallIn(SingletonComponent::class)\n",[195,10191,10192],{"class":197,"line":272},[195,10193,10194],{},"object NetworkModule {\n",[195,10196,10197],{"class":197,"line":293},[195,10198,10199],{},"    @Provides\n",[195,10201,10202],{"class":197,"line":562},[195,10203,10204],{},"    @Singleton\n",[195,10206,10207],{"class":197,"line":583},[195,10208,10209],{},"    fun provideRetrofit(): Retrofit = Retrofit.Builder()\n",[195,10211,10212],{"class":197,"line":962},[195,10213,10214],{},"        .baseUrl(\"https:\u002F\u002Fapi.example.com\")\n",[195,10216,10217],{"class":197,"line":968},[195,10218,10219],{},"        .addConverterFactory(GsonConverterFactory.create())\n",[195,10221,10222],{"class":197,"line":1274},[195,10223,10224],{},"        .build()\n",[195,10226,10227],{"class":197,"line":1282},[195,10228,1241],{"emptyLinePlaceholder":757},[195,10230,10231],{"class":197,"line":1295},[195,10232,10199],{},[195,10234,10235],{"class":197,"line":1309},[195,10236,10204],{},[195,10238,10239],{"class":197,"line":2246},[195,10240,10241],{},"    fun provideUserApi(retrofit: Retrofit): UserApi =\n",[195,10243,10244],{"class":197,"line":1996},[195,10245,10246],{},"        retrofit.create(UserApi::class.java)\n",[195,10248,10249],{"class":197,"line":2257},[195,10250,552],{},[195,10252,10253],{"class":197,"line":2262},[195,10254,1241],{"emptyLinePlaceholder":757},[195,10256,10257],{"class":197,"line":2267},[195,10258,10184],{},[195,10260,10261],{"class":197,"line":2273},[195,10262,10189],{},[195,10264,10265],{"class":197,"line":2033},[195,10266,10267],{},"abstract class RepositoryModule {\n",[195,10269,10270],{"class":197,"line":2284},[195,10271,10272],{},"    @Binds\n",[195,10274,10275],{"class":197,"line":2460},[195,10276,10204],{},[195,10278,10279],{"class":197,"line":2466},[195,10280,10281],{},"    abstract fun bindUserRepository(impl: UserRepositoryImpl): UserRepository\n",[195,10283,10284],{"class":197,"line":2472},[195,10285,552],{},[195,10287,10288],{"class":197,"line":2780},[195,10289,1241],{"emptyLinePlaceholder":757},[195,10291,10292],{"class":197,"line":2786},[195,10293,10294],{},"\u002F\u002F 2. 注入\n",[195,10296,10297],{"class":197,"line":2792},[195,10298,10299],{},"@HiltViewModel\n",[195,10301,10302],{"class":197,"line":2798},[195,10303,10304],{},"class UserViewModel @Inject constructor(\n",[195,10306,10307],{"class":197,"line":2804},[195,10308,10309],{},"    private val repository: UserRepository\n",[195,10311,10312],{"class":197,"line":2810},[195,10313,10314],{},") : ViewModel() { ... }\n",[195,10316,10317],{"class":197,"line":2815},[195,10318,1241],{"emptyLinePlaceholder":757},[195,10320,10321],{"class":197,"line":2820},[195,10322,10323],{},"@AndroidEntryPoint\n",[195,10325,10326],{"class":197,"line":2825},[195,10327,10328],{},"class UserActivity : AppCompatActivity() {\n",[195,10330,10331],{"class":197,"line":2831},[195,10332,10333],{},"    private val viewModel: UserViewModel by viewModels()\n",[195,10335,10336],{"class":197,"line":2837},[195,10337,552],{},[195,10339,10340],{"class":197,"line":2843},[195,10341,1241],{"emptyLinePlaceholder":757},[195,10343,10344],{"class":197,"line":2848},[195,10345,10346],{},"\u002F\u002F 3. Scope 对比\n",[195,10348,10349],{"class":197,"line":2854},[195,10350,10351],{},"\u002F\u002F @Singleton → SingletonComponent（应用级）\n",[195,10353,10354],{"class":197,"line":2860},[195,10355,10356],{},"\u002F\u002F @ActivityScoped → ActivityComponent（Activity 级）\n",[195,10358,10359],{"class":197,"line":2866},[195,10360,10361],{},"\u002F\u002F @ViewModelScoped → ViewModelComponent（ViewModel 级）\n",[195,10363,10364],{"class":197,"line":2872},[195,10365,10366],{},"\u002F\u002F @FragmentScoped → FragmentComponent（Fragment 级）\n",[1890,10368],{},[18,10370,10372],{"id":10371},"_13-性能优化与工具android","13. 性能优化与工具（Android）",[32,10374,10376],{"id":10375},"q131-android-性能优化的关键领域","Q13.1: Android 性能优化的关键领域",[14,10378,10379],{},[125,10380,2077],{},[14,10382,10383],{},[125,10384,10385],{},"1. 启动优化：",[186,10387,10389],{"className":6838,"code":10388,"language":6840,"meta":191,"style":191},"\u002F\u002F App Startup 库：控制初始化顺序和延迟\nclass AnalyticsInitializer : Initializer\u003CAnalytics> {\n    override fun create(context: Context): Analytics {\n        return Analytics.init(context)\n    }\n\n    override fun dependencies(): List\u003CClass\u003Cout Initializer\u003C*>>> {\n        return listOf(CrashReportingInitializer::class.java) \u002F\u002F 依赖关系\n    }\n}\n\n\u002F\u002F Baseline Profile（提升启动和运行时性能）\n\u002F\u002F 提前编译热路径代码为机器码\n@get:Rule\nval rule = BaselineProfileRule()\n\n@Test\nfun generateBaselineProfile() {\n    rule.collect(packageName = \"com.example.app\") {\n        startActivityAndWait()\n        \u002F\u002F 模拟用户操作...\n    }\n}\n",[136,10390,10391,10396,10401,10406,10411,10415,10419,10424,10429,10433,10437,10441,10446,10451,10456,10461,10465,10470,10475,10480,10485,10490,10494],{"__ignoreMap":191},[195,10392,10393],{"class":197,"line":198},[195,10394,10395],{},"\u002F\u002F App Startup 库：控制初始化顺序和延迟\n",[195,10397,10398],{"class":197,"line":230},[195,10399,10400],{},"class AnalyticsInitializer : Initializer\u003CAnalytics> {\n",[195,10402,10403],{"class":197,"line":251},[195,10404,10405],{},"    override fun create(context: Context): Analytics {\n",[195,10407,10408],{"class":197,"line":272},[195,10409,10410],{},"        return Analytics.init(context)\n",[195,10412,10413],{"class":197,"line":293},[195,10414,2403],{},[195,10416,10417],{"class":197,"line":562},[195,10418,1241],{"emptyLinePlaceholder":757},[195,10420,10421],{"class":197,"line":583},[195,10422,10423],{},"    override fun dependencies(): List\u003CClass\u003Cout Initializer\u003C*>>> {\n",[195,10425,10426],{"class":197,"line":962},[195,10427,10428],{},"        return listOf(CrashReportingInitializer::class.java) \u002F\u002F 依赖关系\n",[195,10430,10431],{"class":197,"line":968},[195,10432,2403],{},[195,10434,10435],{"class":197,"line":1274},[195,10436,552],{},[195,10438,10439],{"class":197,"line":1282},[195,10440,1241],{"emptyLinePlaceholder":757},[195,10442,10443],{"class":197,"line":1295},[195,10444,10445],{},"\u002F\u002F Baseline Profile（提升启动和运行时性能）\n",[195,10447,10448],{"class":197,"line":1309},[195,10449,10450],{},"\u002F\u002F 提前编译热路径代码为机器码\n",[195,10452,10453],{"class":197,"line":2246},[195,10454,10455],{},"@get:Rule\n",[195,10457,10458],{"class":197,"line":1996},[195,10459,10460],{},"val rule = BaselineProfileRule()\n",[195,10462,10463],{"class":197,"line":2257},[195,10464,1241],{"emptyLinePlaceholder":757},[195,10466,10467],{"class":197,"line":2262},[195,10468,10469],{},"@Test\n",[195,10471,10472],{"class":197,"line":2267},[195,10473,10474],{},"fun generateBaselineProfile() {\n",[195,10476,10477],{"class":197,"line":2273},[195,10478,10479],{},"    rule.collect(packageName = \"com.example.app\") {\n",[195,10481,10482],{"class":197,"line":2033},[195,10483,10484],{},"        startActivityAndWait()\n",[195,10486,10487],{"class":197,"line":2284},[195,10488,10489],{},"        \u002F\u002F 模拟用户操作...\n",[195,10491,10492],{"class":197,"line":2460},[195,10493,2403],{},[195,10495,10496],{"class":197,"line":2466},[195,10497,552],{},[14,10499,10500],{},[125,10501,10502],{},"2. 内存优化：",[186,10504,10506],{"className":6838,"code":10505,"language":6840,"meta":191,"style":191},"\u002F\u002F 避免内存泄漏\n\u002F\u002F ❌ 静态引用 Activity\ncompanion object {\n    var activity: Activity? = null  \u002F\u002F 永远不要这样做\n}\n\n\u002F\u002F ❌ 内部类持有外部引用\nclass MyActivity : AppCompatActivity() {\n    inner class MyHandler : Handler() { ... }  \u002F\u002F 隐式持有 Activity\n}\n\n\u002F\u002F ✅ 使用 WeakReference 或静态内部类\nclass MyHandler(activity: Activity) : Handler(Looper.getMainLooper()) {\n    private val activityRef = WeakReference(activity)\n    override fun handleMessage(msg: Message) {\n        activityRef.get()?.let { \u002F* ... *\u002F }\n    }\n}\n",[136,10507,10508,10513,10518,10523,10528,10532,10536,10541,10546,10551,10555,10559,10564,10569,10574,10579,10584,10588],{"__ignoreMap":191},[195,10509,10510],{"class":197,"line":198},[195,10511,10512],{},"\u002F\u002F 避免内存泄漏\n",[195,10514,10515],{"class":197,"line":230},[195,10516,10517],{},"\u002F\u002F ❌ 静态引用 Activity\n",[195,10519,10520],{"class":197,"line":251},[195,10521,10522],{},"companion object {\n",[195,10524,10525],{"class":197,"line":272},[195,10526,10527],{},"    var activity: Activity? = null  \u002F\u002F 永远不要这样做\n",[195,10529,10530],{"class":197,"line":293},[195,10531,552],{},[195,10533,10534],{"class":197,"line":562},[195,10535,1241],{"emptyLinePlaceholder":757},[195,10537,10538],{"class":197,"line":583},[195,10539,10540],{},"\u002F\u002F ❌ 内部类持有外部引用\n",[195,10542,10543],{"class":197,"line":962},[195,10544,10545],{},"class MyActivity : AppCompatActivity() {\n",[195,10547,10548],{"class":197,"line":968},[195,10549,10550],{},"    inner class MyHandler : Handler() { ... }  \u002F\u002F 隐式持有 Activity\n",[195,10552,10553],{"class":197,"line":1274},[195,10554,552],{},[195,10556,10557],{"class":197,"line":1282},[195,10558,1241],{"emptyLinePlaceholder":757},[195,10560,10561],{"class":197,"line":1295},[195,10562,10563],{},"\u002F\u002F ✅ 使用 WeakReference 或静态内部类\n",[195,10565,10566],{"class":197,"line":1309},[195,10567,10568],{},"class MyHandler(activity: Activity) : Handler(Looper.getMainLooper()) {\n",[195,10570,10571],{"class":197,"line":2246},[195,10572,10573],{},"    private val activityRef = WeakReference(activity)\n",[195,10575,10576],{"class":197,"line":1996},[195,10577,10578],{},"    override fun handleMessage(msg: Message) {\n",[195,10580,10581],{"class":197,"line":2257},[195,10582,10583],{},"        activityRef.get()?.let { \u002F* ... *\u002F }\n",[195,10585,10586],{"class":197,"line":2262},[195,10587,2403],{},[195,10589,10590],{"class":197,"line":2267},[195,10591,552],{},[14,10593,10594],{},[125,10595,10596],{},"3. 布局优化：",[186,10598,10600],{"className":6838,"code":10599,"language":6840,"meta":191,"style":191},"\u002F\u002F Compose：避免不必要的重组（前面已详述）\n\u002F\u002F View 系统：减少层级、使用 ConstraintLayout\n\u002F\u002F 工具：Layout Inspector\n\n\u002F\u002F R8\u002FProGuard：代码缩减 + 混淆\nandroid {\n    buildTypes {\n        release {\n            isMinifyEnabled = true\n            isShrinkResources = true\n            proguardFiles(getDefaultProguardFile(\"proguard-android-optimize.txt\"), \"proguard-rules.pro\")\n        }\n    }\n}\n",[136,10601,10602,10607,10612,10617,10621,10626,10631,10636,10641,10646,10651,10656,10660,10664],{"__ignoreMap":191},[195,10603,10604],{"class":197,"line":198},[195,10605,10606],{},"\u002F\u002F Compose：避免不必要的重组（前面已详述）\n",[195,10608,10609],{"class":197,"line":230},[195,10610,10611],{},"\u002F\u002F View 系统：减少层级、使用 ConstraintLayout\n",[195,10613,10614],{"class":197,"line":251},[195,10615,10616],{},"\u002F\u002F 工具：Layout Inspector\n",[195,10618,10619],{"class":197,"line":272},[195,10620,1241],{"emptyLinePlaceholder":757},[195,10622,10623],{"class":197,"line":293},[195,10624,10625],{},"\u002F\u002F R8\u002FProGuard：代码缩减 + 混淆\n",[195,10627,10628],{"class":197,"line":562},[195,10629,10630],{},"android {\n",[195,10632,10633],{"class":197,"line":583},[195,10634,10635],{},"    buildTypes {\n",[195,10637,10638],{"class":197,"line":962},[195,10639,10640],{},"        release {\n",[195,10642,10643],{"class":197,"line":968},[195,10644,10645],{},"            isMinifyEnabled = true\n",[195,10647,10648],{"class":197,"line":1274},[195,10649,10650],{},"            isShrinkResources = true\n",[195,10652,10653],{"class":197,"line":1282},[195,10654,10655],{},"            proguardFiles(getDefaultProguardFile(\"proguard-android-optimize.txt\"), \"proguard-rules.pro\")\n",[195,10657,10658],{"class":197,"line":1295},[195,10659,2887],{},[195,10661,10662],{"class":197,"line":1309},[195,10663,2403],{},[195,10665,10666],{"class":197,"line":2246},[195,10667,552],{},[1890,10669],{},[32,10671,10673],{"id":10672},"q132-android-常用性能分析工具","Q13.2: Android 常用性能分析工具",[14,10675,10676],{},[125,10677,2077],{},[36,10679,10680,10689],{},[39,10681,10682],{},[42,10683,10684,10687],{},[45,10685,10686],{},"工具",[45,10688,3111],{},[52,10690,10691,10699,10707,10715,10723,10731,10739],{},[42,10692,10693,10696],{},[57,10694,10695],{},"Android Studio Profiler",[57,10697,10698],{},"CPU、内存、网络、电量实时监控",[42,10700,10701,10704],{},[57,10702,10703],{},"Layout Inspector",[57,10705,10706],{},"Compose\u002FView 层级、重组次数",[42,10708,10709,10712],{},[57,10710,10711],{},"LeakCanary",[57,10713,10714],{},"自动检测内存泄漏",[42,10716,10717,10720],{},[57,10718,10719],{},"Systrace \u002F Perfetto",[57,10721,10722],{},"系统级帧分析",[42,10724,10725,10728],{},[57,10726,10727],{},"Macrobenchmark",[57,10729,10730],{},"启动时间、帧率等自动化基准测试",[42,10732,10733,10736],{},[57,10734,10735],{},"Baseline Profile",[57,10737,10738],{},"预编译热路径提升性能",[42,10740,10741,10744],{},[57,10742,10743],{},"R8",[57,10745,10746],{},"代码缩减、混淆、优化",[186,10748,10750],{"className":6838,"code":10749,"language":6840,"meta":191,"style":191},"\u002F\u002F LeakCanary（Debug 版自动检测泄漏）\ndebugImplementation(\"com.squareup.leakcanary:leakcanary-android:2.14\")\n\u002F\u002F 无需额外代码，泄漏时自动弹出通知\n\n\u002F\u002F Macrobenchmark\n@LargeTest\n@RunWith(AndroidJUnit4::class)\nclass StartupBenchmark {\n    @get:Rule\n    val benchmarkRule = MacrobenchmarkRule()\n\n    @Test\n    fun startupCompilation() = benchmarkRule.measureRepeated(\n        packageName = \"com.example.app\",\n        metrics = listOf(StartupTimingMetric()),\n        iterations = 5,\n        startupMode = StartupMode.COLD\n    ) {\n        pressHome()\n        startActivityAndWait()\n    }\n}\n",[136,10751,10752,10757,10762,10767,10771,10776,10781,10786,10791,10796,10801,10805,10810,10815,10820,10825,10830,10835,10840,10845,10849,10853],{"__ignoreMap":191},[195,10753,10754],{"class":197,"line":198},[195,10755,10756],{},"\u002F\u002F LeakCanary（Debug 版自动检测泄漏）\n",[195,10758,10759],{"class":197,"line":230},[195,10760,10761],{},"debugImplementation(\"com.squareup.leakcanary:leakcanary-android:2.14\")\n",[195,10763,10764],{"class":197,"line":251},[195,10765,10766],{},"\u002F\u002F 无需额外代码，泄漏时自动弹出通知\n",[195,10768,10769],{"class":197,"line":272},[195,10770,1241],{"emptyLinePlaceholder":757},[195,10772,10773],{"class":197,"line":293},[195,10774,10775],{},"\u002F\u002F Macrobenchmark\n",[195,10777,10778],{"class":197,"line":562},[195,10779,10780],{},"@LargeTest\n",[195,10782,10783],{"class":197,"line":583},[195,10784,10785],{},"@RunWith(AndroidJUnit4::class)\n",[195,10787,10788],{"class":197,"line":962},[195,10789,10790],{},"class StartupBenchmark {\n",[195,10792,10793],{"class":197,"line":968},[195,10794,10795],{},"    @get:Rule\n",[195,10797,10798],{"class":197,"line":1274},[195,10799,10800],{},"    val benchmarkRule = MacrobenchmarkRule()\n",[195,10802,10803],{"class":197,"line":1282},[195,10804,1241],{"emptyLinePlaceholder":757},[195,10806,10807],{"class":197,"line":1295},[195,10808,10809],{},"    @Test\n",[195,10811,10812],{"class":197,"line":1309},[195,10813,10814],{},"    fun startupCompilation() = benchmarkRule.measureRepeated(\n",[195,10816,10817],{"class":197,"line":2246},[195,10818,10819],{},"        packageName = \"com.example.app\",\n",[195,10821,10822],{"class":197,"line":1996},[195,10823,10824],{},"        metrics = listOf(StartupTimingMetric()),\n",[195,10826,10827],{"class":197,"line":2257},[195,10828,10829],{},"        iterations = 5,\n",[195,10831,10832],{"class":197,"line":2262},[195,10833,10834],{},"        startupMode = StartupMode.COLD\n",[195,10836,10837],{"class":197,"line":2267},[195,10838,10839],{},"    ) {\n",[195,10841,10842],{"class":197,"line":2273},[195,10843,10844],{},"        pressHome()\n",[195,10846,10847],{"class":197,"line":2033},[195,10848,10484],{},[195,10850,10851],{"class":197,"line":2284},[195,10852,2403],{},[195,10854,10855],{"class":197,"line":2460},[195,10856,552],{},[1890,10858],{},[18,10860,10862],{"id":10861},"_14-系统机制","14. 系统机制",[32,10864,10866],{"id":10865},"q141-android-的进程优先级和后台限制","Q14.1: Android 的进程优先级和后台限制",[14,10868,10869],{},[125,10870,2077],{},[186,10872,10875],{"className":10873,"code":10874,"language":1074},[1072],"进程优先级（从高到低）：\n1. 前台进程（Foreground）     - 用户正在交互的 Activity\n2. 可见进程（Visible）        - 可见但不在前台（如对话框后面的 Activity）\n3. 服务进程（Service）        - 正在运行 startService\n4. 缓存进程（Cached\u002FBackground） - 不可见，可能被系统随时杀死\n5. 空进程（Empty）            - 无活跃组件，最先被杀\n",[136,10876,10874],{"__ignoreMap":191},[14,10878,10879],{},[125,10880,10881],{},"Android 后台限制演进：",[36,10883,10884,10894],{},[39,10885,10886],{},[42,10887,10888,10891],{},[45,10889,10890],{},"版本",[45,10892,10893],{},"限制",[52,10895,10896,10904,10912,10920,10928],{},[42,10897,10898,10901],{},[57,10899,10900],{},"Android 8 (O)",[57,10902,10903],{},"后台服务限制、广播限制",[42,10905,10906,10909],{},[57,10907,10908],{},"Android 9 (P)",[57,10910,10911],{},"Standby Buckets",[42,10913,10914,10917],{},[57,10915,10916],{},"Android 12 (S)",[57,10918,10919],{},"前台服务启动限制、精确闹钟权限",[42,10921,10922,10925],{},[57,10923,10924],{},"Android 13 (T)",[57,10926,10927],{},"通知权限、前台服务类型声明",[42,10929,10930,10933],{},[57,10931,10932],{},"Android 14 (U)",[57,10934,10935],{},"前台服务类型强制声明",[186,10937,10939],{"className":6838,"code":10938,"language":6840,"meta":191,"style":191},"\u002F\u002F 正确的后台工作方式选择\n\u002F\u002F 立即执行 + 长时间 → Foreground Service\n\u002F\u002F 可延迟 + 需要保证执行 → WorkManager\n\u002F\u002F 精确定时 → AlarmManager（需要权限）\n\u002F\u002F 短暂后台操作 → Coroutine（在 ViewModel\u002Flifecycle scope 中）\n",[136,10940,10941,10946,10951,10956,10961],{"__ignoreMap":191},[195,10942,10943],{"class":197,"line":198},[195,10944,10945],{},"\u002F\u002F 正确的后台工作方式选择\n",[195,10947,10948],{"class":197,"line":230},[195,10949,10950],{},"\u002F\u002F 立即执行 + 长时间 → Foreground Service\n",[195,10952,10953],{"class":197,"line":251},[195,10954,10955],{},"\u002F\u002F 可延迟 + 需要保证执行 → WorkManager\n",[195,10957,10958],{"class":197,"line":272},[195,10959,10960],{},"\u002F\u002F 精确定时 → AlarmManager（需要权限）\n",[195,10962,10963],{"class":197,"line":293},[195,10964,10965],{},"\u002F\u002F 短暂后台操作 → Coroutine（在 ViewModel\u002Flifecycle scope 中）\n",[1890,10967],{},[32,10969,10971],{"id":10970},"q142-intent-和-intent-filter-的工作机制","Q14.2: Intent 和 Intent Filter 的工作机制",[14,10973,10974],{},[125,10975,2077],{},[186,10977,10979],{"className":6838,"code":10978,"language":6840,"meta":191,"style":191},"\u002F\u002F 显式 Intent：指定目标组件\nval intent = Intent(this, DetailActivity::class.java).apply {\n    putExtra(\"item_id\", \"123\")\n}\nstartActivity(intent)\n\n\u002F\u002F 隐式 Intent：通过 Action\u002FCategory\u002FData 匹配\nval intent = Intent(Intent.ACTION_VIEW, Uri.parse(\"https:\u002F\u002Fexample.com\"))\nstartActivity(intent)\n\n\u002F\u002F Intent Filter（在 Manifest 中声明）\n\u002F\u002F \u003Cintent-filter>\n\u002F\u002F     \u003Caction android:name=\"android.intent.action.VIEW\" \u002F>\n\u002F\u002F     \u003Ccategory android:name=\"android.intent.category.DEFAULT\" \u002F>\n\u002F\u002F     \u003Cdata android:scheme=\"https\" android:host=\"example.com\" \u002F>\n\u002F\u002F \u003C\u002Fintent-filter>\n\n\u002F\u002F Deep Link 处理\noverride fun onCreate(savedInstanceState: Bundle?) {\n    super.onCreate(savedInstanceState)\n    intent?.data?.let { uri ->\n        when {\n            uri.pathSegments.firstOrNull() == \"product\" -> {\n                val productId = uri.lastPathSegment\n                navigateToProduct(productId)\n            }\n        }\n    }\n}\n\n\u002F\u002F PendingIntent（跨进程传递 Intent）\nval pendingIntent = PendingIntent.getActivity(\n    context, 0, intent,\n    PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE\n)\n",[136,10980,10981,10986,10991,10996,11000,11005,11009,11014,11019,11023,11027,11032,11037,11042,11047,11052,11057,11061,11066,11070,11074,11079,11084,11089,11094,11099,11103,11107,11111,11115,11119,11124,11129,11134,11139],{"__ignoreMap":191},[195,10982,10983],{"class":197,"line":198},[195,10984,10985],{},"\u002F\u002F 显式 Intent：指定目标组件\n",[195,10987,10988],{"class":197,"line":230},[195,10989,10990],{},"val intent = Intent(this, DetailActivity::class.java).apply {\n",[195,10992,10993],{"class":197,"line":251},[195,10994,10995],{},"    putExtra(\"item_id\", \"123\")\n",[195,10997,10998],{"class":197,"line":272},[195,10999,552],{},[195,11001,11002],{"class":197,"line":293},[195,11003,11004],{},"startActivity(intent)\n",[195,11006,11007],{"class":197,"line":562},[195,11008,1241],{"emptyLinePlaceholder":757},[195,11010,11011],{"class":197,"line":583},[195,11012,11013],{},"\u002F\u002F 隐式 Intent：通过 Action\u002FCategory\u002FData 匹配\n",[195,11015,11016],{"class":197,"line":962},[195,11017,11018],{},"val intent = Intent(Intent.ACTION_VIEW, Uri.parse(\"https:\u002F\u002Fexample.com\"))\n",[195,11020,11021],{"class":197,"line":968},[195,11022,11004],{},[195,11024,11025],{"class":197,"line":1274},[195,11026,1241],{"emptyLinePlaceholder":757},[195,11028,11029],{"class":197,"line":1282},[195,11030,11031],{},"\u002F\u002F Intent Filter（在 Manifest 中声明）\n",[195,11033,11034],{"class":197,"line":1295},[195,11035,11036],{},"\u002F\u002F \u003Cintent-filter>\n",[195,11038,11039],{"class":197,"line":1309},[195,11040,11041],{},"\u002F\u002F     \u003Caction android:name=\"android.intent.action.VIEW\" \u002F>\n",[195,11043,11044],{"class":197,"line":2246},[195,11045,11046],{},"\u002F\u002F     \u003Ccategory android:name=\"android.intent.category.DEFAULT\" \u002F>\n",[195,11048,11049],{"class":197,"line":1996},[195,11050,11051],{},"\u002F\u002F     \u003Cdata android:scheme=\"https\" android:host=\"example.com\" \u002F>\n",[195,11053,11054],{"class":197,"line":2257},[195,11055,11056],{},"\u002F\u002F \u003C\u002Fintent-filter>\n",[195,11058,11059],{"class":197,"line":2262},[195,11060,1241],{"emptyLinePlaceholder":757},[195,11062,11063],{"class":197,"line":2267},[195,11064,11065],{},"\u002F\u002F Deep Link 处理\n",[195,11067,11068],{"class":197,"line":2273},[195,11069,7894],{},[195,11071,11072],{"class":197,"line":2033},[195,11073,7899],{},[195,11075,11076],{"class":197,"line":2284},[195,11077,11078],{},"    intent?.data?.let { uri ->\n",[195,11080,11081],{"class":197,"line":2460},[195,11082,11083],{},"        when {\n",[195,11085,11086],{"class":197,"line":2466},[195,11087,11088],{},"            uri.pathSegments.firstOrNull() == \"product\" -> {\n",[195,11090,11091],{"class":197,"line":2472},[195,11092,11093],{},"                val productId = uri.lastPathSegment\n",[195,11095,11096],{"class":197,"line":2780},[195,11097,11098],{},"                navigateToProduct(productId)\n",[195,11100,11101],{"class":197,"line":2786},[195,11102,5781],{},[195,11104,11105],{"class":197,"line":2792},[195,11106,2887],{},[195,11108,11109],{"class":197,"line":2798},[195,11110,2403],{},[195,11112,11113],{"class":197,"line":2804},[195,11114,552],{},[195,11116,11117],{"class":197,"line":2810},[195,11118,1241],{"emptyLinePlaceholder":757},[195,11120,11121],{"class":197,"line":2815},[195,11122,11123],{},"\u002F\u002F PendingIntent（跨进程传递 Intent）\n",[195,11125,11126],{"class":197,"line":2820},[195,11127,11128],{},"val pendingIntent = PendingIntent.getActivity(\n",[195,11130,11131],{"class":197,"line":2825},[195,11132,11133],{},"    context, 0, intent,\n",[195,11135,11136],{"class":197,"line":2831},[195,11137,11138],{},"    PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE\n",[195,11140,11141],{"class":197,"line":2837},[195,11142,410],{},[1890,11144],{},[2055,11146,1993],{"id":11147},"第三部分flutter-开发-1",[18,11149,11151],{"id":11150},"_15-dart-语言与核心机制","15. Dart 语言与核心机制",[32,11153,11155],{"id":11154},"q151-dart-的事件循环和-isolate","Q15.1: Dart 的事件循环和 Isolate",[14,11157,11158],{},[125,11159,2077],{},[186,11161,11165],{"className":11162,"code":11163,"language":11164,"meta":191,"style":191},"language-dart shiki shiki-themes github-light github-dark","\u002F\u002F 事件循环（单线程模型）\n\u002F\u002F 执行顺序：同步代码 → Microtask Queue → Event Queue\n\nvoid main() {\n  print('1');                                    \u002F\u002F 同步\n  Future(() => print('2'));                       \u002F\u002F Event Queue\n  Future.microtask(() => print('3'));             \u002F\u002F Microtask Queue\n  scheduleMicrotask(() => print('4'));            \u002F\u002F Microtask Queue\n  Future(() => print('5'));                       \u002F\u002F Event Queue\n  print('6');                                    \u002F\u002F 同步\n}\n\u002F\u002F 输出：1, 6, 3, 4, 2, 5\n\n\u002F\u002F Isolate：真正的并行（独立内存，消息传递通信）\n\u002F\u002F 适用于 CPU 密集型任务：JSON 解析、图片处理、加密\nfinal result = await Isolate.run(() {\n  return jsonDecode(hugeJsonString); \u002F\u002F 在独立 Isolate 中执行\n});\n\n\u002F\u002F compute() 是 Flutter 封装的便捷方法\nfinal parsed = await compute(parseJson, rawData);\n\n\u002F\u002F 复杂场景：长时间运行的 Isolate + 双向通信\nfinal receivePort = ReceivePort();\nawait Isolate.spawn(heavyWork, receivePort.sendPort);\nreceivePort.listen((message) {\n  if (message is SendPort) {\n    message.send('start');\n  } else {\n    print('Result: $message');\n  }\n});\n","dart",[136,11166,11167,11172,11177,11181,11186,11191,11196,11201,11206,11211,11216,11220,11225,11229,11234,11239,11244,11249,11254,11258,11263,11268,11272,11277,11282,11287,11292,11297,11302,11307,11312,11316],{"__ignoreMap":191},[195,11168,11169],{"class":197,"line":198},[195,11170,11171],{},"\u002F\u002F 事件循环（单线程模型）\n",[195,11173,11174],{"class":197,"line":230},[195,11175,11176],{},"\u002F\u002F 执行顺序：同步代码 → Microtask Queue → Event Queue\n",[195,11178,11179],{"class":197,"line":251},[195,11180,1241],{"emptyLinePlaceholder":757},[195,11182,11183],{"class":197,"line":272},[195,11184,11185],{},"void main() {\n",[195,11187,11188],{"class":197,"line":293},[195,11189,11190],{},"  print('1');                                    \u002F\u002F 同步\n",[195,11192,11193],{"class":197,"line":562},[195,11194,11195],{},"  Future(() => print('2'));                       \u002F\u002F Event Queue\n",[195,11197,11198],{"class":197,"line":583},[195,11199,11200],{},"  Future.microtask(() => print('3'));             \u002F\u002F Microtask Queue\n",[195,11202,11203],{"class":197,"line":962},[195,11204,11205],{},"  scheduleMicrotask(() => print('4'));            \u002F\u002F Microtask Queue\n",[195,11207,11208],{"class":197,"line":968},[195,11209,11210],{},"  Future(() => print('5'));                       \u002F\u002F Event Queue\n",[195,11212,11213],{"class":197,"line":1274},[195,11214,11215],{},"  print('6');                                    \u002F\u002F 同步\n",[195,11217,11218],{"class":197,"line":1282},[195,11219,552],{},[195,11221,11222],{"class":197,"line":1295},[195,11223,11224],{},"\u002F\u002F 输出：1, 6, 3, 4, 2, 5\n",[195,11226,11227],{"class":197,"line":1309},[195,11228,1241],{"emptyLinePlaceholder":757},[195,11230,11231],{"class":197,"line":2246},[195,11232,11233],{},"\u002F\u002F Isolate：真正的并行（独立内存，消息传递通信）\n",[195,11235,11236],{"class":197,"line":1996},[195,11237,11238],{},"\u002F\u002F 适用于 CPU 密集型任务：JSON 解析、图片处理、加密\n",[195,11240,11241],{"class":197,"line":2257},[195,11242,11243],{},"final result = await Isolate.run(() {\n",[195,11245,11246],{"class":197,"line":2262},[195,11247,11248],{},"  return jsonDecode(hugeJsonString); \u002F\u002F 在独立 Isolate 中执行\n",[195,11250,11251],{"class":197,"line":2267},[195,11252,11253],{},"});\n",[195,11255,11256],{"class":197,"line":2273},[195,11257,1241],{"emptyLinePlaceholder":757},[195,11259,11260],{"class":197,"line":2033},[195,11261,11262],{},"\u002F\u002F compute() 是 Flutter 封装的便捷方法\n",[195,11264,11265],{"class":197,"line":2284},[195,11266,11267],{},"final parsed = await compute(parseJson, rawData);\n",[195,11269,11270],{"class":197,"line":2460},[195,11271,1241],{"emptyLinePlaceholder":757},[195,11273,11274],{"class":197,"line":2466},[195,11275,11276],{},"\u002F\u002F 复杂场景：长时间运行的 Isolate + 双向通信\n",[195,11278,11279],{"class":197,"line":2472},[195,11280,11281],{},"final receivePort = ReceivePort();\n",[195,11283,11284],{"class":197,"line":2780},[195,11285,11286],{},"await Isolate.spawn(heavyWork, receivePort.sendPort);\n",[195,11288,11289],{"class":197,"line":2786},[195,11290,11291],{},"receivePort.listen((message) {\n",[195,11293,11294],{"class":197,"line":2792},[195,11295,11296],{},"  if (message is SendPort) {\n",[195,11298,11299],{"class":197,"line":2798},[195,11300,11301],{},"    message.send('start');\n",[195,11303,11304],{"class":197,"line":2804},[195,11305,11306],{},"  } else {\n",[195,11308,11309],{"class":197,"line":2810},[195,11310,11311],{},"    print('Result: $message');\n",[195,11313,11314],{"class":197,"line":2815},[195,11315,965],{},[195,11317,11318],{"class":197,"line":2820},[195,11319,11253],{},[1890,11321],{},[32,11323,11325],{"id":11324},"q152-dart-3-的新特性recordspatternssealed-class","Q15.2: Dart 3 的新特性：Records、Patterns、sealed class",[14,11327,11328],{},[125,11329,2077],{},[186,11331,11333],{"className":11162,"code":11332,"language":11164,"meta":191,"style":191},"\u002F\u002F Records（匿名复合类型）\n(String, int) getUserInfo() => ('Alice', 25);\n\nfinal info = getUserInfo();\nprint(info.$1); \u002F\u002F 'Alice'\nprint(info.$2); \u002F\u002F 25\n\n\u002F\u002F 命名字段\n({String name, int age}) getUser() => (name: 'Alice', age: 25);\nfinal user = getUser();\nprint(user.name);\n\n\u002F\u002F Patterns（模式匹配）\n\u002F\u002F switch 表达式\nString describe(Object obj) => switch (obj) {\n  int n when n \u003C 0 => 'negative',\n  int n => 'int: $n',\n  String s => 'string: $s',\n  (int x, int y) => 'point($x, $y)',  \u002F\u002F 解构 Record\n  [int first, ...rest] => 'list starting with $first',\n  {'name': String name} => 'map with name: $name',\n  _ => 'unknown',\n};\n\n\u002F\u002F if-case\nif (json case {'users': [{'name': String name}, ...]}) {\n  print('First user: $name');\n}\n\n\u002F\u002F sealed class + 穷举\nsealed class Shape {}\nclass Circle extends Shape { final double radius; Circle(this.radius); }\nclass Rect extends Shape { final double w, h; Rect(this.w, this.h); }\n\ndouble area(Shape shape) => switch (shape) {\n  Circle(radius: var r) => 3.14 * r * r,\n  Rect(w: var w, h: var h) => w * h,\n};\n\n\u002F\u002F class modifiers（Dart 3）\n\u002F\u002F base class → 只能继承，不能实现\n\u002F\u002F interface class → 只能实现，不能继承\n\u002F\u002F final class → 不能继承也不能实现（同文件除外）\n\u002F\u002F sealed class → 同文件内穷举子类\n",[136,11334,11335,11340,11345,11349,11354,11359,11364,11368,11373,11378,11383,11388,11392,11397,11402,11407,11412,11417,11422,11427,11432,11437,11442,11447,11451,11456,11461,11466,11470,11474,11479,11484,11489,11494,11498,11503,11508,11513,11517,11521,11526,11531,11536,11541],{"__ignoreMap":191},[195,11336,11337],{"class":197,"line":198},[195,11338,11339],{},"\u002F\u002F Records（匿名复合类型）\n",[195,11341,11342],{"class":197,"line":230},[195,11343,11344],{},"(String, int) getUserInfo() => ('Alice', 25);\n",[195,11346,11347],{"class":197,"line":251},[195,11348,1241],{"emptyLinePlaceholder":757},[195,11350,11351],{"class":197,"line":272},[195,11352,11353],{},"final info = getUserInfo();\n",[195,11355,11356],{"class":197,"line":293},[195,11357,11358],{},"print(info.$1); \u002F\u002F 'Alice'\n",[195,11360,11361],{"class":197,"line":562},[195,11362,11363],{},"print(info.$2); \u002F\u002F 25\n",[195,11365,11366],{"class":197,"line":583},[195,11367,1241],{"emptyLinePlaceholder":757},[195,11369,11370],{"class":197,"line":962},[195,11371,11372],{},"\u002F\u002F 命名字段\n",[195,11374,11375],{"class":197,"line":968},[195,11376,11377],{},"({String name, int age}) getUser() => (name: 'Alice', age: 25);\n",[195,11379,11380],{"class":197,"line":1274},[195,11381,11382],{},"final user = getUser();\n",[195,11384,11385],{"class":197,"line":1282},[195,11386,11387],{},"print(user.name);\n",[195,11389,11390],{"class":197,"line":1295},[195,11391,1241],{"emptyLinePlaceholder":757},[195,11393,11394],{"class":197,"line":1309},[195,11395,11396],{},"\u002F\u002F Patterns（模式匹配）\n",[195,11398,11399],{"class":197,"line":2246},[195,11400,11401],{},"\u002F\u002F switch 表达式\n",[195,11403,11404],{"class":197,"line":1996},[195,11405,11406],{},"String describe(Object obj) => switch (obj) {\n",[195,11408,11409],{"class":197,"line":2257},[195,11410,11411],{},"  int n when n \u003C 0 => 'negative',\n",[195,11413,11414],{"class":197,"line":2262},[195,11415,11416],{},"  int n => 'int: $n',\n",[195,11418,11419],{"class":197,"line":2267},[195,11420,11421],{},"  String s => 'string: $s',\n",[195,11423,11424],{"class":197,"line":2273},[195,11425,11426],{},"  (int x, int y) => 'point($x, $y)',  \u002F\u002F 解构 Record\n",[195,11428,11429],{"class":197,"line":2033},[195,11430,11431],{},"  [int first, ...rest] => 'list starting with $first',\n",[195,11433,11434],{"class":197,"line":2284},[195,11435,11436],{},"  {'name': String name} => 'map with name: $name',\n",[195,11438,11439],{"class":197,"line":2460},[195,11440,11441],{},"  _ => 'unknown',\n",[195,11443,11444],{"class":197,"line":2466},[195,11445,11446],{},"};\n",[195,11448,11449],{"class":197,"line":2472},[195,11450,1241],{"emptyLinePlaceholder":757},[195,11452,11453],{"class":197,"line":2780},[195,11454,11455],{},"\u002F\u002F if-case\n",[195,11457,11458],{"class":197,"line":2786},[195,11459,11460],{},"if (json case {'users': [{'name': String name}, ...]}) {\n",[195,11462,11463],{"class":197,"line":2792},[195,11464,11465],{},"  print('First user: $name');\n",[195,11467,11468],{"class":197,"line":2798},[195,11469,552],{},[195,11471,11472],{"class":197,"line":2804},[195,11473,1241],{"emptyLinePlaceholder":757},[195,11475,11476],{"class":197,"line":2810},[195,11477,11478],{},"\u002F\u002F sealed class + 穷举\n",[195,11480,11481],{"class":197,"line":2815},[195,11482,11483],{},"sealed class Shape {}\n",[195,11485,11486],{"class":197,"line":2820},[195,11487,11488],{},"class Circle extends Shape { final double radius; Circle(this.radius); }\n",[195,11490,11491],{"class":197,"line":2825},[195,11492,11493],{},"class Rect extends Shape { final double w, h; Rect(this.w, this.h); }\n",[195,11495,11496],{"class":197,"line":2831},[195,11497,1241],{"emptyLinePlaceholder":757},[195,11499,11500],{"class":197,"line":2837},[195,11501,11502],{},"double area(Shape shape) => switch (shape) {\n",[195,11504,11505],{"class":197,"line":2843},[195,11506,11507],{},"  Circle(radius: var r) => 3.14 * r * r,\n",[195,11509,11510],{"class":197,"line":2848},[195,11511,11512],{},"  Rect(w: var w, h: var h) => w * h,\n",[195,11514,11515],{"class":197,"line":2854},[195,11516,11446],{},[195,11518,11519],{"class":197,"line":2860},[195,11520,1241],{"emptyLinePlaceholder":757},[195,11522,11523],{"class":197,"line":2866},[195,11524,11525],{},"\u002F\u002F class modifiers（Dart 3）\n",[195,11527,11528],{"class":197,"line":2872},[195,11529,11530],{},"\u002F\u002F base class → 只能继承，不能实现\n",[195,11532,11533],{"class":197,"line":2878},[195,11534,11535],{},"\u002F\u002F interface class → 只能实现，不能继承\n",[195,11537,11538],{"class":197,"line":2884},[195,11539,11540],{},"\u002F\u002F final class → 不能继承也不能实现（同文件除外）\n",[195,11542,11543],{"class":197,"line":2890},[195,11544,11545],{},"\u002F\u002F sealed class → 同文件内穷举子类\n",[1890,11547],{},[18,11549,11551],{"id":11550},"_16-渲染与三棵树","16. 渲染与三棵树",[32,11553,11555],{"id":11554},"q161-widget-element-renderobject-三棵树详解","Q16.1: Widget \u002F Element \u002F RenderObject 三棵树详解",[14,11557,11558],{},[125,11559,2077],{},[186,11561,11564],{"className":11562,"code":11563,"language":1074},[1072],"Widget Tree          Element Tree            RenderObject Tree\n(不可变配置)          (可变实例)               (布局+绘制)\n\nScaffold ──────► StatefulElement\n  │                    │\n  ├─ AppBar ────► ComponentElement ──────► RenderFlex\n  │                    │\n  └─ ListView ──► SliverMultiBoxAdaptorElement ──► RenderSliverList\n       │\n       └─ ListTile ──► StatelessElement ──► RenderPadding\n                                               └─ RenderFlex\n",[136,11565,11563],{"__ignoreMap":191},[14,11567,11568],{},[125,11569,11570],{},"核心流程：",[706,11572,11573,11583,11603],{},[132,11574,11575,11578,11579,11582],{},[125,11576,11577],{},"Widget","：不可变的配置描述，每次 ",[136,11580,11581],{},"build()"," 可能创建新实例",[132,11584,11585,11588,11589],{},[125,11586,11587],{},"Element","：Widget 的实例化，管理生命周期，决定是否复用\n",[129,11590,11591],{},[132,11592,11593,333,11596,4728,11599,11602],{},[136,11594,11595],{},"canUpdate(oldWidget, newWidget)",[136,11597,11598],{},"runtimeType",[136,11600,11601],{},"key"," 都相同则复用",[132,11604,11605,11608,11609,11612,11613,698],{},[125,11606,11607],{},"RenderObject","：真正执行布局（",[136,11610,11611],{},"performLayout","）和绘制（",[136,11614,11615],{},"paint",[14,11617,11618],{},[125,11619,11620],{},"约束传递规则：",[186,11622,11625],{"className":11623,"code":11624,"language":1074},[1072],"Constraints go down（父传子约束）\nSizes go up（子回传尺寸）\nParent sets position（父设子位置）\n",[136,11626,11624],{"__ignoreMap":191},[186,11628,11630],{"className":11162,"code":11629,"language":11164,"meta":191,"style":191},"\u002F\u002F 理解约束的典型问题\nSizedBox(\n  width: 100,\n  height: 100,\n  child: Container(\n    width: 200,  \u002F\u002F 无效！父约束 tight=100，子不能大于 100\n    height: 200,\n    color: Colors.red,\n  ),\n)\n\u002F\u002F 实际渲染为 100x100\n\n\u002F\u002F ConstrainedBox 传递约束\nConstrainedBox(\n  constraints: BoxConstraints(maxWidth: 200),\n  child: Container(width: 300),  \u002F\u002F 实际宽度被限制为 200\n)\n",[136,11631,11632,11637,11642,11647,11652,11657,11662,11667,11672,11677,11681,11686,11690,11695,11700,11705,11710],{"__ignoreMap":191},[195,11633,11634],{"class":197,"line":198},[195,11635,11636],{},"\u002F\u002F 理解约束的典型问题\n",[195,11638,11639],{"class":197,"line":230},[195,11640,11641],{},"SizedBox(\n",[195,11643,11644],{"class":197,"line":251},[195,11645,11646],{},"  width: 100,\n",[195,11648,11649],{"class":197,"line":272},[195,11650,11651],{},"  height: 100,\n",[195,11653,11654],{"class":197,"line":293},[195,11655,11656],{},"  child: Container(\n",[195,11658,11659],{"class":197,"line":562},[195,11660,11661],{},"    width: 200,  \u002F\u002F 无效！父约束 tight=100，子不能大于 100\n",[195,11663,11664],{"class":197,"line":583},[195,11665,11666],{},"    height: 200,\n",[195,11668,11669],{"class":197,"line":962},[195,11670,11671],{},"    color: Colors.red,\n",[195,11673,11674],{"class":197,"line":968},[195,11675,11676],{},"  ),\n",[195,11678,11679],{"class":197,"line":1274},[195,11680,410],{},[195,11682,11683],{"class":197,"line":1282},[195,11684,11685],{},"\u002F\u002F 实际渲染为 100x100\n",[195,11687,11688],{"class":197,"line":1295},[195,11689,1241],{"emptyLinePlaceholder":757},[195,11691,11692],{"class":197,"line":1309},[195,11693,11694],{},"\u002F\u002F ConstrainedBox 传递约束\n",[195,11696,11697],{"class":197,"line":2246},[195,11698,11699],{},"ConstrainedBox(\n",[195,11701,11702],{"class":197,"line":1996},[195,11703,11704],{},"  constraints: BoxConstraints(maxWidth: 200),\n",[195,11706,11707],{"class":197,"line":2257},[195,11708,11709],{},"  child: Container(width: 300),  \u002F\u002F 实际宽度被限制为 200\n",[195,11711,11712],{"class":197,"line":2262},[195,11713,410],{},[1890,11715],{},[18,11717,11719],{"id":11718},"_17-状态管理","17. 状态管理",[32,11721,11723],{"id":11722},"q171-bloc-vs-riverpod-深度对比","Q17.1: BLoC vs Riverpod 深度对比",[14,11725,11726],{},[125,11727,2077],{},[14,11729,11730],{},[125,11731,11732],{},"BLoC（Business Logic Component）：",[186,11734,11736],{"className":11162,"code":11735,"language":11164,"meta":191,"style":191},"\u002F\u002F 事件驱动、流式响应\n\u002F\u002F Event → BLoC → State\n\nsealed class AuthEvent {}\nclass LoginRequested extends AuthEvent {\n  final String email, password;\n  LoginRequested(this.email, this.password);\n}\nclass LogoutRequested extends AuthEvent {}\n\nsealed class AuthState {}\nclass AuthInitial extends AuthState {}\nclass AuthLoading extends AuthState {}\nclass AuthSuccess extends AuthState { final User user; AuthSuccess(this.user); }\nclass AuthFailure extends AuthState { final String message; AuthFailure(this.message); }\n\nclass AuthBloc extends Bloc\u003CAuthEvent, AuthState> {\n  final AuthRepository _repo;\n\n  AuthBloc(this._repo) : super(AuthInitial()) {\n    on\u003CLoginRequested>(_onLogin);\n    on\u003CLogoutRequested>(_onLogout);\n  }\n\n  Future\u003Cvoid> _onLogin(LoginRequested event, Emitter\u003CAuthState> emit) async {\n    emit(AuthLoading());\n    try {\n      final user = await _repo.login(event.email, event.password);\n      emit(AuthSuccess(user));\n    } catch (e) {\n      emit(AuthFailure(e.toString()));\n    }\n  }\n\n  Future\u003Cvoid> _onLogout(LogoutRequested event, Emitter\u003CAuthState> emit) async {\n    await _repo.logout();\n    emit(AuthInitial());\n  }\n}\n\n\u002F\u002F UI\nBlocBuilder\u003CAuthBloc, AuthState>(\n  builder: (context, state) => switch (state) {\n    AuthInitial() => LoginForm(),\n    AuthLoading() => CircularProgressIndicator(),\n    AuthSuccess(user: final u) => HomePage(user: u),\n    AuthFailure(message: final m) => ErrorView(message: m),\n  },\n)\n",[136,11737,11738,11743,11748,11752,11757,11762,11767,11772,11776,11781,11785,11790,11795,11800,11805,11810,11814,11819,11824,11828,11833,11838,11843,11847,11851,11856,11861,11865,11870,11875,11880,11885,11889,11893,11897,11902,11907,11912,11916,11920,11924,11929,11934,11939,11944,11949,11954,11959,11964],{"__ignoreMap":191},[195,11739,11740],{"class":197,"line":198},[195,11741,11742],{},"\u002F\u002F 事件驱动、流式响应\n",[195,11744,11745],{"class":197,"line":230},[195,11746,11747],{},"\u002F\u002F Event → BLoC → State\n",[195,11749,11750],{"class":197,"line":251},[195,11751,1241],{"emptyLinePlaceholder":757},[195,11753,11754],{"class":197,"line":272},[195,11755,11756],{},"sealed class AuthEvent {}\n",[195,11758,11759],{"class":197,"line":293},[195,11760,11761],{},"class LoginRequested extends AuthEvent {\n",[195,11763,11764],{"class":197,"line":562},[195,11765,11766],{},"  final String email, password;\n",[195,11768,11769],{"class":197,"line":583},[195,11770,11771],{},"  LoginRequested(this.email, this.password);\n",[195,11773,11774],{"class":197,"line":962},[195,11775,552],{},[195,11777,11778],{"class":197,"line":968},[195,11779,11780],{},"class LogoutRequested extends AuthEvent {}\n",[195,11782,11783],{"class":197,"line":1274},[195,11784,1241],{"emptyLinePlaceholder":757},[195,11786,11787],{"class":197,"line":1282},[195,11788,11789],{},"sealed class AuthState {}\n",[195,11791,11792],{"class":197,"line":1295},[195,11793,11794],{},"class AuthInitial extends AuthState {}\n",[195,11796,11797],{"class":197,"line":1309},[195,11798,11799],{},"class AuthLoading extends AuthState {}\n",[195,11801,11802],{"class":197,"line":2246},[195,11803,11804],{},"class AuthSuccess extends AuthState { final User user; AuthSuccess(this.user); }\n",[195,11806,11807],{"class":197,"line":1996},[195,11808,11809],{},"class AuthFailure extends AuthState { final String message; AuthFailure(this.message); }\n",[195,11811,11812],{"class":197,"line":2257},[195,11813,1241],{"emptyLinePlaceholder":757},[195,11815,11816],{"class":197,"line":2262},[195,11817,11818],{},"class AuthBloc extends Bloc\u003CAuthEvent, AuthState> {\n",[195,11820,11821],{"class":197,"line":2267},[195,11822,11823],{},"  final AuthRepository _repo;\n",[195,11825,11826],{"class":197,"line":2273},[195,11827,1241],{"emptyLinePlaceholder":757},[195,11829,11830],{"class":197,"line":2033},[195,11831,11832],{},"  AuthBloc(this._repo) : super(AuthInitial()) {\n",[195,11834,11835],{"class":197,"line":2284},[195,11836,11837],{},"    on\u003CLoginRequested>(_onLogin);\n",[195,11839,11840],{"class":197,"line":2460},[195,11841,11842],{},"    on\u003CLogoutRequested>(_onLogout);\n",[195,11844,11845],{"class":197,"line":2466},[195,11846,965],{},[195,11848,11849],{"class":197,"line":2472},[195,11850,1241],{"emptyLinePlaceholder":757},[195,11852,11853],{"class":197,"line":2780},[195,11854,11855],{},"  Future\u003Cvoid> _onLogin(LoginRequested event, Emitter\u003CAuthState> emit) async {\n",[195,11857,11858],{"class":197,"line":2786},[195,11859,11860],{},"    emit(AuthLoading());\n",[195,11862,11863],{"class":197,"line":2792},[195,11864,9611],{},[195,11866,11867],{"class":197,"line":2798},[195,11868,11869],{},"      final user = await _repo.login(event.email, event.password);\n",[195,11871,11872],{"class":197,"line":2804},[195,11873,11874],{},"      emit(AuthSuccess(user));\n",[195,11876,11877],{"class":197,"line":2810},[195,11878,11879],{},"    } catch (e) {\n",[195,11881,11882],{"class":197,"line":2815},[195,11883,11884],{},"      emit(AuthFailure(e.toString()));\n",[195,11886,11887],{"class":197,"line":2820},[195,11888,2403],{},[195,11890,11891],{"class":197,"line":2825},[195,11892,965],{},[195,11894,11895],{"class":197,"line":2831},[195,11896,1241],{"emptyLinePlaceholder":757},[195,11898,11899],{"class":197,"line":2837},[195,11900,11901],{},"  Future\u003Cvoid> _onLogout(LogoutRequested event, Emitter\u003CAuthState> emit) async {\n",[195,11903,11904],{"class":197,"line":2843},[195,11905,11906],{},"    await _repo.logout();\n",[195,11908,11909],{"class":197,"line":2848},[195,11910,11911],{},"    emit(AuthInitial());\n",[195,11913,11914],{"class":197,"line":2854},[195,11915,965],{},[195,11917,11918],{"class":197,"line":2860},[195,11919,552],{},[195,11921,11922],{"class":197,"line":2866},[195,11923,1241],{"emptyLinePlaceholder":757},[195,11925,11926],{"class":197,"line":2872},[195,11927,11928],{},"\u002F\u002F UI\n",[195,11930,11931],{"class":197,"line":2878},[195,11932,11933],{},"BlocBuilder\u003CAuthBloc, AuthState>(\n",[195,11935,11936],{"class":197,"line":2884},[195,11937,11938],{},"  builder: (context, state) => switch (state) {\n",[195,11940,11941],{"class":197,"line":2890},[195,11942,11943],{},"    AuthInitial() => LoginForm(),\n",[195,11945,11946],{"class":197,"line":2895},[195,11947,11948],{},"    AuthLoading() => CircularProgressIndicator(),\n",[195,11950,11951],{"class":197,"line":2900},[195,11952,11953],{},"    AuthSuccess(user: final u) => HomePage(user: u),\n",[195,11955,11956],{"class":197,"line":2905},[195,11957,11958],{},"    AuthFailure(message: final m) => ErrorView(message: m),\n",[195,11960,11961],{"class":197,"line":2911},[195,11962,11963],{},"  },\n",[195,11965,11966],{"class":197,"line":2917},[195,11967,410],{},[14,11969,11970],{},[125,11971,11972],{},"Riverpod：",[186,11974,11976],{"className":11162,"code":11975,"language":11164,"meta":191,"style":191},"\u002F\u002F 编译安全、无需 context、自动依赖管理\nfinal authRepositoryProvider = Provider((ref) => AuthRepository());\n\nfinal authStateProvider = StateNotifierProvider\u003CAuthNotifier, AuthState>((ref) {\n  return AuthNotifier(ref.read(authRepositoryProvider));\n});\n\nclass AuthNotifier extends StateNotifier\u003CAuthState> {\n  final AuthRepository _repo;\n  AuthNotifier(this._repo) : super(AuthInitial());\n\n  Future\u003Cvoid> login(String email, String password) async {\n    state = AuthLoading();\n    try {\n      final user = await _repo.login(email, password);\n      state = AuthSuccess(user);\n    } catch (e) {\n      state = AuthFailure(e.toString());\n    }\n  }\n}\n\n\u002F\u002F UI\nclass LoginPage extends ConsumerWidget {\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    final authState = ref.watch(authStateProvider);\n    return switch (authState) {\n      AuthInitial() => LoginForm(),\n      AuthLoading() => CircularProgressIndicator(),\n      AuthSuccess(user: final u) => HomePage(user: u),\n      AuthFailure(message: final m) => ErrorView(message: m),\n    };\n  }\n}\n",[136,11977,11978,11983,11988,11992,11997,12002,12006,12010,12015,12019,12024,12028,12033,12038,12042,12047,12052,12056,12061,12065,12069,12073,12077,12081,12086,12091,12096,12101,12106,12111,12116,12121,12126,12131,12135],{"__ignoreMap":191},[195,11979,11980],{"class":197,"line":198},[195,11981,11982],{},"\u002F\u002F 编译安全、无需 context、自动依赖管理\n",[195,11984,11985],{"class":197,"line":230},[195,11986,11987],{},"final authRepositoryProvider = Provider((ref) => AuthRepository());\n",[195,11989,11990],{"class":197,"line":251},[195,11991,1241],{"emptyLinePlaceholder":757},[195,11993,11994],{"class":197,"line":272},[195,11995,11996],{},"final authStateProvider = StateNotifierProvider\u003CAuthNotifier, AuthState>((ref) {\n",[195,11998,11999],{"class":197,"line":293},[195,12000,12001],{},"  return AuthNotifier(ref.read(authRepositoryProvider));\n",[195,12003,12004],{"class":197,"line":562},[195,12005,11253],{},[195,12007,12008],{"class":197,"line":583},[195,12009,1241],{"emptyLinePlaceholder":757},[195,12011,12012],{"class":197,"line":962},[195,12013,12014],{},"class AuthNotifier extends StateNotifier\u003CAuthState> {\n",[195,12016,12017],{"class":197,"line":968},[195,12018,11823],{},[195,12020,12021],{"class":197,"line":1274},[195,12022,12023],{},"  AuthNotifier(this._repo) : super(AuthInitial());\n",[195,12025,12026],{"class":197,"line":1282},[195,12027,1241],{"emptyLinePlaceholder":757},[195,12029,12030],{"class":197,"line":1295},[195,12031,12032],{},"  Future\u003Cvoid> login(String email, String password) async {\n",[195,12034,12035],{"class":197,"line":1309},[195,12036,12037],{},"    state = AuthLoading();\n",[195,12039,12040],{"class":197,"line":2246},[195,12041,9611],{},[195,12043,12044],{"class":197,"line":1996},[195,12045,12046],{},"      final user = await _repo.login(email, password);\n",[195,12048,12049],{"class":197,"line":2257},[195,12050,12051],{},"      state = AuthSuccess(user);\n",[195,12053,12054],{"class":197,"line":2262},[195,12055,11879],{},[195,12057,12058],{"class":197,"line":2267},[195,12059,12060],{},"      state = AuthFailure(e.toString());\n",[195,12062,12063],{"class":197,"line":2273},[195,12064,2403],{},[195,12066,12067],{"class":197,"line":2033},[195,12068,965],{},[195,12070,12071],{"class":197,"line":2284},[195,12072,552],{},[195,12074,12075],{"class":197,"line":2460},[195,12076,1241],{"emptyLinePlaceholder":757},[195,12078,12079],{"class":197,"line":2466},[195,12080,11928],{},[195,12082,12083],{"class":197,"line":2472},[195,12084,12085],{},"class LoginPage extends ConsumerWidget {\n",[195,12087,12088],{"class":197,"line":2780},[195,12089,12090],{},"  @override\n",[195,12092,12093],{"class":197,"line":2786},[195,12094,12095],{},"  Widget build(BuildContext context, WidgetRef ref) {\n",[195,12097,12098],{"class":197,"line":2792},[195,12099,12100],{},"    final authState = ref.watch(authStateProvider);\n",[195,12102,12103],{"class":197,"line":2798},[195,12104,12105],{},"    return switch (authState) {\n",[195,12107,12108],{"class":197,"line":2804},[195,12109,12110],{},"      AuthInitial() => LoginForm(),\n",[195,12112,12113],{"class":197,"line":2810},[195,12114,12115],{},"      AuthLoading() => CircularProgressIndicator(),\n",[195,12117,12118],{"class":197,"line":2815},[195,12119,12120],{},"      AuthSuccess(user: final u) => HomePage(user: u),\n",[195,12122,12123],{"class":197,"line":2820},[195,12124,12125],{},"      AuthFailure(message: final m) => ErrorView(message: m),\n",[195,12127,12128],{"class":197,"line":2825},[195,12129,12130],{},"    };\n",[195,12132,12133],{"class":197,"line":2831},[195,12134,965],{},[195,12136,12137],{"class":197,"line":2837},[195,12138,552],{},[36,12140,12141,12153],{},[39,12142,12143],{},[42,12144,12145,12147,12150],{},[45,12146],{},[45,12148,12149],{},"BLoC",[45,12151,12152],{},"Riverpod",[52,12154,12155,12165,12174,12185,12199,12210],{},[42,12156,12157,12159,12162],{},[57,12158,4091],{},[57,12160,12161],{},"事件 → BLoC → 状态（严格单向）",[57,12163,12164],{},"函数式、Provider 依赖图",[42,12166,12167,12170,12172],{},[57,12168,12169],{},"学习曲线",[57,12171,5930],{},[57,12173,5898],{},[42,12175,12176,12179,12182],{},[57,12177,12178],{},"模板代码",[57,12180,12181],{},"较多（Event + State + BLoC）",[57,12183,12184],{},"较少",[42,12186,12187,12190,12196],{},[57,12188,12189],{},"测试",[57,12191,12192,12195],{},[136,12193,12194],{},"blocTest()","，非常规范",[57,12197,12198],{},"Provider override，灵活",[42,12200,12201,12204,12207],{},[57,12202,12203],{},"context 依赖",[57,12205,12206],{},"需要（BlocProvider）",[57,12208,12209],{},"不需要（编译安全）",[42,12211,12212,12215,12218],{},[57,12213,12214],{},"适用团队",[57,12216,12217],{},"大型团队（规范强制）",[57,12219,12220],{},"中小型团队（灵活高效）",[1890,12222],{},[18,12224,12226],{"id":12225},"_18-平台通信与混合开发","18. 平台通信与混合开发",[32,12228,12230],{"id":12229},"q181-platform-channel-和-add-to-app-方案","Q18.1: Platform Channel 和 Add-to-App 方案",[14,12232,12233],{},[125,12234,2077],{},[186,12236,12238],{"className":11162,"code":12237,"language":11164,"meta":191,"style":191},"\u002F\u002F MethodChannel（最常用）\nclass NativeService {\n  static const _channel = MethodChannel('com.app\u002Fnative');\n\n  Future\u003CString> getPlatformVersion() async {\n    return await _channel.invokeMethod('getPlatformVersion');\n  }\n\n  \u002F\u002F 原生调用 Dart\n  void setupCallHandler() {\n    _channel.setMethodCallHandler((call) async {\n      switch (call.method) {\n        case 'onNativeEvent':\n          handleNativeEvent(call.arguments);\n          return 'handled';\n        default:\n          throw MissingPluginException();\n      }\n    });\n  }\n}\n",[136,12239,12240,12245,12250,12255,12259,12264,12269,12273,12277,12282,12287,12292,12297,12302,12307,12312,12317,12322,12327,12332,12336],{"__ignoreMap":191},[195,12241,12242],{"class":197,"line":198},[195,12243,12244],{},"\u002F\u002F MethodChannel（最常用）\n",[195,12246,12247],{"class":197,"line":230},[195,12248,12249],{},"class NativeService {\n",[195,12251,12252],{"class":197,"line":251},[195,12253,12254],{},"  static const _channel = MethodChannel('com.app\u002Fnative');\n",[195,12256,12257],{"class":197,"line":272},[195,12258,1241],{"emptyLinePlaceholder":757},[195,12260,12261],{"class":197,"line":293},[195,12262,12263],{},"  Future\u003CString> getPlatformVersion() async {\n",[195,12265,12266],{"class":197,"line":562},[195,12267,12268],{},"    return await _channel.invokeMethod('getPlatformVersion');\n",[195,12270,12271],{"class":197,"line":583},[195,12272,965],{},[195,12274,12275],{"class":197,"line":962},[195,12276,1241],{"emptyLinePlaceholder":757},[195,12278,12279],{"class":197,"line":968},[195,12280,12281],{},"  \u002F\u002F 原生调用 Dart\n",[195,12283,12284],{"class":197,"line":1274},[195,12285,12286],{},"  void setupCallHandler() {\n",[195,12288,12289],{"class":197,"line":1282},[195,12290,12291],{},"    _channel.setMethodCallHandler((call) async {\n",[195,12293,12294],{"class":197,"line":1295},[195,12295,12296],{},"      switch (call.method) {\n",[195,12298,12299],{"class":197,"line":1309},[195,12300,12301],{},"        case 'onNativeEvent':\n",[195,12303,12304],{"class":197,"line":2246},[195,12305,12306],{},"          handleNativeEvent(call.arguments);\n",[195,12308,12309],{"class":197,"line":1996},[195,12310,12311],{},"          return 'handled';\n",[195,12313,12314],{"class":197,"line":2257},[195,12315,12316],{},"        default:\n",[195,12318,12319],{"class":197,"line":2262},[195,12320,12321],{},"          throw MissingPluginException();\n",[195,12323,12324],{"class":197,"line":2267},[195,12325,12326],{},"      }\n",[195,12328,12329],{"class":197,"line":2273},[195,12330,12331],{},"    });\n",[195,12333,12334],{"class":197,"line":2033},[195,12335,965],{},[195,12337,12338],{"class":197,"line":2284},[195,12339,552],{},[186,12341,12343],{"className":2177,"code":12342,"language":2179,"meta":191,"style":191},"\u002F\u002F iOS 端（Swift）\nlet channel = FlutterMethodChannel(name: \"com.app\u002Fnative\",\n                                    binaryMessenger: controller.binaryMessenger)\nchannel.setMethodCallHandler { (call, result) in\n    switch call.method {\n    case \"getPlatformVersion\":\n        result(\"iOS \\(UIDevice.current.systemVersion)\")\n    default:\n        result(FlutterMethodNotImplemented)\n    }\n}\n",[136,12344,12345,12350,12355,12360,12365,12370,12375,12380,12385,12390,12394],{"__ignoreMap":191},[195,12346,12347],{"class":197,"line":198},[195,12348,12349],{},"\u002F\u002F iOS 端（Swift）\n",[195,12351,12352],{"class":197,"line":230},[195,12353,12354],{},"let channel = FlutterMethodChannel(name: \"com.app\u002Fnative\",\n",[195,12356,12357],{"class":197,"line":251},[195,12358,12359],{},"                                    binaryMessenger: controller.binaryMessenger)\n",[195,12361,12362],{"class":197,"line":272},[195,12363,12364],{},"channel.setMethodCallHandler { (call, result) in\n",[195,12366,12367],{"class":197,"line":293},[195,12368,12369],{},"    switch call.method {\n",[195,12371,12372],{"class":197,"line":562},[195,12373,12374],{},"    case \"getPlatformVersion\":\n",[195,12376,12377],{"class":197,"line":583},[195,12378,12379],{},"        result(\"iOS \\(UIDevice.current.systemVersion)\")\n",[195,12381,12382],{"class":197,"line":962},[195,12383,12384],{},"    default:\n",[195,12386,12387],{"class":197,"line":968},[195,12388,12389],{},"        result(FlutterMethodNotImplemented)\n",[195,12391,12392],{"class":197,"line":1274},[195,12393,2403],{},[195,12395,12396],{"class":197,"line":1282},[195,12397,552],{},[186,12399,12401],{"className":6838,"code":12400,"language":6840,"meta":191,"style":191},"\u002F\u002F Android 端（Kotlin）\nMethodChannel(flutterEngine.dartExecutor.binaryMessenger, \"com.app\u002Fnative\")\n    .setMethodCallHandler { call, result ->\n        when (call.method) {\n            \"getPlatformVersion\" -> result.success(\"Android ${Build.VERSION.RELEASE}\")\n            else -> result.notImplemented()\n        }\n    }\n",[136,12402,12403,12408,12413,12418,12423,12428,12433,12437],{"__ignoreMap":191},[195,12404,12405],{"class":197,"line":198},[195,12406,12407],{},"\u002F\u002F Android 端（Kotlin）\n",[195,12409,12410],{"class":197,"line":230},[195,12411,12412],{},"MethodChannel(flutterEngine.dartExecutor.binaryMessenger, \"com.app\u002Fnative\")\n",[195,12414,12415],{"class":197,"line":251},[195,12416,12417],{},"    .setMethodCallHandler { call, result ->\n",[195,12419,12420],{"class":197,"line":272},[195,12421,12422],{},"        when (call.method) {\n",[195,12424,12425],{"class":197,"line":293},[195,12426,12427],{},"            \"getPlatformVersion\" -> result.success(\"Android ${Build.VERSION.RELEASE}\")\n",[195,12429,12430],{"class":197,"line":562},[195,12431,12432],{},"            else -> result.notImplemented()\n",[195,12434,12435],{"class":197,"line":583},[195,12436,2887],{},[195,12438,12439],{"class":197,"line":962},[195,12440,2403],{},[14,12442,12443],{},[125,12444,12445],{},"Pigeon（类型安全的代码生成替代方案）：",[186,12447,12449],{"className":11162,"code":12448,"language":11164,"meta":191,"style":191},"\u002F\u002F 定义接口（pigeon 文件）\n@HostApi()\nabstract class NativeApi {\n  String getPlatformVersion();\n  @async\n  UserInfo getUserInfo(String userId);\n}\n\n\u002F\u002F 自动生成 Dart\u002FSwift\u002FKotlin 代码，类型安全无需手动序列化\n",[136,12450,12451,12456,12461,12466,12471,12476,12481,12485,12489],{"__ignoreMap":191},[195,12452,12453],{"class":197,"line":198},[195,12454,12455],{},"\u002F\u002F 定义接口（pigeon 文件）\n",[195,12457,12458],{"class":197,"line":230},[195,12459,12460],{},"@HostApi()\n",[195,12462,12463],{"class":197,"line":251},[195,12464,12465],{},"abstract class NativeApi {\n",[195,12467,12468],{"class":197,"line":272},[195,12469,12470],{},"  String getPlatformVersion();\n",[195,12472,12473],{"class":197,"line":293},[195,12474,12475],{},"  @async\n",[195,12477,12478],{"class":197,"line":562},[195,12479,12480],{},"  UserInfo getUserInfo(String userId);\n",[195,12482,12483],{"class":197,"line":583},[195,12484,552],{},[195,12486,12487],{"class":197,"line":962},[195,12488,1241],{"emptyLinePlaceholder":757},[195,12490,12491],{"class":197,"line":968},[195,12492,12493],{},"\u002F\u002F 自动生成 Dart\u002FSwift\u002FKotlin 代码，类型安全无需手动序列化\n",[14,12495,12496],{},[125,12497,12498],{},"Add-to-App（在现有原生应用中嵌入 Flutter）：",[186,12500,12502],{"className":2177,"code":12501,"language":2179,"meta":191,"style":191},"\u002F\u002F iOS：将 Flutter 模块作为 Framework 引入\nlet flutterEngine = FlutterEngine(name: \"my_engine\")\nflutterEngine.run()\n\nlet flutterVC = FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)\npresent(flutterVC, animated: true)\n",[136,12503,12504,12509,12514,12519,12523,12528],{"__ignoreMap":191},[195,12505,12506],{"class":197,"line":198},[195,12507,12508],{},"\u002F\u002F iOS：将 Flutter 模块作为 Framework 引入\n",[195,12510,12511],{"class":197,"line":230},[195,12512,12513],{},"let flutterEngine = FlutterEngine(name: \"my_engine\")\n",[195,12515,12516],{"class":197,"line":251},[195,12517,12518],{},"flutterEngine.run()\n",[195,12520,12521],{"class":197,"line":272},[195,12522,1241],{"emptyLinePlaceholder":757},[195,12524,12525],{"class":197,"line":293},[195,12526,12527],{},"let flutterVC = FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)\n",[195,12529,12530],{"class":197,"line":562},[195,12531,12532],{},"present(flutterVC, animated: true)\n",[186,12534,12536],{"className":6838,"code":12535,"language":6840,"meta":191,"style":191},"\u002F\u002F Android：FlutterFragment 或 FlutterActivity\nval flutterFragment = FlutterFragment\n    .withCachedEngine(\"my_engine\")\n    .build\u003CFlutterFragment>()\n\nsupportFragmentManager.beginTransaction()\n    .replace(R.id.container, flutterFragment)\n    .commit()\n",[136,12537,12538,12543,12548,12553,12558,12562,12567,12572],{"__ignoreMap":191},[195,12539,12540],{"class":197,"line":198},[195,12541,12542],{},"\u002F\u002F Android：FlutterFragment 或 FlutterActivity\n",[195,12544,12545],{"class":197,"line":230},[195,12546,12547],{},"val flutterFragment = FlutterFragment\n",[195,12549,12550],{"class":197,"line":251},[195,12551,12552],{},"    .withCachedEngine(\"my_engine\")\n",[195,12554,12555],{"class":197,"line":272},[195,12556,12557],{},"    .build\u003CFlutterFragment>()\n",[195,12559,12560],{"class":197,"line":293},[195,12561,1241],{"emptyLinePlaceholder":757},[195,12563,12564],{"class":197,"line":562},[195,12565,12566],{},"supportFragmentManager.beginTransaction()\n",[195,12568,12569],{"class":197,"line":583},[195,12570,12571],{},"    .replace(R.id.container, flutterFragment)\n",[195,12573,12574],{"class":197,"line":962},[195,12575,12576],{},"    .commit()\n",[1890,12578],{},[18,12580,12582],{"id":12581},"_19-性能优化flutter","19. 性能优化（Flutter）",[32,12584,12586],{"id":12585},"q191-flutter-性能优化全面指南","Q19.1: Flutter 性能优化全面指南",[14,12588,12589],{},[125,12590,2077],{},[186,12592,12594],{"className":11162,"code":12593,"language":11164,"meta":191,"style":191},"\u002F\u002F 1. 减少 rebuild 范围\n\u002F\u002F ❌ 整个页面因一个计数器重建\nclass _PageState extends State\u003CPage> {\n  int count = 0;\n  Widget build(context) {\n    return Column(children: [\n      ExpensiveHeader(),    \u002F\u002F 不需要重建\n      Text('$count'),       \u002F\u002F 需要重建\n      ExpensiveFooter(),    \u002F\u002F 不需要重建\n    ]);\n  }\n}\n\n\u002F\u002F ✅ 拆分为独立 Widget\nclass CounterText extends StatefulWidget { ... }\n\u002F\u002F 或使用 ValueListenableBuilder\nValueListenableBuilder\u003Cint>(\n  valueListenable: counter,\n  builder: (context, value, child) => Text('$value'),\n)\n\n\u002F\u002F 2. const 构造函数\nconst SizedBox(height: 16)  \u002F\u002F 编译期创建，可复用\nconst EdgeInsets.all(8)\nconst TextStyle(fontSize: 14)\n\n\u002F\u002F 3. ListView 优化\nListView.builder(\n  itemCount: 10000,\n  itemExtent: 72,          \u002F\u002F 固定高度，跳过布局计算\n  itemBuilder: (ctx, i) => ItemTile(items[i]),\n)\n\n\u002F\u002F 4. 图片优化\nImage.asset('photo.png', cacheWidth: 200)  \u002F\u002F 降采样\nCachedNetworkImage(imageUrl: url)          \u002F\u002F 缓存网络图片\n\n\u002F\u002F 5. 动画性能\nAnimatedBuilder(\n  animation: controller,\n  child: const ExpensiveChild(),  \u002F\u002F 缓存不变的子树\n  builder: (ctx, child) => Transform.scale(\n    scale: controller.value,\n    child: child,  \u002F\u002F 复用\n  ),\n)\n\n\u002F\u002F 6. Shader 预热（Impeller 已大幅缓解）\n\u002F\u002F 收集 SkSL → 编译时打包\n\u002F\u002F flutter run --profile --cache-sksl --purge-persistent-cache\n\u002F\u002F flutter build apk --bundle-sksl-path flutter_01.sksl.json\n\n\u002F\u002F 7. 使用 DevTools 分析\n\u002F\u002F flutter run --profile\n\u002F\u002F 打开 DevTools → Performance → 分析帧耗时\n",[136,12595,12596,12601,12606,12611,12616,12621,12626,12631,12636,12641,12646,12650,12654,12658,12663,12668,12673,12678,12683,12688,12692,12696,12701,12706,12711,12716,12720,12725,12730,12735,12740,12745,12749,12753,12758,12763,12768,12772,12777,12782,12787,12792,12797,12802,12807,12811,12815,12819,12824,12829,12834,12839,12843,12848,12853],{"__ignoreMap":191},[195,12597,12598],{"class":197,"line":198},[195,12599,12600],{},"\u002F\u002F 1. 减少 rebuild 范围\n",[195,12602,12603],{"class":197,"line":230},[195,12604,12605],{},"\u002F\u002F ❌ 整个页面因一个计数器重建\n",[195,12607,12608],{"class":197,"line":251},[195,12609,12610],{},"class _PageState extends State\u003CPage> {\n",[195,12612,12613],{"class":197,"line":272},[195,12614,12615],{},"  int count = 0;\n",[195,12617,12618],{"class":197,"line":293},[195,12619,12620],{},"  Widget build(context) {\n",[195,12622,12623],{"class":197,"line":562},[195,12624,12625],{},"    return Column(children: [\n",[195,12627,12628],{"class":197,"line":583},[195,12629,12630],{},"      ExpensiveHeader(),    \u002F\u002F 不需要重建\n",[195,12632,12633],{"class":197,"line":962},[195,12634,12635],{},"      Text('$count'),       \u002F\u002F 需要重建\n",[195,12637,12638],{"class":197,"line":968},[195,12639,12640],{},"      ExpensiveFooter(),    \u002F\u002F 不需要重建\n",[195,12642,12643],{"class":197,"line":1274},[195,12644,12645],{},"    ]);\n",[195,12647,12648],{"class":197,"line":1282},[195,12649,965],{},[195,12651,12652],{"class":197,"line":1295},[195,12653,552],{},[195,12655,12656],{"class":197,"line":1309},[195,12657,1241],{"emptyLinePlaceholder":757},[195,12659,12660],{"class":197,"line":2246},[195,12661,12662],{},"\u002F\u002F ✅ 拆分为独立 Widget\n",[195,12664,12665],{"class":197,"line":1996},[195,12666,12667],{},"class CounterText extends StatefulWidget { ... }\n",[195,12669,12670],{"class":197,"line":2257},[195,12671,12672],{},"\u002F\u002F 或使用 ValueListenableBuilder\n",[195,12674,12675],{"class":197,"line":2262},[195,12676,12677],{},"ValueListenableBuilder\u003Cint>(\n",[195,12679,12680],{"class":197,"line":2267},[195,12681,12682],{},"  valueListenable: counter,\n",[195,12684,12685],{"class":197,"line":2273},[195,12686,12687],{},"  builder: (context, value, child) => Text('$value'),\n",[195,12689,12690],{"class":197,"line":2033},[195,12691,410],{},[195,12693,12694],{"class":197,"line":2284},[195,12695,1241],{"emptyLinePlaceholder":757},[195,12697,12698],{"class":197,"line":2460},[195,12699,12700],{},"\u002F\u002F 2. const 构造函数\n",[195,12702,12703],{"class":197,"line":2466},[195,12704,12705],{},"const SizedBox(height: 16)  \u002F\u002F 编译期创建，可复用\n",[195,12707,12708],{"class":197,"line":2472},[195,12709,12710],{},"const EdgeInsets.all(8)\n",[195,12712,12713],{"class":197,"line":2780},[195,12714,12715],{},"const TextStyle(fontSize: 14)\n",[195,12717,12718],{"class":197,"line":2786},[195,12719,1241],{"emptyLinePlaceholder":757},[195,12721,12722],{"class":197,"line":2792},[195,12723,12724],{},"\u002F\u002F 3. ListView 优化\n",[195,12726,12727],{"class":197,"line":2798},[195,12728,12729],{},"ListView.builder(\n",[195,12731,12732],{"class":197,"line":2804},[195,12733,12734],{},"  itemCount: 10000,\n",[195,12736,12737],{"class":197,"line":2810},[195,12738,12739],{},"  itemExtent: 72,          \u002F\u002F 固定高度，跳过布局计算\n",[195,12741,12742],{"class":197,"line":2815},[195,12743,12744],{},"  itemBuilder: (ctx, i) => ItemTile(items[i]),\n",[195,12746,12747],{"class":197,"line":2820},[195,12748,410],{},[195,12750,12751],{"class":197,"line":2825},[195,12752,1241],{"emptyLinePlaceholder":757},[195,12754,12755],{"class":197,"line":2831},[195,12756,12757],{},"\u002F\u002F 4. 图片优化\n",[195,12759,12760],{"class":197,"line":2837},[195,12761,12762],{},"Image.asset('photo.png', cacheWidth: 200)  \u002F\u002F 降采样\n",[195,12764,12765],{"class":197,"line":2843},[195,12766,12767],{},"CachedNetworkImage(imageUrl: url)          \u002F\u002F 缓存网络图片\n",[195,12769,12770],{"class":197,"line":2848},[195,12771,1241],{"emptyLinePlaceholder":757},[195,12773,12774],{"class":197,"line":2854},[195,12775,12776],{},"\u002F\u002F 5. 动画性能\n",[195,12778,12779],{"class":197,"line":2860},[195,12780,12781],{},"AnimatedBuilder(\n",[195,12783,12784],{"class":197,"line":2866},[195,12785,12786],{},"  animation: controller,\n",[195,12788,12789],{"class":197,"line":2872},[195,12790,12791],{},"  child: const ExpensiveChild(),  \u002F\u002F 缓存不变的子树\n",[195,12793,12794],{"class":197,"line":2878},[195,12795,12796],{},"  builder: (ctx, child) => Transform.scale(\n",[195,12798,12799],{"class":197,"line":2884},[195,12800,12801],{},"    scale: controller.value,\n",[195,12803,12804],{"class":197,"line":2890},[195,12805,12806],{},"    child: child,  \u002F\u002F 复用\n",[195,12808,12809],{"class":197,"line":2895},[195,12810,11676],{},[195,12812,12813],{"class":197,"line":2900},[195,12814,410],{},[195,12816,12817],{"class":197,"line":2905},[195,12818,1241],{"emptyLinePlaceholder":757},[195,12820,12821],{"class":197,"line":2911},[195,12822,12823],{},"\u002F\u002F 6. Shader 预热（Impeller 已大幅缓解）\n",[195,12825,12826],{"class":197,"line":2917},[195,12827,12828],{},"\u002F\u002F 收集 SkSL → 编译时打包\n",[195,12830,12831],{"class":197,"line":2923},[195,12832,12833],{},"\u002F\u002F flutter run --profile --cache-sksl --purge-persistent-cache\n",[195,12835,12836],{"class":197,"line":2929},[195,12837,12838],{},"\u002F\u002F flutter build apk --bundle-sksl-path flutter_01.sksl.json\n",[195,12840,12841],{"class":197,"line":2934},[195,12842,1241],{"emptyLinePlaceholder":757},[195,12844,12845],{"class":197,"line":2939},[195,12846,12847],{},"\u002F\u002F 7. 使用 DevTools 分析\n",[195,12849,12850],{"class":197,"line":2945},[195,12851,12852],{},"\u002F\u002F flutter run --profile\n",[195,12854,12855],{"class":197,"line":2951},[195,12856,12857],{},"\u002F\u002F 打开 DevTools → Performance → 分析帧耗时\n",[1890,12859],{},[2055,12861,2030],{"id":12862},"第四部分跨平台通用-1",[18,12864,12866],{"id":12865},"_20-网络与安全","20. 网络与安全",[32,12868,12870],{"id":12869},"q201-https证书固定certificate-pinning和网络安全最佳实践","Q20.1: HTTPS、证书固定（Certificate Pinning）和网络安全最佳实践",[14,12872,12873],{},[125,12874,2077],{},[186,12876,12879],{"className":12877,"code":12878,"language":1074},[1072],"TLS 握手过程：\nClient ──→ ClientHello（支持的密码套件）──→ Server\nClient ←── ServerHello + 证书 ←── Server\nClient 验证证书链 → 信任 CA → 建立加密连接\n",[136,12880,12878],{"__ignoreMap":191},[14,12882,12883],{},[125,12884,12885],{},"证书固定（Certificate Pinning）：",[186,12887,12889],{"className":2177,"code":12888,"language":2179,"meta":191,"style":191},"\u002F\u002F iOS：使用 URLSession 实现\nclass PinningDelegate: NSObject, URLSessionDelegate {\n    let pinnedHashes: Set\u003CString> = [\"sha256\u002FBBBBBBBBB...\"]\n\n    func urlSession(_ session: URLSession,\n                    didReceive challenge: URLAuthenticationChallenge,\n                    completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {\n        guard let serverTrust = challenge.protectionSpace.serverTrust,\n              let certificate = SecTrustGetCertificateAtIndex(serverTrust, 0) else {\n            completionHandler(.cancelAuthenticationChallenge, nil)\n            return\n        }\n\n        let serverHash = sha256(SecCertificateCopyData(certificate) as Data)\n        if pinnedHashes.contains(serverHash) {\n            completionHandler(.useCredential, URLCredential(trust: serverTrust))\n        } else {\n            completionHandler(.cancelAuthenticationChallenge, nil)\n        }\n    }\n}\n",[136,12890,12891,12896,12901,12906,12910,12915,12920,12925,12930,12935,12940,12945,12949,12953,12958,12963,12968,12972,12976,12980,12984],{"__ignoreMap":191},[195,12892,12893],{"class":197,"line":198},[195,12894,12895],{},"\u002F\u002F iOS：使用 URLSession 实现\n",[195,12897,12898],{"class":197,"line":230},[195,12899,12900],{},"class PinningDelegate: NSObject, URLSessionDelegate {\n",[195,12902,12903],{"class":197,"line":251},[195,12904,12905],{},"    let pinnedHashes: Set\u003CString> = [\"sha256\u002FBBBBBBBBB...\"]\n",[195,12907,12908],{"class":197,"line":272},[195,12909,1241],{"emptyLinePlaceholder":757},[195,12911,12912],{"class":197,"line":293},[195,12913,12914],{},"    func urlSession(_ session: URLSession,\n",[195,12916,12917],{"class":197,"line":562},[195,12918,12919],{},"                    didReceive challenge: URLAuthenticationChallenge,\n",[195,12921,12922],{"class":197,"line":583},[195,12923,12924],{},"                    completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {\n",[195,12926,12927],{"class":197,"line":962},[195,12928,12929],{},"        guard let serverTrust = challenge.protectionSpace.serverTrust,\n",[195,12931,12932],{"class":197,"line":968},[195,12933,12934],{},"              let certificate = SecTrustGetCertificateAtIndex(serverTrust, 0) else {\n",[195,12936,12937],{"class":197,"line":1274},[195,12938,12939],{},"            completionHandler(.cancelAuthenticationChallenge, nil)\n",[195,12941,12942],{"class":197,"line":1282},[195,12943,12944],{},"            return\n",[195,12946,12947],{"class":197,"line":1295},[195,12948,2887],{},[195,12950,12951],{"class":197,"line":1309},[195,12952,1241],{"emptyLinePlaceholder":757},[195,12954,12955],{"class":197,"line":2246},[195,12956,12957],{},"        let serverHash = sha256(SecCertificateCopyData(certificate) as Data)\n",[195,12959,12960],{"class":197,"line":1996},[195,12961,12962],{},"        if pinnedHashes.contains(serverHash) {\n",[195,12964,12965],{"class":197,"line":2257},[195,12966,12967],{},"            completionHandler(.useCredential, URLCredential(trust: serverTrust))\n",[195,12969,12970],{"class":197,"line":2262},[195,12971,5625],{},[195,12973,12974],{"class":197,"line":2267},[195,12975,12939],{},[195,12977,12978],{"class":197,"line":2273},[195,12979,2887],{},[195,12981,12982],{"class":197,"line":2033},[195,12983,2403],{},[195,12985,12986],{"class":197,"line":2284},[195,12987,552],{},[186,12989,12991],{"className":6838,"code":12990,"language":6840,"meta":191,"style":191},"\u002F\u002F Android：OkHttp 证书固定\nval client = OkHttpClient.Builder()\n    .certificatePinner(CertificatePinner.Builder()\n        .add(\"api.example.com\", \"sha256\u002FBBBBBBBBB...\")\n        .add(\"api.example.com\", \"sha256\u002Fbackup-pin...\")  \u002F\u002F 备用 pin\n        .build())\n    .build()\n",[136,12992,12993,12998,13003,13008,13013,13018,13022],{"__ignoreMap":191},[195,12994,12995],{"class":197,"line":198},[195,12996,12997],{},"\u002F\u002F Android：OkHttp 证书固定\n",[195,12999,13000],{"class":197,"line":230},[195,13001,13002],{},"val client = OkHttpClient.Builder()\n",[195,13004,13005],{"class":197,"line":251},[195,13006,13007],{},"    .certificatePinner(CertificatePinner.Builder()\n",[195,13009,13010],{"class":197,"line":272},[195,13011,13012],{},"        .add(\"api.example.com\", \"sha256\u002FBBBBBBBBB...\")\n",[195,13014,13015],{"class":197,"line":293},[195,13016,13017],{},"        .add(\"api.example.com\", \"sha256\u002Fbackup-pin...\")  \u002F\u002F 备用 pin\n",[195,13019,13020],{"class":197,"line":562},[195,13021,8269],{},[195,13023,13024],{"class":197,"line":583},[195,13025,8279],{},[186,13027,13031],{"className":13028,"code":13029,"language":13030,"meta":191,"style":191},"language-xml shiki shiki-themes github-light github-dark","\u003C!-- Android：Network Security Config（推荐） -->\n\u003C!-- res\u002Fxml\u002Fnetwork_security_config.xml -->\n\u003Cnetwork-security-config>\n    \u003Cdomain-config cleartextTrafficPermitted=\"false\">\n        \u003Cdomain includeSubdomains=\"true\">api.example.com\u003C\u002Fdomain>\n        \u003Cpin-set expiration=\"2025-12-31\">\n            \u003Cpin digest=\"SHA-256\">base64-encoded-hash\u003C\u002Fpin>\n            \u003Cpin digest=\"SHA-256\">backup-pin-hash\u003C\u002Fpin>\n        \u003C\u002Fpin-set>\n    \u003C\u002Fdomain-config>\n\u003C\u002Fnetwork-security-config>\n","xml",[136,13032,13033,13038,13043,13048,13053,13058,13063,13068,13073,13078,13083],{"__ignoreMap":191},[195,13034,13035],{"class":197,"line":198},[195,13036,13037],{},"\u003C!-- Android：Network Security Config（推荐） -->\n",[195,13039,13040],{"class":197,"line":230},[195,13041,13042],{},"\u003C!-- res\u002Fxml\u002Fnetwork_security_config.xml -->\n",[195,13044,13045],{"class":197,"line":251},[195,13046,13047],{},"\u003Cnetwork-security-config>\n",[195,13049,13050],{"class":197,"line":272},[195,13051,13052],{},"    \u003Cdomain-config cleartextTrafficPermitted=\"false\">\n",[195,13054,13055],{"class":197,"line":293},[195,13056,13057],{},"        \u003Cdomain includeSubdomains=\"true\">api.example.com\u003C\u002Fdomain>\n",[195,13059,13060],{"class":197,"line":562},[195,13061,13062],{},"        \u003Cpin-set expiration=\"2025-12-31\">\n",[195,13064,13065],{"class":197,"line":583},[195,13066,13067],{},"            \u003Cpin digest=\"SHA-256\">base64-encoded-hash\u003C\u002Fpin>\n",[195,13069,13070],{"class":197,"line":962},[195,13071,13072],{},"            \u003Cpin digest=\"SHA-256\">backup-pin-hash\u003C\u002Fpin>\n",[195,13074,13075],{"class":197,"line":968},[195,13076,13077],{},"        \u003C\u002Fpin-set>\n",[195,13079,13080],{"class":197,"line":1274},[195,13081,13082],{},"    \u003C\u002Fdomain-config>\n",[195,13084,13085],{"class":197,"line":1282},[195,13086,13087],{},"\u003C\u002Fnetwork-security-config>\n",[14,13089,13090],{},[125,13091,13092],{},"安全存储：",[36,13094,13095,13107],{},[39,13096,13097],{},[42,13098,13099,13102,13105],{},[45,13100,13101],{},"平台",[45,13103,13104],{},"安全存储方案",[45,13106,3111],{},[52,13108,13109,13120,13131],{},[42,13110,13111,13114,13117],{},[57,13112,13113],{},"iOS",[57,13115,13116],{},"Keychain",[57,13118,13119],{},"Token、密码、证书",[42,13121,13122,13125,13128],{},[57,13123,13124],{},"Android",[57,13126,13127],{},"EncryptedSharedPreferences \u002F Keystore",[57,13129,13130],{},"Token、敏感配置",[42,13132,13133,13136,13139],{},[57,13134,13135],{},"Flutter",[57,13137,13138],{},"flutter_secure_storage（底层用 Keychain\u002FKeystore）",[57,13140,13141],{},"跨平台安全存储",[186,13143,13145],{"className":6838,"code":13144,"language":6840,"meta":191,"style":191},"\u002F\u002F Android EncryptedSharedPreferences\nval masterKey = MasterKey.Builder(context)\n    .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)\n    .build()\n\nval prefs = EncryptedSharedPreferences.create(\n    context, \"secret_prefs\", masterKey,\n    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,\n    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM\n)\nprefs.edit().putString(\"auth_token\", token).apply()\n",[136,13146,13147,13152,13157,13162,13166,13170,13175,13180,13185,13190,13194],{"__ignoreMap":191},[195,13148,13149],{"class":197,"line":198},[195,13150,13151],{},"\u002F\u002F Android EncryptedSharedPreferences\n",[195,13153,13154],{"class":197,"line":230},[195,13155,13156],{},"val masterKey = MasterKey.Builder(context)\n",[195,13158,13159],{"class":197,"line":251},[195,13160,13161],{},"    .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)\n",[195,13163,13164],{"class":197,"line":272},[195,13165,8279],{},[195,13167,13168],{"class":197,"line":293},[195,13169,1241],{"emptyLinePlaceholder":757},[195,13171,13172],{"class":197,"line":562},[195,13173,13174],{},"val prefs = EncryptedSharedPreferences.create(\n",[195,13176,13177],{"class":197,"line":583},[195,13178,13179],{},"    context, \"secret_prefs\", masterKey,\n",[195,13181,13182],{"class":197,"line":962},[195,13183,13184],{},"    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,\n",[195,13186,13187],{"class":197,"line":968},[195,13188,13189],{},"    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM\n",[195,13191,13192],{"class":197,"line":1274},[195,13193,410],{},[195,13195,13196],{"class":197,"line":1282},[195,13197,13198],{},"prefs.edit().putString(\"auth_token\", token).apply()\n",[1890,13200],{},[32,13202,13204],{"id":13203},"q202-移动端常见安全威胁与防护","Q20.2: 移动端常见安全威胁与防护",[14,13206,13207],{},[125,13208,2077],{},[36,13210,13211,13221],{},[39,13212,13213],{},[42,13214,13215,13218],{},[45,13216,13217],{},"威胁",[45,13219,13220],{},"防护措施",[52,13222,13223,13231,13239,13247,13255,13263,13271],{},[42,13224,13225,13228],{},[57,13226,13227],{},"中间人攻击（MITM）",[57,13229,13230],{},"证书固定、TLS 1.3",[42,13232,13233,13236],{},[57,13234,13235],{},"逆向工程",[57,13237,13238],{},"代码混淆（ProGuard\u002FR8）、防调试检测",[42,13240,13241,13244],{},[57,13242,13243],{},"数据泄露",[57,13245,13246],{},"加密存储、不在日志中打印敏感信息",[42,13248,13249,13252],{},[57,13250,13251],{},"越狱\u002FRoot 检测",[57,13253,13254],{},"Jailbreak\u002FRoot 检测 + 降级功能",[42,13256,13257,13260],{},[57,13258,13259],{},"不安全的 WebView",[57,13261,13262],{},"禁用 JavaScript（除非必要）、验证 URL scheme",[42,13264,13265,13268],{},[57,13266,13267],{},"剪贴板泄露",[57,13269,13270],{},"敏感字段禁止复制、清除剪贴板",[42,13272,13273,13276],{},[57,13274,13275],{},"截屏泄露",[57,13277,13278,13281,13282,13285],{},[136,13279,13280],{},"FLAG_SECURE","（Android）、",[136,13283,13284],{},"applicationDidEnterBackground"," 遮挡（iOS）",[186,13287,13289],{"className":2177,"code":13288,"language":2179,"meta":191,"style":191},"\u002F\u002F iOS 防截屏\nfunc applicationWillResignActive(_ application: UIApplication) {\n    let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .light))\n    blurView.tag = 999\n    window?.addSubview(blurView)\n}\n",[136,13290,13291,13296,13301,13306,13311,13316],{"__ignoreMap":191},[195,13292,13293],{"class":197,"line":198},[195,13294,13295],{},"\u002F\u002F iOS 防截屏\n",[195,13297,13298],{"class":197,"line":230},[195,13299,13300],{},"func applicationWillResignActive(_ application: UIApplication) {\n",[195,13302,13303],{"class":197,"line":251},[195,13304,13305],{},"    let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .light))\n",[195,13307,13308],{"class":197,"line":272},[195,13309,13310],{},"    blurView.tag = 999\n",[195,13312,13313],{"class":197,"line":293},[195,13314,13315],{},"    window?.addSubview(blurView)\n",[195,13317,13318],{"class":197,"line":562},[195,13319,552],{},[186,13321,13323],{"className":6838,"code":13322,"language":6840,"meta":191,"style":191},"\u002F\u002F Android 防截屏\nwindow.setFlags(\n    WindowManager.LayoutParams.FLAG_SECURE,\n    WindowManager.LayoutParams.FLAG_SECURE\n)\n",[136,13324,13325,13330,13335,13340,13345],{"__ignoreMap":191},[195,13326,13327],{"class":197,"line":198},[195,13328,13329],{},"\u002F\u002F Android 防截屏\n",[195,13331,13332],{"class":197,"line":230},[195,13333,13334],{},"window.setFlags(\n",[195,13336,13337],{"class":197,"line":251},[195,13338,13339],{},"    WindowManager.LayoutParams.FLAG_SECURE,\n",[195,13341,13342],{"class":197,"line":272},[195,13343,13344],{},"    WindowManager.LayoutParams.FLAG_SECURE\n",[195,13346,13347],{"class":197,"line":293},[195,13348,410],{},[1890,13350],{},[18,13352,13354],{"id":13353},"_21-cicd-与发布","21. CI\u002FCD 与发布",[32,13356,13358],{"id":13357},"q211-移动端-cicd-流水线设计","Q21.1: 移动端 CI\u002FCD 流水线设计",[14,13360,13361],{},[125,13362,2077],{},[186,13364,13368],{"className":13365,"code":13366,"language":13367,"meta":191,"style":191},"language-yaml shiki shiki-themes github-light github-dark","# 典型流水线（以 GitHub Actions 为例）\n# .github\u002Fworkflows\u002Fmobile.yml\n\nname: Mobile CI\u002FCD\n\non:\n  pull_request:\n    branches: [main]\n  push:\n    tags: ['v*']\n\njobs:\n  test:\n    runs-on: macos-latest\n    steps:\n      - uses: actions\u002Fcheckout@v4\n\n      # Flutter\n      - uses: subosito\u002Fflutter-action@v2\n        with:\n          flutter-version: '3.24.0'\n      - run: flutter pub get\n      - run: flutter analyze\n      - run: flutter test --coverage\n\n      # iOS\n      - run: xcodebuild test -workspace App.xcworkspace -scheme App -destination 'platform=iOS Simulator,name=iPhone 15'\n\n      # Android\n      - run: .\u002Fgradlew test\n      - run: .\u002Fgradlew lint\n\n  build-ios:\n    needs: test\n    runs-on: macos-latest\n    if: startsWith(github.ref, 'refs\u002Ftags\u002F')\n    steps:\n      - run: flutter build ipa --release --export-options-plist=ExportOptions.plist\n      - uses: apple-actions\u002Fupload-testflight-build@v1\n\n  build-android:\n    needs: test\n    runs-on: ubuntu-latest\n    if: startsWith(github.ref, 'refs\u002Ftags\u002F')\n    steps:\n      - run: flutter build appbundle --release\n      - uses: r0adkll\u002Fupload-google-play@v1\n        with:\n          track: internal\n","yaml",[136,13369,13370,13375,13380,13384,13394,13398,13406,13413,13427,13434,13446,13450,13457,13464,13474,13481,13494,13498,13503,13514,13521,13531,13542,13553,13564,13568,13573,13584,13588,13593,13604,13615,13619,13626,13636,13644,13654,13660,13671,13682,13686,13693,13701,13710,13718,13724,13735,13746,13752],{"__ignoreMap":191},[195,13371,13372],{"class":197,"line":198},[195,13373,13374],{"class":415},"# 典型流水线（以 GitHub Actions 为例）\n",[195,13376,13377],{"class":197,"line":230},[195,13378,13379],{"class":415},"# .github\u002Fworkflows\u002Fmobile.yml\n",[195,13381,13382],{"class":197,"line":251},[195,13383,1241],{"emptyLinePlaceholder":757},[195,13385,13386,13389,13391],{"class":197,"line":272},[195,13387,13388],{"class":205},"name",[195,13390,217],{"class":209},[195,13392,13393],{"class":459},"Mobile CI\u002FCD\n",[195,13395,13396],{"class":197,"line":293},[195,13397,1241],{"emptyLinePlaceholder":757},[195,13399,13400,13403],{"class":197,"line":562},[195,13401,13402],{"class":213},"on",[195,13404,13405],{"class":209},":\n",[195,13407,13408,13411],{"class":197,"line":583},[195,13409,13410],{"class":205},"  pull_request",[195,13412,13405],{"class":209},[195,13414,13415,13418,13421,13424],{"class":197,"line":962},[195,13416,13417],{"class":205},"    branches",[195,13419,13420],{"class":209},": [",[195,13422,13423],{"class":459},"main",[195,13425,13426],{"class":209},"]\n",[195,13428,13429,13432],{"class":197,"line":968},[195,13430,13431],{"class":205},"  push",[195,13433,13405],{"class":209},[195,13435,13436,13439,13441,13444],{"class":197,"line":1274},[195,13437,13438],{"class":205},"    tags",[195,13440,13420],{"class":209},[195,13442,13443],{"class":459},"'v*'",[195,13445,13426],{"class":209},[195,13447,13448],{"class":197,"line":1282},[195,13449,1241],{"emptyLinePlaceholder":757},[195,13451,13452,13455],{"class":197,"line":1295},[195,13453,13454],{"class":205},"jobs",[195,13456,13405],{"class":209},[195,13458,13459,13462],{"class":197,"line":1309},[195,13460,13461],{"class":205},"  test",[195,13463,13405],{"class":209},[195,13465,13466,13469,13471],{"class":197,"line":2246},[195,13467,13468],{"class":205},"    runs-on",[195,13470,217],{"class":209},[195,13472,13473],{"class":459},"macos-latest\n",[195,13475,13476,13479],{"class":197,"line":1996},[195,13477,13478],{"class":205},"    steps",[195,13480,13405],{"class":209},[195,13482,13483,13486,13489,13491],{"class":197,"line":2257},[195,13484,13485],{"class":209},"      - ",[195,13487,13488],{"class":205},"uses",[195,13490,217],{"class":209},[195,13492,13493],{"class":459},"actions\u002Fcheckout@v4\n",[195,13495,13496],{"class":197,"line":2262},[195,13497,1241],{"emptyLinePlaceholder":757},[195,13499,13500],{"class":197,"line":2267},[195,13501,13502],{"class":415},"      # Flutter\n",[195,13504,13505,13507,13509,13511],{"class":197,"line":2273},[195,13506,13485],{"class":209},[195,13508,13488],{"class":205},[195,13510,217],{"class":209},[195,13512,13513],{"class":459},"subosito\u002Fflutter-action@v2\n",[195,13515,13516,13519],{"class":197,"line":2033},[195,13517,13518],{"class":205},"        with",[195,13520,13405],{"class":209},[195,13522,13523,13526,13528],{"class":197,"line":2284},[195,13524,13525],{"class":205},"          flutter-version",[195,13527,217],{"class":209},[195,13529,13530],{"class":459},"'3.24.0'\n",[195,13532,13533,13535,13537,13539],{"class":197,"line":2460},[195,13534,13485],{"class":209},[195,13536,7156],{"class":205},[195,13538,217],{"class":209},[195,13540,13541],{"class":459},"flutter pub get\n",[195,13543,13544,13546,13548,13550],{"class":197,"line":2466},[195,13545,13485],{"class":209},[195,13547,7156],{"class":205},[195,13549,217],{"class":209},[195,13551,13552],{"class":459},"flutter analyze\n",[195,13554,13555,13557,13559,13561],{"class":197,"line":2472},[195,13556,13485],{"class":209},[195,13558,7156],{"class":205},[195,13560,217],{"class":209},[195,13562,13563],{"class":459},"flutter test --coverage\n",[195,13565,13566],{"class":197,"line":2780},[195,13567,1241],{"emptyLinePlaceholder":757},[195,13569,13570],{"class":197,"line":2786},[195,13571,13572],{"class":415},"      # iOS\n",[195,13574,13575,13577,13579,13581],{"class":197,"line":2792},[195,13576,13485],{"class":209},[195,13578,7156],{"class":205},[195,13580,217],{"class":209},[195,13582,13583],{"class":459},"xcodebuild test -workspace App.xcworkspace -scheme App -destination 'platform=iOS Simulator,name=iPhone 15'\n",[195,13585,13586],{"class":197,"line":2798},[195,13587,1241],{"emptyLinePlaceholder":757},[195,13589,13590],{"class":197,"line":2804},[195,13591,13592],{"class":415},"      # Android\n",[195,13594,13595,13597,13599,13601],{"class":197,"line":2810},[195,13596,13485],{"class":209},[195,13598,7156],{"class":205},[195,13600,217],{"class":209},[195,13602,13603],{"class":459},".\u002Fgradlew test\n",[195,13605,13606,13608,13610,13612],{"class":197,"line":2815},[195,13607,13485],{"class":209},[195,13609,7156],{"class":205},[195,13611,217],{"class":209},[195,13613,13614],{"class":459},".\u002Fgradlew lint\n",[195,13616,13617],{"class":197,"line":2820},[195,13618,1241],{"emptyLinePlaceholder":757},[195,13620,13621,13624],{"class":197,"line":2825},[195,13622,13623],{"class":205},"  build-ios",[195,13625,13405],{"class":209},[195,13627,13628,13631,13633],{"class":197,"line":2831},[195,13629,13630],{"class":205},"    needs",[195,13632,217],{"class":209},[195,13634,13635],{"class":459},"test\n",[195,13637,13638,13640,13642],{"class":197,"line":2837},[195,13639,13468],{"class":205},[195,13641,217],{"class":209},[195,13643,13473],{"class":459},[195,13645,13646,13649,13651],{"class":197,"line":2843},[195,13647,13648],{"class":205},"    if",[195,13650,217],{"class":209},[195,13652,13653],{"class":459},"startsWith(github.ref, 'refs\u002Ftags\u002F')\n",[195,13655,13656,13658],{"class":197,"line":2848},[195,13657,13478],{"class":205},[195,13659,13405],{"class":209},[195,13661,13662,13664,13666,13668],{"class":197,"line":2854},[195,13663,13485],{"class":209},[195,13665,7156],{"class":205},[195,13667,217],{"class":209},[195,13669,13670],{"class":459},"flutter build ipa --release --export-options-plist=ExportOptions.plist\n",[195,13672,13673,13675,13677,13679],{"class":197,"line":2860},[195,13674,13485],{"class":209},[195,13676,13488],{"class":205},[195,13678,217],{"class":209},[195,13680,13681],{"class":459},"apple-actions\u002Fupload-testflight-build@v1\n",[195,13683,13684],{"class":197,"line":2866},[195,13685,1241],{"emptyLinePlaceholder":757},[195,13687,13688,13691],{"class":197,"line":2872},[195,13689,13690],{"class":205},"  build-android",[195,13692,13405],{"class":209},[195,13694,13695,13697,13699],{"class":197,"line":2878},[195,13696,13630],{"class":205},[195,13698,217],{"class":209},[195,13700,13635],{"class":459},[195,13702,13703,13705,13707],{"class":197,"line":2884},[195,13704,13468],{"class":205},[195,13706,217],{"class":209},[195,13708,13709],{"class":459},"ubuntu-latest\n",[195,13711,13712,13714,13716],{"class":197,"line":2890},[195,13713,13648],{"class":205},[195,13715,217],{"class":209},[195,13717,13653],{"class":459},[195,13719,13720,13722],{"class":197,"line":2895},[195,13721,13478],{"class":205},[195,13723,13405],{"class":209},[195,13725,13726,13728,13730,13732],{"class":197,"line":2900},[195,13727,13485],{"class":209},[195,13729,7156],{"class":205},[195,13731,217],{"class":209},[195,13733,13734],{"class":459},"flutter build appbundle --release\n",[195,13736,13737,13739,13741,13743],{"class":197,"line":2905},[195,13738,13485],{"class":209},[195,13740,13488],{"class":205},[195,13742,217],{"class":209},[195,13744,13745],{"class":459},"r0adkll\u002Fupload-google-play@v1\n",[195,13747,13748,13750],{"class":197,"line":2911},[195,13749,13518],{"class":205},[195,13751,13405],{"class":209},[195,13753,13754,13757,13759],{"class":197,"line":2917},[195,13755,13756],{"class":205},"          track",[195,13758,217],{"class":209},[195,13760,13761],{"class":459},"internal\n",[14,13763,13764],{},[125,13765,13766],{},"代码签名管理：",[36,13768,13769,13781],{},[39,13770,13771],{},[42,13772,13773,13775,13778],{},[45,13774,13101],{},[45,13776,13777],{},"方案",[45,13779,13780],{},"说明",[52,13782,13783,13793,13803],{},[42,13784,13785,13787,13790],{},[57,13786,13113],{},[57,13788,13789],{},"fastlane match",[57,13791,13792],{},"集中管理证书和 Provisioning Profile",[42,13794,13795,13797,13800],{},[57,13796,13124],{},[57,13798,13799],{},"Play App Signing",[57,13801,13802],{},"Google 管理签名密钥",[42,13804,13805,13808,13811],{},[57,13806,13807],{},"通用",[57,13809,13810],{},"CI 环境变量 + Secrets",[57,13812,13813],{},"不在代码中存储密钥",[1890,13815],{},[18,13817,13819],{"id":13818},"_22-架构设计与系统设计","22. 架构设计与系统设计",[32,13821,13823],{"id":13822},"q221-如何设计一个离线优先offline-first的移动应用","Q22.1: 如何设计一个离线优先（Offline-First）的移动应用？",[14,13825,13826],{},[125,13827,2077],{},[186,13829,13832],{"className":13830,"code":13831,"language":1074},[1072],"┌────────────────────────────────────────────────┐\n│                   UI Layer                      │\n│    展示数据（始终从本地数据库读取）                │\n├────────────────────────────────────────────────┤\n│                Repository Layer                 │\n│  ┌──────────┐    ┌─────────────┐               │\n│  │ Local DB  │←──│ Sync Engine │               │\n│  │ (Room\u002F    │   │             │               │\n│  │  SQLite\u002F  │──→│ Conflict    │               │\n│  │  Hive)    │   │ Resolution  │               │\n│  └──────────┘    └──────┬──────┘               │\n│                         │                       │\n│                  ┌──────▼──────┐               │\n│                  │  Remote API  │               │\n│                  └─────────────┘               │\n└────────────────────────────────────────────────┘\n",[136,13833,13831],{"__ignoreMap":191},[186,13835,13837],{"className":6838,"code":13836,"language":6840,"meta":191,"style":191},"\u002F\u002F Repository：先写本地，后台同步\nclass TodoRepository(\n    private val dao: TodoDao,\n    private val api: TodoApi,\n    private val syncManager: SyncManager\n) {\n    \u002F\u002F 读取：始终从本地\n    fun observeTodos(): Flow\u003CList\u003CTodo>> = dao.observeAll()\n\n    \u002F\u002F 写入：先本地，然后排队同步\n    suspend fun addTodo(todo: Todo) {\n        val localTodo = todo.copy(syncStatus = SyncStatus.PENDING)\n        dao.insert(localTodo)\n        syncManager.enqueueSync(localTodo.id)\n    }\n\n    \u002F\u002F 同步引擎\n    suspend fun sync() {\n        \u002F\u002F 1. 上传本地未同步的变更\n        val pending = dao.getPendingSyncItems()\n        for (item in pending) {\n            try {\n                api.upsert(item)\n                dao.updateSyncStatus(item.id, SyncStatus.SYNCED)\n            } catch (e: ConflictException) {\n                resolveConflict(item, e.serverVersion)\n            }\n        }\n\n        \u002F\u002F 2. 拉取远程变更\n        val lastSync = prefs.lastSyncTimestamp\n        val remoteChanges = api.getChangesSince(lastSync)\n        dao.upsertAll(remoteChanges)\n        prefs.lastSyncTimestamp = System.currentTimeMillis()\n    }\n}\n",[136,13838,13839,13844,13849,13854,13859,13864,13868,13873,13878,13882,13887,13892,13897,13902,13907,13911,13915,13920,13925,13930,13935,13940,13944,13949,13954,13959,13964,13968,13972,13976,13981,13986,13991,13996,14001,14005],{"__ignoreMap":191},[195,13840,13841],{"class":197,"line":198},[195,13842,13843],{},"\u002F\u002F Repository：先写本地，后台同步\n",[195,13845,13846],{"class":197,"line":230},[195,13847,13848],{},"class TodoRepository(\n",[195,13850,13851],{"class":197,"line":251},[195,13852,13853],{},"    private val dao: TodoDao,\n",[195,13855,13856],{"class":197,"line":272},[195,13857,13858],{},"    private val api: TodoApi,\n",[195,13860,13861],{"class":197,"line":293},[195,13862,13863],{},"    private val syncManager: SyncManager\n",[195,13865,13866],{"class":197,"line":562},[195,13867,644],{},[195,13869,13870],{"class":197,"line":583},[195,13871,13872],{},"    \u002F\u002F 读取：始终从本地\n",[195,13874,13875],{"class":197,"line":962},[195,13876,13877],{},"    fun observeTodos(): Flow\u003CList\u003CTodo>> = dao.observeAll()\n",[195,13879,13880],{"class":197,"line":968},[195,13881,1241],{"emptyLinePlaceholder":757},[195,13883,13884],{"class":197,"line":1274},[195,13885,13886],{},"    \u002F\u002F 写入：先本地，然后排队同步\n",[195,13888,13889],{"class":197,"line":1282},[195,13890,13891],{},"    suspend fun addTodo(todo: Todo) {\n",[195,13893,13894],{"class":197,"line":1295},[195,13895,13896],{},"        val localTodo = todo.copy(syncStatus = SyncStatus.PENDING)\n",[195,13898,13899],{"class":197,"line":1309},[195,13900,13901],{},"        dao.insert(localTodo)\n",[195,13903,13904],{"class":197,"line":2246},[195,13905,13906],{},"        syncManager.enqueueSync(localTodo.id)\n",[195,13908,13909],{"class":197,"line":1996},[195,13910,2403],{},[195,13912,13913],{"class":197,"line":2257},[195,13914,1241],{"emptyLinePlaceholder":757},[195,13916,13917],{"class":197,"line":2262},[195,13918,13919],{},"    \u002F\u002F 同步引擎\n",[195,13921,13922],{"class":197,"line":2267},[195,13923,13924],{},"    suspend fun sync() {\n",[195,13926,13927],{"class":197,"line":2273},[195,13928,13929],{},"        \u002F\u002F 1. 上传本地未同步的变更\n",[195,13931,13932],{"class":197,"line":2033},[195,13933,13934],{},"        val pending = dao.getPendingSyncItems()\n",[195,13936,13937],{"class":197,"line":2284},[195,13938,13939],{},"        for (item in pending) {\n",[195,13941,13942],{"class":197,"line":2460},[195,13943,8918],{},[195,13945,13946],{"class":197,"line":2466},[195,13947,13948],{},"                api.upsert(item)\n",[195,13950,13951],{"class":197,"line":2472},[195,13952,13953],{},"                dao.updateSyncStatus(item.id, SyncStatus.SYNCED)\n",[195,13955,13956],{"class":197,"line":2780},[195,13957,13958],{},"            } catch (e: ConflictException) {\n",[195,13960,13961],{"class":197,"line":2786},[195,13962,13963],{},"                resolveConflict(item, e.serverVersion)\n",[195,13965,13966],{"class":197,"line":2792},[195,13967,5781],{},[195,13969,13970],{"class":197,"line":2798},[195,13971,2887],{},[195,13973,13974],{"class":197,"line":2804},[195,13975,1241],{"emptyLinePlaceholder":757},[195,13977,13978],{"class":197,"line":2810},[195,13979,13980],{},"        \u002F\u002F 2. 拉取远程变更\n",[195,13982,13983],{"class":197,"line":2815},[195,13984,13985],{},"        val lastSync = prefs.lastSyncTimestamp\n",[195,13987,13988],{"class":197,"line":2820},[195,13989,13990],{},"        val remoteChanges = api.getChangesSince(lastSync)\n",[195,13992,13993],{"class":197,"line":2825},[195,13994,13995],{},"        dao.upsertAll(remoteChanges)\n",[195,13997,13998],{"class":197,"line":2831},[195,13999,14000],{},"        prefs.lastSyncTimestamp = System.currentTimeMillis()\n",[195,14002,14003],{"class":197,"line":2837},[195,14004,2403],{},[195,14006,14007],{"class":197,"line":2843},[195,14008,552],{},[14,14010,14011],{},[125,14012,14013],{},"冲突解决策略：",[129,14015,14016,14022,14028,14034,14040],{},[132,14017,14018,14021],{},[125,14019,14020],{},"Last Write Wins","：最后修改时间戳大的胜出（简单但可能丢数据）",[132,14023,14024,14027],{},[125,14025,14026],{},"Server Wins","：服务器版本始终优先",[132,14029,14030,14033],{},[125,14031,14032],{},"Client Wins","：客户端版本始终优先",[132,14035,14036,14039],{},[125,14037,14038],{},"Manual Merge","：提示用户手动解决冲突",[132,14041,14042,14045],{},[125,14043,14044],{},"CRDT","：无冲突复制数据类型（适合协作编辑）",[1890,14047],{},[32,14049,14051],{"id":14050},"q222-移动端如何实现推送通知系统","Q22.2: 移动端如何实现推送通知系统？",[14,14053,14054],{},[125,14055,2077],{},[186,14057,14060],{"className":14058,"code":14059,"language":1074},[1072],"┌─────────┐    ┌──────────┐    ┌──────────────┐    ┌────────────┐\n│ Backend  │───→│ Push     │───→│ APNs \u002F FCM   │───→│ Mobile App │\n│ Server   │    │ Service  │    │              │    │            │\n└─────────┘    └──────────┘    └──────────────┘    └────────────┘\n",[136,14061,14059],{"__ignoreMap":191},[186,14063,14065],{"className":2177,"code":14064,"language":2179,"meta":191,"style":191},"\u002F\u002F iOS：APNs 注册\nfunc application(_ application: UIApplication,\n                 didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {\n    UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, _ in\n        guard granted else { return }\n        DispatchQueue.main.async { application.registerForRemoteNotifications() }\n    }\n    return true\n}\n\nfunc application(_ application: UIApplication,\n                 didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {\n    let token = deviceToken.map { String(format: \"%02x\", $0) }.joined()\n    api.registerPushToken(token)\n}\n\n\u002F\u002F 处理推送\nextension AppDelegate: UNUserNotificationCenterDelegate {\n    \u002F\u002F 前台收到通知\n    func userNotificationCenter(_ center: UNUserNotificationCenter,\n                                willPresent notification: UNNotification,\n                                withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {\n        completionHandler([.banner, .sound])\n    }\n\n    \u002F\u002F 用户点击通知\n    func userNotificationCenter(_ center: UNUserNotificationCenter,\n                                didReceive response: UNNotificationResponse,\n                                withCompletionHandler completionHandler: @escaping () -> Void) {\n        let userInfo = response.notification.request.content.userInfo\n        handleDeepLink(from: userInfo)\n        completionHandler()\n    }\n}\n",[136,14066,14067,14072,14076,14080,14085,14090,14095,14099,14103,14107,14111,14115,14120,14125,14130,14134,14138,14143,14148,14153,14158,14163,14168,14173,14177,14181,14186,14190,14195,14200,14205,14210,14215,14219],{"__ignoreMap":191},[195,14068,14069],{"class":197,"line":198},[195,14070,14071],{},"\u002F\u002F iOS：APNs 注册\n",[195,14073,14074],{"class":197,"line":230},[195,14075,6334],{},[195,14077,14078],{"class":197,"line":251},[195,14079,6339],{},[195,14081,14082],{"class":197,"line":272},[195,14083,14084],{},"    UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, _ in\n",[195,14086,14087],{"class":197,"line":293},[195,14088,14089],{},"        guard granted else { return }\n",[195,14091,14092],{"class":197,"line":562},[195,14093,14094],{},"        DispatchQueue.main.async { application.registerForRemoteNotifications() }\n",[195,14096,14097],{"class":197,"line":583},[195,14098,2403],{},[195,14100,14101],{"class":197,"line":962},[195,14102,6391],{},[195,14104,14105],{"class":197,"line":968},[195,14106,552],{},[195,14108,14109],{"class":197,"line":1274},[195,14110,1241],{"emptyLinePlaceholder":757},[195,14112,14113],{"class":197,"line":1282},[195,14114,6334],{},[195,14116,14117],{"class":197,"line":1295},[195,14118,14119],{},"                 didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {\n",[195,14121,14122],{"class":197,"line":1309},[195,14123,14124],{},"    let token = deviceToken.map { String(format: \"%02x\", $0) }.joined()\n",[195,14126,14127],{"class":197,"line":2246},[195,14128,14129],{},"    api.registerPushToken(token)\n",[195,14131,14132],{"class":197,"line":1996},[195,14133,552],{},[195,14135,14136],{"class":197,"line":2257},[195,14137,1241],{"emptyLinePlaceholder":757},[195,14139,14140],{"class":197,"line":2262},[195,14141,14142],{},"\u002F\u002F 处理推送\n",[195,14144,14145],{"class":197,"line":2267},[195,14146,14147],{},"extension AppDelegate: UNUserNotificationCenterDelegate {\n",[195,14149,14150],{"class":197,"line":2273},[195,14151,14152],{},"    \u002F\u002F 前台收到通知\n",[195,14154,14155],{"class":197,"line":2033},[195,14156,14157],{},"    func userNotificationCenter(_ center: UNUserNotificationCenter,\n",[195,14159,14160],{"class":197,"line":2284},[195,14161,14162],{},"                                willPresent notification: UNNotification,\n",[195,14164,14165],{"class":197,"line":2460},[195,14166,14167],{},"                                withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {\n",[195,14169,14170],{"class":197,"line":2466},[195,14171,14172],{},"        completionHandler([.banner, .sound])\n",[195,14174,14175],{"class":197,"line":2472},[195,14176,2403],{},[195,14178,14179],{"class":197,"line":2780},[195,14180,1241],{"emptyLinePlaceholder":757},[195,14182,14183],{"class":197,"line":2786},[195,14184,14185],{},"    \u002F\u002F 用户点击通知\n",[195,14187,14188],{"class":197,"line":2792},[195,14189,14157],{},[195,14191,14192],{"class":197,"line":2798},[195,14193,14194],{},"                                didReceive response: UNNotificationResponse,\n",[195,14196,14197],{"class":197,"line":2804},[195,14198,14199],{},"                                withCompletionHandler completionHandler: @escaping () -> Void) {\n",[195,14201,14202],{"class":197,"line":2810},[195,14203,14204],{},"        let userInfo = response.notification.request.content.userInfo\n",[195,14206,14207],{"class":197,"line":2815},[195,14208,14209],{},"        handleDeepLink(from: userInfo)\n",[195,14211,14212],{"class":197,"line":2820},[195,14213,14214],{},"        completionHandler()\n",[195,14216,14217],{"class":197,"line":2825},[195,14218,2403],{},[195,14220,14221],{"class":197,"line":2831},[195,14222,552],{},[186,14224,14226],{"className":6838,"code":14225,"language":6840,"meta":191,"style":191},"\u002F\u002F Android：FCM\nclass MyFirebaseService : FirebaseMessagingService() {\n    override fun onNewToken(token: String) {\n        api.registerPushToken(token)\n    }\n\n    override fun onMessageReceived(remoteMessage: RemoteMessage) {\n        \u002F\u002F 数据消息（自定义处理）\n        remoteMessage.data[\"type\"]?.let { type ->\n            when (type) {\n                \"chat\" -> showChatNotification(remoteMessage.data)\n                \"order\" -> showOrderNotification(remoteMessage.data)\n            }\n        }\n\n        \u002F\u002F 通知消息（系统自动显示）\n        remoteMessage.notification?.let { notification ->\n            showNotification(notification.title, notification.body)\n        }\n    }\n\n    private fun showNotification(title: String?, body: String?) {\n        val intent = Intent(this, MainActivity::class.java).apply {\n            flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK\n        }\n        val pendingIntent = PendingIntent.getActivity(this, 0, intent,\n            PendingIntent.FLAG_IMMUTABLE)\n\n        val notification = NotificationCompat.Builder(this, CHANNEL_ID)\n            .setSmallIcon(R.drawable.ic_notification)\n            .setContentTitle(title)\n            .setContentText(body)\n            .setContentIntent(pendingIntent)\n            .setAutoCancel(true)\n            .build()\n\n        NotificationManagerCompat.from(this).notify(NOTIFICATION_ID, notification)\n    }\n}\n",[136,14227,14228,14233,14238,14243,14248,14252,14256,14261,14266,14271,14276,14281,14286,14290,14294,14298,14303,14308,14313,14317,14321,14325,14330,14335,14340,14344,14349,14354,14358,14363,14368,14373,14378,14383,14388,14393,14397,14402,14406],{"__ignoreMap":191},[195,14229,14230],{"class":197,"line":198},[195,14231,14232],{},"\u002F\u002F Android：FCM\n",[195,14234,14235],{"class":197,"line":230},[195,14236,14237],{},"class MyFirebaseService : FirebaseMessagingService() {\n",[195,14239,14240],{"class":197,"line":251},[195,14241,14242],{},"    override fun onNewToken(token: String) {\n",[195,14244,14245],{"class":197,"line":272},[195,14246,14247],{},"        api.registerPushToken(token)\n",[195,14249,14250],{"class":197,"line":293},[195,14251,2403],{},[195,14253,14254],{"class":197,"line":562},[195,14255,1241],{"emptyLinePlaceholder":757},[195,14257,14258],{"class":197,"line":583},[195,14259,14260],{},"    override fun onMessageReceived(remoteMessage: RemoteMessage) {\n",[195,14262,14263],{"class":197,"line":962},[195,14264,14265],{},"        \u002F\u002F 数据消息（自定义处理）\n",[195,14267,14268],{"class":197,"line":968},[195,14269,14270],{},"        remoteMessage.data[\"type\"]?.let { type ->\n",[195,14272,14273],{"class":197,"line":1274},[195,14274,14275],{},"            when (type) {\n",[195,14277,14278],{"class":197,"line":1282},[195,14279,14280],{},"                \"chat\" -> showChatNotification(remoteMessage.data)\n",[195,14282,14283],{"class":197,"line":1295},[195,14284,14285],{},"                \"order\" -> showOrderNotification(remoteMessage.data)\n",[195,14287,14288],{"class":197,"line":1309},[195,14289,5781],{},[195,14291,14292],{"class":197,"line":2246},[195,14293,2887],{},[195,14295,14296],{"class":197,"line":1996},[195,14297,1241],{"emptyLinePlaceholder":757},[195,14299,14300],{"class":197,"line":2257},[195,14301,14302],{},"        \u002F\u002F 通知消息（系统自动显示）\n",[195,14304,14305],{"class":197,"line":2262},[195,14306,14307],{},"        remoteMessage.notification?.let { notification ->\n",[195,14309,14310],{"class":197,"line":2267},[195,14311,14312],{},"            showNotification(notification.title, notification.body)\n",[195,14314,14315],{"class":197,"line":2273},[195,14316,2887],{},[195,14318,14319],{"class":197,"line":2033},[195,14320,2403],{},[195,14322,14323],{"class":197,"line":2284},[195,14324,1241],{"emptyLinePlaceholder":757},[195,14326,14327],{"class":197,"line":2460},[195,14328,14329],{},"    private fun showNotification(title: String?, body: String?) {\n",[195,14331,14332],{"class":197,"line":2466},[195,14333,14334],{},"        val intent = Intent(this, MainActivity::class.java).apply {\n",[195,14336,14337],{"class":197,"line":2472},[195,14338,14339],{},"            flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK\n",[195,14341,14342],{"class":197,"line":2780},[195,14343,2887],{},[195,14345,14346],{"class":197,"line":2786},[195,14347,14348],{},"        val pendingIntent = PendingIntent.getActivity(this, 0, intent,\n",[195,14350,14351],{"class":197,"line":2792},[195,14352,14353],{},"            PendingIntent.FLAG_IMMUTABLE)\n",[195,14355,14356],{"class":197,"line":2798},[195,14357,1241],{"emptyLinePlaceholder":757},[195,14359,14360],{"class":197,"line":2804},[195,14361,14362],{},"        val notification = NotificationCompat.Builder(this, CHANNEL_ID)\n",[195,14364,14365],{"class":197,"line":2810},[195,14366,14367],{},"            .setSmallIcon(R.drawable.ic_notification)\n",[195,14369,14370],{"class":197,"line":2815},[195,14371,14372],{},"            .setContentTitle(title)\n",[195,14374,14375],{"class":197,"line":2820},[195,14376,14377],{},"            .setContentText(body)\n",[195,14379,14380],{"class":197,"line":2825},[195,14381,14382],{},"            .setContentIntent(pendingIntent)\n",[195,14384,14385],{"class":197,"line":2831},[195,14386,14387],{},"            .setAutoCancel(true)\n",[195,14389,14390],{"class":197,"line":2837},[195,14391,14392],{},"            .build()\n",[195,14394,14395],{"class":197,"line":2843},[195,14396,1241],{"emptyLinePlaceholder":757},[195,14398,14399],{"class":197,"line":2848},[195,14400,14401],{},"        NotificationManagerCompat.from(this).notify(NOTIFICATION_ID, notification)\n",[195,14403,14404],{"class":197,"line":2854},[195,14405,2403],{},[195,14407,14408],{"class":197,"line":2860},[195,14409,552],{},[186,14411,14413],{"className":11162,"code":14412,"language":11164,"meta":191,"style":191},"\u002F\u002F Flutter：使用 firebase_messaging\nFirebaseMessaging.instance.requestPermission();\n\nFirebaseMessaging.instance.getToken().then((token) {\n  api.registerPushToken(token!);\n});\n\n\u002F\u002F 前台消息\nFirebaseMessaging.onMessage.listen((RemoteMessage message) {\n  showLocalNotification(message);\n});\n\n\u002F\u002F 后台\u002F终止态 点击通知\nFirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {\n  navigateToScreen(message.data);\n});\n",[136,14414,14415,14420,14425,14429,14434,14439,14443,14447,14452,14457,14462,14466,14470,14475,14480,14485],{"__ignoreMap":191},[195,14416,14417],{"class":197,"line":198},[195,14418,14419],{},"\u002F\u002F Flutter：使用 firebase_messaging\n",[195,14421,14422],{"class":197,"line":230},[195,14423,14424],{},"FirebaseMessaging.instance.requestPermission();\n",[195,14426,14427],{"class":197,"line":251},[195,14428,1241],{"emptyLinePlaceholder":757},[195,14430,14431],{"class":197,"line":272},[195,14432,14433],{},"FirebaseMessaging.instance.getToken().then((token) {\n",[195,14435,14436],{"class":197,"line":293},[195,14437,14438],{},"  api.registerPushToken(token!);\n",[195,14440,14441],{"class":197,"line":562},[195,14442,11253],{},[195,14444,14445],{"class":197,"line":583},[195,14446,1241],{"emptyLinePlaceholder":757},[195,14448,14449],{"class":197,"line":962},[195,14450,14451],{},"\u002F\u002F 前台消息\n",[195,14453,14454],{"class":197,"line":968},[195,14455,14456],{},"FirebaseMessaging.onMessage.listen((RemoteMessage message) {\n",[195,14458,14459],{"class":197,"line":1274},[195,14460,14461],{},"  showLocalNotification(message);\n",[195,14463,14464],{"class":197,"line":1282},[195,14465,11253],{},[195,14467,14468],{"class":197,"line":1295},[195,14469,1241],{"emptyLinePlaceholder":757},[195,14471,14472],{"class":197,"line":1309},[195,14473,14474],{},"\u002F\u002F 后台\u002F终止态 点击通知\n",[195,14476,14477],{"class":197,"line":2246},[195,14478,14479],{},"FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {\n",[195,14481,14482],{"class":197,"line":1996},[195,14483,14484],{},"  navigateToScreen(message.data);\n",[195,14486,14487],{"class":197,"line":2257},[195,14488,11253],{},[1890,14490],{},[32,14492,14494],{"id":14493},"q223-如何设计一个高效的图片加载框架","Q22.3: 如何设计一个高效的图片加载框架？",[14,14496,14497],{},[125,14498,2077],{},[186,14500,14503],{"className":14501,"code":14502,"language":1074},[1072],"请求 URL\n   ↓\n┌──────────────┐\n│ 内存缓存      │ → 命中 → 返回\n│ (LRU Cache)  │\n└──────┬───────┘\n       ↓ 未命中\n┌──────────────┐\n│ 磁盘缓存      │ → 命中 → 解码 → 存入内存缓存 → 返回\n│ (文件系统)    │\n└──────┬───────┘\n       ↓ 未命中\n┌──────────────┐\n│ 网络请求      │ → 下载 → 存入磁盘缓存 → 解码 → 存入内存缓存 → 返回\n│ (HTTP)       │\n└──────────────┘\n",[136,14504,14502],{"__ignoreMap":191},[14,14506,14507],{},[125,14508,14509],{},"各平台常用方案：",[36,14511,14512,14524],{},[39,14513,14514],{},[42,14515,14516,14518,14521],{},[45,14517,13101],{},[45,14519,14520],{},"框架",[45,14522,14523],{},"特点",[52,14525,14526,14536,14546],{},[42,14527,14528,14530,14533],{},[57,14529,13113],{},[57,14531,14532],{},"Kingfisher \u002F SDWebImage \u002F Nuke",[57,14534,14535],{},"自动缓存、降采样、动画",[42,14537,14538,14540,14543],{},[57,14539,13124],{},[57,14541,14542],{},"Coil \u002F Glide",[57,14544,14545],{},"生命周期感知、协程支持",[42,14547,14548,14550,14553],{},[57,14549,13135],{},[57,14551,14552],{},"cached_network_image",[57,14554,14555],{},"内存+磁盘缓存",[186,14557,14559],{"className":6838,"code":14558,"language":6840,"meta":191,"style":191},"\u002F\u002F Android Coil 示例\nAsyncImage(\n    model = ImageRequest.Builder(LocalContext.current)\n        .data(\"https:\u002F\u002Fexample.com\u002Fphoto.jpg\")\n        .crossfade(true)\n        .size(200, 200)           \u002F\u002F 降采样到指定尺寸\n        .memoryCachePolicy(CachePolicy.ENABLED)\n        .diskCachePolicy(CachePolicy.ENABLED)\n        .build(),\n    contentDescription = null,\n    modifier = Modifier.size(200.dp),\n    placeholder = painterResource(R.drawable.placeholder),\n    error = painterResource(R.drawable.error),\n)\n",[136,14560,14561,14566,14571,14576,14581,14586,14591,14596,14601,14606,14611,14616,14621,14626],{"__ignoreMap":191},[195,14562,14563],{"class":197,"line":198},[195,14564,14565],{},"\u002F\u002F Android Coil 示例\n",[195,14567,14568],{"class":197,"line":230},[195,14569,14570],{},"AsyncImage(\n",[195,14572,14573],{"class":197,"line":251},[195,14574,14575],{},"    model = ImageRequest.Builder(LocalContext.current)\n",[195,14577,14578],{"class":197,"line":272},[195,14579,14580],{},"        .data(\"https:\u002F\u002Fexample.com\u002Fphoto.jpg\")\n",[195,14582,14583],{"class":197,"line":293},[195,14584,14585],{},"        .crossfade(true)\n",[195,14587,14588],{"class":197,"line":562},[195,14589,14590],{},"        .size(200, 200)           \u002F\u002F 降采样到指定尺寸\n",[195,14592,14593],{"class":197,"line":583},[195,14594,14595],{},"        .memoryCachePolicy(CachePolicy.ENABLED)\n",[195,14597,14598],{"class":197,"line":962},[195,14599,14600],{},"        .diskCachePolicy(CachePolicy.ENABLED)\n",[195,14602,14603],{"class":197,"line":968},[195,14604,14605],{},"        .build(),\n",[195,14607,14608],{"class":197,"line":1274},[195,14609,14610],{},"    contentDescription = null,\n",[195,14612,14613],{"class":197,"line":1282},[195,14614,14615],{},"    modifier = Modifier.size(200.dp),\n",[195,14617,14618],{"class":197,"line":1295},[195,14619,14620],{},"    placeholder = painterResource(R.drawable.placeholder),\n",[195,14622,14623],{"class":197,"line":1309},[195,14624,14625],{},"    error = painterResource(R.drawable.error),\n",[195,14627,14628],{"class":197,"line":2246},[195,14629,410],{},[14,14631,14632],{},[125,14633,14634],{},"关键设计考量：",[129,14636,14637,14643,14649,14655,14661,14667],{},[132,14638,14639,14642],{},[125,14640,14641],{},"内存缓存大小","：通常为可用内存的 1\u002F8",[132,14644,14645,14648],{},[125,14646,14647],{},"磁盘缓存大小","：通常 50-250MB",[132,14650,14651,14654],{},[125,14652,14653],{},"图片降采样","：根据 ImageView 实际尺寸解码，避免全尺寸解码浪费内存",[132,14656,14657,14660],{},[125,14658,14659],{},"请求合并","：相同 URL 的并发请求合并为一个",[132,14662,14663,14666],{},[125,14664,14665],{},"优先级管理","：可见区域的图片优先加载",[132,14668,14669,14672],{},[125,14670,14671],{},"生命周期绑定","：页面销毁时取消请求",[1890,14674],{},[32,14676,14678],{"id":14677},"q224-设计一个聊天应用的消息系统websocket-本地存储","Q22.4: 设计一个聊天应用的消息系统（WebSocket + 本地存储）",[14,14680,14681],{},[125,14682,2077],{},[186,14684,14687],{"className":14685,"code":14686,"language":1074},[1072],"┌────────────────────────────────────────────────────────────┐\n│                     Chat Architecture                       │\n│                                                            │\n│   UI Layer                                                 │\n│   ├── MessageList (observe local DB)                       │\n│   └── MessageInput → send()                                │\n│                                                            │\n│   Domain Layer                                             │\n│   └── ChatRepository                                       │\n│       ├── observeMessages() → Flow\u003CList\u003CMessage>>          │\n│       ├── sendMessage(text) → optimistic insert            │\n│       └── syncMessages() → pull missing                    │\n│                                                            │\n│   Data Layer                                               │\n│   ├── WebSocketManager (实时消息收发)                       │\n│   ├── MessageDao (本地持久化)                               │\n│   └── ChatApi (HTTP 补充：历史消息、文件上传)               │\n└────────────────────────────────────────────────────────────┘\n",[136,14688,14686],{"__ignoreMap":191},[186,14690,14692],{"className":6838,"code":14691,"language":6840,"meta":191,"style":191},"\u002F\u002F 消息模型\n@Entity\ndata class Message(\n    @PrimaryKey val id: String = UUID.randomUUID().toString(),\n    val chatId: String,\n    val senderId: String,\n    val text: String,\n    val timestamp: Long,\n    val status: MessageStatus = MessageStatus.SENDING\n)\n\nenum class MessageStatus { SENDING, SENT, DELIVERED, READ, FAILED }\n\n\u002F\u002F WebSocket 管理\nclass WebSocketManager(private val dao: MessageDao) {\n    private var socket: WebSocket? = null\n    private val _connectionState = MutableStateFlow(ConnectionState.DISCONNECTED)\n\n    fun connect() {\n        val request = Request.Builder().url(\"wss:\u002F\u002Fchat.example.com\u002Fws\").build()\n        socket = okHttpClient.newWebSocket(request, object : WebSocketListener() {\n            override fun onMessage(webSocket: WebSocket, text: String) {\n                val message = json.decodeFromString\u003CMessage>(text)\n                scope.launch { dao.insert(message) }  \u002F\u002F 收到消息存入本地\n            }\n\n            override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {\n                _connectionState.value = ConnectionState.DISCONNECTED\n                reconnectWithBackoff()  \u002F\u002F 指数退避重连\n            }\n        })\n    }\n\n    \u002F\u002F 指数退避重连\n    private fun reconnectWithBackoff() {\n        scope.launch {\n            var delay = 1000L\n            repeat(10) {\n                delay(delay)\n                try { connect(); return@launch }\n                catch (e: Exception) { delay = minOf(delay * 2, 30_000L) }\n            }\n        }\n    }\n\n    fun send(message: Message) {\n        socket?.send(json.encodeToString(message))\n    }\n}\n\n\u002F\u002F Repository：乐观更新\nclass ChatRepository(\n    private val dao: MessageDao,\n    private val wsManager: WebSocketManager\n) {\n    fun observeMessages(chatId: String): Flow\u003CList\u003CMessage>> =\n        dao.observeMessages(chatId)\n\n    suspend fun sendMessage(chatId: String, text: String) {\n        val message = Message(\n            chatId = chatId,\n            senderId = currentUserId,\n            text = text,\n            timestamp = System.currentTimeMillis(),\n            status = MessageStatus.SENDING\n        )\n\n        \u002F\u002F 乐观插入：先显示在 UI 上\n        dao.insert(message)\n\n        try {\n            wsManager.send(message)\n            dao.updateStatus(message.id, MessageStatus.SENT)\n        } catch (e: Exception) {\n            dao.updateStatus(message.id, MessageStatus.FAILED)\n        }\n    }\n}\n",[136,14693,14694,14699,14704,14709,14714,14719,14724,14729,14734,14739,14743,14747,14752,14756,14761,14766,14771,14776,14780,14785,14790,14795,14800,14805,14810,14814,14818,14823,14828,14833,14837,14842,14846,14850,14855,14860,14865,14870,14875,14880,14885,14890,14894,14898,14902,14906,14911,14916,14920,14924,14928,14933,14938,14943,14948,14952,14957,14962,14966,14972,14978,14984,14990,14996,15002,15008,15014,15019,15025,15031,15036,15042,15048,15054,15060,15066,15071,15076],{"__ignoreMap":191},[195,14695,14696],{"class":197,"line":198},[195,14697,14698],{},"\u002F\u002F 消息模型\n",[195,14700,14701],{"class":197,"line":230},[195,14702,14703],{},"@Entity\n",[195,14705,14706],{"class":197,"line":251},[195,14707,14708],{},"data class Message(\n",[195,14710,14711],{"class":197,"line":272},[195,14712,14713],{},"    @PrimaryKey val id: String = UUID.randomUUID().toString(),\n",[195,14715,14716],{"class":197,"line":293},[195,14717,14718],{},"    val chatId: String,\n",[195,14720,14721],{"class":197,"line":562},[195,14722,14723],{},"    val senderId: String,\n",[195,14725,14726],{"class":197,"line":583},[195,14727,14728],{},"    val text: String,\n",[195,14730,14731],{"class":197,"line":962},[195,14732,14733],{},"    val timestamp: Long,\n",[195,14735,14736],{"class":197,"line":968},[195,14737,14738],{},"    val status: MessageStatus = MessageStatus.SENDING\n",[195,14740,14741],{"class":197,"line":1274},[195,14742,410],{},[195,14744,14745],{"class":197,"line":1282},[195,14746,1241],{"emptyLinePlaceholder":757},[195,14748,14749],{"class":197,"line":1295},[195,14750,14751],{},"enum class MessageStatus { SENDING, SENT, DELIVERED, READ, FAILED }\n",[195,14753,14754],{"class":197,"line":1309},[195,14755,1241],{"emptyLinePlaceholder":757},[195,14757,14758],{"class":197,"line":2246},[195,14759,14760],{},"\u002F\u002F WebSocket 管理\n",[195,14762,14763],{"class":197,"line":1996},[195,14764,14765],{},"class WebSocketManager(private val dao: MessageDao) {\n",[195,14767,14768],{"class":197,"line":2257},[195,14769,14770],{},"    private var socket: WebSocket? = null\n",[195,14772,14773],{"class":197,"line":2262},[195,14774,14775],{},"    private val _connectionState = MutableStateFlow(ConnectionState.DISCONNECTED)\n",[195,14777,14778],{"class":197,"line":2267},[195,14779,1241],{"emptyLinePlaceholder":757},[195,14781,14782],{"class":197,"line":2273},[195,14783,14784],{},"    fun connect() {\n",[195,14786,14787],{"class":197,"line":2033},[195,14788,14789],{},"        val request = Request.Builder().url(\"wss:\u002F\u002Fchat.example.com\u002Fws\").build()\n",[195,14791,14792],{"class":197,"line":2284},[195,14793,14794],{},"        socket = okHttpClient.newWebSocket(request, object : WebSocketListener() {\n",[195,14796,14797],{"class":197,"line":2460},[195,14798,14799],{},"            override fun onMessage(webSocket: WebSocket, text: String) {\n",[195,14801,14802],{"class":197,"line":2466},[195,14803,14804],{},"                val message = json.decodeFromString\u003CMessage>(text)\n",[195,14806,14807],{"class":197,"line":2472},[195,14808,14809],{},"                scope.launch { dao.insert(message) }  \u002F\u002F 收到消息存入本地\n",[195,14811,14812],{"class":197,"line":2780},[195,14813,5781],{},[195,14815,14816],{"class":197,"line":2786},[195,14817,1241],{"emptyLinePlaceholder":757},[195,14819,14820],{"class":197,"line":2792},[195,14821,14822],{},"            override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {\n",[195,14824,14825],{"class":197,"line":2798},[195,14826,14827],{},"                _connectionState.value = ConnectionState.DISCONNECTED\n",[195,14829,14830],{"class":197,"line":2804},[195,14831,14832],{},"                reconnectWithBackoff()  \u002F\u002F 指数退避重连\n",[195,14834,14835],{"class":197,"line":2810},[195,14836,5781],{},[195,14838,14839],{"class":197,"line":2815},[195,14840,14841],{},"        })\n",[195,14843,14844],{"class":197,"line":2820},[195,14845,2403],{},[195,14847,14848],{"class":197,"line":2825},[195,14849,1241],{"emptyLinePlaceholder":757},[195,14851,14852],{"class":197,"line":2831},[195,14853,14854],{},"    \u002F\u002F 指数退避重连\n",[195,14856,14857],{"class":197,"line":2837},[195,14858,14859],{},"    private fun reconnectWithBackoff() {\n",[195,14861,14862],{"class":197,"line":2843},[195,14863,14864],{},"        scope.launch {\n",[195,14866,14867],{"class":197,"line":2848},[195,14868,14869],{},"            var delay = 1000L\n",[195,14871,14872],{"class":197,"line":2854},[195,14873,14874],{},"            repeat(10) {\n",[195,14876,14877],{"class":197,"line":2860},[195,14878,14879],{},"                delay(delay)\n",[195,14881,14882],{"class":197,"line":2866},[195,14883,14884],{},"                try { connect(); return@launch }\n",[195,14886,14887],{"class":197,"line":2872},[195,14888,14889],{},"                catch (e: Exception) { delay = minOf(delay * 2, 30_000L) }\n",[195,14891,14892],{"class":197,"line":2878},[195,14893,5781],{},[195,14895,14896],{"class":197,"line":2884},[195,14897,2887],{},[195,14899,14900],{"class":197,"line":2890},[195,14901,2403],{},[195,14903,14904],{"class":197,"line":2895},[195,14905,1241],{"emptyLinePlaceholder":757},[195,14907,14908],{"class":197,"line":2900},[195,14909,14910],{},"    fun send(message: Message) {\n",[195,14912,14913],{"class":197,"line":2905},[195,14914,14915],{},"        socket?.send(json.encodeToString(message))\n",[195,14917,14918],{"class":197,"line":2911},[195,14919,2403],{},[195,14921,14922],{"class":197,"line":2917},[195,14923,552],{},[195,14925,14926],{"class":197,"line":2923},[195,14927,1241],{"emptyLinePlaceholder":757},[195,14929,14930],{"class":197,"line":2929},[195,14931,14932],{},"\u002F\u002F Repository：乐观更新\n",[195,14934,14935],{"class":197,"line":2934},[195,14936,14937],{},"class ChatRepository(\n",[195,14939,14940],{"class":197,"line":2939},[195,14941,14942],{},"    private val dao: MessageDao,\n",[195,14944,14945],{"class":197,"line":2945},[195,14946,14947],{},"    private val wsManager: WebSocketManager\n",[195,14949,14950],{"class":197,"line":2951},[195,14951,644],{},[195,14953,14954],{"class":197,"line":2957},[195,14955,14956],{},"    fun observeMessages(chatId: String): Flow\u003CList\u003CMessage>> =\n",[195,14958,14959],{"class":197,"line":2963},[195,14960,14961],{},"        dao.observeMessages(chatId)\n",[195,14963,14964],{"class":197,"line":9396},[195,14965,1241],{"emptyLinePlaceholder":757},[195,14967,14969],{"class":197,"line":14968},59,[195,14970,14971],{},"    suspend fun sendMessage(chatId: String, text: String) {\n",[195,14973,14975],{"class":197,"line":14974},60,[195,14976,14977],{},"        val message = Message(\n",[195,14979,14981],{"class":197,"line":14980},61,[195,14982,14983],{},"            chatId = chatId,\n",[195,14985,14987],{"class":197,"line":14986},62,[195,14988,14989],{},"            senderId = currentUserId,\n",[195,14991,14993],{"class":197,"line":14992},63,[195,14994,14995],{},"            text = text,\n",[195,14997,14999],{"class":197,"line":14998},64,[195,15000,15001],{},"            timestamp = System.currentTimeMillis(),\n",[195,15003,15005],{"class":197,"line":15004},65,[195,15006,15007],{},"            status = MessageStatus.SENDING\n",[195,15009,15011],{"class":197,"line":15010},66,[195,15012,15013],{},"        )\n",[195,15015,15017],{"class":197,"line":15016},67,[195,15018,1241],{"emptyLinePlaceholder":757},[195,15020,15022],{"class":197,"line":15021},68,[195,15023,15024],{},"        \u002F\u002F 乐观插入：先显示在 UI 上\n",[195,15026,15028],{"class":197,"line":15027},69,[195,15029,15030],{},"        dao.insert(message)\n",[195,15032,15034],{"class":197,"line":15033},70,[195,15035,1241],{"emptyLinePlaceholder":757},[195,15037,15039],{"class":197,"line":15038},71,[195,15040,15041],{},"        try {\n",[195,15043,15045],{"class":197,"line":15044},72,[195,15046,15047],{},"            wsManager.send(message)\n",[195,15049,15051],{"class":197,"line":15050},73,[195,15052,15053],{},"            dao.updateStatus(message.id, MessageStatus.SENT)\n",[195,15055,15057],{"class":197,"line":15056},74,[195,15058,15059],{},"        } catch (e: Exception) {\n",[195,15061,15063],{"class":197,"line":15062},75,[195,15064,15065],{},"            dao.updateStatus(message.id, MessageStatus.FAILED)\n",[195,15067,15069],{"class":197,"line":15068},76,[195,15070,2887],{},[195,15072,15074],{"class":197,"line":15073},77,[195,15075,2403],{},[195,15077,15079],{"class":197,"line":15078},78,[195,15080,552],{},[1890,15082],{},[18,15084,15086],{"id":15085},"附录高频面试知识点速查","附录：高频面试知识点速查",[32,15088,15090],{"id":15089},"ios-核心","iOS 核心",[36,15092,15093,15103],{},[39,15094,15095],{},[42,15096,15097,15100],{},[45,15098,15099],{},"主题",[45,15101,15102],{},"关键词",[52,15104,15105,15113,15120,15127,15135,15142],{},[42,15106,15107,15110],{},[57,15108,15109],{},"Swift 语言",[57,15111,15112],{},"struct vs class、POP、enum 关联值、some\u002Fany、@propertyWrapper",[42,15114,15115,15117],{},[57,15116,1913],{},[57,15118,15119],{},"ARC、weak\u002Funowned、闭包 capture list、autoreleasepool",[42,15121,15122,15124],{},[57,15123,4051],{},[57,15125,15126],{},"@State\u002F@StateObject\u002F@ObservedObject、声明式 UI、body 重算",[42,15128,15129,15132],{},[57,15130,15131],{},"并发",[57,15133,15134],{},"async\u002Fawait、Actor\u002F@MainActor、Sendable、TaskGroup",[42,15136,15137,15139],{},[57,15138,3340],{},[57,15140,15141],{},"Instruments、离屏渲染、图片降采样、启动优化",[42,15143,15144,15147],{},[57,15145,15146],{},"系统",[57,15148,15149],{},"RunLoop、App Lifecycle、Scene、Background Task",[32,15151,15153],{"id":15152},"android-核心","Android 核心",[36,15155,15156,15164],{},[39,15157,15158],{},[42,15159,15160,15162],{},[45,15161,15099],{},[45,15163,15102],{},[52,15165,15166,15174,15182,15190,15198,15206],{},[42,15167,15168,15171],{},[57,15169,15170],{},"Kotlin 语言",[57,15172,15173],{},"data\u002Fsealed\u002Fobject class、作用域函数、委托、inline\u002Freified",[42,15175,15176,15179],{},[57,15177,15178],{},"组件",[57,15180,15181],{},"Activity\u002FFragment 生命周期、ViewModel、配置变更",[42,15183,15184,15187],{},[57,15185,15186],{},"Compose",[57,15188,15189],{},"Recomposition、remember、derivedStateOf、Stability",[42,15191,15192,15195],{},[57,15193,15194],{},"Coroutines",[57,15196,15197],{},"结构化并发、Flow 冷热流、StateFlow\u002FSharedFlow",[42,15199,15200,15203],{},[57,15201,15202],{},"Jetpack",[57,15204,15205],{},"Room、Hilt、WorkManager、Navigation、Paging",[42,15207,15208,15210],{},[57,15209,3340],{},[57,15211,15212],{},"Baseline Profile、LeakCanary、R8、Macrobenchmark",[32,15214,15216],{"id":15215},"flutter-核心","Flutter 核心",[36,15218,15219,15227],{},[39,15220,15221],{},[42,15222,15223,15225],{},[45,15224,15099],{},[45,15226,15102],{},[52,15228,15229,15237,15245,15252,15260],{},[42,15230,15231,15234],{},[57,15232,15233],{},"Dart 语言",[57,15235,15236],{},"Null Safety、Isolate、事件循环、Records\u002FPatterns",[42,15238,15239,15242],{},[57,15240,15241],{},"渲染",[57,15243,15244],{},"三棵树、Constraints、RepaintBoundary、Impeller",[42,15246,15247,15249],{},[57,15248,2014],{},[57,15250,15251],{},"BLoC vs Riverpod、InheritedWidget 原理",[42,15253,15254,15257],{},[57,15255,15256],{},"平台通信",[57,15258,15259],{},"MethodChannel\u002FEventChannel\u002FFFI、Pigeon",[42,15261,15262,15264],{},[57,15263,3340],{},[57,15265,15266],{},"const Widget、ListView.builder、AnimatedBuilder child",[32,15268,15269],{"id":15269},"跨平台通用",[36,15271,15272,15280],{},[39,15273,15274],{},[42,15275,15276,15278],{},[45,15277,15099],{},[45,15279,15102],{},[52,15281,15282,15290,15297,15305],{},[42,15283,15284,15287],{},[57,15285,15286],{},"网络",[57,15288,15289],{},"HTTPS\u002FTLS、证书固定、安全存储",[42,15291,15292,15294],{},[57,15293,5860],{},[57,15295,15296],{},"Clean Architecture、单向数据流、离线优先",[42,15298,15299,15302],{},[57,15300,15301],{},"系统设计",[57,15303,15304],{},"推送通知、图片缓存、WebSocket 聊天",[42,15306,15307,15310],{},[57,15308,15309],{},"CI\u002FCD",[57,15311,15312],{},"自动化测试、代码签名、分发",[733,15314,15315],{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":191,"searchDepth":230,"depth":230,"links":15317},[15318,15324,15336,15340,15346,15350,15353,15356,15361,15368,15373,15378,15382,15386,15390,15394,15398,15401,15404,15407,15410,15414,15417,15423],{"id":1894,"depth":230,"text":1894,"children":15319},[15320,15321,15322,15323],{"id":1897,"depth":251,"text":1898},{"id":1946,"depth":251,"text":1947},{"id":1992,"depth":251,"text":1993},{"id":2029,"depth":251,"text":2030},{"id":2060,"depth":230,"text":2061,"children":15325},[15326,15328,15330,15332,15334],{"id":2064,"depth":251,"text":15327},"Q1.1: struct 和 class 的区别？什么时候用哪个？",{"id":2340,"depth":251,"text":15329},"Q1.2: Swift 中的 Protocol 和 Protocol-Oriented Programming 是什么？",{"id":2647,"depth":251,"text":15331},"Q1.3: Swift 中的 enum 有哪些高级用法？",{"id":2970,"depth":251,"text":15333},"Q1.4: 解释 Swift 的 @propertyWrapper",{"id":3198,"depth":251,"text":15335},"Q1.5: some 和 any 关键字的区别是什么？",{"id":3442,"depth":230,"text":3443,"children":15337},[15338,15339],{"id":3446,"depth":251,"text":3447},{"id":3775,"depth":251,"text":3776},{"id":3880,"depth":230,"text":3881,"children":15341},[15342,15343,15345],{"id":3884,"depth":251,"text":3885},{"id":4124,"depth":251,"text":15344},"Q3.2: SwiftUI 中 @State、@StateObject、@ObservedObject、@EnvironmentObject 的区别？",{"id":4461,"depth":251,"text":4462},{"id":4743,"depth":230,"text":4744,"children":15347},[15348,15349],{"id":4747,"depth":251,"text":4748},{"id":5188,"depth":251,"text":5189},{"id":5457,"depth":230,"text":5458,"children":15351},[15352],{"id":5461,"depth":251,"text":5462},{"id":5943,"depth":230,"text":5944,"children":15354},[15355],{"id":5947,"depth":251,"text":5948},{"id":6244,"depth":230,"text":6245,"children":15357},[15358,15359,15360],{"id":6248,"depth":251,"text":6249},{"id":6400,"depth":251,"text":6401},{"id":6529,"depth":251,"text":6530},{"id":6813,"depth":230,"text":6814,"children":15362},[15363,15365,15366,15367],{"id":6817,"depth":251,"text":15364},"Q8.1: Kotlin 中的 data class、sealed class、object、companion object 各有什么用？",{"id":7030,"depth":251,"text":7031},{"id":7362,"depth":251,"text":7363},{"id":7603,"depth":251,"text":7604},{"id":7780,"depth":230,"text":7781,"children":15369},[15370,15371,15372],{"id":7784,"depth":251,"text":7785},{"id":7951,"depth":251,"text":7952},{"id":8168,"depth":251,"text":8169},{"id":8453,"depth":230,"text":8454,"children":15374},[15375,15376,15377],{"id":8457,"depth":251,"text":8458},{"id":8765,"depth":251,"text":8766},{"id":9116,"depth":251,"text":9117},{"id":9403,"depth":230,"text":9404,"children":15379},[15380,15381],{"id":9407,"depth":251,"text":9408},{"id":9678,"depth":251,"text":9679},{"id":9955,"depth":230,"text":9956,"children":15383},[15384,15385],{"id":9959,"depth":251,"text":9960},{"id":10164,"depth":251,"text":10165},{"id":10371,"depth":230,"text":10372,"children":15387},[15388,15389],{"id":10375,"depth":251,"text":10376},{"id":10672,"depth":251,"text":10673},{"id":10861,"depth":230,"text":10862,"children":15391},[15392,15393],{"id":10865,"depth":251,"text":10866},{"id":10970,"depth":251,"text":10971},{"id":11150,"depth":230,"text":11151,"children":15395},[15396,15397],{"id":11154,"depth":251,"text":11155},{"id":11324,"depth":251,"text":11325},{"id":11550,"depth":230,"text":11551,"children":15399},[15400],{"id":11554,"depth":251,"text":11555},{"id":11718,"depth":230,"text":11719,"children":15402},[15403],{"id":11722,"depth":251,"text":11723},{"id":12225,"depth":230,"text":12226,"children":15405},[15406],{"id":12229,"depth":251,"text":12230},{"id":12581,"depth":230,"text":12582,"children":15408},[15409],{"id":12585,"depth":251,"text":12586},{"id":12865,"depth":230,"text":12866,"children":15411},[15412,15413],{"id":12869,"depth":251,"text":12870},{"id":13203,"depth":251,"text":13204},{"id":13353,"depth":230,"text":13354,"children":15415},[15416],{"id":13357,"depth":251,"text":13358},{"id":13818,"depth":230,"text":13819,"children":15418},[15419,15420,15421,15422],{"id":13822,"depth":251,"text":13823},{"id":14050,"depth":251,"text":14051},{"id":14493,"depth":251,"text":14494},{"id":14677,"depth":251,"text":14678},{"id":15085,"depth":230,"text":15086,"children":15424},[15425,15426,15427,15428],{"id":15089,"depth":251,"text":15090},{"id":15152,"depth":251,"text":15153},{"id":15215,"depth":251,"text":15216},{"id":15269,"depth":251,"text":15269},"2026-05-03 10:40:00 CST","覆盖 iOS、Android、Flutter 三端的面试核心考点，包括语言基础、内存管理、并发编程和架构设计。",{},"\u002Fnotes\u002F2026-05-03-mobile-engineer-interview",{"title":1886,"description":15430},"移动端全栈工程师面试详解，涵盖 iOS Swift、Android Kotlin、Flutter Dart 三端的语言基础、架构、性能优化。","移动端全栈工程师面试题与详解｜个人笔记","mobile-engineer-interview","notes\u002F2026-05-03-mobile-engineer-interview","-qbLjxPWSYsO1yg4JnWK_zeTyhxGsZ8tFHsd6W3TOdo",{"id":15440,"title":15441,"body":15442,"category":752,"date":19693,"description":19694,"extension":755,"meta":19695,"navigation":757,"order":752,"path":19696,"seo":19697,"seoDescription":19698,"seoTitle":19699,"slug":19700,"stem":19701,"__hash__":19702},"notes\u002Fnotes\u002F2026-05-03-flutter-to-vue-comparison.md","Flutter 工程师的 Vue 对比学习指南",{"type":8,"value":15443,"toc":19655},[15444,15449,15451,15455,15560,15562,15566,15572,15574,15578,15582,15587,15633,15638,15727,15732,15760,15764,15769,15858,15863,16000,16004,16031,16033,16037,16148,16152,16274,16278,16282,16297,16301,16434,16438,16442,16471,16475,16575,16577,16581,16682,16813,16815,16819,16823,16827,16852,16856,16921,16925,16929,16966,16970,17045,17049,17127,17131,17259,17261,17265,17341,17345,17349,17396,17400,17557,17561,17565,17589,17593,17679,17681,17685,17690,17743,17748,17869,18044,18048,18115,18117,18121,18125,18130,18190,18195,18304,18421,18423,18427,18432,18475,18480,18655,18659,18825,18827,18831,18836,18856,18861,19057,19059,19063,19066,19242,19342,19344,19348,19489,19491,19495,19559,19561,19565,19652],[11,15445,15446],{},[14,15447,15448],{},"以 Flutter\u002FDart 的概念为锚点，快速建立 Vue 3 (Composition API) 的心智模型。",[1890,15450],{},[18,15452,15454],{"id":15453},"_1-整体架构对比","1. 整体架构对比",[36,15456,15457,15469],{},[39,15458,15459],{},[42,15460,15461,15464,15466],{},[45,15462,15463],{},"维度",[45,15465,13135],{},[45,15467,15468],{},"Vue",[52,15470,15471,15482,15492,15506,15516,15527,15538,15549],{},[42,15472,15473,15476,15479],{},[57,15474,15475],{},"语言",[57,15477,15478],{},"Dart",[57,15480,15481],{},"JavaScript \u002F TypeScript",[42,15483,15484,15486,15489],{},[57,15485,15241],{},[57,15487,15488],{},"自绘引擎 (Skia\u002FImpeller)",[57,15490,15491],{},"基于 DOM",[42,15493,15494,15497,15499],{},[57,15495,15496],{},"构建单元",[57,15498,11577],{},[57,15500,15501,15502,15505],{},"Component (",[136,15503,15504],{},".vue"," 单文件组件)",[42,15507,15508,15510,15513],{},[57,15509,2014],{},[57,15511,15512],{},"setState \u002F Provider \u002F Riverpod \u002F Bloc",[57,15514,15515],{},"ref \u002F reactive \u002F Pinia",[42,15517,15518,15521,15524],{},[57,15519,15520],{},"路由",[57,15522,15523],{},"Navigator \u002F GoRouter",[57,15525,15526],{},"Vue Router",[42,15528,15529,15532,15535],{},[57,15530,15531],{},"样式",[57,15533,15534],{},"Widget 属性内联",[57,15536,15537],{},"CSS \u002F Scoped CSS \u002F Tailwind",[42,15539,15540,15543,15546],{},[57,15541,15542],{},"包管理",[57,15544,15545],{},"pub (pubspec.yaml)",[57,15547,15548],{},"npm \u002F pnpm (package.json)",[42,15550,15551,15554,15557],{},[57,15552,15553],{},"构建工具",[57,15555,15556],{},"Flutter CLI",[57,15558,15559],{},"Vite",[1890,15561],{},[18,15563,15565],{"id":15564},"_2-项目结构对比","2. 项目结构对比",[186,15567,15570],{"className":15568,"code":15569,"language":1074},[1072],"# Flutter                          # Vue (Vite 脚手架)\nlib\u002F                               src\u002F\n├── main.dart                      ├── main.ts          # 入口\n├── app.dart                       ├── App.vue          # 根组件\n├── models\u002F                        ├── types\u002F           # 类型定义\n├── screens\u002F                       ├── views\u002F           # 页面组件\n├── widgets\u002F                       ├── components\u002F      # 可复用组件\n├── providers\u002F                     ├── stores\u002F          # Pinia 状态\n├── services\u002F                      ├── api\u002F             # 网络请求\n└── utils\u002F                         ├── utils\u002F\npubspec.yaml                       ├── router\u002F          # 路由配置\n                                   package.json\n",[136,15571,15569],{"__ignoreMap":191},[1890,15573],{},[18,15575,15577],{"id":15576},"_3-组件-widget","3. 组件 = Widget",[32,15579,15581],{"id":15580},"_31-基本组件结构","3.1 基本组件结构",[14,15583,15584],{},[125,15585,15586],{},"Flutter — StatelessWidget",[186,15588,15590],{"className":11162,"code":15589,"language":11164,"meta":191,"style":191},"class Greeting extends StatelessWidget {\n  final String name;\n  const Greeting({required this.name});\n\n  @override\n  Widget build(BuildContext context) {\n    return Text('Hello, $name');\n  }\n}\n",[136,15591,15592,15597,15602,15607,15611,15615,15620,15625,15629],{"__ignoreMap":191},[195,15593,15594],{"class":197,"line":198},[195,15595,15596],{},"class Greeting extends StatelessWidget {\n",[195,15598,15599],{"class":197,"line":230},[195,15600,15601],{},"  final String name;\n",[195,15603,15604],{"class":197,"line":251},[195,15605,15606],{},"  const Greeting({required this.name});\n",[195,15608,15609],{"class":197,"line":272},[195,15610,1241],{"emptyLinePlaceholder":757},[195,15612,15613],{"class":197,"line":293},[195,15614,12090],{},[195,15616,15617],{"class":197,"line":562},[195,15618,15619],{},"  Widget build(BuildContext context) {\n",[195,15621,15622],{"class":197,"line":583},[195,15623,15624],{},"    return Text('Hello, $name');\n",[195,15626,15627],{"class":197,"line":962},[195,15628,965],{},[195,15630,15631],{"class":197,"line":968},[195,15632,552],{},[14,15634,15635],{},[125,15636,15637],{},"Vue — 单文件组件 (SFC)",[186,15639,15643],{"className":15640,"code":15641,"language":15642,"meta":191,"style":191},"language-vue shiki shiki-themes github-light github-dark","\u003Ctemplate>\n  \u003Cp>Hello, {{ name }}\u003C\u002Fp>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\ndefineProps\u003C{ name: string }>()\n\u003C\u002Fscript>\n","vue",[136,15644,15645,15654,15668,15677,15681,15701,15719],{"__ignoreMap":191},[195,15646,15647,15649,15652],{"class":197,"line":198},[195,15648,448],{"class":209},[195,15650,15651],{"class":205},"template",[195,15653,477],{"class":209},[195,15655,15656,15659,15661,15664,15666],{"class":197,"line":230},[195,15657,15658],{"class":209},"  \u003C",[195,15660,14],{"class":205},[195,15662,15663],{"class":209},">Hello, {{ name }}\u003C\u002F",[195,15665,14],{"class":205},[195,15667,477],{"class":209},[195,15669,15670,15673,15675],{"class":197,"line":251},[195,15671,15672],{"class":209},"\u003C\u002F",[195,15674,15651],{"class":205},[195,15676,477],{"class":209},[195,15678,15679],{"class":197,"line":272},[195,15680,1241],{"emptyLinePlaceholder":757},[195,15682,15683,15685,15688,15691,15694,15696,15699],{"class":197,"line":293},[195,15684,448],{"class":209},[195,15686,15687],{"class":205},"script",[195,15689,15690],{"class":201}," setup",[195,15692,15693],{"class":201}," lang",[195,15695,424],{"class":209},[195,15697,15698],{"class":459},"\"ts\"",[195,15700,477],{"class":209},[195,15702,15703,15706,15709,15711,15713,15716],{"class":197,"line":562},[195,15704,15705],{"class":201},"defineProps",[195,15707,15708],{"class":209},"\u003C{ ",[195,15710,13388],{"class":634},[195,15712,638],{"class":223},[195,15714,15715],{"class":213}," string",[195,15717,15718],{"class":209}," }>()\n",[195,15720,15721,15723,15725],{"class":197,"line":583},[195,15722,15672],{"class":209},[195,15724,15687],{"class":205},[195,15726,477],{"class":209},[14,15728,15729],{},[125,15730,15731],{},"对比要点：",[129,15733,15734,15743,15749],{},[132,15735,15736,15737,15739,15740],{},"Flutter 的 ",[136,15738,11581],{}," 方法 ≈ Vue 的 ",[136,15741,15742],{},"\u003Ctemplate>",[132,15744,15745,15746],{},"Flutter 的构造函数参数 ≈ Vue 的 ",[136,15747,15748],{},"props",[132,15750,15751,15752,15755,15756,15759],{},"Vue 用 ",[136,15753,15754],{},"{{ }}"," 做插值，Flutter 用 ",[136,15757,15758],{},"${}"," 在 Dart 字符串里插值",[32,15761,15763],{"id":15762},"_32-有状态组件","3.2 有状态组件",[14,15765,15766],{},[125,15767,15768],{},"Flutter — StatefulWidget",[186,15770,15772],{"className":11162,"code":15771,"language":11164,"meta":191,"style":191},"class Counter extends StatefulWidget {\n  @override\n  State\u003CCounter> createState() => _CounterState();\n}\n\nclass _CounterState extends State\u003CCounter> {\n  int count = 0;\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(children: [\n      Text('$count'),\n      ElevatedButton(\n        onPressed: () => setState(() => count++),\n        child: Text('Add'),\n      ),\n    ]);\n  }\n}\n",[136,15773,15774,15779,15783,15788,15792,15796,15801,15805,15809,15813,15817,15821,15826,15831,15836,15841,15846,15850,15854],{"__ignoreMap":191},[195,15775,15776],{"class":197,"line":198},[195,15777,15778],{},"class Counter extends StatefulWidget {\n",[195,15780,15781],{"class":197,"line":230},[195,15782,12090],{},[195,15784,15785],{"class":197,"line":251},[195,15786,15787],{},"  State\u003CCounter> createState() => _CounterState();\n",[195,15789,15790],{"class":197,"line":272},[195,15791,552],{},[195,15793,15794],{"class":197,"line":293},[195,15795,1241],{"emptyLinePlaceholder":757},[195,15797,15798],{"class":197,"line":562},[195,15799,15800],{},"class _CounterState extends State\u003CCounter> {\n",[195,15802,15803],{"class":197,"line":583},[195,15804,12615],{},[195,15806,15807],{"class":197,"line":962},[195,15808,1241],{"emptyLinePlaceholder":757},[195,15810,15811],{"class":197,"line":968},[195,15812,12090],{},[195,15814,15815],{"class":197,"line":1274},[195,15816,15619],{},[195,15818,15819],{"class":197,"line":1282},[195,15820,12625],{},[195,15822,15823],{"class":197,"line":1295},[195,15824,15825],{},"      Text('$count'),\n",[195,15827,15828],{"class":197,"line":1309},[195,15829,15830],{},"      ElevatedButton(\n",[195,15832,15833],{"class":197,"line":2246},[195,15834,15835],{},"        onPressed: () => setState(() => count++),\n",[195,15837,15838],{"class":197,"line":1996},[195,15839,15840],{},"        child: Text('Add'),\n",[195,15842,15843],{"class":197,"line":2257},[195,15844,15845],{},"      ),\n",[195,15847,15848],{"class":197,"line":2262},[195,15849,12645],{},[195,15851,15852],{"class":197,"line":2267},[195,15853,965],{},[195,15855,15856],{"class":197,"line":2273},[195,15857,552],{},[14,15859,15860],{},[125,15861,15862],{},"Vue — Composition API",[186,15864,15866],{"className":15640,"code":15865,"language":15642,"meta":191,"style":191},"\u003Ctemplate>\n  \u003Cdiv>\n    \u003Cp>{{ count }}\u003C\u002Fp>\n    \u003Cbutton @click=\"count++\">Add\u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nimport { ref } from 'vue'\n\nconst count = ref(0)\n\u003C\u002Fscript>\n",[136,15867,15868,15876,15884,15898,15920,15929,15937,15941,15957,15971,15975,15992],{"__ignoreMap":191},[195,15869,15870,15872,15874],{"class":197,"line":198},[195,15871,448],{"class":209},[195,15873,15651],{"class":205},[195,15875,477],{"class":209},[195,15877,15878,15880,15882],{"class":197,"line":230},[195,15879,15658],{"class":209},[195,15881,451],{"class":205},[195,15883,477],{"class":209},[195,15885,15886,15889,15891,15894,15896],{"class":197,"line":251},[195,15887,15888],{"class":209},"    \u003C",[195,15890,14],{"class":205},[195,15892,15893],{"class":209},">{{ count }}\u003C\u002F",[195,15895,14],{"class":205},[195,15897,477],{"class":209},[195,15899,15900,15902,15905,15908,15910,15913,15916,15918],{"class":197,"line":272},[195,15901,15888],{"class":209},[195,15903,15904],{"class":205},"button",[195,15906,15907],{"class":201}," @click",[195,15909,424],{"class":209},[195,15911,15912],{"class":459},"\"count++\"",[195,15914,15915],{"class":209},">Add\u003C\u002F",[195,15917,15904],{"class":205},[195,15919,477],{"class":209},[195,15921,15922,15925,15927],{"class":197,"line":293},[195,15923,15924],{"class":209},"  \u003C\u002F",[195,15926,451],{"class":205},[195,15928,477],{"class":209},[195,15930,15931,15933,15935],{"class":197,"line":562},[195,15932,15672],{"class":209},[195,15934,15651],{"class":205},[195,15936,477],{"class":209},[195,15938,15939],{"class":197,"line":583},[195,15940,1241],{"emptyLinePlaceholder":757},[195,15942,15943,15945,15947,15949,15951,15953,15955],{"class":197,"line":962},[195,15944,448],{"class":209},[195,15946,15687],{"class":205},[195,15948,15690],{"class":201},[195,15950,15693],{"class":201},[195,15952,424],{"class":209},[195,15954,15698],{"class":459},[195,15956,477],{"class":209},[195,15958,15959,15962,15965,15968],{"class":197,"line":968},[195,15960,15961],{"class":223},"import",[195,15963,15964],{"class":209}," { ref } ",[195,15966,15967],{"class":223},"from",[195,15969,15970],{"class":459}," 'vue'\n",[195,15972,15973],{"class":197,"line":1274},[195,15974,1241],{"emptyLinePlaceholder":757},[195,15976,15977,15979,15982,15984,15986,15988,15990],{"class":197,"line":1282},[195,15978,392],{"class":223},[195,15980,15981],{"class":213}," count",[195,15983,398],{"class":223},[195,15985,401],{"class":201},[195,15987,404],{"class":209},[195,15989,407],{"class":213},[195,15991,410],{"class":209},[195,15993,15994,15996,15998],{"class":197,"line":1295},[195,15995,15672],{"class":209},[195,15997,15687],{"class":205},[195,15999,477],{"class":209},[14,16001,16002],{},[125,16003,15731],{},[129,16005,16006,16021,16028],{},[132,16007,16008,16011,16012,905,16015,16018,16019,698],{},[136,16009,16010],{},"setState()"," ≈ 直接修改 ",[136,16013,16014],{},"ref",[136,16016,16017],{},".value","（模板中自动解包，不用写 ",[136,16020,16017],{},[132,16022,16023,16024,16027],{},"Flutter 需要 StatefulWidget + State 两个类，Vue 只需 ",[136,16025,16026],{},"ref()"," 一行",[132,16029,16030],{},"Vue 的响应式是自动追踪依赖的，不需要手动调用 setState",[1890,16032],{},[18,16034,16036],{"id":16035},"_4-响应式系统对比","4. 响应式系统对比",[36,16038,16039,16049],{},[39,16040,16041],{},[42,16042,16043,16045,16047],{},[45,16044,13135],{},[45,16046,15468],{},[45,16048,13780],{},[52,16050,16051,16064,16079,16094,16110,16129],{},[42,16052,16053,16058,16061],{},[57,16054,16055],{},[136,16056,16057],{},"setState(() { })",[57,16059,16060],{},"自动（修改 ref\u002Freactive 即触发）",[57,16062,16063],{},"Vue 无需手动通知",[42,16065,16066,16071,16076],{},[57,16067,16068],{},[136,16069,16070],{},"ValueNotifier\u003CT>",[57,16072,16073],{},[136,16074,16075],{},"ref\u003CT>()",[57,16077,16078],{},"单值响应式",[42,16080,16081,16086,16091],{},[57,16082,16083],{},[136,16084,16085],{},"ChangeNotifier",[57,16087,16088],{},[136,16089,16090],{},"reactive({})",[57,16092,16093],{},"对象级响应式",[42,16095,16096,16101,16107],{},[57,16097,16098],{},[136,16099,16100],{},"Provider.of\u003CT>(context)",[57,16102,16103,16106],{},[136,16104,16105],{},"inject()"," \u002F Pinia store",[57,16108,16109],{},"跨组件共享状态",[42,16111,16112,16117,16126],{},[57,16113,16114],{},[136,16115,16116],{},"StreamBuilder",[57,16118,16119,16122,16123],{},[136,16120,16121],{},"watch()"," \u002F ",[136,16124,16125],{},"watchEffect()",[57,16127,16128],{},"监听变化并执行副作用",[42,16130,16131,16136,16145],{},[57,16132,16133],{},[136,16134,16135],{},"FutureBuilder",[57,16137,16138,16141,16142],{},[136,16139,16140],{},"onMounted"," + async 或 ",[136,16143,16144],{},"Suspense",[57,16146,16147],{},"异步数据加载",[32,16149,16151],{"id":16150},"ref-vs-reactive","ref vs reactive",[186,16153,16155],{"className":15640,"code":16154,"language":15642,"meta":191,"style":191},"\u003Cscript setup lang=\"ts\">\nimport { ref, reactive } from 'vue'\n\n\u002F\u002F ref — 用于基本类型（类似 ValueNotifier）\nconst count = ref(0)\ncount.value++  \u002F\u002F 脚本中需要 .value\n\n\u002F\u002F reactive — 用于对象（类似 ChangeNotifier）\nconst user = reactive({ name: 'Alice', age: 25 })\nuser.age++     \u002F\u002F 直接修改属性，不需要 .value\n\u003C\u002Fscript>\n",[136,16156,16157,16173,16184,16188,16193,16209,16220,16224,16229,16256,16266],{"__ignoreMap":191},[195,16158,16159,16161,16163,16165,16167,16169,16171],{"class":197,"line":198},[195,16160,448],{"class":209},[195,16162,15687],{"class":205},[195,16164,15690],{"class":201},[195,16166,15693],{"class":201},[195,16168,424],{"class":209},[195,16170,15698],{"class":459},[195,16172,477],{"class":209},[195,16174,16175,16177,16180,16182],{"class":197,"line":230},[195,16176,15961],{"class":223},[195,16178,16179],{"class":209}," { ref, reactive } ",[195,16181,15967],{"class":223},[195,16183,15970],{"class":459},[195,16185,16186],{"class":197,"line":251},[195,16187,1241],{"emptyLinePlaceholder":757},[195,16189,16190],{"class":197,"line":272},[195,16191,16192],{"class":415},"\u002F\u002F ref — 用于基本类型（类似 ValueNotifier）\n",[195,16194,16195,16197,16199,16201,16203,16205,16207],{"class":197,"line":293},[195,16196,392],{"class":223},[195,16198,15981],{"class":213},[195,16200,398],{"class":223},[195,16202,401],{"class":201},[195,16204,404],{"class":209},[195,16206,407],{"class":213},[195,16208,410],{"class":209},[195,16210,16211,16214,16217],{"class":197,"line":562},[195,16212,16213],{"class":209},"count.value",[195,16215,16216],{"class":223},"++",[195,16218,16219],{"class":415},"  \u002F\u002F 脚本中需要 .value\n",[195,16221,16222],{"class":197,"line":583},[195,16223,1241],{"emptyLinePlaceholder":757},[195,16225,16226],{"class":197,"line":962},[195,16227,16228],{"class":415},"\u002F\u002F reactive — 用于对象（类似 ChangeNotifier）\n",[195,16230,16231,16233,16236,16238,16241,16244,16247,16250,16253],{"class":197,"line":968},[195,16232,392],{"class":223},[195,16234,16235],{"class":213}," user",[195,16237,398],{"class":223},[195,16239,16240],{"class":201}," reactive",[195,16242,16243],{"class":209},"({ name: ",[195,16245,16246],{"class":459},"'Alice'",[195,16248,16249],{"class":209},", age: ",[195,16251,16252],{"class":213},"25",[195,16254,16255],{"class":209}," })\n",[195,16257,16258,16261,16263],{"class":197,"line":1274},[195,16259,16260],{"class":209},"user.age",[195,16262,16216],{"class":223},[195,16264,16265],{"class":415},"     \u002F\u002F 直接修改属性，不需要 .value\n",[195,16267,16268,16270,16272],{"class":197,"line":1282},[195,16269,15672],{"class":209},[195,16271,15687],{"class":205},[195,16273,477],{"class":209},[32,16275,16277],{"id":16276},"计算属性-派生状态","计算属性 = 派生状态",[14,16279,16280],{},[125,16281,13135],{},[186,16283,16285],{"className":11162,"code":16284,"language":11164,"meta":191,"style":191},"\u002F\u002F 每次 build 都重新计算\nString get fullName => '${firstName} ${lastName}';\n",[136,16286,16287,16292],{"__ignoreMap":191},[195,16288,16289],{"class":197,"line":198},[195,16290,16291],{},"\u002F\u002F 每次 build 都重新计算\n",[195,16293,16294],{"class":197,"line":230},[195,16295,16296],{},"String get fullName => '${firstName} ${lastName}';\n",[14,16298,16299],{},[125,16300,15468],{},[186,16302,16304],{"className":15640,"code":16303,"language":15642,"meta":191,"style":191},"\u003Cscript setup lang=\"ts\">\nimport { ref, computed } from 'vue'\n\nconst firstName = ref('Alice')\nconst lastName = ref('Smith')\n\n\u002F\u002F 自动缓存，只在依赖变化时重新计算\nconst fullName = computed(() => `${firstName.value} ${lastName.value}`)\n\u003C\u002Fscript>\n",[136,16305,16306,16322,16333,16337,16354,16372,16376,16381,16426],{"__ignoreMap":191},[195,16307,16308,16310,16312,16314,16316,16318,16320],{"class":197,"line":198},[195,16309,448],{"class":209},[195,16311,15687],{"class":205},[195,16313,15690],{"class":201},[195,16315,15693],{"class":201},[195,16317,424],{"class":209},[195,16319,15698],{"class":459},[195,16321,477],{"class":209},[195,16323,16324,16326,16329,16331],{"class":197,"line":230},[195,16325,15961],{"class":223},[195,16327,16328],{"class":209}," { ref, computed } ",[195,16330,15967],{"class":223},[195,16332,15970],{"class":459},[195,16334,16335],{"class":197,"line":251},[195,16336,1241],{"emptyLinePlaceholder":757},[195,16338,16339,16341,16344,16346,16348,16350,16352],{"class":197,"line":272},[195,16340,392],{"class":223},[195,16342,16343],{"class":213}," firstName",[195,16345,398],{"class":223},[195,16347,401],{"class":201},[195,16349,404],{"class":209},[195,16351,16246],{"class":459},[195,16353,410],{"class":209},[195,16355,16356,16358,16361,16363,16365,16367,16370],{"class":197,"line":293},[195,16357,392],{"class":223},[195,16359,16360],{"class":213}," lastName",[195,16362,398],{"class":223},[195,16364,401],{"class":201},[195,16366,404],{"class":209},[195,16368,16369],{"class":459},"'Smith'",[195,16371,410],{"class":209},[195,16373,16374],{"class":197,"line":562},[195,16375,1241],{"emptyLinePlaceholder":757},[195,16377,16378],{"class":197,"line":583},[195,16379,16380],{"class":415},"\u002F\u002F 自动缓存，只在依赖变化时重新计算\n",[195,16382,16383,16385,16388,16390,16393,16396,16399,16402,16405,16408,16411,16414,16417,16419,16421,16424],{"class":197,"line":962},[195,16384,392],{"class":223},[195,16386,16387],{"class":213}," fullName",[195,16389,398],{"class":223},[195,16391,16392],{"class":201}," computed",[195,16394,16395],{"class":209},"(() ",[195,16397,16398],{"class":223},"=>",[195,16400,16401],{"class":459}," `${",[195,16403,16404],{"class":209},"firstName",[195,16406,16407],{"class":459},".",[195,16409,16410],{"class":209},"value",[195,16412,16413],{"class":459},"} ${",[195,16415,16416],{"class":209},"lastName",[195,16418,16407],{"class":459},[195,16420,16410],{"class":209},[195,16422,16423],{"class":459},"}`",[195,16425,410],{"class":209},[195,16427,16428,16430,16432],{"class":197,"line":968},[195,16429,15672],{"class":209},[195,16431,15687],{"class":205},[195,16433,477],{"class":209},[32,16435,16437],{"id":16436},"侦听器-监听变化","侦听器 = 监听变化",[14,16439,16440],{},[125,16441,13135],{},[186,16443,16445],{"className":11162,"code":16444,"language":11164,"meta":191,"style":191},"\u002F\u002F 用 didUpdateWidget 或 addListener\n@override\nvoid didUpdateWidget(oldWidget) {\n  if (widget.id != oldWidget.id) fetchData(widget.id);\n}\n",[136,16446,16447,16452,16457,16462,16467],{"__ignoreMap":191},[195,16448,16449],{"class":197,"line":198},[195,16450,16451],{},"\u002F\u002F 用 didUpdateWidget 或 addListener\n",[195,16453,16454],{"class":197,"line":230},[195,16455,16456],{},"@override\n",[195,16458,16459],{"class":197,"line":251},[195,16460,16461],{},"void didUpdateWidget(oldWidget) {\n",[195,16463,16464],{"class":197,"line":272},[195,16465,16466],{},"  if (widget.id != oldWidget.id) fetchData(widget.id);\n",[195,16468,16469],{"class":197,"line":293},[195,16470,552],{},[14,16472,16473],{},[125,16474,15468],{},[186,16476,16478],{"className":15640,"code":16477,"language":15642,"meta":191,"style":191},"\u003Cscript setup lang=\"ts\">\nimport { ref, watch } from 'vue'\n\nconst id = ref(1)\n\nwatch(id, (newVal, oldVal) => {\n  fetchData(newVal)\n})\n\u003C\u002Fscript>\n",[136,16479,16480,16496,16507,16511,16528,16532,16554,16562,16567],{"__ignoreMap":191},[195,16481,16482,16484,16486,16488,16490,16492,16494],{"class":197,"line":198},[195,16483,448],{"class":209},[195,16485,15687],{"class":205},[195,16487,15690],{"class":201},[195,16489,15693],{"class":201},[195,16491,424],{"class":209},[195,16493,15698],{"class":459},[195,16495,477],{"class":209},[195,16497,16498,16500,16503,16505],{"class":197,"line":230},[195,16499,15961],{"class":223},[195,16501,16502],{"class":209}," { ref, watch } ",[195,16504,15967],{"class":223},[195,16506,15970],{"class":459},[195,16508,16509],{"class":197,"line":251},[195,16510,1241],{"emptyLinePlaceholder":757},[195,16512,16513,16515,16518,16520,16522,16524,16526],{"class":197,"line":272},[195,16514,392],{"class":223},[195,16516,16517],{"class":213}," id",[195,16519,398],{"class":223},[195,16521,401],{"class":201},[195,16523,404],{"class":209},[195,16525,1290],{"class":213},[195,16527,410],{"class":209},[195,16529,16530],{"class":197,"line":293},[195,16531,1241],{"emptyLinePlaceholder":757},[195,16533,16534,16537,16540,16543,16545,16548,16550,16552],{"class":197,"line":562},[195,16535,16536],{"class":201},"watch",[195,16538,16539],{"class":209},"(id, (",[195,16541,16542],{"class":634},"newVal",[195,16544,301],{"class":209},[195,16546,16547],{"class":634},"oldVal",[195,16549,516],{"class":209},[195,16551,16398],{"class":223},[195,16553,496],{"class":209},[195,16555,16556,16559],{"class":197,"line":583},[195,16557,16558],{"class":201},"  fetchData",[195,16560,16561],{"class":209},"(newVal)\n",[195,16563,16564],{"class":197,"line":962},[195,16565,16566],{"class":209},"})\n",[195,16568,16569,16571,16573],{"class":197,"line":968},[195,16570,15672],{"class":209},[195,16572,15687],{"class":205},[195,16574,477],{"class":209},[1890,16576],{},[18,16578,16580],{"id":16579},"_5-生命周期对比","5. 生命周期对比",[36,16582,16583,16596],{},[39,16584,16585],{},[42,16586,16587,16590,16593],{},[45,16588,16589],{},"Flutter (State)",[45,16591,16592],{},"Vue 3 (Composition API)",[45,16594,16595],{},"时机",[52,16597,16598,16613,16628,16643,16657,16670],{},[42,16599,16600,16605,16610],{},[57,16601,16602],{},[136,16603,16604],{},"initState()",[57,16606,16607],{},[136,16608,16609],{},"onMounted()",[57,16611,16612],{},"组件挂载\u002F初始化",[42,16614,16615,16620,16625],{},[57,16616,16617],{},[136,16618,16619],{},"didUpdateWidget()",[57,16621,16622],{},[136,16623,16624],{},"onUpdated()",[57,16626,16627],{},"更新后",[42,16629,16630,16635,16640],{},[57,16631,16632],{},[136,16633,16634],{},"dispose()",[57,16636,16637],{},[136,16638,16639],{},"onUnmounted()",[57,16641,16642],{},"销毁\u002F卸载",[42,16644,16645,16650,16654],{},[57,16646,16647],{},[136,16648,16649],{},"didChangeDependencies()",[57,16651,16652],{},[136,16653,16121],{},[57,16655,16656],{},"依赖变化",[42,16658,16659,16662,16667],{},[57,16660,16661],{},"—",[57,16663,16664],{},[136,16665,16666],{},"onBeforeMount()",[57,16668,16669],{},"挂载前",[42,16671,16672,16674,16679],{},[57,16673,16661],{},[57,16675,16676],{},[136,16677,16678],{},"onBeforeUpdate()",[57,16680,16681],{},"更新前",[186,16683,16685],{"className":15640,"code":16684,"language":15642,"meta":191,"style":191},"\u003Cscript setup lang=\"ts\">\nimport { onMounted, onUnmounted } from 'vue'\n\n\u002F\u002F ≈ initState\nonMounted(() => {\n  console.log('组件已挂载')\n  window.addEventListener('resize', onResize)\n})\n\n\u002F\u002F ≈ dispose\nonUnmounted(() => {\n  window.removeEventListener('resize', onResize)\n})\n\u003C\u002Fscript>\n",[136,16686,16687,16703,16714,16718,16723,16733,16748,16764,16768,16772,16777,16788,16801,16805],{"__ignoreMap":191},[195,16688,16689,16691,16693,16695,16697,16699,16701],{"class":197,"line":198},[195,16690,448],{"class":209},[195,16692,15687],{"class":205},[195,16694,15690],{"class":201},[195,16696,15693],{"class":201},[195,16698,424],{"class":209},[195,16700,15698],{"class":459},[195,16702,477],{"class":209},[195,16704,16705,16707,16710,16712],{"class":197,"line":230},[195,16706,15961],{"class":223},[195,16708,16709],{"class":209}," { onMounted, onUnmounted } ",[195,16711,15967],{"class":223},[195,16713,15970],{"class":459},[195,16715,16716],{"class":197,"line":251},[195,16717,1241],{"emptyLinePlaceholder":757},[195,16719,16720],{"class":197,"line":272},[195,16721,16722],{"class":415},"\u002F\u002F ≈ initState\n",[195,16724,16725,16727,16729,16731],{"class":197,"line":293},[195,16726,16140],{"class":201},[195,16728,16395],{"class":209},[195,16730,16398],{"class":223},[195,16732,496],{"class":209},[195,16734,16735,16738,16741,16743,16746],{"class":197,"line":562},[195,16736,16737],{"class":209},"  console.",[195,16739,16740],{"class":201},"log",[195,16742,404],{"class":209},[195,16744,16745],{"class":459},"'组件已挂载'",[195,16747,410],{"class":209},[195,16749,16750,16753,16756,16758,16761],{"class":197,"line":583},[195,16751,16752],{"class":209},"  window.",[195,16754,16755],{"class":201},"addEventListener",[195,16757,404],{"class":209},[195,16759,16760],{"class":459},"'resize'",[195,16762,16763],{"class":209},", onResize)\n",[195,16765,16766],{"class":197,"line":962},[195,16767,16566],{"class":209},[195,16769,16770],{"class":197,"line":968},[195,16771,1241],{"emptyLinePlaceholder":757},[195,16773,16774],{"class":197,"line":1274},[195,16775,16776],{"class":415},"\u002F\u002F ≈ dispose\n",[195,16778,16779,16782,16784,16786],{"class":197,"line":1282},[195,16780,16781],{"class":201},"onUnmounted",[195,16783,16395],{"class":209},[195,16785,16398],{"class":223},[195,16787,496],{"class":209},[195,16789,16790,16792,16795,16797,16799],{"class":197,"line":1295},[195,16791,16752],{"class":209},[195,16793,16794],{"class":201},"removeEventListener",[195,16796,404],{"class":209},[195,16798,16760],{"class":459},[195,16800,16763],{"class":209},[195,16802,16803],{"class":197,"line":1309},[195,16804,16566],{"class":209},[195,16806,16807,16809,16811],{"class":197,"line":2246},[195,16808,15672],{"class":209},[195,16810,15687],{"class":205},[195,16812,477],{"class":209},[1890,16814],{},[18,16816,16818],{"id":16817},"_6-模板语法-widget-树","6. 模板语法 = Widget 树",[32,16820,16822],{"id":16821},"_61-条件渲染","6.1 条件渲染",[14,16824,16825],{},[125,16826,13135],{},[186,16828,16830],{"className":11162,"code":16829,"language":11164,"meta":191,"style":191},"Column(children: [\n  if (isLoggedIn) Text('Welcome'),\n  if (!isLoggedIn) TextButton(onPressed: login, child: Text('Login')),\n])\n",[136,16831,16832,16837,16842,16847],{"__ignoreMap":191},[195,16833,16834],{"class":197,"line":198},[195,16835,16836],{},"Column(children: [\n",[195,16838,16839],{"class":197,"line":230},[195,16840,16841],{},"  if (isLoggedIn) Text('Welcome'),\n",[195,16843,16844],{"class":197,"line":251},[195,16845,16846],{},"  if (!isLoggedIn) TextButton(onPressed: login, child: Text('Login')),\n",[195,16848,16849],{"class":197,"line":272},[195,16850,16851],{},"])\n",[14,16853,16854],{},[125,16855,15468],{},[186,16857,16859],{"className":15640,"code":16858,"language":15642,"meta":191,"style":191},"\u003Ctemplate>\n  \u003Cp v-if=\"isLoggedIn\">Welcome\u003C\u002Fp>\n  \u003Cbutton v-else @click=\"login\">Login\u003C\u002Fbutton>\n\u003C\u002Ftemplate>\n",[136,16860,16861,16869,16890,16913],{"__ignoreMap":191},[195,16862,16863,16865,16867],{"class":197,"line":198},[195,16864,448],{"class":209},[195,16866,15651],{"class":205},[195,16868,477],{"class":209},[195,16870,16871,16873,16875,16878,16880,16883,16886,16888],{"class":197,"line":230},[195,16872,15658],{"class":209},[195,16874,14],{"class":205},[195,16876,16877],{"class":201}," v-if",[195,16879,424],{"class":209},[195,16881,16882],{"class":459},"\"isLoggedIn\"",[195,16884,16885],{"class":209},">Welcome\u003C\u002F",[195,16887,14],{"class":205},[195,16889,477],{"class":209},[195,16891,16892,16894,16896,16899,16901,16903,16906,16909,16911],{"class":197,"line":251},[195,16893,15658],{"class":209},[195,16895,15904],{"class":205},[195,16897,16898],{"class":201}," v-else",[195,16900,15907],{"class":201},[195,16902,424],{"class":209},[195,16904,16905],{"class":459},"\"login\"",[195,16907,16908],{"class":209},">Login\u003C\u002F",[195,16910,15904],{"class":205},[195,16912,477],{"class":209},[195,16914,16915,16917,16919],{"class":197,"line":272},[195,16916,15672],{"class":209},[195,16918,15651],{"class":205},[195,16920,477],{"class":209},[32,16922,16924],{"id":16923},"_62-列表渲染","6.2 列表渲染",[14,16926,16927],{},[125,16928,13135],{},[186,16930,16932],{"className":11162,"code":16931,"language":11164,"meta":191,"style":191},"ListView.builder(\n  itemCount: items.length,\n  itemBuilder: (ctx, i) => ListTile(\n    key: ValueKey(items[i].id),\n    title: Text(items[i].name),\n  ),\n)\n",[136,16933,16934,16938,16943,16948,16953,16958,16962],{"__ignoreMap":191},[195,16935,16936],{"class":197,"line":198},[195,16937,12729],{},[195,16939,16940],{"class":197,"line":230},[195,16941,16942],{},"  itemCount: items.length,\n",[195,16944,16945],{"class":197,"line":251},[195,16946,16947],{},"  itemBuilder: (ctx, i) => ListTile(\n",[195,16949,16950],{"class":197,"line":272},[195,16951,16952],{},"    key: ValueKey(items[i].id),\n",[195,16954,16955],{"class":197,"line":293},[195,16956,16957],{},"    title: Text(items[i].name),\n",[195,16959,16960],{"class":197,"line":562},[195,16961,11676],{},[195,16963,16964],{"class":197,"line":583},[195,16965,410],{},[14,16967,16968],{},[125,16969,15468],{},[186,16971,16973],{"className":15640,"code":16972,"language":15642,"meta":191,"style":191},"\u003Ctemplate>\n  \u003Cul>\n    \u003Cli v-for=\"item in items\" :key=\"item.id\">\n      {{ item.name }}\n    \u003C\u002Fli>\n  \u003C\u002Ful>\n\u003C\u002Ftemplate>\n",[136,16974,16975,16983,16991,17015,17020,17029,17037],{"__ignoreMap":191},[195,16976,16977,16979,16981],{"class":197,"line":198},[195,16978,448],{"class":209},[195,16980,15651],{"class":205},[195,16982,477],{"class":209},[195,16984,16985,16987,16989],{"class":197,"line":230},[195,16986,15658],{"class":209},[195,16988,129],{"class":205},[195,16990,477],{"class":209},[195,16992,16993,16995,16997,17000,17002,17005,17008,17010,17013],{"class":197,"line":251},[195,16994,15888],{"class":209},[195,16996,132],{"class":205},[195,16998,16999],{"class":201}," v-for",[195,17001,424],{"class":209},[195,17003,17004],{"class":459},"\"item in items\"",[195,17006,17007],{"class":201}," :key",[195,17009,424],{"class":209},[195,17011,17012],{"class":459},"\"item.id\"",[195,17014,477],{"class":209},[195,17016,17017],{"class":197,"line":272},[195,17018,17019],{"class":209},"      {{ item.name }}\n",[195,17021,17022,17025,17027],{"class":197,"line":293},[195,17023,17024],{"class":209},"    \u003C\u002F",[195,17026,132],{"class":205},[195,17028,477],{"class":209},[195,17030,17031,17033,17035],{"class":197,"line":562},[195,17032,15924],{"class":209},[195,17034,129],{"class":205},[195,17036,477],{"class":209},[195,17038,17039,17041,17043],{"class":197,"line":583},[195,17040,15672],{"class":209},[195,17042,15651],{"class":205},[195,17044,477],{"class":209},[32,17046,17048],{"id":17047},"_63-事件绑定","6.3 事件绑定",[36,17050,17051,17061],{},[39,17052,17053],{},[42,17054,17055,17057,17059],{},[45,17056,13135],{},[45,17058,15468],{},[45,17060,13780],{},[52,17062,17063,17078,17093,17108],{},[42,17064,17065,17070,17075],{},[57,17066,17067],{},[136,17068,17069],{},"onPressed: () => {}",[57,17071,17072],{},[136,17073,17074],{},"@click=\"handler\"",[57,17076,17077],{},"点击",[42,17079,17080,17085,17090],{},[57,17081,17082],{},[136,17083,17084],{},"onChanged: (v) => {}",[57,17086,17087],{},[136,17088,17089],{},"@input=\"handler\"",[57,17091,17092],{},"输入",[42,17094,17095,17100,17105],{},[57,17096,17097],{},[136,17098,17099],{},"onSubmitted: (v) => {}",[57,17101,17102],{},[136,17103,17104],{},"@submit.prevent=\"handler\"",[57,17106,17107],{},"表单提交",[42,17109,17110,17115,17124],{},[57,17111,17112],{},[136,17113,17114],{},"GestureDetector",[57,17116,17117,1547,17120,17123],{},[136,17118,17119],{},"@mousedown",[136,17121,17122],{},"@touchstart"," 等",[57,17125,17126],{},"手势",[32,17128,17130],{"id":17129},"_64-属性绑定","6.4 属性绑定",[186,17132,17134],{"className":15640,"code":17133,"language":15642,"meta":191,"style":191},"\u003Ctemplate>\n  \u003C!-- 静态属性 -->\n  \u003Cimg src=\"\u002Flogo.png\" \u002F>\n\n  \u003C!-- 动态绑定（v-bind 缩写为 :） -->\n  \u003Cimg :src=\"imageUrl\" \u002F>\n\n  \u003C!-- class 绑定 -->\n  \u003Cdiv :class=\"{ active: isActive, disabled: isDisabled }\">\u003C\u002Fdiv>\n\n  \u003C!-- style 绑定 -->\n  \u003Cdiv :style=\"{ color: textColor, fontSize: size + 'px' }\">\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n",[136,17135,17136,17144,17149,17167,17171,17176,17192,17196,17201,17222,17226,17231,17251],{"__ignoreMap":191},[195,17137,17138,17140,17142],{"class":197,"line":198},[195,17139,448],{"class":209},[195,17141,15651],{"class":205},[195,17143,477],{"class":209},[195,17145,17146],{"class":197,"line":230},[195,17147,17148],{"class":415},"  \u003C!-- 静态属性 -->\n",[195,17150,17151,17153,17156,17159,17161,17164],{"class":197,"line":251},[195,17152,15658],{"class":209},[195,17154,17155],{"class":205},"img",[195,17157,17158],{"class":201}," src",[195,17160,424],{"class":209},[195,17162,17163],{"class":459},"\"\u002Flogo.png\"",[195,17165,17166],{"class":209}," \u002F>\n",[195,17168,17169],{"class":197,"line":272},[195,17170,1241],{"emptyLinePlaceholder":757},[195,17172,17173],{"class":197,"line":293},[195,17174,17175],{"class":415},"  \u003C!-- 动态绑定（v-bind 缩写为 :） -->\n",[195,17177,17178,17180,17182,17185,17187,17190],{"class":197,"line":562},[195,17179,15658],{"class":209},[195,17181,17155],{"class":205},[195,17183,17184],{"class":201}," :src",[195,17186,424],{"class":209},[195,17188,17189],{"class":459},"\"imageUrl\"",[195,17191,17166],{"class":209},[195,17193,17194],{"class":197,"line":583},[195,17195,1241],{"emptyLinePlaceholder":757},[195,17197,17198],{"class":197,"line":962},[195,17199,17200],{"class":415},"  \u003C!-- class 绑定 -->\n",[195,17202,17203,17205,17207,17210,17212,17215,17218,17220],{"class":197,"line":968},[195,17204,15658],{"class":209},[195,17206,451],{"class":205},[195,17208,17209],{"class":201}," :class",[195,17211,424],{"class":209},[195,17213,17214],{"class":459},"\"{ active: isActive, disabled: isDisabled }\"",[195,17216,17217],{"class":209},">\u003C\u002F",[195,17219,451],{"class":205},[195,17221,477],{"class":209},[195,17223,17224],{"class":197,"line":1274},[195,17225,1241],{"emptyLinePlaceholder":757},[195,17227,17228],{"class":197,"line":1282},[195,17229,17230],{"class":415},"  \u003C!-- style 绑定 -->\n",[195,17232,17233,17235,17237,17240,17242,17245,17247,17249],{"class":197,"line":1295},[195,17234,15658],{"class":209},[195,17236,451],{"class":205},[195,17238,17239],{"class":201}," :style",[195,17241,424],{"class":209},[195,17243,17244],{"class":459},"\"{ color: textColor, fontSize: size + 'px' }\"",[195,17246,17217],{"class":209},[195,17248,451],{"class":205},[195,17250,477],{"class":209},[195,17252,17253,17255,17257],{"class":197,"line":1309},[195,17254,15672],{"class":209},[195,17256,15651],{"class":205},[195,17258,477],{"class":209},[1890,17260],{},[18,17262,17264],{"id":17263},"_7-组件通信对比","7. 组件通信对比",[36,17266,17267,17278],{},[39,17268,17269],{},[42,17270,17271,17274,17276],{},[45,17272,17273],{},"场景",[45,17275,13135],{},[45,17277,15468],{},[52,17279,17280,17292,17309,17330],{},[42,17281,17282,17285,17288],{},[57,17283,17284],{},"父→子",[57,17286,17287],{},"构造函数参数",[57,17289,17290],{},[136,17291,15748],{},[42,17293,17294,17297,17303],{},[57,17295,17296],{},"子→父",[57,17298,17299,17300],{},"回调函数 ",[136,17301,17302],{},"onChanged",[57,17304,17305,17308],{},[136,17306,17307],{},"emit"," 事件",[42,17310,17311,17314,17322],{},[57,17312,17313],{},"跨层级",[57,17315,17316,16122,17319],{},[136,17317,17318],{},"InheritedWidget",[136,17320,17321],{},"Provider",[57,17323,17324,16122,17327],{},[136,17325,17326],{},"provide",[136,17328,17329],{},"inject",[42,17331,17332,17335,17338],{},[57,17333,17334],{},"全局状态",[57,17336,17337],{},"Riverpod \u002F Bloc",[57,17339,17340],{},"Pinia",[32,17342,17344],{"id":17343},"props-传递父子","Props 传递（父→子）",[14,17346,17347],{},[125,17348,13135],{},[186,17350,17352],{"className":11162,"code":17351,"language":11164,"meta":191,"style":191},"\u002F\u002F 父组件\nUserCard(name: userName, onTap: () => goToProfile())\n\n\u002F\u002F 子组件\nclass UserCard extends StatelessWidget {\n  final String name;\n  final VoidCallback onTap;\n  \u002F\u002F ...\n}\n",[136,17353,17354,17359,17364,17368,17373,17378,17382,17387,17392],{"__ignoreMap":191},[195,17355,17356],{"class":197,"line":198},[195,17357,17358],{},"\u002F\u002F 父组件\n",[195,17360,17361],{"class":197,"line":230},[195,17362,17363],{},"UserCard(name: userName, onTap: () => goToProfile())\n",[195,17365,17366],{"class":197,"line":251},[195,17367,1241],{"emptyLinePlaceholder":757},[195,17369,17370],{"class":197,"line":272},[195,17371,17372],{},"\u002F\u002F 子组件\n",[195,17374,17375],{"class":197,"line":293},[195,17376,17377],{},"class UserCard extends StatelessWidget {\n",[195,17379,17380],{"class":197,"line":562},[195,17381,15601],{},[195,17383,17384],{"class":197,"line":583},[195,17385,17386],{},"  final VoidCallback onTap;\n",[195,17388,17389],{"class":197,"line":962},[195,17390,17391],{},"  \u002F\u002F ...\n",[195,17393,17394],{"class":197,"line":968},[195,17395,552],{},[14,17397,17398],{},[125,17399,15468],{},[186,17401,17403],{"className":15640,"code":17402,"language":15642,"meta":191,"style":191},"\u003C!-- 父组件 -->\n\u003CUserCard :name=\"userName\" @tap=\"goToProfile\" \u002F>\n\n\u003C!-- 子组件 UserCard.vue -->\n\u003Cscript setup lang=\"ts\">\ndefineProps\u003C{ name: string }>()\nconst emit = defineEmits\u003C{ tap: [] }>()\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv @click=\"emit('tap')\">{{ name }}\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n",[136,17404,17405,17410,17449,17453,17458,17474,17488,17509,17517,17521,17529,17549],{"__ignoreMap":191},[195,17406,17407],{"class":197,"line":198},[195,17408,17409],{"class":415},"\u003C!-- 父组件 -->\n",[195,17411,17412,17414,17417,17420,17422,17424,17427,17430,17432,17435,17438,17440,17442,17445,17447],{"class":197,"line":230},[195,17413,448],{"class":209},[195,17415,17416],{"class":205},"UserCard",[195,17418,17419],{"class":209}," :",[195,17421,13388],{"class":201},[195,17423,424],{"class":209},[195,17425,17426],{"class":459},"\"",[195,17428,17429],{"class":209},"userName",[195,17431,17426],{"class":459},[195,17433,17434],{"class":209}," @",[195,17436,17437],{"class":201},"tap",[195,17439,424],{"class":209},[195,17441,17426],{"class":459},[195,17443,17444],{"class":209},"goToProfile",[195,17446,17426],{"class":459},[195,17448,17166],{"class":209},[195,17450,17451],{"class":197,"line":251},[195,17452,1241],{"emptyLinePlaceholder":757},[195,17454,17455],{"class":197,"line":272},[195,17456,17457],{"class":415},"\u003C!-- 子组件 UserCard.vue -->\n",[195,17459,17460,17462,17464,17466,17468,17470,17472],{"class":197,"line":293},[195,17461,448],{"class":209},[195,17463,15687],{"class":205},[195,17465,15690],{"class":201},[195,17467,15693],{"class":201},[195,17469,424],{"class":209},[195,17471,15698],{"class":459},[195,17473,477],{"class":209},[195,17475,17476,17478,17480,17482,17484,17486],{"class":197,"line":562},[195,17477,15705],{"class":201},[195,17479,15708],{"class":209},[195,17481,13388],{"class":634},[195,17483,638],{"class":223},[195,17485,15715],{"class":213},[195,17487,15718],{"class":209},[195,17489,17490,17492,17495,17497,17500,17502,17504,17506],{"class":197,"line":583},[195,17491,392],{"class":223},[195,17493,17494],{"class":213}," emit",[195,17496,398],{"class":223},[195,17498,17499],{"class":201}," defineEmits",[195,17501,15708],{"class":209},[195,17503,17437],{"class":634},[195,17505,638],{"class":223},[195,17507,17508],{"class":209}," [] }>()\n",[195,17510,17511,17513,17515],{"class":197,"line":962},[195,17512,15672],{"class":209},[195,17514,15687],{"class":205},[195,17516,477],{"class":209},[195,17518,17519],{"class":197,"line":968},[195,17520,1241],{"emptyLinePlaceholder":757},[195,17522,17523,17525,17527],{"class":197,"line":1274},[195,17524,448],{"class":209},[195,17526,15651],{"class":205},[195,17528,477],{"class":209},[195,17530,17531,17533,17535,17537,17539,17542,17545,17547],{"class":197,"line":1282},[195,17532,15658],{"class":209},[195,17534,451],{"class":205},[195,17536,15907],{"class":201},[195,17538,424],{"class":209},[195,17540,17541],{"class":459},"\"emit('tap')\"",[195,17543,17544],{"class":209},">{{ name }}\u003C\u002F",[195,17546,451],{"class":205},[195,17548,477],{"class":209},[195,17550,17551,17553,17555],{"class":197,"line":1295},[195,17552,15672],{"class":209},[195,17554,15651],{"class":205},[195,17556,477],{"class":209},[32,17558,17560],{"id":17559},"插槽-child-builder","插槽 = child \u002F builder",[14,17562,17563],{},[125,17564,13135],{},[186,17566,17568],{"className":11162,"code":17567,"language":11164,"meta":191,"style":191},"Card(child: Text('内容'))\n\n\u002F\u002F builder 模式\nMyWidget(builder: (context) => Text('动态内容'))\n",[136,17569,17570,17575,17579,17584],{"__ignoreMap":191},[195,17571,17572],{"class":197,"line":198},[195,17573,17574],{},"Card(child: Text('内容'))\n",[195,17576,17577],{"class":197,"line":230},[195,17578,1241],{"emptyLinePlaceholder":757},[195,17580,17581],{"class":197,"line":251},[195,17582,17583],{},"\u002F\u002F builder 模式\n",[195,17585,17586],{"class":197,"line":272},[195,17587,17588],{},"MyWidget(builder: (context) => Text('动态内容'))\n",[14,17590,17591],{},[125,17592,15468],{},[186,17594,17596],{"className":15640,"code":17595,"language":15642,"meta":191,"style":191},"\u003C!-- 默认插槽 ≈ child -->\n\u003CCard>\n  \u003Cp>内容\u003C\u002Fp>\n\u003C\u002FCard>\n\n\u003C!-- 作用域插槽 ≈ builder -->\n\u003CMyList :items=\"items\">\n  \u003Ctemplate #default=\"{ item }\">\n    \u003Cspan>{{ item.name }}\u003C\u002Fspan>\n  \u003C\u002Ftemplate>\n\u003C\u002FMyList>\n",[136,17597,17598,17603,17612,17617,17625,17629,17634,17656,17661,17666,17671],{"__ignoreMap":191},[195,17599,17600],{"class":197,"line":198},[195,17601,17602],{"class":415},"\u003C!-- 默认插槽 ≈ child -->\n",[195,17604,17605,17607,17610],{"class":197,"line":230},[195,17606,448],{"class":209},[195,17608,17609],{"class":205},"Card",[195,17611,477],{"class":209},[195,17613,17614],{"class":197,"line":251},[195,17615,17616],{"class":209},"  \u003Cp>内容\u003C\u002Fp>\n",[195,17618,17619,17621,17623],{"class":197,"line":272},[195,17620,15672],{"class":209},[195,17622,17609],{"class":205},[195,17624,477],{"class":209},[195,17626,17627],{"class":197,"line":293},[195,17628,1241],{"emptyLinePlaceholder":757},[195,17630,17631],{"class":197,"line":562},[195,17632,17633],{"class":415},"\u003C!-- 作用域插槽 ≈ builder -->\n",[195,17635,17636,17638,17641,17643,17646,17648,17650,17652,17654],{"class":197,"line":583},[195,17637,448],{"class":209},[195,17639,17640],{"class":205},"MyList",[195,17642,17419],{"class":209},[195,17644,17645],{"class":201},"items",[195,17647,424],{"class":209},[195,17649,17426],{"class":459},[195,17651,17645],{"class":209},[195,17653,17426],{"class":459},[195,17655,477],{"class":209},[195,17657,17658],{"class":197,"line":962},[195,17659,17660],{"class":209},"  \u003Ctemplate #default=\"{ item }\">\n",[195,17662,17663],{"class":197,"line":968},[195,17664,17665],{"class":209},"    \u003Cspan>{{ item.name }}\u003C\u002Fspan>\n",[195,17667,17668],{"class":197,"line":1274},[195,17669,17670],{"class":209},"  \u003C\u002Ftemplate>\n",[195,17672,17673,17675,17677],{"class":197,"line":1282},[195,17674,15672],{"class":209},[195,17676,17640],{"class":205},[195,17678,477],{"class":209},[1890,17680],{},[18,17682,17684],{"id":17683},"_8-路由对比","8. 路由对比",[14,17686,17687],{},[125,17688,17689],{},"Flutter — GoRouter",[186,17691,17693],{"className":11162,"code":17692,"language":11164,"meta":191,"style":191},"GoRouter(routes: [\n  GoRoute(path: '\u002F', builder: (ctx, state) => HomePage()),\n  GoRoute(path: '\u002Fuser\u002F:id', builder: (ctx, state) {\n    final id = state.pathParameters['id']!;\n    return UserPage(id: id);\n  }),\n])\n\n\u002F\u002F 导航\ncontext.go('\u002Fuser\u002F42');\n",[136,17694,17695,17700,17705,17710,17715,17720,17725,17729,17733,17738],{"__ignoreMap":191},[195,17696,17697],{"class":197,"line":198},[195,17698,17699],{},"GoRouter(routes: [\n",[195,17701,17702],{"class":197,"line":230},[195,17703,17704],{},"  GoRoute(path: '\u002F', builder: (ctx, state) => HomePage()),\n",[195,17706,17707],{"class":197,"line":251},[195,17708,17709],{},"  GoRoute(path: '\u002Fuser\u002F:id', builder: (ctx, state) {\n",[195,17711,17712],{"class":197,"line":272},[195,17713,17714],{},"    final id = state.pathParameters['id']!;\n",[195,17716,17717],{"class":197,"line":293},[195,17718,17719],{},"    return UserPage(id: id);\n",[195,17721,17722],{"class":197,"line":562},[195,17723,17724],{},"  }),\n",[195,17726,17727],{"class":197,"line":583},[195,17728,16851],{},[195,17730,17731],{"class":197,"line":962},[195,17732,1241],{"emptyLinePlaceholder":757},[195,17734,17735],{"class":197,"line":968},[195,17736,17737],{},"\u002F\u002F 导航\n",[195,17739,17740],{"class":197,"line":1274},[195,17741,17742],{},"context.go('\u002Fuser\u002F42');\n",[14,17744,17745],{},[125,17746,17747],{},"Vue — Vue Router",[186,17749,17753],{"className":17750,"code":17751,"language":17752,"meta":191,"style":191},"language-ts shiki shiki-themes github-light github-dark","\u002F\u002F router\u002Findex.ts\nimport { createRouter, createWebHistory } from 'vue-router'\n\nconst router = createRouter({\n  history: createWebHistory(),\n  routes: [\n    { path: '\u002F', component: () => import('@\u002Fviews\u002FHome.vue') },\n    { path: '\u002Fuser\u002F:id', component: () => import('@\u002Fviews\u002FUser.vue') },\n  ],\n})\n","ts",[136,17754,17755,17760,17772,17776,17791,17802,17807,17836,17860,17865],{"__ignoreMap":191},[195,17756,17757],{"class":197,"line":198},[195,17758,17759],{"class":415},"\u002F\u002F router\u002Findex.ts\n",[195,17761,17762,17764,17767,17769],{"class":197,"line":230},[195,17763,15961],{"class":223},[195,17765,17766],{"class":209}," { createRouter, createWebHistory } ",[195,17768,15967],{"class":223},[195,17770,17771],{"class":459}," 'vue-router'\n",[195,17773,17774],{"class":197,"line":251},[195,17775,1241],{"emptyLinePlaceholder":757},[195,17777,17778,17780,17783,17785,17788],{"class":197,"line":272},[195,17779,392],{"class":223},[195,17781,17782],{"class":213}," router",[195,17784,398],{"class":223},[195,17786,17787],{"class":201}," createRouter",[195,17789,17790],{"class":209},"({\n",[195,17792,17793,17796,17799],{"class":197,"line":293},[195,17794,17795],{"class":209},"  history: ",[195,17797,17798],{"class":201},"createWebHistory",[195,17800,17801],{"class":209},"(),\n",[195,17803,17804],{"class":197,"line":562},[195,17805,17806],{"class":209},"  routes: [\n",[195,17808,17809,17812,17815,17817,17820,17823,17825,17828,17830,17833],{"class":197,"line":583},[195,17810,17811],{"class":209},"    { path: ",[195,17813,17814],{"class":459},"'\u002F'",[195,17816,301],{"class":209},[195,17818,17819],{"class":201},"component",[195,17821,17822],{"class":209},": () ",[195,17824,16398],{"class":223},[195,17826,17827],{"class":223}," import",[195,17829,404],{"class":209},[195,17831,17832],{"class":459},"'@\u002Fviews\u002FHome.vue'",[195,17834,17835],{"class":209},") },\n",[195,17837,17838,17840,17843,17845,17847,17849,17851,17853,17855,17858],{"class":197,"line":962},[195,17839,17811],{"class":209},[195,17841,17842],{"class":459},"'\u002Fuser\u002F:id'",[195,17844,301],{"class":209},[195,17846,17819],{"class":201},[195,17848,17822],{"class":209},[195,17850,16398],{"class":223},[195,17852,17827],{"class":223},[195,17854,404],{"class":209},[195,17856,17857],{"class":459},"'@\u002Fviews\u002FUser.vue'",[195,17859,17835],{"class":209},[195,17861,17862],{"class":197,"line":968},[195,17863,17864],{"class":209},"  ],\n",[195,17866,17867],{"class":197,"line":1274},[195,17868,16566],{"class":209},[186,17870,17872],{"className":15640,"code":17871,"language":15642,"meta":191,"style":191},"\u003C!-- 使用路由 -->\n\u003Ctemplate>\n  \u003Crouter-link to=\"\u002Fuser\u002F42\">Go to User\u003C\u002Frouter-link>\n  \u003Crouter-view \u002F>  \u003C!-- ≈ Navigator 的显示区域 -->\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nimport { useRoute, useRouter } from 'vue-router'\n\nconst route = useRoute()     \u002F\u002F 读取当前路由信息\nconst router = useRouter()   \u002F\u002F 编程式导航\n\nconsole.log(route.params.id) \u002F\u002F '42'\nrouter.push('\u002Fuser\u002F42')      \u002F\u002F ≈ context.go()\n\u003C\u002Fscript>\n",[136,17873,17874,17879,17887,17909,17922,17930,17934,17950,17961,17965,17983,18000,18004,18017,18036],{"__ignoreMap":191},[195,17875,17876],{"class":197,"line":198},[195,17877,17878],{"class":415},"\u003C!-- 使用路由 -->\n",[195,17880,17881,17883,17885],{"class":197,"line":230},[195,17882,448],{"class":209},[195,17884,15651],{"class":205},[195,17886,477],{"class":209},[195,17888,17889,17891,17894,17897,17899,17902,17905,17907],{"class":197,"line":251},[195,17890,15658],{"class":209},[195,17892,17893],{"class":205},"router-link",[195,17895,17896],{"class":201}," to",[195,17898,424],{"class":209},[195,17900,17901],{"class":459},"\"\u002Fuser\u002F42\"",[195,17903,17904],{"class":209},">Go to User\u003C\u002F",[195,17906,17893],{"class":205},[195,17908,477],{"class":209},[195,17910,17911,17913,17916,17919],{"class":197,"line":272},[195,17912,15658],{"class":209},[195,17914,17915],{"class":205},"router-view",[195,17917,17918],{"class":209}," \u002F>  ",[195,17920,17921],{"class":415},"\u003C!-- ≈ Navigator 的显示区域 -->\n",[195,17923,17924,17926,17928],{"class":197,"line":293},[195,17925,15672],{"class":209},[195,17927,15651],{"class":205},[195,17929,477],{"class":209},[195,17931,17932],{"class":197,"line":562},[195,17933,1241],{"emptyLinePlaceholder":757},[195,17935,17936,17938,17940,17942,17944,17946,17948],{"class":197,"line":583},[195,17937,448],{"class":209},[195,17939,15687],{"class":205},[195,17941,15690],{"class":201},[195,17943,15693],{"class":201},[195,17945,424],{"class":209},[195,17947,15698],{"class":459},[195,17949,477],{"class":209},[195,17951,17952,17954,17957,17959],{"class":197,"line":962},[195,17953,15961],{"class":223},[195,17955,17956],{"class":209}," { useRoute, useRouter } ",[195,17958,15967],{"class":223},[195,17960,17771],{"class":459},[195,17962,17963],{"class":197,"line":968},[195,17964,1241],{"emptyLinePlaceholder":757},[195,17966,17967,17969,17972,17974,17977,17980],{"class":197,"line":1274},[195,17968,392],{"class":223},[195,17970,17971],{"class":213}," route",[195,17973,398],{"class":223},[195,17975,17976],{"class":201}," useRoute",[195,17978,17979],{"class":209},"()     ",[195,17981,17982],{"class":415},"\u002F\u002F 读取当前路由信息\n",[195,17984,17985,17987,17989,17991,17994,17997],{"class":197,"line":1282},[195,17986,392],{"class":223},[195,17988,17782],{"class":213},[195,17990,398],{"class":223},[195,17992,17993],{"class":201}," useRouter",[195,17995,17996],{"class":209},"()   ",[195,17998,17999],{"class":415},"\u002F\u002F 编程式导航\n",[195,18001,18002],{"class":197,"line":1295},[195,18003,1241],{"emptyLinePlaceholder":757},[195,18005,18006,18009,18011,18014],{"class":197,"line":1309},[195,18007,18008],{"class":209},"console.",[195,18010,16740],{"class":201},[195,18012,18013],{"class":209},"(route.params.id) ",[195,18015,18016],{"class":415},"\u002F\u002F '42'\n",[195,18018,18019,18022,18025,18027,18030,18033],{"class":197,"line":2246},[195,18020,18021],{"class":209},"router.",[195,18023,18024],{"class":201},"push",[195,18026,404],{"class":209},[195,18028,18029],{"class":459},"'\u002Fuser\u002F42'",[195,18031,18032],{"class":209},")      ",[195,18034,18035],{"class":415},"\u002F\u002F ≈ context.go()\n",[195,18037,18038,18040,18042],{"class":197,"line":1996},[195,18039,15672],{"class":209},[195,18041,15687],{"class":205},[195,18043,477],{"class":209},[32,18045,18047],{"id":18046},"路由守卫-navigator-observer","路由守卫 ≈ Navigator Observer",[186,18049,18051],{"className":17750,"code":18050,"language":17752,"meta":191,"style":191},"router.beforeEach((to, from) => {\n  if (to.meta.requiresAuth && !isLoggedIn()) {\n    return '\u002Flogin'  \u002F\u002F 重定向\n  }\n})\n",[136,18052,18053,18076,18096,18107,18111],{"__ignoreMap":191},[195,18054,18055,18057,18060,18063,18066,18068,18070,18072,18074],{"class":197,"line":198},[195,18056,18021],{"class":209},[195,18058,18059],{"class":201},"beforeEach",[195,18061,18062],{"class":209},"((",[195,18064,18065],{"class":634},"to",[195,18067,301],{"class":209},[195,18069,15967],{"class":634},[195,18071,516],{"class":209},[195,18073,16398],{"class":223},[195,18075,496],{"class":209},[195,18077,18078,18081,18084,18087,18090,18093],{"class":197,"line":230},[195,18079,18080],{"class":223},"  if",[195,18082,18083],{"class":209}," (to.meta.requiresAuth ",[195,18085,18086],{"class":223},"&&",[195,18088,18089],{"class":223}," !",[195,18091,18092],{"class":201},"isLoggedIn",[195,18094,18095],{"class":209},"()) {\n",[195,18097,18098,18101,18104],{"class":197,"line":251},[195,18099,18100],{"class":223},"    return",[195,18102,18103],{"class":459}," '\u002Flogin'",[195,18105,18106],{"class":415},"  \u002F\u002F 重定向\n",[195,18108,18109],{"class":197,"line":272},[195,18110,965],{"class":209},[195,18112,18113],{"class":197,"line":293},[195,18114,16566],{"class":209},[1890,18116],{},[18,18118,18120],{"id":18119},"_9-状态管理对比","9. 状态管理对比",[32,18122,18124],{"id":18123},"pinia-riverpod-provider","Pinia ≈ Riverpod \u002F Provider",[14,18126,18127],{},[125,18128,18129],{},"Flutter — Riverpod",[186,18131,18133],{"className":11162,"code":18132,"language":11164,"meta":191,"style":191},"final counterProvider = StateNotifierProvider\u003CCounterNotifier, int>(\n  (ref) => CounterNotifier(),\n);\n\nclass CounterNotifier extends StateNotifier\u003Cint> {\n  CounterNotifier() : super(0);\n  void increment() => state++;\n}\n\n\u002F\u002F 使用\nfinal count = ref.watch(counterProvider);\nref.read(counterProvider.notifier).increment();\n",[136,18134,18135,18140,18145,18149,18153,18158,18163,18168,18172,18176,18180,18185],{"__ignoreMap":191},[195,18136,18137],{"class":197,"line":198},[195,18138,18139],{},"final counterProvider = StateNotifierProvider\u003CCounterNotifier, int>(\n",[195,18141,18142],{"class":197,"line":230},[195,18143,18144],{},"  (ref) => CounterNotifier(),\n",[195,18146,18147],{"class":197,"line":251},[195,18148,527],{},[195,18150,18151],{"class":197,"line":272},[195,18152,1241],{"emptyLinePlaceholder":757},[195,18154,18155],{"class":197,"line":293},[195,18156,18157],{},"class CounterNotifier extends StateNotifier\u003Cint> {\n",[195,18159,18160],{"class":197,"line":562},[195,18161,18162],{},"  CounterNotifier() : super(0);\n",[195,18164,18165],{"class":197,"line":583},[195,18166,18167],{},"  void increment() => state++;\n",[195,18169,18170],{"class":197,"line":962},[195,18171,552],{},[195,18173,18174],{"class":197,"line":968},[195,18175,1241],{"emptyLinePlaceholder":757},[195,18177,18178],{"class":197,"line":1274},[195,18179,3056],{},[195,18181,18182],{"class":197,"line":1282},[195,18183,18184],{},"final count = ref.watch(counterProvider);\n",[195,18186,18187],{"class":197,"line":1295},[195,18188,18189],{},"ref.read(counterProvider.notifier).increment();\n",[14,18191,18192],{},[125,18193,18194],{},"Vue — Pinia",[186,18196,18198],{"className":17750,"code":18197,"language":17752,"meta":191,"style":191},"\u002F\u002F stores\u002Fcounter.ts\nimport { defineStore } from 'pinia'\nimport { ref } from 'vue'\n\nexport const useCounterStore = defineStore('counter', () => {\n  const count = ref(0)\n  function increment() { count.value++ }\n  return { count, increment }\n})\n",[136,18199,18200,18205,18217,18227,18231,18259,18276,18292,18300],{"__ignoreMap":191},[195,18201,18202],{"class":197,"line":198},[195,18203,18204],{"class":415},"\u002F\u002F stores\u002Fcounter.ts\n",[195,18206,18207,18209,18212,18214],{"class":197,"line":230},[195,18208,15961],{"class":223},[195,18210,18211],{"class":209}," { defineStore } ",[195,18213,15967],{"class":223},[195,18215,18216],{"class":459}," 'pinia'\n",[195,18218,18219,18221,18223,18225],{"class":197,"line":251},[195,18220,15961],{"class":223},[195,18222,15964],{"class":209},[195,18224,15967],{"class":223},[195,18226,15970],{"class":459},[195,18228,18229],{"class":197,"line":272},[195,18230,1241],{"emptyLinePlaceholder":757},[195,18232,18233,18236,18239,18242,18244,18247,18249,18252,18255,18257],{"class":197,"line":293},[195,18234,18235],{"class":223},"export",[195,18237,18238],{"class":223}," const",[195,18240,18241],{"class":213}," useCounterStore",[195,18243,398],{"class":223},[195,18245,18246],{"class":201}," defineStore",[195,18248,404],{"class":209},[195,18250,18251],{"class":459},"'counter'",[195,18253,18254],{"class":209},", () ",[195,18256,16398],{"class":223},[195,18258,496],{"class":209},[195,18260,18261,18264,18266,18268,18270,18272,18274],{"class":197,"line":562},[195,18262,18263],{"class":223},"  const",[195,18265,15981],{"class":213},[195,18267,398],{"class":223},[195,18269,401],{"class":201},[195,18271,404],{"class":209},[195,18273,407],{"class":213},[195,18275,410],{"class":209},[195,18277,18278,18281,18284,18287,18289],{"class":197,"line":583},[195,18279,18280],{"class":223},"  function",[195,18282,18283],{"class":201}," increment",[195,18285,18286],{"class":209},"() { count.value",[195,18288,16216],{"class":223},[195,18290,18291],{"class":209}," }\n",[195,18293,18294,18297],{"class":197,"line":962},[195,18295,18296],{"class":223},"  return",[195,18298,18299],{"class":209}," { count, increment }\n",[195,18301,18302],{"class":197,"line":968},[195,18303,16566],{"class":209},[186,18305,18307],{"className":15640,"code":18306,"language":15642,"meta":191,"style":191},"\u003C!-- 使用 -->\n\u003Cscript setup lang=\"ts\">\nimport { useCounterStore } from '@\u002Fstores\u002Fcounter'\n\nconst counter = useCounterStore()\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cp>{{ counter.count }}\u003C\u002Fp>\n  \u003Cbutton @click=\"counter.increment()\">+1\u003C\u002Fbutton>\n\u003C\u002Ftemplate>\n",[136,18308,18309,18314,18330,18342,18346,18360,18368,18372,18380,18393,18413],{"__ignoreMap":191},[195,18310,18311],{"class":197,"line":198},[195,18312,18313],{"class":415},"\u003C!-- 使用 -->\n",[195,18315,18316,18318,18320,18322,18324,18326,18328],{"class":197,"line":230},[195,18317,448],{"class":209},[195,18319,15687],{"class":205},[195,18321,15690],{"class":201},[195,18323,15693],{"class":201},[195,18325,424],{"class":209},[195,18327,15698],{"class":459},[195,18329,477],{"class":209},[195,18331,18332,18334,18337,18339],{"class":197,"line":251},[195,18333,15961],{"class":223},[195,18335,18336],{"class":209}," { useCounterStore } ",[195,18338,15967],{"class":223},[195,18340,18341],{"class":459}," '@\u002Fstores\u002Fcounter'\n",[195,18343,18344],{"class":197,"line":272},[195,18345,1241],{"emptyLinePlaceholder":757},[195,18347,18348,18350,18353,18355,18357],{"class":197,"line":293},[195,18349,392],{"class":223},[195,18351,18352],{"class":213}," counter",[195,18354,398],{"class":223},[195,18356,18241],{"class":201},[195,18358,18359],{"class":209},"()\n",[195,18361,18362,18364,18366],{"class":197,"line":562},[195,18363,15672],{"class":209},[195,18365,15687],{"class":205},[195,18367,477],{"class":209},[195,18369,18370],{"class":197,"line":583},[195,18371,1241],{"emptyLinePlaceholder":757},[195,18373,18374,18376,18378],{"class":197,"line":962},[195,18375,448],{"class":209},[195,18377,15651],{"class":205},[195,18379,477],{"class":209},[195,18381,18382,18384,18386,18389,18391],{"class":197,"line":968},[195,18383,15658],{"class":209},[195,18385,14],{"class":205},[195,18387,18388],{"class":209},">{{ counter.count }}\u003C\u002F",[195,18390,14],{"class":205},[195,18392,477],{"class":209},[195,18394,18395,18397,18399,18401,18403,18406,18409,18411],{"class":197,"line":1274},[195,18396,15658],{"class":209},[195,18398,15904],{"class":205},[195,18400,15907],{"class":201},[195,18402,424],{"class":209},[195,18404,18405],{"class":459},"\"counter.increment()\"",[195,18407,18408],{"class":209},">+1\u003C\u002F",[195,18410,15904],{"class":205},[195,18412,477],{"class":209},[195,18414,18415,18417,18419],{"class":197,"line":1282},[195,18416,15672],{"class":209},[195,18418,15651],{"class":205},[195,18420,477],{"class":209},[1890,18422],{},[18,18424,18426],{"id":18425},"_10-样式对比","10. 样式对比",[14,18428,18429,18431],{},[125,18430,13135],{}," — 所有样式通过 Widget 属性内联",[186,18433,18435],{"className":11162,"code":18434,"language":11164,"meta":191,"style":191},"Container(\n  padding: EdgeInsets.all(16),\n  decoration: BoxDecoration(\n    color: Colors.blue,\n    borderRadius: BorderRadius.circular(8),\n  ),\n  child: Text('Hello', style: TextStyle(fontSize: 18, color: Colors.white)),\n)\n",[136,18436,18437,18442,18447,18452,18457,18462,18466,18471],{"__ignoreMap":191},[195,18438,18439],{"class":197,"line":198},[195,18440,18441],{},"Container(\n",[195,18443,18444],{"class":197,"line":230},[195,18445,18446],{},"  padding: EdgeInsets.all(16),\n",[195,18448,18449],{"class":197,"line":251},[195,18450,18451],{},"  decoration: BoxDecoration(\n",[195,18453,18454],{"class":197,"line":272},[195,18455,18456],{},"    color: Colors.blue,\n",[195,18458,18459],{"class":197,"line":293},[195,18460,18461],{},"    borderRadius: BorderRadius.circular(8),\n",[195,18463,18464],{"class":197,"line":562},[195,18465,11676],{},[195,18467,18468],{"class":197,"line":583},[195,18469,18470],{},"  child: Text('Hello', style: TextStyle(fontSize: 18, color: Colors.white)),\n",[195,18472,18473],{"class":197,"line":962},[195,18474,410],{},[14,18476,18477,18479],{},[125,18478,15468],{}," — CSS (Scoped)",[186,18481,18483],{"className":15640,"code":18482,"language":15642,"meta":191,"style":191},"\u003Ctemplate>\n  \u003Cdiv class=\"card\">\n    \u003Cp class=\"card-text\">Hello\u003C\u002Fp>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cstyle scoped>\n.card {\n  padding: 16px;\n  background-color: blue;\n  border-radius: 8px;\n}\n.card-text {\n  font-size: 18px;\n  color: white;\n}\n\u003C\u002Fstyle>\n",[136,18484,18485,18493,18508,18528,18536,18544,18548,18559,18566,18580,18592,18606,18610,18617,18631,18643,18647],{"__ignoreMap":191},[195,18486,18487,18489,18491],{"class":197,"line":198},[195,18488,448],{"class":209},[195,18490,15651],{"class":205},[195,18492,477],{"class":209},[195,18494,18495,18497,18499,18501,18503,18506],{"class":197,"line":230},[195,18496,15658],{"class":209},[195,18498,451],{"class":205},[195,18500,454],{"class":201},[195,18502,424],{"class":209},[195,18504,18505],{"class":459},"\"card\"",[195,18507,477],{"class":209},[195,18509,18510,18512,18514,18516,18518,18521,18524,18526],{"class":197,"line":251},[195,18511,15888],{"class":209},[195,18513,14],{"class":205},[195,18515,454],{"class":201},[195,18517,424],{"class":209},[195,18519,18520],{"class":459},"\"card-text\"",[195,18522,18523],{"class":209},">Hello\u003C\u002F",[195,18525,14],{"class":205},[195,18527,477],{"class":209},[195,18529,18530,18532,18534],{"class":197,"line":272},[195,18531,15924],{"class":209},[195,18533,451],{"class":205},[195,18535,477],{"class":209},[195,18537,18538,18540,18542],{"class":197,"line":293},[195,18539,15672],{"class":209},[195,18541,15651],{"class":205},[195,18543,477],{"class":209},[195,18545,18546],{"class":197,"line":562},[195,18547,1241],{"emptyLinePlaceholder":757},[195,18549,18550,18552,18554,18557],{"class":197,"line":583},[195,18551,448],{"class":209},[195,18553,733],{"class":205},[195,18555,18556],{"class":201}," scoped",[195,18558,477],{"class":209},[195,18560,18561,18564],{"class":197,"line":962},[195,18562,18563],{"class":201},".card",[195,18565,496],{"class":209},[195,18567,18568,18571,18573,18576,18578],{"class":197,"line":968},[195,18569,18570],{"class":213},"  padding",[195,18572,217],{"class":209},[195,18574,18575],{"class":213},"16",[195,18577,224],{"class":223},[195,18579,547],{"class":209},[195,18581,18582,18585,18587,18590],{"class":197,"line":1274},[195,18583,18584],{"class":213},"  background-color",[195,18586,217],{"class":209},[195,18588,18589],{"class":213},"blue",[195,18591,547],{"class":209},[195,18593,18594,18597,18599,18602,18604],{"class":197,"line":1282},[195,18595,18596],{"class":213},"  border-radius",[195,18598,217],{"class":209},[195,18600,18601],{"class":213},"8",[195,18603,224],{"class":223},[195,18605,547],{"class":209},[195,18607,18608],{"class":197,"line":1295},[195,18609,552],{"class":209},[195,18611,18612,18615],{"class":197,"line":1309},[195,18613,18614],{"class":201},".card-text",[195,18616,496],{"class":209},[195,18618,18619,18622,18624,18627,18629],{"class":197,"line":2246},[195,18620,18621],{"class":213},"  font-size",[195,18623,217],{"class":209},[195,18625,18626],{"class":213},"18",[195,18628,224],{"class":223},[195,18630,547],{"class":209},[195,18632,18633,18636,18638,18641],{"class":197,"line":1996},[195,18634,18635],{"class":213},"  color",[195,18637,217],{"class":209},[195,18639,18640],{"class":213},"white",[195,18642,547],{"class":209},[195,18644,18645],{"class":197,"line":2257},[195,18646,552],{"class":209},[195,18648,18649,18651,18653],{"class":197,"line":2262},[195,18650,15672],{"class":209},[195,18652,733],{"class":205},[195,18654,477],{"class":209},[32,18656,18658],{"id":18657},"常用-css-布局-flutter-布局-widget","常用 CSS 布局 ≈ Flutter 布局 Widget",[36,18660,18661,18672],{},[39,18662,18663],{},[42,18664,18665,18667,18670],{},[45,18666,13135],{},[45,18668,18669],{},"CSS",[45,18671,13780],{},[52,18673,18674,18689,18704,18722,18736,18751,18766,18781,18795,18810],{},[42,18675,18676,18681,18686],{},[57,18677,18678],{},[136,18679,18680],{},"Column",[57,18682,18683],{},[136,18684,18685],{},"display: flex; flex-direction: column",[57,18687,18688],{},"纵向排列",[42,18690,18691,18696,18701],{},[57,18692,18693],{},[136,18694,18695],{},"Row",[57,18697,18698],{},[136,18699,18700],{},"display: flex; flex-direction: row",[57,18702,18703],{},"横向排列",[42,18705,18706,18711,18719],{},[57,18707,18708],{},[136,18709,18710],{},"Stack",[57,18712,18713,4728,18716],{},[136,18714,18715],{},"position: relative",[136,18717,18718],{},"position: absolute",[57,18720,18721],{},"层叠",[42,18723,18724,18729,18733],{},[57,18725,18726],{},[136,18727,18728],{},"Expanded(flex: 1)",[57,18730,18731],{},[136,18732,1436],{},[57,18734,18735],{},"弹性占比",[42,18737,18738,18743,18748],{},[57,18739,18740],{},[136,18741,18742],{},"SizedBox(width: 100)",[57,18744,18745],{},[136,18746,18747],{},"width: 100px",[57,18749,18750],{},"固定尺寸",[42,18752,18753,18758,18763],{},[57,18754,18755],{},[136,18756,18757],{},"Padding",[57,18759,18760],{},[136,18761,18762],{},"padding: 16px",[57,18764,18765],{},"内边距",[42,18767,18768,18773,18778],{},[57,18769,18770],{},[136,18771,18772],{},"Center",[57,18774,18775],{},[136,18776,18777],{},"display: flex; justify-content: center; align-items: center",[57,18779,18780],{},"居中",[42,18782,18783,18788,18792],{},[57,18784,18785],{},[136,18786,18787],{},"ListView",[57,18789,18790],{},[136,18791,1166],{},[57,18793,18794],{},"滚动列表",[42,18796,18797,18802,18807],{},[57,18798,18799],{},[136,18800,18801],{},"GridView",[57,18803,18804],{},[136,18805,18806],{},"display: grid; grid-template-columns: ...",[57,18808,18809],{},"网格",[42,18811,18812,18817,18822],{},[57,18813,18814],{},[136,18815,18816],{},"Wrap",[57,18818,18819],{},[136,18820,18821],{},"display: flex; flex-wrap: wrap",[57,18823,18824],{},"自动换行",[1890,18826],{},[18,18828,18830],{"id":18829},"_11-网络请求对比","11. 网络请求对比",[14,18832,18833],{},[125,18834,18835],{},"Flutter — dio",[186,18837,18839],{"className":11162,"code":18838,"language":11164,"meta":191,"style":191},"final dio = Dio();\nfinal response = await dio.get('\u002Fapi\u002Fusers');\nfinal users = response.data;\n",[136,18840,18841,18846,18851],{"__ignoreMap":191},[195,18842,18843],{"class":197,"line":198},[195,18844,18845],{},"final dio = Dio();\n",[195,18847,18848],{"class":197,"line":230},[195,18849,18850],{},"final response = await dio.get('\u002Fapi\u002Fusers');\n",[195,18852,18853],{"class":197,"line":251},[195,18854,18855],{},"final users = response.data;\n",[14,18857,18858],{},[125,18859,18860],{},"Vue — axios \u002F fetch",[186,18862,18864],{"className":17750,"code":18863,"language":17752,"meta":191,"style":191},"import axios from 'axios'\n\n\u002F\u002F 在 composable 中封装（≈ Flutter 的 Repository）\nexport function useUsers() {\n  const users = ref([])\n  const loading = ref(false)\n\n  async function fetchUsers() {\n    loading.value = true\n    try {\n      const { data } = await axios.get('\u002Fapi\u002Fusers')\n      users.value = data\n    } finally {\n      loading.value = false\n    }\n  }\n\n  onMounted(fetchUsers)\n  return { users, loading }\n}\n",[136,18865,18866,18878,18882,18887,18900,18914,18932,18936,18948,18958,18965,18996,19006,19016,19026,19030,19034,19038,19046,19053],{"__ignoreMap":191},[195,18867,18868,18870,18873,18875],{"class":197,"line":198},[195,18869,15961],{"class":223},[195,18871,18872],{"class":209}," axios ",[195,18874,15967],{"class":223},[195,18876,18877],{"class":459}," 'axios'\n",[195,18879,18880],{"class":197,"line":230},[195,18881,1241],{"emptyLinePlaceholder":757},[195,18883,18884],{"class":197,"line":251},[195,18885,18886],{"class":415},"\u002F\u002F 在 composable 中封装（≈ Flutter 的 Repository）\n",[195,18888,18889,18891,18894,18897],{"class":197,"line":272},[195,18890,18235],{"class":223},[195,18892,18893],{"class":223}," function",[195,18895,18896],{"class":201}," useUsers",[195,18898,18899],{"class":209},"() {\n",[195,18901,18902,18904,18907,18909,18911],{"class":197,"line":293},[195,18903,18263],{"class":223},[195,18905,18906],{"class":213}," users",[195,18908,398],{"class":223},[195,18910,401],{"class":201},[195,18912,18913],{"class":209},"([])\n",[195,18915,18916,18918,18921,18923,18925,18927,18930],{"class":197,"line":562},[195,18917,18263],{"class":223},[195,18919,18920],{"class":213}," loading",[195,18922,398],{"class":223},[195,18924,401],{"class":201},[195,18926,404],{"class":209},[195,18928,18929],{"class":213},"false",[195,18931,410],{"class":209},[195,18933,18934],{"class":197,"line":583},[195,18935,1241],{"emptyLinePlaceholder":757},[195,18937,18938,18941,18943,18946],{"class":197,"line":962},[195,18939,18940],{"class":223},"  async",[195,18942,18893],{"class":223},[195,18944,18945],{"class":201}," fetchUsers",[195,18947,18899],{"class":209},[195,18949,18950,18953,18955],{"class":197,"line":968},[195,18951,18952],{"class":209},"    loading.value ",[195,18954,424],{"class":223},[195,18956,18957],{"class":213}," true\n",[195,18959,18960,18963],{"class":197,"line":1274},[195,18961,18962],{"class":223},"    try",[195,18964,496],{"class":209},[195,18966,18967,18970,18972,18975,18978,18980,18983,18986,18989,18991,18994],{"class":197,"line":1282},[195,18968,18969],{"class":223},"      const",[195,18971,210],{"class":209},[195,18973,18974],{"class":213},"data",[195,18976,18977],{"class":209}," } ",[195,18979,424],{"class":223},[195,18981,18982],{"class":223}," await",[195,18984,18985],{"class":209}," axios.",[195,18987,18988],{"class":201},"get",[195,18990,404],{"class":209},[195,18992,18993],{"class":459},"'\u002Fapi\u002Fusers'",[195,18995,410],{"class":209},[195,18997,18998,19001,19003],{"class":197,"line":1295},[195,18999,19000],{"class":209},"      users.value ",[195,19002,424],{"class":223},[195,19004,19005],{"class":209}," data\n",[195,19007,19008,19011,19014],{"class":197,"line":1309},[195,19009,19010],{"class":209},"    } ",[195,19012,19013],{"class":223},"finally",[195,19015,496],{"class":209},[195,19017,19018,19021,19023],{"class":197,"line":2246},[195,19019,19020],{"class":209},"      loading.value ",[195,19022,424],{"class":223},[195,19024,19025],{"class":213}," false\n",[195,19027,19028],{"class":197,"line":1996},[195,19029,2403],{"class":209},[195,19031,19032],{"class":197,"line":2257},[195,19033,965],{"class":209},[195,19035,19036],{"class":197,"line":2262},[195,19037,1241],{"emptyLinePlaceholder":757},[195,19039,19040,19043],{"class":197,"line":2267},[195,19041,19042],{"class":201},"  onMounted",[195,19044,19045],{"class":209},"(fetchUsers)\n",[195,19047,19048,19050],{"class":197,"line":2273},[195,19049,18296],{"class":223},[195,19051,19052],{"class":209}," { users, loading }\n",[195,19054,19055],{"class":197,"line":2033},[195,19056,552],{"class":209},[1890,19058],{},[18,19060,19062],{"id":19061},"_12-组合式函数-composables-mixin-hook","12. 组合式函数 (Composables) ≈ Mixin \u002F Hook",[14,19064,19065],{},"Vue 的 Composable 是复用逻辑的核心方式，类似 Flutter 中的 Mixin 或 Riverpod 的自定义 Provider。",[186,19067,19069],{"className":17750,"code":19068,"language":17752,"meta":191,"style":191},"\u002F\u002F composables\u002FuseMouse.ts（≈ Flutter 的 mixin）\nimport { ref, onMounted, onUnmounted } from 'vue'\n\nexport function useMouse() {\n  const x = ref(0)\n  const y = ref(0)\n\n  function update(e: MouseEvent) {\n    x.value = e.pageX\n    y.value = e.pageY\n  }\n\n  onMounted(() => window.addEventListener('mousemove', update))\n  onUnmounted(() => window.removeEventListener('mousemove', update))\n\n  return { x, y }\n}\n",[136,19070,19071,19076,19087,19091,19102,19119,19136,19140,19159,19169,19179,19183,19187,19208,19227,19231,19238],{"__ignoreMap":191},[195,19072,19073],{"class":197,"line":198},[195,19074,19075],{"class":415},"\u002F\u002F composables\u002FuseMouse.ts（≈ Flutter 的 mixin）\n",[195,19077,19078,19080,19083,19085],{"class":197,"line":230},[195,19079,15961],{"class":223},[195,19081,19082],{"class":209}," { ref, onMounted, onUnmounted } ",[195,19084,15967],{"class":223},[195,19086,15970],{"class":459},[195,19088,19089],{"class":197,"line":251},[195,19090,1241],{"emptyLinePlaceholder":757},[195,19092,19093,19095,19097,19100],{"class":197,"line":272},[195,19094,18235],{"class":223},[195,19096,18893],{"class":223},[195,19098,19099],{"class":201}," useMouse",[195,19101,18899],{"class":209},[195,19103,19104,19106,19109,19111,19113,19115,19117],{"class":197,"line":293},[195,19105,18263],{"class":223},[195,19107,19108],{"class":213}," x",[195,19110,398],{"class":223},[195,19112,401],{"class":201},[195,19114,404],{"class":209},[195,19116,407],{"class":213},[195,19118,410],{"class":209},[195,19120,19121,19123,19126,19128,19130,19132,19134],{"class":197,"line":562},[195,19122,18263],{"class":223},[195,19124,19125],{"class":213}," y",[195,19127,398],{"class":223},[195,19129,401],{"class":201},[195,19131,404],{"class":209},[195,19133,407],{"class":213},[195,19135,410],{"class":209},[195,19137,19138],{"class":197,"line":583},[195,19139,1241],{"emptyLinePlaceholder":757},[195,19141,19142,19144,19147,19149,19152,19154,19157],{"class":197,"line":962},[195,19143,18280],{"class":223},[195,19145,19146],{"class":201}," update",[195,19148,404],{"class":209},[195,19150,19151],{"class":634},"e",[195,19153,638],{"class":223},[195,19155,19156],{"class":201}," MouseEvent",[195,19158,644],{"class":209},[195,19160,19161,19164,19166],{"class":197,"line":968},[195,19162,19163],{"class":209},"    x.value ",[195,19165,424],{"class":223},[195,19167,19168],{"class":209}," e.pageX\n",[195,19170,19171,19174,19176],{"class":197,"line":1274},[195,19172,19173],{"class":209},"    y.value ",[195,19175,424],{"class":223},[195,19177,19178],{"class":209}," e.pageY\n",[195,19180,19181],{"class":197,"line":1282},[195,19182,965],{"class":209},[195,19184,19185],{"class":197,"line":1295},[195,19186,1241],{"emptyLinePlaceholder":757},[195,19188,19189,19191,19193,19195,19198,19200,19202,19205],{"class":197,"line":1309},[195,19190,19042],{"class":201},[195,19192,16395],{"class":209},[195,19194,16398],{"class":223},[195,19196,19197],{"class":209}," window.",[195,19199,16755],{"class":201},[195,19201,404],{"class":209},[195,19203,19204],{"class":459},"'mousemove'",[195,19206,19207],{"class":209},", update))\n",[195,19209,19210,19213,19215,19217,19219,19221,19223,19225],{"class":197,"line":2246},[195,19211,19212],{"class":201},"  onUnmounted",[195,19214,16395],{"class":209},[195,19216,16398],{"class":223},[195,19218,19197],{"class":209},[195,19220,16794],{"class":201},[195,19222,404],{"class":209},[195,19224,19204],{"class":459},[195,19226,19207],{"class":209},[195,19228,19229],{"class":197,"line":1996},[195,19230,1241],{"emptyLinePlaceholder":757},[195,19232,19233,19235],{"class":197,"line":2257},[195,19234,18296],{"class":223},[195,19236,19237],{"class":209}," { x, y }\n",[195,19239,19240],{"class":197,"line":2262},[195,19241,552],{"class":209},[186,19243,19245],{"className":15640,"code":19244,"language":15642,"meta":191,"style":191},"\u003C!-- 使用 -->\n\u003Cscript setup lang=\"ts\">\nimport { useMouse } from '@\u002Fcomposables\u002FuseMouse'\nconst { x, y } = useMouse()\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cp>Mouse: {{ x }}, {{ y }}\u003C\u002Fp>\n\u003C\u002Ftemplate>\n",[136,19246,19247,19251,19267,19279,19301,19309,19313,19321,19334],{"__ignoreMap":191},[195,19248,19249],{"class":197,"line":198},[195,19250,18313],{"class":415},[195,19252,19253,19255,19257,19259,19261,19263,19265],{"class":197,"line":230},[195,19254,448],{"class":209},[195,19256,15687],{"class":205},[195,19258,15690],{"class":201},[195,19260,15693],{"class":201},[195,19262,424],{"class":209},[195,19264,15698],{"class":459},[195,19266,477],{"class":209},[195,19268,19269,19271,19274,19276],{"class":197,"line":251},[195,19270,15961],{"class":223},[195,19272,19273],{"class":209}," { useMouse } ",[195,19275,15967],{"class":223},[195,19277,19278],{"class":459}," '@\u002Fcomposables\u002FuseMouse'\n",[195,19280,19281,19283,19285,19288,19290,19293,19295,19297,19299],{"class":197,"line":272},[195,19282,392],{"class":223},[195,19284,210],{"class":209},[195,19286,19287],{"class":213},"x",[195,19289,301],{"class":209},[195,19291,19292],{"class":213},"y",[195,19294,18977],{"class":209},[195,19296,424],{"class":223},[195,19298,19099],{"class":201},[195,19300,18359],{"class":209},[195,19302,19303,19305,19307],{"class":197,"line":293},[195,19304,15672],{"class":209},[195,19306,15687],{"class":205},[195,19308,477],{"class":209},[195,19310,19311],{"class":197,"line":562},[195,19312,1241],{"emptyLinePlaceholder":757},[195,19314,19315,19317,19319],{"class":197,"line":583},[195,19316,448],{"class":209},[195,19318,15651],{"class":205},[195,19320,477],{"class":209},[195,19322,19323,19325,19327,19330,19332],{"class":197,"line":962},[195,19324,15658],{"class":209},[195,19326,14],{"class":205},[195,19328,19329],{"class":209},">Mouse: {{ x }}, {{ y }}\u003C\u002F",[195,19331,14],{"class":205},[195,19333,477],{"class":209},[195,19335,19336,19338,19340],{"class":197,"line":968},[195,19337,15672],{"class":209},[195,19339,15651],{"class":205},[195,19341,477],{"class":209},[1890,19343],{},[18,19345,19347],{"id":19346},"_13-开发工具链对比","13. 开发工具链对比",[36,19349,19350,19360],{},[39,19351,19352],{},[42,19353,19354,19356,19358],{},[45,19355,3111],{},[45,19357,13135],{},[45,19359,15468],{},[52,19361,19362,19377,19392,19403,19414,19429,19442,19455,19467,19478],{},[42,19363,19364,19367,19372],{},[57,19365,19366],{},"创建项目",[57,19368,19369],{},[136,19370,19371],{},"flutter create",[57,19373,19374],{},[136,19375,19376],{},"npm create vue@latest",[42,19378,19379,19382,19387],{},[57,19380,19381],{},"开发服务器",[57,19383,19384],{},[136,19385,19386],{},"flutter run",[57,19388,19389,19391],{},[136,19390,1795],{}," (Vite)",[42,19393,19394,19397,19400],{},[57,19395,19396],{},"热重载",[57,19398,19399],{},"内置 Hot Reload",[57,19401,19402],{},"Vite HMR",[42,19404,19405,19408,19411],{},[57,19406,19407],{},"调试工具",[57,19409,19410],{},"Flutter DevTools",[57,19412,19413],{},"Vue DevTools (浏览器插件)",[42,19415,19416,19419,19424],{},[57,19417,19418],{},"构建",[57,19420,19421],{},[136,19422,19423],{},"flutter build",[57,19425,19426],{},[136,19427,19428],{},"npm run build",[42,19430,19431,19434,19439],{},[57,19432,19433],{},"代码检查",[57,19435,19436],{},[136,19437,19438],{},"dart analyze",[57,19440,19441],{},"ESLint",[42,19443,19444,19447,19452],{},[57,19445,19446],{},"格式化",[57,19448,19449],{},[136,19450,19451],{},"dart format",[57,19453,19454],{},"Prettier",[42,19456,19457,19459,19464],{},[57,19458,12189],{},[57,19460,19461],{},[136,19462,19463],{},"flutter test",[57,19465,19466],{},"Vitest",[42,19468,19469,19472,19475],{},[57,19470,19471],{},"组件测试",[57,19473,19474],{},"Widget Test",[57,19476,19477],{},"@vue\u002Ftest-utils",[42,19479,19480,19483,19486],{},[57,19481,19482],{},"E2E 测试",[57,19484,19485],{},"Integration Test",[57,19487,19488],{},"Cypress \u002F Playwright",[1890,19490],{},[18,19492,19494],{"id":19493},"_14-快速上手路径","14. 快速上手路径",[706,19496,19497,19506,19519,19525,19541,19547,19553],{},[132,19498,19499,19502,19503,19505],{},[125,19500,19501],{},"环境搭建","：安装 Node.js → ",[136,19504,19376],{},"（选 TypeScript + Router + Pinia）",[132,19507,19508,19511,19512,997,19515,19518],{},[125,19509,19510],{},"先跑通","：看懂 ",[136,19513,19514],{},"App.vue",[136,19516,19517],{},"main.ts","、路由配置",[132,19520,19521,19524],{},[125,19522,19523],{},"写组件","：把你熟悉的 Flutter Widget 用 Vue SFC 重写一遍",[132,19526,19527,19530,19531,997,19533,997,19536,997,19539],{},[125,19528,19529],{},"学响应式","：重点掌握 ",[136,19532,16014],{},[136,19534,19535],{},"reactive",[136,19537,19538],{},"computed",[136,19540,16536],{},[132,19542,19543,19546],{},[125,19544,19545],{},"学路由","：Vue Router 的配置式路由和 GoRouter 非常像",[132,19548,19549,19552],{},[125,19550,19551],{},"学状态管理","：Pinia 比 Riverpod 简单，先用再深入",[132,19554,19555,19558],{},[125,19556,19557],{},"学 CSS","：这是 Flutter 工程师最大的新领域，重点学 Flexbox 和 Grid",[1890,19560],{},[18,19562,19564],{"id":19563},"_15-核心思维转换","15. 核心思维转换",[36,19566,19567,19577],{},[39,19568,19569],{},[42,19570,19571,19574],{},[45,19572,19573],{},"Flutter 思维",[45,19575,19576],{},"Vue 思维",[52,19578,19579,19587,19595,19603,19616,19630,19640],{},[42,19580,19581,19584],{},[57,19582,19583],{},"一切皆 Widget",[57,19585,19586],{},"一切皆组件",[42,19588,19589,19592],{},[57,19590,19591],{},"Widget 树是不可变的，rebuild 整棵树",[57,19593,19594],{},"模板 + 响应式数据，只更新变化的 DOM",[42,19596,19597,19600],{},[57,19598,19599],{},"样式是 Widget 的属性",[57,19601,19602],{},"样式和结构分离 (CSS)",[42,19604,19605,19611],{},[57,19606,19607,19610],{},[136,19608,19609],{},"BuildContext"," 访问上层数据",[57,19612,19613,19615],{},[136,19614,16105],{}," \u002F Pinia 访问共享数据",[42,19617,19618,19624],{},[57,19619,19620,19623],{},[136,19621,19622],{},"Key"," 控制 Widget 复用",[57,19625,19626,19629],{},[136,19627,19628],{},":key"," 控制 DOM 元素复用",[42,19631,19632,19637],{},[57,19633,19634,19636],{},[136,19635,392],{}," Widget 优化性能",[57,19638,19639],{},"Vue 编译器自动优化",[42,19641,19642,19649],{},[57,19643,19644,19645,19648],{},"手动 ",[136,19646,19647],{},"setState"," 触发重建",[57,19650,19651],{},"修改响应式数据自动触发更新",[733,19653,19654],{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":191,"searchDepth":230,"depth":230,"links":19656},[19657,19658,19659,19663,19668,19669,19675,19679,19682,19685,19688,19689,19690,19691,19692],{"id":15453,"depth":230,"text":15454},{"id":15564,"depth":230,"text":15565},{"id":15576,"depth":230,"text":15577,"children":19660},[19661,19662],{"id":15580,"depth":251,"text":15581},{"id":15762,"depth":251,"text":15763},{"id":16035,"depth":230,"text":16036,"children":19664},[19665,19666,19667],{"id":16150,"depth":251,"text":16151},{"id":16276,"depth":251,"text":16277},{"id":16436,"depth":251,"text":16437},{"id":16579,"depth":230,"text":16580},{"id":16817,"depth":230,"text":16818,"children":19670},[19671,19672,19673,19674],{"id":16821,"depth":251,"text":16822},{"id":16923,"depth":251,"text":16924},{"id":17047,"depth":251,"text":17048},{"id":17129,"depth":251,"text":17130},{"id":17263,"depth":230,"text":17264,"children":19676},[19677,19678],{"id":17343,"depth":251,"text":17344},{"id":17559,"depth":251,"text":17560},{"id":17683,"depth":230,"text":17684,"children":19680},[19681],{"id":18046,"depth":251,"text":18047},{"id":18119,"depth":230,"text":18120,"children":19683},[19684],{"id":18123,"depth":251,"text":18124},{"id":18425,"depth":230,"text":18426,"children":19686},[19687],{"id":18657,"depth":251,"text":18658},{"id":18829,"depth":230,"text":18830},{"id":19061,"depth":230,"text":19062},{"id":19346,"depth":230,"text":19347},{"id":19493,"depth":230,"text":19494},{"id":19563,"depth":230,"text":19564},"2026-05-03 10:30:00 CST","以 Flutter\u002FDart 的概念为锚点，快速建立 Vue 3 Composition API 的心智模型。",{},"\u002Fnotes\u002F2026-05-03-flutter-to-vue-comparison",{"title":15441,"description":19694},"从 Flutter 工程师视角对比学习 Vue 3，覆盖架构、组件、状态管理、路由、构建工具等核心概念。","Flutter 工程师的 Vue 对比学习指南｜个人笔记","flutter-to-vue-comparison","notes\u002F2026-05-03-flutter-to-vue-comparison","HZBGxlYkEGLzgDkK1Dhcrdt3Rup_0LBUIxERAAoHums",{"id":19704,"title":19705,"body":19706,"category":752,"date":21363,"description":21364,"extension":755,"meta":21365,"navigation":757,"order":752,"path":21366,"seo":21367,"seoDescription":21368,"seoTitle":21369,"slug":21370,"stem":21371,"__hash__":21372},"notes\u002Fnotes\u002F2026-05-03-flutter-project-interview.md","Flutter 项目面试准备 — 重点与难点",{"type":8,"value":19707,"toc":21334},[19708,19721,19723,19727,19731,19751,19755,19778,19780,19784,19788,19793,19799,19804,19879,19884,19896,19898,19902,19906,19912,19929,19933,20039,20044,20061,20066,20071,20073,20077,20082,20093,20097,20187,20191,20208,20210,20214,20219,20230,20234,20311,20315,20320,20322,20326,20331,20348,20352,20596,20600,20613,20615,20619,20623,20637,20641,20803,20807,20820,20822,20826,20831,20837,20840,20857,20861,20869,20871,20875,20880,20891,20895,20913,20917,20925,20927,20931,20934,21075,21077,21081,21085,21097,21101,21106,21110,21115,21117,21121,21168,21170,21174,21275,21277,21281,21291,21297,21302,21331],[11,19709,19710],{},[14,19711,19712,19713,19716,19717,19720],{},"基于 ",[125,19714,19715],{},"AIRBS Wholesale","（C端批发采购App）和 ",[125,19718,19719],{},"YesShop Business HD","（B端iPad批发管理系统）两个真实项目的技术总结。",[1890,19722],{},[18,19724,19726],{"id":19725},"一项目概览开场介绍用","一、项目概览（开场介绍用）",[32,19728,19730],{"id":19729},"airbs-wholesalev340","AIRBS Wholesale（v3.4.0）",[129,19732,19733,19739,19745],{},[132,19734,19735,19738],{},[125,19736,19737],{},"定位","：B2B 批发电商移动端，面向采购商",[132,19740,19741,19744],{},[125,19742,19743],{},"核心功能","：商品浏览\u002F搜索、购物车、销售订单、报价单、优惠券、QR扫码、多语言（英\u002F中\u002F西\u002F意）",[132,19746,19747,19750],{},[125,19748,19749],{},"技术栈","：Flutter + GetX + Dio + SharedPreferences + json_serializable",[32,19752,19754],{"id":19753},"yesshop-business-hdv700","YesShop Business HD（v7.0.0）",[129,19756,19757,19762,19767,19772],{},[132,19758,19759,19761],{},[125,19760,19737],{},"：iPad 端批发业务管理系统，面向业务员\u002F仓管",[132,19763,19764,19766],{},[125,19765,19743],{},"：商品管理、采购\u002F销售订单、报价单、WMS仓储、ESL电子价签、PDA扫码枪、相机拍照",[132,19768,19769,19771],{},[125,19770,19749],{},"：Flutter + GetX + Dio + Drift(SQLite) + SharedPreferences + json_serializable",[132,19773,19774,19777],{},[125,19775,19776],{},"亮点","：同一套代码适配手机和 iPad 两种尺寸",[1890,19779],{},[18,19781,19783],{"id":19782},"二架构设计面试高频问题","二、架构设计（面试高频问题）",[32,19785,19787],{"id":19786},"q-项目整体架构是怎么设计的","Q: 项目整体架构是怎么设计的？",[14,19789,19790],{},[125,19791,19792],{},"回答要点：",[186,19794,19797],{"className":19795,"code":19796,"language":1074},[1072],"lib\u002F\n├── common\u002F          # 全局状态、路由、事件总线\n├── runtime\u002F         # 基础设施：HTTP、Auth、Language、枚举\n├── config\u002F          # 环境配置（develop\u002Ftest\u002Frelease）\n├── domains\u002F         # 业务层：Service + Model + Command\n│   ├── service\u002F     # 18个领域服务（ProductService, CartService...）\n│   ├── model\u002F       # 数据模型（38+ @JsonSerializable 类）\n│   └── command\u002F     # 命令模式处理写操作\n├── modules\u002F         # 功能页面（19个模块，每个含 Controller + View）\n├── components\u002F      # 可复用业务组件\n└── ui\u002F              # 基础 UI 组件库（按钮、表单、对话框、骨架屏...）\n",[136,19798,19796],{"__ignoreMap":191},[14,19800,19801],{},[125,19802,19803],{},"关键设计决策：",[36,19805,19806,19818],{},[39,19807,19808],{},[42,19809,19810,19813,19816],{},[45,19811,19812],{},"层级",[45,19814,19815],{},"职责",[45,19817,13780],{},[52,19819,19820,19833,19850,19863],{},[42,19821,19822,19827,19830],{},[57,19823,19824],{},[125,19825,19826],{},"UI 层",[57,19828,19829],{},"纯展示",[57,19831,19832],{},"Widget 只负责渲染，不含业务逻辑",[42,19834,19835,19840,19843],{},[57,19836,19837],{},[125,19838,19839],{},"Controller 层",[57,19841,19842],{},"页面逻辑",[57,19844,19845,19846,19849],{},"继承 ",[136,19847,19848],{},"GetxController","，管理页面状态和交互",[42,19851,19852,19857,19860],{},[57,19853,19854],{},[125,19855,19856],{},"Service 层",[57,19858,19859],{},"业务逻辑",[57,19861,19862],{},"封装 API 调用和数据处理，与 UI 解耦",[42,19864,19865,19870,19873],{},[57,19866,19867],{},[125,19868,19869],{},"Model 层",[57,19871,19872],{},"数据定义",[57,19874,19875,19878],{},[136,19876,19877],{},"@JsonSerializable"," + code gen，保证序列化一致性",[14,19880,19881],{},[125,19882,19883],{},"可以深入展开的点：",[129,19885,19886,19889],{},[132,19887,19888],{},"为什么选 GetX 而不是 BLoC\u002FRiverpod → 项目偏 CRUD 型业务，GetX 的路由+状态+DI 一体化开发效率高",[132,19890,19891,19892,19895],{},"Service 层的设计 → 每个业务领域一个 Service，通过 ",[136,19893,19894],{},"Get.lazyPut()"," 懒加载注入，按需初始化",[1890,19897],{},[18,19899,19901],{"id":19900},"三重点难点及解决方案","三、重点难点及解决方案",[32,19903,19905],{"id":19904},"难点-1购物车状态管理与多端同步","难点 1：购物车状态管理与多端同步",[14,19907,19908,19911],{},[125,19909,19910],{},"问题描述：","\n购物车是核心模块，涉及多种复杂计算和状态同步：",[129,19913,19914,19917,19920,19923,19926],{},[132,19915,19916],{},"商品有多种包装规格（箱\u002F包\u002F个），切换规格时需要合并或拆分",[132,19918,19919],{},"数量变化触发实时价格计算（单价 × 数量 × 包装倍率）",[132,19921,19922],{},"重量\u002F体积需要单位换算后聚合",[132,19924,19925],{},"优惠券折扣叠加计算",[132,19927,19928],{},"用户快速操作时防止频繁请求后端",[14,19930,19931],{},[125,19932,3562],{},[186,19934,19936],{"className":11162,"code":19935,"language":11164,"meta":191,"style":191},"\u002F\u002F 1. CartState 单例 + Rx 响应式\nclass CartState {\n  final cartItems = RxList\u003CCartItemVdto>([]);\n  final totalAmount = 0.0.obs;\n  final totalWeight = 0.0.obs;\n  \n  \u002F\u002F 2. 防抖同步：250ms 内的多次数量变更只触发一次后端请求\n  void onQuantityChanged(String itemId, int qty) {\n    _updateLocalState(itemId, qty);      \u002F\u002F 立即更新本地\n    _debouncer.run(() => _syncToServer()); \u002F\u002F 防抖同步后端\n  }\n  \n  \u002F\u002F 3. 聚合计算：单位换算 + 折扣\n  void calcSumInfo() {\n    totalWeight.value = cartItems.sumBy((x) =>\n      UnitUtils.weightConvert(targetUnit, x.weightUnit, x.weight * x.quantity));\n    \n    final discounted = amount * (100 - discountRate) \u002F 100;\n    totalAmount.value = discounted;\n  }\n}\n",[136,19937,19938,19943,19948,19953,19958,19963,19968,19973,19978,19983,19988,19992,19996,20001,20006,20011,20016,20021,20026,20031,20035],{"__ignoreMap":191},[195,19939,19940],{"class":197,"line":198},[195,19941,19942],{},"\u002F\u002F 1. CartState 单例 + Rx 响应式\n",[195,19944,19945],{"class":197,"line":230},[195,19946,19947],{},"class CartState {\n",[195,19949,19950],{"class":197,"line":251},[195,19951,19952],{},"  final cartItems = RxList\u003CCartItemVdto>([]);\n",[195,19954,19955],{"class":197,"line":272},[195,19956,19957],{},"  final totalAmount = 0.0.obs;\n",[195,19959,19960],{"class":197,"line":293},[195,19961,19962],{},"  final totalWeight = 0.0.obs;\n",[195,19964,19965],{"class":197,"line":562},[195,19966,19967],{},"  \n",[195,19969,19970],{"class":197,"line":583},[195,19971,19972],{},"  \u002F\u002F 2. 防抖同步：250ms 内的多次数量变更只触发一次后端请求\n",[195,19974,19975],{"class":197,"line":962},[195,19976,19977],{},"  void onQuantityChanged(String itemId, int qty) {\n",[195,19979,19980],{"class":197,"line":968},[195,19981,19982],{},"    _updateLocalState(itemId, qty);      \u002F\u002F 立即更新本地\n",[195,19984,19985],{"class":197,"line":1274},[195,19986,19987],{},"    _debouncer.run(() => _syncToServer()); \u002F\u002F 防抖同步后端\n",[195,19989,19990],{"class":197,"line":1282},[195,19991,965],{},[195,19993,19994],{"class":197,"line":1295},[195,19995,19967],{},[195,19997,19998],{"class":197,"line":1309},[195,19999,20000],{},"  \u002F\u002F 3. 聚合计算：单位换算 + 折扣\n",[195,20002,20003],{"class":197,"line":2246},[195,20004,20005],{},"  void calcSumInfo() {\n",[195,20007,20008],{"class":197,"line":1996},[195,20009,20010],{},"    totalWeight.value = cartItems.sumBy((x) =>\n",[195,20012,20013],{"class":197,"line":2257},[195,20014,20015],{},"      UnitUtils.weightConvert(targetUnit, x.weightUnit, x.weight * x.quantity));\n",[195,20017,20018],{"class":197,"line":2262},[195,20019,20020],{},"    \n",[195,20022,20023],{"class":197,"line":2267},[195,20024,20025],{},"    final discounted = amount * (100 - discountRate) \u002F 100;\n",[195,20027,20028],{"class":197,"line":2273},[195,20029,20030],{},"    totalAmount.value = discounted;\n",[195,20032,20033],{"class":197,"line":2033},[195,20034,965],{},[195,20036,20037],{"class":197,"line":2284},[195,20038,552],{},[14,20040,20041],{},[125,20042,20043],{},"YesShop HD 额外难点 — 离线购物车：",[129,20045,20046,20052,20058],{},[132,20047,594,20048,20051],{},[125,20049,20050],{},"Drift ORM (SQLite)"," 做本地持久化，断网时也能操作购物车",[132,20053,20054,20057],{},[136,20055,20056],{},"CartSubTable"," 存储明细，重新联网后批量同步",[132,20059,20060],{},"去重逻辑：相同商品+相同规格 → 合并数量而非新增行",[14,20062,20063],{},[125,20064,20065],{},"面试话术：",[11,20067,20068],{},[14,20069,20070],{},"\"购物车是整个系统最复杂的状态模块。难点在于实时计算（价格、重量、体积的单位换算）和前后端同步。我用 GetX 的 Rx 响应式做本地即时更新，配合 250ms 防抖避免频繁请求。YesShop 项目因为有离线场景，额外用 Drift 做了 SQLite 持久化，保证断网可用。\"",[1890,20072],{},[32,20074,20076],{"id":20075},"难点-2ipad-手机双端适配yesshop-hd","难点 2：iPad + 手机双端适配（YesShop HD）",[14,20078,20079,20081],{},[125,20080,19910],{},"\n同一套代码需要适配 iPad（1024×1366）和手机（375×812），布局差异很大：",[129,20083,20084,20087,20090],{},[132,20085,20086],{},"iPad 用多列布局，手机用单列",[132,20088,20089],{},"字体大小、间距、图标尺寸都不同",[132,20091,20092],{},"部分页面 iPad 有侧边栏，手机没有",[14,20094,20095],{},[125,20096,3562],{},[186,20098,20100],{"className":11162,"code":20099,"language":11164,"meta":191,"style":191},"\u002F\u002F 1. 基于 flutter_screenutil 的双端适配\nScreenUtils.isPad  \u002F\u002F 运行时判断设备类型\n\n\u002F\u002F 2. 封装适配函数\ndouble fitW(double phoneValue, double padValue) {\n  return ScreenUtils.isPad ? padValue.w : phoneValue.w;\n}\n\n\u002F\u002F 3. 不同设计稿尺寸初始化\nScreenUtil.init(context,\n  designSize: ScreenUtils.isPad \n    ? const Size(1024, 1366)  \u002F\u002F iPad\n    : const Size(375, 812),    \u002F\u002F iPhone\n);\n\n\u002F\u002F 4. 资源文件分目录管理\n\u002F\u002F assets\u002Fphone\u002F  → 手机端图片\n\u002F\u002F assets\u002Fpad\u002F    → iPad端图片\n",[136,20101,20102,20107,20112,20116,20121,20126,20131,20135,20139,20144,20149,20154,20159,20164,20168,20172,20177,20182],{"__ignoreMap":191},[195,20103,20104],{"class":197,"line":198},[195,20105,20106],{},"\u002F\u002F 1. 基于 flutter_screenutil 的双端适配\n",[195,20108,20109],{"class":197,"line":230},[195,20110,20111],{},"ScreenUtils.isPad  \u002F\u002F 运行时判断设备类型\n",[195,20113,20114],{"class":197,"line":251},[195,20115,1241],{"emptyLinePlaceholder":757},[195,20117,20118],{"class":197,"line":272},[195,20119,20120],{},"\u002F\u002F 2. 封装适配函数\n",[195,20122,20123],{"class":197,"line":293},[195,20124,20125],{},"double fitW(double phoneValue, double padValue) {\n",[195,20127,20128],{"class":197,"line":562},[195,20129,20130],{},"  return ScreenUtils.isPad ? padValue.w : phoneValue.w;\n",[195,20132,20133],{"class":197,"line":583},[195,20134,552],{},[195,20136,20137],{"class":197,"line":962},[195,20138,1241],{"emptyLinePlaceholder":757},[195,20140,20141],{"class":197,"line":968},[195,20142,20143],{},"\u002F\u002F 3. 不同设计稿尺寸初始化\n",[195,20145,20146],{"class":197,"line":1274},[195,20147,20148],{},"ScreenUtil.init(context,\n",[195,20150,20151],{"class":197,"line":1282},[195,20152,20153],{},"  designSize: ScreenUtils.isPad \n",[195,20155,20156],{"class":197,"line":1295},[195,20157,20158],{},"    ? const Size(1024, 1366)  \u002F\u002F iPad\n",[195,20160,20161],{"class":197,"line":1309},[195,20162,20163],{},"    : const Size(375, 812),    \u002F\u002F iPhone\n",[195,20165,20166],{"class":197,"line":2246},[195,20167,527],{},[195,20169,20170],{"class":197,"line":1996},[195,20171,1241],{"emptyLinePlaceholder":757},[195,20173,20174],{"class":197,"line":2257},[195,20175,20176],{},"\u002F\u002F 4. 资源文件分目录管理\n",[195,20178,20179],{"class":197,"line":2262},[195,20180,20181],{},"\u002F\u002F assets\u002Fphone\u002F  → 手机端图片\n",[195,20183,20184],{"class":197,"line":2267},[195,20185,20186],{},"\u002F\u002F assets\u002Fpad\u002F    → iPad端图片\n",[14,20188,20189],{},[125,20190,20065],{},[11,20192,20193],{},[14,20194,20195,20196,20199,20200,20203,20204,20207],{},"\"YesShop 最大的挑战之一是一套代码同时跑在 iPad 和手机上。我们用 ",[136,20197,20198],{},"flutter_screenutil"," 配合自定义的 ",[136,20201,20202],{},"fitW()"," 函数做尺寸适配，通过 ",[136,20205,20206],{},"ScreenUtils.isPad"," 在运行时决定布局策略。对于布局差异大的页面，用条件渲染切换多列\u002F单列。图片资源也按设备类型分目录管理，避免 iPad 上图片模糊。\"",[1890,20209],{},[32,20211,20213],{"id":20212},"难点-3pda-扫码枪硬件集成yesshop-hd","难点 3：PDA 扫码枪硬件集成（YesShop HD）",[14,20215,20216,20218],{},[125,20217,19910],{},"\n业务员使用 PDA 手持终端（带物理扫码键），需要：",[129,20220,20221,20224,20227],{},[132,20222,20223],{},"识别多种 PDA 品牌（CRUISE、AUTOID、MT5）",[132,20225,20226],{},"监听硬件扫码按键事件",[132,20228,20229],{},"扫码结果要根据当前所在页面路由到不同处理逻辑",[14,20231,20232],{},[125,20233,3562],{},[186,20235,20237],{"className":11162,"code":20236,"language":11164,"meta":191,"style":191},"\u002F\u002F 1. Platform Channel 与原生通信\nstatic const pdaScanChannel = MethodChannel('pdaScanChannel');\n\n\u002F\u002F 2. 设备识别 — 通过 manufacturer 字段判断 PDA 型号\nbool isPdaDevice = ['CRUISE', 'AUTOID', 'MT5']\n    .contains(deviceInfo.manufacturer.toUpperCase());\n\n\u002F\u002F 3. Android 端 BroadcastReceiver 接收扫码数据\n\u002F\u002F 原生层注册广播接收器，扫到条码后通过 Channel 传给 Flutter\n\n\u002F\u002F 4. 路由感知的扫码订阅\n\u002F\u002F 根据当前页面路由，决定扫码结果的处理方式：\n\u002F\u002F - 商品页 → 查询商品\n\u002F\u002F - 库存页 → 查找库位\n\u002F\u002F - 订单页 → 匹配订单号\n",[136,20238,20239,20244,20249,20253,20258,20263,20268,20272,20277,20282,20286,20291,20296,20301,20306],{"__ignoreMap":191},[195,20240,20241],{"class":197,"line":198},[195,20242,20243],{},"\u002F\u002F 1. Platform Channel 与原生通信\n",[195,20245,20246],{"class":197,"line":230},[195,20247,20248],{},"static const pdaScanChannel = MethodChannel('pdaScanChannel');\n",[195,20250,20251],{"class":197,"line":251},[195,20252,1241],{"emptyLinePlaceholder":757},[195,20254,20255],{"class":197,"line":272},[195,20256,20257],{},"\u002F\u002F 2. 设备识别 — 通过 manufacturer 字段判断 PDA 型号\n",[195,20259,20260],{"class":197,"line":293},[195,20261,20262],{},"bool isPdaDevice = ['CRUISE', 'AUTOID', 'MT5']\n",[195,20264,20265],{"class":197,"line":562},[195,20266,20267],{},"    .contains(deviceInfo.manufacturer.toUpperCase());\n",[195,20269,20270],{"class":197,"line":583},[195,20271,1241],{"emptyLinePlaceholder":757},[195,20273,20274],{"class":197,"line":962},[195,20275,20276],{},"\u002F\u002F 3. Android 端 BroadcastReceiver 接收扫码数据\n",[195,20278,20279],{"class":197,"line":968},[195,20280,20281],{},"\u002F\u002F 原生层注册广播接收器，扫到条码后通过 Channel 传给 Flutter\n",[195,20283,20284],{"class":197,"line":1274},[195,20285,1241],{"emptyLinePlaceholder":757},[195,20287,20288],{"class":197,"line":1282},[195,20289,20290],{},"\u002F\u002F 4. 路由感知的扫码订阅\n",[195,20292,20293],{"class":197,"line":1295},[195,20294,20295],{},"\u002F\u002F 根据当前页面路由，决定扫码结果的处理方式：\n",[195,20297,20298],{"class":197,"line":1309},[195,20299,20300],{},"\u002F\u002F - 商品页 → 查询商品\n",[195,20302,20303],{"class":197,"line":2246},[195,20304,20305],{},"\u002F\u002F - 库存页 → 查找库位\n",[195,20307,20308],{"class":197,"line":1996},[195,20309,20310],{},"\u002F\u002F - 订单页 → 匹配订单号\n",[14,20312,20313],{},[125,20314,20065],{},[11,20316,20317],{},[14,20318,20319],{},"\"YesShop 需要对接 PDA 扫码枪，这是典型的 Platform Channel 实战场景。Android 端通过 BroadcastReceiver 监听硬件扫码事件，再通过 MethodChannel 传给 Flutter。难点是同一个扫码入口要根据当前页面路由分发到不同业务逻辑，我设计了一个基于路由的订阅者模式，每个页面注册自己的扫码回调，离开页面时自动取消。\"",[1890,20321],{},[32,20323,20325],{"id":20324},"难点-4网络层封装与统一错误处理","难点 4：网络层封装与统一错误处理",[14,20327,20328,20330],{},[125,20329,19910],{},"\n两个项目共 30+ 个 Service，几百个 API 接口，需要统一管理：",[129,20332,20333,20336,20339,20342,20345],{},[132,20334,20335],{},"请求头动态注入（token、语言、时区、公司ID、区域ID）",[132,20337,20338],{},"登录过期自动跳转",[132,20340,20341],{},"Loading 弹窗统一控制",[132,20343,20344],{},"分页请求的通用封装",[132,20346,20347],{},"文件上传（单张\u002F多张图片）",[14,20349,20350],{},[125,20351,3562],{},[186,20353,20355],{"className":11162,"code":20354,"language":11164,"meta":191,"style":191},"class Http {\n  static final Dio _dio = Dio(BaseOptions(\n    connectTimeout: Duration(seconds: 20),\n    receiveTimeout: Duration(seconds: 25),\n  ));\n\n  \u002F\u002F 动态请求头\n  static Map\u003CString, String> get _headers => {\n    'ide': Auth.identity,          \u002F\u002F 登录 token\n    'lang': Lang.current.apiCode,   \u002F\u002F 语言代码\n    'corp': AppContext.corpId,      \u002F\u002F 公司ID\n    'area': AppContext.areaCorpId,  \u002F\u002F 区域ID\n    'offset_hours': timezone,       \u002F\u002F 时区偏移\n    'type': '3',                    \u002F\u002F 终端类型\n  };\n\n  \u002F\u002F 统一响应处理\n  static Future\u003CHttpResult\u003CT>> post\u003CT>(String url, {\n    Map\u003CString, dynamic>? data,\n    bool showLoading = false,\n    T Function(dynamic)? fromJson,\n  }) async {\n    if (showLoading) DialogUtils.showLoading();\n    try {\n      final response = await _dio.post(url, data: data);\n      final result = HttpResult\u003CT>.fromJson(response.data, fromJson);\n      \n      \u002F\u002F 登录过期统一拦截\n      if (result.errorCode == HttpErrorCode.loginExpired) {\n        _redirectToLogin();\n      }\n      return result;\n    } on DioException catch (e) {\n      return HttpResult.error(e.message);\n    } finally {\n      if (showLoading) DialogUtils.dismiss();\n    }\n  }\n\n  \u002F\u002F 分页请求封装\n  static Future\u003CPagedResult\u003CT>> paged\u003CT>(...) { ... }\n  \n  \u002F\u002F 图片上传\n  static Future\u003CHttpResult\u003CT>> uploadImage(File file) { ... }\n  static Future\u003CHttpResult\u003CList\u003CT>>> multiUploadImage(List\u003CFile> files) { ... }\n}\n",[136,20356,20357,20362,20367,20372,20377,20382,20386,20391,20396,20404,20412,20420,20428,20436,20444,20449,20453,20458,20463,20468,20473,20478,20483,20488,20492,20497,20502,20507,20512,20517,20522,20526,20531,20536,20541,20546,20551,20555,20559,20563,20568,20573,20577,20582,20587,20592],{"__ignoreMap":191},[195,20358,20359],{"class":197,"line":198},[195,20360,20361],{},"class Http {\n",[195,20363,20364],{"class":197,"line":230},[195,20365,20366],{},"  static final Dio _dio = Dio(BaseOptions(\n",[195,20368,20369],{"class":197,"line":251},[195,20370,20371],{},"    connectTimeout: Duration(seconds: 20),\n",[195,20373,20374],{"class":197,"line":272},[195,20375,20376],{},"    receiveTimeout: Duration(seconds: 25),\n",[195,20378,20379],{"class":197,"line":293},[195,20380,20381],{},"  ));\n",[195,20383,20384],{"class":197,"line":562},[195,20385,1241],{"emptyLinePlaceholder":757},[195,20387,20388],{"class":197,"line":583},[195,20389,20390],{},"  \u002F\u002F 动态请求头\n",[195,20392,20393],{"class":197,"line":962},[195,20394,20395],{},"  static Map\u003CString, String> get _headers => {\n",[195,20397,20398,20401],{"class":197,"line":968},[195,20399,20400],{},"    'ide': Auth.identity,",[195,20402,20403],{},"          \u002F\u002F 登录 token\n",[195,20405,20406,20409],{"class":197,"line":1274},[195,20407,20408],{},"    'lang': Lang.current.apiCode,",[195,20410,20411],{},"   \u002F\u002F 语言代码\n",[195,20413,20414,20417],{"class":197,"line":1282},[195,20415,20416],{},"    'corp': AppContext.corpId,",[195,20418,20419],{},"      \u002F\u002F 公司ID\n",[195,20421,20422,20425],{"class":197,"line":1295},[195,20423,20424],{},"    'area': AppContext.areaCorpId,",[195,20426,20427],{},"  \u002F\u002F 区域ID\n",[195,20429,20430,20433],{"class":197,"line":1309},[195,20431,20432],{},"    'offset_hours': timezone,",[195,20434,20435],{},"       \u002F\u002F 时区偏移\n",[195,20437,20438,20441],{"class":197,"line":2246},[195,20439,20440],{},"    'type': '3',",[195,20442,20443],{},"                    \u002F\u002F 终端类型\n",[195,20445,20446],{"class":197,"line":1996},[195,20447,20448],{},"  };\n",[195,20450,20451],{"class":197,"line":2257},[195,20452,1241],{"emptyLinePlaceholder":757},[195,20454,20455],{"class":197,"line":2262},[195,20456,20457],{},"  \u002F\u002F 统一响应处理\n",[195,20459,20460],{"class":197,"line":2267},[195,20461,20462],{},"  static Future\u003CHttpResult\u003CT>> post\u003CT>(String url, {\n",[195,20464,20465],{"class":197,"line":2273},[195,20466,20467],{},"    Map\u003CString, dynamic>? data,\n",[195,20469,20470],{"class":197,"line":2033},[195,20471,20472],{},"    bool showLoading = false,\n",[195,20474,20475],{"class":197,"line":2284},[195,20476,20477],{},"    T Function(dynamic)? fromJson,\n",[195,20479,20480],{"class":197,"line":2460},[195,20481,20482],{},"  }) async {\n",[195,20484,20485],{"class":197,"line":2466},[195,20486,20487],{},"    if (showLoading) DialogUtils.showLoading();\n",[195,20489,20490],{"class":197,"line":2472},[195,20491,9611],{},[195,20493,20494],{"class":197,"line":2780},[195,20495,20496],{},"      final response = await _dio.post(url, data: data);\n",[195,20498,20499],{"class":197,"line":2786},[195,20500,20501],{},"      final result = HttpResult\u003CT>.fromJson(response.data, fromJson);\n",[195,20503,20504],{"class":197,"line":2792},[195,20505,20506],{},"      \n",[195,20508,20509],{"class":197,"line":2798},[195,20510,20511],{},"      \u002F\u002F 登录过期统一拦截\n",[195,20513,20514],{"class":197,"line":2804},[195,20515,20516],{},"      if (result.errorCode == HttpErrorCode.loginExpired) {\n",[195,20518,20519],{"class":197,"line":2810},[195,20520,20521],{},"        _redirectToLogin();\n",[195,20523,20524],{"class":197,"line":2815},[195,20525,12326],{},[195,20527,20528],{"class":197,"line":2820},[195,20529,20530],{},"      return result;\n",[195,20532,20533],{"class":197,"line":2825},[195,20534,20535],{},"    } on DioException catch (e) {\n",[195,20537,20538],{"class":197,"line":2831},[195,20539,20540],{},"      return HttpResult.error(e.message);\n",[195,20542,20543],{"class":197,"line":2837},[195,20544,20545],{},"    } finally {\n",[195,20547,20548],{"class":197,"line":2843},[195,20549,20550],{},"      if (showLoading) DialogUtils.dismiss();\n",[195,20552,20553],{"class":197,"line":2848},[195,20554,2403],{},[195,20556,20557],{"class":197,"line":2854},[195,20558,965],{},[195,20560,20561],{"class":197,"line":2860},[195,20562,1241],{"emptyLinePlaceholder":757},[195,20564,20565],{"class":197,"line":2866},[195,20566,20567],{},"  \u002F\u002F 分页请求封装\n",[195,20569,20570],{"class":197,"line":2872},[195,20571,20572],{},"  static Future\u003CPagedResult\u003CT>> paged\u003CT>(...) { ... }\n",[195,20574,20575],{"class":197,"line":2878},[195,20576,19967],{},[195,20578,20579],{"class":197,"line":2884},[195,20580,20581],{},"  \u002F\u002F 图片上传\n",[195,20583,20584],{"class":197,"line":2890},[195,20585,20586],{},"  static Future\u003CHttpResult\u003CT>> uploadImage(File file) { ... }\n",[195,20588,20589],{"class":197,"line":2895},[195,20590,20591],{},"  static Future\u003CHttpResult\u003CList\u003CT>>> multiUploadImage(List\u003CFile> files) { ... }\n",[195,20593,20594],{"class":197,"line":2900},[195,20595,552],{},[14,20597,20598],{},[125,20599,20065],{},[11,20601,20602],{},[14,20603,20604,20605,20608,20609,20612],{},"\"我封装了一个统一的 Http 工具类，基于 Dio，所有请求自动注入动态 header（token、语言、时区等）。核心设计是 ",[136,20606,20607],{},"HttpResult\u003CT>"," 泛型响应包装，配合 fromJson 工厂方法实现类型安全的反序列化。对于登录过期（errorCode 1003\u002F1004）做了全局拦截自动跳转登录页。还封装了 ",[136,20610,20611],{},"paged\u003CT>()"," 方法统一处理分页逻辑，避免每个列表页重复写分页代码。\"",[1890,20614],{},[32,20616,20618],{"id":20617},"难点-5多语言方案设计与维护","难点 5：多语言方案设计与维护",[14,20620,20621],{},[125,20622,19910],{},[129,20624,20625,20628,20631,20634],{},[132,20626,20627],{},"AIRBS 支持 4 种语言（英\u002F中\u002F西\u002F意），YesShop 支持 3 种（英\u002F中\u002F西）",[132,20629,20630],{},"语言需要运行时切换，且持久化到本地",[132,20632,20633],{},"API 请求头也要同步当前语言",[132,20635,20636],{},"枚举值、单位名称等也需要国际化",[14,20638,20639],{},[125,20640,3562],{},[186,20642,20644],{"className":11162,"code":20643,"language":11164,"meta":191,"style":191},"\u002F\u002F 1. 自定义 L 类 — 编译时类型安全\nclass L {\n  final String en;\n  final String zh;\n  final String es;\n  final String it;\n  \n  const L({required this.en, required this.zh, required this.es, required this.it});\n  \n  \u002F\u002F 根据当前语言返回对应文本\n  String get tr => switch (Lang.current) {\n    Language.en => en,\n    Language.zh => zh,\n    Language.es => es,\n    Language.it => it,\n  };\n}\n\n\u002F\u002F 2. 每个模块定义自己的 locals\nfinal _locals = (\n  msgConfirmClear: L(en: 'Clear cart?', zh: '清空购物车？', es: '¿Vaciar carrito?', it: 'Svuotare?'),\n  btnCheckout: L(en: 'Checkout', zh: '结算', es: 'Pagar', it: 'Pagare'),\n);\n\n\u002F\u002F 3. 使用\nText(_locals.msgConfirmClear.tr)\n\n\u002F\u002F 4. 语言切换 — 通知全局\nLang.changeTo(Language.en);\n\u002F\u002F → 持久化到 SharedPreferences\n\u002F\u002F → Get.updateLocale() 更新 Material 组件语言\n\u002F\u002F → EventBus 发送 LanguageChangeEvent\n\u002F\u002F → API 请求头自动切换\n",[136,20645,20646,20651,20656,20661,20666,20671,20676,20680,20685,20689,20694,20699,20704,20709,20714,20719,20723,20727,20731,20736,20741,20746,20751,20755,20759,20764,20769,20773,20778,20783,20788,20793,20798],{"__ignoreMap":191},[195,20647,20648],{"class":197,"line":198},[195,20649,20650],{},"\u002F\u002F 1. 自定义 L 类 — 编译时类型安全\n",[195,20652,20653],{"class":197,"line":230},[195,20654,20655],{},"class L {\n",[195,20657,20658],{"class":197,"line":251},[195,20659,20660],{},"  final String en;\n",[195,20662,20663],{"class":197,"line":272},[195,20664,20665],{},"  final String zh;\n",[195,20667,20668],{"class":197,"line":293},[195,20669,20670],{},"  final String es;\n",[195,20672,20673],{"class":197,"line":562},[195,20674,20675],{},"  final String it;\n",[195,20677,20678],{"class":197,"line":583},[195,20679,19967],{},[195,20681,20682],{"class":197,"line":962},[195,20683,20684],{},"  const L({required this.en, required this.zh, required this.es, required this.it});\n",[195,20686,20687],{"class":197,"line":968},[195,20688,19967],{},[195,20690,20691],{"class":197,"line":1274},[195,20692,20693],{},"  \u002F\u002F 根据当前语言返回对应文本\n",[195,20695,20696],{"class":197,"line":1282},[195,20697,20698],{},"  String get tr => switch (Lang.current) {\n",[195,20700,20701],{"class":197,"line":1295},[195,20702,20703],{},"    Language.en => en,\n",[195,20705,20706],{"class":197,"line":1309},[195,20707,20708],{},"    Language.zh => zh,\n",[195,20710,20711],{"class":197,"line":2246},[195,20712,20713],{},"    Language.es => es,\n",[195,20715,20716],{"class":197,"line":1996},[195,20717,20718],{},"    Language.it => it,\n",[195,20720,20721],{"class":197,"line":2257},[195,20722,20448],{},[195,20724,20725],{"class":197,"line":2262},[195,20726,552],{},[195,20728,20729],{"class":197,"line":2267},[195,20730,1241],{"emptyLinePlaceholder":757},[195,20732,20733],{"class":197,"line":2273},[195,20734,20735],{},"\u002F\u002F 2. 每个模块定义自己的 locals\n",[195,20737,20738],{"class":197,"line":2033},[195,20739,20740],{},"final _locals = (\n",[195,20742,20743],{"class":197,"line":2284},[195,20744,20745],{},"  msgConfirmClear: L(en: 'Clear cart?', zh: '清空购物车？', es: '¿Vaciar carrito?', it: 'Svuotare?'),\n",[195,20747,20748],{"class":197,"line":2460},[195,20749,20750],{},"  btnCheckout: L(en: 'Checkout', zh: '结算', es: 'Pagar', it: 'Pagare'),\n",[195,20752,20753],{"class":197,"line":2466},[195,20754,527],{},[195,20756,20757],{"class":197,"line":2472},[195,20758,1241],{"emptyLinePlaceholder":757},[195,20760,20761],{"class":197,"line":2780},[195,20762,20763],{},"\u002F\u002F 3. 使用\n",[195,20765,20766],{"class":197,"line":2786},[195,20767,20768],{},"Text(_locals.msgConfirmClear.tr)\n",[195,20770,20771],{"class":197,"line":2792},[195,20772,1241],{"emptyLinePlaceholder":757},[195,20774,20775],{"class":197,"line":2798},[195,20776,20777],{},"\u002F\u002F 4. 语言切换 — 通知全局\n",[195,20779,20780],{"class":197,"line":2804},[195,20781,20782],{},"Lang.changeTo(Language.en);\n",[195,20784,20785],{"class":197,"line":2810},[195,20786,20787],{},"\u002F\u002F → 持久化到 SharedPreferences\n",[195,20789,20790],{"class":197,"line":2815},[195,20791,20792],{},"\u002F\u002F → Get.updateLocale() 更新 Material 组件语言\n",[195,20794,20795],{"class":197,"line":2820},[195,20796,20797],{},"\u002F\u002F → EventBus 发送 LanguageChangeEvent\n",[195,20799,20800],{"class":197,"line":2825},[195,20801,20802],{},"\u002F\u002F → API 请求头自动切换\n",[14,20804,20805],{},[125,20806,20065],{},[11,20808,20809],{},[14,20810,20811,20812,20815,20816,20819],{},"\"我们没用 ARB 文件或第三方国际化包，而是自定义了一个 ",[136,20813,20814],{},"L"," 类，每个翻译项是编译时常量，通过 ",[136,20817,20818],{},".tr"," getter 根据当前语言返回对应文本。好处是类型安全、IDE 自动补全、重构友好。切换语言时通过 EventBus 广播，同时更新 SharedPreferences 持久化、GetX locale、和 API 请求头，保证全链路一致。\"",[1890,20821],{},[32,20823,20825],{"id":20824},"难点-6商品可售性判断的复杂业务逻辑","难点 6：商品可售性判断的复杂业务逻辑",[14,20827,20828,20830],{},[125,20829,19910],{},"\n一个商品是否可以售卖，取决于 5 个条件同时满足：",[186,20832,20835],{"className":20833,"code":20834,"language":1074},[1072],"1. 商品启用状态 (enabled = true)\n2. 至少有一个可售包装规格\n3. 平台销售标志开启\n4. 库存数量 > 0\n5. 售价 > 0\n",[136,20836,20834],{"__ignoreMap":191},[14,20838,20839],{},"还有更复杂的定价策略：",[129,20841,20842,20845,20848,20851,20854],{},[132,20843,20844],{},"基础价 + 数量阶梯价",[132,20846,20847],{},"特价商品（isSpecial + specialPrice）",[132,20849,20850],{},"清仓价 \u002F 返场价",[132,20852,20853],{},"优惠券折扣叠加",[132,20855,20856],{},"按包装规格的倍率计算（1箱=12个，price × rate）",[14,20858,20859],{},[125,20860,20065],{},[11,20862,20863],{},[14,20864,20865,20866,20868],{},"\"批发场景的定价逻辑比 C 端复杂很多。一个商品能不能卖要过 5 个校验，价格计算涉及包装倍率、阶梯价、特价、优惠券叠加。我在 Service 层统一封装了定价计算方法，Model 层用 ",[136,20867,19877],{}," 保证后端数据映射准确，避免前端自己拼凑计算逻辑导致金额不一致。\"",[1890,20870],{},[32,20872,20874],{"id":20873},"难点-7自定义数字键盘与表单交互yesshop-hd","难点 7：自定义数字键盘与表单交互（YesShop HD）",[14,20876,20877,20879],{},[125,20878,19910],{},"\n业务员在 iPad 上录入大量数字（数量、价格、重量），系统键盘体验差：",[129,20881,20882,20885,20888],{},[132,20883,20884],{},"需要支持上一项\u002F下一项快速跳转",[132,20886,20887],{},"小数点可选（数量不需要，价格需要）",[132,20889,20890],{},"输入时顶部实时显示当前值",[14,20892,20893],{},[125,20894,3562],{},[129,20896,20897,20904,20910],{},[132,20898,20899,20900,20903],{},"自定义 ",[136,20901,20902],{},"NumericKeyboard"," 组件，替代系统键盘",[132,20905,20906,20909],{},[136,20907,20908],{},"NumericTextFieldManage"," 管理焦点链，支持 Previous\u002FNext\u002FDone",[132,20911,20912],{},"路由感知 — 离开页面自动关闭键盘、清理焦点",[14,20914,20915],{},[125,20916,20065],{},[11,20918,20919],{},[14,20920,20921,20922,20924],{},"\"iPad 上录入大量数字时系统键盘体验很差，所以我们做了自定义数字键盘。难点不在键盘本身，而是焦点管理——多个输入框之间的 Previous\u002FNext 跳转、页面切换时的自动清理。我用了一个 ",[136,20923,20908],{}," 统一管理焦点链，配合路由监听做生命周期清理。\"",[1890,20926],{},[18,20928,20930],{"id":20929},"四性能优化面试加分项","四、性能优化（面试加分项）",[32,20932,20933],{"id":20933},"可以聊的优化点",[36,20935,20936,20949],{},[39,20937,20938],{},[42,20939,20940,20943,20946],{},[45,20941,20942],{},"优化项",[45,20944,20945],{},"做法",[45,20947,20948],{},"效果",[52,20950,20951,20969,20982,20998,21016,21029,21044,21060],{},[42,20952,20953,20958,20966],{},[57,20954,20955],{},[125,20956,20957],{},"图片加载",[57,20959,20960,4728,20962,20965],{},[136,20961,14552],{},[136,20963,20964],{},"flutter_cache_manager"," 二级缓存",[57,20967,20968],{},"减少重复网络请求，列表滑动流畅",[42,20970,20971,20976,20979],{},[57,20972,20973],{},[125,20974,20975],{},"图片上传",[57,20977,20978],{},"上传前压缩（quality 90%，min 800×800）",[57,20980,20981],{},"减少上传时间和带宽",[42,20983,20984,20989,20995],{},[57,20985,20986],{},[125,20987,20988],{},"列表分页",[57,20990,20991,20994],{},[136,20992,20993],{},"RefreshPagingListController"," 封装分页+下拉刷新+上拉加载",[57,20996,20997],{},"首屏快，按需加载",[42,20999,21000,21005,21013],{},[57,21001,21002],{},[125,21003,21004],{},"骨架屏",[57,21006,20899,21007,4728,21010],{},[136,21008,21009],{},"ShimmerWidget",[136,21011,21012],{},"AnimationController.unbounded()",[57,21014,21015],{},"提升感知性能，替代白屏等待",[42,21017,21018,21023,21026],{},[57,21019,21020],{},[125,21021,21022],{},"防抖",[57,21024,21025],{},"购物车数量变更 250ms 防抖",[57,21027,21028],{},"避免频繁 API 调用",[42,21030,21031,21036,21041],{},[57,21032,21033],{},[125,21034,21035],{},"懒加载",[57,21037,21038,21040],{},[136,21039,19894],{}," 按需初始化 Service\u002FController",[57,21042,21043],{},"减少启动时间",[42,21045,21046,21051,21057],{},[57,21047,21048],{},[125,21049,21050],{},"文字缩放",[57,21052,21053,21056],{},[136,21054,21055],{},"TextScaler.linear(1.0)"," 固定缩放比",[57,21058,21059],{},"防止系统字体放大导致布局错乱",[42,21061,21062,21067,21072],{},[57,21063,21064],{},[125,21065,21066],{},"屏幕适配",[57,21068,21069,21071],{},[136,21070,20198],{}," + splitScreenMode",[57,21073,21074],{},"不同设备一致体验",[1890,21076],{},[18,21078,21080],{"id":21079},"五技术选型问题面试常见追问","五、技术选型问题（面试常见追问）",[32,21082,21084],{"id":21083},"q-为什么选-getx-而不是-bloc","Q: 为什么选 GetX 而不是 BLoC？",[11,21086,21087],{},[14,21088,21089,21090,4728,21093,21096],{},"\"两个项目都是业务驱动的 CRUD 应用，页面多、迭代快。GetX 的优势是路由、状态管理、依赖注入三合一，减少样板代码。BLoC 更适合状态流转复杂的场景，但对于我们这种以表单和列表为主的业务系统，GetX 的 ",[136,21091,21092],{},"Obx",[136,21094,21095],{},"RxList"," 开发效率更高。缺点是 GetX 的隐式依赖查找在大型项目中需要注意管理生命周期。\"",[32,21098,21100],{"id":21099},"q-为什么-yesshop-要用-drift-而-airbs-不用","Q: 为什么 YesShop 要用 Drift 而 AIRBS 不用？",[11,21102,21103],{},[14,21104,21105],{},"\"YesShop 是业务员使用的工具，有离线操作购物车的场景，需要本地持久化。Drift 提供类型安全的 SQL 操作和 schema migration，比直接用 sqflite 写 raw SQL 更可靠。AIRBS 是 C 端采购 App，网络环境稳定，用 SharedPreferences 存简单配置就够了。\"",[32,21107,21109],{"id":21108},"q-网络层为什么选-dio-而不是-http-包","Q: 网络层为什么选 Dio 而不是 http 包？",[11,21111,21112],{},[14,21113,21114],{},"\"Dio 支持拦截器、请求取消、文件上传、超时配置，这些在业务项目中都是必须的。特别是拦截器机制，让我可以统一处理 token 注入、登录过期跳转、Loading 弹窗，不用在每个 API 调用处重复写。\"",[1890,21116],{},[18,21118,21120],{"id":21119},"六可以主动提的亮点","六、可以主动提的亮点",[706,21122,21123,21129,21135,21141,21147,21153,21162],{},[132,21124,21125,21128],{},[125,21126,21127],{},"EventBus 跨模块通信"," — 登录\u002F登出、语言切换、区域切换等全局事件，用 EventBus 解耦模块间依赖",[132,21130,21131,21134],{},[125,21132,21133],{},"Deep Linking"," — AIRBS 支持 App Links，扫码\u002F分享链接可直接跳转到商品详情页",[132,21136,21137,21140],{},[125,21138,21139],{},"TabViewController 基类"," — 封装了底部 Tab 页面的生命周期（激活\u002F失活\u002F首次加载\u002F登录状态），子类只需关注业务",[132,21142,21143,21146],{},[125,21144,21145],{},"RxFilter 响应式过滤器"," — 支持变更追踪、默认值保存\u002F恢复，用于复杂筛选场景",[132,21148,21149,21152],{},[125,21150,21151],{},"多环境配置"," — develop\u002Ftest\u002Frelease 三套环境，Host 配置从服务端动态获取，避免硬编码",[132,21154,21155,21158,21159,698],{},[125,21156,21157],{},"PDF 预览"," — YesShop 支持在 App 内查看 PDF 文档（",[136,21160,21161],{},"flutter_pdfview",[132,21163,21164,21167],{},[125,21165,21166],{},"Syncfusion 图表"," — YesShop Dashboard 使用 Syncfusion Charts 展示业务数据可视化",[1890,21169],{},[18,21171,21173],{"id":21172},"七项目数据量化展示能力","七、项目数据（量化展示能力）",[36,21175,21176,21187],{},[39,21177,21178],{},[42,21179,21180,21183,21185],{},[45,21181,21182],{},"指标",[45,21184,19715],{},[45,21186,19719],{},[52,21188,21189,21200,21210,21221,21232,21243,21253,21264],{},[42,21190,21191,21194,21197],{},[57,21192,21193],{},"功能模块数",[57,21195,21196],{},"19",[57,21198,21199],{},"60+ 路由页面",[42,21201,21202,21205,21207],{},[57,21203,21204],{},"Service 数",[57,21206,18626],{},[57,21208,21209],{},"20+",[42,21211,21212,21215,21218],{},[57,21213,21214],{},"数据模型数",[57,21216,21217],{},"38+",[57,21219,21220],{},"50+",[42,21222,21223,21226,21229],{},[57,21224,21225],{},"支持语言",[57,21227,21228],{},"4（英\u002F中\u002F西\u002F意）",[57,21230,21231],{},"3（英\u002F中\u002F西）",[42,21233,21234,21237,21240],{},[57,21235,21236],{},"设备适配",[57,21238,21239],{},"手机",[57,21241,21242],{},"手机 + iPad",[42,21244,21245,21248,21250],{},[57,21246,21247],{},"本地数据库",[57,21249,2135],{},[57,21251,21252],{},"Drift (SQLite)",[42,21254,21255,21258,21261],{},[57,21256,21257],{},"硬件对接",[57,21259,21260],{},"QR 扫码",[57,21262,21263],{},"QR 扫码 + PDA 扫码枪 + 相机",[42,21265,21266,21269,21272],{},[57,21267,21268],{},"当前版本",[57,21270,21271],{},"v3.4.0",[57,21273,21274],{},"v7.0.0",[1890,21276],{},[18,21278,21280],{"id":21279},"八面试万能回答模板","八、面试万能回答模板",[14,21282,21283,21284,21287,21288,333],{},"当面试官问 ",[125,21285,21286],{},"\"讲一个你遇到的难点，是怎么解决的\""," 时，用 ",[125,21289,21290],{},"STAR 法则",[186,21292,21295],{"className":21293,"code":21294,"language":1074},[1072],"Situation（背景）: 在 XXX 项目中，我们需要实现 XXX 功能...\nTask（任务）:      难点在于 XXX，因为 XXX...\nAction（行动）:    我的解决方案是 XXX，具体做了以下几步...\nResult（结果）:    最终实现了 XXX，效果是 XXX...\n",[136,21296,21294],{"__ignoreMap":191},[14,21298,21299],{},[125,21300,21301],{},"示例：",[11,21303,21304],{},[14,21305,21306,21309,21310,21313,21314,21317,21318,21320,21321,21323,21324,21326,21327,21330],{},[125,21307,21308],{},"S",": 在 YesShop Business HD 项目中，我们需要同一套 Flutter 代码同时运行在 iPad 和手机上。\n",[125,21311,21312],{},"T",": 难点在于两种设备的屏幕尺寸差异巨大，iPad 适合多列布局，手机适合单列，而且字体、间距、图片都需要差异化。\n",[125,21315,21316],{},"A",": 我基于 ",[136,21319,20198],{}," 封装了双端适配方案：用不同的设计稿尺寸初始化，写了 ",[136,21322,20202],{}," 等适配函数，通过 ",[136,21325,20206],{}," 在运行时切换布局策略，图片资源按设备分目录管理。\n",[125,21328,21329],{},"R",": 最终一套代码成功适配两种设备，没有维护两个分支的成本，iPad 上的多列布局充分利用了大屏空间，用户体验良好。",[733,21332,21333],{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":191,"searchDepth":230,"depth":230,"links":21335},[21336,21340,21343,21352,21355,21360,21361,21362],{"id":19725,"depth":230,"text":19726,"children":21337},[21338,21339],{"id":19729,"depth":251,"text":19730},{"id":19753,"depth":251,"text":19754},{"id":19782,"depth":230,"text":19783,"children":21341},[21342],{"id":19786,"depth":251,"text":19787},{"id":19900,"depth":230,"text":19901,"children":21344},[21345,21346,21347,21348,21349,21350,21351],{"id":19904,"depth":251,"text":19905},{"id":20075,"depth":251,"text":20076},{"id":20212,"depth":251,"text":20213},{"id":20324,"depth":251,"text":20325},{"id":20617,"depth":251,"text":20618},{"id":20824,"depth":251,"text":20825},{"id":20873,"depth":251,"text":20874},{"id":20929,"depth":230,"text":20930,"children":21353},[21354],{"id":20933,"depth":251,"text":20933},{"id":21079,"depth":230,"text":21080,"children":21356},[21357,21358,21359],{"id":21083,"depth":251,"text":21084},{"id":21099,"depth":251,"text":21100},{"id":21108,"depth":251,"text":21109},{"id":21119,"depth":230,"text":21120},{"id":21172,"depth":230,"text":21173},{"id":21279,"depth":230,"text":21280},"2026-05-03 10:20:00 CST","基于真实跨境电商和 iPad 批发管理系统项目的技术总结与面试准备。",{},"\u002Fnotes\u002F2026-05-03-flutter-project-interview",{"title":19705,"description":21364},"基于 AIRBS Wholesale 和 YesShop Business HD 两个真实 Flutter 项目的技术重点、难点和面试准备总结。","Flutter 项目面试准备 — 重点与难点｜个人笔记","flutter-project-interview","notes\u002F2026-05-03-flutter-project-interview","VuOnuN2CxdL5bn4EWcnsw5jid9PzK9q0gQa5JbXNCsk",{"id":21374,"title":21375,"body":21376,"category":752,"date":28600,"description":28601,"extension":755,"meta":28602,"navigation":757,"order":752,"path":28603,"seo":28604,"seoDescription":28605,"seoTitle":28606,"slug":1742,"stem":28607,"__hash__":28608},"notes\u002Fnotes\u002F2026-05-03-flutter-interview.md","Flutter 中高级工程师面试题与详解",{"type":8,"value":21377,"toc":28529},[21378,21380,21382,21453,21455,21459,21469,21473,21540,21594,21606,21608,21616,21620,21623,21662,21669,21725,21732,21756,21758,21766,21770,21835,21840,21933,21942,21966,21972,21991,21993,22003,22007,22013,22061,22067,22153,22158,22160,22172,22176,22183,22226,22232,22238,22279,22281,22285,22289,22293,22296,22302,22307,22328,22330,22334,22338,22409,22415,22417,22425,22429,22434,22517,22522,22536,22541,22549,22559,22561,22565,22569,22573,22579,22661,22665,22702,22707,22718,22720,22727,22731,22737,22818,22823,22886,22891,22922,22924,22932,22936,22942,22947,23103,23105,23109,23113,23117,23220,23225,23337,23342,23426,23428,23439,23443,23448,23453,23484,23577,23582,23596,23607,23609,23613,23617,23621,23628,23634,23639,23656,23704,23709,23751,23753,23767,23771,23823,23828,23857,23862,23982,23987,24022,24024,24035,24039,24044,24147,24149,24153,24157,24161,24166,24315,24323,24347,24352,24419,24424,24453,24458,24516,24518,24522,24526,24531,24550,24553,24564,24569,24621,24626,24678,24683,24712,24714,24718,24722,24726,24732,24787,24792,24857,24938,24943,24999,25001,25005,25009,25076,25137,25142,25153,25155,25159,25163,25167,25172,25225,25228,25239,25244,25341,25346,25467,25469,25473,25477,25481,25539,25544,25702,25707,25737,25742,25846,25848,25852,25856,25859,25941,25978,25983,25998,26000,26004,26008,26012,26017,26023,26166,26171,26338,26340,26344,26348,26353,26436,26441,26487,26492,26548,26550,26554,26558,26562,26628,26632,26688,26694,26699,26710,26712,26716,26720,26754,26759,26788,26855,26877,26883,26885,26889,26893,26897,27300,27302,27306,27310,27466,27471,27511,27526,27528,27532,27536,27802,27804,27808,27812,27875,27915,28058,28118,28164,28166,28170,28174,28179,28269,28334,28397,28430,28435,28451,28453,28457,28526],[1890,21379],{},[18,21381,1894],{"id":1894},[706,21383,21384,21390,21396,21402,21407,21413,21418,21424,21430,21435,21441,21447],{},[132,21385,21386],{},[1904,21387,21389],{"href":21388},"#1-dart-%E8%AF%AD%E8%A8%80%E5%9F%BA%E7%A1%80","Dart 语言基础",[132,21391,21392],{},[1904,21393,21395],{"href":21394},"#2-flutter-%E6%B8%B2%E6%9F%93%E6%9C%BA%E5%88%B6","Flutter 渲染机制",[132,21397,21398],{},[1904,21399,21401],{"href":21400},"#3-widget--element--renderobject","Widget \u002F Element \u002F RenderObject",[132,21403,21404],{},[1904,21405,2014],{"href":21406},"#4-%E7%8A%B6%E6%80%81%E7%AE%A1%E7%90%86",[132,21408,21409],{},[1904,21410,21412],{"href":21411},"#5-%E5%BC%82%E6%AD%A5%E7%BC%96%E7%A8%8B","异步编程",[132,21414,21415],{},[1904,21416,2026],{"href":21417},"#6-%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96",[132,21419,21420],{},[1904,21421,21423],{"href":21422},"#7-%E5%B9%B3%E5%8F%B0%E9%80%9A%E4%BF%A1platform-channel","平台通信（Platform Channel）",[132,21425,21426],{},[1904,21427,21429],{"href":21428},"#8-%E8%B7%AF%E7%94%B1%E4%B8%8E%E5%AF%BC%E8%88%AA","路由与导航",[132,21431,21432],{},[1904,21433,12189],{"href":21434},"#9-%E6%B5%8B%E8%AF%95",[132,21436,21437],{},[1904,21438,21440],{"href":21439},"#10-%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1","架构设计",[132,21442,21443],{},[1904,21444,21446],{"href":21445},"#11-%E5%8C%85%E7%AE%A1%E7%90%86%E4%B8%8E%E7%BC%96%E8%AF%91","包管理与编译",[132,21448,21449],{},[1904,21450,21452],{"href":21451},"#12-%E5%AE%9E%E6%88%98%E5%9C%BA%E6%99%AF%E9%A2%98","实战场景题",[1890,21454],{},[18,21456,21458],{"id":21457},"_1-dart-语言基础","1. Dart 语言基础",[32,21460,21462,21463,1437,21465,21468],{"id":21461},"q11-dart-中-const-和-final-的区别是什么","Q1.1: Dart 中 ",[136,21464,392],{},[136,21466,21467],{},"final"," 的区别是什么？",[14,21470,21471],{},[125,21472,2077],{},[36,21474,21475,21489],{},[39,21476,21477],{},[42,21478,21479,21481,21485],{},[45,21480,2086],{},[45,21482,21483],{},[136,21484,21467],{},[45,21486,21487],{},[136,21488,392],{},[52,21490,21491,21502,21513,21527],{},[42,21492,21493,21496,21499],{},[57,21494,21495],{},"赋值时机",[57,21497,21498],{},"运行时确定，只能赋值一次",[57,21500,21501],{},"编译时确定，必须是编译期常量",[42,21503,21504,21507,21510],{},[57,21505,21506],{},"对象可变性",[57,21508,21509],{},"引用不可变，对象内部状态可变",[57,21511,21512],{},"引用和对象都不可变（深度不可变）",[42,21514,21515,21518,21521],{},[57,21516,21517],{},"类成员",[57,21519,21520],{},"可作为实例成员",[57,21522,21523,21524],{},"只能作为 ",[136,21525,21526],{},"static const",[42,21528,21529,21532,21535],{},[57,21530,21531],{},"构造函数",[57,21533,21534],{},"无特殊要求",[57,21536,9064,21537,21539],{},[136,21538,392],{}," 构造函数",[186,21541,21543],{"className":11162,"code":21542,"language":11164,"meta":191,"style":191},"\u002F\u002F final: 运行时确定\nfinal now = DateTime.now(); \u002F\u002F OK\nfinal list = [1, 2, 3];\nlist.add(4); \u002F\u002F OK，可以修改列表内容\n\n\u002F\u002F const: 编译时确定\nconst pi = 3.14; \u002F\u002F OK\nconst now = DateTime.now(); \u002F\u002F 错误！DateTime.now() 不是编译期常量\nconst list = [1, 2, 3];\nlist.add(4); \u002F\u002F 运行时报错，不可修改\n",[136,21544,21545,21550,21555,21560,21565,21569,21574,21579,21584,21589],{"__ignoreMap":191},[195,21546,21547],{"class":197,"line":198},[195,21548,21549],{},"\u002F\u002F final: 运行时确定\n",[195,21551,21552],{"class":197,"line":230},[195,21553,21554],{},"final now = DateTime.now(); \u002F\u002F OK\n",[195,21556,21557],{"class":197,"line":251},[195,21558,21559],{},"final list = [1, 2, 3];\n",[195,21561,21562],{"class":197,"line":272},[195,21563,21564],{},"list.add(4); \u002F\u002F OK，可以修改列表内容\n",[195,21566,21567],{"class":197,"line":293},[195,21568,1241],{"emptyLinePlaceholder":757},[195,21570,21571],{"class":197,"line":562},[195,21572,21573],{},"\u002F\u002F const: 编译时确定\n",[195,21575,21576],{"class":197,"line":583},[195,21577,21578],{},"const pi = 3.14; \u002F\u002F OK\n",[195,21580,21581],{"class":197,"line":962},[195,21582,21583],{},"const now = DateTime.now(); \u002F\u002F 错误！DateTime.now() 不是编译期常量\n",[195,21585,21586],{"class":197,"line":968},[195,21587,21588],{},"const list = [1, 2, 3];\n",[195,21590,21591],{"class":197,"line":1274},[195,21592,21593],{},"list.add(4); \u002F\u002F 运行时报错，不可修改\n",[14,21595,21596,21599,21600,21602,21603,21605],{},[125,21597,21598],{},"深入点："," Flutter 中大量使用 ",[136,21601,392],{}," 构造函数来创建 Widget，因为 ",[136,21604,392],{}," 对象在编译时就确定了，可以被复用，避免重复创建，从而提升性能。",[1890,21607],{},[32,21609,21611,21612,21615],{"id":21610},"q12-dart-的空安全null-safety机制是怎样的late-关键字有什么作用和风险","Q1.2: Dart 的空安全（Null Safety）机制是怎样的？",[136,21613,21614],{},"late"," 关键字有什么作用和风险？",[14,21617,21618],{},[125,21619,2077],{},[14,21621,21622],{},"Dart 2.12 引入了 Sound Null Safety，类型系统区分可空和非空类型：",[186,21624,21626],{"className":11162,"code":21625,"language":11164,"meta":191,"style":191},"String name = 'hello';   \u002F\u002F 非空，不能赋值 null\nString? name = null;      \u002F\u002F 可空\n\n\u002F\u002F 常用操作符\nname?.length;      \u002F\u002F 空安全调用\nname ?? 'default'; \u002F\u002F 空合并\nname!;             \u002F\u002F 强制解包（断言非空，为 null 时抛异常）\n",[136,21627,21628,21633,21638,21642,21647,21652,21657],{"__ignoreMap":191},[195,21629,21630],{"class":197,"line":198},[195,21631,21632],{},"String name = 'hello';   \u002F\u002F 非空，不能赋值 null\n",[195,21634,21635],{"class":197,"line":230},[195,21636,21637],{},"String? name = null;      \u002F\u002F 可空\n",[195,21639,21640],{"class":197,"line":251},[195,21641,1241],{"emptyLinePlaceholder":757},[195,21643,21644],{"class":197,"line":272},[195,21645,21646],{},"\u002F\u002F 常用操作符\n",[195,21648,21649],{"class":197,"line":293},[195,21650,21651],{},"name?.length;      \u002F\u002F 空安全调用\n",[195,21653,21654],{"class":197,"line":562},[195,21655,21656],{},"name ?? 'default'; \u002F\u002F 空合并\n",[195,21658,21659],{"class":197,"line":583},[195,21660,21661],{},"name!;             \u002F\u002F 强制解包（断言非空，为 null 时抛异常）\n",[14,21663,21664],{},[125,21665,21666,21668],{},[136,21667,21614],{}," 关键字：",[186,21670,21672],{"className":11162,"code":21671,"language":11164,"meta":191,"style":191},"class MyWidget {\n  \u002F\u002F 延迟初始化：告诉编译器\"我保证在使用前会赋值\"\n  late final String title;\n\n  \u002F\u002F 懒加载：首次访问时才执行初始化\n  late final db = DatabaseHelper.open();\n\n  void init(String t) {\n    title = t;\n  }\n}\n",[136,21673,21674,21679,21684,21689,21693,21698,21703,21707,21712,21717,21721],{"__ignoreMap":191},[195,21675,21676],{"class":197,"line":198},[195,21677,21678],{},"class MyWidget {\n",[195,21680,21681],{"class":197,"line":230},[195,21682,21683],{},"  \u002F\u002F 延迟初始化：告诉编译器\"我保证在使用前会赋值\"\n",[195,21685,21686],{"class":197,"line":251},[195,21687,21688],{},"  late final String title;\n",[195,21690,21691],{"class":197,"line":272},[195,21692,1241],{"emptyLinePlaceholder":757},[195,21694,21695],{"class":197,"line":293},[195,21696,21697],{},"  \u002F\u002F 懒加载：首次访问时才执行初始化\n",[195,21699,21700],{"class":197,"line":562},[195,21701,21702],{},"  late final db = DatabaseHelper.open();\n",[195,21704,21705],{"class":197,"line":583},[195,21706,1241],{"emptyLinePlaceholder":757},[195,21708,21709],{"class":197,"line":962},[195,21710,21711],{},"  void init(String t) {\n",[195,21713,21714],{"class":197,"line":968},[195,21715,21716],{},"    title = t;\n",[195,21718,21719],{"class":197,"line":1274},[195,21720,965],{},[195,21722,21723],{"class":197,"line":1282},[195,21724,552],{},[14,21726,21727],{},[125,21728,21729,21731],{},[136,21730,21614],{}," 的风险：",[129,21733,21734,21743,21746],{},[132,21735,21736,21737,21739,21740],{},"如果在赋值前访问 ",[136,21738,21614],{}," 变量，会抛出 ",[136,21741,21742],{},"LateInitializationError",[132,21744,21745],{},"编译器无法在编译时捕获这个错误，相当于把空安全的保护推迟到了运行时",[132,21747,21748,21749,21751,21752,21755],{},"应尽量避免滥用 ",[136,21750,21614],{},"，可以考虑用可空类型 + ",[136,21753,21754],{},"??"," 替代",[1890,21757],{},[32,21759,21761,21762,21765],{"id":21760},"q13-dart-中的-mixin-是什么和抽象类接口有什么区别","Q1.3: Dart 中的 ",[136,21763,21764],{},"mixin"," 是什么？和抽象类、接口有什么区别？",[14,21767,21768],{},[125,21769,2077],{},[186,21771,21773],{"className":11162,"code":21772,"language":11164,"meta":191,"style":191},"\u002F\u002F Mixin：用 mixin 关键字定义，提供代码复用\nmixin Swimming {\n  void swim() => print('Swimming');\n}\n\nmixin Flying {\n  void fly() => print('Flying');\n}\n\n\u002F\u002F 使用 with 混入\nclass Duck extends Animal with Swimming, Flying {\n  \u002F\u002F Duck 同时拥有 swim() 和 fly()\n}\n",[136,21774,21775,21780,21785,21790,21794,21798,21803,21808,21812,21816,21821,21826,21831],{"__ignoreMap":191},[195,21776,21777],{"class":197,"line":198},[195,21778,21779],{},"\u002F\u002F Mixin：用 mixin 关键字定义，提供代码复用\n",[195,21781,21782],{"class":197,"line":230},[195,21783,21784],{},"mixin Swimming {\n",[195,21786,21787],{"class":197,"line":251},[195,21788,21789],{},"  void swim() => print('Swimming');\n",[195,21791,21792],{"class":197,"line":272},[195,21793,552],{},[195,21795,21796],{"class":197,"line":293},[195,21797,1241],{"emptyLinePlaceholder":757},[195,21799,21800],{"class":197,"line":562},[195,21801,21802],{},"mixin Flying {\n",[195,21804,21805],{"class":197,"line":583},[195,21806,21807],{},"  void fly() => print('Flying');\n",[195,21809,21810],{"class":197,"line":962},[195,21811,552],{},[195,21813,21814],{"class":197,"line":968},[195,21815,1241],{"emptyLinePlaceholder":757},[195,21817,21818],{"class":197,"line":1274},[195,21819,21820],{},"\u002F\u002F 使用 with 混入\n",[195,21822,21823],{"class":197,"line":1282},[195,21824,21825],{},"class Duck extends Animal with Swimming, Flying {\n",[195,21827,21828],{"class":197,"line":1295},[195,21829,21830],{},"  \u002F\u002F Duck 同时拥有 swim() 和 fly()\n",[195,21832,21833],{"class":197,"line":1309},[195,21834,552],{},[14,21836,21837],{},[125,21838,21839],{},"三者对比：",[36,21841,21842,21857],{},[39,21843,21844],{},[42,21845,21846,21848,21851,21854],{},[45,21847,2086],{},[45,21849,21850],{},"抽象类 (abstract class)",[45,21852,21853],{},"接口 (implicit interface)",[45,21855,21856],{},"Mixin",[52,21858,21859,21871,21893,21907,21920],{},[42,21860,21861,21864,21867,21869],{},[57,21862,21863],{},"实例化",[57,21865,21866],{},"不能直接实例化",[57,21868,21866],{},[57,21870,21866],{},[42,21872,21873,21876,21882,21888],{},[57,21874,21875],{},"继承方式",[57,21877,21878,21881],{},[136,21879,21880],{},"extends","（单继承）",[57,21883,21884,21887],{},[136,21885,21886],{},"implements","（多实现）",[57,21889,21890,21892],{},[136,21891,7173],{},"（多混入）",[42,21894,21895,21897,21900,21902],{},[57,21896,21531],{},[57,21898,21899],{},"可以有",[57,21901,21899],{},[57,21903,21904,21531],{},[125,21905,21906],{},"不能有",[42,21908,21909,21912,21915,21918],{},[57,21910,21911],{},"方法实现",[57,21913,21914],{},"可以有默认实现",[57,21916,21917],{},"需要全部重写",[57,21919,21914],{},[42,21921,21922,21924,21927,21930],{},[57,21923,10893],{},[57,21925,21926],{},"只能继承一个",[57,21928,21929],{},"可以实现多个",[57,21931,21932],{},"可以混入多个",[14,21934,21935,21938,21939,333],{},[125,21936,21937],{},"Mixin 的线性化（Linearization）："," 当多个 mixin 有同名方法时，",[125,21940,21941],{},"最后混入的优先",[186,21943,21945],{"className":11162,"code":21944,"language":11164,"meta":191,"style":191},"mixin A { String greet() => 'A'; }\nmixin B { String greet() => 'B'; }\n\nclass C with A, B {} \u002F\u002F C().greet() 返回 'B'\n",[136,21946,21947,21952,21957,21961],{"__ignoreMap":191},[195,21948,21949],{"class":197,"line":198},[195,21950,21951],{},"mixin A { String greet() => 'A'; }\n",[195,21953,21954],{"class":197,"line":230},[195,21955,21956],{},"mixin B { String greet() => 'B'; }\n",[195,21958,21959],{"class":197,"line":251},[195,21960,1241],{"emptyLinePlaceholder":757},[195,21962,21963],{"class":197,"line":272},[195,21964,21965],{},"class C with A, B {} \u002F\u002F C().greet() 返回 'B'\n",[14,21967,21968,21971],{},[136,21969,21970],{},"mixin on"," 可以限制 mixin 只能用于特定类型：",[186,21973,21975],{"className":11162,"code":21974,"language":11164,"meta":191,"style":191},"mixin Draggable on Widget {\n  \u002F\u002F 只能被 Widget 的子类使用\n}\n",[136,21976,21977,21982,21987],{"__ignoreMap":191},[195,21978,21979],{"class":197,"line":198},[195,21980,21981],{},"mixin Draggable on Widget {\n",[195,21983,21984],{"class":197,"line":230},[195,21985,21986],{},"  \u002F\u002F 只能被 Widget 的子类使用\n",[195,21988,21989],{"class":197,"line":251},[195,21990,552],{},[1890,21992],{},[32,21994,21996,21997,22000,22001],{"id":21995},"q14-解释-dart-中的-extension-方法和-sealed-class","Q1.4: 解释 Dart 中的 ",[136,21998,21999],{},"Extension"," 方法和 ",[136,22002,6824],{},[14,22004,22005],{},[125,22006,2077],{},[14,22008,22009,22012],{},[125,22010,22011],{},"Extension 方法（Dart 2.7+）："," 在不修改原始类的情况下添加方法：",[186,22014,22016],{"className":11162,"code":22015,"language":11164,"meta":191,"style":191},"extension StringX on String {\n  bool get isEmail => RegExp(r'^[\\w-.]+@([\\w-]+\\.)+[\\w-]{2,4}$').hasMatch(this);\n  String capitalize() => '${this[0].toUpperCase()}${substring(1)}';\n}\n\n\u002F\u002F 使用\n'test@email.com'.isEmail; \u002F\u002F true\n'hello'.capitalize();      \u002F\u002F 'Hello'\n",[136,22017,22018,22023,22028,22033,22037,22041,22045,22053],{"__ignoreMap":191},[195,22019,22020],{"class":197,"line":198},[195,22021,22022],{},"extension StringX on String {\n",[195,22024,22025],{"class":197,"line":230},[195,22026,22027],{},"  bool get isEmail => RegExp(r'^[\\w-.]+@([\\w-]+\\.)+[\\w-]{2,4}$').hasMatch(this);\n",[195,22029,22030],{"class":197,"line":251},[195,22031,22032],{},"  String capitalize() => '${this[0].toUpperCase()}${substring(1)}';\n",[195,22034,22035],{"class":197,"line":272},[195,22036,552],{},[195,22038,22039],{"class":197,"line":293},[195,22040,1241],{"emptyLinePlaceholder":757},[195,22042,22043],{"class":197,"line":562},[195,22044,3056],{},[195,22046,22047,22050],{"class":197,"line":583},[195,22048,22049],{},"'test@email.com'.isEmail;",[195,22051,22052],{}," \u002F\u002F true\n",[195,22054,22055,22058],{"class":197,"line":962},[195,22056,22057],{},"'hello'.capitalize();",[195,22059,22060],{},"      \u002F\u002F 'Hello'\n",[14,22062,22063,22066],{},[125,22064,22065],{},"Sealed Class（Dart 3.0+）："," 限制类的继承范围，必须在同一个文件中定义子类，编译器可以做穷举检查：",[186,22068,22070],{"className":11162,"code":22069,"language":11164,"meta":191,"style":191},"sealed class Shape {}\n\nclass Circle extends Shape {\n  final double radius;\n  Circle(this.radius);\n}\n\nclass Square extends Shape {\n  final double side;\n  Square(this.side);\n}\n\n\u002F\u002F switch 穷举检查（不需要 default）\ndouble area(Shape shape) => switch (shape) {\n  Circle(radius: var r) => 3.14 * r * r,\n  Square(side: var s) => s * s,\n  \u002F\u002F 如果遗漏了某个子类，编译器会报错\n};\n",[136,22071,22072,22076,22080,22085,22090,22095,22099,22103,22108,22113,22118,22122,22126,22131,22135,22139,22144,22149],{"__ignoreMap":191},[195,22073,22074],{"class":197,"line":198},[195,22075,11483],{},[195,22077,22078],{"class":197,"line":230},[195,22079,1241],{"emptyLinePlaceholder":757},[195,22081,22082],{"class":197,"line":251},[195,22083,22084],{},"class Circle extends Shape {\n",[195,22086,22087],{"class":197,"line":272},[195,22088,22089],{},"  final double radius;\n",[195,22091,22092],{"class":197,"line":293},[195,22093,22094],{},"  Circle(this.radius);\n",[195,22096,22097],{"class":197,"line":562},[195,22098,552],{},[195,22100,22101],{"class":197,"line":583},[195,22102,1241],{"emptyLinePlaceholder":757},[195,22104,22105],{"class":197,"line":962},[195,22106,22107],{},"class Square extends Shape {\n",[195,22109,22110],{"class":197,"line":968},[195,22111,22112],{},"  final double side;\n",[195,22114,22115],{"class":197,"line":1274},[195,22116,22117],{},"  Square(this.side);\n",[195,22119,22120],{"class":197,"line":1282},[195,22121,552],{},[195,22123,22124],{"class":197,"line":1295},[195,22125,1241],{"emptyLinePlaceholder":757},[195,22127,22128],{"class":197,"line":1309},[195,22129,22130],{},"\u002F\u002F switch 穷举检查（不需要 default）\n",[195,22132,22133],{"class":197,"line":2246},[195,22134,11502],{},[195,22136,22137],{"class":197,"line":1996},[195,22138,11507],{},[195,22140,22141],{"class":197,"line":2257},[195,22142,22143],{},"  Square(side: var s) => s * s,\n",[195,22145,22146],{"class":197,"line":2262},[195,22147,22148],{},"  \u002F\u002F 如果遗漏了某个子类，编译器会报错\n",[195,22150,22151],{"class":197,"line":2267},[195,22152,11446],{},[14,22154,22155,22157],{},[136,22156,6824],{}," 非常适合用于状态建模（如 BLoC 的 State）。",[1890,22159],{},[32,22161,22163,22164,22167,22168,22171],{"id":22162},"q15-dart-中的泛型协变covariance是什么为什么-listcat-可以赋值给-listanimal","Q1.5: Dart 中的泛型协变（Covariance）是什么？为什么 ",[136,22165,22166],{},"List\u003CCat>"," 可以赋值给 ",[136,22169,22170],{},"List\u003CAnimal>","？",[14,22173,22174],{},[125,22175,2077],{},[14,22177,22178,22179,22182],{},"Dart 的泛型是",[125,22180,22181],{},"协变（covariant）的","，这与 Java（不变）不同：",[186,22184,22186],{"className":11162,"code":22185,"language":11164,"meta":191,"style":191},"class Animal {}\nclass Cat extends Animal {}\n\nList\u003CCat> cats = [Cat()];\nList\u003CAnimal> animals = cats; \u002F\u002F Dart 中合法！\n\n\u002F\u002F 但这会带来运行时风险：\nanimals.add(Dog()); \u002F\u002F 运行时报错！因为底层实际是 List\u003CCat>\n",[136,22187,22188,22193,22198,22202,22207,22212,22216,22221],{"__ignoreMap":191},[195,22189,22190],{"class":197,"line":198},[195,22191,22192],{},"class Animal {}\n",[195,22194,22195],{"class":197,"line":230},[195,22196,22197],{},"class Cat extends Animal {}\n",[195,22199,22200],{"class":197,"line":251},[195,22201,1241],{"emptyLinePlaceholder":757},[195,22203,22204],{"class":197,"line":272},[195,22205,22206],{},"List\u003CCat> cats = [Cat()];\n",[195,22208,22209],{"class":197,"line":293},[195,22210,22211],{},"List\u003CAnimal> animals = cats; \u002F\u002F Dart 中合法！\n",[195,22213,22214],{"class":197,"line":562},[195,22215,1241],{"emptyLinePlaceholder":757},[195,22217,22218],{"class":197,"line":583},[195,22219,22220],{},"\u002F\u002F 但这会带来运行时风险：\n",[195,22222,22223],{"class":197,"line":962},[195,22224,22225],{},"animals.add(Dog()); \u002F\u002F 运行时报错！因为底层实际是 List\u003CCat>\n",[14,22227,22228,22229,1589],{},"Dart 选择了协变，在编译时允许这种赋值，但通过运行时检查来保证类型安全。这是一种",[125,22230,22231],{},"实用主义的折衷",[14,22233,22234,22237],{},[136,22235,22236],{},"covariant"," 关键字用于显式声明参数协变：",[186,22239,22241],{"className":11162,"code":22240,"language":11164,"meta":191,"style":191},"class Animal {\n  void chase(covariant Animal other) {} \u002F\u002F 子类可以缩窄参数类型\n}\n\nclass Cat extends Animal {\n  @override\n  void chase(Mouse other) {} \u002F\u002F 参数从 Animal 缩窄为 Mouse\n}\n",[136,22242,22243,22248,22253,22257,22261,22266,22270,22275],{"__ignoreMap":191},[195,22244,22245],{"class":197,"line":198},[195,22246,22247],{},"class Animal {\n",[195,22249,22250],{"class":197,"line":230},[195,22251,22252],{},"  void chase(covariant Animal other) {} \u002F\u002F 子类可以缩窄参数类型\n",[195,22254,22255],{"class":197,"line":251},[195,22256,552],{},[195,22258,22259],{"class":197,"line":272},[195,22260,1241],{"emptyLinePlaceholder":757},[195,22262,22263],{"class":197,"line":293},[195,22264,22265],{},"class Cat extends Animal {\n",[195,22267,22268],{"class":197,"line":562},[195,22269,12090],{},[195,22271,22272],{"class":197,"line":583},[195,22273,22274],{},"  void chase(Mouse other) {} \u002F\u002F 参数从 Animal 缩窄为 Mouse\n",[195,22276,22277],{"class":197,"line":962},[195,22278,552],{},[1890,22280],{},[18,22282,22284],{"id":22283},"_2-flutter-渲染机制","2. Flutter 渲染机制",[32,22286,22288],{"id":22287},"q21-flutter-的渲染流水线rendering-pipeline是怎样的一帧是如何绘制到屏幕上的","Q2.1: Flutter 的渲染流水线（Rendering Pipeline）是怎样的？一帧是如何绘制到屏幕上的？",[14,22290,22291],{},[125,22292,2077],{},[14,22294,22295],{},"Flutter 渲染流水线的核心流程（每帧约 16.6ms @60fps）：",[186,22297,22300],{"className":22298,"code":22299,"language":1074},[1072],"用户输入 \u002F 动画 Ticker\n       ↓\n┌──────────────────────────────────────────────┐\n│  1. Build Phase（构建阶段）                    │\n│     - 调用 Widget.build()                     │\n│     - 生成\u002F更新 Element Tree                   │\n│     - 标记需要更新的 Element (dirty)            │\n├──────────────────────────────────────────────┤\n│  2. Layout Phase（布局阶段）                   │\n│     - RenderObject.performLayout()            │\n│     - 父节点向子节点传递 Constraints            │\n│     - 子节点向父节点返回 Size                   │\n│     - 确定每个节点的大小和位置                   │\n├──────────────────────────────────────────────┤\n│  3. Paint Phase（绘制阶段）                    │\n│     - RenderObject.paint()                    │\n│     - 绘制到 Layer Tree                        │\n│     - 生成绘制指令                              │\n├──────────────────────────────────────────────┤\n│  4. Compositing（合成阶段）                    │\n│     - Layer Tree 发送到 Engine                 │\n│     - Skia\u002FImpeller 执行光栅化                  │\n│     - GPU 渲染到屏幕                            │\n└──────────────────────────────────────────────┘\n",[136,22301,22299],{"__ignoreMap":191},[14,22303,22304],{},[125,22305,22306],{},"关键概念：",[129,22308,22309,22315,22322],{},[132,22310,22311,22314],{},[125,22312,22313],{},"Constraints go down, Sizes go up, Parent sets position","：约束从父到子传递，尺寸从子到父返回，最终由父节点决定子节点的位置",[132,22316,22317,22318,22321],{},"Flutter 使用 ",[125,22319,22320],{},"单次遍历布局算法","（O(N)），非常高效",[132,22323,22324,22327],{},[125,22325,22326],{},"Relayout Boundary","：限制重新布局的范围，避免整棵树重新计算",[1890,22329],{},[32,22331,22333],{"id":22332},"q22-什么是-impeller它和-skia-有什么区别","Q2.2: 什么是 Impeller？它和 Skia 有什么区别？",[14,22335,22336],{},[125,22337,2077],{},[36,22339,22340,22352],{},[39,22341,22342],{},[42,22343,22344,22346,22349],{},[45,22345,2086],{},[45,22347,22348],{},"Skia",[45,22350,22351],{},"Impeller",[52,22353,22354,22365,22376,22387,22398],{},[42,22355,22356,22359,22362],{},[57,22357,22358],{},"来源",[57,22360,22361],{},"Google 通用 2D 图形库",[57,22363,22364],{},"Flutter 团队专门为 Flutter 开发",[42,22366,22367,22370,22373],{},[57,22368,22369],{},"Shader 编译",[57,22371,22372],{},"运行时编译（JIT）",[57,22374,22375],{},"构建时预编译（AOT）",[42,22377,22378,22381,22384],{},[57,22379,22380],{},"首帧卡顿（Jank）",[57,22382,22383],{},"存在 Shader compilation jank",[57,22385,22386],{},"几乎消除",[42,22388,22389,22392,22395],{},[57,22390,22391],{},"平台支持",[57,22393,22394],{},"全平台",[57,22396,22397],{},"iOS（默认），Android（已稳定）",[42,22399,22400,22403,22406],{},[57,22401,22402],{},"渲染后端",[57,22404,22405],{},"OpenGL、Vulkan、Metal",[57,22407,22408],{},"Metal（iOS）、Vulkan\u002FOpenGL（Android）",[14,22410,22411,22414],{},[125,22412,22413],{},"Impeller 解决的核心问题："," Skia 在首次使用某种绘制效果时需要运行时编译 Shader，导致掉帧（shader compilation jank）。Impeller 在构建时就预编译了所有 Shader，从根本上消除了这个问题。",[1890,22416],{},[32,22418,22420,22421,22424],{"id":22419},"q23-解释-flutter-中的-repaintboundary-的作用和使用场景","Q2.3: 解释 Flutter 中的 ",[136,22422,22423],{},"RepaintBoundary"," 的作用和使用场景",[14,22426,22427],{},[125,22428,2077],{},[14,22430,22431,22433],{},[136,22432,22423],{}," 将子树隔离到独立的 Layer 中，当子树需要重绘时，不会影响到父级或兄弟节点。",[186,22435,22437],{"className":11162,"code":22436,"language":11164,"meta":191,"style":191},"\u002F\u002F 没有 RepaintBoundary：整个区域一起重绘\nColumn(\n  children: [\n    StaticHeader(),     \u002F\u002F 每次都跟着重绘\n    AnimatedCounter(),  \u002F\u002F 频繁变化\n  ],\n)\n\n\u002F\u002F 有 RepaintBoundary：AnimatedCounter 重绘时不影响 Header\nColumn(\n  children: [\n    StaticHeader(),\n    RepaintBoundary(\n      child: AnimatedCounter(), \u002F\u002F 独立图层，独立重绘\n    ),\n  ],\n)\n",[136,22438,22439,22444,22449,22454,22459,22464,22468,22472,22476,22481,22485,22489,22494,22499,22504,22509,22513],{"__ignoreMap":191},[195,22440,22441],{"class":197,"line":198},[195,22442,22443],{},"\u002F\u002F 没有 RepaintBoundary：整个区域一起重绘\n",[195,22445,22446],{"class":197,"line":230},[195,22447,22448],{},"Column(\n",[195,22450,22451],{"class":197,"line":251},[195,22452,22453],{},"  children: [\n",[195,22455,22456],{"class":197,"line":272},[195,22457,22458],{},"    StaticHeader(),     \u002F\u002F 每次都跟着重绘\n",[195,22460,22461],{"class":197,"line":293},[195,22462,22463],{},"    AnimatedCounter(),  \u002F\u002F 频繁变化\n",[195,22465,22466],{"class":197,"line":562},[195,22467,17864],{},[195,22469,22470],{"class":197,"line":583},[195,22471,410],{},[195,22473,22474],{"class":197,"line":962},[195,22475,1241],{"emptyLinePlaceholder":757},[195,22477,22478],{"class":197,"line":968},[195,22479,22480],{},"\u002F\u002F 有 RepaintBoundary：AnimatedCounter 重绘时不影响 Header\n",[195,22482,22483],{"class":197,"line":1274},[195,22484,22448],{},[195,22486,22487],{"class":197,"line":1282},[195,22488,22453],{},[195,22490,22491],{"class":197,"line":1295},[195,22492,22493],{},"    StaticHeader(),\n",[195,22495,22496],{"class":197,"line":1309},[195,22497,22498],{},"    RepaintBoundary(\n",[195,22500,22501],{"class":197,"line":2246},[195,22502,22503],{},"      child: AnimatedCounter(), \u002F\u002F 独立图层，独立重绘\n",[195,22505,22506],{"class":197,"line":1996},[195,22507,22508],{},"    ),\n",[195,22510,22511],{"class":197,"line":2257},[195,22512,17864],{},[195,22514,22515],{"class":197,"line":2262},[195,22516,410],{},[14,22518,22519],{},[125,22520,22521],{},"适合使用的场景：",[129,22523,22524,22527,22530],{},[132,22525,22526],{},"频繁重绘的动画组件（如进度条、计数器）",[132,22528,22529],{},"复杂但静态的子树（如复杂地图、图表的静态部分）",[132,22531,22532,22535],{},[136,22533,22534],{},"CustomPaint"," 中复杂的绑定逻辑",[14,22537,22538],{},[125,22539,22540],{},"不适合使用的场景：",[129,22542,22543,22546],{},[132,22544,22545],{},"子树本身就很简单，创建额外 Layer 的开销反而更大",[132,22547,22548],{},"子树和父级总是一起变化",[14,22550,22551,22554,22555,22558],{},[125,22552,22553],{},"调试工具："," 可以使用 ",[136,22556,22557],{},"debugRepaintRainbowEnabled = true"," 来可视化重绘区域。",[1890,22560],{},[18,22562,22564],{"id":22563},"_3-widget-element-renderobject","3. Widget \u002F Element \u002F RenderObject",[32,22566,22568],{"id":22567},"q31-详解-widgetelementrenderobject-三棵树的关系和职责","Q3.1: 详解 Widget、Element、RenderObject 三棵树的关系和职责",[14,22570,22571],{},[125,22572,2077],{},[186,22574,22577],{"className":22575,"code":22576,"language":1074},[1072],"Widget Tree          Element Tree           RenderObject Tree\n(配置\u002F蓝图)          (实例\u002F生命周期)         (布局\u002F绘制)\n\nContainer ──────► ComponentElement\n  │                    │\n  ├─ Padding ────► SingleChildRenderObjectElement ──► RenderPadding\n  │                    │\n  └─ Text ──────► LeafRenderObjectElement ──────────► RenderParagraph\n",[136,22578,22576],{"__ignoreMap":191},[36,22580,22581,22593],{},[39,22582,22583],{},[42,22584,22585,22587,22589,22591],{},[45,22586],{},[45,22588,11577],{},[45,22590,11587],{},[45,22592,11607],{},[52,22594,22595,22610,22626,22645],{},[42,22596,22597,22601,22604,22607],{},[57,22598,22599],{},[125,22600,19815],{},[57,22602,22603],{},"描述 UI 配置（不可变）",[57,22605,22606],{},"管理生命周期、持有状态",[57,22608,22609],{},"实际布局和绘制",[42,22611,22612,22617,22620,22623],{},[57,22613,22614],{},[125,22615,22616],{},"可变性",[57,22618,22619],{},"不可变（immutable）",[57,22621,22622],{},"可变、长寿命",[57,22624,22625],{},"可变",[42,22627,22628,22633,22639,22642],{},[57,22629,22630],{},[125,22631,22632],{},"创建频率",[57,22634,22635,22636,22638],{},"每次 ",[136,22637,11581],{}," 都可能重建",[57,22640,22641],{},"尽量复用（通过 canUpdate）",[57,22643,22644],{},"跟随 Element 复用",[42,22646,22647,22652,22655,22658],{},[57,22648,22649],{},[125,22650,22651],{},"类比",[57,22653,22654],{},"HTML 模板",[57,22656,22657],{},"虚拟 DOM 节点",[57,22659,22660],{},"浏览器 DOM 节点",[14,22662,22663],{},[125,22664,11570],{},[706,22666,22667,22674,22677,22699],{},[132,22668,22669,22670,22673],{},"Widget 是 ",[125,22671,22672],{},"轻量的配置对象","，描述\"我想要什么\"",[132,22675,22676],{},"Flutter 框架根据 Widget 创建或更新 Element",[132,22678,22679,22680,22683,22684],{},"Element 通过 ",[136,22681,22682],{},"Widget.canUpdate(oldWidget, newWidget)"," 判断是否复用：\n",[129,22685,22686,22696],{},[132,22687,22688,1437,22690,22692,22693],{},[136,22689,11598],{},[136,22691,11601],{}," 都相同 → 复用 Element，调用 ",[136,22694,22695],{},"update()",[132,22697,22698],{},"不同 → 销毁旧 Element，创建新的",[132,22700,22701],{},"RenderObjectElement 持有对应的 RenderObject，负责实际的布局和绘制",[14,22703,22704],{},[125,22705,22706],{},"为什么这样设计？",[129,22708,22709,22712,22715],{},[132,22710,22711],{},"Widget 不可变 + 频繁重建 → 声明式 UI 简洁易用",[132,22713,22714],{},"Element 复用 → 保持状态、避免不必要的重建",[132,22716,22717],{},"RenderObject 分离 → 只在真正需要时才重新布局和绘制",[1890,22719],{},[32,22721,22723,22724,22726],{"id":22722},"q32-key-的作用是什么什么时候必须使用-key","Q3.2: ",[136,22725,19622],{}," 的作用是什么？什么时候必须使用 Key？",[14,22728,22729],{},[125,22730,2077],{},[14,22732,22733,22734,22736],{},"Key 影响 Element 的复用策略。没有 Key 时，Flutter 只根据 ",[136,22735,11598],{}," 和在列表中的位置来匹配：",[186,22738,22740],{"className":11162,"code":22739,"language":11164,"meta":191,"style":191},"\u002F\u002F 问题场景：交换两个带状态的 Widget\n\u002F\u002F 没有 Key 时，Element 不会交换，只是更新配置 → 状态错乱\nColumn(\n  children: [\n    TodoTile(todo: todos[0]), \u002F\u002F Element 0 保持原位\n    TodoTile(todo: todos[1]), \u002F\u002F Element 1 保持原位\n    \u002F\u002F 交换 todos 后，Element 复用了，但状态（勾选状态）没跟着走\n  ],\n)\n\n\u002F\u002F 加 Key 后，Flutter 能正确匹配并移动 Element\nColumn(\n  children: [\n    TodoTile(key: ValueKey(todos[0].id), todo: todos[0]),\n    TodoTile(key: ValueKey(todos[1].id), todo: todos[1]),\n  ],\n)\n",[136,22741,22742,22747,22752,22756,22760,22765,22770,22775,22779,22783,22787,22792,22796,22800,22805,22810,22814],{"__ignoreMap":191},[195,22743,22744],{"class":197,"line":198},[195,22745,22746],{},"\u002F\u002F 问题场景：交换两个带状态的 Widget\n",[195,22748,22749],{"class":197,"line":230},[195,22750,22751],{},"\u002F\u002F 没有 Key 时，Element 不会交换，只是更新配置 → 状态错乱\n",[195,22753,22754],{"class":197,"line":251},[195,22755,22448],{},[195,22757,22758],{"class":197,"line":272},[195,22759,22453],{},[195,22761,22762],{"class":197,"line":293},[195,22763,22764],{},"    TodoTile(todo: todos[0]), \u002F\u002F Element 0 保持原位\n",[195,22766,22767],{"class":197,"line":562},[195,22768,22769],{},"    TodoTile(todo: todos[1]), \u002F\u002F Element 1 保持原位\n",[195,22771,22772],{"class":197,"line":583},[195,22773,22774],{},"    \u002F\u002F 交换 todos 后，Element 复用了，但状态（勾选状态）没跟着走\n",[195,22776,22777],{"class":197,"line":962},[195,22778,17864],{},[195,22780,22781],{"class":197,"line":968},[195,22782,410],{},[195,22784,22785],{"class":197,"line":1274},[195,22786,1241],{"emptyLinePlaceholder":757},[195,22788,22789],{"class":197,"line":1282},[195,22790,22791],{},"\u002F\u002F 加 Key 后，Flutter 能正确匹配并移动 Element\n",[195,22793,22794],{"class":197,"line":1295},[195,22795,22448],{},[195,22797,22798],{"class":197,"line":1309},[195,22799,22453],{},[195,22801,22802],{"class":197,"line":2246},[195,22803,22804],{},"    TodoTile(key: ValueKey(todos[0].id), todo: todos[0]),\n",[195,22806,22807],{"class":197,"line":1996},[195,22808,22809],{},"    TodoTile(key: ValueKey(todos[1].id), todo: todos[1]),\n",[195,22811,22812],{"class":197,"line":2257},[195,22813,17864],{},[195,22815,22816],{"class":197,"line":2262},[195,22817,410],{},[14,22819,22820],{},[125,22821,22822],{},"Key 的类型：",[36,22824,22825,22834],{},[39,22826,22827],{},[42,22828,22829,22832],{},[45,22830,22831],{},"Key 类型",[45,22833,3111],{},[52,22835,22836,22846,22856,22866,22876],{},[42,22837,22838,22843],{},[57,22839,22840],{},[136,22841,22842],{},"ValueKey",[57,22844,22845],{},"基于值（如 id）匹配",[42,22847,22848,22853],{},[57,22849,22850],{},[136,22851,22852],{},"ObjectKey",[57,22854,22855],{},"基于对象引用匹配",[42,22857,22858,22863],{},[57,22859,22860],{},[136,22861,22862],{},"UniqueKey",[57,22864,22865],{},"强制不复用（每次创建新 Element）",[42,22867,22868,22873],{},[57,22869,22870],{},[136,22871,22872],{},"GlobalKey",[57,22874,22875],{},"跨树访问 Element\u002FState，可跨父节点移动",[42,22877,22878,22883],{},[57,22879,22880],{},[136,22881,22882],{},"PageStorageKey",[57,22884,22885],{},"保存页面滚动位置等",[14,22887,22888],{},[125,22889,22890],{},"必须使用 Key 的场景：",[706,22892,22893,22904,22909,22917],{},[132,22894,22895,22898,22899,4728,22901,698],{},[125,22896,22897],{},"列表项会增删或重排","（如 ",[136,22900,18787],{},[136,22902,22903],{},"reorder",[132,22905,22906],{},[125,22907,22908],{},"同类型有状态 Widget 的顺序会变化",[132,22910,22911,22914,22915,698],{},[125,22912,22913],{},"需要在不同位置保持同一个 Widget 的状态","（",[136,22916,22872],{},[132,22918,22919],{},[125,22920,22921],{},"AnimatedSwitcher 等需要识别新旧子 Widget 的动画组件",[1890,22923],{},[32,22925,22927,22928,22931],{"id":22926},"q33-statefulwidget-的完整生命周期是怎样的","Q3.3: ",[136,22929,22930],{},"StatefulWidget"," 的完整生命周期是怎样的？",[14,22933,22934],{},[125,22935,2077],{},[186,22937,22940],{"className":22938,"code":22939,"language":1074},[1072],"createState()\n     ↓\ninitState()          ← 只调用一次，初始化状态\n     ↓\ndidChangeDependencies()  ← InheritedWidget 变化时也会调用\n     ↓\nbuild()              ← 返回 Widget 树\n     ↓\n ┌── didUpdateWidget()  ← 父 Widget 重建且 canUpdate 返回 true 时\n │        ↓\n └── build()\n     ↓\ndeactivate()         ← Element 从树中移除（可能是临时的）\n     ↓\ndispose()            ← 永久移除，释放资源\n",[136,22941,22939],{"__ignoreMap":191},[14,22943,22944],{},[125,22945,22946],{},"各阶段注意事项：",[186,22948,22950],{"className":11162,"code":22949,"language":11164,"meta":191,"style":191},"class _MyWidgetState extends State\u003CMyWidget> {\n  late final AnimationController _controller;\n\n  @override\n  void initState() {\n    super.initState(); \u002F\u002F 必须调用 super\n    _controller = AnimationController(vsync: this);\n    \u002F\u002F 不能在这里访问 InheritedWidget（context 还没准备好）\n    \u002F\u002F 不能在这里调用 setState()\n  }\n\n  @override\n  void didChangeDependencies() {\n    super.didChangeDependencies();\n    \u002F\u002F 可以安全地访问 InheritedWidget\n    final theme = Theme.of(context);\n  }\n\n  @override\n  void didUpdateWidget(covariant MyWidget oldWidget) {\n    super.didUpdateWidget(oldWidget);\n    \u002F\u002F 父 Widget 重建时调用，可以对比新旧 widget 的属性\n    if (widget.id != oldWidget.id) {\n      _fetchData();\n    }\n  }\n\n  @override\n  void dispose() {\n    _controller.dispose(); \u002F\u002F 释放资源，避免内存泄漏\n    super.dispose(); \u002F\u002F 必须调用 super\n  }\n}\n",[136,22951,22952,22957,22962,22966,22970,22975,22980,22985,22990,22995,22999,23003,23007,23012,23017,23022,23027,23031,23035,23039,23044,23049,23054,23059,23064,23068,23072,23076,23080,23085,23090,23095,23099],{"__ignoreMap":191},[195,22953,22954],{"class":197,"line":198},[195,22955,22956],{},"class _MyWidgetState extends State\u003CMyWidget> {\n",[195,22958,22959],{"class":197,"line":230},[195,22960,22961],{},"  late final AnimationController _controller;\n",[195,22963,22964],{"class":197,"line":251},[195,22965,1241],{"emptyLinePlaceholder":757},[195,22967,22968],{"class":197,"line":272},[195,22969,12090],{},[195,22971,22972],{"class":197,"line":293},[195,22973,22974],{},"  void initState() {\n",[195,22976,22977],{"class":197,"line":562},[195,22978,22979],{},"    super.initState(); \u002F\u002F 必须调用 super\n",[195,22981,22982],{"class":197,"line":583},[195,22983,22984],{},"    _controller = AnimationController(vsync: this);\n",[195,22986,22987],{"class":197,"line":962},[195,22988,22989],{},"    \u002F\u002F 不能在这里访问 InheritedWidget（context 还没准备好）\n",[195,22991,22992],{"class":197,"line":968},[195,22993,22994],{},"    \u002F\u002F 不能在这里调用 setState()\n",[195,22996,22997],{"class":197,"line":1274},[195,22998,965],{},[195,23000,23001],{"class":197,"line":1282},[195,23002,1241],{"emptyLinePlaceholder":757},[195,23004,23005],{"class":197,"line":1295},[195,23006,12090],{},[195,23008,23009],{"class":197,"line":1309},[195,23010,23011],{},"  void didChangeDependencies() {\n",[195,23013,23014],{"class":197,"line":2246},[195,23015,23016],{},"    super.didChangeDependencies();\n",[195,23018,23019],{"class":197,"line":1996},[195,23020,23021],{},"    \u002F\u002F 可以安全地访问 InheritedWidget\n",[195,23023,23024],{"class":197,"line":2257},[195,23025,23026],{},"    final theme = Theme.of(context);\n",[195,23028,23029],{"class":197,"line":2262},[195,23030,965],{},[195,23032,23033],{"class":197,"line":2267},[195,23034,1241],{"emptyLinePlaceholder":757},[195,23036,23037],{"class":197,"line":2273},[195,23038,12090],{},[195,23040,23041],{"class":197,"line":2033},[195,23042,23043],{},"  void didUpdateWidget(covariant MyWidget oldWidget) {\n",[195,23045,23046],{"class":197,"line":2284},[195,23047,23048],{},"    super.didUpdateWidget(oldWidget);\n",[195,23050,23051],{"class":197,"line":2460},[195,23052,23053],{},"    \u002F\u002F 父 Widget 重建时调用，可以对比新旧 widget 的属性\n",[195,23055,23056],{"class":197,"line":2466},[195,23057,23058],{},"    if (widget.id != oldWidget.id) {\n",[195,23060,23061],{"class":197,"line":2472},[195,23062,23063],{},"      _fetchData();\n",[195,23065,23066],{"class":197,"line":2780},[195,23067,2403],{},[195,23069,23070],{"class":197,"line":2786},[195,23071,965],{},[195,23073,23074],{"class":197,"line":2792},[195,23075,1241],{"emptyLinePlaceholder":757},[195,23077,23078],{"class":197,"line":2798},[195,23079,12090],{},[195,23081,23082],{"class":197,"line":2804},[195,23083,23084],{},"  void dispose() {\n",[195,23086,23087],{"class":197,"line":2810},[195,23088,23089],{},"    _controller.dispose(); \u002F\u002F 释放资源，避免内存泄漏\n",[195,23091,23092],{"class":197,"line":2815},[195,23093,23094],{},"    super.dispose(); \u002F\u002F 必须调用 super\n",[195,23096,23097],{"class":197,"line":2820},[195,23098,965],{},[195,23100,23101],{"class":197,"line":2825},[195,23102,552],{},[1890,23104],{},[18,23106,23108],{"id":23107},"_4-状态管理","4. 状态管理",[32,23110,23112],{"id":23111},"q41-对比-flutter-常用的状态管理方案","Q4.1: 对比 Flutter 常用的状态管理方案",[14,23114,23115],{},[125,23116,2077],{},[36,23118,23119,23132],{},[39,23120,23121],{},[42,23122,23123,23125,23128,23130],{},[45,23124,13777],{},[45,23126,23127],{},"核心理念",[45,23129,5863],{},[45,23131,3680],{},[52,23133,23134,23148,23162,23177,23191,23205],{},[42,23135,23136,23140,23143,23145],{},[57,23137,23138],{},[136,23139,19647],{},[57,23141,23142],{},"局部状态直接修改",[57,23144,5882],{},[57,23146,23147],{},"单个 Widget 内部状态",[42,23149,23150,23154,23157,23159],{},[57,23151,23152],{},[136,23153,17318],{},[57,23155,23156],{},"沿树向下传递数据",[57,23158,5898],{},[57,23160,23161],{},"框架基础，其他方案的底层",[42,23163,23164,23168,23171,23174],{},[57,23165,23166],{},[136,23167,17321],{},[57,23169,23170],{},"InheritedWidget 的封装",[57,23172,23173],{},"低-中",[57,23175,23176],{},"中小项目，官方推荐入门",[42,23178,23179,23183,23186,23188],{},[57,23180,23181],{},[136,23182,12152],{},[57,23184,23185],{},"编译安全、无 context 依赖",[57,23187,5898],{},[57,23189,23190],{},"中大型项目",[42,23192,23193,23197,23200,23202],{},[57,23194,23195],{},[136,23196,12149],{},[57,23198,23199],{},"事件驱动、流式响应",[57,23201,5914],{},[57,23203,23204],{},"大型项目、团队协作",[42,23206,23207,23212,23215,23217],{},[57,23208,23209],{},[136,23210,23211],{},"GetX",[57,23213,23214],{},"极简 API、响应式",[57,23216,5882],{},[57,23218,23219],{},"快速开发（但争议较大）",[14,23221,23222],{},[125,23223,23224],{},"BLoC 模式详解：",[186,23226,23228],{"className":11162,"code":23227,"language":11164,"meta":191,"style":191},"\u002F\u002F Event\nsealed class CounterEvent {}\nclass Increment extends CounterEvent {}\nclass Decrement extends CounterEvent {}\n\n\u002F\u002F State\nclass CounterState {\n  final int count;\n  const CounterState(this.count);\n}\n\n\u002F\u002F BLoC\nclass CounterBloc extends Bloc\u003CCounterEvent, CounterState> {\n  CounterBloc() : super(const CounterState(0)) {\n    on\u003CIncrement>((event, emit) => emit(CounterState(state.count + 1)));\n    on\u003CDecrement>((event, emit) => emit(CounterState(state.count - 1)));\n  }\n}\n\n\u002F\u002F UI\nBlocBuilder\u003CCounterBloc, CounterState>(\n  builder: (context, state) => Text('${state.count}'),\n)\n",[136,23229,23230,23235,23240,23245,23250,23254,23259,23264,23269,23274,23278,23282,23287,23292,23297,23302,23307,23311,23315,23319,23323,23328,23333],{"__ignoreMap":191},[195,23231,23232],{"class":197,"line":198},[195,23233,23234],{},"\u002F\u002F Event\n",[195,23236,23237],{"class":197,"line":230},[195,23238,23239],{},"sealed class CounterEvent {}\n",[195,23241,23242],{"class":197,"line":251},[195,23243,23244],{},"class Increment extends CounterEvent {}\n",[195,23246,23247],{"class":197,"line":272},[195,23248,23249],{},"class Decrement extends CounterEvent {}\n",[195,23251,23252],{"class":197,"line":293},[195,23253,1241],{"emptyLinePlaceholder":757},[195,23255,23256],{"class":197,"line":562},[195,23257,23258],{},"\u002F\u002F State\n",[195,23260,23261],{"class":197,"line":583},[195,23262,23263],{},"class CounterState {\n",[195,23265,23266],{"class":197,"line":962},[195,23267,23268],{},"  final int count;\n",[195,23270,23271],{"class":197,"line":968},[195,23272,23273],{},"  const CounterState(this.count);\n",[195,23275,23276],{"class":197,"line":1274},[195,23277,552],{},[195,23279,23280],{"class":197,"line":1282},[195,23281,1241],{"emptyLinePlaceholder":757},[195,23283,23284],{"class":197,"line":1295},[195,23285,23286],{},"\u002F\u002F BLoC\n",[195,23288,23289],{"class":197,"line":1309},[195,23290,23291],{},"class CounterBloc extends Bloc\u003CCounterEvent, CounterState> {\n",[195,23293,23294],{"class":197,"line":2246},[195,23295,23296],{},"  CounterBloc() : super(const CounterState(0)) {\n",[195,23298,23299],{"class":197,"line":1996},[195,23300,23301],{},"    on\u003CIncrement>((event, emit) => emit(CounterState(state.count + 1)));\n",[195,23303,23304],{"class":197,"line":2257},[195,23305,23306],{},"    on\u003CDecrement>((event, emit) => emit(CounterState(state.count - 1)));\n",[195,23308,23309],{"class":197,"line":2262},[195,23310,965],{},[195,23312,23313],{"class":197,"line":2267},[195,23314,552],{},[195,23316,23317],{"class":197,"line":2273},[195,23318,1241],{"emptyLinePlaceholder":757},[195,23320,23321],{"class":197,"line":2033},[195,23322,11928],{},[195,23324,23325],{"class":197,"line":2284},[195,23326,23327],{},"BlocBuilder\u003CCounterBloc, CounterState>(\n",[195,23329,23330],{"class":197,"line":2460},[195,23331,23332],{},"  builder: (context, state) => Text('${state.count}'),\n",[195,23334,23335],{"class":197,"line":2466},[195,23336,410],{},[14,23338,23339],{},[125,23340,23341],{},"Riverpod 示例：",[186,23343,23345],{"className":11162,"code":23344,"language":11164,"meta":191,"style":191},"\u002F\u002F 定义 Provider（全局，但编译安全）\nfinal counterProvider = StateNotifierProvider\u003CCounterNotifier, int>((ref) {\n  return CounterNotifier();\n});\n\nclass CounterNotifier extends StateNotifier\u003Cint> {\n  CounterNotifier() : super(0);\n  void increment() => state++;\n}\n\n\u002F\u002F UI（不需要 context）\nclass MyWidget extends ConsumerWidget {\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    final count = ref.watch(counterProvider);\n    return Text('$count');\n  }\n}\n",[136,23346,23347,23352,23357,23362,23366,23370,23374,23378,23382,23386,23390,23395,23400,23404,23408,23413,23418,23422],{"__ignoreMap":191},[195,23348,23349],{"class":197,"line":198},[195,23350,23351],{},"\u002F\u002F 定义 Provider（全局，但编译安全）\n",[195,23353,23354],{"class":197,"line":230},[195,23355,23356],{},"final counterProvider = StateNotifierProvider\u003CCounterNotifier, int>((ref) {\n",[195,23358,23359],{"class":197,"line":251},[195,23360,23361],{},"  return CounterNotifier();\n",[195,23363,23364],{"class":197,"line":272},[195,23365,11253],{},[195,23367,23368],{"class":197,"line":293},[195,23369,1241],{"emptyLinePlaceholder":757},[195,23371,23372],{"class":197,"line":562},[195,23373,18157],{},[195,23375,23376],{"class":197,"line":583},[195,23377,18162],{},[195,23379,23380],{"class":197,"line":962},[195,23381,18167],{},[195,23383,23384],{"class":197,"line":968},[195,23385,552],{},[195,23387,23388],{"class":197,"line":1274},[195,23389,1241],{"emptyLinePlaceholder":757},[195,23391,23392],{"class":197,"line":1282},[195,23393,23394],{},"\u002F\u002F UI（不需要 context）\n",[195,23396,23397],{"class":197,"line":1295},[195,23398,23399],{},"class MyWidget extends ConsumerWidget {\n",[195,23401,23402],{"class":197,"line":1309},[195,23403,12090],{},[195,23405,23406],{"class":197,"line":2246},[195,23407,12095],{},[195,23409,23410],{"class":197,"line":1996},[195,23411,23412],{},"    final count = ref.watch(counterProvider);\n",[195,23414,23415],{"class":197,"line":2257},[195,23416,23417],{},"    return Text('$count');\n",[195,23419,23420],{"class":197,"line":2262},[195,23421,965],{},[195,23423,23424],{"class":197,"line":2267},[195,23425,552],{},[1890,23427],{},[32,23429,23431,23432,23434,23435,23438],{"id":23430},"q42-inheritedwidget-是如何工作的为什么-themeofcontext-能获取到主题数据","Q4.2: ",[136,23433,17318],{}," 是如何工作的？为什么 ",[136,23436,23437],{},"Theme.of(context)"," 能获取到主题数据？",[14,23440,23441],{},[125,23442,2077],{},[14,23444,23445,23447],{},[136,23446,17318],{}," 是 Flutter 中沿 Widget 树向下传递数据的机制。",[14,23449,23450],{},[125,23451,23452],{},"工作原理：",[706,23454,23455,23460,23478],{},[132,23456,23457,23459],{},[136,23458,17318],{}," 存储在 Element 树中",[132,23461,23462,23463,23466,23467],{},"当子 Widget 调用 ",[136,23464,23465],{},"context.dependOnInheritedWidgetOfExactType\u003CT>()"," 时：\n",[129,23468,23469,23472],{},[132,23470,23471],{},"沿 Element 树向上查找最近的 T 类型的 InheritedElement",[132,23473,23474,23475],{},"将当前 Element ",[125,23476,23477],{},"注册为依赖者",[132,23479,23480,23481,23483],{},"当 InheritedWidget 更新时，所有注册的依赖者都会收到通知（触发 ",[136,23482,16649],{},"，然后 rebuild）",[186,23485,23487],{"className":11162,"code":23486,"language":11164,"meta":191,"style":191},"class MyTheme extends InheritedWidget {\n  final Color primaryColor;\n\n  const MyTheme({\n    required this.primaryColor,\n    required super.child,\n  });\n\n  \u002F\u002F 便捷方法\n  static MyTheme of(BuildContext context) {\n    return context.dependOnInheritedWidgetOfExactType\u003CMyTheme>()!;\n  }\n\n  @override\n  bool updateShouldNotify(MyTheme oldWidget) {\n    return primaryColor != oldWidget.primaryColor;\n    \u002F\u002F 返回 false 则不通知依赖者，即使自身重建了\n  }\n}\n",[136,23488,23489,23494,23499,23503,23508,23513,23518,23523,23527,23532,23537,23542,23546,23550,23554,23559,23564,23569,23573],{"__ignoreMap":191},[195,23490,23491],{"class":197,"line":198},[195,23492,23493],{},"class MyTheme extends InheritedWidget {\n",[195,23495,23496],{"class":197,"line":230},[195,23497,23498],{},"  final Color primaryColor;\n",[195,23500,23501],{"class":197,"line":251},[195,23502,1241],{"emptyLinePlaceholder":757},[195,23504,23505],{"class":197,"line":272},[195,23506,23507],{},"  const MyTheme({\n",[195,23509,23510],{"class":197,"line":293},[195,23511,23512],{},"    required this.primaryColor,\n",[195,23514,23515],{"class":197,"line":562},[195,23516,23517],{},"    required super.child,\n",[195,23519,23520],{"class":197,"line":583},[195,23521,23522],{},"  });\n",[195,23524,23525],{"class":197,"line":962},[195,23526,1241],{"emptyLinePlaceholder":757},[195,23528,23529],{"class":197,"line":968},[195,23530,23531],{},"  \u002F\u002F 便捷方法\n",[195,23533,23534],{"class":197,"line":1274},[195,23535,23536],{},"  static MyTheme of(BuildContext context) {\n",[195,23538,23539],{"class":197,"line":1282},[195,23540,23541],{},"    return context.dependOnInheritedWidgetOfExactType\u003CMyTheme>()!;\n",[195,23543,23544],{"class":197,"line":1295},[195,23545,965],{},[195,23547,23548],{"class":197,"line":1309},[195,23549,1241],{"emptyLinePlaceholder":757},[195,23551,23552],{"class":197,"line":2246},[195,23553,12090],{},[195,23555,23556],{"class":197,"line":1996},[195,23557,23558],{},"  bool updateShouldNotify(MyTheme oldWidget) {\n",[195,23560,23561],{"class":197,"line":2257},[195,23562,23563],{},"    return primaryColor != oldWidget.primaryColor;\n",[195,23565,23566],{"class":197,"line":2262},[195,23567,23568],{},"    \u002F\u002F 返回 false 则不通知依赖者，即使自身重建了\n",[195,23570,23571],{"class":197,"line":2267},[195,23572,965],{},[195,23574,23575],{"class":197,"line":2273},[195,23576,552],{},[14,23578,23579],{},[125,23580,23581],{},"关键区别：",[129,23583,23584,23590],{},[132,23585,23586,23589],{},[136,23587,23588],{},"dependOnInheritedWidgetOfExactType","：注册依赖，会自动重建",[132,23591,23592,23595],{},[136,23593,23594],{},"getInheritedWidgetOfExactType","：只读取，不注册依赖，不会自动重建",[14,23597,23598,23600,23601,23603,23604,23606],{},[136,23599,23437],{}," 内部就是调用了 ",[136,23602,23588],{},"，所以当 Theme 变化时，所有使用了 ",[136,23605,23437],{}," 的 Widget 都会自动重建。",[1890,23608],{},[18,23610,23612],{"id":23611},"_5-异步编程","5. 异步编程",[32,23614,23616],{"id":23615},"q51-dart-是单线程的那它是如何处理异步操作的","Q5.1: Dart 是单线程的，那它是如何处理异步操作的？",[14,23618,23619],{},[125,23620,2077],{},[14,23622,23623,23624,23627],{},"Dart 使用 ",[125,23625,23626],{},"事件循环（Event Loop）"," 机制，类似 JavaScript：",[186,23629,23632],{"className":23630,"code":23631,"language":1074},[1072],"┌─────────────────────────────────────────┐\n│             Event Loop                   │\n│                                         │\n│  ┌──────────────────────────────┐       │\n│  │  Microtask Queue（微任务队列）│       │\n│  │  - Future.then() 回调        │ ← 优先 │\n│  │  - scheduleMicrotask()       │       │\n│  └──────────────────────────────┘       │\n│                 ↓                        │\n│  ┌──────────────────────────────┐       │\n│  │  Event Queue（事件队列）      │       │\n│  │  - I\u002FO 完成回调              │       │\n│  │  - Timer 回调                │       │\n│  │  - UI 事件（点击、滑动）      │       │\n│  └──────────────────────────────┘       │\n└─────────────────────────────────────────┘\n",[136,23633,23631],{"__ignoreMap":191},[14,23635,23636],{},[125,23637,23638],{},"执行顺序：",[706,23640,23641,23644,23647,23650,23653],{},[132,23642,23643],{},"执行同步代码直到完成",[132,23645,23646],{},"清空所有 Microtask Queue",[132,23648,23649],{},"从 Event Queue 取出一个事件执行",[132,23651,23652],{},"再次清空 Microtask Queue",[132,23654,23655],{},"重复 3-4",[186,23657,23659],{"className":11162,"code":23658,"language":11164,"meta":191,"style":191},"void main() {\n  print('1'); \u002F\u002F 同步\n  Future(() => print('2'));             \u002F\u002F Event Queue\n  Future.microtask(() => print('3'));   \u002F\u002F Microtask Queue\n  Future(() => print('4'));             \u002F\u002F Event Queue\n  Future.microtask(() => print('5'));   \u002F\u002F Microtask Queue\n  print('6'); \u002F\u002F 同步\n}\n\u002F\u002F 输出顺序：1, 6, 3, 5, 2, 4\n",[136,23660,23661,23665,23670,23675,23680,23685,23690,23695,23699],{"__ignoreMap":191},[195,23662,23663],{"class":197,"line":198},[195,23664,11185],{},[195,23666,23667],{"class":197,"line":230},[195,23668,23669],{},"  print('1'); \u002F\u002F 同步\n",[195,23671,23672],{"class":197,"line":251},[195,23673,23674],{},"  Future(() => print('2'));             \u002F\u002F Event Queue\n",[195,23676,23677],{"class":197,"line":272},[195,23678,23679],{},"  Future.microtask(() => print('3'));   \u002F\u002F Microtask Queue\n",[195,23681,23682],{"class":197,"line":293},[195,23683,23684],{},"  Future(() => print('4'));             \u002F\u002F Event Queue\n",[195,23686,23687],{"class":197,"line":562},[195,23688,23689],{},"  Future.microtask(() => print('5'));   \u002F\u002F Microtask Queue\n",[195,23691,23692],{"class":197,"line":583},[195,23693,23694],{},"  print('6'); \u002F\u002F 同步\n",[195,23696,23697],{"class":197,"line":962},[195,23698,552],{},[195,23700,23701],{"class":197,"line":968},[195,23702,23703],{},"\u002F\u002F 输出顺序：1, 6, 3, 5, 2, 4\n",[14,23705,23706],{},[125,23707,23708],{},"真正的并行 → Isolate：",[186,23710,23712],{"className":11162,"code":23711,"language":11164,"meta":191,"style":191},"\u002F\u002F Isolate：独立内存空间，通过消息传递通信\nfinal result = await Isolate.run(() {\n  \u002F\u002F 在独立 Isolate 中执行 CPU 密集型任务\n  return heavyComputation();\n});\n\n\u002F\u002F compute() 是 Flutter 提供的便捷方法\nfinal result = await compute(parseJson, rawData);\n",[136,23713,23714,23719,23723,23728,23733,23737,23741,23746],{"__ignoreMap":191},[195,23715,23716],{"class":197,"line":198},[195,23717,23718],{},"\u002F\u002F Isolate：独立内存空间，通过消息传递通信\n",[195,23720,23721],{"class":197,"line":230},[195,23722,11243],{},[195,23724,23725],{"class":197,"line":251},[195,23726,23727],{},"  \u002F\u002F 在独立 Isolate 中执行 CPU 密集型任务\n",[195,23729,23730],{"class":197,"line":272},[195,23731,23732],{},"  return heavyComputation();\n",[195,23734,23735],{"class":197,"line":293},[195,23736,11253],{},[195,23738,23739],{"class":197,"line":562},[195,23740,1241],{"emptyLinePlaceholder":757},[195,23742,23743],{"class":197,"line":583},[195,23744,23745],{},"\u002F\u002F compute() 是 Flutter 提供的便捷方法\n",[195,23747,23748],{"class":197,"line":962},[195,23749,23750],{},"final result = await compute(parseJson, rawData);\n",[1890,23752],{},[32,23754,23756,23757,1437,23760,21468,23763,23766],{"id":23755},"q52-future-和-stream-的区别是什么streamcontroller-怎么用","Q5.2: ",[136,23758,23759],{},"Future",[136,23761,23762],{},"Stream",[136,23764,23765],{},"StreamController"," 怎么用？",[14,23768,23769],{},[125,23770,2077],{},[36,23772,23773,23783],{},[39,23774,23775],{},[42,23776,23777,23779,23781],{},[45,23778],{},[45,23780,23759],{},[45,23782,23762],{},[52,23784,23785,23796,23812],{},[42,23786,23787,23790,23793],{},[57,23788,23789],{},"值的数量",[57,23791,23792],{},"单个异步值",[57,23794,23795],{},"多个异步值序列",[42,23797,23798,23800,23806],{},[57,23799,22651],{},[57,23801,23802,23805],{},[136,23803,23804],{},"Promise"," (JS)",[57,23807,23808,23811],{},[136,23809,23810],{},"Observable"," (RxJS)",[42,23813,23814,23817,23820],{},[57,23815,23816],{},"完成",[57,23818,23819],{},"一次性完成或失败",[57,23821,23822],{},"可持续发送数据，直到关闭",[14,23824,23825],{},[125,23826,23827],{},"Stream 的两种类型：",[186,23829,23831],{"className":11162,"code":23830,"language":11164,"meta":191,"style":191},"\u002F\u002F 1. 单订阅流（Single-subscription）：只能 listen 一次\nfinal controller = StreamController\u003Cint>();\n\n\u002F\u002F 2. 广播流（Broadcast）：可以多次 listen\nfinal controller = StreamController\u003Cint>.broadcast();\n",[136,23832,23833,23838,23843,23847,23852],{"__ignoreMap":191},[195,23834,23835],{"class":197,"line":198},[195,23836,23837],{},"\u002F\u002F 1. 单订阅流（Single-subscription）：只能 listen 一次\n",[195,23839,23840],{"class":197,"line":230},[195,23841,23842],{},"final controller = StreamController\u003Cint>();\n",[195,23844,23845],{"class":197,"line":251},[195,23846,1241],{"emptyLinePlaceholder":757},[195,23848,23849],{"class":197,"line":272},[195,23850,23851],{},"\u002F\u002F 2. 广播流（Broadcast）：可以多次 listen\n",[195,23853,23854],{"class":197,"line":293},[195,23855,23856],{},"final controller = StreamController\u003Cint>.broadcast();\n",[14,23858,23859],{},[125,23860,23861],{},"StreamController 使用：",[186,23863,23865],{"className":11162,"code":23864,"language":11164,"meta":191,"style":191},"class CounterService {\n  final _controller = StreamController\u003Cint>.broadcast();\n  int _count = 0;\n\n  Stream\u003Cint> get countStream => _controller.stream;\n\n  void increment() {\n    _count++;\n    _controller.sink.add(_count);  \u002F\u002F 发送数据\n  }\n\n  void dispose() {\n    _controller.close(); \u002F\u002F 必须关闭，否则内存泄漏\n  }\n}\n\n\u002F\u002F 在 Widget 中使用\nStreamBuilder\u003Cint>(\n  stream: counterService.countStream,\n  initialData: 0,\n  builder: (context, snapshot) {\n    if (snapshot.hasError) return Text('Error: ${snapshot.error}');\n    return Text('Count: ${snapshot.data}');\n  },\n)\n",[136,23866,23867,23872,23877,23882,23886,23891,23895,23900,23905,23910,23914,23918,23922,23927,23931,23935,23939,23944,23949,23954,23959,23964,23969,23974,23978],{"__ignoreMap":191},[195,23868,23869],{"class":197,"line":198},[195,23870,23871],{},"class CounterService {\n",[195,23873,23874],{"class":197,"line":230},[195,23875,23876],{},"  final _controller = StreamController\u003Cint>.broadcast();\n",[195,23878,23879],{"class":197,"line":251},[195,23880,23881],{},"  int _count = 0;\n",[195,23883,23884],{"class":197,"line":272},[195,23885,1241],{"emptyLinePlaceholder":757},[195,23887,23888],{"class":197,"line":293},[195,23889,23890],{},"  Stream\u003Cint> get countStream => _controller.stream;\n",[195,23892,23893],{"class":197,"line":562},[195,23894,1241],{"emptyLinePlaceholder":757},[195,23896,23897],{"class":197,"line":583},[195,23898,23899],{},"  void increment() {\n",[195,23901,23902],{"class":197,"line":962},[195,23903,23904],{},"    _count++;\n",[195,23906,23907],{"class":197,"line":968},[195,23908,23909],{},"    _controller.sink.add(_count);  \u002F\u002F 发送数据\n",[195,23911,23912],{"class":197,"line":1274},[195,23913,965],{},[195,23915,23916],{"class":197,"line":1282},[195,23917,1241],{"emptyLinePlaceholder":757},[195,23919,23920],{"class":197,"line":1295},[195,23921,23084],{},[195,23923,23924],{"class":197,"line":1309},[195,23925,23926],{},"    _controller.close(); \u002F\u002F 必须关闭，否则内存泄漏\n",[195,23928,23929],{"class":197,"line":2246},[195,23930,965],{},[195,23932,23933],{"class":197,"line":1996},[195,23934,552],{},[195,23936,23937],{"class":197,"line":2257},[195,23938,1241],{"emptyLinePlaceholder":757},[195,23940,23941],{"class":197,"line":2262},[195,23942,23943],{},"\u002F\u002F 在 Widget 中使用\n",[195,23945,23946],{"class":197,"line":2267},[195,23947,23948],{},"StreamBuilder\u003Cint>(\n",[195,23950,23951],{"class":197,"line":2273},[195,23952,23953],{},"  stream: counterService.countStream,\n",[195,23955,23956],{"class":197,"line":2033},[195,23957,23958],{},"  initialData: 0,\n",[195,23960,23961],{"class":197,"line":2284},[195,23962,23963],{},"  builder: (context, snapshot) {\n",[195,23965,23966],{"class":197,"line":2460},[195,23967,23968],{},"    if (snapshot.hasError) return Text('Error: ${snapshot.error}');\n",[195,23970,23971],{"class":197,"line":2466},[195,23972,23973],{},"    return Text('Count: ${snapshot.data}');\n",[195,23975,23976],{"class":197,"line":2472},[195,23977,11963],{},[195,23979,23980],{"class":197,"line":2780},[195,23981,410],{},[14,23983,23984],{},[125,23985,23986],{},"Stream 常用变换操作：",[186,23988,23990],{"className":11162,"code":23989,"language":11164,"meta":191,"style":191},"stream\n  .where((value) => value > 0)        \u002F\u002F 过滤\n  .map((value) => value * 2)          \u002F\u002F 映射\n  .distinct()                         \u002F\u002F 去重\n  .debounceTime(Duration(ms: 300))    \u002F\u002F 防抖（需 rxdart）\n  .listen((value) => print(value));\n",[136,23991,23992,23997,24002,24007,24012,24017],{"__ignoreMap":191},[195,23993,23994],{"class":197,"line":198},[195,23995,23996],{},"stream\n",[195,23998,23999],{"class":197,"line":230},[195,24000,24001],{},"  .where((value) => value > 0)        \u002F\u002F 过滤\n",[195,24003,24004],{"class":197,"line":251},[195,24005,24006],{},"  .map((value) => value * 2)          \u002F\u002F 映射\n",[195,24008,24009],{"class":197,"line":272},[195,24010,24011],{},"  .distinct()                         \u002F\u002F 去重\n",[195,24013,24014],{"class":197,"line":293},[195,24015,24016],{},"  .debounceTime(Duration(ms: 300))    \u002F\u002F 防抖（需 rxdart）\n",[195,24018,24019],{"class":197,"line":562},[195,24020,24021],{},"  .listen((value) => print(value));\n",[1890,24023],{},[32,24025,24027,24028,1437,24031,24034],{"id":24026},"q53-解释-async-和-yield-的用法","Q5.3: 解释 ",[136,24029,24030],{},"async*",[136,24032,24033],{},"yield"," 的用法",[14,24036,24037],{},[125,24038,2077],{},[14,24040,24041,24043],{},[136,24042,24030],{}," 用于创建异步生成器，返回一个 Stream：",[186,24045,24047],{"className":11162,"code":24046,"language":11164,"meta":191,"style":191},"\u002F\u002F 异步生成器\nStream\u003Cint> countDown(int from) async* {\n  for (var i = from; i >= 0; i--) {\n    await Future.delayed(Duration(seconds: 1));\n    yield i; \u002F\u002F 每次 yield 发送一个值到 Stream\n  }\n}\n\n\u002F\u002F yield* 委托到另一个 Stream\nStream\u003Cint> fullSequence() async* {\n  yield* countDown(3);   \u002F\u002F 先倒计时\n  yield -1;              \u002F\u002F 然后发送 -1\n  yield* countDown(2);   \u002F\u002F 再倒计时\n}\n\n\u002F\u002F 同步生成器用 sync* + Iterable\nIterable\u003Cint> range(int start, int end) sync* {\n  for (var i = start; i \u003C= end; i++) {\n    yield i;\n  }\n}\n",[136,24048,24049,24054,24059,24064,24069,24074,24078,24082,24086,24091,24096,24101,24106,24111,24115,24119,24124,24129,24134,24139,24143],{"__ignoreMap":191},[195,24050,24051],{"class":197,"line":198},[195,24052,24053],{},"\u002F\u002F 异步生成器\n",[195,24055,24056],{"class":197,"line":230},[195,24057,24058],{},"Stream\u003Cint> countDown(int from) async* {\n",[195,24060,24061],{"class":197,"line":251},[195,24062,24063],{},"  for (var i = from; i >= 0; i--) {\n",[195,24065,24066],{"class":197,"line":272},[195,24067,24068],{},"    await Future.delayed(Duration(seconds: 1));\n",[195,24070,24071],{"class":197,"line":293},[195,24072,24073],{},"    yield i; \u002F\u002F 每次 yield 发送一个值到 Stream\n",[195,24075,24076],{"class":197,"line":562},[195,24077,965],{},[195,24079,24080],{"class":197,"line":583},[195,24081,552],{},[195,24083,24084],{"class":197,"line":962},[195,24085,1241],{"emptyLinePlaceholder":757},[195,24087,24088],{"class":197,"line":968},[195,24089,24090],{},"\u002F\u002F yield* 委托到另一个 Stream\n",[195,24092,24093],{"class":197,"line":1274},[195,24094,24095],{},"Stream\u003Cint> fullSequence() async* {\n",[195,24097,24098],{"class":197,"line":1282},[195,24099,24100],{},"  yield* countDown(3);   \u002F\u002F 先倒计时\n",[195,24102,24103],{"class":197,"line":1295},[195,24104,24105],{},"  yield -1;              \u002F\u002F 然后发送 -1\n",[195,24107,24108],{"class":197,"line":1309},[195,24109,24110],{},"  yield* countDown(2);   \u002F\u002F 再倒计时\n",[195,24112,24113],{"class":197,"line":2246},[195,24114,552],{},[195,24116,24117],{"class":197,"line":1996},[195,24118,1241],{"emptyLinePlaceholder":757},[195,24120,24121],{"class":197,"line":2257},[195,24122,24123],{},"\u002F\u002F 同步生成器用 sync* + Iterable\n",[195,24125,24126],{"class":197,"line":2262},[195,24127,24128],{},"Iterable\u003Cint> range(int start, int end) sync* {\n",[195,24130,24131],{"class":197,"line":2267},[195,24132,24133],{},"  for (var i = start; i \u003C= end; i++) {\n",[195,24135,24136],{"class":197,"line":2273},[195,24137,24138],{},"    yield i;\n",[195,24140,24141],{"class":197,"line":2033},[195,24142,965],{},[195,24144,24145],{"class":197,"line":2284},[195,24146,552],{},[1890,24148],{},[18,24150,24152],{"id":24151},"_6-性能优化","6. 性能优化",[32,24154,24156],{"id":24155},"q61-flutter-性能优化有哪些关键策略","Q6.1: Flutter 性能优化有哪些关键策略？",[14,24158,24159],{},[125,24160,2077],{},[14,24162,24163],{},[125,24164,24165],{},"1. 减少不必要的 rebuild：",[186,24167,24169],{"className":11162,"code":24168,"language":11164,"meta":191,"style":191},"\u002F\u002F ❌ 整棵树在动画每帧都重建\nAnimatedBuilder(\n  animation: _controller,\n  builder: (context, child) {\n    return Column(\n      children: [\n        Transform.rotate(\n          angle: _controller.value * 2 * pi,\n          child: const Icon(Icons.refresh), \u002F\u002F 每帧都重建\n        ),\n        const ExpensiveWidget(), \u002F\u002F 每帧都重建！\n      ],\n    );\n  },\n)\n\n\u002F\u002F ✅ 使用 child 参数，ExpensiveWidget 只构建一次\nAnimatedBuilder(\n  animation: _controller,\n  child: const ExpensiveWidget(), \u002F\u002F 只构建一次，缓存在这里\n  builder: (context, child) {\n    return Column(\n      children: [\n        Transform.rotate(\n          angle: _controller.value * 2 * pi,\n          child: const Icon(Icons.refresh),\n        ),\n        child!, \u002F\u002F 复用缓存的 Widget\n      ],\n    );\n  },\n)\n",[136,24170,24171,24176,24180,24185,24190,24195,24200,24205,24210,24215,24220,24225,24230,24235,24239,24243,24247,24252,24256,24260,24265,24269,24273,24277,24281,24285,24290,24294,24299,24303,24307,24311],{"__ignoreMap":191},[195,24172,24173],{"class":197,"line":198},[195,24174,24175],{},"\u002F\u002F ❌ 整棵树在动画每帧都重建\n",[195,24177,24178],{"class":197,"line":230},[195,24179,12781],{},[195,24181,24182],{"class":197,"line":251},[195,24183,24184],{},"  animation: _controller,\n",[195,24186,24187],{"class":197,"line":272},[195,24188,24189],{},"  builder: (context, child) {\n",[195,24191,24192],{"class":197,"line":293},[195,24193,24194],{},"    return Column(\n",[195,24196,24197],{"class":197,"line":562},[195,24198,24199],{},"      children: [\n",[195,24201,24202],{"class":197,"line":583},[195,24203,24204],{},"        Transform.rotate(\n",[195,24206,24207],{"class":197,"line":962},[195,24208,24209],{},"          angle: _controller.value * 2 * pi,\n",[195,24211,24212],{"class":197,"line":968},[195,24213,24214],{},"          child: const Icon(Icons.refresh), \u002F\u002F 每帧都重建\n",[195,24216,24217],{"class":197,"line":1274},[195,24218,24219],{},"        ),\n",[195,24221,24222],{"class":197,"line":1282},[195,24223,24224],{},"        const ExpensiveWidget(), \u002F\u002F 每帧都重建！\n",[195,24226,24227],{"class":197,"line":1295},[195,24228,24229],{},"      ],\n",[195,24231,24232],{"class":197,"line":1309},[195,24233,24234],{},"    );\n",[195,24236,24237],{"class":197,"line":2246},[195,24238,11963],{},[195,24240,24241],{"class":197,"line":1996},[195,24242,410],{},[195,24244,24245],{"class":197,"line":2257},[195,24246,1241],{"emptyLinePlaceholder":757},[195,24248,24249],{"class":197,"line":2262},[195,24250,24251],{},"\u002F\u002F ✅ 使用 child 参数，ExpensiveWidget 只构建一次\n",[195,24253,24254],{"class":197,"line":2267},[195,24255,12781],{},[195,24257,24258],{"class":197,"line":2273},[195,24259,24184],{},[195,24261,24262],{"class":197,"line":2033},[195,24263,24264],{},"  child: const ExpensiveWidget(), \u002F\u002F 只构建一次，缓存在这里\n",[195,24266,24267],{"class":197,"line":2284},[195,24268,24189],{},[195,24270,24271],{"class":197,"line":2460},[195,24272,24194],{},[195,24274,24275],{"class":197,"line":2466},[195,24276,24199],{},[195,24278,24279],{"class":197,"line":2472},[195,24280,24204],{},[195,24282,24283],{"class":197,"line":2780},[195,24284,24209],{},[195,24286,24287],{"class":197,"line":2786},[195,24288,24289],{},"          child: const Icon(Icons.refresh),\n",[195,24291,24292],{"class":197,"line":2792},[195,24293,24219],{},[195,24295,24296],{"class":197,"line":2798},[195,24297,24298],{},"        child!, \u002F\u002F 复用缓存的 Widget\n",[195,24300,24301],{"class":197,"line":2804},[195,24302,24229],{},[195,24304,24305],{"class":197,"line":2810},[195,24306,24234],{},[195,24308,24309],{"class":197,"line":2815},[195,24310,11963],{},[195,24312,24313],{"class":197,"line":2820},[195,24314,410],{},[14,24316,24317],{},[125,24318,24319,24320,24322],{},"2. 使用 ",[136,24321,392],{}," 构造函数：",[186,24324,24326],{"className":11162,"code":24325,"language":11164,"meta":191,"style":191},"\u002F\u002F ✅ const Widget 在编译时创建，可以被复用\nconst Text('Hello')\nconst SizedBox(height: 16)\nconst EdgeInsets.all(8)\n",[136,24327,24328,24333,24338,24343],{"__ignoreMap":191},[195,24329,24330],{"class":197,"line":198},[195,24331,24332],{},"\u002F\u002F ✅ const Widget 在编译时创建，可以被复用\n",[195,24334,24335],{"class":197,"line":230},[195,24336,24337],{},"const Text('Hello')\n",[195,24339,24340],{"class":197,"line":251},[195,24341,24342],{},"const SizedBox(height: 16)\n",[195,24344,24345],{"class":197,"line":272},[195,24346,12710],{},[14,24348,24349],{},[125,24350,24351],{},"3. ListView 优化：",[186,24353,24355],{"className":11162,"code":24354,"language":11164,"meta":191,"style":191},"\u002F\u002F ❌ 一次性构建所有子项\nListView(children: items.map((i) => ItemWidget(i)).toList())\n\n\u002F\u002F ✅ 懒加载，只构建可见区域\nListView.builder(\n  itemCount: items.length,\n  itemBuilder: (context, index) => ItemWidget(items[index]),\n)\n\n\u002F\u002F ✅ 固定高度项可以跳过布局计算\nListView.builder(\n  itemExtent: 72.0, \u002F\u002F 已知每项高度\n  itemBuilder: (context, index) => ItemWidget(items[index]),\n)\n",[136,24356,24357,24362,24367,24371,24376,24380,24384,24389,24393,24397,24402,24406,24411,24415],{"__ignoreMap":191},[195,24358,24359],{"class":197,"line":198},[195,24360,24361],{},"\u002F\u002F ❌ 一次性构建所有子项\n",[195,24363,24364],{"class":197,"line":230},[195,24365,24366],{},"ListView(children: items.map((i) => ItemWidget(i)).toList())\n",[195,24368,24369],{"class":197,"line":251},[195,24370,1241],{"emptyLinePlaceholder":757},[195,24372,24373],{"class":197,"line":272},[195,24374,24375],{},"\u002F\u002F ✅ 懒加载，只构建可见区域\n",[195,24377,24378],{"class":197,"line":293},[195,24379,12729],{},[195,24381,24382],{"class":197,"line":562},[195,24383,16942],{},[195,24385,24386],{"class":197,"line":583},[195,24387,24388],{},"  itemBuilder: (context, index) => ItemWidget(items[index]),\n",[195,24390,24391],{"class":197,"line":962},[195,24392,410],{},[195,24394,24395],{"class":197,"line":968},[195,24396,1241],{"emptyLinePlaceholder":757},[195,24398,24399],{"class":197,"line":1274},[195,24400,24401],{},"\u002F\u002F ✅ 固定高度项可以跳过布局计算\n",[195,24403,24404],{"class":197,"line":1282},[195,24405,12729],{},[195,24407,24408],{"class":197,"line":1295},[195,24409,24410],{},"  itemExtent: 72.0, \u002F\u002F 已知每项高度\n",[195,24412,24413],{"class":197,"line":1309},[195,24414,24388],{},[195,24416,24417],{"class":197,"line":2246},[195,24418,410],{},[14,24420,24421],{},[125,24422,24423],{},"4. 图片优化：",[186,24425,24427],{"className":11162,"code":24426,"language":11164,"meta":191,"style":191},"\u002F\u002F 指定 cacheWidth\u002FcacheHeight，避免解码全分辨率\nImage.asset(\n  'assets\u002Flarge_image.png',\n  cacheWidth: 200, \u002F\u002F 解码到指定大小，节省内存\n)\n",[136,24428,24429,24434,24439,24444,24449],{"__ignoreMap":191},[195,24430,24431],{"class":197,"line":198},[195,24432,24433],{},"\u002F\u002F 指定 cacheWidth\u002FcacheHeight，避免解码全分辨率\n",[195,24435,24436],{"class":197,"line":230},[195,24437,24438],{},"Image.asset(\n",[195,24440,24441],{"class":197,"line":251},[195,24442,24443],{},"  'assets\u002Flarge_image.png',\n",[195,24445,24446],{"class":197,"line":272},[195,24447,24448],{},"  cacheWidth: 200, \u002F\u002F 解码到指定大小，节省内存\n",[195,24450,24451],{"class":197,"line":293},[195,24452,410],{},[14,24454,24455],{},[125,24456,24457],{},"5. 合理拆分 Widget：",[186,24459,24461],{"className":11162,"code":24460,"language":11164,"meta":191,"style":191},"\u002F\u002F ❌ 一个巨大的 build 方法\nWidget build(BuildContext context) {\n  return Column(children: [\n    \u002F\u002F 200 行 UI 代码...\n  ]);\n}\n\n\u002F\u002F ✅ 拆分为独立的 Widget 类（不是方法！）\n\u002F\u002F Widget 类可以独立 rebuild，方法提取不行\nclass HeaderSection extends StatelessWidget { ... }\nclass ContentSection extends StatelessWidget { ... }\n",[136,24462,24463,24468,24473,24478,24483,24488,24492,24496,24501,24506,24511],{"__ignoreMap":191},[195,24464,24465],{"class":197,"line":198},[195,24466,24467],{},"\u002F\u002F ❌ 一个巨大的 build 方法\n",[195,24469,24470],{"class":197,"line":230},[195,24471,24472],{},"Widget build(BuildContext context) {\n",[195,24474,24475],{"class":197,"line":251},[195,24476,24477],{},"  return Column(children: [\n",[195,24479,24480],{"class":197,"line":272},[195,24481,24482],{},"    \u002F\u002F 200 行 UI 代码...\n",[195,24484,24485],{"class":197,"line":293},[195,24486,24487],{},"  ]);\n",[195,24489,24490],{"class":197,"line":562},[195,24491,552],{},[195,24493,24494],{"class":197,"line":583},[195,24495,1241],{"emptyLinePlaceholder":757},[195,24497,24498],{"class":197,"line":962},[195,24499,24500],{},"\u002F\u002F ✅ 拆分为独立的 Widget 类（不是方法！）\n",[195,24502,24503],{"class":197,"line":968},[195,24504,24505],{},"\u002F\u002F Widget 类可以独立 rebuild，方法提取不行\n",[195,24507,24508],{"class":197,"line":1274},[195,24509,24510],{},"class HeaderSection extends StatelessWidget { ... }\n",[195,24512,24513],{"class":197,"line":1282},[195,24514,24515],{},"class ContentSection extends StatelessWidget { ... }\n",[1890,24517],{},[32,24519,24521],{"id":24520},"q62-如何使用-devtools-诊断性能问题","Q6.2: 如何使用 DevTools 诊断性能问题？",[14,24523,24524],{},[125,24525,2077],{},[14,24527,24528],{},[125,24529,24530],{},"Performance Overlay：",[186,24532,24534],{"className":11162,"code":24533,"language":11164,"meta":191,"style":191},"MaterialApp(\n  showPerformanceOverlay: true, \u002F\u002F 显示 GPU\u002FUI 线程帧率\n)\n",[136,24535,24536,24541,24546],{"__ignoreMap":191},[195,24537,24538],{"class":197,"line":198},[195,24539,24540],{},"MaterialApp(\n",[195,24542,24543],{"class":197,"line":230},[195,24544,24545],{},"  showPerformanceOverlay: true, \u002F\u002F 显示 GPU\u002FUI 线程帧率\n",[195,24547,24548],{"class":197,"line":251},[195,24549,410],{},[14,24551,24552],{},"两行图表：",[129,24554,24555,24558,24561],{},[132,24556,24557],{},"上方（UI Thread）：build\u002Flayout\u002Fpaint 耗时",[132,24559,24560],{},"下方（Raster Thread）：合成和光栅化耗时",[132,24562,24563],{},"红色柱体 = 该帧超过 16ms，发生掉帧",[14,24565,24566],{},[125,24567,24568],{},"Flutter DevTools 关键面板：",[36,24570,24571,24580],{},[39,24572,24573],{},[42,24574,24575,24578],{},[45,24576,24577],{},"面板",[45,24579,3111],{},[52,24581,24582,24590,24598,24606,24614],{},[42,24583,24584,24587],{},[57,24585,24586],{},"Performance",[57,24588,24589],{},"帧耗时分析，识别卡顿帧",[42,24591,24592,24595],{},[57,24593,24594],{},"CPU Profiler",[57,24596,24597],{},"函数级别耗时分析",[42,24599,24600,24603],{},[57,24601,24602],{},"Memory",[57,24604,24605],{},"内存分配、泄漏检测",[42,24607,24608,24611],{},[57,24609,24610],{},"Widget Inspector",[57,24612,24613],{},"Widget 树结构、rebuild 统计",[42,24615,24616,24618],{},[57,24617,6017],{},[57,24619,24620],{},"HTTP 请求监控",[14,24622,24623],{},[125,24624,24625],{},"常用调试标志：",[186,24627,24629],{"className":11162,"code":24628,"language":11164,"meta":191,"style":191},"import 'package:flutter\u002Frendering.dart';\n\n\u002F\u002F 显示重绘区域（彩虹色边框）\ndebugRepaintRainbowEnabled = true;\n\n\u002F\u002F 显示布局边界\ndebugPaintSizeEnabled = true;\n\n\u002F\u002F 打印重建的 Widget\ndebugPrintRebuildDirtyWidgets = true;\n",[136,24630,24631,24636,24640,24645,24650,24654,24659,24664,24668,24673],{"__ignoreMap":191},[195,24632,24633],{"class":197,"line":198},[195,24634,24635],{},"import 'package:flutter\u002Frendering.dart';\n",[195,24637,24638],{"class":197,"line":230},[195,24639,1241],{"emptyLinePlaceholder":757},[195,24641,24642],{"class":197,"line":251},[195,24643,24644],{},"\u002F\u002F 显示重绘区域（彩虹色边框）\n",[195,24646,24647],{"class":197,"line":272},[195,24648,24649],{},"debugRepaintRainbowEnabled = true;\n",[195,24651,24652],{"class":197,"line":293},[195,24653,1241],{"emptyLinePlaceholder":757},[195,24655,24656],{"class":197,"line":562},[195,24657,24658],{},"\u002F\u002F 显示布局边界\n",[195,24660,24661],{"class":197,"line":583},[195,24662,24663],{},"debugPaintSizeEnabled = true;\n",[195,24665,24666],{"class":197,"line":962},[195,24667,1241],{"emptyLinePlaceholder":757},[195,24669,24670],{"class":197,"line":968},[195,24671,24672],{},"\u002F\u002F 打印重建的 Widget\n",[195,24674,24675],{"class":197,"line":1274},[195,24676,24677],{},"debugPrintRebuildDirtyWidgets = true;\n",[14,24679,24680],{},[125,24681,24682],{},"Timeline 使用：",[186,24684,24686],{"className":11162,"code":24685,"language":11164,"meta":191,"style":191},"import 'dart:developer';\n\nTimeline.startSync('MyExpensiveOperation');\n\u002F\u002F ... 耗时操作\nTimeline.finishSync();\n",[136,24687,24688,24693,24697,24702,24707],{"__ignoreMap":191},[195,24689,24690],{"class":197,"line":198},[195,24691,24692],{},"import 'dart:developer';\n",[195,24694,24695],{"class":197,"line":230},[195,24696,1241],{"emptyLinePlaceholder":757},[195,24698,24699],{"class":197,"line":251},[195,24700,24701],{},"Timeline.startSync('MyExpensiveOperation');\n",[195,24703,24704],{"class":197,"line":272},[195,24705,24706],{},"\u002F\u002F ... 耗时操作\n",[195,24708,24709],{"class":197,"line":293},[195,24710,24711],{},"Timeline.finishSync();\n",[1890,24713],{},[18,24715,24717],{"id":24716},"_7-平台通信platform-channel","7. 平台通信（Platform Channel）",[32,24719,24721],{"id":24720},"q71-flutter-的-platform-channel-有哪几种类型分别适用于什么场景","Q7.1: Flutter 的 Platform Channel 有哪几种类型？分别适用于什么场景？",[14,24723,24724],{},[125,24725,2077],{},[186,24727,24730],{"className":24728,"code":24729,"language":1074},[1072],"Flutter (Dart)  ←──── Platform Channel ────→  Native (iOS\u002FAndroid)\n",[136,24731,24729],{"__ignoreMap":191},[36,24733,24734,24746],{},[39,24735,24736],{},[42,24737,24738,24741,24744],{},[45,24739,24740],{},"Channel 类型",[45,24742,24743],{},"通信方式",[45,24745,3680],{},[52,24747,24748,24761,24774],{},[42,24749,24750,24755,24758],{},[57,24751,24752],{},[136,24753,24754],{},"MethodChannel",[57,24756,24757],{},"异步方法调用（请求-响应）",[57,24759,24760],{},"调用原生 API（相机、蓝牙等）",[42,24762,24763,24768,24771],{},[57,24764,24765],{},[136,24766,24767],{},"EventChannel",[57,24769,24770],{},"原生向 Dart 持续发送事件流",[57,24772,24773],{},"传感器数据、位置更新",[42,24775,24776,24781,24784],{},[57,24777,24778],{},[136,24779,24780],{},"BasicMessageChannel",[57,24782,24783],{},"双向消息传递",[57,24785,24786],{},"自定义编解码、简单数据交换",[14,24788,24789],{},[125,24790,24791],{},"MethodChannel 示例：",[186,24793,24795],{"className":11162,"code":24794,"language":11164,"meta":191,"style":191},"\u002F\u002F Dart 端\nclass BatteryService {\n  static const _channel = MethodChannel('com.example\u002Fbattery');\n\n  Future\u003Cint> getBatteryLevel() async {\n    try {\n      final level = await _channel.invokeMethod\u003Cint>('getBatteryLevel');\n      return level ?? -1;\n    } on PlatformException catch (e) {\n      throw Exception('Failed to get battery level: ${e.message}');\n    }\n  }\n}\n",[136,24796,24797,24802,24807,24812,24816,24821,24825,24830,24835,24840,24845,24849,24853],{"__ignoreMap":191},[195,24798,24799],{"class":197,"line":198},[195,24800,24801],{},"\u002F\u002F Dart 端\n",[195,24803,24804],{"class":197,"line":230},[195,24805,24806],{},"class BatteryService {\n",[195,24808,24809],{"class":197,"line":251},[195,24810,24811],{},"  static const _channel = MethodChannel('com.example\u002Fbattery');\n",[195,24813,24814],{"class":197,"line":272},[195,24815,1241],{"emptyLinePlaceholder":757},[195,24817,24818],{"class":197,"line":293},[195,24819,24820],{},"  Future\u003Cint> getBatteryLevel() async {\n",[195,24822,24823],{"class":197,"line":562},[195,24824,9611],{},[195,24826,24827],{"class":197,"line":583},[195,24828,24829],{},"      final level = await _channel.invokeMethod\u003Cint>('getBatteryLevel');\n",[195,24831,24832],{"class":197,"line":962},[195,24833,24834],{},"      return level ?? -1;\n",[195,24836,24837],{"class":197,"line":968},[195,24838,24839],{},"    } on PlatformException catch (e) {\n",[195,24841,24842],{"class":197,"line":1274},[195,24843,24844],{},"      throw Exception('Failed to get battery level: ${e.message}');\n",[195,24846,24847],{"class":197,"line":1282},[195,24848,2403],{},[195,24850,24851],{"class":197,"line":1295},[195,24852,965],{},[195,24854,24855],{"class":197,"line":1309},[195,24856,552],{},[186,24858,24860],{"className":6838,"code":24859,"language":6840,"meta":191,"style":191},"\u002F\u002F Android (Kotlin) 端\nclass MainActivity : FlutterActivity() {\n    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {\n        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, \"com.example\u002Fbattery\")\n            .setMethodCallHandler { call, result ->\n                when (call.method) {\n                    \"getBatteryLevel\" -> {\n                        val level = getBatteryLevel()\n                        if (level != -1) result.success(level)\n                        else result.error(\"UNAVAILABLE\", \"Battery level not available\", null)\n                    }\n                    else -> result.notImplemented()\n                }\n            }\n    }\n}\n",[136,24861,24862,24867,24872,24877,24882,24887,24892,24897,24902,24907,24912,24917,24922,24926,24930,24934],{"__ignoreMap":191},[195,24863,24864],{"class":197,"line":198},[195,24865,24866],{},"\u002F\u002F Android (Kotlin) 端\n",[195,24868,24869],{"class":197,"line":230},[195,24870,24871],{},"class MainActivity : FlutterActivity() {\n",[195,24873,24874],{"class":197,"line":251},[195,24875,24876],{},"    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {\n",[195,24878,24879],{"class":197,"line":272},[195,24880,24881],{},"        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, \"com.example\u002Fbattery\")\n",[195,24883,24884],{"class":197,"line":293},[195,24885,24886],{},"            .setMethodCallHandler { call, result ->\n",[195,24888,24889],{"class":197,"line":562},[195,24890,24891],{},"                when (call.method) {\n",[195,24893,24894],{"class":197,"line":583},[195,24895,24896],{},"                    \"getBatteryLevel\" -> {\n",[195,24898,24899],{"class":197,"line":962},[195,24900,24901],{},"                        val level = getBatteryLevel()\n",[195,24903,24904],{"class":197,"line":968},[195,24905,24906],{},"                        if (level != -1) result.success(level)\n",[195,24908,24909],{"class":197,"line":1274},[195,24910,24911],{},"                        else result.error(\"UNAVAILABLE\", \"Battery level not available\", null)\n",[195,24913,24914],{"class":197,"line":1282},[195,24915,24916],{},"                    }\n",[195,24918,24919],{"class":197,"line":1295},[195,24920,24921],{},"                    else -> result.notImplemented()\n",[195,24923,24924],{"class":197,"line":1309},[195,24925,6705],{},[195,24927,24928],{"class":197,"line":2246},[195,24929,5781],{},[195,24931,24932],{"class":197,"line":1996},[195,24933,2403],{},[195,24935,24936],{"class":197,"line":2257},[195,24937,552],{},[14,24939,24940],{},[125,24941,24942],{},"EventChannel 示例：",[186,24944,24946],{"className":11162,"code":24945,"language":11164,"meta":191,"style":191},"\u002F\u002F Dart 端：接收原生传感器数据流\nclass AccelerometerService {\n  static const _channel = EventChannel('com.example\u002Faccelerometer');\n\n  Stream\u003CAccelerometerEvent> get events {\n    return _channel.receiveBroadcastStream().map((data) {\n      final list = data as List;\n      return AccelerometerEvent(list[0], list[1], list[2]);\n    });\n  }\n}\n",[136,24947,24948,24953,24958,24963,24967,24972,24977,24982,24987,24991,24995],{"__ignoreMap":191},[195,24949,24950],{"class":197,"line":198},[195,24951,24952],{},"\u002F\u002F Dart 端：接收原生传感器数据流\n",[195,24954,24955],{"class":197,"line":230},[195,24956,24957],{},"class AccelerometerService {\n",[195,24959,24960],{"class":197,"line":251},[195,24961,24962],{},"  static const _channel = EventChannel('com.example\u002Faccelerometer');\n",[195,24964,24965],{"class":197,"line":272},[195,24966,1241],{"emptyLinePlaceholder":757},[195,24968,24969],{"class":197,"line":293},[195,24970,24971],{},"  Stream\u003CAccelerometerEvent> get events {\n",[195,24973,24974],{"class":197,"line":562},[195,24975,24976],{},"    return _channel.receiveBroadcastStream().map((data) {\n",[195,24978,24979],{"class":197,"line":583},[195,24980,24981],{},"      final list = data as List;\n",[195,24983,24984],{"class":197,"line":962},[195,24985,24986],{},"      return AccelerometerEvent(list[0], list[1], list[2]);\n",[195,24988,24989],{"class":197,"line":968},[195,24990,12331],{},[195,24992,24993],{"class":197,"line":1274},[195,24994,965],{},[195,24996,24997],{"class":197,"line":1282},[195,24998,552],{},[1890,25000],{},[32,25002,25004],{"id":25003},"q72-什么是-ffiforeign-function-interface什么时候用-ffi-代替-platform-channel","Q7.2: 什么是 FFI（Foreign Function Interface）？什么时候用 FFI 代替 Platform Channel？",[14,25006,25007],{},[125,25008,2077],{},[36,25010,25011,25023],{},[39,25012,25013],{},[42,25014,25015,25017,25020],{},[45,25016],{},[45,25018,25019],{},"Platform Channel",[45,25021,25022],{},"FFI",[52,25024,25025,25035,25045,25056,25066],{},[42,25026,25027,25029,25032],{},[57,25028,24743],{},[57,25030,25031],{},"异步消息传递",[57,25033,25034],{},"直接函数调用（同步）",[42,25036,25037,25039,25042],{},[57,25038,3340],{},[57,25040,25041],{},"有序列化\u002F反序列化开销",[57,25043,25044],{},"接近原生调用性能",[42,25046,25047,25050,25053],{},[57,25048,25049],{},"适用语言",[57,25051,25052],{},"Java\u002FKotlin、ObjC\u002FSwift",[57,25054,25055],{},"C\u002FC++",[42,25057,25058,25060,25063],{},[57,25059,5863],{},[57,25061,25062],{},"较低",[57,25064,25065],{},"较高",[42,25067,25068,25070,25073],{},[57,25069,7129],{},[57,25071,25072],{},"平台 API 调用",[57,25074,25075],{},"高性能计算、复用 C 库",[186,25077,25079],{"className":11162,"code":25078,"language":11164,"meta":191,"style":191},"\u002F\u002F FFI 示例：调用 C 函数\nimport 'dart:ffi';\n\n\u002F\u002F 定义 C 函数签名\ntypedef NativeAdd = Int32 Function(Int32 a, Int32 b);\ntypedef DartAdd = int Function(int a, int b);\n\nvoid main() {\n  final dylib = DynamicLibrary.open('libnative.so');\n  final add = dylib.lookupFunction\u003CNativeAdd, DartAdd>('add');\n  print(add(3, 4)); \u002F\u002F 7，同步调用\n}\n",[136,25080,25081,25086,25091,25095,25100,25105,25110,25114,25118,25123,25128,25133],{"__ignoreMap":191},[195,25082,25083],{"class":197,"line":198},[195,25084,25085],{},"\u002F\u002F FFI 示例：调用 C 函数\n",[195,25087,25088],{"class":197,"line":230},[195,25089,25090],{},"import 'dart:ffi';\n",[195,25092,25093],{"class":197,"line":251},[195,25094,1241],{"emptyLinePlaceholder":757},[195,25096,25097],{"class":197,"line":272},[195,25098,25099],{},"\u002F\u002F 定义 C 函数签名\n",[195,25101,25102],{"class":197,"line":293},[195,25103,25104],{},"typedef NativeAdd = Int32 Function(Int32 a, Int32 b);\n",[195,25106,25107],{"class":197,"line":562},[195,25108,25109],{},"typedef DartAdd = int Function(int a, int b);\n",[195,25111,25112],{"class":197,"line":583},[195,25113,1241],{"emptyLinePlaceholder":757},[195,25115,25116],{"class":197,"line":962},[195,25117,11185],{},[195,25119,25120],{"class":197,"line":968},[195,25121,25122],{},"  final dylib = DynamicLibrary.open('libnative.so');\n",[195,25124,25125],{"class":197,"line":1274},[195,25126,25127],{},"  final add = dylib.lookupFunction\u003CNativeAdd, DartAdd>('add');\n",[195,25129,25130],{"class":197,"line":1282},[195,25131,25132],{},"  print(add(3, 4)); \u002F\u002F 7，同步调用\n",[195,25134,25135],{"class":197,"line":1295},[195,25136,552],{},[14,25138,25139],{},[125,25140,25141],{},"选择 FFI 的场景：",[129,25143,25144,25147,25150],{},[132,25145,25146],{},"需要调用现有的 C\u002FC++ 库（如 SQLite、OpenCV）",[132,25148,25149],{},"对性能要求极高，不能接受 Channel 的序列化开销",[132,25151,25152],{},"需要同步调用（Platform Channel 只支持异步）",[1890,25154],{},[18,25156,25158],{"id":25157},"_8-路由与导航","8. 路由与导航",[32,25160,25162],{"id":25161},"q81-navigator-10-和-navigator-20router-api有什么区别","Q8.1: Navigator 1.0 和 Navigator 2.0（Router API）有什么区别？",[14,25164,25165],{},[125,25166,2077],{},[14,25168,25169],{},[125,25170,25171],{},"Navigator 1.0（命令式）：",[186,25173,25175],{"className":11162,"code":25174,"language":11164,"meta":191,"style":191},"\u002F\u002F 入栈\nNavigator.push(context, MaterialPageRoute(builder: (_) => DetailPage()));\n\u002F\u002F 或命名路由\nNavigator.pushNamed(context, '\u002Fdetail', arguments: {'id': 42});\n\n\u002F\u002F 出栈\nNavigator.pop(context);\n\n\u002F\u002F 替换\nNavigator.pushReplacement(context, MaterialPageRoute(builder: (_) => HomePage()));\n",[136,25176,25177,25182,25187,25192,25197,25201,25206,25211,25215,25220],{"__ignoreMap":191},[195,25178,25179],{"class":197,"line":198},[195,25180,25181],{},"\u002F\u002F 入栈\n",[195,25183,25184],{"class":197,"line":230},[195,25185,25186],{},"Navigator.push(context, MaterialPageRoute(builder: (_) => DetailPage()));\n",[195,25188,25189],{"class":197,"line":251},[195,25190,25191],{},"\u002F\u002F 或命名路由\n",[195,25193,25194],{"class":197,"line":272},[195,25195,25196],{},"Navigator.pushNamed(context, '\u002Fdetail', arguments: {'id': 42});\n",[195,25198,25199],{"class":197,"line":293},[195,25200,1241],{"emptyLinePlaceholder":757},[195,25202,25203],{"class":197,"line":562},[195,25204,25205],{},"\u002F\u002F 出栈\n",[195,25207,25208],{"class":197,"line":583},[195,25209,25210],{},"Navigator.pop(context);\n",[195,25212,25213],{"class":197,"line":962},[195,25214,1241],{"emptyLinePlaceholder":757},[195,25216,25217],{"class":197,"line":968},[195,25218,25219],{},"\u002F\u002F 替换\n",[195,25221,25222],{"class":197,"line":1274},[195,25223,25224],{},"Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => HomePage()));\n",[14,25226,25227],{},"优点：简单直观\n缺点：",[129,25229,25230,25233,25236],{},[132,25231,25232],{},"难以处理深层链接（Deep Link）",[132,25234,25235],{},"难以从 URL 恢复导航状态（Web）",[132,25237,25238],{},"路由栈不透明，难以声明式管理",[14,25240,25241],{},[125,25242,25243],{},"Navigator 2.0（声明式 \u002F Router API）：",[186,25245,25247],{"className":11162,"code":25246,"language":11164,"meta":191,"style":191},"MaterialApp.router(\n  routerConfig: GoRouter(\n    routes: [\n      GoRoute(\n        path: '\u002F',\n        builder: (context, state) => HomePage(),\n        routes: [\n          GoRoute(\n            path: 'detail\u002F:id',\n            builder: (context, state) {\n              final id = state.pathParameters['id']!;\n              return DetailPage(id: id);\n            },\n          ),\n        ],\n      ),\n    ],\n  ),\n)\n",[136,25248,25249,25254,25259,25264,25269,25274,25279,25284,25289,25294,25299,25304,25309,25314,25319,25324,25328,25333,25337],{"__ignoreMap":191},[195,25250,25251],{"class":197,"line":198},[195,25252,25253],{},"MaterialApp.router(\n",[195,25255,25256],{"class":197,"line":230},[195,25257,25258],{},"  routerConfig: GoRouter(\n",[195,25260,25261],{"class":197,"line":251},[195,25262,25263],{},"    routes: [\n",[195,25265,25266],{"class":197,"line":272},[195,25267,25268],{},"      GoRoute(\n",[195,25270,25271],{"class":197,"line":293},[195,25272,25273],{},"        path: '\u002F',\n",[195,25275,25276],{"class":197,"line":562},[195,25277,25278],{},"        builder: (context, state) => HomePage(),\n",[195,25280,25281],{"class":197,"line":583},[195,25282,25283],{},"        routes: [\n",[195,25285,25286],{"class":197,"line":962},[195,25287,25288],{},"          GoRoute(\n",[195,25290,25291],{"class":197,"line":968},[195,25292,25293],{},"            path: 'detail\u002F:id',\n",[195,25295,25296],{"class":197,"line":1274},[195,25297,25298],{},"            builder: (context, state) {\n",[195,25300,25301],{"class":197,"line":1282},[195,25302,25303],{},"              final id = state.pathParameters['id']!;\n",[195,25305,25306],{"class":197,"line":1295},[195,25307,25308],{},"              return DetailPage(id: id);\n",[195,25310,25311],{"class":197,"line":1309},[195,25312,25313],{},"            },\n",[195,25315,25316],{"class":197,"line":2246},[195,25317,25318],{},"          ),\n",[195,25320,25321],{"class":197,"line":1996},[195,25322,25323],{},"        ],\n",[195,25325,25326],{"class":197,"line":2257},[195,25327,15845],{},[195,25329,25330],{"class":197,"line":2262},[195,25331,25332],{},"    ],\n",[195,25334,25335],{"class":197,"line":2267},[195,25336,11676],{},[195,25338,25339],{"class":197,"line":2273},[195,25340,410],{},[14,25342,25343],{},[125,25344,25345],{},"go_router（官方推荐的 Router 封装）：",[186,25347,25349],{"className":11162,"code":25348,"language":11164,"meta":191,"style":191},"\u002F\u002F 声明式导航\ncontext.go('\u002Fdetail\u002F42');       \u002F\u002F 替换整个栈\ncontext.push('\u002Fdetail\u002F42');     \u002F\u002F 入栈\n\n\u002F\u002F 重定向\nGoRouter(\n  redirect: (context, state) {\n    final isLoggedIn = authService.isLoggedIn;\n    if (!isLoggedIn && state.matchedLocation != '\u002Flogin') {\n      return '\u002Flogin';\n    }\n    return null; \u002F\u002F 不重定向\n  },\n)\n\n\u002F\u002F ShellRoute：共享布局（如底部导航栏）\nShellRoute(\n  builder: (context, state, child) {\n    return ScaffoldWithBottomNav(child: child);\n  },\n  routes: [\n    GoRoute(path: '\u002Fhome', builder: ...),\n    GoRoute(path: '\u002Fsettings', builder: ...),\n  ],\n)\n",[136,25350,25351,25356,25361,25366,25370,25375,25380,25385,25390,25395,25400,25404,25409,25413,25417,25421,25426,25431,25436,25441,25445,25449,25454,25459,25463],{"__ignoreMap":191},[195,25352,25353],{"class":197,"line":198},[195,25354,25355],{},"\u002F\u002F 声明式导航\n",[195,25357,25358],{"class":197,"line":230},[195,25359,25360],{},"context.go('\u002Fdetail\u002F42');       \u002F\u002F 替换整个栈\n",[195,25362,25363],{"class":197,"line":251},[195,25364,25365],{},"context.push('\u002Fdetail\u002F42');     \u002F\u002F 入栈\n",[195,25367,25368],{"class":197,"line":272},[195,25369,1241],{"emptyLinePlaceholder":757},[195,25371,25372],{"class":197,"line":293},[195,25373,25374],{},"\u002F\u002F 重定向\n",[195,25376,25377],{"class":197,"line":562},[195,25378,25379],{},"GoRouter(\n",[195,25381,25382],{"class":197,"line":583},[195,25383,25384],{},"  redirect: (context, state) {\n",[195,25386,25387],{"class":197,"line":962},[195,25388,25389],{},"    final isLoggedIn = authService.isLoggedIn;\n",[195,25391,25392],{"class":197,"line":968},[195,25393,25394],{},"    if (!isLoggedIn && state.matchedLocation != '\u002Flogin') {\n",[195,25396,25397],{"class":197,"line":1274},[195,25398,25399],{},"      return '\u002Flogin';\n",[195,25401,25402],{"class":197,"line":1282},[195,25403,2403],{},[195,25405,25406],{"class":197,"line":1295},[195,25407,25408],{},"    return null; \u002F\u002F 不重定向\n",[195,25410,25411],{"class":197,"line":1309},[195,25412,11963],{},[195,25414,25415],{"class":197,"line":2246},[195,25416,410],{},[195,25418,25419],{"class":197,"line":1996},[195,25420,1241],{"emptyLinePlaceholder":757},[195,25422,25423],{"class":197,"line":2257},[195,25424,25425],{},"\u002F\u002F ShellRoute：共享布局（如底部导航栏）\n",[195,25427,25428],{"class":197,"line":2262},[195,25429,25430],{},"ShellRoute(\n",[195,25432,25433],{"class":197,"line":2267},[195,25434,25435],{},"  builder: (context, state, child) {\n",[195,25437,25438],{"class":197,"line":2273},[195,25439,25440],{},"    return ScaffoldWithBottomNav(child: child);\n",[195,25442,25443],{"class":197,"line":2033},[195,25444,11963],{},[195,25446,25447],{"class":197,"line":2284},[195,25448,17806],{},[195,25450,25451],{"class":197,"line":2460},[195,25452,25453],{},"    GoRoute(path: '\u002Fhome', builder: ...),\n",[195,25455,25456],{"class":197,"line":2466},[195,25457,25458],{},"    GoRoute(path: '\u002Fsettings', builder: ...),\n",[195,25460,25461],{"class":197,"line":2472},[195,25462,17864],{},[195,25464,25465],{"class":197,"line":2780},[195,25466,410],{},[1890,25468],{},[18,25470,25472],{"id":25471},"_9-测试","9. 测试",[32,25474,25476],{"id":25475},"q91-flutter-中有哪些测试类型如何编写-widget-测试","Q9.1: Flutter 中有哪些测试类型？如何编写 Widget 测试？",[14,25478,25479],{},[125,25480,2077],{},[36,25482,25483,25498],{},[39,25484,25485],{},[42,25486,25487,25489,25492,25495],{},[45,25488,3642],{},[45,25490,25491],{},"速度",[45,25493,25494],{},"范围",[45,25496,25497],{},"依赖",[52,25499,25500,25513,25526],{},[42,25501,25502,25505,25508,25511],{},[57,25503,25504],{},"Unit Test",[57,25506,25507],{},"极快",[57,25509,25510],{},"单个函数\u002F类",[57,25512,2135],{},[42,25514,25515,25517,25520,25523],{},[57,25516,19474],{},[57,25518,25519],{},"快",[57,25521,25522],{},"单个 Widget",[57,25524,25525],{},"Flutter 测试框架",[42,25527,25528,25530,25533,25536],{},[57,25529,19485],{},[57,25531,25532],{},"慢",[57,25534,25535],{},"完整应用",[57,25537,25538],{},"真实设备\u002F模拟器",[14,25540,25541],{},[125,25542,25543],{},"Widget 测试示例：",[186,25545,25547],{"className":11162,"code":25546,"language":11164,"meta":191,"style":191},"import 'package:flutter_test\u002Fflutter_test.dart';\n\nvoid main() {\n  testWidgets('Counter increments', (WidgetTester tester) async {\n    \u002F\u002F 构建 Widget\n    await tester.pumpWidget(const MaterialApp(home: CounterPage()));\n\n    \u002F\u002F 验证初始状态\n    expect(find.text('0'), findsOneWidget);\n    expect(find.text('1'), findsNothing);\n\n    \u002F\u002F 模拟交互\n    await tester.tap(find.byIcon(Icons.add));\n    await tester.pump(); \u002F\u002F 触发一帧重建\n\n    \u002F\u002F 验证结果\n    expect(find.text('0'), findsNothing);\n    expect(find.text('1'), findsOneWidget);\n  });\n\n  testWidgets('shows loading then data', (tester) async {\n    await tester.pumpWidget(MyApp());\n\n    \u002F\u002F 验证 loading 状态\n    expect(find.byType(CircularProgressIndicator), findsOneWidget);\n\n    \u002F\u002F 等待异步操作完成\n    await tester.pumpAndSettle(); \u002F\u002F 持续 pump 直到没有待处理的帧\n\n    \u002F\u002F 验证数据状态\n    expect(find.text('Data loaded'), findsOneWidget);\n  });\n}\n",[136,25548,25549,25554,25558,25562,25567,25572,25577,25581,25586,25591,25596,25600,25605,25610,25615,25619,25624,25629,25634,25638,25642,25647,25652,25656,25661,25666,25670,25675,25680,25684,25689,25694,25698],{"__ignoreMap":191},[195,25550,25551],{"class":197,"line":198},[195,25552,25553],{},"import 'package:flutter_test\u002Fflutter_test.dart';\n",[195,25555,25556],{"class":197,"line":230},[195,25557,1241],{"emptyLinePlaceholder":757},[195,25559,25560],{"class":197,"line":251},[195,25561,11185],{},[195,25563,25564],{"class":197,"line":272},[195,25565,25566],{},"  testWidgets('Counter increments', (WidgetTester tester) async {\n",[195,25568,25569],{"class":197,"line":293},[195,25570,25571],{},"    \u002F\u002F 构建 Widget\n",[195,25573,25574],{"class":197,"line":562},[195,25575,25576],{},"    await tester.pumpWidget(const MaterialApp(home: CounterPage()));\n",[195,25578,25579],{"class":197,"line":583},[195,25580,1241],{"emptyLinePlaceholder":757},[195,25582,25583],{"class":197,"line":962},[195,25584,25585],{},"    \u002F\u002F 验证初始状态\n",[195,25587,25588],{"class":197,"line":968},[195,25589,25590],{},"    expect(find.text('0'), findsOneWidget);\n",[195,25592,25593],{"class":197,"line":1274},[195,25594,25595],{},"    expect(find.text('1'), findsNothing);\n",[195,25597,25598],{"class":197,"line":1282},[195,25599,1241],{"emptyLinePlaceholder":757},[195,25601,25602],{"class":197,"line":1295},[195,25603,25604],{},"    \u002F\u002F 模拟交互\n",[195,25606,25607],{"class":197,"line":1309},[195,25608,25609],{},"    await tester.tap(find.byIcon(Icons.add));\n",[195,25611,25612],{"class":197,"line":2246},[195,25613,25614],{},"    await tester.pump(); \u002F\u002F 触发一帧重建\n",[195,25616,25617],{"class":197,"line":1996},[195,25618,1241],{"emptyLinePlaceholder":757},[195,25620,25621],{"class":197,"line":2257},[195,25622,25623],{},"    \u002F\u002F 验证结果\n",[195,25625,25626],{"class":197,"line":2262},[195,25627,25628],{},"    expect(find.text('0'), findsNothing);\n",[195,25630,25631],{"class":197,"line":2267},[195,25632,25633],{},"    expect(find.text('1'), findsOneWidget);\n",[195,25635,25636],{"class":197,"line":2273},[195,25637,23522],{},[195,25639,25640],{"class":197,"line":2033},[195,25641,1241],{"emptyLinePlaceholder":757},[195,25643,25644],{"class":197,"line":2284},[195,25645,25646],{},"  testWidgets('shows loading then data', (tester) async {\n",[195,25648,25649],{"class":197,"line":2460},[195,25650,25651],{},"    await tester.pumpWidget(MyApp());\n",[195,25653,25654],{"class":197,"line":2466},[195,25655,1241],{"emptyLinePlaceholder":757},[195,25657,25658],{"class":197,"line":2472},[195,25659,25660],{},"    \u002F\u002F 验证 loading 状态\n",[195,25662,25663],{"class":197,"line":2780},[195,25664,25665],{},"    expect(find.byType(CircularProgressIndicator), findsOneWidget);\n",[195,25667,25668],{"class":197,"line":2786},[195,25669,1241],{"emptyLinePlaceholder":757},[195,25671,25672],{"class":197,"line":2792},[195,25673,25674],{},"    \u002F\u002F 等待异步操作完成\n",[195,25676,25677],{"class":197,"line":2798},[195,25678,25679],{},"    await tester.pumpAndSettle(); \u002F\u002F 持续 pump 直到没有待处理的帧\n",[195,25681,25682],{"class":197,"line":2804},[195,25683,1241],{"emptyLinePlaceholder":757},[195,25685,25686],{"class":197,"line":2810},[195,25687,25688],{},"    \u002F\u002F 验证数据状态\n",[195,25690,25691],{"class":197,"line":2815},[195,25692,25693],{},"    expect(find.text('Data loaded'), findsOneWidget);\n",[195,25695,25696],{"class":197,"line":2820},[195,25697,23522],{},[195,25699,25700],{"class":197,"line":2825},[195,25701,552],{},[14,25703,25704],{},[125,25705,25706],{},"常用 Finder：",[186,25708,25710],{"className":11162,"code":25709,"language":11164,"meta":191,"style":191},"find.text('Hello');                    \u002F\u002F 按文本查找\nfind.byType(ElevatedButton);          \u002F\u002F 按类型查找\nfind.byIcon(Icons.add);               \u002F\u002F 按图标查找\nfind.byKey(const Key('submit_btn'));   \u002F\u002F 按 Key 查找\nfind.byWidgetPredicate((w) => ...);   \u002F\u002F 自定义条件\n",[136,25711,25712,25717,25722,25727,25732],{"__ignoreMap":191},[195,25713,25714],{"class":197,"line":198},[195,25715,25716],{},"find.text('Hello');                    \u002F\u002F 按文本查找\n",[195,25718,25719],{"class":197,"line":230},[195,25720,25721],{},"find.byType(ElevatedButton);          \u002F\u002F 按类型查找\n",[195,25723,25724],{"class":197,"line":251},[195,25725,25726],{},"find.byIcon(Icons.add);               \u002F\u002F 按图标查找\n",[195,25728,25729],{"class":197,"line":272},[195,25730,25731],{},"find.byKey(const Key('submit_btn'));   \u002F\u002F 按 Key 查找\n",[195,25733,25734],{"class":197,"line":293},[195,25735,25736],{},"find.byWidgetPredicate((w) => ...);   \u002F\u002F 自定义条件\n",[14,25738,25739],{},[125,25740,25741],{},"Mock 依赖（使用 mockito\u002Fmocktail）：",[186,25743,25745],{"className":11162,"code":25744,"language":11164,"meta":191,"style":191},"class MockAuthRepo extends Mock implements AuthRepository {}\n\ntestWidgets('shows error on login failure', (tester) async {\n  final mockRepo = MockAuthRepo();\n  when(() => mockRepo.login(any(), any()))\n      .thenThrow(AuthException('Invalid'));\n\n  await tester.pumpWidget(\n    ProviderScope(\n      overrides: [authRepoProvider.overrideWithValue(mockRepo)],\n      child: const MyApp(),\n    ),\n  );\n\n  await tester.enterText(find.byKey(Key('email')), 'test@test.com');\n  await tester.enterText(find.byKey(Key('password')), 'wrong');\n  await tester.tap(find.text('Login'));\n  await tester.pumpAndSettle();\n\n  expect(find.text('Invalid'), findsOneWidget);\n});\n",[136,25746,25747,25752,25756,25761,25766,25771,25776,25780,25785,25790,25795,25800,25804,25809,25813,25818,25823,25828,25833,25837,25842],{"__ignoreMap":191},[195,25748,25749],{"class":197,"line":198},[195,25750,25751],{},"class MockAuthRepo extends Mock implements AuthRepository {}\n",[195,25753,25754],{"class":197,"line":230},[195,25755,1241],{"emptyLinePlaceholder":757},[195,25757,25758],{"class":197,"line":251},[195,25759,25760],{},"testWidgets('shows error on login failure', (tester) async {\n",[195,25762,25763],{"class":197,"line":272},[195,25764,25765],{},"  final mockRepo = MockAuthRepo();\n",[195,25767,25768],{"class":197,"line":293},[195,25769,25770],{},"  when(() => mockRepo.login(any(), any()))\n",[195,25772,25773],{"class":197,"line":562},[195,25774,25775],{},"      .thenThrow(AuthException('Invalid'));\n",[195,25777,25778],{"class":197,"line":583},[195,25779,1241],{"emptyLinePlaceholder":757},[195,25781,25782],{"class":197,"line":962},[195,25783,25784],{},"  await tester.pumpWidget(\n",[195,25786,25787],{"class":197,"line":968},[195,25788,25789],{},"    ProviderScope(\n",[195,25791,25792],{"class":197,"line":1274},[195,25793,25794],{},"      overrides: [authRepoProvider.overrideWithValue(mockRepo)],\n",[195,25796,25797],{"class":197,"line":1282},[195,25798,25799],{},"      child: const MyApp(),\n",[195,25801,25802],{"class":197,"line":1295},[195,25803,22508],{},[195,25805,25806],{"class":197,"line":1309},[195,25807,25808],{},"  );\n",[195,25810,25811],{"class":197,"line":2246},[195,25812,1241],{"emptyLinePlaceholder":757},[195,25814,25815],{"class":197,"line":1996},[195,25816,25817],{},"  await tester.enterText(find.byKey(Key('email')), 'test@test.com');\n",[195,25819,25820],{"class":197,"line":2257},[195,25821,25822],{},"  await tester.enterText(find.byKey(Key('password')), 'wrong');\n",[195,25824,25825],{"class":197,"line":2262},[195,25826,25827],{},"  await tester.tap(find.text('Login'));\n",[195,25829,25830],{"class":197,"line":2267},[195,25831,25832],{},"  await tester.pumpAndSettle();\n",[195,25834,25835],{"class":197,"line":2273},[195,25836,1241],{"emptyLinePlaceholder":757},[195,25838,25839],{"class":197,"line":2033},[195,25840,25841],{},"  expect(find.text('Invalid'), findsOneWidget);\n",[195,25843,25844],{"class":197,"line":2284},[195,25845,11253],{},[1890,25847],{},[32,25849,25851],{"id":25850},"q92-什么是-golden-test怎么使用","Q9.2: 什么是 Golden Test？怎么使用？",[14,25853,25854],{},[125,25855,2077],{},[14,25857,25858],{},"Golden Test（黄金测试 \u002F 快照测试）将 Widget 渲染结果与预存的参考图片进行像素级对比：",[186,25860,25862],{"className":11162,"code":25861,"language":11164,"meta":191,"style":191},"testWidgets('MyButton golden test', (tester) async {\n  await tester.pumpWidget(\n    MaterialApp(\n      home: Scaffold(\n        body: Center(\n          child: MyCustomButton(label: 'Click Me'),\n        ),\n      ),\n    ),\n  );\n\n  \u002F\u002F 首次运行生成参考图，之后每次运行对比\n  await expectLater(\n    find.byType(MyCustomButton),\n    matchesGoldenFile('goldens\u002Fmy_button.png'),\n  );\n});\n",[136,25863,25864,25869,25873,25878,25883,25888,25893,25897,25901,25905,25909,25913,25918,25923,25928,25933,25937],{"__ignoreMap":191},[195,25865,25866],{"class":197,"line":198},[195,25867,25868],{},"testWidgets('MyButton golden test', (tester) async {\n",[195,25870,25871],{"class":197,"line":230},[195,25872,25784],{},[195,25874,25875],{"class":197,"line":251},[195,25876,25877],{},"    MaterialApp(\n",[195,25879,25880],{"class":197,"line":272},[195,25881,25882],{},"      home: Scaffold(\n",[195,25884,25885],{"class":197,"line":293},[195,25886,25887],{},"        body: Center(\n",[195,25889,25890],{"class":197,"line":562},[195,25891,25892],{},"          child: MyCustomButton(label: 'Click Me'),\n",[195,25894,25895],{"class":197,"line":583},[195,25896,24219],{},[195,25898,25899],{"class":197,"line":962},[195,25900,15845],{},[195,25902,25903],{"class":197,"line":968},[195,25904,22508],{},[195,25906,25907],{"class":197,"line":1274},[195,25908,25808],{},[195,25910,25911],{"class":197,"line":1282},[195,25912,1241],{"emptyLinePlaceholder":757},[195,25914,25915],{"class":197,"line":1295},[195,25916,25917],{},"  \u002F\u002F 首次运行生成参考图，之后每次运行对比\n",[195,25919,25920],{"class":197,"line":1309},[195,25921,25922],{},"  await expectLater(\n",[195,25924,25925],{"class":197,"line":2246},[195,25926,25927],{},"    find.byType(MyCustomButton),\n",[195,25929,25930],{"class":197,"line":1996},[195,25931,25932],{},"    matchesGoldenFile('goldens\u002Fmy_button.png'),\n",[195,25934,25935],{"class":197,"line":2257},[195,25936,25808],{},[195,25938,25939],{"class":197,"line":2262},[195,25940,11253],{},[186,25942,25944],{"className":785,"code":25943,"language":787,"meta":191,"style":191},"# 生成\u002F更新 golden 文件\nflutter test --update-goldens\n\n# 正常测试（对比）\nflutter test\n",[136,25945,25946,25951,25962,25966,25971],{"__ignoreMap":191},[195,25947,25948],{"class":197,"line":198},[195,25949,25950],{"class":415},"# 生成\u002F更新 golden 文件\n",[195,25952,25953,25956,25959],{"class":197,"line":230},[195,25954,25955],{"class":201},"flutter",[195,25957,25958],{"class":459}," test",[195,25960,25961],{"class":213}," --update-goldens\n",[195,25963,25964],{"class":197,"line":251},[195,25965,1241],{"emptyLinePlaceholder":757},[195,25967,25968],{"class":197,"line":272},[195,25969,25970],{"class":415},"# 正常测试（对比）\n",[195,25972,25973,25975],{"class":197,"line":293},[195,25974,25955],{"class":201},[195,25976,25977],{"class":459}," test\n",[14,25979,25980],{},[125,25981,25982],{},"注意事项：",[129,25984,25985,25988,25995],{},[132,25986,25987],{},"不同平台渲染可能有细微差异，建议在 CI 中固定平台",[132,25989,25990,25991,25994],{},"字体渲染差异可能导致误报，可使用 ",[136,25992,25993],{},"flutter_test"," 提供的默认字体",[132,25996,25997],{},"适合用于设计系统组件库、自定义绘制组件的回归测试",[1890,25999],{},[18,26001,26003],{"id":26002},"_10-架构设计","10. 架构设计",[32,26005,26007],{"id":26006},"q101-介绍-flutter-中常用的架构模式","Q10.1: 介绍 Flutter 中常用的架构模式",[14,26009,26010],{},[125,26011,2077],{},[14,26013,26014],{},[125,26015,26016],{},"1. Clean Architecture（推荐用于大型项目）：",[186,26018,26021],{"className":26019,"code":26020,"language":1074},[1072],"lib\u002F\n├── core\u002F                  # 公共工具、常量、错误处理\n├── features\u002F\n│   └── auth\u002F\n│       ├── data\u002F          # 数据层\n│       │   ├── datasources\u002F    # API、本地数据库\n│       │   ├── models\u002F         # JSON 序列化模型（DTO）\n│       │   └── repositories\u002F   # Repository 实现\n│       ├── domain\u002F        # 领域层（纯 Dart，无 Flutter 依赖）\n│       │   ├── entities\u002F       # 业务实体\n│       │   ├── repositories\u002F   # Repository 接口（抽象类）\n│       │   └── usecases\u002F       # 用例（业务逻辑）\n│       └── presentation\u002F  # 表现层\n│           ├── bloc\u002F           # BLoC \u002F Cubit\n│           ├── pages\u002F          # 页面\n│           └── widgets\u002F        # UI 组件\n",[136,26022,26020],{"__ignoreMap":191},[186,26024,26026],{"className":11162,"code":26025,"language":11164,"meta":191,"style":191},"\u002F\u002F Domain 层：纯业务逻辑，不依赖任何框架\nabstract class AuthRepository {\n  Future\u003CEither\u003CFailure, User>> login(String email, String password);\n}\n\nclass LoginUseCase {\n  final AuthRepository repository;\n  LoginUseCase(this.repository);\n\n  Future\u003CEither\u003CFailure, User>> call(String email, String password) {\n    return repository.login(email, password);\n  }\n}\n\n\u002F\u002F Data 层：实现 Repository 接口\nclass AuthRepositoryImpl implements AuthRepository {\n  final AuthRemoteDataSource remote;\n  final AuthLocalDataSource local;\n\n  @override\n  Future\u003CEither\u003CFailure, User>> login(String email, String password) async {\n    try {\n      final model = await remote.login(email, password);\n      await local.cacheToken(model.token);\n      return Right(model.toEntity());\n    } on ServerException catch (e) {\n      return Left(ServerFailure(e.message));\n    }\n  }\n}\n",[136,26027,26028,26033,26038,26043,26047,26051,26056,26061,26066,26070,26075,26080,26084,26088,26092,26097,26102,26107,26112,26116,26120,26125,26129,26134,26139,26144,26149,26154,26158,26162],{"__ignoreMap":191},[195,26029,26030],{"class":197,"line":198},[195,26031,26032],{},"\u002F\u002F Domain 层：纯业务逻辑，不依赖任何框架\n",[195,26034,26035],{"class":197,"line":230},[195,26036,26037],{},"abstract class AuthRepository {\n",[195,26039,26040],{"class":197,"line":251},[195,26041,26042],{},"  Future\u003CEither\u003CFailure, User>> login(String email, String password);\n",[195,26044,26045],{"class":197,"line":272},[195,26046,552],{},[195,26048,26049],{"class":197,"line":293},[195,26050,1241],{"emptyLinePlaceholder":757},[195,26052,26053],{"class":197,"line":562},[195,26054,26055],{},"class LoginUseCase {\n",[195,26057,26058],{"class":197,"line":583},[195,26059,26060],{},"  final AuthRepository repository;\n",[195,26062,26063],{"class":197,"line":962},[195,26064,26065],{},"  LoginUseCase(this.repository);\n",[195,26067,26068],{"class":197,"line":968},[195,26069,1241],{"emptyLinePlaceholder":757},[195,26071,26072],{"class":197,"line":1274},[195,26073,26074],{},"  Future\u003CEither\u003CFailure, User>> call(String email, String password) {\n",[195,26076,26077],{"class":197,"line":1282},[195,26078,26079],{},"    return repository.login(email, password);\n",[195,26081,26082],{"class":197,"line":1295},[195,26083,965],{},[195,26085,26086],{"class":197,"line":1309},[195,26087,552],{},[195,26089,26090],{"class":197,"line":2246},[195,26091,1241],{"emptyLinePlaceholder":757},[195,26093,26094],{"class":197,"line":1996},[195,26095,26096],{},"\u002F\u002F Data 层：实现 Repository 接口\n",[195,26098,26099],{"class":197,"line":2257},[195,26100,26101],{},"class AuthRepositoryImpl implements AuthRepository {\n",[195,26103,26104],{"class":197,"line":2262},[195,26105,26106],{},"  final AuthRemoteDataSource remote;\n",[195,26108,26109],{"class":197,"line":2267},[195,26110,26111],{},"  final AuthLocalDataSource local;\n",[195,26113,26114],{"class":197,"line":2273},[195,26115,1241],{"emptyLinePlaceholder":757},[195,26117,26118],{"class":197,"line":2033},[195,26119,12090],{},[195,26121,26122],{"class":197,"line":2284},[195,26123,26124],{},"  Future\u003CEither\u003CFailure, User>> login(String email, String password) async {\n",[195,26126,26127],{"class":197,"line":2460},[195,26128,9611],{},[195,26130,26131],{"class":197,"line":2466},[195,26132,26133],{},"      final model = await remote.login(email, password);\n",[195,26135,26136],{"class":197,"line":2472},[195,26137,26138],{},"      await local.cacheToken(model.token);\n",[195,26140,26141],{"class":197,"line":2780},[195,26142,26143],{},"      return Right(model.toEntity());\n",[195,26145,26146],{"class":197,"line":2786},[195,26147,26148],{},"    } on ServerException catch (e) {\n",[195,26150,26151],{"class":197,"line":2792},[195,26152,26153],{},"      return Left(ServerFailure(e.message));\n",[195,26155,26156],{"class":197,"line":2798},[195,26157,2403],{},[195,26159,26160],{"class":197,"line":2804},[195,26161,965],{},[195,26163,26164],{"class":197,"line":2810},[195,26165,552],{},[14,26167,26168],{},[125,26169,26170],{},"2. MVVM（适合中型项目）：",[186,26172,26174],{"className":11162,"code":26173,"language":11164,"meta":191,"style":191},"\u002F\u002F ViewModel\nclass LoginViewModel extends ChangeNotifier {\n  final AuthRepository _repo;\n  bool isLoading = false;\n  String? error;\n\n  Future\u003Cvoid> login(String email, String password) async {\n    isLoading = true;\n    notifyListeners();\n\n    final result = await _repo.login(email, password);\n    result.fold(\n      (failure) => error = failure.message,\n      (user) => { \u002F* navigate *\u002F },\n    );\n\n    isLoading = false;\n    notifyListeners();\n  }\n}\n\n\u002F\u002F View\nclass LoginPage extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return ChangeNotifierProvider(\n      create: (_) => LoginViewModel(context.read\u003CAuthRepository>()),\n      child: Consumer\u003CLoginViewModel>(\n        builder: (context, vm, _) {\n          if (vm.isLoading) return CircularProgressIndicator();\n          \u002F\u002F ...\n        },\n      ),\n    );\n  }\n}\n",[136,26175,26176,26180,26185,26189,26194,26199,26203,26207,26212,26217,26221,26226,26231,26236,26241,26245,26249,26254,26258,26262,26266,26270,26274,26279,26283,26287,26292,26297,26302,26307,26312,26317,26322,26326,26330,26334],{"__ignoreMap":191},[195,26177,26178],{"class":197,"line":198},[195,26179,10032],{},[195,26181,26182],{"class":197,"line":230},[195,26183,26184],{},"class LoginViewModel extends ChangeNotifier {\n",[195,26186,26187],{"class":197,"line":251},[195,26188,11823],{},[195,26190,26191],{"class":197,"line":272},[195,26192,26193],{},"  bool isLoading = false;\n",[195,26195,26196],{"class":197,"line":293},[195,26197,26198],{},"  String? error;\n",[195,26200,26201],{"class":197,"line":562},[195,26202,1241],{"emptyLinePlaceholder":757},[195,26204,26205],{"class":197,"line":583},[195,26206,12032],{},[195,26208,26209],{"class":197,"line":962},[195,26210,26211],{},"    isLoading = true;\n",[195,26213,26214],{"class":197,"line":968},[195,26215,26216],{},"    notifyListeners();\n",[195,26218,26219],{"class":197,"line":1274},[195,26220,1241],{"emptyLinePlaceholder":757},[195,26222,26223],{"class":197,"line":1282},[195,26224,26225],{},"    final result = await _repo.login(email, password);\n",[195,26227,26228],{"class":197,"line":1295},[195,26229,26230],{},"    result.fold(\n",[195,26232,26233],{"class":197,"line":1309},[195,26234,26235],{},"      (failure) => error = failure.message,\n",[195,26237,26238],{"class":197,"line":2246},[195,26239,26240],{},"      (user) => { \u002F* navigate *\u002F },\n",[195,26242,26243],{"class":197,"line":1996},[195,26244,24234],{},[195,26246,26247],{"class":197,"line":2257},[195,26248,1241],{"emptyLinePlaceholder":757},[195,26250,26251],{"class":197,"line":2262},[195,26252,26253],{},"    isLoading = false;\n",[195,26255,26256],{"class":197,"line":2267},[195,26257,26216],{},[195,26259,26260],{"class":197,"line":2273},[195,26261,965],{},[195,26263,26264],{"class":197,"line":2033},[195,26265,552],{},[195,26267,26268],{"class":197,"line":2284},[195,26269,1241],{"emptyLinePlaceholder":757},[195,26271,26272],{"class":197,"line":2460},[195,26273,5802],{},[195,26275,26276],{"class":197,"line":2466},[195,26277,26278],{},"class LoginPage extends StatelessWidget {\n",[195,26280,26281],{"class":197,"line":2472},[195,26282,12090],{},[195,26284,26285],{"class":197,"line":2780},[195,26286,15619],{},[195,26288,26289],{"class":197,"line":2786},[195,26290,26291],{},"    return ChangeNotifierProvider(\n",[195,26293,26294],{"class":197,"line":2792},[195,26295,26296],{},"      create: (_) => LoginViewModel(context.read\u003CAuthRepository>()),\n",[195,26298,26299],{"class":197,"line":2798},[195,26300,26301],{},"      child: Consumer\u003CLoginViewModel>(\n",[195,26303,26304],{"class":197,"line":2804},[195,26305,26306],{},"        builder: (context, vm, _) {\n",[195,26308,26309],{"class":197,"line":2810},[195,26310,26311],{},"          if (vm.isLoading) return CircularProgressIndicator();\n",[195,26313,26314],{"class":197,"line":2815},[195,26315,26316],{},"          \u002F\u002F ...\n",[195,26318,26319],{"class":197,"line":2820},[195,26320,26321],{},"        },\n",[195,26323,26324],{"class":197,"line":2825},[195,26325,15845],{},[195,26327,26328],{"class":197,"line":2831},[195,26329,24234],{},[195,26331,26332],{"class":197,"line":2837},[195,26333,965],{},[195,26335,26336],{"class":197,"line":2843},[195,26337,552],{},[1890,26339],{},[32,26341,26343],{"id":26342},"q102-如何实现依赖注入di","Q10.2: 如何实现依赖注入（DI）？",[14,26345,26346],{},[125,26347,2077],{},[14,26349,26350],{},[125,26351,26352],{},"1. get_it（Service Locator 模式）：",[186,26354,26356],{"className":11162,"code":26355,"language":11164,"meta":191,"style":191},"final sl = GetIt.instance;\n\nvoid setupDependencies() {\n  \u002F\u002F 单例\n  sl.registerLazySingleton\u003CApiClient>(() => ApiClient());\n  sl.registerLazySingleton\u003CAuthRepository>(\n    () => AuthRepositoryImpl(sl\u003CApiClient>()),\n  );\n\n  \u002F\u002F 工厂（每次获取新实例）\n  sl.registerFactory\u003CLoginBloc>(\n    () => LoginBloc(sl\u003CAuthRepository>()),\n  );\n}\n\n\u002F\u002F 使用\nfinal bloc = sl\u003CLoginBloc>();\n",[136,26357,26358,26363,26367,26372,26377,26382,26387,26392,26396,26400,26405,26410,26415,26419,26423,26427,26431],{"__ignoreMap":191},[195,26359,26360],{"class":197,"line":198},[195,26361,26362],{},"final sl = GetIt.instance;\n",[195,26364,26365],{"class":197,"line":230},[195,26366,1241],{"emptyLinePlaceholder":757},[195,26368,26369],{"class":197,"line":251},[195,26370,26371],{},"void setupDependencies() {\n",[195,26373,26374],{"class":197,"line":272},[195,26375,26376],{},"  \u002F\u002F 单例\n",[195,26378,26379],{"class":197,"line":293},[195,26380,26381],{},"  sl.registerLazySingleton\u003CApiClient>(() => ApiClient());\n",[195,26383,26384],{"class":197,"line":562},[195,26385,26386],{},"  sl.registerLazySingleton\u003CAuthRepository>(\n",[195,26388,26389],{"class":197,"line":583},[195,26390,26391],{},"    () => AuthRepositoryImpl(sl\u003CApiClient>()),\n",[195,26393,26394],{"class":197,"line":962},[195,26395,25808],{},[195,26397,26398],{"class":197,"line":968},[195,26399,1241],{"emptyLinePlaceholder":757},[195,26401,26402],{"class":197,"line":1274},[195,26403,26404],{},"  \u002F\u002F 工厂（每次获取新实例）\n",[195,26406,26407],{"class":197,"line":1282},[195,26408,26409],{},"  sl.registerFactory\u003CLoginBloc>(\n",[195,26411,26412],{"class":197,"line":1295},[195,26413,26414],{},"    () => LoginBloc(sl\u003CAuthRepository>()),\n",[195,26416,26417],{"class":197,"line":1309},[195,26418,25808],{},[195,26420,26421],{"class":197,"line":2246},[195,26422,552],{},[195,26424,26425],{"class":197,"line":1996},[195,26426,1241],{"emptyLinePlaceholder":757},[195,26428,26429],{"class":197,"line":2257},[195,26430,3056],{},[195,26432,26433],{"class":197,"line":2262},[195,26434,26435],{},"final bloc = sl\u003CLoginBloc>();\n",[14,26437,26438],{},[125,26439,26440],{},"2. Riverpod（推荐，编译安全的 DI）：",[186,26442,26444],{"className":11162,"code":26443,"language":11164,"meta":191,"style":191},"final apiClientProvider = Provider((ref) => ApiClient());\n\nfinal authRepoProvider = Provider((ref) {\n  return AuthRepositoryImpl(ref.read(apiClientProvider));\n});\n\nfinal loginBlocProvider = Provider.autoDispose((ref) {\n  return LoginBloc(ref.read(authRepoProvider));\n});\n",[136,26445,26446,26451,26455,26460,26465,26469,26473,26478,26483],{"__ignoreMap":191},[195,26447,26448],{"class":197,"line":198},[195,26449,26450],{},"final apiClientProvider = Provider((ref) => ApiClient());\n",[195,26452,26453],{"class":197,"line":230},[195,26454,1241],{"emptyLinePlaceholder":757},[195,26456,26457],{"class":197,"line":251},[195,26458,26459],{},"final authRepoProvider = Provider((ref) {\n",[195,26461,26462],{"class":197,"line":272},[195,26463,26464],{},"  return AuthRepositoryImpl(ref.read(apiClientProvider));\n",[195,26466,26467],{"class":197,"line":293},[195,26468,11253],{},[195,26470,26471],{"class":197,"line":562},[195,26472,1241],{"emptyLinePlaceholder":757},[195,26474,26475],{"class":197,"line":583},[195,26476,26477],{},"final loginBlocProvider = Provider.autoDispose((ref) {\n",[195,26479,26480],{"class":197,"line":962},[195,26481,26482],{},"  return LoginBloc(ref.read(authRepoProvider));\n",[195,26484,26485],{"class":197,"line":968},[195,26486,11253],{},[14,26488,26489],{},[125,26490,26491],{},"3. Injectable + get_it（代码生成）：",[186,26493,26495],{"className":11162,"code":26494,"language":11164,"meta":191,"style":191},"@injectable\nclass AuthRepositoryImpl implements AuthRepository {\n  final ApiClient client;\n\n  @factoryMethod\n  AuthRepositoryImpl(this.client);\n}\n\n\u002F\u002F 自动生成注册代码\n@InjectableInit()\nvoid configureDependencies() => getIt.init();\n",[136,26496,26497,26502,26506,26511,26515,26520,26525,26529,26533,26538,26543],{"__ignoreMap":191},[195,26498,26499],{"class":197,"line":198},[195,26500,26501],{},"@injectable\n",[195,26503,26504],{"class":197,"line":230},[195,26505,26101],{},[195,26507,26508],{"class":197,"line":251},[195,26509,26510],{},"  final ApiClient client;\n",[195,26512,26513],{"class":197,"line":272},[195,26514,1241],{"emptyLinePlaceholder":757},[195,26516,26517],{"class":197,"line":293},[195,26518,26519],{},"  @factoryMethod\n",[195,26521,26522],{"class":197,"line":562},[195,26523,26524],{},"  AuthRepositoryImpl(this.client);\n",[195,26526,26527],{"class":197,"line":583},[195,26528,552],{},[195,26530,26531],{"class":197,"line":962},[195,26532,1241],{"emptyLinePlaceholder":757},[195,26534,26535],{"class":197,"line":968},[195,26536,26537],{},"\u002F\u002F 自动生成注册代码\n",[195,26539,26540],{"class":197,"line":1274},[195,26541,26542],{},"@InjectableInit()\n",[195,26544,26545],{"class":197,"line":1282},[195,26546,26547],{},"void configureDependencies() => getIt.init();\n",[1890,26549],{},[18,26551,26553],{"id":26552},"_11-包管理与编译","11. 包管理与编译",[32,26555,26557],{"id":26556},"q111-flutter-的编译模式有哪些debug-和-release-有什么区别","Q11.1: Flutter 的编译模式有哪些？Debug 和 Release 有什么区别？",[14,26559,26560],{},[125,26561,2077],{},[36,26563,26564,26581],{},[39,26565,26566],{},[42,26567,26568,26570,26573,26576,26579],{},[45,26569,852],{},[45,26571,26572],{},"编译方式",[45,26574,26575],{},"JIT",[45,26577,26578],{},"AOT",[45,26580,14523],{},[52,26582,26583,26600,26614],{},[42,26584,26585,26588,26591,26594,26597],{},[57,26586,26587],{},"Debug",[57,26589,26590],{},"Kernel JIT",[57,26592,26593],{},"✅",[57,26595,26596],{},"❌",[57,26598,26599],{},"Hot Reload、断言启用、性能差",[42,26601,26602,26605,26607,26609,26611],{},[57,26603,26604],{},"Profile",[57,26606,26578],{},[57,26608,26596],{},[57,26610,26593],{},[57,26612,26613],{},"性能分析、DevTools 可用",[42,26615,26616,26619,26621,26623,26625],{},[57,26617,26618],{},"Release",[57,26620,26578],{},[57,26622,26596],{},[57,26624,26593],{},[57,26626,26627],{},"最高性能、无调试信息、Tree Shaking",[14,26629,26630],{},[125,26631,23581],{},[186,26633,26635],{"className":11162,"code":26634,"language":11164,"meta":191,"style":191},"\u002F\u002F 条件编译\nif (kDebugMode) {\n  print('Debug only log');\n}\n\nif (kReleaseMode) {\n  \u002F\u002F Release 专有逻辑\n}\n\n\u002F\u002F assert 只在 Debug 模式执行\nassert(value != null, 'Value should not be null');\n",[136,26636,26637,26642,26647,26652,26656,26660,26665,26670,26674,26678,26683],{"__ignoreMap":191},[195,26638,26639],{"class":197,"line":198},[195,26640,26641],{},"\u002F\u002F 条件编译\n",[195,26643,26644],{"class":197,"line":230},[195,26645,26646],{},"if (kDebugMode) {\n",[195,26648,26649],{"class":197,"line":251},[195,26650,26651],{},"  print('Debug only log');\n",[195,26653,26654],{"class":197,"line":272},[195,26655,552],{},[195,26657,26658],{"class":197,"line":293},[195,26659,1241],{"emptyLinePlaceholder":757},[195,26661,26662],{"class":197,"line":562},[195,26663,26664],{},"if (kReleaseMode) {\n",[195,26666,26667],{"class":197,"line":583},[195,26668,26669],{},"  \u002F\u002F Release 专有逻辑\n",[195,26671,26672],{"class":197,"line":962},[195,26673,552],{},[195,26675,26676],{"class":197,"line":968},[195,26677,1241],{"emptyLinePlaceholder":757},[195,26679,26680],{"class":197,"line":1274},[195,26681,26682],{},"\u002F\u002F assert 只在 Debug 模式执行\n",[195,26684,26685],{"class":197,"line":1282},[195,26686,26687],{},"assert(value != null, 'Value should not be null');\n",[14,26689,26690,26693],{},[125,26691,26692],{},"Tree Shaking："," Release 模式下，编译器会移除未使用的代码，减小包体积。",[14,26695,26696],{},[125,26697,26698],{},"Hot Reload vs Hot Restart：",[129,26700,26701,26704],{},[132,26702,26703],{},"Hot Reload：保持应用状态，只更新修改的代码（亚秒级）",[132,26705,26706,26707],{},"Hot Restart：重置应用状态，重新执行 ",[136,26708,26709],{},"main()",[1890,26711],{},[32,26713,26715],{"id":26714},"q112-如何减小-flutter-应用的包体积","Q11.2: 如何减小 Flutter 应用的包体积？",[14,26717,26718],{},[125,26719,2077],{},[186,26721,26723],{"className":785,"code":26722,"language":787,"meta":191,"style":191},"# 1. 分析包体积\nflutter build apk --analyze-size\nflutter build ios --analyze-size\n",[136,26724,26725,26730,26743],{"__ignoreMap":191},[195,26726,26727],{"class":197,"line":198},[195,26728,26729],{"class":415},"# 1. 分析包体积\n",[195,26731,26732,26734,26737,26740],{"class":197,"line":230},[195,26733,25955],{"class":201},[195,26735,26736],{"class":459}," build",[195,26738,26739],{"class":459}," apk",[195,26741,26742],{"class":213}," --analyze-size\n",[195,26744,26745,26747,26749,26752],{"class":197,"line":251},[195,26746,25955],{"class":201},[195,26748,26736],{"class":459},[195,26750,26751],{"class":459}," ios",[195,26753,26742],{"class":213},[14,26755,26756],{},[125,26757,26758],{},"关键策略：",[186,26760,26762],{"className":13365,"code":26761,"language":13367,"meta":191,"style":191},"# 2. 使用 --split-debug-info 分离调试信息\n# flutter build apk --split-debug-info=debug-info\u002F\n\n# 3. 启用混淆\n# flutter build apk --obfuscate --split-debug-info=debug-info\u002F\n",[136,26763,26764,26769,26774,26778,26783],{"__ignoreMap":191},[195,26765,26766],{"class":197,"line":198},[195,26767,26768],{"class":415},"# 2. 使用 --split-debug-info 分离调试信息\n",[195,26770,26771],{"class":197,"line":230},[195,26772,26773],{"class":415},"# flutter build apk --split-debug-info=debug-info\u002F\n",[195,26775,26776],{"class":197,"line":251},[195,26777,1241],{"emptyLinePlaceholder":757},[195,26779,26780],{"class":197,"line":272},[195,26781,26782],{"class":415},"# 3. 启用混淆\n",[195,26784,26785],{"class":197,"line":293},[195,26786,26787],{"class":415},"# flutter build apk --obfuscate --split-debug-info=debug-info\u002F\n",[186,26789,26791],{"className":11162,"code":26790,"language":11164,"meta":191,"style":191},"\u002F\u002F 4. 按需加载资源\n\u002F\u002F pubspec.yaml 中只声明需要的资源\nflutter:\n  assets:\n    - assets\u002Fimages\u002F   # 不要包含不需要的大图\n\n\u002F\u002F 5. 使用 deferred loading（延迟加载）\nimport 'package:my_app\u002Fheavy_feature.dart' deferred as heavy;\n\nFuture\u003Cvoid> loadFeature() async {\n  await heavy.loadLibrary();\n  heavy.showFeature();\n}\n",[136,26792,26793,26798,26803,26808,26813,26818,26822,26827,26832,26836,26841,26846,26851],{"__ignoreMap":191},[195,26794,26795],{"class":197,"line":198},[195,26796,26797],{},"\u002F\u002F 4. 按需加载资源\n",[195,26799,26800],{"class":197,"line":230},[195,26801,26802],{},"\u002F\u002F pubspec.yaml 中只声明需要的资源\n",[195,26804,26805],{"class":197,"line":251},[195,26806,26807],{},"flutter:\n",[195,26809,26810],{"class":197,"line":272},[195,26811,26812],{},"  assets:\n",[195,26814,26815],{"class":197,"line":293},[195,26816,26817],{},"    - assets\u002Fimages\u002F   # 不要包含不需要的大图\n",[195,26819,26820],{"class":197,"line":562},[195,26821,1241],{"emptyLinePlaceholder":757},[195,26823,26824],{"class":197,"line":583},[195,26825,26826],{},"\u002F\u002F 5. 使用 deferred loading（延迟加载）\n",[195,26828,26829],{"class":197,"line":962},[195,26830,26831],{},"import 'package:my_app\u002Fheavy_feature.dart' deferred as heavy;\n",[195,26833,26834],{"class":197,"line":968},[195,26835,1241],{"emptyLinePlaceholder":757},[195,26837,26838],{"class":197,"line":1274},[195,26839,26840],{},"Future\u003Cvoid> loadFeature() async {\n",[195,26842,26843],{"class":197,"line":1282},[195,26844,26845],{},"  await heavy.loadLibrary();\n",[195,26847,26848],{"class":197,"line":1295},[195,26849,26850],{},"  heavy.showFeature();\n",[195,26852,26853],{"class":197,"line":1309},[195,26854,552],{},[186,26856,26858],{"className":13365,"code":26857,"language":13367,"meta":191,"style":191},"# 6. 精简依赖，移除不使用的包\ndependencies:\n  # 定期检查是否所有依赖都在使用\n",[136,26859,26860,26865,26872],{"__ignoreMap":191},[195,26861,26862],{"class":197,"line":198},[195,26863,26864],{"class":415},"# 6. 精简依赖，移除不使用的包\n",[195,26866,26867,26870],{"class":197,"line":230},[195,26868,26869],{"class":205},"dependencies",[195,26871,13405],{"class":209},[195,26873,26874],{"class":197,"line":251},[195,26875,26876],{"class":415},"  # 定期检查是否所有依赖都在使用\n",[186,26878,26881],{"className":26879,"code":26880,"language":1074},[1072],"# 7. 使用 App Bundle（Android）\nflutter build appbundle  # 比 APK 小约 20%\n",[136,26882,26880],{"__ignoreMap":191},[1890,26884],{},[18,26886,26888],{"id":26887},"_12-实战场景题","12. 实战场景题",[32,26890,26892],{"id":26891},"q121-如何实现一个无限滚动列表并处理加载状态和错误状态","Q12.1: 如何实现一个无限滚动列表，并处理加载状态和错误状态？",[14,26894,26895],{},[125,26896,2077],{},[186,26898,26900],{"className":11162,"code":26899,"language":11164,"meta":191,"style":191},"class InfiniteListPage extends StatefulWidget {\n  @override\n  State\u003CInfiniteListPage> createState() => _InfiniteListPageState();\n}\n\nclass _InfiniteListPageState extends State\u003CInfiniteListPage> {\n  final _scrollController = ScrollController();\n  final List\u003CItem> _items = [];\n  bool _isLoading = false;\n  bool _hasMore = true;\n  String? _error;\n  int _page = 1;\n\n  @override\n  void initState() {\n    super.initState();\n    _loadMore();\n    _scrollController.addListener(_onScroll);\n  }\n\n  void _onScroll() {\n    if (_scrollController.position.pixels >=\n        _scrollController.position.maxScrollExtent - 200) {\n      _loadMore();\n    }\n  }\n\n  Future\u003Cvoid> _loadMore() async {\n    if (_isLoading || !_hasMore) return;\n\n    setState(() {\n      _isLoading = true;\n      _error = null;\n    });\n\n    try {\n      final newItems = await api.fetchItems(page: _page, limit: 20);\n      setState(() {\n        _items.addAll(newItems);\n        _page++;\n        _hasMore = newItems.length == 20;\n        _isLoading = false;\n      });\n    } catch (e) {\n      setState(() {\n        _error = e.toString();\n        _isLoading = false;\n      });\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return RefreshIndicator(\n      onRefresh: () async {\n        _page = 1;\n        _items.clear();\n        _hasMore = true;\n        await _loadMore();\n      },\n      child: ListView.builder(\n        controller: _scrollController,\n        itemCount: _items.length + (_hasMore ? 1 : 0),\n        itemBuilder: (context, index) {\n          if (index == _items.length) {\n            if (_error != null) {\n              return _ErrorTile(\n                error: _error!,\n                onRetry: _loadMore,\n              );\n            }\n            return const Center(child: CircularProgressIndicator());\n          }\n          return ItemTile(item: _items[index]);\n        },\n      ),\n    );\n  }\n\n  @override\n  void dispose() {\n    _scrollController.dispose();\n    super.dispose();\n  }\n}\n",[136,26901,26902,26907,26911,26916,26920,26924,26929,26934,26939,26944,26949,26954,26959,26963,26967,26971,26976,26981,26986,26990,26994,26999,27004,27009,27014,27018,27022,27026,27031,27036,27040,27045,27050,27055,27059,27063,27067,27072,27077,27082,27087,27092,27097,27102,27106,27110,27115,27119,27123,27127,27131,27135,27139,27143,27148,27153,27158,27163,27168,27173,27178,27183,27188,27193,27198,27203,27208,27213,27218,27223,27228,27232,27237,27242,27247,27251,27255,27259,27263,27268,27273,27278,27284,27290,27295],{"__ignoreMap":191},[195,26903,26904],{"class":197,"line":198},[195,26905,26906],{},"class InfiniteListPage extends StatefulWidget {\n",[195,26908,26909],{"class":197,"line":230},[195,26910,12090],{},[195,26912,26913],{"class":197,"line":251},[195,26914,26915],{},"  State\u003CInfiniteListPage> createState() => _InfiniteListPageState();\n",[195,26917,26918],{"class":197,"line":272},[195,26919,552],{},[195,26921,26922],{"class":197,"line":293},[195,26923,1241],{"emptyLinePlaceholder":757},[195,26925,26926],{"class":197,"line":562},[195,26927,26928],{},"class _InfiniteListPageState extends State\u003CInfiniteListPage> {\n",[195,26930,26931],{"class":197,"line":583},[195,26932,26933],{},"  final _scrollController = ScrollController();\n",[195,26935,26936],{"class":197,"line":962},[195,26937,26938],{},"  final List\u003CItem> _items = [];\n",[195,26940,26941],{"class":197,"line":968},[195,26942,26943],{},"  bool _isLoading = false;\n",[195,26945,26946],{"class":197,"line":1274},[195,26947,26948],{},"  bool _hasMore = true;\n",[195,26950,26951],{"class":197,"line":1282},[195,26952,26953],{},"  String? _error;\n",[195,26955,26956],{"class":197,"line":1295},[195,26957,26958],{},"  int _page = 1;\n",[195,26960,26961],{"class":197,"line":1309},[195,26962,1241],{"emptyLinePlaceholder":757},[195,26964,26965],{"class":197,"line":2246},[195,26966,12090],{},[195,26968,26969],{"class":197,"line":1996},[195,26970,22974],{},[195,26972,26973],{"class":197,"line":2257},[195,26974,26975],{},"    super.initState();\n",[195,26977,26978],{"class":197,"line":2262},[195,26979,26980],{},"    _loadMore();\n",[195,26982,26983],{"class":197,"line":2267},[195,26984,26985],{},"    _scrollController.addListener(_onScroll);\n",[195,26987,26988],{"class":197,"line":2273},[195,26989,965],{},[195,26991,26992],{"class":197,"line":2033},[195,26993,1241],{"emptyLinePlaceholder":757},[195,26995,26996],{"class":197,"line":2284},[195,26997,26998],{},"  void _onScroll() {\n",[195,27000,27001],{"class":197,"line":2460},[195,27002,27003],{},"    if (_scrollController.position.pixels >=\n",[195,27005,27006],{"class":197,"line":2466},[195,27007,27008],{},"        _scrollController.position.maxScrollExtent - 200) {\n",[195,27010,27011],{"class":197,"line":2472},[195,27012,27013],{},"      _loadMore();\n",[195,27015,27016],{"class":197,"line":2780},[195,27017,2403],{},[195,27019,27020],{"class":197,"line":2786},[195,27021,965],{},[195,27023,27024],{"class":197,"line":2792},[195,27025,1241],{"emptyLinePlaceholder":757},[195,27027,27028],{"class":197,"line":2798},[195,27029,27030],{},"  Future\u003Cvoid> _loadMore() async {\n",[195,27032,27033],{"class":197,"line":2804},[195,27034,27035],{},"    if (_isLoading || !_hasMore) return;\n",[195,27037,27038],{"class":197,"line":2810},[195,27039,1241],{"emptyLinePlaceholder":757},[195,27041,27042],{"class":197,"line":2815},[195,27043,27044],{},"    setState(() {\n",[195,27046,27047],{"class":197,"line":2820},[195,27048,27049],{},"      _isLoading = true;\n",[195,27051,27052],{"class":197,"line":2825},[195,27053,27054],{},"      _error = null;\n",[195,27056,27057],{"class":197,"line":2831},[195,27058,12331],{},[195,27060,27061],{"class":197,"line":2837},[195,27062,1241],{"emptyLinePlaceholder":757},[195,27064,27065],{"class":197,"line":2843},[195,27066,9611],{},[195,27068,27069],{"class":197,"line":2848},[195,27070,27071],{},"      final newItems = await api.fetchItems(page: _page, limit: 20);\n",[195,27073,27074],{"class":197,"line":2854},[195,27075,27076],{},"      setState(() {\n",[195,27078,27079],{"class":197,"line":2860},[195,27080,27081],{},"        _items.addAll(newItems);\n",[195,27083,27084],{"class":197,"line":2866},[195,27085,27086],{},"        _page++;\n",[195,27088,27089],{"class":197,"line":2872},[195,27090,27091],{},"        _hasMore = newItems.length == 20;\n",[195,27093,27094],{"class":197,"line":2878},[195,27095,27096],{},"        _isLoading = false;\n",[195,27098,27099],{"class":197,"line":2884},[195,27100,27101],{},"      });\n",[195,27103,27104],{"class":197,"line":2890},[195,27105,11879],{},[195,27107,27108],{"class":197,"line":2895},[195,27109,27076],{},[195,27111,27112],{"class":197,"line":2900},[195,27113,27114],{},"        _error = e.toString();\n",[195,27116,27117],{"class":197,"line":2905},[195,27118,27096],{},[195,27120,27121],{"class":197,"line":2911},[195,27122,27101],{},[195,27124,27125],{"class":197,"line":2917},[195,27126,2403],{},[195,27128,27129],{"class":197,"line":2923},[195,27130,965],{},[195,27132,27133],{"class":197,"line":2929},[195,27134,1241],{"emptyLinePlaceholder":757},[195,27136,27137],{"class":197,"line":2934},[195,27138,12090],{},[195,27140,27141],{"class":197,"line":2939},[195,27142,15619],{},[195,27144,27145],{"class":197,"line":2945},[195,27146,27147],{},"    return RefreshIndicator(\n",[195,27149,27150],{"class":197,"line":2951},[195,27151,27152],{},"      onRefresh: () async {\n",[195,27154,27155],{"class":197,"line":2957},[195,27156,27157],{},"        _page = 1;\n",[195,27159,27160],{"class":197,"line":2963},[195,27161,27162],{},"        _items.clear();\n",[195,27164,27165],{"class":197,"line":9396},[195,27166,27167],{},"        _hasMore = true;\n",[195,27169,27170],{"class":197,"line":14968},[195,27171,27172],{},"        await _loadMore();\n",[195,27174,27175],{"class":197,"line":14974},[195,27176,27177],{},"      },\n",[195,27179,27180],{"class":197,"line":14980},[195,27181,27182],{},"      child: ListView.builder(\n",[195,27184,27185],{"class":197,"line":14986},[195,27186,27187],{},"        controller: _scrollController,\n",[195,27189,27190],{"class":197,"line":14992},[195,27191,27192],{},"        itemCount: _items.length + (_hasMore ? 1 : 0),\n",[195,27194,27195],{"class":197,"line":14998},[195,27196,27197],{},"        itemBuilder: (context, index) {\n",[195,27199,27200],{"class":197,"line":15004},[195,27201,27202],{},"          if (index == _items.length) {\n",[195,27204,27205],{"class":197,"line":15010},[195,27206,27207],{},"            if (_error != null) {\n",[195,27209,27210],{"class":197,"line":15016},[195,27211,27212],{},"              return _ErrorTile(\n",[195,27214,27215],{"class":197,"line":15021},[195,27216,27217],{},"                error: _error!,\n",[195,27219,27220],{"class":197,"line":15027},[195,27221,27222],{},"                onRetry: _loadMore,\n",[195,27224,27225],{"class":197,"line":15033},[195,27226,27227],{},"              );\n",[195,27229,27230],{"class":197,"line":15038},[195,27231,5781],{},[195,27233,27234],{"class":197,"line":15044},[195,27235,27236],{},"            return const Center(child: CircularProgressIndicator());\n",[195,27238,27239],{"class":197,"line":15050},[195,27240,27241],{},"          }\n",[195,27243,27244],{"class":197,"line":15056},[195,27245,27246],{},"          return ItemTile(item: _items[index]);\n",[195,27248,27249],{"class":197,"line":15062},[195,27250,26321],{},[195,27252,27253],{"class":197,"line":15068},[195,27254,15845],{},[195,27256,27257],{"class":197,"line":15073},[195,27258,24234],{},[195,27260,27261],{"class":197,"line":15078},[195,27262,965],{},[195,27264,27266],{"class":197,"line":27265},79,[195,27267,1241],{"emptyLinePlaceholder":757},[195,27269,27271],{"class":197,"line":27270},80,[195,27272,12090],{},[195,27274,27276],{"class":197,"line":27275},81,[195,27277,23084],{},[195,27279,27281],{"class":197,"line":27280},82,[195,27282,27283],{},"    _scrollController.dispose();\n",[195,27285,27287],{"class":197,"line":27286},83,[195,27288,27289],{},"    super.dispose();\n",[195,27291,27293],{"class":197,"line":27292},84,[195,27294,965],{},[195,27296,27298],{"class":197,"line":27297},85,[195,27299,552],{},[1890,27301],{},[32,27303,27305],{"id":27304},"q122-如何处理-flutter-中的深层链接deep-linking","Q12.2: 如何处理 Flutter 中的深层链接（Deep Linking）？",[14,27307,27308],{},[125,27309,2077],{},[186,27311,27313],{"className":11162,"code":27312,"language":11164,"meta":191,"style":191},"\u002F\u002F 使用 go_router 处理深层链接\nfinal router = GoRouter(\n  routes: [\n    GoRoute(\n      path: '\u002F',\n      builder: (_, __) => HomePage(),\n    ),\n    GoRoute(\n      path: '\u002Fproduct\u002F:id',\n      builder: (_, state) {\n        final id = state.pathParameters['id']!;\n        return ProductPage(id: id);\n      },\n    ),\n    GoRoute(\n      path: '\u002Forder\u002F:orderId\u002Ftracking',\n      builder: (_, state) {\n        return TrackingPage(orderId: state.pathParameters['orderId']!);\n      },\n    ),\n  ],\n  \u002F\u002F 认证守卫\n  redirect: (context, state) {\n    final isLoggedIn = ref.read(authProvider).isLoggedIn;\n    final isLoginRoute = state.matchedLocation == '\u002Flogin';\n\n    if (!isLoggedIn && !isLoginRoute) return '\u002Flogin';\n    if (isLoggedIn && isLoginRoute) return '\u002F';\n    return null;\n  },\n  \u002F\u002F 错误页\n  errorBuilder: (_, __) => NotFoundPage(),\n);\n",[136,27314,27315,27320,27325,27329,27334,27339,27344,27348,27352,27357,27362,27367,27372,27376,27380,27384,27389,27393,27398,27402,27406,27410,27415,27419,27424,27429,27433,27438,27443,27448,27452,27457,27462],{"__ignoreMap":191},[195,27316,27317],{"class":197,"line":198},[195,27318,27319],{},"\u002F\u002F 使用 go_router 处理深层链接\n",[195,27321,27322],{"class":197,"line":230},[195,27323,27324],{},"final router = GoRouter(\n",[195,27326,27327],{"class":197,"line":251},[195,27328,17806],{},[195,27330,27331],{"class":197,"line":272},[195,27332,27333],{},"    GoRoute(\n",[195,27335,27336],{"class":197,"line":293},[195,27337,27338],{},"      path: '\u002F',\n",[195,27340,27341],{"class":197,"line":562},[195,27342,27343],{},"      builder: (_, __) => HomePage(),\n",[195,27345,27346],{"class":197,"line":583},[195,27347,22508],{},[195,27349,27350],{"class":197,"line":962},[195,27351,27333],{},[195,27353,27354],{"class":197,"line":968},[195,27355,27356],{},"      path: '\u002Fproduct\u002F:id',\n",[195,27358,27359],{"class":197,"line":1274},[195,27360,27361],{},"      builder: (_, state) {\n",[195,27363,27364],{"class":197,"line":1282},[195,27365,27366],{},"        final id = state.pathParameters['id']!;\n",[195,27368,27369],{"class":197,"line":1295},[195,27370,27371],{},"        return ProductPage(id: id);\n",[195,27373,27374],{"class":197,"line":1309},[195,27375,27177],{},[195,27377,27378],{"class":197,"line":2246},[195,27379,22508],{},[195,27381,27382],{"class":197,"line":1996},[195,27383,27333],{},[195,27385,27386],{"class":197,"line":2257},[195,27387,27388],{},"      path: '\u002Forder\u002F:orderId\u002Ftracking',\n",[195,27390,27391],{"class":197,"line":2262},[195,27392,27361],{},[195,27394,27395],{"class":197,"line":2267},[195,27396,27397],{},"        return TrackingPage(orderId: state.pathParameters['orderId']!);\n",[195,27399,27400],{"class":197,"line":2273},[195,27401,27177],{},[195,27403,27404],{"class":197,"line":2033},[195,27405,22508],{},[195,27407,27408],{"class":197,"line":2284},[195,27409,17864],{},[195,27411,27412],{"class":197,"line":2460},[195,27413,27414],{},"  \u002F\u002F 认证守卫\n",[195,27416,27417],{"class":197,"line":2466},[195,27418,25384],{},[195,27420,27421],{"class":197,"line":2472},[195,27422,27423],{},"    final isLoggedIn = ref.read(authProvider).isLoggedIn;\n",[195,27425,27426],{"class":197,"line":2780},[195,27427,27428],{},"    final isLoginRoute = state.matchedLocation == '\u002Flogin';\n",[195,27430,27431],{"class":197,"line":2786},[195,27432,1241],{"emptyLinePlaceholder":757},[195,27434,27435],{"class":197,"line":2792},[195,27436,27437],{},"    if (!isLoggedIn && !isLoginRoute) return '\u002Flogin';\n",[195,27439,27440],{"class":197,"line":2798},[195,27441,27442],{},"    if (isLoggedIn && isLoginRoute) return '\u002F';\n",[195,27444,27445],{"class":197,"line":2804},[195,27446,27447],{},"    return null;\n",[195,27449,27450],{"class":197,"line":2810},[195,27451,11963],{},[195,27453,27454],{"class":197,"line":2815},[195,27455,27456],{},"  \u002F\u002F 错误页\n",[195,27458,27459],{"class":197,"line":2820},[195,27460,27461],{},"  errorBuilder: (_, __) => NotFoundPage(),\n",[195,27463,27464],{"class":197,"line":2825},[195,27465,527],{},[14,27467,27468],{},[125,27469,27470],{},"平台配置：",[186,27472,27474],{"className":13028,"code":27473,"language":13030,"meta":191,"style":191},"\u003C!-- Android: AndroidManifest.xml -->\n\u003Cintent-filter>\n  \u003Caction android:name=\"android.intent.action.VIEW\" \u002F>\n  \u003Ccategory android:name=\"android.intent.category.DEFAULT\" \u002F>\n  \u003Ccategory android:name=\"android.intent.category.BROWSABLE\" \u002F>\n  \u003Cdata android:scheme=\"https\" android:host=\"myapp.com\" \u002F>\n\u003C\u002Fintent-filter>\n",[136,27475,27476,27481,27486,27491,27496,27501,27506],{"__ignoreMap":191},[195,27477,27478],{"class":197,"line":198},[195,27479,27480],{},"\u003C!-- Android: AndroidManifest.xml -->\n",[195,27482,27483],{"class":197,"line":230},[195,27484,27485],{},"\u003Cintent-filter>\n",[195,27487,27488],{"class":197,"line":251},[195,27489,27490],{},"  \u003Caction android:name=\"android.intent.action.VIEW\" \u002F>\n",[195,27492,27493],{"class":197,"line":272},[195,27494,27495],{},"  \u003Ccategory android:name=\"android.intent.category.DEFAULT\" \u002F>\n",[195,27497,27498],{"class":197,"line":293},[195,27499,27500],{},"  \u003Ccategory android:name=\"android.intent.category.BROWSABLE\" \u002F>\n",[195,27502,27503],{"class":197,"line":562},[195,27504,27505],{},"  \u003Cdata android:scheme=\"https\" android:host=\"myapp.com\" \u002F>\n",[195,27507,27508],{"class":197,"line":583},[195,27509,27510],{},"\u003C\u002Fintent-filter>\n",[186,27512,27514],{"className":13028,"code":27513,"language":13030,"meta":191,"style":191},"\u003C!-- iOS: Info.plist + Associated Domains -->\n\u003C!-- 添加 applinks:myapp.com 到 Associated Domains -->\n",[136,27515,27516,27521],{"__ignoreMap":191},[195,27517,27518],{"class":197,"line":198},[195,27519,27520],{},"\u003C!-- iOS: Info.plist + Associated Domains -->\n",[195,27522,27523],{"class":197,"line":230},[195,27524,27525],{},"\u003C!-- 添加 applinks:myapp.com 到 Associated Domains -->\n",[1890,27527],{},[32,27529,27531],{"id":27530},"q123-如何实现多主题dark-mode支持","Q12.3: 如何实现多主题（Dark Mode）支持？",[14,27533,27534],{},[125,27535,2077],{},[186,27537,27539],{"className":11162,"code":27538,"language":11164,"meta":191,"style":191},"\u002F\u002F 1. 定义主题\nclass AppTheme {\n  static ThemeData light = ThemeData(\n    brightness: Brightness.light,\n    colorScheme: ColorScheme.fromSeed(\n      seedColor: Colors.blue,\n      brightness: Brightness.light,\n    ),\n    textTheme: _textTheme,\n    appBarTheme: const AppBarTheme(elevation: 0),\n  );\n\n  static ThemeData dark = ThemeData(\n    brightness: Brightness.dark,\n    colorScheme: ColorScheme.fromSeed(\n      seedColor: Colors.blue,\n      brightness: Brightness.dark,\n    ),\n    textTheme: _textTheme,\n  );\n\n  static const _textTheme = TextTheme(\n    headlineLarge: TextStyle(fontWeight: FontWeight.bold),\n  );\n}\n\n\u002F\u002F 2. 使用 Riverpod 管理主题状态\nfinal themeModeProvider = StateProvider\u003CThemeMode>((ref) => ThemeMode.system);\n\n\u002F\u002F 3. 应用主题\nclass MyApp extends ConsumerWidget {\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    final themeMode = ref.watch(themeModeProvider);\n    return MaterialApp(\n      theme: AppTheme.light,\n      darkTheme: AppTheme.dark,\n      themeMode: themeMode,\n      home: HomePage(),\n    );\n  }\n}\n\n\u002F\u002F 4. 在 Widget 中使用语义化颜色\nContainer(\n  color: Theme.of(context).colorScheme.surface,\n  child: Text(\n    'Hello',\n    style: Theme.of(context).textTheme.headlineLarge,\n  ),\n)\n\n\u002F\u002F 5. 自定义扩展（超出 Theme 范围的颜色）\nextension CustomColors on ThemeData {\n  Color get successColor =>\n      brightness == Brightness.light ? Colors.green : Colors.greenAccent;\n}\n",[136,27540,27541,27546,27551,27556,27561,27566,27571,27576,27580,27585,27590,27594,27598,27603,27608,27612,27616,27621,27625,27629,27633,27637,27642,27647,27651,27655,27659,27664,27669,27673,27678,27683,27687,27691,27696,27701,27706,27711,27716,27721,27725,27729,27733,27737,27742,27746,27751,27756,27761,27766,27770,27774,27778,27783,27788,27793,27798],{"__ignoreMap":191},[195,27542,27543],{"class":197,"line":198},[195,27544,27545],{},"\u002F\u002F 1. 定义主题\n",[195,27547,27548],{"class":197,"line":230},[195,27549,27550],{},"class AppTheme {\n",[195,27552,27553],{"class":197,"line":251},[195,27554,27555],{},"  static ThemeData light = ThemeData(\n",[195,27557,27558],{"class":197,"line":272},[195,27559,27560],{},"    brightness: Brightness.light,\n",[195,27562,27563],{"class":197,"line":293},[195,27564,27565],{},"    colorScheme: ColorScheme.fromSeed(\n",[195,27567,27568],{"class":197,"line":562},[195,27569,27570],{},"      seedColor: Colors.blue,\n",[195,27572,27573],{"class":197,"line":583},[195,27574,27575],{},"      brightness: Brightness.light,\n",[195,27577,27578],{"class":197,"line":962},[195,27579,22508],{},[195,27581,27582],{"class":197,"line":968},[195,27583,27584],{},"    textTheme: _textTheme,\n",[195,27586,27587],{"class":197,"line":1274},[195,27588,27589],{},"    appBarTheme: const AppBarTheme(elevation: 0),\n",[195,27591,27592],{"class":197,"line":1282},[195,27593,25808],{},[195,27595,27596],{"class":197,"line":1295},[195,27597,1241],{"emptyLinePlaceholder":757},[195,27599,27600],{"class":197,"line":1309},[195,27601,27602],{},"  static ThemeData dark = ThemeData(\n",[195,27604,27605],{"class":197,"line":2246},[195,27606,27607],{},"    brightness: Brightness.dark,\n",[195,27609,27610],{"class":197,"line":1996},[195,27611,27565],{},[195,27613,27614],{"class":197,"line":2257},[195,27615,27570],{},[195,27617,27618],{"class":197,"line":2262},[195,27619,27620],{},"      brightness: Brightness.dark,\n",[195,27622,27623],{"class":197,"line":2267},[195,27624,22508],{},[195,27626,27627],{"class":197,"line":2273},[195,27628,27584],{},[195,27630,27631],{"class":197,"line":2033},[195,27632,25808],{},[195,27634,27635],{"class":197,"line":2284},[195,27636,1241],{"emptyLinePlaceholder":757},[195,27638,27639],{"class":197,"line":2460},[195,27640,27641],{},"  static const _textTheme = TextTheme(\n",[195,27643,27644],{"class":197,"line":2466},[195,27645,27646],{},"    headlineLarge: TextStyle(fontWeight: FontWeight.bold),\n",[195,27648,27649],{"class":197,"line":2472},[195,27650,25808],{},[195,27652,27653],{"class":197,"line":2780},[195,27654,552],{},[195,27656,27657],{"class":197,"line":2786},[195,27658,1241],{"emptyLinePlaceholder":757},[195,27660,27661],{"class":197,"line":2792},[195,27662,27663],{},"\u002F\u002F 2. 使用 Riverpod 管理主题状态\n",[195,27665,27666],{"class":197,"line":2798},[195,27667,27668],{},"final themeModeProvider = StateProvider\u003CThemeMode>((ref) => ThemeMode.system);\n",[195,27670,27671],{"class":197,"line":2804},[195,27672,1241],{"emptyLinePlaceholder":757},[195,27674,27675],{"class":197,"line":2810},[195,27676,27677],{},"\u002F\u002F 3. 应用主题\n",[195,27679,27680],{"class":197,"line":2815},[195,27681,27682],{},"class MyApp extends ConsumerWidget {\n",[195,27684,27685],{"class":197,"line":2820},[195,27686,12090],{},[195,27688,27689],{"class":197,"line":2825},[195,27690,12095],{},[195,27692,27693],{"class":197,"line":2831},[195,27694,27695],{},"    final themeMode = ref.watch(themeModeProvider);\n",[195,27697,27698],{"class":197,"line":2837},[195,27699,27700],{},"    return MaterialApp(\n",[195,27702,27703],{"class":197,"line":2843},[195,27704,27705],{},"      theme: AppTheme.light,\n",[195,27707,27708],{"class":197,"line":2848},[195,27709,27710],{},"      darkTheme: AppTheme.dark,\n",[195,27712,27713],{"class":197,"line":2854},[195,27714,27715],{},"      themeMode: themeMode,\n",[195,27717,27718],{"class":197,"line":2860},[195,27719,27720],{},"      home: HomePage(),\n",[195,27722,27723],{"class":197,"line":2866},[195,27724,24234],{},[195,27726,27727],{"class":197,"line":2872},[195,27728,965],{},[195,27730,27731],{"class":197,"line":2878},[195,27732,552],{},[195,27734,27735],{"class":197,"line":2884},[195,27736,1241],{"emptyLinePlaceholder":757},[195,27738,27739],{"class":197,"line":2890},[195,27740,27741],{},"\u002F\u002F 4. 在 Widget 中使用语义化颜色\n",[195,27743,27744],{"class":197,"line":2895},[195,27745,18441],{},[195,27747,27748],{"class":197,"line":2900},[195,27749,27750],{},"  color: Theme.of(context).colorScheme.surface,\n",[195,27752,27753],{"class":197,"line":2905},[195,27754,27755],{},"  child: Text(\n",[195,27757,27758],{"class":197,"line":2911},[195,27759,27760],{},"    'Hello',\n",[195,27762,27763],{"class":197,"line":2917},[195,27764,27765],{},"    style: Theme.of(context).textTheme.headlineLarge,\n",[195,27767,27768],{"class":197,"line":2923},[195,27769,11676],{},[195,27771,27772],{"class":197,"line":2929},[195,27773,410],{},[195,27775,27776],{"class":197,"line":2934},[195,27777,1241],{"emptyLinePlaceholder":757},[195,27779,27780],{"class":197,"line":2939},[195,27781,27782],{},"\u002F\u002F 5. 自定义扩展（超出 Theme 范围的颜色）\n",[195,27784,27785],{"class":197,"line":2945},[195,27786,27787],{},"extension CustomColors on ThemeData {\n",[195,27789,27790],{"class":197,"line":2951},[195,27791,27792],{},"  Color get successColor =>\n",[195,27794,27795],{"class":197,"line":2957},[195,27796,27797],{},"      brightness == Brightness.light ? Colors.green : Colors.greenAccent;\n",[195,27799,27800],{"class":197,"line":2963},[195,27801,552],{},[1890,27803],{},[32,27805,27807],{"id":27806},"q124-如何做-flutter-应用的国际化i18n","Q12.4: 如何做 Flutter 应用的国际化（i18n）？",[14,27809,27810],{},[125,27811,2077],{},[186,27813,27815],{"className":13365,"code":27814,"language":13367,"meta":191,"style":191},"# pubspec.yaml\ndependencies:\n  flutter_localizations:\n    sdk: flutter\n  intl: any\n\nflutter:\n  generate: true\n",[136,27816,27817,27822,27828,27835,27845,27855,27859,27865],{"__ignoreMap":191},[195,27818,27819],{"class":197,"line":198},[195,27820,27821],{"class":415},"# pubspec.yaml\n",[195,27823,27824,27826],{"class":197,"line":230},[195,27825,26869],{"class":205},[195,27827,13405],{"class":209},[195,27829,27830,27833],{"class":197,"line":251},[195,27831,27832],{"class":205},"  flutter_localizations",[195,27834,13405],{"class":209},[195,27836,27837,27840,27842],{"class":197,"line":272},[195,27838,27839],{"class":205},"    sdk",[195,27841,217],{"class":209},[195,27843,27844],{"class":459},"flutter\n",[195,27846,27847,27850,27852],{"class":197,"line":293},[195,27848,27849],{"class":205},"  intl",[195,27851,217],{"class":209},[195,27853,27854],{"class":459},"any\n",[195,27856,27857],{"class":197,"line":562},[195,27858,1241],{"emptyLinePlaceholder":757},[195,27860,27861,27863],{"class":197,"line":583},[195,27862,25955],{"class":205},[195,27864,13405],{"class":209},[195,27866,27867,27870,27872],{"class":197,"line":962},[195,27868,27869],{"class":205},"  generate",[195,27871,217],{"class":209},[195,27873,27874],{"class":213},"true\n",[186,27876,27878],{"className":13365,"code":27877,"language":13367,"meta":191,"style":191},"# l10n.yaml\narb-dir: lib\u002Fl10n\ntemplate-arb-file: app_en.arb\noutput-localization-file: app_localizations.dart\n",[136,27879,27880,27885,27895,27905],{"__ignoreMap":191},[195,27881,27882],{"class":197,"line":198},[195,27883,27884],{"class":415},"# l10n.yaml\n",[195,27886,27887,27890,27892],{"class":197,"line":230},[195,27888,27889],{"class":205},"arb-dir",[195,27891,217],{"class":209},[195,27893,27894],{"class":459},"lib\u002Fl10n\n",[195,27896,27897,27900,27902],{"class":197,"line":251},[195,27898,27899],{"class":205},"template-arb-file",[195,27901,217],{"class":209},[195,27903,27904],{"class":459},"app_en.arb\n",[195,27906,27907,27910,27912],{"class":197,"line":272},[195,27908,27909],{"class":205},"output-localization-file",[195,27911,217],{"class":209},[195,27913,27914],{"class":459},"app_localizations.dart\n",[186,27916,27918],{"className":817,"code":27917,"language":819,"meta":191,"style":191},"\u002F\u002F lib\u002Fl10n\u002Fapp_en.arb\n{\n  \"@@locale\": \"en\",\n  \"appTitle\": \"My App\",\n  \"greeting\": \"Hello, {name}!\",\n  \"@greeting\": {\n    \"placeholders\": {\n      \"name\": { \"type\": \"String\" }\n    }\n  },\n  \"itemCount\": \"{count, plural, =0{No items} =1{1 item} other{{count} items}}\",\n  \"@itemCount\": {\n    \"placeholders\": {\n      \"count\": { \"type\": \"int\" }\n    }\n  }\n}\n",[136,27919,27920,27925,27929,27941,27953,27965,27972,27979,27997,28001,28005,28017,28024,28030,28046,28050,28054],{"__ignoreMap":191},[195,27921,27922],{"class":197,"line":198},[195,27923,27924],{"class":415},"\u002F\u002F lib\u002Fl10n\u002Fapp_en.arb\n",[195,27926,27927],{"class":197,"line":230},[195,27928,826],{"class":209},[195,27930,27931,27934,27936,27939],{"class":197,"line":251},[195,27932,27933],{"class":213},"  \"@@locale\"",[195,27935,217],{"class":209},[195,27937,27938],{"class":459},"\"en\"",[195,27940,942],{"class":209},[195,27942,27943,27946,27948,27951],{"class":197,"line":272},[195,27944,27945],{"class":213},"  \"appTitle\"",[195,27947,217],{"class":209},[195,27949,27950],{"class":459},"\"My App\"",[195,27952,942],{"class":209},[195,27954,27955,27958,27960,27963],{"class":197,"line":293},[195,27956,27957],{"class":213},"  \"greeting\"",[195,27959,217],{"class":209},[195,27961,27962],{"class":459},"\"Hello, {name}!\"",[195,27964,942],{"class":209},[195,27966,27967,27970],{"class":197,"line":562},[195,27968,27969],{"class":213},"  \"@greeting\"",[195,27971,926],{"class":209},[195,27973,27974,27977],{"class":197,"line":583},[195,27975,27976],{"class":213},"    \"placeholders\"",[195,27978,926],{"class":209},[195,27980,27981,27984,27987,27990,27992,27995],{"class":197,"line":962},[195,27982,27983],{"class":213},"      \"name\"",[195,27985,27986],{"class":209},": { ",[195,27988,27989],{"class":213},"\"type\"",[195,27991,217],{"class":209},[195,27993,27994],{"class":459},"\"String\"",[195,27996,18291],{"class":209},[195,27998,27999],{"class":197,"line":968},[195,28000,2403],{"class":209},[195,28002,28003],{"class":197,"line":1274},[195,28004,11963],{"class":209},[195,28006,28007,28010,28012,28015],{"class":197,"line":1282},[195,28008,28009],{"class":213},"  \"itemCount\"",[195,28011,217],{"class":209},[195,28013,28014],{"class":459},"\"{count, plural, =0{No items} =1{1 item} other{{count} items}}\"",[195,28016,942],{"class":209},[195,28018,28019,28022],{"class":197,"line":1295},[195,28020,28021],{"class":213},"  \"@itemCount\"",[195,28023,926],{"class":209},[195,28025,28026,28028],{"class":197,"line":1309},[195,28027,27976],{"class":213},[195,28029,926],{"class":209},[195,28031,28032,28035,28037,28039,28041,28044],{"class":197,"line":2246},[195,28033,28034],{"class":213},"      \"count\"",[195,28036,27986],{"class":209},[195,28038,27989],{"class":213},[195,28040,217],{"class":209},[195,28042,28043],{"class":459},"\"int\"",[195,28045,18291],{"class":209},[195,28047,28048],{"class":197,"line":1996},[195,28049,2403],{"class":209},[195,28051,28052],{"class":197,"line":2257},[195,28053,965],{"class":209},[195,28055,28056],{"class":197,"line":2262},[195,28057,552],{"class":209},[186,28059,28061],{"className":817,"code":28060,"language":819,"meta":191,"style":191},"\u002F\u002F lib\u002Fl10n\u002Fapp_zh.arb\n{\n  \"@@locale\": \"zh\",\n  \"appTitle\": \"我的应用\",\n  \"greeting\": \"你好, {name}!\",\n  \"itemCount\": \"{count, plural, =0{没有项目} other{{count} 个项目}}\"\n}\n",[136,28062,28063,28068,28072,28083,28094,28105,28114],{"__ignoreMap":191},[195,28064,28065],{"class":197,"line":198},[195,28066,28067],{"class":415},"\u002F\u002F lib\u002Fl10n\u002Fapp_zh.arb\n",[195,28069,28070],{"class":197,"line":230},[195,28071,826],{"class":209},[195,28073,28074,28076,28078,28081],{"class":197,"line":251},[195,28075,27933],{"class":213},[195,28077,217],{"class":209},[195,28079,28080],{"class":459},"\"zh\"",[195,28082,942],{"class":209},[195,28084,28085,28087,28089,28092],{"class":197,"line":272},[195,28086,27945],{"class":213},[195,28088,217],{"class":209},[195,28090,28091],{"class":459},"\"我的应用\"",[195,28093,942],{"class":209},[195,28095,28096,28098,28100,28103],{"class":197,"line":293},[195,28097,27957],{"class":213},[195,28099,217],{"class":209},[195,28101,28102],{"class":459},"\"你好, {name}!\"",[195,28104,942],{"class":209},[195,28106,28107,28109,28111],{"class":197,"line":562},[195,28108,28009],{"class":213},[195,28110,217],{"class":209},[195,28112,28113],{"class":459},"\"{count, plural, =0{没有项目} other{{count} 个项目}}\"\n",[195,28115,28116],{"class":197,"line":583},[195,28117,552],{"class":209},[186,28119,28121],{"className":11162,"code":28120,"language":11164,"meta":191,"style":191},"\u002F\u002F 配置\nMaterialApp(\n  localizationsDelegates: AppLocalizations.localizationsDelegates,\n  supportedLocales: AppLocalizations.supportedLocales,\n)\n\n\u002F\u002F 使用\nText(AppLocalizations.of(context)!.greeting('Flutter'))\nText(AppLocalizations.of(context)!.itemCount(5))\n",[136,28122,28123,28128,28132,28137,28142,28146,28150,28154,28159],{"__ignoreMap":191},[195,28124,28125],{"class":197,"line":198},[195,28126,28127],{},"\u002F\u002F 配置\n",[195,28129,28130],{"class":197,"line":230},[195,28131,24540],{},[195,28133,28134],{"class":197,"line":251},[195,28135,28136],{},"  localizationsDelegates: AppLocalizations.localizationsDelegates,\n",[195,28138,28139],{"class":197,"line":272},[195,28140,28141],{},"  supportedLocales: AppLocalizations.supportedLocales,\n",[195,28143,28144],{"class":197,"line":293},[195,28145,410],{},[195,28147,28148],{"class":197,"line":562},[195,28149,1241],{"emptyLinePlaceholder":757},[195,28151,28152],{"class":197,"line":583},[195,28153,3056],{},[195,28155,28156],{"class":197,"line":962},[195,28157,28158],{},"Text(AppLocalizations.of(context)!.greeting('Flutter'))\n",[195,28160,28161],{"class":197,"line":968},[195,28162,28163],{},"Text(AppLocalizations.of(context)!.itemCount(5))\n",[1890,28165],{},[32,28167,28169],{"id":28168},"q125-如何处理-flutter-中的内存泄漏","Q12.5: 如何处理 Flutter 中的内存泄漏？",[14,28171,28172],{},[125,28173,2077],{},[14,28175,28176],{},[125,28177,28178],{},"常见内存泄漏场景及解决方案：",[186,28180,28182],{"className":11162,"code":28181,"language":11164,"meta":191,"style":191},"\u002F\u002F ❌ 泄漏 1：未取消的 Stream 订阅\nclass _MyState extends State\u003CMyWidget> {\n  late StreamSubscription _sub;\n\n  @override\n  void initState() {\n    super.initState();\n    _sub = stream.listen((data) => setState(() {}));\n  }\n\n  \u002F\u002F ❌ 忘记取消订阅\n}\n\n\u002F\u002F ✅ 修复：在 dispose 中取消\n@override\nvoid dispose() {\n  _sub.cancel();\n  super.dispose();\n}\n",[136,28183,28184,28189,28194,28199,28203,28207,28211,28215,28220,28224,28228,28233,28237,28241,28246,28250,28255,28260,28265],{"__ignoreMap":191},[195,28185,28186],{"class":197,"line":198},[195,28187,28188],{},"\u002F\u002F ❌ 泄漏 1：未取消的 Stream 订阅\n",[195,28190,28191],{"class":197,"line":230},[195,28192,28193],{},"class _MyState extends State\u003CMyWidget> {\n",[195,28195,28196],{"class":197,"line":251},[195,28197,28198],{},"  late StreamSubscription _sub;\n",[195,28200,28201],{"class":197,"line":272},[195,28202,1241],{"emptyLinePlaceholder":757},[195,28204,28205],{"class":197,"line":293},[195,28206,12090],{},[195,28208,28209],{"class":197,"line":562},[195,28210,22974],{},[195,28212,28213],{"class":197,"line":583},[195,28214,26975],{},[195,28216,28217],{"class":197,"line":962},[195,28218,28219],{},"    _sub = stream.listen((data) => setState(() {}));\n",[195,28221,28222],{"class":197,"line":968},[195,28223,965],{},[195,28225,28226],{"class":197,"line":1274},[195,28227,1241],{"emptyLinePlaceholder":757},[195,28229,28230],{"class":197,"line":1282},[195,28231,28232],{},"  \u002F\u002F ❌ 忘记取消订阅\n",[195,28234,28235],{"class":197,"line":1295},[195,28236,552],{},[195,28238,28239],{"class":197,"line":1309},[195,28240,1241],{"emptyLinePlaceholder":757},[195,28242,28243],{"class":197,"line":2246},[195,28244,28245],{},"\u002F\u002F ✅ 修复：在 dispose 中取消\n",[195,28247,28248],{"class":197,"line":1996},[195,28249,16456],{},[195,28251,28252],{"class":197,"line":2257},[195,28253,28254],{},"void dispose() {\n",[195,28256,28257],{"class":197,"line":2262},[195,28258,28259],{},"  _sub.cancel();\n",[195,28261,28262],{"class":197,"line":2267},[195,28263,28264],{},"  super.dispose();\n",[195,28266,28267],{"class":197,"line":2273},[195,28268,552],{},[186,28270,28272],{"className":11162,"code":28271,"language":11164,"meta":191,"style":191},"\u002F\u002F ❌ 泄漏 2：闭包持有 BuildContext 或 State\nvoid _fetchData() async {\n  final data = await api.getData();\n  \u002F\u002F 此时 Widget 可能已经被销毁\n  setState(() { _data = data; }); \u002F\u002F 可能报错或泄漏\n}\n\n\u002F\u002F ✅ 修复：检查 mounted\nvoid _fetchData() async {\n  final data = await api.getData();\n  if (!mounted) return; \u002F\u002F Widget 已销毁，直接返回\n  setState(() { _data = data; });\n}\n",[136,28273,28274,28279,28284,28289,28294,28299,28303,28307,28312,28316,28320,28325,28330],{"__ignoreMap":191},[195,28275,28276],{"class":197,"line":198},[195,28277,28278],{},"\u002F\u002F ❌ 泄漏 2：闭包持有 BuildContext 或 State\n",[195,28280,28281],{"class":197,"line":230},[195,28282,28283],{},"void _fetchData() async {\n",[195,28285,28286],{"class":197,"line":251},[195,28287,28288],{},"  final data = await api.getData();\n",[195,28290,28291],{"class":197,"line":272},[195,28292,28293],{},"  \u002F\u002F 此时 Widget 可能已经被销毁\n",[195,28295,28296],{"class":197,"line":293},[195,28297,28298],{},"  setState(() { _data = data; }); \u002F\u002F 可能报错或泄漏\n",[195,28300,28301],{"class":197,"line":562},[195,28302,552],{},[195,28304,28305],{"class":197,"line":583},[195,28306,1241],{"emptyLinePlaceholder":757},[195,28308,28309],{"class":197,"line":962},[195,28310,28311],{},"\u002F\u002F ✅ 修复：检查 mounted\n",[195,28313,28314],{"class":197,"line":968},[195,28315,28283],{},[195,28317,28318],{"class":197,"line":1274},[195,28319,28288],{},[195,28321,28322],{"class":197,"line":1282},[195,28323,28324],{},"  if (!mounted) return; \u002F\u002F Widget 已销毁，直接返回\n",[195,28326,28327],{"class":197,"line":1295},[195,28328,28329],{},"  setState(() { _data = data; });\n",[195,28331,28332],{"class":197,"line":1309},[195,28333,552],{},[186,28335,28337],{"className":11162,"code":28336,"language":11164,"meta":191,"style":191},"\u002F\u002F ❌ 泄漏 3：AnimationController 未释放\nclass _MyState extends State\u003CMyWidget> with TickerProviderStateMixin {\n  late final controller = AnimationController(vsync: this);\n\n  \u002F\u002F ❌ 忘记 dispose\n\n  \u002F\u002F ✅ 修复\n  @override\n  void dispose() {\n    controller.dispose();\n    super.dispose();\n  }\n}\n",[136,28338,28339,28344,28349,28354,28358,28363,28367,28372,28376,28380,28385,28389,28393],{"__ignoreMap":191},[195,28340,28341],{"class":197,"line":198},[195,28342,28343],{},"\u002F\u002F ❌ 泄漏 3：AnimationController 未释放\n",[195,28345,28346],{"class":197,"line":230},[195,28347,28348],{},"class _MyState extends State\u003CMyWidget> with TickerProviderStateMixin {\n",[195,28350,28351],{"class":197,"line":251},[195,28352,28353],{},"  late final controller = AnimationController(vsync: this);\n",[195,28355,28356],{"class":197,"line":272},[195,28357,1241],{"emptyLinePlaceholder":757},[195,28359,28360],{"class":197,"line":293},[195,28361,28362],{},"  \u002F\u002F ❌ 忘记 dispose\n",[195,28364,28365],{"class":197,"line":562},[195,28366,1241],{"emptyLinePlaceholder":757},[195,28368,28369],{"class":197,"line":583},[195,28370,28371],{},"  \u002F\u002F ✅ 修复\n",[195,28373,28374],{"class":197,"line":962},[195,28375,12090],{},[195,28377,28378],{"class":197,"line":968},[195,28379,23084],{},[195,28381,28382],{"class":197,"line":1274},[195,28383,28384],{},"    controller.dispose();\n",[195,28386,28387],{"class":197,"line":1282},[195,28388,27289],{},[195,28390,28391],{"class":197,"line":1295},[195,28392,965],{},[195,28394,28395],{"class":197,"line":1309},[195,28396,552],{},[186,28398,28400],{"className":11162,"code":28399,"language":11164,"meta":191,"style":191},"\u002F\u002F ❌ 泄漏 4：全局\u002F静态引用持有 Widget\nclass Cache {\n  static Widget? lastWidget; \u002F\u002F 永远不会被 GC\n}\n\n\u002F\u002F ✅ 修复：避免静态引用 Widget\u002FState\u002FBuildContext\n",[136,28401,28402,28407,28412,28417,28421,28425],{"__ignoreMap":191},[195,28403,28404],{"class":197,"line":198},[195,28405,28406],{},"\u002F\u002F ❌ 泄漏 4：全局\u002F静态引用持有 Widget\n",[195,28408,28409],{"class":197,"line":230},[195,28410,28411],{},"class Cache {\n",[195,28413,28414],{"class":197,"line":251},[195,28415,28416],{},"  static Widget? lastWidget; \u002F\u002F 永远不会被 GC\n",[195,28418,28419],{"class":197,"line":272},[195,28420,552],{},[195,28422,28423],{"class":197,"line":293},[195,28424,1241],{"emptyLinePlaceholder":757},[195,28426,28427],{"class":197,"line":562},[195,28428,28429],{},"\u002F\u002F ✅ 修复：避免静态引用 Widget\u002FState\u002FBuildContext\n",[14,28431,28432],{},[125,28433,28434],{},"检测工具：",[129,28436,28437,28440,28448],{},[132,28438,28439],{},"DevTools Memory 面板：查看对象分配和 GC",[132,28441,28442,905,28445],{},[136,28443,28444],{},"dart:developer",[136,28446,28447],{},"Service.getMemoryUsage()",[132,28449,28450],{},"LeakTracking（Flutter 3.18+）：自动检测某些类型的泄漏",[1890,28452],{},[18,28454,28456],{"id":28455},"附录高频考察知识点速查","附录：高频考察知识点速查",[36,28458,28459,28467],{},[39,28460,28461],{},[42,28462,28463,28465],{},[45,28464,15099],{},[45,28466,15102],{},[52,28468,28469,28476,28483,28490,28498,28505,28512,28519],{},[42,28470,28471,28473],{},[57,28472,15241],{},[57,28474,28475],{},"三棵树、Constraints go down \u002F Sizes go up、RepaintBoundary、Impeller",[42,28477,28478,28480],{},[57,28479,2014],{},[57,28481,28482],{},"InheritedWidget 原理、BLoC vs Riverpod、setState 粒度",[42,28484,28485,28487],{},[57,28486,3340],{},[57,28488,28489],{},"const Widget、ListView.builder、AnimatedBuilder child、Isolate",[42,28491,28492,28495],{},[57,28493,28494],{},"异步",[57,28496,28497],{},"Event Loop、Microtask vs Event Queue、Stream vs Future、Isolate",[42,28499,28500,28502],{},[57,28501,15256],{},[57,28503,28504],{},"MethodChannel、EventChannel、FFI",[42,28506,28507,28509],{},[57,28508,12189],{},[57,28510,28511],{},"Widget Test、pumpAndSettle、Golden Test、Mock",[42,28513,28514,28516],{},[57,28515,5860],{},[57,28517,28518],{},"Clean Architecture、MVVM、依赖注入",[42,28520,28521,28523],{},[57,28522,15233],{},[57,28524,28525],{},"Null Safety、Mixin、Sealed Class、Extension、泛型协变",[733,28527,28528],{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":191,"searchDepth":230,"depth":230,"links":28530},[28531,28532,28544,28550,28557,28562,28569,28573,28577,28580,28584,28588,28592,28599],{"id":1894,"depth":230,"text":1894},{"id":21457,"depth":230,"text":21458,"children":28533},[28534,28536,28538,28540,28542],{"id":21461,"depth":251,"text":28535},"Q1.1: Dart 中 const 和 final 的区别是什么？",{"id":21610,"depth":251,"text":28537},"Q1.2: Dart 的空安全（Null Safety）机制是怎样的？late 关键字有什么作用和风险？",{"id":21760,"depth":251,"text":28539},"Q1.3: Dart 中的 mixin 是什么？和抽象类、接口有什么区别？",{"id":21995,"depth":251,"text":28541},"Q1.4: 解释 Dart 中的 Extension 方法和 sealed class",{"id":22162,"depth":251,"text":28543},"Q1.5: Dart 中的泛型协变（Covariance）是什么？为什么 List\u003CCat> 可以赋值给 List\u003CAnimal>？",{"id":22283,"depth":230,"text":22284,"children":28545},[28546,28547,28548],{"id":22287,"depth":251,"text":22288},{"id":22332,"depth":251,"text":22333},{"id":22419,"depth":251,"text":28549},"Q2.3: 解释 Flutter 中的 RepaintBoundary 的作用和使用场景",{"id":22563,"depth":230,"text":22564,"children":28551},[28552,28553,28555],{"id":22567,"depth":251,"text":22568},{"id":22722,"depth":251,"text":28554},"Q3.2: Key 的作用是什么？什么时候必须使用 Key？",{"id":22926,"depth":251,"text":28556},"Q3.3: StatefulWidget 的完整生命周期是怎样的？",{"id":23107,"depth":230,"text":23108,"children":28558},[28559,28560],{"id":23111,"depth":251,"text":23112},{"id":23430,"depth":251,"text":28561},"Q4.2: InheritedWidget 是如何工作的？为什么 Theme.of(context) 能获取到主题数据？",{"id":23611,"depth":230,"text":23612,"children":28563},[28564,28565,28567],{"id":23615,"depth":251,"text":23616},{"id":23755,"depth":251,"text":28566},"Q5.2: Future 和 Stream 的区别是什么？StreamController 怎么用？",{"id":24026,"depth":251,"text":28568},"Q5.3: 解释 async* 和 yield 的用法",{"id":24151,"depth":230,"text":24152,"children":28570},[28571,28572],{"id":24155,"depth":251,"text":24156},{"id":24520,"depth":251,"text":24521},{"id":24716,"depth":230,"text":24717,"children":28574},[28575,28576],{"id":24720,"depth":251,"text":24721},{"id":25003,"depth":251,"text":25004},{"id":25157,"depth":230,"text":25158,"children":28578},[28579],{"id":25161,"depth":251,"text":25162},{"id":25471,"depth":230,"text":25472,"children":28581},[28582,28583],{"id":25475,"depth":251,"text":25476},{"id":25850,"depth":251,"text":25851},{"id":26002,"depth":230,"text":26003,"children":28585},[28586,28587],{"id":26006,"depth":251,"text":26007},{"id":26342,"depth":251,"text":26343},{"id":26552,"depth":230,"text":26553,"children":28589},[28590,28591],{"id":26556,"depth":251,"text":26557},{"id":26714,"depth":251,"text":26715},{"id":26887,"depth":230,"text":26888,"children":28593},[28594,28595,28596,28597,28598],{"id":26891,"depth":251,"text":26892},{"id":27304,"depth":251,"text":27305},{"id":27530,"depth":251,"text":27531},{"id":27806,"depth":251,"text":27807},{"id":28168,"depth":251,"text":28169},{"id":28455,"depth":230,"text":28456},"2026-05-03 10:10:00 CST","覆盖 Dart 语言、渲染机制、状态管理、异步编程、性能优化等 Flutter 中高级面试核心考点。",{},"\u002Fnotes\u002F2026-05-03-flutter-interview",{"title":21375,"description":28601},"Flutter 中高级工程师面试题详解，涵盖 Dart 基础、渲染机制、Widget 树、状态管理、异步编程和性能优化。","Flutter 中高级工程师面试题与详解｜个人笔记","notes\u002F2026-05-03-flutter-interview","nUaoweAKJHeLHnMa5K8lOJO5U2XVaaDZh-ecZG0o0sQ",{"id":28610,"title":28611,"body":28612,"category":752,"date":36363,"description":36364,"extension":755,"meta":36365,"navigation":757,"order":752,"path":36366,"seo":36367,"seoDescription":36368,"seoTitle":36369,"slug":36370,"stem":36371,"__hash__":36372},"notes\u002Fnotes\u002F2026-05-03-cross-platform-comparison.md","Flutter \u002F iOS \u002F Android \u002F Vue 四平台横向对比",{"type":8,"value":28613,"toc":36299},[28614,28619,28621,28623,28696,28698,28702,28706,28814,28818,28853,28898,28938,29038,29051,29087,29109,29111,29115,29236,29270,29312,29337,29428,29442,29474,29488,29490,29494,29676,29722,29724,29728,29774,29831,29879,30059,30083,30085,30089,30093,30202,30206,30253,30298,30340,30428,30448,30473,30475,30479,30606,30647,30649,30653,30681,30718,30754,30803,30851,30853,30857,30861,30966,31031,31066,31096,31190,31219,31261,31263,31267,31356,31387,31428,31430,31434,31530,31549,31551,31555,31559,31766,31770,31776,31809,31850,31887,31889,31893,31897,32060,32064,32129,32172,32215,32320,32344,32393,32422,32424,32428,32432,32564,32568,32621,32654,32679,32787,32822,32859,32891,32893,32897,32989,33014,33060,33062,33066,33070,33163,33167,33293,33326,33358,33375,33377,33381,33385,33514,33518,33557,33603,33659,33776,33811,33866,33900,33902,33906,34061,34063,34067,34071,34171,34175,34181,34202,34221,34223,34227,34379,34426,34428,34432,34436,34528,34532,34538,34557,34595,34638,34660,34681,34698,34700,34704,34708,34792,34865,34946,35025,35171,35173,35177,35277,35297,35314,35316,35320,35388,35390,35394,35398,35498,35502,35614,35618,35662,35706,35749,35869,35902,35933,35947,35949,35953,36038,36055,36057,36061,36296],[11,28615,28616],{},[14,28617,28618],{},"以同一主题为轴，横向对比四个平台的基础、重难点、易错点与面试高频考点。\n适合有 Flutter\u002FiOS 经验、正在拓展 Android 和 Vue 的工程师复习使用。",[1890,28620],{},[18,28622,1894],{"id":1894},[706,28624,28625,28631,28637,28642,28648,28654,28660,28666,28672,28678,28684,28690],{},[132,28626,28627],{},[1904,28628,28630],{"href":28629},"#1-%E8%AF%AD%E8%A8%80%E5%9F%BA%E7%A1%80%E5%AF%B9%E6%AF%94","语言基础对比",[132,28632,28633],{},[1904,28634,28636],{"href":28635},"#2-ui-%E6%9E%84%E5%BB%BA%E8%8C%83%E5%BC%8F","UI 构建范式",[132,28638,28639],{},[1904,28640,2014],{"href":28641},"#3-%E7%8A%B6%E6%80%81%E7%AE%A1%E7%90%86",[132,28643,28644],{},[1904,28645,28647],{"href":28646},"#4-%E5%B8%83%E5%B1%80%E7%B3%BB%E7%BB%9F","布局系统",[132,28649,28650],{},[1904,28651,28653],{"href":28652},"#5-%E5%AF%BC%E8%88%AA%E4%B8%8E%E8%B7%AF%E7%94%B1","导航与路由",[132,28655,28656],{},[1904,28657,28659],{"href":28658},"#6-%E7%BD%91%E7%BB%9C%E4%B8%8E%E6%95%B0%E6%8D%AE%E5%B1%82","网络与数据层",[132,28661,28662],{},[1904,28663,28665],{"href":28664},"#7-%E6%8C%81%E4%B9%85%E5%8C%96%E5%AD%98%E5%82%A8","持久化存储",[132,28667,28668],{},[1904,28669,28671],{"href":28670},"#8-%E5%B9%B6%E5%8F%91%E4%B8%8E%E5%BC%82%E6%AD%A5%E7%BC%96%E7%A8%8B","并发与异步编程",[132,28673,28674],{},[1904,28675,28677],{"href":28676},"#9-%E6%B8%B2%E6%9F%93%E6%9C%BA%E5%88%B6%E4%B8%8E%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96","渲染机制与性能优化",[132,28679,28680],{},[1904,28681,28683],{"href":28682},"#10-%E5%B9%B3%E5%8F%B0%E9%80%9A%E4%BF%A1%E4%B8%8E%E5%8E%9F%E7%94%9F%E8%83%BD%E5%8A%9B","平台通信与原生能力",[132,28685,28686],{},[1904,28687,28689],{"href":28688},"#11-%E6%9E%B6%E6%9E%84%E6%A8%A1%E5%BC%8F","架构模式",[132,28691,28692],{},[1904,28693,28695],{"href":28694},"#12-%E6%9E%84%E5%BB%BA%E6%B5%8B%E8%AF%95%E4%B8%8E%E5%8F%91%E5%B8%83","构建、测试与发布",[1890,28697],{},[18,28699,28701],{"id":28700},"_1-语言基础对比","1. 语言基础对比",[32,28703,28705],{"id":28704},"_11-语言概览","1.1 语言概览",[36,28707,28708,28726],{},[39,28709,28710],{},[42,28711,28712,28714,28717,28720,28723],{},[45,28713,15463],{},[45,28715,28716],{},"Dart (Flutter)",[45,28718,28719],{},"Swift (iOS)",[45,28721,28722],{},"Kotlin (Android)",[45,28724,28725],{},"TypeScript (Vue)",[52,28727,28728,28743,28766,28781,28797],{},[42,28729,28730,28733,28736,28738,28740],{},[57,28731,28732],{},"类型系统",[57,28734,28735],{},"静态强类型",[57,28737,28735],{},[57,28739,28735],{},[57,28741,28742],{},"静态强类型（JS超集）",[42,28744,28745,28748,28751,28758,28763],{},[57,28746,28747],{},"空安全",[57,28749,28750],{},"Sound null safety",[57,28752,28753,28754,28757],{},"Optional (",[136,28755,28756],{},"?",")",[57,28759,28760,28761,28757],{},"Nullable (",[136,28762,28756],{},[57,28764,28765],{},"strict null checks",[42,28767,28768,28770,28773,28775,28778],{},[57,28769,26572],{},[57,28771,28772],{},"AOT (release) + JIT (debug)",[57,28774,26578],{},[57,28776,28777],{},"JVM 字节码 \u002F Native",[57,28779,28780],{},"转译为 JS",[42,28782,28783,28785,28788,28791,28794],{},[57,28784,1913],{},[57,28786,28787],{},"GC（分代回收）",[57,28789,28790],{},"ARC（引用计数）",[57,28792,28793],{},"GC（JVM）",[57,28795,28796],{},"GC（V8 引擎）",[42,28798,28799,28802,28805,28808,28811],{},[57,28800,28801],{},"函数式支持",[57,28803,28804],{},"有（不如 Kotlin 强）",[57,28806,28807],{},"协议 + 泛型 + 高阶函数",[57,28809,28810],{},"最强（扩展函数、作用域函数）",[57,28812,28813],{},"原生支持",[32,28815,28817],{"id":28816},"_12-空安全机制对比","1.2 空安全机制对比",[186,28819,28821],{"className":11162,"code":28820,"language":11164,"meta":191,"style":191},"\u002F\u002F Dart — Sound null safety\nString? name;          \u002F\u002F 可空\nString nonNull = '';   \u002F\u002F 不可空\nname?.length;          \u002F\u002F 安全调用\nname!.length;          \u002F\u002F 强制解包（可能抛异常）\nname ?? 'default';     \u002F\u002F 空合并\n",[136,28822,28823,28828,28833,28838,28843,28848],{"__ignoreMap":191},[195,28824,28825],{"class":197,"line":198},[195,28826,28827],{},"\u002F\u002F Dart — Sound null safety\n",[195,28829,28830],{"class":197,"line":230},[195,28831,28832],{},"String? name;          \u002F\u002F 可空\n",[195,28834,28835],{"class":197,"line":251},[195,28836,28837],{},"String nonNull = '';   \u002F\u002F 不可空\n",[195,28839,28840],{"class":197,"line":272},[195,28841,28842],{},"name?.length;          \u002F\u002F 安全调用\n",[195,28844,28845],{"class":197,"line":293},[195,28846,28847],{},"name!.length;          \u002F\u002F 强制解包（可能抛异常）\n",[195,28849,28850],{"class":197,"line":562},[195,28851,28852],{},"name ?? 'default';     \u002F\u002F 空合并\n",[186,28854,28856],{"className":2177,"code":28855,"language":2179,"meta":191,"style":191},"\u002F\u002F Swift — Optional\nvar name: String?       \u002F\u002F 可空\nvar nonNull: String = \"\"\u002F\u002F 不可空\nname?.count             \u002F\u002F 可选链\nname!.count             \u002F\u002F 强制解包（可能崩溃）\nname ?? \"default\"       \u002F\u002F 空合并\nif let n = name { }     \u002F\u002F 可选绑定（Dart 没有）\nguard let n = name else { return }  \u002F\u002F 提前退出\n",[136,28857,28858,28863,28868,28873,28878,28883,28888,28893],{"__ignoreMap":191},[195,28859,28860],{"class":197,"line":198},[195,28861,28862],{},"\u002F\u002F Swift — Optional\n",[195,28864,28865],{"class":197,"line":230},[195,28866,28867],{},"var name: String?       \u002F\u002F 可空\n",[195,28869,28870],{"class":197,"line":251},[195,28871,28872],{},"var nonNull: String = \"\"\u002F\u002F 不可空\n",[195,28874,28875],{"class":197,"line":272},[195,28876,28877],{},"name?.count             \u002F\u002F 可选链\n",[195,28879,28880],{"class":197,"line":293},[195,28881,28882],{},"name!.count             \u002F\u002F 强制解包（可能崩溃）\n",[195,28884,28885],{"class":197,"line":562},[195,28886,28887],{},"name ?? \"default\"       \u002F\u002F 空合并\n",[195,28889,28890],{"class":197,"line":583},[195,28891,28892],{},"if let n = name { }     \u002F\u002F 可选绑定（Dart 没有）\n",[195,28894,28895],{"class":197,"line":962},[195,28896,28897],{},"guard let n = name else { return }  \u002F\u002F 提前退出\n",[186,28899,28901],{"className":6838,"code":28900,"language":6840,"meta":191,"style":191},"\u002F\u002F Kotlin — Nullable\nvar name: String? = null  \u002F\u002F 可空\nvar nonNull: String = \"\"  \u002F\u002F 不可空\nname?.length              \u002F\u002F 安全调用\nname!!.length             \u002F\u002F 强制解包\nname ?: \"default\"         \u002F\u002F Elvis 操作符\nname?.let { println(it) } \u002F\u002F 作用域函数（Dart\u002FSwift 没有）\n",[136,28902,28903,28908,28913,28918,28923,28928,28933],{"__ignoreMap":191},[195,28904,28905],{"class":197,"line":198},[195,28906,28907],{},"\u002F\u002F Kotlin — Nullable\n",[195,28909,28910],{"class":197,"line":230},[195,28911,28912],{},"var name: String? = null  \u002F\u002F 可空\n",[195,28914,28915],{"class":197,"line":251},[195,28916,28917],{},"var nonNull: String = \"\"  \u002F\u002F 不可空\n",[195,28919,28920],{"class":197,"line":272},[195,28921,28922],{},"name?.length              \u002F\u002F 安全调用\n",[195,28924,28925],{"class":197,"line":293},[195,28926,28927],{},"name!!.length             \u002F\u002F 强制解包\n",[195,28929,28930],{"class":197,"line":562},[195,28931,28932],{},"name ?: \"default\"         \u002F\u002F Elvis 操作符\n",[195,28934,28935],{"class":197,"line":583},[195,28936,28937],{},"name?.let { println(it) } \u002F\u002F 作用域函数（Dart\u002FSwift 没有）\n",[186,28939,28941],{"className":383,"code":28940,"language":385,"meta":191,"style":191},"\u002F\u002F TypeScript — strict null checks\nlet name: string | null = null;  \u002F\u002F 联合类型\nlet nonNull: string = \"\";\nname?.length;                     \u002F\u002F 可选链\nname!.length;                     \u002F\u002F 非空断言\nname ?? \"default\";                \u002F\u002F 空值合并\n",[136,28942,28943,28948,28974,28992,29006,29022],{"__ignoreMap":191},[195,28944,28945],{"class":197,"line":198},[195,28946,28947],{"class":415},"\u002F\u002F TypeScript — strict null checks\n",[195,28949,28950,28952,28955,28957,28959,28962,28965,28967,28969,28971],{"class":197,"line":230},[195,28951,7138],{"class":223},[195,28953,28954],{"class":209}," name",[195,28956,638],{"class":223},[195,28958,15715],{"class":213},[195,28960,28961],{"class":223}," |",[195,28963,28964],{"class":213}," null",[195,28966,398],{"class":223},[195,28968,28964],{"class":213},[195,28970,1229],{"class":209},[195,28972,28973],{"class":415},"\u002F\u002F 联合类型\n",[195,28975,28976,28978,28981,28983,28985,28987,28990],{"class":197,"line":251},[195,28977,7138],{"class":223},[195,28979,28980],{"class":209}," nonNull",[195,28982,638],{"class":223},[195,28984,15715],{"class":213},[195,28986,398],{"class":223},[195,28988,28989],{"class":459}," \"\"",[195,28991,547],{"class":209},[195,28993,28994,28997,29000,29003],{"class":197,"line":272},[195,28995,28996],{"class":209},"name?.",[195,28998,28999],{"class":213},"length",[195,29001,29002],{"class":209},";                     ",[195,29004,29005],{"class":415},"\u002F\u002F 可选链\n",[195,29007,29008,29010,29013,29015,29017,29019],{"class":197,"line":293},[195,29009,13388],{"class":209},[195,29011,29012],{"class":223},"!",[195,29014,16407],{"class":209},[195,29016,28999],{"class":213},[195,29018,29002],{"class":209},[195,29020,29021],{"class":415},"\u002F\u002F 非空断言\n",[195,29023,29024,29027,29029,29032,29035],{"class":197,"line":562},[195,29025,29026],{"class":209},"name ",[195,29028,21754],{"class":223},[195,29030,29031],{"class":459}," \"default\"",[195,29033,29034],{"class":209},";                ",[195,29036,29037],{"class":415},"\u002F\u002F 空值合并\n",[11,29039,29040],{},[14,29041,29042,29043,29046,29047,29050],{},"⚠️ ",[125,29044,29045],{},"重难点","：Dart 的 null safety 是 sound 的（编译器保证运行时不会出现非空变量为 null），Swift 的 Optional 是类型系统的一部分（",[136,29048,29049],{},"Optional\u003CString>"," 是独立类型），Kotlin 在 JVM 上需要处理 Java 互操作的 platform type，TypeScript 的 null check 只在编译时生效，运行时仍是 JS。",[11,29052,29053,29059],{},[14,29054,29055,29056,333],{},"❌ ",[125,29057,29058],{},"易错点",[129,29060,29061,29067,29074,29077],{},[132,29062,29063,29064,29066],{},"Dart：",[136,29065,21614],{}," 变量未初始化就使用会运行时报错，不是编译时错误",[132,29068,29069,29070,29073],{},"Swift：",[136,29071,29072],{},"implicitly unwrapped optional (!)"," 声明容易忘记判空",[132,29075,29076],{},"Kotlin：Java 互操作时 platform type 不会触发空检查",[132,29078,29079,29080,29082,29083,29086],{},"TypeScript：",[136,29081,3205],{}," 类型绕过所有类型检查，",[136,29084,29085],{},"as"," 断言不做运行时校验",[11,29088,29089,29095],{},[14,29090,29091,29092,333],{},"🎯 ",[125,29093,29094],{},"面试考点",[129,29096,29097,29100,29106],{},[132,29098,29099],{},"Dart sound null safety 和 Kotlin null safety 的区别？（Dart 编译器完全保证，Kotlin 有 platform type 漏洞）",[132,29101,29102,29103,698],{},"Swift Optional 的本质是什么？（是枚举 ",[136,29104,29105],{},"enum Optional\u003CT> { case none, some(T) }",[132,29107,29108],{},"TypeScript 的类型检查发生在什么阶段？（仅编译时，运行时无类型信息）",[1890,29110],{},[32,29112,29114],{"id":29113},"_13-值类型-vs-引用类型","1.3 值类型 vs 引用类型",[36,29116,29117,29135],{},[39,29118,29119],{},[42,29120,29121,29124,29126,29129,29132],{},[45,29122,29123],{},"概念",[45,29125,15478],{},[45,29127,29128],{},"Swift",[45,29130,29131],{},"Kotlin",[45,29133,29134],{},"TypeScript",[52,29136,29137,29159,29185,29209],{},[42,29138,29139,29141,29144,29151,29156],{},[57,29140,4339],{},[57,29142,29143],{},"无（除 int\u002Fdouble 等基本类型）",[57,29145,29146,301,29148,29150],{},[136,29147,2068],{},[136,29149,2651],{},", 元组",[57,29152,29153,29155],{},[136,29154,6821],{},"（copy 语义）",[57,29157,29158],{},"原始类型（number, string, boolean）",[42,29160,29161,29164,29168,29172,29176],{},[57,29162,29163],{},"引用类型",[57,29165,29166],{},[136,29167,2071],{},[57,29169,29170],{},[136,29171,2071],{},[57,29173,29174],{},[136,29175,2071],{},[57,29177,29178,301,29180,301,29183],{},[136,29179,6827],{},[136,29181,29182],{},"array",[136,29184,2071],{},[42,29186,29187,29190,29195,29198,29203],{},[57,29188,29189],{},"Copy 机制",[57,29191,19644,29192],{},[136,29193,29194],{},"copyWith",[57,29196,29197],{},"自动 copy-on-write",[57,29199,19644,29200],{},[136,29201,29202],{},".copy()",[57,29204,29205,29206],{},"手动展开 ",[136,29207,29208],{},"{...obj}",[42,29210,29211,29214,29220,29224,29229],{},[57,29212,29213],{},"不可变",[57,29215,29216,16122,29218],{},[136,29217,21467],{},[136,29219,392],{},[57,29221,29222],{},[136,29223,7138],{},[57,29225,29226],{},[136,29227,29228],{},"val",[57,29230,29231,16122,29233],{},[136,29232,392],{},[136,29234,29235],{},"readonly",[186,29237,29239],{"className":11162,"code":29238,"language":11164,"meta":191,"style":191},"\u002F\u002F Dart — 没有 struct，用 class + copyWith 模拟值语义\nclass Point {\n  final int x, y;\n  const Point(this.x, this.y);\n  Point copyWith({int? x, int? y}) => Point(x ?? this.x, y ?? this.y);\n}\n",[136,29240,29241,29246,29251,29256,29261,29266],{"__ignoreMap":191},[195,29242,29243],{"class":197,"line":198},[195,29244,29245],{},"\u002F\u002F Dart — 没有 struct，用 class + copyWith 模拟值语义\n",[195,29247,29248],{"class":197,"line":230},[195,29249,29250],{},"class Point {\n",[195,29252,29253],{"class":197,"line":251},[195,29254,29255],{},"  final int x, y;\n",[195,29257,29258],{"class":197,"line":272},[195,29259,29260],{},"  const Point(this.x, this.y);\n",[195,29262,29263],{"class":197,"line":293},[195,29264,29265],{},"  Point copyWith({int? x, int? y}) => Point(x ?? this.x, y ?? this.y);\n",[195,29267,29268],{"class":197,"line":562},[195,29269,552],{},[186,29271,29273],{"className":2177,"code":29272,"language":2179,"meta":191,"style":191},"\u002F\u002F Swift — struct 是值类型，赋值即拷贝\nstruct Point {\n    var x: Int\n    var y: Int\n}\nvar a = Point(x: 1, y: 2)\nvar b = a       \u002F\u002F 拷贝，修改 b 不影响 a\nb.x = 10       \u002F\u002F a.x 仍为 1\n",[136,29274,29275,29280,29284,29289,29294,29298,29302,29307],{"__ignoreMap":191},[195,29276,29277],{"class":197,"line":198},[195,29278,29279],{},"\u002F\u002F Swift — struct 是值类型，赋值即拷贝\n",[195,29281,29282],{"class":197,"line":230},[195,29283,2191],{},[195,29285,29286],{"class":197,"line":251},[195,29287,29288],{},"    var x: Int\n",[195,29290,29291],{"class":197,"line":272},[195,29292,29293],{},"    var y: Int\n",[195,29295,29296],{"class":197,"line":293},[195,29297,552],{},[195,29299,29300],{"class":197,"line":562},[195,29301,2214],{},[195,29303,29304],{"class":197,"line":583},[195,29305,29306],{},"var b = a       \u002F\u002F 拷贝，修改 b 不影响 a\n",[195,29308,29309],{"class":197,"line":962},[195,29310,29311],{},"b.x = 10       \u002F\u002F a.x 仍为 1\n",[186,29313,29315],{"className":6838,"code":29314,"language":6840,"meta":191,"style":191},"\u002F\u002F Kotlin — data class 提供 copy()，但仍是引用类型\ndata class Point(val x: Int, val y: Int)\nval a = Point(1, 2)\nval b = a.copy(x = 10)  \u002F\u002F a 不变\n",[136,29316,29317,29322,29327,29332],{"__ignoreMap":191},[195,29318,29319],{"class":197,"line":198},[195,29320,29321],{},"\u002F\u002F Kotlin — data class 提供 copy()，但仍是引用类型\n",[195,29323,29324],{"class":197,"line":230},[195,29325,29326],{},"data class Point(val x: Int, val y: Int)\n",[195,29328,29329],{"class":197,"line":251},[195,29330,29331],{},"val a = Point(1, 2)\n",[195,29333,29334],{"class":197,"line":272},[195,29335,29336],{},"val b = a.copy(x = 10)  \u002F\u002F a 不变\n",[186,29338,29340],{"className":383,"code":29339,"language":385,"meta":191,"style":191},"\u002F\u002F TypeScript — 对象是引用类型，需展开操作符\ninterface Point { x: number; y: number }\nconst a: Point = { x: 1, y: 2 };\nconst b = { ...a, x: 10 };  \u002F\u002F 浅拷贝\n",[136,29341,29342,29347,29375,29402],{"__ignoreMap":191},[195,29343,29344],{"class":197,"line":198},[195,29345,29346],{"class":415},"\u002F\u002F TypeScript — 对象是引用类型，需展开操作符\n",[195,29348,29349,29352,29355,29357,29359,29361,29364,29367,29369,29371,29373],{"class":197,"line":230},[195,29350,29351],{"class":223},"interface",[195,29353,29354],{"class":201}," Point",[195,29356,210],{"class":209},[195,29358,19287],{"class":634},[195,29360,638],{"class":223},[195,29362,29363],{"class":213}," number",[195,29365,29366],{"class":209},"; ",[195,29368,19292],{"class":634},[195,29370,638],{"class":223},[195,29372,29363],{"class":213},[195,29374,18291],{"class":209},[195,29376,29377,29379,29382,29384,29386,29388,29391,29393,29396,29399],{"class":197,"line":251},[195,29378,392],{"class":223},[195,29380,29381],{"class":213}," a",[195,29383,638],{"class":223},[195,29385,29354],{"class":201},[195,29387,398],{"class":223},[195,29389,29390],{"class":209}," { x: ",[195,29392,1290],{"class":213},[195,29394,29395],{"class":209},", y: ",[195,29397,29398],{"class":213},"2",[195,29400,29401],{"class":209}," };\n",[195,29403,29404,29406,29409,29411,29413,29416,29419,29422,29425],{"class":197,"line":272},[195,29405,392],{"class":223},[195,29407,29408],{"class":213}," b",[195,29410,398],{"class":223},[195,29412,210],{"class":209},[195,29414,29415],{"class":223},"...",[195,29417,29418],{"class":209},"a, x: ",[195,29420,29421],{"class":213},"10",[195,29423,29424],{"class":209}," };  ",[195,29426,29427],{"class":415},"\u002F\u002F 浅拷贝\n",[11,29429,29430],{},[14,29431,29042,29432,29434,29435,29437,29438,29441],{},[125,29433,29045],{},"：Swift 的 struct 是真正的值类型（栈分配 + copy-on-write），Kotlin 的 ",[136,29436,6821],{}," 虽然提供 ",[136,29439,29440],{},"copy()"," 但本质仍在堆上。Dart 完全没有值类型 struct，全部是引用类型。",[11,29443,29444,29448],{},[14,29445,29055,29446,333],{},[125,29447,29058],{},[129,29449,29450,29459,29465],{},[132,29451,29452,29453,29455,29456,29458],{},"Swift 在 ",[136,29454,2068],{}," 中使用 ",[136,29457,2156],{}," 方法时容易混淆值语义",[132,29460,29461,29462,29464],{},"TypeScript 的 ",[136,29463,29208],{}," 只是浅拷贝，嵌套对象仍是引用共享",[132,29466,29467,29468,905,29470,29473],{},"Kotlin ",[136,29469,6821],{},[136,29471,29472],{},"equals()"," 只比较主构造函数参数",[11,29475,29476,29480],{},[14,29477,29091,29478,333],{},[125,29479,29094],{},[129,29481,29482,29485],{},[132,29483,29484],{},"Swift 中什么时候用 struct，什么时候用 class？（默认用 struct，需要继承\u002F引用语义\u002Fdeinit 时用 class）",[132,29486,29487],{},"Dart 为什么没有 struct？（Dart 全部对象在堆上，GC 管理，不适合栈分配值类型）",[1890,29489],{},[32,29491,29493],{"id":29492},"_14-集合操作与函数式编程","1.4 集合操作与函数式编程",[36,29495,29496,29511],{},[39,29497,29498],{},[42,29499,29500,29503,29505,29507,29509],{},[45,29501,29502],{},"操作",[45,29504,15478],{},[45,29506,29128],{},[45,29508,29131],{},[45,29510,29134],{},[52,29512,29513,29535,29558,29581,29604,29630,29655],{},[42,29514,29515,29518,29523,29527,29531],{},[57,29516,29517],{},"映射",[57,29519,29520],{},[136,29521,29522],{},".map()",[57,29524,29525],{},[136,29526,29522],{},[57,29528,29529],{},[136,29530,29522],{},[57,29532,29533],{},[136,29534,29522],{},[42,29536,29537,29540,29545,29550,29554],{},[57,29538,29539],{},"过滤",[57,29541,29542],{},[136,29543,29544],{},".where()",[57,29546,29547],{},[136,29548,29549],{},".filter()",[57,29551,29552],{},[136,29553,29549],{},[57,29555,29556],{},[136,29557,29549],{},[42,29559,29560,29563,29568,29573,29577],{},[57,29561,29562],{},"归约",[57,29564,29565],{},[136,29566,29567],{},".fold()",[57,29569,29570],{},[136,29571,29572],{},".reduce()",[57,29574,29575],{},[136,29576,29567],{},[57,29578,29579],{},[136,29580,29572],{},[42,29582,29583,29586,29591,29596,29600],{},[57,29584,29585],{},"扁平化",[57,29587,29588],{},[136,29589,29590],{},".expand()",[57,29592,29593],{},[136,29594,29595],{},".flatMap()",[57,29597,29598],{},[136,29599,29595],{},[57,29601,29602],{},[136,29603,29595],{},[42,29605,29606,29609,29615,29621,29626],{},[57,29607,29608],{},"排序",[57,29610,29611,29614],{},[136,29612,29613],{},".sort()"," (原地)",[57,29616,29617,29620],{},[136,29618,29619],{},".sorted()"," (新数组)",[57,29622,29623],{},[136,29624,29625],{},".sortedBy()",[57,29627,29628,29614],{},[136,29629,29613],{},[42,29631,29632,29635,29640,29645,29650],{},[57,29633,29634],{},"首个匹配",[57,29636,29637],{},[136,29638,29639],{},".firstWhere()",[57,29641,29642],{},[136,29643,29644],{},".first(where:)",[57,29646,29647],{},[136,29648,29649],{},".first {}",[57,29651,29652],{},[136,29653,29654],{},".find()",[42,29656,29657,29660,29663,29668,29673],{},[57,29658,29659],{},"分组",[57,29661,29662],{},"手动",[57,29664,29665],{},[136,29666,29667],{},"Dictionary(grouping:by:)",[57,29669,29670],{},[136,29671,29672],{},".groupBy {}",[57,29674,29675],{},"手动或 lodash",[11,29677,29678,29682],{},[14,29679,29055,29680,333],{},[125,29681,29058],{},[129,29683,29684,29698,29707,29717],{},[132,29685,29686,29687,29689,29690,29693,29694,29697],{},"Dart 的 ",[136,29688,29522],{}," 返回 ",[125,29691,29692],{},"惰性 Iterable","，不是 List！需要 ",[136,29695,29696],{},".toList()"," 才能多次遍历",[132,29699,29700,29701,29703,29704,29706],{},"Swift 的 ",[136,29702,29619],{}," 返回新数组，",[136,29705,29613],{}," 原地排序（注意 mutating）",[132,29708,29709,29710,29712,29713,29716],{},"Kotlin 的 ",[136,29711,29522],{}," 立即求值返回 List，",[136,29714,29715],{},".asSequence().map()"," 才是惰性的",[132,29718,29461,29719,29721],{},[136,29720,29613],{}," 是原地排序且返回自身（容易误以为返回新数组）",[1890,29723],{},[32,29725,29727],{"id":29726},"_15-模式匹配","1.5 模式匹配",[186,29729,29731],{"className":11162,"code":29730,"language":11164,"meta":191,"style":191},"\u002F\u002F Dart 3 — switch 表达式 + sealed class\nsealed class Shape {}\nclass Circle extends Shape { final double r; Circle(this.r); }\nclass Rect extends Shape { final double w, h; Rect(this.w, this.h); }\n\nString describe(Shape s) => switch (s) {\n  Circle(r: var r) => '圆形 r=$r',\n  Rect(w: var w, h: var h) => '矩形 ${w}x$h',\n};\n",[136,29732,29733,29738,29742,29747,29751,29755,29760,29765,29770],{"__ignoreMap":191},[195,29734,29735],{"class":197,"line":198},[195,29736,29737],{},"\u002F\u002F Dart 3 — switch 表达式 + sealed class\n",[195,29739,29740],{"class":197,"line":230},[195,29741,11483],{},[195,29743,29744],{"class":197,"line":251},[195,29745,29746],{},"class Circle extends Shape { final double r; Circle(this.r); }\n",[195,29748,29749],{"class":197,"line":272},[195,29750,11493],{},[195,29752,29753],{"class":197,"line":293},[195,29754,1241],{"emptyLinePlaceholder":757},[195,29756,29757],{"class":197,"line":562},[195,29758,29759],{},"String describe(Shape s) => switch (s) {\n",[195,29761,29762],{"class":197,"line":583},[195,29763,29764],{},"  Circle(r: var r) => '圆形 r=$r',\n",[195,29766,29767],{"class":197,"line":962},[195,29768,29769],{},"  Rect(w: var w, h: var h) => '矩形 ${w}x$h',\n",[195,29771,29772],{"class":197,"line":968},[195,29773,11446],{},[186,29775,29777],{"className":2177,"code":29776,"language":2179,"meta":191,"style":191},"\u002F\u002F Swift — enum + 关联值 + switch 穷举\nenum Shape {\n    case circle(r: Double)\n    case rect(w: Double, h: Double)\n}\nfunc describe(_ s: Shape) -> String {\n    switch s {\n    case .circle(let r): return \"圆形 r=\\(r)\"\n    case .rect(let w, let h): return \"矩形 \\(w)x\\(h)\"\n    }\n}\n",[136,29778,29779,29784,29789,29794,29799,29803,29808,29813,29818,29823,29827],{"__ignoreMap":191},[195,29780,29781],{"class":197,"line":198},[195,29782,29783],{},"\u002F\u002F Swift — enum + 关联值 + switch 穷举\n",[195,29785,29786],{"class":197,"line":230},[195,29787,29788],{},"enum Shape {\n",[195,29790,29791],{"class":197,"line":251},[195,29792,29793],{},"    case circle(r: Double)\n",[195,29795,29796],{"class":197,"line":272},[195,29797,29798],{},"    case rect(w: Double, h: Double)\n",[195,29800,29801],{"class":197,"line":293},[195,29802,552],{},[195,29804,29805],{"class":197,"line":562},[195,29806,29807],{},"func describe(_ s: Shape) -> String {\n",[195,29809,29810],{"class":197,"line":583},[195,29811,29812],{},"    switch s {\n",[195,29814,29815],{"class":197,"line":962},[195,29816,29817],{},"    case .circle(let r): return \"圆形 r=\\(r)\"\n",[195,29819,29820],{"class":197,"line":968},[195,29821,29822],{},"    case .rect(let w, let h): return \"矩形 \\(w)x\\(h)\"\n",[195,29824,29825],{"class":197,"line":1274},[195,29826,2403],{},[195,29828,29829],{"class":197,"line":1282},[195,29830,552],{},[186,29832,29834],{"className":6838,"code":29833,"language":6840,"meta":191,"style":191},"\u002F\u002F Kotlin — sealed class + when\nsealed class Shape\ndata class Circle(val r: Double) : Shape()\ndata class Rect(val w: Double, val h: Double) : Shape()\n\nfun describe(s: Shape) = when (s) {\n    is Circle -> \"圆形 r=${s.r}\"\n    is Rect -> \"矩形 ${s.w}x${s.h}\"\n}\n",[136,29835,29836,29841,29846,29851,29856,29860,29865,29870,29875],{"__ignoreMap":191},[195,29837,29838],{"class":197,"line":198},[195,29839,29840],{},"\u002F\u002F Kotlin — sealed class + when\n",[195,29842,29843],{"class":197,"line":230},[195,29844,29845],{},"sealed class Shape\n",[195,29847,29848],{"class":197,"line":251},[195,29849,29850],{},"data class Circle(val r: Double) : Shape()\n",[195,29852,29853],{"class":197,"line":272},[195,29854,29855],{},"data class Rect(val w: Double, val h: Double) : Shape()\n",[195,29857,29858],{"class":197,"line":293},[195,29859,1241],{"emptyLinePlaceholder":757},[195,29861,29862],{"class":197,"line":562},[195,29863,29864],{},"fun describe(s: Shape) = when (s) {\n",[195,29866,29867],{"class":197,"line":583},[195,29868,29869],{},"    is Circle -> \"圆形 r=${s.r}\"\n",[195,29871,29872],{"class":197,"line":962},[195,29873,29874],{},"    is Rect -> \"矩形 ${s.w}x${s.h}\"\n",[195,29876,29877],{"class":197,"line":968},[195,29878,552],{},[186,29880,29882],{"className":383,"code":29881,"language":385,"meta":191,"style":191},"\u002F\u002F TypeScript — 联合类型 + 判别属性\ntype Shape =\n  | { kind: 'circle'; r: number }\n  | { kind: 'rect'; w: number; h: number };\n\nfunction describe(s: Shape): string {\n  switch (s.kind) {\n    case 'circle': return `圆形 r=${s.r}`;\n    case 'rect': return `矩形 ${s.w}x${s.h}`;\n  }\n}\n",[136,29883,29884,29889,29900,29926,29959,29963,29986,29994,30019,30051,30055],{"__ignoreMap":191},[195,29885,29886],{"class":197,"line":198},[195,29887,29888],{"class":415},"\u002F\u002F TypeScript — 联合类型 + 判别属性\n",[195,29890,29891,29894,29897],{"class":197,"line":230},[195,29892,29893],{"class":223},"type",[195,29895,29896],{"class":201}," Shape",[195,29898,29899],{"class":223}," =\n",[195,29901,29902,29905,29907,29910,29912,29915,29917,29920,29922,29924],{"class":197,"line":251},[195,29903,29904],{"class":223},"  |",[195,29906,210],{"class":209},[195,29908,29909],{"class":634},"kind",[195,29911,638],{"class":223},[195,29913,29914],{"class":459}," 'circle'",[195,29916,29366],{"class":209},[195,29918,29919],{"class":634},"r",[195,29921,638],{"class":223},[195,29923,29363],{"class":213},[195,29925,18291],{"class":209},[195,29927,29928,29930,29932,29934,29936,29939,29941,29944,29946,29948,29950,29953,29955,29957],{"class":197,"line":272},[195,29929,29904],{"class":223},[195,29931,210],{"class":209},[195,29933,29909],{"class":634},[195,29935,638],{"class":223},[195,29937,29938],{"class":459}," 'rect'",[195,29940,29366],{"class":209},[195,29942,29943],{"class":634},"w",[195,29945,638],{"class":223},[195,29947,29363],{"class":213},[195,29949,29366],{"class":209},[195,29951,29952],{"class":634},"h",[195,29954,638],{"class":223},[195,29956,29363],{"class":213},[195,29958,29401],{"class":209},[195,29960,29961],{"class":197,"line":293},[195,29962,1241],{"emptyLinePlaceholder":757},[195,29964,29965,29967,29970,29972,29974,29976,29978,29980,29982,29984],{"class":197,"line":562},[195,29966,626],{"class":223},[195,29968,29969],{"class":201}," describe",[195,29971,404],{"class":209},[195,29973,541],{"class":634},[195,29975,638],{"class":223},[195,29977,29896],{"class":201},[195,29979,28757],{"class":209},[195,29981,638],{"class":223},[195,29983,15715],{"class":213},[195,29985,496],{"class":209},[195,29987,29988,29991],{"class":197,"line":583},[195,29989,29990],{"class":223},"  switch",[195,29992,29993],{"class":209}," (s.kind) {\n",[195,29995,29996,29999,30001,30003,30006,30009,30011,30013,30015,30017],{"class":197,"line":962},[195,29997,29998],{"class":223},"    case",[195,30000,29914],{"class":459},[195,30002,217],{"class":209},[195,30004,30005],{"class":223},"return",[195,30007,30008],{"class":459}," `圆形 r=${",[195,30010,541],{"class":209},[195,30012,16407],{"class":459},[195,30014,29919],{"class":209},[195,30016,16423],{"class":459},[195,30018,547],{"class":209},[195,30020,30021,30023,30025,30027,30029,30032,30034,30036,30038,30041,30043,30045,30047,30049],{"class":197,"line":968},[195,30022,29998],{"class":223},[195,30024,29938],{"class":459},[195,30026,217],{"class":209},[195,30028,30005],{"class":223},[195,30030,30031],{"class":459}," `矩形 ${",[195,30033,541],{"class":209},[195,30035,16407],{"class":459},[195,30037,29943],{"class":209},[195,30039,30040],{"class":459},"}x${",[195,30042,541],{"class":209},[195,30044,16407],{"class":459},[195,30046,29952],{"class":209},[195,30048,16423],{"class":459},[195,30050,547],{"class":209},[195,30052,30053],{"class":197,"line":1274},[195,30054,965],{"class":209},[195,30056,30057],{"class":197,"line":1282},[195,30058,552],{"class":209},[11,30060,30061,30065],{},[14,30062,29091,30063,333],{},[125,30064,29094],{},[129,30066,30067,30070,30073],{},[132,30068,30069],{},"Dart 3 的 sealed class 和 Kotlin 的 sealed class 有什么区别？（Dart 要求同文件，Kotlin 要求同 package）",[132,30071,30072],{},"Swift enum 的关联值 vs Kotlin sealed class，哪个更灵活？（Kotlin 的子类可以有自己的方法和属性）",[132,30074,30075,30076,4728,30079,30082],{},"TypeScript 的判别联合 (discriminated union) 如何保证穷举？（开启 ",[136,30077,30078],{},"strictNullChecks",[136,30080,30081],{},"never"," 类型兜底）",[1890,30084],{},[18,30086,30088],{"id":30087},"_2-ui-构建范式","2. UI 构建范式",[32,30090,30092],{"id":30091},"_21-声明式-ui-对比","2.1 声明式 UI 对比",[36,30094,30095,30113],{},[39,30096,30097],{},[42,30098,30099,30101,30104,30107,30110],{},[45,30100,15463],{},[45,30102,30103],{},"Flutter (Widget)",[45,30105,30106],{},"iOS (SwiftUI)",[45,30108,30109],{},"Android (Compose)",[45,30111,30112],{},"Vue (SFC)",[52,30114,30115,30133,30150,30169,30185],{},[42,30116,30117,30119,30122,30125,30128],{},[57,30118,15496],{},[57,30120,30121],{},"Widget 类",[57,30123,30124],{},"View struct",[57,30126,30127],{},"@Composable 函数",[57,30129,30130,30132],{},[136,30131,15504],{}," 单文件组件",[42,30134,30135,30138,30141,30144,30147],{},[57,30136,30137],{},"最小更新单位",[57,30139,30140],{},"Element 树 diff",[57,30142,30143],{},"View diff",[57,30145,30146],{},"Recomposition",[57,30148,30149],{},"Virtual DOM diff",[42,30151,30152,30155,30158,30161,30164],{},[57,30153,30154],{},"UI 描述方式",[57,30156,30157],{},"Dart 代码嵌套",[57,30159,30160],{},"Swift DSL（ViewBuilder）",[57,30162,30163],{},"Kotlin DSL",[57,30165,30166,30168],{},[136,30167,15742],{}," HTML 模板",[42,30170,30171,30174,30177,30180,30182],{},[57,30172,30173],{},"样式机制",[57,30175,30176],{},"Widget 属性",[57,30178,30179],{},"Modifier 链式调用",[57,30181,30179],{},[57,30183,30184],{},"CSS \u002F Scoped CSS",[42,30186,30187,30190,30193,30196,30199],{},[57,30188,30189],{},"有\u002F无状态区分",[57,30191,30192],{},"StatelessWidget \u002F StatefulWidget",[57,30194,30195],{},"View (统一)",[57,30197,30198],{},"@Composable (统一)",[57,30200,30201],{},"统一（Composition API）",[32,30203,30205],{"id":30204},"_22-基本组件写法","2.2 基本组件写法",[186,30207,30209],{"className":11162,"code":30208,"language":11164,"meta":191,"style":191},"\u002F\u002F Flutter — StatelessWidget\nclass Greeting extends StatelessWidget {\n  final String name;\n  const Greeting({required this.name});\n  \n  @override\n  Widget build(BuildContext context) {\n    return Text('Hello, $name', style: TextStyle(fontSize: 16));\n  }\n}\n",[136,30210,30211,30216,30220,30224,30228,30232,30236,30240,30245,30249],{"__ignoreMap":191},[195,30212,30213],{"class":197,"line":198},[195,30214,30215],{},"\u002F\u002F Flutter — StatelessWidget\n",[195,30217,30218],{"class":197,"line":230},[195,30219,15596],{},[195,30221,30222],{"class":197,"line":251},[195,30223,15601],{},[195,30225,30226],{"class":197,"line":272},[195,30227,15606],{},[195,30229,30230],{"class":197,"line":293},[195,30231,19967],{},[195,30233,30234],{"class":197,"line":562},[195,30235,12090],{},[195,30237,30238],{"class":197,"line":583},[195,30239,15619],{},[195,30241,30242],{"class":197,"line":962},[195,30243,30244],{},"    return Text('Hello, $name', style: TextStyle(fontSize: 16));\n",[195,30246,30247],{"class":197,"line":968},[195,30248,965],{},[195,30250,30251],{"class":197,"line":1274},[195,30252,552],{},[186,30254,30256],{"className":2177,"code":30255,"language":2179,"meta":191,"style":191},"\u002F\u002F SwiftUI — View\nstruct Greeting: View {\n    let name: String\n    \n    var body: some View {\n        Text(\"Hello, \\(name)\")\n            .font(.body)\n    }\n}\n",[136,30257,30258,30263,30268,30272,30276,30280,30285,30290,30294],{"__ignoreMap":191},[195,30259,30260],{"class":197,"line":198},[195,30261,30262],{},"\u002F\u002F SwiftUI — View\n",[195,30264,30265],{"class":197,"line":230},[195,30266,30267],{},"struct Greeting: View {\n",[195,30269,30270],{"class":197,"line":251},[195,30271,5146],{},[195,30273,30274],{"class":197,"line":272},[195,30275,20020],{},[195,30277,30278],{"class":197,"line":293},[195,30279,3985],{},[195,30281,30282],{"class":197,"line":562},[195,30283,30284],{},"        Text(\"Hello, \\(name)\")\n",[195,30286,30287],{"class":197,"line":583},[195,30288,30289],{},"            .font(.body)\n",[195,30291,30292],{"class":197,"line":962},[195,30293,2403],{},[195,30295,30296],{"class":197,"line":968},[195,30297,552],{},[186,30299,30301],{"className":6838,"code":30300,"language":6840,"meta":191,"style":191},"\u002F\u002F Compose — @Composable\n@Composable\nfun Greeting(name: String) {\n    Text(\n        text = \"Hello, $name\",\n        fontSize = 16.sp\n    )\n}\n",[136,30302,30303,30308,30312,30317,30322,30327,30332,30336],{"__ignoreMap":191},[195,30304,30305],{"class":197,"line":198},[195,30306,30307],{},"\u002F\u002F Compose — @Composable\n",[195,30309,30310],{"class":197,"line":230},[195,30311,8477],{},[195,30313,30314],{"class":197,"line":251},[195,30315,30316],{},"fun Greeting(name: String) {\n",[195,30318,30319],{"class":197,"line":272},[195,30320,30321],{},"    Text(\n",[195,30323,30324],{"class":197,"line":293},[195,30325,30326],{},"        text = \"Hello, $name\",\n",[195,30328,30329],{"class":197,"line":562},[195,30330,30331],{},"        fontSize = 16.sp\n",[195,30333,30334],{"class":197,"line":583},[195,30335,4902],{},[195,30337,30338],{"class":197,"line":962},[195,30339,552],{},[186,30341,30343],{"className":15640,"code":30342,"language":15642,"meta":191,"style":191},"\u003C!-- Vue 3 — SFC -->\n\u003Cscript setup lang=\"ts\">\ndefineProps\u003C{ name: string }>()\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cspan style=\"font-size: 16px\">Hello, {{ name }}\u003C\u002Fspan>\n\u003C\u002Ftemplate>\n",[136,30344,30345,30350,30366,30380,30388,30392,30400,30420],{"__ignoreMap":191},[195,30346,30347],{"class":197,"line":198},[195,30348,30349],{"class":415},"\u003C!-- Vue 3 — SFC -->\n",[195,30351,30352,30354,30356,30358,30360,30362,30364],{"class":197,"line":230},[195,30353,448],{"class":209},[195,30355,15687],{"class":205},[195,30357,15690],{"class":201},[195,30359,15693],{"class":201},[195,30361,424],{"class":209},[195,30363,15698],{"class":459},[195,30365,477],{"class":209},[195,30367,30368,30370,30372,30374,30376,30378],{"class":197,"line":251},[195,30369,15705],{"class":201},[195,30371,15708],{"class":209},[195,30373,13388],{"class":634},[195,30375,638],{"class":223},[195,30377,15715],{"class":213},[195,30379,15718],{"class":209},[195,30381,30382,30384,30386],{"class":197,"line":272},[195,30383,15672],{"class":209},[195,30385,15687],{"class":205},[195,30387,477],{"class":209},[195,30389,30390],{"class":197,"line":293},[195,30391,1241],{"emptyLinePlaceholder":757},[195,30393,30394,30396,30398],{"class":197,"line":562},[195,30395,448],{"class":209},[195,30397,15651],{"class":205},[195,30399,477],{"class":209},[195,30401,30402,30404,30406,30409,30411,30414,30416,30418],{"class":197,"line":583},[195,30403,15658],{"class":209},[195,30405,195],{"class":205},[195,30407,30408],{"class":201}," style",[195,30410,424],{"class":209},[195,30412,30413],{"class":459},"\"font-size: 16px\"",[195,30415,15663],{"class":209},[195,30417,195],{"class":205},[195,30419,477],{"class":209},[195,30421,30422,30424,30426],{"class":197,"line":962},[195,30423,15672],{"class":209},[195,30425,15651],{"class":205},[195,30427,477],{"class":209},[11,30429,30430,30434],{},[14,30431,29042,30432,333],{},[125,30433,29045],{},[129,30435,30436,30439,30442],{},[132,30437,30438],{},"Flutter 的 Widget 是不可变的配置描述，真正的可变状态在 State 对象中；SwiftUI 的 View 也是 struct（值类型），每次 body 重新计算",[132,30440,30441],{},"Compose 的 @Composable 不是类，是函数 + 编译器插件注入的位置 key",[132,30443,30444,30445,30447],{},"Vue 的 ",[136,30446,15742],{}," 编译为 render 函数，最终通过 Virtual DOM diff 更新真实 DOM",[11,30449,30450,30454],{},[14,30451,29091,30452,333],{},[125,30453,29094],{},[129,30455,30456,30459,30466],{},[132,30457,30458],{},"Flutter Widget 为什么设计成 immutable？（频繁创建+销毁，GC 高效处理短命对象；真正昂贵的是 RenderObject，它被复用）",[132,30460,30461,30462,30465],{},"SwiftUI 的 ",[136,30463,30464],{},"some View"," 是什么？（不透明返回类型，编译器知道具体类型但调用者不知道）",[132,30467,30468,30469,30472],{},"Vue ",[136,30470,30471],{},"\u003Cscript setup>"," 和 Options API 的区别？（setup 是 Composition API 的语法糖，更好的类型推导和 tree-shaking）",[1890,30474],{},[32,30476,30478],{"id":30477},"_23-组件生命周期对比","2.3 组件生命周期对比",[36,30480,30481,30496],{},[39,30482,30483],{},[42,30484,30485,30487,30489,30491,30493],{},[45,30486,6273],{},[45,30488,13135],{},[45,30490,4051],{},[45,30492,15186],{},[45,30494,30495],{},"Vue 3",[52,30497,30498,30521,30544,30561,30584],{},[42,30499,30500,30503,30508,30513,30516],{},[57,30501,30502],{},"创建",[57,30504,30505],{},[136,30506,30507],{},"createState()",[57,30509,30510],{},[136,30511,30512],{},"init()",[57,30514,30515],{},"首次组合",[57,30517,30518],{},[136,30519,30520],{},"setup()",[42,30522,30523,30526,30530,30535,30540],{},[57,30524,30525],{},"挂载",[57,30527,30528],{},[136,30529,16604],{},[57,30531,30532],{},[136,30533,30534],{},"onAppear",[57,30536,30537],{},[136,30538,30539],{},"LaunchedEffect",[57,30541,30542],{},[136,30543,16609],{},[42,30545,30546,30549,30553,30556,30558],{},[57,30547,30548],{},"更新",[57,30550,30551],{},[136,30552,16619],{},[57,30554,30555],{},"body 重新计算",[57,30557,30146],{},[57,30559,30560],{},"响应式自动触发",[42,30562,30563,30566,30570,30575,30580],{},[57,30564,30565],{},"卸载",[57,30567,30568],{},[136,30569,16634],{},[57,30571,30572],{},[136,30573,30574],{},"onDisappear",[57,30576,30577],{},[136,30578,30579],{},"DisposableEffect",[57,30581,30582],{},[136,30583,16639],{},[42,30585,30586,30588,30592,30597,30602],{},[57,30587,16656],{},[57,30589,30590],{},[136,30591,16649],{},[57,30593,30594],{},[136,30595,30596],{},"onChange(of:)",[57,30598,30599],{},[136,30600,30601],{},"LaunchedEffect(key)",[57,30603,30604],{},[136,30605,16121],{},[11,30607,30608,30612],{},[14,30609,29055,30610,333],{},[125,30611,29058],{},[129,30613,30614,30625,30634,30641],{},[132,30615,30616,30617,30620,30621,30624],{},"Flutter: ",[136,30618,30619],{},"initState"," 中不能直接用 ",[136,30622,30623],{},"context"," 获取 InheritedWidget（此时 didChangeDependencies 还没调用）",[132,30626,30627,30628,30630,30631],{},"SwiftUI: ",[136,30629,30534],{}," 可能被多次调用（NavigationStack 返回时），不等于 ",[136,30632,30633],{},"viewDidLoad",[132,30635,30636,30637,30640],{},"Compose: ",[136,30638,30639],{},"LaunchedEffect(Unit)"," 只在首次组合执行，key 变化会取消重启",[132,30642,30643,30644,30646],{},"Vue: ",[136,30645,30520],{}," 在组件实例创建后、挂载前执行，此时 DOM 不存在",[1890,30648],{},[32,30650,30652],{"id":30651},"_24-条件渲染与列表渲染","2.4 条件渲染与列表渲染",[186,30654,30656],{"className":11162,"code":30655,"language":11164,"meta":191,"style":191},"\u002F\u002F Flutter\nColumn(children: [\n  if (isLoggedIn) Text('Welcome'),      \u002F\u002F 条件\n  ...items.map((e) => ListTile(title: Text(e))), \u002F\u002F 列表\n])\n",[136,30657,30658,30663,30667,30672,30677],{"__ignoreMap":191},[195,30659,30660],{"class":197,"line":198},[195,30661,30662],{},"\u002F\u002F Flutter\n",[195,30664,30665],{"class":197,"line":230},[195,30666,16836],{},[195,30668,30669],{"class":197,"line":251},[195,30670,30671],{},"  if (isLoggedIn) Text('Welcome'),      \u002F\u002F 条件\n",[195,30673,30674],{"class":197,"line":272},[195,30675,30676],{},"  ...items.map((e) => ListTile(title: Text(e))), \u002F\u002F 列表\n",[195,30678,30679],{"class":197,"line":293},[195,30680,16851],{},[186,30682,30684],{"className":2177,"code":30683,"language":2179,"meta":191,"style":191},"\u002F\u002F SwiftUI\nVStack {\n    if isLoggedIn { Text(\"Welcome\") }    \u002F\u002F 条件\n    ForEach(items, id: \\.self) { item in \u002F\u002F 列表\n        Text(item)\n    }\n}\n",[136,30685,30686,30690,30695,30700,30705,30710,30714],{"__ignoreMap":191},[195,30687,30688],{"class":197,"line":198},[195,30689,6641],{},[195,30691,30692],{"class":197,"line":230},[195,30693,30694],{},"VStack {\n",[195,30696,30697],{"class":197,"line":251},[195,30698,30699],{},"    if isLoggedIn { Text(\"Welcome\") }    \u002F\u002F 条件\n",[195,30701,30702],{"class":197,"line":272},[195,30703,30704],{},"    ForEach(items, id: \\.self) { item in \u002F\u002F 列表\n",[195,30706,30707],{"class":197,"line":293},[195,30708,30709],{},"        Text(item)\n",[195,30711,30712],{"class":197,"line":562},[195,30713,2403],{},[195,30715,30716],{"class":197,"line":583},[195,30717,552],{},[186,30719,30721],{"className":6838,"code":30720,"language":6840,"meta":191,"style":191},"\u002F\u002F Compose\nColumn {\n    if (isLoggedIn) { Text(\"Welcome\") }  \u002F\u002F 条件\n    items.forEach { item ->              \u002F\u002F 列表\n        Text(item)\n    }\n}\n",[136,30722,30723,30727,30732,30737,30742,30746,30750],{"__ignoreMap":191},[195,30724,30725],{"class":197,"line":198},[195,30726,9886],{},[195,30728,30729],{"class":197,"line":230},[195,30730,30731],{},"Column {\n",[195,30733,30734],{"class":197,"line":251},[195,30735,30736],{},"    if (isLoggedIn) { Text(\"Welcome\") }  \u002F\u002F 条件\n",[195,30738,30739],{"class":197,"line":272},[195,30740,30741],{},"    items.forEach { item ->              \u002F\u002F 列表\n",[195,30743,30744],{"class":197,"line":293},[195,30745,30709],{},[195,30747,30748],{"class":197,"line":562},[195,30749,2403],{},[195,30751,30752],{"class":197,"line":583},[195,30753,552],{},[186,30755,30757],{"className":15640,"code":30756,"language":15642,"meta":191,"style":191},"\u003C!-- Vue -->\n\u003Cdiv>\n  \u003Cspan v-if=\"isLoggedIn\">Welcome\u003C\u002Fspan>    \u003C!-- 条件 -->\n  \u003Cdiv v-for=\"item in items\" :key=\"item\">   \u003C!-- 列表 -->\n    {{ item }}\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\n",[136,30758,30759,30764,30772,30777,30782,30787,30795],{"__ignoreMap":191},[195,30760,30761],{"class":197,"line":198},[195,30762,30763],{"class":415},"\u003C!-- Vue -->\n",[195,30765,30766,30768,30770],{"class":197,"line":230},[195,30767,448],{"class":209},[195,30769,451],{"class":205},[195,30771,477],{"class":209},[195,30773,30774],{"class":197,"line":251},[195,30775,30776],{"class":209},"  \u003Cspan v-if=\"isLoggedIn\">Welcome\u003C\u002Fspan>    \u003C!-- 条件 -->\n",[195,30778,30779],{"class":197,"line":272},[195,30780,30781],{"class":209},"  \u003Cdiv v-for=\"item in items\" :key=\"item\">   \u003C!-- 列表 -->\n",[195,30783,30784],{"class":197,"line":293},[195,30785,30786],{"class":209},"    {{ item }}\n",[195,30788,30789,30791,30793],{"class":197,"line":562},[195,30790,15924],{"class":209},[195,30792,451],{"class":205},[195,30794,477],{"class":209},[195,30796,30797,30799,30801],{"class":197,"line":583},[195,30798,15672],{"class":209},[195,30800,451],{"class":205},[195,30802,477],{"class":209},[11,30804,30805,30809],{},[14,30806,29055,30807,333],{},[125,30808,29058],{},[129,30810,30811,30820,30829,30839],{},[132,30812,30444,30813,30816,30817,30819],{},[136,30814,30815],{},"v-for"," 必须绑定 ",[136,30818,19628],{},"，否则 diff 算法会就地复用导致状态错乱",[132,30821,15736,30822,30825,30826,30828],{},[136,30823,30824],{},"ListView.builder"," 要用 ",[136,30827,19622],{}," 来保持列表项状态",[132,30830,30461,30831,30834,30835,30838],{},[136,30832,30833],{},"ForEach"," 中的 ",[136,30836,30837],{},"id"," 参数必须唯一，否则 diff 出错",[132,30840,30841,30842,30845,30846,905,30848,30850],{},"Compose 的 ",[136,30843,30844],{},"LazyColumn"," 中 ",[136,30847,17645],{},[136,30849,11601],{}," 同理必须唯一且稳定",[1890,30852],{},[18,30854,30856],{"id":30855},"_3-状态管理","3. 状态管理",[32,30858,30860],{"id":30859},"_31-局部状态","3.1 局部状态",[36,30862,30863,30877],{},[39,30864,30865],{},[42,30866,30867,30869,30871,30873,30875],{},[45,30868,17273],{},[45,30870,13135],{},[45,30872,4051],{},[45,30874,15186],{},[45,30876,30495],{},[52,30878,30879,30906,30927],{},[42,30880,30881,30884,30890,30894,30899],{},[57,30882,30883],{},"组件内状态",[57,30885,30886,4728,30888],{},[136,30887,22930],{},[136,30889,16010],{},[57,30891,30892],{},[136,30893,3120],{},[57,30895,30896],{},[136,30897,30898],{},"remember { mutableStateOf() }",[57,30900,30901,16122,30903],{},[136,30902,16026],{},[136,30904,30905],{},"reactive()",[42,30907,30908,30911,30914,30917,30922],{},[57,30909,30910],{},"派生状态",[57,30912,30913],{},"手动计算（或 ValueNotifier）",[57,30915,30916],{},"computed property",[57,30918,30919],{},[136,30920,30921],{},"derivedStateOf",[57,30923,30924],{},[136,30925,30926],{},"computed()",[42,30928,30929,30932,30939,30950,30957],{},[57,30930,30931],{},"副作用",[57,30933,30934,16122,30936],{},[136,30935,30619],{},[136,30937,30938],{},"didUpdateWidget",[57,30940,30941,16122,30944,16122,30947],{},[136,30942,30943],{},".onAppear",[136,30945,30946],{},".onChange",[136,30948,30949],{},".task",[57,30951,30952,16122,30954],{},[136,30953,30539],{},[136,30955,30956],{},"SideEffect",[57,30958,30959,16122,30961,16122,30963],{},[136,30960,16140],{},[136,30962,16536],{},[136,30964,30965],{},"watchEffect",[186,30967,30969],{"className":11162,"code":30968,"language":11164,"meta":191,"style":191},"\u002F\u002F Flutter\nclass Counter extends StatefulWidget {\n  @override State\u003CCounter> createState() => _CounterState();\n}\nclass _CounterState extends State\u003CCounter> {\n  int count = 0;\n  @override\n  Widget build(BuildContext context) {\n    return TextButton(\n      onPressed: () => setState(() => count++),\n      child: Text('$count'),\n    );\n  }\n}\n",[136,30970,30971,30975,30979,30984,30988,30992,30996,31000,31004,31009,31014,31019,31023,31027],{"__ignoreMap":191},[195,30972,30973],{"class":197,"line":198},[195,30974,30662],{},[195,30976,30977],{"class":197,"line":230},[195,30978,15778],{},[195,30980,30981],{"class":197,"line":251},[195,30982,30983],{},"  @override State\u003CCounter> createState() => _CounterState();\n",[195,30985,30986],{"class":197,"line":272},[195,30987,552],{},[195,30989,30990],{"class":197,"line":293},[195,30991,15800],{},[195,30993,30994],{"class":197,"line":562},[195,30995,12615],{},[195,30997,30998],{"class":197,"line":583},[195,30999,12090],{},[195,31001,31002],{"class":197,"line":962},[195,31003,15619],{},[195,31005,31006],{"class":197,"line":968},[195,31007,31008],{},"    return TextButton(\n",[195,31010,31011],{"class":197,"line":1274},[195,31012,31013],{},"      onPressed: () => setState(() => count++),\n",[195,31015,31016],{"class":197,"line":1282},[195,31017,31018],{},"      child: Text('$count'),\n",[195,31020,31021],{"class":197,"line":1295},[195,31022,24234],{},[195,31024,31025],{"class":197,"line":1309},[195,31026,965],{},[195,31028,31029],{"class":197,"line":2246},[195,31030,552],{},[186,31032,31034],{"className":2177,"code":31033,"language":2179,"meta":191,"style":191},"\u002F\u002F SwiftUI\nstruct Counter: View {\n    @State private var count = 0\n    var body: some View {\n        Button(\"\\(count)\") { count += 1 }\n    }\n}\n",[136,31035,31036,31040,31045,31049,31053,31058,31062],{"__ignoreMap":191},[195,31037,31038],{"class":197,"line":198},[195,31039,6641],{},[195,31041,31042],{"class":197,"line":230},[195,31043,31044],{},"struct Counter: View {\n",[195,31046,31047],{"class":197,"line":251},[195,31048,3976],{},[195,31050,31051],{"class":197,"line":272},[195,31052,3985],{},[195,31054,31055],{"class":197,"line":293},[195,31056,31057],{},"        Button(\"\\(count)\") { count += 1 }\n",[195,31059,31060],{"class":197,"line":562},[195,31061,2403],{},[195,31063,31064],{"class":197,"line":583},[195,31065,552],{},[186,31067,31069],{"className":6838,"code":31068,"language":6840,"meta":191,"style":191},"\u002F\u002F Compose\n@Composable\nfun Counter() {\n    var count by remember { mutableStateOf(0) }\n    Button(onClick = { count++ }) { Text(\"$count\") }\n}\n",[136,31070,31071,31075,31079,31083,31087,31092],{"__ignoreMap":191},[195,31072,31073],{"class":197,"line":198},[195,31074,9886],{},[195,31076,31077],{"class":197,"line":230},[195,31078,8477],{},[195,31080,31081],{"class":197,"line":251},[195,31082,8482],{},[195,31084,31085],{"class":197,"line":272},[195,31086,8487],{},[195,31088,31089],{"class":197,"line":293},[195,31090,31091],{},"    Button(onClick = { count++ }) { Text(\"$count\") }\n",[195,31093,31094],{"class":197,"line":562},[195,31095,552],{},[186,31097,31099],{"className":15640,"code":31098,"language":15642,"meta":191,"style":191},"\u003C!-- Vue 3 -->\n\u003Cscript setup lang=\"ts\">\nimport { ref } from 'vue'\nconst count = ref(0)\n\u003C\u002Fscript>\n\u003Ctemplate>\n  \u003Cbutton @click=\"count++\">{{ count }}\u003C\u002Fbutton>\n\u003C\u002Ftemplate>\n",[136,31100,31101,31106,31122,31132,31148,31156,31164,31182],{"__ignoreMap":191},[195,31102,31103],{"class":197,"line":198},[195,31104,31105],{"class":415},"\u003C!-- Vue 3 -->\n",[195,31107,31108,31110,31112,31114,31116,31118,31120],{"class":197,"line":230},[195,31109,448],{"class":209},[195,31111,15687],{"class":205},[195,31113,15690],{"class":201},[195,31115,15693],{"class":201},[195,31117,424],{"class":209},[195,31119,15698],{"class":459},[195,31121,477],{"class":209},[195,31123,31124,31126,31128,31130],{"class":197,"line":251},[195,31125,15961],{"class":223},[195,31127,15964],{"class":209},[195,31129,15967],{"class":223},[195,31131,15970],{"class":459},[195,31133,31134,31136,31138,31140,31142,31144,31146],{"class":197,"line":272},[195,31135,392],{"class":223},[195,31137,15981],{"class":213},[195,31139,398],{"class":223},[195,31141,401],{"class":201},[195,31143,404],{"class":209},[195,31145,407],{"class":213},[195,31147,410],{"class":209},[195,31149,31150,31152,31154],{"class":197,"line":293},[195,31151,15672],{"class":209},[195,31153,15687],{"class":205},[195,31155,477],{"class":209},[195,31157,31158,31160,31162],{"class":197,"line":562},[195,31159,448],{"class":209},[195,31161,15651],{"class":205},[195,31163,477],{"class":209},[195,31165,31166,31168,31170,31172,31174,31176,31178,31180],{"class":197,"line":583},[195,31167,15658],{"class":209},[195,31169,15904],{"class":205},[195,31171,15907],{"class":201},[195,31173,424],{"class":209},[195,31175,15912],{"class":459},[195,31177,15893],{"class":209},[195,31179,15904],{"class":205},[195,31181,477],{"class":209},[195,31183,31184,31186,31188],{"class":197,"line":962},[195,31185,15672],{"class":209},[195,31187,15651],{"class":205},[195,31189,477],{"class":209},[11,31191,31192,31196],{},[14,31193,29042,31194,333],{},[125,31195,29045],{},[129,31197,31198,31203,31208,31214],{},[132,31199,15736,31200,31202],{},[136,31201,16010],{}," 标记当前 Element dirty，触发 build 重建整棵 Widget 子树（但 Element\u002FRenderObject 会复用）",[132,31204,30461,31205,31207],{},[136,31206,3120],{}," 内部是 property wrapper，值存储在框架管理的存储区，View struct 重建不影响状态",[132,31209,30841,31210,31213],{},[136,31211,31212],{},"remember"," 将值绑定到组合树的位置 (positional memoization)",[132,31215,30444,31216,31218],{},[136,31217,16026],{}," 基于 Proxy 代理实现响应式追踪",[11,31220,31221,31225],{},[14,31222,29091,31223,333],{},[125,31224,29094],{},[129,31226,31227,31232,31245,31252],{},[132,31228,31229,31231],{},[136,31230,16010],{}," 是同步还是异步的？（同步的，但 build 是在下一帧微任务中执行）",[132,31233,31234,31235,31237,31238,31237,31240,31237,31242,31244],{},"SwiftUI ",[136,31236,3120],{}," vs ",[136,31239,3130],{},[136,31241,3140],{},[136,31243,3150],{}," 的区别？（所有权和生命周期不同）",[132,31246,30468,31247,31237,31249,31251],{},[136,31248,16026],{},[136,31250,30905],{}," 怎么选？（ref 适合基本类型和需要替换的对象，reactive 适合不会整体替换的对象）",[132,31253,31254,31255,31237,31257,31260],{},"Compose ",[136,31256,31212],{},[136,31258,31259],{},"rememberSaveable"," 的区别？（后者在 configuration change 时保存状态）",[1890,31262],{},[32,31264,31266],{"id":31265},"_32-全局跨组件状态","3.2 全局\u002F跨组件状态",[36,31268,31269,31283],{},[39,31270,31271],{},[42,31272,31273,31275,31277,31279,31281],{},[45,31274,13777],{},[45,31276,13135],{},[45,31278,4051],{},[45,31280,15186],{},[45,31282,30495],{},[52,31284,31285,31305,31322,31339],{},[42,31286,31287,31290,31293,31297,31303],{},[57,31288,31289],{},"官方推荐",[57,31291,31292],{},"Provider \u002F Riverpod",[57,31294,31295],{},[136,31296,3160],{},[57,31298,31299,31300],{},"ViewModel + ",[136,31301,31302],{},"hiltViewModel()",[57,31304,17340],{},[42,31306,31307,31310,31313,31316,31319],{},[57,31308,31309],{},"依赖注入",[57,31311,31312],{},"Provider \u002F GetIt \u002F GetX",[57,31314,31315],{},"Environment \u002F @EnvironmentObject",[57,31317,31318],{},"Hilt (Dagger)",[57,31320,31321],{},"provide \u002F inject",[42,31323,31324,31327,31330,31333,31336],{},[57,31325,31326],{},"事件总线",[57,31328,31329],{},"EventBus \u002F Stream",[57,31331,31332],{},"Combine Publisher",[57,31334,31335],{},"SharedFlow \u002F Channel",[57,31337,31338],{},"mitt \u002F EventBus",[42,31340,31341,31344,31347,31350,31353],{},[57,31342,31343],{},"不可变状态流",[57,31345,31346],{},"BLoC (Stream)",[57,31348,31349],{},"Combine",[57,31351,31352],{},"StateFlow + MVI",[57,31354,31355],{},"Pinia + actions",[11,31357,31358,31362],{},[14,31359,29042,31360,333],{},[125,31361,29045],{},[129,31363,31364,31374,31381,31384],{},[132,31365,31366,31367,1437,31370,31373],{},"Flutter Provider 基于 InheritedWidget，",[136,31368,31369],{},"context.watch\u003CT>()",[136,31371,31372],{},"context.read\u003CT>()"," 的区别是前者订阅变化后者不订阅",[132,31375,31234,31376,31237,31378,31380],{},[136,31377,3150],{},[136,31379,3140],{},"：前者拥有对象生命周期，后者不拥有（View 重建可能导致对象重建）",[132,31382,31383],{},"Compose 的 ViewModel 在 Activity\u002FFragment 的 ViewModelStore 中，存活于 configuration change",[132,31385,31386],{},"Vue 的 Pinia store 是单例，但 SSR 场景下需要注意 store 隔离",[11,31388,31389,31393],{},[14,31390,29055,31391,333],{},[125,31392,29058],{},[129,31394,31395,31403,31412,31419],{},[132,31396,30616,31397,31399,31400],{},[136,31398,17321],{}," 放错位置导致 ",[136,31401,31402],{},"ProviderNotFoundException",[132,31404,31405,31406,31408,31409,31411],{},"SwiftUI: 用 ",[136,31407,3140],{}," 代替 ",[136,31410,3150],{}," 导致状态丢失（View 重建时对象被重新创建）",[132,31413,31414,31415,31418],{},"Compose: 在 ",[136,31416,31417],{},"@Composable"," 之外收集 StateFlow 导致不响应更新",[132,31420,31421,31422,31424,31425,698],{},"Vue: 在 ",[136,31423,30471],{}," 外部解构 Pinia store 导致响应性丢失（需要 ",[136,31426,31427],{},"storeToRefs()",[1890,31429],{},[32,31431,31433],{"id":31432},"_33-响应式原理对比","3.3 响应式原理对比",[36,31435,31436,31450],{},[39,31437,31438],{},[42,31439,31440,31442,31444,31446,31448],{},[45,31441,15463],{},[45,31443,13135],{},[45,31445,4051],{},[45,31447,15186],{},[45,31449,15468],{},[52,31451,31452,31479,31496,31513],{},[42,31453,31454,31457,31465,31470,31476],{},[57,31455,31456],{},"触发机制",[57,31458,19644,31459,31461,31462],{},[136,31460,16010],{}," 或 ",[136,31463,31464],{},"notifyListeners()",[57,31466,31467,31469],{},[136,31468,3180],{}," 属性变化",[57,31471,31472,31475],{},[136,31473,31474],{},"MutableState"," 值变化",[57,31477,31478],{},"Proxy getter\u002Fsetter 拦截",[42,31480,31481,31484,31487,31490,31493],{},[57,31482,31483],{},"追踪粒度",[57,31485,31486],{},"Widget 级别",[57,31488,31489],{},"View body 级别",[57,31491,31492],{},"读取点级别（最细）",[57,31494,31495],{},"属性级别",[42,31497,31498,31501,31504,31507,31510],{},[57,31499,31500],{},"更新范围",[57,31502,31503],{},"标记 dirty 的 Element 子树",[57,31505,31506],{},"body 重新求值",[57,31508,31509],{},"只重组读取了变化状态的 Composable",[57,31511,31512],{},"关联的组件重新渲染",[42,31514,31515,31518,31521,31524,31527],{},[57,31516,31517],{},"批量更新",[57,31519,31520],{},"同一帧合并",[57,31522,31523],{},"同一 RunLoop 合并",[57,31525,31526],{},"快照系统 (Snapshot)",[57,31528,31529],{},"nextTick 微任务合并",[11,31531,31532,31536],{},[14,31533,29091,31534,333],{},[125,31535,29094],{},[129,31537,31538,31541,31544],{},[132,31539,31540],{},"Vue 的响应式原理是什么？（Vue 3 用 Proxy 拦截 get\u002Fset，收集依赖 track，派发更新 trigger）",[132,31542,31543],{},"Compose 的 Recomposition 如何做到精确更新？（编译器插入的代码在状态读取时建立依赖关系，只重组受影响的 scope）",[132,31545,15736,31546,31548],{},[136,31547,392],{}," Widget 为什么能优化性能？（canUpdate 时 widget == 旧widget，跳过 build）",[1890,31550],{},[18,31552,31554],{"id":31553},"_4-布局系统","4. 布局系统",[32,31556,31558],{"id":31557},"_41-布局方式对比","4.1 布局方式对比",[36,31560,31561,31576],{},[39,31562,31563],{},[42,31564,31565,31567,31569,31571,31573],{},[45,31566,4080],{},[45,31568,13135],{},[45,31570,4051],{},[45,31572,15186],{},[45,31574,31575],{},"Vue (CSS)",[52,31577,31578,31600,31622,31645,31671,31697,31734],{},[42,31579,31580,31583,31587,31592,31596],{},[57,31581,31582],{},"水平排列",[57,31584,31585],{},[136,31586,18695],{},[57,31588,31589],{},[136,31590,31591],{},"HStack",[57,31593,31594],{},[136,31595,18695],{},[57,31597,31598],{},[136,31599,1420],{},[42,31601,31602,31605,31609,31614,31618],{},[57,31603,31604],{},"垂直排列",[57,31606,31607],{},[136,31608,18680],{},[57,31610,31611],{},[136,31612,31613],{},"VStack",[57,31615,31616],{},[136,31617,18680],{},[57,31619,31620],{},[136,31621,1440],{},[42,31623,31624,31626,31630,31635,31640],{},[57,31625,18721],{},[57,31627,31628],{},[136,31629,18710],{},[57,31631,31632],{},[136,31633,31634],{},"ZStack",[57,31636,31637],{},[136,31638,31639],{},"Box",[57,31641,31642],{},[136,31643,31644],{},"position: relative\u002Fabsolute",[42,31646,31647,31649,31653,31661,31666],{},[57,31648,18809],{},[57,31650,31651],{},[136,31652,18801],{},[57,31654,31655,16122,31658],{},[136,31656,31657],{},"LazyVGrid",[136,31659,31660],{},"LazyHGrid",[57,31662,31663],{},[136,31664,31665],{},"LazyVerticalGrid",[57,31667,31668],{},[136,31669,31670],{},"display: grid",[42,31672,31673,31675,31683,31688,31693],{},[57,31674,18735],{},[57,31676,31677,16122,31680],{},[136,31678,31679],{},"Expanded",[136,31681,31682],{},"Flexible",[57,31684,31685],{},[136,31686,31687],{},".frame(maxWidth: .infinity)",[57,31689,31690],{},[136,31691,31692],{},"Modifier.weight()",[57,31694,31695],{},[136,31696,1436],{},[42,31698,31699,31702,31709,31717,31724],{},[57,31700,31701],{},"间距",[57,31703,31704,16122,31707],{},[136,31705,31706],{},"SizedBox",[136,31708,18757],{},[57,31710,31711,16122,31714],{},[136,31712,31713],{},".padding()",[136,31715,31716],{},"Spacer()",[57,31718,31719,16122,31721],{},[136,31720,31716],{},[136,31722,31723],{},"Modifier.padding()",[57,31725,31726,16122,31728,16122,31731],{},[136,31727,1603],{},[136,31729,31730],{},"padding",[136,31732,31733],{},"gap",[42,31735,31736,31739,31746,31754,31761],{},[57,31737,31738],{},"滚动",[57,31740,31741,16122,31743],{},[136,31742,18787],{},[136,31744,31745],{},"SingleChildScrollView",[57,31747,31748,16122,31751],{},[136,31749,31750],{},"ScrollView",[136,31752,31753],{},"List",[57,31755,31756,16122,31758],{},[136,31757,30844],{},[136,31759,31760],{},"verticalScroll",[57,31762,31763],{},[136,31764,31765],{},"overflow: auto",[32,31767,31769],{"id":31768},"_42-约束传递机制","4.2 约束传递机制",[186,31771,31774],{"className":31772,"code":31773,"language":1074},[1072],"Flutter: 父传约束 → 子确定大小 → 父决定位置（单次遍历，O(n)）\n         BoxConstraints(minW, maxW, minH, maxH) → Size → Offset\n\nSwiftUI: 父提供建议尺寸 → 子返回实际尺寸 → 父决定位置\n         ProposedViewSize → ViewDimensions → position\n\nCompose: 父传约束 → 子测量返回 Placeable → 父布局\n         Constraints → MeasureResult → layout { placeable.place(x, y) }\n\nVue\u002FCSS: 盒模型 → 流式布局\u002FFlex\u002FGrid\n         content-box \u002F border-box → 流式计算\n",[136,31775,31773],{"__ignoreMap":191},[11,31777,31778,31782],{},[14,31779,29042,31780,333],{},[125,31781,29045],{},[129,31783,31784,31789,31796,31802],{},[132,31785,15736,31786,31788],{},[136,31787,22313],{}," 是布局三原则",[132,31790,31791,31792,31795],{},"SwiftUI 布局是",[125,31793,31794],{},"子控制大小","：父只提建议，子自己决定多大（和 Flutter 不同）",[132,31797,30841,31798,31801],{},[136,31799,31800],{},"intrinsic measurement"," 允许子组件查询兄弟尺寸（SubcomposeLayout）",[132,31803,31804,31805,31808],{},"CSS 的 ",[136,31806,31807],{},"flex-grow\u002Fshrink\u002Fbasis"," 三属性组合是 Flexbox 的核心",[11,31810,31811,31815],{},[14,31812,29055,31813,333],{},[125,31814,29058],{},[129,31816,31817,31830,31838,31844],{},[132,31818,30616,31819,31821,31822,31825,31826,31461,31828,698],{},[136,31820,18680],{}," 中子组件高度无限 + 外层没有约束 → ",[136,31823,31824],{},"unbounded height"," 崩溃（需 ",[136,31827,31679],{},[136,31829,31682],{},[132,31831,30616,31832,31834,31835,31837],{},[136,31833,18695],{}," 中放 ",[136,31836,18787],{}," 不加约束 → 无限宽度崩溃",[132,31839,30627,31840,31843],{},[136,31841,31842],{},".frame()"," 只是提建议，不是强制约束；子 View 可以超出 frame",[132,31845,31846,31847,31849],{},"Vue\u002FCSS: ",[136,31848,1436],{}," 在嵌套 flex 容器中不生效时，检查父容器是否设了高度",[11,31851,31852,31856],{},[14,31853,29091,31854,333],{},[125,31855,29094],{},[129,31857,31858,31874,31877],{},[132,31859,31860,31861,1437,31863,31865,31866,31869,31870,31873],{},"Flutter 中 ",[136,31862,31679],{},[136,31864,31682],{}," 的区别？（Expanded 是 ",[136,31867,31868],{},"fit: FlexFit.tight"," 必须填满，Flexible 是 ",[136,31871,31872],{},"loose"," 可以小于分配空间）",[132,31875,31876],{},"解释 Flutter 的 \"tight constraints\" 和 \"loose constraints\"？（tight: min==max, loose: min==0）",[132,31878,31879,31880,1437,31883,31886],{},"CSS ",[136,31881,31882],{},"flex: 1 1 0",[136,31884,31885],{},"flex: 1 1 auto"," 的区别？（basis 为 0 按比例分配，auto 先满足内容再分配剩余）",[1890,31888],{},[18,31890,31892],{"id":31891},"_5-导航与路由","5. 导航与路由",[32,31894,31896],{"id":31895},"_51-路由方案对比","5.1 路由方案对比",[36,31898,31899,31916],{},[39,31900,31901],{},[42,31902,31903,31905,31907,31909,31912,31914],{},[45,31904,15463],{},[45,31906,13135],{},[45,31908,30106],{},[45,31910,31911],{},"iOS (UIKit)",[45,31913,30109],{},[45,31915,15468],{},[52,31917,31918,31936,31954,31974,31994,32016,32037],{},[42,31919,31920,31923,31926,31929,31931,31934],{},[57,31921,31922],{},"声明式路由",[57,31924,31925],{},"GoRouter",[57,31927,31928],{},"NavigationStack",[57,31930,16661],{},[57,31932,31933],{},"Navigation Compose",[57,31935,15526],{},[42,31937,31938,31941,31944,31946,31949,31951],{},[57,31939,31940],{},"命令式路由",[57,31942,31943],{},"Navigator.push",[57,31945,16661],{},[57,31947,31948],{},"UINavigationController.push",[57,31950,16661],{},[57,31952,31953],{},"router.push",[42,31955,31956,31959,31962,31965,31968,31971],{},[57,31957,31958],{},"路由定义",[57,31960,31961],{},"RouteConfiguration",[57,31963,31964],{},"NavigationPath",[57,31966,31967],{},"Storyboard\u002F代码",[57,31969,31970],{},"NavHost + composable()",[57,31972,31973],{},"createRouter()",[42,31975,31976,31979,31982,31985,31988,31991],{},[57,31977,31978],{},"参数传递",[57,31980,31981],{},"pathParameters \u002F extra",[57,31983,31984],{},"NavigationDestination init",[57,31986,31987],{},"prepareForSegue \u002F 属性赋值",[57,31989,31990],{},"arguments Bundle \u002F 类型安全",[57,31992,31993],{},"params \u002F query \u002F props",[42,31995,31996,31999,32002,32007,32010,32013],{},[57,31997,31998],{},"深链接",[57,32000,32001],{},"GoRouter deepLink",[57,32003,32004],{},[136,32005,32006],{},"onOpenURL",[57,32008,32009],{},"Universal Links",[57,32011,32012],{},"Deep Links",[57,32014,32015],{},"Vue Router 本身",[42,32017,32018,32021,32024,32026,32028,32030],{},[57,32019,32020],{},"路由守卫",[57,32022,32023],{},"GoRouter redirect",[57,32025,16661],{},[57,32027,16661],{},[57,32029,16661],{},[57,32031,32032,16122,32034],{},[136,32033,18059],{},[136,32035,32036],{},"beforeEnter",[42,32038,32039,32042,32045,32048,32051,32054],{},[57,32040,32041],{},"嵌套路由",[57,32043,32044],{},"ShellRoute",[57,32046,32047],{},"NavigationSplitView",[57,32049,32050],{},"Tab + Nav 组合",[57,32052,32053],{},"nested NavHost",[57,32055,32056,32059],{},[136,32057,32058],{},"children"," 嵌套",[32,32061,32063],{"id":32062},"_52-基本路由代码","5.2 基本路由代码",[186,32065,32067],{"className":11162,"code":32066,"language":11164,"meta":191,"style":191},"\u002F\u002F Flutter — GoRouter\nfinal router = GoRouter(\n  routes: [\n    GoRoute(path: '\u002F', builder: (_, __) => HomePage()),\n    GoRoute(\n      path: '\u002Fproduct\u002F:id',\n      builder: (_, state) => ProductPage(id: state.pathParameters['id']!),\n    ),\n  ],\n  redirect: (context, state) {\n    if (!isLoggedIn) return '\u002Flogin';\n    return null;\n  },\n);\n",[136,32068,32069,32074,32078,32082,32087,32091,32095,32100,32104,32108,32112,32117,32121,32125],{"__ignoreMap":191},[195,32070,32071],{"class":197,"line":198},[195,32072,32073],{},"\u002F\u002F Flutter — GoRouter\n",[195,32075,32076],{"class":197,"line":230},[195,32077,27324],{},[195,32079,32080],{"class":197,"line":251},[195,32081,17806],{},[195,32083,32084],{"class":197,"line":272},[195,32085,32086],{},"    GoRoute(path: '\u002F', builder: (_, __) => HomePage()),\n",[195,32088,32089],{"class":197,"line":293},[195,32090,27333],{},[195,32092,32093],{"class":197,"line":562},[195,32094,27356],{},[195,32096,32097],{"class":197,"line":583},[195,32098,32099],{},"      builder: (_, state) => ProductPage(id: state.pathParameters['id']!),\n",[195,32101,32102],{"class":197,"line":962},[195,32103,22508],{},[195,32105,32106],{"class":197,"line":968},[195,32107,17864],{},[195,32109,32110],{"class":197,"line":1274},[195,32111,25384],{},[195,32113,32114],{"class":197,"line":1282},[195,32115,32116],{},"    if (!isLoggedIn) return '\u002Flogin';\n",[195,32118,32119],{"class":197,"line":1295},[195,32120,27447],{},[195,32122,32123],{"class":197,"line":1309},[195,32124,11963],{},[195,32126,32127],{"class":197,"line":2246},[195,32128,527],{},[186,32130,32132],{"className":2177,"code":32131,"language":2179,"meta":191,"style":191},"\u002F\u002F SwiftUI — NavigationStack (iOS 16+)\nNavigationStack(path: $path) {\n    HomeView()\n        .navigationDestination(for: Product.self) { product in\n            ProductDetailView(product: product)\n        }\n}\n\u002F\u002F 导航: path.append(product)\n",[136,32133,32134,32139,32144,32149,32154,32159,32163,32167],{"__ignoreMap":191},[195,32135,32136],{"class":197,"line":198},[195,32137,32138],{},"\u002F\u002F SwiftUI — NavigationStack (iOS 16+)\n",[195,32140,32141],{"class":197,"line":230},[195,32142,32143],{},"NavigationStack(path: $path) {\n",[195,32145,32146],{"class":197,"line":251},[195,32147,32148],{},"    HomeView()\n",[195,32150,32151],{"class":197,"line":272},[195,32152,32153],{},"        .navigationDestination(for: Product.self) { product in\n",[195,32155,32156],{"class":197,"line":293},[195,32157,32158],{},"            ProductDetailView(product: product)\n",[195,32160,32161],{"class":197,"line":562},[195,32162,2887],{},[195,32164,32165],{"class":197,"line":583},[195,32166,552],{},[195,32168,32169],{"class":197,"line":962},[195,32170,32171],{},"\u002F\u002F 导航: path.append(product)\n",[186,32173,32175],{"className":6838,"code":32174,"language":6840,"meta":191,"style":191},"\u002F\u002F Compose — Navigation\nNavHost(navController, startDestination = \"home\") {\n    composable(\"home\") { HomeScreen() }\n    composable(\"product\u002F{id}\") { backStackEntry ->\n        ProductScreen(id = backStackEntry.arguments?.getString(\"id\"))\n    }\n}\n\u002F\u002F 导航: navController.navigate(\"product\u002F123\")\n",[136,32176,32177,32182,32187,32192,32197,32202,32206,32210],{"__ignoreMap":191},[195,32178,32179],{"class":197,"line":198},[195,32180,32181],{},"\u002F\u002F Compose — Navigation\n",[195,32183,32184],{"class":197,"line":230},[195,32185,32186],{},"NavHost(navController, startDestination = \"home\") {\n",[195,32188,32189],{"class":197,"line":251},[195,32190,32191],{},"    composable(\"home\") { HomeScreen() }\n",[195,32193,32194],{"class":197,"line":272},[195,32195,32196],{},"    composable(\"product\u002F{id}\") { backStackEntry ->\n",[195,32198,32199],{"class":197,"line":293},[195,32200,32201],{},"        ProductScreen(id = backStackEntry.arguments?.getString(\"id\"))\n",[195,32203,32204],{"class":197,"line":562},[195,32205,2403],{},[195,32207,32208],{"class":197,"line":583},[195,32209,552],{},[195,32211,32212],{"class":197,"line":962},[195,32213,32214],{},"\u002F\u002F 导航: navController.navigate(\"product\u002F123\")\n",[186,32216,32218],{"className":383,"code":32217,"language":385,"meta":191,"style":191},"\u002F\u002F Vue Router\nconst router = createRouter({\n  routes: [\n    { path: '\u002F', component: HomePage },\n    { path: '\u002Fproduct\u002F:id', component: ProductPage, props: true },\n  ],\n})\nrouter.beforeEach((to, from) => {\n  if (!isLoggedIn && to.meta.requiresAuth) return '\u002Flogin'\n})\n",[136,32219,32220,32225,32237,32241,32250,32266,32270,32274,32294,32316],{"__ignoreMap":191},[195,32221,32222],{"class":197,"line":198},[195,32223,32224],{"class":415},"\u002F\u002F Vue Router\n",[195,32226,32227,32229,32231,32233,32235],{"class":197,"line":230},[195,32228,392],{"class":223},[195,32230,17782],{"class":213},[195,32232,398],{"class":223},[195,32234,17787],{"class":201},[195,32236,17790],{"class":209},[195,32238,32239],{"class":197,"line":251},[195,32240,17806],{"class":209},[195,32242,32243,32245,32247],{"class":197,"line":272},[195,32244,17811],{"class":209},[195,32246,17814],{"class":459},[195,32248,32249],{"class":209},", component: HomePage },\n",[195,32251,32252,32254,32257,32260,32263],{"class":197,"line":293},[195,32253,17811],{"class":209},[195,32255,32256],{"class":459},"'\u002Fproduct\u002F:id'",[195,32258,32259],{"class":209},", component: ProductPage, props: ",[195,32261,32262],{"class":213},"true",[195,32264,32265],{"class":209}," },\n",[195,32267,32268],{"class":197,"line":562},[195,32269,17864],{"class":209},[195,32271,32272],{"class":197,"line":583},[195,32273,16566],{"class":209},[195,32275,32276,32278,32280,32282,32284,32286,32288,32290,32292],{"class":197,"line":962},[195,32277,18021],{"class":209},[195,32279,18059],{"class":201},[195,32281,18062],{"class":209},[195,32283,18065],{"class":634},[195,32285,301],{"class":209},[195,32287,15967],{"class":634},[195,32289,516],{"class":209},[195,32291,16398],{"class":223},[195,32293,496],{"class":209},[195,32295,32296,32298,32301,32303,32306,32308,32311,32313],{"class":197,"line":968},[195,32297,18080],{"class":223},[195,32299,32300],{"class":209}," (",[195,32302,29012],{"class":223},[195,32304,32305],{"class":209},"isLoggedIn ",[195,32307,18086],{"class":223},[195,32309,32310],{"class":209}," to.meta.requiresAuth) ",[195,32312,30005],{"class":223},[195,32314,32315],{"class":459}," '\u002Flogin'\n",[195,32317,32318],{"class":197,"line":1274},[195,32319,16566],{"class":209},[11,32321,32322,32326],{},[14,32323,29042,32324,333],{},[125,32325,29045],{},[129,32327,32328,32331,32338,32341],{},[132,32329,32330],{},"Flutter Navigator 2.0 的声明式路由（RouterDelegate + RouteInformationParser）非常复杂，GoRouter 是社区对其的简化封装",[132,32332,32333,32334,32337],{},"SwiftUI 在 iOS 16 之前没有好的 programmatic navigation 方案，",[136,32335,32336],{},"NavigationLink"," 是纯声明式的",[132,32339,32340],{},"Compose Navigation 的参数传递用字符串不够类型安全，社区有 type-safe navigation 方案",[132,32342,32343],{},"Vue Router 的路由守卫支持异步（返回 Promise），可以做权限校验和数据预加载",[11,32345,32346,32350],{},[14,32347,29055,32348,333],{},[125,32349,29058],{},[129,32351,32352,32362,32373,32380],{},[132,32353,30616,32354,32357,32358,32361],{},[136,32355,32356],{},"Navigator.pop()"," 返回数据时需要 ",[136,32359,32360],{},"await Navigator.push()"," 接收",[132,32363,30627,32364,905,32366,32369,32370],{},[136,32365,31928],{},[136,32367,32368],{},"path"," 数组中的类型必须是 ",[136,32371,32372],{},"Hashable",[132,32374,32375,32376,32379],{},"Compose: 避免在 ",[136,32377,32378],{},"composable()"," 中传大对象，应该传 ID 再从 ViewModel 获取",[132,32381,32382,32383,32386,32387,32390,32391],{},"Vue: 动态路由 ",[136,32384,32385],{},"\u002Fproduct\u002F:id"," 切换时组件不销毁重建，需要 ",[136,32388,32389],{},"watch(() => route.params.id)"," 或加 ",[136,32392,19628],{},[11,32394,32395,32399],{},[14,32396,29091,32397,333],{},[125,32398,29094],{},[129,32400,32401,32404,32419],{},[132,32402,32403],{},"Flutter Navigator 1.0 和 2.0 的区别？（1.0 命令式栈操作，2.0 声明式路由配置）",[132,32405,32406,32407,32410,32411,32414,32415,32418],{},"Vue Router 的 ",[136,32408,32409],{},"hash"," 模式和 ",[136,32412,32413],{},"history"," 模式的区别？（hash 用 ",[136,32416,32417],{},"#"," 不需服务端配置，history 需要服务端 fallback）",[132,32420,32421],{},"iOS deep link 的 Universal Links 和 URL Schemes 的区别？（Universal Links 走 HTTPS 验证更安全，不会弹确认框）",[1890,32423],{},[18,32425,32427],{"id":32426},"_6-网络与数据层","6. 网络与数据层",[32,32429,32431],{"id":32430},"_61-http-客户端对比","6.1 HTTP 客户端对比",[36,32433,32434,32449],{},[39,32435,32436],{},[42,32437,32438,32440,32442,32444,32446],{},[45,32439,15463],{},[45,32441,13135],{},[45,32443,13113],{},[45,32445,13124],{},[45,32447,32448],{},"Vue \u002F 前端",[52,32450,32451,32479,32496,32513,32530,32547],{},[42,32452,32453,32456,32461,32467,32473],{},[57,32454,32455],{},"主流库",[57,32457,32458],{},[125,32459,32460],{},"Dio",[57,32462,32463,32466],{},[125,32464,32465],{},"URLSession"," (原生) \u002F Alamofire",[57,32468,32469,32472],{},[125,32470,32471],{},"Retrofit"," + OkHttp",[57,32474,32475,32478],{},[125,32476,32477],{},"Axios"," \u002F fetch API",[42,32480,32481,32484,32487,32490,32493],{},[57,32482,32483],{},"拦截器",[57,32485,32486],{},"Dio Interceptor",[57,32488,32489],{},"URLProtocol \u002F Alamofire Interceptor",[57,32491,32492],{},"OkHttp Interceptor",[57,32494,32495],{},"Axios Interceptor",[42,32497,32498,32501,32504,32507,32510],{},[57,32499,32500],{},"序列化",[57,32502,32503],{},"json_serializable \u002F freezed",[57,32505,32506],{},"Codable",[57,32508,32509],{},"Gson \u002F Moshi \u002F kotlinx.serialization",[57,32511,32512],{},"原生 JSON \u002F zod \u002F io-ts",[42,32514,32515,32518,32521,32524,32527],{},[57,32516,32517],{},"取消请求",[57,32519,32520],{},"CancelToken",[57,32522,32523],{},"URLSessionTask.cancel()",[57,32525,32526],{},"Coroutine cancel",[57,32528,32529],{},"AbortController",[42,32531,32532,32535,32538,32541,32544],{},[57,32533,32534],{},"文件上传",[57,32536,32537],{},"FormData (Dio)",[57,32539,32540],{},"URLSession uploadTask",[57,32542,32543],{},"MultipartBody (OkHttp)",[57,32545,32546],{},"FormData (Axios)",[42,32548,32549,32552,32555,32558,32561],{},[57,32550,32551],{},"WebSocket",[57,32553,32554],{},"web_socket_channel",[57,32556,32557],{},"URLSessionWebSocketTask",[57,32559,32560],{},"OkHttp WebSocket",[57,32562,32563],{},"原生 WebSocket \u002F Socket.io",[32,32565,32567],{"id":32566},"_62-json-序列化对比","6.2 JSON 序列化对比",[186,32569,32571],{"className":11162,"code":32570,"language":11164,"meta":191,"style":191},"\u002F\u002F Dart — json_serializable\n@JsonSerializable()\nclass User {\n  final int id;\n  final String name;\n  User({required this.id, required this.name});\n  factory User.fromJson(Map\u003CString, dynamic> json) => _$UserFromJson(json);\n  Map\u003CString, dynamic> toJson() => _$UserToJson(this);\n}\n\u002F\u002F 需要 build_runner 生成 .g.dart\n",[136,32572,32573,32578,32583,32588,32593,32597,32602,32607,32612,32616],{"__ignoreMap":191},[195,32574,32575],{"class":197,"line":198},[195,32576,32577],{},"\u002F\u002F Dart — json_serializable\n",[195,32579,32580],{"class":197,"line":230},[195,32581,32582],{},"@JsonSerializable()\n",[195,32584,32585],{"class":197,"line":251},[195,32586,32587],{},"class User {\n",[195,32589,32590],{"class":197,"line":272},[195,32591,32592],{},"  final int id;\n",[195,32594,32595],{"class":197,"line":293},[195,32596,15601],{},[195,32598,32599],{"class":197,"line":562},[195,32600,32601],{},"  User({required this.id, required this.name});\n",[195,32603,32604],{"class":197,"line":583},[195,32605,32606],{},"  factory User.fromJson(Map\u003CString, dynamic> json) => _$UserFromJson(json);\n",[195,32608,32609],{"class":197,"line":962},[195,32610,32611],{},"  Map\u003CString, dynamic> toJson() => _$UserToJson(this);\n",[195,32613,32614],{"class":197,"line":968},[195,32615,552],{},[195,32617,32618],{"class":197,"line":1274},[195,32619,32620],{},"\u002F\u002F 需要 build_runner 生成 .g.dart\n",[186,32622,32624],{"className":2177,"code":32623,"language":2179,"meta":191,"style":191},"\u002F\u002F Swift — Codable（编译器自动合成）\nstruct User: Codable {\n    let id: Int\n    let name: String\n}\nlet user = try JSONDecoder().decode(User.self, from: data)\n",[136,32625,32626,32631,32636,32641,32645,32649],{"__ignoreMap":191},[195,32627,32628],{"class":197,"line":198},[195,32629,32630],{},"\u002F\u002F Swift — Codable（编译器自动合成）\n",[195,32632,32633],{"class":197,"line":230},[195,32634,32635],{},"struct User: Codable {\n",[195,32637,32638],{"class":197,"line":251},[195,32639,32640],{},"    let id: Int\n",[195,32642,32643],{"class":197,"line":272},[195,32644,5146],{},[195,32646,32647],{"class":197,"line":293},[195,32648,552],{},[195,32650,32651],{"class":197,"line":562},[195,32652,32653],{},"let user = try JSONDecoder().decode(User.self, from: data)\n",[186,32655,32657],{"className":6838,"code":32656,"language":6840,"meta":191,"style":191},"\u002F\u002F Kotlin — kotlinx.serialization\n@Serializable\ndata class User(val id: Int, val name: String)\nval user = Json.decodeFromString\u003CUser>(jsonString)\n",[136,32658,32659,32664,32669,32674],{"__ignoreMap":191},[195,32660,32661],{"class":197,"line":198},[195,32662,32663],{},"\u002F\u002F Kotlin — kotlinx.serialization\n",[195,32665,32666],{"class":197,"line":230},[195,32667,32668],{},"@Serializable\n",[195,32670,32671],{"class":197,"line":251},[195,32672,32673],{},"data class User(val id: Int, val name: String)\n",[195,32675,32676],{"class":197,"line":272},[195,32677,32678],{},"val user = Json.decodeFromString\u003CUser>(jsonString)\n",[186,32680,32682],{"className":383,"code":32681,"language":385,"meta":191,"style":191},"\u002F\u002F TypeScript — 运行时无类型，需要手动校验或用 zod\ninterface User { id: number; name: string }\n\u002F\u002F 简单方式（不安全）\nconst user = response.data as User;\n\u002F\u002F 安全方式（zod）\nconst UserSchema = z.object({ id: z.number(), name: z.string() });\nconst user = UserSchema.parse(response.data);\n",[136,32683,32684,32689,32714,32719,32736,32741,32770],{"__ignoreMap":191},[195,32685,32686],{"class":197,"line":198},[195,32687,32688],{"class":415},"\u002F\u002F TypeScript — 运行时无类型，需要手动校验或用 zod\n",[195,32690,32691,32693,32696,32698,32700,32702,32704,32706,32708,32710,32712],{"class":197,"line":230},[195,32692,29351],{"class":223},[195,32694,32695],{"class":201}," User",[195,32697,210],{"class":209},[195,32699,30837],{"class":634},[195,32701,638],{"class":223},[195,32703,29363],{"class":213},[195,32705,29366],{"class":209},[195,32707,13388],{"class":634},[195,32709,638],{"class":223},[195,32711,15715],{"class":213},[195,32713,18291],{"class":209},[195,32715,32716],{"class":197,"line":251},[195,32717,32718],{"class":415},"\u002F\u002F 简单方式（不安全）\n",[195,32720,32721,32723,32725,32727,32730,32732,32734],{"class":197,"line":272},[195,32722,392],{"class":223},[195,32724,16235],{"class":213},[195,32726,398],{"class":223},[195,32728,32729],{"class":209}," response.data ",[195,32731,29085],{"class":223},[195,32733,32695],{"class":201},[195,32735,547],{"class":209},[195,32737,32738],{"class":197,"line":293},[195,32739,32740],{"class":415},"\u002F\u002F 安全方式（zod）\n",[195,32742,32743,32745,32748,32750,32753,32755,32758,32761,32764,32767],{"class":197,"line":562},[195,32744,392],{"class":223},[195,32746,32747],{"class":213}," UserSchema",[195,32749,398],{"class":223},[195,32751,32752],{"class":209}," z.",[195,32754,6827],{"class":201},[195,32756,32757],{"class":209},"({ id: z.",[195,32759,32760],{"class":201},"number",[195,32762,32763],{"class":209},"(), name: z.",[195,32765,32766],{"class":201},"string",[195,32768,32769],{"class":209},"() });\n",[195,32771,32772,32774,32776,32778,32781,32784],{"class":197,"line":583},[195,32773,392],{"class":223},[195,32775,16235],{"class":213},[195,32777,398],{"class":223},[195,32779,32780],{"class":209}," UserSchema.",[195,32782,32783],{"class":201},"parse",[195,32785,32786],{"class":209},"(response.data);\n",[11,32788,32789,32793],{},[14,32790,29042,32791,333],{},[125,32792,29045],{},[129,32794,32795,32800,32814,32819],{},[132,32796,29700,32797,32799],{},[136,32798,32506],{}," 最优雅，编译器自动生成 encode\u002Fdecode（无需 code gen 工具）",[132,32801,32802,32803,4728,32806,32809,32810,32813],{},"Dart 需要 ",[136,32804,32805],{},"build_runner",[136,32807,32808],{},"json_serializable"," 生成代码，或者用 ",[136,32811,32812],{},"freezed"," 同时生成 copyWith\u002Fequals\u002FtoString",[132,32815,29461,32816,32818],{},[136,32817,29351],{}," 在运行时被擦除，JSON 解析无法自动校验类型，zod 等库填补这个空白",[132,32820,32821],{},"Kotlin 有多种方案：Gson（反射）、Moshi（反射或 codegen）、kotlinx.serialization（编译器插件）",[11,32823,32824,32828],{},[14,32825,29055,32826,333],{},[125,32827,29058],{},[129,32829,32830,32841,32847,32853],{},[132,32831,32832,32833,32836,32837,32840],{},"Dart: 忘记运行 ",[136,32834,32835],{},"dart run build_runner build"," 导致 ",[136,32838,32839],{},".g.dart"," 文件不存在",[132,32842,32843,32844],{},"Swift: JSON key 和属性名不一致时需要自定义 ",[136,32845,32846],{},"CodingKeys",[132,32848,32849,32850,32852],{},"Kotlin: Gson 用反射创建对象，可以绕过 ",[136,32851,29228],{}," 约束创建不合法对象",[132,32854,32855,32856,32858],{},"TypeScript: ",[136,32857,29085],{}," 类型断言不做任何运行时检查，JSON 字段类型错误不会报错",[11,32860,32861,32865],{},[14,32862,29091,32863,333],{},[125,32864,29094],{},[129,32866,32867,32875,32888],{},[132,32868,32869,32870,31237,32872,32874],{},"Dart ",[136,32871,32808],{},[136,32873,32812],{}," 的区别？（freezed 额外生成 copyWith、equals、sealed union 支持）",[132,32876,32877,32878,32880,32881,1437,32884,32887],{},"Swift ",[136,32879,32506],{}," 的底层原理？（编译器自动合成 ",[136,32882,32883],{},"init(from: Decoder)",[136,32885,32886],{},"encode(to: Encoder)","，可自定义 container）",[132,32889,32890],{},"Retrofit 的原理？（动态代理 + 注解处理，接口方法 → HTTP 请求映射）",[1890,32892],{},[32,32894,32896],{"id":32895},"_63-错误处理模式","6.3 错误处理模式",[36,32898,32899,32914],{},[39,32900,32901],{},[42,32902,32903,32905,32908,32910,32912],{},[45,32904,852],{},[45,32906,32907],{},"Flutter (Dart)",[45,32909,29128],{},[45,32911,29131],{},[45,32913,29134],{},[52,32915,32916,32948,32969],{},[42,32917,32918,32921,32929,32936,32942],{},[57,32919,32920],{},"异常",[57,32922,32923,4728,32926],{},[136,32924,32925],{},"try-catch",[136,32927,32928],{},"throw",[57,32930,32931,4728,32934],{},[136,32932,32933],{},"do-try-catch",[136,32935,32928],{},[57,32937,32938,4728,32940],{},[136,32939,32925],{},[136,32941,32928],{},[57,32943,32944,4728,32946],{},[136,32945,32925],{},[136,32947,32928],{},[42,32949,32950,32953,32956,32961,32966],{},[57,32951,32952],{},"Result 类型",[57,32954,32955],{},"无内置（手写或三方）",[57,32957,32958],{},[136,32959,32960],{},"Result\u003CSuccess, Failure>",[57,32962,32963],{},[136,32964,32965],{},"Result\u003CT>",[57,32967,32968],{},"无内置（手写或 neverthrow）",[42,32970,32971,32974,32977,32980,32986],{},[57,32972,32973],{},"推荐模式",[57,32975,32976],{},"sealed class 包装",[57,32978,32979],{},"Result + typed throws (Swift 6)",[57,32981,32982,32985],{},[136,32983,32984],{},"runCatching {}"," \u002F sealed class",[57,32987,32988],{},"联合类型或 Result 模式",[186,32990,32992],{"className":11162,"code":32991,"language":11164,"meta":191,"style":191},"\u002F\u002F Dart — sealed class 包装（推荐）\nsealed class ApiResult\u003CT> {}\nclass Success\u003CT> extends ApiResult\u003CT> { final T data; Success(this.data); }\nclass Failure\u003CT> extends ApiResult\u003CT> { final String message; Failure(this.message); }\n",[136,32993,32994,32999,33004,33009],{"__ignoreMap":191},[195,32995,32996],{"class":197,"line":198},[195,32997,32998],{},"\u002F\u002F Dart — sealed class 包装（推荐）\n",[195,33000,33001],{"class":197,"line":230},[195,33002,33003],{},"sealed class ApiResult\u003CT> {}\n",[195,33005,33006],{"class":197,"line":251},[195,33007,33008],{},"class Success\u003CT> extends ApiResult\u003CT> { final T data; Success(this.data); }\n",[195,33010,33011],{"class":197,"line":272},[195,33012,33013],{},"class Failure\u003CT> extends ApiResult\u003CT> { final String message; Failure(this.message); }\n",[186,33015,33017],{"className":2177,"code":33016,"language":2179,"meta":191,"style":191},"\u002F\u002F Swift — Result 类型\nfunc fetchUser() async -> Result\u003CUser, APIError> {\n    do {\n        let user = try await api.getUser()\n        return .success(user)\n    } catch {\n        return .failure(.networkError(error))\n    }\n}\n",[136,33018,33019,33024,33029,33033,33038,33043,33047,33052,33056],{"__ignoreMap":191},[195,33020,33021],{"class":197,"line":198},[195,33022,33023],{},"\u002F\u002F Swift — Result 类型\n",[195,33025,33026],{"class":197,"line":230},[195,33027,33028],{},"func fetchUser() async -> Result\u003CUser, APIError> {\n",[195,33030,33031],{"class":197,"line":251},[195,33032,4805],{},[195,33034,33035],{"class":197,"line":272},[195,33036,33037],{},"        let user = try await api.getUser()\n",[195,33039,33040],{"class":197,"line":293},[195,33041,33042],{},"        return .success(user)\n",[195,33044,33045],{"class":197,"line":562},[195,33046,4820],{},[195,33048,33049],{"class":197,"line":583},[195,33050,33051],{},"        return .failure(.networkError(error))\n",[195,33053,33054],{"class":197,"line":962},[195,33055,2403],{},[195,33057,33058],{"class":197,"line":968},[195,33059,552],{},[1890,33061],{},[18,33063,33065],{"id":33064},"_7-持久化存储","7. 持久化存储",[32,33067,33069],{"id":33068},"_71-存储方案对比","7.1 存储方案对比",[36,33071,33072,33086],{},[39,33073,33074],{},[42,33075,33076,33078,33080,33082,33084],{},[45,33077,17273],{},[45,33079,13135],{},[45,33081,13113],{},[45,33083,13124],{},[45,33085,32448],{},[52,33087,33088,33105,33130,33147],{},[42,33089,33090,33093,33096,33099,33102],{},[57,33091,33092],{},"键值对",[57,33094,33095],{},"SharedPreferences",[57,33097,33098],{},"UserDefaults",[57,33100,33101],{},"DataStore (Preferences)",[57,33103,33104],{},"localStorage",[42,33106,33107,33110,33116,33122,33127],{},[57,33108,33109],{},"轻量数据库",[57,33111,33112,33115],{},[125,33113,33114],{},"Drift"," \u002F sqflite \u002F Hive",[57,33117,33118,33121],{},[125,33119,33120],{},"SwiftData"," \u002F Core Data \u002F GRDB",[57,33123,33124],{},[125,33125,33126],{},"Room",[57,33128,33129],{},"IndexedDB \u002F Dexie",[42,33131,33132,33135,33138,33141,33144],{},[57,33133,33134],{},"文件存储",[57,33136,33137],{},"path_provider + File",[57,33139,33140],{},"FileManager",[57,33142,33143],{},"Context.filesDir",[57,33145,33146],{},"File API \u002F Blob",[42,33148,33149,33152,33155,33157,33160],{},[57,33150,33151],{},"安全存储",[57,33153,33154],{},"flutter_secure_storage",[57,33156,13116],{},[57,33158,33159],{},"EncryptedSharedPreferences",[57,33161,33162],{},"— (后端处理)",[32,33164,33166],{"id":33165},"_72-orm-对比","7.2 ORM 对比",[36,33168,33169,33184],{},[39,33170,33171],{},[42,33172,33173,33175,33178,33181],{},[45,33174,2086],{},[45,33176,33177],{},"Drift (Flutter)",[45,33179,33180],{},"SwiftData (iOS)",[45,33182,33183],{},"Room (Android)",[52,33185,33186,33206,33229,33247,33261,33279],{},[42,33187,33188,33191,33194,33200],{},[57,33189,33190],{},"定义方式",[57,33192,33193],{},"Dart 类 + 注解",[57,33195,33196,33199],{},[136,33197,33198],{},"@Model"," 宏",[57,33201,33202,33205],{},[136,33203,33204],{},"@Entity"," 注解",[42,33207,33208,33211,33214,33223],{},[57,33209,33210],{},"查询方式",[57,33212,33213],{},"类型安全 Dart DSL",[57,33215,33216,33219,33220],{},[136,33217,33218],{},"@Query"," 宏 + ",[136,33221,33222],{},"#Predicate",[57,33224,33225,33228],{},[136,33226,33227],{},"@Dao"," + SQL 字符串",[42,33230,33231,33234,33237,33242],{},[57,33232,33233],{},"关系",[57,33235,33236],{},"外键 + Join",[57,33238,33239],{},[136,33240,33241],{},"@Relationship",[57,33243,33244],{},[136,33245,33246],{},"@Relation",[42,33248,33249,33252,33255,33258],{},[57,33250,33251],{},"迁移",[57,33253,33254],{},"手动 schema 版本",[57,33256,33257],{},"自动轻量迁移",[57,33259,33260],{},"手动 Migration",[42,33262,33263,33266,33271,33274],{},[57,33264,33265],{},"响应式",[57,33267,33268,33270],{},[136,33269,16121],{}," 返回 Stream",[57,33272,33273],{},"SwiftUI 自动观察",[57,33275,33276],{},[136,33277,33278],{},"Flow\u003CList\u003CT>>",[42,33280,33281,33284,33287,33290],{},[57,33282,33283],{},"Code Gen",[57,33285,33286],{},"需要 build_runner",[57,33288,33289],{},"编译器宏（无 code gen 步骤）",[57,33291,33292],{},"需要 kapt\u002Fksp",[11,33294,33295,33299],{},[14,33296,29042,33297,333],{},[125,33298,29045],{},[129,33300,33301,33314,33317],{},[132,33302,33303,33304,33306,33307,33309,33310,33313],{},"Room 的 ",[136,33305,33227],{}," 方法返回 ",[136,33308,33278],{}," 可以直接在 Compose 中 ",[136,33311,33312],{},"collectAsState()","，实现数据库→UI 的响应式链路",[132,33315,33316],{},"SwiftData 是 Core Data 的现代封装，用 Swift 宏替代了繁琐的 NSManagedObject 子类",[132,33318,33319,33320,33322,33323,33325],{},"Drift 的 ",[136,33321,16121],{}," 返回 Stream，数据库变更时自动推送新数据，配合 ",[136,33324,16116],{}," 使用",[11,33327,33328,33332],{},[14,33329,29055,33330,333],{},[125,33331,29058],{},[129,33333,33334,33341,33347],{},[132,33335,33336,33337,33340],{},"SharedPreferences \u002F UserDefaults ",[125,33338,33339],{},"不是","加密存储！敏感数据（token、密码）必须用 Keychain \u002F flutter_secure_storage",[132,33342,33343,33344,698],{},"Room 的迁移如果漏写某个 schema 变更会导致崩溃（",[136,33345,33346],{},"IllegalStateException",[132,33348,33349,33350,33353,33354,33357],{},"Core Data 的 ",[136,33351,33352],{},"NSManagedObjectContext"," 不是线程安全的，必须用 ",[136,33355,33356],{},"perform {}"," 包裹",[11,33359,33360,33364],{},[14,33361,29091,33362,333],{},[125,33363,29094],{},[129,33365,33366,33369,33372],{},[132,33367,33368],{},"SharedPreferences 的底层实现？（Android: XML 文件，iOS: plist\u002FUserDefaults，Flutter: 平台桥接）",[132,33370,33371],{},"Room vs Realm vs SQLDelight 怎么选？（Room 是 Google 官方，SQLDelight 跨平台，Realm 已停止维护）",[132,33373,33374],{},"前端 localStorage vs sessionStorage vs Cookie 的区别？（localStorage 持久化，sessionStorage 会话级，Cookie 每次请求自动发送）",[1890,33376],{},[18,33378,33380],{"id":33379},"_8-并发与异步编程","8. 并发与异步编程",[32,33382,33384],{"id":33383},"_81-线程并发模型对比","8.1 线程\u002F并发模型对比",[36,33386,33387,33402],{},[39,33388,33389],{},[42,33390,33391,33393,33395,33397,33399],{},[45,33392,15463],{},[45,33394,15478],{},[45,33396,29128],{},[45,33398,29131],{},[45,33400,33401],{},"JavaScript\u002FTypeScript",[52,33403,33404,33430,33447,33480,33498],{},[42,33405,33406,33409,33415,33421,33426],{},[57,33407,33408],{},"线程模型",[57,33410,33411,33414],{},[125,33412,33413],{},"单线程"," + Event Loop",[57,33416,33417,33420],{},[125,33418,33419],{},"多线程"," + GCD\u002Fasync-await",[57,33422,33423,33425],{},[125,33424,33419],{}," + Coroutines",[57,33427,33428,33414],{},[125,33429,33413],{},[42,33431,33432,33435,33438,33441,33444],{},[57,33433,33434],{},"并行隔离",[57,33436,33437],{},"Isolate（独立内存）",[57,33439,33440],{},"Actor（共享内存 + 隔离访问）",[57,33442,33443],{},"Thread \u002F Coroutine Dispatcher",[57,33445,33446],{},"Web Worker（独立内存）",[42,33448,33449,33452,33458,33465,33473],{},[57,33450,33451],{},"异步原语",[57,33453,33454,16122,33456],{},[136,33455,23759],{},[136,33457,23762],{},[57,33459,33460,16122,33462],{},[136,33461,5387],{},[136,33463,33464],{},"AsyncSequence",[57,33466,33467,16122,33470],{},[136,33468,33469],{},"suspend fun",[136,33471,33472],{},"Flow",[57,33474,33475,16122,33477],{},[136,33476,23804],{},[136,33478,33479],{},"AsyncIterator",[42,33481,33482,33485,33488,33491,33496],{},[57,33483,33484],{},"调度器",[57,33486,33487],{},"Event Loop（不可选）",[57,33489,33490],{},"MainActor \u002F 自定义 Actor",[57,33492,33493],{},[136,33494,33495],{},"Dispatchers.Main\u002FIO\u002FDefault",[57,33497,33487],{},[42,33499,33500,33503,33506,33509,33512],{},[57,33501,33502],{},"取消机制",[57,33504,33505],{},"无内置（需手动）",[57,33507,33508],{},"Task.cancel() (cooperative)",[57,33510,33511],{},"Job.cancel() (cooperative)",[57,33513,32529],{},[32,33515,33517],{"id":33516},"_82-异步代码对比","8.2 异步代码对比",[186,33519,33521],{"className":11162,"code":33520,"language":11164,"meta":191,"style":191},"\u002F\u002F Dart — Future + async\u002Fawait\nFuture\u003CUser> fetchUser() async {\n  final response = await dio.get('\u002Fuser');\n  return User.fromJson(response.data);\n}\n\u002F\u002F 并行\nfinal results = await Future.wait([fetchUser(), fetchOrders()]);\n",[136,33522,33523,33528,33533,33538,33543,33547,33552],{"__ignoreMap":191},[195,33524,33525],{"class":197,"line":198},[195,33526,33527],{},"\u002F\u002F Dart — Future + async\u002Fawait\n",[195,33529,33530],{"class":197,"line":230},[195,33531,33532],{},"Future\u003CUser> fetchUser() async {\n",[195,33534,33535],{"class":197,"line":251},[195,33536,33537],{},"  final response = await dio.get('\u002Fuser');\n",[195,33539,33540],{"class":197,"line":272},[195,33541,33542],{},"  return User.fromJson(response.data);\n",[195,33544,33545],{"class":197,"line":293},[195,33546,552],{},[195,33548,33549],{"class":197,"line":562},[195,33550,33551],{},"\u002F\u002F 并行\n",[195,33553,33554],{"class":197,"line":583},[195,33555,33556],{},"final results = await Future.wait([fetchUser(), fetchOrders()]);\n",[186,33558,33560],{"className":2177,"code":33559,"language":2179,"meta":191,"style":191},"\u002F\u002F Swift — structured concurrency\nfunc fetchUser() async throws -> User {\n    let (data, _) = try await URLSession.shared.data(from: url)\n    return try JSONDecoder().decode(User.self, from: data)\n}\n\u002F\u002F 并行\nasync let user = fetchUser()\nasync let orders = fetchOrders()\nlet (u, o) = try await (user, orders)\n",[136,33561,33562,33567,33572,33576,33580,33584,33588,33593,33598],{"__ignoreMap":191},[195,33563,33564],{"class":197,"line":198},[195,33565,33566],{},"\u002F\u002F Swift — structured concurrency\n",[195,33568,33569],{"class":197,"line":230},[195,33570,33571],{},"func fetchUser() async throws -> User {\n",[195,33573,33574],{"class":197,"line":251},[195,33575,4777],{},[195,33577,33578],{"class":197,"line":272},[195,33579,4782],{},[195,33581,33582],{"class":197,"line":293},[195,33583,552],{},[195,33585,33586],{"class":197,"line":562},[195,33587,33551],{},[195,33589,33590],{"class":197,"line":583},[195,33591,33592],{},"async let user = fetchUser()\n",[195,33594,33595],{"class":197,"line":962},[195,33596,33597],{},"async let orders = fetchOrders()\n",[195,33599,33600],{"class":197,"line":968},[195,33601,33602],{},"let (u, o) = try await (user, orders)\n",[186,33604,33606],{"className":6838,"code":33605,"language":6840,"meta":191,"style":191},"\u002F\u002F Kotlin — Coroutines\nsuspend fun fetchUser(): User {\n    val response = apiService.getUser()\n    return response.body()!!\n}\n\u002F\u002F 并行\ncoroutineScope {\n    val user = async { fetchUser() }\n    val orders = async { fetchOrders() }\n    val (u, o) = user.await() to orders.await()\n}\n",[136,33607,33608,33613,33618,33623,33628,33632,33636,33641,33645,33650,33655],{"__ignoreMap":191},[195,33609,33610],{"class":197,"line":198},[195,33611,33612],{},"\u002F\u002F Kotlin — Coroutines\n",[195,33614,33615],{"class":197,"line":230},[195,33616,33617],{},"suspend fun fetchUser(): User {\n",[195,33619,33620],{"class":197,"line":251},[195,33621,33622],{},"    val response = apiService.getUser()\n",[195,33624,33625],{"class":197,"line":272},[195,33626,33627],{},"    return response.body()!!\n",[195,33629,33630],{"class":197,"line":293},[195,33631,552],{},[195,33633,33634],{"class":197,"line":562},[195,33635,33551],{},[195,33637,33638],{"class":197,"line":583},[195,33639,33640],{},"coroutineScope {\n",[195,33642,33643],{"class":197,"line":962},[195,33644,9545],{},[195,33646,33647],{"class":197,"line":968},[195,33648,33649],{},"    val orders = async { fetchOrders() }\n",[195,33651,33652],{"class":197,"line":1274},[195,33653,33654],{},"    val (u, o) = user.await() to orders.await()\n",[195,33656,33657],{"class":197,"line":1282},[195,33658,552],{},[186,33660,33662],{"className":383,"code":33661,"language":385,"meta":191,"style":191},"\u002F\u002F TypeScript — Promise + async\u002Fawait\nasync function fetchUser(): Promise\u003CUser> {\n  const res = await axios.get('\u002Fuser');\n  return res.data;\n}\n\u002F\u002F 并行\nconst [user, orders] = await Promise.all([fetchUser(), fetchOrders()]);\n",[136,33663,33664,33669,33695,33717,33724,33728,33732],{"__ignoreMap":191},[195,33665,33666],{"class":197,"line":198},[195,33667,33668],{"class":415},"\u002F\u002F TypeScript — Promise + async\u002Fawait\n",[195,33670,33671,33674,33676,33679,33682,33684,33687,33689,33692],{"class":197,"line":230},[195,33672,33673],{"class":223},"async",[195,33675,18893],{"class":223},[195,33677,33678],{"class":201}," fetchUser",[195,33680,33681],{"class":209},"()",[195,33683,638],{"class":223},[195,33685,33686],{"class":201}," Promise",[195,33688,448],{"class":209},[195,33690,33691],{"class":201},"User",[195,33693,33694],{"class":209},"> {\n",[195,33696,33697,33699,33702,33704,33706,33708,33710,33712,33715],{"class":197,"line":251},[195,33698,18263],{"class":223},[195,33700,33701],{"class":213}," res",[195,33703,398],{"class":223},[195,33705,18982],{"class":223},[195,33707,18985],{"class":209},[195,33709,18988],{"class":201},[195,33711,404],{"class":209},[195,33713,33714],{"class":459},"'\u002Fuser'",[195,33716,527],{"class":209},[195,33718,33719,33721],{"class":197,"line":272},[195,33720,18296],{"class":223},[195,33722,33723],{"class":209}," res.data;\n",[195,33725,33726],{"class":197,"line":293},[195,33727,552],{"class":209},[195,33729,33730],{"class":197,"line":562},[195,33731,33551],{"class":415},[195,33733,33734,33736,33739,33742,33744,33747,33750,33752,33754,33756,33758,33761,33764,33767,33770,33773],{"class":197,"line":583},[195,33735,392],{"class":223},[195,33737,33738],{"class":209}," [",[195,33740,33741],{"class":213},"user",[195,33743,301],{"class":209},[195,33745,33746],{"class":213},"orders",[195,33748,33749],{"class":209},"] ",[195,33751,424],{"class":223},[195,33753,18982],{"class":223},[195,33755,33686],{"class":213},[195,33757,16407],{"class":209},[195,33759,33760],{"class":201},"all",[195,33762,33763],{"class":209},"([",[195,33765,33766],{"class":201},"fetchUser",[195,33768,33769],{"class":209},"(), ",[195,33771,33772],{"class":201},"fetchOrders",[195,33774,33775],{"class":209},"()]);\n",[11,33777,33778,33782],{},[14,33779,29042,33780,333],{},[125,33781,29045],{},[129,33783,33784,33793,33801,33808],{},[132,33785,33786,33787,33789,33790,33792],{},"Dart 和 JS 都是",[125,33788,33413],{},"，",[136,33791,5387],{}," 不会创建新线程，只是将回调注册到 Event Loop（微任务队列）",[132,33794,29700,33795,33797,33798,33800],{},[136,33796,5387],{}," 是真正的",[125,33799,33419],{},"并发，编译器在 await 点做线程挂起\u002F恢复",[132,33802,33803,33804,33807],{},"Kotlin Coroutines 是基于 CPS (Continuation Passing Style) 的编译器变换，",[136,33805,33806],{},"suspend"," 函数被编译为状态机",[132,33809,33810],{},"Dart Isolate 之间不能共享内存，通信通过 SendPort\u002FReceivePort（类似 Actor 模型）",[11,33812,33813,33817],{},[14,33814,29055,33815,333],{},[125,33816,29058],{},[129,33818,33819,33828,33835,33842,33854],{},[132,33820,33821,33822,33824,33825,33827],{},"Dart: ",[136,33823,23759],{}," 一创建就开始执行，不是 lazy 的（Kotlin 的 ",[136,33826,33806],{}," 是 lazy 的）",[132,33829,33830,33831,33834],{},"Swift: ",[136,33832,33833],{},"Task {}"," 创建的非结构化任务不会随父 Task 取消而自动取消",[132,33836,33837,33838,33841],{},"Kotlin: 在 ",[136,33839,33840],{},"GlobalScope.launch"," 中启动协程不受生命周期管理，容易内存泄漏",[132,33843,32855,33844,33846,33847,33849,33850,33853],{},[136,33845,23804],{}," 不可取消，需要 ",[136,33848,32529],{}," 配合 ",[136,33851,33852],{},"fetch"," 实现取消",[132,33855,33856,333,33859,33861,33862,33865],{},[125,33857,33858],{},"通用陷阱",[136,33860,33673],{}," 函数中忘记 ",[136,33863,33864],{},"await"," 导致异步操作\"射后不管\"（fire-and-forget）",[11,33867,33868,33872],{},[14,33869,29091,33870,333],{},[125,33871,29094],{},[129,33873,33874,33885,33888,33897],{},[132,33875,33876,33877,33880,33881,33884],{},"Dart 的 Event Loop 机制？（微任务队列优先于事件队列，",[136,33878,33879],{},"Future.microtask"," > ",[136,33882,33883],{},"Timer"," > I\u002FO 回调）",[132,33886,33887],{},"Swift Actor 解决了什么问题？（数据竞争：Actor 保证同一时间只有一个任务访问其可变状态）",[132,33889,29709,33890,1437,33893,33896],{},[136,33891,33892],{},"CoroutineScope",[136,33894,33895],{},"supervisorScope"," 的区别？（前者一个子协程失败全部取消，后者互不影响）",[132,33898,33899],{},"JS 的微任务 (microtask) 和宏任务 (macrotask) 的执行顺序？（同步代码 → 微任务队列清空 → 一个宏任务 → 微任务队列清空 → ...）",[1890,33901],{},[32,33903,33905],{"id":33904},"_83-响应式流对比","8.3 响应式流对比",[36,33907,33908,33926],{},[39,33909,33910],{},[42,33911,33912,33914,33917,33920,33923],{},[45,33913,29123],{},[45,33915,33916],{},"Dart Stream",[45,33918,33919],{},"Swift AsyncSequence",[45,33921,33922],{},"Kotlin Flow",[45,33924,33925],{},"Vue (RxJS\u002Fwatch)",[52,33927,33928,33952,33979,34012,34036],{},[42,33929,33930,33933,33938,33943,33948],{},[57,33931,33932],{},"冷流（lazy）",[57,33934,33935],{},[136,33936,33937],{},"Stream.fromIterable",[57,33939,33940],{},[136,33941,33942],{},"AsyncStream",[57,33944,33945],{},[136,33946,33947],{},"flow {}",[57,33949,33950],{},[136,33951,30965],{},[42,33953,33954,33957,33962,33967,33974],{},[57,33955,33956],{},"热流（shared）",[57,33958,33959],{},[136,33960,33961],{},"StreamController.broadcast",[57,33963,33964],{},[136,33965,33966],{},"AsyncChannel",[57,33968,33969,16122,33972],{},[136,33970,33971],{},"SharedFlow",[136,33973,9051],{},[57,33975,33976,33978],{},[136,33977,16026],{}," \u002F Pinia state",[42,33980,33981,33983,33992,33998,34007],{},[57,33982,9094],{},[57,33984,33985,1547,33987,1547,33989],{},[136,33986,29522],{},[136,33988,29544],{},[136,33990,33991],{},".transform()",[57,33993,33994,1547,33996],{},[136,33995,29522],{},[136,33997,29549],{},[57,33999,34000,1547,34002,1547,34004],{},[136,34001,29522],{},[136,34003,29549],{},[136,34005,34006],{},".collect()",[57,34008,34009,34010],{},"RxJS 全套 \u002F ",[136,34011,16536],{},[42,34013,34014,34017,34022,34025,34033],{},[57,34015,34016],{},"背压",[57,34018,34019,34021],{},[136,34020,23765],{}," 支持 pause\u002Fresume",[57,34023,34024],{},"内置",[57,34026,34027,16122,34030],{},[136,34028,34029],{},"buffer()",[136,34031,34032],{},"conflate()",[57,34034,34035],{},"无（单线程无需）",[42,34037,34038,34041,34046,34051,34056],{},[57,34039,34040],{},"取消",[57,34042,34043],{},[136,34044,34045],{},"StreamSubscription.cancel()",[57,34047,34048],{},[136,34049,34050],{},"Task.cancel()",[57,34052,34053],{},[136,34054,34055],{},"Job.cancel()",[57,34057,34058,34060],{},[136,34059,30965],{}," 返回 stop 函数",[1890,34062],{},[18,34064,34066],{"id":34065},"_9-渲染机制与性能优化","9. 渲染机制与性能优化",[32,34068,34070],{"id":34069},"_91-渲染管线对比","9.1 渲染管线对比",[36,34072,34073,34087],{},[39,34074,34075],{},[42,34076,34077,34079,34081,34083,34085],{},[45,34078,6273],{},[45,34080,13135],{},[45,34082,4051],{},[45,34084,15186],{},[45,34086,15468],{},[52,34088,34089,34106,34123,34139,34156],{},[42,34090,34091,34094,34097,34100,34103],{},[57,34092,34093],{},"UI 描述",[57,34095,34096],{},"Widget 树",[57,34098,34099],{},"View 树",[57,34101,34102],{},"@Composable 树",[57,34104,34105],{},"Virtual DOM",[42,34107,34108,34111,34114,34117,34120],{},[57,34109,34110],{},"Diff\u002FReconcile",[57,34112,34113],{},"Element 树 canUpdate",[57,34115,34116],{},"View body diff",[57,34118,34119],{},"Slot Table + positional key",[57,34121,34122],{},"VNode diff",[42,34124,34125,34127,34130,34133,34136],{},[57,34126,4080],{},[57,34128,34129],{},"RenderObject layout",[57,34131,34132],{},"Layout Protocol",[57,34134,34135],{},"Measure + Place",[57,34137,34138],{},"CSS 引擎 (Blink\u002FWebKit)",[42,34140,34141,34144,34147,34150,34153],{},[57,34142,34143],{},"绘制",[57,34145,34146],{},"RenderObject paint → Skia\u002FImpeller",[57,34148,34149],{},"Core Animation layer",[57,34151,34152],{},"Canvas + RenderNode",[57,34154,34155],{},"DOM 操作 → 合成层绘制",[42,34157,34158,34161,34164,34166,34168],{},[57,34159,34160],{},"帧率目标",[57,34162,34163],{},"60\u002F120fps",[57,34165,34163],{},[57,34167,34163],{},[57,34169,34170],{},"60fps",[32,34172,34174],{"id":34173},"_92-三棵树-vs-其他机制","9.2 三棵树 vs 其他机制",[186,34176,34179],{"className":34177,"code":34178,"language":1074},[1072],"Flutter 三棵树:\n  Widget 树  ──build()──→  Element 树  ──createRenderObject()──→  RenderObject 树\n  (配置\u002F蓝图)              (生命周期管理)                        (布局\u002F绘制)\n\nSwiftUI:\n  View struct  ──body──→  View Graph (AttributeGraph)  ──→  Core Animation Layer\n  (声明\u002F配置)              (内部状态图)                       (渲染)\n\nCompose:\n  @Composable  ──composition──→  Slot Table  ──→  LayoutNode  ──→  RenderNode\n  (函数)                         (线性存储)       (布局)          (绘制)\n\nVue:\n  Template  ──compile──→  Render Function  ──exec──→  VNode 树  ──diff\u002Fpatch──→  真实 DOM\n  (模板)                  (渲染函数)                  (虚拟DOM)                  (浏览器DOM)\n",[136,34180,34178],{"__ignoreMap":191},[11,34182,34183,34187],{},[14,34184,29042,34185,333],{},[125,34186,29045],{},[129,34188,34189,34196,34199],{},[132,34190,34191,34192,34195],{},"Flutter: Widget→Element 的对应关系由 ",[136,34193,34194],{},"canUpdate()"," 决定（runtimeType + key 相同则更新，否则新建）",[132,34197,34198],{},"Compose: Slot Table 是线性数组，利用 Gap Buffer 高效插入\u002F删除，比树结构更快",[132,34200,34201],{},"Vue: 编译器在编译阶段就能标记静态节点 (static hoisting)、Patch Flags，跳过不需要 diff 的部分",[11,34203,34204,34208],{},[14,34205,29091,34206,333],{},[125,34207,29094],{},[129,34209,34210,34215,34218],{},[132,34211,31860,34212,34214],{},[136,34213,19622],{}," 的作用？（帮助 Element 树正确匹配 Widget，用于列表重排、动画 Hero 等场景）",[132,34216,34217],{},"GlobalKey vs LocalKey 的区别？（GlobalKey 全局唯一可跨子树访问 State，LocalKey 在同级中唯一）",[132,34219,34220],{},"Vue 的 Virtual DOM diff 算法复杂度？（O(n) 同层比较，不跨层）",[1890,34222],{},[32,34224,34226],{"id":34225},"_93-性能优化手段","9.3 性能优化手段",[36,34228,34229,34243],{},[39,34230,34231],{},[42,34232,34233,34235,34237,34239,34241],{},[45,34234,20942],{},[45,34236,13135],{},[45,34238,4051],{},[45,34240,15186],{},[45,34242,15468],{},[52,34244,34245,34278,34302,34321,34345,34362],{},[42,34246,34247,34250,34255,34261,34268],{},[57,34248,34249],{},"减少重建",[57,34251,34252,34254],{},[136,34253,392],{}," Widget、合理拆分 Widget",[57,34256,34257,34260],{},[136,34258,34259],{},"EquatableView","、提取子 View",[57,34262,34263,34265,34266],{},[136,34264,11601],{}," 稳定、",[136,34267,31212],{},[57,34269,34270,997,34273,997,34275],{},[136,34271,34272],{},"v-once",[136,34274,19538],{},[136,34276,34277],{},"shallowRef",[42,34279,34280,34283,34288,34295,34299],{},[57,34281,34282],{},"列表优化",[57,34284,34285,34287],{},[136,34286,30824],{}," (懒加载)",[57,34289,34290,16122,34293],{},[136,34291,34292],{},"LazyVStack",[136,34294,31753],{},[57,34296,34297],{},[136,34298,30844],{},[57,34300,34301],{},"虚拟列表 (vue-virtual-scroller)",[42,34303,34304,34307,34312,34315,34318],{},[57,34305,34306],{},"图片优化",[57,34308,34309,34311],{},[136,34310,14552],{}," + 尺寸限制",[57,34313,34314],{},"AsyncImage + 缓存",[57,34316,34317],{},"Coil (异步+缓存)",[57,34319,34320],{},"懒加载 + srcset + WebP",[42,34322,34323,34325,34331,34337,34342],{},[57,34324,4102],{},[57,34326,34327,34330],{},[136,34328,34329],{},"AnimationController"," 60fps 独立",[57,34332,34333,34336],{},[136,34334,34335],{},"withAnimation"," 隐式动画",[57,34338,34339],{},[136,34340,34341],{},"animate*AsState",[57,34343,34344],{},"CSS Transition \u002F GSAP",[42,34346,34347,34350,34353,34356,34359],{},[57,34348,34349],{},"分析工具",[57,34351,34352],{},"DevTools (Timeline\u002FWidget Inspector)",[57,34354,34355],{},"Instruments (Time Profiler)",[57,34357,34358],{},"Layout Inspector \u002F Profiler",[57,34360,34361],{},"Chrome DevTools (Performance)",[42,34363,34364,34367,34370,34373,34376],{},[57,34365,34366],{},"内存分析",[57,34368,34369],{},"DevTools Memory 视图",[57,34371,34372],{},"Instruments (Allocations\u002FLeaks)",[57,34374,34375],{},"Android Profiler",[57,34377,34378],{},"Chrome Memory Snapshot",[11,34380,34381,34385],{},[14,34382,29055,34383,333],{},[125,34384,29058],{},[129,34386,34387,34393,34399,34406,34413,34421],{},[132,34388,34389,34390,34392],{},"Flutter: 在 ",[136,34391,11581],{}," 中创建大对象（如 AnimationController）导致每帧重复创建",[132,34394,34395,34396,34398],{},"Flutter: 不加 ",[136,34397,392],{}," 导致 Widget 无法复用，整棵子树每帧重建",[132,34400,34401,34402,34405],{},"SwiftUI: 在 ",[136,34403,34404],{},"body"," 中做耗时计算（body 可能频繁调用）",[132,34407,31414,34408,30845,34410,34412],{},[136,34409,31417],{},[136,34411,31212],{}," 之外创建对象，每次 recomposition 重新创建",[132,34414,30643,34415,34417,34418,34420],{},[136,34416,30815],{}," 不加 ",[136,34419,11601],{}," 或用 index 做 key 导致列表状态错乱",[132,34422,30643,34423,34425],{},[136,34424,19538],{}," 依赖了不需要追踪的变量导致不必要的重算",[1890,34427],{},[18,34429,34431],{"id":34430},"_10-平台通信与原生能力","10. 平台通信与原生能力",[32,34433,34435],{"id":34434},"_101-通信机制对比","10.1 通信机制对比",[36,34437,34438,34455],{},[39,34439,34440],{},[42,34441,34442,34444,34446,34449,34452],{},[45,34443,15463],{},[45,34445,13135],{},[45,34447,34448],{},"SwiftUI \u002F UIKit",[45,34450,34451],{},"Compose \u002F Android",[45,34453,34454],{},"Vue \u002F Web",[52,34456,34457,34476,34492,34511],{},[42,34458,34459,34462,34467,34470,34473],{},[57,34460,34461],{},"调用原生",[57,34463,34464,34466],{},[125,34465,24754],{}," \u002F EventChannel",[57,34468,34469],{},"直接调用 Framework",[57,34471,34472],{},"直接调用 Android SDK",[57,34474,34475],{},"Web API \u002F JS Bridge",[42,34477,34478,34481,34484,34487,34489],{},[57,34479,34480],{},"通信协议",[57,34482,34483],{},"异步消息传递（JSON\u002F二进制）",[57,34485,34486],{},"直接函数调用",[57,34488,34486],{},[57,34490,34491],{},"postMessage \u002F Bridge",[42,34493,34494,34497,34503,34506,34508],{},[57,34495,34496],{},"原生 UI 嵌入",[57,34498,34499,34502],{},[136,34500,34501],{},"PlatformView"," (性能开销大)",[57,34504,34505],{},"直接使用",[57,34507,34505],{},[57,34509,34510],{},"iframe \u002F Web Component",[42,34512,34513,34516,34519,34522,34525],{},[57,34514,34515],{},"插件机制",[57,34517,34518],{},"pub.dev 插件 (Dart + 原生)",[57,34520,34521],{},"Swift Package \u002F Framework",[57,34523,34524],{},"Gradle 依赖",[57,34526,34527],{},"npm 包",[32,34529,34531],{"id":34530},"_102-flutter-platform-channel","10.2 Flutter Platform Channel",[186,34533,34536],{"className":34534,"code":34535,"language":1074},[1072],"Flutter (Dart)                     Native (Swift\u002FKotlin)\n     │                                     │\n     │  MethodChannel('com.app\u002Fbattery')   │\n     │  ──── invokeMethod('level') ────→   │\n     │                                     │  处理请求\n     │  ←──── result.success(85) ────────  │\n     │                                     │\n     │  EventChannel('com.app\u002Fevents')     │\n     │  ──── listen() ────→               │\n     │  ←──── stream of events ────────   │\n",[136,34537,34535],{"__ignoreMap":191},[186,34539,34541],{"className":11162,"code":34540,"language":11164,"meta":191,"style":191},"\u002F\u002F Dart 端\nconst channel = MethodChannel('com.app\u002Fbattery');\nfinal level = await channel.invokeMethod\u003Cint>('getBatteryLevel');\n",[136,34542,34543,34547,34552],{"__ignoreMap":191},[195,34544,34545],{"class":197,"line":198},[195,34546,24801],{},[195,34548,34549],{"class":197,"line":230},[195,34550,34551],{},"const channel = MethodChannel('com.app\u002Fbattery');\n",[195,34553,34554],{"class":197,"line":251},[195,34555,34556],{},"final level = await channel.invokeMethod\u003Cint>('getBatteryLevel');\n",[186,34558,34560],{"className":2177,"code":34559,"language":2179,"meta":191,"style":191},"\u002F\u002F Swift (iOS) 端\nlet channel = FlutterMethodChannel(name: \"com.app\u002Fbattery\", binaryMessenger: messenger)\nchannel.setMethodCallHandler { call, result in\n    if call.method == \"getBatteryLevel\" {\n        result(UIDevice.current.batteryLevel * 100)\n    }\n}\n",[136,34561,34562,34567,34572,34577,34582,34587,34591],{"__ignoreMap":191},[195,34563,34564],{"class":197,"line":198},[195,34565,34566],{},"\u002F\u002F Swift (iOS) 端\n",[195,34568,34569],{"class":197,"line":230},[195,34570,34571],{},"let channel = FlutterMethodChannel(name: \"com.app\u002Fbattery\", binaryMessenger: messenger)\n",[195,34573,34574],{"class":197,"line":251},[195,34575,34576],{},"channel.setMethodCallHandler { call, result in\n",[195,34578,34579],{"class":197,"line":272},[195,34580,34581],{},"    if call.method == \"getBatteryLevel\" {\n",[195,34583,34584],{"class":197,"line":293},[195,34585,34586],{},"        result(UIDevice.current.batteryLevel * 100)\n",[195,34588,34589],{"class":197,"line":562},[195,34590,2403],{},[195,34592,34593],{"class":197,"line":583},[195,34594,552],{},[186,34596,34598],{"className":6838,"code":34597,"language":6840,"meta":191,"style":191},"\u002F\u002F Kotlin (Android) 端\nval channel = MethodChannel(flutterEngine.dartExecutor, \"com.app\u002Fbattery\")\nchannel.setMethodCallHandler { call, result ->\n    if (call.method == \"getBatteryLevel\") {\n        val level = getBatteryLevel()\n        result.success(level)\n    }\n}\n",[136,34599,34600,34605,34610,34615,34620,34625,34630,34634],{"__ignoreMap":191},[195,34601,34602],{"class":197,"line":198},[195,34603,34604],{},"\u002F\u002F Kotlin (Android) 端\n",[195,34606,34607],{"class":197,"line":230},[195,34608,34609],{},"val channel = MethodChannel(flutterEngine.dartExecutor, \"com.app\u002Fbattery\")\n",[195,34611,34612],{"class":197,"line":251},[195,34613,34614],{},"channel.setMethodCallHandler { call, result ->\n",[195,34616,34617],{"class":197,"line":272},[195,34618,34619],{},"    if (call.method == \"getBatteryLevel\") {\n",[195,34621,34622],{"class":197,"line":293},[195,34623,34624],{},"        val level = getBatteryLevel()\n",[195,34626,34627],{"class":197,"line":562},[195,34628,34629],{},"        result.success(level)\n",[195,34631,34632],{"class":197,"line":583},[195,34633,2403],{},[195,34635,34636],{"class":197,"line":962},[195,34637,552],{},[11,34639,34640,34644],{},[14,34641,29042,34642,333],{},[125,34643,29045],{},[129,34645,34646,34652,34657],{},[132,34647,34648,34649,34651],{},"MethodChannel 是",[125,34650,28494],{},"的，数据需要序列化\u002F反序列化（StandardMessageCodec），大量数据传输有性能开销",[132,34653,34654,34656],{},[136,34655,34501],{},"（在 Flutter 中嵌入原生 View）有显著性能开销：Android 用 VirtualDisplay 或 Hybrid Composition，iOS 用 UIKitView",[132,34658,34659],{},"Pigeon 工具可以生成类型安全的 Platform Channel 代码，避免手写字符串匹配",[11,34661,34662,34666],{},[14,34663,29055,34664,333],{},[125,34665,29058],{},[129,34667,34668,34671,34674],{},[132,34669,34670],{},"Channel 名称拼写不一致导致调用无响应",[132,34672,34673],{},"在非主线程调用 MethodChannel 导致崩溃（Flutter engine 要求主线程通信）",[132,34675,34676,34677,34680],{},"忘记处理 ",[136,34678,34679],{},"FlutterMethodNotImplemented"," 错误",[11,34682,34683,34687],{},[14,34684,29091,34685,333],{},[125,34686,29094],{},[129,34688,34689,34692,34695],{},[132,34690,34691],{},"MethodChannel vs EventChannel vs BasicMessageChannel 的区别？（Method: 一次性调用，Event: 持续数据流，Basic: 自定义编解码）",[132,34693,34694],{},"Flutter 如何嵌入原生 View？性能问题如何优化？（PlatformView，尽量用 Texture 替代）",[132,34696,34697],{},"Flutter 的 FFI (dart:ffi) 和 Platform Channel 的区别？（FFI 是直接调用 C 函数，同步，无序列化开销）",[1890,34699],{},[18,34701,34703],{"id":34702},"_11-架构模式","11. 架构模式",[32,34705,34707],{"id":34706},"_111-mvvm-在四平台的实现","11.1 MVVM 在四平台的实现",[36,34709,34710,34724],{},[39,34711,34712],{},[42,34713,34714,34716,34718,34720,34722],{},[45,34715,19812],{},[45,34717,13135],{},[45,34719,4051],{},[45,34721,15186],{},[45,34723,15468],{},[52,34725,34726,34743,34759,34776],{},[42,34727,34728,34731,34734,34737,34739],{},[57,34729,34730],{},"View",[57,34732,34733],{},"Widget (build)",[57,34735,34736],{},"View (body)",[57,34738,31417],{},[57,34740,34741],{},[136,34742,15742],{},[42,34744,34745,34748,34751,34753,34756],{},[57,34746,34747],{},"ViewModel",[57,34749,34750],{},"ChangeNotifier \u002F GetxController \u002F BLoC",[57,34752,4356],{},[57,34754,34755],{},"ViewModel (AAC)",[57,34757,34758],{},"Composition API (setup)",[42,34760,34761,34764,34767,34770,34773],{},[57,34762,34763],{},"Model",[57,34765,34766],{},"Dart class \u002F freezed",[57,34768,34769],{},"struct + Codable",[57,34771,34772],{},"data class + Room Entity",[57,34774,34775],{},"TypeScript interface + API",[42,34777,34778,34781,34784,34787,34789],{},[57,34779,34780],{},"绑定方式",[57,34782,34783],{},"Provider \u002F Obx \u002F BlocBuilder",[57,34785,34786],{},"@StateObject + @Published",[57,34788,33312],{},[57,34790,34791],{},"ref + template 自动绑定",[186,34793,34795],{"className":11162,"code":34794,"language":11164,"meta":191,"style":191},"\u002F\u002F Flutter MVVM — ChangeNotifier + Provider\nclass UserViewModel extends ChangeNotifier {\n  User? _user;\n  User? get user => _user;\n  \n  Future\u003Cvoid> loadUser() async {\n    _user = await userRepository.getUser();\n    notifyListeners();\n  }\n}\n\n\u002F\u002F View\nConsumer\u003CUserViewModel>(\n  builder: (_, vm, __) => Text(vm.user?.name ?? 'Loading'),\n)\n",[136,34796,34797,34802,34807,34812,34817,34821,34826,34831,34835,34839,34843,34847,34851,34856,34861],{"__ignoreMap":191},[195,34798,34799],{"class":197,"line":198},[195,34800,34801],{},"\u002F\u002F Flutter MVVM — ChangeNotifier + Provider\n",[195,34803,34804],{"class":197,"line":230},[195,34805,34806],{},"class UserViewModel extends ChangeNotifier {\n",[195,34808,34809],{"class":197,"line":251},[195,34810,34811],{},"  User? _user;\n",[195,34813,34814],{"class":197,"line":272},[195,34815,34816],{},"  User? get user => _user;\n",[195,34818,34819],{"class":197,"line":293},[195,34820,19967],{},[195,34822,34823],{"class":197,"line":562},[195,34824,34825],{},"  Future\u003Cvoid> loadUser() async {\n",[195,34827,34828],{"class":197,"line":583},[195,34829,34830],{},"    _user = await userRepository.getUser();\n",[195,34832,34833],{"class":197,"line":962},[195,34834,26216],{},[195,34836,34837],{"class":197,"line":968},[195,34838,965],{},[195,34840,34841],{"class":197,"line":1274},[195,34842,552],{},[195,34844,34845],{"class":197,"line":1282},[195,34846,1241],{"emptyLinePlaceholder":757},[195,34848,34849],{"class":197,"line":1295},[195,34850,5802],{},[195,34852,34853],{"class":197,"line":1309},[195,34854,34855],{},"Consumer\u003CUserViewModel>(\n",[195,34857,34858],{"class":197,"line":2246},[195,34859,34860],{},"  builder: (_, vm, __) => Text(vm.user?.name ?? 'Loading'),\n",[195,34862,34863],{"class":197,"line":1996},[195,34864,410],{},[186,34866,34868],{"className":2177,"code":34867,"language":2179,"meta":191,"style":191},"\u002F\u002F SwiftUI MVVM — ObservableObject\n@Observable class UserViewModel {\n    var user: User?\n    \n    func loadUser() async {\n        user = try? await userRepository.getUser()\n    }\n}\n\n\u002F\u002F View\nstruct UserView: View {\n    @State var vm = UserViewModel()\n    var body: some View {\n        Text(vm.user?.name ?? \"Loading\")\n            .task { await vm.loadUser() }\n    }\n}\n",[136,34869,34870,34875,34880,34885,34889,34894,34899,34903,34907,34911,34915,34919,34924,34928,34933,34938,34942],{"__ignoreMap":191},[195,34871,34872],{"class":197,"line":198},[195,34873,34874],{},"\u002F\u002F SwiftUI MVVM — ObservableObject\n",[195,34876,34877],{"class":197,"line":230},[195,34878,34879],{},"@Observable class UserViewModel {\n",[195,34881,34882],{"class":197,"line":251},[195,34883,34884],{},"    var user: User?\n",[195,34886,34887],{"class":197,"line":272},[195,34888,20020],{},[195,34890,34891],{"class":197,"line":293},[195,34892,34893],{},"    func loadUser() async {\n",[195,34895,34896],{"class":197,"line":562},[195,34897,34898],{},"        user = try? await userRepository.getUser()\n",[195,34900,34901],{"class":197,"line":583},[195,34902,2403],{},[195,34904,34905],{"class":197,"line":962},[195,34906,552],{},[195,34908,34909],{"class":197,"line":968},[195,34910,1241],{"emptyLinePlaceholder":757},[195,34912,34913],{"class":197,"line":1274},[195,34914,5802],{},[195,34916,34917],{"class":197,"line":1282},[195,34918,5597],{},[195,34920,34921],{"class":197,"line":1295},[195,34922,34923],{},"    @State var vm = UserViewModel()\n",[195,34925,34926],{"class":197,"line":1309},[195,34927,3985],{},[195,34929,34930],{"class":197,"line":2246},[195,34931,34932],{},"        Text(vm.user?.name ?? \"Loading\")\n",[195,34934,34935],{"class":197,"line":1996},[195,34936,34937],{},"            .task { await vm.loadUser() }\n",[195,34939,34940],{"class":197,"line":2257},[195,34941,2403],{},[195,34943,34944],{"class":197,"line":2262},[195,34945,552],{},[186,34947,34949],{"className":6838,"code":34948,"language":6840,"meta":191,"style":191},"\u002F\u002F Compose MVVM — ViewModel + StateFlow\nclass UserViewModel @Inject constructor(\n    private val repo: UserRepository\n) : ViewModel() {\n    private val _user = MutableStateFlow\u003CUser?>(null)\n    val user = _user.asStateFlow()\n    \n    fun loadUser() { viewModelScope.launch { _user.value = repo.getUser() } }\n}\n\n\u002F\u002F Composable\n@Composable\nfun UserScreen(vm: UserViewModel = hiltViewModel()) {\n    val user by vm.user.collectAsState()\n    Text(user?.name ?: \"Loading\")\n}\n",[136,34950,34951,34956,34960,34965,34970,34975,34980,34984,34989,34993,34997,35002,35006,35011,35016,35021],{"__ignoreMap":191},[195,34952,34953],{"class":197,"line":198},[195,34954,34955],{},"\u002F\u002F Compose MVVM — ViewModel + StateFlow\n",[195,34957,34958],{"class":197,"line":230},[195,34959,10304],{},[195,34961,34962],{"class":197,"line":251},[195,34963,34964],{},"    private val repo: UserRepository\n",[195,34966,34967],{"class":197,"line":272},[195,34968,34969],{},") : ViewModel() {\n",[195,34971,34972],{"class":197,"line":293},[195,34973,34974],{},"    private val _user = MutableStateFlow\u003CUser?>(null)\n",[195,34976,34977],{"class":197,"line":562},[195,34978,34979],{},"    val user = _user.asStateFlow()\n",[195,34981,34982],{"class":197,"line":583},[195,34983,20020],{},[195,34985,34986],{"class":197,"line":962},[195,34987,34988],{},"    fun loadUser() { viewModelScope.launch { _user.value = repo.getUser() } }\n",[195,34990,34991],{"class":197,"line":968},[195,34992,552],{},[195,34994,34995],{"class":197,"line":1274},[195,34996,1241],{"emptyLinePlaceholder":757},[195,34998,34999],{"class":197,"line":1282},[195,35000,35001],{},"\u002F\u002F Composable\n",[195,35003,35004],{"class":197,"line":1295},[195,35005,8477],{},[195,35007,35008],{"class":197,"line":1309},[195,35009,35010],{},"fun UserScreen(vm: UserViewModel = hiltViewModel()) {\n",[195,35012,35013],{"class":197,"line":2246},[195,35014,35015],{},"    val user by vm.user.collectAsState()\n",[195,35017,35018],{"class":197,"line":1996},[195,35019,35020],{},"    Text(user?.name ?: \"Loading\")\n",[195,35022,35023],{"class":197,"line":2257},[195,35024,552],{},[186,35026,35028],{"className":15640,"code":35027,"language":15642,"meta":191,"style":191},"\u003C!-- Vue MVVM — Composition API -->\n\u003Cscript setup lang=\"ts\">\nimport { ref, onMounted } from 'vue'\nimport { getUser } from '@\u002Fapi\u002Fuser'\n\nconst user = ref\u003CUser | null>(null)\nonMounted(async () => { user.value = await getUser() })\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cspan>{{ user?.name ?? 'Loading' }}\u003C\u002Fspan>\n\u003C\u002Ftemplate>\n",[136,35029,35030,35035,35051,35062,35074,35078,35104,35130,35138,35142,35150,35163],{"__ignoreMap":191},[195,35031,35032],{"class":197,"line":198},[195,35033,35034],{"class":415},"\u003C!-- Vue MVVM — Composition API -->\n",[195,35036,35037,35039,35041,35043,35045,35047,35049],{"class":197,"line":230},[195,35038,448],{"class":209},[195,35040,15687],{"class":205},[195,35042,15690],{"class":201},[195,35044,15693],{"class":201},[195,35046,424],{"class":209},[195,35048,15698],{"class":459},[195,35050,477],{"class":209},[195,35052,35053,35055,35058,35060],{"class":197,"line":251},[195,35054,15961],{"class":223},[195,35056,35057],{"class":209}," { ref, onMounted } ",[195,35059,15967],{"class":223},[195,35061,15970],{"class":459},[195,35063,35064,35066,35069,35071],{"class":197,"line":272},[195,35065,15961],{"class":223},[195,35067,35068],{"class":209}," { getUser } ",[195,35070,15967],{"class":223},[195,35072,35073],{"class":459}," '@\u002Fapi\u002Fuser'\n",[195,35075,35076],{"class":197,"line":293},[195,35077,1241],{"emptyLinePlaceholder":757},[195,35079,35080,35082,35084,35086,35088,35090,35092,35094,35096,35099,35102],{"class":197,"line":562},[195,35081,392],{"class":223},[195,35083,16235],{"class":213},[195,35085,398],{"class":223},[195,35087,401],{"class":201},[195,35089,448],{"class":209},[195,35091,33691],{"class":201},[195,35093,28961],{"class":223},[195,35095,28964],{"class":213},[195,35097,35098],{"class":209},">(",[195,35100,35101],{"class":213},"null",[195,35103,410],{"class":209},[195,35105,35106,35108,35110,35112,35115,35117,35120,35122,35124,35127],{"class":197,"line":583},[195,35107,16140],{"class":201},[195,35109,404],{"class":209},[195,35111,33673],{"class":223},[195,35113,35114],{"class":209}," () ",[195,35116,16398],{"class":223},[195,35118,35119],{"class":209}," { user.value ",[195,35121,424],{"class":223},[195,35123,18982],{"class":223},[195,35125,35126],{"class":201}," getUser",[195,35128,35129],{"class":209},"() })\n",[195,35131,35132,35134,35136],{"class":197,"line":962},[195,35133,15672],{"class":209},[195,35135,15687],{"class":205},[195,35137,477],{"class":209},[195,35139,35140],{"class":197,"line":968},[195,35141,1241],{"emptyLinePlaceholder":757},[195,35143,35144,35146,35148],{"class":197,"line":1274},[195,35145,448],{"class":209},[195,35147,15651],{"class":205},[195,35149,477],{"class":209},[195,35151,35152,35154,35156,35159,35161],{"class":197,"line":1282},[195,35153,15658],{"class":209},[195,35155,195],{"class":205},[195,35157,35158],{"class":209},">{{ user?.name ?? 'Loading' }}\u003C\u002F",[195,35160,195],{"class":205},[195,35162,477],{"class":209},[195,35164,35165,35167,35169],{"class":197,"line":1295},[195,35166,15672],{"class":209},[195,35168,15651],{"class":205},[195,35170,477],{"class":209},[1890,35172],{},[32,35174,35176],{"id":35175},"_112-依赖注入对比","11.2 依赖注入对比",[36,35178,35179,35193],{},[39,35180,35181],{},[42,35182,35183,35185,35187,35189,35191],{},[45,35184,13777],{},[45,35186,13135],{},[45,35188,13113],{},[45,35190,13124],{},[45,35192,15468],{},[52,35194,35195,35213,35229,35260],{},[42,35196,35197,35200,35202,35205,35211],{},[57,35198,35199],{},"框架级",[57,35201,31292],{},[57,35203,35204],{},"SwiftUI Environment",[57,35206,35207,35210],{},[125,35208,35209],{},"Hilt"," (Dagger)",[57,35212,31321],{},[42,35214,35215,35218,35221,35224,35227],{},[57,35216,35217],{},"第三方",[57,35219,35220],{},"GetIt \u002F get_it",[57,35222,35223],{},"Swinject \u002F Factory",[57,35225,35226],{},"Koin",[57,35228,16661],{},[42,35230,35231,35234,35240,35247,35255],{},[57,35232,35233],{},"注册方式",[57,35235,35236,35239],{},[136,35237,35238],{},"Provider\u003CT>()"," 在 Widget 树中",[57,35241,35242,16122,35244],{},[136,35243,3170],{},[136,35245,35246],{},"environmentObject",[57,35248,35249,4728,35252,33205],{},[136,35250,35251],{},"@Inject",[136,35253,35254],{},"@Module",[57,35256,35257],{},[136,35258,35259],{},"app.provide()",[42,35261,35262,35265,35268,35271,35274],{},[57,35263,35264],{},"作用域",[57,35266,35267],{},"Widget 子树",[57,35269,35270],{},"View 子树",[57,35272,35273],{},"Activity\u002FFragment\u002FViewModel scope",[57,35275,35276],{},"组件子树",[11,35278,35279,35283],{},[14,35280,29042,35281,333],{},[125,35282,29045],{},[129,35284,35285,35288,35291],{},[132,35286,35287],{},"Hilt 是编译时 DI（基于 Dagger 的注解处理），性能最好但配置最复杂",[132,35289,35290],{},"Riverpod 独立于 Widget 树（不依赖 BuildContext），可以在测试中轻松 override",[132,35292,30444,35293,35296],{},[136,35294,35295],{},"provide\u002Finject"," 不是响应式的（需要 provide ref 才能响应式）",[11,35298,35299,35303],{},[14,35300,29091,35301,333],{},[125,35302,29094],{},[129,35304,35305,35308,35311],{},[132,35306,35307],{},"Provider vs Riverpod 的核心区别？（Provider 依赖 BuildContext，Riverpod 不依赖；Riverpod 有编译时安全）",[132,35309,35310],{},"Hilt 的 Component 层级？（SingletonComponent → ActivityComponent → FragmentComponent → ViewModelComponent → ViewComponent）",[132,35312,35313],{},"Vue 的 provide\u002Finject 和 Pinia 的区别？（provide\u002Finject 是组件树级别的，Pinia 是全局单例 store）",[1890,35315],{},[32,35317,35319],{"id":35318},"_113-模块化方案对比","11.3 模块化方案对比",[36,35321,35322,35336],{},[39,35323,35324],{},[42,35325,35326,35328,35330,35332,35334],{},[45,35327,13777],{},[45,35329,13135],{},[45,35331,13113],{},[45,35333,13124],{},[45,35335,15468],{},[52,35337,35338,35354,35371],{},[42,35339,35340,35343,35346,35348,35351],{},[57,35341,35342],{},"代码组织",[57,35344,35345],{},"Package \u002F 文件夹约定",[57,35347,34521],{},[57,35349,35350],{},"Gradle Module",[57,35352,35353],{},"npm workspace \u002F monorepo",[42,35355,35356,35359,35362,35365,35368],{},[57,35357,35358],{},"动态加载",[57,35360,35361],{},"Deferred Components",[57,35363,35364],{},"Dynamic Framework",[57,35366,35367],{},"Dynamic Feature Module",[57,35369,35370],{},"路由级 lazy import",[42,35372,35373,35376,35379,35382,35385],{},[57,35374,35375],{},"路由解耦",[57,35377,35378],{},"路由表注册",[57,35380,35381],{},"Coordinator 模式",[57,35383,35384],{},"Navigation Component + Deep Link",[57,35386,35387],{},"Vue Router lazy loading",[1890,35389],{},[18,35391,35393],{"id":35392},"_12-构建测试与发布","12. 构建、测试与发布",[32,35395,35397],{"id":35396},"_121-构建工具对比","12.1 构建工具对比",[36,35399,35400,35414],{},[39,35401,35402],{},[42,35403,35404,35406,35408,35410,35412],{},[45,35405,15463],{},[45,35407,13135],{},[45,35409,13113],{},[45,35411,13124],{},[45,35413,15468],{},[52,35415,35416,35436,35450,35465,35481],{},[42,35417,35418,35420,35422,35425,35431],{},[57,35419,15553],{},[57,35421,15556],{},[57,35423,35424],{},"Xcode Build System",[57,35426,35427,35430],{},[125,35428,35429],{},"Gradle"," (Kotlin DSL\u002FGroovy)",[57,35432,35433,35435],{},[125,35434,15559],{}," \u002F Webpack",[42,35437,35438,35440,35442,35445,35448],{},[57,35439,15542],{},[57,35441,15545],{},[57,35443,35444],{},"SPM \u002F CocoaPods",[57,35446,35447],{},"Gradle Dependencies",[57,35449,15548],{},[42,35451,35452,35454,35456,35459,35462],{},[57,35453,33283],{},[57,35455,32805],{},[57,35457,35458],{},"Swift 宏",[57,35460,35461],{},"kapt \u002F KSP",[57,35463,35464],{},"Vite 插件",[42,35466,35467,35469,35472,35475,35478],{},[57,35468,19396],{},[57,35470,35471],{},"✅ Hot Reload (保留状态)",[57,35473,35474],{},"✅ Xcode Previews (SwiftUI)",[57,35476,35477],{},"❌ (仅 Apply Changes 部分支持)",[57,35479,35480],{},"✅ Vite HMR",[42,35482,35483,35486,35489,35492,35495],{},[57,35484,35485],{},"产物",[57,35487,35488],{},"APK \u002F AAB \u002F IPA",[57,35490,35491],{},"IPA",[57,35493,35494],{},"APK \u002F AAB",[57,35496,35497],{},"静态文件 (HTML\u002FJS\u002FCSS)",[32,35499,35501],{"id":35500},"_122-测试框架对比","12.2 测试框架对比",[36,35503,35504,35518],{},[39,35505,35506],{},[42,35507,35508,35510,35512,35514,35516],{},[45,35509,19812],{},[45,35511,13135],{},[45,35513,13113],{},[45,35515,13124],{},[45,35517,15468],{},[52,35519,35520,35538,35556,35574,35597],{},[42,35521,35522,35525,35529,35532,35535],{},[57,35523,35524],{},"单元测试",[57,35526,35527],{},[136,35528,25993],{},[57,35530,35531],{},"XCTest",[57,35533,35534],{},"JUnit \u002F MockK",[57,35536,35537],{},"Vitest \u002F Jest",[42,35539,35540,35542,35547,35550,35553],{},[57,35541,19471],{},[57,35543,35544],{},[136,35545,35546],{},"WidgetTester",[57,35548,35549],{},"XCTest + ViewInspector",[57,35551,35552],{},"Compose Testing",[57,35554,35555],{},"Vue Test Utils + @testing-library\u002Fvue",[42,35557,35558,35561,35566,35569,35572],{},[57,35559,35560],{},"集成测试",[57,35562,35563],{},[136,35564,35565],{},"integration_test",[57,35567,35568],{},"XCUITest",[57,35570,35571],{},"Espresso \u002F UI Automator",[57,35573,19488],{},[42,35575,35576,35579,35584,35589,35592],{},[57,35577,35578],{},"快照测试",[57,35580,35581],{},[136,35582,35583],{},"golden_toolkit",[57,35585,35586],{},[136,35587,35588],{},"PreviewSnapshots",[57,35590,35591],{},"Paparazzi \u002F Roborazzi",[57,35593,35594,35596],{},[136,35595,19477],{}," + snapshot",[42,35598,35599,35602,35605,35608,35611],{},[57,35600,35601],{},"Mock",[57,35603,35604],{},"mockito",[57,35606,35607],{},"Swift mock protocols",[57,35609,35610],{},"Mockito-Kotlin \u002F MockK",[57,35612,35613],{},"vitest mock \u002F msw",[32,35615,35617],{"id":35616},"_123-测试代码对比","12.3 测试代码对比",[186,35619,35621],{"className":11162,"code":35620,"language":11164,"meta":191,"style":191},"\u002F\u002F Flutter — Widget 测试\ntestWidgets('Counter increments', (tester) async {\n  await tester.pumpWidget(MaterialApp(home: CounterPage()));\n  expect(find.text('0'), findsOneWidget);\n  await tester.tap(find.byIcon(Icons.add));\n  await tester.pump();\n  expect(find.text('1'), findsOneWidget);\n});\n",[136,35622,35623,35628,35633,35638,35643,35648,35653,35658],{"__ignoreMap":191},[195,35624,35625],{"class":197,"line":198},[195,35626,35627],{},"\u002F\u002F Flutter — Widget 测试\n",[195,35629,35630],{"class":197,"line":230},[195,35631,35632],{},"testWidgets('Counter increments', (tester) async {\n",[195,35634,35635],{"class":197,"line":251},[195,35636,35637],{},"  await tester.pumpWidget(MaterialApp(home: CounterPage()));\n",[195,35639,35640],{"class":197,"line":272},[195,35641,35642],{},"  expect(find.text('0'), findsOneWidget);\n",[195,35644,35645],{"class":197,"line":293},[195,35646,35647],{},"  await tester.tap(find.byIcon(Icons.add));\n",[195,35649,35650],{"class":197,"line":562},[195,35651,35652],{},"  await tester.pump();\n",[195,35654,35655],{"class":197,"line":583},[195,35656,35657],{},"  expect(find.text('1'), findsOneWidget);\n",[195,35659,35660],{"class":197,"line":962},[195,35661,11253],{},[186,35663,35665],{"className":2177,"code":35664,"language":2179,"meta":191,"style":191},"\u002F\u002F SwiftUI — XCTest + ViewInspector\nfunc testCounterIncrements() throws {\n    var sut = CounterView()\n    let button = try sut.inspect().find(button: \"+\")\n    try button.tap()\n    let text = try sut.inspect().find(text: \"1\")\n    XCTAssertNotNil(text)\n}\n",[136,35666,35667,35672,35677,35682,35687,35692,35697,35702],{"__ignoreMap":191},[195,35668,35669],{"class":197,"line":198},[195,35670,35671],{},"\u002F\u002F SwiftUI — XCTest + ViewInspector\n",[195,35673,35674],{"class":197,"line":230},[195,35675,35676],{},"func testCounterIncrements() throws {\n",[195,35678,35679],{"class":197,"line":251},[195,35680,35681],{},"    var sut = CounterView()\n",[195,35683,35684],{"class":197,"line":272},[195,35685,35686],{},"    let button = try sut.inspect().find(button: \"+\")\n",[195,35688,35689],{"class":197,"line":293},[195,35690,35691],{},"    try button.tap()\n",[195,35693,35694],{"class":197,"line":562},[195,35695,35696],{},"    let text = try sut.inspect().find(text: \"1\")\n",[195,35698,35699],{"class":197,"line":583},[195,35700,35701],{},"    XCTAssertNotNil(text)\n",[195,35703,35704],{"class":197,"line":962},[195,35705,552],{},[186,35707,35709],{"className":6838,"code":35708,"language":6840,"meta":191,"style":191},"\u002F\u002F Compose — UI 测试\n@Test\nfun counterIncrements() {\n    composeTestRule.setContent { CounterScreen() }\n    composeTestRule.onNodeWithText(\"0\").assertIsDisplayed()\n    composeTestRule.onNodeWithContentDescription(\"add\").performClick()\n    composeTestRule.onNodeWithText(\"1\").assertIsDisplayed()\n}\n",[136,35710,35711,35716,35720,35725,35730,35735,35740,35745],{"__ignoreMap":191},[195,35712,35713],{"class":197,"line":198},[195,35714,35715],{},"\u002F\u002F Compose — UI 测试\n",[195,35717,35718],{"class":197,"line":230},[195,35719,10469],{},[195,35721,35722],{"class":197,"line":251},[195,35723,35724],{},"fun counterIncrements() {\n",[195,35726,35727],{"class":197,"line":272},[195,35728,35729],{},"    composeTestRule.setContent { CounterScreen() }\n",[195,35731,35732],{"class":197,"line":293},[195,35733,35734],{},"    composeTestRule.onNodeWithText(\"0\").assertIsDisplayed()\n",[195,35736,35737],{"class":197,"line":562},[195,35738,35739],{},"    composeTestRule.onNodeWithContentDescription(\"add\").performClick()\n",[195,35741,35742],{"class":197,"line":583},[195,35743,35744],{},"    composeTestRule.onNodeWithText(\"1\").assertIsDisplayed()\n",[195,35746,35747],{"class":197,"line":962},[195,35748,552],{},[186,35750,35752],{"className":383,"code":35751,"language":385,"meta":191,"style":191},"\u002F\u002F Vue — Vitest + Vue Test Utils\ntest('counter increments', async () => {\n  const wrapper = mount(Counter)\n  expect(wrapper.text()).toContain('0')\n  await wrapper.find('button').trigger('click')\n  expect(wrapper.text()).toContain('1')\n})\n",[136,35753,35754,35759,35779,35794,35817,35846,35865],{"__ignoreMap":191},[195,35755,35756],{"class":197,"line":198},[195,35757,35758],{"class":415},"\u002F\u002F Vue — Vitest + Vue Test Utils\n",[195,35760,35761,35764,35766,35769,35771,35773,35775,35777],{"class":197,"line":230},[195,35762,35763],{"class":201},"test",[195,35765,404],{"class":209},[195,35767,35768],{"class":459},"'counter increments'",[195,35770,301],{"class":209},[195,35772,33673],{"class":223},[195,35774,35114],{"class":209},[195,35776,16398],{"class":223},[195,35778,496],{"class":209},[195,35780,35781,35783,35786,35788,35791],{"class":197,"line":251},[195,35782,18263],{"class":223},[195,35784,35785],{"class":213}," wrapper",[195,35787,398],{"class":223},[195,35789,35790],{"class":201}," mount",[195,35792,35793],{"class":209},"(Counter)\n",[195,35795,35796,35799,35802,35804,35807,35810,35812,35815],{"class":197,"line":272},[195,35797,35798],{"class":201},"  expect",[195,35800,35801],{"class":209},"(wrapper.",[195,35803,1074],{"class":201},[195,35805,35806],{"class":209},"()).",[195,35808,35809],{"class":201},"toContain",[195,35811,404],{"class":209},[195,35813,35814],{"class":459},"'0'",[195,35816,410],{"class":209},[195,35818,35819,35822,35825,35828,35830,35833,35836,35839,35841,35844],{"class":197,"line":293},[195,35820,35821],{"class":223},"  await",[195,35823,35824],{"class":209}," wrapper.",[195,35826,35827],{"class":201},"find",[195,35829,404],{"class":209},[195,35831,35832],{"class":459},"'button'",[195,35834,35835],{"class":209},").",[195,35837,35838],{"class":201},"trigger",[195,35840,404],{"class":209},[195,35842,35843],{"class":459},"'click'",[195,35845,410],{"class":209},[195,35847,35848,35850,35852,35854,35856,35858,35860,35863],{"class":197,"line":562},[195,35849,35798],{"class":201},[195,35851,35801],{"class":209},[195,35853,1074],{"class":201},[195,35855,35806],{"class":209},[195,35857,35809],{"class":201},[195,35859,404],{"class":209},[195,35861,35862],{"class":459},"'1'",[195,35864,410],{"class":209},[195,35866,35867],{"class":197,"line":583},[195,35868,16566],{"class":209},[11,35870,35871,35875],{},[14,35872,29042,35873,333],{},[125,35874,29045],{},[129,35876,35877,35886,35893],{},[132,35878,15736,35879,16122,35882,35885],{},[136,35880,35881],{},"pump()",[136,35883,35884],{},"pumpAndSettle()"," 控制帧推进，是测试异步 UI 的关键",[132,35887,35888,35889,35892],{},"SwiftUI 的测试生态还不成熟，",[136,35890,35891],{},"ViewInspector"," 是社区库",[132,35894,35895,35896,1437,35899],{},"Compose 的测试基于语义树（semantics），需要正确设置 ",[136,35897,35898],{},"contentDescription",[136,35900,35901],{},"testTag",[11,35903,35904,35908],{},[14,35905,29055,35906,333],{},[125,35907,29058],{},[129,35909,35910,35919,35926],{},[132,35911,30616,35912,35915,35916,35918],{},[136,35913,35914],{},"pumpWidget"," 后没有 ",[136,35917,35881],{}," 导致状态变化未反映到 UI",[132,35920,35921,35922,35925],{},"Vue: 异步操作后没有 ",[136,35923,35924],{},"await nextTick()"," 导致断言时 DOM 还没更新",[132,35927,35928,35929,35932],{},"Android: Espresso 在异步操作时需要 ",[136,35930,35931],{},"IdlingResource","，否则测试不稳定",[11,35934,35935,35939],{},[14,35936,29091,35937,333],{},[125,35938,29094],{},[129,35940,35941,35944],{},[132,35942,35943],{},"Flutter 的三种测试有什么区别？（Unit: 纯逻辑，Widget: 单组件渲染，Integration: 真设备完整流程）",[132,35945,35946],{},"如何测试异步操作？各平台的方案？（Flutter: pump, Swift: async test + expectation, Kotlin: runTest, Vue: nextTick\u002Fflush-promises）",[1890,35948],{},[32,35950,35952],{"id":35951},"_124-cicd-与发布","12.4 CI\u002FCD 与发布",[36,35954,35955,35969],{},[39,35956,35957],{},[42,35958,35959,35961,35963,35965,35967],{},[45,35960,15463],{},[45,35962,13135],{},[45,35964,13113],{},[45,35966,13124],{},[45,35968,15468],{},[52,35970,35971,35987,36004,36021],{},[42,35972,35973,35975,35978,35981,35984],{},[57,35974,15309],{},[57,35976,35977],{},"GitHub Actions \u002F Codemagic",[57,35979,35980],{},"Xcode Cloud \u002F Fastlane",[57,35982,35983],{},"GitHub Actions \u002F Bitrise",[57,35985,35986],{},"Vercel \u002F Netlify \u002F GitHub Pages",[42,35988,35989,35992,35995,35998,36001],{},[57,35990,35991],{},"发布渠道",[57,35993,35994],{},"App Store + Google Play",[57,35996,35997],{},"App Store (TestFlight)",[57,35999,36000],{},"Google Play (内部测试轨道)",[57,36002,36003],{},"CDN 部署",[42,36005,36006,36009,36012,36015,36018],{},[57,36007,36008],{},"签名",[57,36010,36011],{},"Android: keystore, iOS: certificate + provisioning",[57,36013,36014],{},"Certificate + Provisioning Profile",[57,36016,36017],{},"keystore",[57,36019,36020],{},"无需签名",[42,36022,36023,36026,36029,36032,36035],{},[57,36024,36025],{},"热更新",[57,36027,36028],{},"Shorebird (实验性)",[57,36030,36031],{},"不允许 (App Store 政策)",[57,36033,36034],{},"不允许 (Play Store 政策)",[57,36036,36037],{},"直接部署新版本",[11,36039,36040,36044],{},[14,36041,29091,36042,333],{},[125,36043,29094],{},[129,36045,36046,36049,36052],{},[132,36047,36048],{},"Flutter 的 Release 模式和 Debug 模式的区别？（Debug: JIT + assert + DevTools, Release: AOT + tree-shaking + 无 assert）",[132,36050,36051],{},"iOS 的 Code Signing 流程？（开发者证书 + App ID + Provisioning Profile 三件套）",[132,36053,36054],{},"Vue 的 Vite 构建做了哪些优化？（Tree-shaking、Code Splitting、CSS 提取、预构建依赖）",[1890,36056],{},[18,36058,36060],{"id":36059},"附录四平台速查表","附录：四平台速查表",[36,36062,36063,36081],{},[39,36064,36065],{},[42,36066,36067,36070,36072,36075,36078],{},[45,36068,36069],{},"你想做...",[45,36071,13135],{},[45,36073,36074],{},"iOS (Swift)",[45,36076,36077],{},"Android (Kotlin)",[45,36079,36080],{},"Vue 3 (TS)",[52,36082,36083,36101,36120,36144,36169,36182,36195,36210,36224,36240,36257,36274],{},[42,36084,36085,36087,36091,36094,36097],{},[57,36086,19366],{},[57,36088,36089],{},[136,36090,19371],{},[57,36092,36093],{},"Xcode New Project",[57,36095,36096],{},"Android Studio New Project",[57,36098,36099],{},[136,36100,19376],{},[42,36102,36103,36106,36110,36113,36116],{},[57,36104,36105],{},"运行项目",[57,36107,36108],{},[136,36109,19386],{},[57,36111,36112],{},"Cmd+R (Xcode)",[57,36114,36115],{},"Run (Android Studio)",[57,36117,36118],{},[136,36119,1795],{},[42,36121,36122,36125,36130,36136,36139],{},[57,36123,36124],{},"安装依赖",[57,36126,36127],{},[136,36128,36129],{},"flutter pub get",[57,36131,36132,36133],{},"SPM resolve \u002F ",[136,36134,36135],{},"pod install",[57,36137,36138],{},"Gradle Sync",[57,36140,36141],{},[136,36142,36143],{},"npm install",[42,36145,36146,36149,36156,36160,36165],{},[57,36147,36148],{},"状态变量",[57,36150,36151,16122,36153],{},[136,36152,16010],{},[136,36154,36155],{},".obs",[57,36157,36158],{},[136,36159,3120],{},[57,36161,36162],{},[136,36163,36164],{},"mutableStateOf",[57,36166,36167],{},[136,36168,16026],{},[42,36170,36171,36173,36175,36177,36180],{},[57,36172,17334],{},[57,36174,31292],{},[57,36176,3160],{},[57,36178,36179],{},"ViewModel + Hilt",[57,36181,17340],{},[42,36183,36184,36187,36189,36191,36193],{},[57,36185,36186],{},"网络请求",[57,36188,32460],{},[57,36190,32465],{},[57,36192,32471],{},[57,36194,32477],{},[42,36196,36197,36200,36203,36205,36208],{},[57,36198,36199],{},"路由跳转",[57,36201,36202],{},"GoRouter \u002F Navigator",[57,36204,31928],{},[57,36206,36207],{},"NavHost",[57,36209,15526],{},[42,36211,36212,36215,36217,36219,36222],{},[57,36213,36214],{},"本地存储",[57,36216,33095],{},[57,36218,33098],{},[57,36220,36221],{},"DataStore",[57,36223,33104],{},[42,36225,36226,36229,36232,36235,36237],{},[57,36227,36228],{},"数据库",[57,36230,36231],{},"Drift \u002F sqflite",[57,36233,36234],{},"SwiftData \u002F Core Data",[57,36236,33126],{},[57,36238,36239],{},"IndexedDB",[42,36241,36242,36245,36247,36250,36252],{},[57,36243,36244],{},"列表组件",[57,36246,30824],{},[57,36248,36249],{},"LazyVStack \u002F List",[57,36251,30844],{},[57,36253,36254,36256],{},[136,36255,30815],{}," + 虚拟列表",[42,36258,36259,36261,36265,36268,36271],{},[57,36260,19446],{},[57,36262,36263],{},[136,36264,19451],{},[57,36266,36267],{},"Xcode 格式化",[57,36269,36270],{},"ktlint \u002F detekt",[57,36272,36273],{},"ESLint + Prettier",[42,36275,36276,36279,36283,36286,36291],{},[57,36277,36278],{},"运行测试",[57,36280,36281],{},[136,36282,19463],{},[57,36284,36285],{},"Cmd+U (Xcode)",[57,36287,36288],{},[136,36289,36290],{},".\u002Fgradlew test",[57,36292,36293],{},[136,36294,36295],{},"npm run test",[733,36297,36298],{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":191,"searchDepth":230,"depth":230,"links":36300},[36301,36302,36309,36315,36320,36324,36328,36333,36337,36342,36347,36351,36356,36362],{"id":1894,"depth":230,"text":1894},{"id":28700,"depth":230,"text":28701,"children":36303},[36304,36305,36306,36307,36308],{"id":28704,"depth":251,"text":28705},{"id":28816,"depth":251,"text":28817},{"id":29113,"depth":251,"text":29114},{"id":29492,"depth":251,"text":29493},{"id":29726,"depth":251,"text":29727},{"id":30087,"depth":230,"text":30088,"children":36310},[36311,36312,36313,36314],{"id":30091,"depth":251,"text":30092},{"id":30204,"depth":251,"text":30205},{"id":30477,"depth":251,"text":30478},{"id":30651,"depth":251,"text":30652},{"id":30855,"depth":230,"text":30856,"children":36316},[36317,36318,36319],{"id":30859,"depth":251,"text":30860},{"id":31265,"depth":251,"text":31266},{"id":31432,"depth":251,"text":31433},{"id":31553,"depth":230,"text":31554,"children":36321},[36322,36323],{"id":31557,"depth":251,"text":31558},{"id":31768,"depth":251,"text":31769},{"id":31891,"depth":230,"text":31892,"children":36325},[36326,36327],{"id":31895,"depth":251,"text":31896},{"id":32062,"depth":251,"text":32063},{"id":32426,"depth":230,"text":32427,"children":36329},[36330,36331,36332],{"id":32430,"depth":251,"text":32431},{"id":32566,"depth":251,"text":32567},{"id":32895,"depth":251,"text":32896},{"id":33064,"depth":230,"text":33065,"children":36334},[36335,36336],{"id":33068,"depth":251,"text":33069},{"id":33165,"depth":251,"text":33166},{"id":33379,"depth":230,"text":33380,"children":36338},[36339,36340,36341],{"id":33383,"depth":251,"text":33384},{"id":33516,"depth":251,"text":33517},{"id":33904,"depth":251,"text":33905},{"id":34065,"depth":230,"text":34066,"children":36343},[36344,36345,36346],{"id":34069,"depth":251,"text":34070},{"id":34173,"depth":251,"text":34174},{"id":34225,"depth":251,"text":34226},{"id":34430,"depth":230,"text":34431,"children":36348},[36349,36350],{"id":34434,"depth":251,"text":34435},{"id":34530,"depth":251,"text":34531},{"id":34702,"depth":230,"text":34703,"children":36352},[36353,36354,36355],{"id":34706,"depth":251,"text":34707},{"id":35175,"depth":251,"text":35176},{"id":35318,"depth":251,"text":35319},{"id":35392,"depth":230,"text":35393,"children":36357},[36358,36359,36360,36361],{"id":35396,"depth":251,"text":35397},{"id":35500,"depth":251,"text":35501},{"id":35616,"depth":251,"text":35617},{"id":35951,"depth":251,"text":35952},{"id":36059,"depth":230,"text":36060},"2026-05-03 10:00:00 CST","以同一主题为轴，横向对比四个平台的基础、重难点、易错点与面试高频考点。",{},"\u002Fnotes\u002F2026-05-03-cross-platform-comparison",{"title":28611,"description":36364},"横向对比 Flutter、iOS、Android、Vue 四个平台的语言基础、UI 构建、状态管理、路由导航等核心主题。","Flutter \u002F iOS \u002F Android \u002F Vue 四平台横向对比｜个人笔记","cross-platform-comparison","notes\u002F2026-05-03-cross-platform-comparison","H2Emwbp-SKgHbW9rE6ITAecIJ7mXiIiCKZR2c5L8ecY",{"id":36374,"title":36375,"body":36376,"category":752,"date":36635,"description":36636,"extension":755,"meta":36637,"navigation":757,"order":752,"path":36638,"seo":36639,"seoDescription":36640,"seoTitle":36641,"slug":36642,"stem":36643,"__hash__":36644},"notes\u002Fnotes\u002F2026-04-29-homepage-ui-collaboration-review.md","首页模块调整的 AI 协作提问复盘",{"type":8,"value":36377,"toc":36622},[36378,36383,36386,36389,36392,36395,36418,36421,36492,36495,36562,36565,36568,36574,36578,36584,36587,36593,36596,36602,36605],[11,36379,36380],{},[14,36381,36382],{},"记录时间：2026-04-29 17:31:23 CST",[18,36384,36385],{"id":36385},"本轮协作目标",[14,36387,36388],{},"这轮主要是在首页已有结构基础上，继续调整模块宽度、增加个人笔记入口、重做关于我入口，并把卡片细节打磨到更统一。",[14,36390,36391],{},"这不是大规模重构，而是一组连续的 UI 微调。关键不在于一次说完所有需求，而在于每次都指出具体页面、具体模块和明确差异。",[18,36393,36394],{"id":36394},"我提出的问题",[706,36396,36397,36400,36403,36406,36409,36412],{},[132,36398,36399],{},"首页底部模块宽度比上面的模块多出来了，请修改。类型：bug 排查 \u002F 样式调整。",[132,36401,36402],{},"首页添加个人笔记列表，展示最近 3 条，使用列表形式，有“个人笔记”标题，“查看全部”跳转到个人笔记页面。类型：结构新增。",[132,36404,36405],{},"个人笔记模块每一条采用白底、圆角、有阴影的设计。底部关于我模块也要设计成标题“关于我”和“查看全部”跳转。类型：视觉统一 \u002F 结构重构。",[132,36407,36408],{},"个人笔记和关于我卡片里的箭头去掉。类型：样式细节调整。",[132,36410,36411],{},"关于我卡片里的内容右边留白太多，修改为文字到右边框的距离和左边对齐。类型：视觉细节修正。",[132,36413,594,36414,36417],{},[136,36415,36416],{},"write-collaboration-review-note"," skill 生成本轮协作复盘。类型：总结复盘。",[18,36419,36420],{"id":36420},"好问题",[36,36422,36423,36435],{},[39,36424,36425],{},[42,36426,36427,36429,36432],{},[45,36428,20],{},[45,36430,36431],{},"为什么好",[45,36433,36434],{},"可复用提问方式",[52,36436,36437,36448,36459,36470,36481],{},[42,36438,36439,36442,36445],{},[57,36440,36441],{},"“首页底部模块宽度比上面的模块多出来了”",[57,36443,36444],{},"指出了页面、模块和视觉差异。AI 可以直接检查容器宽度、margin、max-width，而不是泛泛地“美化”。",[57,36446,36447],{},"“某页面的 A 模块比 B 模块宽，请检查容器宽度、margin、padding，并让它们视觉对齐。”",[42,36449,36450,36453,36456],{},[57,36451,36452],{},"“首页添加个人笔记列表，展示最近的 3 条”",[57,36454,36455],{},"有数据范围、数量、展示形式和跳转目标，属于可直接实现的需求。",[57,36457,36458],{},"“在某页面新增某模块，数据来源是某 collection，展示最近 N 条，点击跳转到某详情页。”",[42,36460,36461,36464,36467],{},[57,36462,36463],{},"“每一条都采用白底、有圆角、有阴影”",[57,36465,36466],{},"给出了明确视觉属性，避免 AI 自行选择列表、分割线或卡片样式。",[57,36468,36469],{},"“把列表项改成独立卡片：白底、圆角、轻阴影、保留整行点击。”",[42,36471,36472,36475,36478],{},[57,36473,36474],{},"“箭头去掉”",[57,36476,36477],{},"目标很小，范围很明确，能直接清理模板和对应样式。",[57,36479,36480],{},"“去掉某模块卡片里的某元素，并清理对应 CSS。”",[42,36482,36483,36486,36489],{},[57,36484,36485],{},"“右边留白太多，文字到右边框的距离对齐左边”",[57,36487,36488],{},"虽然是视觉反馈，但已经说明了问题方向：左右内边距不一致或内容宽度被限制。",[57,36490,36491],{},"“某卡片内文字列右侧留白过大，请检查 max-width \u002F grid \u002F padding，让内容区撑满卡片内部宽度。”",[18,36493,36494],{"id":36494},"可改进的问题",[36,36496,36497,36510],{},[39,36498,36499],{},[42,36500,36501,36504,36507],{},[45,36502,36503],{},"原问题",[45,36505,36506],{},"问题在哪里",[45,36508,36509],{},"更好的问法",[52,36511,36512,36523,36534,36551],{},[42,36513,36514,36517,36520],{},[57,36515,36516],{},"“展示最近的3三条”",[57,36518,36519],{},"语义能理解，但有错字。AI 通常会自动纠正，不过正式需求里最好减少歧义。",[57,36521,36522],{},"“展示最近 3 条个人笔记，按 date 倒序。”",[42,36524,36525,36528,36531],{},[57,36526,36527],{},"“使用列表形式”",[57,36529,36530],{},"“列表形式”可以是分割线列表，也可以是卡片列表。后续又补充了白底圆角阴影，说明最初还可以更具体。",[57,36532,36533],{},"“使用列表结构，但每条视觉上是独立卡片，不使用表格。”",[42,36535,36536,36539,36545],{},[57,36537,36538],{},"“也要设计成标题 关于我 查看全部跳转”",[57,36540,36541,36542,1589],{},"方向明确，但跳转目标需要 AI 推断是 ",[136,36543,36544],{},"\u002Fabout",[57,36546,36547,36548,36550],{},"“关于我模块也改成标题行：左侧标题‘关于我’，右侧‘查看全部’，链接到 ",[136,36549,36544],{},"。”",[42,36552,36553,36556,36559],{},[57,36554,36555],{},"“右边留白太多”",[57,36557,36558],{},"已经能定位，但如果附带截图或说明桌面端 \u002F 移动端，会更快验证。",[57,36560,36561],{},"“桌面端关于我卡片文字列只占左侧，右边空太多。请只改首页关于我卡片，让文字区撑满卡片内部宽度，左右内边距一致。”",[18,36563,36564],{"id":36564},"下次直接复制的提问模板",[32,36566,36567],{"id":36567},"视觉调整类",[186,36569,36572],{"className":36570,"code":36571,"language":1074,"meta":191},[1072],"请调整首页的 [模块名]。\n当前问题：[具体视觉问题，例如右侧留白太多 \u002F 宽度比上方模块宽 \u002F 卡片之间间距不一致]。\n目标效果：[具体规则，例如左右内边距一致，和上方内容容器对齐]。\n修改范围：只改首页模板和相关 CSS，不改其他页面结构。\n验收：桌面端和移动端都不横向溢出，模块宽度视觉统一。\n",[136,36573,36571],{"__ignoreMap":191},[32,36575,36577],{"id":36576},"bug-修复类","bug 修复类",[186,36579,36582],{"className":36580,"code":36581,"language":1074,"meta":191},[1072],"首页 [模块名] 出现了 [异常现象]。\n请先定位原因，再修改。\n重点检查：HTML 结构、CSS 容器宽度、max-width、margin、padding、响应式规则。\n验收：和 [对照模块] 的左右边界一致。\n",[136,36583,36581],{"__ignoreMap":191},[32,36585,36586],{"id":36586},"内容生成类",[186,36588,36591],{"className":36589,"code":36590,"language":1074,"meta":191},[1072],"请基于本轮对话生成一篇个人笔记复盘。\n时间范围：从上一篇复盘之后到现在。\n重点：\n1. 我提了哪些问题。\n2. 哪些问题表达清楚，为什么。\n3. 哪些问题可以改得更具体。\n4. 下次可以复用的提问模板。\n要求：沿用 content\u002Fnotes 现有 frontmatter 格式，保存为新 Markdown 文件。\n",[136,36592,36590],{"__ignoreMap":191},[32,36594,36595],{"id":36595},"组件重构类",[186,36597,36600],{"className":36598,"code":36599,"language":1074,"meta":191},[1072],"请把首页的 [模块 A] 和 [模块 B] 统一成同一套模块结构。\n每个模块都包含：标题、查看全部链接、内容卡片。\n差异只保留在内容展示方式上。\n要求：不要改数据 collection，不要改详情页，只改首页结构和样式。\n",[136,36601,36599],{"__ignoreMap":191},[18,36603,36604],{"id":36604},"本轮最大的经验",[706,36606,36607,36610,36613,36616,36619],{},[132,36608,36609],{},"UI 微调最有效的表达方式，是“当前差异 + 目标对齐对象”，比如“底部模块比上面宽”。",[132,36611,36612],{},"先说结构，再说视觉属性，比单纯说“设计一下”更稳定。",[132,36614,36615],{},"小问题可以连续提出，但每次最好只改一个清晰目标，这样返工成本低。",[132,36617,36618],{},"对卡片类界面，要明确是“列表结构”还是“卡片视觉”，二者不是一回事。",[132,36620,36621],{},"视觉留白问题最好指出是 padding、内容宽度还是容器宽度；如果不确定，也可以要求 AI 检查这三类原因。",{"title":191,"searchDepth":230,"depth":230,"links":36623},[36624,36625,36626,36627,36628,36634],{"id":36385,"depth":230,"text":36385},{"id":36394,"depth":230,"text":36394},{"id":36420,"depth":230,"text":36420},{"id":36494,"depth":230,"text":36494},{"id":36564,"depth":230,"text":36564,"children":36629},[36630,36631,36632,36633],{"id":36567,"depth":251,"text":36567},{"id":36576,"depth":251,"text":36577},{"id":36586,"depth":251,"text":36586},{"id":36595,"depth":251,"text":36595},{"id":36604,"depth":230,"text":36604},"2026-04-29 17:31:23 CST","复盘首页个人笔记、关于我模块和卡片样式调整过程中的提问方式，总结哪些表达清楚、哪些可以补充验收标准。",{},"\u002Fnotes\u002F2026-04-29-homepage-ui-collaboration-review",{"title":36375,"description":36636},"复盘首页模块宽度、个人笔记列表、关于我入口和卡片细节调整中的 AI 协作提问质量与复用模板。","首页模块调整的 AI 协作提问复盘｜个人笔记","homepage-ui-collaboration-review-2026-04-29","notes\u002F2026-04-29-homepage-ui-collaboration-review","Rev8KVKyV89ehvFZzNS40gRZjPyAW0laVr6S03oi9kE",{"id":36646,"title":36647,"body":36648,"category":752,"date":37141,"description":37142,"extension":755,"meta":37143,"navigation":757,"order":752,"path":37144,"seo":37145,"seoDescription":37146,"seoTitle":37147,"slug":37148,"stem":37149,"__hash__":37150},"notes\u002Fnotes\u002F2026-04-29-ai-collaboration-question-review.md","与 AI 协作完成项目的阶段性提问复盘",{"type":8,"value":36649,"toc":37114},[36650,36655,36658,36661,36664,36675,36678,36681,36685,36711,36715,36738,36742,36769,36772,36775,36779,36784,36787,36790,36801,36804,36808,36813,36816,36819,36823,36828,36831,36838,36841,36845,36850,36853,36856,36860,36865,36868,36871,36882,36885,36888,36891,36895,36900,36903,36906,36923,36926,36932,36936,36941,36944,36947,36953,36957,36960,36963,36966,36972,36975,36981,36985,36990,36993,36995,37001,37004,37007,37010,37016,37020,37026,37030,37036,37040,37046,37050,37053,37056,37073,37076,37099,37102,37105,37108,37111],[11,36651,36652],{},[14,36653,36654],{},"记录时间：2026-04-29 15:03:06 CST",[18,36656,36657],{"id":36657},"这篇笔记在回答什么",[14,36659,36660],{},"这篇笔记复盘的是：为了完成这个多元思维模型网站，到目前为止我向 AI 提过哪些问题，哪些问题提得好，哪些问题还可以更清楚，以及后续如何提问，才能让 AI 更稳定地理解目标、减少返工、提高实现质量。",[14,36662,36663],{},"这里的判断标准不是“语气好不好”，而是三个工程标准：",[706,36665,36666,36669,36672],{},[132,36667,36668],{},"AI 能不能明确知道要改什么。",[132,36670,36671],{},"AI 能不能知道不要改什么。",[132,36673,36674],{},"改完之后能不能验证结果是否满足预期。",[18,36676,36677],{"id":36677},"到目前为止提出过的问题",[14,36679,36680],{},"根据已有项目笔记和这轮最新协作，到目前为止主要提出过这些问题。",[32,36682,36684],{"id":36683},"第一阶段项目方向和内容体系","第一阶段：项目方向和内容体系",[706,36686,36687,36690,36693,36696,36699,36702,36705,36708],{},[132,36688,36689],{},"想做一个介绍芒格多元思维模型的网站，技术栈要求 Vue、Vite、Nuxt，并且 SEO 友好。",[132,36691,36692],{},"纠正方向：不是马上写代码，而是严格按照软件开发流程，从需求、产品、UI 开始规划。",[132,36694,36695],{},"要求继续细化开发规划。",[132,36697,36698],{},"要求每个思维模型按照费曼教学法来解释，先输出模板。",[132,36700,36701],{},"确认模板后，要求更新项目中所有思维模型文档。",[132,36703,36704],{},"要求分析 ayaseeri.com 中按学科列举的思维模型。",[132,36706,36707],{},"要求把这些模型按学科加入项目，重复的不添加。",[132,36709,36710],{},"要求从 Vue 架构师角度讲解项目整体架构、优点、缺点。",[32,36712,36714],{"id":36713},"第二阶段信息架构和页面内容","第二阶段：信息架构和页面内容",[706,36716,36717,36720,36723,36726,36729,36732,36735],{"start":968},[132,36718,36719],{},"要求移除学习路径、查理芒格、出处三个模块，新增个人介绍。",[132,36721,36722],{},"要求首页去掉“我”的多元思维模型”等个人化表达，统一改成“思维模型”。",[132,36724,36725],{},"发现所有思维模型入口消失，要求检查并修复。",[132,36727,36728],{},"要求去掉英文栏目标题和多余文案，并作为资深 UI 设计师优化页面。",[132,36730,36731],{},"对个人介绍页不满意，要求按极简风重新设计。",[132,36733,36734],{},"要求个人理念和核心书单必须保留在个人介绍后面。",[132,36736,36737],{},"要求价值观三句话分行，并继续调整整体字体大小和样式。",[32,36739,36741],{"id":36740},"第三阶段首页-hero-和通用顶部组件","第三阶段：首页 Hero 和通用顶部组件",[706,36743,36744,36747,36757,36760,36763,36766],{"start":2257},[132,36745,36746],{},"要求去掉首页顶部背景图大 Hero，改成黑底白字“做正确的事 \u002F DO THE RIGHT THING”。",[132,36748,36749,36750,30834,36753,36756],{},"要求修改 ",[136,36751,36752],{},"main.css",[136,36754,36755],{},"hero-section"," 样式，让文字保持居中。",[132,36758,36759],{},"发现修改后思维模型列表没有了，要求检查原因。",[132,36761,36762],{},"要求把首页顶部抽象成组件，让思维模型、个人笔记、个人介绍的顶部都使用同一个组件。",[132,36764,36765],{},"个人介绍页修改后样式不对，要求保持顶部不变，重新优化其他内容。",[132,36767,36768],{},"要求总结整个项目中的提问质量，并排版保存到个人笔记模块。",[18,36770,36771],{"id":36771},"哪些问题提得好",[14,36773,36774],{},"好问题通常有一个共同点：目标、范围和约束都比较清楚。",[32,36776,36778],{"id":36777},"_1-先要模板再批量替换","1. 先要模板，再批量替换",[11,36780,36781],{},[14,36782,36783],{},"每个思维模型希望按照费曼教学法解释。请先写一个模板，我觉得合适再全部替换。",[14,36785,36786],{},"这个问题很好，因为它没有一上来就要求大规模改内容，而是先确认结构，再批量执行。",[14,36788,36789],{},"好处是：",[706,36791,36792,36795,36798],{},[132,36793,36794],{},"降低返工成本。",[132,36796,36797],{},"先统一内容标准。",[132,36799,36800],{},"让 AI 可以把后续批量修改变成机械执行，而不是边写边猜。",[14,36802,36803],{},"这是很适合内容型项目的提问方式。",[32,36805,36807],{"id":36806},"_2-明确删除什么保留什么新增什么","2. 明确删除什么、保留什么、新增什么",[11,36809,36810],{},[14,36811,36812],{},"把学习路径、查理芒格、出处这三个模块去掉。加上个人介绍。",[14,36814,36815],{},"这个问题也很好，因为它同时给了删除范围和新增方向。",[14,36817,36818],{},"这种问题适合页面信息架构调整。AI 不需要猜哪些内容还能保留，也不会无意中继续围绕旧模块设计页面。",[32,36820,36822],{"id":36821},"_3-发现异常后直接要求排查原因","3. 发现异常后直接要求排查原因",[11,36824,36825],{},[14,36826,36827],{},"修改之后思维模型列表没有了，请你查查是什么原因引起的。",[14,36829,36830],{},"这是一个有效的 bug 类问题。它说明了异常现象，也明确要求先查原因。",[14,36832,36833,36834,36837],{},"这次实际排查发现：列表消失不是 Hero 组件导致的，而是 dev server 中 Nuxt Content 的 SQLite 表异常，报错是 ",[136,36835,36836],{},"no such table: _content_models","。重启 dev server 后恢复。",[14,36839,36840],{},"这个问题的价值在于：它没有直接要求“把列表加回来”，而是要求先定位原因。对工程问题来说，这是正确方向。",[32,36842,36844],{"id":36843},"_4-明确顶部不变其他内容优化","4. 明确“顶部不变，其他内容优化”",[11,36846,36847],{},[14,36848,36849],{},"个人介绍这个页面我也修改了，但是修改之后样式不对了，我想保持顶部不变，其他内容你帮我重新优化。",[14,36851,36852],{},"这是一个比“帮我美化一下”更好的问题，因为它给了一个重要约束：顶部不变。",[14,36854,36855],{},"这个约束很关键。它能防止 AI 重新设计已经确认过的公共顶部组件，只把注意力放到个人介绍页下方内容。",[32,36857,36859],{"id":36858},"_5-要求抽象公共组件","5. 要求抽象公共组件",[11,36861,36862],{},[14,36863,36864],{},"把首页的顶部抽象成一个组件，让思维模型、个人笔记、个人介绍的顶部都使用同一个组件。",[14,36866,36867],{},"这是一个工程质量问题，不只是视觉问题。",[14,36869,36870],{},"它的价值在于：",[706,36872,36873,36876,36879],{},[132,36874,36875],{},"减少重复结构。",[132,36877,36878],{},"统一页面顶部风格。",[132,36880,36881],{},"后续修改 Hero 时只需要改一个组件。",[14,36883,36884],{},"这类提问说明关注点已经从“页面看起来怎么样”进化到“代码结构是否可维护”。",[18,36886,36887],{"id":36887},"哪些问题还可以改进",[14,36889,36890],{},"这里的“差”不是指问题没价值，而是指它们容易让 AI 猜测，或者容易造成返工。",[32,36892,36894],{"id":36893},"_1-样式不对还不够具体","1. “样式不对”还不够具体",[11,36896,36897],{},[14,36898,36899],{},"样式不对了，帮我重新优化。",[14,36901,36902],{},"这个方向能让 AI 开始工作，但还不够精确。",[14,36904,36905],{},"它缺少这些信息：",[706,36907,36908,36911,36914,36917,36920],{},[132,36909,36910],{},"是间距不对，还是字体不对。",[132,36912,36913],{},"是桌面端不对，还是移动端不对。",[132,36915,36916],{},"是首屏不对，还是下面模块不对。",[132,36918,36919],{},"哪些内容必须保留，哪些可以重组。",[132,36921,36922],{},"更想要简历风、个人品牌风、文档风，还是作品集风。",[14,36924,36925],{},"更好的说法是：",[186,36927,36930],{"className":36928,"code":36929,"language":1074,"meta":191},[1072],"个人介绍页顶部保持不变。\n下面内容现在的问题是：简介散在 main 外面，联系方式和基本信息没有对齐，移动端容易挤。\n请只修改 \u002Fabout 页面顶部以下内容和相关 CSS。\n目标是：像一个克制的个人品牌页，不要传统简历卡片堆叠。\n必须保留：简介、邮箱、电话、学历\u002F地点\u002F价值观、核心能力、项目、职业路径、个人理念、书单。\n验收：桌面端内容宽度统一，移动端不横向溢出。\n",[136,36931,36929],{"__ignoreMap":191},[32,36933,36935],{"id":36934},"_2-美化一下太主观","2. “美化一下”太主观",[11,36937,36938],{},[14,36939,36940],{},"把整体页面样式进行美化。",[14,36942,36943],{},"这个问题容易产生多轮返工，因为“美”没有唯一标准。",[14,36945,36946],{},"更好的方式是给出明确风格边界：",[186,36948,36951],{"className":36949,"code":36950,"language":1074,"meta":191},[1072],"请优化页面视觉。\n风格：极简、克制、偏个人品牌，不要营销感。\n颜色：保留黑白和少量金色，不要新增大面积渐变。\n布局：不要卡片堆叠，优先用分隔线和网格。\n字体：标题有力量，但正文阅读密度要舒服。\n移动端：优先保证不溢出、不裁切。\n",[136,36952,36950],{"__ignoreMap":191},[32,36954,36956],{"id":36955},"_3-继续在复杂任务里容易有歧义","3. “继续”在复杂任务里容易有歧义",[14,36958,36959],{},"如果上下文很短，“继续”可以用。",[14,36961,36962],{},"但在这个项目里，任务包含内容生成、页面设计、组件抽象、bug 修复、CSS 调整、构建验证。“继续”可能表示继续规划、继续改 UI、继续排查 bug，也可能表示继续实现未完成的模块。",[14,36964,36965],{},"更好的方式是：",[186,36967,36970],{"className":36968,"code":36969,"language":1074,"meta":191},[1072],"继续上一轮的个人介绍页优化，只改 about 页面，不改 AppHero。\n",[136,36971,36969],{"__ignoreMap":191},[14,36973,36974],{},"或者：",[186,36976,36979],{"className":36977,"code":36978,"language":1074,"meta":191},[1072],"继续排查模型列表消失的问题，先不要改样式。\n",[136,36980,36978],{"__ignoreMap":191},[32,36982,36984],{"id":36983},"_4-bug-问题最好附带复现信息","4. bug 问题最好附带复现信息",[11,36986,36987],{},[14,36988,36989],{},"修改之后思维模型列表没有了。",[14,36991,36992],{},"这句话本身有效，但如果补充更多信息，会更快定位。",[14,36994,36965],{},[186,36996,36999],{"className":36997,"code":36998,"language":1074,"meta":191},[1072],"首页 \u002F 的“多元思维模型”区域没有卡片了。\n我是在刚改完 hero-section 居中之后发现的。\n\u002Fmodels 页面是否正常我还没确认。\n请检查：数据查询、DOM 是否存在、CSS 是否隐藏、dev server 是否报错。\n",[136,37000,36998],{"__ignoreMap":191},[14,37002,37003],{},"如果能附浏览器控制台或终端报错，会更好。",[18,37005,37006],{"id":37006},"后续应该如何改进提问",[14,37008,37009],{},"后续可以把问题拆成六个部分。",[186,37011,37014],{"className":37012,"code":37013,"language":1074,"meta":191},[1072],"目标：\n我希望最终达到什么效果。\n\n当前问题：\n现在哪里不对，最好指出页面、模块、视口。\n\n修改范围：\n允许改哪些文件或页面，不允许改哪些部分。\n\n必须保留：\n哪些文案、模块、交互或视觉不能动。\n\n风格\u002F参考：\n想要什么感觉，不想要什么感觉。\n\n验收标准：\n改完后怎么判断完成。\n",[136,37015,37013],{"__ignoreMap":191},[18,37017,37019],{"id":37018},"推荐模板视觉优化类","推荐模板：视觉优化类",[186,37021,37024],{"className":37022,"code":37023,"language":1074,"meta":191},[1072],"请优化个人介绍页。\n\n目标：\n做成克制、干净、有信息密度的个人品牌页。\n\n当前问题：\n顶部下面的信息不够整齐，简介、联系方式和基本信息缺少统一容器。\n\n修改范围：\n只改 \u002Fabout 页面和相关 CSS。\n不要改 AppHero，不要改首页、思维模型页、个人笔记页。\n\n必须保留：\n简介、联系方式、基本信息、核心能力、代表项目、职业路径、个人理念、核心书单。\n\n风格要求：\n黑白为主，少量金色点缀；不要大面积卡片；不要花哨背景；移动端优先。\n\n验收标准：\n桌面端内容宽度统一；移动端不横向溢出；首屏能看清“我是谁”和联系方式。\n",[136,37025,37023],{"__ignoreMap":191},[18,37027,37029],{"id":37028},"推荐模板功能修复类","推荐模板：功能修复类",[186,37031,37034],{"className":37032,"code":37033,"language":1074,"meta":191},[1072],"请修复首页模型列表消失的问题。\n\n现象：\n首页 \u002F 的“多元思维模型”区域没有模型卡片。\n\n期望：\n首页展示前 6 个模型，卡片可点击进入详情。\n\n最近改动：\n刚修改了 AppHero \u002F hero-section 样式。\n\n检查范围：\n请检查 Nuxt Content 查询、SSR HTML、DOM 是否存在、CSS 是否隐藏、终端报错。\n\n验收标准：\n首页能看到 6 张卡片；\u002Fmodels 页面正常；npm run typecheck 和 npm run build 通过。\n",[136,37035,37033],{"__ignoreMap":191},[18,37037,37039],{"id":37038},"推荐模板组件抽象类","推荐模板：组件抽象类",[186,37041,37044],{"className":37042,"code":37043,"language":1074,"meta":191},[1072],"请把页面顶部抽象成公共组件。\n\n目标：\n首页、思维模型、个人笔记、个人介绍共用同一个顶部组件。\n\n约束：\n首页顶部视觉保持黑底白字。\n个人介绍页顶部文案保持不变。\n不要影响模型列表和笔记列表。\n\n组件能力：\n支持 title、description，title 可以是单行或多行。\n\n验收：\n四个页面顶部都来自同一个组件；列表内容正常；构建通过。\n",[136,37045,37043],{"__ignoreMap":191},[18,37047,37049],{"id":37048},"和-ai-协作时最重要的习惯","和 AI 协作时最重要的习惯",[14,37051,37052],{},"最有效的协作方式是：我负责方向、约束和验收，AI 负责拆解、实现和验证。",[14,37054,37055],{},"后续少说：",[706,37057,37058,37061,37064,37067,37070],{},[132,37059,37060],{},"美化一下。",[132,37062,37063],{},"高级一点。",[132,37065,37066],{},"样式不对。",[132,37068,37069],{},"继续。",[132,37071,37072],{},"你看着改。",[14,37074,37075],{},"后续多说：",[706,37077,37078,37081,37084,37087,37090,37093,37096],{},[132,37079,37080],{},"哪个页面不对。",[132,37082,37083],{},"哪个模块不动。",[132,37085,37086],{},"哪些内容必须保留。",[132,37088,37089],{},"哪些文件可以改。",[132,37091,37092],{},"桌面端还是移动端优先。",[132,37094,37095],{},"改完要通过什么检查。",[132,37097,37098],{},"期望截图里应该看到什么。",[18,37100,37101],{"id":37101},"总结",[14,37103,37104],{},"这个项目里的提问质量整体是在变好的。",[14,37106,37107],{},"一开始的问题偏方向性，比如“我要做一个网站”“继续规划”。中间开始进入内容结构，比如“按费曼教学法输出模板”“保留个人理念和书单”。后面问题开始更工程化，比如“抽象公共组件”“检查列表消失原因”“顶部不变，只优化其他内容”。",[14,37109,37110],{},"后续最值得继续强化的是：把审美判断翻译成具体约束，把异常现象翻译成可复现步骤，把“我不满意”翻译成“哪里不对、哪里不动、怎样算对”。",[14,37112,37113],{},"这样 AI 才能更像一个稳定的工程协作者，而不是只根据模糊偏好猜方向。",{"title":191,"searchDepth":230,"depth":230,"links":37115},[37116,37117,37122,37129,37135,37136,37137,37138,37139,37140],{"id":36657,"depth":230,"text":36657},{"id":36677,"depth":230,"text":36677,"children":37118},[37119,37120,37121],{"id":36683,"depth":251,"text":36684},{"id":36713,"depth":251,"text":36714},{"id":36740,"depth":251,"text":36741},{"id":36771,"depth":230,"text":36771,"children":37123},[37124,37125,37126,37127,37128],{"id":36777,"depth":251,"text":36778},{"id":36806,"depth":251,"text":36807},{"id":36821,"depth":251,"text":36822},{"id":36843,"depth":251,"text":36844},{"id":36858,"depth":251,"text":36859},{"id":36887,"depth":230,"text":36887,"children":37130},[37131,37132,37133,37134],{"id":36893,"depth":251,"text":36894},{"id":36934,"depth":251,"text":36935},{"id":36955,"depth":251,"text":36956},{"id":36983,"depth":251,"text":36984},{"id":37006,"depth":230,"text":37006},{"id":37018,"depth":230,"text":37019},{"id":37028,"depth":230,"text":37029},{"id":37038,"depth":230,"text":37039},{"id":37048,"depth":230,"text":37049},{"id":37101,"depth":230,"text":37101},"2026-04-29 15:03:06 CST","汇总这个项目到目前为止提出过的问题，分析哪些提问有效、哪些提问容易造成返工，并整理后续和 AI 协作的提问模板。",{},"\u002Fnotes\u002F2026-04-29-ai-collaboration-question-review",{"title":36647,"description":37142},"复盘多元思维模型网站开发过程中和 AI 协作的提问方式，分析好问题、可改进问题和后续提问模板。","与 AI 协作完成项目的阶段性提问复盘｜个人笔记","ai-collaboration-question-review-2026-04-29","notes\u002F2026-04-29-ai-collaboration-question-review","WjZs6hp1TJWca4Odj26GabPIR_oSLYARbSbZ_YpWnYM",{"id":37152,"title":37153,"body":37154,"category":752,"date":37379,"description":37380,"extension":755,"meta":37381,"navigation":757,"order":752,"path":37382,"seo":37383,"seoDescription":37384,"seoTitle":37385,"slug":37386,"stem":37387,"__hash__":37388},"notes\u002Fnotes\u002F2026-04-28-project-question-review.md","与 AI 协作完成项目的提问复盘",{"type":8,"value":37155,"toc":37371},[37156,37161,37163,37166,37198,37200,37203,37206,37211,37214,37228,37231,37236,37239,37242,37247,37250,37252,37255,37257,37262,37265,37267,37272,37275,37277,37282,37285,37288,37291,37297,37300,37303,37309,37312,37318,37321,37324,37327,37339,37342,37368],[11,37157,37158],{},[14,37159,37160],{},"记录时间：2026-04-28 18:32:10 CST",[18,37162,36677],{"id":36677},[14,37164,37165],{},"围绕这个项目，到目前为止主要提出了这些问题和需求：",[706,37167,37168,37170,37172,37174,37176,37178,37180,37182,37184,37186,37188,37190,37192,37194,37196],{},[132,37169,36689],{},[132,37171,36692],{},[132,37173,36695],{},[132,37175,36698],{},[132,37177,36701],{},[132,37179,36704],{},[132,37181,36707],{},[132,37183,36710],{},[132,37185,36719],{},[132,37187,36722],{},[132,37189,36725],{},[132,37191,36728],{},[132,37193,36731],{},[132,37195,36734],{},[132,37197,36737],{},[18,37199,36771],{"id":36771},[14,37201,37202],{},"提得最好的问题，是那些带有明确目标、边界和验收方向的问题。",[14,37204,37205],{},"例如：",[11,37207,37208],{},[14,37209,37210],{},"把学习路径，查理芒格，出处这三个模块去掉。加上个人介绍……",[14,37212,37213],{},"这个需求很好，因为它明确说明了：",[129,37215,37216,37219,37222,37225],{},[132,37217,37218],{},"删除哪些模块。",[132,37220,37221],{},"新增什么模块。",[132,37223,37224],{},"提供了完整内容素材。",[132,37226,37227],{},"允许适当删减和增加。",[14,37229,37230],{},"再比如：",[11,37232,37233],{},[14,37234,37235],{},"这里面关于每个思维模型的解释我希望你能按照费曼教学的方式解释。请你先写输出一个模板出来。我觉得合适再全部替换。",[14,37237,37238],{},"这个问题也很好，因为它没有直接要求批量改所有内容，而是先确认模板，再规模化替换。这是很好的产品开发节奏。",[14,37240,37241],{},"还有：",[11,37243,37244],{},[14,37245,37246],{},"所有的思维模型入口都没有了，检查一下哪里出了问题，并且进行修改。",[14,37248,37249],{},"这类问题也有效，因为它提供了截图，说明了现象，并明确要求排查和修复。",[18,37251,36887],{"id":36887},[14,37253,37254],{},"主要问题不是“差”，而是有些需求表达偏主观，缺少可执行标准。",[14,37256,37205],{},[11,37258,37259],{},[14,37260,37261],{},"把自己当成一位资深 UI 设计师，把整体页面样式进行美化。",[14,37263,37264],{},"这个方向是清楚的，但“美化”很主观。不同设计师会给出完全不同的结果。AI 可以执行，但容易出现反复调整。",[14,37266,37230],{},[11,37268,37269],{},[14,37270,37271],{},"现在的个人介绍页面不符合我的预期。我喜欢极简的样式，突出重点。",[14,37273,37274],{},"这比“美化”更好，但仍然缺少具体参照。比如喜欢哪种极简：苹果官网式、Notion 文档式、个人简历式、高级杂志式，还是作品集式？如果没有参照，AI 只能根据经验判断。",[14,37276,37241],{},[11,37278,37279],{},[14,37280,37281],{},"继续",[14,37283,37284],{},"这个在上下文明确时可以使用，但如果中间经历了多个阶段，“继续”会有歧义。它可能表示继续规划、继续开发、继续改 UI，也可能表示继续补内容。",[18,37286,37287],{"id":37287},"后续如何更高效提问",[14,37289,37290],{},"后续可以按这个结构提问：",[186,37292,37295],{"className":37293,"code":37294,"language":1074,"meta":191},[1072],"目标：\n我希望这个页面达到什么效果。\n\n当前问题：\n现在哪里不满意。\n\n修改范围：\n只改哪个页面 \u002F 哪些组件 \u002F 是否允许改全局样式。\n\n风格参考：\n喜欢什么风格，不喜欢什么风格。\n\n保留内容：\n哪些内容必须保留，哪些可以删减。\n\n验收标准：\n改完后我希望看到什么结果。\n",[136,37296,37294],{"__ignoreMap":191},[18,37298,37299],{"id":37299},"推荐提问模板",[14,37301,37302],{},"如果后续继续改个人介绍页，可以这样说：",[186,37304,37307],{"className":37305,"code":37306,"language":1074,"meta":191},[1072],"请继续优化个人介绍页。\n\n目标：\n做成极简高级的个人履历页，像高质量个人品牌主页，而不是传统简历。\n\n当前问题：\n现在字体偏大，信息密度不够舒服，模块之间节奏还不够干净。\n\n修改范围：\n只允许修改 \u002Fabout 页面和相关 CSS，不要改首页和模型页。\n\n必须保留：\n个人理念、价值观三行、核心标杆书单、项目经历摘要、联系方式。\n\n风格要求：\n黑白灰为主，少量金色点缀；不要大面积卡片；不要花哨背景；移动端优先阅读舒适。\n\n验收标准：\n首屏能快速看出我是谁，后面能清楚看到能力、项目、理念和书单。\n",[136,37308,37306],{"__ignoreMap":191},[14,37310,37311],{},"如果是修复功能，可以这样说：",[186,37313,37316],{"className":37314,"code":37315,"language":1074,"meta":191},[1072],"请修复首页模型入口问题。\n\n现象：\n首页“多元思维模型”区域没有模型卡片。\n\n期望：\n首页展示前 6 个模型，每个卡片能跳转详情页。\n\n检查范围：\n请检查首页查询逻辑、Nuxt Content 数据、模型 frontmatter、样式隐藏问题。\n\n验收：\n首页能看到模型卡片，\u002Fmodels 页面正常，npm run generate 通过。\n",[136,37317,37315],{"__ignoreMap":191},[18,37319,37320],{"id":37320},"最有效的互动方式",[14,37322,37323],{},"最有效的方式是：你负责判断方向和审美，AI 负责拆解、实现和验证。",[14,37325,37326],{},"后续可以少说：",[129,37328,37329,37331,37333,37336],{},[132,37330,37060],{},[132,37332,37063],{},[132,37334,37335],{},"继续改。",[132,37337,37338],{},"不符合预期。",[14,37340,37341],{},"多说：",[129,37343,37344,37347,37350,37353,37356,37359,37362,37365],{},[132,37345,37346],{},"字体再小一点。",[132,37348,37349],{},"间距再收紧。",[132,37351,37352],{},"删除这个模块。",[132,37354,37355],{},"这个内容必须保留。",[132,37357,37358],{},"页面更像个人品牌页，不像简历。",[132,37360,37361],{},"不要卡片堆叠。",[132,37363,37364],{},"移动端优先。",[132,37366,37367],{},"首页只保留模型入口和个人介绍入口。",[14,37369,37370],{},"总体来看，这个项目的提问质量是在逐步变好的：一开始偏方向性，后面开始能指出具体页面、具体文案、具体模块和具体视觉问题。这对实际开发非常关键。",{"title":191,"searchDepth":230,"depth":230,"links":37372},[37373,37374,37375,37376,37377,37378],{"id":36677,"depth":230,"text":36677},{"id":36771,"depth":230,"text":36771},{"id":36887,"depth":230,"text":36887},{"id":37287,"depth":230,"text":37287},{"id":37299,"depth":230,"text":37299},{"id":37320,"depth":230,"text":37320},"2026-04-28 18:32:10 CST","复盘这个多元思维模型网站开发过程中提出过的问题、有效提问方式和后续改进方向。",{},"\u002Fnotes\u002F2026-04-28-project-question-review",{"title":37153,"description":37380},"总结多元思维模型网站开发过程中的提问质量，分析哪些问题有效、哪些问题需要改进，以及如何更高效地和 AI 协作。","与 AI 协作完成项目的提问复盘｜个人笔记","project-question-review","notes\u002F2026-04-28-project-question-review","3EojtiEq62lekhpxWtha-JfgOksPg6Q4kd2FJbO1oAM",1778291658884]