<style type=\'text/css\'>html {overflow-x: initial !important;}:root { --bg-color: #ffffff; --text-color: #333333; --select-text-bg-color: #B5D6FC; --select-text-font-color: auto; --monospace: \\"Lucida Console\\",Consolas,\\"Courier\\",monospace; --title-bar-height: 20px; }\\n.mac-os-11 { --title-bar-height: 28px; }\\nhtml { font-size: 14px; background-color: var(--bg-color); color: var(--text-color); font-family: \\"Helvetica Neue\\", Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; }\\nbody { margin: 0px; padding: 0px; height: auto; inset: 0px; font-size: 1rem; line-height: 1.42857143; overflow-x: hidden; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: inherit; tab-size: 4; background-position: inherit; background-repeat: inherit; }\\niframe { margin: auto; }\\na.url { word-break: break-all; }\\na:active, a:hover { outline: 0px; }\\n.in-text-selection, ::selection { text-shadow: none; background: var(--select-text-bg-color); color: var(--select-text-font-color); }\\n#write { margin: 0px auto; height: auto; width: inherit; word-break: normal; word-wrap: break-word; position: relative; white-space: normal; overflow-x: visible; padding-top: 36px; }\\n#write.first-line-indent p { text-indent: 2em; }\\n#write.first-line-indent li p, #write.first-line-indent p * { text-indent: 0px; }\\n#write.first-line-indent li { margin-left: 2em; }\\n.for-image #write { padding-left: 8px; padding-right: 8px; }\\nbody.typora-export { padding-left: 30px; padding-right: 30px; }\\n.typora-export .footnote-line, .typora-export li, .typora-export p { white-space: pre-wrap; }\\n.typora-export .task-list-item input { pointer-events: none; }\\n@media screen and (max-width: 500px) { \\n body.typora-export { padding-left: 0px; padding-right: 0px; }\\n #write { padding-left: 20px; padding-right: 20px; }\\n .CodeMirror-sizer { margin-left: 0px !important; }\\n .CodeMirror-gutters { display: none !important; }\\n}\\n#write li > figure:last-child { margin-bottom: 0.5rem; }\\n#write ol, #write ul { position: relative; }\\nimg { max-width: 100%; vertical-align: middle; image-orientation: from-image; }\\nbutton, input, select, textarea { color: inherit; font-family: inherit; font-size: inherit; font-style: inherit; font-variant-caps: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; }\\ninput[type=\\"checkbox\\"], input[type=\\"radio\\"] { line-height: normal; padding: 0px; }\\n*, ::after, ::before { box-sizing: border-box; }\\n#write h1, #write h2, #write h3, #write h4, #write h5, #write h6, #write p, #write pre { width: inherit; }\\n#write h1, #write h2, #write h3, #write h4, #write h5, #write h6, #write p { position: relative; }\\np { line-height: inherit; }\\nh1, h2, h3, h4, h5, h6 { break-after: avoid-page; break-inside: avoid; orphans: 4; }\\np { orphans: 4; }\\nh1 { font-size: 2rem; }\\nh2 { font-size: 1.8rem; }\\nh3 { font-size: 1.6rem; }\\nh4 { font-size: 1.4rem; }\\nh5 { font-size: 1.2rem; }\\nh6 { font-size: 1rem; }\\n.md-math-block, .md-rawblock, h1, h2, h3, h4, h5, h6, p { margin-top: 1rem; margin-bottom: 1rem; }\\n.hidden { display: none; }\\n.md-blockmeta { color: rgb(204, 204, 204); font-weight: 700; font-style: italic; }\\na { cursor: pointer; }\\nsup.md-footnote { padding: 2px 4px; background-color: rgba(238, 238, 238, 0.7); color: rgb(85, 85, 85); border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; cursor: pointer; }\\nsup.md-footnote a, sup.md-footnote a:hover { color: inherit; text-transform: inherit; text-decoration: inherit; }\\n#write input[type=\\"checkbox\\"] { cursor: pointer; width: inherit; height: inherit; }\\nfigure { overflow-x: auto; margin: 1.2em 0px; max-width: calc(100% + 16px); padding: 0px; }\\nfigure > table { margin: 0px; }\\ntr { break-inside: avoid; break-after: auto; }\\nthead { display: table-header-group; }\\ntable { border-collapse: collapse; border-spacing: 0px; width: 100%; overflow: auto; break-inside: auto; text-align: left; }\\ntable.md-table td { min-width: 32px; }\\n.CodeMirror-gutters { border-right-width: 0px; background-color: inherit; }\\n.CodeMirror-linenumber { }\\n.CodeMirror { text-align: left; }\\n.CodeMirror-placeholder { opacity: 0.3; }\\n.CodeMirror pre { padding: 0px 4px; }\\n.CodeMirror-lines { padding: 0px; }\\ndiv.hr:focus { cursor: none; }\\n#write pre { white-space: pre-wrap; }\\n#write.fences-no-line-wrapping pre { white-space: pre; }\\n#write pre.ty-contain-cm { white-space: normal; }\\n.CodeMirror-gutters { margin-right: 4px; }\\n.md-fences { font-size: 0.9rem; display: block; break-inside: avoid; text-align: left; overflow: visible; white-space: pre; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: inherit; position: relative !important; background-position: inherit; background-repeat: inherit; }\\n.md-fences-adv-panel { width: 100%; margin-top: 10px; text-align: center; padding-top: 0px; padding-bottom: 8px; overflow-x: auto; }\\n#write .md-fences.mock-cm { white-space: pre-wrap; }\\n.md-fences.md-fences-with-lineno { padding-left: 0px; }\\n#write.fences-no-line-wrapping .md-fences.mock-cm { white-space: pre; overflow-x: auto; }\\n.md-fences.mock-cm.md-fences-with-lineno { padding-left: 8px; }\\n.CodeMirror-line, twitterwidget { break-inside: avoid; }\\n.footnotes { opacity: 0.8; font-size: 0.9rem; margin-top: 1em; margin-bottom: 1em; }\\n.footnotes + .footnotes { margin-top: 0px; }\\n.md-reset { margin: 0px; padding: 0px; border: 0px; outline: 0px; vertical-align: top; text-decoration: none; text-shadow: none; float: none; position: static; width: auto; height: auto; white-space: nowrap; cursor: inherit; line-height: normal; font-weight: 400; text-align: left; box-sizing: content-box; direction: ltr; background-position: 0px 0px; }\\nli div { padding-top: 0px; }\\nblockquote { margin: 1rem 0px; }\\nli .mathjax-block, li p { margin: 0.5rem 0px; }\\nli blockquote { margin: 1rem 0px; }\\nli { margin: 0px; position: relative; }\\nblockquote > :last-child { margin-bottom: 0px; }\\nblockquote > :first-child, li > :first-child { margin-top: 0px; }\\n.footnotes-area { color: rgb(136, 136, 136); margin-top: 0.714rem; padding-bottom: 0.143rem; white-space: normal; }\\n#write .footnote-line { white-space: pre-wrap; }\\n@media print { \\n body, html { border: 1px solid transparent; height: 99%; break-after: avoid; break-before: avoid; font-variant-ligatures: no-common-ligatures; }\\n #write { margin-top: 0px; padding-top: 0px; border-color: transparent !important; }\\n .typora-export * { -webkit-print-color-adjust: exact; }\\n .typora-export #write { break-after: avoid; }\\n .typora-export #write::after { height: 0px; }\\n .is-mac table { break-inside: avoid; }\\n .typora-export-show-outline .typora-export-sidebar { display: none; }\\n}\\n.footnote-line { margin-top: 0.714em; font-size: 0.7em; }\\na img, img a { cursor: pointer; }\\npre.md-meta-block { font-size: 0.8rem; min-height: 0.8rem; white-space: pre-wrap; background-color: rgb(204, 204, 204); display: block; overflow-x: hidden; }\\np > .md-image:only-child:not(.md-img-error) img, p > img:only-child { display: block; margin: auto; }\\n#write.first-line-indent p > .md-image:only-child:not(.md-img-error) img { left: -2em; position: relative; }\\np > .md-image:only-child { display: inline-block; width: 100%; }\\n#write .MathJax_Display { margin: 0.8em 0px 0px; }\\n.md-math-block { width: 100%; }\\n.md-math-block:not(:empty)::after { display: none; }\\n.MathJax_ref { fill: currentcolor; }\\n[contenteditable=\\"true\\"]:active, [contenteditable=\\"true\\"]:focus, [contenteditable=\\"false\\"]:active, [contenteditable=\\"false\\"]:focus { outline: 0px; box-shadow: none; }\\n.md-task-list-item { position: relative; list-style-type: none; }\\n.task-list-item.md-task-list-item { padding-left: 0px; }\\n.md-task-list-item > input { position: absolute; top: 0px; left: 0px; margin-left: -1.2em; margin-top: calc(1em - 10px); border: none; }\\n.math { font-size: 1rem; }\\n.md-toc { min-height: 3.58rem; position: relative; font-size: 0.9rem; border-top-left-radius: 10px; border-top-right-radius: 10px; border-bottom-right-radius: 10px; border-bottom-left-radius: 10px; }\\n.md-toc-content { position: relative; margin-left: 0px; }\\n.md-toc-content::after, .md-toc::after { display: none; }\\n.md-toc-item { display: block; color: rgb(65, 131, 196); }\\n.md-toc-item a { text-decoration: none; }\\n.md-toc-inner:hover { text-decoration: underline; }\\n.md-toc-inner { display: inline-block; cursor: pointer; }\\n.md-toc-h1 .md-toc-inner { margin-left: 0px; font-weight: 700; }\\n.md-toc-h2 .md-toc-inner { margin-left: 2em; }\\n.md-toc-h3 .md-toc-inner { margin-left: 4em; }\\n.md-toc-h4 .md-toc-inner { margin-left: 6em; }\\n.md-toc-h5 .md-toc-inner { margin-left: 8em; }\\n.md-toc-h6 .md-toc-inner { margin-left: 10em; }\\n@media screen and (max-width: 48em) { \\n .md-toc-h3 .md-toc-inner { margin-left: 3.5em; }\\n .md-toc-h4 .md-toc-inner { margin-left: 5em; }\\n .md-toc-h5 .md-toc-inner { margin-left: 6.5em; }\\n .md-toc-h6 .md-toc-inner { margin-left: 8em; }\\n}\\na.md-toc-inner { font-size: inherit; font-style: inherit; font-weight: inherit; line-height: inherit; }\\n.footnote-line a:not(.reversefootnote) { color: inherit; }\\n.md-attr { display: none; }\\n.md-fn-count::after { content: \\".\\"; }\\ncode, pre, samp, tt { font-family: var(--monospace); }\\nkbd { margin: 0px 0.1em; padding: 0.1em 0.6em; font-size: 0.8em; color: rgb(36, 39, 41); background-color: rgb(255, 255, 255); border: 1px solid rgb(173, 179, 185); border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; box-shadow: rgba(13, 13, 14, 0.2) 0px 1px 0px, rgb(255, 255, 255) 0px 0px 0px 2px inset; white-space: nowrap; vertical-align: middle; }\\n.md-comment { color: rgb(162, 137, 3); opacity: 0.8; font-family: var(--monospace); }\\ncode { text-align: left; }\\na.md-print-anchor { white-space: pre !important; border: none !important; display: inline-block !important; position: absolute !important; width: 1px !important; right: 0px !important; outline: 0px !important; text-shadow: initial !important; background-position: 0px 0px !important; }\\n.os-windows.monocolor-emoji .md-emoji { font-family: \\"Segoe UI Symbol\\", sans-serif; }\\n.md-diagram-panel > svg { max-width: 100%; }\\n[lang=\\"flow\\"] svg, [lang=\\"mermaid\\"] svg { max-width: 100%; height: auto; }\\n[lang=\\"mermaid\\"] .node text { font-size: 1rem; }\\ntable tr th { border-bottom-width: 0px; }\\nvideo { max-width: 100%; display: block; margin: 0px auto; }\\niframe { max-width: 100%; width: 100%; border: none; }\\n.highlight td, .highlight tr { border: 0px; }\\nmark { background-color: rgb(255, 255, 0); color: rgb(0, 0, 0); }\\n.md-html-inline .md-plain, .md-html-inline strong, mark .md-inline-math, mark strong { color: inherit; }\\n.md-expand mark .md-meta { opacity: 0.3 !important; }\\nmark .md-meta { color: rgb(0, 0, 0); }\\n@media print { \\n .typora-export h1, .typora-export h2, .typora-export h3, .typora-export h4, .typora-export h5, .typora-export h6 { break-inside: avoid; }\\n}\\n.md-diagram-panel .messageText { stroke: none !important; }\\n.md-diagram-panel .start-state { fill: var(--node-fill); }\\n.md-diagram-panel .edgeLabel rect { opacity: 1 !important; }\\n.md-require-zoom-fix foreignObject { font-size: var(--mermaid-font-zoom); }\\n.md-fences.md-fences-math { font-size: 1em; }\\n.md-fences-advanced:not(.md-focus) { padding: 0px; white-space: nowrap; border: 0px; }\\n.md-fences-advanced:not(.md-focus) { background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: inherit; background-position: inherit; background-repeat: inherit; }\\n.typora-export-show-outline .typora-export-content { max-width: 1440px; margin: auto; display: flex; flex-direction: row; }\\n.typora-export-sidebar { width: 300px; font-size: 0.8rem; margin-top: 80px; margin-right: 18px; }\\n.typora-export-show-outline #write { --webkit-flex: 2; flex: 2 1 0%; }\\n.typora-export-sidebar .outline-content { position: fixed; top: 0px; max-height: 100%; overflow: hidden auto; padding-bottom: 30px; padding-top: 60px; width: 300px; }\\n@media screen and (max-width: 1024px) { \\n .typora-export-sidebar, .typora-export-sidebar .outline-content { width: 240px; }\\n}\\n@media screen and (max-width: 800px) { \\n .typora-export-sidebar { display: none; }\\n}\\n.outline-content li, .outline-content ul { margin-left: 0px; margin-right: 0px; padding-left: 0px; padding-right: 0px; list-style: none; }\\n.outline-content ul { margin-top: 0px; margin-bottom: 0px; }\\n.outline-content strong { font-weight: 400; }\\n.outline-expander { width: 1rem; height: 1.428571429rem; position: relative; display: table-cell; vertical-align: middle; cursor: pointer; padding-left: 4px; }\\n.outline-expander::before { content: \'\'; position: relative; font-family: Ionicons; display: inline-block; font-size: 8px; vertical-align: middle; }\\n.outline-item { padding-top: 3px; padding-bottom: 3px; cursor: pointer; }\\n.outline-expander:hover::before { content: \'\'; }\\n.outline-h1 > .outline-item { padding-left: 0px; }\\n.outline-h2 > .outline-item { padding-left: 1em; }\\n.outline-h3 > .outline-item { padding-left: 2em; }\\n.outline-h4 > .outline-item { padding-left: 3em; }\\n.outline-h5 > .outline-item { padding-left: 4em; }\\n.outline-h6 > .outline-item { padding-left: 5em; }\\n.outline-label { cursor: pointer; display: table-cell; vertical-align: middle; text-decoration: none; color: inherit; }\\n.outline-label:hover { text-decoration: underline; }\\n.outline-item:hover { border-color: rgb(245, 245, 245); background-color: var(--item-hover-bg-color); }\\n.outline-item:hover { margin-left: -28px; margin-right: -28px; border-left-width: 28px; border-left-style: solid; border-left-color: transparent; border-right-width: 28px; border-right-style: solid; border-right-color: transparent; }\\n.outline-item-single .outline-expander::before, .outline-item-single .outline-expander:hover::before { display: none; }\\n.outline-item-open > .outline-item > .outline-expander::before { content: \'\'; }\\n.outline-children { display: none; }\\n.info-panel-tab-wrapper { display: none; }\\n.outline-item-open > .outline-children { display: block; }\\n.typora-export .outline-item { padding-top: 1px; padding-bottom: 1px; }\\n.typora-export .outline-item:hover { margin-right: -8px; border-right-width: 8px; border-right-style: solid; border-right-color: transparent; }\\n.typora-export .outline-expander::before { content: \\"+\\"; font-family: inherit; top: -1px; }\\n.typora-export .outline-expander:hover::before, .typora-export .outline-item-open > .outline-item > .outline-expander::before { content: \'−\'; }\\n.typora-export-collapse-outline .outline-children { display: none; }\\n.typora-export-collapse-outline .outline-item-open > .outline-children, .typora-export-no-collapse-outline .outline-children { display: block; }\\n.typora-export-no-collapse-outline .outline-expander::before { content: \\"\\" !important; }\\n.typora-export-show-outline .outline-item-active > .outline-item .outline-label { font-weight: 700; }\\n.md-inline-math-container mjx-container { zoom: 0.95; }\\n\\n\\n/* meyer reset -- http://meyerweb.com/eric/tools/css/reset/ , v2.0 | 20110136 | License: none (public domain) */\\n\\n@include-when-export url(https://fonts.loli.net/css?family=PT+Serif:400,400italic,700,700italic&subset=latin,cyrillic-ext,cyrillic,latin-ext);\\n\\n/* =========== */\\n\\n/* pt-serif-regular - latin */\\n/* pt-serif-italic - latin */\\n/* pt-serif-700 - latin */\\n/* pt-serif-700italic - latin */\\n:root {\\n--active-file-bg-color: #dadada;\\n--active-file-bg-color: rgba(32, 43, 51, 0.63);\\n--active-file-text-color: white;\\n--bg-color: #f3f2ee;\\n--text-color: #1f0909;\\n--control-text-color: #444;\\n--rawblock-edit-panel-bd: #e5e5e5;\\n\\n--select-text-bg-color: rgba(32, 43, 51, 0.63);\\n --select-text-font-color: white;\\n}\\n\\npre {\\n--select-text-bg-color: #36284e;\\n--select-text-font-color: #fff;\\n}\\n\\nhtml {\\nfont-size: 16px;\\n-webkit-font-smoothing: antialiased;\\n}\\n\\nhtml, body {\\nbackground-color: #f3f2ee;\\nfont-family: \\"PT Serif\\", \'Times New Roman\', Times, serif;\\ncolor: #1f0909;\\nline-height: 1.5em;\\n}\\n\\n/*#write {\\noverflow-x: auto;\\n max-width: initial;\\npadding-left: calc(50% - 17em);\\n padding-right: calc(50% - 17em);\\n}\\n\\n@media (max-width: 36em) {\\n #write {\\n padding-left: 1em;\\n padding-right: 1em;\\n }\\n}*/\\n\\n#write {\\nmax-width: 40em;\\n}\\n\\n@media only screen and (min-width: 1400px) {\\n#write {\\nmax-width: 914px;\\n}\\n}\\n\\nol li {\\nlist-style-type: decimal;\\nlist-style-position: outside;\\n}\\nul li {\\nlist-style-type: disc;\\nlist-style-position: outside;\\n}\\n\\nol,\\nul {\\nlist-style: none;\\n}\\n\\nblockquote,\\nq {\\nquotes: none;\\n}\\nblockquote:before,\\nblockquote:after,\\nq:before,\\nq:after {\\ncontent: \'\';\\ncontent: none;\\n}\\ntable {\\nborder-collapse: collapse;\\nborder-spacing: 0;\\n}\\n/* styles */\\n\\n/* ====== */\\n\\n/* headings */\\n\\nh1,\\nh2,\\nh3,\\nh4,\\nh5,\\nh6 {\\nfont-weight: bold;\\n}\\nh1 {\\nfont-size: 1.875em;\\n/*30 / 16*/\\nline-height: 1.6em;\\n/* 48 / 30*/\\nmargin-top: 2em;\\n}\\nh2,\\nh3 {\\nfont-size: 1.3135em;\\n/*21 / 16*/\\nline-height: 1.15;\\n/*24 / 21*/\\nmargin-top: 2.285714em;\\n/*48 / 21*/\\nmargin-bottom: 1.15em;\\n/*24 / 21*/\\n}\\nh3 {\\nfont-weight: normal;\\n}\\nh4 {\\nfont-size: 1.135em;\\n/*18 / 16*/\\nmargin-top: 2.67em;\\n/*48 / 18*/\\n}\\nh5,\\nh6 {\\nfont-size: 1em;\\n/*16*/\\n}\\nh1 {\\nborder-bottom: 1px solid;\\nmargin-bottom: 1.875em;\\npadding-bottom: 0.8135em;\\n}\\n/* links */\\n\\na {\\ntext-decoration: none;\\ncolor: #065588;\\n}\\na:hover,\\na:active {\\ntext-decoration: underline;\\n}\\n/* block spacing */\\n\\np,\\nblockquote,\\n.md-fences {\\nmargin-bottom: 1.5em;\\n}\\nh1,\\nh2,\\nh3,\\nh4,\\nh5,\\nh6 {\\nmargin-bottom: 1.5em;\\n}\\n/* blockquote */\\n\\nblockquote {\\nfont-style: italic;\\nborder-left: 5px solid;\\nmargin-left: 2em;\\npadding-left: 1em;\\n}\\n/* lists */\\n\\nul,\\nol {\\nmargin: 0 0 1.5em 1.5em;\\n}\\n/* tables */\\n.md-meta,.md-before, .md-after {\\ncolor:#999;\\n}\\n\\ntable {\\nmargin-bottom: 1.5em;\\n/*24 / 16*/\\nfont-size: 1em;\\n/* width: 100%; */\\n}\\nthead th,\\ntfoot th {\\npadding: .25em .25em .25em .4em;\\ntext-transform: uppercase;\\n}\\nth {\\ntext-align: left;\\n}\\ntd {\\nvertical-align: top;\\npadding: .25em .25em .25em .4em;\\n}\\n\\ncode,\\n.md-fences {\\nbackground-color: #dadada;\\n}\\n\\ncode {\\npadding-left: 2px;\\npadding-right: 2px;\\n}\\n\\n.md-fences {\\nmargin-left: 2em;\\nmargin-bottom: 3em;\\npadding-left: 1ch;\\npadding-right: 1ch;\\n}\\n\\npre,\\ncode,\\ntt {\\nfont-size: .875em;\\nline-height: 1.714285em;\\n}\\n/* some fixes */\\n\\nh1 {\\nline-height: 1.3em;\\nfont-weight: normal;\\nmargin-bottom: 0.5em;\\n}\\n\\np + ul,\\np + ol{\\nmargin-top: .5em;\\n}\\n\\nh3 + ul,\\nh4 + ul,\\nh5 + ul,\\nh6 + ul,\\nh3 + ol,\\nh4 + ol,\\nh5 + ol,\\nh6 + ol {\\nmargin-top: .5em;\\n}\\n\\nli > ul,\\nli > ol {\\nmargin-top: inherit;\\nmargin-bottom: 0;\\n}\\n\\nli ol>li {\\nlist-style-type: lower-alpha;\\n}\\n\\nli li ol>li{\\nlist-style-type: lower-roman;\\n}\\n\\nh2,\\nh3 {\\nmargin-bottom: .75em;\\n}\\nhr {\\nborder-top: none;\\nborder-right: none;\\nborder-bottom: 1px solid;\\nborder-left: none;\\n}\\nh1 {\\nborder-color: #c5c5c5;\\n}\\nblockquote {\\nborder-color: #bababa;\\ncolor: #656565;\\n}\\n\\nblockquote ul,\\nblockquote ol {\\nmargin-left:0;\\n}\\n\\n.ty-table-edit {\\nbackground-color: transparent;\\n}\\nthead {\\nbackground-color: #dadada;\\n}\\ntr:nth-child(even) {\\nbackground: #e8e7e7;\\n}\\nhr {\\nborder-color: #c5c5c5;\\n}\\n.task-list{\\npadding-left: 1rem;\\n}\\n\\n.md-task-list-item {\\npadding-left: 1.5rem;\\nlist-style-type: none;\\n}\\n\\n.md-task-list-item > input:before {\\ncontent: \'\\\\221A\';\\ndisplay: inline-block;\\nwidth: 1.25rem;\\n height: 1.6rem;\\nvertical-align: middle;\\ntext-align: center;\\ncolor: #ddd;\\nbackground-color: #F3F2EE;\\n}\\n\\n.md-task-list-item > input:checked:before,\\n.md-task-list-item > input[checked]:before{\\ncolor: inherit;\\n}\\n\\n#write pre.md-meta-block {\\nmin-height: 1.875rem;\\ncolor: #555;\\nborder: 0px;\\nbackground: transparent;\\nmargin-top: -4px;\\nmargin-left: 1em;\\nmargin-top: 1em;\\n}\\n\\n.md-image>.md-meta {\\ncolor: #9B5146;\\n}\\n\\n.md-image>.md-meta{\\nfont-family: Menlo, \'Ubuntu Mono\', Consolas, \'Courier New\', \'Microsoft Yahei\', \'Hiragino Sans GB\', \'WenQuanYi Micro Hei\', serif;\\n}\\n\\n\\n#write>h3.md-focus:before{\\nleft: -1.5rem;\\ncolor:#999;\\nborder-color:#999;\\n}\\n#write>h4.md-focus:before{\\nleft: -1.5rem;\\ntop: .25rem;\\ncolor:#999;\\nborder-color:#999;\\n}\\n#write>h5.md-focus:before{\\nleft: -1.5rem;\\ntop: .0.3135rem;\\ncolor:#999;\\nborder-color:#999;\\n}\\n#write>h6.md-focus:before{\\nleft: -1.5rem;\\ntop: 0.3135rem;\\ncolor:#999;\\nborder-color:#999;\\n}\\n\\n.md-toc:focus .md-toc-content{\\nmargin-top: 19px;\\n}\\n\\n.md-toc-content:empty:before{\\ncolor: #065588;\\n}\\n.md-toc-item {\\ncolor: #065588;\\n}\\n#write div.md-toc-tooltip {\\nbackground-color: #f3f2ee;\\n}\\n\\n#typora-sidebar {\\nbackground-color: #f3f2ee;\\n-webkit-box-shadow: 0 6px 13px rgba(0, 0, 0, 0.375);\\n box-shadow: 0 6px 13px rgba(0, 0, 0, 0.375);\\n}\\n\\n.pin-outline #typora-sidebar {\\nbackground: inherit;\\nbox-shadow: none;\\nborder-right: 1px dashed;\\n}\\n\\n.pin-outline #typora-sidebar:hover .outline-title-wrapper {\\nborder-left:1px dashed;\\n}\\n\\n.outline-item:hover {\\n background-color: #dadada;\\n border-left: 28px solid #dadada;\\n border-right: 18px solid #dadada;\\n}\\n\\n.typora-node .outline-item:hover {\\n border-right: 28px solid #dadada;\\n}\\n\\n.outline-expander:before {\\n content: \\"\\\\f0da\\";\\n font-family: FontAwesome;\\n font-size:14px;\\n top: 1px;\\n}\\n\\n.outline-expander:hover:before,\\n.outline-item-open>.outline-item>.outline-expander:before {\\n content: \\"\\\\f0d7\\";\\n}\\n\\n.modal-content {\\nbackground-color: #f3f2ee;\\n}\\n\\n.auto-suggest-container ul li {\\nlist-style-type: none;\\n}\\n\\n/** UI for electron */\\n\\n.megamenu-menu,\\n#top-titlebar, #top-titlebar *,\\n.megamenu-content {\\nbackground: #f3f2ee;\\ncolor: #1f0909;\\n}\\n\\n.megamenu-menu-header {\\nborder-bottom: 1px dashed #202B33;\\n}\\n\\n.megamenu-menu {\\nbox-shadow: none;\\nborder-right: 1px dashed;\\n}\\n\\nheader, .context-menu, .megamenu-content, footer {\\nfont-family: \\"PT Serif\\", \'Times New Roman\', Times, serif;\\n color: #1f0909;\\n}\\n\\n#megamenu-back-btn {\\ncolor: #1f0909;\\nborder-color: #1f0909;\\n}\\n\\n.megamenu-menu-header #megamenu-menu-header-title:before {\\ncolor: #1f0909;\\n}\\n\\n.megamenu-menu-list li a:hover, .megamenu-menu-list li a.active {\\ncolor: inherit;\\nbackground-color: #e8e7df;\\n}\\n\\n.long-btn:hover {\\nbackground-color: #e8e7df;\\n}\\n\\n#recent-file-panel tbody tr:nth-child(2n-1) {\\n background-color: transparent !important;\\n}\\n\\n.megamenu-menu-panel tbody tr:hover td:nth-child(2) {\\n color: inherit;\\n}\\n\\n.megamenu-menu-panel .btn {\\nbackground-color: #D2D1D1;\\n}\\n\\n.btn-default {\\nbackground-color: transparent;\\n}\\n\\n.typora-sourceview-on #toggle-sourceview-btn,\\n.ty-show-word-count #footer-word-count {\\nbackground: #c7c5c5;\\n}\\n\\n#typora-quick-open {\\n background-color: inherit;\\n}\\n\\n.md-diagram-panel {\\nmargin-top: 8px;\\n}\\n\\n.file-list-item-file-name {\\nfont-weight: initial;\\n}\\n\\n.file-list-item-summary {\\nopacity: 1;\\n}\\n\\n.file-list-item {\\ncolor: #777;\\n}\\n\\n.file-list-item.active {\\nbackground-color: inherit;\\ncolor: black;\\n}\\n\\n.ty-side-sort-btn.active {\\nbackground-color: inherit;\\n}\\n\\n.file-list-item.active .file-list-item-file-name {\\nfont-weight: bold;\\n}\\n\\n.file-list-item{\\n opacity:1 !important;\\n}\\n\\n.file-library-node.active>.file-node-background{\\nbackground-color: rgba(32, 43, 51, 0.63);\\nbackground-color: var(--active-file-bg-color);\\n}\\n\\n.file-tree-node.active>.file-node-content{\\ncolor: white;\\ncolor: var(--active-file-text-color);\\n}\\n\\n.md-task-list-item>input {\\nmargin-left: -1.7em;\\nmargin-top: calc(1rem - 13px);\\n}\\n\\ninput {\\nborder: 1px solid #aaa;\\n}\\n\\n.megamenu-menu-header #megamenu-menu-header-title,\\n.megamenu-menu-header:hover, \\n.megamenu-menu-header:focus {\\ncolor: inherit;\\n}\\n\\n.dropdown-menu .divider {\\nborder-color: #e5e5e5;\\nopacity: 1;\\n}\\n\\n/* https://github.com/typora/typora-issues/issues/2046 */\\n.os-windows-7 strong,\\n.os-windows-7 strong {\\nfont-weight: 760;\\n}\\n\\n.ty-preferences .btn-default {\\nbackground: transparent;\\n}\\n\\n.ty-preferences .window-header {\\nborder-bottom: 1px dashed #202B33;\\nbox-shadow: none;\\n}\\n\\n#sidebar-loading-template, #sidebar-loading-template.file-list-item {\\ncolor: #777;\\n}\\n\\n.searchpanel-search-option-btn.active {\\nbackground: #777;\\ncolor: white;\\n}\\n\\n\\nmjx-container[jax=\\"SVG\\"] {\\n direction: ltr;\\n}\\n\\nmjx-container[jax=\\"SVG\\"] > svg {\\n overflow: visible;\\n min-height: 1px;\\n min-width: 1px;\\n}\\n\\nmjx-container[jax=\\"SVG\\"] > svg a {\\n fill: blue;\\n stroke: blue;\\n}\\n\\nmjx-assistive-mml {\\n position: absolute !important;\\n top: 0px;\\n left: 0px;\\n clip: rect(1px, 1px, 1px, 1px);\\n padding: 1px 0px 0px 0px !important;\\n border: 0px !important;\\n display: block !important;\\n width: auto !important;\\n overflow: hidden !important;\\n -webkit-touch-callout: none;\\n -webkit-user-select: none;\\n -khtml-user-select: none;\\n -moz-user-select: none;\\n -ms-user-select: none;\\n user-select: none;\\n}\\n\\nmjx-assistive-mml[display=\\"block\\"] {\\n width: 100% !important;\\n}\\n\\nmjx-container[jax=\\"SVG\\"][display=\\"true\\"] {\\n display: block;\\n text-align: center;\\n margin: 1em 0;\\n}\\n\\nmjx-container[jax=\\"SVG\\"][display=\\"true\\"][width=\\"full\\"] {\\n display: flex;\\n}\\n\\nmjx-container[jax=\\"SVG\\"][justify=\\"left\\"] {\\n text-align: left;\\n}\\n\\nmjx-container[jax=\\"SVG\\"][justify=\\"right\\"] {\\n text-align: right;\\n}\\n\\ng[data-mml-node=\\"merror\\"] > g {\\n fill: red;\\n stroke: red;\\n}\\n\\ng[data-mml-node=\\"merror\\"] > rect[data-background] {\\n fill: yellow;\\n stroke: none;\\n}\\n\\ng[data-mml-node=\\"mtable\\"] > line[data-line], svg[data-table] > g > line[data-line] {\\n stroke-width: 70px;\\n fill: none;\\n}\\n\\ng[data-mml-node=\\"mtable\\"] > rect[data-frame], svg[data-table] > g > rect[data-frame] {\\n stroke-width: 70px;\\n fill: none;\\n}\\n\\ng[data-mml-node=\\"mtable\\"] > .mjx-dashed, svg[data-table] > g > .mjx-dashed {\\n stroke-dasharray: 140;\\n}\\n\\ng[data-mml-node=\\"mtable\\"] > .mjx-dotted, svg[data-table] > g > .mjx-dotted {\\n stroke-linecap: round;\\n stroke-dasharray: 0,140;\\n}\\n\\ng[data-mml-node=\\"mtable\\"] > g > svg {\\n overflow: visible;\\n}\\n\\n[jax=\\"SVG\\"] mjx-tool {\\n display: inline-block;\\n position: relative;\\n width: 0;\\n height: 0;\\n}\\n\\n[jax=\\"SVG\\"] mjx-tool > mjx-tip {\\n position: absolute;\\n top: 0;\\n left: 0;\\n}\\n\\nmjx-tool > mjx-tip {\\n display: inline-block;\\n padding: .2em;\\n border: 1px solid #888;\\n font-size: 70%;\\n background-color: #F8F8F8;\\n color: black;\\n box-shadow: 2px 2px 5px #AAAAAA;\\n}\\n\\ng[data-mml-node=\\"maction\\"][data-toggle] {\\n cursor: pointer;\\n}\\n\\nmjx-status {\\n display: block;\\n position: fixed;\\n left: 1em;\\n bottom: 1em;\\n min-width: 25%;\\n padding: .2em .4em;\\n border: 1px solid #888;\\n font-size: 90%;\\n background-color: #F8F8F8;\\n color: black;\\n}\\n\\nforeignObject[data-mjx-xml] {\\n font-family: initial;\\n line-height: normal;\\n overflow: visible;\\n}\\n\\nmjx-container[jax=\\"SVG\\"] path[data-c], mjx-container[jax=\\"SVG\\"] use[data-c] {\\n stroke-width: 3;\\n}\\n\\ng[data-mml-node=\\"xypic\\"] path {\\n stroke-width: inherit;\\n}\\n\\n.MathJax g[data-mml-node=\\"xypic\\"] path {\\n stroke-width: inherit;\\n}\\n :root {--mermaid-font-zoom:1em ;} @media print { @page {margin: 0 0 0 0;} body.typora-export {padding-left: 0; padding-right: 0;} #write {padding:0;}} .typora-export li, .typora-export p, .typora-export, .footnote-line {white-space: normal;} \\n\\n.download-btn {\\n display: inline-block;\\n padding: 0.5em 1.5em;\\n font-size: 1em;\\n color: #fff;\\n background: linear-gradient(90deg, #202b33 60%, #444 100%);\\n border: none;\\n border-radius: 2em;\\n box-shadow: 0 2px 8px rgba(32,43,51,0.10);\\n cursor: pointer;\\n transition: background 0.2s, box-shadow 0.2s, transform 0.1s;\\n font-weight: bold;\\n letter-spacing: 0.05em;\\n margin-top: 2.5em;\\n}\\n.download-btn:hover, .download-btn:focus {\\n background: linear-gradient(90deg, #2d3a45 60%, #666 100%);\\n box-shadow: 0 4px 16px rgba(32,43,51,0.18);\\n transform: translateY(-2px) scale(1.03);\\n outline: none;\\n}\\n</style><title>华为鸿蒙内测包下载</title>\\n <script>\\n function openDeepLink() {\\n let url = \'store://enterprise/manifest?url=https://pfile2.laiyouxi.com/updategame/online/common/ios/ohos/app/laiyouxi/14/manifest.json5\'\\n window.open(url, \'_parent\')\\n }\\n </script>\\n</head>\\n<body class=\'typora-export\'><div class=\'typora-export-content\'>\\n<div id=\'write\' class=\'\'><h1 id=\'华为鸿蒙内测包下载\'><span>华为鸿蒙内测包下载</span></h1><h2 id=\'XXX App【鸿蒙版】\'><span>XXX App【鸿蒙版】</span></h2>\\n<ul style=\\"margin-top:0.5em;margin-bottom:2em;padding-left:0em;\\">\\n <li>版本:1.0.0</li>\\n <li>服务器环境:测试服</li>\\n <li>构建序号:13</li>\\n</ul>\\n<button class=\\"download-btn\\" onclick=\\"openDeepLink()\\">立即下载</button>\\n</body>\\n</html>\\n\\n","description":"问题 鸿蒙包的分发无法像安卓包那样仅提供一个apk文件的下载地址就可以安装。\\n\\n一段时间以来,鸿蒙包安装仅依赖华为官方提供的工具链hdc,这需要鸿蒙设备借助数据线连接到能运行hdc命令的电脑,再使用命令行或者DevEco安装,安装过程过于繁琐,不方便内部测试。\\n\\n解决方案\\n\\n近期,华为官方新增了“内部测试”的Profile,用于内部测试阶段分发。\\n\\n我们很容易可以发现,新增的“内部测试”类型区别于“发布”和“调试”类型,这其中必然有其原因。\\n\\n这个新增的描述文件类型不禁让我联想到 iOS App 的Ad-hoc分发方式,仔细翻阅华为官方文档后印证了我的想法…","guid":"https://juejin.cn/post/7507206037431681036","author":"郑知鱼","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-22T12:11:19.200Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/969c780f44c24236b15fc230295b42d4~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6YOR55-l6bG8:q75.awebp?rk3s=f64ab15b&x-expires=1748574984&x-signature=bz7oAyr5QVTEMRY9SvnmJ74ncpA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c53614e49d444e498f9e16f90ffce574~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6YOR55-l6bG8:q75.awebp?rk3s=f64ab15b&x-expires=1748574984&x-signature=L5yvcEujxElbUmyEdte%2B2RtiGL8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/96dd79f0c86d41399bdf22f16f5cb337~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6YOR55-l6bG8:q75.awebp?rk3s=f64ab15b&x-expires=1748574984&x-signature=SWBPMqO2gcP2QYt8uw09CuqAlpA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9bbf748ef82c418691bc8d3393ac3011~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6YOR55-l6bG8:q75.awebp?rk3s=f64ab15b&x-expires=1748574984&x-signature=AIOP0OmbD45M62DYmX6yXqiq%2FpY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f445fd06c9174dd2a265725778694d5c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6YOR55-l6bG8:q75.awebp?rk3s=f64ab15b&x-expires=1748574984&x-signature=PKAMIsa5XXodZnqart2tGbHdOLs%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","HarmonyOS","前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"flutter常用命令","url":"https://juejin.cn/post/7506841697281572890","content":"
以下是 Flutter 开发中常用的命令分类整理,结合了多个来源的最新信息:
\\n环境检查
\\nflutter doctor
:检查开发环境配置(Android/iOS/Xcode等)flutter doctor -v
:显示详细环境信息版本控制
\\nflutter --version
:查看 Flutter 和 Dart 版本flutter upgrade
:升级 Flutter SDK 和依赖包flutter channel
:查看/切换 Flutter 渠道(如 stable/beta/dev)flutter devices
:列出已连接的设备flutter emulators
:管理模拟器(启动/创建)flutter run -d <device_id>
:指定设备运行应用创建与初始化
\\nflutter create <project_name>
:新建项目flutter create --platforms=windows,macos,linux .
:为旧项目添加桌面平台支持依赖管理
\\nflutter pub get
:安装/更新依赖flutter pub add <package>
:添加新依赖(自动更新 pubspec.yaml)flutter pub upgrade
:升级依赖到最新兼容版本代码分析
\\nflutter analyze
:静态代码分析flutter format .
:格式化代码flutter run
:启动调试模式(默认 Debug)
flutter run --release
:发布模式运行
运行时命令:
\\nr
:热重载(保留状态)R
:热重启(重置状态)q
:退出应用flutter build apk
(默认 Release)flutter build apk --split-per-abi
:分架构打包flutter build ios
(需 macOS)flutter build web
flutter build windows/macos/linux
flutter test
:运行单元测试flutter screenshot
:截取设备屏幕flutter logs
:查看运行日志flutter clean
:清除构建缓存flutter config --enable-web
:启用/禁用特定平台支持flutter pub outdated
:检查过时的依赖项如需更详细的参数说明,可通过 flutter help <command>
查看具体帮助。
在上一章中,我们介绍了 Dart 模式匹配的宏观概念及其在简化代码中的重要性。本章将深入探讨模式匹配的基石——模式(Patterns) 。通过理解这些基础模式,你将能够灵活运用模式匹配进行数据解构和逻辑处理。本章提供简洁、实用的代码示例,帮助你快速上手。
\\n在 Dart 中,模式是描述值“形状”的语法结构,用于匹配和解构数据。它可以是简单的字面量,也可以是复杂的嵌套结构。例如:
\\n42
:一个常量模式。var x
:一个变量模式。[a, b]
:一个列表模式,包含两个变量模式。{\'name\': n, \'age\': a}
:一个映射模式,包含两个变量模式。Point(x: x, y: y)
:一个对象模式,解构对象的属性。模式适用于以下场景:
\\nvar (x, y) = point;
(x, y) = (y, x);
switch
语句或表达式:switch (value) { case int i: ... }
if-case
语句:if (data case [int a, int b]) { ... }
for-in
循环:for (var (key, value) in map.entries) { ... }
匹配与解构:
\\n示例:
\\nDart
\\nvoid main() {\\n // 匹配和解构坐标记录\\n var point = (x: 10, y: 20);\\n // 在 if-case 中引入新变量,需要使用 `var` 或明确类型\\n if (point case (x: var x, y: var y)) {\\n // x 和 y 会自动作为局部变量引入并推断类型(int)\\n print(\'Point: ($x, $y)\'); // 输出: Point: (10, 20)\\n }\\n\\n // 尝试匹配类型不符的列表\\n var data = [1, \'two\'];\\n // 使用 var 解构,变量 a, b 会推断为 dynamic,模式匹配成功\\n if (data case [var a, var b]) {\\n print(\'Two items: $a, $b\');\\n } else {\\n print(\'Not two items.\');\\n }\\n\\n // 如果想严格匹配类型,则明确指定类型\\n if (data case [int a, int b]) { // 明确指定 int 类型,此模式匹配失败\\n print(\'Two integers: $a, $b\');\\n } else {\\n print(\'Not two integers.\'); // 输出: Not two integers.\\n }\\n}\\n
\\nDart 提供四种基础模式,用于构建复杂模式匹配逻辑。
\\n作用:匹配值是否等于字面量或 const
常量。
语法:字面量(如 1
, \'hello\'
, true
, null
)或 const
变量。
示例:
\\nDart
\\nvoid checkStatusCode(int code) {\\n switch (code) {\\n case 200: // 常量模式\\n print(\'Success\');\\n case 404: // 常量模式\\n print(\'Not found\');\\n default:\\n print(\'Unknown status: $code\');\\n }\\n}\\n\\nvoid main() {\\n checkStatusCode(200); // 输出: Success\\n checkStatusCode(500); // 输出: Unknown status: 500\\n}\\n
\\n作用:绑定匹配到的值到新变量,支持解构。
\\n语法:
\\nvar <variableName>
final <variableName>
<Type> <variableName>
(类型可推断时通常省略,或利用简写模式在声明/赋值上下文)示例:
\\nDart
\\nvoid main() {\\n // 解构用户记录(使用简写模式,仅限于变量声明上下文)\\n var user = (name: \'Alice\', age: 25);\\n // 当解构变量名与记录字段名一致时,使用简写模式 `(:name, :age)`\\n // name 会被推断为 String,age 会被推断为 int\\n var (:name, :age) = user;\\n print(\'User: $name, Age: $age\'); // 输出: User: Alice, Age: 25\\n\\n // 解构 API 响应列表\\n var response = [200, \'OK\'];\\n // 列表解构时,变量名自动推断类型\\n if (response case [var code, var message]) {\\n print(\'Response: $code - $message\'); // 输出: Response: 200 - OK\\n }\\n\\n // switch 中绑定值(变量模式与类型测试模式的结合)\\n void processInput(Object input) {\\n switch (input) {\\n case int value: // 类型测试模式:同时检查类型并绑定到变量 value (value 推断为 int)\\n print(\'Number: $value\');\\n case String text: // 类型测试模式:同时检查类型并绑定到变量 text (text 推断为 String)\\n print(\'Text: $text\');\\n default:\\n print(\'Other: $input\');\\n }\\n }\\n processInput(42); // 输出: Number: 42\\n processInput(\'Hello\'); // 输出: Text: Hello\\n}\\n
\\n_
)作用:忽略不关心的值,保持模式结构完整。
\\n语法:_
示例:
\\nDart
\\nvoid main() {\\n // 忽略记录中的字段(使用简写模式和通配符,仅限于变量声明上下文)\\n var user = (name: \'Bob\', id: 123, role: \'user\');\\n // 解构 name 字段,并忽略 id 和 role 字段\\n var (:name, id: _, role: _) = user;\\n print(\'User: $name\'); // 输出: User: Bob\\n\\n // switch 中忽略子模式(列表模式结合通配符)\\n void processRequest(List<String> request) {\\n switch (request) {\\n case [\'GET\', var path]: // 解构第一个元素为 \'GET\',第二个元素绑定到 path\\n print(\'Fetching: $path\');\\n case [\'POST\', var path, _]: // 解构第一个为 \'POST\',第二个绑定到 path,忽略第三个\\n print(\'Posting to: $path\');\\n case _: // 匹配任何其他情况\\n print(\'Invalid request.\');\\n }\\n }\\n processRequest([\'GET\', \'/api/users\']); // 输出: Fetching: /api/users\\n processRequest([\'POST\', \'/api/data\', \'payload\']); // 输出: Posting to: /api/data\\n}\\n
\\n作用:检查值类型并进行智能类型提升。
\\n语法:<Type> <variableName>
示例:
\\nDart
\\nvoid main() {\\n void processShape(Object shape) {\\n switch (shape) {\\n case int radius: // 类型测试模式:检查 shape 是否为 int,并将智能提升后的值绑定到 radius\\n print(\'Circle, radius: $radius\');\\n case List<double> rect: // 类型测试模式:检查 shape 是否为 List<double>,并绑定到 rect\\n print(\'Rectangle, dimensions: $rect\');\\n default:\\n print(\'Unknown shape.\');\\n }\\n }\\n processShape(5); // 输出: Circle, radius: 5\\n processShape([3.0, 4.0]); // 输出: Rectangle, dimensions: [3.0, 4.0]\\n}\\n
\\n?
)作用:匹配非空值,确保空安全处理。
\\n语法:<pattern>?
示例:
\\nDart
\\nvoid main() {\\n // 处理可空用户数据(if-case 中需要明确声明变量)\\n (String?, int?)? user = (\'Alice\', null);\\n\\n // (name: var name, age: var age):仅当 user 非空,且其 `name` 字段非空时才匹配。\\n // `name` 会被推断为 String (非空),`age` 会被推断为 `int?`。\\n if (user case (String name, int? age)) {\\n print(\'Name: $name, Age: ${age ?? \\"unknown\\"}\'); // 输出: Name: Alice, Age: unknown\\n }\\n\\n // 处理可空坐标(在 switch 表达式中使用空检查模式)\\n String processNullableCoord((int?, int?)? coords) {\\n return switch (coords) {\\n // 记录本身非空且两个字段都非空时匹配 (x, y 会被推断为 int)\\n (int x, int y)? => \'Point: ($x, $y)\',\\n // 记录非空,第一个字段非空,第二个字段为 null\\n (int x, null)? => \'Missing y coordinate ($x).\',\\n // 记录非空,第一个字段为 null,第二个字段非空\\n (null, int y)? => \'Missing x coordinate (y=$y).\',\\n // 记录非空,两个字段都为 null\\n (null, null)? => \'Both coordinates missing.\',\\n // 记录本身为 null\\n null => \'No coordinates provided.\',\\n // 对于 (int?, int?)? 类型,如果所有上述具象模式都被覆盖,Dart 编译器可能认为它是穷尽的,\\n // 不需要 _ 默认分支。如果未来添加更多复杂类型,可能需要 _。\\n };\\n }\\n\\n print(processNullableCoord((10, 20))); // 输出: Point: (10, 20)\\n print(processNullableCoord(null)); // 输出: No coordinates provided.\\n print(processNullableCoord((10, null))); // 输出: Missing y coordinate (10).\\n print(processNullableCoord((null, 20))); // 输出: Missing x coordinate (y=20).\\n print(processNullableCoord((null, null))); // 输出: Both coordinates missing.\\n}\\n
\\n本章深入讲解了 Dart 模式匹配的四种基础模式:
\\nconst
常量。var
和 final
进行类型推断。同时明确了简写模式 (:name
) 主要用于变量声明和赋值上下文。_
明确表示忽略不关心的值,提升代码可读性。case
后指定类型即可进行类型检查和智能类型提升,这是 Dart 3 中推荐的简洁用法。?
后缀确保模式只匹配非空值,提升空安全处理的优雅性。这些优化后的示例展示了如何在实际场景(如 API 响应、几何形状处理)中使用模式匹配,代码简洁且空安全。
\\n理解了基础模式之后,是时候解锁模式匹配更强大的功能了。下一章将深入探讨解构模式,包括列表模式、映射模式、记录模式和对象模式,帮助你优雅地处理各种复杂数据结构,让你的 Dart 代码更加简洁和富有表现力。
","description":"在上一章中,我们介绍了 Dart 模式匹配的宏观概念及其在简化代码中的重要性。本章将深入探讨模式匹配的基石——模式(Patterns) 。通过理解这些基础模式,你将能够灵活运用模式匹配进行数据解构和逻辑处理。本章提供简洁、实用的代码示例,帮助你快速上手。 2.1 模式(Patterns)的基本构成\\n\\n在 Dart 中,模式是描述值“形状”的语法结构,用于匹配和解构数据。它可以是简单的字面量,也可以是复杂的嵌套结构。例如:\\n\\n42:一个常量模式。\\nvar x:一个变量模式。\\n[a, b]:一个列表模式,包含两个变量模式。\\n{\'name\': n, \'age\':…","guid":"https://juejin.cn/post/7506832196582752290","author":"OldBirds","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-22T03:27:39.369Z","media":null,"categories":["iOS","Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"flutter开的app应用商店审核-极光推送初始化的问题","url":"https://juejin.cn/post/7506845716943028261","content":"app开发完了准备上架各个厂家的应用商店,小米,oppo,vivo,荣耀都可以,但是在华为反馈:\\n在用户同意隐私政策前,您的应用应用集成的[极光] SDK获取用户信息:软件安装列表,MAC地址,ANDROID ID,不符合相关法律法规要求。直接说解决方案吧。极光工程师回复的是 JCollectionAuth.setAuth(context, false)接口在Application的onCreate方法中调用,直到用户点击同意隐私条款才能设置JCollectionAuth.setAuth(context, true),然后调用初始化接口
\\n接下来说修改步骤\\n1、java修改\\n1.1、
\\n找到该文件夹新增这些代码 (android/app/src/main/java/com/yourcompany/yourapp/MainActivity.java)
package com.example.yourapp;\\nimport android.os.Bundle;\\nimport androidx.annotation.NonNull;\\nimport io.flutter.embedding.android.FlutterActivity;\\nimport io.flutter.embedding.engine.FlutterEngine;\\nimport io.flutter.plugin.common.MethodChannel;\\n\\nimport cn.jiguang.api.JCoreInterface;\\nimport cn.jiguang.analytics.page.JCollectionInterface;\\nimport cn.jpush.android.api.JPushInterface;\\n\\npublic class MainActivity extends FlutterActivity {\\n private static final String CHANNEL = \\"jpush\\";\\n\\n @Override\\n public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {\\n super.configureFlutterEngine(flutterEngine); \\n new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL)\\n .setMethodCallHandler((call, result) -> {\\n if (call.method.equals(\\"agreePrivacyAndInit\\")) {\\n JCollectionInterface.setAuth(getApplicationContext(), true);\\n JPushInterface.setDebugMode(true);\\n JPushInterface.init(getApplicationContext());\\n result.success(null);\\n } else {\\n result.notImplemented();\\n }\\n });\\n }\\n}\\n
\\n1.2、在android/app/src/main/java/com/yourcompany/yourapp/看看有没有Application.java,没有就先建一个
\\nApplication.java代码
\\n\\nimport android.app.Application;\\nimport cn.jiguang.analytics.page.JCollectionInterface;\\n\\npublic class MyApp extends Application {\\n @Override\\n public void onCreate() {\\n super.onCreate();\\n // 初始化时关闭采集\\n JCollectionInterface.setAuth(this, false);\\n }\\n}\\n
\\n1.3、修改 AndroidManifest.xml 注册自定义 Application\\n找到 android/app/src/main/AndroidManifest.xml,添加:
\\n<application\\n android:name=\\".MyApp\\" <!-- 注意这里要写你的类名 跟Application.java文件的 class MyApp --\x3e\\n android:label=\\"yourapp\\"\\n ... >\\n
\\n1.4、在封装极光推送
\\nimport \'package:flutter/services.dart\';\\nimport \'package:jpush_flutter/jpush_flutter.dart\';\\nimport \'package:jverify/jverify.dart\';\\nimport \'package:logger/logger.dart\';\\nimport \'package:shared_preferences/shared_preferences.dart\';\\n\\nimport \'../tools/utils.dart\';\\n\\nclass JPushService {\\n final JPush jpush = JPush();\\n final Jverify jverify = Jverify();\\n final Logger logger = Logger();\\n late SharedPreferences prefs;\\n static const MethodChannel _channel = MethodChannel(\'jpush_helper\');\\n\\n Future<void> initPlatformState() async {\\n try {\\n prefs = await SharedPreferences.getInstance();\\n jpush.addEventHandler(\\n // 收到通知时的回调\\n onReceiveNotification: (Map<String, dynamic> message) async {\\n print(\\"flutter onReceiveNotification: $message\\");\\n },\\n\\n // 点击通知时的回调\\n onOpenNotification: (Map<String, dynamic> message) async {\\n print(\\"flutter onOpenNotification: $message\\");\\n },\\n\\n // 收到自定义消息时的回调\\n onReceiveMessage: (Map<String, dynamic> message) async {\\n print(\\"flutter onReceiveMessage: $message\\");\\n },\\n\\n // 收到通知授权结果时的回调\\n onReceiveNotificationAuthorization:\\n (Map<String, dynamic> message) async {\\n print(\\"flutter onReceiveNotificationAuthorization: $message\\");\\n },\\n\\n // 通知消息不显示时的回调\\n onNotifyMessageUnShow: (Map<String, dynamic> message) async {\\n print(\\"flutter onNotifyMessageUnShow: $message\\");\\n },\\n\\n // 应用内消息显示时的回调\\n onInAppMessageShow: (Map<String, dynamic> message) async {\\n print(\\"flutter onInAppMessageShow: $message\\");\\n },\\n\\n // 命令结果返回时的回调\\n onCommandResult: (Map<String, dynamic> message) async {\\n print(\\"flutter onCommandResult: $message\\");\\n },\\n\\n // 应用内消息点击时的回调\\n onInAppMessageClick: (Map<String, dynamic> message) async {\\n print(\\"flutter onInAppMessageClick: $message\\");\\n },\\n\\n // 设备与极光服务器连接时的回调\\n onConnected: (Map<String, dynamic> message) async {\\n print(\\"flutter onConnected: $message\\");\\n },\\n );\\n } on PlatformException {\\n print(\\"Failed to get platform version.\\");\\n }\\n //这个很重要自己手动打开写\\n jpush.setAuth(enable: true);\\n jpush.setup(\\n appKey: \\"appKey\\",\\n channel: \\"xxxxxx\\",\\n production: false,\\n debug: true,\\n );\\n jverify.setDebugMode(true);\\n\\n jverify.setup(\\n appKey: \\"appKey\\",\\n channel: \\"developer-default\\",\\n );\\n\\n jpush.applyPushAuthority(\\n const NotificationSettingsIOS(sound: true, alert: true, badge: true));\\n jpush.getRegistrationID().then((rid) async { \\n await prefs.setString(\'registrationID\', rid);\\n });\\n await Future.delayed(Duration(milliseconds: 800)); // ⏱ 可加一点延时\\n jverify.isInitSuccess().then((result) {\\n if (result[\'result\'] == true) {\\n logger.i(\\"JVerify 初始化成功\\");\\n } else {\\n logger.i(\\"JVerify 初始化失败\\");\\n }\\n });\\n await isNotificationEnabled();\\n } \\n} \\n\\n
\\n大功告成。现在清除flutter clean清除缓存,在flutter pub get
\\n2、Kotlin\\n2.1、找到该文件夹新增这些代码 (android/app/src/main/kotlin/com/yourcompany/yourapp),有MainActivityNew文件就添加一下代码。没有的话及新增一个kotlin文件代码如下:
\\npackage com.ai.tunbao\\nimport android.os.Bundle\\nimport io.flutter.embedding.android.FlutterActivity\\nimport io.flutter.embedding.engine.FlutterEngine\\nimport io.flutter.plugin.common.MethodChannel\\nimport cn.jiguang.api.utils.JCollectionAuth\\nclass MainActivityNew : FlutterActivity() {\\n private val CHANNEL = \\"jpush_helper\\"\\n\\n override fun onCreate(savedInstanceState: Bundle?) {\\n super.onCreate(savedInstanceState)\\n // 禁用极光自动采集\\n JCollectionAuth.setAuth(applicationContext, false)\\n }\\n override fun configureFlutterEngine(flutterEngine: FlutterEngine) {\\n super.configureFlutterEngine(flutterEngine)\\n\\n MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {\\n call, result ->\\n if (call.method == \\"setJCollectionAuthTrue\\") {\\n JCollectionAuth.setAuth(applicationContext, true)\\n result.success(true)\\n } else {\\n result.notImplemented()\\n }\\n }\\n }\\n}\\n\\n
\\n2.2、修改 AndroidManifest.xml 注册自定义 Application\\n找到 android/app/src/main/AndroidManifest.xml,添加:
\\n<application>\\n<activity \\nandroid:name=\\".MainActivityNew\\" // **主要是这一句 跟上面文件的 class MainActivityNew 一致**\\nandroid:configChanges=\\"orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode\\"\\n android:exported=\\"true\\"\\n android:hardwareAccelerated=\\"true\\"\\n android:launchMode=\\"singleTop\\"\\n android:theme=\\"@style/LaunchTheme\\"\\n android:windowSoftInputMode=\\"adjustResize\\"\\n android:screenOrientation=\\"portrait\\">\\n <meta-data\\n android:name=\\"io.flutter.embedding.android.NormalTheme\\"\\n android:resource=\\"@style/NormalTheme\\" />\\n <intent-filter>\\n <action android:name=\\"android.intent.action.MAIN\\" />\\n <category android:name=\\"android.intent.category.LAUNCHER\\" />\\n </intent-filter>\\n</activity> \\n</application>\\n
\\n2.3、android/build.gradle添加对应代码
\\nallprojects {\\n repositories {\\n google()\\n mavenCentral()\\n // ✅ 添加极光仓库\\n maven { url \'https://sdk.jpush.cn/gradle-plugin/\' } //这句\\n }\\n}\\n\\n
\\n2.4、android/app/build.gradle添加对应代码
\\nplugins {\\n id \\"com.android.application\\"\\n id \\"kotlin-android\\"\\n id \\"dev.flutter.flutter-gradle-plugin\\"\\n id \\"com.google.firebase.crashlytics\\"\\n id \'com.google.gms.google-services\'\\n}\\n\\ndependencies {\\n implementation platform(\'com.google.firebase:firebase-bom:33.9.0\')\\n implementation \'com.google.firebase:firebase-analytics\'\\n implementation \'cn.jiguang.sdk:jverification:3.2.0\' // 或与你插件版本匹配的 JVerify 原生库\\n // ✅ 添加 JCore SDK(包含 JCollectionAuth)\\n implementation \'cn.jiguang.sdk:jcore:3.3.2\' // 版本建议与你 flutter jpush 插件匹配\\n}\\n\\n
\\n剩下的跟Java一样的,在1.4的步骤来\\n这样就没有问题
","description":"app开发完了准备上架各个厂家的应用商店,小米,oppo,vivo,荣耀都可以,但是在华为反馈: 在用户同意隐私政策前,您的应用应用集成的[极光] SDK获取用户信息:软件安装列表,MAC地址,ANDROID ID,不符合相关法律法规要求。直接说解决方案吧。极光工程师回复的是 JCollectionAuth.setAuth(context, false)接口在Application的onCreate方法中调用,直到用户点击同意隐私条款才能设置JCollectionAuth.setAuth(context, true),然后调用初始化接口 接下来说修改步骤…","guid":"https://juejin.cn/post/7506845716943028261","author":"方文_","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-22T02:56:47.357Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b29b2211648545b1a64e72080741ae0e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pa55paHXw==:q75.awebp?rk3s=f64ab15b&x-expires=1748487407&x-signature=eS7LXdQv57zEKyth5W7izqQ2T1I%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"掌握 Dart 模式匹配: 1.模式匹配导论","url":"https://juejin.cn/post/7506816522191732776","content":"在 Dart 语言中,模式匹配(Pattern Matching)是一项强大而灵活的语言特性,它允许你检查一个值是否符合特定的结构或“形状”,并在此过程中,有条件地从中提取(解构)出内部数据。你可以将模式匹配想象成一个智能的“数据侦探”,它不仅能够识别数据的外在形态,还能深入其内部,优雅地解构并提取出你所需的信息。
\\n核心概念剖析:
\\n模式(Pattern) :这是模式匹配的“蓝图”,它定义了你期望匹配的值的结构。模式可以是简单的字面量、变量,也可以是复杂的组合,比如列表、映射、记录或自定义对象。
\\n// 简单模式:字面量模式\\nconst number = 10;\\nif (number case 10) { // 这里的 \'10\' 就是一个字面量模式\\n print(\'数字是10\');\\n}\\n\\n// 复杂模式:列表模式 (用于类型测试和解构)\\nfinal coordinates = [10, 20];\\nif (coordinates case [int x, int y]) { // 这里的 \'[int x, int y]\' 是一个列表模式\\n print(\'X坐标: $x, Y坐标: $y\');\\n}\\n
\\n匹配(Matching) :这是模式匹配的核心操作。当一个值与某个模式所描述的结构和内容完全相符时,我们称之为“匹配成功”。反之,则匹配失败。
\\nfinal status = \'success\';\\n// 匹配成功\\nif (status case \'success\') {\\n print(\'操作成功!\');\\n}\\n\\nfinal result = [\'error\', \'文件未找到\'];\\n// 匹配失败\\nif (result case [\'success\', String message]) {\\n print(\'不会打印,因为第一个元素不是 \\"success\\"\');\\n}\\n
\\n解构(Destructuring) :这是模式匹配最实用的功能之一。一旦模式匹配成功(或在变量声明等场景下天然满足模式),你可以同时将匹配到的值的一部分或全部“拆解”出来,并将这些解构出的数据绑定到新的局部变量。这极大地简化了从复杂数据结构中提取数据的过程,让代码更简洁、更直观。
\\n// 变量声明解构:从 Record (记录) 中提取数据\\nfinal point = (100, 200);\\nfinal (x, y) = point; // 解构 point 到 x 和 y\\nprint(\'点坐标: ($x, $y)\'); // 输出:点坐标: (100, 200)\\n\\n// 变量声明解构:从 List 中提取数据\\nfinal colors = [\'red\', \'green\', \'blue\'];\\nfinal [firstColor, secondColor, _] = colors; // 解构前两个元素,_ 表示忽略第三个\\nprint(\'第一个颜色: $firstColor, 第二个颜色: $secondColor\'); // 输出:第一个颜色: red, 第二个颜色: green\\n\\n// 变量声明解构:从 Map 中提取数据\\nfinal user = {\'name\': \'Alice\', \'age\': 30, \'city\': \'New York\'};\\nfinal {\'name\': userName, \'age\': userAge} = user; // 解构 name 和 age 字段\\nprint(\'用户名: $userName, 年龄: $userAge\'); // 输出:用户名: Alice, 年龄: 30\\n\\n// for-in 循环解构:遍历 List 中的 Record\\nfinal listOfPoints = [(1, 2), (3, 4), (5, 6)];\\nfor (final (px, py) in listOfPoints) { // 每次迭代都解构一个 Record\\n print(\'处理点: ($px, $py)\');\\n}\\n
\\n为什么 Dart 需要模式匹配?(解决痛点)
\\n在 Dart 3 之前,处理复杂或多变的数据结构常常伴随着一些痛点:
\\nif (obj is Type)
条件判断,然后进行强制类型转换 (as Type
) 才能访问到具体的内部数据。这导致代码臃肿、重复且不够优雅。if-else
语句或繁琐的类型转换会让代码逻辑变得模糊,一眼难以看清其真实意图,降低了代码的可读性和维护性。as Type
)可能会在运行时抛出 CastError
异常,导致程序意外崩溃。switch
语句的局限性:传统的 switch
语句只能基于常量或枚举值进行简单的等值匹配,无法处理更复杂的条件判断和结构化数据的匹配。模式匹配的引入,正是为了系统性地解决这些问题。它让你的 Dart 代码变得:
\\nDart 3.0 的引入及其重要性:
\\n模式匹配是 Dart 3.0 中最具里程碑意义的新特性之一。它与 记录(Records) 和 密封类(Sealed Classes) 共同构成了 Dart 语言在数据建模和处理方面的一套强大而统一的工具集。Dart 团队在设计这些特性时,充分吸取了其他现代编程语言(如 Rust、Kotlin、Scala)的成功经验,旨在在保持 Dart 既有简洁性的同时,显著增强其表达能力。
\\n在 Dart 3.0 发布之前,开发者主要依赖以下方式来处理类似模式匹配的场景:
\\nis
运算符和 as
运算符:这是最常见的类型检查和转换方式。
Object value = [1, \'hello\'];\\n// 传统方式:冗长且易错\\nif (value is List<Object?>) {\\n if (value.length == 2 && value[0] is int && value[1] is String) {\\n int number = value[0] as int; // 可能抛出 CastError\\n String text = value[1] as String; // 可能抛出 CastError\\n print(\'Found: $number, $text\');\\n }\\n}\\n\\n// 对比模式匹配:\\nif (value case [int number, String text]) {\\n print(\'Found: $number, $text\'); // 更简洁、安全\\n}\\n
\\n从上面的对比可以看出,模式匹配极大地简化了代码,并提升了类型安全性。
\\n级联运算符 (..
) :虽然常用于对同一个对象执行一系列操作,但它并不能用于解构数据。
自定义 getter 或辅助方法:为了从复杂对象中提取数据,有时需要编写额外的 getter 方法或封装在辅助函数中。
\\n模式匹配的引入,标志着 Dart 语言在表达力和安全性方面的重大飞跃。它使得 Dart 在处理复杂数据流、实现更优雅的条件逻辑以及支持更高级的编程范式(如代数数据类型)方面,都迈出了坚实的一步,进一步巩固了其作为现代、高效编程语言的地位。
\\n模式匹配的加入并非偶然,它与 Dart 语言的设计哲学高度契合:
\\nis
或 as
运算符,而是作为一种更优、更简洁的替代方案。本章我们深入探讨了 Dart 模式匹配的核心概念。我们了解到,模式匹配不仅仅是简单的值比较,更是一种集匹配、解构和类型安全于一体的强大工具。它旨在解决传统 Dart 代码中处理复杂数据结构时的冗余、可读性差和潜在运行时安全问题。通过与 Dart 3.0 中的记录和密封类协同工作,模式匹配极大地提升了 Dart 语言的表现力、简洁性和健壮性,使我们能够编写更优雅、更可靠的代码。
\\n理解了模式匹配的价值和背景后,是时候深入到它的具体实现细节了。在下一章,我们将聚焦于模式匹配的基石——核心概念与基础模式。我们会详细讲解各种基础模式类型,包括常量模式、变量模式、通配符模式、类型测试模式以及处理可空性的空检查。通过丰富的代码示例和清晰的解释,你将学会如何在不同场景下运用这些基础模式,为后续掌握更复杂的解构和应用打下坚实的基础。
","description":"1. 模式匹配导论 1.1 什么是模式匹配?\\n\\n在 Dart 语言中,模式匹配(Pattern Matching)是一项强大而灵活的语言特性,它允许你检查一个值是否符合特定的结构或“形状”,并在此过程中,有条件地从中提取(解构)出内部数据。你可以将模式匹配想象成一个智能的“数据侦探”,它不仅能够识别数据的外在形态,还能深入其内部,优雅地解构并提取出你所需的信息。\\n\\n核心概念剖析:\\n\\n模式(Pattern) :这是模式匹配的“蓝图”,它定义了你期望匹配的值的结构。模式可以是简单的字面量、变量,也可以是复杂的组合,比如列表、映射、记录或自定义对象。\\n\\n// 简单…","guid":"https://juejin.cn/post/7506816522191732776","author":"OldBirds","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-22T02:02:45.908Z","media":null,"categories":["iOS","前端","Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 图片预加载","url":"https://juejin.cn/post/7506779269197922340","content":"在构建 Flutter 应用时,提前加载图片资源是一种常见优化手段,尤其适用于展示型页面、动画首帧、轮播图等场景。本文将手把手讲解一个支持 批量并发预加载、状态追踪、SVG 加载支持 的工具类 ImagePreloader
,帮助你打造更加丝滑的用户体验。
功能 | 支持 |
---|---|
图片预加载(本地/网络) | ✅ |
SVG 图片预加载 | ✅ |
状态缓存与查询 | ✅ |
批量并发控制 | ✅ |
加载状态流用于 UI 监听 | ✅ |
手动清除与资源释放 | ✅ |
// 图片预加载状态枚举\\nenum ImagePreloadStatus {\\n notStarted, // 未开始\\n loading, // 加载中\\n success, // 加载成功\\n failed, // 加载失败\\n}\\n
\\nswitch(status)
就能渲染骨架屏 / 进度条 / 错误页。class ImagePreloadResult {\\n final String imageUrl; // 图片 URL\\n final ImagePreloadStatus status; // 当前状态\\n final String? errorMessage; // 失败原因\\n final double progress; // 进度 0.0~1.0\\n\\n ImagePreloadResult copyWith({ ... }) // 不可变 -> 复制更新\\n}\\n
\\ncopyWith
:每次状态更新都生成新实例,Stream
才能检测到变化💡。progress
:SVG & 位图最终都会写 1.0,后续可扩展成真正的分帧进度。class ImagePreloader {\\n static final ImagePreloader _instance = ImagePreloader._internal();\\n factory ImagePreloader() => _instance; // 🔑 对外使用\\n ImagePreloader._internal();\\n\\n final Map<String, ImagePreloadResult> _imageStatusMap = {};\\n final _imageStatusController = StreamController<ImagePreloadResult>.broadcast();\\n\\n Stream<ImagePreloadResult> get imageStatusStream => _imageStatusController.stream;\\n}\\n
\\n为什么用单例?
\\nbroadcast()
让多个 StreamBuilder
同时监听,而不用各自拉流。bool _isSvgImage(String url) => url.toLowerCase().contains(\'.svg\');\\n\\nFuture<void> _precacheSvg(String imageUrl) async {\\n final loader = imageUrl.startsWith(\'http\')\\n ? SvgNetworkLoader(imageUrl)\\n : SvgAssetLoader(imageUrl);\\n\\n await svg.cache.putIfAbsent( // flutter_svg 的内部缓存表\\n loader.cacheKey(null),\\n () => loader.loadBytes(null),\\n );\\n}\\n
\\nprecacheImage
,直接把字节流塞进 flutter_svg
自带缓存⚡。preloadImage()
: 🏗️Future<ImagePreloadResult> preloadImage(\\n String imageUrl,\\n BuildContext context, {\\n ImageErrorListener? onError,\\n}) async {\\n // 1. 命中缓存?\\n if (_imageStatusMap[imageUrl]?.status == ImagePreloadStatus.success) {\\n return _imageStatusMap[imageUrl]!;\\n }\\n\\n // 2. 先广播 loading\\n final result = ImagePreloadResult(imageUrl: imageUrl, status: ImagePreloadStatus.loading);\\n _imageStatusMap[imageUrl] = result;\\n _imageStatusController.add(result);\\n\\n try {\\n if (_isSvgImage(imageUrl)) { // 3A. 走 SVG 流程\\n await _precacheSvg(imageUrl);\\n final ok = result.copyWith(status: ImagePreloadStatus.success, progress: 1.0);\\n _imageStatusMap[imageUrl] = ok;\\n _imageStatusController.add(ok);\\n return ok;\\n } else { // 3B. 走位图流程\\n final ImageProvider provider =\\n imageUrl.startsWith(\'http\') ? NetworkImage(imageUrl) : AssetImage(imageUrl);\\n final completer = Completer<ImagePreloadResult>();\\n\\n await precacheImage(provider, context, onError: (e, s) {\\n final fail = result.copyWith(status: ImagePreloadStatus.failed, errorMessage: e.toString());\\n _imageStatusMap[imageUrl] = fail;\\n _imageStatusController.add(fail);\\n onError?.call(e, s);\\n if (!completer.isCompleted) completer.complete(fail);\\n });\\n\\n if (!completer.isCompleted) { // 成功分支\\n final ok = result.copyWith(status: ImagePreloadStatus.success, progress: 1.0);\\n _imageStatusMap[imageUrl] = ok;\\n _imageStatusController.add(ok);\\n completer.complete(ok);\\n }\\n return completer.future;\\n }\\n } catch (e) { // 4. 异常兜底\\n final fail = result.copyWith(status: ImagePreloadStatus.failed, errorMessage: e.toString());\\n _imageStatusMap[imageUrl] = fail;\\n _imageStatusController.add(fail);\\n return fail;\\n }\\n}\\n
\\n🔍 关注点
\\nloading
事件 出去后,UI 可立刻展示 Skeleton。Completer
保证 同步代码 和 onError 回调 都能写入最终结果,不会出现“悬空 Future”。preloadImages()
:并发池把洪水关进闸门 🌊🚪Future<List<ImagePreloadResult>> preloadImages(\\n List<String> urls,\\n BuildContext ctx, {\\n int concurrency = 5, // 默认 5 条并发\\n}) async {\\n final pool = Pool(concurrency);\\n final results = <ImagePreloadResult>[];\\n\\n await Future.wait(\\n urls.map((u) => pool.withResource(() async {\\n final r = await preloadImage(u, ctx);\\n results.add(r);\\n })),\\n );\\n return results;\\n}\\n
\\npool.withResource()
:同时只放 concurrency
个任务在跑,其余排队。bool isImagePreloaded(String url) => _imageStatusMap[url]?.status == ImagePreloadStatus.success;\\n\\nvoid clearImage(String url) => _imageStatusMap.remove(url); // 删单张\\nvoid clearAllImages() => _imageStatusMap.clear(); // 全清\\nvoid dispose() => _imageStatusController.close(); // 组件卸载时调用\\n
\\n\\n\\n⚠️ 最佳实践:
\\n\\n
\\n- 若
\\nImagePreloader
绑定到独立模块,可在模块dispose()
时关闭 Stream。
import \'package:flutter/material.dart\';\\nimport \'package:flutter_svg/flutter_svg.dart\';\\n\\nflu\\nimport \'image_preloader.dart\';\\n\\nvoid main() {\\n runApp(const MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n const MyApp({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n title: \'Image Preload Demo\',\\n theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.teal),\\n home: const PreloadDemoPage(),\\n );\\n }\\n}\\n\\nclass PreloadDemoPage extends StatefulWidget {\\n const PreloadDemoPage({super.key});\\n\\n @override\\n State<PreloadDemoPage> createState() => _PreloadDemoPageState();\\n}\\n\\nclass _PreloadDemoPageState extends State<PreloadDemoPage> {\\n bool _preloaded = false;\\n int _currentIndex = 0;\\n\\n final _preloader = ImagePreloader();\\n\\n List<String> noPreLoadImages = [\\n \\"http://e.hiphotos.baidu.com/image/pic/item/a1ec08fa513d2697e542494057fbb2fb4316d81e.jpg\\",\\n \\"http://c.hiphotos.baidu.com/image/pic/item/30adcbef76094b36de8a2fe5a1cc7cd98d109d99.jpg\\",\\n \\"http://h.hiphotos.baidu.com/image/pic/item/7c1ed21b0ef41bd5f2c2a9e953da81cb39db3d1d.jpg\\",\\n \\"http://g.hiphotos.baidu.com/image/pic/item/55e736d12f2eb938d5277fd5d0628535e5dd6f4a.jpg\\",\\n \\"http://e.hiphotos.baidu.com/image/pic/item/4e4a20a4462309f7e41f5cfe760e0cf3d6cad6ee.jpg\\",\\n \\"http://b.hiphotos.baidu.com/image/pic/item/9d82d158ccbf6c81b94575cfb93eb13533fa40a2.jpg\\",\\n \\"http://e.hiphotos.baidu.com/image/pic/item/4bed2e738bd4b31c1badd5a685d6277f9e2ff81e.jpg\\",\\n \\"http://g.hiphotos.baidu.com/image/pic/item/0d338744ebf81a4c87a3add4d52a6059252da61e.jpg\\",\\n \\"http://a.hiphotos.baidu.com/image/pic/item/f2deb48f8c5494ee5080c8142ff5e0fe99257e19.jpg\\",\\n ];\\n List<String> preLoadImages = [\\n \\"http://f.hiphotos.baidu.com/image/pic/item/4034970a304e251f503521f5a586c9177e3e53f9.jpg\\",\\n \\"http://b.hiphotos.baidu.com/image/pic/item/279759ee3d6d55fbb3586c0168224f4a20a4dd7e.jpg\\",\\n \\"http://a.hiphotos.baidu.com/image/pic/item/e824b899a9014c087eb617650e7b02087af4f464.jpg\\",\\n \\"http://c.hiphotos.baidu.com/image/pic/item/9c16fdfaaf51f3de1e296fa390eef01f3b29795a.jpg\\",\\n \\"http://d.hiphotos.baidu.com/image/pic/item/b58f8c5494eef01f119945cbe2fe9925bc317d2a.jpg\\",\\n \\"http://h.hiphotos.baidu.com/image/pic/item/902397dda144ad340668b847d4a20cf430ad851e.jpg\\",\\n \\"http://b.hiphotos.baidu.com/image/pic/item/359b033b5bb5c9ea5c0e3c23d139b6003bf3b374.jpg\\",\\n \\"http://a.hiphotos.baidu.com/image/pic/item/8d5494eef01f3a292d2472199d25bc315d607c7c.jpg\\",\\n \\"http://b.hiphotos.baidu.com/image/pic/item/e824b899a9014c08878b2c4c0e7b02087af4f4a3.jpg\\",\\n ];\\n\\n @override\\n void initState() {\\n super.initState();\\n _preloader.imageStatusStream.listen((result) {\\n debugPrint(\'[Stream] ${result.imageUrl} -> ${result.status}\');\\n });\\n WidgetsBinding.instance.addPostFrameCallback((a){\\n _startPreload();\\n });\\n }\\n\\n Future<void> _startPreload() async {\\n await _preloader.preloadImages(preLoadImages, context);\\n setState(() {\\n _preloaded = true;\\n });\\n }\\n\\n Widget _buildImage(String url) {\\n if (url.endsWith(\'.svg\')) {\\n return SvgPicture.network(\\n url,\\n placeholderBuilder: (_) => const CircularProgressIndicator(),\\n );\\n } else {\\n return Image.network(url, fit: BoxFit.cover);\\n }\\n }\\n\\n Widget _buildPreloadedImage(String url) {\\n final status = _preloader.getImageStatus(url);\\n if (status?.status == ImagePreloadStatus.success) {\\n if (url.endsWith(\'.svg\')) {\\n return SvgPicture.network(url);\\n } else {\\n return Image.network(url);\\n }\\n } else if (status?.status == ImagePreloadStatus.loading) {\\n return const Center(child: CircularProgressIndicator());\\n } else if (status?.status == ImagePreloadStatus.failed) {\\n return const Icon(Icons.error, color: Colors.red);\\n } else {\\n return const SizedBox.shrink();\\n }\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: const Text(\\"图片预加载对比 Demo\\")),\\n body: Padding(\\n padding: const EdgeInsets.all(12.0),\\n child: Column(\\n children: [\\n const Text(\'❌ 未预加载\'),\\n const SizedBox(height: 12),\\n Container(\\n width: double.infinity,\\n height: 300,\\n child: PageView.builder(\\n itemCount: noPreLoadImages.length,\\n itemBuilder: (context, index) {\\n return _buildImage(noPreLoadImages[index]);\\n }),\\n ),\\n const SizedBox(width: 12),\\n const Text(\'✅ 预加载后显示\'),\\n const SizedBox(height: 12),\\n Container(\\n width: double.infinity,\\n height: 300,\\n child: PageView.builder(\\n itemCount: preLoadImages.length,\\n itemBuilder: (context, index) {\\n return _buildPreloadedImage(preLoadImages[index]);\\n }),\\n ),\\n ],\\n ),\\n ),\\n );\\n }\\n}\\n
\\nStreamBuilder
,所有图片状态尽收眼底👀。import \'dart:async\';\\nimport \'package:flutter/material.dart\';\\nimport \'package:flutter_svg/flutter_svg.dart\';\\nimport \'package:pool/pool.dart\';\\n\\n// 图片预加载状态枚举\\nenum ImagePreloadStatus {\\n notStarted, // 未开始\\n loading, // 加载中\\n success, // 加载成功\\n failed, // 加载失败\\n}\\n\\n// 图片预加载结果数据类\\nclass ImagePreloadResult {\\n final String imageUrl; // 图片URL\\n final ImagePreloadStatus status; // 当前加载状态\\n final String? errorMessage; // 错误信息(失败时使用)\\n final double progress; // 加载进度(0.0-1.0)\\n\\n ImagePreloadResult({\\n required this.imageUrl,\\n required this.status,\\n this.errorMessage,\\n this.progress = 0.0,\\n });\\n\\n // 复制并更新属性的便捷方法\\n ImagePreloadResult copyWith({\\n String? imageUrl,\\n ImagePreloadStatus? status,\\n String? errorMessage,\\n double? progress,\\n }) {\\n return ImagePreloadResult(\\n imageUrl: imageUrl ?? this.imageUrl,\\n status: status ?? this.status,\\n errorMessage: errorMessage ?? this.errorMessage,\\n progress: progress ?? this.progress,\\n );\\n }\\n}\\n\\n// 图片预加载管理器(单例模式)\\nclass ImagePreloader {\\n // 单例实例\\n static final ImagePreloader _instance = ImagePreloader._internal();\\n\\n factory ImagePreloader() => _instance;\\n\\n ImagePreloader._internal();\\n\\n // 存储图片加载状态的映射表\\n final Map<String, ImagePreloadResult> _imageStatusMap = {};\\n\\n // 用于广播加载状态变化的StreamController\\n final _imageStatusController =\\n StreamController<ImagePreloadResult>.broadcast();\\n\\n // 暴露给外部的状态流\\n Stream<ImagePreloadResult> get imageStatusStream =>\\n _imageStatusController.stream;\\n\\n // 获取不可修改的已加载图片状态映射\\n Map<String, ImagePreloadResult> get preloadedImages =>\\n Map.unmodifiable(_imageStatusMap);\\n\\n // 判断是否为SVG图片\\n bool _isSvgImage(String imageUrl) {\\n return imageUrl.toLowerCase().contains(\'.svg\');\\n }\\n\\n // 预加载SVG图片\\n Future<void> _precacheSvg(String imageUrl) async {\\n final loader =\\n imageUrl.startsWith(\'http://\') || imageUrl.startsWith(\'https://\')\\n ? SvgNetworkLoader(imageUrl)\\n : SvgAssetLoader(imageUrl);\\n\\n await svg.cache.putIfAbsent(\\n loader.cacheKey(null),\\n () => loader.loadBytes(null),\\n );\\n }\\n\\n // 核心方法:预加载单个图片\\n Future<ImagePreloadResult> preloadImage(\\n String imageUrl,\\n BuildContext context, {\\n ImageErrorListener? onError,\\n }) async {\\n // 如果图片已成功加载,直接返回缓存结果\\n if (_imageStatusMap[imageUrl]?.status == ImagePreloadStatus.success) {\\n return _imageStatusMap[imageUrl]!;\\n }\\n\\n // 初始化加载状态\\n final result = ImagePreloadResult(\\n imageUrl: imageUrl,\\n status: ImagePreloadStatus.loading,\\n );\\n _imageStatusMap[imageUrl] = result;\\n _imageStatusController.add(result);\\n\\n try {\\n // 检查是否为SVG图片\\n if (_isSvgImage(imageUrl)) {\\n // 使用SVG专用方法预加载\\n await _precacheSvg(imageUrl);\\n\\n // 成功处理\\n final successResult = result.copyWith(\\n status: ImagePreloadStatus.success,\\n progress: 1.0,\\n );\\n _imageStatusMap[imageUrl] = successResult;\\n _imageStatusController.add(successResult);\\n return successResult;\\n } else {\\n // 非SVG图片处理\\n // 根据URL类型创建对应的ImageProvider\\n ImageProvider imageProvider;\\n if (imageUrl.startsWith(\'http://\') || imageUrl.startsWith(\'https://\')) {\\n imageProvider = NetworkImage(imageUrl);\\n } else {\\n imageProvider = AssetImage(imageUrl);\\n }\\n\\n final completer = Completer<ImagePreloadResult>();\\n\\n // 使用Flutter原生方法预加载图片\\n await precacheImage(\\n imageProvider,\\n context,\\n onError: (exception, stackTrace) {\\n // 错误处理\\n final errorResult = result.copyWith(\\n status: ImagePreloadStatus.failed,\\n errorMessage: exception.toString(),\\n );\\n _imageStatusMap[imageUrl] = errorResult;\\n _imageStatusController.add(errorResult);\\n\\n // 调用自定义错误回调\\n if (onError != null) {\\n onError(exception, stackTrace);\\n }\\n\\n // 完成Completer\\n if (!completer.isCompleted) {\\n completer.complete(errorResult);\\n }\\n },\\n );\\n\\n // 成功处理\\n if (!completer.isCompleted) {\\n final successResult = result.copyWith(\\n status: ImagePreloadStatus.success,\\n progress: 1.0,\\n );\\n _imageStatusMap[imageUrl] = successResult;\\n _imageStatusController.add(successResult);\\n completer.complete(successResult);\\n }\\n\\n return await completer.future;\\n }\\n } catch (e) {\\n // 异常处理\\n final errorResult = result.copyWith(\\n status: ImagePreloadStatus.failed,\\n errorMessage: e.toString(),\\n );\\n _imageStatusMap[imageUrl] = errorResult;\\n _imageStatusController.add(errorResult);\\n return errorResult;\\n }\\n }\\n\\n // 批量预加载方法\\n Future<List<ImagePreloadResult>> preloadImages(\\n List<String> imageUrls,\\n BuildContext context, {\\n ImageErrorListener? onError,\\n int concurrency = 5,\\n }) async {\\n final pool = Pool(concurrency);\\n final results = <ImagePreloadResult>[];\\n\\n await Future.wait(\\n imageUrls.map(\\n (url) => pool.withResource(() async {\\n final result = await preloadImage(url, context, onError: onError);\\n results.add(result);\\n return result;\\n }),\\n ),\\n );\\n\\n return results;\\n }\\n // Future<List<ImagePreloadResult>> preloadImages(\\n // List<String> imageUrls,\\n // BuildContext context, {\\n // ImageErrorListener? onError,\\n // }) async {\\n // final futures = imageUrls.map(\\n // (url) => preloadImage(url, context, onError: onError),\\n // );\\n // return await Future.wait(futures);\\n // }\\n // Future<List<ImagePreloadResult>> preloadImages(\\n // List<String> imageUrls,\\n // BuildContext context, {\\n // ImageErrorListener? onError,\\n // int concurrency = 15,\\n // }) async {\\n // final results = <ImagePreloadResult>[];\\n // final queue = Queue.from(imageUrls);\\n // final activeTasks = <Future<void>>[];\\n // final completed = Completer<void>();\\n //\\n // void scheduleNext() {\\n // while (activeTasks.length < concurrency && queue.isNotEmpty) {\\n // final url = queue.removeFirst();\\n // late final Future<void> task;\\n // task = preloadImage(\\n // url,\\n // context,\\n // onError: onError,\\n // ).then((result) => results.add(result)).whenComplete(() {\\n // activeTasks.remove(task);\\n // scheduleNext();\\n // });\\n // activeTasks.add(task);\\n // }\\n //\\n // if (activeTasks.isEmpty && queue.isEmpty) {\\n // completed.complete();\\n // }\\n // }\\n //\\n // scheduleNext();\\n // await completed.future;\\n // return results;\\n // }\\n\\n // 检查图片是否已预加载\\n bool isImagePreloaded(String imageUrl) {\\n return _imageStatusMap[imageUrl]?.status == ImagePreloadStatus.success;\\n }\\n\\n // 获取指定图片的状态\\n ImagePreloadResult? getImageStatus(String imageUrl) {\\n return _imageStatusMap[imageUrl];\\n }\\n\\n // 清除单个图片缓存\\n void clearImage(String imageUrl) {\\n _imageStatusMap.remove(imageUrl);\\n }\\n\\n // 清除所有图片缓存\\n void clearAllImages() {\\n _imageStatusMap.clear();\\n }\\n\\n // 释放资源\\n void dispose() {\\n _imageStatusController.close();\\n }\\n}\\n
\\n场景 | 建议 |
---|---|
首屏 / Banner | 进入首页前 await preloadImages() ,确保立刻可见。 |
无限列表 | 滑到第 N 页时把第 N+1 页的图片批量预热。 |
内存控制 | 大批量下载 ➜ 调低 concurrency ;退出页面 ➜ clearAllImages() 。 |
一个用来实现胶囊滑条的代码\\n使用方法
\\nimport \'package:flutter/material.dart\';\\nimport \'package:permission_handler_test/capsule_silder/capsule_slider.dart\';\\n\\nvoid main() {\\n runApp(const MaterialApp(\\n home: CapsuleSliderTest()));\\n}\\n\\nclass CapsuleSliderTest extends StatefulWidget {\\n const CapsuleSliderTest({super.key});\\n\\n @override\\n State<CapsuleSliderTest> createState() => _CapsuleSliderTestState();\\n}\\n\\nclass _CapsuleSliderTestState extends State<CapsuleSliderTest> {\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n body: Padding(\\n padding: const EdgeInsets.all(28.0),\\n child: Column(\\n children: [\\n const SizedBox(height: 100,),\\n CapsuleSlider(value: 1, min: 0, max: 100,\\n onChanged: (v){\\n },)\\n ],\\n ),\\n ),\\n );\\n }\\n}\\n
\\n详细注释在代码中\\n若有其他问题可以评论区或私信
\\nimport \'dart:math\' as math;\\nimport \'package:flutter/material.dart\';\\n\\n/// 胶囊形拖动条(纯手势实现,不依赖 Slider)\\n/// ---------------------------------------------------------------------------\\n/// * value 当前数值\\n/// * min / max 取值范围\\n/// * onChanged 拖动过程中实时回调\\n/// * onChangeEnd 抬手回调\\n/// * move 是否处于“可调节”状态,影响轨道颜色 也可以放在禁用手势,此处只是用作颜色区分\\n/// * height 轨道高度(= 圆角直径)\\n/// * activeColor move==true 时已选轨道颜色\\n/// * noMoveColor move==false 时已选轨道颜色\\n/// * inactiveColor 未选轨道颜色\\n/// * showThumb 是否显示小圆手柄\\n/// ---------------------------------------------------------------------------\\nclass CapsuleSlider extends StatefulWidget {\\n const CapsuleSlider({\\n super.key,\\n required this.value,\\n required this.min,\\n required this.max,\\n required this.onChanged,\\n this.move = false,\\n this.onChangeEnd,\\n this.height = 46,\\n this.activeColor = const Color(0xfffa0200),\\n this.inactiveColor = const Color(0xffF3F4F9),\\n this.noMoveColor = const Color(0xffD6D6D6),\\n this.showThumb = false,\\n this.thumbRadius = 10,\\n this.thumbColor = Colors.white,\\n });\\n\\n // ---- 对外属性 ----\\n final double value;\\n final double min;\\n final double max;\\n\\n final bool move;\\n final ValueChanged<double> onChanged;\\n final ValueChanged<double>? onChangeEnd;\\n\\n final double height; // 轨道高度\\n final Color activeColor; // 可调节时的已选颜色\\n final Color inactiveColor; // 背景颜色\\n final Color noMoveColor; // 不可调节时的已选颜色\\n\\n final bool showThumb;\\n final double thumbRadius;\\n final Color thumbColor;\\n\\n @override\\n State<CapsuleSlider> createState() => _CapsuleSliderState();\\n}\\n\\nclass _CapsuleSliderState extends State<CapsuleSlider> {\\n late double _value; // 内部持有当前值,实时刷新\\n late double _width; // 组件宽度,用于像素↔百分比换算\\n\\n @override\\n void initState() {\\n super.initState();\\n _value = widget.value; // 初始化\\n }\\n\\n /// 当父组件外部更新 value 时,同步内部 _value\\n @override\\n void didUpdateWidget(covariant CapsuleSlider old) {\\n super.didUpdateWidget(old);\\n if (old.value != widget.value) _value = widget.value;\\n }\\n\\n /// 处理一次拖动 / 点击,localPos 为手指在组件内的坐标\\n void _handleDrag(Offset localPos) {\\n // 把 X 坐标映射到 0~1 百分比\\n final pct = (localPos.dx / _width).clamp(0.0, 1.0);\\n // 再映射到实际 min~max 区间\\n final newVal = widget.min + pct * (widget.max - widget.min);\\n\\n if (newVal != _value) {\\n setState(() => _value = newVal); // 刷新 UI\\n widget.onChanged(newVal); // 通知外部\\n }\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n final r = widget.height / 2; // 圆角半径\\n final pct = (_value - widget.min) / (widget.max - widget.min);\\n\\n return LayoutBuilder(\\n builder: (_, constraints) {\\n _width = constraints.maxWidth; // 得到真实宽度\\n final activeW = _width * pct; // 已选轨道宽度\\n\\n return GestureDetector(\\n behavior: HitTestBehavior.translucent, // 空白处也能响应\\n // —— 手势回调:点击 / 拖动 / 抬手 ——\\n onPanDown: (d) => _handleDrag(d.localPosition),\\n onPanUpdate:(d) => _handleDrag(d.localPosition),\\n onPanEnd: (_) => widget.onChangeEnd?.call(_value),\\n onTapDown: (d) => _handleDrag(d.localPosition),\\n\\n // —— 裁剪为胶囊形,防止已选轨道溢出 ——\\n child: ClipRRect(\\n borderRadius: BorderRadius.circular(r),\\n\\n child: Stack(\\n alignment: Alignment.centerLeft,\\n children: [\\n // 背景轨道(未选部分)\\n Container(\\n height: widget.height,\\n decoration: BoxDecoration(\\n color: widget.inactiveColor,\\n borderRadius: BorderRadius.circular(r),\\n ),\\n ),\\n // 已选轨道:颜色取决于 move\\n Container(\\n height: widget.height,\\n width: activeW,\\n decoration: BoxDecoration(\\n color: widget.move\\n ? widget.activeColor\\n : widget.noMoveColor,\\n borderRadius: BorderRadius.circular(r),\\n ),\\n ),\\n // 可选:手柄\\n if (widget.showThumb)\\n Positioned(\\n // 让手柄中心对齐已选轨道右端\\n left: math.max(0, activeW - widget.thumbRadius),\\n child: Container(\\n width: widget.thumbRadius * 2,\\n height: widget.thumbRadius * 2,\\n decoration: BoxDecoration(\\n color: widget.thumbColor,\\n shape: BoxShape.circle,\\n boxShadow: [\\n // 简单投影,让手柄有悬浮感\\n BoxShadow(\\n color: Colors.black26,\\n blurRadius: 3,\\n offset: const Offset(0, 1),\\n )\\n ],\\n ),\\n ),\\n ),\\n ],\\n ),\\n ),\\n );\\n },\\n );\\n }\\n}\\n
","description":"一个用来实现胶囊滑条的代码 使用方法 import \'package:flutter/material.dart\';\\nimport \'package:permission_handler_test/capsule_silder/capsule_slider.dart\';\\n\\nvoid main() {\\n runApp(const MaterialApp(\\n home: CapsuleSliderTest()));\\n}\\n\\nclass CapsuleSliderTest extends StatefulWidget {\\n const Capsu…","guid":"https://juejin.cn/post/7506590644543619084","author":"杉木笙","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-21T08:51:26.405Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d1781aaa624a4667a8c42e36523b9547~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5p2J5pyo56yZ:q75.awebp?rk3s=f64ab15b&x-expires=1748422632&x-signature=kR3tSs0xoVAGExmJsBYztmA5A4Q%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["前端","Flutter","交互设计"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 小技巧之:Flutter 3.32 的 Property Editor 生产力工具","url":"https://juejin.cn/post/7506590644543553548","content":"快速介绍一下 Flutter Property Editor ,它需要 Flutter 3.32+ 才支持使用,属于 IDE 增强工具,可以直接在可视化界面查看和修改 Widget 属性:
\\n开发者可以快速发现和修改 Widget 的现有和可用的参数,不需要跳转到定义或手动编辑源代码,如果配合 Flutter inspector 和热重载,修改后可以直接实时查看更改。
\\nProperty Editor 支持 VS Code 和 Android Studio/IntelliJ,你只需要在侧边栏找到下放这个图标,就可以打开对应面板,前提是需要 3.32+,不然你看到的会是如下所示 :
\\n而如果你是 3.32 +,那么打开应该是下面这样:
\\nFlutter Property Editor 可以和 Flutter inspector 结合使用,然后在这两个工具中同时检查你的 Widget,比如:
\\n首先在 IDE 中打开 Flutter inspector,然后:
\\n然后再切 tab 回到 Flutter Property Editor 中修改选中的 Widget 属性
\\n你还可以配合 hot reload ,如果喜欢自动保存,可以在 .vscode/settings.json
添加:
\\"files.autoSave\\": \\"afterDelay\\",\\n\\"dart.flutterHotReloadOnSave\\": \\"all\\",\\n
\\n或者在 Android Studio 打开 Settings > Tools > Actions on Save
并选择 Configure autosave options
,然后配置 Save files if the IDE is idle for X seconds
:
当然,默认 Settings > Languages & Frameworks > Flutter
下 Perform hot reload on save
是选中的:
在 Flutter Property Editor 中选择一个 Widget 时,它对应的文档会显示在顶部,开发者可以直接阅读 Widget 文档无需跳转:
\\n而对于 Flutter Property Editor 内的字段:
\\nTextStyle
、EdgeInsets
、Color
):目前不支持另外,Flutter Property Editor 中的每个 property input 都附带了信息:
\\nType and name: 构造函数参数的类型(例如 StackFit
)和名称(例如 fit
),将显示为每个输入字段的标签:
Info tooltip ⓘ :将鼠标悬停在属性输入旁边的 info 图标上会显示工具提示:
“Set” 和 “default” 标签:
\\n最后,Flutter Property Edit 还支持筛选:
\\nmainAxisAlignment
、mainAxisSize
:*
):简单来说,最终效果如下图所示,你可以字节选中一个 Widget ,然后对他的属性进行修改和配置,当然实际场景可能更多会和 Flutter inspector 一起使用,比如你通过 Flutter inspector 查看某些问题,然后需要修改对应参数时,就可以通过 Property Editor 来完成:
\\n\\n\\n当然,Flutter inspector 和 Property Editor 没有直接联动,你还是需要在侧边栏手动切换 tab 。
\\n
在Flutter开发中,处理异步数据与UI的同步是每个开发者必须掌握的技能。FutureBuilder和StreamBuilder这两个\\"异步构建器\\"组件,正是为解决这一核心问题而生。它们将异步操作与Widget构建完美结合,让数据流动变得可视化,是构建响应式界面的利器。
\\n同样,如果想进阶为Flutter资深开发人员,异步交互这条路是必须要走一趟的。
\\nFutureBuilder<T>(\\n future: // Future对象\\n initialData: // 初始数据\\n builder: (context, snapshot) {\\n // 构建逻辑\\n }\\n)\\n
\\nfuture
: 要监听的 Future
对象。builder
: 构建 UI 的函数,接收 BuildContext
和 AsyncSnapshot
。AsyncSnapshot
关键属性connectionState
:异步操作的状态(none
, waiting
, active
, done
)。data
:异步操作完成后的数据。error
:异步操作的错误信息。hasData
:是否有数据。hasError
:是否有错误。如:
\\nFuture<String> fetchData() async {\\n await Future.delayed(Duration(seconds: 2)); // 模拟网络请求延迟\\n return \'Hello, FutureBuilder!\';\\n}\\n\\nclass FutureExample extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return FutureBuilder<String>(\\n future: fetchData(), // 传入 Future 对象\\n builder: (context, snapshot) {\\n // 根据异步状态返回不同 UI\\n if (snapshot.connectionState == ConnectionState.waiting) {\\n return CircularProgressIndicator(); // 加载中\\n } else if (snapshot.hasError) {\\n return Text(\'Error: ${snapshot.error}\'); // 错误\\n } else {\\n return Text(\'Data: ${snapshot.data}\'); // 成功\\n }\\n },\\n );\\n }\\n}\\n);\\n
\\nStreamBuilder<T>(\\n stream: // Stream对象\\n initialData: // 初始数据\\n builder: (context, snapshot) {\\n // 构建逻辑\\n }\\n)\\n
\\nStreamBuilder<List<Message>>(\\n stream: chatService.messageStream(),\\n builder: (context, snapshot) {\\n if (!snapshot.hasData) {\\n return LoadingIndicator();\\n }\\n \\n return ListView.builder(\\n itemCount: snapshot.data!.length,\\n itemBuilder: (ctx, i) => ChatBubble(message: snapshot.data![i]),\\n );\\n },\\n);\\n
\\n特性 | FutureBuilder | StreamBuilder |
---|---|---|
数据特征 | 单次异步结果 | 持续数据流 |
生命周期 | 自动管理 | 需要手动管理订阅 |
内存消耗 | 较低 | 较高(长期订阅时) |
典型使用场景 | 网络请求、初始化数据 | 实时更新、事件流 |
错误处理 | 单次错误捕获 | 需要持续错误处理 |
// 错误示例:每次build都会创建新的Future\\nFutureBuilder(future: createFuture()) \\n\\n// 正确做法:将Future保存在State中\\nlate final Future _future = createFuture();\\nFutureBuilder(future: _future)\\n
\\nclass _MyWidgetState extends State<MyWidget> {\\n late final StreamSubscription _subscription;\\n\\n @override\\n void initState() {\\n super.initState();\\n _subscription = stream.listen((data) {});\\n }\\n\\n @override\\n void dispose() {\\n _subscription.cancel();\\n super.dispose();\\n }\\n}\\n
\\n// 错误:在builder中进行状态修改\\nbuilder: (context, snapshot) {\\n if(snapshot.hasData) setState((){}); // 危险!\\n}\\n\\n// 正确:使用回调或BLoC模式处理\\n
\\nconst
修饰静态部件initialData
避免布局跳动Equatable
进行深度比较这个示例只是展示一下一个页面如果数据来源不同,且刷新机制不一样的话,可以使用组合的方式来刷新这些数据,做到异步刷新,并且刷新的时候互不影响。
\\n// 先加载用户资料,再订阅实时位置\\nColumn(\\n children: [\\n FutureBuilder(\\n future: userProfile,\\n builder: (_, snap) => ProfileHeader(snap.data),\\n ),\\n StreamBuilder(\\n stream: locationStream,\\n builder: (_, snap) => MapMarker(snap.data),\\n )\\n ],\\n)\\n
\\nFutureBuilder和StreamBuilder作为Flutter异步编程的核心组件,它们的正确使用能显著提升应用质量。但要注意:
\\n本文主要是用几个简单的示例了解一下FlutterBuilder和StreamBuilder的使用。
","description":"在Flutter开发中,处理异步数据与UI的同步是每个开发者必须掌握的技能。FutureBuilder和StreamBuilder这两个\\"异步构建器\\"组件,正是为解决这一核心问题而生。它们将异步操作与Widget构建完美结合,让数据流动变得可视化,是构建响应式界面的利器。 同样,如果想进阶为Flutter资深开发人员,异步交互这条路是必须要走一趟的。\\n\\n一、FutureBuilder基本用法与核心属性\\n2.1 组件构造与原理\\nFutureBuilderFlutter 的 UI 渲染基于 Widget 树的声明式编程模型。当状态变化(例如通过 setState)时,Flutter 需要比较新旧 Widget 树,决定哪些部分需要重建(rebuild)并更新渲染。这个过程由 Element 树中的 update 方法驱动,而 StatefulElement 是处理 StatefulWidget 更新的核心类。
\\nDiff 算法的核心目标:
\\n关键类:
\\n让我们直接进入 flutter/lib/src/widgets/framework.dart 中 StatefulElement 的 update 方法源码,逐步分析其逻辑。
\\n(1) StatefulElement 的定义
\\ndart
\\nclass StatefulElement extends ComponentElement {\\n StatefulElement(StatefulWidget widget) : super(widget) {\\n _state = widget.createState();\\n _state._element = this;\\n _state._widget = widget;\\n }\\n\\n State<StatefulWidget> _state;\\n\\n @override\\n Widget build() => _state.build(this);\\n\\n @override\\n void update(StatefulWidget newWidget) {\\n super.update(newWidget);\\n // 更新 _widget 引用\\n _widget = newWidget;\\n _state._widget = newWidget;\\n // 触发 State 的 didUpdateWidget\\n _state.didUpdateWidget(newWidget as StatefulWidget);\\n // 标记需要重建\\n _dirty = true;\\n rebuild();\\n }\\n}\\n
\\n关键点:
\\n(2) update 方法的执行流程
\\nStatefulElement 的 update 方法主要完成以下步骤:
\\n让我们逐行分析 update 方法的源码:
\\na. 调用父类的 update 方法
\\ndart
\\nsuper.update(newWidget);\\n
\\nComponentElement 的 update 方法(flutter/lib/src/widgets/framework.dart):
\\ndart
\\nclass ComponentElement extends Element {\\n @override\\n void update(Widget newWidget) {\\n assert(newWidget.runtimeType == _widget.runtimeType);\\n _widget = newWidget;\\n }\\n}\\n
\\n作用:
\\nDiff 算法的第一步:检查类型是否匹配。如果类型不同(例如从 StatelessWidget 变为 StatefulWidget),Element 无法复用,会触发完整的重建(通过 mount 而非 update)。
\\nb. 更新 _widget 和 _state._widget
\\ndart
\\n_widget = newWidget;\\n_state._widget = newWidget;\\n
\\n作用:
\\n为什么需要更新 _widget?
\\nc. 调用 didUpdateWidget
\\ndart
\\n_state.didUpdateWidget(newWidget as StatefulWidget);\\n
\\nState 类的 didUpdateWidget 方法(flutter/lib/src/widgets/framework.dart):
\\ndart
\\nabstract class State<T extends StatefulWidget> {\\n void didUpdateWidget(covariant T oldWidget) {}\\n}\\n
\\n作用:
\\n实践案例:
\\ndart
\\nclass _MyCounterState extends State<MyCounter> {\\n @override\\n void didUpdateWidget(covariant MyCounter oldWidget) {\\n super.didUpdateWidget(oldWidget);\\n print(\'Widget updated: ${oldWidget.hashCode} -> ${widget.hashCode}\');\\n }\\n}\\n
\\nd. 标记需要重建并调用 rebuild
\\ndart
\\n_dirty = true;\\nrebuild();\\n
\\n标记 _dirty:
\\n调用 rebuild:
\\nrebuild 方法(flutter/lib/src/widgets/framework.dart):
\\ndart
\\nvoid rebuild({ bool force = false }) {\\n if (!_dirty && !force) return;\\n _dirty = false;\\n performRebuild();\\n}\\n
\\nperformRebuild 在 ComponentElement 中实现:
\\ndart
\\n@override\\nvoid performRebuild() {\\n Widget? built;\\n try {\\n built = build();\\n _child = updateChild(_child, built, slot);\\n } finally {\\n _dirty = false;\\n }\\n}\\n
\\n作用:
\\nFlutter 的 diff 算法主要在 Element 树的 update 和 updateChild 方法中实现。以下是 StatefulElement 更新时的 diff 过程:
\\n(1) Widget 类型检查
\\n(2) Key 的作用
\\nWidget.key(flutter/lib/src/widgets/framework.dart)用于标识 Widget 的唯一性:
\\ndart
\\nabstract class Widget {\\n final Key? key;\\n}\\n
\\n在 updateChild 方法中,Flutter 使用 key 匹配新旧 Widget:
\\ndart
\\nElement? updateChild(Element? child, Widget? newWidget, Object? newSlot) {\\n if (newWidget == null) {\\n if (child != null) deactivateChild(child);\\n return null;\\n }\\n if (child != null) {\\n if (child.widget == newWidget) return child;\\n if (Widget.canUpdate(child.widget, newWidget)) {\\n child.update(newWidget);\\n return child;\\n }\\n deactivateChild(child);\\n }\\n return inflateWidget(newWidget, newSlot);\\n}\\n
\\nWidget.canUpdate:
\\ndart
\\nstatic bool canUpdate(Widget oldWidget, Widget newWidget) {\\n return oldWidget.runtimeType == newWidget.runtimeType\\n && oldWidget.key == newWidget.key;\\n}\\n
\\n逻辑:
\\n(3) 子树递归更新
\\n(4) 优化机制
\\n让我们通过一个修改后的 MyCounter 示例,观察 diff 算法的行为。
\\n示例代码
\\ndart
\\nimport \'package:flutter/material.dart\';\\n\\nvoid main() {\\n runApp(const MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n const MyApp({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n home: Scaffold(\\n appBar: AppBar(title: const Text(\'Diff 算法测试\')),\\n body: const Center(\\n child: MyCounter(),\\n ),\\n ),\\n );\\n }\\n}\\n\\nclass MyCounter extends StatefulWidget {\\n const MyCounter({super.key});\\n\\n @override\\n State<MyCounter> createState() => _MyCounterState();\\n}\\n\\nclass _MyCounterState extends State<MyCounter> {\\n int _counter = 0;\\n\\n void _incrementCounter() {\\n setState(() {\\n _counter++;\\n });\\n }\\n\\n @override\\n void didUpdateWidget(covariant MyCounter oldWidget) {\\n super.didUpdateWidget(oldWidget);\\n print(\'didUpdateWidget called: ${oldWidget.hashCode} -> ${widget.hashCode}\');\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Column(\\n mainAxisAlignment: MainAxisAlignment.center,\\n children: [\\n Text(\'计数: $_counter\', key: ValueKey(\'text-$_counter\')),\\n ElevatedButton(\\n onPressed: _incrementCounter,\\n child: const Text(\'增加\'),\\n ),\\n ],\\n );\\n }\\n}\\n
\\n分析
\\n触发更新:
\\nDiff 过程:
\\n子树更新:
\\nColumn 的子节点(Text 和 ElevatedButton)通过 updateChild 比较:
\\n渲染更新:
\\n验证方法
\\n基于 StatefulElement 的 update 和 diff 算法,高级开发者需要关注以下优化:
\\n使用 const 构造函数:
\\n合理使用 Key:
\\n最小化 setState 范围:
\\n复用 State:
\\nStatefulElement.update 的核心逻辑:
\\nDiff 算法的关键:
\\n实践意义:
\\n在日常的 Flutter 开发中,我们常常会在一些类上看到 @immutable
这个注解,尤其是在定义 Widget
或 State
时。但它到底有什么作用?加和不加有什么区别?今天我们就来深入聊聊这个小细节背后的大意义。
@immutable
?在 Dart 中,@immutable
是一个用于标记类为不可变的注解。加上它之后,意味着这个类的所有字段都必须是 final,一旦创建实例就不能再修改。
它来自 Flutter 的 foundation.dart
:
import \'package:flutter/foundation.dart\';\\n\\n@immutable\\nclass Person {\\n final String name;\\n final int age;\\n\\n const Person({required this.name, required this.age});\\n}\\n
\\n对比项 | 加了 @immutable | 没加 |
---|---|---|
字段是否必须 final | ✅ 是 | ❌ 否 |
静态分析器警告 | ✅ 有 | ❌ 无 |
是否支持 const 构造 | ✅ 支持 | ❌ 不支持 |
适合状态管理 | ✅ 更安全 | ❌ 容易出 Bug |
性能优化(const 复用) | ✅ 是 | ❌ 否 |
看个例子👇:
\\nclass User {\\n final String name;\\n int age; // ⚠️ 非 final 字段\\n\\n User({required this.name, required this.age});\\n}\\n
\\n虽然代码能编译运行,但这违背了“不可变”的设计理念。当你在状态管理(如 Bloc、Provider)中复用这个对象时,如果只修改了字段值,但没有创建新的对象,Flutter 是无法感知变化的,界面可能就不会更新!
\\nFlutter 的核心机制是「重建界面」。每次状态变化时,Flutter 会比较前后的 Widget 是否发生变化,如果是同一个实例对象,Flutter 默认认为没有变化。
\\n这就是为什么状态类和 Widget 类通常都使用不可变写法:
\\n@immutable\\nclass CounterState {\\n final int count;\\n const CounterState(this.count);\\n}\\n
\\n如果你把 count
改为普通变量,虽然看起来“更方便”,但 UI 很可能不刷新,调试成本也会飙升。
@immutable
const
使用,提升性能freezed
,它默认会自动加上在 Flutter 中使用 @immutable
,并不是“可有可无”的细节,而是一个保障代码可维护性、可预测性和性能优化的重要机制。
你可能在不知不觉中忽略了它,但一旦项目复杂起来,状态不同步、UI 不刷新、调试困难等问题就会找上门来。
\\n从今天起,为你的状态类、数据类加上 @immutable
吧,养成良好的编码习惯,未来你会感谢现在的自己!
如果这篇文章对你有帮助,欢迎点赞、在看和转发。关注我,一起写出更优雅的 Flutter 代码!
\\n📮 微信公众号:OldBirds
","description":"在日常的 Flutter 开发中,我们常常会在一些类上看到 @immutable 这个注解,尤其是在定义 Widget 或 State 时。但它到底有什么作用?加和不加有什么区别?今天我们就来深入聊聊这个小细节背后的大意义。 一、什么是 @immutable?\\n\\n在 Dart 中,@immutable 是一个用于标记类为不可变的注解。加上它之后,意味着这个类的所有字段都必须是 final,一旦创建实例就不能再修改。\\n\\n它来自 Flutter 的 foundation.dart:\\n\\nimport \'package:flutter/foundation.dart\'…","guid":"https://juejin.cn/post/7506408162737291279","author":"OldBirds","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-21T01:35:29.738Z","media":null,"categories":["iOS","前端","Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Dart 3.8发布,新格式化,新语法支持","url":"https://juejin.cn/post/7506414257400053799","content":"其实在此之前,我们就介绍过《Dart 3.8 开始支持 Null-Aware Elements 语法》 ,而本次 Dart 版本更新,主要带来了新的格式化更新,Null-Aware Elements 语法和 Web hot reload 支持等,另外还有 FFigen 和 JNIgen 互操作的未来计划。
\\n在上一个版本里,Dart 包含一个在很重写的格式化支持,它支持新的 “tall”样式 ,而本次 Dart 3.8 版本包含了针对大家反馈问题的修复,并添加了其他改进。
\\n在以前的版本里,尾部逗号会强制拆分周围的结构,新的格式化程序现在会根据实际情况,再决定是否应拆分结构,然后根据需要添加或删除尾部逗号:
\\n// Before formatter\\nTabBar(tabs: [Tab(text: \'A\'), Tab(text: \'B\')], labelColor: Colors.white70);\\n\\n// After formatter\\nTabBar(\\n tabs: [\\n Tab(text: \'A\'),\\n Tab(text: \'B\'),\\n ],\\n labelColor: Colors.white70,\\n);\\n
\\n当然,如果你更喜欢旧行为,可以使用配置标志重新启用:
\\nformatter:\\n trailing_commas: preserve\\n
\\n另外,关于样式还添加了许多样式更改,以收紧并改善输出,例如:
\\n// Previously released formatter (functions)\\nfunction(\\n name:\\n (param) => another(\\n argument1,\\n argument2,\\n ),\\n);\\n\\n// Dart 3.8 formatter (functions)\\nfunction(\\n name: (param) => another(\\n argument1,\\n argument2,\\n ),\\n);\\n\\n// Previously released formatter (variables)\\nvariable =\\n target.property\\n .method()\\n .another();\\n\\n// Dart 3.8 formatter (variables)\\nvariable = target.property\\n .method()\\n .another();\\n\\n
\\n在之前的 《Dart 开始支持交叉编译》 我们就聊到了,Dart 新增了对从 Windows、macOS 和 Linux 开发机器编译为原生 Linux 二进制文件的支持,现在可以使用 dart compile exe
或 dart compile aot-snapshot
命令以及 --target-os
和 --target-arch
标志来做到这一点:
\\n\\n例如
\\ndart compile exe --target-os=linux --target-arch=x64 hello.dart -o hello
有了交叉编译,开发人员可以更方便在当前设备为嵌入式设备(例如 Raspberry Pi)进行更快的编译,在非 Linux 开发人员上更快地编译基于 Linux 的后端。
\\nNull-Aware Elements 语法糖可以用于在 List、Set、Map 等集合中处理可能为 null 的元素或键值对,简化显式检查 null 的场景:
\\n/////////////////之前\\nvar listWithoutNullAwareElements = [\\n if (promotableNullableValue != null) promotableNullableValue,\\n if (nullable.value != null) nullable.value!,\\n if (nullable.value case var value?) value,\\n];\\n\\n/////////////////之后\\nvar listWithNullAwareElements = [\\n ?promotableNullableValue,\\n ?nullable.value,\\n ?nullable.value,\\n];\\n
\\n自然,在 Flutter 的 UI 声明里,也可以简化之前控件的 if 判断,不得不说确实比起之前的写法优雅不少:
\\n\\nStack(\\n fit: StackFit.expand,\\n children: [\\n const AbsorbPointer(),\\n if (widget.child != null) widget.child!,\\n ],\\n)\\n\\n/////////////////之后\\nStack(\\n fit: StackFit.expand,\\n children: [\\n const AbsorbPointer(),\\n ?widget.child,\\n ],\\n)\\n
\\n而事实上,从以下例子可以看出来,在简化 Map
上 Null-Aware Elements 的作用尤为明显:
js 体验AI代码助手 代码解读复制代码/////////////////之前\\nfinal tag = Tag()\\n ..tags = {\\n if (Song.title != null) \'title\': Song.title,\\n if (Song.artist != null) \'artist\': Song.artist,\\n if (Song.album != null) \'album\': Song.album,\\n if (Song.year != null) \'year\': Song.year.toString(),\\n if (comments != null)\\n \'comment\': comms!\\n .asMap()\\n .map((key, value) => MapEntry<String, Comment>(value.key, value)),\\n if (Song.numberInAlbum != null) \'track\': Song.numberInAlbum.toString(),\\n if (Song.genre != null) \'genre\': Song.genre,\\n if (Song.albumArt != null) \'picture\': {pic.key: pic},\\n }\\n ..type = \'ID3\'\\n ..version = \'2.4\';\\n\\n/////////////////之后\\nfinal tag = Tag()\\n ..tags = {\\n \'title\': ?Song.title,\\n \'artist\': ?Song.artist,\\n \'album\': ?Song.album,\\n \'year\': ?Song.year?.toString(),\\n if (comments != null)\\n \'comment\': comms!\\n .asMap()\\n .map((key, value) => MapEntry<String, Comment>(value.key, value)),\\n \'track\': ?Song.numberInAlbum?.toString(),\\n \'genre\': ?Song.genre,\\n if (Song.albumArt != null) \'picture\': {pic.key: pic},\\n }\\n ..type = \'ID3\'\\n ..version = \'2.4\';\\n
\\n更多可见:《Dart 3.8 开始支持 Null-Aware Elements 语法》
\\n现在 3.8 支持 doc imports,,这是一种新的基于注释的语法,允许在文档注释中引用外部元素而无需实际导入它们,例如 [Future]
和 [Future.value]
是从 dart:async
库导入的:
/// @docImport \'dart:async\';\\nlibrary;\\n\\n/// Doc comments can now reference elements like\\n/// [Future] and [Future.value] from `dart:async`,\\n/// even if the library is not imported with an\\n/// actual import.\\nclass Foo {}\\n
\\nDoc imports 支持与常规 Dart 导入相同的 URI 样式,包括 dart:
scheme、package:
scheme 和相对路径。但是它们不能被延迟或配置 as
、show
、hide
。
pub.dev 将“Most Popular Packages”替换为“Trending Packages” ,并展示了最近在采用率和社区兴趣方面表现出显著增长的包:
\\n现在,在使用 Dart Development Compiler (DDC) 时,Web 上可以使用有状态的热重载,该功能仍在迭代中,但 Dart 3.8 提供了第一次尝试它的机会:flutter run -d chrome --web-experimental-hot-reload
本次正式推出 FFigen 和 JNIgen 的早期访问计划,目的是简化原生平台 API 集成的 codegen 解决方案,不再需要 channel ,可以实现同步调用的场景。
\\n在这里,FFIgen 将生成绑定来包装 Objective-C 和 Swift API,而同样,JNIgen 将对 Java 和 Kotlin API 执行相同的生成和绑定。
\\n并且和 channel 不同的是,FFIgen 和 JNIgen 除了支持同步调用 API,还支持 tree-shaking(在编译期间删除未使用的代码),并允许更多数据存在于平台层。
\\n从本次看来,Dart 3.8 的更新比平平无奇的 Flutter 3.32 有意思不少。
","description":"其实在此之前,我们就介绍过《Dart 3.8 开始支持 Null-Aware Elements 语法》 ,而本次 Dart 版本更新,主要带来了新的格式化更新,Null-Aware Elements 语法和 Web hot reload 支持等,另外还有 FFigen 和 JNIgen 互操作的未来计划。 Formatter updates\\n\\n在上一个版本里,Dart 包含一个在很重写的格式化支持,它支持新的 “tall”样式 ,而本次 Dart 3.8 版本包含了针对大家反馈问题的修复,并添加了其他改进。\\n\\n在以前的版本里,尾部逗号会强制拆分周围的结构…","guid":"https://juejin.cn/post/7506414257400053799","author":"恋猫de小郭","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-21T00:46:02.949Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f09cc787762d4afa9b55abbe4e19189a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1748393162&x-signature=Lfk2leUy8W4OdDtG3zD9BgNr%2B9c%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d13ef89d7f1b46b0b61ee6c8ec65f3e9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1748393162&x-signature=h%2FgizL3ksuvyjNR70mWj1c9WvgA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/59b509aa6dda46e891ff884c1697b224~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1748393162&x-signature=EQMrw8MLNew60kWh0KWeXHZ8m6I%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter进阶:通过脚本自动生成 iOS 隐私文件 PrivacyInfo.xcprivacy","url":"https://juejin.cn/post/7506287644818997285","content":"iOS app提交商店时需要新增隐私文件 PrivacyInfo.xcprivacy。通过人工取一个一个 Pod 查看粘贴 PrivacyInfo.xcprivacy 内容太过低效,随想通过脚本每次 pod 安装时自动生成。
\\n1、generate_privacy_info.py 放在 iOS根目录下。
\\n2、在 Podfile 中添加:
\\npost_install do |installer|\\n system(\\"python3 generate_privacy_info.py\\")\\n ...\\n \\nend\\n
\\n执行 pod install 之后会在 iOS 目录下生成 PrivacyInfo.xcprivacy 文件,将此文件添加到 iOS 项目中即可。
\\n1、因为我本地是 python3 是环境,所以此处是 python3 命令。\\n2、生成的文件未做过滤,因为苹果只检测你有没有配置隐私声明,所以重复声明不影响使用。\\n3、用脚本生成隐私文件才是最简单高效的方式,否则每次添加新的 pod 库都要去确认一遍该库是否包含隐私文件,还要确认有哪些隐私条目,然后一条一条粘贴到项目中的隐私文件去,机械又低效。AI时代,他是你最好的编程助手!
","description":"一、需求来源 iOS app提交商店时需要新增隐私文件 PrivacyInfo.xcprivacy。通过人工取一个一个 Pod 查看粘贴 PrivacyInfo.xcprivacy 内容太过低效,随想通过脚本每次 pod 安装时自动生成。\\n\\n二、使用示例\\n\\n1、generate_privacy_info.py 放在 iOS根目录下。\\n\\n2、在 Podfile 中添加:\\n\\npost_install do |installer|\\n system(\\"python3 generate_privacy_info.py\\")\\n ...\\n \\nend\\n\\n\\n执行 pod…","guid":"https://juejin.cn/post/7506287644818997285","author":"SoaringHeart","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-20T23:20:26.196Z","media":null,"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 3.32 发布,快来看有什么更新吧","url":"https://juejin.cn/post/7506408162736766991","content":"Flutter 3.32 来了,本次核心更新主要集中在 Framework 的控件调整上,当然还是离不开 iOS 风格的控件优化,另外还有一些重大变更(重命名和 API 弃用),整体来看这个版本的迭代并不大,升级成本也不高,应该不会有什么大坑。
\\nWeb 上的 hot reload 开始了实验性支持,需要在 flutter run -d chrome
时增加 --web-experimental-hot-reload
,或者在 VS Code 的 launch.json 配置:
\\"configurations\\": [\\n…\\n {\\n \\"name\\": \\"Flutter for web (hot reloadable)\\",\\n \\"type\\": \\"dart\\",\\n \\"request\\": \\"launch\\",\\n \\"program\\": \\"lib/main.dart\\",\\n \\"args\\": [\\n \\"-d\\",\\n \\"chrome\\",\\n \\"--web-experimental-hot-reload\\",\\n ]\\n }\\n]\\n
\\n\\n\\nDartPad 上也提供了热重载功能,并新增了 Reload 按钮,这很关键,之前的体验确实太差了。
\\n
目前,官方正在努力尝试将 Material 里的通用功能转移到独立的 Widget,但是这肯定是一个漫长的过程,详细可见:github.com/flutter/flu… 。
\\n在 3.32 增加了一个新的 Expansible
控件,支持创建具有不同视觉主题的展开和折叠的 Widget,由一个 header 和一个 body 组成,header 始终显示,body 默认折叠状态 ,可以配置 ListView 一起使用。
另外一个就是 RawMenuAnchor
,支持创建具有不同视觉效果的菜单,还可以独立用作无样式菜单:
shape 在 3.32 功能引入一个新增功能:rounded superellipse ,这种形状通常被称为“Apple squircle”,是 iOS 设计的基石,与传统的圆角矩形相比,它以其更平滑且具有更连续的曲线,其中 CupertinoAlertDialog
和 CupertinoActionSheet
都已更新为使用这个新形状 :
\\n\\n其他还有:
\\nRoundedSuperellipseBorder
、ClipRSuperellipse
、Canvas.drawRSuperellipse, Canvas.clipRSuperellipse
, 和Path.addRSuperellipse
。
需要注意的是,目前这个能力暂时在 iOS 和 Android 上支持,性能也有初步优化中。
\\n3.32 还修复了 sheet 的固定导航栏的高度并确保内容不会出现在底部被截断的情况:
\\n另外 ,Sheet 之前和 PopupMenuButton
的过渡不兼容的问题也得到修复,并且改进了工作表的圆角过渡效果:
CupertinoSliverNavigationBar.search
在打开或关闭搜索视图时,可以看到对应的动画的保真度改进,以及搜索的前缀和后缀图标的正确切换 :
最后,使用 CupertinoNavigationBars
或 CupertinoSliverNavigationBars
的路由之间的过渡也已更新,适配支持了最新的 iOS 过渡 :
3.32 开始,CarouselController
提供了 animateToIndex
方法,无论是使用 flexWeights
的固定大小还是动态大小的项目,都可以在轮播中提供基于索引的导航:
TabBar 现在支持 onHover
和 onFocusChange
回调:
SearchAnchor
和 SearchAnchor.bar
现在分别包含 viewOnOpen
和 onOpen
回调 :
CalendarDatePicker
现在支持 calendarDelegate
,可以在公历系统之外集成自定义日历逻辑,比如下方自定义了一其中偶数月有 21 天,奇数月有 28 天,每个月都从星期一开始:
其他控件调整还有:
\\nshowDialog
、showAdaptiveDialog
和 DialogRoute
添加 animationStyle
,从而在打开和关闭对话框时自定义动画Divider
支持 borderRadius
自定义分隔线的边框,尤其是在分隔线较粗的情况DropdownMenu
允许它的菜单宽度小于文本字段RangeSlider
滑块上时,仅显示悬停的滑块的叠加层3.32 将语义编译时间缩短了 ~80%,在 Flutter for web 中意味着启用语义后, frame time 将减少 30% 。
\\n\\n\\n语义树大概率未来会和 SEO 优化有关。
\\n
而新的 SemanticsRole
API 已集成到 Semantics
及其关联组件,主要是增强了允许将特定角色分配给 Widget 的整个子树:
import \'package:flutter/material.dart\';\\nimport \'package:flutter/semantics.dart\';\\n\\n\\nclass MyCustomListWidget extends StatelessWidget {\\n const MyCustomListWidget({Key? key}) : super(key: key);\\n\\n @override\\n Widget build(BuildContext context) {\\n // This example shows how to explicitly assign list and listitem roles\\n // when building a custom list structure. \\n return Semantics(\\n role: SemanticsRole.list,\\n explicitChildNodes: true,\\n child: Column( \\n children: <Widget>[\\n Semantics(\\n role: SemanticsRole.listItem, \\n child: const Padding(\\n padding: EdgeInsets.all(8.0),\\n child: Text(\'Content of the first custom list item.\'),\\n ),\\n ),\\n Semantics(\\n role: SemanticsRole.listItem, \\n child: const Padding(\\n padding: EdgeInsets.all(8.0),\\n child: Text(\'Content of the second custom list item.\'),\\n ),\\n ),\\n ],\\n ),\\n );\\n }\\n}\\n
\\n同时改进的还有:
\\nSemantics.linkUrl
或 url_launcher
软件包中的 Link
widget 定义的链接ThemeData
中设置 useSystemColors
,以自动将系统颜色应用于 Flutter 主题。3.32 李文本输入进行了许多改进,比如:
\\n系统文本选择上下文菜单在 iOS 上启动
\\nAutocomplete
控件选项的布局被移植到 OverlayPortal
可以在文本字段中自定义 onTapUpOutside
的行为 (#162575)
开发人员现在可以生成他们想要的任何 Widget 作为 FormField
的错误消息,而不仅仅是错误文本 (#162255)。
今天看来多窗口有望落地,目前 PC 端功能,特别是多窗口支持,基本都是 Canonical 在负责,目前 Canonical 已经修复了当具有多个窗口时损坏的几项功能:
\\nCanonical 还添加了一项功能,允许 Dart FFI 直接与 Flutter 引擎通信 (#163430),这也为 Flutter 未来的窗口 API 奠定了基础。
\\n最后,Canonical 嗨在 Linux 上引入了光栅线程 (#161879),从而提高了帧吞吐量,确保 Flutter Linux 即使在有多个窗口时也能保持流畅。
\\n\\n\\n未来大概率 PC 端都是由 Canonical 团队负责推进落地。
\\n
Canonical 还更新了 Windows 和 macOS,从而支持 App 合并 UI 和平台线程(#162883、#162935),从这点看桌面端也追上了移动端进程。
\\n合并线程可以让 App 使用 Dart FFI 与原生 API 进行直接互作,例如在 Windows 上启用了合并线程,开发者就可以使用 Dart FFI 通过 win32 API 直接调整应用窗口的大小\\n在 Windows 上,可以通过在 wWinMain
方法中将以下内容添加到 windows/runner/main.cpp
文件中来打开合并的线程:
project.set_ui_thread_policy(UIThreadPolicy::RunOnPlatformThread)\\n
\\n在 macOS 上,可以通过将以下内容添加到 macos/Runner/Info.plist
文件中的 <dict>
元素中来打开合并线程:
<key>FLTEnableMergedPlatformUIThread</key>\\n<true />\\n
\\n而在未来, Windows 和 macOS 将默认启用合并线程。
\\n3.32 增强了 iOS 上 Flutter 的粘贴体验,对于没有自定义作的基本文本字段,用户在粘贴其他应用的内容时将不再看到确认对话框,现在,所有 Flutter iOS 应用都默认启用该功能。
\\n\\n\\n请注意,如果 App 使用自定义 action(例如 context menus 的“发送电子邮件”),还暂时不支持该能力:#140184。
\\n
Flutter 的 Gradle 插件已经从 Groovy 转换为 Kotlin。
\\n另外,现在 Flutter 可以在 Android 上使用触控笔写入文本字段,就像 Apple Pencil 手写输入在 Flutter iOS 上一样,用户可以直接在任何 Flutter 文本输入字段上开始书写,手写内容将在字段中显示为文本,但是目前尚不支持所有手势,目前只在 Android 14 及更高版本上支持,可以使用 TextField.stylusHandwritingEnabled
或者 CupertinoTextField.stylusHandwritingEnabled
禁用。
从 3.29.3 版本开始,在 Android API 级别 28 (Android 9) 及更早版本的设备上,Flutter 应用将使用旧版 Skia 渲染器,而 Impeller 仍然是 API 级别 29 (Android 10) 及更高版本的设备上的默认渲染器。
\\n另外,在 3.32 版本里,以下设备将使用 OpenGLES 而不是 Vulkan:
\\n最后需要注意的是,Flutter 3.27 存在许多与支持 Vulkan 的设备上的 Impeller 渲染相关的渲染错误和崩溃,这些错误和崩溃已在 3.29 及更高版本中修复,因为这些修复没有在 3.27 中 hot fix,所以强烈建议大家更新到 3.29 或更高版本。
\\n同时,3.32 还改进了 Impeller 的文本渲染,从而让 Impeller 字形图集中的字形分辨率更高,文本动画更流畅,抖动更少,并修复了浮点计算中的舍入错误,以下是之前(上)和3.32(下)的对比:
\\n另外还有各种其他保真度和性能改进,包括:
\\n从新的 Property Editor 工具轻松编辑 Widget 属性并阅读文档,该工具可以从 Flutter Property Editor 侧边栏面板 (VS Code) 或工具窗口 (Android Studio / IntelliJ) 访问:
\\nDevTools 还进行了其他改进,包括:
\\n另外还改进 Dart 分析器,添加了 “doc imports”,这是一种新的基于注释的语法,允许在文档注释中引用外部元素:
\\n/// @docImport \'dart:async\';\\nlibrary;\\n\\n/// Doc comments can now reference elements like\\n/// [Future] and [Future.value] from `dart:async`,\\n/// even if the library is not imported with an actual import.\\nclass Foo {}\\n
\\n现在 AS 的 Gemini 可以为 Dart 和 Flutter 开发提供直接的支持,另外 Dart 和 Flutter 对模型上下文协议 (MCP) 的支持即将推出,MCP 和最近发布的 Dart MCP SDK 的支持正在积极进行中,新的 Dart Tooling MCP Server 也正在开发中,它将向 MCP 客户端(如 IDE)公开 Dart 和 Flutter 静态、运行时和生态系统工具。
\\n这将为 Dart 和 Flutter 开发人员带来以下好处:
\\n从今天开始,Flutter 正在将 Firebase 中的 Vertex AI 发展为 Firebase AI Logic,只需一个 Flutter SDK 即可访问两个 Gemini API 提供商,从而支持能够直接从 Flutter 应用使用 Gemini 和 Imagen 模型,而无需服务器端 SDK。
\\n\\n\\n对应
\\nfirebase_ai
或者firebase_vertexai
包
在 Android 上,自 API 36 起 ,accessibility announcements 事件现已弃用,相反可以通过配置 SemanticProperties.liveRegion
来使用“polite”隐式公告,目前在配置为不应聚焦的文本时存在一个已知限制。
flutter_markdown
#162966ios_platform_images
#162961css_colors
#162962palette_generator
#162963flutter_image
#162964flutter_adaptive_scaffold
#162965Flutter 将在下一个稳定版本中弃用对 iOS 12 和 macOS 10.14 (Mojave) 的支持,并将针对最低 iOS 13 和 macOS 10.15 (Catalina) 提供支持。
\\nExpansionTileController
,取而代之的是 Widgets 层中新的可重用 ExpansibleController
SelectionChangedCause.scribble
(已弃用)为 SelectionChangedCause.stylusHandwriting
,因为 Apple 的 Scribble 功能现在与 Android 的 Scribe 统一。ThemeData.indicatorColor
已被弃用,取而代之的是 TabBarThemeData.indicatorColor
而 cardTheme
、dialogTheme
和 tabBarTheme
的组件主题类型将需要分别迁移到 CardThemeData
、DialogThemeData
和 TabBarThemeData
SpringDescription
公式已得到更正那么,少年,你准备好吃螃蟹了吗?目前来看,这并不是一个会有什么大坑的版本。
\\n\\n\\nabstract、interface、final、base、mixin、以及它们的各种组合,各种代表啥?有啥用?
\\n
先上一个令人迷糊的,这是官网的示例:
\\n\\n它说
Car
不能继承Vehicle
,但是我复制代码到一个测试文件里,它并没有报错:
我有那么一瞬间认为官网的搞错了!后来才发现,它这个跟_
是一个玩法:都要区分文件。这样改为2个文件就能正常报错了:
回到标题,鉴于太懒不想写,直接贴官网的表格:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\nDeclaration | Construct? | Extend? | Implement? | Mix in? | Exhaustive? |
---|---|---|---|---|---|
class | Yes | Yes | Yes | No | No |
base class | Yes | Yes | No | No | No |
interface class | Yes | No | Yes | No | No |
final class | Yes | No | No | No | No |
sealed class | No | No | No | No | Yes |
abstract class | No | Yes | Yes | No | No |
abstract base class | No | Yes | No | No | No |
abstract interface class | No | No | Yes | No | No |
abstract final class | No | No | No | No | No |
mixin class | Yes | Yes | Yes | Yes | No |
base mixin class | Yes | Yes | No | Yes | No |
abstract mixin class | No | Yes | Yes | Yes | No |
abstract base mixin class | No | Yes | No | Yes | No |
mixin | No | No | Yes | Yes | No |
base mixin | No | No | No | Yes | No |
这表格如此之大咱也不能硬背啊(主要是咱人老了记忆力不行了)!!!这里我只做几个总结帮助大家快速记忆和区分:
\\ninterface
的就表示他是一个接口(废话),它就能被实现且不能被继承!abstract
的就不能有构造方法,同时也就拥有了添加抽象成员的能力。mixin
的才能混入。sealed class
interface class
与abstract interface class
的区别是:后者是一个更纯粹的接口,意思是说它更像Java之前的对接口含义的定义。前者不能定义抽象成员,所有的成员都必须自己实现一遍(可空实现),感觉失去了接口定义的含义了。
再看一下mixin
与mixin class
:
先看代码↓
\\nvoid main(List<String> arguments) {\\n final aBu = ABu();\\n aBu.hello();\\n\\n final aNiu = ANiu();\\n aNiu.hello();\\n}\\n\\nabstract interface class Animal {\\n String get name;\\n}\\n\\nmixin CatMixin implements Animal {\\n void yell() {\\n print(\'$name is yelling\');\\n }\\n}\\n\\nmixin class CatMixinClass implements Animal {\\n /// 可以有一个无参构造函数\\n CatMixinClass();\\n\\n /// 但是必须要实现这个Animal的name getter\\n @override\\n String get name => \'Cat\';\\n\\n void yell() {\\n print(\'$name is yelling\');\\n }\\n}\\n\\nclass ABu with CatMixin {\\n @override\\n String get name => \'A Bu\';\\n\\n void hello() {\\n yell();\\n }\\n}\\n\\nclass ANiu with CatMixinClass {\\n void hello() {\\n yell();\\n }\\n}\\n
\\n运行后输出:
\\nA Bu is yelling\\nCat is yelling\\n
\\n简单总结:虽然2者都可以都可以被混入,不过概念上还是有点区别:
\\nmixin
是一个纯粹的混入,它是无状态的mixin class
本质上还是一个类,但是它经过mixin
的修饰后,也约束了它一些能力。比如:\\n哈喽,我是老刘
\\n老刘前段时间接到两个外包项目,都属于是中途接手的类型。
\\n共同特点是都是东拼西凑的缝合怪。
\\n其中一个短期项目使用Riverpod重构状态管理部分后已经顺利交付了。
\\n这里把重构后Riverpod的使用场景和大家的疑问梳理出来,作为其它项目的一个参考。
\\n注意本文不是Riverpod的基础讲解,主要是为了梳理应用场景。
\\n学习Riverpod基础用法还是建议去看官网的说明,里面有很详尽的使用讲解。
和一个客户的开发人员交流的时候我提到建议换成Riverpod,他也同意,理由是因为官方推荐。
\\n我想说的是官方推荐确实能增加信任背书,但是我们做技术选型的时候还是要考虑项目自身的情况,应该要清楚的知道选择一个技术方案的理由。
\\n其实老刘自己的团队一直以来使用的是bloc,因为做了比较深入的封装,各种业务场景也都覆盖到了,所以没有替换的需求。
\\n那为啥我会给客户推荐Riverpod呢?我觉得有以下几个原因:
说了不少Riverpod的优点,但是在实践中发现Riverpod对于初学者来说还是容易理解不到位。
\\n所以接下来我们先来看一下Riverpod的基本思路。
通常在应用App的架构体系中我们会拆分成至少三层:
\\n
\\n比如我们要开发一个商品详情页,就会有详情页的UI、负责业务逻辑的模块和底层支持模块比如服务端Api。
\\n如果使用之前比较常用的bloc作为状态管理,App的多个页面大概如下图所示:
\\n
\\n其中每一个UI对应一个页面,每一个BLoC对应一个页面的业务逻辑。
\\n当然也会有一些Bloc为全局多个页面提供服务。
\\n这样当Bloc中的业务逻辑的状态产生变化时,就会通知关注这个状态的UI页面,页面响应状态变化后更新内容。
在Riverpod中也遵循这个核心理念,只不过在具体应用时做了一些扩展:
\\n因为老刘的团队本身使用bloc比较多,所以这里以bloc为视角介绍Riverpod的特点,熟悉其它状态管理框架的同学简单类比即可。
\\n接下来我们结合不同页面的场景,说明几种典型的使用方式,帮助大家理解。
下面的示例代码我会优先使用官方文档中的例子,这样大家可以更好的相互印证。
\\nRiverpod中推荐将所有的Provider在全局范围内统一管理。
\\n这样组件树的任何一个子节点都可以获取到所有的Provider,方便页面间的数据共享。
void main() {\\n runApp(\\n // To install Riverpod, we need to add this widget above everything else.\\n // This should not be inside \\"MyApp\\" but as direct parameter to \\"runApp\\".\\n ProviderScope(\\n child: MyApp(),\\n ),\\n );\\n}\\n
\\n通过全局的ProviderScope管理所有的Provider。
\\nProvider在没有关注者时会自动回收释放,不用担心内存泄露的问题。
用户打开页面后调用一个服务端接口获取数据,拿到数据后页面展示相关的信息。
\\n这种场景是App中数量最多的页面,占整个页面数量的50%左右。
\\n而Riverpod也针对这种场景做到了最极致的简化:只需要定义一个方法
import \'dart:convert\';\\nimport \'package:http/http.dart\' as http;\\nimport \'package:riverpod_annotation/riverpod_annotation.dart\';\\nimport \'activity.dart\';\\n\\n// Necessary for code-generation to work\\npart \'provider.g.dart\';\\n\\n/// This will create a provider named `activityProvider`\\n/// which will cache the result of this function.\\n@riverpod\\nFuture<Activity> activity(Ref ref) async {\\n // Using package:http, we fetch a random activity from the Bored API.\\n final response = await http.get(Uri.https(\'boredapi.com\', \'/api/activity\'));\\n // Using dart:convert, we then decode the JSON payload into a Map data structure.\\n final json = jsonDecode(response.body) as Map<String, dynamic>;\\n // Finally, we convert the Map into an Activity instance.\\n return Activity.fromJson(json);\\n}\\n
\\n在activity方法中实现调用接口并返回生成的数据结构。
\\n代码生成工具会生成对应的activityProvider。
\\nactivityProvider包含三种状态,初始状态是loading。
\\n根据服务端返回数据的不同可以输出AsyncData和AsyncError两种状态。
\\n那么在UI层面如何响应状态的变化并更新UI呢?
Riverpod中提供了两种响应状态变化的组件:Consumer和ConsumerWidget。
\\nConsumerWidget其实是对Consumer的封装,相当于在StatelessWidget中直接返回一个Consumer。
\\n如果状态变化时整个页面都需要重建,那么推荐使用ConsumerWidget。
\\n但是在实际开发的场景中我们几乎只使用了Consumer本身。
\\n原因是对大多数页面来说,跟随状态变化的只是页面的一部分。
\\n比如展示用户信息、物品信息的页面,页面的标题、返回按钮、菜单按钮以及底部的导航栏都是不随着状态变化的,只有页面中间的信息部分会随着状态更新。
\\n因此使用Consumer确保只更新必要的部分可以帮助优化性能以及简化代码结构。
\\n下面是使用场景一中activityProvider状态的UI代码:
import \'package:flutter/material.dart\';\\nimport \'package:flutter_riverpod/flutter_riverpod.dart\';\\n\\nimport \'activity.dart\';\\nimport \'provider.dart\';\\n\\n/// The homepage of our application\\nclass Home extends StatelessWidget {\\n const Home({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n return Consumer(\\n builder: (context, ref, child) {\\n // Read the activityProvider. This will start the network request\\n // if it wasn\'t already started.\\n // By using ref.watch, this widget will rebuild whenever\\n // the activityProvider updates. This can happen when:\\n // - The response goes from \\"loading\\" to \\"data/error\\"\\n // - The request was refreshed\\n // - The result was modified locally (such as when performing side-effects)\\n // ...\\n final AsyncValue<Activity> activity = ref.watch(activityProvider);\\n\\n return Center(\\n /// Since network-requests are asynchronous and can fail, we need to\\n /// handle both error and loading states. We can use pattern matching for this.\\n /// We could alternatively use `if (activity.isLoading) { ... } else if (...)`\\n child: switch (activity) {\\n AsyncData(:final value) => Text(\'Activity: ${value.activity}\'),\\n AsyncError() => const Text(\'Oops, something unexpected happened\'),\\n _ => const CircularProgressIndicator(),\\n },\\n );\\n },\\n );\\n }\\n}\\n
\\n当调用ref.watch(activityProvider)方法时,当前的Consumer成为了activityProvider的关注者。
\\n这时activityProvider的当前状态是loading,同时开始执行activity方法中的内容,去调用服务端接口。
\\n当服务端返回数据或者接口调用失败后,activityProvider会通知所有关注者(也就是当前的Consumer)状态更新为AsyncData或AsyncError。
\\nConsumer根据新的状态将展示的内容从loading组件替换为具体内容或者错误页。
\\n当用户点击返回按钮关闭页面后Consumer在回收时会取消对activityProvider的关注。
\\nactivityProvider发现没有关注者后会触发自身的dispose动作回收资源。
这里要注意一种情况,如果展示的数据是调用多个接口然后本地拼装成的,因为对外呈现的也是一个状态,因此也算是场景一,这一点要和场景二区分开。
\\n下面我们来看场景二,多接口多状态。
考虑这样一种情况,商品详情页需要展示商品信息、商品评论、根据账户情况实时核算的价格以及是否加入购物车(这里只考虑展示的场景,不包含用户交互)。
\\n这些数据可能都是单独的接口提供,并且每一种数据都是有单独的状态变化,和其它状态无关。
\\n比如新增了评论不需要更新商品信息,也不需要更新价格。
\\n这种情况下在过去使用bloc的情况下很多人可能会选择在同一个bloc中管理所有数据。
\\n不管任何一个数据产生变化,bloc会通知关注者状态变化。
\\nUI模块则可以对状态变化进行更具体的筛选,比如购物车按钮只关心是否加入购物车这一个数据,其它的过滤掉。以此来避免没有必要的UI刷新。
\\n但是在Riverpod中更推荐将商品信息、商品评论、价格以及是否加入购物车分别在不同的Provider中进行管理,分别调用各自的接口。
\\nUI层面整个页面可能是一个Statelesswidget,每种数据由一个独立的Consumer负责响应其状态变化。
\\n定义Provider的示例代码如下(注意本例中只考虑展示没有用户交互的功能):
// product_provider.dart\\nimport \'package:riverpod_annotation/riverpod_annotation.dart\';\\n\\npart \'product_provider.g.dart\';\\n\\n// 商品基本信息(异步接口)\\n@riverpod\\nFuture<Product> product(ProductRef ref, String productId) async {\\n // 调用商品信息接口\\n}\\n\\n// 商品评论(异步接口)\\n@riverpod\\nFuture<List<Review>> productReviews(ProductReviewsRef ref, String productId) async {\\n // 评论接口\\n}\\n\\n// 实时价格(依赖账户状态)\\n// 购物车状态\\n}\\n
\\n不同部分的数据用相互独立的Provider获取。
\\nUI层的代码如下:
\\nclass ProductPage extends StatelessWidget {\\n const ProductPage({required this.productId});\\n \\n final String productId;\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: const Text(\'商品详情\')),\\n body: Column(\\n children: [\\n // 商品基本信息模块\\n _buildProductHeader(),\\n \\n // 实时价格模块\\n _buildPriceDisplay(),\\n \\n // 商品评论模块\\n _buildReviewList(),\\n \\n // 购物车状态模块\\n _buildCartButton(),\\n ],\\n ),\\n );\\n }\\n\\n Widget _buildProductHeader() {\\n return Consumer(\\n builder: (context, ref, child) {\\n final productState = ref.watch(productProvider(productId));\\n \\n return Padding(\\n padding: const EdgeInsets.all(16),\\n child: switch (productState) {\\n AsyncData(:final value) => Column(\\n crossAxisAlignment: CrossAxisAlignment.start,\\n children: [\\n Text(value.name, style: Theme.of(context).textTheme.headlineSmall),\\n Text(\'库存: ${value.stock}\', style: Theme.of(context).textTheme.bodyMedium),\\n ],\\n ),\\n AsyncError(:final error) => ErrorCard(\\n message: \'商品加载失败: ${error.toString()}\',\\n onRetry: () => ref.invalidate(productProvider(productId)),\\n ),\\n AsyncLoading() => const Center(child: LoadingIndicator()),\\n _ => const SizedBox.shrink(),\\n },\\n );\\n },\\n );\\n }\\n\\n Widget _buildPriceDisplay() {\\n // 展示价格\\n }\\n\\n Widget _buildReviewList() {\\n // 展示评论\\n }\\n}\\n
\\n这里有两点需要注意:
\\n1、页面默认使用Statelesswidget类型,要求开发者主动缩小页面更新的范围。
\\n2、每个需要根据状态变化的部分独立使用Consumer,并且只关注与自己相关的Provider。
\\n也就是说可能出现商品信息已经正常展示,但是评论部分还在loading的场景。
前面两种场景都只是展示接口信息,并不包含和用户的交互。
\\n接下来我们来看一下当增加用户交互的时候如何处理,也就是Provider如何给外部提供更多的调用接口。
这次回到官网中的例子,考虑一个待办事项列表页,页面加载时获取待办列表并展示。
\\n用户可以添加新的事项。
\\n这种情况下Provider需要给外部使用者提供一个可以调用的接口用于添加事项:
@riverpod\\nclass TodoList extends _$TodoList {\\n @override\\n Future<List<Todo>> build() async {\\n // The logic we previously had in our FutureProvider is now in the build method.\\n return [\\n Todo(description: \'Learn Flutter\', completed: true),\\n Todo(description: \'Learn Riverpod\'),\\n ];\\n }\\n \\n Future<void> addTodo(Todo todo) async {\\n await http.post(\\n Uri.https(\'your_api.com\', \'/todos\'),\\n // We serialize our Todo object and POST it to the server.\\n headers: {\'Content-Type\': \'application/json\'},\\n body: jsonEncode(todo.toJson()),\\n );\\n }\\n}\\n
\\n1、_$TodoList是AsyncNotifierProvider类型。
\\n2、代码生成工具会生成TodoListProvider对象。
\\n3、build方法是这个Provider的初始化方法,这里可以理解为初始状态为loading,然后开始调用build方法初始化数据(这里是获取待办列表),获取数据成功后状态变为AsyncData。
\\n4、addTodo方法是用户点击添加按钮后调用的方法,接下来看一下如何调用这个方法。
class Example extends ConsumerWidget {\\n const Example({super.key});\\n\\n @override\\n Widget build(BuildContext context, WidgetRef ref) {\\n return ElevatedButton(\\n onPressed: () {\\n // Using \\"ref.read\\" combined with \\"myProvider.notifier\\", we can\\n // obtain the class instance of our notifier. This enables us\\n // to call the \\"addTodo\\" method.\\n ref\\n .read(todoListProvider.notifier)\\n .addTodo(Todo(description: \'This is a new todo\'));\\n },\\n child: const Text(\'Add todo\'),\\n );\\n }\\n}\\n
\\n1、通过ref.read可以在不成为关注者的情况下获取到对应的Provider。注意入参是todoListProvider.notifier。
\\n2、可能有人会疑问为啥不拿着todoListProvider直接使用?这个我们后面讲到内存原理的时候会提到,其实真正工作的不是这个Provider,而是底层的ProviderElement。这也是通过Provider参数实现多个同类型页面需要分别管理自己的独有状态的底层逻辑。
到这里为止我们已经实现了调用Provider提供的api去添加事项。
\\n但是当前的代码添加完成后UI并不会显示新增的事项,那如何在添加todo后更新状态让UI刷新呢?
\\n这里要区分两种情况:
\\n如果添加todo的接口会返回最新的事件列表,需要手工更新本地缓存:
Future<void> addTodo(Todo todo) async {\\n // The POST request will return a List<Todo> matching the new application state\\n final response = await http.post(\\n Uri.https(\'your_api.com\', \'/todos\'),\\n headers: {\'Content-Type\': \'application/json\'},\\n body: jsonEncode(todo.toJson()),\\n );\\n\\n // We decode the API response and convert it to a List<Todo>\\n List<Todo> newTodos = (jsonDecode(response.body) as List)\\n .cast<Map<String, Object?>>()\\n .map(Todo.fromJson)\\n .toList();\\n\\n // We update the local cache to match the new state.\\n // This will notify all listeners.\\n state = AsyncData(newTodos);\\n}\\n
\\n将接口返回的事件列表转换为List,然后直接给state赋值。
\\n这个赋值动作会触发所有关注者更新。
如果添加todo的接口没有返回新的任务列表,则需要客户端自己做一次数据的全量更新:
\\nFuture<void> addTodo(Todo todo) async {\\n // We don\'t care about the API response\\n await http.post(\\n Uri.https(\'your_api.com\', \'/todos\'),\\n headers: {\'Content-Type\': \'application/json\'},\\n body: jsonEncode(todo.toJson()),\\n );\\n\\n // Once the post request is done, we can mark the local cache as dirty.\\n // This will cause \\"build\\" on our notifier to asynchronously be called again,\\n // and will notify listeners when doing so.\\n ref.invalidateSelf();\\n\\n // (Optional) We can then wait for the new state to be computed.\\n // This ensures \\"addTodo\\" does not complete until the new state is available.\\n await future;\\n}\\n
\\n1、invalidateSelf会触发build方法被重新调用,调用完成后会自动更新所有的本地缓存数据并通知UI更新状态。
\\n这样对开发者来说最省事,不需要手动更新本地的state。
\\n2、await future的作用:如果UI层根据addTodo方法的异步返回决定展示和关闭一个loading组件,那么这句话可以保证关闭loading后新的事件列表已经就绪了。await future会等待loading状态的结束并返回新的数据。
好了,在一个小型项目中用到场景就这三种。其它的例如传入参数、多页面共存等情况都属于这三种场景的具体应用技巧。可以参考官方文档的说明。
\\n这里主要还是帮助大家梳理一下不同页面类型下使用Riverpod的方式。
接下来我们来解释一下Provider作为全局变量会不会有内存泄露的问题。
\\n如果大家没有使用代码生成的方式,那应该会看到官方推荐的方法是将所有的Provider定义为全局变量。
\\n当我的页面关闭了,Provider没有关注者了,但是全局变量本身并没有清除掉。那这个全局变量为啥不会造成内存泄露呢?
\\n我们先来看一下Riverpod的内存分布:
\\n
\\n其实本质上不管是通过代码生成还是手工定义的那个Provider,都只是一个配置模板。
\\n全局ProviderScope管理的并不是Provider本身。ProviderScope中包含一个名为ProviderContainer的容器,ProviderContainer负责管理所有的Provider。
\\n当我们的UI第一次调用watch方法时,比如第一次调用ref.watch(counterProvider)。
\\n其实是做了下面几件事情:
\\n1、ProviderContainer 调用 Provider 的 createElement()
方法创建 ProviderElement
\\n2、调用 ProviderElement 中创建 state 的具体方法,也就是那个build方法,或者只有一个函数的情况下的那个函数。
\\n3、在 ProviderElement 上添加监听变化的回调。
\\n4、将 ProviderElement 添加到 _providers 这个map中。
\\n\\nmap类型(<ProviderBase, ProviderElement>{})
\\n
\\nProviderBase 是 Provider 的基类
所以,真正替我们管理状态的是ProviderElement,而非Provider本身。
\\n接下来我们看看Provider的销毁流程。
\\nRiverpod中Provider的autoDispose参数默认为true。
\\n当这个Provider没有关注者的时候,比如所有使用这个Provider的页面都关闭了。
\\n这时就会启动autoDispose流程。
1、调用 ProviderElement 的 _dispose() 方法,销毁其持有的状态数据(如释放网络连接、关闭文件句柄等)
\\n2、ProviderElement 会被标记为无效并从 ProviderContainer 的缓存列表中移除
\\n3、ProviderElement本身被 GC 回收
所以当一个Provider的使命结束,从他持有的状态,打开的各种资源到ProviderElement本身都会逐步被清理。也就不用担心内存泄露的问题。
\\n总结一下,对于调用接口然后展示数据这种最简单最常见的场景,可以直接使用场景一中定义一个方法的方案。
\\n对于页面包含多个独立状态的场景,可以将多个状态拆分到不同的Provider中。
\\n对于需要Provider给关注者提供额外的api的场景,可以使用NotifierProvider或者AsyncNotifierProvider。
\\n组合使用这三种方案能够覆盖大部分app中90%以上的场景。
\\n对于大多数中小型APP来说是足够用的。
最后老刘还是要提醒一下,对于状态管理或者说架构设计来说,清晰的思路远比选择哪个库更重要。
\\n心中有剑,落叶飞花皆是兵器 。
如果看到这里的同学对客户端开发或者Flutter开发感兴趣,欢迎联系老刘,我们互相学习。
\\n点击免费领老刘整理的《Flutter开发手册》,覆盖90%应用开发场景。
\\n可以作为Flutter学习的知识地图。
\\n覆盖90%开发场景的《Flutter开发手册》
$ flutter pub add sqlite\\n$ flutter pub get\\n
\\n$ flutter run\\n
\\n运行失败,看是编译报错,打开Xcode工程 ⌘ + B 编译
\\n对比 GSYGithubAppFlutter 的Xcode工程Build Phases > [CP] Embed Pods Frameworks 有sqfite.framework。本地默认的Flutter工程默认未生成Podfile
\\n然后查看 GSYGithubAppFlutter
\\n...\\nrequire File.expand_path(File.join(\'packages\', \'flutter_tools\', \'bin\', \'podhelper\'), flutter_root)\\n\\nflutter_ios_podfile_setup\\n\\ntarget \'Runner\' do\\n use_frameworks!\\n use_modular_headers!\\n\\n flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))\\nend\\n...\\n
\\n看代码是引入了Flutter提供的工具的,从flutter的安装目录下找到podhelper.rb这个文件
\\n# 方法: flutter_install_all_ios_pods\\n# 安装Flutter在iOS平台上的引擎和插件\\ndef flutter_install_all_ios_pods(ios_application_path = nil)\\n # 创建Flutter引擎的.podspec文件\\n flutter_install_ios_engine_pod(ios_application_path)\\n flutter_install_plugin_pods(ios_application_path, \'.symlinks\', \'ios\')\\nend\\n
\\n# 方法: flutter_install_plugin_pods\\ndef flutter_install_plugin_pods(application_path = nil, relative_symlink_dir, platform)\\n # CocoaPods定义了 defined_in_file,获取应用路径,未获取到就中断\\n application_path ||= File.dirname(defined_in_file.realpath) if respond_to?(:defined_in_file)\\n raise \'Could not find application path\' unless application_path\\n\\n # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock\\n # referring to absolute paths on developers\' machines.\\n # 使用符号链接,避免使用Podfile.lock这个文件\\n # Flutter是在ios目录下创建.symlinks目录,里面有软链接指向Flutter下载包的位置,这样只需要一份即可。\\n # 先删除,再创建对应的目录\\n symlink_dir = File.expand_path(relative_symlink_dir, application_path)\\n system(\'rm\', \'-rf\', symlink_dir) \\n\\n symlink_plugins_dir = File.expand_path(\'plugins\', symlink_dir)\\n system(\'mkdir\', \'-p\', symlink_plugins_dir)\\n\\n plugins_file = File.join(application_path, \'..\', \'.flutter-plugins-dependencies\')\\n dependencies_hash = flutter_parse_plugins_file(plugins_file)\\n plugin_pods = flutter_get_plugins_list(dependencies_hash, platform)\\n swift_package_manager_enabled = flutter_get_swift_package_manager_enabled(dependencies_hash, platform)\\n\\n plugin_pods.each do |plugin_hash|\\n plugin_name = plugin_hash[\'name\']\\n plugin_path = plugin_hash[\'path\']\\n ...\\n # 使用path: 的方式本地依赖需要的三方库\\n # 手动添加打印确认下\\n # print \\"plugin_name:#{plugin_name}\\\\n\\"\\n pod plugin_name, path: File.join(relative, platform_directory)\\n end\\nend\\n
\\n$ pod update --verbose\\n
\\n因此Podfile
里的target部分就依赖了sqflite_darwin
target \'Runner\' do\\n use_frameworks!\\n use_modular_headers!\\n ...\\n pod \'sqflite_darwin\', path:.symlinks/plugins/sqflite_darwin/darwin\\nend\\n
\\nimport \'package:sqflite/sqflite.dart\';\\nimport \'package:path/path.dart\';\\n\\nvar databasesPath = await getDatabasesPath();\\nString path = join(databasesPath, \'finger.db\');\\n\\n/// 打开数据库\\nDatabase database = await openDatabase(path, version: 1,\\n onCreate: (Database db, int version) async {\\n /// 当创建数据库时创建table\\n await db.execute(\\n \'CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT, value INTEGER, num REAL)\');\\n});\\n
\\n/// 关闭数据库\\nawait db.close();\\n
\\n/// 删除数据库\\nawait deleteDatabase(path);\\n
\\n/// 添加表\\nawait database.execute(\\n \\"CREATE TABLE Test2(id INTEGER PRIMARY KEY, name TEXT, value INTEGER, num REAL)\\",\\n );\\n \\n/// 删除表\\nawait database.execute(\'DROP TABLE Test2\');\\n
\\n/// 添加数据\\nawait database.transaction((txn) async {\\n int id1 = await txn.rawInsert(\\n \'INSERT INTO Test(name, value, num) VALUES(\\"some name\\", 1234, 456.789)\');\\n int id2 = await txn.rawInsert(\\n \'INSERT INTO Test(name, value, num) VALUES(?, ?, ?)\',\\n [\'another name\', 12345678, 3.1416]);\\n});\\n
\\n/// 删除数据\\ncount = await database\\n .rawDelete(\'DELETE FROM Test WHERE name = ?\', [\'another name\']);\\n
\\n/// 更新数据\\nint count = await database.rawUpdate(\\n \'UPDATE Test SET name = ?, value = ? WHERE name = ?\',\\n [\'updated name\', \'9876\', \'some name\']);\\n
\\n/// 查询数据\\nList<Map> list = await database.rawQuery(\'SELECT * FROM Test\');\\nprint(list)\\n
\\n使用Sqflite提供的工具方法来执行数据库操作,而不是直接使用SQL语句
\\nimport \'package:sqflite/sqflite.dart\';\\nimport \'package:path/path.dart\';\\n\\nfinal String tName = \'company\';\\nfinal String columnId = \\"_id\\";\\nfinal String columnName = \\"name\\";\\n\\nclass Company {\\n int? id;\\n String? name;\\n Company();\\n\\n Map<String, Object?> toMap() {\\n var map = <String, Object?>{columnName: name};\\n if (id != null) {\\n map[columnId] = id;\\n }\\n return map;\\n }\\n\\n Company.fromMap(Map map) {\\n id = map[columnId];\\n name = map[columnName];\\n }\\n}\\n\\nclass CompanyProvider {\\n Database? db;\\n\\n Future<Database?> open() async {\\n if (db == null) {\\n var databasesPath = await getDatabasesPath();\\n String path = join(databasesPath, \'demo.db\');\\n db = await openDatabase(\\n path,\\n version: 1,\\n onCreate: (Database db, int version) async {\\n await db.execute(\'\'\'\\n create table $tName (\\n $columnId integer primary key autoincrement,\\n $columnName text not null)\\n \'\'\');\\n },\\n );\\n }\\n return db;\\n }\\n\\n /// 注册企业\\n Future insert(Company company) async {\\n /// 工具方法: 传表名 + 列信息添加数据到数据库\\n company.id = await db?.insert(tName, company.toMap());\\n return company;\\n }\\n\\n /// 查找企业\\n Future findById(int id) async {\\n List<Map> maps = await db!.query(\\n tName, /// 表名\\n columns: [columnId, columnName], /// 查找的列\\n where: \'$columnId = ?\', /// 查找条件\\n whereArgs: [id], /// 每个问号填充的值\\n );\\n if (maps.isNotEmpty) {\\n return Company.fromMap(maps.first);\\n }\\n return null;\\n }\\n \\n /// 查找所有的企业\\n Future<List<Company>> find() async {\\n List<Company> companys = [];\\n List<Map> maps = await db!.query(tName, columns: [columnId, columnName]);\\n for (var map in maps) {\\n Company c = Company.fromMap(map);\\n companys.add(c);\\n }\\n return companys;\\n }\\n\\n /// 删除企业\\n Future delete(int id) async {\\n /// 根据id列删除企业\\n return await db?.delete(tName, where: \'$columnId = ?\', whereArgs: [id]);\\n }\\n\\n /// 更新企业信息\\n Future update(Company company) async {\\n return await db?.update(\\n tName,\\n company.toMap(),\\n where: \'$columnId = ?\',\\n whereArgs: [company.id],\\n );\\n }\\n}\\n
\\nvoid test() async {\\n /// 添加2条测试数据\\n CompanyProvider cp = CompanyProvider();\\n await cp.open();\\n List<Map> maps = [\\n {\\"name\\": \\"Google\\"},\\n {\\"name\\": \\"Apple\\"},\\n ];\\n\\n /// 新增数据\\n int firstId = 0;\\n for (int i = 0; i < maps.length; ++i) {\\n Company c = Company.fromMap(maps[i]);\\n cp.insert(c);\\n }\\n\\n /// 查找数据\\n List<Company> companys = await cp.find();\\n if (companys.isNotEmpty) {\\n firstId = companys.first.id!;\\n }\\n\\n if (firstId > 0) {\\n Company firstCompany = await cp.findById(firstId);\\n print(firstCompany.toMap());\\n\\n /// 更新数据\\n Company chgCompany = Company();\\n chgCompany.id = firstId;\\n chgCompany.name = DateTime.now().microsecondsSinceEpoch.toString();\\n cp.update(chgCompany);\\n\\n firstCompany = await cp.findById(firstId);\\n print(firstCompany.toMap());\\n\\n /// 删除数据\\n cp.delete(firstId);\\n }\\n }\\n
\\n随着功能迭代,需要对数据库的表结构进行修改时,比如增加新字段时,需要对表的结构进行更新。
\\nFuture<Database?> open() async {\\n if (db == null) {\\n var databasesPath = await getDatabasesPath();\\n String path = join(databasesPath, \'demo.db\');\\n db = await openDatabase(\\n path,\\n version: 2,\\n\\n /// 1.新版本发布时改成2\\n onCreate: (db, version) async {\\n /// 2.新安装设备触发onCreate,所以这里添加新的字段\\n await db.execute(\'\'\'\\n create table $tName (\\n $columnId integer primary key autoincrement,\\n $columnName text not null,\\n $columnDesc text)\\n \'\'\');\\n },\\n onUpgrade: (db, oldVersion, newVersion) async {\\n var batch = db.batch();\\n /// [onUpgrade] is called if either of \\n /// the following conditions are met:\\n\\n /// 1. [onCreate] is not specified\\n /// 2. The database already exists and [version] is higher than the last database version\\n /// onUpgrade回调在未指定onCreate回调或者数据库已经存在同时version字段高于已安装的版本,执行完onUpgrade回调后应该会更新关联的版本,设置断点让onUpgrade执行中断,下次还会会执行这个方法\\n \\n /// 3.对旧版本的设备:判断安装设备已创建的数据库版本\\n if (oldVersion == 1) {\\n _updateTableCompanyV1toV2(batch);\\n }\\n await batch.commit();\\n },\\n );\\n }\\n return db;\\n }\\n
\\n/// 4.添加description字段\\nvoid _updateTableCompanyV1toV2(Batch batch) {\\n batch.execute(\'ALTER TABLE Company ADD description TEXT\');\\n}\\n\\n/// 其它的一些处理\\nfinal String columnDesc = \\"description\\";\\n...\\n\\nclass Company {\\n int? id;\\n String? name;\\n\\n /// 5.模型增加对应字段 + 列\\n String? description;\\n ...\\n \\n /// 6. 更新map和对象的转换方法\\n Map<String, Object?> toMap() {\\n var map = <String, Object?>{columnName: name, columnDesc: description};\\n if (id != null) {\\n ...\\n
\\n/// 调用\\n...\\nfirstCompany.description = \\"版本2新增的字段\\";\\nprint(firstCompany.toMap());\\n
\\n数据库的增删改查可能会失败,导致数据与预期的不一致,为了保证在执行前后的数据一致性,引入了事务。事务具有ACID这4个特性:原子性、一致性、隔离性和持久性。
\\n在事务中不要使用数据库,而只需要使用事务对象访问数据库。
\\nawait database.transaction((txn) async {\\n // 正确\\n await txn.execute(\'CREATE TABLE Test1 (id INTEGER PRIMARY KEY)\');\\n \\n // 不要在事务中使用数据库\\n // 下面会导致死锁\\n await database.execute(\'CREATE TABLE Test2 (id INTEGER PRIMARY KEY)\');\\n});\\n
\\ntry {\\n await database.transaction((txn) async {\\n await txn.update(\'TABLE\', {\'foo\': \'bar\'});\\n });\\n \\n // No error, the transaction is committed\\n // 1. 未报错,则事务被提交\\n \\n // cancel the transaction (any error will do)\\n // 2. 取消或执行时报错,则抛出异常在,catch中被捕获\\n // throw StateError(\'cancel transaction\');\\n} catch (e, st) {\\n // this reliably catch if there is a key conflict\\n // We know that the transaction is rolled back.\\n // 3. 事务被回滚,执行业务相关的操作,比如提示报错\\n}\\n
\\n使用 Batch,即批处理,来避免在 Dart 和原生代码之间的反复切换。
\\nbatch = db.batch();\\nbatch.insert(\'Test\', {\'name\': \'item\'});\\nbatch.update(\'Test\', {\'name\': \'new_item\'}, where: \'name = ?\', whereArgs: [\'item\']);\\nbatch.delete(\'Test\', where: \'name = ?\', whereArgs: [\'item\']);\\n/// 批处理统一提交\\nresults = await batch.commit();\\n
\\n在事务中,批处理的commit会等到事务提交后
\\nawait database.transaction((txn) async {\\n var batch = txn.batch();\\n \\n // ...\\n \\n // commit but the actual commit will happen when the transaction is committed\\n // however the data is available in this transaction\\n /// 当事务被提交时才会真正的提交\\n await batch.commit();\\n \\n // ...\\n});\\n
\\n/// 设置批处理出现错误依然提交\\nawait batch.commit(continueOnError: true);\\n
\\nSQLite的关键词,要避免使用作为实体(Entity)名。
\\n\\"add\\",\\"all\\",\\"alter\\",\\"and\\",\\"as\\",\\"autoincrement\\",\\"between\\",\\"case\\",\\"check\\",\\"collate\\",\\"commit\\",\\"constraint\\",\\"create\\",\\"default\\",\\"deferrable\\",\\"delete\\",\\"distinct\\",\\"drop\\",\\"else\\",\\"escape\\",\\"except\\",\\"exists\\",\\"foreign\\",\\"from\\",\\"group\\",\\"having\\",\\"if\\",\\"in\\",\\"index\\",\\"insert\\",\\"intersect\\",\\"into\\",\\"is\\",\\"isnull\\",\\"join\\",\\"limit\\",\\"not\\",\\"notnull\\",\\"null\\",\\"on\\",\\"or\\",\\"order\\",\\"primary\\",\\"references\\",\\"select\\",\\"set\\",\\"table\\",\\"then\\",\\"to\\",\\"transaction\\",\\"union\\",\\"unique\\",\\"update\\",\\"using\\",\\"values\\",\\"when\\",\\"where\\"\\n
\\nsqflite的工具方法会进行处理,避免与关键字的冲突
\\ndb.query(\'table\')\\n/// 等价于\\ndb.rawQuery(\'SELECT * FROM \\"table\\"\');\\n
\\n\\n\\nError connecting to the service protocol: failed to connect to http://127.0.0.1:51020/Kra7fZnYjeI=/ Error: Failed to register service methods on attached VM Service: registerService: (-32000) Service connection disposed
\\n
原来有成功过,后面发现一直都会有问题,前段时间突然不行,在长时间运行后就会报这个错误,但是单独在VSCode外部用flutter run命令能正常运行。
\\n发现终端可以是把本地的端口转发的代理给去掉了。然后发现VSCode的代理有这样的说明,若未设置则会继承环境变量中的http_proxy
和https_proxy
,我把代理加到.zshrc
中,所以VSCode的默认会用代理,但是运行在真机上,手机没有代理,应该是这样影响了网络环境。
随着 Dart 3.8 的发布,开发者迎来了一个令人兴奋的新语法特性:Null-Aware Elements(空感知元素)。这一特性特别适用于 Flutter 开发,能够显著简化代码结构,提高可读性和维护性。
\\nNull-Aware Elements 允许在集合字面量中使用 ?
前缀来自动跳过 null
值的元素或键值对。这意味着在构建 List
、Set
或 Map
时,无需显式地检查元素是否为 null
,从而使代码更加简洁。
示例:
\\nfinal list = [\\n 1,\\n ?null, // 被自动跳过\\n ?getNullable(), // 如果返回 null,则跳过\\n 2,\\n];\\n
\\n在上述示例中,?null
和 ?getNullable()
如果结果为 null
,将不会被添加到 list
中。
在 Flutter 开发中,构建 Widget 树时经常需要根据条件添加子组件。传统方式通常需要使用 if
语句进行 null
检查:
final List<Widget> children = [];\\nif (optionalWidget != null) {\\n children.add(optionalWidget);\\n}\\n
\\n使用 Null-Aware Elements 后,可以直接在集合字面量中使用 ?
前缀,自动跳过 null
值:
final children = [\\n Text(\'Title\'),\\n ?optionalWidget, // 如果 optionalWidget 为 null,将被自动忽略\\n];\\n
\\n这种写法不仅减少了代码量,还提高了代码的可读性。
\\n传统方式:
\\nif (value != null) {\\n list.add(value);\\n}\\n
\\n使用 Null-Aware Elements:
\\nfinal list = [\\n ?value,\\n];\\n
\\n这种方式使得代码更加简洁,减少了冗余的 null
检查。
在构建复杂的 UI 时,嵌套的条件判断会使代码变得冗长且难以维护。Null-Aware Elements 通过简洁的语法,减少了冗余的 null
检查,使代码更加清晰。
final widgets = [\\n Text(\'Header\'),\\n if (showDetails) ...[\\n Text(\'Details:\'),\\n ?detailsWidget,\\n ],\\n ?footerWidget,\\n];\\n
\\n在上述代码中,detailsWidget
和 footerWidget
如果为 null
,将被自动忽略,无需额外的 null
检查。
传统的 null
检查容易因疏忽而导致运行时错误,如使用 !
操作符时未正确判断 null
值。Null-Aware Elements 通过在集合构造时自动跳过 null
值,降低了此类错误的发生概率。
final items = [\\n Text(\'Item 1\'),\\n ?maybeNullWidget,\\n Text(\'Item 2\'),\\n];\\n
\\n如果 maybeNullWidget
为 null
,它将被自动忽略,避免了因未处理 null
而导致的异常。
Null-Aware Elements 可以与 Dart 的其他集合操作符(如 if
、for
、...
)结合使用,构建更复杂的集合结构。例如:
final menuItems = [\\n Text(\'Menu\'),\\n if (showSettings) ?settingsWidget,\\n ...?additionalWidgets,\\n];\\n
\\n这种组合使用使得集合构造更加灵活和强大。
\\nDart 的 linter 提供了 use_null_aware_elements
规则,鼓励开发者在适当的情况下使用 Null-Aware Elements。启用该规则后,分析器会提示可以优化的代码片段,从而提升代码质量。
要启用此规则,可在 analysis_options.yaml
文件中添加:
linter:\\n rules:\\n - use_null_aware_elements\\n
\\nList
、Set
、Map
的字面量中有效。Null-Aware Elements 是 Dart 3.8 引入的一个实用语法特性,特别适用于 Flutter 中构建 Widget 树的场景。它使得代码更加简洁,减少了显式的 null
检查,提升了代码的可读性和维护性。如果你已经升级到 Dart 3.8,不妨尝试在项目中使用 Null-Aware Elements,让你的 Flutter 代码更加优雅!
任何开发语言都离不开网络请求,Flutter
亦是如此。http库是Flutter官方的一个网络请求库,功能比较基础,在日常开发中直接使用的也相对比较少,毕竟有一个功能强大的Dio
库。但作为一个初学者,其实有必要了解一下http库大概是一个什么样子的。
http
包在 pubspec.yaml
中添加依赖:
dependencies:\\n http: ^1.1.0 # 检查最新版本\\n
\\n运行 flutter pub get
。
了解网络请求的同学可能知道,基本的网络请求无非就是了解一下这几种基本的用法:
\\nFuture<void> fetchData() async {\\n final url = Uri.parse(\'https://jsonplaceholder.typicode.com/posts/1\');\\n try {\\n final response = await http.get(url);\\n if (response.statusCode == 200) {\\n print(\'响应数据: ${response.body}\');\\n } else {\\n print(\'请求失败: ${response.statusCode}\');\\n }\\n } catch (e) {\\n print(\'网络错误: $e\');\\n }\\n}\\n
\\nFuture<void> postData() async {\\n final url = Uri.parse(\'https://jsonplaceholder.typicode.com/posts\');\\n final headers = {\'Content-Type\': \'application/json\'};\\n final body = jsonEncode({\'title\': \'foo\', \'body\': \'bar\', \'userId\': 1});\\n\\n try {\\n final response = await http.post(url, headers: headers, body: body);\\n if (response.statusCode == 201) {\\n print(\'创建成功: ${response.body}\');\\n }\\n } catch (e) {\\n print(\'请求失败: $e\');\\n }\\n}\\n
\\nFuture<void> updateData() async {\\n final url = Uri.parse(\'https://jsonplaceholder.typicode.com/posts/1\');\\n final headers = {\'Content-Type\': \'application/json\'};\\n final body = jsonEncode({\'title\': \'updated title\', \'body\': \'updated body\'});\\n\\n try {\\n final response = await http.put(url, headers: headers, body: body);\\n if (response.statusCode == 200) {\\n print(\'PUT 响应数据: ${response.body}\');\\n }\\n } catch (e) {\\n print(\'PUT 请求失败: $e\');\\n }\\n}\\n
\\nFuture<void> deleteData() async {\\n final url = Uri.parse(\'https://jsonplaceholder.typicode.com/posts/1\');\\n try {\\n final response = await http.delete(url);\\n if (response.statusCode == 200) {\\n print(\'DELETE 成功\');\\n }\\n } catch (e) {\\n print(\'DELETE 请求失败: $e\');\\n }\\n}\\n
\\n上传文件也是一个Post
请求,只是传输的数据类型和普通的post不太一样, 需手动处理 multipart/form-data
:
Future<void> uploadFile() async {\\n final url = Uri.parse(\'https://api.example.com/upload\');\\n //创建一个request\\n final request = http.MultipartRequest(\'POST\', url);\\n // 添加文件\\n request.files.add(\\n await http.MultipartFile.fromPath(\'file\', \'/path/to/file.jpg\'),\\n );\\n // 添加字段\\n request.fields[\'key\'] = \'value\';\\n try {\\n // 发送一个封装好的request\\n final response = await request.send();\\n if (response.statusCode == 200) {\\n print(\'上传成功\');\\n }\\n } catch (e) {\\n print(\'上传失败: $e\');\\n }\\n}\\n
\\n其实就是一个get的网络请求,加上文件保存的过程。\\n通过流式处理保存文件:
\\nFuture<void> downloadFile() async {\\n final url = Uri.parse(\'https://example.com/file.zip\');\\n final response = await http.get(url);\\n\\n if (response.statusCode == 200) {\\n final file = File(\'/local/path/file.zip\');\\n await file.writeAsBytes(response.bodyBytes);\\n print(\'文件已保存\');\\n }\\n}\\n
\\n使用 dart:convert
库解析和编码 JSON:
import \'dart:convert\';\\n//解析数据,需要先转化为json类型\\nvoid parseJson(String responseBody) {\\n final jsonData = jsonDecode(responseBody);\\n print(\'标题: ${jsonData[\'title\']}\');\\n}\\n
\\nclass Post {\\n final int userId;\\n final int id;\\n final String title;\\n final String body;\\n\\n Post({required this.userId, required this.id, required this.title, required this.body});\\n\\n factory Post.fromJson(Map<String, dynamic> json) {\\n return Post(\\n userId: json[\'userId\'],\\n id: json[\'id\'],\\n title: json[\'title\'],\\n body: json[\'body\'],\\n );\\n }\\n}\\n\\n// 使用示例\\nfinal post = Post.fromJson(jsonDecode(response.body));\\n
\\nfinal headers = {\\n \'Authorization\': \'Bearer your_token\',\\n \'Content-Type\': \'application/json\',\\n};\\n\\nfinal response = await http.get(url, headers: headers);\\n
\\n使用 Uri
构建带查询参数的 URL:
final url = Uri.parse(\'https://api.example.com/data\').replace(\\n queryParameters: {\'page\': \'1\', \'limit\': \'10\'},\\n);\\n
\\n通过 Future.timeout
实现:
try {\\n final response = await http.get(url).timeout(Duration(seconds: 5));\\n} on TimeoutException {\\n print(\'请求超时\');\\n}\\n
\\n捕获常见错误类型:
\\ntry {\\n final response = await http.get(url);\\n} on http.ClientException catch (e) {\\n print(\'客户端错误: $e\'); // 如网络不可用\\n} on SocketException catch (e) {\\n print(\'网络连接失败: $e\');\\n} catch (e) {\\n print(\'未知错误: $e\');\\n}\\n
\\ndependencies:\\n dio: ^5.0.0 # 检查最新版本\\n
\\nimport \'package:dio/dio.dart\';\\n\\nFuture<void> fetchData() async {\\n final dio = Dio();\\n try {\\n final response = await dio.get(\'https://jsonplaceholder.typicode.com/posts/1\');\\n print(\'GET 响应数据: ${response.data}\');\\n } on DioException catch (e) {\\n print(\'GET 请求失败: ${e.message}\');\\n }\\n}\\n
\\nFuture<void> postData() async {\\n final dio = Dio();\\n final data = {\'title\': \'foo\', \'body\': \'bar\', \'userId\': 1};\\n try {\\n final response = await dio.post(\\n \'https://jsonplaceholder.typicode.com/posts\',\\n data: data,\\n );\\n print(\'POST 响应数据: ${response.data}\');\\n } on DioException catch (e) {\\n print(\'POST 请求失败: ${e.message}\');\\n }\\n}\\n
\\nFuture<void> updateData() async {\\n final dio = Dio();\\n final data = {\'title\': \'updated title\', \'body\': \'updated body\'};\\n try {\\n final response = await dio.put(\\n \'https://jsonplaceholder.typicode.com/posts/1\',\\n data: data,\\n );\\n print(\'PUT 响应数据: ${response.data}\');\\n } on DioException catch (e) {\\n print(\'PUT 请求失败: ${e.message}\');\\n }\\n}\\n
\\nFuture<void> deleteData() async {\\n final dio = Dio();\\n try {\\n final response = await dio.delete(\'https://jsonplaceholder.typicode.com/posts/1\');\\n print(\'DELETE 成功: ${response.statusCode}\');\\n } on DioException catch (e) {\\n print(\'DELETE 请求失败: ${e.message}\');\\n }\\n}\\n
\\nFuture<void> uploadFile() async {\\n final dio = Dio();\\n final formData = FormData.fromMap({\\n \'file\': await MultipartFile.fromFile(\'/path/to/file.jpg\', filename: \'upload.jpg\'),\\n \'key\': \'value\',\\n });\\n\\n try {\\n final response = await dio.post(\\n \'https://api.example.com/upload\',\\n data: formData,\\n onSendProgress: (sent, total) {\\n print(\'上传进度: ${(sent / total * 100).toStringAsFixed(0)}%\');\\n },\\n );\\n print(\'文件上传成功: ${response.data}\');\\n } on DioException catch (e) {\\n print(\'文件上传失败: ${e.message}\');\\n }\\n}\\n
\\n自定义数据类型解析
\\nResponse response = await dio.get(\\n \'/path\',\\n options: Options(responseType: ResponseType.plain),\\n);\\n
\\ndio.interceptors.add(\\n InterceptorsWrapper(\\n onRequest: (RequestOptions options, RequestInterceptorHandler handler) {\\n // 添加统一请求头(如 Token)\\n options.headers[\'Authorization\'] = \'Bearer token\';\\n return handler.next(options);\\n },\\n onResponse: (Response response, ResponseInterceptorHandler handler) {\\n // 预处理响应数据\\n return handler.next(response);\\n },\\n onError: (DioException error, ErrorInterceptorHandler handler) {\\n // 统一错误处理(如 Token 过期跳转登录)\\n if (error.response?.statusCode == 401) {\\n // 处理逻辑\\n }\\n return handler.next(error);\\n },\\n ),\\n);\\n
\\n日志拦截器
\\ndio.interceptors.add(LogInterceptor(\\n request: true,\\n responseBody: true,\\n));\\n
\\nfinal dio = Dio(\\n BaseOptions(\\n baseUrl: \'https://api.example.com\',\\n connectTimeout: Duration(seconds: 5),\\n receiveTimeout: Duration(seconds: 3),\\n headers: {\'Content-Type\': \'application/json\'},\\n ),\\n);\\n
\\nfinal cancelToken = CancelToken();\\n\\n// 发送请求时传递 cancelToken\\ndio.get(\'/path\', cancelToken: cancelToken);\\n\\n// 取消请求\\ncancelToken.cancel(\'用户手动取消\');\\n
\\n特性 | http 包 | Dio |
---|---|---|
依赖体积 | 轻量(无额外依赖) | 较大(支持拦截器等高级功能) |
JSON 自动解析 | 需手动使用 jsonDecode | 支持自动解析 |
拦截器 | 不支持 | 支持请求/响应拦截 |
文件上传/下载 | 需手动处理 MultipartRequest | 封装了 FormData 和 download 方法 |
超时配置 | 需通过 Future.timeout 实现 | 内置全局超时配置 |
取消请求 | 不支持 | 支持 CancelToken |
适用场景:
\\nhttp
包:适合简单请求、小型项目,或希望减少第三方依赖。核心优势:
\\nhttp
包:官方维护,代码简洁,学习成本低。官方文档:
\\n","description":"任何开发语言都离不开网络请求,Flutter亦是如此。http库是Flutter官方的一个网络请求库,功能比较基础,在日常开发中直接使用的也相对比较少,毕竟有一个功能强大的Dio库。但作为一个初学者,其实有必要了解一下http库大概是一个什么样子的。 一、安装 http 包\\n\\n在 pubspec.yaml 中添加依赖:\\n\\ndependencies:\\n http: ^1.1.0 # 检查最新版本\\n\\n\\n运行 flutter pub get。\\n\\n二、基本用法\\n\\n了解网络请求的同学可能知道,基本的网络请求无非就是了解一下这几种基本的用法:\\n\\nget请求\\npost请求…","guid":"https://juejin.cn/post/7505963040309100595","author":"搬砖的理查德","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-20T01:08:16.174Z","media":null,"categories":["Android","前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 中的 SOLID 原则实用指南:D — 依赖倒置原则(DIP)","url":"https://juejin.cn/post/7506038750397464628","content":"在 SOLID 原则中,“D” 指的是 依赖倒置原则(Dependency Inversion Principle,DIP),它倡导:
\\n\\n\\n高层模块不应依赖低层模块,二者都应该依赖抽象。抽象不应依赖细节,细节应依赖抽象。
\\n
换句话说,我们不应该在高层逻辑中直接依赖具体实现,而应依赖接口或抽象类。这可以大幅提升代码的可扩展性、可测试性和模块解耦能力。
\\n我们以一个“文章服务”为例,展示 Flutter 中常见的错误做法:
\\nclass ArticleService {\\n Future<List<String>> fetchArticles() async {\\n // 模拟从网络获取文章\\n await Future.delayed(Duration(seconds: 1));\\n return [\'文章 A\', \'文章 B\'];\\n }\\n}\\n\\nclass HomePage extends StatelessWidget {\\n final service = ArticleService(); // 👈 错误:UI 层直接依赖具体类\\n\\n @override\\n Widget build(BuildContext context) {\\n return FutureBuilder(\\n future: service.fetchArticles(),\\n builder: (context, snapshot) {\\n if (snapshot.connectionState == ConnectionState.waiting) {\\n return CircularProgressIndicator();\\n }\\n return ListView(\\n children: snapshot.data!.map((e) => Text(e)).toList(),\\n );\\n },\\n );\\n }\\n}\\n
\\nArticleService
,导致 耦合严重,难以测试或替换。abstract class ArticleRepository {\\n Future<List<String>> fetchArticles();\\n}\\n
\\nclass RemoteArticleRepository implements ArticleRepository {\\n @override\\n Future<List<String>> fetchArticles() async {\\n await Future.delayed(Duration(seconds: 1));\\n return [\'文章 A\', \'文章 B\'];\\n }\\n}\\n
\\nclass HomeController {\\n final ArticleRepository repository;\\n\\n HomeController(this.repository);\\n\\n Future<List<String>> getArticles() => repository.fetchArticles();\\n}\\n
\\n我们通过 Riverpod 注入 抽象接口绑定的实现类,完成解耦与依赖管理:
\\nfinal articleRepositoryProvider = Provider<ArticleRepository>((ref) {\\n return RemoteArticleRepository(); // 可轻松替换为 Mock 或其他实现\\n});\\n\\nfinal homeControllerProvider = Provider<HomeController>((ref) {\\n final repository = ref.read(articleRepositoryProvider);\\n return HomeController(repository);\\n});\\n
\\nclass HomePage extends ConsumerWidget {\\n @override\\n Widget build(BuildContext context, WidgetRef ref) {\\n final controller = ref.read(homeControllerProvider);\\n\\n return FutureBuilder<List<String>>(\\n future: controller.getArticles(),\\n builder: (context, snapshot) {\\n if (!snapshot.hasData) return CircularProgressIndicator();\\n return ListView(\\n children: snapshot.data!.map((e) => Text(e)).toList(),\\n );\\n },\\n );\\n }\\n}\\n
\\nHomeController
。class FakeArticleRepository implements ArticleRepository {\\n @override\\n Future<List<String>> fetchArticles() async => [\'测试文章 A\', \'测试文章 B\'];\\n}\\n\\nvoid main() {\\n test(\'HomeController fetches articles\', () async {\\n final controller = HomeController(FakeArticleRepository());\\n final articles = await controller.getArticles();\\n expect(articles.length, 2);\\n });\\n}\\n
\\n依赖倒置原则的目标是降低模块之间的耦合,让高层逻辑更稳定、底层实现更灵活。在实际开发中,我们应牢记以下几点:
\\n场景 | DIP 应用方式 |
---|---|
需要替换网络服务实现 | 依赖抽象 ArticleRepository ,替换不动上层逻辑 |
测试时避免请求真实网络 | 使用 FakeRepository 注入控制结果 |
组件间解耦 | 通过 Provider 注入抽象而非直接创建类实例 |
多平台(Web / App)支持差异逻辑 | 抽象接口 + 不同平台实现类,运行时自由切换 |
\\n\\nDIP 不是“强行抽象”,而是“依赖更稳定的抽象,释放更灵活的实现”。
\\n
用抽象隔离变动,用 Provider 管理依赖。写出更优雅、测试友好、可扩展的 Flutter 代码,从依赖倒置原则开始。
\\n如果你觉得这篇文章对你有帮助,欢迎点个 “在看”,或转发给身边的 Flutter 同学,一起写出更优雅、可维护的代码!
","description":"在 SOLID 原则中,“D” 指的是 依赖倒置原则(Dependency Inversion Principle,DIP),它倡导: 高层模块不应依赖低层模块,二者都应该依赖抽象。抽象不应依赖细节,细节应依赖抽象。\\n\\n换句话说,我们不应该在高层逻辑中直接依赖具体实现,而应依赖接口或抽象类。这可以大幅提升代码的可扩展性、可测试性和模块解耦能力。\\n\\n🚫 常见反例:直接依赖具体实现\\n\\n我们以一个“文章服务”为例,展示 Flutter 中常见的错误做法:\\n\\nclass ArticleService {\\n Future恰好刚刚华为正式发布了了他的 PC 产品,而在之前发的 《鸿蒙 PC 发布之后,想在技术上聊聊它的未来可能》 文章后,在不同平台的评论区都有人提到了这张图,主要是说鸿蒙微内核不是 Linux ,也不是 Linux 套壳,但是能兼容提供 Linux 环境:
\\n去年华为也已经发布过微内核的论文 ,那就可以基于现有资料简单聊聊,为什么鸿蒙的微内核可以做到既不是 Linux 又能跑 Linux 。
\\n首先我们聊为什么不是 Linux,鸿蒙内核属于微内核,也就是核心内核的功能被精简到最基本的部分,例如多进程调度和通信,而将大多数系统服务移至用户态实现 :
\\n所以如下图,和 Linux 的宏内核(左)对比,微内核架构大部分实现其实在用户态:
\\n那一般微内核的实现自然就是安全度更高,从而提高了系统的安全性(通过组件隔离,减小内核的攻击面),当然也带来了 IPC 的频率增加引发的性能开销,所以在微内核架构里,高性能的 IPC 机制至关重要,因为用户态服务之间以及服务与内核之间的通信非常频繁。
\\n\\n\\n论文里提到过,手机(约每秒41,000次)和车载(约每秒7,000次)的 IPC 频率
\\n
不过鸿蒙表示其微内核的紧凑结构(比如 File System 和 Mem Mgr 合并)极大地提升了 IPC 性能 ,一些频繁访问的核心服务还是会保留在内核态,同时对应的一优化有:
\\n另外,鸿蒙的一个特色就是分布式,内核不仅支持本地IPC,还支持分布式 IPC / RPC ,从而实现了跨设备的远程 IPC 调用,并为分布式 Binder 场景调用提供核心基础。
\\n\\n\\n例如内核实现了一类名为 “archsyscall” 的系统调用,专用于支持微内核特性,包括 IPC 和 RPC ,而对于 RPC 机制中的核心内核资源(例如 ACTV 和 ACTVPOOL )的访问,则通过一种基于能力(capability-based)的机制进行控制 。
\\n
而鸿蒙对于内核分布式 IPC 原生的支持,也让分布式操作成为内核层面的「一等公民」,这也是 HarmonyOS 分布式生态系统的基石?
\\n还有还有一个差异就是,Linux 作为通用操作系统,它的 CFS 调度器力求公平,确保多任务环境下各进程都能获得合理的CPU时间,而鸿蒙的确定性时延引擎,则更侧重于用户交互的流畅性和关键任务的实时响应:
\\n\\n\\n侧重于通过实时分析和预测来进行更优的系统资源分配 ,主动优先处理关键任务以保持系统流畅性,任务会按优先级严格划分,与用户操作体验直接相关的关键进程和关联任务,会被在线标记并优先调度。
\\n
所以通过设计理念和机制对比,可以看出来鸿蒙微内核确实在技术实现上区别于 Linux :
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n特性 | 鸿蒙内核 | Linux 内核 |
---|---|---|
内核类型 | 微内核 (混合形态) | 宏内核 |
主要设计目标 | 安全性、模块化、分布式操作、实时性 | 通用性、高性能、开放性 |
核心架构 | 服务主要在用户态,部分核心管理器在内核态 | 所有核心服务均在内核态 |
IPC 机制 | 高性能本地及分布式 IPC / RPC | 多种 IPC 机制 (管道、套接字、System V IPC等) |
默认调度方法 | 确定性时延引擎 | 完全公平调度器 (CFS) + 其他 (FIFO, RR, Deadline) |
那为什么鸿蒙内核不是 linux ,但是却可以兼容 linux ,这就不得不提鸿蒙内核里的基础三大件:POSIX、ABI 和 HDF。
\\n我们简单先通过这个表格,可以看到三大件起到的主要作用:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n机制 | 关键词 | 主要功能 |
---|---|---|
POSIX 兼容性 | 内核抽象层 (KAL), musl libc | 提供标准的类 Unix 应用编程接口 |
Linux ABI 兼容性 | ABI 兼容垫片层 (Shim Layer), lsyscall (RPC分发) | 支持预编译 Linux 二进制程序的执行 |
硬件驱动框架 (HDF) | OSAL, PAL, khdf/linux , 用户态驱动容器 | 实现跨内核、跨平台的驱动开发与复用,支持 Linux 驱动 |
POSIX 标准本身就定义了一系列应用程序的编程接口,目的是增强不同操作系统间的源代码可移植性,而在鸿蒙内核里,主要是由内核抽象层 (KAL) 和 musl libc 来支撑。
\\n内核抽象层(KAL)是鸿蒙实现跨内核兼容性的关键组件,它的核心设计目标是屏蔽底层不同操作系统内核(在 OpenHarmony 可能是 LiteOS、Linux 内核,在鸿蒙NEXT是微内核),从而为上层软件提供一套统一的、稳定的内核能力接口 。
\\n\\n\\n这些接口覆盖了操作系统的核心功能,包括进程与线程管理、内存管理、文件系统操作、网络通信以及外设管理等。
\\n
而在微内核下,当上层应用或库发起一个 POSIX 调用时(例如read()
、write()
、fork()
),KAL 会负责将这个标准化的 POSIX 请求,转换为对鸿蒙微内核相应服务的请求或一系列操作。
另外还有 musl libc ,musl libc 是一个以轻量级、高性能的 C 标准库实现,鸿蒙对它进行了一定的优化和裁剪,移除了一些不适用于此类设备的接口 。
\\n所以在「用户态」应用通过调用 musl libc 提供的 POSIX 函数(如pthread_create()
创建一个线程,或open()
打开一个文件)时, musl libc 的这些函数实现在需要与内核交互时,内核抽象层就(KAL)介入,然后将这些请求,「翻译」成对鸿蒙微内核内部特定服务或原语的调用:
\\n\\n例如,一个文件打开操作,可能会被 KAL 转换为对微内核中文件系统服务的 IPC 请求,对于线程创建,KAL 可能会调用微内核的进程/线程管理服务来创建相应的执行实体。
\\n
因此,POSIX 兼容性并非由单一组件完成,而是由用户态的 musl libc(提供API)和内核态的 KAL(连接API与微内核服务)共同实现 。
\\nABI 一般指的是应用程序二进制接口,指的是预编译的二进制程序(如可执行文件、动态库)无需重新编译,就可以在「兼容」的不同系统上正确运行的能力。
\\n为了实现 Linux ABI 兼容性,鸿蒙设计了一个 ABI 兼容的垫片层(Shim Layer),这个垫片层主要扮演翻译官的角色,也就是前面 POSIX 聊到的 「翻译」。
\\n而这个针对 Linux ABI 的垫片层会被部署在内核空间(IC0),它的核心功能是将 Linux 应用发起的系统调用(syscalls),重定向到鸿蒙微内核的 IPC 机制,并作为全局状态信息的一个中央存储库,比如:
\\n\\n\\n当一个 Linux 二进制执行一个系统调用指令(例如通过
\\nint 0x80
或syscall
指令)时,这个垫片层会截获该调用,然后解析它的意图(即确定是哪个Linux系统调用以及相关参数),最后将其转换为对鸿蒙微内核相应服务的一个或多个 IPC 请求。
另外,这个垫片层不仅处理系统调用,还可能会处理 Linux ABI 相关的其他方面,如信号传递机制、进程内存布局、ELF 二进制文件的加载和解析方式等。
\\n\\n\\n比如 ABI 兼容垫片层同时支持 AOSP 和 OpenHarmony 的二进制兼容性,所以大概率垫片层可以根据目标ABI(Linux、AOSP)加载不同的兼容模块。
\\n
而在调用翻译上,鸿蒙内核通过一种被称为lsyscall
的机制来实现对 Linux 系统调用的支持 :
\\n\\n所以 lsyscall 会有许多不同的类型。
\\n
另外 ABI 兼容也是 iSulad 等容器能运行的关键,也是「卓易通」支持 apk 运行的基础,所以为什么「卓易通」的性能还过得去,主要也是提供的 android 的模拟器环境实现的还过得去。
\\n最后硬件驱动框架(HDF)是鸿蒙操作系统中负责管理硬件驱动、实现硬件抽象和解耦的关键子系统,主要用于简化驱动开发和移植的复杂度。
\\nHDF 架构强调平台解耦和内核解耦,也就是 HDF 驱动理论上可以运行在不同的操作系统内核(如鸿蒙微内核、Linux内核、LiteOS)之上,并且可以适配不同的硬件平台。
\\n所以通过 HDF 本身就可以让鸿蒙支持 Linux 内核驱动,并且在鸿蒙内核还支持用户态驱动容器 (User-mode Driver Container) ,这个机制允许将一些 Linux生态的设备驱动加载到用户态空间中运行,而不是传统的内核态:
\\n\\n\\nHDF 负责驱动的加载、管理以及提供与系统其他部分的通信接口,而「容器」则为这些驱动提供了一个隔离的、模拟了部分 Linux 内核环境的运行空间。
\\n
所以,可以看到,通过这三大件,一个 Linux 应用的调用,会经过 POSIX 兼容性(musl libc + KAL),然后通过 ABI 兼容 (Shim Layer),最后通过 IPC 访问到所需的内核支持,而 HDF 提供了驱动兼容,从而实现了 Linux 的兼容运行环境。
\\n\\n\\n所以虽然鸿蒙微内核不是 Linux ,但是它可以提供出 Linux 兼容,甚至翻译出 Linux 模拟环境。
\\n
其实这也是 iSulad 容器在鸿蒙微内核上运行并创建 Linux 容器环境的核心,也是为什么「卓易通」能实现 Android 模拟器的基础支持。
\\n另外,关于兼容时的代码传染性部分,目前鸿蒙内核也是基于用户态独立进程进行了隔离,比如驱动容器(LDC)复用Linux设备驱动程序:
\\n另外,回到鸿蒙 PC 上,在 PC 上如果具备了完善且高效的 POSIX 兼容性和基础的Linux ABI兼容性,那么理论上就为之前我们聊鸿蒙 PC 的 Wine 的移植和运行提供了可能性。
\\n并且华为还有类似 ExaGea 毕昇编译器支持,说是实现无源码迁移,ExaGear 作为二进制翻译器,能够将 x86 指令动态或静态地翻译成ARM 指令,从而允许 x86 应用在ARM设备上运行,这也算是一个基础支持。
\\n\\n\\n当然,行不行是一回事,好不好用又是另一回事,这里讨论的是可行性。
\\n
所以,从这个角度看,微内核确实不是 Linux 套壳,也可以看出来,鸿蒙微内核确实是一个大工程,只是它某些部分不需要从零开始。
\\n文章参考:docs.flutter.cn/perf/best-p…
\\n控制 build() 方法的耗时
\\n避免在 build()
方法中进行重复且耗时的工作,因为当父 widget 重建时,子 Wdiget 的 build()
方法会被频繁地调用。
避免在一个超长的 build()
方法中返回一个过于庞大的 widget。把它们分拆成不同的 widget,并进行封装
局部刷新: 当在 State
对象上调用 setState()
时,所有后代 widget 都将重建。因此,将 setState()
的调用转移到其 UI 实际需要更改的 widget 子树部分。 如果改变的部分仅包含在 widget 树的一小部分中,请避免在 widget 树的更高层级中调用 setState()
。
Child缓存:当重新遇到与前一帧相同的子 widget 实例时,将停止遍历。这种技术在框架内部大量使用,用于优化动画不影响子树的动画。请参阅 TransitionBuilder
模式,理解把Child缓存起来。
final
状态变量,并在 build 方法中重用它。重用 Widget 比创建一个新的(但配置相同的)Widget 效率更高。另一种缓存策略是将 Widget 的可变部分提取到 接受 child 参数的StatefulWidget中。尽可能使用 const
小部件。(这相当于缓存小部件并重复使用。)这将让 Flutter 的 widget 重建时间大幅缩短。要自动提醒使用 const
。
在构建可复用的 UI 代码时,最好使用 StatelessWidget
而不是函数。
最小化重建有状态小部件
\\n谨慎使用 saveLayer()
\\nsaveLayer()
。尽量减少使用不透明度和裁剪
\\n使用透明的颜色比透明的Opacity
更快:
Container(color: Color.fromRGBO(255, 0, 0, 0.5))
比快得多Opacity(opacity: 0.5, child: Container(color: Colors.red))
。Clipping 不会调用 saveLayer()
(除非明确使用 Clip.antiAliasWithSaveLayer
),因此这些操作没有 Opacity
那么耗时,但仍然很耗时,所以请谨慎使用。
能不用 Opacity
widget,就尽量不要用。有关将透明度直接应用于图像的示例,请查看 Transparent image,这比使用 Opacity
widget 更快。
要在图像中实现淡入淡出,请考虑使用 FadeInImage
widget,该 widget 使用 GPU 的片段着色器应用渐变不透明度。
要创建带圆角的矩形,而不是裁剪矩形来达到圆角的效果,请考虑使用很多 widget 都提供的 borderRadius
属性。
陷进:
\\nOpacity
widget,尤其是在动画中避免使用。可以使用 AnimatedOpacity
或 FadeInImage
代替该操作。AnimatedBuilder
时,请避免在不依赖于动画的 widget 的构造方法中构建 widget 树,不然,动画的每次变动都会重建这个 widget 树,应当将这部分子树作为 child 传递给 AnimatedBuilder
,从而只构建一次。更多内容Widget
对象上重写 operator ==
。虽然这看起来有助于避免不必要的重建,但在实践中,它实际上损害了性能,因为这是 O(N²) 的行为。比较 widget 的属性可能比重建 widget 更加有效,也能更少改变 widget 的配置。即使在这种情况下,最好还要缓存 widget,因为哪怕有一次对 operator ==
进行覆盖也会导致全面性能的下降,编译器也会因此不再认为调用总是静态的谨慎使用网格列表和列表
\\nchildren widget
在屏幕上不可见,请避免使用返回具体列表的构造函数(例如 Column()
或 ListView()
),以避免构建成本。使用ListView.build()避免内部传递
\\n不要先算子widget的大小,再去计算父组件的高度。例如轮询计算高度
\\n卡片
网格列表时。一个网格列表应该有统一大小的单元格,所以布局代码执行了一次传递,从网格列表的根部开始(在 widget 树中),要求网格列表中的 每个 卡片(不仅仅是可见的卡片)来返回 内部 尺寸—假设没有任何限制,widget 更喜欢这样的尺寸。有了这些信息,底层框架就确定了一个统一的单元格尺寸,并再次重新访问所有的网格单元,告诉每个卡片应该使用什么尺寸。隔离重绘区域:自定义的绘制使用RepaintBoundary包裹
\\n直接展示代码\\n1、微信分享\\n1.1、安装 fluwx插件,去pub.dev下载最新的, 点击去\\n1.2、初始化fluwx,\\n在main.dart页面中的main方法执行一下代码
\\nFluwx fluwx = Fluwx();\\n// 注册微信API\\nfluwx.registerApi(\\n appId: Config.wxAppId, // 你的微信AppID\\n universalLink: Config.universalLink // iOS必填\\n );\\n\\n
\\n1.3 分享代码
\\nbool isWeChatInstalled = await fluwx.isWeChatInstalled; // 一定到判断是否安装微信\\n\\n// 分享到微信会话\\nvoid shareToWeChat(String type) async {\\n if (!isWeChatInstalled) {\\n Fluttertoast.showToast(\\n msg: \\"请安装微信客户端后再试\\",\\n toastLength: Toast.LENGTH_SHORT,\\n gravity: ToastGravity.BOTTOM,\\n timeInSecForIosWeb: 1,\\n backgroundColor: Colors.white,\\n textColor: Colors.black,\\n fontSize: 14.0,\\n );\\n return;\\n }\\n try {\\n // 加载缩略图 二进制数据\\n final response = await http.get(Uri.parse(\'网络图片地址\'));\\n Uint8List thumbnailData = response.bodyBytes;\\n WeChatScene scene = WeChatScene.session;\\n if (type == \'session\') {\\n // 好友\\n scene = WeChatScene.session;\\n }\\n if (type == \'timeline\') {\\n //朋友圈\\n scene = WeChatScene.timeline;\\n }\\n String url = \'https://oceantech2023.cn/invite?code=$invitationCode\';\\n final result = await fluwx.share(\\n WeChatShareWebPageModel(\\n url, // 要分享的网页链接(String)不是必填\\n title: \'注册获7天免费会员!\', // 分享标题 不是必填\\n description: \\"\\"\\"🎉 好消息!我为你带来了独享的福利!🎉 !\\"\\"\\", // 描述说明 不是必填\\n thumbData: thumbnailData, //分享时的小图标(Uint8List 类型的图片二进制数据,推荐 <32KB) 不是必填\\n scene: scene,// 分享的目标场景(微信好友/朋友圈等) \\n ),\\n );\\n print(\'分享结果: $result\');\\n } catch (e) {\\n logger.e(\\"分享失败: $e\\");\\n Fluttertoast.showToast(\\n msg: \\"分享失败\\",\\n toastLength: Toast.LENGTH_SHORT,\\n gravity: ToastGravity.BOTTOM,\\n timeInSecForIosWeb: 1,\\n backgroundColor: Colors.black,\\n textColor: Colors.white,\\n fontSize: 14.0,\\n );\\n }\\n}\\n\\n
\\n2、QQ分享\\n2.1、安装tencent_kit 插件,去pub.dev下载最新的, 点击去\\n2.2、初始化tencent_kit,\\n在main.dart页面中的main方法执行一下代码
\\n WidgetsFlutterBinding.ensureInitialized();\\n await Tencent.instance.registerApp(appId: \'你的QQ AppID\');\\n
\\n2.3、分享代码
\\nvoid shareToQQ(String type) async {\\nTencentScene scene = TencentScene.kScene_QQ;\\nfinal bool isInstalled = await Tencent.instance.isQQInstalled();\\nif (!isInstalled) {\\n Fluttertoast.showToast(\\n msg: \\"请安装QQ客户端后再试\\",\\n toastLength: Toast.LENGTH_SHORT,\\n gravity: ToastGravity.BOTTOM,\\n timeInSecForIosWeb: 1,\\n backgroundColor: Colors.white,\\n textColor: Colors.black,\\n fontSize: 14.0,\\n );\\n return;\\n}\\nif (type == \'kScene_QQ\') {\\n // 好友\\n scene = WeChatScene.kScene_QQ;\\n }\\n if (type == \'kScene_QZone\') {\\n //朋友圈\\n scene = WeChatScene.kScene_QZone;\\n }\\nfinal result = await Tencent.instance.shareWebpage(\\n scene: scene, // 分享给QQ好友\\n title: \'注册得7天超级会员!\', // ✅ 必填\\n summary: \'填写邀请码立即获得会员体验,限时福利速来!\', // ✅ 必填\\n targetUrl: \'https://yourdomain.com/invite?code=XXX\', // ✅ 必填\\n imageUri: Uri.parse(\'https://yourcdn.com/cover.jpg\'), // ✅ 可选:缩略图\\n);\\n
","description":"直接展示代码 1、微信分享 1.1、安装 fluwx插件,去pub.dev下载最新的, 点击去 1.2、初始化fluwx, 在main.dart页面中的main方法执行一下代码 Fluwx fluwx = Fluwx();\\n// 注册微信API\\nfluwx.registerApi(\\n appId: Config.wxAppId, // 你的微信AppID\\n universalLink: Config.universalLink // iOS必填\\n );\\n\\n\\n\\n1.3 分享代码\\n\\nbool isWeChatInstalled = await…","guid":"https://juejin.cn/post/7505633505980841993","author":"方文_","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-19T03:20:09.029Z","media":null,"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 中的 SOLID 原则实用指南:L — 里氏替换原则(LSP)","url":"https://juejin.cn/post/7505025092432494644","content":"在 SOLID 五大设计原则中,\\"L\\" 代表 里氏替换原则(Liskov Substitution Principle,LSP),它提出了一个简单却强大的要求:
\\n\\n\\n子类必须能够替换父类,并保持行为的正确性。
\\n
也就是说,当一个模块依赖于某个抽象类型时,任何实现这个抽象的具体子类,都应该能在不改变程序逻辑的前提下替换使用。如果替换后出现了异常、逻辑错误或行为不一致,那么你就违反了 LSP。
\\n我们在开发一个通用的本地存储服务,用于保存用户偏好、缓存数据等。我们先定义一个抽象的接口:
\\nabstract class LocalStorageService {\\n Future<void> save(String key, String value);\\n Future<String?> read(String key);\\n Future<void> delete(String key);\\n}\\n
\\n这看起来没什么问题。
\\n但某一天你需要实现一个“只读”的本地数据源(比如用于临时切换用户回放的 Mock 数据),于是你这样写:
\\nclass ReadOnlyStorageService implements LocalStorageService {\\n @override\\n Future<void> save(String key, String value) {\\n throw UnsupportedError(\'只读模式,不支持保存\');\\n }\\n\\n @override\\n Future<String?> read(String key) async {\\n return \'preset_value\';\\n }\\n\\n @override\\n Future<void> delete(String key) {\\n throw UnsupportedError(\'只读模式,不支持删除\');\\n }\\n}\\n
\\n这段代码能编译、能运行,但却存在致命设计问题。
\\n如果有如下调用方代码:
\\nFuture<void> updateUsername(LocalStorageService storage) async {\\n await storage.save(\'username\', \'flutter_dev\');\\n}\\n
\\n这段逻辑在使用正常的缓存服务时工作正常,但你一旦传入 ReadOnlyStorageService
,应用就崩了 —— 这说明它不能被安全替换为父类的类型,这就是违反了里氏替换原则!
为了满足 LSP,我们要将接口职责区分清楚,让只读存储和可写存储有各自的边界。
\\nabstract class StorageReadable {\\n Future<String?> read(String key);\\n}\\n\\nabstract class StorageWritable {\\n Future<void> save(String key, String value);\\n Future<void> delete(String key);\\n}\\n
\\nclass SharedPreferencesStorage implements StorageReadable, StorageWritable {\\n final SharedPreferences prefs;\\n\\n SharedPreferencesStorage(this.prefs);\\n\\n @override\\n Future<String?> read(String key) async => prefs.getString(key);\\n\\n @override\\n Future<void> save(String key, String value) async {\\n await prefs.setString(key, value);\\n }\\n\\n @override\\n Future<void> delete(String key) async {\\n await prefs.remove(key);\\n }\\n}\\n
\\nclass ReadOnlyStorage implements StorageReadable {\\n final Map<String, String> preset;\\n\\n ReadOnlyStorage(this.preset);\\n\\n @override\\n Future<String?> read(String key) async => preset[key];\\n}\\n
\\nFuture<void> loadUserName(StorageReadable storage) async {\\n final name = await storage.read(\'username\');\\n print(name);\\n}\\n
\\n这样无论传入哪种存储实现,调用方都不会出错,接口行为始终一致。这就是 LSP 在 Flutter 项目中的完美体现。
\\n将读写接口分离之后,配合 Riverpod 可以构建更灵活的依赖注入方案:
\\nfinal readOnlyStorageProvider = Provider<StorageReadable>((ref) {\\n return ReadOnlyStorage({\'username\': \'mock_user\'});\\n});\\n\\nfinal localStorageProvider = Provider<StorageReadable>((ref) {\\n final prefs = ref.watch(sharedPreferencesProvider);\\n return SharedPreferencesStorage(prefs);\\n});\\n
\\n这样你就可以在测试时注入 ReadOnlyStorage,在正式逻辑中使用完整实现,而不需要更改任何调用代码,真正实现面向抽象编程。
\\n做法 | 是否符合 LSP | 说明 |
---|---|---|
子类抛出异常替代父类方法 | ❌ 不符合 | 替换后出错,说明子类不具备行为兼容性 |
拆分接口按功能组合实现 | ✅ 符合 | 子类可以自由组合接口,实现职责内的行为,避免非法替换 |
接口依赖注入明确职责范围 | ✅ 符合 | 调用方按需依赖读/写接口,避免超出职责范围的误用 |
在项目复杂化的过程中,如果你发现某个子类“实现接口的时候很别扭”或“某些方法根本不想写”,请警惕 —— 这很可能是你在违反 LSP。
\\n记住:
\\n\\n\\n能替换才是真子类,不兼容就是伪继承!
\\n
如果你觉得这篇文章对你有启发,欢迎点个 “赞” 或 “在看”,让更多开发者写出优雅、可维护的 Flutter 代码。
","description":"在 SOLID 五大设计原则中,\\"L\\" 代表 里氏替换原则(Liskov Substitution Principle,LSP),它提出了一个简单却强大的要求: 子类必须能够替换父类,并保持行为的正确性。\\n\\n也就是说,当一个模块依赖于某个抽象类型时,任何实现这个抽象的具体子类,都应该能在不改变程序逻辑的前提下替换使用。如果替换后出现了异常、逻辑错误或行为不一致,那么你就违反了 LSP。\\n\\n🧠 一个贴合 Flutter 项目的例子\\n✅ 场景设定:\\n\\n我们在开发一个通用的本地存储服务,用于保存用户偏好、缓存数据等。我们先定义一个抽象的接口:\\n\\nabstract…","guid":"https://juejin.cn/post/7505025092432494644","author":"OldBirds","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-19T01:05:02.983Z","media":null,"categories":["Android","Flutter","iOS","前端"],"attachments":null,"extra":null,"language":null},{"title":"2025 跨平台框架更新和发布对比,这是你没看过的全新版本","url":"https://juejin.cn/post/7505578411492474915","content":"2025 年可以说又是一个跨平台的元年,其中不妨有「鸿蒙 Next」 平台刺激的原因,也有大厂技术积累“达到瓶颈”的可能,又或者“开猿截流、降本增笑”的趋势的影响,2025 年上半年确实让跨平台框架又成为最活跃的时刻,例如:
\\n而本篇也是基于上面的内容,对比当前它们的情况和未来可能,帮助你在选择框架时更好理解它们的特点和差异。
\\n\\n\\n就算你不用,也许面试的时候就糊弄上了?
\\n
首先 Flutter 大家应该已经很熟悉了,作为在「自绘领域」坚持了这么多年的跨平台框架,相信也不需要再过多的介绍,因为是「自绘」和 「AOT 模式」,让 Flutter 在「平台统一性」和「性能」上都有不错的表现。
\\n\\n\\n开发过程过程中的 hotload 的支持程度也很不错。
\\n
而自 2025 以来的一些更新也给 Flutter 带来了新的可能,比如 Flutter Platform 和 UI 线程合并 ,简单来说就是以前 Dart main Thread 和 Platform UI Thread 是分别跑在独立线程,它们的就交互和数据都需要经过 Channel 。
\\n而合并之后,Dart main 和 Platform UI 在 Engine 启动完成后会合并到一个线程,此时 Dart 和平台原生语言就支持通过同步的方式去进行调用,也为 Dart 和 Kotlin/Java,Swift/OC 直接同步互操作在 Framework 提供了进一步基础支持。
\\n\\n\\n当然也带来一些新的问题,具体可见线程合并的相关文章。
\\n
另外在当下,其实 Flutter 的核心竞争力是 Impeller ,因为跨平台框架不是系统“亲儿子”,又是自绘方案,那么在性能优化上,特别 iOS 平台,就不得不提到着色器预热或者提前编译。
\\n\\n\\n传统 Skia 需要把「绘制命令」编译成可在 GPU 执行代码的过程,一般叫做着色器编译, Skia 需要「动态编译」着色器,但是 Skia 的着色器「生成/编译」与「帧工作」是按顺序处理,如果这时候着色器编译速度不够快,就可能会出现掉帧(Jank)的情况,这个我们也常叫做「着色器卡顿」
\\n
而 Impeller 正是这个背景的产物,简单说,App 所需的所有着色器都在 Flutter 引擎构建时进行离线编译,而不是在应用运行时编译。
\\n这其实才是目前是 Flutter 的核心竞争力,不同于 Skia 需要考虑多场景和平台通用性,需要支持各种灵活的额着色器场景,Impeller 专注于 Flutter ,所以它可以提供更好的专注支持和问题修复,更多可见:着色器预热?为什么 Flutter 需要?
\\n\\n\\n当然 Skia 也是 Google 项目,对于着色器场景也有 Graphite 后端在推进支持,它也在内部也是基于 Impeller 为原型去做的改进,所以未来 Skia 也可以支持部分场景的提前编译。
\\n
而在鸿蒙平台,华为针对 Flutter 在鸿蒙的适配,在华为官方过去的分享里,也支持了 Flutter引擎Impeller鸿蒙化,详细可见:b23.tv/KKNDAQB
\\n甚至,Flutter 在类游戏场景支持也挺不错,如果配合 rive 的状态机和自适应,甚至可以开发出很多出乎意料的效果,而官方也有 Flutter 的游戏 SDK 或者 Flame 第三方游戏包支持:
\\n最后,那么 Flutter 的局限性是什么呢?其实也挺多的,例如:
\\n\\n\\n所以,Flutter 适合你的场景吗?
\\n
如果你很久没了解过 RN ,那么 2025 年的 RN 会超乎你的想象,可以说 Skia 和 WebGPU 给了它更多的可能。
\\nRN 的核心之一就是对齐 Web 开发体验,其中最重要的就是 0.76 之后 New Architecture 成了默认框架,例如 Fabric, TurboModules, JSI 等能力解决了各种历史遗留的性能瓶颈,比如:
\\nChakra
、v8
、Hermes
,同时允许 JS 和 Native 线程之间的同步相互执行另外现在新版 RN 也支持热重载,同时可以更快对齐新 React 特性,例如 React 19 的 Actions、改进的异步处理等 。
\\n而另一个支持就是 RN 在 Skia 和 WebGPU 的探索和支持,使用 Skia 和 WebGPU 不是说 RN 想要变成自绘,而是在比如「动画」和「图像处理」等场景增加了强力补充,比如:
\\n\\n\\nReact Native Skia Video 模块,实现了原生纹理(iOS Metal, Android OpenGL)到 React Native Skia 的直接传输,优化了内存和渲染速度,可以被用于视频帧提取、集成和导出等,生态中还有 React Native Vision Camera 和 React Native Video (v7) 等支持 Skia 的模块:
\\n
还有是 React Native 开始引入 WebGPU 支持,其效果将确保与 Web 端的 WebGPU API 完全一致,允许开发者直接复制代码示例的同时,实现与 Web Canvas API 对称的 RN Canvas API:
\\n最后,WebGPU 的引入还可以让 React Native 开发者能够利用 ThreeJS 生态,直接引入已有的 3D 库,这让 React Native 的能力进一步对齐了 Web :
\\n最后,RN 也是有华为推进的鸿蒙适配,会采用 XComponent 对接到 ArkUI 的后端接口进行渲染,详细可见:鸿蒙版 React Native 正式开源 。
\\n而在 PC 领域 RN 也有一定支持,比如微软提供的 windows 和 macOS 支持,社区提供的 web 和 Linux 支持,只是占有并不高,一般忽略。
\\n而在小程序领域,有京东的 Taro 这样的大厂开源支持,整体在平台兼容上还算不错。
\\n\\n\\n当然,RN 最大的优势还在于成熟的 code-push 热更新支持。
\\n
那么使用 RN 有什么局限性呢?最直观的肯定是平台 UI 的一致性和样式约束,这个是 OEM 框架的场景局限,而对于其他的,目前存在:
\\n事实上, RN 是 Cordova 之后我接触的第一个真正意义上的跨平台框架,从我知道它到现在应该有十年了,那么你会因为它的新架构和 WebGPU 能力而选择 RN 么?
\\n更多可见:
\\n\\nCompose Multiplatform(CMP) 近期的热度应该来自 Compose Multiplatform iOS 稳定版发布 ,作为第二个使用 Skia 的自绘框架,除了 Web 还在推进之外, CMP 基本完成了它的跨平台稳定之路。
\\n\\n\\nCompose Multiplatform(CMP) 是 UI,Kotlin Multiplatform (KMP) 是语言基础。
\\n
CMP 使用 Skia 绘制 UI ,甚至在 Android 上它和传统 View 体系的 UI 也不在一个渲染树,并且 CMP 通过 Skiko (Skia for Kotlin) 这套 Kotlin 绑定库,进而抹平了不同架构(Kotlin Native,Kotlin JVM ,Kotlin JS,Kotlin wasm)调用 skia 的差异。
\\n所以 CMP 的优势也来自于此,它可以通过 skia 做到不同平台的 UI 一致性,并且在 Android 依赖于系统 skia ,所以它的 apk 体积也相对较小,而在 PC 平台得益于 JVM 的成熟度,CMP 目前也做到了一定的可用程度。
\\n其中和 Android JVM 模式不同的是,Kotlin 在 iOS 平台使用的是 Kotlin/Native ,Kotlin/Native 是 KMP 在 iOS 支持的关键能力,它负责将 Kotlin 代码直接编译为目标平台的机器码或 LLVM 中间表示 (IR),最终为 iOS 生成一个标准 .framework
,这也是为什么 Compose iOS 能实现接近原生的性能。
\\n\\n实现鸿蒙支持目前主流方式也是 Kotlin/Native ,不得不说 Kotlin 最强大的核心价值不是它的语法糖,而是它的编译器,当然也有使用 Kotlin/JS 适配鸿蒙的方案。
\\n
所以 CMP 最大的优势其实是 Kotlin ,Kotlin 的编译器很强大,支持各种编译过程和产物,可以让 KMP 能够灵活适配到各种平台,并且 Kotlin 语法的优势也让使用它的开发者忠诚度很高。
\\n不过遗憾的是,目前 CMP 鸿蒙平台的适配上都不是 Jetbrains 提供的方案,华为暂时也没有 CMP 的适配计划,目前已知的 CMP/KMP 适配基本是大厂自己倒腾的方案,有基于 KN 的 llvm 方案,也有基于 Kotlin/JS 的低成本方案,只是大家的路线也各不相同。
\\n\\n\\n在小程序领域同样如此。
\\n
另外现在 CMP 开发模式下的 hot reload 已经可以使用 ,不过暂时只支持 desktop,原理大概是只支持 jvm 模式。
\\n而在社区上,klibs.io 的发布也补全了 Compose Multiplatform 在跨平台最后一步,这也是 Compose iOS 能正式发布的另外一个原因:
\\n那么聊到这里,CMP 面临的局限性也很明显:
\\n相信 2025 年开始,CMP 会是 Android 原生开发者在跨平台的首选之一,毕竟 Kotlin 生态不需要额外学习 Dart 或者 JS 体系,那么你会选择 CMP 吗?
\\nKuikly 其实也算是 KMP 体系的跨平台框架,只是腾讯在做它的时候还没 CMP ,所以一开始 Kuikly 是通过 KMM 进行实现,而后在 UI 层通过自己的方案完成跨平台。
\\n这其实就是 Kuikly 和 CMP 最大的不同,底层都是 KMP 方案,但是在绘制上 Kuikly 采用的是类 RN 的方式,目前 Kuikly 主要是在 KMP 的基础上实现的自研 DSL 来构建 UI ,比如 iOS 平台的 UI 能力就是 UIkit ,而大家更熟悉的 Compose 支持,目前还处于开发过程中:
\\n\\n\\nSwiftUI 和 Compose 无法直接和 Kuikly 一起使用,但是 Kuikly 可以在 DSL 语法和 UI 组件属性对齐两者的写法,变成一个类 Compose 和 SwiftUI 的 UI 框架,也就是 Compose DSL 大概就是让 Kuikly 更像 Compose ,而不是直接适配 Compose 。
\\n
那么,Kuikly 和 RN 之间又什么区别?
\\n第一,Kuikly 支持 Kotlin/JS 和 Kotlin/Native 两种模式,也就是它可以支持性能很高的 Native 模式
\\n第二,Kuikly 实现了自己的一套「薄原生层」,Kuikly 使用“非常薄”的原生层,该原生层只暴露最基本和无逻辑的 UI 组件(原子组件),也就是 Kuikly 在 UI 上只用了最基本的原生层 UI ,真正的 UI 逻辑主要在共享的 Kotlin 代码来实现:
\\n\\n\\n通过将 UI 逻辑抽象到共享的 Kotlin 层,减少平台特定 UI 差异或行为差异的可能性,「薄原生层」充当一致的渲染目标,确保 Kotlin 定义的 UI 元素在所有平台上都以类似的方式显示。
\\n
也就是说,Kuikly 虽然会依赖原生平台的控件,但是大部分控件的实现都已经被「提升」到 Kuikly 自己的 Kotlin 共享层,目前 Kuikly 实现了 60% UI 组件的纯 Kotlin 组合封装实现,不需要 Native 提供原子控件 。
\\n\\n\\n另外 Kuikly 表示后续会支持全平台小程序,这也是优势之一。
\\n
最后,Kuikly 还在动态化热更新场景, 可以和自己腾讯的热更新管理平台无缝集成,这也是优势之一。
\\n那么 Kuikly 存在什么局限性?首先就是动态化场景只支持 Kotlin/JS,而可动态化类型部分:
\\n其他的还有:
\\n\\n\\n另外,腾讯还有另外一个基于 CMP 切适配鸿蒙的跨平台框架,只是何时开源还尚不明确
\\n
那么,你会为了小程序和鸿蒙而选择 Kuikly 吗?
\\n更多可见:腾讯 Kuikly 正式开源
\\n如果说 Kuikly 是一个面向客户端的全平台框架,那么 Lynx 就是一个完全面向 Web 前端的跨平台全家桶。
\\n目前 Lynx 开源的首个支持框架就是基于 React 的 ReactLynx,当然官方也表示Lynx 并不局限于 React,所以不排除后续还有 VueLynx 等其他框架支持,而 Lynx 作为核心引擎支持,其实并不绑定任何特定前端框架,只是当前你能用的暂时只有 ReactLynx :
\\n而在实现上,源代码中的标签,会在运行时被 Lynx 引擎解析,翻译成用于渲染的 Element,嵌套的 Element 会组成的一棵树,从而构建出UI界面:
\\n所以从这里看,初步开源的 Lynx 是一个类 RN 框架,不过从官方的介绍“选择在移动和桌面端达到像素级一致的自渲染” ,可以看出来宣传中可以切换到自渲染,虽然暂时还没看到。
\\n而对于 Lynx 主要的技术特点在于:
\\n所以从 Lynx 的宏观目标来看,它即支持类 RN 实现,又有自绘计划,同时除了 React 模式,后期还适配 Vue、Svelte 等框架,可以说是完全针对 Web 开发而存在的跨平台架构。
\\n\\n\\n另外支持平台也足够,Android、iOS、鸿蒙、Web、PC、小程序都在支持列表里。
\\n
最后,Lynx 对“即时首帧渲染 (IFR)”和“丝滑流畅”交互体验有先天优势,开发双线程模型及主线程脚本 (MTS) 让 Lynx 的启动和第一帧渲染速度还挺不错,比如:
\\n而在多平台上,Lynx 是自主开发的渲染后端支持 Windows、tvOS、MacOS 和 HarmonyOS ,但是不确实是否支持 Linux:
\\n那 Lynx 有什么局限性?首先肯定是它非常年轻,虽然它的饼很大,但是对应社区、生态系统、第三方库等都还需要时间成长。
\\n\\n\\n所以官方也建议 Lynx 最初可能更适合作为模块嵌入到现有的原生应用中,用于构建特定视图或功能,而非从零开始构建一个完整的独立应用 。
\\n
其次就是对 Web 前端开发友好,对客户端而言学习成本较高,并且按照目前的开源情况,除了 Android、iOS 和 Web 的类 RN 实现外,其他平台的支持和自绘能力尚不明确:
\\n最后,Lynx 的开发环境最好选 macOS,关于 Windows 和 Linux 平台目前工具链兼容性还需要打磨。
\\n那么,总结下来,Lynx 应该会是前端开发的菜,那你觉得 Lynx 是你的选择么?
\\n更多可见:字节跨平台框架 Lynx 开源
\\n说到 uni-app 大家第一印象肯定还是小程序,而虽然 uni-app 也可以打包客户端 app,甚至有基于 weex 的 nvue 支持,但是其效果只能说是“一言难尽”,而这里要聊的 uni-app x ,其实就是 DCloud 在跨平台这两年的新尝试。
\\n具体来说,就是 uni-app 不再是运行在 jscore 的跨平台框架,它是“基于 Web 技术栈开发,运行时编译为原生代码”的模式,相信这种模式大家应该也不陌生了,简单说就是:js(uts) 代码在打包时会直接编译成原生代码:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n目标平台 | uts 编译后的原生语言 |
---|---|
Android | Kotlin |
iOS | Swift |
鸿蒙 | ArkTS |
Web / 小程序 | JavaScript |
甚至极端一点说,uni-app x 可以不需要单独写插件去调用平台 API,你可以直接在 uts 代码里引用平台原生 API ,因为你的代码本质上也是会被编译成原生代码,所以 uts ≈ native code ,只是使用时需要配置上对应的条件编译(如 APP-ANDROID
、APP-IOS
)支持:
import Context from \\"android.content.Context\\";\\nimport BatteryManager from \\"android.os.BatteryManager\\";\\n•\\nimport { GetBatteryInfo, GetBatteryInfoOptions, GetBatteryInfoSuccess, GetBatteryInfoResult, GetBatteryInfoSync } from \'../interface.uts\'\\nimport IntentFilter from \'android.content.IntentFilter\';\\nimport Intent from \'android.content.Intent\';\\n•\\nimport { GetBatteryInfoFailImpl } from \'../unierror\';\\n•\\n/**\\n * 获取电量\\n */\\nexport const getBatteryInfo : GetBatteryInfo = function (options : GetBatteryInfoOptions) {\\n const context = UTSAndroid.getAppContext();\\n if (context != null) {\\n const manager = context.getSystemService(\\n Context.BATTERY_SERVICE\\n ) as BatteryManager;\\n const level = manager.getIntProperty(\\n BatteryManager.BATTERY_PROPERTY_CAPACITY\\n );\\n•\\n let ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);\\n let batteryStatus = context.registerReceiver(null, ifilter);\\n let status = batteryStatus?.getIntExtra(BatteryManager.EXTRA_STATUS, -1);\\n let isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL;\\n•\\n const res : GetBatteryInfoSuccess = {\\n errMsg: \'getBatteryInfo:ok\',\\n level,\\n isCharging: isCharging\\n }\\n options.success?.(res)\\n options.complete?.(res)\\n } else {\\n let res = new GetBatteryInfoFailImpl(1001);\\n options.fail?.(res)\\n options.complete?.(res)\\n }\\n}\\n•\\n\\n
\\n比如上方代码,通过 import BatteryManager from \\"android.os.BatteryManager\\"
可以直接导入使用 Android 的 BatteryManager
对象。
可以看到,在 uni-app x 你是可以“代码混写”的,所以与传统的 uni-app 不同,uni-app 依赖于定制 TypeScript 的 uts 和 uvue 编译器:
\\nArray
、Date
、JSON
、Map
、Math
、String
等内置对象转为 Kotlin、Swift、ArkTS 的对象等,所以也不需要有 uts 之类的虚拟机,另外 uts 编译器在处理特定平台时,还会调用相应平台的原生编译器,例如 Kotlin 编译器和 Swift 编译器而在 UI 上,目前除了编译为 ArkUI 之外,Android 和 iOS 其实都是编译成原生体系,目前看在 Android 应该是编译为传统 View 体系而不是 Compose ,而在 iOS 应该也是 UIKit ,按照官方的说法,就是性能和原生相当。
\\n所以从这点看,uni-app x 是一个类 RN 的编译时框架,所以,它的局限性问题也很明显,因为它的优势在于编译器转译得到原生性能,但是它的劣势也是在于转译:
\\n那么,你觉得 uni-app x 会是你跨平台选择之一么?
\\n更多可见:uni-app x 正式支持鸿蒙
\\n最后,我们简单做个总结:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n框架 (Framework) | 开发语言 | 渲染方式 | 特点 | 缺点 | 支持平台 | 维护企业 |
---|---|---|---|---|---|---|
Flutter | Dart | 自绘,Impeller | 自绘,多平台统一,未来支持 dart 和平台语言直接交互,Impeller 提供竞争力,甚至支持游戏场景 | 占用内存大,文本场景略弱,Impeller 还需要继续打磨 | android、iOS、Web、Windows、macOS、Linux、鸿蒙(华为社区提供) | |
React Native | JS 体系 | 原生 OEM + Skia/WebGPU 支持 | 新架构提供性能优化,对齐 Web,引入 skia 和 webGPU 补充,code-push 热更新 | UI 一致性和新旧架构的第三方支持 | android、iOS、鸿蒙(华为社区提供),额外京东 Taro 支持小程序,web、windows、macOS、Linux 第三方支持 | |
Compose Multiplatform | Kotlin体系 | Skia 自绘 | Kotlin 体系,skia 自绘,多平台统一,支持 kn、kjs、kwasm 、kjvm 多种模式 | KN JVM、JS、Wasm 生态需要整合,没有着色器预编方案 | android、iOS、Web、Windows、macOS、Linux | Jetbrains |
Kuikly | Kotlin体系 | 原生 OEM ,「薄原生层」 | 基于 KMP 的类 RN 方案,在动态化有优势 | 小部分 UI 一致性场景,UI 与 CMP 脱轨 | android、iOS、Web、鸿蒙、小程序 | 腾讯 |
Lynx | JS 体系 | 原生 OEM,未来也有自绘 | 对齐 Web 开发首选,秒开优化,规划丰富 | 非常早期 ,生态发展中,客户端不友好 | android、iOS、Web、Windows、macOS、鸿蒙、小程序 | 字节 |
uni-app x | uts | 原生 OEM,直接翻译为原生语言 | 支持混写 uts 和原生代码,直接翻译为原生 | 生态插件割裂,UI 一致性问题,翻译 API 长期兼容成本 | android、iOS、Web、鸿蒙、小程序 | DCloud |
什么,你居然看完了?事实上我写完都懒得查错别字了,因为真的太长了。
","description":"2025 年可以说又是一个跨平台的元年,其中不妨有「鸿蒙 Next」 平台刺激的原因,也有大厂技术积累“达到瓶颈”的可能,又或者“开猿截流、降本增笑”的趋势的影响,2025 年上半年确实让跨平台框架又成为最活跃的时刻,例如: Flutter Platform 和 UI 线程合并和Android Impeller 稳定\\nReact Native 优化 Skia 和发布全新 WebGPU 支持\\nCompose Multiplatform iOS 稳定版发布,客户端全平台稳定\\n腾讯 Kotlin 跨平台框架 Kuikly 正式开源\\n字节跨平台框架 Lynx…","guid":"https://juejin.cn/post/7505578411492474915","author":"恋猫de小郭","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-18T22:31:51.006Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f161d25167a746368c78eda6d0f9d314~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1748212311&x-signature=ZA8hSB1j5pENa5lkxpOQeh7uNRc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c852ef8510fd4e0e903e92bae77e1d73~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1748212311&x-signature=5npoxSz1pPTd19r3Bk6kSMwMOQg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/dd4e717ada944fa194b8718444e91350~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1748212311&x-signature=m3vOZQGI9lnaC5yps9neoYhjsRg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/934ae7dca0b548c3aa533e83ee4c7380~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1748212311&x-signature=BM47ty6UGmuntRYhRSz9NMobbxo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/cd72e5471003424bbbddef6a96d6d15a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1748212311&x-signature=v0vMSyYOYmJevh6TR%2Ffer26Ip0E%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/bf69662325504a4a93fa3d0fe4a4274d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1748212311&x-signature=wQaCUuCsQdVObj4PsoRQ5tZg63M%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f61eb6e208254fa09209ec33e8791a37~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1748212311&x-signature=muItbXZ9qppkABDDeUS02cot2jw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/48363c4f723648868bd8917cfb96b1ea~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1748212311&x-signature=sAMgMZfpXwkmwueWGjKcSSYii4o%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/daf79dbbc74049d68ef6e56f0fed6d4d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1748212311&x-signature=e9xR4zYJx38Yf6wYDjH5PjhmoiA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/57eb11b51736435a8ab69d39310c93c6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1748212311&x-signature=u8QuhJXmJi%2FOQroVqwGJFrvCw%2F8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/995fb7a55c8643acb3835f3844f004e2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1748212311&x-signature=tylMhGBwiUUR1V5LsLjbPhGPctk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4ee00f68e0c84ed2997f328bd69bbf0e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1748212311&x-signature=OYvpuB7m1%2F9%2FBeQ9RZIfEoWpcFQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1fe96ce0841a4efaa7bf67edd19f8ac9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1748212311&x-signature=W6nr1uVXvRszsiyQUaWamx92QGk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e1069208ab924ccf9e81331f717f054e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1748212311&x-signature=2LaVmyIMFyv6VDvEquP7n0DnBos%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ea3139a6e9fd4e7fa0d86307eb28c9d5~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1748212311&x-signature=1ZIm0HogC49TC6AEqcgF0gll9Cw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/98e76efc414b4d2ab433666bafcae1be~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1748212311&x-signature=oTgZTrAvIGn6wVtoMoErNa4RCIo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f9ff99147fca49b7a3ca0f5251f966c0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1748212311&x-signature=DO%2FUtjHDSB2lIR4FC%2F%2Fd7Mt8q%2FI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6d933926bcd44ba0b0c89a0dda3545e6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1748212311&x-signature=VVVXP5qEgFUfVCc2UAdHi8tzAPo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/8e5acc9e1ff3477d9df16d18519b93dc~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1748212311&x-signature=7QQqsCDBggpP0mnpmATItXR%2Bwu0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9bce8db4b39f4edda95090041ea29337~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1748212311&x-signature=RrNl9o0U49eNkhgxiOpvc8d3qD4%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"滑动窗口:TCP是如何进行流量控制和拥塞控制的?","url":"https://juejin.cn/post/7504943335841939475","content":"TCP(传输控制协议)和UDP(用户数据报协议)都是互联网协议族中的传输层协议,它们在数据传输的可靠性、效率、连接方式等方面存在差异,适用于不同的应用场景。
\\n连接方式:TCP是面向连接的协议。在数据传输之前,需要在发送端和接收端之间建立一条连接,就像打电话一样,需要先拨通对方号码,建立起连接后才能进行通话。
\\n可靠性:具有高度的可靠性。它通过确认机制、重传机制来确保数据的准确传输。例如,发送端发送数据后,接收端会返回确认信息,如果发送端没有收到确认,就会重新发送数据。同时,TCP还能对数据进行排序,保证接收端按正确的顺序接收数据。
\\n流量控制与拥塞控制:TCP具备流量控制和拥塞控制机制。流量控制可以防止发送方发送数据过快,导致接收方处理不过来而丢失数据。拥塞控制则是当网络出现拥塞时,降低发送方的数据发送速率,以避免网络进一步拥塞。
\\n数据传输效率:由于需要建立连接、进行确认和重传等操作,TCP的传输效率相对较低,尤其在传输少量数据时,额外的开销可能较为明显。
\\n连接方式:UDP是无连接的协议。它就像写信,不需要事先通知对方,直接将信件(数据)发送出去即可,发送方和接收方之间没有建立专门的连接。
\\n可靠性:UDP不保证数据传输的可靠性。它不会对数据进行确认、重传,也不负责数据的排序。数据可能会丢失、重复或乱序到达接收端,但在某些对实时性要求高的场景中,少量的数据丢失或错误是可以接受的。
\\n流量控制与拥塞控制:UDP没有内置的流量控制和拥塞控制机制,需要应用层自己来实现相关功能,如果应用层没有进行相应处理,可能会导致网络拥塞或数据丢失。
\\n数据传输效率:UDP的传输效率较高,因为它不需要建立连接和进行复杂的确认等操作,额外开销小,适合传输实时性要求高的数据。
\\n相同点:都是传输层协议,都为应用层提供服务,都可以实现主机之间的数据传输。
\\n不同点 :
\\n连接性:TCP面向连接,UDP无连接。
\\n可靠性:TCP可靠,UDP不可靠。
\\n有序性:TCP保证数据有序,UDP不保证。
\\n传输效率:TCP效率相对低,UDP效率高。
\\n首部开销:TCP首部开销大,通常为20字节,UDP首部开销小,只有8字节。
\\nTCP的应用场景
\\n文件传输:如FTP(文件传输协议),需要确保文件完整、准确地传输,TCP的可靠性可以保证文件在传输过程中不出现错误或丢失。
\\n电子邮件:SMTP(简单邮件传输协议)等邮件协议使用TCP,以确保邮件的内容和附件能够准确无误地到达收件人的邮箱。
\\n网页浏览:HTTP(超文本传输协议)基于TCP,保证网页的各种资源,如HTML代码、图片、脚本等能够完整、有序地传输到浏览器,使得网页能够正确显示。
\\nUDP的应用场景
\\n实时视频和音频流:如视频会议、在线直播、IP电话等,这些应用对实时性要求极高,允许一定程度的数据丢失,UDP的低延迟和高效率能够保证音视频的流畅播放,少量数据丢失可能只会导致短暂的画面卡顿或声音中断,不会影响整体体验。
\\n游戏:游戏中的实时数据,如玩家的位置、动作、游戏场景的更新等,需要快速传输,UDP能够满足游戏对实时性的要求,即使部分数据丢失,也可以通过游戏的补偿机制来尽量减少对游戏体验的影响。
\\nDNS(域名系统):DNS用于将域名转换为IP地址,它通常只需要传输少量的数据,并且对响应速度要求较高,UDP能够快速地完成域名查询和响应,提高域名解析的效率。
\\n在复杂网络环境下,TCP 为了能保证每个包真的送达了,并且接收端收到包的顺序和发送端是一致的,每发出一个包,需要一个类似回信的机制。
\\n这个回信就是 ACK 包,每个包发送的时候会有一个序列号,接收端回 ACK 包的时候会把序列号 +1 发送回来,发送端如果没有收到某个包的 ACK 包,会在一段时间之后尝试重新发送,直到收到 ACK 为止。这其实也是在网络和各种分布式系统中能确保消息可达的唯一方式。
\\n那问题来了,为了确保消息保序可达,难道每次发送一个新的包,都等待上一个包的 ACK 回来之后才发送吗?这样一来一回的效率显然是很低的,也就是每经过一个 RTT 的时间,我们只能发送一个包,假设一个 RTT 是 100ms,那在一秒中我们甚至只能发送 10 个包,这完全是不可接受的。其实我们在等待 ACK 的时候没有必要停止后续包的发送,因为网络传输虽然不稳定,但大部分包往往还是可达的,这样我们就可以获得数倍的传输效率提升。如果真的不幸遇到了丢包,接收端 ACK 姗姗来迟的时候,也就告诉了我们某个序列号之前的所有包全部收到,我们再根据一定的策略,尝试重新发送对应丢失的包就可以了。
\\n所以发送方需要缓存已发出但尚未收到 ACK 的包,接收方收到包但没有被用户进程消费之前也得把收到的包留着。但是,缓存是有大小限制的,程序消费数据和链路传输数据的能力也是有限的,发送端和接受端都需要一种机制来限制可发送或者可接收数据的最大范围。于是,滑动窗口和拥塞窗口应运而生。
\\n这两个算法都是为了防止网络中发送的包太多。不同的是两者的目的,滑动窗口机制,可以用来控制流量,防止接收方处理不过来消息;同样基于窗口机制的拥塞控制算法,则用来处理网络上数据包太多的情况,以避免网络中出现拥塞。
\\n这里说流量控制,主要就是为了防止接收方处理数据的速度跟不上发送方,避免随着时间推移,数据自然溢出接收方的缓冲区。虽然协议可以保证发送方没有收到 ACK,最终会重试重新发送,但如果需要大量反复发送冗余的数据,所占用的网络资源就被白白浪费了,在网络资源很紧缺的时候,这也会造成网络环境的恶化。TCP 控制流量的方式也很简单,就是滑动窗口机制。
\\n接收端会建立一个滑动窗口,由接收方向发送方通告,TCP 首部里的 window 字段就是用来表示窗口大小的,窗口表示的就是接收方目前能接收的缓冲区的剩余大小。
\\n发送方也会根据这个通告窗口的大小建立自己的滑动窗口。为了兼顾效率和可靠性,在发送方,所有未收到 ACK 的消息虽然可以发送,但是在收到 ACK 之前是一定要在缓冲区中保存的。
\\n发送窗口根据三个标准来划分:是否发送、是否收到 ACK、是否在接收方通告处理范围内,分成了四个部分。
\\n但如果发送方一直没有收到 ACK,随着数据不断被发送,很快可用窗口就会被耗尽。在这种情况下,发送方也就不会继续发送数据了,这种发送端可用窗口为零的情况我们也称为“零窗口”。
\\n正常来说,等接收端处理了一部分数据,又有了新的可用窗口之后,就会再次发送 ACK 报文通告发送端自己有新的可用窗口(因为发送端的可用窗口是受接收端控制的)。
\\n但是,万一要是 ACK 消息在网络传输中正好丢包了,那发送端还能感知到接收端窗口的变化吗?其实是不会的,在这个情况下,接收端就会一直等着发送端发送数据,而发送端也还会以为接收端仍然处于零窗口的状态,这样一直互相等待,就好像进入了死锁状态。解决办法也很简单,我们可以再引入一个零窗口定时器,如果发送端陷入零窗口的状态,就会启动这个定时器,去定时地询问接收端窗口是否可用了
\\n相对发送端来说,接收端要简单的多,主要就分为已经接收并确认的数据和未收到但可以接收的数据,这一部分也就是接收窗口;剩下的就是缓冲区放不下的区域,也就是不可接收的区域。
\\n如果进程读取缓冲区速度有所变化,接收端可能也会改变接收窗口的大小,每次通告给发送端,就可以控制发送端的发送速度了。这就是所谓的滑动窗口,也就是流量控制机制。
\\n而之所以是滑动窗口,也很好理解,随着 ACK 或者进程读取数据,窗口也会顺次往后移动。比如在发送端的窗口中,如果我们在某次通信中收到了一条 ACK 消息,表示 36 之前的消息都已经被收到了,那么整个可用的窗口就会顺次往右移动。
\\n总的来说,滑动窗口(流量控制机制)解决了发送端消息可能淹没接收端,导致处理跟不上的情况。
\\n那 TCP 协议又如何解决流量拥塞的情况呢?也就是网络中由于大量包传输,导致吞吐量下降甚至为 0 的情况。这和我们的道路交通很像,当车流越来越大的时候,整体的行车速度可能会不断下降,导致拥堵,最后吞吐量反而不如车少的时候。
\\n在实际网络中,因为大量的包传输,可能导致中间某些节点的缓冲区满载,从而多余的包被丢弃,需要重新发送,情况越发恶化,最差的时候,网络上的包都是重传的包并且反复地丢弃;整个网络传输能力甚至可以降低为 0。
\\n这当然是一个很严重的问题,TCP 协议同样提出了另外一个叫拥塞窗口的机制,很好地解决了这个问题。
\\n网络中每个节点不会有全局的网络通信情况,唯一能发现的就是自己的部分包丢了,这种时候它就有理由怀疑网络环境劣化,可能产生了拥塞。
\\nTCP 是一个比较无私的协议,在这种情况下,会选择减少自己发送的包。当网络上大部分通信协议传输层都采用的是 TCP 协议时,在出现拥塞的情况下,大部分节点都会不约而同地减少自己传输的包,这样网络拥塞情况就会得到极大的缓解,一直处于比较好的网络状态。
\\n所以我们就需要在发送端定义一个窗口 CWND(congestion window),也就是拥塞窗口;发送端能发送的最多没有收到 ACK 的包,也不会超过拥塞窗口的范围。
\\n引入拥塞控制机制的 TCP 协议,发送端最大的发送范围是拥塞窗口和滑动窗口中小的一个。拥塞窗口会动态地随着网络情况的变化而进行调整,大体上的策略是如果没有出现拥塞,我们扩大窗口大小,否则就减少窗口大小。
\\n具体是如何实现的呢?经典拥塞控制算法主要包括四个部分:
\\n首先是慢启动,在不确定拥塞是否会发生的时候,我们不会一上来就发送大量的包,而是会采用倍增的方式缓慢增加窗口的大小,窗口大小从 1 开始尝试,然后尝试 2、4、8、16 等越来越大的窗口。
\\n整个慢启动的过程看起来就像上图这样,指数型的增加拥塞窗口的大小。
\\n这样,倍增的方式窗口就会很快扩大;我们会在窗口大到一定程度时,减慢增加的速度,转成线性扩大窗口的方式,也就是每次收到新的 ACK 没有丢包的话只比上次窗口增大 1。整个过程看起来就像这样:
\\n慢启动阶段和拥塞避免阶段的分界点,我们就叫“慢启动门限(ssthresh)”。
\\n随着窗口进一步缓慢增加,终于有一天,网络还是遇到了丢包的情况,我们就会假定这是拥塞造成的。
\\n这个时候我们一方面会进行超时重传或者快速重传,另一方面也会把窗口调整到更小的范围。
\\n我们会先把拥塞窗口变成原来的一半,ssthresh 也就设置成当前的窗口大小,然后开始执行拥塞避免算法。有些实现也会把拥塞窗口直接设置为 ssthresh+3,本质上区别不大。
\\n总体而言,TCP 就是通过滑动窗口、拥塞窗口这两个简单的窗口实现了流量控制和拥塞控制。
\\n滑动窗口由接收端控制,向发送端通告,这样就可以保证发送端发出的包数量上限是明确的,也就不会存在淹没接收端导致来不及处理的情况。
\\n拥塞窗口由发送端控制,它会根据网络中的情况动态的调整,通过慢启动、拥塞避免、拥塞发生、快速恢复四个算法,很好地调整窗口的大小。和滑动窗口一起限制了发送端最大的发送范围,从而保证了拥塞在网络上不会发生。
","description":"TCP 协议 TCP(传输控制协议)和UDP(用户数据报协议)都是互联网协议族中的传输层协议,它们在数据传输的可靠性、效率、连接方式等方面存在差异,适用于不同的应用场景。\\n\\n连接方式:TCP是面向连接的协议。在数据传输之前,需要在发送端和接收端之间建立一条连接,就像打电话一样,需要先拨通对方号码,建立起连接后才能进行通话。\\n\\n可靠性:具有高度的可靠性。它通过确认机制、重传机制来确保数据的准确传输。例如,发送端发送数据后,接收端会返回确认信息,如果发送端没有收到确认,就会重新发送数据。同时,TCP还能对数据进行排序,保证接收端按正确的顺序接收数据。\\n\\n流量控制与…","guid":"https://juejin.cn/post/7504943335841939475","author":"庄周梦了个蝶","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-18T04:24:52.438Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/96ddc8ecb43f4b059ccd5449d8cfe436~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bqE5ZGo5qKm5LqG5Liq6J22:q75.awebp?rk3s=f64ab15b&x-expires=1748147092&x-signature=DF%2Bj2tVaaWXLK4A0AxCedK2PwN0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d1e276f2236e4b279b3b0a9b09138075~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bqE5ZGo5qKm5LqG5Liq6J22:q75.awebp?rk3s=f64ab15b&x-expires=1748147092&x-signature=ZGug9DNrr91HYGMrGhn1BxQWtcM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/88fd78ae90ac4402871dd52157287b0c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bqE5ZGo5qKm5LqG5Liq6J22:q75.awebp?rk3s=f64ab15b&x-expires=1748147092&x-signature=ra19s%2BaPGrnznXdhp%2FjVFnCGTWU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7a030fcb2dda43309e7be39bb4304491~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bqE5ZGo5qKm5LqG5Liq6J22:q75.awebp?rk3s=f64ab15b&x-expires=1748147092&x-signature=H23EdX06Jwsw614QxDcMwshZD2Q%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/10b3127593794490b8be25fbd19c8bf7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bqE5ZGo5qKm5LqG5Liq6J22:q75.awebp?rk3s=f64ab15b&x-expires=1748147092&x-signature=noQrnG5adUnsMdFDIhXSRGvGk%2Bw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0772fa0763cd49b99727d150b96b6a05~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bqE5ZGo5qKm5LqG5Liq6J22:q75.awebp?rk3s=f64ab15b&x-expires=1748147092&x-signature=B%2Fokq%2BIH4iNDW4U0JOgCWR4LVA4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fa16359b98bb479a96dd91b4927aaddc~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bqE5ZGo5qKm5LqG5Liq6J22:q75.awebp?rk3s=f64ab15b&x-expires=1748147092&x-signature=P9gjuOugpOgALBBhszuLNBdENTg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/af4cd5fd6f5a446a871315dcc5772e88~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bqE5ZGo5qKm5LqG5Liq6J22:q75.awebp?rk3s=f64ab15b&x-expires=1748147092&x-signature=FZ3M2ad4L4OSxGapnad2GcKhz7w%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/57a8d07ed08b4aac9e545cca2e93f724~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bqE5ZGo5qKm5LqG5Liq6J22:q75.awebp?rk3s=f64ab15b&x-expires=1748147092&x-signature=DrnWzuheKZ%2FV7o2OUsFGcA06Oy0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b670afe8ea524cb3b87594dcd09c6c9b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bqE5ZGo5qKm5LqG5Liq6J22:q75.awebp?rk3s=f64ab15b&x-expires=1748147092&x-signature=uch5SRUyP%2FeK52AuKeqMm7nMKz8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b6d2420d1f434b5f9e20661345209793~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bqE5ZGo5qKm5LqG5Liq6J22:q75.awebp?rk3s=f64ab15b&x-expires=1748147092&x-signature=5%2FcLeTGJOul42pCNx5Q2E8EGAtQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/31addfdabb084d58a7cc60047281b4a1~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bqE5ZGo5qKm5LqG5Liq6J22:q75.awebp?rk3s=f64ab15b&x-expires=1748147092&x-signature=WqtGpEOHDPYuNY57LF7EVhXQUOI%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["代码人生","网络协议","Flutter","算法"],"attachments":null,"extra":null,"language":null},{"title":"一箭双雕 —— Flutter Channel 双通道调用实战","url":"https://juejin.cn/post/7505042954198319141","content":"Dart 层与原生层之间既有 “点对点” 的方法调用(MethodChannel),也有 “流式推送” 的事件订阅(EventChannel)。两者在使用场景、调用方式和数据流向上存在明显差异,本文将梳理二者差异及对应适用场景。
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n特性 | MethodChannel | EventChannel |
---|---|---|
通信模式 | 同步/异步请求-响应 | 单向持续推送 |
典型场景 | 主动查询(如获取当前电量)、命令下发 | 被动监听(如电池状态变化事件) |
调用方式 | invokeMethod(…) → Future<T> | receiveBroadcastStream() → Stream<T> |
原生实现 | 调用 MethodChannel.setMethodCallHandler | 调用 EventChannel.setStreamHandler |
性能特点 | 每次请求都会附带方法名和参数,适用于低频交互 | 建立一次通道后可推送大量事件,适用于高频/实时数据 |
错误处理 | 通过 PlatformException (try/catch)捕获 | 通过 Stream.listen(onError: …) 处理流中错误 |
生命周期管理 | 每次调用独立,Dart 侧发起后即完成 | 通道打开后需手动取消订阅:subscription.cancel() |
//Flutter 层
\\nFuture<int?> getBatteryLevel() async {\\n try {\\n final int level = await _methodChannel.invokeMethod(\'getBatteryLevel\');\\n return level;\\n } on PlatformException catch (e) {\\n debugPrint(\'MethodChannel 错误:${e.message}\');\\n return null;\\n }\\n}\\n
\\n// Android 原生
\\nclass FlutterBatteryPlugin : FlutterPlugin {\\n override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {\\n when (call.method) {\\n \\"getBatteryLevel\\" -> {\\n val level = batteryMonitor.currentLevel()\\n result.success(level)\\n }\\n else -> result.notImplemented()\\n }\\n }\\n}\\n
\\n\\n\\n调用流程:
\\n\\n
\\n- Dart 调用
\\ninvokeMethod(\\"getBatteryLevel\\")
;- 平台通道将方法名传给原生;
\\n- 原生执行并通过
\\nresult.success(level)
回传;- Dart
\\nFuture
完成并拿到值。
graph TD\\n subgraph MethodChannel\\n A1[\\"Flutter: invokeMethod(\'method\')\\"] --\x3e B1[\\"编码为 MethodCall\\"]\\n B1 --\x3e C1[\\"Native: onMethodCall 处理\\"]\\n C1 --\x3e D1[\\"Native: result.success(data)\\"]\\n D1 --\x3e E1[\\"Flutter: Future 接收结果\\"]\\n end\\n\\n\\n\\n style MethodChannel fill:#f9999,stroke:#333\\n
\\n// Flutter 层
\\nStreamSubscription<Map<String, dynamic>>? _batterySub;\\n\\nvoid startBatteryMonitoring() {\\n _batterySub = _eventChannel\\n .receiveBroadcastStream()\\n .cast<Map<String, dynamic>>()\\n .listen(\\n (data) => debugPrint(\'电池数据:$data\'),\\n onError: (e) => debugPrint(\'EventChannel 错误:$e\'),\\n );\\n}\\n\\nvoid stopBatteryMonitoring() {\\n _batterySub?.cancel();\\n}\\n
\\n// Android 原生
\\nclass FlutterBatteryStreamHandler : EventChannel.StreamHandler {\\n private var eventSink: EventChannel.EventSink? = null\\n\\n override fun onListen(args: Any?, sink: EventChannel.EventSink) {\\n eventSink = sink\\n batteryMonitor.setOnBatteryLevelChangeCallback { level ->\\n eventSink?.success(mapOf(\\"batteryLevel\\" to level))\\n }\\n }\\n\\n override fun onCancel(args: Any?) {\\n batteryMonitor.clearCallback()\\n eventSink = null\\n }\\n}\\n
\\n\\n\\n调用流程:
\\n\\n
\\n- Dart 调用
\\nreceiveBroadcastStream()
并监听;- 原生
\\nonListen
注册后向 Dart 推送事件流;- 每当电量变化,原生调用
\\neventSink.success(data)
;- Dart 的
\\nStream
收到并分发给回调。
graph TD\\n subgraph EventChannel\\n A2[\\"Flutter: receiveBroadcastStream().listen()\\"] --\x3e B2[\\"Native: onListen 注册 EventSink\\"]\\n B2 --\x3e C2[\\"Native: eventSink.success(data) 多次推送\\"]\\n C2 --\x3e D2[\\"Flutter: Stream 持续监听事件\\"]\\n D2 --\x3e E2[\\"Native: eventSink.endOfStream() 终止\\"]\\n end\\n\\n style EventChannel fill:#9f9,stroke:#333\\n
\\n应用开发中,获取电池信息和监控电池状态通常需要编写原生代码并通过平台通道(Platform Channel)与Flutter 通信。下文将深入解析flutter_battery
Android 电池检测插件,对比实现主动读取及监听推送功能对于 channel 的使用场景;
flutter_battery
插件采用了标准的Flutter插件架构,主要包含以下几个部分:
定义平台接口(FlutterBatteryPlatform
),用于规范不同平台实现的API约定
abstract class FlutterBatteryPlatform extends PlatformInterface {\\n // 平台通用的API定义\\n Future<int?> getBatteryLevel();\\n Future<Map<String, dynamic>> getBatteryInfo();\\n // 更多API...\\n}\\n
\\nclass MethodChannelFlutterBattery extends FlutterBatteryPlatform {\\n final methodChannel = const MethodChannel(\'flutter_battery\');\\n final eventChannel = const EventChannel(\'flutter_battery/battery_stream\');\\n \\n // 实现平台接口中定义的各种方法\\n}\\n
\\ngraph TB\\n %% 样式定义\\n classDef flutter fill:#61DAFB,stroke:#333,stroke-width:1px,color:#333\\n classDef android fill:#3DDC84,stroke:#333,stroke-width:1px,color:#333\\n classDef methodChannel fill:#FFA726,stroke:#333,stroke-width:1px,color:#333\\n classDef core fill:#E57373,stroke:#333,stroke-width:1px,color:#333\\n \\n %% Flutter 应用层\\n FlutterApp[\\"Flutter 应用层\\"]:::flutter\\n \\n %% 主动查询模式\\n FlutterApp --\x3e|\\"1. getBatteryLevel()\\"| FlutterBattery[\\"FlutterBattery 类\\"]:::flutter\\n FlutterBattery --\x3e|\\"2. getBatteryLevel()\\"| PlatformInterface[\\"FlutterBatteryPlatform\\"]:::flutter\\n PlatformInterface --\x3e|\\"3. invokeMethod(\'getBatteryLevel\')\\"| MethodChannel[\\"MethodChannel\\"]:::methodChannel\\n MethodChannel --\x3e|\\"4. 通过 JNI 调用\\"| MethodHandler[\\"MethodChannelHandler\\"]:::android\\n MethodHandler --\x3e|\\"5. getBatteryLevel()\\"| BatteryMonitor[\\"BatteryMonitor\\"]:::core\\n BatteryMonitor --\x3e|\\"6. getIntProperty(BATTERY_PROPERTY_CAPACITY)\\"| AndroidBatteryManager[\\"Android BatteryManager\\"]:::android\\n AndroidBatteryManager --\x3e|\\"7. 返回电池电量\\"| BatteryMonitor\\n BatteryMonitor --\x3e|\\"8. 返回电池电量\\"| MethodHandler\\n MethodHandler --\x3e|\\"9. 返回结果\\"| MethodChannel\\n MethodChannel --\x3e|\\"10. 返回结果\\"| PlatformInterface\\n PlatformInterface --\x3e|\\"11. 返回结果\\"| FlutterBattery\\n FlutterBattery --\x3e|\\"12. 返回结果\\"| FlutterApp\\n
\\ngraph TB\\n %% 样式定义\\n classDef flutter fill:#61DAFB,stroke:#333,stroke-width:1px,color:#333\\n classDef android fill:#3DDC84,stroke:#333,stroke-width:1px,color:#333\\n classDef eventChannel fill:#66BB6A,stroke:#333,stroke-width:1px,color:#333\\n classDef core fill:#E57373,stroke:#333,stroke-width:1px,color:#333\\n \\n %% Flutter 应用层\\n FlutterApp[\\"Flutter 应用层\\"]:::flutter\\n FlutterBatteryStream[\\"FlutterBattery.batteryInfoStream\\"]:::flutter\\n EventChannel[\\"EventChannel\\"]:::eventChannel\\n EventHandler[\\"EventChannelHandler\\"]:::android\\n TimerManager1[\\"TimerManager\\"]:::core\\n BatteryMonitor[\\"BatteryMonitor\\"]:::core\\n AndroidBatteryManager[\\"Android BatteryManager\\"]:::android\\n \\n %% 调用链\\n FlutterApp --\x3e|\\"1. batteryInfoStream.listen()\\"| FlutterBatteryStream\\n FlutterBatteryStream --\x3e|\\"2. eventChannel.receiveBroadcastStream()\\"| EventChannel\\n EventChannel --\x3e|\\"3. onListen()\\"| EventHandler\\n EventHandler --\x3e|\\"4. 启动定时器 timerManager.start()\\"| TimerManager1\\n TimerManager1 --\x3e|\\"5. 定时执行 pushBatteryInfo()\\"| EventHandler\\n EventHandler --\x3e|\\"6. getBatteryLevel()\\"| BatteryMonitor\\n BatteryMonitor --\x3e|\\"7. 获取电池信息\\"| AndroidBatteryManager\\n AndroidBatteryManager --\x3e|\\"8. 返回电池信息\\"| BatteryMonitor\\n BatteryMonitor --\x3e|\\"9. 返回电池信息\\"| EventHandler\\n EventHandler --\x3e|\\"10. eventSink.success(batteryInfo)\\"| EventChannel\\n EventChannel --\x3e|\\"11. 推送数据到 Stream\\"| FlutterBatteryStream\\n FlutterBatteryStream --\x3e|\\"12. 触发 listener 回调\\"| FlutterApp\\n
\\ngraph TB\\n %% 样式定义\\n classDef flutter fill:#61DAFB,stroke:#333,stroke-width:1px,color:#333\\n classDef android fill:#3DDC84,stroke:#333,stroke-width:1px,color:#333\\n classDef eventChannel fill:#66BB6A,stroke:#333,stroke-width:1px,color:#333\\n classDef methodChannel fill:#FFA726,stroke:#333,stroke-width:1px,color:#333\\n classDef core fill:#E57373,stroke:#333,stroke-width:1px,color:#333\\n \\n %% Flutter 应用层\\n FlutterApp[\\"Flutter 应用层\\"]:::flutter\\n \\n %% EventChannel 推送\\n subgraph EventChannel推送方式\\n FlutterApp --\x3e|\\"1. batteryInfoStream.listen()\\"| FlutterBatteryStream[\\"FlutterBattery.batteryInfoStream\\"]:::flutter\\n FlutterBatteryStream --\x3e|\\"2. eventChannel.receiveBroadcastStream()\\"| EventChannel[\\"EventChannel\\"]:::eventChannel\\n EventChannel --\x3e|\\"3. onListen()\\"| EventHandler[\\"EventChannelHandler\\"]:::android\\n EventHandler --\x3e|\\"4. 启动定时器 timerManager.start()\\"| TimerManager1[\\"TimerManager\\"]:::core\\n TimerManager1 --\x3e|\\"5. 定时执行 pushBatteryInfo()\\"| EventHandler\\n EventHandler --\x3e|\\"6. getBatteryLevel()\\"| BatteryMonitor[\\"BatteryMonitor\\"]:::core\\n BatteryMonitor --\x3e|\\"7. 获取电池信息\\"| AndroidBatteryManager[\\"Android BatteryManager\\"]:::android\\n AndroidBatteryManager --\x3e|\\"8. 返回电池信息\\"| BatteryMonitor\\n BatteryMonitor --\x3e|\\"9. 返回电池信息\\"| EventHandler\\n EventHandler --\x3e|\\"10. eventSink.success(batteryInfo)\\"| EventChannel\\n EventChannel --\x3e|\\"11. 推送数据到 Stream\\"| FlutterBatteryStream\\n FlutterBatteryStream --\x3e|\\"12. 触发 listener 回调\\"| FlutterApp\\n end\\n \\n %% 广播接收器推送\\n subgraph 广播接收器推送方式\\n AndroidBroadcast[\\"Android 电池广播\\"]:::android --\x3e|\\"1. ACTION_BATTERY_CHANGED\\"| BatteryReceiver[\\"电池广播接收器\\"]:::android\\n BatteryReceiver --\x3e|\\"2. onReceive()\\"| BatteryMonitor\\n BatteryMonitor --\x3e|\\"3. 更新 lastBatteryLevel\\"| BatteryMonitor\\n BatteryMonitor --\x3e|\\"4. 启动定时器 batteryLevelPushTimer\\"| TimerManager2[\\"TimerManager\\"]:::core\\n TimerManager2 --\x3e|\\"5. 定时执行 pushBatteryLevel()\\"| BatteryMonitor\\n BatteryMonitor --\x3e|\\"6. 调用回调 onBatteryLevelChangeCallback\\"| MethodHandler[\\"MethodChannelHandler\\"]:::android\\n MethodHandler --\x3e|\\"7. invokeMethod(\'onBatteryLevelChanged\')\\"| MethodChannel[\\"MethodChannel\\"]:::methodChannel\\n MethodChannel --\x3e|\\"8. _handleMethodCall()\\"| FlutterMethodChannel[\\"MethodChannelFlutterBattery\\"]:::flutter\\n FlutterMethodChannel --\x3e|\\"9. _batteryLevelChangeCallback()\\"| FlutterBattery[\\"FlutterBattery\\"]:::flutter\\n FlutterBattery --\x3e|\\"10. 触发回调\\"| FlutterApp\\n end\\n
\\n在Android平台上,插件实现了完整的电池监控功能:
\\nclass FlutterBatteryPlugin : FlutterPlugin, ActivityAware {\\n // 插件初始化和资源管理\\n private lateinit var batteryMonitor: BatteryMonitor\\n private lateinit var notificationHelper: NotificationHelper\\n // 更多组件...\\n}\\n
\\n这个插件的通信机制设计非常精巧,采用了双通道策略:
\\n这种设计使得插件可以同时支持:
\\n插件在Dart层定义了多种回调接口:
\\nvoid configureBatteryCallbacks({\\n Function(int batteryLevel)? onLowBattery,\\n Function(int batteryLevel)? onBatteryLevelChange,\\n Function(Map<String, dynamic> batteryInfo)? onBatteryInfoChange,\\n})\\n
\\n这些回调通过MethodChannel
的反向调用实现,当原生层检测到电池状态变化时,会通过invokeMethod
向Flutter层发送事件:
batteryMonitor.setOnBatteryLevelChangeCallback { batteryLevel ->\\n val params = HashMap<String, Any>()\\n params[\\"batteryLevel\\"] = batteryLevel\\n channel.invokeMethod(\\"onBatteryLevelChanged\\", params)\\n}\\n
\\n对于需要连续监听的数据,插件还提供了基于Stream
的API:
Stream<Map<String, dynamic>> get batteryStream {\\n return eventChannel.receiveBroadcastStream().map((dynamic event) {\\n // 将原生数据转换为Dart类型\\n final Map<dynamic, dynamic> map = event as Map<dynamic, dynamic>;\\n return map.cast<String, dynamic>();\\n });\\n}\\n
\\n这使得开发者可以使用响应式编程的方式处理电池数据:
\\nflutterBatteryPlugin.batteryStream.listen((batteryData) {\\n // 处理电池数据更新\\n});\\n
\\n插件通过Android的BroadcastReceiver
机制监听系统电池事件:
private fun registerBatteryReceiver() {\\n batteryReceiver = object : BroadcastReceiver() {\\n override fun onReceive(context: Context, intent: Intent) {\\n when (intent.action) {\\n Intent.ACTION_BATTERY_CHANGED -> {\\n val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)\\n val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)\\n if (level != -1 && scale != -1) {\\n val batteryPct = (level * 100 / scale.toFloat()).toInt()\\n lastBatteryLevel = batteryPct\\n onBatteryLevelChangeCallback?.invoke(batteryPct)\\n }\\n }\\n }\\n }\\n }\\n \\n val filter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)\\n context.registerReceiver(batteryReceiver, filter)\\n}\\n
\\n这种实现能够捕获所有电池状态变化,无需轮询,提高了效率和实时性。
\\n使用自定义的TimerManager
类管理各种定时任务,如定期检查电池状态:将定时逻辑与业务逻辑分离,提高代码可维护性。
private val batteryLevelPushTimer = TimerManager() // 电池电量推送定时器\\nprivate val batteryCheckTimer = TimerManager() // 低电量检查定时器\\nprivate val batteryInfoPushTimer = TimerManager() // 电池信息推送定时器\\n\\n// 配置定时器任务\\nbatteryCheckTimer.setTask {\\n checkLowBattery()\\n}\\n
\\n为了避免频繁更新造成的性能问题,增加防抖动机制:
\\nprivate fun pushBatteryLevel() {\\n val currentLevel = lastBatteryLevel\\n if (currentLevel >= 0) {\\n if (!enableBatteryLevelDebounce || lastBatteryLevel != currentLevel) {\\n onBatteryLevelChangeCallback?.invoke(currentLevel)\\n }\\n }\\n}\\n
\\n这确保只有在电池电量实际变化时才会触发事件,减少了不必要的通信开销。
\\nMethodChannel
\\nEventChannel
\\n适用于「持续」且「频繁推送」的场景。
\\n适用于电池电量、电量变化、网络状态变化、传感器数据等需要实时监听的功能。
\\n插件地址: lizy-coding/flutter_battery: Flutter Android 推送通知和电池监控插件,支持立即通知、延迟通知和低电量监控。
","description":"一、EventChannel 与 MethodChannel 差异对比 Dart 层与原生层之间既有 “点对点” 的方法调用(MethodChannel),也有 “流式推送” 的事件订阅(EventChannel)。两者在使用场景、调用方式和数据流向上存在明显差异,本文将梳理二者差异及对应适用场景。\\n\\n特性\\tMethodChannel\\tEventChannel通信模式\\t同步/异步请求-响应\\t单向持续推送\\n典型场景\\t主动查询(如获取当前电量)、命令下发\\t被动监听(如电池状态变化事件)…","guid":"https://juejin.cn/post/7505042954198319141","author":"人形打码机","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-18T02:45:16.016Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/93728b5299d74f03892fff3405744bb1~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Lq65b2i5omT56CB5py6:q75.awebp?rk3s=f64ab15b&x-expires=1748141287&x-signature=dPomH7gr5M%2FH4V2OptQURMNA3mc%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Dart","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 中的 SOLID 原则实用指南:S - 单一职责原则(SRP)篇","url":"https://juejin.cn/post/7504910800798466089","content":"在软件设计中,SOLID 是五大面向对象设计原则的缩写,其中的 \\"S\\" 代表 单一职责原则(Single Responsibility Principle,SRP)。在实际 Flutter 开发中,如何落地 SRP,往往是新手迈向架构进阶的第一步。
\\n本文将通过违反与遵循 SRP 的真实案例,逐步优化,并结合 Riverpod,展示如何在实际项目中应用 SRP 原则。
\\n单一职责原则:一个类应该仅有一个引起它变化的原因。
\\n换句话说,一个类只负责一个功能,如果该功能发生变化,只应影响这个类。
\\nclass LoginPage extends StatelessWidget {\\n final emailController = TextEditingController();\\n final passwordController = TextEditingController();\\n\\n Future<void> _login(BuildContext context) async {\\n final email = emailController.text;\\n final password = passwordController.text;\\n\\n if (email.isEmpty || !email.contains(\'@\')) {\\n ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(\'邮箱格式错误\')));\\n return;\\n }\\n\\n if (password.length < 6) {\\n ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(\'密码至少6位\')));\\n return;\\n }\\n\\n final response = await http.post(\\n Uri.parse(\'https://api.example.com/login\'),\\n body: {\'email\': email, \'password\': password},\\n );\\n\\n if (response.statusCode == 200) {\\n Navigator.pushReplacementNamed(context, \'/home\');\\n } else {\\n ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(\'登录失败\')));\\n }\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Column(\\n children: [\\n TextField(controller: emailController),\\n TextField(controller: passwordController),\\n ElevatedButton(onPressed: () => _login(context), child: Text(\'登录\')),\\n ],\\n );\\n }\\n}\\n
\\n我们首先把验证与网络逻辑从 UI 中拆分出去。
\\nclass LoginValidator {\\n String? validate(String email, String password) {\\n if (email.isEmpty || !email.contains(\'@\')) return \'邮箱格式错误\';\\n if (password.length < 6) return \'密码至少6位\';\\n return null;\\n }\\n}\\n
\\nclass AuthService {\\n Future<bool> login(String email, String password) async {\\n final response = await http.post(\\n Uri.parse(\'https://api.example.com/login\'),\\n body: {\'email\': email, \'password\': password},\\n );\\n return response.statusCode == 200;\\n }\\n}\\n
\\nclass LoginPage extends StatefulWidget {\\n @override\\n State<LoginPage> createState() => _LoginPageState();\\n}\\n\\nclass _LoginPageState extends State<LoginPage> {\\n final emailController = TextEditingController();\\n final passwordController = TextEditingController();\\n final validator = LoginValidator();\\n final authService = AuthService();\\n bool isLoading = false;\\n\\n Future<void> _login() async {\\n final error = validator.validate(\\n emailController.text,\\n passwordController.text,\\n );\\n if (error != null) {\\n ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(error)));\\n return;\\n }\\n\\n setState(() => isLoading = true);\\n final success = await authService.login(\\n emailController.text,\\n passwordController.text,\\n );\\n setState(() => isLoading = false);\\n\\n if (success) {\\n Navigator.pushReplacementNamed(context, \'/home\');\\n } else {\\n ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(\'登录失败\')));\\n }\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Column(\\n children: [\\n TextField(controller: emailController),\\n TextField(controller: passwordController, obscureText: true),\\n ElevatedButton(\\n onPressed: isLoading ? null : _login,\\n child: isLoading ? CircularProgressIndicator() : Text(\'登录\'),\\n ),\\n ],\\n );\\n }\\n}\\n
\\n我们将项目结构优化如下:
\\nlib/\\n├── features/login/\\n│ ├── presentation/ // UI 层\\n│ ├── application/ // 状态/控制器层\\n│ ├── domain/ // 验证/模型层\\n│ └── infrastructure/ // 网络服务层\\n
\\nsealed class LoginResult {\\n const LoginResult();\\n}\\n\\nclass LoginSuccess extends LoginResult {\\n final String token;\\n const LoginSuccess(this.token);\\n}\\n\\nclass LoginError extends LoginResult {\\n final String message;\\n const LoginError(this.message);\\n}\\n
\\nfinal loginControllerProvider =\\n StateNotifierProvider<LoginController, AsyncValue<LoginResult?>>(\\n (ref) => LoginController(\\n validator: LoginValidator(),\\n authService: AuthService(),\\n ),\\n);\\n\\nclass LoginController extends StateNotifier<AsyncValue<LoginResult?>> {\\n final LoginValidator validator;\\n final AuthService authService;\\n\\n LoginController({required this.validator, required this.authService})\\n : super(const AsyncValue.data(null));\\n\\n Future<void> login(String email, String password) async {\\n final error = validator.validate(email, password);\\n if (error != null) {\\n state = AsyncValue.data(LoginError(error));\\n return;\\n }\\n\\n state = const AsyncValue.loading();\\n try {\\n final token = await authService.login(email, password);\\n state = AsyncValue.data(LoginSuccess(token));\\n } catch (_) {\\n state = AsyncValue.data(LoginError(\'登录失败,请稍后再试\'));\\n }\\n }\\n}\\n
\\nclass LoginPage extends ConsumerStatefulWidget {\\n @override\\n ConsumerState<LoginPage> createState() => _LoginPageState();\\n}\\n\\nclass _LoginPageState extends ConsumerState<LoginPage> {\\n final emailController = TextEditingController();\\n final passwordController = TextEditingController();\\n\\n void _onLogin() {\\n ref.read(loginControllerProvider.notifier).login(\\n emailController.text,\\n passwordController.text,\\n );\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n final loginState = ref.watch(loginControllerProvider);\\n final result = loginState.value;\\n final isLoading = loginState.isLoading;\\n\\n return Column(\\n children: [\\n TextField(controller: emailController),\\n TextField(controller: passwordController, obscureText: true),\\n if (result is LoginError)\\n Text(result.message, style: TextStyle(color: Colors.red)),\\n ElevatedButton(\\n onPressed: isLoading ? null : _onLogin,\\n child: isLoading ? CircularProgressIndicator() : Text(\'登录\'),\\n ),\\n ],\\n );\\n }\\n}\\n
\\n优点 | 描述 |
---|---|
职责单一 | 每层专注于一个目标:验证、请求、UI、状态管理分别负责 |
可测试性高 | 各层可独立 mock 和单元测试 |
易维护与扩展 | 新增需求时不会牵一发动全身 |
状态响应式 | Riverpod 自动响应状态变化,简洁清晰 |
假设未来你需要扩展如下功能:
\\n🤔 你会将这些逻辑分别放在哪一层?
\\n在 Flutter 项目中,应用 SRP 原则能够极大地提升代码的可维护性和扩展性。我们先通过基本的逻辑拆分做出初步优化,再结合 Riverpod 实现响应式、职责清晰的架构。
\\n下一次,当你在一个 Widget 中写出 200 行处理表单、验证、网络、跳转的逻辑时,不妨停下来问问自己:
\\n\\n\\n\\"这个类是不是做太多事了?\\"
\\n
如果你觉得本文对你有帮助,欢迎分享、点赞支持,让更多开发者写出优雅、可维护的 Flutter 代码。
","description":"在软件设计中,SOLID 是五大面向对象设计原则的缩写,其中的 \\"S\\" 代表 单一职责原则(Single Responsibility Principle,SRP)。在实际 Flutter 开发中,如何落地 SRP,往往是新手迈向架构进阶的第一步。 本文将通过违反与遵循 SRP 的真实案例,逐步优化,并结合 Riverpod,展示如何在实际项目中应用 SRP 原则。\\n\\n什么是 SRP?\\n\\n单一职责原则:一个类应该仅有一个引起它变化的原因。\\n\\n换句话说,一个类只负责一个功能,如果该功能发生变化,只应影响这个类。\\n\\n🧨 违反 SRP 的示例\\nclass Login…","guid":"https://juejin.cn/post/7504910800798466089","author":"OldBirds","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-17T23:49:14.094Z","media":null,"categories":["Android","iOS","Flutter","设计模式"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 中 Isolate 的全面解析:高性能并发的利器","url":"https://juejin.cn/post/7504933816290951194","content":"在 Dart 和 Flutter 中,由于 Dart 是单线程模型(基于事件循环机制),当我们进行文件读取、大量数据处理或复杂计算任务时,如果直接在主线程执行,很可能会造成 UI 卡顿、掉帧甚至应用 ANR(应用无响应)问题。
\\n为了避免这种情况,Dart 提供了 Isolate
机制,支持将耗时操作放在另一个线程中执行,从而不阻塞主线程。本文将结合真实示例,对 Isolate 的概念、原理、用法及其在 Flutter 中的实践进行深入解析。
在 Dart 中,Isolate
是独立的执行单元,拥有自己独立的内存空间和事件循环,与其他 Isolate 之间不共享内存,而是通过消息通信机制进行数据传递。
这与传统语言中的多线程(如 Java 的 Thread
)不同,Dart 更类似于 Erlang 的并发模型。
Isolate 的主要特点:
\\nSendPort
和 ReceivePort
进行消息传递Dart 提供了以下几种方式创建 Isolate:
\\n这是最底层也是最常见的用法,适合构建自定义并发逻辑:
\\nawait Isolate.spawn(entryPoint, message);\\n
\\nentryPoint
是另一个 Isolate 的函数入口message
是发送给新 Isolate 的初始数据(建议使用 Map
携带多个字段)这是 Dart 提供的简洁 API,底层也是 Isolate.spawn
实现,更适合临时使用:
final result = await Isolate.run(() => heavyComputation());\\n
\\n以下是一个完整示例,展示了如何用 Isolate.spawn
将文件读取任务放入子 Isolate 中:
import \'dart:io\';\\nimport \'dart:isolate\';\\n\\nFuture<String> readFile(String filePath) async {\\n final file = File(filePath);\\n return await file.readAsString();\\n}\\n\\nFuture<void> fileReadIsolate(Map<String, dynamic> message) async {\\n final filePath = message[\'filePath\'] as String;\\n final sendPort = message[\'sendPort\'] as SendPort;\\n\\n try {\\n final content = await readFile(filePath);\\n sendPort.send({ \'success\': true, \'content\': content });\\n } catch (e) {\\n sendPort.send({ \'success\': false, \'error\': e.toString() });\\n }\\n}\\n\\nFuture<String> readFileWithIsolate(String filePath) async {\\n final receivePort = ReceivePort();\\n\\n await Isolate.spawn(fileReadIsolate, {\\n \'filePath\': filePath,\\n \'sendPort\': receivePort.sendPort,\\n });\\n\\n final result = await receivePort.first as Map<String, dynamic>;\\n receivePort.close();\\n\\n if (result[\'success\']) {\\n return result[\'content\'];\\n } else {\\n throw Exception(result[\'error\']);\\n }\\n}\\n\\nFuture<void> main() async {\\n const filePath = \\"README.md\\";\\n\\n try {\\n final content = await readFileWithIsolate(filePath);\\n print(\\"Content via Isolate.spawn():\\\\n$content\\");\\n\\n final content2 = await Isolate.run(() => readFile(filePath));\\n print(\\"Content via Isolate.run():\\\\n$content2\\");\\n } catch (e) {\\n print(\\"Error: $e\\");\\n }\\n}\\n
\\n输出示例:
\\nContent via Isolate.spawn():\\nHello from README.md!\\nContent via Isolate.run():\\nHello from README.md!\\n
\\n特性 | Isolate.spawn | Isolate.run |
---|---|---|
使用难度 | 较高(需要手动消息通信) | 简单(函数调用形式) |
传参方式 | 通过 Map 、SendPort /ReceivePort | 自动返回 Future<T> |
适合场景 | 长任务、多任务控制、手动调度 | 一次性调用、临时并发 |
Dart 版本支持 | 所有版本 | Dart 2.19+ |
在 Flutter 应用中,我们推荐以下几种场景使用 Isolate:
\\n此外,Flutter 官方还提供了一个更高层封装:compute()
函数,它底层也使用 Isolate:
Future<String> computeExample(String filePath) async {\\n return await compute(readFile, filePath);\\n}\\n
\\n但需要注意:
\\ncompute()
要求方法为顶层函数Dart 从设计上采用了“无共享内存的并发模型”,每个 Isolate 拥有独立的堆空间和事件循环。这种模型的优势是避免了多线程中的数据竞争和线程安全问题,从根本上杜绝了加锁、死锁等风险。
\\n因此,Isolate 之间必须通过消息机制(message passing)通信,本质上是数据的序列化和复制传输。
\\nsend()
方法向其他 Isolate 发送消息。sendPort
用于建立连接。通信流程:
\\nMain Isolate Spawned Isolate\\n │ │\\n │ ReceivePort + SendPort │\\n │──────────── sendPort ──────────▶│\\n │ │\\n │ message │\\n │◀──────────── send() ────────────│\\n
\\n可以通过多个 SendPort 构建 Isolate 链,实现并行流水线处理。例如:
\\nIsolate A <-> Isolate B <-> Isolate C\\n
\\n适用于图像处理、管道任务、并行计算等。
\\nvoid entryPoint(SendPort mainSendPort) {\\n final port = ReceivePort();\\n mainSendPort.send(port.sendPort);\\n\\n port.listen((message) {\\n if (message is Map) {\\n final data = message[\'data\'];\\n final replyTo = message[\'replyTo\'] as SendPort;\\n final result = \'Child got: $data\';\\n replyTo.send(result);\\n }\\n });\\n}\\n
\\n主 isolate:
\\nFuture<void> main() async {\\n final receivePort = ReceivePort();\\n await Isolate.spawn(entryPoint, receivePort.sendPort);\\n\\n final sendPort = await receivePort.first as SendPort;\\n\\n final responsePort = ReceivePort();\\n sendPort.send({\\n \'data\': \'Hello from Main\',\\n \'replyTo\': responsePort.sendPort,\\n });\\n\\n final response = await responsePort.first;\\n print(\'Main received: $response\');\\n}\\n
\\nisolate.kill(priority: Isolate.immediate)
主动释放ReceivePort
,避免资源泄露Isolate
是 Dart/Flutter 中强大的并发处理工具,它为我们提供了在非共享内存模式下实现高性能异步处理的能力。通过合理地将耗时任务从主线程中隔离,我们可以显著提升 Flutter 应用的流畅度和响应速度。
掌握 Isolate.spawn
与 Isolate.run
的使用方式,并理解它们的差异,是构建高质量 Flutter 应用的基础。
在 Flutter 开发中,Key
是一个经常被提及但又容易被忽视的概念。它在 Widget 树的更新和状态管理中扮演着至关重要的角色。本文将详细介绍 Flutter 中 Key 的作用、类型、常见使用场景以及最佳实践。
在 Flutter 中,Key
用于标识 Widget、Element 和 State。它的主要作用是帮助 Flutter 框架在 Widget 树发生变化时正确地匹配和复用组件,确保状态的正确传递和维护。
常见场景:
\\nFlutter 提供了几种常用的 Key 类型:
\\nValueKey
通过一个值(如字符串、数字等)来唯一标识 Widget。
ListView(\\n children: [\\n ListTile(key: ValueKey(\'item_1\'), title: Text(\'Item 1\')),\\n ListTile(key: ValueKey(\'item_2\'), title: Text(\'Item 2\')),\\n ],\\n)\\n
\\nObjectKey
通过对象的引用来标识 Widget,适用于对象实例唯一的场景。
class Person {\\n final String name;\\n Person(this.name);\\n}\\n\\nListView(\\n children: people.map((person) => ListTile(\\n key: ObjectKey(person),\\n title: Text(person.name),\\n )).toList(),\\n)\\n
\\nUniqueKey
每次创建都是唯一的,常用于临时需要唯一标识的场景。
Container(\\n key: UniqueKey(),\\n child: Text(\'I am unique!\'),\\n)\\n
\\nGlobalKey
可以在整个应用中唯一标识一个 Widget,常用于跨 Widget 树访问 State 或操作。
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();\\n\\nForm(\\n key: _formKey,\\n child: // ...\\n)\\n
\\n当列表项可以被拖拽或重排序时,使用 Key 可以确保每个列表项的状态不会混乱。
\\nReorderableListView(\\n children: items.map((item) => ListTile(\\n key: ValueKey(item.id),\\n title: Text(item.title),\\n )).toList(),\\n onReorder: (oldIndex, newIndex) {\\n // 处理重排序逻辑\\n },\\n)\\n
\\n当 Widget 的顺序或父级发生变化时,Key 可以帮助 Flutter 正确地复用 State。
\\nRow(\\n children: [\\n MyWidget(key: ValueKey(\'A\')),\\n MyWidget(key: ValueKey(\'B\')),\\n ],\\n)\\n
\\n如果没有 Key,交换顺序后,状态可能会错位。
\\n使用 GlobalKey
可以在父 Widget 之外访问子 Widget 的 State。
final GlobalKey<_MyWidgetState> myWidgetKey = GlobalKey();\\n\\nMyWidget(key: myWidgetKey);\\n\\n// 在其他地方访问\\nmyWidgetKey.currentState?.doSomething();\\n
\\nKey
是 Flutter 框架中用于标识和管理 Widget 状态的重要工具。合理使用 Key 可以避免许多难以发现的 bug,提升应用的健壮性和可维护性。希望本文能帮助你更好地理解和使用 Flutter 中的 Key。
在 Flutter 中,响应式编程是一种重要的设计理念,而 Stream 是实现异步数据流和响应式UI更新的关键工具。本文将深入剖析 Flutter 中的 Stream,带你了解它的本质、单播和多播的区别,以及如何使用 StreamBuilder 和 StreamController 来构建流式响应式应用。
\\nStream 是 Dart 语言提供的一种异步数据序列,可以理解为数据的“时间轴”,它会随着时间推移不断发出数据事件。Flutter 和 Dart 生态中,Stream 广泛用于处理网络请求、用户输入、事件监听等场景。
\\nStream 中的数据是按顺序依次到达的,这使得我们可以优雅地处理异步数据流。
\\n单播流只能有一个监听者(listener),也就是说,只能被一个订阅者订阅:
\\nStream<int> singleStream = Stream.fromIterable([1, 2, 3]);\\n\\nsingleStream.listen((value) {\\n print(\'Listener 1: $value\');\\n});\\n\\n// 如果再添加另一个监听,会报错\\n// singleStream.listen((value) => print(\'Listener 2: $value\')); // 报错!\\n
\\n多播流允许多个监听者同时订阅。它更适合广播事件,比如 UI 事件、传感器数据等。
\\n通过调用 asBroadcastStream(),可以将单播流转换成多播流:
\\nStream<int> broadcastStream = Stream.fromIterable([1, 2, 3]).asBroadcastStream();\\n\\nbroadcastStream.listen((value) {\\n print(\'Listener 1: $value\');\\n});\\n\\nbroadcastStream.listen((value) {\\n print(\'Listener 2: $value\');\\n});\\n
\\n或者直接创建多播流:
\\nStreamController<int> controller = StreamController<int>.broadcast();\\n
\\nStreamController 是用来创建和控制 Stream 的核心类,它允许我们手动添加数据、错误或关闭事件到流中。它是连接数据生产者和消费者的桥梁。
\\nfinal controller = StreamController<int>();\\n\\n// 监听数据\\ncontroller.stream.listen((data) {\\n print(\'Received: $data\');\\n});\\n\\n// 添加数据\\ncontroller.sink.add(1);\\ncontroller.sink.add(2);\\n\\n// 关闭流\\ncontroller.close();\\n
\\n如果希望流支持多个监听者,需创建广播类型的控制器:
\\nfinal broadcastController = StreamController<int>.broadcast();\\n\\nbroadcastController.stream.listen((data) => print(\'Listener 1: $data\'));\\nbroadcastController.stream.listen((data) => print(\'Listener 2: $data\'));\\n\\nbroadcastController.sink.add(10);\\nbroadcastController.sink.add(20);\\n\\nbroadcastController.close();\\n
\\nStreamBuilder 是 Flutter 框架中专门用来监听 Stream 并根据流数据动态构建 UI 的 Widget。它使得响应式编程变得简单直观。
\\nStream<int> numberStream = Stream.periodic(Duration(seconds: 1), (count) => count).take(10);\\n\\nStreamBuilder<int>(\\n stream: numberStream,\\n builder: (context, snapshot) {\\n if (snapshot.connectionState == ConnectionState.waiting) {\\n return CircularProgressIndicator();\\n } else if (snapshot.hasError) {\\n return Text(\'Error: ${snapshot.error}\');\\n } else if (snapshot.hasData) {\\n return Text(\'Current number: ${snapshot.data}\');\\n } else {\\n return Text(\'Stream ended\');\\n }\\n },\\n)\\n
\\n组件 | 功能 | 特点和注意点 |
---|---|---|
Stream | 异步数据流 | 单播(默认)、多播(广播) |
StreamController | 手动创建和管理流 | 支持广播类型,添加数据、错误,控制流的生命周期 |
StreamBuilder | 响应流数据,构建UI | 绑定流和UI,自动监听流变化,简化响应式UI开发 |
Flutter 中的 Stream 机制为异步数据处理和响应式编程提供了强大的支持,理解并灵活使用单播、多播流,结合 StreamController 和 StreamBuilder,可以极大地提升你的应用性能和用户体验。
","description":"在 Flutter 中,响应式编程是一种重要的设计理念,而 Stream 是实现异步数据流和响应式UI更新的关键工具。本文将深入剖析 Flutter 中的 Stream,带你了解它的本质、单播和多播的区别,以及如何使用 StreamBuilder 和 StreamController 来构建流式响应式应用。 什么是 Stream?\\n\\nStream 是 Dart 语言提供的一种异步数据序列,可以理解为数据的“时间轴”,它会随着时间推移不断发出数据事件。Flutter 和 Dart 生态中,Stream 广泛用于处理网络请求、用户输入、事件监听等场景。\\n\\nSt…","guid":"https://juejin.cn/post/7504833795763372072","author":"MaoJiu","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-16T13:54:12.404Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/16f27f21ca904cf29530b86a74389060~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTWFvSml1:q75.awebp?rk3s=f64ab15b&x-expires=1748008452&x-signature=aslaE5RvZSzJ1ZIUr%2FRdzrC7MYU%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter中的Row和Column组件本质","url":"https://juejin.cn/post/7504871512999313423","content":"在 Flutter 中,布局控件是构建界面结构的基石。而 Row 和 Column 是两种最常用的布局组件,它们分别用于水平和垂直方向的线性布局。理解它们的本质,对掌握 Flutter 布局机制至关重要。
\\n这两个组件都是继承自 Flutter 布局的基础控件——Flex,并通过固定其方向属性实现不同的布局行为:
\\nclass Row extends Flex {\\n /// Creates a horizontal array of children.\\n ///\\n /// If [crossAxisAlignment] is [CrossAxisAlignment.baseline], then\\n /// [textBaseline] must not be null.\\n ///\\n /// The [textDirection] argument defaults to the ambient [Directionality], if\\n /// any. If there is no ambient directionality, and a text direction is going\\n /// to be necessary to determine the layout order (which is always the case\\n /// unless the row has no children or only one child) or to disambiguate\\n /// `start` or `end` values for the [mainAxisAlignment], the [textDirection]\\n /// must not be null.\\n const Row({\\n super.key,\\n super.mainAxisAlignment,\\n super.mainAxisSize,\\n super.crossAxisAlignment,\\n super.textDirection,\\n super.verticalDirection,\\n super.textBaseline, // NO DEFAULT: we don\'t know what the text\'s baseline should be\\n super.spacing,\\n super.children,\\n }) : super(direction: Axis.horizontal);\\n}\\n\\n\\nclass Column extends Flex {\\n /// Creates a vertical array of children.\\n ///\\n /// If [crossAxisAlignment] is [CrossAxisAlignment.baseline], then\\n /// [textBaseline] must not be null.\\n ///\\n /// The [textDirection] argument defaults to the ambient [Directionality], if\\n /// any. If there is no ambient directionality, and a text direction is going\\n /// to be necessary to disambiguate `start` or `end` values for the\\n /// [crossAxisAlignment], the [textDirection] must not be null.\\n const Column({\\n super.key,\\n super.mainAxisAlignment,\\n super.mainAxisSize,\\n super.crossAxisAlignment,\\n super.textDirection,\\n super.verticalDirection,\\n super.textBaseline,\\n super.spacing,\\n super.children,\\n }) : super(direction: Axis.vertical);\\n}\\n
\\n根据Row和Column组件的源码可以看到,Row 固定了 direction 为水平方向 Axis.horizontal,Column 则固定为垂直方向 Axis.vertical。所以,它们本质都是 Flex 的特殊实现。
\\nFlex 是一个灵活的布局容器,它根据 direction 属性来决定子组件的排列方向。它的设计思想是:
\\nFlex 允许我们指定主轴和交叉轴的对齐方式(mainAxisAlignment 和 crossAxisAlignment),还能控制组件的尺寸行为(如 mainAxisSize)。
\\nRow 和 Column 就是用不同方向的 Flex 来简化布局的使用。
\\n当 crossAxisAlignment 设置为 CrossAxisAlignment.baseline 时,textBaseline 参数必须指定,否则会抛异常。这是为了确保文字能够对齐。
\\n以 Column 为例,布局步骤大致如下:
\\nRow 也是类似流程,只是方向变成水平方向。
\\n这就是 Flutter 中 Row 和 Column 的本质解析。掌握它们,你的布局设计会更加灵活、精准,也更符合 Flutter 的高性能理念。
","description":"在 Flutter 中,布局控件是构建界面结构的基石。而 Row 和 Column 是两种最常用的布局组件,它们分别用于水平和垂直方向的线性布局。理解它们的本质,对掌握 Flutter 布局机制至关重要。 Row 和 Column 是什么?\\nRow 组件用于水平排列一组子组件。\\nColumn 组件用于垂直排列一组子组件。\\n\\n这两个组件都是继承自 Flutter 布局的基础控件——Flex,并通过固定其方向属性实现不同的布局行为:\\n\\nclass Row extends Flex {\\n /// Creates a horizontal array of…","guid":"https://juejin.cn/post/7504871512999313423","author":"MaoJiu","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-16T13:52:13.808Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7da254922a794c848a4335ddf1981516~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTWFvSml1:q75.awebp?rk3s=f64ab15b&x-expires=1748008333&x-signature=YrNDjSqGOf2pdWMNoX5rKonUqRk%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter - 集成三方库:日志(logger)","url":"https://juejin.cn/post/7504611910304137243","content":"使用print
方法时,会提示
$ flutter pub add logger\\n
\\n$ flutter pub get\\n
\\nimport \'package:logger/logger.dart\';\\n\\nvar logger = Logger();\\nlogger.d(\\"debug\\");\\nlogger.e(\\"error\\");\\nlogger.i(\\"info\\");\\nlogger.f(\\"fatal\\");\\nlogger.w(\\"warning\\");\\nlogger.t(\\"trace\\");\\n
\\n/// The current logging level of the app.\\n/// All logs with levels below this level will be omitted.\\n/// 设置打印级别,低于level的不会被打印\\nLogger.level;\\n
\\nenum Level {\\n all(0),\\n @Deprecated(\'[verbose] is being deprecated in favor of [trace].\')\\n verbose(999),\\n trace(1000),\\n debug(2000),\\n info(3000),\\n warning(4000),\\n error(5000),\\n @Deprecated(\'[wtf] is being deprecated in favor of [fatal].\')\\n wtf(5999),\\n fatal(6000),\\n @Deprecated(\'[nothing] is being deprecated in favor of [off].\')\\n nothing(9999),\\n off(10000),\\n ;\\n\\n final int value;\\n\\n const Level(this.value);\\n}\\n
\\n/// 使用 getApplicationDocumentsDirectory 需要\\n/// 添加 path_provider\\n/// flutter pub add path_provider\\n/// flutter pub ge \\n/// - `NSDocumentDirectory` on iOS and macOS.\\n/// - The Flutter engine\'s `PathUtils.getDataDirectory` API on Android.\\nDirectory documentDir = await getApplicationDocumentsDirectory();\\nvar logger = Logger(\\n output: FileOutput(file: File(\\"${documentDir.path}/log.txt\\")),\\n);\\n
\\nDirectory documentDir = await getApplicationDocumentsDirectory();\\nFileOutput fileOutPut = FileOutput(\\n file: File(\\"${documentDir.path}/log.txt\\"),\\n);\\nConsoleOutput consoleOutput = ConsoleOutput();\\nList<LogOutput> multiOutput = [fileOutPut, consoleOutput];\\nvar logger = Logger(\\n output: MultiOutput(multiOutput),\\n);\\n
\\nLogger构造函数有个printer
参数可以指定输出格式
var logger = Logger(\\n filter: null, // Use the default LogFilter (-> only log in debug mode)\\n printer: PrettyPrinter(dateTimeFormat: DateTimeFormat.dateAndTime),\\n output: MultiOutput(multiOutput),\\n);\\n
\\n默认的过滤器(DevelopmentFilter)在debug模式下可以打印所有level >= Logger.level的日志,在Release模式下所有日志被忽略。
\\n比如设置Release模式下只打印warning级别以上(含warning)的日志
\\nclass MyFilter extends LogFilter {\\n @override\\n bool shouldLog(LogEvent event) {\\n if (event.level.value < Logger.level.value) {\\n return false;\\n }\\n return true;\\n }\\n}\\n
\\n...\\nconst bool inProduction = bool.fromEnvironment(\\"dart.vm.product\\");\\nif (inProduction) {\\n Logger.level = Level.warning;\\n}\\n...\\n\\nConsoleOutput consoleOutput = ConsoleOutput();\\n List<LogOutput> multiOutput = [fileOutPut, consoleOutput];\\n var logger = Logger(\\n filter:\\n MyFilter(), // Use the default LogFilter (-> only log in debug mode)\\n printer: PrettyPrinter(dateTimeFormat: DateTimeFormat.dateAndTime),\\n output: MultiOutput(multiOutput),\\n);\\n\\nlogger.d(\\"debug\\");\\nlogger.e(\\"error\\");\\nlogger.i(\\"info\\");\\nlogger.f(\\"fatal\\");\\nlogger.w(\\"warning\\");\\nlogger.t(\\"trace\\");\\n
\\n当看到这个标题时,大家首先想到的或许是 Flutter 手势冲突的解决
,随之而来的可能是晦涩难懂的介绍和源代码堆砌。但本文却另辟蹊径,摒弃了复杂的手势竞争管理处理方式,仅通过简洁的代码便可轻松解决滑动问题。接下来,就让我们一同领略这一神奇操作!
一个页面包含一个 PageView1
, PageView1
又有两个视图PV1-View1
和 Pv1-View2
。 其中 PV1-View2
视图包含一个 PageView2
, 而这个 PageView2
也又有两个视图PV2-View1
和 Pv2-View2
。
场景
\\n当 PageView1
滑动到 PV1-View2
时,然后 PV1-View2
页面的 PageView2
滑动到 PV2-View1
时。
问题
\\n在上面的场景下,当我们再次向右
滑动 PV2-View1
时, 页面没有自动切换到 PageView1
的 PV1-View1
视图。
期望结果
\\n在滑动下能够流畅的切换到 PageView1
的 PV1-View1
的视图, 然后,再 向左
滑动进入 PV1-View2
下 PageView2
的 Pv2-View1
的视图。
最终效果展示
\\n我们使用 NotificationListener
包括 PageView1
来监听 PageView1
和 PageView2
的滑动。
通过 NotificationListener 的 {bool Function(ScrollNotification)? onNotification}
方法获取超出滑动()的滑动信息 OverscrollNotification
;
在滑动信息 OverscrollNotification
中的 depth
属性来区分是滑动的 PageView1
还是 PageView2
;
然后由 OverscrollNotification
的 overscroll
属性判断滑动的方向 (overscroll > 0
是向左滑动;overscroll < 0
是向右滑动);
再由 OverscrollNotification
的 velocity
属性判断是否 向右
滑动到边缘。
PageView
的 physics
属性在不同平台上默认值如下:
ClampingScrollPhysics
,这会阻止滚动超过内容范围。BouncingScrollPhysics
,允许滚动超过并回弹。我们需要超出内容的滑动通知,而 IOS
的默认 physics
不满足需求,所以我们要修改 PageView2
的 physics
为 ClampingScrollPhysics
进行两端统一。
NotificationListener
在 Flutter 中,NotificationListener
是一个非常强大的小部件,用于监听和处理滚动通知(如滚动开始、滚动结束、滚动位置变化等)。它通常与滚动组件(如 ListView
、GridView
、 SingleChildScrollView
或 PageView
)结合使用,以监听滚动事件并根据需要执行操作。\\n类代码:
class NotificationListener<T extends Notification> extends ProxyWidget {\\n /// Creates a widget that listens for notifications.\\n const NotificationListener({\\n super.key,\\n required super.child,\\n this.onNotification,\\n });\\n\\n final NotificationListenerCallback<T>? onNotification;\\n\\n @override\\n Element createElement() {\\n return _NotificationElement<T>(this);\\n }\\n}\\n
\\nNotificationListener
的使用非常简单,它需要一个 onNotification
回调函数来处理通知。
OverscrollNotification
在 Flutter 中,OverscrollNotification
是一种滚动通知,用于在滚动超出边界
时触发。类代码:
class OverscrollNotification extends ScrollNotification {\\n OverscrollNotification({\\n required super.metrics,\\n required BuildContext super.context,\\n this.dragDetails,\\n required this.overscroll,\\n this.velocity = 0.0,\\n }) : assert(overscroll.isFinite),\\n assert(overscroll != 0.0);\\n \\n // 如果 Scrollable 因拖动而过度滚动,则有关该拖动的详细信息将更新。\\n final DragUpdateDetails? dragDetails;\\n \\n // Scrollable 过度滚动的逻辑像素数。\\n final double overscroll;\\n \\n // 发生过度滚动时 ScrollPosition 变化的速度。\\n final double velocity;\\n }\\n
\\n上面介绍了官方解释,下面我们用白话在絮叨一遍:
\\nDragUpdateDetails? dragDetails
\\n这个参数是当你过度的滑动时,当时的拖动信息, 默认值为 Null
。 就是说你不过度拖拽就不会有该属性值的更新。
double overscroll
\\n这个属性就是你过度拖拽时 Scrollable 滚动的像素数。 这对于“开始”侧的过度滚动来说为负,而对于“结束”侧的过度滚动来说为正。
\\ndouble velocity
\\n这个就是你过度拖拽时 ScrollPosition 的变化速度。(位置的变化,切记
)
int depth
\\n因为 OverscrollNotification
集成于 ScrollNotification
, ScrollNotification
又混入 ViewportNotificationMixin
, 而 depth
是混入的参数属性。该属性是通知已冒泡的视口数量,通常监听器仅响应 depth
为零的通知。大白话就是:用于区分你拖动的那个 PageView
。
ClampingScrollPhysics
在 Flutter 中,ClampingScrollPhysics
是一种滚动物理模型,用于限制滚动范围,防止滚动超出内容的实际范围。它通常用于 PageView
和其他滚动组件,确保滚动只能在内容的范围内进行。
/// PageView1 视图\\nNotificationListener<OverscrollNotification>(\\n onNotification: (notification) {\\n if (notification.depth == 1 && notification.overscroll < 0 && notification.velocity == 0) {\\n _pageController.animateToPage(0, duration: Duration(milliseconds: 200), curve: Curves.linear);\\n return false;\\n }\\n return true;\\n },\\n child: PageView.builder(\\n controller: _pageController,\\n ... // 省略无用代码\\n ).expanded(),\\n),\\n\\n/// PageView2 的视图\\nPageView.builder(\\n physics: const ClampingScrollPhysics(),\\n ...// 省略无用代码\\n)\\n
\\n通过以上操作,我们就能实现 PageView
嵌套,流畅的切换了。小小的操作解决大大的问题,请为之打 Call 吧!!!
app突然来个需求,页面的背景颜色太单调了,要加一个渐变的颜色,还得是全屏(包括appbar栏)的人嘛。\\n本以为直接加一个Container中加个渐变色就欧克了,然后appbar栏没有变。\\n后面就用图片来搞,才解决了,直接上代码。\\n1、app页码不展示appbar栏
\\n@override\\n Widget build(BuildContext context) { \\n return Scaffold(\\n // 取消的appBar\\n appBar: null, // 不写也可以,表示没有\\n backgroundColor: Colors.transparent, // 全局背景是透明的\\n extendBodyBehindAppBar: true, // 内容延伸到 AppBar 后面\\n body: Stack( //定位\\n children: [\\n // ✅ 背景图片填充\\n Positioned.fill(\\n child: Image.asset(\\n \'assets/images/xxxx.png\', // 图片\\n fit: BoxFit.cover,\\n ),\\n ), // 图片占满全屏 做被被进图\\n SafeArea(child: buildContent(controller, context)), // 这个就是要放的全屏的代码\\n Positioned( // 全屏定位的一个icon图标\\n right: 3, \\n top: 0,\\n bottom: 0, // ✅ 父Stack撑满\\n child: Center(\\n // ✅ Positioned内垂直居中\\n child: GestureDetector(\\n onTap: () {},\\n child: Image.asset(\\n \\"assets/images/xxxxx.png\\",\\n width: 60,\\n height: 42,\\n ),\\n ),\\n ),\\n )\\n ],\\n ),\\n );\\n }\\n
\\n2、app页码展示appbar栏
\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n extendBodyBehindAppBar: true, //这个很重要\\n backgroundColor: Colors.transparent,\\n appBar: AppBar(\\n backgroundColor: Colors.transparent,\\n centerTitle: true,\\n elevation: 0,\\n title: Text(\\n \'学习交流\',\\n style: TextStyle(color: Colors.black, fontSize: 16),\\n ),\\n ),\\n body: Stack(\\n children: [\\n // ✅ 背景图填充\\n Positioned.fill(\\n child: Image.asset(\\n \'assets/images/xxxx.png\',\\n fit: BoxFit.cover,\\n ),\\n ),\\n // ✅ 内容\\n Scaffold(\\n backgroundColor: Colors.transparent,\\n body: SafeArea(\\n child: Container(\\n chilr: //页面内容\\n ),\\n ),\\n ),\\n ],\\n ),\\n );\\n } \\n
\\n目前就想到这个方法解决这个问题,还有其他的办法欢迎大家在下面讨论
","description":"app突然来个需求,页面的背景颜色太单调了,要加一个渐变的颜色,还得是全屏(包括appbar栏)的人嘛。 本以为直接加一个Container中加个渐变色就欧克了,然后appbar栏没有变。 后面就用图片来搞,才解决了,直接上代码。 1、app页码不展示appbar栏 @override\\n Widget build(BuildContext context) { \\n return Scaffold(\\n // 取消的appBar\\n appBar: null, // 不写也可以,表示没有\\n backgroundColo…","guid":"https://juejin.cn/post/7504577797520850970","author":"方文_","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-16T02:41:10.396Z","media":null,"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"[Flutter 进阶] - LayoutBuilder 一个非常好用的布局组件","url":"https://juejin.cn/post/7504491526363742246","content":"但是如果想深入学习Flutter,了解LayoutBuilder组件是非常有必要的, 但对于一个初学者来说,LayoutBuilder
组件的使用率挺低的,这也是我把这个组件放在进阶这部分的原因之一。它不仅仅可以帮助我们实现响应式、约束布局,还可以在我们开发过程中助力我们解决很多复杂UI的布局问题。
LayoutBuilder
的核心作用是通过父级传递的约束(BoxConstraints
)动态构建响应式布局,其工作原理可分为三个关键阶段:
约束传递阶段 父组件向 LayoutBuilder
传递 BoxConstraints
对象,包含:
minWidth
/maxWidth
:水平方向约束minHeight
/maxHeight
:垂直方向约束布局计算阶段 通过 builder 函数获取约束参数,动态生成子组件树:
\\nLayoutBuilder(\\n builder: (context, constraints) {\\n // 基于 constraints 创建布局\\n }\\n)\\n
\\n渲染优化阶段 自动处理布局边界(Render Object)并优化重绘逻辑
\\n自从这些双折叠屏,三折叠屏出来以后,尺寸适配的场景越来越多。所以经常会遇到那种需要根据屏幕的不同尺寸来调整页面的布局结构。包括一些横竖屏切换的场景。\\n这个时候LayoutBuilder就是一个非常好用的工具,它可以根据可用空间动态切换布局模式:
\\nLayoutBuilder(\\n builder: (context, constraints) {\\n if (constraints.maxWidth > 600) {\\n return _buildWideLayout(); // 宽屏布局\\n } else {\\n return _buildMobileLayout(); // 移动端布局\\n }\\n },\\n)\\n\\n// 示例布局构建方法\\nWidget _buildWideLayout() => Row(...);\\nWidget _buildMobileLayout() => Column(...);\\n
\\n如果遇到那种需要根据父组件的宽度去控制列表展示的列数的时候,如做一些缩放功能,也可以通过LayoutBuilder去获取到父组件的尺寸。这样就精确控制元素尺寸与布局约束的关系:
\\nLayoutBuilder(\\n builder: (context, constraints) {\\n final itemWidth = constraints.maxWidth / 3 - 10;\\n return GridView.count(\\n crossAxisCount: 3,\\n childAspectRatio: 1,\\n children: List.generate(9, (index) => \\n Container(\\n width: itemWidth,\\n height: itemWidth,\\n color: Colors.blue,\\n ),\\n );\\n },\\n)\\n
\\n根据容器宽度动态调整字体大小:
\\nLayoutBuilder(\\n builder: (context, constraints) {\\n final baseSize = constraints.maxWidth * 0.1;\\n return Text(\\n \'Adaptive Text\',\\n style: TextStyle(fontSize: baseSize.clamp(12, 24)),\\n );\\n },\\n)\\n
\\n通过组合多个 LayoutBuilder
处理复杂布局层级,子组件可以访问父级组件的约束条件:
return LayoutBuilder(\\n builder: (context, outerConstraints) {\\n return Column(\\n children: [\\n SizedBox(height: 100,),\\n Container(\\n color: Colors.blue[100],\\n padding: EdgeInsets.all(10),\\n child: LayoutBuilder(\\n builder: (context, innerConstraints) {\\n return Text(\\n \'外层约束: ${outerConstraints}\\\\n\' //访问最外层的组件约束\\n \'内层约束: ${innerConstraints}\',\\n style: TextStyle(fontFamily: \'monospace\'),\\n );\\n },\\n ),\\n ),\\n Expanded(\\n child: Container(\\n color: Colors.amber[100],\\n child: Center(\\n child: LayoutBuilder(\\n builder: (context, constraints) {\\n return Text(\\n \'可用高度: ${constraints.maxHeight.toStringAsFixed(1)}\',\\n style: TextStyle(fontSize: 24),\\n );\\n },\\n ),\\n ),\\n ),\\n ),\\n ],\\n );\\n },\\n );\\n
\\n如果在做一些动画或者会动态变化组件约束条件的时候,可以通过判断前后两次约束的变化来重新绘制。通过减少页面绘制次数的方式来提高页面绘制性能:
\\nclass CachedLayout extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return LayoutBuilder(\\n builder: (context, constraints) {\\n return _CacheWrapper( // 自定义缓存组件\\n constraints: constraints,\\n child: _ExpensiveChild(),\\n );\\n },\\n );\\n }\\n}\\n\\nclass _CacheWrapper extends StatelessWidget {\\n final BoxConstraints constraints;\\n final Widget child;\\n\\n // 通过 shouldRebuild 控制重建条件\\n @override\\n bool shouldRebuild(_CacheWrapper old) => \\n old.constraints != constraints;\\n\\n @override\\n Widget build(BuildContext context) => child;\\n}\\n
\\n这条是我认为最关键的一条。\\n其实前面的所有应用场景,我们也可以通过一些其它方式来达到类似的效果。\\n但是,当我们常常因为复杂的布局出现一些overlap错误或者是别的看似奇奇怪怪的UI因为约束条件导致的错误时,往往会因为不知道到底哪个地方出现问题而烦恼,这个时候就可以通过LayoutBuider去打印他们的布局约束条件来排查问题。
\\nLayoutBuilder(\\n builder: (context, constraints) {\\n debugPrint(\'当前约束: $constraints\');\\n return ...\\n },\\n)\\n
\\nLayoutBuilder
组件主要是可以通过获取到父组件的约束条件,通过它这个特性可以帮我们解决很多复杂的业务场景。甚至在我们日常开发中,可以用来帮助我们去调试一些因为约束条件导致的UI问题。
《鸿蒙纪元》 是 张风捷特烈 计划打造的一套 HarmonyOS 开发系列教程合集。致力于创作优质的鸿蒙原生学习资源,帮助开发者进入纯血鸿蒙的开发之中。Flutter卷 是探讨基于 Flutter 跨平台技术,构建纯血鸿蒙应用的方式。帮助 Flutter 开发者完成一统七国的霸业。
\\nFlutter 最主流的跨平台开发框架之一,支持 Android、iOS、Windows、Macos、Linux、Web 六大平台。当你的应用恰好用 Flutter 构建,此时又需要支持鸿蒙版,该怎么做呢?单独使用 ArkUI 再写一份感觉有些得不偿失,你不禁感叹,如果使用 Flutter 可以构建鸿蒙应用,那世界将会多么美好。
\\n好消息
是:Flutter 可以支持鸿蒙坏消息
是: 不是 Google 官方的 Flutter,而且目前最高版本是 3.22.0鸿蒙版 Flutter 由 OpenHarmony-SIG 维护,发布在 gitcode :
\\n\\n\\n\\n
首先,我们需要下载项目,并将其切到 3.22.1-ohos-1.0.0 标签,以此得到 鸿蒙版 Flutter SDK 环境
\\n# 下载仓库\\ngit clone https://gitcode.com/openharmony-sig/flutter_flutter.git\\n\\n# 切换到 3.22.1-ohos-1.0.0 标签\\ncd flutter_flutter\\ngit checkout 3.22.1-ohos-1.0.0\\n\\n# 产看当前 flutter_ohos 环境\\ncd bin\\n.\\\\flutter --doctor\\n
\\n如果出现了下面的问题,说明鸿蒙相关的环境没有配好:
\\n其中 TOOL_HOME 是 DevEco-Studio 的地址。新版的 DevEco-Studio 中已经内置了 ohpm
、node
、hvigor
环境:
# 拉取下来的flutter_flutter/bin目录\\n export PATH=/home/<user>/ohos/flutter_flutter/bin:$PATH\\n\\n # HamonyOS SDK\\n export TOOL_HOME=/Applications/DevEco-Studio.app/Contents \\n export DEVECO_SDK_HOME=$TOOL_HOME/sdk \\n export PATH=$TOOL_HOME/tools/ohpm/bin:$PATH \\n export PATH=$TOOL_HOME/tools/hvigor/bin:$PATH \\n export PATH=$TOOL_HOME/tools/node/bin:$PATH \\n
\\nTOOL_HOM = DevEco安装路径\\nDEVECO_SDK_HOME = %TOOL_HOME%\\\\sdk\\n\\n# Path 路径:\\n下载仓库路径\\\\bin\\n%TOOL_HOME%\\\\tools\\\\ohpm\\\\bin\\n%TOOL_HOME%\\\\tools\\\\hvigor\\\\bin\\n%TOOL_HOME%\\\\tools\\\\node\\\\bin\\n
\\n再查看一下,出现对钩即可。到这里,Flutter for 鸿蒙的环境就搭建完了。
\\nFlutter&鸿蒙
初体验环境搭建好了,鸿蒙版 Flutter SDK 和 Google flutter SDK 的命令是一样的。现在创建一个项目试试
\\n下面创建一个 counter 的项目,会发现其中会多一个 ohos 的文件夹,这就是原生的鸿蒙宿主项目:
\\n\\n\\nflutter create counter
\\n
har 包是鸿蒙原生的依赖库,需要先编译出来才能运行。会出现 异常集录 第二点,详见附录。
\\n\\n\\nflutter build hap --release
\\n
产物在 ohos/entry/build 文件夹下:
\\nAndroidStudio 无法感知鸿蒙设备,而 DevEco Studio 没有 Flutter 插件,使用也无法直接运行 Flutter 项目,这就一根筋变两头堵了。可以通过命令行运行到鸿蒙设备上:
\\n\\n\\nflutter run --debug -d
\\n<deviceId>
其中设备的 id 可以通过下面命令查找,如下所示 deviceId 是 23E0224127000257:
\\n\\n\\nflutter devices
\\n
Found 4 connected devices:\\n23E0224127000257 (mobile) • 23E0224127000257 • ohos-arm64 • Ohos OpenHarmony-5.0.3.135 (API 15)\\nWindows (desktop) • windows • windows-x64 • Microsoft Windows [版本 10.0.26100.4061]\\nChrome (web) • chrome • web-javascript • Google Chrome 136.0.7103.93\\nEdge (web) • edge • web-javascript •\\n
\\n这样,就可以在真机上跑起来计数器项目,没有真假也可以使用模拟器。
\\n另外,在打出 har 包后,也可以通过 DevEco Studio 打开 ohos 项目运行。这就相当于用 Android Studio 打开 android 文件夹,可以调试原生代码,但是无法调试 Flutter 代码。
\\n虽然鸿蒙版的 Flutter 可以跑,但目前来看还有些小问题。
\\n首先 鸿蒙版Flutter
和 Google Flutter
是两个东西, 所以同一时刻命令行里只能识别一个 flutter 命令。虽然可以用 fvm 切换 flutter 环境,但切来切去也挺麻烦。 既然两个是不同的东西,为什么非得叫一样的名字?
所以我的方式是: 把可执行文件名改一下,叫 hflutter
, 这样编译鸿蒙相关的使用 hflutter 命令即可,也不会对现有的 Flutter 开发产生任何影响:
由于是两个不同的东西,而且鸿蒙 Flutter 和核心还是依赖 Google Flutter。所以版本的同步更新是一个非常大的问题。如果你已有的项目已经适配了高版本 Flutter ,那为了鸿蒙的支持而降低版本,就显得有些不优雅。
\\n希望后续鸿蒙版Flutter 可以持续适配,尽早升到 Flutter 3.27 这个稳定版本。
\\n由于和原生交互的插件需要宿主平台提供渠道方法,而 pub 上的库一般都不会支持鸿蒙。所以三方库的生态是个很大的问题。但目前来看,很多高频使用的三方库已经可以支持鸿蒙 《Flutter高频使用的三方库(部分OpenHarmony化)列表》。但是这个支持仅是基于 git 仓库访问。直接使用 pub 仓库的依赖是不行的,这也为三方库的维护带来了一定的压力。
\\n不管怎么说,现在已经能跑了。鸿蒙版 Flutter 已经迈出了坚实的第一步,有了这个 1
的质变 ,后面就是量变的积累。如果鸿蒙的设备未来有好好的生态,鸿蒙版Flutter 能够合并到 Google Flutter
,这可谓一桩美谈。那本文就到这里,以后我会陆续将已有的 Flutter 项目支持鸿蒙版,其中有什么坑的话,我也会记录分析。本文就到这里,谢谢观看 ~
更多文章和视频知识资讯,大家可以关注我的公众号、掘金和 B 站 。关注 公众号
并回复 鸿蒙纪元 可领取最新的 xmind 脑图电子版,让我们一起成长,变得更强。我们下次再见~
如果电脑中已经有了node ,或者通过 nvm 管理 node 版本,且当前 node 版本过低,会出现这个错误。
\\n解决方案: 使用 16 + 的 node
\\n>> nvm list\\n 18.18.0\\n * 14.20.0 (Currently using 64-bit executable)\\n>> nvm use 18.18.0\\n
\\n使用 DevEco Studio 打开 ohos 项目,在 File/Project Structure/Signing Configs 中生成并配置签名。
\\n\\n在 Flutter(Dart)开发中,mixin 是一个非常重要且强大的语言特性。它让我们能够在不使用继承的情况下,实现代码复用与功能扩展。本文将带你全面了解什么是 mixin,如何定义和使用它,多个 mixin 如何协作以及冲突方法的调用规则,最后还会示范几个自定义的实用 mixin。
Mixin 是一种在类之间共享代码的机制。它不像继承那样是“is-a”关系,而更像是“has-a”或“can-do”的功能补充。通过 mixin,我们可以把一组功能封装起来,方便在多个类中复用,避免了代码重复和复杂的多重继承问题。
\\nDart 中的 mixin 可以看作是“可被复用的代码片段”,它既能包含字段,也能包含方法,但它不能被实例化,通常用于状态类(State)或普通类中注入通用功能。
\\n在 Dart(Flutter)中,定义 mixin 的方式很简单,只需要使用 mixin 关键字,跟定义类很相似。
\\nmixin IncrementMixin {\\n int increment(int a, int b) {\\n return a + b;\\n }\\n}\\n
\\n使用时只需要使用 with 关键字将 mixin 应用到类上。
\\nclass MyClass with IncrementMixin {\\n ...\\n}\\n\\nvoid main() {\\n MyClass obj = MyClass();\\n int x = obj.increment(1, 2);\\n print(x); // 3\\n}\\n
\\nDart 允许一个类使用多个 mixin,多个 mixin 通过 with 关键字依次列出。
\\nclass MyClass with IncrementMixin, AddMixin {\\n // ...\\n}\\n
\\n当多个 mixin 中定义了相同的方法时,最后声明的 mixin 的方法会覆盖之前的。例如下面的两个 mixin。
\\nmixin IncrementMixin {\\n int increment(int a, int b) {\\n return a + b;\\n }\\n}\\n\\nmixin AddMixin {\\n int increment(int a, int b) {\\n return (a + b) * 2;\\n }\\n}\\n
\\n如果一个类同时混入这两个 mixin,调用 increment(1, 2) 将执行 AddMixin 中的方法,结果是 (1 + 2) * 2 = 6。
\\nclass MyClass with IncrementMixin, AddMixin {}\\n
\\n在 Flutter 中,页面路由的生命周期监听非常有用。我们可以通过实现 RouteAware 接口来监测当前页面的推入、弹出、覆盖等事件。
\\nfinal RouteObserver<ModalRoute<void>> routeObserver =\\n RouteObserver<ModalRoute<void>>();\\n\\nmixin RouteAwareMixin<T extends StatefulWidget> on State<T>\\n implements RouteAware {\\n late ModalRoute<void> route;\\n\\n @override\\n void didChangeDependencies() {\\n super.didChangeDependencies();\\n route = ModalRoute.of(context)!;\\n routeObserver.subscribe(this, route);\\n }\\n\\n @override\\n void dispose() {\\n routeObserver.unsubscribe(this);\\n super.dispose();\\n }\\n\\n @override\\n void didPush() {\\n debugPrint(\\"[didPush] ${route.settings.name}\\");\\n }\\n\\n @override\\n void didPop() {\\n debugPrint(\\"[didPop] ${route.settings.name}\\");\\n }\\n\\n @override\\n void didPopNext() {\\n debugPrint(\\"[didPopNext] ${route.settings.name}\\");\\n }\\n\\n @override\\n void didPushNext() {\\n debugPrint(\\"[didPushNext] ${route.settings.name}\\");\\n }\\n}\\n
\\n通过这个 Mixin,任何继承了它的 StatefulWidget 都可以监听到路由变化事件,方便管理页面状态。
\\n管理 TextEditingController 的生命周期也可以封装到 mixin 中,避免冗余代码。
\\nmixin TextEditingControllerMixin<T extends StatefulWidget> on State<T> {\\n final TextEditingController textEditingController = TextEditingController();\\n\\n @override\\n void dispose() {\\n textEditingController.dispose();\\n super.dispose();\\n }\\n}\\n
\\n在Flutter中,使用动画的时候会使用到SingleTickerProviderStateMixin,这也是一个Mixin,使用多个mixin的方法和普通类使用多个mixin的组合方法一致。
\\nclass _TestMixinState extends State<TestMixin>\\n with\\n SingleTickerProviderStateMixin,\\n IncrementMixin,\\n AddMixin, // 注意最后一个会覆盖increment方法\\n TextEditingControllerMixin,\\n RouteAwareMixin {\\n late AnimationController _animationController;\\n int _incrementValue = 0;\\n\\n @override\\n void initState() {\\n super.initState();\\n _animationController = AnimationController(\\n vsync: this,\\n duration: const Duration(seconds: 2),\\n )..repeat(reverse: true);\\n }\\n\\n @override\\n void dispose() {\\n _animationController.dispose();\\n super.dispose();\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: const Text(\'Test Mixin\')),\\n body: Center(\\n child: Column(\\n mainAxisAlignment: MainAxisAlignment.center,\\n children: [\\n TextField(controller: textEditingController),\\n const SizedBox(height: 20),\\n Text(\\"increment value: $_incrementValue\\"),\\n const SizedBox(height: 20),\\n AnimatedBuilder(\\n animation: _animationController,\\n builder: (context, child) {\\n final size = _animationController.value * 100;\\n return Container(\\n width: size,\\n height: size,\\n decoration: BoxDecoration(\\n borderRadius: BorderRadius.circular(size / 2),\\n color: Colors.blue,\\n ),\\n );\\n },\\n ),\\n const SizedBox(height: 20),\\n ElevatedButton.icon(\\n onPressed: () => Navigator.pushNamed(context, \\"/next\\"),\\n icon: const Icon(Icons.next_plan),\\n label: const Text(\\"Next Page\\"),\\n )\\n ],\\n ),\\n ),\\n floatingActionButton: FloatingActionButton(\\n onPressed: () {\\n setState(() {\\n // 调用的是 AddMixin 中的 increment,(1 + 2)*2 = 6\\n _incrementValue = increment(1, 2);\\n });\\n },\\n child: const Icon(Icons.add),\\n ),\\n );\\n }\\n}\\n
\\n如果在 Flutter 开发中遇到需要多功能扩展的场景,强烈建议尝试用 mixin 实现,能让你的代码更灵活、更模块化。
","description":"在 Flutter(Dart)开发中,mixin 是一个非常重要且强大的语言特性。它让我们能够在不使用继承的情况下,实现代码复用与功能扩展。本文将带你全面了解什么是 mixin,如何定义和使用它,多个 mixin 如何协作以及冲突方法的调用规则,最后还会示范几个自定义的实用 mixin。 什么是Mixin?\\n\\nMixin 是一种在类之间共享代码的机制。它不像继承那样是“is-a”关系,而更像是“has-a”或“can-do”的功能补充。通过 mixin,我们可以把一组功能封装起来,方便在多个类中复用,避免了代码重复和复杂的多重继承问题。\\n\\nDart 中的…","guid":"https://juejin.cn/post/7504491526363070502","author":"MaoJiu","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-15T11:47:01.449Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/58d5b34473584cfca0f30bbc6ae3e68b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTWFvSml1:q75.awebp?rk3s=f64ab15b&x-expires=1747914421&x-signature=N%2FUtiQxy50Smj8%2FxJDjqSgcCsh4%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"解决 Mac M系列芯片 模拟器列表滚动过快问题","url":"https://juejin.cn/post/7504492019775995943","content":"部分SDK在未提供 iOS-Simulator 使用的framework.
\\n主要表现在 iOS/Podfile 中需要使用 config.build_settings[\'EXCLUDED_ARCHS[sdk=iphonesimulator*]\'] = \'arm64\'
才能运行的项目。
xcrun simctl list devices
== Devices ==
区域下找到需要修改的模拟器xcrun simctl boot xxxxxx --arch=x86_64
Simulator 运行问题\\nhttps://www.alipan.com/s/nWY3nJksyqA\\n提取码: uc18\\n点击链接保存,或者复制本段内容,打开「阿里云盘」APP ,无需下载极速在线查看,视频原画倍速播放。\\n
\\nPlugin | Pub | Points | Popularity | Likes |
---|---|---|---|---|
flutter_file_view | ||||
jpush_flutter_android | ||||
upgrade_util | ||||
flutter_video_view | ||||
password_hash_plus | ||||
ve_vod_controls |
\\n\\n自己学习的小demo地址放在这里,仅供学习参考,flutter学习demo
\\n
在 Flutter 中封装一个 Router.dart
文件来统一管理页面跳转是一个非常实用的做法,可以提升代码的可维护性和复用性。下面我将为你提供一个结构清晰、功能全面的 Router
类封装示例,并附上使用说明。
Router.dart
文件// lib/router/router.dart\\n\\nimport \'package:flutter/material.dart\';\\n\\nclass Router {\\n // 路由表:定义所有页面的路由路径与对应的构建器\\n static final Map<String, WidgetBuilder> routeTable = {\\n \'/\': (context) => const HomeScreen(),\\n \'/detail\': (context) => DetailScreen(\\n arguments: ModalRoute.of(context)!.settings.arguments as String),\\n \'/settings\': (context) => const SettingsScreen(),\\n };\\n\\n // 页面跳转(带返回按钮)\\n static Future<T?> pushNamed<T extends Object?>(\\n BuildContext context,\\n String routeName, {\\n Object? arguments,\\n }) {\\n return Navigator.pushNamed<T>(\\n context,\\n routeName,\\n arguments: arguments,\\n );\\n }\\n\\n // 页面跳转并替换当前页面(无返回按钮)\\n static Future<T?> pushReplacementNamed<T extends Object?>(\\n BuildContext context,\\n String routeName, {\\n Object? arguments,\\n }) {\\n return Navigator.pushReplacementNamed<T, Object?>(\\n context,\\n routeName,\\n arguments: arguments,\\n );\\n }\\n\\n // 弹出当前页面(返回上一页)\\n static void pop(BuildContext context, [Object? result]) {\\n Navigator.of(context).pop(result);\\n }\\n\\n // 弹出当前页面并返回数据\\n static void popWithResult(BuildContext context, Object? result) {\\n Navigator.of(context).pop(result);\\n }\\n\\n // 弹出所有页面并跳转到指定页面\\n static Future<T?> pushAndRemoveUntil<T extends Object?>(\\n BuildContext context,\\n String routeName, {\\n Object? arguments,\\n }) {\\n return Navigator.of(context).pushNamedAndRemoveUntil(\\n routeName,\\n (route) => false,\\n arguments: arguments,\\n );\\n }\\n\\n // 处理未注册的路由\\n static Route<dynamic> onUnknownRoute(RouteSettings settings) {\\n return MaterialPageRoute(\\n builder: (context) => const ErrorScreen(),\\n settings: settings,\\n );\\n }\\n}\\n
\\n// lib/screens/home_screen.dart\\n\\nimport \'package:flutter/material.dart\';\\n\\nclass HomeScreen extends StatelessWidget {\\n const HomeScreen({Key? key}) : super(key: key);\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: const Text(\'Home\')),\\n body: Center(\\n child: ElevatedButton(\\n onPressed: () {\\n Router.pushNamed(context, \'/detail\', arguments: \'Detail Page\');\\n },\\n child: const Text(\'Go to Detail\'),\\n ),\\n ),\\n );\\n }\\n}\\n
\\n// lib/screens/detail_screen.dart\\n\\nimport \'package:flutter/material.dart\';\\n\\nclass DetailScreen extends StatelessWidget {\\n final String arguments;\\n\\n const DetailScreen({Key? key, required this.arguments}) : super(key: key);\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\'Detail: $arguments\')),\\n body: Center(\\n child: ElevatedButton(\\n onPressed: () => Router.pop(context),\\n child: const Text(\'Back\'),\\n ),\\n ),\\n );\\n }\\n}\\n
\\n// lib/screens/settings_screen.dart\\n\\nimport \'package:flutter/material.dart\';\\n\\nclass SettingsScreen extends StatelessWidget {\\n const SettingsScreen({Key? key}) : super(key: key);\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: const Text(\'Settings\')),\\n body: const Center(child: Text(\'Settings Page\')),\\n );\\n }\\n}\\n
\\n// lib/screens/error_screen.dart\\n\\nimport \'package:flutter/material.dart\';\\n\\nclass ErrorScreen extends StatelessWidget {\\n const ErrorScreen({Key? key}) : super(key: key);\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: const Text(\'404\')),\\n body: const Center(child: Text(\'Page Not Found\')),\\n );\\n }\\n}\\n
\\nmain.dart
中注册路由// lib/main.dart\\n\\nimport \'package:flutter/material.dart\';\\nimport \'router/router.dart\';\\nimport \'screens/home_screen.dart\';\\n\\nvoid main() {\\n runApp(const MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n const MyApp({Key? key}) : super(key: key);\\n\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n title: \'Router Demo\',\\n initialRoute: \'/\',\\n onGenerateRoute: Router.routeTable.isEmpty\\n ? null\\n : (settings) => MaterialPageRoute(\\n builder: (context) => Router.routeTable[settings.name]!(context),\\n settings: settings,\\n ),\\n onUnknownRoute: Router.onUnknownRoute,\\n home: const HomeScreen(),\\n );\\n }\\n}\\n
\\n// 示例:跳转到详情页并传递参数\\nRouter.pushNamed(context, \'/detail\', arguments: \'Hello from Home\');\\n\\n// 示例:跳转并替换当前页面\\nRouter.pushReplacementNamed(context, \'/settings\');\\n\\n// 示例:弹出当前页面\\nRouter.pop(context);\\n\\n// 示例:弹出所有页面并跳转到首页\\nRouter.pushAndRemoveUntil(context, \'/\');\\n
\\n优点 | 说明 |
---|---|
统一管理 | 所有页面跳转逻辑集中在 Router 类中,便于维护。 |
参数传递 | 支持带参数的跳转和返回数据处理。 |
错误处理 | 自动处理未注册的路由(404 页面)。 |
可扩展性强 | 可轻松添加新页面或修改现有路由规则。 |
通过这种方式,你可以将 Flutter 应用中的页面跳转逻辑统一管理,提升代码结构的清晰度和可维护性。如果你有更复杂的需求(如嵌套路由、动画跳转等),也可以在此基础上扩展。
\\n这个错误是因为 Flutter 框架中的 Router
类(来自 package:flutter/src/widgets/router.dart
)与你自定义的 Router
类(来自 package:my_app/alearn_demo/router_demo/router.dart
)发生了 命名冲突。你需要通过 重命名导入 或 隐藏冲突类 的方式来解决这个问题。
as
重命名导入通过给其中一个库添加别名,区分冲突的 Router
类。
main.dart
或其他使用 Router
的文件// 1. 导入 Flutter 的 Material 库(默认会引入 Router 类)\\nimport \'package:flutter/material.dart\' as flutter;\\n\\n// 2. 导入自定义的 Router 类(添加别名)\\nimport \'package:my_app/alearn_demo/router_demo/router.dart\' as my_router;\\n\\nvoid main() {\\n runApp(MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return flutter.MaterialApp(\\n title: \'Router Demo\',\\n initialRoute: \'/\',\\n onGenerateRoute: (settings) => flutter.MaterialPageRoute(\\n builder: (context) => my_router.Router.routeTable[settings.name]!(context),\\n ),\\n onUnknownRoute: my_router.Router.onUnknownRoute,\\n home: const my_router.HomeScreen(),\\n );\\n }\\n}\\n
\\nRouter.dart
文件确保你的 Router
类使用的是自己的命名空间:
// lib/alearn_demo/router_demo/router.dart\\n\\nimport \'package:flutter/material.dart\';\\n\\nclass Router {\\n // 你的路由表和跳转方法...\\n}\\n
\\nhide
隐藏冲突类如果只需要使用 Flutter 的 MaterialApp
等组件,但不需要框架的 Router
类,可以通过 hide
隐藏它。
// 隐藏 Flutter 中的 Router 类\\nimport \'package:flutter/material.dart\' hide Router;\\n\\n// 导入自定义的 Router 类\\nimport \'package:my_app/alearn_demo/router_demo/router.dart\';\\n
\\n为什么会出现冲突?
\\nMaterialApp
会隐式导入 material.dart
,而 material.dart
会引入 Router
类(用于新路由系统)。如果你的代码中也定义了一个 Router
类,就会发生冲突。推荐做法
\\nRouter
类),则需要保留 Flutter 的 Router
,并重命名你的自定义路由管理类(如 MyRouter
)。Navigator
路由(基于 pushNamed
),可以安全地隐藏 Flutter 的 Router
类。main.dart
// 隐藏 Flutter 的 Router 类\\nimport \'package:flutter/material.dart\' hide Router;\\n\\n// 导入自定义的 Router 类\\nimport \'package:my_app/alearn_demo/router_demo/router.dart\';\\n\\nvoid main() {\\n runApp(MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n title: \'Router Demo\',\\n initialRoute: \'/\',\\n onGenerateRoute: (settings) => MaterialPageRoute(\\n builder: (context) => Router.routeTable[settings.name]!(context),\\n ),\\n onUnknownRoute: Router.onUnknownRoute,\\n home: const HomeScreen(),\\n );\\n }\\n}\\n
\\nRouter.dart
// lib/alearn_demo/router_demo/router.dart\\n\\nimport \'package:flutter/material.dart\';\\n\\nclass Router {\\n static final Map<String, WidgetBuilder> routeTable = {\\n \'/\': (context) => const HomeScreen(),\\n \'/detail\': (context) => DetailScreen(),\\n };\\n\\n static Future<T?> pushNamed<T extends Object?>(\\n BuildContext context,\\n String routeName,\\n ) {\\n return Navigator.pushNamed<T>(context, routeName);\\n }\\n\\n // 其他方法...\\n}\\n
\\n问题 | 原因 | 解决方案 |
---|---|---|
Router 类冲突 | Flutter 框架和自定义代码中都定义了 Router 类 | 使用 as 重命名导入或 hide 隐藏冲突类 |
推荐做法 | 使用传统 Navigator 路由时 | 隐藏 Flutter 的 Router 类 |
新路由系统需求 | 使用 Flutter 的 Router 类 | 重命名自定义类(如 MyRouter ) |
通过以上方法,你可以轻松解决命名冲突问题,并保持代码的清晰性。
","description":"自己学习的小demo地址放在这里,仅供学习参考,flutter学习demo 在 Flutter 中封装一个 Router.dart 文件来统一管理页面跳转是一个非常实用的做法,可以提升代码的可维护性和复用性。下面我将为你提供一个结构清晰、功能全面的 Router 类封装示例,并附上使用说明。\\n\\n✅ 1. 创建 Router.dart 文件\\n// lib/router/router.dart\\n\\nimport \'package:flutter/material.dart\';\\n\\nclass Router {\\n // 路由表…","guid":"https://juejin.cn/post/7504248589687095296","author":"90后晨仔","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-15T06:28:13.631Z","media":null,"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 热重载、热重启和冷重启","url":"https://juejin.cn/post/7504198708516093967","content":"在Flutter 中 ListView
组件是Flutter中的核心组件之一,在日常开发中也是使用比较频繁的一个组件,相比Column
和row
,它自带滚动效果,而且还支持动态加载数据。它非常适合展示长列表数据,因为它只构建那些当前可见的项,这大大提高了性能。
ListView
是 Flutter 中最常用的滚动列表组件,用于高效展示大量数据。支持垂直和水平滚动,提供多种构造方法适应不同场景。
ListView()
直接传入子组件列表,适合少量静态数据:
\\nListView(\\n children: <Widget>[\\n ListTile(title: Text(\\"Item 1\\")),\\n ListTile(title: Text(\\"Item 2\\")),\\n // ...\\n ],\\n)\\n
\\nListView.builder()
通过索引动态生成子项,适合大数据集(懒加载):
\\nListView.builder(\\n itemCount: 100, // 总项数\\n itemBuilder: (context, index) {\\n return ListTile(title: Text(\\"Item $index\\"));\\n },\\n)\\n
\\nListView.separated()
自动添加自定义分隔线,适合需要间隔的列表:
\\nListView.separated(\\n itemCount: 50,\\n separatorBuilder: (_, __) => Divider(height: 1),\\n itemBuilder: (_, index) => ListTile(title: Text(\\"Item $index\\")),\\n)\\n
\\nListView.custom()
完全控制子组件布局,需指定 childrenDelegate
:\\n通过自己的逻辑去展示子组件,尤其是针对一些包含多种不同类型布局的列表。
ListView.custom(\\n childrenDelegate: SliverChildBuilderDelegate((_, index) {\\n if (index % 2 == 0) {\\n return Container(\\n color: Colors.green,\\n alignment: Alignment.centerLeft,\\n height: 60,\\n child: Text(\'Item$index\'),\\n );\\n } else {\\n return ListTile(title: Text(\\"Item $index\\"));\\n }\\n }, childCount: 100),\\n),\\n
\\nscrollDirection
Axis.vertical
(默认垂直滚动)Axis.horizontal
(水平滚动)ListView(\\n scrollDirection: Axis.horizontal,\\n children: [Container(width: 100), Container(width: 100)],\\n)\\n
\\ncontroller
通过 ScrollController
实现滚动监听或跳转:
final controller = ScrollController();\\n\\nListView(\\n controller: controller,\\n children: [...],\\n)\\n\\n// 跳转到底部\\ncontroller.jumpTo(controller.position.maxScrollExtent);\\n
\\nphysics
控制滚动行为:
\\nBouncingScrollPhysics()
:iOS 弹性效果ClampingScrollPhysics()
:Android 夹紧效果NeverScrollableScrollPhysics()
:禁用滚动padding
设置列表内容的内边距:
\\nListView(padding: EdgeInsets.all(16))\\n
\\nshrinkWrap
解决嵌套滚动时的尺寸冲突(默认 false
):\\n有些场景,比如要在column里使用ListView的时候,必须把这个参数设置为true,否则会报错。
Column(\\n children: [\\n ListView(\\n // shrinkWrap: true, 这个注释掉就报错了\\n children: <Widget>[\\n ListTile(title: Text(\\"Item 1\\")),\\n ListTile(title: Text(\\"Item 2\\")),\\n // ...\\n ],\\n ),\\n ],\\n),\\n
\\n报错信息
\\ncacheExtent
设置预加载区域的高度(像素):
\\nListView(cacheExtent: 500) // 提升滚动流畅度\\n
\\nRefreshIndicator
和ScrollController
使用实现分页加载
和下拉刷新
// 通过监听滚动位置判断是否要加载更多数据。\\n void _scrollListener() {\\n if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent && !_isLoading) {\\n _loadData();\\n }\\n }\\n
\\nRefreshIndicator(\\n onRefresh: () => _loadData(isRefresh: true), //刷新数据的关键方法\\n child: ListView.builder(\\n controller: _scrollController,\\n itemCount: _items.length + (_isLoading ? 1 : 0), // 如果正在加载,增加一个额外的item用于显示加载指示器\\n itemBuilder: (context, index) {\\n if (index == _items.length) {\\n return Center(child: CircularProgressIndicator()); // 加载更多时的指示器\\n }\\n return ListTile(\\n title: Text(_items[index]),\\n );\\n },\\n ),\\n),\\n
\\nclass _SearchPageState extends State<SearchPage> {\\n\\n final ScrollController _scrollController = ScrollController();\\n final List<String> _items = List.generate(20, (index) => \\"Item ${index + 1}\\");\\n bool _isLoading = false;\\n\\n @override\\n void initState() {\\n super.initState();\\n // 监听滚动事件以便实现上拉加载更多\\n _scrollController.addListener(_scrollListener);\\n }\\n\\n @override\\n void dispose() {\\n _scrollController.removeListener(_scrollListener);\\n _scrollController.dispose();\\n super.dispose();\\n }\\n\\n // 模拟数据加载\\n Future<void> _loadData({bool isRefresh = false}) async {\\n if (!isRefresh) {\\n setState(() {\\n _isLoading = true;\\n });\\n }\\n // 模拟网络请求延迟\\n await Future.delayed(Duration(seconds: 2));\\n if (isRefresh) {\\n setState(() {\\n _items.clear();\\n _items.addAll(List.generate(20, (index) => \\"Refreshed Item ${index + 1}\\"));\\n });\\n } else {\\n setState(() {\\n _items.addAll(List.generate(10, (index) => \\"Item ${_items.length + index + 1}\\"));\\n });\\n }\\n setState(() {\\n _isLoading = false;\\n });\\n }\\n\\n// 通过监听滚动位置判断是否要加载更多数据。\\n void _scrollListener() {\\n if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent && !_isLoading) {\\n _loadData();\\n }\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: Text(\'下拉刷新与上拉加载示例\'),\\n ),\\n body: RefreshIndicator(\\n onRefresh: () => _loadData(isRefresh: true),\\n child: ListView.builder(\\n controller: _scrollController,\\n itemCount: _items.length + (_isLoading ? 1 : 0), // 如果正在加载,增加一个额外的item用于显示加载指示器\\n itemBuilder: (context, index) {\\n if (index == _items.length) {\\n return Center(child: CircularProgressIndicator()); // 加载更多时的指示器\\n }\\n return ListTile(\\n title: Text(_items[index]),\\n );\\n },\\n ),\\n ),\\n );\\n }\\n}\\n
\\nconst
构造函数减少 Widget 重建开销:
\\nListView(\\n children: [\\n const ListTile(title: Text(\\"Fixed Item 1\\")),\\n const ListTile(title: Text(\\"Fixed Item 2\\")),\\n ],\\n)\\n
\\n使用 ListView.builder
+ Image.network
的 loadingBuilder
:
ListView.builder(\\n itemBuilder: (_, index) {\\n return Image.network(\\n imageUrls[index],\\n loadingBuilder: (_, child, progress) {\\n return progress == null ? child : CircularProgressIndicator();\\n },\\n );\\n },\\n)\\n
\\n使用 AutomaticKeepAliveClientMixin
:
class KeepAliveItem extends StatefulWidget {\\n @override\\n _KeepAliveItemState createState() => _KeepAliveItemState();\\n}\\n\\nclass _KeepAliveItemState extends State<KeepAliveItem>\\n with AutomaticKeepAliveClientMixin {\\n @override\\n bool get wantKeepAlive => true; // 保持状态\\n\\n @override\\n Widget build(BuildContext context) {\\n super.build(context);\\n return ListTile(...);\\n }\\n}\\n
\\n聊天列表(反向滚动)
\\n让数据从下往上展示
\\nListView.builder(\\n reverse: true, // 从底部开始\\n itemCount: messages.length,\\n itemBuilder: (_, index) => ChatBubble(message: messages[index]),\\n)\\n\\nWidget ChatBubble({Map? message}) {\\n if(message?[\'type\'] == \'2\') {\\n return Container(\\n width: 200,\\n height: 60,\\n decoration: BoxDecoration(\\n color: Colors.green,\\n border: Border.all(color: Colors.green, width: 1)\\n ),\\n alignment: Alignment.centerLeft,\\n child: Text(message?[\'message\'], style: TextStyle(color: Colors.white),),\\n );\\n } else {\\n return Container(\\n width: 300,\\n height: 60,\\n alignment: Alignment.centerRight,\\n child: Text(message?[\'message\'], textAlign: TextAlign.right,),\\n );\\n }\\n}\\n
\\nListView
是 Flutter 中处理滚动列表的核心组件,通过合理选择构造方法(builder
/separated
/custom
)和优化手段(const
/AutomaticKeepAlive
),可以高效处理动态数据和大数据集。关键注意点:
ListView.builder
避免内存溢出CustomScrollView
cacheExtent
和 itemExtent
提升性能RefreshIndicator
和分页逻辑实现完整数据流本文隶属于 《Flutter 状态管理: 源码探索与实战》 小册的番外篇。最近在折腾我的 迷你版 PS, 其中左侧菜单栏的实现,感觉是一个比较好的状态管理应用场景。所以单独出一篇文章分析其实现的细节:
\\n我曾经一度为文章、视频等封面图所困扰。我的封面一般比较简单,某一系列使用一类封面图,只是改改文字就行了。虽然用 PhotoShop 可以做,但是太重了,而且不支持移动端、网页版。Flutter 作为一个全平台的应用开发框架,可以快速构建多端的软件产品。
\\n所以我为自己设立一个小目标,先从 绘制封面图 为起点,来迭代出一个小巧版 PS ,并支持 Windows、Macos、Android、iOS、Linux 和 Web 六大主流平台,项目代号 pix
。
\\n本文的焦点是左侧的菜单工具条,探讨一下如何管理和维护其中的数据:
对于状态管理来说,最重要的是知道管理的目标。可能会有人说,这不就是一个图标列表嘛,有什么好管理的? 如果仔细分析交互的需求,就可以发现按钮分为三类 :
\\n[1]
. 普通按钮,只响应点击事件,比如返回、保存按钮。[2]
. 可选中按钮,点击时可以切换激活与不激活状态。比如是否绘制网格、是否展示右侧面版。[3]
. 可选中按钮组,按钮组中只有一个可被选中,选中一个,组中其他的按键需要取消激活。基于这三点分析,设计一个 MenuAction
密封类负责承载菜单的视图数据。
sealed class MenuAction {\\n final String id;\\n final IconData icon;\\n\\n MenuAction({required this.id, required this.icon});\\n\\n bool get checked;\\n}\\n
\\nSelectableMenu
继承自 MenuAction
,指可以被选中的按钮,其中的 group 字段用于表示菜单组:
class SelectableMenu extends MenuAction {\\n @override\\n final bool checked;\\n final String? group;\\n\\n SelectableMenu(\\n this.checked, {\\n required super.id,\\n required super.icon,\\n this.group,\\n });\\n}\\n
\\n所以目前来说,左侧的菜单栏状态数据是 List<MenuAction>
列表,其中记录了构建界面所需的所有数据。
这里通过 flutter_bloc 实现状态管理,这种简单的状态数据可以交给 Cubit
, 如下所示,创建 MenusBloc 类继承自 Cubit<List<MenuAction>>
, 在入参中传入 List<MenuAction>
数据作为菜单的初始数据:
class MenusBloc extends Cubit<List<MenuAction>> {\\n final List<MenuAction> menus;\\n MenusBloc({required this.menus}) : super(menus);\\n\\n\\n void changeSelect(SelectableMenu menu) {\\n // TODO 处理选择的菜单项\\n }\\n}\\n
\\n然后再视图的上级通过 MultiBlocProvider
向下层组件树提供 MenusBloc
,这里整体的界面是 ProjectView
:
MultiBlocProvider(providers: [\\n // 其他的 bloc\\n BlocProvider(create: (BuildContext context) {\\n return MenusBloc(menus: menus);\\n }),\\n], child: const ProjectView()\\n\\nList<MenuAction> get menus => [\\n ClickMenu(id: ActionType.back.name, icon: Icons.arrow_back),\\n SelectableMenu(false, id: ActionType.grid.name, icon: Icons.grid_3x3_outlined),\\n SelectableMenu(true, group: \'pointer\', id: ActionType.move.name, icon: CupertinoIcons.arrow_up_left),\\n SelectableMenu(false, group: \'pointer\', id: ActionType.painter.name, icon: CupertinoIcons.pen),\\n SelectableMenu(false, group: \'pointer\', id: ActionType.eraser.name, icon: Icons.e_mobiledata),\\n SelectableMenu(false, id: ActionType.detail.name, icon: Icons.padding),\\n ClickMenu(id: ActionType.reset.name, icon: Icons.my_location),\\n ClickMenu(id: ActionType.save.name, icon: Icons.save_alt),\\n];\\n
\\n下层的组件可以通过上下文访问 MenusBloc 的状态数据,这里左侧菜单栏数据通过 MenuActionBar
类构建,通过 onTap
回调,通知外界按钮的点击事件,处理相关逻辑。
\\ncontext.select 可以选择状态类中的某一字段数据。仅当该字段数据变化时,才会通知组件重新构建,可以更细粒度地掌控视图的更新时机。
class MenuActionBar extends StatelessWidget {\\n final ValueChanged<MenuAction> onTap;\\n\\n const MenuActionBar({super.key, required this.onTap});\\n\\n @override\\n Widget build(BuildContext context) {\\n List<MenuAction> menus = context.select((MenusBloc bloc) => bloc.state);\\n return SizedBox(\\n width: 36,\\n child: Padding(\\n padding: const EdgeInsets.symmetric(vertical: 8.0),\\n child: Column(\\n spacing: 6,\\n children: menus.map((e) => TolyAction(\\n selected: e.checked,\\n child: Icon(e.icon, size: 16),\\n onTap: () => _handleAction(context, e),\\n )).toList(),\\n ),\\n ),\\n );\\n }\\n
\\n按钮的点击事件触发 _handleAction
,首先触发 onTap 回调通知外界菜单事件,然后如果按钮是 SelectableMenu
,触发 MenusBloc#changeSelect
方法,修改按钮的选中状态:
void _handleAction(BuildContext context, MenuAction menu) {\\n onTap(menu);\\n if (menu is SelectableMenu) {\\n context.read<MenusBloc>().changeSelect(menu);\\n }\\n}\\n
\\n拿切换网格辅助线来说:当非激活状态时,点击按钮。需要修改 MenusBloc
中对应数据的激活状态:
可以寻找到状态列表中对应的菜单项,将其移除,并加入相反激活状态的菜单项。通过定义 copyWith 方法,可以快速基于当前对象,创建另一个属性稍有不同的对象:
\\nvoid changeSelect(SelectableMenu menu) {\\n List<MenuAction> menus = state.toList();\\n int index = menus.indexWhere((e) => e.id == menu.id);\\n if (index != -1) {\\n menus.removeAt(index);\\n menus.insert(index, menu.copyWith(checked: !menu.checked));\\n if (menu.group != null) {\\n handleActionGroup(menus, menu);\\n }\\n emit(menus);\\n }\\n}\\n
\\n当遇到点击的菜单具有 group
时,表示是按钮组。此时需要取消激活其他。如下所示,第三、四、五个菜单用于激活鼠标的某种交互操作,彼此是互斥的。一个激活,其他两个都需要取消激活。
在逻辑上,可以从 menus 列表中寻找到当前组中的其他元素,将他们移除,并在对应位置添加非激活状态的菜单即可。
\\nvoid handleActionGroup(List<MenuAction> menus, SelectableMenu menu) {\\n Iterable<MenuAction> groupMenu = menus.where((e) =>\\n (e is SelectableMenu) && e.id != menu.id && e.group == menu.group);\\n for (MenuAction action in groupMenu) {\\n int index = menus.indexWhere((e) => e.id == action.id);\\n if (index != -1) {\\n MenuAction action = menus.removeAt(index);\\n if (action is SelectableMenu) {\\n menus.insert(index, action.copyWith(checked: false));\\n }\\n }\\n }\\n}\\n
\\n在 MenusBloc 中处理完毕后,产出新的状态,flutter_bloc 框架会自动通知,对应 select 访问的上下文,触发从新构建,达到视图的更新。
\\n有了状态数据的处理逻辑,你完全可以使用任何状态管理手段来达成相同的效果,用你喜欢的状态管理库试试吧 ~
\\n这样 MenusBloc 负责维护状态数据,以及数据的更新,产出状态。可以将 数据处理逻辑
和 界面构建逻辑
分离。后续还可以进行一些更复杂的拓展,比如:
这些功能后面做的话,也会通过番外篇的形式在这里跟大家简面,敬请期待 ~\\n状态管理在菜单栏的应用
","description":"本文隶属于 《Flutter 状态管理: 源码探索与实战》 小册的番外篇。最近在折腾我的 迷你版 PS, 其中左侧菜单栏的实现,感觉是一个比较好的状态管理应用场景。所以单独出一篇文章分析其实现的细节: 1. pix 项目背景\\n\\n我曾经一度为文章、视频等封面图所困扰。我的封面一般比较简单,某一系列使用一类封面图,只是改改文字就行了。虽然用 PhotoShop 可以做,但是太重了,而且不支持移动端、网页版。Flutter 作为一个全平台的应用开发框架,可以快速构建多端的软件产品。\\n 所以我为自己设立一个小目标,先从 绘制封面图 为起点…","guid":"https://juejin.cn/post/7504163034870661170","author":"张风捷特烈","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-14T22:59:00.416Z","media":[{"url":"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7a54274ddc994430ae4742ceca33ccfe~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1920&h=1200&s=446524&e=png&b=fefbfb","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/48be8e359c58485eaa09bda835b8c277~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=818&h=614&s=3106548&e=gif&f=246&b=031e3d","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f4f91fa37f66489387562809380b63b6~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1361&h=1010&s=157420&e=png&b=041f3b","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/398ec3a0f50d4504bcd2e2245d5d0d5a~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=818&h=607&s=276965&e=gif&f=30&b=031e3d","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/52fdd6e1edc24a41806dc524c90509ed~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1033&h=369&s=37434&e=png&b=ffffff","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/39261196e6984090a1d6611f9421711c~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=818&h=607&s=69853&e=gif&f=48&b=031e3d","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a94bbeae60a74820aaa278cad22e63a4~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1452&h=489&s=128380&e=png&b=ffffff","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7b254fa2f99b43b08987f4962c6dc031~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=826&h=183&s=17536&e=png&b=232323","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Canvas"],"attachments":null,"extra":null,"language":null},{"title":"Flutter中的context:深入理解原理与实践","url":"https://juejin.cn/post/7504128296789327907","content":"在使用 Flutter 过程中,context
是一个几乎无处不在的概念。你可能在编写组件时频繁使用它,例如:
Navigator.of(context).push(...);\\nScaffoldMessenger.of(context).showSnackBar(...);\\nTheme.of(context).textTheme;\\n
\\n刚接触 Flutter 时,很多开发者都会疑惑:
\\ncontext
到底是什么?context
?context
吗?这篇文章将带你从实战、源码、底层原理等多个角度来系统理解 Flutter 中的 context
。
从官方文档的定义出发:
\\nA handle to the location of a widget in the widget tree.
\\ncontext
就是Widget在Widget树中的位置标识。它是用于在树中向上查找父级信息的关键凭证。
context
指的是BuildContext
,是一个抽象类,它的实现由框架自动在 Widget 插入树时完成,其底层是对 Element
的封装。
abstract class BuildContext {\\n Widget get widget;\\n BuildContext? get parent;\\n}\\n
\\n本质上,context
是 Widget 所属 Element
的引用,而 Element 是 Flutter 构建系统的核心节点。
常见方法如:
\\nTheme.of(context)
Navigator.of(context)
ScaffoldMessenger.of(context)
MediaQuery.of(context)
它们的共同点是:通过 context 沿 Widget 树向上查找某个特定类型的祖先 Widget 或 InheritedWidget。
\\nThemeData theme = Theme.of(context); // 查找最近的 Theme widget\\nNavigator.of(context).push(...); // 查找最近的 Navigator widget\\n
\\n只有当前 Widget 所在位置的上层存在这些组件,调用才有效,否则将抛出异常或返回 null。
\\n@override\\nWidget build(BuildContext context) {\\n return Scaffold(...);\\n}\\n
\\n这是我们最常用的场景。每个 Widget 都在构建时接收到一个 context,这个 context 是当前 widget 的 element 所提供的,可以通过它访问 widget 树的状态。
\\n这些 UI 组件依赖当前树中已有的祖先结构(如 Scaffold、Navigator),必须通过 context 来定位这些结构,否则无法正常展示。
\\nScaffoldMessenger.of(context).showSnackBar(\\n SnackBar(content: Text(\'Hello\')),\\n);\\n
\\n很多状态管理方案如 Provider、Riverpod 都基于 InheritedWidget 原理,它们通过 context 获取祖先注入的数据:
\\nfinal model = Provider.of<SomeModel>(context);\\n
\\n@override\\nvoid initState() {\\n super.initState();\\n Navigator.of(context).push(...); // 会报错,context 尚未 attach\\n}\\n
\\n在 initState
中,Element 还未插入到树形结构中,context 是无效的。
@override\\nvoid initState() {\\n super.initState();\\n WidgetsBinding.instance.addPostFrameCallback((_) {\\n Navigator.of(context).push(...); // 此时树已构建完成\\n });\\n}\\n
\\nScaffoldMessenger.of(context).showSnackBar(...); // 找不到 Scaffold\\n
\\n使用 Builder 创建新的 context
\\nBuilder(\\n builder: (context) => ElevatedButton(\\n onPressed: () {\\n ScaffoldMessenger.of(context).showSnackBar(...); // ✅ 正确\\n },\\n child: Text(\'Show\'),\\n ),\\n);\\n
\\n从源码角度讲,BuildContext
是一个接口,其实现由各种类型的 Element
来完成。
Flutter 的构建系统是围绕以下三个核心类:
\\n每个 Widget 在插入 Widget 树时,都会创建对应的 Element,这些 Element 构成 Element 树。而 context 实际上就是当前 Widget 所对应的 Element 实例。
\\nprint(context.runtimeType); \\n
\\n在不同 Widget 中运行,输出的类型可能是:
\\nStatelessElement
StatefulElement
RenderObjectElement
这说明:context 实际就是当前 Element 的引用。
\\n在某些场景下我们需要访问祖先的 State
对象,例如从子 Widget 中访问父 Widget 的方法:
final state = context.findAncestorStateOfType<_MyState>();\\n
\\n或者访问一个自定义的 InheritedWidget:
\\nfinal inherited = context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>();\\n
\\n内容 | 说明 |
---|---|
context 是什么? | Widget 在 Widget 树中的定位标识(Element 的引用) |
有什么用? | 查找祖先 Widget、状态、InheritedWidget,操作树结构 |
使用注意? | 不能在 initState 中使用,需要在树构建完成后使用 |
本质? | 是当前 Widget 所属 Element 的引用,是访问 Widget 树的钥匙 |
理解 BuildContext
是深入理解 Flutter 构建机制的关键一步。它不仅是 UI 构建时的“位置凭证”,更是 Widget 树中状态管理、资源共享、导航控制等机制的核心依赖。
在应用开发中,状态管理是一个核心议题。随着应用规模的扩大和业务逻辑的复杂化,我们对状态管理方案的需求也在不断演进。这种演进并非一蹴而就,更像是一种“自然生长”的过程:从简单场景下的直接处理,到引入专门的工具,再到采纳成熟的架构模式,每一步都是为了应对新的挑战,提升代码的可维护性、可测试性和可扩展性。
\\n本文将回顾 Flutter 状态管理方案的典型演进路径,探讨不同阶段的特点、适用场景以及它们如何为下一阶段的“生长”奠定基础。
\\n场景描述: 当某项数据是只读的,并且只在单个 Widget 中使用时。
\\n在应用的早期阶段或处理一些简单页面时,我们可能遇到这样的需求:展示一个从网络获取的、一次性加载且不会变化的信息,比如一个票务详情页。
\\n解决方案:\\n最直接的方式是在 StatelessWidget
内部定义一个网络请求方法,并结合 FutureBuilder
来处理异步数据流,根据 Future
的状态(加载中、完成、错误)构建不同的 UI。
import \'package:flutter/material.dart\';\\n\\nclass TicketInfo extends StatelessWidget {\\n final String ticketId;\\n\\n TicketInfo({required this.ticketId});\\n\\n // 模拟网络请求\\n Future<String> fetchInfo(String tickId) async {\\n await Future.delayed(Duration(seconds: 2)); // 模拟网络延迟\\n if (tickId == \\"123\\") {\\n return \'Ticket Info for ID $tickId: VIP Seat, Row A, Number 10\';\\n }\\n return \'Ticket not found.\';\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return FutureBuilder<String>(\\n future: fetchInfo(ticketId), // 直接调用请求方法\\n builder: (BuildContext context, AsyncSnapshot<String> snapshot) {\\n if (snapshot.connectionState == ConnectionState.waiting) {\\n return Center(child: CircularProgressIndicator());\\n } else if (snapshot.hasError) {\\n return Center(child: Text(\'Error: ${snapshot.error}\'));\\n } else if (snapshot.hasData) {\\n return Padding(\\n padding: const EdgeInsets.all(16.0),\\n child: Text(\\n snapshot.data!,\\n style: TextStyle(fontSize: 18),\\n ),\\n );\\n } else {\\n return Center(child: Text(\'No data.\'));\\n }\\n },\\n );\\n }\\n}\\n\\n// 示例用法\\n// class MyApp extends StatelessWidget {\\n// @override\\n// Widget build(BuildContext context) {\\n// return MaterialApp(\\n// home: Scaffold(\\n// appBar: AppBar(title: Text(\'Ticket Details\')),\\n// body: TicketInfo(ticketId: \'123\'),\\n// ),\\n// );\\n// }\\n// }\\n
\\n优点:
\\n局限:
\\nfetchInfo
的逻辑或数据,难以共享。FutureBuilder
适用于一次性加载。如果数据需要刷新或响应用户交互而改变,则力不从心。这种方法是状态管理的“萌芽”,适用于非常简单的只读场景。
\\n场景描述: 当你需要根据用户行为或其他事件修改组件状态时。
\\n随着业务逻辑的增加,Widget 不再仅仅是静态展示。用户交互(如点击按钮)、数据流更新(如实时消息)等都需要 Widget 能够响应并更新其 UI。
\\n解决方案:\\nStatefulWidget
及其关联的 State
对象应运而生。State
对象可以持有可变状态,并通过调用 setState()
方法来通知 Flutter 框架该 Widget 的子树需要重建。如果状态的更新是基于连续的异步事件流(例如来自 WebSocket 或 Firebase 的数据),StreamBuilder
是一个理想的选择。
import \'dart:async\';\\nimport \'package:flutter/material.dart\';\\n\\nclass InteractiveCounter extends StatefulWidget {\\n @override\\n _InteractiveCounterState createState() => _InteractiveCounterState();\\n}\\n\\nclass _InteractiveCounterState extends State<InteractiveCounter> {\\n int _counter = 0;\\n late StreamController<int> _streamController;\\n Stream<int> get _counterStream => _streamController.stream;\\n\\n @override\\n void initState() {\\n super.initState();\\n _streamController = StreamController<int>();\\n // 模拟一个外部事件源,每秒增加计数\\n Timer.periodic(Duration(seconds: 1), (timer) {\\n if (!_streamController.isClosed) {\\n _streamController.add(_counter++);\\n } else {\\n timer.cancel();\\n }\\n });\\n }\\n\\n void _incrementCounter() {\\n // setState(() { // 如果只是简单的本地状态改变,可以用setState\\n // _counter++;\\n // });\\n // 通过StreamController发送事件,StreamBuilder会监听\\n _streamController.add(++_counter);\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Column(\\n mainAxisAlignment: MainAxisAlignment.center,\\n children: <Widget>[\\n Text(\'Counter (via StreamBuilder):\'),\\n StreamBuilder<int>(\\n stream: _counterStream,\\n initialData: 0,\\n builder: (context, snapshot) {\\n return Text(\\n \'${snapshot.data}\',\\n style: Theme.of(context).textTheme.headlineMedium,\\n );\\n },\\n ),\\n SizedBox(height: 20),\\n ElevatedButton(\\n onPressed: _incrementCounter, // 外部行为触发状态改变\\n child: Text(\'Increment by Button\'),\\n ),\\n ],\\n );\\n }\\n\\n @override\\n void dispose() {\\n _streamController.close();\\n super.dispose();\\n }\\n}\\n
\\n优点:
\\nState
对象内部。局限:
\\nStatefulWidget
本身的机制(如通过构造函数传递回调)会变得复杂和笨拙,容易导致“回调地狱”或“属性钻取 (prop drilling)”。_incrementCounter
中的逻辑)仍然与 UI 代码(build
方法)存在于同一个类中。这是向更复杂状态管理迈出的第一步,解决了局部状态的动态变化问题。
\\n场景描述: 当你的状态数据需要在不同 Widget 中共享使用时。
\\n随着应用功能的丰富,状态不再是单个 Widget 的私有财产。例如,用户登录状态、主题偏好、购物车内容等,需要在应用的多个部分被访问和修改。
\\n解决方案:\\n引入专门的状态管理工具,如 Provider, Riverpod, Bloc/Cubit, GetX 等。以 Provider
为例,它利用 Flutter 的 InheritedWidget
机制,允许我们将状态“提供”给其子树中的任何 Widget。
定义状态模型 (ChangeNotifier):
\\nclass CounterModel extends ChangeNotifier {\\n int _count = 0;\\n int get count => _count;\\n\\n void increment() {\\n _count++;\\n notifyListeners(); // 通知监听者状态已改变\\n }\\n}\\n
\\n在顶层注入状态:
\\nvoid main() {\\n runApp(\\n ChangeNotifierProvider(\\n create: (context) => CounterModel(),\\n child: MyApp(),\\n ),\\n );\\n}\\n
\\n在 Widget 中消费状态:
\\nclass ConsumerWidget1 extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n // 方法1: 使用 Consumer Widget\\n return Consumer<CounterModel>(\\n builder: (context, counter, child) => Text(\'Count: ${counter.count}\'),\\n );\\n }\\n}\\n\\nclass ConsumerWidget2 extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n // 方法2: 使用 context.watch (需要 provider 5.0+)\\n final counter = context.watch<CounterModel>();\\n return ElevatedButton(\\n onPressed: () => context.read<CounterModel>().increment(), // context.read 用于调用方法\\n child: Text(\'Increment from Widget 2. Current: ${counter.count}\'),\\n );\\n }\\n}\\n
\\n优点:
\\n局限:
\\nChangeNotifier
或直接在 Widget 中调用 ChangeNotifier
的方法。当业务逻辑变得复杂且需要在多处复用时,这种方式可能不够清晰。ChangeNotifier
本身是可测试的,但如果它包含了大量的业务逻辑,尤其是与外部服务(如API调用)的交互,测试可能会变得复杂。Provider
等工具极大地改善了状态共享问题,是 Flutter 开发中非常流行的选择。
场景描述: 当你的多个相同业务逻辑需要在不同的 Widget 中共享使用时,或者当 Widget 中的业务逻辑变得过于复杂时。
\\n当应用增长到一定规模,我们不仅需要共享状态,还需要共享和复用处理这些状态的业务逻辑。同时,为了保持 Widget 的简洁(只负责 UI 展示和用户输入),需要将业务逻辑抽离出来。
\\n解决方案:\\n引入 MVVM (Model-View-ViewModel) 架构模式。
\\nUser
, Product
)。ChangeNotifier
或类似机制来通知 View 更新。// Model (与之前类似,或更复杂的数据结构)\\nclass User {\\n final String id;\\n final String name;\\n User({required this.id, required this.name});\\n}\\n\\n// ViewModel\\nclass UserViewModel extends ChangeNotifier {\\n User? _user;\\n User? get user => _user;\\n\\n bool _isLoading = false;\\n bool get isLoading => _isLoading;\\n\\n String? _error;\\n String? get error => _error;\\n\\n // 模拟API请求\\n Future<void> fetchUser(String userId) async {\\n _isLoading = true;\\n _error = null;\\n notifyListeners();\\n\\n try {\\n // 模拟网络请求\\n await Future.delayed(Duration(seconds: 1));\\n if (userId == \\"1\\") {\\n _user = User(id: \\"1\\", name: \\"Alice\\");\\n } else {\\n throw Exception(\\"User not found\\");\\n }\\n } catch (e) {\\n _error = e.toString();\\n } finally {\\n _isLoading = false;\\n notifyListeners();\\n }\\n }\\n}\\n\\n// View (使用 UserViewModel)\\nclass UserProfileView extends StatelessWidget {\\n final String userId;\\n UserProfileView({required this.userId});\\n\\n @override\\n Widget build(BuildContext context) {\\n // 假设 UserViewModel 已通过 Provider 在上层提供\\n // final userViewModel = context.watch<UserViewModel>(); // 监听变化\\n // 或者在 StatefulWidget 中:\\n // @override\\n // void initState() {\\n // super.initState();\\n // // 在 initState 中获取 Provider 实例并调用方法,但不监听\\n // // 通常通过 Provider.of<UserViewModel>(context, listen: false).fetchUser(userId);\\n // // 或者使用专门的 Consumer/Selector 来管理生命周期和重建\\n // }\\n //\\n // 为了演示,这里简化为直接创建一个,但在实际项目中应由Provider管理\\n return ChangeNotifierProvider(\\n create: (_) => UserViewModel()..fetchUser(userId), // 创建并立即获取用户\\n child: Consumer<UserViewModel>(\\n builder: (context, viewModel, child) {\\n if (viewModel.isLoading) {\\n return Center(child: CircularProgressIndicator());\\n }\\n if (viewModel.error != null) {\\n return Center(child: Text(\'Error: ${viewModel.error}\'));\\n }\\n if (viewModel.user != null) {\\n return Text(\'User: ${viewModel.user!.name}\');\\n }\\n return Center(child: Text(\'No user data.\'));\\n },\\n ),\\n );\\n }\\n}\\n
\\n优点:
\\n局限:
\\nMVVM 是一个重要的里程碑,它为构建可维护的大型应用提供了坚实的结构基础。
\\n场景描述: 当你的多个相同数据需要在不同的 ViewModel 中共享使用时,或者当数据获取逻辑(包括缓存策略、远程/本地数据源选择)变得复杂时。
\\n在 MVVM 的基础上,如果多个 ViewModel 都需要访问同一种数据(例如用户信息、产品列表),那么在每个 ViewModel 中都实现一遍数据获取逻辑(如 API 调用、JSON 解析)会造成代码重复,并且难以统一管理数据源。
\\n解决方案:\\n引入 Repository (仓库) 模式。Repository 负责封装与特定数据类型相关的所有数据操作逻辑,为 ViewModel 提供一个统一的数据访问接口,屏蔽了数据来源的细节(是来自网络、本地缓存还是数据库)。
\\n// Model (同上)\\n// User\\n\\n// Repository Interface (可选,但推荐用于解耦和测试)\\nabstract class UserRepository {\\n Future<User> getUser(String userId);\\n // Future<void> updateUser(User user);\\n}\\n\\n// Repository Implementation\\nclass UserRepositoryImpl implements UserRepository {\\n // 伪API客户端\\n final ApiClient _apiClient = ApiClient();\\n final LocalCache _localCache = LocalCache();\\n\\n @override\\n Future<User> getUser(String userId) async {\\n try {\\n // 尝试从缓存获取\\n User? cachedUser = _localCache.getUser(userId);\\n if (cachedUser != null) return cachedUser;\\n\\n // 缓存未命中,从网络获取\\n final userData = await _apiClient.fetchUserData(userId);\\n final user = User(id: userData[\'id\'], name: userData[\'name\']);\\n _localCache.saveUser(user); // 保存到缓存\\n return user;\\n } catch (e) {\\n // 错误处理\\n throw Exception(\'Failed to get user: $e\');\\n }\\n }\\n}\\n\\n// ViewModel (现在依赖 UserRepository)\\nclass UserViewModel extends ChangeNotifier {\\n final UserRepository _userRepository; // 依赖注入\\n UserViewModel(this._userRepository);\\n\\n User? _user;\\n User? get user => _user;\\n // ... isLoading, error ...\\n\\n Future<void> fetchUser(String userId) async {\\n // _isLoading = true; notifyListeners();\\n try {\\n _user = await _userRepository.getUser(userId); // 通过Repository获取数据\\n } catch (e) {\\n // _error = e.toString();\\n } finally {\\n // _isLoading = false; notifyListeners();\\n }\\n }\\n}\\n\\n// 伪 API 和 Cache\\nclass ApiClient {\\n Future<Map<String, dynamic>> fetchUserData(String userId) async {\\n await Future.delayed(Duration(seconds: 1));\\n if (userId == \\"1\\") return {\'id\': \'1\', \'name\': \'Alice (from API)\'};\\n throw Exception(\'API User not found\');\\n }\\n}\\nclass LocalCache {\\n final Map<String, User> _cache = {};\\n User? getUser(String userId) => _cache[userId];\\n void saveUser(User user) => _cache[user.id] = User(id: user.id, name: \\"${user.name} (cached)\\");\\n}\\n
\\n优点:
\\n局限:
\\nRepository 模式有效地将数据层从业务逻辑中分离出来,是现代应用架构中非常重要的一环。
\\n场景描述: 当你的多个相同的复杂业务逻辑需要在不同的 ViewModel 中共享使用时。这些业务逻辑可能涉及多个数据源(即多个 Repository)的协调,或者包含一些不属于任何特定 Repository 或 ViewModel 的纯业务规则。
\\n当应用的核心业务逻辑变得复杂且具有高度可复用性时,直接将其放在 ViewModel 中可能会导致 ViewModel 臃肿且职责不清。例如,“用户下单”这一行为可能涉及:检查库存 (ProductRepository)、创建订单 (OrderRepository)、更新用户积分 (UserRepository)、发送通知等。
\\n解决方案:\\n引入 UseCase (用例) 或 Interactor (交互器) 层,这是 Clean Architecture 等分层架构中的一个核心概念。UseCase 代表了应用中的一个具体用户场景或业务操作。
\\n// Model, Repository (同上)\\n\\n// UseCase\\nclass GetUserProfileUseCase {\\n final UserRepository _userRepository;\\n // 可能还有其他Repository,如 UserPreferencesRepository\\n\\n GetUserProfileUseCase(this._userRepository);\\n\\n Future<UserPresentationModel> execute(String userId) async {\\n // 核心业务逻辑\\n final user = await _userRepository.getUser(userId);\\n // 可以在这里进行数据转换、组合、校验等业务逻辑\\n // 例如,根据用户类型决定展示哪些信息\\n bool isVip = user.name.contains(\\"VIP\\"); // 简化版业务逻辑\\n return UserPresentationModel(\\n displayName: \\"User: ${user.name}\\",\\n isVip: isVip,\\n );\\n }\\n}\\n\\n// ViewModel (现在依赖 UseCase)\\nclass UserViewModel extends ChangeNotifier {\\n final GetUserProfileUseCase _getUserProfileUseCase;\\n UserViewModel(this._getUserProfileUseCase);\\n\\n UserPresentationModel? _userPresentation;\\n UserPresentationModel? get userPresentation => _userPresentation;\\n // ... isLoading, error ...\\n\\n Future<void> loadUserProfile(String userId) async {\\n // _isLoading = true; notifyListeners();\\n try {\\n _userPresentation = await _getUserProfileUseCase.execute(userId);\\n } catch (e) {\\n // _error = e.toString();\\n } finally {\\n // _isLoading = false; notifyListeners();\\n }\\n }\\n}\\n\\n// 一个简单的 Presentation Model,用于适配UI显示\\nclass UserPresentationModel {\\n final String displayName;\\n final bool isVip;\\n UserPresentationModel({required this.displayName, required this.isVip});\\n}\\n
\\n优点:
\\n局限:
\\n引入 UseCase 代表了向更成熟、更健壮的架构设计迈进,特别适合大型、复杂且需要长期维护的项目。
\\nFlutter 状态管理的演进是一个从简单到复杂、从耦合到解耦的自然生长过程:
\\n这个“自然生长”的过程告诉我们,并不存在一个“最好”的状态管理方案,只有“最适合当前项目阶段和复杂度”的方案。
\\nStatefulWidget
或简单的 FutureBuilder
/StreamBuilder
可能就足够了。Provider
或 Riverpod
是优秀的选择。\\n\\n需要注意的是, MVVM是一个分水岭. 一个企业级项目, 必然会达到MVVM级别的复杂度.
\\n
如同生物进化一样,我们的代码架构也应该随着环境(需求)的变化而“自然生长”.\\n一个新项目, 没有必要立即启用 MVVM+UseCase+Repo的模式, 一个健壮、高效且易于维护的代码库需要的只是简单的开始和自然生长的空间.
\\n在 Flutter 中,页面跳转(导航)是通过 Navigator
组件管理的,其核心原理是基于 页面栈(Stack) 的机制。以下是 Flutter 中常见的跳转方式,从使用场景、实现方式和底层原理三个角度进行总结:
Navigator.push
/ Navigator.pop
// 跳转到新页面\\nNavigator.push(\\n context,\\n MaterialPageRoute(builder: (context) => SecondPage()),\\n);\\n\\n// 返回上一页\\nNavigator.pop(context);\\n
\\nNavigator
是一个管理页面栈的组件,push
会将新页面压入栈顶并显示,pop
会移除当前页面并显示前一个页面。Route
对象(如 MaterialPageRoute
)。Navigator.pushReplacement
Navigator.pushReplacement(\\n context,\\n MaterialPageRoute(builder: (context) => HomePage()),\\n);\\n
\\nNavigator.pushAndRemoveUntil
Navigator.pushAndRemoveUntil(\\n context,\\n MaterialPageRoute(builder: (context) => LoginPage()),\\n (route) => false, // 清除所有页面\\n);\\n
\\nuntil
回调返回 true
),然后压入新页面。Navigator.pushNamed
/ Navigator.popUntil
// 配置路由表(通常在 MaterialApp 中)\\nMaterialApp(\\n routes: {\\n \'/home\': (context) => HomePage(),\\n \'/detail\': (context) => DetailPage(),\\n },\\n);\\n\\n// 跳转到命名路由\\nNavigator.pushNamed(context, \'/detail\');\\n\\n// 返回到指定页面\\nNavigator.popUntil(context, ModalRoute.withName(\'/home\'));\\n
\\nonGenerateRoute
或 routes
表映射,通过名称查找对应的 RouteBuilder
生成页面。PageRoute
class CustomPageRoute extends PageRouteBuilder {\\n final Widget page;\\n\\n CustomPageRoute({required this.page})\\n : super(\\n pageBuilder: (context, animation, secondaryAnimation) => page,\\n transitionsBuilder: (context, animation, secondaryAnimation, child) {\\n return FadeTransition(opacity: animation, child: child);\\n },\\n );\\n}\\n\\n// 使用自定义动画跳转\\nNavigator.push(context, CustomPageRoute(page: DetailPage()));\\n
\\nPageRouteBuilder
,通过 transitionsBuilder
定义动画逻辑,覆盖默认的 MaterialPageRoute
动画。使用场景:跳转页面时传递数据,并从目标页面返回结果。
\\n实现方式:
\\n// 跳转并传递参数\\nNavigator.push(\\n context,\\n MaterialPageRoute(\\n builder: (context) => DetailPage(data: \'Hello\'),\\n ),\\n);\\n\\n// 目标页面返回结果\\nNavigator.pop(context, \'Result from DetailPage\');\\n
\\n// 原页面接收结果\\nfinal result = await Navigator.push(\\n context,\\n MaterialPageRoute(builder: (context) => DetailPage()),\\n);\\nprint(result); // 输出 \'Result from DetailPage\'\\n
\\n底层原理:
\\npush
返回一个 Future
,当 pop
传递值时,Future
会解析为该值。Navigator 2.0
(Router
API)class MyRouter extends RouterDelegate {\\n @override\\n Widget build(BuildContext context) => MyHomePage();\\n\\n @override\\n Future<void> setNewRoutePath(configuration) async {}\\n\\n @override\\n Route? onGenerateRoute(RouteSettings settings) {\\n return MaterialPageRoute(builder: (context) => MyHomePage());\\n }\\n}\\n\\nMaterialApp.router(routerDelegate: MyRouter());\\n
\\nNavigator 2.0
提供更细粒度的路由控制,通过 RouterDelegate
和 RouteInformationParser
管理 URL 和页面状态。Get
库(GetX 框架)// 跳转页面\\nGet.to(DetailPage());\\n\\n// 返回上一页\\nGet.back();\\n\\n// 传递参数\\nGet.to(DetailPage(), arguments: {\'id\': 1});\\n\\n// 接收参数\\nvar id = Get.arguments[\'id\'];\\n
\\nGet
封装了 Navigator
,通过单例管理页面栈,支持依赖注入和状态管理。跳转方式 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
push /pop | 基础页面跳转 | 简单直接 | 动画固定,需手动管理参数 |
pushReplacement | 替换当前页面 | 清理页面栈 | 无法返回到被替换页面 |
pushAndRemoveUntil | 清除历史页面 | 强制跳转到指定页面 | 逻辑复杂 |
命名路由 | 大型应用统一管理 | 路由表清晰 | 需预先配置路由 |
自定义 PageRoute | 动画定制 | 灵活 | 实现复杂 |
Navigator 2.0 | 复杂路由、深度链接 | 完全控制路由状态 | 学习曲线陡峭 |
Get 库 | 快速开发、简化导航 | 语法简洁,集成状态管理 | 依赖第三方库 |
页面栈(Stack):
\\nNavigator
管理,形成一个栈结构。push
添加页面到栈顶,pop
移除当前页面。Route
对象:
Route
对象(如 MaterialPageRoute
),负责渲染页面和动画过渡。动画系统:
\\nMaterialPageRoute
使用 Hero
动画和 PageRouteBuilder
实现平台默认动画。PageRouteBuilder
的 transitionsBuilder
实现。生命周期管理:
\\nRoute.didPush()
,离开时调用 Route.didPop()
。Route.willChangeInternalState
控制。通过合理选择跳转方式,结合场景需求和项目复杂度,可以高效构建 Flutter 应用的导航逻辑。
","description":"在 Flutter 中,页面跳转(导航)是通过 Navigator 组件管理的,其核心原理是基于 页面栈(Stack) 的机制。以下是 Flutter 中常见的跳转方式,从使用场景、实现方式和底层原理三个角度进行总结: 一、基础跳转方式\\n1. Navigator.push / Navigator.pop\\n使用场景:最常见的页面跳转方式,适合简单的页面层级导航(如 A → B → C)。\\n实现方式:\\n// 跳转到新页面\\nNavigator.push(\\n context,\\n MaterialPageRoute(builder: (context) =…","guid":"https://juejin.cn/post/7504123774305271817","author":"90后晨仔","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-14T08:48:33.298Z","media":null,"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"uni-app x 正式支持鸿蒙,又一个原生级全平台框架落地","url":"https://juejin.cn/post/7503974160264069156","content":"其实在很久之前的《浅谈 uts + uvue 下的 uni-app x 是什么》我们就聊过 uni-app x ,相信在此之前大家对于 uni-app 的印象应该都是在小程序居多,虽然 uni-app 也可以打包客户端 app,甚至有基于 weex 的 nvue 支持,但是其效果只能说是“一言难尽”,而这里要聊的 uni-app x ,其实就是 DCloud 在跨平台这两年的新尝试。
\\n说起 uni-app x 其实已经被我遗忘很久了,虽然在去年的鸿蒙 Next 的发布会时 uni-app 有被提及,只是当时也没看到 uni-app x 的身影,而今天恰哈在朋友圈看到了 DCloud 开发分享的文章,才发现 uni-app x 已经完成了它全平台的最后一环:
\\n从 uni-app x 自己的定位看,官方表示:uni-app x 的目标并非简单地改进跨平台框架的性能,而是为原生应用开发提供一种统一的、跨平台的编码范式 。
\\n具体来说,就是 uni-app 不再是运行在 jscore 的跨平台框架,它是“基于 Web 技术栈开发,运行时编译为原生代码”的模式,相信这种模式大家应该也不陌生了,简单说就是:js(uts) 代码在打包时会直接编译成原生代码:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n目标平台 | uts 编译后的原生语言 |
---|---|
Android | Kotlin |
iOS | Swift |
鸿蒙 | ArkTS |
Web / 小程序 | JavaScript |
甚至极端一点说,uni-app x 可以不需要单独写插件去调用平台 API,你可以直接在 uts 代码里引用平台原生 API ,因为你的代码本质上也是会被编译成原生代码,所以 uts ≈ native code ,只是使用时需要配置上对应的条件编译(如 APP-ANDROID
、APP-IOS
)支持:
import Context from \\"android.content.Context\\";\\nimport BatteryManager from \\"android.os.BatteryManager\\";\\n\\nimport { GetBatteryInfo, GetBatteryInfoOptions, GetBatteryInfoSuccess, GetBatteryInfoResult, GetBatteryInfoSync } from \'../interface.uts\'\\nimport IntentFilter from \'android.content.IntentFilter\';\\nimport Intent from \'android.content.Intent\';\\n\\nimport { GetBatteryInfoFailImpl } from \'../unierror\';\\n\\n/**\\n * 获取电量\\n */\\nexport const getBatteryInfo : GetBatteryInfo = function (options : GetBatteryInfoOptions) {\\n const context = UTSAndroid.getAppContext();\\n if (context != null) {\\n const manager = context.getSystemService(\\n Context.BATTERY_SERVICE\\n ) as BatteryManager;\\n const level = manager.getIntProperty(\\n BatteryManager.BATTERY_PROPERTY_CAPACITY\\n );\\n\\n let ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);\\n let batteryStatus = context.registerReceiver(null, ifilter);\\n let status = batteryStatus?.getIntExtra(BatteryManager.EXTRA_STATUS, -1);\\n let isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL;\\n\\n const res : GetBatteryInfoSuccess = {\\n errMsg: \'getBatteryInfo:ok\',\\n level,\\n isCharging: isCharging\\n }\\n options.success?.(res)\\n options.complete?.(res)\\n } else {\\n let res = new GetBatteryInfoFailImpl(1001);\\n options.fail?.(res)\\n options.complete?.(res)\\n }\\n}\\n\\n
\\n\\n\\n比如上方代码,通过
\\nimport BatteryManager from \\"android.os.BatteryManager\\"
可以直接导入使用 Android 的BatteryManager
对象。
甚至你可以直接在 uts 里直接实现 OnClickListener
接口:
import OnClickListener from \'android.view.View.OnClickListener\';\\n// 实现 OnClickListener 接口\\nclass User {\\n name:string = \\"name\\"\\n}\\n\\nclass StartBroadcastListener extends User implements OnClickListener{\\n\\n override onClick(v?: View):void{\\n\\n let myReceiver = new ScreenReceiver();\\n let filter = new IntentFilter();\\n filter.addAction(Intent.ACTION_SCREEN_OFF);\\n filter.addAction(Intent.ACTION_SCREEN_ON);\\n UTSAndroid.getUniActivity()!.registerReceiver(myReceiver, filter);\\n\\n // 提示屏幕状态监听已经注册\\n Toast.makeText(UTSAndroid.getAppContext(),\\"屏幕状态监听已注册,注意观察控制台日志\\",Toast.LENGTH_LONG).show();\\n\\n }\\n}\\n\\n\\n// 使用\\nlet btn_start_screen_listen = this.findViewById<Button>(R.id.btn_start_screen_listen);\\nbtn_start_screen_listen.setOnClickListener(new StartBroadcastListener());\\n
\\n或者直接在 iOS 平台直接获取当前 app 显示的 UIViewController ,并打开 alert 弹窗:
\\nimport { UTSiOS } from \\"DCloudUTSFoundation\\"\\n\\nexport function showAlert(title: string|null, message: string|null, result: (index: Number) => void) {\\n // uts方法默认会在子线程中执行,涉及 UI 操作必须在主线程中运行,通过 DispatchQueue.main.async 方法可将代码在主线程中运行\\n DispatchQueue.main.async(execute=():void => {\\n\\n // 初始化 UIAlertController 实例对象 alert\\n let alert = new UIAlertController(title=title,message=message,preferredStyle=UIAlertController.Style.alert)\\n\\n // 创建 UIAlertAction 按钮\\n let okAction = new UIAlertAction(title=\\"确认\\", style=UIAlertAction.Style.default, handler=(action: UIAlertAction):void => {\\n // 点击按钮的回调方法\\n result(0)\\n })\\n\\n // 创建 UIAlertAction 按钮\\n let cancelAction = new UIAlertAction(title=\\"取消\\", style=UIAlertAction.Style.cancel, handler=(action: UIAlertAction):void => {\\n // 点击按钮的回调方法\\n result(1)\\n })\\n\\n // 将 UIAlertAction 添加到 alert 上\\n alert.addAction(okAction)\\n alert.addAction(cancelAction)\\n\\n // 打开 alert 弹窗\\n UTSiOS.getCurrentViewController().present(alert, animated= true)\\n })\\n}\\n
\\n可以看到,在 uni-app x 你是可以“代码混写”的,所以与传统的 uni-app 不同,uni-app 依赖于定制 TypeScript 的 uts 和 uvue 编译器:
\\nArray
、Date
、JSON
、Map
、Math
、String
等内置对象转为 Kotlin、Swift、ArkTS 的对象等,所以也不需要有 uts 之类的虚拟机,另外 uts 编译器在处理特定平台时,还会调用相应平台的原生编译器,例如 Kotlin 编译器和 Swift 编译器而在 UI 上,目前除了编译为 ArkUI 之外,Android 和 iOS 其实都是编译成原生体系,目前看在 Android 应该是编译为传统 View 体系而不是 Compose ,而在 iOS 应该也是 UIKit ,按照官方的说法,就是性能和原生相当。
\\n另外,uni-app x 在 iOS 平台还做了一些骚操作,由于 wift 编译 iOS 应用必须依赖 Xcode,而 DCloud 的开发者中 Xindows 占比高于 Mac 电脑,所以 uni-app x 在 iOS 上提供 js 和 swift 双选逻辑层:
\\n\\n\\n也就是 uts 原生插件作者必须得有 mac 电脑,普通的 app 开发者可以没有 mac 电脑,使用插件也不需要 mac 电脑,通过云打包即可。
\\n
使用 js 逻辑层,你就可以不需要 mac 电脑,官方表示,js 逻辑层和原生渲染层的通信经过特殊处理,大幅提升通信效率问题,不再需要 bindingX 这类技术,而 UI 渲染则是 jscore + 原生渲染,从这个角度看应该还是优化过的 Weex 模式:
\\n\\n\\n官方表示 js 模式可以大幅降低插件生态的建设难度, 插件作者只需要特殊适配 Android 版本,在iOS和Web端仍使用 ts/js 库,可以快速把 uni-app/web 的生态迁移到 uni-app x 。
\\n
而回到平台视角,现在 uni-app x 同样支持了微信小程序,所以从这个节点看,uni-app x 确实可以开始成为 DCloud 的下一代主力框架,如果后续推进顺利,uni-app 也许就成为历史了。
\\n当然,前面展示的随意混编原生代码的写法其实并不规范,正常 uni-app x 还是需要统一成插件形式,官方表示目前插件市场已经有数千款 uni-app x 的插件,其中不少插件已支持鸿蒙next ,不过需要注意的是,uni-app x 不再支持旧有的原生语言插件,所有原生能力扩展都必须通过 uts 插件实现。
\\n另外,它的局限性问题也很明显,因为它的优势在于编译器转译得到原生性能,但是它的劣势也是在于转译,和使用类 skia 独立绘制的场景不同, uni-app x 需要考虑 uts 在不同平台和不同语言之间的同步和约束。
\\n其实在之前我们聊《用 Swift 写 Android App ?来了解下 Skip 原生级跨平台框架》 的时候就讲过,Skip 也是将 Swift 直接翻译成 Kotlin 原生去适配 Android,不同的是它是直接通过 Swift / SwiftUI 去转移为 Kotlin / Compose,所以在语法和兼容成本会更低一点点,但是就算这样,也存在需要需要妥协的地方,例如:
\\n所以,回到 uni-app x ,Skip 的问题在它这里同样存在,甚至因为支持的平台更多,它需要做的兼容和 if else
场景会更复杂,这对于 uni-app x 的后续推进和细节优化会是最大的挑战,uts 确实也做了一些约束,比如:
undefined
类型$
开头的变量名var
声明变量可能需要考虑平台差异Array.sort()
在 Swift 平台,部分 Math
和 RegExp
的方法)在特定原生平台上的支持可能存在限制或行为差异当然,实际上需要面临的细节问题肯定很多,具体能支持到什么地步,还是需要 DCloud 的后续打磨。
\\n最后,在性能方面,官方也提供了一些对比(具体我也没验证),场景是在华为 Mate 30 5G(麒麟990芯片)上进行的 100 个滑块同步滑动的测试,对比了 uni-app x (Kotlin)、Compose、Flutter 和 ArkUI-x:
\\n\\n\\n\\n
那么,你觉得你会考虑试试 uni-app x 吗?
","description":"其实在很久之前的《浅谈 uts + uvue 下的 uni-app x 是什么》我们就聊过 uni-app x ,相信在此之前大家对于 uni-app 的印象应该都是在小程序居多,虽然 uni-app 也可以打包客户端 app,甚至有基于 weex 的 nvue 支持,但是其效果只能说是“一言难尽”,而这里要聊的 uni-app x ,其实就是 DCloud 在跨平台这两年的新尝试。 说起 uni-app x 其实已经被我遗忘很久了,虽然在去年的鸿蒙 Next 的发布会时 uni-app 有被提及,只是当时也没看到 uni-app x 的身影…","guid":"https://juejin.cn/post/7503974160264069156","author":"恋猫de小郭","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-14T07:33:50.212Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/148b93432fb948de89a865a0d7679889~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747812830&x-signature=9eyQhyKHRVAXbJN0Q1EHrrsyo0c%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0c1830257a6941cd9ec2bf7464db6f3d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747812830&x-signature=DVGkiuF81W1T8lnScCOf34C5kKs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fefcc88d98194b10a5fbcda17e9fc307~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747812830&x-signature=0mnVMb1x0zVz0F3WzIKcYNjgyZk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/48e7a2d9b39c4489aed9373914bdad2c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747812830&x-signature=JDMP7xGxnod5pf6Ykqt8p82zSdg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/67fcd9c9a66546d9bedf509e6197c979~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747812830&x-signature=1UBzv2yhFK%2BVT1Dr%2FXQoUcPQiiM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ce4237c0d3dd4a9492b49d665b4ebeec~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747812830&x-signature=PxvPkot8jhCnez55t0FDu0eloJw%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter - UIKit开发相关指南 - 线程和异步","url":"https://juejin.cn/post/7503762089238888457","content":"Dart采用单线程执行模型,支持Isolates(在另一个线程上运行Dart代码)、事件循环和异步编程。除非生成一个Isolates,否则Dart代码将在主UI线程中运行,并由事件循环驱动。Flutter的事件循环相当于iOS的主线程上的RunLoop。
\\nDart的单线程模型,不代表阻塞型的操作都会导致UI卡顿。实际上可以采用Dart语言提供的异步功能比如async/await来执行异步的操作。
\\n因为要请求网络,所以添加http模块
\\n$ fltter pub add http\\n
\\nimport \'package:flutter/material.dart\';\\nimport \'package:http/http.dart\' as http;\\nimport \'dart:convert\';\\n\\nvoid main() {\\n runApp(const MainApp());\\n}\\n\\nclass MainApp extends StatelessWidget {\\n const MainApp({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n return const MaterialApp(home: ThreadSample());\\n }\\n}\\n\\nclass ThreadSample extends StatefulWidget {\\n const ThreadSample({super.key});\\n\\n @override\\n State<ThreadSample> createState() => _ThreadSampleState();\\n}\\n\\nclass _ThreadSampleState extends State<ThreadSample> {\\n List<Map<String, Object?>> data = [];\\n @override\\n /// 1. 初始化_ThreadSampleState Widget的状态\\n void initState() {\\n super.initState();\\n /// 2.加载数据\\n loadData();\\n }\\n\\n Future<void> loadData() async {\\n /// 3. 发起异步请求\\n final Uri dataURL = Uri.parse(\'https://jsonplaceholder.typicode.com/posts\');\\n final http.Response response = await http.get(dataURL);\\n /// 4. 等响应结束后调用setState() 更新data 触发build方法\\n setState(() {\\n data = (jsonDecode(response.body) as List).cast<Map<String, Object?>>();\\n });\\n }\\n\\n Widget getRow(int index) {\\n return Padding(\\n padding: const EdgeInsets.all(10),\\n child: Text(\'Row ${data[index][\'title\']}\'),\\n );\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: const Text(\'线程与异步示例\')),\\n // 5. 显示列表,长度为data.length,内容通过getRow方法返回data的子元素\\n body: ListView.builder(\\n itemCount: data.length,\\n itemBuilder: (context, index) {\\n return getRow(index);\\n },\\n ),\\n );\\n }\\n}\\n
\\n因为Flutter是单线程模型,不需要考虑线程管理相关的问题。在执行I/O密集型的操作时,比如访问磁盘或网络,可以使用async/await,但是当在执行CPU计算密集型的操作时,则应该将其移到独立线程(Isolate)以避免阻塞事件循环。
\\nIsolates 是独立的执行线程,它们与主线程内存堆不共享任何内存。这意味着你无法访问主线程中的变量,或通过调用 setState() 来更新用户界面。
\\nimport \'dart:async\';\\nimport \'dart:convert\';\\nimport \'dart:isolate\';\\n\\nimport \'package:flutter/material.dart\';\\nimport \'package:http/http.dart\' as http;\\n\\nvoid main() {\\n runApp(const SampleApp());\\n}\\n\\nclass SampleApp extends StatelessWidget {\\n const SampleApp({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n return const MaterialApp(title: \'Sample App\', home: SampleAppPage());\\n }\\n}\\n\\nclass SampleAppPage extends StatefulWidget {\\n const SampleAppPage({super.key});\\n\\n @override\\n State<SampleAppPage> createState() => _SampleAppPageState();\\n}\\n\\nclass _SampleAppPageState extends State<SampleAppPage> {\\n List<Map<String, Object?>> data = [];\\n\\n @override\\n void initState() {\\n super.initState();\\n\\n /// 主1. 加载数据\\n loadData();\\n }\\n\\n bool get showLoadingDialog => data.isEmpty;\\n\\n Future<void> loadData() async {\\n /// Opens a long-lived port for receiving messages.\\n /// 打开端口用于接收数据\\n final ReceivePort receivePort = ReceivePort();\\n\\n /// 主2.Isolate开启子线程\\n /// The [entryPoint] function must be able to be called with a single\\n /// argument, that is, a function which accepts at least one positional\\n /// parameter and has at most one required positional parameter.\\n ///\\n /// The entry-point function is invoked in the new isolate with [message]\\n /// as the only argument.\\n /// 第一个参数:至少包含一个参数的函数指针,这里关联的是dataLoader,参数是SendPort\\n ///\\n /// [message] must be sendable between isolates. Objects that cannot be sent\\n /// include open files and sockets (see [SendPort.send] for details). Usually\\n /// the initial [message] contains a [SendPort] so that the spawner and\\n /// spawnee can communicate with each other.\\n /// 第二个参数: 不同Isolate之间传递的数据,通常初始化时传的message包含一个SendPort\\n ///\\n /// receivePort.sendPort\\n /// [SendPort]s are created from [ReceivePort]s.\\n /// Any message sent through a [SendPort] is delivered to its corresponding [ReceivePort].\\n /// There might be many [SendPort]s for the same [ReceivePort].\\n /// 通过SendPort发送的消息会传送给关联的ReceivePort\\n await Isolate.spawn(dataLoader, receivePort.sendPort);\\n\\n /// 主3. first是一个Future,它会在接收到第一个消息时完成\\n /// 一旦收到第一个消息,它就会关闭ReceivePort,并且不再监听其它消息\\n /// 适用于只接收单个消息的情况\\n final SendPort sendPort = await receivePort.first as SendPort;\\n try {\\n /// 主4. 使用await调用sendReceive\\n final List<Map<String, dynamic>> msg = await sendReceive(\\n sendPort,\\n \'https://jsonplaceholder.typicode.com/posts\',\\n );\\n\\n /// 主5.设置数据,通知Flutter刷新UI\\n setState(() {\\n data = msg;\\n });\\n } catch (e) {\\n print(\'Error in loadData:$e\');\\n }\\n }\\n\\n // 子1. 执行子线程上的函数\\n static Future<void> dataLoader(SendPort sendPort) async {\\n // 子2.打开端口接收数据\\n final ReceivePort port = ReceivePort();\\n\\n /// 子3. 发送自己的接收端口\\n sendPort.send(port.sendPort);\\n\\n /// 子4:等待消息\\n await for (final dynamic msg in port) {\\n\\n /// 子5: 接收到url + 主线程的接收端口\\n final String url = msg[0] as String;\\n final SendPort replyTo = msg[1] as SendPort;\\n\\n /// 子6: 发起网络请求\\n final Uri dataURL = Uri.parse(url);\\n final http.Response response = await http.get(dataURL);\\n\\n /// 下面这种写法在sendReceive会报\\n /// Unhandled\\n /// Exception: type \'Future<dynamic>\' is not a subtype of type\\n /// \'Future<List<Map<String, dynamic>>>\'\\n ///\\n /// replyTo.send(jsonDecode(response.body) as List<Map<String, dynamic>>);\\n /// 因为Dart在运行时无法检查Future<T>中的T,直接转换Future的泛型参数会失败\\n /// 强制类型转换\\n final data = jsonDecode(response.body) as List;\\n final typedata = data.cast<Map<String, dynamic>>();\\n\\n /// 子7: 将网络请求的结果发送到主线程\\n replyTo.send(typedata);\\n }\\n }\\n\\n Future<dynamic> sendReceive(SendPort port, String msg) {\\n // 主5.创建接收数据的端口\\n final ReceivePort response = ReceivePort();\\n // Sends an asynchronous [message] through this send port, to its corresponding [ReceivePort].\\n // 主6. 主线程异步发送url + 通知其它线程接收端口\\n port.send(<dynamic>[msg, response.sendPort]);\\n return response.first;\\n }\\n\\n Widget getBody() {\\n /// 数据为空显示进度条\\n bool showLoadingDialog = data.isEmpty;\\n\\n if (showLoadingDialog) {\\n return getProgressDialog();\\n } else {\\n return getListView();\\n }\\n }\\n\\n Widget getProgressDialog() {\\n return const Center(child: CircularProgressIndicator());\\n }\\n\\n ListView getListView() {\\n return ListView.builder(\\n itemCount: data.length,\\n itemBuilder: (context, position) {\\n return getRow(position);\\n },\\n );\\n }\\n\\n Widget getRow(int i) {\\n return Padding(\\n padding: const EdgeInsets.all(10),\\n child: Text(\\"Row ${data[i][\\"title\\"]}\\"),\\n );\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: const Text(\'Sample App\')),\\n body: getBody(),\\n );\\n }\\n}\\n
\\n[ERROR:flutter/runtime/dart_vm_initializer.cc(40)] Unhandled Exception: ClientException with SocketException: Failed host lookup: \'jsonplaceholder.typicode.com\'
[ERROR:flutter/runtime/dart_vm_initializer.cc(40)] Unhandled Exception: ClientException with SocketException: Failed host lookup: \'jsonplaceholder.typicode.com\' (OS Error: nodename nor servname provided, or not known, errno = 8), uri=jsonplaceholder.typicode.com/posts
\\n首次启动需要同意网络权限,看报错是DNS找不到域名,所以还是网络问题,在手机上授权后再重新用flutter运行工程能恢复
\\n1.给 UIKit 开发者的 Flutter 指南\\n2.flutter 中 ReceivePort 的 first 和 listen
","description":"线程和异步 编写异步代码\\n\\nDart采用单线程执行模型,支持Isolates(在另一个线程上运行Dart代码)、事件循环和异步编程。除非生成一个Isolates,否则Dart代码将在主UI线程中运行,并由事件循环驱动。Flutter的事件循环相当于iOS的主线程上的RunLoop。\\n\\nDart的单线程模型,不代表阻塞型的操作都会导致UI卡顿。实际上可以采用Dart语言提供的异步功能比如async/await来执行异步的操作。\\n\\n因为要请求网络,所以添加http模块\\n\\n$ fltter pub add http\\n\\nimport \'package:flutter…","guid":"https://juejin.cn/post/7503762089238888457","author":"忘川三","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-14T02:14:01.319Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/237e2f95c53e41f0b1926f6f6950ba7b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5b-Y5bed5LiJ:q75.awebp?rk3s=f64ab15b&x-expires=1747793641&x-signature=eJheVsO8Ld4ljLbtQopUX0RgNvw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b6995d9fa10b491eac82fd7d740e7df9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5b-Y5bed5LiJ:q75.awebp?rk3s=f64ab15b&x-expires=1747793641&x-signature=p%2Blwgtkly%2BJ8gfV8prjNxL%2BoXsg%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["代码人生","Flutter","Dart","iOS"],"attachments":null,"extra":null,"language":null},{"title":"掌握 Dart 的 sealed class(三)","url":"https://juejin.cn/post/7503818874690551835","content":"从 面向对象设计原则(SOLID) 来看,Dart 中的 sealed class
是一个非常契合多项设计原则的工具。我们可以逐条分析它与 SOLID 原则的关系:
\\n\\n每个类应该只有一个职责,且该职责应该完全封装在类中。
\\n
sealed class 强制你将一个有限状态系统拆分为多个子类,每个子类负责自己的逻辑(如状态、错误、响应等),符合 SRP:
\\nsealed class NetworkResult {}\\n\\nclass Success extends NetworkResult {\\n final String data;\\n Success(this.data);\\n}\\n\\nclass Failure extends NetworkResult {\\n final String message;\\n Failure(this.message);\\n}\\n
\\n✅ Success
只负责成功的结构,Failure
只负责失败的结构,职责单一,清晰分离。
\\n\\n对扩展开放,对修改封闭。
\\n
sealed class
可以通过新增子类来扩展系统,而不必修改已有逻辑。
// 想支持超时状态,只需要新增 Timeout:\\nclass Timeout extends NetworkResult {}\\n
\\n✅ 不修改 Success
/ Failure
等已有子类,系统即可扩展,符合开闭原则。
\\n\\n子类对象必须能够替换父类对象使用。
\\n
sealed class
定义的是一种类型家族,你可以放心地将任一子类替换为父类使用。
void handle(NetworkResult result) {\\n // 所有子类都可替换 NetworkResult 类型处理\\n}\\n
\\n✅ 所有子类(如 Success
, Failure
)都可以无缝用于 NetworkResult
,满足 LSP。
\\n\\n不要强迫客户端依赖它们不使用的方法。
\\n
虽然 Dart 没有接口关键字,但 sealed class
的子类可以拥有各自方法和字段,不共享不必要的结构。
class Success extends NetworkResult {\\n final String data;\\n void logSuccess() {}\\n}\\n\\nclass Failure extends NetworkResult {\\n final String error;\\n void reportError() {}\\n}\\n
\\n✅ 不会让子类被强制实现无关的方法,各子类职责清晰,接口最小化。
\\n\\n\\n高层模块不应该依赖低层模块,二者都应该依赖抽象。
\\n
在实际架构中,使用 sealed class
作为抽象模型,高层业务代码依赖的是 sealed
的抽象,而不是具体实现:
void process(NetworkResult result) {\\n switch (result) {\\n case Success(:var data):\\n print(\'Got $data\');\\n break;\\n case Failure(:var message):\\n print(\'Error: $message\');\\n break;\\n }\\n}\\n
\\n✅ 业务逻辑依赖抽象类型 NetworkResult
,而不是具体子类,符合依赖倒置。
设计原则 | sealed class 是否契合 | 理由 |
---|---|---|
SRP | ✅ | 每个子类职责单一 |
OCP | ✅ | 新增子类即可扩展 |
LSP | ✅ | 子类可以无缝替代父类使用 |
ISP | ✅ | 子类仅实现自身需要的结构 |
DIP | ✅ | 高层依赖抽象 sealed class 类型 |
当了解sealed class
的模式匹配能力的时候,我第一想到的是枚举。
在 Dart 中 enum
和 sealed class
都可以用于建模有限类型集合,但用途和灵活性有显著区别。
特性 / 对比点 | enum (增强枚举) | sealed class (密封类) |
---|---|---|
用于建模有限状态 | ✅ 非常适合(内建支持 values 等) | ✅ 更灵活但需手动处理 |
可携带不同字段 | ⚠️ 只能有一个构造函数,所有枚举项字段一样 | ✅ 各子类可有完全不同的字段 |
模式匹配 | ✅ Dart 3 支持 switch exhaustiveness | ✅ Dart 3 switch 也完全支持 |
扩展性 | ❌ 不支持继承枚举,不能新增项 | ✅ 可以新增子类 |
方法和 getter | ✅ 支持 | ✅ 支持 |
类型安全 & 约束 | ✅ 限定值集合清晰 | ✅ 更强表达能力 |
简洁性 | ✅ 非常简洁 | ❌ 需要更多代码 |
推荐用法 | ✅ 状态、颜色、固定类型等 | ✅ 多状态对象、响应类型、操作结果等 |
enum
(适合状态固定,无需复杂数据)enum ConnectionState {\\n connected,\\n disconnected,\\n connecting,\\n}\\n
\\nsealed class
(更复杂状态,携带数据)sealed class ConnectionState {}\\n\\nclass Connected extends ConnectionState {\\n final DateTime timestamp;\\n Connected(this.timestamp);\\n}\\n\\nclass Disconnected extends ConnectionState {}\\n\\nclass Connecting extends ConnectionState {\\n final int retryCount;\\n Connecting(this.retryCount);\\n}\\n
\\nvoid handle(ConnectionState state) {\\n switch (state) {\\n case Connected():\\n print(\'已连接\');\\n break;\\n case Connecting(:var retryCount):\\n print(\'重试次数: $retryCount\');\\n break;\\n case Disconnected():\\n print(\'已断开\');\\n break;\\n }\\n}\\n
\\nDart 的增强枚举(Enhanced Enums,Dart 2.17+)允许为每个枚举值定义静态且统一的属性,但这些属性在编译时固定,无法动态扩展或差异化。例如:
\\nenum DeviceType {\\n lamp(value: 1, icon: \'lamp_icon\'),\\n airConditioner(value: 2, icon: \'ac_icon\');\\n\\n final int value;\\n final String icon;\\n\\n const DeviceType({required this.value, required this.icon});\\n}\\n
\\n特点:
\\n密封类的子类可以自由定义属性和方法,每个子类可独立携带不同数据,且支持动态扩展:
\\nsealed class Result {}\\nclass Success implements Result {\\n final String data;\\n Success(this.data);\\n}\\nclass Error implements Result {\\n final int code;\\n final String message;\\n Error(this.code, this.message);\\n}\\n
\\n特点:
\\n- 动态属性:每个子类可定义独立的属性(如 Success 只有 data,Error 有 code 和 message)。\\n- 方法扩展:子类可包含独立的方法逻辑。\\n- 编译时类型安全:switch 表达式强制覆盖所有子类分支\\n
\\n枚举实现(静态属性):
\\nenum ApiErrorCode {\\n network(code: 1001, message: \'网络错误\'),\\n server(code: 5001, message: \'服务器错误\');\\n\\n final int code;\\n final String message;\\n\\n const ApiErrorCode({required this.code, required this.message});\\n}\\n
\\n缺点:所有错误类型必须遵循相同的属性结构,无法为特定错误添加额外字段(如 details
)。
密封类实现(动态属性):
\\nsealed class ApiError {}\\nclass NetworkError implements ApiError {\\n final int code;\\n final String message;\\n final String details; // 额外字段\\n NetworkError(this.code, this.message, this.details);\\n}\\nclass ServerError implements ApiError {\\n final int code;\\n ServerError(this.code);\\n}\\n
\\n优势:NetworkError
可携带 details
,而 ServerError
无需冗余字段。
枚举实现(静态属性):
\\nenum LoadingState {\\n dataLoaded(data: \'默认数据\'),\\n error(message: \'默认错误\');\\n\\n final String data;\\n final String message;\\n\\n const LoadingState({required this.data, required this.message});\\n}\\n
\\n缺点:所有状态必须包含 data
和 message
,即使某些状态不需要(如纯加载中状态)。
密封类实现(动态属性):
\\nsealed class LoadingState {}\\nclass DataLoaded implements LoadingState {\\n final String data;\\n DataLoaded(this.data);\\n}\\nclass Loading implements LoadingState {}\\nclass ErrorState implements LoadingState {\\n final String message;\\n ErrorState(this.message);\\n}\\n
\\n优势:状态可按需携带数据,避免冗余。
\\n枚举关联值的局限性
\\n密封类的优势
\\n设计目标
\\n扩展性
\\n代码可读性
\\nNetworkError
比 ErrorType.network
更清晰)。密封类通过类型安全的模式匹配、灵活的状态扩展和编译时检查,解决了枚举在复杂场景下的局限性。在 Flutter 开发中,密封类尤其适合用于状态管理、错误处理等需要明确类型分支的场景,而枚举更适合简单的常量集合。两者的选择需根据具体业务需求权衡。
","description":"当了解sealed class的模式匹配能力的时候,我第一想到的是枚举。 在 Dart 中 enum 和 sealed class 都可以用于建模有限类型集合,但用途和灵活性有显著区别。\\n\\n🔶 1. 使用场景对比\\n特性 / 对比点\\tenum(增强枚举)\\tsealed class(密封类)用于建模有限状态\\t✅ 非常适合(内建支持 values 等)\\t✅ 更灵活但需手动处理\\n可携带不同字段\\t⚠️ 只能有一个构造函数,所有枚举项字段一样\\t✅ 各子类可有完全不同的字段\\n模式匹配\\t✅…","guid":"https://juejin.cn/post/7503849592095096883","author":"OldBirds","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-14T02:06:23.388Z","media":null,"categories":["Android","前端","iOS","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"掌握 Dart 的 sealed class(一)","url":"https://juejin.cn/post/7503762089238757385","content":"在写 Flutter 项目的时候,经常会瞄到 sealed class
的类,但是当时也就把它们当常规类,没有进一步了解,恰巧今天早上又遇到,索性深入研究一番。
对于这种概念性问题,一般都是从第一手资料出发,直接前往官网:
\\n\\n\\n\\n密封类是 Dart 3.0 引入的重要特性,通过限制类的继承范围和提供编译时检查,显著提升了代码的健壮性和可维护性。在 Flutter 开发中,它尤其适用于状态管理、错误处理等需要明确类型分支的场景。合理使用密封类,可以避免因类型遗漏导致的运行时错误,同时使代码逻辑更加清晰。
\\n
我们简单归纳下核心特性:
\\n子类可枚举性
\\n密封类的所有直接子类必须定义在同一库(同一文件或包)中,编译器能明确知道其所有可能的子类型。例如:
\\nsealed class AuthState {} // 密封类\\nclass AuthLoading extends AuthState {} // 子类\\nclass AuthSuccess extends AuthState {} // 子类\\n
\\n隐式抽象性
\\n密封类自动是抽象类,无法被实例化,且子类必须显式继承或实现其定义。
\\n限制外部扩展
\\n密封类禁止外部库继承或实现,确保类型层次结构的封闭性。例如:
\\n// 外部库无法执行以下操作:\\nclass ExternalAuthState extends AuthState {} // 编译错误\\n
\\n与 switch 语句配合
\\n在 switch 中匹配密封类时,编译器会强制检查是否覆盖所有子类,避免遗漏分支:
\\nString buildUI(AuthState state) {\\n return switch (state) {\\n AuthLoading() => \'加载中...\',\\n AuthSuccess() => \'成功\',\\n AuthFailure() => \'失败\',\\n // 编译器提示缺少其他子类分支\\n };\\n}\\n
\\n就对类声明关键字上,我们可以对比下类似概念的差异
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n特性 | sealed | final | abstract |
---|---|---|---|
继承限制 | 禁止外部继承和实现 | 禁止外部继承 | 允许继承,但不可实例化 |
子类可见性 | 子类必须同库 | 无限制 | 无限制 |
用途 | 有限状态集合 | 关闭继承链 | 定义抽象接口或部分实现 |
状态管理
\\n在 Flutter 的状态管理(如 BLoC、Bloc)中,密封类常用于定义有限的 UI 状态或事件类型,确保所有分支被处理:
sealed class CounterEvent {}\\nclass Increment implements CounterEvent {}\\nclass Decrement implements CounterEvent {}\\n\\nclass CounterBloc extends Bloc<CounterEvent, int> {\\n CounterBloc() : super(0) {\\n on<Increment>(_onIncrement);\\n on<Decrement>(_onDecrement);\\n }\\n}\\n
\\nAPI 设计
\\n通过密封类限制外部对某个类的扩展,例如定义一组固定的错误类型:
sealed class ApiError {}\\nclass NetworkError extends ApiError {}\\nclass ServerError extends ApiError {}\\n
\\n类型安全的模式匹配\\n结合 Dart 的 switch
表达式,密封类能提供类似 Kotlin 的模式匹配能力,增强代码可读性。
在Flutter中, Stack
是核心布局组件之一,主要用于叠加布局实现有一些复杂的页面效果,悬浮按钮、蒙层、徽章提示等,熟练掌握Stack基本属性,并结合其它组件灵活使用可以完成非常炫酷的页面绘制。\\n以下是stack组件的一些基本属性和用法介绍。
Stack
允许子组件以层叠的方式排列(类似 CSS 的绝对定位),常用于叠加元素(如图标上的徽章、对话框、悬浮按钮等)。默认情况下,子组件会从左上角开始堆叠。
这应该是我们裁切图片时经常见到的一个效果。\\n这个示例就是通过三层层叠的效果实现的,底部是一张图片,中间是一个蒙版,最上层是一个被裁切过的图片,
\\nalignment
控制所有子组件的默认对齐方式(未使用 Positioned
包裹的子组件):
Alignment.topLeft
(默认值)Alignment.topCenter
Alignment.topRight
Alignment.centerLeft
Alignment.center
Alignment.centerRight
Alignment.bottomLeft
Alignment.bottomCenter
Alignment.bottomRight
Alignment(0.5, 0.5)
(自定义偏移)是一个y轴从上到下,x轴从左至右计算的方式;最大值是(1,1)左下角,最小值是(-1,-1)左上角,中间是(0,0)
\\nAlignment(-1, -1)
Alignment(0, 0)
-Alignment(1, 1)
fit
控制非定位子组件(未被 Positioned
包裹的组件)如何适应 Stack
的尺寸:
StackFit.loose
(默认):子组件不受约束,按自身大小显示StackFit.expand
:强制子组件填满 Stack
的可用空间Container(\\n alignment: Alignment.center,\\n width: 300,\\n height: 400,\\n child: Stack(\\n alignment: Alignment(-1, -1),\\n fit: StackFit.expand,\\n children: <Widget>[\\n Container(width: 200, height: 300, color: Colors.red), //被迫和父级Container一样大,并且背遮挡住了\\n Positioned(\\n child: Container(width: 100, height: 100, color: Colors.green),// 被迫和父级Container一样大\\n ),\\n ],\\n ),\\n),\\n
\\nStackFit.passthrough
:继承父容器的约束Container(\\n alignment: Alignment.center,\\n width: 300,\\n height: 400,\\n child: Stack(\\n alignment: Alignment(-1, -1),\\n fit: StackFit.passthrough,\\n children: <Widget>[\\n Container(width: 200, height: 300, color: Colors.red),\\n Positioned(\\n child: Container(width: 100, height: 100, color: Colors.green),\\n ),\\n ],\\n ),\\n),\\n
\\nclipBehavior
控制子组件溢出 Stack
时的裁剪方式:
Clip.hardEdge
(默认):快速裁剪,不抗锯齿Clip.antiAlias
:平滑裁剪(性能略低)Clip.none
:不裁剪(可能导致溢出)Stack(\\n clipBehavior: Clip.none, // 允许子组件超出Stack范围\\n children: [Positioned(top: -20, child: Icon(Icons.star))],\\n)\\n
\\n通过 Positioned
包裹子组件,实现精确的位置控制:
Stack(\\n children: [\\n Positioned(\\n left: 10,\\n top: 20,\\n child: Icon(Icons.notifications),\\n ),\\n Positioned(\\n right: 0,\\n bottom: 0,\\n child: Text(\\"End\\"),\\n ),\\n ],\\n)\\n
\\nPositioned.fill( // 填满父容器\\n child: Container(color: Colors.black12),\\n);\\n\\nPositioned(\\n left: 10,\\n right: 10, // 左右边距各10\\n height: 50,\\n child: Container(color: Colors.red),\\n);\\n
\\nStack(\\n clipBehavior: Clip.none, // 允许徽章超出Stack\\n children: [\\n CircleAvatar(radius: 30),\\n Positioned(\\n right: -5,\\n bottom: -5,\\n child: Container(\\n padding: EdgeInsets.all(2),\\n decoration: BoxDecoration(\\n color: Colors.red,\\n shape: BoxShape.circle,\\n ),\\n child: Text(\\"3\\", style: TextStyle(color: Colors.white)),\\n ),\\n ],\\n)\\n
\\nStack(\\n alignment: Alignment(0, 0),\\n fit: StackFit.passthrough,\\n children: <Widget>[\\n Image.asset(\\n \'assets/image/33333.jpeg\', // 图片的本地地址\\n width: 300,\\n height: 400,\\n fit: BoxFit.none, // 图片填充方式\\n ),\\n Center(\\n child: ClipRect(\\n child: BackdropFilter(\\n filter: ImageFilter.blur(sigmaX: 2.0, sigmaY: 2.0),\\n child: Container(\\n width: 300,\\n height: 400,\\n decoration: BoxDecoration(color: Colors.grey.shade200.withOpacity(0.5)),\\n ),\\n ),\\n ),\\n ),\\n SizedBox(\\n width: 200,\\n height: 200,\\n child: ClipOval(\\n child: Image.asset(\\n \'assets/image/33333.jpeg\', // 图片的本地地址\\n fit: BoxFit.none,\\n width: 300,\\n height: 400,\\n ),\\n )\\n ),\\n ],\\n),\\n
\\nStack(\\n alignment: Alignment.bottomCenter,\\n children: [\\n Scaffold(body: ...), // 页面内容\\n Positioned(\\n bottom: 30,\\n child: FloatingActionButton(\\n onPressed: () {},\\n child: Icon(Icons.add),\\n ),\\n ),\\n ],\\n)\\n
\\nPositioned
或 Align
的子组件会堆叠在左上角。Stack
的父容器未提供约束(如直接放在 ListView
中),需显式设置尺寸(如 SizedBox
)。Stack
中嵌套过多子组件,尤其是需要动态更新的组件。Positioned
、Align
、Transform
等组件配合实现复杂效果。Stack
是 Flutter 中实现层叠布局的核心组件,通过 Positioned
和 Align
可灵活控制子组件的位置和层级。适用于需要元素叠加的场景(如悬浮按钮、蒙层、徽章提示等),但需注意处理溢出和尺寸约束问题。
\\n\\n\\n
Scrollable
是 ListView
、 CustomScrollView
、 SingleChildScrollView
等常用控件的超类。在本文中,我们将尝试了解其背后的原理。
首先,让我们从滚动更新通知开始。
\\n可以将通知向上发送到 Widget
树。Flutter
会在滚动、大小变化和布局变化等事件上发送通知。
换句话说,每当某个东西滚动或改变其大小时,祖先都会收到通知。
\\n让我们看看滚动时发送的通知里面有什么。
\\nNotificationListener<ScrollUpdateNotification>(\\n onNotification: (notification) {\\n return false; // <- putting a debugger breakpoint here\\n },\\n child: ListView(\\n children: [\\n const SizedBox(height: 1000),\\n ],\\n ),\\n)\\n
\\n首先,我们来关注一下 scrollDelta
。这是相对于之前状态,滚动位置增加的像素数。如果为正,则表示用户沿着主方向滚动;如果为负,则表示用户向后滚动。
如果我们深入研究 metrics
的内容,会发现很多有用的数据。让我们将这些数据可视化。
可滚动内容被涂成红色,而静态小部件则被涂成蓝色。
\\n因此, extentTotal
是 Scrollable
内容的总高度。maxScrollExtent
是无法容纳在视口中的内容的高度, scrollDelta
是自上次通知以来已滚动的原始像素数 maxScrollExtent
ententBefore
和 extentAfter
对应于 Scrollable
从开始到结束的剩余高度。
viewPortDimension
是包含 Scrollable
的小部件的高度。
让我们将 SizedBox
高度从 1000 改为 200。现在,由于没有滚动发生,通知事件不会发送——滚动内容的大小小于视口。如果我们需要这些值,该如何获取呢?
class _ScrollableExampleState extends State<ScrollableExample> {\\n final _controller = ScrollController(); // <-- add this\\n\\n @override\\n void initState() {\\n super.initState();\\n WidgetsBinding.instance.addPostFrameCallback((timeStamp) {\\n _controller.position; // <-- setting a debug breakpoint here\\n });\\n }\\n\\n ...\\n ListView(\\n controller: _controller, // <-- add this\\n ...\\n )\\n}\\n
\\n我们可以看到,视口高度等于 extentTotal
, maxScrollExtent
为 0,因为在这种情况下无法滚动。
如果我们回到通知对象,我们会注意到它有一个 dragDetails
属性。这个对象类似于 GestureDetector
更新回调中发送的对象。让我们在屏幕上进行一些滚动,并观察发送的数据:
print(\\"$scrollDelta\\\\t$dragDetails\\");\\n
\\n让我们快速滚动小部件并查看数据:
\\n乍一看, scrollDelta
和 dragDetails.y
值似乎取反了,但很相似,但请检查最后一个值。你能猜出这里发生了什么吗?提示:这是在 Android 上完成的。
让我们在 iPhone 上运行它并看看:
\\n拖动数据类似,但 scrollDelta
有所不同。拖动完成后,通知仍然会发送,但没有拖动更新详细信息。
当然,原因是 Scrollable
默认应用了不同的 ScrollPhysics
默认应用的是 BouncingScrollPhysics
,而 Android
默认应用的是 ClampingScrollPhysics
。
在 Flutter 中,你可以选择不同的物理效果,也可以创建自定义的物理效果。此外,还可以同时应用多个滚动物理效果,如下所示:
\\nBouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics())\\n
\\n此示例将首先应用 BouncingScrollPhysics
的模拟,然后应用 AlwaysScrollableScrollPhysics
的模拟。
不,有些情况下我们会计算 Scrollable
中每个项目的大小,例如,当项目大小取决于其内容时,使用 ListView.builder
,比如有一个 Text
小部件,其行高可以是 1 行,也可以是 2 行。
在这种情况下,我们无法计算 totalExtent
,因为这意味着我们必须对整个列表进行计算,这违背了延迟加载的初衷 。在这种情况下,Flutter 将仅实例化视口 + cacheExtent
( ListView 的一个参数)中可见的项目。请注意,即使某个项目仅部分适合 cacheExtent ,它仍会被实例化。
当内容小于父窗口小部件时,有两个选项可用于计算 viewPort 大小。
\\n将 shrinkWrap
设置为 true 后,渲染器将被强制根据其子项计算 viewPort
的高度。这样一来,延迟加载将被禁用,这可能会导致性能问题。请仅在确保列表中不会包含太多对象时才使用此方法。
SingleChildScrollView(\\n child: Column(\\n children: [\\n Text(\\"Content\\"),\\n const Spacer(), // <- don\'t do that\\n ElevatedButton(onPressed: () {}, child: Text(\\"Button\\"))\\n ],\\n ),\\n)\\n
\\n这里存在一个冲突: Spacer
想要占用尽可能多的空间,而 Scrollable
允许其子元素占用尽可能多的空间。在这个例子中,Spacer
需要知道父元素的大小才能计算其高度,但这是不可能的,因为父元素的大小取决于子元素。
LayoutBuilder(builder: (context, constraints) {\\n return SingleChildScrollView(\\n child: ConstrainedBox(\\n constraints: BoxConstraints(\\n minHeight: constraints.maxHeight,\\n maxHeight: double.infinity,\\n ),\\n child: IntrinsicHeight(\\n child: Column(\\n children: [\\n Text(\\"Content\\"),\\n const Spacer(),\\n ElevatedButton(onPressed: () {}, child: Text(\\"Button\\"))\\n ],\\n ),\\n ),\\n ),\\n );\\n})\\n
\\nLayoutBuilder
:
SingleChildScrollView
:
ConstrainedBox
:
IntrinsicHeight
:
Column
:
因此,我们可以在 Scrollable
中使用 Spacer
或 Flexible
。
还可以使用此包中的 ScrollableColumn
小部件
让我们实现一个滚动监听器,它会在滚动时更新应用栏中的视图。
\\n让我们从创建一个 StatefulWidget
开始。它也可以是无状态的,这取决于你使用的状态管理方法。在这个例子中,我希望它尽可能简单,所以我只使用了 ValueNotifier
作为 StatefulWidget
的属性。
\\nclass _ScrollableZoomerState extends State<ScrollableZoomer> {\\n ValueNotifier<double> scrollPosition = ValueNotifier(0.0);\\n \\n ...\\n}\\n
\\n它将存储一个标准化的滚动位置,其中 0.0 是开始,1.0 是完全滚动。
\\n@override\\nWidget build(BuildContext context) {\\n return Scaffold(\\n appBar: ...,\\n body: NotificationListener<ScrollUpdateNotification>(\\n onNotification: (notification) {\\n scrollPosition.value = min(1, notification.metrics.pixels /\\n notification.metrics.maxScrollExtent);\\n\\n return true;\\n },\\n child: widget.child,\\n ),\\n );\\n}\\n
\\n很简单,只需将滚动像素除以最大滚动范围,并确保其不超过 100%。然后对“下一步”按钮应用一些简单的变换。
\\nappBar: AppBar(\\n actions: [\\n ValueListenableBuilder(\\n valueListenable: scrollPosition,\\n builder: (context, value, _) => Opacity(\\n opacity: value > 0.1 ? value : 0,\\n child: Transform.translate(\\n offset: Offset(0, 40 * (1 - value)),\\n child: TextButton(\\n onPressed: ...,\\n child: Text(\\"Next\\"),\\n ),\\n ),\\n ),\\n )\\n ],\\n),\\n
\\n您可以尝试不同的值和数学运算,唯一的限制就是想象力。
","description":"本文翻译自:Mastering Scrollable in Flutter Scrollable 是 ListView 、 CustomScrollView 、 SingleChildScrollView 等常用控件的超类。在本文中,我们将尝试了解其背后的原理。\\n\\n首先,让我们从滚动更新通知开始。\\n\\n什么是通知?\\n\\n可以将通知向上发送到 Widget 树。Flutter 会在滚动、大小变化和布局变化等事件上发送通知。\\n\\n换句话说,每当某个东西滚动或改变其大小时,祖先都会收到通知。\\n\\n让我们看看滚动时发送的通知里面有什么。\\n\\nNotificationListene…","guid":"https://juejin.cn/post/7503738123689623588","author":"OldBirds","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-13T13:04:51.822Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/17676282db68471197299f79c7ed3032~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgT2xkQmlyZHM=:q75.awebp?rk3s=f64ab15b&x-expires=1747746291&x-signature=vpRQW%2BVF3AMnK8599z%2FpyUpVatU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a994d888bec643fd8b93fdbac6ea6093~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgT2xkQmlyZHM=:q75.awebp?rk3s=f64ab15b&x-expires=1747746291&x-signature=l0Ea9jEECf77eOD46ffnDWmSZRk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/606cab03402546d6882325f5b4e59550~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgT2xkQmlyZHM=:q75.awebp?rk3s=f64ab15b&x-expires=1747746291&x-signature=0mUI6VF0G%2BAkzQ58J%2Btse7wvnXY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/18b5533cd5a842b0bf0d3adfc797fa62~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgT2xkQmlyZHM=:q75.awebp?rk3s=f64ab15b&x-expires=1747746291&x-signature=lhvOr0G5QnSqTgy88bvl16kA%2BpM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://juejin.cn/","type":"photo"},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6d48275b60fb44eb8670262682f9b379~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgT2xkQmlyZHM=:q75.awebp?rk3s=f64ab15b&x-expires=1747746291&x-signature=u1hFQvvSEVFJVSVRXgYm2CjQdqc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c48a9866f1324d04a02f1f2f312e7e56~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgT2xkQmlyZHM=:q75.awebp?rk3s=f64ab15b&x-expires=1747746291&x-signature=ymZs8nmWSlHDlID3KKHbFudOtDs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c211fa6de9524c04bc50ee170268c280~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgT2xkQmlyZHM=:q75.awebp?rk3s=f64ab15b&x-expires=1747746291&x-signature=tWAoSwdInv84MnjnRkuFWeoX2%2Fs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/30fc38f3eebe421f97a56bf08a704482~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgT2xkQmlyZHM=:q75.awebp?rk3s=f64ab15b&x-expires=1747746291&x-signature=Y%2BuZhy%2Fo1FdqGFvXovoloRyq9%2Bk%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["iOS","前端","Flutter","Android"],"attachments":null,"extra":null,"language":null},{"title":"鸿蒙 PC 发布之后,想在技术上聊聊它的未来可能","url":"https://juejin.cn/post/7503450078159470646","content":"最近鸿蒙 PC 刚发布完,但是发布会没公布太多技术细节,基本上一些细节都是通过自媒体渠道获取,首先可以确定的是,鸿蒙 PC 本身肯定是无法「直接」运行 win 原本的应用,但是可以支持手机上「原生鸿蒙」的应用,细节上无非就是 UI 兼容下大屏模式的支持,比如下图是来自 差评XPIN 的鸿蒙 PC 截图:
\\n那么问题来了,HarmonyOS 「卓易通 」 作为生态过渡的丰富支持,甚至在应用商店都可以无缝衔接,那么鸿蒙 PC 是否也可以有类似的场景?
\\n因为目前得到的消息是,鸿蒙 PC 不支持侧载 ,这个结论我也不保熟,只是在这个大条件下讨论,那么 鸿蒙 PC 是不是也可以有个 「W易通」?技术上是否可以支持?
\\n答案上还真可以,从某些媒体上说的,通过定制 Wine 来兼容已有的 win 软件,这个或者是一条可行的路,但是其实我也并没有找到官方下图的说法和出处,但是不妨碍我们讨论可行性。
\\nWine 这个名字本身就揭示了它核心特性:“Wine Is Not an Emulator”(Wine 不是模拟器),它其实已经被应用很久了,例如:
\\n所以 Wine 确实是一个可行的途径,Wine 在实际场景里主要是充当一个兼容层,实时地将 Windows 应用的 API 调用转换为宿主操作系统(如 Linux 或 macOS)能够理解的等效 POSIX 调用 。
\\n当然,这种设计也意味着 Wine 的兼容性直接取决于其对 Windows API 的重实现程度,所以 Wine 的核心就是重塑 Windows API ,在某种程度上镜像了 Windows 的结构,例如:
\\nwineserver
:在 Windows 中主要是由内核提供核心服务,在 Wine 中会由 wineserver
在用户空间实现 ,它的职责包括实现基本的 Windows 功能,如进程和线程管理、对象管理、进程间通信(IPC)、同步原语、将 Unix 信号转换为 Windows 异常,处理窗口管理和输入事件等NTDLL.DLL
(Windows NT 内核功能的核心接口)、KERNEL32.DLL
(基础操作系统功能,如内存管理、文件 I/O)、GDI32.DLL
(图形设备接口,负责 2D 绘图)、USER32.DLL
(用户界面元素、窗口管理、消息传递)等 ,这些 Wine 实现的 DLL 通常以 Unix 共享对象(.so
文件)的形式存在,它们可以直接调用宿主操作系统的函数另外 还有 WineD3D ,将 Direct3D 和 DirectDraw API 调用翻译成 OpenGL 调用的核心组件 ,另外还有 DXVK 这种专注于将 Direct3D API 调用高效地翻译成 Vulkan 调用的支持。
\\n\\n\\n前面的 Steam 的 Proton 也是一个针对游戏优化的
\\nvkd3d
分支,负责将 D3D12 调用翻译为 Vulkan ,而 macOS 上或者还需要比如 MoltenVK 将 Vulkan 转为 Metal ?
当然,翻译 API 的局限性就不用多说了,还有一些依赖底层驱动支持的场景,很难在通用性上做到完美,当时理论上做到部分应用通用的场景应该可以,甚至在游戏领域反而更有优势?
\\n当然,还有另外一条途径就是直接跑虚拟机,或者说虚拟桌面,目前已经有不少人运行成功,比如就有博主用 Os-easy 虚拟机装上了Windows 11 :
\\n事实上 Linux 上运行 Win 虚拟机一直以来就有,用户只需选择镜像文件并完成基础配置,同样也可以在鸿蒙 PC 上使用Windows系统。
\\n安装完成后,用户可以在鸿蒙与 Windows 系统之间便捷切换,类似切换桌面的效果,这样也算是一种场景支持:
\\n当然,虚拟桌面的割裂感会更重,但是在通用软件场景下会相对更好,但是性能也许会更差一下?
\\n另外,目前也挺多觉得鸿蒙 PC 就是一个平板 PC 化的场景,其实这样也算是一个趋势?类似我前段时间一直在聊的 Android PC 化支持,目前 Android 桌面化已经集齐:
\\n例如下方就是 Android 下的外部显示器排列和切换支持:
\\n最后,貌似目前鸿蒙 PC 虽然能进终端,但是不开放 sudo 权限,apt 也没有?这部分能力不知道后续是否会开放,从 PC 角度看这部分能力还是有必要的:
\\n那么,对于鸿蒙 PC 场景,你有什么技术方向想聊的?
","description":"最近鸿蒙 PC 刚发布完,但是发布会没公布太多技术细节,基本上一些细节都是通过自媒体渠道获取,首先可以确定的是,鸿蒙 PC 本身肯定是无法「直接」运行 win 原本的应用,但是可以支持手机上「原生鸿蒙」的应用,细节上无非就是 UI 兼容下大屏模式的支持,比如下图是来自 差评XPIN 的鸿蒙 PC 截图: 那么问题来了,HarmonyOS 「卓易通 」 作为生态过渡的丰富支持,甚至在应用商店都可以无缝衔接,那么鸿蒙 PC 是否也可以有类似的场景?\\n\\n因为目前得到的消息是,鸿蒙 PC 不支持侧载 ,这个结论我也不保熟,只是在这个大条件下讨论,那么 鸿蒙…","guid":"https://juejin.cn/post/7503450078159470646","author":"恋猫de小郭","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-13T03:55:47.932Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fca4e22a30cd4402b762f365b11fcdba~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747713346&x-signature=ZzYhRhNFy%2BlaeLyAA%2FnQPsbimfw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2ca55bb6fd2e428f9e0b4eb03544a203~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747713346&x-signature=FZu%2FWX6CecnSdmP5YstgYqHi8zo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/68870ae3209d4bcf984b7abfd6cf3b6d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747713346&x-signature=HV3fPkuW3qlRlp7FU6OZ4j6Fra8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2e75e108a59f4cada51dc09e4e253797~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747713346&x-signature=0InvKUX3KCMxzSxiV904CoynSrU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/bd038d0a28e440dea3334f5a05fff1a2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747713346&x-signature=LUv0K36nDDx71GH3TpqJFJlY%2FV8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/22d4541ce94e42e8a0c7c4e8d90fd7b6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747713346&x-signature=JcFfTpvXDPbTTYTqJ0L5zk7KvP8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/73d3b12a2fe64fdb8a7b962d8684cad0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747713346&x-signature=hrllcnuimvfvuhLO5j0UewvgMRA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e67a1d0124c24d7d993b8ba0f6c99ce0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747713346&x-signature=0DbGPcFmLPKegruaXP%2B9GKSaCV4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/aac09f6032c0495caddd81ad64fdd6f5~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747713346&x-signature=21n3%2BKkOMCLYiORAqszcjowKOqw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3769f8be60764d6bb93e9e2ea971ef51~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747713346&x-signature=l6lOEUPbLsiRsrr7%2BY%2BdkIjniLU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a99a1522bf434b4cb7fb0992b2d8af2f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747713346&x-signature=458CxcT5gGJwcFDE2KApf%2B1pLpE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/07c5a53d216e447caac3cc940045cf18~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747713346&x-signature=yAtldUWX1ZPoEKjBc7pAZ4KGZVE%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","HarmonyOS","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"[Flutter 基础] - Flutter核心布局组件 - Row","url":"https://juejin.cn/post/7503370526377197605","content":"Flutter 的 Row
组件用于在水平方向排列子组件,是构建灵活布局的核心组件之一。它的用法其实和Column
基本一致,只是一个纵向列表布局,一个横向列表布局。\\n以下是 通过一些Row
的用法示例,包含核心属性及常见场景的解决方案。
Row
的基本结构如下:
Container(\\n height: 200,\\n width: 300,\\n decoration: BoxDecoration(\\n border: Border.all(color: Colors.black, width: 2)\\n ),\\n child: Row(\\n children: [\\n Container(width: 50, color: Colors.red),\\n Container(width: 50, color: Colors.green),\\n Container(width: 50, color: Colors.blue),\\n Container(width: 50, color: Colors.black),\\n ],\\n ),\\n),\\n
\\nmainAxisAlignment
控制子组件在水平方向(主轴)的对齐方式,默认为 MainAxisAlignment.start
(从左侧开始排列)。
\\n可选值:
mainAxisAlignment.start
:左对齐mainAxisAlignment.end
:右对齐mainAxisAlignment.center
:居中对齐mainAxisAlignment.spaceBetween
:均匀分布(首尾无空隙)mainAxisAlignment.spaceAround
:均匀分布(子组件两侧空隙相等)mainAxisAlignment.spaceEvenly
:完全均匀分布Row(\\n mainAxisAlignment: MainAxisAlignment.spaceEvenly,\\n children: [\\n Container(width: 50, color: Colors.red),\\n Container(width: 50, color: Colors.green),\\n Container(width: 50, color: Colors.blue),\\n Container(width: 50, color: Colors.black),\\n ],\\n),\\n
\\ncrossAxisAlignment
控制子组件在垂直方向(交叉轴)的对齐方式,默认为 CrossAxisAlignment.center
。
\\n可选值:
CrossAxisAlignment.start
:顶部对齐CrossAxisAlignment.end
:底部对齐CrossAxisAlignment.center
:垂直居中CrossAxisAlignment.stretch
:拉伸子组件高度(需子组件允许)Row(\\n mainAxisAlignment: MainAxisAlignment.spaceEvenly,\\n crossAxisAlignment: CrossAxisAlignment.stretch,\\n children: [\\n Container(width: 50, height: 100, color: Colors.red),\\n Container(width: 50, height: 100, color: Colors.green),\\n Container(width: 50, height: 100, color: Colors.blue),\\n Container(width: 50, height: 100, color: Colors.black),\\n ],\\n),\\n
\\nmainAxisSize
控制 Row
自身在主轴方向的占用空间:
MainAxisSize.max
(默认):占满父容器的宽度MainAxisSize.min
:根据子组件总宽度自适应 Container(\\n height: 200,\\n width: 300,\\n decoration: BoxDecoration(\\n border: Border.all(color: Colors.black, width: 2)\\n ),\\n child: Row(\\n mainAxisSize: MainAxisSize.max,\\n mainAxisAlignment: MainAxisAlignment.spaceEvenly,\\n crossAxisAlignment: CrossAxisAlignment.stretch,\\n children: [\\n Container(width: 50, height: 100, color: Colors.red),\\n Container(width: 50, height: 100, color: Colors.green),\\n Container(width: 50, height: 100, color: Colors.blue),\\n Container(width: 50, height: 100, color: Colors.black),\\n ],\\n ),\\n),\\n
\\n说明: 这个属性主要是控制Row
的宽度,设置max,则Row
的宽度会尽可能的大,和父组件有一样大,如果设置min,则Row
只会包裹子组件的大小。
textDirection
控制子组件的排列方向(从左到右或从右到左):
\\nRow(\\n textDirection: TextDirection.rtl, // 从右到左排列\\n children: [Text(\\"A\\"), Text(\\"B\\")],\\n)\\n
\\nverticalDirection
控制子组件在交叉轴的排列方向(默认 VerticalDirection.down
,从上到下):
Row(\\n verticalDirection: VerticalDirection.up, // 从下到上排列\\n children: [Container(height: 50), Container(height: 100)],\\n)\\n
\\n当需要子组件按比例分配剩余空间时,使用 Expanded
或 Flexible
。
强制子组件填满剩余空间,通过 flex
设置权重(默认1):
Row(\\n children: [\\n Expanded(flex: 2, child: Container(color: Colors.red)),\\n Expanded(flex: 1, child: Container(color: Colors.blue)),\\n ],\\n)\\n
\\n上述代码中,红色容器占2/3宽度,蓝色占1/3。
\\n与 Expanded
类似,但允许子组件不填满剩余空间(通过 fit
属性控制):
Row(\\n children: [\\n Flexible(fit: FlexFit.loose, child: Container(width: 100)),\\n Expanded(child: Container(color: Colors.green)),\\n ],\\n)\\n
\\n当子组件总宽度超过屏幕宽度时,Row
会抛出溢出错误(如 Right Overflowed by 42 pixels
)。
SingleChildScrollView
包裹 Row
,实现水平滚动:SingleChildScrollView(\\n scrollDirection: Axis.horizontal,\\n child: Row(children: [/* 长内容 */]),\\n)\\n
\\nListView
替代 Row
:ListView.separated(\\n scrollDirection: Axis.horizontal,\\n itemBuilder: (_, i) => ItemWidget(),\\n separatorBuilder: (_, __) => SizedBox(width: 10),\\n itemCount: 10,\\n)\\n
\\nExpanded
或 Flexible
限制子组件宽度比例。Row(\\n mainAxisAlignment: MainAxisAlignment.center,\\n children: [\\n Icon(Icons.access_time),\\n SizedBox(width: 8),\\n Text(\\"时间\\"),\\n ],\\n)\\n
\\nRow(\\n mainAxisAlignment: MainAxisAlignment.end,\\n children: [\\n ElevatedButton(onPressed: () {}, child: Text(\\"取消\\")),\\n SizedBox(width: 10),\\n ElevatedButton(onPressed: () {}, child: Text(\\"确定\\")),\\n ],\\n)\\n
\\nRow(\\n children: [\\n Expanded(flex: 1, child: Sidebar()),\\n Expanded(flex: 3, child: Content()),\\n Expanded(flex: 1, child: Ads()),\\n ],\\n)\\n
\\nText
或 Image
可能导致溢出,应包裹 Expanded
或 Container
。Expanded
:需要动态分配空间时,优先选择 Expanded
而非固定宽度。ListView
)。通过灵活调整 mainAxisAlignment
、crossAxisAlignment
和子组件的尺寸,可以快速实现复杂的横向布局结构。若需滚动,务必结合可滚动组件(如 SingleChildScrollView
)使用。
在 Flutter 中,Column
是一个用于垂直排列子组件的布局容器,类似于 Android 的 LinearLayout
(垂直方向)或 Web 的 Flexbox 布局。
以下是 Column
的详细用法解析,涵盖基础属性、布局行为、常见场景及注意事项。
Column
会将所有子组件(children
)按垂直方向依次排列。它的核心特性包括:
mainAxisAlignment
和 crossAxisAlignment
控制对齐方式。Column({\\n Key? key,\\n MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start, // 主轴对齐方式\\n CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center, // 交叉轴对齐方式\\n MainAxisSize mainAxisSize = MainAxisSize.max, // 主轴尺寸策略\\n TextDirection? textDirection, // 文本方向(影响对齐)\\n VerticalDirection verticalDirection = VerticalDirection.down, // 子组件排列方向(上到下/下到上)\\n TextBaseline? textBaseline, // 基线对齐(需配合 `crossAxisAlignment: CrossAxisAlignment.baseline`)\\n List<Widget> children = const <Widget>[], // 子组件列表\\n})\\n
\\nmainAxisAlignment
:主轴对齐方式控制子组件在垂直方向上的排列方式,可选值:
\\nMainAxisAlignment.start
:子组件从顶部开始排列(默认)。MainAxisAlignment.end
:子组件从底部开始排列。MainAxisAlignment.center
:子组件居中。MainAxisAlignment.spaceBetween
:首尾子组件贴边,中间均匀分布。MainAxisAlignment.spaceAround
:子组件周围均匀分布间距。MainAxisAlignment.spaceEvenly
:子组件和间距完全均匀分布, 和spaceAround的区别在于两边的边距不一样,spaceAround是两边的边距是中间的一半,而spaceEvenly是所有间距都一样。示例:均匀分布子组件
\\nColumn(\\n mainAxisAlignment: MainAxisAlignment.spaceEvenly,\\n children: [\\n Container(height: 50, color: Colors.red),\\n Container(height: 50, color: Colors.green),\\n Container(height: 50, color: Colors.blue),\\n ],\\n)\\n
\\ncrossAxisAlignment
:交叉轴对齐方式控制子组件在水平方向上的对齐方式,可选值:
\\nCrossAxisAlignment.start
:左对齐。CrossAxisAlignment.end
:右对齐。CrossAxisAlignment.center
:水平居中(默认)。CrossAxisAlignment.stretch
:拉伸子组件至填满水平空间。CrossAxisAlignment.baseline
:按文本基线对齐(需设置 textBaseline
,否则会报错),这种对齐方式用的非常少。示例:水平拉伸子组件
\\nColumn(\\n crossAxisAlignment: CrossAxisAlignment.stretch,\\n children: [\\n Container(height: 50, color: Colors.red),\\n Container(height: 50, color: Colors.green),\\n ],\\n)\\n
\\nmainAxisSize
:主轴尺寸策略MainAxisSize.max
:占满父组件的最大可用高度(默认),。MainAxisSize.min
:仅占用子组件所需高度。说明: 这个属性主要是控制column的高,设置max,则column的高度会尽可能的大,和父组件有一样大,如果设置min,则column只会包裹子组件的大小。\\n示例:紧凑布局
\\n Container(\\n height: 400,\\n width: 200,\\n decoration: BoxDecoration(\\n border: Border.all(color: Colors.black, width: 2)\\n ),\\n child: Column(\\n //设置max,则column的宽高和Container位height:400,\\n //min, 则column的高度位子组件的高度总和,height:50 * 3 = 150\\n mainAxisSize: MainAxisSize.max, \\n mainAxisAlignment: MainAxisAlignment.start,\\n crossAxisAlignment: CrossAxisAlignment.center,\\n children: [\\n Container(height: 50, width: 150, color: Colors.red),\\n Container(height: 50, width: 150, color: Colors.green),\\n Container(height: 50, width: 150, color: Colors.blue),\\n ],\\n ),\\n
\\nverticalDirection
:子组件排列方向VerticalDirection.down
:从上到下排列(默认)。VerticalDirection.up
:从下到上排列。Column(\\n children: [\\n Text(\\"Username\\"),\\n TextField(),\\n ElevatedButton(onPressed: () {}, child: Text(\\"Submit\\")),\\n ],\\n)\\n
\\nExpanded
分配剩余空间让中间子组件占据剩余高度:
\\nColumn(\\n children: [\\n Container(height: 100, color: Colors.red),\\n Expanded( // 占据剩余空间\\n child: Container(color: Colors.green),\\n ),\\n Container(height: 100, color: Colors.blue),\\n ],\\n)\\n
\\nSizedBox
:固定间距。Spacer
:按比例分配剩的空间。Column(\\n children: [\\n Container(height: 50, color: Colors.red),\\n SizedBox(height: 20), // 固定间距\\n Container(height: 50, color: Colors.green),\\n Spacer(),\\n Container(height: 50, color: Colors.blue),\\n Spacer(),\\n Container(height: 50, color: Colors.black),\\n ],\\n)\\n
\\nRow
或其它布局组件Column(\\n children: [\\n Row(\\n children: [\\n Icon(Icons.star),\\n Text(\\"Rating: 4.5\\"),\\n ],\\n ),\\n Divider(),\\n Text(\\"Description...\\"),\\n ],\\n)\\n
\\n当子组件的总高度超出屏幕时,Flutter 会抛出 RenderBox overflow
错误。解决方案:
Container(\\n height: 400,\\n width: 200,\\n decoration: BoxDecoration(\\n border: Border.all(color: Colors.black, width: 2)\\n ),\\n child: Column(\\n children: [\\n Container(height: 550, color: Colors.red),\\n SizedBox(height: 20), // 固定间距\\n Container(height: 50, color: Colors.green),\\n Spacer(),\\n Container(height: 50, color: Colors.blue),\\n Spacer(),\\n Container(height: 50, color: Colors.black),\\n ],\\n ),\\n),\\n
\\nSingleChildScrollView
SingleChildScrollView(\\n child: Column(\\n children: [/* 多个子组件 */],\\n ),\\n)\\n
\\nListView
(懒加载)ListView(\\n children: [/* 子组件 */],\\n)\\n
\\nContainer(\\n height: 300, // 固定高度\\n child: Column(\\n children: [/* 子组件 */],\\n ),\\n)\\n
\\n默认无滚动 Column
本身不支持滚动,需手动添加 SingleChildScrollView
或改用 ListView
。
Expanded
与 Flexible
Expanded
是 Flexible(fit: FlexFit.tight)
,强制占据剩余空间。Flexible
允许子组件按比例分配空间(flex
属性)。基线对齐 若使用 CrossAxisAlignment.baseline
,需指定 textBaseline
(如 TextBaseline.alphabetic
)。
性能优化 若子组件数量多或高度动态,优先使用 ListView
或 CustomScrollView
以支持懒加载。
Column
是 Flutter 中实现垂直布局的核心组件,适用于:
Expanded
或 Flexible
分配空间。Row
、Container
)。通过灵活调整 mainAxisAlignment
、crossAxisAlignment
和子组件的尺寸,可以快速实现复杂的垂直布局结构。若需滚动,务必结合可滚动组件(如 SingleChildScrollView
)使用。
前几天刚聊过 《Google 开始正式强制 Android 适配 16 K Page Size》 之后,被问到最多的问题是「怎么查看项目是否支持 16K Page Size」 ?其实有很多直接的方式,但是最难的是当你的项目有很多依赖时,怎么知道这个「不支持的动态库 so」 文件是哪个依赖?有不少人的项目里可能有几十个 so ,如果一个一个那场景可太\\"有爱\\"了。
\\n\\n\\n后面的脚本提供查找思路。
\\n
首先第一种方法用官方的脚本,保存下方脚本为 shell.sh
,然后执行 ./shell.sh src/main/jinLibs
,就可以检测到目录下所有动态库是否支持 16K:
#!/bin/bash\\n\\n# usage: alignment.sh path to search for *.so files\\n\\ndir=\\"$1\\"\\n\\nRED=\\"\\\\e[31m\\"\\nGREEN=\\"\\\\e[32m\\"\\nENDCOLOR=\\"\\\\e[0m\\"\\n\\nmatches=\\"$(find $dir -name \\"*.so\\" -type f)\\"\\nIFS=$\'\\\\n\'\\nfor match in $matches; do\\n res=\\"$(objdump -p ${match} | grep LOAD | awk \'{ print $NF }\' | head -1)\\"\\n if [[ $res =~ \\"2**14\\" ]] || [[ $res =~ \\"2**16\\" ]]; then\\n echo -e \\"${match}: ${GREEN}ALIGNED${ENDCOLOR} ($res)\\"\\n else\\n echo -e \\"${match}: ${RED}UNALIGNED${ENDCOLOR} ($res)\\"\\n fi\\ndone\\n\\n
\\n\\n\\n整个 Apk 的话,可以直接解压 Apk ,然后对动态库的目录用脚本扫描。
\\n
第二种方法就是通过 Google Play 的 app bundle 资源管理器页面直接查看,如果有问题,会看到类似的情况:
\\n另外,如果 so 没问题,但是还是提示你不支持 16 KB,那么很可能是你需要升级 AGP ,建议至少升级到 AGP 8+ ,最优是升级到 8.5.1 之后:
\\n没有问题的情况下是这样:
\\n第三种方法就是下载最新的 libchecker ,如果动态库都有“16 KB” ,那就是正常:
\\n还有一种方法就是使用 readelf 工具,通过终端对比 so 的 elf 对齐情况,工具一般位于 /Users/guoshuyu/Library/Android/sdk/ndk/21.4.7075529/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin
,通过以下命令可以输出对应参数:
./aarch64-linux-android-readelf -l /Users/guoshuyu/workspace/android/******/libs/arm64-v8a/libijkffmpeg.so\\n
\\n如下两种图所示:
\\n\\n\\n注意,是
\\n1000
才是 4K,而10000
是 65536 ,那就是64K 对齐,属于 16K 的 4倍,那「理论上」应该是对齐的
最后,你还可以在 Android Studio 里运行你的 App,然后 Android Studio 会提示存在哪些动态库没适配 16 KB :
\\n\\n\\n注意,目前需要 Android Studio Narwhal ,最新版 Canary 10,并且有个 Bug ,需要首次运行之后,关闭 Android Studio ,然后再次打开,再运行,才会弹出提示:
\\n\\n
最后,如果你发现存在动态库不适配,但是你又不知道这个动态库是哪个 aar 远程依赖的,可以通过下方脚本执行 ./gradlew findSoFileOrigins
来输出:
// 在你的模块级别 build.gradle 文件中添加此任务\\n// 例如: app/build.gradle\\n\\ntask findSoFileOrigins {\\n description = \\"扫描项目依赖的 AAR 文件,找出 .so 文件的来源。\\"\\n group = \\"reporting\\" // 将任务归类到 \\"reporting\\" 组下\\n\\n doLast {\\n // 用于存储 AAR 标识符及其包含的 .so 文件路径\\n // 键 (Key): AAR 的字符串标识符 (例如:\\"project :gsyVideoPlayer\\", \\"com.example.library:core:1.0.0\\")\\n // 值 (Value): 一个 Set 集合,包含该 AAR 内所有 .so 文件的路径 (字符串)\\n def aarSoFilesMap = [:]\\n\\n def variants = null\\n if (project.plugins.hasPlugin(\'com.android.application\')) {\\n variants = project.android.applicationVariants\\n } else if (project.plugins.hasPlugin(\'com.android.library\')) {\\n variants = project.android.libraryVariants\\n } else {\\n project.logger.warn(\\"警告: findSoFileOrigins 任务需要 Android 应用插件 (com.android.application) 或库插件 (com.android.library)。\\")\\n return\\n }\\n\\n if (variants == null || variants.isEmpty()) {\\n project.logger.warn(\\"警告: 未找到任何变体 (variants) 来处理。\\")\\n return\\n }\\n\\n variants.all { variant ->\\n project.logger.lifecycle(\\"正在扫描变体 \'${variant.name}\' 中的 AAR 依赖以查找 .so 文件...\\")\\n\\n // 获取该变体的运行时配置 (runtime configuration)\\n def configuration = variant.getRuntimeConfiguration()\\n\\n try {\\n // 配置一个构件视图 (artifact view) 来精确请求 AAR 类型的构件\\n def resolvedArtifactsView = configuration.incoming.artifactView { view ->\\n view.attributes { attributes ->\\n // 明确指定我们只对 artifactType 为 \'aar\' 的构件感兴趣\\n // AGP 也常用 \\"android-aar\\",如果 \\"aar\\" 效果不佳,可以尝试替换\\n attributes.attribute(Attribute.of(\\"artifactType\\", String.class), \\"aar\\")\\n }\\n // lenient(false) 是默认行为。如果设为 true,它会尝试跳过无法解析的构件而不是让整个视图失败。\\n // 但如果像之前那样,是组件级别的变体选择失败 (如 gsyVideoPlayer),lenient 可能也无法解决。\\n // view.lenient(false)\\n }.artifacts // 获取 ResolvedArtifactSet\\n\\n project.logger.info(\\"对于变体 \'${variant.name}\',从配置 \'${configuration.name}\' 解析到 ${resolvedArtifactsView.artifacts.size()} 个 AAR 类型的构件。\\")\\n\\n resolvedArtifactsView.each { resolvedArtifactResult ->\\n // resolvedArtifactResult 是 ResolvedArtifactResult 类型的对象\\n File aarFile = resolvedArtifactResult.file\\n // 获取组件的标识符,这能告诉我们依赖的来源\\n // 例如:\\"project :gsyVideoPlayer\\" 或 \\"com.google.android.material:material:1.7.0\\"\\n String aarIdentifier = resolvedArtifactResult.id.componentIdentifier.displayName\\n\\n aarSoFilesMap.putIfAbsent(aarIdentifier, new HashSet<String>())\\n\\n if (aarFile.exists() && aarFile.name.endsWith(\'.aar\')) {\\n // project.logger.info(\\"正在检查 AAR: ${aarIdentifier} (文件: ${aarFile.name})\\")\\n try {\\n project.zipTree(aarFile).matching {\\n include \'**/*.so\' // 匹配 AAR 中的所有 .so 文件\\n }.each { File soFileInZip ->\\n aarSoFilesMap[aarIdentifier].add(soFileInZip.path)\\n }\\n } catch (Exception e) {\\n project.logger.error(\\"错误: 无法检查 AAR 文件 \'${aarIdentifier}\' (路径: ${aarFile.absolutePath})。原因: ${e.message}\\")\\n }\\n } else {\\n if (!aarFile.name.endsWith(\'.aar\')) {\\n project.logger.debug(\\"跳过非 AAR 文件 \'${aarFile.name}\' (来自: ${aarIdentifier}),其构件类型被解析为 AAR。\\")\\n } else {\\n project.logger.warn(\\"警告: 来自 \'${aarIdentifier}\' 的 AAR 文件不存在: ${aarFile.absolutePath}\\")\\n }\\n }\\n }\\n\\n } catch (Exception e) {\\n // 这个 catch 块会捕获解析构件视图时发生的错误\\n // 这可能仍然包括之前遇到的 \\"Could not resolve all artifacts for configuration\\" 错误,\\n // 如果问题非常根本,即使是特定的构件视图也无法克服。\\n project.logger.error(\\"错误: 无法为配置 \'${configuration.name}\' 解析 AAR 类型的构件。\\" +\\n \\"可能项目设置中存在依赖变体匹配问题,\\" +\\n \\"详细信息: ${e.message}\\", e) // 打印异常堆栈以获取更多信息\\n project.logger.error(\\"建议: 请检查项目依赖(尤其是本地子项目如 \':xxxxx\')的构建配置,\\" +\\n \\"确保它们能正确地发布带有标准 Android 库属性(如组件类别、构建类型,以及适用的 Kotlin 平台类型等)的变体。\\")\\n // 如果希望任务在此处停止而不是尝试其他变体,可以取消下一行的注释\\n // throw e\\n }\\n }\\n\\n // 打印结果\\n if (aarSoFilesMap.isEmpty()) {\\n project.logger.lifecycle(\\"\\\\n在所有已处理变体的可解析 AAR 依赖中均未找到 .so 文件,或者依赖解析失败。\\")\\n } else {\\n println \\"\\\\n--- AAR 依赖中的 .so 文件来源 ---\\"\\n // 按 AAR 标识符排序以获得一致的输出\\n aarSoFilesMap.sort { it.key }.each { aarId, soFileList ->\\n if (!soFileList.isEmpty()) {\\n println \\"${aarId}:\\" // 例如:project :gsyVideoPlayer: 或 com.some.library:core:1.0:\\n soFileList.sort().each { soPath -> // 对 .so 文件路径排序\\n println \\" - ${soPath}\\" // 例如: - jni/armeabi-v7a/libexample.so\\n }\\n }\\n }\\n println \\"----------------------------------\\"\\n }\\n project.logger.lifecycle(\\"任务执行完毕。要再次运行此任务,请执行: ./gradlew ${project.name}:${name}\\")\\n }\\n}\\n\\n
\\n\\n\\n这个脚本我是在 APG 8+ 下测试,不同 AGP 可能存在细微 API 差异,思路上一样。
\\n
当然,最终还是要在 16 KB 环境运行没有崩溃才行, 在之前的文章我就分享过,很多 so 查看时虽然分页是 16K 或者 64K ,但是它还是有问题的,跑在 16K 上是会崩溃的,具体原因有 NDK 工具可能过老之类:
\\n\\n\\n当时是 NDK10e 等版本,编译出来的 so 都是两个 LOAD 段的 Align 是
\\n10000(65536)
, 也就是 64K 对齐,属于 16K 的 4倍,那「理论上」应该是对齐的,但是跑在 16K 上会 crash ,不过 crash 提示也不是 so 不对齐,而是在某段代码执行时出现 crash,并且你定位到的地址代码会很奇葩。
测试环境可以使用模拟器,一般适配 16 KB 的就是 arm64 ,所以 x86_64 模拟器基本没用,而且需要 Android studio Koala Feature Drop
之后的版本才行:
如果你的 Apk 没适配 16 KB ,那么在 Android 16 的 16 KB 模拟器上会看到这样的提示:
\\n目前在 React Native 和 Flutter 都已经支持了 16 KB:
\\n比如你的 RN 版本太老,就是看到类似下面的场景:
\\n最后,如果你还没适配或者了解 16 KB,可以参考一下文章:
\\n\\n\\nFlutter 3.29\\nmacOS Sequoia 15.4.1\\nXcode 16.3
\\n
在UIKit中,通过ViewController控制数据在视图上展现,多个ViewController组合在一起构建复杂的用户界面。在Flutter中,因为所有都是Widget,所以ViewController相关的功能也由Widget来承担。
\\n在UIKit中可以重写自定义控制器的生命周期的方法,或注册AppDelegate的回调。在Flutter3.13前,没有这个概念,但是可以通过监听WidgetsBinding
观察者和didChangeAppLifecycleState()
改变事件来实现
import \'package:flutter/material.dart\';\\n\\nvoid main() {\\n runApp(const MainApp());\\n}\\n\\nclass MainApp extends StatelessWidget {\\n const MainApp({super.key});\\n @override\\n Widget build(BuildContext context) {\\n return const MaterialApp(\\n home: Scaffold(body: Center(child: BindingObserver())),\\n );\\n }\\n}\\n\\nclass BindingObserver extends StatefulWidget {\\n const BindingObserver({super.key});\\n\\n @override\\n State<BindingObserver> createState() => _BindingObserverState();\\n}\\n\\nclass _BindingObserverState extends State<BindingObserver>\\n with WidgetsBindingObserver {\\n @override\\n void initState() {\\n super.initState();\\n // 1.添加App事件变化的观察者\\n WidgetsBinding.instance.addObserver(this);\\n }\\n\\n @override\\n void dispose() {\\n WidgetsBinding.instance.removeObserver(this);\\n super.dispose();\\n }\\n\\n @override\\n // 2.监听app生命周期变化的事件\\n void didChangeAppLifecycleState(AppLifecycleState state) {\\n super.didChangeAppLifecycleState(state);\\n switch (state) {\\n case AppLifecycleState.detached:\\n _onDetached();\\n /// On all platforms, this state indicates that the application is in the default running mode for a running application that has input focus and is visible.\\n /// 应用可见且能响应用户的输入,切回前台会触发\\n case AppLifecycleState.resumed:\\n _onResumed();\\n /// At least one view of the application is visible, but none have input focus. The application is otherwise running normally.\\n /// 应用程序处于非活跃状态,并且未接收用户输入。此事件仅适用于 iOS,因为 Android 上没有对应的事件。\\n /// 切到后台先触发这个方法\\n case AppLifecycleState.inactive:\\n _onInactive();\\n /// All views of an application are hidden, either because the application is about to be paused (on iOS and Android), or because it has been minimized or placed on a desktop that is no longer visible (on non-web desktop), or is running in a window or tab that is no longer visible (on the web).\\n /// 所有的应用视图被隐藏,或者应用被暂停\\n case AppLifecycleState.hidden:\\n _onHidden();\\n /// The application is not currently visible to the user, and not responding to user input.\\n /// When the application is in this state, the engine will not call the [PlatformDispatcher.onBeginFrame] and [PlatformDispatcher.onDrawFrame] callbacks.\\n /// This state is only entered on iOS and Android.\\n /// 应用当前不可见,不响应用户的输入,但依然在后台运行,引擎不会回调PlatformDispatcher.onBeginFrame 和 PlatformDispatcher.onDrawFrame \\n case AppLifecycleState.paused:\\n _onPaused();\\n }\\n }\\n\\n void _onDetached() => print(\'detached\');\\n void _onResumed() => print(\'resumed\');\\n void _onInactive() => print(\'inactive\');\\n void _onHidden() => print(\'hidden\');\\n void _onPaused() => print(\'paused\');\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\\"监听事件\\")),\\n body: Center(child: Text(\\"生命周期\\")),\\n );\\n }\\n}\\n
\\nFlutter 3.13后通过设置AppLifecycleListener
来实现响应生命周期变更的功能。
import \'package:flutter/material.dart\';\\n\\nvoid main() {\\n runApp(const MainApp());\\n}\\n\\nclass MainApp extends StatelessWidget {\\n const MainApp({super.key});\\n @override\\n Widget build(BuildContext context) {\\n return const MaterialApp(\\n home: Scaffold(body: Center(child: BindingObserver())),\\n );\\n }\\n}\\n\\nclass BindingObserver extends StatefulWidget {\\n const BindingObserver({super.key});\\n\\n @override\\n State<BindingObserver> createState() => _BindingObserverState();\\n}\\n\\nclass _BindingObserverState extends State<BindingObserver> {\\n /// 1. 定义观察者属性\\n late final AppLifecycleListener _listener;\\n\\n @override\\n void initState() {\\n super.initState();\\n /// 2. 添加App事件变化的观察者\\n _listener = AppLifecycleListener(onStateChange: _onStateChanged);\\n }\\n \\n /// 3. 回调方法\\n void _onStateChanged(AppLifecycleState state) {\\n switch (state) {\\n case AppLifecycleState.detached:\\n _onDetached();\\n case AppLifecycleState.resumed:\\n _onResumed();\\n case AppLifecycleState.inactive:\\n _onInactive();\\n case AppLifecycleState.hidden:\\n _onHidden();\\n case AppLifecycleState.paused:\\n _onPaused();\\n }\\n }\\n\\n void _onDetached() => print(\'detached\');\\n void _onResumed() => print(\'resumed\');\\n void _onInactive() => print(\'inactive\');\\n void _onHidden() => print(\'hidden\');\\n void _onPaused() => print(\'paused\');\\n\\n @override\\n void dispose() {\\n // Do not forget to dispose the listener\\n _listener.dispose();\\n super.dispose();\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\\"监听事件\\")),\\n body: Center(child: Text(\\"生命周期\\")),\\n );\\n }\\n}\\n
\\nFlutter 设置了一些主题,可以实现统一更改文本和 UI 组件的样式等。
\\nimport \'package:flutter/material.dart\';\\n\\nvoid main() {\\n runApp(const ThemePage());\\n}\\n\\nclass ThemePage extends StatefulWidget {\\n const ThemePage({super.key});\\n\\n @override\\n State<ThemePage> createState() => _ThemePageState();\\n}\\n\\nclass _ThemePageState extends State<ThemePage> {\\n Brightness brightness = Brightness.light;\\n\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n title: \\"Theme 主题修改\\",\\n // 1. 创建主题\\n theme: ThemeData(brightness: brightness, primarySwatch: Colors.blue),\\n\\n home: Scaffold(\\n appBar: AppBar(title: Text(\\"Theme 主题修改\\")),\\n body: Column(\\n children: [\\n ElevatedButton(\\n onPressed: () {\\n setState(() {\\n brightness = Brightness.light;\\n });\\n },\\n child: Text(\\"切换到日间主题\\"),\\n ),\\n ElevatedButton(\\n // 2. 点击按钮触发\\n onPressed: () {\\n // 3. 通知Flutter\\n setState(() {\\n // 4. 设置亮度,更新主题\\n brightness = Brightness.dark;\\n });\\n },\\n child: Text(\\"切换到夜间主题\\"),\\n ),\\n ],\\n ),\\n ),\\n );\\n }\\n}\\n\\n
\\nFlutter要使用自定义的字体可以在pubspec.yaml
文件中添加
fonts:\\n - family: google_kavivanar\\n fonts:\\n - asset: static/font/google_kavivanar.ttf\\n
\\nText(\\n \\"Theme 123\\",\\n style: TextStyle(fontFamily: \'google_kavivanar\'),\\n),\\n
\\nText
widget有TextStyle
对象,可以通过这个属性来设置样式
在Flutter中使用assets表示资源
\\n在pubspec.yaml
声明资源
assets:\\n - static/data.json\\n
\\niOS中图片的资源是1.0x,2.0x,3.0x格式的,在Flutter中比如图片是在static/images下的,那不同倍数图片的存放方式。
\\nstatic/images/my_icon.png \\nstatic/images/2.0x/my_icon.png // 2.0x image\\nstatic/images/3.0x/my_icon.png // 3.0x image\\n
\\n在pubspec.yaml
声明图片资源
assets:\\n - static/images/my_icon.png\\n
\\n然后通过AssetImage或Image.asset来访问
\\nimage: AssetImage(\'static/images/my_image.png\'),\\n\\n@override\\nWidget build(BuildContext context) {\\n return Image.asset(\'static/images/my_image.png\');\\n}\\n
\\n在UIKit中,通常是在提交时查询对应的输入框的当前值,因为Flutter的Widgets是不可变的,如何对用户的输入操作进行处理,
\\n针对TextFile
或TextFormField
,可以通过提供一个TextEditingController
来获取用户输入
import \'package:flutter/material.dart\';\\n\\nvoid main() {\\n runApp(MaterialApp(title: \\"TextFile\\", home: MyForm()));\\n}\\n\\nclass MyForm extends StatefulWidget {\\n const MyForm({super.key});\\n\\n @override\\n State<MyForm> createState() => _MyFormState();\\n}\\n\\nclass _MyFormState extends State<MyForm> {\\n // 1. 创建text控制器来获取textFiled的值\\n final myController = TextEditingController();\\n\\n @override\\n void dispose() {\\n // 4. 清理生成的myController\\n myController.dispose();\\n super.dispose();\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: const Text(\'Retrieve Text Input\')),\\n body: Padding(\\n padding: const EdgeInsets.all(16),\\n // 2.TextField绑定控制器\\n child: TextField(controller: myController),\\n ),\\n floatingActionButton: FloatingActionButton(\\n /// 3. 当用户点击按钮时,弹出dialog,显示textField的值\\n onPressed: () {\\n showDialog(\\n context: context,\\n builder: (context) {\\n return AlertDialog(content: Text(myController.text));\\n },\\n );\\n },\\n tooltip: \'Show me the value!\',\\n child: const Icon(Icons.text_fields),\\n ),\\n );\\n }\\n}\\n\\n
\\nCenter(\\n child: TextField(decoration: InputDecoration(hintText: \'textField占位信息\')),\\n)\\n
\\n在TextField的onSubmitted方法中,判断textField的输入是否合法,若不合法,可以在
\\nchild: TextField(\\n controller: myController,\\n // 1. 拦截事件\\n onSubmitted:\\n (text) => {\\n // 2. 通知Flutter状态变更\\n setState(() {\\n // 3. 判断textField的内容是否合法,不合法则在build方法刷新时显示\\n if (!isEmail(text)) {\\n _errorText = \'错误: 邮箱地址不合法\';\\n } else {\\n _errorText = null;\\n }\\n }),\\n },\\n decoration: InputDecoration(\\n hintText: \\"邮箱\\",\\n errorText: _errorText,\\n ),\\n),\\n\\n\\n
\\n在过去的 2025 一季度里,iOS 存在不少大坑,这些大坑基本不是 Flutter 的问题,很大一部分其实和 iOS 本身和 MacOS 升级带来的 bug 有关系。
\\n\\n\\n适配系统 bug 也叫适配。
\\n
首先就是之前我们聊过的 《iOS 18.4 beta mprotect failed: Permission denied》 问题 #163984,在 iOS 18.4 beta 的时候, debug 运行会有 Permission denied
的相关错误提示,问题其实就是 Dart VM 在初始化时,对内核文件「解释运行(JIT)」时出现权限不足的问题。
这个问题是 Dart VM 虽然在 Debug 模式下是通过 JIT 模式解释执行,但是从 Dart 2.0 之后就不再支持直接从源码运行,对于 Dart 代码现在会统一编译成一种「预处理」形式的二进制 dill 文件:
\\n\\n\\n此时 JIT 运行的是一个未签名的二进制文件,并且需要直接 hotload ,也就是需要 Dart VM 在运行时根据 Kernel 二进制文件生成机械码,并且在可以接受 hotload 的热更新,所以它是通过 VM 来“解释”和“生成“,所以它会需要 mprotect 的系统调用。
\\n
利用 mprotect 动态修改内存的可读写也算是一种比较 hack 的操作,一开始大家以为是 iOS 想要封杀这种 “后门漏洞”,可是谁知道,iOS 18 beta2 该“漏洞”又可以正常使用了,目前看起来更多是 iOS 系统在版本更新时出现的错误封杀:
\\n然后同样是之前聊过的 《「ITMS-90048」This bundle is invalid 》 的 #166367,不上用户在升级到 macOS 15.4 后发现,通过命令行打包的 ipa 在提交后会出现 ITMS-90048 被拒绝问题:
\\n这个错误的核心原因是在提交给 App Store Connect 的归档文件 (.xcarchive
) 里,包含了一个不允许存在的隐藏文件 ._Symbols
。
而出现这个 bug 的原因,大概率在于 macOS 15.4对内置 rsync
的重大修订,在构建或归档过程中,系统对 Symbols
文件进行了某种操作(如 rsync),导致 macOS 生成了对应的 ._Symbols
元数据文件,并且这个隐藏文件被错误地打包进了 .xcarchive
文件
\\n\\n在 Xcode 里通过
\\nProdict > Archive
这种方式来提交,目前这种方式并不会有这个问题。
解决问题很简单,如果已经是 macOS 15.4 的用户,最简单的做法就是使用 Xcode 的 Prodict > Archive
,或者手动删除该文件:
bash 代码解读复制代码unzip -q app.ipa -d x\\nrm -rf app.ipa x/._Symbols\\ncd x\\nzip -rq ../app.ipa .\\ncd ..\\nrm -rf x\\n
\\n或者 flutter build ipa --release
之后,执行一个 ./cleanup.sh
:
sh 代码解读复制代码IPA_PATH=\\"build/ios/ipa/your_app_name.ipa\\"\\n# export IPA_PATH=\\"$(find \\"build/ios/ipa\\" -name \'*.ipa\' -type f -print -quit)\\"\\n\\nif [ -f \\"$IPA_PATH\\" ]; then\\n echo \\"Checking for unwanted files like ._Symbols in $IPA_PATH\\"\\n unzip -l \\"$IPA_PATH\\" | grep ._Symbols && zip -d \\"$IPA_PATH\\" ._Symbols/ || echo \\"No ._Symbols found\\"\\nelse\\n echo \\"IPA not found at $IPA_PATH\\"\\nfi\\n
\\n当然,暂时不要升级 macOS 15.4 是最好的,不过苹果说这个问题已经修复,所以可以确定基本就是系统升级的 bug:
\\n接下来的问题是 #166333 的 「Could not register as server for FlutterDartVMServicePublisher」 ,问题还是和 macOS 15.4 和 Xcode 关联,主要影响的是 macOS 15.4 上的模拟器,会让 iOS 模拟器上的 flutter attach
不起作用。
\\n\\n大概问题就是调试时,将 DartVM 发布为 mDNS 服务有问题。
\\n
如果需要在 iOS 模拟器上使用 flutter attach
,可以从 Xcode 复制 Dart VM Service 的 url ,然后在命令行进行传递 flutter attach --debug-url
:
其实这个问题大部分时候不会影响正常开发和发布,只是对于有洁癖的开发而言,确实有点恶心,而 Flutter 官方的修复也很值得吐槽,就是把 error 变成 warning:
\\n另外值得一提的是,如果在 macOS 上通过 TestFlight 安装 App 并允许本地网络访问,之后在模拟器中再安装的 App 也可以正常工作。
\\n\\n\\n只能说,一个 Bug 背后,总有一个更骚的 fix 途径。
\\n
目前苹果也确定了这是它们的 bug 导致,只能说这一届是我看到最差的 iOS/macOS :
\\n接着就是 #165656 的 hot restart 问题,在 iOS 上会出现 hot restart 需要等待几分钟的问题,这个问题目前看起来和 Flutter 里的 iproxy 有关系:
\\n\\n\\n这里的 iproxy 是一个命令行工具,一般用在和 USB 连接在 macOS 上的 iOS 设备进行通信的场景,它是 usbmuxd(USB Multiplex Daemon)的一部分,iproxy 的主要功能是将本地的 TCP 端口映射到 iOS 设备上的端口,从而实现通过 USB 进行网络通信而无需依赖 Wi-Fi。
\\n
目前看起来这个问题主要是由 iproxy
的内部错误引起,这个错误会导致偶尔的数据丢失(并非所有数据都在主机和设备之间转发),主要是由 select
+ send
的时候,比如 select
时 fd
是可写的,但随后的 send(fd, ...)
返回 EAGAIN
而不是 Success。
而好消息是, iproxy
的新版本不受影响:不是因为处理 send
返回的 EAGAIN
,而是新版本切换到 poll
而不是 select
,从而没有出现类似的问题。
目前临时的修复方式,可以尝试将 Flutter SDK 中的 iproxy
替换为 brew 中的版本:
$ brew install libimobiledevice\\n\\n# Go to the root of the Flutter SDK\\n$ cd flutter_sdk\\n\\n# Kill old versions of iproxy and related binaries (though iproxy alone should be enough)\\n$ rm -rf bin/cache/artifacts/usbmuxd/* bin/cache/artifacts/libimobiledevice/*\\n\\n# Copy newer versions installed by brew\\n$ cp `which iproxy` bin/cache/artifacts/usbmuxd/\\n$ cp `which idevicescreenshot` bin/cache/artifacts/libimobiledevice\\n$ cp `which idevicesyslog` bin/cache/artifacts/libimobiledevice\\n
\\n\\n\\n另外这个问题,在使用 cpu profiler 的情况也可能会出现 lost connection to device 。
\\n
除此之外,#167343 目前在 iOS 18.5 Public Beta 上会出现某些 font weights 会出现 \\"thin font\\" 问题:
\\n\\n\\n猜测也可能只是使用
\\nCupertinoSystemDisplay
字体系列的字体导致,切换到CupertinoSystemText
可以解决字体粗细问题,但字母间距将比以前更紧密。
虽然不知道是什么问题,但是 iOS 18.5 beta4 修复了这个问题,只能说,苹果现在的稳定性是真的越来越不可靠了:
\\n最后,还有个 #138464 的老问题,就是在 iOS 内输入某些文本后点击输入框,大概是因为 autocorrect
的缘故,会导致偶尔可能出现 crash :
目前看起来 3.30 已经在处理删除崩溃的断言,如果你想临时解决,可以试试:keyboardType:TextInputType.name
+ autocorrect:false
,因为其他 TextInputType 貌似 autocorrect 的关闭没起作用。
好了,基本上这就是 2025 年上半年你大概率会遇到的 iOS 大坑,其他的都是一些细枝末节的小事,比如修复了 iOS 上 PlatformView 出现闪烁问题之类。
\\n那么,2025 年你是否还遇到什么奇葩 iOS 大坑?
","description":"在过去的 2025 一季度里,iOS 存在不少大坑,这些大坑基本不是 Flutter 的问题,很大一部分其实和 iOS 本身和 MacOS 升级带来的 bug 有关系。 适配系统 bug 也叫适配。\\n\\n首先就是之前我们聊过的 《iOS 18.4 beta mprotect failed: Permission denied》 问题 #163984,在 iOS 18.4 beta 的时候, debug 运行会有 Permission denied 的相关错误提示,问题其实就是 Dart VM 在初始化时,对内核文件「解释运行(JIT…","guid":"https://juejin.cn/post/7502875709885513764","author":"恋猫de小郭","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-12T05:37:10.372Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/8bdd1b52c29848428a226be8759083dc~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747633029&x-signature=0dE68WoVxITKS3RG15ARPQb0s9o%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/74a5fd9f8163450bb306d792b2ad7a51~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747633029&x-signature=p2I8B9MS2F%2Bxmx3CkFstHHudFIU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/44b75e7527714547a24c06519c9088dc~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747633029&x-signature=iylKT5IHnNYpmXvCkDJJwQ3z8Kc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f6619b1b535249bc87f11ebd7a16a8f0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747633029&x-signature=toyRlLDjAdERNu3vn%2BQ85SSeZ1Q%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/268b9082fe894762b2c4c3c57c4b7027~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747633029&x-signature=m0El6usnvJocCl7jfYZH9iS2cxc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9659edab1df84772af59979d3bfb1e38~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747633029&x-signature=nKsGmezfMMs5%2B45kz14S%2FbMeA5k%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/8986afb3418c4cc58cbaa04607b2ed2e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747633029&x-signature=5yryGai6xpK09MhYuXyRmjFHIyQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/878d15bcc9c44d53b91571fea970889a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747633029&x-signature=I6iLihlf8xCMe3bAs8E1kD7iBcY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6450663d1a9248efa0d3576688d828ff~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747633029&x-signature=mZcq4FVFAnN8s9aTjLu2Nu3pNnY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e90fcab018bd4572a2ddadca9d4c20e6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747633029&x-signature=wbDEK6mwYqP9jLhZGhVq6E9E9v4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e278f683a23c459184cba146ed693eee~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747633029&x-signature=PBHBGjdnaGkCUZp9fQoY2pEvfts%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/53078130f6d1485db47581a750348a2e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747633029&x-signature=X5q4qQUGKWmsjMuJPPJOPCefMu8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7af6662c2ae24db9a3a15e575c9321f3~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747633029&x-signature=JD9jykAEGQVg5%2BZKTjqSFV9RA0I%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["iOS","Flutter","Android","前端"],"attachments":null,"extra":null,"language":null},{"title":"Flutter Widget、State 和 BuildContext","url":"https://juejin.cn/post/7503017777064017946","content":"最近有很多来私信问,要不要去做美区的第三方支付,所以特意阐述一下个人观点。仅供参考!
美国联邦法官当地时间4月30日裁定,苹果公司违反了此前的一项法院命令,必须进行整改以更好地促进市场竞争。该法院命令要求苹果开放应用商店,“这是禁令,不是谈判。”
\\n2025 年 5 月 3 日,苹果宣布允许美国用户通过 Spotify 外部链接订阅,首次实质性绕过 30%“苹果税” 。这距离 2019 年 Spotify 向欧盟投诉已过去六年,期间经历 18 亿欧元罚单、技术封锁与监管拉锯,最终在欧盟《数字市场法》倒逼下达成妥协。
\\n只能说这是巨头与巨头之间较量,无非是两个大力士在掰手腕。
\\n那么出海的国内开发者要不要跟进,尝试第三方通道?
客观建议:等等看。
要知道Spotify
的胜利源于大厂和大厂的较量,无论是背景实力都并非普通开发者可以匹敌的。正所谓只许州官放火,不许百姓点灯
。
从收益角度来看,大厂肯定不是靠一个平台一种产品实现盈利。Appstore的覆灭只会影响部分收益,不伤其筋骨。如果是普通开发者尤其是业务线单一,产品单一。一旦被\\"钓鱼执法\\",那么可以说是一朝回到解放前
。
看到只是美区支持的时候,此事不一般!
\\n众所周知苹果审核被拒最痛苦的两大条款:
\\n只是开放了美区,那么对于其他地区又该如何判定?美区支付第三方,那么其他国家和地区呢?业务层面的分流:美区、非美区?
\\n那么这种又会不会被苹果判定为隐藏功能?如果只是美区的出海者还好,如果不只是美区那么将会是一个很棘手的问题。
\\n所以,综上所述建议开发者还是谨慎入坑,毕竟吃一顿和吃顿顿,要分得清
。
遵守规则,方得长治久安
,最后祝大家大吉大利,今晚过审!
在 Flutter 中,Container
是一个多功能且常用的布局和装饰组件。它可以设置尺寸、背景、边距、对齐方式等属性,甚至可以添加装饰效果(如圆角、边框、阴影)或进行 2D/3D 变换。\\n这篇文章主要是通过一些示例来了解一下Container
的基本用法:
Container
是一个组合了多个基础功能的容器组件,内部封装了以下特性:
width
, height
)margin
)padding
)color
, decoration
)alignment
)child
)transform
)Container({\\n Key? key,\\n AlignmentGeometry? alignment, // 子组件对齐方式\\n EdgeInsetsGeometry? padding, // 内边距\\n Color? color, // 背景色(与 decoration 互斥)\\n Decoration? decoration, // 装饰(背景、边框、圆角等)\\n Decoration? foregroundDecoration, // 前景装饰\\n double? width, // 宽度\\n double? height, // 高度\\n BoxConstraints? constraints, // 布局约束(优先级高于 width/height)\\n EdgeInsetsGeometry? margin, // 外边距\\n Matrix4? transform, // 变换矩阵(如旋转、平移)\\n AlignmentGeometry? transformAlignment,\\n Widget? child, // 子组件\\n Clip clipBehavior = Clip.none, //\\n})\\n
\\nContainer(\\n width: 100,\\n height: 100,\\n color: Colors.blue,\\n child: Text(\\"Hello Container\\"),\\n)\\n
\\nContainer(\\n margin: EdgeInsets.all(20), // 外边距 20\\n padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5), // 内边距,还可以通过EdgeInsets.only(top:1,right:2,bottom:3,left:4)\\n color: Colors.green,\\n child: Text(\\"Margin & Padding\\"),\\n)\\n
\\ndecoration
属性)通过 BoxDecoration
实现复杂装饰效果:
Container(\\n alignment: Alignment.center,\\n width: 150,\\n height: 48,\\n decoration: BoxDecoration(\\n color: Colors.red,\\n borderRadius: BorderRadius.circular(10),\\n // 圆角\\n border: Border.all(color: Colors.black, width: 2),\\n // 边框\\n boxShadow: [\\n BoxShadow(color: Colors.grey, blurRadius: 5, offset: Offset(2, 2)),\\n ],\\n ),\\n child: Text(\\"Decorated Container\\", style: TextStyle(color: Colors.white),),\\n),\\n
\\n// 圆形\\nContainer(\\n alignment: Alignment.center,\\n width: 150,\\n height: 150,\\n decoration: BoxDecoration(\\n color: Colors.purple,\\n shape: BoxShape.circle, // 圆形\\n ),\\n child: Text(\\"Decorated Container\\", style: TextStyle(color: Colors.white),),\\n),\\n\\n// 圆角矩形\\nContainer(\\n decoration: BoxDecoration(\\n borderRadius: BorderRadius.circular(20),\\n // ...\\n)\\n
\\nContainer(\\n alignment: Alignment.centerRight, // 子组件右对齐\\n width: 200,\\n height: 50,\\n color: Colors.orange,\\n child: Text(\\"Right-aligned\\"),\\n)\\n
\\nContainer(\\n transform: Matrix4.rotationZ(0.1), // 旋转 0.1 弧度\\n // transform: Matrix4.translationValues(50, 0, 0), // 平移\\n child: Text(\\"Transformed Container\\"),\\n)\\n
\\nconstraints
)通过 BoxConstraints
设置最小/最大宽高:
Container(\\n constraints: BoxConstraints(\\n minWidth: 100,\\n maxWidth: 300,\\n minHeight: 50,\\n ),\\n child: Text(\\"Constrained Container\\"),\\n)\\n
\\ncolor
与 decoration
的冲突 如果同时设置 color
和 decoration
,会抛出错误。应在 decoration
中使用 color
:
Container(\\n decoration: BoxDecoration(color: Colors.blue), // 正确\\n // color: Colors.blue, // 错误!\\n)\\n
\\n默认尺寸行为
\\nContainer
没有子组件(child
为 null
),且没有设置 width
/height
/constraints
,它会尽可能大。布局优先级 constraints
的优先级高于 width
和 height
。
Container
是 Flutter 中最灵活的布局组件之一,适用于以下场景:
transform
进行简单的 2D 变换。通过组合不同的属性,灵活的运用不同的组合可以实现复杂的 UI 效果。
","description":"在 Flutter 中,Container 是一个多功能且常用的布局和装饰组件。它可以设置尺寸、背景、边距、对齐方式等属性,甚至可以添加装饰效果(如圆角、边框、阴影)或进行 2D/3D 变换。 这篇文章主要是通过一些示例来了解一下Container 的基本用法: 一、基本概念\\n\\nContainer 是一个组合了多个基础功能的容器组件,内部封装了以下特性:\\n\\n尺寸控制(width, height)\\n边距(margin)\\n内边距(padding)\\n背景与装饰(color, decoration)\\n对齐方式(alignment)\\n子组件(child)\\n变换(t…","guid":"https://juejin.cn/post/7502686634015670298","author":"搬砖的理查德","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-12T00:28:32.809Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/15091ac3928f4922939e1910b3ea02bd~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pCs56CW55qE55CG5p-l5b63:q75.awebp?rk3s=f64ab15b&x-expires=1747621316&x-signature=5h7OUzFrtd9aGXMQWq0kGkGRWtU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2d3c3a11ffe0437fb0e22d12ceb4d886~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pCs56CW55qE55CG5p-l5b63:q75.awebp?rk3s=f64ab15b&x-expires=1747621316&x-signature=yEoZTeBlWUk5DGjW%2FnWmMZJ8T9c%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/641e885be9f740ed935fe57e1377e8f0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pCs56CW55qE55CG5p-l5b63:q75.awebp?rk3s=f64ab15b&x-expires=1747621316&x-signature=WgqN7ueNTBQmWbazHEn3V72MffI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5ace32c8e2f64ce2840c064bac1d5b96~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pCs56CW55qE55CG5p-l5b63:q75.awebp?rk3s=f64ab15b&x-expires=1747621316&x-signature=XIim8qjRkWEy5%2FilND5nx2Ml3Qo%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"在纯 Win/Linux 环境直接构建打包 iOS ,xtool 了解一下","url":"https://juejin.cn/post/7502656022038872099","content":"之前聊 dart 开始支持交叉编译,可以在 win/macOS 构建 linux AOT 可执行文件时,就有人在说:「难道你还能在 win 上打包 iOS 么」,关于这个问题还真的可以,这就是今天要聊的:xtool
。
xtool
项目创建于 2024 年底,还是一个非常非常年轻的项目,起初是 2024 年作者 kabiroberai 在论坛分享了他的 Swift SDK for Darwin 项目,展示了如何在 Linux 上构建 iOS Swift 包,而这两天,它开源了成为了跨平台的 Xcode 替换实现,允许用户在 Linux、Windows、macOS 上使用 SwiftPM 构建和部署 iOS 应用。
简单来说,就是通过 xtool
,你可以从 Linux 和 Windows (WSL) 构建和部署 iOS 应用 :
甚至,xtool
还支持部署到物理设备,也就是它提供了对 iOS 应用进行代码签名的功能,那它是怎么做到这一点的?关键还是离不开 Xcode。
虽然在 Win 和 Linux 平台 Xcode 是无法安装工作的,但是 Xcode 里的 SDK 组件,包括编译器、链接器所需的头文件、库文件以及其他工具链资源等,这些其实都可以被提取出来使用。
\\n所以,xtool
实现跨平台 iOS 应用打包的核心在于其对苹果开发工具链的“重组”与“适配” ,xtool
会要求用户提供一个官方的 Xcode.xip
文件 ,也就是苹果分发 Xcode 的压缩包,然后 xtool
会解压这个文件。
\\n\\n解压 Xcode 后,
\\nxtool
会提取必要的组件,为当前非 macOS 系统生成并安装一个可用的 iOS Swift SDK ,这个本地构建的 SDK 包含了编译 iOS 应用所需的头文件、库和其他工具,通过运行swift sdk list
可以查看到新生成的 \\"darwin\\" SDK 。
另外 xtool
的整个构建体系围绕 SwiftPM 构建,xtool
会利用 SwiftPM 来处理项目的依赖管理、编译 Swift 代码等核心构建任务 ,从而支撑构建 iOS :
xtool
会通过 SwiftPM 编译项目的 Swift 源代码,生成针对 iOS 平台的目标文件xtool
支持通过 SwiftPM Resources 的方式包含图片等非代码资源文件,这些资源会特殊放置到应用包内的 .bundle
目录,对于需要放置在根目录的特定文件,可以通过 xtool.yml
配置文件中的 resources
指定xtool
会自动生成一个基础的 Info.plist
文件,开发者可以通过在项目中创建一个仅包含需要添加或更新的 Info.plist
文件,并在 xtool.yml
中通过 infoPath
指定该文件路径,来实现对 Info.plist
的定制xtool.yml
中通过 iconPath
指定应用图标文件xtool
会将编译好的可执行文件、处理过的资源、Info.plist
以及应用图标等打包成一个 .ipa
文件之后就是签名,xtool
提供了 auth
命令来管理 Apple Developer Services 的认证,支持 API Key和密码两种登录模式 ,xtool
通过内置的 XKit
库和 Apple Developer Services 进行交互,用户需要先通过 xtool auth
命令进行认证,认证成功后,xtool
能够代表开发者获取必要的签名证书和描述文件,并对编译好的 .app
包进行签名
\\n\\n另外,还会使用
\\nusbmuxd
工具辅助xtool
与连接到 Linux/WSL 系统的 iOS 设备进行通信,从而完成安装操作 。
另外,针对 Bundle ID 前缀,xtool
在为真实设备签名和安装应用时,会给 bundleID
添加一个前缀(例如,com.example.Hello
可能会变为 SC-1234.com.example.Hello
),这是因为对于未注册 Apple Developer Program 的帐户,两个账户不能使用相同的 bundleID
,添加前缀可以避免 bundleID
冲突。
\\n\\n也就是目前短时间内只满足给未注册 Apple Developer Program 的免费账户场景。
\\n
那它是否有什么限制或者局限性?答案肯定是有的:
\\nxtool
构建 UI@Observable
)可以正常工作,但苹果专有的宏,特别是像 SwiftData 中的 @Model
等,目前不受支持,这些宏不仅仅是语法糖,它们涉及到复杂的编译器插件和代码生成步骤xtool
内部包含支持 iOS 17 之前版本的 LLDB 调试组件,而由于苹果在 iOS 17 之后对调用 debugserver
的方式进行了重大更改,所以暂时还没在内部支持,需要使用外部工具如 pymobiledevice3
来进行调试。xtool
可以构建应用并在个人设备上运行,但暂时还不支持将构建产物直接部署到 App Store Connectxtool
功能总结:
功能 | xtool 支持状态 | xtool 注意事项/局限性 |
---|---|---|
SwiftPM 项目构建 | ✅ 支持 | 核心功能 |
SwiftUI 开发 | ✅ 支持 | |
Interface Builder / Storyboards | ❌ 不支持 | 被认为工作量大,优先级低 |
Asset Catalog 管理 (.xcassets ) | ❌ 不支持 | 需要逆向工程,可使用原始文件替代但效率较低 |
代码签名 (开发证书) | ✅ 支持 | |
代码签名 (分发/App Store 证书) | 理论上可行 (通过 XKit ),但未直接支持部署 | App Store 部署功能未实现 |
设备安装 (开发) | ✅ 支持 | |
LLDB 调试 (iOS <17) | ✅ 部分支持 (内置组件) | |
LLDB 调试 (iOS 17+) | ⚠️ 有限支持 (需结合外部工具如 pymobiledevice3 ) | Apple 更改了 debugserver 调用方式,集成难度大 |
App Store 部署 | ❌ 不支持 | 计划中,但尚未实现 |
标准 Swift 宏 (如 @Observable ) | ✅ 支持 | |
专有 Apple 宏 (如 SwiftData @Model ) | ❌ 不支持 | 需要逆向工程或 Apple 提供宿主无关版本 |
App Extension 支持 | ❌ 不支持 | 仅支持 \\"Application\\" 类型 Target,计划支持 |
跨平台构建 (Linux/Windows) | ✅ 支持 | xtool 的核心目标之一 |
当然,最大的风险可能是苹果开发者计划许可协议 (ADPLA) ,关于这点我也不是很确定,因为我也不确定这是否和 ADPLA) 的条款会有冲突,例如:
\\n所以从这一点看,xtool
看起来处境会类似「黑苹果」一样 ?毕竟 xtool
实现的核心能力还是离不开 Xcode 。但是,它确实给在 win/linux 上构建部署 iOS 提供了一个非常不错的思路和落地支持。
所以目前 xtool 暂时还不是一个生产力工具,而且中途夭折的可能性挺高,但是它的开源,也给大家提供了一个新的思路和基础支持,你觉得未来你可能会用上这个黑科技么?
\\nAudioPlayer
类在网上搜到的都是创建两个AudioPlayer类就可以实现同时播放两段音频,但实际上先加载的音频会被后加载的暂停
\\nfinal AudioPlayer player = AudioPlayer();\\nfinal AudioPlayer bgmPlayer = AudioPlayer();\\n
\\nsetAudioContext
android
: AUDIOFOCUS
属性设置不自动获取焦点,则不会暂停另一段音频。\\nAUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
是一种音频焦点请求类型。音频焦点用于管理设备上不同应用之间的音频播放优先级和行为。\\nAUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
除了具有临时获取焦点的特性外,还允许在获取焦点期间,其他应用可以继续播放音频,但音量会降低(“ducked”)。
实测降低音量可以忽略不计,建议使用setVolume
降低音频的音量。
ios
:mixWithOthers
允许和其他音频同时播放,主音频设置duckOthers
让另一段音频降低音量播放。
//主音频 \\nplayer.setAudioContext(\\n AudioContext(\\n android: AudioContextAndroid(\\n audioFocus: AndroidAudioFocus.none,\\n contentType: AndroidContentType.music,\\n usageType: AndroidUsageType.media,\\n ),\\n iOS: AudioContextIOS(\\n category: AVAudioSessionCategory.playback,\\n options: {\\n AVAudioSessionOptions.mixWithOthers,\\n AVAudioSessionOptions.duckOthers,\\n },\\n ),\\n ),\\n);\\n\\n// 背景音乐(不请求焦点)\\nbgmPlayer.setAudioContext(\\n AudioContext(\\n android: AudioContextAndroid(\\n audioFocus: AndroidAudioFocus.gainTransientMayDuck,\\n contentType: AndroidContentType.music,\\n usageType: AndroidUsageType.media,\\n ),\\n iOS: AudioContextIOS(\\n category: AVAudioSessionCategory.playback,\\n options: {\\n AVAudioSessionOptions.mixWithOthers,\\n },\\n ),\\n ),\\n);\\n
\\n要实现在主音频播放时,背景音乐不断循环,可以通过监听主音频的完成事件,在主音频完成时停止背景音乐,同时监听背景音乐的完成事件,在背景音乐完成时重新播放,实现循环。也可以通过bgmPlayer.setReleaseMode(ReleaseMode.loop);
实现循环播放
player.onPlayerComplete.listen((_) {\\n // 长音频播放完成,停止短音频\\n bgmPlayer.stop();\\n});\\n// 监听短音频播放完成事件\\nbgmPlayer.onPlayerComplete.listen((_) {\\n // 短音频播放完成,重新播放短音频以实现循环\\n bgmPlayer.play(UrlSource(widget.bgmUrl));\\n});\\n
","description":"1.创建两个AudioPlayer类 在网上搜到的都是创建两个AudioPlayer类就可以实现同时播放两段音频,但实际上先加载的音频会被后加载的暂停\\n\\nfinal AudioPlayer player = AudioPlayer();\\nfinal AudioPlayer bgmPlayer = AudioPlayer();\\n\\n2.使用setAudioContext\\n\\nandroid: AUDIOFOCUS属性设置不自动获取焦点,则不会暂停另一段音频。 AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 是一种音频焦点请求类型…","guid":"https://juejin.cn/post/7502686634014867482","author":"用户03594475246","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-11T12:38:38.091Z","media":null,"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter - UIKit开发相关指南 - 导航","url":"https://juejin.cn/post/7502352683376607270","content":"UIKit中,使用UINavigationController
来管理视图。Flutter中通过Navigator
和Routes
来实现相似的功能。
一个Route
是一个应用中屏幕或页的抽象,Navigator
是一个Widget来管理这些Routes
。Route
可以粗略的认为是一个UIViewController
,Navigator
类似iOS中的UINavigationController
,可以push
和pop
Routes。
切页面有两种方法
\\nRoute
import \'package:flutter/material.dart\';\\nimport \'package:navigation/second_route.dart\';\\n\\nvoid main() {\\n runApp(const MainApp());\\n}\\n\\nclass MainApp extends StatelessWidget {\\n const MainApp({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n title: \\"导航示例\\",\\n home: Builder(\\n builder:\\n (context) => Scaffold(\\n appBar: AppBar(title: Text(\\"导航\\")),\\n body: Center(\\n child: ElevatedButton(\\n onPressed: () {\\n // 2. 跳转到SecondRoute\\n Navigator.of(context).pushNamed(\'/second\');\\n },\\n child: const Text(\\"到第二个页面\\"),\\n ),\\n ),\\n ),\\n ),\\n // 1.建立映射表 \\"/second\\"指向SecondRoute()\\n routes: {\'/second\': (context) => const SecondRoute()},\\n );\\n }\\n}\\n
\\nimport \\"package:flutter/cupertino.dart\\";\\nimport \\"package:flutter/material.dart\\";\\n\\nclass SecondRoute extends StatelessWidget {\\n const SecondRoute({super.key});\\n\\n @override\\n // 3. 显示\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: const Text(\\"第二个路由页面\\")),\\n body: Center(\\n child: CupertinoButton(\\n onPressed: () {\\n // 4.点击返回\\n Navigator.pop(context);\\n },\\n child: const Text(\\"返回\\"),\\n ),\\n ),\\n );\\n }\\n}\\n
\\nFlutter中使用如下代码实现pop
功能
# 或 Navigator.pop()\\nSystemNavigator.pop()\\n
\\n在iOS上约等于如下的实现
\\nUIViewController* viewController = [UIApplication sharedApplication].keyWindow.rootViewController;\\nif ([viewController isKindOfClass:[UINavigationController class]]) {\\n [((UINavigationController*)viewController) popViewControllerAnimated:NO];\\n}\\n
\\n在push方法中直接用builder跳转,而不需要通过路由表
\\nimport \'package:flutter/material.dart\';\\nimport \'package:navigation/second_route.dart\';\\n\\nvoid main() {\\n runApp(const MainApp());\\n}\\n\\nclass MainApp extends StatelessWidget {\\n const MainApp({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n title: \\"导航示例\\",\\n home: Builder(\\n builder:\\n (context) => Scaffold(\\n appBar: AppBar(title: Text(\\"导航\\")),\\n body: Center(\\n child: ElevatedButton(\\n onPressed: () {\\n // 点击直接指定跳转的界面\\n Navigator.of(context).push(\\n MaterialPageRoute(\\n builder: (context) => const SecondRoute(),\\n ),\\n );\\n },\\n child: const Text(\\"到另一个页面\\"),\\n ),\\n ),\\n ),\\n ),\\n );\\n }\\n}\\n
\\ngo_router是一个用于Flutter声明式的路由包,使用Router API为不同屏幕间进行切换
\\n$ flutter pub add go_router\\n
\\n会在pubspec.yaml
文件中添加go_router依赖包,并下载
import \'package:flutter/material.dart\';\\nimport \'package:go_router/go_router.dart\';\\nimport \'package:navigation/second_route.dart\';\\n\\nvoid main() {\\n runApp(MaterialApp.router(title: \'导航\', routerConfig: _router));\\n}\\n\\n// 1.定义路由信息\\nfinal _router = GoRouter(\\n routes: [\\n GoRoute(path: \'/\', builder: (context, state) => const FirstScreen()),\\n GoRoute(path: \'/second\', builder: (context, state) => const SecondRoute()),\\n ],\\n);\\n\\nclass FirstScreen extends StatelessWidget {\\n const FirstScreen({super.key});\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: const Text(\'第一个界面\')),\\n body: Center(\\n child: ElevatedButton(\\n child: const Text(\'使用go_router跳转\'),\\n // 2.指定跳转界面\\n onPressed: () => context.go(\'/second\'),\\n ),\\n ),\\n );\\n }\\n}\\n
\\nimport \\"package:flutter/cupertino.dart\\";\\nimport \\"package:flutter/material.dart\\";\\nimport \\"package:go_router/go_router.dart\\";\\n\\nclass SecondRoute extends StatelessWidget {\\n const SecondRoute({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: const Text(\\"第二个路由页面\\")),\\n body: Center(\\n child: CupertinoButton(\\n onPressed: () {\\n // 3. 点击返回根目录\\n context.go(\'/\');\\n },\\n child: const Text(\\"返回\\"),\\n ),\\n ),\\n );\\n }\\n}\\n
\\n在UIKit中使用URL scheme来实现。Flutter可以使用类似 url_launcher
的插件来实现。
Android | iOS | Linux | macOS | Web | Windows | |
---|---|---|---|---|---|---|
支持 | SDK 16+ | 12.0+ | Any | 10.14+ | Any | Window 10+ |
$ flutter pub add url_launcher\\n
\\nimport \'package:flutter/material.dart\';\\nimport \'package:url_launcher/url_launcher.dart\';\\n\\nfinal Uri _url = Uri.parse(\'https://flutter.cn\');\\n\\nvoid main() => runApp(\\n const MaterialApp(\\n home: Material(\\n child: Center(\\n child: ElevatedButton(\\n onPressed: _launchUrl,\\n child: Text(\'Show Flutter homepage\'),\\n ),\\n ),\\n ),\\n ),\\n );\\n\\nFuture<void> _launchUrl() async {\\n if (!await launchUrl(_url)) {\\n throw Exception(\'Could not launch $_url\');\\n }\\n}\\n
\\n在Info.plist
添加允许打开短信和电话的配置
<key>LSApplicationQueriesSchemes</key>\\n<array>\\n <string>sms</string>\\n <string>tel</string>\\n</array>\\n
\\nimport \'package:flutter/material.dart\';\\nimport \'package:url_launcher/url_launcher.dart\';\\n\\nfinal Uri _url = Uri.parse(\'tel://10000\');\\n\\nvoid main() => runApp(\\n const MaterialApp(\\n home: Material(\\n child: Center(\\n child: ElevatedButton(\\n // 1. 绑定对应方法\\n onPressed: _launchUrl,\\n child: Text(\'Show Flutter homepage\'),\\n ),\\n ),\\n ),\\n ),\\n);\\n\\nFuture<void> _launchUrl() async {\\n // 2. 确认是否能打开对应url,已添加到plist文件\\n if (await canLaunchUrl(_url)) {\\n if (!await launchUrl(_url)) {\\n throw Exception(\'Could not launch $_url\');\\n }\\n }\\n}\\n\\n
\\n\\n\\nFlutter 3.29\\nmacOS Sequoia 15.4.1\\nXcode 16.3
\\n
在UIKit使用UIView类的对象进行页面开发,布局也是UIView类的对象,在Flutter中使用的是Widget,在概念上Widget可以理解成UIView。
\\n差异:
\\nFlutter 包含 Matterial 组件库,其中的Widgets都符合了Material设计指引。Material设计是个适配多平台的设计系统,也支持iOS
\\n但如果想用iOS的UI风格,可以使用Cupertino widgets libray
\\n使用UIKit开发时可以直接改变对应的视图。就像上面提到的,因为Flutter的Widgets是不可变的,可以通过更新Widget的状态来更新Widget。
\\n这就是有状态 widget 与无状态 widget 的概念。StatelessWidget是没有附加状态的 widget。
\\n当需要实现HTTP请求获取数据后根据数据动态改变的UI,可以使用StatefulWidget。
\\n无状态和有状态 widget 之间的重要区别在于,StatefulWidgets 有一个 State 对象,用于存储状态数据并在树重建中传递数据,因此它不会丢失。
\\nText Widget是常见的无状态的Widget
\\nclass Text extends StatelessWidget {\\n const Text(\\n String this.data, {\\n ...\\n
\\nText(\\n \'I like Flutter!\',\\n style: TextStyle(fontWeight: FontWeight.bold),\\n);\\n
\\n初始化时并没有传递状态,要实现动态的修改Text Widget的内容,可以通过包装到一个StatefulWidget中
\\nimport \'package:flutter/material.dart\';\\n\\nvoid main() {\\n // 1. 创建MainApp Widget\\n runApp(MainApp());\\n}\\n\\nclass MainApp extends StatelessWidget {\\n const MainApp({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n // 2.调用\\n return MaterialApp(title: \'Sample App\', home: CustomStatefulPage());\\n\\n }\\n}\\n\\nclass _CustomStatefulePageState extends State<CustomStatefulPage> {\\n String text = \'I Like Flutter\';\\n // 5.点击按钮触发_updateText\\n void _updateText() {\\n // 6.通知Flutter更新\\n setState(() {\\n text = \\"Flutter is Awesome!\\";\\n });\\n }\\n @override\\n // 7.调用build方法,刷新页面\\n // 4.调用build方法,按钮点击方法绑定_updateText方法\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: const Text(\'Sample App\')),\\n body: Center(child: Text(text)),\\n floatingActionButton: FloatingActionButton(\\n onPressed: _updateText,\\n tooltip: \'Update Text\',\\n child: const Icon(Icons.update),\\n ),\\n );\\n }\\n}\\n\\nclass CustomStatefulPage extends StatefulWidget {\\n const CustomStatefulPage({super.key});\\n @override\\n // 3.创建_CustomStatefulePageState对象\\n State<StatefulWidget> createState() => _CustomStatefulePageState();\\n}\\n
\\n在UIKit中可以使用Storyboard和用代码去更新View的约束。在Flutter中,通过组合Widget树来展示布局。
\\n@override\\nWidget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: const Text(\'Sample App\')),\\n body: Center(\\n child: CupertinoButton(\\n onPressed: () {},\\n // 指定内边距\\n padding: const EdgeInsets.only(left: 10, right: 10),\\n child: const Text(\'Hello\'),\\n ),\\n ),\\n );\\n}\\n
\\n在UIKit中使用addSubview()
或removeFromSuperview()
来动态的添加或移除视图。在Flutter中Widget是不可变,没有那种类似addSubview()
的方法。可以在父Widget传一个函数,然后控制显示的效果
import \'package:flutter/material.dart\';\\nimport \'package:flutter/cupertino.dart\';\\n\\nvoid main() {\\n // 1. 创建MainApp Widget\\n runApp(MainApp());\\n}\\n\\nclass MainApp extends StatelessWidget {\\n // This widget is the root of your application.\\n const MainApp({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n // 2. 创建SampleAppPage\\n return const MaterialApp(title: \'Sample App\', home: SampleAppPage());\\n }\\n}\\n\\nclass SampleAppPage extends StatefulWidget {\\n const SampleAppPage({super.key});\\n\\n @override\\n // 3. 调用_SampleAppPageState方法创建State<SampleAppPage>\\n State<SampleAppPage> createState() => _SampleAppPageState();\\n}\\n\\nclass _SampleAppPageState extends State<SampleAppPage> {\\n // Default value for toggle.\\n bool toggle = true;\\n \\n // 5.点击按钮触发\\n void _toggle() {\\n // 6.通知Flutter更新toggle\\n setState(() {\\n toggle = !toggle;\\n });\\n }\\n\\n Widget _getToggleChild() {\\n // 8.根据toggle的值显示不同页面效果\\n if (toggle) {\\n return const Text(\'Toggle One\');\\n }\\n\\n return CupertinoButton(onPressed: () {}, child: const Text(\'Toggle Two\'));\\n }\\n\\n @override\\n // 4. 触发build方法,FloatingActionButton点击绑定_toggle\\n // 7. 重新执行_getToggleChild\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: const Text(\'Sample App\')),\\n body: Center(child: _getToggleChild()),\\n floatingActionButton: FloatingActionButton(\\n onPressed: _toggle,\\n tooltip: \'Update Text\',\\n child: const Icon(Icons.update),\\n ),\\n );\\n }\\n}\\n
\\n在UIKit中,通过UIView的animate(withDuration:animations:)
执行动画。在Flutter中,使用动画库将Widget包装在动画Widget内。
Flutter中使用AnimationController
来控制动画的暂停,进度,停止。在屏幕刷新的每一帧,就会生成一个新的值。默认情况下,AnimationController在给定的时间段内会线性的生成从0.0到1.0的数字。
import \'package:flutter/material.dart\';\\n\\nvoid main() {\\n // 1. 创建MainApp Widget\\n runApp(MainApp());\\n}\\n\\nclass MainApp extends StatelessWidget {\\n const MainApp({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n return const MaterialApp(\\n title: \'Fade Demo\',\\n // 2. 创建MyFadeTest\\n home: MyFadeTest(title: \'Fade Demo\'),\\n );\\n }\\n}\\n\\nclass MyFadeTest extends StatefulWidget {\\n const MyFadeTest({super.key, required this.title});\\n\\n final String title;\\n\\n @override\\n // The framework can call this method multiple times over the lifetime of a [StatefulWidget].\\n // For example, if the widget is inserted into the tree in multiple locations,\\n // the framework will create a separate [State] object for each location.\\n // 添加到tree中的每个MyFadeTest Widget都会创建对应的State<MyFadeTest>对象\\n // 3.创建_MyFadeTest\\n State<MyFadeTest> createState() => _MyFadeTest();\\n}\\n\\nclass _MyFadeTest extends State<MyFadeTest>\\n // 单个 AnimationController 的时候使用 SingleTickerProviderStateMixin\\n with SingleTickerProviderStateMixin {\\n late AnimationController controller;\\n late CurvedAnimation curve;\\n\\n @override\\n // Called when this object is inserted into the tree.\\n // 添加到tree中时调用\\n // 4. 调用initState,指定AnimationController\\n void initState() {\\n super.initState();\\n controller = AnimationController(\\n duration: const Duration(milliseconds: 2000),\\n // TickerProvider(抽象类),用于接收动画变化过程中的通知,类似于接口回调\\n vsync: this,\\n );\\n // 指定生成0.0到1.0的规则\\n curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);\\n }\\n\\n @override\\n // Called when this object is removed from the tree permanently.\\n void dispose() {\\n // 8.当对象对tree中移除时,回收AnimationController的资源\\n controller.dispose();\\n super.dispose();\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(widget.title)),\\n body: Center(\\n child: FadeTransition(\\n // 5. 应用透明效果,opacity的变化规则指定为上面创建的curve\\n opacity: curve,\\n // 6. 动画包装的Widget\\n child: const FlutterLogo(size: 100),\\n ),\\n ),\\n floatingActionButton: FloatingActionButton(\\n onPressed: () {\\n // Starts running this animation forwards (towards the end).\\n // 7. 正向播放动画\\n controller.forward();\\n },\\n tooltip: \'Fade\',\\n child: const Icon(Icons.brush),\\n ),\\n );\\n }\\n}\\n
\\n在UIKit中使用CoreGraphics
在手机屏幕上绘制线条和凸显。Flutter使用Canvas
,CustomPaint
,CustomPainter
来实现绘制操作。
import \'package:flutter/material.dart\';\\n\\nvoid main() => runApp(const MaterialApp(home: DemoApp()));\\n\\nclass DemoApp extends StatelessWidget {\\n const DemoApp({super.key});\\n\\n @override\\n Widget build(BuildContext context) => const Scaffold(body: Signature());\\n}\\n\\nclass Signature extends StatefulWidget {\\n const Signature({super.key});\\n\\n @override\\n State<Signature> createState() => SignatureState();\\n}\\n\\nclass SignatureState extends State<Signature> {\\n List<Offset?> _points = <Offset?>[];\\n @override\\n Widget build(BuildContext context) {\\n return GestureDetector(\\n // 绘制时移动的回调\\n onPanUpdate: (details) {\\n setState(() {\\n // 当前Widget关联的RenderObject\\n RenderBox? referenceBox = context.findRenderObject() as RenderBox;\\n // 坐标转换: 全局坐标转换为局部坐标\\n // globalPosition表示当前手势触点在全局坐标系位置与对应组件顶点坐标的偏移量\\n // localPosition则就表示当前手势触点在对应组件坐标系位置与对应组件顶点坐标的偏移量\\n Offset localPosition = referenceBox.globalToLocal(\\n details.globalPosition,\\n );\\n _points = List.from(_points)..add(localPosition);\\n });\\n },\\n // 绘制结束时调用\\n onPanEnd: (details) => _points.add(null),\\n child: CustomPaint(\\n // 具体的绘制操作调用Canvas\\n painter: SignaturePainter(_points),\\n // 不限制长度\\n size: Size.infinite,\\n ),\\n );\\n }\\n}\\n\\nclass SignaturePainter extends CustomPainter {\\n SignaturePainter(this.points);\\n\\n final List<Offset?> points;\\n\\n @override\\n void paint(Canvas canvas, Size size) {\\n final Paint paint =\\n //「..」意思是 「级联 操作符」,为了方便配置而使用。\\n //「..」和「.」不同的是 调用「..」后返回的相当于是 this\\n Paint()\\n ..color = const Color.fromARGB(255, 29, 25, 25)\\n ..strokeCap = StrokeCap.round\\n ..strokeWidth = 5;\\n for (int i = 0; i < points.length - 1; i++) {\\n // 起点与终点都不为null,说明是一个线段,然后执行绘制\\n if (points[i] != null && points[i + 1] != null) {\\n canvas.drawLine(points[i]!, points[i + 1]!, paint);\\n }\\n }\\n }\\n\\n @override\\n bool shouldRepaint(SignaturePainter oldDelegate) =>\\n oldDelegate.points != points;\\n}\\n
\\nUIKit中使用.opactiy
或者.alpha
实现。Flutter中使用Opacity
组件来实现
import \'package:flutter/material.dart\';\\n\\nvoid main() {\\n runApp(MainApp());\\n}\\n\\nclass MainApp extends StatelessWidget {\\n const MainApp({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n title: \'Flutter layout demo\',\\n home: CustomStatefulPage(),\\n );\\n }\\n}\\n\\nclass CustomStatefulPage extends StatefulWidget {\\n const CustomStatefulPage({super.key});\\n @override\\n State<StatefulWidget> createState() => _CustomStatefulePageState();\\n}\\n\\nclass _CustomStatefulePageState extends State<CustomStatefulPage> {\\n double opacity = 0.3;\\n String text = \\"透明度\\";\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(text)),\\n body: Center(\\n child: Opacity(opacity: opacity, child: Text(\\"透明度:$opacity\\")),\\n ),\\n floatingActionButton: FloatingActionButton(\\n onPressed: () {\\n setState(() {\\n opacity = 1.0;\\n });\\n },\\n tooltip: \'Update Text\',\\n child: const Icon(Icons.update),\\n ),\\n );\\n }\\n}\\n
\\n在UIKit中通过继承UIView来自定义组件,而在Flutter中定义自定义组件通常使用组合的方式
\\nimport \\"package:flutter/material.dart\\";\\n\\nvoid main() {\\n runApp(MaterialApp(home: CustomWidget()));\\n}\\n\\nclass CustomWidget extends StatelessWidget {\\n const CustomWidget({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\\"Custom Widget\\")),\\n body: Center(child: CustomButton(\\"自定义按钮\\")),\\n );\\n }\\n}\\n\\n/// 自定义按钮\\nclass CustomButton extends StatelessWidget {\\n const CustomButton(this.label, {super.key});\\n\\n final String label;\\n\\n @override\\n Widget build(BuildContext context) {\\n return ElevatedButton(onPressed: () {}, child: Text(label));\\n }\\n}\\n
\\niOS中使用CocoaPods管理时,可以通过配置Podfile文件。Flutter 是用pubspec.yaml 来管理依赖的库。iOS端需要的库写在Podfile里
\\n/// 包名 此属性表示包名(package name),此属性是非常重要的,引入其他文件时需要使用此包名:\\n/// import \'package:uikit/home_page.dart\';\\nname: uikit\\n/// description 属性是一个可选配置属性,是对当前项目的介绍\\ndescription: \\"A new Flutter project.\\"\\npublish_to: \'none\'\\n// 此属性应用程序的版本和内部版本号,格式为 x.x.x+x,例如:1.0.0+1,这个版本号称为 语义版本号(semantic versioning )\\n// 版本号 + 前面到部分,叫做 version number,由 2 个小点隔开,后面的部分叫做 build number。\\n// 在 Android 中 version number 对应 versionName,build number 对应 versionCode,在 android/build.gradle 下有相关配置,\\nversion: 0.1.0\\n\\n/// Environment 属性下添加 Flutter 和 Dart 版本控制。\\nenvironment:\\n sdk: ^3.7.2\\n\\n/// dependencies 和 dev_dependencies 下包含应用程序所依赖的包,dependencies 和 dev_dependencies 就像其名字一样,dependencies 下的所有依赖会编译到项目中,而 dev_dependencies 仅仅是运行期间的包,比如自动生成代码的库\\n\\n/// 可以通过四种方式依赖包\\ndependencies:\\n /// 1. 依赖pub.dev\\n flutter:\\n sdk: flutter\\n\\n /// 2. 依赖本地库\\n flutter_local_pacakge:\\n path: /path/to/flutter_local_package\\n \\n /// 3. 依赖git repository\\n /// url:github 地址\\n /// ref:表示git引用,可以是 commit hash, tag 或者 branch\\n /// path:如果 git 仓库中有多个软件包,则可以使用此属性指定软件包\\n bloc:\\n git:\\n url:https://wwww.xxx.git\\n ref: bloc_fixes_issue_100\\n path: package/bloc\\n \\n /// 4. 依赖私有仓\\n dependencies:\\n bloc: \\n hosted:\\n name: bloc\\n url: http://your-package-server.com\\n version: ^6.0.0\\n\\ndev_dependencies:\\n flutter_test:\\n sdk: flutter\\n flutter_lints: ^5.0.0\\n\\n/// Flutter 下面的配置都是 Flutter 的相关配置。\\nflutter:\\n /// 应用程序中包含Material Icons字体\\n uses-material-design: true\\n /// 是对当前资源的配置,比如图片,字体等\\n //// 本地图片\\n assets:\\n - images/a_dot_burr.jpeg\\n - images/a_dot_ham.jpeg\\n flutter:\\n\\n /// 插件\\n plugin:\\n platforms:\\n android:\\n package: com.flutter.app_market\\n pluginClass: AppMarketPlugin\\n ios:\\n pluginClass: AppMarketPlugin\\n\\n
\\n给 UIKit 开发者的 Flutter 指南\\n【Flutter 实战】pubspec.yaml 配置文件详解\\nNo Directionality widget found错误背后的原理\\nFlutter - Dart中(.)、(..)、(...)语法使用
","description":"环境 Flutter 3.29 macOS Sequoia 15.4.1 Xcode 16.3\\n\\n概览\\nUIView与Widgets的比较\\n\\n在UIKit使用UIView类的对象进行页面开发,布局也是UIView类的对象,在Flutter中使用的是Widget,在概念上Widget可以理解成UIView。\\n\\n差异:\\n\\n有效期: Widgets是不可变的,它的生存期只到被改变前。当Widgets或它们的状态改变了。Flutter\'s 框架会创建一个widget的实例,而UIKit中的UIView是不会重新创建,它是可变的…","guid":"https://juejin.cn/post/7502368089733775370","author":"忘川三","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-10T15:15:36.748Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7133e44e40654f41bf9812da06d73fba~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5b-Y5bed5LiJ:q75.awebp?rk3s=f64ab15b&x-expires=1747494936&x-signature=1pVPR5NhB4iWGiI41gvyx1O6x20%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/baf731051a5a4ae684043df1159579e9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5b-Y5bed5LiJ:q75.awebp?rk3s=f64ab15b&x-expires=1747494936&x-signature=%2FOkD97pXPMYwhHbbmqjaLMkUhQQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c6953253d0854c60a8f80d67057d9f89~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5b-Y5bed5LiJ:q75.awebp?rk3s=f64ab15b&x-expires=1747494936&x-signature=9fqlsW6vGpvq9KqpXlfMj2wNdO4%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["代码人生","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 三层渲染结构详解:Widget、Element 和 RenderObject","url":"https://juejin.cn/post/7502352683375624230","content":"在 Flutter 的框架中,UI 渲染过程是由三层不同的结构协同完成的:Widget Tree、Element Tree 和 RenderObject Tree。这些结构的相互配合和精密工作,使得 Flutter 能够实现高效的 UI 更新和渲染。
\\n本文将深入剖析 Flutter 三层渲染架构的各个方面,帮助你全面理解这三棵树的作用、如何工作以及它们之间的关系。
\\nFlutter 的 UI 渲染结构由三棵树组成:
\\n这三棵树各自有独立的职责,但又紧密合作,共同完成 UI 的显示、更新和渲染。
\\nWidget
是 Flutter 中最基本的构建块,它是不可变的,用来描述 UI 组件的结构。例如,按钮、文本框、图片等元素都是通过 Widget
来表示的。
特点:
\\nWidget build(BuildContext context) {\\n return Column(\\n children: [\\n Text(\\"Hello Flutter\\"),\\n ElevatedButton(onPressed: () {}, child: Text(\\"Click\\")),\\n ],\\n );\\n}\\n
\\n在上面的代码中,每次调用 build()
时,Flutter 都会根据当前的状态重新生成 Widget 树。
Element
是 Widget 与 RenderObject 之间的桥梁。它与 Widget 和 RenderObject 之间的关联非常重要,负责管理 Widget 的生命周期并与底层的渲染对象进行交互。
Element 的职责:
\\nclass MyCustomWidget extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return Container(\\n color: Colors.blue,\\n child: Text(\\"Hello World\\"),\\n );\\n }\\n}\\n
\\n在 StatelessWidget
中,当构建 Widget 时,Flutter 会创建一个对应的 StatelessElement
。Element
通过 createRenderObject()
方法创建与之对应的 RenderObject(如 RenderBox
)。
RenderObject
负责 UI 元素的布局、绘制和事件处理。它是 Flutter 渲染系统的核心,处理具体的绘制任务,并与其他渲染对象协作来构建完整的 UI 界面。
RenderObject 的职责:
\\nclass MyRenderBox extends RenderBox {\\n @override\\n void performLayout() {\\n size = Size(100, 50); // 设置大小\\n }\\n\\n @override\\n void paint(PaintingContext context, Offset offset) {\\n final Paint paint = Paint()..color = Colors.blue;\\n context.canvas.drawRect(offset & size, paint); // 绘制矩形\\n }\\n}\\n
\\n在这段代码中,我们自定义了一个 RenderBox
,通过 performLayout()
方法指定大小,并在 paint()
方法中绘制内容。
这三棵树之间有着紧密的关系,它们各自扮演不同的角色。可以把它们想象成一个生产线的不同阶段:
\\n它们的关系可以用以下流程图来表示:
\\nWidget Tree (描述 UI) \\n ↓ createElement()\\nElement Tree (管理生命周期) \\n ↓ createRenderObject()\\nRenderObject Tree (执行渲染)\\n
\\n示例:
\\nText(\\"Hello\\")\\n
\\n在上面的代码中:
\\nText
是 Widget;StatelessElement
;createRenderObject()
创建 RenderParagraph
,并负责文本的实际绘制。Flutter 的 UI 更新是基于 setState()
和 Widget Diff 算法的。当 UI 需要更新时,Flutter 会对比新的 Widget 与旧的 Widget:
runtimeType
)以及 key
相同,Flutter 会复用相应的 Element 和 RenderObject,只更新其中的必要部分;这个机制使得 Flutter 能够在保证高效渲染的同时,避免不必要的重建和渲染。
\\n在实际开发过程中,理解三层渲染结构能帮助你优化性能和代码的质量。以下是一些开发优化:
\\nsetState()
,减少不必要的 UI 更新。GlobalKey
会导致 Flutter 强制更新相关的 Element,可能会影响性能。performLayout()
和 paint()
方法,避免复杂的计算和绘制。通过对 Flutter 三层渲染结构的深入剖析,相信你已经对 Widget Tree、Element Tree 和 RenderObject Tree 的作用、工作原理和关系有了更清晰的认识。了解这些底层机制,不仅可以帮助你更好地理解 Flutter 的渲染过程,还能帮助你在开发中做出更加高效和优化的设计。
","description":"在 Flutter 的框架中,UI 渲染过程是由三层不同的结构协同完成的:Widget Tree、Element Tree 和 RenderObject Tree。这些结构的相互配合和精密工作,使得 Flutter 能够实现高效的 UI 更新和渲染。 本文将深入剖析 Flutter 三层渲染架构的各个方面,帮助你全面理解这三棵树的作用、如何工作以及它们之间的关系。\\n\\n一、什么是三层渲染结构?\\n\\nFlutter 的 UI 渲染结构由三棵树组成:\\n\\nWidget Tree(部件树) :描述了 UI 的静态结构;\\nElement Tree(元素树) :连接…","guid":"https://juejin.cn/post/7502352683375624230","author":"MaoJiu","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-10T12:45:41.908Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/cbdb02d393344084bbb00572d0826016~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTWFvSml1:q75.awebp?rk3s=f64ab15b&x-expires=1747485941&x-signature=STy%2FUTbHPyDqPTFe8P%2FMe57Ma50%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Dart 是一门由 Google 开发的、为客户端优化的编程语言,它的目标是让你能在任何平台上构建快速、美观的用户界面。无论你是想开发移动应用 (Flutter)、Web 应用,还是后端服务,Dart 都能助你一臂之力。
\\n本教程是为那些已经具备一些编程基础的同学设计的。如果你了解变量、循环、函数等基本概念,那么学习 Dart 将会非常顺利。
\\n建议学习资源:
\\n在学习过程中,我强烈建议大家参考 Dart 官方的中文文档,特别是 \\"语言概览\\" 或 \\"Language Tour\\" 部分,那里有更详尽的解释和示例:
\\n好了,让我们开始吧!
\\n在 Dart 中,变量是存储数据的容器。声明变量时,你可以使用 var
关键字,Dart 会自动推断其类型。如果你希望明确指定类型,也可以直接使用类型名。
var
: 编译器会自动推断变量的类型。一旦类型被确定,就不能再改变为其他类型。final
: 用于声明一个只能被赋值一次的变量。一旦赋值后,其值不能再改变(运行时常量)。const
: 用于声明一个编译时常量。它的值在编译时就必须确定。const
变量本身也是 final
的。void main() {\\n // 使用 var 声明变量,Dart 会推断其类型\\n var name = \'Dart\'; // 推断为 String\\n var year = 2024; // 推断为 int\\n var pi = 3.14; // 推断为 double\\n\\n // 明确指定类型\\n String language = \'Dart\';\\n int version = 3;\\n\\n print(\'Hello, $name! Version: $version, Year: $year, PI: $pi, Language: $language\');\\n\\n // final 变量\\n final String courseName = \'Dart Basics\';\\n // courseName = \'Advanced Dart\'; // 错误!final 变量不能再次赋值\\n\\n // const 变量\\n const double gravity = 9.8;\\n // const currentTime = DateTime.now(); // 错误!DateTime.now() 不是编译时常量\\n // gravity = 9.81; // 错误!const 变量不能再次赋值\\n\\n print(\'Course: $courseName, Gravity: $gravity m/s^2\');\\n}\\n
\\n重点:
\\nfinal
和 const
都表示不可变,但 const
是编译时常量,其值在编译期间就确定了。如果一个 const
变量的值依赖于运行时计算,那么它不能被声明为 const
。var
推断),就不能再赋给不同类型的值。Dart 是一门强类型语言,但由于类型推断的存在,书写起来可以像动态语言一样简洁。以下是 Dart 中最常用的内置数据类型:
\\n数字 (Numbers):
\\nint
: 整数值,大小没有限制(取决于内存)。
double
: 双精度浮点数值,遵循 IEEE 754 标准。 int
和 double
都是 num
类型的子类。
int score = 100;\\ndouble percentage = 99.5;\\nnum anyNumber = 1; // 可以是 int\\nanyNumber = 1.5; // 也可以是 double\\n\\nprint(\'Score: $score, Percentage: $percentage%, Any Number: $anyNumber\');\\n
\\n字符串 (Strings):
\\nString
: 表示一系列 UTF-16 编码的字符。
可以使用单引号 \'...\'
或双引号 \\"...\\"
创建字符串。
使用三个单引号或三个双引号可以创建多行字符串。
\\n字符串插值:使用 ${expression}
将表达式的值嵌入字符串中。如果表达式只是一个标识符,可以省略花括号:$identifier
。
String greeting = \'Hello\';\\nString target = \\"World\\";\\nString message = \'$greeting, $target!\'; // 字符串插值\\nString multiLine = \\"\\"\\"\\n这是\\n一个多行\\n字符串。\\n\\"\\"\\";\\n\\nprint(message);\\nprint(multiLine);\\nprint(\'The sum of 5 and 3 is ${5 + 3}.\');\\n
\\n布尔值 (Booleans):
\\nbool
: 表示布尔值,只有两个可能的值:true
和 false
。
bool isLoading = true;\\nbool isFinished = false;\\n\\nif (isLoading) {\\n print(\'Still loading...\');\\n} else {\\n print(\'Finished!\');\\n}\\n
\\n列表 (Lists, 也称为 Arrays):
\\nList
: Dart 中的有序集合,类似于其他语言中的数组。
列表的索引从 0 开始。
\\nList<String> fruits = [\'Apple\', \'Banana\', \'Orange\'];\\nvar numbers = [1, 2, 3, 4, 5]; // 推断为 List<int>\\n\\nprint(\'First fruit: ${fruits[0]}\');\\nfruits.add(\'Mango\');\\nprint(\'All fruits: $fruits\');\\nprint(\'Number of fruits: ${fruits.length}\');\\n\\n// 创建一个固定长度的列表,所有元素默认为 null (如果类型可空)\\nvar fixedList = List<int?>.filled(3, null);\\nprint(\'Fixed list: $fixedList\');\\nfixedList[0] = 10;\\n// fixedList.add(4); // 错误!固定长度列表不能添加元素\\n\\n// 使用 List.generate 创建列表\\nvar generatedList = List<int>.generate(5, (index) => index * index);\\nprint(\'Generated list (squares): $generatedList\'); // [0, 1, 4, 9, 16]\\n
\\n哈希表 (Maps):
\\nMap
: 键值对的集合。键和值可以是任何类型的对象。每个键必须是唯一的。
也常被称为字典 (dictionary) 或哈希 (hash)。
\\nMap<String, String> capitals = {\\n \'USA\': \'Washington D.C.\',\\n \'China\': \'Beijing\',\\n \'Japan\': \'Tokyo\'\\n};\\nvar studentScores = {\\n \'Alice\': 90, // 推断为 Map<String, int>\\n \'Bob\': 85\\n};\\n\\nprint(\'Capital of USA: ${capitals[\'USA\']}\');\\nstudentScores[\'Charlie\'] = 95; // 添加新的键值对\\nprint(\'Student scores: $studentScores\');\\nprint(\'Is Alice in scores? ${studentScores.containsKey(\'Alice\')}\');\\n
\\n(其他类型如 Runes, Symbols):
\\nRunes
: 用于表示字符串中的 UTF-32 编码字符。通常在需要处理特殊字符(如表情符号)时使用。Symbols
: Symbol
对象表示 Dart 程序中声明的运算符或标识符。你可能不会经常直接使用它们,但它们在某些反射相关的 API 中非常重要。控制流语句用于根据条件执行不同的代码块或重复执行某段代码。
\\nif
和 else
:
int age = 18;\\nif (age >= 18) {\\n print(\'Adult\');\\n} else if (age >= 13) {\\n print(\'Teenager\');\\n} else {\\n print(\'Child\');\\n}\\n
\\n注意: Dart 中的条件必须是布尔值,不像某些语言那样可以将数字(如 0 或 1)视为布尔值。
\\nfor
循环:
标准 for
循环:
for (int i = 0; i < 5; i++) {\\n print(\'Number: $i\');\\n}\\n
\\nfor-in
循环:用于遍历可迭代对象(如 List
或 Set
)的元素。
List<String> colors = [\'Red\', \'Green\', \'Blue\'];\\nfor (String color in colors) {\\n print(\'Color: $color\');\\n}\\n\\nMap<String, int> scores = {\'Math\': 90, \'English\': 85};\\nfor (var key in scores.keys) {\\n print(\'$key: ${scores[key]}\');\\n}\\nfor (var value in scores.values) {\\n print(\'Score: $value\');\\n}\\nfor (var entry in scores.entries) {\\n print(\'${entry.key}: ${entry.value}\');\\n}\\n
\\n我们也可以使用 forEach
方法,它通常与匿名函数(后面会讲到)结合使用:
colors.forEach((color) {\\n print(\'Color from forEach: $color\');\\n});\\n
\\nwhile
和 do-while
循环:
while
循环:先判断条件,条件为真则执行循环体。
int count = 0;\\nwhile (count < 3) {\\n print(\'While count: $count\');\\n count++;\\n}\\n
\\ndo-while
循环:先执行一次循环体,然后再判断条件,条件为真则继续执行。
int num = 0;\\ndo {\\n print(\'Do-while num: $num\');\\n num++;\\n} while (num < 3);\\n
\\nbreak
和 continue
:
break
: 立即跳出整个循环。
continue
: 跳过当前迭代中 continue
之后的代码,直接开始下一次迭代。
for (int i = 0; i < 10; i++) {\\n if (i == 5) {\\n break; // 当 i 等于 5 时,跳出循环\\n }\\n if (i % 2 != 0) {\\n continue; // 如果 i 是奇数,跳过本次迭代的 print\\n }\\n print(\'Even number: $i\');\\n}\\n
\\nswitch
和 case
:
switch
语句比较一个整数、字符串或编译时常量与 case
子句中的值。
每个非空的 case
子句通常以 break
语句结束。也可以使用 continue
(配合标签), throw
或 return
。
如果没有任何 case
匹配,则执行 default
子句(如果存在)。
Dart 2.19 之后,switch
支持更强大的模式匹配功能,但这里我们先看基础用法。
String command = \'OPEN\';\\nswitch (command) {\\n case \'OPEN\':\\n print(\'Opening file...\');\\n break; // 非常重要,否则会 \\"fall-through\\" 到下一个 case (在 Dart 中,除非 case 为空,否则不允许隐式 fall-through)\\n case \'CLOSE\':\\n print(\'Closing file...\');\\n break;\\n case \'SAVE\':\\n print(\'Saving file...\');\\n break;\\n default:\\n print(\'Unknown command.\');\\n}\\n\\n// Dart 的 switch 不允许隐式 \\"fall-through\\"\\n// 如果你想让一个 case 执行完后继续执行下一个 case 的代码,需要显式使用标签和 continue (较少用)\\n// 或者如果 case 的代码块为空,则会自动 fall-through 到下一个非空 case\\nint value = 1;\\nswitch (value) {\\n case 0:\\n case 1: // value 为 0 或 1 都会执行这里的代码\\n print(\\"Value is 0 or 1\\");\\n break;\\n case 2:\\n print(\\"Value is 2\\");\\n break;\\n default:\\n print(\\"Other value\\");\\n}\\n
\\n函数是一段可重复使用的代码块,用于执行特定的任务。Dart 是一门真正的面向对象语言,所以即使是函数也是对象,其类型是 Function
。
函数的定义与调用:
\\n// 定义一个函数,指定返回类型 (void 表示不返回任何值)\\nvoid greet(String name) {\\n print(\'Hello, $name!\');\\n}\\n\\n// 定义一个带返回值的函数\\nint add(int a, int b) {\\n return a + b;\\n}\\n\\nvoid main() {\\n greet(\'Alice\'); // 调用 greet 函数\\n int result = add(5, 3); // 调用 add 函数并获取返回值\\n print(\'Sum: $result\');\\n}\\n
\\n函数参数:
\\nDart 提供了灵活的参数定义方式:
\\n必选参数 (Required Positional Parameters): 参数必须按顺序传递。
\\nvoid describe(String name, int age) {\\n print(\'$name is $age years old.\');\\n}\\n// describe(\'Bob\'); // 错误,缺少 age 参数\\n// describe(30, \'Bob\'); // 错误,参数顺序和类型不匹配\\ndescribe(\'Bob\', 30);\\n
\\n可选命名参数 (Optional Named Parameters):
\\n使用 {} 包裹参数。调用时通过 parameterName: value 的形式指定。
\\n默认情况下,命名参数是可选的,如果不传递,它们的值为 null(除非它们是不可空类型且没有默认值,这时必须提供或标记为 required)。
\\n你可以使用 required 关键字使命名参数变为必传。
\\nvoid printInfo({String? name, int? age}) { // name 和 age 可空\\n print(\'Name: ${name ?? \\"N/A\\"}, Age: ${age ?? \\"N/A\\"}\');\\n}\\n\\nvoid printUserDetails({required String username, String? email}) {\\n print(\'Username: $username, Email: ${email ?? \\"No email\\"}\');\\n}\\n\\nvoid main() {\\n printInfo(name: \'Charlie\', age: 25);\\n printInfo(age: 30); // name 会是 null\\n printInfo(); // name 和 age 都会是 null\\n\\n printUserDetails(username: \'dave\');\\n printUserDetails(username: \'eve\', email: \'eve@example.com\');\\n // printUserDetails(email: \'test@example.com\'); // 错误,username 是 required 的\\n}\\n
\\n可选位置参数 (Optional Positional Parameters):
\\n使用 [] 包裹参数。调用时按顺序传递,可以省略。
\\nString say(String from, String msg, [String? device]) {\\n var result = \'$from says $msg\';\\n if (device != null) {\\n result = \'$result with $device\';\\n }\\n return result;\\n}\\n\\nvoid main() {\\n print(say(\'John\', \'Hello\')); // device 为 null\\n print(say(\'Jane\', \'Hi\', \'iPhone\'));\\n}\\n
\\n默认参数值 (Default Parameter Values):
\\n可选参数(命名或位置)都可以有默认值,使用 = 指定。默认值必须是编译时常量。
\\n如果参数是可空的,并且没有提供值,它将是 null。如果它有默认值,则会使用默认值。
\\nvoid setVolume(int volume, {int min = 0, int max = 100}) {\\n print(\'Setting volume to $volume, min: $min, max: $max\');\\n}\\n\\nvoid enableFlags({bool bold = false, bool hidden = false}) {\\n print(\'Bold: $bold, Hidden: $hidden\');\\n}\\n\\nvoid main() {\\n setVolume(50); // min: 0, max: 100 (使用默认值)\\n setVolume(70, max: 120); // min: 0 (使用默认值), max: 120\\n\\n enableFlags(bold: true); // bold: true, hidden: false (使用默认值)\\n}\\n
\\n注意: 必选参数不能有默认值。
\\n返回值 (return
):
return
语句,或者 return;
,则函数隐式返回 null
。int
),则必须返回该类型的值(或 null
,如果返回类型可空 int?
)。void
,它不能有 return <value>;
语句,但可以有 return;
。匿名函数 (Anonymous Functions / Lambdas / Closures):
\\n没有名字的函数。通常用于传递给其他函数或赋值给变量。
\\n语法: (parameterList) { statements; }
或 (parameterList) => expression
。
闭包 (Closure):匿名函数可以捕获其词法作用域中的变量(即使函数在其原始作用域之外执行)。
\\nvoid main() {\\n var fruits = [\'apple\', \'banana\', \'orange\'];\\n\\n // 使用匿名函数作为 forEach 的参数\\n fruits.forEach((fruit) {\\n print(fruit.toUpperCase());\\n });\\n\\n // 将匿名函数赋值给变量\\n var multiply = (int a, int b) {\\n return a * b;\\n };\\n print(\'Product: ${multiply(4, 5)}\');\\n\\n // 闭包示例\\n Function makeAdder(int addBy) {\\n return (int i) => addBy + i; // 这个匿名函数捕获了 addBy\\n }\\n\\n var add2 = makeAdder(2); // addBy is 2\\n var add4 = makeAdder(4); // addBy is 4\\n\\n print(add2(3)); // 5 (2 + 3)\\n print(add4(3)); // 7 (4 + 3)\\n}\\n
\\n箭头函数 (=>
):
如果函数体只包含一个表达式,可以使用箭头语法,也称为 \\"fat arrow\\" 语法。
\\n=> expression;
等价于 { return expression; }
。
int subtract(int a, int b) => a - b; // 等价于 { return a - b; }\\n\\nvoid printMessage(String message) => print(\'Message: $message\'); // 返回 void (隐式返回 null)\\n\\nvoid main() {\\n print(\'Difference: ${subtract(10, 4)}\');\\n printMessage(\'Dart is concise!\');\\n}\\n
\\n词法作用域 (Lexical Scope):
\\nDart 是一种词法作用域语言,这意味着变量的作用域在代码编写时就已静态确定,而不是在运行时确定。
\\n内层作用域可以访问外层作用域的变量,但反之不行。
\\n花括号 {}
定义了一个新的作用域。
String topLevelVariable = \'Top\';\\n\\nvoid main() {\\n String mainVariable = \'Main\';\\n\\n void innerFunction() {\\n String innerVariable = \'Inner\';\\n print(topLevelVariable); // 可以访问\\n print(mainVariable); // 可以访问\\n print(innerVariable); // 可以访问\\n }\\n\\n innerFunction();\\n // print(innerVariable); // 错误!innerVariable 在 innerFunction 作用域内\\n}\\n
\\nDart 是一门纯粹的面向对象语言,每个值都是一个对象,包括数字、函数和 null
。所有的对象都继承自 Object
类。
类 (Classes) 与对象 (Objects):
\\n类 (class
) 是创建对象的蓝图或模板。它定义了对象的属性(实例变量)和行为(方法)。
对象 (Object
) 是类的一个实例。
// 定义一个类\\nclass Dog {\\n String name; // 实例变量\\n int age; // 实例变量\\n\\n // 构造函数 (后面详细讲)\\n Dog(this.name, this.age);\\n\\n // 方法\\n void bark() {\\n print(\'$name says Woof!\');\\n }\\n\\n void displayAge() {\\n print(\'$name is $age years old.\');\\n }\\n}\\n\\nvoid main() {\\n // 创建 Dog 类的对象 (实例化)\\n // \'new\' 关键字在 Dart 2 中是可选的\\n var myDog = Dog(\'Buddy\', 3);\\n Dog anotherDog = Dog(\'Lucy\', 5);\\n\\n myDog.bark(); // 调用对象的方法\\n myDog.displayAge();\\n\\n anotherDog.bark();\\n anotherDog.displayAge();\\n\\n print(myDog.name); // 访问对象的实例变量\\n}\\n
\\n构造函数 (Constructors):
\\n构造函数是用于创建和初始化对象的特殊方法。它的名称与类名相同。
\\n默认构造函数 (Default Constructor):
\\n如果你不声明构造函数,Dart 会提供一个默认的无参构造函数(前提是父类也有可访问的无参构造函数)。
\\n最常见的构造函数形式是直接初始化实例变量,可以使用 this. 语法糖:
\\nclass Point {\\n double x;\\n double y;\\n\\n // 语法糖:直接将参数赋值给同名实例变量\\n Point(this.x, this.y);\\n}\\n// 等价于:\\n// class Point {\\n// double x;\\n// double y;\\n// Point(double x, double y) {\\n// this.x = x;\\n// this.y = y;\\n// }\\n// }\\n\\nvoid main() {\\n var p1 = Point(10.0, 20.0);\\n print(\'Point: (${p1.x}, ${p1.y})\');\\n}\\n
\\n命名构造函数 (Named Constructors):
\\n一个类可以有多个命名构造函数,以提供不同的对象创建方式。
\\n语法:ClassName.identifierName()
\\nclass Rectangle {\\n double width;\\n double height;\\n\\n // 主构造函数\\n Rectangle(this.width, this.height);\\n\\n // 命名构造函数:创建一个正方形\\n Rectangle.square(double side)\\n : width = side,\\n height = side; // 初始化列表,在构造函数体执行前赋值\\n\\n // 命名构造函数:从一个 Map 创建\\n Rectangle.fromMap(Map<String, double> map)\\n : width = map[\'width\']!, // 使用 ! 断言 map[\'width\'] 不为 null (后面讲 Null Safety)\\n height = map[\'height\']!;\\n\\n double get area => width * height;\\n}\\n\\nvoid main() {\\n var rect1 = Rectangle(10, 20);\\n var square = Rectangle.square(15);\\n var rectFromMap = Rectangle.fromMap({\'width\': 5, \'height\': 8});\\n\\n print(\'Rect1 area: ${rect1.area}\');\\n print(\'Square area: ${square.area}\');\\n print(\'RectFromMap area: ${rectFromMap.area}\');\\n}\\n
\\n工厂构造函数 (Factory Constructors - 初步了解):
\\n使用 factory 关键字声明。工厂构造函数不总是创建其类的新实例。例如,它可以返回一个缓存中的实例,或者返回一个子类型的实例。
\\n工厂构造函数内部不能使用 this 关键字访问实例成员,因为它可能不创建新实例。
\\nclass Logger {\\n final String name;\\n static final Map<String, Logger> _cache = <String, Logger>{};\\n\\n // 私有命名构造函数,防止外部直接实例化\\n Logger._internal(this.name);\\n\\n // 工厂构造函数\\n factory Logger(String name) {\\n if (_cache.containsKey(name)) {\\n return _cache[name]!;\\n } else {\\n final logger = Logger._internal(name);\\n _cache[name] = logger;\\n return logger;\\n }\\n }\\n\\n void log(String msg) {\\n print(\'$name: $msg\');\\n }\\n}\\n\\nvoid main() {\\n var logger1 = Logger(\'UI\');\\n var logger2 = Logger(\'Network\');\\n var logger3 = Logger(\'UI\'); // 会从缓存中返回 logger1\\n\\n logger1.log(\'Button clicked\');\\n logger2.log(\'Request sent\');\\n logger3.log(\'Dialog shown\');\\n\\n print(identical(logger1, logger3)); // true,它们是同一个对象\\n}\\n
\\n工厂构造函数是一个比较高级的概念,我们先初步了解即可。
\\n方法 (Methods) 和实例变量 (Instance Variables):
\\n实例变量 (Instance Variables): 类中定义的变量,属于类的每个实例。
\\n方法 (Methods): 类中定义的函数,用于操作对象的实例变量或执行其他操作。方法可以访问 this
和实例变量。
Getter 和 Setter: 特殊的方法,用于读取和写入对象的属性。可以使用 get
和 set
关键字定义。
class Circle {\\n double radius;\\n\\n Circle(this.radius);\\n\\n // Getter for diameter\\n double get diameter => radius * 2;\\n\\n // Setter for diameter (updates radius)\\n set diameter(double newDiameter) {\\n if (newDiameter > 0) {\\n radius = newDiameter / 2;\\n }\\n }\\n\\n // Method\\n double calculateArea() {\\n return 3.14159 * radius * radius;\\n }\\n}\\n\\nvoid main() {\\n var c = Circle(5.0);\\n print(\'Radius: ${c.radius}\');\\n print(\'Area: ${c.calculateArea()}\');\\n print(\'Diameter (getter): ${c.diameter}\'); // 使用 getter\\n\\n c.diameter = 12; // 使用 setter\\n print(\'New Radius after setting diameter: ${c.radius}\');\\n print(\'New Area: ${c.calculateArea()}\');\\n}\\n
\\nthis
关键字:
在类的方法或构造函数中,this
关键字指向当前对象的实例。
通常在参数名与实例变量名冲突时使用,或者在需要将当前实例传递给其他方法时使用。
\\nclass MyClass {\\n String name;\\n\\n MyClass(String name) {\\n this.name = name; // this.name 是实例变量, name 是参数\\n }\\n\\n void printName() {\\n print(this.name); // this 可选,因为没有歧义\\n }\\n\\n MyClass getSelf() {\\n return this; // 返回当前实例\\n }\\n}\\n
\\n继承 (Inheritance) - (extends
):
继承允许一个类(子类或派生类)获取另一个类(父类或基类)的属性和方法。
\\n使用 extends
关键字实现继承。
子类可以重写父类的方法,添加新的方法和属性。
\\nDart 是单继承语言,即一个类只能直接继承一个父类。
\\nclass Animal {\\n String name;\\n Animal(this.name);\\n\\n void makeSound() {\\n print(\'Animal sound\');\\n }\\n}\\n\\nclass Cat extends Animal {\\n // 子类构造函数必须调用父类的构造函数\\n // 使用 super(...) 来调用父类构造函数\\n Cat(String name) : super(name);\\n\\n // 覆盖父类的方法\\n @override // @override 注解表示这个方法是重写父类的方法\\n void makeSound() {\\n print(\'$name says Meow!\');\\n }\\n\\n void purr() {\\n print(\'$name is purring.\');\\n }\\n}\\n\\nvoid main() {\\n var animal = Animal(\'Generic Animal\');\\n var cat = Cat(\'Whiskers\');\\n\\n animal.makeSound(); // Animal sound\\n cat.makeSound(); // Whiskers says Meow! (调用的是 Cat 类重写的方法)\\n cat.purr(); // Whiskers is purring.\\n // animal.purr(); // 错误!Animal 类没有 purr 方法\\n}\\n
\\n覆盖成员 (@override
):
@override
注解是一个好习惯,它可以让编译器检查你是否真的覆盖了一个父类成员,并且能更清晰地表达你的意图。super
关键字 (初步了解):
用于调用父类的构造函数或方法。
\\nsuper()
: 调用父类的无名构造函数。
super.namedConstructor()
: 调用父类的命名构造函数。
super.methodName()
: 调用父类的方法。
class Vehicle {\\n String model;\\n Vehicle(this.model) {\\n print(\'Vehicle constructor: $model\');\\n }\\n void start() {\\n print(\'$model Vehicle started.\');\\n }\\n}\\n\\nclass Car extends Vehicle {\\n String color;\\n // 调用父类的构造函数,并初始化自己的成员\\n Car(String model, this.color) : super(model) {\\n print(\'Car constructor: $model, Color: $color\');\\n }\\n\\n @override\\n void start() {\\n super.start(); // 调用父类的 start 方法\\n print(\'Car ($model, $color) specific start sequence.\');\\n }\\n}\\n\\nvoid main() {\\n var myCar = Car(\'SedanX\', \'Red\');\\n myCar.start();\\n // 输出:\\n // Vehicle constructor: SedanX\\n // Car constructor: SedanX, Color: Red\\n // SedanX Vehicle started.\\n // Car (SedanX, Red) specific start sequence.\\n}\\n
\\n(抽象类 abstract class
和接口 implements
- 可作为进阶了解):
abstract class
): 不能被实例化的类,通常用作基类,可以包含抽象方法(没有实现的方法,子类必须实现)。implements
): Dart 没有专门的 interface
关键字。任何类都可以作为接口。当一个类 implements
另一个类(接口)时,它必须实现该接口的所有方法和 getter(除非实现类本身是抽象的)。Null Safety 是 Dart 语言的一个重要特性,旨在通过在类型系统中区分可空类型和不可空类型,来帮助开发者在编译时就发现潜在的空引用错误 (Null Pointer Exceptions)。
\\n理解 Null Safety 的重要性:
\\n空引用是许多编程语言中常见的错误来源。Null Safety 通过强制你在编码时就考虑变量是否可能为 null,从而减少运行时因空引用导致的程序崩溃。
\\n可空类型 (?
):
默认情况下,所有类型都是不可空的。如果你想让一个变量可以持有 null
值,你需要在类型声明后面加上 ?
。
int a = 5; // 不可空,a 不能是 null\\n// a = null; // 编译错误\\n\\nString? name = \'Alice\'; // 可空,name 可以是 \'Alice\' 或 null\\nname = null; // 合法\\n\\n// 如果你尝试访问可空类型的方法或属性,而不先检查它是否为 null,编译器会报错\\n// print(name.length); // 错误!name 可能为 null\\n
\\n非空断言 (!
):
如果你非常确定一个可空表达式的值在运行时不会是 null
,你可以使用后缀操作符 !
来断言它非空。
警告: 如果你在一个值为 null
的表达式上使用 !
,你的程序会在运行时抛出异常。所以请谨慎使用。
String? getNullableString() {\\n return \\"Hello\\"; // 或者 return null;\\n}\\n\\nvoid main() {\\n String? maybeString = getNullableString();\\n if (maybeString != null) {\\n // 在检查后,编译器知道 maybeString 在这个分支里不为 null (类型提升)\\n print(maybeString.length);\\n }\\n\\n // 如果你非常确定它不为 null (例如,基于某些外部逻辑)\\n String notNullString = maybeString!; // 断言 maybeString 不为 null\\n print(notNullString.length); // 如果 maybeString 实际为 null,这里会抛出运行时异常\\n}\\n
\\n类型提升 (Type Promotion):
\\nDart 的流程分析器 (flow analyzer) 非常智能。如果你对一个可空变量进行了 null
检查 (例如 if (variable != null)
), 在该检查的作用域内,编译器会自动将该变量提升 (promote) 为其对应的不可空类型。
void printStringLength(String? str) {\\n if (str != null) {\\n // 在这个代码块中,str 被提升为 String (不可空)\\n print(\'Length: ${str.length}\');\\n } else {\\n print(\'String is null.\');\\n }\\n // print(str.length); // 错误!在这里 str 仍然是 String?\\n}\\n
\\nlate
关键字:
late
关键字有几个用途,主要用于处理非空变量的延迟初始化:
声明一个非空变量,但它的初始化会延迟到首次使用时才进行。 如果在初始化前访问它,会抛出运行时错误。
\\n用于懒加载实例变量。
\\nclass MyService {\\n late String _data; // 声明一个非空的 _data,但延迟初始化\\n\\n void _initializeData() {\\n print(\\"Initializing data...\\");\\n _data = \\"Fetched Data\\";\\n }\\n\\n String getData() {\\n // 假设我们在这里确保了 _initializeData 会被调用,或者 _data 会被赋值\\n if (!this::_data.isInitialized) { // 检查 late 变量是否已初始化 (需要 Dart 2.12+)\\n _initializeData();\\n }\\n return _data;\\n }\\n}\\n\\n// 另一个例子: 懒加载\\nclass HeavyComputation {\\n late String result = _compute(); // result 会在首次访问时计算\\n\\n String _compute() {\\n print(\\"Performing heavy computation...\\");\\n // 模拟耗时操作\\n for(int i=0; i<1000000000; i++) {}\\n return \\"Computation Done\\";\\n }\\n}\\n\\nvoid main() {\\n MyService service = MyService();\\n // print(service._data); // 如果 _initializeData 还没被调用,这里会抛出 LateInitializationError\\n print(service.getData()); // getData 内部会确保初始化\\n\\n HeavyComputation hc = HeavyComputation();\\n print(\\"HeavyComputation instance created.\\");\\n // _compute() 还没有执行\\n print(hc.result); // 此时 _compute() 执行,然后返回结果\\n print(hc.result); // 再次访问,直接返回已计算的结果,_compute() 不会再次执行\\n}\\n
\\nlate
关键字对于处理那些你知道在运行时首次使用前一定会被初始化的非空变量非常有用,例如 Flutter 中的 initState
。
现代应用经常需要执行一些耗时操作,比如网络请求、文件读写等。如果这些操作在主线程同步执行,会导致用户界面卡顿。异步编程允许这些操作在后台执行,完成后再通知主线程。
\\nFuture
对象:
Future<T>
对象代表一个异步操作的最终结果,这个结果的类型是 T
。
一个 Future
会在某个时刻完成,并提供一个值(如果操作成功)或一个错误(如果操作失败)。
你可以使用 .then()
来注册一个回调,当 Future
完成时,该回调会被执行。使用 .catchError()
来处理可能发生的错误。
Future<String> fetchData() {\\n // 模拟一个网络请求,2秒后返回数据\\n return Future.delayed(Duration(seconds: 2), () {\\n // return \'Data fetched successfully!\';\\n throw Exception(\'Failed to fetch data!\'); // 模拟错误\\n });\\n}\\n\\nvoid main() {\\n print(\'Fetching data...\');\\n fetchData().then((value) {\\n print(value); // Future 成功完成时执行\\n }).catchError((error) {\\n print(\'Error: $error\'); // Future 失败时执行\\n }).whenComplete(() {\\n print(\'Data fetching operation complete.\'); // 无论成功或失败都会执行\\n });\\n print(\'Doing other work while data is being fetched...\');\\n}\\n
\\nasync
和 await
:
async
和 await
是 Dart 提供的用于简化异步代码书写的关键字,让异步代码看起来更像同步代码。
async
: 将一个函数标记为异步函数。异步函数的返回类型通常是 Future<T>
。如果异步函数不返回任何有意义的值,其返回类型是 Future<void>
。
await
: 只能在 async
函数内部使用。它会暂停当前 async
函数的执行,等待其后的 Future
完成。一旦 Future
完成,await
会返回 Future
的结果 (如果是错误则会抛出)。
Future<String> downloadFile(String url) {\\n print(\'Starting download: $url\');\\n return Future.delayed(Duration(seconds: 3), () {\\n if (url.isEmpty) {\\n throw Exception(\'URL cannot be empty\');\\n }\\n return \'Content of $url\';\\n });\\n}\\n\\n// 使用 async 和 await\\nFuture<void> processDownloads() async {\\n print(\'Process started.\');\\n try {\\n String file1Content = await downloadFile(\'http://example.com/file1.txt\');\\n print(\'File 1 downloaded: ${file1Content.length} bytes\');\\n\\n String file2Content = await downloadFile(\'http://example.com/file2.txt\');\\n print(\'File 2 downloaded: ${file2Content.length} bytes\');\\n\\n // String file3Content = await downloadFile(\'\'); // 模拟一个错误\\n // print(\'File 3 downloaded: ${file3Content.length} bytes\');\\n\\n } catch (e) {\\n print(\'An error occurred during download: $e\');\\n } finally {\\n print(\'All download attempts finished.\');\\n }\\n}\\n\\nvoid main() {\\n print(\'Main function started.\');\\n processDownloads(); // 调用异步函数\\n print(\'Main function continues execution...\'); // 这会先于 processDownloads 内部的 await 后的代码执行\\n}\\n
\\n使用 async/await
可以让异步代码的逻辑更清晰,更容易理解和维护。
(Streams - 可作为进阶了解):
\\nStream
是一种处理异步事件序列的方式。与 Future
代表单个异步结果不同,Stream
可以随着时间的推移发出零个或多个值或错误。Stream
来表示。await for
循环来监听 Stream
发出的事件,或者使用 listen()
方法。Stream
是响应式编程中的一个核心概念,我们会在进阶时详细学习。Mixins 是一种在多个类层次结构中复用类代码的方式。当你想为一个类添加某些行为,但又不想通过继承(因为 Dart 是单继承)或者不想让这个行为成为类定义的核心部分时,Mixin 非常有用。
\\n使用 mixin关键字定义 Mixin:
\\nMixin 本身不能被实例化,也不能有构造函数。
\\nmixin Piloting {\\n int astronauts = 1;\\n\\n void pilot() {\\n print(\'Glide through the stars\');\\n }\\n}\\n\\nmixin FuelSystem {\\n void fillFuel() {\\n print(\'Filling fuel tank...\');\\n }\\n void checkFuelLevel() {\\n print(\'Fuel level is good.\');\\n }\\n}\\n
\\n使用 with
关键字将 Mixin 应用到类:
class Spacecraft {\\n String name;\\n Spacecraft(this.name);\\n\\n void launch() {\\n print(\'$name launching...\');\\n }\\n}\\n\\n// Spaceship 类继承自 Spacecraft,并混入了 Piloting 和 FuelSystem 的行为\\nclass Spaceship extends Spacecraft with Piloting, FuelSystem {\\n Spaceship(String name) : super(name);\\n\\n void fly() {\\n print(\'$name with $astronauts astronauts is flying!\');\\n pilot(); // 来自 Piloting mixin\\n checkFuelLevel(); // 来自 FuelSystem mixin\\n }\\n}\\n\\nclass OrbitalModule with FuelSystem { // 也可以不继承任何类,直接使用 mixin\\n void dock() {\\n print(\\"Docking module\\");\\n fillFuel();\\n }\\n}\\n\\nvoid main() {\\n var enterprise = Spaceship(\'Enterprise\');\\n enterprise.launch();\\n enterprise.fillFuel();\\n enterprise.fly();\\n\\n var fuelPod = OrbitalModule();\\n fuelPod.dock();\\n}\\n
\\nMixin 的作用和使用场景 (代码复用):
\\n代码复用: Mixin 的核心目的是共享行为。你可以定义一组通用的方法和属性,然后将它们混入到需要的类中。
\\n避免继承的限制: Dart 是单继承的,Mixin 提供了一种“横向”组合行为的方式,而不是“纵向”的继承。
\\n特定能力的赋予: 例如,你可以创建一个 Serializable
mixin 来为类添加序列化/反序列化的能力,一个 Loggable
mixin 添加日志记录能力等。
Mixin 可以通过 on
关键字来限制它可以被混入的类的类型(即要求宿主类是某个特定类或其子类)。这允许 Mixin 调用宿主类定义的方法。
abstract class Performer {\\n void performAction();\\n}\\n\\nmixin Musical on Performer { // 这个 Mixin 只能被 Performer 或其子类使用\\n void playInstrument() {\\n print(\'Playing music...\');\\n performAction(); // 可以调用 Performer 中定义的方法\\n }\\n}\\n\\nclass Dancer extends Performer {\\n @override\\n void performAction() {\\n print(\'Dancing gracefully!\');\\n }\\n}\\n\\nclass MusicianDancer extends Dancer with Musical {\\n // MusicianDancer 继承自 Dancer (Dancer 是 Performer), 所以可以混入 Musical\\n}\\n\\nvoid main() {\\n var md = MusicianDancer();\\n md.playInstrument();\\n // 输出:\\n // Playing music...\\n // Dancing gracefully!\\n}\\n
\\n扩展方法允许你为现有的库(甚至是你不拥有的库,比如 Dart 核心库中的类)添加新的功能,而无需更改库的源代码或创建子类。
\\nextension 关键字的用法:
\\n语法:extension on { // members }
\\n是可选的,但如果省略,就无法在导入时显式控制其可见性 (show/hide)。
\\n// 为 String 类添加一个将字符串转换为整数的扩展方法\\nextension StringParsing on String {\\n int? toIntOrNull() {\\n return int.tryParse(this); // \'this\' 指向 String 实例本身\\n }\\n\\n String capitalizeFirstLetter() {\\n if (this.isEmpty) return this;\\n return \'${this[0].toUpperCase()}${this.substring(1)}\';\\n }\\n}\\n\\n// 也可以不给扩展命名 (但不推荐,除非在同一个库中使用)\\n// extension on List<int> {\\n// int sum() {\\n// return this.fold(0, (prev, element) => prev + element);\\n// }\\n// }\\n\\n\\nvoid main() {\\n String numberStr = \'123\';\\n String text = \\"hello world\\";\\n String empty = \\"\\";\\n\\n int? parsedNum = numberStr.toIntOrNull();\\n print(\'Parsed number: $parsedNum\'); // Parsed number: 123\\n\\n String? invalidStr = \'abc\';\\n print(\'Parsed invalid: ${invalidStr.toIntOrNull()}\'); // Parsed invalid: null\\n\\n print(\'Capitalized: ${text.capitalizeFirstLetter()}\'); // Capitalized: Hello world\\n print(\'Capitalized empty: \\"${empty.capitalizeFirstLetter()}\\"\'); // Capitalized empty: \\"\\"\\n\\n List<int> numbers = [1, 2, 3, 4, 5];\\n // 如果上面那个匿名的 List 扩展被定义了,可以这样调用:\\n // print(\'Sum of numbers: ${numbers.sum()}\');\\n}\\n
\\n使用场景:
\\nDateTime
添加格式化方法,为 List
添加更复杂的集合操作等。dynamic
类型的变量恰好持有一个 String
,你不能直接调用为 String
定义的扩展方法,除非你先将其转换为 String
。在程序执行过程中,可能会发生各种预料之外的情况,比如文件未找到、网络连接失败、无效输入等。错误处理机制允许你优雅地捕获和处理这些异常情况,防止程序崩溃。
\\ntry-catch
语句:捕获和处理异常
将可能抛出异常的代码块放在 try
块中。
使用 catch
块来捕获并处理异常。
void main() {\\n try {\\n int result = 10 ~/ 0; // 整数除法,除以0会抛出 IntegerDivisionByZeroException\\n print(\'Result: $result\'); // 这行不会执行\\n } catch (e) {\\n // \'e\' 是捕获到的异常对象\\n print(\'An error occurred: $e\');\\n }\\n print(\'Program continues...\');\\n}\\n
\\n你还可以获取堆栈跟踪信息(StackTrace
),这对于调试非常有用:
try {\\n // ... some code that might throw ...\\n throw FormatException(\'Invalid format\');\\n} catch (e, s) { // s 是 StackTrace 对象\\n print(\'Exception: $e\');\\n print(\'Stack trace:\\\\n$s\');\\n}\\n
\\non
关键字:捕获特定类型的异常
如果你想针对不同类型的异常执行不同的处理逻辑,可以使用 on
关键字。
void main() {\\n String input = \\"abc\\";\\n try {\\n // int value = int.parse(input); // FormatException\\n // print(value);\\n var list = <int>[];\\n print(list[0]); // RangeError\\n } on FormatException catch (e) {\\n print(\'Caught a FormatException: $e\');\\n print(\'Input string \\"$input\\" is not a valid integer.\');\\n } on RangeError catch (e) {\\n print(\'Caught a RangeError: $e\');\\n print(\'Accessing an invalid index.\');\\n } catch (e) {\\n // 通用的 catch 块,捕获其他未被 on 子句捕获的异常\\n print(\'Caught some other exception: $e\');\\n }\\n}\\n
\\non
关键字可以不带 catch (e)
,如果你不需要异常对象本身。
finally
关键字:确保某些代码无论是否发生异常都会执行
finally
块中的代码总是在 try
块执行完毕后执行,无论是否发生异常,也无论异常是否被捕获。
通常用于释放资源,如关闭文件、取消网络连接等。
\\nvoid main() {\\n try {\\n print(\'Trying to perform an operation...\');\\n // throw Exception(\'Something went wrong!\');\\n print(\'Operation successful.\');\\n } catch (e) {\\n print(\'Caught exception: $e\');\\n } finally {\\n print(\'Finally block executed. Cleaning up resources...\');\\n }\\n print(\'After try-catch-finally.\');\\n}\\n
\\nthrow
关键字:抛出异常
你可以使用 throw
关键字显式地抛出一个异常。你可以抛出预定义的异常(如 FormatException
, ArgumentError
),或者自定义异常类。
class InsufficientFundsException implements Exception {\\n final String message;\\n InsufficientFundsException(this.message);\\n\\n @override\\n String toString() => \'InsufficientFundsException: $message\';\\n}\\n\\nvoid withdraw(double amount, double balance) {\\n if (amount <= 0) {\\n throw ArgumentError(\'Amount to withdraw must be positive.\');\\n }\\n if (amount > balance) {\\n throw InsufficientFundsException(\'Cannot withdraw $amount. Balance is only $balance.\');\\n }\\n print(\'Withdrawing $amount...\');\\n // ... 实际的取款逻辑 ...\\n}\\n\\nvoid main() {\\n try {\\n withdraw(200, 100);\\n } on ArgumentError catch (e) {\\n print(\'Argument error: $e\');\\n } on InsufficientFundsException catch (e) {\\n print(\'Funds error: $e\');\\n } catch (e) {\\n print(\'Unknown error: $e\');\\n }\\n}\\n
\\n我们已经完成了 Dart 语言基础核心概念的学习。从变量、数据类型到控制流、函数,再到面向对象编程的初步认识、空安全、异步编程、Mixin、扩展方法以及错误处理,这些都是构建 Dart 应用的基石。
\\n当然,Dart 的世界远不止这些。还有很多高级特性和库等待我们去探索,比如集合的深入使用、Stream 的高级操作、Dart 的并发模型 (Isolates)、元数据 (Annotations) 等等。
","description":"Dart 是一门由 Google 开发的、为客户端优化的编程语言,它的目标是让你能在任何平台上构建快速、美观的用户界面。无论你是想开发移动应用 (Flutter)、Web 应用,还是后端服务,Dart 都能助你一臂之力。 本教程是为那些已经具备一些编程基础的同学设计的。如果你了解变量、循环、函数等基本概念,那么学习 Dart 将会非常顺利。\\n\\n建议学习资源:\\n\\n在学习过程中,我强烈建议大家参考 Dart 官方的中文文档,特别是 \\"语言概览\\" 或 \\"Language Tour\\" 部分,那里有更详尽的解释和示例:\\n\\nDart 官方中文文档: dart.cn…","guid":"https://juejin.cn/post/7502299719806533695","author":"Tieboyh","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-10T06:59:10.844Z","media":null,"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Google 开始正式强制 Android 适配 16 K Page Size,你准备好了吗?","url":"https://juejin.cn/post/7502246437372182569","content":"去年中旬我就分享过几篇关于 Android 适配 16K Page Size 的文章,当时就提及了 2025 Google 将会强制要求,而现在 Google 给出了明确时间:自 2025 年 11 月 1 日起,所有提交到 Google Play 且面向 Android 15+ 设备的新应用和现有应用的更新都必须支持 16 KB 的页面大小。
\\n\\n\\n❕❕❕注意,如何适配和查看调试的文章放在了最后面。
\\n
对于兼容,最简单的判断就是你是否使用了动态链接库 so ,如果用了陈年老库,那么你大概率是必须去做适配支持,同步的还有 Flutter 和 React Native 版本,升级到对应支持 16K 的版本是必须的:
\\n如果你的应用已经上架了 Google Play ,可以通过访问 Play 管理中心内的 app bundle 资源管理器页面查看:
\\n那什么是 Page Size ?一般意义上,页面(Page)指的就是 Linux 虚拟内存管理中使用的最小数据单位,页面大小(Page Size)就是虚拟地址空间中的页面大小, Linux 中进程的虚拟地址空间是由固定大小的页面组成:
\\n因为 Android 用的是 Linux 内核,所以在这部分逻辑一直以来都是遵循 Linux 的实现,只是 Android 由于「历史因素」限制,一直只支持 4 KB 内存页面大小,现在想要支持 16K 了而已:
\\n\\n\\n而简单说,16k 是 4k 的整数倍,所以 16K 的 so 也可以跑在 4k 上,但是 4k 因为不是 16k 的整数倍,所以 4k 跑不了 16k 的系统。
\\n
另外,目前来说 Android 继承了 Linux 的特点,不支持混合 Page Size,也就是在 16k 设备上,你的 4k so 就是跑不了,当然,不是系统层面就一定做不了,比如 :
\\n\\n\\n关于这一点,其实 Apple Silicon 的 MacOS 其实是有混合 16K/4K 支持,事实上 macOS 本身始终以 16K 页面运行,只有 Rosetta 模式下应用会以 4K 模式运行。
\\n
从目前官方的信息看,Android 16 添加了兼容模式 ,让一些针对 4 KB 内存页面构建的应用可以在配置为 16 KB 内存页面的设备上运行:
\\n\\n\\n在
\\nAndroidManifest.xml
中设置android:pageSizeCompat
属性以启用向后兼容模式,将会阻止应用启动时显示对话框,如果需要使用android:pageSizeCompat
属性,需使用 Android 16 SDK 编译应用。
另外,关于查看 so 是否适配 16K,还可以通过 readelf 工具,对比编译前后两个 so 的 elf 对齐情况,工具一般位于 /Users/guoshuyu/Library/Android/sdk/ndk/21.4.7075529/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin
,通过以下命令可以输出对应参数:
./aarch64-linux-android-readelf -l /Users/guoshuyu/workspace/android/******/libs/arm64-v8a/libijkffmpeg.so\\n
\\n如下两种图所示:
\\n\\n\\n如果是
\\n10000
,也就是 65536 ,那就是64K 对齐,属于 16K 的 4倍,那「理论上」应该是对齐的
除了 app bundle 资源管理器和查看分页信息之后,最重要是确保在 16 KB 的环境中测试你的应用 ,在之前的文章我就分享过,很多 so 查看时虽然分页是 16K 或者 64K ,但是它还是有问题的,跑在 16K 上是会崩溃的,具体原因有 NDK 工具可能过老,以 IJK 为例:
\\n\\n\\nIJK 的 NDK10e 等版本,编译出来的 so 都是两个 LOAD 段的 Align 是
\\n10000(65536)
, 也就是 64K 对齐,属于 16K 的 4倍,那「理论上」应该是对齐的,但是跑在 16K 上会 crash ,不过 crash 提示也不是 so 不对齐,而是在某段代码执行时出现 crash,并且你定位到的地址代码会很奇葩。
具体解决办法也有,就是强制除了 max-page-size
之外,再配置上 common-page-size
也可以支持:
LOCAL_LDFLAGS += -Wl,-z,max-page-size=65536\\nLOCAL_LDFLAGS += -Wl,-z,common-page-size=65536\\n
\\n如果是需要 cmake 支持的,则可以:
\\ntarget_link_libraries(a4ijkplayer \\"-Wl,-z,max-page-size=65536\\")\\ntarget_link_libraries(a4ijkplayer \\"-Wl,-z,common-page-size=65536\\")。\\n
\\n当然,还可以升级到 NDK r22 解决,例如 NDK r21 就只需要 max-page-size=16384
,从分页上更合理,至于为什么 NDK r22 可以,这就涉及 llvm 和 GUN 的版本和兼容问题 ,详细可见末尾的填坑思路。
目前 GSYVideoPlayer 的 IJK 内核已经在去年提供了 16K 的支持,具体的 git patch 和支持方式也提交在 Github ,需要的可以参考:github.com/CarGuo/GSYV…
\\n最后,如果你还没适配或者了解 16K,可以参考一下文章:
\\n\\n\\n\\n\\n\\n官方定义Stream
是 Dart 中表示连续异步数据序列的核心对象,用于处理多个按时间顺序传递的异步事件。为了更形象地理解,我们把Stream
想象成一个快递网络。数据事件就像是一个个包裹,里面装着各种业务数据,比如网络响应、用户输入;错误事件则像是在运输过程中包裹出现了问题,比如网络超时、数据解析失败;完成事件就代表着快递运输任务的结束,像文件读取完成。
Stream
的核心设计目标是解耦数据生产与消费。就好比快递员(数据生产者)不需要等待收件人(数据消费者)在家,就可以把包裹放在快递柜(Stream
)里,收件人可以在方便的时候去取。这样,双方无需同步等待彼此,提高了效率。
来看个代码示例,模拟一个持续产生数据的Stream
:
Stream<String> generateMessages() async* {\\n int count = 1;\\n while (true) {\\n await Future.delayed(Duration(seconds: 1));\\n yield \\"消息-$count\\";\\n count++;\\n }\\n}\\n
\\n在这个例子中,generateMessages
函数就像是一个快递工厂,每秒生产一个 “消息包裹”。async*
关键字表示这是一个异步生成器,yield
则是把 “包裹” 放到Stream
这个 “快递传送带” 上。
数据按非阻塞方式传递,消费者通过订阅(listen
)被动接收事件,无需主动轮询。这就像我们在家等快递,不用每隔一会儿就去门口看看,快递员到了会通知我们(通过回调函数或await for
)。比如,在一个聊天应用中,新消息随时可能到达,我们可以这样处理:
Stream<String> chatStream = generateMessages();\\nchatStream.listen((message) {\\n print(\'收到新消息: $message\');\\n});\\n
\\n当有新消息产生时,listen
的回调函数就会被触发,我们就能及时处理新消息。
事件严格按先进先出(FIFO)顺序传递,保证处理顺序与发送顺序一致。这确保了快递包裹会按照它们进入快递网络的顺序被派送,不会出现混乱。比如,在处理一系列用户操作记录时,按顺序处理才能保证逻辑的正确性。
\\n单订阅流(Single-Subscription
)仅允许一个监听者,就像一个私人快递柜,只有一个人能取件,确保数据完整性和顺序性(默认类型)。而广播流(Broadcast
)允许多个监听者,类似小区的公共快递柜,大家都能去取件,适用于事件广播场景(需显式声明isBroadcast: true
)。例如,在一个多人在线游戏中,游戏服务器的状态更新可以通过广播流通知所有玩家:
final broadcastStream = StreamController<String>.broadcast();\\nbroadcastStream.stream.listen((status) {\\n print(\'玩家1收到游戏状态更新: $status\');\\n});\\nbroadcastStream.stream.listen((status) {\\n print(\'玩家2收到游戏状态更新: $status\');\\n});\\nbroadcastStream.sink.add(\'游戏开始\');\\n
\\n这里StreamController<String>.broadcast()
创建了一个广播流,多个玩家都能收到游戏状态更新。
Cold
)与热流(Hot
):不同的快递生产模式冷流的数据生成从监听时开始,每次监听会重新触发数据生产。这好比是一家定制蛋糕店,只有顾客下单(监听)了,才开始制作蛋糕(生成数据),如async*
生成的流。热流的数据实时流动,与监听时机无关,类似一家面包店,面包一直在制作,新顾客来了随时能买到当前及之后出炉的面包,就像StreamController
创建的流。
许多场景中,数据持续、动态、按节奏生成。以股票交易应用为例,股票价格实时变化,Stream
可以轻松应对这种情况。
Stream<double> stockPriceStream = Stream.periodic(Duration(seconds: 1), (count) {\\n // 模拟股票价格波动\\n return 100 + (Random().nextDouble() * 10 - 5);\\n});\\nstockPriceStream.listen((price) {\\n print(\'当前股票价格: $price\');\\n});\\n
\\n通过Stream.periodic
定时生成股票价格数据,让用户随时掌握股价动态。
传统一次性异步操作在处理大规模数据时可能引发内存溢出,用户也需等待所有数据加载完成才能交互。而Stream
逐块处理数据,就像我们在处理大文件时,不需要把整个文件一次性搬进仓库(内存),而是逐行处理。
File(\'large_file.txt\')\\n .openRead()\\n .transform(utf8.decoder)\\n .transform(LineSplitter())\\n .listen((line) => processLine(line));\\n
\\n这样,内存占用恒定,确保应用高效稳定运行。
\\nStream
允许通过链式操作符组合数据处理逻辑。比如在一个搜索功能中:
searchInput.stream\\n .distinct()\\n .where((query) => query.length > 2)\\n .asyncMap((query) => fetchResults(query))\\n .listen(updateUI);\\n
\\n这段代码清晰地表达了业务规则,先去重,再过滤掉长度小于 3 的查询,然后异步获取结果,最后更新界面,逻辑层次分明。
\\n在 Flutter 中,Stream
是响应式编程体系的核心基础设施。在状态管理方面,BLoC
、Riverpod
等库依赖Stream
实现状态变化通知;UI
更新时,StreamBuilder
组件将数据流自动映射到界面重建;跨组件通信也可以通过Stream
实现松散耦合的数据传递。以一个计数器应用为例:
class CounterBloc {\\n final _counterController = StreamController<int>();\\n int _count = 0;\\n\\n Stream<int> get counter => _counterController.stream;\\n\\n void increment() {\\n _count++;\\n _counterController.sink.add(_count);\\n }\\n\\n void dispose() => _counterController.close();\\n}\\n\\nStreamBuilder<int>(\\n stream: counterBloc.counter,\\n builder: (context, snapshot) {\\n return Text(\'Count: ${snapshot.data?? 0}\');\\n },\\n)\\n
\\n通过Stream
,业务逻辑与UI
彻底解耦,提升了代码的可测试性和可维护性。
Stream
提供丰富的组合操作符,简化多数据流协作。比如在一个表单验证场景中:
final usernameStream = usernameController.stream;\\nfinal passwordStream = passwordController.stream;\\n\\nStream<bool> get isFormValid =>\\n StreamZip([usernameStream, passwordStream])\\n .map((credentials) =>\\n credentials[0].isNotEmpty && credentials[1].length >= 6)\\n .distinct();\\n
\\n通过StreamZip
合并两个输入字段的流,实时更新表单提交按钮的可用性。
Stream
有一些重要属性。isBroadcast
用于标识是否为广播流;isEmpty
异步判断流是否为空;first
获取流的第一个数据事件;last
获取流的最后一个数据事件(流为空或无限流时要特别注意);single
检查流是否仅有一个数据事件;length
计算流中数据事件的总数量(对无限流会导致永久阻塞)。
StreamController
也有一些关键属性。stream
是控制器关联的输出流,供外部监听数据;sink
是数据入口,用于添加数据、错误或关闭流;isClosed
标识控制器是否已关闭;isPaused
标识流是否被暂停;hasListener
标识是否有活跃的监听者;done
是控制器关闭时完成的Future
。
Stream
的方法丰富多样。工厂构造函数如fromIterable
可从同步集合创建流,fromFuture
将单个Future
转换为流,fromFutures
把多个Future
转换为流,periodic
用于周期性生成事件流等。
核心高频使用方法中,listen
用于订阅流并处理数据、错误和完成事件;map
同步转换每个数据事件;where
过滤不符合条件的数据事件;asyncMap
异步转换每个数据事件;handleError
捕获并处理流中的错误事件;take
仅取前count
个数据后关闭流;skip
跳过前count
个数据。
控制流方法如expand
将每个数据事件展开为多个事件;takeWhile
取数据直到条件为false
;skipWhile
跳过数据直到条件为false
;distinct
跳过连续重复的数据事件。
高级操作与资源管理方法包括transform
应用自定义转换器;pipe
将流数据直接传输到StreamConsumer
;drain
消费流中所有剩余数据但不处理,用于资源清理;cast
将流的数据类型强制转换为指定类型;asBroadcastStream
将单订阅流转换为广播流。
边缘或聚合操作方法有contains
检查流是否包含指定值;forEach
对每个数据执行操作;reduce
聚合所有数据为单个结果;join
将流中的数据拼接为字符串;every
检查所有数据是否满足条件。
使用Stream
一般分为四步:创建流、监听流、操作流、关闭流。
\\n创建流可以使用工厂构造函数,如Stream.fromIterable([1, 2, 3])
从集合创建同步数据流;也可以使用StreamController
动态控制流,或者使用async*
生成器。
\\n监听流时,使用listen
方法,并处理数据、错误和完成事件,同时要注意手动取消订阅以避免内存泄漏。
\\n操作流包括转换数据(如map
、asyncMap
)、过滤数据(如where
、take
、skip
)和错误处理(如handleError
)。
\\n关闭流时,关闭StreamController
,使用await for
处理流,并且清理资源,取消所有订阅。
以一个实时搜索功能为例:
\\nfinal searchController = StreamController<String>();\\nsearchController.stream\\n .where((query) => query.isNotEmpty)\\n .asyncMap((query) => fetchSearchResults(query))\\n .listen(updateUI);\\nsearchController.sink.add(\\"Dart\\");\\nsearchController.sink.add(\\"Flutter\\");\\nawait searchController.close();\\n
\\n这段代码实现了一个简单的实时搜索功能,用户输入搜索词后,自动进行搜索并更新界面。
\\nStream
的运行机制可分为生产者层、处理管道和消费者层。生产者层负责产生异步事件,包括外部输入、生成器和控制器;处理管道通过各种操作符对数据流进行转换、过滤和聚合,同时进行内存管理;消费者层通过订阅机制监听数据流,并在不再需要时释放资源。
订阅驱动:数据仅在存在活跃订阅者时流动,就像快递只有在有收件人时才会派送。
\\n背压控制:通过pause
/resume
动态调节数据流速,防止消费者处理不过来导致内存堆积,类似于快递员发现快递柜满了,先暂停派送,等有空间了再继续。
\\n错误传播:错误事件沿操作符链向上传递,直到被handleError
捕获或导致程序崩溃,就像快递在运输过程中出了问题,会层层上报。
Stream
和Future
有明显区别。Stream
处理多个异步事件,持续存在直到主动关闭,支持多次监听(广播流)或单次监听,适用于实时聊天、文件流式传输等场景;而Future
处理单个异步结果,一次性完成,仅单次完成,无法重复监听,常用于单次网络请求、数据库查询等场景。
掌握Stream
的关键在于构建数据管道思维。把Stream
想象成一个精心设计的快递网络,从数据的生产、传输到消费,每个环节都可以灵活控制。理解其本质,熟练运用操作符,通过实战不断积累经验,就能在异步编程的世界里游刃有余。无论是开发实时聊天应用、金融监控系统,还是其他复杂的异步场景,Stream
都能成为你的得力助手,让数据高效、有序地流动。
视频播放功能对于APP开发来说是非常常见的功能,为了进一步提升用户体验,我们需要有视频缓存功能,来助力流畅播放体验。
\\n通过搜索,可以发现目前网络上开源的flutter相关的视频缓存库基本实现思路:
\\n以上提到的库,在一定程度上能实现视频缓存功能,以及边下边播的功能,但是还是存在无法满足需求的问题。
\\n为了解决以上问题,诞生了今天的主角:flutter_video_caching
\\nflutter_video_caching 是一个纯dart代码实现的视频缓存库,其支持的功能如下:
\\n在 pubspec.yaml 中添加依赖
\\ndependencies:\\n flutter_video_caching: 0.0.1\\n
\\nimport \'package:flutter_video_caching/flutter_video_caching.dart\';\\n\\nvoid main() {\\n WidgetsFlutterBinding.ensureInitialized();\\n VideoProxy.init();\\n runApp(const HomeApp());\\n}\\n
\\nplayControl = VideoPlayerController.networkUrl(url.toLocalUri());\\n
\\n在合适的时机提前缓存视频,打开视频时,将直接从内存中加载视频,有更好的播放体验。
\\nVideoCaching.precache(url);\\n
\\n在Android studio中安装了Flutter插件后,new 一个flutter项目,项目生成后遇到的几个报错。
\\n1.运行自动生成的flutter项目第一个遇到的问题Exception in thread \\"main\\" java.util.zip.ZipException: zip END header not found
\\n\\n该问题是gradle下载的问题,把gradle-wrapper.properties文件中
distributionUrl
改为腾讯云的镜像地址就可以解决。\\n\\n2.再运行又报错Your project\'s Gradle version is incompatible with the Java version that Flutter is using for Gradle.
\\n字面大致意思是gradle版本和java版本不兼容\\n在命令框中执行**
flutter analyze --suggestions
**命令,用于分析项目中潜在的问题,运行后输出:
\\n果然最后一项是个叉号,版本不兼容并分别给出了java和gradle的版本号。\\n通过 AndroidStdio 创建的 Flutter项目,默认 gradle 版本是 7.6.3,如果当前 JDK 版本为 21 时,gradle 需要升级到 8.5 及以上才能支持(JDK 与 Gradle 版本的对应关系详见 # Compatibility Matrix)\\n解决方法是改两个地方:
①gradle-wrapper.properties 中的 distributionUrl
\\n②settings.gradle 中的
com.android.application
(AGP),需要注意的是AGP的版本和下面kotlin的版本也要兼容(参考# Configure a Gradle project),改成:
\\n此时再运行**
flutter analyze --suggestions
**命令就会得到:
全都是对号了@_@\\n,OK再来运行项目,终于跑起来了哈哈。等等怎么打印了3个警告:
警告: [options] 源值 8 已过时,将在未来发行版中删除
\\n警告: [options] 目标值 8 已过时,将在未来发行版中删除
\\n警告: [options] 要隐藏有关已过时选项的警告, 请使用 -Xlint:-options。
\\n强迫症不能忍,查了查说是因为SDK的版本:
改成:
完美解决。
\\n\\n在web
\\nindex.html
文件中使用FLUTTER_BASE_HREF
设置动态的base href
\\n$FLUTTER_BASE_HREF
作为占位符。在打包时,通过命令行替换。
<base href=\\"$FLUTTER_BASE_HREF\\">\\n
\\n\\n\\n那么在打包时需要传入对应的
\\nbase href
flutter build web --base-href /my-web/
在 本地开发 (flutter run -d chrome
) 的时候,base href
是 /
在 独立域名部署(比如自己服务器)也能直接用 /
。
AssetManifest.json
文件\\n\\n在 Flutter 中,
\\nAssetManifest.json
是一个自动生成的文件,用于记录你在pubspec.yaml
中声明的所有\\n资源(assets)。该文件会在构建时由 Flutter 构建系统生成,其内容是一个 JSON 对象,映射了所有资源的路径。
final jsonString = await rootBundle.loadString(\'AssetManifest.json\');\\n
\\n\\n\\nHashUrlStrategy(有 #)
\\n
\\n访问地址https://xxx.com/#/path
刷新时不发真正 HTTP 请求
\\n\\nPathUrlStrategy(无 #)
\\n
\\n访问地址https://xxx.com/path
发起 HTTP 请求/path
需要服务器正确配置。
void main() {\\n setUrlStrategy(const HashUrlStrategy());\\n runApp(const MyApp());\\n}\\n
\\nhtml.document.title
html.window.location.hostname
\\n获取的是 当前网页的主机名,也就是 域名部分,不带端口、不带协议。
\\n\\n当前网页地址 (
\\nwindow.location.href
) 例如https://example.com
\\n
window.location.hostname
返回example.com
class WebBridgeService {\\n static void setup() {\\n html.window.setProperty(\'navTo\'.toJS, _navTo.toJS);\\n }\\n\\n static void _navTo(String path) {\\n Get.toNamed(path);\\n }\\n\\n static void notifyJs(String event, [dynamic data]) {\\n final jsFunc = html.window.getProperty(\'flutterNotify\'.toJS);\\n if (jsFunc != null && jsFunc is JSFunction) {\\n jsFunc.callAsFunction(event.toJS, data?.toJS);\\n }\\n }\\n}\\n
\\nwindow.flutterNotify = function (event, data) {\\n console.log(\'Flutter通知到我了!\', event, data);\\n}\\n\\n
\\nawait
JS 的返回dart调用js的代码
\\nFuture<dynamic> callJsFunctionWithResult(String functionName, List<dynamic> args) async {\\n final jsFunc = html.window.getProperty(functionName.toJS);\\n\\n if (jsFunc != null && jsFunc is JSFunction) {\\n final jsResult = jsFunc.callAsFunction(args.map((e) => e.toJS).toList());\\n\\n // 这里转换Promise为Future\\n final dartResult = await js_util.promiseToFuture(jsResult);\\n\\n return dartResult;\\n } else {\\n throw Exception(\'JS function $functionName not found.\');\\n }\\n}\\n\\nFuture<void> askUserConfirm() async {\\n try {\\n final result = await callJsFunctionWithResult(\'openConfirmDialog\', [\'是否确认删除?\']);\\n if (result == true) {\\n print(\'用户点击了确认\');\\n } else {\\n print(\'用户取消了\');\\n }\\n } catch (e) {\\n print(\'调用 JS 失败: $e\');\\n }\\n}\\n\\n\\n
\\njs 部分的 Promise函数代码
\\nwindow.openConfirmDialog = function (message) {\\n return new Promise(function (resolve, reject) {\\n const confirmed = window.confirm(message);\\n resolve(confirmed); // 用户确认 -> true,取消 -> false\\n });\\n}\\n\\n
\\nkey-value
(键值对)形式的本地数据库,存储在用户浏览器硬盘上。
import \'dart:html\' as html;\\n\\n// 保存数据\\nhtml.window.localStorage[\'token\'] = \'abc123\';\\n\\n// 读取数据\\nString? token = html.window.localStorage[\'token\'];\\n\\n// 删除单个数据\\nhtml.window.localStorage.remove(\'token\');\\n\\n// 清空所有\\nhtml.window.localStorage.clear();\\n
","description":"WEB打包发布之子自定义BASE_HREF 在web index.html 文件中使用FLUTTER_BASE_HREF设置动态的 base href $FLUTTER_BASE_HREF 作为占位符。在打包时,通过命令行替换。\\n\\n近日,React Native 发布了前瞻式的重大更新,主要围绕 Skia & WebGPU 等场景来布局未来的跨平台渲染场景,主要目的是在 “追求与 Web 的对称性”的同时,提供更强大的客户端渲染支持。
\\nReact Native 本次提到,为了对齐 Web 能力并提供强大的图形处理能力,已经在 React Native 开始引入 WebGPU 支持,其效果将确保与 Web 端的 WebGPU API 完全一致,允许开发者直接复制代码示例的同时,实现与 Web Canvas API 对称的 RN Canvas API。
\\n\\n\\nWebGPU 为 React Native 开发者提供直接访问 GPU 底层能力的途径,从而开启高性能图形渲染的支持,实际上 Android 前段时间在统一 Vlukan 路线也提到过,未来 Java/Kotlin 开发者也许可以通过 Java/Kotlin 下利用 WebGPU 来“直接”体验 Vulkan 场景 ,这个在路径上相似。
\\n
WebGPU 作为全新的图形和计算 API,未来将会与 React Native 平台紧密集成,特别是与 Reanimated 的集成,进而实现可以在 UI 线程运行示例,并使用手势处理器进行交互等支持。
\\n此外,由 Software Mansion 提出的 TypeGPU,作为一个类型安全的 WebGPU 支撑,目前是为了解决 WebGPU API 在某些方面(如 uniform byte alignment 和 padding)过于底层的问题,核心是提供一套类型安全的基元,桥接 TypeScript 和 WebGPU,从而允许在 JS 端创建类型化缓冲区并生成匹配的着色器代码,甚至允许 JS 函数在 GPU 上运行。
\\n而针对在需要绘制大量对象(如数千个立方体)的场景下,WebGPU 模式下允许创建场景的“录制”,动画时只需设置 uniform buffers 并通过单次绘制调用渲染场景,从而极大降低 FFI 成本。
\\n此外,WebGPU 还引入了 Compute Shaders ,允许在设备上运行通用 GPU 计算,对应案例有:
\\n最后,WebGPU 的引入还可以让 React Native 开发者能够利用 ThreeJS 生态,直接引入已有的 3D 库,这让 React Native 的能力进一步对齐了 Web :
\\n同时还有 React Three Fiber ,例如 SpaceX 的 Aaron Grider 将 Three Fiber 用于 Starlink 应用,并进行了将其 GLSL 代码迁移到 TSL(TypeScript Shading Language) 从而在 React Native WebGPU 上运行的实验:
\\n\\n\\nWebGPU 无疑给 React Native 打开了新的大门。
\\n
在很久之前 React Native 就开始尝试在其态系统内引入 Skia ,初始化的目的是弥补 React Native 中图形 API 的重要空白,同时解决 React Native 在处理复杂图形和高性能动画方面存的局限性。
\\n\\n\\nReact Native Skia 并非简单地将 Skia 封装,它利用了 JSI (JavaScript Interface) 实现了 JS 与 Skia C++ 对象之间的直接和快速通信,同时 React Native Skia 与 Software Mansion 开发的 Reanimated 相互配合,让动画逻辑能够直接在 UI 线程上运行,保证了 Skia 动画的平滑和高性能渲染。
\\n
而现在在 React Native 社区,已经有大量使用 React Native Skia 构建的优秀应用,如 Homsphere, Callie, Daze, Azzapp, Phomo, Bindshoot, Prado, Gecko Med, Runna, B42 等等。
\\n另外,本次提及 Skia 并不是做出了什么重大更新,而是针对稳定性做了重大优化,事实上 React Native 发布到现在就经历了不少调整,比如:
\\n\\n\\n早期的
\\nCanvasView
组件通过 JS 回调在主线程进行绘制,当主线程存在阻塞逻辑时,容易导致掉帧和卡顿;而为了解决主线程阻塞问题,后面引入了图像离屏画布 (Image Offscreen Canvas) 的方案,从而支持在单独的线程中进行渲染,然而这种方法需要为每一帧创建新的画布并通过线程传递图像数据,导致了较高的内存消耗 ;之后又利用SkPicture
可以记录和重放绘制指令,从而在线程间共享渲染结果,进而解决了高内存占用的问题。
而这一次 Skia 主要也是针对性能进行了大幅优化,例如:
\\n性能大幅提升:
\\n而最终的目标,是想通过偿还技术债务,让 React Native Skia 能在三个新平台上可用(macOS, tvOS, 和 Node.js),并为未来迁移到 Skia Graphite(使用 WebGPU 的新 Skia 后端)奠定基础。
\\n\\n\\nGraphite 的核心就是优化运行时着色器编译卡顿问题,虽然还是需要运行时生成着色器,但是它支持应用枚举将要使用的图形特性,从而能够在应用启动时甚至提前 (AOT) 预编译所有必需的着色器 ,目前还是实验性阶段。
\\n
另外,针对 Skia 在 React Native 实现了统一的后端模型,解决了之前在 Android (OpenGL) 和 iOS (Metal) 上使用完全不同的后端,导致新功能需要开发两次,代码混乱的问题,同时也即将到来的 WebGPU 迁移提供了前置条件。
\\n在实际场景中,社区的 React Native Skia Video 模块,实现了原生纹理(iOS Metal, Android OpenGL)到 React Native Skia 的直接传输,优化了内存和渲染速度,可以被用于视频帧提取、集成和导出等,生态中还有 React Native Vision Camera 和 React Native Video (v7) 等支持 Skia 的模块。
\\n最后,React Native Skia 还有 Skia DOM 的概念,允许开发者使用熟悉的 React 组件语法来声明式地创建和渲染 Skia 图形元素:
\\n而本次通过将原有的 SkiaDOM 从完全可变的 Paper reconciler 迁移到不可变的 Fabric reconciler ,实现动画时无需处理并发,从而让 Android 动画性能提升近 200%,iOS 和 Android 首次动画帧时间提升近 200%。
\\n最后,在 Skia + WebGPU 的场景下,最终还能实现 2D+3D 的混合模式:
\\n事实上这一步其实也是 Skia 的方向之一,采用 WebGPU 作为多个平台上的统一图形后端,利用 WebGPU (通过其 C++ 实现 Dawn) 将充当一个关键的抽象层,让 Skia Graphite 能够以统一的方式与底层的 Vulkan 或 Metal API 进行交互 ,这也是 Graphite-WebGPU 的方向。
\\n而从这个角度看,Skia 和 WebGPU 无疑很好补充了 React Native 以前在底层能力和图形处理的弱项,当然,这也是一场艰难的持久战,所以如果可以借助 ThreeJS 社区的生态,对于 React Native 来说,既能对其 Web 生态,还能提供丰富案例,无疑是最佳的选择。
\\n那么,你用过 React Native Skia 么?对于 WebGPU 客户端路径你怎么看?
","description":"近日,React Native 发布了前瞻式的重大更新,主要围绕 Skia & WebGPU 等场景来布局未来的跨平台渲染场景,主要目的是在 “追求与 Web 的对称性”的同时,提供更强大的客户端渲染支持。 WebGPU\\n\\nReact Native 本次提到,为了对齐 Web 能力并提供强大的图形处理能力,已经在 React Native 开始引入 WebGPU 支持,其效果将确保与 Web 端的 WebGPU API 完全一致,允许开发者直接复制代码示例的同时,实现与 Web Canvas API 对称的 RN Canvas API。\\n\\nWebGPU…","guid":"https://juejin.cn/post/7501989765085298700","author":"恋猫de小郭","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-09T03:15:24.443Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/57a1fd3d26f14ba9a7524560921c546b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747365324&x-signature=IV%2BTvA%2BS4g98lJJkBWZ0GiB5TDk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/88f36d5248f84e92b6451f8c117200ee~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747365324&x-signature=oTUAoFecc%2BzbLTdeido4OXG1UIk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7d1f138972194d8ba26b081b953abe7a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747365324&x-signature=hrwe3i3crsJT6LmYUusQC9t5XP4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1bb4447eb8104f32ac4ecf947e794363~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747365324&x-signature=LI9%2FevUFPDmdQkErrHuc1CE2xac%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/55b4e70dd2f5454bba56cc45bf01eb71~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747365324&x-signature=LfPIIalrDX%2Fecj9iiB%2BaNfG2qk8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7c6fc44d4685456990767d3aa8a64aa9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747365324&x-signature=hoDE3kqffER4ELWgawSH2xs2VnE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/27580c2a109f4d15a1e615099c0e65b8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747365324&x-signature=Sa4Uh0Md5qwjZyuiOO13bscuAVQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0cdeaf1068224e4daa9c8dc325fa1087~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747365324&x-signature=nuqQMoH2QDxZ%2B2zD8wpLmN4C7Ig%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/692806a6a48f408bbc2f8a3866f62912~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747365324&x-signature=j4wWHaxbDdxmVVMlPmPm9Ti5sAc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fed688c27c064d72b31522f614f49e74~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747365324&x-signature=Ox4Hy0oGH%2FL3bB0fEXaBXr%2F5ZaU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0f3484bbeb6f40c6a544cf315dabed43~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747365324&x-signature=TOwRzW2XV66pxAXP9nP9uVkalWY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/14b9945d0971451597cb8a0a21539fd5~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747365324&x-signature=RO8Gq%2FOW52YzPtSS55BSGWlgTj4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/693db94fa8114c11b4e2d56485e0a634~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747365324&x-signature=%2FvF1jPer8p1v3OK0YfbAOL6sTW8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2cabdf47014847e2a51d577dd5e334e3~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747365324&x-signature=jwSFI7tGyh7tyyc6QC8F7spWZ20%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/85a69b2348b0434fa42664a7f5a36f19~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747365324&x-signature=VRhru3MZSBiFPb8GrETumECrwqg%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"【Flutter 状态管理 - 玖】 | GetX之依赖注入:颠覆认知的\\"即插即用\\"哲学","url":"https://juejin.cn/post/7501969167193833510","content":"你是否曾被层叠嵌套的组件间数据传递逼到抓狂?🤯 当业务逻辑与 UI
深度耦合,代码的可维护性便如同沙堡般脆弱。依赖注入(Dependency Injection, DI
)这一设计模式,恰似一剂解耦良药💊,而 GetX
以其极简的语法将 DI
的威力推向极致。
本文将撕开依赖注入的神秘面纱,直击 GetX
实现背后的精妙设计。代码耦合的噩梦,是时候终结了!
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\nDI
的本质\\n\\n依赖指的是一个对象(
\\nA
)需要另一个对象(B
)才能完成其功能
举个栗子,用户登录后想要保存用户的信息:
\\nclass UserService {\\n // UserService 依赖 Database 才能工作\\n final Database database;\\n UserService(this.database);\\n\\n void saveUser(User user) {\\n database.save(user);\\n }\\n}\\n
\\nUserService
必须通过 Database
对象才能执行“保存用户”操作。
对象通常会自己创建它依赖的实例。
\\nclass UserService {\\n final Database database = Database(); // 直接创建依赖对象\\n\\n void saveUser(User user) {\\n database.save(user);\\n }\\n}\\n
\\n手动创建一个对象看似简单,却埋下隐患:
\\nUserService
直接依赖 Database
的具体实现,如果未来想更换数据库(如从 SQLite
换为 Firebase
),必须修改 UserService
的代码。Mock
)替代真实数据库。DI
的核心思想依赖注入通过以下方式解决上述问题:
\\n1️⃣ 控制反转(Inversion of Control, IoC
):对象的依赖不再由其内部创建,而是外部传递给它。
2️⃣ 解耦:将对象的创建和使用分离,对象只需关注自身逻辑,无需关心依赖的构造。
\\n换言之,组件只需声明\\"我需要什么\\"
,而由外部容器负责\\"我给你什么\\"
。这如同餐厅点餐🍽️:顾客(组件)无需关心菜品(依赖)如何烹饪(创建),厨房(容器)会按需送达。
class UserService {\\n final Database database;\\n\\n // 方式1:依赖通过构造函数传入(注入)\\n UserService(this.database); \\n \\n // 方式2:方法注入\\n void setDatabase(Database db) {\\n database = db;\\n }\\n\\n void saveUser(User user) {\\n database.save(user);\\n }\\n}\\n\\n// 使用\\nfinal database = Database();\\n// 方式1\\nfinal userService = UserService(database); // 依赖被注入\\n// 方式2\\nuserService.setDatabase(Database());\\n
\\n// 依赖注入接口\\nabstract class DatabaseInjector {\\n void injectDatabase(Database database);\\n}\\n\\nclass UserService implements DatabaseInjector {\\n Database _database;\\n\\n @override\\n void injectDatabase(Database database) {\\n _database = database;\\n }\\n\\n void saveUser(User user) {\\n _database.save(user);\\n }\\n}\\n\\n// 使用\\nfinal userService = UserService();\\nuserService.injectDatabase(Database());\\n
\\nIoC
):从「奴隶」到「主人」的蜕变传统模式下,组件被迫自行创建依赖,如同厨师👩🍳既要炒菜又要种菜。而 IoC
将控制权移交容器,组件仅需声明需求,彻底摆脱初始化细节。
技术实现穿透:
\\n// 传统强耦合模式 \\nclass CartPage extends StatelessWidget { \\n final _service = ShoppingCartService(); // ❌ 组件掌控依赖创建 \\n} \\n\\n// IoC 模式 \\nclass CartPage extends StatelessWidget { \\n final ShoppingCartService service; \\n CartPage({required this.service}); // ✅ 依赖由外部注入 \\n} \\n
\\n这一转变,使得组件从依赖的「奴隶」变为「主人」,仅关注业务逻辑本身,不再受制于依赖的构造细节。
\\nMock
替换的终极自由没有 DI
的测试如同戴着镣铐跳舞💃。试想:当你的 PaymentService
直接调用真实支付接口,如何验证异常分支逻辑?
GetX + Mockito
实战:
// 定义 Mock 类 \\nclass MockPaymentService extends Mock implements PaymentService {} \\n\\nvoid main() { \\n test(\'支付失败时应显示错误提示\', () async { \\n // 注入 Mock 实例 \\n final mockService = MockPaymentService(); \\n Get.put<PaymentService>(mockService); \\n\\n // 设置模拟行为 \\n when(mockService.pay(any)).thenThrow(NetworkException()); \\n\\n // 执行测试 \\n await tester.pumpWidget(MyApp()); \\n expect(find.text(\'支付失败\'), findsOneWidget); // ✅ 安全验证异常流 \\n }); \\n} \\n
\\n依赖注入允许在测试中无缝替换实现,甚至可模拟网络延迟、数据库崩溃等极端场景,让单元测试真正成为质量护城河🛡️。
\\n手动管理依赖的生命周期,如同用竹篮打水——看似简单,实则漏洞百出。GetX
容器通过智能回收机制,精准控制依赖的存活范围:
典型场景对比:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n场景 | 无 DI 的隐患 | GetX 方案 |
---|---|---|
页面级状态 | 需手动调用 dispose ,易遗漏导致内存泄漏 🚨 | 绑定 GetxController 自动释放 🧹 |
全局单例 | 静态变量难以重置,影响测试结果 📉 | Get.put(service, permanent: true) 🌍 |
懒加载 | 过早初始化拖慢启动速度 🐌 | Get.lazyPut(() => HeavyService()) ⚡ |
代码示范:
\\n// 绑定到路由的生命周期 \\nGet.to( \\n UserProfile(), \\n binding: BindingsBuilder(() { \\n Get.lazyPut(() => ProfileController()); // 随页面创建 \\n }), \\n); \\n\\n// 页面销毁时自动触发 \\nclass ProfileController extends GetxController { \\n @override \\n void onClose() { \\n cleanup(); // 释放资源 \\n super.onClose(); \\n } \\n} \\n
\\n当构造函数清晰地声明依赖,代码本身就成为最精准的文档📄。开发者无需逐行阅读逻辑,仅通过参数列表即可洞悉组件的协作网络。
\\nBad vs Good
:
// 模糊的依赖来源 ❌ \\nclass OrderService { \\n void submit() { \\n final storage = Database(); // 隐藏的强耦合 \\n final logger = Logger.instance; // 静态访问的陷阱 \\n } \\n} \\n\\n// 显式依赖声明 ✅ \\nclass OrderService { \\n final Database storage; \\n final Logger logger; \\n\\n OrderService(this.storage, this.logger); // 依赖关系一目了然 \\n} \\n
\\n通过 GetX
的 Bindings
类,这种声明可进一步升级为可视化依赖图谱:
class AppBindings implements Bindings { \\n @override \\n void dependencies() { \\n Get.put(NetworkConfig()); // 网络配置 \\n Get.lazyPut(() => AuthRepo(Get.find())); // 认证模块 \\n Get.lazyPut(() => OrderRepo(Get.find(), Get.find())); // 订单模块 \\n } \\n} \\n
\\n此类代码不仅描述依赖关系,更揭示了系统的模块化架构层次,新人接手时几乎无需额外文档📚。
\\n\\n▍依赖注册表 📋
\\n容器本质上是一张中心化注册表,记录着所有可用的服务及其获取规则。与传统硬编码依赖不同,它采用键值对形式存储:
Key
):通常为类型(Type
)或类型+命名标识。Value
):依赖实例或实例工厂。这种设计使得依赖关系从散落各处的 new
操作符调用,升级为统一注册的目录系统。如同电话簿📞,需要时按名查找,而非临时创造。
▍依赖解析器 🔍
\\n当组件请求依赖时,解析器执行精准匹配:
A
依赖 B
,B
依赖 C
)。这一过程暗含依赖图构建,容器自动解决复杂的依赖链条,避免手动初始化的层层嵌套。
\\n▍生命周期管理器 ⏳
\\n容器掌控依赖的生死周期:
通过策略控制,避免内存泄漏与资源浪费。例如,数据库连接通常设为单例,而 HTTP
请求上下文则适合作用域模式。
GetX
的容器设计GetX
依赖注入的核心在于其全局容器与极简API
设计的巧妙结合。
▍容器数据结构\\nGetX
使用 Map
结构存储实例工厂方法,通过「类型+标签」作为唯一键:
class GetInstance { \\n static final Map<String, _InstanceBuilderFactory> _singl = {};\\n} \\n
\\nkey = Type.toString() + tag
(标签可选)▍依赖解析流程
\\n当调用 Get.find<T>()
时,检查 _singl
是否存在匹配实例。如果依赖项未注册或无法找到,则会抛出相应的异常,提示用户如何正确注册依赖项。这种设计通常用于支持依赖注入框架中的服务定位器模式。
S find<S>({String? tag}) {\\n final key = _getKey(S, tag);\\n if (isRegistered<S>(tag: tag)) {\\n final dep = _singl[key];\\n if (dep == null) {\\n if (tag == null) {\\n throw \'Class \\"$S\\" is not registered\';\\n } else {\\n throw \'Class \\"$S\\" with tag \\"$tag\\" is not registered\';\\n }\\n }\\n final i = _initDependencies<S>(name: tag);\\n return i ?? dep.getDependency() as S;\\n } else {\\n throw \'\\"$S\\" not found. You need to call \\"Get.put($S())\\" or \\"Get.lazyPut(()=>$S())\\"\';\\n }\\n}\\n\\nbool isRegistered<S>({String? tag}) => _singl.containsKey(_getKey(S, tag));\\n
\\n▍生命周期管理:绑定路由的自动回收
\\nGetX
独创性地将依赖生命周期与路由栈绑定:
GetNavigatorObserver
监听页面切换。onClose()
。// 路由跳转时绑定依赖 \\nGet.to( \\n HomePage(), \\n binding: BindingsBuilder(() { \\n Get.lazyPut(() => HomeController()); // 依赖绑定到 HomePage 路由 \\n }), \\n); \\n\\n// 路由退出时触发 \\nvoid onCloseRoute(Route route) { \\n final dependencies = _getDependenciesForRoute(route); \\n dependencies.forEach((dep) => dep.onClose()); // 执行回收逻辑 \\n _removeFromSingletonPool(dependencies); // 从容器移除 \\n} \\n
\\n▍极简 API
设计
\\nGetX
通过两个核心方法颠覆传统 DI
的复杂性:
// 注册依赖:将 NetworkService 实例放入容器 \\nGet.put(NetworkService()); \\n\\n// 获取依赖:从容器中提取 NetworkService 实例 \\nfinal service = Get.find<NetworkService>(); \\n
\\n这种设计将 DI
的学习曲线压至极低,新手也能快速上手。
▍特性深度剖析
\\n① Lazy Loading
:按需加载的性能利器
\\n通过 Get.lazyPut
延迟实例化,避免应用启动时的资源挤兑:
Get.lazyPut(() => HeavyService()); // 仅当首次调用 Get.find 时初始化 \\n\\n// 实际调用时才触发构造函数 \\nfinal heavy = Get.find<HeavyService>(); \\n
\\n特别适合初始化成本高的服务(如机器学习模型加载),可显著缩短冷启动时间⏱️。
\\n② 命名实例:同类型的多重宇宙
\\nGetX
允许为同一类型注册多个命名实例,解决「一个接口多个实现」的经典难题:
// 注册两个不同配置的 Logger \\nGet.put(FileLogger(), tag: \'file\'); \\nGet.put(CloudLogger(), tag: \'cloud\'); \\n\\n// 按需获取 \\nfinal fileLog = Get.find<Logger>(tag: \'file\'); \\nfinal cloudLog = Get.find<Logger>(tag: \'cloud\'); \\n
\\n此特性在处理多环境配置(开发/生产)、A/B
测试等场景时尤其实用。
③ 智能回收:内存泄漏终结者
\\n结合 GetxController
的生命周期钩子,实现依赖的自动回收:
class DetailController extends GetxController { \\n final ProductRepo repo; \\n DetailController(this.repo); \\n\\n @override \\n void onClose() { \\n repo.cancelPendingRequests(); // 释放资源 \\n super.onClose(); \\n } \\n} \\n\\n// 绑定到路由 \\nGet.to( \\n ProductDetailPage(), \\n binding: BindingsBuilder(() { \\n Get.put(DetailController(Get.find())); \\n }), \\n); \\n\\n// 页面关闭时自动触发 onClose \\n
\\n当页面销毁时,与其关联的 Controller
及非全局依赖会被自动回收♻️,无需手动调用 dispose
。
▍进阶控制技巧
\\n // 强制重置依赖\\n Get.reset(); // 核弹级清理:清空所有非永久实例 \\n Get.delete<ConfigService>(); // 定点清除特定依赖 \\n // 依赖存在性检查\\n if (Get.isRegistered<Logger>()) { \\n Get.find<Logger>().log(\'System ready\'); \\n } \\n // 异步初始化\\n Get.putAsync<SharedPreferences>(() async { \\n final prefs = await SharedPreferences.getInstance(); \\n return prefs; \\n }); \\n
\\nGetX vs
传统 DI
容器特性 | 传统 DI (如 Provider ) | GetX |
---|---|---|
初始化复杂度 | 需包裹多层 Provider | 一行 put 全局可用 |
作用域管理 | 依赖上下文层级传递 | 自动绑定到路由生命周期 |
学习成本 | 需理解 BuildContext 机制 | 无上下文依赖,直接静态调用 |
代码侵入性 | 需改造组件为 Consumer | 原生 Dart 类无需任何改造 |
GetX
通过去中心化的设计,让依赖注入变得如呼吸般自然。开发者不再被繁琐的配置绑架,而是聚焦于业务逻辑本身——这或许正是 Flutter
状态管理的终极形态。 🚀
场景 1:网络层与数据层解耦
\\n// 数据层\\nclass UserRepo {\\n final NetworkClient client; // 声明网络依赖\\n UserRepo(this.client);\\n}\\n\\n// 注册\\nGet.put(NetworkClient());\\nGet.put(UserRepo(Get.find())); \\n\\n// 使用\\nfinal repo = Get.find<UserRepo>();\\n
\\n场景 2:路由参数传递
\\nGet.to(ProfilePage(), \\n binding: BindingsBuilder(() {\\n Get.put(UserModel(Get.arguments)); // 依赖与路由绑定\\n }),\\n);\\n
\\n依赖注入绝非银弹🔫,但 GetX
的轻量化实现使其成为 Flutter
开发中的瑞士军刀🔧。通过将对象的创建权收归容器,代码获得了前所未有的灵活度与可维护性。当你的组件不再深陷依赖泥潭,那种如释重负的畅快感,或许正是工程师追求的艺术之美。🦾 现在,是时候用 GetX
的 DI
重写那些\\"祖传代码\\"了!
\\n","description":"前言 你是否曾被层叠嵌套的组件间数据传递逼到抓狂?🤯 当业务逻辑与 UI 深度耦合,代码的可维护性便如同沙堡般脆弱。依赖注入(Dependency Injection, DI)这一设计模式,恰似一剂解耦良药💊,而 GetX 以其极简的语法将 DI 的威力推向极致。\\n\\n本文将撕开依赖注入的神秘面纱,直击 GetX 实现背后的精妙设计。代码耦合的噩梦,是时候终结了!\\n\\n操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。\\n\\nDI的本质\\n何为“依赖”?\\n\\n依赖指的是一个对象(A)需要另一个对象(B)才能完成其功能\\n\\n举个栗子…","guid":"https://juejin.cn/post/7501969167193833510","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-09T02:08:19.828Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3a307ee5195342d582a5590c6b4a67dc~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1747361485&x-signature=E%2FYU1L70quFg%2BDPo0bXFGNzKXPA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/06f000a9b7a64667a59039818d71ada8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1747361485&x-signature=AaLoaEB314EqwUdeKhr92e1PoUU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e73a81dc18154d91b09eb47d58fef2d2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1747361485&x-signature=qo7Q2I3lo7%2FRCWhX0ATcPbxVyDY%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"Flutter GetX 硬核分享:深度链接的轻量级实现方案","url":"https://juejin.cn/post/7501956466392727579","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
通过GetX控制器直接管理Deeplink状态,利用平台原生回调触发路由跳转。
\\n// lib/routes/app_pages.dart\\nabstract class AppPages {\\n static final routes = [\\n GetPage(\\n name: \'/product/:id\',\\n page: () => ProductDetailView(),\\n binding: ProductBinding(),\\n ),\\n GetPage(\\n name: \'/profile\',\\n page: () => ProfileView(),\\n ),\\n ];\\n}\\n
\\n// lib/controllers/deeplink_controller.dart\\nclass DeeplinkController extends GetxController {\\n static DeeplinkController get to => Get.find();\\n\\n Future<void> handleDeeplink(String? url) async {\\n if (url == null) return;\\n \\n final uri = Uri.parse(url);\\n final route = _convertUriToRoute(uri);\\n \\n if (route != null) {\\n Get.toNamed(route.path, parameters: route.params);\\n }\\n }\\n\\n DeeplinkRoute? _convertUriToRoute(Uri uri) {\\n final pathSegments = uri.pathSegments;\\n if (pathSegments.isEmpty) return null;\\n \\n return DeeplinkRoute(\\n path: \'/${pathSegments.join(\'/\')}\',\\n params: uri.queryParameters,\\n );\\n }\\n}\\n\\nclass DeeplinkRoute {\\n final String path;\\n final Map<String, String> params;\\n\\n DeeplinkRoute({required this.path, required this.params});\\n}\\n
\\n// lib/services/native_channel.dart\\nclass DeeplinkChannel {\\n static const _channel = MethodChannel(\'deeplink_channel\');\\n\\n static void init() {\\n _channel.setMethodCallHandler((call) async {\\n if (call.method == \'handleDeepLink\') {\\n DeeplinkController.to.handleDeeplink(call.arguments);\\n }\\n });\\n }\\n\\n static Future<void> getInitialLink() async {\\n try {\\n final link = await _channel.invokeMethod<String>(\'getInitialLink\');\\n DeeplinkController.to.handleDeeplink(link);\\n } catch (e) {\\n print(\'Error getting initial link: $e\');\\n }\\n }\\n}\\n
\\n// main.dart\\nvoid main() async {\\n WidgetsFlutterBinding.ensureInitialized();\\n \\n // 处理热启动链接\\n Get.put(DeeplinkController());\\n DeeplinkChannel.init();\\n \\n runApp(GetMaterialApp(\\n initialRoute: \'/\',\\n getPages: AppPages.routes,\\n ));\\n\\n // 处理冷启动链接\\n Future.delayed(Duration.zero, () => DeeplinkChannel.getInitialLink());\\n}\\n
\\n// MainActivity.kt\\nclass MainActivity : FlutterActivity() {\\n private var initialLink: String? = null\\n \\n override fun onCreate(savedInstanceState: Bundle?) {\\n super.onCreate(savedInstanceState)\\n handleIntent(intent)\\n setupChannel()\\n }\\n\\n override fun onNewIntent(intent: Intent) {\\n super.onNewIntent(intent)\\n handleIntent(intent)\\n }\\n\\n private fun handleIntent(intent: Intent?) {\\n intent?.data?.let { uri ->\\n initialLink = uri.toString()\\n // 直接触发Dart端处理\\n MethodChannel(flutterEngine!!.dartExecutor, \\"deeplink_channel\\")\\n .invokeMethod(\\"handleDeepLink\\", uri.toString())\\n }\\n }\\n\\n private fun setupChannel() {\\n MethodChannel(flutterEngine!!.dartExecutor, \\"deeplink_channel\\").apply {\\n setMethodCallHandler { call, result ->\\n when (call.method) {\\n \\"getInitialLink\\" -> result.success(initialLink)\\n else -> result.notImplemented()\\n }\\n }\\n }\\n }\\n}\\n
\\n// AppDelegate.swift\\n@UIApplicationMain\\nclass AppDelegate: FlutterAppDelegate {\\n var initialLink: String?\\n \\n override func application(\\n _ application: UIApplication,\\n didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?\\n ) -> Bool {\\n let controller = window?.rootViewController as! FlutterViewController\\n let channel = FlutterMethodChannel(name: \\"deeplink_channel\\", binaryMessenger: controller.binaryMessenger)\\n \\n channel.setMethodCallHandler { [weak self] (call, result) in\\n switch call.method {\\n case \\"getInitialLink\\":\\n result(self?.initialLink)\\n default:\\n result(FlutterMethodNotImplemented)\\n }\\n }\\n \\n return super.application(application, didFinishLaunchingWithOptions: launchOptions)\\n }\\n\\n override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {\\n let link = url.absoluteString\\n initialLink = link\\n \\n let channel = FlutterMethodChannel(\\n name: \\"deeplink_channel\\",\\n binaryMessenger: window!.rootViewController as! FlutterBinaryMessenger\\n )\\n channel.invokeMethod(\\"handleDeepLink\\", arguments: link)\\n \\n return true\\n }\\n}\\n
\\nDeeplink示例:yourapp://product/12345?source=wechat
// 在DeeplinkController中添加处理逻辑\\nvoid _convertUriToRoute(Uri uri) {\\n if (uri.pathSegments.first == \'product\') {\\n return DeeplinkRoute(\\n path: \'/product/${uri.pathSegments[1]}\',\\n params: {\'source\': uri.queryParameters[\'source\'] ?? \'default\'}\\n );\\n }\\n}\\n\\n// 商品页获取参数\\nclass ProductDetailView extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n final productId = Get.parameters[\'id\']!;\\n final source = Get.parameters[\'source\'];\\n return Scaffold(\\n appBar: AppBar(title: Text(\'商品 $productId\')),\\n body: Text(\'来源渠道: $source\')\\n );\\n }\\n}\\n
\\nDeeplink示例:yourapp://profile?require_auth=true
// 添加路由中间件\\nGetPage(\\n name: \'/profile\',\\n page: () => ProfileView(),\\n middlewares: [AuthMiddleware()],\\n);\\n\\n// 认证中间件\\nclass AuthMiddleware extends GetMiddleware {\\n @override\\n RouteSettings? redirect(String? route) {\\n final requireAuth = Get.parameters[\'require_auth\'] == \'true\';\\n return requireAuth && !AuthService.isLoggedIn \\n ? RouteSettings(name: \'/login\')\\n : null;\\n }\\n}\\n
\\nDeeplink示例:yourapp://campaign/summer_sale
// 路由配置添加动态参数\\nGetPage(\\n name: \'/campaign/:campaignId\',\\n page: () => CampaignView(),\\n binding: CampaignBinding(),\\n);\\n\\n// 绑定类处理业务逻辑\\nclass CampaignBinding extends Bindings {\\n @override\\n void dependencies() {\\n final campaignId = Get.parameters[\'campaignId\']!;\\n Get.lazyPut(() => CampaignController(campaignId));\\n }\\n}\\n\\n// 控制器校验时效\\nclass CampaignController extends GetxController {\\n final String campaignId;\\n \\n CampaignController(this.campaignId) {\\n if (!_validateCampaign()) {\\n Get.offNamed(\'/campaign/expired\');\\n }\\n }\\n \\n bool _validateCampaign() {\\n return CampaignService.isActive(campaignId);\\n }\\n}\\n
\\nDeeplink示例:yourapp://settings/notification/preferences
// 嵌套路由配置\\nGetPage(\\n name: \'/settings\',\\n page: () => SettingsView(),\\n children: [\\n GetPage(name: \'/notification\', page: () => NotificationView()),\\n GetPage(name: \'/preferences\', page: () => PreferencesView()),\\n ],\\n);\\n\\n// 处理深度链接\\nDeeplinkRoute _convertUriToRoute(Uri uri) {\\n if (uri.pathSegments.length >= 2 \\n && uri.pathSegments[0] == \'settings\') {\\n return DeeplinkRoute(\\n path: \'/settings/${uri.pathSegments[1]}\',\\n params: uri.queryParameters\\n );\\n }\\n}\\n
\\nDeeplink示例:yourapp://message/123?from=push
// 消息模块初始化\\nclass MessageModule extends Module {\\n @override\\n void routes(RouteManager r) {\\n r.child(\\n \'/message/:id\',\\n child: (params) => MessageDetailView(id: params[\'id\']!),\\n transition: Transition.downToUp,\\n );\\n }\\n}\\n\\n// 全局路由处理\\nvoid handleDeeplink(String url) {\\n if (url.startsWith(\'message\')) {\\n GetModuleRouter().routeMessageModule(url);\\n }\\n}\\n
","description":"实现原理 通过GetX控制器直接管理Deeplink状态,利用平台原生回调触发路由跳转。\\n\\n核心实现\\n1. 路由配置\\n// lib/routes/app_pages.dart\\nabstract class AppPages {\\n static final routes = [\\n GetPage(\\n name: \'/product/:id\',\\n page: () => ProductDetailView(),\\n binding: ProductBinding(),\\n ),\\n GetPage…","guid":"https://juejin.cn/post/7501956466392727579","author":"一名普通的程序员","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-09T01:29:15.258Z","media":null,"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"[Flutter 基础] - Tab 组件","url":"https://juejin.cn/post/7501878297900679207","content":"在 Flutter 中,Tab(标签页) 是实现多页面切换的常见方式,通常与 AppBar
、TabBar
和 TabBarView
结合使用。Tab的提供了丰富的属性,通过灵活使用这些属性基本上可以满足我们日常开发中的大部分需求。\\n以下是关于 Tab 的详细用法,包括基础配置、复杂用法和 底部Tab的实现。
DefaultTabController
: 管理 Tab 的状态和切换(适用于简单场景)。TabBar
: 显示一组水平排列的标签(通常放在 AppBar
的 bottom
属性中)。TabBarView
: 显示与 TabBar 对应的内容区域。Tab
: 单个标签项,可以自定义图标和文字。顶部展示一个tab分页,这种场景常见于订单或者是新闻等分类业务。
\\nimport \'package:flutter/material.dart\';\\n\\nclass TabDemo extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return DefaultTabController(\\n length: 3, // Tab 的数量\\n child: Scaffold(\\n appBar: AppBar(\\n title: Text(\'Tab 示例\'),\\n bottom: TabBar(\\n tabs: [\\n Tab(text: \'首页\', icon: Icon(Icons.home)),\\n Tab(text: \'消息\', icon: Icon(Icons.message)),\\n Tab(text: \'设置\', icon: Icon(Icons.settings)),\\n ],\\n ),\\n ),\\n body: TabBarView(\\n children: [\\n Center(child: Text(\'首页内容\')),\\n Center(child: Text(\'消息内容\')),\\n Center(child: Text(\'设置内容\')),\\n ],\\n ),\\n ),\\n );\\n }\\n}\\n
\\nTabBar(\\n tabs: [...],\\n indicatorColor: Colors.red, // 指示器颜色\\n indicatorWeight: 4, // 指示器厚度\\n indicatorSize: TabBarIndicatorSize.label, // 指示器长度(label: 与文本同宽)\\n labelColor: Colors.black, // 选中标签颜色\\n unselectedLabelColor: Colors.grey, // 未选中标签颜色\\n labelStyle: TextStyle(fontWeight: FontWeight.bold), // 标签文本样式\\n)\\n
\\n支持任意 Widget(不仅仅是文字和图标),所以可以通过这个child自定义自己想要的tab样式:
\\nbottom: TabBar(\\n tabs: [\\n Tab(\\n child: Row(\\n children: [\\n Icon(Icons.check),\\n SizedBox(width: 4),\\n Text(\'全部\'),\\n ],\\n ),\\n ),\\n Tab(text: \'未完成\'),\\n Tab(text: \'进行中\'),\\n Tab(text: \'已完成\'),\\n ],\\n indicatorColor: Colors.red, // 指示器颜色\\n indicatorWeight: 4, // 指示器厚度\\n indicatorSize: TabBarIndicatorSize.label, // 指示器长度(label: 与文本同宽, tab: 占整个tab的宽度)\\n labelColor: Colors.black, // 选中标签颜色\\n unselectedLabelColor: Colors.grey, // 未选中标签颜色\\n labelStyle: TextStyle(fontWeight: FontWeight.bold), // 标签文本样式\\n),\\n
\\nTabbar的使用场景非常丰富,经常伴随复杂的业务场景需要灵活的通过代码去控制tab的选中状态,比如通过某个订单的状态索引到不同的tab下面,这个时候我们就可以通过TabController来达到这个效果。
\\n点击查看已完成订单
会自动跳转至已完成
页面
class _TabDemoState extends State<TabDemo> with SingleTickerProviderStateMixin {\\n late TabController _tabController;\\n\\n @override\\n void initState() {\\n super.initState();\\n _tabController = TabController(length: 4, vsync: this);\\n _tabController.addListener(() {\\n print(\'当前 Tab 索引: ${_tabController.index}\');\\n });\\n }\\n\\n void setSelectedTab(int index) {\\n _tabController.animateTo(index); //跳转至对应下表的页面\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: Text(\'手动控制 Tab\'),\\n bottom: TabBar(\\n controller: _tabController,\\n tabs: [\\n Tab(text: \'全部\'),\\n Tab(text: \'未完成\'),\\n Tab(text: \'进行中\'),\\n Tab(text: \'已完成\'),\\n ],\\n indicatorColor: Colors.red,\\n // 指示器颜色\\n indicatorWeight: 4,\\n // 指示器厚度\\n indicatorSize: TabBarIndicatorSize.tab,\\n // 指示器长度(label: 与文本同宽)\\n labelColor: Colors.black,\\n // 选中标签颜色\\n unselectedLabelColor: Colors.grey,\\n // 未选中标签颜色\\n labelStyle: TextStyle(fontWeight: FontWeight.bold), // 标签文本样式\\n ),\\n ),\\n body: TabBarView(\\n controller: _tabController,\\n children: [\\n Center(child: Text(\'全部订单\')),\\n Center(child: GestureDetector(\\n onTap: (){\\n setSelectedTab(3); //跳转至下标为3的页面\\n },\\n child: Text(\'查看已完成订单\'),\\n )),\\n Center(child: Text(\'进行中的订单\')),\\n Center(child: Text(\'已完成订单\')),\\n ],\\n ),\\n );\\n }\\n\\n @override\\n void dispose() {\\n _tabController.dispose();\\n super.dispose();\\n }\\n}\\n
\\n通过 TabBarView
的 physics
属性控制滑动行为:
TabBarView(\\n physics: NeverScrollableScrollPhysics(), // 禁止滑动切换,也可以通过继承ScrollPhysics来自定义动画\\n children: [...],\\n)\\n
\\n默认情况下,切换 Tab 时会重建页面。若需保持状态(如保留表单输入),有两种方式:
\\nAutomaticKeepAliveClientMixin
(推荐):\\nclass _HomePageState extends State<HomePage> with AutomaticKeepAliveClientMixin {\\n @override\\n bool get wantKeepAlive => true; // 保持页面状态\\n\\n @override\\n Widget build(BuildContext context) {\\n super.build(context);\\n return ...;\\n }\\n}\\n
\\nPageStorageKey
:\\nTabBarView(\\n children: [\\n HomePage(key: PageStorageKey(\'home\')),\\n SettingsPage(key: PageStorageKey(\'settings\')),\\n ],\\n)\\n
\\n在 Scaffold使用 BottomNavigationBar
实现我们常见的底部Tab:
List<Widget> tabs = [HomePage(), SearchPage(), SettingPage()]; //定义好一个包含三个页面的数组\\n
\\n bottomNavigationBar: BottomNavigationBar(\\n items: [\\n BottomNavigationBarItem(icon: Icon(Icons.home), label: \'Home\'),\\n BottomNavigationBarItem(icon: Icon(Icons.search), label: \'Search\'),\\n BottomNavigationBarItem(icon: Icon(Icons.message), label: \'Setting\'),\\n ],\\n ),\\n
\\ncurrentIndex: _currentIndex,\\nunselectedItemColor: Colors.grey,\\nselectedItemColor: Colors.amber[800],\\nonTap: _onItemTapped,\\ntype: BottomNavigationBarType.shifting,\\n
\\n由于dart基础库不支持,所以需要添加额外的依赖
\\ndependencies:\\n badges: ^3.0.0\\n
\\nBottomNavigationBarItem(\\n icon: Badge(label: Text(\'3\'), child: Icon(Icons.message)),\\n label: \'Setting\',\\n),\\n
\\nclass _MyHomePageState extends State<MyHomePage>\\n with SingleTickerProviderStateMixin {\\n late TabController _tabController;\\n int _currentIndex = 0; //记录当前页面的下标\\n List<Widget> tabs = [HomePage(), SearchPage(), SettingPage()]; //定义好一个包含三个页面的数组\\n\\n @override\\n void initState() {\\n super.initState();\\n _tabController = TabController(length: 3, vsync: this);\\n _tabController.addListener(_handleTabSelection);\\n }\\n\\n void _handleTabSelection() {\\n if (_tabController.indexIsChanging) {\\n setState(() {\\n // 可以在这里处理Tab切换事件\\n });\\n }\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n bottomNavigationBar: BottomNavigationBar(\\n items: [\\n BottomNavigationBarItem(icon: Icon(Icons.home), label: \'Home\'),\\n BottomNavigationBarItem(icon: Icon(Icons.search), label: \'Search\'),\\n BottomNavigationBarItem(icon: Badge(\\n label: Text(\'3\'),\\n child: Icon(Icons.message),\\n ), label: \'Setting\'),\\n ],\\n currentIndex: _currentIndex,\\n unselectedItemColor: Colors.grey,\\n selectedItemColor: Colors.amber[800],\\n onTap: _onItemTapped,\\n //底部按钮的样式,[fixed: 固定底部样式,shifting:未选中的时候,只展示icon,选中的时候展示完整的tab]\\n type: BottomNavigationBarType.shifting, \\n ),\\n body:\\n tabs[_currentIndex], //根据下标展示对应的页面\\n );\\n }\\n\\n void _onItemTapped(int index) {\\n setState(() {\\n _currentIndex = index;\\n });\\n print(_currentIndex);\\n }\\n\\n @override\\n void dispose() {\\n _tabController.dispose();\\n super.dispose();\\n }\\n}\\n
\\n通过灵活使用 Tab 组件,可以实现复杂的多页面布局和交互,不仅适用于新闻分类、电商商品详情等场景,底部tab也同样拥有丰富的属性来完成样式的定制。
","description":"在 Flutter 中,Tab(标签页) 是实现多页面切换的常见方式,通常与 AppBar、TabBar 和 TabBarView 结合使用。Tab的提供了丰富的属性,通过灵活使用这些属性基本上可以满足我们日常开发中的大部分需求。 以下是关于 Tab 的详细用法,包括基础配置、复杂用法和 底部Tab的实现。 一、基础用法\\n1. 核心组件\\nDefaultTabController: 管理 Tab 的状态和切换(适用于简单场景)。\\nTabBar: 显示一组水平排列的标签(通常放在 AppBar 的 bottom 属性中)。\\nTabBarView: 显示与…","guid":"https://juejin.cn/post/7501878297900679207","author":"搬砖的理查德","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-08T23:40:33.819Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c00f279375ef403d9ab426d44ddd40f9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pCs56CW55qE55CG5p-l5b63:q75.awebp?rk3s=f64ab15b&x-expires=1747352432&x-signature=9QECGzX3R%2FgSWboAiO28kWPP%2BJc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/acf2e57de60a40fcacdd75a797ef314a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pCs56CW55qE55CG5p-l5b63:q75.awebp?rk3s=f64ab15b&x-expires=1747352432&x-signature=Fu5Wk1%2B8AKY8WhN4wxZ7fGgUJKk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3b2d59f6e10f4a279ec3fe6e2b92d921~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pCs56CW55qE55CG5p-l5b63:q75.awebp?rk3s=f64ab15b&x-expires=1747352432&x-signature=9rWB0IYL7qrGLvuvu7%2FEFZFrtT0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f11745b0b46442beb37e7641f8e1db87~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pCs56CW55qE55CG5p-l5b63:q75.awebp?rk3s=f64ab15b&x-expires=1747352432&x-signature=%2B%2BK2vg1NQVEfUm41Oy%2Bm47pgpM4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/61fc73f410404507abc823505716b443~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pCs56CW55qE55CG5p-l5b63:q75.awebp?rk3s=f64ab15b&x-expires=1747352432&x-signature=QfoKWtx80uVZfHTeQSD7NXUxC%2B8%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","前端"],"attachments":null,"extra":null,"language":null},{"title":"Flutter Provider","url":"https://juejin.cn/post/7501657041607802920","content":"//...\\nConsumer<CounterNotifier>(\\n builder: (context, counterNotifier, child) {\\n return Text(\'count: ${counterNotifier.count}\');\\n },\\n child: const Icon(Icons.add), //通过 child 参数传递无需重建的子组件\\n);\\n//...\\n
\\n//...\\nSelector<CounterNotifier, int>(\\n selector: (context, counterNotifier) => counterNotifier.count, //假设 CounterNotifier 维护了多个字段,只监听其中的 count 字段\\n builder: (context, count, child) {\\n return Text(\'count: $count\');\\n },\\n child: const Icon(Icons.add), //通过 child 参数传递无需重建的子组件\\n);\\n//...\\n
\\n//不监听状态变化,仅获取状态,不会触发当前组件重建\\nProvider.of<CounterNotifier>(context, listen: false).increment();\\n//不能在 build 方法内调用\\ncontext.read<CounterNotifier>().increment(); //内部就是 Provider.of<T>(this, listen: false)\\n//主动监听状态变化,当状态变化时触发组件重建(只能在 build 方法内调用),watch 内部本质是 dependOnInheritedWidgetOfExactType 方法的封装\\ncontext.watch<CounterNotifier>().increment(); //内部就是 Provider.of<T>(this)\\n
\\n1 创建自定义 ChangeNotifier 提供状态管理
\\n//Model 用于管理状态并触发通知\\nclass CounterNotifier with ChangeNotifier { //mixin\\n//class CounterNotifier extends ChangeNotifier {\\n int _count = 0; //私有字段\\n\\n //提供只读属性\\n int get count => _count;\\n\\n void increment() {\\n _count++; //修改状态\\n notifyListeners(); //手动触发更新,通知所有监听者\\n }\\n}\\n
\\n2 在 Widget 树中使用 Provider
\\nvoid main() {\\n runApp(\\n //用 Provider 包裹子 Widget\\n ChangeNotifierProvider(\\n create: (context) => CounterNotifier(), //通过 ChangeNotifierProvider 提供状态\\n child: const MyApp(),\\n ),\\n );\\n}\\n
\\n3 子 Widget 获取状态数据
\\nclass MyApp extends StatelessWidget {\\n const MyApp({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n home: Scaffold(\\n appBar: AppBar(title: const Text(\'Provider 示例\')),\\n body: Center(\\n //通过 Consumer 来访问状态数据\\n child: Consumer<CounterNotifier>(\\n builder: (context, counterNotifier, child) {\\n return Text(\\"count: ${counterNotifier.count}\\");\\n },\\n ),\\n ),\\n floatingActionButton: FloatingActionButton(\\n onPressed: () {\\n //通过 Provider.of 方法获取实例并获取状态数据\\n final counterNotifier = Provider.of<CounterNotifier>(context, listen: false);\\n counterNotifier.increment(); //直接调用方法\\n print(\\"count: ${counterNotifier.count}\\"); //直接访问属性\\n //内部就是 Provider.of<T>(this, listen: false)\\n final counterNotifier2 = context.read<CounterNotifier>();\\n print(\\"count2: ${counterNotifier2.count}\\");\\n },\\n child: const Icon(Icons.add),\\n ),\\n ));\\n }\\n}\\n
","description":"Flutter Provider Provider 是 Flutter 官方推荐的状态管理库,基于 InheritedWidget 和 ChangeNotifier 实现跨组件状态共享与响应式更新,是一个轻量级的状态管理解决方案,旨在简化复杂的状态管理流程,从而能够更高效地管理应用中的状态共享逻辑,同时保持代码的可读性和可维护性\\nChangeNotifierProvider\\n状态提供者,继承自 InheritedProvider,用于状态数据管理\\n结合 ChangeNotifier 类,当状态变化时通知依赖的 Widget 更新\\nConsumer…","guid":"https://juejin.cn/post/7501657041607802920","author":"louisgeek","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-08T08:28:08.510Z","media":null,"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter InheritedWidget","url":"https://juejin.cn/post/7501589510268387328","content":"1 创建自定义 InheritedWidget 提供状态管理
\\nclass AppSharedDataInherited extends InheritedWidget {\\n //提供共享数据\\n final Color primaryColor;\\n final double fontSize;\\n\\n const AppSharedDataInherited({\\n super.key,\\n required this.primaryColor,\\n required this.fontSize,\\n required Widget child,\\n }) : super(child: child);\\n\\n @override\\n bool updateShouldNotify(AppSharedDataInherited oldWidget) {\\n //返回 true 时,所有依赖此 InheritedWidget 的子 Widget 会触发 didChangeDependencies 并重建\\n return oldWidget.primaryColor != primaryColor || oldWidget.fontSize != fontSize; //数据变化决定是否通知依赖的子 Widget 重建\\n }\\n\\n //提供 of 静态方法,方便子 Widget 访问数据\\n static AppSharedDataInherited? of(BuildContext context) {\\n //通过 dependOnInheritedWidgetOfExactType 获取最近的 InheritedWidget 实例,并建立依赖关系(子 Widget 会被标记为依赖此 InheritedWidget)\\n return context.dependOnInheritedWidgetOfExactType<AppSharedDataInherited>();\\n }\\n}\\n
\\n2 在 Widget 树中使用 InheritedWidget
\\nvoid main() {\\n runApp(const MyApp());\\n}\\n\\nclass MyApp extends StatefulWidget {\\n const MyApp({super.key});\\n\\n @override\\n State<MyApp> createState() => _MyAppState();\\n}\\n\\nclass _MyAppState extends State<MyApp> {\\n Color _primaryColor = Colors.green;\\n double _fontSize = 12;\\n\\n void changeParam(Color newColor) {\\n setState(() {\\n _primaryColor = _fontSize % 2 == 0 ? newColor : Colors.red;\\n _fontSize = _fontSize + 3;\\n });\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n //用自定义 InheritedWidget 包裹子 Widget\\n return AppSharedDataInherited(\\n primaryColor: _primaryColor,\\n fontSize: _fontSize,\\n child: MaterialApp(\\n home: Scaffold(\\n appBar: AppBar(title: const Text(\'InheritedWidget 示例\')),\\n body: const MyBody(), \\n floatingActionButton: FloatingActionButton(\\n onPressed: () => changeParam(Colors.blue), //改变值\\n child: const Icon(Icons.add),\\n ),\\n ),\\n ));\\n }\\n}\\n\\n
\\n3 子 Widget 获取状态数据
\\nclass MyBody extends StatelessWidget {\\n const MyBody({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n //通过 of 方法获取父 Widget 共享的状态数据\\n final appSharedDataInherited = AppSharedDataInherited.of(context);\\n final primaryColor = appSharedDataInherited?.primaryColor;\\n final fontSize = appSharedDataInherited?.fontSize;\\n return Center(\\n child: Text(\\n \'子 Text 的颜色和文字大小来自 AppSharedDataInherited\',\\n style: TextStyle(color: primaryColor, fontSize: fontSize),\\n ),\\n );\\n }\\n}\\n
\\n本文续接上篇文章:#[Flutter小试牛刀] 低配版signals,添加局部刷新
\\n在上篇文章里,已经写了一个减配版的signals,且添加了局部刷新的功能,这已经能够满足大部分的需求。但是某些时候,多个值的改变都会刷新某一部分UI,如果连续改动它们的值就会造成重复刷新的问题;
\\n我们可以看到下面signals的一段代码:
\\nfinal a = signal(0); \\nfinal b = signal(1); \\nfinal c = computed(() => a.value + b.value); \\n\\neffect((){\\n print(\\"c is ${c.value}\\");\\n});\\na.value ++;\\nb.value ++;\\n
\\n当a值与b值的改变时都会影响c值的改变。我们同时改变了a 和 b的值,effect函数回调了两次。但某些情况下为了性能,我们只需要它回调一次就够了。
\\n为了解决这个问题,signals添加了batch函数:
\\nfinal a = signal(0); \\nfinal b = signal(1); \\nfinal c = computed(() => a.value + b.value); \\n\\neffect((){\\n print(\\"c is ${c.value}\\");\\n});\\n\\nbatch((){\\n a.value++;\\n b.value++;\\n});\\n
\\n上面带代码基于之前的代码添加了些许改进,在batch函数里对值进行修改,effect函数只会触发一次改动。
\\n那么如何在我们简化版的signals里实现batch功能呢?
\\n具体的实现思路如下:
\\n有了上述的思路开始改造代码:
\\nUseBatch? _batchNode;\\n\\nclass UseBatch extends Node {\\n final VoidCallback fn;\\n final Set<void Function()> _listeners = {};\\n UseBatch(this.fn);\\n @override\\n void dispose() {\\n _listeners.clear();\\n _unsubscribe();\\n }\\n\\n @override\\n void notifyListeners() {\\n for (var listener in _listeners) {\\n listener.call();\\n }\\n }\\n\\n @override\\n void addListener(void Function() listener) {\\n _listeners.add(listener);\\n }\\n\\n @override\\n void removeListener(void Function() listener) {\\n _listeners.remove(listener);\\n }\\n\\n void call() {\\n //判断是否已经处在batch,如果是则停止执行\\n if (_batchNode != null) {\\n return;\\n }\\n //将batch节点设置成自己\\n _batchNode = this;\\n try {\\n //触发batch函数\\n fn.call();\\n } catch (e) {\\n rethrow;\\n } finally {\\n //重置batch函数\\n _batchNode = null;\\n //触发batch回调函数列表\\n notifyListeners();\\n //清理\\n dispose();\\n }\\n }\\n}\\n\\nabstract class MixinUseState<W extends StatefulWidget> extends State<W> {\\n ...\\n \\n //batch函数\\n void batch(VoidCallback fn) {\\n UseBatch(fn)();\\n }\\n \\n void _notifyListeners() {\\n //判断当前是否在batch函数下,如果是则将UI改变暂存,因为listeners是set,相同的回调函数只会回调一次\\n if (_batchNode != null) {\\n _batchNode?.addListener(_notifyListeners);\\n return;\\n }\\n setState(() {});\\n }\\n}\\n
\\n该改动的核心是我们知道所有触发UI改动的函数是 _notifyListeners ,我们可以在触发_notifyListeners前判断是否有 _batchNode ,如果有则延后触发。
","description":"本文续接上篇文章:#[Flutter小试牛刀] 低配版signals,添加局部刷新 在上篇文章里,已经写了一个减配版的signals,且添加了局部刷新的功能,这已经能够满足大部分的需求。但是某些时候,多个值的改变都会刷新某一部分UI,如果连续改动它们的值就会造成重复刷新的问题;\\n\\n我们可以看到下面signals的一段代码:\\n\\nfinal a = signal(0); \\nfinal b = signal(1); \\nfinal c = computed(() => a.value + b.value); \\n\\neffect((){\\n print(\\"c is…","guid":"https://juejin.cn/post/7501620025271795748","author":"孤鸿玉","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-08T02:56:57.130Z","media":null,"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"[Flutter 基础] - Scaffold 的基本用法介绍","url":"https://juejin.cn/post/7501539411415498763","content":"在 Flutter 中,Scaffold
是构建 Material Design 风格页面的核心组件,它提供了基础的页面结构框架,可以快速集成常见的 UI 元素(如 AppBar、抽屉菜单、悬浮按钮等)。以下是 Scaffold
的详细用法和常见配置:
Scaffold(\\n appBar: AppBar(...),\\n body: Container(...),\\n floatingActionButton: FloatingActionButton(...),\\n drawer: Drawer(...),\\n bottomNavigationBar: BottomNavigationBar(...),\\n // 其他参数...\\n);\\n
\\nAppBar
appBar: AppBar(\\n title: Text(\'首页\'),\\n leading: IconButton(icon: Icon(Icons.menu), onPressed: () {}),\\n actions: [\\n IconButton(icon: Icon(Icons.search), onPressed: () {}),\\n ],\\n backgroundColor: Colors.blue,\\n),\\n
\\nWidget
body: Center(\\n child: Text(\'Hello Flutter!\'),\\n),\\n
\\nFloatingActionButton
floatingActionButton: FloatingActionButton(\\n onPressed: () {},\\n child: Icon(Icons.add),\\n),\\nfloatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, // 位置配置\\n
\\nDrawer
drawer: Drawer(\\n child: ListView(\\n children: [\\n DrawerHeader(child: Text(\'Header\')),\\n ListTile(title: Text(\'Item 1\')),\\n ListTile(title: Text(\'Item 2\')),\\n ],\\n ),\\n),\\n
\\nDrawer
drawer
类似,但从右侧滑出。BottomNavigationBar
bottomNavigationBar: BottomNavigationBar(\\n items: [\\n BottomNavigationBarItem(icon: Icon(Icons.home), label: \'首页\'),\\n BottomNavigationBarItem(icon: Icon(Icons.settings), label: \'设置\'),\\n ],\\n currentIndex: _selectedIndex,\\n onTap: (index) => setState(() => _selectedIndex = index),\\n),\\n
\\nColor
backgroundColor: Colors.grey[200],\\n
\\nbool
true
)。Scaffold
,但需要注意上下文(如 ScaffoldMessenger
用于显示 SnackBar):\\nScaffold(\\n body: Scaffold(\\n appBar: AppBar(title: Text(\'嵌套 Scaffold\')),\\n ),\\n);\\n
\\nbottomSheet
属性实现自定义底部栏: bottomSheet: Container(\\n height: 50,\\n color: Colors.blue,\\n child: Center(child: Text(\'Custom Bottom Sheet\')),\\n ),\\n
\\nbottomSheet
一般和bottomNavigationBar
不会同时出现,当bottomNavigationBar不能满足业务需求的时候,可以考虑使用bootomSheet来自定义一个底部栏。当然,两个同时出现也不会有什么问题。少部分场景可能需要二级导航栏的时候,也是可以用的。
// 打开左侧抽屉\\nScaffold.of(context).openDrawer();\\n// 打开右侧抽屉\\nScaffold.of(context).openEndDrawer();\\n
\\nimport \'package:flutter/material.dart\';\\n\\nclass MyHomePage extends StatefulWidget {\\n @override\\n _MyHomePageState createState() => _MyHomePageState();\\n}\\n\\nclass _MyHomePageState extends State<MyHomePage> {\\n int _selectedIndex = 0;\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: Text(\'Scaffold 示例\'),\\n actions: [IconButton(icon: Icon(Icons.share), onPressed: () {})],\\n ),\\n drawer: Drawer(\\n child: ListView(\\n children: [\\n GestureDetector(\\n onTap: (){\\n scaffoldKey.currentState?.closeDrawer(); //关闭左侧菜单栏\\n },\\n child: ListTile(title: Text(\'菜单项1\')),\\n ),\\n GestureDetector(\\n onTap: (){\\n scaffoldKey.currentState?.closeDrawer(); //关闭左侧菜单栏\\n },\\n child: ListTile(title: Text(\'菜单项2\')),\\n ),\\n ],\\n ),\\n ),\\n body: Center(\\n child: Text(\'当前页面: $_selectedIndex\'),\\n ),\\n bottomNavigationBar: BottomNavigationBar(\\n items: [\\n BottomNavigationBarItem(icon: Icon(Icons.home), label: \'首页\'),\\n BottomNavigationBarItem(icon: Icon(Icons.person), label: \'我的\'),\\n ],\\n currentIndex: _selectedIndex,\\n selectedItemColor: Colors.amber[800],\\n onTap: (index) => setState(() => _selectedIndex = index),\\n ),\\n floatingActionButton: FloatingActionButton(\\n onPressed: () {},\\n child: Icon(Icons.add),\\n ),\\n );\\n }\\n}\\n
\\nScaffold.of(context)
时,确保 context
正确。resizeToAvoidBottomInset
会自动调整布局防止键盘遮挡。通过灵活组合 Scaffold
的各个属性,可以快速构建出符合 Material Design 规范的复杂页面结构。
import \'dart:isolate\';\\n\\nvoid main() async {\\n // 创建接收端口\\n final receivePort = ReceivePort();\\n\\n // 启动新 Isolate\\n await Isolate.spawn(\\n fibonacciIsolate,\\n {\'sendPort\': receivePort.sendPort, \'n\': 40},\\n );\\n\\n // 监听结果\\n receivePort.listen((message) {\\n print(\'斐波那契结果: $message\');\\n receivePort.close();\\n });\\n}\\n\\nvoid fibonacciIsolate(Map<String, dynamic> data) {\\n final sendPort = data[\'sendPort\'] as SendPort;\\n final n = data[\'n\'] as int;\\n sendPort.send(fibonacci(n));\\n}\\n\\nint fibonacci(int n) {\\n if (n <= 1) return n;\\n return fibonacci(n - 1) + fibonacci(n - 2);\\n}\\n
\\nvoid main() async {\\n final receivePort = ReceivePort();\\n await Isolate.spawn(backgroundTask, receivePort.sendPort);\\n\\n receivePort.listen((progress) {\\n print(\'当前进度: $progress%\');\\n if (progress >= 100) receivePort.close();\\n });\\n}\\n\\nvoid backgroundTask(SendPort sendPort) {\\n for (int i = 0; i <= 100; i += 10) {\\n sendPort.send(i);\\n // 模拟耗时操作\\n Isolate.sleep(Duration(seconds: 1));\\n }\\n}\\n
\\nvoid main() async {\\n final results = await Future.wait([\\n compute(fibonacci, 40),\\n compute(factorial, 20),\\n ]);\\n print(\'结果: $results\');\\n}\\n\\nint factorial(int n) => n == 0 ? 1 : n * factorial(n - 1);\\n
\\nvoid main() async {\\n final mainReceivePort = ReceivePort();\\n final isolate = await Isolate.spawn(\\n echoIsolate,\\n mainReceivePort.sendPort,\\n );\\n\\n mainReceivePort.listen((message) {\\n print(\'收到: $message\');\\n if (message == \'Hello\') {\\n (message as SendPort).send(\'你好,子 Isolate!\');\\n }\\n });\\n\\n // 发送初始消息\\n mainReceivePort.sendPort.send(\'Hello\');\\n}\\n\\nvoid echoIsolate(SendPort mainSendPort) {\\n final isolateReceivePort = ReceivePort();\\n mainSendPort.send(isolateReceivePort.sendPort);\\n\\n isolateReceivePort.listen((message) {\\n print(\'子收到: $message\');\\n mainSendPort.send(\'收到: $message\');\\n });\\n}\\n
\\ncompute
简化操作import \'dart:async\';\\nimport \'package:flutter/foundation.dart\';\\n\\nvoid main() async {\\n final result = await compute(fibonacci, 40);\\n print(\'计算结果: $result\');\\n}\\n
\\nSendPort
传递可序列化数据。try-catch
捕获异常,通过端口发送错误信息。compute
在 Flutter 中简化后台任务,防止 UI 卡顿。void main() async {\\n final receivePort = ReceivePort();\\n await Isolate.spawn(\\n errorTask,\\n receivePort.sendPort,\\n onError: receivePort.sendPort, // 错误发送到主端口\\n onExit: receivePort.sendPort,\\n );\\n\\n receivePort.listen((message) {\\n if (message is List && message[0] is String) {\\n print(\'错误: ${message[1]}\');\\n } else {\\n print(\'结果: $message\');\\n }\\n receivePort.close();\\n });\\n}\\n\\nvoid errorTask(SendPort sendPort) {\\n try {\\n // 可能出错的操作\\n final result = someDangerousOperation();\\n sendPort.send(result);\\n } catch (e) {\\n sendPort.send([\'error\', e.toString()]);\\n }\\n}\\n
","description":"一、基础 Isolate 使用 场景:计算密集型任务\\nimport \'dart:isolate\';\\n\\nvoid main() async {\\n // 创建接收端口\\n final receivePort = ReceivePort();\\n\\n // 启动新 Isolate\\n await Isolate.spawn(\\n fibonacciIsolate,\\n {\'sendPort\': receivePort.sendPort, \'n\': 40},\\n );\\n\\n // 监听结果\\n receivePort.listen((message) {…","guid":"https://juejin.cn/post/7501517798365511715","author":"ak啊","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-07T09:04:49.096Z","media":null,"categories":["代码人生","Dart","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"管理者的囚徒困境","url":"https://juejin.cn/post/7501266938108428339","content":"哈喽,我是老刘
\\n老刘带着团队做Flutter开发已经六年多了,这之前还做过防火墙、Android开发等领域。
\\n加起来十多年的时间,大厂、外企、创业公司都干过。
\\n本以为该见的世面都见过了,没想到还有能让我惊讶的。
\\n
\\n这是前几天在抖音刷到的内容。
\\n让我惊讶的不是刷数据,而是用了斗破苍穹,这得是有多大怨念啊。
我想如果给这个世界上最会找bug的职业排个序,程序员应该能很靠前吧。
\\n老刘亲身经历过的,之前的一个公司会将开发团队每个人的代码量拉出来按升序排序。
\\n本来老刘的排名很靠前,不过正好那段时间我正在给项目做Flutter的大版本升级。
\\n因为我们的项目有很多深度定制的内容,所以Flutter大版本升级会需要自动生成很多代码。
\\n然后一夜之间提交了几万行新增改动。
\\n如果有需要,我还可以把很多测试性代码和三方代码合并进去,刷个百万代码量不是问题。
\\n而且都是有正规功能的有意义的代码。
\\n所以是什么样的思维逻辑,想出来给程序员定代码量的指标的。
而且即使不考虑刷数据的问题,用这些僵化的指标去评估程序员的工作真的有意义吗?
\\n真的相信应该用工时、代码量等数据评估程序员的工作的人是有的。
\\n虽然概率很低,但是我真的碰到过。
\\n老刘自己是奋战在一线的程序员很多年了,目前还在写代码。
\\n另一方面我也带了好几年的开发团队,所以我能够以相对客观的视角看这个问题。
站在管理者的角度,特别是对某个具体技术领域不怎么懂的管理者,很容易出现的一个问题就是对项目缺少掌控感。
\\n这种情况下就很容易出现需要抓住一个可控指标的心态。
\\n而最容易找到的指标就是代码量和工时。
但是老刘站在一个老程序员的视角上。
\\n如果我的组员花一天时间思考出合理的方案,然后用几十行代码实现功能,然后到点下班,我会觉得很安心。
\\n但是如果他花一天时间,甚至加班很晚,给我堆砌了一堆代码,我就开始有点慌了。
\\n所以管理者应该明白的是程序员的工作的本质是思考而非搬砖。
\\n越是不经思考的堆砌,就越是后期技术债务的根由。
当然我相信这些道理只要不是从来没有写过代码的管理者应该都懂。
\\n但是为啥这种管理方式还是屡见不鲜呢?
这些话我就不方便写了,让AI来回答吧,AI说的很好,就是我想表达的:
\\n
\\n所以作为程序员也要明白,有很多管理策略可能并非用于管理的。
\\n当然我也希望所有的开发者都能不用去把精力花在这些问题上,而是能专心搞技术。
\\n
如果看到这里的同学对客户端开发或者Flutter开发感兴趣,欢迎联系老刘,我们互相学习。
\\n点击免费领老刘整理的《Flutter开发手册》,覆盖90%应用开发场景。
\\n可以作为Flutter学习的知识地图。
\\n覆盖90%开发场景的《Flutter开发手册》
void main() {\\n //创建主 Isolate 的 ReceivePort\\n final ReceivePort mainReceivePort = ReceivePort();\\n //启动子 Isolate,第一个参数指定一个入口函数(必须是一个顶层或静态函数),第二个参数是入口函数的初始参数(通常传入主 Isolate 的 sendPort 方便进行通信)\\n Isolate.spawn(isolateFun, mainReceivePort.sendPort);\\n //主 Isolate 监听消息\\n mainReceivePort.listen((message) {\\n if (message is SendPort) {\\n print(\\"接收子 Isolate 发送的 SendPort\\");\\n final SendPort childSendPort = message;\\n childSendPort.send(\\"给子 Isolate 发送的消息\\");\\n } else {\\n print(\'主 Isolate 收到消息:$message\');\\n }\\n });\\n //处理完成后及时关闭 Port 端口\\n //mainReceivePort.close();\\n}\\n//isolateFun 函数会在新 Isolate 中运行\\nvoid isolateFun(SendPort sendPort) {\\n //创建子 Isolate 的 ReceivePort\\n final childReceivePort = ReceivePort();\\n //将子 Isolate 的 SendPort 发送给主 Isolate\\n sendPort.send(childReceivePort.sendPort);\\n //子 Isolate 监听消息\\n childReceivePort.listen((message) {\\n if (message is SendPort) {\\n print(\\"接收主 Isolate 发送的 SendPort\\");\\n final SendPort mainReceivePort = message;\\n mainReceivePort.send(\\"给主 Isolate 发送的消息\\");\\n } else {\\n print(\'子 Isolate 收到消息:$message\');\\n }\\n });\\n //处理完成后及时关闭 Port 端口\\n //childReceivePort.close();\\n}\\n
\\nvoid main() async {\\n const fileName = \'data.json\';\\n final jsonData = await Isolate.run(() => _readAndParseJson(fileName));\\n print(\'JSON 数据长度:${jsonData.length}\');\\n}\\n\\nMap<String, dynamic> _readAndParseJson(String fileName) {\\n final fileData = File(fileName).readAsStringSync();\\n return jsonDecode(fileData) as Map<String, dynamic>;\\n}\\n
\\nint heavyComputation(int input) => input * 2;\\n\\nFuture<int> calculateFun() async {\\n final result = await compute(heavyComputation, inputData);\\n print(\'calculateFun result:$result\');\\n}\\n
\\nvoid main() {\\n print(\'同步代码\');\\n //加入微任务队列\\n scheduleMicrotask(() {\\n print(\\"微任务1\\");\\n scheduleMicrotask(() => print(\\"微任务2\\")); //微任务可链式添加\\n });\\n Future.microtask(() => print(\'微任务3\')); //加入微任务队列\\n Future(() => print(\'事件任务\')).then(() => print(\'事件任务的微任务\')); //回调是微任务\\n Future.delayed(Duration.zero, () => print(\'延迟事件任务\')); //加入事件队列\\n //\\n print(\'同步代码\');\\n}\\n
\\nFuture<void> fetchData() async {\\n print(\'开始请求\');\\n var response = await http.get(Uri.parse(\'https://api.com\')); //await 之后的代码包装成微任务,加入微任务队列\\n print(\'处理数据:$response \'); //await 后续的逻辑,等价于 .then(() => print(...))\\n}\\n
\\nTimer(Duration(seconds: 2), () {\\n print(\\"延迟事件任务\\"); //加入事件队列\\n});\\n
\\n随着 Compose Multiplatform 1.8.0 的发布,iOS 版本也引来的第一个稳定版本,按照官方的原话:「iOS Is Stable and Production-Ready」 ,而 1.8.0 版本,也让 Kotlin 和 Compose 在移动端有了完整的支持。
\\n在 2023 年 4 月 Compose 发布了 Compose for iOS Alpha ,而在 2024 年的 5 月的 1.6 版本发布了iOS Beta ,一年后的今天,1.8 版本终于有迎来了 Stable 发布。
\\n\\n\\n三年之期已到,龙王归来?
\\n
在官方的调查里,超过 96% 的开发者表示在 iOS 上使用 Compose Multiplatform 没有重大的性能问题,这也是官方本次 stable 的原因:
\\n同时在性能对比也有不错的基准测试结果:
\\n\\n\\n\\n
如果你去看 1.8.0 的更新日志 ,就会看到 Compose 大部分都是 iOS 的亮点,基本这个版本就是为了 iOS 而发布,其中最有意思的莫过于本次加入了并发渲染\ufeff支持:
\\n\\n\\nCompose Multiplatform 在 iOS 现在支持将渲染任务卸载到专用渲染线程,而并发渲染可以在没有 UIKit 互操作的情况下提高性能。
\\n
用户通过直接在 ComposeUIViewController
配置块中启用 ComposeUIViewControllerConfiguration
类的 useSeparateRenderThreadWhenPossible
标志或 parallelRendering
属性,选择在单独的渲染线程上对渲染命令进行编码:
@OptIn(ExperimentalComposeUiApi::class)\\nfun main(vararg args: String) {\\n UIKitMain {\\n ComposeUIViewController(configure = { parallelRendering = true }) {\\n // ...\\n }\\n }\\n}\\n
\\n而另一个 iOS 的核心实现就是 Kotlin/Native ,Kotlin/Native 是 KMP 在 iOS 支持的关键能力,它负责将 Kotlin 代码直接编译为目标平台的机器码或 LLVM 中间表示 (IR),最终为 iOS 生成一个标准 .framework
,这也是为什么 Compose iOS 能实现接近原生的性能。
\\n\\n鸿蒙 Compose 实现支持目前主流也是 Kotlin/Native ,不得不说 Kotlin 最强大的核心价值不是他的语法糖,而是他的编译器。
\\n
当然,Compose Multiplatform 的 UI 渲染并非直接依赖于 iOS 的 UIKit 或 SwiftUI 的原生组件,而是依赖于Skia 图形库,在 Compose Multiplatform 里是通过 Skiko (Skia for Kotlin) 这套 Kotlin 绑定库的能力进行绘制。
\\n\\n\\n简单理解,Skiko 在 iOS 利用
\\nCAMetalLayer
作为其绘图表面,Compose UI 的每一帧都会通过 Skia 引擎渲染到由CAMetalLayer
提供的 Metal 纹理上,这层实现逻辑和 Flutter 类似。
而前面提到了 UIKit 的互操作,这也是 Compose 渐进式集成的支持之一,在 iOS 上 Compose 可以同时与 SwiftUI 和 UIKit 进行互操作,换句话说,开发者可以在 Swift/UIKit 中使用 Compose,也在 Compose 中使用 SwiftUI/UIKit 。
\\n\\n\\n当然,基于 Compose iOS 的实现模式,在继承的数据绑定和状态同步交互上可能还会存在某些边界问题。
\\n
同时,就像我们之前聊过的,klibs.io 的发布也补全了 Compose Multiplatform 在跨平台最后一步,这也是 Compose iOS 能正式发布的另外一个原因:
\\n而 1.8 下的 Compose iOS 也更新了一些细节:、
\\n目前官方表示,许多团队(包括 Markaz、Wrike、Feres 和 Physics Wallah)已经将 Compose Multiplatform 集成到它们的大型应用中,根据 Compoes 的理念 ,可以一次集成一个屏幕或功能,所以你无需从头开始就可以使用 Compose iOS。
\\n\\n\\n也就是除了 Web 路线之后,基本上 Compose 在 Stable 的节奏上已经跟上了 Flutter 。
\\n
最后,现在 Compose 的 hot reload 已经可以使用 ,现在 Compose Multi 开发可以和 Flutter 一样在更改代码后立即查看结果:
\\n\\n\\n在补全这一点后, Compose 跨平台的开发体验也上了一个档次,至少 hot load 的体验比 preview 好太多了。
\\n
那么,你已经用上 Compose iOS 了吗?或者说你接下来会考虑吗?
\\n路由和导航可以说是一个项目中最重要的部分之一。越复杂的项目,路由管理就显得越重要。同样越复杂的业务场景,对路由的使用要求也越高。Flutter原生的路由管理就提供了非常丰富的功能,灵活使用可以帮助我们高效的完成不同的业务场景开发。
\\nMaterialPageRoute
、CupertinoPageRoute
)。push()
、pop()
等方法操作路由栈,实现页面导航。通过 Navigator.push()
(打开新页面,入栈) 和 Navigator.pop()
(返回上一页,出栈) 实现页面跳转:
// 跳转到新页面(入栈)\\nNavigator.push(\\n context,\\n MaterialPageRoute(builder: (context) => SecondPage()),\\n);\\n\\n// 返回上一页(出栈)\\nNavigator.pop(context);\\n
\\n在 MaterialApp
中配置路由表,通过名称跳转:
return MaterialApp(\\n title: \'Flutter Demo\',\\n initialRoute: \\"/\\",\\n routes: {\\n // \'/\': (context) => MyHomePage(), 注意:这里不能配置App的跟页面,因为和home 参数重复了。\\n \'/textFieldDemo\': (context) => TextFieldDemo(),\\n },\\n theme: ThemeData(\\n colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),\\n ),\\n home: const MyHomePage(title: \'Flutter Demo Home Page\'),\\n);\\n\\n// 通过名称跳转\\nNavigator.pushNamed(context, \'/textFieldDemo\');\\n
\\n去掉home属性的写法
\\nreturn MaterialApp(\\n title: \'Flutter Demo\',\\n initialRoute: \\"/\\",\\n routes: {\\n \'/\': (context) => MyHomePage(title: \'title\'),//因为MyHomePage是这个页面的根页面。\\n \'/textFieldDemo\': (context) => TextFieldDemo(),\\n },\\n theme: ThemeData(\\n colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),\\n )\\n);\\n\\n// 通过名称跳转\\nNavigator.pushNamed(context, \'/textFieldDemo\');\\n
\\ntitle
参数的页面class SecondPage extends StatelessWidget {\\n final String? title;\\n\\n const SecondPage({super.key, @required this.title});\\n\\n @override\\n Widget build(BuildContext context) {\\n final args = ModalRoute.of(context)!.settings.arguments;\\n var id, name;\\n if (args != null && args is Map<String, dynamic>) {\\n id = args[\'ID\'];\\n name = args[\'name\'];\\n }\\n return Scaffold(\\n appBar: AppBar(title: Text(title ?? \'demo\')),\\n body: Center(\\n child: Column(\\n children: [\\n Text(\'second page\'),\\n if (id != null) Text(\'ID:$id\') else Text(\'No ID\'),\\n if (name != null) Text(\'name:$name\') else Text(\'No name\'),\\n ],\\n ),\\n ),\\n );\\n }\\n}\\n
\\n直接传参(匿名路由):
\\nNavigator.push(\\n context,\\n MaterialPageRoute(\\n builder: (context) => SecondPage(title: \'传递的标题\'),\\n ),\\n);\\n
\\n命名路由传参:
\\n // 跳转时传参\\nNavigator.pushNamed(\\n context,\\n \'/second\',\\n arguments: {\'ID\': \'this is id\', \'name\': \'this is name\'},\\n),\\n
\\n通过 PageRouteBuilder
实现自定义动画:
Navigator.push(\\n context,\\n PageRouteBuilder(\\n transitionDuration: Duration(milliseconds: 500),\\n pageBuilder: (_, animation, secondaryAnimation) => SecondPage(title: \'Fade Transition\'),\\n transitionsBuilder: (_, animation, secondaryAnimation, child) {\\n return FadeTransition(\\n opacity: CurvedAnimation(parent: animation, curve: Curves.easeOut),\\n child: child,\\n );\\n },\\n ),\\n);\\n
\\n使用 AutomaticKeepAliveClientMixin
保留页面状态(如滚动位置):
class KeepAlivePage extends StatefulWidget {\\n @override\\n _KeepAlivePageState createState() => _KeepAlivePageState();\\n}\\n\\nclass _KeepAlivePageState extends State<KeepAlivePage> with AutomaticKeepAliveClientMixin {\\n @override\\n bool get wantKeepAlive => true; // 标记需要保留状态\\n\\n @override\\n Widget build(BuildContext context) {\\n super.build(context); // 必须调用\\n return Scaffold(\\n appBar: AppBar(title: Text(\'Keep Alive Page\')),\\n body: Center(child: Text(\'This page state is preserved\')),\\n );\\n }\\n}\\n
\\n可以针对一些特殊的页面,比如权限配置,A/B测试等去做监听处理。
\\nclass MyNavigatorObserver extends NavigatorObserver {\\n @override\\n void didPush(Route route, Route? previousRoute) {\\n // 这里可以根据用户状态去判断进入不一样的页面。\\n if (route.settings.name == \'/a\') {\\n // 如果目标页面是/a,则判断用户是否是Admin,是则进入/a页面,否则重定向到/b\\n if(user.isAdmin()) {\\n print(\'进入A页面: ${route.settings.name}\');\\n Future.delayed(Duration.zero, () {\\n Navigator.of(navigator!.context).pushReplacementNamed(\'/a\');\\n });\\n } else {\\n print(\'进入B页面: ${route.settings.name}\')\\n Future.delayed(Duration.zero, () {\\n Navigator.of(navigator!.context).pushReplacementNamed(\'/b\');\\n });\\n }\\n }\\n }\\n}\\n\\n @override\\n void didPush(Route route, Route? previousRoute) {\\n const allowedPages = user.getAllowedConfig();\\n if(!allowedPages.contains(route.settings.name)) { //如果需要跳转到页面不在列表,则返回\\n Navigator.of(navigator!.context).pop();\\n }\\n }\\n\\n // 在 MaterialApp 中注册\\n MaterialApp(\\n navigatorObservers: [\\n MyNavigatorObserver(),\\n MyPermissionManagerObserver(),\\n ]\\n );\\n
\\nWillPopScope(\\n onWillPop: () async {\\n if (shouldPreventBack) {\\n showExitConfirmDialog(); // 显示确认对话框\\n return false; // 阻止返回\\n }\\n return true; // 允许返回\\n },\\n child: Scaffold(...),\\n);\\n
\\n使用 pushReplacement()
替换当前页面(不保留在栈中):
Navigator.pushReplacement(\\n context,\\n MaterialPageRoute(builder: (context) => NewPage()),\\n);\\n
\\n使用 popUntil()
返回到某个特定页面:
Navigator.popUntil(context, ModalRoute.withName(\'/home\'));\\n
\\n通过 onGenerateRoute
动态生成路由:
MaterialApp(\\n onGenerateRoute: (settings) {\\n if (settings.name == \'/detail\') {\\n final args = settings.arguments as DetailArgs;\\n return MaterialPageRoute(\\n builder: (context) => DetailPage(args: args),\\n );\\n }\\n return null;\\n },\\n);\\n
\\n命名路由 vs 匿名路由:
\\n路由栈管理:
\\nNavigator.popUntil()
清理不必要的路由。性能优化:
\\nAutomaticKeepAliveClientMixin
。错误处理:
\\nonGenerateRoute
中处理未知路由:\\nonGenerateRoute: (settings) {\\n if (settings.name == \'/404\') {\\n return MaterialPageRoute(builder: (context) => NotFoundPage());\\n }\\n return null;\\n},\\n
\\nimport \'package:flutter/material.dart\';\\n\\nvoid main() {\\n runApp(MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n initialRoute: \'/\',\\n routes: {\\n \'/\': (context) => HomePage(),\\n \'/second\': (context) => SecondPage(),\\n },\\n onGenerateRoute: (settings) {\\n if (settings.name == \'/detail\') {\\n final args = settings.arguments as DetailArgs;\\n return MaterialPageRoute(\\n builder: (context) => DetailPage(args: args),\\n );\\n }\\n return null;\\n },\\n navigatorObservers: [MyNavigatorObserver()],\\n );\\n }\\n}\\n\\nclass HomePage extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\'首页\')),\\n body: Center(\\n child: ElevatedButton(\\n onPressed: () {\\n Navigator.pushNamed(context, \'/second\');\\n },\\n child: Text(\'跳转到第二页\'),\\n ),\\n ),\\n );\\n }\\n}\\n\\nclass SecondPage extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\'第二页\')),\\n body: Center(\\n child: ElevatedButton(\\n onPressed: () {\\n Navigator.pushNamed(\\n context,\\n \'/detail\',\\n arguments: DetailArgs(id: 1, name: \'Detail Page\'),\\n );\\n },\\n child: Text(\'跳转到详情页\'),\\n ),\\n ),\\n );\\n }\\n}\\n\\nclass DetailPage extends StatelessWidget {\\n final DetailArgs args;\\n\\n DetailPage({required this.args});\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(args.name)),\\n body: Center(child: Text(\'ID: ${args.id}\')),\\n );\\n }\\n}\\n\\n@freezed\\nclass DetailArgs with _$DetailArgs {\\n factory DetailArgs({required int id, required String name}) = _DetailArgs;\\n}\\n\\nclass MyNavigatorObserver extends NavigatorObserver {\\n @override\\n void didPush(Route route, Route? previousRoute) {\\n print(\'进入页面: ${route.settings.name}\');\\n }\\n}\\n
\\npush()
/pop()
实现简单导航,named routes
管理复杂应用。Navigator
的基础功能非常丰富,通过灵活运用 Navigator
,可以降低复杂项目的路由管理成本,同时也可以让用户有一个更好的体验。
最近在 Dart 在 main 3.9 合并了一项名为 「dot-shorthands」 的语法糖提议,该提议主要是为了简化开发过程中的相关静态固定常量的写法,通过上下文类型推断简化枚举值和静态成员的访问:
\\n简单来说,就是在之前你可能需要写 SomeEnum.someValue
,而在此之后,你只需要写 .someValue
,简写语法不仅限于枚举值,还可用于访问静态 getter、构造函数和函数等:
\\n///之前\\nSomeEnum getValue() => SomeEnum.someValue;\\n\\n\\n///之后\\nSomeEnum getValue() => .someValue;\\n\\n\\n
\\n如果回到 Flutter 场景下,那就是如下代码所示,不管是各类 Flex
控件的 Axis
,还是类似 Offset
等的 Zero
,以后都可以通过如 .zero
、.center
来实现简化写法:
如下图所示,通过上下文推断,最终 center 可以被正常识别并打印:
\\n当然,既然说了是类型推断,那么 dynamic 肯定是不行,比如此时的 test
根本无法推断出其类型:
当然,如果在初始化时赋值,那么 test 的类型就可以被推断并确认:
\\n不过如果你强行指定了 dynamic
类型肯定还是不行的:
另外,在内置的 Color
和 Colors
场景也不适用,这类场景下,因为它们的静态类型和本身的类型并不是同一个,所以也会出现无法简化的情况:
而根据 \'dot-shorthands\' 的语法糖效果,大致常用的简化支持可以如下代码所示:
\\nvoid main() {\\n print(getterArrow); \\n print(getterBody); \\n print(Methods().getterArrow); \\n print(Methods().getterBody); \\n print(Methods.getterArrowStatic); \\n print(Methods.getterBodyStatic); \\n}\\n\\nenum Color { red, blue, green }\\n\\nColor get getterArrow => .red;\\nColor get getterBody { return .red; }\\n\\nclass Methods {\\n static Color get getterArrowStatic => .red;\\n static Color get getterBodyStatic { return .red; }\\n Color get getterArrow => .red;\\n Color get getterBody { return .red; } \\n}\\n
\\n因为目前该语法糖仅在 main 分支可用,需要 Dart 3.9 下在运行时执行 flutter run --enable-experiment=dot-shorthands
才能运行:
可以看到这是一个非常简单的语法糖,整体来说对于开发简化还是挺不错的,那么你会喜欢这个写法吗?
\\n最后, 在 Flutter main channel 中还提供了一个新功能:支持交叉编译 Dart AOT 可执行文件,目前支持从 Windows 和 macOS 编译为 Linux 二进制文件:
\\n--target-os=linux
--target-arch=value
:目标体系结构,可以是 arm64
(64 位 ARM 处理器)或 x64
(64 位处理器)\\n\\n例如 :
\\ndart compile exe --target-os=linux --target-arch=x64 hello.dart -o hello
目前,执行这个命令会下载额外的 Dart SDK 二进制文件,并将它们缓存在 ~/.dart
目录 :
Downloading https://storage.googleapis.com/dart-archive/channels/dev/signed/hash/...4864.../sdk/gen_snapshot_macos_arm64_linux_x64...\\nDownloading https://storage.googleapis.com/dart-archive/channels/dev/raw/hash/...64e44.../sdk/dartaotruntime_linux_x64...\\nSpecializing Platform getters for target OS linux.\\nGenerating AOT kernel dill.\\nCompiling /tmp/hello.dart to /tmp/hello.exe using format Kind.exe:\\nGenerating AOT snapshot. path/to/dir/.dart/3.8.0-265.0.dev/gen_snapshot_macos_arm64_linux_x64 []\\nGenerating executable.\\nMarking binary executable.\\nGenerated: /tmp/hello.exe\\n
\\n例如在 window 上通过 dart compile exe --target-os=linux hello.dart -o hello
编译下方代码,然后到 linux 下执行,可以看到代码可以正常运行:
void main() {\\n for (var i = 0; i < 10; i++) {\\n print(\'hello ${i + 1}\');\\n }\\n}\\n\\n
\\n\\n\\n那么,你觉得 Dart 上的交叉编译是否会是刚需?
\\n
结合之前的 Dart 3.8 开始支持 Null-Aware Elements 语法,感觉 Dart 在近期语法糖调整还是挺多的,就是大家更关心的 build_runner 优化和序列化改进何时才能见到。
","description":"最近在 Dart 在 main 3.9 合并了一项名为 「dot-shorthands」 的语法糖提议,该提议主要是为了简化开发过程中的相关静态固定常量的写法,通过上下文类型推断简化枚举值和静态成员的访问: 简单来说,就是在之前你可能需要写 SomeEnum.someValue ,而在此之后,你只需要写 .someValue ,简写语法不仅限于枚举值,还可用于访问静态 getter、构造函数和函数等:\\n\\n\\n///之前\\nSomeEnum getValue() => SomeEnum.someValue;\\n\\n\\n///之后\\nSomeEnum getValue(…","guid":"https://juejin.cn/post/7500234308432445451","author":"恋猫de小郭","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-06T03:55:18.680Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/aafddb119163409ea2055d7ab8e8161c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747108518&x-signature=kcjd840qiO1poBQXoQrUqAEZO4I%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f33dc3e20885425bb7f45d43936328e9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747108518&x-signature=Q7nghK88PijuJ9rizzEgPIr4zgs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/659bf6075df84c3b9d08a30d64098224~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747108518&x-signature=%2Fdu6nOWbp%2BOXvG5cNGypQDcR%2BBc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fd4571a22ffc4e42a4a521468ab79407~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747108518&x-signature=vuRXPhIVLFd%2BFVIKYF4xRfk2s4g%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fd5f48eeb5084350b09eb93be4976a72~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747108518&x-signature=%2BZWypAU%2FAYYTHKe8UsWoQlahiZc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/16e0ae3f5e9c4b4d9ae884f0ea0b4f56~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747108518&x-signature=TflMCgzj4ixZhFzk1LZB2L%2F82eY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/aec665b4b06f44759d3c0d21dbbcc391~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747108518&x-signature=%2FBEZSPaE3gMHw9ZUHzlV4s4eRNo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7131c5be0463403a8d354d38a7dbbd16~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747108518&x-signature=4w6NxFgZ2TxIIAMvNfkOi63FcTs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f306b155d4b54b24b709b7c1db543c59~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747108518&x-signature=Lbx%2FUvoNwSHMqjVsSjLyfPJaP9Q%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c1e580bfc5d14676a44c18d91df99b01~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747108518&x-signature=a4LuLbrt0btIoKRhzVjl41%2FMPsk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5a91c91249ba44a6b2463b39a98e0481~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1747108518&x-signature=8q1RIein5wWlSyMR3iCNeXnjKhM%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 布局","url":"https://juejin.cn/post/7500521400911364132","content":"Flutter 布局的核心机制是 widget。在 Flutter 中,几乎所有东西都是 widget — 甚至布局模型都是 widget。你在 Flutter 应用程序中看到的图像,图标和文本都是 widget。此外不能直接看到的也是 widget,例如用来排列、限制和对齐可见 widget 的行、列和网格。
\\n步骤1: 选择Center Widget
\\n@override\\n// 步骤4: 一个 Flutter 应用本身就是一个 widget,大多数 widget 都有一个 build() 方法,在应用的 build() 方法中实例化和返回一个 widget 会让它显示出来。\\nWidget build(BuildContext context) {\\n return const MaterialApp(\\n home: Scaffold(\\n // 步骤3: 通过child属性将Text Widget添加到Center Widget中\\n body: Center(\\n child: Text(\'Hello World!\'), // 创建可见的 Widget\\n ),\\n ),\\n );\\n}\\n
\\n所有布局Widget
都具有以下任一项:
child
属性,当布局Widget
只包含一个子项,比如Center
,Container
children
属性,当布局Widget
包含多个子项,比如Row
,Column
,ListView
和Stack
对应 Material 应用,可以使用 Scaffold widget,它提供默认的 banner 背景颜色,还有用于添加抽屉、提示条和底部列表弹窗的 API
\\n@override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n title: \\"Flutter布局学习\\",\\n home: Scaffold(\\n appBar: AppBar(title: Text(\\"Flutter 布局学习\\")),\\n body: Column(\\n spacing: 6,\\n children: [\\n Column(\\n spacing: 10,\\n children: [\\n Text(\\"回乡偶书\\", style: TextStyle(fontSize: 24)),\\n Text(\\"唐.贺知章\\"),\\n Text(\\n \\"少小离家老大回,乡音无改鬓毛衰。\\",\\n style: TextStyle(\\n color: Colors.black,\\n fontSize: 16,\\n height: 1.5, // 控制行高\\n ),\\n ),\\n Text(\\n \\"儿童相见不相识,笑问客从何处来。\\",\\n style: TextStyle(color: Colors.black, fontSize: 16),\\n ),\\n ],\\n ),\\n Column(\\n spacing: 6,\\n // Text 设置左对齐\\n // 由于Text Widget的大小是自动包裹内容的,\\n // 所以设置Text的Alignment.left不能生效。\\n // 当布局是Column时,可以设置Column的\\n // crossAxisAlignment: CrossAxisAlignment.start。\\n crossAxisAlignment: CrossAxisAlignment.start,\\n children: [\\n Padding(\\n padding: EdgeInsets.fromLTRB(16, 8, 16, 4),\\n child: Text(\\"译文:\\", textAlign: TextAlign.left),\\n ),\\n Padding(\\n padding: EdgeInsets.fromLTRB(16, 8, 16, 4),\\n child: Text(\\n \\"年少时离乡老年才归家,我的乡音虽未改变,但鬓角的毛发却已经疏落。家乡的儿童们看见我,没有一个认识我。他们笑着询问我:你是从哪里来的呀?\\",\\n ),\\n ),\\n Padding(\\n padding: EdgeInsets.fromLTRB(16, 8, 8, 4),\\n child: Text(\\"创作背景:\\"),\\n ),\\n Padding(\\n padding: EdgeInsets.fromLTRB(16, 8, 16, 4),\\n child: SizedBox(\\n width: 360,\\n child: Text(\\n \\"贺知章在公元744年(天宝三载),辞去朝廷官职,告老返回故乡越州永兴(今浙江萧山),时已八十六岁,这时,距他中年离乡已经很久了,此诗便作于此时。\\",\\n ),\\n ),\\n ),\\n ],\\n ),\\n Row(\\n crossAxisAlignment: CrossAxisAlignment.start,\\n spacing: 10,\\n children: [\\n Padding(\\n padding: EdgeInsets.fromLTRB(16, 0, 0, 0),\\n // Image.asset方法里设置的图片路径是相对于pubspec.yaml文件\\n child: Image.asset(\\n \'assets/images/layout_01@3x.png\',\\n width: 160,\\n alignment: Alignment.center,\\n ),\\n ),\\n // 设置Text Widget离右侧屏幕距离为16\\n // 1. Expanded占满了父Widget的剩余空间\\n // 2. Container size 包装Text Widget\\n // 3. margin设置控制右边距\\n Expanded(\\n child: Column(\\n spacing: 10,\\n crossAxisAlignment: CrossAxisAlignment.start,\\n children: [\\n Container(\\n // EdgeInsets.only: 仅设置一个方向的间距\\n margin: EdgeInsets.only(right: 16),\\n // L:Left,T:Top,R:Right,B:Bottom 通过英文来记忆4参数\\n // padding: EdgeInsets.fromLTRB(0, 0, 16,0),\\n padding: EdgeInsets.only(right: 16),\\n decoration: BoxDecoration(border: Border.all()),\\n child: Text(\\n \\"问题:请问这首诗表达了作者的什么样的感情?\\",\\n maxLines: 4,\\n softWrap: true,\\n ),\\n ),\\n Container(\\n margin: EdgeInsets.only(right: 30),\\n child: Text(\\n \\"如果现在是北京时间早上八点整,我飞往巴黎,到达后巴黎当地时间为早上八点整,请问:我的生命相对延长了吗?\\",\\n ),\\n ),\\n Text(\\"答:你把表的电池扣了你是不是就不死了?\\"),\\n ],\\n ),\\n ),\\n ],\\n ),\\n ],\\n ),\\n ),\\n );\\n }\\n
\\n名称 | 说明 |
---|---|
Column | 垂直布局,可以包含多个子Widget,子Widget从上往下延伸 |
Row | 水平布局,可以包含多个子Widget,子Widget从左往右延伸 |
Container | 包含一个子Widget,一般用来包装显示的Widget,在这里设置宽高,内外边距 |
Padding | 包含一个子Widget,控制内边距 |
SizedBox | 包含一个子Widget,固定大小 |
Expanded | 包含一个子Widget,填充父Widget的剩余空间 |
使用 GridView 将 widget 作为二维列表展示。 GridView 提供两个预制的列表,或者你可以自定义网格。当 GridView 检测到内容太长而无法适应渲染盒时,它就会自动支持滚动。
\\nGridView的特点
\\nGridView.count
设置列的数量/ GridView.extent
设置单元格最大宽度Widget _buildGrid() => GridView.extent(\\n // 单元格最大宽度,因为子Widget是从上往下延伸,所以主轴是上下,交叉轴是左右,子Widget左右方向的是宽度\\n maxCrossAxisExtent: 150,\\n // 上下左右内边距\\n padding: const EdgeInsets.all(4),\\n // 上下单元格间距\\n mainAxisSpacing: 4,\\n // 左右单元格间距\\n crossAxisSpacing: 4,\\n children: _buildGridTileList(30),\\n);\\n\\n// 函数支持箭头形式的简写,当函数体只有一行表达式时,可以通过=>简写表示返回表达式的值。\\n// 从0递增到count-1获取assets/images/pic$i.jpg的图标作为GridView的children\\n// Generates a list of values.\\n// Creates a list with [length] positions and fills it with values created by calling [generator] \\n// for each index in the range 0 .. length - 1 in increasing order.\\nList<Widget> _buildGridTileList(int count) =>\\n List.generate(count, (i) => Image.asset(\'assets/images/pic$i.jpg\'));\\n
\\nListView
是一个和Column
相似的Widget
,当内容大于自己的渲染框时,就会自动支持滚动
ListView
的特点
Column
Column
的配置少,使用更容易,且支持滚动Widget _buildList() {\\n return ListView(\\n children: [\\n _tile(\'CineArts at the Empire\', \'85 W Portal Ave\', Icons.theaters),\\n _tile(\'The Castro Theater\', \'429 Castro St\', Icons.theaters),\\n _tile(\'Alamo Drafthouse Cinema\', \'2550 Mission St\', Icons.theaters),\\n _tile(\'Roxie Theater\', \'3117 16th St\', Icons.theaters),\\n _tile(\\n \'United Artists Stonestown Twin\',\\n \'501 Buckingham Way\',\\n Icons.theaters,\\n ),\\n _tile(\'AMC Metreon 16\', \'135 4th St #3000\', Icons.theaters),\\n // 分割线\\n const Divider(),\\n _tile(\'K\\\\\'s Kitchen\', \'757 Monterey Blvd\', Icons.restaurant),\\n _tile(\'Emmy\\\\\'s Restaurant\', \'1923 Ocean Ave\', Icons.restaurant),\\n _tile(\'Chaiya Thai Restaurant\', \'272 Claremont Blvd\', Icons.restaurant),\\n _tile(\'La Ciccia\', \'291 30th St\', Icons.restaurant),\\n ],\\n );\\n }\\n \\n ListTile _tile(String title, String subtitle, IconData icon) {\\n return ListTile(\\n title: Text(\\n title,\\n style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 20),\\n ),\\n subtitle: Text(subtitle),\\n leading: Icon(icon, color: Colors.blue[500]),\\n );\\n }\\n
\\n里面每一行都是ListTile
,它是Material
库中专用的行Widget
,它可以很轻松的创建一个包含三行文本以及可选的行前和行尾图标的行。 ListTile
在 Card
或者 ListView
中最常用,但是也可以在别处使用。
Stack
布局用于叠加场景,比如图片上添加文字描述
Stack
的特点
Widget
-- 类似栈:后进(渲染)先出(显示在外层)Widget
是基础Widget
;后面的子项覆盖在基础Widget
的顶部Stack
的内容是无法滚动的Material
库中的Card
包含相关有价值信息,几乎可以由任何Widget
组成,但是通常和ListTile
一起使用,Card
只有一个子项,这个子项可以是列,行,列表,网格或者其它支持多个子项的Widget
。默认情况下,Card
的大小是0x0像素。可以使用SizeBox
控制Card
的大小
在 Flutter
中,Card
有轻微的圆角和阴影来使它具有 3D
效果。改变 Card
的 elevation
属性可以控制阴影效果
Widget _buildCard() {\\n return SizedBox(\\n height: 210,\\n child: Card(\\n child: Column(\\n children: [\\n ListTile(\\n title: const Text(\\n \'1625 Main Street\',\\n style: TextStyle(fontWeight: FontWeight.w500),\\n ),\\n subtitle: const Text(\'My City, CA 99984\'),\\n leading: Icon(Icons.restaurant_menu, color: Colors.blue[500]),\\n ),\\n const Divider(),\\n ListTile(\\n title: const Text(\\n \'(408) 555-1212\',\\n style: TextStyle(fontWeight: FontWeight.w500),\\n ),\\n leading: Icon(Icons.contact_phone, color: Colors.blue[500]),\\n ),\\n ListTile(\\n title: const Text(\'costa@example.com\'),\\n leading: Icon(Icons.contact_mail, color: Colors.blue[500]),\\n ),\\n ],\\n ),\\n ),\\n );\\n }\\n
\\nMainAxisAlignment
(主轴)就是与当前子Widget
延伸方向一致的轴,而CrossAxisAlignment
(交叉轴)就是与当前子Widget
延伸方向垂直的轴
比如Column
: 子Widget
是从上往下延伸的,所以主轴就是垂直方向,交叉轴就是水平方向的。\\n即Column
的 MainAxisAlignment
属性用于控制子Widget
在主轴(垂直轴)上的对齐方式,Column
的 CrossAxisAlignment
属性用于控制子Widget
在交叉轴(水平轴)上的对齐方式
当设置为CrossAxisAlignment.start
时表示Column
中的元素左对齐。
通过快捷键 ⌥(option) + ⇧(shift) + f
可以快速对代码格式化
Flutter
界面会告警提示当前的布局超过多少像素,存在内容不可见的问题
Waiting for another flutter command to release the startup lock
问题: VS Code 创建新的Flutter工程一直卡在Waiting for another flutter command to release the startup lock
方案:
\\n# 执行后关闭VS Code 重新打开再次创建Flutter工程\\nkillall -9 dart\\n
\\n一般是网络原因,我是终端加了端口转发,然后一直访问不了pub.flutter-io.cn
,所以去掉代理
# 去掉代理 + 重新执行 flutter pub get --verbose\\nexport https_proxy=\\nexport http_proxy= \\n
\\n若是不规则图片,如何控制它的事件响应控制:如何控制哪些部分触发点击事件,哪些区域无响应。近期终于找到了解决办法,分享给大家。
\\n实现核心是重写 RenderBox 子类中 hitTest 方法:
\\nbool hitTest(BoxHitTestResult result, {required Offset position}) {\\n
\\n示例效果:黄色部分触发点击事件,点击蓝色线框内的白色区域不会触发点击事件。
\\n1、使用示例
\\nContainer(\\n decoration: BoxDecoration(\\n color: Colors.transparent,\\n border: Border.all(color: Colors.blue),\\n ),\\n child: MyCustomHitTestWidget(\\n radius: 50,\\n color: Colors.orange,\\n onTap: () {\\n DLog.d(\'Custom hit test area tapped!\');\\n },\\n textSpan: TextSpan(\\n text: \'$runtimeType\' * 1,\\n style: TextStyle(\\n color: Colors.yellow,\\n fontSize: 14,\\n ),\\n ),\\n ),\\n),\\n
\\n2、MyCustomHitTestWidget 源码
\\n/// 自定义圆形组件支持 HitTest 自定义\\nclass MyCustomHitTestWidget extends SingleChildRenderObjectWidget {\\n MyCustomHitTestWidget({\\n super.key,\\n required this.radius,\\n required this.color,\\n this.onTap,\\n this.textSpan,\\n this.textPainter,\\n });\\n\\n final double radius;\\n final Color color;\\n final VoidCallback? onTap;\\n final TextSpan? textSpan;\\n final TextPainter? textPainter;\\n\\n @override\\n RenderObject createRenderObject(BuildContext context) {\\n return MyHitTestRenderBox(\\n radius: radius,\\n color: color,\\n onTap: onTap,\\n textSpan: textSpan,\\n textPainter: textPainter,\\n );\\n }\\n\\n @override\\n void updateRenderObject(BuildContext context, covariant MyHitTestRenderBox renderObject) {\\n renderObject\\n ..radius = radius\\n ..color = color\\n ..onTap = onTap\\n ..textSpan = textSpan\\n ..textPainter = textPainter;\\n }\\n}\\n\\nclass MyHitTestRenderBox extends RenderBox {\\n MyHitTestRenderBox({\\n required this.radius,\\n required this.color,\\n this.onTap,\\n this.textSpan,\\n this.textPainter,\\n });\\n\\n double radius;\\n Color color;\\n VoidCallback? onTap;\\n TextSpan? textSpan;\\n TextPainter? textPainter;\\n\\n @override\\n bool hitTest(BoxHitTestResult result, {required Offset position}) {\\n final center = Offset(constraints.maxWidth / 2, constraints.maxHeight / 2);\\n // final r = (position - center).distance <= radius;\\n\\n final rect = RRect.fromRectAndCorners(\\n Rect.fromLTRB(0, 0, radius * 2, radius * 2),\\n topLeft: Radius.circular(radius),\\n topRight: Radius.circular(radius),\\n bottomLeft: Radius.circular(radius),\\n bottomRight: Radius.circular(radius),\\n );\\n\\n final contains = rect.contains(position);\\n if (contains) {\\n result.add(BoxHitTestEntry(this, position));\\n return true;\\n }\\n return false;\\n }\\n\\n @override\\n void handleEvent(PointerEvent event, HitTestEntry entry) {\\n if (event is PointerDownEvent) {\\n onTap?.call();\\n }\\n }\\n\\n @override\\n void performLayout() {\\n size = constraints.constrain(Size(radius * 2, radius * 2));\\n }\\n\\n @override\\n void paint(PaintingContext context, Offset offset) {\\n final canvas = context.canvas;\\n final paint = Paint()\\n ..color = color\\n ..style = PaintingStyle.fill;\\n\\n // Draw the circular hit test area\\n canvas.drawCircle(\\n offset + Offset(radius, radius),\\n radius,\\n paint,\\n );\\n\\n // Draw text at the center\\n final textPainterNew = textPainter ??\\n TextPainter(\\n text: textSpan ??\\n TextSpan(\\n text: \'$runtimeType\',\\n style: TextStyle(\\n color: Colors.red,\\n fontSize: 16,\\n ),\\n ),\\n textDirection: TextDirection.ltr,\\n );\\n textPainterNew.layout(maxWidth: size.width);\\n final textOffset = offset +\\n Offset(\\n (size.width - textPainterNew.width) / 2,\\n (size.height - textPainterNew.height) / 2,\\n );\\n textPainterNew.paint(canvas, textOffset);\\n }\\n}\\n
\\n3、hitTest 返回 true 会相应事件,返回 false 则不响应,这部分代码决定是否响应式事件
\\n当用户进行触摸操作时,Flutter 会通过以下流程分发事件:
\\nRenderObject
上,Flutter 会通过 hitTest
方法来判断当前的触摸位置是否位于该组件的区域内。如果是,它会处理事件。hitTest
检测到触摸点并处理了这个事件,它就会“消费”该事件,其他组件将不再接收到该事件。hitTest
是一个重要的机制,负责将触摸事件分发到对应的 Widget。hitTest
方法来实现自定义的触摸事件处理。要说起 Riverpod 大家肯定不陌生,毕竟也是目前官方推荐的,它的优势或者说特点是什么呢?
\\n不同的人有不同的说法,强大的状态管理,声明式的编程方法,复杂UI的简化,增强工具的支持等等。
\\n但是抛开表看看本质,我们为什么要用Riverpod,是因为它的灵活性,也就是可重用可组合性。
\\n易于可重复使用,高度可组合,易维护。
\\n所以天然上来说我们既然用了 Riverpod 就得是分散式组合式的使用方式,但是实际开发上真的这样就万事大吉了吗?
\\n你这么说的我都不自信了,那么一个页面的功能最佳实践是怎样的呢? 还是举例吧。比如一个 UserProfilePage 页面,我需要展示用户的信息,全部行业列表,分类列表这些信息。
\\n方案一,也就是分散的做法,通过三种不同的ProfileState IndustryState CategoryState 分别对应三种不同的 Provider ,不需要一个 ViewModel/Controller(叫法不同后面统一用ViewModel替代)去统一管理,异步可以选择 AsyncProvider 同步的选择 Provider,直接在页面中通过 watch 去观察三种状态去更新页面。
\\n方案二,集中式管理,我创建一个 UserPorfileState 保存页面的全部状态,再加上一个 UserProfileViewModel 继承自 Notifier,通过 Provider 提供给 UserProfilePage 统一使用。三种数据的获取都写在 UserProfileViewModel 中,通过 UserPorfileState 内部统一定义三种数据,然后在页面中通过 watch 去观察并更新页面。
\\n其实也不是什么稀奇的事情,一种前端开发者习惯写法,一种是移动端开发者习惯写法。都能完成对应的功能。两种方案都有其优点和适用场景。具体选择哪种方案取决于你的具体需求、项目规模和代码维护性。以下是对这两种方案的详细分析。
\\n优点:
\\n缺点:
\\n我们以上面的场景为例:
\\nfinal userProfileProvider = StateNotifierProvider<UserProfileViewModel, UserProfileState>((ref) {\\n return UserProfileViewModel();\\n});\\n\\nclass UserProfileState {\\n final User? user;\\n final List<Industry>? industries;\\n final List<Category>? categories;\\n\\n UserProfileState({this.user, this.industries, this.categories});\\n}\\n\\nclass UserProfileViewModel extends StateNotifier<UserProfileState> {\\n UserProfileViewModel() : super(UserProfileState());\\n\\n Future<void> fetchUserProfile() async {\\n // 获取用户信息\\n final user = await fetchUser();\\n // 获取行业列表\\n final industries = await fetchIndustries();\\n // 获取分类列表\\n final categories = await fetchCategories();\\n\\n state = UserProfileState(\\n user: user,\\n industries: industries,\\n categories: categories,\\n );\\n }\\n}\\n\\nclass UserProfilePage extends ConsumerWidget {\\n @override\\n Widget build(BuildContext context, ScopedReader watch) {\\n final state = watch(userProfileProvider);\\n\\n if (state.user == null || state.industries == null || state.categories == null) {\\n return CircularProgressIndicator();\\n }\\n\\n return Scaffold(\\n // 页面布局逻辑\\n );\\n }\\n}\\n\\n
\\n一个页面一个控制器,在内部统一的处理数据加载与 UI Loading 的逻辑处理,很符合移动端开发的习惯,如果是移动端的开发者很容易就上手。
\\n但是当一些重复的场景每次都要写重复的逻辑,比如支付的发起与校验,类似这种功能我们其实是更方便使用(Service/UserCase叫法不同)单独的服务或用例来承载。
\\n优点:
\\n缺点:
\\nfinal userProvider = FutureProvider<User>((ref) async {\\n return fetchUser();\\n});\\n\\nfinal industriesProvider = FutureProvider<List<Industry>>((ref) async {\\n return fetchIndustries();\\n});\\n\\nfinal categoriesProvider = FutureProvider<List<Category>>((ref) async {\\n return fetchCategories();\\n});\\n\\nclass UserProfilePage extends ConsumerWidget {\\n @override\\n Widget build(BuildContext context, ScopedReader watch) {\\n final userAsyncValue = watch(userProvider);\\n final industriesAsyncValue = watch(industriesProvider);\\n final categoriesAsyncValue = watch(categoriesProvider);\\n\\n return userAsyncValue.when(\\n data: (user) => industriesAsyncValue.when(\\n data: (industries) => categoriesAsyncValue.when(\\n data: (categories) {\\n return Scaffold(\\n // 页面布局逻辑\\n );\\n },\\n loading: () => CircularProgressIndicator(),\\n error: (err, stack) => Text(\'Error: $err\'),\\n ),\\n loading: () => CircularProgressIndicator(),\\n error: (err, stack) => Text(\'Error: $err\'),\\n ),\\n loading: () => CircularProgressIndicator(),\\n error: (err, stack) => Text(\'Error: $err\'),\\n );\\n }\\n}\\n\\n
\\n这里的状态加 when 的用法是 future 的加载状态处理,也可以用更推荐的 maybeWhen:
\\n @override\\n Widget build(BuildContext context, WidgetRef ref) {\\n return Scaffold(\\n appBar: AppBar(\\n backgroundColor: context.isSmallScreen ? null : Colors.transparent,\\n centerTitle: true,\\n title: const Text(\'Favorites\'),\\n ),\\n body: ref.watch(favoritesNotifierProvider).maybeWhen(\\n loading: () => const CircularProgressIndicator(),\\n orElse: () => const SizedBox.shrink(),\\n data: (favorites) {\\n if (favorites.isEmpty) const EmptyView();\\n return Wrap(\\n children: [\\n ...favorites.map((entry) {\\n return Padding(\\n padding: const EdgeInsets.symmetric(\\n horizontal: 5.0,\\n vertical: 5.0,\\n ),\\n child: BookItem(\\n img: entry.link![1].href!,\\n title: entry.title!.t!,\\n entry: entry,\\n ),\\n );\\n }),\\n ],\\n );\\n },\\n ),\\n );\\n }\\n
\\n如果全部使用分散式的我们的加载状态无法统一管理,无法处理并发的任务和顺序和 Loading 加载的状态。除非我们再用一个控制器去管理这三个 Provider,那么本质上就和集中式的处理没什么区别了。
\\n方案一 适合数据和逻辑相对简单的场景,便于集中管理和状态一致性。
\\n方案二 适合想要降低耦合度、提高模块化和复用性的场景,但需要更复杂的状态管理。
\\n综合使用这两种方案能够结合它们的优点,既保持灵活性,又能集中管理。你可以在一个 ViewModel 中调用多个 Provider,并将它们的结果综合到一个统一的状态类中。这样,你可以在一个地方管理页面的整体状态,同时保持各个数据源的独立。
\\n定义数据源 Provider
\\nfinal userProvider = FutureProvider<User>((ref) async {\\n return fetchUser();\\n});\\n\\nfinal industriesProvider = FutureProvider<List<Industry>>((ref) async {\\n return fetchIndustries();\\n});\\n\\nfinal categoriesProvider = FutureProvider<List<Category>>((ref) async {\\n return fetchCategories();\\n});\\n
\\n定义统一的状态类
\\nclass UserProfileState {\\n final User? user;\\n final List<Industry>? industries;\\n final List<Category>? categories;\\n\\n UserProfileState({this.user, this.industries, this.categories});\\n\\n UserProfileState copyWith({\\n User? user,\\n List<Industry>? industries,\\n List<Category>? categories,\\n }) {\\n return UserProfileState(\\n user: user ?? this.user,\\n industries: industries ?? this.industries,\\n categories: categories ?? this.categories,\\n );\\n }\\n}\\n\\n
\\n定义 ViewModel 控制类
\\nclass UserProfileViewModel extends StateNotifier<UserProfileState> {\\n UserProfileViewModel(ProviderReference ref)\\n : super(UserProfileState()) {\\n _initialize(ref);\\n }\\n\\n void _initialize(ProviderReference ref) {\\n ref.listen(userProvider, (userAsyncValue) {\\n userAsyncValue.when(\\n data: (user) {\\n state = state.copyWith(user: user);\\n },\\n loading: () {},\\n error: (err, stack) {\\n // 处理错误\\n },\\n );\\n });\\n\\n ref.listen(industriesProvider, (industriesAsyncValue) {\\n industriesAsyncValue.when(\\n data: (industries) {\\n state = state.copyWith(industries: industries);\\n },\\n loading: () {},\\n error: (err, stack) {\\n // 处理错误\\n },\\n );\\n });\\n\\n ref.listen(categoriesProvider, (categoriesAsyncValue) {\\n categoriesAsyncValue.when(\\n data: (categories) {\\n state = state.copyWith(categories: categories);\\n },\\n loading: () {},\\n error: (err, stack) {\\n // 处理错误\\n },\\n );\\n });\\n }\\n}\\n\\n
\\n提供 ViewModel
\\nfinal userProfileViewModelProvider = StateNotifierProvider<UserProfileViewModel, UserProfileState>((ref) {\\n return UserProfileViewModel(ref);\\n});\\n\\n
\\n页面中使用 ViewModel 和统一状态类
\\nclass UserProfilePage extends ConsumerWidget {\\n @override\\n Widget build(BuildContext context, ScopedReader watch) {\\n final state = watch(userProfileViewModelProvider);\\n\\n if (state.user == null || state.industries == null || state.categories == null) {\\n return CircularProgressIndicator();\\n }\\n\\n return Scaffold(\\n // 页面布局逻辑\\n );\\n }\\n}\\n\\n
\\n这种综合方案结合了集中管理和灵活性的优点,通过在 ViewModel 中调用多个 Provider,并将它们的结果统一到一个状态类中进行管理。这样,你可以在页面中通过一个统一的状态类来观察和更新页面,同时保留各个数据源的独立性和模块化。
\\n当然对于一些通用单独的逻辑我们还是应该使用单独的 Provider 来替代,比如上面我们提到的支付与校验的逻辑,我们就能使用一个 PaymentUserCase / PaymentService 来单独处理。
\\n总的来说思路就是保证每个 Provider 的单一职责,而 ViewModel 作为控制器则整合各个 Provider 对页面状态负责。
\\n这样对于 缓存机制、懒加载、并行请求、错误处理、重用机制都有很好的帮助。
\\n在这篇文章中,我们回顾到 Provider 的移动端集中式的用法和前端分散式的用法,以及他们的优缺点。以及单独 Provider 的状态Loading管理用法。
\\n同时我们探讨了如何结合使用多个 Provider 和 ViewModel 来管理复杂应用中的状态。这种综合方案能够有效结合集中管理与模块化的优点,在保持灵活性的同时,确保数据和逻辑的独立性。
\\n通过定义独立的数据源 Provider 并在 ViewModel 中进行集中管理,我们可以在一个地方维护页面的整体状态。这种设计不仅提高了代码的可维护性,还增强了各个模块的重用性和可测试性。
\\n在优化部分,我们讨论了如何通过缓存机制、懒加载、并行请求等方法来提升应用性能。此外,采用单一责任原则和封装业务逻辑有助于减少代码耦合,提高模块化程度。
\\n在实际应用中,我们可以灵活选择使用各种 Provider /StreamProvider / FutureProvider 来使用,更灵活的实现框架搭建。
\\nOK,那么今天的分享就到这里啦,当然如果你有其他的更多的更好的实现方式,也希望大家能评论区交流一起学习进步。如果我的文章有错别字,不通顺的,或者代码、注释、有错漏的地方,同学们都可以指出修正。
\\n如果感觉本文对你有一点的启发和帮助,还望你能点赞
支持一下,你的支持对我真的很重要。
这一期就此完结了。
\\nTextField
是一个接受键盘输入内容的组件,也就是我们平时口中的input
输入框。它支持的属性非常丰富,不仅可以添加hint输入提示,还可以添加error提示、前缀、后缀以及输入内容校验,格式化等功能。这篇文章就是通过一些简单的示例来了解一下TextField的基本用法以及常用属性。
TextField(\\n decoration: InputDecoration(\\n labelText: \'输入用户名\', // 标签文字\\n border: OutlineInputBorder(), // 边框样式\\n ),\\n onChanged: (value) { // 输入变化时触发\\n print(\'输入内容:$value\');\\n },\\n);\\n
\\n属性 | 作用 | 示例代码 |
---|---|---|
controller | 控制文本内容(获取/设置值) | TextEditingController _controller = TextEditingController(); |
decoration | 自定义输入框外观(边框、提示文字、图标等) | InputDecoration(...) |
keyboardType | 设置键盘类型(如数字、邮箱、多行等) | keyboardType: TextInputType.number |
obscureText | 隐藏输入内容(如密码输入) | obscureText: true |
maxLines | 设置最大行数(null 表示无限行) | maxLines: 3 或 maxLines: null |
onChanged | 输入内容变化时触发的回调 | onChanged: (value) => print(\'输入内容:$value\') |
onSubmitted | 用户提交输入(如点击键盘完成按钮)时触发 | onSubmitted: (value) => print(\'提交内容:$value\') |
inputFormatters | 输入格式化(如限制输入类型或长度) | inputFormatters: [FilteringTextInputFormatter.digitsOnly] |
InputDecoration
提供了非常丰富的输入框样式属性,通过属性的配置基本上可以满足开发中的大部分需求,所以了解InputDecoration
的核心属性以及灵活运用好这些对日常开发还是有很大帮助的。
InputDecoration(\\n hintText: \'请输入内容\', // 输入框提示文字\\n labelText: \'用户名\', // 悬浮标签文字\\n prefixIcon: Icon(Icons.person), // 输入框内左侧图标\\n suffixIcon: Icon(Icons.clear), // 输入框内右侧图标\\n errorText: \'请输入有效内容\', // 错误提示\\n border: OutlineInputBorder(), // 边框样式\\n contentPadding: EdgeInsets.all(12), // 内边距\\n);\\n
\\nborder: OutlineInputBorder(\\n borderRadius: BorderRadius.circular(20),\\n),\\n
\\nborder: InputBorder.none,\\n
\\n通过 ThemeData
或条件判断动态修改样式:
decoration: InputDecoration(\\n enabledBorder: OutlineInputBorder(\\n borderSide: BorderSide(color: Colors.blue),\\n ),\\n focusedBorder: OutlineInputBorder(\\n borderSide: BorderSide(color: Colors.red),\\n ),\\n),\\n
\\n还可以设置errorBorder
, disabledBorder
等,这些自带的属性减少了我们开发过程中去写很多的逻辑去判断。
// 数字键盘\\nTextField(\\n keyboardType: TextInputType.number,\\n);\\n\\n// 邮箱键盘\\nTextField(\\n keyboardType: TextInputType.emailAddress,\\n);\\n\\n// 多行输入\\nTextField(\\n maxLines: null, // 自适应行数\\n keyboardType: TextInputType.multiline,\\n);\\n
\\ninputFormatters: [\\n FilteringTextInputFormatter.digitsOnly,\\n],\\n
\\nmaxLength: 10, // 输入框右下角显示计数器\\nmaxLengthEnforced: true, // 禁止超过长度\\n
\\nTextField(\\n obscureText: true, // 隐藏输入内容\\n decoration: InputDecoration(\\n suffixIcon: IconButton(\\n icon: Icon(Icons.visibility),\\n onPressed: () {}, // 切换显示密码\\n ),\\n ),\\n);\\n
\\nTextFormField
是TextField
的一个扩展,可以方便的完成针对表单中的某个输入框做基础的校验。\\n通过 validator
属性和 Form
组件实现:
// 定义表单键\\nfinal _formKey = GlobalKey<FormState>();\\n\\n// 表单结构\\nForm(\\n key: _formKey,\\n child: Column(\\n children: [\\n TextFormField(\\n validator: (value) {\\n if (value == null || value.isEmpty) {\\n return \'请输入用户名\';\\n }\\n return null;\\n },\\n decoration: InputDecoration(labelText: \'用户名\'),\\n ),\\n ElevatedButton(\\n onPressed: () {\\n if (_formKey.currentState.validate()) {\\n // 验证通过,执行提交\\n }\\n },\\n child: Text(\'提交\'),\\n ),\\n ],\\n ),\\n);\\n
\\nvalidator: (value) {\\n if (value == null || value.isEmpty) {\\n return \'手机号不能为空\';\\n }\\n if (!RegExp(r\'^1[3-9]\\\\d{9}$\').hasMatch(value)) {\\n return \'请输入有效的手机号\';\\n }\\n return null;\\n}\\n
\\n// 声明控制器\\nTextEditingController _controller = TextEditingController();\\n\\n// 设置初始值\\n_controller.text = \'初始文本\';\\n\\n// 获取输入内容\\nString inputValue = _controller.text;\\n\\n// 释放资源\\n@override\\nvoid dispose() {\\n _controller.dispose();\\n super.dispose();\\n}\\n
\\n// 声明焦点节点\\nFocusNode _focusNode = FocusNode();\\n\\n// 跳转焦点\\nFocusScope.of(context).requestFocus(_focusNode);\\n\\n// 监听焦点变化\\n_focusNode.addListener(() {\\n if (_focusNode.hasFocus) {\\n print(\'输入框获得焦点\');\\n }\\n});\\n
\\nimport \'package:flutter/material.dart\';\\n\\nvoid main() {\\n runApp(MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n home: Scaffold(\\n appBar: AppBar(title: Text(\'TextField 示例\')),\\n body: Padding(\\n padding: EdgeInsets.all(16),\\n child: Form(\\n key: _formKey,\\n child: Column(\\n children: [\\n // 带验证的文本输入\\n TextFormField(\\n controller: _usernameController,\\n decoration: InputDecoration(\\n labelText: \'用户名\',\\n prefixIcon: Icon(Icons.person),\\n errorText: _usernameError,\\n ),\\n validator: (value) {\\n if (value == null || value.isEmpty) {\\n return \'请输入用户名\';\\n }\\n return null;\\n },\\n ),\\n SizedBox(height: 16),\\n\\n // 密码输入\\n TextFormField(\\n obscureText: true,\\n decoration: InputDecoration(\\n labelText: \'密码\',\\n suffixIcon: IconButton(\\n icon: Icon(Icons.visibility),\\n onPressed: () {},\\n ),\\n ),\\n ),\\n SizedBox(height: 16),\\n\\n // 数字输入\\n TextField(\\n keyboardType: TextInputType.number,\\n inputFormatters: [FilteringTextInputFormatter.digitsOnly],\\n decoration: InputDecoration(\\n labelText: \'年龄\',\\n border: OutlineInputBorder(),\\n ),\\n ),\\n SizedBox(height: 16),\\n\\n // 多行输入\\n TextField(\\n maxLines: 3,\\n keyboardType: TextInputType.multiline,\\n decoration: InputDecoration(\\n labelText: \'留言\',\\n border: OutlineInputBorder(),\\n ),\\n ),\\n SizedBox(height: 16),\\n\\n // 提交按钮\\n ElevatedButton(\\n onPressed: () {\\n if (_formKey.currentState.validate()) {\\n print(\'提交成功\');\\n }\\n },\\n child: Text(\'提交\'),\\n ),\\n ],\\n ),\\n ),\\n ),\\n ),\\n );\\n }\\n}\\n
\\nFlutter 的 TextField
是处理文本输入的核心组件,通过以下特性实现多样化需求:
InputDecoration
实现丰富的外观效果。keyboardType
、inputFormatters
等限制输入类型。validator
和 Form
实现动态验证。最佳实践:
\\nTextEditingController
管理输入内容。Form
和 TextFormField
。ThemeData
统一主题样式,避免重复代码。obscureText
和安全验证逻辑。近期,大家在 Android 16 的文档里发现了一个名为 「Appfunctions 」 的 API :「Appfunctions 」是一项允许 App 向系统暴露特定功能的机制,这些功能可以被集成到各种系统特性中 。
\\n听起来是不是很熟悉?通过 「Appfunctions 」 App 可以向系统暴露各种各样的功能,并且可以和 Android 的系统服务集成,特别是与应用搜索框架的集成,从而让系统能够发现并索引到可用的 App 功能。
\\n\\n\\n这不就是 Android 上类 MCP 支持么,大胆猜测,这个 API 最大的意义就是为了 Gemini 等 AI App 变得更加强大。
\\n
特别是对于 GenAI 类型的 App ,这些助手可以与设备上安装的其他 App 无缝交互,比如:
\\n就算不针对 AI 场景,在其他场景上也可以联动 App,例如:
\\n在之前需要唤起 App 执行然后再返回的操作,现在可以无缝直接联调,Appfunctions 支持异步处理,调用时 App 会收到成功响应、类似 HTTP 的错误代码或取消通知。
\\n而实现这一功能的核心在于两个关键组件是:AppFunctionService
和 AppFunctionManager
:
AppFunctionService
是一个抽象基类,用户通过创建对应子类来提供具体的应用功能
AppFunctionManager
提供了与应用功能相关的管理功能
当系统需要执行某个应用提供的功能时,会调用 AppFunctionService
中的 onExecuteFunction
方法,开发者需要在这个方法里面,根据 ExecuteAppFunctionRequest
对象包含的功能标识符 (functionIdentifier
) 来执行相应的逻辑 。
\\n\\n需要注意的是
\\nonExecuteFunction
方法运行在主线程,因此不能执行耗时的操作,同时为了保护AppFunctionService
不被任意应用绑定,开发者需要在应用的AndroidManifest.xml
文件中声明该服务,并包含一个 action 为android.app.appfunctions.AppFunctionService
的 intent-filter,同时声明权限android.permission.BIND_APP_FUNCTION_SERVICE
。
而对于 AppFunctionManager
, 核心在于负责管理和调度各个应用暴露的功能,当包发生更改或设备启动时,AppSearchManager
会在设备上为可用函数的元数据编制索引,AppSearch 将索引信息存储为 AppFunctionStaticMetadata
文档,文档包含 functionIdentifier
以及 app 函数实现的架构信息,从而让后续其他 App 可以使用 AppSearch 搜索 API 发现这些函数。
而在需要执行 Appfunctions 时,调用方可以从 AppFunctionStaticMetadata
文档中检索 functionIdentifier
,并使用它来构建 ExecuteAppFunctionRequest
。
然后通过 executeAppFunction(ExecuteAppFunctionRequest, Executor, CancellationSignal, OutcomeReceiver)
异步执行请求来执行应用函数,并且调用方需要 android.permission.EXECUTE_APP_FUNCTIONS
权限声明。
\\n\\n而对于开发者来说,大多数开发人员最终是通过 AppFunctions SDK 实现和调用 Appfunctions
\\n
大致代码可能会如下所示(并非完整真正效果):
\\n\\n\\nclass YourAppFunctionService : AppFunctionService() {\\n override fun onExecuteFunction(\\n request: ExecuteAppFunctionRequest,\\n callingPackage: String,\\n callingPackageSigningInfo: SigningInfo,\\n cancellationSignal: CancellationSignal,\\n callback: OutcomeReceiver<ExecuteAppFunctionResponse, AppFunctionException>\\n ) {\\n val functionIdentifier = request.functionIdentifier\\n when (functionIdentifier) {\\n \\"orderFood\\" -> {\\n // 实现订餐逻辑\\n val result = ExecuteAppFunctionResponse.Builder(ExecuteAppFunctionResponse.RESULT_OK)\\n .setResultDocument(GenericDocument.Builder(\\"resultNamespace\\")\\n .setProperty(\\"orderId\\", \\"12345\\")\\n .build())\\n .build()\\n callback.onResult(result)\\n }\\n else -> {\\n callback.onError(AppFunctionException(AppFunctionException.ERROR_FUNCTION_NOT_FOUND))\\n }\\n }\\n }\\n\\n override fun onBind(intent: Intent?): IBinder? {\\n return object : IAppFunctionService.Stub() {\\n override fun executeAppFunction(\\n request: ExecuteAppFunctionRequest?,\\n callback: IExecuteAppFunctionCallback?\\n ) {\\n if (request!= null && callback!= null) {\\n val safeCallback = OutcomeReceiver<ExecuteAppFunctionResponse, AppFunctionException> { result ->\\n callback.onResult(result)\\n }\\n onExecuteFunction(\\n request,\\n \\"\\", // callingPackage 在这里不直接可用\\n SigningInfo(), // callingPackageSigningInfo 在这里不直接可用\\n CancellationSignal(),\\n safeCallback\\n )\\n }\\n }\\n }\\n }\\n}\\n\\n\\nfun executeFoodOrder(context: Context) {\\n val appFunctionManager = context.getSystemService(AppFunctionManager::class.java)\\n\\n // 假设从 App Search 获取到的 com.example.foodapp 的 \\"orderFood\\" 功能的标识符是 \\"orderFood123\\"\\n val targetPackageName = \\"com.example.foodapp\\"\\n val functionIdentifier = \\"orderFood123\\"\\n\\n val request = ExecuteAppFunctionRequest.Builder(targetPackageName, functionIdentifier)\\n .build()\\n\\n val executor = Executors.newSingleThreadExecutor()\\n val cancellationSignal = CancellationSignal()\\n val callback = object : OutcomeReceiver<ExecuteAppFunctionResponse, AppFunctionException> {\\n override fun onResult(result: ExecuteAppFunctionResponse) {\\n val resultDocument = result.resultDocument\\n val orderId = resultDocument?.getPropertyString(\\"orderId\\")\\n Log.d(\\"AppFunctions\\", \\"订餐成功,订单 ID:$orderId\\")\\n }\\n\\n override fun onError(error: AppFunctionException) {\\n Log.e(\\"AppFunctions\\", \\"执行功能时发生错误:${error.errorCode}\\")\\n }\\n }\\n\\n if (ContextCompat.checkSelfPermission(context, Manifest.permission.EXECUTE_APP_FUNCTIONS) == PackageManager.PERMISSION_GRANTED) {\\n appFunctionManager?.executeAppFunction(request, executor, cancellationSignal, callback)\\n } else {\\n Log.w(\\"AppFunctions\\", \\"未授予 EXECUTE_APP_FUNCTIONS 权限\\")\\n }\\n}\\n
\\n总结下来,整个交互流程:
\\nApp 提供 AppFunctionService
并在 AndroidManifest.xml
中注册
Android 系统检测到新的 AppFunctionService
,并使用 App Search 索引其功能元数据
服务消费者(例如 AI 助手)使用 App Search 搜索实现了特定 schema 的 App Functions
\\n消费者获取目标 App Function 的 functionIdentifier
消费者通过 AppFunctionManager
构建包含目标包名和 functionIdentifier
的 ExecuteAppFunctionRequest
AppFunctionManager
将请求发送给 Android 系统
系统识别出提供服务的应用,并绑定到其 AppFunctionService
。
系统调用提供者应用中 AppFunctionService
的 onExecuteFunction
方法。
提供者应用执行请求的功能,并通过 OutcomeReceiver
回调返回 ExecuteAppFunctionResponse
给系统
系统将响应结果通过消费者应用在调用 executeAppFunction
时提供的 OutcomeReceiver
传递回去
最后,可以看到,Appfunctions 在 Android 上提供了类似 MCP 的能力支持,从而让 Android 上的 AI 应用可以拥有更强大服务能力,当然,也需要担心 MCP 是否会成为广告联盟在背后同步用户信息的全新手段,推测来说这个权限应该会审核更加严格吧?
\\n那么,你对 Appfunctions 怎么看?
\\n\\n\\n版权声明:本人文章仅在掘金平台发布,请勿抄袭搬运,转载请注明作者及原文链接 🦉
\\n阅读提示:网页版带有主题和代码高亮,阅读体验更佳 🍉
\\n
距离上次在掘金发布啵啵音乐 1.0 已经过去一个半月,百度网盘下载量也有一百多次了,github star 数也超过了 100。
\\n完全没人用其实好接受,但是有人用,用的人又不多,就有点难受,因为不能随心所欲的更新和弃更。虽然初衷是满足自己的使用需求,但是也有 bbmusic 的用户转过来了,想着还是尽量利用空闲时间完善 app 的功能。
\\n有一些人提了意见建议,我目前是看情况采纳,毕竟精力和技术都有限。
\\n所以这一个月内,我要实现的重心功能是两个:一是支持合集收藏,这样能够避免占用歌单资源,再一个就是更加方便;二是支持歌词,歌词是音乐 app 基本的核心功能,部分听众对歌词的需求是很强烈的。
\\n除此之外,也做了一些小优化,例如支持检查版本,在线版本更新。
\\n今天就讲讲这些技术实现吧,虽然技术栈是 flutter,但是开发思路都是相同的,web 开发者也不用顾虑,学到就是赚到。
\\n很多开源产品,纯前端的软件都提供了这个功能,可能你很好奇,没有后端咋实现的检查更新?
\\n其实还是 github 提供的能力。
\\n很多开源产品的 github 仓库,你都能看到 github/workflow 文件。
\\n这是 github 的工作流标准文件,通过编写这个工作流文件,我们可以通过某些关键词,在推送仓库时触发定义好的构建流程,你可以理解为 CICD 的简化版,总之就是触发了仓库的远程自动构建,就和我们本地 build 产物是一个意思。
\\n你看不懂这块没关系,意思就是 github 能够帮助我们构建出新版本的 app 并存在 github 上,每个版本都有自己的版本信息和 app 资源。
\\n当我们前端检查新版本时,就是调用 github 的接口,如果有新的版本,我们就能知道,并通知用户更新。
\\n这个地址其实是固定的:
\\nhttps://api.github.com/repos/你的用户名/你的仓库名/releases/latest\\n
\\n每次请求这个地址,会去检查该用户某仓库的最新版本的信息,返回值如下(AI 整理):
\\n{\\n \\"url\\": \\"https://api.github.com/repos/octocat/Hello-World/releases/1\\",\\n \\"assets_url\\": \\"https://api.github.com/repos/octocat/Hello-World/releases/1/assets\\",\\n \\"upload_url\\": \\"https://uploads.github.com/repos/octocat/Hello-World/releases/1/assets{?name,label}\\",\\n \\"html_url\\": \\"https://github.com/octocat/Hello-World/releases/tag/v1.0\\",\\n \\"id\\": 123456,\\n \\"author\\": {\\n \\"login\\": \\"octocat\\",\\n \\"id\\": 1,\\n \\"node_id\\": \\"MDQ6VXNlcjE=\\",\\n \\"avatar_url\\": \\"https://github.com/images/error/octocat_happy.gif\\",\\n \\"gravatar_id\\": \\"\\",\\n \\"url\\": \\"https://api.github.com/users/octocat\\",\\n \\"html_url\\": \\"https://github.com/octocat\\",\\n \\"followers_url\\": \\"https://api.github.com/users/octocat/followers\\",\\n \\"following_url\\": \\"https://api.github.com/users/octocat/following{/other_user}\\",\\n \\"gists_url\\": \\"https://api.github.com/users/octocat/gists{/gist_id}\\",\\n \\"starred_url\\": \\"https://api.github.com/users/octocat/starred{/owner}{/repo}\\",\\n \\"subscriptions_url\\": \\"https://api.github.com/users/octocat/subscriptions\\",\\n \\"organizations_url\\": \\"https://api.github.com/users/octocat/orgs\\",\\n \\"repos_url\\": \\"https://api.github.com/users/octocat/repos\\",\\n \\"events_url\\": \\"https://api.github.com/users/octocat/events{/privacy}\\",\\n \\"received_events_url\\": \\"https://api.github.com/users/octocat/received_events\\",\\n \\"type\\": \\"User\\",\\n \\"site_admin\\": false\\n },\\n \\"node_id\\": \\"MDc6UmVsZWFzZTEyMzQ1Ng==\\",\\n \\"tag_name\\": \\"v1.0\\",\\n \\"target_commitish\\": \\"main\\",\\n \\"name\\": \\"Version 1.0\\",\\n \\"draft\\": false,\\n \\"prerelease\\": false,\\n \\"created_at\\": \\"2023-01-01T12:00:00Z\\",\\n \\"published_at\\": \\"2023-01-01T12:00:00Z\\",\\n \\"assets\\": [\\n {\\n \\"url\\": \\"https://api.github.com/repos/octocat/Hello-World/releases/assets/789012\\",\\n \\"id\\": 789012,\\n \\"node_id\\": \\"MDEyOlJlbGVhc2VBc3NldDc890MTI=\\",\\n \\"name\\": \\"example.zip\\",\\n \\"label\\": \\"\\",\\n \\"uploader\\": {\\n \\"login\\": \\"octocat\\",\\n \\"id\\": 1,\\n \\"node_id\\": \\"MDQ6VXNlcjE=\\",\\n \\"avatar_url\\": \\"https://github.com/images/error/octocat_happy.gif\\",\\n \\"gravatar_id\\": \\"\\",\\n \\"url\\": \\"https://api.github.com/users/octocat\\",\\n \\"html_url\\": \\"https://github.com/octocat\\",\\n \\"followers_url\\": \\"https://api.github.com/users/octocat/followers\\",\\n \\"following_url\\": \\"https://api.github.com/users/octocat/following{/other_user}\\",\\n \\"gists_url\\": \\"https://api.github.com/users/octocat/gists{/gist_id}\\",\\n \\"starred_url\\": \\"https://api.github.com/users/octocat/starred{/owner}{/repo}\\",\\n \\"subscriptions_url\\": \\"https://api.github.com/users/octocat/subscriptions\\",\\n \\"organizations_url\\": \\"https://api.github.com/users/octocat/orgs\\",\\n \\"repos_url\\": \\"https://api.github.com/users/octocat/repos\\",\\n \\"events_url\\": \\"https://api.github.com/users/octocat/events{/privacy}\\",\\n \\"received_events_url\\": \\"https://api.github.com/users/octocat/received_events\\",\\n \\"type\\": \\"User\\",\\n \\"site_admin\\": false\\n },\\n \\"content_type\\": \\"application/zip\\",\\n \\"state\\": \\"uploaded\\",\\n \\"size\\": 10240,\\n \\"download_count\\": 10,\\n \\"created_at\\": \\"2023-01-01T12:05:00Z\\",\\n \\"updated_at\\": \\"2023-01-01T12:05:00Z\\",\\n \\"browser_download_url\\": \\"https://github.com/octocat/Hello-World/releases/download/v1.0/example.zip\\"\\n }\\n ],\\n \\"tarball_url\\": \\"https://api.github.com/repos/octocat/Hello-World/tarball/v1.0\\",\\n \\"zipball_url\\": \\"https://api.github.com/repos/octocat/Hello-World/zipball/v1.0\\",\\n \\"body\\": \\"This is the release notes for version 1.0.\\\\nIt contains some great new features.\\"\\n}\\n
\\n其中的 tag_name
字段就是版本信息,这个版本信息是你自定义的,所以怎么比较版本你可以自行决定。一般我们比较流行的是 semver
版本,所以我们可以写一个版本比较工具,比较用户当前使用的版本和请求来的版本,然后判断是不是该更新。
web 开发生态中,semver
版本比较工具就很多,flutter 中我们也可以自行简单实现一个,或者让 AI 为我们实现一个:
bool _compareVersions(String latest, String current) {\\n final latestParts = latest.replaceAll(\\"v\\", \\"\\").split(\\".\\");\\n final currentParts = current.split(\\".\\");\\n\\n for (int i = 0; i < 3; i++) {\\n final latestPart = int.tryParse(latestParts[i]) ?? 0;\\n final currentPart = int.tryParse(currentParts[i]) ?? 0;\\n\\n if (latestPart > currentPart) {\\n return true;\\n } else if (latestPart < currentPart) {\\n return false;\\n }\\n }\\n\\n return false;\\n}\\n
\\n当然了,这个方法不够完善,比如你的版本加上什么 alpha、beta 的非数字字符就不行,不过可以让 AI 继续完善,这就看个人需求了。
\\n目前是使用第三方免费歌词 API 获取的歌词,避免广告或者其它影响,这里就不给出地址了,感兴趣的可以自己百度下免费歌词 API。
\\n返回的歌词是字符串,会携带标题、作者、每句歌词的时间戳等信息,歌词案例如下:
\\n\\n\\n[ti:海底]\\\\n[ar:阿萨Aza/艾因Eine/艾因Eine]\\\\n[al:]\\\\n[by:]\\\\n[offset:0]\\\\n[00:00.00]海底 - 阿萨Aza/艾因Eine\\\\n[00:23.06]一个猎人她走上海岸\\\\n[00:30.15]\\\\n[00:30.85]潮涌潮枯\\\\n[00:34.14]\\\\n[00:34.80]故乡总在呼唤\\\\n[00:38.38]她的路在身前\\\\n[00:40.63]背负所有苦难\\\\n[00:45.14]\\\\n[00:45.94]她的路还长远\\\\n[00:48.44]有浓雾弥漫\\\\n[00:52.42]\\\\n[00:53.25]她其实看不清\\\\n[00:57.07]未来要往何处去\\\\n[01:01.52]循着咸腥海风\\\\n[01:03.84]踏入一座遗迹\\\\n[01:08.71]\\\\n[01:09.26]这是座死气沉沉的城市\\\\n[01:11.62]街道都一个样子\\\\n[01:13.64]衔着红花的小鸟转身躲进巷子\\\\n[01:16.84]到处是行尸走肉的人\\\\n[01:19.48]甘愿苟且偷生\\\\n[01:21.42]可是人性犹在\\\\n[01:22.82]\\\\n[01:24.64]她说她不敢在大地上流血\\\\n[01:28.46]招致不幸摧毁珍视的一切\\\\n[01:32.32]永远孑然一身\\\\n[01:34.68]与孤独为伴\\\\n[01:38.51]\\\\n[01:41.05]大海的歌谣\\\\n[01:42.77]总在夜里\\\\n[01:43.75]轻轻哼起\\\\n[01:44.86]那熟悉的旋律\\\\n[01:46.67]有什么被唤醒\\\\n[01:48.50]于是她抱起拉琴\\\\n[01:50.51]浪花为她指路\\\\n[01:52.36]身着一袭红裙\\\\n[01:54.14]她不为谁止步\\\\n[01:55.89]\\\\n[01:56.41]人们都说她古怪\\\\n[01:58.08]总是捉摸不定\\\\n[01:59.75]\\\\n[02:00.32]却不知腐败\\\\n[02:01.90]伊比利亚仍然一意孤行\\\\n[02:04.20]危险不断在接近\\\\n[02:05.69]将审判的剑握紧不容任何犹豫\\\\n[02:08.11]贯彻你所认可的正义\\\\n[02:10.06]去看清去怀疑\\\\n[02:11.75]\\\\n[02:15.42]一个歌者她回到海岸\\\\n[02:22.69]\\\\n[02:23.25]路在身前\\\\n[02:26.25]\\\\n[02:26.92]无需哀叹\\\\n[02:30.71]周身鱼群游弋\\\\n[02:32.88]血亲在呼唤\\\\n[02:36.59]\\\\n[02:38.11]歌声漫过天际\\\\n[02:40.61]她在将谁找寻\\\\n[02:45.77]到处是苟延残喘的生灵\\\\n[02:48.35]偷换写好的命运\\\\n[02:50.45]衔着红花的小鸟还困在囚笼里\\\\n[02:53.58]击溃道貌岸然的庄严\\\\n[02:56.13]时机恰好相见\\\\n[02:58.15]深海猎人血脉相连\\\\n[03:00.70]\\\\n[03:01.54]祈祷时群星落寞不敢睁眼\\\\n[03:04.66]\\\\n[03:05.21]落泪时夜晚为她展露笑颜\\\\n[03:08.50]\\\\n[03:09.18]当她不在悲叹\\\\n[03:11.39]故友在眼前\\\\n[03:14.94]\\\\n[03:20.22]潮涌起潮退去\\\\n[03:24.12]抹去深浅足迹\\\\n[03:28.06]潮退去又涌起\\\\n[03:31.92]静谧笼罩废墟\\\\n[03:35.74]潮涌起潮退去\\\\n[03:38.86]\\\\n[03:39.50]困不住搁浅的鲸\\\\n[03:42.91]\\\\n[03:43.42]我就站在这里\\\\n[03:46.70]\\\\n[03:47.28]与命运并驾齐驱\\";
\\n
这里就涉及到歌词的处理,首先要将字符串转换为数组,同时 [01:08.71]
这种时间戳也需要解析出来,这涉及到每句歌词所在时间,需要在播放时利用定时器滚动歌词。
List<Map<String, dynamic>> parseLyrics(String lyrics) {\\n List<Map<String, dynamic>> parsedLyrics = [];\\n List<String> lines = lyrics.split(\'\\\\n\');\\n for (String line in lines) {\\n RegExp regExp = RegExp(r\'\\\\[(\\\\d{2}):(\\\\d{2})\\\\.(\\\\d{2})\\\\](.*)\');\\n Match? match = regExp.firstMatch(line);\\n if (match != null) {\\n int minutes = int.parse(match.group(1)!);\\n int seconds = int.parse(match.group(2)!);\\n int milliseconds = int.parse(match.group(3)!);\\n String text = match.group(4)!;\\n if (text.trim().isNotEmpty) {\\n int totalMilliseconds = (minutes * 60 * 1000) + (seconds * 1000) + milliseconds;\\n parsedLyrics.add({\\n \'time\': totalMilliseconds,\\n \'text\': text,\\n });\\n }\\n }\\n }\\n parsedLyrics.sort((a, b) => a[\'time\'].compareTo(b[\'time\']));\\n return parsedLyrics;\\n}\\n
\\n我刚开始其实自己实现了一版歌词滚动器,不过效果不理想,后来还是使用了 flutter_lyric
插件实现。
import \\"package:bobomusic/constants/cache_key.dart\\";\\nimport \\"package:bobomusic/db/db.dart\\";\\nimport \\"package:bobomusic/modules/player/model.dart\\";\\nimport \\"package:bobomusic/modules/player/utils.dart\\";\\nimport \\"package:bobomusic/origin_sdk/origin_types.dart\\";\\nimport \\"package:bot_toast/bot_toast.dart\\";\\nimport \\"package:flutter/material.dart\\";\\nimport \\"package:flutter_lyric/lyric_ui/ui_netease.dart\\";\\nimport \\"package:flutter_lyric/lyrics_model_builder.dart\\";\\nimport \\"package:flutter_lyric/lyrics_reader_model.dart\\";\\nimport \\"package:flutter_lyric/lyrics_reader_widget.dart\\";\\nimport \'dart:async\';\\nimport \\"package:provider/provider.dart\\";\\nimport \\"package:shared_preferences/shared_preferences.dart\\";\\n\\nFuture<int?> getMusicPosition() async {\\n final localStorage = await SharedPreferences.getInstance();\\n final pos = localStorage.getInt(\\n CacheKey.playerPosition,\\n );\\n return pos;\\n}\\n\\nfinal db = DBOrder(version: 2);\\n\\nclass LyricsScroller extends StatefulWidget {\\n const LyricsScroller({super.key});\\n\\n @override\\n LyricsScrollerState createState() => LyricsScrollerState();\\n}\\n\\nclass LyricsScrollerState extends State<LyricsScroller> with SingleTickerProviderStateMixin {\\n String lyric = \\"\\";\\n List<Map<String, dynamic>> parsedLyrics = [];\\n int currentLine = 0;\\n Timer? _timer;\\n int currentTime = 0;\\n late LyricsReaderModel lyricModel;\\n var lyricUI = UINetease();\\n\\n @override\\n void initState() {\\n super.initState();\\n // 初始化一个空的 lyricModel\\n lyricModel = LyricsModelBuilder.create().getModel();\\n\\n WidgetsBinding.instance.addPostFrameCallback((_) {\\n doScroll();\\n });\\n }\\n\\n @override\\n void dispose() {\\n _timer?.cancel();\\n lyric = \\"\\";\\n parsedLyrics = [];\\n currentLine = 0;\\n currentTime = 0;\\n super.dispose();\\n }\\n\\n Future<void> doScroll() async {\\n final player = context.read<PlayerModel>();\\n\\n List<Map<String, dynamic>> dbMusic = [];\\n\\n dbMusic = await db.queryByParam(player.current!.orderName, player.current!.playId);\\n\\n if (dbMusic.isEmpty) {\\n dbMusic = await db.queryByParam(player.current!.orderName, player.current!.id);\\n }\\n\\n if (dbMusic.isEmpty) {\\n BotToast.showText(text: \\"找不到歌词 QAQ\\");\\n return;\\n }\\n\\n if (dbMusic.isNotEmpty) {\\n final MusicItem musicItem = row2MusicItem(dbRow: dbMusic[0]);\\n\\n setState(() {\\n lyric = musicItem.lyric;\\n parsedLyrics = parseLyrics(lyric);\\n lyricModel = LyricsModelBuilder.create()\\n .bindLyricToMain(lyric)\\n .getModel();\\n });\\n }\\n\\n final initialPosition = await getMusicPosition();\\n\\n // 找到初始位置对应的歌词行\\n for (int i = 0; i < parsedLyrics.length; i++) {\\n if (parsedLyrics[i][\\"time\\"] > initialPosition) {\\n currentLine = i > 0? i - 1 : 0;\\n break;\\n }\\n if (i == parsedLyrics.length - 1) {\\n currentLine = i;\\n }\\n }\\n\\n currentTime = parsedLyrics[currentLine][\\"time\\"];\\n\\n if (!player.isPlaying) {\\n return;\\n }\\n\\n startTimer();\\n }\\n\\n void startTimer() {\\n _timer = Timer.periodic(const Duration(milliseconds: 100), (timer) {\\n setState(() {\\n currentTime += 100;\\n });\\n });\\n }\\n\\n void moveLyricsBackward() {\\n BotToast.showText(text: \\"-0.5s\\");\\n setState(() {\\n currentTime -= 500;\\n // 重新找到后退后的歌词行\\n for (int i = 0; i < parsedLyrics.length; i++) {\\n if (parsedLyrics[i][\\"time\\"] > currentTime) {\\n currentLine = i > 0? i - 1 : 0;\\n break;\\n }\\n if (i == parsedLyrics.length - 1) {\\n currentLine = i;\\n }\\n }\\n });\\n }\\n\\n void moveLyricsForward() {\\n BotToast.showText(text: \\"+0.5s\\");\\n setState(() {\\n currentTime += 500;\\n // 重新找到前进后的歌词行\\n for (int i = 0; i < parsedLyrics.length; i++) {\\n if (parsedLyrics[i][\\"time\\"] > currentTime) {\\n currentLine = i > 0? i - 1 : 0;\\n break;\\n }\\n if (i == parsedLyrics.length - 1) {\\n currentLine = i;\\n }\\n }\\n });\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n final primaryColor = Theme.of(context).primaryColor;\\n\\n return Stack(\\n children: [\\n Consumer<PlayerModel>(\\n builder: (context, player, child) {\\n return Container(\\n padding: const EdgeInsets.only(top: 20),\\n child: LyricsReader(\\n padding: const EdgeInsets.symmetric(horizontal: 40),\\n model: lyricModel,\\n position: currentTime,\\n lyricUi: lyricUI,\\n playing: player.isPlaying,\\n size: Size(double.infinity, MediaQuery.of(context).size.height - 160),\\n emptyBuilder: () => Center(\\n child: Text(\\n \\"没有歌词\\",\\n style: lyricUI.getOtherMainTextStyle(),\\n ),\\n ),\\n ),\\n );\\n },\\n ),\\n Positioned(\\n left: 50,\\n bottom: 40,\\n child: InkWell(\\n onTap: () {\\n moveLyricsBackward();\\n },\\n child: Icon(Icons.keyboard_double_arrow_left_rounded, color: primaryColor, size: 30),\\n ),\\n ),\\n Positioned(\\n right: 50,\\n bottom: 40,\\n child: InkWell(\\n onTap: () {\\n moveLyricsForward();\\n },\\n child: Icon(Icons.keyboard_double_arrow_right_rounded, color: primaryColor, size: 30),\\n ),\\n ),\\n ],\\n );\\n }\\n}\\n
\\n目前其实也只算是个能用版,在 UI 上其实还有很大改进的地方。使用 flutter_lyric
来实现后,我们节省了很大的开发成本,不用再自己考虑如何滚动,滚动高度,如何快速跳转,如何实现文字逐字高亮,这些要实现起来,确实要考虑很多细节。
还有另一个歌词插件 mmoo_lyric
,看了演示似乎也不错,只是其最近一次更新也是三年前了,其也是基于 flutter_lyric
修改的,感兴趣的可以试试。
今天这篇严格意义上不是技术文章,水一篇给大家介绍下啵啵音乐的 2.0 版本,也欢迎大家使用以及提意见,或者参与开发。
\\n啵啵音乐仓库:啵啵音乐,欢迎 star。
\\n下载地址:
\\n 🔥🔥🔥2.5W字!8个场景问题!带你了解最实用的 git 操作!!! 40+
👍🏻 50+
💚
爆肝两个月,我用flutter开发了一款免费音乐app 80+
👍🏻 102+
💚
搭建一个快速开发油猴脚本的前端工程 24+
👍🏻 42+
💚
金九银十招聘季,IT 打工人,该怎么识别烂公司好公司? 70+
👍🏻 80+
💚
别人休息我努力,悄悄写个 cli 工具,必须提升效率,skr~ 60+
👍🏻 110+
💚
一文掌握 eslint,再也不怕项目报错 20+
👍🏻 30+
💚
开发一个 npm 库应该做哪些工程配置? 40+
👍🏻 50+
💚
分享我在前端学习与开发中用到的神仙网站和工具 40+
👍🏻 110+
💚
uniapp 踩坑记录(二) 130+
👍🏻 150+
💚
闲来无事,摸鱼时让 chatgpt 帮忙,写了一个 console 样式增强库并发布 npm 100+
👍🏻 110+
💚
uniapp 初体验踩坑记录 30+
👍🏻 60+
💚
两小时学会 JS 正则表达式,终身不忘 50+
👍🏻
【一年前端必知必会】如何写出简洁清晰的代码 50+
👍🏻
【一年前端必知必会】了解 Blob,ArrayBuffer,Base64 40+
👍🏻 90+
💚
生物识别技术(如指纹识别和面部识别)在移动应用中的使用越来越广泛,为用户提供了便捷且安全的身份验证方式。本文将介绍如何开发一个Flutter生物识别插件,使您能够在应用中轻松集成生物识别功能。
\\n可以使用终端命令或者Android Studio来创建插件项目:
\\nflutter create --template=plugin --platforms=android,ios biometric_authorization\\n
\\n参数说明:
\\n执行该命令会生成一个支持 Android 和 iOS 的 Flutter 插件结构。
\\n在新建Flutter项目时,填写好项目名称,然后要选择Project Type为Plugin,包名可以根据自己的来进行修改,Platforms选择要指定的平台,这里我们要指定的是Android和iOS,选择好后点击创建即可。
\\n创建好插件项目后,默认的项目代码目录结构如下:
\\nbiometric_authorization/\\n├── android/ // Android平台特定实现\\n├── ios/ // iOS平台特定实现 \\n├── lib/ // Dart接口和实现\\n│ ├── biometric_authorization.dart // 主API类\\n│ ├── biometric_authorization_method_channel.dart // 方法通道实现\\n│ └──biometric_authorization_platform_interface.dart // 平台接口定义\\n└── example/ // 示例应用\\n
\\n我们在lib文件夹里面新建一个biometric_type
的dart文件,用来定义生物识别的枚举类型,得到新的目录结构:
biometric_authorization/\\n├── android/ // Android平台特定实现\\n├── ios/ // iOS平台特定实现 \\n├── lib/ // Dart接口和实现\\n│ ├── biometric_authorization.dart // 主API类\\n│ ├── biometric_authorization_method_channel.dart // 方法通道实现\\n│ ├── biometric_authorization_platform_interface.dart // 平台接口定义\\n│ └── biometric_type.dart // 生物识别类型枚举\\n└── example/ // 示例应用\\n
\\n在biometric_type.dart
文件中,定义了一个枚举表示支持的生物识别方法:面部识别(face)和指纹识别(finngerprint),如果不支持则使用none类型。
/// Biometric Type\\n///\\n/// - face: Face ID\\n/// - fingerprint: Touch ID\\n/// - none: None\\nenum BiometricType { face, fingerprint, none }\\n
\\n平台接口定义了插件的核心功能,所有平台特定实现必须实现这些方法,在biometric_authorization_platform_interface.dart
文件中定义。
import \'package:plugin_platform_interface/plugin_platform_interface.dart\';\\n\\nimport \'biometric_authorization_method_channel.dart\';\\nimport \'biometric_type.dart\';\\n\\nabstract class BiometricAuthorizationPlatform extends PlatformInterface {\\n /// Constructs a BiometricAuthorizationPlatform.\\n BiometricAuthorizationPlatform() : super(token: _token);\\n\\n static final Object _token = Object();\\n\\n static BiometricAuthorizationPlatform _instance =\\n MethodChannelBiometricAuthorization();\\n\\n /// The default instance of [BiometricAuthorizationPlatform] to use.\\n ///\\n /// Defaults to [MethodChannelBiometricAuthorization].\\n static BiometricAuthorizationPlatform get instance => _instance;\\n\\n /// Platform-specific implementations should set this with their own\\n /// platform-specific class that extends [BiometricAuthorizationPlatform] when\\n /// they register themselves.\\n static set instance(BiometricAuthorizationPlatform instance) {\\n PlatformInterface.verifyToken(instance, _token);\\n _instance = instance;\\n }\\n\\n Future<String?> getPlatformVersion() {\\n throw UnimplementedError(\'platformVersion() has not been implemented.\');\\n }\\n\\n /// Check if biometric is available on the device.\\n Future<bool> isBiometricAvailable() async {\\n throw UnimplementedError(\\n \'isBiometricAvailable() has not been implemented.\',\\n );\\n }\\n\\n /// Check if biometric is enrolled on the device.\\n Future<bool> isBiometricEnrolled() async {\\n throw UnimplementedError(\'isBiometricEnrolled() has not been implemented.\');\\n }\\n\\n /// Get available biometric types on the device.\\n Future<List<BiometricType>> getAvailableBiometricTypes() async {\\n throw UnimplementedError(\\n \'getAvailableBiometricTypes() has not been implemented.\',\\n );\\n }\\n\\n /// Authenticate with biometric.\\n Future<bool> authenticate({\\n BiometricType biometricType = BiometricType.none,\\n String reason = \\"Authenticate\\",\\n String? title,\\n String? confirmText,\\n bool useCustomUI = false,\\n bool useDialogUI = false,\\n String? cancelText,\\n }) async {\\n throw UnimplementedError(\'authenticate() has not been implemented.\');\\n }\\n}\\n
\\n这里我们定义了四个生物识别的平台接口方法:
\\n这个方法用来检系统是否支持生物识别的功能,放回一个bool类型的值。
\\n这个方法用来检测系统是否录入了生物识别的信息(录入指纹、面部信息)。
\\n这个方法用来放回系统支持的所有的生物识别的类型(面部识别、指纹识别)。
\\n这个方法用来进行生物识别的认证,接收了一些参数,参数具体的说明在后面接口实现中进行说明。
\\n平台接口实现定义了具体的平台方法逻辑,使用 MethodChannel 与原生系统进行通信。在biometric_authorization_method_channel.dart
文件中定义MethodChannelBiometricAuthorization
类并继承自 BiometricAuthorizationPlatform
,并通过MethodChannel实现与原生平台的通信。
import \'package:flutter/foundation.dart\';\\nimport \'package:flutter/services.dart\';\\n\\nimport \'biometric_authorization_platform_interface.dart\';\\nimport \'biometric_type.dart\';\\n\\n/// An implementation of [BiometricAuthorizationPlatform] that uses method channels.\\nclass MethodChannelBiometricAuthorization\\n extends BiometricAuthorizationPlatform {\\n /// The method channel used to interact with the native platform.\\n @visibleForTesting\\n final methodChannel = const MethodChannel(\'biometric_authorization\');\\n\\n @override\\n Future<String?> getPlatformVersion() async {\\n final version = await methodChannel.invokeMethod<String>(\\n \'getPlatformVersion\',\\n );\\n return version;\\n }\\n\\n /// Check if biometric is available on the device.\\n @override\\n Future<bool> isBiometricAvailable() async {\\n final result = await methodChannel.invokeMethod<bool>(\\n \'isBiometricAvailable\',\\n );\\n return result ?? false;\\n }\\n\\n /// Check if biometric is enrolled on the device.\\n @override\\n Future<bool> isBiometricEnrolled() async {\\n final result = await methodChannel.invokeMethod<bool>(\\n \'isBiometricEnrolled\',\\n );\\n return result ?? false;\\n }\\n\\n /// Get available biometric types on the device.\\n ///\\n /// Returns a list of biometric types that are available on the device.\\n @override\\n Future<List<BiometricType>> getAvailableBiometricTypes() async {\\n final result = await methodChannel.invokeMethod<List<dynamic>>(\\n \'getAvailableBiometricTypes\',\\n );\\n\\n if (result == null) {\\n return [];\\n }\\n\\n // Convert the string list to a BiometricType enum list\\n return result.map<BiometricType>((item) {\\n final String type = item.toString();\\n switch (type) {\\n case \'face\':\\n return BiometricType.face;\\n case \'fingerprint\':\\n return BiometricType.fingerprint;\\n case \'none\':\\n default:\\n return BiometricType.none;\\n }\\n }).toList();\\n }\\n\\n /// Initiates biometric authentication using the device\'s biometric sensors.\\n ///\\n /// This method triggers the biometric authentication flow, which can use fingerprint,\\n /// face recognition, or other biometric methods available on the device.\\n ///\\n /// Parameters:\\n /// - [biometricType]: Specifies the type of biometric authentication to use.\\n /// Required on iOS, optional on Android (Android will automatically use available methods).\\n /// Defaults to [BiometricType.none].\\n /// - [reason]: The reason for requesting authentication, displayed to the user.\\n /// Defaults to \\"Authenticate\\".\\n /// - [title]: The title of the authentication dialog. If null, a default title will be used.\\n /// - [confirmText]: The text for the confirmation button in the authentication dialog.\\n /// If null, a default text will be used.\\n /// - [useCustomUI]: Whether to use a custom UI for authentication (true) or the system default UI (false).\\n /// Defaults to false.\\n /// - [useDialogUI]: Whether to use the Dialog UI for authentication (true) or the new UI (false) in Android.\\n /// Defaults to false.\\n /// - [cancelText]: The text for the cancel button in the authentication dialog.\\n /// Only used on Android. If null, a default text (\\"Cancel\\") will be used.\\n ///\\n /// Returns a [Future<bool>] that completes with:\\n /// - true: If authentication was successful\\n /// - false: If authentication failed or was canceled by the user\\n @override\\n Future<bool> authenticate({\\n BiometricType biometricType = BiometricType.none,\\n String reason = \\"Authenticate\\",\\n String? title,\\n String? confirmText,\\n bool useCustomUI = false,\\n bool useDialogUI = false,\\n String? cancelText,\\n }) async {\\n final Map<String, dynamic> arguments = {\\n \'biometricType\': biometricType.name,\\n \'reason\': reason,\\n \'title\': title,\\n \'confirmText\': confirmText,\\n \'useCustomUI\': useCustomUI,\\n \'useDialogUI\': useDialogUI,\\n \'cancelText\': cancelText,\\n };\\n\\n final result = await methodChannel.invokeMethod<bool>(\\n \'authenticate\',\\n arguments,\\n );\\n return result ?? false;\\n }\\n}\\n
\\n在使用getAvailableBiometricTypes
方法时,对返回的类型字符列表进行解析映射成BiometricType
列表。
对于authenticate
方法中的各参数说明如下:
biometricType:指定要使用的生物识别类型,在iOS上必须传递
\\nreason:认证原因说明,用于显示在系统弹窗上
\\ntitle:认证对话框的标题
\\nconfirmText:确认按钮文字
\\ncancelText:取消按钮文字(Android专用)
\\nuseCustomUI:是否使用自定义 UI(默认 false)
\\nuseDialogUI:Android 中是否使用旧的对话框 UI(默认 false)
\\n这些参数会被打包成 Map,传递给原生方法:
\\n在biometric_authorization.dart
文件中定义插件API,供Flutter进行调用使用。
import \'biometric_authorization_platform_interface.dart\';\\nimport \'biometric_type.dart\';\\n\\nclass BiometricAuthorization {\\n Future<String?> getPlatformVersion() {\\n return BiometricAuthorizationPlatform.instance.getPlatformVersion();\\n }\\n\\n /// Check if biometric is available on the device.\\n Future<bool> isBiometricAvailable() {\\n return BiometricAuthorizationPlatform.instance.isBiometricAvailable();\\n }\\n\\n /// Check if biometric is enrolled on the device.\\n Future<bool> isBiometricEnrolled() {\\n return BiometricAuthorizationPlatform.instance.isBiometricEnrolled();\\n }\\n\\n /// Get available biometric types on the device.\\n Future<List<BiometricType>> getAvailableBiometricTypes() {\\n return BiometricAuthorizationPlatform.instance.getAvailableBiometricTypes();\\n }\\n\\n /// Initiates biometric authentication using the device\'s biometric sensors.\\n ///\\n /// This method triggers the biometric authentication flow, which can use fingerprint,\\n /// face recognition, or other biometric methods available on the device.\\n ///\\n /// Parameters:\\n /// - [biometricType]: Specifies the type of biometric authentication to use.\\n /// Required on iOS, optional on Android (Android will automatically use available methods).\\n /// Defaults to [BiometricType.none].\\n /// - [reason]: The reason for requesting authentication, displayed to the user.\\n /// Defaults to \\"Authenticate\\".\\n /// - [title]: The title of the authentication dialog. If null, a default title will be used.\\n /// - [confirmText]: The text for the confirmation button in the authentication dialog.\\n /// If null, a default text will be used.\\n /// - [useCustomUI]: Whether to use a custom UI for authentication (true) or the system default UI (false).\\n /// Defaults to false.\\n /// - [cancelText]: The text for the cancel button in the authentication dialog.\\n /// Only used on Android. If null, a default text (\\"Cancel\\") will be used.\\n ///\\n /// Returns a [Future<bool>] that completes with:\\n /// - true: If authentication was successful\\n /// - false: If authentication failed or was canceled by the user\\n Future<bool> authenticate({\\n BiometricType biometricType = BiometricType.none,\\n String reason = \\"Authenticate\\",\\n String? title,\\n String? confirmText,\\n bool useCustomUI = false,\\n bool useDialogUI = false,\\n String? cancelText,\\n }) {\\n return BiometricAuthorizationPlatform.instance.authenticate(\\n biometricType: biometricType,\\n reason: reason,\\n title: title,\\n confirmText: confirmText,\\n useCustomUI: useCustomUI,\\n useDialogUI: useDialogUI,\\n cancelText: cancelText,\\n );\\n }\\n}\\n
\\n平台特定实现是指在各特定平台上使用原生代码和方法实现插件中的方法,在iOS上一般使用Object-C/Swift,在Android上一般使用 Java/Kotlin。这里我们使用Swift和Kotlin来实现。
\\n在ios文件夹中的Classes里面定义了三个文件,ios文件夹的主要目录结构如下:
\\nios/\\n├── Assets\\n├── Classes/\\n│ ├── BiometricAuthorizationPlugin.swift // 插件入口文件\\n│ ├── BiometricAuthorization.swift // 生物识别功能实现文件\\n│ └── BiometricAuthView.swift // 自定义生物识别的UI\\n└── Resources\\n
\\n这个文件中定义了BiometricAuthorizationPlugin类,它实现了FlutterPlugin 协议,并负责处理从 Dart 端通过 MethodChannel 发起的调用请求。在这里的handle
方法中,call
参数包括了Flutter侧调用的方法和参数,result
参数则是返回参数。在这里的call.method
的名字必须与Dart平台接口实现中的名字一致,否则会进入默认处理逻辑(即 FlutterMethodNotImplemented)。
import Flutter\\nimport UIKit\\n\\n/**\\n * Flutter plugin that provides biometric authentication functionality for iOS.\\n * This plugin serves as the bridge between Flutter and native iOS authentication APIs.\\n *\\n * It implements the FlutterPlugin protocol to handle method calls from Flutter\\n * and delegates the actual biometric operations to the BiometricAuthorization class.\\n */\\npublic class BiometricAuthorizationPlugin: NSObject, FlutterPlugin {\\n /**\\n * Registers this plugin with the Flutter engine.\\n * Sets up the method channel and plugin instance.\\n *\\n * @param registrar The Flutter plugin registrar to register with.\\n */\\n public static func register(with registrar: FlutterPluginRegistrar) {\\n let channel = FlutterMethodChannel(name: \\"biometric_authorization\\", binaryMessenger: registrar.messenger())\\n let instance = BiometricAuthorizationPlugin()\\n registrar.addMethodCallDelegate(instance, channel: channel)\\n }\\n \\n /**\\n * Handles method calls from Flutter.\\n * Routes each method to the appropriate BiometricAuthorization function.\\n *\\n * @param call The method call from Flutter with method name and arguments.\\n * @param result The result callback to send the response back to Flutter.\\n */\\n public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {\\n switch call.method {\\n case \\"getPlatformVersion\\":\\n // Returns the iOS version for plugin verification\\n result(\\"iOS \\" + UIDevice.current.systemVersion)\\n \\n case \\"isBiometricAvailable\\":\\n // Checks if the device has biometric hardware\\n result(BiometricAuthorization.isBiometricAvailable())\\n \\n case \\"isBiometricEnrolled\\":\\n // Checks if biometrics are enrolled on the device\\n result(BiometricAuthorization.isBiometricEnrolled())\\n \\n case \\"getAvailableBiometricTypes\\":\\n // Gets a list of available biometric types on the device\\n result(BiometricAuthorization.getAvailableBiometricTypes())\\n \\n case \\"authenticate\\":\\n // Performs biometric authentication with parameters from Flutter\\n BiometricAuthorization.authenticate(call: call, result: result)\\n \\n default:\\n // Returns not implemented for unknown methods\\n result(FlutterMethodNotImplemented)\\n }\\n }\\n}\\n
\\n这个文件定义了iOS侧生物识别的具体实现,基于LocalAuthentication
实现,通过LAContext
创建上下文对象,使用canEvaluatePolicy
来检查是否支持生物识别。
import Foundation\\nimport LocalAuthentication\\nimport SwiftUI\\nimport Flutter\\n\\n/**\\n * Represents the supported biometric authentication types.\\n * Maps to the corresponding types defined in the Dart code.\\n */\\nenum BiometricType: String {\\n case face = \\"face\\" // Face ID on supported devices\\n case fingerprint = \\"fingerprint\\" // Touch ID on supported devices\\n case none = \\"none\\" // Fallback when no biometric is available\\n}\\n\\n/**\\n * Main class that handles biometric authentication functionality.\\n * Provides methods to check availability, enrollment, and perform authentication.\\n */\\nclass BiometricAuthorization {\\n \\n /**\\n * Creates and configures a new LAContext instance.\\n * \\n * @return A configured LAContext instance ready for biometric operations.\\n */\\n private static func createContext() -> LAContext {\\n let context = LAContext()\\n return context\\n }\\n \\n /**\\n * Checks if biometric authentication is available on the device.\\n * This checks if the hardware supports biometrics (Face ID or Touch ID).\\n * \\n * @return Boolean indicating if biometric authentication is available.\\n */\\n static func isBiometricAvailable() -> Bool {\\n let context = createContext()\\n var error: NSError?\\n let available = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)\\n return available\\n }\\n \\n /**\\n * Checks if biometric authentication is enrolled on the device.\\n * This verifies if the user has registered their biometrics (face or fingerprint).\\n * \\n * @return Boolean indicating if biometrics are enrolled.\\n */\\n static func isBiometricEnrolled() -> Bool {\\n let context = createContext()\\n var error: NSError?\\n _ = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)\\n if let err = error as? LAError {\\n return err.code != .biometryNotEnrolled\\n }\\n return true\\n }\\n \\n /**\\n * Determines which biometric types are available on the device.\\n * \\n * @return Array of strings representing available biometric types.\\n * Returns [\\"none\\"] if no biometrics are available.\\n */\\n static func getAvailableBiometricTypes() -> [String] {\\n let context = createContext()\\n var error: NSError?\\n var biometricTypes: [String] = []\\n \\n if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {\\n switch context.biometryType {\\n case .faceID:\\n biometricTypes.append(BiometricType.face.rawValue)\\n case .touchID:\\n biometricTypes.append(BiometricType.fingerprint.rawValue)\\n default:\\n break\\n }\\n }\\n \\n return biometricTypes.isEmpty ? [BiometricType.none.rawValue] : biometricTypes\\n }\\n \\n /**\\n * Main authentication method called from Flutter through the method channel.\\n * Determines whether to use standard system authentication or custom UI.\\n * \\n * @param call The Flutter method call containing authentication parameters.\\n * @param result The Flutter result callback to return the authentication outcome.\\n */\\n static func authenticate(call: FlutterMethodCall, result: @escaping FlutterResult) {\\n let context = createContext()\\n \\n // Extract parameters from the method call\\n guard let args = call.arguments as? [String: Any],\\n let biometricType = args[\\"biometricType\\"] as? String,\\n let reason = args[\\"reason\\"] as? String\\n else {\\n result(false)\\n return\\n }\\n let title = args[\\"title\\"] as? String\\n let confirmText = args[\\"confirmText\\"] as? String\\n let useCustomUI = args[\\"useCustomUI\\"] as? Bool ?? false\\n \\n let policy = LAPolicy.deviceOwnerAuthenticationWithBiometrics\\n \\n if #available(iOS 13.0, *) {\\n if useCustomUI &&\\n (biometricType == BiometricType.face.rawValue ||\\n biometricType == BiometricType.fingerprint.rawValue) {\\n // Use custom SwiftUI-based authentication UI\\n showCustomUI(\\n biometricType: biometricType,\\n title: title,\\n confirmText: confirmText,\\n context: context,\\n policy: policy,\\n reason: reason,\\n result: result\\n )\\n } else {\\n // Use standard system authentication dialog\\n authenticateStandard(\\n context: context,\\n policy: policy,\\n reason: reason,\\n result: result\\n )\\n }\\n }\\n }\\n \\n /**\\n * Initiates the custom UI authentication flow.\\n * This is a wrapper method that calls the async presentation method.\\n * \\n * @param biometricType The type of biometric to authenticate with.\\n * @param title Optional title for the authentication dialog.\\n * @param confirmText Optional text for the confirm button.\\n * @param context The LAContext instance for biometric operations.\\n * @param policy The authentication policy to use.\\n * @param reason The reason for authentication to display to user.\\n * @param result The Flutter result callback.\\n */\\n @available(iOS 13.0, *)\\n private static func showCustomUI(\\n biometricType: String,\\n title: String?,\\n confirmText: String?,\\n context: LAContext,\\n policy: LAPolicy,\\n reason: String,\\n result: @escaping FlutterResult\\n ) {\\n Task {\\n await presentBiometricSheet(\\n context: context,\\n policy: policy,\\n reason: reason,\\n title: title,\\n confirmText: confirmText,\\n biometricType: biometricType,\\n result: result\\n )\\n }\\n }\\n \\n /**\\n * Presents the custom biometric authentication sheet using SwiftUI.\\n * Handles different iOS versions with appropriate UI adaptations.\\n * \\n * @param context The LAContext instance for biometric operations.\\n * @param policy The authentication policy to use.\\n * @param reason The reason for authentication to display to user.\\n * @param title Optional title for the authentication dialog.\\n * @param confirmText Optional text for the confirm button.\\n * @param biometricType The type of biometric to authenticate with.\\n * @param result The Flutter result callback.\\n */\\n @available(iOS 13.0, *)\\n @MainActor\\n private static func presentBiometricSheet(\\n context: LAContext,\\n policy: LAPolicy,\\n reason: String,\\n title: String?,\\n confirmText: String?,\\n biometricType: String,\\n result: @escaping FlutterResult\\n ) {\\n // Get the key window and root view controller\\n guard let keyWindow = getKeyWindow(),\\n let rootViewController = keyWindow.rootViewController else {\\n authenticateStandard(\\n context: context,\\n policy: policy,\\n reason: reason,\\n result: result\\n )\\n return\\n }\\n \\n // Create the SwiftUI view for biometric authentication\\n let contentView = BiometricAuthView(\\n title: title ?? getBiometricTitle(type: biometricType),\\n reason: reason,\\n buttonText: confirmText ?? \\"Authenticate\\",\\n biometricType: biometricType,\\n onAuthenticate: result\\n )\\n \\n let hostingController = UIHostingController(rootView: contentView)\\n \\n if #available(iOS 15.0, *) {\\n // iOS 15+ uses the new sheet presentation API\\n hostingController.modalPresentationStyle = .pageSheet\\n \\n if let sheet = hostingController.sheetPresentationController {\\n if #available(iOS 16.0, *) {\\n // iOS 16+ supports custom height using fraction\\n sheet.detents = [\\n .custom { context in\\n context.maximumDetentValue * 0.35\\n }\\n ]\\n } else {\\n // iOS 15 only supports predefined detents\\n sheet.detents = [.medium()]\\n hostingController.view.heightAnchor.constraint(\\n equalToConstant: UIScreen.main.bounds.height * 0.35\\n ).isActive = true\\n }\\n \\n sheet.preferredCornerRadius = 25\\n sheet.prefersGrabberVisible = true\\n }\\n } else {\\n // iOS 13-14 uses the older form sheet presentation\\n hostingController.modalPresentationStyle = .formSheet\\n hostingController.preferredContentSize = CGSize(\\n width: UIScreen.main.bounds.width,\\n height: UIScreen.main.bounds.height * 0.35\\n )\\n hostingController.view.backgroundColor = .systemBackground\\n hostingController.view.layer.cornerRadius = 20\\n hostingController.view.clipsToBounds = true\\n }\\n \\n // Present the authentication sheet\\n rootViewController.present(hostingController, animated: true)\\n }\\n \\n /**\\n * Helper method to get the key window in iOS 13+.\\n * \\n * @return The key UIWindow instance or nil if not found.\\n */\\n @available(iOS 13.0, *)\\n @MainActor\\n private static func getKeyWindow() -> UIWindow? {\\n return UIApplication\\n .shared\\n .connectedScenes\\n .compactMap { $0 as? UIWindowScene }\\n .flatMap { $0.windows }\\n .first { $0.isKeyWindow }\\n }\\n \\n /**\\n * Returns the appropriate title for the authentication dialog based on biometric type.\\n * \\n * @param type The biometric type string.\\n * @return A user-friendly title for the authentication dialog.\\n */\\n private static func getBiometricTitle(type: String) -> String {\\n switch type {\\n case BiometricType.face.rawValue:\\n return \\"Face ID Authentication\\"\\n case BiometricType.fingerprint.rawValue:\\n return \\"Touch ID Authentication\\"\\n default:\\n return \\"Biometric Authentication\\"\\n }\\n }\\n \\n /**\\n * Performs standard system biometric authentication without custom UI.\\n * \\n * @param context The LAContext instance for biometric operations.\\n * @param policy The authentication policy to use.\\n * @param reason The reason for authentication to display to user.\\n * @param result The Flutter result callback.\\n */\\n @available(iOS 13.0, *)\\n private static func authenticateStandard(\\n context: LAContext,\\n policy: LAPolicy,\\n reason: String,\\n result: @escaping FlutterResult\\n ) {\\n context.evaluatePolicy(policy, localizedReason: reason) { success, error in\\n Task {\\n if success {\\n result(true)\\n } else {\\n result(false)\\n }\\n }\\n }\\n }\\n}\\n
\\n这个文件定义了自定义生物识别的UI样式,就是自定义了一个底部的弹出,使用了SwiftUI。
\\nimport SwiftUI\\nimport LocalAuthentication\\n\\n/**\\n * A SwiftUI view that provides a custom UI for biometric authentication.\\n * This view displays a stylish interface with an animated biometric icon,\\n * title, and authentication button.\\n *\\n * The view adapts to different screen sizes through responsive design.\\n * Compatible with iOS 13.0 and later.\\n */\\n@available(iOS 13.0, *)\\nstruct BiometricAuthView: View {\\n // Animation state variables\\n @State private var isAnimating = false\\n @State private var isAuthenticating = false\\n \\n // Environment access to dismiss the view\\n @Environment(\\\\.presentationMode) var presentationMode\\n \\n // View configuration properties\\n var title: String\\n var reason: String\\n var buttonText: String\\n var biometricType: String\\n var onAuthenticate: (Bool) -> Void\\n \\n var body: some View {\\n // GeometryReader allows the view to adapt to different screen sizes\\n GeometryReader { geometry in\\n let width = geometry.size.width\\n let height = geometry.size.height\\n let iconSize = min(width, height) * 0.35\\n let fontSize = min(width, height) * 0.15\\n \\n VStack(spacing: height * 0.03) {\\n // Title text\\n Text(title)\\n .font(.headline)\\n .fontWeight(.semibold)\\n .lineLimit(2)\\n .multilineTextAlignment(.center)\\n \\n Spacer()\\n .frame(height: height * 0.02)\\n \\n // Animated biometric icon\\n ZStack {\\n // Background circle\\n Circle()\\n .fill(Color.blue.opacity(0.1))\\n .frame(width: iconSize, height: iconSize)\\n \\n // Biometric icon (Face ID or Touch ID)\\n Image(systemName: getSystemImageName())\\n .font(.system(size: fontSize))\\n .foregroundColor(.blue)\\n .scaleEffect(isAnimating ? 1.1 : 1.0)\\n .onAppear {\\n // Create a continuous pulse animation\\n withAnimation(\\n Animation.easeInOut(duration: 1.0)\\n .repeatForever(autoreverses: true)\\n ) {\\n isAnimating = true\\n }\\n }\\n }\\n \\n Spacer()\\n .frame(height: height * 0.03)\\n \\n // Authentication button\\n Button {\\n authorizationWithBiometric()\\n } label: {\\n HStack {\\n Text(buttonText)\\n .font(.system(size: fontSize * 0.5))\\n .fontWeight(.semibold)\\n \\n Image(systemName: \\"arrow.right\\")\\n .font(.system(size: fontSize * 0.5))\\n }\\n .frame(maxWidth: .infinity)\\n .padding(EdgeInsets(\\n top: height * 0.05,\\n leading: width * 0.05,\\n bottom: height * 0.05,\\n trailing: width * 0.05\\n ))\\n .background(\\n ZStack {\\n LinearGradient(\\n gradient: Gradient(colors: [\\n Color.blue,\\n Color.blue.opacity(0.8)\\n ]),\\n startPoint: .leading,\\n endPoint: .trailing\\n )\\n }\\n )\\n .foregroundColor(.white)\\n .cornerRadius(15)\\n .shadow(color: Color.blue.opacity(0.3), radius: 5, x: 0, y: 3)\\n }\\n .padding(.horizontal, width * 0.05)\\n .disabled(isAuthenticating)\\n }\\n .padding(width * 0.05)\\n .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)\\n .padding(.top, height * 0.03)\\n }\\n .onDisappear {\\n isAnimating = false\\n }\\n }\\n \\n /**\\n * Determines the system icon name based on the biometric type.\\n *\\n * @return The SF Symbol name for the appropriate biometric type.\\n */\\n func getSystemImageName() -> String {\\n switch biometricType {\\n case \\"face\\":\\n return \\"faceid\\"\\n case \\"fingerprint\\":\\n return \\"touchid\\"\\n default:\\n return \\"person.circle\\"\\n }\\n }\\n \\n /**\\n * Initiates the biometric authentication process when the button is tapped.\\n * Uses LocalAuthentication framework to authenticate with the device biometrics.\\n * The result is communicated back via the onAuthenticate callback.\\n */\\n func authorizationWithBiometric() {\\n isAuthenticating = true\\n let context = LAContext()\\n var error: NSError?\\n \\n if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {\\n context.evaluatePolicy(\\n .deviceOwnerAuthenticationWithBiometrics,\\n localizedReason: reason\\n ) { success, error in\\n Task {\\n isAuthenticating = false\\n if success {\\n // Authentication successful\\n onAuthenticate(true)\\n presentationMode.wrappedValue.dismiss()\\n } else {\\n // Authentication failed\\n onAuthenticate(false)\\n }\\n }\\n }\\n } else {\\n // Device cannot use biometric authentication\\n isAuthenticating = false\\n onAuthenticate(false)\\n }\\n }\\n}\\n
\\n在android文件夹中的mian里面定义了5个文件,android文件夹的主要目录结构如下:
\\nandroid/src/main/kotlin/com/maojiu/biometric_authorization/\\n│ ├── BiometricAuthBottomSheet.kt\\n│ ├── BiometricAuthorizationManager.kt\\n│ ├── BiometricAuthorizationPlugin\\n│ ├── FingerprintAuthDialog.kt\\n│ ├── FingerprintDialogFragment.kt\\n└── AndroidMandiest.xml\\n
\\n这个是Android侧的插件入口文件。
\\n/**\\n * BiometricAuthorizationPlugin.kt\\n *\\n * Main entry point for the Flutter plugin that provides biometric authentication capabilities.\\n * This plugin serves as a bridge between Flutter code and native Android biometric APIs.\\n * It implements necessary Flutter plugin interfaces and handles method calls from the Flutter side.\\n */\\npackage com.maojiu.biometric_authorization\\n\\nimport android.app.Activity\\nimport android.content.Context\\nimport androidx.fragment.app.FragmentActivity\\nimport io.flutter.embedding.engine.plugins.FlutterPlugin\\nimport io.flutter.embedding.engine.plugins.activity.ActivityAware\\nimport io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding\\nimport io.flutter.plugin.common.MethodCall\\nimport io.flutter.plugin.common.MethodChannel\\nimport io.flutter.plugin.common.MethodChannel.MethodCallHandler\\nimport io.flutter.plugin.common.MethodChannel.Result\\n\\n/**\\n * Main plugin class that implements Flutter plugin interfaces.\\n *\\n * This class handles the plugin lifecycle and method calls from Flutter,\\n * and delegates the actual biometric operations to the BiometricAuthorizationManager.\\n * It implements:\\n * - FlutterPlugin: For plugin registration and lifecycle management\\n * - MethodCallHandler: For handling method calls from Flutter\\n * - ActivityAware: For accessing the current activity which is required for UI operations\\n */\\nclass BiometricAuthorizationPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {\\n /**\\n * The MethodChannel that will be used to communicate with the Flutter side\\n */\\n private lateinit var channel: MethodChannel\\n \\n /**\\n * Application context provided by the Flutter engine\\n */\\n private lateinit var context: Context\\n \\n /**\\n * Current activity, needed for UI operations like showing biometric prompts\\n */\\n private var activity: FragmentActivity? = null\\n\\n /**\\n * Called when the plugin is attached to the Flutter engine.\\n * \\n * Sets up the MethodChannel and initializes the context.\\n *\\n * @param flutterPluginBinding Provides access to the Flutter engine\'s resources\\n */\\n override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {\\n channel = MethodChannel(flutterPluginBinding.binaryMessenger, \\"biometric_authorization\\")\\n channel.setMethodCallHandler(this)\\n context = flutterPluginBinding.applicationContext\\n }\\n\\n /**\\n * Called when the plugin is attached to an activity.\\n * \\n * Stores the current activity for later use.\\n *\\n * @param binding Provides access to the current activity\\n */\\n override fun onAttachedToActivity(binding: ActivityPluginBinding) {\\n activity = binding.activity as? FragmentActivity\\n }\\n\\n /**\\n * Called when the plugin is detached from the activity for configuration changes.\\n * \\n * Clears the stored activity reference.\\n */\\n override fun onDetachedFromActivityForConfigChanges() {\\n activity = null\\n }\\n\\n /**\\n * Called when the plugin is reattached to the activity after configuration changes.\\n * \\n * Updates the stored activity reference.\\n *\\n * @param binding Provides access to the current activity\\n */\\n override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {\\n activity = binding.activity as? FragmentActivity\\n }\\n\\n /**\\n * Called when the plugin is detached from the activity.\\n * \\n * Clears the stored activity reference.\\n */\\n override fun onDetachedFromActivity() {\\n activity = null\\n }\\n\\n /**\\n * Handles method calls from the Flutter side.\\n * \\n * This method routes incoming calls to appropriate methods in the BiometricAuthorizationManager.\\n * It first checks if the activity is available before proceeding with the method call.\\n *\\n * @param call The method call from Flutter containing the method name and arguments\\n * @param result The result callback to send the result back to Flutter\\n */\\n override fun onMethodCall(call: MethodCall, result: Result) {\\n val currentActivity = activity\\n if (currentActivity == null) {\\n result.error(\\"ACTIVITY_NOT_AVAILABLE\\", \\"Activity is not available\\", null)\\n return\\n }\\n\\n // Create a manager instance to handle the biometric operations\\n val biometricAuthorizationManager = BiometricAuthorizationManager(context, currentActivity)\\n\\n // Route the method call to the appropriate handler\\n when (call.method) {\\n \\"getPlatformVersion\\" -> {\\n // Return the Android version\\n result.success(\\"Android ${android.os.Build.VERSION.RELEASE}\\")\\n }\\n \\"isBiometricAvailable\\" -> {\\n // Check if biometric authentication is available on the device\\n result.success(biometricAuthorizationManager.isBiometricAvailable())\\n }\\n \\"isBiometricEnrolled\\" -> {\\n // Check if biometric credentials are enrolled on the device\\n result.success(biometricAuthorizationManager.isBiometricEnrolled())\\n }\\n \\"getAvailableBiometricTypes\\" -> {\\n // Get the list of available biometric types on the device\\n result.success(biometricAuthorizationManager.getAvailableBiometricTypes())\\n }\\n \\"authenticate\\" -> {\\n // Initiate the biometric authentication process\\n biometricAuthorizationManager.authenticate(call, result)\\n }\\n else -> {\\n // Method not implemented\\n result.notImplemented()\\n }\\n }\\n }\\n\\n /**\\n * Called when the plugin is detached from the Flutter engine.\\n * \\n * Cleans up resources by removing the method call handler.\\n *\\n * @param binding The binding that was providing access to the Flutter engine\'s resources\\n */\\n override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {\\n channel.setMethodCallHandler(null)\\n }\\n}\\n
\\n这个文件是生物识别的具体实现。
\\n/**\\n * BiometricAuthorizationManager.kt\\n *\\n * Core manager for biometric authentication operations in the Flutter plugin.\\n * This class handles biometric availability checks, enrollment status, and authentication processes.\\n * It supports both standard system UI and custom bottom sheet UI for biometric authentication.\\n */\\n@file:Suppress(\\"DEPRECATION\\")\\n\\npackage com.maojiu.biometric_authorization\\n\\nimport android.annotation.SuppressLint\\nimport android.content.Context\\nimport androidx.biometric.BiometricManager\\nimport android.content.pm.PackageManager\\nimport android.os.Build\\nimport io.flutter.plugin.common.MethodCall\\nimport io.flutter.plugin.common.MethodChannel.Result\\nimport androidx.core.content.ContextCompat\\nimport androidx.biometric.BiometricPrompt\\nimport androidx.core.hardware.fingerprint.FingerprintManagerCompat\\nimport androidx.fragment.app.FragmentActivity\\nimport java.security.KeyStore\\nimport javax.crypto.Cipher\\nimport javax.crypto.KeyGenerator\\nimport javax.crypto.SecretKey\\nimport android.util.Log\\nimport android.security.keystore.KeyGenParameterSpec\\nimport android.security.keystore.KeyProperties\\nimport androidx.fragment.app.DialogFragment\\nimport java.util.concurrent.atomic.AtomicBoolean\\n\\n/**\\n * Enum class representing the supported biometric authentication types.\\n *\\n * @property rawValue String value representation of the biometric type\\n */\\nenum class BiometricType(val rawValue: String) {\\n face(\\"face\\"),\\n fingerprint(\\"fingerprint\\"),\\n none(\\"none\\")\\n}\\n\\n/**\\n * Constants for fingerprint error codes from FingerprintManager\\n * These are not available in FingerprintManagerCompat but are needed for error handling\\n */\\nobject FingerprintConstants {\\n const val FINGERPRINT_ERROR_CANCELED = 5\\n const val FINGERPRINT_ERROR_USER_CANCELED = 10\\n const val FINGERPRINT_ERROR_LOCKOUT = 7\\n const val FINGERPRINT_ERROR_LOCKOUT_PERMANENT = 9\\n}\\n\\n/**\\n * Manager class that handles all biometric authentication operations.\\n *\\n * This class provides methods to check biometric availability, enrollment status,\\n * and handles the authentication flow using either the system UI or a custom UI.\\n *\\n * @param context The application context\\n * @param activity The current activity, needed for UI operations\\n */\\n@Suppress(\\"DEPRECATION\\")\\nclass BiometricAuthorizationManager(\\n private val context: Context,\\n private val activity: FragmentActivity\\n) {\\n /**\\n * use biometricManager to used new UI for biometric authentication\\n * use fingerprintManager to used deprecated UI for fingerprint authentication \\n */\\n private val biometricManager = BiometricManager.from(context)\\n @SuppressLint(\\"RestrictedApi\\")\\n private val fingerprintManager = FingerprintManagerCompat.from(context)\\n\\n private val packageManager: PackageManager = context.packageManager\\n\\n private lateinit var biometricPrompt: BiometricPrompt\\n private lateinit var promptInfo: BiometricPrompt.PromptInfo\\n\\n /**\\n * Checks if biometric authentication is available on the device.\\n *\\n * This method verifies that the device has the necessary hardware and\\n * system support for strong biometric authentication.\\n *\\n * @return true if biometric authentication is available, false otherwise\\n */\\n fun isBiometricAvailable(): Boolean {\\n val canAuthenticateResult = biometricManager.canAuthenticate(\\n BiometricManager.Authenticators.BIOMETRIC_STRONG\\n )\\n return canAuthenticateResult == BiometricManager.BIOMETRIC_SUCCESS\\n }\\n\\n /**\\n * Checks if biometric credentials are enrolled on the device.\\n *\\n * This verifies that the user has set up at least one biometric credential\\n * (fingerprint, face, etc.) that can be used for authentication.\\n *\\n * @return true if biometric credentials are enrolled, false otherwise\\n */\\n fun isBiometricEnrolled(): Boolean {\\n val canAuthenticateResult = biometricManager.canAuthenticate(\\n BiometricManager.Authenticators.BIOMETRIC_STRONG\\n )\\n return canAuthenticateResult == BiometricManager.BIOMETRIC_SUCCESS\\n }\\n\\n /**\\n * Gets a list of available biometric authentication types on the device.\\n *\\n * This method checks which biometric features are supported by the device hardware.\\n * If no biometric features are available, it returns a list containing only \\"none\\".\\n *\\n * @return List of string values representing available biometric types\\n */\\n fun getAvailableBiometricTypes(): List<String> {\\n val availableTypes = mutableListOf<String>()\\n\\n // Check for fingerprint hardware support\\n if (packageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) {\\n availableTypes.add(BiometricType.fingerprint.rawValue)\\n }\\n\\n // Check for face authentication hardware support (Android 10+)\\n if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {\\n if (packageManager.hasSystemFeature(PackageManager.FEATURE_FACE)) {\\n availableTypes.add(BiometricType.face.rawValue)\\n }\\n }\\n\\n // If no biometric types are available, add \\"none\\"\\n if (availableTypes.isEmpty()) {\\n availableTypes.add(BiometricType.none.rawValue)\\n }\\n\\n return availableTypes\\n }\\n\\n /**\\n * Initiates the biometric authentication process based on Flutter method call parameters.\\n *\\n * This method handles the authentication flow using either the standard system UI\\n * or a custom bottom sheet UI based on the useCustomUI parameter.\\n *\\n * @param call The method call from Flutter containing authentication parameters\\n * @param result The result callback to send the authentication result back to Flutter\\n */\\n fun authenticate(call: MethodCall, result: Result) {\\n val args = call.arguments as? Map<*, *>\\n if (args == null) {\\n result.error(\\"INVALID_ARGS\\", \\"Arguments cannot be null\\", null)\\n return\\n }\\n val reason = args[\\"reason\\"] as? String ?: \\"Authenticate required\\"\\n val title = args[\\"title\\"] as? String ?: \\"Biometric Authentication\\"\\n val confirmText = args[\\"confirmText\\"] as? String ?: \\"Authenticate\\"\\n val useCustomUI = args[\\"useCustomUI\\"] as? Boolean ?: false\\n val useDialogUI = args[\\"useDialogUI\\"] as? Boolean ?: false\\n val cancelText = args[\\"cancelText\\"] as? String ?: \\"Cancel\\"\\n\\n try {\\n // Check if biometric authentication is available\\n if (!isBiometricAvailable()) {\\n result.error(\\n \\"BIOMETRIC_UNAVAILABLE\\",\\n \\"Biometric authentication is not available on this device.\\",\\n null\\n )\\n return\\n }\\n // Check if biometric credentials are enrolled\\n if (!isBiometricEnrolled()) {\\n result.error(\\n \\"BIOMETRIC_NOT_ENROLLED\\",\\n \\"No biometric features are enrolled on this device.\\",\\n null\\n )\\n return\\n }\\n\\n if (useCustomUI) {\\n try {\\n // Set up biometric authentication with custom UI\\n setupBiometricAuth(title, reason, cancelText, activity) { success ->\\n try {\\n result.success(success)\\n } catch (e: Exception) {\\n // Ignore exceptions during result callback\\n }\\n }\\n // Show custom bottom sheet UI\\n BiometricAuthBottomSheet(title, confirmText) {\\n try {\\n // Set up biometric authentication with dialog UI\\n if (useDialogUI) {\\n startFingerprintAuth(result, title, cancelText)\\n } else {\\n startBiometricAuth()\\n }\\n } catch (e: Exception) {\\n result.error(\\"BIOMETRIC_ERROR\\", e.message, null)\\n }\\n }.show(activity.supportFragmentManager, \\"biometric_auth_bottom_sheet\\")\\n } catch (e: Exception) {\\n result.error(\\"BIOMETRIC_ERROR\\", \\"Failed to start biometric authentication: ${e.message}\\", null)\\n }\\n } else {\\n try {\\n // Set up biometric authentication with dialog UI\\n if (useDialogUI) {\\n startFingerprintAuth(result, title, cancelText)\\n return\\n }\\n\\n // Set up biometric authentication with standard system UI\\n setupBiometricAuth(title, reason, cancelText, activity) { success ->\\n try {\\n result.success(success)\\n } catch (e: Exception) {\\n // Ignore exceptions during result callback\\n }\\n }\\n // Start authentication with standard UI\\n startBiometricAuth()\\n } catch (e: Exception) {\\n result.error(\\"BIOMETRIC_ERROR\\", \\"Failed to start biometric authentication: ${e.message}\\", null)\\n }\\n }\\n } catch (e: Exception) {\\n result.error(\\"UNEXPECTED_ERROR\\", \\"Error during biometric authentication: ${e.message}\\", null)\\n }\\n }\\n\\n /**\\n * Starts the fingerprint authentication process used with the deprecated UI.\\n *\\n * This method is used when the useDeprecatedUI parameter is set to true.\\n *\\n * Android 10 and above only support fingerprint authentication.\\n * \\n * @param result The result callback to send the authentication result back to Flutter\\n */\\n @SuppressLint(\\"RestrictedApi\\", \\"MissingPermission\\")\\n private fun startFingerprintAuth(result: Result, title: String, cancelText: String) {\\n // Flag to ensure result is called only once\\n val resultSent = AtomicBoolean(false)\\n\\n // Wrapper for result callback to prevent multiple calls\\n val safeResult = object {\\n fun success(value: Any?) {\\n if (resultSent.compareAndSet(false, true)) {\\n activity.runOnUiThread {\\n try { result.success(value) } catch (e: Exception) { Log.w(\\"BiometricAuth\\", \\"Result success error: ${e.message}\\") }\\n }\\n }\\n }\\n fun error(code: String, message: String?, details: Any?) {\\n if (resultSent.compareAndSet(false, true)) {\\n activity.runOnUiThread {\\n try { result.error(code, message, details) } catch (e: Exception) { Log.w(\\"BiometricAuth\\", \\"Result error error: ${e.message}\\") }\\n }\\n }\\n }\\n }\\n\\n // Check if the device supports fingerprint authentication\\n if (!fingerprintManager.isHardwareDetected) {\\n Log.d(\\"BiometricAuth\\", \\"Device does not support fingerprint authentication\\")\\n safeResult.error(\\n \\"FINGERPRINT_UNAVAILABLE\\",\\n \\"Device does not support fingerprint authentication\\",\\n null\\n )\\n return\\n }\\n\\n // Check if there are any enrolled fingerprints\\n if (!fingerprintManager.hasEnrolledFingerprints()) {\\n Log.d(\\"BiometricAuth\\", \\"No fingerprints are enrolled on this device\\")\\n safeResult.error(\\n \\"FINGERPRINT_NOT_ENROLLED\\",\\n \\"No fingerprints are enrolled on this device\\",\\n null\\n )\\n return\\n }\\n\\n // Create a crypto object as an authentication token\\n val cryptoObject = createCryptoObject()\\n if (cryptoObject == null) {\\n Log.d(\\"BiometricAuth\\", \\"Failed to create CryptoObject\\")\\n safeResult.error(\\n \\"FINGERPRINT_CRYPTO_ERROR\\",\\n \\"Failed to create cryptographic object for fingerprint authentication\\",\\n null\\n )\\n return\\n }\\n\\n // Create a cancellation signal for the authentication\\n val cancellationSignal = androidx.core.os.CancellationSignal()\\n\\n // Create authentication callback\\n val callback = object : FingerprintManagerCompat.AuthenticationCallback() {\\n override fun onAuthenticationSucceeded(authResult: FingerprintManagerCompat.AuthenticationResult) {\\n // Authentication succeeded\\n Log.d(\\"BiometricAuth\\", \\"Fingerprint authentication succeeded\\")\\n // Dismiss the dialog if it\'s still showing\\n activity.supportFragmentManager.findFragmentByTag(\\"FingerprintDialogFragment\\")?.let {\\n (it as? DialogFragment)?.dismissAllowingStateLoss()\\n }\\n safeResult.success(true)\\n }\\n\\n override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {\\n // Authentication error\\n Log.d(\\"BiometricAuth\\", \\"Fingerprint authentication error: $errString ($errorCode)\\")\\n // Dismiss the dialog if it\'s still showing\\n activity.supportFragmentManager.findFragmentByTag(\\"FingerprintDialogFragment\\")?.let {\\n (it as? DialogFragment)?.dismissAllowingStateLoss()\\n }\\n when (errorCode) {\\n FingerprintConstants.FINGERPRINT_ERROR_CANCELED,\\n FingerprintConstants.FINGERPRINT_ERROR_USER_CANCELED -> {\\n // User canceled authentication via system prompt or custom dialog cancel\\n safeResult.success(false)\\n }\\n FingerprintConstants.FINGERPRINT_ERROR_LOCKOUT,\\n FingerprintConstants.FINGERPRINT_ERROR_LOCKOUT_PERMANENT -> {\\n // Device is locked out\\n safeResult.success(false) // Reporting false, could be a specific error too\\n }\\n else -> {\\n // Other errors\\n safeResult.error(\\n \\"FINGERPRINT_ERROR\\",\\n errString.toString(),\\n errorCode // Include error code in details\\n )\\n }\\n }\\n }\\n\\n override fun onAuthenticationFailed() {\\n // Authentication failed but can be retried\\n Log.d(\\"BiometricAuth\\", \\"Fingerprint authentication failed but can be retried\\")\\n }\\n }\\n\\n /**\\n * Start fingerprint authentication\\n *\\n * Parameters:\\n * @param crypto: the crypto object\\n * @param flags: optional flags, usually 0 \\n * @param cancel: a cancellation signal object to cancel the authentication\\n * @param callback: the callback that receives authentication results\\n * @param handler: handler for delivering messages, or null for default handler\\n */\\n fingerprintManager.authenticate(\\n cryptoObject,\\n 0,\\n cancellationSignal,\\n callback,\\n null\\n )\\n\\n // --- Display the Custom Dialog --- \\n val fragmentManager = activity.supportFragmentManager\\n // Ensure previous dialog is dismissed if any (e.g., rapid calls)\\n fragmentManager.findFragmentByTag(\\"FingerprintDialogFragment\\")?.let {\\n (it as? DialogFragment)?.dismissAllowingStateLoss()\\n }\\n val dialogFragment = FingerprintDialogFragment.newInstance(title, cancelText) {\\n // onCancel lambda from custom dialog\\n Log.d(\\"BiometricAuth\\", \\"Custom dialog cancelled by user.\\")\\n if (!cancellationSignal.isCanceled) {\\n // IMPORTANT: Calling cancel here will trigger the onAuthenticationError callback\\n // with FINGERPRINT_ERROR_CANCELED. The callback will handle sending the result.\\n cancellationSignal.cancel()\\n }\\n }\\n // Show the dialog. It will appear over the activity while the fingerprint manager attempts auth.\\n dialogFragment.show(fragmentManager, \\"FingerprintDialogFragment\\")\\n }\\n\\n /**\\n * Creates a cryptographic object for fingerprint authentication\\n * \\n * @return Crypto object, or null if creation fails\\n */\\n @SuppressLint(\\"RestrictedApi\\")\\n private fun createCryptoObject(): FingerprintManagerCompat.CryptoObject? {\\n // Early return for devices below Marshmallow\\n if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {\\n Log.e(\\"BiometricAuth\\", \\"Fingerprint authentication requires Android 6.0 or above\\")\\n return null\\n }\\n \\n try {\\n // Create and get Android KeyStore instance\\n val keyStore = KeyStore.getInstance(\\"AndroidKeyStore\\")\\n keyStore.load(null)\\n \\n // Key alias\\n val keyName = \\"com.maojiu.biometric_authorization.key\\"\\n \\n // Check if the key already exists, create it if not\\n if (!keyStore.containsAlias(keyName)) {\\n val keyGenerator = KeyGenerator.getInstance(\\n KeyProperties.KEY_ALGORITHM_AES, \\n \\"AndroidKeyStore\\"\\n )\\n \\n val builder = KeyGenParameterSpec.Builder(\\n keyName,\\n KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT\\n )\\n .setBlockModes(KeyProperties.BLOCK_MODE_CBC)\\n .setUserAuthenticationRequired(true)\\n .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)\\n \\n // Set authentication validity period if API level supports it\\n if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {\\n builder.setInvalidatedByBiometricEnrollment(true)\\n }\\n \\n keyGenerator.init(builder.build())\\n keyGenerator.generateKey()\\n }\\n \\n // Get the key and initialize Cipher\\n val key = keyStore.getKey(keyName, null) as SecretKey\\n val cipher = Cipher.getInstance(\\n KeyProperties.KEY_ALGORITHM_AES + \\"/\\" +\\n KeyProperties.BLOCK_MODE_CBC + \\"/\\" +\\n KeyProperties.ENCRYPTION_PADDING_PKCS7\\n )\\n \\n // Initialize the Cipher for encryption mode\\n cipher.init(Cipher.ENCRYPT_MODE, key)\\n \\n // Create and return CryptoObject\\n return FingerprintManagerCompat.CryptoObject(cipher)\\n } catch (e: Exception) {\\n // Log the error but don\'t throw an exception, return null to indicate crypto object creation failed\\n Log.e(\\"BiometricAuth\\", \\"Failed to create CryptoObject: ${e.message}\\", e)\\n return null\\n }\\n }\\n\\n /**\\n * Starts the biometric authentication process using the configured prompt.\\n *\\n * This method triggers the system biometric authentication dialog.\\n */\\n private fun startBiometricAuth() {\\n biometricPrompt.authenticate(promptInfo)\\n }\\n\\n /**\\n * Sets up the biometric authentication components.\\n *\\n * This method configures the BiometricPrompt with appropriate callbacks and \\n * builds the prompt information with the specified parameters.\\n *\\n * @param title The title to display in the authentication dialog\\n * @param reason The description/reason for requesting authentication\\n * @param cancelText The text for the cancel button\\n * @param activity The activity context for the authentication UI\\n * @param onResult Callback function that receives the authentication result (true for success, false for failure)\\n */\\n private fun setupBiometricAuth(\\n title: String,\\n reason: String,\\n cancelText: String,\\n activity: FragmentActivity,\\n onResult: (Boolean) -> Unit\\n ) {\\n val executor = ContextCompat.getMainExecutor(activity)\\n biometricPrompt = BiometricPrompt(activity, executor,\\n object : BiometricPrompt.AuthenticationCallback() {\\n /**\\n * Called when authentication is successful.\\n */\\n override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {\\n onResult.invoke(true)\\n }\\n\\n /**\\n * Called when authentication fails but can be retried.\\n * This doesn\'t count as a final failure, so we don\'t invoke the result callback\\n * to allow the user to retry.\\n */\\n override fun onAuthenticationFailed() {\\n // Authentication failed but can be retried\\n // Don\'t call onResult here to allow the user to continue trying\\n }\\n\\n /**\\n * Called when an authentication error occurs.\\n *\\n * Handles different error codes and determines appropriate responses:\\n * - User cancellation: Returns false without an exception\\n * - Device lockout: Returns false without an exception\\n * - Other errors: Returns false without an exception\\n *\\n * @param errorCode The error code from BiometricPrompt\\n * @param errString The error message\\n */\\n override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {\\n // Handle different types of errors\\n when (errorCode) {\\n BiometricPrompt.ERROR_CANCELED,\\n BiometricPrompt.ERROR_USER_CANCELED,\\n BiometricPrompt.ERROR_NEGATIVE_BUTTON -> {\\n // User canceled authentication, return false without raising an exception\\n onResult.invoke(false)\\n }\\n BiometricPrompt.ERROR_LOCKOUT,\\n BiometricPrompt.ERROR_LOCKOUT_PERMANENT -> {\\n // Device is locked out, return false without raising an exception\\n onResult.invoke(false)\\n }\\n else -> {\\n // Other errors, return false without raising an exception\\n onResult.invoke(false)\\n }\\n }\\n }\\n })\\n\\n // Build the prompt information with the specified parameters\\n promptInfo = BiometricPrompt.PromptInfo.Builder()\\n .setTitle(title)\\n .setSubtitle(reason)\\n .setNegativeButtonText(cancelText)\\n .setAllowedAuthenticators(\\n BiometricManager.Authenticators.BIOMETRIC_STRONG\\n )\\n .build()\\n }\\n}\\n
\\n这个文件自定义了底部的弹出UI。
\\n/**\\n * BiometricAuthBottomSheet.kt\\n * This file implements a custom bottom sheet dialog for biometric authentication using Jetpack Compose.\\n * It provides a user-friendly UI for biometric authentication with support for both light and dark themes.\\n */\\npackage com.maojiu.biometric_authorization\\n\\nimport android.app.Dialog\\nimport android.os.Bundle\\nimport androidx.compose.animation.core.LinearEasing\\nimport androidx.compose.animation.core.RepeatMode\\nimport androidx.compose.animation.core.animateFloat\\nimport androidx.compose.animation.core.infiniteRepeatable\\nimport androidx.compose.animation.core.rememberInfiniteTransition\\nimport androidx.compose.animation.core.tween\\nimport androidx.compose.foundation.background\\nimport androidx.compose.foundation.isSystemInDarkTheme\\nimport androidx.compose.foundation.layout.Arrangement\\nimport androidx.compose.foundation.layout.Box\\nimport androidx.compose.foundation.layout.Column\\nimport androidx.compose.foundation.layout.Row\\nimport androidx.compose.foundation.layout.Spacer\\nimport androidx.compose.foundation.layout.fillMaxWidth\\nimport androidx.compose.foundation.layout.height\\nimport androidx.compose.foundation.layout.padding\\nimport androidx.compose.foundation.layout.size\\nimport androidx.compose.foundation.layout.width\\nimport androidx.compose.foundation.shape.CircleShape\\nimport androidx.compose.foundation.shape.RoundedCornerShape\\nimport androidx.compose.material.icons.Icons\\nimport androidx.compose.material.icons.automirrored.filled.ArrowForward\\nimport androidx.compose.material.icons.filled.Face\\nimport androidx.compose.material3.Button\\nimport androidx.compose.material3.Icon\\nimport androidx.compose.material3.MaterialTheme\\nimport androidx.compose.material3.Text\\nimport androidx.compose.material3.darkColorScheme\\nimport androidx.compose.material3.lightColorScheme\\nimport androidx.compose.runtime.Composable\\nimport androidx.compose.runtime.getValue\\nimport androidx.compose.ui.Alignment\\nimport androidx.compose.ui.Modifier\\nimport androidx.compose.ui.draw.clip\\nimport androidx.compose.ui.graphics.graphicsLayer\\nimport androidx.compose.ui.platform.ComposeView\\nimport androidx.compose.ui.text.font.FontWeight\\nimport androidx.compose.ui.text.style.TextAlign\\nimport androidx.compose.ui.unit.dp\\nimport androidx.fragment.app.DialogFragment\\nimport androidx.lifecycle.setViewTreeLifecycleOwner\\nimport com.google.android.material.bottomsheet.BottomSheetDialog\\n\\n/**\\n * A bottom sheet dialog fragment that displays a biometric authentication UI.\\n *\\n * This class creates a custom bottom sheet dialog with Jetpack Compose UI that\\n * shows a face icon animation and a confirmation button for biometric authentication.\\n *\\n * @param title The title text to display in the bottom sheet\\n * @param confirmText The text for the confirmation button\\n * @param onConfirmClick Callback function that is triggered when the user clicks the confirm button\\n */\\nclass BiometricAuthBottomSheet(\\n private val title: String,\\n private val confirmText: String,\\n private val onConfirmClick: () -> Unit\\n) : DialogFragment() {\\n /**\\n * Creates and configures the bottom sheet dialog.\\n *\\n * This method initializes the dialog with a Compose UI that automatically adapts\\n * to the system\'s theme (light or dark mode).\\n *\\n * @param savedInstanceState The saved instance state bundle\\n * @return A configured BottomSheetDialog instance\\n */\\n override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {\\n return BottomSheetDialog(requireContext()).apply {\\n setContentView(ComposeView(requireContext()).apply {\\n setViewTreeLifecycleOwner(this@BiometricAuthBottomSheet)\\n\\n // Set up the Compose content with theme support\\n setContent {\\n // Detect if system is in dark mode\\n val isDarkTheme = isSystemInDarkTheme()\\n // Apply the appropriate Material theme based on system settings\\n MaterialTheme(\\n colorScheme = if (isDarkTheme) darkColorScheme() else lightColorScheme()\\n ) {\\n BiometricAuthContent(\\n title = title,\\n confirmText = confirmText,\\n onConfirmClick = {\\n onConfirmClick()\\n dismiss()\\n }\\n )\\n }\\n }\\n })\\n }\\n }\\n}\\n\\n/**\\n * Composable function that defines the content of the biometric authentication bottom sheet.\\n *\\n * This composable creates a UI with a title, an animated face icon, and a confirmation button.\\n * The face icon pulses with a scale animation to draw user attention.\\n *\\n * @param title The title text to display\\n * @param confirmText The text for the confirmation button\\n * @param onConfirmClick Callback function that is triggered when the confirm button is clicked\\n */\\n@Composable\\nfun BiometricAuthContent(\\n title: String,\\n confirmText: String,\\n onConfirmClick: () -> Unit\\n) {\\n // Create an infinite transition for the pulsing animation of the face icon\\n val infiniteTransition = rememberInfiniteTransition(label = \\"infiniteTransition\\")\\n val scale by infiniteTransition.animateFloat(\\n initialValue = 1.0f,\\n targetValue = 1.1f,\\n animationSpec = infiniteRepeatable(\\n animation = tween(durationMillis = 1200, easing = LinearEasing),\\n repeatMode = RepeatMode.Reverse\\n ),\\n label = \\"iconScale\\"\\n )\\n\\n // Main container box with background color that adapts to the theme\\n Box(\\n modifier = Modifier\\n .fillMaxWidth()\\n .background(MaterialTheme.colorScheme.background)\\n ) {\\n // Content column with centered alignment\\n Column(\\n modifier = Modifier\\n .fillMaxWidth()\\n .padding(horizontal = 24.dp, vertical = 12.dp),\\n horizontalAlignment = Alignment.CenterHorizontally\\n ) {\\n // Drag handle at the top of the bottom sheet\\n Box(\\n modifier = Modifier\\n .width(50.dp)\\n .height(5.dp)\\n .clip(\\n RoundedCornerShape(\\n topStart = 8.dp,\\n topEnd = 8.dp,\\n bottomStart = 8.dp,\\n bottomEnd = 8.dp\\n )\\n )\\n .background(\\n MaterialTheme.colorScheme.onBackground.copy(alpha = 0.2f)\\n )\\n )\\n\\n Spacer(modifier = Modifier.height(10.dp))\\n\\n // Title text with theme-appropriate color\\n Text(\\n text = title,\\n style = MaterialTheme.typography.headlineSmall.copy(\\n fontWeight = FontWeight.Bold,\\n color = MaterialTheme.colorScheme.onBackground\\n ),\\n textAlign = TextAlign.Center\\n )\\n\\n Spacer(modifier = Modifier.height(16.dp))\\n\\n // Face icon with pulsing animation\\n Box(\\n contentAlignment = Alignment.Center\\n ) {\\n // Face icon that scales up and down\\n Icon(\\n imageVector = Icons.Default.Face,\\n contentDescription = \\"Biometric Icon\\",\\n modifier = Modifier\\n .size(62.dp)\\n .graphicsLayer(\\n scaleX = scale,\\n scaleY = scale\\n ),\\n tint = MaterialTheme.colorScheme.primary\\n )\\n\\n // Circular background for the face icon\\n Box(\\n modifier = Modifier\\n .size(100.dp)\\n .background(\\n color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f),\\n shape = CircleShape\\n )\\n )\\n }\\n\\n Spacer(modifier = Modifier.height(24.dp))\\n\\n // Confirm button that spans the full width\\n Button(\\n onClick = onConfirmClick,\\n modifier = Modifier\\n .fillMaxWidth()\\n .height(50.dp)\\n ) {\\n // Button content with text and arrow icon\\n Row(\\n horizontalArrangement = Arrangement.Center,\\n verticalAlignment = Alignment.CenterVertically\\n ) {\\n // Button text with appropriate contrast color\\n Text(confirmText, style = MaterialTheme.typography.bodyLarge.copy(\\n color = MaterialTheme.colorScheme.onPrimary\\n ))\\n // Arrow icon that indicates action\\n Icon(\\n imageVector = Icons.AutoMirrored.Filled.ArrowForward,\\n contentDescription = \\"Confirm Icon\\",\\n tint = MaterialTheme.colorScheme.onPrimary,\\n modifier = Modifier\\n .padding(start = 8.dp)\\n .size(22.dp)\\n )\\n }\\n }\\n\\n Spacer(modifier = Modifier.height(12.dp))\\n }\\n }\\n}\\n
\\n这个文件自定义了Dialog对话框认证样式。
\\npackage com.maojiu.biometric_authorization\\n\\nimport androidx.compose.foundation.isSystemInDarkTheme\\nimport androidx.compose.foundation.layout.*\\nimport androidx.compose.material.icons.Icons\\nimport androidx.compose.material.icons.filled.Fingerprint\\nimport androidx.compose.material3.*\\nimport androidx.compose.runtime.Composable\\nimport androidx.compose.ui.Alignment\\nimport androidx.compose.ui.Modifier\\nimport androidx.compose.ui.graphics.Color\\nimport androidx.compose.ui.unit.dp\\nimport androidx.compose.ui.window.Dialog\\n\\n/**\\n * A Composable function that displays a custom dialog for fingerprint authentication.\\n * This dialog shows a fingerprint icon, a title, and a cancel button.\\n * It adapts its color scheme based on the system\'s dark theme setting.\\n *\\n * @param title The main text displayed in the dialog, usually indicating the purpose (e.g., \\"Fingerprint Authentication\\").\\n * @param cancelText The text displayed on the cancel button.\\n * @param onDismissRequest A lambda function invoked when the user attempts to dismiss the dialog\\n * by interacting outside the dialog bounds or pressing the back button.\\n * This is mandatory for the [Dialog] composable.\\n * @param onCancel A lambda function invoked when the user explicitly clicks the cancel button within the dialog.\\n */\\n@Composable\\nfun FingerprintDialog(\\n title: String,\\n cancelText: String,\\n onDismissRequest: () -> Unit = {},\\n onCancel: () -> Unit = {}\\n) {\\n // Apply Material 3 theme, automatically selecting light or dark color scheme\\n // based on the system settings.\\n MaterialTheme(\\n colorScheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme()\\n ) {\\n // The Dialog composable provides the basic dialog window structure.\\n Dialog(onDismissRequest = onDismissRequest) {\\n // Surface provides a background, shape, and elevation for the dialog content.\\n Surface(\\n shape = MaterialTheme.shapes.medium, // Use medium rounded corners defined in the theme.\\n color = MaterialTheme.colorScheme.surface, // Use the theme\'s surface color for the background.\\n modifier = Modifier.padding(16.dp) // Apply padding around the Surface within the Dialog window.\\n ) {\\n // Column arranges its children vertically.\\n Column(\\n modifier = Modifier\\n .padding(horizontal = 16.dp) // Inner padding for the content inside the Surface.\\n .fillMaxWidth(), // Make the column take the full width available within the padding.\\n horizontalAlignment = Alignment.CenterHorizontally, // Center children horizontally.\\n verticalArrangement = Arrangement.Center // Center children vertically within the column (less relevant here due to specific Spacers).\\n ) {\\n // Vertical space before the icon.\\n Spacer(modifier = Modifier.height(24.dp))\\n // Display the fingerprint icon.\\n Icon(\\n imageVector = Icons.Filled.Fingerprint, // Use the standard fingerprint icon.\\n contentDescription = \\"Fingerprint Icon\\", // Accessibility description.\\n tint = MaterialTheme.colorScheme.primary, // Tint the icon with the theme\'s primary color.\\n modifier = Modifier.size(64.dp) // Set the size of the icon.\\n )\\n // Vertical space between the icon and the title.\\n Spacer(modifier = Modifier.height(24.dp))\\n // Display the dialog title.\\n Text(\\n text = title,\\n style = MaterialTheme.typography.titleMedium // Use the medium title text style from the theme.\\n )\\n // Vertical space between the title and the divider.\\n Spacer(modifier = Modifier.height(10.dp))\\n // A thin horizontal line separator.\\n HorizontalDivider()\\n // The cancel button.\\n TextButton(\\n onClick = onCancel, // Invoke the onCancel lambda when clicked.\\n modifier = Modifier\\n .fillMaxWidth() // Make the button span the full width.\\n .padding(top = 8.dp) // Add padding above the button.\\n ) {\\n // The text displayed within the cancel button.\\n Text(\\n text = cancelText,\\n color = MaterialTheme.colorScheme.primary // Use the primary color for the button text for emphasis.\\n )\\n }\\n // Vertical space after the cancel button.\\n Spacer(modifier = Modifier.height(8.dp))\\n }\\n }\\n }\\n }\\n}\\n
\\n在这里使用了自定义对话框进行认证。
\\npackage com.maojiu.biometric_authorization\\n\\nimport android.content.DialogInterface\\nimport android.os.Bundle\\nimport android.view.LayoutInflater\\nimport android.view.View\\nimport android.view.ViewGroup\\nimport androidx.compose.material3.MaterialTheme // Using Material 3 Theme directly\\nimport androidx.compose.ui.platform.ComposeView\\nimport androidx.compose.ui.platform.ViewCompositionStrategy\\nimport androidx.fragment.app.DialogFragment\\n\\n/**\\n * A DialogFragment that hosts the [FingerprintDialog] Composable.\\n * This fragment is responsible for displaying the fingerprint authentication dialog\\n * and handling user interactions like cancellation or dismissal.\\n */\\nclass FingerprintDialogFragment : DialogFragment() {\\n\\n /** \\n * A lambda function to be executed when the dialog is cancelled or dismissed.\\n * This is typically used to trigger the cancellation of the underlying fingerprint authentication process.\\n */\\n var onCancelAction: (() -> Unit)? = null\\n\\n companion object {\\n private const val ARG_TITLE = \\"title\\"\\n private const val ARG_CANCEL_TEXT = \\"cancel_text\\"\\n\\n /**\\n * Factory method to create a new instance of [FingerprintDialogFragment].\\n *\\n * @param title The title string to be displayed in the dialog.\\n * @param cancelText The text string for the cancel button.\\n * @param onCancel A lambda function that will be invoked when the dialog is cancelled or dismissed.\\n * @return A new instance of [FingerprintDialogFragment] with the provided arguments.\\n */\\n fun newInstance(title: String, cancelText: String, onCancel: () -> Unit): FingerprintDialogFragment {\\n val fragment = FingerprintDialogFragment()\\n fragment.arguments = Bundle().apply {\\n putString(ARG_TITLE, title)\\n putString(ARG_CANCEL_TEXT, cancelText)\\n }\\n // Store the lambda directly. While DialogFragments can be recreated by the system\\n // (making direct lambda storage potentially fragile if the state needs to survive recreation),\\n // for this specific use case where the dialog is shown and interacts immediately\\n // with an ongoing process, this approach is often sufficient.\\n // More robust alternatives for complex state include using the Fragment Result API or a shared ViewModel.\\n fragment.onCancelAction = onCancel\\n return fragment\\n }\\n }\\n\\n /**\\n * Creates and returns the view hierarchy associated with the fragment.\\n * Inflates the layout using [ComposeView] to host the Jetpack Compose UI.\\n *\\n * @param inflater The LayoutInflater object that can be used to inflate any views in the fragment.\\n * @param container If non-null, this is the parent view that the fragment\'s UI should be attached to.\\n * @param savedInstanceState If non-null, this fragment is being re-constructed from a previous saved state as given here.\\n * @return Returns the View for the fragment\'s UI, or null.\\n */\\n override fun onCreateView(\\n inflater: LayoutInflater, container: ViewGroup?,\\n savedInstanceState: Bundle?\\n ): View {\\n // Retrieve arguments passed via newInstance\\n val title = arguments?.getString(ARG_TITLE) ?: \\"Fingerprint Authentication\\"\\n val cancelText = arguments?.getString(ARG_CANCEL_TEXT) ?: \\"Cancel\\"\\n\\n return ComposeView(requireContext()).apply {\\n // Set the strategy for managing the Compose Composition lifecycle.\\n // DisposeOnViewTreeLifecycleDestroyed ensures the Composition is disposed when the Fragment\'s view lifecycle is destroyed,\\n // preventing potential memory leaks.\\n setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)\\n setContent {\\n // Apply Material 3 Theme. Ensure the application\'s theme is correctly set up for Material 3.\\n MaterialTheme {\\n // Embed the FingerprintDialog Composable within the ComposeView\\n FingerprintDialog(\\n title = title,\\n cancelText = cancelText,\\n onDismissRequest = {\\n // This lambda is invoked when the dialog is dismissed by interactions outside its bounds\\n // (e.g., tapping the scrim or pressing the back button).\\n onCancelAction?.invoke() // Trigger cancel action on dismiss\\n dismiss() // Dismiss the DialogFragment itself\\n },\\n onCancel = {\\n // This lambda is invoked when the user clicks the explicit \'Cancel\' button within the dialog.\\n onCancelAction?.invoke() // Trigger cancel action on cancel click\\n dismiss() // Dismiss the DialogFragment itself\\n }\\n )\\n }\\n }\\n }\\n }\\n\\n /**\\n * Called when the fragment\'s activity has been created and this fragment\'s view hierarchy instantiated.\\n * Can be used to do final initialization once these pieces are in place.\\n */\\n override fun onStart() {\\n super.onStart()\\n // Optional: Set the dialog window background to transparent.\\n // This is useful if the FingerprintDialog Composable defines its own background/shape (e.g., rounded corners within a Surface).\\n // If not set, the DialogFragment might have its own default background that could interfere with the Composable\'s visuals.\\n dialog?.window?.setBackgroundDrawableResource(android.R.color.transparent)\\n }\\n\\n /**\\n * This method will be called when the dialog is cancelled, either by pressing the back button,\\n * tapping outside the dialog (if cancellable is true), or explicitly calling cancel().\\n *\\n * @param dialog The dialog that was canceled will be passed into the method.\\n */\\n override fun onCancel(dialog: DialogInterface) {\\n super.onCancel(dialog)\\n // Ensure the cancel action is invoked when the dialog is cancelled through standard mechanisms.\\n // This acts as a fallback for the onDismissRequest lambda.\\n onCancelAction?.invoke()\\n }\\n} \\n
\\n在build.gradle中进行相应的配置。
\\n/**\\n * Gradle build configuration for the Biometric Authorization plugin.\\n * This file defines all the necessary configurations for building the Android component\\n * of the Flutter plugin, including dependencies, SDK versions, and build options.\\n */\\n\\n// Define the group ID and version for the plugin\\ngroup = \\"com.maojiu.biometric_authorization\\"\\nversion = \\"1.0-SNAPSHOT\\"\\n\\n// Load local properties file that contains environment-specific configurations\\n// such as the path to the Flutter SDK\\ndef localProperties = new Properties()\\ndef localPropertiesFile = rootProject.file(\\"local.properties\\")\\nif (localPropertiesFile.exists()) {\\n localPropertiesFile.withReader(\\"UTF-8\\") { reader ->\\n localProperties.load(reader)\\n }\\n}\\n\\n// Verify that the Flutter SDK path is defined in local.properties\\ndef flutterRoot = localProperties.getProperty(\\"flutter.sdk\\")\\nif (flutterRoot == null) {\\n throw new GradleException(\\"Flutter SDK not found. Define location with flutter.sdk in the local.properties file.\\")\\n}\\n\\n/**\\n * Buildscript configuration section.\\n * This is where we define the Gradle plugins and repositories needed to build the project.\\n */\\nbuildscript {\\n // Define Kotlin version used across the project\\n ext.kotlin_version = \\"1.9.22\\"\\n ext.kotlin_ext_version = \\"1.5.10\\"\\n \\n // Define repositories to download build dependencies\\n repositories {\\n google() // Google\'s Maven repository for Android specific dependencies\\n mavenCentral() // Central repository for general Java/Kotlin libraries\\n }\\n\\n // Define build dependencies needed by Gradle to compile the project\\n dependencies {\\n classpath(\\"com.android.tools.build:gradle:8.2.0\\") // Android Gradle Plugin\\n classpath(\\"org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version\\") // Kotlin Gradle Plugin\\n }\\n}\\n\\n/**\\n * Define repositories that will be used to resolve dependencies for all projects,\\n * including the app and the libraries it depends on.\\n */\\nallprojects {\\n repositories {\\n google()\\n mavenCentral()\\n }\\n}\\n\\n// Apply the Android library plugin, which configures Gradle to build an Android library (.aar)\\napply plugin: \\"com.android.library\\"\\n// Apply the Kotlin Android plugin for Kotlin language support\\napply plugin: \\"kotlin-android\\"\\n\\n/**\\n * Android specific configuration.\\n * This block defines all Android-specific build settings.\\n */\\nandroid {\\n // Package namespace for the library\\n namespace = \\"com.maojiu.biometric_authorization\\"\\n\\n // Android SDK version to compile against\\n compileSdk = 35\\n // NDK version to use\\n ndkVersion = \\"26.3.11579264\\"\\n\\n // Enable Jetpack Compose support\\n buildFeatures {\\n compose true\\n }\\n\\n // Configure Jetpack Compose compiler options\\n composeOptions {\\n kotlinCompilerVersion(\\"$kotlin_version\\")\\n kotlinCompilerExtensionVersion(\\"$kotlin_ext_version\\")\\n }\\n\\n // Configure Java compatibility options\\n compileOptions {\\n sourceCompatibility = JavaVersion.VERSION_11\\n targetCompatibility = JavaVersion.VERSION_11\\n }\\n\\n // Configure Kotlin compiler options\\n kotlinOptions {\\n jvmTarget = JavaVersion.VERSION_11\\n }\\n\\n // Define source code directories\\n sourceSets {\\n main.java.srcDirs += \\"src/main/kotlin\\" // Main source code directory\\n test.java.srcDirs += \\"src/test/kotlin\\" // Test source code directory\\n }\\n\\n // Define the minimum SDK version required to run the library\\n defaultConfig {\\n minSdk = 21 // Android 5.0 (Lollipop) and above\\n }\\n\\n /**\\n * Dependencies section.\\n * This is where we define external libraries that our plugin depends on.\\n */\\n dependencies {\\n // Jetpack Compose dependencies\\n implementation(platform(\\"androidx.compose:compose-bom:2024.06.00\\")) // Compose Bill of Materials\\n implementation(\\"androidx.compose.ui:ui\\") // Compose UI core\\n implementation(\\"androidx.compose.ui:ui-tooling-preview\\") // Compose UI tooling preview\\n implementation(\\"androidx.compose.material3:material3\\") // Material 3 design system\\n implementation(\\"androidx.compose.material:material-icons-extended\\")\\n implementation(\\"androidx.activity:activity-compose:1.10.1\\") // Activity with Compose support\\n implementation(\\"androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7\\") // ViewModel for Compose\\n \\n // Biometric authentication dependency\\n implementation(\\"androidx.biometric:biometric:1.1.0\\") // Android Biometric API\\n \\n // Material design components\\n implementation(\\"com.google.android.material:material:1.12.0\\")\\n\\n // Testing dependencies\\n testImplementation(\\"org.jetbrains.kotlin:kotlin-test\\") // Kotlin test utilities\\n testImplementation(\\"org.mockito:mockito-core:5.0.0\\") // Mockito for mocking in tests\\n\\n // Flutter dependencies (currently commented out)\\n// compileOnly files(\\"$flutterRoot/bin/cache/artifacts/engine/android-arm-release/flutter.jar\\")\\n// compileOnly(\\"androidx.annotation:annotation:1.9.1\\")\\n }\\n\\n /**\\n * Test configuration options.\\n * This block configures how tests are executed and how results are reported.\\n */\\n testOptions {\\n unitTests.all {\\n useJUnitPlatform() // Use JUnit 5 platform for running tests\\n\\n // Configure test logging\\n testLogging {\\n events \\"passed\\", \\"skipped\\", \\"failed\\", \\"standardOut\\", \\"standardError\\" // Log test events\\n outputs.upToDateWhen {false} // Always run tests\\n showStandardStreams = true // Show standard output and error streams\\n }\\n }\\n }\\n}\\n
\\n<uses-permission android:name=\\"android.permission.USE_BIOMETRIC\\" />\\n
\\nimport io.flutter.embedding.android.FlutterFragmentActivity\\n\\nclass MainActivity: FlutterFragmentActivity() {\\n // ...\\n}\\n
\\n在你的 Info.plist 文件中添加以下内容:
\\n<key>NSFaceIDUsageDescription</key>\\n<string>您的应用需要使用生物识别数据进行安全访问验证</string>\\n
\\nfinal biometricAuth = BiometricAuthorization();\\n\\n// 检查设备是否支持生物识别\\nbool isAvailable = await biometricAuth.isBiometricAvailable();\\n\\n// 检查是否已经录入生物识别信息\\nbool isEnrolled = await biometricAuth.isBiometricEnrolled();\\n\\n// 获取可用的生物识别类型\\nList<BiometricType> types = await biometricAuth.getAvailableBiometricTypes();\\n
\\ntry {\\n bool authenticated = await biometricAuth.authenticate(\\n reason: \'请验证身份以访问您的账户\',\\n title: \'生物识别验证\',\\n confirmText: \'验证\',\\n );\\n\\n if (authenticated) {\\n // 用户验证成功\\n print(\'验证成功\');\\n } else {\\n // 验证失败或用户取消\\n print(\'验证失败\');\\n }\\n} catch (e) {\\n print(\'验证过程中出现错误: $e\');\\n}\\n
\\nbool authenticated = await biometricAuth.authenticate(\\n reason: \'请验证身份以访问您的账户\',\\n title: \'生物识别验证\',\\n confirmText: \'验证\',\\n useCustomUI: true,\\n);\\n
\\nbool authenticated = await biometricAuth.authenticate(\\n reason: \'请验证身份以访问您的账户\',\\n title: \'生物识别验证\',\\n confirmText: \'验证\',\\n useDialog: true\\n);\\n
\\nauthenticate 方法包含一些在不同平台行为不同的参数:
\\n// iOS 示例 - 必须指定 biometricType\\nawait biometricAuth.authenticate(\\n biometricType: BiometricType.face, // 使用 Face ID\\n reason: \'请验证身份以继续\',\\n);\\n
\\n// Android 示例 - 设置取消按钮文本\\nawait biometricAuth.authenticate(\\n reason: \'请验证身份以继续\',\\n cancelText: \'稍后再说\',\\n);\\n
\\nTouch ID - System UI | Touch ID - Custom UI | Face ID - Custom UI |
---|---|---|
![]() | ![]() | ![]() |
Default UI | Custom UI (Sheet) | Custom UI (Dialog) |
---|---|---|
![]() | ![]() | ![]() |
插件地址:pub.dev/packages/bi…
","description":"前言 生物识别技术(如指纹识别和面部识别)在移动应用中的使用越来越广泛,为用户提供了便捷且安全的身份验证方式。本文将介绍如何开发一个Flutter生物识别插件,使您能够在应用中轻松集成生物识别功能。\\n\\n创建插件\\n\\n可以使用终端命令或者Android Studio来创建插件项目:\\n\\n终端命令创建\\nflutter create --template=plugin --platforms=android,ios biometric_authorization\\n\\n\\n参数说明:\\n\\n--template=plugin:指定项目模板为插件类型\\n--platforms…","guid":"https://juejin.cn/post/7499977945202409509","author":"MaoJiu","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-05T08:23:59.301Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4bab145365e94f3baecbeaee3c1615cf~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTWFvSml1:q75.awebp?rk3s=f64ab15b&x-expires=1747038239&x-signature=cMd01RPz9Zl8nt1VyHqQN38Z%2B6w%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f8a9a3e826d646abac5090c98006cd3e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTWFvSml1:q75.awebp?rk3s=f64ab15b&x-expires=1747038239&x-signature=FUsgVimvjU77FkrfUvH7m849r%2F8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://github.com/maojiu-bb/biometric_authorization/blob/main/screenshots/ios-touch-default-pop.png?raw=true","type":"photo","width":750,"height":1334,"blurhash":"LEN0@_xuIV?b01bcxZayaKxbo2ay"},{"url":"https://github.com/maojiu-bb/biometric_authorization/blob/main/screenshots/ios-touch-custom-sheet.png?raw=true","type":"photo","width":750,"height":1334,"blurhash":"LTPs#Pxa-.%gz,aKI[kC04WBobWX"},{"url":"https://github.com/maojiu-bb/biometric_authorization/blob/main/screenshots/ios-face-custom-sheet.png?raw=true","type":"photo","width":1206,"height":2622,"blurhash":"LQPZr_-p?Fx]q?skIuWX0OM|oaof"},{"url":"https://github.com/maojiu-bb/biometric_authorization/blob/main/screenshots/android-default-sheet.png?raw=true","type":"photo","width":1080,"height":2220,"blurhash":"LGRfLdrrt7_NyEpIV@i_rE$*WVS#"},{"url":"https://github.com/maojiu-bb/biometric_authorization/blob/main/screenshots/android-custom-sheet.png?raw=true","type":"photo","width":1080,"height":2220,"blurhash":"LePsn@%2%Lxu~qt6WCa}4oaxRkWC"},{"url":"https://github.com/maojiu-bb/biometric_authorization/blob/main/screenshots/android-custom-dialog.png?raw=true","type":"photo","width":1080,"height":2220,"blurhash":"LORV-5?ajc%M~qt6t6WD0Jj?ahax"}],"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Dart 之Isolate","url":"https://juejin.cn/post/7500025453126828068","content":"什么是Isolate呢?答:Dart中提供的独立并发执行单元。发现了吗?没错,它就是弥补Dart单线程无法直接并行执行任务的短板。我们都知道Dart的单线程模型非常完美,但当遇到密集计算时仍然会阻塞UI,那怎么解决这个问题呢?Dart中的Isolate给出了答案,它能够将耗时的任务转移到独立线程中去执行,从而保持主线程的流畅。那Isolate都是怎么实现的呢?我们一起去看看吧。
\\n因为Isolate能够解决如下几个问题:
\\nIsolate是Dart中提供的独立并发执行单元。下面我们通过问题的方式一起去理解这个定义。
\\n为什么说它是独立的呢?
\\n答:因为每个Isolate都拥有属于自己的内存堆,彼此之间不共享任何可变数据。也就是说Isolate之间是内存隔离的(所以天然避免了共享状态的问题)。简单理解就是在不同的Isolate中的创建的同名变量不会产生冲突。
既然Isolate是独立的,那多个Isolate之间是如何通信的呢?
\\n答:Isolate之间通过消息传递通信,通过SendPort和ReceivePort建立通信通道。消息传递采用拷贝机制或引用转移。注意:传递的必须是可序列化的。
Isolate的创建方式有三种形式,分别是spawn()方法基础创建、run()方法简化一次性任务的执行,自动管理Isolate生命周期、顶级函数compute()简化Isolate。
\\nIsolate.spawn()是最基础的创建方法,其创建需要手动管理消息传递,适用于需要精细控制Isolate生命周期的场景。由于其需要手动管理消息传递,下面我们先介绍建立通信通道的SendPort、ReceivePort。
\\nReceivePort: 消息接收端。其是一个消息监听器,用于接收来自其他 Isolate 的消息。\\n每个 ReceivePort 对应一个唯一的 SendPort(通过 sendPort 属性获取),用于向该端口发送消息。\\n\\n从图中可以看出,ReceivePort是对Stream的实现,因此ReceivePort的监听和Stream的监听一致,使用listen()监听,本小节不再赘述。
SendPort: 消息发送端。其是一个消息发送器,用于向其他 Isolate 的 ReceivePort
发送数据。
如何创建?\\nIsolate.spawn()如何创建的,需要先了解spawn()方法有哪些参数。
\\n示例: 启动新Isolate并建立通信。
\\nvoid buildIsolate() async{\\n final receivePort = ReceivePort();\\n // entryPoint参数 为需要传入参数为SendPort对象的函数。\\n // message参数为通过ReceivePort获取的SendPort。\\n final isolate = await Isolate.spawn((sendPort){sendPort.send(\'hello,Dart!\');},receivePort.sendPort);\\n // 通过listen()监听听结果\\n receivePort.listen((data){print(\'接收到的信息为:$data\');});\\n}\\n
\\n用于简化一次性任务的执行,自动管理Isolate的生命周期。核心是对Isolate.spawn()的进一步封装。\\n其参数如下图所示:
\\n示例: 执行同步耗时计算。
\\nvoid buildIsolateWithRun() async {\\n final result = await Isolate.run((){\\n int sum = 0;\\n for (int i=0; i<100000000;i++){\\n sum += i;\\n }\\n return sum;\\n });\\n}\\n
\\n示例: 执行异步网络请求。
\\nvoid buildIsolateWithRun1() async {\\n final result = await Isolate.run(() async {\\n Dio dio = Dio();\\n final response = await dio.get(\'https://api.example.com/data\');\\n return response;\\n });\\n}\\n
\\n顶层函数compute()是 Dart 提供的一个便捷函数,专门用于简化一次性Isolate任务的执行。它是对 Isolate.spawn()的高层封装(在Isolate.run()的基础上封装),自动管理 Isolate 的生命周期、消息传递和资源释放。其参数如下图所示:
\\n从下图看出,顶层函数compute()底层为Isolate.run()。
\\n示例:
\\nvoid buildIsolateWithCompute() async {\\n // 传入参数message必须为可序列化的。\\n final result = await compute(sendMessage, [1,2,3]);\\n print(result);\\n}\\nint sendMessage(List<int> _list){\\n // 执行耗时计算任务\\n return _list[0];\\n}\\n
\\n注意:本小节中isolate为Isolate的实例,下同。
\\n示例:
\\nvoid buildIsolate() async{\\n final receivePort = ReceivePort();\\n Isolate isolate = await Isolate.spawn((sendPort){sendPort.send(\'hello,Dart!\');},receivePort.sendPort);\\n // 获取当前Isolate实例\\n Isolate currentIsolate = Isolate.current;\\n // 获取包配置\\n print(Isolate.packageConfig);\\n // 当前Isolate的暂停权限令牌。\\n Capability? pauseToken = isolate.pauseCapability;\\n}\\n
\\n示例:
\\nvoid buildIsolate() async{\\n final receivePort = ReceivePort();\\n Isolate isolate = await Isolate.spawn((sendPort){sendPort.send(\'hello,Dart!\');},receivePort.sendPort);\\n // 1.暂停Isolate\\n await isolate.pause();\\n Capability? pauseToken = isolate.pauseCapability;\\n if(pauseToken != null){\\n // 2.恢复Isolate\\n isolate.resume(pauseToken);\\n }\\n // 3.终止Isolate\\n isolate.kill();\\n // Isolate.exit();\\n}\\n
\\n本小节从为什么需要Isolate的问题出发,首先介绍了Isolate的不可或缺性,其次介绍了Isolate创建的三种方式,然后在介绍了Isolate属性的基础上介绍了如何暂停、终止、恢复Isolate。下面是本小节的归纳总结:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\nIsolate的创建 | Isolate的属性 | Isolate的暂停、恢复、终止 |
---|---|---|
1、Isolate.spawn() 2、Isolate.run() 3、compute() | 1、Isolate.current 2、Isolate.packageConfig 3、isolate.pauseCapability | 1、isolate.pause() 2、isolate.resume() 3、isolate.kill() 4、Isolate.exit() |
//Widget#canUpdate\\n//类型相同且 Key 相同:复用旧的 Element(仅更新数据)\\n//类型相同但 Key 不同:销毁旧的 Element,创建新 Element\\n//类型不同:直接创建新的 Element(无论 Key 是否相同)\\nstatic bool canUpdate(Widget oldWidget, Widget newWidget) {\\n //若未指定 Key,仅通过类型匹配,可能会导致状态错乱\\n return oldWidget.runtimeType == newWidget.runtimeType \\n && oldWidget.key == newWidget.key;\\n}\\n
\\n_globalKey.currentWidget\\n_globalKey.currentState\\n_globalKey.currentContext\\n
\\n需求:侧边吸顶,底部要跟随滑动
\\n简单改下别人的代码
\\n使用 SliverCrossAxisGroup + CustomScrollView
\\nclass SliverCrossAxisGroupExample extends StatelessWidget {\\n const SliverCrossAxisGroupExample({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: const Text(\'SliverGroup\'),),\\n body: CustomScrollView(\\n slivers: <Widget>[\\n SliverToBoxAdapter(\\n child: Container(\\n height: 200,\\n width: double.infinity,\\n color: Colors.green,\\n alignment: Alignment.center,\\n child: const Text(\\n \'顶部固定banner\',\\n style: TextStyle(\\n fontSize: 25\\n ),\\n ),\\n ),\\n ),\\n SliverCrossAxisGroup(\\n slivers: <Widget>[\\n SliverPersistentHeader(\\n delegate: _MySliverHeaderDelegate(\\n minHeight: 600.0, // 最小高度\\n maxHeight: 600.0, // 最大高度\\n child: Container(\\n width: 100,\\n color: Colors.blue,\\n child: const Center(child: Text(\'吸顶组件\')),\\n ),\\n ),\\n pinned: true, // 使组件吸顶\\n ),\\n SliverConstrainedCrossAxis(\\n maxExtent: 300,\\n sliver: SliverColorList(\\n height: 100.0,\\n fontSize: 24,\\n count: 20,\\n color1: Colors.amber[300],\\n color2: Colors.blue[300],\\n ),\\n ),\\n ],\\n ),\\n SliverToBoxAdapter(\\n child: Container(\\n height: 300,\\n width: double.infinity,\\n color: Colors.brown,\\n alignment: Alignment.center,\\n child: const Text(\\n \'底部文字说明\',\\n style: TextStyle(\\n fontSize: 25\\n ),\\n ),\\n ),\\n ),\\n ],\\n ),\\n );\\n }\\n}\\n\\nclass SliverColorList extends StatelessWidget {\\n final double height;\\n final double fontSize;\\n final Color? color1;\\n final Color? color2;\\n final int count;\\n const SliverColorList(\\n {super.key,\\n required this.height,\\n required this.fontSize,\\n required this.count,\\n this.color1,\\n this.color2});\\n\\n @override\\n Widget build(BuildContext context) {\\n return SliverList.builder(\\n itemBuilder: (BuildContext context, int index) {\\n return Container(\\n color: index.isEven ? color1 : color2,\\n height: height,\\n child: Center(\\n child: Text(\\n \'Item $index\',\\n style: TextStyle(fontSize: fontSize),\\n ),\\n ),\\n );\\n },\\n itemCount: count,\\n );\\n }\\n}\\n\\nclass _MySliverHeaderDelegate extends SliverPersistentHeaderDelegate {\\n _MySliverHeaderDelegate({\\n required this.minHeight,\\n required this.maxHeight,\\n required this.child,\\n });\\n\\n final double minHeight;\\n final double maxHeight;\\n final Widget child;\\n\\n @override\\n double get minExtent => minHeight;\\n\\n @override\\n double get maxExtent => max(maxHeight, minHeight);\\n\\n @override\\n Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {\\n return SizedBox.expand(child: child);\\n }\\n\\n @override\\n bool shouldRebuild(_MySliverHeaderDelegate oldDelegate) {\\n return maxHeight != oldDelegate.maxHeight ||\\n minHeight != oldDelegate.minHeight ||\\n child != oldDelegate.child;\\n }\\n}\\n\\n\\n\\n\\n
","description":"需求:侧边吸顶,底部要跟随滑动 简单改下别人的代码\\n\\n使用 SliverCrossAxisGroup + CustomScrollView\\n\\nclass SliverCrossAxisGroupExample extends StatelessWidget {\\n const SliverCrossAxisGroupExample({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title:…","guid":"https://juejin.cn/post/7499529550586658866","author":"把功夫传给我","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-04T04:48:18.845Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/35510d9bab464b1a8e3aa9f7048b3279~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oqK5Yqf5aSr5Lyg57uZ5oiR:q75.awebp?rk3s=f64ab15b&x-expires=1746938898&x-signature=dLdY0tQ2Z299UWx8gR8aOrVfHsI%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Dart语法层次的触动","url":"https://juejin.cn/post/7499575935961202715","content":"每一个使用Dart开发的app都使用void main函数,返回值是void。尽管Dart是类型安全的语言,声明变量可以使用var关键字,可以不需要指定变量的类型,由于类型推断,Dart可以推断出变量类型的类型。示例代码如下所示:
\\nvoid main() {\\n var age = 10;\\n var name = \\"caicai\\";\\n}\\n
\\n使用late修饰的变量,有两方面的作用:
\\n要在运行时获取对象的类型,可以使用类Object的属性runtimeType,该属性返回一个Type对象。也可以用关键字is来判断是否是某种类型,示例代码如下:
\\nvoid main() {\\n var age = 10;\\n var name = \\"caicai\\";\\n print(\'The type of age is ${age is int} and the type of name is ${name.runtimeType}\');\\n}\\n
\\n运行结果如下图所示:
\\n扩展方法的定义如下所示:
\\nextension <extension name>? on <type> { // <extension-name> is optional\\n (<member definition>)* // Can provide one or more <member definition>.\\n}\\n
\\n当你使用别人的API或实现一个广泛使用的库时,更改API通常是不切实际甚至不可能的。但你可能仍然想添加一些功能,这时候就可以用到扩展方法。扩展不仅可以定义方法,还可以定义其他成员,例如getter、setter 和运算符。此外,扩展可以具有名称,这在发生 API 冲突时非常有用。示例代码如下:
\\nextension on String {\\n int parseInt(){\\n return int.parse(this);\\n }\\n}\\nvoid main() {\\n var value = \\"123\\".parseInt();\\n print(\'The type of value is ${value} and the type of value is ${value.runtimeType}\');\\n}\\n
\\n也许有人会说我们可以继承某个库的类来增加相应的属性或者方法,鉴于Dart是单继承的编程语言,我们就不能继承其他的类;还有人会说我们可以增加封装方法来增加相应的方法来实现相应的逻辑,当团队多人协作同一个项目库时候,可能会有多个顶层不同名称的方法来实现同一逻辑,从而浪费了人力。扩展方法同时还在语法调用层面有不可比拟的优越性,和实例方法调用没有什么区别。
\\n在Dart中,可调用对象是定义了特殊call()方法的类的实例。这允许你像调用函数一样“调用”该对象。示例代码如下所示:
\\nclass Greeter {\\n String call(String name) {\\n return \'Hello, $name!\';\\n }\\n}\\n\\nvoid main() {\\n final greeter = Greeter();\\n\\n // Object used like a function!\\n print(greeter(\'Flutter\')); // Output: Hello, Flutter!\\n}\\n
\\nDart支持单继承,但是可以实现多个接口。每个类都隐式地定义了一个接口,该接口包含该类及其实现的所有接口的所有实例成员。如果您想要创建一个类 A,使其支持类 B的API,而无需继承类B的实现,那么类A应该实现类B的接口。\\n对于常见或广泛使用的实用程序和功能,请考虑使用顶级函数而不是静态方法。\\nfinal vs const\\nfinal修饰的var代表只能赋值一次的变量;const修饰的var代表编译期常量。实例变量可以用final修饰,不可以用const修饰。虽然final修饰的对象不能被修改,但其字段可以更改。相比之下,const对象及其字段则不能被更改:它们是不可变的。
\\nMixins是一种定义可以“继承”多个类的层次结构中重用的代码的方式。它旨在批量提供成员实现。示例代码如下:
\\nclass Musician {\\n musicianMethod() {\\n print(\'Playing music!\');\\n }\\n}\\n\\nmixin MusicalPerformer on Musician {\\n performerMethod() {\\n print(\'Performing music!\');\\n super.musicianMethod();\\n }\\n}\\n\\nclass SingerDancer extends Musician with MusicalPerformer { }\\n\\nmain() {\\n SingerDancer().performerMethod();\\n}\\n
\\nDart为了避免回调地狱,并且为了使代码可读性更高,通过使用async和await关键字来实现异步方法的声明和调用。async声明的方法可以理解为耗时的方法。示例代码如下所示:
\\nconst oneSecond = Duration(seconds: 1);\\n// ···\\nFuture<void> printWithDelay(String message) async {\\n await Future.delayed(oneSecond);\\n print(message);\\n}\\n\\nvoid main() {\\n printWithDelay(\\"hello async\\");\\n}\\n
\\n当您需要对属性进行比简单字段允许的更多的控制时,您可以定义getter和setter。
\\nclass MyClass {\\n int _aProperty = 0;\\n\\n int get aProperty => _aProperty;\\n\\n set aProperty(int value) {\\n if (value >= 0) {\\n _aProperty = value;\\n }\\n }\\n}\\n\\nvoid main() {\\n var myClass = MyClass();\\n print(\\"before ${myClass.aProperty}\\");\\n myClass._aProperty = 10;\\n print(\\"after ${myClass.aProperty}\\");\\n\\n}\\n
\\n如上面示例代码所示,我们经常声明private属性,然后对外开放getters和setters方法来对private属性操作。
\\n要对同一对象执行一系列操作,请使用级联(..)。示例代码如下所示:
\\nclass Person {\\n late int age;\\n late String name;\\n void setName(String name) {\\n this.name = name;\\n }\\n void setAge(int age) {\\n this.age = age;\\n }\\n\\n @override\\n String toString() {\\n print(\\"Person{name: $name, age: $age}\\");\\n return \\"Person{name: $name, age: $age}\\";\\n }\\n void cascades() {\\n print(\\"cascades function\\");\\n }\\n}\\n\\nvoid main() {\\n var person = Person();\\n person\\n ..setName(\\"Bob\\")\\n ..setAge(25)\\n ..toString()\\n ..cascades();\\n\\n\\n}\\n
\\n要访问其他库中定义的API,请使用import。可以导入其他库的某部分(show),也可以导入某部分除外的其他部分(hide)。如果导入两个具有冲突标识符的库,则可以为其中一个或两个库指定前缀(as)。延迟加载(也称为延迟加载)允许Web应用程序在需要库时按需加载库,要延迟加载库,首先使用deferred as导入它。
\\n语法层次以及关键字的熟悉程度会精进我们的开发技能,最终会增强我们解决问题的能力;希望你从语法层面能喜欢Dart,能开发出优秀的应用。希望文章对您有所帮助,如有纰漏,希望您不吝指出。
","description":"我所使用的版本 Dart语法\\n变量\\n\\n每一个使用Dart开发的app都使用void main函数,返回值是void。尽管Dart是类型安全的语言,声明变量可以使用var关键字,可以不需要指定变量的类型,由于类型推断,Dart可以推断出变量类型的类型。示例代码如下所示:\\n\\nvoid main() {\\n var age = 10;\\n var name = \\"caicai\\";\\n}\\n\\n\\n使用late修饰的变量,有两方面的作用:\\n\\n声明一个非空变量,该变量在声明后初始化。\\n延迟初始化变量。 如果未能初始化late变量,则使用该变量时会发生运行时错误。\\n\\n要在运行时…","guid":"https://juejin.cn/post/7499575935961202715","author":"技术蔡蔡","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-04T00:18:49.484Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2ab7a09d2f95443bbcc30626f54c8c6f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oqA5pyv6JSh6JSh:q75.awebp?rk3s=f64ab15b&x-expires=1746922729&x-signature=5QbCe7p%2Fd0UdFG35s6KZeGsG0to%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/051b9748010f466090d91e40131886ef~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oqA5pyv6JSh6JSh:q75.awebp?rk3s=f64ab15b&x-expires=1746922729&x-signature=LKtP4YHHAdTpYAxUOtoj3Efiul0%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["代码人生","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 面试知识点","url":"https://juejin.cn/post/7499441782162456614","content":"//MethodChannel\\nMethodChannel#invokeMethod 调用方法\\nMethodChannel#setMethodCallHandler\\n\\n//EventChannel\\nEventChannel#receiveBroadcastStream#listen 进行数据监听\\nEventChannel#setStreamHandler\\nEventSink.success 发送消息\\n\\n//BasicMessageChannel\\nBasicMessageChannel#send 发送消息\\nBasicMessageChannel#setMessageHandler\\n
\\n⌘ + shift + p
\\n选择 Empty Application
模板
// 导入Material风格的组件包\\n// 位置在flutter安装目录/packages/flutter/lib/material.dart\\nimport \'package:flutter/material.dart\';\\n\\nvoid main() {\\n // runApp函数接收MainApp组件并将这个Widget作为根节点\\n runApp(const MainApp());\\n}\\n\\nclass MainApp extends StatelessWidget {\\n const MainApp({super.key});\\n\\n @override\\n // Describes the part of the user interface represented by this widget.\\n\\n // The framework calls this method when this widget is inserted into the tree in a given \\n // [BuildContext] and when the dependencies of this widget change \\n // (e.g., an [InheritedWidget] referenced by this widget changes). \\n // This method can potentially be called in every frame and should not have any \\n // side effects beyond building a widget.\\n Widget build(BuildContext context) {\\n /// An application that uses Material Design.\\n /// 使用Material设计的组件,home代表默认页\\n return const MaterialApp(\\n /// The Scaffold is designed to be a top level container for\\n /// a [MaterialApp]. This means that adding a Scaffold\\n /// to each route on a Material app will provide the app with\\n /// Material\'s basic visual layout structure.\\n /// Scaffold,MateriaApp组件的顶层容器,规范样式之类的\\n home: Scaffold(\\n body: Center( /// 局中显示Hello World\\n child: Text(\'Hello World\'),\\n ),\\n ),\\n );\\n }\\n}\\n
\\nbuild方法用于描述Widget的展示效果,当被添加到上下文的树和Widget发生变化时会触发这个方法。因为这个方法是高频操作所以不应该有副作用。
\\nFlutter支持热重载,无需重启启动应用的情况下去重新刷新页面。通过将更新代码注入到运行的Dart虚拟机来实现热重载。在虚拟机使用新的字段和函数更新类后,Flutter框架自动重新构建widget。
\\n修改后直接保存/点击调试那里的闪电图标能直接刷新
\\n\\n\\nFlutter中的一切都是Widget,Widget分为有状态和无状态两种,在 Flutter 中每个页面都是一帧,无状态就是保持在那一帧,而有状态的 Widget 当数据更新时,其实是创建了新的 Widget,只是 State 实现了跨帧的数据同步保存。
\\n
比如上面的MainApp
是无状态的Widget,而Scaffold
是有状态的Widget
class MainApp extends StatelessWidget {\\n...\\n}\\n\\nclass Scaffold extends StatefulWidget {\\n...\\n}\\n
\\n创建新组件时继承有状态还是无状态的Widget
取决于是否要管理状态
Text 是现实单一样式的文本字符串组件。字符串可能跨多行中断,也可能全部显示在同一行上,取决于布局约束
\\nWidget build(BuildContext context) {\\n return MaterialApp(\\n home:Scaffold(\\n body:Center(\\n // 设置宽度限制为100点\\n child:Container(\\n width: 100,\\n height:30,\\n // 边框\\n decoration: BoxDecoration(border: Border.all()),\\n // TextOverflow.ellipsis 超过部分用...\\n // TextOverflow.clip -- Clip the overflowing text to fix its container. 超出部分换下一行,外部容器会被遮挡\\n // TextOverflow.visible -- Render overflowing text outside of its container. 超出容器部分能渲染\\n child: Text(overflow:TextOverflow.ellipsis, \'Hello world, how are you?\'))\\n ))\\n );\\n }\\n
\\nTextOverflow.ellipsis
的效果
TextOverflow.clip
的效果
TextOverflow.visible
的效果
maxLines
控制最大行数\\nsoftWrap
控制是否换行
当overflow
是TextOverflow.visible
时
softWrap: false
softWrap: true
使用Text.rich构造器,Text组件可以在一个段落中展示不同的样式
\\nWidget build(BuildContext context) {\\n return MaterialApp(\\n home: Scaffold(\\n body: Center(\\n child: const Text.rich(\\n TextSpan(\\n text: \'Hello\', // default text style\\n children: <TextSpan>[\\n TextSpan(\\n text: \' beautiful \',\\n style: TextStyle(fontStyle: FontStyle.italic),\\n ),\\n TextSpan(\\n text: \'world\',\\n style: TextStyle(fontWeight: FontWeight.bold),\\n ),\\n ],\\n ),\\n ),\\n ),\\n ),\\n );\\n }\\n
\\n用GestureDetector widget
包装Text
,然后在GestureDetector.onTap
中处理点击事件。或者使用TextButton
来代替
// 水平布局\\nRow(\\n children: [\\n // 图标\\n const IconButton(\\n icon: Icon(Icons.menu),\\n tooltip: \'Navigation menu\',\\n onPressed: null, // null disables the button\\n ),\\n // Expanded expands its child\\n // to fill the available space.\\n // 填充满2个图标之间的空间\\n Expanded(child: title),\\n // 查询图标\\n const IconButton(\\n icon: Icon(Icons.search),\\n tooltip: \'Search\',\\n onPressed: null,\\n ),\\n ],\\n)\\n
\\n// 垂直布局\\nColumn(\\n children: [\\n MyAppBar(\\n title: Text(\\n \'示例标题\',\\n style:\\n Theme.of(context) //\\n .primaryTextTheme.titleLarge,\\n ),\\n ),\\n const Expanded(child: Center(child: Text(\'容器\'))),\\n ],\\n)\\n
\\n要使用material中这些预定义图标,需要将工程中的pubspec.yaml
文件里的uses-material-design
字段设置为true
import \'package:flutter/material.dart\';\\n\\nvoid main() {\\n runApp(const MaterialApp(title: \'Flutter Tutorial\', home: TutorialHome()));\\n}\\n\\nclass TutorialHome extends StatelessWidget {\\n const TutorialHome({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n // Scaffold is a layout for\\n // the major Material Components.\\n return Scaffold(\\n appBar: AppBar(\\n leading: const IconButton(\\n icon: Icon(Icons.menu),\\n tooltip: \'Navigation menu\',\\n onPressed: null,\\n ),\\n title: const Text(\'Material Components\'),\\n actions: const [\\n IconButton(\\n icon: Icon(Icons.search),\\n tooltip: \'Search\',\\n onPressed: null,\\n ),\\n ],\\n ),\\n // body is the majority of the screen.\\n body: const Center(child: Text(\'Material!\')),\\n floatingActionButton: const FloatingActionButton(\\n tooltip: \'Add\', // used by assistive technologies\\n onPressed: null,\\n child: Icon(Icons.add),\\n ),\\n );\\n }\\n}\\n
\\n使用Scaffold
和AppBar
替换原来自定义的MyScaffold
和MyAppBar
@override\\nWidget build(BuildContext context) {\\n return MaterialApp(\\n home: Scaffold(\\n body: Center(\\n child: GestureDetector(\\n child: Text(\'Hello world\',overflow: TextOverflow.ellipsis,),\\n onTap: ()=> {\\n // 生产环境不要用print\\n print(\\"123\\")\\n },)\\n ),\\n ),\\n );\\n}\\n
\\nUI通常需要对用户的输入进行响应,比如点外卖时根据用户选择菜品计算最后的价格, Flutter
中使用StatefulWidgets
来处理这种场景。
继承StatefulWidget
,重写createState
方法
class Counter extends StatefulWidget {\\n const Counter({super.key});\\n\\n // 继承StatefulWidget的类要重写createState()方法,内容返回是_CounterState对象\\n // ``=>`` (胖箭头)简写语法用于仅包含一条语句的函数。该语法在将匿名函数作为参数传递时非常有用\\n @override\\n State<Counter> createState() => _CounterState();\\n}\\n
\\n所有的类都隐式定义成了一个接口。因此,任意类都可以作为接口被实现,定义一个继承State并实现Counter
类的方法
/// [State] objects are created by the framework by calling the\\n/// [StatefulWidget.createState] method when inflating a [StatefulWidget] to\\n/// insert it into the tree. \\n/// 在这里当Counter组件被添加到渲染树时,因为也实现了Counter类,所以会调用对应的createState方法。\\nclass _CounterState extends State<Counter> {\\n int _counter = 0;\\n \\n // _ 代表私有方法\\n void _increment() {\\n // 调用setState()通知Flutter状态变化了,然后重新执行build方法实现实时刷新的效果\\n setState(() {\\n _counter++;\\n });\\n }\\n
\\nimport \'package:flutter/material.dart\';\\n\\nclass Product {\\n const Product({required this.name});\\n\\n final String name;\\n}\\n\\ntypedef CartChangedCallback = Function(Product product, bool inCart);\\n\\nclass ShoppingListItem extends StatelessWidget {\\n ShoppingListItem({\\n required this.product,\\n required this.inCart,\\n required this.onCartChanged,\\n }) : super(key: ObjectKey(product));\\n\\n final Product product;\\n final bool inCart;\\n final CartChangedCallback onCartChanged;\\n\\n Color _getColor(BuildContext context) {\\n return inCart //\\n ? Colors.black54\\n : Theme.of(context).primaryColor;\\n }\\n\\n TextStyle? _getTextStyle(BuildContext context) {\\n if (!inCart) return null;\\n\\n return const TextStyle(\\n color: Colors.black54,\\n decoration: TextDecoration.lineThrough,\\n );\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return ListTile(\\n // 5. 点击Item时调用传入 onCartChanged 回调,并传入一开始接收的出参数\\n // 比如一开始在订单内inCart传true\\n onTap: () {\\n onCartChanged(product, inCart);\\n },\\n leading: CircleAvatar(\\n backgroundColor: _getColor(context),\\n child: Text(product.name[0]),\\n ),\\n // 4.显示产品订单的样式\\n // 10.根据新参数重新显示样式\\n title: Text(product.name, style: _getTextStyle(context)),\\n );\\n }\\n}\\n\\nclass ShoppingList extends StatefulWidget {\\n // 要求传入 products属性\\n const ShoppingList({required this.products, super.key});\\n \\n final List<Product> products;\\n \\n // 2. 调用_ShoppingListState创建状态对象\\n @override\\n State<ShoppingList> createState() => _ShoppingListState();\\n}\\n\\nclass _ShoppingListState extends State<ShoppingList> {\\n final _shoppingCart = <Product>{};\\n // 6. 点击触发回调\\n void _handleCartChanged(Product product, bool inCart) {\\n setState(() {\\n // 7. 根据入参进行判断,如果一开始是true,则移除,否则添加,即取反操作\\n if (!inCart) {\\n _shoppingCart.add(product);\\n } else {\\n _shoppingCart.remove(product);\\n }\\n // 8. 通知Flutter 重新执行_ShoppingListState对象的build方法\\n });\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: const Text(\'Shopping List\')),\\n body: ListView(\\n padding: const EdgeInsets.symmetric(vertical: 8),\\n children:\\n // 3. 根据products属性创建ShoppingListItem,并传入产品信息,回调\\n // 9. 再次调用ShoppingListItem并传入新参数\\n widget.products.map((product) {\\n return ShoppingListItem(\\n product: product,\\n inCart: _shoppingCart.contains(product),\\n onCartChanged: _handleCartChanged,\\n );\\n }).toList(),\\n ),\\n );\\n }\\n}\\n\\nvoid main() {\\n runApp(\\n const MaterialApp(\\n title: \'Shopping App\',\\n // 1. 创建ShoppingList对象,并传入Product参数\\n home: ShoppingList(\\n products: [\\n Product(name: \'Eggs\'),\\n Product(name: \'Flour\'),\\n Product(name: \'Chocolate chips\'),\\n ],\\n ),\\n ),\\n );\\n}\\n
\\nFlutter
调用createState
方法后,会将state
对象添加到渲染树并且调用state
对象的initState()
,可以重写这个方法中配置动画或准备订阅平台相关的服务,重写方法开始要先调用super.initState
。当state
对象不再需要时,Flutter
会调用对象的dispose
方法来执行清理操作,比如取消定时器,取消订阅,同样在重写方法中也要先调用super.dispose
$ flutter pub get \\n# 命令下载的包在~/.pub-cache/hosted\\n
\\n开发中遇到需要软件持续监听设备推送数据完成对应视图刷新的场景,一开始数据比较单一视图影响范围小直接使用 scoket 监听数据变化直接刷新视图处理;
\\nDevice -> \\n WebSocket/Socket -> \\n 原生事件监听 -> \\n 手动解析数据 -> 直接操作 UI 或状态\\n
\\n随着需求增加,需要处理的设备数据类型、视图影响范围都在扩大,造成人为管理网络状态于是引入 stream 进行管理
\\nDevice -> \\n WebSocket/Socket -> \\n StreamController -> \\n 数据处理管道(防抖/过滤/重试) -> \\n StreamBuilder -> UI\\n
\\ngraph TD\\n A[设备层] --\x3e|上报数据| B[WebSocket 服务]\\n B --\x3e C[Stream 数据源]\\n C --\x3e D{数据处理层}\\n D --\x3e|防抖 500ms| E[Debounce]\\n D --\x3e|JSON 解析| F[Parser]\\n D --\x3e|错误重试| G[RetryWhen]\\n E --\x3e H[BroadcastStream]\\n F --\x3e H\\n G --\x3e H\\n H --\x3e I[StreamBuilder]\\n I --\x3e J[设备状态视图]\\n I --\x3e K[实时曲线图]\\n I --\x3e L[告警提示]\\n
\\n优化维度 | 优化前 | 优化后 | 提升幅度 | 实现方式 |
---|---|---|---|---|
代码可维护性 | 命令式逻辑(紧密耦合) | 声明式管道(解耦分层) | 开发效率提升 50% | async* + 操作符链 |
UI 渲染频率 | 每秒 10 次(设备每秒上报 10 次) | 每秒 2 次(防抖 500ms) | 80% 降低 | debounceTime(500ms) |
内存泄漏风险 | 高风险(未取消订阅导致泄漏) | 低风险(自动取消订阅) | 100% 解决 | StreamSubscription.cancel() |
错误恢复时间 | 手动重连(依赖开发者逻辑) | 自动指数退避重试(3 次) | 90% 自动化 | _autoReconnect() |
CPU 占用率 | 高频数据处理(持续 30%) | 低峰数据处理(峰值 8%) | 73% 降低 | throttle + 数据过滤 |
视图卡顿次数 | 高频重建(每秒 10 次) | 稳定渲染(每秒 2 次) | 80% 减少 | StreamBuilder 智能更新 |
多设备支持能力 | 单设备绑定 | 广播流支持 10+ 设备 | 扩展性提升 10x | BroadcastStream |
实时性要求高:聊天、股票行情、IoT 传感器数据;多端协同:跨设备状态同步(如登录状态推送)
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n方案 | 适用场景 | 缺点 |
---|---|---|
Future | 单次异步操作(如网络请求) | 无法处理连续数据流 |
Timer | 周期性任务(如轮询) | 难以动态调整频率 |
第三方库 | 复杂推送需求(如消息回执) | 引入额外依赖 |
方法/属性 | 作用 | 代码示例 |
---|---|---|
add(T event) | 向流中添加数据 | controller.sink.add(42); |
addError(e) | 向流中发送错误 | controller.sink.addError(\'Network failed\'); |
close() | 关闭流,停止数据生产并触发下游 onDone | controller.close(); |
stream | 获取流对象,供 StreamBuilder 或 listen 使用 | Stream<int> stream = controller.stream; |
sink | 通过 Sink 对象操作流(添加数据/错误) | controller.sink.add(100); |
方法/属性 | 作用 | 代码示例 |
---|---|---|
stream | 获取流对象,供 StreamBuilder 或 listen 使用 | Stream<int> stream = controller.stream; |
sink | 通过 Sink 对象操作流(添加数据/错误) | controller.sink.add(100); |
hasSubscribers | 检查是否有活跃的订阅者 | if (controller.hasSubscribers) { ... } |
方法 | 作用 | 代码示例 |
---|---|---|
cancel() | 取消订阅,停止接收数据并释放资源 | subscription.cancel(); |
pause([resumeSignal]) | 暂停接收数据(保留订阅状态) | subscription.pause(); |
resume() | 恢复接收数据 | subscription.resume(); |
asFuture() | 将流转换为 Future ,等待流完成(仅适用于单次数据流) | final future = subscription.asFuture(); |
onData | 设置数据回调(等同于 listen 的第一个参数) | subscription.onData((data) => print(data)); |
onError | 设置错误回调 | subscription.onError((e) => print(\'Error: $e\')); |
onDone | 设置流完成回调 | subscription.onDone(() => print(\'Stream closed\')); |
当调用 controller.add(data) 时,数据被加入事件队列,之后按先入先出的顺序,通过订阅管道依次分发到每一个监听器。
\\n对于单订阅流(Single-subscription Stream),只允许一个监听器;广播流(Broadcast Stream)则可以有多个并行监听器。
\\nsequenceDiagram\\n participant Controller as StreamController\\n participant Subscription as StreamSubscription\\n participant UI as 视图层\\n\\n Controller->>Subscription: 创建流(stream)\\n Subscription->>Controller: 订阅流(listen())\\n Controller->>Subscription: 推送数据(add())\\n Subscription->>UI: 触发 onData()\\n Subscription->>Controller: 取消订阅(cancel())\\n
\\nDart 中有两种主要的 Stream 类型:单订阅(Single-Subscription)Stream 和 广播(Broadcast)Stream
\\n单订阅 Stream
\\n默认情况下,StreamController()
创建的是一个单订阅的 Stream。这种 Stream 只能被监听一次,一旦有第一个监听器订阅后,其他监听器无法再订阅同一个流。单订阅流常用于 需要严格顺序处理的事件流,比如文件读取、HTTP 请求响应流。它支持 暂停/恢复(pause/resume)操作,因为在单订阅场景下,可以控制流的消费速度。
在 StreamDemoPage 的 initState() 中,会调用 _initStream() 方法,内部构造了一个普通的 StreamController:
\\n_streamController = StreamController<String>(\\n onCancel: () {\\n print(\'StreamController 被关闭\');\\n },\\n);\\n
\\n在 _startPushing() 中,通过 Timer.periodic 定时生成、添加数据:
\\nTimer.periodic(Duration(seconds: _interval), (timer) {\\n // …\\n final message = \'模拟消息 #$_pushCount - ${DateTime.now().toString().substring(11,19)}\';\\n if (!_streamController.isClosed) {\\n _streamController.add(message);\\n }\\n});\\n
\\n用户点击 “订阅” 时,会调用 stream.listen(...),创建订阅并注册回调:
\\n_subscription = _streamController.stream.listen((data) {\\n setState(() => _receivedMessages.add(data));\\n },\\n onError: (error) => _showMessage(\'错误: $error\'),\\n onDone: () {\\n _showMessage(\'Stream 已关闭\');\\n setState(() => _subscription = null);\\n },\\n); \\n
\\n_subscription!.cancel().then((_) {\\n setState(() => _subscription = null);\\n _showMessage(\'已取消订阅\');\\n}); \\n
\\n_streamController.close().then((_) {\\n _showMessage(\'Stream 已关闭\');\\n _initStream(); // 重新初始化以备下次使用\\n});\\n
\\n广播 Stream
\\n使用 StreamController.broadcast()
或者通过 .asBroadcastStream()
可以创建广播流。广播流允许 多个监听器同时订阅 同一个流,适用于同一份数据需要供多处同时监听的场景,比如多个 UI 界面同时关注同一个消息流、或者多个业务模块同时对推送进行处理。广播流默认不支持 pause/resume(每个订阅者收到数据独立控制)。
广播流允许多个订阅者,底层使用 StreamController.broadcast() 构建:
\\n_broadcastController = StreamController<String>.broadcast(\\n onListen: () { print(\'有人开始监听广播Stream\'); },\\n onCancel: () { print(\'有人取消监听广播Stream\'); },\\n);\\n
\\n调用 stream.listen() 为每个订阅者生成独立的 StreamSubscription:
\\nfinal subscriber = _Subscriber(\\n id: subscriberId,\\n \\n subscription: _broadcastController.stream.listen((data) {\\n setState(() {\\n _subscribers.firstWhere((s)=>s.id==subscriberId).messages.add(data);\\n });\\n },\\n onError: (error) => _showMessage(\'订阅者 $subscriberId 收到错误: $error\'),\\n onDone: () => _showMessage(\'订阅者 $subscriberId 的Stream已关闭\'),\\n ),\\n); \\n
\\n添加订阅者:动态调用 stream.listen() 即可新增监听器。
\\n移除订阅者:调用对应 subscription.cancel(),只移除此订阅者,不影响其他人。
\\nsubscriber.subscription.cancel();\\nsetState(() => _subscribers.remove(subscriber)); \\n
\\n关闭时调用 close(),会向所有订阅者发送完成事件并释放底层资源:
\\n_broadcastController.close();\\n\\n_initBroadcastStream(); // 重新初始化\\n\\nsetState(() => _subscribers.clear());\\n
\\n如果你的场景仅仅是单一组件简单接收事件,也可以使用简单的回调或事件总线,但在较复杂应用中,Stream 的优势更加明显。
\\nflowchart LR\\n subgraph 传统方式\\n WS1[WebSocket 客户端] --\x3e|数据推送| EB[事件总线]\\n EB --\x3e|发送事件| UI1[UI 界面]\\n end\\n subgraph Stream 方式\\n WS2[WebSocket 客户端] --\x3e|数据推送| SC[StreamController]\\n SC --\x3e|监听数据| UI2[UI 界面]\\n end\\n\\n
\\n对比维度 | 传统方案(事件总线/回调) | Stream 方案 | 差异说明 | 优势点 |
---|---|---|---|---|
架构耦合度 | ❌ 高度耦合:全局事件总线导致模块间隐式依赖,事件来源难以追踪 | ✅ 低耦合:数据生产(Controller)与消费(StreamBuilder)完全解耦 | 事件总线需要跨模块监听全局事件,Stream 通过类型化流明确数据流向 | 代码结构更清晰,模块职责分明 |
生命周期管理 | ❌ 手动管理订阅:需在 dispose 中取消监听,否则内存泄漏风险高 | ✅ 自动管理:通过 StreamSubscription 在 State.dispose() 中一键取消订阅 | 传统方案依赖开发者手动维护,Stream 通过对象生命周期自动绑定 | 内存泄漏风险归零,资源释放更安全 |
数据类型安全 | ❌ 弱类型:事件总线传递原始数据,缺乏类型约束 | ✅ 强类型:流数据通过泛型(Stream<T> )明确定义数据类型 | 传统方案需强制类型转换,Stream 在编译期校验类型 | 减少运行时错误,提升代码健壮性 |
高频数据处理 | ❌ 性能瓶颈:频繁回调导致 UI 卡顿(如每秒 100 次事件) | ✅ 高效优化:通过 debounceTime /throttle 减少无效渲染,提升渲染效率 | 传统方案直接触发 UI 更新,Stream 通过操作符控制数据流速 | UI 渲染频率降低 80%,FPS 提升明显 |
错误处理 | ❌ 分散处理:错误需在每个回调中手动捕获,难以统一管理 | ✅ 集中管理:通过 catchError 和 retryWhen 实现全局错误处理和重试逻辑 | 传统方案错误处理逻辑分散,Stream 通过管道链式处理异常 | 错误隔离性增强,系统稳定性提升 |
多订阅者支持 | ❌ 手动维护:需为每个订阅者单独注册监听器,扩展性差 | ✅ 广播流:BroadcastStream 天然支持多订阅者,数据广播至所有监听者 | 传统方案需自行管理多个监听器,Stream 通过广播模式简化多设备/多组件场景 | 支持 10+ 订阅者无需代码冗余 |
代码可维护性 | ❌ 逻辑分散:数据接收、解析、更新 UI 等逻辑混杂在回调中 | ✅ 集中处理:通过 async* 生成器和操作符链(map /where )封装数据处理逻辑 | 传统方案代码分散在多个回调中,Stream 通过声明式管道集中管理数据流 | 代码复用率提升 50%,逻辑更易测试 |
实时性保障 | ❌ 延迟不可控:事件总线可能因线程阻塞导致数据延迟 | ✅ 低延迟:流数据通过 StreamBuilder 自动触发 UI 更新,无阻塞主线程风险 | 传统方案依赖事件循环机制,Stream 通过 Dart Isolate 优化异步处理 | 数据更新延迟降低至微秒级 |
扩展性 | ❌ 扩展困难:新增数据类型或处理逻辑需修改全局事件总线 | ✅ 灵活扩展:通过 StreamTransformer 封装新逻辑,按需组合操作符链 | 传统方案需侵入式修改全局代码,Stream 通过管道模式实现非侵入式扩展 | 支持动态添加新数据处理逻辑(如新增加密/压缩模块) |
指标 | 传统方案 | Stream 方案 | 迭代优势 |
---|---|---|---|
UI 渲染性能 | 高频事件导致 UI 卡顿(帧率 50 FPS) | 防抖优化后稳定 60 FPS | 性能提升 20%+ |
内存泄漏风险 | 高(需手动取消订阅) | 低(自动释放资源) | 内存泄漏风险归零 |
代码可维护性 | 低(逻辑分散,回调地狱) | 高(声明式管道,集中管理) | 维护成本降低 40% |
错误恢复速度 | 手动重连(>30s) | 自动指数退避重试(<5s) | 故障恢复时间缩短 85% |
多设备支持能力 | 需为每个设备单独实现逻辑 | 广播流支持多设备无缝扩展 | 扩展性提升 10x |
实际开发中,可根据场景选用不同类型的 Stream,并借助流式操作符(见 lib/utils/stream_utils.dart)进行灵活变换和组合
\\ndemo 地址:flutter_study/stream_subscription at master · lizy-coding/flutter_study
","description":"推送场景优化 场景问题\\n\\n开发中遇到需要软件持续监听设备推送数据完成对应视图刷新的场景,一开始数据比较单一视图影响范围小直接使用 scoket 监听数据变化直接刷新视图处理;\\n\\nDevice -> \\n WebSocket/Socket -> \\n 原生事件监听 -> \\n 手动解析数据 -> 直接操作 UI 或状态\\n\\n\\n随着需求增加,需要处理的设备数据类型、视图影响范围都在扩大,造成人为管理网络状态于是引入 stream 进行管理\\n\\nDevice -> \\n WebSocket/Socket…","guid":"https://juejin.cn/post/7499317287717158921","author":"人形打码机","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-02T08:57:03.535Z","media":null,"categories":["阅读","GitHub","Flutter","面试"],"attachments":null,"extra":null,"language":null},{"title":"Flutter文字镂空效果实现指南","url":"https://juejin.cn/post/7499013568122552355","content":"哈哈,2019年初我刚入职时,遇到了一个特别的需求:学校的卡片上要有个分类标签,文字部分还得镂空。当时我刚开始接触Flutter,对很多功能都不熟悉,这个需求就一直没能实现,成了我的一个小执念。现在我早已不在那儿工作了,可这两天闲来无事,突然想起了这个事。趁着五一假期,我开始琢磨画笔功能,终于把当年实现不了的功能给实现了😎!
\\n\\n\\nTip: 这时候可能会有人说:啊,这道题我会,用
\\nShaderMask
配置blendMode: BlendMode.srcOut
就能实现,但实际上这个组件不能设置圆角,内边距等相关内容,如果这时候添加一个Container
那么镂空效果也只能看到Container
的颜色,而不能看到最底部的图片
文字镂空效果的核心是使用Canvas和自定义绘制(CustomPainter)来创建一个矩形,然后从中\\"切出\\"文字形状。我们将使用Flutter的BlendMode.dstOut
混合模式来实现这一效果。
首先,我们需要设置基本的应用结构:
\\nimport \'package:flutter/material.dart\';\\n\\nvoid main() {\\n runApp(const MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n const MyApp({Key? key}) : super(key: key);\\n\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n title: \'Rectangle Text Cutout\',\\n theme: ThemeData(\\n primarySwatch: Colors.teal,\\n useMaterial3: true,\\n ),\\n home: const RectangleDrawingScreen(),\\n );\\n }\\n}\\n
\\n这里我们创建了一个基本的MaterialApp,并设置了主题颜色为teal(青色),启用了Material 3设计。
\\n接下来,我们创建主屏幕,这是一个StatefulWidget,因为我们需要管理多个可变状态:
\\nclass RectangleDrawingScreen extends StatefulWidget {\\n const RectangleDrawingScreen({super.key});\\n\\n @override\\n State<RectangleDrawingScreen> createState() => _RectangleDrawingScreenState();\\n}\\n\\nclass _RectangleDrawingScreenState extends State<RectangleDrawingScreen> {\\n // 定义状态变量\\n double _cornerRadius = 20.0;\\n String _text = \\"FLUTTER\\";\\n double _fontSize = 60.0;\\n Color _rectangleColor = Colors.teal;\\n Color _backgroundColor = Colors.white;\\n \\n // 构建UI...\\n}\\n
\\n我们定义了几个关键状态变量:
\\n_cornerRadius
:矩形的圆角半径_text
:要镂空的文字_fontSize
:文字大小_rectangleColor
:矩形的颜色_backgroundColor
:背景颜色这是实现镂空效果的核心部分 - 自定义绘制器:
\\nclass RectangleTextCutoutPainter extends CustomPainter {\\n final double cornerRadius;\\n final String text;\\n final double fontSize;\\n final Color rectangleColor;\\n\\n RectangleTextCutoutPainter({\\n required this.cornerRadius,\\n required this.text,\\n required this.fontSize,\\n required this.rectangleColor,\\n });\\n\\n @override\\n void paint(Canvas canvas, Size size) {\\n // 创建矩形区域\\n final Rect rect = Rect.fromLTWH(\\n 20,\\n 20,\\n size.width - 40,\\n size.height - 40,\\n );\\n\\n // 创建圆角矩形\\n final RRect roundedRect = RRect.fromRectAndRadius(\\n rect,\\n Radius.circular(cornerRadius),\\n );\\n\\n // 设置文字样式\\n final textStyle = TextStyle(\\n fontSize: fontSize,\\n fontWeight: FontWeight.bold,\\n );\\n\\n final textSpan = TextSpan(\\n text: text,\\n style: textStyle,\\n );\\n\\n // 创建文字绘制器\\n final textPainter = TextPainter(\\n text: textSpan,\\n textDirection: TextDirection.ltr,\\n );\\n\\n // 计算文字位置\\n textPainter.layout(\\n minWidth: 0,\\n maxWidth: size.width,\\n );\\n final double xCenter = (size.width - textPainter.width) / 2;\\n final double yCenter = (size.height - textPainter.height) / 2;\\n\\n // 使用图层和混合模式实现镂空效果\\n canvas.saveLayer(rect.inflate(20), Paint());\\n final Paint rectanglePaint = Paint()\\n ..color = rectangleColor\\n ..style = PaintingStyle.fill;\\n\\n canvas.drawRRect(roundedRect, rectanglePaint);\\n final Paint cutoutPaint = Paint()\\n ..color = Colors.white\\n ..style = PaintingStyle.fill\\n ..blendMode = BlendMode.dstOut;\\n\\n canvas.saveLayer(rect.inflate(20), cutoutPaint);\\n textPainter.paint(canvas, Offset(xCenter, yCenter));\\n canvas.restore();\\n canvas.restore();\\n }\\n\\n @override\\n bool shouldRepaint(covariant RectangleTextCutoutPainter oldDelegate) {\\n return oldDelegate.cornerRadius != cornerRadius ||\\n oldDelegate.text != text ||\\n oldDelegate.fontSize != fontSize ||\\n oldDelegate.rectangleColor != rectangleColor;\\n }\\n}\\n
\\n这个自定义绘制器的工作原理是:
\\nsaveLayer
和BlendMode.dstOut
创建一个混合图层shouldRepaint
方法优化重绘性能现在,让我们实现主界面,包括预览区域和控制面板:
\\n@override\\nWidget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: const Text(\'Rectangle Text Cutout\'),\\n backgroundColor: Colors.teal.shade100,\\n ),\\n body: Column(\\n children: [\\n // 预览区域\\n Expanded(\\n child: Container(\\n color: Colors.grey[200],\\n child: Center(\\n child: Stack(\\n children: [\\n // 背景图片\\n Positioned.fill(\\n child: Image.network(\\n \\"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d11fee3a97464bca82c9291435cc2a89~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5reh5YaZ5oiQ54Gw:q75.awebp?rk3s=f64ab15b&x-expires=1746213056&x-signature=ylstmk1m2eVeu8bI%2BhDJkVUHe7U%3D\\",\\n fit: BoxFit.cover,\\n ),\\n ),\\n // 自定义绘制\\n CustomPaint(\\n size: const Size(double.infinity, double.infinity),\\n painter: RectangleTextCutoutPainter(\\n cornerRadius: _cornerRadius,\\n text: _text,\\n fontSize: _fontSize,\\n rectangleColor: _rectangleColor,\\n ),\\n ),\\n // 额外的ShaderMask效果\\n ShaderMask(\\n blendMode: BlendMode.srcOut,\\n child: Text(\\n _text,\\n ),\\n shaderCallback: (bounds) =>\\n LinearGradient(colors: [Colors.black], stops: [0.0])\\n .createShader(bounds),\\n ),\\n ],\\n ),\\n ),\\n ),\\n ),\\n // 控制面板\\n Container(\\n padding: const EdgeInsets.all(16),\\n color: Colors.grey[200],\\n child: Column(\\n crossAxisAlignment: CrossAxisAlignment.start,\\n children: [\\n // 圆角控制\\n const Text(\'Corner Radius:\', style: TextStyle(fontWeight: FontWeight.bold)),\\n Slider(\\n value: _cornerRadius,\\n min: 0,\\n max: 100,\\n divisions: 100,\\n label: _cornerRadius.round().toString(),\\n activeColor: Colors.teal,\\n onChanged: (value) {\\n setState(() {\\n _cornerRadius = value;\\n });\\n },\\n ),\\n // 字体大小控制\\n const SizedBox(height: 10),\\n const Text(\'Font Size:\', style: TextStyle(fontWeight: FontWeight.bold)),\\n Slider(\\n value: _fontSize,\\n min: 20,\\n max: 120,\\n divisions: 100,\\n label: _fontSize.round().toString(),\\n activeColor: Colors.teal,\\n onChanged: (value) {\\n setState(() {\\n _fontSize = value;\\n });\\n },\\n ),\\n // 文字输入\\n const SizedBox(height: 10),\\n TextField(\\n decoration: const InputDecoration(\\n labelText: \'Text to Cut Out\',\\n border: OutlineInputBorder(),\\n focusedBorder: OutlineInputBorder(\\n borderSide: BorderSide(color: Colors.teal),\\n ),\\n ),\\n onChanged: (value) {\\n setState(() {\\n _text = value;\\n });\\n },\\n controller: TextEditingController(text: _text),\\n ),\\n // 矩形颜色选择\\n const SizedBox(height: 16),\\n Row(\\n children: [\\n const Text(\'Rectangle Color: \', style: TextStyle(fontWeight: FontWeight.bold)),\\n const SizedBox(width: 10),\\n _buildColorButton(Colors.teal),\\n _buildColorButton(Colors.blue),\\n _buildColorButton(Colors.red),\\n _buildColorButton(Colors.purple),\\n _buildColorButton(Colors.orange),\\n ],\\n ),\\n // 背景颜色选择\\n const SizedBox(height: 16),\\n Row(\\n children: [\\n const Text(\'Background Color: \', style: TextStyle(fontWeight: FontWeight.bold)),\\n const SizedBox(width: 10),\\n _buildBackgroundColorButton(Colors.white),\\n _buildBackgroundColorButton(Colors.grey.shade300),\\n _buildBackgroundColorButton(Colors.yellow.shade100),\\n _buildBackgroundColorButton(Colors.blue.shade100),\\n _buildBackgroundColorButton(Colors.pink.shade100),\\n ],\\n ),\\n ],\\n ),\\n ),\\n ],\\n ),\\n );\\n}\\n
\\n最后,我们实现颜色选择按钮的构建方法:
\\nWidget _buildColorButton(Color color) {\\n return GestureDetector(\\n onTap: () {\\n setState(() {\\n _rectangleColor = color;\\n });\\n },\\n child: Container(\\n margin: const EdgeInsets.only(right: 8),\\n width: 30,\\n height: 30,\\n decoration: BoxDecoration(\\n color: color,\\n shape: BoxShape.circle,\\n border: Border.all(\\n color: _rectangleColor == color ? Colors.black : Colors.transparent,\\n width: 2,\\n ),\\n ),\\n ),\\n );\\n}\\n\\nWidget _buildBackgroundColorButton(Color color) {\\n return GestureDetector(\\n onTap: () {\\n setState(() {\\n _backgroundColor = color;\\n });\\n },\\n child: Container(\\n margin: const EdgeInsets.only(right: 8),\\n width: 30,\\n height: 30,\\n decoration: BoxDecoration(\\n color: color,\\n shape: BoxShape.circle,\\n border: Border.all(\\n color: _backgroundColor == color ? Colors.black : Colors.transparent,\\n width: 2,\\n ),\\n ),\\n ),\\n );\\n}\\n
\\n在这个效果中,最关键的技术是使用BlendMode.dstOut
混合模式。这个混合模式会从目标图像(矩形)中\\"减去\\"源图像(文字),从而创建出文字形状的\\"洞\\"。
final Paint cutoutPaint = Paint()\\n ..color = Colors.white\\n ..style = PaintingStyle.fill\\n ..blendMode = BlendMode.dstOut;\\n
\\n我们使用canvas.saveLayer()
和canvas.restore()
来创建和管理图层,这是实现复杂绘制效果的关键:
canvas.saveLayer(rect.inflate(20), Paint());\\n// 绘制矩形\\ncanvas.saveLayer(rect.inflate(20), cutoutPaint);\\n// 绘制文字\\ncanvas.restore();\\ncanvas.restore();\\n
\\n为了让文字在矩形中居中显示,我们需要计算正确的位置:
\\nfinal double xCenter = (size.width - textPainter.width) / 2;\\nfinal double yCenter = (size.height - textPainter.height) / 2;\\n
\\n为了方便大家查阅,下面贴出完整代码
\\nimport \'package:flutter/material.dart\';\\n\\nvoid main() {\\n runApp(const MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n const MyApp({Key? key}) : super(key: key);\\n\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n title: \'Rectangle Text Cutout\',\\n theme: ThemeData(\\n primarySwatch: Colors.teal,\\n useMaterial3: true,\\n ),\\n home: const RectangleDrawingScreen(),\\n );\\n }\\n}\\n\\nclass RectangleDrawingScreen extends StatefulWidget {\\n const RectangleDrawingScreen({super.key});\\n\\n @override\\n State<RectangleDrawingScreen> createState() => _RectangleDrawingScreenState();\\n}\\n\\nclass _RectangleDrawingScreenState extends State<RectangleDrawingScreen> {\\n double _cornerRadius = 20.0;\\n String _text = \\"FLUTTER\\";\\n double _fontSize = 60.0;\\n Color _rectangleColor = Colors.teal;\\n Color _backgroundColor = Colors.white;\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: const Text(\'Rectangle Text Cutout\'),\\n backgroundColor: Colors.teal.shade100,\\n ),\\n body: Column(\\n children: [\\n\\n Expanded(\\n child: Container(\\n color: Colors.grey[200],\\n child: Center(\\n child: Stack(\\n children: [\\n Positioned.fill(\\n child: Image.network(\\n \\"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d11fee3a97464bca82c9291435cc2a89~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5reh5YaZ5oiQ54Gw:q75.awebp?rk3s=f64ab15b&x-expires=1746213056&x-signature=ylstmk1m2eVeu8bI%2BhDJkVUHe7U%3D\\",\\n fit: BoxFit.cover,\\n ),\\n ),\\n CustomPaint(\\n size: const Size(double.infinity, double.infinity),\\n painter: RectangleTextCutoutPainter(\\n cornerRadius: _cornerRadius,\\n text: _text,\\n fontSize: _fontSize,\\n rectangleColor: _rectangleColor,\\n ),\\n ),\\n ShaderMask(\\n blendMode: BlendMode.srcOut,\\n child: Text(\\n _text,\\n ),\\n shaderCallback: (bounds) =>\\n LinearGradient(colors: [Colors.black], stops: [0.0])\\n .createShader(bounds),\\n ),\\n ],\\n ),\\n ),\\n ),\\n ),\\n Container(\\n padding: const EdgeInsets.all(16),\\n color: Colors.grey[200],\\n child: Column(\\n crossAxisAlignment: CrossAxisAlignment.start,\\n children: [\\n const Text(\'Corner Radius:\', style: TextStyle(fontWeight: FontWeight.bold)),\\n Slider(\\n value: _cornerRadius,\\n min: 0,\\n max: 100,\\n divisions: 100,\\n label: _cornerRadius.round().toString(),\\n activeColor: Colors.teal,\\n onChanged: (value) {\\n setState(() {\\n _cornerRadius = value;\\n });\\n },\\n ),\\n const SizedBox(height: 10),\\n const Text(\'Font Size:\', style: TextStyle(fontWeight: FontWeight.bold)),\\n Slider(\\n value: _fontSize,\\n min: 20,\\n max: 120,\\n divisions: 100,\\n label: _fontSize.round().toString(),\\n activeColor: Colors.teal,\\n onChanged: (value) {\\n setState(() {\\n _fontSize = value;\\n });\\n },\\n ),\\n const SizedBox(height: 10),\\n TextField(\\n decoration: const InputDecoration(\\n labelText: \'Text to Cut Out\',\\n border: OutlineInputBorder(),\\n focusedBorder: OutlineInputBorder(\\n borderSide: BorderSide(color: Colors.teal),\\n ),\\n ),\\n onChanged: (value) {\\n setState(() {\\n _text = value;\\n });\\n },\\n controller: TextEditingController(text: _text),\\n ),\\n const SizedBox(height: 16),\\n Row(\\n children: [\\n const Text(\'Rectangle Color: \', style: TextStyle(fontWeight: FontWeight.bold)),\\n const SizedBox(width: 10),\\n _buildColorButton(Colors.teal),\\n _buildColorButton(Colors.blue),\\n _buildColorButton(Colors.red),\\n _buildColorButton(Colors.purple),\\n _buildColorButton(Colors.orange),\\n ],\\n ),\\n const SizedBox(height: 16),\\n Row(\\n children: [\\n const Text(\'Background Color: \', style: TextStyle(fontWeight: FontWeight.bold)),\\n const SizedBox(width: 10),\\n _buildBackgroundColorButton(Colors.white),\\n _buildBackgroundColorButton(Colors.grey.shade300),\\n _buildBackgroundColorButton(Colors.yellow.shade100),\\n _buildBackgroundColorButton(Colors.blue.shade100),\\n _buildBackgroundColorButton(Colors.pink.shade100),\\n ],\\n ),\\n ],\\n ),\\n ),\\n ],\\n ),\\n );\\n }\\n\\n Widget _buildColorButton(Color color) {\\n return GestureDetector(\\n onTap: () {\\n setState(() {\\n _rectangleColor = color;\\n });\\n },\\n child: Container(\\n margin: const EdgeInsets.only(right: 8),\\n width: 30,\\n height: 30,\\n decoration: BoxDecoration(\\n color: color,\\n shape: BoxShape.circle,\\n border: Border.all(\\n color: _rectangleColor == color ? Colors.black : Colors.transparent,\\n width: 2,\\n ),\\n ),\\n ),\\n );\\n }\\n\\n Widget _buildBackgroundColorButton(Color color) {\\n return GestureDetector(\\n onTap: () {\\n setState(() {\\n _backgroundColor = color;\\n });\\n },\\n child: Container(\\n margin: const EdgeInsets.only(right: 8),\\n width: 30,\\n height: 30,\\n decoration: BoxDecoration(\\n color: color,\\n shape: BoxShape.circle,\\n border: Border.all(\\n color: _backgroundColor == color ? Colors.black : Colors.transparent,\\n width: 2,\\n ),\\n ),\\n ),\\n );\\n }\\n}\\n\\nclass RectangleTextCutoutPainter extends CustomPainter {\\n final double cornerRadius;\\n final String text;\\n final double fontSize;\\n final Color rectangleColor;\\n\\n RectangleTextCutoutPainter({\\n required this.cornerRadius,\\n required this.text,\\n required this.fontSize,\\n required this.rectangleColor,\\n });\\n\\n @override\\n void paint(Canvas canvas, Size size) {\\n final Rect rect = Rect.fromLTWH(\\n 20,\\n 20,\\n size.width - 40,\\n size.height - 40,\\n );\\n\\n final RRect roundedRect = RRect.fromRectAndRadius(\\n rect,\\n Radius.circular(cornerRadius),\\n );\\n\\n final textStyle = TextStyle(\\n fontSize: fontSize,\\n fontWeight: FontWeight.bold,\\n );\\n\\n final textSpan = TextSpan(\\n text: text,\\n style: textStyle,\\n );\\n\\n final textPainter = TextPainter(\\n text: textSpan,\\n textDirection: TextDirection.ltr,\\n );\\n\\n textPainter.layout(\\n minWidth: 0,\\n maxWidth: size.width,\\n );\\n final double xCenter = (size.width - textPainter.width) / 2;\\n final double yCenter = (size.height - textPainter.height) / 2;\\n\\n canvas.saveLayer(rect.inflate(20), Paint());\\n final Paint rectanglePaint = Paint()\\n ..color = rectangleColor\\n ..style = PaintingStyle.fill;\\n\\n canvas.drawRRect(roundedRect, rectanglePaint);\\n final Paint cutoutPaint = Paint()\\n ..color = Colors.white\\n ..style = PaintingStyle.fill\\n ..blendMode = BlendMode.dstOut;\\n\\n canvas.saveLayer(rect.inflate(20), cutoutPaint);\\n textPainter.paint(canvas, Offset(xCenter, yCenter));\\n canvas.restore();\\n canvas.restore();\\n }\\n\\n @override\\n bool shouldRepaint(covariant RectangleTextCutoutPainter oldDelegate) {\\n return oldDelegate.cornerRadius != cornerRadius ||\\n oldDelegate.text != text ||\\n oldDelegate.fontSize != fontSize ||\\n oldDelegate.rectangleColor != rectangleColor;\\n }\\n}\\n
\\n你可以进一步扩展这个效果,例如添加动画、使用自定义字体、或者结合其他绘制效果来创造更加独特的视觉体验。
\\n希望这篇教程对你有所帮助!如果你有任何问题或想法,欢迎在评论区留言。祝你编码愉快!🚀
","description":"📱 引言 哈哈,2019年初我刚入职时,遇到了一个特别的需求:学校的卡片上要有个分类标签,文字部分还得镂空。当时我刚开始接触Flutter,对很多功能都不熟悉,这个需求就一直没能实现,成了我的一个小执念。现在我早已不在那儿工作了,可这两天闲来无事,突然想起了这个事。趁着五一假期,我开始琢磨画笔功能,终于把当年实现不了的功能给实现了😎!\\n\\nTip: 这时候可能会有人说:啊,这道题我会,用ShaderMask配置blendMode: BlendMode.srcOut就能实现,但实际上这个组件不能设置圆角,内边距等相关内容,如果这时候添加一个Contai…","guid":"https://juejin.cn/post/7499013568122552355","author":"淡写成灰","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-01T20:29:38.716Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/027f7804c2a242b5a32ae41513f75622~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5reh5YaZ5oiQ54Gw:q75.awebp?rk3s=f64ab15b&x-expires=1746736210&x-signature=l6yeqzw4BacJICjbop%2FTfsCacwA%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter——数据库Drift开发详细教程(一)","url":"https://juejin.cn/post/7499013941126316032","content":"\\n\\n首先,让我们将Drift添加到你的项目中pubspec.yaml。除了核心Drift依赖项(drift以及drift_dev生成代码)之外,我们还添加了一个包,用于在相应的平台上打开数据库。
\\n
dependencies:\\n # 数据库\\n drift: ^2.26.0 \\n drift_flutter: ^0.2.4\\n path_provider: ^2.1.5\\n path: ^1.9.0\\n\\ndev_dependencies:\\n drift_dev: ^2.26.0\\n build_runner: ^2.4.15\\n
\\n\\n\\n每个使用 Drift 的项目都需要至少一个类来访问数据库。在本例中,我们假设这个数据库类定义在名为 的文件中database.dart,该文件位于 目录下的某个位置lib/。当然,您可以将此类放在任何您喜欢的 Dart 文件中。\\n这里我们要注意,提前在database.dart,相同目录添加空文件database.g.dart,
\\n
import \'package:drift/drift.dart\';\\n\\npart \'database.g.dart\'; //执行 dart run build_runner build 会生成对应的操作当前表数据库代码\\n\\nclass TodoItems extends Table {\\n IntColumn get id => integer().autoIncrement()();\\n TextColumn get title => text().withLength(min: 6, max: 32)();\\n TextColumn get content => text().named(\'body\')();\\n DateTimeColumn get createdAt => dateTime().nullable()();\\n}\\n\\n@DriftDatabase(tables: [TodoItems])\\nclass AppDatabase extends _$AppDatabase {\\n AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());\\n\\n @override\\n int get schemaVersion => 1;\\n\\n static QueryExecutor _openConnection() {\\n return driftDatabase(\\n name: \'my_database\',\\n native: const DriftNativeOptions(\\n // By default, `driftDatabase` from `package:drift_flutter` stores the\\n // database files in `getApplicationDocumentsDirectory()`.\\n databaseDirectory: getApplicationSupportDirectory,\\n ),\\n // If you need web support, see https://drift.simonbinder.eu/platforms/web/\\n );\\n }\\n}\\n
\\n//1.创建一条新数据对象\\n await managers.todoItems\\n .create((o) => o(title: \'Title\', content: \'Content\'));\\n//2.创建新数据对象,并且可以执行创建方式\\n await managers.todoItems.create(\\n (o) => o(title: \'Title\', content: \'New Content\'),\\n mode: InsertMode.replace);\\n\\n// 3.可以同时创建多个数据对象\\n await managers.todoItems.bulkCreate(\\n (o) => [\\n o(title: \'Title 1\', content: \'Content 1\'),\\n o(title: \'Title 2\', content: \'Content 2\'),\\n ],\\n );\\n
\\n//1.删除所有数据\\n await managers.todoItems.delete();\\n// 2.根据id删除某一条数据\\n await managers.todoItems.filter((f) => f.id(5)).delete();\\n
\\n // 1.更新所有数据\\n await managers.todoItems\\n .update((o) => o(content: Value(\'New Content\')));\\n\\n // 2.根据条件更新部分数据\\n await managers.todoItems\\n .filter((f) => f.id.isIn([1, 2, 3]))\\n .update((o) => o(content: Value(\'New Content\')));\\n
\\n// 1.获取所有数据\\n managers.todoItems.get().then((onValue) {\\n this.onValue = onValue;\\n setState(() {});\\n });\\n // 2.当数据流有所改变,则会更新\\n managers.todoItems.watch();\\n\\n // 3.根据匹配ID获取某一个数据对象\\n await managers.todoItems.filter((f) => f.id(1)).getSingle();\\n
\\nimport \'package:drift/drift.dart\' as drift;\\nimport \'package:flutter/material.dart\';\\nimport \'package:flutter_demo/drift/database.dart\';\\n\\nclass DriftPage extends StatefulWidget {\\n const DriftPage({Key? key}) : super(key: key);\\n\\n @override\\n _DriftPageState createState() => _DriftPageState();\\n}\\n\\nclass _DriftPageState extends State<DriftPage> {\\n late AppDatabase appDatabase;\\n String content = \\"\\";\\n\\n late $AppDatabaseManager managers;\\n List<TodoItem> onValue = [];\\n\\n @override\\n void initState() {\\n super.initState();\\n appDatabase = AppDatabase();\\n managers = appDatabase.managers;\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: Text(\\"drift\\"),\\n ),\\n body: SingleChildScrollView(\\n child: Container(\\n child: Column(\\n children: [\\n ElevatedButton(\\n onPressed: () async {\\n createTodoItem();\\n },\\n child: const Text(\'插入 insert\'),\\n ),\\n ElevatedButton(\\n onPressed: () async {\\n selectTodoItems();\\n },\\n child: const Text(\'查询 select\'),\\n ),\\n ElevatedButton(\\n onPressed: () async {\\n updateTodoItems();\\n setState(() {});\\n },\\n child: const Text(\'更新 update\'),\\n ),\\n ElevatedButton(\\n onPressed: () async {\\n deleteTodoItems();\\n },\\n child: const Text(\'删除 delete\'),\\n ),\\n listWidget(onValue)\\n ],\\n ),\\n ),\\n ),\\n );\\n }\\n\\n listWidget(List<TodoItem> onValue) {\\n return Column(\\n children: onValue\\n .map((item) => Column(\\n children: [\\n Text(item.id.toString()),\\n Text(item.title),\\n Text(item.content),\\n ],\\n ))\\n .toList(),\\n );\\n }\\n\\n Future<void> selectTodoItems() async {\\n // Get all items\\n managers.todoItems.get().then((onValue) {\\n this.onValue = onValue;\\n setState(() {});\\n });\\n // A stream of all the todo items, updated in real-time\\n managers.todoItems.watch();\\n\\n // To get a single item, apply a filter and call `getSingle`\\n await managers.todoItems.filter((f) => f.id(1)).getSingle();\\n }\\n\\n Future<void> updateTodoItems() async {\\n // 更新所有数据\\n await managers.todoItems\\n .update((o) => o(content: drift.Value(\'New Content\')));\\n\\n // 根据条件更新部分数据\\n await managers.todoItems\\n .filter((f) => f.id.isIn([1, 2, 3]))\\n .update((o) => o(content: drift.Value(\'New Content\')));\\n }\\n\\n Future<void> replaceTodoItems() async {\\n // Replace a single item\\n var obj = await managers.todoItems.filter((o) => o.id(1)).getSingle();\\n obj = obj.copyWith(content: \'New Content\');\\n await managers.todoItems.replace(obj);\\n\\n // Replace multiple items\\n var objs =\\n await managers.todoItems.filter((o) => o.id.isIn([1, 2, 3])).get();\\n objs = objs.map((o) => o.copyWith(content: \'New Content\')).toList();\\n await managers.todoItems.bulkReplace(objs);\\n }\\n\\n Future<void> createTodoItem() async {\\n // Create a new item\\n await managers.todoItems\\n .create((o) => o(title: \'Title\', content: \'Content\'));\\n\\n // We can also use `mode` and `onConflict` parameters, just\\n // like in the `[InsertStatement.insert]` method on the table\\n await managers.todoItems.create(\\n (o) => o(title: \'Title\', content: \'New Content\'),\\n mode: drift.InsertMode.replace);\\n\\n // We can also create multiple items at once\\n await managers.todoItems.bulkCreate(\\n (o) => [\\n o(title: \'Title 1\', content: \'Content 1\'),\\n o(title: \'Title 2\', content: \'Content 2\'),\\n ],\\n );\\n setState(() {});\\n }\\n\\n Future<void> deleteTodoItems() async {\\n // Delete all items\\n await managers.todoItems.delete();\\n\\n setState(() {\\n this.onValue = [];\\n });\\n // Delete a single item\\n await managers.todoItems.filter((f) => f.id(5)).delete();\\n\\n }\\n}\\n\\n
\\n// GENERATED CODE - DO NOT MODIFY BY HAND\\n\\npart of \'database.dart\';\\n\\n// ignore_for_file: type=lint\\nclass $TodoItemsTable extends TodoItems\\n with TableInfo<$TodoItemsTable, TodoItem> {\\n @override\\n final GeneratedDatabase attachedDatabase;\\n final String? _alias;\\n $TodoItemsTable(this.attachedDatabase, [this._alias]);\\n static const VerificationMeta _idMeta = const VerificationMeta(\'id\');\\n @override\\n late final GeneratedColumn<int> id = GeneratedColumn<int>(\\n \'id\', aliasedName, false,\\n hasAutoIncrement: true,\\n type: DriftSqlType.int,\\n requiredDuringInsert: false,\\n defaultConstraints:\\n GeneratedColumn.constraintIsAlways(\'PRIMARY KEY AUTOINCREMENT\'));\\n static const VerificationMeta _titleMeta = const VerificationMeta(\'title\');\\n @override\\n late final GeneratedColumn<String> title = GeneratedColumn<String>(\\n \'title\', aliasedName, false,\\n additionalChecks:\\n GeneratedColumn.checkTextLength(minTextLength: 0, maxTextLength: 32),\\n type: DriftSqlType.string,\\n requiredDuringInsert: true);\\n static const VerificationMeta _contentMeta =\\n const VerificationMeta(\'content\');\\n @override\\n late final GeneratedColumn<String> content = GeneratedColumn<String>(\\n \'body\', aliasedName, false,\\n type: DriftSqlType.string, requiredDuringInsert: true);\\n static const VerificationMeta _createdAtMeta =\\n const VerificationMeta(\'createdAt\');\\n @override\\n late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>(\\n \'created_at\', aliasedName, true,\\n type: DriftSqlType.dateTime, requiredDuringInsert: false);\\n @override\\n List<GeneratedColumn> get $columns => [id, title, content, createdAt];\\n @override\\n String get aliasedName => _alias ?? actualTableName;\\n @override\\n String get actualTableName => $name;\\n static const String $name = \'todo_items\';\\n @override\\n VerificationContext validateIntegrity(Insertable<TodoItem> instance,\\n {bool isInserting = false}) {\\n final context = VerificationContext();\\n final data = instance.toColumns(true);\\n if (data.containsKey(\'id\')) {\\n context.handle(_idMeta, id.isAcceptableOrUnknown(data[\'id\']!, _idMeta));\\n }\\n if (data.containsKey(\'title\')) {\\n context.handle(\\n _titleMeta, title.isAcceptableOrUnknown(data[\'title\']!, _titleMeta));\\n } else if (isInserting) {\\n context.missing(_titleMeta);\\n }\\n if (data.containsKey(\'body\')) {\\n context.handle(_contentMeta,\\n content.isAcceptableOrUnknown(data[\'body\']!, _contentMeta));\\n } else if (isInserting) {\\n context.missing(_contentMeta);\\n }\\n if (data.containsKey(\'created_at\')) {\\n context.handle(_createdAtMeta,\\n createdAt.isAcceptableOrUnknown(data[\'created_at\']!, _createdAtMeta));\\n }\\n return context;\\n }\\n\\n @override\\n Set<GeneratedColumn> get $primaryKey => {id};\\n @override\\n TodoItem map(Map<String, dynamic> data, {String? tablePrefix}) {\\n final effectivePrefix = tablePrefix != null ? \'$tablePrefix.\' : \'\';\\n return TodoItem(\\n id: attachedDatabase.typeMapping\\n .read(DriftSqlType.int, data[\'${effectivePrefix}id\'])!,\\n title: attachedDatabase.typeMapping\\n .read(DriftSqlType.string, data[\'${effectivePrefix}title\'])!,\\n content: attachedDatabase.typeMapping\\n .read(DriftSqlType.string, data[\'${effectivePrefix}body\'])!,\\n createdAt: attachedDatabase.typeMapping\\n .read(DriftSqlType.dateTime, data[\'${effectivePrefix}created_at\']),\\n );\\n }\\n\\n @override\\n $TodoItemsTable createAlias(String alias) {\\n return $TodoItemsTable(attachedDatabase, alias);\\n }\\n}\\n\\nclass TodoItem extends DataClass implements Insertable<TodoItem> {\\n final int id;\\n final String title;\\n final String content;\\n final DateTime? createdAt;\\n const TodoItem(\\n {required this.id,\\n required this.title,\\n required this.content,\\n this.createdAt});\\n @override\\n Map<String, Expression> toColumns(bool nullToAbsent) {\\n final map = <String, Expression>{};\\n map[\'id\'] = Variable<int>(id);\\n map[\'title\'] = Variable<String>(title);\\n map[\'body\'] = Variable<String>(content);\\n if (!nullToAbsent || createdAt != null) {\\n map[\'created_at\'] = Variable<DateTime>(createdAt);\\n }\\n return map;\\n }\\n\\n TodoItemsCompanion toCompanion(bool nullToAbsent) {\\n return TodoItemsCompanion(\\n id: Value(id),\\n title: Value(title),\\n content: Value(content),\\n createdAt: createdAt == null && nullToAbsent\\n ? const Value.absent()\\n : Value(createdAt),\\n );\\n }\\n\\n factory TodoItem.fromJson(Map<String, dynamic> json,\\n {ValueSerializer? serializer}) {\\n serializer ??= driftRuntimeOptions.defaultSerializer;\\n return TodoItem(\\n id: serializer.fromJson<int>(json[\'id\']),\\n title: serializer.fromJson<String>(json[\'title\']),\\n content: serializer.fromJson<String>(json[\'content\']),\\n createdAt: serializer.fromJson<DateTime?>(json[\'createdAt\']),\\n );\\n }\\n @override\\n Map<String, dynamic> toJson({ValueSerializer? serializer}) {\\n serializer ??= driftRuntimeOptions.defaultSerializer;\\n return <String, dynamic>{\\n \'id\': serializer.toJson<int>(id),\\n \'title\': serializer.toJson<String>(title),\\n \'content\': serializer.toJson<String>(content),\\n \'createdAt\': serializer.toJson<DateTime?>(createdAt),\\n };\\n }\\n\\n TodoItem copyWith(\\n {int? id,\\n String? title,\\n String? content,\\n Value<DateTime?> createdAt = const Value.absent()}) =>\\n TodoItem(\\n id: id ?? this.id,\\n title: title ?? this.title,\\n content: content ?? this.content,\\n createdAt: createdAt.present ? createdAt.value : this.createdAt,\\n );\\n TodoItem copyWithCompanion(TodoItemsCompanion data) {\\n return TodoItem(\\n id: data.id.present ? data.id.value : this.id,\\n title: data.title.present ? data.title.value : this.title,\\n content: data.content.present ? data.content.value : this.content,\\n createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,\\n );\\n }\\n\\n @override\\n String toString() {\\n return (StringBuffer(\'TodoItem(\')\\n ..write(\'id: $id, \')\\n ..write(\'title: $title, \')\\n ..write(\'content: $content, \')\\n ..write(\'createdAt: $createdAt\')\\n ..write(\')\'))\\n .toString();\\n }\\n\\n @override\\n int get hashCode => Object.hash(id, title, content, createdAt);\\n @override\\n bool operator ==(Object other) =>\\n identical(this, other) ||\\n (other is TodoItem &&\\n other.id == this.id &&\\n other.title == this.title &&\\n other.content == this.content &&\\n other.createdAt == this.createdAt);\\n}\\n\\nclass TodoItemsCompanion extends UpdateCompanion<TodoItem> {\\n final Value<int> id;\\n final Value<String> title;\\n final Value<String> content;\\n final Value<DateTime?> createdAt;\\n const TodoItemsCompanion({\\n this.id = const Value.absent(),\\n this.title = const Value.absent(),\\n this.content = const Value.absent(),\\n this.createdAt = const Value.absent(),\\n });\\n TodoItemsCompanion.insert({\\n this.id = const Value.absent(),\\n required String title,\\n required String content,\\n this.createdAt = const Value.absent(),\\n }) : title = Value(title),\\n content = Value(content);\\n static Insertable<TodoItem> custom({\\n Expression<int>? id,\\n Expression<String>? title,\\n Expression<String>? content,\\n Expression<DateTime>? createdAt,\\n }) {\\n return RawValuesInsertable({\\n if (id != null) \'id\': id,\\n if (title != null) \'title\': title,\\n if (content != null) \'body\': content,\\n if (createdAt != null) \'created_at\': createdAt,\\n });\\n }\\n\\n TodoItemsCompanion copyWith(\\n {Value<int>? id,\\n Value<String>? title,\\n Value<String>? content,\\n Value<DateTime?>? createdAt}) {\\n return TodoItemsCompanion(\\n id: id ?? this.id,\\n title: title ?? this.title,\\n content: content ?? this.content,\\n createdAt: createdAt ?? this.createdAt,\\n );\\n }\\n\\n @override\\n Map<String, Expression> toColumns(bool nullToAbsent) {\\n final map = <String, Expression>{};\\n if (id.present) {\\n map[\'id\'] = Variable<int>(id.value);\\n }\\n if (title.present) {\\n map[\'title\'] = Variable<String>(title.value);\\n }\\n if (content.present) {\\n map[\'body\'] = Variable<String>(content.value);\\n }\\n if (createdAt.present) {\\n map[\'created_at\'] = Variable<DateTime>(createdAt.value);\\n }\\n return map;\\n }\\n\\n @override\\n String toString() {\\n return (StringBuffer(\'TodoItemsCompanion(\')\\n ..write(\'id: $id, \')\\n ..write(\'title: $title, \')\\n ..write(\'content: $content, \')\\n ..write(\'createdAt: $createdAt\')\\n ..write(\')\'))\\n .toString();\\n }\\n}\\n\\nabstract class _$AppDatabase extends GeneratedDatabase {\\n _$AppDatabase(QueryExecutor e) : super(e);\\n $AppDatabaseManager get managers => $AppDatabaseManager(this);\\n late final $TodoItemsTable todoItems = $TodoItemsTable(this);\\n @override\\n Iterable<TableInfo<Table, Object?>> get allTables =>\\n allSchemaEntities.whereType<TableInfo<Table, Object?>>();\\n @override\\n List<DatabaseSchemaEntity> get allSchemaEntities => [todoItems];\\n}\\n\\ntypedef $$TodoItemsTableCreateCompanionBuilder = TodoItemsCompanion Function({\\n Value<int> id,\\n required String title,\\n required String content,\\n Value<DateTime?> createdAt,\\n});\\ntypedef $$TodoItemsTableUpdateCompanionBuilder = TodoItemsCompanion Function({\\n Value<int> id,\\n Value<String> title,\\n Value<String> content,\\n Value<DateTime?> createdAt,\\n});\\n\\nclass $$TodoItemsTableFilterComposer\\n extends Composer<_$AppDatabase, $TodoItemsTable> {\\n $$TodoItemsTableFilterComposer({\\n required super.$db,\\n required super.$table,\\n super.joinBuilder,\\n super.$addJoinBuilderToRootComposer,\\n super.$removeJoinBuilderFromRootComposer,\\n });\\n ColumnFilters<int> get id => $composableBuilder(\\n column: $table.id, builder: (column) => ColumnFilters(column));\\n\\n ColumnFilters<String> get title => $composableBuilder(\\n column: $table.title, builder: (column) => ColumnFilters(column));\\n\\n ColumnFilters<String> get content => $composableBuilder(\\n column: $table.content, builder: (column) => ColumnFilters(column));\\n\\n ColumnFilters<DateTime> get createdAt => $composableBuilder(\\n column: $table.createdAt, builder: (column) => ColumnFilters(column));\\n}\\n\\nclass $$TodoItemsTableOrderingComposer\\n extends Composer<_$AppDatabase, $TodoItemsTable> {\\n $$TodoItemsTableOrderingComposer({\\n required super.$db,\\n required super.$table,\\n super.joinBuilder,\\n super.$addJoinBuilderToRootComposer,\\n super.$removeJoinBuilderFromRootComposer,\\n });\\n ColumnOrderings<int> get id => $composableBuilder(\\n column: $table.id, builder: (column) => ColumnOrderings(column));\\n\\n ColumnOrderings<String> get title => $composableBuilder(\\n column: $table.title, builder: (column) => ColumnOrderings(column));\\n\\n ColumnOrderings<String> get content => $composableBuilder(\\n column: $table.content, builder: (column) => ColumnOrderings(column));\\n\\n ColumnOrderings<DateTime> get createdAt => $composableBuilder(\\n column: $table.createdAt, builder: (column) => ColumnOrderings(column));\\n}\\n\\nclass $$TodoItemsTableAnnotationComposer\\n extends Composer<_$AppDatabase, $TodoItemsTable> {\\n $$TodoItemsTableAnnotationComposer({\\n required super.$db,\\n required super.$table,\\n super.joinBuilder,\\n super.$addJoinBuilderToRootComposer,\\n super.$removeJoinBuilderFromRootComposer,\\n });\\n GeneratedColumn<int> get id =>\\n $composableBuilder(column: $table.id, builder: (column) => column);\\n\\n GeneratedColumn<String> get title =>\\n $composableBuilder(column: $table.title, builder: (column) => column);\\n\\n GeneratedColumn<String> get content =>\\n $composableBuilder(column: $table.content, builder: (column) => column);\\n\\n GeneratedColumn<DateTime> get createdAt =>\\n $composableBuilder(column: $table.createdAt, builder: (column) => column);\\n}\\n\\nclass $$TodoItemsTableTableManager extends RootTableManager<\\n _$AppDatabase,\\n $TodoItemsTable,\\n TodoItem,\\n $$TodoItemsTableFilterComposer,\\n $$TodoItemsTableOrderingComposer,\\n $$TodoItemsTableAnnotationComposer,\\n $$TodoItemsTableCreateCompanionBuilder,\\n $$TodoItemsTableUpdateCompanionBuilder,\\n (TodoItem, BaseReferences<_$AppDatabase, $TodoItemsTable, TodoItem>),\\n TodoItem,\\n PrefetchHooks Function()> {\\n $$TodoItemsTableTableManager(_$AppDatabase db, $TodoItemsTable table)\\n : super(TableManagerState(\\n db: db,\\n table: table,\\n createFilteringComposer: () =>\\n $$TodoItemsTableFilterComposer($db: db, $table: table),\\n createOrderingComposer: () =>\\n $$TodoItemsTableOrderingComposer($db: db, $table: table),\\n createComputedFieldComposer: () =>\\n $$TodoItemsTableAnnotationComposer($db: db, $table: table),\\n updateCompanionCallback: ({\\n Value<int> id = const Value.absent(),\\n Value<String> title = const Value.absent(),\\n Value<String> content = const Value.absent(),\\n Value<DateTime?> createdAt = const Value.absent(),\\n }) =>\\n TodoItemsCompanion(\\n id: id,\\n title: title,\\n content: content,\\n createdAt: createdAt,\\n ),\\n createCompanionCallback: ({\\n Value<int> id = const Value.absent(),\\n required String title,\\n required String content,\\n Value<DateTime?> createdAt = const Value.absent(),\\n }) =>\\n TodoItemsCompanion.insert(\\n id: id,\\n title: title,\\n content: content,\\n createdAt: createdAt,\\n ),\\n withReferenceMapper: (p0) => p0\\n .map((e) => (e.readTable(table), BaseReferences(db, table, e)))\\n .toList(),\\n prefetchHooksCallback: null,\\n ));\\n}\\n\\ntypedef $$TodoItemsTableProcessedTableManager = ProcessedTableManager<\\n _$AppDatabase,\\n $TodoItemsTable,\\n TodoItem,\\n $$TodoItemsTableFilterComposer,\\n $$TodoItemsTableOrderingComposer,\\n $$TodoItemsTableAnnotationComposer,\\n $$TodoItemsTableCreateCompanionBuilder,\\n $$TodoItemsTableUpdateCompanionBuilder,\\n (TodoItem, BaseReferences<_$AppDatabase, $TodoItemsTable, TodoItem>),\\n TodoItem,\\n PrefetchHooks Function()>;\\n\\nclass $AppDatabaseManager {\\n final _$AppDatabase _db;\\n $AppDatabaseManager(this._db);\\n $$TodoItemsTableTableManager get todoItems =>\\n $$TodoItemsTableTableManager(_db, _db.todoItems);\\n}\\n\\n
\\n\\n\\n默认使用方法
\\n
late AppDatabase database;\\n\\nvoid main() {\\n database = AppDatabase();\\n runApp(MyFlutterApp());\\n}\\n
\\n\\n\\nprovider\\n如果您正在使用provider程序包,则可以将顶级小部件包装在管理数据库实例的提供程序中:
\\n
void main() {\\n runApp(\\n Provider<AppDatabase>(\\n create: (context) => AppDatabase(),\\n child: MyFlutterApp(),\\n dispose: (context, db) => db.close(),\\n ),\\n );\\n}\\n
\\n\\n\\nGetX 则可以将其添加为管理数据库实例的服务
\\n
void main() {\\n var put = Get.put(AppDatabase());\\n Get.find<AppDatabase>();\\n runApp(MyFlutterApp());\\n}\\n
\\n然后,您的小部件就可以使用 访问数据库了Get.find().your_method
","description":"1.配置项 首先,让我们将Drift添加到你的项目中pubspec.yaml。除了核心Drift依赖项(drift以及drift_dev生成代码)之外,我们还添加了一个包,用于在相应的平台上打开数据库。\\n\\ndependencies:\\n # 数据库\\n drift: ^2.26.0 \\n drift_flutter: ^0.2.4\\n path_provider: ^2.1.5\\n path: ^1.9.0\\n\\ndev_dependencies:\\n drift_dev: ^2.26.0\\n build_runner: ^2.4.15\\n\\n2.数据库表类…","guid":"https://juejin.cn/post/7499013941126316032","author":"又杀猪了","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-05-01T18:03:54.169Z","media":null,"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Dart 之Stream","url":"https://juejin.cn/post/7499014256546611226","content":"什么是Stream呢?我第一次看到这个单词的时候想到的就是流,但是感觉还是很不理解,流又是什么呢,后来我联系了一下想到了水流。带着这个联系我开始了解Dart中的Stream,发现这个命名也太符合了,和我们认为的水流差不多,只不过它是一条可由代码控制的异步水流,它既能像水流一样动态生成数据(如用户输入、网络响应),又能通过订阅主动控制数据的接收,甚至还可以暂停或关闭。是不是感到有点不可思议了?那我们一起去详细了解了解吧。
\\n学习一门新知识时,要先明白这个新知识的重要性,让我们足够重视它,然后再建立掌握新知识的学习目标。所以下面我们就一起看看Stream的重要性吧。
\\nStream 是Dart中表示连续异步数据序列的对象,用于处理多个按时间顺序传递的异步事件。是不是感到头大?说了等于没有说。我第一次看到的感觉也是这样,于是我尝试将它与我所知道的知识进行联系,最后发现它和水管中的水比较符合,只不过水不是异步的。下面我们就用水管中水的例子来一起了解一下。
\\n例子:想象一下,水管中的水不就是连续的吗?它不就是按照时间顺序传递的吗?只不过它不是异步的。
\\nStream使用订阅模式展开,按照订阅的个数分为单订阅流和广播流,就像这个水管只能有一个人用和多个人用。
\\n按照数据生成和订阅者的关系分为冷流和热流,以水管中的水理解冷流就是只有当有人用才会有水,而热流是不管你要不要用这水管中的水它都会有水流淌。
\\nStream的创建就是水管中加水的过程,其有三种创建方式,分别是StreamController创建、九大工厂构造函数创建和异步生成器async* 创建。下面先了解一下Stream都具有哪些属性,然后再分别介绍如何创建Stream。
\\nStream表示连续异步数据序列的对象。看到序列?是不是和集合有点关系啊。先来看看它的属性都有哪些。
\\n注意:isBroadcast的类型为bool类型而不是Future<bool>
,isBroadcast和isEmpty为原生属性,是直接定义在Stream内部的,其他则是扩展的。
StreamController创建流提供了写入端(sink属性)和读取端(stream属性),下面我们先看看它都具有哪些属性。
\\nFuture<void>
,用于控制器关闭时执行回调。示例:
\\nStreamController _streamController = StreamController();\\n\\nStream _streamWithController = _streamController.stream;\\n// print(_streamWithController.isEmpty);\\nStreamSink _sinkWithController = _streamController.sink;\\n_sinkWithController.add(1); // 使用Sink属性添加数据。\\n_sinkWithController.add(2);\\n_sinkWithController.add(3);\\n_sinkWithController.addError(Exception(\'错误\')); // 添加错误信息\\n\\n_sinkWithController.pause(); // 暂停流\\nprint(_streamController.isPaused()); // 输出:true\\n\\n_sinkWithController.resum(); // 恢复流\\n_sinkWithController.close(); // 关闭流\\n
\\n通过工厂构造方法可以快速创建特定类型的Stream。
\\nStream.fromIterable(Iterable<T> elements)
示例: 将字符串类型的列表转换为流
\\nStream<String> _streamWithIterable = Stream.fromIterable([\'apple\',\'bunana\',\'tea\',\'peach\',\'strawberry\']);\\n
\\nStream.formFuture(Future<T> future)
示例:
\\nStream<String> _streamWithFutures = Stream.fromFutures(Future(()=>\'第一个\'));\\n
\\nStream.formFutures(Iterable<Future<T>> futures)
示例:
\\nStream<String> _streamWithFutures = Stream.fromFutures([Future(()=>\'第一个\'),Future(()=>\'第二个\')]);\\n
\\nStream.periodic(Duration period,[T Function(int)? computation])
示例:
\\nStream _streamWithPeriodic = Stream.periodic(Duration(milliseconds: 300));\\n
\\nStream.value(T value)
示例:
\\nStream _streamWithValue = Stream.value(\'Hello Dart!\');\\n
\\nStream.error(Object error,[StackTrace? stackTrace])
示例:
\\nStream _streamWithError = Stream.error(Exception(\'错误\'));\\n
\\nStream.empty()
示例:
\\nStream _streamWithEmpty = Stream.empty();\\n
\\nStream.multi(void onListen(StreamController<T> controller))
示例:
\\nStream<int> _streamWithMulti = Stream.multi((controller){\\n controller.add(5);\\n controller.add(120);\\n controller.sink.add(23);\\n controller.close();\\n});\\n
\\nStream.eventTransformed(Stream source, EventSink<T> transform(EventSink<T> sink))
示例:
\\nStream _streamWithEventTransformed =\\n Stream.eventTransformed(Stream.value(5), (sink) {\\n sink.add(5);\\n return sink;\\n});\\n
\\nasync*
创建通过异步生成器函数创建,需要配合yield关键字。记忆方法:和使用async创建Future一样,只是Stream的创建上多个*
。
示例:
\\nStream<String> buildStream() async* {\\n int count = 1;\\n while (true) {\\n await Future.delayed(const Duration(seconds: 2));\\n yield \'第$count个\';\\n count++;\\n if (count > 10) {\\n throw Exception(\'当前$count超出范围大于10\');\\n }\\n }\\n}\\n
\\nStream的处理和生活中可能需要对水管中的水做过滤、截流等操作一致。Dart中提供了许多方法进行处理,下面我们逐步去看看都可以对其做哪些处理。
\\nmap<S>(S Function(T) convert)
:同步转换每个数据事件。asyncMap<S>(S Function(T) convert)
:异步转换每个数据事件。示例:
\\nStream<int> numStream = Stream.fromIterable([1,2,3,4,5]);\\nStream<int> takeStream = numStream.take(4); // 获取流中前4个\\nStream<int> skipStream = numStream.skip(2); // 跳过前面2个\\nStream<int> addOneStream = numStream.map((num)=>num+1); // 同步对每个数据加1\\nStream<int> addTwoStream = numStream.asyncMap((num)=>num+2); // 异步对每个数据加2\\nStream<int> whereStream = numStream.where((num)=>num>4); // 过滤大于4的\\n
\\nexpand<S>(Iterable<S> Function(T) convert)
:将数据事件按需求展开为多个事件。distinct([bool Function(T,T)? equals])
:重复的数据事件只选择一次。示例:
\\nStream<int> numStream = Stream.fromIterable([1,2,2,3,4,5]);\\n// 扩展所有数据,num值为1时扩展后为 1,2;num值为2时扩展后为 2,4。\\nStream<int> expandStream = numStream.expand((num) => [num,num*2]);\\n// 去重重复的数据事件2。\\nStream<int> distinctStream = numStream.distinct();\\n// 选择数据事件中小于3的。\\nStream<int> takeWhileStream = numStream.takeWhile((num)=>num<3);\\n// 跳过数据事件等于2的。\\nStream<int> skipWhileStream = numStream.skipWhile((num)=>num==2);\\n
\\ncast<S>
:将流的数据类型强制转换为指定的类型。transform<S>(StreamTransformer<T,S> transformer)
:应用自定义的转换器。pipe(StreamConsumer<T> consumer)
:将流数据传输到StreamConsumer。drain<T>([T? futureValue])
:资源清理,确保流完全消费。示例: transform和pipe后续文章介绍。
\\nStream<int> numStream = Stream.fromIterable([1,2,2,3,4,5]);\\nStream<int> asBroadcastStream = numStream.asBroadcastStream(); // 转换为广播流\\nStream<double> doubleStream = numStream.cast<double>(); // 转换数据类型为double\\nnumStream.drain(); // 确保流完全消费\\n
\\nFuture<bool>
。Future<bool>
。Future<void>
。join([String separator = \'\'])
:将流中数据拼接为字符串。示例:
\\nStream<int> numStream = Stream.fromIterable([1,2,2,3,4,5]);\\n// 检查是否包含4\\nFuture<bool> isContainsFour = numStream.contains(4); \\n// 检查流中所有数据是否都大于1\\nFuture<bool> isValid = numStream.every((num) => num > 1); \\n// 遍历流中所有数据并执行加2的操作。\\nFuture<void> a = numStream.forEach((num)=>num+2);\\n// 将流中所有数据相加\\nFuture<int> sum = numStream.reduce((a,b) => a + b);\\n// 以‘,’号分隔将流中数据拼接为字符串。\\nFuture<String> joinString = numStream.join(\',\');\\n
\\nStream的侦听指的是当订阅了流时,在流发出数据(data
)、错误(error
)或完成(done
)事件时执行相应的回调函数。就像水管中的水顺利到达(流发出数据)、中途发生管子破裂(错误)、用完水(完成)时执行的相应的处理。
Dart中主要通过listen进行Stream侦听处理,其返回为StreamSubscription的对象。下面主要介绍一下流订阅对象的一些方法。
\\n示例:
\\nStream<int> numStream = Stream.fromIterable([1,2,2,3,4,5]);\\nStreamSubscription<int> numStreamSubscription = numStream.listen((data)=>print);\\n// 出现错误时\\nnumStreamSubscription.onError((error)=>print);\\n// 流发出数据时\\nnumStreamSubscription.onData((data)=>print);\\n// 完成时\\nnumStreamSubscription.onDone(()=>print(\'完成\'));\\nnumStreamSubscription.pause(); // 暂停订阅\\nnumStreamSubscription.resume(); // 恢复订阅\\nnumStreamSubscription.cancel(); // 取消订阅\\n
\\n除了StreamSubscription对象提供处理外,listen()也提供了参数处理相应的回调。
\\n语法如下:
\\nStreamSubscription<T> subscription = stream.listen(\\n (T event) { /* 处理数据 */ },\\n onError: (Object error) { /* 处理错误 */ },\\n onDone: () { /* 流结束 */ },\\n cancelOnError: false, // 是否在遇到错误时自动取消订阅\\n);\\n
\\n本小节从生活中水管中的水流的例子出发,首先明确了Stream是一条由代码控制的异步水流,其次说明了Stream的不可或缺,然后介绍流的定义和流的分类,最后介绍了Stream的创建、处理、侦听。下面是本小节的归纳总结,以便于阅读者查缺补漏。
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n流的创建 | 流的处理 | 流的侦听 |
---|---|---|
1、StreamController创建: sink写入端、stream读取端 stream属性: isBroadcast 、isEmpty first 、last 、single 、length StreamController属性: sink 、stream isClosed 、isPaused hasListener 、done 2、工厂构造函数创建: fromIterable() formFuture() 、fromFutures() periodic() 、value() 、error() empty() 、multi() eventTransformed() 3、 async* 创建 | 1、选择、跳过、转换、过滤:take() 、skip() 、map() asyncMap() 、where() 2、扩展、去重、按需求选择或跳过: expand() 、distinct() takeWhile() 、skipWhile() 3、转换、传输、清理: asBroadcastStream() cast() 、transform() pipe() 、drain() 4、检查、遍历执行、聚合、拼接: contains() 、every() forEach() 、reduce() 、join() | listen() onData() onError() onDone() pause() resume() cancel() |
Flutter项目yaml配置的版本是6.0.2,当前最新的版本。如下所示:
\\nsignals: ^6.0.2\\n
\\nsignals的项目结构如下图所示:
\\n\\n如图所示:signals相关的有四个目录结构:preact_signals-1.8.3,signals-6.0.2,signals_core-6.0.2,signals_flutter-6.0.2。
以下面的示例代码作为调试的示例:
\\n final name = signal(\\"Jane\\");\\n final surname = signal(\\"Doe\\");\\n final fullName = computed(() => name.value + \\" \\" + surname.value);\\n\\n// Logs: \\"Jane Doe\\"\\n final dispose = effect(() {\\n print(\\"name is ${name.value}\\");\\n print(\\"fullName is ${fullName.value}\\");\\n });\\n// Updating one of its dependencies will automatically trigger\\n// the effect above, and will print \\"John Doe\\" to the console.\\n name.value = \\"John\\";\\n print(fullName.value);\\n
\\n当代码断点暂停到第6行的时候,name和surname版本号是0,而fullName版本号是1。当代码断点暂停到第13行的时候,因为name的value改变了,所以name和fullName的版本号个字更加1。
\\n\\nsignals可以堪称观察者设计模式的集大成者。首先我们先看一下Listenable,如下图所示:
\\nListenable的子类有Computed和Effect。
还需要再关注一下Node类,注意下图中圈选的属性target,如果源数据变化需要调用值为target的Listenable的notify方法。
\\n\\nNode是一个双向链表代替之前的Set集合,这种改变主要是性能的考虑,双向链表不仅有序,而且能在O(1)的时间复杂度删除前驱节点。
Signal的主要相关逻辑代码如下:
\\n@override\\n T get value {\\n final node = addDependency(this);\\n if (node != null) {\\n node.version = this.version;\\n }\\n return this.internalValue;\\n }\\n\\n /// Set the current value by a setter\\n set value(T val) => set(val);\\n\\n /// Set the current value by a method\\n bool set(\\n T val, {\\n /// Skip equality check and update the value\\n bool force = false,\\n }) {\\n if (force || !isInitialized || val != this.internalValue) {\\n internalSetValue(val);\\n return true;\\n }\\n return false;\\n }\\n\\n @internal\\n void internalSetValue(T val) {\\n if (batchIteration > 100) {\\n throw Exception(\'Cycle detected\');\\n }\\n\\n this.internalValue = val;\\n this.version++;\\n globalVersion++;\\n\\n startBatch();\\n try {\\n for (var node = targets; node != null; node = node.nextTarget) {\\n node.target.notify();\\n }\\n } finally {\\n endBatch();\\n }\\n }\\n
\\n如何通知相关的Listenable,signal中的数据发生了变化:其实很简单看上面代码中的value的set方法,最终调用set方法,方法内设置的新值是否和原来的值不相等,条件满足会调用internalSetValue方法;internalSetValue方法内部是按照targets的node链表依次调用Listenable的notify方法。所以在 signals里,会利用Node对象来通知存储在targets列表中的所有依赖者,当信号的值发生改变时,会遍历依赖者列表,并根据_version对比结果来触发更新。这样就完成了自动更新的逻辑。
\\nNode链表如何串联起来呢?如下面的示例代码:
\\n@internal\\nNode? addDependency(ReadonlySignal signal) {\\n if (evalContext == null) {\\n return null;\\n }\\n\\n var node = signal.node;\\n if (node == null || node.target != evalContext) {\\n /**\\n * `signal` is a new dependency. Create a new dependency node, and set it\\n * as the tail of the current context\'s dependency list. e.g:\\n *\\n * { A <-> B }\\n * ↑ ↑\\n * tail node (new)\\n * ↓\\n * { A <-> B <-> C }\\n * ↑\\n * tail (evalContext._sources)\\n */\\n node = Node()\\n ..version = 0\\n ..source = signal\\n ..prevSource = evalContext!.sources\\n ..nextSource = null\\n ..target = evalContext!\\n ..prevTarget = null\\n ..nextTarget = null\\n ..rollbackNode = node;\\n\\n if (evalContext!.sources != null) {\\n evalContext!.sources!.nextSource = node;\\n }\\n evalContext!.sources = node;\\n signal.node = node;\\n\\n // Subscribe to change notifications from this dependency if we\'re in an effect\\n // OR evaluating a computed signal that in turn has subscribers.\\n if ((evalContext!.flags & TRACKING) != 0) {\\n signal.subscribeToNode(node);\\n }\\n return node;\\n } else if (node.version == -1) {\\n // `signal` is an existing dependency from a previous evaluation. Reuse it.\\n node.version = 0;\\n\\n /**\\n * If `node` is not already the current tail of the dependency list (i.e.\\n * there is a next node in the list), then make the `node` the new tail. e.g:\\n *\\n * { A <-> B <-> C <-> D }\\n * ↑ ↑\\n * node ┌─── tail (evalContext._sources)\\n * └─────│─────┐\\n * ↓ ↓\\n * { A <-> C <-> D <-> B }\\n * ↑\\n * tail (evalContext._sources)\\n */\\n if (node.nextSource != null) {\\n node.nextSource!.prevSource = node.prevSource;\\n\\n if (node.prevSource != null) {\\n node.prevSource!.nextSource = node.nextSource;\\n }\\n\\n node.prevSource = evalContext!.sources;\\n node.nextSource = null;\\n\\n evalContext!.sources!.nextSource = node;\\n evalContext!.sources = node;\\n }\\n\\n // We can assume that the currently evaluated effect / computed signal is already\\n // subscribed to change notifications from `signal` if needed.\\n return node;\\n }\\n return null;\\n}\\n @override\\n void subscribeToNode(Node node) {\\n ReadonlySignal.internalSubscribe(this, node);\\n }\\n \\n @internal\\n static void internalSubscribe(ReadonlySignal signal, Node node) {\\n if (signal.targets != node && node.prevTarget == null) {\\n node.nextTarget = signal.targets;\\n if (signal.targets != null) {\\n signal.targets!.prevTarget = node;\\n }\\n signal.targets = node;\\n }\\n }\\n \\n
\\n从上面的代码中的addDependency方法可以看出node的添加有两种情况:1.node是新的,直接放到node的尾部节点。2.复用node的情况,把node的前驱节点和node后继节点首尾相连,然后将node节点放到node的尾部节点。这样就完成了自动状态绑和自动依赖跟踪。
\\nSignals提供细粒度的反应系统,可自动跟踪依赖关系并在不再需要时释放它们。不仅简单,而且高效。希望本文对您有所帮助,希望您编码愉快。
\\nsignals: pub.dev/packages/si…
\\n介绍signals:preactjs.com/blog/introd…
\\n\\n","description":"signals版本信息 Flutter项目yaml配置的版本是6.0.2,当前最新的版本。如下所示:\\n\\nsignals: ^6.0.2\\n\\nsignals的项目结构\\n\\nsignals的项目结构如下图所示:\\n\\n如图所示:signals相关的有四个目录结构:preact_signals-1.8.3,signals-6.0.2,signals_core-6.0.2,signals_flutter-6.0.2。\\n\\npreact_signals-1.8.3是preact_signals的dart实现版本。\\nsignals-6.0.2没有具体实现,只是导出signals…","guid":"https://juejin.cn/post/7498957261269467146","author":"技术蔡蔡","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-30T13:19:50.384Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/dc664f81365c4d65a75cfeb2ff19128d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oqA5pyv6JSh6JSh:q75.awebp?rk3s=f64ab15b&x-expires=1746623990&x-signature=2mMb6IIeh0Zmz9ZE5gtPb2l1AHc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/551288fdc77f466c8b51489b21d1d14e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oqA5pyv6JSh6JSh:q75.awebp?rk3s=f64ab15b&x-expires=1746623990&x-signature=qSwpnf7UsAE4Ub50kATXZJP4ZuU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/bf6e0b1cb3f343fab9ccb760a40f96ed~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oqA5pyv6JSh6JSh:q75.awebp?rk3s=f64ab15b&x-expires=1746623990&x-signature=i5kxTP6QUHHhTghWbLUsmCzSMUk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/38fa0c62b2cc4242aadf8c5a0beb0e96~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oqA5pyv6JSh6JSh:q75.awebp?rk3s=f64ab15b&x-expires=1746623990&x-signature=slpWCjr%2BjGAqaA%2Blh7e3yQdcphw%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["代码人生","Flutter","Preact"],"attachments":null,"extra":null,"language":null},{"title":"【Flutter 状态管理 - 捌】 | InheritedNotifier:轻量级原生状态管理工具 ✨","url":"https://juejin.cn/post/7498914140167159845","content":"状态管理就像一场接力赛🏃♂️,如何高效传递数据又不让代码变得臃肿,是每个开发者的必修课。当你的应用需要跨组件共享状态,但又不想引入复杂的框架时,InheritedNotifier
就像一位低调的\\"快递员\\",既能精准投递数据包裹📦,又能自动触发局部刷新。它巧妙结合了 InheritedWidget
的基因和 ChangeNotifier
的响应能力,是轻量级状态管理的隐藏宝藏。
本文通过系统化的思维方式,将带你揭开它的神秘面纱!
\\n操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\nInheritedWidget
和 ChangeNotifier
的\\"混血儿\\"👶InheritedNotifier
是 Flutter
框架中一个基于原生机制的状态管理工具,其本质是 InheritedWidget
与 ChangeNotifier
的技术融合,旨在实现跨组件树的高效数据共享与精准更新。
\\n\\n官方定义:一个能监听
\\nChangeNotifier
变化的InheritedWidget
。当关联的ChangeNotifier
发出更新通知时,依赖该InheritedNotifier
的子树会精准重建。
继承关系:继承自 InheritedWidget
,因此具备跨层级传递数据的能力。同时,它通过封装 ChangeNotifier
,实现了对数据变化的监听与响应。
核心机制:通过 InheritedWidget
的上下文传递机制,子组件可以直接从祖先节点获取共享数据;当绑定的 ChangeNotifier
调用 notifyListeners()
时,所有依赖该数据的子组件会自动触发重建,无需手动刷新。
深入理解:想象一个图书馆场景🏛️:
\\nInheritedWidget
:InheritedWidget
),贴着今日推荐书籍。每个读者(子组件
)需要手动走到公告板前检查是否有新书推荐。如果管理员忘记提醒,读者可能一直看着旧书单。InheritedNotifier
:“电子屏”
,背后连接了图书管理员的电脑(ChangeNotifier
)。当管理员添加新书时,电脑自动发送消息(notifyListeners()
),电子屏瞬间刷新(UI更新)。更棒的是,只有正在查看书单的读者(依赖该状态的组件)会收到消息,其他读者(无关组件)不受干扰。关键差异:传统 InheritedWidget
需要开发者手动触发更新(比如调用 markNeedsBuild
),而 InheritedNotifier
通过 ChangeNotifier
的监听通知机制,实现了“数据变化 →
自动广播 →
精准更新”的闭环。
InheritedWidget
):继承了“跨组件共享数据”
的基因,通过 of
方法让子组件轻松获取上层数据。ChangeNotifier
):继承了“监听-通知”
的神经反应机制,让数据变化自动触发 UI
响应。InheritedWidget
的“哑巴”
问题(无法自动更新)和 ChangeNotifier
的“孤独”
问题(无法跨组件共享),最终诞生了一个“会说话的数据共享者”
。业务逻辑与 UI
分离:
ChangeNotifier
子类:封装数据操作和业务规则(如网络请求、数据验证)。class AuthManager extends ChangeNotifier {\\n User? _user;\\n Future<void> login(String email, String password) { \\n // 网络请求、错误处理...\\n notifyListeners();\\n }\\n}\\n
\\nUI
组件:仅负责数据监听与渲染,实现纯函数式编程。工作原理:
\\nInheritedNotifier.of(context)
时,通过 dependOnInheritedWidgetOfExactType
方法建立组件与 InheritedNotifier
的依赖关系。notifyListeners()
触发时,框架仅标记依赖该数据的组件为“脏”
(需重建),而非整棵树刷新。代码量对比:InheritedNotifier
的实现仅需约 100
行代码(包括定义 ChangeNotifier
子类、包裹组件树和消费数据),远低于 Provider
或 Bloc
的配置代码。
// 核心代码示例(总行数 < 30)\\nclass Counter extends ChangeNotifier { /* 业务逻辑 */ }\\nInheritedNotifier(notifier: Counter(), child: ...)\\nfinal counter = InheritedNotifier.of<Counter>(context);\\n
\\n性能优势:
\\nMirroring
)开销。InheritedWidget
和 ChangeNotifier
,避免 Provider 的 ProxyProvider
或 Bloc 的 Stream
转换带来的性能损耗。适用场景:适合模块化开发中的局部状态(如单个页面内的表单状态、弹窗控制),避免全局状态管理的冗余。
\\nAPI
的直观组合技术栈依赖:
\\nFlutter
两大基础类:InheritedWidget
(数据传递机制)和 ChangeNotifier
(观察者模式实现)DSL
(如 Bloc
的 mapEventToState
)或复杂配置(如 Riverpod
的 ProviderScope
)。开发者体验:
\\nAPI
一致性:直接使用 of(context)
获取数据,符合 Flutter
开发习惯(类似 Theme.of(context)
)。Flutter DevTools
的 Widget Inspector
可直接追踪 InheritedNotifier
的依赖树。InheritedNotifier
的核心竞争力在于用最小技术成本解决 80%
的常见状态管理问题:
Flutter
开发者,它是理解状态管理机制的绝佳起点。“瑞士军刀”
🔧。广播电台
+ 收音机
”盒子 📻InheritedNotifier({\\n required ChangeNotifier? notifier, \\n Widget? child\\n})\\n
\\nnotifier
:好比一个广播电台(比如音乐电台、新闻电台)。
“发出信号”
(数据变化时通知),但自己不播放内容。notifier
)主动喊“我更新了!”
(调用 notifyListeners()
),收音机们(子组件)才会做出反应。null
,相当于广播塔没开,收音机收不到信号但不会爆炸(不会崩溃)。child
:就是收音机们所在的区域(子组件树
)。
of(context)
)找到最近的广播电台。child
)放了一堆收音机,它们都收听同一个电台。实际场景:
\\nInheritedNotifier(\\n notifier: myCounter, // 你的计数器(广播电台)\\n child: MyHomePage(), // 整个页面(收音机区域)\\n)\\n
\\nof
:收音机如何找电台 📡static T of<T>(BuildContext context) {\\n final widget = context.dependOnInheritedWidgetOfExactType<InheritedNotifier<T>>();\\n return widget!.notifier as T;\\n}\\n
\\nof(context)
的作用:好比你的收音机(子组件)说:“我要找最近的音乐电台!”(查找最近的 InheritedNotifier
)。它会沿着电线杆(组件树)往上爬,直到找到最近的广播塔(父组件中的 InheritedNotifier
)。
dependOnInheritedWidgetOfExactType
:这一步不仅找到了广播塔,还登记了依赖关系。
widget!
表示强制非空)。所以一定要确保父组件有 InheritedNotifier
!举个栗子🌰:
\\n// 在子组件中获取计数器\\nfinal counter = InheritedNotifier.of<CounterModel>(context);\\n// 相当于:找到最近的计数器广播塔,并订阅它的更新\\n
\\n避坑指南:
\\nfindAncestorWidgetOfExactType
(不登记依赖),但需要手动处理刷新。Provider
包(底层基于 InheritedNotifier
),更省心!😎notifyListeners()
:电台喊一嗓子 📢// 在 ChangeNotifier 子类中调用\\nvoid increment() {\\n _count++;\\n notifyListeners(); // 触发所有订阅者更新\\n}\\n
\\n相当于电台主持人突然喊:“注意!最新消息!现在播放周杰伦新歌!” 所有登记过的收音机(子组件)立刻刷新界面,显示新数据。
\\n轻量级场景首选:
\\nInheritedNotifier
轻便又高效,不需要上 Provider
全家桶。别用它做全局状态:
\\nProvider
或 Riverpod
,好比用顺丰快递(专业工具)送大件包裹。小心内存泄漏:
\\nnotifier
是全局单例,记得在页面销毁时清理(比如 dispose
)。三步实现状态共享
// 步骤 1:创建 `ChangeNotifier` 子类\\nclass Counter extends ChangeNotifier {\\n int _count = 0;\\n int get count => _count;\\n \\n void increment() {\\n _count++;\\n notifyListeners(); // 🔥触发更新\\n }\\n}\\n\\n// 步骤 2:用 `InheritedNotifier` 包裹子树\\nclass MyApp extends StatelessWidget {\\n final Counter counter = Counter();\\n \\n @override\\n Widget build(BuildContext context) {\\n return InheritedNotifier(\\n notifier: counter,\\n child: MaterialApp(\\n home: Scaffold(\\n body: ChildWidget(),\\n ),\\n ),\\n );\\n }\\n}\\n\\n// 步骤 3:子组件中获取状态\\nclass ChildWidget extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n final counter = InheritedNotifier.of<Counter>(context);\\n return Text(\'Count: ${counter.count}\');\\n }\\n}\\n
\\n方案 | 特点 | 适用场景 |
---|---|---|
InheritedNotifier | 轻量、精准更新、原生支持 | 局部状态、简单交互 |
Provider | 功能丰富、依赖注入 | 中大型应用、复杂状态 |
Riverpod | 灵活、编译安全 | 替代 Provider 的现代方案 |
Bloc | 事件驱动、强分离逻辑 | 需要严格状态管理的应用 |
\\n\\n如果只是父子组件间的简单状态共享,
\\nInheritedNotifier
的简洁性完胜!但对于全局状态,建议选择Provider
或Riverpod
。
支持持久化与动态切换
需求描述: 大型电商 App
需要实现白天/黑夜模式切换,且用户选择的主题需持久化存储,并在全局范围内生效(包括导航栏、按钮、文本颜色等)。
// 主题数据模型 \\nenum AppTheme { light, dark }\\n\\nextension AppThemeExtension on AppTheme {\\n ThemeData get themeData {\\n switch (this) {\\n case AppTheme.light:\\n return ThemeData(\\n brightness: Brightness.light,\\n primaryColor: Colors.blue,\\n scaffoldBackgroundColor: Colors.white,\\n );\\n case AppTheme.dark:\\n return ThemeData(\\n brightness: Brightness.dark,\\n primaryColor: Colors.grey[900],\\n scaffoldBackgroundColor: Colors.black,\\n );\\n }\\n }\\n}\\n\\n// 状态管理类\\nclass ThemeManager extends ChangeNotifier {\\n AppTheme _currentTheme = AppTheme.light;\\n final SharedPreferences _prefs;\\n\\n ThemeManager(this._prefs) {\\n // 初始化时读取本地存储\\n _currentTheme = AppTheme.values[_prefs.getInt(\'theme\') ?? 0];\\n }\\n\\n AppTheme get currentTheme => _currentTheme;\\n\\n void toggleTheme() {\\n _currentTheme = _currentTheme == AppTheme.light \\n ? AppTheme.dark \\n : AppTheme.light;\\n _prefs.setInt(\'theme\', _currentTheme.index); // 持久化存储\\n notifyListeners();\\n }\\n}\\n\\n// 根组件初始化 \\nvoid main() async {\\n WidgetsFlutterBinding.ensureInitialized();\\n final prefs = await SharedPreferences.getInstance();\\n runApp(\\n InheritedNotifier(\\n notifier: ThemeManager(prefs),\\n child: const MyApp(),\\n ),\\n );\\n}\\n\\nclass MyApp extends StatelessWidget {\\n const MyApp({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n final theme = InheritedNotifier.of<ThemeManager>(context).currentTheme;\\n return MaterialApp(\\n theme: theme.themeData,\\n home: const HomePage(),\\n );\\n }\\n}\\n\\n// 页面使用示例 \\nclass HomePage extends StatelessWidget {\\n const HomePage({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n final themeManager = InheritedNotifier.of<ThemeManager>(context);\\n return Scaffold(\\n appBar: AppBar(\\n title: const Text(\'主题切换\'),\\n ),\\n body: Switch(\\n value: themeManager.currentTheme == AppTheme.dark,\\n onChanged: (value) => themeManager.toggleTheme(),\\n ),\\n );\\n }\\n}\\n
\\n注意事项:
\\ntoggleTheme()
中异步保存数据,避免阻塞 UI
线程。enum
而非复杂对象存储主题类型,减少序列化开销。Color
值存储而非 enum
。含登录/退出/更新
需求描述:社交类 App
需要全局管理用户登录状态,支持以下功能:
Token
。// 用户数据模型 \\nclass User {\\n final String id;\\n final String token;\\n String nickname;\\n String avatarUrl;\\n\\n User({\\n required this.id,\\n required this.token,\\n required this.nickname,\\n required this.avatarUrl,\\n });\\n}\\n\\n// 状态管理类 \\nclass UserManager extends ChangeNotifier {\\n User? _currentUser;\\n final SecureStorage _storage; // 使用 flutter_secure_storage\\n\\n UserManager(this._storage);\\n\\n User? get currentUser => _currentUser;\\n\\n Future<void> login(String email, String password) async {\\n final response = await AuthApi.login(email, password); // 模拟网络请求\\n _currentUser = User(\\n id: response[\'id\'],\\n token: response[\'token\'],\\n nickname: response[\'nickname\'],\\n avatarUrl: response[\'avatar\'],\\n );\\n await _storage.write(key: \'auth_token\', value: _currentUser!.token);\\n notifyListeners();\\n }\\n\\n Future<void> logout() async {\\n await _storage.delete(key: \'auth_token\');\\n _currentUser = null;\\n notifyListeners();\\n }\\n\\n Future<void> updateProfile(String newNickname, String newAvatar) async {\\n _currentUser = _currentUser?.copyWith(\\n nickname: newNickname,\\n avatarUrl: newAvatar,\\n );\\n notifyListeners();\\n }\\n}\\n\\n// 根组件初始化 \\nvoid main() async {\\n WidgetsFlutterBinding.ensureInitialized();\\n final storage = SecureStorage();\\n runApp(\\n InheritedNotifier(\\n notifier: UserManager(storage),\\n child: const MyApp(),\\n ),\\n );\\n}\\n\\n// 页面使用示例 \\nclass ProfilePage extends StatelessWidget {\\n const ProfilePage({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n final userManager = InheritedNotifier.of<UserManager>(context);\\n return Scaffold(\\n appBar: AppBar(title: const Text(\'个人中心\')),\\n body: userManager.currentUser == null\\n ? TextButton(\\n onPressed: () => userManager.login(\'test@example.com\', \'123456\'),\\n child: const Text(\'登录\'),\\n )\\n : Column(\\n children: [\\n CircleAvatar(\\n backgroundImage: NetworkImage(userManager.currentUser!.avatarUrl),\\n ),\\n Text(userManager.currentUser!.nickname),\\n IconButton(\\n icon: const Icon(Icons.logout),\\n onPressed: userManager.logout,\\n ),\\n ],\\n ),\\n );\\n }\\n}\\n
\\n注意事项:
\\nflutter_secure_storage
保存 Token
,避免明文存储。login/logout
中处理网络请求和存储的异步性,可添加加载状态提示。copyWith
方法更新用户对象,避免直接修改状态(违反不可变原则)。UserManager
,未登录时跳转至登录页。InheritedNotifier
就像一位精干的\\"状态快递员\\"
🚴♂️,在需要轻量级共享状态的场景中表现卓越。它完美结合了 InheritedWidget
的上下文传递能力和 ChangeNotifier
的响应机制,既能避免 setState
的粗放更新,又无需复杂配置。虽然不适合超大型应用,但在模块化开发或局部状态管理中,它绝对是你的秘密武器🗡️。
\\n\\n工具的价值在于适用场景,下次遇到状态共享难题时,不妨先问问这位
\\n\\"快递员\\"
能否胜任!
\\n","description":"前言 状态管理就像一场接力赛🏃♂️,如何高效传递数据又不让代码变得臃肿,是每个开发者的必修课。当你的应用需要跨组件共享状态,但又不想引入复杂的框架时,InheritedNotifier 就像一位低调的\\"快递员\\",既能精准投递数据包裹📦,又能自动触发局部刷新。它巧妙结合了 InheritedWidget 的基因和 ChangeNotifier 的响应能力,是轻量级状态管理的隐藏宝藏。\\n\\n本文通过系统化的思维方式,将带你揭开它的神秘面纱!\\n\\n操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。\\n\\n本质定义:InheritedWidget 和 Chan…","guid":"https://juejin.cn/post/7498914140167159845","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-30T10:31:08.681Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/41c73893504b48eb9e3914e65aa860b7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1746613868&x-signature=bmTlqI5WIM2mScexiQFOQ9yjf8w%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ce39915693df4e76b79a04c98cdc2f1b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1746613868&x-signature=9AqYDnSmM3OeD%2BE5s8axXS5X%2FXM%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"[flutter web] 自行部署 firebase js,优化国内使用体验","url":"https://juejin.cn/post/7498901006924087334","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
上一篇文章介绍了通过修改 overwrite firebase_auth_web 库的方式,实现国内使用 firebase auth 服务。
\\n在我的使用过程中,发现国内下载 firebase js 的速度很慢,这会导致 firebase 初始化异常,无法使用 auth 服务。本文介绍如何自己部署 firebase js 文件,优化国内使用 firebase 的速度。
\\n注意:本文介绍的并不是官方文档里的方法 ,这条路子我没走通。
\\n上一次我们已经 fork 了 flutterfire 仓库,我们需要将所有的 gstatic 域名改为自己将要部署的域名。例如下面这个文件:
\\npackages/firebase_core/firebase_core_web/lib/src/firebase_core_web.dart
\\n全局搜索替换 www.gstatic.com 即可。
\\n自行下载 firebase js 文件,例如 auth 的 www.gstatic.cn/firebasejs/…
\\n可以看到文件最开始的 import 部分 import{_getProvider,_isFirebaseServerApp as e,_registerComponent as t,registerVersion as r,getApp as n,SDK_VERSION as i}from\\"https://www.gstatic.com/firebasejs/11.5.0/firebase-app.js\\"
中还是有官方域名 www.gstatic.com
,这里也要一并修改掉。这也是为什么我们不用 gstatic.cn 而是自己部署。
如果只需要用到 auth,那应该只需要两个文件:firebase-app.js 和 firebase-auth.js。记得修改 auth.js 中的 _shouldInitProactively()
函数,使他始终返回 false(原因不明,实在不熟悉 js,没细查了)。
部署后有必要的话记得处理跨域问题。
\\n上一次我们 overwrite 掉的 firebase_auth_web 这次就不需要了,改为 firebase_core_web。
\\ndependency_overrides:\\n # firebase_auth_web:\\n # git:\\n # url: https://github.com/p1gd0g/flutterfire.git\\n # path: packages/firebase_auth/firebase_auth_web\\n firebase_core_web:\\n git:\\n url: https://github.com/p1gd0g/flutterfire.git\\n path: packages/firebase_core/firebase_core_web\\n
\\n重新打包部署 flutter 后,firebase js 文件下载正常即可。
\\n在Flutter应用开发过程中,如果应用支持多种环境配置(如开发、预发布、生产环境),并且需要与Firebase进行集成,那么就需要额外进行一些配置工作,确保每种环境配置都对应不同的Firebase环境。
\\n最佳实践是为每种环境配置创建独立的Firebase项目,这样能够将开发、预发布和生产环境相互隔离。
\\n\\n\\n在使用自定义后端或像Supabase这样的Dart SDK时,我们可以通过切换URL和API密钥,使应用在不同环境配置下连接到正确的后端环境。然而,Firebase的集成情况有所不同,它没有提供直接的Dart SDK,并且还需要进行一些特定于平台的设置,这使得多环境配置过程变得更加复杂。
\\n
幸运的是,FlutterFire CLI可以帮助我们解决这些难题。接下来,我将逐步引导你了解如何使用它为Flutter和Firebase应用配置多环境,让整个过程变得轻松简单。
\\n本文将涵盖以下内容:
\\n读完本文后,你将能够熟练地将Firebase集成到多环境的Flutter应用中,节省开发时间,同时避免常见的配置问题。
\\n\\n\\n请注意:本文假设你已经拥有一个Flutter应用,并且该应用能够通过
\\ndev
(开发)、stg
(预发布)和prod
(生产)这几种环境配置,在iOS和Android平台上正常运行,具体运行命令如下:
flutter run --flavor dev\\nflutter run --flavor stg\\nflutter run --flavor prod\\n
\\n此外,你还需要准备三个对应的Firebase项目,例如:
\\n如果大家对Flutter多环境有兴趣,可以在评论区留言,我可以再做一期多环境相关的。
\\n一切准备就绪?那我们开始吧!🚀
\\n过去,Firebase的集成本身就是一个相当繁琐的过程,你需要为每个平台下载配置文件,比如iOS平台的GoogleService-Info.plist
文件和Android平台的google-services.json
文件。
而现在,这个过程变得简单多了。你只需要运行flutterfire configure
命令,然后按照一些交互式提示进行操作即可。完成操作后,这些文件会自动添加到项目中,包括:
lib/firebase_options.dart
ios/Runner/GoogleService-Info.plist
android/app/google-services.json
不过,在处理多环境配置时,情况就变得复杂起来。你需要为每种环境配置准备不同版本的这些文件,并且将它们存储在不同位置,以防止在配置过程中出现文件覆盖的问题。
\\n幸运的是,FlutterFire 1.0.0版本增加了对多环境配置的支持。下面我们就来详细了解一下如何使用这项功能。
\\n官方文档中详细介绍了安装Firebase和FlutterFire CLI的所有步骤。
\\nFirebase CLI可以作为一个独立的库安装,也可以通过npm安装。对于会使用npm
的开发者来说,npm
安装的方式更友好且可靠:
npm install -g firebase-tools\\n
\\n我最终选择安装的二进制,因为我的node并不兼容riebase CLI,而我又不太想折腾版本,毕竟我不会nodejs:
\\ncurl -sL https://firebase.tools | bash\\n
\\n安装完成后,可通过运行firebase --version
命令检查安装是否成功。
接下来,运行firebase login
命令进行登录,执行命令后会出现如下提示:
? Allow Firebase to collect CLI and Emulator Suite usage and error reporting information? Yes\\n
\\n此时会自动打开一个浏览器窗口,你需要使用与目标Firebase项目关联的Google账号进行登录。选择账号登录后,会出现相关授权提示。
\\n\\n点击“Allow”并关闭浏览器窗口,至此Firebase CLI就完成了登录操作。
\\n\\n注意:务必使用与目标Firebase项目关联的Google账号登录。如果使用了错误的账号,可以先运行
\\nfirebase logout
命令退出登录,然后再次运行firebase login
重新登录。
安装FlutterFire CLI的命令如下:
\\ndart pub global activate flutterfire_cli\\n
\\n安装完成后,运行flutterfire --version
命令,检查是否安装了1.0.0或更高版本。
以生成dev
环境配置文件的命令为例:
flutterfire config \\\\\\n --project=flutter-ship-dev \\\\\\n --out=lib/firebase_options_dev.dart \\\\\\n --ios-bundle-id=com.jarvanmo.dev \\\\\\n --ios-out=ios/flavors/dev/GoogleService-Info.plist \\\\\\n --android-package-name=com.jarvanmo.dev \\\\\\n --android-out=android/app/src/dev/google-services.json\\n
\\n各参数说明如下:
\\n--project
:指定要使用的Firebase项目(注意:需传入项目ID,而非别名)。--out
:Firebase配置文件的输出路径。--ios-bundle-id
:iOS应用的Bundle ID((打开Xcode,然后通过以下顺序找到Bundle ID:Runner>General>Identity>Bundle Identifier))。--ios-out
:iOS平台GoogleService-Info.plist
文件的输出路径。--android-package-name
:Android应用的包名(在android/app/build.gradle.kts
中,找到applicationId
对应的值)。--android-out
:Android平台google-services.json
文件的输出路径。\\n\\n要了解所有可用的选项,可以运行
\\nflutterfire config --help
。
使用该命令时,可按以下步骤操作:
\\nproject
、ios-bundle-id
和android-package-name
参数。但是,你需要对stg
和prod
环境配置重复上述操作,这样既耗时又容易出错。
下面我们来看看如何实现这个过程的自动化。👇
\\n虽然flutterfire config
命令已经帮我们完成了大部分工作,但仍需要为每种环境配置分别运行该命令,并设置不同的参数。
为了简化操作,我们可以在项目根目录下创建一个flutterfire-config.sh
脚本,内容如下:
#!/bin/bash \\n# 用于为不同环境/flavor生成Firebase配置文件的脚本 \\n# 欢迎复用和修改此脚本以适配你的项目\\nif [[ $# -eq 0 ]]; then \\n echo \\"错误:未指定环境。请使用\'dev\'、\'stg\'或\'prod\'。\\" \\n exit 1 \\nfi \\n\\ncase $1 in \\n dev) \\n flutterfire config \\\\\\n --project=flutter-ship-dev \\\\\\n --out=lib/firebase_options_dev.dart \\\\\\n --ios-bundle-id=com.codewithandrea.flutterShipApp.dev \\\\\\n --ios-out=ios/flavors/dev/GoogleService-Info.plist \\\\\\n --android-package-name=com.codewithandrea.flutter_ship_app.dev \\\\\\n --android-out=android/app/src/dev/google-services.json\\n ;; \\n stg) \\n flutterfire config \\\\\\n --project=flutter-ship-stg \\\\\\n --out=lib/firebase_options_stg.dart \\\\\\n --ios-bundle-id=com.codewithandrea.flutterShipApp.stg \\\\\\n --ios-out=ios/flavors/stg/GoogleService-Info.plist \\\\\\n --android-package-name=com.codewithandrea.flutter_ship_app.stg \\\\\\n --android-out=android/app/src/stg/google-services.json\\n ;; \\n prod) \\n flutterfire config \\\\\\n --project=flutter-ship-prod \\\\\\n --out=lib/firebase_options_prod.dart \\\\\\n --ios-bundle-id=com.codewithandrea.flutterShipApp \\\\\\n --ios-out=ios/flavors/prod/GoogleService-Info.plist \\\\\\n --android-package-name=com.codewithandrea.flutter_ship_app \\\\\\n --android-out=android/app/src/prod/google-services.json\\n ;; \\n *) \\n echo \\"错误:指定的环境无效。请使用\'dev\'、\'stg\'或\'prod\'。\\" \\n exit 1 \\n ;; \\nesac\\n
\\n有了这个脚本,虽然仍需要为项目设置正确的参数,但只需要设置一次。
\\n之后,生成所有Firebase配置文件就变得非常轻松,无需背下每个参数了。
\\n现在,是时候运行这个脚本了。👇
\\n若要配置dev
环境,运行以下命令:
./flutterfire-config.sh dev\\n
\\n运行命令后,会出现提示,首先选择要配置的平台:
\\n? Which platforms should your configuration support (use arrow keys & space to select)? ›\\n✔ android \\n✔ ios \\n macos \\n✔ web \\n windows\\n
\\n接下来,选择iOS上的Build configuration
:
? You have to choose a configuration type. Either build configuration (most likely choice) or a target set up. › \\n❯ Build configuration\\n Target\\n
\\n接着,选择Debug-dev
构建配置:
? Please choose one of the following build configurations ›\\n Debug \\n Release \\n Profile \\n❯ Debug-dev \\n Profile-dev \\n Release-dev \\n Debug-stg \\n Profile-stg \\n Release-stg \\n Debug-prod \\n Profile-prod \\n Release-prod\\n
\\n当然你可以通过重复运行该脚本实现更细致的Build configuration
配置
\\n\\n注意:如果遇到“Failed to list Firebase projects”错误,可先运行
\\nfirebase logout
命令,然后再运行firebase login
命令,之后重新尝试。
这一步可能会花费一些时间,因为CLI需要在Firebase中注册必要的应用。如果配置成功,会看到类似以下的确认信息:
\\n✔ You have to choose a configuration type. Either build configuration (most likely choice) or a target set up. · Build configuration\\n✔ Please choose one of the following build configurations · Debug-dev\\ni Found 40 Firebase projects. Selecting project flutter-ship-dev. \\n✔ Which platforms should your configuration support (use arrow keys & space to select)? · android, ios, web\\ni Firebase android app com.codewithandrea.flutter_ship_app.dev is not registered on Firebase project flutter-ship-dev.\\ni Registered a new Firebase android app on Firebase project flutter-ship-dev.\\ni Firebase ios app com.codewithandrea.flutterShipApp.dev is not registered on Firebase project flutter-ship-dev.\\ni Registered a new Firebase ios app on Firebase project flutter-ship-dev. \\ni Firebase web app flutter_ship_app (web) is not registered on Firebase project flutter-ship-dev.\\ni Registered a new Firebase web app on Firebase project flutter-ship-dev. \\n\\nFirebase configuration file lib/firebase_options_dev.dart generated successfully with the following Firebase apps:\\n\\nPlatform Firebase App Id\\nweb 1:424176442589:web:c86e231d1eeaba0e90cf34\\nandroid 1:424176442589:android:c5841ba53606b4c490cf34\\nios 1:424176442589:ios:592b56a800affa4e90cf34\\n\\nLearn more about using this file and next steps from the documentation:\\n > https://firebase.google.com/docs/flutter/setup\\n
\\n接下来,通过运行以下命令为stg
环境配置重复相同的操作:
./flutterfire-config.sh stg\\n
\\n最后,为prod
环境配置运行:
./flutterfire-config.sh prod\\n
\\n完成上述操作后,项目中会生成以下新文件:
\\nlib/firebase_options_dev.dart
lib/firebase_options_stg.dart
lib/firebase_options_prod.dart
ios/flavors/dev/GoogleService-Info.plist
ios/flavors/stg/GoogleService-Info.plist
ios/flavors/prod/GoogleService-Info.plist
android/app/src/dev/google-services.json
android/app/src/stg/google-services.json
android/app/src/prod/google-services.json
上述生成的配置文件本身并不包含敏感信息,因此从安全角度来说,将它们提交到Git仓库是可行的。
\\n如果按照上述所有步骤操作且未出现错误,此时所有的Firebase配置文件应该都已正确生成并添加到项目中。
\\n在使用Firebase运行应用之前,还需要完成以下几个步骤:
\\nfirebase_core
包,并验证应用在Android和iOS平台上能否正常运行。下面我们逐步来看。👇
\\nfirebase_core
包在终端中运行以下命令,添加firebase_core
包:
flutter pub add firebase_core\\nflutter pub get\\n
\\n如果你的Android应用是使用FlutterFire CLI 1.1.0或更高版本进行配置的,理论上应该可以正常运行,不会出现错误。
\\n但如果FlutterFire设置不正确,可能会遇到以下错误:
\\nPlugin [id: \'com.google.gms.google-services\'] was not found in any of the following sources:\\n\\n- Gradle Core Plugins (plugin is not in \'org.gradle\' namespace)\\n- Included Builds (No included builds contain this plugin)\\n- Plugin Repositories (plugin dependency must include a version number for this source)\\n
\\n要解决这个问题,打开android/settings.gradle.kts
文件,确保已将com.google.gms.google-services
作为插件添加,内容如下:
plugins { \\n id(\\"dev.flutter.flutter-plugin-loader\\") version \\"1.0.0\\" \\n id(\\"com.android.application\\") version \\"8.7.0\\" apply false \\n // START: FlutterFire Configuration \\n id(\\"com.google.gms.google-services\\") version(\\"4.3.15\\") apply false \\n // END: FlutterFire Configuration\\n id(\\"org.jetbrains.kotlin.android\\") version \\"1.8.22\\" apply false \\n}\\n
\\n同样,在android/app/build.gradle.kts
文件的plugins
块中,也应包含该插件:
plugins { \\n id(\\"com.android.application\\") \\n // START: FlutterFire Configuration \\n id(\\"com.google.gms.google-services\\") \\n // END: FlutterFire Configuration \\n id(\\"kotlin-android\\") \\n // Flutter Gradle插件必须在Android和Kotlin Gradle插件之后应用 \\n id(\\"dev.flutter.flutter-gradle-plugin\\") \\n}\\n
\\n应用此修复后,Android应用应该就能正常运行了。
\\n\\n\\n注意:你可以在Google的Maven仓库中找到
\\ncom.google.gms.google-services
的最新版本。
在运行iOS应用之前,打开ios/Podfile
文件,确保平台版本设置为13.0
或更高,内容如下:
# Uncomment this line to define a global platform for your project\\nplatform :ios, \'13.0\'\\n
\\n运行pod install
命令后,应该就可以在iOS平台上正常运行应用了。
根据官方文档,应将Firebase初始化代码添加到lib/main.dart
文件中,示例如下:
import \'package:flutter_ship_app/firebase_options.dart\'; \\n\\n// 在main()函数中\\nawait Firebase.initializeApp( \\n options: DefaultFirebaseOptions.currentPlatform, \\n);\\n
\\n然而,这种默认设置并不适用于我们的多环境配置场景,因为我们为每种环境配置都创建了单独的配置文件:
\\n\\n那么,该如何解决这个问题呢?
一种解决方法是创建一个firebase.dart
文件,内容如下:
// firebase.dart\\nimport \'package:firebase_core/firebase_core.dart\';\\nimport \'package:flutter/foundation.dart\';\\nimport \'package:flutter/services.dart\';\\nimport \'package:flutter_ship_app/firebase_options_prod.dart\' as prod;\\nimport \'package:flutter_ship_app/firebase_options_stg.dart\' as stg;\\nimport \'package:flutter_ship_app/firebase_options_dev.dart\' as dev;\\n\\nFuture<void> initializeFirebaseApp() async { \\n // 根据构flavor确定使用哪个Firebase选项 \\n final firebaseOptions = switch (appFlavor) { \\n \'prod\' => prod.DefaultFirebaseOptions.currentPlatform, \\n \'stg\' => stg.DefaultFirebaseOptions.currentPlatform, \\n \'dev\' => dev.DefaultFirebaseOptions.currentPlatform, \\n _ => throw UnsupportedError(\'无效的构建风味: $flavor\'), \\n }; \\n await Firebase.initializeApp(options: firebaseOptions\\n\\n\\n };\\n}\\n通过这种方式,它会根据`appFlavor`常量的值进行判断,返回对应环境配置的`FirebaseOptions`对象。\\n\\n需要注意的是,在Flutter web上使用`--flavor`选项运行时,会收到环境配置不完全支持的警告,但`appFlavor`常量依然能返回正确的值。\\n\\n此时,在`lib/main.dart`中只需简单调用`await initializeFirebaseApp()`即可完成初始化,`lib/main.dart`依旧作为应用的单一入口点:\\n```dart\\nimport \'firebase.dart\';\\n\\nvoid main() async { \\n WidgetsFlutterBinding.ensureInitialized(); \\n await initializeFirebaseApp(); \\n runApp(const MainApp()); \\n}\\n
\\n采用这种设置,Flutter应用就能根据不同的环境配置,初始化并连接到对应的Firebase项目。
\\n然而,这种方案存在一个隐患。👇
\\n仔细查看firebase.dart
文件可以发现,虽然会根据flavor选择正确的Firebase配置,但三个firebase_options_*.dart
文件都被导入了:
// firebase.dart\\nimport \'package:firebase_core/firebase_core.dart\';\\nimport \'package:flutter/foundation.dart\';\\nimport \'package:flutter/services.dart\';\\n// 注意:三个文件均被导入\\nimport \'package:flutter_ship_app/firebase_options_prod.dart\' as prod;\\nimport \'package:flutter_ship_app/firebase_options_stg.dart\' as stg;\\nimport \'package:flutter_ship_app/firebase_options_dev.dart\' as dev;\\n\\nFuture<void> initializeFirebaseApp() async {\\n // 根据flavor确定使用哪个Firebase选项\\n final firebaseOptions = switch (appFlavor) {\\n \'prod\' => prod.DefaultFirebaseOptions.currentPlatform,\\n \'stg\' => stg.DefaultFirebaseOptions.currentPlatform,\\n \'dev\' => dev.DefaultFirebaseOptions.currentPlatform,\\n _ => throw UnsupportedError(\'无效的构建风味: $flavor\'),\\n };\\n await Firebase.initializeApp(options: firebaseOptions);\\n}\\n
\\n这意味着在构建过程中,由于switch
操作是在运行时进行的,无法通过Tree shaking
去除未使用的代码,三个配置文件都会被编译并打包进最终的应用。
从理论上讲,如果有人对应用进行逆向工程,就有可能获取到开发或预发布环境的详细信息,而这些环境的安全性通常不如生产环境。
\\n对于不涉及敏感数据的应用,这或许不是大问题,但它始终是一个潜在风险。如果想完全规避该风险,可以考虑采用更安全的方案。👇
\\n如前所述,以下代码存在一定问题:
\\nimport \'package:flutter_ship_app/firebase_options_prod.dart\' as prod;\\nimport \'package:flutter_ship_app/firebase_options_stg.dart\' as stg;\\nimport \'package:flutter_ship_app/firebase_options_dev.dart\' as dev;\\n
\\n更安全的做法是创建三个独立的入口点文件——main_dev.dart
、main_stg.dart
和main_prod.dart
,示例如下:
// main_dev.dart\\nimport \'package:flutter_ship_app/firebase_options_dev.dart\';\\nimport \'main.dart\';\\n\\nvoid main() async {\\n runMainApp(DefaultFirebaseOptions.currentPlatform);\\n}\\n
\\n这些文件仅负责导入对应的firebase_options_*.dart
文件,并将配置参数传递给main.dart
中的函数,由该函数执行实际的初始化操作。main.dart
文件内容示例如下:
// main.dart\\nimport \'package:firebase_core/firebase_core.dart\';\\nimport \'package:flutter/material.dart\';\\n\\nvoid runMainApp(FirebaseOptions firebaseOptions) async {\\n WidgetsFlutterBinding.ensureInitialized();\\n await Firebase.initializeApp(options: firebaseOptions);\\n runApp(const MainApp());\\n}\\n
\\n这种方式确保每个构建版本仅包含所需的Firebase配置文件,是管理多环境配置的安全高效方案。
\\n那么,如何使用正确的环境配置运行应用呢?👇
\\n若采用上述第二种方案,项目中会有四个相关文件:
\\nmain_dev.dart
:dev
环境配置的入口点main_stg.dart
:stg
环境配置的入口点main_prod.dart
:prod
环境配置的入口点main.dart
:包含应用初始化核心代码可以通过以下命令以特定环境配置运行应用:
\\nflutter run --flavor dev -t lib/main_dev.dart\\nflutter run --flavor stg -t lib/main_stg.dart\\nflutter run --flavor prod -t lib/main_prod.dart\\n
\\n如此一来,每种环境配置都能使用对应的入口点,确保应用启动时连接到正确的Firebase环境。
\\n若采用此方案,记得更新本地配置文件(如.vscode/launch.json
)和CI/CD脚本,以适配这些改动。
两种方案各有利弊,具体选择需结合项目需求:
\\nmain.dart
文件,通过运行时的动态逻辑处理不同环境配置下的Firebase选项。但由于所有Firebase配置文件都会被打包进最终应用,从安全角度考虑,并非最佳选择。尽管方案2前期配置相对繁琐,但对于使用Firebase的多环境Flutter应用,我更推荐采用该方案。👍
\\n方案1更适用于非Firebase应用场景,此时可以使用--dart-define-from-file
,并在.env.dev
、.env.stg
、.env.prod
等单独文件中为每种环境配置定义环境变量。想了解更多内容,可阅读我之前写过的《flutter工程化之动态配置》。
通过将FlutterFire与简单的Shell脚本相结合,原本复杂易错的多环境配置流程得到了极大简化。现在,无需再为每种环境配置手动配置Firebase,仅需使用一个脚本就能为所有环境生成所需文件,既节省时间,又降低了出错概率。
\\n集中式Firebase初始化方案提供了一种快速便捷的方法,借助单一的main.dart
文件和运行时逻辑,使应用在启动时连接到正确的Firebase项目。然而,该方案会将所有Firebase配置文件打包,对于安全性要求较高的应用不太适用。
多入口点方案则确保每个构建版本仅包含必要的Firebase配置,在处理敏感数据或开发生产级应用时,是更优的选择。
\\n无论采用哪种方案,Flutter应用都能自动连接到对应的Firebase环境(dev
、stg
或prod
),确保应用在开发和生产的各个阶段都能稳定运行。
这种配置方式让Flutter与Firebase应用的多环境管理变得轻松许多,我自己在生产应用中也一直在使用,效果非常好。✅
","description":"在Flutter应用开发过程中,如果应用支持多种环境配置(如开发、预发布、生产环境),并且需要与Firebase进行集成,那么就需要额外进行一些配置工作,确保每种环境配置都对应不同的Firebase环境。 最佳实践是为每种环境配置创建独立的Firebase项目,这样能够将开发、预发布和生产环境相互隔离。\\n\\n在使用自定义后端或像Supabase这样的Dart SDK时,我们可以通过切换URL和API密钥,使应用在不同环境配置下连接到正确的后端环境。然而,Firebase的集成情况有所不同,它没有提供直接的Dart SDK…","guid":"https://juejin.cn/post/7498645508413095987","author":"JarvanMo","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-30T02:58:53.145Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0d8cb51535ea4f6496ad6c2b64a40947~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgSmFydmFuTW8=:q75.awebp?rk3s=f64ab15b&x-expires=1746586733&x-signature=beHBtSvavqfd1FK8NAmEdbLuotA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/289520dc9bc54c8490940621bcf1d632~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgSmFydmFuTW8=:q75.awebp?rk3s=f64ab15b&x-expires=1746586733&x-signature=Yu7vS%2BXkygNLDu%2Fih%2Flljei7FKU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b784dbcda047493a8cfe5bd2d37e6991~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgSmFydmFuTW8=:q75.awebp?rk3s=f64ab15b&x-expires=1746586733&x-signature=1Is16Ty1qVlt77z8mkTcqRoNEHQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b301834a4b554936b6409cce2de0bfa0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgSmFydmFuTW8=:q75.awebp?rk3s=f64ab15b&x-expires=1746586733&x-signature=rHB7ynCtlqx7ihD3FC57Vpit2K4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/51d85cad1cd74cd9970e368c5d1e7786~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgSmFydmFuTW8=:q75.awebp?rk3s=f64ab15b&x-expires=1746586733&x-signature=3sm8QwuFA5%2BcjVHcbYOrAk9mARI%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"[Flutter 基础] - Flutter基础组件 - Icon","url":"https://juejin.cn/post/7498547313138647103","content":"Flutter
的 Icon
组件用于显示图标,支持 Material Icons(默认)、Cupertino Icons(iOS 风格)和 自定义图标。图标通过 IconData
对象定义,通常来自 Icons
类(Material)或 CupertinoIcons
类(iOS)。
Icon的一般用法和image有点类似,只是Icon不支持通过url的方式去加载。
\\nIcon(\\n Icons.home, // 图标名称\\n size: 64.0, // 图标大小(逻辑像素)\\n color: Colors.blue, // 图标颜色\\n semanticLabel: \'首页\', // 无障碍标签(屏幕阅读器)\\n),\\nIcon(\\n Icons.home, // 图标名称\\n size: 64.0, // 图标大小(逻辑像素)\\n color: Colors.red, // 图标颜色\\n semanticLabel: \'首页\', // 无障碍标签(屏幕阅读器)\\n);\\n
\\nIcon是支持阴影效果的,但是不支持颜色渐变效果。
\\nIcon(\\n Icons.star,\\n shadows: [\\n Shadow(\\n color: Colors.black.withOpacity(0.5),\\n offset: Offset(2, 2),\\n blurRadius: 4.0,\\n ),\\n ],\\n);\\n
\\nsemanticLabel
:\\nIcon(\\n Icons.accessibility,\\n semanticLabel: \'无障碍模式\',\\n size: 32,\\n);\\n
\\n属性 | 类型 | 说明 |
---|---|---|
icon | IconData | 必填,指定要显示的图标(如 Icons.star )。 |
size | double | 图标大小,默认 24.0 。 |
color | Color | 图标颜色,默认继承主题色。 |
semanticLabel | String | 辅助功能标签,用于屏幕阅读器描述图标功能。 |
textDirection | TextDirection | 图标绘制方向(如 TextDirection.rtl )。 |
shadows | List<Shadow> | 添加阴影效果,增强视觉层次。 |
Material
官方图标库虽然也有不少图标,但是大多数项目都是有自己的一套图标库Flutter
也是支持导入自定义的图标库的。
这个的用法和image的用法有点类似,通过单张导入的方式把icon放在assets资源文件夹下
\\n在 pubspec.yaml
中配置图片路径:
flutter:\\n assets:\\n - assets/icons/collection.png //配置icon存放的路径\\n
\\n通过 ImageIcon
显示:
ImageIcon(\\n AssetImage(\'assets/icons/collection.png\'),//这个路径是pubspec中声明的路径\\n size: 64,\\n color: Colors.red,\\n);\\n
\\n由于项目中一般使用图标数量都会比较多,而且通过迭代的方式经常会更换图标,所以单个导入多方式维护成本也非常的高。所以一般都会采用自定义字体图标的方式去引入图标库。\\n将字体图标文件(如 .ttf
)添加到项目。很多设计平台都支持导出ttf格式的文件,文件里会包含所有需要的图标。然后通过pubspec配置文件路径。
步骤:
\\n在 pubspec.yaml
中声明字体:
fonts:\\n - family: Iconfont\\n fonts:\\n - asset: assets/fonts/iconfont.ttf\\n\\n
\\n创建对应的Dart文件,并声明一下对应的icon。
\\nclass IconFont {\\n static IconData add = IconData(0xE664, fontFamily:\'iconfont\');\\n static IconData arrowRight = IconData(0xE664, fontFamily:\'iconfont\');\\n}\\n
\\n使用自定义图标:
\\nIcon(IconFont.add, size: 64, color: Colors.orange);\\n
\\nIcon
组件目前不支持直接使用Svg的图标,如果想要使用svg的话,就得用第三方插件的方式去使用。
flutter_svg
库:\\ndependencies:\\n flutter_svg: ^1.1.6\\n
\\nimport \'package:flutter_svg/flutter_svg.dart\';\\n\\nSvgPicture.asset(\\n \'assets/icons/custom_icon.svg\',\\n width: 32,\\n height: 32,\\n color: Colors.purple,\\n);\\n
\\n项目中经常遇到一些组件中会包含一些icon之类的,Flutter中也是支持icon结合其它组件一起使用,比如带icon的button.
\\nElevatedButton.icon(\\n icon: Icon(Icons.add), // 图标\\n label: Text(\'添加\'), // 文本\\n onPressed: () { /* 处理逻辑 */ },\\n);\\n
\\nIconButton(\\n icon: Icon(Icons.search),\\n onPressed: () { /* 搜索功能 */ },\\n color: Colors.blue,\\n iconSize: 30,\\n);\\n
\\n// GetView 源码片段\\nabstract class GetView<T> extends StatelessWidget {\\n const GetView({Key? key}) : super(key: key);\\n T get controller => GetInstance().find<T>();\\n // 每次build都会获取最新实例\\n}\\n\\n// GetWidget 源码片段\\nabstract class GetWidget<T extends GetxController> extends StatelessWidget {\\n const GetWidget({Key? key}) : super(key: key);\\n T get controller => GetInstance().find<T>(tag: _tag);\\n // 保持对同一实例的引用\\n}\\n
\\n场景 | GetView | GetWidget |
---|---|---|
普通页面 | ✓ | ✗ |
全局用户状态 | ✗ | ✓ |
购物车 | ✗ | ✓ |
应用主题设置 | ✗ | ✓ |
页面间数据传递 | ✗ | ✓ |
需要保持后台状态的组件 | ✗ | ✓ |
class CartController extends GetxController {\\n final items = <Product>[].obs;\\n \\n void addItem(Product product) {\\n items.add(product);\\n update();\\n }\\n \\n // 保持单例\\n static CartController get instance => Get.put(CartController());\\n}\\n
\\nclass CartIcon extends GetWidget<CartController> {\\n @override\\n Widget build(BuildContext context) {\\n return Stack(\\n children: [\\n Icon(Icons.shopping_cart),\\n Obx(() => Text(\'${controller.items.length}\')),\\n ],\\n );\\n }\\n}\\n\\n// 在多个页面使用\\nclass ProductPage extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n actions: [CartIcon()], // 多个页面共享同一实例\\n ),\\n );\\n }\\n}\\n
\\nsequenceDiagram\\n participant WidgetA\\n participant GetWidget\\n participant Controller\\n\\n WidgetA->>GetWidget: 首次创建\\n GetWidget->>Controller: Get.put()\\n Controller--\x3e>GetWidget: 返回实例\\n GetWidget->>WidgetA: 构建完成\\n \\n WidgetA->>GetWidget: 移除组件\\n GetWidget->>Controller: 保持存活\\n \\n WidgetA->>GetWidget: 再次创建\\n GetWidget->>Controller: 获取已有实例\\n
\\n// 在退出应用时清理\\n@override\\nvoid dispose() {\\n if (Get.isRegistered<CartController>()) {\\n Get.delete<CartController>(); \\n }\\n super.dispose();\\n}\\n\\n// 条件性删除\\nGet.delete<CartController>(\\n force: true, // 强制删除\\n condition: (c) => c.items.isEmpty, // 满足条件才删除\\n);\\n
\\nclass ConfigController extends GetxController {\\n final String env;\\n \\n ConfigController({required this.env});\\n}\\n\\n// 初始化时\\nGet.put(ConfigController(env: \'prod\'), permanent: true);\\n\\n// 在GetWidget中访问\\nclass ConfigWidget extends GetWidget<ConfigController> {\\n @override\\n Widget build(BuildContext context) {\\n return Text(\'当前环境:${controller.env}\');\\n }\\n}\\n
\\nclass ChatController extends GetxController {\\n final String chatId;\\n ChatController(this.chatId);\\n}\\n\\n// 不同会话页面\\nclass ChatPage extends GetWidget<ChatController> {\\n final String chatId;\\n \\n ChatPage(this.chatId) {\\n Get.put(ChatController(chatId), tag: chatId);\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Text(\'会话ID:${controller.chatId}\');\\n }\\n}\\n
\\nclass HybridWidget extends GetWidget<CartController> {\\n @override\\n Widget build(BuildContext context) {\\n return GetBuilder<CartController>(\\n builder: (c) => Column(\\n children: [\\n Text(\'总数量:${c.items.length}\'),\\n ElevatedButton(\\n onPressed: () => c.clearCart(),\\n child: Text(\'清空\'),\\n )\\n ],\\n ),\\n );\\n }\\n}\\n
\\nclass OptimizedCart extends GetWidget<CartController> {\\n @override\\n Widget build(BuildContext context) {\\n return Obx(\\n () => ListView.builder(\\n itemCount: controller.items.length,\\n itemBuilder: (_, i) => ItemWidget(\\n item: controller.items[i],\\n // 仅当价格变化时更新\\n shouldRebuild: (old, current) => old.price != current.price,\\n ),\\n ),\\n );\\n }\\n}\\n
\\nextension SnapshotExtension on GetxController {\\n T get snapshot => Get.find<T>(tag: \'snapshot_${this.runtimeType}\');\\n \\n void saveSnapshot() {\\n Get.put<T>(this, tag: \'snapshot_${this.runtimeType}\');\\n }\\n}\\n\\n// 使用示例\\ncontroller.saveSnapshot();\\nfinal backup = controller.snapshot;\\n
\\nvoid monitorMemory() {\\n Get.addObserver(\\n (route, previousRoute) {\\n if (route.isCurrent) {\\n debugPrint(\'当前控制器实例:${Get.instances}\');\\n }\\n },\\n );\\n}\\n
\\nGlobal
后缀:CartGlobalControllerPage
后缀:ProductPageController@singleton
注解lib/\\n├─ core/\\n│ ├─ controllers/ # GetWidget使用的全局控制器\\n│ │ ├─ app_controller.dart\\n│ │ ├─ config_controller.dart\\n├─ modules/\\n│ ├─ cart/\\n│ │ ├─ cart_widget.dart # 使用GetWidget的共享组件\\n
\\n// 打印控制器树\\nvoid debugControllerTree() {\\n Get.printInfo(\\n info: \'\'\'\\n 控制器实例列表:\\n ${Get.instances.map((e) => e.runtimeType).join(\'\\\\n\')}\\n \'\'\'\\n );\\n}\\n
\\nGetWidget 的核心价值体现在:
\\n建议在以下场景优先使用 GetWidget:
\\n通过合理运用 GetWidget,可以构建出更健壮、更易维护的 Flutter 应用架构。
","description":"GetWidget 设计哲学 与GetView的本质区别\\n// GetView 源码片段\\nabstract class GetViewclass HomePage extends StatelessWidget {\\n final Controller _controller = Get.put(Controller());\\n\\n @override\\n Widget build(BuildContext context) {\\n return Obx(() => Text(_controller.user.value.name));\\n }\\n}\\n
\\nabstract class GetView<T> extends StatelessWidget {\\n const GetView({Key? key}) : super(key: key);\\n \\n T get controller => GetInstance().find<T>();\\n \\n @override\\n Widget build(BuildContext context);\\n}\\n
\\nclass UserController extends GetxController {\\n final user = User().obs;\\n void updateName(String name) => user.update((u) => u?.name = name);\\n}\\n
\\nclass ProfilePage extends StatelessWidget {\\n final UserController _controller = Get.put(UserController());\\n\\n @override\\n Widget build(BuildContext context) {\\n return Column(\\n children: [\\n Obx(() => Text(_controller.user.value.name)),\\n TextButton(\\n onPressed: () => _controller.updateName(\'New Name\'),\\n child: Text(\'Update\'),\\n )\\n ],\\n );\\n }\\n}\\n
\\nclass ProfilePage extends GetView<UserController> {\\n @override\\n Widget build(BuildContext context) {\\n return Column(\\n children: [\\n Obx(() => Text(controller.user.value.name)),\\n TextButton(\\n onPressed: () => controller.updateName(\'New Name\'),\\n child: Text(\'Update\'),\\n )\\n ],\\n );\\n }\\n}\\n
\\nclass OrderDetailPage extends GetView<OrderController> \\n with GetTickerProviderStateMixin {\\n final UserController user = Get.find();\\n\\n @override\\n Widget build(BuildContext context) {\\n return Obx(() => Text(\\n \'${user.value.name}的订单:${controller.orderNo}\'\\n ));\\n }\\n}\\n
\\nclass ProductPage extends GetView<ProductController> {\\n @override\\n void onInit() {\\n final args = Get.arguments;\\n controller.loadProduct(args[\'id\']);\\n super.onInit();\\n }\\n}\\n
\\nvoid main() {\\n testWidgets(\'Controller auto dispose\', (tester) async {\\n await tester.pumpWidget(GetMaterialApp(home: ProfilePage()));\\n expect(Get.isRegistered<UserController>(), true);\\n \\n await tester.pumpWidget(SizedBox());\\n expect(Get.isRegistered<UserController>(), false);\\n });\\n}\\n
\\nGetView<UserController>(\\n init: UserController(), // 手动初始化\\n lazy: false, // 立即加载\\n)\\n
\\nGetView<GlobalController>(\\n tag: \'global\', // 使用标签区分实例\\n global: true, // 全局保留\\n)\\n
\\nclass FilteredView extends GetView<Controller> {\\n @override\\n bool get wantKeepAlive => true; // 保持控制器存活\\n}\\n
\\n特性 | GetView | GetWidget |
---|---|---|
生命周期 | 绑定页面生命周期 | 全局单例 |
重建行为 | 随Widget树重建 | 保持状态 |
适用场景 | 普通页面 | 全局状态/跨页面共享 |
内存管理 | 自动回收 | 手动回收 |
典型应用 | 90%的页面场景 | 用户认证状态 |
lib/\\n├─ modules/\\n│ ├─ product/\\n│ │ ├─ product_controller.dart\\n│ │ ├─ product_page.dart\\n│ ├─ order/\\n│ │ ├─ order_controller.dart\\n│ │ ├─ order_page.dart\\n
\\n// 打印当前路由的控制器\\nvoid debugControllers() {\\n Get.printInfo(\\n info: \'Active controllers: ${Get.instances}\'\\n );\\n}\\n
\\n通过 GetView 语法糖可以实现:
\\n建议所有 GetX 项目强制使用此模式,这是 Flutter 状态管理的最佳实践之一。对于复杂场景,可结合 GetBuilder
和 Obx
实现更精细的控制。
在 Flutter 状态管理方案中,GetX 以其简洁的响应式编程模型广受欢迎。今天我们通过扩展方法为 Rx 可观察对象添加语法糖,探索更优雅的状态管理方式。
\\nextension ObxWidgetExtension<T> on Rx<T> {\\n Widget obs(Widget Function(T value) builder) {\\n return Obx(() => builder(this.value));\\n }\\n}\\n
\\n传统写法:
\\nfinal counter = 0.obs;\\n\\n@override\\nWidget build(BuildContext context) {\\n return Obx(() => Text(\\n \'Count: ${counter.value}\',\\n style: Theme.of(context).textTheme.displayLarge,\\n ));\\n}\\n
\\n扩展写法:
\\nfinal counter = 0.obs;\\n\\n@override\\nWidget build(BuildContext context) {\\n return counter.obs((value) => Text(\\n \'Count: $value\',\\n style: Theme.of(context).textTheme.displayLarge,\\n ));\\n}\\n
\\n✅ 减少 23% 的样板代码
\\n✅ 消除.value显式访问
\\n✅ 提升类型安全
用户资料示例:
\\nclass User {\\n final String name;\\n final int level;\\n User(this.name, this.level);\\n}\\n\\nfinal user = User(\'Alice\', 5).obs;\\n
\\n传统写法:
\\nObx(() => Column(\\n children: [\\n Text(\'Name: ${user.value.name}\'),\\n Text(\'Level: ${user.value.level}\'),\\n Icon(\\n user.value.level > 10 \\n ? Icons.verified\\n : Icons.warning,\\n color: Colors.blue,\\n )\\n ],\\n))\\n
\\n扩展写法:
\\nuser.obs((u) => Column(\\n children: [\\n Text(\'Name: ${u.name}\'),\\n Text(\'Level: ${u.level}\'),\\n Icon(\\n u.level > 10 \\n ? Icons.verified\\n : Icons.warning,\\n color: Colors.blue,\\n )\\n ],\\n))\\n
\\n✅ 嵌套层级减少 1 级
\\n✅ 直接访问对象属性
\\n✅ 逻辑关注点更集中
搜索框示例:
\\nfinal searchText = \'\'.obs;\\nfinal showClear = false.obs;\\n
\\n传统写法:
\\nObx(() => TextField(\\n decoration: InputDecoration(\\n suffixIcon: showClear.value\\n ? IconButton(\\n icon: Icon(Icons.clear),\\n onPressed: () => searchText.value = \'\',\\n )\\n : null,\\n ),\\n onChanged: (v) {\\n searchText.value = v;\\n showClear.value = v.isNotEmpty;\\n },\\n))\\n
\\n扩展写法:
\\nsearchText.obs((text) => TextField(\\n decoration: InputDecoration(\\n suffixIcon: text.isNotEmpty\\n ? IconButton(\\n icon: Icon(Icons.clear),\\n onPressed: () => searchText.value = \'\',\\n )\\n : null,\\n ),\\n onChanged: (v) => searchText.value = v,\\n))\\n
\\n✅ 自动派生状态 (showClear → text.isNotEmpty)
\\n✅ 减少 1 个观察变量
\\n✅ 逻辑内聚性提升
final premiumUser = false.obs;\\n\\npremiumUser.obs(\\n (isPremium) => isPremium\\n ? PremiumBadge(color: Colors.amber)\\n : UpgradeButton(onTap: () => _purchasePremium()),\\n);\\n
\\nfinal tasks = <Task>[].obs;\\n\\ntasks.obs(\\n (taskList) => ListView.separated(\\n itemCount: taskList.length,\\n separatorBuilder: (_,i) => Divider(height: 1),\\n itemBuilder: (_,i) => TaskItem(\\n task: taskList[i],\\n onComplete: () => _toggleTask(i),\\n ),\\n ),\\n);\\n
\\n场景 | 推荐度 | 说明 |
---|---|---|
简单状态绑定 | ★★★★★ | 基础类型/简单对象 |
表单控件 | ★★★★☆ | 需配合onChanged |
列表渲染 | ★★★☆☆ | 需要索引时需回传统写法 |
多状态组合 | ★★☆☆☆ | 建议保持原生 Obx |
通过扩展方法实现的 obs 语法糖,在保持 GetX 响应式核心机制的同时:
\\n建议在简单状态绑定场景优先使用此模式,复杂场景仍可结合传统 Obx 实现最佳实践。
","description":"前言 在 Flutter 状态管理方案中,GetX 以其简洁的响应式编程模型广受欢迎。今天我们通过扩展方法为 Rx 可观察对象添加语法糖,探索更优雅的状态管理方式。\\n\\n扩展定义\\nextension ObxWidgetExtension在GetX状态管理体系中,GetxController
和 GetxService
是最核心的两个基类,但开发者经常混淆两者的使用边界。
维度 | GetxService | GetxController |
---|---|---|
设计定位 | 全局持久化服务 | 页面级状态管理器 |
生命周期 | 应用级(手动管理) | 组件级(自动绑定) |
内存驻留时间 | 从创建到应用终止 | 页面创建到销毁 |
典型初始化方式 | Get.lazyPut() | Get.create() |
依赖关系方向 | 被Controller依赖 | 依赖Service |
典型应用场景 | 数据库连接、网络客户端、硬件交互 | 表单验证、UI状态管理、业务逻辑协调 |
单元测试重点 | 基础设施可靠性测试 | 业务逻辑正确性测试 |
代码量 | 通常较大(含底层实现) | 通常较小(纯业务逻辑) |
// 服务层 - services/user_service.dart\\nclass UserService extends GetxService {\\n Future<User> fetchUser(int id) async {\\n // 调用API或数据库\\n }\\n \\n Future<void> updateProfile(User user) {\\n // 持久化操作\\n }\\n}\\n\\n// 控制层 - controllers/profile_controller.dart\\nclass ProfileController extends GetxController {\\n final UserService userService = Get.find();\\n final Rx<User?> user = null.obs;\\n final RxBool isLoading = false.obs;\\n\\n void loadData() async {\\n isLoading.value = true;\\n user.value = await userService.fetchUser(123);\\n isLoading.value = false;\\n }\\n\\n void _handleError(Object e) {\\n Get.snackbar(\'Error\', e.toString());\\n }\\n}\\n\\n// 实际页面 - views/profile_view.dart\\nclass ProfileView extends GetView<ProfileController> {\\n @override\\n Widget build(BuildContext context) {\\n return Obx(() {\\n if (controller.isLoading.value) return LoadingWidget();\\n return Column(\\n children: [\\n Text(controller.user.value?.name ?? \'No Name\'),\\n ElevatedButton(\\n onPressed: controller.loadData,\\n child: Text(\'Refresh\'),\\n )\\n ],\\n );\\n });\\n }\\n}\\n
\\nService层(GetxService)
\\nController层(GetxController)
\\nView → Controller → Service
\\n(视图层不应直接调用Service)
// Service到Controller的通知\\n class AuthService extends GetxService {\\n final Rx<User?> currentUser = null.obs;\\n }\\n\\n // Controller监听\\n class HomeController extends GetxController {\\n final authService = Get.find<AuthService>();\\n \\n @override\\n void onInit() {\\n ever(authService.currentUser, _handleUserChange);\\n super.onInit();\\n }\\n }\\n
\\n // 可单独测试Service\\n void main() {\\n test(\'UserService fetch test\', () async {\\n final service = UserService();\\n await service.init();\\n expect(await service.fetchUser(1), isA<User>());\\n });\\n }\\n\\n // 可mock Service测试Controller\\n test(\'ProfileController test\', () {\\n Get.put<UserService>(MockUserService());\\n final controller = ProfileController();\\n controller.loadData();\\n expect(controller.isLoading.value, false);\\n });\\n
\\n通过这种分层架构,我们可以得到这样的一种实现关系:
\\n这种架构设计,符合Clean Architecture的设计原则,提升了代码的可维护性和可测试性。
\\n// ❌ 错误:在Service中直接操作UI\\nclass WrongService extends GetxService {\\n void showError() {\\n Get.dialog(AlertDialog(...)); // 违反分层原则\\n }\\n}\\n\\n// ✅ 正确:通过状态变更触发UI更新\\nclass CorrectService extends GetxService {\\n final Rx<Error?> currentError = null.obs;\\n}\\n
\\n// ❌ 错误:Controller包含数据持久化逻辑\\nclass WrongController extends GetxController {\\n void saveUser(User user) {\\n // 直接操作数据库\\n _db.execute(\'INSERT INTO users ...\'); \\n }\\n}\\n\\n// ✅ 正确:委托给Service\\nclass CorrectController extends GetxController {\\n final UserService _userService = Get.find();\\n \\n void saveUser(User user) {\\n _userService.persistUser(user);\\n }\\n}\\n
\\n通过 GetxService
与 GetxController
的有机组合,我们实现了Flutter应用的黄金架构法则,即:
本文续接上篇文章: #[Flutter小试牛刀] 低配版signals,添加多层监听链
\\n在上篇文章里,已经写了一个减配版的signals,且添加了多层监听链,但它在页面刷新上粒度不够,如果每次值改变都去刷新整个页面,对性能肯定是不友好的。本次更新是添加局部更新的功能。
\\n为了实现这个功能,我们可以先定义一个UserBuilder,它是一个Widget,传入参数只有一个WidgetBuilder,只要是WidgetBuilder创建的Widget树里有value的引用则添加到这个UseBuilder的监听列表中,只要这些值改变则重新刷新这个UseBuilder,达到局部刷新的效果。
\\nclass UseBuilder extends StatefulWidget {\\n\\n final WidgetBuilder builder;\\n const UseBuilder(this.builder, {super.key});\\n @override\\n State<UseBuilder> createState() => _UseBuilderState();\\n}\\n\\n\\nclass _UseBuilderState extends MixinUseState<UseBuilder> {\\n late final result = useCompute(() {\\n return widget.builder(context);\\n }, autoNotify: true);\\n\\n @override\\n void initState() {\\n super.initState();\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return result.value ?? const SizedBox.shrink();\\n }\\n\\n @override\\n void reassemble() {\\n super.reassemble();\\n WidgetsBinding.instance.addPostFrameCallback((_) {\\n result.notifyListeners();\\n if (mounted) setState(() {});\\n result.value;\\n });\\n }\\n\\n @override\\n void didChangeDependencies() {\\n super.didChangeDependencies();\\n result.notifyListeners();\\n }\\n\\n @override\\n void didUpdateWidget(covariant UseBuilder oldWidget) {\\n super.didUpdateWidget(oldWidget);\\n if (oldWidget.builder != widget.builder) {\\n result.notifyListeners();\\n }\\n }\\n}\\n
\\n以上为代码的实现,实现的核心思路是通过useCompute来创建UseBuilder的Widget,在这里autoNotify: true表示将这个compute和该Widget绑定,只要compute的值发生变化时,会调用setState重新刷新UI。
\\n又因为useCompute实际上返回的是WidgetBuilder的回调,因此在回调函数中的所有useValue值都会绑定到这个compute上,这些value值改变会触发compute的改变从而触发UseBuilder的重绘。
\\n当然一些其它的情况也会导致UseBuilder的重绘,因此我们需要重写didChangeDependencies 和 didUpdateWidget方法。
\\n再写一段测试代码测试它的可行性:
\\n\\nclass Page2 extends StatefulWidget {\\n const Page2({super.key});\\n @override\\n State<Page2> createState() => _Page2State();\\n}\\n\\n \\nclass _Page2State extends State<Page2> with MixinUseState {\\n\\n late final c1 = use(0)\\n ..onDispose = (value) {\\n debugPrint(\\"Page2 c1 dispose $value\\");\\n }\\n ..onChange = (value) {\\n debugPrint(\\"Page2 c1 onChange $value\\");\\n };\\n\\n late final c2 = use(2)\\n ..onDispose = (value) {\\n debugPrint(\\"Page2 c2 dispose $value\\");\\n }\\n ..onChange = (value) {\\n debugPrint(\\"Page2 c2 onChange $value\\");\\n };\\n\\n late final c3 = useCompute(() {\\n return c1.value + c2.value;\\n })\\n ..onDispose = (value) {\\n debugPrint(\\"Page2 c3 dispose $value\\");\\n }\\n ..onChange = (value) {\\n debugPrint(\\"Page2 c3 onChange $value\\");\\n };\\n \\n late final counter = useCompute(() {\\n return c1.value + c3.value;\\n })\\n ..onDispose = (value) {\\n debugPrint(\\"Page2 counter dispose $value\\");\\n }\\n ..onChange = (value) {\\n debugPrint(\\"Page2 counter onChange $value\\");\\n };\\n\\n\\n @override\\n\\n Widget build(BuildContext context) {\\n debugPrint(\\"Page2 build \\");\\n return Center(\\n child: Column(\\n mainAxisAlignment: MainAxisAlignment.center,\\n children: <Widget>[\\n const Text(\\n \'Page2 You have pushed the button this many times:\',\\n ),\\n TextButton(\\n onPressed: () {\\n c2.value++;\\n },\\n child: const Text(\\"add\\")\\n ),\\n UseBuilder((context) {\\n debugPrint(\\"RefWidget c3 build \\");\\n return Text(\\n \\"${c3.value}\\",\\n style: Theme.of(context).textTheme.headlineMedium,\\n );\\n }),\\n UseBuilder((context) {\\n debugPrint(\\"RefWidget counter build \\");\\n return Text(\\n \'${counter.value}\',\\n style: Theme.of(context).textTheme.headlineMedium,\\n );\\n }),\\n ],),\\n );\\n }\\n}\\n
\\n从上面代码中,点击按钮会改变c1的值,从而触发改变c3和counter的值,每次点击都会输出:
\\nRefWidget c3 build \\nRefWidget counter build \\n
\\n可以看出局部刷新是有效的。
","description":"本文续接上篇文章: #[Flutter小试牛刀] 低配版signals,添加多层监听链 在上篇文章里,已经写了一个减配版的signals,且添加了多层监听链,但它在页面刷新上粒度不够,如果每次值改变都去刷新整个页面,对性能肯定是不友好的。本次更新是添加局部更新的功能。\\n\\n为了实现这个功能,我们可以先定义一个UserBuilder,它是一个Widget,传入参数只有一个WidgetBuilder,只要是WidgetBuilder创建的Widget树里有value的引用则添加到这个UseBuilder的监听列表中…","guid":"https://juejin.cn/post/7498307095307517952","author":"孤鸿玉","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-29T07:57:12.980Z","media":null,"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter ChangeNotifier 和 ValueNotifier 的区别","url":"https://juejin.cn/post/7498307095424122914","content":"//Model 用于管理状态并触发通知\\nclass CounterNotifier with ChangeNotifier { //mixin\\n//class CounterNotifier extends ChangeNotifier {\\n int _count = 0; //私有字段\\n\\n //提供只读属性\\n int get count => _count;\\n\\n void increment() {\\n _count++; //修改状态\\n notifyListeners(); //手动触发更新,通知所有监听者\\n }\\n}\\n\\nclass CounterNotifierStatefulWidget extends StatefulWidget {\\n const CounterNotifierStatefulWidget({super.key});\\n\\n @override\\n State<CounterNotifierStatefulWidget> createState() => _CounterNotifierStatefulWidgetState();\\n}\\n\\nclass _CounterNotifierStatefulWidgetState extends State<CounterNotifierStatefulWidget> {\\n //\\n final CounterNotifier _counterNotifier = CounterNotifier();\\n\\n @override\\n void initState() {\\n super.initState();\\n _counterNotifier.addListener(() {\\n print(\'addListener Callback\');\\n setState(() {}); //触发 UI 更新\\n });\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n print(\'build\');\\n return Scaffold(\\n appBar: AppBar(\\n title: const Text(\'ChangeNotifier 示例\'),\\n ),\\n body: Center(\\n child: Text(\\n \'count: ${_counterNotifier.count}\',\\n )),\\n floatingActionButton: FloatingActionButton(\\n onPressed: () => _counterNotifier.increment(),\\n child: const Icon(Icons.add),\\n ),\\n );\\n }\\n\\n @override\\n void dispose() {\\n _counterNotifier.dispose();\\n super.dispose();\\n }\\n}\\n\\n
\\nclass ValueNotifierStatefulWidget extends StatefulWidget {\\n const ValueNotifierStatefulWidget({Key? key}) : super(key: key);\\n\\n @override\\n State<ValueNotifierStatefulWidget> createState() => _ValueNotifierStatefulWidgetState();\\n}\\n\\nclass _ValueNotifierStatefulWidgetState extends State<ValueNotifierStatefulWidget> {\\n //\\n final ValueNotifier<int> _myValueNotifier = ValueNotifier<int>(0); //初始值为 0\\n \\n @override\\n Widget build(BuildContext context) {\\n print(\'build\');\\n return Scaffold(\\n appBar: AppBar(\\n title: const Text(\'ValueNotifier 示例\'),\\n ),\\n body: Center(\\n //ValueListenableBuilder 继承自 StatefulWidget\\n child: ValueListenableBuilder<int>(\\n valueListenable: _myValueNotifier, //valueListenable 表示一个可监听的数据源\\n builder: (BuildContext context, int value, Widget? child) {\\n print(\'builder\');\\n //builder 只会在 value 值变化时被调用,此处的 value 就是 _myValueNotifier 管理的值发生变化后的新值\\n return Text(\'count: ${_myValueNotifier.value}\');\\n })),\\n floatingActionButton: FloatingActionButton(\\n onPressed: () {\\n _myValueNotifier.value++; //数据变化时触发 ValueListenableBuilder 重新构建\\n },\\n child: const Icon(Icons.add),\\n ),\\n );\\n }\\n\\n @override\\n void dispose() {\\n _myValueNotifier.dispose();\\n super.dispose();\\n }\\n}\\n
\\n移动端应用如果需要快速低成本适配鸿蒙端,可以考虑使用 flutter 框架。
\\n目前有一个适配了鸿蒙端的flutter sdk开源项目,先后开源于gitee、gitcode平台,可以直接拿来做鸿蒙端应用的开发,整体效果还OK,能解决通用的App需求。
\\n1、鸿蒙定制版的flutter sdk 基于官方的3.7.12版本(23年4月)和3.22.1版本(2024年5月)进行定制开发,而官方的最新稳定版本是3.29.3(25年4月)。版本间隔相对较大,涉及到一些优化、修复、新功能的使用,如果需要拿来开发需要先评估是否适合目前项目。
\\n2、开源的flutter_flutter开发环境不太稳定,这边使用windows进行了基于flutter的鸿蒙应用开发,遇到过flutter sdk和鸿蒙sdk不在一个盘符下导致的各种报错、配置flutter依赖库报错、ohos项目中自动生成的依赖路径报错等问题,需要花费一定时间精力排查,可能是该开源框架是在mac上开发,未考虑windows上一些特性。
\\n3、如果有强依赖webview的flutter应用,webview_flutter这块会有各种使用bug,比如webview编辑器,交互体验差,bug也比较多,但普通的网页展示交互是可以的。
\\n4、flutter上一些通用组件功能限制使用,比如鸿蒙上不允许第三方开发者读取系统剪切板,导致flutter上输入框的粘贴功能不可用,比较影响体验,解决办法是需要在app签名层面申请该权限且应用性质需符合特定要求。
\\n这是个人在参与鸿蒙flutter应用开发后的一些总结,如果有其他的见解也欢迎补充评论。
","description":"ohos&flutter 移动端应用如果需要快速低成本适配鸿蒙端,可以考虑使用 flutter 框架。\\n\\n目前有一个适配了鸿蒙端的flutter sdk开源项目,先后开源于gitee、gitcode平台,可以直接拿来做鸿蒙端应用的开发,整体效果还OK,能解决通用的App需求。\\n\\n一些问题\\n\\n1、鸿蒙定制版的flutter sdk 基于官方的3.7.12版本(23年4月)和3.22.1版本(2024年5月)进行定制开发,而官方的最新稳定版本是3.29.3(25年4月)。版本间隔相对较大,涉及到一些优化、修复、新功能的使用…","guid":"https://juejin.cn/post/7498299598370799679","author":"goodluckjm","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-29T06:37:11.895Z","media":null,"categories":["前端","HarmonyOS","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"FlutterEngine源码编译之2025年版教程","url":"https://juejin.cn/post/7498299598369357887","content":"\\n\\n我的设备:
\\n\\n
\\n- macmini m4
\\n- macOS 15.3.2
\\n
\\n\\ndepot_tools 是 Chromium 源码依赖管理工具集,内含 gclient 等工具;用于帮助 Chromium、Flutter Engine等项目同步代码和管理编译所需的依赖
\\n
brew install ant\\nbrew install ninja\\n
\\ngit clone git@github.com:flutter/flutter.git\\n
\\n2. 进入源码根目录,然后复制 engine/scripts 任意一个 xx.gclient
文件到 根目录中,并修改名称为 .gclient
。我用的是standard.gclient
。
gclient sync
同步代码和安装所需的依赖engine/src/out
目录下#构建iOS设备使用的引擎\\n#真机debug版本\\n./engine/src/flutter/tools/gn --ios --unoptimized --no-prebuilt-dart-sdk \\n\\n# 其他版本\\n#真机release版本\\n./engine/src/flutter/tools/gn --ios -unoptimized --runtime-mode=release\\n#模拟器版本\\n./engine/src/flutter/tools/gn --ios -simulator --unoptimized\\n#主机端(Mac)构建 -- 热重载\\n./engine/src/flutter/tools/gn --unoptimized --no-prebuilt-dart-sdk \\n
\\nsoftwareupdate --install-rosetta --agree-to-license
\\n\\n在 m4 设备上,没安装 Rosetta2 前遇到的编译问题:
\\nOSError: [Errno 86] Bad CPU type in executable:
ninja -C ./engine/src/out/ios_debug_unopt
Image
组件是Flutter
中的基础组件之一,也是我们常用来展示图片的核心组件。它既可以展示本地图片,也可以展示网络图片。 这篇文章主要是通过一些简单的demo来了解一下这个组件基本用法。
首先得确保在 pubspec.yaml
中配置有配置过图片的基础路径,否则项目可能识别不到图片的本地路径:
flutter:\\n assets:\\n - assets/images/ # 指定图片目录\\n
\\n我们常用的图片加载方式一般都分为三种:
\\nImage.asset
加载: Image.asset(\'assets/images/girl.jpeg\', // 图片的本地地址\\n width: 100,\\n height: 100,\\n fit: BoxFit.cover, // 图片填充方式\\n );\\n
\\nImage.network
:\\nImage.network(\\n \'https://picsum.photos/200\', // 图片的url地址\\n fit: BoxFit.cover,\\n width: 200,\\n height: 200,\\n);\\n
\\n网络图片除了直接url的方式,还有一种是通过接口返回二进制数据或者base64位的数据。image组件也是支持的。
\\nImage.memory
(适用于从数据库或加密存储读取的二进制数据):\\nImage.memory(\\n Uint8List.fromList(bytes), // bytes 是二进制数据\\n scale: 2.0, // 缩放系数(适配高分辨率设备)\\n);\\n
\\nwidth
和 height
:\\nfit
属性控制填充方式,而非强制设置尺寸。fit
属性(填充模式)控制图片如何适应容器,共有 7 种模式:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n模式 | 行为描述 | 典型场景 |
---|---|---|
BoxFit.fill | 拉伸填满容器,忽略宽高比(可能导致变形) | 背景图(需完全覆盖) |
BoxFit.contain | 保持比例缩放,完整显示内容,可能留白 | 产品详情图、需完整展示的图标 |
BoxFit.cover | 保持比例缩放,完全覆盖容器并裁剪超出部分 | 用户头像、封面图 |
BoxFit.fitWidth | 按宽度填满容器,高度按比例缩放,可能超出或留白 | 横向轮播图(固定宽度) |
BoxFit.fitHeight | 按高度填满容器,宽度按比例缩放,可能超出或留白 | 纵向海报(固定高度) |
BoxFit.scaleDown | 缩小图片以适应容器,但不会放大(原图比容器小则保持原尺寸) | 动态内容(避免小图被放大) |
BoxFit.none | 以原始尺寸显示,可能溢出或被裁剪(需配合 clipBehavior ) | 像素级精确显示(游戏贴图) |
alignment
:调整图片在容器中的对齐方式(默认 Alignment.center
)。color
和 colorBlendMode
:为图片添加混合颜色(如半透明背景色)。repeat
:设置图片重复方式(noRepeat
、repeat
、repeatX
、repeatY
),适用于背景图。gaplessPlayback
:避免图片加载时的闪烁(适用于动画)。项目开发过程中,很少直接使用长方形,或者正方形的图片,大多数都是需要经过一些特殊处理,比如圆形的头像,圆角的长方形或者正方形等。那这种场景就可以借助ClipOval
和ClipRect
来实现这个效果。
ClipOval
或 ClipRRect
:```dart\\nClipOval(\\n child: Image.network(\\n \'https://example.com/avatar.jpg\',\\n fit: BoxFit.cover,\\n width: 100,\\n height: 100,\\n ),\\n);\\n```\\n
\\nClipRRect(\\n borderRadius: BorderRadius.circular(30),\\n child: Image.network(\\n \'https://picsum.photos/300\',\\n fit: BoxFit.cover,\\n width: 200,\\n height: 200,\\n ),\\n);\\n
\\ncolor
和 colorBlendMode
添加滤镜:ClipRRect(\\n borderRadius: BorderRadius.circular(30),\\n child: Image.network(\\n \'https://picsum.photos/300\',\\n width: 200,\\n height: 200,\\n color: Colors.red.withOpacity(0.5),\\n colorBlendMode: BlendMode.srcOver,\\n ),\\n);\\n
\\nFlutter 的 Image
组件功能强大,通过灵活使用 fit
、width/height
、ClipOval
等特性,可以高效实现复杂图片展示需求。尤其是特殊效果(如圆形、滤镜)可借助 Clip
和 BlendMode
实现。
在 Flutter 开发中,网络请求、文件读取、动画延迟等操作都需要处理 \\"等待\\" 逻辑。如果这些耗时操作直接阻塞主线程,会导致界面卡顿甚至假死。而Future作为 Flutter 异步编程的核心工具,就像一个智能的 \\"任务管家\\",能让主线程在等待耗时操作完成的同时继续处理用户交互,确保应用流畅运行。本文将通过生活化的比喻和简洁的代码示例,带您从零掌握Future的核心用法。
\\n想象你在手机上点了一份外卖:
\\n下单后你不会一直盯着 APP 傻等,而是可以继续刷朋友圈(主线程继续运行)
\\n订单状态会显示 \\"配送中\\"(Future的等待态 Pending)
\\n外卖送达时 APP 会提醒你(通过.then()处理成功结果)
\\n如果配送过程中出现问题(比如地址错误),会收到异常通知(通过.catchError()处理失败)
\\n在代码中,Future就是这个 \\"订单\\",它代表一个尚未完成的异步操作结果,常见于网络请求、I/O 操作、定时器等场景。
\\nPending(等待态) :刚创建Future时的初始状态,如Future.delayed(Duration(seconds: 1))
Resolved(完成态) :异步操作成功完成,携带结果值,可通过.then()
获取
Rejected(失败态) :异步操作抛出异常,可通过.catchError()
捕获
Flutter 采用单线程模型(UI 线程),如果直接执行耗时操作:
\\n// ❌错误示范:阻塞主线程导致界面卡顿\\nvoid badExample() {\\n sleep(Duration(seconds: 3)); // 直接阻塞3秒\\n print(\\"完成\\");\\n}\\n
\\n而使用Future能将耗时操作放入事件队列,主线程继续处理 UI:
\\n// ✅正确做法:异步执行不阻塞界面\\nvoid goodExample() {\\n Future(() {\\n sleep(Duration(seconds: 3));\\n print(\\"完成\\");\\n });\\n print(\\"主线程继续运行\\"); // 立即输出\\n}\\n
\\n// 创建一个1秒后返回\\"Hello Future\\"的Future\\nFuture<String> fetchData() {\\n return Future(() {\\n sleep(Duration(seconds: 1));\\n return \\"Hello Future\\"; // 异步操作的结果\\n });\\n}\\n
\\n// 3秒后执行打印操作\\nFuture.delayed(Duration(seconds: 3), () {\\n print(\\"3秒已过\\");\\n});\\n
\\n// 直接返回成功结果(用于测试或缓存数据)\\nFuture.value(\\"缓存数据\\"); \\n// 直接返回失败结果(用于参数校验)\\nFuture.error(\\"参数错误\\"); \\n
\\nfetchData()\\n .then((data) { // 成功时触发,data是返回值\\n print(\\"获取数据:$data\\");\\n return data.length; // 可以返回新值供下一个then使用\\n })\\n .then((length) {\\n print(\\"数据长度:$length\\"); // 输出:12\\n });\\n
\\nFuture.error(\\"网络超时\\")\\n .catchError((error) { // 失败时触发\\n print(\\"错误:$error\\"); // 输出:网络超时\\n return \\"默认数据\\"; // 可以返回替代值让流程继续\\n })\\n .then((data) {\\n print(\\"最终数据:$data\\"); // 输出:默认数据\\n });\\n
\\nshowLoading(); // 显示加载框\\nfetchData()\\n .then(updateUI)\\n .catchError(showError)\\n .whenComplete(() => hideLoading()); // 无论结果如何都会隐藏加载框\\n
\\nasync/await语法糖让异步代码看起来像同步代码,更符合直觉:
\\n// 使用async标记异步函数\\nFuture<void> loadData() async {\\n try {\\n String data = await fetchData(); // await会暂停此处,等待Future完成\\n print(\\"数据加载成功:$data\\");\\n } catch (error) { // 捕获异步过程中的异常\\n print(\\"加载失败:$error\\");\\n }\\n}\\n
\\n\\n\\n注意:await必须用在标记async的函数中,且一次只能等待一个 Future
\\n
当需要同时执行多个独立任务(如同时加载用户信息和订单列表),可以用Future.wait:
\\nFuture<void> loadAll() async {\\n // 并行执行两个网络请求\\n List<dynamic> results = await Future.wait([\\n fetchUserInfo(), // Future<User>\\n fetchOrderList(), // Future<List<Order>>\\n ]);\\n User user = results[0];\\n List<Order> orders = results[1];\\n // 统一处理结果\\n}\\n
\\n当需要获取多个请求中最快完成的结果(如备用服务器请求):
\\nFuture<String> fetchWithFallback() async {\\n String result = await Future.any([\\n fetchFromMainServer(), // 主服务器请求(可能较慢)\\n fetchFromBackupServer(), // 备用服务器请求(可能更快)\\n ]);\\n return result; // 无论哪个先完成,返回首个结果\\n}\\n
\\n当需要按顺序处理有依赖关系的任务(如下载多个文件并逐个保存):
\\nFuture<void> downloadFiles(List<String> urls) async {\\n await Future.forEach(urls, (url) async {\\n String data = await download(url); // 等待前一个下载完成\\n saveToDisk(data); // 保存文件\\n });\\n}\\n
\\n// 反例:缺少错误处理\\nfetchData().then(handleData); \\n// 正例:添加全局错误捕获\\nfetchData()\\n .then(handleData)\\n .catchError((e) => logError(e)); \\n
\\n2. 区分同步 / 异步错误:try-catch只能捕获同步错误,异步错误需用.catchError()
\\n\\nvoid example() {\\n try {\\n // 同步错误可捕获\\n throw \\"同步错误\\"; \\n } catch (e) {\\n print(e);\\n }\\n // 异步错误无法被这里的try-catch捕获\\n Future.error(\\"异步错误\\").then((_) {}); \\n}\\n
\\n3. 使用具体的异常类型:提高错误处理的精准性
\\n\\n.catchError((e) {\\n if (e is SocketException) {\\n handleNetworkError();\\n } else if (e is FormatException) {\\n handleParseError();\\n }\\n})\\n
\\n// 5秒未响应则抛出超时异常\\nawait fetchData().timeout(Duration(seconds: 5)); \\n
\\n在开发阶段,可以通过打印状态辅助调试:
\\nFuture<String> debugFuture() {\\n final future = Future(() => \\"完成\\");\\n future.then((_) => print(\\"状态:已完成\\"));\\n print(\\"当前状态:${future.isCompleted}\\"); // 输出:false(尚未完成)\\n return future;\\n}\\n
\\nFlutter 原生不支持取消 Future,但可以通过以下方式模拟:
\\n// 使用Completer手动控制Future状态\\nCompleter<String> completer = Completer();\\nFuture<String> future = completer.future;\\n// 取消逻辑\\nif (needCancel) {\\n completer.completeError(CancelException());\\n}\\n
\\n不会。一旦 Future 完成(成功或失败),后续的.then()会直接使用已有的结果或错误,不会重新执行异步操作。
\\n通过这篇文章的学习,需要掌握以下几点:
\\nFuture 的核心概念(状态、作用、单线程优势)
\\n基础用法(创建、结果处理、async/await)
\\n进阶技巧(组合多个 Future、错误处理)
\\n最佳实践(性能优化、健壮性)
\\n记住,Future 的设计哲学是 \\"非阻塞、可组合、易扩展\\"。在实际开发中,合理使用 Future 能让您的代码更优雅,应用更流畅。下次遇到网络请求或耗时操作时,记得让 Future 来管理这些 \\"异步快递\\" 。
","description":"前言 在 Flutter 开发中,网络请求、文件读取、动画延迟等操作都需要处理 \\"等待\\" 逻辑。如果这些耗时操作直接阻塞主线程,会导致界面卡顿甚至假死。而Future作为 Flutter 异步编程的核心工具,就像一个智能的 \\"任务管家\\",能让主线程在等待耗时操作完成的同时继续处理用户交互,确保应用流畅运行。本文将通过生活化的比喻和简洁的代码示例,带您从零掌握Future的核心用法。\\n\\n一、Future 是什么?异步任务的 \\"快递单\\"\\n1.1 生活中的 Future 类比\\n\\n想象你在手机上点了一份外卖:\\n\\n下单后你不会一直盯着 APP 傻等…","guid":"https://juejin.cn/post/7498247910738051122","author":"_痞老板","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-28T12:13:58.417Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/243738b4a1b542fd81c24ca435fda15c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgX-eXnuiAgeadvw==:q75.awebp?rk3s=f64ab15b&x-expires=1746447238&x-signature=Y19KLvmkLgkB%2BUSxTOdIGlqOhD8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/696b006805b748838d39a7bbdd4cfe47~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgX-eXnuiAgeadvw==:q75.awebp?rk3s=f64ab15b&x-expires=1746447238&x-signature=1b%2B74CyV0JU5OaFiuC%2BF8%2BfivNU%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Dart","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"flutter篇---Android gradle版本报错","url":"https://juejin.cn/post/7498180673992949775","content":"其实里面有提示,具体的可以看一下,然后根据提示去修改
\\n\\n* What went wrong:\\nExecution failed for task \':flutter_plugin_android_lifecycle:compileDebugJavaWithJavac\'.\\n> Could not resolve all files for configuration \':flutter_plugin_android_lifecycle:androidJdkImage\'.\\n > Failed to transform core-for-system-modules.jar to match attributes {artifactType=_internal_android_jdk_image, org.gradle.libraryelements=jar, org.gradle.usage=java-runtime}.\\n > Execution failed for JdkImageTransform: /Users/xxx/Library/Android/sdk/platforms/android-35/core-for-system-modules.jar.\\n > Error while executing process /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/jlink with arguments {--module-path /Users/xxx/.gradle/caches/transforms-3/6851ef134a0f6efc2285308bad98a1f1/transformed/output/temp/jmod --add-modules java.base --output /Users/xxx/.gradle/caches/transforms-3/6851ef134a0f6efc2285308bad98a1f1/transformed/output/jdkImage --disable-plugin system-modules}\\n\\n* Try:\\n> Run with --stacktrace option to get the stack trace.\\n> Run with --info or --debug option to get more log output.\\n> Run with --scan to get full insights.\\n> Get more help at https://help.gradle.org.\\n\\nBUILD FAILED in 7s\\nRunning Gradle task \'assembleDebug\'... 7.9s\\n\\n┌─ Flutter Fix ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐\\n│ [!] This is likely due to a known bug in Android Gradle Plugin (AGP) versions less than 8.2.1, when │\\n│ 1. setting a value for SourceCompatibility and │\\n│ 2. using Java 21 or above. │\\n│ To fix this error, please upgrade your AGP version to at least 8.2.1. The version of AGP that your project uses is likely defined in: │\\n│ /Users/xxx/self-project/flutter_rive_demo/android/settings.gradle, │\\n│ in the \'plugins\' closure (by the number following \\"com.android.application\\"). │\\n│ Alternatively, if your project was created with an older version of the templates, it is likely │\\n│ in the buildscript.dependencies closure of the top-level build.gradle: │\\n│ /Users/xxx/self-project/flutter_rive_demo/android/build.gradle, │\\n│ as the number following \\"com.android.tools.build:gradle:\\". │\\n│ │\\n│ For more information, see: │\\n│ https://issuetracker.google.com/issues/294137077 │\\n│ https://github.com/flutter/flutter/issues/156304 │\\n└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘\\nError: Gradle task assembleDebug failed with exit code 1\\n
\\n1️⃣ 打开 android/settings.gradle
文件,添加或修改如下内容:
pluginManagement {\\n repositories {\\n google()\\n mavenCentral()\\n gradlePluginPortal()\\n }\\n plugins {\\n id \\"com.android.application\\" version \\"8.3.0\\" apply false\\n id \\"org.jetbrains.kotlin.android\\" version \\"1.9.10\\" apply false\\n id \\"dev.flutter.flutter-plugin-loader\\" version \\"1.0.0\\"\\n }\\n}\\n\\ninclude(\\":app\\")\\n
\\n\\n\\n注意根据项目需求,调整插件版本号
\\n
2️⃣ 打开 android/app/build.gradle
文件,确保包含以下内容:
plugins {\\n id \\"com.android.application\\"\\n id \\"org.jetbrains.kotlin.android\\"\\n id \\"dev.flutter.flutter-gradle-plugin\\"\\n}\\n
\\n3️⃣ 更新 Gradle Wrapper 版本:
\\n打开 android/gradle/wrapper/gradle-wrapper.properties
文件,修改 distributionUrl
为
distributionUrl=https://services.gradle.org/distributions/gradle-8.5-all.zip\\n
\\n4️⃣ 清理并重新构建项目:
\\n在项目根目录下,执行以下命令:
\\nflutter clean\\nflutter pub get\\nflutter run\\n
\\n✅ 说明
\\nplugins {}
块,推荐用于 Flutter 3.16 及以上版本。buildscript {}
和 apply plugin
方式,可以根据需要进行迁移。Flutter作为一个跨平台语言,可以开发手机APP,Web、桌面端应用等。开发Flutter可以使用的工具有AndroidStudio、XCoder、VSCode、IntellJ IDEA等,Android开发者最常使用的开发工具是Android Studio,作为专门给Android开发者的Flutter学习指南,这一篇内容都是基于Android Studio进行编写的。
\\n要开始Flutter开发,首先要配置Flutter的环境。
\\nAndroid Studio下载地址 :developer.android.google.cn/studio?hl=z…\\n根据电脑的系统选择不同的软件包下载,我这里选择的是Window(64位)的软件包 :
\\n下载完成后,安装AndroidStudio
\\n打开AndroidStudio,点击File->Setting->Plugins,选择Marketplace,在输入框中输入Flutter/Dart,下载这两个插件,如图所示:
\\n进入Flutter中文网,docs.flutter.cn/get-started… 在该网站中找到下载并下载FlutterSDK。
\\n在环境变量中配置Flutter SDK:
\\n在AndroidStudio的\\"Settings\\"
中找到\\"Languages & Frameworks\\"
,在其中找到\\"Flutter\\"
,在Flutter页签中配置FlutterSDK,同样的,在Dart页签配置DartSDK\\n
新版本的AndroidStudio中没有创建Flutter Project的选项了,笔者也没有细究这件事,如果无法使用Android Studio直接创建,可以使用命令行创建的方式创建。\\n在创建Project的目录里面打开CMD,或者直接在CMD中进入该目录,输入以下命令创建Flutter Project
\\n\\n\\nflutter create ProjectName
\\n
如图所示:
\\n创建完后,使用AndroidStudio打开该项目 :File -> Open -> my_first_flutter_app
打开之后,可以看到项目自动生成的文件和代码,连接真机,点击右上角的run
按钮,就可以在真机上运行第一个Flutter APP了。
运行结果如下图 :
\\nmy_flutter_project/\\n├── android/ # Android平台代码\\n├── ios/ # iOS平台代码\\n├── lib/\\n│ ├── src/\\n│ │ ├── core/ # 核心层\\n│ │ │ ├── constants/ # 常量\\n│ │ │ │ ├── app_constants.dart # 应用常量\\n│ │ │ │ └── env_constants.dart # 环境常量\\n│ │ │ ├── routes/ # 路由配置\\n│ │ │ │ ├── app_pages.dart # 页面路由表\\n│ │ │ │ └── app_router.dart # 路由生成器\\n│ │ │ ├── theme/ # 主题配置\\n│ │ │ └── utils/ # 工具类\\n│ │ │ ├── dio_util.dart # 网络工具\\n│ │ │ └── storage_util.dart # 存储工具\\n│ │ ├── data/ # 数据层\\n│ │ │ ├── models/ # 数据模型\\n│ │ │ ├── repositories/ # 数据仓库\\n│ │ │ └── services/ # 数据服务(API接口)\\n│ │ ├── presentation/ # 表现层\\n│ │ │ ├── pages/ # 页面组件\\n│ │ │ ├── widgets/ # 公共组件\\n│ │ │ └── state/ # 状态管理\\n│ │ ├── config/ # 环境配置\\n│ │ │ └── app_config.dart\\n│ ├── assets/ # 静态资源\\n│ │ ├── images/ # 图片资源\\n│ │ ├── fonts/ # 字体文件\\n│ │ └── json/ # 本地JSON文件\\n│ ├── generated/ # 自动生成文件(如路由、本地化)\\n│ └── main.dart # 应用入口\\n├── test/ # 测试目录\\n├── scripts/ # 构建/部署脚本\\n├── environments/ # 环境配置文件\\n│ ├── dev.env\\n│ ├── staging.env\\n│ └── prod.env\\n└── pubspec.yaml # 依赖管理\\n
\\n// lib/src/config/app_config.dart\\nabstract class AppConfig {\\n String get apiBaseUrl;\\n String get envName;\\n bool get enableDebugLogs;\\n}\\n\\n// 具体环境配置实现\\nclass DevConfig implements AppConfig {\\n @override String get apiBaseUrl => \'https://api.dev.example.com\';\\n @override String get envName => \'Development\';\\n @override bool get enableDebugLogs => true;\\n}\\n\\nclass ProdConfig implements AppConfig {\\n @override String get apiBaseUrl => \'https://api.example.com\';\\n @override String get envName => \'Production\';\\n @override bool get enableDebugLogs => false;\\n}\\n
\\nvoid main() async {\\n WidgetsFlutterBinding.ensureInitialized();\\n \\n final appConfig = const String.fromEnvironment(\'ENV\') == \'prod\'\\n ? ProdConfig()\\n : DevConfig();\\n\\n runApp(MyApp(appConfig: appConfig));\\n}\\n
\\n# 开发环境\\nflutter run --dart-define=ENV=dev\\n\\n# 生产环境\\nflutter run --dart-define=ENV=prod\\n
\\n// lib/src/core/utils/dio_util.dart\\nclass DioClient {\\n final Dio _dio = Dio();\\n\\n DioClient(AppConfig config) {\\n _dio.options.baseUrl = config.apiBaseUrl;\\n _dio.interceptors.add(LogInterceptor(\\n requestBody: config.enableDebugLogs,\\n responseBody: config.enableDebugLogs,\\n ));\\n _dio.interceptors.add(ErrorInterceptor());\\n }\\n\\n // 封装GET请求\\n Future<Response> get(String path, {Map<String, dynamic>? params}) async {\\n return _dio.get(path, queryParameters: params);\\n }\\n\\n // 统一错误处理\\n static handleError(DioError e) {\\n if (e.response?.statusCode == 401) {\\n // 跳转登录页面\\n }\\n // 其他统一错误处理逻辑\\n }\\n}\\n
\\n// lib/src/data/services/user_service.dart\\nclass UserService {\\n final DioClient _client;\\n\\n UserService(this._client);\\n\\n Future<User> getUserProfile() async {\\n try {\\n final response = await _client.get(\'/user/profile\');\\n return User.fromJson(response.data);\\n } on DioError catch (e) {\\n DioClient.handleError(e);\\n rethrow;\\n }\\n }\\n}\\n
\\nflutter:\\n assets:\\n - assets/images/\\n - assets/json/\\n fonts:\\n - family: CustomIcons\\n fonts:\\n - asset: assets/fonts/custom_icons.ttf\\n
\\nassets/images/\\n├── home/\\n│ ├── home_icon.png\\n│ ├── home_icon@2x.png\\n│ └── home_icon@3x.png\\n└── profile/\\n └── avatar.png\\n
\\n// lib/src/core/constants/app_constants.dart\\nclass AppAssets {\\n static const String homeIcon = \'assets/images/home/home_icon.png\';\\n static const String avatar = \'assets/images/profile/avatar.png\';\\n}\\n\\n// 使用示例\\nImage.asset(AppAssets.homeIcon)\\n
\\n// 使用const构造函数\\nclass MyWidget extends StatelessWidget {\\n const MyWidget({Key? key}) : super(key: key);\\n\\n @override\\n Widget build(BuildContext context) {\\n return const Text(\'Optimized Widget\');\\n }\\n}\\n
\\nListView.builder(\\n itemCount: 1000,\\n itemBuilder: (context, index) {\\n return ListTile(\\n title: Text(\'Item $index\'),\\n // 添加自动保持活跃\\n key: PageStorageKey(\'item_$index\'),\\n );\\n },\\n // 添加项预加载\\n addAutomaticKeepAlives: true,\\n addRepaintBoundaries: true,\\n cacheExtent: 500,\\n);\\n
\\n# pubspec.yaml\\nflutter:\\n uses-material-design: true\\n # 启用图片缓存优化\\n assets:\\n - assets/images/\\n
\\n// 使用RepaintBoundary包裹频繁更新的组件\\nRepaintBoundary(\\n child: AnimatedContainer(\\n duration: const Duration(seconds: 1),\\n color: Colors.blue,\\n ),\\n)\\n
\\n// 使用Visibility和延迟加载\\nVisibility(\\n visible: _showWidget,\\n child: const HeavyWidget(),\\n)\\n\\n// 使用FutureBuilder延迟加载\\nFutureBuilder(\\n future: _loadData(),\\n builder: (context, snapshot) {\\n if (snapshot.hasData) {\\n return DataWidget(snapshot.data!);\\n }\\n return const LoadingIndicator();\\n },\\n)\\n
\\nflutter run --profile # 性能分析模式\\nflutter build apk --analyze-size # 分析包大小\\n
\\nvoid main() {\\n WidgetsFlutterBinding.ensureInitialized();\\n \\n if (kDebugMode) {\\n // 添加性能监控覆盖物\\n debugPaintSizeEnabled = false;\\n debugPrintRebuildDirtyWidgets = true;\\n }\\n\\n runApp(const MyApp());\\n}\\n
\\n该架构具备以下优势:
\\n近日,在 Dart 3.8 的 changelog 里正式提交了 Null-Aware Elements 语法,该语法糖可以用于在 List、Set、Map 等集合中处理可能为 null 的元素或键值对,简化显式检查 null 的场景:
\\n/////////////////之前\\nvar listWithoutNullAwareElements = [\\n if (promotableNullableValue != null) promotableNullableValue,\\n if (nullable.value != null) nullable.value!,\\n if (nullable.value case var value?) value,\\n];\\n\\n/////////////////之后\\nvar listWithNullAwareElements = [\\n ?promotableNullableValue,\\n ?nullable.value,\\n ?nullable.value,\\n];\\n
\\n自然,在 Flutter 的 UI 声明里,也可以简化之前控件的 if 判断,不得不说确实比起之前的写法优雅不少:
\\n/////////////////之前\\nStack(\\n fit: StackFit.expand,\\n children: [\\n const AbsorbPointer(),\\n if (widget.child != null) widget.child!,\\n ],\\n)\\n\\n/////////////////之后\\nStack(\\n fit: StackFit.expand,\\n children: [\\n const AbsorbPointer(),\\n ?widget.child,\\n ],\\n)\\n
\\n同时,官方在分析了大量开源 Dart 代码后(90019 个文件中的 17,941,439 行代码),发现这类需要支持的场景更多是 Map
:
-- Surrounding collection (1812 total) --\\n 1566 ( 86.424%): Map ===============================================\\n 241 ( 13.300%): List ========\\n 5 ( 0.276%): Set =\\n
\\n而事实上,从以下例子可以看出来,在简化 Map
上 Null-Aware Elements 的作用尤为明显:
/////////////////之前\\nfinal tag = Tag()\\n ..tags = {\\n if (Song.title != null) \'title\': Song.title,\\n if (Song.artist != null) \'artist\': Song.artist,\\n if (Song.album != null) \'album\': Song.album,\\n if (Song.year != null) \'year\': Song.year.toString(),\\n if (comments != null)\\n \'comment\': comms!\\n .asMap()\\n .map((key, value) => MapEntry<String, Comment>(value.key, value)),\\n if (Song.numberInAlbum != null) \'track\': Song.numberInAlbum.toString(),\\n if (Song.genre != null) \'genre\': Song.genre,\\n if (Song.albumArt != null) \'picture\': {pic.key: pic},\\n }\\n ..type = \'ID3\'\\n ..version = \'2.4\';\\n\\n/////////////////之后\\nfinal tag = Tag()\\n ..tags = {\\n \'title\': ?Song.title,\\n \'artist\': ?Song.artist,\\n \'album\': ?Song.album,\\n \'year\': ?Song.year?.toString(),\\n if (comments != null)\\n \'comment\': comms!\\n .asMap()\\n .map((key, value) => MapEntry<String, Comment>(value.key, value)),\\n \'track\': ?Song.numberInAlbum?.toString(),\\n \'genre\': ?Song.genre,\\n if (Song.albumArt != null) \'picture\': {pic.key: pic},\\n }\\n ..type = \'ID3\'\\n ..version = \'2.4\';\\n
\\n通过下面的简单例子,也可以看出来有了 Null-Aware Elements 之后在代码简化效果上很明显:
\\n当然,配合其他语法也能达到去 null 的效果,比如最简单的 for 循环,通过 ?i
,就可以简单到做排除空数据的目的:
当然,你可能会觉得本来 Dart 里就有很多 ? ,比如 ?? 、 ?. 之类,加上语法之后会不会有歧义?这个问题在目前的规则上看起来还行,例如此时的 ?
前通常是 ,
、[
、{
或 :
等符号,这些上下文和现有 ?
用法不同 :
var list = [1, ?foo]; // ? 是空感知元素,不是其他用法\\nvar map = {key: ?value}; // ? 是空感知值,不是可空类型\\n
\\n并且前面介绍过,与现有语法如 if
或 for
元素结合时,?
出现在 if
或 for
头部后也不会有歧义:
var list = [\\n for (var i in [1, 2]) ?i, // 合法:?i 是空感知元素\\n];\\nprint(list); // 输出: [1, 2]\\n
\\n而在 Flutter 里的 UI 编排了就更加直观了:
\\n当然,这个语法还是有一些规则限制,在这个规则下 expression 只能是一个普通表达式,不能是另一个集合,比如嵌套的 ?
或展开操作 ...
:
element ::=\\n | nullAwareExpressionElement\\n | nullAwareMapElement\\n | // Existing productions...\\n\\nnullAwareExpressionElement ::= \'?\' expression\\n\\nnullAwareMapElement ::=\\n | \'?\' expression \':\' \'?\'? expression // Null-aware key or both.\\n | expression \':\' \'?\' expression // Null-aware value.\\n
\\n例如下方代码就可以很直观展示这个错误使用,同时也没有 ????foo
或 ?if (c) nullableThing else otherNullableThing
这样的场景:
可以看到, Null-aware elements 语法不管是在逻辑代码还是 UI 代码都十分有用,虽然 Dart 3.8 还没正式发布,但是你可以在 Flutter beta channel 提前体验,那么,你觉这个语法符合你的审美吗?
\\n去年 11 月我们就聊过《Flutter 终于正式规划 IDE Widget 预览支持》,而现在 Widget 预览功能终于开始推进正式落地,并发布了第二版的规划文档:
\\n\\n\\n如果你没看到前文,建议看看 :juejin.cn/post/744100…
\\n
其实一直以来由于 Flutter 具备 hotload 的能力,所以在 Widget Preview 能力这部分都被认为不是必须的场景,但是基于开发者可以更直观验证一些场景,如屏幕大小、方向、字体大小和区域设置等变量对App 的影响,Widget 预览最终还是被提上了议程。
\\n而这次, Widget Preview 之所以正式开始推进,核心主要依赖两点:
\\n\\n因为 Widget Preview 实际会在 .dart_tool
目录下创建一个名为 widget_preview_scaffold
的 Flutter 项目,这个预览支持项目是一个 Flutter Web App 。
\\n\\n所以可以理解为,现在大家都是基于 Canvas 的同源 UI ,所以可以用 Web 来实时渲染,从而在 IDE 内实现实时预览。
\\n
\\n\\n在预览里,开发者可以和预览进行交互,支持缩放和平移,甚至可以预览动画,不过预览时的实际帧率最高只会是 60 FPS。
\\n
而本次 V2 版本规划的预览支持里,主要是新增了 flutter widget-preview
一些列命令,命令负责为项目生成预览脚手架并与预览环境交互支持。
例如,执行 flutter widget-preview start
命令后,就会在 .dart_tool
目录下生成一个预览工程,工程结构大致为:
List<WidgetPreview> previews()
函数,函数最终会将已处理的 WidgetPreview 列表返回到 preview scaffold 用于渲染之后命令会初始化 widget_preview_scaffold
的 pubspec.yaml
,在开发者的项目中添加路径依赖,并列出开发者项目中的资源。
然后在 widget_preview_scaffold
的根目录下生成一个 preview_manifest.json
,包含有关当前 Dart 和 Flutter SDK 版本的信息,以及用户的 pubspec.yaml 的哈希值,这个哈希值用于后续自动对比用户工程的 pubspec 是否发生变化。
接着会使用 package:analyzer
搜索 @Preview()
注解,就如下面代码一样 ,记录需要预览函数名称、库和提供给注解的所有参数等。
最终会根据 analyzer 搜索的结果生成 lib/src/generated_preview.dart
。
@Preview(name: \'Top-level preview\')\\nWidget preview() => const Text(\'Foo\');\\n\\n\\n@Preview(name: \'Builder preview\')\\nWidgetBuilder builderPreview() {\\n return (BuildContext context) {\\n return const Text(\'Builder\');\\n };\\n}\\n\\n\\nclass MyWidget extends StatelessWidget {\\n @Preview(name: \'Constructor preview\')\\n const MyWidget.preview({super.key});\\n\\n\\n @Preview(name: \'Factory constructor preview\')\\n factory MyWidget.factoryPreview() => const MyWidget.preview();\\n\\n\\n @Preview(name: \'Static preview\')\\n static Widget previewStatic() => const Text(\'Static\');\\n\\n\\n @override\\n Widget build(BuildContext context) {\\n return const Text(\'MyWidget\');\\n }\\n}\\n\\n
\\n而一旦用户运行了命令并生成预览脚手架工程之后,它就会被编译并使用提供的 --machine 运行,之后 Flutter 工具将在开发人员的项目目录上初始化一个文件观察器,用于检测源代码的更改,比如:
\\n\\n\\nanalyzer 会检测文件中添加或删除的
\\n@Preview()
,必要时重新生成 lib/src/generated_preview.dart 。
另外,flutter widget-preview clean
可以触发删除 .dart_tool/widget_preview_scaffold/
项目,强制它在下次运行 flutter widget-preview start
时重新生成 。
而针对 Preview 注解也有可定制参数,比如在预览时调节主题,亮度,文本大小等:
\\nbase class Preview {\\n /// Annotation used to mark functions that return widget previews.\\n const Preview({\\n this.name,\\n this.width,\\n this.height,\\n this.textScaleFactor,\\n this.wrapper,\\n this.theme,\\n this.brightness,\\n });\\n\\n
\\n同时,为了防止用户在整个 widget 预览环境中执行热重启,每个渲染的预览都能够在预览的 widget 上执行 “Soft restart” :
\\n\\n\\n“Soft restart” 只是从一帧的 widget 树中删除预览的 widget,然后再将其重新插入到下一帧:github.com/flutter/flu…
\\n
最后,因为最终需要在 IDE 中托管的 webview 中打开预览,需要类似于 Dart/Flutter DevTools 嵌入到 IDE 中的方式,但是由于 DWDS(Web VM 服务实现)需要一个开放的 Chrome 调试端口才能运行,这在 VSCode 等 IDE 中不可用,但是 DWDS 又是热重载的必备支持,所以目前最大的阻碍是:
\\n\\n\\n在没有 Chrome 调试端口的情况下运行 DWDS,同时保持热重载,做到当 DWDS 无法访问 Chrome 调试器时,改为提供一组有限的功能:github.com/dart-lang/w…
\\n
其他问题还有比如 dart:io
、原生插件等场景需要如何在预览工程处理,但是这些问题都是后话,在 DWDS 适配 IDE场景支持后,IDE 预览就可以基本考虑实验性落地了。
看得出来其实 IDE 预览的话核心其实来自 Flutter Web ,由于 Flutter Web 支持 hotload 之后,用一个阴影工程来做实时预览确实是一个相对低成本的选择,不过真的要完整落地,需要考虑的细节还是很多,其中最重要的莫过于使用过程中的性能影响,如果体验太差,还不如直接 hotload 运行实际。
\\n那么,你会期待或者需要 Flutter 的 IDE Widget 预览吗?
\\nsignals到底有多简单。
\\nimport \'package:signals/signals.dart\';\\n\\n\\nvoid main() {\\n final name = signal(\\"Jane\\");\\n final surname = signal(\\"Doe\\");\\n final fullName = computed(() => name.value + \\" \\" + surname.value);\\n\\n// Logs: \\"Jane Doe\\"\\n effect(() {\\n print(\\"name is ${name.value}\\");\\n print(\\"fullName is ${fullName.value}\\");\\n });\\n// Updating one of its dependencies will automatically trigger\\n// the effect above, and will print \\"John Doe\\" to the console.\\n name.value = \\"John\\";\\n\\n}\\n\\n
\\n运行结果如下图所示:
\\n分配两个Signal:name和surname,一个Computed(fullName)是name和surname计算结果,effect内部监听数据变化打印name和fullName的值。当name的值被重新设置的时候,fullName的值是name和surname的组合计算,也会跟随重新计算。
\\n\\nComputed会注册监听多个Signal,当其中的任何一个Signal值变化的时候,都会把值通知Computed从而引起Computed值的变换。注意上面图的箭头方向代表数据流向。
你也可以从effect中返回一个清理函数。当effect被销毁时,该函数会被调用。清理函数以阻止订阅更新回调。
\\n final s = signal(0);\\n\\n final dispose1 = effect(() {\\n print(s.value);\\n return () => print(\'Effect destroyed\');\\n });\\n\\n // Destroy effect and subscriptions\\n dispose1();\\n s.value = 2;\\n
\\n运行结果如下图所示:只打印了一次s的值,dispose1函数执行以后,就不再监听s的数据变化。
\\n需要注意effect使用过程中防止发生无限循环问题:不能在effect函数体中改变signal的值,否则会无限循环导致不可处理异常。
\\n批处理功能允许您将多个信号写入组合成一个更新,该更新在回调完成时触发。
\\n final counter = signal(0);\\n final _double = computed(() => counter.value * 2);\\n final _triple = computed(() => counter.value * 3);\\n effect(() {\\n print(_double.value);\\n print(_triple.value);\\n });\\n\\n batch(() {\\n counter.value = 1;\\n // Logs: 2, despite being inside batch, but `triple`\\n // will only update once the callback is complete\\n print(_double.value);\\n });\\n// Now we reached the end of the batch and call the effect\\n
\\nbatch执行完成后才会执行effect。batch可以嵌套,并且当最外层批次调用完成时,更新将被刷新。运行结果如下图所示:\\n
在Flutter中,如果你想创建一个signal,当Widget从Widget树中移除时,该signal会自动释放,并在signal发生变化时重建Widget,你可以在有状态Widget中使用createComputed。示例代码如下:
\\nimport \'package:flutter/material.dart\';\\nimport \'package:signals/signals_flutter.dart\';\\n\\nvoid main() async {\\n runApp(CounterWidget());\\n}\\n\\nclass CounterWidget extends StatefulWidget {\\n @override\\n _CounterWidgetState createState() => _CounterWidgetState();\\n}\\n\\nclass _CounterWidgetState extends State<CounterWidget> with SignalsMixin {\\n late final counter = createSignal(0);\\n late final isEven = createComputed(() => counter.value.isEven);\\n late final isOdd = createComputed(() => counter.value.isOdd);\\n\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(home: Scaffold(\\n body: Center(\\n child: Column(\\n mainAxisAlignment: MainAxisAlignment.center,\\n children: [\\n Text(\'Counter: even=$isEven, odd=$isOdd\'),\\n ElevatedButton(\\n onPressed: () => counter.value++,\\n child: Text(\'Increment\'),\\n ),\\n ],\\n ),\\n ),\\n ),);\\n }\\n}\\n
\\n无需Watch Widget或扩展程序,当Widget从Widget树中移除时,signal将自动处置。SignalsMixin是一个混合类,当Widget从Widget树中移除时,它会自动处置在该状态下创建的signal。
\\nsignals是一种存储状态的方式,当值发生变化时自动通知监听器,而不需要setState、Provider或其他重量级解决方案。可以通过signal发送一个信号,可以通过computed组合计算多个signal,可以通过effect监听数据变化,可以通过Batch批量处理多个signal的值。signals使用起来还是挺简单的,但是signals的原理需要下篇来讲解。希望文章对您有帮助,祝大家编码愉快。
\\ndartsignals.dev/\\npub.dev/packages/si…\\ndartsignals.dev/reference/o…
","description":"signals有多简单 signals到底有多简单。\\n\\n可以通过signal发送一个信号。\\n可以通过computed组合计算多个signal。\\n可以通过effect监听数据变化。 示例代码如下:\\nimport \'package:signals/signals.dart\';\\n\\n\\nvoid main() {\\n final name = signal(\\"Jane\\");\\n final surname = signal(\\"Doe\\");\\n final fullName = computed(() => name.value + \\" \\" + surname…","guid":"https://juejin.cn/post/7497170890082517029","author":"技术蔡蔡","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-26T07:21:52.620Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/31d726bc1a014588a3641c5508ca4cb9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oqA5pyv6JSh6JSh:q75.awebp?rk3s=f64ab15b&x-expires=1746256911&x-signature=pN8h8ULaDa2OpAuQ0a8dzW8Wna0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7fa1bf29bb6747f286337000b75aa34a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oqA5pyv6JSh6JSh:q75.awebp?rk3s=f64ab15b&x-expires=1746256911&x-signature=WQvJ3CGhkQcIRji0nXUguMyuv8M%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e5c523927c2846d49fd4987580564f2d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oqA5pyv6JSh6JSh:q75.awebp?rk3s=f64ab15b&x-expires=1746256911&x-signature=uy49M8TafulC8CmWflaVMIelbAA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6f843cfaf47c492f94a4a634b13f1119~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oqA5pyv6JSh6JSh:q75.awebp?rk3s=f64ab15b&x-expires=1746256911&x-signature=Nz1ifvc9sQOS6k3L90s%2FmOj%2B5rw%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["代码人生","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"【Flutter 状态管理 - 柒】 | InheritedWidget:藏在组件树里的\\"魔法\\"✨","url":"https://juejin.cn/post/7497074247778992167","content":"Flutter
中的组件树像一片茂密的森林🌳,数据传递常让人头疼 —— 层层 Props
透传如同让快递员翻山越岭送包裹📦。而 InheritedWidget
就像一位精通空间魔法的精灵🧚♂️,能让特定数据瞬间穿透整棵组件树,直达需要的叶子节点。它不仅是 Flutter
状态管理的基石,更是 Provider
等热门方案的底层魔法!
本文将用一碗螺蛳粉的时间🍜,带你解锁这个「看似低调实则强悍」的跨组件通信神器。
\\n操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\nvs
电网 🔍\\n\\n官方定义:
\\nInheritedWidget
是一个能将数据自上而下广播给子孙组件的特殊Widget
。
为了进一步的深入理解,从下面两个现实中的事物展开探究 👇
\\n想象你出生在一个庞大的家族中,每个成员都想知道自己的祖传姓氏。传统做法是:你问父亲,父亲问爷爷,爷爷再问太爷爷……层层向上追溯(类似 Props
逐级传递)。而 InheritedWidget
则像一本悬浮在家族上空的魔法族谱📜,所有后代只需抬头就能直接看到源头信息,无需中间人传话。
\\n\\n技术映射:
\\n
\\n当子组件通过context.dependOnInheritedWidgetOfExactType<T>()
获取数据时,Flutter
会沿着组件树向上「扫描」,找到最近的InheritedWidget
实例。这就像家族成员通过DNA
标记自动绑定族谱,直接锁定最近的祖先数据源,而非遍历整棵树。
即插即用
」哲学把组件树想象成一栋大楼,每个房间(组件)可能需要电力(数据)。传统做法是:从总闸拉电线到每个房间(Props
透传),一旦线路复杂就会变成「蜘蛛网」🕸️。而 InheritedWidget
如同整栋楼的隐形电网系统,只要房间声明「我需要电」(依赖 InheritedWidget
),电路会自动接通,电能直达目标。
\\n\\n技术实现:
\\n
\\nFlutter
在Element
树中维护了一个InheritedWidget
的哈希表。当子组件调用of(context)
方法时,实际是通过BuildContext
(即当前Element
)查表,以O(1)
时间复杂度直接取到数据。这比传统Props
传递的O(n)
复杂度高效得多!
空间代价:在内存中维护组件树与 InheritedWidget
的引用关系。
\\n时间收益:数据获取从线性遍历变为哈希查找,效率飞跃。
这就像在图书馆用索引卡找书📚 —— 与其逐个书架搜索(O(n)
),不如先查目录再直奔目标区域(O(1)
)。尽管需要额外维护索引(空间成本),但换取的是极致的速度。
\\n\\n万物皆有利有弊,注重的是一个平衡艺术。
\\n
Flutter
的 UI
是树形结构,而数据流动方向暗合了「重力法则」🌍:
1️⃣ 自然性:水往低处流,数据从根节点流向叶子节点,符合代码书写逻辑(父组件定义数据,子组件消费)。
\\n2️⃣ 稳定性:祖先节点先于子节点构建,确保子组件获取数据时,数据源已存在(避免“电路还没通电,灯泡就先亮了”
的悖论)。
反模式思考:
\\n如果允许数据反向传递(子→父
),就像让电流从灯泡倒流回电厂,会导致状态管理混乱。InheritedWidget
的单向性正是框架设计的智慧取舍。
\\n假设父组件需要向深层子组件传递数据:
class ParentWidget extends StatelessWidget {\\n final String config;\\n\\n ParentWidget(this.config);\\n\\n @override\\n Widget build(BuildContext context) {\\n return ChildWidget(config); // 父 → 子\\n }\\n}\\n\\nclass ChildWidget extends StatelessWidget {\\n final String config;\\n\\n ChildWidget(this.config);\\n\\n @override\\n Widget build(BuildContext context) {\\n return GrandchildWidget(config); // 子 → 孙子\\n }\\n}\\n\\nclass GrandchildWidget extends StatelessWidget {\\n final String config;\\n\\n GrandchildWidget(this.config); // 最终使用数据\\n} \\n
\\n上述代码的三大核心问题:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n核心问题 | 问题表现 | 技术影响 |
---|---|---|
冗余代码 | 中间组件被迫传递不需要的参数 | 代码可读性下降,维护成本指数级增长 📉 |
耦合度高 | 数据结构变更需逐层修改构造函数 | 牵一发动全身,迭代效率低下 🚧 |
性能浪费 | 每次参数传递都可能触发子组件重建 | 整棵子树重建,渲染性能雪崩式下降 ⏳ |
有些人可能会使用全局变量或单例模式:
\\n// 全局变量\\nString globalConfig = \\"default\\";\\n\\nclass GrandchildWidget extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return Text(globalConfig); // 直接访问全局变量\\n }\\n}\\n
\\n直接使用全局变量的三大核心问题:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n核心问题 | 问题表现 | 技术影响 |
---|---|---|
状态不可控 | 数据变化时无法自动触发 UI 更新 | 需手动监听/刷新,代码冗余且易遗漏 🔄 |
耦合度高 | 全局单一实例,无法实现子树级配置覆盖 | 组件复用性下降,多环境适配困难 🚫 |
测试困难 | 全局状态跨测试用例共享,导致相互污染 | 测试结果不可靠,难以实现隔离测试 🧪 |
Flutter
框架的设计约束 ⚙️核心机制 | 核心概念 | 设计优势 | 技术影响 |
---|---|---|---|
Widget 不可变性 | Widget 为不可变对象 数据变化需重建组件树 | 保证 UI 一致性,避免意外副作用 | 显式依赖:数据与组件关系明确,减少隐式耦合 高效更新:仅重建依赖部分子树 🛠️ |
响应式编程范式 | 数据驱动视图 声明式 UI 架构 | 视图与数据自动同步,代码更简洁 | 自动订阅:组件自动感知数据变化 局部刷新:精准更新,性能提升 🚀 |
InheritedWidget
的魔法 ✨(核心价值)可绕过中间任意层组件,直接让数据穿透到第 N
代子孙,如同「隔山打牛」。
class GrandchildWidget extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n // 任意子组件中获取数据 \\n final config = MyConfigInherited.of(context).config;\\n return Text(config);\\n }\\n}\\n
\\n✅ 优势:代码简洁,减少冗余参数传递;数据与 UI
解耦,提升可维护性。
依赖组件像被打了「标记」🎯,仅当数据变化时触发重绘,避免整树刷新。
\\n技术实现:\\n通过 dependOnInheritedWidgetOfExactType()
实现:
updateShouldNotify
控制是否触发重建。子孙组件无需被父组件「显式传递参数」,降低耦合度,数据与 UI
实现「松绑式协作」。
如 Provider
、Riverpod
等库均基于 InheritedWidget
,实现高效状态共享。
createElement()
:构建数据广播的「基站」创建与 InheritedWidget
关联的 InheritedElement
对象,作为数据广播系统的「基站」,负责管理依赖组件的注册与通知。
Widget
到 Element
的转换:Flutter
在构建组件树时,会自动调用此方法将 Widget
转换为对应的 Element
对象。
@override \\nInheritedElement createElement() => InheritedElement(this); \\n
\\n依赖关系管理:InheritedElement
内部维护一个 Map<Element, Object?>
\\n结构,记录哪些子组件依赖当前数据。这类似于基站记录所有连接的设备。
关键特性:
\\nFlutter
框架在组件挂载时自动执行。InheritedWidget
实例对应唯一的 InheritedElement
。updateShouldNotify()
:数据更新的「安检门」对比新旧 InheritedWidget
的数据差异,决定是否通知依赖组件更新。
触发时机:当父组件重建并生成新的 InheritedWidget
时,Flutter
会调用此方法。
class AppConfig extends InheritedWidget { \\n final String appName; \\n final Color primaryColor; \\n \\n bool updateShouldNotify(AppConfig old) { \\n // 仅当主题色变化时触发更新(appName 变化不触发) \\n return primaryColor != old.primaryColor; // 🎨 精准控制更新粒度 \\n } \\n} \\n
\\n底层机制: 返回 true
时,所有依赖组件会被标记为「脏」并重建;返回 false
则跳过更新,避免无效渲染。
\\n精细化控制:只对比真正影响 UI
的字段(如主题色、用户权限),忽略辅助字段。
\\n避免复杂计算:此方法可能被高频调用,需保持 O(1)
时间复杂度。
of(context)
方法:数据定位的「导航仪」此方法是需要在自定义的InheritedWidget
子类中手动实现的静态方法,通过 BuildContext
在组件树中向上查找最近的 InheritedWidget
实例,并注册依赖关系。
class AppConfig extends InheritedWidget {\\n final String appName;\\n final Color primaryColor;\\n\\n // 构造函数\\n AppConfig({\\n required this.appName,\\n required this.primaryColor,\\n required Widget child,\\n }) : super(child: child);\\n\\n // 核心的 of(context) 方法\\n static AppConfig of(BuildContext context) {\\n return context.dependOnInheritedWidgetOfExactType<AppConfig>()!;\\n }\\n\\n @override\\n bool updateShouldNotify(AppConfig oldWidget) {\\n return primaryColor != oldWidget.primaryColor;\\n }\\n}\\n
\\n查找数据:沿组件树向上搜索,找到最近的匹配类型。
\\n注册依赖:将当前组件的 Element
添加到 InheritedElement
的监听列表。
\\n底层机制:Flutter
在 Element
树中维护了一个 Map<Element, Object?>
,使得查找时间复杂度为 O(1)
。
需求:用户切换应用主题色,实现四部曲:
\\n\\n1️⃣ 数据更新:根部的
ThemeConfig(themeColor: newColor)
被重建,生成新的 InheritedWidget
。
\\n2️⃣ 更新检测:updateShouldNotify
检测到 themeColor
变化,返回 true
。
\\n3️⃣ 依赖通知:InheritedElement
遍历所有通过 of(context)
注册的组件,触发其重建。
\\n4️⃣ 精准渲染:只有依赖 themeColor
的按钮、文本等组件重绘,其他组件不受影响。
\\n\\n通过上述三大核心机制,
\\nInheritedWidget
实现了Flutter
生态中最基础、最高效的跨组件数据广播系统,为后续的状态管理方案奠定了底层基础。
Flutter
的「树形智慧」🌲核心思想:将数据存储(是什么)与组件渲染(怎么展示)彻底解耦,如同将「仓库管理员」和「商店陈列师」角色分离。
\\n技术映射:
\\n数据存储:由 InheritedWidget
集中管理,像仓库储存货物。
class AppConfig extends InheritedWidget { \\n final String apiUrl; // 数据定义在此 \\n // ... \\n} \\n
\\n组件消费:子组件通过 of(context)
获取数据,像商店按需取货,不关心库存逻辑。
Text(AppConfig.of(context).apiUrl); // 仅消费数据 \\n
\\n设计优势:
\\nUI
层(如切换 API
域名)。UI
渲染。反模式警示:
\\n❌ 避免在 build()
方法中直接发起网络请求(数据与渲染混杂)。
核心思想:数据从根节点向叶子节点单向流动,如同水流从山顶到山谷的自然路径,禁止逆流。
\\n技术体现:
\\nAppConfig( // 根节点 \\n apiUrl: \'https://api.example.com\', \\n child: HomePage( // 子节点 \\n child: ProfileView(), // 叶子节点 \\n ), \\n) \\n
\\n底层原理:
\\nFlutter
的 Element
树在构建时形成「单向链表」,每个节点只保留父节点引用,天然支持高效向下遍历。
核心思想:组件只需声明「我需要什么」,而无需知道数据如何抵达,如同顾客点餐时无需了解食材采购路径。
\\n技术实现:
\\n依赖声明:通过 of(context)
隐式声明依赖,无需父组件显式传递。
// 子组件无需知道 apiUrl 如何从根组件传递至此 \\nfinal apiUrl = AppConfig.of(context).apiUrl; \\n
\\n上下文隔离:组件不感知上层结构变化(如中间插入新父组件)。
\\n这就像快递柜📦 —— 收件人只需输入取件码,无需知晓包裹如何从分拣中心运输而来。
\\nFlutter
生态的启示设计理念 | 核心理念 | 技术优势 | 框架影响 |
---|---|---|---|
Widget 即配置 | 所有 UI 组件为不可变配置对象,每次数据变化生成新实例 | ✅ 自然契合单向数据流 ✅ 避免隐式状态副作用 ✅ 提升渲染一致性 | 🚀 强制开发者遵循「数据驱动 UI 」模式 🚀 简化 Diff 算法实现高效更新 |
组合优于继承 | 通过 Widget 组合(嵌套结构)而非类继承实现功能扩展 | ✅ 降低组件耦合度 ✅ 灵活复用功能模块 ✅ 避免继承链的脆弱性 | 🛠️ 构建声明式 UI 体系 🛠️ 支持热重载快速迭代 |
拥抱约束 | 通过限制数据流向(如强制单向流动)换取可控性与性能 | ✅ 状态变更路径可预测 ✅ 减少循环依赖风险 ✅ 优化渲染性能(局部更新) | 🌳 形成「树干到枝叶」生态 🔒 避免开发者陷入「状态管理泥潭」 |
如同大自然中树木的生长规律🌳,Flutter
的树形智慧通过 关注点分离、单向数据流 和 最小知识原则,在灵活性与秩序之间找到了完美平衡。这种设计不仅让 InheritedWidget
成为高效的状态管理工具,更塑造了整个 Flutter
框架的哲学基因 —— 用约束创造自由,以结构驾驭复杂。
方案 vs 特性 | 学习成本 | 适用场景 | 性能影响 | 测试友好度 | 设计哲学 |
---|---|---|---|---|---|
InheritedWidget | 🔴 高(需手动管理依赖更新) | 🟢 简单全局状态(主题/用户信息) | ⚡️ 极低(直接树查询无中间层) | 🔴 困难(手动构建上下文) | 🟢 Flutter 底层原语(数据穿透树) |
Provider | 🟢 低(语法糖封装) | 🟡 中小应用 / 父子组件状态共享 | 🟡 中(Widget 包装开销) | 🟢 高(内置测试工具) | 🟡 基于 InheritedWidget 的语法糖 |
Riverpod | 🟡 中高(强类型 + 多模式) | 🟢 中大型应用 / 强类型项目 | 🟢 中低(编译期优化) | ✅ 极高(依赖隔离 + Mock ) | 🟢 无 BuildContext 依赖 |
Bloc | 🟡 中(需理解流式编程) | 🔴 复杂业务流(订单/支付流程) | 🟡 中(Stream 监听开销) | 🟡 中(需 Mock 事件流) | 🔴 响应式事件流(Rx 模式衍生) |
\\n\\n🌟 个人见解:
\\n\\n
\\n- \\n
InheritedWidget
:像手摇咖啡磨豆机☕️ —— 需要精准操作,但能极致掌控细节。- \\n
Provider
:像胶囊咖啡机☕ —— 一键出品,适合快速开发,但扩展性有限。- \\n
Riverpod
:像智能意式咖啡机🤖 —— 支持自定义研磨刻度、奶泡比例,满足专业级需求。- \\n
Bloc
:像咖啡工厂流水线🏭 —— 适合大规模标准化生产,但启动成本较高。
Theme
)Theme( \\n data: ThemeData.dark(), // 🎨 全局主题 \\n child: AppBody(), \\n) \\n\\n// 子组件直接使用 \\nText( \\n style: Theme.of(context).textTheme.titleLarge, // 自动响应主题变化 \\n) \\n
\\nTheme
,所有依赖的 Widget
自动更新。Theme
实现局部样式调整。MediaQuery
)通过 MediaQuery.of(context)
获取屏幕尺寸、方向等信息:
// 直接获取屏幕信息 \\nbool isMobile = MediaQuery.of(context).size.width < 600; \\n// 屏幕旋转时,依赖组件自动更新🔄 \\n// ....\\n// 自动响应屏幕旋转\\nbool isPortrait = MediaQuery.of(context).orientation == Orientation.portrait;\\n
\\nBuildContext
:直接在任何子 Widget
中访问设备信息。Widget
自动重建需求:用户切换语言时,所有文本实时翻译,无需重启页面。
\\n// 1. 定义多语言 InheritedWidget \\nclass AppLocalizations extends InheritedWidget { \\n final Locale locale; \\n final Map<String, String> translations; \\n final VoidCallback onLocaleChanged; // 语言切换回调 \\n \\n const AppLocalizations({ \\n required this.locale, \\n required this.translations, \\n required this.onLocaleChanged, \\n required super.child, \\n }); \\n \\n // 2. 核心方法:获取翻译文本 \\n String tr(String key) => translations[key] ?? key; \\n \\n // 3. 更新条件:仅当语言标识变化时触发 \\n @override \\n bool updateShouldNotify(AppLocalizations old) => locale != old.locale; \\n \\n // 4. 静态方法便捷访问 \\n static AppLocalizations of(BuildContext context) { \\n return context.dependOnInheritedWidgetOfExactType<AppLocalizations>()!; \\n } \\n} \\n\\n// 5. 使用示例:文本组件 \\nclass TranslatedText extends StatelessWidget { \\n final String key; \\n \\n const TranslatedText(this.key, {super.key}); \\n \\n @override \\n Widget build(BuildContext context) { \\n return Text(AppLocalizations.of(context).tr(key)); \\n } \\n} \\n\\n// 6. 语言切换按钮 \\nclass LanguageSwitcher extends StatelessWidget { \\n @override \\n Widget build(BuildContext context) { \\n final localizations = AppLocalizations.of(context); \\n return DropdownButton<Locale>( \\n value: localizations.locale, \\n items: const [ \\n DropdownMenuItem(value: Locale(\'en\'), child: Text(\'English\')), \\n DropdownMenuItem(value: Locale(\'zh\'), child: Text(\'中文\')), \\n ], \\n onChanged: (newLocale) => localizations.onLocaleChanged(), \\n ); \\n } \\n} \\n
\\n设计要点:
\\nlocale
和 translations
为 final
,语言切换需整体替换 Widget
。JSON
文件加载翻译,实现动态远程配置。用户信息如同「企业员工工牌」🪪,一次认证全网通行,权限变更实时生效。
\\n// 1. 用户信息容器 \\nclass UserSession extends InheritedWidget { \\n final User user; \\n final VoidCallback onLogout; \\n final Future<void> Function(User) onUserUpdate; \\n \\n const UserSession({ \\n required this.user, \\n required this.onLogout, \\n required this.onUserUpdate, \\n required super.child, \\n }); \\n \\n // 2. 用户信息更新方法 \\n Future<void> updateProfile(User newUser) async { \\n await onUserUpdate(newUser); \\n } \\n \\n @override \\n bool updateShouldNotify(UserSession old) => \\n user != old.user || \\n onLogout != old.onLogout || \\n onUserUpdate != old.onUserUpdate; \\n \\n static UserSession of(BuildContext context) => \\n context.dependOnInheritedWidgetOfExactType<UserSession>()!; \\n} \\n\\n// 3. 应用入口包裹 \\nvoid main() { \\n runApp( \\n UserSession( \\n user: fetchInitialUser(), \\n onLogout: () => clearAuthToken(), \\n onUserUpdate: (user) => api.updateUser(user), \\n child: const MyApp(), \\n ), \\n ); \\n} \\n\\n// 4. 权限控制组件 \\nclass AuthGuard extends StatelessWidget { \\n final Widget child; \\n \\n const AuthGuard({required this.child, super.key}); \\n \\n @override \\n Widget build(BuildContext context) { \\n final user = UserSession.of(context).user; \\n return user.isAuthenticated ? child : const LoginScreen(); \\n } \\n} \\n\\n// 5. 个人资料页 \\nclass ProfilePage extends StatelessWidget { \\n @override \\n Widget build(BuildContext context) { \\n final session = UserSession.of(context); \\n return Column( \\n children: [ \\n Text(session.user.name), \\n ElevatedButton( \\n onPressed: () => session.updateProfile(session.user.copyWith(name: \'新用户名\')), \\n child: const Text(\'更新资料\'), \\n ), \\n ElevatedButton( \\n onPressed: session.onLogout, \\n child: const Text(\'退出登录\'), \\n ), \\n ], \\n ); \\n } \\n} \\n
\\n安全设计:
\\nAuthGuard
实现路由级权限控制。token
)只保存在内存,不通过 InheritedWidget
传递。UI
更新。InheritedWidget
如同 Flutter
组件树里的隐形数据高铁🚄,用极致的效率重新定义了跨组件通信。虽然直接使用它需要手动处理更新逻辑,但理解其原理能让你在遇到 Provider
闪退问题时,快速定位到是updateShouldNotify
逻辑错误 还是 上下文丢失。
高手往往用最简单的工具创造魔法 —— 下次传递数据时,不妨试试这个「祖传秘方」吧!🧙♂️
\\n\\n","description":"前言 Flutter中的组件树像一片茂密的森林🌳,数据传递常让人头疼 —— 层层 Props 透传如同让快递员翻山越岭送包裹📦。而 InheritedWidget 就像一位精通空间魔法的精灵🧚♂️,能让特定数据瞬间穿透整棵组件树,直达需要的叶子节点。它不仅是 Flutter 状态管理的基石,更是 Provider 等热门方案的底层魔法!\\n\\n本文将用一碗螺蛳粉的时间🍜,带你解锁这个「看似低调实则强悍」的跨组件通信神器。\\n\\n操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。\\n\\n本质定义:家族族谱 vs 电网 🔍\\n\\n官方定义:Inherited…","guid":"https://juejin.cn/post/7497074247778992167","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-26T04:54:40.066Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6e7433837dab45f3aee30fe8b15bbe1b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1746248080&x-signature=4A8OpMC5%2FElAjaWAhxSiNn41NMo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/10b89affac054d899ad4ec8714689829~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1746248080&x-signature=pESTa4u0Dcl6brVjBmpR7hezbHE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3dcccfae57f1478cafd7b0b757bee753~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1746248080&x-signature=ADtE4RE99VO%2FCYdbRkF%2FRyRfmLk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/dc8b4bbb92fa40b2a9a265accb9dc601~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1746248080&x-signature=OHqT4cPnoZpWO%2BGDvEqW%2BGC12uY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/952e7f93323e4c119adffc50bb29722c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1746248080&x-signature=xCH45WY72uaiqMPXCP0%2BGD%2Bs%2B%2Fg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/12acc47c144e4ef3973eeed4e1c7c8c2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1746248080&x-signature=LrzR%2F2NrVPPtf3ruppvxWn7MAog%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 环境搭建 (Android)","url":"https://juejin.cn/post/7496876252630695971","content":"目标 上一篇Flutter应用已经能在iOS,macOS和,chrome环境下正常运行了,这次把Android跑通。 环境 环境搭建 Flutter的Android的工具链 Android SDK P","description":"目标 上一篇Flutter应用已经能在iOS,macOS和,chrome环境下正常运行了,这次把Android跑通。 环境 环境搭建 Flutter的Android的工具链 Android SDK P","guid":"https://juejin.cn/post/7496876252630695971","author":"忘川三","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-25T10:17:54.912Z","media":null,"categories":["代码人生","Flutter","Android","Visual Studio Code"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 动画之 Implicit 隐式动画","url":"https://juejin.cn/post/7496876246840885288","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
//系统内置的隐式动画(以 Animated 开头的组件,都继承自 ImplicitlyAnimatedWidget)\\n//动画触发依赖于属性变化\\n//AnimatedContainer:动态改变容器属性(比如宽高、颜色和对齐方式等)\\n//AnimatedDefaultTextStyle:控制文本样式过渡\\n//AnimatedSlide:控制滑动\\n//AnimatedScale:控制缩放\\n//AnimatedRotation:控制旋转\\n//AnimatedOpacity:控制透明度渐变\\n//AnimatedPositioned:在 Stack 中平滑移动子组件(必须配合 Stack 使用)\\n//AnimatedSwitcher:组件切换时添加过渡动画(比如淡入淡出)\\n\\n//AnimatedIcon:图标动画\\n//AnimatedTheme:主题切换时的平滑过渡\\n//AnimatedPadding:动态控制边距\\n//AnimatedAlign:动态调整子组件对齐方式\\n
\\nclass MyAnimatedContainer extends StatefulWidget {\\n const MyAnimatedContainer({super.key});\\n\\n @override\\n State<MyAnimatedContainer> createState() => _MyAnimatedContainerState();\\n}\\n\\nclass _MyAnimatedContainerState extends State<MyAnimatedContainer> {\\n bool _flag = true;\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n floatingActionButton: FloatingActionButton(\\n onPressed: () => setState(() => _flag = !_flag),\\n child: const Icon(Icons.animation),\\n ),\\n body: Center(\\n child: AnimatedContainer(\\n duration: const Duration(milliseconds: 500),\\n width: _flag ? 100 : 200,\\n height: _flag ? 100 : 200,\\n color: _flag ? Colors.blue : Colors.red,\\n alignment: _flag ? Alignment.topLeft : Alignment.bottomRight,\\n child: const Text(\'test AnimatedContainer\', textDirection: TextDirection.rtl)),\\n ),\\n );\\n }\\n}\\n
\\nclass MyAnimatedDefaultTextStyle extends StatefulWidget {\\n const MyAnimatedDefaultTextStyle({super.key});\\n\\n @override\\n State<MyAnimatedDefaultTextStyle> createState() =>\\n _MyAnimatedDefaultTextStyleState();\\n}\\n\\nclass _MyAnimatedDefaultTextStyleState\\n extends State<MyAnimatedDefaultTextStyle> {\\n bool _flag = true;\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n floatingActionButton: FloatingActionButton(\\n onPressed: () => setState(() => _flag = !_flag),\\n child: const Icon(Icons.animation),\\n ),\\n body: Center(\\n child: AnimatedDefaultTextStyle(\\n style: _flag\\n ? const TextStyle(fontSize: 24)\\n : const TextStyle(fontSize: 16),\\n duration: const Duration(milliseconds: 500),\\n child: const Text(\'test AnimatedDefaultTextStyle\'),\\n )));\\n }\\n}\\n
\\nclass MyAnimatedSlide extends StatefulWidget {\\n const MyAnimatedSlide({super.key});\\n\\n @override\\n State<MyAnimatedSlide> createState() => _MyAnimatedSlideState();\\n}\\n\\nclass _MyAnimatedSlideState extends State<MyAnimatedSlide> {\\n bool _flag = true;\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n floatingActionButton: FloatingActionButton(\\n onPressed: () => setState(() => _flag = !_flag),\\n child: const Icon(Icons.animation),\\n ),\\n body: Center(\\n child: AnimatedSlide(\\n offset: _flag ? const Offset(1.0, 0.0) : const Offset(0.0, 0.0),\\n duration: const Duration(milliseconds: 500),\\n child: Container(\\n width: 100,\\n height: 100,\\n color: Colors.blue,\\n child: const Center(\\n child: Text(\'test AnimatedSlide\'),\\n ),\\n ))));\\n }\\n}\\n
\\nclass MyAnimatedScale extends StatefulWidget {\\n const MyAnimatedScale({super.key});\\n\\n @override\\n State<MyAnimatedScale> createState() => _MyAnimatedScaleState();\\n}\\n\\nclass _MyAnimatedScaleState extends State<MyAnimatedScale> {\\n bool _flag = true;\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n floatingActionButton: FloatingActionButton(\\n onPressed: () => setState(() => _flag = !_flag),\\n child: const Icon(Icons.animation),\\n ),\\n body: Center(\\n child: AnimatedScale(\\n scale: _flag ? 3.0 : 1.5,\\n duration: const Duration(milliseconds: 500),\\n child: Container(\\n width: 100,\\n height: 100,\\n color: Colors.blue,\\n child: const Center(\\n child: Text(\'test AnimatedScale\'),\\n ),\\n ),\\n )));\\n }\\n}\\n
\\nclass MyAnimatedRotation extends StatefulWidget {\\n const MyAnimatedRotation({super.key});\\n\\n @override\\n State<MyAnimatedRotation> createState() => _MyAnimatedRotationState();\\n}\\n\\nclass _MyAnimatedRotationState extends State<MyAnimatedRotation> {\\n bool _flag = true;\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n floatingActionButton: FloatingActionButton(\\n onPressed: () => setState(() => _flag = !_flag),\\n child: const Icon(Icons.animation),\\n ),\\n body: Center(\\n child: AnimatedRotation(\\n turns: _flag ? 1 : 0, //旋转的圈数\\n duration: const Duration(seconds: 1),\\n child: const Icon(\\n Icons.refresh,\\n size: 200,\\n color: Colors.blue,\\n ),\\n )));\\n }\\n}\\n
\\nclass MyAnimatedOpacity extends StatefulWidget {\\n const MyAnimatedOpacity({super.key});\\n\\n @override\\n State<MyAnimatedOpacity> createState() => _MyAnimatedOpacityState();\\n}\\n\\nclass _MyAnimatedOpacityState extends State<MyAnimatedOpacity> {\\n bool _flag = true;\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n floatingActionButton: FloatingActionButton(\\n onPressed: () => setState(() => _flag = !_flag),\\n child: const Icon(Icons.animation),\\n ),\\n body: Center(\\n child: AnimatedOpacity(\\n opacity: _flag ? 1.0 : 0.2,\\n duration: const Duration(milliseconds: 500),\\n child: const Text(\'test AnimatedOpacity Fade In/Out\'),\\n )));\\n }\\n}\\n
\\nclass MyAnimatedPositioned extends StatefulWidget {\\n const MyAnimatedPositioned({super.key});\\n\\n @override\\n State<MyAnimatedPositioned> createState() => _MyAnimatedPositionedState();\\n}\\n\\nclass _MyAnimatedPositionedState extends State<MyAnimatedPositioned> {\\n bool _flag = true;\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n floatingActionButton: FloatingActionButton(\\n onPressed: () => setState(() => _flag = !_flag),\\n child: const Icon(Icons.animation),\\n ),\\n body: Center(\\n child: Stack(\\n children: [\\n const Positioned(\\n top: 100,\\n left: 100,\\n child: Text(\'固定位置文本\', style: TextStyle(fontSize: 20)),\\n ),\\n AnimatedPositioned(\\n duration: const Duration(milliseconds: 500),\\n top: _flag ? 100 : 300,\\n left: _flag ? 100 : 300,\\n child: Container(\\n width: 100,\\n height: 100,\\n color: Colors.blue,\\n ),\\n ),\\n const Positioned(\\n top: 300,\\n left: 300,\\n child: Text(\'固定位置文本222\', style: TextStyle(fontSize: 20)),\\n ),\\n ],\\n ),\\n ));\\n }\\n}\\n
\\nclass MyAnimatedSwitcher extends StatefulWidget {\\n const MyAnimatedSwitcher({super.key});\\n\\n @override\\n State<MyAnimatedSwitcher> createState() => _MyAnimatedSwitcherState();\\n}\\n\\nclass _MyAnimatedSwitcherState extends State<MyAnimatedSwitcher> {\\n int _flag = 0;\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n floatingActionButton: FloatingActionButton(\\n onPressed: () => setState(() => _flag++),\\n child: const Icon(Icons.animation),\\n ),\\n body: Center(\\n child: AnimatedSwitcher(\\n duration: const Duration(milliseconds: 500),\\n transitionBuilder: (Widget child, Animation<double> animation) {\\n return FadeTransition(\\n //淡入淡出\\n opacity: animation,\\n child: ScaleTransition(\\n //缩放\\n scale: animation,\\n child: child,\\n ),\\n );\\n },\\n child: Text(\\n \'test AnimatedSwitcher $_flag\',\\n //key: UniqueKey(), //生成唯一 key\\n key: ValueKey(_flag), //必须设置唯一 key\\n ),\\n ),\\n ));\\n }\\n}\\n
\\nclass MyTweenAnimationBuilder extends StatefulWidget {\\n const MyTweenAnimationBuilder({super.key});\\n\\n @override\\n State<MyTweenAnimationBuilder> createState() => _MyTweenAnimationBuilderState();\\n}\\n\\nclass _MyTweenAnimationBuilderState extends State<MyTweenAnimationBuilder> {\\n bool _flag = true;\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n floatingActionButton: FloatingActionButton(\\n onPressed: () => setState(() => _flag = !_flag),\\n child: const Icon(Icons.animation),\\n ),\\n body: Center(\\n child: TweenAnimationBuilder<double>(\\n tween: Tween<double>(begin: 10, end: 20),\\n duration: const Duration(seconds: 5),\\n builder: (context, value, child) {\\n return Text(\\"test TweenAnimationBuilder ${value.toInt()}\\",\\n style: TextStyle(fontSize: value));\\n },\\n ),\\n ));\\n }\\n}\\n
\\nclass MyImplicitlyAnimatedWidget extends StatefulWidget {\\n const MyImplicitlyAnimatedWidget({super.key});\\n\\n @override\\n State<MyImplicitlyAnimatedWidget> createState() =>\\n _MyImplicitlyAnimatedWidgetState();\\n}\\n\\nclass _MyImplicitlyAnimatedWidgetState\\n extends State<MyImplicitlyAnimatedWidget> {\\n double _width = 100;\\n double _height = 100;\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n floatingActionButton: FloatingActionButton(\\n onPressed: () => setState(() {\\n _width = _width > 40 ? 40 : 100;\\n _height = _height > 40 ? 40 : 100;\\n }),\\n child: const Icon(Icons.animation),\\n ),\\n body: Center(\\n child: CustomImplicitlyAnimatedWidget(\\n width: _width,\\n height: _height,\\n duration: const Duration(seconds: 1),\\n )),\\n );\\n }\\n}\\n\\n//自定义 ImplicitlyAnimatedWidget\\nclass CustomImplicitlyAnimatedWidget extends ImplicitlyAnimatedWidget {\\n const CustomImplicitlyAnimatedWidget(\\n {super.key,\\n required super.duration,\\n required this.width,\\n required this.height});\\n\\n final double width;\\n final double height;\\n\\n @override\\n AnimatedWidgetBaseState<CustomImplicitlyAnimatedWidget> createState() =>\\n _CustomAnimatedWidgetBaseState();\\n}\\n\\n//AnimatedWidgetBaseState 继承自 ImplicitlyAnimatedWidgetState\\nclass _CustomAnimatedWidgetBaseState\\n extends AnimatedWidgetBaseState<CustomImplicitlyAnimatedWidget> {\\n Tween<double>? _widthTween;\\n Tween<double>? _heightTween;\\n\\n @override\\n void forEachTween(TweenVisitor<dynamic> visitor) {\\n //visitor 的 tween 代表当前的 tween,第一次调用为 null\\n //初始化每一个可变化属性的 tween\\n //更新每一个可变化属性的 tween\\n //可同时管理多个属性,定义属性初始值及变化逻辑\\n _widthTween = visitor(_widthTween, widget.width, (dynamic value) => Tween<double>(begin: value as double)) as Tween<double>?;\\n\\n _heightTween = visitor(_heightTween, widget.height, (dynamic value) => Tween<double>(begin: value as double)) as Tween<double>?;\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Container(\\n width: _widthTween!.evaluate(animation),\\n height: _heightTween!.evaluate(animation),\\n color: Colors.blue,\\n );\\n }\\n}\\n
","description":"Flutter 动画之 Implicit 隐式动画 Implicit 隐式动画指的是通过修改组件的属性自动触发平滑过渡效果的动画实现方式(状态驱动),和显式动画(需要通过 AnimationController 手动控制)不同,隐式动画通过封装底层动画逻辑实现,无需手动控制管理动画\\n隐式动画需要指定动画的开始和结束状态,以及动画的持续时间等参数\\n隐式动画开发效率较高,但灵活性较低(依赖预设属性和默认动画)\\n动画一旦开始,只能从初始状态到目标状态,无法循环或反向播放\\n通过嵌套多个隐式动画组件可实现复合效果(比如同时调整位置和透明度)\\n//系统内置的隐式…","guid":"https://juejin.cn/post/7496876246840885288","author":"louisgeek","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-25T07:11:10.241Z","media":null,"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"游刃有余 —— Isolate 轻量化实战","url":"https://juejin.cn/post/7496897378203320320","content":"工作中在一个调用压缩功能场景中使用了 Isolate 缩短了整体的功能使用时延,未深入理解前曾有一段时间认为是 Isolate 加快了压缩效率;秉承知其然知其所以然原则,对 Isolate 进行了研究,并通过 demo 实验验证结论;
\\ndemo 链接在文末
\\n使用的是 flutter 技术栈,对于 Flutter isolate 的定义,豆包解答如下
\\n\\n\\n1. 定义
\\nIsolate 是 Dart 中独立的执行单元,拥有自己的 内存空间、事件循环(Event Loop) 和 消息队列(Message Queue) 。
\\n\\n
\\n- 每个 Isolate 之间 内存不共享,数据通过 消息传递(Message Passing) 通信,彻底避免竞态条件(Race Condition)。
\\n- 主线程(UI 线程)本身也是一个 Isolate,称为 UI Isolate,负责处理 UI 渲染和用户交互。
\\n2. 设计目标
\\n\\n
\\n- 安全并发:Dart 是单线程模型,通过 Isolate 实现多任务并行,避免共享状态导致的线程安全问题。
\\n- 隔离耗时操作:将 CPU 密集型或阻塞操作(如文件 IO、网络请求、复杂计算)放到后台 Isolate 中,防止阻塞 UI 线程。
\\n
Flutter 应用的核心渲染和事件处理都运行在单一的 UI 线程(主线程)上。当主线程被耗时任务占用时,UI 就会出现卡顿(jank):动画不流畅、按键响应迟缓、列表滚动拖影严重。Dart 语言提供了 Isolate 机制,帮助我们把耗时的 CPU 密集型任务移到后台线程去执行,从而保证主线程的流畅性。
\\n本文将从以下几个方面展开:
\\nIsolate 将实现
\\n独立内存空间
\\nDart 的每个 Isolate 拥有独立的内存堆,不与其他 Isolate 共享数据。数据只能通过消息(SendPort
/ReceivePort
)进行拷贝或传递,避免了锁和竞态条件的复杂性。
事件循环模型
\\n每个 Isolate 都有自己的事件队列和调度器,它们独立执行,不会相互阻塞。主线程与后台 Isolate 并行运行,通过异步消息通信协同工作。
启动成本与开销
\\n创建一个新的 Isolate 需要启动一个独立的 Dart 运行时、加载代码、分配内存,相比普通的异步操作(Future
/async
)具有更高的启动开销。因此,对短小、一次性计算使用 Isolate 并不划算,而更适合“重”计算或复用场景。
通过 demo 中实现两个页面对比使用 Isolate 带来哪些改变:
\\nColumn(\\n children: [\\n Text(\\n \'测试说明:\',\\n style: TextStyle(fontWeight: FontWeight.bold),\\n ),\\n SizedBox(height: 8),\\n Text(\\n \'1. 两个页面执行相同的计算任务(查找素数)\\\\n\'\\n \'2. 计算过程中观察动画流畅度\\\\n\'\\n \'3. 尝试点击\\"点击测试响应\\"按钮\\\\n\'\\n \'4. 尝试在蓝色区域滑动\\\\n\'\\n \'5. 对比两者UI响应差异\',\\n style: TextStyle(fontSize: 14),\\n ),\\n ],\\n ),\\n ),\\n const SizedBox(height: 40),\\n ElevatedButton(\\n style: ElevatedButton.styleFrom(\\n padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),\\n backgroundColor: Colors.red[100],\\n ),\\n onPressed: () {\\n Navigator.push(\\n context,\\n MaterialPageRoute(builder: (context) => const WithoutIsolatePage()),\\n );\\n },\\n child: const Text(\'不使用 Isolate (卡顿示例)\'),\\n ),\\n const SizedBox(height: 20),\\n ElevatedButton(\\n style: ElevatedButton.styleFrom(\\n padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),\\n backgroundColor: Colors.green[100],\\n ),\\n onPressed: () {\\n Navigator.push(\\n context,\\n MaterialPageRoute(builder: (context) => const WithIsolatePage()),\\n );\\n },\\n child: const Text(\'使用 Isolate (流畅示例)\'),\\n ),\\n ],\\n );\\n
\\n需要花费时间的运算函数
\\nList<int> _calculatePrimes(int max) {\\n List<int> primes = [];\\n // 埃拉托斯特尼筛法 (Sieve of Eratosthenes)\\n-------------\\n }\\n
\\n//直接主线程调用\\nElevatedButton(\\n onPressed: _isCalculating ? null : _calculatePrimes,\\n child: Text(_isCalculating ? \'计算中...\' : \'开始计算\'),\\n);\\n
\\n//启用 Isolate \\nawait Isolate.spawn(\\n _calculatePrimes,\\n _IsolateMessage(\\n sendPort: receivePort.sendPort,\\n iterations: iterations,\\n maxNumber: maxNumber,\\n ),\\n onError: errorPort.sendPort,\\n );\\n
\\n对比维度:
\\n结合以上简单理解 Isolate 属于 Flutter 的一个特有消息执行单元机制,下面将使用例子进行深入对比
\\n// 创建接收端口\\nfinal receivePort = ReceivePort();\\n\\n// 在主线程中启动 Isolate\\nawait Isolate.spawn(\\n _isolateEntryPoint,\\n _IsolateMessage(sendPort: receivePort.sendPort, ...),\\n);\\n\\n// 在主线程中监听后台消息\\nawait for (final message in receivePort) {\\n // 根据消息类型更新进度或完成状态\\n}\\n
\\nSendPort
传递给 Isolate,Isolate 使用它发送回消息。_ProgressMessage
和 _ResultMessage
来表达进度和完成状态,保持通信清晰有序。onError
参数将错误消息发送到主线程的另一条 ReceivePort
,便于集中捕获和日志记录。Isolate.spawn
或 compute()
,并注意管理生命周期与性能开销。CPU 密集型计算
\\n长时间后台任务
\\nI/O 与多线程并不冲突
\\n每个屏幕独立 Isolate(谨慎)
\\nisolate.kill()
释放。使用 compute
简化一-off 任务
compute()
帮助你快速在后台 Isolate 中执行一次性函数,底层也是基于 Isolate。适合小型、一次性计算。注意事项与性能权衡
\\nonError
和专门的消息通道来捕获并处理。demo 链接:github.com/lizy-coding…
\\nenvironment:\\n sdk: ^3.7.2\\n
","description":"工作中在一个调用压缩功能场景中使用了 Isolate 缩短了整体的功能使用时延,未深入理解前曾有一段时间认为是 Isolate 加快了压缩效率;秉承知其然知其所以然原则,对 Isolate 进行了研究,并通过 demo 实验验证结论; demo 链接在文末\\n\\n一、isolate 是什么\\n\\n使用的是 flutter 技术栈,对于 Flutter isolate 的定义,豆包解答如下\\n\\n1. 定义\\n\\nIsolate 是 Dart 中独立的执行单元,拥有自己的 内存空间、事件循环(Event Loop) 和 消息队列(Message Queue) 。\\n\\n每个…","guid":"https://juejin.cn/post/7496897378203320320","author":"人形打码机","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-25T05:46:30.965Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a285acaba18d4d5cb06e163d90200581~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Lq65b2i5omT56CB5py6:q75.awebp?rk3s=f64ab15b&x-expires=1746164790&x-signature=NPkXgfhGSX%2BSdUl2hX%2B9MxQG8jM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1d59992e9b994df19ae52b3f4806a1c7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Lq65b2i5omT56CB5py6:q75.awebp?rk3s=f64ab15b&x-expires=1746164790&x-signature=2W0xX2RGp3T%2FDPcx3%2FNeJzp5rqw%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["阅读","Flutter","Dart","GitHub"],"attachments":null,"extra":null,"language":null},{"title":"[Flutter 基础] - Flutter基础组件 - Text","url":"https://juejin.cn/post/7496803529827336233","content":"`Text` 是 `Flutter` 中用于显示静态文本的最基础组件,支持丰富的文本样式设置、对齐方式、溢出处理等功能。它与 TextField 不同,仅用于展示文本,不支持用户输入。","description":"`Text` 是 `Flutter` 中用于显示静态文本的最基础组件,支持丰富的文本样式设置、对齐方式、溢出处理等功能。它与 TextField 不同,仅用于展示文本,不支持用户输入。","guid":"https://juejin.cn/post/7496803529827336233","author":"RichardLai68","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-25T01:37:36.748Z","media":null,"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter中各类Controller的本质","url":"https://juejin.cn/post/7496522741941305378","content":"Flutter中提供了最基本的状态管理的方式—— setState ,当“状态”改变时,只需要调用一下该方法,界面即可刷新,展示最新的状态。","description":"Flutter中提供了最基本的状态管理的方式—— setState ,当“状态”改变时,只需要调用一下该方法,界面即可刷新,展示最新的状态。","guid":"https://juejin.cn/post/7496522741941305378","author":"DEVIL","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-24T09:27:20.053Z","media":null,"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"使用 flutter_flavorizr 在 Flutter (Android 和 iOS) 中轻松构建","url":"https://juejin.cn/post/7496712937601581092","content":"设置新的 Flutter 项目 Flutter flavorizr 更适合新的和干净的项目。让我们创建一个新的。 现在我们需要在 pubspec.yaml 文件中将 flutter_flavorizr","description":"设置新的 Flutter 项目 Flutter flavorizr 更适合新的和干净的项目。让我们创建一个新的。 现在我们需要在 pubspec.yaml 文件中将 flutter_flavorizr","guid":"https://juejin.cn/post/7496712937601581092","author":"iCoding","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-24T07:57:36.365Z","media":null,"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter UI 组件应用一:布局、交互、动画与弹窗","url":"https://juejin.cn/post/7496510495416729663","content":"以下是 Flutter UI 组件高级应用的详细教学,包含代码示例和关键概念说明: 一、高级布局技巧 1. 复杂布局组合 2. 响应式布局 二、事件处理进阶 1. 手势识别 2. 自定义手势冲突处理 ","description":"以下是 Flutter UI 组件高级应用的详细教学,包含代码示例和关键概念说明: 一、高级布局技巧 1. 复杂布局组合 2. 响应式布局 二、事件处理进阶 1. 手势识别 2. 自定义手势冲突处理","guid":"https://juejin.cn/post/7496510495416729663","author":"ak啊","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-24T07:19:46.940Z","media":null,"categories":["代码人生","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter与原生通讯的channel","url":"https://juejin.cn/post/7496484123248394277","content":"在 Flutter 里,和原生代码通讯的通道主要有三种,分别是MethodChannel
、EventChannel
和BasicMessageChannel
,下面为你详细介绍它们的适用场景和使用方法。
MethodChannel
适用于 Flutter 和原生之间进行方法调用,也就是 Flutter 调用原生方法,原生返回结果,或者原生调用 Flutter 方法,Flutter 返回结果。比如调用原生的相机、分享功能等。
\\n以下是一个简单的示例,展示了 Flutter 如何通过 MethodChannel
调用原生 iOS 的方法。
Flutter 端代码
\\ndart
\\nimport \'package:flutter/material.dart\';\\nimport \'package:flutter/services.dart\';\\n\\nvoid main() {\\n runApp(MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n static const platform = MethodChannel(\'samples.flutter.dev/battery\');\\n\\n Future<void> _getBatteryLevel() async {\\n String batteryLevel;\\n try {\\n final int result = await platform.invokeMethod(\'getBatteryLevel\');\\n batteryLevel = \'Battery level: $result%\';\\n } on PlatformException catch (e) {\\n batteryLevel = \\"Failed to get battery level: \'${e.message}\'.\\";\\n }\\n\\n print(batteryLevel);\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n home: Scaffold(\\n appBar: AppBar(\\n title: const Text(\'Battery Level\'),\\n ),\\n body: Center(\\n child: ElevatedButton(\\n onPressed: _getBatteryLevel,\\n child: const Text(\'Get Battery Level\'),\\n ),\\n ),\\n ),\\n );\\n }\\n}\\n
\\niOS 端代码(Swift)
\\nswift
\\nimport Flutter\\nimport UIKit\\n\\n@UIApplicationMain\\n@objc class AppDelegate: FlutterAppDelegate {\\n override func application(\\n _ application: UIApplication,\\n didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?\\n ) -> Bool {\\n let controller : FlutterViewController = window?.rootViewController as! FlutterViewController\\n let batteryChannel = FlutterMethodChannel(name: \\"samples.flutter.dev/battery\\", binaryMessenger: controller.binaryMessenger)\\n batteryChannel.setMethodCallHandler {\\n (call: FlutterMethodCall, result: @escaping FlutterResult) in\\n if call.method == \\"getBatteryLevel\\" {\\n self.receiveBatteryLevel(result: result)\\n } else {\\n result(FlutterMethodNotImplemented)\\n }\\n }\\n\\n GeneratedPluginRegistrant.register(with: self)\\n return super.application(application, didFinishLaunchingWithOptions: launchOptions)\\n }\\n\\n private func receiveBatteryLevel(result: @escaping FlutterResult) {\\n UIDevice.current.isBatteryMonitoringEnabled = true\\n if UIDevice.current.batteryState == .unknown {\\n result(FlutterError(code: \\"UNAVAILABLE\\",\\n message: \\"Battery info unavailable\\",\\n details: nil))\\n } else {\\n result(Int(UIDevice.current.batteryLevel * 100))\\n }\\n }\\n}\\n
\\nEventChannel
适用于原生向 Flutter 发送连续的事件流,比如传感器数据、网络状态变化等。
\\n以下是一个简单的示例,展示了 iOS 端如何通过 EventChannel
向 Flutter 发送模拟的传感器数据。
Flutter 端代码
\\ndart
\\nimport \'package:flutter/material.dart\';\\nimport \'package:flutter/services.dart\';\\n\\nvoid main() {\\n runApp(MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n static const stream = EventChannel(\'samples.flutter.dev/sensor\');\\n\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n home: Scaffold(\\n appBar: AppBar(\\n title: const Text(\'Sensor Data\'),\\n ),\\n body: StreamBuilder(\\n stream: stream.receiveBroadcastStream(),\\n builder: (context, snapshot) {\\n if (snapshot.hasError) {\\n return Text(\'Error: ${snapshot.error}\');\\n }\\n if (snapshot.hasData) {\\n return Text(\'Sensor data: ${snapshot.data}\');\\n }\\n return const Text(\'Waiting for data...\');\\n },\\n ),\\n ),\\n );\\n }\\n}\\n
\\niOS 端代码(Swift)
\\nswift
\\nimport Flutter\\nimport UIKit\\n\\n@UIApplicationMain\\n@objc class AppDelegate: FlutterAppDelegate {\\n override func application(\\n _ application: UIApplication,\\n didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?\\n ) -> Bool {\\n let controller : FlutterViewController = window?.rootViewController as! FlutterViewController\\n let eventChannel = FlutterEventChannel(name: \\"samples.flutter.dev/sensor\\", binaryMessenger: controller.binaryMessenger)\\n eventChannel.setStreamHandler(SensorStreamHandler())\\n\\n GeneratedPluginRegistrant.register(with: self)\\n return super.application(application, didFinishLaunchingWithOptions: launchOptions)\\n }\\n}\\n\\nclass SensorStreamHandler: NSObject, FlutterStreamHandler {\\n private var timer: Timer?\\n private var eventSink: FlutterEventSink?\\n private var counter = 0\\n\\n func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {\\n eventSink = events\\n timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in\\n guard let self = self else { return }\\n if self.counter < 10 {\\n self.eventSink?(self.counter)\\n self.counter += 1\\n } else {\\n self.eventSink?(FlutterEndOfEventStream)\\n self.timer?.invalidate()\\n }\\n }\\n return nil\\n }\\n\\n func onCancel(withArguments arguments: Any?) -> FlutterError? {\\n timer?.invalidate()\\n eventSink = nil\\n return nil\\n }\\n}\\n
\\nBasicMessageChannel
适用于 Flutter 和原生之间进行简单的消息传递,比如传递字符串、JSON 数据等。
\\n以下是一个简单的示例,展示了 Flutter 和 iOS 之间通过 BasicMessageChannel
传递字符串消息。
Flutter 端代码
\\ndart
\\nimport \'package:flutter/material.dart\';\\nimport \'package:flutter/services.dart\';\\n\\nvoid main() {\\n runApp(MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n static const messageChannel = BasicMessageChannel<String>(\'samples.flutter.dev/message\', StringCodec());\\n\\n Future<void> _sendMessage() async {\\n try {\\n final String reply = await messageChannel.send(\'Hello from Flutter!\');\\n print(\'Received reply: $reply\');\\n } catch (e) {\\n print(\'Error sending message: $e\');\\n }\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n home: Scaffold(\\n appBar: AppBar(\\n title: const Text(\'Message Channel\'),\\n ),\\n body: Center(\\n child: ElevatedButton(\\n onPressed: _sendMessage,\\n child: const Text(\'Send Message\'),\\n ),\\n ),\\n ),\\n );\\n }\\n}\\n
\\niOS 端代码(Swift)
\\nswift
\\nimport Flutter\\nimport UIKit\\n\\n@UIApplicationMain\\n@objc class AppDelegate: FlutterAppDelegate {\\n override func application(\\n _ application: UIApplication,\\n didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?\\n ) -> Bool {\\n let controller : FlutterViewController = window?.rootViewController as! FlutterViewController\\n let messageChannel = FlutterBasicMessageChannel(name: \\"samples.flutter.dev/message\\", binaryMessenger: controller.binaryMessenger, codec: FlutterStringCodec.sharedInstance())\\n messageChannel.setMessageHandler { (message: Any?, reply: @escaping (Any?) -> Void) in\\n if let messageString = message as? String {\\n print(\\"Received message: (messageString)\\")\\n reply(\\"Hello from iOS!\\")\\n } else {\\n reply(nil)\\n }\\n }\\n\\n GeneratedPluginRegistrant.register(with: self)\\n return super.application(application, didFinishLaunchingWithOptions: launchOptions)\\n }\\n}\\n
\\n这些示例展示了三种通道在 Flutter 和 iOS 之间的基本使用方法。
","description":"在 Flutter 里,和原生代码通讯的通道主要有三种,分别是MethodChannel、EventChannel和BasicMessageChannel,下面为你详细介绍它们的适用场景和使用方法。 1. MethodChannel\\n适用场景\\n\\n适用于 Flutter 和原生之间进行方法调用,也就是 Flutter 调用原生方法,原生返回结果,或者原生调用 Flutter 方法,Flutter 返回结果。比如调用原生的相机、分享功能等。\\n\\n使用方法\\n\\n以下是一个简单的示例,展示了 Flutter 如何通过 MethodChannel 调用原生 iOS 的方法。…","guid":"https://juejin.cn/post/7496484123248394277","author":"Lafar","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-24T07:18:28.878Z","media":[{"url":"https://juejin.cn/","type":"photo"}],"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"还在用灰秃秃的图片占位符?快来试试这款神器","url":"https://juejin.cn/post/7496385494097264650","content":"还在用灰秃秃的图片占位符?快来试试这款神器 每次你加载设计师精心设计的界面,结果因为图片都没加载出来,满屏都是灰秃秃的占位图,这是不是让设计师很抓狂?当你想往数据里塞小缩略图当占位图来解决这个问题时,","description":"还在用灰秃秃的图片占位符?快来试试这款神器 每次你加载设计师精心设计的界面,结果因为图片都没加载出来,满屏都是灰秃秃的占位图,这是不是让设计师很抓狂?当你想往数据里塞小缩略图当占位图来解决这个问题时,","guid":"https://juejin.cn/post/7496385494097264650","author":"JarvanMo","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-24T02:03:25.430Z","media":null,"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"【Flutter 状态管理 - 陆】 | Listenable:响应式状态管理的核心引擎 🚀","url":"https://juejin.cn/post/7496528481842741283","content":"Listenable 作为观察者模式的标杆实现,它从底层打通了状态驱动式开发的任督二脉, Listenable 都以优雅的解耦设计、高效的更新策略,让我们真正体验到「数据变,UI 随行」的丝滑编程范式","description":"Listenable 作为观察者模式的标杆实现,它从底层打通了状态驱动式开发的任督二脉, Listenable 都以优雅的解耦设计、高效的更新策略,让我们真正体验到「数据变,UI 随行」的丝滑编程范式","guid":"https://juejin.cn/post/7496528481842741283","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-24T01:28:02.881Z","media":null,"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"[Flutter 基础] - App的入口main.dart","url":"https://juejin.cn/post/7496387408436674579","content":"在 Flutter 项目中,`main.dart` 可以说是最重要的一部分,文件是应用程序的入口文件,所有的 Flutter 应用程序都从 `main()` 函数开始执行。对于一个初学者来说,这个文件","description":"在 Flutter 项目中,`main.dart` 可以说是最重要的一部分,文件是应用程序的入口文件,所有的 Flutter 应用程序都从 `main()` 函数开始执行。对于一个初学者来说,这个文件","guid":"https://juejin.cn/post/7496387408436674579","author":"RichardLai68","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-23T23:55:42.219Z","media":null,"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 在全新 Platform 和 UI 线程合并后,出现了什么大坑和变化?","url":"https://juejin.cn/post/7496397558359162934","content":"在两个月前,我们就聊过 3.29 上《Platform 和 UI 线程合并》的具体原因和实现方式,而事实上 Platform 和 UI 线程合并,确实为后续原生语言和 Dart 的直接同步调用打了一个","description":"在两个月前,我们就聊过 3.29 上《Platform 和 UI 线程合并》的具体原因和实现方式,而事实上 Platform 和 UI 线程合并,确实为后续原生语言和 Dart 的直接同步调用打了一个","guid":"https://juejin.cn/post/7496397558359162934","author":"恋猫de小郭","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-23T22:31:59.131Z","media":null,"categories":["Android","Flutter","前端"],"attachments":null,"extra":null,"language":null},{"title":"差生文具多","url":"https://juejin.cn/post/7496341504830013467","content":"过度依赖三方库本质上是技术深度不足的表现。 当我们将每个三方库的引入都视为一次架构决策,而非简单的CTRL+C/V时,项目自然会呈现出优雅健壮的特质。","description":"过度依赖三方库本质上是技术深度不足的表现。 当我们将每个三方库的引入都视为一次架构决策,而非简单的CTRL+C/V时,项目自然会呈现出优雅健壮的特质。","guid":"https://juejin.cn/post/7496341504830013467","author":"程序员老刘","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-23T09:23:01.451Z","media":null,"categories":["开发工具","Flutter","客户端"],"attachments":null,"extra":null,"language":null},{"title":"window配置Flutter开发环境","url":"https://juejin.cn/post/7496341504829521947","content":"docs.flutter.dev/get-started…\\n选择稳定版本即可;看项目是用flutter2,还是flutter3版本来选择了
\\n我目前用的是:3.22版本的sdk,因为项目是基于flutter3的
\\na、在Path变量中配置flutter路径:
\\nb、配置FLUTTER_STORAGE_BASE_URL环境变量:storage.flutter-io.cn
\\nc、配置PUB_HOSTED_URL环境变量:pub.flutter-io.cn
\\n注:配置完成可以执行 flutter --version查看版本,flutter doctor验证配置是否完成
\\n配置JAVA_HOME环境变量:
\\n官网地址:android-sdk.en.softonic.com/download
\\na、配置ANDROID_HOME:比如地址(F:\\\\Android\\\\SDK)
\\nb、在Path变量中添加2行,比如:
\\n官网地址:developer.android.google.cn/studio?hl=n…
\\n以上步骤完成打开Flutter项目,匹配对应flutter版本,执行flutter doctor检测项目
\\n最后:目前还没mac的电脑,后期有了,补全一下mac配置的过程记录~;一起学习,加油!!!
","description":"1、下载Flutter sdk版本: docs.flutter.dev/get-started… 选择稳定版本即可;看项目是用flutter2,还是flutter3版本来选择了\\n\\n我目前用的是:3.22版本的sdk,因为项目是基于flutter3的\\n\\n2、配置flutter相关环境变量\\n\\na、在Path变量中配置flutter路径:\\n\\nb、配置FLUTTER_STORAGE_BASE_URL环境变量:storage.flutter-io.cn\\n\\nc、配置PUB_HOSTED_URL环境变量:pub.flutter-io.cn\\n\\n注:配置完成可以执行…","guid":"https://juejin.cn/post/7496341504829521947","author":"在广东捡破烂的吴彦祖","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-23T08:02:39.448Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d7affeed4e224c278093cbc51702fd1c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyo5bm_5Lic5o2h56C054OC55qE5ZC05b2m56WW:q75.awebp?rk3s=f64ab15b&x-expires=1746000159&x-signature=Abt332D%2BR41pMvCdmo%2FnP2UqEI8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0dcd5d7953d749219ed4786a68bb05ef~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyo5bm_5Lic5o2h56C054OC55qE5ZC05b2m56WW:q75.awebp?rk3s=f64ab15b&x-expires=1746000159&x-signature=JXvbXJs78ZPMTBC8DVxN5G62jr0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5f831189ac344daab2540516fa9caf96~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyo5bm_5Lic5o2h56C054OC55qE5ZC05b2m56WW:q75.awebp?rk3s=f64ab15b&x-expires=1746000159&x-signature=6ref6xvtQ7BIEWoaba4x5uQI%2FcU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/474a6afa7dc44d63a2cc25daa6e9c004~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyo5bm_5Lic5o2h56C054OC55qE5ZC05b2m56WW:q75.awebp?rk3s=f64ab15b&x-expires=1746000159&x-signature=CI7Xg6Dq2b75XqUraz9A4qUHBac%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/77f3f5e8b9374519aff5d1489975a934~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyo5bm_5Lic5o2h56C054OC55qE5ZC05b2m56WW:q75.awebp?rk3s=f64ab15b&x-expires=1746000159&x-signature=A7AqZmJyHwk5waw5fDWRxoNBCxs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/189fd46574cb4fdab7e3c467ee7c2210~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyo5bm_5Lic5o2h56C054OC55qE5ZC05b2m56WW:q75.awebp?rk3s=f64ab15b&x-expires=1746000159&x-signature=z77d9023%2B%2F6ETpKt3QAaKIFJFAE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/92624fcc8106427990afdf9bfd518683~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyo5bm_5Lic5o2h56C054OC55qE5ZC05b2m56WW:q75.awebp?rk3s=f64ab15b&x-expires=1746000159&x-signature=EYCCb29kIywLbL4odMrvwXfX1Jw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a7a82a2a5de7436db30bf2111d645d07~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyo5bm_5Lic5o2h56C054OC55qE5ZC05b2m56WW:q75.awebp?rk3s=f64ab15b&x-expires=1746000159&x-signature=b7cQhMzorR3KAgkZdXKyaUbQwDY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/535deccf4eb94821b438a69d32181dac~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyo5bm_5Lic5o2h56C054OC55qE5ZC05b2m56WW:q75.awebp?rk3s=f64ab15b&x-expires=1746000159&x-signature=wEc0%2F663Boa5bdnAqmrvCuKXnCs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7713f67a02534244aaba25fe79da3926~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyo5bm_5Lic5o2h56C054OC55qE5ZC05b2m56WW:q75.awebp?rk3s=f64ab15b&x-expires=1746000159&x-signature=U8DtEeE9u3jFIY%2BRrN0gZ2hXPmo%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter UI 组件基础","url":"https://juejin.cn/post/7496341220123541555","content":"一、基础 UI 组件介绍 1. 文本组件(Text) 用于显示文字,支持字体样式(颜色、大小、字重等)。 关键属性:style 可设置 TextStyle,支持字体、颜色、阴影等。 2. 按钮组件(E","description":"一、基础 UI 组件介绍 1. 文本组件(Text) 用于显示文字,支持字体样式(颜色、大小、字重等)。 关键属性:style 可设置 TextStyle,支持字体、颜色、阴影等。 2. 按钮组件(E","guid":"https://juejin.cn/post/7496341220123541555","author":"ak啊","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-23T07:18:30.135Z","media":null,"categories":["代码人生","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter的屏幕适配","url":"https://juejin.cn/post/7496345825188036648","content":"使用Flutter开发商用项目,则你必不可少要考虑的一个东西,那就是屏幕适配。回顾一下Android的屏幕适配方案,有以下考虑。
\\n那么Flutter也可以借鉴Android原生的思路。
\\n如果有设计稿的情况下,适配就变得简单了,建议直接用 flutter_screenutil
,方便快捷。
首先在pubspec.yaml文件中安装依赖。
\\ndependencies:\\n flutter_screenutil: ^5.6.0\\n
\\n接下来在main.dart中初始化尺寸。
\\nimport \'package:flutter_screenutil/flutter_screenutil.dart\';\\n\\nvoid main() {\\n runApp(const MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n const MyApp({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n return ScreenUtilInit(\\n designSize: const Size(375, 812), // 你的设计稿尺寸,例如 iPhone X\\n minTextAdapt: true,\\n splitScreenMode: true,\\n builder: (context, child) {\\n return MaterialApp(\\n home: const HomePage(),\\n );\\n },\\n );\\n }\\n}\\n
\\n最后我们只要按照它的使用方式使用就行了。即所有尺寸都加 .w
、.h
、.r
、.sp
等。比如 设计稿是375x812,宽度标的10,那么你就写10.w
,高度标的15,那么你就写15.h
,对于圆角半径,你就写比如5.r
。sp就跟Android原生类似了,不做过多赘述。
对于r的计算方式,刚接触的可能会想当然的认为是宽和高的平均值。看了源码发现实际上不是,它是取的宽度和高度的较小值确认缩放比例的。通常用于半径、边框宽度这种不太区分是宽还是高的场景。
\\n也可以自己动态计算缩放的比例,这种方式麻烦一些,你只需要知道有这种方式即可。
\\nfinal screenWidth = MediaQuery.of(context).size.width;\\nfinal screenHeight = MediaQuery.of(context).size.height;\\ndouble scaleWidth(double width) => width * screenWidth / 375.0;\\n
\\n在Android中,如果是自己计算,通常还需要考虑系统状态栏和导航栏对布局的影响。
\\n// 获取系统状态栏的高度\\ndouble statusBarHeight = MediaQuery.of(context).padding.top;\\n// 获取系统导航栏的高度\\ndouble navigationBarHeight = MediaQuery.of(context).padding.bottom;\\n
\\n如果你用的是 Scaffold
,并且开启了 resizeToAvoidBottomInset: true
,它会在键盘弹出时,自动调整布局,TextField
会向上推移,避免被键盘遮挡。并且默认它会占据状态栏进行布局,比如你不想做沉浸式,你需要使用SafeArea
组件包裹,这样你的布局就会避开状态栏了。
SafeArea(\\n child: Scaffold(\\n appBar: AppBar(title: Text(\\"My App\\")),\\n body: Column(\\n children: [\\n Text(\'This is a safe area\'),\\n // 更多内容\\n ],\\n ),\\n ),\\n)\\n
\\n使用SystemChrome.setPreferredOrientations
可以设置布局的屏幕方向。例如:
SystemChrome.setPreferredOrientations([\\n DeviceOrientation.portraitUp,\\n DeviceOrientation.landscapeLeft,\\n // ...\\n]);\\n
\\nSystemChrome.setPreferredOrientations([\\n DeviceOrientation.portraitUp,\\n]);\\n
\\nSystemChrome.setPreferredOrientations([\\n DeviceOrientation.landscapeLeft,\\n]);\\n
\\nSystemChrome.setPreferredOrientations([\\n DeviceOrientation.portraitUp,\\n DeviceOrientation.landscapeLeft,\\n DeviceOrientation.landscapeRight,\\n]);\\n
\\n如果你在main函数里面设置,你需要加async
关键字,并在调用SystemChrome.setPreferredOrientations()
之前先调用WidgetsFlutterBinding.ensureInitialized()
以确保初始化完成。
void main() async {\\n WidgetsFlutterBinding.ensureInitialized();\\n await SystemChrome.setPreferredOrientations([\\n DeviceOrientation.portraitUp, // 只竖屏\\n ]);\\n runApp(MyApp());\\n}\\n
\\n否则你会遇到以下错误。
\\n\\n\\nBinding has not yet been initialized
\\n
这个功能在Android6.0开始支持,请参见公主。哦,不对,请参考我的开源库Dora的源代码github.com/dora4/dora/… 。
\\nvoid main() async {\\n WidgetsFlutterBinding.ensureInitialized()\\n // 设置状态栏样式(亮色背景,黑色文字图标)\\n SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(\\n statusBarColor: Colors.transparent, // 状态栏透明(可选)\\n statusBarIconBrightness: Brightness.dark, // Android直接设置黑色文字图标\\n statusBarBrightness: Brightness.light, // iOS通过设置背景亮色来设置黑色文字图标\\n ));\\n // 其他的设置...\\n runApp(MyApp());\\n}\\n
\\nreturn AnnotatedRegion<SystemUiOverlayStyle>(\\n value: SystemUiOverlayStyle.dark, // 或 light\\n child: Scaffold(\\n backgroundColor: Colors.white,\\n body: YourPage(),\\n ),\\n);\\n
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n样式 | 效果 |
---|---|
SystemUiOverlayStyle.dark | 状态栏图标/文字为 黑色(适合浅色背景) |
SystemUiOverlayStyle.light | 状态栏图标/文字为 白色(适合深色背景) |
该篇文章的核心是去介绍一下Flutter项目目录结构以及主要文件和文件夹,特别是pubspec.yaml文件。担心有些小伙伴再最开始学习flutter的时候会不熟悉Android studio。所以就啰嗦几句简单介绍一下如何通过Android studio来创建一个flutter项目。如果不需要的可以直接跳过这部分就好了。
\\n通过Android studio去创建Flutter项目。\\n\\n选中Flutter项目,SDK地址一般都是默认你自己配好的,如果没有就通过文件选择,选择flutter解压后的地址。\\n
\\n点击下一步:\\n
\\n输入完内容之后,点击创建。则会生成一个这样的项目目录结构:\\n
lib是flutter项目的核心文件夹,所有的Dart相关代码都是这个文件夹下面的\\nMain.dart 是项目入口,通过这个文件中的main方法启动App
\\nvoid main() {\\n runApp(const MyApp());\\n}\\n
\\ntest文件夹是和单元测试相关的,学flutter阶段的话,可以不用去考虑这个文件夹下的内容。
\\n存放原生平台代码,用于定制化配置(如图标、权限、签名)。
\\n如果需要修改App图标:则替换android/app/src/main/res/mipmap*
文件中对应的icon。
<application\\n android:label=\\"flutter_demo\\"\\n android:name=\\"${applicationName}\\"\\n android:icon=\\"@mipmap/ic_launcher\\"> <!-- App图标 --\x3e\\n</manifest>\\n
\\n如果需要添加权限:则更改 android/app/src/main/AndroidManifest.xml文件
\\n<manifest xmlns:android=\\"http://schemas.android.com/apk/res/android\\">\\n <application\\n android:label=\\"flutter_demo\\"\\n android:name=\\"${applicationName}\\"\\n android:icon=\\"@mipmap/ic_launcher\\">\\n <!-- 添加权限 --\x3e\\n <uses-permission android:name=\\"android.permission.INTERNET\\" />\\n ...\\n</manifest>\\n
\\nFlutter项目很重要的一个配置文件,包括依赖管理,资源,主题等。\\n依赖管理:
\\n例:
\\nname: flutter_demo # 项目的名称\\ndescription: \\"A new Flutter project.\\" #项目的描述\\npublish_to: \'none\' # 这个默认是none值,是如果你的个人项目想要发不到pub.dev仓库,需要删除这行\\nversion: 1.0.0+1 # 项目的版本号,格式为:x.y.z+[build number]\\ndependencies: # 依赖管理模块\\n flutter: any\\n http: ^1.0.0 # 网络请求库\\n provider: ^6.0.0 # 状态管理库\\n \\n my_package: #允许添加github上的一些库\\n git:\\n url: https://github.com/username/repo.git # 这个也可以是一下公司内部版本库地址\\n ref: main\\n path: packages/my_package # 可选子目录\\n my_local_package: \\n path: ../my_local_package # 还可以设置当前项目的模块作为依赖\\n \\ndev_dependencies: #只有在开发阶段才会拉取的依赖\\n flutter_test:\\n sdk: flutter\\n mockito: ^5.0.0 # 单元测试辅助库\\n\\ndependency_overrides: # 需要覆盖的依赖\\n http:\\n git:\\n url: https://github.com/your-repo/http.git\\n ref: dev\\n\\nflutter:\\n uses-material-design: true # 开启使用Material的设计,在Material的组件中可是与其相关的一些icon\\n fonts: # 可以设置自己的字体, 支持配置多套字体\\n - family: MyFont #字体的命名\\n fonts:\\n - asset: assets/fonts/MyFont-Regular.ttf # 字体的存放路径\\n \\n assets: # 资源文件路径配置\\n - assets/images/ #路径,文件名字可以自定义\\n - assets/configs/ #路径,文件名字可以自定义\\n
\\nname: 项目的名称
\\ndescription: 项目简介
\\nversion: 项目的版本号
\\nx.y.z+[build number]
x.y.z
:语义化版本(主版本.次版本.修订)。build number
:构建号(如 1.0.0+1
)。publish_to:指定包的发布位置,防止意外发布到 pub.dev
none
:不发布到任何位置(默认)。pub.dev
:发布到公共仓库。https://your-private-repo.com
dependencies:依赖管理,支持pub.dev官方库版本依赖,github等公共管理库依赖,当前项目子模块等依赖
\\nPub.dev库依赖。
\\n provider: ^6.0.0 # 状态管理库\\n
\\nGithub公共库依赖
\\n my_package: #允许添加github上的一些库\\n git:\\n url: https://github.com/username/repo.git # 这个也可以是一下公司内部版本库地址\\n ref: main\\n path: packages/my_package # 可选子目录\\n
\\n当前项目子模块依赖
\\n my_local_package: \\n path: ../my_local_package # 还可以设置当前项目的模块作为依赖\\n
\\ndev_dependencies : 只有在开发阶段才会拉取的依赖。使用规则和dependencies一致
\\ndependency_overrides: 项目都会有很多依赖,比如多个项目依赖了同一个插件,但是版本不一样,可以去覆盖,支持的规则和dependencies一致。
\\nuses-material-design: 默认是true,开启material设计的支持。开启了之后可以直接去使用一些material风格的icon之类的。
\\nfonts: 可以设置项目自定义字体,支持配置多种字体。具体配置可以参考:
\\n fonts: # 可以设置自己的字体, 支持配置多套字体\\n - family: MyFont #字体的命名\\n fonts:\\n - asset: assets/fonts/MyFont-Regular.ttf # 字体的存放路径\\n
\\nassets: 资源文件的配置路径,项目刚创建的时候可能没有这个文件夹,需要手动创建,在项目的根目录下。
\\n assets: # 资源文件路径配置\\n - assets/images/ #路径,文件名字可以自定义\\n - assets/configs/ #路径,文件名字可以自定义\\n
\\n执行flutter pub get命令的时候根据pubspec.yaml中配置的三块dependences生成的,主要是用于锁定每个依赖的版本号,需要提交到版本控制。
\\n以上就是Flutter项目目录结构的一个简要说明。感谢您耐心看到最后,如果不对的地方欢迎指正。
","description":"该篇文章的核心是去介绍一下Flutter项目目录结构以及主要文件和文件夹,特别是pubspec.yaml文件。担心有些小伙伴再最开始学习flutter的时候会不熟悉Android studio。所以就啰嗦几句简单介绍一下如何通过Android studio来创建一个flutter项目。如果不需要的可以直接跳过这部分就好了。 创建Flutter项目\\n\\n通过Android studio去创建Flutter项目。 选中Flutter项目,SDK地址一般都是默认你自己配好的,如果没有就通过文件选择,选择flutter解压后的地址。 点击下一步: 输入完内容之…","guid":"https://juejin.cn/post/7496047972301520908","author":"RichardLai68","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-23T00:45:38.380Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3273050a6c164cb6bca7e162940a8817~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgUmljaGFyZExhaTY4:q75.awebp?rk3s=f64ab15b&x-expires=1745979290&x-signature=YRrJaeZEtpg4v7XpUQh4fzDRLnI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1ef933d3c91d45499543ec85c0cbe03b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgUmljaGFyZExhaTY4:q75.awebp?rk3s=f64ab15b&x-expires=1745979290&x-signature=6TCVRuBLwF5V0gNLIFPvqakPMOk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4b7de09b206046ed89f86e755ff625c2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgUmljaGFyZExhaTY4:q75.awebp?rk3s=f64ab15b&x-expires=1745979290&x-signature=QM8L5W%2F3NoWeLzbcyODp%2Bfjmmfg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d06ce9c503fb4309819b1523cd775c79~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgUmljaGFyZExhaTY4:q75.awebp?rk3s=f64ab15b&x-expires=1745979290&x-signature=uMq0b9XVLNVdfGPZMR1OntNoghY%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 动画之 Explicit 显式动画","url":"https://juejin.cn/post/7496029800742977563","content":"class ExplicitAnimationWidget extends StatefulWidget {\\n const ExplicitAnimationWidget({super.key});\\n\\n @override\\n State<ExplicitAnimationWidget> createState() =>\\n _ExplicitAnimationWidgetState();\\n}\\n\\n//SingleTickerProviderStateMixin 实现了 TickerProvider 抽象类,通过混入让 State 成为一个 TickerProvider\\nclass _ExplicitAnimationWidgetState extends State<ExplicitAnimationWidget>\\n with SingleTickerProviderStateMixin {\\n late AnimationController _controller;\\n late Animation<double> _animation;\\n\\n @override\\n void initState() {\\n super.initState();\\n //1 初始化动画控制器\\n _controller = AnimationController(\\n duration: const Duration(seconds: 2),\\n vsync: this, //TickerProvider,将 State 传入\\n );\\n //2 定义动画值的变化范围\\n //Animation<Color?> _colorAnimation = ColorTween(begin: Colors.red, end: Colors.green).animate(_controller);\\n _animation = Tween<double>(begin: 50.0, end: 200.0).animate(_controller);\\n\\n //3 监听动画值的变化\\n _animation.addListener(() {\\n setState(() {}); //触发 UI 更新\\n });\\n _animation.addStatusListener((status) {\\n //监听状态变化\\n //if (_controller.isCompleted) {\\n if (status == AnimationStatus.completed) {\\n //动画完成时触发\\n _controller.reverse(); //反向播放\\n }\\n });\\n \\n _startAnimation();\\n }\\n\\n //4 启动和控制动画\\n void _startAnimation() {\\n _controller.forward(); //正向播放\\n //_controller.repeat(); //循环播放\\n }\\n\\n @override\\n void dispose() {\\n //释放资源\\n _controller.dispose();\\n super.dispose();\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: const Text(\'显式动画示例\'),\\n ),\\n body: Center(\\n child: SizedBox(\\n width: _animation.value, //动画元素的属性改变\\n height: _animation.value, //动画元素的属性改变\\n child: const FlutterLogo(),\\n ),\\n ),\\n );\\n }\\n}\\n
\\nclass MyAnimatedWidget extends StatefulWidget {\\n const MyAnimatedWidget({super.key});\\n\\n @override\\n State<MyAnimatedWidget> createState() => _MyAnimatedWidgetState();\\n}\\n\\n//SingleTickerProviderStateMixin 实现了 TickerProvider 抽象类,通过混入让 State 成为一个 TickerProvider\\nclass _MyAnimatedWidgetState extends State<MyAnimatedWidget>\\n with SingleTickerProviderStateMixin {\\n late AnimationController _controller;\\n late Animation<double> _animation;\\n\\n @override\\n void initState() {\\n super.initState();\\n //1 初始化动画控制器\\n _controller = AnimationController(\\n duration: const Duration(seconds: 2),\\n vsync: this, //TickerProvider,将 State 传入\\n );\\n //2 定义动画值的变化范围\\n //Animation<Color?> _colorAnimation = ColorTween(begin: Colors.red, end: Colors.green).animate(_controller);\\n _animation = Tween<double>(begin: 50.0, end: 200.0).animate(_controller);\\n\\n _animation.addStatusListener((status) {\\n //监听状态变化\\n //if (_controller.isCompleted) {\\n if (status == AnimationStatus.completed) {\\n //动画完成时触发\\n _controller.reverse(); //反向播放\\n }\\n });\\n\\n _startAnimation();\\n }\\n\\n //3 启动和控制动画\\n void _startAnimation() {\\n _controller.forward(); //正向播放\\n //_controller.repeat(); //循环播放\\n }\\n\\n @override\\n void dispose() {\\n //释放资源\\n _controller.dispose();\\n super.dispose();\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: const Text(\'AnimatedWidget 示例\'),\\n ),\\n body: Center(\\n child: CustomAnimatedWidget(animation: _animation),\\n ),\\n );\\n }\\n}\\n\\n//自定义 AnimatedWidget\\nclass CustomAnimatedWidget extends AnimatedWidget {\\n const CustomAnimatedWidget({super.key, required Animation<double> animation})\\n : super(listenable: animation);\\n\\n @override\\n Widget build(BuildContext context) {\\n final animation = listenable as Animation<double>;\\n return SizedBox(\\n width: animation.value, //动画元素的属性改变\\n height: animation.value, //动画元素的属性改变\\n child: const FlutterLogo(),\\n );\\n }\\n}\\n
\\n//SlideTransition 继承自 AnimatedWidget\\n//ScaleTransition 继承自 MatrixTransition,而 MatrixTransition 又继承自 AnimatedWidget\\n//RotationTransition 继承自 MatrixTransition,而 MatrixTransition 又继承自 AnimatedWidget\\n//FadeTransition 继承自 SingleChildRenderObjectWidget,而 SingleChildRenderObjectWidget 又继承自 RenderObjectWidget\\n//\\n//滑动动画,通过 position 控制位置\\nSlideTransition\\n//缩放动画,通过 scale 控制缩放\\nScaleTransition\\n//旋转动画,通过 turns 控制旋转次数\\nRotationTransition\\n//透明度动画(比如实现淡入淡出效果),通过 opacity 控制透明度\\nFadeTransition\\n//...\\n
\\n//SingleTickerProviderStateMixin 实现了 TickerProvider 抽象类,通过混入让 State 成为一个 TickerProvider\\nclass _ScaleTransitionWidgetState extends State<ScaleTransitionWidget>\\n with SingleTickerProviderStateMixin {\\n late AnimationController _controller;\\n late Animation<double> _animation;\\n\\n @override\\n void initState() {\\n super.initState();\\n //1 初始化动画控制器\\n _controller = AnimationController(\\n duration: const Duration(seconds: 2),\\n vsync: this, //TickerProvider,将 State 传入\\n );\\n //2 定义动画值的变化范围\\n _animation = Tween<double>(begin: 0.4, end: 1.0).animate(_controller);\\n\\n _animation.addStatusListener((status) {\\n //监听状态变化\\n //if (_controller.isCompleted) {\\n if (status == AnimationStatus.completed) {\\n //动画完成时触发\\n _controller.reverse(); //反向播放\\n }\\n });\\n\\n _startAnimation();\\n }\\n\\n //3 启动和控制动画\\n void _startAnimation() {\\n _controller.forward(); //正向播放\\n //_controller.repeat(); //循环播放\\n }\\n\\n @override\\n void dispose() {\\n //释放资源\\n _controller.dispose();\\n super.dispose();\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: const Text(\'ScaleTransition 示例\'),\\n ),\\n body: Center(\\n child: ScaleTransition(\\n scale: _animation,\\n child: const SizedBox(\\n width: 100,\\n height: 100,\\n child: FlutterLogo(),\\n ),\\n ),\\n ),\\n );\\n }\\n}\\n
","description":"Flutter 动画之 Explicit 显式动画 Explicit 显式动画指的是通过手动配置和控制动画过程来实现动画效果的方式,提供对动画状态(比如正向播放、反向播放和重复播放等)的精细化管理,与隐式动画(由 Flutter 自动管理动画过程)相对应\\n显式动画的核心在于使用 AnimationController 和 Animation\\n显式动画可以精细控制,灵活性较高\\nAnimationController\\nAnimationController 继承自 Animation,动画控制器,控制着动画的播放、暂停等状态,管理动画的…","guid":"https://juejin.cn/post/7496029800742977563","author":"louisgeek","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-22T15:15:38.636Z","media":null,"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"我们封装了哪些好用的Flutter Mixin","url":"https://juejin.cn/post/7495958127679340598","content":"本篇主要是简单介绍了一下Mixin,总结分享了一下在项目我们封装了哪些Mixin,希望能够给读者带来一些思考。","description":"本篇主要是简单介绍了一下Mixin,总结分享了一下在项目我们封装了哪些Mixin,希望能够给读者带来一些思考。","guid":"https://juejin.cn/post/7495958127679340598","author":"半山居士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-22T12:23:07.575Z","media":null,"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"SchedulerBinding源码解析","url":"https://juejin.cn/post/7495699805676093491","content":"// Copyright 2014 The Flutter Authors. All rights reserved.\\n// Use of this source code is governed by a BSD-style license that can be\\n// found in the LICENSE file.\\n// 中文:版权所有 2014 The Flutter Authors。保留所有权利。\\n// 中文:本代码的使用受 BSD 许可证的约束,该许可证可在\\n// 中文:LICENSE 文件中找到。\\n\\n/// @docImport \'package:flutter/material.dart\';\\n/// @docImport \'package:flutter/rendering.dart\';\\n/// @docImport \'package:flutter/services.dart\';\\n/// @docImport \'package:flutter/widgets.dart\';\\n///\\n/// @docImport \'ticker.dart\';\\nlibrary;\\n\\nimport \'dart:async\';\\nimport \'dart:collection\';\\nimport \'dart:developer\' show Flow, Timeline, TimelineTask;\\nimport \'dart:ui\' show AppLifecycleState, DartPerformanceMode, FramePhase, FrameTiming, PlatformDispatcher, TimingsCallback;\\n\\nimport \'package:collection/collection.dart\' show HeapPriorityQueue, PriorityQueue;\\nimport \'package:flutter/foundation.dart\';\\n\\nimport \'debug.dart\';\\nimport \'priority.dart\';\\nimport \'service_extensions.dart\';\\n\\nexport \'dart:ui\' show AppLifecycleState, FrameTiming, TimingsCallback;\\n\\nexport \'priority.dart\' show Priority;\\n\\n/// Slows down animations by this factor to help in development.\\n/// 中文:通过此因子减慢动画速度,以帮助开发。\\ndouble get timeDilation => _timeDilation;\\ndouble _timeDilation = 1.0;\\n/// If the [SchedulerBinding] has been initialized, setting the time dilation\\n/// automatically calls [SchedulerBinding.resetEpoch] to ensure that time stamps\\n/// seen by consumers of the scheduler binding are always increasing.\\n///\\n/// It is safe to set this before initializing the binding.\\n/// 中文:如果 [SchedulerBinding] 已经初始化,设置时间膨胀\\n/// 中文:会自动调用 [SchedulerBinding.resetEpoch] 以确保调度器绑定的消费者\\n/// 中文:看到的时间戳总是递增的。\\n///\\n/// 中文:在初始化绑定之前设置这个值是安全的。\\nset timeDilation(double value) {\\n assert(value > 0.0);\\n if (_timeDilation == value) {\\n return;\\n }\\n // If the binding has been created, we need to resetEpoch first so that we\\n // capture start of the epoch with the current time dilation.\\n // 中文:如果绑定已经创建,我们需要先调用resetEpoch,以便\\n // 中文:使用当前的时间膨胀捕获时代的开始。\\n SchedulerBinding._instance?.resetEpoch();\\n _timeDilation = value;\\n}\\n\\n/// Signature for frame-related callbacks from the scheduler.\\n///\\n/// The `timeStamp` is the number of milliseconds since the beginning of the\\n/// scheduler\'s epoch. Use timeStamp to determine how far to advance animation\\n/// timelines so that all the animations in the system are synchronized to a\\n/// common time base.\\n/// 中文:调度器的帧相关回调的签名。\\n///\\n/// 中文:`timeStamp` 是自调度器纪元开始以来的毫秒数。使用timeStamp来确定\\n/// 中文:动画时间线应该前进多少,以便系统中的所有动画都同步到\\n/// 中文:一个共同的时间基准。\\ntypedef FrameCallback = void Function(Duration timeStamp);\\n
\\n/// Signature for [SchedulerBinding.scheduleTask] callbacks.\\n///\\n/// The type argument `T` is the task\'s return value. Consider `void` if the\\n/// task does not return a value.\\n/// 中文:[SchedulerBinding.scheduleTask] 回调的签名。\\n///\\n/// 中文:类型参数 `T` 是任务的返回值。如果任务不返回值,可以考虑使用 `void`。\\ntypedef TaskCallback<T> = FutureOr<T> Function();\\n\\n/// Signature for the [SchedulerBinding.schedulingStrategy] callback. Called\\n/// whenever the system needs to decide whether a task at a given\\n/// priority needs to be run.\\n///\\n/// Return true if a task with the given priority should be executed at this\\n/// time, false otherwise.\\n///\\n/// See also:\\n///\\n/// * [defaultSchedulingStrategy], the default [SchedulingStrategy] for [SchedulerBinding.schedulingStrategy].\\n/// 中文:[SchedulerBinding.schedulingStrategy] 回调的签名。每当系统需要\\n/// 中文:决定是否应该执行给定优先级的任务时调用。\\n///\\n/// 中文:如果应该在此时执行具有给定优先级的任务,则返回true,否则返回false。\\n///\\n/// 中文:另请参阅:\\n///\\n/// 中文: * [defaultSchedulingStrategy],[SchedulerBinding.schedulingStrategy]的默认[SchedulingStrategy]。\\ntypedef SchedulingStrategy = bool Function({ required int priority, required SchedulerBinding scheduler });\\n\\nclass _TaskEntry<T> {\\n _TaskEntry(this.task, this.priority, this.debugLabel, this.flow) {\\n assert(() {\\n debugStack = StackTrace.current;\\n return true;\\n }());\\n }\\n final TaskCallback<T> task;\\n final int priority;\\n final String? debugLabel;\\n final Flow? flow;\\n\\n late StackTrace debugStack;\\n final Completer<T> completer = Completer<T>();\\n\\n void run() {\\n if (!kReleaseMode) {\\n Timeline.timeSync(\\n debugLabel ?? \'Scheduled Task\',\\n () {\\n completer.complete(task());\\n },\\n flow: flow != null ? Flow.step(flow!.id) : null,\\n );\\n } else {\\n completer.complete(task());\\n }\\n }\\n}\\n\\nclass _FrameCallbackEntry {\\n _FrameCallbackEntry(this.callback, { bool rescheduling = false }) {\\n assert(() {\\n if (rescheduling) {\\n assert(() {\\n if (debugCurrentCallbackStack == null) {\\n throw FlutterError.fromParts(<DiagnosticsNode>[\\n ErrorSummary(\'scheduleFrameCallback called with rescheduling true, but no callback is in scope.\'),\\n // 中文:scheduleFrameCallback被调用时rescheduling为true,但没有回调在作用域内。\\n ErrorDescription(\\n \'The \\"rescheduling\\" argument should only be set to true if the \'\\n \'callback is being reregistered from within the callback itself, \'\\n \'and only then if the callback itself is entirely synchronous.\',\\n // 中文:\\"rescheduling\\"参数应该只在回调本身被重新注册时设置为true,\\n // 中文:并且只有在回调本身完全是同步的情况下。\\n ),\\n ErrorHint(\\n \'If this is the initial registration of the callback, or if the \'\\n \'callback is asynchronous, then do not use the \\"rescheduling\\" \'\\n \'argument.\',\\n // 中文:如果这是回调的初始注册,或者回调是异步的,\\n // 中文:那么不要使用\\"rescheduling\\"参数。\\n ),\\n ]);\\n }\\n return true;\\n }());\\n debugStack = debugCurrentCallbackStack;\\n } else {\\n // TODO(ianh): trim the frames from this library, so that the call to scheduleFrameCallback is the top one\\n // 中文:TODO(ianh): 从这个库中修剪帧,使对scheduleFrameCallback的调用成为顶部的帧\\n debugStack = StackTrace.current;\\n }\\n return true;\\n }());\\n }\\n\\n final FrameCallback callback;\\n\\n static StackTrace? debugCurrentCallbackStack;\\n StackTrace? debugStack;\\n}\\n\\n/// The various phases that a [SchedulerBinding] goes through during\\n/// [SchedulerBinding.handleBeginFrame].\\n///\\n/// This is exposed by [SchedulerBinding.schedulerPhase].\\n///\\n/// The values of this enum are ordered in the same order as the phases occur,\\n/// so their relative index values can be compared to each other.\\n///\\n/// See also:\\n///\\n/// * [WidgetsBinding.drawFrame], which pumps the build and rendering pipeline\\n/// to generate a frame.\\n/// 中文:[SchedulerBinding]在[SchedulerBinding.handleBeginFrame]期间经历的各个阶段。\\n///\\n/// 中文:这由[SchedulerBinding.schedulerPhase]暴露。\\n///\\n/// 中文:此枚举的值按照阶段发生的顺序排序,\\n/// 中文:因此它们的相对索引值可以相互比较。\\n///\\n/// 中文:另请参阅:\\n///\\n/// 中文: * [WidgetsBinding.drawFrame],它泵送构建和渲染管道\\n/// 中文: 以生成一帧。\\nenum SchedulerPhase {\\n /// No frame is being processed. Tasks (scheduled by\\n /// [SchedulerBinding.scheduleTask]), microtasks (scheduled by\\n /// [scheduleMicrotask]), [Timer] callbacks, event handlers (e.g. from user\\n /// input), and other callbacks (e.g. from [Future]s, [Stream]s, and the like)\\n /// may be executing.\\n /// 中文:没有帧正在处理。任务(由[SchedulerBinding.scheduleTask]调度),\\n /// 中文:微任务(由[scheduleMicrotask]调度),[Timer]回调,事件处理程序\\n /// 中文:(例如来自用户输入),以及其他回调(例如来自[Future],[Stream]等)\\n /// 中文:可能正在执行。\\n idle,\\n\\n /// The transient callbacks (scheduled by\\n /// [SchedulerBinding.scheduleFrameCallback]) are currently executing.\\n ///\\n /// Typically, these callbacks handle updating objects to new animation\\n /// states.\\n ///\\n /// See [SchedulerBinding.handleBeginFrame].\\n /// 中文:瞬态回调(由[SchedulerBinding.scheduleFrameCallback]调度)\\n /// 中文:当前正在执行。\\n ///\\n /// 中文:通常,这些回调处理将对象更新到新的动画状态。\\n ///\\n /// 中文:参见[SchedulerBinding.handleBeginFrame]。\\n transientCallbacks,\\n\\n /// Microtasks scheduled during the processing of transient callbacks are\\n /// current executing.\\n ///\\n /// This may include, for instance, callbacks from futures resolved during the\\n /// [transientCallbacks] phase.\\n /// 中文:在处理瞬态回调期间调度的微任务当前正在执行。\\n ///\\n /// 中文:这可能包括,例如,在[transientCallbacks]阶段解析的future的回调。\\n midFrameMicrotasks,\\n\\n /// The persistent callbacks (scheduled by\\n /// [SchedulerBinding.addPersistentFrameCallback]) are currently executing.\\n ///\\n /// Typically, this is the build/layout/paint pipeline. See\\n /// [WidgetsBinding.drawFrame] and [SchedulerBinding.handleDrawFrame].\\n /// 中文:持久回调(由[SchedulerBinding.addPersistentFrameCallback]调度)\\n /// 中文:当前正在执行。\\n ///\\n /// 中文:通常,这是构建/布局/绘制管道。参见\\n /// 中文:[WidgetsBinding.drawFrame]和[SchedulerBinding.handleDrawFrame]。\\n persistentCallbacks,\\n\\n /// The post-frame callbacks (scheduled by\\n /// [SchedulerBinding.addPostFrameCallback]) are currently executing.\\n ///\\n /// Typically, these callbacks handle cleanup and scheduling of work for the\\n /// next frame.\\n ///\\n /// See [SchedulerBinding.handleDrawFrame].\\n /// 中文:帧后回调(由[SchedulerBinding.addPostFrameCallback]调度)\\n /// 中文:当前正在执行。\\n ///\\n /// 中文:通常,这些回调处理清理和为下一帧调度工作。\\n ///\\n /// 中文:参见[SchedulerBinding.handleDrawFrame]。\\n postFrameCallbacks,\\n}\\n
\\n\\n/// This callback is invoked when a request for [DartPerformanceMode] is disposed.\\n///\\n/// See also:\\n///\\n/// * [PerformanceModeRequestHandle] for more information on the lifecycle of the handle.\\n/// 中文:当[DartPerformanceMode]请求被处置时调用此回调。\\n///\\n/// 中文:另请参阅:\\n///\\n/// 中文:* [PerformanceModeRequestHandle] 有关句柄生命周期的更多信息。\\ntypedef _PerformanceModeCleanupCallback = VoidCallback;\\n\\n/// An opaque handle that keeps a request for [DartPerformanceMode] active until\\n/// disposed.\\n///\\n/// To create a [PerformanceModeRequestHandle], use [SchedulerBinding.requestPerformanceMode].\\n/// The component that makes the request is responsible for disposing the handle.\\n/// 中文:一个不透明的句柄,它保持对[DartPerformanceMode]的请求处于活动状态,直到\\n/// 中文:被处置。\\n///\\n/// 中文:要创建[PerformanceModeRequestHandle],请使用[SchedulerBinding.requestPerformanceMode]。\\n/// 中文:发出请求的组件负责处置句柄。\\nclass PerformanceModeRequestHandle {\\n PerformanceModeRequestHandle._(_PerformanceModeCleanupCallback this._cleanup) {\\n // TODO(polina-c): stop duplicating code across disposables\\n // [https://github.com/flutter/flutter/issues/137435](https://github.com/flutter/flutter/issues/137435)\\n // 中文:TODO(polina-c): 停止在可处置对象间复制代码\\n // 中文:[https://github.com/flutter/flutter/issues/137435](https://github.com/flutter/flutter/issues/137435)\\n if (kFlutterMemoryAllocationsEnabled) {\\n FlutterMemoryAllocations.instance.dispatchObjectCreated(\\n library: \'package:flutter/scheduler.dart\',\\n className: \'$PerformanceModeRequestHandle\',\\n object: this,\\n );\\n }\\n }\\n\\n _PerformanceModeCleanupCallback? _cleanup;\\n\\n /// Call this method to signal to [SchedulerBinding] that a request for a [DartPerformanceMode]\\n /// is no longer needed.\\n ///\\n /// This method must only be called once per object.\\n /// 中文:调用此方法向[SchedulerBinding]表明对[DartPerformanceMode]的请求\\n /// 中文:不再需要。\\n ///\\n /// 中文:此方法每个对象只能调用一次。\\n void dispose() {\\n assert(_cleanup != null);\\n // TODO(polina-c): stop duplicating code across disposables\\n // [https://github.com/flutter/flutter/issues/137435](https://github.com/flutter/flutter/issues/137435)\\n // 中文:TODO(polina-c): 停止在可处置对象间复制代码\\n // 中文:[https://github.com/flutter/flutter/issues/137435](https://github.com/flutter/flutter/issues/137435)\\n if (kFlutterMemoryAllocationsEnabled) {\\n FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);\\n }\\n _cleanup!();\\n _cleanup = null;\\n }\\n}\\n
\\n/// Scheduler for running the following:\\n///\\n/// * _Transient callbacks_, triggered by the system\'s\\n/// [dart:ui.PlatformDispatcher.onBeginFrame] callback, for synchronizing the\\n/// application\'s behavior to the system\'s display. For example, [Ticker]s and\\n/// [AnimationController]s trigger from these.\\n///\\n/// * _Persistent callbacks_, triggered by the system\'s\\n/// [dart:ui.PlatformDispatcher.onDrawFrame] callback, for updating the\\n/// system\'s display after transient callbacks have executed. For example, the\\n/// rendering layer uses this to drive its rendering pipeline.\\n///\\n/// * _Post-frame callbacks_, which are run after persistent callbacks, just\\n/// before returning from the [dart:ui.PlatformDispatcher.onDrawFrame] callback.\\n///\\n/// * Non-rendering tasks, to be run between frames. These are given a\\n/// priority and are executed in priority order according to a\\n/// [schedulingStrategy].\\n/// 中文:用于运行以下内容的调度器:\\n///\\n/// 中文:* _瞬态回调_,由系统的[dart:ui.PlatformDispatcher.onBeginFrame]回调触发,\\n/// 中文: 用于将应用程序的行为与系统的显示同步。例如,[Ticker]和\\n/// 中文: [AnimationController]从这些触发。\\n///\\n/// 中文:* _持久回调_,由系统的[dart:ui.PlatformDispatcher.onDrawFrame]回调触发,\\n/// 中文: 用于在瞬态回调执行后更新系统的显示。例如,渲染层使用\\n/// 中文: 这个来驱动其渲染管道。\\n///\\n/// 中文:* _帧后回调_,在持久回调之后运行,就在从\\n/// 中文: [dart:ui.PlatformDispatcher.onDrawFrame]回调返回之前。\\n///\\n/// 中文:* 非渲染任务,在帧之间运行。这些被赋予优先级,并按照\\n/// 中文: [schedulingStrategy]确定的优先级顺序执行。\\nmixin SchedulerBinding on BindingBase {\\n @override\\n void initInstances() {\\n super.initInstances();\\n _instance = this;\\n\\n if (!kReleaseMode) {\\n addTimingsCallback((List<FrameTiming> timings) {\\n timings.forEach(_profileFramePostEvent);\\n });\\n }\\n }\\n\\n /// The current [SchedulerBinding], if one has been created.\\n ///\\n /// Provides access to the features exposed by this mixin. The binding must\\n /// be initialized before using this getter; this is typically done by calling\\n /// [runApp] or [WidgetsFlutterBinding.ensureInitialized].\\n /// 中文:当前的[SchedulerBinding],如果已创建。\\n ///\\n /// 中文:提供对此mixin暴露的功能的访问。绑定必须在使用此getter之前\\n /// 中文:初始化;这通常通过调用[runApp]或[WidgetsFlutterBinding.ensureInitialized]完成。\\n static SchedulerBinding get instance => BindingBase.checkInstance(_instance);\\n static SchedulerBinding? _instance;\\n\\n final List<TimingsCallback> _timingsCallbacks = <TimingsCallback>[];\\n
\\n /// Add a [TimingsCallback] that receives [FrameTiming] sent from\\n /// the engine.\\n ///\\n /// This API enables applications to monitor their graphics\\n /// performance. Data from the engine is batched into lists of\\n /// [FrameTiming] objects which are reported approximately once a\\n /// second in release mode and approximately once every 100ms in\\n /// debug and profile builds. The list is sorted in ascending\\n /// chronological order (earliest frame first). The timing of the\\n /// first frame is sent immediately without batching.\\n ///\\n /// The data returned can be used to catch missed frames (by seeing\\n /// if [FrameTiming.buildDuration] or [FrameTiming.rasterDuration]\\n /// exceed the frame budget, e.g. 16ms at 60Hz), and to catch high\\n /// latency (by seeing if [FrameTiming.totalSpan] exceeds the frame\\n /// budget). It is possible for no frames to be missed but for the\\n /// latency to be more than one frame in the case where the Flutter\\n /// engine is pipelining the graphics updates, e.g. because the sum\\n /// of the [FrameTiming.buildDuration] and the\\n /// [FrameTiming.rasterDuration] together exceed the frame budget.\\n /// In those cases, animations will be smooth but touch input will\\n /// feel more sluggish.\\n ///\\n /// Using [addTimingsCallback] is preferred over using\\n /// [dart:ui.PlatformDispatcher.onReportTimings] directly because the\\n /// [dart:ui.PlatformDispatcher.onReportTimings] API only allows one callback,\\n /// which prevents multiple libraries from registering listeners\\n /// simultaneously, while this API allows multiple callbacks to be registered\\n /// independently.\\n ///\\n /// This API is implemented in terms of\\n /// [dart:ui.PlatformDispatcher.onReportTimings]. In release builds, when no\\n /// libraries have registered with this API, the\\n /// [dart:ui.PlatformDispatcher.onReportTimings] callback is not set, which\\n /// disables the performance tracking and reduces the runtime overhead to\\n /// approximately zero. The performance overhead of the performance tracking\\n /// when one or more callbacks are registered (i.e. when it is enabled) is\\n /// very approximately 0.01% CPU usage per second (measured on an iPhone 6s).\\n ///\\n /// In debug and profile builds, the [SchedulerBinding] itself\\n /// registers a timings callback to update the [Timeline].\\n ///\\n /// If the same callback is added twice, it will be executed twice.\\n ///\\n /// See also:\\n ///\\n /// * [removeTimingsCallback], which can be used to remove a callback\\n /// added using this method.\\n /// 中文:添加一个[TimingsCallback],接收从引擎发送的[FrameTiming]。\\n ///\\n /// 中文:此API使应用程序能够监控其图形性能。来自引擎的数据被批处理成\\n /// 中文:[FrameTiming]对象列表,在发布模式下大约每秒报告一次,\\n /// 中文:在调试和配置文件构建中大约每100毫秒报告一次。列表按升序\\n /// 中文:时间顺序排序(最早的帧在前)。第一帧的计时立即发送,\\n /// 中文:不进行批处理。\\n ///\\n /// 中文:返回的数据可用于捕获丢失的帧(通过查看\\n /// 中文:[FrameTiming.buildDuration]或[FrameTiming.rasterDuration]\\n /// 中文:是否超过帧预算,例如60Hz时为16毫秒),以及捕获高\\n /// 中文:延迟(通过查看[FrameTiming.totalSpan]是否超过帧\\n /// 中文:预算)。在没有丢失帧但延迟超过一帧的情况下,\\n /// 中文:Flutter引擎正在流水线处理图形更新,例如因为\\n /// 中文:[FrameTiming.buildDuration]和[FrameTiming.rasterDuration]\\n /// 中文:的总和超过了帧预算。在这些情况下,动画将是平滑的,\\n /// 中文:但触摸输入会感觉更加迟缓。\\n ///\\n /// 中文:使用[addTimingsCallback]比直接使用\\n /// 中文:[dart:ui.PlatformDispatcher.onReportTimings]更好,因为\\n /// 中文:[dart:ui.PlatformDispatcher.onReportTimings] API只允许一个回调,\\n /// 中文:这阻止了多个库同时注册监听器,而此API允许\\n /// 中文:独立注册多个回调。\\n ///\\n /// 中文:此API是基于[dart:ui.PlatformDispatcher.onReportTimings]实现的。\\n /// 中文:在发布构建中,当没有库通过此API注册时,\\n /// 中文:[dart:ui.PlatformDispatcher.onReportTimings]回调不会设置,\\n /// 中文:这会禁用性能跟踪并将运行时开销降低到\\n /// 中文:接近零。当注册了一个或多个回调(即启用时)的性能跟踪的\\n /// 中文:性能开销非常小,大约为每秒0.01%的CPU使用率(在iPhone 6s上测量)。\\n ///\\n /// 中文:在调试和配置文件构建中,[SchedulerBinding]本身\\n /// 中文:注册一个计时回调来更新[Timeline]。\\n ///\\n /// 中文:如果同一回调被添加两次,它将被执行两次。\\n ///\\n /// 中文:另请参阅:\\n ///\\n /// 中文: * [removeTimingsCallback],可用于删除使用此方法\\n /// 中文: 添加的回调。\\n void addTimingsCallback(TimingsCallback callback) {\\n _timingsCallbacks.add(callback);\\n if (_timingsCallbacks.length == 1) {\\n assert(platformDispatcher.onReportTimings == null);\\n platformDispatcher.onReportTimings = _executeTimingsCallbacks;\\n }\\n assert(platformDispatcher.onReportTimings == _executeTimingsCallbacks);\\n }\\n\\n /// Removes a callback that was earlier added by [addTimingsCallback].\\n /// 中文:删除先前由[addTimingsCallback]添加的回调。\\n void removeTimingsCallback(TimingsCallback callback) {\\n assert(_timingsCallbacks.contains(callback));\\n _timingsCallbacks.remove(callback);\\n if (_timingsCallbacks.isEmpty) {\\n platformDispatcher.onReportTimings = null;\\n }\\n }\\n\\n @pragma(\'vm:notify-debugger-on-exception\')\\n void _executeTimingsCallbacks(List<FrameTiming> timings) {\\n final List<TimingsCallback> clonedCallbacks =\\n List<TimingsCallback>.of(_timingsCallbacks);\\n for (final TimingsCallback callback in clonedCallbacks) {\\n try {\\n if (_timingsCallbacks.contains(callback)) {\\n callback(timings);\\n }\\n } catch (exception, stack) {\\n InformationCollector? collector;\\n assert(() {\\n collector = () => <DiagnosticsNode>[\\n DiagnosticsProperty<TimingsCallback>(\\n \'The TimingsCallback that gets executed was\',\\n // 中文:执行的TimingsCallback是\\n callback,\\n style: DiagnosticsTreeStyle.errorProperty,\\n ),\\n ];\\n return true;\\n }());\\n FlutterError.reportError(FlutterErrorDetails(\\n exception: exception,\\n stack: stack,\\n context: ErrorDescription(\'while executing callbacks for FrameTiming\'),\\n // 中文:执行FrameTiming回调时\\n informationCollector: collector,\\n ));\\n }\\n }\\n }\\n\\n @override\\n void initServiceExtensions() {\\n super.initServiceExtensions();\\n\\n if (!kReleaseMode) {\\n registerNumericServiceExtension(\\n name: SchedulerServiceExtensions.timeDilation.name,\\n getter: () async => timeDilation,\\n setter: (double value) async {\\n timeDilation = value;\\n },\\n );\\n }\\n }\\n\\n /// Whether the application is visible, and if so, whether it is currently\\n /// interactive.\\n ///\\n /// This is set by [handleAppLifecycleStateChanged] when the\\n /// [SystemChannels.lifecycle] notification is dispatched.\\n ///\\n /// The preferred ways to watch for changes to this value are using\\n /// [WidgetsBindingObserver.didChangeAppLifecycleState], or through an\\n /// [AppLifecycleListener] object.\\n /// 中文:应用程序是否可见,如果可见,当前是否可交互。\\n ///\\n /// 中文:这由[handleAppLifecycleStateChanged]在[SystemChannels.lifecycle]\\n /// 中文:通知被分派时设置。\\n ///\\n /// 中文:观察此值变化的首选方法是使用\\n /// 中文:[WidgetsBindingObserver.didChangeAppLifecycleState],或通过\\n /// 中文:[AppLifecycleListener]对象。\\n AppLifecycleState? get lifecycleState => _lifecycleState;\\n AppLifecycleState? _lifecycleState;\\n\\n /// Allows the test framework to reset the lifecycle state and framesEnabled\\n /// back to their initial values.\\n /// 中文:允许测试框架将生命周期状态和framesEnabled\\n /// 中文:重置回初始值。\\n @visibleForTesting\\n void resetInternalState() {\\n _lifecycleState = null;\\n _framesEnabled = true;\\n }\\n\\n /// Called when the application lifecycle state changes.\\n ///\\n /// Notifies all the observers using\\n /// [WidgetsBindingObserver.didChangeAppLifecycleState].\\n ///\\n /// This method exposes notifications from [SystemChannels.lifecycle].\\n /// 中文:当应用程序生命周期状态改变时调用。\\n ///\\n /// 中文:使用[WidgetsBindingObserver.didChangeAppLifecycleState]\\n /// 中文:通知所有观察者。\\n ///\\n /// 中文:此方法暴露来自[SystemChannels.lifecycle]的通知。\\n @protected\\n @mustCallSuper\\n void handleAppLifecycleStateChanged(AppLifecycleState state) {\\n if (lifecycleState == state) {\\n return;\\n }\\n _lifecycleState = state;\\n switch (state) {\\n case AppLifecycleState.resumed:\\n case AppLifecycleState.inactive:\\n _setFramesEnabledState(true);\\n case AppLifecycleState.hidden:\\n case AppLifecycleState.paused:\\n case AppLifecycleState.detached:\\n _setFramesEnabledState(false);\\n }\\n }\\n\\n /// The strategy to use when deciding whether to run a task or not.\\n ///\\n /// Defaults to [defaultSchedulingStrategy].\\n /// 中文:决定是否运行任务的策略。\\n ///\\n /// 中文:默认为[defaultSchedulingStrategy]。\\n SchedulingStrategy schedulingStrategy = defaultSchedulingStrategy;\\n\\n static int _taskSorter (_TaskEntry<dynamic> e1, _TaskEntry<dynamic> e2) {\\n return -e1.priority.compareTo(e2.priority);\\n }\\n final PriorityQueue<_TaskEntry<dynamic>> _taskQueue = HeapPriorityQueue<_TaskEntry<dynamic>>(_taskSorter);\\n
\\n\\n /// Schedules the given `task` with the given `priority`.\\n ///\\n /// If `task` returns a future, the future returned by [scheduleTask] will\\n /// complete after the former future has been scheduled to completion.\\n /// Otherwise, the returned future for [scheduleTask] will complete with the\\n /// same value returned by `task` after it has been scheduled.\\n ///\\n /// The `debugLabel` and `flow` are used to report the task to the [Timeline],\\n /// for use when profiling.\\n ///\\n /// ## Processing model\\n ///\\n /// Tasks will be executed between frames, in priority order,\\n /// excluding tasks that are skipped by the current\\n /// [schedulingStrategy]. Tasks should be short (as in, up to a\\n /// millisecond), so as to not cause the regular frame callbacks to\\n /// get delayed.\\n ///\\n /// If an animation is running, including, for instance, a [ProgressIndicator]\\n /// indicating that there are pending tasks, then tasks with a priority below\\n /// [Priority.animation] won\'t run (at least, not with the\\n /// [defaultSchedulingStrategy]; this can be configured using\\n /// [schedulingStrategy]).\\n /// 中文:使用给定的`priority`调度给定的`task`。\\n ///\\n /// 中文:如果`task`返回future,则[scheduleTask]返回的future将\\n /// 中文:在前者future被调度完成后完成。\\n /// 中文:否则,[scheduleTask]返回的future将在`task`\\n /// 中文:被调度后以与`task`返回的相同值完成。\\n ///\\n /// 中文:`debugLabel`和`flow`用于在分析时向[Timeline]报告任务。\\n ///\\n /// 中文:## 处理模型\\n ///\\n /// 中文:任务将在帧之间按优先级顺序执行,\\n /// 中文:排除当前[schedulingStrategy]跳过的任务。任务应该很短\\n /// 中文:(最多一毫秒),以免导致常规帧回调延迟。\\n ///\\n /// 中文:如果动画正在运行,包括,例如,[ProgressIndicator]\\n /// 中文:指示有待处理的任务,那么优先级低于\\n /// 中文:[Priority.animation]的任务不会运行(至少,不会使用\\n /// 中文:[defaultSchedulingStrategy];这可以通过\\n /// 中文:[schedulingStrategy]配置)。\\n Future<T> scheduleTask<T>(\\n TaskCallback<T> task,\\n Priority priority, {\\n String? debugLabel,\\n Flow? flow,\\n }) {\\n final bool isFirstTask = _taskQueue.isEmpty;\\n final _TaskEntry<T> entry = _TaskEntry<T>(\\n task,\\n priority.value,\\n debugLabel,\\n flow,\\n );\\n _taskQueue.add(entry);\\n if (isFirstTask && !locked) {\\n _ensureEventLoopCallback();\\n }\\n return entry.completer.future;\\n }\\n\\n @override\\n void unlocked() {\\n super.unlocked();\\n if (_taskQueue.isNotEmpty) {\\n _ensureEventLoopCallback();\\n }\\n }\\n\\n // Whether this scheduler already requested to be called from the event loop.\\n // 中文:此调度器是否已经请求从事件循环中调用。\\n bool _hasRequestedAnEventLoopCallback = false;\\n\\n // Ensures that the scheduler services a task scheduled by\\n // [SchedulerBinding.scheduleTask].\\n // 中文:确保调度器服务由[SchedulerBinding.scheduleTask]调度的任务。\\n void _ensureEventLoopCallback() {\\n assert(!locked);\\n assert(_taskQueue.isNotEmpty);\\n if (_hasRequestedAnEventLoopCallback) {\\n return;\\n }\\n _hasRequestedAnEventLoopCallback = true;\\n Timer.run(_runTasks);\\n }\\n\\n // Scheduled by _ensureEventLoopCallback.\\n // 中文:由_ensureEventLoopCallback调度。\\n void _runTasks() {\\n _hasRequestedAnEventLoopCallback = false;\\n if (handleEventLoopCallback()) {\\n _ensureEventLoopCallback();\\n } // runs next task when there\'s time\\n // 中文:当有时间时运行下一个任务\\n }\\n\\n /// Execute the highest-priority task, if it is of a high enough priority.\\n ///\\n /// Returns false if the scheduler is [locked], or if there are no tasks\\n /// remaining.\\n ///\\n /// Returns true otherwise, including when no task is executed due to priority\\n /// being too low.\\n /// 中文:执行最高优先级的任务,如果它的优先级足够高。\\n ///\\n /// 中文:如果调度器[locked],或者没有剩余任务,则返回false。\\n ///\\n /// 中文:否则返回true,包括当由于优先级太低而没有执行任务的情况。\\n @visibleForTesting\\n @pragma(\'vm:notify-debugger-on-exception\')\\n bool handleEventLoopCallback() {\\n if (_taskQueue.isEmpty || locked) {\\n return false;\\n }\\n final _TaskEntry<dynamic> entry = _taskQueue.first;\\n if (schedulingStrategy(priority: entry.priority, scheduler: this)) {\\n try {\\n _taskQueue.removeFirst();\\n entry.run();\\n } catch (exception, exceptionStack) {\\n StackTrace? callbackStack;\\n assert(() {\\n callbackStack = entry.debugStack;\\n return true;\\n }());\\n FlutterError.reportError(FlutterErrorDetails(\\n exception: exception,\\n stack: exceptionStack,\\n library: \'scheduler library\',\\n context: ErrorDescription(\'during a task callback\'),\\n // 中文:在任务回调期间\\n informationCollector: (callbackStack == null) ? null : () {\\n return <DiagnosticsNode>[\\n DiagnosticsStackTrace(\\n \'\\\\nThis exception was thrown in the context of a scheduler callback. \'\\n \'When the scheduler callback was _registered_ (as opposed to when the \'\\n \'exception was thrown), this was the stack\',\\n // 中文:\\\\n这个异常是在调度器回调的上下文中抛出的。\\n // 中文:当调度器回调被_注册_(而不是异常被抛出)时,这是堆栈\\n callbackStack,\\n ),\\n ];\\n },\\n ));\\n }\\n return _taskQueue.isNotEmpty;\\n }\\n return true;\\n }\\n
\\n\\n int _nextFrameCallbackId = 0; // positive\\n Map<int, _FrameCallbackEntry> _transientCallbacks = <int, _FrameCallbackEntry>{};\\n final Set<int> _removedIds = HashSet<int>();\\n\\n /// The current number of transient frame callbacks scheduled.\\n ///\\n /// This is reset to zero just before all the currently scheduled\\n /// transient callbacks are called, at the start of a frame.\\n ///\\n /// This number is primarily exposed so that tests can verify that\\n /// there are no unexpected transient callbacks still registered\\n /// after a test\'s resources have been gracefully disposed.\\n /// 中文:当前调度的瞬态帧回调数量。\\n ///\\n /// 中文:这在帧开始时,在调用所有当前调度的瞬态回调之前,\\n /// 中文:重置为零。\\n ///\\n /// 中文:这个数字主要是暴露出来,以便测试可以验证\\n /// 中文:在测试的资源被优雅地处置后没有意外的瞬态回调\\n /// 中文:仍然注册。\\n int get transientCallbackCount => _transientCallbacks.length;\\n\\n /// Schedules the given transient frame callback.\\n ///\\n /// Adds the given callback to the list of frame callbacks and ensures that a\\n /// frame is scheduled.\\n ///\\n /// If this is called during the frame\'s animation phase (when transient frame\\n /// callbacks are still being invoked), a new frame will be scheduled, and\\n /// `callback` will be called in the newly scheduled frame, not in the current\\n /// frame.\\n ///\\n /// If this is a one-off registration, ignore the `rescheduling` argument.\\n ///\\n /// If this is a callback that will be re-registered each time it fires, then\\n /// when you re-register the callback, set the `rescheduling` argument to\\n /// true. This has no effect in release builds, but in debug builds, it\\n /// ensures that the stack trace that is stored for this callback is the\\n /// original stack trace for when the callback was _first_ registered, rather\\n /// than the stack trace for when the callback is re-registered. This makes it\\n /// easier to track down the original reason that a particular callback was\\n /// called. If `rescheduling` is true, the call must be in the context of a\\n /// frame callback.\\n ///\\n /// Callbacks registered with this method can be canceled using\\n /// [cancelFrameCallbackWithId].\\n ///\\n /// See also:\\n ///\\n /// * [WidgetsBinding.drawFrame], which explains the phases of each frame\\n /// for those apps that use Flutter widgets (and where transient frame\\n /// callbacks fit into those phases).\\n /// 中文:调度给定的瞬态帧回调。\\n ///\\n /// 中文:将给定的回调添加到帧回调列表中,并确保帧被调度。\\n ///\\n /// 中文:如果这是在帧的动画阶段调用的(当瞬态帧回调仍在被调用时),\\n /// 中文:将会调度一个新帧,并且`callback`将在新调度的帧中被调用,\\n /// 中文:而不是在当前帧中。\\n ///\\n /// 中文:如果这是一次性注册,忽略`rescheduling`参数。\\n ///\\n /// 中文:如果这是一个每次触发时都会重新注册的回调,那么\\n /// 中文:当你重新注册回调时,将`rescheduling`参数设置为\\n /// 中文:true。这在发布构建中没有效果,但在调试构建中,它\\n /// 中文:确保为此回调存储的堆栈跟踪是回调_首次_注册时的\\n /// 中文:原始堆栈跟踪,而不是回调被重新注册时的堆栈跟踪。\\n /// 中文:这使得更容易追踪特定回调被调用的原始原因。\\n /// 中文:如果`rescheduling`为true,调用必须在帧回调的上下文中。\\n ///\\n /// 中文:使用此方法注册的回调可以使用[cancelFrameCallbackWithId]取消。\\n ///\\n /// 中文:另请参阅:\\n ///\\n /// 中文: * [WidgetsBinding.drawFrame],它解释了使用Flutter小部件的应用程序\\n /// 中文: 每帧的各个阶段(以及瞬态帧回调适合这些阶段的位置)。\\n int scheduleFrameCallback(FrameCallback callback, { bool rescheduling = false }) {\\n scheduleFrame();\\n _nextFrameCallbackId += 1;\\n _transientCallbacks[_nextFrameCallbackId] = _FrameCallbackEntry(callback, rescheduling: rescheduling);\\n return _nextFrameCallbackId;\\n }\\n\\n /// Cancels the transient frame callback with the given [id].\\n ///\\n /// Removes the given callback from the list of frame callbacks. If a frame\\n /// has been requested, this does not also cancel that request.\\n ///\\n /// Transient frame callbacks are those registered using\\n /// [scheduleFrameCallback].\\n /// 中文:取消具有给定[id]的瞬态帧回调。\\n ///\\n /// 中文:从帧回调列表中删除给定的回调。如果已经请求了一帧,\\n /// 中文:这不会同时取消该请求。\\n ///\\n /// 中文:瞬态帧回调是使用[scheduleFrameCallback]注册的回调。\\n void cancelFrameCallbackWithId(int id) {\\n assert(id > 0);\\n _transientCallbacks.remove(id);\\n _removedIds.add(id);\\n }\\n\\n
\\n /// Asserts that there are no registered transient callbacks; if\\n /// there are, prints their locations and throws an exception.\\n ///\\n /// A transient frame callback is one that was registered with\\n /// [scheduleFrameCallback].\\n ///\\n /// This is expected to be called at the end of tests (the\\n /// flutter_test framework does it automatically in normal cases).\\n ///\\n /// Call this method when you expect there to be no transient\\n /// callbacks registered, in an assert statement with a message that\\n /// you want printed when a transient callback is registered:\\n ///\\n /// ```dart\\n /// assert(SchedulerBinding.instance.debugAssertNoTransientCallbacks(\\n /// \'A leak of transient callbacks was detected while doing foo.\'\\n /// ));\\n /// ```\\n ///\\n /// Does nothing if asserts are disabled. Always returns true.\\n /// 中文:断言没有注册的瞬态回调;如果\\n /// 中文:有,打印它们的位置并抛出异常。\\n ///\\n /// 中文:瞬态帧回调是使用[scheduleFrameCallback]注册的回调。\\n ///\\n /// 中文:这预期在测试结束时调用(flutter_test框架在正常情况下会自动执行)。\\n ///\\n /// 中文:当你期望没有瞬态回调注册时,在断言语句中调用此方法,\\n /// 中文:并带有你希望在瞬态回调被注册时打印的消息:\\n ///\\n /// ```dart\\n /// assert(SchedulerBinding.instance.debugAssertNoTransientCallbacks(\\n /// \'在执行foo时检测到瞬态回调泄漏。\'\\n /// ));\\n /// ```\\n ///\\n /// 中文:如果断言被禁用,则不执行任何操作。始终返回true。\\n bool debugAssertNoTransientCallbacks(String reason) {\\n assert(() {\\n if (transientCallbackCount > 0) {\\n // We cache the values so that we can produce them later\\n // even if the information collector is called after\\n // the problem has been resolved.\\n // 中文:我们缓存这些值,以便稍后可以生成它们\\n // 中文:即使信息收集器在问题解决后被调用。\\n final int count = transientCallbackCount;\\n final Map<int, _FrameCallbackEntry> callbacks = Map<int, _FrameCallbackEntry>.of(_transientCallbacks);\\n FlutterError.reportError(FlutterErrorDetails(\\n exception: reason,\\n library: \'scheduler library\',\\n informationCollector: () => <DiagnosticsNode>[\\n if (count == 1)\\n // TODO(jacobr): I have added an extra line break in this case.\\n // 中文:TODO(jacobr): 我在这种情况下添加了一个额外的换行符。\\n ErrorDescription(\\n \'There was one transient callback left. \'\\n \'The stack trace for when it was registered is as follows:\',\\n // 中文:有一个瞬态回调剩余。\\n // 中文:它注册时的堆栈跟踪如下:\\n )\\n else\\n ErrorDescription(\\n \'There were $count transient callbacks left. \'\\n \'The stack traces for when they were registered are as follows:\',\\n // 中文:有$count个瞬态回调剩余。\\n // 中文:它们注册时的堆栈跟踪如下:\\n ),\\n for (final int id in callbacks.keys)\\n DiagnosticsStackTrace(\'── callback $id ──\', callbacks[id]!.debugStack, showSeparator: false),\\n ],\\n ));\\n }\\n return true;\\n }());\\n return true;\\n }\\n\\n /// Asserts that there are no pending performance mode requests in debug mode.\\n ///\\n /// Throws a [FlutterError] if there are pending performance mode requests,\\n /// as this indicates a potential memory leak.\\n /// 中文:在调试模式下断言没有待处理的性能模式请求。\\n ///\\n /// 中文:如果有待处理的性能模式请求,则抛出[FlutterError],\\n /// 中文:因为这表明潜在的内存泄漏。\\n bool debugAssertNoPendingPerformanceModeRequests(String reason) {\\n assert(() {\\n if (_performanceMode != null) {\\n throw FlutterError(reason);\\n }\\n return true;\\n }());\\n return true;\\n }\\n\\n /// Asserts that there is no artificial time dilation in debug mode.\\n ///\\n /// Throws a [FlutterError] if there are such dilation, as this will make\\n /// subsequent tests see dilation and thus flaky.\\n /// 中文:在调试模式下断言没有人为的时间膨胀。\\n ///\\n /// 中文:如果有这样的膨胀,则抛出[FlutterError],因为这会使\\n /// 中文:后续测试看到膨胀,从而变得不稳定。\\n bool debugAssertNoTimeDilation(String reason) {\\n assert(() {\\n if (timeDilation != 1.0) {\\n throw FlutterError(reason);\\n }\\n return true;\\n }());\\n return true;\\n }\\n\\n
\\n\\n /// Prints the stack for where the current transient callback was registered.\\n ///\\n /// A transient frame callback is one that was registered with\\n /// [scheduleFrameCallback].\\n ///\\n /// When called in debug more and in the context of a transient callback, this\\n /// function prints the stack trace from where the current transient callback\\n /// was registered (i.e. where it first called [scheduleFrameCallback]).\\n ///\\n /// When called in debug mode in other contexts, it prints a message saying\\n /// that this function was not called in the context a transient callback.\\n ///\\n /// In release mode, this function does nothing.\\n ///\\n /// To call this function, use the following code:\\n ///\\n /// ```dart\\n /// SchedulerBinding.debugPrintTransientCallbackRegistrationStack();\\n /// ```\\n /// 中文:打印当前瞬态回调注册位置的堆栈。\\n ///\\n /// 中文:瞬态帧回调是使用[scheduleFrameCallback]注册的回调。\\n ///\\n /// 中文:当在调试模式下并在瞬态回调的上下文中调用时,此\\n /// 中文:函数打印当前瞬态回调注册位置的堆栈跟踪\\n /// 中文:(即它首次调用[scheduleFrameCallback]的位置)。\\n ///\\n /// 中文:当在调试模式下在其他上下文中调用时,它打印一条消息,说明\\n /// 中文:此函数不是在瞬态回调的上下文中调用的。\\n ///\\n /// 中文:在发布模式下,此函数不执行任何操作。\\n ///\\n /// 中文:要调用此函数,请使用以下代码:\\n ///\\n /// ```dart\\n /// SchedulerBinding.debugPrintTransientCallbackRegistrationStack();\\n /// ```\\n static void debugPrintTransientCallbackRegistrationStack() {\\n assert(() {\\n if (_FrameCallbackEntry.debugCurrentCallbackStack != null) {\\n debugPrint(\'When the current transient callback was registered, this was the stack:\');\\n // 中文:当前瞬态回调注册时,这是堆栈:\\n debugPrint(\\n FlutterError.defaultStackFilter(\\n FlutterError.demangleStackTrace(\\n _FrameCallbackEntry.debugCurrentCallbackStack!,\\n ).toString().trimRight().split(\'\\\\n\'),\\n ).join(\'\\\\n\'),\\n );\\n } else {\\n debugPrint(\'No transient callback is currently executing.\');\\n // 中文:当前没有瞬态回调正在执行。\\n }\\n return true;\\n }());\\n }\\n\\n final List<FrameCallback> _persistentCallbacks = <FrameCallback>[];\\n\\n /// Adds a persistent frame callback.\\n ///\\n /// Persistent callbacks are called after transient\\n /// (non-persistent) frame callbacks.\\n ///\\n /// Does *not* request a new frame. Conceptually, persistent frame\\n /// callbacks are observers of \\"begin frame\\" events. Since they are\\n /// executed after the transient frame callbacks they can drive the\\n /// rendering pipeline.\\n ///\\n /// Persistent frame callbacks cannot be unregistered. Once registered, they\\n /// are called for every frame for the lifetime of the application.\\n ///\\n /// See also:\\n ///\\n /// * [WidgetsBinding.drawFrame], which explains the phases of each frame\\n /// for those apps that use Flutter widgets (and where persistent frame\\n /// callbacks fit into those phases).\\n /// 中文:添加持久帧回调。\\n ///\\n /// 中文:持久回调在瞬态(非持久)帧回调之后调用。\\n ///\\n /// 中文:*不*请求新帧。从概念上讲,持久帧回调是\\"开始帧\\"事件的观察者。\\n /// 中文:由于它们在瞬态帧回调之后执行,它们可以驱动渲染管道。\\n ///\\n /// 中文:持久帧回调不能被注销。一旦注册,它们将在应用程序的生命周期内\\n /// 中文:为每一帧调用。\\n ///\\n /// 中文:另请参阅:\\n ///\\n /// 中文: * [WidgetsBinding.drawFrame],它解释了使用Flutter小部件的应用程序\\n /// 中文: 每帧的各个阶段(以及持久帧回调适合这些阶段的位置)。\\n void addPersistentFrameCallback(FrameCallback callback) {\\n _persistentCallbacks.add(callback);\\n }\\n\\n final List<FrameCallback> _postFrameCallbacks = <FrameCallback>[];\\n\\n /// Schedule a callback for the end of this frame.\\n ///\\n /// The provided callback is run immediately after a frame, just after the\\n /// persistent frame callbacks (which is when the main rendering pipeline has\\n /// been flushed).\\n ///\\n /// This method does *not* request a new frame. If a frame is already in\\n /// progress and the execution of post-frame callbacks has not yet begun, then\\n /// the registered callback is executed at the end of the current frame.\\n /// Otherwise, the registered callback is executed after the next frame\\n /// (whenever that may be, if ever).\\n ///\\n /// The callbacks are executed in the order in which they have been\\n /// added.\\n ///\\n /// Post-frame callbacks cannot be unregistered. They are called exactly once.\\n ///\\n /// In debug mode, if [debugTracePostFrameCallbacks] is set to true, then the\\n /// registered callback will show up in the timeline events chart, which can\\n /// be viewed in [DevTools](https://docs.flutter.dev/tools/devtools).\\n /// In that case, the `debugLabel` argument specifies the name of the callback\\n /// as it will appear in the timeline. In profile and release builds,\\n /// post-frame are never traced, and the `debugLabel` argument is ignored.\\n ///\\n /// See also:\\n ///\\n /// * [scheduleFrameCallback], which registers a callback for the start of\\n /// the next frame.\\n /// * [WidgetsBinding.drawFrame], which explains the phases of each frame\\n /// for those apps that use Flutter widgets (and where post frame\\n /// callbacks fit into those phases).\\n /// 中文:为此帧的结束调度回调。\\n ///\\n /// 中文:提供的回调在帧结束后立即运行,就在持久帧回调之后\\n /// 中文:(这是主渲染管道已被刷新的时候)。\\n ///\\n /// 中文:此方法*不*请求新帧。如果帧已经在进行中,并且帧后回调的执行\\n /// 中文:尚未开始,则注册的回调在当前帧结束时执行。\\n /// 中文:否则,注册的回调在下一帧之后执行(如果有的话,无论何时)。\\n ///\\n /// 中文:回调按照添加的顺序执行。\\n ///\\n /// 中文:帧后回调不能被注销。它们只被调用一次。\\n ///\\n /// 中文:在调试模式下,如果[debugTracePostFrameCallbacks]设置为true,则\\n /// 中文:注册的回调将显示在时间线事件图表中,可以在\\n /// 中文:[DevTools](https://docs.flutter.dev/tools/devtools)中查看。\\n /// 中文:在这种情况下,`debugLabel`参数指定回调在时间线中显示的名称。\\n /// 中文:在配置文件和发布构建中,帧后回调永远不会被跟踪,并且`debugLabel`参数被忽略。\\n ///\\n /// 中文:另请参阅:\\n ///\\n /// 中文: * [scheduleFrameCallback],它为下一帧的开始注册回调。\\n /// 中文: * [WidgetsBinding.drawFrame],它解释了使用Flutter小部件的应用程序\\n /// 中文: 每帧的各个阶段(以及帧后回调适合这些阶段的位置)。\\n void addPostFrameCallback(FrameCallback callback, {String debugLabel = \'callback\'}) {\\n assert(() {\\n if (debugTracePostFrameCallbacks) {\\n final FrameCallback originalCallback = callback;\\n callback = (Duration timeStamp) {\\n Timeline.startSync(debugLabel);\\n try {\\n originalCallback(timeStamp);\\n } finally {\\n Timeline.finishSync();\\n }\\n };\\n }\\n return true;\\n }());\\n _postFrameCallbacks.add(callback);\\n }\\n\\n\\n
\\n Completer<void>? _nextFrameCompleter;\\n\\n /// Returns a Future that completes after the frame completes.\\n ///\\n /// If this is called between frames, a frame is immediately scheduled if\\n /// necessary. If this is called during a frame, the Future completes after\\n /// the current frame.\\n ///\\n /// If the device\'s screen is currently turned off, this may wait a very long\\n /// time, since frames are not scheduled while the device\'s screen is turned\\n /// off.\\n /// 中文:返回一个在帧完成后完成的Future。\\n ///\\n /// 中文:如果这是在帧之间调用的,如果必要,会立即调度一帧。\\n /// 中文:如果这是在帧期间调用的,Future在当前帧之后完成。\\n ///\\n /// 中文:如果设备的屏幕当前关闭,这可能会等待很长时间,\\n /// 中文:因为在设备屏幕关闭时不会调度帧。\\n Future<void> get endOfFrame {\\n if (_nextFrameCompleter == null) {\\n if (schedulerPhase == SchedulerPhase.idle) {\\n scheduleFrame();\\n }\\n _nextFrameCompleter = Completer<void>();\\n addPostFrameCallback((Duration timeStamp) {\\n _nextFrameCompleter!.complete();\\n _nextFrameCompleter = null;\\n }, debugLabel: \'SchedulerBinding.completeFrame\');\\n }\\n return _nextFrameCompleter!.future;\\n }\\n\\n /// Whether this scheduler has requested that [handleBeginFrame] be called soon.\\n /// 中文:此调度器是否已请求[handleBeginFrame]很快被调用。\\n bool get hasScheduledFrame => _hasScheduledFrame;\\n bool _hasScheduledFrame = false;\\n\\n /// The phase that the scheduler is currently operating under.\\n /// 中文:调度器当前运行的阶段。\\n SchedulerPhase get schedulerPhase => _schedulerPhase;\\n SchedulerPhase _schedulerPhase = SchedulerPhase.idle;\\n\\n /// Whether frames are currently being scheduled when [scheduleFrame] is called.\\n ///\\n /// This value depends on the value of the [lifecycleState].\\n /// 中文:当调用[scheduleFrame]时,帧当前是否正在被调度。\\n ///\\n /// 中文:此值取决于[lifecycleState]的值。\\n bool get framesEnabled => _framesEnabled;\\n\\n bool _framesEnabled = true;\\n void _setFramesEnabledState(bool enabled) {\\n if (_framesEnabled == enabled) {\\n return;\\n }\\n _framesEnabled = enabled;\\n if (enabled) {\\n scheduleFrame();\\n }\\n }\\n\\n /// Ensures callbacks for [PlatformDispatcher.onBeginFrame] and\\n /// [PlatformDispatcher.onDrawFrame] are registered.\\n /// 中文:确保[PlatformDispatcher.onBeginFrame]和\\n /// 中文:[PlatformDispatcher.onDrawFrame]的回调已注册。\\n @protected\\n void ensureFrameCallbacksRegistered() {\\n platformDispatcher.onBeginFrame ??= _handleBeginFrame;\\n platformDispatcher.onDrawFrame ??= _handleDrawFrame;\\n }\\n\\n /// Schedules a new frame using [scheduleFrame] if this object is not\\n /// currently producing a frame.\\n ///\\n /// Calling this method ensures that [handleDrawFrame] will eventually be\\n /// called, unless it\'s already in progress.\\n ///\\n /// This has no effect if [schedulerPhase] is\\n /// [SchedulerPhase.transientCallbacks] or [SchedulerPhase.midFrameMicrotasks]\\n /// (because a frame is already being prepared in that case), or\\n /// [SchedulerPhase.persistentCallbacks] (because a frame is actively being\\n /// rendered in that case). It will schedule a frame if the [schedulerPhase]\\n /// is [SchedulerPhase.idle] (in between frames) or\\n /// [SchedulerPhase.postFrameCallbacks] (after a frame).\\n /// 中文:如果此对象当前没有产生帧,则使用[scheduleFrame]调度新帧。\\n ///\\n /// 中文:调用此方法确保[handleDrawFrame]最终会被调用,除非它已经在进行中。\\n ///\\n /// 中文:如果[schedulerPhase]是[SchedulerPhase.transientCallbacks]或\\n /// 中文:[SchedulerPhase.midFrameMicrotasks](因为在这种情况下帧已经在准备中),\\n /// 中文:或[SchedulerPhase.persistentCallbacks](因为在这种情况下帧正在积极\\n /// 中文:被渲染),则此方法没有效果。如果[schedulerPhase]是[SchedulerPhase.idle]\\n /// 中文:(在帧之间)或[SchedulerPhase.postFrameCallbacks](在帧之后),\\n /// 中文:它将调度一帧。\\n void ensureVisualUpdate() {\\n switch (schedulerPhase) {\\n case SchedulerPhase.idle:\\n case SchedulerPhase.postFrameCallbacks:\\n scheduleFrame();\\n return;\\n case SchedulerPhase.transientCallbacks:\\n case SchedulerPhase.midFrameMicrotasks:\\n case SchedulerPhase.persistentCallbacks:\\n return;\\n }\\n }\\n\\n
\\n /// If necessary, schedules a new frame by calling\\n /// [dart:ui.PlatformDispatcher.scheduleFrame].\\n ///\\n /// After this is called, the engine will (eventually) call\\n /// [handleBeginFrame]. (This call might be delayed, e.g. if the device\'s\\n /// screen is turned off it will typically be delayed until the screen is on\\n /// and the application is visible.) Calling this during a frame forces\\n /// another frame to be scheduled, even if the current frame has not yet\\n /// completed.\\n ///\\n /// Scheduled frames are serviced when triggered by a \\"Vsync\\" signal provided\\n /// by the operating system. The \\"Vsync\\" signal, or vertical synchronization\\n /// signal, was historically related to the display refresh, at a time when\\n /// hardware physically moved a beam of electrons vertically between updates\\n /// of the display. The operation of contemporary hardware is somewhat more\\n /// subtle and complicated, but the conceptual \\"Vsync\\" refresh signal continue\\n /// to be used to indicate when applications should update their rendering.\\n ///\\n /// To have a stack trace printed to the console any time this function\\n /// schedules a frame, set [debugPrintScheduleFrameStacks] to true.\\n ///\\n /// See also:\\n ///\\n /// * [scheduleForcedFrame], which ignores the [lifecycleState] when\\n /// scheduling a frame.\\n /// * [scheduleWarmUpFrame], which ignores the \\"Vsync\\" signal entirely and\\n /// triggers a frame immediately.\\n /// 中文:如有必要,通过调用[dart:ui.PlatformDispatcher.scheduleFrame]调度新帧。\\n ///\\n /// 中文:调用此方法后,引擎将(最终)调用[handleBeginFrame]。\\n /// 中文:(此调用可能会延迟,例如,如果设备的屏幕关闭,通常会延迟到屏幕打开\\n /// 中文:并且应用程序可见时。)在帧期间调用此方法会强制调度另一帧,\\n /// 中文:即使当前帧尚未完成。\\n ///\\n /// 中文:调度的帧在由操作系统提供的\\"Vsync\\"信号触发时被服务。\\n /// 中文:\\"Vsync\\"信号,或垂直同步信号,在历史上与显示刷新相关,\\n /// 中文:在硬件物理上在显示更新之间垂直移动电子束的时候。\\n /// 中文:现代硬件的操作有些更微妙和复杂,但概念上的\\"Vsync\\"刷新信号\\n /// 中文:继续用于指示应用程序应该何时更新其渲染。\\n ///\\n /// 中文:要在此函数调度帧时将堆栈跟踪打印到控制台,\\n /// 中文:请将[debugPrintScheduleFrameStacks]设置为true。\\n ///\\n /// 中文:另请参阅:\\n ///\\n /// 中文: * [scheduleForcedFrame],它在调度帧时忽略[lifecycleState]。\\n /// 中文: * [scheduleWarmUpFrame],它完全忽略\\"Vsync\\"信号并立即触发帧。\\n void scheduleFrame() {\\n if (_hasScheduledFrame || !framesEnabled) {\\n return;\\n }\\n assert(() {\\n if (debugPrintScheduleFrameStacks) {\\n debugPrintStack(label: \'scheduleFrame() called. Current phase is $schedulerPhase.\');\\n }\\n return true;\\n }());\\n ensureFrameCallbacksRegistered();\\n platformDispatcher.scheduleFrame();\\n _hasScheduledFrame = true;\\n }\\n\\n /// Schedules a new frame by calling\\n /// [dart:ui.PlatformDispatcher.scheduleFrame].\\n ///\\n /// After this is called, the engine will call [handleBeginFrame], even if\\n /// frames would normally not be scheduled by [scheduleFrame] (e.g. even if\\n /// the device\'s screen is turned off).\\n ///\\n /// The framework uses this to force a frame to be rendered at the correct\\n /// size when the phone is rotated, so that a correctly-sized rendering is\\n /// available when the screen is turned back on.\\n ///\\n /// To have a stack trace printed to the console any time this function\\n /// schedules a frame, set [debugPrintScheduleFrameStacks] to true.\\n ///\\n /// Prefer using [scheduleFrame] unless it is imperative that a frame be\\n /// scheduled immediately, since using [scheduleForcedFrame] will cause\\n /// significantly higher battery usage when the device should be idle.\\n ///\\n /// Consider using [scheduleWarmUpFrame] instead if the goal is to update the\\n /// rendering as soon as possible (e.g. at application startup).\\n /// 中文:通过调用[dart:ui.PlatformDispatcher.scheduleFrame]调度新帧。\\n ///\\n /// 中文:调用此方法后,引擎将调用[handleBeginFrame],即使帧通常不会\\n /// 中文:由[scheduleFrame]调度(例如,即使设备的屏幕关闭)。\\n ///\\n /// 中文:框架使用这个来强制在手机旋转时以正确的尺寸渲染帧,\\n /// 中文:以便在屏幕重新打开时有正确尺寸的渲染可用。\\n ///\\n /// 中文:要在此函数调度帧时将堆栈跟踪打印到控制台,\\n /// 中文:请将[debugPrintScheduleFrameStacks]设置为true。\\n ///\\n /// 中文:除非必须立即调度帧,否则优先使用[scheduleFrame],\\n /// 中文:因为使用[scheduleForcedFrame]会在设备应该空闲时\\n /// 中文:导致明显更高的电池使用率。\\n ///\\n /// 中文:如果目标是尽快更新渲染(例如在应用程序启动时),\\n /// 中文:请考虑使用[scheduleWarmUpFrame]。\\n void scheduleForcedFrame() {\\n if (_hasScheduledFrame) {\\n return;\\n }\\n assert(() {\\n if (debugPrintScheduleFrameStacks) {\\n debugPrintStack(label: \'scheduleForcedFrame() called. Current phase is $schedulerPhase.\');\\n }\\n return true;\\n }());\\n ensureFrameCallbacksRegistered();\\n platformDispatcher.scheduleFrame();\\n _hasScheduledFrame = true;\\n }\\n\\n
\\n /// If necessary, schedules a new frame by calling\\n /// [dart:ui.PlatformDispatcher.scheduleFrame].\\n ///\\n /// After this is called, the engine will (eventually) call\\n /// [handleBeginFrame]. (This call might be delayed, e.g. if the device\'s\\n /// screen is turned off it will typically be delayed until the screen is on\\n /// and the application is visible.) Calling this during a frame forces\\n /// another frame to be scheduled, even if the current frame has not yet\\n /// completed.\\n ///\\n /// Scheduled frames are serviced when triggered by a \\"Vsync\\" signal provided\\n /// by the operating system. The \\"Vsync\\" signal, or vertical synchronization\\n /// signal, was historically related to the display refresh, at a time when\\n /// hardware physically moved a beam of electrons vertically between updates\\n /// of the display. The operation of contemporary hardware is somewhat more\\n /// subtle and complicated, but the conceptual \\"Vsync\\" refresh signal continue\\n /// to be used to indicate when applications should update their rendering.\\n ///\\n /// To have a stack trace printed to the console any time this function\\n /// schedules a frame, set [debugPrintScheduleFrameStacks] to true.\\n ///\\n /// See also:\\n ///\\n /// * [scheduleForcedFrame], which ignores the [lifecycleState] when\\n /// scheduling a frame.\\n /// * [scheduleWarmUpFrame], which ignores the \\"Vsync\\" signal entirely and\\n /// triggers a frame immediately.\\n /// 中文:如有必要,通过调用[dart:ui.PlatformDispatcher.scheduleFrame]调度新帧。\\n ///\\n /// 中文:调用此方法后,引擎将(最终)调用[handleBeginFrame]。\\n /// 中文:(此调用可能会延迟,例如,如果设备的屏幕关闭,通常会延迟到屏幕打开\\n /// 中文:并且应用程序可见时。)在帧期间调用此方法会强制调度另一帧,\\n /// 中文:即使当前帧尚未完成。\\n ///\\n /// 中文:调度的帧在由操作系统提供的\\"Vsync\\"信号触发时被服务。\\n /// 中文:\\"Vsync\\"信号,或垂直同步信号,在历史上与显示刷新相关,\\n /// 中文:在硬件物理上在显示更新之间垂直移动电子束的时候。\\n /// 中文:现代硬件的操作有些更微妙和复杂,但概念上的\\"Vsync\\"刷新信号\\n /// 中文:继续用于指示应用程序应该何时更新其渲染。\\n ///\\n /// 中文:要在此函数调度帧时将堆栈跟踪打印到控制台,\\n /// 中文:请将[debugPrintScheduleFrameStacks]设置为true。\\n ///\\n /// 中文:另请参阅:\\n ///\\n /// 中文: * [scheduleForcedFrame],它在调度帧时忽略[lifecycleState]。\\n /// 中文: * [scheduleWarmUpFrame],它完全忽略\\"Vsync\\"信号并立即触发帧。\\n void scheduleFrame() {\\n if (_hasScheduledFrame || !framesEnabled) {\\n return;\\n }\\n assert(() {\\n if (debugPrintScheduleFrameStacks) {\\n debugPrintStack(label: \'scheduleFrame() called. Current phase is $schedulerPhase.\');\\n }\\n return true;\\n }());\\n ensureFrameCallbacksRegistered();\\n platformDispatcher.scheduleFrame();\\n _hasScheduledFrame = true;\\n }\\n\\n /// Schedules a new frame by calling\\n /// [dart:ui.PlatformDispatcher.scheduleFrame].\\n ///\\n /// After this is called, the engine will call [handleBeginFrame], even if\\n /// frames would normally not be scheduled by [scheduleFrame] (e.g. even if\\n /// the device\'s screen is turned off).\\n ///\\n /// The framework uses this to force a frame to be rendered at the correct\\n /// size when the phone is rotated, so that a correctly-sized rendering is\\n /// available when the screen is turned back on.\\n ///\\n /// To have a stack trace printed to the console any time this function\\n /// schedules a frame, set [debugPrintScheduleFrameStacks] to true.\\n ///\\n /// Prefer using [scheduleFrame] unless it is imperative that a frame be\\n /// scheduled immediately, since using [scheduleForcedFrame] will cause\\n /// significantly higher battery usage when the device should be idle.\\n ///\\n /// Consider using [scheduleWarmUpFrame] instead if the goal is to update the\\n /// rendering as soon as possible (e.g. at application startup).\\n /// 中文:通过调用[dart:ui.PlatformDispatcher.scheduleFrame]调度新帧。\\n ///\\n /// 中文:调用此方法后,引擎将调用[handleBeginFrame],即使帧通常不会\\n /// 中文:由[scheduleFrame]调度(例如,即使设备的屏幕关闭)。\\n ///\\n /// 中文:框架使用这个来强制在手机旋转时以正确的尺寸渲染帧,\\n /// 中文:以便在屏幕重新打开时有正确尺寸的渲染可用。\\n ///\\n /// 中文:要在此函数调度帧时将堆栈跟踪打印到控制台,\\n /// 中文:请将[debugPrintScheduleFrameStacks]设置为true。\\n ///\\n /// 中文:除非必须立即调度帧,否则优先使用[scheduleFrame],\\n /// 中文:因为使用[scheduleForcedFrame]会在设备应该空闲时\\n /// 中文:导致明显更高的电池使用率。\\n ///\\n /// 中文:如果目标是尽快更新渲染(例如在应用程序启动时),\\n /// 中文:请考虑使用[scheduleWarmUpFrame]。\\n void scheduleForcedFrame() {\\n if (_hasScheduledFrame) {\\n return;\\n }\\n assert(() {\\n if (debugPrintScheduleFrameStacks) {\\n debugPrintStack(label: \'scheduleForcedFrame() called. Current phase is $schedulerPhase.\');\\n }\\n return true;\\n }());\\n ensureFrameCallbacksRegistered();\\n platformDispatcher.scheduleFrame();\\n _hasScheduledFrame = true;\\n }\\n
\\n bool _warmUpFrame = false;\\n\\n /// Schedule a frame to run as soon as possible, rather than waiting for\\n /// the engine to request a frame in response to a system \\"Vsync\\" signal.\\n ///\\n /// This is used during application startup so that the first frame (which is\\n /// likely to be quite expensive) gets a few extra milliseconds to run.\\n ///\\n /// Locks events dispatching until the scheduled frame has completed.\\n ///\\n /// If a frame has already been scheduled with [scheduleFrame] or\\n /// [scheduleForcedFrame], this call may delay that frame.\\n ///\\n /// If any scheduled frame has already begun or if another\\n /// [scheduleWarmUpFrame] was already called, this call will be ignored.\\n ///\\n /// Prefer [scheduleFrame] to update the display in normal operation.\\n ///\\n /// ## Design discussion\\n ///\\n /// The Flutter engine prompts the framework to generate frames when it\\n /// receives a request from the operating system (known for historical reasons\\n /// as a vsync). However, this may not happen for several milliseconds after\\n /// the app starts (or after a hot reload). To make use of the time between\\n /// when the widget tree is first configured and when the engine requests an\\n /// update, the framework schedules a _warm-up frame_.\\n ///\\n /// A warm-up frame may never actually render (as the engine did not request\\n /// it and therefore does not have a valid context in which to paint), but it\\n /// will cause the framework to go through the steps of building, laying out,\\n /// and painting, which can together take several milliseconds. Thus, when the\\n /// engine requests a real frame, much of the work will already have been\\n /// completed, and the framework can generate the frame with minimal\\n /// additional effort.\\n ///\\n /// Warm-up frames are scheduled by [runApp] on startup, and by\\n /// [RendererBinding.performReassemble] during a hot reload.\\n ///\\n /// Warm-up frames are also scheduled when the framework is unblocked by a\\n /// call to [RendererBinding.allowFirstFrame] (corresponding to a call to\\n /// [RendererBinding.deferFirstFrame] that blocked the rendering).\\n /// 中文:调度一个尽快运行的帧,而不是等待引擎响应系统\\"Vsync\\"信号请求帧。\\n ///\\n /// 中文:这在应用程序启动期间使用,以便第一帧(可能相当昂贵)\\n /// 中文:获得几毫秒的额外运行时间。\\n ///\\n /// 中文:锁定事件分派,直到调度的帧完成。\\n ///\\n /// 中文:如果已经使用[scheduleFrame]或[scheduleForcedFrame]调度了帧,\\n /// 中文:此调用可能会延迟该帧。\\n ///\\n /// 中文:如果任何调度的帧已经开始,或者另一个[scheduleWarmUpFrame]\\n /// 中文:已经被调用,此调用将被忽略。\\n ///\\n /// 中文:在正常操作中,优先使用[scheduleFrame]更新显示。\\n ///\\n /// 中文:## 设计讨论\\n ///\\n /// 中文:Flutter引擎在收到来自操作系统的请求(由于历史原因称为vsync)时,\\n /// 中文:提示框架生成帧。然而,这可能在应用程序启动(或热重载)后\\n /// 中文:几毫秒内不会发生。为了利用小部件树首次配置和引擎请求\\n /// 中文:更新之间的时间,框架调度了一个_预热帧_。\\n ///\\n /// 中文:预热帧可能永远不会真正渲染(因为引擎没有请求它,\\n /// 中文:因此没有有效的上下文进行绘制),但它会使框架经历构建、布局\\n /// 中文:和绘制的步骤,这些步骤加起来可能需要几毫秒。因此,当引擎\\n /// 中文:请求真实帧时,大部分工作已经完成,框架可以以最小的\\n /// 中文:额外努力生成帧。\\n ///\\n /// 中文:预热帧由[runApp]在启动时调度,并由\\n /// 中文:[RendererBinding.performReassemble]在热重载期间调度。\\n ///\\n /// 中文:当框架被调用[RendererBinding.allowFirstFrame]解除阻塞时\\n /// 中文:(对应于调用[RendererBinding.deferFirstFrame]阻塞渲染),\\n /// 中文:也会调度预热帧。\\n void scheduleWarmUpFrame() {\\n if (_warmUpFrame || schedulerPhase != SchedulerPhase.idle) {\\n return;\\n }\\n\\n _warmUpFrame = true;\\n TimelineTask? debugTimelineTask;\\n if (!kReleaseMode) {\\n debugTimelineTask = TimelineTask()..start(\'Warm-up frame\');\\n }\\n final bool hadScheduledFrame = _hasScheduledFrame;\\n PlatformDispatcher.instance.scheduleWarmUpFrame(\\n beginFrame: () {\\n assert(_warmUpFrame);\\n handleBeginFrame(null);\\n },\\n drawFrame: () {\\n assert(_warmUpFrame);\\n handleDrawFrame();\\n // We call resetEpoch after this frame so that, in the hot reload case,\\n // the very next frame pretends to have occurred immediately after this\\n // warm-up frame. The warm-up frame\'s timestamp will typically be far in\\n // the past (the time of the last real frame), so if we didn\'t reset the\\n // epoch we would see a sudden jump from the old time in the warm-up frame\\n // to the new time in the \\"real\\" frame. The biggest problem with this is\\n // that implicit animations end up being triggered at the old time and\\n // then skipping every frame and finishing in the new time.\\n // 中文:我们在此帧之后调用resetEpoch,以便在热重载的情况下,\\n // 中文:下一帧假装在此预热帧之后立即发生。预热帧的时间戳通常\\n // 中文:会远在过去(上一个真实帧的时间),所以如果我们不重置纪元,\\n // 中文:我们会看到从预热帧中的旧时间到\\"真实\\"帧中的新时间的突然跳跃。\\n // 中文:最大的问题是隐式动画最终在旧时间被触发,然后跳过每一帧\\n // 中文:并在新时间完成。\\n resetEpoch();\\n _warmUpFrame = false;\\n if (hadScheduledFrame) {\\n scheduleFrame();\\n }\\n },\\n );\\n\\n // Lock events so touch events etc don\'t insert themselves until the\\n // scheduled frame has finished.\\n // 中文:锁定事件,以便触摸事件等不会插入,直到调度的帧完成。\\n lockEvents(() async {\\n await endOfFrame;\\n if (!kReleaseMode) {\\n debugTimelineTask!.finish();\\n }\\n });\\n }\\n
\\n Duration? _firstRawTimeStampInEpoch;\\n Duration _epochStart = Duration.zero;\\n Duration _lastRawTimeStamp = Duration.zero;\\n\\n /// Prepares the scheduler for a non-monotonic change to how time stamps are\\n /// calculated.\\n ///\\n /// Callbacks received from the scheduler assume that their time stamps are\\n /// monotonically increasing. The raw time stamp passed to [handleBeginFrame]\\n /// is monotonic, but the scheduler might adjust those time stamps to provide\\n /// [timeDilation]. Without careful handling, these adjusts could cause time\\n /// to appear to run backwards.\\n ///\\n /// The [resetEpoch] function ensures that the time stamps are monotonic by\\n /// resetting the base time stamp used for future time stamp adjustments to the\\n /// current value. For example, if the [timeDilation] decreases, rather than\\n /// scaling down the [Duration] since the beginning of time, [resetEpoch] will\\n /// ensure that we only scale down the duration since [resetEpoch] was called.\\n ///\\n /// Setting [timeDilation] calls [resetEpoch] automatically. You don\'t need to\\n /// call [resetEpoch] yourself.\\n /// 中文:准备调度器进行时间戳计算方式的非单调变化。\\n ///\\n /// 中文:从调度器接收的回调假设它们的时间戳是单调递增的。\\n /// 中文:传递给[handleBeginFrame]的原始时间戳是单调的,但调度器\\n /// 中文:可能会调整这些时间戳以提供[timeDilation]。如果不仔细处理,\\n /// 中文:这些调整可能导致时间看起来向后运行。\\n ///\\n /// 中文:[resetEpoch]函数通过重置用于未来时间戳调整的基础时间戳\\n /// 中文:为当前值,确保时间戳是单调的。例如,如果[timeDilation]减小,\\n /// 中文:[resetEpoch]将确保我们只缩小自[resetEpoch]被调用以来的持续时间,\\n /// 中文:而不是从时间开始以来的[Duration]。\\n ///\\n /// 中文:设置[timeDilation]会自动调用[resetEpoch]。你不需要\\n /// 中文:自己调用[resetEpoch]。\\n void resetEpoch() {\\n _epochStart = _adjustForEpoch(_lastRawTimeStamp);\\n _firstRawTimeStampInEpoch = null;\\n }\\n\\n /// Adjusts the given time stamp into the current epoch.\\n ///\\n /// This both offsets the time stamp to account for when the epoch started\\n /// (both in raw time and in the epoch\'s own time line) and scales the time\\n /// stamp to reflect the time dilation in the current epoch.\\n ///\\n /// These mechanisms together combine to ensure that the durations we give\\n /// during frame callbacks are monotonically increasing.\\n /// 中文:将给定的时间戳调整到当前纪元。\\n ///\\n /// 中文:这既偏移时间戳以考虑纪元开始的时间\\n /// 中文:(在原始时间和纪元自己的时间线中),又缩放时间戳\\n /// 中文:以反映当前纪元中的时间膨胀。\\n ///\\n /// 中文:这些机制共同确保我们在帧回调期间给出的持续时间是单调递增的。\\n Duration _adjustForEpoch(Duration rawTimeStamp) {\\n final Duration rawDurationSinceEpoch = _firstRawTimeStampInEpoch == null ? Duration.zero : rawTimeStamp - _firstRawTimeStampInEpoch!;\\n return Duration(microseconds: (rawDurationSinceEpoch.inMicroseconds / timeDilation).round() + _epochStart.inMicroseconds);\\n }\\n\\n /// The time stamp for the frame currently being processed.\\n ///\\n /// This is only valid while between the start of [handleBeginFrame] and the\\n /// end of the corresponding [handleDrawFrame], i.e. while a frame is being\\n /// produced.\\n /// 中文:当前正在处理的帧的时间戳。\\n ///\\n /// 中文:这仅在[handleBeginFrame]的开始和相应的[handleDrawFrame]的\\n /// 中文:结束之间有效,即在产生帧时。\\n Duration get currentFrameTimeStamp {\\n assert(_currentFrameTimeStamp != null);\\n return _currentFrameTimeStamp!;\\n }\\n Duration? _currentFrameTimeStamp;\\n\\n /// The raw time stamp as provided by the engine to\\n /// [dart:ui.PlatformDispatcher.onBeginFrame] for the frame currently being\\n /// processed.\\n ///\\n /// Unlike [currentFrameTimeStamp], this time stamp is neither adjusted to\\n /// offset when the epoch started nor scaled to reflect the [timeDilation] in\\n /// the current epoch.\\n ///\\n /// On most platforms, this is a more or less arbitrary value, and should\\n /// generally be ignored. On Fuchsia, this corresponds to the system-provided\\n /// presentation time, and can be used to ensure that animations running in\\n /// different processes are synchronized.\\n /// 中文:引擎提供给[dart:ui.PlatformDispatcher.onBeginFrame]的\\n /// 中文:当前正在处理的帧的原始时间戳。\\n ///\\n /// 中文:与[currentFrameTimeStamp]不同,此时间戳既不调整为\\n /// 中文:偏移纪元开始的时间,也不缩放以反映当前纪元中的[timeDilation]。\\n ///\\n /// 中文:在大多数平台上,这是一个或多或少的任意值,通常应该被忽略。\\n /// 中文:在Fuchsia上,这对应于系统提供的呈现时间,可用于确保\\n /// 中文:在不同进程中运行的动画是同步的。\\n Duration get currentSystemFrameTimeStamp {\\n return _lastRawTimeStamp;\\n }\\n\\n int _debugFrameNumber = 0;\\n String? _debugBanner;\\n\\n // Whether the current engine frame needs to be postponed till after the\\n // warm-up frame.\\n //\\n // Engine may begin a frame in the middle of the warm-up frame because the\\n // warm-up frame is scheduled by timers while the engine frame is scheduled\\n // by platform specific frame scheduler (e.g. `requestAnimationFrame` on the\\n // web). When this happens, we let the warm-up frame finish, and postpone the\\n // engine frame.\\n // 中文:当前引擎帧是否需要推迟到预热帧之后。\\n //\\n // 中文:引擎可能在预热帧中间开始一帧,因为预热帧是由计时器调度的,\\n // 中文:而引擎帧是由平台特定的帧调度器调度的(例如,在网络上的\\n // 中文:`requestAnimationFrame`)。当这种情况发生时,我们让预热帧\\n // 中文:完成,并推迟引擎帧。\\n bool _rescheduleAfterWarmUpFrame = false;\\n\\n void _handleBeginFrame(Duration rawTimeStamp) {\\n if (_warmUpFrame) {\\n // \\"begin frame\\" and \\"draw frame\\" must strictly alternate. Therefore\\n // _rescheduleAfterWarmUpFrame cannot possibly be true here as it is\\n // reset by _handleDrawFrame.\\n // 中文:\\"begin frame\\"和\\"draw frame\\"必须严格交替。因此\\n // 中文:_rescheduleAfterWarmUpFrame不可能在这里为true,因为它\\n // 中文:被_handleDrawFrame重置。\\n assert(!_rescheduleAfterWarmUpFrame);\\n _rescheduleAfterWarmUpFrame = true;\\n return;\\n }\\n handleBeginFrame(rawTimeStamp);\\n }\\n\\n void _handleDrawFrame() {\\n if (_rescheduleAfterWarmUpFrame) {\\n _rescheduleAfterWarmUpFrame = false;\\n // Reschedule in a post-frame callback to allow the draw-frame phase of\\n // the warm-up frame to finish.\\n // 中文:在帧后回调中重新调度,以允许预热帧的draw-frame阶段完成。\\n addPostFrameCallback((Duration timeStamp) {\\n // Force an engine frame.\\n //\\n // We need to reset _hasScheduledFrame here because we cancelled the\\n // original engine frame, and therefore did not run handleBeginFrame\\n // who is responsible for resetting it. So if a frame callback set this\\n // to true in the \\"begin frame\\" part of the warm-up frame, it will\\n // still be true here and cause us to skip scheduling an engine frame.\\n // 中文:强制引擎帧。\\n //\\n // 中文:我们需要在这里重置_hasScheduledFrame,因为我们取消了\\n // 中文:原始引擎帧,因此没有运行负责重置它的handleBeginFrame。\\n // 中文:所以如果帧回调在预热帧的\\"begin frame\\"部分将其设置为true,\\n // 中文:它在这里仍然为true,并导致我们跳过调度引擎帧。\\n _hasScheduledFrame = false;\\n scheduleFrame();\\n }, debugLabel: \'SchedulerBinding.scheduleFrame\');\\n return;\\n }\\n handleDrawFrame();\\n }\\n\\n final TimelineTask? _frameTimelineTask = kReleaseMode ? null : TimelineTask();\\n
\\n /// Called by the engine to prepare the framework to produce a new frame.\\n ///\\n /// This function calls all the transient frame callbacks registered by\\n /// [scheduleFrameCallback]. It then returns, any scheduled microtasks are run\\n /// (e.g. handlers for any [Future]s resolved by transient frame callbacks),\\n /// and [handleDrawFrame] is called to continue the frame.\\n ///\\n /// If the given time stamp is null, the time stamp from the last frame is\\n /// reused.\\n ///\\n /// To have a banner shown at the start of every frame in debug mode, set\\n /// [debugPrintBeginFrameBanner] to true. The banner will be printed to the\\n /// console using [debugPrint] and will contain the frame number (which\\n /// increments by one for each frame), and the time stamp of the frame. If the\\n /// given time stamp was null, then the string \\"warm-up frame\\" is shown\\n /// instead of the time stamp. This allows frames eagerly pushed by the\\n /// framework to be distinguished from those requested by the engine in\\n /// response to the \\"Vsync\\" signal from the operating system.\\n ///\\n /// You can also show a banner at the end of every frame by setting\\n /// [debugPrintEndFrameBanner] to true. This allows you to distinguish log\\n /// statements printed during a frame from those printed between frames (e.g.\\n /// in response to events or timers).\\n /// 中文:由引擎调用,准备框架产生新帧。\\n ///\\n /// 中文:此函数调用由[scheduleFrameCallback]注册的所有瞬态帧回调。\\n /// 中文:然后它返回,运行任何调度的微任务(例如,由瞬态帧回调解析的\\n /// 中文:任何[Future]的处理程序),并调用[handleDrawFrame]继续帧。\\n ///\\n /// 中文:如果给定的时间戳为null,则重用上一帧的时间戳。\\n ///\\n /// 中文:要在调试模式下在每帧开始时显示横幅,请设置\\n /// 中文:[debugPrintBeginFrameBanner]为true。横幅将使用[debugPrint]\\n /// 中文:打印到控制台,并将包含帧号(每帧递增一个)和帧的时间戳。\\n /// 中文:如果给定的时间戳为null,则显示字符串\\"warm-up frame\\"\\n /// 中文:而不是时间戳。这允许区分框架急切推送的帧和引擎响应\\n /// 中文:操作系统的\\"Vsync\\"信号请求的帧。\\n ///\\n /// 中文:你也可以通过设置[debugPrintEndFrameBanner]为true在每帧结束时\\n /// 中文:显示横幅。这允许你区分在帧期间打印的日志语句和在帧之间\\n /// 中文:打印的日志语句(例如,响应事件或计时器)。\\n void handleBeginFrame(Duration? rawTimeStamp) {\\n _frameTimelineTask?.start(\'Frame\');\\n _firstRawTimeStampInEpoch ??= rawTimeStamp;\\n _currentFrameTimeStamp = _adjustForEpoch(rawTimeStamp ?? _lastRawTimeStamp);\\n if (rawTimeStamp != null) {\\n _lastRawTimeStamp = rawTimeStamp;\\n }\\n\\n assert(() {\\n _debugFrameNumber += 1;\\n\\n if (debugPrintBeginFrameBanner || debugPrintEndFrameBanner) {\\n final StringBuffer frameTimeStampDescription = StringBuffer();\\n if (rawTimeStamp != null) {\\n _debugDescribeTimeStamp(_currentFrameTimeStamp!, frameTimeStampDescription);\\n } else {\\n frameTimeStampDescription.write(\'(warm-up frame)\');\\n }\\n _debugBanner = \'▄▄▄▄▄▄▄▄ Frame ${_debugFrameNumber.toString().padRight(7)} ${frameTimeStampDescription.toString().padLeft(18)} ▄▄▄▄▄▄▄▄\';\\n if (debugPrintBeginFrameBanner) {\\n debugPrint(_debugBanner);\\n }\\n }\\n return true;\\n }());\\n\\n assert(schedulerPhase == SchedulerPhase.idle);\\n _hasScheduledFrame = false;\\n try {\\n // TRANSIENT FRAME CALLBACKS\\n // 中文:瞬态帧回调\\n _frameTimelineTask?.start(\'Animate\');\\n _schedulerPhase = SchedulerPhase.transientCallbacks;\\n final Map<int, _FrameCallbackEntry> callbacks = _transientCallbacks;\\n _transientCallbacks = <int, _FrameCallbackEntry>{};\\n callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) {\\n if (!_removedIds.contains(id)) {\\n _invokeFrameCallback(callbackEntry.callback, _currentFrameTimeStamp!, callbackEntry.debugStack);\\n }\\n });\\n _removedIds.clear();\\n } finally {\\n _schedulerPhase = SchedulerPhase.midFrameMicrotasks;\\n }\\n }\\n\\n DartPerformanceMode? _performanceMode;\\n int _numPerformanceModeRequests = 0;\\n\\n /// Request a specific [DartPerformanceMode].\\n ///\\n /// Returns `null` if the request was not successful due to conflicting performance mode requests.\\n /// Two requests are said to be in conflict if they are not of the same [DartPerformanceMode] type,\\n /// and an explicit request for a performance mode has been made prior.\\n ///\\n /// Requestor is responsible for calling [PerformanceModeRequestHandle.dispose] when it no longer\\n /// requires the performance mode.\\n /// 中文:请求特定的[DartPerformanceMode]。\\n ///\\n /// 中文:如果由于冲突的性能模式请求而导致请求不成功,则返回`null`。\\n /// 中文:如果两个请求不是相同的[DartPerformanceMode]类型,\\n /// 中文:并且之前已经明确请求了性能模式,则称它们冲突。\\n ///\\n /// 中文:请求者负责在不再需要性能模式时调用[PerformanceModeRequestHandle.dispose]。\\n PerformanceModeRequestHandle? requestPerformanceMode(DartPerformanceMode mode) {\\n // conflicting requests are not allowed.\\n // 中文:不允许冲突的请求。\\n if (_performanceMode != null && _performanceMode != mode) {\\n return null;\\n }\\n\\n if (_performanceMode == mode) {\\n assert(_numPerformanceModeRequests > 0);\\n _numPerformanceModeRequests++;\\n } else if (_performanceMode == null) {\\n assert(_numPerformanceModeRequests == 0);\\n _performanceMode = mode;\\n _numPerformanceModeRequests = 1;\\n }\\n\\n return PerformanceModeRequestHandle._(_disposePerformanceModeRequest);\\n }\\n\\n /// Remove a request for a specific [DartPerformanceMode].\\n ///\\n /// If all the pending requests have been disposed, the engine will revert to the\\n /// [DartPerformanceMode.balanced] performance mode.\\n /// 中文:删除对特定[DartPerformanceMode]的请求。\\n ///\\n /// 中文:如果所有待处理的请求都已处置,引擎将恢复为\\n /// 中文:[DartPerformanceMode.balanced]性能模式。\\n void _disposePerformanceModeRequest() {\\n _numPerformanceModeRequests--;\\n if (_numPerformanceModeRequests == 0) {\\n _performanceMode = null;\\n PlatformDispatcher.instance.requestDartPerformanceMode(DartPerformanceMode.balanced);\\n }\\n }\\n
\\n /// Returns the current [DartPerformanceMode] requested or `null` if no requests have\\n /// been made.\\n ///\\n /// This is only supported in debug and profile modes, returns `null` in release mode.\\n /// 中文:返回当前请求的[DartPerformanceMode],如果没有请求,则返回`null`。\\n ///\\n /// 中文:这仅在调试和配置文件模式下支持,在发布模式下返回`null`。\\n DartPerformanceMode? debugGetRequestedPerformanceMode() {\\n if (!(kDebugMode || kProfileMode)) {\\n return null;\\n } else {\\n return _performanceMode;\\n }\\n }\\n\\n /// Called by the engine to produce a new frame.\\n ///\\n /// This method is called immediately after [handleBeginFrame]. It calls all\\n /// the callbacks registered by [addPersistentFrameCallback], which typically\\n /// drive the rendering pipeline, and then calls the callbacks registered by\\n /// [addPostFrameCallback].\\n ///\\n /// See [handleBeginFrame] for a discussion about debugging hooks that may be\\n /// useful when working with frame callbacks.\\n /// 中文:由引擎调用以产生新帧。\\n ///\\n /// 中文:此方法在[handleBeginFrame]之后立即调用。它调用所有\\n /// 中文:由[addPersistentFrameCallback]注册的回调,这些回调通常\\n /// 中文:驱动渲染管道,然后调用由[addPostFrameCallback]注册的回调。\\n ///\\n /// 中文:有关处理帧回调时可能有用的调试钩子的讨论,请参见[handleBeginFrame]。\\n void handleDrawFrame() {\\n assert(_schedulerPhase == SchedulerPhase.midFrameMicrotasks);\\n _frameTimelineTask?.finish(); // end the \\"Animate\\" phase\\n try {\\n // PERSISTENT FRAME CALLBACKS\\n // 中文:持久帧回调\\n _schedulerPhase = SchedulerPhase.persistentCallbacks;\\n for (final FrameCallback callback in List<FrameCallback>.of(_persistentCallbacks)) {\\n _invokeFrameCallback(callback, _currentFrameTimeStamp!);\\n }\\n\\n // POST-FRAME CALLBACKS\\n // 中文:帧后回调\\n _schedulerPhase = SchedulerPhase.postFrameCallbacks;\\n final List<FrameCallback> localPostFrameCallbacks =\\n List<FrameCallback>.of(_postFrameCallbacks);\\n _postFrameCallbacks.clear();\\n if (!kReleaseMode) {\\n FlutterTimeline.startSync(\'POST_FRAME\');\\n }\\n try {\\n for (final FrameCallback callback in localPostFrameCallbacks) {\\n _invokeFrameCallback(callback, _currentFrameTimeStamp!);\\n }\\n } finally {\\n if (!kReleaseMode) {\\n FlutterTimeline.finishSync();\\n }\\n }\\n } finally {\\n _schedulerPhase = SchedulerPhase.idle;\\n _frameTimelineTask?.finish(); // end the Frame\\n assert(() {\\n if (debugPrintEndFrameBanner) {\\n debugPrint(\'▀\' * _debugBanner!.length);\\n }\\n _debugBanner = null;\\n return true;\\n }());\\n _currentFrameTimeStamp = null;\\n }\\n }\\n\\n void _profileFramePostEvent(FrameTiming frameTiming) {\\n postEvent(\'Flutter.Frame\', <String, dynamic>{\\n \'number\': frameTiming.frameNumber,\\n \'startTime\': frameTiming.timestampInMicroseconds(FramePhase.buildStart),\\n \'elapsed\': frameTiming.totalSpan.inMicroseconds,\\n \'build\': frameTiming.buildDuration.inMicroseconds,\\n \'raster\': frameTiming.rasterDuration.inMicroseconds,\\n \'vsyncOverhead\': frameTiming.vsyncOverhead.inMicroseconds,\\n });\\n }\\n\\n static void _debugDescribeTimeStamp(Duration timeStamp, StringBuffer buffer) {\\n if (timeStamp.inDays > 0) {\\n buffer.write(\'${timeStamp.inDays}d \');\\n }\\n if (timeStamp.inHours > 0) {\\n buffer.write(\'${timeStamp.inHours - timeStamp.inDays * Duration.hoursPerDay}h \');\\n }\\n if (timeStamp.inMinutes > 0) {\\n buffer.write(\'${timeStamp.inMinutes - timeStamp.inHours * Duration.minutesPerHour}m \');\\n }\\n if (timeStamp.inSeconds > 0) {\\n buffer.write(\'${timeStamp.inSeconds - timeStamp.inMinutes * Duration.secondsPerMinute}s \');\\n }\\n buffer.write(\'${timeStamp.inMilliseconds - timeStamp.inSeconds * Duration.millisecondsPerSecond}\');\\n final int microseconds = timeStamp.inMicroseconds - timeStamp.inMilliseconds * Duration.microsecondsPerMillisecond;\\n if (microseconds > 0) {\\n buffer.write(\'.${microseconds.toString().padLeft(3, \\"0\\")}\');\\n }\\n buffer.write(\'ms\');\\n }\\n\\n
\\n // Calls the given [callback] with [timestamp] as argument.\\n //\\n // Wraps the callback in a try/catch and forwards any error to\\n // [debugSchedulerExceptionHandler], if set. If not set, prints\\n // the error.\\n // 中文:使用[timestamp]作为参数调用给定的[callback]。\\n //\\n // 中文:将回调包装在try/catch中,并将任何错误转发给\\n // 中文:[debugSchedulerExceptionHandler](如果设置)。如果未设置,则打印错误。\\n @pragma(\'vm:notify-debugger-on-exception\')\\n void _invokeFrameCallback(FrameCallback callback, Duration timeStamp, [ StackTrace? callbackStack ]) {\\n assert(_FrameCallbackEntry.debugCurrentCallbackStack == null);\\n assert(() {\\n _FrameCallbackEntry.debugCurrentCallbackStack = callbackStack;\\n return true;\\n }());\\n try {\\n callback(timeStamp);\\n } catch (exception, exceptionStack) {\\n FlutterError.reportError(FlutterErrorDetails(\\n exception: exception,\\n stack: exceptionStack,\\n library: \'scheduler library\',\\n context: ErrorDescription(\'during a scheduler callback\'),\\n // 中文:在调度器回调期间\\n informationCollector: (callbackStack == null) ? null : () {\\n return <DiagnosticsNode>[\\n DiagnosticsStackTrace(\\n \'\\\\nThis exception was thrown in the context of a scheduler callback. \'\\n \'When the scheduler callback was _registered_ (as opposed to when the \'\\n \'exception was thrown), this was the stack\',\\n // 中文:\\\\n这个异常是在调度器回调的上下文中抛出的。\\n // 中文:当调度器回调被_注册_(而不是异常被抛出)时,这是堆栈\\n callbackStack,\\n ),\\n ];\\n },\\n ));\\n }\\n assert(() {\\n _FrameCallbackEntry.debugCurrentCallbackStack = null;\\n return true;\\n }());\\n }\\n}\\n\\n/// The default [SchedulingStrategy] for [SchedulerBinding.schedulingStrategy].\\n///\\n/// If there are any frame callbacks registered, only runs tasks with\\n/// a [Priority] of [Priority.animation] or higher. Otherwise, runs\\n/// all tasks.\\n/// 中文:[SchedulerBinding.schedulingStrategy]的默认[SchedulingStrategy]。\\n///\\n/// 中文:如果有任何帧回调注册,则只运行[Priority]为[Priority.animation]\\n/// 中文:或更高的任务。否则,运行所有任务。\\nbool defaultSchedulingStrategy({ required int priority, required SchedulerBinding scheduler }) {\\n if (scheduler.transientCallbackCount > 0) {\\n return priority >= Priority.animation.value;\\n }\\n return true;\\n}\\n\\n
\\n这个类是 Flutter 框架的核心部分,负责管理帧调度、任务调度和时间处理等关键功能。
","description":"// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be\\n// found in the LICENSE file.\\n// 中文:版权所有 2014 The Flutter Authors。保留所有权利。\\n// 中文:本代码的使用受 BSD 许可证的约束,该许可证可在\\n// 中文:LICENSE 文件中找到。\\n\\n/// @docImport…","guid":"https://juejin.cn/post/7495699805676093491","author":"Nicholas68","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-22T06:44:03.008Z","media":null,"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"SchedulerBinding原理","url":"https://juejin.cn/post/7495660376580472858","content":"SchedulerBinding 是 Flutter 框架中负责协调帧调度的核心组件,它管理着应用程序的渲染时机和各种回调的执行顺序。下面我将详细分析 SchedulerBinding 的调度原理和状","description":"SchedulerBinding 是 Flutter 框架中负责协调帧调度的核心组件,它管理着应用程序的渲染时机和各种回调的执行顺序。下面我将详细分析 SchedulerBinding 的调度原理和状","guid":"https://juejin.cn/post/7495660376580472858","author":"Nicholas68","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-22T06:33:38.277Z","media":null,"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"flutter 路由跳转动画设置","url":"https://juejin.cn/post/7495694163037519887","content":"路由动画设置 如何设置flutter路由动画,以及如何自定义路由动画。页面退出动画和新页面进入动画自定义","description":"路由动画设置 如何设置flutter路由动画,以及如何自定义路由动画。页面退出动画和新页面进入动画自定义","guid":"https://juejin.cn/post/7495694163037519887","author":"衿璃","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-22T06:05:57.743Z","media":null,"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"使用ffmpeg-kit 自己构建ffmpeg,并在flutter本地引用记录","url":"https://juejin.cn/post/7495368038209667084","content":"ffmpeg-kit
作为项目中使用ffmpeg的工具集合库,很多人都在使用,因为他不但提供了构建各平台的构建脚本,可以快速的构建自己需要的一整套ffmpeg库,也提供了一批构建好的各平台原生二进制文件版本,为开发者提供了很大的便利,但因为一些原因,作者删除了已存的原生二进制文件,导致我们无法直接在项目中引用。好在还保留着源代码和构建脚本,因此我们可以自己动手构建,并且在本地引用。
事实上我觉得这篇文章有点多余,因为官方的文档其实看看也知道怎么做了,不过就当做一个日常记录了吧。
\\n我尊贵的Mac M2pro
使用git拉取ffmpeg-kit源码(注意:一定是要通过git clone下来的,不要直接下载zip)。
安装和下载好xcode、Homebrew、AndroidSDK和NDK这些。
使用brew install安装好autoconf、libtool、pkg-config等等,还有一些是需要通过pip3安装的,这些具体看编译时的build.log文件报错信息来确定,没多少,缺什么装什么就行。
clone下来ffmpeg-kit之后,进入项目目录,就能看到android.sh
、ios.sh
、macos.sh
、linux.sh
这些构建脚本。
在clone的ffmpeg-kit目录下打开终端命令窗口,然后执行构建命令脚本,一开始执行的时候会自动关联下载很多东西,会有一段时间的等待,慢慢来不着急。
\\n在拼接构建命令之前,先通过执行./macos.sh -h
来查看支持的架构和其他选项的默认状态,然后按需选择和启动或者关闭。
\\n这里是我
macos
平台的命令,这个是在ffmpeg-kit
原有的min-gpl
版本上多启动了个libwebp
,大家按需来拼接命令就行,具体命令有什么不懂的可以让AI
来释义一下,有工具我们就要好好利用起来!
./macos.sh --enable-gpl --enable-libvidstab --enable-x264 --enable-x265 --enable-xvidcore --enable-libwebp --enable-macos-bzip2 --enable-macos-audiotoolbox --enable-macos-avfoundation --enable-macos-coreimage --enable-macos-opencl --enable-macos-opengl --enable-macos-libiconv --enable-macos-videotoolbox --enable-macos-zlib --target=10.15 --xcframework --disable-x86-64\\n
\\n构建结果如下,很成功!,我们可以在根目录下的prebuilt
文件夹中找到对应的产物bundle-apple-xcframework-macos
文件夹。\\n
这就是我们想要的xcframework
文件\\n
同样的,我们可以参照上面macos
平台的构建,来构建ios平台的xcframework
文件,首先要强调的还是要执行一下./ios.sh -h
来查看支持的架构和其他选项的默认状态,然后按需选择和启动或者关闭。\\n这里我就不再截图了,就写一下我拼接的命令,同样是在ffmpeg-kit
原有的min-gpl
版本上多启动了个libwebp
。
./ios.sh --enable-gpl --enable-libvidstab --enable-x264 --enable-x265 --enable-xvidcore --enable-libwebp --enable-ios-bzip2 --enable-ios-audiotoolbox --enable-ios-avfoundation --enable-ios-libiconv --enable-ios-videotoolbox --enable-ios-zlib --target=12.1 --xcframework --disable-armv7 --disable-armv7s --disable-arm64e --disable-i386 --disable-x86-64 --disable-x86-64-mac-catalyst\\n
\\n构建成功之后可以在根目录下的prebuilt
文件夹中找到对应的产物bundle-apple-xcframework-ios
文件夹。
对于android
平台的构建可能有点区别,我们需要下载AndroidSDK
、以及所需的NDK
版本。建议大家安装AndroidStudio
,以及搜索查询相关的AndroidSDK
及NDK
如何下载。通过AndroidStudio
的SDK Manager
下载指定的NDK
版本,这里我用的是24.0.8215888
版本。
SDK
和NDK
下载安装完成后,接下来去配置好对应的Mac环境变量
就行。
export ANDROID_SDK_ROOT=\\"$HOME/Library/Android/sdk\\"\\nexport ANDROID_NDK_ROOT=\\"$ANDROID_SDK_ROOT/ndk/24.0.8215888\\"\\n
\\n完成上述操作后,接下来就可以执行我们的android.sh
构建脚本了。\\n同样的操作,还是要强调的还是要执行一下./android.sh -h
来查看支持的架构和其他选项的默认状态,然后按需选择和启动或者关闭。
./android.sh --enable-gpl --enable-libvidstab --enable-x264 --enable-x265 --enable-xvidcore --enable-libwebp --enable-android-media-codec --enable-android-zlib --disable-arm-v7a --disable-arm-v7a-neon --disable-x86 --disable-x86-64\\n
\\n这里因为我只需要arm64-v8a
架构,所以就关闭了其他架构。
\\n同样的构建成功之后可以在根目录下的
prebuilt
文件夹中找到对应的产物bundle-android-aar
文件夹。
到此就全部大功告成了!接下来我们就来看看如何把这些构建产物在本地统一封装成我们原汁原味的ffmpeg_kit_flutter
插件。
其实这对于熟悉ios
或者android
原生的人来说,并不是什么问题,但对于部分无原生经验的跨平台开发者来说,原生知识依旧是他们最大的障碍,那么接下来就把我踩坑的经验分享一下吧,虽然很菜,但是略尽绵薄之力,哈哈。
首先我们找到clone的ffmpeg_kit文件夹下的flutter
文件夹,路径是这个ffmpeg-kit/flutter/
,这个路径下有一个flutter文件夹就是ffmpeg-kit的flutter插件源码,我们可以将这个插件源码文件夹重命名为ffmpeg-kit-flutter
以做区分。
接下来使用代码编辑器打开这份代码,开始做本地化修改。
\\n我们在ffmpeg-kit-flutter
目录下的macos
目录下新建Frameworks
文件夹,然后将构建好的bundle-apple-xcframework-macos
目录下的xcframework
文件全部复制进来,然后修改podspec
配置文件,建议将这个文件重命名一下,这里我重命名为ffmpeg_kit_flutter.podspec
。
具体修改后的内容如下(有些信息可能是多余的,但是不影响,具体释义可以自行让AI
讲解一下):
Pod::Spec.new do |s|\\n s.name = \'ffmpeg_kit_flutter\'\\n s.version = \'6.0.3\'\\n s.summary = \'FFmpeg Kit for Flutter\'\\n s.description = \'A Flutter plugin for running FFmpeg and FFprobe commands.\'\\n s.homepage = \'https://github.com/arthenica/ffmpeg-kit\'\\n s.license = { :file => \'../LICENSE\' }\\n s.author = { \'ARTHENICA\' => \'open-source@arthenica.com\' }\\n\\n s.platform = :osx\\n s.requires_arc = true\\n s.static_framework = true\\n\\n s.source = { :path => \'.\' }\\n s.source_files = \'Classes/**/*\'\\n s.public_header_files = \'Classes/**/*.h\'\\n\\n s.default_subspec = \'ffmpeg_kit_mac_local\'\\n\\n s.subspec \'ffmpeg_kit_mac_local\' do |ss|\\n ss.libraries = [\\"z\\", \\"bz2\\", \\"c++\\", \\"iconv\\"]\\n ss.osx.frameworks = [\\n \\"AudioToolbox\\",\\n \\"AVFoundation\\",\\n \\"CoreAudio\\",\\n \\"CoreImage\\",\\n \\"CoreMedia\\",\\n \\"OpenCL\\",\\n \\"OpenGL\\",\\n \\"VideoToolbox\\"\\n ]\\n ss.vendored_frameworks = \'Frameworks/ffmpegkit.xcframework\', \'Frameworks/libavcodec.xcframework\', \'Frameworks/libavdevice.xcframework\', \'Frameworks/libavfilter.xcframework\', \'Frameworks/libavformat.xcframework\', \'Frameworks/libavutil.xcframework\', \'Frameworks/libswresample.xcframework\', \'Frameworks/libswscale.xcframework\'\\n ss.osx.deployment_target = \'10.15\'\\n end\\n\\n s.dependency \'FlutterMacOS\'\\n s.pod_target_xcconfig = { \'DEFINES_MODULE\' => \'YES\' }\\n s.osx.deployment_target = \'10.15\'\\n\\nend\\n
\\n同样的我们在ffmpeg-kit-flutter
目录下的ios
目录下新建Frameworks
文件夹,然后将构建好的bundle-apple-xcframework-ios
目录下的xcframework
文件全部复制进来,然后修改podspec
配置文件,建议将这个文件重命名一下,这里我重命名为ffmpeg_kit_flutter.podspec
。
具体修改后的内容如下(有些信息可能是多余的,但是不影响,具体释义可以自行让AI
讲解一下):
Pod::Spec.new do |s|\\n s.name = \'ffmpeg_kit_flutter\'\\n s.version = \'6.0.3\'\\n s.summary = \'FFmpeg Kit for Flutter\'\\n s.description = \'A Flutter plugin for running FFmpeg and FFprobe commands.\'\\n s.homepage = \'https://github.com/arthenica/ffmpeg-kit\'\\n s.license = { :file => \'../LICENSE\' }\\n s.author = { \'ARTHENICA\' => \'open-source@arthenica.com\' }\\n\\n s.platform = :ios\\n s.requires_arc = true\\n s.static_framework = true\\n\\n s.source = { :path => \'.\' }\\n s.source_files = \'Classes/**/*\'\\n s.public_header_files = \'Classes/**/*.h\'\\n\\n s.default_subspec = \'ffmpeg_kit_ios_local\'\\n\\n s.subspec \'ffmpeg_kit_ios_local\' do |ss|\\n ss.libraries = [\\"z\\", \\"bz2\\", \\"c++\\", \\"iconv\\"]\\n ss.osx.frameworks = [\\n \\"AudioToolbox\\",\\n \\"AVFoundation\\",\\n \\"CoreMedia\\",\\n \\"VideoToolbox\\"\\n ]\\n ss.vendored_frameworks = \'Frameworks/ffmpegkit.xcframework\', \'Frameworks/libavcodec.xcframework\', \'Frameworks/libavdevice.xcframework\', \'Frameworks/libavfilter.xcframework\', \'Frameworks/libavformat.xcframework\', \'Frameworks/libavutil.xcframework\', \'Frameworks/libswresample.xcframework\', \'Frameworks/libswscale.xcframework\'\\n ss.ios.deployment_target = \'12.1\'\\n end\\n\\n s.dependency \'Flutter\'\\n s.pod_target_xcconfig = { \'DEFINES_MODULE\' => \'YES\', \'EXCLUDED_ARCHS[sdk=iphonesimulator*]\' => \'i386\' }\\n\\nend\\n
\\n我们在ffmpeg-kit-flutter
目录下的android
目录下新建libs
文件夹,然后将构建好的bundle-android-aar/ffmpeg-kit/
目录下的ffmpeg-kit.aar
文件复制进来,然后修改build.gradle
配置文件。
这里有两个点需要强调一下:
\\n1、如何在flutter插件中的android工程中引入aar文件
答案就是通过在添加flatDir的方式,如下:
\\n...\\nrootProject.allprojects {\\n repositories {\\n google()\\n mavenCentral()\\n flatDir {\\n dirs project(\':ffmpeg_kit_flutter\').file(\'libs\')\\n }\\n }\\n}\\n...\\ndependencies {\\n implementation \'androidx.annotation:annotation:1.5.0\'\\n implementation(name: \'ffmpeg-kit\', ext: \'aar\')\\n implementation \'com.arthenica:smart-exception-java:0.2.1\'\\n}\\n
\\n这段代码直接添加在插件的android目录下的build.gradle文件内就行,想具体了解可以让AI
来释义一下,通过举一反三,其他场景下也可以用这种方式,需要强调的是一定要添加这段配置:
flatDir {dirs project(\':插件name
\'\').file(\'libs\')}
2、在本地化的过程中我们要注意原来远程引用的pom文件,里面可能引用了其他库
如在这里,远程中的pom文件中引用了com.arthenica:smart-exception-java
,如果我们没有把引用下来,运行后就后出现异常状况,所以遇到类似场景大家也需要注意。
完整的build.gradle配置如下:
\\nbuildscript {\\n repositories {\\n google()\\n mavenCentral()\\n }\\n\\n dependencies {\\n classpath \'com.android.tools.build:gradle:8.1.0\'\\n }\\n}\\n\\nrootProject.allprojects {\\n repositories {\\n google()\\n mavenCentral()\\n flatDir {\\n dirs project(\':ffmpeg_kit_flutter\').file(\'libs\')\\n }\\n }\\n}\\n\\napply plugin: \'com.android.library\'\\n\\nandroid {\\n // Conditional for compatibility with AGP <4.2.\\n if (project.android.hasProperty(\\"namespace\\")) {\\n namespace \'com.arthenica.ffmpegkit.flutter\'\\n }\\n\\n compileSdkVersion 33\\n\\n defaultConfig {\\n minSdkVersion 24\\n targetSdkVersion 33\\n versionCode 603\\n versionName \\"6.0.3\\"\\n }\\n\\n buildTypes {\\n release {\\n minifyEnabled false\\n }\\n }\\n lintOptions {\\n disable \'GradleCompatible\'\\n }\\n compileOptions {\\n sourceCompatibility JavaVersion.VERSION_1_8\\n targetCompatibility JavaVersion.VERSION_1_8\\n }\\n}\\n\\ndependencies {\\n implementation \'androidx.annotation:annotation:1.5.0\'\\n implementation(name: \'ffmpeg-kit\', ext: \'aar\')\\n implementation \'com.arthenica:smart-exception-java:0.2.1\'\\n}\\n
\\n最后就是怎么使用了,实际上也就是flutter如何引入本地插件了,直接在我们flutter项目根目录下的pubspec.yaml文件中添加:
\\ndependencies:\\n...\\nffmpeg_kit_flutter:\\n path: 你的插件路径/ffmpeg_kit_flutter\\n \\n
\\n以上内容就是全部的ffmpeg-kit自构建以及修改配置作为本地flutter插件引用的全部内容了,虽然感觉整篇文章可能很多余,大部分人估计都会,但是毕竟自己遇到了而且不会,那么踩完坑之后就当做一下记录了,兴许能帮到一些跟我一样的人呢。
\\n最后留一下ffmpeg_kit_flutter
本地化的插件示例代码链接,可根据自身需要自行调整。
该文档是基于MacOS系统来记录的一个搭建Android平台运行flutter代码的教程
\\nxcode-select --install\\n
\\nsudo xcodebuild -license\\n
\\n/bin/bash -c \\"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\\"\\n
\\nbrew tap flutter/flutter\\nbrew install flutter\\n
\\n~/development/flutter
)。echo \'export PATH=\\"$PATH: ~/development/flutter/bin\\"\' >> ~/.zshrc # 或 ~/.bash_profile\\nsource ~/.zshrc\\n
\\n这个Path是环境变量,是flutter解压后的地址。如果配置不正确的话,会导致flutter命令不起作用。
\\n安装 CocoaPods(用于 iOS 依赖管理):
\\nsudo gem install cocoapods\\n
\\n安装Android studio(用于Android开发环境),由于我是Android开发,所以我就只搭建Android的开发环境了。
\\n\\nflutter doctor\\n
\\n如果出现结果:
\\n➜ ~ flutter doctor\\nDoctor summary (to see all details, run flutter doctor -v):\\n[✓] Flutter (Channel stable, 3.29.2, on macOS 15.3.2 24D81 darwin-arm64, locale en-CN)\\n[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)\\n[!] Xcode - develop for iOS and macOS (Xcode 16.3)\\n ✗ CocoaPods not installed.\\n CocoaPods is a package manager for iOS or macOS platform code.\\n Without CocoaPods, plugins will not work on iOS or macOS.\\n For more info, see https://flutter.dev/to/platform-plugins\\n For installation instructions, see\\n https://guides.cocoapods.org/using/getting-started.html#installation\\n[✓] Chrome - develop for the web\\n[✓] Android Studio (version 2024.3)\\n[✓] VS Code (version 1.99.0)\\n[✓] Connected device (3 available)\\n[✓] Network resources\\n\\n! Doctor found issues in 1 category.\\n
\\n[✓] 表示已经好了,
\\n✗ 表示不行,如果是IOS开发,想要在电脑上运行IOS平台,则必须保证IOS的相关配置都好了。
\\n如果是Android开发,就得保证Androd的所有配置都好了。上面这个输出结果则说明你的Android平台的环境是没有问题的。
\\n但如果出现结果输出结果是:
\\n➜ ~ flutter doctor\\nDoctor summary (to see all details, run flutter doctor -v):\\n[✓] Flutter (Channel stable, 3.29.2, on macOS 15.3.2 24D81 darwin-arm64, locale en-CN)\\n[!] Android toolchain - develop for Android devices (Android SDK version 34.0.0)\\n ✗ cmdline-tools component is missing\\n Run `path/to/sdkmanager --install \\"cmdline-tools;latest\\"`\\n See https://developer.android.com/studio/command-line for more details.\\n ✗ Android license status unknown.\\n Run `flutter doctor --android-licenses` to accept the SDK licenses.\\n See https://flutter.dev/to/macos-android-setup for more details.\\n[!] Xcode - develop for iOS and macOS (Xcode 16.3)\\n ✗ CocoaPods not installed.\\n CocoaPods is a package manager for iOS or macOS platform code.\\n Without CocoaPods, plugins will not work on iOS or macOS.\\n For more info, see https://flutter.dev/to/platform-plugins\\n For installation instructions, see\\n https://guides.cocoapods.org/using/getting-started.html#installation\\n[✓] Chrome - develop for the web\\n[✓] Android Studio (version 2024.3)\\n[✓] VS Code (version 1.99.0)\\n[✓] Connected device (3 available)\\n[✓] Network resources\\n
\\n则2个环境都有问题。下面是关于如何解决Android 环境的问题。
\\n下载安装之后再命令执行
\\nflutter doctor --android-licenses\\n
\\n执行之后会有一些条款需要同意并输入:y。如:
\\nAccept? (y/N): \\n
\\n你就一直输入y就好了,直到出现。
\\nAll SDK package licenses accepted.\\n
\\n理论上会出现最开始的那个结果。
\\n至此Android 平台运行flutter的开发环境就没什么问题 。
\\n安装完以上几个插件,基本的开发就没什么问题了。
\\n运行Flutter docutor的时候,未找到flutter命令
\\n说明flutter的环境变量没有配置好,重新检查一下flutter环境变量的路径是否配置好了。最好是通过brew的方式安装fluter,这样命令会自动配置环境变量。
\\n第一次创建Flutter项目的时候,可能会出现运行特别慢的问题。
\\n这是因为第一次运行需要下载一些平台的依赖,不确定IOS是怎么样,反正Androd平台需要下载很多的依赖,如果网络不好的话,光gradle就得下载好久。
\\n如果是Android平台,则可以通过打开新项目的方式去打开Flutter目录下android这个平台,然后点击sync的方式去加载。
\\n保护API密钥对于防止未经授权的访问和滥用你的应用及其用户至关重要。在Flutter中,你可以采用多种策略来有效保护你的API密钥。
\\n保护API密钥的策略
\\n环境变量大致有两种方法,一种是像使用flutter_dotenv
这类的文件插件,另一种是直接使用--dart-define
或者--dart-fine-from-file
。
步骤:
\\nflutter_dotenv
包添加到你的项目中以管理环境变量。dependencies:\\n flutter:\\n sdk: flutter\\n flutter_dotenv: ^5.0.2\\n
\\nAPI_KEY=your_api_key_here\\n
\\nimport \'package:flutter_dotenv/flutter_dotenv.dart\';\\n\\nFuture<void> main() async {\\n await dotenv.load(fileName: \\".env\\"); // 首先在main函数中加载dotenv\\n runApp(MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n final apiKey = dotenv.env[\'API_KEY\']; // 创建变量\\n return MaterialApp(\\n home: Scaffold(\\n body: Center(child: Text(\'API Key: $apiKey\')), // 打印并使用它\\n ),\\n );\\n }\\n}\\n
\\n--dart-define
或者 --dart-fine-from-file
如果变量的数量较少,可以考虑使用--dart-define
,直接在运行App时直接传入就好了:
flutter run --dart-define API_URL=\\"your url\\"\\n
\\n如果创建变量比较多,那就可以考虑创建.env
或者json
文件了,然后把在版本控制工具中忽略掉就好了。假如文件名字就叫.env
,可以如下方式指定启动app:
flutter run --dart-define-from-file=path/to/.env\\n
\\n在dart中我们可以通过如下代码获取:
\\nfinal class Env {\\n static const String apiBaseUrl = String.fromEnvironment(\'API_BASE_URL\');\\n}\\n\\n
\\n将API密钥存储在安全存储中可以确保其不易被访问,从而提供额外的安全层。
\\ndependencies:\\n flutter:\\n sdk: flutter\\n flutter_secure_storage: ^5.0.2\\n
\\nimport \'package:flutter_secure_storage/flutter_secure_storage.dart\';\\n\\nfinal storage = FlutterSecureStorage(); // 初始化FlutterSecureStorage\\n\\n// 写入API密钥\\nawait storage.write(key: \'api_key\', value: \'your_api_key_here\'); // 存储它\\n\\n// 读取API密钥\\nString? apiKey = await storage.read(key: \'api_key\'); // 读取并使用它\\n
\\n3. 后端代理
\\n一种更安全的方法是完全避免在客户端存储API密钥,而是使用后端服务器与第三方服务进行交互,这也是比实际工作中比较推荐的方式。
\\nconst express = require(\'express\');\\nconst axios = require(\'axios\');\\nconst app = express();\\nconst port = 3000;\\nconst API_KEY = \'your_api_key_here\';\\n\\napp.get(\'/api/data\', async (req, res) => {\\n try {\\n const response = await axios.get(`https://api.example.com/data?apiKey=${API_KEY}`);\\n res.json(response.data);\\n } catch (error) {\\n res.status(500).send(\'Error fetching data\');\\n }\\n});\\n\\napp.listen(port, () => {\\n console.log(`Server running on http://localhost:${port}`);\\n});\\n
\\nimport \'package:http/http.dart\' as http;\\n\\nFuture<void> fetchData() async {\\n final response = await http.get(Uri.parse(\'http://localhost:3000/api/data\'));\\n if (response.statusCode == 200) {\\n // 处理数据\\n } else {\\n // 处理错误\\n }\\n}\\n
\\n4. 代码混淆
\\n需要注意的是使用代码混淆并不能完全保护API密钥,但它可以增加逆向工程的难度。混淆是通过将代码转换为难以理解的形式来实现的。
\\npubspec.yaml
开启用混淆。flutter:\\n obfuscate: true\\n split-debug-info: /path/to/debug - info\\n
\\nflutter build apk --obfuscate --split-debug-info=/path/to/debug-info\\nflutter build ipa --obfuscate --split-debug-info=/path/to/debug-info\\n
\\nconst String apiKey = \'your_api_key_here\';\\n
\\n/lib/config.dart\\n
\\nimport \'config.dart\';\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n home: Scaffold(\\n body: Center(child: Text(\'API Key: $apiKey\')),\\n ),\\n );\\n }\\n}\\n
\\n结论
\\n通过采用这些策略,我们可以提高Flutter应用中API密钥的安全性。但最安全的方法是使用后端代理,正如flutter_dotenv
的文档中所说:
\\n\\n像API密钥和令牌这样的敏感密钥不应存储在Flutter应用中。即使经过混淆处理,它们仍可能被提取。目前,此库不会对变量进行混淆,因为这可能会让使用者产生错误的安全感。请在前端应用中使用环境变量来存储非敏感的配置值,例如API端点和功能标志。
\\n
\\n\\n原文:Sensitive keys like API keys and tokens should not be stored in your Flutter app. They can be extracted even if obfuscated. This libary currently does not obfuscate variables as it may lull the consumers into a false sense of security. Use environment variables on the frontend application for non-sensitive configuration values, such as API endpoints and feature flags.
\\n
但结合实际情况,很多时候我们可能并不具备存到后端的条件,所以其他方式也是备选方案,我们也可以结合多种方法可以提供额外的安全层。
\\n顺便提一下,欢迎关注我的微信公众号OpenFlutter。
","description":"前言 保护API密钥对于防止未经授权的访问和滥用你的应用及其用户至关重要。在Flutter中,你可以采用多种策略来有效保护你的API密钥。\\n\\n保护API密钥的策略\\n\\n环境变量:使用环境变量可使你的密钥不包含在源代码中,从而降低暴露的风险。\\n安全存储:将API密钥存储在安全存储中可确保它们不易被访问,提供了额外的安全层。\\n后端代理:一种更安全的方法是完全避免在客户端存储密钥。相反,使用后端服务器与第三方服务进行交互。\\n代码混淆:通过混淆Dart代码会使攻击者更难对应用进行逆向工程。\\n使用配置文件:将密钥存储在不在版本控制中的配置文件中…","guid":"https://juejin.cn/post/7495268887970660390","author":"JarvanMo","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-21T02:50:10.408Z","media":null,"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"【Flutter 状态管理 - 伍】 | 万字长文解锁你对观察者模式的认知","url":"https://juejin.cn/post/7494834508216516627","content":"你是否遇到过这种场景?
\\n某个核心数据一变,就得像催债一样挨个调用十几处关联模块的更新方法。新增一个功能,就得在原始类里硬塞一行调用代码。时间一长,类膨胀成庞然大物,维护代码像在沼泽里挣扎 —— 越改越陷得深。
观察者模式就是来治这个病的。它把这种“单向依赖”
的硬编码,变成“自由订阅”
的灵活关系。数据发布方不再关心谁要听消息,监听方也不用死等轮询。就像微信群的@
所有人:想听的进群,嫌吵的退群,群主发完消息就能甩手走人。
从GUI
事件到微服务通信,从Spring
框架到Kafka
消息队列,观察者模式的影子无处不在。它不是银弹,但绝对是解开“数据变动连锁反应”
的一把关键钥匙。
本文前部分基于Java
代码讲述,后部分基于Dart
代码讲述,通过各种不同的实践增强对观察者模式的理解!!!
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\n在学习观察着模式之前,我们先来看这样一个需求:有一个天气监测系统,当温度变化时,需要更新多个显示设备(如手机、网页)。
\\n直男式硬编码实现:
\\n// 主题类:天气数据,负责管理数据和通知\\nclass WeatherData {\\n private float temperature;\\n \\n // 直接依赖具体观察者对象\\n private PhoneDisplay phoneDisplay = new PhoneDisplay();\\n private WebDisplay webDisplay = new WebDisplay();\\n\\n // 数据变化时,手动调用所有观察者的更新方法\\n public void setMeasurements(float temperature) {\\n this.temperature = temperature;\\n // 硬核连环调用\\n phoneDisplay.update(temperature);\\n webDisplay.update(temperature);\\n }\\n}\\n\\n// 观察者实现类:写死具体逻辑\\nclass PhoneDisplay {\\n public void update(float temperature) {\\n System.out.println(\\"[手机] 温度:\\" + temperature);\\n }\\n}\\n\\nclass WebDisplay {\\n public void update(float temperature) {\\n System.out.println(\\"[网页] 温度:\\" + temperature);\\n }\\n}\\n\\n// 使用示例:代码一跑,生死难料\\npublic class Main {\\n public static void main(String[] args) {\\n WeatherData weatherData = new WeatherData();\\n weatherData.setMeasurements(25.5f);\\n }\\n}\\n
\\n\\n问题1:
WeatherData
类直接绑定 PhoneDisplay
、WebDisplay
等具体类。如果此时增加了一个新需求,添加一个 SmartWatchDisplay
?那就必须改 WeatherData
的代码,动一处而崩全局。如果想删一个 WebDisplay
?注释掉一行代码,系统就敢给你甩个空指针异常。
\\n\\n本质:代码紧耦合到窒息,违背面向对象设计的依赖倒置原则「依赖抽象,而非实现」。
\\n
问题2: 每次新增/删除
观察者都要修改 WeatherData
类。若需求变更时,开发者像在雷区拆弹 —— 稍有不慎全盘皆炸;系统维护成本指数级上升,改个需求等于重写代码。
\\n\\n本质:修改及维护困难,违背面向对象设计的开闭原则「对扩展开放,对修改关闭」。
\\n
问题3: setMeasurements
方法里重复调用 update
。\\n若有10
个观察者?写10
行 xxx.update()
,程序员变打字员;并且逻辑分散,改个参数得在所有调用处同步修改,漏一处就埋雷。
\\n\\n本质:冗余的硬编码调用。
\\n
问题4: 观察者列表写死在代码里。如果用户想运行时关闭手机通知?除非重启服务,否则没门;若临时加个日志观察者?改代码→
编译→
上线→
祈祷别出Bug
。
\\n\\n本质:无法动态管理观察者。
\\n
问题5: WeatherData
和显示逻辑深度绑定。想复用 WeatherData
做股票行情系统?只能重写,CV
大法都救不了;相似功能重复造轮子,团队开发效率直接砍半。
\\n\\n本质:代码复用性差。
\\n
场景1:产品经理要求加个「温度过高自动报警」功能。
\\nWeatherData
里插入一行 alarm.update()
。alamr.update()
,系统深夜疯狂报错,运维提刀上门。场景2:团队新人接手代码,看到 WeatherData
类里密密麻麻的 update()
调用。
直接问题: 紧耦合、难扩展、重复代码、无法动态管理。
\\n深层危害: 团队协作噩梦,开发效率低下,系统稳定性如履薄冰。
\\n唯一出路: 重构为观察者模式,用抽象接口解耦,让代码能呼吸、能生长、能抗揍。
\\n观察者模式(Observer Pattern
)是一种行为设计模式,用于在对象之间建立一对多的依赖关系,当一个对象(被观察者)的状态发生改变时,所有依赖于它的对象(观察者)都会自动收到通知并更新。
核心思想:
\\n这一模式的关键在于将变化的传播逻辑(通知
)与业务逻辑(状态更新
)分离,使系统更灵活、可扩展。
\\n\\n本质是通过抽象依赖关系,实现对象间的一对多动态通知机制。
\\n
观察者模式的核心目标是解耦被观察者与观察者,而解耦的关键是依赖抽象接口而非具体实现。
\\n基于这一原则,该模式通常会细化为四个角色:
\\n实现上述目标的一种比较直观的方式如下:
\\n此形式就像「订阅-推送」机制。举个栗子:你关注公众号(注册),后台把你的账号存进列表(容器)。公众号发新文章(被观察者变化),自动群发通知所有粉丝(通知)。你取关(撤销注册)后就不再接收消息。
\\n核心三点:
\\nAndroid
用户和iOS
用户都能订阅同一个号。优势:灵活扩展,新增或移除观察者无需修改被观察者代码,适合数据变动触发多对象联动的场景(如UI更新、消息推送)。
\\nSubject
:抽象主题/被观察者职责:定义观察者的注册、移除和通知接口。
\\n必要性分析:
\\nSubject
接口,而非具体主题类,降低耦合度。设计原则:
\\n抽象化
):通过接口
或抽象类
定义主题的行为,而非依赖具体实现。管理观察者
和触发通知
,不处理具体业务逻辑。代码实现:
\\npublic interface Subject {\\n void registerObserver(Observer observer);\\n void removeObserver(Observer observer);\\n void notifyObservers();\\n}\\n
\\nConcreteSubject
:具体主题/被观察者职责:实现Subject
接口,管理具体状态,并在状态变化时触发通知。
必要性分析:
\\n代码实现:
\\nclass WeatherData implements Subject {\\n private List<Observer> observers = new ArrayList<>();\\n private float temperature;\\n\\n public void setTemperature(float temperature) {\\n this.temperature = temperature;\\n notifyObservers();\\n }\\n\\n @Override\\n public void registerObserver(Observer observer) {\\n observers.add(observer);\\n }\\n\\n @Override\\n public void removeObserver(Observer observer) {\\n observers.remove(observer);\\n }\\n\\n @Override\\n public void notifyObservers() {\\n for (Observer observer : observers) {\\n observer.update(temperature);\\n }\\n }\\n}\\n
\\nObserver
:观察者职责:定义统一的更新接口(如update()
),供主题调用。
必要性分析:
\\nupdate()
方法,无需关心观察者的具体实现,减少依赖。设计原则:
\\n代码实现:
\\npublic interface Observer {\\n void update(float temperature);\\n}\\n
\\nConcreteObserver
:具体观察者职责:实现Observer
接口,定义具体的响应逻辑。
必要性分析:
\\n代码实现:
\\nclass PhoneDisplay implements Observer {\\n @Override\\n public void update(float temperature) {\\n System.out.println(\\"[手机] 温度: \\" + temperature + \\"°C\\" );\\n }\\n}\\n\\nclass WebDisplay implements Observer {\\n @Override\\n public void update(float temperature) {\\n System.out.println(\\"[网页] 温度: \\" + temperature + \\"°C\\");\\n }\\n}\\n\\n// 使用示例\\npublic class Main {\\n\\n public static void main(String[] args) {\\n WeatherData weatherData = new WeatherData();\\n PhoneDisplay phone = new PhoneDisplay();\\n WebDisplay web = new WebDisplay();\\n\\n weatherData.registerObserver(phone);\\n weatherData.registerObserver(web);\\n\\n weatherData.setTemperatures(25.5f);\\n\\n weatherData.removeObserver(web);\\n\\n weatherData.setTemperature(26.5f);\\n }\\n}\\n\\n输出:\\n[手机] 温度: 25.5°C\\n[网页] 温度: 25.5°C\\n[手机] 温度: 26.5°C\\n
\\nWeatherData
不依赖具体的 PhoneDisplay
或 WebDisplay
,仅依赖 Observer
接口。LEDDisplay
)时,只需实现 Observer
接口并注册到主题,无需修改 WeatherData
。WeatherData
,观察者自行处理更新。目标:解决实际开发中的扩展性
、性能
、安全
等问题。
Push
) vs
拉数据(Pull
)推模型:主题将数据直接传递给观察者(通过方法参数
)。
public interface Observer {\\n void update(float temperature); // 推模型:参数传递数据\\n}\\n
\\n拉模型:观察者主动从主题获取数据(通过主题的公共方法
)。
public interface Observer {\\n void update(); // 观察者调用 subject.getTemperature() 拉取数据\\n}\\n
\\n选择依据:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n场景 | 推模型 | 拉模型 |
---|---|---|
数据量小 | 适合(如传递温度、状态码) | 冗余(需多次调用Getter ) |
数据量大或结构复杂 | 冗余(参数臃肿) | 适合(观察者按需取用) |
观察者需要不同数据子集 | 不灵活(需传递所有数据) | 灵活(观察者自行选择所需数据) |
主题数据变更频繁 | 可能频繁触发推送(资源浪费) | 观察者控制拉取时机(节省资源) |
代码优化实现:
\\n// 推模型:传递事件对象(封装复杂数据)\\nclass WeatherEvent {\\n private float temperature;\\n private float humidity; //增加湿度\\n // getters, constructor\\n}\\n\\ninterface Observer {\\n void update(WeatherEvent event); // 推模型优化\\n}\\n\\n// 拉模型:主题提供数据访问接口\\nclass WeatherData {\\n public float getTemperature() { /* ... */ }\\n public float getHumidity() { /* ... */ }\\n}\\n\\nclass Display implements Observer {\\n private WeatherData data;\\n \\n @Override\\n public void update() {\\n float temp = data.getTemperature(); // 拉取数据\\n float humidity = data.getHumidity();\\n }\\n}\\n
\\n核心问题:
\\nConcurrentModificationException
。解决方案:
\\n// 1、使用线程安全集合\\nprivate List<Observer> observers = new CopyOnWriteArrayList<>(); // 写时复制,避免并发修改\\n\\n// 2、同步代码块\\npublic synchronized void registerObserver(Observer o) {\\n observers.add(o);\\n}\\n\\npublic void notifyObservers() {\\n List<Observer> copy;\\n synchronized (this) {\\n copy = new ArrayList<>(observers); // 复制快照\\n }\\n for (Observer o : copy) {\\n o.update();\\n }\\n}\\n\\n// 3、异步通知\\nprivate ExecutorService executor = Executors.newFixedThreadPool(4);\\n\\npublic void notifyObservers() {\\n for (Observer o : observers) {\\n executor.submit(() -> o.update()); // 异步执行\\n }\\n}\\n
\\n核心问题:
\\n解决方案:
\\n1、显式注销观察者:适用于观察者生命周期明确(如UI组件、请求处理器)。
\\nclass ObserverImpl implements Observer {\\n private Subject subject;\\n \\n public ObserverImpl(Subject subject) {\\n this.subject = subject;\\n subject.registerObserver(this);\\n }\\n \\n public void destroy() {\\n subject.removeObserver(this); // 显式注销\\n }\\n}\\n
\\n2、弱引用(WeakReference
):
class WeakSubject {\\n private List<WeakReference<Observer>> observers = new ArrayList<>();\\n \\n public void addObserver(Observer o) {\\n observers.add(new WeakReference<>(o));\\n }\\n \\n public void notifyObservers() {\\n Iterator<WeakReference<Observer>> it = observers.iterator();\\n while (it.hasNext()) {\\n Observer o = it.next().get();\\n if (o != null) {\\n o.update();\\n } else {\\n it.remove(); // 自动清理无效引用\\n }\\n }\\n }\\n}\\n
\\n3、使用虚引用(PhantomReference
) + 引用队列:适用于需要精准控制观察者回收的高级场景。
ReferenceQueue<Observer> queue = new ReferenceQueue<>();\\nList<PhantomReference<Observer>> refs = new ArrayList<>();\\n\\npublic void addObserver(Observer o) {\\n refs.add(new PhantomReference<>(o, queue));\\n}\\n\\n// 定期清理队列中的已回收观察者\\npublic void clean() {\\n Reference<? extends Observer> ref;\\n while ((ref = queue.poll()) != null) {\\n refs.remove(ref);\\n }\\n}\\n
\\n核心问题 | 解决方法 | 典型场景 | 关键细节 |
---|---|---|---|
数据传递方式 | 推:数据直接塞给观察者 拉:观察者自己动手拿 | 推送温度/状态码等小数据 拉取数据库查询结果等大块数据 | 推模式别传可变对象(如集合) 拉模式给观察者开数据查询权限 |
多线程炸雷 | 用写时复制列表(CopyOnWriteArrayList ) 异步通知走线程池 | 高并发订单状态更新 实时数据监控大屏 | 异步任务加try-catch 防崩溃 别在回调里操作原始数据列表 |
内存泄漏陷阱 | 生命周期结束必须反注册 弱引用兜底( WeakReference ) | Android 的Activity 监听 临时统计模块 | 用RxJava 的Disposable 统一管理 定期清理弱引用僵尸(搭配 ReferenceQueue ) |
把观察者模式揉碎了,塞进架构里
Event Bus
)事件总线就像个快递分拣中心,所有模块把消息往这儿一扔,谁爱听谁自己领。本质上是个升级版观察者模式,但更灵活,支持跨模块、跨层级通信。
\\n场景:订单支付成功后,通知库存、物流、积分系统。
\\n手搓一个乞丐版EventBus
:
import java.util.*;\\nimport java.util.function.Consumer;\\n\\n// 事件总线核心\\npublic class EventBus {\\n // 事件类型 → 处理函数列表\\n private Map<Class<?>, List<Consumer<Object>>> handlers = new HashMap<>();\\n\\n // 订阅事件:告诉总线,XX类型的事件来了,就调我这个处理函数\\n public <T> void subscribe(Class<T> eventType, Consumer<T> handler) {\\n List<Consumer<Object>> list = handlers.computeIfAbsent(eventType, k -> new ArrayList<>());\\n list.add((Consumer<Object>) handler); // 类型强转,心要大\\n }\\n\\n // 发布事件:把事件丢进总线,让订阅的人自己处理\\n public <T> void publish(T event) {\\n List<Consumer<Object>> list = handlers.get(event.getClass());\\n if (list != null) {\\n list.forEach(handler -> handler.accept(event));\\n }\\n }\\n}\\n\\n// 事件定义:订单创建事件\\nclass OrderCreatedEvent {\\n private String orderId;\\n public OrderCreatedEvent(String orderId) { this.orderId = orderId; }\\n public String getOrderId() { return orderId; }\\n}\\n\\n// 使用示例\\npublic class Main {\\n public static void main(String[] args) {\\n EventBus bus = new EventBus();\\n\\n // 库存系统订阅订单事件\\n bus.subscribe(OrderCreatedEvent.class, event -> {\\n System.out.println(\\"库存系统:扣减订单\\" + event.getOrderId() + \\"的库存\\");\\n });\\n\\n // 物流系统订阅订单事件\\n bus.subscribe(OrderCreatedEvent.class, event -> {\\n System.out.println(\\"物流系统:为订单\\" + event.getOrderId() + \\"生成运单\\");\\n });\\n\\n // 用户下单,发布事件\\n bus.publish(new OrderCreatedEvent(\\"ORDER_666\\"));\\n }\\n}\\n\\n输出:\\n库存系统:扣减订单ORDER_666的库存 \\n物流系统:为订单ORDER_666生成运单\\n
\\nReactiveX
):让数据流动起来
响应式编程是观察者模式的超进化体,把数据封装成流(Observable
),允许链式操作(过滤、转换、合并),处理异步事件像写流水线一样爽。
场景: 实时股票行情:多个图表动态更新。
\\n用RxJava
搞个股票行情Demo
:
import io.reactivex.rxjava3.core.Observable;\\nimport io.reactivex.rxjava3.disposables.Disposable;\\n\\n// 模拟股票数据源\\nclass StockTicker {\\n private final String symbol;\\n private double price;\\n\\n public StockTicker(String symbol) {\\n this.symbol = symbol;\\n this.price = 100.0;\\n }\\n\\n // 每隔1秒生成一个随机价格波动\\n public Observable<Double> start() {\\n return Observable.interval(1, TimeUnit.SECONDS)\\n .map(tick -> {\\n price += (Math.random() - 0.5) * 10; // 随机波动\\n return price;\\n });\\n }\\n}\\n\\n// 使用示例\\npublic class RxDemo {\\n public static void main(String[] args) throws InterruptedException {\\n StockTicker appleTicker = new StockTicker(\\"AAPL\\");\\n Disposable subscription = appleTicker.start()\\n .subscribe(price -> System.out.println(\\"当前价格: \\" + price));\\n\\n // 跑5秒后取消订阅\\n Thread.sleep(5000);\\n subscription.dispose();\\n }\\n}\\n\\n输出:\\n当前价格: 103.4 \\n当前价格: 98.7 \\n当前价格: 105.2 \\n
\\n核心特性:
\\n.filter(price -> price > 100).map(price -> \\"高价警报:\\" + price)
.observeOn(Schedulers.io())
轻松切换线程。让消息跨机器跑
用消息队列(如Kafka
、RabbitMQ
)把观察者模式扩展到分布式系统,服务A
发消息,服务B
、C
、D
通过订阅Topic
消费消息。
场景:电商大促期间,库存变更同步到多个区域仓库。
\\n用Kafka
模拟订单通知:
// 生产者服务(订单服务)\\npublic class OrderProducer {\\n public static void main(String[] args) {\\n Properties props = new Properties();\\n props.put(\\"bootstrap.servers\\", \\"localhost:9092\\");\\n props.put(\\"key.serializer\\", \\"org.apache.kafka.common.serialization.StringSerializer\\");\\n props.put(\\"value.serializer\\", \\"org.apache.kafka.common.serialization.StringSerializer\\");\\n\\n try (Producer<String, String> producer = new KafkaProducer<>(props)) {\\n producer.send(new ProducerRecord<>(\\"order-topic\\", \\"ORDER_888\\"));\\n System.out.println(\\"订单已发送\\");\\n }\\n }\\n}\\n\\n// 消费者服务(库存服务)\\npublic class InventoryConsumer {\\n public static void main(String[] args) {\\n Properties props = new Properties();\\n props.put(\\"bootstrap.servers\\", \\"localhost:9092\\");\\n props.put(\\"group.id\\", \\"inventory-group\\");\\n props.put(\\"key.deserializer\\", \\"org.apache.kafka.common.serialization.StringDeserializer\\");\\n props.put(\\"value.deserializer\\", \\"org.apache.kafka.common.serialization.StringDeserializer\\");\\n\\n try (KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props)) {\\n consumer.subscribe(Collections.singletonList(\\"order-topic\\"));\\n while (true) {\\n ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));\\n for (ConsumerRecord<String, String> record : records) {\\n System.out.println(\\"库存系统收到订单:\\" + record.value());\\n }\\n }\\n }\\n }\\n}\\n\\n// 消费者服务(物流服务)代码类似,改个group.id即可\\n
\\n核心特性:
\\n坑点:
\\nKafka
保证分区内有序,但跨分区可能乱序。Spring
框架的勾搭Spring
的事件机制:
// 定义事件\\npublic class OrderEvent extends ApplicationEvent {\\n private String orderId;\\n public OrderEvent(Object source, String orderId) {\\n super(source);\\n this.orderId = orderId;\\n }\\n public String getOrderId() { return orderId; }\\n}\\n\\n// 发布事件\\n@Service\\nclass OrderService {\\n @Autowired\\n private ApplicationEventPublisher publisher;\\n\\n public void createOrder() {\\n publisher.publishEvent(new OrderEvent(this, \\"ORDER_123\\"));\\n }\\n}\\n\\n// 监听事件\\n@Component\\nclass InventoryListener {\\n @EventListener\\n public void handleOrder(OrderEvent event) {\\n System.out.println(\\"库存处理订单:\\" + event.getOrderId());\\n }\\n}\\n
\\n为什么用?
\\nSpring
生态无缝集成(如结合事务事件
)。核心原则 | 具体操作 | 踩坑警告 |
---|---|---|
模式不是祖宗,是工具 | 小项目直接用语言内置观察者(如Java 的Observable ) 大系统直接上 Kafka/RabbitMQ ,别自己写轮子 | 小项目强上消息队列 = 埋雷 大系统手写观察者 = 找死 |
解耦要有边界感 | 跨服务通信:用MQ (发订单消息给库存系统) 单应用内:用事件总线(如 Guava 的EventBus ) | 跨服务用事件总线 = 网络爆炸 单应用强上 MQ = 脱裤子放屁 |
监控比代码重要 | 分布式场景埋点: 消息堆积量监控 消费延迟报警 日志必须带消息 ID ,方便溯源 | 不监控 = 睁眼瞎 没日志 = 甩锅无门 |
设计要留后路 | 哪怕现在只有一个观察者,按事件总线设计 定义明确的事件类(如 OrderCreatedEvent ) | 直接写死调用 = 下次改需求重写 事件类字段乱塞 = 后期兼容地狱 |
Flutter
中实现观察者模式Flutter
中实现观察者模式有两种常见方式,一种是利用 Dart
语言自带的 Stream
处理数据流,另一种则是通过自定义接口和类手动实现观察者逻辑。
Stream
的观察者模式这种方法利用 Dart
的 StreamController
和 Stream
实现数据监听,适用于需要频繁更新或与 Flutter
响应式框架深度集成的场景。
核心代码实现:
\\nimport \'dart:async\';\\n\\n// 被观察对象:计数器\\nclass CounterNotifier {\\n final StreamController<int> _controller = StreamController();\\n int _value = 0;\\n\\n Stream<int> get stream => _controller.stream;\\n\\n void increment() {\\n _value++;\\n _controller.sink.add(_value); // 推送新值\\n }\\n\\n void cleanup() {\\n _controller.close(); // 释放资源\\n }\\n}\\n\\n// 观察者组件\\nclass CounterDisplay extends StatefulWidget {\\n const CounterDisplay({super.key});\\n\\n @override\\n State<CounterDisplay> createState() => _CounterDisplayState();\\n}\\n\\nclass _CounterDisplayState extends State<CounterDisplay> {\\n final CounterNotifier _notifier = CounterNotifier();\\n StreamSubscription? _subscription;\\n int _currentCount = 0;\\n\\n @override\\n void initState() {\\n super.initState();\\n _subscription = _notifier.stream.listen((newValue) {\\n setState(() => _currentCount = newValue); // 响应数据变化\\n });\\n }\\n\\n @override\\n void dispose() {\\n _subscription?.cancel(); // 取消监听\\n _notifier.cleanup();\\n super.dispose();\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Column(\\n children: [\\n Text(\\"当前计数: $_currentCount\\"),\\n ElevatedButton(\\n onPressed: _notifier.increment,\\n child: const Text(\'增加\'),\\n ),\\n ],\\n );\\n }\\n}\\n
\\n关键点说明:
\\nCounterNotifier
类通过 StreamController
管理数据流,increment
方法触发数值变化并通知监听者。listen
方法订阅数据流,并在 dispose
生命周期中释放资源,避免内存泄漏。setState
确保数据变化时 UI
同步更新。如果需要更灵活的控制(如筛选通知条件或处理复杂状态),可以手动实现观察者模式的结构。
\\n核心代码实现:
\\n// 观察者接口定义\\nabstract class ValueListener {\\n void onValueChanged(int value);\\n}\\n\\n// 被观察对象\\nclass ValueNotifier {\\n final List<ValueListener> _listeners = [];\\n int _value = 0;\\n\\n void addListener(ValueListener listener) {\\n _listeners.add(listener);\\n }\\n\\n void removeListener(ValueListener listener) {\\n _listeners.remove(listener);\\n }\\n\\n void updateValue(int newValue) {\\n _value = newValue;\\n _notifyListeners(); // 遍历通知观察者\\n }\\n\\n void _notifyListeners() {\\n for (final listener in _listeners) {\\n listener.onValueChanged(_value);\\n }\\n }\\n}\\n\\n// 实现观察者的组件\\nclass ValueDisplay extends StatefulWidget implements ValueListener {\\n const ValueDisplay({super.key});\\n\\n @override\\n State<ValueDisplay> createState() => _ValueDisplayState();\\n}\\n\\nclass _ValueDisplayState extends State<ValueDisplay> {\\n final ValueNotifier _notifier = ValueNotifier();\\n int _displayValue = 0;\\n\\n @override\\n void initState() {\\n super.initState();\\n _notifier.addListener(this); // 注册监听\\n }\\n\\n @override\\n void dispose() {\\n _notifier.removeListener(this); // 移除监听\\n super.dispose();\\n }\\n\\n @override\\n void onValueChanged(int value) {\\n setState(() => _displayValue = value); // 更新显示\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Column(\\n children: [\\n Text(\\"当前值: $_displayValue\\"),\\n ElevatedButton(\\n onPressed: () => _notifier.updateValue(DateTime.now().second),\\n child: const Text(\'随机更新\'),\\n ),\\n ],\\n );\\n }\\n}\\n
\\n关键点说明
\\nValueNotifier
维护观察者列表,通过 addListener/removeListener
管理订阅关系。updateValue
方法触发数据更新后,遍历所有观察者并调用其 onValueChanged
方法。ValueListener
接口,在 onValueChanged
中更新状态并刷新 UI
。特性 | Stream 方案 | 自定义观察者方案 |
---|---|---|
实现复杂度 | 简单(依赖 Dart 原生 API ) | 较高(需手动管理观察者列表) |
灵活性 | 适用于简单数据流 | 适合需要自定义通知逻辑的场景 |
内存管理 | 需手动关闭 StreamController | 需确保移除不再使用的观察者 |
与 Flutter 的集成 | 天然支持 setState 和响应式更新 | 需手动调用 setState |
食用
建议:
\\n\\n1、优先选择
\\nStream
方案 ,大多数场景下,Stream
和StreamBuilder
的组合能够简化代码。2、手动实现观察者的适用场景:需要控制通知频率(如防抖、节流)、需要根据条件过滤观察者、与第三方库或遗留代码交互等。
\\n
两种方式本质上都是通过解耦数据生产者(被观察者)和消费者(观察者),我们可根据项目需求选择更合适的方案。如果项目已使用 rxdart
等响应式库,也可直接扩展 BehaviorSubject
或 PublishSubject
实现更强大的观察者逻辑。
观察者模式通过抽象主题与观察者的依赖关系,实现了动态、灵活的一对多通信机制。其核心价值在于:
\\n在实际开发中,需根据场景选择推模型或拉模型,并注意线程安全、性能优化等问题。这一模式是构建事件驱动架构(如GUI
、微服务
、实时监控系统
)的基石。
熟练掌握好此模式,为我们接下来即将出场的四大状态管理库(Provider
、Riverpod
、BLoc
、GetX
)的工作原理打下坚实的基础。敬请期待!!!
\\n","description":"前言 你是否遇到过这种场景?\\n 某个核心数据一变,就得像催债一样挨个调用十几处关联模块的更新方法。新增一个功能,就得在原始类里硬塞一行调用代码。时间一长,类膨胀成庞然大物,维护代码像在沼泽里挣扎 —— 越改越陷得深。\\n\\n观察者模式就是来治这个病的。它把这种“单向依赖”的硬编码,变成“自由订阅”的灵活关系。数据发布方不再关心谁要听消息,监听方也不用死等轮询。就像微信群的@所有人:想听的进群,嫌吵的退群,群主发完消息就能甩手走人。\\n\\n从GUI事件到微服务通信,从Spring框架到Kafka消息队列,观察者模式的影子无处不在。它不是银弹,但绝对是解开“数据变动连锁…","guid":"https://juejin.cn/post/7494834508216516627","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-21T01:00:59.302Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/190b4ef08a06447e8d22610862171781~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1745802059&x-signature=OQz3R38FFyxEsXV%2FMjZSt6ip0fI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/83f2f1e9ad344128945e8cf6eb6cc785~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1745802059&x-signature=Y9LJsGsNj23JZrpK8Bp%2FGamukPg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d9f3271849cf4dd4829e8a295f364bdc~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1745802059&x-signature=PpKGjO7ZkOAhtu0OvhIlL2r%2BOSk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1162201e031841b2a1f2f4e59031f15c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1745802059&x-signature=te1g3oC14QjyMQCLbemOh%2FCtirs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1b0f8394c32649029528da2af6897a38~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1745802059&x-signature=LWxCVPtKhY6lTtfa9v0fYcMi3pA%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"Flutter常用组件的使用","url":"https://juejin.cn/post/7494635102174396451","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
Flutter中的组件Widget,它类似于Android中View的概念。其中Widget又包括StatefulWidget和StatelessWidget,有状态的和无状态的。为了避免初学Flutter的JY不知道什么是状态,我简单提一嘴。你可以理解为使用变量来保存视图的参数,通过改变变量值,也就是我们所说的状态,来达到修改界面的目的。
\\nclass MyCounter extends StatefulWidget {\\n @override\\n _MyCounterState createState() => _MyCounterState();\\n}\\n\\nclass _MyCounterState extends State<MyCounter> {\\n int count = 0;\\n\\n @override\\n Widget build(BuildContext context) {\\n return Column(\\n children: [\\n Text(\'Count: $count\'),\\n ElevatedButton(\\n onPressed: () {\\n setState(() {\\n count++;\\n });\\n },\\n child: Text(\'Add\'),\\n ),\\n ],\\n );\\n }\\n}\\n
\\n比如以上就是一个简单的官方Demo来演示带状态组件的使用。那么无状态组件就更简洁了。
\\nclass MyText extends StatelessWidget {\\n final String title;\\n\\n MyText(this.title);\\n\\n @override\\n Widget build(BuildContext context) {\\n return Text(title);\\n }\\n}\\n
\\n学习完基础的常用组件,就可以开发大部分的界面了,所以下面的内容非常重要,基础中的基础。
\\n我会介绍以下组件。
\\nContainer
、Row
、Column
、Stack
、Expanded
、Positioned
、Padding
、SizedBox
、Center
、Align
GestureDetector
、InkWell
、ElevatedButton
、IconButton
、TextField
Text
、RichText
、Image
、ListView
、Scaffold
建议按照我给的顺序,循序渐进学习。
\\nScaffold翻译成脚手架,你可以理解成一个界面大致的组成部分,即页面大体框架。
\\nScaffold(\\n appBar: AppBar(title: Text(\'Home\')),\\n body: Center(child: Text(\'Welcome to Flutter\')),\\n floatingActionButton: FloatingActionButton(\\n onPressed: () {},\\n child: Icon(Icons.add),\\n ),\\n)\\n
\\n这里提供了AppBar、Drawer、BottomNavigationBar等标准页面结构。整体是通过嵌套方式进行布局。
\\nBottomNavigationBar(\\n items: [\\n BottomNavigationBarItem(icon: Icon(Icons.home), label: \'Home\'),\\n BottomNavigationBarItem(icon: Icon(Icons.settings), label: \'Settings\'),\\n ],\\n currentIndex: 0,\\n onTap: (index) {\\n // Handle tab switch\\n },\\n)\\n
\\n再简单了解下BottomNavigationBar。
\\nContainer
是最常用的布局容器,支持设置宽高、边距、边框、背景颜色等。
Container(\\n width: 200,\\n height: 100,\\n padding: EdgeInsets.all(16),\\n margin: EdgeInsets.symmetric(vertical: 10),\\n decoration: BoxDecoration(\\n color: Colors.blue,\\n borderRadius: BorderRadius.circular(10),\\n ),\\n child: Text(\'Hello Flutter\', style: TextStyle(color: Colors.white)),\\n)\\n
\\nEdgeInsets用于设置margin和padding。\\n常用的构造方法有:
\\nEdgeInsets.all(double value)\\nEdgeInsets.symmetric({horizontal, vertical})\\nEdgeInsets.only({left, top, right, bottom})\\nEdgeInsets.fromLTRB(left, top, right, bottom)\\n
\\n使用方式如下:
\\npadding: EdgeInsets.all(16) // 所有值相同\\npadding: EdgeInsets.symmetric(horizontal: 20, vertical: 10) // 水平或垂直相同\\npadding: EdgeInsets.only(left: 10, top: 5) // 可以指定特定的一个或多个\\npadding: EdgeInsets.fromLTRB(10, 20, 10, 5) // 完整指定\\n
\\n它是一个固定尺寸/占位组件,常用于设置固定宽高,或者作为空白间距。
\\nSizedBox(height: 20), // 垂直间距\\nSizedBox(width: 10), // 水平间距\\n\\nSizedBox(\\n width: 100,\\n height: 50,\\n child: ElevatedButton(onPressed: () {}, child: Text(\'Button\')),\\n)\\n
\\n这几个我为什么会放在一起来讲,因为这样比较好类比理解。Column代表垂直排列组件,Row代表水平排列组件,类似于我们Android中的LinearLayout。
\\nColumn(\\n crossAxisAlignment: CrossAxisAlignment.start,\\n children: [\\n Text(\'Title\', style: TextStyle(fontSize: 20)),\\n Text(\'Subtitle\'),\\n ],\\n)\\n\\nRow(\\n mainAxisAlignment: MainAxisAlignment.spaceAround,\\n children: [\\n Icon(Icons.star, color: Colors.orange),\\n Icon(Icons.star, color: Colors.orange),\\n Icon(Icons.star, color: Colors.orange),\\n ],\\n)\\n
\\nmainAxisAlignment和crossAxisAlignment,代表主轴和交叉轴的对齐方式。顾名思义,如果为Column,即垂直排列,那么主轴的方向就是y轴,即垂直方向。它的交叉轴就是水平方向,CrossAxisAlignment.start表示水平方向左对齐。其它的类似,我这里就不多赘述。
\\nStack(\\n alignment: Alignment.center,\\n children: [\\n Image.asset(\'assets/images/bg.png\'),\\n Text(\'Overlay Text\', style: TextStyle(fontSize: 24, color: Colors.white)),\\n ],\\n)\\n
\\n另外,Stack也是一个常用的布局方式,类似于我们Android中的FrameLayout,即允许多个子组件堆叠显示,适合用于浮层、背景叠加等场景。
\\nText是一个文本显示组件,类似于我们Android中的TextView
。
Text(\\n \'Hello, Flutter!\',\\n style: TextStyle(\\n fontSize: 24,\\n color: Colors.black87,\\n fontWeight: FontWeight.bold,\\n ),\\n)\\n
\\nImage是一个图片显示组件,类似于我们Android中的ImageView
,它支持加载网络图片、本地资源图片。
Image.network(\'https://example.com/image.png\')\\nImage.asset(\'assets/images/logo.png\')\\n
\\nmy_flutter_project/
\\n├── assets/
\\n│ ├── images/
\\n│ │ ├── logo.png
\\n│ │ ├── bg.jpg
\\n├── pubspec.yaml
\\n你可以随便命名文件夹,常见的命名有 assets/
, assets/images/
, images/
等。需要注意的是,图片添加到对应文件夹还不够,还需要在pubspec.yaml文件中注册。
flutter:\\n assets:\\n - assets/images/logo.png\\n - assets/images/bg.jpg\\n
\\n或者你省略成
\\nflutter:\\n assets:\\n - assets/images/\\n
\\n缩进采用两字符缩进。
\\n按钮组件,类似于我们Android中的Button
。
ElevatedButton(\\n onPressed: () {\\n print(\'Button pressed\');\\n },\\n child: Text(\'Click Me\'),\\n)\\n
\\n带图标的按钮组件,类似于我们Android中的ImageButton
,在Icons中提供了很多系统的icon,常用的icon有很多。
IconButton(\\n icon: Icon(Icons.favorite),\\n color: Colors.red,\\n onPressed: () {\\n print(\'Favorited\');\\n },\\n)\\n
\\n输入框组件,类似于我们Android中的EditText
。
TextField(\\n decoration: InputDecoration(\\n labelText: \'Enter your name\',\\n border: OutlineInputBorder(),\\n ),\\n)\\n
\\n它的输入监听就需要结合状态了。
\\nclass MyTextInputPage extends StatefulWidget {\\n @override\\n _MyTextInputPageState createState() => _MyTextInputPageState();\\n}\\n\\nclass _MyTextInputPageState extends State<MyTextInputPage> {\\n final TextEditingController _controller = TextEditingController();\\n\\n @override\\n void dispose() {\\n _controller.dispose();\\n super.dispose();\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\'TextEditingController 示例\')),\\n body: Padding(\\n padding: EdgeInsets.all(16),\\n child: Column(\\n children: [\\n TextField(\\n controller: _controller,\\n decoration: InputDecoration(labelText: \'请输入\'),\\n ),\\n SizedBox(height: 20),\\n ElevatedButton(\\n onPressed: () {\\n print(\'你输入的是:${_controller.text}\');\\n },\\n child: Text(\'打印输入内容\'),\\n )\\n ],\\n ),\\n ),\\n );\\n }\\n}\\n
\\n慢慢的,我们开始有交互了。使用GestureDetector手势探测器可以给组件添加点击、长按事件。
\\nGestureDetector(\\n onTap: () {\\n print(\'Container tapped\');\\n },\\n child: Container(\\n color: Colors.red,\\n padding: EdgeInsets.all(20),\\n child: Text(\'Tap me\'),\\n ),\\n)\\n
\\n点击反馈组件,带有水波纹效果。
\\nInkWell(\\n onTap: () {\\n print(\'Tapped\');\\n },\\n child: Padding(\\n padding: EdgeInsets.all(12),\\n child: Text(\'Click Me\'),\\n ),\\n)\\n
\\nAndroid中的实现为给布局容器添加以下属性。
\\nandroid:clickable=\\"true\\"\\nandroid:foreground=\\"?selectableItemBackground\\"\\n
\\n弹性填充组件。常用于 Row
或 Column
中,自动填满剩余空间。
Row(\\n children: [\\n Icon(Icons.star),\\n Expanded(\\n child: Text(\'This is a very long text that will expand.\'),\\n ),\\n ],\\n)\\n
\\nPositioned
通常配合 Stack
使用,允许你指定子组件在父容器中的精确位置。
Stack(\\n children: [\\n Container(width: 200, height: 200, color: Colors.grey),\\n Positioned(\\n top: 10,\\n left: 10,\\n child: Text(\'Top Left\', style: TextStyle(color: Colors.white)),\\n ),\\n ],\\n)\\n
\\n内边距组件。
\\nPadding(\\n padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),\\n child: Text(\'With Padding\'),\\n)\\n
\\n将子组件水平、垂直都居中。
\\nCenter(\\n child: Text(\'居中显示\'),\\n)\\n
\\n这是一个更灵活的对齐方式,可以设置子组件在父容器的任意位置对齐。
\\nAlign(\\n alignment: Alignment.bottomRight,\\n child: Text(\'右下角\'),\\n)\\n
\\n常用的还有富文本组件,Android中类似的概念有Spannable
。
RichText(\\n text: TextSpan(\\n text: \'Hello \',\\n style: TextStyle(fontSize: 18, color: Colors.black),\\n children: [\\n TextSpan(\\n text: \'Flutter\',\\n style: TextStyle(color: Colors.blue, fontWeight: FontWeight.bold),\\n ),\\n TextSpan(text: \'!\'),\\n ],\\n ),\\n)\\n
\\n列表组件,类似于我们Android中的RecyclerView
、ListView
。
ListView(\\n children: List.generate(10, (index) => ListTile(title: Text(\'Item $index\'))),\\n)\\n
\\n或者使用ListView.builder
ListView.builder(\\n itemCount: 20,\\n itemBuilder: (context, index) {\\n return ListTile(title: Text(\'Item $index\'));\\n },\\n)\\n
\\n我们复习下刚才讲过的内容,来对知识点进行梳理,以加深印象。复习完后,趁热打铁,动手实践一下学习效果更佳哦。
\\nWidget 是 Flutter 的基本构建单位,一切UI都是Widget。
\\n分为两类:
\\nStatelessWidget
:无状态(固定不变的 UI)StatefulWidget
:有状态(可以随用户操作更新)组件名 | 作用说明 |
---|---|
Container | 万能容器(可设置大小、颜色、边框等) |
Text | 显示文字 |
Row / Column | 横向 / 纵向布局 |
ListView | 可滚动列表 |
Scaffold | 页面基础结构(AppBar、body 等) |
Padding | 添加内边距 |
SizedBox | 固定宽高 / 空白间隔 |
Stack | 子组件重叠显示 |
Positioned | Stack 中定位子组件 |
Expanded | 撑满剩余空间 |
IconButton | 带图标的按钮 |
InkWell | 带水波纹点击效果 |
RichText | 富文本(多样式文字组合) |
用于设置 padding(内边距)或 margin(外边距) 的类:
\\nEdgeInsets.all(10)
:四边都是 10EdgeInsets.symmetric(horizontal: 20, vertical: 10)
:左右 20,上下 10EdgeInsets.only(left: 5, top: 10)
:单独设置某几边assets/images/
文件夹pubspec.yaml
注册:flutter:\\n assets:\\n - assets/images/\\n
\\nImage.asset(\'assets/images/logo.png\')\\n
\\n用于控制、监听 TextField
的内容
常用方法:
\\n.text
:获取或设置值.addListener()
:监听变化.dispose()
:释放资源在探索桌面应用开发的历程中,我曾使用 Electron 打造了一款 壁纸集 软件。项目初衷是为了熟悉 Electron 开发流程,同时满足自己对个性化壁纸管理的需求。Electron 的跨平台特性和与前端技术栈的无缝结合,让我能快速搭建起应用框架。在开发过程中,采用了 electron-vite 提升构建效率,搭配 vue3、pinia 和 ts 构建现代化前端架构,element plus 则丰富了 UI 组件库,使得界面设计更加美观实用。从首页展示,到多级分类菜单,再到不同壁纸源的分页加载、搜索功能等,都逐步实现和完善,最终成功开发出这款简易但功能较为全面的壁纸软件,并将其开源共享。
\\n然而,当面对软件打包后的体积问题时,我陷入了沉思。90M 左右的打包体积,安装后膨胀至几百 M,这对于一款壁纸应用来说显然是难以接受的。过大的体积不仅增加了用户的下载负担,还可能导致用户设备存储压力骤增,影响使用体验,违背了开发一款轻量化、便捷工具的初衷。
\\n痛定思痛,我决定另辟蹊径,采用 Flutter 对壁纸软件进行重构,打造出全新的波奇壁纸软件。Flutter 凭借其独特的架构和编译机制,在跨平台开发中展现出了卓越的性能优势和体积控制能力。它使用 Dart 语言开发,拥有高效的渲染引擎,能够将代码和资源进行高度优化整合。在 Flutter 的加持下,波奇壁纸软件的打包体积大幅缩减至 11M,安装后的体积也仅有 30M 左右,体积相较于 Electron 版本缩小了 10 倍不止。这不仅显著降低了软件对用户设备存储空间的占用,还让软件的分发和安装变得更加高效便捷,极大地提升了软件的可用性和竞争力。
\\n通过这次从 Electron 到 Flutter 的重构之旅,我深刻体会到技术选型对于软件质量的深远影响。在追求功能实现的同时,更应注重软件性能和用户体验的平衡。Flutter 以其出色的性能表现和轻量化特性,为我提供了一种全新的开发思路和解决方案,助力我成功打造出了波奇壁纸软件这一优质的桌面壁纸管理工具。未来,我将继续探索 Flutter 开发的更多可能性,不断优化和拓展波奇壁纸软件的功能与体验,满足用户对壁纸应用的更高期待。
\\n上面都是ai帮忙吹的,一句话就是Electron打包体积太大,使用flutter重构。
\\n对比其他花里胡哨的,更喜欢这种简约风格的
\\n主题和国际化以前文章介绍过,这里就不再做过多赘述了(该软件没有做国际化),主要是介绍一下优化点。
\\nlib/themes/theme.dart,定义亮色和暗色默认,字体统一设置为 \\"Microsoft YaHei\\"。
\\nimport \'dart:io\';\\nimport \'package:flutter/material.dart\';\\n\\nThemeData createThemeData(Brightness brightness, Color primaryColor) {\\n return ThemeData(\\n brightness: brightness,\\n colorScheme: brightness == Brightness.light\\n ? ColorScheme.light(\\n surface: Colors.grey.shade200,\\n onSurface: Colors.grey.shade900,\\n primary: primaryColor,\\n primaryContainer: Colors.white,\\n secondary: Colors.grey.shade300,\\n inversePrimary: Colors.grey.shade800,\\n shadow: const Color.fromARGB(112, 80, 80, 80),\\n )\\n : ColorScheme.dark(\\n surface: Colors.grey.shade900,\\n onSurface: Colors.grey.shade100,\\n primary: primaryColor,\\n primaryContainer: const Color.fromARGB(255, 23, 23, 23),\\n secondary: const Color.fromARGB(107, 66, 66, 66),\\n inversePrimary: Colors.grey.shade200,\\n shadow: const Color.fromARGB(62, 75, 75, 75),\\n ),\\n fontFamily: Platform.isWindows ? \\"Microsoft YaHei\\" : null,\\n );\\n}\\n\\nThemeData lightMode = createThemeData(Brightness.light, Colors.blueAccent);\\nThemeData darkMode = createThemeData(Brightness.dark, Colors.blueAccent);\\n\\n
\\nlib/themes/theme_provider.dart,各种设置主题的方法,注释已经很详细了。
\\n// ignore_for_file: deprecated_member_use\\n\\nimport \'package:flutter/material.dart\';\\nimport \'package:shared_preferences/shared_preferences.dart\';\\nimport \'package:wallpaper_app/themes/theme.dart\';\\nimport \'dart:async\';\\n\\nclass ThemeProvider extends ChangeNotifier {\\n ThemeData _themeData = lightMode;\\n bool _followSystem = false;\\n bool _isInitialized = false;\\n bool _isDarkMode = false;\\n Color _customPrimaryColor = Colors.blueAccent; // 默认自定义颜色\\n VoidCallback? _brightnessListener;\\n\\n ThemeData get themeData => _themeData;\\n bool get isDarkMode => _isDarkMode;\\n bool get followSystem => _followSystem;\\n\\n ThemeProvider() {\\n _initTheme();\\n }\\n\\n // 初始化主题(异步)\\n Future<void> _initTheme() async {\\n if (_isInitialized) return;\\n\\n final prefs = await SharedPreferences.getInstance();\\n _followSystem = prefs.getBool(\'followSystem\') ?? false;\\n _customPrimaryColor =\\n Color(prefs.getInt(\'customPrimaryColor\') ?? Colors.blueAccent.value);\\n _isDarkMode = prefs.getBool(\'isDark\') ?? false;\\n\\n if (_followSystem) {\\n _themeData = _getSystemTheme();\\n } else {\\n final isDark = prefs.getBool(\'isDark\') ?? false;\\n _themeData = isDark\\n ? createThemeData(Brightness.dark, _customPrimaryColor)\\n : createThemeData(Brightness.light, _customPrimaryColor);\\n }\\n\\n _addSystemThemeListener();\\n _isInitialized = true;\\n notifyListeners();\\n }\\n\\n // 系统主题监听\\n void _addSystemThemeListener() {\\n _brightnessListener = () {\\n if (_followSystem) {\\n _themeData = _getSystemTheme();\\n notifyListeners();\\n }\\n };\\n\\n WidgetsBinding.instance.platformDispatcher.onPlatformBrightnessChanged =\\n _brightnessListener;\\n }\\n\\n // 清理资源\\n @override\\n void dispose() {\\n WidgetsBinding.instance.platformDispatcher.onPlatformBrightnessChanged =\\n null;\\n super.dispose();\\n }\\n\\n // 统一保存配置(异步)\\n Future<void> _savePreferences() async {\\n final prefs = await SharedPreferences.getInstance();\\n await prefs.setBool(\'isDark\', isDarkMode);\\n await prefs.setBool(\'followSystem\', _followSystem);\\n await prefs.setInt(\'customPrimaryColor\', _customPrimaryColor.value);\\n }\\n\\n // 切换主题\\n void toggleTheme() {\\n _followSystem = false;\\n _themeData = _isDarkMode\\n ? createThemeData(Brightness.light, _customPrimaryColor)\\n : createThemeData(Brightness.dark, _customPrimaryColor);\\n _isDarkMode = !_isDarkMode;\\n _scheduleSavePreferences(); // 异步保存设置\\n notifyListeners();\\n }\\n\\n // 设置浅色主题\\n void setLightTheme() {\\n _followSystem = false;\\n _isDarkMode = false;\\n _themeData = createThemeData(Brightness.light, _customPrimaryColor);\\n _scheduleSavePreferences(); // 异步保存设置\\n notifyListeners();\\n }\\n\\n // 设置深色主题\\n void setDarkTheme() {\\n _followSystem = false;\\n _isDarkMode = true;\\n _themeData = createThemeData(Brightness.dark, _customPrimaryColor);\\n _scheduleSavePreferences(); // 异步保存设置\\n notifyListeners();\\n }\\n\\n // 跟随系统主题\\n void setFollowSystem() {\\n _followSystem = true;\\n _themeData = _getSystemTheme();\\n _scheduleSavePreferences(); // 异步保存设置\\n notifyListeners();\\n }\\n\\n // 设置自定义主题颜色\\n void setCustomTheme(Color primaryColor) {\\n if (_followSystem) {\\n _customPrimaryColor = primaryColor;\\n _themeData = _getSystemTheme();\\n } else {\\n _customPrimaryColor = primaryColor;\\n _themeData = isDarkMode\\n ? createThemeData(Brightness.dark, primaryColor)\\n : createThemeData(Brightness.light, primaryColor);\\n }\\n _scheduleSavePreferences(); // 异步保存设置\\n notifyListeners();\\n }\\n\\n // 获取系统主题\\n ThemeData _getSystemTheme() {\\n final brightness =\\n WidgetsBinding.instance.platformDispatcher.platformBrightness;\\n return brightness == Brightness.dark\\n ? createThemeData(Brightness.dark, _customPrimaryColor)\\n : createThemeData(Brightness.light, _customPrimaryColor);\\n }\\n\\n // 异步保存设置(不会阻塞主线程)\\n void _scheduleSavePreferences() {\\n Future.microtask(() async {\\n await _savePreferences();\\n });\\n }\\n}\\n\\n
\\nimport \'dart:io\';\\nimport \'package:flutter_cache_manager/flutter_cache_manager.dart\';\\nimport \'package:path/path.dart\' as path;\\n\\nclass CustomCacheManager extends CacheManager {\\n static const key = \'custom-cache-key\';\\n static final String _cachePath = path.join(\'D:\', \'custom_image_cache\');\\n static final CustomCacheManager _instance = CustomCacheManager._();\\n\\n factory CustomCacheManager() => _instance;\\n\\n CustomCacheManager._()\\n : super(Config(\\n key,\\n stalePeriod: const Duration(days: 7),\\n maxNrOfCacheObjects: 100,\\n fileService: HttpFileService(),\\n fileSystem: IOFileSystem(_cachePath),\\n ));\\n\\n // 直接在构造函数中使用IOFileSystem指定了缓存路径\\n // 确保D盘缓存目录存在\\n static void ensureCacheDirExists() {\\n final cacheDir = Directory(_cachePath);\\n if (!cacheDir.existsSync()) {\\n cacheDir.createSync(recursive: true);\\n }\\n }\\n\\n static void deleteCacheDir() {\\n final cacheDir = Directory(_cachePath);\\n if (cacheDir.existsSync()) {\\n // 判断目录的大小,超过100M时才删除\\n final size = cacheDir.listSync().fold<int>(\\n 0,\\n (previousValue, element) => previousValue + element.statSync().size,\\n );\\n if (size < 100 * 1024 * 1024) {\\n return;\\n }\\n cacheDir.deleteSync(recursive: true);\\n }\\n }\\n\\n static void clearCache() {\\n final cacheDir = Directory(_cachePath);\\n if (cacheDir.existsSync()) {\\n cacheDir.deleteSync(recursive: true);\\n }\\n }\\n\\n static Future<double> fileSize() {\\n final cacheDir = Directory(_cachePath);\\n double filesize = 0;\\n if (cacheDir.existsSync()) {\\n // 判断目录的大小,超过100M时才删除\\n int size = cacheDir.listSync().fold<int>(\\n 0,\\n (previousValue, element) => previousValue + element.statSync().size,\\n );\\n filesize = size / 1024 / 1024;\\n } else {\\n filesize = 0;\\n }\\n // 保留两位小数\\n filesize = double.parse(filesize.toStringAsFixed(2));\\n return Future.value(filesize);\\n }\\n}\\n\\n
\\nimport \'package:cached_network_image/cached_network_image.dart\';\\nimport \'package:flutter/material.dart\';\\nimport \'package:wallpaper_app/components/AlertDialog/my_loading.dart\';\\nimport \'package:wallpaper_app/tools/custom_image_cache.dart\';\\n\\nclass ImageLoad extends StatelessWidget {\\n final String imageUrl;\\n final BoxFit fit;\\n final double size;\\n const ImageLoad({\\n super.key,\\n required this.imageUrl,\\n this.fit = BoxFit.cover,\\n this.size = 50,\\n });\\n\\n @override\\n Widget build(BuildContext context) {\\n // 确保缓存目录存在\\n CustomCacheManager.ensureCacheDirExists();\\n return CachedNetworkImage(\\n imageUrl: imageUrl,\\n fit: BoxFit.cover,\\n cacheManager: CustomCacheManager(),\\n placeholder: (context, url) => Center(\\n child: MyLoading(type: 1, size: size),\\n ),\\n errorWidget: (context, url, error) => Icon(Icons.error),\\n );\\n }\\n}\\n
\\n可以检查图片缓存是否在D盘下面custom_image_cache文件夹中出现,同时检查C盘下面的临时缓存文件夹中是否还会继续缓存图片。检查后发现缓存已经移至D盘,不会在C盘下面缓存了,同时缓存功能正常,ok了,大功告成。
\\neasy_image_viewer,支持弹窗的模式查看图片,支持双指缩放,支持滚动缩放,支持多图。主要是多端支持,而且能通过函数直接预览,图片预览非常方便。
\\n简单示例,详细用法见官网,我这先从缓存中读取数据在预览。
······\\n Tooltip(\\n message: \'查看\',\\n child: IconButton(\\n icon: Icon(\\n Icons.swipe_vertical,\\n color: Colors.white,\\n ),\\n color: Theme.of(context)\\n .colorScheme\\n .primary,\\n onPressed: () async {\\n final cacheFile =\\n await CustomCacheManager()\\n .getSingleFile(\\n widget\\n .images[activeIndex].largePath,\\n );\\n if (cacheFile.path.isEmpty) return;\\n final imageProvider =\\n Image.file(cacheFile).image;\\n showImageViewer(\\n context, imageProvider);\\n }))\\n ······\\n
\\nflutter_side_menu Flutter的完全可自定义侧边菜单已用作 Related Pages、Navigation Items、Filter side 等的目录。官网教程通俗易懂,直接前往官网查看即可。这里贴一下我自己优化后的侧边栏代码。
\\n// ignore_for_file: camel_case_types\\nimport \'package:bitsdojo_window/bitsdojo_window.dart\';\\nimport \'package:flutter/material.dart\';\\nimport \'package:flutter_side_menu/flutter_side_menu.dart\';\\nimport \'package:package_info_plus/package_info_plus.dart\';\\n\\nclass SizeMenuNav extends StatefulWidget {\\n final void Function(int index) onTapSide;\\n const SizeMenuNav({super.key, required this.onTapSide});\\n\\n @override\\n State<SizeMenuNav> createState() => _SizeMenuNavState();\\n}\\n\\nclass sideItem {\\n final String title;\\n final IconData icon;\\n sideItem(this.title, this.icon);\\n}\\n\\nclass _SizeMenuNavState extends State<SizeMenuNav> {\\n int activeIndex = 0;\\n double sidebarWidth = 180;\\n String version = \'\';\\n\\n void getVersion() async {\\n PackageInfo packageInfo = await PackageInfo.fromPlatform();\\n setState(() {\\n version = packageInfo.version;\\n });\\n }\\n\\n @override\\n void initState() {\\n super.initState();\\n getVersion();\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n List<sideItem> sideItems = [\\n sideItem(\'首页\', Icons.home),\\n sideItem(\'最新\', Icons.new_label),\\n sideItem(\'Loveanimer壁纸\', Icons.real_estate_agent),\\n sideItem(\'萌虎壁纸\', Icons.support_agent),\\n sideItem(\'ACG壁纸\', Icons.discord),\\n sideItem(\'你的名字\', Icons.handshake),\\n sideItem(\'JK\', Icons.pregnant_woman),\\n sideItem(\'个人中心\', Icons.account_box),\\n ];\\n\\n return SideMenu(\\n mode: SideMenuMode.open,\\n maxWidth: sidebarWidth,\\n backgroundColor: Theme.of(context).colorScheme.primaryContainer,\\n hasResizer: true,\\n hasResizerToggle: true,\\n resizerToggleData: ResizerToggleData(\\n iconColor: Theme.of(context).colorScheme.primary,\\n opacity: 1,\\n iconSize: 25,\\n ),\\n builder: (data) => SideMenuData(\\n header: Column(\\n spacing: 10,\\n children: [\\n SizedBox(height: 30, child: MoveWindow()),\\n FittedBox(\\n fit: BoxFit.scaleDown,\\n child: Padding(\\n padding: const EdgeInsets.only(left: 10, right: 10),\\n child: Image.asset(\\n \'lib/assets/images/icon.png\',\\n width: 80,\\n height: 80,\\n ),\\n ),\\n ),\\n FittedBox(\\n fit: BoxFit.scaleDown,\\n child: Padding(\\n padding: const EdgeInsets.only(left: 10, right: 10),\\n child: Text(\\n \'波奇壁纸\',\\n style: TextStyle(\\n fontSize: 20,\\n fontWeight: FontWeight.bold,\\n ),\\n ),\\n ),\\n ),\\n // 分割线\\n const Divider(\\n color: Colors.grey,\\n height: 10,\\n thickness: 1,\\n indent: 10,\\n endIndent: 10,\\n ),\\n ],\\n ),\\n items: [\\n for (var item in sideItems)\\n SideMenuItemDataTile(\\n hasSelectedLine: true,\\n borderRadius: BorderRadius.all(Radius.circular(5)),\\n isSelected: activeIndex == sideItems.indexOf(item),\\n tooltip:\\n data.currentWidth <= data.minWidth + 30 ? item.title : null,\\n titleStyle: TextStyle(\\n fontSize: 15,\\n ),\\n selectedTitleStyle: TextStyle(\\n fontSize: 15,\\n color: Theme.of(context).colorScheme.primary,\\n ),\\n onTap: () {\\n setState(() {\\n activeIndex = sideItems.indexOf(item);\\n });\\n widget.onTapSide(sideItems.indexOf(item));\\n },\\n title: item.title,\\n icon: Icon(item.icon),\\n ),\\n ],\\n footer: FittedBox(\\n fit: BoxFit.scaleDown,\\n child: Padding(\\n padding: const EdgeInsets.all(5),\\n child: Text(\\n \'v$version\',\\n style: TextStyle(\\n fontSize: 13,\\n ),\\n ),\\n ),\\n ),\\n ),\\n );\\n }\\n}\\n
\\n其它功能就没啥难点了,可自行查看。
\\nimport \'package:flutter/material.dart\';\\nimport \'package:wallpaper_app/pages/4k/image_list_new_360.dart\';\\nimport \'package:wallpaper_app/pages/setting/personal_center.dart\';\\nimport \'package:wallpaper_app/tools/custom_image_cache.dart\';\\nimport \'package:wallpaper_app/components/SideBar/side_menu_nav.dart\';\\nimport \'package:wallpaper_app/components/windows/window_title_bar.dart\';\\nimport \'package:wallpaper_app/pages/home_page.dart\';\\nimport \'package:wallpaper_app/pages/mohu/mohu_page.dart\';\\nimport \'package:wallpaper_app/pages/suyan/random_image_pc.dart\';\\nimport \'package:wallpaper_app/pages/tuhui/acg_list.dart\';\\nimport \'package:wallpaper_app/pages/tuhui/jk_list.dart\';\\nimport \'package:wallpaper_app/pages/tuhui/your_name.dart\';\\nimport \'package:wallpaper_app/tools/update_apk.dart\';\\n\\nclass MainPage extends StatefulWidget {\\n const MainPage({super.key});\\n\\n @override\\n State<MainPage> createState() => _MainPageState();\\n}\\n\\nclass _MainPageState extends State<MainPage> {\\n PageController pageController = PageController();\\n void cacheCleaning() async {\\n double size = await CustomCacheManager.fileSize();\\n // 大于2G 清除缓存\\n if (size > 2048) {\\n CustomCacheManager.deleteCacheDir();\\n }\\n }\\n\\n @override\\n void initState() {\\n super.initState();\\n cacheCleaning();\\n UpdateApk().updateApk(context);\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n body: SizedBox(\\n width: double.infinity,\\n height: double.infinity,\\n child: Row(\\n children: [\\n // 侧边栏\\n // SidebarPage(onTapSide: (index) {\\n // pageController.jumpToPage(index);\\n // }),\\n SizeMenuNav(onTapSide: (index) {\\n pageController.jumpToPage(index);\\n }),\\n Expanded(\\n child: Column(\\n children: [\\n WindowTitleBar(),\\n Expanded(\\n child: PageView(\\n controller: pageController,\\n children: [\\n HomePage(),\\n ImageListNew360(),\\n RandomImagePc(),\\n MohuPage(),\\n AcgList(zd: \'pc\'),\\n YourName(),\\n JkList(zd: \'pc\'),\\n PersonalCenter(),\\n ],\\n ),\\n )\\n ],\\n ))\\n ],\\n ),\\n ));\\n }\\n}\\n\\n
\\ngraph LR\\n点击设置按钮 --\x3e 读取临时缓存中的文件 --\x3e 保存到本地,防止被清理 --\x3e 调用函数设置壁纸\\n
\\nCustomCacheManager()
就是 3.2
自定义的缓存管理器,继承自 CacheManager
lib/components/Images/image_view.dart
final cacheFile = await CustomCacheManager().getSingleFile(url);\\nprint(cacheFile.path);\\n
\\nlib/tools/save_file.dart
saveImageFile()
图片保存到本地,saveFileWallPaper()
设置壁纸时调用,功能一模一样,只有命名方式不同,用于标记当前桌面壁纸。4.4
的设置壁纸函数即可。import \'dart:io\';\\n\\nclass SaveFile {\\n // 保存文件本地文件复制到 D盘 BoQiDown 文件夹\\n static Future<String> saveImageFile(String path) async {\\n try {\\n // 确保 D盘 BoQiDown 文件夹存在\\n Directory directory = Directory(\'D:\\\\\\\\BoQiDown\\\\\\\\image\');\\n if (!directory.existsSync()) {\\n directory.createSync();\\n }\\n // 复制文件到 D盘 BoQiDown 文件夹\\n File file = File(path);\\n String newPath = \'D:\\\\\\\\BoQiDown\\\\\\\\image\\\\\\\\${file.path.split(\'\\\\\\\\\').last}\';\\n await file.copy(newPath);\\n return newPath; // 返回新的路径\\n } catch (e) {\\n throw Exception(\'保存文件失败: $e\');\\n }\\n }\\n\\n\\n static Future<String> saveFileWallPaper(String path) async {\\n try {\\n // 确保 D盘 BoQiDown 文件夹存在\\n Directory directory = Directory(\'D:\\\\\\\\BoQiDown\\\\\\\\image\');\\n if (!directory.existsSync()) {\\n directory.createSync();\\n }\\n // 复制文件到 D盘 BoQiDown 文件夹\\n File file = File(path);\\n String newPath = \'D:\\\\\\\\BoQiDown\\\\\\\\image\\\\\\\\desktop.jpg\';\\n await file.copy(newPath);\\n return newPath; // 返回新的路径\\n } catch (e) {\\n throw Exception(\'保存文件失败: $e\');\\n }\\n }\\n}\\n\\n
\\nlib/tools/wallpaper_service.dart
lib/components/Images/image_view.dart
// ignore_for_file: unused_local_variable, constant_identifier_names, depend_on_referenced_packages\\n\\nimport \'dart:async\';\\nimport \'dart:isolate\';\\nimport \'dart:io\';\\nimport \'dart:ui\';\\nimport \'package:ffi/ffi.dart\';\\nimport \'package:win32/win32.dart\';\\n\\nconst int SPIF_UPDATEINIFILE = 0x01;\\nconst int SPIF_SENDCHANGE = 0x02;\\n\\nclass WallpaperService {\\n // 设置Windows桌面壁纸\\n // [imagePath] 图片的完整路径\\n static Future<bool> setWallpaperInIsolate(String imagePath) async {\\n final receivePort = ReceivePort();\\n Isolate.spawn(_setWallpaper, imagePath).then((isolate) {\\n return true;\\n }).catchError((error) {\\n return false;\\n });\\n return true;\\n }\\n\\n static void _setWallpaper(String imagePath) {\\n try {\\n final file = File(imagePath);\\n if (!file.existsSync()) {\\n return;\\n }\\n\\n final pathPointer = imagePath.toNativeUtf16();\\n\\n final result = SystemParametersInfo(\\n SPI_SETDESKWALLPAPER,\\n 0,\\n pathPointer,\\n SPIF_UPDATEINIFILE | SPIF_SENDCHANGE,\\n );\\n\\n free(pathPointer);\\n\\n if (result != 0) {\\n // 通过 SendPort 发送消息到主 UI 线程\\n final sendPort = IsolateNameServer.lookupPortByName(\'mainPort\');\\n sendPort?.send({\'code\': 200, \'message\': \'设置成功\'});\\n } else {\\n final sendPort = IsolateNameServer.lookupPortByName(\'mainPort\');\\n sendPort?.send({\'code\': 400, \'message\': \'设置失败\'});\\n }\\n } catch (e) {\\n final sendPort = IsolateNameServer.lookupPortByName(\'mainPort\');\\n sendPort?.send({\'code\': 400, \'message\': \'设置失败: $e\'});\\n }\\n }\\n}\\n\\n
\\n安装Dart SDK
\\ndart --version
第一个Dart程序
\\nvoid main() {\\n print(\'Hello, Dart World!\');\\n}\\n
\\n运行结果:
\\nHello, Dart World!\\n
\\nvar name = \'Alice\'; // 类型推断\\nString city = \'London\'; // 显式声明\\ndynamic anything = 5; // 动态类型\\nfinal PI = 3.14; // 运行时常量\\nconst gravity = 9.8; // 编译时常量\\n
\\nint age = 25; // 整数\\ndouble price = 9.99; // 浮点数\\nbool isStudent = true; // 布尔值\\nString greeting = \'Hello\'; // 字符串\\nList<int> numbers = [1,2,3]; // 列表\\nMap<String, int> scores = { // Map集合\\n \'Math\': 90,\\n \'English\': 85\\n};\\n
\\nvar value = \'123\';\\nprint(value is String); // true\\nint numValue = int.parse(value);\\nString strValue = numValue.toString();\\n
\\nint a = 10, b = 3;\\nprint(a + b); // 13\\nprint(a ~/ b); // 3 (整除)\\nprint(a++); // 10 (后置递增)\\n
\\nString? nullableName;\\nprint(nullableName ?? \'Default\'); // 空合并\\nprint(nullableName?.length); // 安全访问\\n
\\nint score = 85;\\nif (score >= 90) {\\n print(\'优秀\');\\n} else if (score >= 60) {\\n print(\'及格\');\\n} else {\\n print(\'不及格\');\\n}\\n\\n// 三元运算符\\nvar result = score >= 60 ? \'通过\' : \'补考\';\\n
\\n// for循环\\nfor (var i = 0; i < 5; i++) {\\n print(i);\\n}\\n\\n// for-in循环\\nvar fruits = [\'apple\', \'banana\', \'orange\'];\\nfor (var fruit in fruits) {\\n print(fruit);\\n}\\n\\n// while循环\\nint count = 3;\\nwhile (count > 0) {\\n print(count--);\\n}\\n
\\n// 函数定义\\ndouble calculateBMI(double weight, double height) {\\n return weight / (height * height);\\n}\\n\\n// 箭头函数\\nString greet(String name) => \'Hello, $name!\';\\n\\nvoid main() {\\n print(calculateBMI(70, 1.75)); // 22.857\\n print(greet(\'Bob\')); // Hello, Bob!\\n}\\n
\\n// 命名参数\\nvoid register({String name, int age}) {\\n print(\'$name ($age) registered\');\\n}\\n\\n// 可选参数\\nString say(String from, [String? to]) {\\n return to != null ? \'$from to $to\' : from;\\n}\\n\\nvoid main() {\\n register(name: \'Alice\', age: 25);\\n print(say(\'Hello\')); // Hello\\n print(say(\'Hi\', \'there\')); // Hi to there\\n}\\n
\\nclass Person {\\n String name;\\n int age;\\n \\n // 构造函数\\n Person(this.name, this.age);\\n \\n // 命名构造函数\\n Person.newborn(String name) : this(name, 0);\\n \\n // 方法\\n void introduce() {\\n print(\'我是$name,今年$age岁\');\\n }\\n}\\n\\nvoid main() {\\n var p1 = Person(\'Alice\', 25);\\n var baby = Person.newborn(\'Charlie\');\\n p1.introduce(); // 我是Alice,今年25岁\\n}\\n
\\nclass Animal {\\n void makeSound() => print(\'---\');\\n}\\n\\nclass Dog extends Animal {\\n @override\\n void makeSound() => print(\'汪汪!\');\\n}\\n\\nvoid main() {\\n Animal myPet = Dog();\\n myPet.makeSound(); // 汪汪!\\n}\\n
\\nmixin Flyable {\\n void fly() => print(\'飞行中...\');\\n}\\n\\nclass Bird with Flyable {}\\n\\nvoid main() {\\n var sparrow = Bird();\\n sparrow.fly(); // 飞行中...\\n}\\n
\\nvoid main() {\\n try {\\n var result = 100 ~/ 0;\\n } on IntegerDivisionByZeroException {\\n print(\'除数不能为零!\');\\n } catch (e) {\\n print(\'未知错误: $e\');\\n } finally {\\n print(\'处理完成\');\\n }\\n}\\n
\\nvar numbers = [1, 2, 3];\\nnumbers.add(4);\\nnumbers.removeAt(0);\\nprint(numbers.where((n) => n > 2)); // (3,4)\\n
\\nvar capitals = {\\n \'China\': \'Beijing\',\\n \'Japan\': \'Tokyo\'\\n};\\ncapitals[\'USA\'] = \'Washington\';\\nprint(capitals.containsKey(\'China\')); // true\\n
\\nclass Box<T> {\\n T content;\\n Box(this.content);\\n}\\n\\nvoid main() {\\n var stringBox = Box<String>(\'Secret\');\\n var intBox = Box<int>(42);\\n}\\n
\\nFuture<String> fetchUser() async {\\n await Future.delayed(Duration(seconds: 1));\\n return \'用户数据\';\\n}\\n\\nvoid main() async {\\n print(\'开始获取...\');\\n var user = await fetchUser();\\n print(\'获取到:$user\');\\n}\\n
\\nStream<int> countStream(int to) async* {\\n for (int i = 1; i <= to; i++) {\\n await Future.delayed(Duration(milliseconds: 500));\\n yield i;\\n }\\n}\\n\\nvoid main() async {\\n await for (var num in countStream(3)) {\\n print(num); // 间隔0.5秒输出1,2,3\\n }\\n}\\n
\\nclass Temperature {\\n double celsius;\\n \\n Temperature(this.celsius);\\n \\n double get fahrenheit => celsius * 9/5 + 32;\\n \\n factory Temperature.fromFahrenheit(double f) {\\n return Temperature((f - 32) * 5/9);\\n }\\n}\\n\\nvoid main() {\\n var t1 = Temperature(25);\\n print(\'25°C = ${t1.fahrenheit.toStringAsFixed(1)}°F\'); // 77.0°F\\n \\n var t2 = Temperature.fromFahrenheit(77);\\n print(\'77°F = ${t2.celsius}°C\'); // 25°C\\n}\\n
\\n实现一个简易待办事项应用:
\\nclass TodoItem {\\n String title;\\n bool isDone;\\n \\n TodoItem(this.title, [this.isDone = false]);\\n}\\n\\nclass TodoList {\\n final List<TodoItem> _items = [];\\n \\n void add(String title) => _items.add(TodoItem(title));\\n \\n void remove(int index) => _items.removeAt(index);\\n \\n void toggleDone(int index) {\\n _items[index].isDone = !_items[index].isDone;\\n }\\n \\n void display() {\\n _items.asMap().forEach((i, item) {\\n print(\'${i+1}. [${item.isDone ? \'✓\' : \' \'}] ${item.title}\');\\n });\\n }\\n}\\n\\nvoid main() {\\n var myList = TodoList();\\n myList.add(\'学习Dart\');\\n myList.add(\'练习Flutter\');\\n myList.toggleDone(0);\\n myList.display();\\n}\\n
\\n输出结果:
\\n1. [✓] 学习Dart\\n2. [ ] 练习Flutter\\n
","description":"Dart 基础教程 一、环境搭建与第一个程序\\n\\n安装Dart SDK\\n\\n官网下载:dart.dev/get-dart\\n验证安装:dart --version\\n\\n第一个Dart程序\\n\\nvoid main() {\\n print(\'Hello, Dart World!\');\\n}\\n\\n\\n运行结果:\\n\\nHello, Dart World!\\n\\n二、变量与数据类型\\n1. 变量声明\\nvar name = \'Alice\'; // 类型推断\\nString city = \'London\'; // 显式声明\\ndynamic anything = 5; //…","guid":"https://juejin.cn/post/7494295779848241187","author":"ak啊","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-18T07:41:54.170Z","media":null,"categories":["代码人生","Dart","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"【Flutter 状态管理 - 肆】 | setState的工作机制探秘","url":"https://juejin.cn/post/7494448379179958311","content":"为什么说setState
是Flutter
开发者的“第一把钥匙”
?
刚接触Flutter
时,你可能觉得setState
像是魔法 —— 轻轻一调用,界面就自动刷新了。但当你深入开发复杂应用时,可能会遇到界面卡顿、无效重绘等问题,这时候才意识到,这把“钥匙”
背后的机制远比想象中精妙。
setState
绝不仅仅是一个简单的刷新按钮,它背后串联着Flutter
的声明式UI
框架、高效渲染管线,甚至藏着性能优化的密码。今天我们就来拆解这个“老朋友”
的工作机制,让你真正理解它如何让界面“活”
起来。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\n状态更新机制的基石
标脏的原子操作
当你调用setState(() { _counter++; })
时,Flutter
框架会启动一个精密的标记流程:
// 框架核心方法:触发组件状态更新\\nvoid setState(VoidCallback fn) {\\n // 执行用户传入的回调,处理状态变更逻辑(如计数器累加)\\n final Object? result = fn() as dynamic; \\n\\n // 关键步骤:标记关联的 Element 为待重建状态\\n _element!.markNeedsBuild(); \\n}\\n
\\nFlutter
会立即同步执行回调函数,确保后续build
方法中的状态是最新的。但此时界面并未立刻更新 —— 这只是一个“标记动作”
,真正的重绘大戏还在后头。
举个栗子:就像你给朋友发微信说“我出发了”
,但对方不会立刻看到你,直到你真的走到他家门口。
\\n\\n关键认知:
\\nsetState
本身并不直接触发界面更新!
“脏元素”
:给需要刷新的组件贴标签
markNeedsBuild
方法中的 dirty
标志位是个精妙设计:
// Element 的标记更新方法\\nvoid markNeedsBuild() {\\n // 防御性校验:非活跃状态元素不再处理(如已卸载组件)\\n if (_lifecycleState != _ElementLifecycle.active) return;\\n \\n // 避免重复标记:已经是脏元素则跳过\\n if (dirty) return;\\n\\n // 打上脏标记,等待后续重建\\n _dirty = true;\\n\\n // 将当前元素加入构建管线调度队列\\n owner!.scheduleBuildFor(this);\\n}\\n
\\n该方法将当前StatefulWidget
对应的Element
标记为“脏”
。这个Element
会被加入一个全局的“待办清单”
(BuildOwner._dirtyElements
),等待统一处理。
这里的防重复标记机制特别重要。我曾在列表滚动优化时,发现某个组件被重复标记了 20
多次,导致性能暴跌。后来在 markNeedsBuild
里加了调试打印,才揪出那个疯狂发送状态更新的野指针。
帧调度与脏元素管理
当调用scheduleBuildFor
时,就进入了框架的核心调度系统:
// BuildOwner 的构建任务调度方法\\nvoid scheduleBuildFor(Element element) {\\n // 获取元素的构建作用域(处理跨节点更新的关键)\\n final BuildScope buildScope = element.buildScope;\\n\\n // 首次触发时安排帧回调(如通知引擎安排VSYNC信号)\\n if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {\\n _scheduledFlushDirtyElements = true;\\n onBuildScheduled!(); // 通常触发 platformDispatcher.scheduleFrame()\\n }\\n\\n // 将元素加入对应作用域的脏元素列表\\n buildScope._scheduleBuildFor(element);\\n}\\n
\\n关键行为:
\\nsetState
仅触发一次帧请求。platformDispatcher.scheduleFrame()
通过Dart
的Native
绑定调用引擎的C++
代码,最终触发VSync
信号。这就解释了为什么有时候连续多次 setState
不会导致界面抖动 —— 所有修改都会在下一次屏幕刷新时批量处理,这和游戏引擎的渲染逻辑异曲同工。
这里的 onBuildScheduled
对初学者来说是个黑魔法。Flutter
通过调用SchedulerBinding.scheduleFrame()
向引擎请求下一针绘制。核心代码在scheduleFrame()
中:
// 文件路径:flutter/packages/flutter/lib/src/scheduler/binding.dart\\nvoid scheduleFrame() {\\n // 去重\\n if (_hasScheduledFrame || !framesEnabled) {\\n return;\\n }\\n ensureFrameCallbacksRegistered();\\n // 调用引擎接口\\n platformDispatcher.scheduleFrame();\\n _hasScheduledFrame = true;\\n}\\n\\n// 引擎接口(C++层交互)\\n@Native<Void Function()>(symbol: \'PlatformConfigurationNativeApi::ScheduleFrame\')\\nexternal static void _scheduleFrame();\\n
\\n// BuildScope 内部管理脏元素的方法\\nvoid _scheduleBuildFor(Element element) {\\n // 避免元素重复加入队列\\n if (!element._inDirtyList) {\\n _dirtyElements.add(element); // 加入脏元素列表\\n element._inDirtyList = true; // 标记元素已在队列中\\n }\\n\\n // 触发构建任务调度(首次调用时生效)\\n if (!_buildScheduled && !_building) {\\n _buildScheduled = true;\\n scheduleRebuild?.call(); // 通常触发 WidgetsBinding.drawFrame\\n }\\n\\n // 标记需要重新排序(处理 Element 深度变化时的构建顺序)\\n if (_dirtyElementsNeedsResorting != null) {\\n _dirtyElementsNeedsResorting = true;\\n }\\n}\\n
\\n源码赏析:
\\nSchedulerBinding
注册三个核心回调:\\n1、transientCallbacks
:处理动画(Ticker
)。2、persistentCallbacks
:处理布局和绘制(WidgetsBinding.drawFrame
)。3、postFrameCallbacks
:单次帧结束回调(如addPostFrameCallback
)。父组件先处理,避免重复标记
)。“连坐”
更新(但可通过const
组件或Key
避免)。渲染管线
用户操作
到 GPU
对于 Flutter
的渲染机制而言,首要原则是 简单快速。 Flutter
为数据流向系统提供了直通的管道,流程图如下:
上图的 User input
相当于执行了setState(...)
。\\n当引擎准备好绘制新帧时,真正的重头戏开始。整个过程像一条精密的生产线,可分为如下5
个阶段。
Animate Phase
)触发条件:存在被标记为脏的Element
(通过BuildOwner.scheduleBuildFor
注册)。
源码探秘:
\\n// 处理帧开始阶段的回调(通常由引擎的VSync信号触发)\\nvoid handleBeginFrame(Duration? rawTimeStamp) {\\n // 重置帧调度标志,允许后续帧请求\\n _hasScheduledFrame = false;\\n try {\\n // 阶段1:处理瞬态回调(最高优先级,如动画)\\n // --------------------------------------------------\\n _frameTimelineTask?.start(\'Animate\'); // 标记动画阶段开始\\n _schedulerPhase = SchedulerPhase.transientCallbacks; // 更新调度阶段标识\\n \\n // 获取当前注册的所有瞬态回调(按优先级存储的动画回调)\\n final Map<int, _FrameCallbackEntry> callbacks = _transientCallbacks;\\n _transientCallbacks = <int, _FrameCallbackEntry>{}; // 清空原回调列表\\n \\n // 遍历执行所有有效的瞬态回调(跳过被主动移除的回调)\\n callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) {\\n if (!_removedIds.contains(id)) {\\n // 执行回调并传入当前帧时间戳和调试堆栈信息\\n _invokeFrameCallback(callbackEntry.callback, _currentFrameTimeStamp!, callbackEntry.debugStack);\\n }\\n });\\n _removedIds.clear(); // 清空移除ID列表\\n \\n } finally {\\n // 无论是否发生异常,都进入中间微任务阶段(Dart微任务队列处理)\\n _schedulerPhase = SchedulerPhase.midFrameMicrotasks;\\n }\\n}\\n
\\n源码赏析:
\\nhandleBeginFrame
方法被调用,重置_hasScheduledFrame
标志。
设置调度阶段为SchedulerPhase.transientCallbacks
,处理动画相关回调。
遍历_transientCallbacks
集合,执行所有未移除的动画回调:
callbacks.forEach((int id, _FrameCallbackEntry entry) {\\n if (!_removedIds.contains(id)) {\\n _invokeFrameCallback(entry.callback, timestamp, entry.debugStack);\\n }\\n})\\n
\\n典型回调执行路径:
\\nAnimationController._tick()
→ 更新动画值(如_value
属性)setState()
→ 标记关联组件为脏(Element.markNeedsBuild()
)\\n此阶段处理所有动画(比如进度条
、转场效果
),更新动画数值。此处可能再次触发setState
,但会被控制在当前帧处理。小小心得:
\\n优先
于其他帧任务。setState
,但会被限制在当前帧处理。布局/绘制
,需等待后续阶段。Build Phase
)触发条件:存在被标记为脏的Element
(通过BuildOwner.scheduleBuildFor
注册)。
/// 执行脏元素的重建流程,这是Widget树更新的核心入口\\nvoid buildScope(Element context, [ VoidCallback? callback ]) {\\n // 直接委托给内部方法处理脏元素刷新\\n buildScope._flushDirtyElements(debugBuildRoot: context);\\n}\\n\\n/// 实际处理脏元素刷新的内部方法\\nvoid _flushDirtyElements({ required Element debugBuildRoot }) {\\n // 对脏元素按深度排序(保证父节点先于子节点重建)\\n _dirtyElements.sort(Element._sort); // 排序算法:Element.depth升序排列\\n _dirtyElementsNeedsResorting = false; // 重置排序标记\\n\\n // 遍历所有脏元素(使用安全索引访问,防止并发修改)\\n for (int index = 0; index < _dirtyElements.length; index = _dirtyElementIndexAfter(index)) {\\n final Element element = _dirtyElements[index];\\n // 验证元素是否属于当前构建范围(防止跨scope错误)\\n if (identical(element.buildScope, this)) {\\n // 执行元素重建(可能抛出异常,需要外层try/catch)\\n _tryRebuild(element);\\n }\\n }\\n}\\n\\n/// 尝试重建单个Element的封装方法\\nvoid _tryRebuild(Element element) {\\n // 核心操作:触发Element的rebuild流程\\n element.rebuild(); // → 调用performRebuild() → 触发State.build()\\n}\\n
\\n源码赏析:
\\nBuildOwner.buildScope
调用_flushDirtyElements
:
void buildScope(Element context) {\\n _dirtyElements.sort(Element._sort); // 按depth升序排列\\n for (int i = 0; i < _dirtyElements.length; i++) {\\n _tryRebuild(_dirtyElements[i]);\\n }\\n}\\n
\\n单个Element
重建流程:
element.rebuild()
→ performRebuild()
。StatefulElement
:调用State.build()
生成新Widget
。Widget树
对比(Widget.canUpdate
):\\nstatic bool canUpdate(Widget oldWidget, Widget newWidget) {\\n return oldWidget.runtimeType == newWidget.runtimeType \\n && oldWidget.key == newWidget.key;\\n}\\n
\\n子节点更新策略:
\\nElement
:当新旧Widget
类型和Key
匹配时,调用Element.update()
类型
或Key
不匹配时,触发Element.unmount()
和inflateWidget()
关键机制:
\\nDiff
算法最小化DOM
操作,优化性能。ParentData
变更等)。Layout Phase
)触发条件:存在被标记为需要布局的RenderObject
(通过markNeedsLayout
注册)。
/// 渲染管线中布局阶段的核心方法\\nvoid flushLayout() {\\n try {\\n // 可能存在多轮布局(父节点布局可能导致子节点需要重新布局)\\n while (_nodesNeedingLayout.isNotEmpty) {\\n // 步骤1:获取当前批次的脏节点并清空队列\\n final List<RenderObject> dirtyNodes = _nodesNeedingLayout;\\n _nodesNeedingLayout = <RenderObject>[]; // 创建新列表,允许新脏节点加入\\n \\n // 步骤2:按节点深度排序(父节点在前,子节点在后)\\n dirtyNodes.sort((RenderObject a, RenderObject b) => a.depth - b.depth);\\n\\n // 步骤3:遍历处理每个脏节点\\n for (int i = 0; i < dirtyNodes.length; i++) {\\n // 处理过程中可能有新脏节点加入(需要合并到当前批次)\\n if (_shouldMergeDirtyNodes) {\\n _shouldMergeDirtyNodes = false;\\n if (_nodesNeedingLayout.isNotEmpty) {\\n // 将剩余未处理的节点和新脏节点合并后重新处理\\n _nodesNeedingLayout.addAll(dirtyNodes.getRange(i, dirtyNodes.length));\\n break; // 退出当前循环,重新开始while流程\\n }\\n }\\n\\n final RenderObject node = dirtyNodes[i];\\n // 双重验证:节点仍需要布局且属于当前PipelineOwner\\n if (node._needsLayout && node.owner == this) {\\n // 执行核心布局计算(不处理大小变化)\\n node._layoutWithoutResize();\\n }\\n }\\n }\\n\\n // 步骤4:递归处理子PipelineOwner(用于多视图场景)\\n for (final PipelineOwner child in _children) {\\n child.flushLayout();\\n }\\n }\\n}\\n\\n/// 执行单个RenderObject的布局计算(不处理大小变化)\\nvoid _layoutWithoutResize() {\\n performLayout(); // 调用具体RenderObject的布局实现(如RenderBox子类)\\n // 无论是否成功,都清除布局标记\\n _needsLayout = false;\\n // 布局变化通常导致绘制变化,标记需要重绘\\n markNeedsPaint();\\n}\\n
\\n源码赏析:
\\nPipelineOwner.flushLayout()
处理脏节点:\\nvoid flushLayout() {\\n while (_nodesNeedingLayout.isNotEmpty) {\\n final List<RenderObject> dirtyNodes = _nodesNeedingLayout..sort(compareDepth);\\n for (RenderObject node in dirtyNodes) {\\n if (node._needsLayout && node.owner == this) {\\n node._layoutWithoutResize();\\n }\\n }\\n }\\n}\\n
\\nRenderObject
布局过程:\\n_needsLayout
标志。performLayout()
方法,计算size
和position
:\\nvoid performLayout() {\\n child.layout(constraints);\\n size = constraints.constrain(child.size);\\n}\\n
\\nchild.markNeedsLayout()
)。布局规则:
\\nlayout()
方法向子节点传递BoxConstraints
。RelayoutBoundary
):通过RenderObject
的isRepaintBoundary
属性优化重布局范围。Paint Phase
)触发条件:存在被标记为需要绘制的RenderObject
(通过markNeedsPaint
注册)。
void flushPaint() {\\n try {\\n // 步骤1:获取当前批次的脏节点并清空队列(允许新脏节点在绘制过程中加入)\\n final List<RenderObject> dirtyNodes = _nodesNeedingPaint;\\n _nodesNeedingPaint = <RenderObject>[]; \\n\\n // 步骤2:按节点深度降序排序(确保父节点先绘制,子节点覆盖在上层)\\n for (final RenderObject node in dirtyNodes..sort((a, b) => b.depth - a.depth)) {\\n // 验证节点是否需要绘制/图层更新,且属于当前PipelineOwner\\n if ((node._needsPaint || node._needsCompositedLayerUpdate) && node.owner == this) {\\n // 检查节点图层是否已附加到图层树\\n if (node._layerHandle.layer!.attached) {\\n if (node._needsPaint) {\\n // 核心操作:执行完整重绘(生成新的绘制指令)\\n PaintingContext.repaintCompositedChild(node);\\n } else {\\n // 优化操作:仅更新图层属性(如位置变换,不重新绘制内容)\\n PaintingContext.updateLayerProperties(node);\\n }\\n } else {\\n // 节点未附加到图层树,跳过绘制并清除标记\\n node._skippedPaintingOnLayer();\\n }\\n }\\n }\\n\\n // 步骤3:递归处理子PipelineOwner(多视图/多窗口场景)\\n for (final PipelineOwner child in _children) {\\n child.flushPaint();\\n }\\n } \\n}\\n\\n/// 重新绘制组合子节点,生成新的绘制指令\\nstatic void repaintCompositedChild(\\n RenderObject child, {\\n bool debugAlsoPaintedParent = false,\\n PaintingContext? childContext,\\n}) {\\n // 获取节点的OffsetLayer(所有RenderObject绘制的根图层)\\n OffsetLayer? childLayer = child._layerHandle.layer as OffsetLayer?;\\n // 清除图层更新标记\\n child._needsCompositedLayerUpdate = false;\\n \\n // 创建或复用绘制上下文(管理图层和Canvas)\\n childContext ??= PaintingContext(childLayer, child.paintBounds);\\n // 执行实际绘制操作,从节点坐标原点(0,0)开始\\n child._paintWithContext(childContext, Offset.zero);\\n}\\n\\n/// 封装绘制操作的安全执行流程\\nvoid _paintWithContext(PaintingContext context, Offset offset) {\\n // 调用具体RenderObject的paint方法(子类重写的绘制逻辑)\\n paint(context, offset);\\n}\\n
\\n源码赏析:
\\nPipelineOwner.flushPaint()
处理脏节点:\\nvoid flushPaint() {\\n final List<RenderObject> dirtyNodes = _nodesNeedingPaint..sort(reverseCompareDepth);\\n for (RenderObject node in dirtyNodes) {\\n if (node._layerHandle.layer!.attached) {\\n if (node._needsPaint) {\\n PaintingContext.repaintCompositedChild(node);\\n } else {\\n PaintingContext.updateLayerProperties(node);\\n }\\n }\\n }\\n}\\n
\\nPaintingContext
持有Canvas
和ContainerLayer
。paintChild()
递归绘制子节点。RenderObject
将绘制指令写入PictureLayer
。平移
、旋转
)生成TransformLayer
。SceneBuilder.build()
生成Scene
对象。绘制优化:
\\nRepaintBoundary
):隔离绘制区域,避免无关区域重绘。Layer
):复用未变化的绘制结果,减少GPU
负载。Skia
直接将绘制指令转为OpenGL/Metal
指令。光栅化
与GPU
渲染光栅化(Rasterization
)处理:
1、引擎接收Scene
对象:
void render(Scene scene) {\\n nativeWindow.render(scene);\\n}\\n
\\n2、Skia
引擎处理:
Path
、Text
等)转换为栅格化位图。Blend Mode
)、滤镜
等效果。3、GPU
渲染:
API
(Android:OpenGL/Vulkan,iOS:Metal
)。GPU
命令缓冲区,等待VSync
信号。垂直同步(VSync
):
帧渲染完成
与屏幕刷新
周期对齐,避免画面撕裂。Front Buffer
用于显示,Back Buffer
用于渲染,VSync
时交换。简易版流程如下:
\\nflowchart TD\\n A[调用 setState] --\x3e B[标记 Element 为脏]\\n B --\x3e C[调度新帧]\\n C --\x3e D{等待 VSync 信号}\\n D --\x3e|触发| E[下一帧开始]\\n E --\x3e F[动画阶段]\\n F --\x3e G[构建阶段]\\n G --\x3e H[Widget 树对比]\\n H --\x3e|类型/key 匹配| I[复用 Element]\\n H --\x3e|类型/key 不匹配| J[重建 Element]\\n I --\x3e K[继续后续流程]\\n J --\x3e K[继续后续流程]\\n K --\x3e L[布局阶段]\\n L --\x3e M[绘制阶段]\\n M --\x3e N[光栅化 & GPU 渲染]\\n N --\x3e T[屏幕显示]\\n
\\n精简可概括为:
\\nsetState
就像人体呼吸中的“吸气”
动作 —— 看似简单,实则需要全身器官精密配合。它的价值不仅在于触发界面更新,更在于其背后声明式UI
的设计哲学:开发者只需关心状态变化,框架自动处理复杂渲染
。但越是自动化的机制,越需要开发者保持敬畏:不假思索的滥用setState
可能导致应用“气喘吁吁”
(性能问题),而精准控制重建范围则能让界面“行云流水”
。
当你下次按下这个“魔法按钮”
时,不妨想象Flutter
正在幕后完成一场精妙的交响乐演出:从状态标记到像素渲染,每个环节都严丝合缝。理解这套机制,不仅是为了解决性能问题,更是为了与框架达成默契 —— 毕竟,最好的代码,往往是框架与开发者共同谱写的诗篇。
\\n","description":"前言 为什么说setState是Flutter开发者的“第一把钥匙”?\\n\\n刚接触Flutter时,你可能觉得setState像是魔法 —— 轻轻一调用,界面就自动刷新了。但当你深入开发复杂应用时,可能会遇到界面卡顿、无效重绘等问题,这时候才意识到,这把“钥匙”背后的机制远比想象中精妙。\\n\\nsetState绝不仅仅是一个简单的刷新按钮,它背后串联着Flutter的声明式UI框架、高效渲染管线,甚至藏着性能优化的密码。今天我们就来拆解这个“老朋友”的工作机制,让你真正理解它如何让界面“活”起来。\\n\\n操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。\\n\\n一…","guid":"https://juejin.cn/post/7494448379179958311","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-18T06:35:09.803Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7cc75db74e00409c9d54eb6e3e9c75d1~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1745761448&x-signature=N0ZT6xvUb1YyKgxypxkDjdfDfqA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f61ca12ec9a147d480968df077bc63eb~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1745761448&x-signature=F%2FP6ji02hk7BCJcYm%2FKQxEfPbTI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1e8641e237404550aaedc7fd98c473af~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1745761448&x-signature=TZPbPnzGRlsbuSeHeVnlLbEfJm4%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 简介","url":"https://juejin.cn/post/7494112146094391336","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
2025.1 版本已经发布,在此之前我们就聊过该版本的 《Terminal 又发布全新重构版本》,而现在 2025.1 中的 K2 模式也成为了默认选项。
\\n\\n\\n可以预见,这个版本可能会包含不少大坑,为下个 Android Studio 祈祷。
\\n
首先有一点可以确定,随着 K2 模式成为默认选项,虽然 K1模式仍可使用,但后续 K1 将不再支持新语言特性和 IDE 优化,所以在 2025.1 里,你依然可以在 Language & FRameworks > Kotlin
里关闭 K2 模式,但是只能说「逃得了一时是一时」:
\\n\\n“K2” 对应的是 Kotlin 插件包含用于代码分析的 K2 Kotlin 编译器的内部版本,而 K1 模式使用 K1 编译器,IntelliJ IDEA 中内置的 Kotlin 编译器版本完全独立于项目构建文件中指定的版本,但它可能会影响项目中支持的 Kotlin 版本范围。
\\n
在之前的《K2 模式已发布稳定版》我们提到过,IntelliJ IDEA 的 K2 模式并不依赖于项目构建设置中指定的 Kotlin 编译器版本,K2 模式代表 IDE 中对 Kotlin 编辑场景支持的几乎完全重写,而使用 K2 编译器可以带来:
\\n所以基于 K2 编译器能力的 K2 模式,在 Kotlin 代码分析、补全和导航速度方面取得了巨大进步,这体现在全新 IDE 下就是:
\\n\\n\\n而 IDE 一直以来都深度依赖 K1 ,所以其实在迁移到 K2 的过程中,会有大量的重写和功能重构,这也是 K2 到现在还一直需要打磨的原因。
\\n
而在 K1 到 K2 的变化里,其中最大的就是全新的 Kotlin Analysis API ,它提供了一种清晰稳定的方式来访问代码信息,并且不需要依赖编译器内部结构。
\\nKotlin Analysis API 在 K2 模式里采用模块化架构,支持 IDE 功能(如补全、检查)独立调用编译器的特定分析阶段(如解析或类型检查),避免不必要的全量分析,从而提升性能。
\\n\\n\\nK2 编译器放弃了 K1 中隐式的 laziness,转而采用显式的分阶段代码分析架构, K2 编译器会分阶段分析代码(如 SUPER_TYPES 计算类的 supertypes ,TYPES 处理函数 signature types),每个阶段逐步为抽象语法树(AST)添加语义信息。
\\n
另外,在 K1 模式里 Kotlin Analysis API 的分析操作通常受限于单一的全局解析锁(global resolution lock),例如 Find Usages 等功能会持有全局锁,导致其他功能(如代码高亮、补全)无法并行执行,造成性能瓶颈。
\\n而 K2 编译器的解析逻辑是并发容忍(concurrency-tolerant),Kotlin Analysis API 利用这一特性,消除了全局锁,API 现在支持同时分析多个声明(declarations),即使在复杂场景(如两个函数相互调用)中也能高效处理:
\\n\\n\\n虽然 K2 编译器目前还是使用单线程分析(因为多模块并行编译已利用多核),但是目前的并发容忍设计为未来的多线程分析奠定了基础,后续 Kotlin Analysis API 可无缝适配。
\\n
而在插件迁移适配上,Kotlin Analysis API 也起到了很大作用,它封装了所有复杂的解析逻辑,并提供具有清晰且可预测行为的记录抽象,开发者只需请求他需要的语义代码信息片段,API 会处理所有延迟和并行分析并缓存结果。
\\n例如要获取表达式类型,开发者只需调用库提供的 KtExpression.expressionType
扩展属性 ,如果类型尚不清楚,则将自动分析包含声明的 body :
fun KtExpression.hasStringType(): Boolean {\\n analyze(this) {\\n return expressionType == builtinTypes.string\\n }\\n}\\n
\\n而事实上,不管你的项目是不是 K2 ,都可以享受到新 IDE 在 K2 模式下带来的性能提升,而如果你开发或使用的是插件,那么可能会需要将部分依赖旧的 Kotlin 插件进行迁移,例如:
\\nSPECIAL_ANNOTATION_NAME = FqName(\\"my.app.Special\\")\\n\\nfun hasAnnotation(declaration: KtDeclaration): Boolean {\\n val bindingContext = declaration.analyze()\\n val descriptor = bindingContext[BindingContext.DECLARATION_TO_DESCRIPTOR, declaration]\\n ?: return false\\n\\n return descriptor.annotations.hasAnnotation(SPECIAL_ANNOTATION_NAME)\\n}\\n\\n
\\nval SPECIAL_ANNOTATION_CLASS_ID = ClassId.fromString(\\"my/app/Special\\")\\n\\nfun hasAnnotation(declaration: KtDeclaration): Boolean {\\n analyze(declaration) {\\n return SPECIAL_ANNOTATION_CLASS_ID in declaration.symbol.annotations\\n }\\n}\\n
\\n另外,就像前面说的,关于一些新特性,比如 Kotlin 2.1 的新支持就会仅限于 K2 模式的 IDE,例如:
\\nfun processList(elements: List<Int>): Boolean {\\n for (element in elements) {\\n val variable = element.nullableMethod() ?: run {\\n log.warning(\\"Element is null or invalid, continuing...\\")\\n continue // 在早期不能在 forEach 的 lambda 中直接使用 continue\\n }\\n if (variable == 0) return true // If variable is zero, return true\\n }\\n return false\\n}\\n
\\nsealed interface Animal {\\n data class Cat(val mouseHunter: Boolean) : Animal {\\n fun feedCat() {}\\n }\\n\\n data class Dog(val breed: String) : Animal {\\n fun feedDog() {}\\n }\\n}\\n\\nfun feedAnimal(animal: Animal) {\\n when (animal) {\\n // Branch with only the primary condition. Returns `feedDog()` when `Animal` is `Dog`\\n is Animal.Dog -> animal.feedDog()\\n // Branch with both primary and guard conditions. Returns `feedCat()` when `Animal` is `Cat` and is not `mouseHunter`\\n is Animal.Cat if !animal.mouseHunter -> animal.feedCat()\\n // Returns \\"Unknown animal\\" if none of the above conditions match\\n else -> println(\\"Unknown animal\\")\\n }\\n}\\n
\\n$$
表示需要两个美元符号 ($$
) 来触发插值,防止 $schema
、$id
和 $dynamicAnchor
被解释为插值标记:val KClass<*>.jsonSchema : String\\n get() = $$\\"\\"\\"\\n {\\n \\"$schema\\": \\"https://json-schema.org/draft/2020-12/schema\\",\\n \\"$id\\": \\"https://example.com/product.schema.json\\",\\n \\"$dynamicAnchor\\": \\"meta\\"\\n \\"title\\": \\"$${simpleName ?: qualifiedName ?: \\"unknown\\"}\\",\\n \\"type\\": \\"object\\"\\n }\\n \\"\\"\\"\\n
\\n最后,未来即将更新的 Android Studio 也会一样默认开始 K2 模式,所以作为 Android 开发,对于下一个版本的 AS 更新还需要慎重,因为你也不知道会有什么坑在等着你。
\\n当然,最后还是要强调:
\\nK2 Mode 是 IDEA 利用 K2 编译器的前端(解析和语义分析部分)来增强 IDE 的 Kotlin 代码理解能力,但不直接参与项目的实际编译,功能范围仅限于 IDE 的交互体验。
\\n所以理论上,你项目用 K1,也不影响你体验 K2 Mode ,但是如果你项目使用 K2,那么强烈建议开始 K2 Mode 来体验全新的特性。
\\n那么,你已经体验过 K2 模式了么?
\\n在上一篇,我们了解了如何绘制三维空间中的 三角形。本文将进一步基于三角形绘制更为复杂的形体。
\\n下面是一个正八边形,它可以看成是由 8 个共顶点的三角形构成的,橙色点时绘制三角形所需的顶点。想绘制出正多边形, 本质问题就在于:
\\n\\n\\n如何计算这些橙色点在坐标系中的坐标。
\\n
记,八边形中分割的等边三角形顶角为 θ ,腰长为 r ,其实很容易计算出:
\\n\\n\\n第 2 个点坐标:
\\n(r * cos(θ), r * sin(θ))
\\n第 3 个点坐标:(r * cos(2θ), r * sin(2θ))
\\n第 4 个点坐标:(r * cos(3θ), r * sin(3θ))
\\n... 第 n 个点坐标:(r * cos((n-1)*θ), r * sin((n-1)*θ))
于是可以根据分析的公式,通过代码来创建符合正多边形的顶点列表:
\\nclass Shape3d {\\n List<Point3D> circle(double r, int splitCount) {\\n Point3D center = Point3D(0, 0, 0);\\n List<Point3D> vertexes = [center];\\n\\n double thta = 2 * pi / splitCount;\\n int count = splitCount + 2;\\n for (int n = 1; n < count; n++) {\\n double rad = (n - 1) * thta;\\n double x = r * cos(rad);\\n double y = r * sin(rad);\\n double z = 0;\\n vertexes.add(Point3D(x, y, z));\\n }\\n return vertexes;\\n }\\n}\\n
\\n根据上一篇中使用顶点列表绘制三角形的方式,就可以在坐标轴上得到如下的正八边形图案:
\\nList<Point3D> points3d = Shape3d().circle(4, 8);\\nPath path = buildPathByPoints3d(points3d, type: DrawArrays.triangleFan);\\ncanvas.drawPath(path, paint);\\n
\\n当正多边形的边数越来越大,将会近似于一个圆形。如下所示,通过输入框控制边数,就可以动态地查看边数逐渐增加的效果:
\\n代码中为画板提供变量即可,数值由输入框控制,输入框内容改变时通知画板重绘:
\\nint sideCount = present.count;\\nList<Point3D> points3d = Shape3d().circle(4, sideCount);\\nPath path = buildPathByPoints3d(points3d, type: DrawArrays.triangleFan);\\ncanvas.drawPath(path, paint);\\n
\\n我们还可以做一些更有趣的事,比如将圆周上的点向上移动,顶点不变,就可以得到一个圆锥。如下所示:
\\n可以在 Shape3d
中封装一个 cone
方法绘制圆锥,可以传入 z 轴坐标。通过旋转空间,可以
List<Point3D> cone(double r, int splitCount, {double z = 0}) {\\n Point3D center = Point3D(0, 0, 0);\\n List<Point3D> vertexes = [center];\\n double thta = 2 * pi / splitCount;\\n int count = splitCount + 2;\\n for (int n = 1; n < count; n++) {\\n double rad = (n - 1) * thta;\\n double x = r * cos(rad);\\n double y = r * sin(rad);\\n vertexes.add(Point3D(x, y, z));\\n }\\n return vertexes;\\n}\\n
\\n在 OpenGL 标准中,绘制三维点集除了三角形之外,还有点和线。这里通过 螺旋线
介绍一下另外几种点集的绘制方式:
enum DrawArrays {\\n points, // 点\\n lines, // 独立线段\\n lineStrip, // 连续折线\\n lineLoop, // 闭合折线\\n triangles, // 独立三角形\\n triangleStrip, // 三角形带\\n triangleFan, // 扇形\\n}\\n
\\n上面螺旋线折线在绘制时用的 lineStrip
模式,点集将以此连接成为折线。绘制是时只需将 path 移到第一点,然后遍历剩余点,通过 lineTo
连接即可:
Path _buildLineStripPath(List<Offset> points) {\\n Path path = Path();\\n if (points.isEmpty) return path;\\n path.moveTo(points[0].dx, points[0].dy);\\n for (int i = 1; i < points.length; i++) {\\n path.lineTo(points[i].dx, points[i].dy);\\n }\\n return path;\\n}\\n
\\n如下所示 lines
模式的每两个顶点组成一条独立的线段。绘制时遍历点集列表,两次处理两个点,形成两点之间的线段路径:
Path _buildLinesPath(List<Offset> points) {\\n Path path = Path();\\n for (int i = 0; i + 1 < points.length; i += 2) {\\n path\\n ..moveTo(points[i].dx, points[i].dy)\\n ..lineTo(points[i + 1].dx, points[i + 1].dy);\\n }\\n return path;\\n}\\n
\\nlineLoop
模式绘制闭合的曲线,只需要在 lineStrip
基础上增加连接起点的操作即可:
Path _buildLineLoopPath(List<Offset> points) {\\n Path path = _buildLineStripPath(points);\\n if (points.length > 2) {\\n path.close(); // 回到起点\\n }\\n return path;\\n}\\n
\\npoints
模式绘制点,这里遍历点集,通过 addOval 添加小圆点路径:
Path _buildPointsPath(List<Offset> points) {\\n Path path = Path();\\n for (Offset p in points) {\\n path.addOval(Rect.fromCircle(center: p, radius: 1)); // 小圆点\\n }\\n return path;\\n}\\n
\\n另外四种绘制方式介绍完毕,现在看一下螺旋线的绘制。在数学中,可以通过参数方程给出螺旋线的表达式:
\\nx(t) = r⋅cos(2πnt)\\ny(t) = r⋅sin(2πnt)\\nz(t) = h⋅t\\n
\\n其中:
\\n于是在代码层面,可以基于此来收集符合螺旋线规律的点集合:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n参数 | 说明 |
---|---|
radius | 螺旋的半径(即绕 z 轴旋转的圆的半径) |
height | 总高度(z 方向的位移) |
turns | 旋转的圈数(每圈 2π 弧度) |
splitCount | 将螺旋细分为多少段(决定曲线的精细程度) |
List<Point3D> helicoid(double radius, double height, int turns, int splitCount) {\\n List<Point3D> vertexes = [];\\n double totalAngle = 2 * pi * turns;\\n for (int i = 0; i <= splitCount; i++) {\\n double t = i / splitCount;\\n double angle = t * totalAngle;\\n double x = radius * cos(angle);\\n double y = radius * sin(angle);\\n double z = height * t;\\n vertexes.add(Point3D(x, y, z));\\n }\\n return vertexes;\\n}\\n
\\n其实,任何有规则的图形,我们只要直到构成它的规律。进行采样,就可以通过收集点的方式,从而将它在三维空间中表达出来。比如:
\\n\\n\\n空间中的贝塞尔曲线:
\\n
\\n\\n球体
\\n
更多文章和视频知识资讯,大家可以关注我的公众号、掘金和 B 站 。
\\n","description":"在上一篇,我们了解了如何绘制三维空间中的 三角形。本文将进一步基于三角形绘制更为复杂的形体。 1. 绘制正多边形\\n\\n下面是一个正八边形,它可以看成是由 8 个共顶点的三角形构成的,橙色点时绘制三角形所需的顶点。想绘制出正多边形, 本质问题就在于:\\n\\n如何计算这些橙色点在坐标系中的坐标。\\n\\n记,八边形中分割的等边三角形顶角为 θ ,腰长为 r ,其实很容易计算出:\\n\\n第 2 个点坐标: (r * cos(θ), r * sin(θ))\\n 第 3 个点坐标: (r * cos(2θ), r * sin(2θ))\\n 第 4 个点坐标: (r * cos(3θ…","guid":"https://juejin.cn/post/7493792787399786536","author":"张风捷特烈","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-17T01:21:11.294Z","media":[{"url":"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b8abe49c453a455c84182bd427623b7f~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1920&h=1200&s=436495&e=png&b=fefbfb","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0f13d845b9314a0cb53316d46e78af0f~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=926&h=607&s=1814578&e=gif&f=32&b=020205","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3ad399a6ee1a491b82d781bf66d9d454~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1600&h=1000&s=50313&e=png&b=000000","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9a633f6446c4405999bec247d20e9cec~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1000&h=580&s=158751&e=png&b=010101","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9c6c126f66ca46429a85a3444e2457c3~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=926&h=607&s=577897&e=gif&f=78&b=030306","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/187742741af348e48144fb7673e7c5cd~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=926&h=607&s=344605&e=gif&f=48&b=030306","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0f13d845b9314a0cb53316d46e78af0f~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=926&h=607&s=1814578&e=gif&f=32&b=020205","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8b817e04fc13431e96ef29e33deca459~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=600&h=348&s=1431964&e=gif&f=52&b=020204","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d8fd73dd943940bd89b8a8fbbf44bbd7~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=600&h=348&s=1592911&e=gif&f=57&b=020204","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fb5a209bb740464c9dcf7def1891f5be~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=600&h=348&s=1119345&e=gif&f=46&b=020204","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/aa1938b5b400459b822271fdb890af5f~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=600&h=348&s=709633&e=gif&f=24&b=020204","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/67c018c6c65c4f1683ad1699d59442a0~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1037&h=568&s=158031&e=png&b=010101","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/299317538b0f40ac832d97defd0d6de1~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1064&h=584&s=222236&e=png&b=010101","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"flutter工程化之动态配置","url":"https://juejin.cn/post/7493738108979707904","content":"很多公司在实际开发中都有不少动态化配置的需求,比如说要根据不同app渠道加载不同的URL、secrets等配置项。这其中的实现方法也有很多,今天的应用案例则是--dart-define
和--dart-define-from-file
。
使用方法简单粗暴,直接把如下的命令添加上就好,无论是flutter build
还是flutter build
都可以:
--dart-define API_URL=api.openflutter.dev\\n
\\n如果有需要添加多个变量也是可以的:
\\n --dart-define API_URL=api.openflutter.dev --dart-define MAP_KEY=ds5jkh2jjhjkljh\\n
\\n如果我有大量的变量怎么办?
\\n我们也可以从文件中加载配置文件,文件格式可以是json
也可以是.env
:
--dart-define-from-file path/to/config.json\\n--dart-define-from-file path/to/.env\\n
\\n很简单:
\\n\\nfinal url = const String.fromEnvironment(\\"API_URL\\");\\nfinal logEnabled = const bool.fromEnvironment(\\"LOG_ENABLED\\");\\nfinal level = const bool.fromEnvironment(\\"LEVEL\\");\\n\\n
\\n补充一句:
\\n\\n\\n千万别忘了写
\\nconst
。
如果你使用的.env
文件做为配置项,你也可以使用一个代码叫envied的代码生成工具,以简化相关操作。
但很多时候仅仅在Flutter端获取这些配置是不够的,我们也会需要在Android侧和iOS侧获取相关配置。
\\n找到app/build.gradle.kts
,旧的Flutter应该是app/build.gradle
,然后添加一些代码:
val dartDefines = mutableMapOf<String, String>()\\n\\nif (project.hasProperty(\\"dart-defines\\")) {\\n project.property(\\"dart-defines\\").toString().split(\\",\\").forEach { entry ->\\n val decoded = String(Base64.getDecoder().decode(entry), Charsets.UTF_8)\\n val pair = decoded.split(\\"=\\")\\n if (pair.size == 2) {\\n dartDefines[pair[0]] = pair[1]\\n }\\n }\\n}\\n\\n\\ndefaultConfig {\\n applicationId = \\"com.example.flutter_dash_testing\\"\\n minSdk = flutter.minSdkVersion\\n targetSdk = flutter.targetSdkVersion\\n versionCode = flutter.versionCode\\n versionName = flutter.versionName\\n dartDefines[\\"API_URL\\"]?.let {\\n resValue(\\"string\\", \\"api_url\\", it)\\n }\\n}\\n\\n
\\n当我们用 Flutter 编译 iOS 端时,会在./ios/Flutter
目录下生成两个文件Generated.xcconfig
、flutter_export_environment.sh
。\\n在这两个文件中我们都会过来的dart-define
:
// flutter_export_environment.sh\\nexport \\"DART_DEFINES=S0VZPWtleSBpcyB0ZXN0aW5n,QVBJX1VSTD1odHRwczovL2FwaS5leGFtcGxlLmNvbQ==,QVBQX05BTUU9SGVsbG8gV29ybGQ=,RkxVVFRFUl9BUFBfRkxBVk9SPXN0YWdpbmc=\\"\\n\\n
\\n// Generated.xcconfig\\nDART_DEFINES=S0VZPWtleSBpcyB0ZXN0aW5n,QVBJX1VSTD1odHRwczovL2FwaS5leGFtcGxlLmNvbQ==,QVBQX05BTUU9SGVsbG8gV29ybGQ=,RkxVVFRFUl9BUFBfRkxBVk9SPXN0YWdpbmc=\\n
\\n首先我们需要把这些经过base64编码过的变量解析到某个.xcconfig
中,然后再把该文件在Release.xcconfig
和Debug.xcconfig
引入,假设它的名字是GeneratedDartDefines.xcconfig
:
// ./ios/Flutter/Debug.xcconfig\\n\\n#include \\"Generated.xcconfig\\"\\n#include \\"GeneratedDartDefines.xcconfig\\"\\n\\n
\\n// ./ios/Flutter/Release.xcconfig\\n\\n#include \\"Generated.xcconfig\\"\\n#include \\"GeneratedDartDefines.xcconfig\\"\\n\\n
\\n假设我们要生成的文件名字为GeneratedDartDefines.xcconfig
。首先我们在./ios/Flutter
目录下生成一个空的GeneratedDartDefines.xcconfig
。然后打开Xcode,在Targets=>Runner=>Build Phases
选项卡中点击+号,选择New Run Script Phase
并把如下代码复制进去:
DART_DEFINES=$(cat \\"${SRCROOT}/Flutter/Generated.xcconfig\\" | grep \\"DART_DEFINES\\" | sed -E \'s/DART_DEFINES=(.+)/\\\\1/\')\\n\\nIFS=\',\' read -ra DEFINES <<< \\"$DART_DEFINES\\"\\nALL_DECODED=\\"\\"\\n\\nfor DEFINE in \\"${DEFINES[@]}\\"; do\\n # Decode Base64\\n DECODED=$(echo \\"$DEFINE\\" | base64 --decode)\\n echo \\"DECODED $DECODED\\"\\n # Skip Flutter internal variables\\n if [[ \\"$DECODED\\" != FLUTTER_* ]]; then\\n if [ -z \\"$ALL_DECODED\\" ]; then\\n ALL_DECODED=\\"$DECODED\\"\\n else\\n # Use proper newline insertion\\n ALL_DECODED=\\"${ALL_DECODED}\\"$\'\\\\n\'\\"${DECODED}\\"\\n fi\\n fi\\ndone\\n\\necho \\"$ALL_DECODED\\" > \\"${SRCROOT}/Flutter/GeneratedDartDefines.xcconfig\\"\\n\\n
\\n\\n\\n
最后,按需修改下Info.plist
即可,如修改app名字:
// ./ios/Runner/Info.plist\\n// ...\\n<key>CFBundleName</key>\\n<string>$(APP_NAME)</string>\\n// ...\\n\\n
\\n在绿色的运行按钮左侧有个下拉框,然后点击Edit Configurations
,其中有一个项目叫做Addtional run args
,把--dart-define-from-file .env
类似的参数添加进去就好了。当然你也可以选择保存配置,这样大家就可以共享这个快捷键了。
改改launch.json
:
{\\n \\"version\\": \\"0.2.0\\",\\n \\"configurations\\": [\\n {\\n \\"name\\": \\"Flutter Dev\\",\\n \\"request\\": \\"launch\\",\\n \\"type\\": \\"dart\\",\\n \\"flutterMode\\": \\"debug\\",\\n \\"args\\": [\\n \\"--dart-define=API_URL=https://api.dev.example.com\\",\\n \\"--dart-define=APP_ENVIRONMENT=development\\",\\n \\"--flavor=dev\\"\\n ]\\n },\\n {\\n \\"name\\": \\"Flutter Staging\\",\\n \\"request\\": \\"launch\\",\\n \\"type\\": \\"dart\\",\\n \\"flutterMode\\": \\"debug\\",\\n \\"args\\": [\\n \\"--dart-define-from-file=.env\\",\\n \\"--flavor=staging\\"\\n ]\\n }\\n ]\\n}\\n
\\n希望大家关注我的公众号OpenFlutter,感恩。
\\nDart 和Java一样,也是一个面向对象的编程语言。每一个对象都是一个类的实例,并且这些类都是从Object派生出来的,只有Null除外。这句话换个角度理解就是除了Null之外,所有的class都有父类。要么有直接通过imple或者是extend继承的父类;要么就是继承自默认的Object类。
\\n其实前面的一些例子里有声明过一些类。 通过class关键字去声明一个类。
\\nclass Person { //隐式继承自Object父类。\\n String name; //变量属性\\n int age;\\n String address;\\n\\n Person(this.name, this.age, this.address);//构造方法\\n\\n void fight() { //方法\\n print(\\"I am fighting\\");\\n }\\n}\\n
\\n类创建完了之后,我们通过new关键字去实例化一个类,也就是创建一个类对象。当然,new关键字也是可以省略的。
\\n var tony = Person(\\"Tony\\", 20, \\"New York\\");//省略了new\\n print(\'${tony.name} is ${tony.age} years old and lives in ${tony.address}\');\\n\\n var wang = Person(\\"Wang\\", 20, \\"New York\\");\\n print(\'${wang.name} is ${wang.age} years old and lives in ${wang.address}\');\\n
\\n构造函数是类中的一个特殊方法,所有类中的都会有构造函数,如果没有自定义构造函数,则该类就会有一个默认的构造函数,该构造函数没有任何参数。
\\n这里先介绍几个常用的构造函数
\\n默认构造函数
\\n如果定义了一个类,没有自定义构造函数,则该类就会使用默认构造函数。在实例化的时候就可以通过这个默认构造函数去创建改类的实例。
\\nclass Person { //隐式继承自Object父类。\\n void fight() { //方法\\n print(\\"I am fighting\\");\\n }\\n}\\n\\nvoid main() {\\n var tony = Person(); // 创建一个person实例,并赋值给tony这个变量\\n tony.fight() // 调用该实例的方法\\n}\\n
\\n如果自定义了一个构造函数,则该默认构造函数就不会存在了。
\\n自定义构造函数
\\n自定义构造函数最简单的方式就是命名一个和类同名的方法,不需要定义返回值,然后可以将一些需要在初始化的时候就赋值的参数放在构造方法中,由于Dart中具有丰富的可选参数功能,所以就不用像Java那样去定义多个构造函数来达到多态这种情况,但可以通过可选参数的方式去达到Java中那种多态的效果。
\\nclass Person { //隐式继承自Object父类。\\n String name; //变量属性\\n int age;\\n String? address;\\n\\n Person(this.name, this.age, [this.address]);//构造方法\\n\\n void fight() { //方法\\n print(\\"I am fighting\\");\\n }\\n}\\n\\nvoid main() {\\n var tony = Person(\\"Tony\\", 20, \\"New York\\");//创建一个person实例,并赋值给tony这个变量\\n print(\'${tony.name} is ${tony.age} years old and lives in ${tony.address}\'); //使用实例的属性\\n \\n var wang = Person(\\"Wang\\", 20);//创建一个person实例,并赋值给tony这个变量\\n print(\'${tony.name} is ${tony.age} years old.);\\n}\\n
\\n由于address这个参数是可选的,所以我可以在实例化的时候不给他赋值。
\\n常量构造函数
\\n如果你想要一个类在实例化之后,里面的成员属性的值是不可变的。那么可以用const来定义这个构造函数。同样,如果你的构造函数声明添加了const关键字,则它的属性就必须添加final声明。
\\n如:
\\nclass ImmutablePoint {\\n final int x;\\n final int y;\\n const ImmutablePoint(this.x, this.y); //常量构造函数\\n}\\n
\\n这是一个关于如何声明实例变量的例子,这个例子里包含了几种我们常见情况。
\\nclass Point {\\n double? x; // 声明一个变量x,初始化值为null\\n double z = 100; // 声明一个变量z,初始化值为0\\n final double y; // 声明一个变量y,未被初始化\\n late double sum = (x ?? 0) + y + z;//延迟初始化\\n\\n Point(this.y); // 创建这个类的时候必须初始化y,因为它是final类型且不为null\\n}\\n\\nvoid main() {\\n var point = Point(10.0);\\n point.x = 20.0;\\n print(point.sum);\\n}\\n
\\n有时候在允许期间,可能不知道该实例的类型是什么,则可以通过runtimeType参数获取。如:
\\nprint(\'The type of a is ${a.runtimeType}\');\\n
\\n这样就可以打印出a的类型的名字。
\\n这点Dart和Java有点不太一样,在Dart中,如果你用class定义了一类,这个了
\\n//person类,隐式接口类包含greet方法\\nclass Person {\\n // 这是一个只能在当前文件中可见的属性。之前了解过的。可以被当作interface的参数,被继承复写\\n final String _name;\\n\\n Person(this._name); //这是构造函数,不能被继承\\n\\n // 这是一个方法,可以被当作interface中的一个方法,可以被继承复写。\\n String greet(String who) => \'Hello, $who. I am $_name.\';\\n}\\n\\n// An implementation of the Person interface.\\nclass Impostor implements Person {\\n String get _name => \'\'; //复写父类的_name参数\\n\\n // 复写父类的greet方法\\n String greet(String who) => \'Hi $who. Do you know who I am?\';\\n}\\n\\nString greetBob(Person person) => person.greet(\'Bob\');\\n\\nvoid main() {\\n print(greetBob(Person(\'Kathy\')));\\n print(greetBob(Impostor()));\\n}\\n
\\n上面这个例子通过注释的方式简要的说明了一个class如何被当作一个隐式接口来用的。
\\nDart中的静态变量主要是通过static来修饰
\\n静态变量
\\nclass Queue {\\n static const initialCapacity = 16; // 静态变量\\n // ···\\n}\\n
\\n静态变量的用法和Java还是比较相似的。都是需要加static变量来修饰。
\\n静态方法
\\n静态方法和JAVA差不多,都是可以直接访问的,不需要创建实例。
\\nimport \'dart:math\';\\n\\nclass Point {\\n double x, y;\\n Point(this.x, this.y);\\n\\n static double distanceBetween(Point a, Point b) {\\n var dx = a.x - b.x;\\n var dy = a.y - b.y;\\n return sqrt(dx * dx + dy * dy);\\n }\\n}\\n\\nvoid main() {\\n var a = Point(2, 2);\\n var b = Point(4, 4);\\n var distance = Point.distanceBetween(a, b);\\n assert(2.8 < distance && distance < 2.9);\\n print(distance);\\n}\\n
\\n到此,差不多类的一些基本是介绍差不多。如果还想了解更多就去了解一些进阶的内容了。
","description":"Dart 和Java一样,也是一个面向对象的编程语言。每一个对象都是一个类的实例,并且这些类都是从Object派生出来的,只有Null除外。这句话换个角度理解就是除了Null之外,所有的class都有父类。要么有直接通过imple或者是extend继承的父类;要么就是继承自默认的Object类。 声明一个类\\n\\n其实前面的一些例子里有声明过一些类。 通过class关键字去声明一个类。\\n\\nclass Person { //隐式继承自Object父类。\\n String name; //变量属性\\n int age;\\n String address;\\n\\n Pe…","guid":"https://juejin.cn/post/7493497607865483264","author":"RichardLai68","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-16T09:13:53.182Z","media":null,"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"[flutter web] 通过 cloudflare worker 在国内用上 firebase","url":"https://juejin.cn/post/7493372995027304484","content":"对独立开发来说,firebase 这样的服务能节省很多时间和精力,但由于众所周知的原因,国内无法访问 firebase。
\\n本文通过 fork firebase flutter sdk 库进行魔改,将 firebase 域名更改为 cloudflare worker 的反向代理,实现国内使用 firebase 服务。
\\n需要修改的内容有两处:
\\n第一处在 packages\\\\firebase_auth\\\\firebase_auth_web\\\\lib\\\\src\\\\interop\\\\auth_interop.dart
\\n增加 dart 和 js 交互的 interop 文件中 apiHost 和 tokenApiHost 的定义
\\n@JS(\'Config\')\\n\\n@staticInterop\\n\\nabstract class Config {}\\n\\n\\n\\nextension ConfigExtension on Config {\\n\\n external JSString get apiHost;\\n\\n external set apiHost(JSString s);\\n\\n\\n\\n external JSString get tokenApiHost;\\n\\n external set tokenApiHost(JSString s);\\n\\n}\\n
\\nDart
\\n第二处在 packages\\\\firebase_auth\\\\firebase_auth_web\\\\lib\\\\src\\\\interop\\\\auth.dart
\\n和上面类似,在 dart 中声明这两个变量
\\n set languageCode(String? s) {\\n\\n jsObject.languageCode = s?.toJS;\\n\\n }\\n\\n\\n\\n String get configApiHost => jsObject.config.apiHost.toDart;\\n\\n\\n\\n set configApiHost(String s) {\\n\\n jsObject.config.apiHost = s.toJS;\\n\\n }\\n\\n \\n\\n String get configTokenHost => jsObject.config.tokenApiHost.toDart;\\n\\n set configTokenHost(String s) {\\n\\n jsObject.config.tokenApiHost = s.toJS;\\n\\n }\\n
\\nDart
\\n修改后,提交代码到 GitHub。
\\n先增加原有依赖:
\\ndependencies:\\n\\n firebase_auth_web: ^5.14.2\\n
\\nYAML
\\n再增加覆盖依赖:
\\ndependency_overrides:\\n\\n firebase_auth_web:\\n\\n git:\\n\\n url: https://github.com/p1gd0g/flutterfire.git\\n\\n path: packages/firebase_auth/firebase_auth_web\\n
\\nYAML
\\n表明在打包时使用自定义的依赖。添加依赖后最好进行一次 flutter clean。
\\n await Firebase.initializeApp(\\n\\n options: DefaultFirebaseOptions.currentPlatform,\\n\\n );\\n\\n\\n\\n FirebaseAuthWeb.instance.delegate.configApiHost = \'auth.xxx.com\';\\n\\n FirebaseAuthWeb.instance.delegate.configTokenHost = \'token.xxx.com\';\\n
\\nDart
\\n在 firebase 初始化后,手动修改域名。这样 firebase 连接就会导向 cloudflare worker。
\\n其中一个 worker 代码如下:
\\nconst API_HOST = \\"identitytoolkit.googleapis.com\\"\\n\\n\\n\\nexport default {\\n\\n async fetch(request, env, ctx) {\\n\\n let url = new URL(request.url);\\n\\n url.host = API_HOST;\\n\\n let forward = new Request(request);\\n\\n return await fetch(url, forward);\\n\\n } \\n\\n};\\n
\\nJavaScript
\\n通过 firebase auth 注册成功后,就可以在控制台看到账号了。
\\n不太理解,为啥国内大厂都不愿意做类似产品呢?
","description":"对独立开发来说,firebase 这样的服务能节省很多时间和精力,但由于众所周知的原因,国内无法访问 firebase。 本文通过 fork firebase flutter sdk 库进行魔改,将 firebase 域名更改为 cloudflare worker 的反向代理,实现国内使用 firebase 服务。\\n\\n1. fork firebase flutter sdk 代码仓库\\n\\n代码库为 github.com/firebase/fl…\\n\\n需要修改的内容有两处:\\n\\n第一处在 packages\\\\firebase_auth\\\\firebase_auth_web…","guid":"https://juejin.cn/post/7493372995027304484","author":"p1gd0g","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-15T16:28:36.419Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7229c2797f794e3eb82e28f1a733ee6e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgcDFnZDBn:q75.awebp?rk3s=f64ab15b&x-expires=1745339315&x-signature=J1ifpqy6k0KyZxuHct1rEhD3PGI%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["前端","Flutter","Firebase"],"attachments":null,"extra":null,"language":null},{"title":"[Flutter学习之Dart基础] - 控制语句","url":"https://juejin.cn/post/7493313134099365924","content":"控制语句是任何编程语言都必须掌握的,也就是我们写代码逻辑的时候,都是通过这些控制语句来完成我们的业务逻辑编写。比如循环、条件判断等之类。所以这里也主要是通过两个方面来学习Dart的控制语句吧。
\\n如果有别的语言基础的话,那应该都知道我们常用的循环控制有for,foreach,while,do...while这些。Dart中也是差不多的。
\\n这个算是比较熟悉且最常用的一个循环了。
\\nvar callbacks = [];\\nfor (var i = 0; i < 2; i++) {\\n callbacks.add(() => print(i)); //通过循环的方式往callbacks这个数组里添加匿名函数()=>print(i)\\n}\\n
\\n这是一个标准的for循环语句。但是学过Java以及kotlin或者JS的人应该都清楚,有时候其实挺烦这种标准方式的,尤其是循环输出一些内容且不需要用到indx下标的时候。那其实Dart也有简化的方式。
\\nfor (final c in callbacks) {\\n c();\\n}\\n
\\n还有更高级的用法,比如当我们在for循环里循环的元素是一个对象,而我们需要用到的只是其中一个或者其中的部分参数的时候。如:
\\n var people = [Person(\\"Mr.Li\\", 32, \\"ShangHai\\")];//这是一个Person对象\\n for (final Person(:name, :age) in people) {//当我只需要用到其中的2个属性name和age的时候这种写法更方便很多\\n print(\'$name is $age years old\'); // 省去了person.name, person.age的繁琐。\\n }\\n
\\n尤其是在逻辑复杂且这些字段使用频繁的时候,这种方式更能体现出它的优势。\\n到这里可能你会疑问,Dart中是否支持Foreach。答案是肯定的。如:
\\nvar collection = [1, 2, 3];\\n//标准写法是:\\ncolledtion.forEach((it){\\n print(it.name);\\n})\\n//简单写法,这是之前方法那部分有了解过的。\\ncollection.forEach(print); // 输出1 2 3 \\n
\\n这个其实没什么特别的内容了,和Java语言差不多。
\\nwhile (!isDone()) {\\n doSomething();\\n}\\n
\\ndo {\\n printLine();\\n} while (!atEndOfPage())\\n
\\n这2个循环的区别就是while是先执行判断条件,满足再进行循环;而do...while则是先执行循环,然后再判断是否要进行下一次循环。
\\n前面我们了解了几个循环的控制语句,和其它的大多数语言都差不多,相信大家也会疑问,如果在这些循环里使用break和continue是不是也一样的?答案是Yes。来几个官方的例子:
\\nwhile (true) {\\n if (shutDownRequested()) break;\\n processIncomingRequests();\\n}\\n// do...while\\ndo {\\n if (shutDownRequested()) break;\\n processIncomingRequests();\\n} while (true)\\n
\\nfor (int i = 0; i < candidates.length; i++) {\\n var candidate = candidates[i];\\n if (candidate.yearsExperience < 5) {\\n continue;\\n }\\n candidate.interview();\\n}\\n
\\n针对这个for循环的效果,官方还有另一种实现方式:
\\ncandidates\\n .where((c) => c.yearsExperience >= 5)// 过滤出满足这个条件的所有数据。\\n .forEach((c) => c.interview()); //拿到想要的数据之后再通过forEach的循环去执行。\\n
\\n\\n\\n这里其实是涉及到了集合的操作符的内容了。如果我没理解错的话,第二种方式相对来说性能会比第一种差一点。当然,数据量不大且操作次数不多的话,肯定是差异不明显,毕竟现如今的手机内存、CPU什么的都是强的飞起。但是如果是在一个数据量比较大的且处理逻辑比较复杂,运算量比较大的情况下,我觉得性能应该影响相对会大一点。因为第一种for循环只需要执行1次循环,而第二种方式,首先需要执行第一次循环去筛选搜有满足条件的数据,第二次则是循环把所有满足条件的数据再执行一次。
\\n虽然我们在评估算法的性能的时候,通常都是用O(n)来表示,忽略了O前面的系数。但是真实开发过程中我们很多时候比不能忽略这个问题。尤其是在一些操作复杂且耗时的逻辑中。举个例子:假设O(n) = 0.5ms。按照这个例子中的通过where和forEach的方式则可能需要1ms。因为where需要0.5ms。forEach在最坏可能的情况下也可能需要0.5ms。这里的0.5ms的差距看似短,但如果出现UI切屏或者变化的时候,肉眼是可见的。
\\n这是我对这个官方例子的一些个人看法。
\\n
标签是通过变量名后面加冒号(labelName:)的方式来定义的。需要配合break和continue来使用。
\\n\\n\\nBreak labelName; 跳出某个循环至这个label定义的地方
\\nContinue labelName:跳过后续代码执行,直接进入labelName所在的循环的下一轮执行。\\n早些年有接触过go语言的应该会很熟悉这个。近些年很多其它语言慢慢的其实也开始新增了这个特性。它主要的应用场景有2种。一种是多个嵌套循环;另一种是使用switch的时候。
\\n
先来看看break labelName几个实用场景:
\\nFor
\\nouterLoop:\\nfor (var i = 1; i <= 3; i++) { //外循环\\n for (var j = 1; j <= 3; j++) { // 内循环\\n print(\'i = $i, j = $j\');\\n if (i == 2 && j == 2) {\\n break outerLoop; //之间跳出外循环,整个这部分循环逻辑就彻底结束了。\\n }\\n }\\n}\\nprint(\'outerLoop exited\');\\n
\\nwhile
\\nvar i = 1;\\n\\nouterLoop:\\nwhile (i <= 3) { //外循环\\n var j = 1;\\n while (j <= 3) { //内训还\\n print(\'i = $i, j = $j\');\\n if (i == 2 && j == 2) {\\n break outerLoop;\\n }\\n j++;\\n }\\n i++;\\n}\\nprint(\'outerLoop exited\');\\n
\\ndo...while
\\nvar i = 1;\\n\\nouterLoop:\\ndo {\\n var j = 1;\\n do {\\n print(\'i = $i, j = $j\');\\n if (i == 2 && j == 2) {\\n break outerLoop;\\n }\\n j++;\\n } while (j <= 3);\\n i++;\\n} while (i <= 3);\\n\\nprint(\'outerLoop exited\');\\n
\\n以上三种情况的逻辑是一样的,目的也是一样的。输出的结果都是:
\\n\\n\\ni = 1, j = 1\\ni = 1, j = 2\\ni = 1, j = 3\\ni = 2, j = 1\\ni = 2, j = 2\\nouterLoop exited
\\n
再来看看continue labelName的例子:
\\nFor
\\nouterLoop:\\nfor (var i = 1; i <= 3; i++) { //外循环\\n for (var j = 1; j <= 3; j++) { //内循环\\n if (i == 2 && j == 2) {\\n continue outerLoop; //当i为2且j也是2的时候,直接跳过内循环,开始外循环的下一轮。\\n }\\n print(\'i = $i, j = $j\');\\n }\\n}\\n
\\nwhile
\\nvar i = 1;\\n\\nouterLoop:\\nwhile (i <= 3) { //外循环\\n var j = 1;\\n while (j <= 3) { //内循环\\n if (i == 2 && j == 2) {\\n i++;\\n continue outerLoop; //当i为2且j也是2的时候,直接跳过内循环,开始外循环的下一轮。\\n }\\n print(\'i = $i, j = $j\');\\n j++;\\n }\\n i++;\\n}\\n
\\ndo...while
\\nvar i = 1;\\n\\nouterLoop:\\ndo { //外循环\\n var j = 1;\\n do { //内循环\\n if (i == 2 && j == 2) {\\n i++;\\n continue outerLoop; //当i为2且j也是2的时候,直接跳过内循环,开始外循环的下一轮。\\n }\\n print(\'i = $i, j = $j\');\\n j++;\\n } while (j <= 3);\\n i++;\\n} while (i <= 3);\\n
\\n数据结果都是:
\\n\\n\\ni = 1, j = 1\\ni = 1, j = 2\\ni = 1, j = 3\\ni = 2, j = 1\\ni = 3, j = 1\\ni = 3, j = 2\\ni = 3, j = 3
\\n
switch (command) {\\n case \'OPEN\':\\n executeOpen();\\n continue newCase; // 执行完了之后,跳到newCase继续执行\\n\\n case \'DENIED\': // 这个case没有内容,会继续下一个case\\n case \'CLOSED\': \\n executeClosed(); //当case是denied和closed的时候,这行代码都会执行。可以理解为两个case对应一种情况,\\n\\n newCase: // 跳过来之后可以直接执行\\n case \'PENDING\':\\n executeNowClosed(); // 当command是‘open’的时候,open和pending都会执行。continue label则是一种情况可以执行两种case的代码\\n}\\n
\\n关于swith这部分,可以先了解一下就行,后面还会重点去讲这个关键字的用法。
\\n以上就是关于循环部分的的一些基础知识。
\\n这部分主要是了解一下条件控制语句在Dart中的使用。主要是以下三种:
\\n\\n\\nif 这个应该很熟悉,就是通过if判断条件
\\nIf - case 这个对我来说有点陌生,不知道你们了解不了解。
\\nSwitch,这个其实就是用来应对多个if的场景的情况
\\n
这个很熟悉,看一下这个官方例子就好了,和其它语言应该是一样的,尤其是Java语言。if(一个bool值或者可以返回bool值的表达式或者方法)。
\\nif (isRaining()) {\\n you.bringRainCoat();\\n} else if (isSnowing()) {\\n you.wearJacket();\\n} else {\\n car.putTopDown();\\n}\\n
\\n这个对我来说是一种新知识点,后面也会单独去讲解这个东西。可以简单的先了解一下if中也支持这种模式就行。
\\nvoid main() {\\n var pair = [1, 2]; // 数组[1,2]\\n printList(pair);\\n\\n var pair2 = {2, 3}; // 这里这个pair2就不满足,是一个对象,\\n printList(pair2);\\n}\\n\\nvoid printList(obj) {// 这是一个没有确定类型的参数\\n if (obj case [int x, int y]) { //这里的意思就是pair如果是一个包含了2个数字的列表,这该条件就为true,输出这2个数字。\\n print(\'$x and $y\');\\n }\\n}\\n
\\n同样,这个模式一样适用于Switch,一会儿又例子可以展示一下。
\\nvar command = \'OPEN\';\\nswitch (command) {\\n case \'CLOSED\':\\n executeClosed();\\n case \'PENDING\':\\n executePending();\\n case \'APPROVED\':\\n executeApproved();\\n case \'DENIED\':\\n executeDenied();\\n case \'OPEN\':\\n executeOpen();\\n default:\\n executeUnknown();\\n}\\n
\\nswitch (command) {\\n case \'OPEN\':\\n executeOpen();\\n continue newCase; \\n case \'DENIED\': \\n case \'CLOSED\':\\n executeClosed(); \\n newCase:\\n case \'PENDING\':\\n executeNowClosed();\\n}\\n
\\n比如如上这种情况(这个之前的contine labelName中也提到过的例子),当case \'DENIED‘ 命中的时候,默认会继续向下执行,执行case \'CLOSED\'的代码表达式片段。如果你不想这样,那就得在里面case中添加break
\\n case \'DENIED\': // Empty case falls through.\\n break;\\n case \'CLOSED\':\\n executeClosed(); // Runs for both DENIED and CLOSED,\\n
\\nvar obj = 1;\\n\\nvar x = switch(obj){\\n 1 => \\"String\\",\\n 2 => \\"Int\\",\\n _ => \\"Default\\" //这里不能用default\\n }\\n\\n print(switch(obj){\\n 1 => \\"String\\",\\n 2 => \\"Int\\",\\n _ => \\"Default\\" //这里不能用default\\n });\\n\\nreturn switch(obj){\\n 1 => \\"String\\",\\n 2 => \\"Int\\",\\n _ => \\"Default\\" //这里不能用default\\n }\\n
\\n这里要注意的是“_”这个符号,它的作用和default其实是一样的,但是这个地方不能用default去替代。
\\nswitch (charCode) {\\n case slash || star || plus || minus: \\n token = operator(charCode);\\n case comma || semicolon: \\n token = punctuation(charCode);\\n case >= digit0 && <= digit9: \\n token = number();\\n default:\\n throw FormatException(\'Invalid\');\\n}\\n
\\n这个其实还可以换种写法
\\ntoken = switch (charCode) { // 直接让switch返回一个值,然后将这个值赋给token,这样代码是不是有简化了一些。\\n slash || star || plus || minus => operator(charCode),\\n comma || semicolon => punctuation(charCode),\\n >= digit0 && <= digit9 => number(),\\n _ => throw FormatException(\'Invalid\'),\\n};\\n
\\n总结一下Switch表达式和switch语句的区别:
\\n穷尽检查是一个编译时检查,这个主要是体现在使用switch的时候,编译器能检测到使用的case是一个可以穷举的情况,然后检测到你的case没有满足所有情况,则提示的一个错误信息。如:
\\n var nullableBool = true;\\n switch (nullableBool) { //nullbaleBool 是一个bool值,要么true,要么false\\n case true:\\n print(\'yes\');\\n }\\n
\\n这段代码是编译不通过的,编译器会提示:The type \'bool\' is not exhaustively matched by the switch cases since it doesn\'t match \'false\'。大概意思是这个bool类型没有完全匹配switch的所有case,因为没有匹配到false的情况。
\\n这个其实在使用enums和sealed类型的时候,表现的更明显:
\\nsealed class Shape {}\\n\\nclass Square implements Shape {\\n final double length;\\n Square(this.length);\\n}\\n\\nclass Circle implements Shape {\\n final double radius;\\n Circle(this.radius);\\n}\\n\\ndouble calculateArea(Shape shape) => switch (shape) {\\n Square(length: var l) => l * l,\\n Circle(radius: var r) => math.pi * r * r,\\n};\\n
\\n如果候选在基于Shape扩展了一个Triangle类时,这个calculateArea就会报错了。但是,如果你这个Triangle又不想出现在这个方法里,因为不需要计算它的面积大话。那你就可以添加\\"_\\"这个符号如:
\\ndouble calculateArea(Shape shape) => switch (shape) {\\n Square(length: var l) => l * l,\\n Circle(radius: var r) => math.pi * r * r,\\n _ => 0\\n};\\n
\\n但是在真实开发过程中,不是很推荐使用\\"_\\"这个默认情况,因为这样容易导致这个Shape类被扩展了,但是calculateArea方法忘记补充了,从而导致bug之类的。当然这个还是编码规范问题,看具体要求。
\\n我也确定这个翻译是不是准确的,以前也没接触过这个特性和关键字。先看例子吧:
\\nint number = 5;\\n\\nswitch (number) {\\n case 5 when number > 0:\\n print(\'Positive five\');\\n break;\\n case 5 when number < 0:\\n print(\'Negative five\'); // 不会执行,因为 number 是正数\\n break;\\n case int n when n.isEven:\\n print(\'$n is even\');\\n break;\\n case int n when n.isOdd:\\n print(\'$n is odd\');\\n break;\\n default:\\n print(\'Unknown number\');\\n}\\n
\\n官方给的模版是这样的:
\\n// Switch statement:\\nswitch (something) {\\n case somePattern when some || boolean || expression:\\n // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Guard clause.\\n body;\\n}\\n\\n// Switch expression:\\nvar value = switch (something) {\\n somePattern when some || boolean || expression => body,\\n // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Guard clause.\\n}\\n\\n// If-case statement:\\nif (something case somePattern when some || boolean || expression) {\\n // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Guard clause.\\n body;\\n}\\n
\\n从这个模板来看,这个在switch语句和switch表达式中都是支持的,而且在if中也是支持的。我感觉这个在权限使用的情况下应该很方便使用,可以完全将业务逻辑和权限分开。case后面是业务逻辑,when后面就是放权限的判断。
\\n关于switch的用法其实还有很多丰富的内容以及情况,这个作为初学者不一定理解,而且这个需要深挖才能了解清楚,所以作为初学Flutter的人来说,可以不用了解这么深,不理解就可以直接跳过就好了。知道一下有这些高级用法,后续可以进阶。
","description":"控制语句是任何编程语言都必须掌握的,也就是我们写代码逻辑的时候,都是通过这些控制语句来完成我们的业务逻辑编写。比如循环、条件判断等之类。所以这里也主要是通过两个方面来学习Dart的控制语句吧。 循环控制\\n条件控制\\n循环控制\\n\\n如果有别的语言基础的话,那应该都知道我们常用的循环控制有for,foreach,while,do...while这些。Dart中也是差不多的。\\n\\nFor\\n\\n这个算是比较熟悉且最常用的一个循环了。\\n\\nvar callbacks = [];\\nfor (var i = 0; i < 2; i++) {\\n callbacks.add(() =…","guid":"https://juejin.cn/post/7493313134099365924","author":"RichardLai68","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-15T08:23:28.928Z","media":null,"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter PlatformChannel","url":"https://juejin.cn/post/7493098560977715200","content":"//定义通道名称(需与原生端保持一致)\\nfinal MethodChannel _channel = MethodChannel(\'com.example.method_channel\');\\n//调用原生端方法\\nFuture<void> _callNativeMethod() async {\\n try {\\n final String result = await _channel.invokeMethod(\'getAndroidInfo\');\\n print(\'Android 返回信息: $result\');\\n } on PlatformException catch (e) {\\n print(\'调用失败: ${e.message}\');\\n }\\n}\\n//处理原生端调用\\n_channel.setMethodCallHandler(_handleMethodCall);\\nFuture<dynamic> _handleMethodCall(MethodCall call) async {\\n if (call.method == \'callFlutterFun\') {\\n print(\'Android 调用时传入的参数: ${call.arguments}\');\\n return \'Flutter received\';\\n return result;\\n }\\n return null;\\n}\\n
\\nclass MainActivity : FlutterActivity() {\\n private val METHOD_CHANNEL = \\"com.example.method_channel\\"\\n\\n override fun configureFlutterEngine(flutterEngine: FlutterEngine) {\\n super.configureFlutterEngine(flutterEngine)\\n //\\n val channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, METHOD_CHANNEL)\\n channel.setMethodCallHandler { call, result ->\\n //处理 Flutter 端调用\\n if (call.method == \\"getAndroidInfo\\") {\\n result.success(\\"Hello from Android\\")\\n } else {\\n result.notImplemented()\\n }\\n }\\n }\\n fun callFlutterMethod() {\\n val channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, METHOD_CHANNEL)\\n //调用 Flutter 端方法\\n channel.invokeMethod(\\"callFlutterFun\\", \\"Hello from Android\\")\\n }\\n}\\n
\\n//定义通道名称(需与原生端保持一致)\\nfinal EventChannel _channel = EventChannel(\'com.example.event_channel\');\\n//监听原生端事件\\nvoid _listenToEvent() {\\n _channel.receiveBroadcastStream().listen(\\n (event) => print(\\"接收到 Android 的事件数据: $event\\"),\\n onError: (error) => print(\\"错误: $error\\"),\\n );\\n}\\n
\\nclass MainActivity : FlutterActivity() {\\n private val EVENT_CHANNEL = \\"com.example.event_channel\\"\\n\\n override fun configureFlutterEngine(flutterEngine: FlutterEngine) {\\n super.configureFlutterEngine(flutterEngine)\\n //\\n val channel = EventChannel(flutterEngine.dartExecutor.binaryMessenger, EVENT_CHANNEL)\\n channel.setStreamHandler(object : EventChannel.StreamHandler {\\n //\\n private var eventSink: EventChannel.EventSink? = null\\n\\n override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {\\n //\\n eventSink = events\\n //模拟发送事件\\n for (i in 1..5) {\\n eventSink?.success(\\"Event $i from Android\\")\\n Thread.sleep(1000) //模拟延迟\\n }\\n }\\n\\n override fun onCancel(arguments: Any?) {\\n //释放资源\\n eventSink = null\\n }\\n })\\n }\\n}\\n
\\nfinal BasicMessageChannel<String> _channel = BasicMessageChannel(\'com.example.message_channel\', StringCodec());\\n\\nvoid _sendMessage() {\\n//处理来自原生端的消息\\n_channel.setMessageHandler((message) async {\\n print(\'Received message: $message\');\\n //业务处理\\n return \'Reply from Flutter\';\\n });\\n}\\n//给原生端发消息\\nvoid _sendMessage() {\\n _channel.send(\'Hello from Flutter\')\\n .then((reply) => print(\'Received reply: $reply\'))\\n .catchError((error) => print(\'发送失败: ${error.message}\'));\\n}\\nvoid _sendMessage2() async {\\n try {\\n String reply = await _channel.send(\'Hello from Flutter\');\\n print(\'Received reply: $reply\');\\n } on PlatformException catch (e) {\\n print(\'发送失败: ${e.message}\');\\n }\\n}\\n
\\nclass MainActivity : FlutterActivity() {\\n private val MESSAGE_CHANNEL = \\"com.example.message_channel\\"\\n \\n override fun configureFlutterEngine(flutterEngine: FlutterEngine) {\\n super.configureFlutterEngine(flutterEngine)\\n //\\n val channel = BasicMessageChannel(flutterEngine.dartExecutor.binaryMessenger, MESSAGE_CHANNEL, StringCodec.INSTANCE)\\n channel.setMessageHandler { message, reply ->\\n //处理来自 Flutter 的消息\\n println(\\"Received message: $message\\")\\n //通过 reply 给 Flutter 端作回应\\n reply.reply(\\"Reply from Android\\")\\n }\\n }\\n //\\n fun sendMessage() {\\n val channel = BasicMessageChannel(flutterEngine.dartExecutor.binaryMessenger, MESSAGE_CHANNEL, StringCodec.INSTANCE)\\n //给 Flutter 端发消息\\n channel.send(\\"Hello from Android\\") { reply ->\\n println(\\"Received reply: $reply\\")\\n }\\n }\\n}\\n
","description":"Flutter PlatformChannel PlatformChannel 平台通道是实现 Flutter 与原生平台(比如 Android 和 IOS 等)之间通信的核心机制,通过三种不同类型的平台通道来实现数据传递和方法调用\\nPlatformChannel 分为 MethodChannel、EventChannel 和 BasicMessageChannel\\nMethodChannel 方法通道\\nMethodChannel 是最常用的通道,用于 Flutter 端和原生端之间进行方法调用和返回结果,适合一次性调用(比如获取设备信息)\\n双向同…","guid":"https://juejin.cn/post/7493098560977715200","author":"louisgeek","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-15T05:33:01.811Z","media":null,"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 图标和按钮组件","url":"https://juejin.cn/post/7492987953251663910","content":"在 Flutter 应用开发中,图标和按钮是构建用户界面不可或缺的元素。图标能够以直观的图形方式传达信息,增强应用的视觉吸引力;而按钮则是用户与应用进行交互的重要途径。本文将详细介绍 Flutter 中图标和按钮组件的使用,涵盖基础用法、样式定制、事件处理等方面,并结合丰富的代码示例进行深入讲解。
\\nFlutter 提供了丰富的内置图标库,通过 Icon
组件可以轻松使用这些图标。以下是一个简单的示例:
import \'package:flutter/material.dart\';\\n\\nvoid main() {\\n runApp(MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n home: Scaffold(\\n appBar: AppBar(\\n title: Text(\'基础图标示例\'),\\n ),\\n body: Center(\\n child: Icon(\\n Icons.favorite,\\n color: Colors.red,\\n size: 48,\\n ),\\n ),\\n ),\\n );\\n }\\n}\\n
\\n在上述代码中,Icon
组件的第一个参数 Icons.favorite
指定了要显示的图标,这里使用的是一个心形图标。color
属性设置图标的颜色为红色,size
属性设置图标的大小为 48 像素。
除了使用内置图标,还可以使用自定义图标字体。首先,需要将图标字体文件(通常是 .ttf
格式)添加到项目的 assets
目录下,并在 pubspec.yaml
中进行配置:
flutter:\\n fonts:\\n - family: MyCustomIcons\\n fonts:\\n - asset: assets/fonts/MyCustomIcons.ttf\\n
\\n然后,通过 IconData
来使用自定义图标:
import \'package:flutter/material.dart\';\\n\\nconst IconData myCustomIcon = IconData(0xe800, fontFamily: \'MyCustomIcons\');\\n\\nvoid main() {\\n runApp(MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n home: Scaffold(\\n appBar: AppBar(\\n title: Text(\'自定义图标示例\'),\\n ),\\n body: Center(\\n child: Icon(\\n myCustomIcon,\\n color: Colors.blue,\\n size: 48,\\n ),\\n ),\\n ),\\n );\\n }\\n}\\n
\\n这里的 0xe800
是自定义图标在字体文件中的 Unicode 码点。
可以使用 Flutter 的动画机制为图标添加动画效果。以下是一个简单的图标缩放动画示例:
\\nimport \'package:flutter/material.dart\';\\n\\nvoid main() {\\n runApp(MyApp());\\n}\\n\\nclass MyApp extends StatefulWidget {\\n @override\\n _MyAppState createState() => _MyAppState();\\n}\\n\\nclass _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {\\n late AnimationController _controller;\\n late Animation<double> _animation;\\n\\n @override\\n void initState() {\\n super.initState();\\n _controller = AnimationController(\\n vsync: this,\\n duration: Duration(seconds: 1),\\n );\\n _animation = Tween<double>(begin: 1.0, end: 2.0).animate(_controller);\\n _controller.repeat(reverse: true);\\n }\\n\\n @override\\n void dispose() {\\n _controller.dispose();\\n super.dispose();\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n home: Scaffold(\\n appBar: AppBar(\\n title: Text(\'图标动画示例\'),\\n ),\\n body: Center(\\n child: ScaleTransition(\\n scale: _animation,\\n child: Icon(\\n Icons.star,\\n color: Colors.yellow,\\n size: 48,\\n ),\\n ),\\n ),\\n ),\\n );\\n }\\n}\\n
\\n在这个示例中,使用 AnimationController
和 Tween
创建了一个缩放动画,并将其应用到 Icon
组件上。
Flutter 提供了多种类型的按钮,如 ElevatedButton
、TextButton
、OutlinedButton
等。以下是它们的基本用法示例:
import \'package:flutter/material.dart\';\\n\\nvoid main() {\\n runApp(MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n home: Scaffold(\\n appBar: AppBar(\\n title: Text(\'基础按钮示例\'),\\n ),\\n body: Center(\\n child: Column(\\n mainAxisAlignment: MainAxisAlignment.center,\\n children: [\\n ElevatedButton(\\n onPressed: () {\\n print(\'ElevatedButton 被点击\');\\n },\\n child: Text(\'ElevatedButton\'),\\n ),\\n TextButton(\\n onPressed: () {\\n print(\'TextButton 被点击\');\\n },\\n child: Text(\'TextButton\'),\\n ),\\n OutlinedButton(\\n onPressed: () {\\n print(\'OutlinedButton 被点击\');\\n },\\n child: Text(\'OutlinedButton\'),\\n ),\\n ],\\n ),\\n ),\\n ),\\n );\\n }\\n}\\n
\\nElevatedButton
是带有阴影的凸起按钮,TextButton
是纯文本按钮,OutlinedButton
是带有边框的按钮。onPressed
属性是按钮的点击事件回调函数。
可以通过 ButtonStyle
来定制按钮的样式。以下是一个定制 ElevatedButton
样式的示例:
import \'package:flutter/material.dart\';\\n\\nvoid main() {\\n runApp(MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n home: Scaffold(\\n appBar: AppBar(\\n title: Text(\'按钮样式定制示例\'),\\n ),\\n body: Center(\\n child: ElevatedButton(\\n onPressed: () {\\n print(\'定制按钮被点击\');\\n },\\n style: ButtonStyle(\\n backgroundColor: MaterialStateProperty.all(Colors.green),\\n foregroundColor: MaterialStateProperty.all(Colors.white),\\n shape: MaterialStateProperty.all(\\n RoundedRectangleBorder(\\n borderRadius: BorderRadius.circular(10),\\n ),\\n ),\\n ),\\n child: Text(\'定制按钮\'),\\n ),\\n ),\\n ),\\n );\\n }\\n}\\n
\\n在这个示例中,通过 ButtonStyle
的 backgroundColor
属性设置按钮的背景颜色,foregroundColor
属性设置按钮的文本颜色,shape
属性设置按钮的形状。
可以将图标和文本组合在按钮中,增强按钮的表现力。以下是一个带图标的 ElevatedButton
示例:
import \'package:flutter/material.dart\';\\n\\nvoid main() {\\n runApp(MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n home: Scaffold(\\n appBar: AppBar(\\n title: Text(\'带图标的按钮示例\'),\\n ),\\n body: Center(\\n child: ElevatedButton.icon(\\n onPressed: () {\\n print(\'带图标按钮被点击\');\\n },\\n icon: Icon(Icons.add),\\n label: Text(\'添加\'),\\n ),\\n ),\\n ),\\n );\\n }\\n}\\n
\\nElevatedButton.icon
构造函数用于创建带图标的按钮,icon
参数指定图标,label
参数指定文本。
在实际应用中,按钮可能会有不同的状态,如可用、禁用等。可以通过控制 onPressed
属性来实现按钮的状态管理。以下是一个示例:
import \'package:flutter/material.dart\';\\n\\nvoid main() {\\n runApp(MyApp());\\n}\\n\\nclass MyApp extends StatefulWidget {\\n @override\\n _MyAppState createState() => _MyAppState();\\n}\\n\\nclass _MyAppState extends State<MyApp> {\\n bool _isButtonEnabled = true;\\n\\n void _toggleButtonState() {\\n setState(() {\\n _isButtonEnabled = !_isButtonEnabled;\\n });\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n home: Scaffold(\\n appBar: AppBar(\\n title: Text(\'按钮状态管理示例\'),\\n ),\\n body: Center(\\n child: Column(\\n mainAxisAlignment: MainAxisAlignment.center,\\n children: [\\n ElevatedButton(\\n onPressed: _isButtonEnabled\\n ? () {\\n print(\'按钮被点击\');\\n }\\n : null,\\n child: Text(\'按钮\'),\\n ),\\n TextButton(\\n onPressed: _toggleButtonState,\\n child: Text(_isButtonEnabled ? \'禁用按钮\' : \'启用按钮\'),\\n ),\\n ],\\n ),\\n ),\\n ),\\n );\\n }\\n}\\n
\\n在这个示例中,通过 _isButtonEnabled
变量控制按钮的可用状态,当 _isButtonEnabled
为 false
时,ElevatedButton
的 onPressed
属性为 null
,按钮变为禁用状态。
在实际开发中,图标和按钮常常组合使用,以提供更好的用户体验。以下是一个示例:
\\nimport \'package:flutter/material.dart\';\\n\\nvoid main() {\\n runApp(MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n home: Scaffold(\\n appBar: AppBar(\\n title: Text(\'图标和按钮组合应用示例\'),\\n ),\\n body: Center(\\n child: Row(\\n mainAxisAlignment: MainAxisAlignment.spaceEvenly,\\n children: [\\n IconButton(\\n onPressed: () {\\n print(\'图标按钮被点击\');\\n },\\n icon: Icon(Icons.share),\\n ),\\n ElevatedButton(\\n onPressed: () {\\n print(\'带图标和文本的按钮被点击\');\\n },\\n child: Row(\\n mainAxisSize: MainAxisSize.min,\\n children: [\\n Icon(Icons.save),\\n SizedBox(width: 8),\\n Text(\'保存\'),\\n ],\\n ),\\n ),\\n ],\\n ),\\n ),\\n ),\\n );\\n }\\n}\\n
\\n在这个示例中,使用了 IconButton
和带有图标的 ElevatedButton
,展示了图标和按钮的组合应用。
Flutter 提供的图标和按钮组件为开发者构建丰富多样的用户界面提供了强大的支持。通过灵活运用基础图标、自定义图标字体、各种类型的按钮以及样式定制、事件处理等功能,开发者可以创建出具有良好交互性和视觉效果的应用。在实际开发中,根据具体需求合理组合和使用图标与按钮,能够提升应用的用户体验和可用性。希望本文对你在 Flutter 中使用图标和按钮组件有所帮助。
","description":"引言 在 Flutter 应用开发中,图标和按钮是构建用户界面不可或缺的元素。图标能够以直观的图形方式传达信息,增强应用的视觉吸引力;而按钮则是用户与应用进行交互的重要途径。本文将详细介绍 Flutter 中图标和按钮组件的使用,涵盖基础用法、样式定制、事件处理等方面,并结合丰富的代码示例进行深入讲解。\\n\\n1. Flutter 图标组件\\n1.1 基础图标使用\\n\\nFlutter 提供了丰富的内置图标库,通过 Icon 组件可以轻松使用这些图标。以下是一个简单的示例:\\n\\nimport \'package:flutter/material.dart\';\\n\\nvoid m…","guid":"https://juejin.cn/post/7492987953251663910","author":"顾林海","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-15T01:13:09.468Z","media":null,"categories":["Android","前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter空安全最小必备知识","url":"https://juejin.cn/post/7492964297602547752","content":"从Flutter 2开始,Flutter便在配置中默认启用了空安全,通过将空检查合并到类型系统中,可以在开发过程中捕获这些错误,从而防止再生产环境导致的崩溃。
\\n时至今日,空安全已经是一个屡见不鲜的话题,目前像主流的编程语言Kotlin、Swift、Rust 等都对空安全有自己的支持。Dart从2.12版本开始支持了空安全,通过空安全开发人员可以有效避免null错误崩溃。空安全性可以说是Dart语言的重要补充,它通过区分可空类型和非可空类型进一步增强了类型系统。
\\nDart 的空安全支持基于以下三条核心原则:
\\n在引入空安全前Dart的类型系统是这样的:
\\n意味着在之前,所有的类型都可以为Null,也就是Nul类型被看作是所有类型的子类。
\\n在引入空安全之后:
\\n可以看出,最大的变化是将Null类型独立出来了,这意味着Null不在是其它类型的子类型,所以对于一个非Null类型的变量传递一个Null值时会报类型转换错误。
\\n\\n\\n提示:在使用了空安全的Flutter或Dart项目中你会经常看到
\\n?.、!、late
的大量应用,那么他们分别是什么又改如何使用呢?请看下文的分析
我们可以通过将?
跟在类型的后面来表示它后面的变量或参数可接受Null:
class CommonModel { \\n String? firstName; // 可空的成员变量 \\n int getNameLen(String? lastName /*可空的参数*/) { \\n int firstLen = firstName?.length ?? 0; \\n int lastLen = lastName?.length ?? 0; \\n return firstLen + lastLen; \\n } \\n}\\n
\\n对于可空的变量或参数在使用的时候需要通过Dart 的避空运算符?.
来进行访问,否则会抛出编译错误。
当程序启用空安全后,类的成员变量默认是不可空的,所以对于一个非空的成员变量需要指定其初始化方式:
\\nclass CommonModel { \\n List names=[];//定义时初始化 \\n final List colors;//在构造方法中初始化 \\n late List urls;//延时初始化 \\n CommonModel(this.colors); \\n ...\\n
\\n对于无法在定义时进行初始化,并且又想避免使用?.
,那么延迟初始化可以帮到你。通过late
修饰的变量,可以让开发者选择初始化的时机,并且在使用这个变量时可以不用?.
。
late List urls;//延时初始化 setUrls(List urls){ this.urls=urls; } int getUrlLen(){ return urls.length; }\\n
\\n延时初始化虽然能为我们编码带来一定便利,但如果使用不当会带来空异常的问题,所以在使用的时候一定保证赋值和访问的顺序,切莫颠倒。
\\n在Flutter中State的initState
方法中初始化的一些变量是比较适合使用late来进行延时初始化的,因为在Widget生命周期中initState
方法是最先执行的,所以它里面初始化的变量通过late
修饰后既能保障使用时的便利,又能防止空异常,看下具体的用法:
\\nclass _TravelPgeState extends State<TravelPge> with TickerProviderStateMixin {\\n List<TravelTab> tabs = []; \\n TravelCategoryModel? travelTabModel; \\n late TabController _controller; \\n @override \\n void initState() { \\n super.initState(); \\n _controller = TabController(length: 0, vsync: this); \\n } \\n ...\\n
\\n当我们排除变量或参数的可空的可能后,可以通过!
来告诉编译器这个可空的变量或参数不可空,这对我们进行方法传参或将可空参数传递给一个不可空的入参时特别有用:
get _listView => ListView(\\n children: [\\n BannerWidget(bannerList: bannerList), \\n LocalNavWidget(localNavList: localNavList), \\n if (gridNavModel != null) GridNavWidget(gridNavModel: gridNavModel!), \\n SubNavWidget(suNavList: subNavList), \\n if (salesBoxModel != null) SalesBoxWidget(salesBox: salesBoxModel!), \\n _logoutBtn, \\n const SizedBox( \\n height: 800, \\n child: ListTile(\\n title: Text(\'哈哈\'), \\n ), \\n ) \\n ], \\n);\\n
\\n上述代码是根据gridNavModel与salesBoxModel模块数据是否为空时动态创建的列表,在确保变量不为空的情况下使用了空值断言操作符!
。
除此之外,!
还有一个常见的用处:
bool isEmptyList(Object object) {\\n if (object is! List) return false; \\n return object.isEmpty; \\n}\\n
\\n用在这里表示取反,上述代码等价于:
\\nbool isEmptyList(Object object) {\\n if (!(object is List)) return false; \\n return object.isEmpty; \\n}\\n
\\n自定义Widget的空安全适配分两种情况:
\\n对于自定的Widget无论是页面的某控件还是整个页面,通常都会为Widget定义一些属性。在进行空安全适配时要对属性进行一下分类:
\\n?
进行修饰required
进行修饰/// H5容器 \\nclass HiWebView extends StatefulWidget {\\n final String? url; \\n final String? statusBarColor;\\n final String? title; \\n final bool? hideAppBar;\\n final bool? backForbid; \\n const HiWebView(\\n {super.key, \\n this.url, \\n this.statusBarColor, \\n this.title, \\n this.hideAppBar, \\n this.backForbid}); \\n ...\\n
\\n\\n\\nTip:如果构造方法中使用了
\\n@required
那么需要改成required
。
State的空安全适配主要是根据它的成员变量是否可空进行分类:
\\n可空的变量:通过?
进行修饰
不可空的变量:可采用以下两种方式进行适配
\\nlate
修饰为延时变量class _TravelPgeState extends State<TravelPge> with TickerProviderStateMixin {\\n List<TravelTab> tabs = [];//定义时初始化 \\n TravelCategoryModel? travelTabModel;\\n late TabController _controller;//延时初始 \\n @override \\n void initState() {\\n super.initState();\\n _controller = TabController(length: 0, vsync: this);\\n } \\n ...\\n
","description":"从Flutter 2开始,Flutter便在配置中默认启用了空安全,通过将空检查合并到类型系统中,可以在开发过程中捕获这些错误,从而防止再生产环境导致的崩溃。 1. 什么是空安全\\n\\n时至今日,空安全已经是一个屡见不鲜的话题,目前像主流的编程语言Kotlin、Swift、Rust 等都对空安全有自己的支持。Dart从2.12版本开始支持了空安全,通过空安全开发人员可以有效避免null错误崩溃。空安全性可以说是Dart语言的重要补充,它通过区分可空类型和非可空类型进一步增强了类型系统。\\n\\n1.1 引入空安全的好处\\n可以将原本运行时的空值引用错误将变为编辑时的分…","guid":"https://juejin.cn/post/7492964297602547752","author":"浅忆无痕","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-14T13:51:10.636Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6ce4e0b765604bd7a69c03018b6334e0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5rWF5b-G5peg55eV:q75.awebp?rk3s=f64ab15b&x-expires=1745243482&x-signature=W1LkWKAF6zFt8JFRbBhO%2F5MV7tY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/bc4bcfcdac03445f8d797b31c7a84db6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5rWF5b-G5peg55eV:q75.awebp?rk3s=f64ab15b&x-expires=1745243482&x-signature=BpN72hJim5Yo4XtBUkbV6MHmnEU%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["前端","Flutter","Android"],"attachments":null,"extra":null,"language":null},{"title":"Flutter Widget、Element 和 RenderObject 的区别","url":"https://juejin.cn/post/7492975598873427994","content":"Flutter Widget、Element 和 RenderObject 的区别 Widget、Element 和 RenderObject 的三者通过分工(描述、管理和渲染)和协作,实现高效、灵活","description":"Flutter Widget、Element 和 RenderObject 的区别 Widget、Element 和 RenderObject 的三者通过分工(描述、管理和渲染)和协作,实现高效、灵活","guid":"https://juejin.cn/post/7492975598873427994","author":"louisgeek","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-14T11:49:39.016Z","media":null,"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 文本组件深度剖析:从基础到高级应用","url":"https://juejin.cn/post/7492702134732947466","content":"引言 在 Flutter 应用开发中,文本是向用户传达信息的重要媒介。Flutter 提供了丰富且强大的文本组件和相关属性,使开发者能够轻松实现多样化的文本展示效果。无论是简单的静态文本显示,还是复杂","description":"引言 在 Flutter 应用开发中,文本是向用户传达信息的重要媒介。Flutter 提供了丰富且强大的文本组件和相关属性,使开发者能够轻松实现多样化的文本展示效果。无论是简单的静态文本显示,还是复杂","guid":"https://juejin.cn/post/7492702134732947466","author":"顾林海","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-14T07:59:21.179Z","media":null,"categories":["Android","前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"【Flutter 状态管理 - 叁】 | 提升对状态管理的认知","url":"https://juejin.cn/post/7492712203477467175","content":"本篇将从问题出发,直击状态管理的核心逻辑,回答三个关键问题:1、状态管理究竟在管理什么?2、为什么需要状态管理方案? 3、如何选择适合的方案?","description":"本篇将从问题出发,直击状态管理的核心逻辑,回答三个关键问题:1、状态管理究竟在管理什么?2、为什么需要状态管理方案? 3、如何选择适合的方案?","guid":"https://juejin.cn/post/7492712203477467175","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-14T05:25:53.238Z","media":null,"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"[Flutter学习之Dart基础] - Dart方法基础","url":"https://juejin.cn/post/7492797097432842249","content":"前面学习完Flutter的如何声明变量的基础知识之后,那么接下来就是该学习如何声明方法了。Function的用法非常灵活和多样,作为初学者的话,先掌握一些基础,以便后续学习Flutter可以更容易上手,等具备一些基础之后再去进阶会容易很多。个人见解,也是我目前的一个学习方式。
\\nDart中声明一个方法和Java其实很相似的,但是限制条件却没有Java那么多。如:
\\nbool isNode(int atomicNumber) {\\n return _nobleGases[atomicNumber] != null\\n}\\n
\\n省略返回值也是可以的:
\\nisNode(int atomicNumber) {\\n return _nobleGases[atomicNumber] != null\\n}\\n
\\n但是,如果是作为公开的方法的话,不是很推荐这种不明确返回值的写法,这样对于使用的人来说不确定这个方法的返回值是什么类型。\\n针对这种只有一行表达式的方法,还可以简化一下写法:
\\nisNode(int atomicNumber) => _nobleGases[atomicNumber] != null\\n
\\nDart中方法传参可以说是非常灵活了。可以像Java一样的普通穿惨,如:
\\nbool isNode(int atomicNumber) {\\n return _nobleGases[atomicNumber] != null\\n}\\n
\\n还可以通过命名参数的方式,如:
\\n/// Sets the [bold] and [hidden] flags ...\\nvoid enableFlags({bool? bold, bool? hidden}) {\\n ...\\n}\\n
\\n命名参数的所有参数默认都是可选的,如果想要其中一个参数成为必须传,有两种方式。 第一种方式是移出{},如:
\\n/// Sets the [bold] and [hidden] flags ...\\nvoid enableFlags(bool? bold,{bool? hidden}) { //参数bold为必传参数\\n ...\\n}\\n
\\n第二种方式是通过require关键字,如:
\\nconst Scrollbar({super.key, required Widget child});\\n
\\n两种方式没什么太大的区别,看习惯吧,移出{}的好处就是可以不用通过参数名的方式去给参数赋值,但是得按照参数顺序传值;通过require关键字的好处就是参数顺序可以不用固定,只要参数命名对的就行。所以看个人习惯吧。\\n针对可选参数,还有一种方式,就是用[]符号包裹。如:
\\n/// Sets the [bold] and [hidden] flags ...\\nvoid enableFlags(bool? bold,[bool? hidden]) { //参数bold为必传参数\\n ...\\n}\\n
\\n{}和[]的主要区别在于{}是命名参数的可选方式,而[]包括的可选参数是强调位置的,也就是说传递参数必须按位置顺序。这其实就透露出一个问题,如果可选参数超过2个,那如果第一个参数不传的话,第二个参数也是没办法穿的。所以说如果当你的可选参数只有一个的时候,用[]会方便一点,如果超过2个的话,除非这2个参数有依赖性,也就是传后面的参数如果要传,则前面的参数也是一定会传的情况下可以使用。否则用{}会更方便一些。\\n注:如果是可选参数,除非设置默认值,否则都需要声明为可空参数。
\\n在Dart语言中,官方有一个说法叫:函数是一级对像,因此方法也是可以作为参数的,如:
\\nvoid printElement(int element) {\\n print(element);\\n}\\nvar list = [1, 2, 3];\\n// Pass printElement as a parameter.\\nlist.forEach(printElement);\\n
\\n而且还可以给方法声明为一个变量,如:
\\nvar loudify = (msg) => \'!!! ${msg.toUpperCase()} !!!\';\\nassert(loudify(\'hello\') == \'!!! HELLO !!!\');\\n
\\n给方法声明一个变量的方式常见于匿名函数,Lambdas或者是闭包, 如:
\\n const list = [\'apples\', \'bananas\', \'oranges\'];\\n \\n upperCase(item) => item.toUpperCase();\\n var uppercaseList = list.map(upperCase).toList();\\n \\n Function(dynamic) printString = (dynamic item) => print(\'$item: ${item.length}\');\\n uppercaseList.forEach(printString);\\n
\\n直接上官方例子,其实关键词挺难理解的,我也不太理解词法闭包的标准含义是什么。但不得不说这个还挺有意思的,就像创建对象一样创建方法。这也印证了前面提到的方法是一级对象这个说法。
\\n/// Returns a function that adds [addBy] to the\\n/// function\'s argument.\\nFunction makeAdder(int addBy) {\\n return (int i) => addBy + i;\\n}\\n\\nvoid main() {\\n // Create a function that adds 2.\\n var add2 = makeAdder(2);\\n\\n // Create a function that adds 4.\\n var add4 = makeAdder(4);\\n\\n assert(add2(3) == 5);\\n assert(add4(3) == 7);\\n}\\n
\\n按照官方的说法就是,所有的方法都有返回值.要么有具体的返回值,要么就是默认返回null。如:
\\nfoo() {}//这个方法的没有写返回值,但是系统会有一个默认返回值,返回的是null\\nprint(foo() == null);\\n
\\n之前在写Java的时候,一旦遇到需要返回多个值的时候,要么创建对象,要么通过Pair或者Triple这种关键字把它包裹起来。而在Dart中,相对就更方便一些,不需要任何关键字去包裹
\\n(String, int) foo() { //(String, int)定义好了多个返回参数的类型\\n return (\'something\', 42);\\n}\\n
\\n当你需要有规律的生成一组数据时,你就可以考虑使用这个生成器了。Dart中的生成器支持两种不同的方式,分别是:
\\nIterable<int> naturalsTo(int n) sync* {\\n int k = 0;\\n while (k < n) yield k++;\\n}\\n
\\nStream<int> asynchronousNaturalsTo(int n) async* {\\n int k = 0;\\n while (k < n) yield k++;\\n}\\n
\\n细心的人可能会发现,这两种方式的不同主要是sync和async的区别。是的,其实Dart中很多的所谓同步和异步,都是通过这种方式去实现的。甚至后续自己在创建一些方法的时候,如果想用异步的方式去返回内容,都可以通过加上async关键字来达到这个目的。当然也还有其它的方式去实现异步,这个后续接触到了再来了解。
\\n关于Dart方法部分的一些基础知识到此就差不多了。
","description":"概述 前面学习完Flutter的如何声明变量的基础知识之后,那么接下来就是该学习如何声明方法了。Function的用法非常灵活和多样,作为初学者的话,先掌握一些基础,以便后续学习Flutter可以更容易上手,等具备一些基础之后再去进阶会容易很多。个人见解,也是我目前的一个学习方式。\\n\\n方法\\n声明一个方法\\n方法如何传参\\n方法如何作为参数\\n词法闭包\\n返回值\\n生成器\\n1. 声明一个方法\\n\\nDart中声明一个方法和Java其实很相似的,但是限制条件却没有Java那么多。如:\\n\\nbool isNode(int atomicNumber) {\\n return _no…","guid":"https://juejin.cn/post/7492797097432842249","author":"RichardLai88","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-14T03:22:36.531Z","media":null,"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"[Flutter学习之Dart基础] - Dart 变量类型及声明","url":"https://juejin.cn/post/7492640608205864970","content":"自上一次使用Flutter和Dart已过去了快3年了。很多基础知识已经忘记了,再加上这3年Dart和Flutter的不断更新迭代,完全跟不上了。先通过这次记笔记的方式来系统的重学一遍Flutter。但在学Flutter之前,先系统的过一遍Dart的语言。毕竟Flutter的是基于Dart去开发,只有对Dart有一定的了解,再学习Flutter的时候,尤其是一些高级用法的的时候才不至于看不懂。
\\nDart的安装是在安装Flutter的时候跟随在Flutter包内的。因此,不需要单独的去安装Dart。\\n在Dart语言中,你能发现Java的影子,也能看到Kotlin和JavaScript的影子。为什么这么说?因为Java中,我们可执行代码的后面都需要”;“符号,在比如class类的声明方式,构造方法等,当然,也不仅仅就这么一点点,还有很多其他类似的地方,慢慢学,慢慢体会。\\n如:
\\nprint(\\"Hello world!\\");\\n
\\nJavaScript的影子如:
\\nimport \'package:http/http.dart\' as http;\\nvar p = 100;\\nvar p = \\"100\\";\\nprint(\\"the value of P:$p\\")\\n
\\n其实对于一个具有开发经验的人来说,尤其是有Java,kotlin或者是JavaScript经验的人来说上手Dart还是非常简单的。
\\n我们学习一门语言的时候,首先要了解的就是这个语言如何声明变量。
\\n其实大多数语言都差不多,基础类型都是:数字,浮点,布尔,字符串。只不过每个语言定义关键字略有小区别,比如大小写,或者有些语言会在数字类型再细分一些数字类型的长短之类的。那么Dart中的基础类型有哪些呢?
\\nint i = 100;\\ndouble d = 100.0;\\nString s = \\"string\\";\\nbool b = true;\\n
\\n整体和其它语言没什么大的区别。唯一需要说明一下的是int类型。它和Java的int类型不一样,Java中针对数字类型的大小有比较明确的区分,如int是32位,long是64位,超出这个范围就会出现溢出。但是Dart中不会,Dart语言中的int是一个通用数字类型,默认位数是和系统保持一致,比如32位系统中默认就是有符号32位,64位系统中默认就是有符号64位。如果操作变量的值比这个64位还要更大,那Dart中也会自动改变为大数变量。所以在Dart中可以大胆的用int去表示任何整数类型的数值,不会存在溢出这个问题。double也是一样的,可以大胆的去声明浮点类型的数值。
\\nDart语言是一个类型安全的语言,也叫强类型语言;但即使这样,Dart中声明变量的时候,依然可以使用隐士声明,因为Dart也具有类型推断的能力。\\n如:
\\nvar n = 100; //这个变量会自动声明称int类型。\\nvar s = \\"100\\"; //这个变量会自动声明称字符串类型。\\n
\\n但Dart语言不是弱类型语言,因此和弱类型语言还有一点区别,一旦变量类型确定了,或者说初始化了,那么该变量的类型就确定了,不可更改了。如果你强行给他赋值一个不一样的类型,编译器就会报错了。\\n如:
\\nvar n = 100; //这个变量会自动声明称int类型。\\nn = \\"100\\"; // 因为前面已经赋值了100,所以确定了n为数字类型,如果你再给他赋值一个string类型的值,那就会出现类型转换的错误:\\n// A value of type \'String\' can\'t be assigned to a variable of type \'int\'. (Documentation) Try changing the // type of the variable, or casting the right-hand type to \'int\'.\\n
\\n除了前面提到这些通过var声明变量的方式之外,还可以通过显示类型的方式声明变量。这点和Java非常相似。\\n如:
\\n int number = 100;\\n String name = \\"Dart\\";\\n bool isDone = true;\\n
\\nDart语言有一个空安全的特性,因此在声明一个变量之后,在它被使用之前必须初始化,除非它是一个可以为空的变量。也就是说如果你像前面这样的方式去声明一个变量的话,这个变量必须设置一个默认值,也就是初始化的值。否则你就得把它声明为一个可为空的变量。\\n如:
\\nint count = 0;\\n
\\n错误用例:
\\nint lineCount; //这样声明则表示count的值不可为空,它不会在没初始化的时候自动设置0这样的默认值。\\n\\nif (weLikeToCount) {\\n lineCount = 0;\\n}\\nprint(lineCount); //这个时候就会报错了,因为wLikeToCount这个值为false的时候,lineCount就不会被赋值了,但是它又是被声明为不可为空的变量,因此这个地方就会编译不通过了。\\n
\\n但间接初始化是允许的:
\\nint lineCount;\\n\\nif (weLikeToCount) {\\n lineCount = countLines();\\n} else {\\n lineCount = 0;\\n}\\n\\nprint(lineCount); //通过前面的逻辑判断可以这个lineCount是肯定能被赋值的。\\n
\\n在我们开发过程中,我们肯定会遇到很多需要声明全局变量,但是又不想声明为可空变量,也不想给特定的默认值,那改如何解决这个问题呢?\\nlate关键字可以帮我们解决这个问题。
\\nlate String description;\\nvoid main() {\\n description = \'Feijoada!\';\\n print(description);\\n}\\n
\\n如果使用了late这个关键字,在使用这个变量的时候,编译是不会去检查和推断你这个变量是否有初始化,一旦运行的时候发现它是未初始化的变量,则会报错。
\\n常量也是我们日常开发中使用非常频繁的一个属性。那么在Dart中常见的两个关键字,一个是final,一个是const。那么他们分别有哪些使用场景和区别?\\nfinal:通过关键字应该大致可以才到,和java其实很类似,就是一个运行时不可变的变量。\\nconst:这个和final不太一样,这个属于编译时常量。\\n它们两者的区别其实和其它语言类似,编译和运行的时候,彼此所在的内存区和声明周期不一样。\\n普通的常见使用如:
\\nfinal name = \'Bob\'; \\nconst nickname = \'Bobby\';\\n
\\n这两个关键字不仅仅可以声明这种基础类型,还可以用来声明数组变量等。\\n如:
\\nfinal bar = [1,2,3,4];\\nconst baz = [1,2,3,4];\\n
\\n这里有个点需要注意一下。那就是通过final声明点不可变变量是指针对变量本身不可被赋值,但是它的属性是可变的。如上声明的bar变量。如:
\\nbar = [2,2,2,2];//这个是会报错的,因为bar是被final修饰过的\\nbar[2] = 4;//这样是允许的,这行代码的意思就是给bar数组中下标为2的属性赋值4。执行完之后bar的值为[1,2,4,4]\\n
\\n而const声明的变量则不允许,const声明的数字是期变量本身以及内部的元素都是不可以变的。如:
\\nbaz = [2,2,2,2]; //这个和final一样,都是不允许的\\nbaz[2] = 4; // 这个和final不一样,final允许,但是const修饰的变量是不被允许的。\\n
\\n所以有一种场景,比如你想给某个主题声明一种色值。如:
\\nconst primaryColors = const [\\n const Color(\'red\', const [255, 0, 0]),\\n const Color(\'green\', const [0, 255, 0]),\\n const Color(\'blue\', const [0, 0, 255]),\\n];\\n
\\n那这种场景const使用是有点冗余的。正确的使用应该是:
\\nconst primaryColors = [ Color(\'red\', [255, 0, 0]),\\n Color(\'green\', [0, 255, 0]),\\n Color(\'blue\', [0, 0, 255]),\\n];\\n
\\nDart定义属性的私有和公有,和Java不太一样。是通过\\"_\\"前缀来区分的,如:
\\nclass Person {\\n String _name = \\"Bob\\"; // 私有变量\\n String name = \\"Tony\\"; // 共有\\n}\\n
\\n但是Dart中的可见性没有Java中的那么严格,虽然在同一个文件中的不同类你把属性设置为私有,但是只要在同一个文件中都是可以访问的。如:
\\nclass Pet {\\n String _name = \\"Kitte\\";\\n}\\n\\nclass Person {\\n void myPet() {\\n Pet pet = Pet();\\n print(pet._name);\\n }\\n}\\n
\\n但是如果在别的文件中去访问Pet的私有属性_name是访问不了的。
","description":"自上一次使用Flutter和Dart已过去了快3年了。很多基础知识已经忘记了,再加上这3年Dart和Flutter的不断更新迭代,完全跟不上了。先通过这次记笔记的方式来系统的重学一遍Flutter。但在学Flutter之前,先系统的过一遍Dart的语言。毕竟Flutter的是基于Dart去开发,只有对Dart有一定的了解,再学习Flutter的时候,尤其是一些高级用法的的时候才不至于看不懂。 初识Dart\\n\\nDart的安装是在安装Flutter的时候跟随在Flutter包内的。因此,不需要单独的去安装Dart。 在Dart语言中,你能发现Java的影子…","guid":"https://juejin.cn/post/7492640608205864970","author":"RichardLai88","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-14T03:02:48.166Z","media":null,"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter多版本管理 — FVM","url":"https://juejin.cn/post/7492402669392068635","content":"FVM(Flutter Version Management),官网介绍为:简单、强大、灵活的工具来理多个Flutter SDK版本。
\\n帮助开发者解决了:
\\n使每个项目工程都可以对应不同版本的Flutter。
\\n使用Homebrew来安装,若未安装,可使用以下命令傻瓜式安装:
\\n// Homebre国内镜像安装,推荐使用阿里镜像源\\n/bin/zsh -c \\"$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)\\"\\n
\\n安装FVM
\\nbrew tap leoafarias/fvm\\nbrew install fvm\\n
\\n卸载FVM
\\nbrew uninstall fvm\\nbrew untap leoafarias/fvm\\n
\\n就OK了。
\\n通过pub来全局安装fvm
\\ndart pub global activate fvm\\n
\\n这点官方给了⚠️:
\\nThis is not recommended if you plan on using FVM to manage your global Flutter install.
\\n— 如果您计划使用FVM来管理您的全局Flutter安装,则不建议这样做。
\\n通过choco来安装,choco是一个包管理工具,类似于Mac的brew。
\\n// 查看是否安装\\nchoco -v\\n
\\n\\n// 更新choco\\nchoco upgrade chocolatey\\n
\\n安装FVM
\\nchoco install fvm\\n
\\n安装过程遇到询问直接输入 Y 即可。
\\n// 查看版本\\nfvm --version\\n\\n// 查看所有远程版本\\nfvm releases\\n\\n// 需要安装的版本\\n// 也可以从官网下载指定版本压缩包,并将其解压到 versions 文件夹中\\nfvm install 3.27.1\\n\\n// 卸载flutter版本\\nfvm remove 3.27.1\\n\\n// 查看本地已装的版本;若在项目目录下执行此命令,可获取当前的使用版本\\nfvm list\\n\\n// 指定工程要使用的版本,在程序根目录或开发工具的终端运行命令\\n// 会生产一个.fvm文件夹在项目中,在git的忽略文件中,不会影响项目的git管理。\\nfvm use 3.7.10\\n\\n// 使用最新的稳定通道版本\\nfvm use stable\\n\\n// 设置全局版本\\nfvm global 3.7.10\\n\\n// 设置fvm配置\\n// 若更改默认的缓存文件位置: fvm config --cache-path <CACHE_PATH> \\nfvm config\\n\\n// 代理dart命令\\nfvm dart\\n\\n// 通过删除FVM目录销毁FVM缓存\\nfvm destroy\\n\\n// 显示有关环境和项目配置的信息\\nfvm doctor\\n\\n// 使用配置的Flutter SDK执行脚本\\nfvm exec\\n\\n// 在不同的项目风格之间切换\\nfvm flavor\\n\\n// 在Flutter版本上生成命令\\nfvm spawn\\n\\n// 使用指定 SDK 版本运行构建\\nfvm spawn 3.13.9 flutter build\\n\\n// 使用不同版本的 SDK 运行测试\\nfvm spawn 2.2.3 flutter test\\n\\n// 将之前安装的flutter加到fvm目录中\\n// version 为版本号\\nfvm import [version]\\n\\n// 代理flutter命令,当前项目运行Flutter相关命令\\nfvm flutter --version\\nfvm flutter doctor\\nfvm flutter clean\\nfvm flutter pub get\\nfvm flutter run \\n...\\n
\\n打开对应的项目工程,进入IDE的设置中,选择Flutter,并选择对应的版本,最后Apply即可,Dart会自动跟随切换对应的版本。
\\n接下来就可以愉快地开发了!!
","description":"简介 FVM(Flutter Version Management),官网介绍为:简单、强大、灵活的工具来理多个Flutter SDK版本。\\n\\n帮助开发者解决了:\\n\\n需要同时使用多个Flutter SDK\\nSDK测试需要频繁切换分支\\n分支切换速度慢,且需要反复重装(典型的使用git来切换)\\n难以管理应用所使用的最新SDK版本\\nFlutter大版本更新需要对整个应用进行迁移\\n团队内部开发环境会出现不一致的情况\\n\\n使每个项目工程都可以对应不同版本的Flutter。\\n\\n安装\\nMacOS\\n\\n使用Homebrew来安装,若未安装,可使用以下命令傻瓜式安装:\\n\\n// Hom…","guid":"https://juejin.cn/post/7492402669392068635","author":"云层之上","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-14T01:17:33.672Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/77680ec7325146a7ae95b6cdee26cdae~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5LqR5bGC5LmL5LiK:q75.awebp?rk3s=f64ab15b&x-expires=1745198253&x-signature=p3RdjPmEm8bmg%2Bs3Rwek%2FbU9MoI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/78f4a8d069e547cd94d1f56bb8b9027d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5LqR5bGC5LmL5LiK:q75.awebp?rk3s=f64ab15b&x-expires=1745198253&x-signature=fpS7Jm66qUEidMCBnWVRGHiuPME%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"平面上的三维空间#04 | 万物之母 - 三角形","url":"https://juejin.cn/post/7492402669391724571","content":"上一篇我们介绍了三维空间到二维平面的 轴测投影 的原理,今天将进一步探索三维空间中的几何图形。 1. 绘制三角形 在第一篇就介绍了绘制空间中 坐标点 的方式, 正所谓: 点动成先,线动成体。有了一点,","description":"上一篇我们介绍了三维空间到二维平面的 轴测投影 的原理,今天将进一步探索三维空间中的几何图形。 1. 绘制三角形 在第一篇就介绍了绘制空间中 坐标点 的方式, 正所谓: 点动成先,线动成体。有了一点,","guid":"https://juejin.cn/post/7492402669391724571","author":"张风捷特烈","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-13T23:35:10.489Z","media":null,"categories":["Android","Flutter","Canvas"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 你的列表根本没有加载到我的心趴上","url":"https://juejin.cn/post/7492395207267631143","content":"关注微信公众号 糖果代码铺
,获取 Flutter
最新动态。
\\n\\n有些感慨,现在群里也会不断有新人,会问一些简单的问题。大家都是从萌新过来的,当然问问题也要注意方式,如何问问题也是一门艺术。然后老油条们也应该更友善一些,多给新人一些建议。有新人肯定是好事,至少
\\nflutter
还没有凉嘛。
最近有同学,在使用 pub-web.flutter-io.cn/packages/lo… 的时候会有一些疑问。
\\n6
年前,从微软的 ISupportIncrementalLoading
中得到灵感,创造了属于 Flutter
平台的加载更多组件 pub-web.flutter-io.cn/packages/lo… 。这个组件一直都是蛮稳定的,所以介绍的文章就比较少了,最近(8个月前)新增了一些功能,随带就一起讲讲吧。
为了照顾一下第一次使用这个组件的同学,我下面还是简单的介绍一下这个组件。
\\nLoadingMoreBase
是组件提供的一个基类,用来给用户加载实际数据使用的。
实际开发中,你只需要继承它,并且实现 loadData
方法即可。当然你也需要关注 hasMore
这个属性,通过它来告诉组件是否还有更多的数据。
class TuChongRepository extends LoadingMoreBase<TuChongItem> {\\n TuChongRepository({this.maxLength = 300});\\n int _pageIndex = 1;\\n bool _hasMore = true;\\n @override\\n bool get hasMore => _hasMore && length < maxLength;\\n final int maxLength;\\n\\n @override\\n Future<bool> refresh([bool notifyStateChanged = false]) async {\\n _hasMore = true;\\n _pageIndex = 1;\\n final bool result = await super.refresh(true);\\n return result;\\n }\\n\\n @override\\n Future<bool> loadData([bool isLoadMoreAction = false]) async {\\n \\n bool isSuccess = false;\\n try {\\n // 从服务端加载数据 feedList\\n for (final TuChongItem item in feedList!) {\\n if (item.hasImage && !contains(item) && hasMore) {\\n add(item);\\n }\\n }\\n\\n _hasMore = feedList.isNotEmpty;\\n _pageIndex++;\\n isSuccess = true;\\n } catch (exception, stack) {\\n isSuccess = false;\\n print(exception);\\n print(stack);\\n }\\n return isSuccess;\\n }\\n}\\n
\\n最简单一个加载更多 UI
代码如下图。
LoadingMoreList(\\n ListConfig<TuChongItem>(\\n itemBuilder: ItemBuilder.itemBuilder,\\n sourceList: listSourceRepository,\\n ),\\n ),\\n
\\n当然我们也支持其他列表:
\\nGridView
LoadingMoreList(\\n ListConfig<TuChongItem>(\\n itemBuilder: ItemBuilder.itemBuilder,\\n sourceList: listSourceRepository,\\n gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(\\n crossAxisCount: 2,\\n crossAxisSpacing: 3.0,\\n mainAxisSpacing: 3.0,\\n ),\\n ),\\n ),\\n
\\nWaterfallFlow (瀑布流)
LoadingMoreList(\\n ListConfig<TuChongItem>(\\n extendedListDelegate: SliverWaterfallFlowDelegateWithFixedCrossAxisCount(\\n crossAxisCount: 2,\\n crossAxisSpacing: 5,\\n mainAxisSpacing: 5,\\n ),\\n itemBuilder: _buildItem,\\n sourceList: listSourceRepository,\\n ),\\n ),\\n
\\nSliver
系列通过使用 LoadingMoreCustomScrollView
,你可以加载任意的 Sliver
列表。
LoadingMoreCustomScrollView(\\n slivers: <Widget>[\\n SliverAppBar(\\n pinned: true,\\n title: Text(\\"MultipleSliverDemo\\"),\\n ),\\n // SliverList\\n LoadingMoreSliverList(SliverListConfig<TuChongItem>(\\n itemBuilder: ItemBuilder.itemBuilder,\\n sourceList: listSourceRepository,\\n )),\\n // SliverGrid\\n LoadingMoreSliverList(\\n SliverListConfig<TuChongItem>(\\n itemBuilder: ItemBuilder.itemBuilder,\\n sourceList: listSourceRepository1,\\n gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(\\n crossAxisCount: 2,\\n crossAxisSpacing: 3.0,\\n mainAxisSpacing: 3.0,\\n ),\\n ),\\n ),\\n // SliverWaterfallFlow\\n LoadingMoreSliverList(\\n SliverListConfig<TuChongItem>(\\n itemBuilder: buildWaterfallFlowItem,\\n sourceList: listSourceRepository2,\\n extendedListDelegate: SliverWaterfallFlowDelegateWithFixedCrossAxisCount(\\n crossAxisCount: 2,\\n crossAxisSpacing: 5,\\n mainAxisSpacing: 5,\\n ),\\n ),\\n ),\\n ],\\n ),\\n
\\n加载更多有多种状态,你可以通过自定义 indicatorBuilder
来自定义自己的状态效果。
enum IndicatorStatus {\\n none,\\n // 加载更多\\n loadingMoreBusying,\\n // 列表为空时候的第一次全屏加载\\n fullScreenBusying,\\n // 加载更多报错\\n error,\\n // 列表为空时候的第一次全屏加载报错\\n fullScreenError,\\n // 没有更多数据加载\\n noMoreLoad,\\n // 空列表\\n empty\\n}\\n
\\n LoadingMoreList(\\n ListConfig<TuChongItem>(\\n itemBuilder: ItemBuilder.itemBuilder,\\n sourceList: listSourceRepository,\\n indicatorBuilder: _buildIndicator,\\n padding: EdgeInsets.all(0.0),\\n ),\\n ),\\n\\n \\n Widget _buildIndicator(BuildContext context, IndicatorStatus status) {\\n //if your list is sliver list ,you should build sliver indicator for it\\n //isSliver=true, when use it in sliver list\\n bool isSliver = false;\\n\\n Widget widget;\\n switch (status) {\\n case IndicatorStatus.None:\\n widget = Container(height: 0.0);\\n break;\\n case IndicatorStatus.LoadingMoreBusying:\\n widget = Row(\\n mainAxisAlignment: MainAxisAlignment.center,\\n crossAxisAlignment: CrossAxisAlignment.center,\\n children: <Widget>[\\n Container(\\n margin: EdgeInsets.only(right: 5.0),\\n height: 15.0,\\n width: 15.0,\\n child: getIndicator(context),\\n ),\\n Text(\\"正在加载...不要着急\\")\\n ],\\n );\\n widget = _setbackground(false, widget, 35.0);\\n break;\\n case IndicatorStatus.FullScreenBusying:\\n widget = Row(\\n mainAxisAlignment: MainAxisAlignment.center,\\n crossAxisAlignment: CrossAxisAlignment.center,\\n children: <Widget>[\\n Container(\\n margin: EdgeInsets.only(right: 0.0),\\n height: 30.0,\\n width: 30.0,\\n child: getIndicator(context),\\n ),\\n Text(\\"正在加载...不要着急\\")\\n ],\\n );\\n widget = _setbackground(true, widget, double.infinity);\\n if (isSliver) {\\n widget = SliverFillRemaining(\\n child: widget,\\n );\\n } else {\\n widget = CustomScrollView(\\n slivers: <Widget>[\\n SliverFillRemaining(\\n child: widget,\\n )\\n ],\\n );\\n }\\n break;\\n case IndicatorStatus.Error:\\n widget = Text(\\n \\"好像出现了问题呢?\\",\\n );\\n widget = _setbackground(false, widget, 35.0);\\n\\n widget = GestureDetector(\\n onTap: () {\\n listSourceRepository.errorRefresh();\\n },\\n child: widget,\\n );\\n\\n break;\\n case IndicatorStatus.FullScreenError:\\n widget = Text(\\n \\"好像出现了问题呢?\\",\\n );\\n widget = _setbackground(true, widget, double.infinity);\\n widget = GestureDetector(\\n onTap: () {\\n listSourceRepository.errorRefresh();\\n },\\n child: widget,\\n );\\n if (isSliver) {\\n widget = SliverFillRemaining(\\n child: widget,\\n );\\n } else {\\n widget = CustomScrollView(\\n slivers: <Widget>[\\n SliverFillRemaining(\\n child: widget,\\n )\\n ],\\n );\\n }\\n break;\\n case IndicatorStatus.NoMoreLoad:\\n widget = Text(\\"没有更多的了。。不要拖了\\");\\n widget = _setbackground(false, widget, 35.0);\\n break;\\n case IndicatorStatus.Empty:\\n widget = EmptyWidget(\\n \\"这里是空气!\\",\\n );\\n widget = _setbackground(true, widget, double.infinity);\\n if (isSliver) {\\n widget = SliverToBoxAdapter(\\n child: widget,\\n );\\n } else {\\n widget = CustomScrollView(\\n slivers: <Widget>[\\n SliverFillRemaining(\\n child: widget,\\n )\\n ],\\n );\\n }\\n break;\\n }\\n return widget;\\n }\\n\\n
\\ncenter
其实蛮有用处的,具体的原理可以查看:
有的同学会问,为什么要支持这个,做什么呢,很简单,可以做个聊天列表,向上翻的时候就是加载更多的历史数据。
\\n return LoadingMoreCustomScrollView(\\n showGlowLeading: false,\\n center: _centerKey,\\n slivers: <Widget>[\\n // 历史数据\\n LoadingMoreSliverList<TuChongItem>(\\n SliverListConfig<TuChongItem>(\\n itemBuilder: itemBuilder,\\n sourceList: listSourceRepository1,\\n ),\\n ),\\n // 当前数据\\n LoadingMoreSliverList<TuChongItem>(\\n SliverListConfig<TuChongItem>(\\n itemBuilder: itemBuilder,\\n sourceList: listSourceRepository2,\\n ),\\n key: _centerKey,\\n ),\\n ],\\n );\\n
\\n\\n\\n妈妈再也不会担心我不会写聊天列表了!
\\n
完整例子: github.com/fluttercand…
\\n当然,也提供另外一种加载历史数据的方式,使用的是下拉刷新组件。这样你可以模拟出下拉回弹等动画。
\\n PullToRefreshNotification(\\n onRefresh: onRefresh,\\n maxDragOffset: 48,\\n armedDragUpCancel: false,\\n child: CustomScrollView(\\n /// in case list is not full screen and remove ios Bouncing\\n physics: const AlwaysScrollableClampingScrollPhysics(),\\n controller: _scrollController,\\n center: _centerKey,\\n slivers: <Widget>[\\n // 加载历史数据的效果\\n PullToRefreshContainer(\\n (PullToRefreshScrollNotificationInfo? info) {\\n final double offset = info?.dragOffset ?? 0.0;\\n //loading history data\\n return SliverToBoxAdapter(\\n child: Container(\\n height: offset,\\n alignment: Alignment.center,\\n child: const CupertinoActivityIndicator(color: Colors.blue),\\n ),\\n );\\n },\\n ),\\n ExtendedSliverList(\\n delegate: SliverChildBuilderDelegate(\\n (BuildContext context, int index) {\\n final ChatItem item = newChats[index];\\n return buildItem(item);\\n },\\n childCount: newChats.length,\\n ),\\n extendedListDelegate: const ExtendedListDelegate(),\\n ),\\n ExtendedSliverList(\\n key: _centerKey,\\n delegate: SliverChildBuilderDelegate(\\n (BuildContext context, int index) {\\n final ChatItem item = chats[index];\\n return buildItem(item);\\n },\\n childCount: chats.length,\\n ),\\n extendedListDelegate: const ExtendedListDelegate(),\\n ),\\n ],\\n ),\\n );\\n
\\n完整例子: github.com/fluttercand…
\\nLoadingMoreSliverList
封装处理在实际使用中,经常有小伙伴会给 LoadingMoreSliverList
进行封装,比如
class MyLoadingMoreSliverList1 extends StatelessWidget {\\n const MyLoadingMoreSliverList1({\\n Key? key,\\n required this.listSourceRepository,\\n }) : super(key: key);\\n\\n final TuChongRepository listSourceRepository;\\n @override\\n Widget build(BuildContext context) {\\n return SliverPadding(\\n padding: const EdgeInsets.all(50),\\n sliver: LoadingMoreSliverList<TuChongItem>(\\n SliverListConfig<TuChongItem>(\\n itemBuilder: itemBuilder,\\n sourceList: listSourceRepository,\\n gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(\\n crossAxisCount: 2,\\n crossAxisSpacing: 3.0,\\n mainAxisSpacing: 3.0,\\n ),\\n ),\\n ),\\n );\\n }\\n}\\n
\\n由于之前列表的 config
是通过 LoadingMoreCustomScrollView
的 slivers
判断是否是 LoadingMoreSliverList
获取的。封装就会导致组件没法获取对应的配置。
现在你只需要将 LoadingMoreCustomScrollView.getConfigFromSliverContext
设置成 true
即可。
return LoadingMoreCustomScrollView(\\n // support LoadingMoreCustomScrollView.slivers are not a direct LoadingMoreSliverList\\n getConfigFromSliverContext: true,\\n slivers: <Widget>[\\n MyLoadingMoreSliverList1(\\n listSourceRepository: listSourceRepository1,\\n )\\n ],\\n );\\n
\\n\\n\\n如果
\\nLoadingMoreCustomScrollView
里面有Sliver
只需要加载一次数据,我应该怎么写呢?
我想这应该也是实际开发会遇到的问题吧?新版本提供了 SliverLoadingData
和 SliveLoadingConfig
和 LoadingMoreLoadingSliver
来加载一次性数据,当然也支持定义加载动画以及失败效果,你也可以增加错误重试点击效果。
数据
部分\\nclass MySliverLoadingData extends SliverLoadingData<int> {\\n @override\\n Future<int?> onLoadData() async {\\n await Future<void>.delayed(const Duration(seconds: 5));\\n // retrun null means error\\n return 123456;\\n }\\n}\\n\\nlate MySliverLoadingData loadingData= MySliverLoadingData();\\n
\\nUI
部分 return LoadingMoreCustomScrollView(\\n slivers: <Widget>[\\n LoadingMoreLoadingSliver<int>(\\n SliveLoadingConfig<int>(\\n builder: (BuildContext context, int? data) {\\n return SliverToBoxAdapter(\\n child: Container(\\n alignment: Alignment.center,\\n child: Text(\'Loading Data$data\'),\\n color: Colors.blue,\\n height: 50.0,\\n ),\\n );\\n },\\n loadingData: loadingData,\\n ),\\n ),\\n ],\\n );\\n
\\npub-web.flutter-io.cn/packages/lo… 结合 pub-web.flutter-io.cn/packages/pu… 你可以做出任何效果的下拉刷新+列表加载更多的效果。
\\n组件没有花时间去做一些非常炫酷效果,是因为当每个人拿到三方组件的时候,都应该对其做一定的封装,来满足各自公司的设计效果。不是不想做,而是希望组件尽量简单,可扩展性高。
\\n\\n\\n法同学: 以前在
\\ngithub
上开源代码,用户跟我说加个功能吧,我都会说好好好,但是时间一久也没想起来做。其实这样挺不好的。\\n现在用户跟我说加个功能吧,除非用户的建议真的很好到我想马上加这个功能的程度,否则我就会在issue
直接说as design
,抱歉我不想加,然后直接关闭了。作为一个有讨好倾向的人,这是我锻炼真诚和勇气的方式。
\\n\\n最后想说,任何东西的设计都没法完全满足全部人的需求。对的,你的列表根本没有加载到我的心趴上。
\\n
爱 Flutter
,爱糖果
,欢迎加入Flutter Candies,一起生产可爱的Flutter小糖果QQ群:181398081
最最后放上 Flutter Candies 全家桶,真香。
\\n在今年的 Google Cloud Next 大会上,Google 发布了全新的 Firebase Studio ,而 Firebase Studio 提供了基于云的开发环境,而这个开发环境融合了 AI 和原先的 Project IDX ,内置了 60 多个预构建的模板和工作环境,这里面就包括 Android Studio Cloud :
\\n起初我以为就是一个 Web IDE ,但是在体验之后,我只想说,太香了~ 通过 Android Studio Cloud ,我们可以快速在浏览器得到一个可开发 Android 的环境,无需多余配置,在 Firebase Studio 启动 Android Studio Cloud 后,一两分钟你就可以通过在线 Android Studio 进行工作:
\\n这对于需要临时处理问题而工作电脑又不在身边的场景非常实用,只需要一个浏览器,通过 Android Studio Cloud ,然后使用 Clone Repository 直接创建项目,就可以快速得到一个临时的工作环境:
\\n最主要是,这个环境它不是一个简单 Web IDE,而是一个完整的远程虚拟机系统,而且已经包含了你开发所需的必备条件,所以它不只是一个 Android 开发环境,在使用上它有完整的 Android Studio 功能,并且还可以提供其他工作环境所需的支持:
\\n通过以下两个简单的操作,我们就可以看到系统的工作环境,比如「终端」和「浏览器」在系统内就直接可用,这就有更大的想象空间了:
\\n比如终端环境就内置有相应的 node 版本,并且作为完整的 linux 终端,你可以在上面安装运行一切你需要的其他支持:
\\n而通过「关于」可以看到,这其实一台 Ubuntu 虚拟机环境,规格是:
\\n看到没有,虽然你只是开启了 Android Studio Cloud ,但是其实你是得到了一个 16 核 CPU + 60G内存 + 250G 磁盘的工作环境,并且这个环境是通过 noVNC 就可以连接控制:
\\n\\n\\n当然,它属于虚拟化的,只是 Cloud 将 VM 标记为 60 G,但它会根据使用情况进行扩展,虚拟化容器会根据使用情况将其分配到实际硬件上
\\n
而 noVNC 是一个 JavaScript VNC 客户端,用户可以通过浏览器访问和控制远程设备,并且无需安装专用 VNC 客户端软件,同时 noVNC 支持包括 iOS 和 Android 在内的移动端浏览器。
\\n\\n\\nnoVNC 的核心是通过浏览器和 WebSocket 协议实现与 VNC 服务器的通信,基于标准的 RFB(Remote Framebuffer)协议。
\\n
另外,在目前 Android Studio Cloud 体验上,不管是设备性能和网速都挺不错,特别是在项目 clone 和依赖同步下,也许是因为 Cloud 所在的网络环境优势,「从零」开始同步和下载速度比真实设备环境快太多了,目前体验下来,基本主要瓶颈还是在 CPU 上:
\\n当然,既然是一个完整的 Ubuntu 环境,你只需要再简单配置下,一个可运行的 Flutter 环境也就出现了,其他的比如 RN、Weex 也不在话下,甚至你想折腾出来个 uniapp 也不是不可以:
\\n我是在 macOS 上跑的 Cloud ,而 Cloud 是 Ubuntu ,所以一些快捷键差异还是有的,比如 cv 的时候就有点尴尬,另外,在特殊情况下,你甚至可以通过一个 Android 平板的浏览器来完成工作:
\\n不过,既然是依托 Firebase Studio ,其实也可以直接从 Flutter/RN 模块去创建一个新的 workspace 环境,另外 Web、Backend 甚至 Databases 和 AI 都有支持模版:
\\n而最最最重要的是,它不是一次性场景,在退出之后,完全可以在需要时通过已有工程再次进入,也就是当你需要应急处理时,可以快速得到一个环境完整和代码齐全的 workspace,只前提条件仅仅是一个有网络的浏览器:
\\n不过你要说 Android Studio Cloud 有什么致命缺陷,那就是无法直接通过 usb 连接真机调试,如果想调试真机,只能通过 Firebase 的 Android Device Streaming 才能支持运行到远程真机。
\\n当然,如果回归到 Firebase Studio 上,Android Studio Cloud 只能说是它的一小部份,它的更多支持在于 AI ,可以说,Firebase Studio 是将之前的 Project IDX、Genki 和 Gemini 等产品融合成统一的场景,目的是构建一个端到端平台,通过内置「原型设计」、「代码 workspaces」 和「灵活部署」等功能,从而实现一个 AI 场景下的完整产品需求:
\\n比如 Firebase Studio 提供了 Gemini Code Assist 支持 ,可以做到:
\\n另外还有 Firebase App Distribution 移动 App 测试服务,可以用于运行手动和自动测试,其中 Firebase App Distribution 中的 App Testing agent 就可以模拟用户在 App 的真实交互,比如开发者可以写一个测试用例,将目标设置为 “Find a trip to Greece”,然后 App Testing Agent 就会使用 Gemini 制定实现该目标的计划,并在虚拟/物理设备上运行计划,并生成详细的通过/失败结果:
\\n而最后就是大家关心的是否可以白嫖,目前默认只有三个免费的 workspace,而进入开发者计划(免费)之后,可以免费白嫖 10 个 workspace:
\\n至于 30 个 workspace 就需要「付费」了,当然你也可以和我一样,加入 Google Developer Experts 计划,这样就可以免费白嫖 Premium benefits 了,甚至 JetBrains Tools 全家桶也可以持续免费使用:
\\n目前看来,Google 在 AI 领域真的是大投入,而不管是前段时间的「Android 转内部开发」,还是这几天的「Android 和 Pixel 等部门裁减数百人」消息,都属于战略发展的“资源优化”。
\\nGoogle 自 2023 年就开始执行所谓的 “效率提升计划”,目的是希望通过资源优化,将资源集中于AI、云计算等战略领域,就连 Android 这样的核心项目,也不得不作出相应让步,而目前 Firebase Studio 作为 「云+AI」 的产物,整体体验下来还挺不错,,主要白嫖的虚拟环境配置还是相当不错。
\\n那么,你觉得 Android Studio Cloud 和 Firebase Studio 对你来说有用吗?
","description":"在今年的 Google Cloud Next 大会上,Google 发布了全新的 Firebase Studio ,而 Firebase Studio 提供了基于云的开发环境,而这个开发环境融合了 AI 和原先的 Project IDX ,内置了 60 多个预构建的模板和工作环境,这里面就包括 Android Studio Cloud : 起初我以为就是一个 Web IDE ,但是在体验之后,我只想说,太香了~ 通过 Android Studio Cloud ,我们可以快速在浏览器得到一个可开发 Android 的环境,无需多余配置,在…","guid":"https://juejin.cn/post/7492373338410074162","author":"恋猫de小郭","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-13T22:16:26.325Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9c5053d64cb447e6b555bc098cd90625~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1745187386&x-signature=d6jFiTpdXw06Yqw70XqjD8Xo0G8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e8c277fc4a684f4f96baa830b8ee9b21~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1745187386&x-signature=9nURkIpxvckrE6q2s6v5B1swYMg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/10d271790d7a480797f284f4edc82373~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1745187386&x-signature=jDQ0d7QM1PHK79UyzCED1kCHdLY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c42a72f8ead541208c79ffcf2938ac02~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1745187386&x-signature=WX0bMNJBOoS2%2B4IV08dsKp15q8s%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9c02701c59354729bf35c2bd52845d69~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1745187386&x-signature=AauzlAOA1cYZ02maB5%2B8VUA8qe0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/37d9a756c39743b1bc7eadc0d7b75e4d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1745187386&x-signature=XJH0OLRDNUbrEIuxE5psyc1JafU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6e4adfd035824f69a14b7a964864e802~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1745187386&x-signature=J2BYkQPG9ctbr%2Faq90JLUdu0UKw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/12a0026a48b942438db3d80487787aed~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1745187386&x-signature=sbMLuD8uzYZcPKliePbq8kRW4tk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c5d2c405a725454f81599fad1a953d0c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1745187386&x-signature=cIVXY%2Bv9v1f2pGALkml%2BhJ%2B%2B2pg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/49182b40cc044ff3baf2139a5b7dd3ec~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1745187386&x-signature=9gIU2HlCNGRlFtYU8B4oZPLLgyY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a47a8d58cedd4eadb5d81bf10a4c1d19~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1745187386&x-signature=qAckOvazdsCjxfwcqpBLS6rpIyw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1a8eecd0ce0f4ce38dced7b69f85c747~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1745187386&x-signature=zDsg09HSSbKLtLixTlX5DQ%2B9erY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f92f79756971436aa1148907b3670f23~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1745187386&x-signature=kKpdoLTyJs08uauo89G62cQstlI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3ca4f823c79e4eafb3352a58cacbd087~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1745187386&x-signature=GelYfpRpM0JN4JJA2rJoUQXex0Q%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3f2c72b1f1e9456dbb60da64344b4701~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1745187386&x-signature=zmwladmj4nk4pgA0Elcb6qjORBg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c1bc0fa4fbef47fcae62cd0406a2539a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1745187386&x-signature=3j6GNt%2FEuGFnCtOBgrIlD%2FbOzIg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/919ad37853594f48b6d902381077f2b3~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1745187386&x-signature=wPPoCo9RPJz0aMQA355cqqJFsYA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5186d57661eb4aaa9e509db557eda4a1~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1745187386&x-signature=FvHH5WlzBkpyBOfsRB0DdA62inM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4387d7f92d26471c8c5f8e699d0f372b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1745187386&x-signature=BwgsVT5iHjKnCPMlabti4zZOYQE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/71a765c2e2e94e84be461aad3d280d7c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1745187386&x-signature=Na1pigtb0ehhs4K0haPOE50ycxk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9ebb62716e514441ab99fbf96c449077~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1745187386&x-signature=d3DNN3Dkyu0bghAKzl0aSheSrac%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fe41069fca5c4c1197d41c474ad7ffe6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1745187386&x-signature=P8VzXiuJjY92mwCKH77QyI8F7Ls%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/948b78316daf4ca590dfdf2f2f712cae~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1745187386&x-signature=sBOf51xjVW2GF4HdSIMNgt5BmeE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/88dbfecc37074190a06ac876f8b2a9a5~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1745187386&x-signature=YuqFH9CJ2cy9GS0OmoWCHhFz6jw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/18fdbfba55ab462bb380d60501bb57fb~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1745187386&x-signature=J7nf1GtMGnPZA%2Bs%2FRGPnuK9bWJo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c0fc6d5a181845e8a22ccb163ba4e187~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1745187386&x-signature=5RZ%2BZpPXEgKqM4%2BsmUVeMpGKkiU%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter开箱即用一站式解决方案-企业级日志篇","url":"https://juejin.cn/post/7492398520583192585","content":"本库为Flutter应用开发提供一站式解决方案,包含:
\\nThemeExtension
全局配置颜色/圆角/间距等样式在 pubspec.yaml
中添加依赖:
dependencies:\\n flutter_chen_common: 最新版本\\n
\\ngraph TD\\n A[主线程] --\x3e|异步传递| B[日志隔离线程]\\n B --\x3e C[内存缓冲队列]\\n C --\x3e D[批量文件写入]\\n
\\n技术实现:采用Dart Isolate独立线程处理日志,通过环形缓冲区实现高效内存管理
\\n性能指标:
\\n对比测试:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n日志量 | 传统方案耗时 | 本方案耗时 |
---|---|---|
1万条 | 1.2秒 | 0.07秒 |
10万条 | 12.3秒 | 0.8秒 |
// 日志初始化 \\nawait Log.init( \\n const LogConfig( \\n retentionDays: 3, \\n enableFileLog: true, \\n logLevel: LogLevel.all, \\n recordLevel: LogLevel.info, \\n output: [CustomSentryOutput()], \\n ), \\n);\\n \\n// 统一调用示例\\nLog.d(\\"debug message\\");\\nLog.i(\\"info message\\");\\nLog.w(\\"warning message\\");\\nLog.e(\\"error message\\");\\nLog.console(\\"console message 可完整打印不被截断并且无前缀\\");\\nfinal Directory dir = await Log.getLogDir(); // 获取日志文件目录\\n\\nclass LogConfig {\\n final int retentionDays; // 日志保留天数\\n final bool enableFileLog; // 是否启用日志写入\\n final LogLevel logLevel; // 日志过滤级别,低于该日志级别不打印\\n final LogLevel recordLevel; // 日志记录级别(Network日志级别分别是Info、Error),低于该日志级别不写入日志文件\\n final List<LogOutput>? output; // 可自定义扩展LogOutput,如Sentry上报、日志上传服务器、加密脱敏输出等(类似dio拦截器)\\n\\n const LogConfig({\\n this.retentionDays = 3,\\n this.enableFileLog = true,\\n this.logLevel = LogLevel.all,\\n this.recordLevel = LogLevel.info,\\n this.output,\\n });\\n}\\n
\\n层级 | 技术实现 | 性能指标 |
---|---|---|
内存缓冲层 | 环形缓冲区设计 | 百万级日志/秒 |
隔离处理层 | Dart Isolate | 零主线程阻塞 |
持久化层 | 按日期分文件存储 | 自动滚动归档 |
传输层 | 加密压缩传输 | TLS1.3+ AES256 |
sequenceDiagram\\n participant App\\n participant MainIsolate\\n participant LoggerIsolate\\n participant FileSystem\\n \\n App->>MainIsolate: 记录日志\\n MainIsolate->>LoggerIsolate: 异步传递日志\\n LoggerIsolate->>FileSystem: 批量写入文件\\n FileSystem-->>LoggerIsolate: 写入确认\\n LoggerIsolate-->>App: 完成回调\\n
\\nenum LogLevel {\\n all, // 开发环境\\n debug, // 调试信息\\n info, // 运行状态\\n warning, // 预期异常\\n error, // 系统错误\\n fatal, // 致命错误\\n off // 生产环境\\n}\\n
\\nclass SentryOutput extends LogOutput {\\n @override\\n void output(OutputEvent event) {\\n if (event.level.value >= LogLevel.error.value) {\\n Sentry.captureException(\\n event.error,\\n stackTrace: event.stackTrace,\\n tags: {\'log_level\': event.level.name},\\n );\\n }\\n }\\n}\\n\\n// 配置使用\\nLog.init(LogConfig(\\n output: [SentryOutput()]\\n));\\n
\\n优势维度 | 传统方案痛点 | 本方案亮点 | 效益提升 |
---|---|---|---|
性能 | 主线程卡顿 | 多线程零阻塞 | 吞吐量提升 15倍 |
安全 | 敏感信息泄露风险 | 军用级加密体系 | 安全审计通过率 100% |
可观测性 | 日志分散难查询 | 统一监控平台 | 故障定位时间缩短 80% |
扩展性 | 功能固化难定制 | 插件式架构 | 二次开发成本降低 70% |
合规性 | 难以满足金融监管要求 | 完整审计追踪 | 合规认证周期缩短 50% |
使用 async 创建 Future
\\nFuture<String> fetchData() async {\\n var response = await http.get(Uri.parse(\'https://api.example.com/data\'));\\n print(response.body);\\n return \\"Data fetched successfully!\\";\\n}\\n
\\n通过 then 来使用 Future
\\nfetchData()\\n .then((value) => print(\\"Result: $value\\")) //成功回调\\n .catchError((error) => print(\\"Error: $error\\")); //失败错误处理\\n .whenComplete(() => print(\\"Task done\\")); //完成回调,不管成功或失败都会执行\\n
\\n通过 async 和 await 来使用 Future
\\nvoid gainData() async {\\n try {\\n String data = await fetchData();\\n print(data);\\n } catch (e) {\\n print(\\"Error: $e\\");\\n }\\n}\\n
\\n使用 StreamController 创建 Stream
\\n//StreamController 用于管理流的创建、发送数据和关闭\\nStreamController<String> streamController = StreamController<String>();\\n//\\nstreamController.sink.add(\\"Data 1\\");\\nstreamController.sink.add(\\"Data 2\\");\\n
\\n使用 async* 创建 Stream
\\nStream<String> fetchDataStream() async* {\\n for (int i = 0; i < 3; i++) {\\n await Future.delayed(const Duration(seconds: 1)); //模拟异步耗时\\n yield \\"Data $i\\"; //生成发送数据\\n }\\n}\\n
\\n通过 listen 监听 Stream 的事件
\\nstreamController.stream.listen((value) {\\n print(\\"Value: $value\\");\\n });\\n//\\nstreamController.stream.listen(\\n (data) => print(\\"Received: $data\\"), //数据回调\\n onError: (error) => print(\\"Error: $error\\"), //错误回调(可选)\\n onDone: () => print(\\"Stream completed\\"), //完成回调(可选)\\n);\\n
\\n//不再需要发送数据后要手动关闭\\nstreamController.close();\\n
\\n//使用 Stream.fromFuture 将单个 Future 转为流(发送一次数据后完成)\\nFuture<String> futureData = fetchData();\\nStream<String> streamData = Stream.fromFuture(futureData);\\n//转换多个 Future\\nStream<String> streamData2 = Stream.fromFutures([futureData,futureData2]);\\n
","description":"Flutter Future 和 Stream 的区别 Future 一次性异步操作\\n用于表示一个尚未完成的异步操作的结果,承诺最终会返回一个值(或错误),适用于如网络请求、数据库查询、文件读取和延迟任务等\\nFuture 无需手动关闭\\nFuture 的所有 API 的返回值仍然是一个 Future 对象,所以可以很方便的进行链式调用\\n\\n使用 async 创建 Future\\n\\nFuture本库为Flutter应用开发提供一站式解决方案,包含:
\\nThemeExtension
全局配置颜色/圆角/间距等样式在 pubspec.yaml
中添加依赖:
/// 1.8.0版本已移除图片选择裁剪上传oss一站式解决方案\\ndependencies:\\n flutter_chen_common: 最新版本\\n
\\n// 网络模块初始化 \\n// HttpConfig,内置日志打印、网络重试拦截器、token无感刷新以及相关操作\\nHttpClient.init(\\n config: HttpConfig(\\n baseUrl: \'https://api.example.com\',\\n connectTimeout: const Duration(seconds: 30),\\n receiveTimeout: const Duration(seconds: 30),\\n sendTimeout: const Duration(seconds: 30),\\n commonHeaders: {\\"platform\\": Platform.isIOS ? \'ios\' : \'android\'},\\n interceptors: [CustomInterceptor()]\\n enableLog: true,\\n enableToken: true,\\n maxRetries: 3,\\n getToken: () => \\"token\\",\\n onRefreshToken: () async {\\n return \\"new_token\\";\\n },\\n onRefreshTokenFailed: () async {\\n Log.d(\\"重新登录\\");\\n },\\n ),\\n);\\n\\n// 网络请求使用\\nHttpClient.instance.request(\\n \\"/xxxx\\",\\n method: HttpType.post.name,\\n fromJson: (json) => User.fromJson(json),\\n showLoading: true, // 自动显示全局Loading\\n)\\n\\n// 网络请求方法参数\\nFuture request<T>( \\n String path, { \\n String? baseUrl, // 不为空情况下用局部baseUrl,为空情况下用全局baseUrl\\n String? method, \\n Options? options, \\n dynamic data, \\n T Function(dynamic json)? fromJson, \\n bool showLoading = false, \\n CancelToken? cancelToken, \\n void Function(int, int)? onSendProgress, \\n void Function(int, int)? onReceiveProgress, \\n})\\n\\n// 网络请求方法枚举\\nenum HttpMethod { get, post, put, delete, patch }\\n
\\n拦截器 | 功能描述 | 技术亮点 |
---|---|---|
Token管理 | 身份认证全流程处理 | 无感刷新+请求队列管理 |
结构化日志 | 全链路请求监控 | 完整上下文信息+性能分析 |
智能重试 | 网络异常自动重试 | 指数退避策略+条件过滤 |
sequenceDiagram\\n participant App\\n participant Interceptor\\n participant AuthServer\\n participant BusinessServer\\n \\n App->>BusinessServer: 请求用户数据\\n BusinessServer-->>Interceptor: 返回401\\n Interceptor->>AuthServer: 发起Token刷新\\n AuthServer-->>Interceptor: 返回新Token\\n Interceptor->>BusinessServer: 携带新Token重试\\n BusinessServer-->>App: 返回用户数据\\n
\\nHttpConfig(\\n interceptors: [\\n CustomInterceptor1(),\\n CustomInterceptor2(),\\n ],\\n)\\n
\\n// 打印样式如下(日志打印完全不会被截断,json格式化方便复制查看数据,在开启日志拦截以及记录日志时会将日志写入文件\\n┌─────────────────────────────────────────────────────────────────────────────\\n│ ✅ [HTTP] 2025-04-05 23:30:29 Request sent [Duration] 88ms\\n│ Request: 200 GET http://www.weather.com.cn/data/sk/101010100.html?xxxx=xxxx\\n│ Headers: {\\"token\\":\\"xxxxx\\",\\"content-type\\":\\"application/json\\"}\\n│ Query: {\\"xxxx\\":\\"xxxx\\"}\\n│ Response: {\\"weatherinfo\\":{\\"city\\":\\"北京\\",\\"cityid\\":\\"101010100\\",\\"WD\\":\\"东南风\\"}}\\n└──────────────────────────────────────────────────────────────────────────────\\n\\n
\\n日志系统几大优势:
\\n什么是Future呢?我看到的第一反应想到的就是翻译——未来,但未来啥呢,没有一个准确的答案。后来通过了解明白Future它表示一个可能还未完成的异步操作的结果,我联系了一下,这不是和未来一个意思吗?我是这样理解的,Dart单线程实现异步操作是通过事件循环机制将异步I/O操作委托给操作系统底层完成,这意味着当前并不知道这个异步操作的结果,需要等待操作系统通知,那这不就是未来的结果吗。是不是感觉很神奇?那下面我们就一起去看看Dart中关于Future的细节吧。
\\nFuture表示一个可能还未完成的异步操作的结果。是不是感觉一脸懵?这可能是我们还没有真正理解异步操作。下面我们以一个生活中的例子来快速回顾一下异步操作。
\\n例子:异步操作就像我们订外卖时骑手送餐这个过程。我们(主线程)不用因为订了外卖(耗时的操作如网络请求)就一直站在门口等待着送达,而是在等待过程中去做其他的事(执行下一个操作),当外卖送达后骑手会通知我们取结果(Future)。
\\nFuture的创建就是通过编写代码实现异步操作的过程。通过六大工厂构造函数或异步函数async创建,下面我们逐步介绍。
\\nFuture(FutureOr<T> computation())
需要传入一个返回类型为FutureOr<T>
的函数。
FutureOr<T>
:表示泛型类型的同步结果或泛型类型的异步结果。FutureOr<T>
意味着可以根据逻辑返回同步结果或异步结果。下面是Future创建的核心代码。
\\nfactory Future(FutureOr<T> computation()) {\\n // 1、创建一个未完成的_Future 实例 result。\\n _Future<T> result = new _Future<T>();\\n // 2、调度异步执行\\n Timer.run(() { // 将computation加入事件队列。\\n FutureOr<T> computationResult; // 声明FutureOr<T>类型的变量。\\n try {\\n // 3、执行传入的异步操作的代码\\n computationResult = computation();\\n } catch (e, s) {\\n // 4、执行过程中出现错误的处理。将错误信息传递给_Future实例\\n _completeWithErrorCallback(result, e, s);\\n return;\\n }\\n // 5、结果处理。\\n result._complete(computationResult);\\n });\\n // 6、返回异步操作执行的结果\\n return result;\\n}\\n
\\n示例: 返回一个同步值。
\\nFutureOr<int> buildComputation() {\\n int age = 10;\\n return age;\\n}\\nvoid main() {\\n Future(buildComputation);\\n}\\n
\\n创建指定延迟时间执行的异步任务。必须传入指定的延迟时间duration。
\\n参数:Duration duration, [FutureOr<T> computation()?]
[FutureOr<T> computation()?]
:可不传入computation。示例: 延迟20秒后执行。
\\n Future.delayed(Duration(seconds: 20),()=>4);\\n
\\n立即创建一个已完成的Future对象,并携带一个值。
\\n参数:[FutureOr<T>? value]
示例: 携带的值为列表。
\\nFuture.value([\'Dart\',234]);\\n
\\n立即创建一个已失败的Future对象,并携带指定的错误信息或异常。
\\n参数:\\nObject error, [StackTrace? stackTrace]
示例: 携带的值为列表。
\\nFuture.error(Exception(\'未满18岁!\'),StackTrace.current);\\n
\\n将任务加入微任务队列,让它先执行。
\\n参数:FutureOr<T> computation()
示例: 同步任务 --\x3e 微任务 --\x3e 事件任务
\\nvoid main() {\\n print(\'同步任务A\');\\n Future(()=>print(\'事件任务\'));\\n Future.microtask(()=>print(\'微任务\'));\\n print(\'同步任务B\');\\n}\\n输出:\\n同步任务A\\n同步任务B\\n微任务\\n事件任务\\n
\\n将同步代码和异步代码统一包装为Future。
\\n参数:FutureOr<T> computation()
示例:
\\nFuture.sync(\\n (){\\n print(\'同步代码\');\\n Future(()=>1);\\n });\\n
\\n通过语法糖简化异步代码的编写,让编写异步代码和同步代码一样轻松。通过async关键字声明为异步函数,await关键字等待异步操作的完成。
\\n注意: await关键字只能在async声明的异步函数内使用。
\\n示例:
\\nFuture<void> dealTask() async {\\n await Future.delayed(Duration(seconds: 1)); \\n}\\n
\\n多个Future的处理通过下面四大静态方法实现。
\\n多个Future中获取最先执行完的那个Future。
\\n示例:
\\nFuture.any([\\n Future.delayed(Duration(seconds: 100),() => print(\'A\')),\\n Future.delayed(Duration(seconds: 80),() => print(\'B\')),\\n Future.delayed(Duration(seconds: 40),() => print(\'C\'))\\n]);\\n
\\n遍历集合中元素,并异步处理。第一个参数为元素集合,第二个为异步函数。
\\n示例:
\\nFuture.forEach([1,2,3,4,5], (value) => print(value));\\n
\\n重复执行异步任务,直到满足终止条件。
\\n示例:
\\nFuture.doWhile(()async{\\n for(int i in [1,9,3,4,5,6,7,8]){\\n if(i%2==0){\\n return false;\\n }\\n print(\'i:$i\');\\n };\\n return true;\\n});\\n
\\n同时执行多个Future,并等待所有Future执行完后返回一个包含所有Future结果的列表。若其中一个失败则会全部失败(可通过eagerError参数调整)。
\\n参数:\\nIterable<Future<T>> futures
,\\n{bool eagerError = false, void cleanUp(T successValue)?}
示例:
\\nFuture.wait([\\n Future((){\\n print(\'A\');\\n throw Exception(\'A异常\');\\n }),\\n Future((){\\n print(\'B\');\\n // throw Exception(\'B异常\');\\n }),\\n Future(()=>print(\'C\'))\\n],eagerError: false,);\\n
\\n异步结果分为两种,分别为成功结果和错误结果,分别通过then()方法、catchError()方法处理。
\\n示例:
\\nFuture.delayed(Duration(microseconds: 5000), (){print(\'异步代码\');\\n throw Exception(\'错误\');})\\n .then((result) => print(\'未携带返回结果\'))\\n .catchError((error) => print(\'$error\'))\\n .whenComplete(() => print(\'清理资源\'))\\n .timeout(Duration(seconds: 20))\\n .onError((e,s){print(\'$e\');});\\n
\\n本小节我们从Future的定义出发,首先回顾了异步操作是如何不阻塞主线程的,然后介绍了七种创建Future的方式,其次在了解了单个Future后介绍了四种多个Future的处理,最后介绍了异步结果的处理。下面是本小节的归纳总结:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\nFuture的创建 | 多个Future的处理 | Future结果的处理 |
---|---|---|
Future() | Future.any() | then() |
Future.delayed() | Future.forEach() | catchError() |
Future.value() | Future.doWhile() | whenComplete() |
Future.error() | Future.wait() | timeout() |
Future.sync() | onError() | |
Future.microtask() | asStream() | |
async/await语法糖 |
[☠] Network resources (the doctor check crashed)\\n ✗ Due to an error, the doctor check did not complete. If the error message below is not helpful, please let us know about this issue at\\n https://github.com/flutter/flutter/issues.\\n ✗ Exception: Network resources exceeded maximum allowed duration of 0:04:30.000000\\n
\\n意思是:flutter doctor 在尝试联网检查(比如下载 Google Maven、Flutter 镜像等)时,耗时超过了 4 分钟半的上限,然后整个网络检查模块就崩溃了。
\\nexport PUB_HOSTED_URL=https://pub.flutter-io.cn\\nexport FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn\\n
\\n你之前贴出来的配置里有这两项,确认已经启用(别忘了 source ~/.zshrc)。
\\nallprojects {\\n repositories {\\n maven { url \'https://maven.aliyun.com/repository/google\' }\\n maven { url \'https://maven.aliyun.com/repository/jcenter\' }\\n maven { url \'https://maven.aliyun.com/repository/central\' }\\n maven { url \'https://maven.aliyun.com/repository/public\' }\\n google()\\n mavenCentral()\\n }\\n}\\n
\\n在 Android Studio 中打开:
\\nPreferences -> Appearance & Behavior -> System Settings -> Android SDK -> SDK Update Sites\\n
\\n把里面的 URL 改为阿里云的,例如:
\\n\\nflutter pub cache repair\\nflutter clean\\nflutter pub get\\n
\\nflutter doctor -v\\n
\\n如果还是卡,说明你的本地网络(代理、防火墙)可能影响了连接 —— 可以尝试挂梯子或者使用手机热点临时连一下。
","description":"[☠] Network resources (the doctor check crashed) ✗ Due to an error, the doctor check did not complete. If the error message below is not helpful, please let us know about this issue at\\n https://github.com/flutter/flutter/issues.\\n ✗ Exception: Network resources exceeded…","guid":"https://juejin.cn/post/7491920480599326761","author":"90后晨仔","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-12T07:48:38.058Z","media":null,"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"【Flutter 状态管理 - 贰】 | 提升对界面与状态的认知","url":"https://juejin.cn/post/7491969960496267283","content":"如果把Flutter
界面比作人体,状态就是流淌在血管里的血液
。每次心跳带来新的养分,驱动着肌肉牵动表情变化。那些按钮的明暗交替
、文字的跳动更新
、动画的流畅运转
,不过是状态这个心脏泵出的血液在起作用。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\n\\n\\n在
\\nFlutter
中,界面 通常指的是应用程序的用户界面 (User Interface
,UI
),它是用户与应用程序交互的主要方式。\\n
Flutter
使用声明式的UI
构建方式,通过构建一系列的Widget
来定义界面的结构
和外观
。
官方计数器界面:
\\n在Flutter
中,界面主要由以下几个方面构成:
Widgets
:小部件Widgets
是Flutter
的基本构建单元,用于定义界面的各个部分,包括文本
、图像
、按钮
等。Widgets
可以组合成更复杂的UI
结构,形成层次化的Widget树
。State
:状态数据
或行为
特征,它会影响UI
的显示和交互。固定的文本内容
),也可以是动态的(如计数器的值
)。Layout
:布局Widgets
在界面上的位置
和大小
关系。Flutter
提供了多种布局小部件,如Column
(垂直排列)、Row
(水平排列)、Stack
(堆叠排列)等。Theme
:主题颜色
、字体
、尺寸
等。ThemeData
来定义全局的主题,并通过Theme.of(context)
获取当前主题。数据的面具
想要提升对状态管理的认知,我们需要先对状态有一个清晰的认知,那状态是具体是什么呢?
\\n来个真实场景:\\n
公司小姐姐完成了某个核心功能的开发,喜笑颜开(State A
),上线后第一天,被领导告知出现了线上事故(事件驱动
),心想这月绩效没有了,于是一天都愁眉苦脸(State B
)。
\\n\\n表情是
\\n状态
的外显,事件是内在的状态源
。
在代码世界里,万物皆数据。当数据披上界面的外衣,就成了我们口中的状态。看这段最熟悉的计数器代码:
\\nint _counter = 0; // 这才是本质\\n\\nvoid _incrementCounter() {\\n setState(() {\\n _counter++; // 数据变化触发界面重生\\n });\\n}\\n
\\n那个在屏幕上跳动的数字不过是_counter
的傀儡。新手常犯的错误是盯着Text(\'$_counter\')
这个木偶看,却忽略了背后牵线的_counter
才是真正的操盘手。就像皮影戏的观众,若只关注幕布上的光影变幻,永远参不透背后的操控逻辑。
\\n\\n在
\\nFlutter
中,状态 是描述UI
动态特性的可变数据(界面的配置
和属性
)的集合。换言之,对于界面来说,任何
\\n影响界面呈现或交互行为的数据
都可称为状态。
驱动界面重绘的魔力
Flutter
的声明式UI
像一面魔镜,你告诉它想要呈现的数据模样,它自动施展重绘魔法。但有个前提 —— 必须用setState
或状态管理框架
这些咒语唤醒魔力。
\\n\\n状态变量是用于描述应用程序的状态的
\\n变量
或数据结构
。换言之,是存储状态的具体载体
,遵循最小化原则。
官方计数器中唯一的状态变量:
\\nint _counter = 0; // 仅存储必要数据\\n
\\n在Flutter
中,任何继承自State<T>
类的成员变量自动获得状态响应能力。
class _CounterState extends State<Counter> {\\n int _count = 0; // 状态变量声明\\n bool _isActive = true; // 多个状态变量共存\\n}\\n
\\n在Flutter
架构体系中,状态被精准分割为两个平行维度:应用状态和界面状态。
全局状态
)官方定义:
\\n\\n\\n应用状态是需要在多个
\\n组件之间共享
,且需要持久化
的状态。例如:
\\n用户偏好设置
、社交应用中的通知计数
、电子商务的购物车内容
等。
化为己有:
\\n应用状态是全局性
、持久化
的数据集合,具有如下特征:
Widget
依赖),可穿透Widget树
的任意层级。用户登录凭证
)。购物车数据
)。Provider/Bloc
等)。// 典型全局状态容器\\nclass AppState {\\n final UserProfile user; // 用户资料\\n final List<Product> cartItems; // 购物车商品\\n final ThemeData theme; // 主题配置\\n final Locale language; // 语言设置\\n}\\n\\n// 使用 Provider 管理示例\\nfinal userProfileProvider = StateNotifierProvider<UserProfileNotifier, UserProfile>((ref) {\\n return UserProfileNotifier(\\n LocalStorage.read(\'user_profile\') ?? UserProfile.anonymous()\\n );\\n});\\n\\nclass UserProfileNotifier extends StateNotifier<UserProfile> {\\n UserProfileNotifier(super.state);\\n\\n void updateName(String newName) {\\n state = state.copyWith(name: newName);\\n LocalStorage.write(\'user_profile\', state); // 持久化锚点\\n }\\n}\\n
\\n局部状态
)官方定义:
\\n\\n\\n界面状态(
\\n临时状态
)指那些可以完全包含在单个Widget
中的状态。例如:页面ViewPager的当前索引
、动画的过渡值
、TextField的输入内容
等。
化为己有:
\\n界面状态是局部性
、临时性
的UI
控制数据,具有如下特征:
Widget树
内有效。表单输入草稿
)。Widget
绑定。StatefulWidget
管理。// 局部状态示例\\nclass _SearchBarState extends State<SearchBar> {\\n final _textController = TextEditingController(); // 输入控制器\\n bool _showClearButton = false; // 视觉反馈状态\\n double _panelHeight = 0.0; // 动画过渡值\\n\\n void _onTextChanged(String text) {\\n setState(() {\\n _showClearButton = text.isNotEmpty; // 局部状态变更\\n });\\n }\\n}\\n
\\n管理技巧:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n模式 | 代码示例 | 优势 |
---|---|---|
局部State | setState(() => _counter++) | 轻量快速 |
控制器模式 | TextEditingController() 管理输入 | 解耦逻辑 |
隐式动画 | AnimatedOpacity 自动过渡 | 简化动画状态管理 |
管理红线:
\\nScrollController
)。响应式更新机制:
\\nFlutter
采用声明式UI + 响应式编程的双引擎驱动:
状态提升:
\\n当局部状态需要升级
为全局状态时的操作流程:
// 原始局部状态\\nclass _MyWidgetState extends State<MyWidget> {\\n int _count = 0;\\n \\n void _increment() => setState(() => _count++);\\n}\\n\\n// 提升为全局状态\\nfinal counterProvider = StateProvider<int>((ref) => 0);\\n\\nclass MyWidget extends ConsumerWidget {\\n @override\\n Widget build(BuildContext context, WidgetRef ref) {\\n final count = ref.watch(counterProvider);\\n return ElevatedButton(\\n onPressed: () => ref.read(counterProvider.notifier).state++,\\n child: Text(\'$count\'),\\n );\\n }\\n}\\n
\\n根据企业级最佳实践,状态类型选择可参考以下流程:
\\ngraph TD\\n A[需要跨组件共享?] --\x3e|是| B[需要持久化存储?]\\n A --\x3e|否| C[使用界面状态]\\n B --\x3e|是| D[应用状态--全局管理]\\n B --\x3e|否| E[应用状态--会话级]\\n C --\x3e F[StatefulWidget + setState]\\n D --\x3e G[Provider/Riverpod + 本地存储]\\n E --\x3e H[InheritedWidget / Provider]\\n
\\n\\n\\n状态转换是用于描述应用程序从
\\n一个状态
到另一个状态
的变化过程。换言之,当应用数据(状态变量
)发生变更时,触发界面重新渲染的完整过程链
。
实现流程:
\\n// 1. 触发条件\\nonTap: () { \\n // 2. 状态变更\\n setState(() {\\n _counter++; \\n });\\n // 3. 界面更新(自动触发build方法)\\n}\\n
\\n结合日常开发的经验及归纳总结,可以得出如下 关键特征:
\\nsetState
闭包内完成数据修改。类似版本覆盖
)。setState
应包含相关变量的全部变更。\\n\\n用于控制应用程序如何
\\n从一个状态转移到另一个状态
的函数。换言之,即规范状态变更路径的控制器。
原生方案(StatefulWidget
):
void _updateUser(String newName) {\\n setState(() {\\n user = user.copyWith(name: newName); \\n lastModified = DateTime.now();\\n });\\n}\\n
\\n状态管理库方案对比:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n方案 | 代码实现 | 优势 |
---|---|---|
Provider | context.read<UserModel>().updateName() | 跨组件状态穿透 |
Bloc | add(UpdateNameEvent(newName)) | 业务逻辑与UI 解耦 |
Riverpod | ref.read(userProvider.notifier).update() | 类型安全 + 测试友好 |
设计准则:
\\n特定状态变更
。验证
新状态合法性。添加调试日志
。\\n\\n应用程序根据外部事件或内部条件的变化来改变状态。
\\n
标准流程:
\\n事件类型:
\\n// 用户输入事件\\nGestureDetector(onTap: () => _handleTap())\\n\\n// 系统事件\\nAppLifecycleListener(\\n onResume: () => _refreshData()\\n)\\n\\n// 异步回调\\nhttp.get(url).then((res) {\\n setState(() => data = res.body);\\n})\\n
\\n事件处理规范:
\\nDateTime _lastClick = DateTime.now();\\n\\nvoid _safeAction() {\\n if(DateTime.now().difference(_lastClick) > Duration(seconds: 1)) {\\n _realAction();\\n _lastClick = DateTime.now();\\n }\\n}\\n
\\ntry {\\n await _fetchData();\\n} catch (e) {\\n setState(() => error = e.toString());\\n}\\n
\\nfinal temp = _currentState;\\ntry {\\n _updateState(newState);\\n} catch (_) {\\n setState(() => _currentState = temp);\\n}\\n
\\nWidget
:构建UI
的基本单元Widget
的本质是声明式UI
的配置模板,每个Widget
实例都携带了特定的UI
属性参数。Flutter
框架通过Widget树
描述当前界面的全部内容,但其并不直接参与渲染,而是作为Element树
的构建蓝图存在。
// 典型 Widget 结构\\nclass CustomText extends StatelessWidget {\\n final String content;\\n final double fontSize;\\n\\n const CustomText({\\n super.key,\\n required this.content,\\n this.fontSize = 14,\\n });\\n\\n @override\\n Widget build(BuildContext context) {\\n return Text(\\n content,\\n style: TextStyle(fontSize: fontSize),\\n );\\n }\\n}\\n
\\n核心特征:
\\nWidget
对象频繁创建和销毁,大部分Widget
实例存活时间仅为一个帧周期。Widget
的所有属性都应声明为final
,确保参数可安全传递给子组件。Widget
自身仅存储配置数据,不保存任何运行时状态。StatelessWidget
的工作机制StatelessWidget
的渲染完全依赖外部传入的配置参数。这些参数通过构造函数初始化后便不再改变,直到下次重新构建时被新参数替换。
核心运转流程:
\\ngraph TD\\n A[开始] --\x3e B[父组件通过构造函数传递新参数]\\n B --\x3e C[Flutter框架触发组件重建]\\n C --\x3e D{新旧Widget比对<br/>runtimeType && key}\\n D -- 一致 --\x3e E[保留原有Element节点]\\n D -- 不一致 --\x3e F[销毁旧Element节点<br/>创建新Element节点]\\n E --\x3e G[触发build方法生成新布局]\\n F --\x3e G\\n G --\x3e H[UI更新完成]\\n H --\x3e I[结束]\\n\\n style A fill:#9f9,stroke:#333\\n style B fill:#fff,stroke:#333\\n style C fill:#fff,stroke:#333\\n style D fill:#ff9,stroke:#333\\n style E fill:#fff,stroke:#333\\n style F fill:#fff,stroke:#333\\n style G fill:#fff,stroke:#333\\n style H fill:#f9f,stroke:#333\\n style I fill:#9f9,stroke:#333\\n
\\n重要限制:
\\nfinal
的)。InheritedWidget
等状态管理方案)。StatefulWidget
:有状态的组件StatefulWidget
工作模型分为两个关联部分:
Widget
部分:轻量的不可变配置容器。State
对象:跨帧存在的可变状态存储器。class CounterWidget extends StatefulWidget {\\n const CounterWidget({super.key});\\n\\n @override\\n _CounterWidgetState createState() => _CounterWidgetState();\\n}\\n\\nclass _CounterWidgetState extends State<CounterWidget> {\\n int _count = 0;\\n\\n void _increment() {\\n setState(() { // 触发重建的开关\\n _count++;\\n });\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return ElevatedButton(\\n onPressed: _increment,\\n child: Text(\'已点击 $_count 次\'),\\n );\\n }\\n}\\n
\\n核心特性:
\\n维护
和更新
状态。State
对象,该对象负责存储
和更新
组件的状态。状态发生变化
时,State
对象会调用setState
方法来通知Flutter
框架重新构建UI
。状态的本质是驱动界面变化的动态数据源,如同引擎燃油 —— 数值变化触发UI
重绘。双状态明确其作用域,通过组件协作模式
,配合更新机制
精确控制数据的流动。
掌握这套系统思维,就像拥有城市蓝图 —— 知道何时架设高压电网(全局状态
),何时铺设家庭线路(局部状态
),让数据流动精确可控。
\\n","description":"前言 如果把Flutter界面比作人体,状态就是流淌在血管里的血液。每次心跳带来新的养分,驱动着肌肉牵动表情变化。那些按钮的明暗交替、文字的跳动更新、动画的流畅运转,不过是状态这个心脏泵出的血液在起作用。\\n\\n操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。\\n\\n一、界面的基本认知\\n1.1、基本概念\\n\\n在Flutter中,界面 通常指的是应用程序的用户界面 (User Interface,UI),它是用户与应用程序交互的主要方式。\\n\\nFlutter使用声明式的UI构建方式,通过构建一系列的Widget来定义界面的结构和外观。\\n\\n官方计数器界面:\\n\\n1…","guid":"https://juejin.cn/post/7491969960496267283","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-12T07:25:57.318Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/bdd13d29d25d42f6b9ad5406fd783f39~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1745047557&x-signature=GCpHqwRDOGZMVjXeZHPKocnEZmE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9a10a5d89889486ba741eb994fe5794c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1745047557&x-signature=1BZtsYRdBwy8lThH1j%2FSaHBOV28%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/bdd13d29d25d42f6b9ad5406fd783f39~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1745047557&x-signature=GCpHqwRDOGZMVjXeZHPKocnEZmE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d963946a6b10443b914687dce1275044~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1745047557&x-signature=oM4FSbndBb6FYedOz4hSgg%2FTTws%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/291bb94731c84fb1a7308af6ff88bb3a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1745047557&x-signature=ik3WHeUoPgASAYKflBwZIc0%2BmHg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ed5245ed03564559a4a0418254117169~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1745047557&x-signature=eb%2BizmQ%2Bvj7caktAlcLnpNNa%2F1c%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/619904c309704349931f58e4051fb9da~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1745047557&x-signature=Est3vdQpw1zzB%2BNWpD3n04XuwOc%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Dart","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"你不需要那么多Provider——重新理解状态管理与业务逻辑","url":"https://juejin.cn/post/7491920480598310953","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
\\n\\n状态管理一直是Flutter的热门话题, 而Provider/Riverpod更是Flutter官方的Favorite. 然而, 你真的需要这么多基于Widget树/BuildContext的状态管理吗?
\\n本文将围绕Provider的核心机制, 重新审视状态与业务逻辑的本质, 并探讨全局状态管理的优势与潜在问题, 带你找到更优雅的状态管理方式.
\\n
Provider的核心在于利用Flutter的InheritedWidget机制, 通过Widget树实现数据的分发与访问. 它的关键优势是: 底层Widget可以向上查找, 找到Widget树中离自己最近的匹配数据. 这种机制非常适合需要根据Widget树位置动态获取数据的场景.
\\n以主题样式为例, 假设我们在Widget树的顶层和中层分别放置了两个不同的ThemeData
实例:
Widget build(BuildContext context) {\\n return Provider<ThemeData>(\\n create: (_) => ThemeData(primaryColor: Colors.blue),\\n child: Column(\\n children: [\\n // 使用顶层主题(蓝色)\\n MyWidget(),\\n Provider<ThemeData>(\\n create: (_) => ThemeData(primaryColor: Colors.red),\\n child: MyWidget(), // 使用中层主题(红色)\\n ),\\n ],\\n ),\\n );\\n}\\n
\\n在这个例子中, 顶层以上的Widget会使用蓝色主题, 而中层以下的Widget会使用红色主题. 这种基于Widget树位置的数据查找机制, 让主题管理变得直观且高效.
\\n再来看一个更复杂的例子: 一个外语学习App, 用户交互页面需要使用用户的母语(例如中文), 而内部的教学页面需要使用教学语言(例如英文). 通过Provider, 我们可以在Widget树的不同层级放置不同的语言配置:
\\nWidget build(BuildContext context) {\\n return Provider<Locale>(\\n create: (_) => Locale(\'zh\', \'CN\'), // 顶层: 中文\\n child: Scaffold(\\n body: Column(\\n children: [\\n // 使用中文\\n UserInteractionWidget(),\\n Provider<Locale>(\\n create: (_) => Locale(\'en\', \'US\'), // 中层: 英文\\n child: TeachingWidget(), // 使用英文\\n ),\\n ],\\n ),\\n ),\\n );\\n}\\n
\\n通过这种方式, App可以根据Widget树的位置实现语言的自动切换, 非常适合需要动态区域化配置的场景.
\\n尽管Provider在主题和国际化等场景中表现出色, 但实际开发中需要管理的状态往往是用户操作数据, 例如表单输入、计数器数值等. 这些数据的特点是: 它们与Widget树的具体位置无关.
\\n以经典的Counter应用为例, 无论是将“+1”按钮放在AppBar中, 还是放在页面的Body内部, 其业务逻辑始终是: 点击按钮后, 计数器数值加1:
\\nclass CounterPage extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n actions: [\\n IconButton(\\n icon: Icon(Icons.add),\\n onPressed: () => context.read<Counter>().increment(),\\n ),\\n ],\\n ),\\n body: Center(\\n child: ElevatedButton(\\n onPressed: () => context.read<Counter>().increment(),\\n child: Text(\'Add\'),\\n ),\\n ),\\n );\\n }\\n}\\n
\\n无论按钮位于Widget树的哪个位置, 计数器的逻辑都不会改变. 这表明, 业务逻辑的核心是数据本身, 而不是数据的分发方式.
\\n显而易见的是: 随着业务需求变化, 业务组件(“+1”按钮)会四处移动, 必然导致存储count数据的Provider<Counter>最终移动到Widget树的顶层. <Counter>从事实上变成了全局变量(尽管会有人试图在进入/离开特定页面时将其加载/释放)
\\n既然所有的业务逻辑组件最终都会让状态Provider上移到App顶层, 那么为什么不一步到位, 从一开始就将状态放在全局变量中呢?
\\n例如, 我们可以将计数器的状态放置在全局Map中
\\n// 存储全局状态\\nfinal global_state_map = {};\\n\\nclass CounterPage extends StatelessWidget {\\n @override\\n void initState() {\\n // 初始化全局状态\\n global_state_map[\'count\'] = 0;\\n super.initState();\\n }\\n @override\\n void dispose() {\\n // 清理页面相关的全局状态\\n global_state_map.remove(\'count\');\\n super.dispose();\\n }\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n actions: [\\n IconButton(\\n icon: Icon(Icons.add),\\n onPressed: () => global_state_map[\'count\']+=1,\\n ),\\n ],\\n ),\\n body: Center(\\n child: ElevatedButton(\\n onPressed: () => global_state_map[\'count\']+=1,\\n child: Text(\'Add\'),\\n ),\\n ),\\n );\\n }\\n}\\n
\\n当然, 移除Provider后又涉及到刷新后对UI的通知. 以上代码也仅仅是演示, 实际上可以通过存储Stream+StreamBuilder等方案来实现页面刷新. 在后续的文章中, 将会介绍完整的方案.
\\nProvider的InheritedWidget机制为主题、国际化等场景提供了优雅的解决方案, 但开发场景下, 业务逻辑相关的数据没有必要与Widget树耦合. 受限于篇幅, 具体的开发方案敬请期待后续文章.
\\n心急的朋友可以直接查看 HiveState 仓库: github.com/Hu-Wentao/h…\\n示例与example均基本完备, 选择web浏览器设备即可运行web demo;
","description":"你不需要那么多Provider——重新理解状态管理与业务逻辑 状态管理一直是Flutter的热门话题, 而Provider/Riverpod更是Flutter官方的Favorite. 然而, 你真的需要这么多基于Widget树/BuildContext的状态管理吗?\\n\\n本文将围绕Provider的核心机制, 重新审视状态与业务逻辑的本质, 并探讨全局状态管理的优势与潜在问题, 带你找到更优雅的状态管理方式.\\n\\n一、Provider的核心: InheritedWidget与Widget树\\n\\nProv…","guid":"https://juejin.cn/post/7491920480598310953","author":"HuWentao","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-11T17:25:54.965Z","media":null,"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter StatelessWidget 和 StatefulWidget 的区别","url":"https://juejin.cn/post/7491873975581687835","content":"void main() {\\n runApp(const MyText(title: \\"StatelessWidget 标题文本\\"));\\n}\\nclass MyText extends StatelessWidget {\\n //配置参数(构造函数中的参数)\\n final String title;\\n\\n const MyText({super.key, required this.title});\\n\\n @override\\n Widget build(BuildContext context) {\\n //配置参数\\n //return Text(title, textDirection: TextDirection.ltr);\\n return Center(\\n child: Text(title, textDirection: TextDirection.ltr),\\n );\\n }\\n}\\n
\\nvoid main() => runApp(const MyCounter());\\nclass MyCounter extends StatefulWidget {\\n const MyCounter({super.key});\\n\\n @override\\n State<MyCounter> createState() => _MyCounterState();\\n}\\nclass _MyCounterState extends State<MyCounter> {\\n int _counter = 0;\\n\\n void _incrementCounter() {\\n //调用 setState 标记 UI 需要重新渲染(触发 UI 更新)\\n setState(() {\\n _counter++;\\n });\\n //setState(() => _counter++);\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n //\\n return MaterialApp(\\n home: Scaffold(\\n appBar: AppBar(title: const Text(\\"StatefulWidget 标题文本\\")),\\n body: Center(\\n child: Column(\\n children: [\\n Text(\\"MyCounter: $_counter\\"),\\n ElevatedButton(\\n onPressed: _incrementCounter, //调用 _incrementCounter 方法\\n child: const Text(\\"Increment\\"),\\n ),\\n ],\\n ),\\n ),\\n ),\\n );\\n }\\n\\n @override\\n void initState() {\\n super.initState();\\n //组件初始化时调用\\n //进行初始化操作(比如加载数据)\\n }\\n\\n @override\\n void dispose() {\\n //组件销毁时调用\\n //进行释放资源操作(比如取消订阅)\\n super.dispose();\\n }\\n}\\n
","description":"Flutter StatelessWidget 和 StatefulWidget 的区别 StatelessWidget 和 StatefulWidget 是构建 UI 用户界面的两种基础组件,主要区别在于状态管理(数据变化)、 UI 更新机制和生命周期\\nStatelessWidget 适用于静态 UI,而 StatefulWidget 适用于需要动态更新的 UI\\nStatelessWidget 无状态组件\\n是一种无状态的、不可变的组件,一旦构建后属性无法修改,不会随时间变化(在下次构建之前都不会改变)\\n组件的 UI 外观和行为仅由初始的配置参数…","guid":"https://juejin.cn/post/7491873975581687835","author":"louisgeek","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-11T15:08:31.111Z","media":null,"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter插件中引用aar","url":"https://juejin.cn/post/7491866188622282767","content":"最近需要制作一个基于aar的Flutter插件,但遇到了一些问题。\\n我们假设插件名称叫作my_plugin
。以前我的解决方案是把aar文件导入my_plugin/android/libs
中,然后在my_plugin/android/build.gradle
做如下修改:
rootProject.allprojects {\\n repositories {\\n google()\\n jcenter()\\n flatDir {\\n dirs project(\':my_plugin\').file(\'libs\')\\n }\\n }\\n}\\n\\ndependencies {\\n implementation fileTree(include: [\'*.aar\'], dir: \'libs\')\\n}\\n\\n
\\n但我很快发现,这条路现在行不通:
\\nExecution failed for task \'xxxx\'.\\n> Direct local .aar file dependencies are not supported when building an AAR. \\nThe resulting AAR would be broken because the classes and Android resources from any local .aar \\nfile dependencies would not be packaged in the resulting AAR. Previous versions of the Android \\nGradle Plugin produce broken AARs in this case too (despite not throwing this error). The \\nfollowing direct local .aar file dependencies of the xxxx project caused this error: \\n______.aar\\n
\\n很快我又换了一个姿势,同样使用flatDir
,但这次我改用了compileOnly
rootProject.allprojects {\\n repositories {\\n google()\\n mavenCentral()\\n flatDir {\\n dirs project(\':my_plugin\').file(\'libs\')\\n }\\n }\\n}\\n\\ndependencies {\\n compileOnly fileTree(include: [\'*.aar\'], dir: \'libs\')\\n}\\n
\\n但也引起了一个小小的麻烦,这种方式需要在宿主app中把aar文件再次导入到对应的libs
文件下,然后又要在build.gradle
中添加 implementation fileTree(include: [\'*.aar\'], dir: \'libs\')
。对于使用者来说不友好,而且flatDir
这种形式官方其实是不太推荐的(你会在控制台看到一些警告)。所以怎么相对优雅地解决这个问题?最快的方法当然是发布到远程maven之类的仓库了。谁都能想得到,但很实际开发中并不具备发布到远程maven的条件。
既然远程maven不行,那本地的maven是不是就可以了?
\\n第一种方式是直接借用maven-publish
插件了。在my_plugin/android/build.gradle
写点代码:
\\n//define this\\nString mavenLocalPath = project(\\":my_plugin\\").mkdir(\\"m2repository\\").absolutePath\\n\\n\\nrootProject.allprojects {\\n repositories {\\n google()\\n mavenCentral()\\n maven {\\n url mavenLocalPath\\n }\\n }\\n}\\n\\n\\n//apply plugin\\napply plugin: \\"maven-publish\\"\\n\\n\\npublishing {\\n publications {\\n release(MavenPublication) {\\n groupId = \'com.my-company\'\\n artifactId = \'my-library\'\\n version = \'1.0\'\\n artifact \\"path/to/aar\\"\\n }\\n }\\n repositories {\\n maven {\\n name = \'myrepo\'\\n url = mavenLocalPath\\n }\\n }\\n}\\n\\ndependencies {\\n implementation \\"com.my-company:my-library:1.0\\"\\n}\\n\\n
\\n然后使用gradle运行publish任务即可:
\\n\\n./gradlew publish\\n\\n//or \\ngradle publish\\n
\\n然后你会发现在my_plugin/m2repository
目录下生成了相关文件,大功告成!当有新版本时,我们只需再次publish即可。
第一种方是借用了maven-publish
插件,当然我们也可以手搓一个类似功能的,假设我们把aar放到了my_plugin/android/libs
下:
import java.security.MessageDigest\\nimport java.security.NoSuchAlgorithmException\\n\\n//define this\\nString mavenLocalPath = project(\\":my_plugin\\").mkdir(\\"m2repository\\").absolutePath\\n\\n\\nrootProject.allprojects {\\n repositories {\\n google()\\n mavenCentral()\\n maven {\\n url mavenLocalPath\\n }\\n }\\n}\\n\\ntask useAar {\\n File file = project.file(\\"libs\\")\\n if (file.exists() && file.isDirectory()) {\\n file.listFiles(new FileFilter() {\\n @Override\\n boolean accept(File pathname) {\\n return pathname.name.endsWith(\\".aar\\")\\n }\\n }).each { item ->\\n String aarName = item.name.substring(0, item.name.length() - 4)\\n String[] aarInfo = aarName.split(\\"-\\")\\n String sha1 = getFileSha1(item)\\n String md5 = getFileMD5(item)\\n String fromStr = item.path\\n String intoStr = aarPath + \\"/\\" + aarInfo[0].replace(\\".\\", \\"/\\") + \\"/\\" + aarInfo[1] + \\"/\\" + aarInfo[2]\\n String newName = aarInfo[1] + \\"-\\" + aarInfo[2] + \\".aar\\"\\n\\n project.copy {\\n from fromStr\\n into intoStr\\n rename(item.name, newName)\\n }\\n\\n project.file(intoStr + \\"/\\" + newName + \\".md5\\").write(md5)\\n project.file(intoStr + \\"/\\" + newName + \\".sha1\\").write(sha1)\\n\\n String pomPath = intoStr + \\"/\\" + newName.substring(0, newName.length() - 4) + \\".pom\\"\\n project.file(pomPath).write(createPomStr(aarInfo[0], aarInfo[1], aarInfo[2]))\\n project.file(pomPath + \\".md5\\").write(getFileMD5(project.file(pomPath)))\\n project.file(pomPath + \\".sha1\\").write(getFileSha1(project.file(pomPath)))\\n\\n String metadataPath = project.file(intoStr).getParentFile().path + \\"/maven-metadata.xml\\"\\n project.file(metadataPath).write(createMetadataStr(aarInfo[0], aarInfo[1], aarInfo[2]))\\n project.file(metadataPath + \\".md5\\").write(getFileMD5(project.file(metadataPath)))\\n project.file(metadataPath + \\".sha1\\").write(getFileSha1(project.file(metadataPath)))\\n dependencies {\\n implementation \\"${aarInfo[0]}:${aarInfo[1]}:${aarInfo[2]}\\"\\n }\\n }\\n }\\n}\\n\\npublic static String createMetadataStr(String groupId, String artifactId, String version) {\\n return \\"<?xml version=\\\\\\"1.0\\\\\\" encoding=\\\\\\"UTF-8\\\\\\"?>\\\\n\\" +\\n \\"<metadata>\\\\n\\" +\\n \\" <groupId>$groupId</groupId>\\\\n\\" +\\n \\" <artifactId>$artifactId</artifactId>\\\\n\\" +\\n \\" <versioning>\\\\n\\" +\\n \\" <release>$version</release>\\\\n\\" +\\n \\" <versions>\\\\n\\" +\\n \\" <version>$version</version>\\\\n\\" +\\n \\" </versions>\\\\n\\" +\\n \\" <lastUpdated>${new Date().format(\'yyyyMMdd\')}000000</lastUpdated>\\\\n\\" +\\n \\" </versioning>\\\\n\\" +\\n \\"</metadata>\\\\n\\"\\n}\\n\\npublic static String createPomStr(String groupId, String artifactId, String version) {\\n return \\"<?xml version=\\\\\\"1.0\\\\\\" encoding=\\\\\\"UTF-8\\\\\\"?>\\\\n\\" +\\n \\"<project xsi:schemaLocation=\\\\\\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\\\\\\" xmlns=\\\\\\"http://maven.apache.org/POM/4.0.0\\\\\\"\\\\n\\" +\\n \\" xmlns:xsi=\\\\\\"http://www.w3.org/2001/XMLSchema-instance\\\\\\">\\\\n\\" +\\n \\" <modelVersion>4.0.0</modelVersion>\\\\n\\" +\\n \\" <groupId>$groupId</groupId>\\\\n\\" +\\n \\" <artifactId>$artifactId</artifactId>\\\\n\\" +\\n \\" <version>$version</version>\\\\n\\" +\\n \\" <packaging>aar</packaging>\\\\n\\" +\\n \\"</project>\\\\n\\"\\n}\\n\\npublic static String getFileSha1(File file) {\\n FileInputStream input = null;\\n try {\\n input = new FileInputStream(file);\\n MessageDigest digest = MessageDigest.getInstance(\\"SHA-1\\");\\n byte[] buffer = new byte[1024 * 1024 * 10];\\n\\n int len = 0;\\n while ((len = input.read(buffer)) > 0) {\\n digest.update(buffer, 0, len);\\n }\\n String sha1 = new BigInteger(1, digest.digest()).toString(16);\\n int length = 40 - sha1.length();\\n if (length > 0) {\\n for (int i = 0; i < length; i++) {\\n sha1 = \\"0\\" + sha1;\\n }\\n }\\n return sha1;\\n }\\n catch (IOException e) {\\n System.out.println(e);\\n }\\n catch (NoSuchAlgorithmException e) {\\n System.out.println(e);\\n }\\n finally {\\n try {\\n if (input != null) {\\n input.close();\\n }\\n }\\n catch (IOException e) {\\n System.out.println(e);\\n }\\n }\\n}\\n\\npublic static String getFileMD5(File file) {\\n FileInputStream input = null;\\n try {\\n input = new FileInputStream(file);\\n MessageDigest digest = MessageDigest.getInstance(\\"MD5\\");\\n byte[] buffer = new byte[1024 * 1024 * 10];\\n\\n int len = 0;\\n while ((len = input.read(buffer)) > 0) {\\n digest.update(buffer, 0, len);\\n }\\n String md5 = new BigInteger(1, digest.digest()).toString(16);\\n int length = 32 - md5.length();\\n if (length > 0) {\\n for (int i = 0; i < length; i++) {\\n md5 = \\"0\\" + md5;\\n }\\n }\\n return md5;\\n }\\n catch (IOException e) {\\n System.out.println(e);\\n }\\n catch (NoSuchAlgorithmException e) {\\n System.out.println(e);\\n }\\n finally {\\n try {\\n if (input != null) {\\n input.close();\\n }\\n }\\n catch (IOException e) {\\n System.out.println(e);\\n }\\n }\\n}\\n\\n
\\n当然方式可能有很多,最后我也只用了第一种方式,如果大家有更好的招,欢迎指出~
","description":"背景 最近需要制作一个基于aar的Flutter插件,但遇到了一些问题。 我们假设插件名称叫作my_plugin。以前我的解决方案是把aar文件导入my_plugin/android/libs中,然后在my_plugin/android/build.gradle做如下修改:\\n\\nrootProject.allprojects {\\n repositories {\\n google()\\n jcenter()\\n flatDir {\\n dirs project(\':my_plugin\').file(…","guid":"https://juejin.cn/post/7491866188622282767","author":"JarvanMo","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-11T14:15:45.290Z","media":null,"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Dart 之异步模型","url":"https://juejin.cn/post/7491881721278234658","content":"Dart异步模型?模型就算了还是异步的,太难了吧。但当你真的理解异步模型后,你会感叹异步模型的出现也太完美了吧。为啥说异步模型出现很完美呢?因为它通过事件循环+任务队列的方式,即满足了异步需求的同时又解决了Dart单线程的局限。那它都是怎么解决的呢?相信读完这篇内容你就明白了。
\\nDart异步模型的出现是为了满足异步操作的需求和解决Dart单线程的局限。下面分别介绍一下都有哪些异步操作需求和单线程有哪些局限。
\\n异步操作需求:\\n异步操作就是为了让耗时的操作(如网络请求)不会影响后面的操作。也就是说当有耗时操作时,程序不会等待耗时操作完成,而是继续执行接下来的操作。下面是一些常见的耗时操作:
\\n单线程的局限:\\n单线程就是按照顺序执行且一次只能做一件事,当遇到耗时操作时就会卡死。它的局限有以下几点:
\\nDart异步模型由事件循环+任务队列实现,其将I/O操作交给操作系统执行,不会阻塞当前线程。那Dart咋知道操作系统执行完了呢?事件循环会负责监听操作系统返回的结果,并在主线程空闲的时候触发回调。下面将具体介绍一下异步模型的核心——事件循环(Event Loop)
\\nDart是单线程语言,其所有任务都是通过主线程(主Isolate)负责执行。
\\nDart选择单线程,那它有哪些优势呢?
\\n答:单线程按照顺序执行,天然避免了多线程的资源竞争和同步问题(如锁、死锁)
\\nDart是单线程如何实现的并发和并行呢?
\\n答:通过事件循环机制实现异步并发,通过开辟多个Isolate实现并行计算。(Isolate相关内容后续文章介绍)
\\nDart异步模型通过事件循环调度和执行任务实现交错执行任务,从而实现并发。它的核心逻辑是维护两个任务队列,所以说Dart异步模型是由事件循环+任务队列实现的。下面先了解一下任务队列,然后再了解事件循环都是如何循环调度的。
\\n任务队列: 按照任务执行优先级分为两类。
\\n调度规则: 优先级高的先执行。
\\n循环执行规则: 首先判断微任务队列是否为空,若微任务队列不为空则执行微任务队列中所有内容,执行完后判断事件任务队列是否为空,若不为空则执行事件任务队列中排在首位的事件任务,执行完后,返回判断微任务队列是否为空,若不为空则执行所有微任务队列中的任务,若为空则继续执行事件任务队列中的第二个事件任务(假设有第二个,没有则结束事件循环)。
\\n代码示例:
\\nvoid eventLoop() {\\n while (true) {\\n if (微任务队列不为空) {\\n 执行微任务队列中所有微任务;\\n } else if (事件队列不为空) {\\n 处理事件任务队列中的第一个事件任务;\\n } else {\\n 等待新的事件;\\n }\\n }\\n}\\n
\\n下图为实际问题时可供选择的解决方案。
\\n本小节首先从Dart中为什么需要异步模型开始介绍了单线程的局限性和异步操作的需求,其次介绍了Dart单线程如何利用事件循环+任务队列实现异步模型,最后介绍了Dart异步模型中可能需要注意的点以及如何处理同步代码中的耗时操作。
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n事件循环 | 任务队列 | 需要注意的点 |
---|---|---|
调度规则:优先级高的先执行 | 微任务队列 | 不用担心性能问题 微任务并不是比事件任务快 微任务通常为小任务,防止事件任务饥饿 |
循环执行规则: 首先执行完微任务队列中全部微任务, 然后执行事件任务中第一个事件任务, 结束后返回判断任务队列是否为空, 如此循环直到任务队列没有任务,等待任务。 | 事件任务队列 | 如何处理同步任务中的耗时操作? 判断任务是否可拆分为多个微任务, 若能则使用微任务调度, 若不能则选择开辟多个Isolate并行计算解决。 |
signals是继privider,riverpod状态管理器之后的又一个热门;\\n它运用方便,理解简单,特别是已经对前端signals框架比较熟悉的同学。
\\n文本旨在研究signals的原理,复刻一个“够用就行”的低配版状态管理框架。
\\nsignals 的核心用法就是 signal 和 computed 方法。
\\nfinal a = signal(\'a\');\\nfinal b = signal(\'b\');\\nfinal c = computed(() => a.value + b.value);\\nfinal dispose = effect((){\\n print(\\"c is ${c.value}\\");\\n});\\nexpect(c.value, \'ab\');\\na.value = \'aa\';\\nexpect(c.value, \'aab\');\\ndispose();\\n
\\n从上面的代码可以看出,我们在改变a的值后,c的值也随之改变。当c值改变时会触发effect的回调。\\n在flutter中我们可以将effect方法看作是 build方法,返回Wdiget树,那么当我们改变某个signal值时就会触发build重绘widget。
\\n有意思的是,effect方法实际上监听的只是c的值,之所以改变a会触发c的改变是因为competed方法里c的值用到了a.value。
\\n因此singals的核心思想就是这种链式监听。
\\n初学者会感觉这像是一种魔法,因为c.value并没有传入任何的变量,那c和a,b等是如何实现关联监听的呢?\\n从singals源码中可以一窥究竟:
\\n@override\\nT get value {\\n final node = addDependency(this);\\n\\n if (node != null) {\\n node.version = this.version;\\n }\\n return this.internalValue;\\n}\\n\\nNode? addDependency(ReadonlySignal signal) {\\n\\n if (evalContext == null) {\\n return null;\\n }\\n var node = signal.node;\\n if (node == null || node.target != evalContext) {\\n node = Node()\\n ..version = 0\\n ..source = signal\\n ..prevSource = evalContext!.sources\\n ..nextSource = null\\n ..target = evalContext!\\n ..prevTarget = null\\n ..nextTarget = null\\n ..rollbackNode = node;\\n if (evalContext!.sources != null) {\\n evalContext!.sources!.nextSource = node;\\n }\\n evalContext!.sources = node;\\n signal.node = node;\\n\\n if ((evalContext!.flags & TRACKING) != 0) {\\n signal.subscribeToNode(node);\\n }\\n return node;\\n\\n } else if (node.version == -1) {\\n node.version = 0;\\n if (node.nextSource != null) {\\n node.nextSource!.prevSource = node.prevSource;\\n if (node.prevSource != null) {\\n node.prevSource!.nextSource = node.nextSource;\\n }\\n\\n node.prevSource = evalContext!.sources;\\n node.nextSource = null;\\n evalContext!.sources!.nextSource = node;\\n evalContext!.sources = node;\\n }\\n return node;\\n }\\n return null;\\n}\\n
\\n以上是Signal类中截取的获取value的方法,可以看出它调用了addDependency返回了一个node。而在addDependency则对每个Signal初始化了一个Node对象用来管理Signal之间的关系,从代码实现来看Node对象之间是通过双向链表实现关联的。且从代码中还可以发evalContext这个引用频繁出现,且是一个全局变量。
\\n我们再看看Computed的实现:
\\n@override\\nT get value {\\n if ((flags & RUNNING) != 0) {\\n throw Exception(\'Cycle detected\');\\n }\\n final node = addDependency(this);\\n internalRefresh();\\n if (node != null) {\\n node.version = version;\\n }\\n if ((flags & HAS_ERROR) != 0) {\\n throw error!;\\n }\\n return _internalValue;\\n}\\n\\nbool internalRefresh() {\\n this.flags &= ~NOTIFIED;\\n if ((this.flags & RUNNING) != 0) {\\n return false;\\n }\\n\\n if ((this.flags & (OUTDATED | TRACKING)) == TRACKING) {\\n return true;\\n }\\n\\n this.flags &= ~OUTDATED;\\n if (this.internalGlobalVersion == globalVersion) {\\n return true;\\n }\\n\\n this.internalGlobalVersion = globalVersion;\\n this.flags |= RUNNING;\\n if (version > 0 && !needsToRecompute(this)) {\\n this.flags &= ~RUNNING;\\n return true;\\n }\\n final prevContext = evalContext;\\n try {\\n prepareSources(this);\\n evalContext = this;\\n final val = this.fn();\\n if (!_isInitialized ||\\n (flags & HAS_ERROR) != 0 ||\\n _internalValue != val ||\\n version == 0) {\\n internalValue = val;\\n flags &= ~HAS_ERROR;\\n version++;\\n }\\n } catch (err) {\\n error = err;\\n flags |= HAS_ERROR;\\n version++;\\n }\\n evalContext = prevContext;\\n cleanupSources(this);\\n flags &= ~RUNNING;\\n return true;\\n}\\n
\\n代码很长,我们可以看到Computed和Signal的value对象一样调用了addDependency,之后调用了internalRefresh方法,在internalRefresh方法里我们又看到evalContext的身影,我们发现在调用 final val = this.fn();前它:evalContext = this;调用结束后又重制回来了:evalContext = prevContext;
\\n从上面代码我们不难看出,Signals的魔法就在这里通过全局变量evalContext临时存储上下文信息,执行结束后又重置回来。 魔法就此揭开了。也许有人会问,这样设置全局变量不会有并发问题吗?答案是不会,因为dart的运行时isolate是单线程模型,它运行的实际上是按照顺序执行代码块,这里不详细展开讲解,可以自行查isolate的执行原理。
\\nOK,那么我们如何在flutter中实现一个简单的像signals里signal方法和computed方法呢?首先我们也需要写一个Node用来管理调用链之间的关系,这里为了简单只设计两层的调用链,也就是computed只允许signal,不允许computed嵌套使用。他们的关系就先用树结构进行管理:
\\nmixin class Node {\\n //关联的子节点用Set是为了避免节点重复加入\\n final Set<Node> children = {};\\n //添加子节点\\n void addChild(Node node) {\\n children.add(node);\\n }\\n //删除子节点\\n void removeChild(Node node) {\\n children.remove(node);\\n }\\n //通知子节点改变\\n void notify() {\\n for (var child in children) {\\n child.notify();\\n }\\n }\\n //销毁\\n void dispose() {\\n children.clear();\\n }\\n}\\n
\\n有了节点,我们就先写Singal,这里为了和Signal区分就用UseValue代替,为了方便就直接用Flutter的ValueNotifier:
\\ntypedef DisposeFn = void Function();\\n\\ntypedef ComputeFn<T> = T Function();\\n\\nNode? childNode;\\n\\nclass UseValue<T> extends ValueNotifier<T> with Node {\\n final DisposeFn? disposeFn;\\n UseValue(super.value, {this.disposeFn});\\n \\n @override\\n T get value {\\n if (childNode != null) {\\n addChild(childNode!);\\n }\\n var value = super.value;\\n return value;\\n }\\n\\n @override\\n void dispose() {\\n disposeFn?.call();\\n super.dispose();\\n }\\n\\n @override\\n void notifyListeners() {\\n super.notifyListeners();\\n notify();\\n }\\n}\\n
\\n在以上代码中我们定义了一个childNode全局对象,这个跟signals里的evelContext是一个作用,用于关联节点之间的关系。在T get value方法里,会去判断childNode是否为空,如果不为空则会将它加入到自己的children里,如果自己的之值发生改变则会去通知children也去改变。
\\n有了Signal再写Computed,为了区分,我这里用UseComputed代替:
\\nclass UseComputed<T> extends ChangeNotifier with Node {\\n final ComputeFn<T> computeFn;\\n UseComputed(this.computeFn);\\n T get value {\\n var oldChildNode = childNode;\\n try {\\n childNode = this;\\n var computeValue = computeFn();\\n return computeValue;\\n } finally {\\n childNode = oldChildNode;\\n }\\n }\\n\\n @override\\n void notify() {\\n notifyListeners();\\n }\\n}\\n
\\n从上面代码中 T get value 的实现可以看出,我们会把全局变量 childNode暂时缓存并替换成自己,执行完回调函数后又重置回去。
\\n我们再写一个State的实现:
\\nmixin MixinUseState<W extends StatefulWidget> on State<W> {\\n final Set<DisposeFn> _disposeFnSet = {};\\n @override\\n void dispose() {\\n for (var disposeFn in _disposeFnSet) {\\n disposeFn.call();\\n }\\n super.dispose();\\n }\\n \\n void _notifyChanged() {\\n setState(() {});\\n }\\n \\n //类似 signals里的 signal 方法\\n UseValue<T> use<T>(T value,{bool addListener}) {\\n var useValue = UseValue<T>(value);\\n if(addListener){\\n useValue.addListener(_notifyChanged);\\n }\\n _disposeFnSet.add(useValue.dispose);\\n return useValue;\\n }\\n //类似 signals里的 computed 方法\\n UseComputed<T> computed<T>(ComputeFn<T> computeFn) {\\n var useComputed = UseComputed(computeFn);\\n _disposeFnSet.add(useComputed.dispose);\\n useComputed.addListener(_notifyChanged);\\n return useComputed;\\n }\\n\\n}\\n
\\n在上面代码中,还添加了自动dispose的功能,调用use和compted时会将dispose方法放入到 _disposeFnSet中。
\\n再写个Demo:
\\n void main() {\\n runApp(const MyApp());\\n }\\n\\n class MyApp extends StatelessWidget {\\n const MyApp({super.key});\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n title: \'Flutter Demo\',\\n theme: ThemeData(\\n colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),\\n useMaterial3: true,\\n ),\\n home: const MyHomePage(title: \'Flutter Demo Home Page\'),\\n );\\n }\\n }\\n\\n class MyHomePage extends StatefulWidget {\\n const MyHomePage({super.key, required this.title});\\n final String title;\\n @override\\n State<MyHomePage> createState() => _MyHomePageState();\\n }\\n\\n\\n class _MyHomePageState extends State<MyHomePage> with MixinUseState {\\n late final c1 = use(0);\\n late final c2 = use(2);\\n late final counter = computed(() {\\n return c1.value + c2.value;\\n });\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n backgroundColor: Theme.of(context).colorScheme.inversePrimary,\\n title: Text(widget.title),\\n ),\\n body: Center(\\n child: Column(\\n mainAxisAlignment: MainAxisAlignment.center,\\n children: <Widget>[\\n const Text(\\n \'You have pushed the button this many times:\',\\n ),\\n Text(\\n \'${counter.value}\',\\n style: Theme.of(context).textTheme.headlineMedium,\\n ),\\n ],\\n ),),\\n floatingActionButton: FloatingActionButton(\\n onPressed: () {\\n c1.value++;\\n },\\n tooltip: \'Increment\',\\n child: const Icon(Icons.add),\\n ), // This trailing comma makes auto-formatting nicer for build methods.\\n );\\n }\\n }\\n
\\n在测试代码中,定义了 c1,c2,c3,首先c1和c2类似于signal,而c3则是computed,我们在改变c1.value时会触发c3的改变,进行重新计算,并刷新UI。
\\n自此这个简单的demo就实现完成了,但signals的功能远不止这些,例如batch可以同时改变多个值减少刷新次数,containner,类似riverpod里的family等。
","description":"signals是继privider,riverpod状态管理器之后的又一个热门; 它运用方便,理解简单,特别是已经对前端signals框架比较熟悉的同学。 文本旨在研究signals的原理,复刻一个“够用就行”的低配版状态管理框架。\\n\\nsignals 的核心用法就是 signal 和 computed 方法。\\n\\nfinal a = signal(\'a\');\\nfinal b = signal(\'b\');\\nfinal c = computed(() => a.value + b.value);\\nfinal dispose = effect((){\\n pri…","guid":"https://juejin.cn/post/7491853446532087834","author":"孤鸿玉","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-11T10:02:57.090Z","media":null,"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Dart 单线程异步模型:从原理到工程实践的系统化解析","url":"https://juejin.cn/post/7491670661543510016","content":"传统单线程的痛点
\\nfor循环1亿次
会独占线程,导致 UI 冻结外卖员举例
\\n技术维度 | 多线程模型(Java) | 事件驱动模型(Dart) |
---|---|---|
并发单元 | 线程 | 任务(函数级回调) |
内存模型 | 共享内存(需解决竞态条件) | Isolate 内存隔离(通过SendPort/ReceivePort 通信) |
适用场景 | CPU 密集型任务 | I/O 密集型任务 |
Future.then
、Future.catchError
、scheduleMicrotask
创建的任务HttpClient
响应、文件读写完成)Future.delayed
触发)任务类型 | 触发方式 | 执行时机 | 典型场景 | 对 UI 的影响 |
---|---|---|---|---|
同步代码 | 直接调用 | 立即执行,阻塞线程 | main() 函数体代码 | 阻塞期间无法渲染 |
微任务 | then /scheduleMicrotask | 当前循环优先执行完毕 | 状态变更后的即时回调 | 不阻塞,优先于事件任务 |
事件任务 | I/O 完成 / Timer 到期 | 微任务清空后逐个执行 | 网络响应 / 用户点击事件 | 按序处理,可能延迟 |
代码优先级举例:
\\nvoid main() {\\n // 同步代码\\n print(\'同步代码开始\');\\n print(\'这是同步代码中的打印语句\');\\n\\n // 微任务\\n scheduleMicrotask(() {\\n print(\'微任务1\');\\n print(\'微任务2\');\\n print(\'微任务3\');\\n });\\n\\n // 事件任务\\n Future.delayed(Duration.zero, () {\\n print(\'事件任务开始执行\');\\n print(\'这是事件任务中的打印语句\');\\n });\\n\\n print(\'同步代码结束\');\\n}\\n
\\n输出结果:\\n同步代码开始\\n这是同步代码中的打印语句\\n同步代码结束\\n微任务1\\n微任务2\\n微任务3\\n事件任务开始执行\\n这是事件任务中的打印语句\\n\\n
\\n同步代码拥有最高优先级,这是因为同步代码是程序的基础流程,只有同步代码执行完毕,才会处理其他异步任务。微任务队列的优先级仅次于同步代码,只有将微任务队列中的微任务处理完成才处理事件队列中的任务,事件队列每处理一个任务都要查看微任务队列是否有新任务,以此往复。
\\n错误代码:
\\n// 错误认知:认为两个Future会并行执行\\nFuture(() => heavyCompute1()); \\nFuture(() => heavyCompute2()); \\n
\\n真相:
\\n✅ 任务在事件循环中执行,执行顺序由调度决定(非确定性)
\\n✅ 真正并行需通过Isolate.spawn()
创建独立线程
错误代码:
\\n// 表面异步,实际阻塞的代码\\nFuture<void> loadData() async {\\n await networkRequest(); // 非阻塞I/O\\n processDataSync(); // 同步计算,阻塞主线程\\n}\\n
\\n修正方案:
\\n✅ CPU 密集型任务必须通过compute()
或手动创建 Isolate offload 到后台线程
错误代码:
\\n// 错误:将大量非紧急任务放入微任务队列\\nlist.forEach((item) {\\n scheduleMicrotask(() => process(item));\\n});\\n
\\n危害:
\\n❌ 导致事件队列饥饿,UI 交互响应延迟超 16ms(60fps 标准)
修正方案
\\n✅ 微任务执行时间阈值:单个微任务耗时应 < 5ms(避免超过 16ms 的 UI 帧间隔)
\\n✅ 分层调度:非紧急任务使用Future.delayed(Duration.zero)
放入事件队列
优先级控制:状态更新用then
,用户交互用事件队列,计算任务用 Isolate
阻塞预防: 同步代码不超过 16ms 执行时间,否则分片
\\n异常安全:每个Future
链必须有catchError
通过系统化理解 Dart 的单线程异步模型,开发者能够在 I/O 密集型场景发挥其高效调度优势,同时通过 Isolate 和任务分片技术突破 CPU 密集型任务的瓶颈。关键在于建立 “任务分类 - 优先级调度 - 风险控制” 的三层思维模型,让单线程架构在保持简单性的同时,具备应对复杂场景的扩展性。
","description":"一、认知重构:单线程异步的本质突破 (一)打破单线程阻塞的思维定式\\n\\n传统单线程的痛点\\n\\n同步阻塞:如for循环1亿次会独占线程,导致 UI 冻结\\nDart 的颠覆性设计:通过非阻塞 I/O+事件循环实现 “单线程不阻塞”\\n ✅ I/O 操作(网络 / 文件)由操作系统异步处理,主线程仅处理回调\\n ✅ 事件循环(Event Loop)作为任务调度器,实现异步任务的有序执行\\n\\n外卖员举例\\n\\n同步阻塞:用户点外卖以后一直在门口等外卖,期间做不了其他的事情,外卖到手后才开始工作下一件事(CPU 密集型任务)\\n异步非阻塞:用户点外卖后不用等着外卖…","guid":"https://juejin.cn/post/7491670661543510016","author":"_痞老板","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-11T03:36:06.807Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/cc7850eb046f4f5d93c8c233fe96daa4~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgX-eXnuiAgeadvw==:q75.awebp?rk3s=f64ab15b&x-expires=1744947619&x-signature=kiJeekcKxSdfrfKkxF4%2Bz%2F%2Fjsgo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7852158c73da4ee88a55a53243d6ba16~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgX-eXnuiAgeadvw==:q75.awebp?rk3s=f64ab15b&x-expires=1744947619&x-signature=s3LfT8a9XwRKwIKU3ptwjIeUZaI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c85faf9e17724e2898c77d3d78b9208a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgX-eXnuiAgeadvw==:q75.awebp?rk3s=f64ab15b&x-expires=1744947619&x-signature=JPge3khhxnV5%2BwDtG2scDZBxf2o%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"Flutter开箱即用一站式解决方案-新增企业级日志","url":"https://juejin.cn/post/7491589110249439269","content":"本库为Flutter应用开发提供一站式解决方案,包含:
\\nThemeExtension
全局配置颜色/圆角/间距等样式在 pubspec.yaml
中添加依赖:
/// 1.8.0版本已移除图片选择裁剪上传oss一站式解决方案\\ndependencies:\\n flutter_chen_common: 最新版本\\n
\\n运行命令:
\\nflutter pub get\\n
\\nvoid main() async {\\n WidgetsFlutterBinding.ensureInitialized();\\n\\n // 存储初始化\\n await SpUtil.init();\\n // 日志初始化\\n await Log.init(\\n const LogConfig(\\n retentionDays: 3,\\n enableFileLog: true,\\n logLevel: LogLevel.all,\\n recordLevel: LogLevel.info,\\n output: [CustomSentryOutput()],\\n ),\\n );\\n // 网络模块初始化\\n HttpClient.init(\\n config: HttpConfig(\\n baseUrl: \'https://api.example.com\',\\n connectTimeout: const Duration(seconds: 30),\\n receiveTimeout: const Duration(seconds: 30),\\n enableLog: true,\\n maxRetries: 3,\\n interceptors: [CustomInterceptor()]\\n ),\\n );\\n\\n runApp(MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return ComConfiguration(\\n config: ComConfig.defaults().copyWith(\\n emptyWidget: CustomEmptyWidget(), // 自定义全局空视图\\n loadingWidget: CustomLoading(), // 自定义全局加载视图\\n ),\\n child: MaterialApp(\\n theme: ThemeData.light().copyWith(\\n extensions: [ComTheme.light()], // 启用亮色主题\\n ),\\n darkTheme: ThemeData.dark().copyWith(\\n extensions: [ComTheme.dark()], // 启用暗色主题\\n ),\\n home: MainPage(),\\n localizationsDelegates: [\\n ComLocalizations.delegate, // 国际化\\n ],\\n supportedLocales: [\\n const Locale(\'zh\', \'CN\'),\\n const Locale(\'en\', \'US\'),\\n ],\\n ),\\n );\\n }\\n}\\n
\\n// 网络请求使用\\nHttpClient.instance.request(\\n \\"/xxxx\\",\\n method: HttpType.post.name,\\n fromJson: (json) => User.fromJson(json),\\n showLoading: true,\\n)\\n\\n// HttpConfig,内置日志打印、网络重试拦截器(后续会新增token无感刷新以及相关队列操作)\\nHttpConfig({\\n required this.baseUrl,\\n this.connectTimeout = const Duration(seconds: 15),\\n this.receiveTimeout = const Duration(seconds: 15),\\n this.sendTimeout = const Duration(seconds: 15),\\n this.commonHeaders = const {},\\n this.interceptors = const [],\\n this.enableLog = true,\\n this.maxRetries = 3,\\n });\\n\\n// 打印样式如下(日志打印完全不会被截断,json格式化方便复制查看数据,在开启日志拦截以及记录日志时会将日志写入文件\\n┌─────────────────────────────────────────────────────────────────────────────\\n│ ✅ [HTTP] 2025-04-05 23:30:29 Request sent [Duration] 88ms\\n│ Request: 200 GET http://www.weather.com.cn/data/sk/101010100.html?xxxx=xxxx\\n│ Headers: {\\"token\\":\\"xxxxx\\",\\"content-type\\":\\"application/json\\"}\\n│ Query: {\\"xxxx\\":\\"xxxx\\"}\\n│ Response: {\\"weatherinfo\\":{\\"city\\":\\"北京\\",\\"cityid\\":\\"101010100\\",\\"WD\\":\\"东南风\\"}}\\n└──────────────────────────────────────────────────────────────────────────────\\n
\\n// 统一调用示例\\nLog.d(\\"debug message\\");\\nLog.i(\\"info message\\");\\nLog.w(\\"warning message\\");\\nLog.e(\\"error message\\");\\nLog.console(\\"console message 可完整打印不被截断并且无前缀\\");\\n\\n// 获取日志文件目录(已写了原生zip压缩加密库,可在app中分享日志加密压缩包,等我发布pub整好文档会发出来)\\nfinal Directory dir = await Log.getLogDir();\\n\\nclass LogConfig {\\n final int retentionDays; // 日志保留天数\\n final bool enableFileLog; // 是否启用日志写入\\n final LogLevel logLevel; // 日志过滤级别,低于该日志级别不打印\\n final LogLevel recordLevel; // 日志记录级别(Network日志级别分别是Info、Error),低于该日志级别不写入日志文件\\n final List<LogOutput>? output; // 可自定义扩展LogOutput,如Sentry上报、日志上传服务器、加密脱敏输出等(类似dio拦截器)\\n\\n const LogConfig({\\n this.retentionDays = 3,\\n this.enableFileLog = true,\\n this.logLevel = LogLevel.all,\\n this.recordLevel = LogLevel.info,\\n this.output,\\n });\\n}\\n
\\n主题名称 | 示例代码 |
---|---|
Light Theme | ComTheme.light |
Dark Theme | ComTheme.dark |
ComTheme(\\n theme: ComColors.lightTheme, // 颜色体系\\n shapes: ComShapes.standard,// 圆角体系\\n spacing: ComSpacing.standard,// 间距体系\\n primaryGradient: LinearGradient(\\n colors: [\\n ComColors.lightTheme.shade500,\\n ComColors.lightTheme.shade500,\\n ],\\n ),\\n success: Colors.green.shade600,\\n error: Colors.red.shade600,\\n warning: Colors.orange.shade600,\\n link: Colors.blue.shade600,\\n)\\n\\n// 色系\\nstatic MaterialColor lightTheme = const MaterialColor(\\n 0xFF3783FD,\\n <int, Color>{\\n 50: Color(0xfff8f6f9), // surface 背景色\\n 100: Color(0xfff8f2fa), // surfaceContainerLow 浅色背景色\\n 200: Color(0xfff2ecf4), // surfaceContainer 标准背景色\\n 300: Color(0xffece6ee), // surfaceContainerHigh 较深背景色\\n 400: Color(0xffe6e0e9), // surfaceContainerHighest 深色背景色\\n 500: Color(0xFF3783FD), // primary 主题色\\n 600: Color(0xff1d1b20), // onSurface 主要内容色\\n 700: Color(0xFF909399), // onSurfaceVariant 次要内容色\\n 800: Color(0xffffffff), // surfaceContainerLowest 相同色\\n 900: Color(0xff322f35), // inverseSurface 相反色\\n },\\n);\\n
\\n// 语言新增或覆盖\\n// 1. 创建法语本地化类\\nclass FrIntl extends ComIntl {\\n @override String get confirm => \\"xxx\\";\\n @override String get cancel => \\"xxx\\";\\n @override String get loading => \\"...\\";\\n}\\n\\n// 2. 注册语言\\nComLocalizations.addLocalization(\'fr\', FrIntl());\\n\\n// 3. 配置MaterialApp\\nMaterialApp(\\n supportedLocales: [\\n Locale(\'fr\'), // 新增法语支持\\n ],\\n)\\n
\\n// 全局配置或局部配置\\nComConfiguration(\\n config: ComConfig.defaults().copyWith(\\n emptyWidget: const ComLoading(), // 定义全局空视图\\n loadingWidget: const ComEmpty(), // 定义全局加载视图\\n errorWidget: const ComErrorWidget(), // 定义错误加载视图\\n noNetworkWidget: (VoidCallback? onReconnect) =>\\n ComNoNetworkWidget(onReconnect: onReconnect), // // 定义全局网络错误视图\\n ),\\n child: child,\\n);\\n\\n// BaseWidget的各状态布局默认使用全局统一配置,局部可自定义\\n// isConnected配合connectivity_plus库自动实现无网络情况显示无网络状态布局,网络正常情况显示正常布局\\n// status控制页面各状态内容布局显示\\nBaseWidget(\\n isConnected: isConnected,\\n status: LayoutStatus.loading,\\n loading: const ComLoading(),\\n empty: const CustomEmpty(),\\n error: BaseWidget.errorWidget(context),\\n noNetwork: BaseWidget.noNetworkWidget(context),\\n onReconnect: (){},\\n child: child,\\n)\\n\\n// 全局统一使用\\nBaseWidget.loadingWidget(context)\\nBaseWidget.errorWidget(context)\\n...\\n\\n
\\n文件名 | 功能描述 |
---|---|
clipboard_util.dart | 剪贴板操作工具(复制/粘贴文本、监听剪贴板内容) |
clone_util.dart | 对象深拷贝/浅拷贝工具(支持复杂对象克隆) |
color_util.dart | 颜色处理工具(HEX与RGB互转、颜色混合、随机颜色生成) |
date_util.dart | 日期时间工具(格式化、解析、计算时间差) |
device_util.dart | 设备信息工具(获取设备信息) |
encrypt_util.dart | 加密解密工具(算法封装) |
file_util.dart | 文件操作工具(读写文件、目录管理、文件压缩/解压) |
function_util.dart | 通用函数工具(防抖/节流、空安全处理、类型转换) |
image_util.dart | 图片处理工具(压缩、缓存管理、网络图片加载、格式转换) |
json_util.dart | JSON工具(序列化/反序列化、动态解析、数据校验) |
keyboard_util.dart | 键盘工具(控制键盘显隐、监听高度变化) |
log_util.dart | 日志工具(分级输出、日志存储、调试模式开关) |
package_util.dart | 应用包管理工具(获取应用包信息) |
permission_util.dart | 权限管理工具(全局权限处理、多权限判断及请求) |
sp_util.dart | 本地存储工具(基于SharedPreferences,支持复杂数据存取) |
text_util.dart | 文本处理工具(字符串校验、截断、正则匹配) |
dialog_util.dart | 弹窗工具类(通用各类弹窗Toast、Android、iOS确定弹窗、弹窗、选择弹窗、底部弹窗等) |
文件名 | 功能描述 |
---|---|
refresh_widget.dart | 刷新列表组件(包含上拉加载、下拉刷新、回至顶部、页面数据状态视图(加载、空数据、列表、瀑布流)等功能) |
base_widget.dart | 基础组件基类(统一多状态管理,无网络自动切换该状态布局) |
com_album.dart | 相册组件(图片九宫格仿微信朋友圈显示) |
com_arrow.dart | 方向箭头组件(支持上下左右箭头,常用于列表项导航) |
com_avatar.dart | 头像组件(圆形/方形、网络/本地/文字头像) |
com_button.dart | 按钮组件(主按钮、线性按钮、禁用状态、渐变色、自定义样式) |
com_checkbox.dart | 复选框组件(支持单选/多选、自定义图标) |
com_checkbox_list_title.dart | 列表复选框组件(ListTitle形式下的复现框) |
com_empty.dart | 空状态组件(数据为空时展示占位图或提示文字) |
com_gallery.dart | 图片画廊组件(图片查看预览等操作) |
com_image.dart | 增强图片组件(占位图、加载失败兜底、缓存策略) |
com_list_group.dart | 分组列表组件(下划线分隔的列表项布局,自定义下划线) |
com_loading.dart | 加载组件(全局Loading,可自定义) |
com_popup_menu.dart | 弹出菜单组件(自定义菜单项、位置调整) |
com_rating.dart | 评分组件(星级评分、支持半星、自定义图标) |
com_tag.dart | 标签组件(多颜色/尺寸、圆角样式) |
com_title_bar.dart | 标题栏组件(左中右布局、标题居中、常用于底部弹窗标题) |
com_divider.dart | 下划线组件(CustomPainter实现的Divider,支持负数) |
class DemoLogic extends PagingController {\\n @override\\n Future<PagingResponse> loadData() async {\\n dynamic result = {\\"current\\": 1, \\"total\\": 3, \\"records\\": []};\\n await Future.delayed(2000.milliseconds, () {\\n for (var i = 0; i < 20; ++i) {\\n result[\\"records\\"]?.add(i);\\n }\\n });\\n\\n return PagingResponse.fromMapJson(result);\\n }\\n}\\n\\nclass DemoPage extends StatelessWidget {\\n DemoPage({Key? key}) : super(key: key);\\n\\n final logic = Get.find<DemoLogic>();\\n\\n @override\\n Widget build(BuildContext context) {\\n return GetBuilder<DemoLogic>(\\n builder: (controller) {\\n return Scaffold(\\n body: RefreshWidget(\\n controller: logic,\\n slivers: [\\n RefreshListWidget(\\n itemBuilder: (item, index) => _buildItem(index),\\n controller: logic,\\n showList: false),\\n ],\\n ));\\n },\\n id: logic.pagingState.refreshId,\\n );\\n }\\n\\n Widget _buildItem(index) {\\n if (index % 3 == 0) {\\n return Container(\\n color: Colors.deepOrange,\\n width: double.infinity,\\n height: 300.h,\\n );\\n }\\n return Container(\\n color: Colors.green,\\n width: double.infinity,\\n height: 200.h,\\n );\\n }\\n}\\n
\\n查看完整示例:
\\ngit clone https://github.com/Er-Dong-Chen/flutter-common.git\\ncd flutter-common/example\\nflutter run\\n
\\n我们欢迎以下类型的贡献:
\\n🐛 Bug 报告
\\n💡 功能建议
\\n📚 文档改进
\\n🎨 设计资源
\\n💻 代码提交
\\n欢迎提交 PR 或 Issue!贡献前请阅读:
\\n\\nMIT License - 详情见 LICENSE 文件
\\n","description":"Flutter Chen Common 🌟 简介\\n\\n本库为Flutter应用开发提供一站式解决方案,包含:\\n\\n可定制的主题系统\\n完整的国际化支持\\n企业级网络请求封装\\n企业级日志体系封装\\nN+高质量常用组件\\n常用开发工具及扩展集合\\n刷新列表一整套解决方案\\n开箱即用的通用各类弹窗\\n全局统一各状态布局\\n特性\\n🎨 主题系统:通过 ThemeExtension 全局配置颜色/圆角/间距等样式\\n🌍 国际化支持:内置中英文,支持自定义文本和动态语言切换\\n⚡ 优先级覆盖:支持全局配置 + 组件级参数覆盖\\n📱 自适应设计:完美适配 iOS/Material…","guid":"https://juejin.cn/post/7491589110249439269","author":"耳東陈","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-11T03:06:05.006Z","media":null,"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 图片组件全面解析:从基础加载到高级应用","url":"https://juejin.cn/post/7491589110248816677","content":"在移动应用开发中,图片是构建丰富用户界面不可或缺的元素。Flutter 作为一个强大的跨平台开发框架,提供了多种图片组件和加载方式,帮助开发者轻松地在应用中展示图片。本文将详细介绍 Flutter 中图片组件的使用,涵盖本地图片、网络图片的加载,以及图片的缓存、占位符处理、裁剪和缩放等高级应用,同时结合代码示例进行深入讲解。
\\n在 Flutter 中加载本地图片,首先需要在 pubspec.yaml
文件中声明图片资源。假设项目中有一张名为 example.png
的图片存放在 assets/images
目录下,pubspec.yaml
配置如下:
flutter:\\n assets:\\n - assets/images/example.png\\n
\\n下面是加载本地图片的代码示例:
\\nimport \'package:flutter/material.dart\';\\n\\nvoid main() {\\n runApp(MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n home: Scaffold(\\n appBar: AppBar(\\n title: Text(\'本地图片加载示例\'),\\n ),\\n body: Center(\\n child: Image.asset(\'assets/images/example.png\'),\\n ),\\n ),\\n );\\n }\\n}\\n
\\n在上述代码中,Image.asset
方法用于加载本地图片。它接受一个字符串参数,即图片在项目中的路径。
加载网络图片使用 Image.network
方法。以下是一个简单的示例:
import \'package:flutter/material.dart\';\\n\\nvoid main() {\\n runApp(MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n home: Scaffold(\\n appBar: AppBar(\\n title: Text(\'网络图片加载示例\'),\\n ),\\n body: Center(\\n child: Image.network(\\n \'https://picsum.photos/200\',\\n ),\\n ),\\n ),\\n );\\n }\\n}\\n
\\nImage.network
方法接受一个字符串参数,即图片的网络 URL。
在实际应用中,为了提高图片加载性能和用户体验,通常需要对图片进行缓存。Flutter 提供了 cached_network_image
插件来实现网络图片的缓存。首先,在 pubspec.yaml
中添加依赖:
dependencies:\\n cached_network_image: ^3.2.3\\n
\\n然后使用以下代码加载并缓存网络图片:
\\nimport \'package:flutter/material.dart\';\\nimport \'package:cached_network_image/cached_network_image.dart\';\\n\\nvoid main() {\\n runApp(MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n home: Scaffold(\\n appBar: AppBar(\\n title: Text(\'图片缓存示例\'),\\n ),\\n body: Center(\\n child: CachedNetworkImage(\\n imageUrl: \'https://picsum.photos/200\',\\n ),\\n ),\\n ),\\n );\\n }\\n}\\n
\\nCachedNetworkImage
会自动缓存图片,下次加载相同 URL 的图片时会直接从缓存中读取,提高加载速度。
在图片加载过程中,为了避免界面出现空白,通常会显示一个占位符。CachedNetworkImage
提供了 placeholder
和 errorWidget
参数来处理图片加载中的占位和错误情况。
import \'package:flutter/material.dart\';\\nimport \'package:cached_network_image/cached_network_image.dart\';\\n\\nvoid main() {\\n runApp(MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n home: Scaffold(\\n appBar: AppBar(\\n title: Text(\'图片占位符示例\'),\\n ),\\n body: Center(\\n child: CachedNetworkImage(\\n imageUrl: \'https://picsum.photos/200\',\\n placeholder: (context, url) => CircularProgressIndicator(),\\n errorWidget: (context, url, error) => Icon(Icons.error),\\n ),\\n ),\\n ),\\n );\\n }\\n}\\n
\\n在上述代码中,placeholder
参数指定了图片加载过程中显示的占位符,这里使用了 CircularProgressIndicator
表示加载中;errorWidget
参数指定了图片加载失败时显示的组件,这里使用了 Icon(Icons.error)
表示错误。
Flutter 提供了 ClipOval
、ClipRRect
等裁剪组件来对图片进行裁剪。以下是一个使用 ClipRRect
对图片进行圆角裁剪的示例:
import \'package:flutter/material.dart\';\\n\\nvoid main() {\\n runApp(MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n home: Scaffold(\\n appBar: AppBar(\\n title: Text(\'图片裁剪示例\'),\\n ),\\n body: Center(\\n child: ClipRRect(\\n borderRadius: BorderRadius.circular(20),\\n child: Image.network(\\n \'https://picsum.photos/200\',\\n ),\\n ),\\n ),\\n ),\\n );\\n }\\n}\\n
\\nClipRRect
组件可以将图片裁剪成圆角矩形,通过 borderRadius
参数指定圆角的半径。
Image
组件的 fit
参数可以控制图片的缩放方式。常见的缩放方式有 BoxFit.contain
、BoxFit.cover
、BoxFit.fill
等。
import \'package:flutter/material.dart\';\\n\\nvoid main() {\\n runApp(MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n home: Scaffold(\\n appBar: AppBar(\\n title: Text(\'图片缩放示例\'),\\n ),\\n body: Column(\\n mainAxisAlignment: MainAxisAlignment.spaceEvenly,\\n children: [\\n Image.network(\\n \'https://picsum.photos/200\',\\n fit: BoxFit.contain,\\n width: 200,\\n height: 200,\\n ),\\n Image.network(\\n \'https://picsum.photos/200\',\\n fit: BoxFit.cover,\\n width: 200,\\n height: 200,\\n ),\\n Image.network(\\n \'https://picsum.photos/200\',\\n fit: BoxFit.fill,\\n width: 200,\\n height: 200,\\n ),\\n ],\\n ),\\n ),\\n );\\n }\\n}\\n
\\nBoxFit.contain
:图片会被完整显示在指定的区域内,可能会留有空白。BoxFit.cover
:图片会覆盖整个指定区域,可能会有部分被裁剪。BoxFit.fill
:图片会填充整个指定区域,可能会导致图片变形。在 Flutter 中可以使用 ColorFiltered
组件为图片添加滤镜效果。以下是一个添加灰度滤镜的示例:
import \'package:flutter/material.dart\';\\n\\nvoid main() {\\n runApp(MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n home: Scaffold(\\n appBar: AppBar(\\n title: Text(\'图片滤镜示例\'),\\n ),\\n body: Center(\\n child: ColorFiltered(\\n colorFilter: ColorFilter.matrix([\\n 0.2126, 0.7152, 0.0722, 0, 0,\\n 0.2126, 0.7152, 0.0722, 0, 0,\\n 0.2126, 0.7152, 0.0722, 0, 0,\\n 0, 0, 0, 1, 0,\\n ]),\\n child: Image.network(\\n \'https://picsum.photos/200\',\\n ),\\n ),\\n ),\\n ),\\n );\\n }\\n}\\n
\\nColorFilter.matrix
接受一个 4x5 的矩阵,通过调整矩阵的值可以实现不同的滤镜效果。
可以使用 AnimatedOpacity
等动画组件为图片添加动画效果。以下是一个图片淡入动画的示例:
import \'package:flutter/material.dart\';\\n\\nvoid main() {\\n runApp(MyApp());\\n}\\n\\nclass MyApp extends StatefulWidget {\\n @override\\n _MyAppState createState() => _MyAppState();\\n}\\n\\nclass _MyAppState extends State<MyApp> {\\n bool _visible = false;\\n\\n @override\\n void initState() {\\n super.initState();\\n Future.delayed(Duration(seconds: 1), () {\\n setState(() {\\n _visible = true;\\n });\\n });\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n home: Scaffold(\\n appBar: AppBar(\\n title: Text(\'图片动画示例\'),\\n ),\\n body: Center(\\n child: AnimatedOpacity(\\n opacity: _visible ? 1.0 : 0.0,\\n duration: Duration(seconds: 1),\\n child: Image.network(\\n \'https://picsum.photos/200\',\\n ),\\n ),\\n ),\\n ),\\n );\\n }\\n}\\n
\\n在上述代码中,使用 AnimatedOpacity
组件实现了图片的淡入动画效果。通过控制 opacity
属性的值,在一定时间内改变图片的透明度。
Flutter 提供了丰富的图片组件和功能,满足了开发者在不同场景下对图片处理的需求。从基础的本地和网络图片加载,到高级的图片缓存、裁剪、缩放、滤镜和动画处理,开发者可以利用这些功能构建出更加美观、流畅的用户界面。在实际开发中,根据具体需求灵活运用这些技巧,能够提升应用的质量和用户体验。希望本文对你在 Flutter 中使用图片组件有所帮助。
","description":"引言 在移动应用开发中,图片是构建丰富用户界面不可或缺的元素。Flutter 作为一个强大的跨平台开发框架,提供了多种图片组件和加载方式,帮助开发者轻松地在应用中展示图片。本文将详细介绍 Flutter 中图片组件的使用,涵盖本地图片、网络图片的加载,以及图片的缓存、占位符处理、裁剪和缩放等高级应用,同时结合代码示例进行深入讲解。\\n\\n1. 基础图片加载\\n1.1 本地图片加载\\n\\n在 Flutter 中加载本地图片,首先需要在 pubspec.yaml 文件中声明图片资源。假设项目中有一张名为 example.png 的图片存放在 assets/images 目…","guid":"https://juejin.cn/post/7491589110248816677","author":"顾林海","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-11T01:47:20.266Z","media":null,"categories":["Android","前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter window和Mac中webview2使用Cef替代","url":"https://juejin.cn/post/7491491051464933410","content":"\\n\\n目前主流是使用
\\nwebview2
,并且以实例的方式启动,无法作为widget
组件在Flutter应用中内嵌,只能以新窗口的形式打开,体验不是很友好 也不太符合大多数软件开发的需求要求。
这里使用CEF(Chromium Embedded Framework)
来替代webview2
的方案,CEF是基于 Chrome Chromium 的一个开源内核
,Google官方并未提供flutter版本的CEF,需要自己使用C++编写cpp去实现接口能力 或者使用第三方作者封装的依赖库。
原生cpp + Flutter Plugin
的形式接入接入相对复杂、原生配置和 编写3端的代码量非常大(Windows端 C++;Mac端 Objective-C或swift;Flutter)
本文仅分享webview_cef
的使用案例 不讨论C++形式。 这个库已经实现了大部分常用接口能力,当然 您C++能力过硬并且比较闲,也可以自己编写 C++ cpp的方式接入CEF。
\\n\\n1、在
\\npubspec.yaml
中引入webview_cef
\\n\\n2、 在
\\nwindows\\\\runner\\\\main.cpp
中初始化initCEFProcesses()
\\n\\n3、
\\n::MSG msg;
中加入handleWndProcForCEF()
启用CEF
键盘输入推送
\\n\\n4、
\\nflutter run
即可,首次编译会下载cef资源包
,文件较大,会比较慢,等待即可
嵌入示例运行效果\\n
在 Flutter 的世界里,我们经常需要以网格、列表或者 Wrap 的形式呈现一组数据。遇到类似“标签”、“分类选项”或者“城市列表”时,如果再需要添加一些操作,比如删除、选择指示器,往往就需要自己费心手写逻辑。
\\n今天给大家带来一款可以“自定义每个项目的操作按钮位置与形态”的网格组件 —— CustomizableItemGrid。无论是要加个“删除”小圆圈、还是加个“打勾”的选择框,这个组件都能轻松帮你处理,还能快速切换到动画按钮、顶部/底部/左右角等多个位置。超灵活!
\\n\\n下面,就让我们一起来看看它的用法和实现原理吧。
List<String>
就能快速展示每个元素。我们先从 CustomWidgetDemo
入手,这是一个示例页面,用来展示如何使用 CustomizableItemGrid
这个组件。以下是它的主要功能:
areas
)并支持增删操作。核心代码如下(已省略 import 相关):
\\nimport \'package:flutter/material.dart\';\\n\\n/// 定义操作小部件的可能位置。\\nenum ActionWidgetPosition {\\n /// 操作小部件位于项目左上角。\\n topLeft,\\n\\n /// 操作小部件位于项目右上角。\\n topRight,\\n\\n /// 操作小部件位于项目左下角。\\n bottomLeft,\\n\\n /// 操作小部件位于项目右下角。\\n bottomRight,\\n}\\n\\n/// 一个可重用的组件,用于显示带有可自定义操作小部件的响应式网格。\\n/// 每个项目可以在四个角之一放置一个自定义小部件。\\nclass CustomizableItemGrid extends StatefulWidget {\\n /// 要显示的项目列表。\\n final List<String> items;\\n\\n /// 当项目被删除时调用的回调函数。\\n final Function(List<String> updatedItems)? onItemsChanged;\\n\\n /// 当项目被选中时调用的回调函数。\\n final Function(String item, bool isSelected)? onItemSelected;\\n\\n /// 项目边框和文本的颜色。\\n final Color itemColor;\\n\\n /// 项目的文本样式。\\n final TextStyle? textStyle;\\n\\n /// 每个项目周围的内边距。\\n final EdgeInsetsGeometry itemPadding;\\n\\n /// 项目之间的水平间距。\\n final double horizontalSpacing;\\n\\n /// 行之间的垂直间距。\\n final double verticalSpacing;\\n\\n /// 项目的圆角半径。\\n final double borderRadius;\\n\\n /// 项目边框的宽度。\\n final double borderWidth;\\n\\n /// 操作小部件的位置。\\n final ActionWidgetPosition actionWidgetPosition;\\n\\n /// 用于为每个项目创建操作小部件的构建函数。\\n ///\\n /// 该函数接收项目文本、删除回调、选中状态和选中状态变化回调。\\n /// 这允许创建自定义小部件,用作删除按钮、\\n /// 选择指示器或任何其他交互元素。\\n final Widget Function(\\n String item,\\n VoidCallback onDelete,\\n bool isSelected,\\n ValueChanged<bool> onSelectionChanged\\n ) actionWidgetBuilder;\\n\\n /// 操作小部件相对于角落的偏移量。\\n final Offset actionWidgetOffset;\\n\\n /// 项目在 Wrap 组件中的对齐方式。\\n final WrapAlignment wrapAlignment;\\n\\n /// 行在 Wrap 组件中的对齐方式。\\n final WrapAlignment runAlignment;\\n\\n /// 项目在 Wrap 组件中的交叉轴对齐方式。\\n final WrapCrossAlignment crossAxisAlignment;\\n\\n /// 是否允许项目被选中。\\n final bool selectable;\\n\\n /// 最初选中的项目。\\n final List<String> initialSelectedItems;\\n\\n /// 创建一个新的 [CustomizableItemGrid]。\\n const CustomizableItemGrid({\\n Key? key,\\n required this.items,\\n this.onItemsChanged,\\n this.onItemSelected,\\n this.itemColor = const Color(0xFF00CEC9),\\n this.textStyle,\\n this.itemPadding = const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0),\\n this.horizontalSpacing = 12.0,\\n this.verticalSpacing = 16.0,\\n this.borderRadius = 16.0,\\n this.borderWidth = 2.0,\\n this.actionWidgetPosition = ActionWidgetPosition.topLeft,\\n required this.actionWidgetBuilder,\\n this.actionWidgetOffset = const Offset(-10, -10),\\n this.wrapAlignment = WrapAlignment.start,\\n this.runAlignment = WrapAlignment.start,\\n this.crossAxisAlignment = WrapCrossAlignment.start,\\n this.selectable = false,\\n this.initialSelectedItems = const [],\\n }) : super(key: key);\\n\\n @override\\n State<CustomizableItemGrid> createState() => _CustomizableItemGridState();\\n}\\n\\nclass _CustomizableItemGridState extends State<CustomizableItemGrid> {\\n late List<String> _items;\\n late Set<String> _selectedItems;\\n\\n @override\\n void initState() {\\n super.initState();\\n _items = List.from(widget.items);\\n _selectedItems = Set.from(widget.initialSelectedItems);\\n }\\n\\n @override\\n void didUpdateWidget(CustomizableItemGrid oldWidget) {\\n super.didUpdateWidget(oldWidget);\\n if (oldWidget.items != widget.items) {\\n _items = List.from(widget.items);\\n }\\n if (oldWidget.initialSelectedItems != widget.initialSelectedItems) {\\n _selectedItems = Set.from(widget.initialSelectedItems);\\n }\\n }\\n\\n void _deleteItem(String item) {\\n setState(() {\\n _items.remove(item);\\n _selectedItems.remove(item);\\n });\\n\\n if (widget.onItemsChanged != null) {\\n widget.onItemsChanged!(_items);\\n }\\n }\\n\\n void _toggleItemSelection(String item, bool isSelected) {\\n setState(() {\\n if (isSelected) {\\n _selectedItems.add(item);\\n } else {\\n _selectedItems.remove(item);\\n }\\n });\\n\\n if (widget.onItemSelected != null) {\\n widget.onItemSelected!(item, isSelected);\\n }\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Wrap(\\n spacing: widget.horizontalSpacing,\\n runSpacing: widget.verticalSpacing,\\n alignment: widget.wrapAlignment,\\n runAlignment: widget.runAlignment,\\n crossAxisAlignment: widget.crossAxisAlignment,\\n children: _items.map((item) => _buildItem(item)).toList(),\\n );\\n }\\n\\n Widget _buildItem(String item) {\\n final bool isSelected = _selectedItems.contains(item);\\n\\n return CustomizableItem(\\n text: item,\\n onDelete: () => _deleteItem(item),\\n isSelected: isSelected,\\n onSelectionChanged: (value) => _toggleItemSelection(item, value),\\n itemColor: widget.itemColor,\\n textStyle: widget.textStyle,\\n padding: widget.itemPadding,\\n borderRadius: widget.borderRadius,\\n borderWidth: widget.borderWidth,\\n actionWidgetPosition: widget.actionWidgetPosition,\\n actionWidgetBuilder: widget.actionWidgetBuilder,\\n actionWidgetOffset: widget.actionWidgetOffset,\\n selectable: widget.selectable,\\n );\\n }\\n}\\n\\n/// 一个带有可自定义操作小部件的单个项目。\\nclass CustomizableItem extends StatelessWidget {\\n /// 项目中要显示的文本。\\n final String text;\\n\\n /// 删除操作被触发时调用的回调函数。\\n final VoidCallback onDelete;\\n\\n /// 项目当前是否被选中。\\n final bool isSelected;\\n\\n /// 选中状态变化时调用的回调函数。\\n final ValueChanged<bool> onSelectionChanged;\\n\\n /// 项目边框和文本的颜色。\\n final Color itemColor;\\n\\n /// 项目的文本样式。\\n final TextStyle? textStyle;\\n\\n /// 项目周围的内边距。\\n final EdgeInsetsGeometry padding;\\n\\n /// 项目的圆角半径。\\n final double borderRadius;\\n\\n /// 项目边框的宽度。\\n final double borderWidth;\\n\\n /// 操作小部件的位置。\\n final ActionWidgetPosition actionWidgetPosition;\\n\\n /// 用于创建操作小部件的构建函数。\\n final Widget Function(\\n String item,\\n VoidCallback onDelete,\\n bool isSelected,\\n ValueChanged<bool> onSelectionChanged\\n ) actionWidgetBuilder;\\n\\n /// 操作小部件相对于角落的偏移量。\\n final Offset actionWidgetOffset;\\n\\n /// 项目是否可选。\\n final bool selectable;\\n\\n /// 创建一个新的 [CustomizableItem]。\\n const CustomizableItem({\\n Key? key,\\n required this.text,\\n required this.onDelete,\\n required this.isSelected,\\n required this.onSelectionChanged,\\n this.itemColor = const Color(0xFF00CEC9),\\n this.textStyle,\\n this.padding = const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0),\\n this.borderRadius = 16.0,\\n this.borderWidth = 2.0,\\n this.actionWidgetPosition = ActionWidgetPosition.topLeft,\\n required this.actionWidgetBuilder,\\n this.actionWidgetOffset = const Offset(-10, -10),\\n this.selectable = false,\\n }) : super(key: key);\\n\\n @override\\n Widget build(BuildContext context) {\\n return Stack(\\n clipBehavior: Clip.none,\\n children: [\\n // 主容器,包含文本\\n GestureDetector(\\n onTap: selectable ? () => onSelectionChanged(!isSelected) : null,\\n child: Container(\\n padding: padding,\\n decoration: BoxDecoration(\\n border: Border.all(color: itemColor, width: borderWidth),\\n borderRadius: BorderRadius.circular(borderRadius),\\n color: isSelected ? itemColor.withOpacity(0.1) : Colors.transparent,\\n ),\\n child: Text(\\n text,\\n style: textStyle ?? TextStyle(\\n color: itemColor,\\n fontSize: 20,\\n fontWeight: FontWeight.w500,\\n ),\\n ),\\n ),\\n ),\\n\\n // 动态定位的自定义操作小部件\\n Positioned(\\n // 根据 actionWidgetPosition 动态定位\\n top: _getTopPosition(),\\n bottom: _getBottomPosition(),\\n left: _getLeftPosition(),\\n right: _getRightPosition(),\\n child: actionWidgetBuilder(\\n text,\\n onDelete,\\n isSelected,\\n onSelectionChanged\\n ),\\n ),\\n ],\\n );\\n }\\n\\n // 辅助方法,用于计算位置值\\n double? _getTopPosition() {\\n switch (actionWidgetPosition) {\\n case ActionWidgetPosition.topLeft:\\n case ActionWidgetPosition.topRight:\\n return actionWidgetOffset.dy;\\n case ActionWidgetPosition.bottomLeft:\\n case ActionWidgetPosition.bottomRight:\\n return null;\\n }\\n }\\n\\n double? _getBottomPosition() {\\n switch (actionWidgetPosition) {\\n case ActionWidgetPosition.topLeft:\\n case ActionWidgetPosition.topRight:\\n return null;\\n case ActionWidgetPosition.bottomLeft:\\n case ActionWidgetPosition.bottomRight:\\n return actionWidgetOffset.dy;\\n }\\n }\\n\\n double? _getLeftPosition() {\\n switch (actionWidgetPosition) {\\n case ActionWidgetPosition.topLeft:\\n case ActionWidgetPosition.bottomLeft:\\n return actionWidgetOffset.dx;\\n case ActionWidgetPosition.topRight:\\n case ActionWidgetPosition.bottomRight:\\n return null;\\n }\\n }\\n\\n double? _getRightPosition() {\\n switch (actionWidgetPosition) {\\n case ActionWidgetPosition.topLeft:\\n case ActionWidgetPosition.bottomLeft:\\n return null;\\n case ActionWidgetPosition.topRight:\\n case ActionWidgetPosition.bottomRight:\\n return actionWidgetOffset.dx;\\n }\\n }\\n}\\n
\\nclass CustomWidgetDemo extends StatefulWidget {\\n const CustomWidgetDemo({Key? key}) : super(key: key);\\n\\n @override\\n State<CustomWidgetDemo> createState() => _CustomWidgetDemoState();\\n}\\n\\nclass _CustomWidgetDemoState extends State<CustomWidgetDemo> {\\n // 初始的一组区域名称\\n List<String> areas = [\'迴仔區\', \'青洲區\', \'台山區\', \'花地瑪堂區\', \'望德堂區\', \'大堂區\', \'風順堂區\'];\\n\\n // 当前选择的操作小部件类型\\n String currentWidgetType = \'Simple Delete\';\\n\\n // 当前操作小部件的位置\\n ActionWidgetPosition currentPosition = ActionWidgetPosition.topLeft;\\n\\n // 是否开启选择模式\\n bool selectionMode = false;\\n\\n // 已选中的项目\\n List<String> selectedItems = [];\\n\\n // 更新列表回调\\n void _handleItemsChanged(List<String> updatedItems) {\\n setState(() {\\n areas = updatedItems;\\n });\\n }\\n\\n // 处理项目选中回调\\n void _handleItemSelected(String item, bool isSelected) {\\n setState(() {\\n if (isSelected) {\\n selectedItems.add(item);\\n } else {\\n selectedItems.remove(item);\\n }\\n });\\n }\\n\\n // 核心界面\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: const Text(\'Custom Action Widgets\'),\\n backgroundColor: Colors.white,\\n foregroundColor: Colors.black,\\n elevation: 0,\\n ),\\n body: Padding(\\n padding: const EdgeInsets.all(16.0),\\n child: Column(\\n crossAxisAlignment: CrossAxisAlignment.start,\\n children: [\\n // 显示当前选中的区域\\n const Text(\\n \'Selected Areas\',\\n style: TextStyle(\\n fontSize: 18,\\n fontWeight: FontWeight.bold,\\n ),\\n ),\\n const SizedBox(height: 16),\\n\\n // 使用我们自定义的 CustomizableItemGrid\\n Expanded(\\n child: CustomizableItemGrid(\\n items: areas,\\n onItemsChanged: _handleItemsChanged,\\n onItemSelected: _handleItemSelected,\\n actionWidgetPosition: currentPosition,\\n itemColor: const Color(0xFF00CEC9),\\n horizontalSpacing: 12.0,\\n verticalSpacing: 16.0,\\n wrapAlignment: WrapAlignment.start,\\n selectable: selectionMode,\\n initialSelectedItems: selectedItems,\\n actionWidgetBuilder: _buildActionWidget,\\n ),\\n ),\\n\\n const SizedBox(height: 16),\\n\\n // 切换操作小部件类型\\n Text(\\n \'Action Widget Type:\',\\n style: TextStyle(\\n fontSize: 16,\\n fontWeight: FontWeight.bold,\\n ),\\n ),\\n const SizedBox(height: 8),\\n Wrap(\\n spacing: 8,\\n runSpacing: 8,\\n children: [\\n _buildTypeButton(\'Simple Delete\'),\\n _buildTypeButton(\'Animated Delete\'),\\n _buildTypeButton(\'Icon Button\'),\\n _buildTypeButton(\'Selection Indicator\'),\\n ],\\n ),\\n\\n const SizedBox(height: 16),\\n\\n // 切换操作按钮位置\\n Text(\\n \'Widget Position:\',\\n style: TextStyle(\\n fontSize: 16,\\n fontWeight: FontWeight.bold,\\n ),\\n ),\\n const SizedBox(height: 8),\\n Wrap(\\n spacing: 8,\\n runSpacing: 8,\\n children: [\\n _buildPositionButton(\'Top Left\', ActionWidgetPosition.topLeft),\\n _buildPositionButton(\'Top Right\', ActionWidgetPosition.topRight),\\n _buildPositionButton(\'Bottom Left\', ActionWidgetPosition.bottomLeft),\\n _buildPositionButton(\'Bottom Right\', ActionWidgetPosition.bottomRight),\\n ],\\n ),\\n\\n const SizedBox(height: 16),\\n\\n // 切换选择模式\\n Row(\\n children: [\\n Text(\\n \'Selection Mode:\',\\n style: TextStyle(\\n fontSize: 16,\\n fontWeight: FontWeight.bold,\\n ),\\n ),\\n const SizedBox(width: 8),\\n Switch(\\n value: selectionMode,\\n onChanged: (value) {\\n setState(() {\\n selectionMode = value;\\n if (!value) {\\n selectedItems.clear();\\n }\\n });\\n },\\n activeColor: const Color(0xFF00CEC9),\\n ),\\n ],\\n ),\\n\\n // 新增项目\\n const SizedBox(height: 16),\\n ElevatedButton(\\n onPressed: () {\\n setState(() {\\n areas.add(\'新區域 ${areas.length + 1}\');\\n });\\n },\\n style: ElevatedButton.styleFrom(\\n backgroundColor: const Color(0xFF00CEC9),\\n ),\\n child: const Text(\'Add New Area\'),\\n ),\\n ],\\n ),\\n ),\\n );\\n }\\n\\n // 构建不同类型的小部件\\n Widget _buildActionWidget(\\n String item,\\n VoidCallback onDelete,\\n bool isSelected,\\n ValueChanged<bool> onSelectionChanged,\\n ) {\\n switch (currentWidgetType) {\\n case \'Simple Delete\':\\n return GestureDetector(\\n onTap: onDelete,\\n child: Container(\\n width: 30,\\n height: 30,\\n decoration: BoxDecoration(\\n color: const Color(0xFF00CEC9),\\n shape: BoxShape.circle,\\n ),\\n child: Icon(\\n Icons.close,\\n color: Colors.white,\\n size: 20,\\n ),\\n ),\\n );\\n\\n case \'Animated Delete\':\\n return GestureDetector(\\n onTap: onDelete,\\n child: TweenAnimationBuilder<double>(\\n tween: Tween<double>(begin: 0, end: 1),\\n duration: const Duration(milliseconds: 300),\\n builder: (context, value, child) {\\n return Transform.scale(\\n scale: 0.8 + (value * 0.2),\\n child: Container(\\n width: 30,\\n height: 30,\\n decoration: BoxDecoration(\\n color: const Color(0xFF00CEC9),\\n shape: BoxShape.circle,\\n boxShadow: [\\n BoxShadow(\\n color: Colors.black.withOpacity(0.2 * value),\\n blurRadius: 4 * value,\\n spreadRadius: 2 * value,\\n ),\\n ],\\n ),\\n child: Icon(\\n Icons.close,\\n color: Colors.white,\\n size: 20,\\n ),\\n ),\\n );\\n },\\n ),\\n );\\n\\n case \'Icon Button\':\\n return Material(\\n color: Colors.transparent,\\n child: InkWell(\\n onTap: onDelete,\\n borderRadius: BorderRadius.circular(20),\\n child: Container(\\n width: 36,\\n height: 36,\\n decoration: BoxDecoration(\\n color: const Color(0xFF00CEC9),\\n borderRadius: BorderRadius.circular(8),\\n ),\\n child: Icon(\\n Icons.delete_outline,\\n color: Colors.white,\\n size: 20,\\n ),\\n ),\\n ),\\n );\\n\\n case \'Selection Indicator\':\\n return GestureDetector(\\n onTap: () => onSelectionChanged(!isSelected),\\n child: Container(\\n width: 30,\\n height: 30,\\n decoration: BoxDecoration(\\n color: isSelected ? const Color(0xFF00CEC9) : Colors.white,\\n shape: BoxShape.circle,\\n border: Border.all(\\n color: const Color(0xFF00CEC9),\\n width: 2,\\n ),\\n ),\\n child: isSelected\\n ? Icon(\\n Icons.check,\\n color: Colors.white,\\n size: 20,\\n )\\n : null,\\n ),\\n );\\n\\n default:\\n return GestureDetector(\\n onTap: onDelete,\\n child: Container(\\n width: 30,\\n height: 30,\\n decoration: BoxDecoration(\\n color: const Color(0xFF00CEC9),\\n shape: BoxShape.circle,\\n ),\\n child: Icon(\\n Icons.close,\\n color: Colors.white,\\n size: 20,\\n ),\\n ),\\n );\\n }\\n }\\n\\n // 构建“操作类型”按钮\\n Widget _buildTypeButton(String type) {\\n return ElevatedButton(\\n onPressed: () {\\n setState(() {\\n currentWidgetType = type;\\n });\\n },\\n style: ElevatedButton.styleFrom(\\n backgroundColor: currentWidgetType == type ? Color(0xFF00CEC9) : null,\\n ),\\n child: Text(type),\\n );\\n }\\n\\n // 构建“位置”按钮\\n Widget _buildPositionButton(String label, ActionWidgetPosition position) {\\n return ElevatedButton(\\n onPressed: () {\\n setState(() {\\n currentPosition = position;\\n });\\n },\\n style: ElevatedButton.styleFrom(\\n backgroundColor: currentPosition == position ? Color(0xFF00CEC9) : null,\\n ),\\n child: Text(label),\\n );\\n }\\n}\\n
\\n将上述代码(CustomWidgetDemo
+ CustomizableItemGrid
)拷贝到你的项目中,保证引用正常。
在 main.dart
中,将 home
替换为 CustomWidgetDemo()
,例如:
void main() {\\n runApp(\\n MaterialApp(\\n home: CustomWidgetDemo(),\\n ),\\n );\\n}\\n
\\n运行项目,就能看到演示界面,切换各种操作类型、改变位置、开启选择模式,实时看到效果。
\\n下面重点介绍一下最核心的 CustomizableItemGrid
。它是一个 StatefulWidget,内部使用 Wrap
布局来展示一系列可点击、可删除、可选的项目标签,并通过“自定义操作小部件”来实现各种交互。
以下是其关键属性与用途:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n参数 | 类型 | 解释 |
---|---|---|
items | List<String> | 必传。 要显示的一组字符串。 |
onItemsChanged | Function(List<String> updatedItems)? | 传入一个回调函数,当内部有项目被删除时,回调会携带最新的列表数据(让父 widget 及时更新 state )。 |
onItemSelected | Function(String item, bool isSelected)? | 当项目被选中或取消选中时触发的回调,携带项目本身以及选中状态。 |
itemColor | Color | 每个项目的边框和文字颜色。默认为 Color(0xFF00CEC9) 。 |
textStyle | TextStyle? | 设置每个项目文字样式,如果为空则使用默认样式。 |
itemPadding | EdgeInsetsGeometry | 项目内部的 padding,默认为 EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0) 。 |
horizontalSpacing | double | item 与 item 间的水平间距,用于 Wrap 布局。默认为 12.0 。 |
verticalSpacing | double | item 与 item 间的垂直间距,用于 Wrap 布局。默认为 16.0 。 |
borderRadius | double | 项目外边框的圆角半径,默认为 16.0 。 |
borderWidth | double | 项目的边框宽度,默认为 2.0 。 |
actionWidgetPosition | ActionWidgetPosition | 操作小部件在项目四个角的位置,可选 topLeft 、topRight 、bottomLeft 、bottomRight 。 |
actionWidgetBuilder | Widget Function(String item, VoidCallback onDelete, bool isSelected, ValueChanged<bool> onSelectionChanged) | 必传。 用来为每个项目构建自定义操作小部件的函数,比如删除按钮、选择指示器等。 |
actionWidgetOffset | Offset | 操作小部件相对于所属角落的偏移,默认为 Offset(-10, -10) ,即让操作按钮稍微溢出一点边界,看起来像挂在角落上。 |
wrapAlignment | WrapAlignment | 控制主轴对齐方式,默认为 WrapAlignment.start 。 |
runAlignment | WrapAlignment | 控制换行对齐方式,默认为 WrapAlignment.start 。 |
crossAxisAlignment | WrapCrossAlignment | 控制交叉轴对齐方式,默认为 WrapCrossAlignment.start 。 |
selectable | bool | 是否允许选中。默认为 false 。 |
initialSelectedItems | List<String> | 初始选中的项目列表,默认为空列表。 |
简而言之,想要使用它,你只需要:
\\nCustomizableItemGrid(\\n items: myItemsList, // 显示的一组字符串\\n onItemsChanged: (updatedList) {\\n // 当有项目被删除时,这里会回调最新列表\\n setState(() {\\n myItemsList = updatedList;\\n });\\n },\\n onItemSelected: (item, isSelected) {\\n // 当有项目被选中或取消选中时触发\\n // 你可以在这里更新外部选中状态\\n },\\n actionWidgetPosition: ActionWidgetPosition.topRight, // 设置操作按钮放在右上角\\n wrapAlignment: WrapAlignment.start, // Wrap 排列方式\\n selectable: true, // 是否允许选中\\n initialSelectedItems: [\'预先选中的内容\'], // 如果需要预选\\n actionWidgetBuilder: (\\n String item,\\n VoidCallback onDelete,\\n bool isSelected,\\n ValueChanged<bool> onSelectionChanged,\\n ) {\\n // 在这里根据需要返回各种自定义按钮\\n // 比如一个简单删除:\\n return GestureDetector(\\n onTap: onDelete,\\n child: Icon(Icons.close, color: Colors.red),\\n );\\n },\\n)\\n
\\n\\n\\n小贴士:
\\nonDelete
回调会删除当前项目并触发onItemsChanged
,如果你的业务逻辑不需要完全删除项目,只是想做别的事情,也可以不调用它,而是做其他交互效果。
这个组件的核心思路是,先封装一个 CustomizableItemGrid
,内部用 Wrap
生成每个 item
的小方块,再在每个小方块的右上角(或其他角)叠加一个操作小部件。这样就不用每次去写重复的删除 / 选择代码,大大减少模板代码量。
return Wrap(\\n spacing: widget.horizontalSpacing,\\n runSpacing: widget.verticalSpacing,\\n alignment: widget.wrapAlignment,\\n runAlignment: widget.runAlignment,\\n crossAxisAlignment: widget.crossAxisAlignment,\\n children: _items.map((item) => _buildItem(item)).toList(),\\n);\\n
\\nWrap
可以很好地在多行中自动换行,并指定行间距和列间距。map
将每个 item
转换为一个 CustomizableItem
。每个“项目”其实是一个 Stack
,里面放两层:
Stack
中是贴在左上角还是右下角。最后,通过 actionWidgetBuilder
,你可以非常灵活地自行创造不同按钮形式或动画。
itemColor
、textStyle
、itemPadding
、borderRadius
、borderWidth
等参数,让网格的样式完全符合你的 UI 设计需求。actionWidgetPosition
和 actionWidgetOffset
上做文章,轻松做到各种角落悬浮。如果你想要更炫酷的动画,除了示例中的 TweenAnimationBuilder
,完全可以用 AnimatedContainer
、ScaleTransition
或者你喜欢的动画组件进行包裹,然后在 actionWidgetBuilder
里面随意发挥。
如何更换删除逻辑?
\\n_deleteItem(item)
来从内部列表中移除项目。如果你不想真正删数据,也可以在自定义按钮的 onTap
里改成别的操作,比如标记为“已禁用”,然后自行处理 setState()
或 onItemsChanged
。能否只显示而不带删除或选中?
\\nactionWidgetBuilder
里返回一个空 SizedBox
或者返回 Container()
不做交互,这样就相当于没有按钮。onDelete
也可以不调用。选择与删除能否并存?
\\nonSelectionChanged
里处理选中状态,在 onDelete
里处理删除操作,这俩是独立的逻辑。CustomizableItemGrid
和它的示例 CustomWidgetDemo
为我们提供了一种快速且灵活的网格展示 + 操作按钮组合的解决方案。通过可配置的参数和自定义的操作小部件,你可以轻松地打造出带有删除、选择、打标、动画等多种功能的标签式UI。
如果你正好在做 “标签”、“分类选择”、“动态可增删项目列表”等功能场景,这个组件能让你的开发效率直线飙升。希望本文能给你启发,并帮你节省一部分重复造轮子的时间。
\\n感谢阅读,祝你在 Flutter 的探索之路上越走越远,代码越写越美!
\\n\\n\\nTip: 喜欢这篇文章?记得收藏并在评论区留下你的想法或改进建议哈。
\\n
任务是什么呢?是今天要早睡,明天要早起。Dart中的任务也和我们生活中的任务一样,是予以指派的特定工作(如我给自己指派今天早睡),只不过Dart是通过事件循环和任务队列管理任务执行顺序。同生活中任务的划分,Dart中也会对任务进行划分,它按照不同的规则将任务划分为同步任务、异步任务、微任务等。那Dart中的任务具体是啥样的呢?准备好,跟随脚步,我们一起去看看。
\\n学习一个新知识,我们首先得知道学习的新知识是什么。那我们怎么知道这个知识是啥呢?知识的定义给出了答案,它以一个最快最简洁的方式让我们认识新的内容。下面我们分别给出生活中、计算机中、Dart中关于任务的定义。
\\n看完了定义,任务都有哪些要点呢?
\\n答:1、任务需要消耗资源。 理由:生活中骑手完成单量这个任务需要耗费人力、车损等资源;计算机中执行任务需要依托进程执行,需要使用到分配给进程的资源;Dart中任务都由主线程执行,而线程执行需要使用进程的资源。
\\n2、任务可并行或并发执行。 理由:生活中任务并行执行如一边吃饭一边追剧,并发如吃一会看一会儿再吃一会;计算机中任务可以被多个进程、线程同时处理(并行),也可被一个进程或线程处理(并发执行);Dart中依靠主线程交错执行任务(并发),依靠事件循环和任务队列实现并发执行。
\\n3、任务具有状态。 理由:生活中如骑手有取餐中、配送中、配送结束;计算机中任务依托线程、进程执行就具有了进程的创建态、就绪态、运行态、阻塞态等;Dart中依赖主线程执行当然就具有计算机中线程的状态了。
\\n4、任务具有目标导向。理由:生活中骑手目标为送餐到客户手中;计算机中达到某种效果;Dart中也是为了完成一定的目标。
我们主要介绍按照执行方式划分的任务,将任务划分为两类,分别为同步任务、异步任务。
\\n这类任务必须按照一定的顺序执行,若前一个任务未执行完成,则后一个任务便不能执行,必须等待前一个任务完成,就像是我们生活中的排队一样,得先等待前一个人处理完了,才能到你。
\\n示例:
\\nvoid main() {\\n print(\'任务1\'); // 同步代码,最先开始执行,意味着最先打印这行。\\n print(\'任务2\'); \\n for(int i = 0; i < 100000; i++){} // 需要耗费时间的同步代码\\n print(\'任务3\'); // 需要等待前面的同步代码执行完才能执行。\\n print(\'任务4\'); \\n}\\n输出:\\n任务1\\n任务2\\n任务3\\n任务4\\n
\\n注意:Dart中同步任务比异步任务先执行。
\\n为解决排队过程中的处理时间较长,导致后面排队等待过长。生活中采取了发短信通知等方式解决此问题,而在Dart中采用异步任务的方式解决此问题。那异步是咋解决的呢?异步任务通常是一个耗时的操作(如网络请求),异步任务并不会等待,但会贴出告示说自己完成没有,就像发短信通知状态一样。了解了异步任务是咋回事,我们来看看Dart异步任务的分类。
\\n异步任务的分类:
\\n微任务:指优先级较高的异步任务。
\\n事件任务:指由外部事件触发的异步任务,如网络响应等。他的优先级低于微任务。
\\n同步任务与异步任务的执行顺序如下图所示。
\\n在此部分,我们先回顾一下操作系统中任务的状态。然后再看Dart中异步任务的状态。
\\n本小节从日常生活中的任务出发,首先对比生活中、计算机中、Dart中任务的定义,引出任务都具有的要点。然后在了解任务的基础上,介绍了Dart中按执行方式划分的任务分类,主要分为同步任务和异步任务,其中异步任务中简述了微任务与事件任务。最后在回顾了操作系统中任务的状态基础上介绍了Dart中异步任务的状态。
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n任务要点 | Dart中任务的分类 | Dart中异步任务的状态 |
---|---|---|
任务需要消耗资源 | 同步任务 (最先执行) | 创建(Created) |
任务可并行或并发执行 | 异步任务(所有同步任务执行完才执行) | 待定(Pending) |
任务具有状态 | 微任务 (异步任务中优先级最高) | 执行(Running) |
任务具有目标导向 | 事件任务(一般优先级) | 完成(Completed) |
清理(Disposed) |
在 Flutter 开发中,容器组件是构建用户界面的基石。它们为开发者提供了强大而灵活的方式来组织和布局界面元素。通过使用容器组件,开发者可以轻松地控制子组件的大小、位置、边距、背景等属性,从而创建出美观、易用且响应式的界面。本文将详细介绍 Flutter 中常见的容器组件,包括 Container
、Padding
、Center
、Align
、SizedBox
等,并结合代码示例深入讲解其用法和原理。
Container
组件Container
是 Flutter 中最常用的容器组件之一,它可以组合多个子组件,并对它们进行布局和样式设置。Container
可以包含一个子组件,并且可以设置其大小、边距、内边距、背景颜色、边框等属性。
import \'package:flutter/material.dart\';\\n\\nvoid main() {\\n runApp(MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n home: Scaffold(\\n appBar: AppBar(\\n title: Text(\'Container Example\'),\\n ),\\n body: Center(\\n child: Container(\\n width: 200,\\n height: 200,\\n margin: EdgeInsets.all(20),\\n padding: EdgeInsets.all(10),\\n decoration: BoxDecoration(\\n color: Colors.blue,\\n border: Border.all(color: Colors.black, width: 2),\\n borderRadius: BorderRadius.circular(10),\\n ),\\n child: Text(\\n \'Hello, Container!\',\\n style: TextStyle(color: Colors.white),\\n ),\\n ),\\n ),\\n ),\\n );\\n }\\n}\\n
\\nwidth
和 height
:设置容器的宽度和高度。margin
:设置容器与其他组件之间的外边距。padding
:设置容器内部子组件与容器边缘之间的内边距。decoration
:使用 BoxDecoration
来设置容器的背景颜色、边框和圆角等样式。Container
组件实际上是一个组合组件,它内部封装了多个其他组件,如 Padding
、Align
、DecoratedBox
等。当我们设置 Container
的属性时,它会根据这些属性创建相应的子组件来实现所需的效果。例如,当我们设置 color
属性时,Container
会创建一个 DecoratedBox
组件来设置背景颜色。
Padding
组件Padding
组件用于给子组件添加内边距。它可以在子组件的四周添加一定的空白区域,从而调整子组件的布局。
import \'package:flutter/material.dart\';\\n\\nvoid main() {\\n runApp(MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n home: Scaffold(\\n appBar: AppBar(\\n title: Text(\'Padding Example\'),\\n ),\\n body: Center(\\n child: Padding(\\n padding: EdgeInsets.symmetric(vertical: 20, horizontal: 30),\\n child: Container(\\n color: Colors.green,\\n width: 100,\\n height: 100,\\n ),\\n ),\\n ),\\n ),\\n );\\n }\\n}\\n
\\nEdgeInsets.symmetric
:用于设置垂直和水平方向的内边距。这里设置垂直方向的内边距为 20 像素,水平方向的内边距为 30 像素。Padding
组件是一个 SingleChildRenderObjectWidget
,它通过 RenderPadding
来实现内边距的效果。RenderPadding
会在布局时调整子组件的位置,从而在子组件的四周留出指定的空白区域。
Center
组件Center
组件用于将子组件居中显示。它可以在水平和垂直方向上同时将子组件居中。
import \'package:flutter/material.dart\';\\n\\nvoid main() {\\n runApp(MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n home: Scaffold(\\n appBar: AppBar(\\n title: Text(\'Center Example\'),\\n ),\\n body: Center(\\n child: Container(\\n color: Colors.yellow,\\n width: 150,\\n height: 150,\\n ),\\n ),\\n ),\\n );\\n }\\n}\\n
\\nCenter
组件将 Container
组件在屏幕上居中显示。
Center
组件是一个 Align
组件的特殊情况,它的 alignment
属性默认设置为 Alignment.center
。Align
组件会根据 alignment
属性的值来调整子组件的位置,从而实现居中显示的效果。
Align
组件Align
组件用于将子组件按照指定的对齐方式进行排列。它可以在水平和垂直方向上指定子组件的对齐位置。
import \'package:flutter/material.dart\';\\n\\nvoid main() {\\n runApp(MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n home: Scaffold(\\n appBar: AppBar(\\n title: Text(\'Align Example\'),\\n ),\\n body: Container(\\n width: 300,\\n height: 300,\\n color: Colors.grey,\\n child: Align(\\n alignment: Alignment.topRight,\\n child: Container(\\n color: Colors.red,\\n width: 50,\\n height: 50,\\n ),\\n ),\\n ),\\n ),\\n );\\n }\\n}\\n
\\nalignment
:设置子组件的对齐方式。这里设置为 Alignment.topRight
,表示将子组件对齐到父容器的右上角。Align
组件通过 RenderPositionedBox
来实现子组件的对齐效果。RenderPositionedBox
会根据 alignment
属性的值计算子组件的位置,并将其放置在相应的位置上。
SizedBox
组件SizedBox
组件用于创建一个具有固定大小的盒子。它可以用于占位或者调整组件之间的间距。
import \'package:flutter/material.dart\';\\n\\nvoid main() {\\n runApp(MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n home: Scaffold(\\n appBar: AppBar(\\n title: Text(\'SizedBox Example\'),\\n ),\\n body: Column(\\n children: [\\n Container(\\n color: Colors.blue,\\n width: 100,\\n height: 100,\\n ),\\n SizedBox(height: 20),\\n Container(\\n color: Colors.green,\\n width: 100,\\n height: 100,\\n ),\\n ],\\n ),\\n ),\\n );\\n }\\n}\\n
\\nSizedBox(height: 20)
:创建一个高度为 20 像素的空白盒子,用于在两个 Container
组件之间添加间距。SizedBox
组件是一个 RenderConstrainedBox
,它会强制子组件具有指定的大小。如果没有子组件,它会创建一个空白的盒子。
在实际开发中,我们经常会组合使用多个容器组件来实现复杂的布局。以下是一个组合使用 Container
、Padding
、Center
和 Align
组件的示例:
import \'package:flutter/material.dart\';\\n\\nvoid main() {\\n runApp(MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n home: Scaffold(\\n appBar: AppBar(\\n title: Text(\'Combined Container Example\'),\\n ),\\n body: Center(\\n child: Padding(\\n padding: EdgeInsets.all(20),\\n child: Container(\\n width: 300,\\n height: 300,\\n decoration: BoxDecoration(\\n color: Colors.purple,\\n borderRadius: BorderRadius.circular(10),\\n ),\\n child: Align(\\n alignment: Alignment.bottomLeft,\\n child: Padding(\\n padding: EdgeInsets.all(10),\\n child: Text(\\n \'Combined Containers!\',\\n style: TextStyle(color: Colors.white),\\n ),\\n ),\\n ),\\n ),\\n ),\\n ),\\n ),\\n );\\n }\\n}\\n
\\nCenter
组件将整个布局居中。Padding
组件添加外边距。Container
组件设置背景颜色和圆角。Align
组件将文本组件对齐到容器的左下角,并使用 Padding
组件添加内边距。Flutter 中的容器组件为开发者提供了丰富的布局和样式设置选项。通过合理使用 Container
、Padding
、Center
、Align
、SizedBox
等容器组件,我们可以轻松地构建出复杂而美观的用户界面。在实际开发中,需要根据具体的需求灵活组合使用这些组件,以达到最佳的布局效果。同时,深入理解这些组件的原理和用法,也有助于我们更好地调试和优化界面布局。希望本文对你在 Flutter 开发中使用容器组件有所帮助。
以下是在 Mac 上搭建 Flutter 开发环境的步骤:
\\n如果还未安装 Xcode,可以从 App Store 进行安装。Xcode 提供了开发 iOS 和 macOS 应用所需的工具和环境。
\\n运行以下命令安装 Homebrew:\\n/bin/bash -c \\"$(curl -fsSL raw.githubusercontent.com/Homebrew/in…)\\"
\\n打开终端。\\n使用 Homebrew 安装 Flutter:\\nbrew install --cask flutter\\n四、配置环境变量
\\n打开终端,使用文本编辑器(如 vim 或 nano)打开 .bash_profile 或 .zshrc 文件:
\\nvim ~/.bash_profile\\n\\n
\\n或者
\\nvim ~/.zshrc\\n
\\n按下回车键 出现如下界面\\n\\n输入字母 E
\\n按一下键盘上面的i 进入编辑模式
\\n编辑好环境变量后 wq! 保存退出
\\n然后在终端输入如下命令来刷新才生效
source ~/.zshrc \\n
\\n在文件中添加 Flutter 的安装路径:
\\nexport PATH=\\"$PATH:/Users/your_username/flutter/bin\\"\\n将 your_username 替换为你的 Mac 用户名称。
\\n对于 .bash_profile:
\\nsource ~/.bash_profile\\n
\\n对于 .zshrc:
\\nsource ~/.zshrc\\n
\\n下载并安装 Android Studio。\\n在 Android Studio 中安装 Flutter 和 Dart 插件:\\n打开 Android Studio,点击菜单栏中的 “Preferences”(或 “Android Studio”>“Preferences”)。\\n在 “Plugins” 选项中,搜索 “Flutter” 和 “Dart”,并安装这两个插件。
\\n在 Android Studio 中,打开 “Preferences”>“Appearance & Behavior”>“System Settings”>“Android SDK”。\\n选择需要的 Android SDK 版本进行安装,并确保安装了必要的工具,如 Android SDK Build-Tools、Android SDK Platform-Tools 等。
\\n打开终端,运行以下命令检查 Flutter 是否安装成功:
\\nflutter doctor\\nFlutter 会检查你的开发环境并给出报告,根据报告中的提示解决可能存在的问题。
","description":"以下是在 Mac 上搭建 Flutter 开发环境的步骤: 一、安装 Xcode\\n\\n如果还未安装 Xcode,可以从 App Store 进行安装。Xcode 提供了开发 iOS 和 macOS 应用所需的工具和环境。\\n\\n二、安装 Homebrew(如果未安装)\\n打开终端。\\n\\n运行以下命令安装 Homebrew: /bin/bash -c \\"$(curl -fsSL raw.githubusercontent.com/Homebrew/in…)\\"\\n\\n三、安装 Flutter\\n\\n打开终端。 使用 Homebrew 安装 Flutter: brew…","guid":"https://juejin.cn/post/7491474391504175155","author":"坚果派_xq9527","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-10T08:47:57.450Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/050efe47e8a84466a0a54a20677b43fc~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Z2a5p6c5rS-X3hxOTUyNw==:q75.awebp?rk3s=f64ab15b&x-expires=1744879676&x-signature=KUsmX7W%2Bd56LhOzQABbDtdtFfSc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/230b78bb75b140f0ac5cdf8f7e518bed~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Z2a5p6c5rS-X3hxOTUyNw==:q75.awebp?rk3s=f64ab15b&x-expires=1744879676&x-signature=GW7Q%2Fw7tK%2Bj%2F2KMb9Q91NMLhU0o%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fbcdcddd14004776803d200538876b9c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Z2a5p6c5rS-X3hxOTUyNw==:q75.awebp?rk3s=f64ab15b&x-expires=1744879676&x-signature=yvvOynKlwiFM6Z140zz3%2BMtjJkQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/cdad4121b9b741128a26813956568dd6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Z2a5p6c5rS-X3hxOTUyNw==:q75.awebp?rk3s=f64ab15b&x-expires=1744879676&x-signature=8hYHteaUYK9XyB3KehlftNhY8sY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/40bd92f055704f459994a94c1a20072f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Z2a5p6c5rS-X3hxOTUyNw==:q75.awebp?rk3s=f64ab15b&x-expires=1744879676&x-signature=6%2FE7VP234iuMJJPCGqmNFETPozY%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"[Get 源码] GetPageRoute 与 GetxController 的自动回收机制","url":"https://juejin.cn/post/7491234023738638373","content":"dartCopy Code\\nmixin PageRouteReportMixin<T> on Route<T> {\\n @override\\n void install() {\\n super.install();\\n RouterReportManager.reportCurrentRoute(this);\\n }\\n\\n @override\\n void dispose() {\\n super.dispose();\\n RouterReportManager.reportRouteDispose(this);\\n }\\n}\\n
\\ndartCopy Code\\nclass GetPageRoute<T> extends PageRoute<T>\\n with GetPageRouteTransitionMixin<T>, PageRouteReportMixin {\\n @override\\n void dispose() {\\n super.dispose();\\n final middlewareRunner = MiddlewareRunner(middlewares);\\n middlewareRunner.runOnPageDispose();\\n }\\n}\\n
\\nGetX 通过 **Bindings
** 机制实现依赖注入与自动回收,其核心逻辑如下:
Bindings
声明**:每个路由页面关联一个 Binding 类dependencies()
方法中调用 Get.lazyPut
或 Get.put
dartCopy Code\\nclass DetailBinding extends Bindings {\\n @override\\n void dependencies() {\\n Get.lazyPut(() => DetailController()); // 路由级单例\\n }\\n}\\n
\\nGetPageRoute
被销毁时(页面关闭),触发 RouterReportManager.reportRouteDispose
_removeDependencyByRoute
移除路由关联的控制器GetPageRoute.dispose()
→ PageRouteReportMixin
→ RouterReportManager
→ 清理依赖方法 | 自动销毁支持 | 典型场景 |
---|---|---|
Get.put() | ❌ 不支持 | 全局单例(如用户信息管理) |
Get.create() | ✅ 支持 | 需要动态参数的控制器 |
Bindings 绑定 | ✅ 支持 | 路由级作用域控制器(推荐方式) |
Get.put()
问题:直接通过该方法注册的实例会 常驻内存**,需手动调用 Get.delete()
Get.create()
优势**:与路由绑定,页面关闭时自动触发 onClose()
Bindings
管理控制器,避免内存泄漏dartCopy Code\\nclass AuthMiddleware extends GetMiddleware {\\n @override\\n RouteSettings? redirect(String? route) {\\n if (!isLogin && route == \\"/profile\\") {\\n return RouteSettings(name: \\"/login\\");\\n }\\n return null;\\n }\\n}\\n
\\n场景:未登录用户访问个人主页时跳转登录页
\\ndartCopy Code\\nclass ParamsMiddleware extends GetMiddleware {\\n @override\\n GetPage? onPageCalled(GetPage? page) {\\n if (page?.name == \\"/search\\") {\\n return page?.copy(parameters: {\\"timestamp\\": DateTime.now().toString()});\\n }\\n return page;\\n }\\n}\\n
\\n场景:动态添加时间戳参数到搜索页面
\\ndartCopy Code\\nclass AnalyticsMiddleware extends GetMiddleware {\\n @override\\n void onPageDispose() {\\n Analytics.reportClose(Get.currentRoute);\\n }\\n}\\n
\\n场景:页面关闭时上报用户行为埋点
\\n清理与特定路由关联的依赖项,确保内存回收
\\ndartCopy Code\\nstatic void _removeDependencyByRoute(Route route) {\\n // 清理通过 Get.create() 注册的实例\\n if (_routesByCreate.containsKey(route)) {\\n _routesByCreate[route]!.forEach((onClose) => onClose());\\n _routesByCreate.remove(route);\\n }\\n\\n // 清理通过 Bindings 注册的依赖\\n final keys = _routesKey[route];\\n keys?.forEach((key) => GetInstance().delete(key: key));\\n _routesKey.remove(route);\\n}\\n
\\n双重清理机制:
\\nGet.create()
实例**:直接执行注册时的 onClose
回调Bindings
依赖**:通过 GetInstance().delete()
触发 onClose()
路由级作用域:确保不同路由的依赖项 完全隔离
\\n生命周期绑定:
\\nGetPageRoute
通过重写 dispose()
方法,将控制器生命周期与路由销毁事件强绑定
中间件扩展性:
\\nMiddlewareRunner
提供可插拔的扩展点(参数修改、拦截、埋点)
依赖治理策略:
\\n这种设计在保持灵活性的同时,最大程度避免了 Flutter 应用常见的内存泄漏问题,其实现方式与 Android 的 ViewModel
+ LiveData
生命周期管理有着异曲同工之妙。
每个Flutter
开发者都踩过这样的坑:点了按钮没反应,列表滑动像卡帧,debug
半天发现少写个setState
。你像个救火队员,到处补状态更新 —— 按下葫芦浮起瓢。传统开发逼你既当业务设计师,又得做视图保姆,这种精神分裂该到头了。
Flutter
甩来一剂猛药:别告诉我按钮怎么变色,直接说什么时候该红!把界面写成状态的条件表达式,剩下的脏活累活
引擎自己包圆。从此告别setState
满天飞,你要做的就是定规矩
,框架负责执行。当界面成了状态的影子,代码才能回归它该有的样子。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\n想要提升对声明式编程的认知,离不开的一个话题就是命令式编程。从对已有事物的认知过渡到未知事物,做横纵向对比(没有对比就没有伤害
)。方能体现新事物的价值。
精准执行力每一步
\\n\\n命令式编程是一种明确告诉计算机
\\n\\"如何做\\"
的编程范式。需要一步步写出执行细节,如同给计算机下达操作指令的微操大师。
就像按照菜谱炒菜一样:好吃的秘诀在于精准的执行每一步操作,否则做出来的菜就难以下咽。
\\n特性 | 示例场景 | 典型代码表现 |
---|---|---|
逐步指令 | 实现列表排序 | 手写冒泡/快速排序算法 |
可变状态 | 更新用户界面 | view.setText(...) view.setVisibility(...) |
显式控制流 | 处理业务逻辑 | for /if /while 等流程控制语句 |
副作用依赖 | 读写文件/网络请求 | 交替执行的赋值/函数调用 |
看透命令式的本质
你以为你在写代码?不,你是在当UI
的急诊科医生!先来看一个经典Android
场景:
// 传统Android写法:每次改UI都像在抢救病人\\nTextView titleView = findViewById(R.id.tv_title);\\nImageView iconView = findViewById(R.id.iv_icon);\\n\\nvoid updateUI(User user) {\\n // 第一步:找到病人(findViewById)\\n titleView.setText(user.name);\\n // 第二步:打针吃药(setText/setVisibility)\\n iconView.setVisibility(user.isVip ? View.VISIBLE : View.GONE);\\n // 第三步:处理并发症(可能的内存泄漏)\\n iconView.setOnClickListener(v -> showVipDialog());\\n}\\n
\\n这种写法有三大致命伤:
\\nView
找到手抽筋:每次操作都要先findViewById
,代码里遍布着R.id.*
的魔法数字。View
是否已经被修改过。匿名内部类
持有外部引用,稍不留神就埋下炸弹。更可怕的是动态布局场景:
\\n// 动态添加View的噩梦\\nLinearLayout container = findViewById(R.id.container);\\nfor (int i = 0; i < 100; i++) {\\n TextView tv = new TextView(context);\\n tv.setText(\\"Item \\" + i);\\n container.addView(tv); // 内存警告:这里可能瞬间创建100个View!\\n}\\n
\\n\\n\\n小结:这种命令式写法就像用镊子组装火箭 —— 每个零件都要亲手拧,效率低还容易出错。
\\n
优势与软肋
✅ 优势 | ❌ 软肋 |
---|---|
1、精细控制: 可精确控制每个对象的状态 | 1、代码膨胀: 简单UI 需大量显式操作代码 |
2、直观易懂: 代码顺序即执行流程,符合直觉 | 2、状态失控: 跨组件状态同步困难,易引发不一致 |
3、性能调优: 可直接优化关键代码路径 | 3. 维护成本高: 修改UI 需手动调整多处关联逻辑 |
下次有人跟你说:\\"声明式编程是未来的唯一方向\\"
,请优雅回应:
\\n\\n乌克兰谚语:\\"你用叉子喝汤吗?不,但叉子依然存在\\"
\\n编程范式如同餐具:
\\n\\n
\\n- 喝汤用勺子(
\\n声明式
)- 切牛排用刀(
\\n命令式
)真正的开发者应当 善用工具,而非迷信工具。
\\n
用数学函数描述界面
声明式编程是一种通过描述目标状态(What
)而非具体步骤(How
)来构建界面的编程范式。
在Flutter
中,整个UI
被抽象为状态(State
)的函数,公式可简化为:
说人话版解释:想象你点外卖
318
步按门铃3
下(迟早被当成神经病
)。\\"北京市朝阳区xx大厦18层\\"
,管他骑电动车还是开飞机。Flutter
就是这个外卖平台,Widget树
就是你的订单地址。你只管说\\"要什么\\"
,别操心\\"怎么送\\"
,这才是程序员该干的活!
三条军规记死了
说一不二原则
幂等性是指某个操作或函数可以多次执行,但其结果与执行一次相同。换言之,即\\n相同输入必须输出相同界面,就像你妈喊你全名时,甭管正在打游戏还是拉屎,都得立马回话。
\\n// 坏代码:今天晴天明天暴雨 \\nWidget buildWeather() { \\n return isSunny ? Sun() : Rain(); // 这个isSunny要是外部变量就完犊子 \\n} \\n\\n// 好代码:老天爷说了算 \\nWidget buildWeather(bool isSunny) { \\n return isSunny ? Sun() : Rain(); \\n} \\n
\\n别碰我的组件
侧重于使用不可变数据结构及纯函数来处理数据,以避免副作用
。换言之,状态变更不会直接
修改现有界面元素,而是生成新的描述。
// 作死写法:直接改旧对象 \\nvoid updateProfile() { \\n currentUser.name = \'王二狗\'; // 等着界面装死吧 \\n} \\n\\n// 专业写法:换人换到底 \\nvoid updateProfile() { \\n userState.value = currentUser.copyWith(name: \'王二狗\'); \\n} \\n
\\n框架是你小弟
框架负责将最新的状态描述同步到实际渲染层,别自己吭哧吭哧调setState
,把状态往Riverpod/Provider
一扔。
final weatherProvider = StateProvider((ref) => \'晴天\'); \\n\\nclass WeatherScreen extends ConsumerWidget { \\n @override \\n Widget build(BuildContext context, WidgetRef ref) { \\n final weather = ref.watch(weatherProvider); \\n return Text(\'今天天气:$weather\'); \\n } \\n} \\n
\\n别告诉我怎么做,直接说想要啥样?
见过新手写界面吗?在onPressed
里疯狂操作:改文本颜色
、调图片尺寸
、切组件显隐
...代码写成八爪鱼,最后发现漏改了个Container
透明度。这就是命令式编程的日常 —— 你既当老板又当小弟,累成狗还容易翻车。
Flutter
甩过来一巴掌:把界面写成数学公式会不会? 管他用户怎么点怎么滑,你只要搞清楚:
\\n\\n\\n
\\n- 1、当前这个界面
\\n有多少种状态
?- 2、每个状态
\\n对应
的界面长啥样?
剩下的交给框架自己算!
\\n举个真代码你细品:
\\n// 传统命令式:操作具体控件(当保姆) \\nvoid updateUI(bool isError) { \\n if (isError) { \\n submitButton.style = redStyle; \\n errorText.visible = true; \\n } else { \\n submitButton.style = blueStyle; \\n errorText.visible = false; \\n } \\n} \\n\\n// 声明式:定义状态与界面的映射(当老板) \\nWidget buildButton(bool isError) { \\n return Column( \\n children: [ \\n ElevatedButton( \\n style: isError ? redStyle : blueStyle, \\n onPressed: handleSubmit, \\n child: const Text(\'提交\'), \\n ), \\n if (isError) \\n const Text(\'出错了老铁!\', style: errorStyle) \\n ] \\n ); \\n} \\n
\\n看出门道了吗?声明式编程让你从操作工变成设计师,只定规则不干脏活。
\\nWidget树
的生存法则刚学Flutter
的新手最困惑:每次都重建整个Widget树
,性能不得炸?这就是没吃透Flutter
的三棵树:
Widget
树:轻量级配置描述(你的代码
)。Element
树:内存中的控件管家(框架维护
)。RenderObject
树:真正的渲染猛将(GPU打交道
)。举个栗子:你写了十个Text
组件
Column(\\n children: [\\n Text(\\"张三\\"),\\n Text(\\"李四\\"),\\n // ...八个重复Text\\n ]\\n)\\n
\\n当某个Text
内容变化时:
Widget树
全部重建(你的代码层面
)。Element树
对比新旧Widget
,发现只有第三个Text
不同。RenderObject树
只更新第三个文本的绘制指令。\\n\\n这才是声明式的精髓:你负责大胆描述,框架负责小心求证。
\\n
程序员的修真传
青铜泥潭
在build()
方法中堆砌业务逻辑,导致视图与逻辑深度耦合,这种反模式常引发代码维护难题(团队协作中的高危操作
)。
/// 青铜段位 - 反例:视图与逻辑混杂\\nclass BadCounter extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n int count = 0; // 状态直接定义在build内部\\n \\n return Scaffold(\\n body: Center(\\n child: InkWell(\\n onTap: () {\\n // 直接在视图层修改状态\\n count++;\\n print(\'Current count: $count\');\\n },\\n child: Text(\'点击次数: $count\'),\\n ),\\n ),\\n );\\n }\\n}\\n
\\n白银初悟
遵循组件设计规范,合理切分StatelessWidget
与StatefulWidget
,初步建立响应式编程思维。
/// 白银段位 - 正例:组件职责分离\\nclass CounterButton extends StatelessWidget {\\n final VoidCallback onPressed;\\n \\n const CounterButton({required this.onPressed});\\n \\n @override\\n Widget build(BuildContext context) {\\n return ElevatedButton(\\n onPressed: onPressed,\\n child: const Text(\'增加计数\'),\\n );\\n }\\n}\\n\\nclass CounterDisplay extends StatelessWidget {\\n final int count;\\n \\n const CounterDisplay({required this.count});\\n \\n @override\\n Widget build(BuildContext context) {\\n return Text(\'当前计数: $count\');\\n }\\n}\\n
\\n黄金通玄
精通状态管理范式,能够基于Provider
架构实现跨组件通信,完成复杂业务场景下的状态同步。
/// 黄金段位 - Provider状态管理\\nfinal counterProvider = ChangeNotifierProvider((_) => CounterModel());\\n\\nclass CounterModel with ChangeNotifier {\\n int _count = 0;\\n \\n int get count => _count;\\n \\n void increment() {\\n _count++;\\n notifyListeners();\\n }\\n}\\n\\n// 使用Consumer消费状态\\nConsumer<CounterModel>(\\n builder: (_, model, __) => Text(\'全局计数: ${model.count}\'),\\n)\\n
\\n钻石窥道
深入框架底层,通过继承InheritedWidget
实现定制化状态共享方案,理解Widget
与Element
的绑定机制。
/// 钻石段位 - 自定义InheritedWidget\\nclass CounterScope extends InheritedWidget {\\n final int count;\\n final VoidCallback increment;\\n \\n CounterScope({\\n required this.count,\\n required this.increment,\\n required Widget child,\\n }) : super(child: child);\\n\\n static CounterScope? of(BuildContext context) =>\\n context.dependOnInheritedWidgetOfExactType<CounterScope>();\\n\\n @override\\n bool updateShouldNotify(CounterScope old) => count != old.count;\\n}\\n
\\n王者合道
洞悉框架设计哲学,在视觉层面对Widget树
进行拓扑分析时,能同步推演出Element树
的动态更新过程,达到人机合一的调试境界。
/// 王者段位 - 状态驱动UI(伪代码示意)\\n// 定义最小化状态\\nclass _PageState {\\n final counterState = Stateful<int>(0); // 声明式状态容器\\n final loadingState = Stateful<bool>(false);\\n \\n void _handleRefresh() {\\n loadingState.value = true; // 触发加载指示器重建\\n fetchData().then((res) {\\n counterState.value = res.count; // 触发计数器重建\\n loadingState.value = false; // 关闭加载指示器\\n });\\n }\\n}\\n\\n// UI仅响应状态变化\\nBuilder((ctx) => [\\n if (_pageState.loadingState.value) LoadingIndicator(),\\n Text(\'${_pageState.counterState.value}\'),\\n Button(onTap: _pageState._handleRefresh),\\n]);\\n
\\n\\n\\n核心进阶法则:
\\nFlutter
声明式架构并非简单的语法糖,而是需要我们建立状态驱动思维。当技术视角从\\"如何操作界面元素\\"
转换为\\"如何设计状态拓扑\\"
,才标志着真正突破编程范式转型的关键节点。
当你用声明式写界面时,本质上是在做界面代数:
\\n状态
)。build
方法)。Flutter
解方程(渲染
)。那些还在手动操作DOM
的前端兄弟们,就像拿着算盘解微积分。而你已经用上计算器了 —— 这就是维度差距。下次见到setState
手忙脚乱的新手,把这篇拍他脸上:
\\n\\n“ 别动那个按钮!先想清楚你的状态变量!”
\\n
vs
声明式:暴力对比表
维度 | 命令式编程 | 声明式编程 | 暴言点评 |
---|---|---|---|
操作对象 | 具体View 实例(findViewById 找控件) | 抽象Widget 描述(写蓝图不碰实物) | 一个在工地搬砖,一个在办公室画图纸✅ |
更新方式 | 手动改属性(setText() setVisibility() ) | 推倒Widget 树重建(框架智能diff ) | 前者像给汽车边跑边换轮胎,后者直接换新车但只改零件⚠️ |
代码结构 | 过程式代码(先A 后B 再C ) | 状态映射方程(当X 时显示Y ) | 流水线工人 vs 数学老师,维度碾压🔥 |
思维模式 | 时间轴操作(点击→改数据→找控件→更新) | 状态空间映射(数据变→界面自动变) | 前者需要记住所有操作步骤,后者只要定义好对应关系💡 |
实战场景 | 改完列表项忘记更新详情页 | 状态源一改全家爆炸更新 | 命令式是扫雷游戏,声明式是自动排雷🚩 |
性能陷阱 | 频繁findView 耗性能 | Widget 树重建但有智能diff | 你以为右栏更耗性能?框架比你懂优化🚀 |
调试难度 | 漏更新时像捉迷藏 | 状态快照直接看时间轴 | 左栏调试像破案,右栏直接看监控录像📸 |
代码传染性 | 改个需求得满世界找关联代码 | 改状态定义自动波及相关UI | 前者是病毒传播,后者是精准核爆💥 |
setText
的兄弟,你代码里藏着的findViewById
比我的相亲对象还多!用数学公式干翻体力活
,Widget
树就是你的尚方宝剑!Flutter
框架比你更懂怎么更新界面 —— 不服跑个分?学习至此,你应该对声明式编程有了一个深入的认知,接下来,我们将继续深入探索状态管理相关的知识!
\\n\\n","description":"前言 每个Flutter开发者都踩过这样的坑:点了按钮没反应,列表滑动像卡帧,debug半天发现少写个setState。你像个救火队员,到处补状态更新 —— 按下葫芦浮起瓢。传统开发逼你既当业务设计师,又得做视图保姆,这种精神分裂该到头了。\\n\\nFlutter甩来一剂猛药:别告诉我按钮怎么变色,直接说什么时候该红!把界面写成状态的条件表达式,剩下的脏活累活引擎自己包圆。从此告别setState满天飞,你要做的就是定规矩,框架负责执行。当界面成了状态的影子,代码才能回归它该有的样子。\\n\\n操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。\\n\\n一、命令式编程…","guid":"https://juejin.cn/post/7491155241192177718","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-09T14:18:48.386Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/70f34cc526fe4d978041a992b7d3c34e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1744813127&x-signature=3qNd42TaGVuhRJfYdlgwQEV3%2FY0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e8b680d2bfdc428882f972f576e332d6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1744813127&x-signature=Tw295b82x5KpIGzN%2FSuywCFfpLk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a6255acdca6943b78f6b4d1b90bf8fa3~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1744813127&x-signature=ZmYpe63QSj%2BL3uLwCu8zhbGsQYA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/02e80322c6cc42059c475532f275d8b2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1744813127&x-signature=0AWcx08M8UZ4%2F1iOOmaBGAqaA%2Bk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2182646e1de244b6ba623b7b2f8589c2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1744813127&x-signature=etdoOCwTAk%2FnKc7BzAVzeA7oFGE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/435140bacfe447be8dc99146eec96762~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1744813127&x-signature=YUkXVvf2j0g5innN5ObS9tp%2BxYA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a8930834266e4710ac2a70661123c474~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1744813127&x-signature=8C1vrjnnMRG%2FMdX4t9pl1Y%2BCyQQ%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"PlatformInterface 的双向通信能力解析","url":"https://juejin.cn/post/7491098594008940571","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
原生调用 Flutter 的限制
\\nPlatformInterface
本身专为 Flutter → Native 单向通信设计,但可通过以下方式实现双向交互:
MethodChannel.Result
)4核心设计原则
\\nFlutterToNative
和 NativeToFlutter
独立接口,通过组合实现双向通信3StandardMessageCodec
确保两端数据类型兼容5api.dart
) dartCopy Code\\nimport \'package:pigeon/pigeon.dart\';\\n\\n// Flutter 调用原生的方法\\n@HostApi()\\nabstract class DeviceInfoApi {\\n String getBatteryLevel();\\n}\\n\\n// 原生调用 Flutter 的方法\\n@FlutterApi()\\nabstract class FlutterEventApi {\\n void onDataReceived(String data);\\n}\\n
\\nbashCopy Code\\nflutter pub run pigeon \\\\\\n --input api.dart \\\\\\n --dart_out lib/api_generated.dart \\\\\\n --java_out android/src/main/java/com/example/DeviceApi.java \\\\\\n --java_package \\"com.example\\"\\n
\\ndartCopy Code\\nimport \'package:plugin_platform_interface/plugin_platform_interface.dart\';\\n\\nabstract class HybridPlatformInterface extends PlatformInterface {\\n HybridPlatformInterface() : super(token: _token);\\n static final Object _token = Object();\\n\\n static HybridPlatformInterface _instance = HybridPlatform();\\n static HybridPlatformInterface get instance => _instance;\\n static set instance(HybridPlatformInterface instance) {\\n PlatformInterface.verify(instance, _token);\\n _instance = instance;\\n }\\n\\n // Flutter → Native 方法\\n Future<String> getBatteryLevel() {\\n throw UnimplementedError();\\n }\\n\\n // Native → Flutter 回调注册\\n void registerDataHandler(Function(String) handler) {\\n throw UnimplementedError();\\n }\\n}\\n
\\ndartCopy Code\\nclass HybridPlatform extends HybridPlatformInterface {\\n final _eventHandlers = <Function(String)>[];\\n\\n @override\\n Future<String> getBatteryLevel() async {\\n // 调用 Pigeon 生成的接口\\n return DeviceInfoApi().getBatteryLevel();\\n }\\n\\n @override\\n void registerDataHandler(Function(String) handler) {\\n _eventHandlers.add(handler);\\n }\\n\\n // 触发 Native 发来的事件\\n void handleNativeEvent(String data) {\\n for (final handler in _eventHandlers) {\\n handler(data);\\n }\\n }\\n}\\n
\\nkotlinCopy Code\\n// 实现 Pigeon 生成的接口\\nclass DeviceApiImpl : DeviceInfoApi.DeviceInfoApi {\\n override fun getBatteryLevel(): String {\\n val batteryLevel = getSystemBatteryLevel() // 实现原生逻辑\\n return batteryLevel.toString()\\n }\\n}\\n\\n// 注册原生到 Flutter 的调用\\nclass FlutterEventImpl : FlutterEventApi.FlutterEventApi {\\n override fun onDataReceived(data: String) {\\n // 通过 MethodChannel 反向调用\\n HybridPlatformInterface.instance.handleNativeEvent(data)\\n }\\n}\\n
\\ndartCopy Code\\n// Flutter 使用示例\\nvoid main() {\\n HybridPlatformInterface.instance.registerDataHandler((data) {\\n print(\'Received from Native: $data\');\\n });\\n\\n // 触发原生方法\\n HybridPlatformInterface.instance.getBatteryLevel().then((level) {\\n print(\'Battery Level: $level%\');\\n });\\n}\\n
\\n技术组件 | PlatformInterface | Pigeon |
---|---|---|
代码生成 | 无(需手动实现)4 | 自动生成多平台代码3 |
类型安全 | 低(依赖动态类型转换)4 | 高(接口强类型约束)3 |
双向通信支持 | 需结合 EventChannel/回调实现4 | 原生支持双向接口定义(@HostApi/@FlutterApi)3 |
维护成本 | 高(需同步多端协议)4 | 低(协议变更后重新生成代码)3 |
减少跨线程切换
\\nHandler(Looper.getMainLooper())
确保回调在主线程执行5kotlinCopy Code\\nHandler(Looper.getMainLooper()).post {\\n FlutterEventImpl().onDataReceived(\\"data\\")\\n}\\n
\\n数据序列化优化
\\nByteData
代替 JSON
(减少 30% 解析开销)5dartCopy Code\\n@HostApi()\\nabstract class BinaryDataApi {\\n Uint8List fetchLargeData();\\n}\\n
\\n事件防抖处理
\\ndartCopy Code\\nDateTime _lastEventTime = DateTime.now();\\n\\nvoid handleNativeEvent(String data) {\\n if (DateTime.now().difference(_lastEventTime) > Duration(milliseconds: 16)) {\\n _lastEventTime = DateTime.now();\\n // 处理事件\\n }\\n}\\n
\\n通过扩展 PlatformInterface
并集成 Pigeon 代码生成工具,可实现类型安全、高效的双向跨平台通信。该方案结合了两者的优势:
适用于需要频繁双向交互的场景(如实时协作编辑、物联网设备控制),相比传统 MethodChannel 方案可降低 40% 的通信延迟5。
\\n功能维度 | Pigeon | PlatformInterface | 对应场景 |
---|---|---|---|
代码生成能力 | 自动生成类型安全的跨平台接口代码37 | 提供接口基类约束,需手动实现具体逻辑36 | 减少手写通信代码量 |
协议管理 | 通过 .dart 文件统一管理接口定义3 | 依赖继承和抽象方法强制接口规范36 | 确保多端实现一致性 |
通信模式支持 | 支持双向接口(@HostApi/@FlutterApi)3 | 需结合 MethodChannel/EventChannel 实现67 | 复杂双向交互场景 |
分层架构设计
\\nVideoPlayerPlatform
抽象基类,强制所有平台实现遵循统一 API 规范36。历史演进因素
\\nPlatformInterface + MethodChannel
实现,后期引入 Pigeon 优化通信层代码,但需保留基类以保证向后兼容性37。扩展性需求
\\ndartCopy Code\\n// 1. PlatformInterface 定义核心接口\\nabstract class VideoPlayerPlatform extends PlatformInterface {\\n Future<void> initialize() { /* 基类约束 */ }\\n}\\n\\n// 2. Pigeon 生成通信协议\\n@HostApi()\\nabstract class PigeonVideoApi {\\n @async\\n bool play(String videoId);\\n}\\n\\n// 3. 实现类整合两者\\nclass HybridVideoPlayer extends VideoPlayerPlatform {\\n final _pigeonApi = PigeonVideoApi();\\n\\n @override\\n Future<void> initialize() {\\n return _pigeonApi.play(\\"video_123\\"); // 调用 Pigeon 生成的方法:ml-citation{ref=\\"3,7\\" data=\\"citationList\\"}\\n }\\n}\\n
\\n接口抽象层级不同
\\n跨平台兼容性处理
\\nSurfaceTexture
),此时仍需 PlatformInterface 提供统一入口67。Pigeon 与 PlatformInterface 在 Flutter 插件开发中并非替代关系,而是协同分工:
\\nTLHC 是 Flutter 3.29+ 引入的 Hybrid Composition 优化模式,通过 TextureView 替代 SurfaceView 实现原生视图嵌入,减少内存拷贝并提升渲染性能57。其核心优势在于:
\\nSurfaceTexture
输出到 Flutter 的纹理层,避免双缓冲内存开销48。在 AndroidView
中启用 TLHC 模式:
dartCopy Code\\nAndroidView(\\n viewType: \'native_texture_view\',\\n creationParams: {\'key\': \'value\'},\\n creationParamsCodec: StandardMessageCodec(),\\n // 关键配置:启用 TLHC\\n useTextureLayerHybridComposition: true, // :ml-citation{ref=\\"5,8\\" data=\\"citationList\\"}\\n)\\n
\\n需基于 TextureView
实现 PlatformView
:
kotlinCopy Code\\nclass NativeTextureView(context: Context, id: Int) : PlatformView {\\n private val textureView = TextureView(context)\\n\\n init {\\n textureView.surfaceTextureListener = object : TextureView.SurfaceTextureListener {\\n override fun onSurfaceTextureAvailable(texture: SurfaceTexture, width: Int, height: Int) {\\n // 初始化 OpenGL ES 或 Canvas 绘制逻辑(如视频解码):ml-citation{ref=\\"6,7\\" data=\\"citationList\\"}\\n val surface = Surface(texture)\\n // 示例:绘制红色背景\\n val canvas = surface.lockCanvas(null)\\n canvas.drawColor(Color.RED)\\n surface.unlockCanvasAndPost(canvas)\\n }\\n // 其他回调省略...\\n }\\n }\\n\\n override fun getView(): View = textureView // :ml-citation{ref=\\"5,8\\" data=\\"citationList\\"}\\n}\\n
\\n在 FlutterEngine
初始化时注册视图工厂:
kotlinCopy Code\\nflutterEngine.platformViewsController.registry\\n .registerViewFactory(\\"native_texture_view\\", NativeTextureViewFactory())\\n
\\n版本要求
\\n透明背景处理
\\n若需透明背景,需同时在 Flutter 和原生端配置:
dartCopy Code\\nAndroidView(\\n ...\\n hitTestBehavior: PlatformViewHitTestBehavior.transparent, // Flutter 侧透明:ml-citation{ref=\\"3\\" data=\\"citationList\\"}\\n)\\n
\\nkotlinCopy Code\\n// 原生端\\ntextureView.setBackgroundColor(Color.TRANSPARENT) // :ml-citation{ref=\\"3,8\\" data=\\"citationList\\"}\\n
\\n性能优化建议
\\n避免频繁更新:减少 SurfaceTexture
的 updateTexImage()
调用频率67。
启用 HCPP:在 FlutterActivity
中强制启用 Hybrid Composition++:
kotlinCopy Code\\nFlutterImageView.enableHybridCompositionPlusPlus(true) // :ml-citation{ref=\\"8\\" data=\\"citationList\\"}\\n
\\n特性 | TLHC 模式 | 默认 Hybrid Composition |
---|---|---|
内存占用 | 低(单纹理) | 高(双缓冲 + 视图控制器)58 |
GPU 负载 | 低(无纹理拷贝) | 高(需同步纹理到 Flutter)48 |
交互延迟 | <5ms | 10-20ms |
适用场景 | 视频播放、动态地图 | 静态原生控件(如 WebView) |
黑屏问题
\\nSurfaceTextureListener
是否正确初始化绘制逻辑67。TextureView
已设置透明背景且未被其他视图遮挡38。性能卡顿
\\nSurfaceTexture
的更新频率,限制至 60Hz67。通过以上配置,可充分发挥 TLHC 模式的高性能优势,适用于高频渲染场景(如视频、游戏),同时保持与 Flutter UI 的无缝合成58。
","description":"一、TLHC 核心原理 TLHC 是 Flutter 3.29+ 引入的 Hybrid Composition 优化模式,通过 TextureView 替代 SurfaceView 实现原生视图嵌入,减少内存拷贝并提升渲染性能57。其核心优势在于:\\n\\n单纹理共享:原生视图直接通过 SurfaceTexture 输出到 Flutter 的纹理层,避免双缓冲内存开销48。\\n硬件加速兼容:支持与其他 Flutter Widgets 混合合成(如透明度、动画)56。\\n二、配置步骤与代码实现\\n1. Flutter 端配置\\n\\n在 …","guid":"https://juejin.cn/post/7491231734953361458","author":"zonda的地盘","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-09T09:46:39.657Z","media":null,"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter PlatformViewLink vs Texture","url":"https://juejin.cn/post/7491116054371532809","content":"在 Flutter 中,通过 **PlatformViewLink
+ AndroidViewSurface
和直接使用 Texture(textureId: ...)
** 都能实现原生视图的嵌入,但两者的实现机制、适用场景和性能特征有显著差异。以下是详细对比:
PlatformViewLink
+ AndroidViewSurface
**定位:属于 Flutter 的 PlatformView 高层抽象,专为复杂交互场景设计。
\\n实现流程:
\\nPlatformViewLink
创建平台视图控制器(AndroidViewController
)。AndroidViewSurface
将原生视图的渲染与 Flutter 的 LayerTree
合成。代码示例:
\\ndartCopy Code\\nPlatformViewLink(\\n viewType: \'native_view\',\\n surfaceFactory: (context, controller) {\\n return AndroidViewSurface(\\n controller: controller as AndroidViewController,\\n hitTestBehavior: PlatformViewHitTestBehavior.opaque,\\n );\\n },\\n onCreatePlatformView: (params) {\\n return PlatformViewsService.initSurfaceAndroidView(...);\\n },\\n)\\n
\\nTexture
组件定位:底层纹理渲染方案,仅处理像素数据,不涉及交互逻辑。
\\n实现流程:
\\nTextureRegistry
创建纹理(SurfaceTextureEntry
)。Texture(textureId: ...)
直接引用纹理 ID 显示内容。MethodChannel
传递)。代码示例:
\\ndartCopy Code\\n// 原生端返回 textureId\\nfinal textureId = await channel.invokeMethod(\'getTextureId\');\\nreturn Texture(textureId: textureId);\\n
\\n特性 | PlatformViewLink + AndroidViewSurface | 直接使用 Texture 组件 |
---|---|---|
输入事件处理 | ✅ 自动支持触摸、手势、焦点 | ❌ 需手动通过 MethodChannel 转发 |
生命周期管理 | ✅ 自动同步 Flutter 与原生视图的生命周期 | ❌ 需手动释放纹理资源 |
渲染层级控制 | ✅ 支持与其他 Flutter Widgets 混合布局(如透明度、遮罩) | ✅ 支持,但需手动处理合成逻辑 |
内存占用 | 较高(维护完整的视图层级和控制器) | 较低(仅维护纹理内存) |
性能优化潜力 | 适合动态交互场景(如地图、WebView) | 适合静态或高频渲染(如视频、游戏) |
代码复杂度 | 较高(需理解 PlatformView 框架) | 较低(仅纹理传递) |
平台兼容性 | ✅ 官方推荐,适配所有 PlatformView 场景 | ⚠️ 部分高级功能需自行实现 |
PlatformViewLink
:FlutterRenderer
)将原生视图作为独立图层(Surface
)与 Flutter UI 混合。在 Android 上,这会触发 SurfaceFlinger
的多层合成,可能导致 GPU 负载增加。Texture
组件:SurfaceTexture
,Flutter 直接将其作为纹理绘制到 Skia 画布。跳过了 Android 视图系统的合成器,减少了 GPU 指令提交次数。指标 | PlatformViewLink | Texture 组件 |
---|---|---|
内存占用 (1080p) | ~50 MB(含视图控制器) | ~20 MB(仅纹理) |
60 FPS 稳定性 | 可能波动(多层合成开销) | 更稳定(单一纹理绘制) |
首帧渲染延迟 | 100-200ms | 50-100ms |
PlatformViewLink
的场景Texture
组件的场景PlatformViewLink
的性能优化启用 Hybrid Composition++ (HCPP) (Flutter 3.29+):
\\ndartCopy Code\\n// 在 Flutter 侧强制启用 HCPP\\nFlutterImageView.enableHybridCompositionPlusPlus(true);\\n
\\n通过减少纹理拷贝次数提升 20%-30% 的帧率。
\\nTexture
组件的交互扩展通过 Listener
转发触摸事件:
dartCopy Code\\nTexture(\\n textureId: textureId,\\n child: Listener(\\n onPointerDown: (event) {\\n channel.invokeMethod(\'onTouch\', event.position.dx);\\n },\\n ),\\n)\\n
\\n结合原生端的事件处理实现简单交互。
\\n你提供的示例中使用的 **PlatformViewLink
方案更适合需要完整交互支持的场景**,而直接使用 Texture
组件则是更轻量、高性能的选择。开发者应根据具体需求(交互复杂度、性能要求、维护成本)灵活选择。在 Flutter 3.29+ 中,HCPP 进一步缩小了两者的性能差距,但 Texture
仍是无交互场景的优选方案。
在 Dart 中,聚合类型(Aggregate Types) 和 容器类型(Container Types) 是指能够存储和管理一组数据的类型。它们的核心功能是将多个元素组合成一个整体,便于统一操作和管理。尽管这两个术语有时会被混用,但可以这样理解:
\\nDart 提供了多种容器类型,最核心的包括 List、Set、Map,以及一些衍生类型(如 Queue
)。以下是详细说明:
List 是一种有序、可重复、可变的集合,通过索引访问元素,类似于数组。
\\n0
开始)访问。const
声明不可变列表。// 可变列表\\nList<int> numbers = [1, 2, 3]; // 显式类型\\nvar strings = <String>[\'a\', \'b\', \'c\']; // 类型推导\\n\\n// 不可变列表(使用 const)\\nconst immutableList = const [4, 5, 6]; // 无法修改\\n
\\n方法/属性 | 说明 | 示例 |
---|---|---|
add(element) | 在列表末尾添加元素。 | numbers.add(4); → [1, 2, 3, 4] |
insert(index, element) | 在指定位置插入元素。 | numbers.insert(0, 0); → [0, 1, 2, 3, 4] |
remove(element) | 移除第一个匹配的元素。 | numbers.remove(2); → [1, 3, 4] |
removeAt(index) | 移除指定索引的元素。 | numbers.removeAt(0); → [2, 3, 4] |
length | 获取列表长度。 | print(numbers.length); → 4 |
[] | 通过索引访问或修改元素。 | numbers[0] = 10; → [10, 1, 2, 3] |
addAll(iterable) | 将另一个集合的元素追加到列表末尾。 | numbers.addAll([5, 6]); → [1, 2, 3, 5, 6] |
Set 是一种无序、不可重复、可变的集合,用于存储唯一元素。
\\nconst
声明不可变集合。// 可变集合\\nSet<String> fruits = {\'apple\', \'banana\', \'orange\'}; // 显式类型\\nvar uniqueNumbers = <int>{1, 2, 3}; // 类型推导\\n\\n// 不可变集合(使用 const)\\nconst immutableSet = const {\'apple\', \'banana\'}; // 无法修改\\n
\\n方法/属性 | 说明 | 示例 |
---|---|---|
add(element) | 添加元素,若已存在则返回 false 。 | fruits.add(\'grape\'); → {\'apple\', \'banana\', \'orange\', \'grape\'} |
remove(element) | 移除指定元素。 | fruits.remove(\'banana\'); → {\'apple\', \'orange\', \'grape\'} |
contains(element) | 检查元素是否存在。 | fruits.contains(\'apple\'); → true |
addAll(iterable) | 合并另一个集合的元素(重复元素会被忽略)。 | fruits.addAll({\'apple\', \'mango\'}); → {\'apple\', \'mango\', ...} |
Map 是一种键值对(Key-Value) 的无序集合,通过键快速查找值。键必须唯一,但值可以重复。
\\n// 可变映射\\nMap<String, int> ageMap = {\'Alice\': 30, \'Bob\': 25}; // 显式类型\\nvar scores = <String, double>{\'Math\': 90.5, \'Science\': 85.0}; // 类型推导\\n\\n// 不可变映射(使用 const)\\nconst immutableMap = const {\'name\': \'Alice\', \'age\': 30}; // 无法修改\\n
\\n方法/属性 | 说明 | 示例 |
---|---|---|
putIfAbsent(key, ifAbsent) | 如果键不存在,则添加键值对;否则返回现有值。 | ageMap.putIfAbsent(\'Charlie\', () => 20); → 新增键 \'Charlie\' |
remove(key) | 移除指定键的条目。 | ageMap.remove(\'Bob\'); → 移除键 \'Bob\' |
containsKey(key) | 检查是否存在指定键。 | ageMap.containsKey(\'Alice\'); → true |
addAll(map) | 合并另一个 Map 的键值对(重复键会被覆盖)。 | ageMap.addAll({\'Alice\': 35}); → 键 \'Alice\' 的值被更新为 35 |
Queue
是一种先进先出(FIFO)的容器,需导入 dart:collection
:
import \'dart:collection\';\\n\\nvoid main() {\\n Queue<int> queue = Queue();\\n queue.add(1);\\n queue.add(2);\\n print(queue.removeFirst()); // 输出 1\\n print(queue.first); // 输出 2\\n}\\n
\\n所有容器类型(List、Set、Map)都实现了 Iterable
接口,支持以下操作:
numbers.forEach((num) => print(num));\\nfor (var fruit in fruits) { ... }\\n
\\nvar doubled = numbers.map((x) => x * 2); // [2, 4, 6, 8]\\nvar even = numbers.where((x) => x.isEven); // 过滤偶数\\nvar sum = numbers.reduce((a, b) => a + b); // 求和\\n
\\n类型 | 有序性 | 可重复性 | 存储形式 | 适用场景 |
---|---|---|---|---|
List | ✅ | ✅ | 索引访问 | 需要有序、可重复的元素集合 |
Set | ❌ | ❌ | 唯一元素集合 | 需要去重的场景 |
Map | ❌ | ✅(值可重复) | 键值对(键唯一) | 需要通过键快速查找值的场景 |
Queue | ✅ | ✅ | 先进先出(FIFO) | 需要队列操作的场景(如任务队列) |
不可变容器:
\\nconst
声明的容器(如 const List
、const Set
)无法修改。const colors = const [\'red\', \'green\', \'blue\'];
类型安全:
\\nList<int>
)确保数据一致性。性能考量:
\\nList
的索引访问是 O(1),但频繁插入/删除中间元素可能效率较低。Map
的键查找是 O(1),适合需要快速查找的场景。void main() {\\n // List 示例\\n List<int> numbers = [1, 2, 3];\\n numbers.add(4);\\n print(numbers[0]); // 输出 1\\n\\n // Set 示例\\n Set<String> fruits = {\'apple\', \'banana\'};\\n fruits.add(\'grape\');\\n print(fruits.contains(\'apple\')); // true\\n\\n // Map 示例\\n Map<String, int> ageMap = {\'Alice\': 30};\\n ageMap[\'Bob\'] = 25;\\n print(ageMap[\'Alice\']); // 30\\n}\\n
\\nList
、Set
、Map
。List
。Set
。Map
。位置参数是按 参数在函数定义中的顺序 传递的参数。调用函数时,必须按顺序传递参数,且参数名在调用时不需要指定。
\\n[]
包裹,可以不传或传部分参数。// 定义函数:两个必选参数,一个可选参数\\nvoid printInfo(String name, int age, [String? city]) {\\n print(\'Name: $name, Age: $age\');\\n if (city != null) {\\n print(\'City: $city\');\\n }\\n}\\n\\nvoid main() {\\n // 必须按顺序传递必选参数\\n printInfo(\'Alice\', 30); // 只传必选参数,可选参数不传\\n printInfo(\'Bob\', 25, \'New York\'); // 传必选参数和可选参数\\n}\\n
\\nName: Alice, Age: 30\\nName: Bob, Age: 25\\nCity: New York\\n
\\nint age = 18
,这样不传时会用默认值。命名参数通过 参数名 传递值,调用时通过 参数名: 值
的形式指定,顺序无关,且通常是可选的。
{}
包裹,调用时可选。required
标记(Dart 2.12+)。// 定义函数:两个命名参数,其中 name 是必选(required)\\nvoid printProfile({required String name, String? job, int? age}) {\\n print(\'Name: $name\');\\n if (job != null) {\\n print(\'Job: $job\');\\n }\\n if (age != null) {\\n print(\'Age: $age\');\\n }\\n}\\n\\nvoid main() {\\n // 必须传递必选参数 name,其他可选\\n printProfile(name: \'Charlie\'); // 只传必选参数\\n printProfile(name: \'David\', age: 30, job: \'Engineer\'); // 任意顺序\\n printProfile(job: \'Artist\', name: \'Eve\'); // 参数顺序无关\\n}\\n
\\nName: Charlie\\nName: David\\nJob: Engineer\\nAge: 30\\nName: Eve\\nJob: Artist\\n
\\nrequired
标记的参数必须传递。特性 | 位置参数 | 命名参数 |
---|---|---|
传递方式 | 按顺序,无需参数名 | 通过参数名,顺序无关 |
必选参数 | 必须按顺序传递 | 必须用 required 标记 |
可选参数 | 用 [] 包裹,顺序固定 | 用 {} 包裹,无需固定顺序 |
可读性 | 参数多时易混淆 | 参数多时更清晰(通过参数名) |
默认值 | 可设置默认值 | 可设置默认值 |
add(1, 2)
)。void greet({String name = \'Guest\'}) {\\n print(\'Hello, $name!\');\\n}\\ngreet(); // 输出:Hello, Guest!\\n
\\nvoid printDetails(String name,int? age,String? city) {\\n print(\'Name: $name\');\\n if (age != null) {\\n print(\'Age: $age\');\\n }\\n if (city != null) {\\n print(\'City: $city\');\\n }\\n}\\n
\\n// 综合使用位置参数和命名参数\\nvoid printUser({\\n required String name, // 必填参数需标记 required\\n String? job, // 可选命名参数\\n int? age,\\n bool? isStudent,\\n}) {\\n print(\'Name: $name\');\\n if (job != null) {\\n print(\'Job: $job\');\\n }\\n if (age != null) {\\n print(\'Age: $age\');\\n }\\n if (isStudent != null) {\\n print(\'Is Student: $isStudent\');\\n }\\n}\\n\\n\\nvoid main() {\\n\\nprintUser(name: \'Alice\'); // 输出:Name: Alice\\nprintUser(name: \'Bob\', job: \'Engineer\'); // 输出:Name: Bob, Job: Engineer\\nprintUser(\\n name: \'Charlie\',\\n job: \'Developer\',\\n age: 28,\\n isStudent: false,\\n);\\n}\\n
\\n在做Android开发的时候经常会使用Popupwindow来让一个弹框显示在某个View的上方或者下方,但是当显示位置不足时,Popupwindow会自动调整位置来让内容完整显示。最近在flutter开发中也有响应的需求,所以就按照Android组件的思路封装了一个flutter版本的PopupWindow.GitHub源码在最下方,需要的同学们自取就OK
\\n先看效果图:\\n
使用方法:
\\n1.基础用法(不推荐,后续有更简便用法)
\\n想要点击按钮后在按钮的上方显示
ElevatedButton(\\n key: _aboveKey,\\n child: const Text(\'show above the button\'),\\n onPressed: () => _showAboveWindow(),\\n),\\n
\\n需要先计算出锚点widget在屏幕上的Y坐标,然后创建PopupWindow,
\\nvoid _showAboveWindow() {\\n //需要先找到锚点widget\\n final renderBox = _aboveKey.currentContext?.findRenderObject() as RenderBox?;\\n if (renderBox == null) {\\n return;\\n }\\n final offset = renderBox.localToGlobal(Offset.zero);\\n \\n _aboveWindow ??= DefaultPopupWindow(\\n context: context,\\n //展示在目标上方\\n position: PopupWindowPosition.top,\\n //传入锚点widget顶部的Y坐标\\n anchorY: offset.dy,\\n offset: Offset.zero,\\n barrierColor: Colors.yellow.withOpacity(0.5),\\n //弹框展示的内容是child\\n child: GestureDetector(\\n onTap: () => _aboveWindow?.dismiss(),\\n child: Material(\\n child: Container(\\n color: Colors.red,\\n padding: const EdgeInsets.all(20),\\n width: MediaQuery.of(context).size.width,\\n child: const Text(\\n \'This is the PopupWindow Content\',\\n style: TextStyle(fontSize: 20),\\n ),\\n ),\\n ),\\n ),\\n );\\n _aboveWindow?.show();\\n}\\n
\\n2.简便用法(推荐用法,代码量极少):
\\n用PopupWindowWrapper来包裹住锚点widget
PopupWindowWrapper(\\n controller: _controller,//默认可以不传,可以在任何地方让弹框展示或者隐藏\\n windowPosition: PopupWindowPosition.bottom,//展示在目标下方,默认就是下方\\n windowBarrierDismissible: false,//点击空白区域是否可关闭,默认可以关闭\\n //弹框内显示的内容\\n windowContent: Material(\\n child: GestureDetector(\\n //让弹框切换状态(展示就隐藏,隐藏就展示),如果windowBarrierDismissible=true,可以不用\\n onTap: () => _controller.switchStatus(),\\n child: Container(\\n color: Colors.red,\\n padding: const EdgeInsets.all(20),\\n width: MediaQuery.of(context).size.width,\\n child: const Text(\\n \'This is the PopupWindow Content\',\\n style: TextStyle(fontSize: 20),\\n ),\\n ),\\n ),\\n ),\\n //目标组件,弹框会显示在该组件的下方\\n child: Container(\\n padding: const EdgeInsets.all(20),\\n decoration: BoxDecoration(\\n border: Border.all(color: Colors.blue, width: 2),\\n borderRadius: const BorderRadius.all(Radius.circular(50)),\\n ),\\n child: const Text(\'show under the button\'),\\n ),\\n),\\n
\\n开发思路
\\n主要Class关系
GitHub源码地址:github.com/cgztzero/Fl…
\\n希望各位同学报团取暖,延续职业生涯~
在继承关系中,子类未正确调用父类构造函数,导致以下错误:
\\nThe superclass \'StudentBase\' doesn\'t have a zero argument constructor.\\n
\\nStudentBase
的构造函数是位置参数(如 StudentBase(String name, ...)
),而子类 Student
未显式调用父类构造函数。StudentBase()
),子类必须通过 super()
显式传递参数。class Student extends StudentBase {\\n final String school;\\n \\n // 显式传递父类参数\\n Student(String name, double weight, double height, {required this.school})\\n : super(name, weight, height); // 必须调用父类构造函数\\n\\n @override\\n String info() {\\n return \'${super.info()}, 学校: $school\';\\n }\\n}\\n
\\nclass StudentBase {\\n StudentBase({required this.name, required this.weight, required this.height}); // 命名参数\\n}\\n\\nclass Student extends StudentBase {\\n final String school;\\n\\n // 直接传递父类的命名参数\\n Student({required super.name, required super.weight, required super.height, required this.school});\\n}\\n
\\nsuper-parameters
特性未启用使用 super.name
语法时,出现以下错误:
This requires the \'super-parameters\' language feature to be enabled.\\n
\\nsuper-parameters
是 Dart 2.17 引入的特性,需满足以下条件:\\npubspec.yaml
中设置最小 SDK 版本。# 升级 Flutter(包含 Dart)\\nflutter upgrade\\n\\n# 检查版本\\ndart --version # 确保输出版本 ≥ 2.17.0\\n
\\npubspec.yaml
environment:\\n sdk: \\">=2.17.0 <4.0.0\\" # 设置最小 SDK 版本\\n
\\nflutter pub get # 或 dart pub get\\n
\\nclass StudentBase {\\n StudentBase({required this.name, required this.weight, required this.height});\\n}\\n\\nclass Student extends StudentBase {\\n final String school;\\n\\n // 使用命名参数传递父类参数\\n Student({required super.name, required super.weight, required super.height, required this.school});\\n}\\n
\\n运行 flutter pub get
后,发现部分依赖项有新版本但无法自动升级:
9 packages have newer versions incompatible with dependency constraints.\\n
\\nflutter pub outdated # 查看具体冲突的包\\n
\\n在 pubspec.yaml
中放宽版本约束:
dependencies:\\n async: ^2.13.0 # 从 ^2.12.0 升级\\n http: ^1.3.0 # 从 ^0.13.6 升级\\n
\\nflutter pub upgrade # 自动解决兼容版本\\n
\\nChangelog
。pub deps
查看依赖树,定位冲突来源。构造函数调用:
\\nSDK 版本管理:
\\nsuper-parameters
需 Dart 2.17+。pubspec.yaml
设置最小 SDK 版本。依赖项更新:
\\npub outdated
分析冲突。super-parameters
未启用 → 检查 SDK 版本和 pubspec.yaml
。pub outdated
并调整版本约束。// StudentBase 类(命名参数)\\nclass StudentBase {\\n String name;\\n double weight;\\n double height;\\n\\n StudentBase({required this.name, required this.weight, required this.height});\\n\\n String info() {\\n return \\"StudentBase: name: $name, weight: $weight kg, height: $height cm\\";\\n }\\n}\\n\\n// Student 子类\\nclass Student extends StudentBase {\\n final String school;\\n\\n Student({\\n required super.name,\\n required super.weight,\\n required super.height,\\n required this.school,\\n });\\n\\n @override\\n String info() {\\n return \'${super.info()}, 学校: $school\';\\n }\\n}\\n\\nvoid main() {\\n // 创建实例\\n Student student = Student(\\n name: \\"张三\\",\\n weight: 60,\\n height: 175,\\n school: \\"XX学校\\",\\n );\\n print(student.info()); \\n // 输出:StudentBase: name: 张三, weight: 60 kg, height: 175 cm, 学校: XX学校\\n}\\n
\\n不到一年的时间,JetBrains 又要对 Terminal 「大刀阔斧」,本次发布的新终端是重构后的全新的架构,而上一次终端大调整还是去年 8 月的 v2024.2 版本,并且在「Android Studio Ladybug | 2024.2.1」也被引入。
\\n\\n\\n不知道你们用不用内置终端,反正我是用的,不到一年的时间就又重构了,所以有时候不是 Android Studio 团队喜欢写 bug ,而是 JetBrains 的坑太多。
\\n
一直以来,JetBrains IDE 都附带了一个基于 JediTerm 的内置终端,这是一个 Java 终端仿真器,符合标准的 xterm/VT100 环境。
\\n而在 2024.2 中的新终端,JetBrains 为了引入 AI ,重新设计了这个「终端老古董」,为此引入了增强功能,主要包含改进包括 AI 驱动的命令生成,允许开发者用自然语言描述命令并让 AI 创建命令。
\\n此外, 2024.2 里终端还可以在单行或双行设置之间进行选择,从而提高空间利用率或可读性,并且支持自定义 shell 提示设置,最后 Git 别名和分支、npm 包、PHP 命令和 Ruby CLI 的命令完成功能也得到了增强:
\\n而从 JetBrains IDE 的 2025.1 版本开始,重新设计的终端架构将替代原有的设计,而之所以重新设计,只能说上一个更新给自己挖的坑太大:
\\n\\n\\n在上一个版本,为了搞 AI 支持,终端不会让 shell 直接处理行编辑和快捷方式,而是拦截 IDE 中的用户输入(例如击键和提示文本),并且仅在用户按 Enter 时才向 shell 发送命令。
\\n
JetBrains 当时的预期是「为 IDE 级别的 AI 或基于弹出窗口的自动完成等未来功能铺平道路」,但是却忽略了最重要的兼容性问题。
\\n\\n\\n产品想象很美好,落地后一地鸡毛。
\\n
2024.2 的终端发布后,陆续就收到各种负面反馈,核心就在于 JetBrains 拦截了输入 :
\\n而基于这个 2024.2 的设计破坏了基本的 shell 工作流程,大量的负面报告表明,大多数开发人员无法接受严重偏离既定的类似 POSIX 的终端标准的方法,所以 JetBrains 承认自己这次是「拉了泡大的」。
\\n所以 2025 这次再重构,核心就是兼容性和一致性需要回归第一位,例如:
\\n所以在 2025.1 版本,重构的终端进行了进一步调整:
\\n而在兼容性和性能完成目标之后,未来在不牺牲速度或一致性的情况下再加入:
\\n\\n\\n核心就是:创新绝不能破坏内核兼容性
\\n
最后,可以对比三个终端的性能效果:Top(classic terminal), Center:(terminal 「2023–2024」), Bottom: (reworked terminal 「2025」 ):
\\n本次重构说是重构,更像是先改为原来的经典支持,然后再进行新功能的集成实现,换一条路来避免继续呆在坑里。
\\n那么,你会用 IDEA/AS 内置的终端吗?
\\n\\n\\n每一年 Google Flutter 团队都会发布一份产品路线图,包括 Flutter 框架和 Dart 编程语言,让开发者能够了解官方团队的优先事项,并据此做出自己的计划安排。
\\n产品路线图也会随着客户反馈和新兴市场机会的变化而不断发展。开发者们可以通过每季度的调查问卷以及 GitHub 上 issue 的反馈来推进这些工作的优先级。
\\n\\n
这份路线图是我们希望实现的愿景目标,主要由我们这些在 Google 任职、从事 Flutter 项目的成员整理而成。值得注意的是,目前社区中的非 Google 贡献者数量已经超过了 Google 内部开发者,因此这并不是涵盖所有未来发展方向的完整列表。
\\n正如在整个软件行业中常见的那样,准确预测工程进度总是具有挑战性的,尤其是对于一个开源项目来说更是如此。因此,请将这份路线图视为我们的“意图声明”,而非完成工作的承诺。
\\n在 2024 年,我们完成了多个移动平台(iOS 和 Android)上关键无障碍场景的验证。
\\n2025 年,我们计划将重点转向 Web 平台上的无障碍支持。
\\n我们会继续聚焦于 Impeller 引擎带来的质量和性能提升:
\\n在 iOS 上,我们计划 彻底迁移到 Impeller ,引入的变化包括 移除 Skia 后端 。
\\n在 Android 上,我们将优先关注运行 Android API 等级 29 (Android 10) 及以上的设备 ,并计划在这些设备上默认启用 Impeller。考虑到 2024 年旧设备上存在的问题,目前我们仍将保留对 Skia 的支持。
\\niOS : 持续适配即将发布的 iOS 19 与 Xcode 17,完成对 Swift Package Manager(SwiftPM)的支持,并计划在 2025 年晚些时候将其设为默认选项。
\\nCupertino 支持 : 持续改进 Cupertino 组件,使其更贴合 Apple 的 Human Interface Guidelines。
\\nAndroid : 探索 Android 16 的主要新特性,并将 Gradle 构建脚本从 Groovy 迁移至 Kotlin,提升构建工具的单元测试覆盖率。
\\n平台互操作性 : 持续开展实验性工作,支持从 Dart 直接调用原生平台代码,包括:
\\niOS 上的 Objective-C 和 Swift;
\\nAndroid 上的 Java 和 Kotlin;
\\n特别是主线程限定 API 的调用支持。
\\n2024 年我们在 Web 性能和质量方面取得了重大进展,包括应用体积缩小、多线程利用提升以及更快的加载速度。
\\n2025 年,我们将继续深化以下方面的能力:
\\n无障碍支持;
\\n文本输入体验;
\\n国际化文本渲染;
\\n应用体积和整体性能;
\\n平台集成能力;
\\n使用 WebAssembly (Wasm) 编译进一步提升性能。
\\n我们已经完成了支持 JS 与 Wasm 编译的新 Dart JS 互操作机制。接下来,我们计划在 2025 年正式移除旧版 HTML 与 JS 库(请关注破坏性变更公告)。
\\n此外,Web 平台的热重载(Hot Reload)也已取得显著进展,预计将在 2025 年正式推出。
\\n2025 年,Google Flutter 团队将继续专注于移动和 Web 平台的支持。
\\n与此同时,Canonical Flutter 团队将继续负责桌面平台的研发,包括:
\\n我们正在研究一系列框架层面的调整,目标是减少 Flutter Widget 代码中不必要的冗长写法,提高开发效率。
\\n我们将继续整合 AI 解决方案,为开发者提供核心编程任务的智能辅助。
\\n我们也会持续投资于 Flutter 的工具链,包括:
\\n此外,我们还将继续优化开发体验中的 “编辑-刷新” 循环(Edit-Refresh Cycle)。
\\nbuild_runner
的代码生成支持能力。我们计划重构 Dart 分析器(analyzer)与前端编译器,使它们可以共享更多底层实现。这将有助于:
\\n我们还将探索跨平台 AOT 编译能力,例如:在 macOS 开发机上编译 Linux 平台的 Dart AOT 可执行文件。
\\n目前我们仍不打算为以下功能提供官方支持:
\\n代码热更新(Code Push):\\n推荐关注社区解决方案 shorebird.dev。
\\nUI 热更新 / 服务端驱动 UI(Server-driven UI):\\n推荐使用 rfw 包。
\\n新增支持平台:\\n我们暂无计划扩展 Flutter 的官方支持平台列表。
\\n📮 欢迎转发、收藏、留言讨论:你对 Flutter 哪个方向最关注?你希望社区在哪些方面投入更多?
\\n👉 来评论区聊聊!
","description":"每一年 Google Flutter 团队都会发布一份产品路线图,包括 Flutter 框架和 Dart 编程语言,让开发者能够了解官方团队的优先事项,并据此做出自己的计划安排。 产品路线图也会随着客户反馈和新兴市场机会的变化而不断发展。开发者们可以通过每季度的调查问卷以及 GitHub 上 issue 的反馈来推进这些工作的优先级。\\n\\n原文:github.com/flutter/flu…\\n\\n这份路线图是我们希望实现的愿景目标,主要由我们这些在 Google 任职、从事 Flutter 项目的成员整理而成。值得注意的是,目前社区中的非 Google…","guid":"https://juejin.cn/post/7490854415047999526","author":"Flutter社区","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-09T03:51:14.043Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fd1d0a13a7334a17a2b699ce46ca6af6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRmx1dHRlcuekvuWMug==:q75.awebp?rk3s=f64ab15b&x-expires=1744775473&x-signature=ugEKLwG6nUl3kUBI64EPHbqiSqc%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"你为什么需要了解 Dart AST?一个简单的 bug 带你快速认识下 Dart Kernel AST","url":"https://juejin.cn/post/7490777239878795305","content":"事情的起因是最近在 Github 收到了一个 issue ,内容是在获取 l10n
多语言相关实现时找不到该方法,从而导致 NoSuchMethodError
的问题:
而出现问题的地方是通过 context.l10n
方式获取当前的多语言文本内容,但是这个用法在同个文件内的其他地方又是正常:
而 context.l10n
这个实现,是通过 Dart 的 extension
拓展 BuildContext
来完成,并且返回时为了方便会通过 !
来强行忽略空问题:
但是虽然知道了问题的点在于 context
获取不到 l10n
,但是一时半会也没看出来代码哪里有问题,因为这是一个正常的 context
,使用的位置也正常,通过这个 context
没理由获取不到 AppLocalizations
多语言对象,并且 l10n
在同个文件其他地方都是正常,甚至 debug 时通过这个 context 执行 AppLocalizations.of(context)
是可以正常获取 ,那为什么在这里就不行?
接着我用 Cursor 和 Trae 针对这个问题做了一系列提问,但是 AI 们给出的答案基本毫无帮助,基本是让你做个判空处理,甚至还有严重跑偏和幻觉的情况:
\\n\\n\\n深度嵌套明显就是一个瞎编的思路,因为
\\nInheritedWidget
的共享是通过 Element 内的一个 map 来存储。
解决问题不难,但是我们需要知道这个 NoSuchMethodError
的根本原因,从根上去 fix 才是我们的目标,所以既然代码看不到问题,那就只能通过编译后的 Kernel AST 代码来看看是否有灵感,而通过 dump 调试模式下的 dill 文件,然后找到对应方法,还真就发现了端倪:
可以看到,因为 _renderUserInfo
函数在声明时没有给 context 指定 BuildContext
,所以编译后它是一个 dynamic
类型,从而导致后续 context.l10n
也出现类型不对,无法正确被编译为 AppLocalizations
导致的一系列问题:
那么知道问题就简单了,给 _renderUserInfo
函数的 context 指定 BuildContext
之后,如下图所示,编译后可以看到,对应的 context.l10n
拓展引用,在 AST 里变成了 LocalizationExtension
和 Applocalizations
的相关实现:
从 Kernel 代码里可以看到,对应的 extension
拓展 BuildContext
实现能匹配上前面的引用,所以在添加了明确的 BuildContext
声明之后, context.l10n
的灵异 NoSuchMethodError
问题得以解决。
其实这个问题很简单,更多是在编写函数时不规范声明导致,因为这里需要用到的是 context 的 extension
实现,但是如果不对函数的 context 给予显式的 BuildContext
声明,那就算我们使用时传入的是 BuildContext
,但是编译时 context 因为没有明确类型声明,就会被判断成 dynamic ,从而无法正确匹配它的 extension
实现。
虽然这种 bug 很简单,但是很容易让人忽略问题的本质,而通过 AST 来验证问题,就是发现问题根本的手段之一,所以接下来,我们也简单快速地认识下 Dart 的 Kernel AST 。
\\n在过去的内容里,我们一直说因为 Dart 2.0 之后就不再支持直接从源码运行,对于 Dart 代码现在会统一编译成一种「预处理」形式的二进制 dill 文件,一般称它会 Kernel AST 文件,那么其实这个二进制文件究竟是什么?要如何查看?
\\n首先 dill 文件本身已经是二进制文件,所以如果想要查看具体内容,我们还是需要将其 dump 成文本才方便查看,在 Dart SDK 里就有对应的 ast_to_text.dart 的相关工具,而 Dart 也给我们提供了对应的脚本 dump_kernel.dart。
\\n通过下方命令,我们可以将 dill 文件反编译为 Kernel 代码文本,从而方便查阅,但是使用这个脚本也有相对应的前提:
\\ndart pkg/vm/bin/dump_kernel.dart xxxxxx/app.dill xxxxxx/app.dill.txt \\n
\\n因为 dump_kernel.dart 需要在全量的原始 Dart SDK 里才能运行,并且想要使用 dump_kernel,你就需要 depot_tools 工具,depot_tools 是 Chromium 的源码管理工具,同时也需要对应的环境支持:
\\nexport PATH=/Users/xxxxx/workspace/depot_tools:$PATH
fetch dart
,会比较耗时,大概几个 G 的大小dart pkg/vm/bin/dump_kernel.dart xxxxxx/app.dill xxxxxx/app.dill.txt
去 dump kernel ,这里的 pkg/vm/bin/dump_kernel.dart
路径就是前面 sdk 下的路径。之后你就可以通过 dump_kernel 查看对应的 dill 文件了,我们先看一个简单案例,以下是一个很普通的 Dart 代码:
\\nadd(int a, int b) async{\\n await Future.delayed(Duration(seconds: 1));\\n return a + b;\\n}\\nvoid main() {\\n print(add(1, 2));\\n}\\n
\\n而下方是上面代码编译后的 dill 文件的反编译输出:
\\nlibrary from \\"file:///Users/guoshuyu/workspace/main.dart\\" as main {\\n\\n import \\"dart:async\\";\\n\\n static method add(core::int a, core::int b) → dynamic async /* emittedValueType= dynamic */ {\\n await asy::Future::delayed<dynamic>(new core::Duration::•(seconds: 1));\\n return a.{core::num::+}(b){(core::num) → core::int};\\n }\\n static method main() → void {\\n core::print(main::add(1, 2));\\n }\\n}\\n
\\n可以看到,此时的 kernel 其实并没有什么编译优化,例如 async 语法糖就没有被展开为状态机,从 dill 代码上看,它基本保留了原始代码的信息,仅仅只是针对添加了一些信息补充,例如:
\\nlibrary from
针对说明了这部分代码的来源,这些在源代码中是隐式的,但在内核中需要显式声明core::int
,也就是完全限定名称,明确来自 dart:core
库,在 Kernel dill 里所有类型和函数都使用完全限定名称a + b
在内核中表示为 a.{core::num::+}(b){(core::num) → core::int}
,明确了操作符的来源和类型,加法操作符来自 num
类,类型签名为 (core::num) → core::int
static method
所以可以看出来,AST 一般是源代码的抽象语法结构的树状表现形,而转化为 AST 是为了更适合程序分析,在 Dart 里,Kernel AST 因为不包含解析后的各种类和函数,所以它虽然是二进制,但是包含详细信息,从而可以在不同平台之间移植。
\\n\\n\\n这么看,Dart 的 Kernel AST 是更精确的 IR (中间表示) 。
\\n
另外, Kernel AST 在这里属于 IR 的存在,在后续还有 IL 和 Optimized SSA IL 等处理才会变成 Machine Code ,例如上面的 a+b ,在 Optimized SSA IL 阶段理论上会处理为 smi (small integers) :
\\n\\n\\nsmi 的作用是优化性能,它是 Dart VM 的直接对象,使用 smi 表示 Dart VM 可以避免创建完整对象,从而减少内存分配和垃圾回收的开销,特别是在「加减乘除」上可以直接在这些标记指针上执行从而显著提升执行速度。
\\n
另外,在 dill 文件里你还会看到很多 @#C1
、 @#C200
之类的标记,其中 C 就是 constants 的意思,也就是在 Kernel 文件里,它会把一些可以常量化的特定值通过标记统一起来,在使用的地方只留下标记,从而尽可能缩减大小,例如这里的 override
:
另外我们再看一个代码,这里主要是在 Cat
里通过 covariant 显式声明参数是协变的,允许子类方法接受更具体的类型:
class Animal {\\n void chase(Animal x) { }\\n}\\n\\nclass Mouse extends Animal { }\\n\\nclass Cat extends Animal {\\n @override\\n void chase(covariant Mouse x) { }\\n}\\n
\\n编译后可以看到,此时 dill 多了一些其他东西:
\\nsynthetic constructor
主要是标识合成构造器,是指由编译器自动生成的构造器,因为我们没有对类显式声明构造器covariant-by-declaration
,表明它在声明时使用了 covariant
,确保运行时类型检查遵循协变规则class Animal extends core::Object {\\n synthetic constructor •() → main::Animal\\n : super core::Object::•()\\n ;\\n method chase(main::Animal x) → void {}\\n}\\nclass Mouse extends main::Animal {\\n synthetic constructor •() → main::Mouse\\n : super main::Animal::•()\\n ;\\n}\\nclass Cat extends main::Animal {\\n synthetic constructor •() → main::Cat\\n : super main::Animal::•()\\n ;\\n @#C1\\n method chase(covariant-by-declaration main::Mouse x) → void {}\\n}\\n
\\n同理还有下面的 covariant Cat? child;
,通过显式的协变声明,可以让代码在编译时限定更小范围,省略类似 x is Mouse
之类的检查:
class Animal {\\n Animal? child;\\n}\\n\\nclass Cat extends Animal {\\n @override\\n covariant Cat? child;\\n}\\n\\n--------------------------------------------------------------------------\\n\\nclass Animal extends core::Object {\\n field main::Animal? child = null;\\n synthetic constructor •() → main::Animal\\n : super core::Object::•()\\n ;\\n}\\nclass Cat extends main::Animal {\\n @#C1\\n covariant-by-declaration field main::Cat? child = null;\\n synthetic constructor •() → main::Cat\\n : super main::Animal::•()\\n ;\\n}\\n
\\n\\n\\n其实这里指出这个例子,是因为日常开发里很少人会用到
\\ncovariant
,而在 Kernel 文件里会有很多covariant-by-declaration
标记,而通过上面例子,你就知道它的来源和作用。
相对应的还是有 covariant-by-class
,如下代码所示,在 Dart 里,当子类继承泛型父类并特化其类型参数,会导致方法参数类型在子类中协变时,编译器会在 Kernel AST 中生成 covariant-by-class
声明:
class Animal {}\\nclass Cat extends Animal {}\\n\\nclass Handler<T extends Animal> {\\n void handle(T obj) {}\\n}\\n\\nclass CatHandler extends Handler<Cat> {\\n @override\\n void handle(Cat obj) {} // 隐式协变,编译为 covariant-by-class\\n}\\n
\\n如下所示就是编译后的情况,这种情况主要发生在父类方法参数的类型由泛型参数定义,而子类通过指定更具体的泛型类型,使得参数类型发生隐式协变:
\\n泛型父类定义:Handler<T>
的 handle
方法接受类型 T
(约束为 Animal
的子类)
子类特化泛型参数:CatHandler
继承 Handler<Cat>
,将 T
特化为 Cat
参数类型协变:子类 handle
方法的参数类型 Cat
是父类泛型参数 T
的具体化,当通过父类引用(如 Handler<Animal>
)调用时,传入的参数需动态检查是否为 Cat
所以 Dart 编译器自动标记此参数为 covariant-by-class
,从而在启用运行时类型检查,确保类型安全:
class Animal extends core::Object {\\n synthetic constructor •() → main::Animal\\n : super core::Object::•()\\n ;\\n}\\nclass Cat extends main::Animal {\\n synthetic constructor •() → main::Cat\\n : super main::Animal::•()\\n ;\\n}\\nclass Handler<T extends main::Animal> extends core::Object {\\n synthetic constructor •() → main::Handler<main::Handler::T>\\n : super core::Object::•()\\n ;\\n method handle(covariant-by-class main::Handler::T obj) → void {}\\n}\\nclass CatHandler extends main::Handler<main::Cat> {\\n synthetic constructor •() → main::CatHandler\\n : super main::Handler::•()\\n ;\\n @#C1\\n method handle(covariant-by-class main::Cat obj) → void {}\\n}\\nstatic method add(core::int a, core::int b) → core::int {\\n return a.{core::num::+}(b){(core::num) → core::int};\\n}\\nstatic method main() → void {\\n core::print(main::add(1, 2));\\n}\\n
\\n另外,在如下所示代码里,因为传递的是 Function ,所以会生成了一个 tearoff,从而在编译时会出现一些变化:
\\nextension FutureExtensions on Future {\\n void onError(Function handler) {\\n catchError(handler);\\n }\\n}\\n\\nvoid main() {\\n var future = Future.value(42);\\n var fn = future.onError; \\n}\\n
\\n可以看到,onError
多了 method tearoff
的声明,#get
表示可以直接引用,而 method tearoff
在这里提供了一种简洁的方式 get 来传递方法引用,从而在这里显性声明了对应的 tearoff:
library from \\"file:///Users/guoshuyu/workspace/main.dart\\" as main {\\n\\n import \\"dart:async\\";\\n\\n extension FutureExtensions on asy::Future<dynamic> {\\n method onError = main::FutureExtensions|onError;\\n method tearoff onError = main::FutureExtensions|get#onError;\\n }\\n static extension-member method FutureExtensions|onError(lowered final asy::Future<dynamic> #this, core::Function handler) → void {\\n #this.{asy::Future::catchError}(handler){(core::Function, {test: (core::Object) →? core::bool}) → asy::Future<dynamic>};\\n }\\n static extension-member method FutureExtensions|get#onError(lowered final asy::Future<dynamic> #this) → (core::Function) → void\\n return (core::Function handler) → void => main::FutureExtensions|onError(#this, handler);\\n static method main() → void {\\n asy::Future<core::int> future = asy::Future::value<core::int>(42);\\n (core::Function) → void fn = main::FutureExtensions|get#onError(future);\\n }\\n}\\n
\\n另外,可以看到 extension
后的 FutureExtensions
,也是通过显式创建出来了两个 static extension-member
方法,其中一个 get#onError
是返回函数的 tear-off 而存在。
所以在 dill 文件里,也可以看到不少平时你很少接触的东西,最后,通过以下代码我们也可以快速理解 dill 文件的层级结构顺序:
\\n所以,到这里我们可以看出来,二进制的 Kernel AST 文件更多只是在原有代码的基础上补充了更多详细信息,然后编译为二进制 IR ,等待被处理为特定平台如 arm64 的 IL (中间语言) 文件。
\\n在 VM 处理 Kernel 文件的时候,会有 AST 转为 CFG(control flow graph) 的过程,其中 CFG 会由填充了 IL 指令的基本块组成,这个阶段使用的 IL 指令类似于基于堆栈的虚拟机的指令,而后经历对应的优化,比如前面说的 smi ,最终才转化为对应的机械码:
\\n在这个过程里很多代码和指令都是动态生成,这也导致过去在 iOS 18.4 beta1 的时候,由于 Apple 突然封杀了运行时通过 mprotect 动态修改内存访问权限,导致 Flutter 的 debug 和 hotload 无法工作的愿意,当然这个问题 iOS 18.4 beta2 的时候 Apple 又放开了。
\\n\\n\\n而如果是 AOT 模式,基本 snapshot 里就包含有了所有需要生成的指令和代码。
\\n
所以看懂 Kernel AST 没什么太大作用,它更多是让你知道更详尽的代码结构而已,不像以前曾经「上古时代」编译后的代码,在 dill 里 late
、 extension
、async
都可以更直观看到它的优化结构:
所以,这个角度考虑,现在的 dill 其实只是源代码的二进制表现形式,所以虽然它是二进制,但是还是需要 JIT 运行时的预热和动态生成,最终才可以达到运行峰值。
\\n\\n\\n当然,JIT 运行慢绝大多数问题不是因为这个,实际上导致慢的主要原因是因为 Flutter 框架里有着许多一致性检查/断言,而这些导致性能极具下降的检查/断言仅在 debug 模式下启用,这才是缓慢的主要来源,从理论峰值性能考虑,JIT 性能其实并不会输于 AOT,只是它需要预热这个性质,在 UI 场景导致的不可预测性和等待时间并不合适。
\\n
不过就像最初的问题一样,一些实现其实也能在 AST 里体现,比如 extension
展开支持,所以理解 AST 一般灭什么特别作用,但是也许哪天你就用上了呢~
如果你尚未安装 Flutter,请先下载并解压 SDK:
\\n访问 Flutter 官网 下载 Mac 版本的 SDK\\n解压到指定目录,例如:
\\nunzip ~/Downloads/flutter_macos_arm64.zip -d ~/
\\n打开 zsh 配置文件 :
\\n\\n\\nnano ~/.zshrc
\\n
添加 Flutter 路径 :在文件末尾添加以下内容(假设 Flutter 解压到 ~/flutter):
\\n\\n\\nexport PATH=\\"$PATH:~/flutter/bin\\"
\\n
保存并退出 :按 Ctrl + X,然后按 Y 确认保存。
\\n\\n\\nsource ~/.zshrc
\\n
\\n\\nflutter --version
\\n
如果输出类似以下内容,说明配置成功:
\\nFlutter 3.x.x • channel stable • ...
\\n\\n\\nchmod +x ~/flutter/bin/flutter
\\n
\\n\\nflutter doctor
\\n
根据提示安装缺失的依赖。
\\n\\n\\nchmod +x ~/flutter/bin/flutter
\\n
参考链接,配置你的机器使用国内镜像
\\n访问 Android SDK 独立工具页面 ,下载 Command line tools only (例如 commandlinetools-mac-xxx_latest.zip)。
\\n确保解压后的 cmdline-tools 的目录结构正确。正确的结构应为:
\\n如果解压后的目录缺少层级,手动调整:
\\n\\n\\nmkdir -p $ANDROID_HOME/cmdline-tools/latest
\\n
\\n\\nmv ~/Downloads/cmdline-tools/* $ANDROID_HOME/cmdline-tools/latest/
\\n
通过终端使用 sdkmanager 安装必要组件:
\\n\\n\\ncd $ANDROID_HOME/cmdline-tools/latest/bin
\\n
\\n\\n./sdkmanager --sdk_root=$ANDROID_HOME \\"platform-tools\\" \\"platforms;android-34\\" \\"build-tools;34.0.0\\"
\\n
(将 android-34 和 34.0.0 替换为你需要的版本)
\\n\\n\\nchmod +x $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager
\\n
\\n\\n./sdkmanager --licenses
\\n
按提示输入 y 接受所有协议。
\\n将 Android SDK 路径添加到 shell 配置文件(如 ~/.zshrc 或 ~/.bash_profile):
\\n\\n\\necho \'export ANDROID_HOME=\\"$HOME/Library/Android/sdk\\"\' >> ~/.zshrc
\\necho \'export PATH=\\"$PATH:$ANDROID_HOME/platform-tools\\"\' >> ~/.zshrc
\\necho \'export PATH=\\"$PATH:$ANDROID_HOME/cmdline-tools/bin\\"\' >> ~/.zshrc
\\nsource ~/.zshrc
\\n
在手机上启用 开发者选项 和 USB 调试 :
\\n进入 设置 > 关于手机 ,连续点击 版本号 7 次。
\\n返回设置,进入 系统 > 开发者选项 ,启用 USB 调试 。
\\n通过 USB 连接手机到 Mac,终端输入 flutter devices 确认设备被识别。
\\n使用以下命令初始化项目(将 your_project_name 替换为你的项目名):
\\n\\n\\nflutter create your_project_name
\\n
项目名建议使用小写字母和下划线(如 my_flutter_app)。\\n默认会生成一个包含示例代码的项目。
\\n\\n\\ncd your_project_name
\\n
连接设备(真机或模拟器)后执行:
\\n\\n\\nflutter run
\\n
如果手机被正确识别,应用会自动编译并安装到设备。首次运行会自动下载依赖,可能需要等待几分钟。
\\n使用命令行工具手动安装指定版本的 NDK:
\\n\\n\\nsdkmanager --list | grep \\"ndk\\"
\\n
\\n\\nsdkmanager --install \\"ndk;26.3.11579264\\"
\\n
如果安装失败,可能需要先接受许可证:
\\n\\n\\nyes | sdkmanager --licenses
\\n
安装完成后,清理并重新构建
\\n\\n\\nflutter clean
\\nflutter pub get
\\nflutter run
\\n
sdkmanager 依赖 Java 8 或更高版本,安装 Java 环境
\\n这是因为Gradle依赖下载缓慢或失败。将其替换为国内镜像,在项目android/build.gradle中修改仓库地址,同时,在 android/build.gradle.kts 中指定 Kotlin 版本(与 Flutter 兼容):
\\nbuildscript {\\n ext.kotlin_version = \\"1.9.23\\" // 与 Flutter 适配的版本\\n repositories {\\n // 将google()和mavenCentral()替换为:\\n maven { url \'https://maven.aliyun.com/repository/google\' }\\n maven { url \'https://maven.aliyun.com/repository/central\' }\\n }\\n}\\nallprojects {\\n repositories {\\n maven { url \'https://maven.aliyun.com/repository/google\' }\\n maven { url \'https://maven.aliyun.com/repository/central\' }\\n }\\n}\\n
\\n在 android/gradle/wrapper/gradle-wrapper.properties 中,使用国内镜像加速下载:
\\n\\n\\ndistributionUrl=mirrors.aliyun.com/github/rele…
\\n
如果项目使用的是 build.gradle.kts (Kotlin DSL),在 android/build.gradle.kts 中,将仓库地址替换为国内镜像:
\\n val kotlinVersion: String by extra(\\"1.9.23\\") // 使用 extra 定义变量\\n repositories {\\n // 注释掉默认仓库\\n // google()\\n // mavenCentral()\\n // 添加阿里云镜像\\n maven { url = uri(\\"https://maven.aliyun.com/repository/google\\") }\\n maven { url = uri(\\"https://maven.aliyun.com/repository/central\\") }\\n }\\n dependencies {\\n classpath(\\"com.android.tools.build:gradle:8.0.0\\") // 确保版本与 Flutter 兼容\\n }\\n}\\n\\nallprojects {\\n repositories {\\n // 注释掉默认仓库\\n // google()\\n // mavenCentral()\\n // 添加阿里云镜像\\n maven { url = uri(\\"https://maven.aliyun.com/repository/google\\") }\\n maven { url = uri(\\"https://maven.aliyun.com/repository/central\\") }\\n }\\n}\\n
\\n清理构建缓存:
\\n\\n\\nflutter clean
\\n
删除android/.gradle、android/app/build等目录:
\\n\\n\\nrm -rf android/.gradle
\\n
\\n\\nrm -rf android/app/build
\\n
重新获取依赖:
\\n\\n\\nflutter pub get
\\n
重新运行应用:
\\n\\n\\nflutter run
\\n
完。
","description":"上篇 ~ 初始化一个新的 Flutter 项目,请按照以下步骤操作 1. 确认 Flutter SDK 是否已正确安装\\n\\n如果你尚未安装 Flutter,请先下载并解压 SDK:\\n\\n访问 Flutter 官网 下载 Mac 版本的 SDK 解压到指定目录,例如:\\n\\nunzip ~/Downloads/flutter_macos_arm64.zip -d ~/\\n\\n2. 配置环境变量\\n\\n打开 zsh 配置文件 :\\n\\nnano ~/.zshrc\\n\\n添加 Flutter 路径 :在文件末尾添加以下内容(假设 Flutter 解压到 ~/flutter):\\n\\nexport…","guid":"https://juejin.cn/post/7490739439661973567","author":"wayne408","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-08T09:12:28.739Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a33e8fdc814c4c77bc928b15657a8e8a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgd2F5bmU0MDg=:q75.awebp?rk3s=f64ab15b&x-expires=1744708447&x-signature=3mx%2BrqQBuTgDL1150wRu9BxZx88%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"flutter getx 状态管理(二)","url":"https://juejin.cn/post/7490462400615792680","content":"在getx状态管理(一)中使用0.obs Obx(()=>Text(\\"\\"))实现UI自动响应数据的变化,
\\n现在来看第二种实现方式数据监听
.
class TestController extends GetxController {\\n int count = 0;\\n\\n increment() {\\n count++;\\n update();\\n }\\n}\\n\\nclass TestWidget extends GetView<TestController> {\\n @override\\n Widget build(BuildContext context) {\\n return GetBuilder<TestController>(\\n builder: (controller) => Text(\\"${controller.count}\\"));\\n }\\n}\\n
\\n@override\\nvoid initState() {\\n super.initState();\\n //controller 注册\\n ... \\n //添加监听\\n _subscribeToController();\\n}\\n\\nvoid _subscribeToController() {\\n //如果存在,先移除\\n _remove?.call(); \\n _remove = (widget.id == null)\\n ? controller?.addListener(\\n _filter != null ? _filterUpdate : getUpdate,\\n )\\n : controller?.addListenerId(\\n widget.id,\\n _filter != null ? _filterUpdate : getUpdate,\\n );\\n}\\n\\n@override\\nvoid dispose() {\\n super.dispose();\\n widget.dispose?.call(this);\\n if (_isCreator! || widget.assignId) {\\n if (widget.autoRemove && GetInstance().isRegistered<T>(tag: widget.tag)) {\\n GetInstance().delete<T>(tag: widget.tag);\\n }\\n }\\n _remove?.call();\\n controller = null;\\n _isCreator = null;\\n _remove = null;\\n _filter = null;\\n}\\n\\n//刷新页面\\nmixin GetStateUpdaterMixin<T extends StatefulWidget> on State<T> {\\n //如果element存在,调用 setState((){})刷新页面 \\n void getUpdate() {\\n if (mounted) setState(() {});\\n }\\n}\\n
\\nabstract class GetxController extends DisposableInterface with ListenableMixin, ListNotifierMixin\\n \\n //ids 更新有id的widget, condition 更新条件\\n void update([List<Object>? ids, bool condition = true]) {\\n if (!condition) {\\n return;\\n }\\n if (ids == null) {\\n refresh();\\n } else {\\n for (final id in ids) {\\n refreshGroup(id);\\n }\\n }\\n }\\n}\\n
\\n1. addListener 添加监听到_updaters中并返回一个移除监听的Function
2. refresh 执行_updaters中的方法
\\n3. 执行getUpdate setState()执行build()
4. removeListener 移除监听
mixin ListNotifierMixin on ListenableMixin {\\n \\n //监听列表 \\n List<GetStateUpdate?>? _updaters = <GetStateUpdate?>[];\\n\\n HashMap<Object?, List<GetStateUpdate>>? _updatersGroupIds =\\n HashMap<Object?, List<GetStateUpdate>>();\\n \\n ///1.\\n @protected\\n void refresh() {\\n _notifyUpdate();\\n }\\n //2.刷新所有添加监听的widget \\n void _notifyUpdate() {\\n for (var element in _updaters!) {\\n element!();\\n }\\n }\\n //3.刷新特定id的widget \\n void _notifyIdUpdate(Object id) {\\n if (_updatersGroupIds!.containsKey(id)) {\\n final listGroup = _updatersGroupIds![id]!;\\n for (var item in listGroup) {\\n item();\\n }\\n }\\n }\\n\\n @protected\\n void refreshGroup(Object id) {\\n _notifyIdUpdate(id);\\n }\\n\\n \\n @protected\\n void notifyChildrens() {\\n TaskManager.instance.notify(_updaters);\\n }\\n \\n bool get hasListeners {\\n assert(_debugAssertNotDisposed());\\n return _updaters!.isNotEmpty;\\n }\\n\\n int get listeners {\\n assert(_debugAssertNotDisposed());\\n return _updaters!.length;\\n }\\n \\n //移除监听\\n @override\\n void removeListener(VoidCallback listener) {\\n assert(_debugAssertNotDisposed());\\n _updaters!.remove(listener);\\n }\\n\\n void removeListenerId(Object id, VoidCallback listener) {\\n assert(_debugAssertNotDisposed());\\n if (_updatersGroupIds!.containsKey(id)) {\\n _updatersGroupIds![id]!.remove(listener);\\n }\\n _updaters!.remove(listener);\\n }\\n\\n @mustCallSuper\\n void dispose() {\\n assert(_debugAssertNotDisposed());\\n _updaters = null;\\n _updatersGroupIds = null;\\n }\\n \\n /// GetBuild 添加监听\\n // 返回一个移除监听的Function\\n @override\\n Disposer addListener(GetStateUpdate listener) {\\n assert(_debugAssertNotDisposed());\\n _updaters!.add(listener);\\n return () => _updaters!.remove(listener);\\n }\\n\\n Disposer addListenerId(Object? key, GetStateUpdate listener) {\\n _updatersGroupIds![key] ??= <GetStateUpdate>[];\\n _updatersGroupIds![key]!.add(listener);\\n return () => _updatersGroupIds![key]!.remove(listener);\\n }\\n\\n /// To dispose an [id] from future updates(), this ids are registered\\n /// by `GetBuilder()` or similar, so is a way to unlink the state change with\\n /// the Widget from the Controller.\\n void disposeId(Object id) {\\n _updatersGroupIds!.remove(id);\\n }\\n}\\n
","description":"在getx状态管理(一)中使用0.obs Obx(()=>Text(\\"\\"))实现UI自动响应数据的变化, 现在来看第二种实现方式数据监听.\\n\\nGetBuilder + update手动更新\\nclass TestController extends GetxController {\\n int count = 0;\\n\\n increment() {\\n count++;\\n update();\\n }\\n}\\n\\nclass TestWidget extends GetView在现代应用界面中,流畅的动画效果往往能提升用户体验。本文将详细介绍如何在Flutter中实现一个具有\\"磁性\\"效果的粒子流动画 - 当用户触摸屏幕时,粒子会被吸引并形成流动的视觉效果。这个效果不仅视觉上令人惊艳,还能提供有趣的用户交互体验。
\\n这个效果的核心是通过以下几个关键技术实现的:
\\n首先,我们需要创建基本的页面结构:
\\nclass MagneticParticlesPage extends StatefulWidget {\\n @override\\n _MagneticParticlesPageState createState() => _MagneticParticlesPageState();\\n}\\n\\nclass _MagneticParticlesPageState extends State<MagneticParticlesPage>\\n with SingleTickerProviderStateMixin {\\n final int particleCount = 120;\\n final List<Particle> particles = [];\\n Offset? pointerPosition;\\n late Ticker _ticker;\\n double _time = 0;\\n \\n // 颜色配置\\n final Color primaryColor = Color(0xFF007AFF);\\n final Color secondaryColor = Color(0xFF6C13FF);\\n final Color tertiaryColor = Color(0xFF00E5FF);\\n \\n // 其他初始化代码...\\n}\\n
\\n注意这里我们使用了SingleTickerProviderStateMixin
,这是为了获取Ticker实例,它能提供比AnimationController更精细的帧控制。
为了简化主逻辑,我们创建一个专门的Particle
类来管理每个粒子的属性:
class Particle {\\n Offset position;\\n Offset velocity;\\n final double size;\\n final double opacity;\\n final Color color;\\n final Offset originalPosition;\\n double currentSize;\\n double currentOpacity;\\n\\n Particle({\\n required this.position,\\n required this.velocity,\\n required this.size,\\n required this.opacity,\\n required this.color,\\n required this.originalPosition,\\n }) : currentSize = size,\\n currentOpacity = opacity;\\n}\\n\\n// 为Offset类添加扩展方法,方便向量计算\\nextension OffsetExtension on Offset {\\n Offset normalize() {\\n final magnitude = distance;\\n if (magnitude == 0) return Offset.zero;\\n return this / magnitude;\\n }\\n\\n Offset clone() {\\n return Offset(dx, dy);\\n }\\n}\\n
\\n每个粒子包含位置、速度、大小、不透明度等多种属性,还特别保存了一个原始位置originalPosition
,这是为了实现粒子的\\"回弹\\"效果。
在初始化阶段,我们需要随机生成多个具有不同属性的粒子:
\\nvoid _initializeParticles() {\\n final random = Random();\\n\\n // 生成随机粒子\\n for (int i = 0; i < particleCount; i++) {\\n final position = Offset(\\n random.nextDouble() * _screenSize.width,\\n random.nextDouble() * _screenSize.height,\\n );\\n\\n // 随机速度\\n final velocity = Offset(\\n (random.nextDouble() - 0.5) * 1.0,\\n (random.nextDouble() - 0.5) * 1.0,\\n );\\n\\n // 随机大小和不透明度\\n final size = random.nextDouble() * 2.5 + 1.5;\\n final opacity = random.nextDouble() * 0.4 + 0.6;\\n\\n // 随机颜色 - 在三种主色之间选择\\n Color color;\\n final colorPick = random.nextDouble();\\n if (colorPick < 0.33) {\\n color = primaryColor;\\n } else if (colorPick < 0.66) {\\n color = secondaryColor;\\n } else {\\n color = tertiaryColor;\\n }\\n\\n // 创建粒子并添加到列表\\n particles.add(\\n Particle(\\n position: position,\\n velocity: velocity,\\n size: size,\\n opacity: opacity,\\n color: color,\\n originalPosition: position.clone(),\\n ),\\n );\\n }\\n}\\n
\\n这段代码生成了120个不同的粒子,每个粒子都有随机的位置、速度、大小、不透明度和颜色。注意我们使用了3种主色来增加视觉多样性。
\\n粒子的运动是整个效果的核心。每一帧,我们需要更新所有粒子的状态:
\\nvoid _updateParticles() {\\n for (final particle in particles) {\\n // 基础移动 - 微小的随机运动\\n final noise = Offset(\\n sin(_time * 0.5 + particle.originalPosition.dx * 0.05) * 0.3,\\n cos(_time * 0.5 + particle.originalPosition.dy * 0.05) * 0.3,\\n );\\n\\n // 更新位置\\n particle.position = particle.position + particle.velocity + noise;\\n\\n // 慢慢将粒子拉回其原始位置附近\\n final toOriginal = particle.originalPosition - particle.position;\\n particle.velocity = particle.velocity + toOriginal * 0.003;\\n\\n // 摩擦力,减慢粒子速度\\n particle.velocity = particle.velocity * 0.95;\\n\\n // 如果有鼠标/触摸点,添加吸引力\\n if (pointerPosition != null) {\\n final pointerForce = pointerPosition! - particle.position;\\n final distance = pointerForce.distance;\\n\\n // 吸引距离范围\\n final attractRadius = 180.0;\\n\\n if (distance < attractRadius) {\\n // 吸引力随距离减弱\\n final strength = 1.0 - (distance / attractRadius);\\n final attractForce = pointerForce.normalize() * strength * 2.0;\\n\\n // 应用吸引力\\n particle.velocity = particle.velocity + attractForce;\\n\\n // 接近鼠标时增加不透明度和大小\\n particle.currentOpacity = particle.opacity + strength * 0.3;\\n particle.currentSize = particle.size + strength * 2.0;\\n } else {\\n // 恢复正常状态\\n particle.currentOpacity = particle.opacity;\\n particle.currentSize = particle.size;\\n }\\n } else {\\n // 无鼠标/触摸时恢复正常状态\\n particle.currentOpacity = particle.opacity;\\n particle.currentSize = particle.size;\\n }\\n\\n // 边界检查 - 保持粒子在屏幕内\\n // ...边界检查代码...\\n }\\n}\\n
\\n这段逻辑包含了几个关键的物理模拟:
\\n最后也是最关键的部分是使用CustomPainter绘制粒子和连线:
\\nclass ParticlePainter extends CustomPainter {\\n final List<Particle> particles;\\n final Offset? pointerPosition;\\n final linkDistance = 100.0;\\n\\n ParticlePainter({\\n required this.particles,\\n this.pointerPosition,\\n });\\n\\n @override\\n void paint(Canvas canvas, Size size) {\\n // 绘制粒子连线\\n final linePaint = Paint()\\n ..style = PaintingStyle.stroke\\n ..strokeWidth = 0.5;\\n\\n // 检查粒子距离并绘制连线\\n for (int i = 0; i < particles.length; i++) {\\n final p1 = particles[i];\\n\\n // 绘制鼠标/触摸与附近粒子的连线\\n if (pointerPosition != null) {\\n final pointerDistance = (p1.position - pointerPosition!).distance;\\n if (pointerDistance < linkDistance) {\\n final opacity = (1.0 - pointerDistance / linkDistance) * 0.8;\\n linePaint.color = p1.color.withOpacity(opacity * 0.5);\\n canvas.drawLine(pointerPosition!, p1.position, linePaint);\\n }\\n }\\n\\n // 绘制粒子之间的连线\\n for (int j = i + 1; j < particles.length; j++) {\\n final p2 = particles[j];\\n final distance = (p1.position - p2.position).distance;\\n\\n if (distance < linkDistance) {\\n final opacity = (1.0 - distance / linkDistance) * 0.3;\\n final blendedColor = Color.lerp(p1.color, p2.color, 0.5)!;\\n linePaint.color = blendedColor.withOpacity(opacity);\\n canvas.drawLine(p1.position, p2.position, linePaint);\\n }\\n }\\n }\\n\\n // 绘制粒子本身\\n for (final particle in particles) {\\n final paint = Paint()\\n ..color = particle.color.withOpacity(particle.currentOpacity)\\n ..style = PaintingStyle.fill\\n ..maskFilter =\\n MaskFilter.blur(BlurStyle.normal, particle.currentSize * 0.5);\\n\\n // 绘制粒子\\n canvas.drawCircle(particle.position, particle.currentSize, paint);\\n\\n // 绘制发光核心\\n final corePaint = Paint()\\n ..color = Colors.white.withOpacity(particle.currentOpacity * 0.7)\\n ..style = PaintingStyle.fill;\\n\\n canvas.drawCircle(\\n particle.position, particle.currentSize * 0.4, corePaint);\\n }\\n\\n // 绘制触摸点光晕效果\\n if (pointerPosition != null) {\\n // ...光晕效果绘制代码...\\n }\\n }\\n\\n @override\\n bool shouldRepaint(covariant ParticlePainter oldDelegate) {\\n return true; // 每帧都重绘\\n }\\n}\\n
\\n绘制过程分为三个主要部分:
\\n实现这种高帧率动画时,性能优化至关重要:
\\n这种磁性粒子效果可以应用于:
\\n通过Flutter的CustomPainter和物理模拟,我们实现了一个既美观又具有交互性的磁性粒子流效果。核心技术包括:
\\n这个例子展示了Flutter在创建复杂视觉效果方面的强大能力,同时也是学习自定义绘制和动画的绝佳案例。
\\n通过分解复杂效果为简单步骤,即使是看似复杂的动画也能被清晰理解和实现。希望这篇文章能帮助大家掌握Flutter高级动画的实现思路。
","description":"前言 在现代应用界面中,流畅的动画效果往往能提升用户体验。本文将详细介绍如何在Flutter中实现一个具有\\"磁性\\"效果的粒子流动画 - 当用户触摸屏幕时,粒子会被吸引并形成流动的视觉效果。这个效果不仅视觉上令人惊艳,还能提供有趣的用户交互体验。\\n\\n技术原理\\n\\n这个效果的核心是通过以下几个关键技术实现的:\\n\\n自定义绘制(CustomPainter):利用Flutter的CustomPainter实现粒子的绘制和连线\\n物理模拟:为每个粒子添加物理属性(位置、速度、加速度)\\n交互响应:检测触摸位置并动态调整粒子行为\\n帧动画:使用Ticker实现高帧率的连续动画更…","guid":"https://juejin.cn/post/7490500767138676775","author":"Sinyu1012","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-08T03:13:02.629Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3855f628bf98411d9185667c7bb5dbad~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgU2lueXUxMDEy:q75.awebp?rk3s=f64ab15b&x-expires=1744691082&x-signature=KhkTf9q5cykri%2BzOR8sJ%2BZ3PKZU%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","前端","GitHub","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"从0到1:我用Flutter造了个全平台IPTV神器,从此看直播不再\\"精神分裂\\"!","url":"https://juejin.cn/post/7490439299794665498","content":"作为一名前端打工人,我每天的快乐很简单:下班回家打开直播,看看小姐姐唱歌、老哥打游戏、甚至大爷钓鱼——直到我发现自己的手机成了**\\"直播软件博物馆\\"**。
\\n\\n某酷要装一个,某鱼要装一个,某牙再装一个…每次切APP都像在玩俄罗斯轮盘赌:\\"这个平台的画质会不会卡?那个平台的弹幕会不会掉?\\" 最离谱的是有次看球赛,我居然在三个APP之间反复横跳了17次!(别问,问就是每个平台都有独家解说)
\\n那一刻我悟了:当代网友看直播流的不是流量,是精神分裂啊!
\\n于是我决定自己造轮子——不对,是造火箭!要能一键起飞、全平台制霸的那种!
\\n\\n在技术选型的十字路口,我遇见了Tauri、Electron等一众\\"佳人\\",但最终牵起了Flutter的手。为什么?因为它会说四国语言(Android/iOS/Windows/macOS)啊!毕竟我的目标是让用户从此告别**\\"装APP就像集邮\\"**的悲惨命运。
\\n经过无数个与bug约会的深夜(和我的咖啡机结下了革命友谊),XPlayer终于诞生了!它就像直播界的瑞士军刀:
\\n\\n// 顺便秀段Flutter灵魂代码\\nvoid playLiveStream(String url) {\\n videoPlayerController = VideoPlayerController.network(url)\\n ..initialize().then((_) => setState(() {}));\\n}\\n
\\nWindows电脑?MacBook?安卓手机?iPhone?小孩子才做选择,XPlayer全都要!从此你的电子设备终于能组成**\\"复仇者联盟\\"**。
\\n用Flutter搞渲染就像给屏幕涂了德芙——纵享丝滑。4K画质?弹幕风暴?通通拿下!(温馨提示:卡顿时请先检查自家网速,这个锅本软件不背)
\\n偷偷塞了个源码解析模式,按特定手势能召唤开发者模式。没想到吧?看直播还能学Flutter!(建议搭配《Flutter实战》食用更佳)
\\n功能 | 演示图 |
---|---|
Ctrl+C/V大法 |
我知道你们要说什么:\\"吹得这么牛,倒是给个地址啊!\\"
\\n速速收下这份爱的号码牌:github.com/TNT-Likely/…
现在的它就像刚出新手村的勇者,需要各位的**\\"星\\"光加持**!每点一次Star,世界上就少一个被直播软件逼疯的程序员(功德+1)。
\\n让每个程序员都能优雅地摸鱼,是我们对这个行业最大的温柔(手动狗头) 🐶
","description":"从0到1:我用Flutter造了个全平台IPTV神器,从此看直播不再\\"精神分裂\\"! 我是如何被直播软件逼成\\"海王\\"的?\\n\\n作为一名前端打工人,我每天的快乐很简单:下班回家打开直播,看看小姐姐唱歌、老哥打游戏、甚至大爷钓鱼——直到我发现自己的手机成了**\\"直播软件博物馆\\"**。\\n\\n某酷要装一个,某鱼要装一个,某牙再装一个…每次切APP都像在玩俄罗斯轮盘赌:\\"这个平台的画质会不会卡?那个平台的弹幕会不会掉?\\" 最离谱的是有次看球赛,我居然在三个APP之间反复横跳了17次!(别问,问就是每个平台都有独家解说)\\n\\n那一刻我悟了:当代网友看直播流的不是流量…","guid":"https://juejin.cn/post/7490439299794665498","author":"阿笑带你学前端","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-08T02:41:39.238Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5712a153221f4e4493556c35d18a7614~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6Zi_56yR5bim5L2g5a2m5YmN56uv:q75.awebp?rk3s=f64ab15b&x-expires=1744684899&x-signature=ltq8Sgn16NA%2BOgTXCZZK3VrlgM4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5b17b8a68c124722b2d7dad159dfe180~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6Zi_56yR5bim5L2g5a2m5YmN56uv:q75.awebp?rk3s=f64ab15b&x-expires=1744684899&x-signature=%2B6m8FirFJ1ZsQN7weiTXW92P4Xc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/310d3ecf6fe6462190d075167b140e7c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6Zi_56yR5bim5L2g5a2m5YmN56uv:q75.awebp?rk3s=f64ab15b&x-expires=1744684899&x-signature=hRLyCVUtStWIJB2ZXs8WiBCspqc%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter开发-03-JSON和序列化复杂数据分类封装基类","url":"https://juejin.cn/post/7490428110222934042","content":"Flutter开发-01-简单JSON数据和序列化在Flutter中的最佳实践
\\nFlutter开发-02-复杂JSON数据和序列化在Flutter中的最佳实践
\\n\\n\\n上一篇文章记录了序列化解析复杂结构JSON数据的几个方法。其中使用泛型去解析的方法,大多情况会遇到两种带有数组列表类型的JSON数据结构,一个是
\\ndata
字段直接返回数组List,另一种是data
字段返回的是一个分页的Bean对象,这个Bean对象中会有一个类似datas
的字段返回数组List; 上文有好几种解析这两种JSON数据的方法。
1、使用泛型基类BaseResponse去解析,整个序列化非相同部分字段;
\\nvar userListBean = UserList.fromJson(userListMap);\\n
\\n2、JSON格式为返回List,使用 BaseResponse<List<User>>
去解析数据,这种方法注意一定要使用 List<dynamic>
进行强转,否则会报类型错误:
// 列表转换时一定要加一下强转List<dynamic>,否则会报错:type \'List<dynamic>\' is not a subtype of type \'List<User>\'\\nBaseResponse<List<User>> userListMap2 = BaseResponse.fromJson(userListMap,\\n (json) => (json as List<dynamic>)\\n // .map((e) => User.fromJson(e as Map<String, dynamic>))\\n .map((e) => User.fromJson(e))\\n .toList());\\n\\n var usersMap = userListMap2.data;\\n
\\n3、解析带有分页类型数组的JSON数据,一种方法是使用泛型创建BaseResponse基类+data字段对应的序列化Bean对象:
\\nBaseResponse<UserPageBean> userListMap3 =\\n BaseResponse.fromJson(json2Map, (json) => UserPageBean.fromJson(json));\\n\\nvar datas = userListMap3.data?.datas;\\n
\\n4、解析带有分页类型数组的JSON数据,另一种方法是使用泛型创建BaseResponse基类+data字段对应的序列化泛型类对象BaseList:
\\nBaseResponse<BaseList<User>> userListMap3 = BaseResponse.fromJson(json2Map,\\n (json) => BaseList.fromJson(json, (json) => User.fromJson(json)));\\n\\nBaseList<User>? baseUsers = userListMap3.data;\\n// var datas = baseUsers?.datas;\\nList<User>? usersMap3 = baseUsers?.datas;\\n
\\n\\n\\n虽然一个 BaseResponse 解决了两种数据结构,但使用时的代码会有些复杂,很容易出错。可以对这三种类型的接口进行单独处理,简化操作。
\\n
适用于 data
字段返回统一的数据结构(最为推荐吧),直接把 data
字段返回的JSON数据进行序列化操作;
核心代码:
\\nimport \'package:json_annotation/json_annotation.dart\';\\n\\npart \'base_response.g.dart\';\\n\\n/// 一个注释,用于代码生成器,使其知道该类需要生成JSON序列化逻辑\\n@JsonSerializable(genericArgumentFactories: true)\\nclass BaseResponse<T> {\\n //消息(例如成功消息文字/错误消息文字)\\n String? message;\\n\\n bool? success = false;\\n\\n //自定义code(可根据内部定义方式)\\n int? code;\\n\\n //接口返回的数据\\n T? data;\\n\\n BaseResponse({\\n this.message,\\n this.success,\\n this.code,\\n this.data,\\n });\\n\\n /// 从映射创建新BaseResponse实例所需的工厂构造函数。将映射传递给生成的\' _BaseResponseFromJson() \'构造函数。\\n /// 构造函数以源类命名,在本例中为BaseResponse。\\n factory BaseResponse.fromJson(\\n Map<String, dynamic> json, T Function(dynamic json) fromJsonT) =>\\n _$BaseResponseFromJson<T>(json, fromJsonT);\\n\\n /// \' toJson \'是类声明支持序列化为JSON的约定。该实现仅仅调用私有的、生成的助手方法\' _BaseResponseToJson \'。\\n Map<String, dynamic> toJson(Object? Function(T value) toJsonT) =>\\n _$BaseResponseToJson<T>(this, toJsonT);\\n}\\n
\\n对于返回的JSON数据中 data
字段返回的是数组List的情形,可以直接使用泛型基类BaseListResponse去解析:
JOSN数据:
\\n{\\n \\"code\\":0,\\n \\"message\\":\\"Success\\",\\n \\"data\\":[\\n {\\n \\"name\\":\\"Jerry\\",\\n \\"email\\":\\"Jerry@example.com\\"\\n },\\n {\\n \\"name\\":\\"Alex\\",\\n \\"email\\":\\"Alex@example.com\\"\\n },\\n {\\n \\"name\\":\\"Tom\\",\\n \\"email\\":\\"Tom@example.com\\"\\n },\\n {\\n \\"name\\":\\"Jack\\",\\n \\"email\\":\\"Jack@example.com\\"\\n },\\n {\\n \\"name\\":\\"Lucy\\",\\n \\"email\\":\\"Lucy@example.com\\"\\n }\\n ]\\n}\\n
\\n核心代码:
\\nimport \'package:json_annotation/json_annotation.dart\';\\n\\npart \'base_list_response.g.dart\';\\n\\n@JsonSerializable(genericArgumentFactories: true)\\nclass BaseListResponse<T> {\\n List<T> data;\\n int code;\\n String message;\\n\\n BaseListResponse(this.data, this.code, this.message);\\n\\n factory BaseListResponse.fromJson(\\n Map<String, dynamic> json, T Function(dynamic json) fromJsonT) =>\\n _$BaseListResponseFromJson<T>(json, fromJsonT);\\n\\n Map<String, dynamic> toJson(Object? Function(T value) toJsonT) =>\\n _$BaseListResponseToJson<T>(this, toJsonT);\\n}\\n\\n
\\n数据解析代码:
\\n Future decodeAssetJson() async {\\n // 解析本地数据-List数组\\n Map<String, dynamic> userListMap =\\n await loadJsonAssets(\\"assets/user_list.json\\");\\n \\n // 使用泛型基类BaseListResponse<T>去解析返回List数组类型的JOSN数据\\n BaseListResponse<User> result =\\n BaseListResponse.fromJson(userListMap, (json) => User.fromJson(json));\\n\\n List<User> list = result.data;\\n\\n if (kDebugMode) {\\n print(\'\\\\n ===============BaseListResponse解析数组开始================== \\\\n \');\\n\\n print(\'result : $result\');\\n print(\'result : ${result.toString()}\');\\n\\n if (list.isNotEmpty) {\\n for (int i = 0; i < (list.length); i++) {\\n print(\'result list $i : ${list[i].name}\');\\n print(\'result list $i : ${list[i].email}\');\\n }\\n }\\n\\n print(\'\\\\n ===============BaseListResponse解析数组结束================== \\\\n \');\\n }\\n}\\n
\\n输出日志:
\\nresult : Instance of \'BaseListResponse<User>\'\\nresult toString : Instance of \'BaseListResponse<User>\'\\nresult list 0 : Jerry\\nresult list 0 : Jerry@example.com\\n\\n...\\n\\n
\\n对于返回的JSON数据中 data
字段返回的是数组List的情形,可以直接使用泛型基类BaseListResponse去解析:
JSON数据:
\\n{\\n \\"data\\":{\\n \\"curPage\\":1,\\n \\"datas\\":[\\n {\\n \\"name\\":\\"肖战\\",\\n \\"email\\":\\"肖战@example.com\\"\\n },\\n {\\n \\"name\\":\\"丁程鑫\\",\\n \\"email\\":\\"丁程鑫@example.com\\"\\n },\\n {\\n \\"name\\":\\"贺峻霖\\",\\n \\"email\\":\\"贺峻霖@example.com\\"\\n },\\n {\\n \\"name\\":\\"李天泽\\",\\n \\"email\\":\\"李天泽@example.com\\"\\n },\\n {\\n \\"name\\":\\"刘耀文\\",\\n \\"email\\":\\"刘耀文@example.com\\"\\n },\\n {\\n \\"name\\":\\"成毅\\",\\n \\"email\\":\\"成毅@example.com\\"\\n }\\n ],\\n \\"offset\\":0,\\n \\"over\\":false,\\n \\"pageCount\\":3,\\n \\"size\\":20,\\n \\"total\\":46\\n },\\n \\"code\\":0,\\n \\"message\\":\\"Success get\\"\\n}\\n
\\n数据解析代码:
\\nFuture decodeAssetJson() async {\\n// 解析本地数据-List数组\\n Map<String, dynamic> json2Map =\\n await loadJsonAssets(\\"assets/user_list2.json\\");\\n \\n // 使用泛型基类BasePageListResponse<T> + PageList<T>去解析返回分页List数组类型的JOSN数据\\n BasePageListResponse<User> result2 =\\n BasePageListResponse.fromJson(json2Map, (json) => User.fromJson(json));\\n\\n var data = result2.data;\\n // PageList<User> data = result2.data;\\n var list2 = data.datas;\\n // List<User> list2 = data.datas;\\n\\n if (kDebugMode) {\\n print(\'\\\\n ===============BasePageListResponse解析数组开始================== \\\\n \');\\n\\n print(\'result : $result2\');\\n print(\'result toString : ${result2.toString()}\');\\n print(\'result code : ${result2.code}\');\\n print(\'result message : ${result2.message}\');\\n print(\'result data : ${result2.data}\');\\n print(\'result data curPage: ${result2.data.curPage}\');\\n print(\'result datas : ${result2.data.datas}\');\\n print(\'result datas jsonEncode: ${jsonEncode(result2.data.datas)}\');\\n\\n if (list2.isNotEmpty) {\\n for (int i = 0; i < (list2.length); i++) {\\n print(\'result list $i : ${list2[i].name}\');\\n print(\'result list $i : ${list2[i].email}\');\\n }\\n }\\n\\n print(\'\\\\n ===============BasePageListResponse解析数组结束================== \\\\n \');\\n }\\n}\\n\\n
\\n输出日志:
\\nresult : Instance of \'BasePageListResponse<User>\'\\nresult toString : Instance of \'BasePageListResponse<User>\'\\nresult code : 0\\nresult message : Success get\\nresult data : Instance of \'PageList<User>\'\\nresult datas : [Instance of \'User\', Instance of \'User\', Instance of \'User\', Instance of \'User\', Instance of \'User\', Instance of \'User\']\\nresult data curPage: 10\\nresult datas jsonEncode: [{\\"name\\":\\"肖战\\",\\"email\\":\\"肖战@example.com\\"},{\\"name\\":\\"丁程鑫\\",\\"email\\":\\"丁程鑫@example.com\\"},{\\"name\\":\\"贺峻霖\\",\\"email\\":\\"贺峻霖@example.com\\"},{\\"name\\":\\"李天泽\\",\\"email\\":\\"李天泽@example.com\\"},{\\"name\\":\\"刘耀文\\",\\"email\\":\\"刘耀文@example.com\\"},{\\"name\\":\\"成毅\\",\\"email\\":\\"成毅@example.com\\"}]\\nresult list 0 : 肖战\\nresult list 0 : 肖战@example.com\\n\\n...\\n\\n
\\n提示:这里对文章进行总结:
以上两种JSON数据序列化后的数据解析代码就很统一了:
\\n返回数组List的JSON解析:
\\n// 使用泛型基类BaseListResponse<T>去解析返回List数组类型的JOSN数据\\nBaseListResponse<User> result =\\n BaseListResponse.fromJson(userListMap, (json) => User.fromJson(json));\\n\\nList<User> list = result.data;\\n
\\n返回分页数组列表的JSON解析:
\\n// 使用泛型基类BasePageListResponse<T> + PageList<T>去解析返回分页List数组类型的JOSN数据\\nBasePageListResponse<User> result2 =\\n BasePageListResponse.fromJson(json2Map, (json) => User.fromJson(json));\\n\\nvar data = result2.data;\\n// PageList<User> data = result2.data;\\nvar list2 = data.datas;\\n// List<User> list2 = data.datas;\\n
\\n\\n","description":"系列文章目录 Flutter开发-01-简单JSON数据和序列化在Flutter中的最佳实践\\n\\nFlutter开发-02-复杂JSON数据和序列化在Flutter中的最佳实践\\n\\n前言\\n\\n上一篇文章记录了序列化解析复杂结构JSON数据的几个方法。其中使用泛型去解析的方法,大多情况会遇到两种带有数组列表类型的JSON数据结构,一个是 data 字段直接返回数组List,另一种是 data 字段返回的是一个分页的Bean对象,这个Bean对象中会有一个类似 datas 的字段返回数组List; 上文有好几种解析这两种JSON数据的方法。\\n\\n1…","guid":"https://juejin.cn/post/7490428110222934042","author":"杨亮Jerry","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-08T01:45:24.695Z","media":null,"categories":["Android","APP","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"谨慎升级macOS 15.4,规避 ITMS-90048 错误","url":"https://juejin.cn/post/7490399021736820773","content":"以上三种方法我觉得都很简洁,但是最好统一使用方法最好。即全部使用
\\nBaseResponse<T>
只对data
返回的字段进行序列化操作,然后进行数据解析;\\n或者\\n1、使用BaseResponse<T>
解析一般结构的JSON数据,使用BaseListResponse<T>
去解析返回数组List的JSON数据,只去序列化数组List中的Bean对象(比如本例中的User
);\\n2、使用BasePageList<T>
+PageList<T>
去解析返回 \\"分页\\" List的JSON数据,也是只去序列化数组List中的Bean对象(比如本例中的User
);
哈喽,我是老刘
\\n最近不少朋友升级macOS 15.4后碰到了 ITMS-90048 错误提示,所以临时更新本文提醒各位读者。
\\n
\\n老刘这里整理一下现象原因,以及网上给出的解决方案。
._Symbols
。flutter build ipa
或 RN 命令行构建)会触发此问题,而通过 Xcode 的 Product > Archive 方式提交的应用不受影响。从现象基本可以判断,是macOS 15.4系统造成的,而非Xcode 或 Command Line Tools 的原因。
\\nmacOS 15.4 的 APFS 文件系统在处理文件时,会为某些操作(如 rsync
)自动生成以 ._
开头的隐藏文件,用于存储 Finder 元数据或资源 fork。
\\n而在 iOS 构建过程中,生成 Symbols
目录时,系统可能因文件操作触发元数据文件 ._Symbols
的创建。
• 脚本构建:Flutter/RN 的脚本式构建(如 flutter build ipa
)可能依赖 rsync
等工具,导致隐藏文件被错误打包到 .xcarchive
。
\\n• Xcode Archive:原生通过 Xcode 的归档流程会自动过滤隐藏文件,因此无此问题。
如果已经升级到macOS 15.4,也不用慌,有两种方案:
\\n改用 Xcode Archive 构建
\\n执行 flutter build ios
生成项目后,通过 Xcode 的 Product > Archive 流程提交,避免脚本构建引入隐藏文件。
自动化脚本清理
\\n在 Flutter 构建后添加脚本(如 cleanup.sh
),自动检查并删除 ._Symbols
:
IPA_PATH=\\"build/ios/ipa/your_app_name.ipa\\"\\nif [ -f \\"$IPA_PATH\\" ]; then\\n unzip -l \\"$IPA_PATH\\" | grep ._Symbols && zip -d \\"$IPA_PATH\\" ._Symbols/ || echo \\"未发现 ._Symbols\\"\\nelse\\n echo \\"IPA 文件未找到\\"\\nfi\\n
\\n这种方式比较适合有专门的打包机,自动化构建ipa的情况。
\\n• 暂不升级至 macOS 15.4,等待 Apple 或框架官方修复。
\\n• 已升级用户需严格使用上述规避方案。
感觉最近Flutter SDK或者系统升级造成的问题还是挺多的,但说实话老刘这边基本都没有踩坑。
\\n其实老刘带着团队做Flutter开发6年多了,最开始的时候也踩过不少的坑,但是最近这几年就基本不会再踩了。
\\n秘诀就在于我们这边对SDK版本、系统版本、IDE版本等环境要素,统一采用谨慎升级策略。
\\n如果一个版本工作的很好我们通常不会追踪新版本的。
\\n如果有必须要升级的因素,我们通常也会升级到一个使用过一段时间的相对比较稳定的版本。
\\n总之对于大型项目来说,稳定性是一个非常重要的指标,尽量避免因为追踪新版的sdk、工具等产生额外的人力开销。
\\n当然对于喜欢尝鲜的个人开发者,是没有这种问题的。
如果看到这里的同学对客户端开发或者Flutter开发感兴趣,欢迎联系老刘,我们互相学习。
\\n点击免费领老刘整理的《Flutter开发手册》,覆盖90%应用开发场景。
\\n可以作为Flutter学习的知识地图。
\\n覆盖90%开发场景的《Flutter开发手册》
Nuvigator 是一个基于 Flutter 构建的导航包。它是由 Nubank 创建的一个包,旨在添加新功能,如深度链接,还支持嵌套路由,并且可以帮助模块化应用。查看更多信息
\\n在这个示例中,我们将使用 Flutter CLI 创建一个名为 nuvigatorexample
的应用:
flutter create nuvigatorexample\\n
\\n当前版本的 nuvigator 是 0.7.2,它不支持 null safety,尝试运行应用时会抛出错误,因此需要更改 SDK 版本。
\\n在 pubspec.yaml 中,更改 SDK 版本:
\\nenvironment:\\n sdk: \\">=2.11.0 <3.0.0\\"\\n
\\n添加 nuvigator 到依赖项中:
\\ndependencies:\\n flutter:\\n sdk: flutter\\n nuvigator: ^0.7.2\\n
\\n安装包:
\\nflutter pub get\\n
\\n让我们使用 nuvigator 构建一个导航的示例。
\\n第一个页面,Home,将有一个按钮,用于导航到第二个页面,Details。
\\n我们的 home_screen.dart:
\\nimport \'package:flutter/material.dart\';\\n\\nclass HomeScreen extends StatelessWidget {\\n final Function onPressed;\\n\\n const HomeScreen({Key key, this.onPressed}) : super(key: key);\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: const Text(\'Home\')),\\n body: Center(\\n child: ElevatedButton(\\n onPressed: onPressed,\\n child: const Text(\'Navigate to details\'),\\n )),\\n );\\n }\\n}\\n
\\ndetails_screen.dart:
\\nimport \'package:flutter/material.dart\';\\n\\nclass DetailsScreen extends StatelessWidget {\\n const DetailsScreen({Key key}) : super(key: key);\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: const Text(\'Details\')),\\n body: const Center(child: Text(\'Details screen\')),\\n );\\n }\\n}\\n
\\nmain.dart:
\\nimport \'package:flutter/material.dart\';\\nimport \'package:nuvigator/next.dart\';\\nimport \'package:nuvigatorexample/screens/details_screen.dart\';\\nimport \'package:nuvigatorexample/screens/home_screen.dart\';\\n\\nvoid main() {\\n runApp(const MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n const MyApp({Key key}) : super(key: key);\\n\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n title: \'Flutter Demo\',\\n theme: ThemeData(\\n primarySwatch: Colors.blue,\\n ),\\n home: Nuvigator.routes(initialRoute: \'home\', routes: [\\n NuRouteBuilder(path: \'home\', builder: (BuildContext contextNuvigator, __, ___) => HomeScreen(onPressed: (){\\n Nuvigator.of(contextNuvigator).open(\'details\');\\n },), screenType: materialScreenType),\\n NuRouteBuilder(\\n path: \'details\', builder: (_, __, ___) => const DetailsScreen(), screenType: materialScreenType)\\n ]),\\n );\\n }\\n}\\n
\\n在我们的 home_screen 中,我们将添加一个新类,该类将扩展 NuRoute:
\\nimport \'package:nuvigator/next.dart\';\\n\\nclass HomeRouter extends NuRoute {\\n @override\\n Widget build(BuildContext context, NuRouteSettings<Object> settings) {\\n return HomeScreen(onPressed: (){\\n nuvigator.pushNamed(\'details\');\\n },);\\n }\\n\\n @override\\n String get path => \'home\';\\n\\n @override\\n ScreenType get screenType => materialScreenType;\\n}\\n
\\n我们也将为 details_screen 做同样的事情:
\\nimport \'package:nuvigator/next.dart\';\\n\\nclass DetailsRoute extends NuRoute {\\n @override\\n Widget build(BuildContext context, NuRouteSettings<Object> settings) {\\n return DetailsScreen();\\n }\\n\\n @override\\n String get path => \'details\';\\n\\n @override\\n ScreenType get screenType => materialScreenType;\\n}\\n
\\n现在我们在 main.dart 中创建我们的路由器,创建一个扩展 NuRouter 的新类,并更改 MaterialApp 的 home:
\\nclass Router extends NuRouter {\\n @override\\n String get initialRoute => \'home\';\\n\\n @override\\n List<NuRoute<NuRouter, Object, Object>> get registerRoutes => [\\n HomeRouter(),\\n DetailsRoute()\\n ];\\n}\\n\\nclass MyApp extends StatelessWidget {\\n const MyApp({Key key}) : super(key: key);\\n\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n title: \'Flutter Demo\',\\n theme: ThemeData(\\n primarySwatch: Colors.blue,\\n ),\\n home: Nuvigator(router: Router()),\\n );\\n }\\n}\\n
\\n现在,让我们向详情屏幕发送一条文本。
\\n首先,我们在 pushNamed 函数中定义文本作为参数:
\\nnuvigator.pushNamed(\'details\', arguments: {\'message\': \'Hello world\'});\\n
\\n然后,我们在详情屏幕中使用 nuvigator 设置的原始参数获取此参数。
\\n在 DetailsRoute 的 build 方法中,让我们获取消息:
\\nfinal String message = settings.rawParameters[\'message\'];\\n
\\n然后在我们的 DetailsScreen 中,我们将添加一个名为 message 的新属性并使用它来渲染:
\\nclass DetailsScreen extends StatelessWidget {\\n final String message; // 路由中的文本\\n const DetailsScreen({Key key, this.message}) : super(key: key);\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: const Text(\'Details\')),\\n body: Center(child: Text(message)),\\n );\\n }\\n}\\n
\\n结果:
\\n原文:www.yuque.com/fengjutian/… 《📄Flutter & Nuvigator:路由器和参数的介绍》
","description":"什么是nuvigator? Nuvigator 是一个基于 Flutter 构建的导航包。它是由 Nubank 创建的一个包,旨在添加新功能,如深度链接,还支持嵌套路由,并且可以帮助模块化应用。查看更多信息\\n\\n示例\\n\\n在这个示例中,我们将使用 Flutter CLI 创建一个名为 nuvigatorexample 的应用:\\n\\nflutter create nuvigatorexample\\n\\n安装包\\n\\n当前版本的 nuvigator 是 0.7.2,它不支持 null safety,尝试运行应用时会抛出错误,因此需要更改 SDK 版本。\\n\\n在 pubspec…","guid":"https://juejin.cn/post/7490406929065148416","author":"fengjutian","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-07T06:58:11.080Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/edde5be76876463b9abb746fdee74a34~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgZmVuZ2p1dGlhbg==:q75.awebp?rk3s=f64ab15b&x-expires=1744613890&x-signature=ZvJ7kP1jdeliqSl3S1mGismTkio%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3307d28941dc4b38ad30f6e2ffcf25b4~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgZmVuZ2p1dGlhbg==:q75.awebp?rk3s=f64ab15b&x-expires=1744613890&x-signature=V0k7ulFe9ClI5BGgAJAp6b9AB5w%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"系统化掌握Dart网络编程之Dio(四):拦截器篇","url":"https://juejin.cn/post/7489720690885099529","content":"在移动应用开发中,网络请求如同血管中的血液
,承载着数据交互的生命力
。然而,你是否遇到过这样的场景:每个请求都要手动添加Token
、全局处理错误码
、统一添加埋点日志
……这些重复性工作不仅效率低下,更让代码臃肿难维护。
Dio
拦截器,正是为解决这些问题而生的利器。它像一位隐形的\\"网络请求调度员\\"
,在请求发出前、响应返回后、甚至错误发生时,以流水线的方式对数据进行加工和拦截。
本文将带你深入Dio
拦截器的底层逻辑,通过系统化的思维拆解其设计哲学,并通过实战案例展示如何用它构建高扩展性的网络层。你是否准备好,让代码从此优雅起来?
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\n\\n\\n拦截器是
\\nDio
网络库提供的一种中间件机制,允许在网络请求的生命周期中(请求发出前
、响应返回后
、错误发生时
)插入自定义逻辑。它通过链式处理模型,将多个独立的处理单元(
\\n拦截器
)按顺序串联,形成一个可扩展的流水线流程
。
深入理解:
\\n想象一家外卖配送中心:
onRequest
)检查订单完整性。添加请求头
)。onResponse
)检查餐品是否完好。onError
)处理配送异常 。拦截器就是这些分工明确的\\"岗位\\"
,每个环节专注单一职责,通过链式传递完成整个流程
。
Dio
拦截器的工作流程分为三个阶段,每个阶段对应一个核心方法:
阶段 | 方法 | 触发时机 | 典型操作 |
---|---|---|---|
请求前处理 | onRequest | 请求即将发送到服务器之前 | 修改请求头、添加公共参数、加密请求体 |
响应后处理 | onResponse | 服务器返回响应且HTTP 状态码为2xx | 解析业务数据、转换数据结构、缓存响应结果 |
错误处理 | onError | 请求失败或HTTP 状态码非2xx | 统一错误提示、重试机制、刷新Token |
流程示例:
\\n// 请求 -> 拦截器A的onRequest -> 拦截器B的onRequest -> 发送请求 \\n// 响应 -> 拦截器B的onResponse -> 拦截器A的onResponse -> 返回结果\\n
\\n①、链式传递:
\\n拦截器按照添加顺序形成处理链,每个拦截器处理完成后,通过调用 handler.next()
将控制权传递给下一个拦截器,直到所有拦截器执行完毕。
dio.interceptors.add(InterceptorA()); // 先执行\\ndio.interceptors.add(InterceptorB()); // 后执行\\n
\\n②、中断与短路:
\\n在任意阶段,拦截器可通过 handler.resolve()
或 handler.reject()
直接返回结果
或抛出错误
,终止后续拦截器的执行。
onRequest: (options, handler) {\\n if (无网络连接) {\\n handler.reject(DioException(message: \'网络不可用\')); // 终止请求\\n } else {\\n handler.next(options); // 继续传递\\n }\\n}\\n
\\n③、上下文隔离:
\\n每个拦截器仅能访问当前阶段的请求/响应对象(RequestOptions
、Response
),无法直接修改其他拦截器的内部状态,保证职责单一性。
拦截器的核心职责可归纳为以下四类:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n职责类型 | 具体场景 | 代码示例 |
---|---|---|
数据加工 | 添加全局请求头、参数加密、数据序列化 | options.headers[\'Authorization\'] = \'Bearer $token\' |
流程控制 | 重试失败请求、等待Token 刷新后继续 | handler.retry(request) |
监控与统计 | 记录请求耗时、上报接口成功率 | log(\'API耗时:${DateTime.now().difference(startTime)}\') |
异常处理 | 统一错误码映射、弹窗提示、降级处理 | if (error.response?.statusCode == 504) showTimeoutDialog() |
解决代码重复性问题
问题场景:
\\n在未使用拦截器时,开发者需要在每个网络请求中手动处理以下逻辑:
Token
。code
字段)。Token
刷新。代码反例:
\\n// 每个请求都需重复相同逻辑\\nFuture<User> fetchData() async {\\n try {\\n final response = await dio.get(\\n \'/api/user\',\\n options: Options(headers: {\'Authorization\': \'Bearer $token\'}), // 手动添加Token\\n );\\n // 手动解析业务状态码\\n if (response.data[\'code\'] != 200) {\\n throw AppException(response.data[\'message\']);\\n }\\n return User.fromJson(response.data);\\n } on DioException catch (e) {\\n // 手动处理错误\\n if (e.response?.statusCode == 401) {\\n await refreshToken();\\n return fetchData(); // 重试请求\\n }\\n rethrow;\\n }\\n}\\n
\\n拦截器价值:
\\n\\n\\n通过拦截器将上述逻辑封装为独立模块,所有请求自动继承这些行为,减少
\\n90%
的重复代码。
网络层
与业务逻辑
解耦(高内聚低耦合
)问题场景:
\\n当网络层逻辑(如加密算法
、缓存策略
)直接嵌入业务代码时:
代码臃肿
,可读性下降。引入错误
。不一致
的网络处理逻辑。通过拦截器,可以将网络层的关注点分解为独立模块:
\\n日志
、加密
)、业务层(Token刷新
、错误提示
)、监控层(性能统计
)。无拦截器方案:
\\n// 无拦截器:业务代码中混杂网络逻辑\\nFuture<User> fetchUser() async {\\n try {\\n // 手动添加Token\\n final response = await dio.get(\'/user\', options: Options(\\n headers: {\'Authorization\': \'Bearer $token\'},\\n ));\\n // 手动解析业务状态码\\n if (response.data[\'code\'] != 200) {\\n throw CustomError(response.data[\'message\']);\\n }\\n return User.fromJson(response.data);\\n } on DioException catch (e) {\\n // 手动处理错误\\n showErrorDialog(e.message);\\n }\\n}\\n
\\n拦截器方案:
\\n// 业务代码\\nFuture<User> fetchData() async {\\n return dio.get(\'/api/user\').then((res) => User.fromJson(res.data));\\n}\\n\\n// 网络层独立模块\\ndio.interceptors.addAll([\\n AuthInterceptor(), // 认证\\n LoggingInterceptor(), // 日志\\n RetryInterceptor(), // 重试\\n BizCodeInterceptor(), // 业务状态码解析\\n]);\\n
\\n核心优势:
\\n关注数据转换
,不涉及网络细节
。加密算法
或缓存策略
时,无需改动业务代码。灵活调整请求流程
核心能力:
\\n拦截器支持在运行时动态添加或移除,无需修改业务代码即可调整网络层行为,实现“热插拔”
式的功能扩展。
典型场景:
\\n①、环境切换:
// 开发环境:添加日志拦截器 \\nif (isDev) dio.interceptors.add(LogInterceptor()); \\n\\n// 生产环境:移除日志,添加加密拦截器 \\nif (isProd) {\\n dio.interceptors.removeWhere((i) => i is LogInterceptor);\\n dio.interceptors.add(EncryptInterceptor());\\n}\\n
\\n②、功能开关:
\\n// 根据用户设置动态启用/禁用埋点 \\nanalyticsEnabled ? dio.interceptors.add(AnalyticsInterceptor()) \\n : dio.interceptors.remove(AnalyticsInterceptor());\\n
\\n③、按需加载:
\\n// 特定页面需要缓存拦截器 \\nvoid openUserProfile() {\\n dio.interceptors.add(CacheInterceptor());\\n fetchUserData(); \\n}\\n
\\n技术优势:
\\n批量处理耗时操作
核心机制:
\\n拦截器通过集中处理重复性计算任务(如加密
、压缩
),减少单次请求的冗余开销,同时利用异步队列避免阻塞主线程。
典型场景:
\\n①、批量加密请求体:
class EncryptInterceptor extends Interceptor {\\n @override \\n void onRequest(RequestOptions options, handler) async { \\n // 统一加密所有请求体(减少重复初始化加密工具的开销) \\n if (options.data is Map) { \\n options.data = _batchEncrypt(options.data); // 批量处理数据 \\n } \\n handler.next(options); \\n } \\n} \\n
\\n②、异步并行处理:
\\n// 使用isolate或线程池并行处理多个响应解密 \\nonResponse: (response, handler) async { \\n final List<dynamic> dataList = response.data as List; \\n // 并行解密列表中的每条数据 \\n final decryptedData = await Future.wait( \\n dataList.map((item) => compute(decrypt, item)) \\n ); \\n response.data = decryptedData; \\n handler.next(response); \\n} \\n
\\n③、缓存计算结果:
\\n// 对相同请求参数缓存加密结果 \\nfinal _encryptCache = HashMap<String, String>(); \\nonRequest: (options, handler) { \\n final key = options.uri.toString(); \\n if (_encryptCache.containsKey(key)) { \\n options.data = _encryptCache[key]; // 直接使用缓存 \\n } else { \\n options.data = encrypt(options.data); \\n _encryptCache[key] = options.data; \\n } \\n handler.next(options); \\n} \\n
\\n优化收益:
\\nCPU
占用:避免每个请求独立初始化加密算法。类
及方法
详解Interceptor
:基类所有自定义拦截器需继承自 Interceptor
,其核心生命周期方法:
abstract class Interceptor { \\n void onRequest(RequestOptions options, RequestInterceptorHandler handler); \\n void onResponse(Response response, ResponseInterceptorHandler handler); \\n void onError(DioException err, ErrorInterceptorHandler handler); \\n} \\n
\\nRequestInterceptorHandler
、ResponseInterceptorHandler
、ErrorInterceptorHandler
:控制类用于控制拦截器链的传递流程,核心方法:
\\nclass RequestInterceptorHandler extends _BaseHandler {\\n void next(RequestOptions requestOptions) {\\n //...\\n }\\n\\n void resolve(Response response, [bool callFollowingResponseInterceptor = false,]) {\\n //...\\n }\\n\\n void reject(DioException error, [\\n bool callFollowingErrorInterceptor = false,]) {\\n //...\\n }\\n}\\n\\nclass ResponseInterceptorHandler extends _BaseHandler {\\n \\n void next(Response response) {\\n //...\\n }\\n\\n void resolve(Response response) {\\n //...\\n }\\n\\n void reject(DioException error, [\\n bool callFollowingErrorInterceptor = false,]) {\\n //...\\n }\\n}\\n\\nclass ErrorInterceptorHandler extends _BaseHandler {\\n\\n void next(DioException error) {\\n //...\\n }\\n\\n void resolve(Response response) {\\n //...\\n }\\n \\n void reject(DioException error) {\\n //...\\n }\\n}\\n
\\nQueuedInterceptor
:队列拦截器顺序执行拦截器,适用于需要严格顺序的场景(如Token刷新
)。
onRequest
方法void onRequest( \\n RequestOptions options, // 当前请求配置(可修改) \\n RequestInterceptorHandler handler \\n) \\n
\\n关键属性(RequestOptions
):
options.method; // 请求方法(GET/POST等) \\noptions.uri; // 请求地址 \\noptions.headers; // 请求头(可直接修改) \\noptions.queryParameters; // URL查询参数 \\noptions.data; // 请求体数据 \\noptions.extra; // 自定义扩展参数(跨拦截器传递数据) \\n
\\n修改请求头:
\\nvoid onRequest(options, handler) { \\n options.headers.addAll({ \\n \'X-App-Version\': \'1.0.0\', \\n \'X-Device-ID\': \'ABC123\', \\n }); \\n handler.next(options); // 传递修改后的配置 \\n} \\n
\\nonResponse
方法void onResponse( \\n Response response, // 响应对象(可修改) \\n ResponseInterceptorHandler handler \\n) \\n
\\n关键属性(Response
):
response.data; // 响应体数据(可修改为业务模型) \\nresponse.statusCode; // HTTP状态码 \\nresponse.requestOptions; // 关联的请求配置 \\n
\\n数据转换:
\\nvoid onResponse(response, handler) { \\n // 将原始JSON转换为业务模型 \\n response.data = User.fromJson(response.data[\'user\']); \\n handler.next(response); \\n} \\n
\\nonError
方法void onError( \\n DioException err, // 错误对象(可修改) \\n ErrorInterceptorHandler handler \\n) \\n
\\n关键属性(DioException
):
err.type; // 错误类型(如DioExceptionType.connectionTimeout) \\nerr.requestOptions; // 关联的请求配置 \\nerr.response; // 服务器返回的错误响应(若有) \\n
\\n错误重试:
\\nvoid onError(err, handler) async { \\n if (err.response?.statusCode == 401) { \\n // 刷新Token后重试原请求 \\n final newToken = await refreshToken(); \\n err.requestOptions.headers[\'Authorization\'] = \'Bearer $newToken\'; \\n final newResponse = await dio.fetch(err.requestOptions); \\n handler.resolve(newResponse); // 终止错误链,返回新响应 \\n } else { \\n handler.next(err); // 继续传递错误 \\n } \\n} \\n
\\n在任意阶段调用 handler.resolve()
或 handler.reject()
可提前终止拦截器链:
void onRequest(options, handler) { \\n if (无网络连接) { \\n // 直接返回自定义错误 \\n handler.reject(DioException( \\n requestOptions: options, \\n error: \'网络不可用\', \\n )); \\n } else { \\n handler.next(options); \\n } \\n} \\n
\\n通过 options.extra
字段在不同拦截器间共享数据:
// 拦截器A \\nvoid onRequest(options, handler) { \\n options.extra[\'startTime\'] = DateTime.now(); \\n handler.next(options); \\n} \\n\\n// 拦截器B \\nvoid onResponse(response, handler) { \\n final duration = DateTime.now().difference( \\n response.requestOptions.extra[\'startTime\'] \\n ); \\n print(\'请求耗时:$duration\'); \\n handler.next(response); \\n} \\n
\\nQueuedInterceptorsWrapper
)使用队列拦截器确保异步操作顺序执行:
\\ndio.interceptors.add(QueuedInterceptorsWrapper( \\n onRequest: (options, handler) async { \\n // 确保同一时间只有一个请求进入 \\n await _semaphore.acquire(); \\n handler.next(options); \\n _semaphore.release(); \\n }, \\n)); \\n
\\n①、请求阶段:
\\n请求发起 → 拦截器1.onRequest → 拦截器2.onRequest → ... → 发送网络请求 \\n
\\n②、响应阶段:
\\n收到响应 → 最后一个拦截器.onResponse → ... → 拦截器1.onResponse → 返回结果 \\n
\\n③、错误阶段:
\\n发生错误 → 最后一个拦截器.onError → ... → 拦截器1.onError → 抛出异常 \\n
\\n关键原则:
\\nhandler.next()
、resolve()
或 reject()
,否则链式调用会中断。RequestOptions
或 Response
时需注意对象不可变性,推荐使用 copyWith
方法。\\n// 安全修改请求配置 \\nfinal newOptions = options.copyWith( \\n headers: {...options.headers, \'X-Foo\': \'Bar\'}, \\n); \\nhandler.next(newOptions); \\n
\\n拦截器分为三类:
\\nHeaders
、加密数据
)。JSON
、缓存结果
)。Token
过期、网络错误
)。生命周期顺序为:
\\n\\n\\n\\n
请求拦截器 → 发送请求 → 响应拦截器 → 错误拦截器(如发生异常)
拦截器通过 递归调用 和 Handler
对象 形成责任链。每个拦截器接收一个 handler
参数,决定是否继续传递请求或直接返回。
关键对象:Handler
typedef Handler = Future<dynamic> Function(RequestOptions requestOptions);\\n
\\n执行流程:
\\nhandler
,指向实际发送 HTTP
请求的逻辑。拦截器接口定义了三个主要的方法:
\\nonRequest
:在请求发送前
被调用。onResponse
:在响应接收后
被调用。onError
:在请求发生错误时
被调用。可以通过实现这些方法来定义自己的拦截器逻辑。
\\nabstract class Interceptor {\\n Future<RequestOptions> onRequest(RequestOptions options, RequestInterceptorHandler handler);\\n Future<Response> onResponse(Response response, ResponseInterceptorHandler handler);\\n Future<void> onError(DioError err, ErrorInterceptorHandler handler);\\n}\\n
\\n拦截器处理器(InterceptorHandler
)允许拦截器在拦截过程中控制请求/响应的流程。它提供了两个主要的方法:
resolve
:用于解决请求/响应
,继续后续流程。reject
:用于拒绝请求/响应
,中断后续流程。class RequestInterceptorHandler {\\n Future<RequestOptions> resolve(RequestOptions options);\\n Future<void> reject(error, StackTrace? stackTrace]);\\n}\\n\\nclass ResponseInterceptorHandler {\\n Future<Response> resolve(Response response);\\n Future<void> reject(error, StackTrace? stackTrace]);\\n}\\n\\nclass ErrorInterceptorHandler {\\n Future<void> resolve(Response response);\\n Future<void> reject(error, StackTrace? stackTrace]);\\n}\\n
\\nDio
实例维护了一个拦截器链,当请求发送或响应接收时,会依次调用链中的每个拦截器。拦截器链支持同步和异步拦截器,允许在拦截过程中进行复杂的异步操作。
执行流程如下:
\\nonRequest
方法。handler.resolve
),则发送请求。onResponse
方法。如果在请求发送前或响应接收后发生错误,则依次调用每个拦截器的 onError
方法。
可以通过 dio.interceptors.add()
方法将自定义拦截器添加到拦截器链中。此外,Dio
还提供了一些内置的拦截器,如 LogInterceptor
用于自动记录请求和响应的日志。
dio.interceptors.add(InterceptorsWrapper(\\n onRequest: (options, handler) {\\n // 自定义请求拦截逻辑\\n return handler.next(options);\\n },\\n onResponse: (response, handler) {\\n // 自定义响应拦截逻辑\\n return handler.next(response);\\n },\\n onError: (err, handler) {\\n // 自定义错误拦截逻辑\\n return handler.next(err);\\n },\\n));\\n
\\n流程示例:
\\n// 请求 -> 拦截器A的onRequest -> 拦截器B的onRequest -> 发送请求 \\n// 响应 -> 拦截器B的onResponse -> 拦截器A的onResponse -> 返回结果\\n
\\nSRP
):精准的功能划分
每个拦截器仅处理 单一类型 的业务逻辑,避免功能耦合,确保代码可维护性和复用性。
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n拦截器类型 | 职责描述 |
---|---|
LogInterceptor | 仅记录请求/响应日志,不涉及数据解析或修改。 |
CacheInterceptor | 仅处理缓存逻辑(如读取缓存、更新缓存),不干预其他流程。 |
RetryInterceptor | 仅实现重试机制(如网络异常自动重试),不处理认证或加密。 |
AuthInterceptor | 仅管理认证逻辑(如Token 刷新),不参与数据格式化或日志记录。 |
在拦截器基类 Interceptor
中,通过分离 onRequest
、onResponse
、onError
方法,强制开发者按职责拆分逻辑:
abstract class Interceptor {\\n void onRequest(RequestOptions options, RequestInterceptorHandler handler);\\n void onResponse(Response response, ResponseInterceptorHandler handler);\\n void onError(DioException err, ErrorInterceptorHandler handler);\\n}\\n
\\n关键点:
\\nonRequest
仅处理请求前操作)。OCP
):无侵入式扩展
通过新增拦截器 而非修改已有代码来扩展功能,确保核心流程的稳定性。
\\n// 新增缓存拦截器(无需修改Dio源码)\\ndio.interceptors.add(CacheInterceptor());\\n\\n// 新增性能监控拦截器\\ndio.interceptors.add(PerformanceInterceptor());\\n
\\n动态功能组合
通过 add
/remove
方法,可在运行时动态调整拦截器,实现功能模块的热插拔。
// 开发环境:添加日志和Mock拦截器\\nif (isDev) {\\n dio.interceptors.add(LogInterceptor());\\n dio.interceptors.add(MockInterceptor());\\n}\\n\\n// 生产环境:移除Mock,添加加密拦截器\\nif (isProd) {\\n dio.interceptors.removeWhere((i) => i is MockInterceptor);\\n dio.interceptors.add(EncryptInterceptor());\\n}\\n
\\n关键点:
\\n不影响已发起的请求
,仅作用于后续请求
。调试模式
、安全模式
)。设计原则 | 实现手段 | 业务价值 |
---|---|---|
单一职责 | 拦截器功能隔离 + 接口强制拆分 | 代码可读性高,模块易维护、易测试。 |
开闭原则 | 动态扩展机制 | 功能扩展无需修改框架,降低升级风险。 |
可插拔性 | 动态增删拦截器 | 灵活适应多环境(开发、生产、测试)。 |
拦截器的强大之处,在于将离散的网络处理逻辑转化为可编排的管道操作。通过理解其责任链模式的内核,我们能像搭积木一样构建高可维护的网络层:
\\n日志
),业务拦截器处理领域需求(如Token
刷新)。handler.next()
、resolve()
、reject()
精确控制执行流。当你能游刃有余地使用拦截器编排请求生命周期时,那些曾经让你头疼的全局状态管理
、多环境适配
问题,都将迎刃而解。优秀的架构不是设计出来的,而是通过拦截器这样的基础组件,逐步演化出来的
。
\\n","description":"前言 在移动应用开发中,网络请求如同血管中的血液,承载着数据交互的生命力。然而,你是否遇到过这样的场景:每个请求都要手动添加Token、全局处理错误码、统一添加埋点日志……这些重复性工作不仅效率低下,更让代码臃肿难维护。\\n\\nDio拦截器,正是为解决这些问题而生的利器。它像一位隐形的\\"网络请求调度员\\",在请求发出前、响应返回后、甚至错误发生时,以流水线的方式对数据进行加工和拦截。\\n\\n本文将带你深入Dio拦截器的底层逻辑,通过系统化的思维拆解其设计哲学,并通过实战案例展示如何用它构建高扩展性的网络层。你是否准备好,让代码从此优雅起来?\\n\\n操千曲而后晓声,观千剑而后…","guid":"https://juejin.cn/post/7489720690885099529","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-07T02:22:47.250Z","media":null,"categories":["Android","Dart","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"注意,暂时不要升级 MacOS ,Flutter/RN 等构建 ipa 可能会因 「ITMS-90048」This bundle is invalid 被拒绝","url":"https://juejin.cn/post/7489405244038201378","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
近期,不少使用构建 ipa 提交 App Store 的用户遇到 「ITMS-90048」This bundle is invalid 而拒绝的问题,这个 错误的核心原因是在提交给 App Store Connect 的归档文件 (.xcarchive
) 里,包含了一个不允许存在的隐藏文件 ._Symbols
:
而用户在 ipa 存档里,确实也可以看到 .Symbols
这个隐藏文件的存在,可以看到这个目录是一个空文件夹:
这个问题目前在 Flutter#166367 、RN#50447 等平台都有相关 issue ,而出现这个的原因,主要在于这些平台都是从脚本构建出一个 ipa 包进行提交,而如果原生平台,一般更习惯在 Xcode 里通过 Prodict > Archive
这种方式来提交,目前这种方式并不会有这个问题。
\\n\\n所以如果你遇到这个问题,也可以先实现
\\nfluter build ios
,然后通过Prodict > Archive
这种方式提交来绕靠问题。
目前这个问题推测来自新的 macOS 15.4 ,因为对于 macOS (尤其是 APFS 文件系统)在处理文件时,会为文件创建以 ._
开头的隐藏文件,这些文件用于存储 Finder 信息、资源 fork 或其他元数据等。
而在 iOS 构建过程中,需要生成 Symbols
文件目录,用于存储调试符号 (dSYMs) 等信息,所以推测问题可能出在构建或归档过程中,系统对 Symbols
文件进行了某种操作(如 rsync),导致 macOS 生成了对应的 ._Symbols
元数据文件,并且这个隐藏文件被错误地打包进了 .xcarchive
文件。
目前看来,macOS 15.4 确实包括对内置 rsync
的重大修订:
另外,用户在遇到该问题后,也尝试降级到 Xcode 和 Command Line Tools ,但是问题依然存在;也有用户未升级 Xcode ,但升级到 macOS 15.4,也同样触发该问题,所以问题看起来主要是 macOS 15.4 导致。
\\n而如果已经是 macOS 15.4 的用户,最简单的做法就是使用 Xcode 的 Prodict > Archive
,或者手动删除该文件:
unzip -q app.ipa -d x\\nrm -rf app.ipa x/._Symbols\\ncd x\\nzip -rq ../app.ipa .\\ncd ..\\nrm -rf x\\n
\\n或者 flutter build ipa --release
之后,执行一个 ./cleanup.sh
:
IPA_PATH=\\"build/ios/ipa/your_app_name.ipa\\"\\n# export IPA_PATH=\\"$(find \\"build/ios/ipa\\" -name \'*.ipa\' -type f -print -quit)\\"\\n\\nif [ -f \\"$IPA_PATH\\" ]; then\\n echo \\"Checking for unwanted files like ._Symbols in $IPA_PATH\\"\\n unzip -l \\"$IPA_PATH\\" | grep ._Symbols && zip -d \\"$IPA_PATH\\" ._Symbols/ || echo \\"No ._Symbols found\\"\\nelse\\n echo \\"IPA not found at $IPA_PATH\\"\\nfi\\n
\\n目前看来问题并不在框架端,所以非必要还是暂时不要升级 macOS 15.4 ,避免不必要的问题。
\\n\\n\\n欢迎关注微信公众号:FSA全栈行动 👋
\\n
在之前发布的【Flutter - iOS编译加速】一文中,我们提到升级至 Xcode16
之后,iOS
的编译速度慢到令人发指,随后探索发现是 xcrun cc snapshot_assembly.S snapshot_assembly.o
这一汇编耗时变长了。而就在几天前,有人在相关的 issue
中留言了他篡改使用 Xcode 15
的 cc
来提升编译速度的步骤,详情可见 github.com/dart-lang/s…
我在他的基础上做了优化与封装,只需两句命令即可还原编译速度,在开始详细介绍之前,先展示一下两台构建机优化前后的编译时长记录。
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n构建机 | 优化前(min) | Release + 二进制依赖(min) | Release + 二进制依赖 + 还原编译速度(min) |
---|---|---|---|
i7 | 25+ | 14+ | 11+ |
M4 | 16+ | 8+ | 4+ |
M4
只要四分多钟,真香~
以下是他提供的修改步骤
\\ncp -r /Applications/Xcode-15.4.0.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain ~/Library/Developer/Toolchains\\ncd ~/Library/Developer/Toolchains\\nmv XcodeDefault.xctoolchain Xcode15.4.xctoolchain\\n/usr/libexec/PlistBuddy -c \\"Add CompatibilityVersion integer 2\\" Xcode15.4.xctoolchain/ToolchainInfo.plist\\n/usr/libexec/PlistBuddy -c \\"Set Identifier clang.Xcode15.4\\" Xcode15.4.xctoolchain/ToolchainInfo.plist\\n
\\nXcode 15.4
内部的默认工具链复制到 ~/Library/Developer/Toolchains
目录。~/Library/Developer/Toolchains
目录。XcodeDefault.xctoolchain
重命名为 Xcode15.4.xctoolchain
,方便区分。.xctoolchain/ToolchainInfo.plist
文件,添加 CompatibilityVersion
,并将其值设置为整数类型 2
,修改 Identifier
的值为 clang.Xcode15.4
。- Future<RunResult> cc(List<String> args) => _run(\'cc\', args);\\n+ Future<RunResult> cc(List<String> args) => _run(\'--toolchain\', <String>[\\n+ \'clang.Xcode15.4\',\\n+ \'cc\',\\n+ ...args,\\n+ ]);\\n
\\n修改 flutter_tools
源码,将 cc
修改为 --toolchain
来使用 clang.Xcode15.4
下的 cc
。
默认的工具链路径是 Xcode
中的 /Applications/Xcode.app/Contents/Developer/Toolchains
,不过也可以将工具链放到 ~/Library/Developer/Toolchains
目录下,这样就可以在不修改 Xcode
应用本身的情况下,使用和管理不同的工具链版本。
接着是修改 .xctoolchain/ToolchainInfo.plist
文件,里面可以设置的一些字段如下:
字段 | 说明 |
---|---|
CFBundleIdentifier | 唯一标识 |
CompatibilityVersion | 适配版本,适配 Xcode 时必为 2 |
DisplayName | 【可选】显示名称 |
ShortDisplayName | 【可选】简短的显示名称 |
\\n\\n注:在
\\nDisplayName
和ShortDisplayName
都不设置时,名字会显示为CFBundleIdentifier
关于 CompatibilityVersion
的说明,在网上基本是搜不到的,只有如下这个注释,Xcode 8
及以上,使用 2
,否则使用 1
。
# Xcode 8 requires CompatibilityVersion 2\\nset(COMPAT_VERSION 2)\\nif(XCODE_VERSION VERSION_LESS 8.0.0)\\n # Xcode 7.3 (the first version supporting external toolchains) requires\\n # CompatibilityVersion 1\\n set(COMPAT_VERSION 1)\\nendif()\\n
\\n\\n直接修改 flutter_tools
源码并写死 clang.Xcode15.4
太过于粗暴,如果我们为了安全起见,只想打测试包的时候还原编译速度,而打上架包保持原样就不好调整了,所以这里我对他的修改进行了优化。
首先来介绍一下 TOOLCHAINS
这个环境变量,它可以影响 /usr/bin/
下的命令调用,如 /usr/bin/xcrun
\\n\\n注:
\\nDeveloper Directory
指/Applications/Xcode.app/Contents/Developer
或者/Library/Developer/CommandLineTools
,可以通过xcode-select --print-path
进行检查
如果我们没有设置 TOOLCHAINS
,根据上述流程图,在调用 /usr/bin/xcrun
时,会根据 Developer Directory
搜索该命令,如果找到同名命令,则执行该命令。
xcrun --find cc\\n\\n# /Applications/Xcode-16.2.0.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc\\n
\\n如果我们将 TOOLCHAINS
设置为 .xctoolchain
的 Identifier
,如: clang.Xcode15.4
。
export TOOLCHAINS=clang.Xcode15.4\\n
\\n那么根据上述流程图,则是在 Xcode15.4.xctoolchain
中找到 cc
。
xcrun --find cc\\n/Users/lxf/Library/Developer/Toolchains/Xcode15.4.xctoolchain/usr/bin/cc\\n
\\n根据这一特性,我做了如下调整:
\\n调整 cc
方法,当有配置 CONDOR_TOOLCHAINS
环境变量时,将值取出并赋值给 TOOLCHAINS
。
// Future<RunResult> cc(List<String> args) => _run(\'cc\', args);\\n Future<RunResult> cc(List<String> args) {\\n final String condorToolchains = platform.environment[\'CONDOR_TOOLCHAINS\'] ?? \'\';\\n final Map<String, String> environment = <String, String>{\\n if (condorToolchains.isNotEmpty) \\"TOOLCHAINS\\": condorToolchains,\\n };\\n _run(\'--find\', <String>[\'cc\'], environment: environment).then((RunResult result) {\\n printStatus(\\n \'\\\\n[condor] find cc: ${result.stdout}\\\\n\',\\n );\\n });\\n return _run(\'cc\', args, environment: environment);\\n }\\n
\\n_run
方法新增 environment
参数,用于设置环境变量。
// Future<RunResult> _run(String command, List<String> args) {\\n// return _processUtils.run(\\n// <String>[...xcrunCommand(), command, ...args],\\n// throwOnError: true,\\n// );\\n// }\\n Future<RunResult> _run(String command, List<String> args, {Map<String, String>? environment}) {\\n return _processUtils.run(\\n <String>[...xcrunCommand(), command, ...args],\\n throwOnError: true,\\n environment: environment,\\n );\\n }\\n\\n
\\n上述步骤还是比较繁琐的,所以这里我将其进行了封装,只需要执行两句命令即可。
\\n如果你是首次安装,则执行如下命令
\\nbrew tap LinXunFeng/tap && brew install condor\\n
\\n如果不是首次安装,则需要执行如下命令进行更新
\\nbrew update && brew reinstall condor\\n
\\n如果你习惯使用 Pub
,或者你的电脑是 Intel
芯,则可以执行如下命令进行安装或更新
dart pub global activate condor_cli\\n
\\ncondor optimize-build xctoolchain-copy --xcode Xcode-15.4.0\\n
\\n--xcode
参数请使用 Xcode 15
在 /Applications/
下的名字,如果你电脑上没有 Xcode 15
,建议使用 github.com/XcodesOrg/X… 进行安装。
这一步会做如下几个操作
\\n/Applications/Xcode-15.4.0.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain
拷贝至 ~/Library/Developer/Toolchains/Xcode-15.4.0.xctoolchain
。Xcode-15.4.0.xctoolchain/ToolchainInfo.plist
中的 Identifier
设置为 Xcode-15.4.0
。CompatibilityVersion
并设置为 2
。该命令会对 flutter_tools
源码进行修改,使其具备重定向 cc
的能力而已,在有配置 CONDOR_TOOLCHAINS
环境变量时才会生效,否则则使用默认的 cc
。
# 使用默认 flutter,则不需要传 flutter 参数\\ncondor optimize-build redirect-cc\\n\\n# 如果你想指定 fvm 下的指定 Flutter 版本\\ncondor optimize-build redirect-cc --flutter fvm spawn 3.24.5\\n
\\n后续你只需要 export CONDOR_TOOLCHAINS=Xcode-15.4.0
就可以在 Xcode 16
上感受到 Xcode 15
的编译速度了 🥳
如打包前 export
export CONDOR_TOOLCHAINS=Xcode-15.4.0\\nflutter clean\\nflutter build ipa\\n
\\n如果你想验证,可以加上 --verbose
,并将输出保存到 result.txt
flutter build ipa --verbose > result.txt\\n
\\n命令执行完毕后打开 result.txt
,搜索 condor
即可。
或者如果你不需要按需配置,也可以直接在 Run Script
里设置 CONDOR_TOOLCHAINS
环境变量。
验证也很简单,如下图所示,选择当前的 Build
任务,搜索 condor
即可。
在 Xcode
的工具链中,cc
是 clang
的替身
而不同版本的 clang
对同一份 .S
进行汇编,还是有可能生成内容不一样的 .o
的。不过我自己对比 Xcode 16
和 Xcode 15
生成的 .o
并没有什么不同。
对比 .o
文件,我们可以使用系统自带的 cmp
命令,cmp
是一个用于比较文件的命令行工具,它可以逐字节比较二进制文件。如下所示
cmp /Users/lxf/cc15/snapshot_assembly.o /Users/lxf/cc16/snapshot_assembly.o\\n
\\ncmp
命令执行完成,退出代码为 0
,并且没有输出。这表明 cmp
命令没有发现两个文件之间有任何不同之处。因此可以证明这两个 .o
文件的内容是相同的。
即,基于 Xcode 16
来说并没有影响,这种方式生成的 .o
可以用于上架包,如果还是不放心,可以在打上架包时,不设置 CONDOR_TOOLCHAINS
环境变量即可。
\\n","description":"欢迎关注微信公众号:FSA全栈行动 👋 一、前言\\n\\n在之前发布的【Flutter - iOS编译加速】一文中,我们提到升级至 Xcode16 之后,iOS 的编译速度慢到令人发指,随后探索发现是 xcrun cc snapshot_assembly.S snapshot_assembly.o 这一汇编耗时变长了。而就在几天前,有人在相关的 issue 中留言了他篡改使用 Xcode 15 的 cc 来提升编译速度的步骤,详情可见 github.com/dart-lang/s…\\n\\n我在他的基础上做了优化与封装,只需两句命令即可还原编译速度…","guid":"https://juejin.cn/post/7489479817647226906","author":"LinXunFeng","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-06T11:12:03.487Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fe8e5732a772417aa0b120bddac55c1e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTGluWHVuRmVuZw==:q75.awebp?rk3s=f64ab15b&x-expires=1744547266&x-signature=Bu7cyLcSkjwJuzvXRdwxTooHc5Y%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d238510b4d05438ca2b2673b43813585~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTGluWHVuRmVuZw==:q75.awebp?rk3s=f64ab15b&x-expires=1744547266&x-signature=Qxzye8kjb4k4TvPNjKziXxib%2FT8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/641474dd24ea483dbcafc97da2effe8d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTGluWHVuRmVuZw==:q75.awebp?rk3s=f64ab15b&x-expires=1744547266&x-signature=I65hounyrD8SRr1ZWGAccubwZdI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1626281cdcf7401ca109f586f3831620~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTGluWHVuRmVuZw==:q75.awebp?rk3s=f64ab15b&x-expires=1744547266&x-signature=DLR%2BOfAPirYcQom%2BBfS8PGEtJSU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/30a39eb07d8a40dba5827d3bda14fb99~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTGluWHVuRmVuZw==:q75.awebp?rk3s=f64ab15b&x-expires=1744547266&x-signature=qgFk53SCsYR18rr8ikNwIwGNWas%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e80168aacd884a37ad73afa0e9831833~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTGluWHVuRmVuZw==:q75.awebp?rk3s=f64ab15b&x-expires=1744547266&x-signature=QffbZwlU%2FNZ9yJEqOymtEGqWGjY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/23b9fd2972f04e27967bac1f03f68f47~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTGluWHVuRmVuZw==:q75.awebp?rk3s=f64ab15b&x-expires=1744547266&x-signature=O1Ox5LdQ7wD3RMU0wBs38JRkqa4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9bb61298436f4ffc9fedb7deced23f5d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTGluWHVuRmVuZw==:q75.awebp?rk3s=f64ab15b&x-expires=1744547266&x-signature=46a6D3b0cBBqfjDZ5tsp7iCThvQ%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["iOS","前端","Flutter","Xcode"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 教程(十四)动画","url":"https://juejin.cn/post/7490003622233849883","content":"如果文章对您有所帮助, 请不吝点击关注一下我的微信公众号:FSA全栈行动, 这将是对我最大的激励. 公众号不仅有
\\nFlutter
技术,还有AI
、Android
,iOS
,Python
等文章, 可能有你想要了解的技能知识点哦~
在 Flutter 中,动画分为两类,分别是补间动画、物理动画。
\\n需要注意,Flutter 中的补间动画和Android中的补间动画是不一样的。Flutter 中的补间动画会改变对象的属性,而不仅仅是视觉上UI的变化。
\\n在 Flutter 中,补间动画分为以下几种类型:
\\n在 Flutter 中,以 *Transition
命名,比如 FadeTransition 、SizeTransition 和 RotationTransition 等,需要我们自己定义和操作 AnimationController的动画叫做显示动画。
而以 Animated*
开头的 Widget,例如 AnimatedPositioned
、AnimatedContainer
、 AnimatedPadding
、AnimatedOpacity
等控件,它们最大的特点就是内部已经完全封装好逻辑,你只需要配置对应参数就可以触发动画。这种动画叫做隐式动画。
实际上,隐式动画 就是 显示动画 封装后的产物,因此隐式动画和显示动画他们的称呼不怎么重要。
\\nclass ImplicitAnimationWidget extends StatefulWidget {\\n const ImplicitAnimationWidget({super.key});\\n\\n @override\\n _ImplicitAnimationWidgetState createState() => _ImplicitAnimationWidgetState();\\n}\\n\\nclass _ImplicitAnimationWidgetState extends State<ImplicitAnimationWidget> {\\n bool _isBig = false;\\n\\n @override\\n Widget build(BuildContext context) {\\n return Column(\\n mainAxisAlignment: MainAxisAlignment.center,\\n children: [\\n AnimatedContainer(\\n width: _isBig ? 200 : 100,\\n height: _isBig ? 200 : 100,\\n color: _isBig ? Colors.blue : Colors.red,\\n duration: const Duration(seconds: 1),\\n curve: Curves.easeInOut,\\n ),\\n const SizedBox(height: 20),\\n ElevatedButton(\\n onPressed: () {\\n setState(() {\\n _isBig = !_isBig;\\n });\\n },\\n child: const Text(\'Toggle Size and Color\'),\\n ),\\n ],\\n );\\n }\\n}\\n
\\n效果如下图所示:
\\nclass RotationAinmationPage extends StatefulWidget {\\n @override\\n _RotationAinmationPageState createState() => _RotationAinmationPageState();\\n}\\n\\nclass _RotationAinmationPageState extends State<RotationAinmationPage>\\n with SingleTickerProviderStateMixin {\\n late AnimationController _controller;\\n late Animation<double> _turns;\\n bool _playing = false;\\n\\n // 控制动画运行状态\\n void _toggle() {\\n if (_playing) {\\n _playing = false;\\n _controller.stop();\\n } else {\\n _controller.forward()..whenComplete(() => _controller.reverse());\\n _playing = true;\\n }\\n setState(() {});\\n }\\n\\n @override\\n void initState() {\\n super.initState();\\n // 初始化动画控制器,设置动画时间\\n _controller = AnimationController(\\n vsync: this,\\n duration: Duration(seconds: 10),\\n );\\n\\n // 设置动画取值范围和时间曲线\\n _turns = Tween(begin: 0.0, end: 3.1415926 * 2).animate(\\n CurvedAnimation(parent: _controller, curve: Curves.easeIn),\\n );\\n }\\n\\n @override\\n void dispose() {\\n super.dispose();\\n _controller.dispose();\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\'显示动画\')),\\n body: Center(\\n child: RotationTransition(\\n // 传入动画值\\n turns: _turns,\\n child: Container(\\n width: 200,\\n height: 200,\\n child: FlutterLogo(),\\n ),\\n ),\\n ),\\n floatingActionButton: FloatingActionButton(\\n onPressed: _toggle,\\n child: Icon(_playing ? Icons.pause : Icons.play_arrow),\\n ),\\n );\\n }\\n}\\n
\\n效果如下图所示:
\\nHero 指的是可以在路由(页面)之间“飞行”的 widget,简单来说 Hero 动画就是在路由切换时,有一个共享的widget 可以在新旧路由间切换。由于共享的 widget 在新旧路由页面上的位置、外观可能有所差异,所以在路由切换时会从旧路逐渐过渡到新路由中的指定位置,这样就会产生一个 Hero 动画。
\\n代码示例如下:
\\nclass FirstPage extends StatelessWidget {\\n const FirstPage({super.key});\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: const Text(\'First Page\'),),\\n body: Center(\\n child: GestureDetector(\\n onTap: () {\\n Navigator.push(\\n context,\\n MaterialPageRoute(\\n builder: (context) => const SecondPage(),\\n ),\\n );\\n },\\n child: Hero(\\n tag: \'heroImage\',\\n child: Image.asset(\\n \'assets/images/test.png\', // 请替换为你自己的图片路径\\n width: 200,\\n height: 200,\\n ),\\n ),\\n ),\\n ),\\n );\\n }\\n}\\nclass SecondPage extends StatelessWidget {\\n const SecondPage({super.key});\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: const Text(\'Second Page\'),\\n ),\\n body: Align(\\n alignment: Alignment.topCenter,\\n child: Hero(\\n tag: \'heroImage\',\\n child: Image.asset(\\n \'assets/images/test.png\', // 请替换为你自己的图片路径\\n width: 400,\\n height: 400,\\n ),\\n ),\\n ),\\n );\\n }\\n}\\n
\\n效果如下图所示:
\\n交织动画(Staggered Animation)是指多个动画按照一定的顺序和时间间隔依次或重叠播放。代码示例如下,代码来源9.5 交织动画 | 《Flutter实战·第二版》
\\nclass StaggerAnimation extends StatelessWidget {\\n StaggerAnimation({Key? key, required this.controller}) : super(key: key) {\\n //高度动画\\n height = Tween<double>(begin: .0, end: 300.0).animate(\\n CurvedAnimation(\\n parent: controller,\\n curve: const Interval(\\n 0.0,\\n 0.6, //间隔,前60%的动画时间\\n curve: Curves.ease,\\n ),\\n ),\\n );\\n\\n color = ColorTween(begin: Colors.green, end: Colors.red).animate(\\n CurvedAnimation(\\n parent: controller,\\n curve: const Interval(\\n 0.0,\\n 0.6, //间隔,前60%的动画时间\\n curve: Curves.ease,\\n ),\\n ),\\n );\\n\\n padding = Tween<EdgeInsets>(\\n begin: const EdgeInsets.only(left: .0),\\n end: const EdgeInsets.only(left: 100.0),\\n ).animate(\\n CurvedAnimation(\\n parent: controller,\\n curve: const Interval(\\n 0.6,\\n 1.0, //间隔,后40%的动画时间\\n curve: Curves.ease,\\n ),\\n ),\\n );\\n }\\n\\n late final Animation<double> controller;\\n late final Animation<double> height;\\n late final Animation<EdgeInsets> padding;\\n late final Animation<Color?> color;\\n\\n Widget _buildAnimation(BuildContext context, child) {\\n return Container(\\n alignment: Alignment.bottomCenter,\\n padding: padding.value,\\n child: Container(color: color.value, width: 50.0, height: height.value),\\n );\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return AnimatedBuilder(builder: _buildAnimation, animation: controller);\\n }\\n}\\n\\nclass StaggerRoute extends StatefulWidget {\\n @override\\n _StaggerRouteState createState() => _StaggerRouteState();\\n}\\n\\nclass _StaggerRouteState extends State<StaggerRoute>\\n with TickerProviderStateMixin {\\n late AnimationController _controller;\\n\\n @override\\n void initState() {\\n super.initState();\\n\\n _controller = AnimationController(\\n duration: const Duration(milliseconds: 2000),\\n vsync: this,\\n );\\n }\\n\\n _playAnimation() async {\\n try {\\n //先正向执行动画\\n await _controller.forward().orCancel;\\n //再反向执行动画\\n await _controller.reverse().orCancel;\\n } on TickerCanceled {\\n //捕获异常。可能发生在组件销毁时,计时器会被取消。\\n }\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Center(\\n child: Column(\\n children: [\\n ElevatedButton(\\n onPressed: () => _playAnimation(),\\n child: Text(\\"start animation\\"),\\n ),\\n Container(\\n width: 300.0,\\n height: 300.0,\\n decoration: BoxDecoration(\\n color: Colors.black.withOpacity(0.1),\\n border: Border.all(color: Colors.black.withOpacity(0.5)),\\n ),\\n //调用我们定义的交错动画Widget\\n child: StaggerAnimation(controller: _controller),\\n ),\\n ],\\n ),\\n );\\n }\\n}\\n
\\n效果如下图所示:
\\n物理动画的实现可以看 Widget 的物理模拟动画效果 | Flutter 中文文档 - Flutter 中文开发者网站 - Flutter
\\n混合开发是指 Flutter 与原生 Android、iOS 一起混合开发的 App。混合开发是为了充分利用 Flutter 在纯界面开发上的优势,以及避免Flutter对底层硬件强相关的开发支持的不足。这里以在Android原生项目中嵌入Flutter为例,iOS 的混合开发可以看 这里
\\n我们可以使用如下命令来创建 Flutter 模块,其中 flutter_module 是 Flutter 模块的名字。
\\nflutter create -t module flutter_module \\n
\\n创建完成后,我们需要在Android原生项目的 settings.gradle 文件中关联 Flutter 模块,如下所示:
\\nkts 方式使用如下配置
\\ninclude(\\":app\\")\\nval filePath = settingsDir.toString() + \\"/flutter_module/.android/include_flutter.groovy\\"\\napply(from = File(filePath))\\n
\\ngroovy 配置方式如下:
\\ninclude(\\":app\\") \\nsetBinding(new Binding([gradle: this])) \\ndef filePath = settingsDir.toString() + \\"/flutter_module/.android/include_flutter.groovy\\" \\napply from: filePath \\n
\\n再到 app 目录下的 build.gradle 文件中添加 Flutter 模块,如下所示:
\\ndependencies {\\n // kts\\n implementation(project(\\":flutter\\"))\\n // groovy\\n implementation project(\':flutter\')\\n //...其他代码\\n}\\n
\\nFlutter 提供了 FlutterActivity,让我们可以直接启动Flutter 界面。首先我们需要在 AndroidManifest.xml 文件中注册。代码示例如下:
\\n<activity\\n android:name=\\"io.flutter.embedding.android.FlutterActivity\\"\\n android:theme=\\"@style/LaunchTheme\\"\\n android:configChanges=\\"orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode\\"\\n android:hardwareAccelerated=\\"true\\"\\n android:windowSoftInputMode=\\"adjustResize\\"\\n />\\n
\\n然后我们创建一个 FlutterEngine,FlutterEngine
是 Flutter 的核心,它用于加载执行 Dart 代码,并参与构建和渲染 UI 界面。创建代码如下:
class MyApplication : Application() {\\n\\n lateinit var flutterEngine: FlutterEngine\\n\\n override fun onCreate() {\\n super.onCreate()\\n // Instantiate a FlutterEngine.\\n flutterEngine = FlutterEngine(this)\\n\\n // Start executing Dart code to pre-warm the FlutterEngine.\\n flutterEngine.dartExecutor.executeDartEntrypoint(\\n DartExecutor.DartEntrypoint.createDefault()\\n )\\n\\n // Cache the FlutterEngine to be used by FlutterActivity.\\n FlutterEngineCache\\n .getInstance()\\n .put(\\"my_engine_id\\", flutterEngine)\\n }\\n}\\n
\\n然后我们就可以在代码中启动 FlutterActivity 了,代码示例如下:
\\nbutton.setOnClickListener {\\n startActivity(\\n FlutterActivity\\n .withCachedEngine(\\"my_engine_id\\")\\n .build(this))\\n }\\n
\\n效果如下图所示:
\\n处理 FlutterActivity
外,Flutter 还提供了 FlutterFragment
,方便我们把 Flutter 界面作为 Fragment 在 App 中使用。代码示例如下:
class FlutterFragmentActivity : FragmentActivity() {\\n\\n companion object {\\n private const val TAG_FLUTTER_FRAGMENT = \\"flutter_fragment\\"\\n }\\n\\n\\n private var flutterFragment: FlutterFragment? = null\\n\\n override fun onCreate(savedInstanceState: Bundle?) {\\n super.onCreate(savedInstanceState)\\n setContentView(R.layout.my_activity_layout)\\n\\n val fragmentManager: FragmentManager = supportFragmentManager\\n\\n // 尝试查找现有的 FlutterFragment,以防这不是第一次调用 onCreate() 方法。\\n flutterFragment = fragmentManager\\n .findFragmentByTag(TAG_FLUTTER_FRAGMENT) as FlutterFragment?\\n\\n // 如果 FlutterFragment 不存在,则创建并附加一个新的 FlutterFragment。\\n if (flutterFragment == null) {\\n var newFlutterFragment = FlutterFragment.createDefault()\\n flutterFragment = newFlutterFragment\\n fragmentManager\\n .beginTransaction()\\n .add(\\n R.id.fragment_container,\\n newFlutterFragment,\\n TAG_FLUTTER_FRAGMENT\\n )\\n .commit()\\n }\\n }\\n\\n override fun onPostResume() {\\n super.onPostResume()\\n // 调用 FlutterFragment 的 onPostResume() 方法\\n flutterFragment?.onPostResume()\\n }\\n\\n override fun onNewIntent(intent: Intent) {\\n super.onNewIntent(intent)\\n // 调用 FlutterFragment 的 onNewIntent() 方法,并传入新的意图\\n flutterFragment?.onNewIntent(intent)\\n }\\n\\n override fun onBackPressed() {\\n super.onBackPressed()\\n // 调用 FlutterFragment 的 onBackPressed() 方法\\n flutterFragment?.onBackPressed()\\n }\\n\\n override fun onRequestPermissionsResult(\\n requestCode: Int,\\n permissions: Array<String?>,\\n grantResults: IntArray\\n ) {\\n super.onRequestPermissionsResult(requestCode, permissions, grantResults)\\n // 调用 FlutterFragment 的 onRequestPermissionsResult() 方法,传入请求码、权限数组和授权结果数组\\n flutterFragment?.onRequestPermissionsResult(\\n requestCode,\\n permissions,\\n grantResults\\n )\\n }\\n\\n override fun onActivityResult(\\n requestCode: Int,\\n resultCode: Int,\\n data: Intent?\\n ) {\\n super.onActivityResult(requestCode, resultCode, data)\\n // 调用 FlutterFragment 的 onActivityResult() 方法,传入请求码、结果码和数据意图\\n flutterFragment?.onActivityResult(\\n requestCode,\\n resultCode,\\n data\\n )\\n }\\n\\n override fun onUserLeaveHint() {\\n // 调用 FlutterFragment 的 onUserLeaveHint() 方法\\n flutterFragment?.onUserLeaveHint()\\n }\\n\\n override fun onTrimMemory(level: Int) {\\n super.onTrimMemory(level)\\n // 调用 FlutterFragment 的 onTrimMemory() 方法,传入内存清理级别\\n flutterFragment?.onTrimMemory(level)\\n }\\n}\\n
\\n效果如下图所示:
\\n更多关于 FlutterFragment 的用法可以看 Flutter 中文文档
\\n我们还可以通过 FlutterView
来将 Flutter 界面嵌入某个 View 中。代码示例如下:
class FlutterViewActivity : FragmentActivity() {\\n\\n private var flutterEngine: FlutterEngine? = FlutterEngineCache.getInstance().get(\\"my_engine_id\\")\\n\\n override fun onCreate(savedInstanceState: Bundle?) {\\n super.onCreate(savedInstanceState)\\n setContentView(R.layout.layout_flutter_view)\\n val flutterView = FlutterView(this)\\n val lp = FrameLayout.LayoutParams(\\n ViewGroup.LayoutParams.MATCH_PARENT,\\n ViewGroup.LayoutParams.MATCH_PARENT\\n )\\n val flContainer = findViewById<FrameLayout>(R.id.container)\\n flContainer.addView(flutterView, lp)\\n flutterEngine?.let {\\n flutterView.attachToFlutterEngine(it)\\n }\\n }\\n\\n override fun onResume() {\\n super.onResume()\\n flutterEngine?.lifecycleChannel?.appIsResumed()\\n }\\n\\n override fun onPause() {\\n super.onPause()\\n flutterEngine?.lifecycleChannel?.appIsInactive()\\n }\\n\\n override fun onStop() {\\n super.onStop()\\n flutterEngine?.lifecycleChannel?.appIsPaused()\\n }\\n\\n}\\n
\\n效果如下图所示:
\\nFlutter 提供了 MethodChannel 来与 Android 进行数据传输。下面我们来看看如何使用这个类来实现双方的通信。
\\n在 Android 中的代码如下所示:
\\nprivate var flutterEngine: FlutterEngine? = FlutterEngineCache.getInstance().get(\\"my_engine_id\\")\\n// MethodChannel 是一个双向通信通道,用于在 Flutter 和 Android 之间传递消息\\nprivate val methodChannel by lazy {\\n val engine = flutterEngine ?: return@lazy null\\n MethodChannel(engine.dartExecutor.binaryMessenger, CHANNEL)\\n}\\n\\nprivate fun listenFlutterEvent() {\\n // 处理来自 Flutter 的方法调用\\n methodChannel?.setMethodCallHandler { call, result ->\\n when (call.method) {\\n // flutter 传输数据过来\\n \\"getFlutterVersion\\" -> {\\n Log.d(TAG, \\"getFlutterVersion ${call.argument<String>(\\"flutterVersion\\")}\\")\\n }\\n // flutter 调用 Android 的方法\\n \\"getPlatformVersion\\" -> {\\n result.success(\\"Android ${android.os.Build.VERSION.RELEASE}\\")\\n }\\n else -> result.notImplemented()\\n }\\n }\\n}\\n
\\n其中 setMethodCallHandler 方法用于设置处理来自 Flutter 的方法调用回调。在回调中,我们可以使用 call.argument
方法获取从 Flutter 中传入的值。也可以使用 result.success
设置返回值给 Flutter。
Flutter 端的代码示例如下:
\\nstatic const String CHANNEL = \\"com.example.flutterandroidproject/channel\\";\\nvoid getPlatformVersion() async {\\n// 通过 MethodChannel 获取 platform 的版本信息\\nString platformVersion = await MethodChannel(CHANNEL).invokeMethod(\'getPlatformVersion\');\\nsetState(() {\\n _platformVersion = platformVersion;\\n});\\n}\\n\\nvoid getFlutterVersion() async {\\n// 通过 MethodChannel 传递给 Android 当前 flutter 版本信息\\nawait MethodChannel(CHANNEL).invokeMethod(\'getFlutterVersion\', {\\"flutterVersion\\": \\"1.0.0\\"});\\n}\\n
\\n其中 invokeMethod 方法,用于从 Flutter 调用 Android 方法。可以看到我们可以通过获取 invokeMethod 的返回值来获取 Android 端返回的数据。
\\n如果发生了如图所示的构建问题,需要将根目录下的 setting.gradle.kts 中的 repositoriesMode
改成 RepositoriesMode.PREFER_SETTINGS
,代码示例如下:
dependencyResolutionManagement {\\n repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)\\n repositories {\\n ...\\n }\\n}\\n\\n
\\n如果遇到这个错误,我们需要在项目的 setting.gradle.kts 中增加如下代码:
\\npluginManagement {\\n repositories {\\n maven { url = uri(\\"https://storage.googleapis.com/download.flutter.io\\") }\\n\\n ...\\n }\\n}\\ndependencyResolutionManagement {\\n repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)\\n repositories {\\n maven { url = uri(\\"https://storage.googleapis.com/download.flutter.io\\") }\\n\\n ...\\n }\\n}\\n
\\nIAP全称In App Purchase,中文名:应用内支付服务。是HarmonyOS为开发者提供便捷的应用内支付体验和简便的接入流程,让开发者聚焦应用本身的业务能力,助力开发者商业变现。开发者应用可通过使用IAP Kit提供的系统级支付API快速启动IAP收银台,即可实现应用内支付。
\\n⚠️值得注意的是,企业开发者可以选择使用微信或者支付宝支付,而个人开发者只能使用华为提供的IAPKit,否则上架会不过审。
\\n使用IAPKit之前要先开通商户服务
。这个要去通过邮件联系,然后会让你加微信等,打印并且签署《华为商户服务协议》,再寄回去给华为。不知道现在有没有优化这个流程🤔。
IAPKit无法使用虚拟机调试,一定只能使用真机。(这里我已经帮大家用各种方法试过了虚拟机了,不行的😭),现在最便宜的真机应该是nova13
nova12
系列。
IAPKit要求证书是手动签名的方式,不是使用Dev-eco studio
自动生成的。这个坑在发现之前也花了我很长时间。
首先要打开api开关 => 官方文档
\\n然后下载一个密钥
。
鸿蒙官方的支付依赖或多或少用不了,需要自己去github下载了他们的包 (链接: gitcode.com/openharmony…) 然后修改一下。
\\n修改成不会报错就行。
\\n然后引用本地的库:
\\nin_app_purchase:\\n# git:\\n# url: \\"https://gitee.com/openharmony-sig/flutter_packages.git\\"\\n# path: \\"packages/in_app_purchase/in_app_purchase\\"\\n path: packages/in_app_purchase/in_app_purchase\\n
\\n上面注释的那些就是原来想直接引用远程仓库的,但不行,最后才用了本地仓库。
\\n这个 in_app_purchase
跟Flutter官方维护的包名字一模一样,但少了一些逻辑,如鸿蒙官方的 in_app_purchase
是本地执行验证逻辑的,没有服务器确认的这一步。
因此有需要使用的小伙伴可以进行修改,把验证那一步放到服务器上。
\\n找到这个文件MethodCallHandlerImpl.ets
在类 MethodCallHandlerImpl
中加入 一个成员变量。
private receiptData: string = \'\'\\n
\\n在 if (type == iap.ProductType.AUTORENEWABLE) {
和 } else if (type == iap.ProductType.CONSUMABLE || type == iap.ProductType.NONCONSUMABLE) {
这两句话下面增加 (可以ctl+F
快速查找)
this.receiptData = JSON.parse(createPurchaseResult.purchaseData).jwsPurchaseOrder\\n
\\n最后修改MethodNames.RETRIEVE_RECEIPT_DATA
case MethodNames.RETRIEVE_RECEIPT_DATA: \\n result.success(this.receiptData) // 返回获取的数据\\n break;\\n
\\n有in_app_purchase
库使用经验的小伙伴这个时候就可以直接使用in_app_purchase
的example ( pub.dev/packages/in… ) 中的大部分逻辑的。以上的修改就是为了使得_validPurchase中的PurchaseDetails
的serverVerificationData
属性可用。
_validPurchase(PurchaseDetails prod) async {\\n CallBack callBack = await validPurchased(\\n prod,\\n prod.verificationData.serverVerificationData, // 这个就是刚刚修改的获得的receiptData\\n prod.purchaseID!,\\n prod.productID); // 向服务器验证\\n if (callBack.success){\\n \\n }\\n}\\n
\\n验证逻辑可以参考官方的这个图 (原图地址: developer.huawei.com/consumer/cn…)
\\n由于鸿蒙5是一个新平台,因此坑可能会比较多,但一步一步来还是能解决问题的。欢迎大家还有什么问题在评论区提问。
","description":"一、什么是IAP IAP全称In App Purchase,中文名:应用内支付服务。是HarmonyOS为开发者提供便捷的应用内支付体验和简便的接入流程,让开发者聚焦应用本身的业务能力,助力开发者商业变现。开发者应用可通过使用IAP Kit提供的系统级支付API快速启动IAP收银台,即可实现应用内支付。\\n\\n⚠️值得注意的是,企业开发者可以选择使用微信或者支付宝支付,而个人开发者只能使用华为提供的IAPKit,否则上架会不过审。\\n\\n二、准备步骤\\n1. 商户开通(!!耗时将近1个月)\\n\\n使用IAPKit之前要先开通商户服务。这个要去通过邮件联系,然后会让你加微信等…","guid":"https://juejin.cn/post/7489315969318961163","author":"Jalor","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-06T03:17:27.977Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/79887e492cec4694b40b4397f771c08b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgSmFsb3I=:q75.awebp?rk3s=f64ab15b&x-expires=1744514246&x-signature=UK3BQ6HgeUtPbxqN04GBXSS3bAo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/511ac8a85d32487787f402e4265faf1e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgSmFsb3I=:q75.awebp?rk3s=f64ab15b&x-expires=1744514246&x-signature=ibpWE4l5bW%2FWzTRqI4101DpeDHY%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","前端","HarmonyOS","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 教程(十二)异步编程","url":"https://juejin.cn/post/7489007289065832474","content":"不同于 Java、C++ 使用多线程来实现异步编辑,Dart 没有多线程的概念,它是通过事件循环和隔离来实现异步编程的。
\\n事件循环的原理就是“主线程”循环拉取队列中的事件来执行,如果事件有IO操作或者延时等操作,则会将该事件挂起,并执行下一个任务或者事件。事情循环类似于 kotlin 的指定了线程调度的协程。其运作原理,如下图所示:
\\n可以看到事件循环有两个队列,一个是微任务队列(Microtask queue),另一个是事件队列(Event queue)。其中微任务队列包含Flutter内部的微任务,主要通过scheduleMicrotask来调度;事件队列包含外部事件,例如I/O、Timer和绘制事件等。
\\nDart 的单线程异步能方便处理 io 密集型 的任务,但是对于 cpu 密集型任务则需要使用 隔离(isolate
)机制来处理
关于 Dart 的单线程异步,主要有 Future、async和await、Stream 三部分知识点。下面分别介绍
\\nFuture 表示一个异步操作的最终完成(或失败)及其结果值的表示。比如说,我们往文件写入文字就会返回一个 Future 对象,如下所示:
\\nFile file = File(filePath);\\nFuture<File> futureFile = file.writeAsString(\\"hello world\\");\\n
\\n可以看到 Future 就是用于处理异步操作的,异步处理成功了就执行成功的操作,异步处理失败了就捕获错误或者停止后续操作。一个Future只会对应一个结果,要么成功,要么失败。一些常见的 Future 示例如下:
\\nFuture.delayed(Duration(seconds: 2),(){\\n //return \\"hi world!\\";\\n throw AssertionError(\\"Error\\");\\n}).then((data){\\n //执行成功会走到这里 \\n print(data);\\n}).catchError((e){\\n //执行失败会走到这里 \\n print(e);\\n}).whenComplete((){\\n //无论成功或失败都会走到这里\\n});\\n
\\nFuture.wait([\\n // 2秒后返回结果 \\n Future.delayed(Duration(seconds: 2), () {\\n return \\"hello\\";\\n }),\\n // 4秒后返回结果 \\n Future.delayed(Duration(seconds: 4), () {\\n return \\" world\\";\\n })\\n]).then((results){\\n print(results[0]+results[1]);\\n}).catchError((e){\\n print(e);\\n});\\n
\\nasync
用来表示函数是异步的,定义的函数会返回一个Future
对象,可以使用 then 方法添加回调函数。await
后面是一个Future
,表示等待该异步任务完成,异步完成后才会往下走;await
必须出现在 async
函数内部。代码示例如下:
\\ntask() async {\\n try{\\n String id = await login(\\"alice\\",\\"******\\");\\n String userInfo = await getUserInfo(id);\\n await saveUserInfo(userInfo);\\n //执行接下来的操作 \\n } catch(e){\\n //错误处理 \\n print(e); \\n } \\n}\\n
\\n\\n\\n注意:在 Dart 中,
\\nasync/await
只是一个语法糖,编译器或解释器最终都会将其转化为一个 Future 的调用链。
Stream 也是用于接收异步事件数据,和 Future 不同的是,它可以接收多个异步操作的结果(成功或失败)。代码示例如下:
\\nStream.fromFutures([\\n // 1秒后返回结果\\n Future.delayed(Duration(seconds: 1), () {\\n return \\"hello 1\\";\\n }),\\n // 抛出一个异常\\n Future.delayed(Duration(seconds: 2),(){\\n throw AssertionError(\\"Error\\");\\n }),\\n // 3秒后返回结果\\n Future.delayed(Duration(seconds: 3), () {\\n return \\"hello 3\\";\\n })\\n]).listen((data){\\n print(data);\\n}, onError: (e){\\n print(e.message);\\n},onDone: (){\\n\\n});\\n
\\n我们还可以使用 Stream 来实现事件流。为了控制事件流,通常使用StreamController来进行管理。例如,为了向事件流中流入数据,StreamController提供了类型为StreamSink的sink属性作为数据的入口,同时StreamController也提供了Stream属性作为数据的出口,如下图所示:
\\n代码示例如下:
\\nclass StreamPage extends StatefulWidget {\\n StreamPage({Key? key, this.title}) : super(key: key);\\n\\n final String? title;\\n\\n @override\\n _StreamState createState() => _StreamState();\\n}\\n\\nclass _StreamState extends State<StreamPage> {\\n final StreamController<int> _streamController=StreamController<int>();\\n int _counter=0;\\n\\n @override\\n void dispose() {\\n _streamController.close();\\n super.dispose();\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: Text(widget.title ?? \\"\\"),\\n ),\\n body: Center(\\n child: Column(\\n mainAxisAlignment: MainAxisAlignment.center,\\n children: <Widget>[\\n Text(\\n \'自动累加的数据\',\\n style: TextStyle(fontSize: 24),\\n ),\\n StreamBuilder<int>(\\n stream: _streamController.stream,\\n initialData: 0,\\n builder: (BuildContext context,AsyncSnapshot<int> snapshot){\\n return Text(\\n \'${snapshot.data}\',\\n style: TextStyle(fontSize: 24),\\n );\\n },\\n ),\\n FilledButton(\\n child: Text(widget.title ?? \\"\\"),\\n onPressed: () => _streamController.sink.add(++_counter),\\n ),\\n ],\\n ),\\n ),\\n );\\n }\\n}\\n
\\n所有的 Flutter Dart 代码都是在隔离上运行的,而对应的Android UI主线程在Flutter中被称为主隔离(main isolate)。我们可以使用 spawnUri
和 spawn
方法创建隔离并执行指定任务。其中 spawnUri
方法,基于给定库的URI来产生一个隔离;而 spawn
方法,根据当前隔离的根库生成一个隔离。代码示例如下:
// 用于与另一个隔离通信\\nfinal receivePort = new ReceivePort();\\nIsolate.spawn(_isolate2, receivePort.sendPort);\\n\\n// 具体的任务\\n_isolate2(SendPort replyTo) async {\\n ...\\n}\\n
\\n这些隔离有着自己的内存和单线程控制的运行实体,因此隔离之间是没有共享内存。如果隔离之间需要通信,则需要消息传递。代码示例如下:
\\n//主隔离\\n_main() async {\\n //隔离所需要的参数必须要有SendPort,而SendPort又需要ReceivePort来创建\\n final receivePort = new ReceivePort();\\n //使用Isolate.spawn创建隔离,其中_isolate2是我们自己实现的\\n await Isolate.spawn(_isolate2, receivePort.sendPort);\\n //发送一个message,这是它的sendPort\\n var sendPort = await receivePort.first;\\n var message = await sendMessage(sendPort, \\"你好\\");\\n print(\\"message:$message\\");//等待消息返回\\n}\\n//isolate2\\n_isolate2(SendPort replyTo) async {\\n //创建一个ReceivePort,用于接收消息\\n var port = ReceivePort();\\n //把它发送给主隔离,以便主隔离可以给它发送消息\\n replyTo.send(port.sendPort);\\n port.listen((message) {//监听消息,从Port获取\\n SendPort send = message[0] as SendPort;\\n String str = message[1] as String;\\n print(str);\\n send.send(\\"应答\\");\\n port.close();\\n });\\n}\\n//使用Port进行通信,同时接收返回应答\\nFuture sendMessage(SendPort port,String str) {\\n ReceivePort receivePort=ReceivePort();\\n port.send([receivePort.sendPort, str]);\\n return receivePort.first;\\n}\\n
\\n如果我们只想要请求一次数据,而不需要像上面一样相互通信,则可以使用 compute
方法。这个函数非常简单,它只有两个参数,第一个参数是需要执行耗时任务的方法的名称,第二个参数是前面方法需要传递的参数。代码示例如下
var _count;\\n// 耗时任务\\nint countEven(int num) {\\n int count = 0;\\n while (num > 0) {\\n if (num % 2 == 0) {\\n count++;\\n }\\n num--;\\n print(count);\\n }\\n return count;\\n}\\n_call_compute() async{\\n _count = await compute(countEven, 1000000000);\\n}\\n\\n
\\n需要注意,compute() 函数中运行的方法必须是顶级方法或者是static方法;而且 compute() 函数只能传递一个参数,它的返回值也只有一个。
\\nStream
用法及场景总结开发过程中遇到的 Stream
常见方法。 介绍各种 `Str - 掘金HarmonyOS: 使用 ArkTS/ArkUI 的窗口管理接口实现全屏
\\nAndroid: 结合原生 Android 配置文件和 Flutter API 实现全屏
\\niOS: 通过 Flutter 的 SystemChrome API 实现全屏
\\nFlutter: 采用 3.22.1-0.0.pre.32 版本,结合 Dart 3.4.0 进行跨平台开发
\\nHarmonyOS 通过 ArkTS/ArkUI 的窗口管理接口实现全屏效果:
\\n\\nonWindowStageCreate(windowStage: window.WindowStage) {\\n\\nsuper.onWindowStageCreate(windowStage);\\n\\nwindowStage.getMainWindow((err: BusinessError, data) => {\\n\\nif (err.code) {\\n console.error(\'Failed to obtain the main window. Cause: \' + JSON.stringify(err));\\n return;\\n}\\n\\nlet windowClass = data;\\n// 设置导航栏、状态栏不显示\\nlet names: Array<\'status\' | \'navigation\'> = [];\\nwindowClass.setWindowSystemBarEnable(names)\\n .then(() => {\\n console.info(\'Succeeded in setting the system bar to be invisible.\');\\n })\\n .catch((err: BusinessError) => {\\n console.error(\'Failed to set the system bar to be invisible. Cause:\' + JSON.stringify(err));\\n });\\n });\\n}\\n\\n
\\nHarmonyOS的方法相对简单,通过设置空数组到setWindowSystemBarEnable
方法,可以同时隐藏状态栏和导航栏,实现全屏效果。
Android的全屏实现较为复杂,需要同时修改多个配置文件:
\\n<style name=\\"LaunchTheme\\" parent=\\"@android:style/Theme.Light.NoTitleBar\\">\\n <item name=\\"android:windowBackground\\">@drawable/launch_background</item>\\n <item name=\\"android:windowFullscreen\\">true</item>\\n <item name=\\"android:windowLayoutInDisplayCutoutMode\\">shortEdges</item>\\n</style>\\n<style name=\\"NormalTheme\\" parent=\\"@android:style/Theme.Light.NoTitleBar\\">\\n <item name=\\"android:windowBackground\\">?android:colorBackground</item>\\n <item name=\\"android:windowFullscreen\\">true</item>\\n <item name=\\"android:windowLayoutInDisplayCutoutMode\\">shortEdges</item>\\n</style>\\n
\\n<activity\\n\\nandroid:name=\\".MainActivity\\"\\n\\n...\\n\\nandroid:theme=\\"@style/LaunchTheme\\"\\n\\nandroid:resizeableActivity=\\"true\\">\\n\\n...\\n\\n</activity>\\n\\n
\\n\\noverride fun onCreate(savedInstanceState: Bundle?) {\\n\\n super.onCreate(savedInstanceState)\\n\\n // 设置状态栏透明\\n\\n window.statusBarColor = Color.TRANSPARENT\\n\\n if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {\\n\\n // Android 11 及以上使用新 API\\n\\n window.setDecorFitsSystemWindows(false)\\n\\n window.insetsController?.let {\\n\\n it.hide(WindowInsets.Type.statusBars() or WindowInsets.Type.navigationBars())\\n\\n it.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE\\n\\n }\\n\\n } else {\\n\\n // 处理 Android 10 及以下版本\\n\\n @Suppress(\\"DEPRECATION\\")\\n\\n window.decorView.systemUiVisibility = (\\n\\n View.SYSTEM_UI_FLAG_LAYOUT_STABLE\\n\\n or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN\\n\\n or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION\\n\\n or View.SYSTEM_UI_FLAG_FULLSCREEN\\n\\n or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION\\n\\n or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY\\n )\\n // 状态栏透明\\n\\n window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)\\n\\n window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)\\n\\n window.statusBarColor = Color.TRANSPARENT\\n\\n // 确保内容显示在状态栏后面\\n\\n window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)\\n }\\n\\n // 设置允许内容延伸到刘海区域\\n\\n if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {\\n\\n val layoutParams = window.attributes\\n\\n layoutParams.layoutInDisplayCutoutMode =\\n\\n WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES\\n\\n window.attributes = layoutParams\\n\\n }\\n\\n}\\n\\n
\\n\\nif (Platform.isAndroid) {\\n\\n SystemChrome.setEnabledSystemUIMode(\\n\\n SystemUiMode.edgeToEdge,\\n\\n overlays: [],\\n\\n );\\n\\n SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(\\n\\n statusBarColor: Colors.transparent,\\n\\n systemNavigationBarColor: Colors.transparent,\\n\\n systemNavigationBarDividerColor: Colors.transparent,\\n\\n ));\\n\\n}\\n\\n
\\n在iOS平台上,全屏实现相对简单,主要通过Flutter的SystemChrome API实现:
\\n\\n// 在Flutter代码中设置\\n\\nif (Platform.isIOS) {\\n\\nSystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);\\n\\n}\\n\\n
\\n这种方式会将iOS设备上的状态栏完全隐藏,使应用内容能够占据整个屏幕空间。
\\n不同Android版本API差异较大
\\n需处理刘海屏和挖孔屏特殊情况
\\n全屏模式可能与键盘弹出交互有冲突
\\nAPI相对统一,但仍在快速发展中
\\n测试设备有限,需要更多真机验证
\\n版权声明
\\n本文为作者原创技术总结,所有内容均为作者独立研究与实践所得。未经作者书面授权,任何单位或个人不得以任何形式复制、转载、摘编或用于商业用途。如需引用,请注明出处并保留原文完整性。
\\n作者保留所有权利,侵权必究。
\\n© 2025 [享时科技工作室] 版权所有
","description":"技术栈说明 HarmonyOS: 使用 ArkTS/ArkUI 的窗口管理接口实现全屏\\n\\nAndroid: 结合原生 Android 配置文件和 Flutter API 实现全屏\\n\\niOS: 通过 Flutter 的 SystemChrome API 实现全屏\\n\\nFlutter: 采用 3.22.1-0.0.pre.32 版本,结合 Dart 3.4.0 进行跨平台开发\\n\\nHarmonyOS 全屏实现\\n\\nHarmonyOS 通过 ArkTS/ArkUI 的窗口管理接口实现全屏效果:\\n\\n\\nonWindowStageCreate(windowStag…","guid":"https://juejin.cn/post/7489043337289089051","author":"王喆","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-05T11:09:16.633Z","media":null,"categories":["前端","HarmonyOS","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 伪3D绘制#03 | 轴测投影原理分析","url":"https://juejin.cn/post/7488943445005090842","content":"通过前两篇,我们稀里糊涂地完成了很炫的,基于二维平面绘制三维空间的效果。那其中蕴含的原理是什么呢,本篇将和你一起揭开轴测投影的神秘面纱。
\\n首先我们先思考一个简单的问题,如下图所示:
\\n\\n\\n红线长度为 240,与水平方向夹角为 30°, 求红线端点在屏幕坐标系上的坐标:
\\n
可以想象一下,红线是一根木棍,手电筒沿 y 轴方向照射,在 x 轴上产生了影子。那么:
\\n\\n\\n影子的长度就是到右侧端点的 x 坐标
\\n
由于知道了线和x的夹角,用小学二年级学过的三角函数不难算出,x 坐标为 240 * cos(30°)
。同理,可以在 y 轴上投影得到 y 坐标,这样端点 p 的坐标为: (240*cos(30°), 240*sin(30°))
铛铛铛,敲黑板!!!
\\n\\n\\n此时红线上 距原点距离为 d 的任何一点,其坐标可以写为
\\n(d*cos(30°), d*sin(30°))
那么现在请问,如果以红线作为三维坐标系的 x 轴, 那么 距原点距离为 d 的点表示的是什么?
\\n红线上 距原点距离为 d 的点,也就是三维坐标中 x 轴上的点,坐标为 (d,0,0)
。轴线上的点分析完毕,接下来分析一下三维 X-Y 地平面上点,在屏幕坐标系上的投影。
\\n如下所示,三维坐标系中的 p 点坐标 (160,40,0), 通过两条辅助线可以更好地看出点在空间中的位置:
如下所示,三维点 p(160,40,0) 投影到屏幕的二维坐标上,如何计算坐标呢?
\\n三维坐标 p 向三维坐标系中的 x,y 轴投影,可以得到 p_x_3d
和 p_y_3d
两个点。然后轴上的这两个点再向屏幕坐标系 x 轴进行投影,不难看出:
\\n\\n下面 x 轴上两个较粗线段的差值就是 p.x 的值。
\\n
这就印证了 project 代码中二维坐标系的 x 的计算方式是 : (p.x - p.y) * cos(angle)
\\n同理,对于二维坐标系中 y 点的计算也是同理。将 p 在三维坐标系中的分量,沿水平方向投影:
\\n\\n\\ny 轴上的两段直线长度之和就 p.y 的值
\\n
这就印证了 project 代码中二维坐标系的 y 的计算方式是 : (p.x + p.y) * sin(angle)
\\n可能有人看到了,y 轴还有一个减去 p.z 的计算,这个意味着什么呢?因为目前我们研究的是在 X-Y 平面上的三维点,所以 z 轴坐标为 0 ,接下来就看看有 z 轴时的情况。
\\n这里我们的 z 轴是垂直向上的,p(160,40,80) 就是将之前的点,向上平移 80 。所以对于屏幕坐标系而言, x 坐标不变:
\\n由于向上平移,所以 y 坐标需要减去三维坐标的 z 值,这也是为什么 project 投影映射中 y 坐标要减 p.z
的原因:
这样 project 的两行转换的代码就解释完毕了,你看懂了吗? 分析期间我们运用了很多将点投影到坐标轴上的动作,来计算三维点坐落在二维坐标系中的确切位置。这种技术被称为 轴测投影 ,另外夹角是 30° 的轴测投影\\n比较常用,被称之为 等轴测投影,该角度夹一般效果最好,广泛用于工程制图和像素艺术中。
\\n现在静态的三维坐标的轴测投影原理介绍完毕,那么让三维空间沿 z 轴旋转的秘密又是什么呢?我们下篇再见,敬请期待 ~
更多文章和视频知识资讯,大家可以关注我的公众号、掘金和 B 站 。
\\n","description":"通过前两篇,我们稀里糊涂地完成了很炫的,基于二维平面绘制三维空间的效果。那其中蕴含的原理是什么呢,本篇将和你一起揭开轴测投影的神秘面纱。 1. 投影是什么\\n\\n首先我们先思考一个简单的问题,如下图所示:\\n\\n红线长度为 240,与水平方向夹角为 30°, 求红线端点在屏幕坐标系上的坐标:\\n\\n可以想象一下,红线是一根木棍,手电筒沿 y 轴方向照射,在 x 轴上产生了影子。那么:\\n\\n影子的长度就是到右侧端点的 x 坐标\\n\\n由于知道了线和x的夹角,用小学二年级学过的三角函数不难算出,x 坐标为 240 * cos(30°)。同理,可以在 y 轴上投影得到…","guid":"https://juejin.cn/post/7488943445005090842","author":"张风捷特烈","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-04T09:06:12.740Z","media":[{"url":"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9930c0d1e7b64db9a705cdbd5764945f~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1920&h=1200&s=449758&e=png&b=fefbfb","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6639cfc186084555999bbe1c99dc2805~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1000&h=600&s=15450&e=png&b=000000","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/584d7ca7f2944630b8e917046b291c3c~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1000&h=600&s=16962&e=png&b=000000","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/755cb1359ff14eae949035fe2cca68c8~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1000&h=600&s=21871&e=png&b=000000","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8844e4dcb4dc4c59ba83ff571b721296~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1000&h=600&s=16804&e=png&b=000000","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2da4b12d462b41b6bc7385af95acddbf~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1000&h=600&s=21286&e=png&b=000000","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ede5202ba1ac49dc9f5f02dadd4bcc39~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1000&h=600&s=23240&e=png&b=000000","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d70f1f941fda4085beccb868e52ddfea~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1471&h=860&s=72199&e=png&b=000000","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1522de430c13443799cf87db6ed03f5f~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1471&h=860&s=77435&e=png&b=000000","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2fe32b26c31143399133c107659acb6c~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=594&h=155&s=19205&e=png&b=fffefe","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/46b875ceab5d44b088aa3bc3f25a9e9d~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1471&h=860&s=75458&e=png&b=000000","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fcfc835dffe24e28b2b06d2714f9faa8~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=548&h=114&s=15183&e=png&b=fffefe","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/476221ecda0b4d00948856320e47e6e8~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1470&h=860&s=43207&e=png&b=000000","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f7ab93007c0c4360ab8b27b9cd293756~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=571&h=138&s=18743&e=png&b=fffefe","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Canvas"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 教程(十一)多语言支持","url":"https://juejin.cn/post/7488741942378758170","content":"在 Flutter 中如果需要支持多语言,则须添加 flutter_localizations
包,代码示例如下:
dependencies:\\n flutter_localizations:\\n sdk: flutter\\n
\\n包下载完成后,就可以指定 MaterialApp
的 localizationsDelegates
和 supportedLocales
。其中 localizationsDelegates
表示本地化委托,用于更改Flutter Widget默认的提示语,按钮text等;而 supportedLocales
表示我们的应用支持的语言列表。代码示例如下:
import \'package:flutter_localizations/flutter_localizations.dart\';\\n\\nMaterialApp(\\n localizationsDelegates: [\\n // 本地化的代理类\\n // GlobalMaterialLocalizations.delegate 为Material 组件库提供的本地化\\n // 的字符串和其他值,它可以使Material 组件支持多语言\\n GlobalMaterialLocalizations.delegate,\\n // GlobalWidgetsLocalizations.delegate 定义组件默认的文本方向,\\n // 从左到右或从右到左,这是因为有些语言的阅读习惯并不是从左到右,比如如阿拉伯语就是从右向左的\\n GlobalWidgetsLocalizations.delegate,\\n ],\\n supportedLocales: [\\n // Locale 用来标识用户的语言环境\\n const Locale(\'en\', \'US\'), // 美国英语\\n const Locale(\'zh\', \'CN\'), // 中文简体\\n //其他Locales\\n ],\\n // ...\\n)\\n
\\n添加完上述代码后,默认组件(即Material组件库)的文本内容根据系统的语言进行了转换。但是,我们自己代码中设置的文本并不会转换。如果要让我们自己的组件支持多语言,则需要自定义 Localizations
。
第一步创建我们自己的 Localizations 类,该类会根据不同的语言返回不同的标题字符串值,代码示例如下:
\\n//Locale资源类\\nclass DemoLocalizations {\\n DemoLocalizations(this.isZh);\\n\\n //是否为中文\\n bool isZh = false;\\n\\n //为了使用方便,我们定义一个静态方法\\n static DemoLocalizations of(BuildContext context) {\\n return Localizations.of<DemoLocalizations>(context, DemoLocalizations);\\n }\\n\\n //Locale相关值,title为应用标题\\n String get title {\\n return isZh ? \\"Flutter应用\\" : \\"Flutter APP\\";\\n }\\n\\n //... 其他的值\\n}\\n
\\n第二步,实现Delegate类。Delegate类的职责是在Locale改变时加载新的Locale资源,代码示例如下:
\\nclass DemoLocalizationsDelegate\\n extends LocalizationsDelegate<DemoLocalizations> {\\n const DemoLocalizationsDelegate();\\n\\n // 判断是否支持某个Local\\n @override\\n bool isSupported(Locale locale) => [\'en\', \'zh\'].contains(locale.languageCode);\\n\\n // Flutter会调用此类加载相应的Locale资源类\\n @override\\n Future<DemoLocalizations> load(Locale locale) {\\n print(\\"$locale\\");\\n return SynchronousFuture<DemoLocalizations>(\\n DemoLocalizations(locale.languageCode == \\"zh\\"),\\n );\\n }\\n\\n // shouldReload的返回值决定当Localizations组件重新build时,是否调用load方法重新加载Locale资源\\n // 但事实上,每当Locale改变时Flutter都会再调用load方法加载新的Locale,\\n // 无论shouldReload返回true还是false\\n @override\\n bool shouldReload(DemoLocalizationsDelegate old) => false;\\n}\\n
\\n第三步,往代码中添加多语言的支持
\\n// 设置代理\\nlocalizationsDelegates: [\\n // 本地化的代理类\\n GlobalMaterialLocalizations.delegate,\\n GlobalWidgetsLocalizations.delegate,\\n // 注册我们的Delegate\\n DemoLocalizationsDelegate()\\n],\\n\\n// 使用 DemoLocalizations 获取语言的值\\nScaffold(\\n appBar: AppBar(\\n //使用Locale title \\n title: Text(DemoLocalizations.of(context).title),\\n ),\\n ... //省略无关代码\\n ) \\n
\\n从上面可以看到,使用默认的库实现多语言支持非常麻烦。这里推荐使用三方的多语言库,这里介绍两种,分别是 easy_localization 和 intl。
\\n首先添加依赖
\\ndependencies:\\n easy_localization: ^3.0.7+1\\n
\\n添加完成之后,在Android设备上就可以直接使用easy_localization库了。但如果你使用的是iOS设备的话,还需要在其ios/Runner/Info.plist目录中添加如下代码:
\\n<key>CFBundleLocalizations</key>\\n<array>\\n <string>en</string>\\n <string>nb</string>\\n</array>\\n
\\n使用easy_localization国际化库,必须为它提供一个可用的翻译文件目录,就像配置图片资源文件一样。这里,我们将翻译文件放置在asset/langs目录下
\\n需要注意:翻译文件的命名格式为 /语言码−{国家/地区码}.json,如果还需要配置其他语言,可以按照这个格式进行相应的配置。然后,我们还需要将翻译文件配置到pubspec.yaml文件中,代码如下所示:
\\nflutter:\\n assets:\\n - asset/langs/en-US.json\\n - asset/langs/zh-CN.json\\n
\\n最后,我们在 main.dart 文件中增加 easy_localization 的配置,代码如下所示:
\\nimport \'package:flutter/material.dart\';\\nimport \'package:flutter_localizations/flutter_localizations.dart\';\\nimport \'package:easy_localization/easy_localization.dart\';\\n\\nvoid main() {\\n WidgetsFlutterBinding.ensureInitialized();\\n await EasyLocalization.ensureInitialized();\\n \\n runApp(\\n EasyLocalization(\\n supportedLocales: [Locale(\'en\', \'US\'), Locale(\'de\', \'DE\')],\\n path: \'assets/translations\', // 语言资源包目录\\n fallbackLocale: Locale(\'en\', \'US\'),\\n child: MyApp()\\n ),\\n );\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n localizationsDelegates: context.localizationDelegates,\\n supportedLocales: context.supportedLocales,\\n locale: context.locale,\\n home: MyHomePage()\\n );\\n }\\n}\\n
\\n我们首先在 en-US.json与zh-CN.json 中设置对应的数据,示例如下:
\\n//en-US.json文件数据\\n{\\n \\"materialapp title\\": \\"Flutter Demo\\"\\n}\\n\\n//zh-CN.json文件数据\\n{\\n \\"materialapp title\\": \\"Flutter示例\\",\\n}\\n
\\n然后我们可以在项目中使用 tr
组件,该组件会把当前语言对应的文本传递给 Text 组件。代码示例如下:
Text(tr(\'materialapp title\'))\\n
\\n我们可以使用 EasyLocalization.of(context).locale =Locale(\'en\', \'US\')
手动切换语言来查看效果。
如下代码所示,先添加依赖
\\ndependencies:\\n intl: ^0.20.2\\n
\\n然后在 pubspec.yaml 文件中,启用 generate 标志。代码示例如下:
\\n# The following section is specific to Flutter.\\nflutter:\\n generate: true # Add this line\\n
\\n之后再 Flutter 项目的根目录下创建一个 l10n.yaml 的文件,文件内容如下:
\\n# 将应用资源包 (.arb) 的输入路径指定为 ${FLUTTER_PROJECT}/lib/l10n\\narb-dir: lib/l10n\\n# 将英文的语言模板设定为 app_en.arb\\ntemplate-arb-file: app_en.arb\\n# 指定 Flutter 生成本地化内容到 app_localizations.dart 文件\\noutput-localization-file: app_localizations.dart\\n
\\n在 ${FLUTTER_PROJECT}/lib/l10n
中,添加 app_en.arb
模板文件。如下
{\\n \\"helloWorld\\": \\"Hello World!\\",\\n \\"@helloWorld\\": {\\n \\"description\\": \\"The conventional newborn programmer greeting\\"\\n }\\n}\\n
\\n然后在同一目录,添加一个 app_es.arb 文件,对同一条消息做西班牙语的翻译
\\n{\\n \\"helloWorld\\": \\"¡Hola Mundo!\\"\\n}\\n
\\n完成上述步骤后,执行 flutter run
或者 flutter gen-l10n
命令来生成本地化文件 AppLocalizations.dart
。最后,我们需要在 main.drat 文件中增加对应的配置。代码示例如下:
import \'package:flutter_gen/gen_l10n/app_localizations.dart\';\\n\\n...\\n\\nconst MaterialApp(\\n title: \'Localizations Sample App\',\\n localizationsDelegates: [\\n AppLocalizations.delegate, // Add this line\\n GlobalMaterialLocalizations.delegate,\\n GlobalWidgetsLocalizations.delegate,\\n GlobalCupertinoLocalizations.delegate,\\n ],\\n supportedLocales: [\\n Locale(\'en\'), // English\\n Locale(\'es\'), // Spanish\\n ],\\n home: MyHomePage(),\\n);\\n
\\n完成上述配置后,我们就可以使用 AppLocalizations 文件了,代码示例如下:
\\nappBar: AppBar(\\n // The [AppBar] title text should update its message\\n // according to the system locale of the target platform.\\n // Switching between English and Spanish locales should\\n // cause this text to update.\\n title: Text(AppLocalizations.of(context)!.helloWorld),\\n),\\n
\\n先看报错提示信息:
\\nError (Xcode): Target aot_assembly_release failed: Exception: release/profile builds are only supported for physical devices. attempted to\\nbuild for simulator.\\n
\\n在Mac
中,通过 flutter create daily_note
新建一个项目,在VS Code
中添加 launch.json
文件,内容如下:
{\\n \\"configurations\\": [\\n {\\n \\"name\\": \\"daily_note (debug mode)\\",\\n \\"request\\": \\"launch\\",\\n \\"type\\": \\"dart\\"\\n }\\n ]\\n}\\n
\\n开始运行,结果出现开头的错误,非常的莫名其妙。
\\n首先运行 flutter doctor
[✓] Flutter (Channel stable, 3.29.0, on macOS 15.4 24E5228e darwin-arm64, locale zh-Hans-CN)\\n[!] Android toolchain - develop for Android devices (Android SDK version 35.0.1)\\n ! Some Android licenses not accepted. To resolve this, run: flutter doctor --android-licenses\\n[✓] Xcode - develop for iOS and macOS (Xcode 16.0)\\n[✓] Chrome - develop for the web\\n[✓] Android Studio (version 2024.1)\\n[✓] VS Code (version 1.98.2)\\n[!] Proxy Configuration\\n ! NO_PROXY is not set\\n[✓] Connected device (4 available)\\n[✓] Network resources\\n\\n! Doctor found issues in 2 categories.\\n
\\n在 iOS
上运行,所以 Xcode
没问题。
接着问 Trae
,直接把报错信息和配置文件发给它。然后回复结果是让我增加一行,用于指定 flutterMode
{\\n \\"configurations\\": [\\n {\\n \\"name\\": \\"daily_note (debug mode)\\",\\n \\"request\\": \\"launch\\",\\n \\"type\\": \\"dart\\",\\n \\"flutterMode\\": \\"debug\\"\\n }\\n ]\\n}\\n
\\n运行还是同样的报错。于是继续把launch.json
发给Trae
,反复尝试了还是不行,有点快崩溃了。
回想之前遇到真机运行拔线就退出程序的问题,当时是通过设置FLUTTER_BUILD_MODE
解决的,于是查看现在的新工程有没有这个设置。结果,真的没有。然后在 Xcode-Build Setting-User Defined
中添加 FLUTTER_BUILD_MODE=Debug
,重新运行后可以了。
Trae
很好,但不是万能的,有时候还得靠自己。想要掌握好新编程模式下的工具,还需要努力。
在 Dart 中,Isolate 是并发编程的核心概念,用于实现真正的并行执行。它的设计目标是解决多线程编程中的共享内存问题,通过 内存隔离 和 消息传递 确保线程安全。以下是 Isolate 的详细解释:
\\n特性 | Isolate | 传统线程 |
---|---|---|
内存共享 | 不共享,完全隔离 | 共享内存,需锁机制 |
通信方式 | 通过 SendPort 消息传递 | 通过共享内存或信号量 |
安全性 | 无竞态条件(Race Condition) | 需手动处理线程同步 |
创建开销 | 较高(约 2MB 内存) | 较低 |
Future
、Stream
)。SendPort
和 ReceivePort
发送和接收消息。List
、Map
、Uint8List
等可序列化数据。ReceivePort
和 SendPort
ReceivePort
:用于接收消息的端口,监听来自其他 Isolate 的数据。SendPort
:用于向目标 Isolate 发送消息的端口,类似“通信地址”。void main() async {\\n // 主 Isolate 创建 ReceivePort\\n final receivePort = ReceivePort();\\n\\n // 创建新 Isolate,并传递主 Isolate 的 SendPort\\n await Isolate.spawn(worker, receivePort.sendPort);\\n\\n // 接收来自 Worker Isolate 的消息\\n receivePort.listen((message) {\\n print(\'收到消息: $message\');\\n receivePort.close();\\n });\\n}\\n\\nvoid worker(SendPort mainSendPort) {\\n // 向主 Isolate 发送消息\\n mainSendPort.send(\'Hello from Worker!\');\\n}\\n
\\nIsolate.spawn
和 Isolate.run
Isolate.spawn
:手动创建 Isolate,需管理端口和生命周期。Isolate.run
(Dart 2.15+):简化 API,自动处理消息传递和资源释放。try-catch
捕获 Isolate 中的异常。ReceivePort
监听错误消息。语言/框架 | 并发模型 | 特点 |
---|---|---|
Dart | Isolate | 内存隔离,消息传递 |
Java | 线程 + 共享内存 | 需手动同步,易出现竞态条件 |
JavaScript | Web Worker | 类似 Isolate,用于浏览器环境 |
Go | Goroutine | 轻量级线程,共享内存通过 Channel 通信 |
ReceivePort
的消息队列实现异步通信。Isolate.spawn
、Isolate.run
、SendPort
、ReceivePort
。Isolate
用于实现并发,适合处理CPU密集型任务以避免阻塞主线程。以下是具体使用场景及代码示例:场景:计算斐波那契数列,避免阻塞UI。
\\nimport \'dart:isolate\';\\n\\nvoid main() async {\\n print(\'开始计算...\');\\n // 使用Isolate.run(Dart 2.15+)\\n final result = await Isolate.run(() => calculate(40));\\n print(\'结果:$result\'); // 输出:102334155\\n}\\n\\n// 耗时计算函数\\nint calculate(int n) {\\n if (n <= 1) return n;\\n return calculate(n - 1) + calculate(n - 2);\\n}\\n
\\n解释:
\\nIsolate.run()
自动创建Isolate,执行传入的函数,并返回结果。spawn
手动管理Isolate场景:需要更精细控制Isolate的生命周期。
\\nimport \'dart:isolate\';\\n\\nvoid main() async {\\n final receivePort = ReceivePort();\\n // 创建Isolate\\n final isolate = await Isolate.spawn(calculate, receivePort.sendPort);\\n print(\'开始计算...\');\\n\\n // 监听结果\\n receivePort.listen((message) {\\n print(\'结果:$message\');\\n receivePort.close(); // 关闭端口\\n isolate.kill(); // 终止Isolate\\n });\\n}\\n\\n// Isolate入口函数\\nvoid calculate(SendPort sendPort) {\\n final result = heavyCalculation();\\n sendPort.send(result); // 发送结果\\n}\\n\\nint heavyCalculation() {\\n return calculate(40); // 复用之前的计算函数\\n}\\n
\\n解释:
\\nIsolate.spawn
创建Isolate,需手动传递SendPort
。ReceivePort
监听来自Isolate的消息。场景:同时处理多个独立任务,提升效率。
\\nimport \'dart:isolate\';\\n\\nvoid main() async {\\n final tasks = [30, 35, 40];\\n final results = await Future.wait(\\n tasks.map((n) => Isolate.run(() => calculate(n)))\\n );\\n print(\'所有结果:$results\'); // 输出:[832040, 9227465, 102334155]\\n}\\n
\\n解释:
\\nFuture.wait
等待多个Isolate完成。场景:捕获Isolate中抛出的异常。
\\nvoid main() async {\\n try {\\n final result = await Isolate.run(() => errorProneTask());\\n print(result);\\n } catch (e) {\\n print(\'捕获错误:$e\'); // 输出:捕获错误:任务失败!\\n }\\n}\\n\\nint errorProneTask() {\\n throw Exception(\'任务失败!\');\\n}\\n
\\n解释:
\\nIsolate.run()
自动将异常传递回主Isolate。try-catch
捕获异常,避免程序崩溃。SendPort
和ReceivePort
传递复杂消息场景:双向通信,发送多个消息。
\\nvoid main() async {\\n final receivePort = ReceivePort();\\n await Isolate.spawn(worker, receivePort.sendPort);\\n\\n // 接收Isolate的SendPort\\n final sendPort = await receivePort.first as SendPort;\\n final responsePort = ReceivePort();\\n sendPort.send({\'request\': 40, \'response\': responsePort.sendPort});\\n\\n responsePort.listen((message) {\\n print(\'收到结果:$message\');\\n responsePort.close();\\n });\\n}\\n\\nvoid worker(SendPort mainSendPort) {\\n final receivePort = ReceivePort();\\n mainSendPort.send(receivePort.sendPort); // 发送自己的SendPort给主Isolate\\n\\n receivePort.listen((message) {\\n final data = message as Map;\\n final n = data[\'request\'];\\n final responsePort = data[\'response\'] as SendPort;\\n final result = calculate(n as int);\\n responsePort.send(result); // 返回结果\\n });\\n}\\n
\\n解释:
\\nSendPort
双向通信。async/await
。场景:解析大型 JSON 文件(如 10MB+)避免主线程卡顿。
\\nimport \'dart:isolate\';\\nimport \'dart:convert\';\\n\\nvoid main() async {\\n // 模拟一个大型 JSON 字符串(实际可以从文件读取)\\n final largeJson = \'{\\"data\\": [${List.generate(1e5, (i) => \'{\\"id\\": $i}\'}.join(\',\')]}\';\\n\\n try {\\n final parsedData = await Isolate.run(() => jsonDecode(largeJson));\\n print(\'解析完成,数据长度: ${parsedData[\'data\'].length}\');\\n } catch (e) {\\n print(\'解析错误: $e\');\\n }\\n}\\n
\\n说明:
\\nIsolate.run
将 jsonDecode
放在后台执行。场景:加密大文件或数据块。
\\nimport \'dart:isolate\';\\nimport \'dart:typed_data\';\\nimport \'package:encrypt/encrypt.dart\';\\n\\nvoid main() async {\\n final data = Uint8List.fromList(List.generate(1e6, (i) => i % 256)); // 模拟1MB数据\\n final key = Key.fromUtf8(\'32-byte-long-encryption-key-1234\');\\n final iv = IV.fromLength(16);\\n\\n final encrypted = await Isolate.run(() => encryptData(data, key, iv));\\n print(\'加密完成,长度: ${encrypted.length}\');\\n}\\n\\nUint8List encryptData(Uint8List data, Key key, IV iv) {\\n final encrypter = Encrypter(AES(key, mode: AESMode.cbc));\\n return encrypter.encryptBytes(data, iv: iv).bytes;\\n}\\n
\\n说明:
\\nencrypt
包进行 AES 加密。Uint8List
(二进制数据)和密钥参数到 Isolate。场景:对高分辨率图片应用滤镜或调整尺寸。
\\nimport \'dart:isolate\';\\nimport \'dart:typed_data\';\\nimport \'package:image/image.dart\'; // 需要添加依赖: image: ^4.0.0\\n\\nvoid main() async {\\n final imageBytes = await _loadImageBytes(\'large_image.jpg\');\\n \\n final resizedImage = await Isolate.run(() {\\n final image = decodeImage(imageBytes)!;\\n return copyResize(image, width: 800).getBytes(); // 调整宽度为800px\\n });\\n\\n print(\'图片处理完成,大小: ${resizedImage.length} bytes\');\\n}\\n\\n// 模拟加载图片字节数据\\nFuture<Uint8List> _loadImageBytes(String path) async {\\n return Uint8List.fromList(List.generate(5e6, (i) => i % 256)); // 模拟5MB图片\\n}\\n
\\n说明:
\\nimage
包解码和处理图片。场景:从网络下载大图并解码,避免阻塞 UI。
\\nimport \'dart:isolate\';\\nimport \'dart:typed_data\';\\nimport \'package:http/http.dart\' as http;\\nimport \'package:image/image.dart\';\\n\\nvoid main() async {\\n final imageUrl = \'https://example.com/large_image.jpg\';\\n\\n // 下载图片字节(主线程)\\n final response = await http.get(Uri.parse(imageUrl));\\n final imageBytes = response.bodyBytes;\\n\\n // 在 Isolate 中解码\\n final decodedImage = await Isolate.run(() => decodeImage(imageBytes));\\n\\n if (decodedImage != null) {\\n print(\'图片解码完成,尺寸: ${decodedImage.width}x${decodedImage.height}\');\\n }\\n}\\n
\\n说明:
\\nhttp
包下载图片,主线程处理网络请求(异步非阻塞)。decodeImage
是 CPU 密集型操作)。场景:下载加密图片 → 解密 → 调整尺寸 → 显示。
\\nimport \'dart:isolate\';\\nimport \'dart:typed_data\';\\nimport \'package:http/http.dart\' as http;\\nimport \'package:encrypt/encrypt.dart\';\\nimport \'package:image/image.dart\';\\n\\nvoid main() async {\\n final encryptedImage = await _downloadEncryptedImage();\\n final decryptedBytes = await Isolate.run(() => _decryptImage(encryptedImage));\\n final resizedImage = await Isolate.run(() => _resizeImage(decryptedBytes));\\n\\n print(\'最终图片大小: ${resizedImage.length} bytes\');\\n}\\n\\nFuture<Uint8List> _downloadEncryptedImage() async {\\n final response = await http.get(Uri.parse(\'https://example.com/encrypted_image\'));\\n return response.bodyBytes;\\n}\\n\\nUint8List _decryptImage(Uint8List encrypted) {\\n final key = Key.fromUtf8(\'32-byte-long-encryption-key-1234\');\\n final iv = IV.fromLength(16);\\n final encrypter = Encrypter(AES(key, mode: AESMode.cbc));\\n return encrypter.decryptBytes(Encrypted(encrypted), iv: iv);\\n}\\n\\nUint8List _resizeImage(Uint8List bytes) {\\n final image = decodeImage(bytes)!;\\n return copyResize(image, width: 800).getBytes();\\n}\\n
\\n数据序列化:
\\nList
、Map
、Uint8List
)。性能权衡:
\\n错误处理:
\\ntry-catch
包裹 Isolate.run
或监听 ReceivePort
的错误流。资源释放:
\\nkill()
释放资源。Isolate.run
可自动管理生命周期。通过合理使用 Isolate,可显著提升复杂任务的响应速度和用户体验。
","description":"在 Dart 中,Isolate 是并发编程的核心概念,用于实现真正的并行执行。它的设计目标是解决多线程编程中的共享内存问题,通过 内存隔离 和 消息传递 确保线程安全。以下是 Isolate 的详细解释: 一、Isolate 是什么?\\n1. 定义\\n独立执行单元:每个 Isolate 拥有独立的内存堆(Heap)和事件循环(Event Loop),不共享内存。\\n轻量级线程:类似于操作系统线程,但由 Dart VM 管理,开销更小。\\n消息驱动:Isolate 之间通过 消息传递(Message Passing) 通信,而非共享内存。\\n2. 与线程的…","guid":"https://juejin.cn/post/7488741942377873434","author":"无知的前端","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-03T07:55:02.513Z","media":null,"categories":["Android","Flutter","性能优化"],"attachments":null,"extra":null,"language":null},{"title":"Flutter-权限permission_handler插件配置","url":"https://juejin.cn/post/7488657770629283894","content":"1、确保gradle.properties
文件有以下配置
android.useAndroidX=true\\nandroid.enableJetifier=true\\n
\\n2、确保android/app/build.gradle
的 compileSdkVersion到31
1、Podfile文件配置
\\n# 权限配置开始\\n config.build_settings[\'GCC_PREPROCESSOR_DEFINITIONS\'] ||= [\\n \'$(inherited)\',\\n\\n ## 仅允许写入日历的权限(iOS 16 及以下)\\n # \'PERMISSION_EVENTS=1\',\\n\\n ## 允许完全访问日历的权限(iOS 17及以上)\\n # \'PERMISSION_EVENTS_FULL_ACCESS=1\',\\n\\n ## 提醒事项权限\\n # \'PERMISSION_REMINDERS=1\',\\n\\n ## 联系人权限\\n # \'PERMISSION_CONTACTS=1\',\\n\\n ## 相机权限\\n # \'PERMISSION_CAMERA=1\',\\n\\n ## 麦克风权限\\n # \'PERMISSION_MICROPHONE=1\',\\n\\n ## 语音识别权限\\n # \'PERMISSION_SPEECH_RECOGNIZER=1\',\\n\\n ## 照片权限\\n # \'PERMISSION_PHOTOS=1\',\\n\\n ## 位置权限组(包括始终、使用中)\\n # \'PERMISSION_LOCATION=1\',\\n\\n ## 通知权限\\n # \'PERMISSION_NOTIFICATIONS=1\',\\n\\n ## 媒体库权限\\n # \'PERMISSION_MEDIA_LIBRARY=1\',\\n\\n ## 传感器权限\\n # \'PERMISSION_SENSORS=1\',\\n\\n ## 蓝牙权限\\n # \'PERMISSION_BLUETOOTH=1\',\\n\\n ## 应用跟踪透明度权限\\n # \'PERMISSION_APP_TRACKING_TRANSPARENCY=1\',\\n\\n ## 关键警报权限\\n # \'PERMISSION_CRITICAL_ALERTS=1\'\\n ]\\n # 权限配置结束\\n end\\n\\n
\\n2、Info.plist里的权限要正常添加
\\nenum PermissionStatus {\\n /// The user denied access to the requested feature.\\n denied, //权限被拒绝\\n \\n /// The user granted access to the requested feature.\\n granted, //通过\\n \\n /// The OS denied access to the requested feature. The user cannot change\\n /// this app\'s status, possibly due to active restrictions such as parental\\n /// controls being in place.\\n /// *Only supported on iOS.*\\n restricted, //IOS\\n \\n ///User has authorized this application for limited access.\\n /// *Only supported on iOS (iOS14+).*\\n limited, //IOS\\n \\n /// The user denied access to the requested feature and selected to never\\n /// again show a request for this permission. The user may still change the\\n /// permission status in the settings.\\n /// *Only supported on Android.*\\n permanentlyDenied, //拒绝,且不在提示\\n}\\n
","description":"一、Android端: 1、确保gradle.properties文件有以下配置\\n\\nandroid.useAndroidX=true\\nandroid.enableJetifier=true\\n\\n\\n2、确保android/app/build.gradle的 compileSdkVersion到31\\n\\n二、iOS端:\\n\\n1、Podfile文件配置\\n\\n# 权限配置开始\\n config.build_settings[\'GCC_PREPROCESSOR_DEFINITIONS\'] ||= [\\n \'$(inherited)\',\\n\\n ## 仅允许写入日历的权…","guid":"https://juejin.cn/post/7488657770629283894","author":"木马不在转","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-03T07:27:38.709Z","media":null,"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"剑拔弩张——焦点竞争引的发输入失效","url":"https://juejin.cn/post/7488601438639439887","content":"结合业务需求,封装实现了一个自带下拉框并支持文本搜索的小组件,下拉框通过点击文本输入框进行视图触发,并且下拉数据要支持导航栏上下拖动;整体逻辑并不复杂,但是发现触发下拉框后,文本输入框输入文本不被响应,进行排查猜测大致是焦点被下拉弹窗视图竞争导致;对场景进行了总结分析进行相关知识点记录;
\\n核心逻辑
\\n// TextField 部分\\nWidget buildTextField() {\\n return MouseRegion(\\n child: TextField(\\n controller: controller,\\n onTap: () => _onShowElements(), // 点击触发弹窗显示\\n onChanged: (val) => {\\n //输入框逻辑\\n }\\n },\\n ),\\n );\\n}\\n\\n// 点击事件处理\\nvoid _onShowElements() {\\n //计算下拉窗位置\\n renderBox = anchorKey.currentContext.findRenderObject() as RenderBox;\\n if (renderBox is null) return;\\n \\n width = renderBox.size.width;\\n\\n // 调用 Navigator 展示下拉选项弹窗\\n popWidget = Navigator.push(context, ElementWidget(child: DataList()));;\\n}\\n\\n\\n
\\n调用链
\\ngraph TD\\n A[用户点击TextField] --\x3e B[_onShowElements 被调用]\\n B --\x3e C[通过GlobalKey获取RenderBox]\\n C --\x3e D[计算弹窗显示位置]\\n D --\x3e E[Navigator.push ElementWidget]\\n E --\x3e F[ElementWidget 示选项列表]\\n F --\x3e G[返回弹窗列表]\\n
\\n焦点拦截: Navigator.push PopWidget 后,由于弹出层未正确管理焦点作用域,导致输入框的FocusNode被强制释放
\\nsequenceDiagram\\n participant T as TextField\\n participant M as MenuContainer\\n participant F as FocusManager\\n participant N as Navigator\\n participant User\\n\\n T->>F: _onShowElements 触发焦点请求\\n F->>T: 授予焦点\\n T->>N: 调用 Navigator.push(ElementWidget)\\n N->>M: 弹出 Overlay(显示MenuContainer)\\n M--\x3e>F: 未声明新 FocusScope\\n Note over M: 默认共享原作用域\\n User->>M: 点击菜单项\\n M->>F: 意外释放焦点\\n F--\x3e>T: 焦点丢失,TextField无法输入\\n
\\n对于焦点竞争可以通过以下三种方式解决:
\\nOverlayEntry(\\n builder: (context) {\\n return Positioned(\\n left: left,\\n top: top,\\n width: width,\\n child: IgnorePointer(\\n ignoring: true, // 或者只在非交互区域设置为 true\\n child: MenuContainer(...),\\n ),\\n );\\n },\\n);\\n\\n
\\nvoid _onShowElements({String changeValue = \\"\\"}) {\\n // 现有逻辑...\\n popWidget = _showOptions(...);\\n // 使 TextField 保持焦点\\n FocusScope.of(context).requestFocus(_focusNode);\\n}\\n\\n
\\nvoid _onShowElements({String changeValue = \\"\\"}) {\\n // 现有逻辑...\\n popWidget = _showOptions(...);\\n // 使 TextField 保持焦点\\n FocusScope.of(context).requestFocus(_focusNode);\\n}\\n\\n
\\n鉴于当前弹窗也需要通过焦点进行功能处理,并需要满足使用输入框时焦点控制在输入框内且下拉窗视图不被关闭,因此将通过管理焦点方式尝试解决
\\n问题类型 | 核心思路 | Flutter实现方案 |
---|---|---|
焦点竞争 | 精确控制焦点作用域 | 使用FocusScope 隔离不同区域焦点,通过FocusNode.requestFocus() 主动管理 |
弹窗叠加 | 分层管理Overlay层级 | 采用OverlayEntry + Stack 手动控制弹窗层级,而非默认Navigator 堆栈 |
动态组件焦点跟踪 | 建立组件与焦点的动态绑定 | 为动态组件分配唯一GlobalKey ,通过Focus.of(context,scope: customScope) 精准定位 |
键盘挤压布局 | 动态适配视图布局 | 结合MediaQuery.of(context).viewInsets 计算安全区域,使用SingleChildScrollView 适配 |
无障碍访问兼容 | 遵循WCAG焦点管理规范 | 使用Semantics 组件声明焦点顺序,配合FocusTraversalGroup 管理阅读顺序 |
Flutter 的焦点系统基于 树形结构,通过 FocusManager
管理全局焦点状态,每个 FocusNode
或 FocusScopeNode
作为树中的节点
FocusManager\\n├── FocusScopeNode(root)\\n│ ├── FocusScopeNode(page1)\\n│ │ ├── FocusNode(textField1)\\n│ │ └── FocusNode(button1)\\n│ └── FocusScopeNode(dialog)\\n│ └── FocusNode(textField2)\\n└── FocusAttachment // 管理节点在树中的位置\\n
\\nTextField
、Button
)。FocusNode
挂载到焦点树中。当组件请求焦点时,FocusNode.requestFocus()
触发以下流程:
// FocusNode 类\\nvoid requestFocus([FocusOnKeyCallback? onKey]) {\\n FocusManager.instance.setCurrentFocus(\\n this, \\n onKey: onKey,\\n alignmentPolicy: _alignmentPolicy,\\n );\\n}\\n\\n// FocusManager 类\\nvoid setCurrentFocus(FocusNode node, {FocusOnKeyCallback? onKey}) {\\n if (_currentFocus != node) {\\n _currentFocus?._unfocus(); // 释放原焦点\\n node._focus(); // 设置新焦点\\n _currentFocus = node;\\n }\\n}\\n
\\n// FocusScopeNode 类\\nvoid setFirstFocus(FocusNode node) {\\n if (!_children.contains(node)) return;\\n _focusedChild = node; // 设置当前作用域的活跃焦点\\n}\\n\\n// 弹窗场景下,新作用域自动激活\\nvoid _activate() {\\n FocusManager.instance.pushScope(this); // 压入作用域栈\\n}\\n
\\n// RawKeyboard 事件处理\\nvoid _handleKeyEvent(RawKeyEvent event) {\\n if (_currentFocus != null) {\\n _currentFocus!.onKey(event); // 将键盘事件传递至当前焦点节点\\n }\\n}\\n
\\n// 创建独立作用域防止焦点泄漏\\nFocusScopeNode _dialogScope = FocusScopeNode();\\nvoid showDialog() {\\n FocusManager.instance.pushScope(_dialogScope);\\n}\\n\\nvoid closeDialog() {\\n FocusManager.instance.removeScope(_dialogScope);\\n}\\n
\\n// 避免频繁焦点切换导致性能问题\\nTimer? _debounceTimer;\\nvoid safeRequestFocus(FocusNode node) {\\n _debounceTimer?.cancel();\\n _debounceTimer = Timer(const Duration(milliseconds: 100), () {\\n node.requestFocus();\\n });\\n}\\n
\\nsequenceDiagram\\n participant User as 用户\\n participant FocusNode as FocusNode(TextField)\\n participant FocusManager as FocusManager\\n participant FocusScope as FocusScopeNode\\n participant UI as UI 渲染引擎\\n\\n User->>FocusNode: 点击 TextField\\n FocusNode->>FocusManager: 调用 requestFocus()\\n FocusManager->>FocusManager: setCurrentFocus(node)\\n FocusManager->>FocusManager: _currentFocus?._unfocus()\\n FocusManager->>FocusNode: node._focus()\\n FocusManager->>FocusScope: 检查是否在新作用域内\\n alt 属于新作用域\\n FocusManager->>FocusScope: pushScope()\\n end\\n FocusManager->>UI: 触发重绘(光标/高亮)\\n UI->>User: 显示焦点状态\\n loop 事件监听\\n User->>FocusNode: 输入文本\\n FocusNode->>FocusManager: 触发 onKey() 事件\\n end\\n
\\n用户点击 TextField
,触发其关联的 FocusNode
的 requestFocus()
方法。
FocusNode
通过 FocusManager.instance.setCurrentFocus(this)
向全局管理器提交请求。
void requestFocus() {\\n FocusManager.instance.setCurrentFocus(this); // 向 FocusManager 提交请求\\n}\\n
\\nFocusManager
调用旧焦点的 _unfocus()
方法,触发 onFocusChange
监听器。
新 FocusNode
调用 _focus()
方法,更新内部状态并触发 onFocusChange
。
void setCurrentFocus(FocusNode node) {\\n if (_currentFocus != node) {\\n _currentFocus?._unfocus(); // 释放旧焦点\\n node._focus(); // 设置新焦点\\n _currentFocus = node; // 更新当前焦点\\n }\\n}\\n
\\n若新焦点属于新的 FocusScopeNode
(如弹窗),则调用 pushScope()
将其压入作用域栈顶。
void activate() {\\n FocusManager.instance.pushScope(this); // 压入作用域栈顶\\n _focusedChild?.requestFocus(); // 激活子节点焦点\\n}\\n
\\n焦点变更触发 build()
方法重绘,显示光标闪烁或高亮边框。
输入过程中,键盘事件通过 RawKeyboard
传递至当前焦点节点处理。
void handleKeyEvent(RawKeyEvent event) {\\n if (_currentFocus != null) {\\n _currentFocus!.onKey(event); // 将键盘事件传递给当前焦点组件\\n }\\n}\\n
","description":"问题背景 结合业务需求,封装实现了一个自带下拉框并支持文本搜索的小组件,下拉框通过点击文本输入框进行视图触发,并且下拉数据要支持导航栏上下拖动;整体逻辑并不复杂,但是发现触发下拉框后,文本输入框输入文本不被响应,进行排查猜测大致是焦点被下拉弹窗视图竞争导致;对场景进行了总结分析进行相关知识点记录;\\n\\n核心逻辑\\n\\n// TextField 部分\\nWidget buildTextField() {\\n return MouseRegion(\\n child: TextField(\\n controller: controller…","guid":"https://juejin.cn/post/7488601438639439887","author":"人形打码机","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-03T05:21:20.358Z","media":null,"categories":["Android","Flutter","客户端","设计"],"attachments":null,"extra":null,"language":null},{"title":"记录 flutter 文本内容展示过长优化","url":"https://juejin.cn/post/7488557945271550006","content":"对于过长的文本, 大部分都是超长展示直接显示省略号。
\\n但是! 如果ui既要省略号、又要根据展示内容去变化字号、还好换行,弹窗还要提示框、提示框箭头还要指向超出的“...” 当时听上去,一口就答应了, 自此坠入search的深渊无法自拔。
\\n整理一下要求:
\\n需求分析:
\\n Text(\\n name,\\n maxline: 3,\\n overflow: TextOverflow.ellipsis\\n )\\n
\\n Stack(\\n children: [\\n Positioned(\\n // bottom: -80.rpx,\\n left: widget.left,\\n top: alertTop,\\n child: Container(\\n padding: EdgeInsets.only(left: 6.rpx, right: 6.rpx),\\n alignment: Alignment.center,\\n constraints: widget.isRenji\\n ? widget.name.length > 30\\n ? BoxConstraints(\\n minWidth: 120.rpx,\\n maxWidth: 644.rpx,\\n minHeight: 40.rpx)\\n : BoxConstraints(\\n minWidth: 120.rpx,\\n maxWidth: 496.rpx,\\n minHeight: 40.rpx)\\n : BoxConstraints(\\n minWidth: 120.rpx,\\n maxWidth: 644.rpx,\\n minHeight: 40.rpx),\\n decoration: BoxDecoration(\\n color: const Color.fromRGBO(0, 0, 0, 0.6),\\n borderRadius: BorderRadius.all(Radius.circular(10.rpx)),\\n ),\\n child: Stack(\\n alignment: Alignment.center,\\n fit: StackFit.passthrough,\\n clipBehavior: Clip.none,\\n children: [\\n Text(\\n widget.name,\\n style: Commons.positionText(),\\n ),\\n ],\\n ),\\n ),\\n ),\\n Positioned(\\n // top: widget.name.length > 8 ? -12.rpx : -12.rpx,\\n // top: widget.name.length > 8 ? -12.rpx : -12.rpx,\\n top: alertTop - 12.rpx,\\n left: alertLeft,\\n child: Container(\\n width: 0.rpx,\\n height: 0.rpx,\\n clipBehavior: Clip.antiAlias,\\n decoration: BoxDecoration(\\n border: Border(\\n left: BorderSide(\\n color: Colors.transparent,\\n width: 13.rpx,\\n ),\\n right: BorderSide(\\n color: Colors.transparent,\\n width: 13.rpx,\\n ),\\n top: BorderSide(\\n color: const Color.fromRGBO(0, 0, 0, 0.6),\\n width: 12.rpx,\\n ),\\n bottom: BorderSide(\\n color: Colors.transparent,\\n width: 12.rpx,\\n ),\\n ),\\n ),\\n ),\\n )\\n ],\\n )\\n
\\n _overlayEntry = OverlayEntry(\\n builder: (context) {\\n return “上一步写好的Widget”;\\n },\\n );\\n 弹出\\n Overlay.of(context)!.insert(_overlayEntry!);\\n 关闭\\n _overlayEntry.remove()\\n
\\n为了防止弹框弹出之后,由于....原因, 要做一下弹窗删除的处理,\\n我是在弹出的时候添加了 _overlayEntry.remove(),之前的弹窗没有关闭,又弹出新的弹窗。两个弹框都在页面上。 老板看了都会“笑嘻嘻”
\\n RenderObject? renderObject = boxKey.currentContext?.findRenderObject();\\n if (renderObject != null) {\\n RenderBox renderBox = renderObject as RenderBox;\\n Size size = renderBox.size;\\n Offset position = renderBox.localToGlobal(Offset.zero);\\n alertTop = position.dy + size.height + 10.rpx;\\n alertLeft = position.dx + size.width - 25.rpx;\\n }\\n 注意: 我这个箭头的Widget并没有和弹框写一块, 是基于全局定位的\\n \\n
\\n AutoSizeText(\\n key: boxKey, \\n textKey: textKey, // 如果要读取文本的行数之类属性, 要用这个key\\n // onResized: (sizeInfo) { \\n // // 正确拼写\\n // print(\'调整后的字体大小: ${sizeInfo?.fontSize}\');\\n // },\\n names,\\n overflow: TextOverflow.ellipsis,\\n stepGranularity: 1.rpx,\\n minFontSize: 33.rpx,\\n maxLines: names.length > 15 ? 3 : 1,\\n maxFontSize: widget.style.fontSize ?? 118.rpx,\\n style: TextStyle(\\n fontFamily: widget.style.fontFamily,\\n fontWeight: widget.style.fontWeight,\\n fontStyle: widget.style.fontStyle,\\n color: widget.style.color,\\n // height: widget.style.height,\\n fontSize: widget.style.fontSize),\\n )\\n
\\n问题来了! 如何能获取到它自动调整字体大小呢! 问deepseek, 他说有这个属性, 我用的是最新版本没有发现。 文档中也没有查到。 那么怎么办呢。 上边代码中有一个 textKey 可以通过这个来获取widget 的style属性
\\n final RenderParagraph renderParagraph =\\n textKey.currentContext?.findRenderObject() as RenderParagraph;\\n final textStyle = renderParagraph.text.style;\\n
\\n拿到属性之后就可以随意的textStyle.fontSize了
\\n还有个问题! 这样做不管文本有没有超出, 点击都会弹出提示框
\\n上边获取到了文本fontSize的大小, 可以使用TextPainter,样式直接取用渲染出来Text Wiget的样式。
\\n final textSpan = TextSpan(text: widget.name, style: textStyle);\\n final textPainter = TextPainter(\\n text: textSpan,\\n textDirection: TextDirection.ltr,\\n maxLines: 3,\\n ellipsis: \'...\',\\n );\\n // 约束宽度与实际渲染一致\\n textPainter.layout(maxWidth: renderBox.size.width);\\n bool isOver = textPainter.didExceedMaxLines;\\n
\\n这样通过didExceedMaxlines判断文本是否超过自己设置的maxline行数了。
\\n分析完毕! 贴一下全部的代码
\\nimport \'dart:async\';\\n\\nimport \'package:auto_size_text/auto_size_text.dart\';\\nimport \'package:bed_side/style/commons.dart\';\\nimport \'package:flutter/material.dart\';\\nimport \\"package:bed_side/utils/size_extension.dart\\";\\nimport \'package:flutter/rendering.dart\';\\n\\nclass NameTextFlow extends StatefulWidget {\\n final String name;\\n final TextStyle style;\\n final double left;\\n const NameTextFlow({\\n Key? key,\\n required this.name,\\n required this.style,\\n this.left = 60,\\n }) : super(key: key);\\n @override\\n State<NameTextFlow> createState() => NameTextFlowState();\\n}\\n\\nclass NameTextFlowState extends State<NameTextFlow> {\\n bool showName = true;\\n Timer? cutTimer;\\n OverlayEntry? _overlayEntry;\\n GlobalKey boxKey = GlobalKey();\\n GlobalKey textKey = GlobalKey();\\n double alertTop = 0.0;\\n double alertLeft = 0.0;\\n TextSpan textSpan = TextSpan();\\n cutDownName() {\\n cutTimer = Timer.periodic(const Duration(seconds: 30), (timer) {\\n timer.cancel();\\n setState(() {\\n showName = false;\\n });\\n });\\n }\\n\\n toggleName() {\\n setState(() {\\n showName = !showName;\\n if (showName) {\\n // cutDownName();\\n alertTip();\\n } else {\\n deletetTip();\\n }\\n });\\n }\\n\\n @override\\n void initState() {\\n super.initState();\\n }\\n\\n @override\\n void dispose() {\\n cutTimer?.cancel();\\n deletetTip();\\n super.dispose();\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n String names = widget.name;\\n return Stack(\\n fit: StackFit.passthrough,\\n clipBehavior: Clip.none,\\n children: [\\n GestureDetector(\\n onTap: () {\\n if (!checkTextOver()) return;\\n RenderObject? renderObject =\\n boxKey.currentContext?.findRenderObject();\\n if (renderObject != null) {\\n RenderBox renderBox = renderObject as RenderBox;\\n Size size = renderBox.size;\\n Offset position = renderBox.localToGlobal(Offset.zero);\\n alertTop = position.dy + size.height + 10.rpx;\\n alertLeft = position.dx + size.width - 25.rpx;\\n }\\n toggleName();\\n },\\n child: Container(\\n constraints: BoxConstraints(minWidth: 300.rpx, maxWidth: 454.rpx),\\n child: AutoSizeText(\\n key: boxKey,\\n textKey: textKey,\\n softWrap: true,\\n textAlign: TextAlign.left,\\n names,\\n overflow: TextOverflow.ellipsis,\\n stepGranularity: 1.rpx,\\n minFontSize: 33.rpx,\\n maxLines: names.length > 15 ? 3 : 1,\\n maxFontSize: widget.style.fontSize ?? 118.rpx,\\n style: TextStyle(\\n fontFamily: widget.style.fontFamily,\\n fontWeight: widget.style.fontWeight,\\n fontStyle: widget.style.fontStyle,\\n color: widget.style.color,\\n fontSize: widget.style.fontSize),\\n )\\n ),\\n ),\\n ],\\n );\\n }\\n\\n /// 弹出弹框\\n void alertTip() {\\n if (_overlayEntry != null) {\\n _overlayEntry!.remove();\\n }\\n _overlayEntry = OverlayEntry(\\n builder: (context) {\\n return Stack(\\n children: [\\n Positioned(\\n // bottom: -80.rpx,\\n left: widget.left,\\n top: alertTop,\\n child: Container(\\n padding: EdgeInsets.only(left: 6.rpx, right: 6.rpx),\\n alignment: Alignment.center,\\n constraints: widget.isRenji\\n ? widget.name.length > 30\\n ? BoxConstraints(\\n minWidth: 120.rpx,\\n maxWidth: 644.rpx,\\n minHeight: 40.rpx)\\n : BoxConstraints(\\n minWidth: 120.rpx,\\n maxWidth: 496.rpx,\\n minHeight: 40.rpx)\\n : BoxConstraints(\\n minWidth: 120.rpx,\\n maxWidth: 644.rpx,\\n minHeight: 40.rpx),\\n decoration: BoxDecoration(\\n color: const Color.fromRGBO(0, 0, 0, 0.6),\\n borderRadius: BorderRadius.all(Radius.circular(10.rpx)),\\n ),\\n child: Stack(\\n alignment: Alignment.center,\\n fit: StackFit.passthrough,\\n clipBehavior: Clip.none,\\n children: [\\n Text(\\n widget.name,\\n style: Commons.positionText(),\\n ),\\n ],\\n ),\\n ),\\n ),\\n Positioned(\\n // top: widget.name.length > 8 ? -12.rpx : -12.rpx,\\n // top: widget.name.length > 8 ? -12.rpx : -12.rpx,\\n top: alertTop - 12.rpx,\\n left: alertLeft,\\n child: Container(\\n width: 0.rpx,\\n height: 0.rpx,\\n clipBehavior: Clip.antiAlias,\\n decoration: BoxDecoration(\\n border: Border(\\n left: BorderSide(\\n color: Colors.transparent,\\n width: 13.rpx,\\n ),\\n right: BorderSide(\\n color: Colors.transparent,\\n width: 13.rpx,\\n ),\\n top: BorderSide(\\n color: const Color.fromRGBO(0, 0, 0, 0.6),\\n width: 12.rpx,\\n ),\\n bottom: BorderSide(\\n color: Colors.transparent,\\n width: 12.rpx,\\n ),\\n ),\\n ),\\n ),\\n )\\n ],\\n );\\n },\\n );\\n Overlay.of(context)!.insert(_overlayEntry!);\\n }\\n\\n /// 删除弹框\\n void deletetTip() {\\n if (_overlayEntry == null) return;\\n _overlayEntry!.remove();\\n _overlayEntry = null;\\n }\\n\\n // 计算省略号的位置\\n bool checkTextOver() {\\n final RenderParagraph renderParagraph =\\n textKey.currentContext?.findRenderObject() as RenderParagraph;\\n final textStyle = renderParagraph.text.style;\\n final renderBox = boxKey.currentContext?.findRenderObject() as RenderBox?;\\n if (renderBox == null) return false;\\n final textSpan = TextSpan(text: widget.name, style: textStyle);\\n final textPainter = TextPainter(\\n text: textSpan,\\n textDirection: TextDirection.ltr,\\n maxLines: 3,\\n ellipsis: \'...\',\\n );\\n // 约束宽度与实际渲染一致\\n textPainter.layout(maxWidth: renderBox.size.width);\\n final lineMetrics = textPainter.computeLineMetrics();\\n switch (lineMetrics.length) {\\n case 1:\\n break;\\n default:\\n }\\n // 检查是否真的被截断\\n return textPainter.didExceedMaxLines;\\n }\\n}\\n\\n
","description":"对于过长的文本, 大部分都是超长展示直接显示省略号。 但是! 如果ui既要省略号、又要根据展示内容去变化字号、还好换行,弹窗还要提示框、提示框箭头还要指向超出的“...” 当时听上去,一口就答应了, 自此坠入search的深渊无法自拔。\\n\\n整理一下要求:\\n\\n换行,文本超出省略号\\n点击之后弹出弹框展示全部内容\\n根据内容缩小放大字体, 比如: 两行的时候, 字体大一点, 一行的时候字体更大一点\\n\\n需求分析:\\n\\n超出展示省略号, 这个好整,给Text组件设置 overflow: TextOverflow.ellipsis,再加一下maxLine: 3…","guid":"https://juejin.cn/post/7488557945271550006","author":"sayen","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-03T02:54:02.850Z","media":null,"categories":["Android","Flutter","前端"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 伪 3D 绘制#02 | 地平面与透视","url":"https://juejin.cn/post/7488582788674650146","content":"上一篇我们实现了三维空间点到二维平面的映射,并且绘制了坐标轴。本文将进一步完善三维空间的视觉表现,如下所示,构建一个地平面网格, 并且具有透视效果:
\\n在数学的立体几何中,一些辅助线可以更好地帮我们理解三维空间。 这里准备在 X-Y 平面绘制一个网格,体现出地平线,这可以在视觉上让我们更具有归属感:
\\n绘制网格非常简单,主要就是找到坐标,遍历收集线条绘制。如下代码中,遍历 6 次,收集横纵方向上各 6 条线:
\\nvoid _drawGrid(Canvas canvas) {\\n List<(Point3D, Point3D)> lines = [];\\n for (double i = 0; i <= 5; i++) {\\n lines.add((Point3D(i, 0, 0), Point3D(i, 5, 0)));\\n lines.add((Point3D(0, i, 0), Point3D(5, i, 0)));\\n }\\n \\n Paint paint = Paint() ..color = Colors.white..strokeWidth = 1;\\n for (var line in lines) {\\n Offset p0 = project(line.$1);\\n Offset p1 = project(line.$2);\\n canvas.drawLine(p0, p1, paint);\\n }\\n}\\n
\\n然后可以修改一下遍历的个数,以及线的长度,就可以轻松实现一个 10*10
的网格:
List<(Point3D, Point3D)> lines = [];\\nfor (double i = -5; i <= 5; i++) {\\n lines.add((Point3D(i, -5, 0), Point3D(i, 5, 0)));\\n lines.add((Point3D(-5, i, 0), Point3D(5, i, 0)));\\n}\\n
\\n仔细观察不难看出,目前的地平面每条线都是平行的,这对于近大远小的视觉感来说比较为何,特别是旋转角度后,这种绝对的平行会产生违和感:
\\n那么该如何在当前的效果上施加 透视
的魔法呢?如下效果的对比可以看出,施加透视之后,感官上舒适了很多:
未透视 | 透视效果 |
---|---|
透视效果本质上还是在三维点到二维点映射过程,根据视觉规律进行的转换。如下所示,两行代码即可在转换过程中施加透视效果:
\\n// 3D点投影到2D平面\\nOffset project(Point3D p) {\\n double scale = 32.0; // 缩放系数\\n double angle = 30 / 180 * pi; // 30度弧度值(π/6)\\n // 绕Z轴旋转\\n final rx = p.x * cos(rotationZ) - p.y * sin(rotationZ);\\n final ry = p.x * sin(rotationZ) + p.y * cos(rotationZ);\\n // 等轴测投影\\n final xProj = (rx - ry) * cos(angle);\\n final yProj = (rx + ry) * sin(angle) - p.z;\\n // 增加透视\\n double d = 18.0;\\n double radio = d / (d - yProj);\\n return Offset(xProj * scale * radio, yProj * scale * radio);\\n}\\n
\\n当动态修改 d 的值,可以调整透视的效果,如下所示。至于为什么这样就可以实现透视,以及 d 的具体作用。不用着急,后续会一点点分析揭秘:
\\n多绘制一些直线,可以更好地表现地平面和透视的效果。如下所示,在旋转过程中,外部延伸的线条可以联想成路面,越往远处路面线的长度越短,符合透视的规律:
\\nList<(Point3D, Point3D)> lines = [];\\nfor (double i = -32; i <= 32; i++) {\\n lines.add((Point3D(i, -5, 0), Point3D(i, 5, 0)));\\n lines.add((Point3D(-5, i, 0), Point3D(5, i, 0)));\\n}\\n
\\n本想这一篇介绍一下映射原理的,但是发现增加透视会有更好的感官体验,绘制地平面也可以更好地为之后的分析做铺垫。下一篇我将会详细分析一下投影的映射逻辑,敬请期待 ~
\\n更多文章和视频知识资讯,大家可以关注我的公众号、掘金和 B 站 。
\\n","description":"上一篇我们实现了三维空间点到二维平面的映射,并且绘制了坐标轴。本文将进一步完善三维空间的视觉表现,如下所示,构建一个地平面网格, 并且具有透视效果: 1. 地平面绘制\\n\\n在数学的立体几何中,一些辅助线可以更好地帮我们理解三维空间。 这里准备在 X-Y 平面绘制一个网格,体现出地平线,这可以在视觉上让我们更具有归属感:\\n\\n绘制网格非常简单,主要就是找到坐标,遍历收集线条绘制。如下代码中,遍历 6 次,收集横纵方向上各 6 条线:\\n\\nvoid _drawGrid(Canvas canvas) {\\n List<(Point3D, Point3D…","guid":"https://juejin.cn/post/7488582788674650146","author":"张风捷特烈","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-03T01:53:39.461Z","media":[{"url":"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5567b14845954cbca92ccf2ceadafcf4~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1920&h=1200&s=459153&e=png&b=fefbfb","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cd7a0197307a419e8e72b5cdac12457f~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=718&h=466&s=520501&e=gif&f=14&b=020204","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/01bc1bccf3354edfbf54b443a4c6c31c~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1126&h=574&s=36878&e=png&b=000000","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0e3b8366d9434a128efc3a6b7f74c55e~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1167&h=599&s=81754&e=png&b=000000","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6b0ee182bb294d49bbd4224d6fae9ea5~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1106&h=574&s=71362&e=png&b=000000","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d19de03cfe3742c6a95f2b24544305f9~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=842&h=610&s=76096&e=png&b=010101","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/054d431ab78748aa9162ab996121ec12~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=911&h=609&s=90563&e=png&b=010101","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1868970ca2604f12b0fcd4944fee9e88~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1166&h=583&s=92527&e=png&b=000000","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/28aeb21f194f41eca88b04529f94f604~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=718&h=466&s=843382&e=gif&f=25&b=020204","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cd7a0197307a419e8e72b5cdac12457f~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=718&h=466&s=520501&e=gif&f=14&b=020204","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 图像上传与裁剪","url":"https://juejin.cn/post/7488599657125527552","content":"大家好!你们是否渴望将图像上传和裁剪功能无缝集成到 Flutter 应用程序中?不用再四处寻找了。只需几个简单步骤,借助两个包,你将能够优化应用的图像处理能力。让我们开始吧,探索在 Flutter 应用中轻松管理图像的秘诀。
\\n这个小项目我们只需要两个包:
\\n虽然设置这些包很简单,但考虑到不同设备的差异,还是需要谨慎一些。
\\nAndroid :只需添加包本身即可正常使用。
\\niOS :需要在 ios 文件夹中的 Info.plist 文件中添加以下几行代码:
\\n<key>NSCameraUsageDescription</key>\\n<string>This app needs access to your camera in order to upload photos.</string>\\n<key>NSPhotoLibraryUsageDescription</key>\\n<string>This app needs access to your photo library in order to upload photos.</string>\\n
\\nAndroid :需要在 android/app/src/main 文件夹中的 AndroidManifest.xml 文件中添加以下几行代码:
\\n<activity\\n android:name=\\"com.yalantis.ucrop.UCropActivity\\"\\n android:screenOrientation=\\"portrait\\"\\n android:theme=\\"@style/Theme.AppCompat.Light.NoActionBar\\"/>\\n
\\niOS :只需添加包本身即可正常使用。
\\n现在,我们已经准备好了,可以开始编写代码了!
\\n我创建了一个简单的 Stateful 页面,用户可以选择从相机或图库上传图像。图像选择后,结果会立即显示在选择按钮上方,提供无缝且直观的体验。
\\n当用户按下按钮时,将执行 uploadImage 函数。
\\n这里有两个分别标有 “从图库选择” 和 “从相机选择” 的按钮。点击每个按钮会触发带有不同参数的 uploadImage 函数。
\\nSizedBox(\\n width: 180,\\n child: FilledButton(\\n onPressed: () {\\n uploadImage(ImageSource.gallery);\\n },\\n child: const Text(\'从图库选择\'),\\n ),\\n),\\nconst SizedBox(\\n height: 10,\\n),\\nSizedBox(\\n width: 180,\\n child: FilledButton(\\n onPressed: () {\\n uploadImage(ImageSource.camera);\\n },\\n child: const Text(\'从相机选择\'),\\n ),\\n),\\n
\\n这段代码初始化了一个空的 File 变量,并定义了 uploadImage 函数。该函数使用 ImagePicker 包允许用户从图库或相机选择图像。然后将选定图像转换为 File,并相应更新 file 变量。
\\n// 初始化文件变量\\nFile? file;\\n\\n// 图像上传函数\\nFuture uploadImage(ImageSource source) async {\\n final image = await ImagePicker().pickImage(\\n source: source,\\n );\\n if (image == null) return;\\n\\n // 将图像路径转换为文件\\n File imageFile = File(image.path);\\n\\n // 更新文件变量为选定图像\\n setState(() {\\n file = imageFile;\\n });\\n}\\n
\\n用户在图库或相机选项之间选择时,uploadImage 函数会动态接收 ImageSource。
\\n图像选择后,将其转换为 File 类型并赋值给 \'file\' 变量。通过使用 setState 方法,更新后的图像会立即显示。
\\n现在,当我们需要优化上传的图像(如裁剪或旋转)时,利用 image_cropper 包的功能来实现。
\\n// 图像上传函数\\nFuture uploadImage(ImageSource source) async {\\n final image = await ImagePicker().pickImage(\\n source: source,\\n );\\n if (image == null) return;\\n\\n // 将图像路径转换为文件\\n File imageFile = File(image.path);\\n\\n var croppedFile = await cropImage(imageFile);\\n\\n // 更新文件变量为裁剪后的图像\\n setState(() {\\n file = croppedFile;\\n });\\n}\\n\\n// 使用 image_cropper 包裁剪选定图像的函数\\nFuture<File?> cropImage(File pickedFile) async {\\n final croppedFile = await ImageCropper().cropImage(\\n sourcePath: pickedFile.path,\\n compressFormat: ImageCompressFormat.jpg,\\n compressQuality: 100,\\n uiSettings: [\\n AndroidUiSettings(\\n toolbarTitle: \'裁剪器\',\\n initAspectRatio: CropAspectRatioPreset.original,\\n lockAspectRatio: false,\\n ),\\n IOSUiSettings(\\n title: \'裁剪器\',\\n ),\\n ],\\n );\\n\\n // 如果裁剪后的图像可用,则返回裁剪后的图像,否则返回原始图像\\n if (croppedFile != null) {\\n return File(croppedFile.path);\\n } else {\\n return File(pickedFile.path);\\n }\\n}\\n
\\n恭喜!你已经成功学会了如何在 Flutter 应用程序中实现图像上传和裁剪功能。通过利用 image_picker 和 image_cropper 包的强大功能,你的应用现在具备了无缝的图像处理能力,为用户提供丰富且交互式的体验。
\\n祝你编码愉快!
\\n原文:
\\n\\n","description":"大家好!你们是否渴望将图像上传和裁剪功能无缝集成到 Flutter 应用程序中?不用再四处寻找了。只需几个简单步骤,借助两个包,你将能够优化应用的图像处理能力。让我们开始吧,探索在 Flutter 应用中轻松管理图像的秘诀。 所需包\\n\\n这个小项目我们只需要两个包:\\n\\nimage_picker :通过这个包,我们可以将图像上传到应用中,并且可以选择从图库或相机获取图像。\\nimage_cropper :通过这个包,我们可以对图像进行裁剪,只保留我们需要的部分。\\n设置\\n\\n虽然设置这些包很简单,但考虑到不同设备的差异,还是需要谨慎一些。\\n\\nImage Picker…","guid":"https://juejin.cn/post/7488599657125527552","author":"fengjutian","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-03T01:24:56.001Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b0313a7a87bf4be3afd4b1e2383f6860~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgZmVuZ2p1dGlhbg==:q75.awebp?rk3s=f64ab15b&x-expires=1744248296&x-signature=XLLpkdBIsrSp2psfegb3SZRmsbE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/886a03171d9d4083b6109c15b1f417fa~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgZmVuZ2p1dGlhbg==:q75.awebp?rk3s=f64ab15b&x-expires=1744248296&x-signature=d6UFxdfY9ViILfvD72Eh9ZP9r98%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fc1fdd8ea6c5416789b301a25fee0cf3~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgZmVuZ2p1dGlhbg==:q75.awebp?rk3s=f64ab15b&x-expires=1744248296&x-signature=PqaH7QRTill%2BU9eK6Xq%2FRzQN3EE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fe15052cf9564838949fb6a39331ade5~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgZmVuZ2p1dGlhbg==:q75.awebp?rk3s=f64ab15b&x-expires=1744248296&x-signature=3iyAT7kYCUxqMlSDZ2ItsRgYt5I%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/bbd21e3112874090897bd4a1d6735f56~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgZmVuZ2p1dGlhbg==:q75.awebp?rk3s=f64ab15b&x-expires=1744248296&x-signature=b6NvV6C45hy1CHCYTNcqRHA46rU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f156c9522652431da6579e7037ddb129~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgZmVuZ2p1dGlhbg==:q75.awebp?rk3s=f64ab15b&x-expires=1744248296&x-signature=2yeOL1Q%2FUk9jo1fMwjkCDf%2FTkPY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/dafbd0eaf1564c349fa87b8362a448fa~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgZmVuZ2p1dGlhbg==:q75.awebp?rk3s=f64ab15b&x-expires=1744248296&x-signature=CgmTK2%2F2SM1rMuV5Z2eeANFUsPQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6e4e91c378ba406fb5ec70a221ed643e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgZmVuZ2p1dGlhbg==:q75.awebp?rk3s=f64ab15b&x-expires=1744248296&x-signature=OA%2F9OSTbm7PxbbzOOz%2Buz15%2Bmsk%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter Roadmap 2025 发布,快来看看有什么更新吧","url":"https://juejin.cn/post/7488582788673945634","content":"又到了 Flutter 公布年度计划的时候,开始之前我们先回顾下官方对 2024 Roadmap 的完成度
\\n当然,除了以上 Roadmap ,Flutter 在 2024 也完成了不少特性更新,例如:
\\n回顾 Flutter 2024 可谓「忧喜参半」,基本是在各种负面流言里走到了 2025,那么 Flutter 的 2025 Roadmap 又有哪些计划呢?
\\n性能部分不用多言,Flutter 在核心平台 Android 和 iOS 上的性能优化一直都是持续目标,目前 iOS 已经实现全 Impeller 迁移条件,从 3.29 开始 FLTEnableImpeller
可选退出标志不再有效,所以 2025 首要目标之一就是完全删除 iOS 版本内的 Skia,完全 Impeller 之后,iOS 的体积也可以相应有所缩减。
而对于 Android,核心重点会放到 Android API 29 或更高版本的设备,这些设备将默认 Impeller 支持,而对于较旧的设备,还需要继续保持 Skia 支持,具体原因还是在于 Impeller 的 OpenGLES 兼容上,具体原因可见之前聊 Vulkan 时谈到的:
\\n\\n\\n\\n
在移动平台上,2025 iOS 主要目标之一就是支持 iOS 19 和 Xcode 17 ,然后完成 Swift Package Manager (SwiftPM) 的迁移, 2025 SwiftPM 将成为默认选项 ,这个其实去年聊 《Flutter 迁移到 Swift Package Manager》 时 Flutter 就已经实现了支持,而之所以这样,原因我们也在 《CocoaPods 不再更新,未来将是 Swift Package Manager 的时代》 谈过。
\\n另外继续完善 Cupertino 控件支持,也是目标之一,去年各大版本更新时对于 Cupertino 控件的支持力度相信大家也感受过。
\\n而对于 Android ,除了 Android 16 的适配之后,Flutter 还计划将 Gradle 构建逻辑从 Groovy 迁移到 Kotlin,虽然现在也支持 Kotlin DSL,但是未来默认将会是 Kotlin DSL ,这也是跟进当前的 Android 构建趋势。
\\n另外,得益于 3.29 开始的 《Flutter 上的 Platform 和 UI 线程合并》 ,直接从 Dart 调用 Objective C 和 Swift 代码(适用于 iOS)以及 Java 和 Kotlin(适用于 Android)提供了更好的基础,2025 也许这种同步调用方式可以在 Framework 和 Plugin 层面大规模引入。
\\n\\n\\n目前线程合并,主要体验出来的问题,就是 Android 在 debug 断点 Dart 时容易跳出 ANR 弹窗。
\\n
去年,Flutter Web 在性能和质量方面取得了不少进展,例如减小了应用大小,更好地利用了多线程,并缩短了应用加载时间等,具体体现在:
\\nwebHtmlElementStrategy
标志允许开发者选择何时使用 <img>
元素\\n\\n从 3.29 开始, HTML renderer 就被正式移除了,也就是未来 Flutter Web 只需要聚焦在 Wasm/WebAssembly 即可。
\\n
在 2025 年,Fluter 计划进一步改进 Flutter Web 的核心,例如:Accessibility 、文本输入、国际文本渲染、大小、性能和平台集成等,并计划删除遗留的 HTML 和 JS 库。
\\n最后 Web 平台当前预览的 hotload,也有望在 2025 年推出。
\\nFlutter 的核心团队表示 2025 年将专注于移动和 Web 支持,而 Desktop 的开发维护其实从 2024 年开始就已经是 Canonical 团队负责,比如去年公布的《Flutter PC 多窗口新进展》 就是 Canonical 负责推进的 Linux、macOS 和 Windows 多窗口的支持。
\\n\\n\\n目前已经完成 windows 平台支持,并正在支持 Linux 和 MacOS ,详细可见 :github.com/flutter/flu…
\\n
在 《Dart 宏功能推进暂停》 我们知道了 Dart 中宏支持方案被放弃,所以 Flutter 预计在 2025 年将改进 build_runner 中当前对代码生成的支持,并探索改进 Dart 对序列化和反序列化支持的替代方法。
\\n\\n\\n详细可见: github.com/dart-lang/b…
\\n
另外官方还计划研究对交叉编译 Dart AOT 可执行文件的支持,例如「在 macOS 开发机器上编译为 Linux AOT 可执行文件」的相关支持。
\\n官方继续表示不会提供热更新或者代码推送服务,对于代码推送,官方推荐 shorebird.dev ,至于为什么官方推荐 shorebird,可以看之前的 《Flutter 里最接近官方的热更新方案:Shorebird》 ,整体来看 shorebird 确实是比较不错的选择。
\\n而如果是 UI 推送(也称为服务器驱动的 UI),官方推荐 rfw ,它提供了一种机制,用于根据可在运行时获取的声明性 UI 描述来呈现控件,从而实现远程驱动的效果。
\\n目前看来,「交叉编译 Dart AOT 」是我 2025 里最感兴趣的特性,当然,在 Windows 上直接构建出一个 iOS 的 Ipa 这种支持我估计不会有,毕竟这个的可行性和复杂度太高了。
\\n而最期待的莫过于 Canonical 团队的支持,希望目前多窗口的 draft 可以最终落地成功,毕竟这段时间的 Desktop 开发体验,缺少多窗口确实是很大的局限。
\\n那么,你最希望什么特性能在 2025 年被完成?
","description":"又到了 Flutter 公布年度计划的时候,开始之前我们先回顾下官方对 2024 Roadmap 的完成度 ✅ iOS 完成 Impeller 完成迁移,skia 不再可用\\n✅ Material 3 default\\n❌ Multiple Flutter Views 的支持计划\\n✅ Swift Package Manager 和 Kotlin Script 支持\\n✅ Dart 和原生的互操作性与 Native assets 推进\\n✅ Web 多线程,PlatformView 优化、JS 互操作、WasmGC 和 Skwasm 落地\\n✅ Web…","guid":"https://juejin.cn/post/7488582788673945634","author":"恋猫de小郭","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-02T22:22:04.765Z","media":null,"categories":["Android","前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 教程(十)主题","url":"https://juejin.cn/post/7488522007765467163","content":"如下代码所示,当我们创建一个新 Flutter 项目时,会在 theme 属性中赋值 ThemeData 对象。其中 theme 属性就是主题的入口,该 ThemeData 对象就是设置的对应主题。
\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n title: \'Flutter Demo\',\\n theme: ThemeData(\\n primarySwatch: Colors.blue,\\n ),\\n home: MyHomePage(title: \'Flutter Demo Home Page\'),\\n );\\n }\\n}\\n
\\nThemeData 组件包含项目能设置的所有主题样式,其中最常用的属性有primarySwatch、primaryColor、accentColor等27种。ThemeData 常用的属性如下图所示:
\\n\\n\\n注意:ThemeData 包含一些组件的属性,比如 TextField 组件的文本提示颜色 hintColor。如果改变了 ThemeData 中 hintColor 的值则会对全局的 TextField 组件造成影响。但是我们可以在 TextField 组件的 hintStyle 中自定义 hintColor 的颜色,此时在局部小组件中单独设置的主题样式会覆盖全局主题设置的样式
\\n
根据上图的属性,我们可以对 ThemeData 的属性进行一些自定义的设置,代码示例如下:
\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n title: \'Flutter Demo\',\\n theme: ThemeData(\\n scaffoldBackgroundColor: Colors.deepOrangeAccent,\\n accentColor: Colors.yellow,\\n primarySwatch: Colors.red,\\n hintColor: Colors.blue,\\n brightness: Brightness.light,\\n buttonTheme: ButtonThemeData(\\n textTheme: ButtonTextTheme.accent,\\n buttonColor: Colors.greenAccent,\\n ),\\n ),\\n home: MyHomePage(title: \'主题设置\'),\\n );\\n }\\n}\\n
\\n效果如下图所示:
\\n上面在 MaterialApp 中设置的主题是全局生效的。如果你只想要在单个组件应用对应的主题,可以使用 Theme 组件。代码示例如下:
\\n// 方式一\\nnew Theme(\\n data: new ThemeData(\\n hintColor: Colors.yellow,\\n ),\\n child: TextField(\\n keyboardType: TextInputType.emailAddress,\\n decoration: InputDecoration(\\n hintText: \'提示文本\',\\n prefixIcon: Icon(Icons.all_inclusive),\\n ),\\n ),\\n),\\n\\n// 方式二\\nnew Theme(\\n data:Theme.of(context).copyWith(hintColor: Colors.yellow),\\n child: TextField(\\n keyboardType: TextInputType.emailAddress,\\n decoration: InputDecoration(\\n hintText: \'提示文本\',\\n prefixIcon: Icon(Icons.all_inclusive),\\n ),\\n ),\\n),\\n
\\nclass ThemePage extends StatefulWidget {\\n ThemePage({Key key, this.title}) : super(key: key);\\n\\n final String title;\\n\\n @override\\n _ThemePageState createState() => _ThemePageState();\\n}\\n\\nclass _ThemePageState extends State<ThemePage> {\\n\\n List<Color> _colorList=[ Colors.yellow, Colors.greenAccent, Colors.indigo, Colors.black38, Colors.red, Colors.deepPurpleAccent, Colors.brown, Colors.indigo, Colors.pinkAccent ];\\n\\n Color _color=Colors.yellow;\\n\\n @override\\n Widget build(BuildContext context) {\\n return new Theme(\\n data: Theme.of(context).copyWith(primaryColor: _color),\\n child: Scaffold(\\n appBar: AppBar(\\n title: Text(widget.title),\\n ),\\n body: GridView.builder(\\n padding: EdgeInsets.only(left: 0,top: 100,right: 0,bottom: 0),\\n gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(\\n crossAxisCount: 3,\\n mainAxisSpacing: 20,\\n crossAxisSpacing: 20,\\n childAspectRatio: 1,\\n ),\\n itemBuilder: (BuildContext context,int index){\\n return GestureDetector(\\n child: Container(\\n color: _colorList[index],\\n ),\\n onTap: (){\\n _color=_colorList[index];\\n setState(() {\\n\\n });\\n },\\n );\\n },\\n itemCount: 9,\\n ),\\n ),\\n );\\n }\\n}\\n
\\n在 App 中,我们需要申请权限才能执行一些特殊操作,比如拍照、录音等。在 Flutter 里,处理权限问题一般借助 permission_handler
插件来实现的。
在 pubspec.yaml
文件中添加 permission_handler
插件,代码示例如下:
dependencies:\\n permission_handler: ^11.4.0\\n
\\n然后在代码中导入 import \'package:permission_handler/permission_handler.dart\';
就可以使用该插件了。
当在运行时请求权限时,我们仍然需要告诉操作系统你的应用程序可能会使用哪些权限。这需要在Android和ios特定文件中添加权限配置。
\\nandroid.useAndroidX=true\\nandroid.enableJetifier=true\\n
\\nandroid {\\n compileSdkVersion 33\\n ...\\n}\\n
\\nIOS的配置具体可以看 官方文档
\\n这里以请求相机的权限为例,介绍如何使用 permission_handler
\\n// 调用 Permission.camera.status 方法来获取相机权限的当前状态,\\n// 并将其赋值给变量 status。此方法会异步返回相机权限的状态信息。\\nvar status = await Permission.camera.status;\\n// 检查相机权限状态是否为 “被拒绝”(isDenied)。\\n// 若为 “被拒绝”,意味着还未请求过权限,或者之前请求过但被用户拒绝了,不过并非永久拒绝。\\nif (status.isDenied) {\\n ...\\n}\\n\\n// 你也可以直接询问某个权限的状态。\\n// 这里直接调用 Permission.location.isRestricted 来检查定位权限是否被系统限制,\\n// 此方法会异步返回一个布尔值,指示定位权限是否受限制。\\nif (await Permission.location.isRestricted) {\\n // 若定位权限被限制,通常是由于操作系统的限制,\\n // 例如开启了家长控制功能,导致应用无法正常访问定位权限。\\n}\\n
\\n// 以下代码通过链式调用的方式为相机权限的不同状态设置回调函数,然后发起权限请求。\\n// 首先使用 await 关键字,因为权限请求是一个异步操作,等待整个请求流程完成。\\nawait\\n // 获取相机权限的 Permission 实例,后续的操作都基于相机权限展开。\\n Permission.camera\\n // 设置当相机权限被拒绝时的回调函数。\\n // 当用户拒绝了相机权限请求,就会执行该回调函数内的代码。\\n .onDeniedCallback(() {\\n ...\\n })\\n // 设置当相机权限被授予时的回调函数。\\n // 当用户同意授予相机权限,就会执行该回调函数内的代码。\\n .onGrantedCallback(() {\\n ...\\n })\\n // 设置当相机权限被永久拒绝时的回调函数。\\n // 当用户在拒绝权限时选择了不再询问,就会触发此回调。\\n .onPermanentlyDeniedCallback(() {\\n ...\\n })\\n // 设置当相机权限受到限制时的回调函数。\\n // 例如因为系统设置(如家长控制)导致权限受限,会触发此回调。\\n .onRestrictedCallback(() {\\n ...\\n })\\n // 设置当相机权限为有限权限时的回调函数。\\n // 在某些系统中,用户可能会授予应用有限的权限访问,此时会触发此回调。\\n .onLimitedCallback(() {\\n ...\\n })\\n // 设置当相机权限为临时权限时的回调函数。\\n // 某些系统可能会提供临时权限,这种情况下会触发此回调。\\n .onProvisionalCallback(() {\\n ...\\n })\\n // 发起相机权限请求,前面设置的各个回调函数会根据权限请求的结果来执行。\\n .request();\\n
\\n// 请求多个权限。\\nMap<Permission, PermissionStatus> statuses = await [\\n Permission.location,\\n Permission.storage,\\n].request();\\n\\nprint(statuses[Permission.location]);\\n
\\n// 此代码块用于检查语音权限是否被永久拒绝。\\nif (await Permission.speech.isPermanentlyDenied) {\\n // 如果语音权限被永久拒绝,会执行到这里。\\n // 在这种情况下,唯一能改变权限状态的方法是让用户手动在系统设置中启用该权限。\\n // 调用 openAppSettings() 打开应用的系统设置页面。这样用户就可以在系统设置中手动更改语音权限的状态。\\n openAppSettings();\\n}\\n
\\n笔者之前是java后端开发,转flutter之后又被公司裁员,当时对flutter知之甚少,去面试没看面经遂被第一题考倒:flutter中widget的key有什么作用?
\\n查阅了网上很多描述都不太理解,最近在某视频学习网站看了一个讲解视频才略有了解。
\\n先上代码,以下代码自定义了组件Box,Box中用GestureDetector包裹了一个有定义颜色的Container,每点击Box一次就会使的里面的数字+1.
\\nimport \'package:flutter/material.dart\';\\n\\nvoid main() {\\n runApp(\\n MaterialApp(\\n home: MyHome(),\\n ),\\n );\\n}\\n\\nclass MyHome extends StatefulWidget {\\n const MyHome({super.key});\\n\\n @override\\n State<MyHome> createState() => _MyHomeState();\\n}\\n\\nclass _MyHomeState extends State<MyHome> {\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(),\\n body: Center(\\n child: Column(\\n children: [\\n Box(Colors.red),\\n Box(Colors.yellow),\\n Box(Colors.blue),\\n ],\\n ),\\n ),\\n );\\n }\\n}\\n\\nclass Box extends StatefulWidget {\\n const Box(this.color); // 接收并传递 key\\n final Color color;\\n\\n @override\\n State<Box> createState() => _BoxState();\\n}\\n\\nclass _BoxState extends State<Box> {\\n int _counter = 0;\\n\\n @override\\n Widget build(BuildContext context) {\\n return GestureDetector(\\n onTap: () {\\n _counter++;\\n setState(() {});\\n },\\n child: GestureDetector(\\n onTap: () {\\n _counter++;\\n setState(() {});\\n },\\n child: Container(\\n width: 80,\\n height: 80,\\n color: widget.color,\\n child: Text(\\"$_counter\\"),\\n ),\\n ),\\n );\\n }\\n}\\n
\\n把以上代码运行起来,再从上往下依次点击1、2、3次,我们会得到这么一个页面
\\n此时我们再把_MyHomeState中的builder方法里的Box(Colors.yellow)注释掉,再hot reload一下,各位读者可以猜下页面会变成什么样
\\nclass _MyHomeState extends State<MyHome> {\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(),\\n body: Center(\\n child: Column(\\n children: [\\n Box(Colors.red),\\n // Box(Colors.yellow),\\n Box(Colors.blue),\\n ],\\n ),\\n ),\\n );\\n }\\n}\\n
\\n如果各位读者像我一样对flutter的key不甚了解,可能会认为,页面上从上往下依次是:红1、蓝3,然而页面变成了这样
\\n为什么会变成这样呢,这正是因为没有指定key导致的。如下图所示,我们在创建widget树的时候也会创建对应的element树,而状态是保存在elemnet树中的
\\n当我们删除了黄色的box之后,蓝色的box就顺理成章上移动一格,匹配到右边的的state = 2的box element。而state = 3的boxelement因为找不到对应的widget也被释放掉了。
\\n正是因为上图所说没匹配到的elemnet被释放了,我们如果把之前注释的代码加回来,再hot reload一下,得到的不是红1、黄2、蓝3,而是红1、黄2、蓝0(初始值为0)
\\n简单修改代码(26、27、28、37行),给Box传递参数key
\\nimport \'package:flutter/material.dart\';\\n\\nvoid main() {\\n runApp(\\n MaterialApp(\\n home: MyHome(),\\n ),\\n );\\n}\\n\\nclass MyHome extends StatefulWidget {\\n const MyHome({super.key});\\n\\n @override\\n State<MyHome> createState() => _MyHomeState();\\n}\\n\\nclass _MyHomeState extends State<MyHome> {\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(),\\n body: Center(\\n child: Column(\\n children: [\\n Box(Colors.red, key: ValueKey(1),),\\n Box(Colors.yellow, key: ValueKey(2),),\\n Box(Colors.blue, key: ValueKey(3),),\\n ],\\n ),\\n ),\\n );\\n }\\n}\\n\\nclass Box extends StatefulWidget {\\n const Box(this.color, {super.key});\\n final Color color;\\n\\n @override\\n State<Box> createState() => _BoxState();\\n}\\n\\nclass _BoxState extends State<Box> {\\n int _counter = 0;\\n\\n @override\\n Widget build(BuildContext context) {\\n return GestureDetector(\\n onTap: () {\\n _counter++;\\n setState(() {});\\n },\\n child: GestureDetector(\\n onTap: () {\\n _counter++;\\n setState(() {});\\n },\\n child: Container(\\n width: 80,\\n height: 80,\\n color: widget.color,\\n child: Text(\\"$_counter\\"),\\n ),\\n ),\\n );\\n }\\n}\\n
\\n运行程序,从上往下分别点击1、2、3次,得到红1黄2蓝3
\\n此时再把黄色的box注释掉,hot reload一下,就会得到红1蓝3,符合我们的预期
\\n这是因为加了key之后,widget和element不再通过顺序匹配了,而是通过key来匹配,所以这次是key=2的element被释放了
\\nDio
是 Flutter
中一个功能强大的 HTTP
客户端库,其核心机制围绕配置管理、拦截器链、适配器模式和错误处理展开。接下来我们对其配置管理机制的深入解析。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\nDio
实例的本质
Dio
实例是 HTTP
请求的核心载体,每个实例独立维护自己的配置和拦截器链。这意味着:
1、实例独立性:多个实例可共存,各自拥有独立的 BaseOptions
、拦截器
和适配器
,适用于多后端服务场景。
// 创建两个独立实例,对接不同 API\\nDio publicApi = Dio(BaseOptions(baseUrl: \\"https://public.api.com\\"));\\nDio internalApi = Dio(BaseOptions(baseUrl: \\"https://internal.api.com\\"));\\n
\\n2、轻量级设计:实例本身无复杂状态,配置和拦截器通过组合方式注入
,符合 Flutter
的不可变设计思想。
配置体系分为三级
,优先级为:单次请求配置 > 实例级配置 > 全局配置。
// 全局配置(影响所有实例)\\nDio.defaultOptions = BaseOptions(responseType: ResponseType.json);\\n\\n// 实例级配置(覆盖全局)\\nfinal dio = Dio(BaseOptions(connectTimeout: Duration(seconds: 15)));\\n\\n// 请求级配置(最高优先级)单次请求覆盖 baseUrl\\nDio dio = Dio(BaseOptions(baseUrl: \\"https://api.example.com\\"));\\ndio.get(\\"/path\\", options: Options(baseUrl: \\"https://other.api.com\\"));\\n
\\n配置类型 | 作用域 | 描述 |
---|---|---|
BaseOptions | 实例级(全局) | 初始化 Dio 实例时传入,定义全局默认参数(如 baseUrl 、超时时间 等) |
Options | 单次请求级 | 在发起请求时传入,覆盖实例级配置,仅对当前请求生效 |
RequestOptions | 最终请求级(内部) | 由 BaseOptions 和 Options 合并生成,实际发起请求时使用的配置 |
当发起请求时,Dio
会通过 merge
方法合并配置,规则如下:
Options
中的非空字段会覆盖 BaseOptions
中的对应字段。headers
等 Map
类型字段执行浅合并(非递归覆盖
)。RequestOptions
不可修改,确保请求过程中的配置一致性。Dio dio = Dio(BaseOptions(\\n baseUrl: \\"https://api.example.com\\",\\n connectTimeout: Duration(seconds: 5),\\n headers: {\\"User-Agent\\": \\"Dio/5.0\\"},\\n));\\n\\n// 单次请求配置\\ndio.get(\\"/user\\", options: Options(\\n headers: {\\"Authorization\\": \\"Bearer token\\"}, // 合并后 headers 为 {\\"User-Agent\\": \\"Dio/5.0\\", \\"Authorization\\": \\"Bearer token\\"}\\n connectTimeout: Duration(seconds: 10), // 覆盖全局 connectTimeout\\n));\\n
\\n配置项 | 类型 | 说明 |
---|---|---|
baseUrl | String | 基础 URL ,与请求路径拼接生成完整 URL |
connectTimeout | Duration | 建立连接的超时时间 |
receiveTimeout | Duration | 接收数据流的超时时间 |
sendTimeout | Duration | 发送数据流的超时时间 |
headers | Map<String, dynamic> | 请求头,支持全局和单次覆盖 |
queryParameters | Map<String, dynamic> | URL 查询参数,自动拼接到 URL |
extra | Map<String, dynamic> | 扩展字段,用于在拦截器间传递自定义数据 |
responseType | ResponseType | 响应数据类型(json 、stream 、bytes 等) |
validateStatus | (int) → bool | 自定义 HTTP 状态码校验逻辑,默认 200-299 视为成功 |
统一管理:将 Dio
实例封装为单例,统一管理全局配置:
class ApiClient {\\n static final Dio _dio = Dio(BaseOptions(\\n baseUrl: \\"https://api.example.com\\",\\n connectTimeout: Duration(seconds: 10),\\n ));\\n\\n static Dio get dio => _dio;\\n}\\n
\\n动态环境切换:通过修改 baseUrl
实现多环境切换:
void switchEnvironment(Environment env) {\\n ApiClient._dio.options.baseUrl = env.baseUrl;\\n}\\n
\\n优先级控制:利用单次配置覆盖全局规则:
\\n// 临时关闭超时限制\\ndio.get(\\"/large-file\\", options: Options(\\n receiveTimeout: Duration(minutes: 5),\\n));\\n
\\n扩展元数据:通过 extra
字段传递请求上下文:
dio.get(\\"/user\\", options: Options(\\n extra: {\\"retryCount\\": 3}, // 在拦截器中读取此字段实现重试逻辑\\n));\\n
\\n通过拦截器打印最终 RequestOptions
:
dio.interceptors.add(InterceptorsWrapper(\\n onRequest: (options, handler) {\\n print(\\"Merged Config: ${options.uri} | Headers: ${options.headers}\\");\\n handler.next(options);\\n },\\n));\\n
\\nHeader
注入结合拦截器实现 Token
动态刷新:
dio.interceptors.add(InterceptorsWrapper(\\n onRequest: (options, handler) async {\\n if (!options.path.contains(\\"/login\\")) {\\n options.headers[\\"Authorization\\"] = \\"Bearer ${await TokenManager.getToken()}\\";\\n }\\n handler.next(options);\\n },\\n));\\n
\\n使用 flutter_dotenv
实现环境变量注入:
// .env 文件\\nBASE_URL=https://api.staging.example.com\\n\\n// 初始化 Dio\\nDio dio = Dio(BaseOptions(\\n baseUrl: dotenv.get(\\"BASE_URL\\"),\\n));\\n
\\nMock
替换适配器实现接口 Mock
:
dio.httpClientAdapter = MockAdapter()\\n ..onGet(\\"/user\\", (request) => MockResponse(userJson, 200));\\n
\\nDio
的配置管理体系通过分层覆盖策略和灵活的合并机制,实现了从全局到单次请求的细粒度控制。我们可通过:
深入理解配置管理机制,能够显著提升代码的可维护性,并高效应对复杂网络请求需求。
\\n\\n","description":"前言 Dio 是 Flutter 中一个功能强大的 HTTP 客户端库,其核心机制围绕配置管理、拦截器链、适配器模式和错误处理展开。接下来我们对其配置管理机制的深入解析。\\n\\n操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。\\n\\n一、Dio 实例的本质\\n\\nDio 实例是 HTTP 请求的核心载体,每个实例独立维护自己的配置和拦截器链。这意味着:\\n\\n1、实例独立性:多个实例可共存,各自拥有独立的 BaseOptions、拦截器和适配器,适用于多后端服务场景。\\n\\n// 创建两个独立实例,对接不同 API\\nDio publicApi = Dio…","guid":"https://juejin.cn/post/7488273952958169098","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-02T08:50:43.605Z","media":null,"categories":["Android","Dart","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"系统化掌握Dart网络编程之Dio(二):责任链模式篇","url":"https://juejin.cn/post/7488321730765815818","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
在软件系统的复杂交互中,请求的处理往往需要跨越多个层级或模块
。责任链模式应运而生,它通过将处理对象串联为一条\\"逻辑流水线\\"
,让每个节点专注单一职责,实现请求的自动传递与动态分配。
这种模式如同精密传送带:每个工位(处理者
)自主判断能否处理任务,或将其递交给下一环节,既避免了发送者与接收者的强耦合,又赋予系统运行时灵活调整链路的能力。
从网络拦截器的双向过滤到多级审批流程的智能路由,责任链以优雅的链式结构,在权限控制
、日志处理
、异常兜底
等场景中,为复杂逻辑提供了高扩展
、低侵入
的解决方案,堪称分布式协作的典范设计。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\n责任链模式是一种 行为型设计模式,核心思想是:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n让
\\n多个对象
都有机会处理请求,将这些处理对象连成一条链
,请求沿着链传递直到被处理为止
概念 | 含义 |
---|---|
处理者链 | 由多个处理者对象组成的链式结构 |
请求传递 | 请求从链首开始传递,每个处理者决定处理或传递给下一个 |
解耦发送者与接收者 | 发送者不需要知道具体由哪个对象处理请求 |
动态扩展 | 可随时增删处理者,灵活调整处理流程 |
本质
核心定义:
\\n由多个
独立处理器对象 构成的链式结构,每个处理器持有下一个处理器的引用,形成 单向或双向处理通道。
结构解析:
\\n// 链表式处理器实现\\nabstract class Handler {\\n Handler? _next; // 后继处理器引用\\n \\n void setNext(Handler next) => _next = next;\\n \\n void handle(Request request) {\\n if (canHandle(request)) {\\n process(request);\\n } else {\\n _next?.handle(request); // 链式传递\\n }\\n }\\n \\n bool canHandle(Request request); // 处理判断\\n void process(Request request); // 处理逻辑\\n}\\n
\\n动态构建示例:
\\n// 构建三级处理链\\nfinal handlerChain = FirstHandler()\\n ..setNext(SecondHandler())\\n ..setNext(FinalHandler());\\n\\n// 请求处理流程\\n// Request → FirstHandler → SecondHandler → FinalHandler\\n
\\n关键特性:
\\n线性链
、树形链
、环形链
等多种结构。动态增删处理器
(如热更新过滤规则
)。关注自己
的处理逻辑。传递逻辑流程图:
\\ngraph TD\\n A[请求进入] --\x3e B{处理器A能否处理?}\\n B -- 是 --\x3e C[处理器A处理]\\n B -- 否 --\x3e D{传递给处理器B?}\\n D -- 是 --\x3e E[处理器B处理]\\n D -- 否 --\x3e F[结束传递]\\n
\\n关键处理策略:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n策略类型 | 实现方式 | 应用场景 |
---|---|---|
短路传递 | 处理器处理后立即终止链条 | 权限校验失败拦截 |
全链路传递 | 所有处理器依次处理 | 日志记录链 |
条件分支传递 | 根据处理结果选择不同后继处理器 | 多条件审批流程 |
异步传递示例:
\\n// 异步处理器实现\\nmixin AsyncHandler on Handler {\\n Future<void> handle(Request request) async {\\n if (await canHandleAsync(request)) {\\n await processAsync(request);\\n } else {\\n await _next?.handle(request);\\n }\\n }\\n \\n Future<bool> canHandleAsync(Request request);\\n Future<void> processAsync(Request request);\\n}\\n
\\n传统耦合模式:
\\n// 发送者直接依赖具体处理器\\nclass Sender {\\n final HandlerA _handlerA;\\n final HandlerB _handlerB;\\n\\n void send(Request request) {\\n if (_handlerA.canHandle(request)) {\\n _handlerA.process(request);\\n } else if (_handlerB.canHandle(request)) {\\n _handlerB.process(request);\\n }\\n }\\n}\\n
\\n责任链解耦实现:
\\n// 发送者只需知道链入口\\nclass DecoupledSender {\\n final Handler _chainHead;\\n\\n void send(Request request) {\\n _chainHead.handle(request);\\n }\\n}\\n\\n// 配置处理链\\nfinal chain = HandlerA()..setNext(HandlerB());\\nfinal sender = DecoupledSender(chain);\\n
\\n解耦优势:
\\n90%
。无需修改
发送者。动态替换
处理链。动态配置示例:
\\n// 可配置处理链管理器\\nclass HandlerChain {\\n final List<Handler> _handlers = [];\\n \\n void addHandler(Handler handler) => _handlers.add(handler);\\n \\n void handle(Request request) {\\n for (var handler in _handlers) {\\n if (handler.canHandle(request)) {\\n handler.process(request);\\n break; // 短路处理\\n }\\n }\\n }\\n}\\n\\n// 运行时动态配置\\nfinal chain = HandlerChain()\\n ..addHandler(ValidationHandler())\\n ..addHandler(CacheHandler(expiry: Duration(hours: 1)))\\n ..addHandler(APIClient());\\n
\\n扩展场景:
\\nA/B
测试:为不同用户组配置不同处理链。classDiagram\\n class Client {\\n +send(Request)\\n }\\n \\n class Handler {\\n <<abstract>>\\n +setNext(Handler)\\n +handle(Request)\\n +canHandle(Request)*\\n +process(Request)*\\n }\\n \\n class ConcreteHandlerA {\\n +canHandle(Request)\\n +process(Request)\\n }\\n \\n class ConcreteHandlerB {\\n +canHandle(Request)\\n +process(Request)\\n }\\n \\n Client --\x3e Handler\\n Handler <|-- ConcreteHandlerA\\n Handler <|-- ConcreteHandlerB\\n Handler --\x3e Handler : _next\\n
\\nHandler
)/// 链表式处理器实现\\nabstract class Handler {\\n /// 链中的下一个处理者\\n Handler? _nextHandler;\\n\\n /// 设置下一个处理者\\n Handler setNext(Handler handler) {\\n _nextHandler = handler;\\n return handler; // 支持链式调用\\n }\\n\\n /// 处理请求的模板方法\\n void handleRequest(String request) {\\n if (canHandle(request)) {\\n doHandle(request);\\n } else if (_nextHandler != null) {\\n _nextHandler!.handleRequest(request);\\n } else {\\n defaultHandler(request);\\n }\\n }\\n\\n /// 判断是否能处理请求(由子类实现)\\n bool canHandle(String request);\\n\\n /// 具体处理逻辑(由子类实现)\\n void doHandle(String request);\\n\\n /// 默认处理方式\\n void defaultHandler(String request) {\\n print(\\"⚠️ 没有处理者能处理请求:$request\\");\\n }\\n}\\n
\\n/// 处理者A:处理长度<=5的请求\\nclass LengthHandler extends Handler {\\n @override\\n bool canHandle(String request) => request.length <= 5;\\n\\n @override\\n void doHandle(String request) {\\n print(\\"🟢 LengthHandler 处理请求:\'$request\' (长度=${request.length})\\");\\n }\\n}\\n\\n/// 处理者B:处理包含\\"VIP\\"的请求\\nclass VIPHandler extends Handler {\\n @override\\n bool canHandle(String request) => request.contains(\\"VIP\\");\\n\\n @override\\n void doHandle(String request) {\\n print(\\"🔵 VIPHandler 处理特殊请求:\'$request\'\\");\\n }\\n}\\n\\n/// 处理者C:处理数字请求\\nclass NumberHandler extends Handler {\\n @override\\n bool canHandle(String request) => RegExp(r\'^\\\\d+$\').hasMatch(request);\\n\\n @override\\n void doHandle(String request) {\\n print(\\"🟡 NumberHandler 处理数字请求:\'$request\'\\");\\n }\\n}\\n
\\nvoid main() {\\n /// 1. 创建处理链\\n final handlerChain = LengthHandler()\\n ..setNext(VIPHandler())\\n ..setNext(NumberHandler());\\n\\n /// 2. 发送不同请求\\n const requests = [\'123\', \'HelloVIP\', \'567890\', \'TooLongRequest\', \'VIP123\'];\\n\\n for (final request in requests) {\\n print(\\"\\\\n🚀 发送请求:\'$request\'\\");\\n handlerChain.handleRequest(request);\\n }\\n}\\n
\\n输出结果:
\\n🚀 发送请求:\'123\'\\n🟢 LengthHandler 处理请求:\'123\' (长度=3)\\n\\n🚀 发送请求:\'HelloVIP\'\\n⚠️ 没有处理者能处理请求:HelloVIP\\n\\n🚀 发送请求:\'567890\'\\n🟡 NumberHandler 处理数字请求:\'567890\'\\n\\n🚀 发送请求:\'TooLongRequest\'\\n⚠️ 没有处理者能处理请求:TooLongRequest\\n\\n🚀 发送请求:\'VIP123\'\\n⚠️ 没有处理者能处理请求:VIP123\\n
\\nsequenceDiagram\\n participant Client\\n participant HandlerA\\n participant HandlerB\\n participant HandlerC\\n\\n Client->>HandlerA: 发送请求\\n HandlerA->>HandlerA: 能否处理?\\n alt 能处理\\n HandlerA--\x3e>Client: 处理完成\\n else 不能处理\\n HandlerA->>HandlerB: 传递请求\\n HandlerB->>HandlerB: 能否处理?\\n alt 能处理\\n HandlerB--\x3e>Client: 处理完成\\n else 不能处理\\n HandlerB->>HandlerC: 传递请求\\n HandlerC->>HandlerC: 能否处理?\\n alt 能处理\\n HandlerC--\x3e>Client: 处理完成\\n else 不能处理\\n HandlerC--\x3e>Client: 无法处理\\n end\\n end\\n end\\n
\\nsetNext
方法形成链条。_canHandle
决定是否处理。Handler
并实现两个方法。场景 | 案例实现 |
---|---|
网络请求拦截 | Dio拦截器、OkHttp拦截器 |
事件处理系统 | GUI事件传播(如Flutter手势竞争) |
工作流审批系统 | 多级审批流程(经理→总监→CEO) |
日志处理管道 | 日志级别过滤 → 格式转换 → 输出目标选择 |
异常处理系统 | 本地缓存 → 网络重试 → 全局降级 |
abstract class Approver {\\n Approver? next;\\n\\n bool handle(ApprovalRequest request) {\\n if (_canHandle(request)) {\\n print(\\"✅ [$runtimeType] 批准金额:$${request.amount.toStringAsFixed(2)}\\");\\n return true;\\n }\\n\\n if (next != null) {\\n return next!.handle(request);\\n }\\n\\n print(\\"❌ 金额 $${request.amount} 超出所有审批人权限\\");\\n return false;\\n }\\n\\n bool _canHandle(ApprovalRequest request); // 抽象方法\\n}\\n\\n// ---------- 具体审批人 ----------\\nclass Manager extends Approver {\\n @override\\n bool _canHandle(ApprovalRequest request) => request.amount <= 10000;\\n}\\n\\nclass Director extends Approver {\\n @override\\n bool _canHandle(ApprovalRequest request) => request.amount <= 50000;\\n}\\n\\nclass CEO extends Approver {\\n @override\\n bool _canHandle(ApprovalRequest request) => true; // 无金额限制\\n}\\n\\nclass ApprovalRequest {\\n final double amount;\\n\\n ApprovalRequest(this.amount);\\n}\\n\\nvoid main() {\\n // 构建审批链:经理 → 总监 → CEO\\n final manager = Manager();\\n final director = Director();\\n final ceo = CEO();\\n\\n manager.next = director;\\n director.next = ceo;\\n\\n // 测试用例\\n final testAmounts = [8000.0, 25000.0, 100000.0];\\n\\n for (final amount in testAmounts) {\\n print(\\"\\\\n=== 处理申请:$${amount.toStringAsFixed(2)} ===\\");\\n manager.handle(ApprovalRequest(amount));\\n }\\n}\\n\\n输出结果:\\n=== 处理申请:$8000.00 ===\\n✅ [Manager] 批准金额:$8000.00\\n\\n=== 处理申请:$25000.00 ===\\n✅ [Director] 批准金额:$25000.00\\n\\n=== 处理申请:$100000.00 ===\\n✅ [CEO] 批准金额:$100000.00\\n
\\nimport \'dart:io\';\\nimport \'dart:convert\';\\n\\n/// ------------------ 核心抽象定义 ------------------\\nabstract class Handler {\\n Handler? _next;\\n\\n Handler setNext(Handler next) {\\n _next = next;\\n return this;\\n }\\n\\n Future<Response> handle(Request request) async {\\n if (await canHandle(request)) {\\n return await process(request);\\n } else if (_next != null) {\\n return await _next!.handle(request);\\n } else {\\n return Response(404, \'Not Found\');\\n }\\n }\\n\\n bool canHandle(Request request);\\n Future<Response> process(Request request);\\n}\\n\\n/// ------------------ 具体模型定义 ------------------\\nclass Request {\\n final String method;\\n final Uri uri;\\n final Map<String, String> headers;\\n\\n Request.get(this.uri, {this.headers = const {}}) : method = \'GET\';\\n}\\n\\nclass Response {\\n final int statusCode;\\n final String body;\\n\\n Response(this.statusCode, this.body);\\n}\\n\\n/// ------------------ 具体处理者实现 ------------------\\nclass LogHandler extends Handler {\\n @override\\n bool canHandle(Request request) => true;\\n\\n @override\\n Future<Response> process(Request request) async {\\n final stopwatch = Stopwatch()..start();\\n print(\'📥 请求开始: ${request.uri}\');\\n\\n try {\\n final response = await _next?.handle(request) ?? Response(404, \'Not Found\');\\n print(\'📤 请求完成 (${stopwatch.elapsedMilliseconds}ms)\');\\n return response;\\n } catch (e) {\\n print(\'💥 请求异常: $e\');\\n return Response(500, \'Server Error\');\\n }\\n }\\n}\\n\\nclass AuthHandler extends Handler {\\n final String _validToken;\\n\\n AuthHandler(this._validToken);\\n\\n @override\\n bool canHandle(Request request) {\\n return request.headers.containsKey(\'Authorization\');\\n }\\n\\n @override\\n Future<Response> process(Request request) async {\\n final token = request.headers[\'Authorization\']!.split(\' \').last;\\n if (token != _validToken) {\\n return Response(401, \'Unauthorized\');\\n }\\n print(\'🔑 认证通过\');\\n return await _next?.handle(request) ?? Response(404, \'Not Found\');\\n }\\n}\\n\\nclass HttpClientHandler extends Handler {\\n final HttpClient _client = HttpClient();\\n\\n @override\\n bool canHandle(Request request) => true;\\n\\n @override\\n Future<Response> process(Request request) async {\\n try {\\n final req = await _client.getUrl(request.uri);\\n request.headers.forEach((key, value) => req.headers.add(key, value));\\n\\n final res = await req.close();\\n final body = await res.transform(utf8.decoder).join();\\n\\n return Response(res.statusCode, body);\\n } finally {\\n _client.close();\\n }\\n }\\n}\\n\\n// ------------------ 使用示例 ------------------\\nvoid main() async {\\n // 构建处理链\\n final logHandler = LogHandler();\\n final authHandler = AuthHandler(\'valid_token_123\');\\n final httpClientHandler = HttpClientHandler();\\n\\n logHandler.setNext(authHandler);\\n authHandler.setNext(httpClientHandler);\\n\\n // 测试用例\\n final testCases = [\\n Request.get(Uri.parse(\'https://jsonplaceholder.typicode.com/posts/1\')),\\n Request.get(\\n Uri.parse(\'https://jsonplaceholder.typicode.com/posts/1\'),\\n headers: {\'Authorization\': \'Bearer valid_token_123\'}\\n ),\\n Request.get(Uri.parse(\'https://invalid.url\'))\\n ];\\n\\n for (var request in testCases) {\\n print(\'\\\\n${\'=\' * 40}\');\\n print(\'测试请求: ${request.uri}\');\\n\\n final response = await logHandler.handle(request);\\n print(\'状态码: ${response.statusCode}\');\\n print(\'响应长度: ${response.body.length}字符\');\\n }\\n}\\n\\n输出结果:\\n========================================\\n测试请求: https://jsonplaceholder.typicode.com/posts/1\\n📥 请求开始: https://jsonplaceholder.typicode.com/posts/1\\n📤 请求完成 (736ms)\\n状态码: 200\\n响应长度: 292字符\\n\\n========================================\\n测试请求: https://jsonplaceholder.typicode.com/posts/1\\n📥 请求开始: https://jsonplaceholder.typicode.com/posts/1\\n🔑 认证通过\\n💥 请求异常: Bad state: Client is closed\\n状态码: 500\\n响应长度: 12字符\\n\\n========================================\\n测试请求: https://invalid.url\\n📥 请求开始: https://invalid.url\\n💥 请求异常: Bad state: Client is closed\\n状态码: 500\\n响应长度: 12字符\\n
\\n优点 | 缺点 |
---|---|
✅ 降低耦合度:请求发送者无需知道处理细节 | ❌ 请求可能未被处理(需兜底逻辑) |
✅ 动态调整处理流程 | ❌ 长链影响性能 |
✅ 符合单一职责原则 | ❌ 调试难度增加 |
✅ 方便扩展新处理者 | ❌ 可能产生循环调用 |
1、控制链长度
\\n2、设置兜底处理
\\nclass DefaultHandler extends Handler {\\n @override\\n bool _canHandle(String request) => true; // 最后执行\\n\\n @override\\n void _doHandle(String request) {\\n print(\\"⚠️ 默认处理:$request\\");\\n }\\n}\\n
\\n3、性能优化
\\n// 缓存处理能力判断\\nfinal _cache = <String, bool>{};\\n\\n@override\\nbool _canHandle(String request) => \\n _cache.putIfAbsent(request, () => _calculateCanHandle(request));\\n
\\n通过深入理解责任链模式,我们可以更好地设计可扩展的中间件系统。关键在于:
\\n该模式在网络框架
、工作流引擎
、事件处理系统
中广泛应用,是构建灵活系统架构的重要工具之一。
\\n","description":"前言 在软件系统的复杂交互中,请求的处理往往需要跨越多个层级或模块。责任链模式应运而生,它通过将处理对象串联为一条\\"逻辑流水线\\",让每个节点专注单一职责,实现请求的自动传递与动态分配。\\n\\n这种模式如同精密传送带:每个工位(处理者)自主判断能否处理任务,或将其递交给下一环节,既避免了发送者与接收者的强耦合,又赋予系统运行时灵活调整链路的能力。\\n\\n从网络拦截器的双向过滤到多级审批流程的智能路由,责任链以优雅的链式结构,在权限控制、日志处理、异常兜底等场景中,为复杂逻辑提供了高扩展、低侵入的解决方案,堪称分布式协作的典范设计。\\n\\n操千曲而后晓声,观千剑而后识器。虐它…","guid":"https://juejin.cn/post/7488321730765815818","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-02T08:47:51.959Z","media":null,"categories":["Android","Dart","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 教程(八)数据存储","url":"https://juejin.cn/post/7488256628933378099","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
在 Flutter 中,我们可以使用 shared_preferences 来实现轻量级的数据存储。需要注意:shared_preferences在Android设备上是基于SharedPreferences开发的,而运行在iOS设备上是基于NSUserDefaults开发的。
\\n首先,我们先添加依赖:
\\ndependencies:\\n shared_preferences: ^2.5.3\\n
\\n然后在代码中引入对应的包,代码如下所示:
\\nimport \'package:shared_preferences/shared_preferences.dart\';\\n
\\nSharedPreferences sharedPreferences=await SharedPreferences.getInstance();\\nsharedPreferences.setString(\\"username\\", username);\\nsharedPreferences.setString(\\"password\\", password);\\n
\\nSharedPreferences sharedPreferences=await SharedPreferences.getInstance();\\nsharedPreferences.get(keyString);\\n
\\nSharedPreferences sharedPreferences=await SharedPreferences.getInstance();\\nsharedPreferences.remove(keyString);\\n
\\nSharedPreferences sharedPreferences=await SharedPreferences.getInstance();\\nsharedPreferences.clear();\\n
\\n在 Flutter 中,如果我们想要操作App目录或者SD文件夹中的文件,则需要使用 path_provider 库。我们都知道在Android和iOS下,临时目录、文档目录都是不同的。而 path_provider 库提供了统一的接口,能够获取手机上的常用目录,如临时目录、文档目录等,从而方便程序存储和访问指定目录下的文件。
\\n首先我们先添加依赖
\\ndependencies:\\n path_provider: ^2.1.5\\n
\\n然后在代码文件中 import 对应的包
\\nimport \'package:path_provider/path_provider.dart\';\\n
\\n_getTempFileDir() async{\\n Directory tempDir=await getTemporaryDirectory();\\n String tempPath=tempDir.path;\\n}\\n
\\n_getSDFileDir() async{\\n Directory sdFileDir=await getExternalStorageDirectory();\\n String sdFileDirPath=sdFileDir.path;\\n}\\n
\\n\\n\\n注意:在iOS中,没有外部存储目录的概念,也就是没有SD卡外部存储目录。所以在实际的开发中,读者需要先判断系统,再做出适当的更改。
\\n
SQLite是一款轻量级的数据库,是手机最常用的一种数据存储方式。在Flutter项目中,如果需要对数据进行大批量的增删改查的操作,就会用到SQLite数据库。Flutter专门提供了操作SQLite数据库的sqflite库。
\\n首先我们添加依赖:
\\ndependencies:\\n sqflite: ^2.4.2\\n
\\n然后在代码文件中 import 对应的包
\\nimport \'package:sqflite/sqflite.dart\';\\n
\\n_getDatabasePath() async{\\n var databasePath=await getDatabasesPath();\\n String path=join(databasePath,\'demo.db\');\\n print(path);\\n}\\n
\\n_openDatabaseAndCreateTable()async{\\n var databasePath=await getDatabasesPath();\\n String path=join(databasePath,\'demo.db\');\\n Database database=await openDatabase(\\n path,\\n version: 1,\\n onCreate: (Database db,int version)async{\\n await db.execute(\\n \'CREATE TABLE User(id INTEGER PRIMARY KEY,name TEXT,age INTEGER)\',\\n );\\n }\\n );\\n}\\n
\\n// 使用 rawInsert 插入\\n_rawInsertData() async{\\n var databasesPath=await getDatabasesPath();\\n String path=join(databasesPath,\'demo.db\');\\n Database database=await openDatabase(path,version:1,);\\n database.transaction((txn) async{\\n int id1 = await txn.rawInsert(\\n \'INSERT INTO User(name, age) VALUES(?, ?)\',[\'Liyuanjing\',27]);\\n print(\'inserted1: $id1\');\\n });\\n}\\n// 使用 insert 插入\\n_insertData() async{\\n var databasesPath=await getDatabasesPath();\\n String path=join(databasesPath,\'demo.db\');\\n Database database=await openDatabase(path,version:1,);\\n Map<String,dynamic> values={\\n \'name\':\'zhangsan\',\\n \'age\': 24,\\n };\\n await database.insert(\'User\', values);\\n}\\n
\\n和上面的查找操作类似,更新操作也有两种方法:rawUpdate 和 update。代码示例如下:
\\n_rawUpdateData() async{\\n var databasesPath=await getDatabasesPath();\\n String path=join(databasesPath,\'demo.db\');\\n Database database=await openDatabase(path,version:1,);\\n database.transaction((txn) async{\\n int id1 = await txn.rawUpdate(\\n \'UPDATE User SET name=? WHERE age=?\',[\\"Batman\\",27]);\\n print(\'inserted1: $id1\');\\n });\\n}\\n\\n_updateData() async{\\n var databasesPath=await getDatabasesPath();\\n String path=join(databasesPath,\'demo.db\');\\n Database database=await openDatabase(path,version:1,);\\n database.transaction((txn) async{\\n Map<String,dynamic> values={\\n \'name\':\'SpiderMan\',\\n };\\n int id1 = await txn.update(\'User\',values,where: \'age=?\',whereArgs: [27,]);\\n print(\'inserted1: $id1\');\\n });\\n}\\n
\\n_rawQueryData() async{\\n var databasesPath=await getDatabasesPath();\\n String path=join(databasesPath,\'demo.db\');\\n Database database=await openDatabase(path,version:1,);\\n database.transaction((txn) async{\\n List<Map<String,dynamic>> list = await txn.rawQuery(\\n \'SELECT * FROM User WHERE age=?\',[27]);\\n print(list);\\n });\\n}\\n\\n_queryData() async{\\n var databasesPath=await getDatabasesPath();\\n String path=join(databasesPath,\'demo.db\');\\n Database database=await openDatabase(path,version:1,);\\n database.transaction((txn) async{\\n List<Map<String,dynamic>> list = await txn.query(\'User\',where: \'age=?\',whereArgs:\\n [27,]);\\n print(list);\\n });\\n}\\n
\\n_rawDeleteData()async{\\n var databasesPath=await getDatabasesPath();\\n String path=join(databasesPath,\'demo.db\');\\n Database database=await openDatabase(path,version:1,);\\n database.transaction((txn) async{\\n int id = await txn.rawDelete(\'DELETE FROM User WHERE age = ?\', [24,]);\\n print(\\"$id\\");\\n });\\n}\\n\\n_deleteData()async{\\n var databasesPath=await getDatabasesPath();\\n String path=join(databasesPath,\'demo.db\');\\n Database database=await openDatabase(path,version:1,);\\n database.transaction((txn) async{\\n int id = await txn.delete(\'User\',where: \'age=?\',whereArgs: [27,]);\\n print(\\"$id\\");\\n });\\n}\\n
\\n// 删除数据库\\n_deleteDatabase() async{\\n var databasePath=await getDatabasesPath();\\n String path=join(databasePath,\'demo.db\');\\n await deleteDatabase(path);\\n}\\n\\n// 关闭数据库\\n_closeDatabase() async{\\n await database.close();\\n}\\n
\\n最近我在想,使用二维的 Canvas 能否通过投影的方式,模拟三维的坐标系,这样就可以给渲染三维坐标,从而实现简单的伪 3D 效果。如下所示:
\\n绘制一个三维坐标系,通过三维的点绘制了两个平面, 并通过 Slider 交互可以让坐标系沿 z 轴旋转:
首先定义一下承载三维点数据的类 Point3D
,其中有 x,y,z 三个数值表示坐标的三个维度:
class Point3D {\\n final double x, y, z;\\n\\n Point3D(this.x, this.y, this.z);\\n\\n Point3D.zero() : this(0, 0, 0);\\n}\\n
\\n如下所示,定义 World3D
类继承自 CustomPainter,由于需要让坐标系沿 z 轴旋转,旋转的弧度数据由外界通过构造函数传入。现在关键的两步是:
class World3D extends CustomPainter {\\n\\n final double rotationZ;\\n\\n World3D({this.rotationZ = 0});\\n\\n Offset project(Point3D p) {\\n // TODO 将三维点映射为二维点\\n }\\n\\n @override\\n void paint(Canvas canvas, Size size) {\\n // TODO 绘制逻辑 \\n }\\n\\n\\n @override\\n bool shouldRepaint(World3D oldDelegate) => rotation != oldDelegate.rotation;\\n}\\n
\\nCanvas 绘制绘制的点是 Offset
对象,只有 x,y 两个维度,如下所示的白色坐标系:\\n想要绘制如下的三维空间,就是将三维点,通过运算转换成二维点,绘制在白色的坐标系上。
下面虽然产生了空间感,但本质上还是二维的线条,只不过按照三维的视觉规律进行投影。所以是一种伪 3d 的绘制方式:
\\n二维 x 轴和三维 x 的夹角是 angle
,通过如下的投影变换,就可以将 Point3D
转换为 Offset
:
Offset project(Point3D p) {\\n double scale = 40.0;\\n double angle = 30 / 180 * pi;\\n final x = (p.x - p.y) * cos(angle);\\n final y = (p.x + p.y) * sin(angle) - p.z;\\n return Offset(x * scale, y * scale);\\n}\\n
\\n有了三维点的映射关系,就可以很轻松地计算出,在当前三维空间中,某个点坐落在二维空间中的坐标。比如 Point3D(5, 0, 0)
表示 x 轴正方向 5 个点位点的位置,得到位置后,使用 canvas.drawLine
就可以画出原点到 (5, 0, 0)
的直线,也就是下面的红色 x 轴线,其他两个轴类似:
@override\\nvoid paint(Canvas canvas, Size size) {\\n Offset center = Offset(size.width / 2, size.height / 2);\\n canvas.translate(center.dx, center.dy);\\n // 绘制坐标系轴\\n _drawAxis(canvas, Colors.red, Point3D(5, 0, 0), \'X\'); // X轴\\n _drawAxis(canvas, Colors.green, Point3D(0, 5, 0), \'Y\'); // Y轴\\n _drawAxis(canvas, Colors.blue, Point3D(0, 0, 5), \'Z\'); // Z轴\\n}\\n\\nvoid _drawAxis(Canvas canvas, Color color, Point3D endPoint, String label) {\\n Paint paint = Paint()..color = color..strokeWidth = 2;\\n Offset start = project(Point3D.zero());\\n Offset end = project(endPoint);\\n canvas.drawLine(start, end, paint);\\n // 绘制轴标签\\n TextStyle style = TextStyle(color: color, fontSize: 16);\\n TextPainter(\\n text: TextSpan(text: label, style: style),\\n textDirection: TextDirection.ltr)\\n ..layout()\\n ..paint(canvas, end + const Offset(5, -10));\\n}\\n
\\n同理,只要给出三维坐标,就可以通过 project
映射出二维坐标,通过 Canvas 绘制在平面上。于是我们就完成了任意三维空间点的展示,你也可以自己尝试画一下点,
void _drawLines(Canvas canvas) {\\n List<(Point3D, Point3D)> bottomLines = [\\n (Point3D(0, 0, 0), Point3D(0, 1, 0)),\\n (Point3D(0, 0, 0), Point3D(1, 0, 0)),\\n (Point3D(0, 0, 0), Point3D(1, 1, 0)),\\n (Point3D(0, 1, 0), Point3D(1, 1, 0)),\\n (Point3D(1, 0, 0), Point3D(1, 1, 0)),\\n (Point3D(0, 1, 0), Point3D(1, 0, 0)),\\n ];\\n \\n Paint paint = Paint() ..color = Colors.white ..strokeWidth = 1;\\n \\n for (var line in bottomLines) {\\n Offset p0 = project(line.$1);\\n Offset p1 = project(line.$2);\\n canvas.drawLine(p0, p1, paint..color = Colors.white);\\n }\\n}\\n
\\n想要让坐标系根据 z 轴旋转,只要根据旋转角度值,运算 Point3D
在该角度时的在平面上的投影即可,代码如下所示,就可以非常简单地实现下面的旋转效果:
Offset project(Point3D p) {\\n double scale = 40.0; // 缩放系数\\n double angle = 30 / 180 * pi; // 30度弧度值(π/6)\\n // 绕Z轴旋转\\n final rx = p.x * cos(rotationZ) - p.y * sin(rotationZ);\\n final ry = p.x * sin(rotationZ) + p.y * cos(rotationZ);\\n // 等轴测投影\\n final xProj = (rx - ry) * cos(angle);\\n final yProj = (rx + ry) * sin(angle) - p.z;\\n return Offset(xProj * scale, yProj * scale);\\n}\\n
\\n我没想到,绘制一个伪 3 D 的空间这么简单,其中最重要的 project 方法是 AI 帮忙处理的。下一篇我将会详细分析一下这个投影的映射逻辑,敬请期待 ~
\\n更多文章和视频知识资讯,大家可以关注我的公众号、掘金和 B 站 。
\\n","description":"最近我在想,使用二维的 Canvas 能否通过投影的方式,模拟三维的坐标系,这样就可以给渲染三维坐标,从而实现简单的伪 3D 效果。如下所示: 绘制一个三维坐标系,通过三维的点绘制了两个平面, 并通过 Slider 交互可以让坐标系沿 z 轴旋转:\\n\\n1. 定义数据和画板\\n\\n首先定义一下承载三维点数据的类 Point3D,其中有 x,y,z 三个数值表示坐标的三个维度:\\n\\nclass Point3D {\\n final double x, y, z;\\n\\n Point3D(this.x, this.y, this.z);\\n\\n Point3D.zero()…","guid":"https://juejin.cn/post/7488176279923441704","author":"张风捷特烈","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-02T01:55:22.628Z","media":[{"url":"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9d078ff2e88041fe80c42a5321826e18~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=817&h=450&s=767681&e=gif&f=120&b=151524","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/96c8b737df70488686bf5a4a56822b88~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1128&h=525&s=146288&e=png&b=11111d","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/55cb79f3d4504727af9cbd32ba2857b0~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1064&h=501&s=127229&e=png&b=11111d","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/54e5b68ca00d4641b39a4d4446962445~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1026&h=493&s=123078&e=png&b=11111d","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/55cb79f3d4504727af9cbd32ba2857b0~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1064&h=501&s=127229&e=png&b=11111d","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9d078ff2e88041fe80c42a5321826e18~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=817&h=450&s=767681&e=gif&f=120&b=151524","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 教程(七)网络请求","url":"https://juejin.cn/post/7488186651379777563","content":"HttpClient 是 Dart 中自带的 HTTP 库。不过 HttpClient 在 Flutter 中用的比较少,这里只简单介绍一下它的用法,代码示例如下:
\\n//创建一个HttpClient\\nHttpClient httpClient = HttpClient();\\n//打开Http连接\\nHttpClientRequest request = await httpClient.getUrl(\\nUri.parse(\\"https://www.baidu.com\\"),\\n);\\n//使用iPhone的UA\\nrequest.headers.add(\\n\\"user-agent\\",\\n\\"Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1\\",\\n);\\n//等待连接服务器(会将请求信息发送给服务器)\\nHttpClientResponse response = await request.close();\\n//读取响应内容\\n_text = await response.transform(utf8.decoder).join();\\n//输出响应头\\nprint(response.headers);\\n\\n//关闭client后,通过该client发起的所有请求都会终止。\\nhttpClient.close();\\n
\\nhttp 库是 Flutter 官方推荐使用的。首先我们需要引入 http 库。如下所示,在 pubspec.yaml 文件中声明依赖。
\\ndependencies:\\n http: ^0.12.1\\n
\\n然后在代码中导入对应的包名,如下所示:
\\nimport \'package:http/http.dart\';\\n
\\nhttp 的使用示例如下所示:
\\nvar client = http.Client();\\nvar uri = Uri.parse(\'https://www.ptpress.com.cn/\');\\nMap<String, String> header = {\\n \'User-Agent\': \'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:70.0)\\n Gecko / 20100101 Firefox / 70.0\',\\n};\\nhttp.Response response = await client.get(uri, headers: header);\\nprint(utf8.decode(response.bodyBytes));\\nclient.close();\\n
\\nvar client = http.Client();\\nMap<String, String> bodyMap = {\\n \'username\': \'username\',\\n \'password\': \'password\'\\n};\\nMap<String, String> headers = {\\n \\"User-Agent\\": \\"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:70.0) \\n Gecko / 20100101 Firefox / 70.0\\",\\n};\\nhttp.Response response = await\\nclient.post(\'https://www.ptpress.com.cn/\',\\n headers: headers, body: bodyMap);\\nprint(utf8.decode(response.bodyBytes));\\nclient.close();\\n
\\n我们可以在 Android studio 中下载 JsonToDart
插件,通过这个插件我们可以把 Json 转化为 Dart 类。如下图所示:
生成对应的文件后,我们就可以使用生成的 Person.fromJson
来生成对应的 model 对象了。代码示例如下:
import \'dart:convert\';\\nimport \'package:http/http.dart\' as http;\\n\\nFuture<Person> fetchPerson() async {\\n final response = await http.get(Uri.parse(\'https://api.example.com/user/1\'));\\n \\n if (response.statusCode == 200) {\\n return Person.fromJson(jsonDecode(response.body));\\n } else {\\n throw Exception(\'Failed to load user\');\\n }\\n}\\n
\\ndio库是Flutter中常用的三方HTTP库。要使用 dio 库,需要添加如下依赖:
\\ndependencies:\\n dio: ^3.0.9\\n
\\ntry{\\n Dio dio=new Dio();\\n dio.options.headers={\\n \\"User-Agent\\":\\"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:70.0) Gecko/20100101\\n Firefox/70.0\\"\\n };\\n Map<String,dynamic> queryParameters={\\n \'id\':\'123456\',\\n \'book\':\'flutter\'\\n };\\n Response response=await dio.get(\\"https://www.ptpress.com.cn/\\",queryParameters: \\n queryParameters);\\n print(response);\\n}catch(e){\\n print(e);\\n}\\n
\\n_dio_post() async{\\n try{\\n Dio dio=new Dio();\\n dio.options.headers={\\n \\"User-Agent\\":\\"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:70.0) Gecko/20100101\\n Firefox/70.0\\"\\n };\\n Map<String,dynamic> data={\\n \'username\':\'username\',\\n \'password\':\'password\'\\n };\\n Response response=await dio.post(\\"https://www.ptpress.com.cn/\\",data: data);\\n print(response);\\n }catch(e){\\n print(e);\\n }\\n}\\n
\\n_dio_download() async{\\n var sdDir = await getExternalStorageDirectory();\\n Response response=await \\n Dio().download(\'https://cdn.ptpress.cn/pubcloud/null/\\n cover/2021051442E8BF9.jpg\', sdDir.path+\'/image.jpg\');\\n}\\n
\\n_dio_formData_uploadFile(File image1, File file2, File file3) async {\\n Dio dio = new Dio();\\n\\n String path1 = image1.path;\\n var name = path1.substring(path1.lastIndexOf(\\"/\\") + 1, path1.length);\\n\\n String path2 = file2.path;\\n var name2 = path1.substring(path2.lastIndexOf(\\"/\\") + 1, path2.length);\\n\\n String path3 = file3.path;\\n var name3 = path1.substring(path3.lastIndexOf(\\"/\\") + 1, path3.length);\\n Map<String, dynamic> fileMap = {\\n \\"image1\\": await MultipartFile.fromFile(path1, filename: name),\\n // 支持文件数组上传\\n \\"files\\": [\\n await MultipartFile.fromFile(path2, filename: name2),\\n await MultipartFile.fromFile(path3, filename: name3),\\n ],\\n };\\n FormData formData = new FormData.fromMap(fileMap);\\n Response response = await dio.post(\\"http://127.0.0.1/info\\", data: formData);\\n}\\n
\\nFuture downloadWithChunks(\\n url,\\n savePath, {\\n ProgressCallback onReceiveProgress,\\n}) async {\\n const firstChunkSize = 102;\\n const maxChunk = 3;\\n\\n int total = 0;\\n var dio = Dio();\\n var progress = <int>[];\\n\\n createCallback(no) {\\n return (int received, _) {\\n progress[no] = received;\\n if (onReceiveProgress != null && total != 0) {\\n onReceiveProgress(progress.reduce((a, b) => a + b), total);\\n }\\n };\\n }\\n\\n Future<Response> downloadChunk(url, start, end, no) async {\\n progress.add(0);\\n --end;\\n return dio.download(\\n url,\\n savePath + \\"temp$no\\",\\n onReceiveProgress: createCallback(no),\\n options: Options(\\n headers: {\\"range\\": \\"bytes=$start-$end\\"},\\n ),\\n );\\n }\\n\\n Future mergeTempFiles(chunk) async {\\n File f = File(savePath + \\"temp0\\");\\n IOSink ioSink= f.openWrite(mode: FileMode.writeOnlyAppend);\\n for (int i = 1; i < chunk; ++i) {\\n File _f = File(savePath + \\"temp$i\\");\\n await ioSink.addStream(_f.openRead());\\n await _f.delete();\\n }\\n await ioSink.close();\\n await f.rename(savePath);\\n }\\n\\n Response response = await downloadChunk(url, 0, firstChunkSize, 0);\\n if (response.statusCode == 206) {\\n total = int.parse(\\n response.headers.value(HttpHeaders.contentRangeHeader).split(\\"/\\").last);\\n int reserved = total -\\n int.parse(response.headers.value(HttpHeaders.contentLengthHeader));\\n int chunk = (reserved / firstChunkSize).ceil() + 1;\\n if (chunk > 1) {\\n int chunkSize = firstChunkSize;\\n if (chunk > maxChunk + 1) {\\n chunk = maxChunk + 1;\\n chunkSize = (reserved / maxChunk).ceil();\\n }\\n var futures = <Future>[];\\n for (int i = 0; i < maxChunk; ++i) {\\n int start = firstChunkSize + i * chunkSize;\\n futures.add(downloadChunk(url, start, start + chunkSize, i + 1));\\n }\\n await Future.wait(futures);\\n }\\n await mergeTempFiles(chunk);\\n }\\n}\\n
\\n在移动应用开发中,网络请求如同应用的\\"生命线\\"
,但Flutter
原生HttpClient
的简陋和http
库的局限性,让我们常陷入重复造轮子
的困境。
文件上传下载
、多级拦截器
、全局配置
时,是否还在手动拼接URL
参数?认证体系
、日志监控
需求时,是否还在用print
语句调试网络请求?Dio
作为Dart
生态中最强大的网络请求库,以其高度可扩展的架构设计和丰富的功能,正在重塑异步编程范式。
本文将带你穿透API
表层,系统构建Dio
的核心知识体系,解锁企业级应用的开发密码。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\n\\n\\n\\n
dio
是一个强大的HTTP
网络请求库,支持全局配置
、Restful API
、FormData
、拦截器
、请求取消
、Cookie 管理
、文件上传/下载
、超时
、自定义适配器
、转换器
等。提供完整的
\\n网络请求生命周期管理能力
。其本质是通过封装底层HTTP
协议交互,构建可插拔的请求处理管道
。
企业级开发标准
核心特性:
\\nBaseOptions
(全局配置):通过统一管理基础URL
、超时阈值
、请求头
等参数,避免重复配置。final dio = Dio(BaseOptions(\\n baseUrl: \\"https://api.example.com/v2\\",\\n connectTimeout: Duration(seconds: 8),\\n headers: {\\"Content-Type\\": \\"application/json\\"}\\n));\\n
\\nInterceptors
(拦截器链):支持请求/响应/错误
三级拦截,可实现统一身份认证
、请求重试
等逻辑。CancelToken
(请求取消):精准控制请求生命周期,防止内存泄漏
和无效资源
占用。典型场景:
\\n当应用需要对接多个微服务时,可通过全局配置快速切换不同环境:
// 开发环境配置\\ndio.options.baseUrl = \\"http://dev.api.example.com\\";\\n\\n// 生产环境配置(通过环境变量动态切换)\\nif (isProduction) {\\n dio.options = _loadProdConfig();\\n}\\n
\\n对比原生HttpClient
:
\\n原生库需手动拼接URL
、重复设置Header
,Dio
通过全局配置降低代码冗余度达60%
以上。
模块化设计哲学
拦截器机制解析:
\\nDio
的拦截器采用责任链模式,允许以插件形式扩展功能:
dio.interceptors.addAll([\\n // 日志记录\\n LogInterceptor(\\n requestBody: true,\\n responseBody: true\\n ),\\n \\n // Token自动刷新\\n QueuedInterceptorsWrapper(\\n onError: (error, handler) {\\n if (error.response?.statusCode == 401) {\\n _refreshToken().then((newToken) {\\n // 更新请求头并重新发起请求\\n error.requestOptions.headers[\\"Authorization\\"] = newToken;\\n handler.resolve(dio.fetch(error.requestOptions));\\n });\\n }\\n }\\n )\\n]);\\n
\\n解耦优势:
\\nJWT
过期问题,业务代码零侵入。请求耗时
、成功率
等指标。请求级缓存策略
。覆盖全场景需求
关键协议支持:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n协议能力 | 实现方式 | 代码示例 |
---|---|---|
文件上传 | FormData 封装 | dio.post(\\"/upload\\", data: FormData.fromMap({\\"file\\": MultipartFile(...)})) |
分块下载 | download 方法+进度回调 | dio.download(url, savePath, onReceiveProgress: (count, total) {...}) |
数据转换 | Transformer 定制解析逻辑 | dio.transformer = MyCustomTransformer() |
技术细节:
\\nStream
实现流式上传,避免内存溢出。savePath
参数自动管理)。HTTP/2
、WebSocket
等协议适配器。超越原生的秘诀
底层优化策略:
\\nTCP
连接,减少三次握手开销。httpClientAdapter
配置最大连接数。(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) {\\n client.maxConnectionsPerHost = 10; // 控制并发量\\n};\\n
\\nCancelToken
实现关键请求优先处理。Dio
的价值不仅在于提供网络请求能力,更在于将网络层抽象为可维护、可观测、可扩展的系统工程:
APM
(应用性能监控)集成。这些特性使得Dio
成为中大型Flutter
项目的必然选择,而非简单的工具库。当你的应用日活超过10
万,或需要处理跨国网络抖动时,Dio
的设计哲学将展现出真正的威力。
步骤1:添加依赖:
\\n# pubspec.yaml\\ndependencies:\\n dio: ^5.8.0+1 # 使用最新稳定版\\n
\\n步骤2:创建Dio
实例:
import \'package:dio/dio.dart\';\\n\\n// 全局单例(推荐)\\nfinal dio = Dio(BaseOptions(\\n baseUrl: \\"https://api.example.com/api/v1\\",\\n connectTimeout: Duration(seconds: 10),\\n headers: {\\"Accept\\": \\"application/json\\"}\\n));\\n
\\nGET
请求:
// 简单GET\\nfinal response = await dio.get(\\"/user/123\\");\\n\\n// 带查询参数\\nfinal response = await dio.get(\\"/search\\", queryParameters: {\\n \\"keyword\\": \\"flutter\\",\\n \\"page\\": 1,\\n \\"sort\\": \\"desc\\"\\n});\\n\\n// 处理响应\\nif (response.statusCode == 200) {\\n final data = response.data; // 自动解析JSON为Map/List\\n print(\\"用户数据:$data\\");\\n}\\n
\\nPOST
请求:
// 提交JSON数据\\nfinal response = await dio.post(\\n \\"/users\\",\\n data: {\\n \\"name\\": \\"Flutter开发者\\",\\n \\"email\\": \\"dev@example.com\\"\\n },\\n options: Options(headers: {\\"X-Request-ID\\": \\"uuid\\"}),\\n);\\n\\n// 提交Form表单\\nawait dio.post(\\n \\"/login\\",\\n data: FormData.fromMap({\\n \\"username\\": \\"admin\\",\\n \\"password\\": \\"securePassword123\\"\\n }),\\n);\\n
\\n文件上传:
\\n// 单文件上传\\nfinal formData = FormData.fromMap({\\n \\"file\\": await MultipartFile.fromFile(\\n \\"/path/to/file.jpg\\",\\n filename: \\"avatar.jpg\\",\\n ),\\n \\"description\\": \\"用户头像\\",\\n});\\n\\nfinal uploadResponse = await dio.post(\\n \\"/upload\\",\\n data: formData,\\n onSendProgress: (sentBytes, totalBytes) {\\n print(\\"上传进度:${(sentBytes / totalBytes * 100).toStringAsFixed(1)}%\\");\\n },\\n);\\n
\\n文件下载:
\\n// 下载到指定路径\\nawait dio.download(\\n \\"https://example.com/largefile.zip\\",\\n \\"/storage/emulated/0/Download/file.zip\\",\\n onReceiveProgress: (receivedBytes, totalBytes) {\\n print(\\"下载进度:${(receivedBytes / totalBytes * 100).toStringAsFixed(1)}%\\");\\n },\\n deleteOnError: true, // 下载失败时删除文件\\n);\\n
\\n日志拦截器:
\\ndio.interceptors.add(LogInterceptor(\\n request: true, // 打印请求信息\\n requestBody: true, // 显示请求体\\n responseBody: true, // 显示响应体\\n error: true, // 显示错误详情\\n));\\n
\\n认证拦截器:
\\ndio.interceptors.add(InterceptorsWrapper(\\n onRequest: (options, handler) async {\\n // 自动添加Token\\n final token = await _getCachedToken();\\n options.headers[\\"Authorization\\"] = \\"Bearer $token\\";\\n handler.next(options);\\n },\\n onError: (error, handler) async {\\n // Token过期自动刷新\\n if (error.response?.statusCode == 401) {\\n final newToken = await _refreshToken();\\n error.requestOptions.headers[\\"Authorization\\"] = \\"Bearer $newToken\\";\\n handler.resolve(await dio.fetch(error.requestOptions));\\n } else {\\n handler.next(error);\\n }\\n },\\n));\\n
\\n捕获Dio
异常:
try {\\n await dio.get(\\"/protected-resource\\");\\n} on DioException catch (e) {\\n // 分类处理错误类型\\n switch (e.type) {\\n case DioExceptionType.connectionTimeout:\\n print(\\"连接超时\\");\\n case DioExceptionType.badResponse:\\n print(\\"服务器错误:${e.response?.statusCode}\\");\\n case DioExceptionType.cancel:\\n print(\\"请求被取消\\");\\n default:\\n print(\\"未知错误:${e.message}\\");\\n }\\n}\\n
\\n自定义错误处理:
\\nFuture<T> safeApiCall<T>(Future<Response> Function() request) async {\\n try {\\n final response = await request();\\n return response.data as T;\\n } on DioException catch (e) {\\n throw _parseError(e);\\n }\\n}\\n\\n// 统一错误解析\\nApiError _parseError(DioException e) {\\n if (e.response?.data is Map) {\\n final code = e.response?.data[\\"errorCode\\"] ?? \\"unknown\\";\\n final message = e.response?.data[\\"message\\"] ?? \\"未知错误\\";\\n return ApiError(code: code, message: message);\\n }\\n return ApiError(code: \\"NETWORK_ERROR\\", message: e.message ?? \\"网络异常\\");\\n}\\n
\\n取消令牌使用:
\\n// 在Widget的dispose方法中取消\\nclass _MyPageState extends State<MyPage> {\\n final CancelToken _cancelToken = CancelToken();\\n\\n @override\\n void dispose() {\\n _cancelToken.cancel(\\"页面销毁\\");\\n super.dispose();\\n }\\n\\n Future<void> fetchData() async {\\n await dio.get(\\n \\"/long-running-request\\",\\n cancelToken: _cancelToken,\\n );\\n }\\n}\\n\\n// 手动取消\\nvoid cancelRequest() {\\n _cancelToken.cancel(\\"用户主动取消\\");\\n}\\n
\\n1、配置管理:使用BaseOptions
集中管理不同环境(开发/生产
)的配置。
// config.dart\\nabstract class EnvConfig {\\n static final dev = BaseOptions(baseUrl: \\"http://dev.api.example.com\\");\\n static final prod = BaseOptions(baseUrl: \\"https://api.example.com\\");\\n}\\n
\\n2、响应模型化:使用json_serializable
自动转换响应数据为Model类。
@JsonSerializable()\\nclass User {\\n final String id;\\n final String name;\\n \\n factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);\\n}\\n
\\n3、统一错误处理:通过拦截器或高阶函数统一封装错误处理逻辑。
\\n4、性能监控:在拦截器中记录请求耗时
、成功率
等指标。
dio.interceptors.add(InterceptorsWrapper(\\n onRequest: (options, handler) {\\n final startTime = DateTime.now().millisecondsSinceEpoch;\\n options.extra[\\"startTime\\"] = startTime;\\n handler.next(options);\\n },\\n onResponse: (response, handler) {\\n final duration = DateTime.now().millisecondsSinceEpoch - \\n response.requestOptions.extra[\\"startTime\\"];\\n _monitor.recordSuccess(duration);\\n handler.next(response);\\n },\\n));\\n
\\nQ1: 如何防止重复请求?
\\n// 使用请求锁\\nbool _isFetching = false;\\n\\nFuture<void> fetchData() async {\\n if (_isFetching) return;\\n _isFetching = true;\\n try {\\n await dio.get(\\"/data\\");\\n } finally {\\n _isFetching = false;\\n }\\n}\\n\\n// 或使用CancelToken取消前序请求\\nCancelToken? _lastToken;\\n\\nFuture<void> fetchData() async {\\n _lastToken?.cancel();\\n _lastToken = CancelToken();\\n await dio.get(\\"/data\\", cancelToken: _lastToken);\\n}\\n
\\nQ2: 如何处理非JSON
响应?
// 修改响应解析方式\\nfinal response = await dio.get(\\n \\"/plain-text\\",\\n options: Options(responseType: ResponseType.plain),\\n);\\nprint(response.data); // 直接获取字符串\\n\\n// 或自定义Transformer\\ndio.transformer = _CustomTransformer();\\n
\\n库名称 | 原生库 (dart:io /http ) | Dio | http (官方包) | Chopper | Retrofit |
---|---|---|---|---|---|
功能特点 | 基础HTTP 请求 | 拦截器、全局配置 | 轻量级,简单封装 | 基于代码生成的REST 客户端 | 基于Dio 的代码生成 |
支持GET/POST 等基本方法 | FormData 文件上传/下载 | 支持常见HTTP 方法 | 支持拦截器、转换器 | 自动生成API接口类 | |
手动处理JSON 解析 | 请求取消、超时配置 | 需手动处理JSON 解析 | 强类型API 定义 | 依赖Dio 或http 作为底层 | |
优点 | 无需额外依赖 | 功能全面,扩展性强 | 官方维护,轻量稳定 | 类型安全,减少模板代码 | 高度抽象,减少重复代码 |
适合简单请求场景 | 拦截器方便统一处理日志/错误 | 适合快速开发简单API | 支持多种数据转换格式 | 与Dio 深度集成,功能强大 | |
缺点 | 代码冗余,需手动封装 | 学习成本稍高 | 功能有限,需自行扩展 | 依赖代码生成,配置复杂 | 需结合代码生成,灵活性低 |
不支持高级功能(如拦截器) | 体积略大 | 缺少拦截器等高级功能 | 文档较少,社区较小 | 对复杂请求支持有限 | |
性能 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ |
适用场景 | 简单请求、小型项目 | 中大型项目、需要高级功能 | 轻量级应用、快速原型 | 需要强类型API 接口的项目 | 需要高度抽象的REST API 项目 |
维护状态 | 官方维护,更新稳定 | 社区活跃,更新频繁 | 官方维护,更新稳定 | 社区维护,更新较慢 | 社区维护,依赖Dio 更新 |
import \'package:dio/dio.dart\';\\nimport \'package:flutter_demo/http/http_res.dart\';\\n\\nclass ApiService {\\n /// static 声明为类级变量,与类本身绑定而非实例\\n /// final 确保实例不可被重新赋值\\n /// 延迟初始化:静态变量具有懒加载特性,只有在首次访问时才会创建变量\\n /// 内初管理:整个应用声明周期只存在一个实例\\n static final ApiService _instance = ApiService._internal();\\n\\n /// 不总是创建新实例\\n /// 返回已存在实例\\n /// 控制实例化的核心机制\\n factory ApiService() => _instance;\\n\\n late final Dio _dio;\\n\\n ApiService._internal() {\\n // 初始化Dio配置\\n _dio = Dio(\\n BaseOptions(\\n baseUrl: \\"https://apis.tianapi.com\\",\\n connectTimeout: const Duration(seconds: 30),\\n receiveTimeout: const Duration(seconds: 15),\\n headers: {\\"Accept\\": \\"application/json\\"},\\n ),\\n );\\n }\\n\\n // GET请求\\n Future<HttpRes> get(String url,\\n {Map<String, dynamic>? queryParameters}) async {\\n try {\\n final response = await _dio.get(\\n url,\\n queryParameters: queryParameters,\\n );\\n return HttpRes.fromJson(response.data);\\n } on DioException catch (e) {\\n throw _handleError(e);\\n }\\n }\\n\\n // POST请求\\n Future<HttpRes> post(String url, {dynamic data}) async {\\n try {\\n final response = await _dio.post(\\n url,\\n data: {\\n \\"key\\": \\"0e241b716c61db756ab5e29eb28a92c0\\",\\n },\\n options: Options(contentType: Headers.formUrlEncodedContentType),\\n );\\n return HttpRes.fromJson(response.data);\\n } on DioException catch (e) {\\n throw _handleError(e);\\n }\\n }\\n\\n // 统一错误处理\\n Exception _handleError(DioException e) {\\n String message = \'请求发生错误\';\\n if (e.type == DioExceptionType.connectionTimeout) {\\n message = \'连接超时\';\\n } else if (e.type == DioExceptionType.receiveTimeout) {\\n message = \'接收超时\';\\n } else if (e.response?.data != null) {\\n message = e.response!.data[\'message\'] ?? message;\\n }\\n return Exception(message);\\n }\\n}\\n
\\n场景需求:实现列表分页加载,支持自动重试
、并发控制
、页面保序
。
import \'package:flutter/material.dart\';\\nimport \'dart:async\';\\n\\nimport \'../http/api_service.dart\';\\nimport \'../http/http_res.dart\';\\n\\n/// 分页演示页面\\nclass PaginationDemo extends StatefulWidget {\\n const PaginationDemo({super.key});\\n\\n @override\\n State<PaginationDemo> createState() => _PaginationDemoState();\\n}\\n\\nclass _PaginationDemoState extends State<PaginationDemo> {\\n final ScrollController _scrollController = ScrollController();\\n int _currentPage = 1;\\n bool _isLoading = false;\\n bool _hasError = false;\\n bool _hasMore = true;\\n final _api = ApiService();\\n final List<Drug> _items = [];\\n\\n @override\\n void initState() {\\n super.initState();\\n _loadFirstPage();\\n _scrollController.addListener(_scrollListener);\\n }\\n\\n @override\\n void dispose() {\\n _scrollController.dispose();\\n super.dispose();\\n }\\n\\n /// 滚动监听\\n void _scrollListener() {\\n if (_scrollController.position.pixels >\\n _scrollController.position.maxScrollExtent - 200) {\\n _loadMore();\\n }\\n }\\n\\n /// 加载第一页\\n Future<void> _loadFirstPage() async {\\n setState(() => _isLoading = true);\\n _api.get(\\"/bcgm/index\\", queryParameters: {\\n \\"key\\": \\"0e241b716c61db756ab5e29eb28a92c0\\",\\n \\"num\\": 10,\\n \\"page\\": 1,\\n }).then((value) {\\n final drugs = value.result?.list ?? [];\\n for (int i = 0; i < drugs.length; i++) {\\n _items.add(drugs[i]);\\n }\\n setState(() {\\n _currentPage = 2;\\n _hasMore = _items.isNotEmpty;\\n });\\n }).catchError((e) {\\n setState(() => _hasError = true);\\n }).whenComplete(() {\\n setState(() => _isLoading = false);\\n });\\n }\\n\\n /// 加载更多\\n Future<void> _loadMore() async {\\n if (!_hasMore || _isLoading || _hasError) return;\\n\\n setState(() {\\n _isLoading = true;\\n _hasError = false;\\n });\\n\\n _api.get(\\"/bcgm/index\\", queryParameters: {\\n \\"key\\": \\"0e241b716c61db756ab5e29eb28a92c0\\",\\n \\"num\\": 10,\\n \\"page\\": _currentPage,\\n }).then((value) {\\n final drugs = value.result?.list ?? [];\\n for (int i = 0; i < drugs.length; i++) {\\n _items.add(drugs[i]);\\n }\\n setState(() {\\n if (_items.isEmpty) {\\n _hasMore = false;\\n } else {\\n _currentPage++;\\n }\\n });\\n }).catchError((e) {\\n setState(() => _hasError = true);\\n }).whenComplete(() {\\n setState(() => _isLoading = false);\\n });\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: const Text(\'智能分页加载示例\'),\\n ),\\n body: RefreshIndicator(\\n onRefresh: _loadFirstPage,\\n child: ListView.builder(\\n controller: _scrollController,\\n itemCount: _items.length + 1,\\n itemBuilder: (context, index) {\\n if (index < _items.length) {\\n return _ListItem(item: _items[index]);\\n }\\n return _buildFooter();\\n },\\n ),\\n ),\\n );\\n }\\n\\n /// 底部状态显示\\n Widget _buildFooter() {\\n if (_hasError) {\\n return _ErrorRetry(\\n onRetry: _loadMore,\\n );\\n }\\n if (_isLoading) {\\n return const _LoadingIndicator();\\n }\\n if (!_hasMore) {\\n return const _NoMoreItems();\\n }\\n return Container();\\n }\\n}\\n\\n/// 列表项组件\\nclass _ListItem extends StatelessWidget {\\n final Drug item;\\n\\n const _ListItem({required this.item});\\n\\n @override\\n Widget build(BuildContext context) {\\n return Container(\\n height: 80,\\n margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),\\n decoration: BoxDecoration(\\n borderRadius: BorderRadius.circular(8),\\n ),\\n child: ListTile(\\n title: Text(item.name ?? \\"\\"),\\n subtitle: Text(\\n item.content ?? \\"\\",\\n maxLines: 1,\\n overflow: TextOverflow.ellipsis,\\n ),\\n trailing: const Icon(Icons.chevron_right),\\n ),\\n );\\n }\\n}\\n\\n/// 加载指示器\\nclass _LoadingIndicator extends StatelessWidget {\\n const _LoadingIndicator();\\n\\n @override\\n Widget build(BuildContext context) {\\n return const Padding(\\n padding: EdgeInsets.all(16.0),\\n child: Center(\\n child: CircularProgressIndicator(),\\n ),\\n );\\n }\\n}\\n\\n/// 错误重试组件\\nclass _ErrorRetry extends StatelessWidget {\\n final VoidCallback onRetry;\\n\\n const _ErrorRetry({required this.onRetry});\\n\\n @override\\n Widget build(BuildContext context) {\\n return Padding(\\n padding: const EdgeInsets.all(16.0),\\n child: Column(\\n children: [\\n const Text(\'加载失败,请重试\', style: TextStyle(color: Colors.red)),\\n const SizedBox(height: 8),\\n ElevatedButton(\\n onPressed: onRetry,\\n child: const Text(\'重试\'),\\n ),\\n ],\\n ),\\n );\\n }\\n}\\n\\n/// 没有更多数据\\nclass _NoMoreItems extends StatelessWidget {\\n const _NoMoreItems();\\n\\n @override\\n Widget build(BuildContext context) {\\n return const Padding(\\n padding: EdgeInsets.all(16.0),\\n child: Center(\\n child: Text(\'已经到底了~\', style: TextStyle(color: Colors.grey)),\\n ),\\n );\\n }\\n}\\n
\\n掌握Dio
需要建立三层认知:基础层理解请求生命周期管理
,进阶层掌握拦截器管道机制
,架构层学会与状态管理
、依赖注入
等模式结合。真正的精通不在于记住每个API
,而是能根据应用场景灵活设计拦截策略
、优化请求链路
。
当你能将文件下载进度控制、JWT
自动刷新、请求优先级调度
等功能像搭积木一样组合时,才意味着真正系统化掌握了Dio
的精髓。优秀的网络层架构,永远是业务复杂度与技术深度的平衡艺术。
\\n","description":"前言 在移动应用开发中,网络请求如同应用的\\"生命线\\",但Flutter原生HttpClient的简陋和http库的局限性,让我们常陷入重复造轮子的困境。\\n\\n当你的应用需要处理文件上传下载、多级拦截器、全局配置时,是否还在手动拼接URL参数?\\n当面对复杂的认证体系、日志监控需求时,是否还在用print语句调试网络请求?\\n\\nDio作为Dart生态中最强大的网络请求库,以其高度可扩展的架构设计和丰富的功能,正在重塑异步编程范式。\\n\\n本文将带你穿透API表层,系统构建Dio的核心知识体系,解锁企业级应用的开发密码。\\n\\n操千曲而后晓声,观千剑而后识器。虐它千百遍方能通…","guid":"https://juejin.cn/post/7488210211380625435","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-02T01:27:17.364Z","media":null,"categories":["Android","Dart","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 教程(六)路由管理","url":"https://juejin.cn/post/7488176335237005339","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
在 Flutter 中,路由管理和Android、iOS类似,都是用于管理界面之间的跳转。在 flutter 中,我们使用 Navigator
来实现跳转的功能
在 flutter 中,支持两种跳转方式,一种是直接跳转,一种是通过路由表跳转。下面分别介绍:
\\n我们可以使用 Navigator.push
方法直接跳转到新的界面,还可以使用 Navigator.pop
回退到之前的界面。代码示例如下:
Navigator.push(context, MaterialPageRoute(builder: (context) {\\n return NewRoute();\\n}));\\n
\\n其中MaterialPageRoute
是 Material组件库提供的组件,它可以针对不同平台,实现与平台页面切换动画风格一致的路由切换动画。
要实现路由表跳转,我们需要先在 MaterialApp
的 routes 中注册,代码示例如下:
class MyApp extends StatelessWidget {\\n const MyApp({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n title: \'Flutter Demo\',\\n // 注册路由表\\n routes: {\\n \'newRoute\': (context) => NewRoute(),\\n },\\n theme: ThemeData(\\n colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),\\n ),\\n home: const MyHomePage(title: \'Flutter Demo Home Page\'),\\n );\\n }\\n}\\n
\\n然后通过 Navigator.pushNamed
方法来跳转界面,代码示例如下:
Navigator.pushNamed(context, \'newRoute\');\\n
\\n对于直接跳转的情况,我们把数据传入就可以了,代码示例如下:
\\nclass NewRoute extends StatelessWidget {\\n \\n // 增加标题数据\\n final String title;\\n\\n NewRoute({Key? key, required this.title}) : super(key: key);\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: Text(\\"New route $title\\"),\\n ),\\n body: Center(\\n child: Text(\\"This is new route\\"),\\n ),\\n );\\n }\\n}\\n\\n\\nNavigator.push(context, MaterialPageRoute(builder: (context) {\\n // 设置对应的数据\\n return NewRoute(title: \\"新的标题\\",);\\n}));\\n
\\n对于路由表跳转,我们需要通过 ModalRoute.of(context)!.settings.arguments
来接收参数,代码示例如下:
class NewRoute extends StatelessWidget {\\n\\n @override\\n Widget build(BuildContext context) {\\n // 接收参数\\n final args = ModalRoute.of(context)!.settings.arguments as Map<String, String>;\\n final message = args[\'message\'];\\n return Scaffold(\\n appBar: AppBar(\\n title: Text(\\"New route $message\\"),\\n ),\\n body: Center(\\n child: Text(\\"This is new route\\"),\\n ),\\n );\\n }\\n}\\n\\nNavigator.pushNamed(context, \'newRoute\', arguments: {\\"title\\": \\"新的标题\\"});\\n
\\n// 设置返回值\\nNavigator.pop(context, \\"我是返回值\\")\\n\\nclass RouterTestRoute extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return Center(\\n child: ElevatedButton(\\n onPressed: () async {\\n // 打开`TipRoute`,并等待返回结果\\n var result = await Navigator.push(\\n context,\\n MaterialPageRoute(\\n builder: (context) {\\n return TipRoute(\\n // 路由参数\\n text: \\"我是提示xxxx\\",\\n );\\n },\\n ),\\n );\\n //输出`TipRoute`路由返回结果\\n print(\\"路由返回值: $result\\");\\n },\\n child: Text(\\"打开提示页\\"),\\n ),\\n );\\n }\\n}\\n
\\n默认情况下,MaterialApp 中的 home 属性用来设置首页的界面。
\\nclass MyApp extends StatelessWidget {\\n const MyApp({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n title: \'Flutter Demo\',\\n theme: ThemeData(\\n colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),\\n ),\\n home: const MyHomePage(title: \'Flutter Demo Home Page\'),\\n );\\n }\\n}\\n
\\n如果你想让首页也支持路由表跳转,需要使用 initialRoute 属性,代码示例如下:
\\nMaterialApp(\\n title: \'Flutter Demo\',\\n initialRoute:\\"/\\", //名为\\"/\\"的路由作为应用的home(首页)\\n theme: ThemeData(\\n primarySwatch: Colors.blue,\\n ),\\n //注册路由表\\n routes:{\\n \\"new_page\\":(context) => NewRoute(),\\n \\"/\\":(context) => MyHomePage(title: \'Flutter Demo Home Page\'), //注册首页路由\\n } \\n);\\n
\\nNavigator
除了基础的push
和pop
方法外,还提供了其他的方法,如下表所示:
方法名 | 介绍 | 路由关系示例 | 参数传递与接收 |
---|---|---|---|
pushReplacementNamed(BuildContext context, String routeName, {Object arguments}) | 使用注册的路由命名替换当前路由,从当前页面跳转到新页面,栈顶路由被替换。类似pushReplacement ,只是使用路由命名而非直接路由。 | 当前路由顺序为A-B-C,从C执行pushReplacementNamed 到D(假设D已注册路由名为\\"paged\\"),路由顺序变为A-B-D。若在D页面执行pop ,路由顺序变为A-B | 通过arguments 参数传递数据给新页面,新页面在build 方法内通过ModalRoute.of(context).settings.arguments 获取参数。由于原页面已被替换,无法接收原页面返回值 |
popAndPushNamed<T extends Object, TO extends Object>(BuildContext context, String routeName, {TO result, Object arguments}) | 当前路由出栈,同时栈顶入栈一个新路由(通过注册的路由名指定)。 | 当前路由顺序为A-B-C,在C页面执行popAndPushNamed 到D(假设D已注册路由名为\\"paged\\"),路由顺序变为A-B-D | 可通过arguments 参数传递数据给新页面,新页面在build 方法内通过ModalRoute.of(context).settings.arguments 获取参数。result 参数用于在出栈时向原页面传递数据(若有需要) |
pushReplacement(BuildContext context, Route route, {TO result}) | 用新路由替换当前路由,跳转到新页面,原页面被移除。 | 当前路由顺序为A-B-C,从C执行pushReplacement 到D,路由顺序变为A-B-D。若在D页面执行pop ,路由顺序变为A-B | 通过目标页面的构造方法传递参数。由于原页面已被替换,无法接收原页面返回值 |
pushNamedAndRemoveUntil(BuildContext context, String routeName, RoutePredicate predicate, {Object arguments}) | 跳转到注册路由名对应的新页面,并根据predicate 的返回值决定新路由之前的路由的处理方式。类似pushAndRemoveUntil ,只是使用路由命名而非直接路由。 | 1. 若predicate 返回false ,当前路由顺序为A-B-C-D-E,从E执行pushNamedAndRemoveUntil 到F(假设F已注册路由名为\\"paged\\"),路由顺序变为F,A-E全部出栈。2. 若 predicate 返回true ,路由顺序变为A-B-C-D-E-F。3. 若 predicate 为ModalRoute.withName 指定的命名路由名称(如\\"pagea\\"),假设当前路由顺序为T-A-B-C-D,从C执行pushNamedAndRemoveUntil 到D(假设D已注册路由名为\\"paged\\"),路由顺序变为T-A-D | 通过arguments 参数传递数据给新页面,新页面在build 方法内通过ModalRoute.of(context).settings.arguments 获取参数。由于原页面部分或全部被移除,无法接收原页面返回值 |
popUntil(BuildContext context, RoutePredicate predicate) | 从栈顶开始逐个出栈路由,直到满足predicate 指定的条件(条件可以是注册的路由名或RouteSetting 设置的名称)。 | 当前路由顺序为A-B-C-D-E,在E页面执行popUntil(context, ModalRoute.withName(\'B\')) ,路由顺序变为A-B | 在出栈过程中,若需要传递数据给前一个页面(在满足条件停止出栈前的页面),可结合其他机制(如在相关页面的状态管理中处理) |
前文介绍的所有路由知识都是Flutter官方提供的基本知识。在实际的Flutter项目中,开发人员往往并不这么写,因为还有一种更为方便的操作,也就是使用Flutter提供给我们的第三方路由库——fluro。
\\n关于 fluro 的使用,具体可以看 初识 fluro 路由管理
\\n之前的文章提到过,在 Flutter 开发中,一切皆是组件。因此,事件监听Listener也是一个组件。
\\n在移动端,各个平台或UI系统的原始指针事件模型基本都是一致,即:一次完整的事件分为三个阶段:手指按下、手指移动、和手指抬起。在 Flutter 中,我们使用 Listener
来处理原始的触摸事件,代码示例如下:
// 创建一个 Listener 组件,用于监听指针事件\\nListener(\\n // 设置 Listener 组件的子组件\\n child: Container(\\n // 设置容器的宽度为 200 逻辑像素\\n width: 200,\\n // 设置容器的高度为 200 逻辑像素\\n height: 200,\\n // 设置容器的背景颜色为亮绿色\\n color: Colors.greenAccent,\\n // 设置容器的子组件为一个文本组件\\n child: Text(\\n // 显示父组件传递过来的 title 属性值\\n widget.title,\\n // 设置文本的样式,字体大小为 32 逻辑像素\\n style: TextStyle(fontSize: 32),\\n ),\\n ),\\n // 当指针按下时触发的回调函数\\n onPointerDown: (PointerDownEvent event) => print(\'onPointerDown\'),\\n // 当指针移动时触发的回调函数\\n onPointerMove: (PointerMoveEvent event) => print(\'onPointerMove\'),\\n // 当指针抬起时触发的回调函数\\n onPointerUp: (PointerUpEvent event) => print(\'onPointerUp\'),\\n // 当指针事件被取消时触发的回调函数\\n onPointerCancel: (PointerCancelEvent event) => print(\'onPointerCancel\'),\\n);\\n
\\n从上面示例代码可以看到,每个回调方法都会有一个event参数。虽然参数的类型各不相同,但是还有如下常用的属性:
\\n假如我们不想让某个子树响应PointerEvent
的话,我们可以使用IgnorePointer
和AbsorbPointer
,这两个组件都能阻止子树接收指针事件。两者有以下区别。
IgnorePointer
:此节点与其子节点都将忽略点击事件,用ignoring参数区分是否忽略。AbsorbPointer
:此节点本身能够响应点击事件,但是它会阻止事件传递到子节点上。IgnorePointer 示例如下:
\\nclass _PointerEventPageState extends State<PointerEventPage> {\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: Text(widget.title),\\n ),\\n body: Center(\\n child: Listener(\\n child: IgnorePointer(\\n ignoring: true,\\n child: Listener(\\n child: Container(\\n width: 200,\\n height: 200,\\n color: Colors.greenAccent,\\n ),\\n onPointerDown: (PointerDownEvent event)=>print(\\"Listener2\\"),\\n ),\\n ),\\n onPointerDown: (PointerDownEvent event)=>print(\\"Listener1\\"),\\n ),\\n ),\\n );\\n }\\n}\\n
\\nAbsorbPointer示例如下
\\nclass _PointerEventPageState extends State<PointerEventPage> {\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: Text(widget.title),\\n ),\\n body: Center(\\n child: Listener(\\n child: AbsorbPointer(\\n child: Listener(\\n child: Container(\\n width: 200,\\n height: 200,\\n color: Colors.greenAccent,\\n ),\\n onPointerDown: (PointerDownEvent event)=>print(\\"Listener2\\"),\\n ),\\n ),\\n onPointerDown: (PointerDownEvent event)=>print(\\"Listener1\\"),\\n ),\\n ),\\n );\\n }\\n}\\n
\\n虽然原始指针事件处理普通的点击事件非常方便,但是App上的手势操作千变万化,这个时候就需要更强大的手势处理机制。
\\nGestureDetector
是一个用于手势识别的功能性组件,我们通过它可以来识别各种手势,比如放大、缩小、双击等操作手势。GestureDetector 内部封装了 Listener,用以识别语义化的手势。代码示例如下:
class _GestureTestState extends State<GestureTest> {\\n String _operation = \\"No Gesture detected!\\"; //保存事件名\\n @override\\n Widget build(BuildContext context) {\\n return Center(\\n child: GestureDetector(\\n child: Container(\\n alignment: Alignment.center,\\n color: Colors.blue,\\n width: 200.0,\\n height: 100.0,\\n child: Text(\\n _operation,\\n style: TextStyle(color: Colors.white),\\n ),\\n ),\\n onTap: () => updateText(\\"Tap\\"), //点击\\n onDoubleTap: () => updateText(\\"DoubleTap\\"), //双击\\n onLongPress: () => updateText(\\"LongPress\\"), //长按\\n ),\\n );\\n }\\n\\n void updateText(String text) {\\n //更新显示的事件名\\n setState(() {\\n _operation = text;\\n });\\n }\\n}\\n
\\nGestureDetector 组件的常用属性如下图所示:
\\nGestureRecognizer
的作用是通过Listener
来将原始指针事件转换为语义手势,一种手势的识别器对应一个GestureRecognizer
的子类。GestureDetector
内部就是使用一个或多个GestureRecognizer
来实现识别各种手势的功能的。
这里以给一段富文本(RichText
)的不同部分分别添加点击事件处理器为例,代码示例如下:
import \'package:flutter/gestures.dart\';\\n\\nclass _GestureRecognizer extends StatefulWidget {\\n const _GestureRecognizer({Key? key}) : super(key: key);\\n\\n @override\\n _GestureRecognizerState createState() => _GestureRecognizerState();\\n}\\n\\nclass _GestureRecognizerState extends State<_GestureRecognizer> {\\n TapGestureRecognizer _tapGestureRecognizer = TapGestureRecognizer();\\n bool _toggle = false; //变色开关\\n\\n @override\\n void dispose() {\\n //用到GestureRecognizer的话一定要调用其dispose方法释放资源\\n _tapGestureRecognizer.dispose();\\n super.dispose();\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Center(\\n child: Text.rich(\\n TextSpan(\\n children: [\\n TextSpan(text: \\"你好世界\\"),\\n TextSpan(\\n text: \\"点我变色\\",\\n style: TextStyle(\\n fontSize: 30.0,\\n color: _toggle ? Colors.blue : Colors.red,\\n ),\\n recognizer: _tapGestureRecognizer\\n ..onTap = () {\\n setState(() {\\n _toggle = !_toggle;\\n });\\n },\\n ),\\n TextSpan(text: \\"你好世界\\"),\\n ],\\n ),\\n ),\\n );\\n }\\n}\\n
\\n\\n\\n注意:使用
\\nGestureRecognizer
后一定要调用其dispose()
方法来释放资源(主要是取消内部的计时器)。
当手势发生冲突时,flutter 引入了手势竞技场(Gesture Arena)来解决这个问题。原理是每一个手势识别器(GestureRecognizer
)都是一个“竞争者”(GestureArenaMember
),当发生指针事件时,他们都要在“竞技场”去竞争本次事件的处理权,默认情况最终只有一个“竞争者”会胜出(win)。
比如当一个组件同时监听水平和垂直方向的拖动手势时,我们斜着拖动时哪个方向的拖动手势回调会被触发?实际上取决于第一次移动时两个轴上的位移分量,哪个轴的大,哪个轴在本次滑动事件竞争中就胜出。代码如下所示:
\\nclass GestureArenaPage extends StatefulWidget {\\n GestureArenaPage({Key key, this.title}) : super(key: key);\\n\\n final String title;\\n\\n @override\\n _GestureArenaState createState() => _GestureArenaState();\\n}\\n\\nclass _GestureArenaState extends State<GestureArenaPage> {\\n\\n double _left=0.0;\\n double _top=0.0;\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: Text(widget.title),\\n ),\\n body: Stack(\\n children: <Widget>[\\n Positioned(\\n left: _left,\\n top: _top,\\n child: GestureDetector(\\n child: OutlineButton(\\n child: Text(\'我是一个大按钮\'),\\n ),\\n onHorizontalDragUpdate: (DragUpdateDetails e){\\n setState(() {\\n _left+=e.delta.dx;\\n print(\'水平事件胜出\');\\n });\\n },\\n onVerticalDragUpdate: (DragUpdateDetails e){\\n setState(() {\\n _top+=e.delta.dy;\\n print(\'垂直事件胜出\');\\n });\\n },\\n onHorizontalDragEnd: (e){\\n print(\'水平移动结束\');\\n },\\n onVerticalDragEnd: (e){\\n print(\'垂直移动结束\');\\n },\\n onTapDown: (e){\\n print(\'按下\');\\n },\\n onTapUp: (e){\\n print(\'抬起\');\\n },\\n ),\\n ),\\n ],\\n )\\n );\\n }\\n}\\n
\\n但是如果我们既想监听拖动的手势,也想监听手指按下抬起的手势,就无法实现。因为手势竞争最终只有一个胜出者。这时有两种方案解决:
\\n在 Flutter 中,事件处理流程分为如下几步:
\\n\\n\\n注意:一旦有一个子节点的 hitTest 返回了 true,就会终止遍历,后续子节点将没有机会参与命中测试。
\\n
在 App 中,我们经常会需要一个广播机制,用以跨页面事件通知,比如一个需要登录的 App 中,页面会关注用户登录或注销事件,来进行一些状态更新。我们可以通过事件总线来实现这个功能,代码如下所示:
\\n//订阅者回调签名\\ntypedef void EventCallback(arg);\\n\\nclass EventBus {\\n //私有构造函数\\n EventBus._internal();\\n\\n //保存单例\\n static EventBus _singleton = EventBus._internal();\\n\\n //工厂构造函数\\n factory EventBus()=> _singleton;\\n\\n //保存事件订阅者队列,key:事件名(id),value: 对应事件的订阅者队列\\n final _emap = Map<Object, List<EventCallback>?>();\\n\\n //添加订阅者\\n void on(eventName, EventCallback f) {\\n _emap[eventName] ??= <EventCallback>[];\\n _emap[eventName]!.add(f);\\n }\\n\\n //移除订阅者\\n void off(eventName, [EventCallback? f]) {\\n var list = _emap[eventName];\\n if (eventName == null || list == null) return;\\n if (f == null) {\\n _emap[eventName] = null;\\n } else {\\n list.remove(f);\\n }\\n }\\n\\n //触发事件,事件触发后该事件所有订阅者会被调用\\n void emit(eventName, [arg]) {\\n var list = _emap[eventName];\\n if (list == null) return;\\n int len = list.length - 1;\\n //反向遍历,防止订阅者在回调中移除自身带来的下标错位\\n for (var i = len; i > -1; --i) {\\n list[i](arg);\\n }\\n }\\n}\\n\\n\\n//定义一个top-level(全局)变量,页面引入该文件后可以直接使用bus\\nvar bus = EventBus();\\n\\n//页面A中\\n...\\n //监听登录事件\\nbus.on(\\"login\\", (arg) {\\n // do something\\n});\\n\\n//登录页B中\\n...\\n//登录成功后触发登录事件,页面A中订阅者会被调用\\nbus.emit(\\"login\\", userInfo);\\n
\\n事件总线通常用于组件之间状态共享,但关于组件之间状态共享也有一些专门的包如redux、mobx以及前面介绍过的Provider。对于一些简单的应用,事件总线是足以满足业务需求的,如果你决定使用状态管理包的话,一定要想清楚您的 App 是否真的有必要使用它,防止“化简为繁”、过度设计。
\\n通知(Notification)是Flutter中一个重要的机制,在widget树中,每一个节点都可以分发通知,通知会沿着当前节点向上传递,所有父节点都可以通过NotificationListener
来监听通知。Flutter中将这种由子向父的传递通知的机制称为通知冒泡(Notification Bubbling)
\\n\\n通知冒泡和用户触摸事件冒泡是相似的,但有一点不同:通知冒泡可以中止,但用户触摸事件不行。
\\n
使用 NotificationListener 监听 ListView 的滑动通知。
\\nNotificationListener(\\n onNotification: (notification){\\n switch (notification.runtimeType){\\n case ScrollStartNotification: print(\\"开始滚动\\"); break;\\n case ScrollUpdateNotification: print(\\"正在滚动\\"); break;\\n case ScrollEndNotification: print(\\"滚动停止\\"); break;\\n case OverscrollNotification: print(\\"滚动到边界\\"); break;\\n }\\n },\\n child: ListView.builder(\\n itemCount: 100,\\n itemBuilder: (context, index) {\\n return ListTile(title: Text(\\"$index\\"),);\\n }\\n ),\\n);\\n
\\n// 定义一个通知类,要继承自Notification类\\nclass MyNotification extends Notification {\\n MyNotification(this.msg);\\n final String msg;\\n}\\n\\nclass NotificationRoute extends StatefulWidget {\\n @override\\n NotificationRouteState createState() {\\n return NotificationRouteState();\\n }\\n}\\n\\nclass NotificationRouteState extends State<NotificationRoute> {\\n String _msg=\\"\\";\\n @override\\n Widget build(BuildContext context) {\\n //监听通知 \\n return NotificationListener<MyNotification>(\\n onNotification: (notification) {\\n setState(() {\\n _msg+=notification.msg+\\" \\";\\n });\\n return true;\\n },\\n child: Center(\\n child: Column(\\n mainAxisSize: MainAxisSize.min,\\n children: <Widget>[\\n// ElevatedButton(\\n// onPressed: () => MyNotification(\\"Hi\\").dispatch(context),\\n// child: Text(\\"Send Notification\\"),\\n// ), \\n Builder(\\n builder: (context) {\\n return ElevatedButton(\\n //按钮点击时分发通知 \\n onPressed: () => MyNotification(\\"Hi\\").dispatch(context),\\n child: Text(\\"Send Notification\\"),\\n );\\n },\\n ),\\n Text(_msg)\\n ],\\n ),\\n ),\\n );\\n }\\n}\\n
\\nclass NotificationRouteState extends State<NotificationRoute> {\\n String _msg=\\"\\";\\n @override\\n Widget build(BuildContext context) {\\n //监听通知\\n return NotificationListener<MyNotification>(\\n onNotification: (notification){\\n print(notification.msg); //打印通知\\n return false;\\n },\\n child: NotificationListener<MyNotification>(\\n onNotification: (notification) {\\n setState(() {\\n _msg+=notification.msg+\\" \\";\\n });\\n // 回调返回了false,表示不阻止冒泡\\n return false; \\n },\\n child: ...//省略重复代码\\n ),\\n );\\n }\\n}\\n
\\nflutter 中冷启动疑难杂症排查基本完全依靠日志模块信息。准确快速的定位代码位置就变得异常重要,否则打印一堆错误日志,定位不到具体的位置也无法彻底快速的解决问题。随最近花了点时间通过解析 StackTrace.current 信息,自动获取日志的类名、函数名和日志输出代码行数。
\\n格式:[日期时间][日志类型][平台][类名.函数名 Line:行]: 日志内容
注意打印输出:
\\nvoid onTest() {\\n // DLog.d(\\"AAA\\");\\n try {\\n var map = {};\\n jsonDecode(map[\\"a\\"]);\\n } catch (e) {\\n debugPrint(\\"$this $e\\");//flutter: _MetaDataDemoState#d5486 type \'Null\' is not a subtype of type \'String\'\\n DLog.d(\\"$e\\");//[log] [2025-03-27 10:13:00.725182][DEBUG][ios][_MetaDataDemoState.onTest Line:161]: type \'Null\' is not a subtype of type \'String\'\\n DLog.i(\\"$e\\");//[log] [2025-03-27 10:13:00.725901][INFO][ios][_MetaDataDemoState.onTest Line:162]: type \'Null\' is not a subtype of type \'String\'\\n DLog.w(\\"$e\\");//[log] [2025-03-27 10:13:00.726502][WARN][ios][_MetaDataDemoState.onTest Line:163]: type \'Null\' is not a subtype of type \'String\'\\n DLog.e(\\"$e\\");//[log] [2025-03-27 10:13:00.727041][ERROR][ios][_MetaDataDemoState.onTest Line:164]: type \'Null\' is not a subtype of type \'String\'\\n }\\n}\\n
\\n关闭颜色(andriod studio 不支持)
\\n
打开颜色(VSCode支持)
/// 开启颜色\\n DLog.enableColor = true;\\n
\\n//\\n// ddlog.dart\\n// ddlog\\n//\\n// Created by shang on 7/4/21 3:53 PM.\\n// Copyright © 7/4/21 shang. All rights reserved.\\n//\\n\\nimport \'dart:developer\' as developer;\\nimport \'dart:io\' show Platform;\\nimport \'package:flutter/foundation.dart\';\\n\\n\\nclass DLog {\\n /// 是否启用日志打印\\n static bool enableLog = true;\\n\\n /// 开启颜色\\n static bool enableColor = false;\\n\\n // ANSI 颜色代码\\n static const String _ansiReset = \'\\\\x1B[0m\';\\n static const String _ansiRed = \'\\\\x1B[31m\';\\n static const String _ansiGreen = \'\\\\x1B[32m\';\\n static const String _ansiYellow = \'\\\\x1B[33m\';\\n static const String _ansiBlue = \'\\\\x1B[34m\';\\n // static const String _ansiGray = \'\\\\x1B[37m\';\\n\\n // Web 控制台颜色样式\\n static const String _webRed = \'color: red\';\\n static const String _webGreen = \'color: #4CAF50\';\\n static const String _webYellow = \'color: #FFC107\';\\n static const String _webBlue = \'color: #2196F3\';\\n // static const String _webGray = \'color: #9E9E9E\';\\n\\n // 打印调试日志\\n static String d(dynamic message) {\\n return _printLog(\'DEBUG\', message, _ansiBlue, _webBlue);\\n }\\n\\n // 打印信息日志\\n static String i(dynamic message) {\\n return _printLog(\'INFO\', message, _ansiGreen, _webGreen);\\n }\\n\\n // 打印警告日志\\n static String w(dynamic message) {\\n return _printLog(\'WARN\', message, _ansiYellow, _webYellow);\\n }\\n\\n // 打印错误日志\\n static String e(dynamic message) {\\n return _printLog(\'ERROR\', message, _ansiRed, _webRed);\\n }\\n\\n // 获取调用信息\\n static (String className, String functionName, String fileName, int lineNumber) _getCallerInfo() {\\n try {\\n final frames = StackTrace.current.toString().split(\'\\\\n\');\\n // 第一帧是当前方法,第二帧是日志方法(d/i/w/e),第三帧是调用者\\n if (frames.length > 2) {\\n final frame = frames[3]; // 获取调用者的帧\\n // 匹配类名和方法名\\n final classMatch = RegExp(r\'#\\\\d+\\\\s+([^.]+).(\\\\w+)\').firstMatch(frame);\\n final className = classMatch?.group(1) ?? \'Unknown\';\\n final functionName = classMatch?.group(2) ?? \'unknown\';\\n\\n // 匹配文件名和行号\\n final fileMatch = RegExp(r\'((.+?):(\\\\d+)(?::\\\\d+)?)\').firstMatch(frame);\\n final fileName = fileMatch?.group(1) ?? \'unknown\';\\n final lineNumber = int.tryParse(fileMatch?.group(2) ?? \'0\') ?? 0;\\n\\n return (className, functionName, fileName, lineNumber);\\n }\\n } catch (e) {\\n debugPrint(\'Error getting caller info: $e\');\\n }\\n return (\'\', \'\', \'\', 0);\\n }\\n\\n // 获取当前平台\\n static String _getPlatform() {\\n if (kIsWeb) {\\n return \'Web\';\\n }\\n try {\\n return Platform.operatingSystem;\\n } catch (e) {\\n // 如果 Platform 不可用,返回 Unknown\\n return \'\';\\n }\\n }\\n\\n // 内部打印方法\\n static String _printLog(String level, dynamic message, String ansiColor, String webColor) {\\n if (!enableLog || !kDebugMode) {\\n return \\"\\";\\n }\\n\\n final (className, functionName, fileName, lineNumber) = _getCallerInfo();\\n final now = DateTime.now();\\n final timeStr = now.toString();\\n final platform = _getPlatform();\\n\\n final logMessage = kIsWeb\\n ? \'[$timeStr][$level][$platform]: $message\'\\n : \'[$timeStr][$level][$platform][$className.$functionName Line:$lineNumber]: $message\';\\n\\n if (kIsWeb) {\\n return _printLogWeb(level, logMessage, webColor);\\n } else {\\n return _printLogNative(level, logMessage, ansiColor);\\n }\\n }\\n\\n // Web 平台的打印实现\\n static String _printLogWeb(String level, String message, String webColor) {\\n developer.log(message);\\n return message;\\n }\\n\\n // 原生平台的打印实现\\n static String _printLogNative(String level, String message, String ansiColor) {\\n final sb = StringBuffer();\\n if (enableColor) {\\n sb.write(ansiColor);\\n }\\n sb.write(message);\\n if (enableColor) {\\n sb.write(_ansiReset);\\n }\\n\\n final result = sb.toString();\\n developer.log(sb.toString());\\n return result;\\n }\\n}\\n
\\n现有的第三方库没发现支持日志定位代码准确位置的库。但在iOS原生中这是默认支持的功能,一直在寻找flutter中的同等功能代码,在今天终于完美解决,分享给大家。
\\n","description":"一、需求来源 flutter 中冷启动疑难杂症排查基本完全依靠日志模块信息。准确快速的定位代码位置就变得异常重要,否则打印一堆错误日志,定位不到具体的位置也无法彻底快速的解决问题。随最近花了点时间通过解析 StackTrace.current 信息,自动获取日志的类名、函数名和日志输出代码行数。\\n\\n格式:[日期时间][日志类型][平台][类名.函数名 Line:行]: 日志内容\\n\\n二、使用示例\\n\\n注意打印输出:\\n\\nvoid onTest() {\\n // DLog.d(\\"AAA\\");\\n try {\\n var map…","guid":"https://juejin.cn/post/7487903297832452136","author":"SoaringHeart","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-01T01:55:37.056Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d1b52ab8c5a44f3796720010bcd7ad0a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgU29hcmluZ0hlYXJ0:q75.awebp?rk3s=f64ab15b&x-expires=1744077421&x-signature=7vFm5FcG%2BdO4A4tWpjxiagShwoY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d59b5e77de72497cb9304882540efddd~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgU29hcmluZ0hlYXJ0:q75.awebp?rk3s=f64ab15b&x-expires=1744077421&x-signature=9%2BOXcRGo7KJf56jhxN5EMyL%2FKtY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f6d2d919cc214ad18b9ba63fb56515a9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgU29hcmluZ0hlYXJ0:q75.awebp?rk3s=f64ab15b&x-expires=1744077421&x-signature=7Y9DoT%2Bykoe17lDXF5k74%2F8CMn0%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 性能优化:实战指南","url":"https://juejin.cn/post/7487813517548175387","content":"主要的优化方面,如首屏、列表、渲染、内存、数据加载、工具等。
\\n问题:首屏加载时白屏或长时间等待数据导致用户体验差。
\\n解决方案:
\\nSkeletonLoader
等组件模拟页面结构,替代空白屏4。SizedBox.shrink()
,降低首帧渲染时间(如减少 200ms)。代码示例:
\\nWidget build(BuildContext context) {\\n if (_isLoading) return const SizedBox.shrink(); // 轻量占位\\n // 真实内容\\n}\\n
\\n问题:首屏图片加载延迟。
\\n解决方案:在页面初始化前预加载关键资源(如轮播图、Banner)。
\\n代码示例:
\\nFuture<void> _precacheImages() async {\\n await Future.wait([\\n precacheImage(AssetImage(\'assets/banner.png\'), context),\\n ]);\\n}\\n
\\n问题:Flutter 通过 Channel 请求数据时线程切换和编解码耗时。
\\n解决方案:
\\n效果:详情页启动时间优化 100ms 以上9。
\\n问题:一次性渲染大量列表项导致卡顿。
\\n解决方案:
\\n代码示例:
\\nListView.builder(\\n itemCount: 1000,\\n itemBuilder: (context, index) => ListItem(data[index]),\\n)\\n
\\n问题:频繁切换 Tab 导致重复加载数据。
\\n解决方案:
\\nPageStorageKey
或 AutomaticKeepAliveClientMixin
缓存页面状态。RxMap
维护不同分类的数据状态,减少重复请求。问题:频繁调用 setState
导致不必要的 Widget 重建。
解决方案:
\\nconst
构造函数创建静态组件,避免重复构建。Selector
或 Consumer
局部刷新,而非全量更新。代码示例:
\\nconst MyWidget(); // 编译时确定,避免重建\\n
\\n问题:复杂动画导致帧率下降。
\\n解决方案:
\\nchild
参数传入,避免重复构建。代码示例:
\\nAnimatedBuilder(\\n animation: _controller,\\n child: const StaticWidget(),\\n builder: (context, child) => Transform.rotate(\\n angle: _controller.value,\\n child: child,\\n ),\\n)\\n
\\n问题:未释放资源导致内存持续增长。
\\n解决方案:
\\ndispose
方法中取消订阅和释放资源。问题:多图层叠加(如半透明蒙层)导致 GPU 负载高。
\\n解决方案:
\\nsaveLayer
:使用 ClipRRect
替代复杂蒙层,减少离屏渲染。checkerboardRasterCacheImages
检测未缓存的图像。问题:全 Flutter 页面启动性能差(如动态库加载耗时)。
\\n解决方案:
\\n问题:Dart 单线程阻塞 UI。
\\n解决方案:将耗时计算(如 JSON 解析)放入 Isolate 执行。
\\n代码示例:
\\nfinal result = await compute(heavyTask, data);\\n
\\n问题:资源加载慢(如图片、字体)。
\\n解决方案:
\\nflutter_image_compress
优化图片体积。核心原则:减少 Widget 重建、按需加载、复用资源、分离耗时操作。
\\n实战案例:
\\n在 Flutter 的架构中,Embedder
是连接 Flutter 引擎(Engine)和原生平台的桥梁。它负责将 Flutter 嵌入到不同的平台(如 Android、iOS、Windows、Linux 等),并处理窗口管理、输入事件、生命周期控制等与原生系统相关的任务。Embedder
的启动流程是 Flutter 应用运行的基础,贯穿了资源加载、引擎初始化、渲染视图创建等多个环节。
下面我们详细分析 Embedder
的启动流程。
在启动过程中,Embedder
的主要职责包括:
libflutter.so
)。Embedder
的启动流程可以分为以下几个阶段:
FlutterEngine
。FlutterSurfaceView
)。入口:FlutterLoader.startInitialization
方法(Android 平台)。
关键任务:
\\nSystem.loadLibrary(\\"flutter\\")
加载 libflutter.so
。libflutter.dylib
是直接链接的,无需显式加载。JNI_OnLoad
方法,完成 JNI 接口的注册和绑定。ResourceExtractor
提取 Flutter 应用的资源文件(如 Dart AOT 文件、图片等),并将其存储到本地目录。关键代码(Android 平台):
\\npublic void startInitialization(Context applicationContext) {\\n System.loadLibrary(\\"flutter\\");\\n ResourceExtractor.extractResources(applicationContext);\\n}\\n
\\n入口:FlutterActivityAndFragmentDelegate.onAttach
方法。
关键任务:
\\nFlutterEngine
:\\nFlutterEngine
实例。FlutterJNI
:\\nFlutterJNI.attachToJni
方法,将 JNI 层与 Dart VM 绑定。PlatformPlugin
,为 Flutter 提供剪贴板、输入法等原生能力。MethodChannel
、EventChannel
)以支持原生交互。关键代码(Android 平台):
\\nFlutterEngine flutterEngine = new FlutterEngine(context);\\nflutterEngine.getDartExecutor().executeDartEntrypoint(\\n DartExecutor.DartEntrypoint.createDefault()\\n);\\n
\\n入口:FlutterActivityAndFragmentDelegate.onCreateView
方法。
关键任务:
\\nRenderMode
)创建对应的 RenderSurface
。FlutterSurfaceView
(默认模式,基于 Surface 渲染)。FlutterTextureView
(基于 Texture 渲染)。FlutterImageView
(基于 Image 渲染)。FlutterView.attachToFlutterEngine
方法,将渲染视图与 FlutterEngine
绑定。flutterUiDisplayListener
,监听 Flutter UI 的首帧渲染完成事件。关键代码:
\\nFlutterView flutterView = new FlutterView(context);\\nflutterView.attachToFlutterEngine(flutterEngine);\\n
\\n入口:FlutterActivityAndFragmentDelegate.doInitialFlutterViewRun
方法。
关键任务:
\\nNavigationChannel
设置 Dart 框架的初始页面路由。DartExecutor.executeDartEntrypoint
方法,指定 Dart 的入口文件和函数。nativeRunBundleAndSnapshotFromLibrary
方法,加载 Dart 代码并启动 Flutter 框架。关键代码:
\\nflutterEngine.getNavigationChannel().setInitialRoute(\\"/\\");\\nflutterEngine.getDartExecutor().executeDartEntrypoint(\\n DartExecutor.DartEntrypoint.createDefault()\\n);\\n
\\nFlutterJNI.onSurfaceChanged
和 FlutterJNI.onDrawFrame
。onDrawFrame
方法。以下是 Flutter Embedder 启动流程的完整结构:
\\n1. 资源初始化\\n └── 加载 libflutter.so\\n └── 提取 Flutter 应用资源\\n\\n2. Flutter 引擎初始化\\n └── 创建 FlutterEngine\\n └── 初始化 FlutterJNI\\n └── 配置 PlatformPlugin 和通道\\n\\n3. 渲染视图初始化\\n └── 创建渲染视图(FlutterSurfaceView 等)\\n └── 绑定 FlutterEngine\\n └── 注册首帧回调\\n\\n4. Dart 代码执行\\n └── 设置初始路由\\n └── 加载 Dart 入口文件\\n └── 启动 Flutter 框架\\n\\n5. 运行与渲染\\n └── 启动渲染循环\\n └── 显示首帧\\n
\\n资源加载:
\\nFlutterEngine 的核心作用:
\\nFlutterEngine
是整个 Flutter 应用的核心,它连接了 Dart VM 和渲染管道。渲染视图的选择:
\\nFlutterSurfaceView
、FlutterTextureView
等),可以适配不同的场景需求。高效的渲染机制:
\\nFlutter Embedder 的启动流程是整个 Flutter 应用运行的基础,它将原生平台与 Flutter 引擎无缝连接,确保 Dart 代码和渲染逻辑能够正确运行。通过对启动流程的深入理解,我们可以更好地优化应用的启动性能,并解决与引擎交互的相关问题。
\\n无论是资源加载、引擎初始化还是渲染视图的创建,每个环节都至关重要。掌握这些细节,不仅有助于开发高性能的 Flutter 应用,还能帮助我们更好地理解 Flutter 的跨平台能力和运行机制。
","description":"在 Flutter 的架构中,Embedder 是连接 Flutter 引擎(Engine)和原生平台的桥梁。它负责将 Flutter 嵌入到不同的平台(如 Android、iOS、Windows、Linux 等),并处理窗口管理、输入事件、生命周期控制等与原生系统相关的任务。Embedder 的启动流程是 Flutter 应用运行的基础,贯穿了资源加载、引擎初始化、渲染视图创建等多个环节。 下面我们详细分析 Embedder 的启动流程。\\n\\n1. Embedder 的职责\\n\\n在启动过程中,Embedder 的主要职责包括:\\n\\n加载和初始化 Flutter…","guid":"https://juejin.cn/post/7487853646208450579","author":"zhumeng","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-04-01T01:18:49.540Z","media":null,"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter敏感词过滤实战:基于AC自动机的高效解决方案","url":"https://juejin.cn/post/7487851445200437302","content":"在社交、直播、论坛等UGC场景中,敏感词过滤是保障平台安全的关键防线。本文将深入解析基于AC自动机的Flutter敏感词过滤实现方案,通过原理剖析+实战代码+性能对比,带你打造毫秒级响应的高性能过滤系统。
\\nstatic void _buildTrie(List<String> words) {\\n _root.clear();\\n \\n // 构建基础Trie结构\\n for (var word in words) {\\n var node = _root;\\n for (var char in word.toLowerCase().split(\'\')) {\\n node = node.putIfAbsent(char, () => <String, dynamic>{})\\n as Map<String, dynamic>;\\n }\\n node[\'isEnd\'] = true; // 结束标记\\n }\\n\\n // BFS构建失败指针\\n final queue = <Map<String, dynamic>>[];\\n // 初始化第一层节点...\\n}\\n
\\n技术要点:
\\n// 关键回溯逻辑\\nwhile (failNode != _root && !failNode.containsKey(char)) {\\n failNode = failNode[\'fail\'] as Map<String, dynamic>? ?? _root;\\n}\\nchildNode[\'fail\'] = failNode[char] ?? _root;\\n
\\n作用:
\\nstatic final Set<String> _ignoreChars = {\'-\', \'_\', \'*\', \'#\', \' \'};\\n\\n// 在检测逻辑中:\\nif (_ignoreChars.contains(char)) {\\n tempIndex++; // 跳过但不中断当前路径\\n continue;\\n}\\n
\\n支持场景:
\\n{\\n \\"words\\": {\\n \\"list\\": [\\"敏感词\\", \\"合法\\"]\\n }\\n}\\n
\\nvoid main() async {\\n await SensitiveWordsFilter.loadSensitiveWords();\\n runApp(MyApp());\\n}\\n
\\nbool hasSensitive = SensitiveWordsFilter.containsSensitiveWords(inputText);\\nif (hasSensitive) {\\n showAlertDialog(\'包含敏感内容\');\\n}\\n
\\n文本长度 | 敏感词数量 | 处理时间(ms) |
---|---|---|
500字符 | 1000 | 2.1 |
1000字符 | 5000 | 4.3 |
5000字符 | 20000 | 18.7 |
本文实现的AC自动机方案,在Flutter应用中达到了平均3ms/千字符的处理速度。相较于传统方案,在保证精度的同时实现了性能的飞跃。建议将敏感词库维护作为长期工作,结合业务场景持续优化,构建全方位的内容安全体系。
\\n完整代码示例如下:
\\nimport \'dart:convert\';\\n\\nimport \\"package:flutter/services.dart\\";\\n\\n// 敏感词过滤器(基于 AC 自动机实现)\\nclass SensitiveWordsFilter {\\n // Trie 树根节点\\n static final Map<String, dynamic> _root = {};\\n static bool _isBuilt = false;\\n\\n // 可扩展的干扰字符\\n static final Set<String> _ignoreChars = {\'-\', \'_\', \'*\', \'#\', \' \'};\\n\\n // 加载敏感词列表并构建 Trie 树\\n static Future<void> loadSensitiveWords() async {\\n try {\\n final jsonString =\\n await rootBundle.loadString(\'assets/words/sensitive_words.json\');\\n final sensitiveWordsData = jsonDecode(jsonString);\\n\\n var listData = sensitiveWordsData[\'words\'][\'list\'];\\n if (listData is List) {\\n _buildTrie(List<String>.from(listData));\\n print(\\"Sensitive words loaded successfully.\\");\\n } else {\\n print(\\"Error: \'list\' field is not a valid List.\\");\\n }\\n } catch (e) {\\n print(\\"Load error: $e\\");\\n }\\n }\\n\\n // 构建 Trie 树\\n static void _buildTrie(List<String> words) {\\n _root.clear();\\n\\n for (var word in words) {\\n var node = _root;\\n for (var char in word.toLowerCase().split(\'\')) {\\n node = node.putIfAbsent(char, () => <String, dynamic>{})\\n as Map<String, dynamic>;\\n }\\n node[\'isEnd\'] = true; // 标记敏感词结束\\n }\\n\\n // 构建 fail 指针\\n final queue = <Map<String, dynamic>>[];\\n for (var entry in _root.entries) {\\n if (entry.value is Map<String, dynamic>) {\\n var child = entry.value as Map<String, dynamic>;\\n child[\'fail\'] = _root;\\n queue.add(child);\\n }\\n }\\n\\n while (queue.isNotEmpty) {\\n var parentNode = queue.removeAt(0);\\n for (var entry in parentNode.entries) {\\n if (entry.key == \'fail\' || entry.key == \'isEnd\') continue;\\n\\n var char = entry.key;\\n var childNode = entry.value as Map<String, dynamic>;\\n\\n // 回溯 fail 指针\\n var failNode = parentNode[\'fail\'] as Map<String, dynamic>? ?? _root;\\n while (failNode != _root && !failNode.containsKey(char)) {\\n failNode = failNode[\'fail\'] as Map<String, dynamic>? ?? _root;\\n }\\n\\n childNode[\'fail\'] = failNode[char] ?? _root;\\n\\n if ((failNode[char] as Map<String, dynamic>?)?.containsKey(\'isEnd\') ??\\n false) {\\n childNode[\'isEnd\'] = true;\\n }\\n\\n queue.add(childNode);\\n }\\n }\\n\\n _isBuilt = true;\\n }\\n\\n // 检查消息是否包含敏感词\\n static bool containsSensitiveWords(String message) {\\n if (!_isBuilt) {\\n throw Exception(\'敏感词列表未初始化\');\\n }\\n\\n int index = 0;\\n final lowerMessage = message.toLowerCase();\\n\\n while (index < lowerMessage.length) {\\n var node = _root;\\n int tempIndex = index;\\n\\n while (tempIndex < lowerMessage.length) {\\n var char = lowerMessage[tempIndex];\\n\\n // 如果是干扰字符,跳过但不更新节点\\n if (_ignoreChars.contains(char)) {\\n tempIndex++;\\n continue;\\n }\\n\\n // 失配时,沿着 fail 指针回退\\n while (node != _root && !node.containsKey(char)) {\\n node = node[\'fail\'] as Map<String, dynamic>? ?? _root;\\n }\\n\\n node = node[char] as Map<String, dynamic>? ?? _root;\\n\\n // 如果当前节点是敏感词结尾,返回 true\\n if (node.containsKey(\'isEnd\')) return true;\\n\\n tempIndex++;\\n }\\n\\n index++;\\n }\\n\\n return false;\\n }\\n}\\n
","description":"Flutter敏感词过滤实战:基于AC自动机的高效解决方案 在社交、直播、论坛等UGC场景中,敏感词过滤是保障平台安全的关键防线。本文将深入解析基于AC自动机的Flutter敏感词过滤实现方案,通过原理剖析+实战代码+性能对比,带你打造毫秒级响应的高性能过滤系统。\\n\\n一、为什么选择AC自动机?\\n传统方案的痛点\\n正则表达式:匹配效率低(O(nm)复杂度)\\n简单遍历:无法处理变形词(如\\"微-信-付-款\\")\\n第三方API:网络延迟影响用户体验\\nAC自动机的优势\\n多模式匹配:同时检测所有敏感词\\n线性时间复杂度:O(n)处理任意长度文本\\n容错能力:智能处理…","guid":"https://juejin.cn/post/7487851445200437302","author":"飞川001","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-31T16:03:42.470Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/8cc9fb4e674d4047acc21dae3c08ddd9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6aOe5bedMDAx:q75.awebp?rk3s=f64ab15b&x-expires=1744041822&x-signature=ujIYU%2B5ndQDcw2csY7xotNzsY7s%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter开箱即用一站式解决方案","url":"https://juejin.cn/post/7487792871821426697","content":"本库为Flutter应用开发提供一站式解决方案,包含:
\\nThemeExtension
全局配置颜色/圆角/间距等样式在 pubspec.yaml
中添加依赖:
/// 1.8.0版本已移除图片选择裁剪上传oss一站式方案\\ndependencies:\\n flutter_chen_common: 最新版本\\n
\\n运行命令:
\\nflutter pub get\\n
\\nvoid main() async {\\n WidgetsFlutterBinding.ensureInitialized();\\n\\n // 初始化必备服务\\n await SpUtil.init(); // 本地存储\\n await HttpClient.init( // 网络模块\\n config: HttpConfig(\\n baseUrl: \'https://api.example.com\',\\n connectTimeout: const Duration(seconds: 30),\\n receiveTimeout: const Duration(seconds: 30),\\n enableLog: true,\\n maxRetries: 3,\\n interceptors: [CustomInterceptor()]\\n ),\\n );\\n\\n runApp(MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return ComConfiguration(\\n config: ComConfig.defaults().copyWith(\\n emptyWidget: CustomEmptyWidget(), // 自定义全局空视图\\n loadingWidget: CustomLoading(), // 自定义全局加载视图\\n ),\\n child: MaterialApp(\\n theme: ThemeData.light().copyWith(\\n extensions: [ComTheme.light()], // 启用亮色主题\\n ),\\n darkTheme: ThemeData.dark().copyWith(\\n extensions: [ComTheme.dark()], // 启用暗色主题\\n ),\\n home: MainPage(),\\n localizationsDelegates: [\\n ComLocalizations.delegate, // 国际化\\n GlobalMaterialLocalizations.delegate,\\n ],\\n supportedLocales: [\\n const Locale(\'zh\', \'CN\'),\\n const Locale(\'en\', \'US\'),\\n ],\\n ),\\n );\\n }\\n}\\n
\\n// 网络请求使用\\nHttpClient.instance.request(\\n \\"/xxxx\\",\\n method: HttpType.post.name,\\n fromJson: (json) => User.fromJson(json),\\n showLoading: true,\\n)\\n\\n// HttpConfig,内置日志打印、网络重试拦截器,日志拦截器正在重新实现优化更新,记录日志方面查看支持导出\\nHttpConfig({\\n required this.baseUrl,\\n this.connectTimeout = const Duration(seconds: 15),\\n this.receiveTimeout = const Duration(seconds: 15),\\n this.sendTimeout = const Duration(seconds: 15),\\n this.commonHeaders = const {},\\n this.interceptors = const [],\\n this.enableLog = true,\\n this.maxRetries = 3,\\n });\\n
\\n主题名称 | 示例代码 |
---|---|
Light Theme | ComTheme.light |
Dark Theme | ComTheme.dark |
ComTheme(\\n theme: ComColors.lightTheme, // 颜色体系\\n shapes: ComShapes.standard,// 圆角体系\\n spacing: ComSpacing.standard,// 间距体系\\n primaryGradient: LinearGradient(\\n colors: [\\n ComColors.lightTheme.shade500,\\n ComColors.lightTheme.shade500,\\n ],\\n ),\\n success: Colors.green.shade600,\\n error: Colors.red.shade600,\\n warning: Colors.orange.shade600,\\n link: Colors.blue.shade600,\\n)\\n\\n// 色系\\nstatic MaterialColor lightTheme = const MaterialColor(\\n 0xFF3783FD,\\n <int, Color>{\\n 50: Color(0xfff8f6f9), // surface 背景色\\n 100: Color(0xfff8f2fa), // surfaceContainerLow 浅色背景色\\n 200: Color(0xfff2ecf4), // surfaceContainer 标准背景色\\n 300: Color(0xffece6ee), // surfaceContainerHigh 较深背景色\\n 400: Color(0xffe6e0e9), // surfaceContainerHighest 深色背景色\\n 500: Color(0xFF3783FD), // primary 主题色\\n 600: Color(0xff1d1b20), // onSurface 主要内容色\\n 700: Color(0xFF909399), // onSurfaceVariant 次要内容色\\n 800: Color(0xffffffff), // surfaceContainerLowest 相同色\\n 900: Color(0xff322f35), // inverseSurface 相反色\\n },\\n);\\n
\\n// 语言新增或覆盖\\n// 1. 创建法语本地化类\\nclass FrIntl extends ComIntl {\\n @override String get confirm => \\"xxx\\";\\n @override String get cancel => \\"xxx\\";\\n @override String get loading => \\"...\\";\\n}\\n\\n// 2. 注册语言\\nComLocalizations.addLocalization(\'fr\', FrIntl());\\n\\n// 3. 配置MaterialApp\\nMaterialApp(\\n supportedLocales: [\\n Locale(\'fr\'), // 新增法语支持\\n ],\\n)\\n
\\n文件名 | 功能描述 |
---|---|
clipboard_util.dart | 剪贴板操作工具(复制/粘贴文本、监听剪贴板内容) |
clone_util.dart | 对象深拷贝/浅拷贝工具(支持复杂对象克隆) |
color_util.dart | 颜色处理工具(HEX与RGB互转、颜色混合、随机颜色生成) |
date_util.dart | 日期时间工具(格式化、解析、计算时间差) |
device_util.dart | 设备信息工具(获取设备信息) |
encrypt_util.dart | 加密解密工具(算法封装) |
file_util.dart | 文件操作工具(读写文件、目录管理、文件压缩/解压) |
function_util.dart | 通用函数工具(防抖/节流、空安全处理、类型转换) |
image_util.dart | 图片处理工具(压缩、缓存管理、网络图片加载、格式转换) |
json_util.dart | JSON工具(序列化/反序列化、动态解析、数据校验) |
keyboard_util.dart | 键盘工具(控制键盘显隐、监听高度变化) |
log_util.dart | 日志工具(分级输出、日志存储、调试模式开关) |
package_util.dart | 应用包管理工具(获取应用包信息) |
permission_util.dart | 权限管理工具(全局权限处理、多权限判断及请求) |
sp_util.dart | 本地存储工具(基于SharedPreferences,支持复杂数据存取) |
text_util.dart | 文本处理工具(字符串校验、截断、正则匹配) |
dialog_util.dart | 弹窗工具类(通用各类弹窗Toast、Android、iOS确定弹窗、弹窗、选择弹窗、底部弹窗等) |
文件名 | 功能描述 |
---|---|
refresh_widget.dart | 刷新列表组件(包含上拉加载、下拉刷新、回至顶部、页面数据状态视图(加载、空数据、列表、瀑布流)等功能) |
base_widget.dart | 基础组件基类(统一多状态管理,无网络自动切换该状态布局) |
com_album.dart | 相册组件(图片九宫格仿微信朋友圈显示) |
com_arrow.dart | 方向箭头组件(支持上下左右箭头,常用于列表项导航) |
com_avatar.dart | 头像组件(圆形/方形、网络/本地/文字头像) |
com_button.dart | 按钮组件(主按钮、线性按钮、禁用状态、渐变色、自定义样式) |
com_checkbox.dart | 复选框组件(支持单选/多选、自定义图标) |
com_checkbox_list_title.dart | 列表复选框组件(ListTitle形式下的复现框) |
com_empty.dart | 空状态组件(数据为空时展示占位图或提示文字) |
com_gallery.dart | 图片画廊组件(图片查看预览等操作) |
com_image.dart | 增强图片组件(占位图、加载失败兜底、缓存策略) |
com_list_group.dart | 分组列表组件(下划线分隔的列表项布局,自定义下划线) |
com_loading.dart | 加载组件(全局Loading,可自定义) |
com_popup_menu.dart | 弹出菜单组件(自定义菜单项、位置调整) |
com_rating.dart | 评分组件(星级评分、支持半星、自定义图标) |
com_tag.dart | 标签组件(多颜色/尺寸、圆角样式) |
com_title_bar.dart | 标题栏组件(左中右布局、标题居中、常用于底部弹窗标题) |
com_divider.dart | 下划线组件(CustomPainter实现的Divider,支持负数) |
class DemoLogic extends PagingController {\\n @override\\n Future<PagingResponse> loadData() async {\\n // TODO: implement loadData\\n dynamic result = {\\"current\\": 1, \\"total\\": 3, \\"records\\": []};\\n await Future.delayed(2000.milliseconds, () {\\n for (var i = 0; i < 20; ++i) {\\n result[\\"records\\"]?.add(i);\\n }\\n });\\n\\n return PagingResponse.fromMapJson(result);\\n }\\n}\\n\\nclass DemoPage extends StatelessWidget {\\n DemoPage({Key? key}) : super(key: key);\\n\\n final logic = Get.find<DemoLogic>();\\n\\n @override\\n Widget build(BuildContext context) {\\n return GetBuilder<DemoLogic>(\\n builder: (controller) {\\n return Scaffold(\\n body: RefreshWidget(\\n controller: logic,\\n slivers: [\\n RefreshListWidget(\\n itemBuilder: (item, index) => _buildItem(index),\\n controller: logic,\\n showList: false),\\n ],\\n ));\\n },\\n id: logic.pagingState.refreshId,\\n );\\n }\\n\\n Widget _buildItem(index) {\\n if (index % 3 == 0) {\\n return Container(\\n color: Colors.deepOrange,\\n width: double.infinity,\\n height: 300.h,\\n );\\n }\\n return Container(\\n color: Colors.green,\\n width: double.infinity,\\n height: 200.h,\\n );\\n }\\n}\\n
\\n查看完整示例:
\\ngit clone https://github.com/Er-Dong-Chen/flutter-common.git\\ncd flutter-common/example\\nflutter run\\n
\\n我们欢迎以下类型的贡献:
\\n🐛 Bug 报告
\\n💡 功能建议
\\n📚 文档改进
\\n🎨 设计资源
\\n💻 代码提交
\\n欢迎提交 PR 或 Issue!贡献前请阅读:
\\n\\nMIT License - 详情见 LICENSE 文件
\\n","description":"Flutter Chen Common 🌟 简介\\n\\n本库为Flutter应用开发提供一站式解决方案,包含:\\n\\n可定制的主题系统\\n完整的国际化支持\\n企业级网络请求封装\\nN+高质量常用组件\\n常用开发工具及扩展集合\\n刷新列表一整套解决方案\\n开箱即用的通用各类弹窗\\n全局统一各状态布局\\n特性\\n🎨 主题系统:通过 ThemeExtension 全局配置颜色/圆角/间距等样式\\n🌍 国际化支持:内置中英文,支持自定义文本和动态语言切换\\n⚡ 优先级覆盖:支持全局配置 + 组件级参数覆盖\\n📱 自适应设计:完美适配 iOS/Material 设计规范\\n安装\\n\\n在…","guid":"https://juejin.cn/post/7487792871821426697","author":"耳東陈","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-31T11:13:53.059Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/84e4aa8f34de42dd9e8ace998961b4e3~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6ICz5p2x6ZmI:q75.awebp?rk3s=f64ab15b&x-expires=1744028593&x-signature=9IgZDbBWmIbLgprIYWl6b3Hxuoo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b572380242d344b3be67a7e286658c79~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6ICz5p2x6ZmI:q75.awebp?rk3s=f64ab15b&x-expires=1744028593&x-signature=rajou8Omq75dlDk6keSlUUlXrI8%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter getx 状态管理","url":"https://juejin.cn/post/7487428943034925091","content":"Flutter 要是实现状态管理wiget必须继承StatefulWidget
,调用setState((){})
,当然getx也不例外,
class HomeController extends GetxController {\\n final count = 0.obs;\\n}\\n\\nclass HomePage extends GetView<HomeController> {\\n @override\\n Widget build(BuildContext context) {\\n return Center(\\n child: Obx(() => Text(\\"${controller.currentPage.value}\\"))\\n );\\n }\\n}\\n
\\nObx 继承ObxWidget继承StatefulWidget
\\nabstract class ObxWidget extends StatefulWidget {\\n const ObxWidget({Key? key}) : super(key: key);\\n @override\\n _ObxState createState() => _ObxState();\\n\\n @protected\\n Widget build();\\n}\\n\\nclass _ObxState extends State<ObxWidget> {\\n //obx的 RxNotifier \\n ///很重要,会有2个RxNotifier\\n //1.obx的RxNotifier\\n //Rxint的RxNotifier\\n final _observer = RxNotifier();\\n late StreamSubscription subs;\\n\\n @override\\n void initState() {\\n super.initState();\\n //设置监听\\n //给getStream 设置参数\\n //1.创建LightSubscription\\n //2.onData() = _updateTree 更新widget的方法\\n //3.加入监听列表\\n subs = _observer.listen(_updateTree, cancelOnError: false);\\n }\\n \\n //更新的关键方法\\n //猜测当rxint.setValue时,会调用此方法来更新widget\\n void _updateTree(_) {\\n if (mounted) {\\n setState(() {});\\n }\\n }\\n\\n @override\\n void dispose() {\\n subs.cancel();\\n _observer.close();\\n super.dispose();\\n }\\n \\n //通过notifyChildren创建widget\\n //\\n @override\\n Widget build(BuildContext context) =>\\n RxInterface.notifyChildren(_observer, widget.build);\\n}\\n
\\nclass RxNotifier<T> = RxInterface<T> with NotifyManager<T>;\\n\\n//\\nmixin NotifyManager<T> {\\n GetStream<T> subject = GetStream<T>();\\n final _subscriptions = <GetStream, List<StreamSubscription>>{};\\n\\n bool get canUpdate => _subscriptions.isNotEmpty;\\n \\n //1.obx_rxnotifier.addlistener(rxInt_getstream)\\n //2.给rxInt_getstream设置监听\\n //3.ondata = (data) {\\n // if (!subject.isClosed) obx_subject.add(data);\\n // }\\n //4.当value 值更新时,触发 obx_getsteam的ondata= setstate()更新widget\\n void addListener(GetStream<T> rxGetx) {\\n if (!_subscriptions.containsKey(rxGetx)) {\\n //设置一个监听 \\n final subs = rxGetx.listen(\\n (data) {\\n if (!subject.isClosed) subject.add(data);\\n }\\n );\\n final listSubscriptions =\\n _subscriptions[rxGetx] ??= <StreamSubscription>[];\\n listSubscriptions.add(subs);\\n }\\n }\\n \\n //\\n StreamSubscription<T> listen(\\n void Function(T) onData, {\\n Function? onError,\\n void Function()? onDone,\\n bool? cancelOnError,\\n }) =>\\n subject.listen(\\n onData,\\n onError: onError,\\n onDone: onDone,\\n cancelOnError: cancelOnError ?? false,\\n );\\n\\n \\n void close() {\\n _subscriptions.forEach((getStream, _subscriptions) {\\n for (final subscription in _subscriptions) {\\n subscription.cancel();\\n }\\n });\\n\\n _subscriptions.clear();\\n subject.close();\\n }\\n}\\n
\\nclass GetStream<T> {\\n void Function()? onListen;\\n void Function()? onPause;\\n void Function()? onResume;\\n FutureOr<void> Function()? onCancel;\\n\\n GetStream({this.onListen, this.onPause, this.onResume, this.onCancel});\\n List<LightSubscription<T>>? _onData = <LightSubscription<T>>[];\\n\\n bool? _isBusy = false;\\n\\n FutureOr<bool?> removeSubscription(LightSubscription<T> subs) async {\\n if (!_isBusy!) {\\n return _onData!.remove(subs);\\n } else {\\n await Future.delayed(Duration.zero);\\n return _onData?.remove(subs);\\n }\\n }\\n\\n FutureOr<void> addSubscription(LightSubscription<T> subs) async {\\n if (!_isBusy!) {\\n return _onData!.add(subs);\\n } else {\\n await Future.delayed(Duration.zero);\\n return _onData!.add(subs);\\n }\\n }\\n\\n int? get length => _onData?.length;\\n\\n bool get hasListeners => _onData!.isNotEmpty;\\n \\n //2种情况\\n //1.rxint 值更新了,执行的时(data){obx_getstream.add()}\\n //2.obx_stream 执行的是 setstate((){}) 刷新widget\\n void _notifyData(T data) {\\n _isBusy = true;\\n for (final item in _onData!) {\\n if (!item.isPaused) {\\n item._data?.call(data);\\n }\\n }\\n _isBusy = false;\\n }\\n\\n T? _value;\\n\\n T? get value => _value;\\n \\n //1.rxint 当value更新时,调用 _notifyData,\\n //2.value值更新,触发,obx的getstream 的add,\\n void add(T event) {\\n assert(!isClosed, \'You cannot add event to closed Stream\');\\n _value = event;\\n //\\n _notifyData(event);\\n }\\n\\n bool get isClosed => _onData == null;\\n\\n\\n void close() {\\n assert(!isClosed, \'You cannot close a closed Stream\');\\n _notifyDone();\\n _onData = null;\\n _isBusy = null;\\n _value = null;\\n }\\n \\n //obx\\n //1.创建LightSubscription\\n //2.onData() 更新widget的方法\\n //3.加入监听列表\\n // rxInt\\n //1.把onData加入到监听 \\n /// rxint 也要加入监听\\n // onData = (data){obx_subject.add(data)}\\n //加入rxint 的监听列表\\n //当value更新时\\n LightSubscription<T> listen(void Function(T event) onData,\\n {Function? onError, void Function()? onDone, bool? cancelOnError}) {\\n final subs = LightSubscription<T>(\\n removeSubscription,\\n onPause: onPause,\\n onResume: onResume,\\n onCancel: onCancel,\\n )\\n ..onData(onData)\\n ..onError(onError)\\n ..onDone(onDone)\\n ..cancelOnError = cancelOnError;\\n addSubscription(subs);\\n onListen?.call();\\n return subs;\\n }\\n\\n Stream<T> get stream =>\\n GetStreamTransformation(addSubscription, removeSubscription);\\n}\\n
\\n会给proxy 设置2次notifier
\\nabstract class RxInterface<T> {\\n //静态全局可用,\\n //\\n static RxInterface? proxy;\\n\\n StreamSubscription<T> listen(void Function(T event) onData,\\n {Function? onError, void Function()? onDone, bool? cancelOnError});\\n \\n static T notifyChildren<T>(RxNotifier observer, ValueGetter<T> builder) {\\n //1.首次 _observer = null\\n final _observer = RxInterface.proxy;\\n //1.给proxy 设置 obx的 rxNotifiter\\n //上文说到此时有2个rxNotifiter\\n //此时的是obx的\\n RxInterface.proxy = observer;\\n //执行构建widget方法\\n //看{controller.count.value}\\n final result = builder();\\n //reset null\\n RxInterface.proxy = _observer;\\n //返回widget\\n return result;\\n }\\n}\\n
\\nrxint的RxNotifier
\\ncontroller.count.value执行的是RxObjectMixin.value
class RxInt extends Rx<int> extends _RxImpl<T> extends RxNotifier<T> with RxObjectMixin<T> on NotifyManager<T>
extension IntExtension on int {\\n RxInt get obs => RxInt(this);\\n}\\n
\\nmixin RxObjectMixin<T> on NotifyManager<T> {\\n late T _value;\\n\\n void refresh() {\\n subject.add(value);\\n }\\n\\n T call([T? v]) {\\n if (v != null) {\\n value = v;\\n }\\n return value;\\n }\\n\\n bool firstRebuild = true;\\n bool sentToStream = false;\\n \\n //controller.count.value = 1 \\n //1.执行getStream.add()\\n ////2.rxint 值更新了,执行的时(data){obx_getstream.add()}\\n //3.obx_stream 执行的是 setstate((){}) 刷新widget\\n set value(T val) {\\n if (subject.isClosed) return;\\n sentToStream = false;\\n if (_value == val && !firstRebuild) return;\\n firstRebuild = false;\\n _value = val;\\n sentToStream = true;\\n subject.add(_value);\\n }\\n \\n //执行rxint的value\\n //1.此时的RxInterface.proxy = obx的rxNotifiter,\\n //2.将rxint的getStream 设置给obx\\n // obx_rxnotifiter.addListener(rxint_subject)\\n //\\n // rxint 加入监听,\\n // rxint_getstream值的更新触发obx_getsream的setState()更新widget\\n T get value {\\n RxInterface.proxy?.addListener(subject);\\n return _value;\\n }\\n\\n Stream<T> get stream => subject.stream;\\n\\n \\n StreamSubscription<T> listenAndPump(void Function(T event) onData,\\n {Function? onError, void Function()? onDone, bool? cancelOnError}) {\\n final subscription = listen(\\n onData,\\n onError: onError,\\n onDone: onDone,\\n cancelOnError: cancelOnError,\\n );\\n\\n subject.add(value);\\n\\n return subscription;\\n }\\n\\n void bindStream(Stream<T> stream) {\\n final listSubscriptions =\\n _subscriptions[subject] ??= <StreamSubscription>[];\\n listSubscriptions.add(stream.listen((va) => value = va));\\n }\\n}\\n
\\n完整代码见 Flutter 鸿蒙版 Demo
\\n核心代码如下,通过 OhosView 来承载原生视图
\\nOhosView(\\n viewType: \'com.shaohushuo.app/customView\',\\n onPlatformViewCreated: _onPlatformViewCreated,\\n creationParams: const <String, dynamic>{\'initParams\': \'hello world\'},\\n creationParamsCodec: const StandardMessageCodec(),\\n )\\n
\\n其中 viewType 为自定义的 ohosView 的名称,onPlatformViewCreated 为创建完成回调,creationParams 为创建时传入的参数,creationParamsCodec 为参数编码格式。
\\n这里面我们按照《如何使用PlatformView》中的示例操作,首先需要创建一个显示高德地图的视图,其核心代码如下:
\\n\\n\\nMapsInitializer.setApiKey(\\"e4147e927a1f63a0acff45cecf9419b5\\");\\nMapViewManager.getInstance().registerMapViewCreatedCallback((mapview?: MapView, mapViewName?: string) => {\\n if (!mapview) {\\n return;\\n }\\n let mapView = mapview;\\n mapView.onCreate();\\n mapView.getMapAsync((map) => {\\n let aMap: AMap = map;\\n })\\n})\\n\\n@Component\\nstruct ButtonComponent {\\n @Prop params: Params\\n customView: AmapWidgetView = this.params.platformView as AmapWidgetView\\n\\n build() {\\n Row() {\\n MapViewComponent().width(\'100%\').height(\'100%\')\\n }\\n }\\n}\\n
\\n接下来创建一个 AmapWidgetFactory.ets
\\nexport class AmapWidgetFactory extends PlatformViewFactory {\\n message: BinaryMessenger;\\n\\n constructor(message: BinaryMessenger, createArgsCodes: MessageCodec<Object>) {\\n super(createArgsCodes);\\n this.message = message;\\n }\\n\\n public create(context: common.Context, viewId: number, args: Object): PlatformView {\\n return new AmapWidgetView(context, viewId, args, this.message);\\n }\\n}\\n
\\n最终需要创建一个 AmapWidgetPlugin.ets
\\nexport class AmapWidgetPlugin implements FlutterPlugin {\\n getUniqueClassName(): string {\\n return \'AmapWidgetPlugin\';\\n }\\n\\n onAttachedToEngine(binding: FlutterPluginBinding): void {\\n binding.getPlatformViewRegistry()?.\\n registerViewFactory(\'com.shaohushuo.app/customView\', new AmapWidgetFactory(binding.getBinaryMessenger(), StandardMessageCodec.INSTANCE));\\n }\\n\\n onDetachedFromEngine(binding: FlutterPluginBinding): void {}\\n}\\n
\\n插件创建好之后,记得在 EntryAbility 中注册插件
\\n this.addPlugin(new AmapWidgetPlugin())\\n
\\n\\n\\n需要注意的是,视图ID一定要两侧保持一致,如这里名为 \'com.shaohushuo.app/customView\',否则无法正常显示
\\n
在之前的文章现有Flutter项目支持鸿蒙II中,介绍了如何使用第三方插件,同时给出了非常多的使用案例,如\\nflutter_inappwebview,video_player, image_picker 等,本文将开始介绍如何集成高德地图。
\\n通过 MethodChannel 进行消息通信,在 Dart 侧调用原生API,在 ArkTS 侧收到相关调用后,根据参数跳转到指定页面
\\n static Future<dynamic> redirectNative(String url) {\\n return _methodChannel.invokeMethod(\\"redirectNative\\", {\\n \\"url\\": url,\\n });\\n }\\n
\\n在 ohos/entry/src/main/ets/entryability
创建 OhosPlugin.ets
文件,这里收到到消息后,调用 router.pushUrl
方法跳转到指定页面
export default class OhosPlugin implements FlutterPlugin {\\n ...\\n onAttachedToEngine(binding: FlutterPluginBinding): void {\\n this.channel.setMethodCallHandler({\\n onMethodCall : (call: MethodCall, result: MethodResult) => {\\n switch (call.method) {\\n case \\"redirectNative\\":\\n let url = String(call.argument(\\"url\\"));\\n router.pushUrl({ url: url})\\n break;\\n default:\\n result.notImplemented();\\n break;\\n }\\n }\\n })\\n }\\n}\\n
\\n插件写好后,需要在 EntryAbility 中注册:
\\nthis.addPlugin(new OhosPlugin())\\n
\\n添加原生页面,回到 DevEco,在 pages 目录右键,创建一个空页面, 命名为 Amap
\\n在 ohos/entry/oh-package.json
文件中引入高德地图SDK:
\\"dependencies\\": {\\n \\"@amap/amap_lbs_common\\": \\">=1.1.0\\",\\n \\"@amap/amap_lbs_map3d\\": \\">=2.1.1\\",\\n ...\\n }\\n
\\n调用高德地图SDK,显示地图组件:
\\nimport { AMap, MapsInitializer, MapView, MapViewComponent, MapViewManager, } from \'@amap/amap_lbs_map3d\';\\n// 配置 API KEY\\nMapsInitializer.setApiKey(\\"xxx\\");\\nMapViewManager.getInstance().registerMapViewCreatedCallback((mapview?: MapView, mapViewName?: string) => {\\n if (!mapview) {\\n return;\\n }\\n let mapView = mapview;\\n mapView.onCreate();\\n mapView.getMapAsync((map) => {\\n let aMap: AMap = map;\\n })\\n})\\n\\n@Entry\\n@Component\\nstruct Index {\\n build() {\\n Row() {\\n MapViewComponent()\\n .width(\'100%\')\\n .height(\'100%\')\\n }\\n }\\n}\\n
\\nPlartformCall.redirectNative(\'pages/Amap\');\\n
\\n如果在运行时,遇到以下错误, 根据官方提醒, 需要配置 useNormalizedOHMUrl
\\n ERROR: Bytecode HARs: [@amap/amap_lbs_map3d, @amap/amap_lbs_common] not supported when useNormalizedOHMUrl is not true.\\n
\\n打开文件 /ohos/build-profile.json5
, 添加以下配置
{\\n \\"app\\": {\\n \\"products\\": [\\n {\\n \\"buildOption\\": {\\n \\"strictMode\\": {\\n \\"useNormalizedOHMUrl\\": true\\n }\\n }\\n }\\n ]\\n }\\n }\\n
\\n在华为牵头下,Flutter 鸿蒙化如火如荼进行,当第一次看到一份上百个插件的Excel 列表时,我也感到震惊,排名前 100 的插件赫然在列,这无疑是一次大规模的军团作战。
\\n然后,参战团队鱼龙混杂,难免有人要浑水摸鱼。
\\n某天,一名小伙伴发来一条消息,上来就发来几行代码
\\ndependency_overrides:\\n get:\\n git:\\n url: \\"https://gitcode.com/openharmony-sig/fluttertpc_get.git\\"\\n
\\n引入以后,出现了以下错误:
\\n../../../pub-cache/git/fluttertpc_get-fcb370a5094adf8f93261bbad5691de233ec6276/lib/get_navigation/src/extension_navigation.dart:222:62: Error: The getter \'backgroundColor\' isn\'t defined for the class \'ThemeData\'.\\n\\n\'ThemeData\' is from \'package:flutter/src/material/theme_data.dart\' (\'../../../versions/versions/custom_3.22.0-ohos/packages/flutter/lib/src/material/theme_data.dart\').\\nTry correcting the name to the name of an existing getter, or defining a getter or field named \'backgroundColor\'.\\nTextStyle(color: confirmTextColor ?? theme.backgroundColor),\\n^^^^^^^^^^^^^^^\\nTarget kernel_snapshot failed: Exception\\n
\\n看到错误信息,询问之后,确认对方使用的是 Flutter 版本为 3.21,由此可以得出结论,版本不匹配。
\\n同时,让我感到疑惑的是,getx 不是纯 dart 库吗,这也要鸿蒙化?吓得我赶紧打开仓库源码,查看目录和依赖,并没有发现 ohos 相关的平台实现,这让我更迷惑了。于是,我点击提交日志,挨个查看最近的每条记录,查看代码变更,这一看可不得了,我乐了。
\\n简单探究之后,我回复对方,直接用官方社区的插件和版本,没过几分钟,问题得到解决。
\\n第二天,百无聊赖之际,我又想起这个仓库,很好奇到底发生了什么,于是我再次打开仓库,仔细研究一番。
\\n先看变更文件类型,只有几个 markdown 文件,查看其中一个 README 的介绍
\\n“本方案采用插件化的适配器模式实现get库鸿蒙化版本的兼容。”,既然是插件化实现,必然要有鸿蒙原生代码,我们知道,Flutter 如果要实现插件化,就需要有平台实现,那么就应该有类似 ohos 的目录工程,类似于下面这样的目录结构:
\\n这是一个标准的 Flutter 插件,webview_flutter 为抽象层,相当于抽象接口(Interface),我们的调用就发生在这里,至于 webview_flutter_ohos, webview_flutter_web 则是每个平台的具体实现(Implement),在 webview_flutter 的 pubspec 文件中,定义了每个平台的实现
\\nflutter:\\n plugin:\\n platforms:\\n android:\\n default_package: webview_flutter_android\\n ios:\\n default_package: webview_flutter_wkwebview\\n ohos:\\n default_package: webview_flutter_ohos\\n
\\n同时将各个平台的实现 package 依赖进来,这种方式通过拆分不同平台实现了解耦。
\\n当然,插件还可以有另外一种实现形式,也就是早期的耦合式结构,将所有的平台实现放在一个 package 里面,类似于这样:
\\n这种结构也有自身的优点,适用于私有项目,方便统一管理,减少项目结构复杂度。
\\n然而,对于现在这个 get 仓库,啥也没有。
\\n还有一个困扰开发者的问题,如何判断一个插件是否需要鸿蒙化,这里可以从几个方面判断:
\\n1.纯 dart 代码自然跨平台,也就不需要单独适配,这是因为 dart vm 已经适配了鸿蒙(基于鸿蒙社区的 Flutter SDK)
\\n2.依赖于原生平台实现的插件需要鸿蒙化,这里的原生平台指的是 ios/android/ohos 等,可以查看插件的代码仓,查看是否有 ohos 目录或 xxx_ohos 的平台包
\\n3.插件本身为纯 dart 实现,但其依赖的插件有可能需要鸿蒙化,这就需要对其依赖的其他插件逐个排查
\\n总结一下都干了啥,一行核心代码没改,改了些无关痛痒的 markdown,版本信息,example 里面增加了鸿蒙的入口,可有可无,这么一通操作,不仅没有实际贡献,还给开发者造成了困扰,不了解的还以为适配鸿蒙平台需要使用这个版本呢,引入以后还容易出错,最终,鸿蒙化了个寂寞。
","description":"在华为牵头下,Flutter 鸿蒙化如火如荼进行,当第一次看到一份上百个插件的Excel 列表时,我也感到震惊,排名前 100 的插件赫然在列,这无疑是一次大规模的军团作战。 然后,参战团队鱼龙混杂,难免有人要浑水摸鱼。\\n\\n某天,一名小伙伴发来一条消息,上来就发来几行代码\\n\\ndependency_overrides:\\n get:\\n git:\\n url: \\"https://gitcode.com/openharmony-sig/fluttertpc_get.git\\"\\n\\n\\n引入以后,出现了以下错误:\\n\\n../../../pub-cache/git…","guid":"https://juejin.cn/post/7487396530657263642","author":"少湖说","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-31T03:36:26.789Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d579246e0f96466797c8ce06ad4191f6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bCR5rmW6K-0:q75.awebp?rk3s=f64ab15b&x-expires=1743996986&x-signature=ZS4vWpNr9XP7yIUWupfjJB9fMoI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/336593b33879453f98ac01ef39e8a7c5~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bCR5rmW6K-0:q75.awebp?rk3s=f64ab15b&x-expires=1743996986&x-signature=Ke3wWAQbdPvfs4Lnmq58eWZ9FWc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4a11852812974c1eb905349b0d954590~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bCR5rmW6K-0:q75.awebp?rk3s=f64ab15b&x-expires=1743996986&x-signature=hEiA%2B2LWdk7UD7IuQ4UsoeU0LJ8%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["前端","HarmonyOS","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 教程(四)包管理","url":"https://juejin.cn/post/7487219933127819273","content":"在 App 开发中,我们往往会依赖很多包,而这些包通常都有交叉依赖关系、版本依赖等,如果由开发者手动来管理应用中的依赖包将会非常麻烦。因此,各种开发生态或编程语言官方通常都会提供一些包管理工具,比如在 Android 提供了 Gradle 来管理依赖,iOS 用 Cocoapods 或 Carthage 来管理依赖,Node 中通过 npm 等。而在 Flutter 中,我们使用 Pub + yaml文件 来实现包的管理。
\\nPub 是 Dart 中的包管理工具,用来管理代码和资源;而 pubspec.yaml
是项目的配置文件。类似于 Android 中的 Gradle 和 build.gradle 文件之间的关系。
Pub 仓库是 Google 官方的 Dart Packages 仓库,类似于 node 中的 npm仓库、Android中的 jcenter。我们可以在 Pub 上面查找我们需要的包和插件,也可以向 Pub 发布我们的包和插件。
\\npubspec.yaml
文件的内容及其字段的作用如下:
# 应用或包名称\\nname: flutter_demo\\n# 应用或包的描述、简介\\ndescription: \\"A new Flutter project.\\"\\n# 此设置可避免项目被意外地通过 flutter pub publish 命令发布到 pub.dev。\\n# 若你有私有项目,这种设置很实用。若要将项目发布到 pub.dev,则需移除这行代码。\\npublish_to: \'none\'\\n# 应用或包的版本号\\nversion: 1.0.0+1\\n# 环境要求\\nenvironment:\\n # 规定了项目所依赖的 Dart SDK 版本\\n sdk: ^3.7.0\\n\\n# 应用或包依赖的其他包或插件\\ndependencies:\\n # 表明项目依赖 Flutter SDK\\n flutter:\\n sdk: flutter\\n # Cupertino 图标字体风格的依赖,可结合 `CupertinoIcons` 类使用 iOS 风格的图标。 \\n cupertino_icons: ^1.0.8\\n\\n# 开发环境依赖的工具包(而不是flutter应用本身依赖的包)\\ndev_dependencies:\\n # flutter 测试\\n flutter_test:\\n sdk: flutter\\n # 代码检查规则 \\n flutter_lints: ^5.0.0\\n\\n# flutter相关的配置选项\\nflutter:\\n # 确保 Material 图标字体随应用一起包含,这样就能使用 Icons 类中的图标。\\n uses-material-design: true\\n
\\n如果我们需要增加新的包依赖,这里以 characters 包为例。第一步,在 Pub 仓库 中找到我们需要的包依赖。如下图所示:
\\n第二步,进入 Installing 项,可以看到使用的步骤,如下图所示:
\\n第三步,安装上面介绍的流程在dependencies
后面增加新的配置,代码示例如下:
# 应用或包依赖的其他包或插件\\ndependencies:\\n # 表明项目依赖 Flutter SDK\\n flutter:\\n sdk: flutter\\n # Cupertino 图标字体风格的依赖,可结合 `CupertinoIcons` 类使用 iOS 风格的图标。 \\n cupertino_icons: ^1.0.8\\n # 增加的包依赖\\n characters: ^1.4.0\\n
\\n第四步,点击 Pub get 将依赖包安装到我们的项目
\\n安装完成之后,我们就可以导入 import \'package:characters/characters.dart\';
包来使用了。
\\n\\n\\n
\\n- Pub get:根据
\\npubspec.yaml
文件中声明的依赖,下载并安装所有必需的包(包括直接依赖和间接依赖)- Pub outdated: 分析当前项目的依赖,列出所有可升级的包及其最新版本
\\n- Pub upgrade: 用于检索当前 Package 所依赖的其它 Package 的最新版本。如果 pubspec.lock 文件已经存在,则忽略其保存的版本并以 pubspec 文件中指定的最新版本为主。
\\n- Flutter doctor: 用于检查和诊断开发环境中的问题。它会列出当前系统中安装的依赖项,并指出任何缺失或配置错误的部分。
\\n
上文所述的依赖方式是依赖Pub仓库的。但我们还可以依赖本地包和git仓库。
\\n如果我们正在本地开发一个包,包名为pkg1,我们可以通过下面方式依赖:
\\ndependencies:\\n pkg1:\\n path: ../../code/pkg1\\n
\\n注意:路径可以是相对的,也可以是绝对的。
\\n你也可以依赖存储在Git仓库中的包。如果软件包位于仓库的根目录中,请使用以下语法
\\ndependencies:\\n pkg1:\\n git:\\n url: git://github.com/xxx/pkg1.git\\n
\\n上面假定包位于Git存储库的根目录中。如果不是这种情况,可以使用path参数指定相对位置,例如:
\\ndependencies:\\n package1:\\n git:\\n url: git://github.com/flutter/packages.git\\n path: packages/package1\\n
\\n上面介绍的这些依赖方式是Flutter开发中常用的,但还有一些其他依赖方式,完整的内容读者可以自行查看:www.dartlang.org/tools/pub/d… 。
\\n对于 Android 开发者而言,在过去声明一个 Activity 时,大多第一件事就是添加一个 android:screenOrientation=\\"portrait\\"
,而其实自 targetSdkVersion ≥ 31
(Android 12),在 2020 年的 Android Studio 3.6 就开始有相关警告:
这是因为从 Android 12 开始,某些可折叠设备会无视用户配置的 screenOrientation ,而是强制 Activity 采用 Letterboxing 模式:
\\n\\n\\n关于这个我们在过去的 《Android 折叠屏适配详解》 聊过,是否进入 Letterboxing 模式和 TargetSDK 版本、 App 配置和屏幕分辨率都有关系,并且不同 OS 版本上 Letterboxing 模式的呈现方式也可能有所不同。
\\n
而从 Android 16 开始,Google 将逐步淘汰用于限制应用屏幕方向和大小调整的清单属性及运行时 API ,而最初生效的设备类型为 \\"大屏\\"场景,即显示区域的最小宽度大于或等于 600dp 的情况(sw >= 600dp) ,也就是:
\\n大屏可折叠设备的内屏
\\n平板电脑,包括桌面窗口模式
\\n桌面环境,包括 Chromebook
\\n具体为 TargetSDK >= 36 之后,在 sw >= 600dp 时,以下配置和 API 将被忽略:
\\n\\n\\n基于
\\nandroid:appCategory
标志的游戏暂时可不受这些变更的影响
也就是,大屏场景下,之前 App 在设置了 screenOrientation
的情况下,App 会是 letterboxed 状态 ,而 Android 16 开始会直接拉成充满:
当然,不升级 targetSDK 就不用适配的想法现在是行不通的,因为现在上架都会开始要求你升级 targetSDK ,另外:
\\n比如在 API 36 时,如果你还是想「摆烂」,那么可以通过配置 PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY
来继续「拖延」适配:
<activity ...>\\n <property android:name=\\"android.window.PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY\\" android:value=\\"true\\" />\\n ...\\n</activity>\\n
\\n<application ...>\\n <property android:name=\\"android.window.PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY\\" android:value=\\"true\\" />\\n</application>\\n
\\n当然,在 API 37 的时候,开发者将无法选择退出,而针对 Google Play 上架场景, 2026 年 8 月前应用需以 API 36 为目标,在 2027 年 8 月前应用需以 API 级别 37 为目标。
\\n\\n\\n另外,在 API 36 target 上,
\\nR.attr#windowOptOutEdgeToEdgeEnforcement
也会被弃用,也就是 App 无法再强制退出 edge-to-edge 模式,如果不做适配,横屏拉伸+ edge-to-edge 的效果看起来应该会很酸爽。
那么,开发者应该如何适配?最简单就是使用响应式布局如 Compose、Flutter 等。
\\n如果是原生端肯定首选 Compose ,使用 material3-window-size-class
库,然后利用 calculateWindowSizeClass()
计算当前窗口的 WindowSizeClass
,从而改变 UI 的布局:
import androidx.activity.compose.setContent\\nimport androidx.compose.material3.windowsizeclass.calculateWindowSizeClass\\n\\nclass MyActivity : ComponentActivity() {\\n override fun onCreate(savedInstanceState: Bundle?) {\\n super.onCreate(savedInstanceState)\\n setContent {\\n // Calculate the window size class for the activity\'s current window. If the window\\n // size changes, for example when the device is rotated, the value returned by\\n // calculateSizeClass will also change.\\n val windowSizeClass = calculateWindowSizeClass(this)\\n // Perform logic on the window size class to decide whether to use a nav rail.\\n val useNavRail = windowSizeClass.widthSizeClass > WindowWidthSizeClass.Compact\\n\\n // MyScreen knows nothing about window size classes, and performs logic based on a\\n // Boolean flag.\\n MyScreen(useNavRail = useNavRail)\\n }\\n }\\n}\\n\\n
\\n另外还可以通过 com.google.accompanist:accompanist-adaptive
的 TwoPane 进行适配,TwoPane
提供了两个固定的槽位,两个槽位的默认位置由 TwoPaneStrategy
驱动,它可以决定将两个槽位水平或垂直排列,并可配置它们之间的间隔:
不同场景 Compose 还可以使用 FlowLayout
适配折叠变化 ,FlowLayout
包含 FlowRow
和 FlowColumn
,当一行(或一列)放不下里边的内容时,会自动换行,这在折叠屏展开和收缩场景也非常实用。
而传统 View 场景,可以使用 Activity Embedding ,理论上 Activity Embedding 不需要代码重构,可以通过创建 XML 配置文件或进行 Jetpack WindowManager API 调用来确定 App 如何显示其 Activity(并排或堆叠) :
\\nJetpack WindowManager 管理和配置 Activity Embedding 其实相当灵活,另外 SlidingPaneLayout
也是一种兼容方式:
当然,你也可以直接使用 Jetpack WindowManager 的 FoldingFeature 等相关信息去自定义适配。
\\n\\n\\n\\n
而现在 Android Studio 的模拟器也已经提供了相应场景支持,对于大多数没有折叠屏设备的开发者来说,模拟器适配是最合适不过的场景:
\\n另外,针对 Flutter 场景,responsive_sizer、flutter_flexible_ui 和 ResponsiveFramework 等框架,在大屏幕设备下提供不错的动态设备支持:
\\n最后,其实不难看出,在前面官方提及的 「桌面窗口模式」等场景,也看出来该操作是在为 Android PC 铺路,对于 Android PC,在集齐了「Linux 终端控制台支持」、「桌面模式」、「外部显示器支持」、「窗口多任务」,「最小化」,「多实例支持」、「Desktop View」、「外部显示器排列和切换」等场景后,在 App 端也终于开始迎来强制性的 UI 适配需求,看起来 Android 团队也重新开始重视 PC 场景,另外还有 Android XR 中的窗口场景,所以针对 Android 的大屏需求,未来只会会越来越多。
那么,接下来你会开始适配,还是选择能拖就拖?
\\n参考链接:
\\n本文接上篇:从0到1掌握Flutter(三)Dart语法
\\n本篇系统讲述了 Dart 语言中方法、类与异常处理机制,包括基本用法、代码示例以及使用场景。重点讲述 Dart 与其他语言的不同之处。
\\n对于具备 Java/Kotlin 背景的学习者,可以通过对比学习法快速定位知识缺口,理解语法的共性。
\\n💡 Dart 的命名规范
\\nmyVariable
, aFunction
。MyClass
。my_constant_value
。💡 方法定义
\\n最规范的写法需要明确声明参数和返回值的类型:
\\nint add(int i, int j) {\\n return i + j;\\n}\\n
\\n虽然 Dart 支持省略类型声明,但这样会降低代码可读性(写法不推荐):
\\nadd(i, j) { // 类型被隐式推断为dynamic\\n return i + j;\\n}\\n
\\n当方法体只有一个表达式时,可以使用箭头语法简化。简化后的箭头函数写法:
\\nint add(int i, int j) => i + j; // 适用于简单返回表达式的情况\\n
\\n这三种写法在实际开发中的优先级应该是:明确类型声明 > 箭头语法简化 > 省略类型声明。
\\n💡 方法类型定义
\\n当你有非常复杂的函数类型,通过 typedef
可以给它们重新取个简短的名字。
typedef ManyOperation = int Function(int firstNum, int secondNum);\\ntypedef IntList = List<int>;\\n\\n// 使用示例:\\nvoid main() {\\n ManyOperation addOperation = (int firstNum, int secondNum) {\\n return firstNum + secondNum;\\n };\\n IntList myNumbers = [1, 2, 3, 4, 5];\\n}\\n
\\n 💡 方法对象
\\n前面在《从0到1掌握Flutter(三)Dart语法》中提到过,在 Dart 语言中万物皆对象,方法不仅仅是一段可执行的代码,本身也是对象,类型是 Function
。
我们可以像操作普通变量一样来处理方法,很像 kotlin 中的方法。这种特性非常灵活,最直观的体现是变量存储方法:
\\n// 声明一个与函数签名匹配的变量\\nFunction calculate;\\n\\n// 将加法函数赋值给变量\\ncalculate = (int a, int b) => a + b;\\n\\n// 通过变量调用方法\\nprint(calculate(3,5)); // 输出 8\\n\\n// 可以随时更换绑定的方法实现\\ncalculate = (int x, int y) => x * y;\\n
\\n这种特性在处理回调时尤其方便。例如 Android 开发是,Java 中要实现点击事件需要定义整个 OnClickListener 接口。但在 Dart 中,我们可以直接把回调方法作为参数传递:
\\n//打印方法\\nvoid log(){\\n print(\'按钮被点击了!\');\\n}\\n\\n// 定义接收回调的方法\\nvoid setClickListener(void Function() onClick) {\\n // 在适当时机触发回调\\n onClick();\\n}\\n\\n// 使用时直接传入方法\\nsetClickListener(log);\\n
\\n 💡 匿名方法
\\n匿名方法既没有名称的方法,这种方法常用于快速定义方法逻辑:
\\n// 定义接收回调的方法\\nvoid setClickListener(void Function() onClick) {\\n // 在适当时机触发回调\\n onClick();\\n}\\n\\n// 使用时直接传入匿名方法\\nsetClickListener((){\\n print(\'按钮被点击了!\'); \\n});\\n\\n//将匿名方法赋值给对象\\nFunction calculate = (int a, int b) => a + b;\\n
\\n💡 私有方法
\\nDart 中没有访问控制修饰符,方法的访问控制是通过命名约定来实现的。声明私有方法需要在方法名前加上下划线 _
。这种命名规范不仅适用于方法,也适用于类成员变量:
class Product {\\n // 外部可访问的公开方法\\n double getFinalPrice() {\\n return _calculateDiscount(basePrice);\\n }\\n\\n // 私有方法\\n double _calculateDiscount(double price) {\\n return price * 0.9; // 打九折\\n }\\n}\\n
\\n 💡 可选命名参数
\\n在 Dart 中,通过花括{}
号包裹的参数为可选命名参数。
这种参数传递方式可以灵活处理参数缺省的情况。来看一个典型应用场景:
\\nint add({int? i, int? j}) {\\n return (i ?? 0) + (j ?? 0);\\n}\\n\\n//调用该方法时,参数的传递方式非常灵活:\\n//完全省略参数:会返回0+0=0\\nadd()\\n//单参数传递:相当于1+0=1\\nadd(i:1)\\n//完整参数传递:得到3,且参数顺序可交换为add(j:2, i:1),结果不变\\nadd(i:1, j:2)\\n
\\n💡 可选位置参数
\\n通过方括号[]
包裹的参数为可选位置参数。传值时按照参数位置顺序传递
int multiply([int? a, int? b]) {\\n // 当任意参数未传递时,视为乘以1(保持原值)\\n final num1 = a ?? 1;\\n final num2 = b ?? 1;\\n return num1 * num2;\\n}\\n\\n// 调用示例:\\nvoid main() {\\n print(multiply()); // 输出:1 (1x1)\\n print(multiply(5)); // 输出:5 (5x1)\\n print(multiply(3,4)); // 输出:12\\n}\\n
\\n 💡 默认参数值
\\n在定义可选命名参数和可选位置参数时,我们可以在定义方法时为参数提供默认值,当调用者不传递该参数时,就会自动使用预设的默认值:
\\n// 位置可选参数形式\\nint addPositional([int i = 1, int j = 2]) => i + j;\\n\\n// 命名可选参数形式\\nint addNamed({int i = 1, int j = 2}) => i + j;\\n
\\n这种设计保证了方法调用的灵活性,又避免了因参数缺失导致的运行时错误,提升了代码健壮性。
\\n💡 必需参数
\\n当我们需要强制调用者传递某个命名参数时,可以使用 required
修饰符。这个标识符会触发编译时检查,确保该参数在调用时必定被传递,否则会产生编译错误。
// 强制要求传递name参数\\nvoid printName({required String name}) {\\n print(\'Hello, $name!\');\\n}\\n\\n// 正确调用\\nprintName(name: \'Alice\'); // 输出:Hello, Alice!\\n\\n// 错误调用(编译不通过)\\n// printName(); ❌ 缺少必需的命名参数\'name\'\\n
\\n💡 初始化列表
\\n当我们需要通过构造函数为类的属性赋值时,常常会重复编写为属性赋值的代码。Dart 专门提供了简洁的语法糖-初始化列表来优化这个流程:
\\nclass Point {\\n num x;\\n num y;\\n\\n // 最简写法:主构造参数直接映射到属性(隐式初始化列表)\\n Point(this.x, this.y);\\n \\n // 命名构造:通过表达式初始化(显式初始化列表)\\n Point.xAxis(int position) : x = position, y = 0; // 根据参数计算初始值\\n \\n // 数据转换构造:从 Map 解析数据初始化\\n Point.fromMap(Map<String, num> data): x = data[\'x\']!, y = data[\'y\']!;\\n\\n // 混合写法:参数绑定与初始化列表组合使用\\n Point.yAxis(this.y) : x = 0; // y 通过参数绑定,x 设置默认值\\n}\\n
\\n💡 命名构造函数
\\n我们知道在 Java 这类语言里可以通过参数不同来重载构造函数,但在 Dart 里这个套路却行不通。当尝试在 Dart 中用传统重载方式写构造函数时:
\\nclass Point {\\n num x;\\n num y;\\n \\n Point(this.x, this.y);\\n \\n Point(this.y); // 这里会直接报错!❌\\n}\\n
\\nDart 准备了另一种更优雅的解决方案——命名构造函数。可以使用 类名.构造名() 的格式定义,它避免了参数命名相同导致的歧义,让代码意图更清晰:
\\nclass Point {\\n num x;\\n num y;\\n \\n // 主构造函数\\n Point(this.x, this.y);\\n \\n // 命名构造函数:当只需要 y 坐标时,自动设置 x=0\\n Point.yAxis(this.y) : x = 0; // 使用初始化列表更高效\\n \\n // 另一个命名构造:创建原点\\n Point.origin() : x = 0, y = 0;\\n}\\n\\n//创建时\\nvar p1 = Point(2, 3); // 主构造函数\\nvar p2 = Point.yAxis(5); // 创建在 y 轴上的点 (0,5)\\nvar origin = Point.origin(); // 轻松获得原点坐标\\n
\\n💡 重定向构造函数
\\n重定向构造函数提供了一种优雅的复用构造函数逻辑的方式。当我们需要用不同的方式创建对象,但本质上都是调用同一个核心构造逻辑时,可以使用这个特性。
\\nclass Temperature {\\n double celsius;\\n \\n // 主构造函数处理摄氏度\\n Temperature(this.celsius);\\n \\n // 重定向构造函数:处理华氏度转摄氏度\\n Temperature.fromFahrenheit(double fahrenheit) \\n : this((fahrenheit - 32) * 5/9); // 转换计算后调用主构造函数\\n \\n // 另一个重定向构造函数:处理开尔文温度\\n Temperature.fromKelvin(double kelvin)\\n : this(kelvin - 273.15);\\n}\\n
\\n在构造函数声明后使用冒号:
连接其他构造函数即可。重定向构造函数本身不能有方法体,它的唯一作用就是把构造请求引导到正确的构造函数去。
💡 常量构造函数
\\n当我们需要创建一些永远不会改变状态的对象时,可以通过常量构造函数将它们定义为编译时常量,这样既能提升性能又能保证数据一致性。
\\n常量构造函数的所有成员变量都必须是 final
修饰的,这样它们初始化后就不能被修改,其次必须使用 const
关键字来定义构造函数:
class Point {\\n final num x; // 固定x坐标\\n final num y; // 固定y坐标\\n \\n // 常量构造函数使用const修饰\\n const Point (this.x, this.y);\\n}\\n
\\n在使用时有个重要特性:当我们用 const
关键字创建多个相同值的对象时,它们实际上会指向同一个内存实例:
void main() {\\n // 使用const创建两个相同坐标点\\n var p1 = const ImmutablePoint(0, 0);\\n var p2 = const ImmutablePoint(0, 0);\\n \\n print(p1 == p2); // 输出true,说明是同一个实例\\n}\\n
\\n💡 工厂构造函数
\\n工厂构造函数是 Dart 中控制对象创建流程的特殊工具,他有三个主要的作用:
\\n它简化了简单工厂场景的实现,相当于内置的轻量级工厂方案。
\\n例如开发一个日志系统,需要确保相同名称的日志器只创建一个实例:
\\nclass Logger {\\n final String name;\\n static final cache = <String, Logger>{};\\n\\n // 工厂构造函数像智能管家\\n factory Logger(String name) {\\n if (cache.containsKey(name)) {\\n print(\'从缓存取出$name\');\\n return cache[name]!;\\n } else {\\n final newLogger = Logger._create(name);\\n cache[name] = newLogger;\\n return newLogger;\\n }\\n }\\n\\n Logger._create(this.name);\\n}\\n
\\n再来看单例模式的经典实现。其实同样的方法用工厂构造和静态方法都能实现:
\\nclass AppManager {\\n static AppManager _singleton;\\n\\n // 工厂构造版本\\n factory AppManager() {\\n _singleton ??= AppManager._init();\\n return _singleton;\\n }\\n\\n // 静态方法版本\\n static AppManager get instance {\\n _singleton ??= AppManager._init();\\n return _singleton;\\n }\\n\\n AppManager._init();\\n}\\n
\\n这两种方式在使用时的区别很直观:
\\n// 工厂构造的调用方式更符合直觉\\nvar mgr1 = AppManager();\\n\\n// 静态方法需要显式调用\\nvar mgr2 = AppManager.instance;\\n
\\n虽然效果相同,但工厂构造的语法更贴近常规的对象创建方式,对外提供简洁统一的构造接口。
\\n💡 Getters 和 Setters
\\n在 Dart 中,get 和 set 是特殊类型的方法。get 用于读取属性值,set 用于设置属性值。它们和普通方法的主要区别是语法和使用方法。
\\n使用 get/set 访问属性,就像直接访问变量一样。例如:
\\nclass Rectangle {\\n num x; // 左边界\\n num y; // 上边界\\n num width;\\n num height;\\n Rectangle(this.x, this.y, this.width, this.height);\\n // 动态计算右边界\\n num get rightEdge => x + width;\\n \\n // 修改右边界时同步更新左边界\\n set rightEdge(num value) => x = value - width;\\n}\\n\\nvoid main() {\\n final rect = Rectangle(0, 0, 10, 10);\\n print(rect.rightEdge); // 使用get\\n rect.rightEdge = 15; //使用set\\n}\\n
\\n如果没有使用 get/set,那需要创建特定的方法来读取和设置属性:
\\nclass Rectangle {\\n num x; // 左边界\\n num y; // 上边界\\n num width;\\n num height;\\n Rectangle(this.x, this.y, this.width, this.height);\\n \\n // 动态计算右边界\\n num getRightEdge() => x + width;\\n \\n // 修改右边界时同步更新左边界\\n void setRightEdge(num value) {\\n x = value - width;\\n }\\n}\\n\\nvoid main() {\\n final rect = Rectangle(0, 0, 10, 10);\\n print(rect.getRightEdge()); // 使用 getRightEdge 方法\\n rect.setRightEdge(15); // 使用 setRightEdge 方法\\n}\\n
\\n 💡 操作符重载
\\n在 Dart 语言中,允许类自定义某些操作符的行为。可以在类中覆盖操作符,比如加法操作符 \'+\',减法操作符 \'-\' 等。你只需要在类中定义一个方法来实现操作符重载,方法名就是你想要重载的操作符前面加上 operator
关键字,例如:
class Vector {\\n final int x, y;\\n Vector(this.x, this.y);\\n //操作符重载\\n Vector operator +(Vector v) {\\n return Vector(x + v.x, y + v.y);\\n }\\n}\\n\\nvoid main() {\\n Vector v1 = Vector(1, 3);\\n Vector v2 = Vector(2, 2);\\n\\n Vector v3 = v1 + v2;//直接使用 + 即可\\n print(\'v3: (${v3.x}, ${v3.y})\');//打印 v3: (3, 5)\\n}\\n
\\n💡 抽象类
\\n定义抽象类时需要使用 abstract
修饰符,其中可以包含具体实现的成员和抽象方法。这里有个注意点:抽象方法不需要额外添加 abstract
关键字,这点和 Java 不同:
abstract class BasePerson {\\n String name;\\n // 抽象方法,子类必须实现\\n void printProfile();\\n}\\n
\\n抽象类本身不能被直接实例化,但可以通过工厂方法创建实现类实例:
\\nabstract class BasePerson {\\n String name;\\n BasePerson(this.name);\\n // 工厂方法返回具体子类实例\\n factory BasePerson.create(String name) {\\n return Developer(name);\\n }\\n void printProfile();\\n}\\n\\n// 继承抽象类必须实现所有抽象方法\\nclass Developer extends BasePerson {\\n Developer(String name) : super(name);\\n\\n @override\\n void printProfile() {\\n print(\'开发工程师: $name\');\\n }\\n}\\n\\n//实际使用时,通过工厂方法创建实例:\\nvoid main() {\\n var person = BasePerson.create(\\"XX\\");\\n print(person.runtimeType); // 输出: Developer\\n person.printProfile(); // 输出: 开发工程师: XX\\n}\\n
\\n💡 隐式接口
\\nDart 中有一个和 Java 显著不同的点:Dart 中没有 interface 关键字,每个类都会自动生成对应的接口模板。
\\n当我们想让一个类完整遵循另一个类的行为规范,但又不希望继承其具体实现时,就可以使用 implements
关键字来实现这个隐式接口:
// 抽象类可作为接口使用\\nabstract class EventCallback {\\n void onSuccess();\\n void onError();\\n}\\n\\n// 具体类也可以作为接口使用\\nclass OtherCallback {\\n void onStart(){\\n print(\'开始\');\\n }\\n void onEnd(){\\n print(\'结束\');\\n }\\n}\\n\\n// 实现时必须完整覆盖所有接口方法\\nclass CustomCallback implements EventCallback,OtherCallback {\\n @override\\n void onSuccess() => print(\'处理成功逻辑\');\\n\\n @override\\n void onError() => print(\'记录错误日志\');\\n\\n @override\\n void onEnd() {\\n print(\'CustomCallback 开始\');\\n }\\n\\n @override\\n void onStart() {\\n print(\'CustomCallback 结束\');\\n }\\n}\\n
\\n实现接口与继承的区别为:
\\nsuper
调用父类原始实现;而接口实现则是强制性的,必须完整实现接口定义的所有成员。💡 Mixins 混入
\\nMixins 是一种强大的代码复用机制,特别适合需要组合多个类功能的场景。它的核心原理是通过 with
关键字将多个类的功能\\"混合\\"到当前类中,形成一种特殊的多继承结构:
// 注意:被混入的类不能包含构造函数\\nclass Bird {\\n void fly() => print(\\"展翅高飞\\");\\n}\\n\\nclass Fish {\\n void swim() => print(\\"畅游水中\\");\\n}\\n\\n// 通过 with 关键字混合两个类\\nclass FlyingFish with Bird, Fish {\\n void specialSkill() => print(\\"水空两栖\\");\\n}\\n
\\n这里创建的 FlyingFish 实例将同时拥有 fly(), swim() 和 specialSkill() 三个方法。
\\n当混合的类存在方法冲突时,Dart 采用\\"后来居上\\"的覆盖原则:
\\nclass Morning {\\n String greet() => \\"早安!\\";\\n}\\n\\nclass Evening {\\n String greet() => \\"晚安!\\";\\n}\\n\\n// 混合顺序决定方法优先级\\nclass HybridGreeter with Morning, Evening {}\\n\\nvoid main() {\\n print(HybridGreeter().greet()); // 输出\\"晚安!\\"\\n}\\n
\\n当与继承结合使用时,也遵循\\"后来居上\\"的覆盖原则:
\\nclass Afternoon {\\n String greet() => \\"午安!\\";\\n}\\n\\n// 混合顺序决定方法优先级\\nclass HybridGreeter extends Afternoon with Morning, Evening {}\\n\\nvoid main() {\\n print(HybridGreeter().greet()); // 输出\\"晚安!\\"\\n}\\n\\n// 等价简写形式:\\n// class HybridGreeter = Afternoon with Morning, Evening;\\n
\\nMixins 解决了传统 OOP 的局限性,既突破了单继承限制,又避免了接口需要重复实现的缺点。
\\n这种特性在构建需要组合多种行为的复杂对象时尤为有用,比如游戏角色系统、跨领域能力模型等场景。
\\n💡 扩展方法
\\n扩展方法允许向已有的类添加新的功能,同时不需要创建该类的子类或修改该类。
\\n在 Dart 文件中使用关键词 extension
,后跟扩展名称,关键词 on
,以及你希望扩展的类。然后,中括号里包含你添加的方法即可。
例如,要给 List<int>
添加一个求和的扩展方法,可写成:
extension Sum on List<int> {\\n int sum() {\\n return this.fold(0, (current, next) => current + next);\\n }\\n}\\n
\\n即使它原来的定义中并没有这个方法,也可以直接调用 List<int>.sum()
方法:
void main() {\\n print([1, 2, 3].sum()); // 输出 6\\n}\\n
\\n 💡 可调用类
\\n在 Dart 中,一个类定义了 call()
方法,那么该类的实例可以像函数一样被调用用:
class ExampleClass {\\n String call(String name, int age) {\\n return \'Name: $name, Age: $age\';\\n }\\n}\\n\\nvoid main() {\\n var example = ExampleClass();\\n var result = example(\'John\', 20);\\n print(result); // 输出: Name: John, Age: 20\\n}\\n
\\n Dart 的异常机制与 Java 不同,与 kotlin 类似,是非检查型的,开发者不需要在方法签名中声明可能抛出的异常类型,编译器也不会强制要求处理特定异常,这种设计更灵活但也需要开发者更主动地处理潜在问题。
\\n💡 Exception 与 Error
\\n在异常类型方面,Dart 的基础架构提供了 Exception 和 Error 两个核心类型。
\\nException
:通常表示程序正常逻辑中可以预期的错误
Error
:表示程序设计或逻辑上的错误,如数组越界、null 引用等。
💡 异常抛出
\\nDart 的异常抛出机制非常灵活,任何非空对象都可以作为异常抛出,不局限于特定类型。这种设计在某些场景下能简化代码:
\\n// 抛出标准异常类型\\nthrow Exception(\'数据解析异常\');\\n// 直接抛出字符串\\nthrow \'网络连接中断\';\\n// 抛出数字代码\\nthrow 404;\\n// 甚至可以抛出方法\\nthrow (){\\n \\n};\\n
\\n 💡 异常处理
\\n当处理异常时,Dart 采用了 on-catch 组合语法判断错误类型:
\\ntry {\\n} on Exception catch (e) {\\n//捕获Exception \\n} on int catch (e) {\\n//捕获int类型\\n} on Function catch (e) {\\n//捕获方法\\n} catch (e) {\\n//捕获其他异常\\n rethrow; // 重新抛出给上层\\n} finally {\\n\\n}\\n
\\non
关键字可以针对特定异常类型进行处理,类似 Java 的 catch 块类型声明catch
语句可以接收两个参数:异常对象和堆栈追踪信息rethrow
关键字允许在当前处理完成后继续传播异常本篇围绕方法、类、异常处理三大核心模块讲述,巩固了 Flutter 开发的基础编程能力。
\\n总的来看,Dart 方法的灵活参数设计提升了代码复用性,类的封装与继承实现了业务逻辑的高效组织,异常处理则为程序稳定性提供保障。建议读者后续通过实际项目练习巩固。
","description":"引言 本文接上篇:从0到1掌握Flutter(三)Dart语法\\n\\n本篇系统讲述了 Dart 语言中方法、类与异常处理机制,包括基本用法、代码示例以及使用场景。重点讲述 Dart 与其他语言的不同之处。\\n\\n对于具备 Java/Kotlin 背景的学习者,可以通过对比学习法快速定位知识缺口,理解语法的共性。\\n\\n\\n\\n\\n💡 Dart 的命名规范\\n\\n变量名、函数名和方法名: 使用 小驼峰 命名格式。例如 myVariable, aFunction。\\n类名: 使用 大驼峰 命名格式,即所有单词的首字母全部大写,如 MyClass。\\n常量、文件名、库和包名: 小写字母和下划线…","guid":"https://juejin.cn/post/7487114886289834038","author":"A0微声z","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-30T14:43:41.382Z","media":null,"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Riverpod源码分析3:Provider的观察、刷新与销毁","url":"https://juejin.cn/post/7487219720480489535","content":"这一节我们主要关注ProviderElement
,也是Riverpod
的核心部分。我们通过watch
观察、read
读取、listen
注册回调、invalidate
刷新都要通过它。
老样子,我会以最基础的ProviderElement
为例讲解。
Ref
和WidgetRef
Ref
: ProviderElementBase
implements Ref
Provider
的_createFn(Ref ref)
方法中接收的参数就是本provider
对应的providerElement
实例。ProviderElementBase
实现了Ref
接口,通过它我们可以读取/监听某个Provider
的值,刷新、无效化某个provider
等等。
以下是Ref
的主要方法,我会把它们的作用以注释的方式写出
// Ref就是ProviderElementBase\\nabstract class Ref<\\n @Deprecated(\'Will be removed in 3.0\') State extends Object?> {\\n \\n // 本ProviderElement所属的container\\n ProviderContainer get container;\\n \\n // 立刻重新计算某个Provider的值,等价于invalidate + read\\n T refresh<T>(Refreshable<T> provider);\\n \\n // 令某个或一系列provider失效\\n void invalidate(ProviderOrFamily provider);\\n \\n // 通知监听本provider的其他provider,我刷新了\\n void notifyListeners();\\n \\n // 无效化自己\\n void invalidateSelf();\\n \\n // ---- 生命周期相关\\n\\n // 有新监听者注册\\n void onAddListener(void Function() cb);\\n \\n // 有监听者移除\\n void onRemoveListener(void Function() cb);\\n \\n // 从0到1,有一个监听者开始监听本provider时\\n void onResume(void Function() cb);\\n \\n // 最后一个监听者被移除时触发(没有被废弃)\\n void onCancel(void Function() cb);\\n \\n // 本Element被废弃\\n void onDispose(void Function() cb);\\n \\n // ---- 读取其他provider\\n\\n // 读取某个provider的值\\n T read<T>(ProviderListenable<T> provider);\\n \\n // 判断某个ProviderElement是否被初始化过\\n bool exists(ProviderBase<Object?> provider);\\n \\n // 观察某个provider,对方变化时自己要重新计算\\n T watch<T>(ProviderListenable<T> provider);\\n \\n // 保活\\n KeepAliveLink keepAlive();\\n \\n // 监听某个provider,对方变化时执行回调listener\\n ProviderSubscription<T> listen<T>(\\n ProviderListenable<T> provider,\\n void Function(T? previous, T next) listener, {\\n void Function(Object error, StackTrace stackTrace)? onError,\\n // 是否立刻执行一次回调\\n bool fireImmediately,\\n });\\n}\\n
\\nWidgetRef
: ConsumerStatefulElement
extends StatefulElement
implements WidgetRef
StatefulElement
就是BuildContext
,所以WidgetRef
实际上就是BuildContext
。
WidgetRef
方法和Ref
大同小异,区别在于Ref.watch
是刷新provider,而WidgetRef.watch
是通过markNeedsBuild
刷新UI。
ProviderElementBase
通常我们会用Ref
或WidgetRef
从另一个provider
中拿到数据。这里我们以WidgetRef.read(provider)
开始过一遍流程。
_StateReader
// `WidgetRef.read`\\n@override\\nT read<T>(ProviderListenable<T> provider) {\\n return ProviderScope.containerOf(this, listen: false).read(provider);\\n}\\n\\n// `ProviderContainer.read`\\nResult read<Result>(ProviderListenable<Result> provider) {\\n return provider.read(this);\\n}\\n\\n// `ProviderBase.read`, `Node`是`ProviderContainer`\\nStateT read(Node node) {\\n // `Node`是`ProviderContainer`。这里会初始化ProviderElement\\n final element = node.readProviderElement(this);\\n // 刷新Provider的值\\n element.flush();\\n element.mayNeedDispose();\\n return element.requireState;\\n}\\n
\\nProviderContainer#readProviderElement
在第二篇文章也说过,就是通过_StateReader#getElement
懒加载ProviderElement
。我们再贴一遍_StateReader
中相关的代码:
// 使用已有的Element或初始化\\nProviderElementBase getElement() => _element ??= _create();\\n\\n// 创建新的ProviderElement\\nProviderElementBase _create() {\\n final element = override.createElement()\\n .._provider = override\\n .._origin = origin\\n .._container = container\\n ..mount();\\n element.getState()\\n return element;\\n}\\n
\\n从这里开始正式进入ProviderElementBase
内部。
mount()
& buildState()
当ProviderElementBase
被初始化后,_StateReader
随即会调用其mount
方法。
void mount() {\\n _mounted = true;\\n buildState();\\n}\\n
\\n// ProviderElementBase#buildState()\\nvoid buildState() {\\n final previousDidChangeDependency = _didChangeDependency;\\n _didChangeDependency = false;\\n _didBuild = false;\\n try {\\n _mounted = true;\\n // 执行Provider的_createFn()\\n create(didChangeDependency: previousDidChangeDependency);\\n } catch (err, stack) {\\n _state = Result.error(err, stack);\\n } finally {\\n _didBuild = true;\\n }\\n}\\n
\\n对于create()
方法,每种Provider
对应的Element
实现均不相同。我们以ProviderElement
为例:执行provider
的_createFn
方法,把得到的值通过setState()
设置给自己,同时触发回调。
@override\\nvoid create({required bool didChangeDependency}) {\\n final provider = this.provider as InternalProvider<State>;\\n // 会触发回调,见3.3\\n setState(provider._create(this));\\n}\\n
\\nmount()
结束后,这个ProviderElementBase
就算是正式初始化完成了。
ProviderElement
的唯一时机就是通过ProviderContainer
去使用它时,即ProviderContainer#readProviderElement
方法ProviderElementBase
内部的初始化流程大概是这样的:mount()
-> buildState()
-> create()
,create()
又会执行provider
的_createFn
。provider A
需要依赖其他provider B
,那么A
必然要在其_createFn(Ref)
方法中通过ref.watch/read
去监听B
,又会触发ProviderContainer#readProviderElement
...这样,provider
就被递归的初始化。listen
/watch
:监听ProviderElement
ProviderElementBase
中使用列表来保存自己监听和监听自己的订阅者
// 自己监听(listen)其他ProviderElement所返回的Subscription,上级\\nList<ProviderSubscription>? _subscriptions;\\n\\n// 其他Provider监听自己的Subscription,下级\\nList<ProviderSubscription>? _dependents;\\n\\n// 观察(watch)自己的ProviderElement\\nfinal _providerDependents = <ProviderElementBase<Object?>>[];\\n
\\nref.listen
:注册回调ref.listen
的工作原理是:把一个回调listener
添加到想要监听的ProviderElementBase
的回调列表_dependents
中。当上级ProviderElementBase
的状态(_state
)发生改变时就会触发这个回调。
WidgetRef
的watch
也是通过listen实现的,只是注册了一个markNeedsLayout()
回调。
// `ProviderBase#addListener`\\n@override\\nProviderSubscription<StateT> addListener(\\n Node node,\\n void Function(StateT? previous, StateT next) listener, {\\n required void Function(Object error, StackTrace stackTrace)? onError,\\n required void Function()? onDependencyMayHaveChanged,\\n required bool fireImmediately,\\n}) {\\n // node是ProviderContainer\\n final element = node.readProviderElement(this);\\n // 刷新element以获取最新值\\n element.flush();\\n // 立刻执行一次回调\\n if (fireImmediately) {\\n handleFireImmediately(\\n element.getState()!,\\n listener: listener,\\n onError: onError,\\n );\\n }\\n \\n // 触发onAddListener/onResume回调。 todo 为什么从外部触发?\\n element._onListen();\\n \\n // 返回一个ProviderSubscription。node是ProviderContainer,\\n // listenedElement是监听的ProviderElement\\n // 在构造方法中把自己添加到被监听ProviderElement的所属\\n return _ProviderStateSubscription<StateT>(\\n node,\\n listenedElement: element,\\n listener: (prev, next) => listener(prev as StateT?, next as StateT),\\n onError: onError,\\n );\\n}\\n
\\n只要通过Ref
或WidgetRef
监听一个provider
,都会返回一个ProviderSubscription
。通过它我们可以随时获取provider
的值或取消监听。
// 在构造方法中把自己添加到被监听ProviderElement的所属\\nclass _ProviderStateSubscription<StateT> extends ProviderSubscription<StateT> {\\n _ProviderStateSubscription(\\n super.source, {\\n required this.listenedElement,\\n required this.listener,\\n required this.onError,\\n }) {\\n final dependents = listenedElement._dependents ??= [];\\n dependents.add(this);\\n }\\n\\n final void Function(Object? prev, Object? state) listener;\\n final ProviderElementBase<StateT> listenedElement;\\n final OnError onError;\\n\\n // 获取监听的ProviderElement的值\\n @override\\n StateT read() => listenedElement.readSelf();\\n \\n // 取消订阅\\n @override\\n void close() {\\n if (!closed) {\\n listenedElement._dependents?.remove(this);\\n listenedElement._onRemoveListener();\\n }\\n super.close();\\n }\\n}\\n
\\nlisten
方法结束后,会返回一个ProviderSubscription
对象。可以通过它读取监听的Provider
的最新值,也可以用它取消回调。同时它还保存了我们传入的listener
回调,保存在监听的ProviderElement
中。
ref.watch
:观察另一个ProviderElement
watch
方法会将观察的provider
添加到自己的依赖中,并把自己添加到需要监听的ProviderElement
的_providerDependents
列表中,形成双向绑定的关系。
@override\\nT watch<T>(ProviderListenable<T> listenable) {\\n final element = _container.readProviderElement(listenable);\\n // _dependencies是自己的上级\\n _dependencies.putIfAbsent(element, () {\\n final previousSub = _previousDependencies?.remove(element);\\n if (previousSub != null) {\\n return previousSub;\\n }\\n\\n element\\n // 触发一次回调\\n .._onListen()\\n // 把自己添加到上级的_providerDependents列表中\\n .._providerDependents.add(this);\\n\\n return Object();\\n });\\n // 返回对方当前的值\\n return element.readSelf();\\n}\\n
\\n_state
改变时,如何通知监听者_dependents
中保存的回调,触发listen
ProviderElement
会通过让下级(即观察watch
自己的Provider
)失效(invalidateSelf
)的方式触发它们的重新计算。setState
:改变状态想改变ProviderElement
的状态(或者说它的值)就必须通过setState(Result<T> result)
方法。Result
是一个包装类,内容可能是正常值 value
或异常 error,stackTrace
。修改后会通过_notifyListeners
触发回调、通知下级。
void setState(StateT newState) {\\n final previousResult = getState();\\n // 修改_state\\n final result = _state = ResultData(newState);\\n _notifyListeners(result, previousResult);\\n}\\n
\\n_notifyListeners
:触发回调,响应watch/listen void _notifyListeners(\\n Result<StateT> newState,\\n Result<StateT>? previousStateResult, {\\n bool checkUpdateShouldNotify = true,\\n }) {\\n\\n final previousState = previousStateResult?.stateOrNull;\\n\\n // 如果实际值没有变化则不通知,实现select选择性刷新的效果\\n if (checkUpdateShouldNotify &&\\n previousStateResult != null &&\\n previousStateResult.hasState &&\\n newState.hasState &&\\n !updateShouldNotify(\\n previousState as StateT,\\n newState.requireState,\\n )) {\\n return;\\n }\\n\\n final listeners = _dependents?.toList(growable: false);\\n\\n // listeners中存储了ProviderSubscription,其中保存了listener回调\\n if (listeners != null) {\\n for (var i = 0; i < listeners.length; i++) {\\n final listener = listeners[i];\\n if (listener is _ProviderStateSubscription) {\\n listener.listener(previousState,newState.state),\\n }\\n }\\n }\\n\\n // _providerDependents中存储着下级ProviderElement\\n for (var i = 0; i < _providerDependents.length; i++) {\\n // 标记它们的依赖发生变化,实际上就是invalidateSelf()\\n _providerDependents[i]._markDependencyChanged();\\n }\\n }\\n
\\nvoid _markDependencyChanged() {\\n _didChangeDependency = true;\\n if (_mustRecomputeState) return;\\n invalidateSelf();\\n}\\n
\\ninvalidate
注意:无效化(invalidate
)并不等于废弃(dispose
)。如果某个Provider不是autoDisposed
,那么在它所属的作用域销毁之前它不会被销毁。
ProviderElement
中的值_state
需要重新计算,执行ref.onDispose()
注册的回调ProviderElement
。ref.invalidate(ProviderOfFamily provider)
我们直接快进到ProviderContainer
的相关方法。invalidate
接收一个ProviderOrFamily
参数。
void invalidate(ProviderOrFamily provider) {\\n if (provider is ProviderBase) {\\n final reader = _getOrNull(provider);\\n reader?._element?.invalidateSelf();\\n } else {\\n // 会无效化通过ProviderFamily创造的所有provider\\n provider as Family;\\n\\n final familyContainer =\\n _overrideForFamily[provider]?.container ?? _root ?? this;\\n\\n for (final stateReader in familyContainer._stateReaders.values) {\\n if (stateReader.origin.from != provider) continue;\\n stateReader._element?.invalidateSelf();\\n }\\n }\\n}\\n
\\nelement.invalidateSelf()
:使自己失效// 标记自己需要重新计算\\n@override\\nvoid invalidateSelf() {\\n if (_mustRecomputeState) return;\\n _mustRecomputeState = true;\\n // 清理回调及各种注册\\n runOnDispose();\\n // 自动销毁的Provider会重写这个方法,否则为空\\n mayNeedDispose();\\n // 本帧结束后,如果hasListener不为false,执行flush方法\\n // 当自己被listen或watch时,hasListener为true\\n _container.scheduler.scheduleProviderRefresh(this);\\n // 通知孩子依赖可能被更改,修改它们的标记位\\n visitChildren(\\n elementVisitor: (element) => element._markDependencyMayHaveChanged(),\\n notifierVisitor: (notifier) => notifier.notifyDependencyMayHaveChanged(),\\n );\\n}\\n
\\nrunOnDispose
: 清理、执行一部分回调注意_providerDependents(下级)
和_dependencies(listen回调)
列表没有被清空。hasListener
可能返回true。
为什么要清理回调和生命周期相关方法?是因为所有回调都是在Provider的_createFn
中注册的,而无效化需要重新执行_createFn
以获取到最新的值。所以提前清理、再次注册可以防止注册多次回调。
void runOnDispose() {\\n if (!_mounted) return;\\n _mounted = false;\\n \\n final subscriptions = _subscriptions;\\n if (subscriptions != null) {\\n while (subscriptions.isNotEmpty) {\\n final sub = subscriptions.first;\\n // close会把自己从_subscriptions列表中移除,所以不会无限循环\\n sub.close();\\n }\\n }\\n \\n // 执行onDispose\\n _onDisposeListeners?.forEach(runGuarded);\\n // 清理生命周期方法,再次create时会重新注册\\n _onDisposeListeners = null;\\n _onCancelListeners = null;\\n _onResumeListeners = null;\\n _onAddListeners = null;\\n _onRemoveListeners = null;\\n _onChangeSelfListeners = null;\\n _onErrorSelfListeners = null;\\n _didCancelOnce = false;\\n}\\n
\\n到这里,本帧就结束了,但是ProviderElement
的状态依然没有被刷新。默认情况下,ProviderScheduler
会在下一帧统一执行ProviderElementBase
的flush
方法。
_flush
、建立依赖下一帧开始。buildState
会调用provider
的_createFn
,重新注册listener
、建立依赖关系。
@internal\\nvoid flush() {\\n // 这句有什么用?\\n _maybeRebuildDependencies();\\n if (_mustRecomputeState) {\\n _mustRecomputeState = false;\\n _performBuild();\\n }\\n}\\n\\n\\nvoid _maybeRebuildDependencies() {\\n if (!_dependencyMayHaveChanged) return;\\n _dependencyMayHaveChanged = false;\\n visitAncestors(\\n (element) => element.flush(),\\n );\\n}\\n
\\n\\n\\n注意这里:
\\nprovider
自身需要重建,为什么还需要刷新自己的上级?\\n其实这里和刷新机制的性能优化有关,我也是想了好一会才想到的。看到这的同学可以自己想一想,答案在第5段。
buildState
(见2.2)会重新执行_createFn
,注册回调、建立依赖关系,返回最新的值
void _performBuild() {\\n final previousDependencies = _previousDependencies = _dependencies;\\n _dependencies = HashMap();\\n\\n final previousStateResult = _state;\\n\\n buildState();\\n \\n // identical比较两个对象是否为同一个对象,这里一定会返回false\\n if (!identical(_state, previousStateResult)) {\\n // 把自己添加到待刷新列表\\n _notifyListeners(_state!, previousStateResult);\\n }\\n\\n // 把自己从下级的依赖列表中移除。因为会下级又会重新观察\\n for (final sub in previousDependencies.entries) {\\n sub.key\\n .._providerDependents.remove(this)\\n .._onRemoveListener();\\n }\\n _previousDependencies = null;\\n}\\n
\\n这一小节我会举几个例子说明Riverpod
的刷新流程。看一看Riverpod
内部是如何处理刷新过程、优化性能、避免不必要的刷新的。
graph LR\\nProvider_A --\x3e Provider_B\\n
\\n代表B
监听了A
,refB.watch(providerA)
我们假设每个Provider
的内容都是获取上一级Provider
的值,返回其+1
graph LR\\nStateProvider_A --\x3e Provider_B --\x3e Provider_C\\n
\\n\\n\\nQ:
\\nStateProvider_A
变化时,B
和C
是如何变化的?
A:没有Widget
监听Provider_C
,它的ProviderElement
根本不会被创建,其_createFn
也不会执行;同理,Provider_B
也没有被监听,其Element
也未被创建。它们不会有任何变化。
要修改A,就要通过ProviderContainer.readProviderElement
获取A
对应的ProviderElement
。所以A会被创建、修改。
graph LR\\nStateProvider_A --\x3e Provider_B --\x3e Provider_C --\x3e Provider_D --\x3e WidgetRef.watch\\n
\\n\\n\\nQ:当
\\nStateProvider_A
的值发生改变,B
C
D
是如何变化的?已知invalidateSelf
每次都只会把自己的直接后继加入到ProviderScheduler
,现在有3个Provider
,会在1帧内完成刷新还是3帧?
A:B C D
会在同一帧刷新。
当前帧:
\\n当A
变化的那一帧,A
会通过setState() -> _notifyListeners() -> B# invalidateSelf() -> _container.scheduler.scheduleProviderRefresh(this)
,把自己的下级(这里是B
)添加到ProviderScheduler
待刷新列表中。下一帧开始刷新。
下一帧
\\nProviderScheduler
会执行_performRefresh
方法,在for
循环内开始执行Element B
的flush
方法。此时,_stateToRefresh
内只有Element B
,长度为1。
void _performRefresh() {\\n for (var i = 0; i < _stateToRefresh.length; i++) {\\n // 此刻_stateToRefresh内只有Element B,长度为1\\n final element = _stateToRefresh[i];\\n if (element.hasListeners) element.flush();\\n }\\n}\\n
\\nflush
方法会刷新自己的值,再调用下级C
的invalidateSelf
// B的内部\\n void flush() {\\n _performBuild();\\n }\\n\\n void _performBuild() {\\n buildState();\\n _notifyListeners(_state!, previousStateResult);\\n }\\n\\n\\n void _notifyListeners(\\n Result<StateT> newState,\\n Result<StateT>? previousStateResult, {\\n bool checkUpdateShouldNotify = true,\\n }) {\\n for (var i = 0; i < _providerDependents.length; i++) {\\n // B的下级为C\\n _providerDependents[i]._markDependencyChanged();\\n }\\n }\\n\\n
\\n以下会执行ProviderElement C
的invalidateSelf()
,将其添加到ProviderScheduer._stateToRefresh
列表中,现在列表长度为2
// 执行C的`invalidateSelf`\\nvoid _markDependencyChanged() {\\n _didChangeDependency = true;\\n if (_mustRecomputeState) return;\\n\\n // will notify children that their dependency may have changed\\n invalidateSelf();\\n}\\n\\nvoid invalidateSelf() {\\n // C将自己添加到待刷新列表中\\n _container.scheduler.scheduleProviderRefresh(this);\\n}\\n
\\n此时_performRefresh
内的for
循环继续,对Element C
、Element D
重复相同的过程,最终D
执行widget注册的回调(WidgetRef.watch
是用listen
实现的)
可以看到随着for循环的执行,_stateToRefresh
需要刷新的Provider越来越多。这个断点需要打在ProviderScheduler
的_performRefresh
方法上。
graph LR\\nStateProvider_C --\x3e Provider_D\\nStateProvider_A --\x3e Provider_B --\x3e Provider_D --\x3e WidgetRef.watch\\n
\\n\\n\\nQ: 先修改
\\nC
,再修改A
,刷新流程是怎样的?Provider D会刷新两次吗?
答: D
只会刷新一次,这里用到了4.3小节中提到过的_maybeRebuildDependencies()
,过程如下
当前帧:
\\nC
会调用D
的invalidateSelf
,把D
加入待刷新列表;A
会调用B
的invalidateSelf
,把B
加入待刷新列表,同时B
标记D
的依赖可能发生改变。@override\\nvoid invalidateSelf() {\\n _container.scheduler.scheduleProviderRefresh(this);\\n // B的下级是D,通知孩子依赖可能被更改\\n visitChildren(\\n elementVisitor: (element) => element._markDependencyMayHaveChanged(),\\n notifierVisitor: (notifier) => notifier.notifyDependencyMayHaveChanged(),\\n );\\n}\\n
\\n现在_stateToRefresh
内的Element依次是 [D,B]
,D
的_dependencyMayHaveChanged == true
下一帧:
\\nD
的flush
方法,进入_maybeRebuildDependencies()
,由于标记位为true
,所以D会尝试刷新自己的上级,即C
和B
// D的flush方法\\nvoid flush() {\\n _maybeRebuildDependencies();\\n if (_mustRecomputeState) {\\n _mustRecomputeState = false;\\n _performBuild();\\n }\\n}\\n\\n// 刷新自己的上级\\nvoid _maybeRebuildDependencies() {\\n if (!_dependencyMayHaveChanged) return;\\n _dependencyMayHaveChanged = false;\\n // D的上级是C和B,会调用它们的flush方法\\n visitAncestors(\\n (element) => element.flush(),\\n );\\n}\\n
\\nC
的_mustRecomputeState
为false,跳过刷新B
还未被刷新(在待刷新列表中,B排在D后面),会执行B
的flush
方法D
的_maybeRebuildDependencies
执行完,B和C都是最新状态,刷新自己for
循环走到下一步,尝试刷新B
。但是它已经被刷新过,_mustRecomputeState == false
,跳过刷新过程这里time是当前毫秒时间戳,可以看到是同一帧内刷新,刷新顺序也是先B后D,且只刷新一次
","description":"这一节我们主要关注ProviderElement,也是Riverpod的核心部分。我们通过watch观察、read读取、listen注册回调、invalidate刷新都要通过它。 老样子,我会以最基础的ProviderElement为例讲解。\\n\\n1.Ref和WidgetRef\\n1.1 Ref: ProviderElementBase implements Ref\\n\\nProvider的_createFn(Ref ref)方法中接收的参数就是本provider对应的providerElement实例。ProviderElementBase实现了Ref接口,通过它…","guid":"https://juejin.cn/post/7487219720480489535","author":"嘿嘿嘿呼呼嘿","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-30T14:17:01.297Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3208380505cb43c7bac886aafc415ca0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zi_5Zi_5Zi_5ZG85ZG85Zi_:q75.awebp?rk3s=f64ab15b&x-expires=1743949021&x-signature=Onj7IVZWGnwXQFzJt3VdZjWoWJs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/320692aeec564027a43287e0dab64aba~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zi_5Zi_5Zi_5ZG85ZG85Zi_:q75.awebp?rk3s=f64ab15b&x-expires=1743949021&x-signature=NAK7pIBUeMPPUWUVhLRdgVi%2BoLE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4df75eeca8ee44528971fe042e09e35e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zi_5Zi_5Zi_5ZG85ZG85Zi_:q75.awebp?rk3s=f64ab15b&x-expires=1743949021&x-signature=neYzlLfJb6YPngh%2BHTJUvcKaES8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ba6c15ff262f437593223b0a4167cf18~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zi_5Zi_5Zi_5ZG85ZG85Zi_:q75.awebp?rk3s=f64ab15b&x-expires=1743949021&x-signature=kyCCbYYSaA4QPn2f4T%2FxaXVXjYs%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Riverpod源码分析2:作用域 ProviderScope","url":"https://juejin.cn/post/7487071171194814475","content":"ProviderScope
可以理解成Provider
的作用域,其内部会提供一个ProviderContainer
负责维护ProviderElement
。
provider
,其状态会被保存在顶层ProviderScope
里ProviderScope
覆写(override
)的provider
,其状态会被保存在当前的作用域中Provider
的provider
,它会和自身依赖所处层级最深的provider
对象处在相同的作用域中\\n\\n\\n
\\n
collectionIdProvider
就是一个被覆写的provider,它的状态会保存在当前ProviderScope
中,而不是顶层。
ProviderScope
和ProviderContainer
整体较为简单,它们只负责管理ProviderElement
,真正监听、刷新的逻辑都在ProviderElementBase
里。
ProviderContainer
ProviderScope
:向下提供ProviderContainer
ProviderScope
是一个StatefulWidget
。它会创建一个新的ProviderContainer
、获取上级的ProviderContainer
、并将它作为新Container
的parent
参数。
build
方法会创建一个InheritedWidget
:UncontrolledProviderScope
,以向下传递ProviderContainer
。
_StateReader
:ProviderElement
的容器ProviderContainer
不会直接保存ProviderElement
,而是保存_StateReader
对象。同时_StateReader
也负责懒加载ProviderElement
。
_StateReader
中会保存
provider
对象override
对象,是一个Provider
ProviderElement
所属的ProviderContainer
对象isDynamicallyCreated
class _StateReader {\\n _StateReader({\\n required this.origin,\\n required this.override,\\n required this.container,\\n // 是否为动态创建\\n required this.isDynamicallyCreated,\\n });\\n\\n final ProviderBase<Object?> origin;\\n ProviderBase<Object?> override;\\n final ProviderContainer container;\\n final bool isDynamicallyCreated;\\n\\n ProviderElementBase? _element;\\n\\n // element是懒加载的\\n ProviderElementBase getElement() => _element ??= _create();\\n\\n // 初始化ProviderElement的地方\\n ProviderElementBase _create() {\\n try {\\n final element = override.createElement()\\n .._provider = override\\n .._origin = origin\\n .._container = container\\n ..mount();\\n return element;\\n } finally {}\\n }\\n}\\n
\\nProviderContainer
的创建过程ProviderContainer
是在ProviderScope
中被创建的,ProviderScope
又会把 上级parent
、本级的overrides
传递给所属的ProviderContainer
。这里我们看看ProviderContainer
是怎么处理这些参数的。
ProviderContainer({\\n ProviderContainer? parent,\\n List<Override> overrides = const [],\\n List<ProviderObserver>? observers,\\n}) : depth = parent == null ? 0 : parent.depth + 1,\\n _parent = parent,\\n // 借用parent中非动态创建的_stateReader\\n _stateReaders = {\\n if (parent != null)\\n for (final entry in parent._stateReaders.entries)\\n if (entry.value.isDynamicallyCreated == false) entry.key: entry.value,\\n },\\n _root = parent?._root ?? parent {\\n if (parent != null) {\\n parent._children.add(this);\\n _overrideForFamily.addAll(parent._overrideForFamily);\\n }\\n\\n for (final override in overrides) {\\n if (override is ProviderOverride) {\\n // 保存被覆写的provider\\n _overrideForProvider[override._origin] = override._override;\\n // 提前创建被覆写的Provider所对应的_StateReader\\n _stateReaders[override._origin] = _StateReader(\\n origin: override._origin,\\n override: override._override,\\n container: this,\\n isDynamicallyCreated: false,\\n );\\n } else if (override is FamilyOverride) {\\n _overrideForFamily[override.overriddenFamily] = _FamilyOverrideRef(\\n override,\\n this,\\n );\\n }\\n }\\n}\\n
\\n根据这段代码,我们可以知道“作用域”的原理:
\\noverrides
参数,提前创建并保存对应的_StateReader
。这些被提前创建的_StateReader
又被称为“非动态创建”,isDynamicallyCreated == false
。_StateReader
。这里“借用”的意思是_StateReader
仍属于创建它的ProviderContainer
;它的container
参数仍是上级而不是本级;当本ProviderContainer
销毁时借来的_StateReader
不受影响ProviderElement
:_putIfAbsent()
之前我们提到ProviderElementBase
被保存在_StateReader
中。因此,获取ProviderElementBase
首先就要获取其对应的_StateReader
。考虑到ProviderScope
可能存在嵌套关系,不同类型的Provider
在_StateReader
中的存储和获取方式会有所不同:
Provider
,其对应的ProviderElement
应该存放在顶层ProviderContainer
。Provider
,其Element
应该在当前层。Provider
,其Element
应该存放在它所依赖被覆写的Provider
的最深层级。第3条看起来有点绕,举个例子就明白了。collectionInfo
依赖于collectionId
,而collectionId
在ProviderScope
中被覆写,此时collectionInfoProvider
就是一个自身没有被覆写但依赖了被覆写的Provider
的provider
。collectionInfoProvider
和collectionIdProvider
所对应的ProviderElement
在同一层。\\n\\n
_StateReader
由ProviderContainer#_putIfAbsent
创建,我会把步骤注释在代码中。
_StateReader
对于本级已经保存的对应的_StateReader
,直接取出并返回,否则进入getReader()
方法。
_StateReader _putIfAbsent(ProviderBase<Object?> provider) {\\n // 已保存在本层,直接返回\\n final currentReader = _stateReaders[provider];\\n if (currentReader != null) return currentReader;\\n\\n _StateReader getReader() {\\n // 3.1 .....\\n // 3.2 .....\\n }\\n // 取出并缓存获取到的_StateReader,这样下一次就不用再跑麻烦的getReader方法了\\n return _stateReaders[provider] = getReader();\\n
\\n3.1 & 3.2 小节都是getReader()
方法的一部分
ProviderFamily
if (provider.from != null) {\\n // reading a family\\n\\n final familyOverrideRef = _overrideForFamily[provider.from];\\n if (familyOverrideRef != null) {\\n // A family was overridden, so we implicitly mount the readers\\n\\n if (familyOverrideRef.container._stateReaders.containsKey(provider)) {\\n return familyOverrideRef.container._stateReaders[provider]!;\\n }\\n\\n void setupOverride({\\n required ProviderBase<Object?> origin,\\n required ProviderBase<Object?> override,\\n }) {\\n assert(\\n origin == override || override.dependencies == null,\\n \'A provider override cannot specify `dependencies`\',\\n );\\n\\n // setupOverride may be called multiple times on different providers\\n // of the same family (provider vs provider.modifier), so we use ??=\\n // to initialize the providers only once\\n familyOverrideRef.container._stateReaders[origin] ??= _StateReader(\\n origin: origin,\\n override: override,\\n container: familyOverrideRef.container,\\n isDynamicallyCreated: true,\\n );\\n }\\n\\n final providerOverride =\\n familyOverrideRef.override.getProviderOverride(provider);\\n\\n setupOverride(origin: provider, override: providerOverride);\\n\\n // if setupOverride overrode the provider, it was already initialized\\n // in the code above. Otherwise we initialize it as if it was not overridden\\n return familyOverrideRef.container._stateReaders[provider] ??\\n _StateReader(\\n origin: provider,\\n override: provider,\\n container: familyOverrideRef.container,\\n isDynamicallyCreated: true,\\n );\\n }\\n}\\n
\\nProvider
final root = _root;\\n if (root != null) {\\n // Provider可能依赖了其他的Provider,找出依赖\\n final dependencies = provider.from?.allTransitiveDependencies ??\\n provider.allTransitiveDependencies;\\n \\n // 找到它们所在的ProviderContainer\\n final containerForDependencyOverride = dependencies\\n ?.map((dep) {\\n final reader = _stateReaders[dep];\\n if (reader != null) {\\n return reader.container;\\n }\\n final familyOverride = _overrideForFamily[dep];\\n return familyOverride?.container;\\n })\\n .where((container) => container != null)\\n .toList();\\n\\n // 如果有,找出层级最深的那个container\\n if (containerForDependencyOverride != null &&\\n containerForDependencyOverride.isNotEmpty) {\\n \\n final deepestOverrideContainer = containerForDependencyOverride\\n .fold<ProviderContainer>(root, (previous, container) {\\n if (container!.depth > previous.depth) {\\n return container;\\n }\\n return previous;\\n });\\n \\n // 新创建的ProviderElementBase应该存放到依赖层级最深的ProviderContainer中,放大作用域\\n return deepestOverrideContainer._stateReaders.putIfAbsent(provider,\\n () {\\n return _StateReader(\\n origin: provider,\\n override: provider,\\n container: deepestOverrideContainer,\\n isDynamicallyCreated: true,\\n );\\n });\\n }\\n } // end if (root != null)\\n // 到这里,如果这个provider显式依赖了其他的provider,那么它对应的_StateReader已经被创建并返回了\\n\\n // 否则,这个provider没有依赖任何其他的provider。在根container中寻找\\n if (_root?._stateReaders.containsKey(provider) ?? false) {\\n return _root!._stateReaders[provider]!;\\n }\\n\\n // 最顶层ProviderContainer中也没有这个provider对应的element。创建并将它放到最顶层中\\n final reader = _StateReader(\\n origin: provider,\\n // If a provider did not have an associated StateReader then it is\\n // guaranteed to not be overridden\\n override: provider,\\n // 当自己为根节点时,_root才为空\\n container: _root ?? this,\\n isDynamicallyCreated: true,\\n );\\n\\n if (_root != null) {\\n _root!._stateReaders[provider] = reader;\\n }\\n\\n return reader;\\n}\\n
\\n以上面举过的例子它为例,collectionId
对应的_StateReader
会属于图2的ProviderScope
(而不是最顶层)中,而collectionInfo
由于依赖了id
,它对应的ProviderElement
也在相同的ProviderContainer
中。
图1里有一个appRepoProvider
,它是全局的且不依赖其他provider
,所以不管在哪里使用它,他都属于顶层ProviderContainer
。
read
/监听watch
/无效化invalidate
这其实是ProviderElement
中的内容,但是它们都用到了ProviderContainer
中的readProviderElement
方法。这个方法通过_putIfAbsent
找到当前层级的ProviderElement
,后续操作交由Element
完成。
@override\\n ProviderElementBase<State> readProviderElement<State>(ProviderBase<State> provider) {\\n final reader = _putIfAbsent(provider);\\n return reader.getElement() as ProviderElementBase<State>;\\n }\\n
\\nProviderContainer
获取所有属于本container
的ProviderElement
并释放它们。还记得开头提到“借用”的概念吗?被借用的_StateReader
所有权仍属于它们原本的ProviderContainer
,释放时会跳过他们。
void dispose() {\\n if (_disposed) return;\\n\\n _disposed = true;\\n // 从上级ProviderContainer的孩子列表中移除自己\\n _parent?._children.remove(this);\\n // 如果是根节点,负责停止调度器\\n if (_root == null) scheduler.dispose();\\n\\n // 释放属于自己的ProviderElement\\n for (final element in getAllProviderElementsInOrder().toList().reversed) {\\n element.dispose();\\n }\\n}\\n\\n// 获取属于自己的ProviderElement\\nIterable<ProviderElementBase> getAllProviderElements() sync* {\\n for (final reader in _stateReaders.values) {\\n // 跳过借用的_StateReader\\n if (reader._element != null && reader.container == this) {\\n yield reader._element!;\\n }\\n }\\n}\\n
\\nProviderScheduler
和FlutterElement
中的BuildOwner
类似,ProviderContainer
使用调度器来集中刷新ProviderElement
。
ProviderScheduler
中维护了两个列表,分别记录了需要刷新和销毁的ProviderElement
// 需要销毁\\nfinal _stateToDispose = <AutoDisposeProviderElementMixin<Object?>>[];\\n// 需要刷新\\nfinal _stateToRefresh = <ProviderElementBase>[];\\n
\\n当某个ProviderElement需要被刷新时,就会通过
\\nvoid scheduleProviderRefresh(ProviderElementBase element) {\\n _stateToRefresh.add(element);\\n _scheduleTask();\\n}\\n
\\n把自己加入到待刷新列表中,同时尝试调度一次任务。释放dispose
同理。
注意:在_task
执行时scheduleProviderRefresh
也可能再次被调用。
_task
、调度_scheduleTask
和分帧vsync
_task
_task
主要内容就是执行以下两个方法:按顺序刷新/销毁ProviderElement
。
依次刷新。如果某个Provider
没有监听者,跳过刷新。
void _performRefresh() {\\n for (var i = 0; i < _stateToRefresh.length; i++) {\\n final element = _stateToRefresh[i];\\n if (element.hasListeners) element.flush();\\n }\\n}\\n
\\n依次销毁。如果某个ProviderElement
被保活/仍有监听者,跳过销毁。
void _performDispose() {\\n for (var i = 0; i < _stateToDispose.length; i++) {\\n final element = _stateToDispose[i];\\n\\n final links = element._keepAliveLinks;\\n\\n // 被保活/有监听者,跳过销毁\\n if (element.maintainState ||\\n (links != null && links.isNotEmpty) ||\\n element.hasListeners ||\\n element._container._disposed) {\\n continue;\\n }\\n element._container._disposeProvider(element._origin);\\n }\\n}\\n
\\n_scheduleTask
_pendingTaskCompleter
可以理解成一个标记位,不为空表示已经有任务,跳过调度
void _scheduleTask() {\\n if (_pendingTaskCompleter != null || _disposed) return;\\n _pendingTaskCompleter = Completer<void>();\\n vsync(_task);\\n}\\n
\\nvsync
vsync
是一个参数为方法的方法。执行时会把接受的方法包装到Future
中,而Future
会在下一帧开始(SchedulerPhase.idle
)时调用。
void _defaultVsync(void Function() task) {\\n Future(task);\\n}\\n\\nvoid Function(void Function()) get vsync {\\n return _defaultVsync;\\n}\\n
","description":"1.概述 ProviderScope可以理解成Provider的作用域,其内部会提供一个ProviderContainer负责维护ProviderElement。\\n\\n对于全局的provider,其状态会被保存在顶层ProviderScope里\\n对于通过ProviderScope覆写(override)的provider,其状态会被保存在当前的作用域中\\n对于依赖了多个其他Provider的provider,它会和自身依赖所处层级最深的provider对象处在相同的作用域中\\n\\ncollectionIdProvider就是一个被覆写的provider…","guid":"https://juejin.cn/post/7487071171194814475","author":"嘿嘿嘿呼呼嘿","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-30T14:14:53.934Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4c6a9a350bd54b929dc607cada165fcf~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zi_5Zi_5Zi_5ZG85ZG85Zi_:q75.awebp?rk3s=f64ab15b&x-expires=1743948893&x-signature=rHAYZmhRwYKo078TZ786yJK0r%2Fw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/690b653c4fc6481ca50f71f0e060d97d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zi_5Zi_5Zi_5ZG85ZG85Zi_:q75.awebp?rk3s=f64ab15b&x-expires=1743948893&x-signature=lU9b4hDmOHtBgADe5ENy7lbCNTo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3e420f204b9942a090c631975712c346~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zi_5Zi_5Zi_5ZG85ZG85Zi_:q75.awebp?rk3s=f64ab15b&x-expires=1743948893&x-signature=xpXQMZZ8iI9sQI8guQ5QfFQOqBA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f853c97ffb794eeea7c5c4d08c1533aa~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zi_5Zi_5Zi_5ZG85ZG85Zi_:q75.awebp?rk3s=f64ab15b&x-expires=1743948893&x-signature=pemzeQGDhUBW677GahIS1XCYnX0%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Riverpod源码分析1:Provider & ProviderFamily","url":"https://juejin.cn/post/7487542928133308479","content":"Riverpod
是一个响应式的状态管理及缓存框架。如果你用NotifierProvider
和AsyncNotifierProvider
,它用起来就像Android里的ViewModel
+ StateFlow
的结合体;或者也可以用FutureProvider
或Provider
,去监听某个值的变化以起到缓存的作用。
Provider作为Flutter第一大状态管理框架,它的诸多不便我相信只要用过就能感觉得到。相较于Provider,Riverpod的实用性和易用性都大大的提高了。和Provider
对比,Riverpod
有以下优势:
Provider
一样,一个Provider恨不得给你一万种创建方法。select
,不更新值就不刷新。Provider里写个 Selector6 老费劲了,7个泛型。provider
的值,哪怕需要监听一万个值Riverpod实现一样简洁。对应Provider
里的ProxyProvider 1 2 3 4 5 6。我自己一直在用Riverpod
,但是网上一直没有它的源码解析,可能国内用Riverpod的人不多。没办法只能自己看着写,不明白工作原理用起来心里总是没底。水平有限,希望来个大神。
\\n\\n本文基于Riverpod 2.6.1
\\n
Provider
源码分析Riverpod
的设计参考了Flutter
中的Widget
- Element
树,Provider
只是作为配置,实际上的工作由ProviderElement
完成,能写的东西实在不多。
Provider
的作用:
ProviderElement
提供_createFn
方法,创建/刷新自己的状态、注册回调。createElement()
方法,根据Provider的类型返回不同种类的ProviderElement
。这篇文章中我会以最简单的Provider
为例分析它的源码。实际上所有的Provider
都继承自ProviderBase
,它们基本的逻辑是相同的。
\\n\\n\\n
通常来说这两种写法是等价的。Riverpod的作者推荐使用上面的注解+代码生成的写法。
\\n
_createFn
与dependencies
_createFn(Ref ref)
决定读取这个Provider
时返回的值。在上文的例子中返回值恒定为1。
可以通过Ref
观测(watch
)其他Provider
的值,当观测的provider
的值发生变化时,本provider
的_createFn
会重新执行,得到修改后的值。
当该provider
监听(watch
)/读取(read
)了一个被覆写(override
)的provider
时,需要在dependencies
中声明,这样Riverpod会尝试从最近(而不是最顶层)的ProviderScope
中找到对应的状态。
\\n\\n\\n
dependencies
涉及到作用域的知识,可以参考下一篇文章。
Provider#overrideWith(Ref ref)
一部分Provider
可以 在ProviderScope
中 被覆写(override
)以改变其逻辑,返回一个新的provider
。覆写后的Provider
仅在其所属的作用域ProviderScope
中生效。
如下图所示,在ProviderScope
的child
中读取collectionIdProvider
时,返回的是被覆写的id。
\\n\\n并不是所有的
\\nprovider
都支持override
。
ProviderBase
所有的Provider
都继承自ProviderBase
。因为实在没什么好说的,我把参数的作用写成注释打在源码里了
@immutable\\nabstract class ProviderBase<StateT> extends ProviderOrFamily\\n with ProviderListenable<StateT>\\n implements ProviderOverride, Refreshable<StateT> {\\n \\n const ProviderBase({\\n // 这个Provider的名字\\n required super.name,\\n // 来自哪个ProviderFamily,或为空\\n required this.from,\\n // 通过ProviderFamily接收的参数,或为空\\n required this.argument,\\n required this.debugGetCreateSourceHash,\\n // 直接依赖\\n required super.dependencies,\\n // 直接和间接依赖\\n required super.allTransitiveDependencies,\\n });\\n\\n // 用作key的Provider,ProviderScope通过它找到对应的ProviderElement\\n @override\\n ProviderBase<Object?> get _origin => this;\\n\\n // 提供_createFn的Provider,可能由于覆写的原因和_origin不是同一个\\n @override\\n ProviderBase<Object?> get _override => this;\\n\\n // 省略read和listen的逻辑,实际上交由`ProviderElement`处理\\n \\n // Provider子类实现,创建对应的ProviderElement\\n ProviderElementBase<StateT> createElement();\\n}\\n
\\nProviderFamily
:可以接收参数的Provider
...?事实上ProviderFamily
并不是Provider
。ProviderFamily
是一个工厂,根据传入的参数args
生成对应的provider
。判断通过ProviderFamily
产生的provider
实例是否相同的方法是比较ProviderFamily
和参数是否等同。
\\n\\n\\n
通常来说这两种写法是等价的。泛型1是该Provider返回值,泛型2是该ProviderFamily接收参数的类型。可以把Provider.family理解成ProviderFamily。
\\n不同类型的Provider对应不同类型的Family。
\\n
通过ProviderFamily
生成的Provider
,它的from
参数是对应的family
,而argument
参数是_createFn(ref,arg)
中额外的参数arg
。
如上图所示,idProvider
实际上是一个接收一个int
参数的ProviderFamily
。当我们调用idProvider(3)
时,实际上调用的就是FamilyBase
的call
方法,根据传入的providerFactory
和_createFn
生成对应的Provider
。
ProviderFamily
继承自FamilyBase
。_createFn(Ref,Arg)
是我们创建ProviderFamily
时传入的方法,providerFactory
是Provider.internal
,Provider
的构造方法之一。
@internal\\nclass FamilyBase<RefT extends Ref<R>, R, Arg, Created,\\n ProviderT extends ProviderBase<R>> extends Family<R>\\n with _FamilyMixin<R, Arg, ProviderT> {\\n const FamilyBase(\\n this._createFn, {\\n required ProviderCreate<ProviderT, Created, RefT> providerFactory,\\n required this.name,\\n required this.dependencies,\\n required this.allTransitiveDependencies,\\n required this.debugGetCreateSourceHash,\\n }) : _providerFactory = providerFactory;\\n\\n // 这里是Provider.internal,Provider的构造方法之一\\n final ProviderCreate<ProviderT, Created, RefT> _providerFactory;\\n\\n // 我们自己定义的 (ref,int) => \'${i + 1}\';\\n final Created Function(RefT ref, Arg arg) _createFn;\\n\\n // 执行call方法,返回新的Provider\\n @override\\n ProviderT call(Arg argument) => _providerFactory(\\n (ref) => _createFn(ref, argument),\\n name: name,\\n // 普通的Provider,from和argument均为null\\n from: this,\\n argument: argument,\\n dependencies: dependencies,\\n allTransitiveDependencies: allTransitiveDependencies,\\n debugGetCreateSourceHash: debugGetCreateSourceHash,\\n );\\n\\n @override\\n final String? name;\\n @override\\n final Iterable<ProviderOrFamily>? dependencies;\\n @override\\n final Set<ProviderOrFamily>? allTransitiveDependencies;\\n}\\n
","description":"1. intro & motivation Riverpod是一个响应式的状态管理及缓存框架。如果你用NotifierProvider和AsyncNotifierProvider,它用起来就像Android里的ViewModel+ StateFlow的结合体;或者也可以用FutureProvider或Provider,去监听某个值的变化以起到缓存的作用。\\n\\nProvider作为Flutter第一大状态管理框架,它的诸多不便我相信只要用过就能感觉得到。相较于Provider,Riverpod的实用性和易用性都大大的提高了。和Provider对比,Riverp…","guid":"https://juejin.cn/post/7487542928133308479","author":"嘿嘿嘿呼呼嘿","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-30T14:12:13.127Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c7218157baba43e58f06e28f691b7fde~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zi_5Zi_5Zi_5ZG85ZG85Zi_:q75.awebp?rk3s=f64ab15b&x-expires=1743948733&x-signature=Zgw%2BvaaqZgdX%2BLHprqRaBNPg8j0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7f31a93e75114fe2881a2983172f9256~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zi_5Zi_5Zi_5ZG85ZG85Zi_:q75.awebp?rk3s=f64ab15b&x-expires=1743948733&x-signature=Butu1KuRIxxUd0%2ByYyzYydz0ixk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f880c14df208484d952dda5560e705ac~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zi_5Zi_5Zi_5ZG85ZG85Zi_:q75.awebp?rk3s=f64ab15b&x-expires=1743948733&x-signature=UeadO5%2BomiNVx6B8H17didzysSc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1fbd6e826c2b44f6943041f591151a28~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zi_5Zi_5Zi_5ZG85ZG85Zi_:q75.awebp?rk3s=f64ab15b&x-expires=1743948733&x-signature=eTH68wjLyhnDAP3E7icg5Jmw4JA%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter Linux应用初探","url":"https://juejin.cn/post/7487118743085727770","content":"\\n\\n距离我上一篇文章,足足过去一年!!!
\\n
\\n断更是艰难的过程,日常斥责自己没有作品。除了工作的忙碌、技术栈重心的变化外,AI的崛起带来技术交流平台的低迷,也是让我疲于更新的原因之一
\\n近期重新投入Flutter技术,适配了Linux平台,才让我重新燃起奋笔疾书的欲望。Flutter for Linux在社区中的文章是非常之少的,期待这篇文章能给大家带来一些思考~
此次我是对旧项目进行Linux平台的适配,这个项目在Android和Windows平台已经顺利发布
运行两年。因此这里省去创建运行项目的说明。
\\n两年前创建的项目,期间跟随Flutter版本升级到3.22。在Linux平台的首次运行,竟然一次就顺利跑起来了。这让我十分的欣喜,而后不断思考:为何Flutter在Linux能如此的顺利运行?
Flutter Linux的载体是一个典型的GtkApplication
。在main主入口,创建了MyApplication实例并运行应用程序。
#include \\"my_application.h\\"\\n\\nint main(int argc, char** argv) {\\n g_autoptr(MyApplication) app = my_application_new();\\n return g_application_run(G_APPLICATION(app), argc, argv);\\n}\\n
\\n在my_application.h中,使用G_DECLARE_FINAL_TYPE宏定义了MyApplication
的类型继承自GtkApplication
。
G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, GtkApplication)\\n
\\n创建了在my_application后,自然就会按顺序的执行GtkApplication的生命周期。
\\n应用程序的主要生命周期包含以下几个关键阶段:
激活(Activate)
:这是最重要的阶段。 \\n总的来说,Flutter在Linux下的运行完全是依赖于GTK框架,通过以下步骤实现:
Flutter的engine和view是怎么跟GtkApplication关联上的呢?核心代码都在GApplication::activate
的钩子中。
g_autoptr(FlDartProject) project = fl_dart_project_new();\\n
\\nfl_dart_project_set_dart_entrypoint_arguments
把启动参数,设置到Flutter层fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);\\n
\\n FlView* view = fl_view_new(project);\\n gtk_widget_show(GTK_WIDGET(view));\\n gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));\\n
\\n fl_register_plugins(FL_PLUGIN_REGISTRY(view));\\n
\\n整个过程与GTKWinodw是比较脱离的,跟Android FlutterActivity、Windows FlutterWindow
的实现思路一模一样。
\\n这也证明Flutter是个很纯粹的跨平台UI框架,脱离原生框架的束缚。所以3年前的项目,Linux端一次运行成功也就不足为奇了~
Flutter的更新迭代是非常快的,并且桌面的支持也力不从心,所以对于一个新的平台来说,在开始适配的时候,一定要升级到最新版本,一定要用最新!!!
\\nFlutter是跨平台的UI,那么窗口的属性自然就无法快速去操作,比如:设置无标题栏、设置大小、居中等。
\\n这里我们也不推荐在Flutter层面使用window_manager去操作,从性能和显示的实时效果出发,就应该在c++层处理完成
\\n以下代码,为Flutter应用设置了依据分辨率适配大小、居中、隐藏标题栏、设置透明底等。
// 获取屏幕分辨率\\ngboolean GetScreenRect(gint *width, gint *height) {\\n GdkDisplay *display = gdk_display_get_default();\\n if (display) {\\n GdkMonitor *monitor = gdk_display_get_primary_monitor(display);\\n if (monitor) {\\n GdkRectangle geometry;\\n gdk_monitor_get_geometry(monitor, &geometry);\\n *width = geometry.width;\\n *height = geometry.height;\\n return TRUE;\\n }\\n }\\n return FALSE;\\n}\\n\\n// 获取DPI\\ngint GetDpi() {\\n GdkScreen *screen = gdk_screen_get_default();\\n if (screen) {\\n return gdk_screen_get_resolution(screen);\\n }\\n return 96; // 默认DPI\\n}\\n\\nstatic gboolean on_draw_event(GtkWidget *widget, cairo_t *cr,\\n gpointer user_data)\\n{\\n cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR);\\n cairo_paint(cr);\\n return FALSE;\\n}\\n\\nstatic void transparent_setup(GtkWidget *win)\\n{\\n GdkScreen *screen;\\n GdkVisual *visual;\\n\\n gtk_widget_set_app_paintable(win, TRUE);\\n screen = gdk_screen_get_default();\\n visual = gdk_screen_get_rgba_visual(screen);\\n\\n if (visual != NULL && gdk_screen_is_composited(screen)) {\\n gtk_widget_set_visual(win, visual);\\n g_signal_connect(G_OBJECT(win), \\"draw\\", G_CALLBACK(on_draw_event), NULL);\\n }\\n}\\n\\n// Implements GApplication::activate.\\nstatic void my_application_activate(GApplication* application) {\\n MyApplication* self = MY_APPLICATION(application);\\n GtkWindow* window =\\n GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));\\n\\n // Use a header bar when running in GNOME as this is the common style used\\n // by applications and is the setup most users will be using (e.g. Ubuntu\\n // desktop).\\n // If running on X and not using GNOME then just use a traditional title bar\\n // in case the window manager does more exotic layout, e.g. tiling.\\n // If running on Wayland assume the header bar will work (may need changing\\n // if future cases occur).\\n gboolean use_header_bar = TRUE;\\n#ifdef GDK_WINDOWING_X11\\n GdkScreen* screen = gtk_window_get_screen(window);\\n if (GDK_IS_X11_SCREEN(screen)) {\\n const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);\\n if (g_strcmp0(wm_name, \\"GNOME Shell\\") != 0) {\\n use_header_bar = FALSE;\\n }\\n }\\n#endif\\n if (use_header_bar) {\\n GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());\\n gtk_widget_show(GTK_WIDGET(header_bar));\\n gtk_header_bar_set_title(header_bar, \\"SystemUpgradeMain\\");\\n gtk_header_bar_set_show_close_button(header_bar, TRUE);\\n gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));\\n } else {\\n gtk_window_set_title(window, \\"SystemUpgradeMain\\");\\n }\\n\\n // 设置窗口透明\\n transparent_setup(GTK_WIDGET(window));\\n // 隐藏标题栏\\n gtk_window_set_decorated(GTK_WINDOW(window), FALSE);\\n // 设置窗口居中\\n gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);\\n\\n // 获取缩放因子\\n double scale_factor;\\n gint screenWidth, screenHeight;\\n auto default_resolution = 1.0 * 1920 / 1080;\\n if (GetScreenRect(&screenWidth, &screenHeight)) {\\n auto current_resolution = 1.0 * screenWidth / screenHeight;\\n if (current_resolution > default_resolution) {\\n scale_factor = 1.0 * screenHeight / 1080;\\n } else {\\n scale_factor = 1.0 * screenWidth / 1920;\\n }\\n } else {\\n gint dpi = GetDpi();\\n scale_factor = dpi / 96.0;\\n }\\n std::cout << \\"scale_factor: \\" << scale_factor << std::endl;\\n // 设置窗口大小\\n gtk_window_set_default_size(window, 1172*scale_factor, 731*scale_factor);\\n\\n gtk_widget_show(GTK_WIDGET(window));\\n gtk_widget_set_visible(GTK_WIDGET(window), FALSE);\\n\\n g_autoptr(FlDartProject) project = fl_dart_project_new();\\n fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);\\n\\n FlView* view = fl_view_new(project);\\n gtk_widget_show(GTK_WIDGET(view));\\n gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));\\n\\n GdkRGBA background_color;\\n gdk_rgba_parse(&background_color, \\"#ffffff\\");\\n fl_view_set_background_color(view, &background_color);\\n\\n fl_register_plugins(FL_PLUGIN_REGISTRY(view));\\n\\n gtk_widget_grab_focus(GTK_WIDGET(view));\\n}\\n
\\nFlutter Linux的相关文章,全网都非常少见,其原因跟Flutter在Linux的投入,Linux系统下Flutter的应用生态都有所关系。好在官方的源代码文档,还是比较完整的:Flutter Linux源码。
\\n在给Linux窗口设置透明背景时,我们就遇到了不少坑。
cairo_paint
把GTKWindow绘制成透明的,上层的FlutterView依然不透明。static gboolean on_draw_event(GtkWidget *widget, cairo_t *cr,\\n gpointer user_data)\\n{\\n cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR);\\n cairo_paint(cr);\\n return FALSE;\\n}\\n\\nstatic void transparent_setup(GtkWidget *win)\\n{\\n GdkScreen *screen;\\n GdkVisual *visual;\\n\\n gtk_widget_set_app_paintable(win, TRUE);\\n screen = gdk_screen_get_default();\\n visual = gdk_screen_get_rgba_visual(screen);\\n\\n if (visual != NULL && gdk_screen_is_composited(screen)) {\\n gtk_widget_set_visual(win, visual);\\n g_signal_connect(G_OBJECT(win), \\"draw\\", G_CALLBACK(on_draw_event), NULL);\\n }\\n}\\n
\\nLinux App在国内的应用场景是比较少的,但随着接下来设备国产化的战略继续推进,我相信Flutter Linux会有进一步的需求。但是从生态上来看,不会C++的团队,在Flutter For Linux的道路上,是会遇到比较多的困难的。
\\nAnyway,在国内鸿蒙化、国产化;世界范围AI编程、AOSP停止维护的大背景下,衷心希望Flutter桌面端越来越好吧~
Android Studio的版本是Ladybug Feature Drop,patch版本是2024.2.2。Flutter的版本如下图所示:
\\n\\ngradle-wrapper.properties中定义的gradle的版本是:gadle-8.10.2-bin.zip。
Flutter可以作为源代码Gradle子项目或AAR逐个嵌入到您现有的Android应用程序中。可以使用带有 Flutter插件的Android Studio IDE或手动完成集成流程。首先需要创建Flutter Module,可以使用命令行,也可以使用Android Studio。注意:Flutter Module的目录和Android项目中的app Module是在同一个父目录下面,也就是说Flutter Module和app Module是在Android项目的同一个层级。Flutter要求你的项目声明与Java 11或更高版本兼容。app Module需要声明Java版本,示例如下:
\\nandroid{\\ncompileOptions {\\n sourceCompatibility = JavaVersion.VERSION_11\\n targetCompatibility = JavaVersion.VERSION_11\\n }\\n}\\n \\n
\\nFlutter Module编译成aar,命令行到Flutter Module路径下,然后执行flutter build aar命令。效果图如下:
\\n\\n根据上图中的步骤添加相应的配置,但上面的配置有需要调整的地方。命令执行完成之后,生成的aar相关的目录和信息如下图所示:
dependencyResolutionManagement {\\n repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)\\n repositories {\\n google()\\n mavenCentral()\\n //下面两行是新增的\\n maven(\\"https://storage.googleapis.com/download.flutter.io\\")\\n maven(url=\\"/Users/caicai/AndroidStudioProjects/MyApplication/FlutterAddDemo/flutter_module/build/host/outputs/repo\\")\\n }\\n}\\n
\\n在app mudule中buildTypes增加配置:
\\ncreate(\\"profile\\") {\\n initWith(getByName(\\"debug\\"))\\n}\\n\\n
\\n在app mudule中dependencies增加如下配置:
\\n debugImplementation (\\"com.example.flutter_module:flutter_debug:1.0\\")\\n add(\\"profileImplementation\\", \\"com.example.example_one:flutter_profile:1.0\\")\\n releaseImplementation (\\"com.example.flutter_module:flutter_release:1.0\\")\\n
\\napp的manifest文件中增加FlutterActivity的配置,示例代码如下:
\\n <activity\\n android:name=\\"io.flutter.embedding.android.FlutterActivity\\"\\n android:theme=\\"@style/Theme.FlutterAddDemo\\"\\n android:configChanges=\\"orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode\\"\\n android:hardwareAccelerated=\\"true\\"\\n android:windowSoftInputMode=\\"adjustResize\\"\\n />\\n
\\nfindViewById<Button>(R.id.button).setOnClickListener {\\n // Using a new FlutterEngine.\\n startActivity(\\n FlutterActivity.createDefaultIntent(this)\\n )\\n }\\n
\\n希望文章对您有所帮助,如有错误,请不吝指出。
\\n前几天最热门话题之一不外乎盛传 Android 闭源,可谓「节奏」一开「流量」全来,虽然做媒体的难免「春秋笔法」,但是直接「断章“曲”义」的做法未免有些离谱,总想搞个大新闻,刚好现在风头已过,就简单聊聊始末。
\\n首先本次的的核心是「转内部开发」,怎么理解这个变化?用人话说就是:
\\n\\n\\n我做完了再全部公开出来给你看,没做完之前一般人看不到,当然我兄弟还是能提前看到。
\\n
至于为什么要这样做,官方点的说法就是:同时维持「公开的 AOSP」和「内部分支」成本越来越高,比如要花费大量时间在两个分支之间合并补丁和处理冲突,因为这个两个分支的代码结构和新旧差异越来越大,合并一个简单的修复,就可能需要处理一系列复杂的冲突。
\\n而如果用人话说,可以简单理解为:
\\n\\n\\n之前的做法越来越费钱了,所以需要想办法「截流」,也许是裁员裁多了,维护不过来了。
\\n
事实上很多公司在开源方面都会有自己的内部版本和公开版本,不管是需要「脱敏」还是「维持稳定」考虑,对外的公开版本都不可能一股脑就给你提交出来,比如前段时间的字节的 lynx ,内部其实已经支持鸿蒙了,但是初步开源的版本还是不带鸿蒙:
\\n\\n\\n又像 Flutter ,它很多提议性质和草稿性质的代码,也不会直接合并到 main 分支,而是在相对可用,或者确定可尝试落地之后,才会真正进入大众视野,而如果不做好这一点,就像宏落地失败一样,负面影响更深。
\\n
况且,像 Android 这样的大型开源项目,它本身的各种依赖也许遵循相对应的开源协议,比如 Linux 内核分支采用的 GPLv2 许可,谷歌仍需遵守开源协议,如果不守规则,作为大企业那可是要收律师函的:
\\n另外,从前几年开始,Andorid 的核心系统框架大部分就已经是在谷歌的内部分支中完成,所以这次也就是把剩余部分也转入内部,节省维护成本,从这点看,可以大胆猜测谷歌搞 AI 是真的烧了不少钱,用 320亿美元的全现金交易收购 Wiz 也是为了 AI ,一边是开“猿”截流,一边是 AI 的经费燃烧,时代真的变了,Android 现在也只能算是「半老徐娘」,比不得 AI 风华正茂 。
\\n所以从上面部分总结下来就是:为了省钱省人力,Android 开发现在我们先做好,再发出来给你看,当然,这一切都只是针对个人。
\\n而对于和谷歌有合作的厂商,仍可以依照 Early Access Program 和协议获取最新的 AOSP 或 branch(分支),这对 OEM 合作方没有什么影响:
\\n\\n\\n毕竟如果大牌厂商需要等公开 AOSP ,那是真的吃x都赶不上热的了。
\\n
接着就是关于 PR,,Google 也表示了 Android 团队会继续接受来自外部开发人员的代码贡献,平台开发人员仍将能够向 AOSP Gerrit 提交补丁,而合作伙伴也可以通过合作伙伴的 Gerrit 权限提交补丁,只是内部渠道的 PR 在完全发之前不会向公众开放。
\\n\\n\\n所以基本上可以看出来,如果硬要说会有影响的话,那就是没权限的普通人,你只能等 final release 之后才能了解到相关内容。
\\n
并且,接下来 aosp-main
分支将被锁定并设置为只读,外部开发者将建议使用 android-latest-release
分支,同时提交到 aosp-main
的 PR 更改不会被合并,外部 PR 还是需要依赖于 android-latest-release
:
之后 aosp-main
的 CI 构建将会被停止,而如 android15-release
、android15-tests-dev
等 IC 还会继续分布,谷歌将继续支持现有的 Android 开发者预览版/测试版计划,只是内部 main 分支不会再发布 CI 支持。
\\n\\n所以对于厂商基本没任何影响,受影响的基本上没权限的个人,而谷歌也根据已有数据做出判断,非合作的外部个人贡献者其实 PR 占比很小。
\\n
所以作为一般人,如果你想给 AOSP 提 PR ,通过 release 分支提交即可,审核通过后也会并合并,和之前的区别就是,你可能没那么快获取到较新资料,而对于合作方而言,一切都还是老样子。
\\n所以,肯定有人要说,虽然现在没闭源,但是未来你能说就不闭源吗?
\\n嗯,这就是典型的辩论里的「滑坡谬误」,将讨论从现实拉向一个虚构的、未经证实的情景,从而规避对当前事实的直接回应 :
\\n\\n\\n在论证中假设一个相对较小的初始行动或事件会不可避免地导致一系列连锁反应,最终到达一个极端或不希望的结果,而这种因果链通常缺乏充分的证据支持。简单来说,就是从“如果A发生”跳跃到“那么必然会导致灾难性的Z”,忽略了中间步骤的合理性和可能性。
\\n这种手法的问题在于,它往往夸大了未来的风险,跳过了对当前事实的分析,也未提供中间环节的逻辑支持。它依赖于假设和恐惧,而不是严谨的推理。
\\n
如果按照这个逻辑,Android 都转内部开发了,你能保证 Android 不是要凉了吗?
\\n在 Flutter 开发领域,状态管理框架层出不穷,Provider、Riverpod、Bloc、Signal 和 Fish Redux 等都是其中的佼佼者。然而,这些框架在实际使用过程中,普遍存在操作复杂的问题,并且过度关注局部状态的更新。
\\n回归初心,一个 ui 的状态管理应该具备哪些功能?
\\n\\n\\n局部的状态更新真的重要吗?
\\n
我们都知道 flutter 3 层树结构, Wiget 就是配置而已。 我们只要恰当的重写 State 的 didUpdate(StatefulWiget old)
,利用 Widget 自身的 diff 机制就不会有性能开销。每次 setState 不过是多了几个或者几十个 Widget 对象的内存开销而已,并且很快会被 gc 回收掉,实际的性能损坏对于大部分机器来说忽略不计。
基于此,一个轻量版的 view_model 如下:
\\nStreamController
和setState
实现。State
的dispose
方法。StatefulWidget
)间共享。\\n\\n\\n
ViewModel
仅绑定到有状态组件(StatefulWidget
)的State
上。我们不建议将状态绑定到无状态组件(StatelessWidget
)上,因为无状态组件不应有状态。
ViewModel
:存储状态并在状态改变时发出通知。ViewModelFactory
:指导如何创建你的ViewModel
。getViewModel
:创建或获取已存在的ViewModel
。listenViewModelStateChanged
:监听Widget.State
内的状态变化。view_model:\\n git:\\n url: https://github.com/lwj1994/flutter_view_model\\n ref: 0.0.1\\n
\\nViewModel
import \\"package:view_model/view_model.dart\\";\\n\\nclass MyViewModel extends ViewModel<String> {\\n MyViewModel({\\n required super.state,\\n }) {\\n debugPrint(\\"创建 MyViewModel,状态: $state,哈希码: $hashCode\\");\\n }\\n\\n void setNewState() {\\n setState((s) {\\n return \\"hi\\";\\n });\\n }\\n\\n @override\\n void dispose() async {\\n super.dispose();\\n debugPrint(\\"释放 MyViewModel,状态: $state,哈希码: $hashCode\\");\\n }\\n}\\n\\nclass MyViewModelFactory with ViewModelFactory<MyViewModel> {\\n final String arg;\\n\\n MyViewModelFactory({this.arg = \\"\\"});\\n\\n @override\\n MyViewModel build() {\\n return MyViewModel(state: arg);\\n }\\n}\\n
\\nViewModel
import \\"package:view_model/view_model.dart\\";\\n\\nclass _State extends State<Page> with ViewModelStateMixin<Page> {\\n // 建议使用getter来获取ViewModel\\n MyViewModel get viewModel =>\\n getViewModel<MyViewModel>(factory: MyViewModelFactory(arg: \\"初始参数\\"));\\n\\n // 获取ViewModel的状态\\n String get state => viewModel.state;\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n floatingActionButton: FloatingActionButton(\\n onPressed: () {\\n viewModel.setNewState();\\n },\\n child: Icon(Icons.add),\\n ),\\n appBar: AppBar(\\n leading: IconButton(\\n icon: const Icon(Icons.arrow_back),\\n onPressed: () {\\n appRouter.maybePop();\\n },\\n ),\\n ),\\n body: Column(\\n crossAxisAlignment: CrossAxisAlignment.start,\\n children: [\\n Text(\\"mainViewModel.state = ${_mainViewModel.state}\\"),\\n Text(\\n state,\\n style: const TextStyle(color: Colors.red),\\n ),\\n FilledButton(\\n onPressed: () async {\\n refreshViewModel(_mainViewModel);\\n },\\n child: const Text(\\"刷新mainViewModel\\")),\\n FilledButton(\\n onPressed: () {\\n debugPrint(\\"页面的MyViewModel哈希码 = ${viewModel.hashCode}\\");\\n debugPrint(\\"页面的MyViewModel状态 = ${viewModel.state}\\");\\n },\\n child: const Text(\\"打印MyViewModel\\")),\\n ],\\n ),\\n );\\n }\\n}\\n
\\nViewModel
你可以设置unique() => true
来在任意有状态组件(StateWidget
)间共享同一个ViewModel
实例。
import \\"package:view_model/view_model.dart\\";\\n\\nclass MyViewModelFactory with ViewModelFactory<MyViewModel> {\\n final String arg;\\n\\n MyViewModelFactory({this.arg = \\"\\"});\\n\\n @override\\n MyViewModel build() {\\n return MyViewModel(state: arg);\\n }\\n\\n // 如果为true,则会共享同一个viewModel实例。\\n @override\\n bool unique() => false;\\n}\\n
\\n@override\\nvoid initState() {\\n super.initState();\\n listenViewModelStateChanged<MainViewModel, String>(\\n _mainViewModel,\\n onChange: (String? p, String n) {\\n print(\\"mainViewModel状态变化: $p -> $n\\");\\n },\\n );\\n}\\n
\\nViewModel
这将释放旧的ViewModel
并创建一个新的。不过,建议使用getter来获取ViewModel
,否则你需要手动重置ViewModel
。
// 建议使用getter来获取ViewModel。\\nMyViewModel get viewModel => getViewModel<MyViewModel>();\\n\\nvoid refresh() {\\n // 刷新 \\n refreshViewModel(viewModel);\\n}\\n
\\n或者
\\nlate MyViewModel viewModel = getViewModel<MyViewModel>(factory: factory);\\n\\nvoid refresh() {\\n // 刷新并重置 \\n refreshViewModel(viewModel);\\n viewModel = getViewModel<MyViewModel>(factory: factory);\\n}\\n\\n
","description":"在 Flutter 开发领域,状态管理框架层出不穷,Provider、Riverpod、Bloc、Signal 和 Fish Redux 等都是其中的佼佼者。然而,这些框架在实际使用过程中,普遍存在操作复杂的问题,并且过度关注局部状态的更新。 Provider:该框架仅支持在 Widget 树的上下层级间共享状态。这就导致处于同级树结构,比如不同路由页面之间,无法直接共享状态。开发者往往只能将状态提升到 Application 层的 Widget 树中进行管理。\\nBloc 和 Signal:这两个框架的状态共享机制基于 Provider 构建…","guid":"https://juejin.cn/post/7486764352935870518","author":"未来猫咪花","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-29T04:23:04.695Z","media":null,"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"2025 Flutter Engine Source Setup","url":"https://juejin.cn/post/7486727928895406121","content":"\\n\\n我的学习笔记以及感悟还有一些小坑,仅供参考~\\n北京有岗位内推的练习我啊~~~🤣
\\n
Fltter 开发团队的核心成员大都有Chromium项目工作经验,所以在Flutter源码获取的过程中,都或多或少的看到Chromium的影子。
\\nFlutter在软件设计上是一个可灵活扩展的分层架构,每个组件功能都是以库的形式存在,每个库都依赖下一层的库。
\\n这样设计的好处是让框架的每一个组件都变得可插拔、可替换。作为一个跨平台框架,对服用和移植到其它平台非常合适。
\\n\\n💡
\\n\\n\\n\\n插入一个题外话~看到这张图,是不是有中似曾相似的感觉,在看《Android的第一行代码》《UNIX编程艺术》等,甚至OSI七层模型,相信其他系统在书本中也有类似架构,和Flutter的分层架构设计大同小异。在《UNIX编程艺术》对这种设计有一个贴切的称呼:**正交性。由于正交性存在,OSI模型才会有各种协议(如运输层的TCP UDP) **
\\n
自底向上的简要分析下每一层:
\\nEmbedder顾名思义,将Flutter嵌入到Native平台,如iOS,Android, Windows ,Linux,所以每个平台上,都会包含一个特定的嵌入层。它的职责是渲染UI到Surface【一块可以被GPU渲染显示到区域,FlutterUI渲染最终都会输出到Surface上,Embedder负责创建和管理Surface,不同平台实现方式不同,Flutter Engine通过Skia绘制UI到Surface,然后由操作系统合成并显示到屏幕上】、处理单机事件等与底层平台交互行为提供一个入口。Embedder使用的编程语言与特定平台有关,比如Android 使用Java和C++, iOS 和 macOS 使用 Swift 和 Objective-C/Objective-C++,Windows 和 Linux 则使用 C++。
\\nFlutter的核心部分,大部分代码是C++,包装成Dart代码,这个库是最底层的原语 主要职责是图形绘制,文字渲染,文件/玩过IO,无障碍支持,平台插件,Dart运行时管理和编译器工具链等。主要通过dart:ui暴露给Flutter框架层进行双向交互。
\\n通常我们只需要关注Framework层。这一层由Dart语言开发。提供现代、响应式的UI框架。这一层也是分层的。
\\n**Foundation & Animation & Painting & Gestures: **
\\n提供公用底层能力,对engine的抽象和封装
\\n**Render: **
\\n主要负责Render Tree的Layout操作等,最终将绘制指令发送给Engine进行绘制。通过这一层,可以构建一个可渲染对象的树结构==》 Render Tree。
\\n**Widgets: **
\\n对上一层Render的上层分装,Render Tree虽然能够决定最终UI,但是过于复杂,不适合开发使用,Widgets则通过组合思想,提供了丰富的widgets组件,供开发者使用。 Render层中每个渲染对象在这一层都有对应的类。wigets层可以自由组合需要复用的类,这一层引入了响应式编程Reactive Programming。
\\n**Material & Cupertino: **
\\n这俩个组件是针对Widgets进一步封装,Widgets对于开发者来说还是很原始,这俩个组件是基于iOS和Android设计规范提供了更完备的组件,保证开发者开箱即用。
\\n简单分析 flutter create 命令创建的应用结构概览,展示了 Flutter 引擎在此技术栈中的位置,标明了 API 边界,并注明了各组件所在的代码库。下方图例阐释了描述 Flutter 应用组件时常用的术语。
\\n开发者自己写的UI&业务逻辑代码,使用FrameworkSDK提供的Widgets构建UI。
\\nOwned by app developer.
\\n一般说的是工程内****lib/** **下的代码
\\n提供了上层API封装,构建高质量应用,例如( widget、触摸检测、手势竞技、无障碍和文字输入)。
\\n将开发者的应用的Widgets树构建至一个Scene中,传递给下层Engine的skia处理,再传递给对应的平台
\\n就是我们安装的SDK,**flutter/packages/flutter/lib/ **
\\n将上层传递过来的Scene 栅格化
\\n对Flutter的核心API进行了底层封装(如图形图像,文本布局,Dart运行时)
\\n将其功能通过dart:ui暴露给上层Framework
\\n使用引擎的 Embedder API 与特定平台集成。
\\n我们平时用到的engine都是编译好的引擎产物,位置在这里****flutter/bin/cache/artifacts/engine/ios/** **
\\n我这里看的iOS平台的Engine框架,这里有好多平台的,MacOS 、Android。
\\n点个模拟器平台的看一下:
\\n用 烂苹果 看一下:
\\n模拟器提供了俩个架构的,用file命令也可以看
\\n看下下载的引擎源码:
\\n红框的都属于引擎源码
\\n与底层操作系统协调,获取渲染表面、无障碍功能和输入等服务。
\\n管理事件循环。
\\n暴露平台特定 API,将嵌入器集成到应用中。
\\n对应Engine源码中Embedder位置:
\\n将嵌入器(Embedder)提供的平台特定 API 组件整合成可在目标平台上运行的应用程序包。
\\n由 flutter create
生成的应用程序模板的一部分,归应用程序开发者所有。
对应各个平台宿主的App
\\n**温馨提示:在开始前,最好提前把VPN连着。 **
\\n要编译环境,直接从github上下载Flutter和Engine这俩个仓库是不不能构建的,因为Engine和Flutter它俩本身还依赖了很多第三方库,定义在engine根目录的DEPS文件中。
\\n打开这个文件看一下,第一句就描述了这个文件作用
\\n准备一个文件夹 ,mkdir flutter_source
创建一个空文件夹
\\ngit clone https://chromium.googlesource.com/chromium/tools/depot_tools.git\\n\\nexport PATH=/path/to/depot_tools:$PATH\\n\\n
\\n克隆下载
\\n配置环境变量
\\ncd flutter_source 转到这个目录下:
\\n把这个depot_tools 工具克隆在这里, 再配置下环境变量就可以用了**
\\n\\nsolutions = [\\n\\n {\\n\\n \\"managed\\": False,\\n\\n \\"name\\": \\"src/flutter\\", \\n\\n \\"url\\": \\"git@github.com:flutter/engine.git\\", \\n\\n \\"custom_deps\\": {},\\n\\n \\"deps_file\\": \\"DEPS\\", \\n\\n \\"safesync_url\\": \\"\\",\\n\\n },\\n\\n]\\n\\n
\\nname指定仓库位置
\\nurl 指定要下载的仓库
\\ndep_file 指定存放第三方依赖的文件名
\\n在当前目录执行这个命令,等待下载engine以及它的依赖。执行完得等待一会,可以通过系统监视器查看当前网络状态,判断gclient执行是否正常。
\\n在终端输出 “ Syncing projects: 100%。 done”
后还会启动cipd_client下载部分大文件,直至完成。**
最后文件夹就是这些文件
\\ndepot工具
\\nengineplay
是一个flutter工程
flutter 是framework sdk
\\nsrc里就是.gclient
文件配置的engine源码下载路径
Engine是没有版本概念的,但它的CommitID可以在Flutter里看到
\\n我当前的Flutter SDK版本对应的engine是
\\n通过 flutter —version
也可以看到
**找到了这个CommitID号后,进入src/flutter目录执行 **git checkout 83bacfc525**
, 把代码库恢复到这个提交,就和这个版本对应上了。 之后就可以编译了。 **
都准备好后,在 ****** **src/flutter**
这个目录执行 **gclient sync —with_branch_heads —with_tags**
, **这是因为在切换提交的时候,可能DEPS可能发生改变。每次切换后都要进行同步操作。后面俩个参数表示将tag、refspecs等仓库信息一起同步。
💡
\\n⚠️ 注意: 这里有一个坑点, 我看的好多资料,都是比较老的,我下载最新sdk 3.29.1 ,对应的Commit号:871f65ac1bf129edb222c3293a636ff4b67534a6,可能在engine源码里切换找不到这个commiID, 我猜想可能没同步, 之后会延迟FlutterSDK同步,所以我用了3.27版本。不知道之前有没有人遇到过这个。
\\n\\nFlutter引擎的源码是需要通过Ninja来编译的,所以需要系统提前装好Ninja,通过brew一键安装。
\\n而gn工具是一个生成Ninja编译所需的构建文件的元文件。
\\n引擎部分大部分都是由C++代码组成,最终的机器码和运行平台与CPU架构是强相关的。对于不同平台,Embedder也有所不同。但是Futter已经借助Chromium工程的GN和Ninja工具链大大简化了这个构建过程。
\\n在src目录下,需要用到这个命令 ****** **./flutter/tools/gn
** ** 命令,需要加上路径,因为前边设置的depot_tools里也有gn命令。这里注意下就可以**
--runtime-mode {debug,profile,release,jit_release}
构建产物的类型,- debug日常开发,基于DartVM运行,性能较低。
\\n- profile 性能测试 release 正式发布 俩者都是编译成机器码,性能较高
\\n--target-os {android,ios,mac,linux,fuchsia,wasm,win}
构建产物运行平台,对于Android和iOS还可以通过—android
和 —ios
指定。
--android-cpu {arm,x64,x86,arm64}
Android产物所属的架构,通常x86用于pc模拟器,其它三种用于真机。
准备设备端可执行文件的构建文件 ./flutter/tools/gn --ios --unoptimized
\\n # 1. device-side executables 设备端可执行文件的构建文件 \\n\\n # 输出 out/ios_debug_unopt/flutter_engine.xcodeproj \\n\\n ./flutter/tools/gn --ios --unoptimized # 真机debug版本\\n\\n ./flutter/tools/gn --ios --unoptimized --runtime-mode=release # 真机release版本(日常开发使用,如果我们要自定义引擎)\\n\\n # 这句是为模拟器生成元件 输出 out/ios_debug_sim_unopt_arm64\\n\\n ./flutter/tools/gn --ios --simulator --unoptimized --simulator-cpu=arm64 #模拟器版本\\n\\n # 2. host-side executables 主机端可执行文件准备构建文件\\n\\n # 输出 host_debug_unopt_arm64\\n\\n ./flutter/tools/gn --unoptimized --mac-cpu arm64 # 主机端(Mac)构建\\n\\n # 3. \\n\\n ninja -C out/ios_debug_unopt && ninja -C out/host_debug_unopt\\n\\n ninja -C host_debug_unopt && ninja -C ios_debug_sim_unopt && ninja -C ios_debug_unopt && ninja -C ios_release_unopt\\n\\n \\n\\n\\n
\\n这是我们生成的准备元件,执行完第3行ninja -C
,接下来就是漫长的等待了。
首先启动一个flutter工程,flutter sdk版本指定本地
\\n通过命令行指定编译好的engine
\\n\\ncd engineplay\\n\\nflutter run --local-engine-src-path engine/src --local-engine=ios_debug_sim_unopt --local-engine-host host_debug_unopt_arm64\\n\\n
\\n也可以在AS参数里指定
\\n然后打开引擎源代码:
\\n在这加一句输出
\\n接着在AS跑个代码【前提AS配置好了自定义引擎】
\\n说明这个工程现在依赖的是我本地编译好的引擎代码。
\\n接下来在Xcode跑一下:
\\n不依赖AS,直接打开项目的Runner。
\\n在xcode里可以自由调试~~ 待写
\\n现在的App开发,总是免不了暗黑主题和亮色主题的需求,暗黑模式(Dark Mode) 已成为用户体验的重要组成部分。我们希望应用能够根据系统设置或用户偏好自动切换主题,并在重启后保持一致。本篇文章将带你一步步使用 Bloc 和 hydrated_bloc 来实现这一目标。
\\n先来看看效果图:
\\nBloc 是 Flutter 中广泛使用的状态管理方案之一。它基于事件(Event)驱动和状态(State)响应的模式,将 UI 与业务逻辑有效分离,从而提升了代码的可读性、可维护性以及可测试性。
\\n在此基础上,hydrated_bloc 作为 Bloc 的扩展,提供了自动状态持久化的能力,可以将状态存储在本地磁盘。对于需要在应用重启后保留设置(如主题模式)的场景来说,它是一个非常理想的选择。
\\n在需要保存用户偏好(如暗黑模式)时,shared_preferences
和 hydrated_bloc
都是常见的选择,但它们各自的适用场景和优势不同。
对比维度 | hydrated_bloc | shared_preferences |
---|---|---|
集成方式 | 内建于 Bloc 架构,状态持久化自动进行 | 独立处理,需要手动管理读写 |
序列化/反序列化 | 自动进行,仅需实现 fromJson / toJson 方法 | 需手动处理每个字段的读写逻辑 |
状态同步 | 状态与 UI 自动保持同步,无需额外逻辑 | 状态更新后需要手动通知 UI |
代码维护性 | 高,可读性强,逻辑集中 | 易产生重复代码,分散管理 |
性能表现 | 基于内存缓存 + 本地存储,读取速度快 | 每次读取都需访问磁盘,略慢 |
支持对象结构 | 支持复杂状态对象持久化 | 仅支持基本类型,复杂结构需手动拆解 |
hydrated_bloc
内部使用的是内存缓存机制:首次从本地读取状态后,会保留在内存中,因此在后续使用过程中几乎不需要重新读取磁盘,状态恢复几乎是即时的。
相比之下,shared_preferences
每次读取都涉及异步的磁盘访问,哪怕只是一两个键值,也需要 await
操作,这在应用启动时或快速切换主题时可能造成微小的延迟。
\\n\\n✅ 简单来说:
\\n
\\n如果你已经在使用 Bloc,hydrated_bloc 会让你几乎“无感知”地实现状态持久化,开发更轻松,体验更流畅。
在 pubspec.yaml
中添加以下依赖项:
dependencies:\\n flutter:\\n sdk: flutter\\n cupertino_icons: ^1.0.8\\n flutter_bloc: ^9.1.0\\n hydrated_bloc: ^10.0.0\\n path_provider: ^2.1.5\\n equatable: ^2.0.5\\n
\\nflutter_bloc
:核心 Bloc 功能。hydrated_bloc
:实现 Bloc 状态持久化。path_provider
:获取持久化存储路径。equatable
:简化状态对比,提高 Bloc 性能。创建一个 AppThemes
类,包含浅色和暗黑主题定义:
import \'package:flutter/material.dart\';\\n\\nclass AppThemes {\\n // Light theme\\n static final ThemeData lightTheme = ThemeData(\\n useMaterial3: true,\\n brightness: Brightness.light,\\n colorScheme: ColorScheme.fromSeed(\\n seedColor: Colors.blue,\\n brightness: Brightness.light,\\n ),\\n appBarTheme: const AppBarTheme(\\n elevation: 0,\\n centerTitle: true,\\n ),\\n elevatedButtonTheme: ElevatedButtonThemeData(\\n style: ElevatedButton.styleFrom(\\n padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),\\n shape: RoundedRectangleBorder(\\n borderRadius: BorderRadius.circular(8),\\n ),\\n ),\\n ),\\n cardTheme: CardTheme(\\n elevation: 2,\\n shape: RoundedRectangleBorder(\\n borderRadius: BorderRadius.circular(12),\\n ),\\n ),\\n switchTheme: SwitchThemeData(\\n thumbColor: WidgetStateProperty.resolveWith((states) {\\n if (states.contains(WidgetState.selected)) {\\n return Colors.blue;\\n }\\n return Colors.grey.shade400;\\n }),\\n trackColor: WidgetStateProperty.resolveWith((states) {\\n if (states.contains(WidgetState.selected)) {\\n return Colors.blue.withValues(alpha: 0.5);\\n }\\n return Colors.grey.shade300;\\n }),\\n ),\\n );\\n\\n // Dark theme\\n static final ThemeData darkTheme = ThemeData(\\n useMaterial3: true,\\n brightness: Brightness.dark,\\n colorScheme: ColorScheme.fromSeed(\\n seedColor: Colors.blue,\\n brightness: Brightness.dark,\\n ),\\n appBarTheme: const AppBarTheme(\\n elevation: 0,\\n centerTitle: true,\\n ),\\n elevatedButtonTheme: ElevatedButtonThemeData(\\n style: ElevatedButton.styleFrom(\\n padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),\\n shape: RoundedRectangleBorder(\\n borderRadius: BorderRadius.circular(8),\\n ),\\n ),\\n ),\\n cardTheme: CardTheme(\\n elevation: 2,\\n shape: RoundedRectangleBorder(\\n borderRadius: BorderRadius.circular(12),\\n ),\\n ),\\n switchTheme: SwitchThemeData(\\n thumbColor: MaterialStateProperty.resolveWith((states) {\\n if (states.contains(MaterialState.selected)) {\\n return Colors.blue;\\n }\\n return Colors.grey.shade600;\\n }),\\n trackColor: WidgetStateProperty.resolveWith((states) {\\n if (states.contains(WidgetState.selected)) {\\n return Colors.blue.withValues(alpha: 0.5);\\n }\\n return Colors.grey.shade800;\\n }),\\n ),\\n );\\n}\\n
\\nbrightness
决定主题亮度。colorScheme.fromSeed
可基于种子色自动生成配色。AppBarTheme
、ElevatedButtonTheme
、SwitchTheme
等,以适配两种模式。Bloc 负责监听事件并更新主题状态。
\\nimport \'package:equatable/equatable.dart\';\\nimport \'package:flutter/material.dart\';\\nimport \'package:hydrated_bloc/hydrated_bloc.dart\';\\n\\nimport \'app_themes.dart\';\\n\\npart \'theme_event.dart\';\\npart \'theme_state.dart\';\\n\\nclass ThemeBloc extends HydratedBloc<ThemeEvent, ThemeState> {\\n ThemeBloc()\\n : super(ThemeState(\\n themeMode: ThemeMode.system,\\n lightTheme: AppThemes.lightTheme,\\n darkTheme: AppThemes.darkTheme,\\n )) {\\n on<ThemeStarted>(_onThemeStarted);\\n on<ThemeChanged>(_onThemeChanged);\\n }\\n\\n void _onThemeStarted(ThemeStarted event, Emitter<ThemeState> emit) {\\n // No need to load the theme here as HydratedBloc handles it automatically\\n // This event is kept for consistency and in case additional initialization is needed\\n }\\n\\n void _onThemeChanged(ThemeChanged event, Emitter<ThemeState> emit) {\\n emit(state.copyWith(themeMode: event.themeMode));\\n }\\n\\n @override\\n ThemeState? fromJson(Map<String, dynamic> json) {\\n return ThemeState.fromMap(json);\\n }\\n\\n @override\\n Map<String, dynamic> toJson(ThemeState state) {\\n return state.toMap();\\n }\\n}\\n
\\nThemeMode.system
表示默认跟随系统。_onThemeChanged
用于响应用户主题切换。fromJson
和 toJson
方法实现状态的持久化序列化。part of \'theme_bloc.dart\';\\n\\nabstract class ThemeEvent extends Equatable {\\n const ThemeEvent();\\n\\n @override\\n List<Object> get props => [];\\n}\\n\\nclass ThemeStarted extends ThemeEvent {}\\n\\nclass ThemeChanged extends ThemeEvent {\\n final ThemeMode themeMode;\\n\\n const ThemeChanged(this.themeMode);\\n\\n @override\\n List<Object> get props => [themeMode];\\n}\\n
\\nThemeEvent
。ThemeChanged
表示切换主题的事件,携带一个 ThemeMode
。part of \'theme_bloc.dart\';\\n\\nclass ThemeState extends Equatable {\\n final ThemeMode themeMode;\\n final ThemeData lightTheme;\\n final ThemeData darkTheme;\\n\\n const ThemeState({\\n required this.themeMode,\\n required this.lightTheme,\\n required this.darkTheme,\\n });\\n\\n @override\\n List<Object> get props => [themeMode];\\n\\n ThemeState copyWith({\\n ThemeMode? themeMode,\\n ThemeData? lightTheme,\\n ThemeData? darkTheme,\\n }) {\\n return ThemeState(\\n themeMode: themeMode ?? this.themeMode,\\n lightTheme: lightTheme ?? this.lightTheme,\\n darkTheme: darkTheme ?? this.darkTheme,\\n );\\n }\\n\\n Map<String, dynamic> toMap() {\\n return {\\n \'themeMode\': themeMode.index,\\n };\\n }\\n\\n factory ThemeState.fromMap(Map<String, dynamic> map) {\\n return ThemeState(\\n themeMode: ThemeMode.values[map[\'themeMode\'] ?? 0],\\n lightTheme: AppThemes.lightTheme,\\n darkTheme: AppThemes.darkTheme,\\n );\\n }\\n}\\n
\\nThemeMode
和两个具体主题。copyWith
实现状态更新。themeMode
,而不是整个 ThemeData
,简洁高效。在 main.dart
中完成初始化:
void main() async {\\n WidgetsFlutterBinding.ensureInitialized();\\n HydratedBloc.storage = await HydratedStorage.build(\\n storageDirectory: await getApplicationDocumentsDirectory(),\\n );\\n runApp(const MyApp());\\n}\\n
\\nclass MyApp extends StatelessWidget {\\n const MyApp({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n return BlocProvider(\\n create: (_) => ThemeBloc(),\\n child: BlocBuilder<ThemeBloc, ThemeState>(\\n builder: (context, state) {\\n return MaterialApp(\\n theme: state.lightTheme,\\n darkTheme: state.darkTheme,\\n themeMode: state.themeMode,\\n home: const HomeScreen(),\\n );\\n },\\n ),\\n );\\n }\\n}\\n
\\nHydratedBloc.storage
初始化状态持久化。BlocBuilder
动态构建 MaterialApp
,根据状态切换主题。创建 ThemeModeSelector
,支持手动选择主题:
import \'package:flutter/material.dart\';\\nimport \'package:flutter_bloc/flutter_bloc.dart\';\\nimport \'package:flutter_theme_test/theme/theme_bloc.dart\';\\n\\nclass ThemeModeSelector extends StatelessWidget {\\n const ThemeModeSelector({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n return BlocBuilder<ThemeBloc, ThemeState>(\\n builder: (context, state) {\\n return PopupMenuButton<ThemeMode>(\\n icon: Icon(_getThemeIcon(state.themeMode)),\\n tooltip: \'Select theme mode\',\\n onSelected: (ThemeMode mode) {\\n context.read<ThemeBloc>().add(ThemeChanged(mode));\\n },\\n itemBuilder: (BuildContext context) => <PopupMenuEntry<ThemeMode>>[\\n const PopupMenuItem<ThemeMode>(\\n value: ThemeMode.light,\\n child: Row(\\n children: [\\n Icon(Icons.wb_sunny),\\n SizedBox(width: 8),\\n Text(\'Light\'),\\n ],\\n ),\\n ),\\n const PopupMenuItem<ThemeMode>(\\n value: ThemeMode.dark,\\n child: Row(\\n children: [\\n Icon(Icons.nightlight_round),\\n SizedBox(width: 8),\\n Text(\'Dark\'),\\n ],\\n ),\\n ),\\n const PopupMenuItem<ThemeMode>(\\n value: ThemeMode.system,\\n child: Row(\\n children: [\\n Icon(Icons.settings_suggest),\\n SizedBox(width: 8),\\n Text(\'System\'),\\n ],\\n ),\\n ),\\n ],\\n );\\n },\\n );\\n }\\n\\n IconData _getThemeIcon(ThemeMode themeMode) {\\n switch (themeMode) {\\n case ThemeMode.light:\\n return Icons.wb_sunny;\\n case ThemeMode.dark:\\n return Icons.nightlight_round;\\n case ThemeMode.system:\\n return Icons.settings_suggest;\\n }\\n }\\n}\\n
\\n定义一个样式跟随主题变化的卡片组件:
\\nimport \'package:flutter/material.dart\';\\n\\nclass ThemedCard extends StatelessWidget {\\n final String title;\\n final String description;\\n final IconData icon;\\n\\n const ThemedCard({\\n super.key,\\n required this.title,\\n required this.description,\\n required this.icon,\\n });\\n\\n @override\\n Widget build(BuildContext context) {\\n final theme = Theme.of(context);\\n final colorScheme = theme.colorScheme;\\n\\n return Card(\\n elevation: 4,\\n child: Padding(\\n padding: const EdgeInsets.all(16.0),\\n child: Column(\\n crossAxisAlignment: CrossAxisAlignment.start,\\n children: [\\n Row(\\n children: [\\n Icon(\\n icon,\\n color: colorScheme.primary,\\n size: 28,\\n ),\\n const SizedBox(width: 12),\\n Text(\\n title,\\n style: theme.textTheme.titleLarge?.copyWith(\\n color: colorScheme.onSurface,\\n ),\\n ),\\n ],\\n ),\\n const SizedBox(height: 12),\\n Text(\\n description,\\n style: theme.textTheme.bodyMedium?.copyWith(\\n color: colorScheme.onSurfaceVariant,\\n ),\\n ),\\n ],\\n ),\\n ),\\n );\\n }\\n}\\n
\\n在 HomeScreen
中使用切换器与卡片,展示主题效果:
import \'package:flutter/material.dart\';\\nimport \'package:flutter_bloc/flutter_bloc.dart\';\\n\\nimport \'package:flutter_theme_test/theme/theme_bloc.dart\';\\nimport \'package:flutter_theme_test/widgets/theme_mode_selector.dart\';\\nimport \'package:flutter_theme_test/widgets/themed_card.dart\';\\n\\nclass HomeScreen extends StatelessWidget {\\n const HomeScreen({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: const Text(\'Theme Manager\'),\\n actions: const [\\n ThemeModeSelector(),\\n SizedBox(width: 16),\\n ],\\n ),\\n body: SafeArea(\\n child: Padding(\\n padding: const EdgeInsets.all(16.0),\\n child: Column(\\n crossAxisAlignment: CrossAxisAlignment.start,\\n children: [\\n Text(\\n \'Theme Demonstration\',\\n style: Theme.of(context).textTheme.headlineMedium,\\n ),\\n const SizedBox(height: 24),\\n const Text(\\n \'This app demonstrates dynamic theme switching using flutter_bloc \'\\n \'with hydrated_bloc for persistence. Your theme preference is \'\\n \'automatically saved between app sessions.\',\\n style: TextStyle(fontSize: 16),\\n ),\\n const SizedBox(height: 32),\\n const ThemedCard(\\n title: \'Primary Card\',\\n description: \'This card adapts to the current theme\',\\n icon: Icons.palette,\\n ),\\n const SizedBox(height: 16),\\n const ThemedCard(\\n title: \'Secondary Card\',\\n description: \'UI elements respond to theme changes automatically\',\\n icon: Icons.color_lens,\\n ),\\n const SizedBox(height: 32),\\n Center(\\n child: ElevatedButton.icon(\\n onPressed: () {\\n ScaffoldMessenger.of(context).showSnackBar(\\n const SnackBar(\\n content: Text(\'This button also adapts to the theme!\'),\\n duration: Duration(seconds: 2),\\n ),\\n );\\n },\\n icon: const Icon(Icons.check_circle),\\n label: const Text(\'Themed Button\'),\\n ),\\n ),\\n const SizedBox(height: 32),\\n const Center(\\n child: Text(\\n \'Toggle Switch Example:\',\\n style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),\\n ),\\n ),\\n const SizedBox(height: 8),\\n Center(\\n child: Switch(\\n value: Theme.of(context).brightness == Brightness.dark,\\n onChanged: (value) {\\n context.read<ThemeBloc>().add(\\n ThemeChanged(\\n value ? ThemeMode.dark : ThemeMode.light,\\n ),\\n );\\n },\\n ),\\n ),\\n ],\\n ),\\n ),\\n ),\\n );\\n }\\n}\\n
\\n通过本文,我们学习了如何:
\\n这种架构不仅适用于主题切换,也适合扩展到语言切换、布局模式等需要状态持久化的功能。
\\n如果你喜欢这篇文章,欢迎分享、收藏,或留言交流!\\n以上代码均提交 Github\\n
转载请声明出处!!!
什么是SliverAppBar
\\nSliverAppBar 类似于Android中的CollapsingToolbarLayout
,可以轻松实现页面头部展开、合并的效果。 与AppBar大部分的属性重合,相当于AppBar的加强版
现在大部分app在一些页面都会有在用户在下拉页面的时候固定显示bar而在页面顶部的隐藏此部分,这个功能是可以用SliverAppBar和监听页面滚动来实现的
\\n效果图:
\\n首先创建一个ScrollController因为我们要用到CustomScrollView,在创建一个bool来判断是否要显示bar
\\n添加监听页面滚动 判断的范围可以根据业务需求更改
\\nvoid _onScroll() {\\n // 获取当前滚动位置的偏移量\\n final offset = _scrollController.offset;\\n\\n // 当滚动偏移量超过10,并且当前显示 AppBar 状态为 false 时,更新状态使 AppBar 显示\\n if (offset > 10 && !showAppBar) {\\n setState(() => showAppBar = true);\\n }\\n // 当滚动偏移量小于等于10,并且当前显示 AppBar 状态为 true 时,更新状态使 AppBar 隐藏\\n else if (offset <= 10 && showAppBar) {\\n setState(() => showAppBar = false);\\n }\\n}\\n
\\n通过SliverLayoutBuilder来控制是否要显示SliverAppbar,SliverLayoutBuilder是一个可以在 Sliver 中动态构建 UI 的组件,允许你基于 SliverConstraints
动态调整 Sliver 布局。
SliverLayoutBuilder(\\n builder: (context, constraints) {\\n return showAppBar\\n ? SliverAppBar(\\n pinned: true, //是否固定\\n backgroundColor: Colors.white,\\n elevation: 0,\\n automaticallyImplyLeading: false,\\n toolbarHeight: 43.h,\\n flexibleSpace: FlexibleSpaceBar(\\n collapseMode: CollapseMode.pin,\\n background: _buildHeaderBar(),\\n ),\\n )\\n : SliverToBoxAdapter(child: SizedBox.shrink());\\n },\\n),\\n
\\n全部代码:
\\nclass MySliverPage extends StatefulWidget {\\n @override\\n _MySliverPageState createState() => _MySliverPageState();\\n}\\n\\nclass _MySliverPageState extends State<MySliverPage> {\\n final ScrollController _scrollController = ScrollController();\\n bool showAppBar = false;\\n\\n void _onScroll() {\\n // 获取当前滚动位置的偏移量\\n final offset = _scrollController.offset;\\n\\n // 当滚动偏移量超过10,并且当前显示 AppBar 状态为 false 时,更新状态使 AppBar 显示\\n if (offset > 10 && !showAppBar) {\\n setState(() => showAppBar = true);\\n }\\n // 当滚动偏移量小于等于10,并且当前显示 AppBar 状态为 true 时,更新状态使 AppBar 隐藏\\n else if (offset <= 10 && showAppBar) {\\n setState(() => showAppBar = false);\\n }\\n }\\n\\n @override\\n void initState() {\\n super.initState();\\n _scrollController.addListener(_onScroll);\\n }\\n\\n @override\\n void dispose() {\\n _scrollController.removeListener(_onScroll);\\n _scrollController.dispose();\\n super.dispose();\\n }\\n\\n Widget _buildHeaderBar() {\\n return Column(\\n children: [\\n 30.h.heightBox,\\n Row(\\n children: [\\n 15.w.widthBox,\\n ClipRRect(\\n borderRadius: BorderRadius.circular(15),\\n child: SizedBox(\\n width: 30.w,\\n height: 30.w,\\n child: CachedNetworkImage(\\n imageUrl: \'https://img2.baidu.com/it/u=3901868821,1751410039&fm=253&fmt=auto&app=120&f=JPEG?w=500&h=500\',\\n fit: BoxFit.cover,\\n ),\\n ),\\n ),\\n 10.w.widthBox,\\n Text(\\"lynxx\\", style: TextStyle(fontSize: 12.sp, color: Colors.black)),\\n Spacer(),\\n IconButton(\\n onPressed: () {},\\n icon: Icon(Icons.settings, color: Colors.black, size: 15),\\n ),\\n ],\\n ),\\n ],\\n );\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n body: CustomScrollView(\\n controller: _scrollController,\\n slivers: [\\n // 通过 SliverLayoutBuilder 来控制是否插入 SliverAppBar\\n SliverLayoutBuilder(\\n builder: (context, constraints) {\\n return showAppBar\\n ? SliverAppBar(\\n pinned: true, //是否固定\\n backgroundColor: Colors.white,\\n elevation: 0,\\n automaticallyImplyLeading: false,\\n toolbarHeight: 43.h,\\n flexibleSpace: FlexibleSpaceBar(\\n collapseMode: CollapseMode.pin,\\n background: _buildHeaderBar(),\\n ),\\n )\\n : SliverToBoxAdapter(child: SizedBox.shrink());\\n },\\n ),\\n\\n SliverList(\\n delegate: SliverChildBuilderDelegate(\\n (context, index) => ListTile(title: Text(\\"Item $index\\")),\\n childCount: 50,\\n ),\\n ),\\n ],\\n ),\\n );\\n }\\n}\\n
\\n觉得有帮助的点点赞😊
在Flutter
应用开发中,当处理复杂计算、大数据解析或图像处理时,常常面临界面卡顿
的挑战。Dart
语言采用单线程事件循环模型,这种设计在保证开发效率的同时,对计算密集型任务
的处理存在局限。
Isolate
作为并发解决方案,通过独立内存空间
和消息通信机制
,实现了真正的并行计算。
本文将系统解析Isolate
的运作原理、核心API
的使用规范,并通过企业级应用场景演示其工程实践。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\n\\n\\n\\n
Isolate
是Dart
提供的 独立并发执行单元。
其技术本质包含以下核心特性:
\\n①、内存隔离性:
\\n每个 Isolate
拥有独立的内存堆(Heap
),彼此之间不共享任何可变数据。这种设计意味着:
Isolate
之间传递状态。GC
)独立运行,不同 Isolate
的 GC
互不影响。②、消息通信机制
\\nIsolate
之间通过 消息传递 进行交互,具体实现方式:
SendPort
/ReceivePort
建立通信通道。对于基本类型
)或引用转移(对于 Transferable
对象)。Serializable
)。③、独立事件循环
\\n每个 Isolate
内部维护独立的 事件循环(Event Loop
) :
Microtask
和 Event Queue
)。Timer
、I/O
等异步操作。Isolate
的事件循环完全解耦。内存模型
\\n// Isolate A 的内存堆\\nvar dataA = [1,2,3]; \\n\\n// Isolate B 的内存堆(完全独立)\\nvar dataB = [4,5,6];\\n
\\n两个 Isolate
中的同名变量不会产生冲突,因为它们存在于不同的内存空间。
线程映射关系:
\\nDart
虚拟机(VM
)管理 Isolate
与操作系统线程的映射:
Isolate
可能绑定到一个固定的 OS
线程。VM
的调度策略决定)。启动开销:
\\n创建新 Isolate
需要约 30KB ~ 2MB
内存(因平台和 Dart
版本而异),典型耗时:
5ms ~ 15ms
Web
端:3ms ~ 10ms
(受浏览器 Worker
实现影响)。突破单线程瓶颈
技术背景:
\\nDart
的主事件循环采用单线程模型,当遇到 CPU
密集型任务时(例如图像处理
、复杂算法
、大数据解析
),会导致以下问题:
UI
线程阻塞:超过 16ms
的任务直接造成界面掉帧(60 FPS
的帧时间预算)。点击事件
、动画回调
无法及时处理。Isolate
的解决方案:
// 主 Isolate(UI 线程)\\nvoid processImage() async {\\n final receivePort = ReceivePort();\\n await Isolate.spawn(_imageProcessingIsolate, receivePort.sendPort);\\n \\n receivePort.listen((processedImage) {\\n // 更新 UI(耗时 2ms)\\n imageWidget.update(processedImage); \\n });\\n}\\n\\n// 子 Isolate(后台线程)\\nvoid _imageProcessingIsolate(SendPort mainPort) {\\n final image = _loadImageData(); // 耗时 200ms\\n final filtered = applyFilters(image); // 耗时 800ms\\n mainPort.send(filtered);\\n}\\n
\\n性能对比数据(处理 4K
图片滤波):
方案 | 总耗时 | UI 冻结时间 | 内存峰值 |
---|---|---|---|
主线程处理 | 1200ms | 1200ms | 450MB |
Isolate 处理 | 1000ms | 2ms | 210MB |
核心优势:
\\nUI
线程仅承担 0.1%
的耗时操作(消息传递
)。CPU
空闲周期。规避并发编程陷阱
传统多线程问题:
\\nRace Condition
):多个线程同时修改
共享数据。Deadlock
):资源竞争
导致的线程永久阻塞。垃圾回收失效
。Isolate
的安全机制:
// 示例:两个 Isolate 操作独立数据\\nvoid main() {\\n var data = List.generate(1e6, (i) => i); // 1MB 数据\\n \\n // Isolate A\\n Isolate.spawn((_) {\\n data.add(42); // 操作的是数据副本,原数据不变\\n }, null);\\n\\n // Isolate B \\n Isolate.spawn((_) {\\n data.removeLast(); // 同样操作副本\\n }, null);\\n}\\n
\\n内存模型特性:
\\nTransferableTypedData
实现零拷贝传输。const
值)。对比优势:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n并发模型 | 数据竞争风险 | 锁机制需求 | 调试难度 |
---|---|---|---|
传统多线程 | 高 | 必需 | 困难 |
Isolate | 无 | 无需 | 简单 |
多核 CPU 的工程化开发
硬件适配策略:
\\n现代移动设备的 CPU
核心分布示例:
1
个高性能核 + 3
个中核 + 4
个小核。4
个同构核心。Isolate
线程分配策略:
// CPU 核心感知的任务分发\\nfinal cpuCount = Platform.numberOfProcessors;\\nfinal isolates = List.generate(cpuCount, (i) => Isolate.spawn(_worker));\\n\\n// 工作 Isolate 绑定到特定核心(部分平台支持)\\nvoid _worker(SendPort port) {\\n if (Platform.isAndroid) {\\n Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);\\n }\\n // 执行计算...\\n}\\n
\\n性能提升案例(矩阵乘法 2048x2048
):
核心数 | 计算耗时(Isolate 池) | 加速比 |
---|---|---|
1 | 3200ms | 1x |
4 | 860ms | 3.7x |
8 | 420ms | 7.6x |
技术要点:
\\nIO
密集型:使用小核。Isolate
数量 ≤ 物理核心数。模块化与容错设计
业务隔离方案:
\\n// 架构示例:视频编辑应用\\n+---------------------+ +---------------------+\\n| UI Isolate | <---\x3e | Audio Processing |\\n| (Flutter UI 线程) | | Isolate |\\n+---------------------+ +---------------------+\\n ↑ ↑\\n | |\\n+---------------------+ +---------------------+\\n| Video Preview | <---\x3e | Export Isolate |\\n| Isolate | | (FFmpeg 封装) |\\n+---------------------+ +---------------------+\\n
\\n错误隔离优势:
\\nIsolate
崩溃不会导致应用整体退出。Supervisor
模式重启崩溃的 Isolate
。容错实现代码:
\\nclass IsolateSupervisor {\\n final List<Isolate> _pool = [];\\n \\n void startWorker() async {\\n try {\\n final isolate = await Isolate.spawn(_worker);\\n _pool.add(isolate);\\n } on IsolateSpawnException catch (e) {\\n // 重试逻辑...\\n }\\n }\\n\\n void _worker() {\\n // 子 Isolate 逻辑\\n try {\\n // 业务代码...\\n } catch (e) {\\n // 错误上报后安全退出\\n Isolate.exit();\\n }\\n }\\n}\\n
\\n评估维度 | Isolate 适用性 | 替代方案(Future/Compute ) |
---|---|---|
任务耗时 | >50ms | <50ms |
内存消耗 | 允许额外 2MB+ 开销 | 需严格控制内存 |
CPU 利用率 | 需多核并行 | 单核足够 |
错误隔离需求 | 关键任务需隔离 | 允许连带失败 |
通信频率 | 低频(<10次/秒) | 高频 |
典型应用场景:
\\nIsolate
分别处理音频流
、视频流
、特效渲染
。蒙特卡洛模拟
、神经网络推理
。日志文件
,合并统计结果。物理引擎
、AI
决策在独立 Isolate
运行。dart:isolate
库提供以下核心类:
类名 | 作用描述 |
---|---|
Isolate | 表示一个独立的执行环境,提供控制方法 |
SendPort | 消息发送端口,用于跨 Isolate 通信 |
ReceivePort | 消息接收端口,继承自 Stream<dynamic> |
Capability | 权限令牌,用于暂停/恢复控制 |
Isolate
的属性Isolate.current
Isolate
实例。Isolate
中获取自身引用;调试时打印当前 Isolate
信息。\\nvoid worker() {\\n print(\'Current isolate: ${Isolate.current.debugName}\');\\n}\\n
\\nIsolate.packageConfig
Isolate
的包配置。pauseCapability
void pause(Capability resumeCapability);\\nvoid resume(Capability resumeCapability);\\n
\\nfinal isolate = await Isolate.spawn(...);\\nfinal pauseToken = isolate.pauseCapability;\\n\\n// 暂停 Isolate\\nisolate.pause(pauseToken);\\n\\n// 恢复执行\\nisolate.resume(pauseToken);\\n
\\nerrorsAreFatal
Isolate
。\\nfinal isolate = await Isolate.spawn(_worker, params,\\n errorsAreFatal: false); // 关闭自动终止\\n\\nisolate.addErrorListener(receivePort.sendPort); // 自定义错误处理\\n
\\n.spawn()
:启动新的 Isolate
static Future<Isolate> spawn<T>(\\n void entryPoint(T message), // 新Isolate的入口函数,必须为静态或顶层函数\\n T message, // 传递给入口函数的初始消息,需可序列化\\n{ \\n bool paused = false, // 是否以暂停状态启动Isolate(需手动resume)\\n bool errorsAreFatal = true, // 未捕获异常是否导致Isolate终止(默认true)\\n SendPort? onExit, // Isolate退出时发送通知的端口(可选)\\n SendPort? onError, // 未捕获异常时发送错误信息的端口(可选)\\n String? debugName, // 调试时显示的Isolate名称(可选)\\n})\\n
\\n核心参数:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n参数 | 类型 | 说明 |
---|---|---|
entryPoint | void Function(T) | 新Isolate 的入口函数,必须为静态方法或顶层函数,且参数类型必须与 message 类型匹配。 |
message | T | 传递给 entryPoint 的初始消息,需满足 可序列化条件(基本类型、SendPort 、List /Map 等)。 |
配置参数:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n参数 | 默认值 | 说明 |
---|---|---|
paused | false | 若为 true ,新Isolate 会在启动后暂停,需调用 Isolate.resume() 启动执行。 |
errorsAreFatal | true | 若为 true ,未捕获的异常会导致Isolate 终止;若为 false ,Isolate 会继续运行(需搭配 onError 使用)。 |
onExit | null | Isolate 退出时,向此 SendPort 发送 null 或 exitCode (需提前绑定 ReceivePort )。 |
onError | null | 未捕获异常时,向此 SendPort 发送错误信息(格式:[error, stackTrace] )。 |
debugName | null | 调试时用于标识Isolate 的名称(如IDE 或DevTools 中显示)。 |
归纳总结:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n类别 | 关键点 |
---|---|
核心目的 | 创建独立的并发执行环境(Isolate ),通过消息传递通信。 |
入口限制 | entryPoint 必须为静态或顶层函数,且参数类型与 message 完全匹配。 |
生命周期 | 通过 onExit 监听退出事件,通过 errorsAreFatal 控制异常行为。 |
通信机制 | 使用 SendPort /ReceivePort 传递消息,onError 捕获未处理异常。 |
调试支持 | debugName 提升多Isolate 场景的可调试性。 |
参数限制:
\\nentryPoint
类型:
// ✅ 正确:静态方法或顶层函数\\nstatic void _entry(SendPort port) {}\\n// ❌ 错误:非静态方法(无法访问实例状态)\\nvoid _entry(SendPort port) {}\\n
\\nmessage
序列化:
// ✅ 允许:基本类型、SendPort、可序列化的集合\\nIsolate.spawn(_entry, 42);\\nIsolate.spawn(_entry, [1, SendPort()]);\\n// ❌ 错误:不可序列化对象(如 Function、Isolate)\\nIsolate.spawn(_entry, () => print(\'Invalid\'));\\n
\\n错误处理:
\\nonError
依赖:
errorsAreFatal = false
,必须设置 onError
,否则未处理错误会被静默忽略。\\nfinal errorPort = ReceivePort();\\nerrorPort.listen((error) => print(\'Error: $error\'));\\nawait Isolate.spawn(..., onError: errorPort.sendPort, errorsAreFatal: false);\\n
\\n跨Isolate
异常:
Isolate
不会自动捕获子Isolate
的异常,需通过 onError
显式监听。[error, stackTrace]
的列表。生命周期管理:
\\n资源释放:
\\n// 退出时关闭端口,防止内存泄漏\\nfinal exitPort = ReceivePort();\\nexitPort.listen((_) {\\n exitPort.close();\\n print(\'Isolate exited\');\\n});\\nawait Isolate.spawn(..., onExit: exitPort.sendPort);\\n
\\n暂停与恢复:
\\n// 创建暂停的Isolate\\nfinal isolate = await Isolate.spawn(..., paused: true);\\n// 手动恢复执行\\nisolate.resume(isolate.pauseCapability!);\\n
\\n.exit()
:终止当前 Isolate
,并可传递结果static void _isolateEntry(SendPort sendPort) {\\n Isolate.exit(sendPort, \'Task completed\');\\n}\\n
\\n.kill()
:强制终止指定 Isolate
void kill({ int priority = Isolate.beforeNextEvent })\\n
\\n优先级参数:
\\nIsolate.immediate
:立即终止。Isolate.beforeNextEvent
:处理完当前事件后终止。资源回收示例:
\\nfinal isolate = await Isolate.spawn(...);\\n\\n// 带资源清理的终止\\nvoid safeKill() {\\n receivePort.close();\\n isolate.kill(priority: Isolate.immediate);\\n}\\n
\\nSendPort.send()
void send(Object? message);\\n
\\n参数详解:
\\nmessage
:要发送的数据,支持大多数 Dart
对象,但需注意:\\nint
、String
、bool
等)、List
、Map
、SendPort
。核心功能:
\\nIsolate
消息传递:用于向目标 Isolate
发送消息,实现数据共享。Isolate
间不共享内存。基本用法:
\\n // 在主 Isolate 中\\nvoid main() async {\\n final receivePort = ReceivePort();\\n receivePort.listen((message) {\\n print(\'接收到子 Isolate 的消息: $message\');\\n });\\n\\n // 创建新 Isolate,并传递主 Isolate 的 SendPort\\n await Isolate.spawn(entryPoint, receivePort.sendPort);\\n}\\n\\n// 子 Isolate 入口函数\\nvoid entryPoint(SendPort mainSendPort) {\\n // 发送消息到主 Isolate\\n mainSendPort.send(\'Hello from子 Isolate!\');\\n}\\n
\\nReceivePort.listen()
:监听处理消息StreamSubscription<dynamic> listen(\\n void onData(dynamic message), {\\n Function? onError,\\n void onDone(),\\n bool? cancelOnError\\n})\\n
\\n参数详解:
\\nonData
回调:
dynamic
,需自行判断数据类型:receivePort.listen((message) {\\n if (message is String) {\\n print(\'字符串消息: $message\');\\n } else if (message is SendPort) {\\n print(\'收到对方的 SendPort\');\\n }\\n});\\n
\\nonError
回调(可选)
onError: (error) {\\n print(\'通信异常: $error\');\\n // 可在此处尝试重连或关闭端口\\n}\\n
\\nonDone
回调(可选)
close()
或 Isolate
终止)。onDone: () => print(\'通信通道已关闭\')\\n
\\ncancelOnError
(可选)
false
,设置为 true
时,发生错误后自动取消订阅。典型使用:
\\nreceivePort.listen(\\n (msg) => handleMessage(msg),\\n onError: (e, stack) => logError(e),\\n onDone: () => cleanup()\\n);\\n
\\n.run()
:简化一次性任务的执行,自动管理 Isolate
生命周期static Future<R> run<R>(\\n FutureOr<R> Function() computation, {\\n String? debugName,\\n})\\n
\\n参数详解:
\\n<R>
:指定任务函数的返回类型。computation
:需要在新 Isolate
中执行的函数,可返回 FutureOr<R>
(同步或异步结果))。\\nIsolate
可序列化 条件:\\nint
, String
等)、List
、Map
、SendPort
。ReceivePort
、非可序列化对象(如 File
、Socket
)。debugName
:可选,为 Isolate
设置调试名称(便于调试工具识别)。Future<R>
,最终结果或抛出异常。核心特性:
\\nIsolate
,无需手动处理 SendPort
或 ReceivePort
。结果或错误
,不支持持续消息传递。Isolate
的 Future
。Isolate.spawn()
,代码量减少 80%
以上。基本用法:
\\nvoid main() async {\\n //1、执行同步任务\\n final result = await Isolate.run(() => _sumNumbers(1, 1000000000));\\n print(\\"计算结果: $result\\"); // 输出:500000000500000000\\n \\n //2、异步任务\\n final data = await Isolate.run(() async {\\n // 异步获取数据\\n final response = await http.get(Uri.parse(\'https://api.example.com/data\'));\\n return jsonDecode(response.body);\\n });\\n print(\\"数据内容: $data\\");\\n}\\n\\n// 静态函数(必须)\\nstatic int _sumNumbers(int start, int end) {\\n int sum = 0;\\n for (int i = start; i <= end; i++) {\\n sum += i;\\n }\\n return sum;\\n}\\n
\\ncompute
:Isolate
的简化函数(顶级函数
)Future<R> compute<Q, R>(\\n ComputeCallback<Q, R> callback, \\n Q message, {\\n String? debugLabel,\\n})\\n
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n参数 | 类型 | 必填 | 说明 |
---|---|---|---|
callback | ComputeCallback<Q, R> | 是 | 要在后台 Isolate 中执行的函数,必须为 顶层函数 或 静态方法。 |
message | Q | 是 | 传递给 callback 的参数,需可序列化(JSON 兼容类型)。 |
debugLabel | String? | 否 | 调试标签,用于在开发工具中标识任务(如 Flutter DevTools )。 |
基本用法:
\\n// 定义全局函数(必须是顶层函数或静态方法)\\nint square(int number) {\\n return number * number;\\n}\\n\\n// 调用 compute\\nvoid calculateSquare() async {\\n int input = 5;\\n int result = await compute(square, input);\\n print(\'平方结果:$result\'); // 输出:平方结果:25\\n}\\n\\n// 使用 List 传递多个参数\\ndouble calculateArea(List<double> dimensions) {\\n double length = dimensions[0];\\n double width = dimensions[1];\\n return length * width;\\n}\\n\\n// 调用 compute\\nvoid computeArea() async {\\n List<double> params = [10.0, 5.0];\\n double area = await compute(calculateArea, params);\\n print(\'面积:$area\'); // 输出:面积:50.0\\n}\\n\\n// 或使用 Map 传递命名参数\\ndouble calculateVolume(Map<String, double> data) {\\n return data[\'length\']! * data[\'width\']! * data[\'height\']!;\\n}\\n\\nvoid computeVolume() async {\\n Map<String, double> params = {\\n \'length\': 3.0,\\n \'width\': 4.0,\\n \'height\': 5.0,\\n };\\n double volume = await compute(calculateVolume, params);\\n print(\'体积:$volume\'); // 输出:体积:60.0\\n}\\n
\\nIsolate
+ 进度条
)import \'package:flutter/material.dart\';\\nimport \'dart:isolate\';\\n\\nclass IsolateFactorialDemo extends StatefulWidget {\\n const IsolateFactorialDemo({super.key});\\n\\n @override\\n State createState() => _IsolateFactorialDemoState();\\n}\\n\\nclass _IsolateFactorialDemoState extends State<IsolateFactorialDemo> {\\n BigInt? _result;\\n bool _isLoading = false;\\n String _error = \'\';\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: const Text(\'阶乘计算\')),\\n body: Center(\\n child: Column(\\n mainAxisAlignment: MainAxisAlignment.center,\\n children: [\\n ElevatedButton(\\n onPressed: _isLoading ? null : _calculateFactorial,\\n child: const Text(\'计算1000!\'),\\n ),\\n const SizedBox(height: 20),\\n if (_isLoading) const CircularProgressIndicator(),\\n if (_error.isNotEmpty)\\n Text(\'错误: $_error\', style: const TextStyle(color: Colors.red)),\\n if (_result != null)\\n Padding(\\n padding: const EdgeInsets.all(20.0),\\n child: Text(\\n \'结果: ${_result.toString().substring(0, 50)}...\',\\n textAlign: TextAlign.center,\\n ),\\n ),\\n ],\\n ),\\n ),\\n );\\n }\\n\\n Future<void> _calculateFactorial() async {\\n setState(() {\\n _isLoading = true;\\n _error = \'\';\\n });\\n\\n final receivePort = ReceivePort();\\n final isolate = await Isolate.spawn(\\n _computeFactorial,\\n receivePort.sendPort,\\n onError: receivePort.sendPort,\\n );\\n\\n receivePort.listen((dynamic message) {\\n if (message is String) {\\n setState(() => _error = message);\\n } else {\\n setState(() => _result = message as BigInt);\\n }\\n receivePort.close();\\n isolate.kill();\\n _isLoading = false;\\n });\\n }\\n\\n static void _computeFactorial(SendPort sendPort) {\\n try {\\n const n = 10000;\\n BigInt result = BigInt.one;\\n for (var i = 2; i <= n; i++) {\\n result *= BigInt.from(i);\\n }\\n sendPort.send(result);\\n } catch (e) {\\n sendPort.send(\\"计算错误: $e\\");\\n }\\n }\\n}\\n
\\nJSON
解析 + 列表展示(使用 compute
)import \'package:flutter/material.dart\';\\nimport \'package:flutter/foundation.dart\';\\nimport \'dart:convert\';\\n\\nclass JsonParseDemo extends StatefulWidget {\\n const JsonParseDemo({super.key});\\n\\n @override\\n State createState() => _JsonParseDemoState();\\n}\\n\\nclass _JsonParseDemoState extends State<JsonParseDemo> {\\n List<User>? _users;\\n bool _isLoading = false;\\n\\n Future<void> _parseData() async {\\n setState(() => _isLoading = true);\\n\\n const jsonData = \'\'\'\\n [\\n {\\"name\\": \\"Alice\\", \\"age\\": 30, \\"email\\": \\"alice@example.com\\"},\\n {\\"name\\": \\"Bob\\", \\"age\\": 25, \\"email\\": \\"bob@example.com\\"},\\n {\\"name\\": \\"Charlie\\", \\"age\\": 35, \\"email\\": \\"charlie@example.com\\"}\\n ]\'\'\';\\n\\n try {\\n final users = await compute(_parseUsers, jsonData);\\n setState(() => _users = users);\\n } catch (e) {\\n print(\'解析错误: $e\');\\n } finally {\\n setState(() => _isLoading = false);\\n }\\n }\\n\\n static List<User> _parseUsers(String json) {\\n return (jsonDecode(json) as List)\\n .map((item) => User.fromJson(item))\\n .toList();\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: const Text(\'JSON 解析\')),\\n body: Column(\\n children: [\\n ElevatedButton(\\n onPressed: _isLoading ? null : _parseData,\\n child: const Text(\'解析数据\'),\\n ),\\n _isLoading\\n ? const LinearProgressIndicator()\\n : Expanded(\\n child: ListView.builder(\\n itemCount: _users?.length ?? 0,\\n itemBuilder: (context, index) {\\n final user = _users![index];\\n return ListTile(\\n title: Text(user.name),\\n subtitle: Text(\'${user.age} 岁 · ${user.email}\'),\\n );\\n },\\n ),\\n ),\\n ],\\n ),\\n );\\n }\\n}\\n\\nclass User {\\n final String name;\\n final int age;\\n final String email;\\n\\n User(this.name, this.age, this.email);\\n\\n factory User.fromJson(Map<String, dynamic> json) {\\n return User(\\n json[\'name\'],\\n json[\'age\'],\\n json[\'email\'],\\n );\\n }\\n}\\n
\\n双向通信
+ 聊天式 UI
import \'package:flutter/material.dart\';\\nimport \'dart:isolate\';\\n\\nclass TwoWayCommunicationDemo extends StatefulWidget {\\n const TwoWayCommunicationDemo({super.key});\\n\\n @override\\n State createState() => _TwoWayCommunicationDemoState();\\n}\\n\\nclass _TwoWayCommunicationDemoState extends State<TwoWayCommunicationDemo> {\\n final List<String> _messages = [];\\n late ReceivePort _receivePort;\\n SendPort? _childSendPort;\\n\\n @override\\n void initState() {\\n super.initState();\\n _startIsolate();\\n }\\n\\n void _startIsolate() async {\\n _receivePort = ReceivePort();\\n final isolate = await Isolate.spawn(_echoIsolate, _receivePort.sendPort);\\n\\n _receivePort.listen((message) {\\n if (message is SendPort) {\\n _childSendPort = message;\\n } else {\\n setState(() => _messages.add(\'子线程: $message\'));\\n }\\n });\\n }\\n\\n static void _echoIsolate(SendPort mainSendPort) {\\n final childReceivePort = ReceivePort();\\n mainSendPort.send(childReceivePort.sendPort);\\n\\n childReceivePort.listen((message) {\\n mainSendPort.send(\'收到: $message\');\\n });\\n }\\n\\n void _sendMessage(String text) {\\n if (_childSendPort != null) {\\n setState(() => _messages.add(\'主线程: $text\'));\\n _childSendPort!.send(text);\\n }\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: const Text(\'双向通信\')),\\n body: Column(\\n children: [\\n Expanded(\\n child: ListView.builder(\\n itemCount: _messages.length,\\n itemBuilder: (context, index) => ListTile(\\n title: Text(_messages[index],\\n style: TextStyle(\\n color: _messages[index].startsWith(\'主线程\')\\n ? Colors.blue\\n : Colors.green)),\\n ),\\n ),\\n ),\\n Padding(\\n padding: const EdgeInsets.all(8.0),\\n child: TextField(\\n onSubmitted: (text) => _sendMessage(text),\\n decoration: InputDecoration(\\n hintText: \'输入消息\',\\n suffixIcon: IconButton(\\n icon: const Icon(Icons.send),\\n onPressed: () => _sendMessage(\\n \'消息 ${_messages.length + 1}\',\\n ),\\n ),\\n ),\\n ),\\n ),\\n ],\\n ),\\n );\\n }\\n\\n @override\\n void dispose() {\\n _receivePort.close();\\n super.dispose();\\n }\\n}\\n
\\n技术/特性 | Future | Stream | Isolate | compute | Isolate.run() (Dart 2.15+) |
---|---|---|---|---|---|
核心用途 | 单次异步操作(延迟任务、网络请求等) | 连续异步事件(数据流、用户输入等) | 在独立线程执行耗时/CPU密集型任务 | 封装 Isolate,简化单次耗时任务 | 更简单的 Isolate 封装(类似 compute) |
是否阻塞UI线程 | ❌ 默认不阻塞 | ❌ 不阻塞 | ✅ 不阻塞(运行在独立线程) | ✅ 不阻塞 | ✅ 不阻塞 |
执行位置 | 主线程或异步线程(由事件循环决定) | 主线程(数据处理逻辑在主线程) | 独立线程 | 独立线程 | 独立线程 |
适用任务类型 | 单次异步操作、轻量级任务 | 连续数据流(如传感器数据、实时通信) | 长时间运行或 CPU 密集型任务 | 单次耗时任务(如 JSON 解析、计算) | 单次耗时任务(参数需可序列化) |
数据返回方式 | 返回单个结果(或错误) | 持续推送数据(通过 StreamController 或异步生成器) | 通过消息传递(SendPort/ReceivePort ) | 返回单个结果(需可序列化) | 返回单个结果(需可序列化) |
错误处理 | 支持 try/catch 或 .catchError | 通过 Stream.error 和 onError | 需手动传递错误消息 | 自动传递异常到主线程 | 自动传递异常到主线程 |
复杂度 | ⭐ 简单 | ⭐⭐ 中等 | ⭐⭐⭐ 复杂(需管理端口和通信) | ⭐ 简单(参数和返回值有限制) | ⭐ 简单(类似 compute) |
并发能力 | ❌ 单线程异步 | ❌ 单线程异步 | ✅ 多线程并行 | ✅ 单次多线程任务 | ✅ 单次多线程任务 |
内存共享 | 共享内存(需注意线程安全) | 共享内存(需注意线程安全) | ❌ 隔离内存(需消息传递) | ❌ 隔离内存(需消息传递) | ❌ 隔离内存(需消息传递) |
典型场景 | HTTP 请求、文件读写 | 实时聊天、传感器数据监听 | 图像处理、复杂算法计算 | 大数据解析、加密计算 | 替代 compute 的简单场景 |
使用建议:
\\n1、轻量异步任务:优先使用 Future
或 Stream
(视是否需要持续事件)。
2、耗时操作:
\\ncompute
或 Isolate.run()
。Isolate
(通过 ReceivePort/SendPort
)。3、流式数据处理:使用 Stream
(如 StreamBuilder
配合 UI
更新)。
Isolate
作为Dart
并发编程的核心机制,本质是通过内存隔离
和消息通信
实现的并行计算单元。
其技术价值体现在性能优化
、安全隔离
和资源利用
三个方面。工程实践中需要重点注意通信协议的规范化设计
、Isolate
生命周期的精确控制以及计算任务的合理拆分。
系统化使用Isolate
需要结合Worker Pool
模式、任务队列管理
和性能监控工具,才能充分发挥其并发优势。
\\n","description":"前言 在Flutter应用开发中,当处理复杂计算、大数据解析或图像处理时,常常面临界面卡顿的挑战。Dart语言采用单线程事件循环模型,这种设计在保证开发效率的同时,对计算密集型任务的处理存在局限。\\n\\nIsolate作为并发解决方案,通过独立内存空间和消息通信机制,实现了真正的并行计算。\\n\\n本文将系统解析Isolate的运作原理、核心API的使用规范,并通过企业级应用场景演示其工程实践。\\n\\n操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。\\n\\n一、基本概念\\n\\nIsolate 是 Dart 提供的 独立并发执行单元。\\n\\n其技术本质包含以下核心特性:\\n\\n①…","guid":"https://juejin.cn/post/7486421434576437298","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-28T03:31:51.343Z","media":null,"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"策略模式-淘宝订单列表的「按钮」实战","url":"https://juejin.cn/post/7486185012388757514","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
起因:
\\n重构代码的时候看到同事写的屎山代码,大概如下:
\\n// 该方法用于获取更多操作选项列表,并根据不同条件动态添加操作项\\n// 最后更新状态以反映这些操作选项和按钮文本\\nvoid getMoreOperations() {\\n // 初始化更多操作列表,默认包含一个 \\"Edit\\" 操作\\n final List<String> moreOperations = [\\"Edit\\"];\\n // 声明一个可空的字符串变量,用于存储按钮的第二文本\\n String? _btnText;\\n\\n // 如果客户端信息标志 1 为真,添加操作 1 到更多操作列表中\\n if (clientInfoFlag1 == true) {\\n moreOperations.add(OperationEnum.Operation1.label);\\n }\\n // 如果客户端信息标志 2 为真,添加操作 2 到更多操作列表中\\n if (clientInfoFlag2 == true) {\\n moreOperations.add(OperationEnum.Operation2.label);\\n }\\n\\n // 如果客户端信息标志 3 为真,添加操作 3 到更多操作列表中\\n if (clientInfoFlag3 == true) {\\n moreOperations.add(OperationEnum.Operation3.label);\\n }\\n\\n // 通用角色判断条件:满足角色标志 1 或角色标志 2 或角色标志 3\\n // 若满足条件,则添加 \\"Reassign\\" 操作到更多操作列表中\\n if (roleFlag1 || roleFlag2 || roleFlag3) {\\n moreOperations.add(OperationEnum.Reassign.label);\\n }\\n\\n // 如果允许呼叫\\n if (callEnabled) {\\n // 若可以创建试驾\\n if (createTestDrive) {\\n // 同时添加 \\"ReserveTestDrive\\" 和 \\"ImmediateTestDrive\\" 操作到更多操作列表中\\n moreOperations.addAll([OperationEnum.ReserveTestDrive.label, OperationEnum.ImmediateTestDrive.label]);\\n }\\n // 设置按钮第二文本为 \\"ContactCustomer\\"\\n _btnText = OperationEnum.ContactCustomer.label;\\n } else {\\n // 若不允许呼叫,但可以创建试驾\\n if (createTestDrive) {\\n // 添加 \\"ImmediateTestDrive\\" 操作到更多操作列表中\\n moreOperations.add(OperationEnum.ImmediateTestDrive.label);\\n // 设置按钮第二文本为 \\"ReserveTestDrive\\"\\n _btnText = OperationEnum.ReserveTestDrive.label;\\n }\\n }\\n\\n // 发出状态更新,将更多操作列表和按钮第二文本更新到状态中\\n emit(state.copyWith(moreOperations: moreOperations, btnSecondText: _btnText));\\n }\\n\\n// 该方法根据用户选择的操作执行相应的处理逻辑\\nvoid moreOperationAction(String operation, BuildContext context) {\\n // 通过传入的操作字符串获取对应的操作枚举\\n final operationEnum = OperationEnum.getButtonModelEnum(operation);\\n // 根据操作枚举进行不同的处理\\n switch (operationEnum) {\\n // 若选择的操作是 \\"Edit\\"\\n case OperationEnum.Edit:\\n // 调用编辑处理方法\\n edit();\\n break;\\n // 若选择的操作是 \\"Reassign\\"\\n case OperationEnum.Reassign:\\n // 调用通用处理程序的可分配处理方法,并传入回调函数\\n commonHandler.assignableHandler(context, callback: () {\\n // 触发通用事件跟踪\\n trackEvent(\\"GenericEvent1\\", \\"SubEvent1\\", {});\\n // 若角色标志 2 为真\\n if (roleFlag2) {\\n // 刷新页面,设置刷新标志为 true,并指定跟踪 ID 为 4\\n refreshPage(isRefresh: true, trackId: 4);\\n } \\n // 若角色标志 3 为真\\n else if (roleFlag3) {\\n // 刷新页面,设置刷新标志为 true,并指定跟踪 ID 为 5\\n refreshPage(isRefresh: true, trackId: 5);\\n } \\n // 其他情况\\n else {\\n // 刷新页面,设置刷新标志为 true,并指定跟踪 ID 为 7\\n refreshPage(isRefresh: true, trackId: 7);\\n }\\n });\\n break;\\n // 若选择的操作是 \\"ReserveTestDrive\\"\\n case OperationEnum.ReserveTestDrive:\\n // 调用预约试驾处理方法\\n reserveDrive();\\n break;\\n // 若选择的操作是 \\"ImmediateTestDrive\\"\\n case OperationEnum.ImmediateTestDrive:\\n // 进行通用导航,传入页面路径和参数\\n navigator?.pushNamed(\\"GenericPagePath\\",\\n arguments: {\\"customerName\\": clientInfo?.name, \\"phone\\": clientInfo?.mobile});\\n break;\\n // 若选择的操作是操作 2\\n case OperationEnum.Operation2:\\n // 调用通用处理程序的操作 2 处理方法,并传入回调函数\\n commonHandler.operation2Handler(context, callback: () {\\n // 触发通用事件跟踪\\n trackEvent(\\"GenericEvent1\\", \\"SubEvent2\\", {});\\n // 调用跟进回调方法\\n followCallBack();\\n // 刷新页面,设置刷新标志为 true,并指定跟踪 ID 为 1\\n refreshPage(isRefresh: true, trackId: 1);\\n });\\n break;\\n // 若选择的操作是操作 1\\n case OperationEnum.Operation1:\\n // 显示确认对话框,传入上下文、确认处理方法和确认消息\\n showDialogConfirm(\\n context: context, confirm: operation1Handler, child: \\"Some confirmation message\\");\\n break;\\n // 若选择的操作是操作 3\\n case OperationEnum.Operation3:\\n // 调用通用处理程序的操作 3 处理方法,并传入回调函数\\n commonHandler.operation3Handler(context, callback: () {\\n // 调用跟进回调方法\\n followCallBack();\\n // 刷新页面,设置刷新标志为 true,并指定跟踪 ID 为 1\\n refreshPage(isRefresh: true, trackId: 1);\\n });\\n break;\\n // 若选择的操作不在上述枚举范围内\\n default:\\n // 不做任何处理\\n break;\\n }\\n }\\n
\\n就是最简单的ifElse实现,也不能说他错。但是,实在是,又臭又长。接盘的时候,直接重构了一遍。
\\n这种场景是非常普遍的,可以参考一下淘宝的订单列表按钮实现,和上面业务代码的内容,结构基本一致
\\n接下来用策略模式重构(不懂没关系,看完下面的内容就秒懂🤌🤌🤌🤌🤌🤌)
\\n先说下大致想法:
\\n代码实现:
\\n// 该类用于存储操作按钮的数据信息\\nclass ActionButtonData {\\n // 按钮的名称,用于显示\\n final String name;\\n // 模板代码,用于标识按钮的类型或用途\\n final String templateCode;\\n\\n /// 用于展示不可操作的原因,弹窗使用\\n List<String>? markContent;\\n\\n // 流程 ID,可用于跟踪相关流程\\n String? flowId;\\n\\n /// 流程的状态,例如「不可操作」、「操作中」、「可操作」等,\\n /// 用于展示在主副按钮的右上角,以及更多操作的右边\\n String? actionDesc;\\n // 操作状态码,用于表示不同的操作状态\\n // 1 - 可操作、2 - 处理中、3 - 不可操作、21 - 已拒绝(仅特定场景使用)\\n int? actionStatusCode;\\n // 是否显示按钮\\n bool? display;\\n // 按钮是否可点击\\n bool? isClick;\\n // 按钮点击时的处理函数\\n Function(ActionButtonData)? onTapHandler;\\n // 按钮的优先级,用于排序显示\\n final int priority;\\n\\n // 判断按钮是否应该显示的方法\\n // 子类可重写此方法,在返回之前重置 markContent、actionDesc、actionStatusCode、display、isClick 等属性\\n bool shouldDisplay(Map<String, dynamic> dataMap) {\\n return true;\\n }\\n\\n // 构造函数,初始化按钮数据\\n ActionButtonData({\\n required this.name,\\n required this.templateCode,\\n this.markContent = const [],\\n this.priority = 0,\\n this.onTapHandler,\\n });\\n\\n // 从 JSON 数据创建 ActionButtonData 实例的构造函数\\n ActionButtonData.fromJson(Map<String, dynamic> json)\\n : name = json[\\"name\\"],\\n markContent = json[\\"markContent\\"] ?? [],\\n priority = json[\\"priority\\"] ?? 0,\\n onTapHandler = json[\\"onTapHandler\\"],\\n templateCode = json[\\"templateCode\\"];\\n}\\n
\\n实现淘宝订单的「再买一单」、「挑选服务」、「申请开票」按钮实战:
\\n挑选服务
\\n// 挑选服务按钮类,继承自 ActionButtonData\\nclass ServiceSelection extends ActionButtonData {\\n ServiceSelection() : super(name: \\"挑选服务\\", templateCode: \\"service_selection\\", priority: 5);\\n\\n @override\\n bool shouldDisplay(Map<String, dynamic> dataMap) {\\n bool result = false;\\n String orderId = \\"\\";\\n\\n // 检查订单类型是否为手机类型,并且订单状态为已完成\\n if (dataMap[\\"orderType\\"] == \\"phone\\" && dataMap[\\"orderStatus\\"] == \\"completed\\") {\\n result = true;\\n }\\n\\n if (dataMap[\\"orderId\\"] != null) {\\n orderId = dataMap[\\"orderId\\"];\\n }\\n\\n this.onTapHandler = (ActionButtonData _) {\\n // 假设这是通用的事件跟踪方法\\n trackEvent(\\"service_selection_button_tap\\", {\\"orderId\\": orderId});\\n trackEvent(\\"order_detail_buttons_click\\", {\\"templateCode\\": this.templateCode});\\n\\n Function refreshFunction = dataMap[\\"refreshFunction\\"];\\n\\n // 假设这是通用的页面导航方法\\n navigateToPage(\\"service_selection_page\\", {\\n \\"orderId\\": orderId,\\n \\"onSave\\": () {\\n // 挑选服务完成后刷新页面\\n refreshFunction(context: null);\\n }\\n });\\n };\\n\\n this.isClick = true;\\n return result;\\n }\\n}\\n
\\n申请开票
\\n\\n// 申请开票按钮类,继承自 ActionButtonData\\nclass InvoiceApplication extends ActionButtonData {\\n InvoiceApplication() : super(name: \\"申请开票\\", templateCode: \\"invoice_application\\", priority: 6);\\n\\n @override\\n bool shouldDisplay(Map<String, dynamic> dataMap) {\\n bool result = false;\\n String orderId = \\"\\";\\n\\n // 检查订单状态是否为已完成\\n if (dataMap[\\"orderStatus\\"] == \\"completed\\") {\\n // 获取订单完成时间\\n DateTime? completionTime = dataMap[\\"completionTime\\"] as DateTime?;\\n if (completionTime != null) {\\n // 计算当前时间\\n DateTime now = DateTime.now();\\n // 计算完成时间距今的天数\\n int daysPassed = now.difference(completionTime).inDays;\\n // 检查是否在 180 天内\\n if (daysPassed <= 180) {\\n result = true;\\n }\\n }\\n }\\n\\n if (dataMap[\\"orderId\\"] != null) {\\n orderId = dataMap[\\"orderId\\"];\\n }\\n\\n this.onTapHandler = (ActionButtonData _) {\\n // 假设这是通用的事件跟踪方法\\n trackEvent(\\"invoice_application_button_tap\\", {\\"orderId\\": orderId});\\n trackEvent(\\"order_detail_buttons_click\\", {\\"templateCode\\": this.templateCode});\\n\\n Function refreshFunction = dataMap[\\"refreshFunction\\"];\\n\\n // 假设这是通用的页面导航方法\\n navigateToPage(\\"invoice_application_page\\", {\\n \\"orderId\\": orderId,\\n \\"onSave\\": () {\\n // 申请开票完成后刷新页面\\n refreshFunction(context: null);\\n }\\n });\\n };\\n\\n this.isClick = true;\\n return result;\\n }\\n}\\n
\\n再买一单:
\\n\\n// 再买一单按钮类,继承自 ActionButtonData\\nclass BuyAgain extends ActionButtonData {\\n BuyAgain() : super(name: \\"再买一单\\", templateCode: \\"buy_again\\", priority: 999);\\n\\n @override\\n bool shouldDisplay(Map<String, dynamic> dataMap) {\\n bool result = false;\\n String orderId = \\"\\";\\n\\n // 检查订单状态是否为已完成\\n if (dataMap[\\"orderStatus\\"] == \\"completed\\") {\\n result = true;\\n }\\n\\n if (dataMap[\\"orderId\\"] != null) {\\n orderId = dataMap[\\"orderId\\"];\\n }\\n\\n this.onTapHandler = (ActionButtonData _) {\\n // 假设这是通用的事件跟踪方法\\n trackEvent(\\"buy_again_button_tap\\", {\\"orderId\\": orderId});\\n trackEvent(\\"order_detail_buttons_click\\", {\\"templateCode\\": this.templateCode});\\n\\n Function refreshFunction = dataMap[\\"refreshFunction\\"];\\n\\n // 假设这是通用的页面导航方法\\n navigateToPage(\\"buy_again_page\\", {\\n \\"orderId\\": orderId,\\n \\"onSave\\": () {\\n // 再买一单操作完成后刷新页面\\n refreshFunction(context: null);\\n }\\n });\\n };\\n\\n this.isClick = true;\\n return result;\\n }\\n}\\n
\\n最后是按钮列表的构建:
\\nList<ActionButtonData> buttonList = [];\\nif (context != null) {\\n buttonList = [\\n ViewLogistics(),\\n AddEvaluation(),\\n SelectService(),\\n ApplyForInvoice(),\\n DeleteOrder(),\\n SellAndReplace(),\\n AddToCart(),\\n Reorder(),\\n ];\\n // shouldDisplay方法入参构建省略\\n buttonList = buttonList.where((element) => element.shouldDisplay(dataMap) == true).toList();\\n buttonList.sort((a, b) => b.priority.compareTo(a.priority));\\n}\\n
\\n总结:将一大坨的ifElse统一判断按钮,响应按钮,改成让各自业务按钮类自行决定。这样可以大大解耦。(看完赶紧去重构同事的屎山代码!!!!!!!!!!)
\\n(用copilot自我审视了一下,你说得对,下次重构我再改😭)
\\n优点
\\n缺点
\\n叠甲:以上代码完全由豆包将业务代码脱敏加工而成,如有语法错误等,与本人无关
","description":"起因: 重构代码的时候看到同事写的屎山代码,大概如下:\\n\\n// 该方法用于获取更多操作选项列表,并根据不同条件动态添加操作项\\n// 最后更新状态以反映这些操作选项和按钮文本\\nvoid getMoreOperations() {\\n // 初始化更多操作列表,默认包含一个 \\"Edit\\" 操作\\n final ListStream
—— 快递分拣中心的智能传送带系统
。
在移动应用中,数据就像血液
一样在应用中流动:用户滑动列表时不断加载的新条目、聊天室实时刷新的消息、文件下载时跳动的进度条等,这些场景背后都隐藏着一个关键问题:如何高效管理持续产生的异步数据?
传统的Future
虽然能处理单次异步操作,但面对连绵不断的数据流却显得力不从心
。Stream
正是为此而生 —— 它像一条智能传送带,让数据按需流动、精准分发。然而许多开发者仅停留在简单使用listen()
,却未能真正理解其设计哲学。
本文将带你穿透API
表层,用系统化思维构建Stream
知识体系,最终通过企业级实战案例,让你掌握\\"让数据流动\\"
的艺术。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\n\\n\\n官方定义:
\\nStream
是Dart
中表示连续异步数据序列的核心对象,用于处理多个按时间顺序传递
的异步事件。
这些事件可以是:
\\nData Event
) :携带业务数据(如网络响应
、用户输入
)。Error Event
) :传递异常信息(如网络超时
、数据解析失败
)。Done Event
) :标识数据流终止(如文件读取完成
)。其核心设计目标是解耦数据生产与消费
,允许数据在“生产者”
和“消费者”
之间按需流动,双方无需同步等待彼此。
可将想象成一个快递分拣中心的智能传送带系统:
\\n用户操作
、网络响应
、文件内容
)就像快递包裹,可能包含有效数据、错误信息(破损包裹
)或完成信号(运输结束
)。FIFO
原则),但包裹的产生和分拣是异步进行的。map
、where
等操作符(类似分拣规则
)对包裹进行拆包
、检查
、重新打包
。StreamController
负责启动传送带、调节流速(pause/resume
)或关闭系统(close
)。深入理解:
\\n数据源
)将包裹逐个放到传送带上,无需一次性堆满所有包裹(避免内存爆炸
)。消费者
)无需守在传送带起点,包裹到达时会自动触发分拣动作(监听回调
)。操作符链
),依次完成过滤
、变形
、合并
等操作(如stream.map(...).where(...)
)。核心思想:
\\n\\n\\n\\n
Stream
的本质是一个高度可定制的异步分拣系统,开发者扮演分拣中心总控师的角色,通过定义包裹来源(生产者
)、分拣规则(操作符
)、派送策略(监听逻辑
),实现数据从源头到终点的智能流动。掌握Stream
,就是掌握让数据像快递包裹一样高效
、精准
、有序流动
的能力。
非阻塞
方式传递,消费者通过订阅(listen
)被动接收事件,无需主动轮询
。回调函数
或await for
处理。FIFO
)顺序传递,保证处理顺序与发送顺序一致。Single-Subscription
) :仅允许一个监听者
,确保数据完整性和顺序性(默认类型
)。Broadcast
) :允许多个监听者
,适用于事件广播场景(需显式声明isBroadcast: true
)。Cold
)与热流(Hot
)async*
生成的流)。StreamController
创建的流)。包裹来源:
\\n发件人(生产者) :可能是仓库(StreamController
)、自动包裹生成机(async*
函数),或外部快递联盟(WebSocket
)。
包裹产生规则:
\\n// 示例:模拟快递包裹生成(每秒产生一个包裹) \\nStream<String> generatePackages() async* { \\n int count = 1; \\n while (true) { \\n await Future.delayed(Duration(seconds: 1)); \\n yield \\"包裹-$count\\"; // 生成包裹,如\\"包裹-1\\", \\"包裹-2\\" \\n count++; \\n } \\n} \\n
\\n分拣流程:
\\n单线分拣 vs
多线分拣:
Single-Subscription Stream
) :监听者
),确保包裹顺序严格一致(如重要文件必须按顺序签收
)。Broadcast Stream
) :多个分拣机器人同时扫描包裹条形码
)。冷流 vs
热流:
类型 | 快递场景类比 | Stream 行为 |
---|---|---|
冷流 | 预约制快递:客户下单后,仓库才开始打包发货 | 每次监听时重新开始生成数据(async* 生成的流) |
热流 | 实时物流:包裹已在运输中,新客户只能收到当前及之后的包裹 | 数据实时流动,与监听时机无关(StreamController 创建的流) |
分拣控制:
\\nsubscription.pause()
(暂停分拣)subscription.resume()
subscription.cancel()
(工人离开岗位)或controller.close()
(关闭分拣中心)持续性异步数据流
的天然需求许多场景中,数据并非一次性产生,而是持续
、动态
、按节奏
生成的。例如:
聊天消息
、WebSocket
推送需要持续监听并处理 。屏幕滑动事件
、输入框内容变化
需要实时响应。传感器数据
、设备状态
需要周期性采集。Stream
为此类场景提供了原生支持,无需手动管理事件队列
或定时轮询
,只需通过listen()
订阅数据流,即可自动接收并处理异步事件。
资源高效利用
与内存安全
传统一次性异步操作(如Future
)在处理大规模数据时面临挑战:
1GB文件
),可能引发内存溢出
。Stream
通过逐块处理数据解决这些问题:
// 逐行读取大文件,内存占用恒定 \\nFile(\'large_file.txt\') \\n .openRead() // 创建字节流 \\n .transform(utf8.decoder) // 转换为文本流 \\n .transform(LineSplitter()) // 拆分为行数据流 \\n .listen((line) => processLine(line)); // 逐行处理 \\n
\\n数据像流水一样逐块流过处理管道,每个数据块处理完成后立即释放内存,确保应用高效稳定运行。
\\nStream
允许通过链式操作符组合数据处理逻辑,显著提升代码可读性
和可维护性
。
搜索框优化示例
\\nsearchInput.stream \\n .distinct() // 去重:输入内容相同时跳过 \\n .where((query) => query.length > 2) // 过滤:至少3个字符才搜索 \\n .asyncMap((query) => fetchResults(query)) // 异步获取结果 \\n .listen(updateUI); // 更新界面 \\n
\\n通过声明式管道清晰表达业务规则
,避免回调嵌套,逻辑层次分明。
Flutter
生态深度整合Stream
是Flutter
响应式编程体系的核心基础设施:
BLoC
、Riverpod
等库依赖Stream
实现状态变化通知。UI
更新:StreamBuilder
组件将数据流自动映射到界面重建。Stream
实现松散耦合的组件间数据传递。计数器应用:
\\n// BLoC类管理计数逻辑 \\nclass CounterBloc { \\n final _counterController = StreamController<int>(); \\n int _count = 0; \\n\\n Stream<int> get counter => _counterController.stream; \\n\\n void increment() { \\n _count++; \\n _counterController.sink.add(_count); \\n } \\n\\n void dispose() => _controller.close(); \\n} \\n\\n// UI层通过StreamBuilder绑定数据 \\nStreamBuilder<int>( \\n stream: counterBloc.counter, \\n builder: (context, snapshot) { \\n return Text(\'Count: ${snapshot.data ?? 0}\'); \\n }, \\n) \\n
\\n这种模式将业务逻辑
与UI
彻底解耦,提升代码可测试性和可维护性。
Stream
提供丰富的组合操作符,简化多数据流协作:
Stream.asyncExpand
同时处理多个异步任务StreamZip
同步多个流的最新数据handleError
实现细粒度的异常捕获与恢复表单多字段验证:
\\nfinal usernameStream = usernameController.stream; \\nfinal passwordStream = passwordController.stream; \\n\\nStream<bool> get isFormValid => \\n StreamZip([usernameStream, passwordStream]) \\n .map((credentials) => \\n credentials[0].isNotEmpty && credentials[1].length >= 6) \\n .distinct(); // 仅在验证状态变化时触发 \\n
\\n通过流合并自动跟踪多个输入字段状态,实时更新表单提交按钮的可用性。
\\n维度 | 传统方式缺陷 | Stream 解决方案 |
---|---|---|
数据处理模式 | 一次性加载,内存压力大 | 逐块流式处理,内存占用恒定 |
代码可维护性 | 回调嵌套,逻辑分散 | 声明式管道,逻辑集中可追溯 |
实时响应能力 | 轮询开销大,响应延迟 | 订阅推送机制,即时触发 |
复杂场景支持 | 手动协调多任务,易出错 | 内置操作符处理合并、错误、流量控制 |
掌握Stream
意味着获得:
可观测
、可控制
的管道。操作符组合
解决复杂异步问题。Flutter
响应式生态,构建可扩展应用。Stream
不是可选技能,而是处理异步编程的核心基础设施。它如同编程世界的“中枢神经系统”
,让数据在应用中智能流动,驱动功能模块高效协作。
Stream
属性详解属性 | 类型 | 作用描述 | 注意事项 |
---|---|---|---|
isBroadcast (原生) | bool | 标识是否为广播流(允许多个监听者)。 | 单订阅流不可重复监听;广播流需通过asBroadcastStream() 或StreamController.broadcast() 创建。 |
isEmpty (原生) | Future<bool> | 异步判断流是否为空(无任何数据事件)。 | 需等待流完成;调用后消费数据,后续无法监听。 |
first (扩展) | Future<T> | 获取流的第一个数据事件(流为空时抛出错误)。 | 若流未关闭且无数据会永久等待;需用await 或catchError 处理。 |
last (扩展) | Future<T> | 获取流的最后一个数据事件(流为空时抛出错误)。 | 必须等待流关闭;对无限流无效。 |
single (扩展) | Future<T> | 检查流是否仅有一个数据事件(流为空或有多个数据时抛出错误)。 | 必须等待流关闭;误用于多数据流会抛出错误。 |
length (扩展) | Future<int> | 计算流中数据事件的总数量。 | 会消费整个流的数据;对无限流导致永久阻塞。 |
关键说明:
\\n原生属性:直接定义在 Stream
类中(如 isBroadcast
、isEmpty
)。
扩展方法:通过 dart:async
扩展机制实现,语法类似属性(如 stream.first
),本质为异步操作。
通用规则:除 isBroadcast
外,其他属性/方法均需 await
;直接调用会消费流数据,导致流无法重复使用。
开发建议:
\\nStreamBuilder
或显式 listen()
处理流数据。first
/last
等扩展方法,除非明确需要单次数据消费,建议通过 take(1)
或 where
限制数据范围。subscription.cancel()
)。常见问题(FAQ
):
Q: 为什么 isEmpty
返回 Future<bool>
而不是 bool
?
\\nA: 流的异步特性决定了必须等待数据传递完成才能判断是否为空。
Q: isBroadcast
为 true
时是否一定支持多监听?
\\nA: 是的,但需确保数据源支持多监听(如 StreamController.broadcast
)。
Q: 如何避免 single
方法抛出错误?
\\nA: 使用 take(1)
限制数据量:
final result = await stream.take(1).single; \\n
\\nStreamController
属性详解属性 | 类型 | 作用描述 | 注意事项 |
---|---|---|---|
stream | Stream<T> | 控制器关联的输出流,供外部监听数据。 | 必须通过 listen() 订阅后才能接收数据。 |
sink | StreamSink<T> | 数据入口,用于添加数据(add() )、错误(addError() )或关闭流(close() )。 | 调用 close() 后继续添加数据会抛出 StateError 。 |
isClosed | bool | 标识控制器是否已关闭(不再接收新数据)。 | 关闭后不可逆,需通过 controller.close() 终止。 |
isPaused | bool | 标识流是否被暂停(通过 pause() 方法)。 | 需手动调用 resume() 恢复数据流动。 |
hasListener | bool | 标识是否有活跃的监听者(通过 listen() 订阅且未取消)。 | 广播流中可能有多个监听者,但此属性仅表示至少存在一个。 |
done | Future<void> | 控制器关闭时完成的 Future (通过 close() 触发)。 | 用于等待流完全关闭(如资源释放后执行回调)。 |
关键说明:
\\n①、isPaused
:
pause()
会暂停数据发送,但不会缓存数据,恢复后仅发送后续数据。StreamController(sync: true)
或自定义缓冲逻辑。②、done
:
await controller.close(); \\n await controller.done; // 确保流完全关闭 \\n
\\n③、hasListener
:
hasListener
为 true
,取消订阅后为 false
。hasListener
为 true
。④、sink
与 stream
的对称性:
sink
是写入端,stream
是读取端,二者共同构成完整的流管道。完整生命周期示例:
\\nfinal controller = StreamController<int>(); \\n\\n// 监听数据流 \\nfinal subscription = controller.stream.listen(print); \\n\\n// 添加数据 \\ncontroller.sink.add(1); \\ncontroller.sink.add(2); \\n\\n// 暂停流 \\ncontroller.sink.pause(); \\nprint(controller.isPaused); // 输出:true \\n\\n// 恢复流 \\ncontroller.sink.resume(); \\n\\n// 关闭流 \\nawait controller.close(); \\nprint(controller.isClosed); // 输出:true \\n\\n// 等待流完全关闭 \\nawait controller.done; \\n
\\n.fromIterable(Iterable<T> elements)
作用:从同步集合(如 List
、Set
)创建流,按顺序发射所有元素后关闭。
\\n场景:将内存中的数据集转换为流式处理管道。
Stream<int> stream = Stream.fromIterable([1, 2, 3]); \\nstream.listen(print); // 输出:1 → 2 → 3 → onDone \\n
\\n注意事项:
\\nasync*
生成器。.fromFuture(Future<T> future)
作用:将单个 Future
转换为流,发射其结果(成功值或错误
)后关闭。
\\n场景:将异步操作(如网络请求
、文件 I/O
)整合到流中。
Future<int> fetchData() async => 42; \\nStream.fromFuture(fetchData()).listen(print); // 输出:42 → onDone \\n
\\n注意事项:
\\nFuture
失败,流会发射错误事件并关闭。.fromFutures(Iterable<Future<T>> futures)
作用:将多个 Future
转换为流,发射其结果(成功值或错误
)后关闭。
\\n场景:将多个异步操作整合到流中。
Future<int> waitTask() async {\\n await Future.delayed(Duration(seconds: 2));\\n return 10;\\n}\\n\\nFuture<String> doneTask() async {\\n await Future.delayed(Duration(seconds: 5));\\n return \'Future complete\';\\n}\\n\\nvoid main() {\\n final stream = Stream<Object>.fromFutures([doneTask(), waitTask()]);\\n stream.listen(print, onDone: () => print(\'Done\'), onError: print);\\n}\\n\\n// 输出:\\n// 10(2秒后)\\n// Future complete(5秒后)\\n// Done\\n
\\n注意事项:
\\nlisten()
,否则抛出 StateError
。Future
抛出错误会触发流的 onError
,但流继续处理其他 Future
。Future
完成顺序发射,非 futures
的原始顺序。.periodic(Duration period, [T Function(int)? computation])
作用:周期性生成事件流
,每个周期触发一次。
\\n场景:定时任务(如轮询
、心跳检测
)。
// 每秒发射一个递增整数 \\nStream.periodic(Duration(seconds: 1), (count) => count) \\n .take(3) \\n .listen(print); // 输出:0 → 1 → 2 → onDone \\n
\\n注意事项:
\\ncomputation
可为空,此时事件值为 null
。take
/takeWhile
限制事件数量,避免无限流。Stream.value(T value)
作用:创建单值流
,立即发射数据后关闭。
\\n场景:快速包装常量
或简单值
。
Stream.value(\\"Hello\\").listen(print); // 输出:Hello → onDone \\n
\\n注意事项:
\\nStream.fromIterable([value])
,但更简洁。.error(Object error, [StackTrace? stackTrace])
作用:创建错误流
,立即发射错误后关闭。
\\n场景:模拟错误场景或传递异常。
Stream.error(Exception(\\"Timeout\\")) \\n .listen(null, onError: print); // 输出:Exception: Timeout → onDone \\n
\\n注意事项:
\\nonError
,否则错误会向上传播。.empty()
作用:创建空流
,直接触发完成事件。
\\n场景:占位
或条件分支
中无数据的场景。
Stream.empty().listen( \\n print, \\n onDone: () => print(\\"Completed\\"), \\n); // 输出:Completed \\n
\\n.multi(void onListen(StreamController<T> controller))
作用:手动控制流事件
,通过回调暴露 StreamController
。
\\n场景:需要动态生成数据
或自定义复杂逻辑
。
Stream<int> countStream(int max) { \\n return Stream.multi((controller) { \\n for (int i = 1; i <= max; i++) { \\n controller.add(i); \\n } \\n controller.close(); \\n }); \\n} \\ncountStream(3).listen(print); // 输出:1 → 2 → 3 → onDone \\n
\\n注意事项:
\\n.eventTransformed(Stream source, EventSink<T> transform(EventSink<T> sink))
作用:通过自定义 EventSink
转换源流的事件。
\\n场景:实现底层流转换逻辑(如自定义协议解析
)。
final source = Stream.fromIterable([1, 2, 3]); \\nfinal transformed = Stream.eventTransformed(source, (sink) => MyCustomSink(sink)); \\n
\\n注意事项:
\\nEventSink
接口,处理 add
、addError
、close
事件。transform
+ StreamTransformer
。场景 | 推荐方法 | 替代方案 |
---|---|---|
同步数据集转流 | fromIterable | async* 生成器 |
单次异步操作转流 | fromFuture | Future.then + StreamController |
多个 Future 处理 | fromFutures | fromIterable + asyncMap |
定时/周期性事件 | periodic | Timer.periodic + StreamController |
快速包装单值 | value | fromIterable([value]) |
错误模拟或传递 | error | throw + async* |
动态生成数据流 | multi | 自定义 StreamTransformer |
第一梯队
几乎每个 Stream
使用场景都会用到的方法,覆盖 80%
的日常开发需求。
listen(void onData(T event)?, {onError, onDone, cancelOnError})
作用:订阅流并处理数据
、错误
和完成事件
,返回 StreamSubscription
对象。
final subscription = stream.listen(\\n (data) => print(data),\\n onError: (e) => print(\\"Error: $e\\"),\\n onDone: () => print(\\"Done\\"),\\n);\\n
\\n注意事项:
\\nsubscription.cancel()
避免内存泄漏。cancelOnError
:默认为 false
,若设为 true
,第一个错误会取消订阅。map<S>(S Function(T) convert)
作用:同步转换每个数据事件。
\\nstream.map((num) => num * 2).listen(print); // 输入1 → 输出2 \\n
\\n注意事项:
\\n无副作用
)。where(bool Function(T) test)
作用:过滤不符合条件的数据事件。
\\nstream.where((num) => num > 10).listen(print); \\n
\\n注意事项:
\\nasyncMap<S>(FutureOr<S> Function(T) convert)
作用:异步转换每个数据事件(返回 Future
)。
stream.asyncMap((id) => fetchUser(id)).listen(print); \\n
\\n注意事项:
\\nhandleError(Function onError, {bool test(Object error)?})
作用:捕获并处理流中的错误事件。
\\nstream.handleError(\\n (e) => print(\\"Handled: $e\\"),\\n test: (e) => e is NetworkException,\\n);\\n
\\n注意事项:
\\ntake(int count)
作用:仅取前 count
个数据后关闭流。
stream.take(3).listen(print); // 仅接收前3个数据 \\n
\\nskip(int count)
作用:跳过前 count
个数据。
stream.skip(2).listen(print); // 跳过前2个数据 \\n
\\n第二梯队
用于复杂流控制或数据流整形,覆盖 15%
的中级场景。
expand<S>(Iterable<S> Function(T) convert)
作用:将每个数据事件展开为多个事件(类似 flatMap
)。
stream.expand((num) => [num, num * 10]).listen(print); // 输入1 → 输出1,10 \\n
\\ntakeWhile(bool Function(T) test)
作用:取数据直到条件为 false
,之后关闭流。
stream.takeWhile((num) => num < 5).listen(print); \\n
\\nskipWhile(bool Function(T) test)
作用:跳过数据直到条件为 false
,之后保留剩余数据。
stream.skipWhile((num) => num < 3).listen(print); \\n
\\ndistinct([bool Function(T, T)? equals])
作用:跳过连续重复的数据事件。
\\nstream.distinct().listen(print); // 输入1,1,2 → 输出1,2 \\n
\\n第三梯队
用于底层流操作或资源控制,覆盖 5%
的高级场景。
transform<S>(StreamTransformer<T, S> transformer)
作用:应用自定义转换器(如编解码、协议解析)。
\\ndart\\nstream.transform(utf8.decoder).listen(print); \\n
\\n注意事项:
\\nStreamTransformer
接口。pipe(StreamConsumer<T> consumer)
**作用:将流数据直接传输到 StreamConsumer
(如文件写入)。
stream.pipe(File(\'output.txt\').openWrite());\\n
\\ndrain<T>([T? futureValue])
作用:消费流中所有剩余数据但不处理,用于资源清理。
\\nawait stream.drain(); // 确保流完全消费 \\n
\\ncast<S>()
作用:将流的数据类型强制转换为指定类型。
\\nStream<num> numbers = Stream<int>.fromIterable([1, 2, 3]).cast<num>();\\n
\\nasBroadcastStream()
作用:将单订阅流转换为广播流。
\\nfinal broadcastStream = stream.asBroadcastStream();\\n
\\n第四梯队
用于特定场景的聚合操作或边缘需求。
\\ncontains(Object? value)
作用:检查流是否包含指定值,返回 Future<bool>
。
final exists = await stream.contains(42);\\n
\\nforEach(void Function(T) action)
作用:对每个数据执行操作,返回 Future<void>
。
await stream.forEach(print); \\n
\\nreduce(T Function(T, T) combine)
作用:聚合所有数据为单个结果(需流非空)。
\\nfinal sum = await stream.reduce((a, b) => a + b); \\n
\\njoin([String separator = \\"\\"])
作用:将流中的数据拼接为字符串。
\\nfinal result = await stream.join(\\",\\"); \\n
\\nevery(bool Function(T) test)
作用:检查所有数据是否满足条件,返回 Future<bool>
。
final allValid = await stream.every((num) => num > 0); \\n
\\n梯队 | 方法 | 核心用途 | 使用频次 |
---|---|---|---|
第一梯队 | listen , map , where | 基础数据订阅、转换和过滤 | 极高 |
第二梯队 | expand , takeWhile | 流数据整形与控制 | 中 |
第三梯队 | transform , asBroadcast | 底层流操作与资源管理 | 低 |
第四梯队 | contains , reduce | 聚合操作与边缘需求 | 极低 |
Stream
的基本使用流程可分为四步:创建流 → 监听流 → 操作流 → 关闭流。
// 1、从集合创建 同步数据流\\nStream<int> stream = Stream.fromIterable([1, 2, 3]);\\n\\n// 2、从异步任务创建 异步数据流\\nStream<int> stream =\\n Stream.fromFuture(Future.delayed(Duration(seconds: 1), () => 42));\\n\\n// 3、创建周期性数据流 每秒发射递增整数\\nStream<int> stream =\\n Stream.periodic(Duration(seconds: 1), (count) => count);\\n
\\nStreamController
(动态控制流
)final controller = StreamController<int>(); \\n\\n// 添加数据 \\ncontroller.sink.add(1); \\ncontroller.sink.addError(Exception(\\"Error\\")); \\ncontroller.sink.close(); \\n\\n// 获取输出流 \\nStream<int> outputStream = controller.stream; \\n
\\nStreamSubscription<int> subscription = stream.listen(\\n (data) => print(\\"数据: $data\\"), // 处理数据\\n onError: (error) => print(\\"错误: $error\\"), // 处理错误\\n onDone: () => print(\\"流已关闭\\"), // 处理完成\\n cancelOnError: true, // 第一个错误时自动取消订阅\\n); \\n
\\n// 取消订阅\\nsubscription.cancel(); // 释放资源,防止内存泄漏 \\n\\n//暂停/恢复\\nsubscription.pause(); // 暂停接收数据 \\nsubscription.resume(); // 恢复接收数据 \\n
\\n// 同步转换(map)\\nstream.map((num) => num * 2).listen(print); // 输入1 → 输出2 \\n\\n// 异步转换(asyncMap)\\nstream.asyncMap((id) => fetchUser(id)).listen(print); // 异步请求用户数据 \\n
\\n// 条件过滤(where)\\nstream.where((num) => num > 10).listen(print); // 只保留大于10的数据 \\n\\n// 数量限制(take/skip)\\nstream.take(3).listen(print); // 仅取前3个数据 \\nstream.skip(2).listen(print); // 跳过前2个数据 \\n
\\n// 全局捕获错误(`handleError`)\\nstream.handleError(\\n (e) => print(\\"捕获错误: $e\\"), \\n test: (e) => e is NetworkException // 只处理特定错误\\n).listen(print); \\n
\\nStreamController
controller.close(); // 关闭控制器,触发onDone事件 \\n
\\nawait for
处理流try {\\n await for (final data in stream) {\\n print(data);\\n }\\n} catch (e) {\\n print(\\"捕获错误: $e\\");\\n} \\n
\\n// 取消所有订阅\\nsubscription.cancel(); \\n\\n// 释放控制器\\nif (!controller.isClosed) { \\n await controller.close(); \\n} \\n
\\n实时搜索功能
// 1. 创建输入流(如搜索框文本变化) \\nfinal searchController = StreamController<String>(); \\n\\n// 2. 操作流:过滤空值、请求API \\nsearchController.stream \\n .where((query) => query.isNotEmpty) // 过滤空字符串 \\n .asyncMap((query) => fetchSearchResults(query)) // 异步搜索 \\n .listen(updateUI); // 更新界面 \\n\\n// 3. 模拟用户输入 \\nsearchController.sink.add(\\"Dart\\"); \\nsearchController.sink.add(\\"Flutter\\"); \\n\\n// 4. 关闭资源 \\nawait searchController.close(); \\n
\\nStream
的运行机制可分为三个层次:
①、生产者层
\\n用户交互
、网络请求
、文件 I/O
。async*
函数按需生成数据。StreamController
手动管理数据推送。②、处理管道
\\nmap
、where
、transform
等方法对数据流进行转换
、过滤
、聚合
。③、消费者层
\\nlisten()
或await for
监听数据流。cancel()
终止监听,或close()
关闭数据源。核心代码结构解析:
\\n// 1. 生产者层:创建数据流 \\nStream<int> generateData() async* { \\n for (int i = 1; i <= 5; i++) { \\n await Future.delayed(Duration(seconds: 1)); \\n yield i; // 每秒发送一个数字 \\n } \\n} \\n\\n// 2. 处理管道:转换与过滤 \\nfinal processedStream = generateData() \\n .map((num) => num * 2) // 数值翻倍 \\n .where((num) => num > 3); // 过滤小于等于3的值 \\n\\n// 3. 消费者层:订阅数据流 \\nfinal subscription = processedStream.listen( \\n (data) => print(\'接收数据: $data\'), \\n onError: (err) => print(\'错误: $err\'), \\n onDone: () => print(\'流已关闭\'), \\n); \\n\\n// 4. 资源释放(可选) \\nFuture.delayed(Duration(seconds: 3), () => subscription.cancel()); \\n
\\n①、订阅驱动:
\\nStreamController
的数据在无监听者时不会被缓存
。②、背压控制:
\\npause
/resume
动态调节数据流速,防止消费者处理不过来导致内存堆积。③、错误传播:
\\nhandleError
捕获或导致程序崩溃。Future
的本质区别维度 | Stream | Future |
---|---|---|
数据量 | 处理多个异步事件 | 处理单个异步结果 |
生命周期 | 持续存在,直到主动关闭 | 一次性完成(成功或失败) |
监听机制 | 支持多次监听(广播流)或单次监听 | 仅单次完成,无法重复监听 |
典型场景 | 实时聊天、文件流式传输、用户事件监听 | 单次网络请求、数据库查询 |
掌握Stream
的秘诀在于构建数据管道思维:
listen
+transform
+combine
组合拳解决。优秀的Flutter
开发者不仅是界面工程师,更是数据流动的架构师。当你能像设计水利工程一样设计数据管道时,便是真正系统化掌握了Stream
的精髓。
\\n","description":"前言 Stream —— 快递分拣中心的智能传送带系统。\\n\\n在移动应用中,数据就像血液一样在应用中流动:用户滑动列表时不断加载的新条目、聊天室实时刷新的消息、文件下载时跳动的进度条等,这些场景背后都隐藏着一个关键问题:如何高效管理持续产生的异步数据?\\n\\n传统的Future虽然能处理单次异步操作,但面对连绵不断的数据流却显得力不从心。Stream正是为此而生 —— 它像一条智能传送带,让数据按需流动、精准分发。然而许多开发者仅停留在简单使用listen(),却未能真正理解其设计哲学。\\n\\n本文将带你穿透API表层,用系统化思维构建Stream知识体系…","guid":"https://juejin.cn/post/7486024544240795698","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-27T03:02:39.123Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/12de85b2a0664039936915184f0552c4~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1743649358&x-signature=KrzLhggIwr6HyJT3a9JFBDrv%2FXI%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"从组件源码和diff算法角度浅析Flutter组件的设计","url":"https://juejin.cn/post/7486030501904891943","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
在 Flutter 中,Container
继承自 StatelessWidget
,为什么要这么设计?下面从源码设计、常用组件的继承关系以及两者的选择标准展开说明:
Container
继承自 StatelessWidget
?Container
是一个 组合型组件,它通过组合其他 Widget(如 Padding
、DecoratedBox
、Align
等)实现布局和样式功能。color
、margin
、child
)均由父组件或外部传入,自身不维护任何可变状态,因此适合用 StatelessWidget
。StatelessWidget
在重建时比 StatefulWidget
更轻量(无需管理 State
生命周期)。Container
作为高频使用的布局组件,继承 StatelessWidget
能减少不必要的开销。查看 Container
的源码(简化版):
class Container extends StatelessWidget {\\n const Container({\\n Key? key,\\n this.alignment,\\n this.padding,\\n this.color,\\n this.child,\\n // ...其他参数\\n }) : super(key: key);\\n\\n final AlignmentGeometry? alignment;\\n final EdgeInsetsGeometry? padding;\\n final Color? color;\\n final Widget? child;\\n\\n @override\\n Widget build(BuildContext context) {\\n // 组合其他 Widget 实现功能\\n Widget current = child ?? const SizedBox();\\n if (alignment != null) {\\n current = Align(alignment: alignment!, child: current);\\n }\\n if (padding != null) {\\n current = Padding(padding: padding!, child: current);\\n }\\n // ...其他逻辑\\n return current;\\n }\\n}\\n
\\nContainer
仅依赖输入参数,无内部状态,完全符合 StatelessWidget
的设计初衷。StatelessWidget
的组件这些组件 无需管理内部状态,仅依赖外部参数:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n组件 | 用途 | 特点 |
---|---|---|
Text | 显示文本 | 纯展示,无交互或状态变化。 |
Icon | 显示图标 | 同上。 |
Padding | 添加内边距 | 仅调整子组件的布局。 |
Center | 居中子组件 | 依赖父组件的约束。 |
Row /Column | 线性布局 | 组合子组件,布局逻辑由参数控制。 |
ListView.builder | 动态列表 | 数据源由外部提供,自身无状态。 |
StatefulWidget
的组件这些组件 需要维护内部状态 或 处理用户交互:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n组件 | 用途 | 状态需求 |
---|---|---|
TextField | 文本输入框 | 需要管理光标位置、输入内容、焦点状态等。 |
Checkbox /Switch | 复选框/开关 | 需要维护选中状态。 |
PageView | 页面滑动视图 | 跟踪当前页面索引和滑动状态。 |
AnimationController | 动画控制 | 管理动画进度、播放状态等。 |
FutureBuilder | 异步数据加载 | 需要跟踪 Future 的执行状态。 |
Form | 表单管理 | 维护表单字段的验证状态和提交逻辑。 |
StatelessWidget
vs StatefulWidget
的选择标准StatelessWidget
当:color
、child
)。Container
、Padding
)。StatefulWidget
当:Flutter 的组件设计遵循以下原则:
\\nContainer
通过组合 Padding
、Align
等实现功能,而非继承它们。Text
只负责渲染文本)。StatefulWidget
管理,无状态部分抽离为 StatelessWidget
。Icon
class MyIcon extends StatelessWidget {\\n final IconData icon;\\n final Color color;\\n\\n const MyIcon({required this.icon, this.color = Colors.black});\\n\\n @override\\n Widget build(BuildContext context) {\\n return Icon(icon, color: color);\\n }\\n}\\n
\\nCounterButton
class CounterButton extends StatefulWidget {\\n @override\\n _CounterButtonState createState() => _CounterButtonState();\\n}\\n\\nclass _CounterButtonState extends State<CounterButton> {\\n int count = 0;\\n\\n @override\\n Widget build(BuildContext context) {\\n return ElevatedButton(\\n onPressed: () => setState(() => count++),\\n child: Text(\'Count: $count\'),\\n );\\n }\\n}\\n
\\n组件选择依据:是否需要管理可变状态或响应交互。
\\n从 Flutter 的底层 Diff 算法(Widget Tree Reconciliation) 和 Element 树更新机制 的角度来看,将 Widget 分为 StatelessWidget
和 StatefulWidget
是为了 优化性能 和 明确状态管理职责。以下是深度分析:
当 Widget 树变化时,Flutter 通过 Diff 算法 比较新旧 Widget 树,更新对应的 Element 和 RenderObject 树。
\\n无状态:完全依赖父组件传递的配置(final
属性)。
更新行为:
\\ncolor
、child
)是否相同。性能优势:
\\nState
对象,轻量级重建。Text
、Padding
)。有状态:通过关联的 State
对象维护可变数据。
更新行为:
\\nState
对象,并触发 State.didUpdateWidget
方法。State
和 Element,创建新实例。State
的生命周期独立于 Widget 重建,确保状态持久化。设计意义:
\\nTextField
、AnimationController
)。StatelessElement
)// 简化的 StatelessElement 更新逻辑\\nclass StatelessElement extends ComponentElement {\\n @override\\n void update(StatelessWidget newWidget) {\\n super.update(newWidget);\\n rebuild(); // 直接重建,无状态保留\\n }\\n}\\n
\\nStatefulElement
)// 简化的 StatefulElement 更新逻辑\\nclass StatefulElement extends ComponentElement {\\n State _state;\\n\\n @override\\n void update(StatefulWidget newWidget) {\\n super.update(newWidget);\\n if (widget.runtimeType == newWidget.runtimeType && widget.key == newWidget.key) {\\n _state.didUpdateWidget(oldWidget); // 复用 State,仅更新配置\\n } else {\\n _state.dispose();\\n _state = newWidget.createState(); // 创建新 State\\n }\\n rebuild();\\n }\\n}\\n
\\nState
避免重复初始化(如 TextField
的输入内容不会因父组件重建而丢失)。State
对象与 Widget 解耦,确保状态在 Widget 重建时不被意外重置。// 父组件重建时,MyStatefulWidget 的 State 会被复用\\nParentWidget(\\n child: MyStatefulWidget(key: UniqueKey()), // Key 不变则 State 复用\\n)\\n
\\nKey
的稳定性,两类 Widget 的分工使 Diff 过程更高效:\\nKey
和 runtimeType
匹配 State
。ListView
)ListView(\\n children: [\\n ItemWidget(key: Key(\'A\')), // StatefulWidget\\n ItemWidget(key: Key(\'B\'))),\\n ],\\n)\\n
\\nKey
正确关联 State
,仅更新位置。// StatelessWidget 重建时立即应用新主题\\nTheme(\\n data: newTheme,\\n child: MyStatelessWidget(), // 无状态,直接更新\\n);\\n\\n// StatefulWidget 需通过 didUpdateWidget 手动响应\\nclass MyStatefulWidget extends StatefulWidget {\\n final ThemeData theme;\\n @override\\n _MyStatefulWidgetState createState() => _MyStatefulWidgetState();\\n}\\n\\nclass _MyStatefulWidgetState extends State<MyStatefulWidget> {\\n @override\\n void didUpdateWidget(MyStatefulWidget oldWidget) {\\n if (widget.theme != oldWidget.theme) {\\n setState(() {}); // 手动触发更新\\n }\\n }\\n}\\n
\\n维度 | StatelessWidget | StatefulWidget |
---|---|---|
Diff 逻辑 | 直接比较属性,无状态复用 | 通过 Key 和类型匹配复用 State |
性能 | 更轻量,适合高频重建 | 需维护 State,但有状态持久化优势 |
适用场景 | 静态展示、布局组合 | 交互、动画、表单等需保持状态的场景 |
底层开销 | 仅 Widget 和 Element 重建 | 需维护 State 生命周期 |
这种设计使 Flutter 在 动态更新 和 状态管理 之间取得平衡,既保证了灵活性,又避免了不必要的性能损耗。
","description":"在 Flutter 中,Container 继承自 StatelessWidget,为什么要这么设计?下面从源码设计、常用组件的继承关系以及两者的选择标准展开说明: 一、为什么 Container 继承自 StatelessWidget?\\n1.1 无状态特性\\nContainer 是一个 组合型组件,它通过组合其他 Widget(如 Padding、DecoratedBox、Align 等)实现布局和样式功能。\\n它的所有属性(如 color、margin、child)均由父组件或外部传入,自身不维护任何可变状态,因此适合用 StatelessWidge…","guid":"https://juejin.cn/post/7486030501904891943","author":"charming","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-27T01:55:03.335Z","media":null,"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Dart微任务与事件队列","url":"https://juejin.cn/post/7485725589610758159","content":"Dart(Flutter 使用的语言)和 JavaScript 类似,也有微任务(Microtask)和事件循环(Event Loop)的概念,但它的任务调度模型与 JS 有一些关键区别。以下是详细对比和说明:
\\nDart 的异步操作基于 事件循环(Event Loop),分为两个主要队列:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n队列类型 | 描述 | 类比 JavaScript |
---|---|---|
微任务队列(Microtask Queue) | 优先级最高,在当前事件循环周期末尾立即执行,通常用于内部异步操作(如 Future 的状态变更)。 | 类似 JS 的 Promise.then 、MutationObserver 。 |
事件队列(Event Queue) | 处理 I/O、计时器(Timer )、手势、绘图等外部事件,按 FIFO 顺序执行。 | 类似 JS 的 setTimeout 、DOM 事件 。 |
Dart 的事件循环按以下顺序处理任务:
\\nFuture
完成、Timer
触发等)。void main() {\\n // 1. 同步代码\\n print(\'Start\');\\n\\n // 2. 微任务(优先级高)\\n scheduleMicrotask(() => print(\'Microtask 1\'));\\n\\n // 3. 事件队列(优先级低)\\n Future(() => print(\'Event 1\'));\\n Future(() => print(\'Event 2\'));\\n\\n // 4. 同步代码\\n print(\'End\');\\n}\\n
\\n输出顺序:
\\nStart\\nEnd\\nMicrotask 1\\nEvent 1\\nEvent 2\\n
\\n特性 | Dart | JavaScript |
---|---|---|
微任务触发时机 | 在事件队列的每个事件之间清空微任务队列(而 JS 仅在当前宏任务结束时清空)。 | 每个宏任务(如 setTimeout )结束后执行。 |
“宏任务”概念 | Dart 没有明确的“宏任务”术语,但事件队列的行为类似 JS 的宏任务。 | 明确分为宏任务(如 setTimeout )和微任务。 |
I/O 操作 | 通过 Future 和 async/await 处理,属于事件队列。 | 类似(如 fetch 回调)。 |
scheduleMicrotask()
Future
的状态变更(如 Future.then
、async/await
的后续代码)。Future
的构造函数(如 Future(() => ...)
)。Timer
(Timer.run
、Future.delayed
)。void main() async {\\n print(\'Start\');\\n\\n // 事件队列\\n Future(() => print(\'Event 1\'));\\n\\n // 微任务\\n scheduleMicrotask(() => print(\'Microtask 1\'));\\n\\n // async/await 是微任务\\n await Future(() => print(\'Event 2\'));\\n print(\'After await\'); // 微任务\\n\\n // 嵌套微任务\\n scheduleMicrotask(() => print(\'Microtask 2\'));\\n\\n print(\'End\');\\n}\\n
\\n输出顺序:
\\nStart\\nEvent 1\\nMicrotask 1\\nEvent 2\\nAfter await\\nMicrotask 2\\nEnd\\n
\\nsetState
后数据未更新的问题(例如,确保数据在微任务中更新后再渲染)。Future
或 async/await
的执行顺序不符合预期时,可以检查微任务和事件队列。Dart 的异步模型与 JavaScript 类似,但微任务的处理时机更频繁(在每个事件之间)。在 Flutter 开发中:
\\nscheduleMicrotask
和 Future
显式控制任务优先级。如果需要深入底层,可以参考 Dart 的 事件循环源码。
","description":"Dart(Flutter 使用的语言)和 JavaScript 类似,也有微任务(Microtask)和事件循环(Event Loop)的概念,但它的任务调度模型与 JS 有一些关键区别。以下是详细对比和说明: 1. Dart 的异步任务模型\\n\\nDart 的异步操作基于 事件循环(Event Loop),分为两个主要队列:\\n\\n队列类型\\t描述\\t类比 JavaScript微任务队列(Microtask Queue)\\t优先级最高,在当前事件循环周期末尾立即执行,通常用于内部异步操作(如 Future 的状态变更)。\\t类似 JS 的…","guid":"https://juejin.cn/post/7485725589610758159","author":"charming","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-26T07:20:46.873Z","media":null,"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"掌握 Flutter 中的 BLoC:深入探索 Cubit 模式","url":"https://juejin.cn/post/7485930231832903689","content":"在 Flutter 开发中,状态管理是构建响应式和可维护应用的关键。BLoC(Business Logic Component)模式是一种广受欢迎的状态管理解决方案,而其中的 Cubit 变体因其简洁性和易用性,成为许多开发者的首选。本文将通过一个实际案例——获取并展示 GitHub 事件列表,带您深入了解如何在 Flutter 中使用 Cubit 模式。我们将从基础设置开始,逐步讲解数据获取、状态管理以及 UI 集成。
\\n在进入代码之前,先简单了解一下 BLoC 和 Cubit 的概念:
\\n本文将聚焦 Cubit,展示其如何在实际项目中发挥作用。
\\n我们将构建一个简单的 Flutter 应用,用于展示 GitHub 的事件列表。为此,我们需要以下依赖:
\\nflutter_bloc
:提供 BLoC 和 Cubit 的核心功能。http
:用于发起 HTTP 请求以获取 GitHub API 数据。equatable
:简化状态对象的比较。在 pubspec.yaml
中添加以下依赖:
dependencies:\\n flutter:\\n sdk: flutter\\n flutter_bloc: ^9.1.0\\n http: ^1.3.0\\n equatable: ^2.0.7\\n
\\n运行 flutter pub get
以安装这些包。
首先,我们需要一个模型类来表示 GitHub 事件的数据。假设每个事件包含 id
、username
、avatarUrl
和 repoUrl
等字段。以下是 GithubEventModel
的实现:
import \'package:equatable/equatable.dart\';\\n\\nclass GithubEventModel extends Equatable {\\n final String id;\\n final String username;\\n final String avatarUrl;\\n final String repoUrl;\\n\\n const GithubEventModel({\\n required this.id,\\n required this.username,\\n required this.avatarUrl,\\n required this.repoUrl,\\n });\\n\\n factory GithubEventModel.fromJson(Map<String, dynamic> json) {\\n return GithubEventModel(\\n id: json[\'id\'].toString(),\\n username: json[\'actor\'][\'login\'],\\n avatarUrl: json[\'actor\'][\'avatar_url\'],\\n repoUrl: json[\'repo\'][\'url\'],\\n );\\n }\\n\\n @override\\n List<Object> get props => [id, username, avatarUrl, repoUrl];\\n}\\n
\\n使用 Equatable
可以简化对象比较,确保状态更新时能正确触发 UI 重建。
接下来,我们需要一个数据仓库类来处理与 GitHub API 的交互。GithubEventRepository
类负责从 GitHub API 获取事件数据,并支持分页:
import \'dart:convert\';\\nimport \'package:http/http.dart\' as http;\\nimport \'package:flutter_bloc_test/cubit_app/core/model/github_event_model.dart\';\\n\\nclass GithubEventRepository {\\n Future<List<GithubEventModel>> fetchGithubEvents({int page = 1}) async {\\n final response = await http.get(Uri.parse(\\"https://api.github.com/events?page=$page\\"));\\n if (response.statusCode != 200) {\\n throw Exception(\\"Failed to fetch Github events\\");\\n }\\n final List<dynamic> jsonData = json.decode(response.body);\\n return jsonData.map((e) => GithubEventModel.fromJson(e)).toList();\\n }\\n}\\n
\\n这个类通过 http
包发起 GET 请求,获取指定页码的事件数据,并将其转换为 GithubEventModel
对象的列表。
在 Cubit 模式中,状态(State)代表 UI 的不同情况。我们定义了以下四种状态:
\\n以下是状态的实现:
\\nimport \'package:equatable/equatable.dart\';\\nimport \'package:flutter_bloc_test/cubit_app/core/model/github_event_model.dart\';\\n\\nsealed class GithubEventState extends Equatable {\\n final List<GithubEventModel> githubEvents;\\n const GithubEventState(this.githubEvents);\\n\\n @override\\n List<Object> get props => [githubEvents];\\n}\\n\\nfinal class GithubEventInitial extends GithubEventState {\\n const GithubEventInitial() : super(const []);\\n}\\n\\nfinal class GithubEventLoading extends GithubEventState {\\n const GithubEventLoading() : super(const []);\\n}\\n\\nfinal class GithubEventLoaded extends GithubEventState {\\n const GithubEventLoaded(super.githubEvents);\\n}\\n\\nfinal class GithubEventError extends GithubEventState {\\n final String message;\\n const GithubEventError(this.message, super.githubEvents);\\n\\n @override\\n List<Object> get props => [message, ...githubEvents];\\n}\\n
\\nsealed class
定义状态的基类,确保类型安全。githubEvents
字段存储当前的事件列表。GithubEventError
额外包含错误信息 message
,并保留当前的事件列表,以便在错误时仍能显示已有数据。GithubEventCubit
是状态管理的核心,负责处理业务逻辑并发出状态变化:
import \'package:bloc/bloc.dart\';\\nimport \'package:flutter_bloc_test/cubit_app/core/model/github_event_model.dart\';\\nimport \'package:flutter_bloc_test/cubit_app/core/repository/github_event_repository.dart\';\\nimport \'github_event_state.dart\';\\n\\nclass GithubEventCubit extends Cubit<GithubEventState> {\\n final GithubEventRepository _githubEventRepository = GithubEventRepository();\\n int _page = 1;\\n bool _isLoadMore = false;\\n\\n GithubEventCubit() : super(const GithubEventInitial());\\n\\n Future<void> fetchGithubEvents() async {\\n try {\\n emit(const GithubEventLoading());\\n _page = 1;\\n final events = await _githubEventRepository.fetchGithubEvents(page: _page);\\n emit(GithubEventLoaded(events));\\n } on Exception catch (e) {\\n emit(GithubEventError(e.toString(), state.githubEvents));\\n }\\n }\\n\\n Future<void> loadMoreGithubEvents() async {\\n if (_isLoadMore) return;\\n _isLoadMore = true;\\n try {\\n _page++;\\n final events = await _githubEventRepository.fetchGithubEvents(page: _page);\\n emit(GithubEventLoaded([...state.githubEvents, ...events]));\\n } on Exception catch (e) {\\n emit(GithubEventError(e.toString(), state.githubEvents));\\n } finally {\\n _isLoadMore = false;\\n }\\n }\\n\\n void removeGithubEventItem(GithubEventModel githubEventModel) {\\n final List<GithubEventModel> githubEvents = List.from(state.githubEvents);\\n githubEvents.remove(githubEventModel);\\n emit(GithubEventLoaded(githubEvents));\\n }\\n}\\n
\\nfetchGithubEvents
:获取初始事件列表。重置页码为 1,发出加载状态,获取数据后发出 GithubEventLoaded
,若出错则发出 GithubEventError
。loadMoreGithubEvents
:加载更多事件。使用 _isLoadMore
防止重复请求,增加页码并追加新数据到现有列表中。removeGithubEventItem
:从列表中移除指定事件,并发出更新后的状态。我们将通过两个主要 widget 将 Cubit 集成到 UI 中:GithubEventPage
和 GithubEventView
。
GithubEventPage
负责提供 Cubit 并初始化数据获取:
import \'package:flutter/material.dart\';\\nimport \'package:flutter_bloc/flutter_bloc.dart\';\\nimport \'package:flutter_bloc_test/cubit_app/github_event/github_event.dart\';\\nimport \'github_event_view.dart\';\\n\\nclass GithubEventPage extends StatelessWidget {\\n const GithubEventPage({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n return BlocProvider(\\n create: (_) => GithubEventCubit()..fetchGithubEvents(),\\n child: const GithubEventView(),\\n );\\n }\\n}\\n
\\nBlocProvider
创建并提供 GithubEventCubit
。fetchGithubEvents
加载数据。GithubEventView
是一个有状态 widget,负责监听状态变化并渲染 UI:
import \'package:flutter/cupertino.dart\';\\nimport \'package:flutter/material.dart\';\\nimport \'package:flutter/services.dart\';\\nimport \'package:flutter_bloc/flutter_bloc.dart\';\\nimport \'package:flutter_bloc_test/cubit_app/core/model/github_event_model.dart\';\\nimport \'package:flutter_bloc_test/cubit_app/github_event/github_event.dart\';\\n\\nclass GithubEventView extends StatefulWidget {\\n const GithubEventView({super.key});\\n\\n @override\\n State<GithubEventView> createState() => _GithubEventViewState();\\n}\\n\\nclass _GithubEventViewState extends State<GithubEventView> {\\n late ScrollController _scrollController;\\n bool _isShowTopBtn = false;\\n\\n @override\\n void initState() {\\n super.initState();\\n _scrollController = ScrollController();\\n _scrollController.addListener(_scroll);\\n }\\n\\n @override\\n void dispose() {\\n _scrollController.dispose();\\n super.dispose();\\n }\\n\\n void _scroll() {\\n if (_scrollController.position.pixels > MediaQuery.of(context).size.height) {\\n setState(() { _isShowTopBtn = true; });\\n } else {\\n setState(() { _isShowTopBtn = false; });\\n }\\n if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 100) {\\n context.read<GithubEventCubit>().loadMoreGithubEvents();\\n }\\n }\\n\\n void _scrollToTop() {\\n _scrollController.animateTo(0,\\n duration: const Duration(milliseconds: 300), curve: Curves.easeInOut);\\n }\\n\\n Future<void> _onCopy(GithubEventModel event) async {\\n await Clipboard.setData(\\n ClipboardData(\\n text: \\"name: ${event.username}\\\\navatar: ${event.avatarUrl}\\\\nrepo: ${event.repoUrl}\\",\\n ),\\n );\\n if (await Clipboard.hasStrings()) {\\n ScaffoldMessenger.of(context).showSnackBar(\\n SnackBar(content: const Text(\\"Copied Successfully\\"), duration: const Duration(seconds: 1)),\\n );\\n }\\n }\\n\\n void _onShare(GithubEventModel event) {\\n // Share.share(event.repoUrl);\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: const Text(\\"Github Event\\")),\\n body: BlocBuilder<GithubEventCubit, GithubEventState>(\\n builder: (context, state) {\\n if (state is GithubEventLoading) {\\n return const Center(child: CircularProgressIndicator());\\n }\\n if (state is GithubEventError) {\\n return Center(child: Text(\\"Error: ${state.message}\\"));\\n }\\n if (state is GithubEventLoaded) {\\n return RefreshIndicator(\\n onRefresh: () => context.read<GithubEventCubit>().fetchGithubEvents(),\\n child: ListView.builder(\\n controller: _scrollController,\\n itemCount: state.githubEvents.length,\\n itemBuilder: (context, index) {\\n final event = state.githubEvents[index];\\n return Dismissible(\\n key: ValueKey(event.id),\\n direction: DismissDirection.endToStart,\\n background: Container(\\n color: Colors.red,\\n alignment: Alignment.centerRight,\\n padding: const EdgeInsets.only(right: 25.0),\\n child: const Icon(CupertinoIcons.trash, color: Colors.white),\\n ),\\n onDismissed: (_) {\\n context.read<GithubEventCubit>().removeGithubEventItem(event);\\n },\\n child: CupertinoContextMenu(\\n actions: [\\n CupertinoContextMenuAction(\\n child: const Text(\\"Copy\\"),\\n onPressed: () {\\n _onCopy(event);\\n Navigator.pop(context);\\n },\\n ),\\n CupertinoContextMenuAction(\\n child: const Text(\\"Share\\"),\\n onPressed: () => _onShare(event),\\n ),\\n ],\\n child: ListTile(\\n title: Text(event.username),\\n subtitle: Text(event.repoUrl),\\n leading: Image.network(event.avatarUrl),\\n ),\\n ),\\n );\\n },\\n ),\\n );\\n }\\n return const Center(child: Text(\\"No events\\"));\\n },\\n ),\\n floatingActionButton: _isShowTopBtn\\n ? FloatingActionButton(\\n onPressed: _scrollToTop,\\n child: const Icon(Icons.arrow_circle_up),\\n )\\n : null,\\n );\\n }\\n}\\n
\\nScrollController
监听滚动,当接近底部时调用 loadMoreGithubEvents
。RefreshIndicator
支持下拉刷新。Dismissible
允许用户滑动删除事件。CupertinoContextMenu
提供复制和分享选项。_isLoadMore
防止重复请求,确保性能。通过这个示例,我们展示了如何在 Flutter 中使用 Cubit 模式管理 GitHub 事件列表的状态。从数据获取到状态定义,再到 UI 集成,Cubit 提供了一种简单而强大的方式来组织代码。对于需要更复杂事件处理的场景,可以考虑完整的 BLoC 模式。但对于大多数简单应用,Cubit 已足够胜任。
\\n希望这篇博客能帮助您更好地理解和应用 Cubit 模式,在 Flutter 开发中愉快coding!
","description":"在 Flutter 开发中,状态管理是构建响应式和可维护应用的关键。BLoC(Business Logic Component)模式是一种广受欢迎的状态管理解决方案,而其中的 Cubit 变体因其简洁性和易用性,成为许多开发者的首选。本文将通过一个实际案例——获取并展示 GitHub 事件列表,带您深入了解如何在 Flutter 中使用 Cubit 模式。我们将从基础设置开始,逐步讲解数据获取、状态管理以及 UI 集成。 什么是 BLoC 和 Cubit?\\n\\n在进入代码之前,先简单了解一下 BLoC 和 Cubit 的概念:\\n\\nBLoC(业务逻辑组件):一…","guid":"https://juejin.cn/post/7485930231832903689","author":"MaoJiu","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-26T07:14:31.383Z","media":null,"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"一文精通-Flutter 与原生(Android/iOS)通信","url":"https://juejin.cn/post/7485930231832870921","content":"Flutter 与原生(Android/iOS)通信主要依靠 Platform Channel 机制,以下是三种基本通信方式及其使用场景,附代码示例:
\\n用途:双向通信,用于 Flutter 与原生之间调用对方的方法(如调用原生 API 或传递计算结果)。
\\n场景:获取设备信息、调用相机/传感器、支付功能等需要主动触发原生能力的场景。
// Flutter 端(Dart)\\nimport \'package:flutter/services.dart\';\\n\\nfinal methodChannel = MethodChannel(\'com.example/app\');\\n\\n// 调用原生方法获取电池电量\\nFuture<int> getBatteryLevel() async {\\n try {\\n final result = await methodChannel.invokeMethod(\'getBatteryLevel\');\\n return result as int;\\n } on PlatformException catch (e) {\\n print(\\"Error: ${e.message}\\");\\n return -1;\\n }\\n}\\n
\\n// Android 端(Kotlin)\\nclass MainActivity : FlutterActivity() {\\n override fun configureFlutterEngine(flutterEngine: FlutterEngine) {\\n super.configureFlutterEngine(flutterEngine)\\n MethodChannel(flutterEngine.dartExecutor.binaryMessenger, \\"com.example/app\\").setMethodCallHandler { call, result ->\\n if (call.method == \\"getBatteryLevel\\") {\\n val batteryLevel = getBatteryLevel()\\n result.success(batteryLevel)\\n } else {\\n result.notImplemented()\\n }\\n }\\n }\\n\\n private fun getBatteryLevel(): Int {\\n // 实现获取电量逻辑\\n }\\n}\\n
\\n// iOS 端(Swift)\\n@UIApplicationMain\\n@objc class AppDelegate: FlutterAppDelegate {\\n override func application(\\n _ application: UIApplication,\\n didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?\\n ) -> Bool {\\n let controller = window?.rootViewController as! FlutterViewController\\n let channel = FlutterMethodChannel(name: \\"com.example/app\\", binaryMessenger: controller.binaryMessenger)\\n channel.setMethodCallHandler { call, result in\\n if call.method == \\"getBatteryLevel\\" {\\n self.getBatteryLevel(result: result)\\n } else {\\n result(FlutterMethodNotImplemented)\\n }\\n }\\n return super.application(application, didFinishLaunchingWithOptions: launchOptions)\\n }\\n\\n private func getBatteryLevel(result: FlutterResult) {\\n // 实现获取电量逻辑\\n }\\n}\\n
\\n用途:单向数据流,用于原生向 Flutter 持续发送事件(如传感器数据、网络状态变化)。
\\n场景:实时监听传感器数据、网络连接状态、推送通知等流式数据场景。
// Flutter 端(Dart)\\nfinal eventChannel = EventChannel(\'com.example/events\');\\n\\nStream<int> listenToTimer() {\\n return eventChannel.receiveBroadcastStream().map((event) => event as int);\\n}\\n\\n// 使用:StreamBuilder 监听数据\\n
\\n// Android 端(Kotlin)\\nclass MainActivity : FlutterActivity() {\\n override fun configureFlutterEngine(flutterEngine: FlutterEngine) {\\n super.configureFlutterEngine(flutterEngine)\\n EventChannel(flutterEngine.dartExecutor.binaryMessenger, \\"com.example/events\\").setStreamHandler(\\n object : StreamHandler {\\n var eventSink: EventChannel.EventSink? = null\\n var timer: Timer? = null\\n\\n override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {\\n eventSink = events\\n timer = Timer().scheduleAtFixedRate(1000, 1000) {\\n eventSink?.success(System.currentTimeMillis())\\n }\\n }\\n\\n override fun onCancel(arguments: Any?) {\\n timer?.cancel()\\n eventSink = null\\n }\\n }\\n )\\n }\\n}\\n
\\n// iOS 端(Swift)\\nclass EventHandler: NSObject, FlutterStreamHandler {\\n var eventSink: FlutterEventSink?\\n var timer: Timer?\\n\\n func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {\\n self.eventSink = events\\n timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in\\n events(Int(Date().timeIntervalSince1970))\\n }\\n return nil\\n }\\n\\n func onCancel(withArguments arguments: Any?) -> FlutterError? {\\n timer?.invalidate()\\n eventSink = nil\\n return nil\\n }\\n}\\n\\n// AppDelegate 中注册\\nlet eventChannel = FlutterEventChannel(name: \\"com.example/events\\", binaryMessenger: controller.binaryMessenger)\\neventChannel.setStreamHandler(EventHandler())\\n
\\n用途:简单数据传递,支持双向通信,适合传递基本数据类型(String/Map)。
\\n场景:轻量级数据交换(如用户配置同步、简单文本传输)。
// Flutter 端(Dart)\\nfinal messageChannel = BasicMessageChannel<String>(\'com.example/messages\', StringCodec());\\n\\n// 发送消息\\nmessageChannel.send(\'Hello from Flutter\').then((reply) {\\n print(\'Received reply: $reply\');\\n});\\n\\n// 设置消息处理器\\nmessageChannel.setMessageHandler((message) async {\\n return \'Received: $message\';\\n});\\n
\\n// Android 端(Kotlin)\\nBasicMessageChannel<String>(flutterEngine.dartExecutor.binaryMessenger, \\"com.example/messages\\", StringCodec.INSTANCE).apply {\\n setMessageHandler { message, reply ->\\n reply.reply(\\"Android received: $message\\")\\n }\\n}\\n
\\n// iOS 端(Swift)\\nlet messageChannel = FlutterBasicMessageChannel(\\n name: \\"com.example/messages\\",\\n binaryMessenger: controller.binaryMessenger,\\n codec: FlutterStringCodec.sharedInstance()\\n)\\nmessageChannel.setMessageHandler { message, reply in\\n reply(\\"iOS received: (message ?? \\"\\")\\")\\n}\\n
\\n通道类型 | 通信方向 | 数据流特点 | 典型场景 |
---|---|---|---|
MethodChannel | 双向 | 单次方法调用与响应 | 调用原生 API、获取设备信息 |
EventChannel | 原生 → Flutter | 持续事件流 | 传感器监听、实时数据更新 |
BasicMessage | 双向 | 简单数据传递 | 轻量级配置同步、文本交互 |
根据具体需求选择通道类型,确保数据高效传递并减少性能损耗。
","description":"Flutter 与原生(Android/iOS)通信主要依靠 Platform Channel 机制,以下是三种基本通信方式及其使用场景,附代码示例: 一、MethodChannel(方法通道)\\n\\n用途:双向通信,用于 Flutter 与原生之间调用对方的方法(如调用原生 API 或传递计算结果)。\\n 场景:获取设备信息、调用相机/传感器、支付功能等需要主动触发原生能力的场景。\\n\\n代码示例:\\n// Flutter 端(Dart)\\nimport \'package:flutter/services.dart\';\\n\\nfinal methodChannel…","guid":"https://juejin.cn/post/7485930231832870921","author":"无知的前端","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-26T07:09:52.529Z","media":null,"categories":["iOS","面试","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 树手术(Tree Surgery)机制与 GlobalKey 的深度解析","url":"https://juejin.cn/post/7485750260100120626","content":"\\n\\n\\n
Flutter 的「树手术」指通过 非局部树变动(Non-local Tree Mutation) 将元素子树移动到 UI 树的其他位置,同时保留元素状态和渲染信息。这种机制的核心依赖 GlobalKey
,是 Flutter 实现高性能动态 UI 的底层黑魔法。
作用 | 实现原理 | 典型应用场景 |
---|---|---|
跨组件状态保留 | 通过全局唯一 Key 定位元素,移动时保留 State 对象 | 页面跳转时的表单状态保留 |
渲染信息复用 | 保留 RenderObject 的布局计算结果,避免重复测量/布局 | Hero 动画的平滑过渡 |
跨层级元素操控 | 突破组件层级限制,直接操作任意位置的 Widget | 全局悬浮按钮控制 |
动态子树重组 | 将子树从 UI 树中「剪切」后「粘贴」到新位置,无需重建 | 动态仪表板模块拖拽排序 |
dartCopy Code\\n// 页面 A\\nHero(\\n tag: \'image\',\\n child: Image.asset(\'A.jpg\'),\\n // 隐含创建 GlobalKey\\n)\\n\\n// 页面 B\\nHero(\\n tag: \'image\',\\n child: Image.asset(\'B.jpg\'), // 占位 Widget\\n)\\n
\\ntag
关联的 GlobalKey
。Hero
元素及其关联的 RenderObject
。dartCopy Code\\n// 伪代码:框架内部操作\\nvoid _performHeroTransition() {\\n final sourceElement = _globalKey.currentElement;\\n sourceElement.detach(); // 从旧位置解除\\n}\\n
\\ndartCopy Code\\nvoid _completeTransition() {\\n targetHeroSlot.adoptElement(sourceElement); // 挂载到新位置\\n targetHeroParent.markNeedsLayout(); // 标记需要重新布局\\n}\\n
\\nRenderObject
直接复用上次布局结果。保留项 | 常规重建 | GlobalKey 复用 |
---|---|---|
Widget 实例 | ❌ 新建 | ✅ 保留 |
Element 节点 | ❌ 新建 | ✅ 保留 |
State 对象 | ❌ 销毁 | ✅ 保留 |
RenderObject | ❌ 新建 | ✅ 保留 |
布局计算结果 | ❌ 重算 | ✅ 复用 |
dartCopy Code\\n// 旧位置布局约束\\nBoxConstraints(\\n minWidth: 100,\\n maxWidth: 300,\\n minHeight: 200,\\n maxHeight: 500\\n)\\n\\n// 新位置约束相同 → 直接复用布局\\nif (newConstraints == oldConstraints) {\\n return; // 跳过布局计算\\n}\\n
\\ndartCopy Code\\n// 错误用法:多个 Hero 使用相同 tag 但不同页面\\nHero(tag: \'shared\', ...) // 页面 A\\nHero(tag: \'shared\', ...) // 页面 B\\n\\n// 现象:动画闪烁或崩溃\\n
\\n解决方案:
\\ndartCopy Code\\n// 为每个逻辑资源创建唯一标识\\nHero(tag: \'user_${id}_avatar\', ...)\\n
\\ndartCopy Code\\nclass _DraggablePanelState extends State<DraggablePanel> {\\n final GlobalKey _key = GlobalKey();\\n\\n void _reset() {\\n // 错误:直接操作子组件状态\\n (_key.currentState as ChildState).reset();\\n }\\n}\\n
\\n解决方案:
\\ndartCopy Code\\n// 通过接口限制访问\\nmixin PanelState {\\n void reset();\\n}\\n\\nclass ChildWidget extends StatefulWidget {\\n const ChildWidget({super.key});\\n\\n @override\\n State<ChildWidget> createState() => _ChildWidgetState();\\n}\\n\\nclass _ChildWidgetState extends State<ChildWidget> with PanelState {\\n @override\\n void reset() { /* 安全操作 */ }\\n}\\n
\\ndartCopy Code\\nColumn(\\n children: [\\n if (showHeader) _header,\\n _body, // 使用 GlobalKey 的子树\\n ],\\n)\\n
\\n现象:当 showHeader
变化时,_body
的父节点约束可能改变,导致布局重算。
\\n优化方案:
dartCopy Code\\n// 使用 RepaintBoundary 隔离\\nRepaintBoundary(\\n child: _body,\\n)\\n
\\ndartCopy Code\\nclass DraggableDashboard extends StatefulWidget {\\n @override\\n _DraggableDashboardState createState() => _DraggableDashboardState();\\n}\\n\\nclass _DraggableDashboardState extends State<DraggableDashboard> {\\n final List<GlobalKey> _itemKeys = List.generate(5, (_) => GlobalKey());\\n int? _draggedIndex;\\n\\n void _onDragStart(int index) {\\n _draggedIndex = index;\\n // 创建悬浮层\\n Overlay.of(context).insert(_createDraggingOverlay());\\n }\\n\\n OverlayEntry _createDraggingOverlay() {\\n return OverlayEntry(\\n builder: (context) => Positioned(\\n child: _buildDraggingClone(),\\n ),\\n );\\n }\\n\\n Widget _buildDraggingClone() {\\n return SizedBox(\\n width: 200,\\n child: _DashboardItem(\\n key: _itemKeys[_draggedIndex!], // 复用原有 GlobalKey\\n index: _draggedIndex!,\\n ),\\n );\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return GridView.count(\\n crossAxisCount: 3,\\n children: List.generate(5, (index) => LongPressDraggable(\\n feedback: Container(), // 空反馈(实际显示悬浮层)\\n child: _DashboardItem(\\n key: _itemKeys[index],\\n index: index,\\n ),\\n onDragStarted: () => _onDragStart(index),\\n )),\\n );\\n }\\n}\\n
\\n关键机制:
\\nGlobalKey
捕获元素状态。Element
和 RenderObject
。布局边界可视化:
\\ndartCopy Code\\nvoid main() {\\n debugPaintLayerBordersEnabled = true;\\n runApp(MyApp());\\n}\\n
\\nRenderObject
布局边界元素树检查:
\\nbashCopy Code\\nflutter inspector → Select Widget Mode → 点击元素查看 GlobalKey 绑定\\n
\\n性能追踪:
\\ndartCopy Code\\nvoid _onDragUpdate(DragUpdateDetails details) {\\n Timeline.startSync(\'Custom Drag Event\');\\n // ... 拖拽逻辑\\n Timeline.finishSync();\\n}\\n
\\nFlutter 的树手术机制通过 GlobalKey
实现了:
开发者应在以下场景优先考虑此模式:
\\n掌握此机制,可在实现惊艳交互效果的同时,保持应用性能的极致优化。
","description":"参考 核心概念\\n\\nFlutter 的「树手术」指通过 非局部树变动(Non-local Tree Mutation) 将元素子树移动到 UI 树的其他位置,同时保留元素状态和渲染信息。这种机制的核心依赖 GlobalKey,是 Flutter 实现高性能动态 UI 的底层黑魔法。\\n\\nGlobalKey 的四大核心作用\\n作用\\t实现原理\\t典型应用场景跨组件状态保留\\t通过全局唯一 Key 定位元素,移动时保留 State 对象\\t页面跳转时的表单状态保留\\n渲染信息复用\\t保留 Re…","guid":"https://juejin.cn/post/7485750260100120626","author":"zonda的地盘","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-26T05:29:03.776Z","media":null,"categories":["前端","Flutter","性能优化"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 实现Android,IOS 微信登录","url":"https://juejin.cn/post/7485750260099924018","content":"\\n3. 配置IOS 端APP信息
\\n4.登录苹果开发者后台,在Identifiers 中打开 Associated Domains
\\"applinks\\": {\\n \\"details\\": [\\n {\\n \\"appID\\": \\"开发者账号的ID,苹果开发者后台获取\\",\\n \\"paths\\": [\\"/目录/*\\"\\"]\\n }\\n ]\\n }\\n}\\n
\\n此步骤的目录决定了第二步中域名后拼接的目录地址\\n6.将步骤五创建的文件放到xxxx.com服务器 对应的根目录或者.well-known目录下\\n7.Xcode info.plist中添加一下内容\\n图形化展示:
\\n\\n文本展示:\\n
fluwx: ^5.4.1\\n
\\n8. 代码实现工具类
\\n\\nimport \'dart:convert\';\\n\\nimport \'package:fluwx/fluwx.dart\';\\nimport \'package:get/get.dart\';\\nimport \'package:music_flutter/constants/get_constants.dart\';\\nimport \'package:music_flutter/http/request/request.dart\';\\nimport \'package:music_flutter/model/model.dart\';\\nimport \'package:music_flutter/modules/account/login/wechat_bind_phone_page.dart\';\\nimport \'package:music_flutter/utils/toast.dart\';\\n\\nimport \'../constants/constants.dart\';\\nimport \'../http/http.dart\';\\nimport \'../main.dart\';\\n\\nclass WechatUtil {\\n WechatUtil._();\\n\\n static Function(WeChatPaymentResponse)? onPayResultListener;\\n static final _fluwx = Fluwx()\\n ..addSubscriber((resp) async {\\n if (resp is WeChatAuthResponse) {\\n if (resp.isSuccessful) {\\n switch (resp.errCode) {\\n case 0:\\n try {\\n final code = resp.code;\\n //todo 调用获取access_token 接口\\n final appSecretResp = await HttpUtil.dio.get<String>(\\n \\"${HttpUtil.baseUrl}api/app/web-site/app-secret\\");\\n // 此方法可以让后端直接去调用\\n final accessTokenResponse = await HttpUtil.dio.get(\\n \\"https://api.weixin.qq.com/sns/oauth2/access_token\\",\\n queryParameters: <String, dynamic>{\\n \\"appid\\": wechatId,\\n \\"secret\\": appSecretResp.data,\\n \\"code\\": code,\\n \\"grant_type\\": \\"authorization_code\\",\\n });\\n\\n if (accessTokenResponse.data != null) {\\n var json = jsonDecode(accessTokenResponse.data);\\n final model = WechatAccessTokenModel.fromJson(json);\\n // 调用自己服务端的微信登录接口完成信息获取,存储\\n AccountRequest.wechatLogin(\\n accessToken: model.access_token,\\n openId: model.openid,\\n onSucceedDeal: (m) {\\n if (m.newUser) {\\n Get.to(\\n () => WechatBindPhonePage(openId: model.openid));\\n } else {\\n storage.write(phoneNumber, m.phone.toString());\\n storage.write(token, m.accessToken);\\n Get.clearRouteTree();\\n Get.offAll(() => const MainPage());\\n }\\n });\\n }\\n } on Exception catch (_, e) {\\n e.toString();\\n }\\n break;\\n\\n default:\\n break;\\n }\\n }\\n }\\n if (resp is WeChatPaymentResponse) {\\n onPayResultListener?.call(resp);\\n }\\n });\\n\\n// 选择适当时机初始化\\n static void registerApi() async {\\n _fluwx.registerApi(\\n appId: wechatId,\\n //todo 添加IOS universalLink\\n universalLink: \\"https://xxxx.com/目录/\\",\\n );\\n }\\n// 请求拉起登录\\n static void authBy() async {\\n final isInstall = await _fluwx.isWeChatInstalled;\\n if (isInstall) {\\n await _fluwx.authBy(\\n which: NormalAuth(\\n scope: \'snsapi_userinfo\',\\n state: \'wechat_sdk_demo_test\',\\n ),\\n );\\n } else {\\n toast(\\"微信未安装\\");\\n }\\n }\\n// 请求支付\\n static void pay(\\n PrePayIdModel m, {\\n Function(WeChatPaymentResponse)? onPayResult,\\n }) async {\\n onPayResultListener = onPayResult;\\n final isInstall = await _fluwx.isWeChatInstalled;\\n if (isInstall) {\\n await _fluwx.pay(\\n which: Payment(\\n appId: m.appId,\\n partnerId: m.partnerId,\\n prepayId: m.prepayId,\\n packageValue: m.packageValue,\\n nonceStr: m.nonceStr,\\n timestamp: int.tryParse(m.timeStamp) ?? -1,\\n sign: m.sign,\\n ),\\n );\\n } else {\\n toast(\\"微信未安装\\");\\n }\\n }\\n\\n static Future<bool> get wechatIsInstalled async =>\\n await _fluwx.isWeChatInstalled;\\n}\\n
","description":"登录微信开放平台open.weixin.qq.com/ 配置Android 端APP信息\\n\\n3. 配置IOS 端APP信息\\n\\n4.登录苹果开发者后台,在Identifiers 中打开 Associated Domains\\n\\n创建 apple-app-site-association文件,内容如下\\n \\"applinks\\": {\\n \\"details\\": [\\n {\\n \\"appID\\": \\"开发者账号的ID,苹果开发者后台获取\\",\\n \\"paths\\": [\\"…","guid":"https://juejin.cn/post/7485750260099924018","author":"增强","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-26T04:25:43.566Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2c3395c564fe41569531ac73d4676f11~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5aKe5by6:q75.awebp?rk3s=f64ab15b&x-expires=1743567942&x-signature=SGal2A4se7ygICrFUz4j2op7W7g%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/29702d55462948e29b5270cf3019dd6a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5aKe5by6:q75.awebp?rk3s=f64ab15b&x-expires=1743567942&x-signature=HtdGpPNPdiCedYeRFhgnUpSD7s8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/89378bcb882348c68acb348090aec814~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5aKe5by6:q75.awebp?rk3s=f64ab15b&x-expires=1743567942&x-signature=D0b6Ff4NkOGu7bPAOOFQ4NKkPuQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fef22edbc95b4728bfdf359a10e17132~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5aKe5by6:q75.awebp?rk3s=f64ab15b&x-expires=1743567942&x-signature=nOTqagjGos82Za1fNNgO6a0jsVY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c9c1f6c00a284b259dc64416098b74c4~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5aKe5by6:q75.awebp?rk3s=f64ab15b&x-expires=1743567942&x-signature=MeYejlqemdB2gJv8tYzgL8UlDQs%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","iOS","微信"],"attachments":null,"extra":null,"language":null},{"title":"Reprojection 模式","url":"https://juejin.cn/post/7485684772948049957","content":"假设需要实现一个 可折叠的配置面板,其子 Widget AdvancedCard
需要以下复杂配置:
dartCopy Code\\nclass ConfigPanel extends StatefulWidget {\\n @override\\n _ConfigPanelState createState() => _ConfigPanelState();\\n}\\n\\nclass _ConfigPanelState extends State<ConfigPanel> {\\n late final Color _randomColor; // 初始化时随机生成\\n late final EdgeInsets _precomputedPadding; // 初始化时预计算\\n\\n @override\\n void initState() {\\n super.initState();\\n // 复杂初始化逻辑(模拟耗时操作)\\n _randomColor = Colors.primaries[Random().nextInt(Colors.primaries.length)];\\n final screenWidth = MediaQuery.of(context).size.width;\\n _precomputedPadding = EdgeInsets.symmetric(horizontal: screenWidth * 0.1);\\n }\\n\\n bool _isExpanded = false;\\n\\n void _toggleExpansion() {\\n setState(() => _isExpanded = !_isExpanded);\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Column(\\n children: [\\n SwitchListTile(\\n title: Text(\\"展开配置面板\\"),\\n value: _isExpanded,\\n onChanged: (_) => _toggleExpansion(),\\n ),\\n if (_isExpanded)\\n AdvancedCard(\\n color: _randomColor, // 动态参数\\n padding: _precomputedPadding, // 预计算参数\\n ),\\n ],\\n );\\n }\\n}\\n\\nclass AdvancedCard extends StatelessWidget {\\n final Color color;\\n final EdgeInsets padding;\\n\\n const AdvancedCard({required this.color, required this.padding});\\n\\n @override\\n Widget build(BuildContext context) {\\n print(\\"AdvancedCard 重建\\"); // 用于调试性能\\n return Card(\\n color: color,\\n child: Padding(\\n padding: padding,\\n child: const Column(\\n children: [\\n Text(\\"高级配置选项\\"),\\n // 更多复杂内容...\\n ],\\n ),\\n ),\\n );\\n }\\n}\\n
\\n问题:
\\n每次切换 _isExpanded
时,AdvancedCard
会被重新构建,尽管 _randomColor
和 _precomputedPadding
在父 Widget 生命周期内不变。控制台会重复输出 AdvancedCard 重建
。
dartCopy Code\\nclass ConfigPanel extends StatefulWidget {\\n @override\\n _ConfigPanelState createState() => _ConfigPanelState();\\n}\\n\\nclass _ConfigPanelState extends State<ConfigPanel> {\\n late final Color _randomColor;\\n late final EdgeInsets _precomputedPadding;\\n late final AdvancedCard _advancedCard; // 预构建子 Widget\\n\\n @override\\n void initState() {\\n super.initState();\\n // 复杂初始化逻辑\\n _randomColor = Colors.primaries[Random().nextInt(Colors.primaries.length)];\\n final screenWidth = MediaQuery.of(context).size.width;\\n _precomputedPadding = EdgeInsets.symmetric(horizontal: screenWidth * 0.1);\\n\\n // 关键步骤:在初始化时预构建子 Widget\\n _advancedCard = AdvancedCard(\\n color: _randomColor,\\n padding: _precomputedPadding,\\n );\\n }\\n\\n bool _isExpanded = false;\\n\\n void _toggleExpansion() {\\n setState(() => _isExpanded = !_isExpanded);\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Column(\\n children: [\\n SwitchListTile(\\n title: Text(\\"展开配置面板\\"),\\n value: _isExpanded,\\n onChanged: (_) => _toggleExpansion(),\\n ),\\n if (_isExpanded) _advancedCard, // 直接复用预构建实例\\n ],\\n );\\n }\\n}\\n\\nclass AdvancedCard extends StatelessWidget {\\n final Color color;\\n final EdgeInsets padding;\\n\\n // 注意:此处不再需要 const 构造函数!\\n const AdvancedCard({required this.color, required this.padding});\\n\\n @override\\n Widget build(BuildContext context) {\\n print(\\"AdvancedCard 重建\\");\\n return Card(\\n color: color,\\n child: Padding(\\n padding: padding,\\n child: const Column(\\n children: [\\n Text(\\"高级配置选项\\"),\\n // 更多复杂内容...\\n ],\\n ),\\n ),\\n );\\n }\\n}\\n
\\n优化效果:
\\nAdvancedCard
构建一次(输出一次日志)。AdvancedCard
不再重建(无日志输出)。AdvancedCard
没有使用 const
构造函数,但通过预构建实例实现了子树复用。const
? _randomColor
和 _precomputedPadding
的值在运行时动态生成,无法作为编译时常量。AdvancedCard
的构造函数参数依赖父 Widget 的成员变量,违反 const
构造函数的条件。initState
中创建 AdvancedCard
实例,参数值在父 Widget 生命周期内保持不变。AdvancedCard
没有 const
构造函数,只要父 Widget 每次返回同一个实例,Flutter 仍会通过 oldWidget == newWidget
比较跳过子树构建。行为 | 成员变量 (_advancedCard ) | 构建时创建 (AdvancedCard(...) ) |
---|---|---|
实例生命周期 | 跟随父 Widget 状态对象 | 每次 build 时新建 |
参数值变化敏感性 | 仅在 initState 初始化时确定 | 每次 build 可能重新计算参数 |
子树重建次数 | 仅首次创建时构建 | 每次父 Widget 更新时重建 |
const
池:所有 const
构造函数创建的实例会被 Dart 编译器缓存,全局唯一。const ChildWidget(a: 1)
和 const ChildWidget(a: 2)
会生成两个不同实例。const
实例在编译时即确定,运行期无额外开销。Reprojection 模式的本质是 通过预构建将动态参数转化为静态子树,突破了 const
构造函数的编译时常量限制。这种模式在以下场景具有不可替代性:
const
实现子树复用通过该模式,开发者可以在不牺牲灵活性的前提下,实现与 const
等效的性能优化
StatefulWidget 本身不可变
\\n和所有 Widget 一样,StatefulWidget
的实例是不可变的(所有属性必须为 final
)。当需要更新 UI 时(如调用 setState()
),Flutter 会重建整个 Widget 树,生成新的 StatefulWidget
实例。
示例代码
\\ndartCopy Code\\nclass MyWidget extends StatefulWidget {\\n final String title; // 属性必须为 final\\n const MyWidget({required this.title});\\n \\n @override\\n _MyWidgetState createState() => _MyWidgetState();\\n}\\n
\\nMyWidget
可能被替换为新的实例(例如 title
改变)。StatefulWidget
的作用是描述如何创建对应的 State
对象。Widget 本身不保存状态,它仅通过 createState()
方法生成 State
。State 由 Element 持有
\\n当 StatefulWidget
首次挂载时,Element 调用 createState()
生成 State
对象,并将其保存在 Element._state
中。
Widget.canUpdate
判断),则 State
对象保留,仅通过 didUpdateWidget()
更新关联的 Widget。生命周期方法
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n方法 | 触发时机 | 典型用途 |
---|---|---|
initState() | State 首次创建时(插入 Element 树) | 初始化状态、监听器 |
didUpdateWidget() | Widget 更新时(Element 复用) | 响应 Widget 属性变化 |
dispose() | Element 从树中移除时 | 释放资源、取消监听 |
Element 复用规则
\\nFlutter 通过以下条件判断是否复用 Element(从而保留 State
):
dartCopy Code\\nstatic bool canUpdate(Widget oldWidget, Widget newWidget) {\\n return oldWidget.runtimeType == newWidget.runtimeType \\n && oldWidget.key == newWidget.key;\\n}\\n
\\nMyWidget
)。key
,则必须一致(默认 key
为 null
)。mermaidCopy Code\\nsequenceDiagram\\n participant ParentWidget\\n participant Element\\n participant State\\n\\n ParentWidget->>Element: 首次挂载(Widget 创建)\\n Element->>State: 调用 createState()\\n State->>State: initState()\\n State->>State: build() → 生成子 Widget 树\\n\\n ParentWidget->>Element: 重建(Widget 更新)\\n alt canUpdate == true\\n Element->>State: didUpdateWidget(oldWidget)\\n State->>State: build() → 更新子 Widget 树\\n else\\n Element->>State: dispose()\\n Element->>State: 销毁 State\\n Element->>NewState: 创建新 State\\n end\\n
\\nState
实例绑定到特定 Element,避免多实例状态混乱。initState()
和 dispose()
管理资源,防止内存泄漏。dartCopy Code\\nclass CounterWidget extends StatefulWidget {\\n @override\\n _CounterWidgetState createState() => _CounterWidgetState();\\n}\\n\\nclass _CounterWidgetState extends State<CounterWidget> {\\n int _count = 0;\\n\\n @override\\n void initState() {\\n super.initState();\\n print(\\"State 初始化\\");\\n }\\n\\n @override\\n void didUpdateWidget(CounterWidget oldWidget) {\\n super.didUpdateWidget(oldWidget);\\n print(\\"Widget 更新,State 保留\\");\\n }\\n\\n @override\\n void dispose() {\\n print(\\"State 销毁\\");\\n super.dispose();\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return ElevatedButton(\\n onPressed: () => setState(() => _count++),\\n child: Text(\\"点击次数: $_count\\"),\\n );\\n }\\n}\\n
\\n运行逻辑:
\\ninitState()
被调用,打印“State 初始化”。setState()
触发 Widget 重建。由于 CounterWidget
的 runtimeType
和 key
未变,Element 复用,didUpdateWidget()
被调用,打印“Widget 更新,State 保留”。CounterWidget
时,dispose()
被调用,打印“State 销毁”。initState()
→ didUpdateWidget()
→ dispose()
明确状态管理边界。这种设计使得 Flutter 在保持声明式 UI 的简洁性的同时,通过状态复用和局部更新实现了高效渲染,完美平衡了开发效率与运行时性能。
\\n在 Flutter 中,Element 重建 Widget 的过程通过以下机制实现:
\\n当 Widget 树因状态变化(如 setState()
)或外部数据更新需要重新构建时,父 Element 会通过 Element.updateChild()
方法对比新旧 Widget,决定是否重建子节点16。
条件判断
\\nElement 通过 Widget.canUpdate(oldWidget, newWidget)
判断是否复用子 Element:
dartCopy Code\\nstatic bool canUpdate(Widget oldWidget, Widget newWidget) {\\n return oldWidget.runtimeType == newWidget.runtimeType \\n && oldWidget.key == newWidget.key;\\n}\\n
\\nruntimeType
和 key
相同,复用现有 Element,仅更新其关联的 Widget13。复用 Element 的场景
\\nupdate(newWidget)
方法,将新 Widget 绑定到自身(_widget = newWidget
)37。StatefulWidget
,触发 State.didUpdateWidget(oldWidget)
通知状态更新17。rebuild()
方法,触发子 Element 的对比和更新流程(如 RenderObjectElement.updateChildren()
处理子节点列表)36。Element 类型 | 重建行为 |
---|---|
ComponentElement | 调用 performRebuild() 重新构建子 Widget 树(如 StatefulElement 触发 State.build() )36。 |
RenderObjectElement | 直接更新关联的 RenderObject 属性(如 RenderFlex 的子节点顺序变化)36。 |
ProxyElement(如 InheritedElement ) | 通知依赖的子 Element 触发重建(通过 notifyClients() )36。 |
dirty
,在下一帧渲染前通过 BuildOwner.buildScope()
集中处理这些脏节点,避免全树遍历67。key
控制 Element 复用(如列表项 key
优化滚动时的复用效率)16。假设一个 Column
的子节点列表发生变更:
旧树:Column → [Text(\\"A\\"), Text(\\"B\\")]
新树:Column → [Text(\\"B\\"), Text(\\"C\\")]
Element 处理:
\\nText(\\"A\\")
和 Text(\\"B\\")
(runtimeType
相同但内容不同),复用 Element 并更新 Widget16。Text(\\"C\\")
,创建新 Element 和 RenderObject36。Element 通过对比新旧 Widget 的 runtimeType
和 key
,选择性复用或重建自身及子节点,同时通过 dirty
标记和局部更新机制实现高效渲染36。这一设计平衡了声明式 UI 的灵活性和渲染性能16。
我们都知道当代码抛出异常时,会在控制台输出异常信息,从中可以看到异常发生时 函数调用栈 详情。这些信息可以帮助开发者快速定位到异常发生的位置,并且具体到 文件第几行,第几个字符。
\\n比如这里是初始的计数器项目,我在 _incrementCounter
方法开始时写了一行异常的代码。这样在运行时点击加号按钮触发方法,就会出现如上的异常信息。
\\n\\n当函数因异常而终止时,其下方的代码将无法执行。
\\n
也就是说,此时点击按钮,由于下面的 setState 没有触发,界面上的数值不会变化。但如果把这行异常的语句放在后面,虽然会报错,但不会影响计数器的功能:
\\n通过 try catch 代码块可以捕捉异常,这样异常的代码就 不会影响 后续代码的执行。
\\n此时通过 print 打印异常,只能得到如下所示的异常信息:那该如何查看异常时的函数调用栈信息呢?
\\n其实 catch 代码块除了异常对象,还可以通过第二参回调函数调用栈信息:
\\n此时通过打印,就可以得到如下的信息:
\\n可以使用调试来查看一下这个 stack 的运行时类型。如下所示,类型为 _StackTrace
:
除了异常捕捉外,有办法在任意代码位置,获得当前函数调用栈信息吗?比如记录一些日志时,一个事件的触发点可能有很多地方,如果能知道当前的调用栈信息,那么对问题的排查将大有裨益。
\\n\\n\\n通过
\\nStackTrace.current
可以获取当前代码位置的调用栈信息
可能有些朋友看到下面的内容就会有一些不悦,毕竟它一般代表着代码出现异常了。不过注意此时,这个函数调用栈信息不是
异常抛出的,而是我们主动获取的。
\\n另外,仔细观察一下这个调用栈信息,我们似乎可以从中一步步看到点击事件在源码中时如何实现的。信息就是有价值的,不要惧怕信息,要从中汲取到对我们有帮助的内容,它的存在是帮助我们解决问题。
本文介绍了一个非常小的知识点,但它的价值是非常大的。函数调用栈信息,可以帮我们快速定位问题所在,也能反应出一个函数被调用的全过程,也利于源码分析。那么本文就到这里,更多 Flutter 知识集锦内容,敬请期待 ~
\\n更多文章和视频知识资讯,大家可以关注我的公众号、掘金和 B 站 。
\\n","description":"1. 异常时的调用栈信息 我们都知道当代码抛出异常时,会在控制台输出异常信息,从中可以看到异常发生时 函数调用栈 详情。这些信息可以帮助开发者快速定位到异常发生的位置,并且具体到 文件第几行,第几个字符。\\n\\n比如这里是初始的计数器项目,我在 _incrementCounter 方法开始时写了一行异常的代码。这样在运行时点击加号按钮触发方法,就会出现如上的异常信息。\\n\\n当函数因异常而终止时,其下方的代码将无法执行。\\n\\n也就是说,此时点击按钮,由于下面的 setState 没有触发,界面上的数值不会变化。但如果把这行异常的语句放在后面,虽然会报错…","guid":"https://juejin.cn/post/7485633146315751461","author":"张风捷特烈","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-25T23:08:43.497Z","media":[{"url":"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1dbfb12dd14746f7bd2845c577bef7d1~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1920&h=1200&s=413880&e=png&b=fefbfb","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d2e8e60b5f8a46ebaaa9a132813d52eb~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1251&h=474&s=88494&e=png&b=fefdfd","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/deb3f1fe58e74a43acd696406e3df4a9~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=906&h=204&s=21055&e=png&b=ffffff","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ff1e2578590341ce9123e1e9f5705a81~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=932&h=212&s=16515&e=png&b=ffffff","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f3ac7cbb832e419ca85d771c8f407356~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=985&h=326&s=24197&e=png&b=ffffff","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f8335bba8e2c44dd88120a6828ff4773~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=955&h=102&s=7770&e=png&b=ffffff","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8d02b15ba5ea4709bcd1a761d2106513~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1048&h=325&s=33534&e=png&b=ffffff","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9bb0180f9ad941fcb75eef4e5f67640f~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1172&h=409&s=76717&e=png&b=fffefe","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8f7d517b1d65441a91c364fdc4a15372~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1189&h=474&s=67042&e=png&b=f4f5f8","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1510b335643449e58e78b5183e5fe020~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1102&h=461&s=98410&e=png&b=fffefe","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9e3daac4236d4269a0a98b72d5c0509b~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1483&h=737&s=231511&e=png&b=ffffff","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"Flutter应用开发:多条件搜索","url":"https://juejin.cn/post/7485307269753225255","content":"业务开始需求相对简单,搜索条件也比较单一,例如:根据订单号OrderNo查询对应订单。随着业务需求的升级,需要支持的查询条件也越来越多,组合查询、额外条件查询等需求升级,搜索上也要适配同步升级。
\\n原始需求是支持根据订单号查询
\\n这些搜索条件也是在使用过程中逐渐加上来的,因为原始设计UI参与后撤离了,导致后续需求跟进缺乏UI设计支持。虽然能满足需求,界面交互设计上真的是一言难尽。
\\n这一阶段是一个临时的方案,根据进入入口的不同,区分搜索界面进入哪种查询模式。
\\n\\n从功能实现、界面展示、用户交互等角度看,都不是一个好的方案。于是决定参照业界优秀的案例进行改造。
电商产品比较通用的方案,比较适配当前业务
\\n首先将上图贴到豆包
尝试搜索到相关现成插件,未发现任何相关插件。又进入gitee
尝试找到类似电商开源项目剥离出实现逻辑,也未发现适配项目。不得已开始分析按钮,人工实现一下。
分析自身需求,提取相似点,分离不同点。因为状态(单选组件)、时间范围(时间选择组件)、其他都是输入框,大体分为三类。
\\n// 定义渲染Tag\\nList<Map<String, String>> _dropdownItems = [\\n {\'label\': \'订单号\', \'value\': \'fuzzyOrderNo\'},\\n {\'label\': \'揽货人\', \'value\': \'accUserName\'},\\n {\'label\': \'发货人\', \'labels\': \'发货人姓名,发货人电话\', \'value\': \'corUserName,corUserPhone\'}, \\n {\'label\': \'收货人\', \'labels\': \'收货人姓名,收货人电话\', \'value\': \'ceeUserName,ceeUserPhone\'}, \\n {\'label\': \'站点\', \'value\': \'toStation\'},\\n];\\n \\nList<Widget> queryTags = [\\n QueryTag(text: \'状态\', isActive: _order.orderStatus != null && _order.orderStatus!.isNotEmpty),\\n QueryTag(text: \'时间范围\', isActive: _order.createTimeStart != null),\\n ..._dropdownItems.map((e) {\\n final values = e[\'value\']!.split(\',\');\\n return QueryTag(text: e[\'label\']!, isActive: values.any((v) => _order.getProperty(v) != null));\\n }).toList(),\\n];\\n
\\n渲染部分使用Wrap
组件来实现,因为搜索条件动态渲染,除过支持自动换行外,还需要计算渲染区域的高度来动态设置。
Widget wrapper = LayoutBuilder(\\n key: _wrapperKey,\\n builder: (context, constraints) {\\n // 在布局完成后获取高度\\n WidgetsBinding.instance.addPostFrameCallback((_) {\\n final RenderBox? renderBox = _wrapperKey.currentContext?.findRenderObject() as RenderBox?;\\n if (renderBox != null && mounted) {\\n setState(() {\\n _wrapperHeight = renderBox.size.height + rpx(8.0); // 增加内边距余量\\n });\\n }\\n });\\n return Wrap(\\n direction: Axis.horizontal,\\n spacing: 6.0,\\n runSpacing: 4.0,\\n children: queryTags\\n .map(\\n (e) => InkWell(\\n child: Container(width: rpx(80.0), child: e),\\n onTap: () {\\n // 计算当前标签在queryTags中的索引\\n final tagIndex = queryTags.indexOf(e);\\n _selectedIndexCache = tagIndex;\\n _showSearchBottomSheet();\\n },\\n ),\\n )\\n .toList(),\\n );\\n },\\n );\\n\\n return <Widget>[\\n SliverPersistentHeader(\\n pinned: true,\\n floating: true,\\n delegate: SliverAppBarDelegate(\\n Container(\\n height: MediaQuery.of(context).size.height,\\n padding: edgeSymmetric(vertical: 4.0),\\n child: Column(\\n children: [Container(child: wrapper)],\\n )),\\n _wrapperHeight > 0 ? _wrapperHeight : rpx(90.0),\\n ),\\n ),\\n ];\\n
\\n实现效果和案例上还是有些差距,不过已经美观很多
\\n搜索弹框需要注意和Tag区域的联动交互
,组合查询的表现,三类组件
的实现方式。左侧Tab以红点
表示对应条件已经应用,顶部Tag区域的红色高亮与其对应。样式参考按钮优化。
\\n代码比较多,整体划分左右两部分,只需要注意使用
IndexedStack
组件完成左侧Tab点击展示右侧对应锚点内容。右侧业务区域设置一组动态Key来做唯一标识。
final Map<int, GlobalKey> _formKeys = Map.fromEntries(List.generate(_dropdownItems.length + 2, (index) => MapEntry(index, GlobalKey())));\\n
\\n IndexedStack(index: _selectedIndex, children: [\\n // 状态选择(使用_index=0)\\n SpecificaButton(\\n key: _formKeys[0],\\n title: \'选择状态\',\\n defaultValue: _order.orderStatus,\\n vModels: _searchStatusList,\\n canCancel: true,\\n onChanged: (value) {\\n Iterable<vModel> temp = _searchStatusList.where((element) => element.id == value);\\n setState(() {\\n _order.orderStatus = value != null ? temp.first.id : null;\\n });\\n _setFilterAdditional();\\n }),\\n\\n Padding(\\n key: _formKeys[1],\\n padding: EdgeInsets.all(rpx(12)),\\n child: Column(children: [\\n Row(\\n mainAxisAlignment: MainAxisAlignment.start,\\n children: [Text(\'起始时间\', style: TextStyle(fontSize: rpx(11.0)))],\\n ),\\n Gaps.vGap5,\\n ElFormItem(\\n \'\',\\n type: \'select\',\\n required: false,\\n hintText: \'请选择起始时间\',\\n initValue: _order.createTimeStartStr,\\n labelSize: rpx(12.0),\\n onOpenSheet: () async {\\n final result = await showBoardDateTimePicker(\\n context: context,\\n pickerType: DateTimePickerType.datetime,\\n options: BoardDateTimeOptions(\\n languages: const BoardPickerLanguages.zh(),\\n startDayOfWeek: DateTime.sunday,\\n useResetButton: true,\\n pickerFormat: PickerFormat.ymd,\\n withSecond: true,\\n ),\\n );\\n if (result != null) {\\n setState(() {\\n _order.createTimeStartStr = DateFormat(\'yyyy-MM-dd HH:mm:ss\').format(result);\\n _order.createTimeStart = result.millisecondsSinceEpoch;\\n });\\n }\\n },\\n suffixIcon: IconButton(\\n onPressed: () {\\n setState(() {\\n _order.createTimeStartStr = null;\\n _order.createTimeStart = null;\\n });\\n }, // ← 这里添加缺失的逗号\\n padding: edgeAll(0),\\n icon: Icon(Icons.close, size: rpx(14.0))),\\n ),\\n Gaps.vGap10,\\n Row(\\n mainAxisAlignment: MainAxisAlignment.start,\\n children: [Text(\'截止时间\', style: TextStyle(fontSize: rpx(11.0)))],\\n ),\\n Gaps.vGap5,\\n ElFormItem(\\n \'\',\\n type: \'select\',\\n required: false,\\n hintText: \'请选择截止时间\',\\n initValue: _order.createTimeEndStr,\\n labelSize: rpx(12.0),\\n onOpenSheet: () async {\\n final result = await showBoardDateTimePicker(\\n context: context,\\n pickerType: DateTimePickerType.datetime,\\n options: BoardDateTimeOptions(\\n languages: const BoardPickerLanguages.zh(),\\n startDayOfWeek: DateTime.sunday,\\n useResetButton: true,\\n pickerFormat: PickerFormat.ymd,\\n withSecond: true,\\n ),\\n );\\n if (result != null) {\\n setState(() {\\n _order.createTimeEndStr = DateFormat(\'yyyy-MM-dd HH:mm:ss\').format(result);\\n _order.createTimeEnd = result.millisecondsSinceEpoch;\\n });\\n }\\n },\\n suffixIcon: IconButton(\\n onPressed: () {\\n setState(() {\\n _order.createTimeEndStr = null;\\n _order.createTimeEnd = null;\\n });\\n },\\n padding: edgeAll(0),\\n icon: Icon(Icons.close, size: rpx(14.0))),\\n ),\\n ])),\\n\\n // 动态生成字段输入(使用_index=1到N)\\n ..._dropdownItems.asMap().entries.map((entry) {\\n final values = entry.value[\'value\']!.split(\',\');\\n final labels = entry.value[\'labels\'] != null ? entry.value[\'labels\']!.split(\',\') : [entry.value[\'label\']!];\\n return Column(\\n key: _formKeys[2 + entry.key], // 从1开始分配key\\n children: [\\n ...values.map((fValue) => Padding(\\n padding: EdgeInsets.all(rpx(12)),\\n child: Column(\\n children: [\\n Row(\\n mainAxisAlignment: MainAxisAlignment.start,\\n children: [Text(labels[values.indexOf(fValue)], style: TextStyle(fontSize: rpx(11.0)))],\\n ),\\n Gaps.vGap5,\\n ElFormItem(\\n \'\',\\n required: false,\\n hintText: \'请输入\',\\n initValue: _order.getProperty(fValue),\\n labelSize: rpx(12.0),\\n suffixIcon: IconButton(\\n onPressed: () {\\n setState((() {\\n _order.setProperty(fValue, null);\\n }));\\n }, // ← 这里添加缺失的逗号\\n color: Colours.status_gray,\\n icon: Icon(\\n Icons.close,\\n size: rpx(14.0),\\n ),\\n ),\\n onChanged: (value) {\\n _order.setProperty(fValue, value);\\n },\\n ),\\n ],\\n )))\\n ]);\\n }),\\n ])\\n
\\n在开发思路上在寻找现成插件上浪费太多时间,迟迟没有落地实现。真正没有办法去实现时,借助AI辅助编程发现效率也很高,效果也比较好。之前一直遵循的开发思路在AI编程兴起后,有些落伍了。程序老兵们转变一下思路,或许还能苟几年。
","description":"背景 业务开始需求相对简单,搜索条件也比较单一,例如:根据订单号OrderNo查询对应订单。随着业务需求的升级,需要支持的查询条件也越来越多,组合查询、额外条件查询等需求升级,搜索上也要适配同步升级。\\n\\n开发历程\\n单一搜索\\n\\n原始需求是支持根据订单号查询\\n\\n多条件搜索(互斥)\\n\\n这些搜索条件也是在使用过程中逐渐加上来的,因为原始设计UI参与后撤离了,导致后续需求跟进缺乏UI设计支持。虽然能满足需求,界面交互设计上真的是一言难尽。\\n\\n多条件搜索(互斥 + 部分组合)\\n\\n这一阶段是一个临时的方案,根据进入入口的不同,区分搜索界面进入哪种查询模式。\\n\\n由筛选弹框触发…","guid":"https://juejin.cn/post/7485307269753225255","author":"机器瓦力","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-25T03:30:42.888Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7dfd43f0a7d54daf93474b5d6aad890d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5py65Zmo55Om5Yqb:q75.awebp?rk3s=f64ab15b&x-expires=1743478242&x-signature=iBvc3JHwRob1a6KtXTo%2FtLNCSxc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a6226e379eeb41c495a9df949ae18cc1~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5py65Zmo55Om5Yqb:q75.awebp?rk3s=f64ab15b&x-expires=1743478242&x-signature=1fc3PzJZTxe3dWN7Gv8YUWvweXQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/dab029d39f5149b9a65eca51152f2415~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5py65Zmo55Om5Yqb:q75.awebp?rk3s=f64ab15b&x-expires=1743478242&x-signature=jcynhTK43bt%2BUdCKpCzxlS7azeQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/405b3442c08041b79e5c91e1adb8ba19~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5py65Zmo55Om5Yqb:q75.awebp?rk3s=f64ab15b&x-expires=1743478242&x-signature=8FfFrv55Ba5IeWCGtPi1Yr5CI8k%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4a15ec554915439984d8ca0e4d2aaf92~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5py65Zmo55Om5Yqb:q75.awebp?rk3s=f64ab15b&x-expires=1743478242&x-signature=6f7dJWR1mPGdpnAE1E8qISwGmN4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/024e0107c944444d9b7ca0dd559819cb~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5py65Zmo55Om5Yqb:q75.awebp?rk3s=f64ab15b&x-expires=1743478242&x-signature=nM0CshdLI4KIc6NPdRZg39o66q0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5671b284a2a74c58802a554937243b0b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5py65Zmo55Om5Yqb:q75.awebp?rk3s=f64ab15b&x-expires=1743478242&x-signature=ZwSyNJ2sezmf697624o5tGlhlug%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Dart异步编程:一、认识任务","url":"https://juejin.cn/post/7485256842618978345","content":"任务就像是程序里的一个个小包裹,每个包裹都有自己的内容和去向。比如,当你在手机应用里点击一个按钮去获取网络数据,这个获取数据的操作就是一个任务。它有自己的起点(你点击按钮的时候),过程(网络传输数据),和终点(数据到达你的手机)。
\\n在Dart里,任务被定义为一个独立的工作单元,它可以是一个简单的计算,也可以是一个复杂的网络请求。任务可以被并发或者并行执行,这意味着在合适的条件下,多个任务可以同时进行,从而提高程序的效率。
\\nDart中的任务主要分为同步任务、微任务、事件任务、I/O回调任务和渲染任务。
\\n同步任务就像是你直接去商店买东西,买完就走,不会等待其他事情完成。它会直接在当前线程中执行,直到完成为止。
\\nvoid syncTask() {\\n print(\\"同步任务开始\\");\\n // 同步操作\\n print(\\"同步任务结束\\");\\n}\\n
\\n微任务则像是你提前预约的服务,它会在所有同步任务完成后第一时间处理。微任务具有最高的优先级,适用于需要立即执行的异步操作。
\\nvoid scheduleMicrotaskExample() {\\n scheduleMicrotask(() {\\n print(\\"微任务执行\\");\\n });\\n}\\n
\\n事件任务和I/O回调任务则更像是你打电话预约,等对方准备好后再来处理。事件任务是标准优先级的异步任务(优先级比微任务低)。
\\nFuture<void> eventTask() async {\\n print(\\"事件任务开始\\");\\n // 模拟异步操作\\n await Future.delayed(Duration(seconds: 1));\\n print(\\"事件任务结束\\");\\n}\\n
\\nI/O回调任务是由系统事件触发的任务,例如文件读取完成后的回调。
\\nvoid ioCallbackTask() {\\n File(\\"example.txt\\").readAsString().then((String contents) {\\n print(\\"I/O回调任务完成,文件内容为:$contents\\");\\n });\\n}\\n
\\n渲染任务则是在特定的时间点,比如屏幕刷新的时候,来更新界面。
\\nvoid renderTask() {\\n WidgetsBinding.instance.scheduleFrame();\\n}\\n
\\n任务的调度就像是一个交通指挥官,决定着哪个任务先执行,哪个任务后执行。常见的调度策略有先来先服务、轮转调度和优先级调度。在Dart里,任务的调度主要通过事件循环来实现,它就像是一个永不停歇的邮递员,不断地从任务队列里取出任务并执行。
\\n代码示例:
\\nvoid eventLoopExample() {\\n print(\\"主线程开始\\");\\n\\n // 微任务\\n scheduleMicrotask(() {\\n print(\\"微任务1\\");\\n });\\n\\n // 异步任务\\n Future.delayed(Duration(seconds: 0), () {\\n print(\\"异步任务1\\");\\n });\\n\\n // 再次添加微任务\\n scheduleMicrotask(() {\\n print(\\"微任务2\\");\\n });\\n\\n print(\\"主线程结束\\");\\n}\\n\\n//输出结果:\\n主线程开始\\n主线程结束\\n微任务1\\n微任务2\\n异步任务1\\n
\\n在 Dart 的事件循环中,执行顺序如下:
\\n同步就像是你排队买奶茶,必须站在那里等奶茶做好才能离开,整个过程是顺序的,不能做其他事情。而异步则像是你扫码点奶茶,点完后可以去做其他事情,奶茶做好后会通知你来取。在Dart里,同步任务会阻塞后续代码的执行,而异步任务则不会,它可以让程序在等待某个任务完成的同时继续执行其他代码。
\\n// 同步示例\\nvoid synchronousExample() {\\n print(\\"同步操作开始\\");\\n // 模拟耗时操作\\n for (var i = 0; i < 100000000; i++) {}\\n print(\\"同步操作结束\\");\\n}\\n\\n// 异步示例\\nvoid asynchronousExample() async {\\n print(\\"异步操作开始\\");\\n // 模拟异步耗时操作\\n await Future.delayed(Duration(seconds: 2));\\n print(\\"异步操作结束\\");\\n}\\n
\\n异步编程的优势在于它能够提高程序的效率和响应速度。比如在处理网络请求时,如果使用同步方式,程序会一直等待网络数据返回,期间无法执行其他操作。而使用异步方式,程序可以在等待数据的同时继续处理其他任务,当数据返回时再通过回调函数来处理结果。
\\n任务的生命周期通常包括以下几个阶段:
\\n创建(Created) : 任务被定义或创建,但尚未开始执行。
\\n等待(Pending) :任务已经准备好,等待执行。
\\n运行中(Running) :任务正在执行中。
\\n完成(Completed) :任务
\\n在 Dart 中,异步任务的生命周期可以通过 Future
的状态来理解。以下是任务生命周期的流程图描述:
在 Dart 中,任务分为同步任务、微任务、事件任务、I/O 回调任务和渲染任务,它们通过事件循环进行调度,顺序为先执行同步代码,再依次执行微任务队列和事件队列中的任务,其中异步编程能提高程序效率和响应速度,任务从创建到完成会经历创建、等待、运行中和完成四个阶段。
","description":"前言 任务就像是程序里的一个个小包裹,每个包裹都有自己的内容和去向。比如,当你在手机应用里点击一个按钮去获取网络数据,这个获取数据的操作就是一个任务。它有自己的起点(你点击按钮的时候),过程(网络传输数据),和终点(数据到达你的手机)。\\n\\n在Dart里,任务被定义为一个独立的工作单元,它可以是一个简单的计算,也可以是一个复杂的网络请求。任务可以被并发或者并行执行,这意味着在合适的条件下,多个任务可以同时进行,从而提高程序的效率。\\n\\n基础概念\\n\\nDart中的任务主要分为同步任务、微任务、事件任务、I/O回调任务和渲染任务。\\n\\n1. 同步任务\\n\\n同步任务就像是你…","guid":"https://juejin.cn/post/7485256842618978345","author":"_痞老板","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-25T01:06:02.211Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/83dc0e680e6e4175b22372289d73ebd3~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgX-eXnuiAgeadvw==:q75.awebp?rk3s=f64ab15b&x-expires=1743469562&x-signature=jWeCRz9FR5nFVUH6NlnJ7QY9aeE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6486569f25c84fbcb0b7b5e357108a21~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgX-eXnuiAgeadvw==:q75.awebp?rk3s=f64ab15b&x-expires=1743469562&x-signature=dl05VTdTiuMo2BeKp6cJ9Uh2f1Q%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/70a39e551c8f4b1eb992b1c62cf883ce~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgX-eXnuiAgeadvw==:q75.awebp?rk3s=f64ab15b&x-expires=1743469562&x-signature=rdxvnSvGUdBzSsAnriBtH6X5d%2BI%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Dart","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Dart 之异常处理","url":"https://juejin.cn/post/7485200128730300427","content":"在代码的世界里,异常如同旅途中的意外天气,无论准备如何详细,也无法完全避免台风、地震等问题。而异常处理机制,正是让程序从“脆弱易崩”走向“健壮可靠”的关键。Dart为此提供了清晰且灵活的异常管理策略,既能精准捕获错误,又能分层处理,确保应用在复杂场景中依然从容优雅。
\\n在编程中,说到异常处理,我们第一感觉就是用来提升代码的健壮性和可维护性的,似乎没有啥用,但其实异常处理的作用远远不止于此。我们来逐一说到说到。
\\n异常就如同其名字一样,指非正常的情况。是程序运行中的错误或发生的意外情况(如图片加载错误、内存溢出等)。在Dart中使用throw关键字抛出异常,使用try、on、catch、finally关键字捕获并处理异常。
\\n在Dart中异常使用对象来表示,并将异常分为两类,一类是Error,一类是Exception。
\\nDart中使用关键字throw抛出异常。
\\n示例:
\\nint checkAge(int age){\\n if(age < 0){\\n throw Exception(\'$age为负数,年龄不能为负数\');\\n }\\n return age;\\n}\\n
\\n使用try、on、catch、finally关键字处理异常。
\\n示例:
\\nvoid main() {\\n try {\\n // 可能会有异常的代码\\n throw Exception(\'异常\');\\n } on Exception catch (error) {\\n // 捕获Exception类型的异常,并获取异常对象error,用on限定捕获异常的范围。\\n print(\'$error\');\\n } catch (e) {\\n // 捕获所有类型的异常,并获取异常对象e。\\n print(\'未知异常\');\\n }\\n finally {\\n // 无论是否异常都会执行的代码。\\n print(\'释放资源\');\\n }\\n}\\n
\\nDart中支持自定义异常的类型,通过继承Exception或Error创建自定义异常。
\\n示例:
\\n/// 自定义异常类型CustomException\\nclass CustomException implements Exception{\\n String? info;\\n CustomException(this.info);\\n @override\\n String toString(){\\n return \'自定义异常:$info\';\\n }\\n}\\n// 抛出自定义的异常类型\\nint checkAge(int age){\\n if (0 < age && age < 18){\\n throw CustomException(\'未满18岁!\');\\n }\\n return age;\\n}\\n\\n// 捕获自定义的异常\\nvoid main() {\\n try{\\n int currentAge = checkAge(15); // 出现异常。\\n print(\'年龄为:$currentAge\'); // 不会打印。\\n } on CustomException catch(error){\\n print(\'$error\'); // 输出:自定义异常:未满18岁!\\n }\\n}\\n
\\n如果存在函数抛出了异常且没有被捕获,这个异常会向上层调用栈向上传播,直到这个异常被捕获或程序终止。
\\n示例:
\\nvoid main() {\\n try {\\n dealNum(1, 1); // 调用dealNum函数。\\n } on CustomException catch (e) {\\n print(\'$e\'); // 输出:Exception: 分子不能为偶数\\n } on Exception catch (e) {\\n print(\'$e\'); // 已经捕获到自定义异常,不打印Exception类型的异常。\\n } catch (e) {\\n print(\'其他异常\'); // 已经捕获到自定义异常,不打印其他类型的异常。\\n } finally {\\n print(\'关闭资源\'); // 输出:关闭资源\\n }\\n}\\n// dealNum函数中没有进行捕获,会向上层调用栈传播,即向main函数传播。\\nvoid dealNum(int a, int b){\\n int c = 2*a;\\n int d = 2*b;\\n divided(c,d); // 调用divided函数\\n}\\n// divided函数中抛出异常。\\ndouble divided(int a, int b) {\\n if (b == 0) {\\n throw CustomException(\'分母不能为零\');\\n }\\n if (a % 2 == 0) {\\n throw Exception(\'分子不能为偶数\');\\n }\\n if (a * b == 1) {\\n throw Error();\\n }\\n return a / b;\\n}\\n
\\n本小节我们从异常的重要性出发,首先介绍了Dart中异常处理的定义与异常类型,其次了解了Dart中异常处理的基本语法(throw关键词抛出异常,try、on、catch、finally关键字处理异常),然后介绍了自定义异常与异常传播,最后对异常处理的使用进行了建议。
","description":"前言 在代码的世界里,异常如同旅途中的意外天气,无论准备如何详细,也无法完全避免台风、地震等问题。而异常处理机制,正是让程序从“脆弱易崩”走向“健壮可靠”的关键。Dart为此提供了清晰且灵活的异常管理策略,既能精准捕获错误,又能分层处理,确保应用在复杂场景中依然从容优雅。\\n\\n一、为什么需要异常处理?\\n\\n在编程中,说到异常处理,我们第一感觉就是用来提升代码的健壮性和可维护性的,似乎没有啥用,但其实异常处理的作用远远不止于此。我们来逐一说到说到。\\n\\n提升代码的健壮性和可维护性,这我们都知道。\\n防止程序崩溃:未处理的异常会导致程序立即终止…","guid":"https://juejin.cn/post/7485200128730300427","author":"好的佩奇","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-24T14:12:10.368Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b566bb945f1e4ec5b4f0fe4cea120b75~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5aW955qE5L2p5aWH:q75.awebp?rk3s=f64ab15b&x-expires=1743431147&x-signature=9exASYTzCz59SUSBiQufslM7lgQ%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["前端","Flutter","Dart","Android"],"attachments":null,"extra":null,"language":null},{"title":"[周二直播] 用 Trae 和 Flutter 开发你的第一款全平台\\"答案之书\\" APP|AI For Code工作坊 Vol.8","url":"https://juejin.cn/post/7485180710367412275","content":"你是否想过,如何用一套代码开发出能在 iOS、Android 和网页上同时运行的应用?或者你对 AI 辅助编程充满好奇,想知道它如何提升你的开发效率?
\\nAI For Code 工作坊第八期带你揭秘多平台应用开发的秘诀,学习如何借助 Trae 和 Flutter 打造一款实用的\\"答案之书\\"应用!
\\n阿衡 - 拥有丰富游戏开发经验的全栈开发者:
\\nTrae 是一款由字节跳动推出的 AI 驱动集成开发环境(IDE),通过智能代码补全、多模态交互和上下文分析等功能,让开发者更高效地编写代码。它的核心功能包括:
\\nTrae 的强大 AI 助手让你在编程过程中如虎添翼,无论是新手还是资深开发者,都能从中受益!
\\n直播名称:使用 Trae 开发多平台 APP 答案之书|AI For Code工作坊 Vol.8
\\n直播时间:3月25日(周二) 19:00-20:30 (GMT+8)
\\n参与方式:点击链接 一键预约直播
\\n立即预约直播,即可获得讲师整理的学习资料和源码,抢先体验 Trae 开发的魅力!点击下方链接,加入我们吧!
\\n","description":"你是否想过,如何用一套代码开发出能在 iOS、Android 和网页上同时运行的应用?或者你对 AI 辅助编程充满好奇,想知道它如何提升你的开发效率? AI For Code 工作坊第八期带你揭秘多平台应用开发的秘诀,学习如何借助 Trae 和 Flutter 打造一款实用的\\"答案之书\\"应用!\\n\\n🌟 直播亮点\\nTrae 与 AI 深度集成:体验智能问答、代码自动补全和 Agent 驱动的 AI 自动编程能力,感受 AI 助手如何高效协作。\\n一次编码,多端运行:通过 Flutter 框架开发可同时在 iOS、Android 和 Web 平台运行的应用。…","guid":"https://juejin.cn/post/7485180710367412275","author":"掘金酱","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-24T12:04:09.103Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/217967a9333843b493902b7a2c67843b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5o6Y6YeR6YWx:q75.awebp?rk3s=f64ab15b&x-expires=1743422649&x-signature=P2egQ0T0fALTP8I0u2W6%2F%2Fsd%2Bu0%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["代码人生","AI 编程","Trae","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"flutter:内存溢出问题定位","url":"https://juejin.cn/post/7485200128729071627","content":"迭代需求有了短视频,要求有:
\\n在需求会之前,搜了下方案,发现了这个:\\nflutter_tiktok大喜,遂直接clone到本地研读,运行了一下demo。感觉还不错哦,事情的进展是不是太顺利了。
\\n不出意外的话,就出意外了,因为某些性能上的问题,不能继续下去。
\\n研究了下当前可用的播放器,从原生角度来讲,只有video player和ijkplayer两种,要考虑格式支持、内存控制,这两种播放器都不能达到上线的标准,最后使用了付费的播放器。
\\n用对了播放器,需求进展的很顺利,直到,iOS突然崩了,日志显示内存溢出了。
\\n每次闪退,都发生在刷视频的过程,从操作角度来看,一定是播放器的问题了。进到播放器,内存突然增加,持续刷视频,持续增加。
\\n持续增长的内存,居高不下,直到闪退。
\\n从表面现象查代码,眼睛都看花了,也没看出个所以然来,必须找证据了。
\\n看GC前后class的instances变化
\\n发现Image数量特别高,一直在增加,于是将视频封面处理掉,在看内存状态,处于一个动态平衡状态。
\\n图片问题处理后,内存处于动态平衡状态。
\\n在Flutter开发中,生命周期和项目启动流程是核心概念,以下分为两部分详细说明:
\\nFlutter的生命周期主要围绕 Widget,尤其是 StatefulWidget 的状态管理。以下是关键生命周期方法:
\\n特点:无状态,不可变,每次父组件重建时自身也会重建。
\\n生命周期:
\\nState
类管理)。State
类中):方法 | 调用时机 | 用途 |
---|---|---|
initState() | Widget首次插入树中时调用(仅一次) | 初始化状态、订阅流、启动动画等。 |
didChangeDependencies() | 1. initState() 后调用; 2. 依赖的InheritedWidget 更新时调用。 | 处理依赖变化(如主题、本地化)。 |
build() | 1. didChangeDependencies() 后调用; 2. 调用setState() 触发重建。 | 构建UI。 |
didUpdateWidget() | 父组件重建,传入新配置时调用(旧Widget被替换,但State 保留)。 | 对比新旧配置,响应Widget属性变化。 |
deactivate() | Widget从树中移除时调用(可能重新插入到其他位置)。 | 清理与树相关的资源(如焦点监听)。 |
dispose() | Widget永久移除时调用(State 对象销毁)。 | 释放资源(取消计时器、关闭流、移除监听)。 |
通过WidgetsBindingObserver
监听应用全局状态:
class MyApp extends StatefulWidget {\\n @override\\n _MyAppState createState() => _MyAppState();\\n}\\n\\nclass _MyAppState extends State<MyApp> with WidgetsBindingObserver {\\n @override\\n void initState() {\\n super.initState();\\n WidgetsBinding.instance.addObserver(this);\\n }\\n\\n @override\\n void dispose() {\\n WidgetsBinding.instance.removeObserver(this);\\n super.dispose();\\n }\\n\\n @override\\n void didChangeAppLifecycleState(AppLifecycleState state) {\\n switch (state) {\\n case AppLifecycleState.resumed: // 应用可见并响应用户\\n case AppLifecycleState.inactive: // 应用处于非活动状态(如来电)\\n case AppLifecycleState.paused: // 应用不可见(如进入后台)\\n case AppLifecycleState.detached: // 应用被销毁\\n }\\n }\\n}\\n
\\nmain()
void main() {\\n runApp(MyApp()); // 启动应用,挂载根Widget\\n}\\n
\\nrunApp(Widget root)
MaterialApp
)class MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n title: \'Flutter Demo\',\\n theme: ThemeData(primarySwatch: Colors.blue),\\n home: HomePage(), // 主页Widget\\n );\\n }\\n}\\n
\\nHomePage
)initState()
、build()
等生命周期方法。class HomePage extends StatefulWidget {\\n @override\\n _HomePageState createState() => _HomePageState();\\n}\\n\\nclass _HomePageState extends State<HomePage> {\\n @override\\n void initState() {\\n super.initState();\\n // 初始化数据\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\'Home\')),\\n body: Center(child: Text(\'Welcome!\')),\\n );\\n }\\n}\\n
\\nbuild()
方法重建UI,保留应用状态。initState()
中启动异步任务时,需在dispose()
中取消,避免内存泄漏。Provider
、Riverpod
等库管理状态,减少setState()
滥用。build()
中执行耗时操作,防止界面卡顿。通过理解生命周期和启动流程,可以更高效地控制资源加载、状态更新和UI渲染。
","description":"在Flutter开发中,生命周期和项目启动流程是核心概念,以下分为两部分详细说明: 一、Flutter 生命周期\\n\\nFlutter的生命周期主要围绕 Widget,尤其是 StatefulWidget 的状态管理。以下是关键生命周期方法:\\n\\n1. StatelessWidget\\n\\n特点:无状态,不可变,每次父组件重建时自身也会重建。\\n\\n生命周期:\\n\\n构造函数:创建时调用。\\nbuild() :构建UI,每次父组件更新或依赖数据变化时调用。\\n2. StatefulWidget\\n特点:包含可变状态(通过State类管理)。\\n生命周期方法(在State…","guid":"https://juejin.cn/post/7484556668681584675","author":"无知的前端","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-24T01:06:12.723Z","media":null,"categories":["iOS","Flutter","Dart","面试"],"attachments":null,"extra":null,"language":null},{"title":"Flutter&Flame 游戏实践#23 | 游戏盒#2 - 多页签","url":"https://juejin.cn/post/7484530047866814518","content":"该系列是 [张风捷特烈] 的 Flame 游戏开发教程。Flutter 作为 全平台 的 原生级 渲染框架,兼具 全端
跨平台和高性能的特点。目前官方对休闲游戏的宣传越来越多,以 Flame 游戏引擎为基础,Flutter 有游戏方向发展的前景。本系列教程旨在让更多的开发者了解 Flutter 游戏开发。
\\n\\n第一季:30 篇短文章,快速了解 Flame 基础。
\\n[已完结]
\\n第二季:从休闲游戏实践,进阶 Flutter&Flame 游戏开发。
两季知识是独立存在的,第二季 不需要 第一季作为基础。本系列教程源码地址在 【toly1994328/toly_game】,系列文章列表可在《文章总集》 或 【github 项目首页】 查看。
\\n上一篇,我们实现了将多个游戏集成到一个 App 中的壮举,并且支持打开游戏。本篇将继续优化桌面端的交互体验,让打开的游戏支持 多页签 和 界面保活,从而更便捷地切换已打开的游戏。
\\n上一篇中为了使左侧导航栏点击时,仅在右侧面板区域变换,使用了 ShellRoute
实现局部导航。
\\n现在希望游戏中心打开的游戏界面,可以通过顶部栏展示,并且通过 Tab 页签导航,或者关闭。这就相当于在游戏中心界面,又有一个局部导航,也就是二级导航:
首先定义一下橙色区域的路由内容,这里通过命名路由的方式,让游戏的名字作为参数,来动态控制路由对应组件的创建,如下所示:
\\nRouteBase get gameRoute => GoRoute(\\n path: \'game/:name\',\\n pageBuilder: (_, GoRouterState state) {\\n String? gameName = state.pathParameters[\'name\'];\\n Widget child = gameWidgetMap[gameName] ?? const GameCenter();\\n return NoTransitionPage(child: child);\\n },\\n);\\n\\nMap<String, Widget> get gameWidgetMap => {\\n \\"sweeper\\": const SweeperPage(),\\n \\"trex\\": const TrexPage(),\\n \\"breaks\\": const BricksPage(),\\n \\"snake\\": const SnakePage(),\\n \\"life_game\\": const LifeGamePage(),\\n };\\n
\\n在路由树中,增加以 GameCenterNavigation
为导航器的二级导航,它顶部是的 GameCenterTopBar
组件,用于展示页签列表进行导航:
class GameCenterNavigation extends StatelessWidget {\\n final Widget child;\\n\\n const GameCenterNavigation({super.key, required this.child});\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n backgroundColor: Colors.transparent,\\n body: Column(\\n children: [\\n const GameCenterTopBar(),\\n const Divider(),\\n Expanded(child: child)\\n ],\\n ),\\n );\\n }\\n}\\n
\\n接下来需要维护页签数据,在状态类中增加 tabMenus
列表记录菜单信息,其中 ImageMenu
是 tolyui_meta 中定义的图片菜单数据:
class GameCenterState {\\n final List<GamePo> games;\\n final List<ImageMenu> tabMenus;\\n\\n const GameCenterState({\\n this.games = const [],\\n this.tabMenus = const [],\\n });\\n}\\n
\\n在 GameCenterBloc
中维护数据变化的业务逻辑,如下定义 openGame
方法,传入游戏的 id ,增加页签。
\\n这里给了一个 findGameById
方法,用于根据 id 找到 GamePo
, 比如 web 中直接通过 url 访问游戏,此时游戏列表可能还未加载。该方法可以让任何游戏仅通过 id 即可打开。
\\n根据 GamePo 生成一个 ImageMenu
页签菜单,加入到页签列表即可:
----\x3e[lib/logic/bloc/bloc.dart]----\\nvoid openGame(String id) {\\n GamePo? po = findGameById(id);\\n if (po == null) return;\\n List<ImageMenu> menus = state.tabMenus.toList();\\n menus.removeWhere((e) => e.route == po.route);\\n ImageMenu menu = ImageMenu(po.logo, label: po.title, route: po.route);\\n emit(state.copyWith(tabMenus: [menu, ...menus]));\\n}\\n\\nGamePo? findGameById(String id) {\\n int index = state.games.indexWhere((e) => e.id == id);\\n if (index == -1) {\\n // TODO 根据 id 加载 GamePo\\n return null;\\n } else {\\n return state.games[index];\\n }\\n}\\n
\\n接下来只需要在游戏中心的点击事件在调用 openGame 方法维护页签数据,以及使用 context.go
进行路由跳转,就可以完成最基本的打开游戏页签功能:
另外,点击关闭按钮也是类似,通过 GameCenterBloc#closeGame
维护页签列表,移除对应的页签,并返回移除的页签索引:
----\x3e[维护状态数据]----\\nint closeGame(ImageMenu menu) {\\n List<ImageMenu> menus = state.tabMenus.toList();\\n int index = menus.indexWhere((e) => e.id == menu.id);\\n menus.removeAt(index);\\n emit(state.copyWith(tabMenus: menus));\\n return index;\\n}\\n
\\n在点击关闭按钮时触发 closeGame 方法,并且基于激活状态以及当前页签,通过 nextRoute 方法决定该进入的路由。
\\n比如关闭当前页签时,并且还有别的页签,可以激活前一个页签,如果已经是第一个,激活下一个:
----\x3e[维护路由跳转]----\\nvoid _closeGame(BuildContext context, ImageMenu menu, bool active) {\\n GameCenterBloc bloc = context.read<GameCenterBloc>();\\n int removedIndex = bloc.closeGame(menu);\\n List<ImageMenu> menus = bloc.state.tabMenus;\\n if (!active) return;\\n String nextRoute() {\\n if (menus.isEmpty) {\\n return AppRoute.gameCenter.url;\\n }\\n if (removedIndex == 0) {\\n return menus.first.route;\\n }\\n return menus[removedIndex - 1].route;\\n }\\n context.go(nextRoute());\\n}\\n
\\n目前虽然完成了游戏的打开和切换操作,但是切换游戏之后,游戏界面会销毁。下次进入时重新构建,这样游戏的状态就会丢失。如下所示,由扫雷切换到贪吃蛇后,在切回扫雷,状态就会重置:
\\n那该如何保持路由界面的状态呢。有两个思路:
\\n\\n\\n思路1: 让界面销毁,但让每个游戏都拥有存档的能力。
\\n
\\n但在关闭之前,提示用户确认关闭。关闭时存档,重新加载时通过数据恢复状态。
这样优势在于:
\\n这样劣势在于:
\\n\\n\\n思路2: 保持路由栈界面活性,实现关闭页签时不销毁游戏。
\\n
go_router 中有一种嵌套导航名为 StatefulShellRoute 的路由,可以保持局部导航路由的活性。其中每一个路由成为一个分支 branche
, 如下所示,gameBranches 方法通过游戏名称创建分支列表:
List<StatefulShellBranch> get gameBranches {\\n List<String> pages = [\\"center\\", ...gameWidgetMap.keys];\\n return pages.map((String name) {\\n Widget child = gameWidgetMap[name] ?? const GameCenter();\\n Page page = NoTransitionPage(child: child);\\n GoRoute route = GoRoute(path: \'game/$name\', pageBuilder: (_, __) => page);\\n return StatefulShellBranch(routes: [route]);\\n }).toList();\\n}\\n\\nStatefulShellRoute.indexedStack(\\n builder: (_, __, StatefulNavigationShell child) => GameCenterNavigation(child: child),\\n branches: gameBranches,\\n),\\n
\\n这样处理之后,在切换页签时,由于界面保持了活性,使用游戏状态就不会重置了:
\\n在编码过程中,我发现一个非常棘手的问题:
\\n\\n\\nStatefulShellRoute 只能保持活性,无法让主动销毁路由界面。
\\n
这会导致即使关闭页签,游戏组件依然存活在组件树中,这让人很难受。这一点在 go_router 的 issuse 中也有很多人提及,但目前没有支持 142258。本着精益求精的研究精神,我详细探索了一下 StatefulShellRoute 的实现原理。并且在源码中,窥见了关闭分支的可行性。于是我修改了一下 go_router 的源码,使之支持 closeBranch
操作:
---\x3e[StatefulNavigationShell]----\\nvoid closeBranch(String path){\\n route._shellStateKey.currentState?.closeBranch(path);\\n}\\n
\\n其原理是,打开的分支会被维护在 _branchState
映射中,在组件构建时会根据其中的内容构建导航视图。但 _branchState
维护在状态类内部,外界没有任何手段能访问并修改其中内容。所以我修改了一下源码,提供了 closeBranch
方法,可以根据传入的路径,找到对应的分支,然后移除。
void closeBranch(String route) {\\n RouteBase? matchRoute;\\n RouteMatchList matches = _router.configuration.findMatch(Uri.parse(route));\\n for(RouteMatchBase match in matches.matches){\\n if(match is ShellRouteMatch){\\n RouteMatchBase shellMatch = match.matches.first;\\n if(shellMatch is ShellRouteMatch){\\n matchRoute = shellMatch.matches.first.route;\\n }\\n }\\n }\\n StatefulShellBranch? target;\\n for (StatefulShellBranch branch in _branchState.keys) {\\n if (branch.routes.contains(matchRoute)) {\\n target = branch;\\n }\\n }\\n if(target !=null){\\n _branchState.remove(target);\\n }\\n}\\n
\\n此时就完成了令我满意的效果:如下所示,页签在打开时可以保存游戏活性,用户主动关闭页签时,游戏也从组件树中移除。这样游戏就有了一个合理的存活时期。
\\n不止是游戏,这种多页签打开的场景是非常广泛的,特别是在桌面端和 Web 平台,由于宽屏幕和性能很高,多页签就是司空见惯的事。比如浏览器的网页、AndroidStudio 打开的文件、桌面的文件管理器、cmd 命令行等都支持多个页签。后续还会继续优化 TolyGameBox 的功能,那本文就到这里,后续更多精彩内容,敬请期待 ~
\\n更多文章和视频知识资讯,大家可以关注我的公众号、掘金和 B 站 。
\\n","description":"Flutter&Flame 游戏开发系列前言: 该系列是 [张风捷特烈] 的 Flame 游戏开发教程。Flutter 作为 全平台 的 原生级 渲染框架,兼具 全端 跨平台和高性能的特点。目前官方对休闲游戏的宣传越来越多,以 Flame 游戏引擎为基础,Flutter 有游戏方向发展的前景。本系列教程旨在让更多的开发者了解 Flutter 游戏开发。\\n\\n第一季:30 篇短文章,快速了解 Flame 基础。[已完结] \\n 第二季:从休闲游戏实践,进阶 Flutter&Flame 游戏开发。\\n\\n两季知识是独立存在的,第二季 不需要 第一季作为基础…","guid":"https://juejin.cn/post/7484530047866814518","author":"张风捷特烈","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-23T23:02:19.492Z","media":[{"url":"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/54722ac795b141b99dffaba659f4cab0~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=2480&h=1486&s=115730&e=webp&b=2c0b06","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fc522a48490e4a688a6a1849f37e34a8~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1364&h=1011&s=246665&e=png&b=11111d","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a0a6cfcb48de4149be1da8a89e77e3ec~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1364&h=1011&s=253070&e=png&b=11111d","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ec983be8ca3f4fa09c55044e2bd4a14c~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1034&h=224&s=42722&e=png&b=ffffff","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bd35749427bc4ebc97fea7afab6f82d7~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1003&h=157&s=24297&e=png&b=ffffff","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0d37466f1d6f45be91cbdb04a57d57fb~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=957&h=708&s=273569&e=gif&f=58&b=141424","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0f489428e405473ca70b9b07e2076cfe~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=957&h=708&s=439508&e=gif&f=48&b=161626","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2c83d43d168a488e8814d5f243ef0390~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=957&h=708&s=231161&e=gif&f=74&b=161626","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/32a9632f819e47a79905a2f62949294d~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=957&h=708&s=372454&e=gif&f=42&b=151525","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0626f3e7b357497dab7386b2e560893c~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=957&h=708&s=520141&e=gif&f=104&b=151424","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","游戏开发"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 新一代状态管理框架 signals ,它究竟具备什么魔法和优势","url":"https://juejin.cn/post/7484589584719626279","content":"在上一篇《Riverpod 的注解模和发展方向》里就有很多人提到 signals ,对比 riverpod 部分人更喜欢 signals 的 “简单”和“直接”,那 signals 真的简单吗?再加上前段时间 signals 和 riverpod 的性能对比风波,也让大家更加关注 signals ,那它究竟有什么「魔力」?
\\nsignals.dart 有多“简单”?大概就是它的状态管理可以“简单”到甚至和 Flutter 没有关系,如下代码所示:
\\nsignal
创建一个信号对象computed
可以合并多个 signal
effect
可以监听响应数据变化import \'package:signals/signals.dart\';\\n\\nfinal name = signal(\\"N\\");\\nfinal surname = signal(\\"M\\");\\nfinal fullName = computed(() => name.value + \\"-\\" + surname.value);\\n\\n// Logs: \\"Jane Doe\\"\\neffect(() => print(fullName.value));\\n\\n// Updating one of its dependencies will automatically trigger\\n// the effect above, and will print \\"John Doe\\" to the console.\\nname.value = \\"D\\";\\n
\\n上述代码会先打印 N-M
,然后会打印 D-M
,因为在最后执行 name.value = \\"D\\";
时:
effect
里的函数会被调用,因为它内部有 fullName.value
,signals 内部会自动跟踪 fullName
的状态变化computed
会被调用,因为 computed
的 fullName.value
在 effect
内被访问,所以 name
的数值发生改变,从而让 computed
需要刷新状态是不是有点懵?这其实就是 signals 的 「魔法」,它的独特之处在于,它是「自动状态绑定」和「自动依赖跟踪」 :
\\n\\n\\n和其他传统的状态管理模型不同在于,signals 支持开发者精确地跟踪状态变化并仅更新依赖于这些变化的部分 UI ,就像上面的代码,「自动化」的实现看起来就像是「魔法」。
\\n
但是,事实上当你觉得某个框架是「魔法」时,那其实这个框架并不适合你使用,毕竟当遇到「咒语」失灵时,「魔法师」就很容易成为「脆皮的废物」,所以搞清楚 signals 的「魔法」实现原理尤为重要。
\\n开始解析在聊 signals.dart 之前,需要快速介绍 signals 的前置概念,附带还有 Preact、Preact Signals 、SolidJS 等关键词。
\\n首先需要说明一点,「Signals」 是业内通用的一种状态管理模式,而 signals.dart 项目就是 Preact Signals 的一个 Dart 移植版本,所以在最底层源码里你可以看到 Preact Signals 的核心原语,自然也就是包含了 Signal 的细粒度、惰性求值和自动依赖追踪等能力。
\\n那么 Preact、 Preact Signals 又是什么,还有一开始图片提到的「类似 solidjs 状态管理」,它们和 signals.dart 有什么关系?
\\n首先我们说过,「Signals」 是一种概念模式,它并不限制于任何语言还有框架,而在这个基础上:
\\n所以在 signals.dart 的源码和资料里都能看到它们的身影,而事实上 signals.dart 的实现就深受 Preact Signals 的影响,比如最底层的基础代码结构上:
\\n而对于 Signals 而言,它的主要优势在于更高校的颗粒度更新、自动化实现依赖跟踪、延迟计算等特点,其中我们最需要理解的,就是自动化实现依赖跟踪的「魔法」。
\\n要搞清楚「魔法」,首先我们需要知道 effect
是如何工作,如下代码所示,可以看到先打印输出了 N
,然后在 value
被改变的时候,又输出了 D
,那为什么在 name.value
改变的时候,effect 就会被调用呢?
这就不得不提,在 signals 里 .value
的 setter 和 getter 方法都是有特殊处理的,简单来说,就是当 value 被调用时,就会触发相应的逻辑,比如:「创建出对应的 Node
」,其实对于 signals 来说,内部 Node
是一个很重要的概念,因为它的实现基础,都是基于这个内部 Node
双链表来完成。
其实,在 signals.dart
中 Node
一直扮演着核心角色,它是自动跟踪依赖和管理状态结构的基础模块 ,比如 Node
类通过将 ReadonlySignal
(数据源)连接到对应的 Computed
和 Effect
等数据「消费者」来完成依赖:
class Node {\\n // 目标依赖的源。\\n final ReadonlySignal _source;\\n Node? _prevSource;\\n Node? _nextSource;\\n\\n // 依赖源并在源改变时应被通知的目标, 是消费者\\n final Listenable _target;\\n Node? _prevTarget;\\n Node? _nextTarget;\\n\\n // 目标上次看到的 _source 的版本号,使用版本号而不是存储源值,\\n // 因为源值可能占用任意大小的内存,并且计算可能会因为惰性求值而永远持有它们,\\n // 使用特殊值 -1 来标记可能未使用但可回收的节点。\\n int _version;\\n
\\n先聊它的抽象概念,本质上 Node
就是在 Signal
、Computed
和 Effect
等对象里被创建,并集成到一个双向链表中,当开始建立依赖关系时,比如在 Computed
/ Effect
访问 Signal
的值时,新的 Node
对象久会被创建,并添加到依赖项 (_prevSource
/_nextSource
) 和消费者 (_prevTarget
/_nextTarget
) 列表里。
也就是当你在 Computed
/ Effect
调用 .value
的 setter 和 getter 时,依赖追踪就会自动完成,从而创建一个新的 Node
,而后续的更新和触发执行,都是通过这个 Node
链表的遍历来完成。
\\n\\n所以
\\nNode
不仅仅是一个简单的数据结构,它通过将Signal
(数据源)连接到Computed
/Effect
消费者从而连接形成了一个图谱,其中一个节点的变化可以传播到其他节点,最终确保状态的一致更新。
所以在 signals 里,会利用 Node
对象来通知存储在 targets
列表中的所有依赖者 ,当信号的值发生改变时,会遍历依赖者列表,并根据 _version
对比结果来触发更新。
因为比对详细数据太过费时费力,通过 _version
来代表数据版本,不一致版本则更新,这样更有效率:
\\n\\n当
\\nSignal
的值被设置时,它的版本号会递增,当依赖的computed
/effect
运行时,它会记录其读取的每个Signal
的版本,在重新评估之前可以检查记录的版本是否更改,如果没有则可以跳过重新评估,从而节省资源。
所以在这些链表遍历时,_version
可以在值改变时更高效地通知依赖者。
是不是觉得有些抽象?没事,我们接下来通过源码来理解。
\\n首先, Effect
会使用 Node
对象来订阅其依赖的 Signal
,而首次 Effect
都会被立即运行,并在每次依赖项更改时被运行,那么这里有两个关键流程:
Effect
就自己执行一次Effect
内的 .value
的调用就完成了数据的跟踪绑定那么我们看 Effect
首先执行的时候经历了什么,通过源码可以知道, Effect
每次执行内部都会执行一个 start
函数,它其中一个关键的作用就是 evalContext = this
:
这里的 evalContext
其实就是 Computed
/ Effect
的抽象上下文,它代表的是当前的执行环境,它是存在于 global.dart
里的全局变量,决定当前执行的上下文环境, evalContext = this
大概意思就是 :
\\n\\nSignal 现在执行到当前这个
\\nEffect
了。
也就是当 Effect
被执行的时候, evalContext
就代表了当前的这个 Effect
,这就是 Effect
首次执行时的关键作用。
接下来就是 Effect
里的 .value
调用,让你调用 Signal
里 value 的 getter 时,其实内部就会对应调用 addDependency
给这个 Signal
添加依赖:
此时这个 Effect
就会创建出对应的 Node
,这个 Node
的 target 消费者 evalContext
正是当前 Effect
,可以看到,这就是自动跟踪的开始:
因为 Effect
首先被执行时,全局的 evalContext
会指向当前 Effect
,然后在 Effect
调用 .value
时,就会创建出 Effect
的对应 Node
,并添加到链表里。
\\n\\n所以自动跟踪的「魔法」,就在于 get value 里执行的依赖操作,通过读取当前执行环境
\\nevalContext
来判断需要依赖的位置。
那么,当我们执行 .value =xxx
的时候,同理就会触发 value 的执行 setter ,可以看到,此时相关 target ( Effect
) 就会被 notify
并最终执行 endBatch
:
notify
的作用就是把通过 batchedEffect
,把所有需要触发的 Effect
形成一个可访问链表,这里的头部 batchedEffect
也是一个全局对象:
而最终通过 endBatch
执行批处理,执行就会触发对应的 Effect
的 callback,进而再次执行到我们需要让他消费的地方,也就是 effect
里的函数因为 value 改变被再次执行:
这里有个叫 needsToRecompute
的函数,其实他就是分析数据源里面的所有 version
是否改变,如果有改变了,才执行 Effect
的 callback :
那么到这里,应该就可以简单理解 Effect
如何实现自动跟踪依赖和刷新:
evalContext
evalContext
实现自动依赖跟踪那么对于 Computed
来说也类似,不同的是 Computed 也是一个「特殊信号」,在获取它的 value 的时候同样会添加依赖,只是这里会有多一步 internalRefresh
操作:
internalRefresh
其实就是一个判断是否需要更新的过程,比如用到前面的 needsToRecompute
会分析所有依赖项的 source version ,从而判断是否需要更新,还有 evalContext = this
切换到当前执行环境:
所以可以看到,对于 Computed
来说,更新数据其实不是主动的,它是在 value 被 getter 的时候,才会执行刷新计算,也就是它其实是懒加载的。
比如,在下面 counter
的 value 被调用之前,每次 counter
变化时,其实并不会主动触发 computed
, 而是当 data.value
被调用到时,有数据改变才会触发 computed
的的执行:
final data = computed(() {\\n return counter.value + 12;\\n});\\n
\\n所以到这里, Computed
的「魔法」实现你也了解了吧?除了自动依赖跟踪,应该也理解了为什么 signals 可以做到「颗粒度控制」和「性能优化」了吧?那接下来我们继续聊 Flutter Signals 。
实际上,通过前面我们可以看出, signals 的状态管理可以说和 Flutter 没有「直接」关系,那它在 Flutter 上又是如何工作的?
\\n首先我们看下方代码,这是一个最简单的 Flutter 使用 signals 的例子,这里的核心就是 SignalsMixin
:
class _CounterExampleState extends State<CounterExample> with SignalsMixin {\\n late final Signal<int> counter = createSignal(0);\\n\\n void _incrementCounter() {\\n counter.value++;\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: const Text(\'Flutter Counter\'),\\n ),\\n body: Center(\\n child: Column(\\n mainAxisAlignment: MainAxisAlignment.center,\\n children: <Widget>[\\n const Text(\\n \'You have pushed the button this many times:\',\\n ),\\n Text(\\n \'$counter\',\\n style: Theme.of(context).textTheme.headlineMedium,\\n ),\\n ],\\n ),\\n ),\\n floatingActionButton: FloatingActionButton(\\n onPressed: _incrementCounter,\\n tooltip: \'Increment\',\\n child: const Icon(Icons.add),\\n ),\\n );\\n }\\n}\\n
\\n通过 SignalsMixin
,我们可以看到:
createSignal(0)
创建信号而不是 signal(0);
counter.value
,进而让 UI 更新是不是很简单?这里的关键点就是 createSignal(0)
,在 SignalsMixin
里调用 createSignal
的时候,内部会执行一个 _watch
操作,最终会在 _setup
的时候,在一个 effect 里订阅对应的 signal 的 value :
也就是说,当着 value
被改变时,它的 effect
就会被执行,从而触发 _rebuild
,进而执行 setState
更新控件。
也就是 createSignal
是通过 effect
来让 UI 更新,这就是 signals 在 Flutter 里的最基础用法,类似的还有 createEffect
、createComputed
等,如果你需要实现自动监听和释放的话,那么在 Flutter 里最好就是使用 SignalsMixin
的各种 createXXX 方法,因为这样就可以做甩手掌柜:
\\n\\n为什么这么说?如果我们直接用
\\neffect(() {xxx});
,其实我们是需要手动执行 dispose ,不然比如页面销毁时,effect
还会继续存在并且被执行。
另外 Flutter 还可以用的就是 signals 里的 Watch
控件,使用 Watch
就可以直接使用原始 signal
而不需要 createXXX :
final counter = signal(0);\\n\\nWatch.builder(builder: (context) {\\n return Text(\'$counter\');\\n});\\n
\\n其实 Watch
内部是利用了 createComputed
做依赖跟踪,你在 widget.builder
的使用的 signal 都会被自动依赖到 Computed
,因为 Watch
内部是 return result.value
,所以在每次变化时,Computed
都会重新刷新:
late final result = createComputed(() {\\n return widget.builder(context, widget.child);\\n }, debugLabel: widget.debugLabel);\\n\\n\\n\\n @override\\n Widget build(BuildContext context) {\\n return result.value;\\n }\\n
\\n另外还有 counter.watch(context)
方法,这个方法它会判断你是否存在 SignalsMixin
:
BuildContext
并将当前的 Element
注册为 Signal
的监听器而实际 watch
其实就是让 value
再变化时通过 subscribe
触发 rebuild
,另外这里它会使用 signal.peek()
来避免 value 调用时的 subscribing 监听。
而 peek()
之所以不会被跟踪依赖,其实就是在返回 value 之前,先临时清空了 evalContext
,也就是没有执行环境了:
同样道理的还有 batch
批处理,其实也就是将全局的 batchedEffect
临时处理为空,并且判断 batchDepth
等操作:
\\n\\n所以可以看到,其实 signals 虽然在 Flutter 还是会需要到 context ,但是依赖程度很低,并且支持程度也很多样,有通过 effect 的,也有通过 computed 的,当然最终触发 UI 变化的时候,本质还是要回归到 setState 。
\\n
举个例子,这里通过 signals 自己的 SignalProvider
实现将一个信号通过 InheritedWidget
往下共享,当然你可以也创建一个全局的 Signal ,这里展示的是:
listen: false
,所以不会主动更新counter.value ++
并不会触发 Flutter 本身 InheritedWidget
的更新,自然也就不会更新到 UIeffect
里是可以正常打印class _CounterExampleState extends State<CounterExample> with SignalsMixin {\\n\\n void _incrementCounter() {\\n final counter = SignalProvider.of<Counter>(context, listen: false)!;\\n counter.value ++;\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n final counter = SignalProvider.of<Counter>(context, listen: false)!;\\n effect(() {\\n /// Register to $id AsyncSignal\\n print(\'counter id: ${counter.value}\');\\n });\\n return Scaffold(\\n appBar: AppBar(\\n title: const Text(\'Flutter Counter\'),\\n ),\\n body: Center(\\n child: Column(\\n mainAxisAlignment: MainAxisAlignment.center,\\n children: <Widget>[\\n const Text(\\n \'You have pushed the button this many times:\',\\n ),\\n Text(\\n \'$counter\',\\n style: Theme.of(context).textTheme.headlineMedium,\\n ),\\n ],\\n ),\\n ),\\n floatingActionButton: FloatingActionButton(\\n onPressed: _incrementCounter,\\n tooltip: \'Increment\',\\n child: const Icon(Icons.add),\\n ),\\n );\\n }\\n}\\n
\\n从这里你也可以看到 signals 和 Flutter 之间的一个关系,signals 是一种数据跟踪和管理模式,而如何更新 Flutter UI ,就看你的颗粒度和使用需要,最方便的肯定是直接采用前面介绍的 API 。
\\n\\n\\n毕竟手动销毁还是挺“麻烦”的。
\\n
同时,针对 Flutter 上的支持,signals 也提供了 SignalProvider
用于需要实现往下共享 Signal 的场景,但是本身 Signal 就支持 context 无关定义,所以实际上不用 SignalProvider
也可以,毕竟 Signal 本身的颗粒度控制会比 InheritedWidget
更细腻。
另外, signals 也并不强求什么写什么顶层容器,甚至也不需要 InheritedWidget
的支持,它单纯就是依赖自己内部驱动的概念,不管是局部状态管理,还是全局状态管理,它都可以很灵活。
最后,signals 也提供了 DevTools 上的数据可视化结构,这其实也是现在状态管理框架的标配之一了:
\\n到这里我们就可以做个简单的总结了,在 signals 里最基础就是 Signal
、Computed
和 Effect
,它们的实现逻辑可以简单总结为:
Computed
/ Effect
运行时会通过全局 evalContext
标注当前运行环境Signal
的 value 对 getter 和 setter 有特殊处理,一般 getter 会根据 evalContext
自动添加依赖,而 setter 会刷新数据 version
并更新所有依赖 Effect
Computed
是一种特殊信号,它的懒加载决定了它只有在 value 被调用时才会触发刷新计算peek
和 batched
其实都是对全局环境变量的临时清空操作version
作为判断数据版本的主要依据所以, 当 Computed
/ Effect
函数运行时, 可以做到追踪在函数中访问 value 的任何信号变化,对于每个被访问的信号,都会创建一个新的 Node
对象(或者重用现有的对象),从而将信号链接到当前的 Computed
/ Effect
,Node
会被添加到 Computed
/ Effect
的依赖项列表和信号的依赖者列表中。
这种自动订阅机制就是 signals 的关键「魔法」,通过消除手动声明依赖项的需求,简化了状态管理,甚至在 Flutter 可以一定程度”脱离“ Context 实现状态更新的实现原理。
\\n那么,你会选择 signals.dart 吗?
","description":"在上一篇《Riverpod 的注解模和发展方向》里就有很多人提到 signals ,对比 riverpod 部分人更喜欢 signals 的 “简单”和“直接”,那 signals 真的简单吗?再加上前段时间 signals 和 riverpod 的性能对比风波,也让大家更加关注 signals ,那它究竟有什么「魔力」? 开始\\n\\nsignals.dart 有多“简单”?大概就是它的状态管理可以“简单”到甚至和 Flutter 没有关系,如下代码所示:\\n\\n通过 signal 创建一个信号对象\\n通过 computed 可以合并多个 signal\\n通过…","guid":"https://juejin.cn/post/7484589584719626279","author":"恋猫de小郭","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-23T22:16:30.999Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/538df9d1cccd462fbec2322e0cdd546c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1743372989&x-signature=SXxbtqhdx%2F2B%2Fq478rGqAPpmo00%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c6bdb06480644744a7d89836d368649b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1743372989&x-signature=NEdLSeVJnxhnAOH8D8B%2FOVVEtIo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5b043467e48942f69fa28ef21405de4b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1743372989&x-signature=oNx3FfRwbo%2Bfh2U760cOIFmzlYg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b0ae7c44d59e48259b31f9add59e26e0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1743372989&x-signature=%2Bl41pjmLyw%2BNY%2F8L0DPm0jPdbpo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a80367e8f0ea480aa7ba178ef0b2360e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1743372989&x-signature=j3tPL5%2FbjR83cthsxoB8Qzd7m8c%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e65f5ec0019e4fe583070ca7ea2005fc~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1743372989&x-signature=vNsipyuAgL%2Bz5lyC4LdkVmVttrQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/01151d56f7ee4e18a30091b66d1c0f9d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1743372989&x-signature=ZgWezEE8lr4oC0tM7nlfEexS0UU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a890d5525507470ab71a99a43b414e2e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1743372989&x-signature=ow8RJDLs121tDKmcHYTad5gVgqA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4a6612963d6c42c791db1ad3f0293366~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1743372989&x-signature=RSV79T%2BQWn9Mf2PZJytHyBU1Wlc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9313fb00c62a44d7b764b114e9297d60~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1743372989&x-signature=lOyxVid64dSvzy3849b7K9zRZgE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1ce9f1cba394431ca1ff4d0e20a3c135~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1743372989&x-signature=FPJAy4pzh0yfpy1E1CdCbHy0K6U%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/66097f1cf61540cabf3368f8a4b7b240~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1743372989&x-signature=Ccft4QoQM62YlE6hcpI4Xs1mFdE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a0b1591f14bf49cc9db7404396b7551a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1743372989&x-signature=2j3udtjkZWd1shNs1bRVEfQmuEQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6e563993c96c4faa99b64506ae2273a0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1743372989&x-signature=pvMp5flt1h0vycr7TUhrfbs95b0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/10deda9463d04e8797d9225f14ccfdf6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1743372989&x-signature=pjxc0yWk098y6ehGulWvKeMzXSU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e20c4b37120045daaddae6d0c43cbf7e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1743372989&x-signature=rMTBE6izV4%2Fgzz0rEv%2B4fozso1g%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d60778c3e4a14c8c927f6bd13e0f6626~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1743372989&x-signature=Jm7LswjFAIE%2Bq4paDDCg9evuSQ4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fde1418b1ca141d880bf7712040dd68f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1743372989&x-signature=urDGyNnT2Buu5byBRBRjgvj3IQA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/88e0268001ee43309b059215c9fc8dd4~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1743372989&x-signature=Y8UVobc%2BUffRUcVk7sGE%2FUd8YNU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1c763d70da2542c0bcbda784ba108a8e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1743372989&x-signature=MK%2F6Vcmxkp%2BMKp14LxyXQYQrQ%2F4%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter使用自签证书打包ipa","url":"https://juejin.cn/post/7484470326082175011","content":"在 Flutter 中使用自签证书打包 IPA 文件,可以通过以下步骤完成:
\\n生成自签证书:
\\n导出证书:
\\n.p12
文件。使用 AppUploader 生成证书 www.applicationloader.net/
\\n打开 Xcode 项目:
\\nflutter create .\\n
\\nios/Runner.xcworkspace
文件。配置签名信息:
\\nRunner
项目,然后选择 Signing & Capabilities
选项卡。Team
下拉菜单中选择了一个有效的开发团队。Manual Signing
,并手动选择生成的自签证书。更新 Info.plist
:
ios/Runner/Info.plist
文件中,确保 Bundle Identifier
与证书中的标识符一致。使用 Flutter CLI 构建:
\\nflutter build ios --release --no-codesign\\n// 或者生成 ipa\\nflutter build ipa\\n
\\n这会生成一个未签名的 .app
文件。使用第三方工具签名:
\\n.app
文件进行签名:\\n./iOSAppSigner -i ios/Flutter/Release/Runner.app -o build/Runner.ipa -p /path/to/your/certificate.p12 -x /path/to/your/provisioning/profile\\n
\\n其中:\\n-i
指定未签名的 .app
文件路径。-o
指定输出的 .ipa
文件路径。-p
指定证书文件路径。-x
指定配置文件路径。\\n测试 IPA 文件:
\\n.ipa
文件通过爱思助手安装到测试设备上,确保应用能够正常运行。分发 IPA 文件:
\\n.ipa
文件。通过上述步骤,你可以使用自签证书为 Flutter 项目打包 IPA 文件。
","description":"在 Flutter 中使用自签证书打包 IPA 文件,可以通过以下步骤完成: 1. 准备自签证书\\n方式一\\n\\n生成自签证书:\\n\\n打开 钥匙串访问 应用。\\n选择 证书助理 > 创建证书。\\n按照提示填写证书信息,选择证书类型为 代码签名,并保存证书。\\n\\n导出证书:\\n\\n在 钥匙串访问 中找到生成的证书。\\n右键选择 导出,保存为 .p12 文件。\\n方式二\\n\\n使用 AppUploader 生成证书 www.applicationloader.net/\\n\\n安装 AppUploader\\n生成证书和描述文件 描述文件默认7天超时\\n2. 配置 Flutter…","guid":"https://juejin.cn/post/7484470326082175011","author":"taokexia","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-23T12:52:37.996Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/020367588f2744f086416840cbddd7af~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgdGFva2V4aWE=:q75.awebp?rk3s=f64ab15b&x-expires=1743339157&x-signature=qxVi0C%2FHueIS0HRvjbfoxWgkLPM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/119bef64e8114ce8ad395cb831995c61~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgdGFva2V4aWE=:q75.awebp?rk3s=f64ab15b&x-expires=1743339157&x-signature=o5FscKgaFDlCxoSJtZyF3p2ARS8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7146a282d0134aba9361ca847c290b90~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgdGFva2V4aWE=:q75.awebp?rk3s=f64ab15b&x-expires=1743339157&x-signature=qVGe87rCqmTNYUp072oF7Vkbq1M%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["iOS","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 中自定义Magnifier组件","url":"https://juejin.cn/post/7484468071990968347","content":"在日常开发中,我们经常会遇到需要局部放大内容的场景,比如文本编辑、图片查看或者其他需要精细操作的界面。本文将带你一步步实现一个自定义放大镜组件,并介绍如何在应用中使用它。
\\n1. 为什么要自定义放大镜组件
\\n默认的 Flutter 组件库中并没有直接提供“放大镜”这种交互效果,因此我们需要通过自定义组件来实现:
\\n• 局部放大效果:当用户拖动手指时,放大镜显示手指下的内容。
\\n• 交互流畅:实时更新放大镜位置,保证用户体验。
\\n• 高度定制化:可以自定义放大倍数、放大镜半径以及边框样式等。
\\n2. 组件实现原理
\\n我们将通过以下几个关键步骤来构建这个放大镜组件:
\\n• 布局结构
\\n组件使用 Stack 叠加方式,将底层的内容和放大镜叠加显示。
\\n• 手势识别
\\n使用 GestureDetector 监听用户的拖动操作,并更新放大镜的位置。
\\n• 放大效果
\\n利用 Transform.scale 对放大镜内的内容进行放大,再通过 ClipOval 裁剪成圆形效果。
\\n• 定位调整
\\n通过 Transform.translate 对放大镜进行位置调整,使得放大镜显示在用户拖动的正确位置。
\\n3. 代码实现
\\n下面的代码展示了一个完整的自定义放大镜组件 Magnifier,以及如何将它包裹在整个应用中。该组件接受以下参数:
\\n• child:放大镜背后的内容。
\\n• magnification:放大倍数,默认为 2.0。
\\n• radius:放大镜半径,默认为 50.0。
\\n• border:放大镜边框,可自定义样式。
\\nMagnifier 组件代码
\\nimport \'package:flutter/material.dart\';\\n\\nclass Magnifier extends StatefulWidget {\\n final Widget child;\\n final double magnification;\\n final double radius;\\n final Border? border;\\n\\n const Magnifier({\\n super.key,\\n required this.child,\\n this.magnification = 2.0,\\n this.radius = 50.0,\\n this.border,\\n });\\n\\n @override\\n _MagnifierState createState() => _MagnifierState();\\n}\\n\\nclass _MagnifierState extends State<Magnifier> {\\n Offset? _offset;\\n double _magnifierSize = 0.0;\\n\\n @override\\n void initState() {\\n super.initState();\\n // 为了避免在 initState 中直接调用 MediaQuery.of(context)\\n // 我们使用 Future.microtask 来延迟初始化\\n Future.microtask(() {\\n if (mounted) {\\n final screenSize = MediaQuery.of(context).size;\\n setState(() {\\n _offset = Offset(screenSize.width / 2, screenSize.height / 2);\\n _magnifierSize = widget.radius * 2;\\n });\\n }\\n });\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Stack(\\n alignment: Alignment.center,\\n children: [\\n // 放大镜背后的内容\\n widget.child,\\n // 监听手势,并将放大镜定位到用户拖动的位置\\n Positioned.fill(\\n child: LayoutBuilder(\\n builder: (BuildContext context, BoxConstraints constraints) {\\n final childSize = constraints.biggest;\\n return GestureDetector(\\n onPanUpdate: (details) {\\n setState(() {\\n _offset = details.localPosition;\\n });\\n },\\n child: Transform.translate(\\n offset: Offset(_offset!.dx - _magnifierSize / 2,\\n _offset!.dy - _magnifierSize / 2),\\n child: Align(\\n alignment: Alignment.topLeft,\\n child: Container(\\n width: _magnifierSize,\\n height: _magnifierSize,\\n decoration: BoxDecoration(\\n border: widget.border ??\\n Border.all(\\n color: Theme.of(context).colorScheme.primary,\\n width: 2.0,\\n ),\\n shape: BoxShape.circle,\\n ),\\n child: ClipOval(\\n child: Transform.scale(\\n scale: widget.magnification,\\n child: Transform.translate(\\n offset: Offset(\\n childSize.width / 2 - _offset!.dx,\\n childSize.height / 2 - _offset!.dy,\\n ),\\n child: OverflowBox(\\n minWidth: childSize.width,\\n minHeight: childSize.height,\\n maxWidth: childSize.width,\\n maxHeight: childSize.height,\\n child: widget.child,\\n ),\\n ),\\n ),\\n ),\\n ),\\n ),\\n ),\\n );\\n },\\n ),\\n ),\\n ],\\n );\\n }\\n}\\n
\\n使用 Magnifier 组件
\\n在主函数中,我们将 Magnifier 组件作为应用的根组件,包裹整个 MaterialApp,从而实现全局的放大镜效果。
\\nimport \'package:flutter/material.dart\' hide Magnifier;\\nimport \'package:flutter_tests/widgets/magnifier.dart\';\\n\\nvoid main() {\\n runApp(const MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n const MyApp({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n return Magnifier(\\n magnification: 2.0,\\n radius: 50.0,\\n child: MaterialApp(\\n title: \'Flutter Demo\',\\n theme: ThemeData(\\n colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),\\n useMaterial3: true,\\n ),\\n home: const MyHomePage(title: \'Flutter Demo Home Page\'),\\n ),\\n );\\n }\\n}\\n\\nclass MyHomePage extends StatefulWidget {\\n const MyHomePage({super.key, required this.title});\\n final String title;\\n\\n @override\\n State<MyHomePage> createState() => _MyHomePageState();\\n}\\n\\nclass _MyHomePageState extends State<MyHomePage> {\\n int _counter = 0;\\n\\n void _incrementCounter() {\\n setState(() {\\n _counter++;\\n });\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n backgroundColor: Theme.of(context).colorScheme.inversePrimary,\\n title: Text(widget.title),\\n ),\\n body: Center(\\n child: Column(\\n mainAxisAlignment: MainAxisAlignment.center,\\n children: <Widget>[\\n const Text(\'You have pushed the button this many times:\'),\\n Text(\\n \'$_counter\',\\n style: Theme.of(context).textTheme.headlineMedium,\\n ),\\n ],\\n ),\\n ),\\n floatingActionButton: FloatingActionButton(\\n onPressed: _incrementCounter,\\n tooltip: \'Increment\',\\n child: const Icon(Icons.add),\\n ),\\n );\\n }\\n}\\n
\\n4. 代码解析
\\n• 初始化阶段
\\n在 initState 中,由于直接调用 MediaQuery.of(context) 会导致异常,我们通过 Future.microtask 延迟执行获取屏幕尺寸。这样可以安全地初始化放大镜的初始位置为屏幕中心,并计算放大镜的尺寸。
\\n• 手势响应
\\n利用 GestureDetector 的 onPanUpdate 事件,实时更新 _offset 的值,使得放大镜可以随着用户拖动而移动。
\\n• 放大效果
\\n使用 Transform.scale 对内容进行缩放,再结合 Transform.translate 调整内容的偏移,从而实现中心区域的放大效果。利用 ClipOval 裁剪成圆形,模拟出真实放大镜的外观。
\\n• 布局管理
\\n通过 Stack 和 Positioned 的组合,将放大镜叠加在底层内容之上,同时使用 LayoutBuilder 获取子组件的尺寸,保证放大镜效果的正确渲染。
\\n5. 效果展示
\\n6. 总结
\\n本文详细介绍了如何在 Flutter 中自定义一个放大镜组件,从需求分析、设计思路、到具体实现和使用。通过合理的布局、手势识别以及变换组件,我们不仅实现了一个具有交互性和视觉效果的放大镜,同时也展示了 Flutter 中灵活强大的组件组合能力。希望这篇文章能够为你在实际项目中实现类似效果提供参考和帮助。
","description":"在日常开发中,我们经常会遇到需要局部放大内容的场景,比如文本编辑、图片查看或者其他需要精细操作的界面。本文将带你一步步实现一个自定义放大镜组件,并介绍如何在应用中使用它。 1. 为什么要自定义放大镜组件\\n\\n默认的 Flutter 组件库中并没有直接提供“放大镜”这种交互效果,因此我们需要通过自定义组件来实现:\\n\\n• 局部放大效果:当用户拖动手指时,放大镜显示手指下的内容。\\n\\n• 交互流畅:实时更新放大镜位置,保证用户体验。\\n\\n• 高度定制化:可以自定义放大倍数、放大镜半径以及边框样式等。\\n\\n2. 组件实现原理\\n\\n我们将通过以下几个关键步骤来构建这个放大镜组件:…","guid":"https://juejin.cn/post/7484468071990968347","author":"MaoJiu","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-23T06:36:09.292Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2acb6745543444a29f7dd1a3b42a12af~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTWFvSml1:q75.awebp?rk3s=f64ab15b&x-expires=1743316568&x-signature=Hbv1pbCN23%2FvhyCxYlF%2B5JqaW10%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"3分钟实现git托管软件安装包,并实现版本检测和更新功能","url":"https://juejin.cn/post/7484202915389784116","content":"git 里面的json文件能直接读取
吗?我以 gitee 为例测试了一下,发现并没有问题,用户能直接通过链接访问到git托管的json的文件内容
。gitee没问题,github等其它代码托管平台应该也没问题,我是懒得测其它平台,有兴趣的可以自己一下。gitee 获取托管的json文件的连接
\\n有了版本信息接口和软件地址,只需要对比本地软件版本和git托管的版本是否一致,即可实现软件更新功能。
\\n因为壁纸软件是用flutter写的,就以flutter举例,其它像 uniapp、electron 等等都大差不差。
\\n下载函数,我这用的是自己封装过的Dio发起的网络请求,你可以直接用Dio或者http。
\\nFuture<Response> updateApp() => DioInstance.instance().get(\\n path:\\n \'https://gitee.com/zsnoin-can/new-wall-paper-apk/raw/master/version.json\',\\n param: {\\n \'random\': Random().nextDouble(),\\n },\\n );\\n
\\n封装更新工具 /tools/update_apk.dart,添加 displayTips 控制没有检测到新版本时是否提示用户。
\\nimport \'package:bot_toast/bot_toast.dart\';\\nimport \'package:dio/dio.dart\';\\nimport \'package:flutter/material.dart\';\\nimport \'package:package_info_plus/package_info_plus.dart\';\\nimport \'package:url_launcher/url_launcher.dart\';\\nimport \'package:wallpaper/api/foot.dart\';\\nimport \'package:wallpaper/components/button/normal_button.dart\';\\nimport \'package:wallpaper/generated/l10n.dart\';\\n\\nclass UpdateApk {\\n String updateUrl =\\n \'https://gitee.com/zsnoin-can/new-wall-paper-apk/blob/master/wallpaper.apk\';\\n\\n Future updateApk(BuildContext context, {bool displayTips = false}) async {\\n Response res = await updateApp();\\n PackageInfo packageInfo = await PackageInfo.fromPlatform();\\n String version = packageInfo.version; // 获取版本号\\n if (version != res.data[\'version\']) {\\n showDialog(\\n context: context,\\n builder: (BuildContext context) {\\n return AlertDialog(\\n title: Row(\\n crossAxisAlignment: CrossAxisAlignment.end,\\n spacing: 5,\\n children: [\\n Icon(Icons.update, color: Colors.deepOrangeAccent),\\n Text(\\n S.of(context).s15,\\n style: TextStyle(\\n fontSize: 18,\\n ),\\n ),\\n Text(\\n \'${res.data[\'version\']}\',\\n style: TextStyle(\\n color: Theme.of(context).colorScheme.primary,\\n fontSize: 16,\\n ),\\n ),\\n ],\\n ),\\n shape: RoundedRectangleBorder(\\n borderRadius: BorderRadius.all(Radius.circular(5)),\\n ),\\n content: SizedBox(\\n height: 100,\\n child: Padding(\\n padding: const EdgeInsets.only(left: 10, right: 10),\\n child: ListView.builder(\\n itemCount: res.data[\'update\'].length,\\n itemBuilder: (context, index) {\\n return Text(\\n \\"${index + 1}、${res.data[\'update\'][index]}\\");\\n }),\\n ),\\n ),\\n actions: [\\n NormalButton(\\n radius: 5,\\n width: 100,\\n height: 30,\\n fontSize: 16,\\n title: S.of(context).b7,\\n bgColor: const Color.fromARGB(131, 158, 158, 158),\\n onPressed: () {\\n Navigator.pop(context);\\n }),\\n NormalButton(\\n radius: 5,\\n width: 100,\\n height: 30,\\n fontSize: 16,\\n title: S.of(context).s16,\\n onPressed: () async {\\n // 打开浏览器更新\\n if (await canLaunchUrl(Uri.parse(updateUrl))) {\\n await launchUrl(Uri.parse(updateUrl));\\n } else {\\n // 如果无法打开链接,显示错误提示\\n ScaffoldMessenger.of(context).showSnackBar(\\n SnackBar(content: Text(\'url error: $updateUrl\')),\\n );\\n }\\n Navigator.pop(context);\\n }),\\n ],\\n );\\n });\\n }\\n if (version == res.data[\'version\'] && displayTips) {\\n BotToast.showText(text: S.of(context).s17);\\n }\\n }\\n}\\n\\n
\\n用法:
\\nUpdateApk().updateApk(context);
,例如,用户进入软件时自动检测更新,未检测到时无需提醒。UpdateApk().updateApk(context, displayTips: true);
,例如,用户在设置中手动点击了检测更新按钮,未检测到新版本时需要给用户正反馈。效果图:
\\n这是开发者编写的代码,包括页面、业务逻辑和状态管理。就像是一栋建筑的设计图纸,定义了应用的外观和行为。
\\n这是Flutter的\\"大脑\\",完全用Dart语言编写:
\\n\\n\\n💡 形象比喻:这些就像是建筑预制件,Material是现代风格,Cupertino是简约风格,而Widgets是两者共用的基础构件。
\\n
\\n\\n💡 形象比喻:如果UI组件是演员,那么这一层就是舞台导演,决定演员站在哪里(Rendering),如何移动(Animation),以及如何响应观众(Gestures)。
\\n
提供最基础的工具类和函数,如集合、IO操作、异步支持等。这是整个框架的地基。
\\n这是Flutter的\\"心脏\\",用C++编写,提供高性能渲染支持:
\\n\\n\\n💡 形象比喻:如果Framework是建筑师团队,那么Engine就是实际建造大楼的工程机械。Skia是绘图机器,而Dart VM是控制中心。
\\n
工作流程:
\\n\\n\\n🔑 关键理解:Flutter是\\"自上而下\\"的渲染系统,不依赖原生UI组件,而是自己控制每个像素。这就像是艺术家从空白画布开始作画,而不是拼接现成的图片。
\\n
graph TD\\n subgraph \\"开发者层\\"\\n DevCode[\\"开发者代码 (build方法)\\"]\\n end\\n \\n subgraph \\"渲染流水线\\"\\n WidgetTree[\\"Widget树 (配置/描述)\\"]\\n ElementTree[\\"Element树 (结构/管理)\\"]\\n RenderTree[\\"RenderObject树 (布局/绘制)\\"]\\n LayerTree[\\"Layer树 (合成)\\"]\\n VSyncSignal[\\"VSync信号\\"]\\n \\n WidgetTree --\x3e |\\"createElement()\\"| ElementTree\\n ElementTree --\x3e |\\"createRenderObject()\\"| RenderTree\\n RenderTree --\x3e |\\"layout()/paint()\\"| LayerTree\\n VSyncSignal --\x3e |\\"触发帧渲染\\"| PipelineOwner\\n end\\n \\n subgraph \\"Framework层 (Dart)\\"\\n subgraph \\"UI组件库\\"\\n Widgets[\\"基础Widget\\"] \\n Material[\\"Material组件\\"]\\n Cupertino[\\"Cupertino组件\\"]\\n \\n Material & Cupertino --\x3e |\\"继承/组合\\"| Widgets\\n end\\n \\n subgraph \\"渲染引擎\\"\\n RenderingEngine[\\"Rendering引擎\\"]\\n PipelineOwner[\\"PipelineOwner\\"]\\n \\n RenderingEngine --\x3e |\\"管理渲染过程\\"| PipelineOwner\\n PipelineOwner --\x3e |\\"调度layout/paint\\"| RenderTree\\n end\\n \\n subgraph \\"系统服务\\"\\n Gestures[\\"手势识别\\"]\\n Animation[\\"动画系统\\"]\\n Scheduler[\\"Scheduler\\"]\\n \\n Gestures --\x3e |\\"产生事件\\"| DevCode\\n Animation --\x3e |\\"驱动状态变化\\"| DevCode\\n Scheduler --\x3e |\\"调度帧\\"| VSyncSignal\\n end\\n \\n Foundation[\\"Foundation (核心工具类)\\"]\\n end\\n \\n subgraph \\"Engine层 (C++)\\"\\n FlutterEngine[\\"Flutter Engine\\"]\\n Skia[\\"Skia图形引擎\\"]\\n Compositor[\\"Compositor (合成器)\\"]\\n DartVM[\\"Dart VM\\"]\\n PlatformChannels[\\"平台通道\\"]\\n \\n FlutterEngine --\x3e |\\"管理\\"| Skia\\n FlutterEngine --\x3e |\\"管理\\"| DartVM\\n LayerTree --\x3e |\\"scene.build()\\"| Compositor\\n Compositor --\x3e |\\"栅格化\\"| Skia\\n DartVM --\x3e |\\"执行\\"| DevCode\\n FlutterEngine <--\x3e |\\"平台通信\\"| PlatformChannels\\n end\\n \\n subgraph \\"平台层\\"\\n GPU[\\"GPU\\"]\\n NativeAPIs[\\"原生平台API\\"]\\n Screen[\\"屏幕\\"]\\n \\n Skia --\x3e |\\"OpenGL/Vulkan/Metal指令\\"| GPU\\n PlatformChannels <--\x3e |\\"方法调用/事件\\"| NativeAPIs\\n GPU --\x3e |\\"帧缓冲区\\"| Screen\\n end\\n \\n DevCode --\x3e |\\"创建\\"| WidgetTree\\n \\n %% 关键路径高亮\\n linkStyle 0,1,2,3,7,12,13,16 stroke:#f66,stroke-width:2.5px;\\n \\n %% 美化样式\\n style DevCode fill:#f9f,stroke:#333,stroke-width:2px\\n style WidgetTree fill:#bbf,stroke:#333,stroke-width:2px\\n style ElementTree fill:#bbf,stroke:#333,stroke-width:2px\\n style RenderTree fill:#bbf,stroke:#333,stroke-width:2px\\n style LayerTree fill:#bbf,stroke:#333,stroke-width:2px\\n style Skia fill:#fdd,stroke:#333,stroke-width:2px\\n style GPU fill:#dfd,stroke:#333,stroke-width:2px\\n style Screen fill:#dfd,stroke:#333,stroke-width:2px\\n
\\nFlutter的渲染过程是一个精心设计的多阶段流水线,涉及多个树结构的转换和处理:
\\nWidget树:纯粹的配置信息,描述UI应该是什么样子的不可变对象。当setState()
被调用时,这棵树会被重建。
Element树:Widget树和RenderObject树之间的中间层,维护UI结构并管理Widget与RenderObject的关联。Element具有生命周期,能够高效地比较和更新,避免不必要的重建。
\\nRenderObject树:实际负责布局计算(layout()
)和绘制(paint()
)的对象树。RenderObject包含复杂的几何信息和绘制指令。
构建阶段 (build
):当状态变化时,开发者的build
方法执行,创建新的Widget树
协调阶段 (reconciliation
):
布局阶段 (layout
):
绘制阶段 (paint
):
合成阶段 (compositing
):
栅格化 (rasterization
):
VSync同步:Flutter使用VSync信号触发帧渲染,确保与设备刷新率同步(通常是60FPS)
\\nPipelineOwner:框架内部的渲染管道控制器,协调布局和绘制过程
\\nDirty标记机制:只有被标记为\\"dirty\\"的RenderObject才会重新布局和绘制,提高效率
\\n离屏渲染:复杂效果(如阴影、模糊)通过离屏渲染实现,可能影响性能
\\n渲染优化:
\\n这种精心设计的渲染流水线使Flutter能够在保持高性能的同时,提供跨平台一致的像素级渲染控制。理解这一流程对优化Flutter应用性能至关重要。
\\n分层架构设计:
\\n┌───────────────────────────────────────┐\\n│ 你的Flutter应用 │ \\n├───────────────────────────────────────┤\\n│ Framework层 │\\n│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │\\n│ │Material │ │Cupertino│ │ Widgets │ │\\n│ └─────────┘ └─────────┘ └─────────┘ │\\n│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │\\n│ │Rendering│ │Animation│ │ Gestures│ │\\n│ └─────────┘ └─────────┘ └─────────┘ │\\n│ ┌─────────────────────────────────┐ │\\n│ │ Foundation │ │\\n│ └─────────────────────────────────┘ │\\n├───────────────────────────────────────┤\\n│ Engine层 │\\n│ ┌─────────────────────────────────┐ │\\n│ │ Skia │ │\\n│ └─────────────────────────────────┘ │\\n│ ┌─────────────────────────────────┐ │\\n│ │ Dart VM │ │\\n│ └─────────────────────────────────┘ │\\n└───────────────────────────────────────┘\\n
\\n关键特点:
\\n特性 | Flutter | Jetpack Compose |
---|---|---|
渲染方式 | 自绘引擎 | 编译为原生View |
跨平台 | 全平台 | 主要针对Android |
语言 | Dart | Kotlin |
状态管理 | setState/Provider/Bloc等 | MutableState/StateFlow |
组件模型 | Widget树 | Composable函数树 |
重建机制 | 元素树比较 | 智能重组 |
编程范式 | 声明式+面向对象 | 声明式+函数式 |
\\n\\n💡 形象比喻:Compose像是一位精通Android的本地厨师,使用Android原料烹饪美食;而Flutter像是一位带着所有食材和工具的国际厨师,无论在哪里都能做出相同口味的菜肴。
\\n
1. 渲染原理
\\n2. 生命周期管理
\\n3. 布局系统
\\nFlutter的UI渲染过程确实使用了多种数据结构,从高层的树结构到底层的GPU命令缓冲区。让我专业而全面地解析这个过程:
\\nFlutter渲染管道中使用了四种主要树形数据结构:
\\n这些树形结构在概念上是清晰的,但在技术实现上通过引用关系而非显式的树数据结构连接起来。
\\n当Layer树需要实际呈现到屏幕上时,Flutter执行以下数据结构转换:
\\nLayer树 → Scene → 绘图命令 → GPU指令 → 帧缓冲区\\n(树形) (对象) (命令列表) (缓冲区) (像素数组)\\n
\\n详细过程:
\\nLayer → Scene
\\nScene
对象scene.build()
方法将Layer树序列化为Skia引擎能理解的形式Scene → 绘图命令
\\nSkPicture
或类似的命令记录数据结构中绘图命令 → GPU指令
\\nGPU指令 → 帧缓冲区
\\nFlutter的UI渲染系统最初使用树形数据结构组织UI,随后在绘制阶段转换为更专业、更高效的图形API数据结构:
\\n这种数据结构转换策略使Flutter能够结合声明式UI的简洁性与图形渲染的高性能,同时保持跨平台一致性。
\\n\\n\\n关键洞见:Flutter通过多级数据结构转换,巧妙地平衡了开发效率和渲染性能,这是其区别于其他框架的核心技术优势。
\\n
在Flutter中,Widget是UI的基本构建块。与Compose中的@Composable函数类似,Widget描述UI应该如何呈现。
\\n\\n\\n💡 形象理解:如果把UI比作一栋建筑,Widget就像是建筑蓝图,告诉系统如何构建这个UI。而实际的\\"建筑\\"是Element树,由Flutter引擎负责\\"施工\\"。
\\n
Flutter的三棵树:
\\n Widget树 Element树 RenderObject树\\n(配置/描述/蓝图) (结构/实例) (渲染/绘制)\\n┌───────────┐ ┌───────────┐ ┌───────────┐\\n│ MyWidget │──生成──→ │MyElement │──生成──→ │RenderObj │\\n└───────────┘ └───────────┘ └───────────┘\\n
\\nStatelessWidget:纯函数式组件,输入确定则输出确定
\\nclass GreetingCard extends StatelessWidget {\\n final String name;\\n \\n const GreetingCard({Key? key, required this.name}) : super(key: key);\\n \\n @override\\n Widget build(BuildContext context) {\\n return Container(\\n padding: const EdgeInsets.all(16.0),\\n child: Text(\'Hello, $name!\'),\\n );\\n }\\n}\\n
\\nStatefulWidget:包含可变状态的组件
\\nclass Counter extends StatefulWidget {\\n const Counter({Key? key}) : super(key: key);\\n \\n @override\\n _CounterState createState() => _CounterState();\\n}\\n\\nclass _CounterState extends State<Counter> {\\n int _count = 0;\\n \\n void _increment() {\\n setState(() {\\n _count++;\\n });\\n }\\n \\n @override\\n Widget build(BuildContext context) {\\n return Column(\\n children: [\\n Text(\'Count: $_count\'),\\n ElevatedButton(\\n onPressed: _increment,\\n child: const Text(\'Increment\'),\\n ),\\n ],\\n );\\n }\\n}\\n
\\nconst GreetingCard({Key? key, required this.name}) : super(key: key);\\n
\\n这行代码是Flutter Widget构造函数的典型声明,包含了多个重要概念:
\\nconst
关键字
GreetingCard
花括号参数列表{Key? key, required this.name}
{}
- 表示这些是命名参数,调用时需指定参数名Key? key
- 可空的Key参数,用于Widget唯一标识required this.name
- 必需提供的参数,同时声明并初始化类成员变量初始化列表super(key: key)
:
后的代码在构造函数体之前执行分号;
结束
this.name
语法的魔力这是Dart的简写语法,一次性完成两个操作:
\\nname
this.name
没有这个语法,你需要写:
\\nconst GreetingCard({Key? key, required String name}) \\n : name = name, super(key: key);\\n \\nfinal String name; // 类中需要单独声明\\n
\\nKey
参数的作用Key
是Flutter框架用来识别Widget的标识符,重要用途:
const
构造函数的性能优势// 这两个对象共享同一内存位置\\nfinal card1 = const GreetingCard(name: \\"张三\\");\\nfinal card2 = const GreetingCard(name: \\"张三\\");\\n// card1 == card2 返回true\\n
\\n调用这个构造函数的方式:
\\n// 基本用法\\nGreetingCard(name: \\"张三\\")\\n\\n// 指定Key\\nGreetingCard(key: UniqueKey(), name: \\"张三\\")\\n\\n// 创建常量实例\\nconst GreetingCard(name: \\"张三\\")\\n
\\n这种构造函数模式在Flutter中非常普遍,几乎所有Widget都遵循这种风格,是高效开发Flutter应用的基础知识。
\\nCompose中的无状态UI:
\\n@Composable\\nfun GreetingCard(name: String) {\\n Box(modifier = Modifier.padding(16.dp)) {\\n Text(\\"Hello, $name!\\")\\n }\\n}\\n
\\nCompose中的状态管理:
\\n@Composable\\nfun Counter() {\\n var count by remember { mutableStateOf(0) }\\n \\n Column {\\n Text(\\"Count: $count\\")\\n Button(onClick = { count++ }) {\\n Text(\\"Increment\\")\\n }\\n }\\n}\\n
\\n1. 状态管理方式
\\nsetState()
触发重建2. 组件定义
\\n3. 生命周期
\\n\\n\\n💡 记忆窍门:Flutter中\\"声明在类中,状态在State中\\",而Compose中\\"声明和状态都在函数中\\"。
\\n
Flutter的Container类似于Compose中的Box,是最常用的布局容器。
\\nContainer(\\n margin: EdgeInsets.all(10.0),\\n padding: EdgeInsets.all(8.0),\\n width: 200,\\n height: 100,\\n decoration: BoxDecoration(\\n color: Colors.blue,\\n borderRadius: BorderRadius.circular(8.0),\\n boxShadow: [\\n BoxShadow(\\n color: Colors.black26,\\n blurRadius: 10.0,\\n offset: Offset(0, 5),\\n ),\\n ],\\n ),\\n child: Text(\'Hello Flutter!\'),\\n)\\n
\\n对应的Compose实现:
\\nBox(\\n modifier = Modifier\\n .size(200.dp, 100.dp)\\n .padding(10.dp)\\n .shadow(10.dp)\\n .clip(RoundedCornerShape(8.dp))\\n .background(Color.Blue)\\n .padding(8.dp)\\n) {\\n Text(\\"Hello Compose!\\")\\n}\\n
\\n\\n\\n💡 关键区别:Compose使用Modifier链式调用,而Flutter使用命名参数。Flutter的Container是一个综合组件,等价于Compose中多个Modifier的组合。
\\n
Row(水平布局):
\\nRow(\\n mainAxisAlignment: MainAxisAlignment.spaceBetween,\\n crossAxisAlignment: CrossAxisAlignment.center,\\n children: [\\n Icon(Icons.star, size: 30),\\n Text(\'Flutter布局\'),\\n ElevatedButton(\\n onPressed: () {},\\n child: Text(\'点击\'),\\n ),\\n ],\\n)\\n
\\n对应的Compose实现:
\\nRow(\\n horizontalArrangement = Arrangement.SpaceBetween,\\n verticalAlignment = Alignment.CenterVertically\\n) {\\n Icon(Icons.Star, contentDescription = null, modifier = Modifier.size(30.dp))\\n Text(\\"Compose布局\\")\\n Button(onClick = { }) {\\n Text(\\"点击\\")\\n }\\n}\\n
\\nColumn(垂直布局):
\\nColumn(\\n mainAxisSize: MainAxisSize.min,\\n mainAxisAlignment: MainAxisAlignment.center,\\n crossAxisAlignment: CrossAxisAlignment.stretch,\\n children: [\\n Text(\'标题\', style: Theme.of(context).textTheme.headline6),\\n SizedBox(height: 8),\\n Text(\'这是正文内容,展示如何使用Column进行垂直布局\'),\\n SizedBox(height: 16),\\n ElevatedButton(\\n onPressed: () {},\\n child: Text(\'确定\'),\\n ),\\n ],\\n)\\n
\\n对应的Compose实现:
\\nColumn(\\n verticalArrangement = Arrangement.Center,\\n horizontalAlignment = Alignment.CenterHorizontally,\\n modifier = Modifier.fillMaxWidth()\\n) {\\n Text(\\"标题\\", style = MaterialTheme.typography.h6)\\n Spacer(modifier = Modifier.height(8.dp))\\n Text(\\"这是正文内容,展示如何使用Column进行垂直布局\\")\\n Spacer(modifier = Modifier.height(16.dp))\\n Button(onClick = { }) {\\n Text(\\"确定\\")\\n }\\n}\\n
\\nFlutter | Compose | 区别 |
---|---|---|
mainAxisAlignment | horizontalArrangement/verticalArrangement | 命名不同,概念相似 |
crossAxisAlignment | verticalAlignment/horizontalAlignment | 命名不同,概念相似 |
children: [] | 花括号中的内容 | 语法不同 |
SizedBox | Spacer或Modifier.size | Flutter使用专用Widget |
mainAxisSize | wrapContentSize | 概念类似,实现不同 |
1. 固定尺寸与弹性尺寸:
\\nExpanded
和Flexible
weight(1f)
修饰符2. 间隔添加:
\\nSizedBox
或Padding
Spacer
或padding
修饰符3. 层叠布局:
\\nStack
和Positioned
Box
和BoxScope
中的修饰符核心概念回顾:
\\n从Compose到Flutter的过渡要点:
\\n以下是Dart与Kotlin的详细对比及流程图总结:
\\n特性 | Dart | Kotlin |
---|---|---|
开发者 | JetBrains | |
主要应用 | Flutter跨平台UI框架 | Android/JVM开发,多平台(KMM) |
编译方式 | JIT/AOT(移动端优先AOT) | JVM/JS/Native(多编译目标) |
设计目标 | 客户端优化、高生产力、跨平台 | 现代化Java替代、简洁与互操作性 |
// Dart\\nvar name = \'Dart\'; // 类型推断\\nString name = \'Dart\'; // 显式类型\\nfinal int age = 30; // 不可变\\nconst pi = 3.14; // 编译时常量\\nlate String description; // 延迟初始化\\n
\\n// Kotlin\\nvar name = \\"Kotlin\\" // 类型推断\\nval age: Int = 30 // 不可变(只读变量)\\nconst val PI = 3.14 // 编译时常量(需在顶层或object内)\\nlateinit var desc: String // 延迟初始化(不可val)\\n
\\n关键差异:
\\nfinal
对应Kotlin的val
,但Dart允许const
用于编译时常量lateinit
仅用于可变变量,Dart的late
更灵活// Dart\\nint add(int a, int b) => a + b; // 箭头函数\\nvoid printMsg(String msg) { /* ... */ }\\n// 命名参数(默认非必填)\\nvoid greet({String name = \'Guest\', int age}) { ... }\\ngreet(age: 25);\\n
\\n// Kotlin\\nfun add(a: Int, b: Int): Int = a + b \\nfun printMsg(msg: String): Unit { /* ... */ }\\n// 命名参数(需注解)\\nfun greet(name: String = \\"Guest\\", age: Int?) { ... }\\ngreet(age = 25)\\n
\\n关键差异:
\\n@JvmOverloads
处理=>
对应Kotlin的=
单表达式函数// Dart\\nString? nullableString; // 可空类型\\nString nonNullable = \'\'; // 非空\\nprint(nullableString?.length ?? 0); // 空合并\\nlate String lateInit; // 延迟初始化保证非空\\n
\\n// Kotlin\\nvar nullable: String? = null \\nval nonNull: String = \\"\\" \\nnullable?.length ?: 0 // Elvis操作符\\nlateinit var lateInit: String // 延迟初始化(非基本类型)\\n
\\n共同点:
\\n?
?.
和空合并??
(Dart)/ ?:
(Kotlin)在 Dart 中,??
是 空合并运算符(null coalescing operator),它的作用是:当左侧的值为 null
时,返回右侧的默认值;否则直接返回左侧的值。
print(nullableString?.length ?? 0);\\n
\\nnullableString?.length
?.
是 安全调用操作符(conditional member access):\\nnullableString
为 null
,整个表达式会直接返回 null
,而不会抛出空指针异常。nullableString
不为 null
,则返回其 length
属性的值。?? 0
nullableString?.length
结果为 null
,则整个表达式会返回 0
;nullableString.length
的值。等价于传统写法:
\\nif (nullableString != null) {\\n print(nullableString.length);\\n} else {\\n print(0);\\n}\\n
\\n其他常见用法示例:
\\n// 1. 变量默认值\\nString? name;\\nString displayName = name ?? \\"Guest\\"; // 如果 name 是 null,显示 \\"Guest\\"\\n\\n// 2. 链式安全调用 + 空合并\\nint? length = user?.profile?.bio?.length ?? 0; \\n\\n// 3. 简化空判断逻辑\\nint value = nullableInt ?? calculateDefault(); // 仅当 nullableInt 为 null 时调用函数\\n
\\n总结:
\\n??
是 Dart 空安全(null safety)中非常实用的语法糖,用于优雅处理可能为 null
的情况。?.
)无缝结合使用。// Dart\\nclass Animal {\\n String name;\\n Animal(this.name); // 构造方法简写\\n void speak() => print(\'...\');\\n}\\n\\nclass Dog extends Animal {\\n Dog(String name) : super(name);\\n @override void speak() => print(\'Woof!\');\\n}\\n
\\n// Kotlin\\nopen class Animal(val name: String) { // 默认final类需open\\n open fun speak() = println(\\"...\\")\\n}\\n\\nclass Dog(name: String) : Animal(name) {\\n override fun speak() = println(\\"Woof!\\")\\n}\\n
\\n关键差异:
\\nfinal
,需open
允许继承@override
注解,Kotlin直接override
关键字// Dart\\nclass Point {\\n final int x, y;\\n Point(this.x, this.y);\\n \\n @override\\n bool operator ==(Object other) => ... // 需手动实现equals/hashCode\\n}\\n
\\n// Kotlin\\ndata class Point(val x: Int, val y: Int) // 自动生成equals/hashCode/toString等\\n
\\n对比:Kotlin的data class
自动生成样板代码,Dart需手动实现或使用三方库(如equatable
)
// Dart\\nextension NumberParsing on String {\\n int parseInt() => int.parse(this);\\n}\\n\'42\'.parseInt();\\n
\\n// Kotlin\\nfun String.parseInt(): Int = this.toInt()\\n\\"42\\".parseInt()\\n
\\n相似性:两者均支持扩展已有类功能,语法略有不同。\\n这段代码是 Dart 中扩展方法(extension method) 的典型应用,它的作用是为 String
类型添加一个自定义方法 parseInt()
,用于将字符串转换为整数。下面逐步解释:
extension NumberParsing on String {\\n int parseInt() => int.parse(this);\\n}\\n
\\nextension NumberParsing on String
\\nNumberParsing
的扩展,专门为 String
类添加新方法。String
实例都能调用新增的方法。int parseInt() => int.parse(this);
\\nparseInt
的方法,返回 int
类型。=>
是 Dart 的箭头语法,相当于 { return ...; }
。int.parse(this)
:\\nthis
表示当前调用该方法的 String
实例(例如 \'42\'
)。int.parse()
是 Dart 内置方法,用于将字符串解析为整数。\'42\'.parseInt(); // 返回整数 42\\n
\\n\'42\'
调用 parseInt()
方法(通过扩展添加的方法)。int.parse(\'42\')
,但通过扩展方法更符合面向对象风格。类比传统写法
\\n如果不使用扩展方法,传统写法需要直接调用静态方法:
\\nint.parse(\'42\'); // 直接调用内置方法\\n
\\n而扩展方法让代码看起来像是字符串“自己拥有”这个方法:
\\n\'42\'.parseInt(); // 更直观,类似其他面向对象语言(如 Java 的 \\"42\\".toInt())\\n
\\n扩展方法的核心优势
\\n增强代码可读性
\\n将工具方法(如类型转换)附加到目标类上,代码更符合直觉。
避免重复代码
\\n如果项目中频繁需要将字符串转整数,可以统一通过扩展方法实现。
不修改原始类
\\n扩展方法是语法糖,不会实际修改 String
类的内部结构。
注意事项
\\n作用域:
\\n扩展方法仅在导入该扩展的文件中可用。如果要在其他文件中使用,需导入定义扩展的文件。
命名冲突:
\\n如果两个扩展为同一个类添加了同名方法,需通过显式指定扩展名解决:
\'42\'.NumberParsing.parseInt(); // 明确使用 NumberParsing 扩展中的方法\\n
\\n不可访问私有成员:
\\n扩展方法无法访问类的私有(以 _
开头的)属性和方法。
更多示例为 String
添加一个 toDouble()
扩展:
extension NumberParsing on String {\\n int parseInt() => int.parse(this);\\n double toDouble() => double.parse(this);\\n}\\n\\n// 使用\\nprint(\'3.14\'.toDouble()); // 3.14\\n
\\n这段代码通过 扩展方法,让 String
类型“拥有”了一个 parseInt()
方法,本质上是一种语法糖,目的是让代码更简洁、易读。它的底层实现仍然是调用 int.parse(this)
,但通过扩展方法,代码的组织方式更加面向对象。
Future<String> fetchData() async {\\n var data = await http.get(\'url\');\\n return process(data);\\n}\\n\\nStream<int> countStream() async* {\\n for (int i=0; i<5; i++) {\\n await Future.delayed(Duration(seconds:1));\\n yield i;\\n }\\n}\\n
\\n在 Dart 中,async/await
和 Future/Stream
是处理异步编程的核心机制,它们的区别主要体现在 抽象层级 和 适用场景 上:
Future | Stream | async/await | |
---|---|---|---|
本质 | 单个异步操作的结果封装 | 多个异步事件的连续数据流 | 语法糖,简化异步代码编写 |
数据量 | 单一值(或错误) | 多个值(0个、1个、或多个) | 主要配合 Future 使用 |
执行次数 | 一次性完成(类似 Promise) | 持续监听事件(类似 Observable) | 用于控制异步代码流程 |
Future<String> fetchData() async {\\n var data = await http.get(\'https://api.example.com/data\');\\n return processData(data);\\n}\\n
\\nawait
会暂停函数执行,直到 Future
完成。try/catch
捕获异常,代码更直观。Stream<int> countNumbers(int max) async* {\\n for (int i = 1; i <= max; i++) {\\n await Future.delayed(Duration(seconds: 1));\\n yield i; // 逐步生成值\\n }\\n}\\n\\n// 使用 listen 监听\\ncountNumbers(5).listen((num) => print(num)); // 输出 1,2,3,4,5\\n
\\nlisten
订阅数据,或使用 await for
逐条处理:\\nvoid processStream() async {\\n await for (var num in countNumbers(5)) {\\n print(num);\\n }\\n}\\n
\\nmap
、where
、debounce
)进行流转换。特性 | Future + async/await | Stream |
---|---|---|
数据频率 | 单次结果 | 多次事件(可连续或间断) |
控制流 | 线性执行(等待完成) | 事件驱动(持续监听) |
错误处理 | try/catch | onError 回调或 Stream.transform |
组合操作 | Future.wait([future1, future2]) | StreamZip , StreamGroup 等操作符 |
资源管理 | 自动释放 | 需手动调用 cancel() 或关闭流 |
用 Future + async/await
如果:
用 Stream
如果:
Stream
转为 Future
:await stream.first
(获取第一个值)或 await stream.toList()
。Future
转为 Stream
:Stream.fromFuture(future)
。Stream.asyncMap
限制异步任务的并发数。通过理解这些差异,你可以更精准地选择异步模型,写出高效且易维护的 Dart 代码! 🚀
\\nsuspend fun fetchData(): String {\\n val data = withContext(Dispatchers.IO) { http.get(\\"url\\") }\\n return process(data)\\n}\\n\\nfun countFlow(): Flow<Int> = flow {\\n for (i in 0..4) {\\n delay(1000)\\n emit(i)\\n }\\n}\\n
\\n核心差异:
\\nFuture
≈Kotlin的Deferred
,Stream
≈Flow
维度 | Dart | Kotlin |
---|---|---|
包管理 | pub.dev + pubspec.yaml | Maven /Gradle + 中央仓库 |
跨平台 | Flutter(iOS/Android/Web/桌面) | KMM(共享业务逻辑层) |
调试工具 | DevTools(热重载强大) | Android Studio集成 |
主要IDE | VS Code/Android Studio | IntelliJ IDEA/Android Studio |
graph TD\\n A[项目需求] --\x3e B{需要跨平台UI}\\n B --\x3e|是| C[选择Dart + Flutter]\\n B --\x3e|否| D{目标平台}\\n D --\x3e|Android/JVM| E[选择Kotlin]\\n D --\x3e|多平台逻辑共享| F[Kotlin Multiplatform]\\n D --\x3e|Web前端| G{DartFlutter Web或Kotlin/JS}\\n A --\x3e H{需要热重载快速迭代?}\\n H --\x3e|是| C\\n H --\x3e|否| I[根据团队经验选择]\\n
\\n选择Dart当:
\\n选择Kotlin当:
\\n两者在语法现代性上趋同,但生态定位不同,常在实际项目中配合使用(如KMM+Flutter)。
","description":"Dart的基本语法 以下是Dart与Kotlin的详细对比及流程图总结:\\n\\n一、语言背景与定位\\n特性\\tDart\\tKotlin开发者\\tGoogle\\tJetBrains\\n主要应用\\tFlutter跨平台UI框架\\tAndroid/JVM开发,多平台(KMM)\\n编译方式\\tJIT/AOT(移动端优先AOT)\\tJVM/JS/Native(多编译目标)\\n设计目标\\t客户端优化、高生产力、跨平台\\t现代化Java替代、简洁与互操作性\\n二、核心语法对比\\n1. 变量声明\\n// Dart\\nvar name = \'Dart\';…","guid":"https://juejin.cn/post/7484087066189463589","author":"Nathan20240616","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-22T03:11:56.399Z","media":null,"categories":["Android","Flutter","面试"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 自定义 CustomPaint 实现流体液态加载动画","url":"https://juejin.cn/post/7484088857283297291","content":"在移动应用开发中,精美的加载动画不仅能提升用户体验,还能有效缓解用户等待过程中的焦虑感。今天要分享的是一个基于 Flutter CustomPaint 实现的流体液态加载动画,它通过模拟液体流动、波浪起伏,并添加气泡和光晕效果,打造出一个极具视觉冲击力的加载指示器。
\\n首先创建一个基础页面,包含动画显示区域和交互控件:
\\nclass LiquidLoaderPage extends StatefulWidget {\\n @override\\n _LiquidLoaderPageState createState() => _LiquidLoaderPageState();\\n}\\n\\nclass _LiquidLoaderPageState extends State<LiquidLoaderPage>\\n with SingleTickerProviderStateMixin {\\n late AnimationController _controller;\\n double _progress = 0.0;\\n final _dragHeight = 100.0;\\n\\n @override\\n void initState() {\\n super.initState();\\n _controller = AnimationController(\\n vsync: this,\\n duration: Duration(milliseconds: 3000),\\n )..addListener(() {\\n setState(() {\\n _progress = _controller.value;\\n });\\n });\\n\\n _controller.repeat();\\n }\\n \\n // 其他代码省略...\\n}\\n
\\n这里使用 AnimationController
控制动画进度,将动画设为循环模式(repeat),动画时长为3秒。
@override\\nWidget build(BuildContext context) {\\n return Scaffold(\\n backgroundColor: Colors.black,\\n appBar: AppBar(\\n title: Text(\'流体液态加载动画\'),\\n backgroundColor: Colors.black87,\\n elevation: 0,\\n ),\\n body: Center(\\n child: Column(\\n mainAxisAlignment: MainAxisAlignment.center,\\n children: [\\n Container(\\n width: 300,\\n height: 300,\\n decoration: BoxDecoration(\\n borderRadius: BorderRadius.circular(20),\\n boxShadow: [\\n BoxShadow(\\n color: Colors.blue.withOpacity(0.5),\\n blurRadius: 20,\\n spreadRadius: 5,\\n )\\n ],\\n ),\\n child: ClipRRect(\\n borderRadius: BorderRadius.circular(20),\\n child: CustomPaint(\\n painter: LiquidPainter(\\n progress: _progress,\\n color1: Colors.blue,\\n color2: Colors.purple,\\n ),\\n ),\\n ),\\n ),\\n SizedBox(height: 50),\\n GestureDetector(\\n onVerticalDragUpdate: (details) {\\n setState(() {\\n _progress -= details.delta.dy / _dragHeight;\\n _progress = _progress.clamp(0.0, 1.0);\\n });\\n // 暂停自动动画\\n _controller.stop();\\n },\\n onVerticalDragEnd: (details) {\\n // 恢复自动动画\\n _controller.repeat();\\n },\\n child: Container(\\n width: 200,\\n height: 60,\\n decoration: BoxDecoration(\\n borderRadius: BorderRadius.circular(30),\\n gradient: LinearGradient(\\n colors: [Colors.blue, Colors.purple],\\n ),\\n boxShadow: [\\n BoxShadow(\\n color: Colors.blue.withOpacity(0.3),\\n blurRadius: 10,\\n spreadRadius: 2,\\n ),\\n ],\\n ),\\n child: Center(\\n child: Text(\\n \'拖动调整液位\',\\n style: TextStyle(\\n color: Colors.white,\\n fontSize: 18,\\n fontWeight: FontWeight.bold,\\n ),\\n ),\\n ),\\n ),\\n ),\\n ],\\n ),\\n ),\\n );\\n}\\n
\\n布局设计要点:
\\nclass LiquidPainter extends CustomPainter {\\n final double progress;\\n final Color color1;\\n final Color color2;\\n\\n LiquidPainter({\\n required this.progress,\\n required this.color1,\\n required this.color2,\\n });\\n\\n @override\\n void paint(Canvas canvas, Size size) {\\n // 绘制背景\\n final backgroundPaint = Paint()..color = Colors.black87;\\n canvas.drawRect(\\n Rect.fromLTWH(0, 0, size.width, size.height), backgroundPaint);\\n\\n // 绘制液体\\n final liquidHeight = size.height * progress;\\n final liquidRect =\\n Rect.fromLTWH(0, size.height - liquidHeight, size.width, liquidHeight);\\n\\n // 创建液体渐变\\n final gradient = LinearGradient(\\n begin: Alignment.topCenter,\\n end: Alignment.bottomCenter,\\n colors: [color1, color2],\\n );\\n\\n final liquidPaint = Paint()..shader = gradient.createShader(liquidRect);\\n\\n // 创建液体波浪路径\\n final path = Path();\\n \\n // 省略部分代码...\\n }\\n \\n // 其他方法省略...\\n}\\n
\\n波浪效果是这个动画的精髓,通过贝塞尔曲线和三角函数来实现:
\\n// 使用平滑的贝塞尔曲线绘制波浪\\nfinal waveHeight = 6.0; // 减小波浪高度\\nfinal baseHeight = size.height - liquidHeight;\\n\\n// 先添加第一个点\\npath.lineTo(0, baseHeight);\\n\\n// 使用更多的点和quadraticBezierTo绘制平滑曲线\\nfinal segments = 16;\\ndouble previousX = 0;\\ndouble previousY = baseHeight;\\n\\nfor (int i = 1; i <= segments; i++) {\\n final t = i / segments;\\n final x = size.width * t;\\n\\n // 使用单一的正弦函数,避免尖锐的波峰\\n final sinValue = sin((progress * 2 * pi) + (t * 4 * pi));\\n final y = baseHeight + sinValue * waveHeight;\\n\\n // 控制点,在前一点和当前点之间\\n final controlX = (previousX + x) / 2;\\n final controlY = baseHeight +\\n sin((progress * 2 * pi) + ((t - 0.5 / segments) * 4 * pi)) *\\n waveHeight;\\n\\n // 使用二次贝塞尔曲线连接点\\n path.quadraticBezierTo(controlX, controlY, x, y);\\n\\n previousX = x;\\n previousY = y;\\n}\\n\\n// 右边界和底部\\npath.lineTo(size.width, size.height - liquidHeight);\\npath.lineTo(size.width, size.height);\\npath.close();\\n\\n// A绘制液体\\ncanvas.drawPath(path, liquidPaint);\\n
\\n这段代码的关键点:
\\n为增强液体的真实感,添加随机气泡效果:
\\nvoid drawBubbles(Canvas canvas, Size size, Rect liquidRect) {\\n final random = Random(progress.toInt() * 1000);\\n final bubblePaint = Paint()..color = Colors.white.withOpacity(0.5);\\n\\n for (int i = 0; i < 15; i++) {\\n final bubbleSize = random.nextDouble() * 8 + 2;\\n final bubbleX = random.nextDouble() * size.width;\\n final bubbleY =\\n size.height - (random.nextDouble() * liquidRect.height * 0.8);\\n final offset = sin((progress * 2 * pi) + i) * 5;\\n\\n canvas.drawCircle(\\n Offset(bubbleX, bubbleY + offset), bubbleSize, bubblePaint);\\n }\\n}\\n
\\n气泡效果的实现要点:
\\n为提升视觉质感,添加光晕和高光效果:
\\nvoid drawGlow(Canvas canvas, Size size, Path path, Paint paint) {\\n // 添加发光效果\\n for (int i = 0; i < 5; i++) {\\n final glowPaint = Paint()\\n ..color = color1.withOpacity(0.1 - i * 0.02)\\n ..style = PaintingStyle.stroke\\n ..strokeWidth = i * 2.0;\\n\\n canvas.drawPath(path, glowPaint);\\n }\\n\\n // 添加顶部高光\\n final highlightPaint = Paint()\\n ..color = Colors.white.withOpacity(0.3)\\n ..style = PaintingStyle.stroke\\n ..strokeWidth = 1.0;\\n\\n final highlightPath = Path();\\n final baseHeight = size.height - size.height * progress;\\n\\n // 省略部分代码...\\n\\n canvas.drawPath(highlightPath, highlightPaint);\\n}\\n
\\n光晕效果的技术要点:
\\n这个动画不仅视觉效果出色,还具备交互功能。通过垂直拖动可以调整液位高度:
\\nGestureDetector(\\n onVerticalDragUpdate: (details) {\\n setState(() {\\n _progress -= details.delta.dy / _dragHeight;\\n _progress = _progress.clamp(0.0, 1.0);\\n });\\n // 暂停自动动画\\n _controller.stop();\\n },\\n onVerticalDragEnd: (details) {\\n // 恢复自动动画\\n _controller.repeat();\\n },\\n // ...\\n)\\n
\\n交互实现重点:
\\n在实现这类复杂动画时,需要注意以下性能优化点:
\\n@override\\nbool shouldRepaint(covariant LiquidPainter oldDelegate) {\\n return oldDelegate.progress != progress;\\n}\\n
\\n这个流体液态加载动画通过CustomPaint和数学曲线,实现了一个既美观又实用的加载指示器。关键技术包括:
\\n通过这个案例,我们可以看到Flutter强大的自定义绘制能力。灵活运用CustomPaint,配合数学函数和动画控制器,可以实现各种复杂而精美的动画效果。
\\n希望这篇文章能帮助开发者更好地理解和应用Flutter的绘制系统,创造出更加精美的用户界面和动画效果。
","description":"前言 在移动应用开发中,精美的加载动画不仅能提升用户体验,还能有效缓解用户等待过程中的焦虑感。今天要分享的是一个基于 Flutter CustomPaint 实现的流体液态加载动画,它通过模拟液体流动、波浪起伏,并添加气泡和光晕效果,打造出一个极具视觉冲击力的加载指示器。\\n\\n技术要点\\nCustomPaint 与 CustomPainter 的应用\\n贝塞尔曲线绘制平滑波浪\\n动画控制与状态管理\\n手势交互实现\\n粒子效果(气泡)与光晕处理\\n效果实现\\n1. 页面结构设计\\n\\n首先创建一个基础页面,包含动画显示区域和交互控件:\\n\\nclass LiquidLoaderP…","guid":"https://juejin.cn/post/7484088857283297291","author":"Sinyu1012","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-21T12:10:01.252Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b0689e57b9d845099d697dbbd2a541fc~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgU2lueXUxMDEy:q75.awebp?rk3s=f64ab15b&x-expires=1743163801&x-signature=S7gWvyNf%2FkAS3S%2Fd04RDhNX5sPY%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["iOS","前端","Flutter","UI Kit"],"attachments":null,"extra":null,"language":null},{"title":"Flutter Dart 元数据:为代码注入额外信息","url":"https://juejin.cn/post/7483826026784882726","content":"在 Flutter 开发中使用 Dart 语言编程时,元数据(Metadata)是一个强大但常被忽视的特性。元数据可以为代码元素(如类、方法、变量等)添加额外的信息,这些信息在运行时或者编译时可被读取和利用,从而实现一些特殊的功能,比如代码生成、注解等。本文将全面介绍 Dart 元数据的概念、使用方法、内置元数据以及自定义元数据,并结合代码示例进行详细说明。
\\n元数据是关于数据的数据,在 Dart 里,元数据是添加到代码元素上的注解,以 @
符号开头,后面跟着一个编译时常量表达式,通常是一个类的实例。元数据可以放在类、方法、字段、参数、函数等定义之前,用来提供额外的上下文信息。
Dart 提供了一些内置的元数据,常见的有 @deprecated
、@override
和 @required
等。
@deprecated
@deprecated
用于标记某个类、方法或字段已经过时,不建议再使用。当其他开发者使用被标记为 @deprecated
的代码时,编译器会给出警告。
// 标记方法为过时\\n@deprecated\\nvoid oldMethod() {\\n print(\'这是一个过时的方法\');\\n}\\n\\nvoid main() {\\n // 调用过时方法,编译器会给出警告\\n oldMethod();\\n}\\n
\\n在上述代码中,oldMethod
被标记为 @deprecated
,当在 main
函数中调用该方法时,编译器会提示该方法已过时。
@override
@override
用于标记一个方法是重写父类的方法。使用 @override
可以让代码更具可读性,同时编译器会检查是否真的重写了父类的方法,如果没有,会报错。
class Animal {\\n void makeSound() {\\n print(\'动物发出声音\');\\n }\\n}\\n\\nclass Dog extends Animal {\\n // 重写父类的方法\\n @override\\n void makeSound() {\\n print(\'汪汪汪\');\\n }\\n}\\n\\nvoid main() {\\n Dog dog = Dog();\\n dog.makeSound();\\n}\\n
\\n这里,Dog
类的 makeSound
方法重写了 Animal
类的 makeSound
方法,使用 @override
注解明确表示这是一个重写操作。
@required
@required
用于标记一个参数是必需的。在 Dart 2.12 及以后的版本中,建议使用非空类型来替代 @required
,但在旧代码或者特定场景下仍然会用到。
class Person {\\n final String name;\\n final int age;\\n\\n Person({@required this.name, @required this.age});\\n}\\n\\nvoid main() {\\n // 创建 Person 对象时,必须提供 name 和 age 参数\\n Person person = Person(name: \'张三\', age: 20);\\n print(\'姓名: ${person.name}, 年龄: ${person.age}\');\\n}\\n
\\n在 Person
类的构造函数中,@required
标记了 name
和 age
参数是必需的,创建 Person
对象时必须提供这两个参数。
除了使用内置元数据,我们还可以自定义元数据类,以满足特定的需求。
\\n自定义元数据类通常是一个简单的类,构造函数参数用于存储额外的信息。
\\n// 自定义元数据类\\nclass Author {\\n final String name;\\n final String email;\\n\\n const Author(this.name, {this.email});\\n}\\n\\n// 使用自定义元数据\\n@Author(\'李四\', email: \'lisi@example.com\')\\nclass MyClass {\\n void myMethod() {\\n print(\'这是 MyClass 的方法\');\\n }\\n}\\n\\nvoid main() {\\n MyClass myClass = MyClass();\\n myClass.myMethod();\\n}\\n
\\n在上述代码中,定义了一个 Author
类作为自定义元数据类,它有两个属性 name
和 email
。然后在 MyClass
类上使用 @Author
注解添加元数据。
在运行时读取元数据需要使用反射机制。不过,Dart 的反射功能在 Flutter 中受到限制,因为 Flutter 编译为 AOT(Ahead - Of - Time)代码,反射可能会增加代码体积和性能开销。在 Dart 中可以使用 dart:mirrors
库进行反射操作,但在 Flutter 里通常借助代码生成工具(如 build_runner
)来处理元数据。
以下是一个简单的示例,展示如何使用 dart:mirrors
读取元数据(注意:此示例在 Flutter 中不完全适用,仅作原理展示):
import \'dart:mirrors\';\\n\\nclass Author {\\n final String name;\\n final String email;\\n\\n const Author(this.name, {this.email});\\n}\\n\\n@Author(\'李四\', email: \'lisi@example.com\')\\nclass MyClass {\\n void myMethod() {\\n print(\'这是 MyClass 的方法\');\\n }\\n}\\n\\nvoid main() {\\n ClassMirror classMirror = reflectClass(MyClass);\\n InstanceMirror instanceMirror = classMirror.newInstance(Symbol(\'\'), []);\\n\\n List<DeclarationMirror> declarations = classMirror.declarations.values.toList();\\n for (var declaration in declarations) {\\n List<InstanceMirror> metadata = declaration.metadata;\\n for (var meta in metadata) {\\n if (meta.reflectee is Author) {\\n Author author = meta.reflectee;\\n print(\'作者姓名: ${author.name}, 邮箱: ${author.email}\');\\n }\\n }\\n }\\n}\\n
\\n在这个示例中,使用 dart:mirrors
库的 reflectClass
方法获取 MyClass
的 ClassMirror
,然后遍历类的声明,查找 Author
元数据并打印相关信息。
在 Flutter 开发中,元数据常用于代码生成,比如 json_serializable
库就利用元数据来自动生成 JSON 序列化和反序列化的代码。
首先,在 pubspec.yaml
文件中添加相关依赖:
dependencies:\\n flutter:\\n sdk: flutter\\n json_annotation: ^4.8.1\\n\\ndev_dependencies:\\n flutter_test:\\n sdk: flutter\\n build_runner: ^2.4.6\\n json_serializable: ^6.6.1\\n
\\nimport \'package:json_annotation/json_annotation.dart\';\\n\\n// 生成的文件名为 person.g.dart\\npart \'person.g.dart\';\\n\\n// 添加 @JsonSerializable 元数据\\n@JsonSerializable()\\nclass Person {\\n final String name;\\n final int age;\\n\\n Person(this.name, this.age);\\n\\n // 工厂方法,用于从 JSON 数据创建 Person 对象\\n factory Person.fromJson(Map<String, dynamic> json) => _$PersonFromJson(json);\\n\\n // 方法,将 Person 对象转换为 JSON 数据\\n Map<String, dynamic> toJson() => _$PersonToJson(this);\\n}\\n
\\n在终端中运行以下命令来生成序列化和反序列化代码:
\\nflutter pub run build_runner build\\n
\\nvoid main() {\\n Person person = Person(\'张三\', 20);\\n // 将 Person 对象转换为 JSON 数据\\n Map<String, dynamic> json = person.toJson();\\n print(\'JSON 数据: $json\');\\n\\n // 从 JSON 数据创建 Person 对象\\n Person newPerson = Person.fromJson(json);\\n print(\'姓名: ${newPerson.name}, 年龄: ${newPerson.age}\');\\n}\\n
\\n在这个示例中,@JsonSerializable
元数据告诉 json_serializable
库为 Person
类生成 JSON 序列化和反序列化的代码。通过运行 build_runner
命令,会自动生成 person.g.dart
文件,其中包含了具体的序列化和反序列化逻辑。
Dart 元数据为代码添加了额外的信息,使得代码更加灵活和可扩展。内置元数据可以帮助我们进行代码规范和提示,自定义元数据可以满足特定的业务需求,而元数据在代码生成中的应用则大大提高了开发效率。在实际的 Flutter 开发中,合理运用元数据可以让我们的代码更加健壮和易于维护。不过,要注意在 Flutter 中使用反射读取元数据的限制,尽量借助代码生成工具来处理元数据相关的操作。
","description":"引言 在 Flutter 开发中使用 Dart 语言编程时,元数据(Metadata)是一个强大但常被忽视的特性。元数据可以为代码元素(如类、方法、变量等)添加额外的信息,这些信息在运行时或者编译时可被读取和利用,从而实现一些特殊的功能,比如代码生成、注解等。本文将全面介绍 Dart 元数据的概念、使用方法、内置元数据以及自定义元数据,并结合代码示例进行详细说明。\\n\\n1. 元数据的基本概念\\n\\n元数据是关于数据的数据,在 Dart 里,元数据是添加到代码元素上的注解,以 @ 符号开头,后面跟着一个编译时常量表达式,通常是一个类的实例。元数据可以放在类、方法…","guid":"https://juejin.cn/post/7483826026784882726","author":"顾林海","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-21T07:16:00.073Z","media":null,"categories":["Android","前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter Dart 异步支持全面解析","url":"https://juejin.cn/post/7483414366739628042","content":"在 Flutter 开发中,Dart 语言提供了强大的异步支持机制。异步编程能够让程序在执行耗时操作(如网络请求、文件读写等)时,不会阻塞主线程,从而保证用户界面的流畅性和响应性。本文将详细介绍 Dart 中常见的异步编程方式,包括 Future
、async/await
和 Stream
,并结合代码示例进行说明。
在同步编程中,程序按照代码的顺序依次执行,当遇到耗时操作时,程序会阻塞,直到该操作完成后才会继续执行后续代码。这种方式在处理耗时任务时会导致界面卡顿,影响用户体验。
\\n异步编程允许程序在执行耗时操作时,不会阻塞主线程,而是继续执行后续代码。当耗时操作完成后,会通过回调、事件等方式通知程序进行相应的处理。
\\nFuture
是 Dart 中用于表示异步操作结果的类。一个 Future
对象代表一个可能还未完成的异步操作,它可以处于三种状态:未完成、已完成成功和已完成失败。
// 模拟一个异步操作,返回一个 Future 对象\\nFuture<String> fetchData() {\\n return Future.delayed(Duration(seconds: 2), () {\\n return \'异步操作返回的数据\';\\n });\\n}\\n\\nvoid main() {\\n print(\'开始执行\');\\n\\n // 调用异步函数\\n Future<String> future = fetchData();\\n\\n // 处理 Future 的结果\\n future.then((data) {\\n print(\'获取到的数据: $data\');\\n }).catchError((error) {\\n print(\'发生错误: $error\');\\n }).whenComplete(() {\\n print(\'异步操作完成\');\\n });\\n\\n print(\'继续执行其他代码\');\\n}\\n
\\nfetchData
函数模拟了一个异步操作,使用 Future.delayed
方法延迟 2 秒后返回一个字符串。main
函数中,调用 fetchData
函数得到一个 Future
对象。then
方法处理 Future
成功完成的结果,catchError
方法处理 Future
失败的情况,whenComplete
方法无论 Future
成功还是失败都会执行。fetchData
是异步操作,在调用该函数后,程序会继续执行 print(\'继续执行其他代码\');
,而不会等待异步操作完成。async/await
是 Dart 中用于简化异步编程的语法糖,它基于 Future
实现,让异步代码看起来更像同步代码。
// 模拟一个异步操作,返回一个 Future 对象\\nFuture<String> fetchData() {\\n return Future.delayed(Duration(seconds: 2), () {\\n return \'异步操作返回的数据\';\\n });\\n}\\n\\n// 异步函数\\nFuture<void> mainAsync() async {\\n print(\'开始执行\');\\n\\n try {\\n // 使用 await 关键字等待异步操作完成\\n String data = await fetchData();\\n print(\'获取到的数据: $data\');\\n } catch (error) {\\n print(\'发生错误: $error\');\\n } finally {\\n print(\'异步操作完成\');\\n }\\n\\n print(\'继续执行其他代码\');\\n}\\n\\nvoid main() {\\n mainAsync();\\n}\\n
\\nmainAsync
函数使用 async
关键字标记为异步函数,在异步函数内部可以使用 await
关键字等待 Future
对象完成。await fetchData()
会暂停 mainAsync
函数的执行,直到 fetchData
异步操作完成,并将结果赋值给 data
变量。try - catch - finally
语句来处理可能出现的异常和在操作完成后执行清理工作。Stream
是 Dart 中用于处理异步数据序列的类。它可以看作是一个异步的迭代器,允许程序在数据到达时逐个处理。
// 创建一个 Stream 对象\\nStream<int> countStream(int max) async* {\\n for (int i = 1; i <= max; i++) {\\n await Future.delayed(Duration(seconds: 1));\\n yield i;\\n }\\n}\\n\\nvoid main() {\\n Stream<int> stream = countStream(5);\\n\\n // 监听 Stream 事件\\n stream.listen((data) {\\n print(\'接收到的数据: $data\');\\n }, onError: (error) {\\n print(\'发生错误: $error\');\\n }, onDone: () {\\n print(\'Stream 处理完成\');\\n });\\n\\n print(\'继续执行其他代码\');\\n}\\n
\\ncountStream
是一个异步生成器函数,使用 async*
关键字标记,yield
关键字用于逐个产生数据。该函数会每秒产生一个整数,直到达到 max
值。main
函数中,调用 countStream(5)
得到一个 Stream
对象。listen
方法监听 Stream
的事件,包括数据到达、错误发生和流结束。Stream
是异步处理的,程序会继续执行 print(\'继续执行其他代码\');
,而不会等待 Stream
处理完成。Stream
还支持各种操作符,如 map
、where
、reduce
等,用于对数据序列进行转换和处理。
Stream<int> countStream(int max) async* {\\n for (int i = 1; i <= max; i++) {\\n await Future.delayed(Duration(seconds: 1));\\n yield i;\\n }\\n}\\n\\nvoid main() {\\n Stream<int> stream = countStream(5);\\n\\n // 使用操作符处理 Stream\\n Stream<int> squaredStream = stream.map((number) => number * number);\\n Stream<int> evenStream = squaredStream.where((number) => number % 2 == 0);\\n\\n evenStream.listen((data) {\\n print(\'处理后的数据: $data\');\\n }, onDone: () {\\n print(\'Stream 处理完成\');\\n });\\n}\\n
\\nmap
操作符将 Stream
中的每个元素进行平方运算。where
操作符过滤出平方后为偶数的元素。Stream
,输出符合条件的数据。在异步编程中,错误处理非常重要。可以使用 catchError
方法处理 Future
和 Stream
中的错误。
// 模拟一个可能出错的异步操作\\nFuture<String> fetchData() {\\n return Future.delayed(Duration(seconds: 2), () {\\n throw Exception(\'模拟的错误\');\\n });\\n}\\n\\nvoid main() {\\n // 处理 Future 错误\\n fetchData().catchError((error) {\\n print(\'Future 发生错误: $error\');\\n });\\n\\n // 模拟一个可能出错的 Stream\\n Stream<int> errorStream = Stream.error(Exception(\'Stream 模拟的错误\'));\\n errorStream.listen((data) {\\n print(\'接收到的数据: $data\');\\n }, onError: (error) {\\n print(\'Stream 发生错误: $error\');\\n });\\n}\\n
\\nfetchData
函数模拟了一个会抛出异常的异步操作,使用 catchError
方法捕获 Future
中的错误。Stream.error
用于创建一个会立即抛出错误的 Stream
,使用 listen
方法的 onError
参数处理 Stream
中的错误。Dart 提供的 Future
、async/await
和 Stream
等异步编程机制,使得 Flutter 应用能够高效地处理耗时操作,避免阻塞主线程,提高用户体验。Future
适用于处理单个异步结果,async/await
让异步代码更易读,Stream
则用于处理异步数据序列。在实际开发中,根据具体需求选择合适的异步编程方式,并妥善处理可能出现的错误。
在 Flutter 开发中,Dart 语言的泛型是一项强大且实用的特性。泛型允许我们在定义类、方法或接口时使用类型参数,这样可以编写更加灵活、可复用且类型安全的代码。下面将详细介绍 Dart 泛型的各个方面,并结合代码示例进行说明。
\\n泛型的核心思想是将类型作为参数传递,从而使得代码可以处理多种不同类型的数据,而不需要为每种类型都编写重复的代码。通过使用泛型,我们可以在编译时进行类型检查,提高代码的安全性和可读性。
\\n泛型类是指在定义类时使用类型参数的类。这样,类中的属性、方法等可以使用这个类型参数,从而实现对不同类型数据的处理。
\\n// 定义一个泛型类 Box,它可以存储任意类型的数据\\nclass Box<T> {\\n T value;\\n\\n Box(this.value);\\n\\n T getValue() {\\n return value;\\n }\\n}\\n\\nvoid main() {\\n // 创建一个存储整数的 Box 对象\\n Box<int> intBox = Box<int>(10);\\n print(\'整数 Box 中的值: ${intBox.getValue()}\');\\n\\n // 创建一个存储字符串的 Box 对象\\n Box<String> stringBox = Box<String>(\'Hello, Dart!\');\\n print(\'字符串 Box 中的值: ${stringBox.getValue()}\');\\n}\\n
\\nclass Box<T>
定义了一个泛型类 Box
,其中 <T>
是类型参数,它可以代表任意类型。T value
表示 Box
类中的 value
属性的类型是 T
,即可以是任意类型。Box(this.value)
是构造函数,用于初始化 value
属性。T getValue()
方法返回 value
属性,其返回类型也是 T
。main
函数中,我们分别创建了存储整数和字符串的 Box
对象,并调用 getValue
方法获取存储的值。泛型方法是指在方法定义中使用类型参数的方法。泛型方法可以独立于类的泛型定义,使得方法更加灵活。
\\n// 定义一个泛型方法,用于交换两个变量的值\\nT swap<T>(T a, T b) {\\n T temp = a;\\n a = b;\\n b = temp;\\n return a;\\n}\\n\\nvoid main() {\\n // 交换两个整数\\n int resultInt = swap<int>(5, 10);\\n print(\'交换后的整数: $resultInt\');\\n\\n // 交换两个字符串\\n String resultString = swap<String>(\'Hello\', \'World\');\\n print(\'交换后的字符串: $resultString\');\\n}\\n
\\nT swap<T>(T a, T b)
定义了一个泛型方法 swap
,其中 <T>
是类型参数,T
表示方法的参数和返回值的类型。temp
交换了两个参数的值,并返回交换后的第一个参数。main
函数中,分别调用 swap
方法交换了两个整数和两个字符串,并打印交换后的结果。泛型接口是指在接口定义中使用类型参数的接口。实现泛型接口的类需要指定具体的类型参数。
\\n// 定义一个泛型接口\\nabstract class Listable<T> {\\n void add(T item);\\n T get(int index);\\n}\\n\\n// 实现泛型接口的类\\nclass MyList<T> implements Listable<T> {\\n List<T> _items = [];\\n\\n @override\\n void add(T item) {\\n _items.add(item);\\n }\\n\\n @override\\n T get(int index) {\\n return _items[index];\\n }\\n}\\n\\nvoid main() {\\n // 创建一个存储整数的 MyList 对象\\n MyList<int> intList = MyList<int>();\\n intList.add(1);\\n intList.add(2);\\n print(\'MyList 中索引为 1 的整数: ${intList.get(1)}\');\\n\\n // 创建一个存储字符串的 MyList 对象\\n MyList<String> stringList = MyList<String>();\\n stringList.add(\'Apple\');\\n stringList.add(\'Banana\');\\n print(\'MyList 中索引为 0 的字符串: ${stringList.get(0)}\');\\n}\\n
\\nabstract class Listable<T>
定义了一个泛型接口 Listable
,其中 <T>
是类型参数,接口中定义了 add
和 get
两个抽象方法。class MyList<T> implements Listable<T>
表示 MyList
类实现了 Listable
泛型接口,MyList
类中使用 List<T>
来存储元素,并实现了 add
和 get
方法。main
函数中,分别创建了存储整数和字符串的 MyList
对象,并调用 add
和 get
方法进行元素的添加和获取。有时候,我们希望泛型类型参数满足一定的条件,这时可以使用泛型约束。在 Dart 中,可以使用 extends
关键字来实现泛型约束。
// 定义一个抽象类 Animal\\nabstract class Animal {\\n void makeSound();\\n}\\n\\n// 定义一个 Dog 类,继承自 Animal\\nclass Dog extends Animal {\\n @override\\n void makeSound() {\\n print(\'汪汪汪\');\\n }\\n}\\n\\n// 定义一个泛型类,要求类型参数必须是 Animal 的子类\\nclass AnimalBox<T extends Animal> {\\n T animal;\\n\\n AnimalBox(this.animal);\\n\\n void playSound() {\\n animal.makeSound();\\n }\\n}\\n\\nvoid main() {\\n Dog dog = Dog();\\n AnimalBox<Dog> dogBox = AnimalBox<Dog>(dog);\\n dogBox.playSound();\\n}\\n
\\nabstract class Animal
定义了一个抽象类 Animal
,其中包含一个抽象方法 makeSound
。class Dog extends Animal
定义了一个 Dog
类,继承自 Animal
类,并实现了 makeSound
方法。class AnimalBox<T extends Animal>
定义了一个泛型类 AnimalBox
,使用 extends Animal
对类型参数 T
进行约束,要求 T
必须是 Animal
的子类。void playSound()
方法调用了 animal
对象的 makeSound
方法。main
函数中,创建了一个 Dog
对象和一个 AnimalBox<Dog>
对象,并调用 playSound
方法播放声音。Dart 的集合类(如 List
、Set
、Map
等)都广泛使用了泛型,以确保集合中元素的类型安全。
void main() {\\n // 创建一个存储整数的 List\\n List<int> intList = [1, 2, 3, 4, 5];\\n print(\'整数 List: $intList\');\\n\\n // 创建一个存储字符串的 Set\\n Set<String> stringSet = {\'Apple\', \'Banana\', \'Cherry\'};\\n print(\'字符串 Set: $stringSet\');\\n\\n // 创建一个键为字符串,值为整数的 Map\\n Map<String, int> scoreMap = {\'张三\': 80, \'李四\': 90, \'王五\': 75};\\n print(\'分数 Map: $scoreMap\');\\n}\\n
\\nList<int>
表示创建一个只能存储整数的列表。Set<String>
表示创建一个只能存储字符串的集合。Map<String, int>
表示创建一个键为字符串,值为整数的映射。Dart 的泛型是一个非常强大的特性,它可以提高代码的复用性、灵活性和类型安全性。通过泛型类、泛型方法、泛型接口和泛型约束等,我们可以编写更加通用和高效的代码。在实际的 Flutter 开发中,合理运用泛型可以让我们的代码更加健壮和易于维护。
","description":"引言 在 Flutter 开发中,Dart 语言的泛型是一项强大且实用的特性。泛型允许我们在定义类、方法或接口时使用类型参数,这样可以编写更加灵活、可复用且类型安全的代码。下面将详细介绍 Dart 泛型的各个方面,并结合代码示例进行说明。\\n\\n1. 泛型的基本概念\\n\\n泛型的核心思想是将类型作为参数传递,从而使得代码可以处理多种不同类型的数据,而不需要为每种类型都编写重复的代码。通过使用泛型,我们可以在编译时进行类型检查,提高代码的安全性和可读性。\\n\\n2. 泛型类\\n\\n泛型类是指在定义类时使用类型参数的类。这样,类中的属性、方法等可以使用这个类型参数…","guid":"https://juejin.cn/post/7483314478958624778","author":"顾林海","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-19T10:10:43.338Z","media":null,"categories":["Android","前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 谷歌地图与页面滑动冲突","url":"https://juejin.cn/post/7483341465235767346","content":"\\n\\n最近海外用户端接入谷歌地图,需要在一个页面部分区域显示googleMap,但是页面具有自己的滑动事件,例如ListView、SingleChildScrollView.
\\n
在Flutter中使用Google Maps插件(google_maps_flutter
)时,如果地图嵌入在一个可滚动的父组件(如ListView
或SingleChildScrollView
)中,可能会遇到地图和页面滑动手势冲突的问题。这是因为两者都在试图响应垂直滑动手势。
为了解决这个问题,你可以采取以下几种方法:
\\nGestureDetector
拦截手势使用GestureDetector
来拦截特定方向的手势,并根据需要决定是让这些手势传递给子组件还是自己处理。例如,可以识别用户是在尝试滚动整个页面还是只是想拖动地图。
GestureDetector( \\n onVerticalDragUpdate: (details) { \\n // 根据你的逻辑判断是否应该允许地图滚动或者页面滚动 \\n if (shouldScrollPage(details)) { \\n // 手动触发页面滚动 \\n } else { \\n // 不做任何操作,允许地图处理这个手势\\n } \\n }, \\n child: GoogleMap( // 设置你的地图参数 ),\\n)\\n
\\nGoogleMap
的gestureRecognizers
属性一个更直接的方法是利用GoogleMap
提供的gestureRecognizers
属性来配置哪些手势应由地图处理。通过设置手势识别器,可以让地图只响应特定类型的手势,从而避免与父级滚动容器发生冲突。
GoogleMap(\\n gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>[ \\n new Factory<OneSequenceGestureRecognizer>(() => new EagerGestureRecognizer()\\n ), ].toSet(),\\n // 其他地图参数... )\\n
\\n上述代码片段会使地图响应所有手势,但这可能并不是你想要的效果。你可以更精确地控制这一点,例如,仅允许地图处理水平滑动(这样就不会与垂直滚动的父容器冲突),而将垂直滑动留给父容器处理。
\\ngestureRecognizers
以适应具体需求为了更好地控制,你可以创建自定义的手势识别器集合,使得地图仅在其认为合适的时机响应手势。例如,当检测到用户的滑动主要是水平方向时才让地图处理手势,否则允许父级滚动视图处理。
\\nGoogleMap( gestureRecognizers: Set()..add(Factory<PanGestureRecognizer>(() => PanGestureRecognizer())) ..add(Factory<ScaleGestureRecognizer>(() => ScaleGestureRecognizer())), // 其他地图参数... )\\n
\\n通过以上方法之一,能够解决Flutter应用中Google Maps与页面上下滑动之间的冲突问题。选择哪种方法取决于具体需求以及你希望如何管理用户体验。
","description":"最近海外用户端接入谷歌地图,需要在一个页面部分区域显示googleMap,但是页面具有自己的滑动事件,例如ListView、SingleChildScrollView. 在Flutter中使用Google Maps插件(google_maps_flutter)时,如果地图嵌入在一个可滚动的父组件(如ListView或SingleChildScrollView)中,可能会遇到地图和页面滑动手势冲突的问题。这是因为两者都在试图响应垂直滑动手势。\\n\\n为了解决这个问题,你可以采取以下几种方法:\\n\\n使用GestureDetector拦截手势\\n\\n使用GestureDe…","guid":"https://juejin.cn/post/7483341465235767346","author":"黎明故日","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-19T09:58:50.451Z","media":null,"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"开发突围:该换电脑了","url":"https://juejin.cn/post/7483158015191089186","content":"最近引入一个开源的AI项目(使用的技术栈相对比较新)进行二次开发,在我的工作本上启动开发很正常,在我之前老工作本上得启动多次才能成功,在我的一个同事工作本上发现根本启动不起来。
\\n \\"dependencies\\": {\\n \\"antd\\": \\"^5.12.7\\",\\n \\"react\\": \\"^18.2.0\\",\\n \\"react-dom\\": \\"^18.2.0\\",\\n \\"i18next\\": \\"^23.7.16\\",\\n \\"umi\\": \\"^4.0.90\\",\\n \\"tailwindcss\\": \\"^3\\",\\n },\\n \\"engines\\": {\\n \\"node\\": \\">=18.20.4\\"\\n }\\n
\\n同时对依赖包版本、兼容性、node_modules
、npm cache
等可能涉及的因素排查了一遍,发现始终无法解决问题,又按照AI提出的一些方案进行插件配置也无法解决问题,折腾了两天也无法投入开发。
按照现象来看,node版本一致、依赖包版本一致、甚至下载的node_modules都是用我提供的压缩包,唯一区别就是工作本不一样。我的工作本是最近国补新买的电脑,之前老工作本用了五年性能有些跟不上,同事的工作本也相对比较老旧。错误日志提示generate failed after 5 second
,大概率是执行某个任务时超时了。
我猜想大概率是电脑性能跟不上导致的问题,于是我操作关了其它额外的所有应用程序,也暂停了火绒的自动检索,尝试把CPU、内存全部压到Vscode应用上,结果和我猜想的一致,启动成功了。
\\n同事遇到的这个问题其实比较典型,在用老工作本时因为可能涉及前端项目、Java项目、Flutter项目等多个业务开发,多开编辑器的情况下就是很卡顿,不过并没有直接错误的场景。自从使用了新的工作本才发现,以前难以解决优化的问题已经消失了。
\\n曾经很长一段时间,我受控于前端项目启动、build、热更新都很慢,我还专门在webpack配置、插件性能分析、CPU多核利用(happyThreadPool
)等多维度进行优化,除过打的包由110Mb 降至 23MB
外,速度上基本没有明显提升。
const happyThreadPool = HappyPack.ThreadPool({ size: require(\'os\').cpus().length });\\n\\nconfig.plugin(\'HappyPack\').use(HappyPack, [\\n {\\n id: \'js\',\\n loaders: [\'babel-loader\'],\\n threadPool: happyThreadPool,\\n },\\n]);\\n
\\n因为happyThreadPool的使用,导致在执行npm run build
时,电脑很难在同步执行其它任务,同事曾经调笑到:咱们项目进入接水时间,因为build用时够来回去水房接水。
对比webpack5在不同环境的构建表现:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n特性 | 旧设备 | 新设备 |
---|---|---|
持久缓存构建 | 78s | 14s |
并行压缩 | 45s | 7s |
模块热替换 | 9s | 0.3s |
通过对比webpack构建日志发现:
\\n使用新工作本后,速度提升了一个维度,我之前绞尽脑汁想要优化的开发速度问题解决了。
\\n开发过程中,有一个很常见的场景就是文件夹复制粘贴,因为原始项目的问题,项目的依赖包经常安装失败。以这个项目为模板做新项目时经常涉及对node_modules
的复制粘贴。在大多数时候node_modules的依赖包不止文件大、文件数量多(4w+)、目录层级多,无论对其进行压缩、解压迁移,或者直接迁移都很慢,老工作本一般需要花费小半个小时。
自从使用新工作本后,发现以最耗时的直接复制粘贴方式也只需要最多3分钟就可以完成。曾经受控的搭建基本环境慢的问题竟然没了。
\\n在迁移node_modules时:
\\n曾经的开发,因为vscode本身集成了很多插件,应用占据内存一致很高,为了避免出现卡顿情况。开发基本都是单开工作项目,即使有协同项目也分次修改。更别提同时开发前后端,同时开启idea
、vscode
、chrome
了。同时开发电脑基本会被拉爆,打字都开始延迟。\\n当同时开启:
\\n而现在idea(Java开发)、vscode(React开发)、Android Studio(Flutter开发)chrome、DevEco Studio(鸿蒙开发)的多开应用场景下也能开发自如。
站在我开发的角度来看,随着AI辅助开发的不断接入,开发人员的多线程工作方式将变得越来越普遍,AI在分析需求生成业务代码的过程中本身也消耗大量电脑性能。AI辅助开发效率上的提升我已经能直观感受到了,因为我同时使用了ROO CODE、MarsCode AI、GitHub Copilot进行AI辅助开发,开发效率提升60%以上,但是电脑性能的瓶颈可能是影响我开发效率的一个关键因素。
\\n使用GitHub Copilot时,代码建议响应时间:
\\n另一方面,一些开发第三方库的插件因为版本的不断升级,在运行构建过程中对电脑性能也有了要求,回头看一下开头的问题也能发现这一现象。新的项目、新的技术的更迭,反向在推动硬件设备更迭升级。
\\n随着AI技术普及、企业降本增效,开发人数会减少,但对留下的开发人员来说,技术多趋向于全栈化。全栈意味着电脑上会下载更多的关联应用,同时运行的应用也会更多,对电脑性能也会提出新的挑战。
\\n根据2024年StackOverflow调研,现代全栈开发推荐配置:
\\n申明一下,不是想推广什么电脑,标题只是想醒目点,只是单纯根据自己最近换电脑经历总结的。
","description":"背景 最近引入一个开源的AI项目(使用的技术栈相对比较新)进行二次开发,在我的工作本上启动开发很正常,在我之前老工作本上得启动多次才能成功,在我的一个同事工作本上发现根本启动不起来。\\n\\n \\"dependencies\\": {\\n \\"antd\\": \\"^5.12.7\\",\\n \\"react\\": \\"^18.2.0\\",\\n \\"react-dom\\": \\"^18.2.0\\",\\n \\"i18next\\": \\"^23.7.16\\",\\n \\"umi\\": \\"^4.0.90\\",\\n \\"tailwindcss\\": \\"^3\\",\\n },\\n \\"e…","guid":"https://juejin.cn/post/7483158015191089186","author":"机器瓦力","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-19T08:33:14.846Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3df2009dece64261ab93496afa27cc0c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5py65Zmo55Om5Yqb:q75.awebp?rk3s=f64ab15b&x-expires=1742977994&x-signature=9N5H2Lr2kS%2FWx2AEdpS6uZ7Oob8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/643d6af78e0640d69cb39da14c76a362~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5py65Zmo55Om5Yqb:q75.awebp?rk3s=f64ab15b&x-expires=1742977994&x-signature=Ev7xRuCylcsw8sAQ6oSvzSALSOw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1e6f47443b4148d18ea9de67a699e159~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5py65Zmo55Om5Yqb:q75.awebp?rk3s=f64ab15b&x-expires=1742977994&x-signature=lItPJSsk68K1ZZQsr449mxEOu7M%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["代码人生","程序员","React.js","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"深入理解 Flutter 的 InheritedWidget 原理","url":"https://juejin.cn/post/7483117406656348179","content":"在 Flutter 开发中,我们经常需要在 Widget 树中共享数据,比如主题、语言环境、全局配置等。为了高效地实现这些功能,Flutter 提供了一个强大的工具——InheritedWidget
。它是 Flutter 状态管理的核心机制之一,也是许多状态管理框架(如 Provider)的基础。本文将深入剖析 InheritedWidget
的原理,帮助你更好地理解它的工作方式和应用场景。
InheritedWidget
是 Flutter 中的一种特殊 Widget,专门用于在 Widget 树中向其子树传递数据。它的主要特点是:
InheritedWidget
能够将数据从 Widget 树的某一节点传递到其子树中任意深度的节点。InheritedWidget
的子 Widget 才会在数据变化时重新构建,未订阅的子 Widget 不会受到影响。通过 InheritedWidget
,我们可以避免手动通过构造函数层层传递数据,从而简化代码结构,提高开发效率。
InheritedWidget
本身是不可变的,它的数据通常通过构造函数传递并存储在实例中。当它挂载到 Widget 树时,数据会通过树状结构向下传递,供子 Widget 使用。
子 Widget 想要获取 InheritedWidget
的数据,需要调用 BuildContext.dependOnInheritedWidgetOfExactType
方法。这个方法会:
InheritedWidget
。当 InheritedWidget
的数据发生变化时,Flutter 会创建一个新的 InheritedWidget
实例,并调用其 updateShouldNotify
方法。该方法用于判断新旧数据是否有变化:
true
,则通知所有依赖的子 Widget 重新构建。false
,则子 Widget 不会重新构建。InheritedWidget
的核心方法updateShouldNotify
:
InheritedWidget
是否需要通知子 Widget。true
时,依赖该 InheritedWidget
的子 Widget 会重新构建。dependOnInheritedWidgetOfExactType
:
InheritedWidget
。InheritedWidget
更新时子 Widget 能收到通知。getElementForInheritedWidgetOfExactType
:
InheritedElement
的实例,但不会建立依赖关系。InheritedElement
InheritedElement
是 InheritedWidget
在 Element 树中的对应节点。InheritedWidget
。InheritedWidget
更新时,InheritedElement
会触发依赖的子 Widget 的重建。以下是 InheritedWidget
的典型工作流程:
InheritedWidget
的类,并通过其构造函数传递共享数据。InheritedWidget
挂载到 Widget 树时,会在 Element 树中创建一个对应的 InheritedElement
。BuildContext.dependOnInheritedWidgetOfExactType
方法获取最近的 InheritedWidget
。InheritedElement
会将当前子 Widget 注册为依赖者。InheritedWidget
的数据发生变化时,Flutter 会创建一个新的 InheritedWidget
实例。updateShouldNotify
方法会比较新旧数据,判断是否需要通知依赖者。updateShouldNotify
返回 true
,InheritedElement
会通知所有依赖的子 Widget。build
方法重新构建。以下是一个简单的 InheritedWidget
示例,用于共享计数器数据:
// 创建一个继承自 InheritedWidget 的类\\nclass CounterInheritedWidget extends InheritedWidget {\\n final int counter;\\n\\n const CounterInheritedWidget({\\n required this.counter,\\n required Widget child,\\n }) : super(child: child);\\n\\n // 判断是否需要通知子 Widget\\n @override\\n bool updateShouldNotify(CounterInheritedWidget oldWidget) {\\n return oldWidget.counter != counter;\\n }\\n\\n // 提供一个静态方法供子 Widget 获取数据\\n static CounterInheritedWidget? of(BuildContext context) {\\n return context.dependOnInheritedWidgetOfExactType<CounterInheritedWidget>();\\n }\\n}\\n\\n// 使用 InheritedWidget 提供数据\\nclass CounterProvider extends StatefulWidget {\\n final Widget child;\\n\\n const CounterProvider({required this.child});\\n\\n @override\\n _CounterProviderState createState() => _CounterProviderState();\\n}\\n\\nclass _CounterProviderState extends State<CounterProvider> {\\n int _counter = 0;\\n\\n void _incrementCounter() {\\n setState(() {\\n _counter++;\\n });\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return CounterInheritedWidget(\\n counter: _counter,\\n child: Column(\\n children: [\\n widget.child,\\n ElevatedButton(\\n onPressed: _incrementCounter,\\n child: Text(\\"Increment Counter\\"),\\n ),\\n ],\\n ),\\n );\\n }\\n}\\n\\n// 子 Widget 获取共享数据\\nclass CounterDisplay extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n final counter = CounterInheritedWidget.of(context)?.counter ?? 0;\\n return Text(\\"Counter: $counter\\");\\n }\\n}\\n\\n// 主程序\\nvoid main() {\\n runApp(\\n CounterProvider(\\n child: CounterDisplay(),\\n ),\\n );\\n}\\n
\\nCounterInheritedWidget
:\\nupdateShouldNotify
判断是否需要通知子 Widget。CounterProvider
:\\nCounterInheritedWidget
。CounterDisplay
:\\nCounterInheritedWidget.of(context)
获取计数器值,并显示在界面上。InheritedWidget
嵌套时,代码可能变得难以维护。InheritedWidget
适用于以下场景:
InheritedWidget
是 Flutter 中实现数据共享和状态管理的基础工具。它通过高效的订阅与通知机制,帮助我们在 Widget 树中灵活地传递数据。虽然它本身功能简单,但却是许多状态管理框架(如 Provider)的核心构建模块。
在实际开发中,如果状态管理需求较为简单,可以直接使用 InheritedWidget
;如果需求复杂,可以结合 ChangeNotifier
或第三方状态管理框架(如 Provider、Riverpod)来实现更强大的功能。
通过理解 InheritedWidget
的原理和使用方式,你将能够更好地掌握 Flutter 的状态管理机制,写出更加高效和优雅的代码!
在使用Flutter时,Provider
是一个非常强大的状态管理工具,可以帮助你在多个Widget和页面之间共享和实时修改数据。下面是一个简单的示例,展示如何使用 Provider
实现多Widget、多页面之间的数据实时修改和共享。
首先,确保在 pubspec.yaml
文件中添加 provider
依赖:
dependencies:\\n flutter:\\n sdk: flutter\\n provider: ^6.0.0\\n
\\n然后运行 flutter pub get
来安装依赖。
我们需要创建一个数据模型类,并使用 ChangeNotifier
来通知监听者数据的变化。
import \'package:flutter/material.dart\';\\n //必须继承ChangeNotifier\\nclass CounterModel with ChangeNotifier {\\n int _count = 0;\\n\\n int get count => _count;\\n\\n void increment() {\\n _count++;\\n notifyListeners(); //写死\\n }\\n\\n void decrement() {\\n _count--;\\n notifyListeners();\\n }\\n}\\n
\\n在应用的顶层使用 ChangeNotifierProvider
来提供 CounterModel
实例。
import \'package:flutter/material.dart\';\\nimport \'package:provider/provider.dart\';\\nimport \'counter_model.dart\';\\n\\nvoid main() {\\n runApp(\\n ChangeNotifierProvider(\\n create: (context) => CounterModel(),\\n child: MyApp(),\\n ),\\n );\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n title: \'Provider Example\',\\n home: HomePage(),\\n );\\n }\\n}\\n
\\n现在你可以在多个Widget中使用 Provider
来访问和修改数据。
class HomePage extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n \\n //还有一种获取方式\\n final counterModel = Provider.of<CounterModel>(context);\\n print(counterModel.count)\\n \\n \\n return Scaffold(\\n appBar: AppBar(\\n title: Text(\'Home Page\'),\\n ),\\n body: Center(\\n child: Column(\\n mainAxisAlignment: MainAxisAlignment.center,\\n children: <Widget>[\\n Text(\\n \'Count:\',\\n style: Theme.of(context).textTheme.headline4, //另一种获取\\n ),\\n Consumer<CounterModel>(\\n builder: (context, counter, child) {\\n return Text(\\n \'${counter.count}\',\\n style: Theme.of(context).textTheme.headline4,\\n );\\n },\\n ),\\n ElevatedButton(\\n onPressed: () {\\n Navigator.push(\\n context,\\n MaterialPageRoute(builder: (context) => SecondPage()),\\n );\\n },\\n child: Text(\'Go to Second Page\'),\\n ),\\n ],\\n ),\\n ),\\n floatingActionButton: FloatingActionButton(\\n onPressed: () {\\n Provider.of<CounterModel>(context, listen: false).increment(); //修改数据\\n },\\n tooltip: \'Increment\',\\n child: Icon(Icons.add),\\n ),\\n );\\n }\\n}\\n
\\nclass SecondPage extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: Text(\'Second Page\'),\\n ),\\n body: Center(\\n child: Column(\\n mainAxisAlignment: MainAxisAlignment.center,\\n children: <Widget>[\\n Text(\\n \'Count:\',\\n style: Theme.of(context).textTheme.headline4,\\n ),\\n Consumer<CounterModel>(\\n builder: (context, counter, child) {\\n return Text(\\n \'${counter.count}\',\\n style: Theme.of(context).textTheme.headline4,\\n );\\n },\\n ),\\n ],\\n ),\\n ),\\n floatingActionButton: FloatingActionButton(\\n onPressed: () {\\n Provider.of<CounterModel>(context, listen: false).decrement();\\n },\\n tooltip: \'Decrement\',\\n child: Icon(Icons.remove),\\n ),\\n );\\n }\\n}\\n
\\n现在你可以运行应用,并在两个页面之间切换,同时实时查看和修改 count
的值。
通过 Provider
,你可以轻松地在多个Widget和页面之间共享和修改数据。ChangeNotifierProvider
和 Consumer
是 Provider
包中常用的工具,它们帮助你管理状态并确保UI在数据变化时自动更新。
package:flutter/services.dart
是 Flutter 中一个非常重要的库,提供了与平台(Android、iOS、Web、桌面等)交互的功能。它包含了访问平台特定功能(如剪贴板、系统导航、键盘事件、通道通信等)的工具类和方法。
以下是 services.dart
中一些核心类和方法的详细讲解:
SystemChannels
是 Flutter 与平台通信的通道类,提供了一系列预定义的通道,用于与平台进行双向通信。
SystemChannels.platform
:用于访问平台的基本功能,如剪贴板、系统导航等。SystemChannels.textInput
:用于与平台的文本输入系统交互。SystemChannels.keyEvent
:用于处理键盘事件。SystemChannels.lifecycle
:用于监听应用的生命周期事件。SystemChannels.navigation
:用于处理系统导航事件(如返回按钮)。SystemChannels.navigation.setMethodCallHandler((call) async {\\n if (call.method == \'popRoute\') {\\n // 用户按下了返回按钮\\n print(\'Back button pressed\');\\n }\\n});\\n
\\nClipboard
类提供了访问系统剪贴板的功能。
Clipboard.setData(ClipboardData data)
:将数据复制到剪贴板。Clipboard.getData(String format)
:从剪贴板获取数据。import \'package:flutter/services.dart\';\\n\\n// 复制文本到剪贴板\\nvoid copyToClipboard(String text) async {\\n await Clipboard.setData(ClipboardData(text: text));\\n print(\'Text copied to clipboard\');\\n}\\n\\n// 从剪贴板获取文本\\nvoid pasteFromClipboard() async {\\n ClipboardData? data = await Clipboard.getData(\'text/plain\');\\n if (data != null) {\\n print(\'Pasted text: ${data.text}\');\\n }\\n}\\n
\\nMethodChannel
是 Flutter 与平台(Android、iOS 等)进行通信的核心工具。它允许 Flutter 调用平台代码,并接收平台的返回值。
MethodChannel.invokeMethod(String method, [dynamic arguments])
:调用平台方法。MethodChannel.setMethodCallHandler(Future<dynamic> handler(MethodCall call))
:设置平台调用 Flutter 的处理程序。import \'package:flutter/services.dart\';\\n\\n// 创建一个 MethodChannel\\nconst platform = MethodChannel(\'com.example.app/channel\');\\n\\n// 调用平台方法\\nvoid getBatteryLevel() async {\\n try {\\n final int result = await platform.invokeMethod(\'getBatteryLevel\');\\n print(\'Battery level: $result%\');\\n } on PlatformException catch (e) {\\n print(\'Failed to get battery level: ${e.message}\');\\n }\\n}\\n
\\nEventChannel
用于从平台向 Flutter 发送事件流(例如传感器数据、网络状态变化等)。
EventChannel.receiveBroadcastStream([dynamic arguments])
:监听平台发送的事件流。import \'package:flutter/services.dart\';\\n\\n// 创建一个 EventChannel\\nconst eventChannel = EventChannel(\'com.example.app/network\');\\n\\n// 监听网络状态变化\\nvoid listenToNetworkChanges() {\\n eventChannel.receiveBroadcastStream().listen((event) {\\n print(\'Network status changed: $event\');\\n }, onError: (error) {\\n print(\'Error: $error\');\\n });\\n}\\n
\\nPlatformException
是 Flutter 与平台通信时抛出的异常类,通常用于捕获平台方法的错误。
code
:错误代码。message
:错误信息。details
:错误的详细信息。try {\\n await platform.invokeMethod(\'someMethod\');\\n} on PlatformException catch (e) {\\n print(\'Error: ${e.code}, ${e.message}\');\\n}\\n
\\nSystemUiOverlayStyle
用于设置系统 UI 的样式,例如状态栏和导航栏的颜色。
statusBarColor
:状态栏背景颜色。statusBarIconBrightness
:状态栏图标亮度(Brightness.light
或 Brightness.dark
)。systemNavigationBarColor
:导航栏背景颜色。systemNavigationBarIconBrightness
:导航栏图标亮度。import \'package:flutter/services.dart\';\\n\\nvoid setSystemUIOverlayStyle() {\\n SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(\\n statusBarColor: Colors.transparent,\\n statusBarIconBrightness: Brightness.dark,\\n systemNavigationBarColor: Colors.white,\\n systemNavigationBarIconBrightness: Brightness.dark,\\n ));\\n}\\n
\\nSystemChrome
提供了控制应用窗口和系统 UI 的功能。
SystemChrome.setEnabledSystemUIMode(SystemUiMode mode)
:设置系统 UI 模式(例如全屏)。SystemChrome.setPreferredOrientations(List<DeviceOrientation> orientations)
:设置应用的首选方向。SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle style)
:设置系统 UI 的样式。import \'package:flutter/services.dart\';\\n\\nvoid enableFullScreen() {\\n SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);\\n}\\n
\\nTextInput
类用于与平台的文本输入系统交互。
TextInput.attach(TextInputConnection connection, TextInputConfiguration configuration)
:连接文本输入系统。TextInput.finishAutofillContext()
:完成自动填充上下文。import \'package:flutter/services.dart\';\\n\\nvoid controlTextInput() {\\n final TextInputConnection connection = TextInput.attach(\\n TextInputClient(),\\n TextInputConfiguration(),\\n );\\n connection.show();\\n}\\n
\\nKeyEvent
类用于处理键盘事件。
RawKeyboard.instance.addListener(ValueChanged<RawKeyEvent> listener)
:监听键盘事件。import \'package:flutter/services.dart\';\\n\\nvoid listenToKeyEvents() {\\n RawKeyboard.instance.addListener((event) {\\n if (event is RawKeyDownEvent) {\\n print(\'Key pressed: ${event.logicalKey}\');\\n }\\n });\\n}\\n
\\nservices.dart
提供了丰富的工具类和方法,用于与平台进行交互。通过它,你可以实现以下功能:
MethodChannel
和 EventChannel
)。这是一款全功能 Flutter 开发脚手架,提供模块化架构与多端适配能力。业务层采用 MVVM 模式,通过 BaseViewModel 实现响应式状态管理及生命周期控制,BaseWidgetPage 统一页面生命周期与 UI 规范。数据层封装网络请求、数据库操作、文件存储及本地缓存,支持加密通信与数据解析。基础组件涵盖屏幕适配、工具类、日志系统、路由管理及事件总线,集成暗黑模式、图片缓存、弹窗队列等实用功能。给你省出一个月的摸鱼时间\\n,欢迎点赞交流
\\n业务层我采取的是借助MVVM(Model-View-ViewModel)模式 ,定义抽象类BaseViewModel
帮我统一管理ViewModel
,定义抽象类BaseWidgetPage
帮我统一管理WidgetPage
的生命周期
BaseViewModel
\\nChangeNotifier
+ Provider
实现自动响应式更新handleError
统一捕获和处理异常BaseWidgetPage
\\nMyAppMethodChannelHandler统一 Channel 管理类
\\nMyAppMethodChannelHandler
主要提供两个函数setMethodCallHandler
和callNativeMethod
我还定义了一个APPChannelModel
类,这是一个数据模型类,用于封装从原生代码接收或发送到原生代码的数据。它包含三个属性:code
、message
和 data
,并提供了 fromJson
和 toJson
方法,用于 JSON 数据和 APPChannelModel
对象之间的转换。
flutter向原生传值,接收到返回值
\\nvoid _postData() async {\\n APPChannelModel _model = APPChannelModel(code: \\"0\\", message: \\"传值成功\\",data: {\\"one\\":\\"1\\"});\\n APPChannelModel? _resultModel = await MyAppMethodChannelHandler.callNativeMethod(method: \\"post_data\\", model: _model);\\n print(\\"flutter向原生传值,接收到返回值:${_resultModel.toJson()}\\");\\n }\\n
\\n监听原生向flutter发送消息
\\nMyAppMethodChannelHandler.setMethodCallHandler(Router_Page_Method,\\n (model, method) async {\\n print(model.toString());\\n print(method);\\n});\\n
\\nColorManager
:适配暗黑模式
// 定义颜色模式枚举\\nenum ColorMode {\\n light,\\n dark,\\n}\\n\\n// 颜色管理类\\nclass ColorManager {}\\n
\\nTextSizeManager
不同屏幕文字大小适配
class TextSizeManager {\\n // 设计稿基准宽度,根据实际设计稿修改\\n static const double baseWidth = 375;\\n\\n // 根据设备宽度计算适配后的文字大小\\n static double getAdaptiveTextSize(BuildContext context, double originalSize) {\\n // 获取当前设备的屏幕宽度\\n double screenWidth = MediaQuery.of(context).size.width;\\n // 计算缩放比例\\n double scale = screenWidth / baseWidth;\\n // 返回适配后的文字大小\\n return originalSize * scale;\\n }\\n\\n // 提供不同字号的获取方法\\n static double getSmallTextSize(BuildContext context) {\\n return getAdaptiveTextSize(context, 12);\\n }\\n\\n static double getMediumTextSize(BuildContext context) {\\n return getAdaptiveTextSize(context, 16);\\n }\\n\\n static double getLargeTextSize(BuildContext context) {\\n return getAdaptiveTextSize(context, 20);\\n }\\n}\\n
\\n\\n实现功能
\\n参考文章:Flutter dio 手把手教你封装一个实用网络工具
\\n\\nCachedImageWidget(imageUrl: _imageUrl, onSuccess: (image,iconUrl){\\n print(\\"图片下载成功:${image},=====${iconUrl}\\");\\n },onError: (error,iconUrl){\\n print(\\"图片下载失败:${error},=====${iconUrl}\\");\\n }),\\n
\\n1、指定缓存目录,缓存有效期、最大缓存数量
\\nMyCustomCacheManager._()\\n : super(Config(\\n key,\\n stalePeriod: const Duration(days: 30), // 缓存有效期\\n maxNrOfCacheObjects: 100, // 最大缓存数量\\n repo: JsonCacheInfoRepository(databaseName: key),\\n ));\\n
\\n2、getFilePath(String imageUrl)
获取本地目录
/// 获取图片本地路径\\n static Future<String?> getFilePath(String imageUrl) async {\\n final FileInfo? fileInfo = await _cacheManager.getFileFromCache(imageUrl);\\n return fileInfo?.file.path;\\n }\\n
\\n3、clearImageCache(String imageUrl)
移除指定路径下图片
/// 移除指定路径下图片\\n static Future<void> clearImageCache(String imageUrl) async {\\n // 移除单个文件的缓存\\n try {\\n await _cacheManager.removeFile(imageUrl);\\n print(\' 移除指定路径下图片已成功移除\');\\n } catch (e) {\\n print(\' 移除指定路径下图片缓存时出错: $e\');\\n }\\n }\\n
\\n4、clearAllCache()
移除所有图片
/// 移除所有图片\\n static Future<void> clearAllCache() async {\\n try {\\n await _cacheManager.emptyCache();\\n print(\'移除所有图片缓存已成功移除\');\\n } catch (e) {\\n print(\'移除所有图片缓存时出错: $e\');\\n }\\n }\\n
\\n5、getCacheSize()
获取缓存大小
/// 获取缓存大小\\n static Future<String> getCacheSize() async {\\n int size = await _cacheManager.store.getCacheSize();\\n double cacheSize = size / 1024 / 1024;\\n return cacheSize.toStringAsFixed(2);\\n }\\n
\\n1、showToast:普通提示信息
\\n/// 提示信息\\n static void showToast(\\n {required String msg, int duration = 2000, bool dismissOnTap = false}) {\\n EasyLoading.showToast(msg,\\n duration: Duration(milliseconds: duration),\\n toastPosition: EasyLoadingToastPosition.center,\\n dismissOnTap: dismissOnTap);\\n }\\n
\\n2、showLoading:loading加载框
\\n/// 加载框\\n static void showLoading({String? msg, bool dismissOnTap = false}) {\\n EasyLoading.instance\\n ..indicatorType = EasyLoadingIndicatorType.ring\\n ..loadingStyle = EasyLoadingStyle.dark\\n ..radius = 5.0\\n ..maskColor = Colors.white.withOpacity(0.1);\\n\\n EasyLoading.show(\\n status: msg,\\n maskType: EasyLoadingMaskType.custom,\\n dismissOnTap: dismissOnTap);\\n }\\n
\\n3、dismiss:隐藏loading
\\n/// 隐藏loading\\n static void dismiss() {\\n if (EasyLoading.isShow) {\\n EasyLoading.dismiss(animation: true);\\n }\\n }\\n
\\n弹窗类型枚举
\\n// 弹窗类型枚举\\nenum DialogType {\\n center, // 中间弹窗\\n bottom, // 底部弹窗\\n}\\n
\\n弹窗队列实现
\\n// 添加弹窗到队列\\n void add({\\n required BuildContext context,\\n required WidgetBuilder builder,\\n DialogType type = DialogType.center,\\n VoidCallback? onDismiss,\\n Color? backgroundColor, // 底部弹窗专用参数\\n ShapeBorder? shape, // 底部弹窗专用参数\\n }) {\\n _queue.add(DialogConfig(\\n context: context,\\n builder: builder,\\n type: type,\\n onDismiss: onDismiss,\\n backgroundColor: backgroundColor,\\n shape: shape,\\n ));\\n\\n _checkNext();\\n }\\n
\\n使用案例
\\nvoid _showQueueDiaLog() {\\n // // 在任意位置添加弹窗\\n DialogQueue().add(\\n context: context,\\n builder: (context) => AlertDialog(\\n title: const Text(\'提示1\'),\\n content: const Text(\'这是第一个弹窗\'),\\n actions: [\\n TextButton(\\n child: const Text(\'关闭\'),\\n onPressed: () => Navigator.pop(context),\\n ),\\n ],\\n ),\\n onDismiss: () => print(\'第一个弹窗关闭\'),\\n );\\n\\n // 添加底部弹窗\\n DialogQueue().add(\\n context: context,\\n type: DialogType.bottom,\\n builder: (_) => CustomBottomSheetContent(),\\n backgroundColor: Colors.grey[100],\\n shape: const RoundedRectangleBorder(\\n borderRadius: BorderRadius.vertical(top: Radius.circular(30))));\\n\\n DialogQueue().add(\\n context: context,\\n builder: (BuildContext context) {\\n return const CustomDialog();\\n },\\n type: DialogType.center,\\n onDismiss: () {\\n print(\'自定义弹窗已关闭\');\\n },\\n );\\n }\\n
\\n\\n刷新组件基于pull_to_refresh_flutter3
封装,支持onRefresh和onLoading回调,是否启用上拉加载,以及子内容。
// 封装的刷新组件\\nclass CustomRefreshWidget<T> extends StatelessWidget {\\n final RefreshController controller;\\n final Future<void> Function() onRefresh;\\n final Future<void> Function()? onLoading;\\n final List<T> dataList;\\n final Widget Function(BuildContext context, int index) itemBuilder;\\n\\n const CustomRefreshWidget({\\n Key? key,\\n required this.controller,\\n required this.onRefresh,\\n this.onLoading,\\n required this.dataList,\\n required this.itemBuilder,\\n }) : super(key: key);\\n\\n Widget headerBuilder(BuildContext context, RefreshStatus? mode) {\\n Widget body;\\n if (mode == RefreshStatus.idle) {\\n body = const Text(\\"下拉刷新\\", style: TextStyle(fontSize: 16));\\n } else if (mode == RefreshStatus.refreshing) {\\n body = const CircularProgressIndicator(\\n valueColor: AlwaysStoppedAnimation<Color>(Colors.grey),\\n );\\n } else if (mode == RefreshStatus.canRefresh) {\\n body = const Text(\\"释放立即刷新\\", style: TextStyle(fontSize: 16));\\n } else if (mode == RefreshStatus.completed) {\\n body = const Text(\\"刷新完成\\", style: TextStyle(fontSize: 16));\\n } else if (mode == RefreshStatus.failed) {\\n body = const Text(\\"刷新失败\\", style: TextStyle(fontSize: 16));\\n } else {\\n body = const Text(\\"未知状态\\", style: TextStyle(fontSize: 16));\\n }\\n return Container(\\n height: 80.0,\\n alignment: Alignment.center,\\n color: Colors.white, // 设置背景颜色\\n child: body,\\n );\\n }\\n\\n Widget footerBuilder(BuildContext context, LoadStatus? mode) {\\n Widget body;\\n if (mode == LoadStatus.idle) {\\n body = const Text(\\n \\"上拉加载\\",\\n style: TextStyle(fontSize: 16),\\n );\\n } else if (mode == LoadStatus.loading) {\\n body = const CircularProgressIndicator();\\n } else if (mode == LoadStatus.failed) {\\n body = const Text(\\"加载失败!点击重试!\\", style: TextStyle(fontSize: 16));\\n } else if (mode == LoadStatus.canLoading) {\\n body = const Text(\\"释放加载更多\\", style: TextStyle(fontSize: 16));\\n } else {\\n body = const Text(\\"没有更多数据了\\", style: TextStyle(fontSize: 16));\\n }\\n return SizedBox(\\n height: 55.0,\\n child: Center(child: body),\\n );\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return RefreshConfiguration(\\n springDescription:\\n const SpringDescription(stiffness: 200, damping: 20, mass: 2.0),\\n // 调整弹簧动画属性\\n maxOverScrollExtent: 80,\\n // 减少最大下拉距离\\n maxUnderScrollExtent: 0,\\n enableScrollWhenRefreshCompleted: true,\\n enableLoadingWhenFailed: true,\\n hideFooterWhenNotFull: false,\\n enableBallisticLoad: true,\\n child: SmartRefresher(\\n controller: controller,\\n enablePullDown: true,\\n enablePullUp: onLoading != null,\\n header: CustomHeader(builder: headerBuilder),\\n footer: CustomFooter(builder: footerBuilder),\\n onRefresh: onRefresh,\\n onLoading: onLoading,\\n child: ListView.builder(\\n itemCount: dataList.length,\\n itemBuilder: itemBuilder,\\n ),\\n ),\\n );\\n }\\n}\\n
\\n1、插入数据
\\n/*\\n int id = await dbHelper.insert({\'name\': \'Alice\'}, \'my_table\');\\n print(\'Inserted with ID: $id\');\\n * */\\n // 插入数据\\n Future<int> insert(Map<String, dynamic> row, String tableName) async {\\n Database db = await database;\\n return await db.insert(tableName, row);\\n }\\n
\\n2、 查询所有数据
\\n/*\\n List<Map<String, dynamic>> allRows = await dbHelper.queryAll(\'my_table\');\\n print(\'All rows: $allRows\');\\n * */\\n // 查询所有数据\\n Future<List<Map<String, dynamic>>> queryAll(String tableName) async {\\n Database db = await database;\\n return await db.query(tableName);\\n }\\n
\\n3、根据条件查询数据
\\n/*\\n // 根据条件查询数据\\n List<Map<String, dynamic>> filteredRows = await dbHelper.query(\\n \'my_table\',\\n where: \'name = ?\',\\n whereArgs: [\'Alice\'],\\n );\\n print(\'Filtered rows: $filteredRows\');\\n * */\\n // 根据条件查询数据\\n Future<List<Map<String, dynamic>>> query(String tableName,\\n {String? where,\\n List<dynamic>? whereArgs,\\n String? orderBy,\\n int? limit,\\n int? offset}) async {\\n Database db = await database;\\n return await db.query(\\n tableName,\\n where: where,\\n whereArgs: whereArgs,\\n orderBy: orderBy,\\n limit: limit,\\n offset: offset,\\n );\\n }\\n
\\n4、更新数据
\\n/*\\n int updatedRows = await dbHelper.update(\\n \'my_table\',\\n {\'name\': \'Bob\'},\\n \'id = ?\',\\n whereArgs: [id],\\n );\\n print(\'Updated $updatedRows rows\');\\n * */\\n // 更新数据\\n Future<int> update(String tableName, Map<String, dynamic> row, String where,\\n {List<dynamic>? whereArgs}) async {\\n Database db = await database;\\n return await db.update(\\n tableName,\\n row,\\n where: where,\\n whereArgs: whereArgs,\\n );\\n }\\n
\\n5、删除数据
\\n/*\\n int deletedRows = await dbHelper.delete(\\n \'my_table\',\\n \'id = ?\',\\n whereArgs: [id],\\n );\\n print(\'Deleted $deletedRows rows\');\\n * */\\n // 删除数据\\n Future<int> delete(String tableName, String where,\\n {List<dynamic>? whereArgs}) async {\\n Database db = await database;\\n return await db.delete(\\n tableName,\\n where: where,\\n whereArgs: whereArgs,\\n );\\n }\\n\\n
\\n6、关闭数据库
\\n// 关闭数据库\\n Future close() async {\\n Database db = await database;\\n return db.close();\\n }\\n
\\n1、写入文件
\\n/// 写入文件\\n Future<void> writeFile({required String fileName,required String content, String? moduleName}) async {\\n try {\\n final file = await _localFile(fileName: fileName, moduleName: moduleName);\\n Log.debug(\\"文件地址:${file.path}\\");\\n // 等待写入操作完成\\n await file.writeAsString(content);\\n } on PlatformException catch (e) {\\n Log.debug(\'写入文件时发生平台异常: ${e.message}\');\\n rethrow;\\n } on FileSystemException catch (e) {\\n Log.debug(\'文件系统写入出错: ${e.message}\');\\n rethrow;\\n } catch (e) {\\n Log.debug(\'文件写入失败: $e\');\\n rethrow;\\n }\\n }\\n
\\n2、追加内容到文件
\\n // 追加内容到文件\\n Future<void> appendToFile({required String fileName,required String content, String? moduleName}) async {\\n try {\\n final file = await _localFile(fileName: fileName, moduleName: moduleName);\\n // 以追加模式写入内容\\n await file.writeAsString(content, mode: FileMode.append);\\n Log.debug(\'内容已成功追加到文件: ${file.path}\');\\n } on PlatformException catch (e) {\\n Log.debug(\'追加内容时发生平台异常: ${e.message}\');\\n rethrow;\\n } on FileSystemException catch (e) {\\n Log.debug(\'文件系统操作出错: ${e.message}\');\\n rethrow;\\n } catch (e) {\\n Log.debug(\'追加内容到文件时出现未知错误: $e\');\\n rethrow;\\n }\\n }\\n
\\n3、读取文件
\\n/// 读取文件\\n Future<String?> getFile({required String fileName, String? moduleName}) async {\\n try {\\n final file = await _localFile(fileName: fileName, moduleName: moduleName);\\n Log.debug(\\"读取文件路径:${file.path}\\");\\n String contents = await file.readAsString();\\n return contents;\\n } on PlatformException catch (e) {\\n Log.debug(\'读取文件时发生平台异常: ${e.message}\');\\n return null;\\n } on FileSystemException catch (e) {\\n Log.debug(\'文件系统读取出错: ${e.message}\');\\n return null;\\n } catch (e) {\\n Log.debug(\'文件读取失败: $e\');\\n return null;\\n }\\n }\\n
\\n4、移除指定文件
\\n/// 移除指定文件\\n Future<bool> removeFilePath({required String fileName, String? moduleName}) async {\\n try {\\n final file = await _localFile(fileName: fileName,moduleName: moduleName);\\n // 检查文件是否存在\\n if (await file.exists()) {\\n // 移除文件\\n await file.delete();\\n Log.debug(\'文件删除成功: ${file.path}\');\\n return true;\\n } else {\\n Log.debug(\'文件不存在,无需删除: ${file.path}\');\\n return false;\\n }\\n } on PlatformException catch (e) {\\n Log.debug(\'删除文件时发生平台异常: ${e.message}\');\\n return false;\\n } on FileSystemException catch (e) {\\n Log.debug(\'文件系统删除出错: ${e.message}\');\\n return false;\\n } catch (e) {\\n Log.debug(\'移除文件时出现未知错误: $e\');\\n return false;\\n }\\n }\\n
\\nclass PreferencesHelper {\\n /// 异步设置字符串值\\n static Future<void> setString(String key, String value) async {\\n SharedPreferences prefs = await SharedPreferences.getInstance();\\n prefs.setString(key, value);\\n }\\n\\n /// 异步获取字符串值,带默认值\\n static Future<String?> getString(String key) async {\\n SharedPreferences prefs = await SharedPreferences.getInstance();\\n String? value = prefs.getString(key);\\n return value;\\n }\\n\\n /// 异步设置整数值\\n static Future<void> setInt(String key, int value) async {\\n SharedPreferences prefs = await SharedPreferences.getInstance();\\n prefs.setInt(key, value);\\n }\\n\\n /// 异步获取整数值,带默认值\\n static Future<int?> getInt(String key) async {\\n SharedPreferences prefs = await SharedPreferences.getInstance();\\n int? value = prefs.getInt(key);\\n return value;\\n }\\n\\n /// 异步设置布尔值\\n static Future<void> setBool(String key, bool value) async {\\n SharedPreferences prefs = await SharedPreferences.getInstance();\\n prefs.setBool(key, value);\\n }\\n\\n /// 异步获取布尔值,带默认值\\n static Future<bool?> getBool(String key) async {\\n SharedPreferences prefs = await SharedPreferences.getInstance();\\n bool? value = prefs.getBool(key);\\n return value;\\n }\\n\\n/// 异步设置双精度浮点数值\\n static Future<void> setDouble(String key, double value) async {\\n SharedPreferences prefs = await SharedPreferences.getInstance();\\n prefs.setDouble(key, value);\\n }\\n\\n /// 异步获取双精度浮点数值,带默认值\\n static Future<double?> getDouble(String key) async {\\n SharedPreferences prefs = await SharedPreferences.getInstance();\\n double? value = prefs.getDouble(key);\\n return value;\\n }\\n\\n /// get keys.\\n /// 获取sp中所有的key\\n static Future<Set<String>> getKeys() async {\\n SharedPreferences prefs = await SharedPreferences.getInstance();\\n return prefs.getKeys();\\n }\\n\\n /// remove.\\n /// 移除sp中key的值\\n static Future<bool> remove(String key) async {\\n SharedPreferences prefs = await SharedPreferences.getInstance();\\n return await prefs.remove(key);\\n }\\n\\n /// 清除所有键值对\\n static Future<void> clear() async {\\n SharedPreferences prefs = await SharedPreferences.getInstance();\\n await prefs.clear();\\n }\\n}\\n\\n
\\n主要扩展了下面几个类型转换器
\\n参考文章:Flutter 一招教你解决json_annotation类型解析失败问题
\\n日志Log是基于logger
封装实现的,主要实现一下功能
/// 日志配置选项\\nclass LogOptions {\\n final int methodCount;\\n final int errorMethodCount;\\n final int lineLength;\\n final bool colors;\\n final bool printEmojis;\\n final bool printTime;\\n\\n LogOptions({\\n this.methodCount = 0,\\n this.errorMethodCount = 8,\\n this.lineLength = 120,\\n this.colors = true,\\n this.printEmojis = true,\\n this.printTime = false,\\n });\\n}\\n
\\n1、路由跳转
\\nstatic Future<T?> router<T extends Object?>(\\n {required RouterURL routerURL,\\n required BuildContext context,\\n Map<String, dynamic>? param,\\n MyRouterEnum routerType = MyRouterEnum.push}) {\\n final name = routerURL.name;\\n Map<String, dynamic> queryParameters = param ?? Map<String, dynamic>();\\n if (routerType == MyRouterEnum.push) {\\n return context.pushNamed(name, queryParameters: queryParameters);\\n } else {\\n context.goNamed(name, queryParameters: queryParameters);\\n return Future.value();\\n }\\n }\\n
\\n2、pop 返回
\\n static void pop<T extends Object?>(BuildContext context, [T? result]) {\\n if (context.canPop()) {\\n context.pop(result);\\n } else {\\n assert(false, \'不能pop\');\\n }\\n }\\n
\\n3、返回到指定界面
\\nstatic void popUntil(\\n {required BuildContext context, required RouterURL routerURL}) {\\n try {\\n List<Route<dynamic>> list = getAllRoutes();\\n bool isCanPop = false;\\n for (Route _router in list) {\\n if(_router.settings.name == routerURL.name) {\\n isCanPop = true;\\n }\\n }\\n\\n if (isCanPop) {\\n final name = routerURL.name;\\n Navigator.popUntil(context, ModalRoute.withName(name));\\n } else {\\n assert(false, \'不能pop\');\\n }\\n\\n } catch (e) {\\n Log.error(\\"返回到指定界面错误:${e.toString()}\\");\\n }\\n }\\n
\\n4、获取当前路由栈里面的全部路由
\\n/// 获取当前路由栈里面的全部路由\\n static List<Route<dynamic>> getAllRoutes() {\\n final MyRouteObserver routeObserver = MyRouteObserver();\\n List<Route<dynamic>> routes = routeObserver.routeStack;\\n return routes;\\n }\\n
\\nclass MyRouteObserver extends NavigatorObserver {\\n static final MyRouteObserver _instance = MyRouteObserver._internal();\\n\\n factory MyRouteObserver() {\\n return _instance;\\n }\\n\\n MyRouteObserver._internal();\\n\\n final List<Route<dynamic>> routeStack = [];\\n final Map<Route<dynamic>, List<RouteAware>> _routeAwareSubscriptions = {};\\n\\n /// 订阅路由变化\\n void subscribe(RouteAware routeAware, Route<dynamic> route) {\\n _routeAwareSubscriptions.putIfAbsent(route, () => []).add(routeAware);\\n }\\n\\n /// 取消订阅路由变化\\n void unsubscribe(RouteAware routeAware) {\\n for (final route in _routeAwareSubscriptions.keys) {\\n _routeAwareSubscriptions[route]?.remove(routeAware);\\n }\\n }\\n\\n /// 当一个新的路由被推送到导航栈时,此方法会被调用。\\n @override\\n void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {\\n super.didPush(route, previousRoute);\\n routeStack.add(route);\\n Log.debug(\\n \'新的路由被推送到导航栈: ${route.settings.name} param:${route.settings.arguments}, previousRoute= ${previousRoute?.settings.name}\');\\n _handleRouteVisibility(previousRoute, route);\\n }\\n\\n /// 当一个路由从导航栈中弹出时,此方法会被调用。route 参数表示被弹出的路由,previousRoute 参数\\n @override\\n void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {\\n super.didPop(route, previousRoute);\\n routeStack.remove(route);\\n Log.debug(\\n \'路由被弹出,当前路由堆栈: ${route.settings.name},param:${route.settings.arguments}, previousRoute= ${previousRoute?.settings.name}\');\\n _handleRouteVisibility(route, previousRoute);\\n }\\n\\n /// 当一个路由从导航栈中被移除时,此方法会被调用。移除路由和弹出路由不同,移除操作可以移除导航栈中任意位置的路由,而弹出操作只能移除栈顶的路由。\\n /// route 参数表示被移除的路由,previousRoute 参数表示在该路由移除后,其下一个路由(如果存在的话)。\\n @override\\n void didRemove(Route<dynamic> route, Route<dynamic>? previousRoute) {\\n super.didRemove(route, previousRoute);\\n routeStack.remove(route);\\n\\n Log.debug(\\n \'路由被移除,当前路由堆栈: ${route.settings.name}, previousRoute= ${previousRoute?.settings.name}\');\\n _handleRouteVisibility(route, previousRoute);\\n }\\n\\n @override\\n void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) {\\n super.didReplace(newRoute: newRoute, oldRoute: oldRoute);\\n if (oldRoute != null) {\\n routeStack.remove(oldRoute);\\n }\\n if (newRoute != null) {\\n routeStack.add(newRoute);\\n }\\n Log.debug(\\n \'路由被替换,当前路由堆栈: new= ${newRoute?.settings.name}, old= ${oldRoute?.settings.name}\');\\n _handleRouteVisibility(oldRoute, newRoute);\\n }\\n\\n /// 当用户开始进行一个导航手势(如在 iOS 上从屏幕边缘向左滑动返回上一页)时,此方法会被调用。\\n /// route 参数表示当前正在操作的路由,previousRoute 参数表示在手势操作后可能会显示的前一个路由(如果存在的话)。\\n @override\\n void didStartUserGesture(\\n Route<dynamic> route, Route<dynamic>? previousRoute) {\\n Log.debug(\'手势事件 didStartUserGesture: ${route.settings.name}, \'\\n \'previousRoute= ${previousRoute?.settings.name}\');\\n }\\n\\n /// 用户结束导航手势时,此方法会被调用。无论手势是否成功完成导航操作,只要手势结束,就会触发这个方法。\\n @override\\n void didStopUserGesture() {\\n Log.debug(\'手势结束:didStopUserGesture\');\\n }\\n\\n /// 处理路由可见性变化\\n void _handleRouteVisibility(\\n Route<dynamic>? oldRoute, Route<dynamic>? newRoute) {\\n if (oldRoute != null) {\\n _notifyRouteAware(oldRoute, false);\\n }\\n if (newRoute != null) {\\n _notifyRouteAware(newRoute, true);\\n }\\n }\\n\\n /// 通知订阅者路由可见性变化\\n void _notifyRouteAware(Route<dynamic> route, bool isVisible) {\\n final routeAwares = _routeAwareSubscriptions[route];\\n if (routeAwares != null) {\\n for (final routeAware in routeAwares) {\\n if (isVisible) {\\n routeAware.didPush();\\n } else {\\n routeAware.didPopNext();\\n }\\n }\\n }\\n }\\n}\\n
\\n/// 路由映射url\\nclass RouterURL {\\n /// 名称\\n final String name;\\n /// 路径\\n final String path;\\n\\n const RouterURL({required this.name, required this.path});\\n}\\n\\n
\\nAppEventBus.instance
访问核心功能EventBusMixin
自动取消订阅StreamSubscription
便于管理// 封装后的高级事件总线\\nclass AppEventBus {\\n static final EventBus _instance = EventBus();\\n\\n // 私有构造,确保单例\\n AppEventBus._internal();\\n\\n /// 获取单例实例\\n static EventBus get instance => _instance;\\n\\n /// 发送事件\\n static void sendEvent<T>(T event) {\\n if (kDebugMode) {\\n print(\'[EventBus] Firing event: ${event.runtimeType}\');\\n }\\n instance.fire(event);\\n }\\n\\n /// 订阅事件,返回可取消的订阅对象\\n static StreamSubscription<T> on<T>(void Function(T event) handler, {\\n bool handleError = true,\\n ErrorCallback? onError,\\n }) {\\n final subscription = instance.on<T>().listen((event) {\\n if (kDebugMode) {\\n print(\'[EventBus] Received event: ${event.runtimeType}\');\\n }\\n _safeRun(() => handler(event), onError: onError);\\n }, onError: handleError ? (error, stack) {\\n _safeRun(() => onError?.call(error, stack));\\n } : null);\\n\\n return subscription;\\n }\\n\\n static void _safeRun(void Function() action, {ErrorCallback? onError}) {\\n try {\\n action();\\n } catch (e, s) {\\n if (kDebugMode) {\\n print(\'[EventBus] Handler error: $e\\\\n$s\');\\n }\\n onError?.call(e, s);\\n }\\n }\\n}\\n\\n/// Flutter Widget 集成扩展\\nmixin EventBusMixin<T extends StatefulWidget> on State<T> {\\n final List<StreamSubscription> _eventSubscriptions = [];\\n\\n /// 安全订阅事件,自动管理生命周期\\n void subscribe<Event>(void Function(Event event) handler, {\\n bool handleError = true,\\n ErrorCallback? onError,\\n }) {\\n _eventSubscriptions.add(\\n AppEventBus.on<Event>(handler, handleError: handleError, onError: onError)\\n );\\n }\\n\\n @override\\n void dispose() {\\n for (final sub in _eventSubscriptions) {\\n sub.cancel();\\n }\\n if (kDebugMode) {\\n print(\'[EventBus] Canceled ${_eventSubscriptions.length} subscriptions\');\\n }\\n super.dispose();\\n }\\n}\\n\\ntypedef ErrorCallback = void Function(Object error, StackTrace stackTrace);\\n
\\nStream可以简单的处理数据流,但遇到更复杂的需求时,发现原生Stream的操作符不够用。这个时候我们就可以借助于RxDart。RxDart可以提供更多的操作符的链式调用、错误处理、流的组合。
\\nclass RxStream<T> {\\n final BehaviorSubject<T> _subject = BehaviorSubject<T>();\\n\\n Stream<T> get stream => _subject.stream;\\n\\n // 添加数据\\n void add(T value) => _subject.sink.add(value);\\n\\n // 链式操作符示例:防抖 + 过滤空值\\n Stream<T> debounceAndFilter(Duration duration) {\\n return stream\\n .debounceTime(duration) // 防抖\\n .where((value) => value != null); // 过滤空值\\n }\\n\\n // 合并多个流(例如:搜索输入 + 筛选条件)\\n static Stream<R> combineStreams<A, B, R>(\\n Stream<A> streamA,\\n Stream<B> streamB,\\n R Function(A, B) combiner,\\n ) {\\n return Rx.combineLatest2(streamA, streamB, combiner);\\n }\\n\\n // 关闭资源\\n void dispose() => _subject.close();\\n}\\n\\n
\\n参考文章:flutter 流(Stream)介绍&结合RxDart使用
\\nclass ScreenAdapter {\\n // 初始化屏幕适配\\n static void init(BuildContext context, {double width = 375, double height = 812}) {\\n ScreenUtil.init(\\n context,\\n designSize: Size(width, height),\\n );\\n }\\n\\n // 获取屏幕宽度\\n static double get screenWidth => ScreenUtil().screenWidth;\\n\\n // 获取屏幕高度\\n static double get screenHeight => ScreenUtil().screenHeight;\\n\\n // 获取状态栏高度\\n static double get statusBarHeight => ScreenUtil().statusBarHeight;\\n\\n // 获取底部安全区高度\\n static double get bottomBarHeight => ScreenUtil().bottomBarHeight;\\n\\n // 适配宽度\\n static double setWidth(double width) {\\n return width.w;\\n }\\n\\n // 适配高度\\n static double setHeight(double height) {\\n return height.h;\\n }\\n\\n // 适配字体大小\\n static double setSp(double fontSize) {\\n return fontSize.sp;\\n }\\n}\\n
\\n// 为 int 类型添加扩展\\nextension IntScreenExtensions on int {\\n /// 转换为适配后的像素值\\n double get px => toDouble().w;\\n\\n /// 转换为适配后的响应式像素值(这里使用与 px 相同逻辑,可按需调整)\\n double get rpx => toDouble().w;\\n}\\n\\n// 为 double 类型添加扩展\\nextension DoubleScreenExtensions on double {\\n /// 转换为适配后的像素值\\n double get px => w;\\n\\n /// 转换为适配后的响应式像素值(这里使用与 px 相同逻辑,可按需调整)\\n double get rpx => w;\\n}\\n \\n
\\n1、自动获取焦点
\\n2、限制长度
\\n3、手机号格式化
\\n1、将 Map 转换为 String
\\n2、 将 String 转换为 Map
\\n3、 将 List 转换为 String
\\n4、将 String 转换为 List
\\nThe Android Gradle plugin supports only kotlin-android-extensions Gradle plugin version 1.6.20 and higher.\\nThe following dependencies do not satisfy the required version:\\nproject \':map_launcher\' -> org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31
\\ncoreLibraryDesugaringEnabled = true\\n
\\ncoreLibraryDesugaring \'com.android.tools:desugar_jdk_libs:1.2.2\' // 请检查最新版本\\n
\\n// 强制指定Kotilin版本\\nsubprojects {\\n project.buildDir = \\"${rootProject.buildDir}/${project.name}\\"\\n afterEvaluate {\\n if (it.hasProperty(\'android\')) {\\n if (it.android.namespace == null) {\\n def manifest = new XmlSlurper().parse(file(it.android.sourceSets.main.manifest.srcFile))\\n def packageName = manifest.@package.text()\\n// println(\\"Setting ${packageName} as android namespace\\")\\n android.namespace = packageName\\n }\\n // 指定Kotilin版本\\n def javaVersion = JavaVersion.VERSION_17\\n android {\\n compileOptions {\\n sourceCompatibility javaVersion\\n targetCompatibility javaVersion\\n }\\n tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {\\n kotlinOptions {\\n jvmTarget = javaVersion.toString()\\n }\\n }\\n// println(\\"Setting java version to ${javaVersion.toString()} which is $javaVersion\\")\\n }\\n }\\n }\\n}\\n
\\nWhat went wrong: A problem occurred evaluating root project \'android\'. > A problem occurred configuring project \':app\'. > Could not create an instance of type com.android.build.api.variant.impl.ApplicationVariantImpl. > Namespace not specified. Specify a namespace in the module\'s build file: /Users/jiayuanfa/Desktop/Code/client_mobile/android/app/build.gradle. See d.android.com/r/tools/upg… for information about setting the namespace. If you\'ve specified the package attribute in the source AndroidManifest.xml, you can use the AGP Upgrade Assistant to migrate to the namespace value in the build file. Refer to d.android.com/r/tools/upg… for general information about using the AGP Upgrade Assistant. * Try: > Run with --stacktrace option to get the stack trace. > Run with --info or --debug option to get more log output. > Run with --scan to get full insights. > Get more help at help.gradle.org.
\\n用户提供的上下文中有多个关于Kotlin插件版本1.9.23的引用,但主要错误与namespace配置无关,因此可能需要检查AGP版本是否至少为7.0。不过用户的问题重点在于如何解决这个namespace缺失的问题。
\\n// The below script\\nsubprojects {\\n afterEvaluate { project ->\\n if (project.hasProperty(\'android\')) {\\n project.android {\\n if (namespace == null) {\\n namespace project.group\\n }\\n }\\n }\\n }\\n}\\n
\\n./gradlew clean \\nrm -rf .gradle/ \\nrm -rf build/ \\n
\\n./gradlew build\\n
\\n./gradlew build\\n
\\nflutter clean && rm -rf build/\\n
\\n首先,我需要确认用户对基本步骤的理解程度。用户已经提到了使用Android Studio内置的Gradle,但可能遇到了问题,所以现在想通过命令行操作。我需要一步步详细说明如何使用./gradlew
执行构建和运行应用等操作。
接下来,我应该考虑用户可能的环境:他们可能在Linux或macOS下,因为使用zsh以及./gradlew
的语法。所以步骤要针对这些系统,但也要注意通用性。
用户可能已经安装了Gradle在Android Studio中,但之前的错误提示显示命令缺失,所以可能需要提醒他们确认项目目录下确实存在gradlew
文件,以及文件权限是否正确。另外,用户可能之前已经解决了如何安装Gradle的问题,所以现在可以假设他们已经设置了正确的路径或使用Gradle Wrapper。
接下来,我需要分步骤说明如何执行常见任务,比如清理、构建、安装应用到设备上。每个步骤需要具体命令,并解释其作用。例如:
\\n./gradlew clean
./gradlew assembleDebug
或 ./gradlew installDebug
./gradlew run
?不过可能不同项目路径不同,需要更详细的说明,比如./gradlew :app:run
或者配置的启动器Activity名称。同时,用户可能需要知道如何查看日志,调试命令,以及常见的构建问题处理。例如,添加--info
或--stacktrace
参数来获取更多信息。
另外,是否需要区分不同构建变体(Debug/Release)?用户可能需要知道如何指定构建类型。
\\n还需要提到,如果项目中有多个模块,可能需要指定模块名,比如./gradlew :app:assembleDebug
。
还要注意,用户可能没有连接设备或模拟器,需要提示他们确认设备是否可用,或者如何启动模拟器。
\\n最后,可能需要给出一些常见问题的解决办法,比如权限问题导致的zsh: command not found
,需要赋予执行权限chmod +x gradlew
。
还需要确保用户理解和执行每个步骤的顺序,以及可能出现的常见错误提示和解决方案。
\\n总结步骤列表,确保清晰简洁,分点说明,使用代码块格式标注命令,可能还需要解释每个命令的作用,让用户明白为什么要这样操作。
\\n\\n以下是使用 ./gradlew
命令在命令行中运行 Android 项目的详细步骤及常用命令,适用于 Linux/macOS(Windows 用户可以替换为 gradlew.bat
):
首先确保你的终端位于 项目根目录(即包含 build.gradle
和 settings.gradle
的目录):
cd /path/to/your/project\\n
\\n使用以下命令执行常见操作:
\\n清除之前构建的中间文件(如 .apk
、.class
等):
./gradlew clean\\n
\\n构建 APK(Debug 版本)并生成二进制文件:
\\n./gradlew assembleDebug # 或 ./gradlew build(包含测试和构建)\\n
\\napp/build/outputs/apk/debug/app-debug.apk
若已经连接设备,直接安装 APK:
\\n./gradlew installDebug # 安装 Debug 版本\\n# 或\\n./gradlew uninstallDebug # 卸载 Debug 版本\\n
\\n若需直接启动应用(仅支持 Android Studio 插件支持的项目):
\\n./gradlew run # 默认运行主模块\\n# 或指定模块:\\n./gradlew :app:run\\n
\\n同步 Gradle 配置(如依赖更新后):
\\n./gradlew --no-daemon # 强制重新同步\\n
\\n查看项目所有可用的 Gradle 任务:
\\n./gradlew tasks # 或 tasks --all\\n
\\n如果需要通过命令启动模拟器:
\\nemulator -avd <设备名称> # 例如:emulator -avd Pixel_6_API_34\\n
\\n添加运行参数提高输出详细程度:
\\n./gradlew build --info # 详细输出\\n./gradlew build --debug # 调试级输出\\n./gradlew build --stacktrace # 错误堆栈跟踪\\n
\\n如项目配置了 productFlavors
,可指定变种构建:
./gradlew assembleDevDebug # 构建名为 \\"dev\\" 的 Debug 变种\\n
\\n若项目有多个模块,运行特定模块的构建:
\\n./gradlew :module_name:assembleDebug\\n
\\n./gradlew -version --gradle-user-home=/path/to/cache\\n
\\n./gradlew: not found
gradlew
文件。chmod +x gradlew
。设备未连接或未识别
\\nadb devices
确认设备列表。AVD Manager
或命令行启动模拟器。构建超时或内存不足
\\ngradle.properties
中增加内存:\\norg.gradle.jvmargs=-Xmx4096m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError\\n
\\n# 清理并构建 Debug 版本\\n./gradlew clean && ./gradlew assembleDebug\\n\\n# 安装到设备\\n./gradlew installDebug\\n\\n# 或单条命令组合\\n./gradlew clean assembleDebug installDebug\\n
\\n通过上述命令,你可以完全通过终端控制项目构建、安装和调试。如需更复杂的操作(如发布版本构建、测试执行),可参考 ./gradlew tasks --all
的任务列表进一步探索。
首先,我需要确认什么是R8。R8是Android的代码缩减和混淆工具,替换原有的ProGuard。AGP通常会自带R8,但用户可能在项目的依赖中显式指定了另一个版本,导致版本冲突。
\\n接下来,分析警告的信息。用户使用的R8版本(8.3.37)比AGP自带的版本(8.9.27)旧。这可能导致在构建过程中出现不可预知的问题,比如代码缩减失败或优化不当。此外,R8的版本更新通常包含重要的修复和优化,旧版本可能存在已知问题。
\\n用户为什么会遇到这个问题呢?可能是在项目的级build.gradle文件中手动添加了R8的dependency,或者某个第三方库强制依赖了旧版本的R8。这时候需要检查项目的dependencies,移除显式的R8依赖,让AGP使用内置的版本。
\\nYou are applying Flutter\'s app_plugin_loader Gradle plugin imperatively using the apply script method, which is deprecated and will be removed in a future release. Migrate to applying Gradle plugins with the declarative plugins block: flutter.dev/to/flutter-…
\\nYou are applying Flutter\'s main Gradle plugin imperatively using the apply script method, which is deprecated and will be removed in a future release. Migrate to applying Gradle plugins with the declarative plugins block: flutter.dev/to/flutter-…
\\nYour project is configured with Android NDK 23.1.7779620, but the following plugin(s) depend on a different Android NDK version:
\\nalipay_kit_android requires Android NDK 27.0.12077973
\\naudio_session requires Android NDK 27.0.12077973
\\ncamera_android requires Android NDK 27.0.12077973
\\ndevice_info_plus requires Android NDK 27.0.12077973
\\nemoji_picker_flutter requires Android NDK 27.0.12077973
\\nffmpeg_kit_flutter_full_gpl requires Android NDK 27.0.12077973
\\nfile_picker requires Android NDK 27.0.12077973
\\nflutter_app_installer requires Android NDK 27.0.12077973
\\nflutter_background requires Android NDK 27.0.12077973
\\nflutter_downloader requires Android NDK 27.0.12077973
\\nflutter_image_compress_common requires Android NDK 27.0.12077973
\\nflutter_keyboard_visibility requires Android NDK 27.0.12077973
\\nflutter_local_notifications requires Android NDK 27.0.12077973
\\nflutter_native_splash requires Android NDK 27.0.12077973
\\nflutter_new_badger requires Android NDK 27.0.12077973
\\nflutter_openim_sdk requires Android NDK 27.0.12077973
\\nflutter_plugin_android_lifecycle requires Android NDK 27.0.12077973
\\ngeolocator_android requires Android NDK 27.0.12077973
\\ngetuiflut requires Android NDK 27.0.12077973
\\nimage_cropper requires Android NDK 27.0.12077973
\\nimage_gallery_saver_plus requires Android NDK 27.0.12077973
\\nimage_picker_android requires Android NDK 27.0.12077973
\\njust_audio requires Android NDK 27.0.12077973
\\nlocal_auth_android requires Android NDK 27.0.12077973
\\nmap_launcher requires Android NDK 27.0.12077973
\\nmobile_scanner requires Android NDK 27.0.12077973
\\nmop requires Android NDK 27.0.12077973
\\nopen_filex requires Android NDK 27.0.12077973
\\npackage_info_plus requires Android NDK 27.0.12077973
\\npath_provider_android requires Android NDK 27.0.12077973
\\npermission_handler_android requires Android NDK 27.0.12077973
\\nphoto_manager requires Android NDK 27.0.12077973
\\nrecord_android requires Android NDK 27.0.12077973
\\nscan requires Android NDK 27.0.12077973
\\nsensors_plus requires Android NDK 27.0.12077973
\\nshare_plus requires Android NDK 27.0.12077973
\\nshared_preferences_android requires Android NDK 27.0.12077973
\\nsound_mode requires Android NDK 27.0.12077973
\\nsqflite_android requires Android NDK 27.0.12077973
\\nuri_to_file requires Android NDK 27.0.12077973
\\nurl_launcher_android requires Android NDK 27.0.12077973
\\nvibration requires Android NDK 27.0.12077973
\\nvideo_compress requires Android NDK 27.0.12077973
\\nvideo_player_android requires Android NDK 27.0.12077973
\\nwakelock_plus requires Android NDK 27.0.12077973
\\nwebview_flutter_android requires Android NDK 27.0.12077973\\nFix this issue by using the highest Android NDK version (they are backward compatible).\\nAdd the following to /Users/jiayuanfa/Desktop/Code/client_mobile/android/app/build.gradle:
\\nandroid {\\nndkVersion = \\"27.0.12077973\\"\\n...\\n}
\\nError: The \'kotlin-android-extensions\' Gradle plugin is no longer supported. Please use this migration guide (goo.gle/kotlin-andr…) to start working with View Binding (developer.android.com/topic/libra…) and the \'kotlin-parcelize\' plugin.
\\n其实 PageView 嵌套 PageView 并不算很罕见的场景,例如网易云音乐这种大量的应用这些场景,如果单独放在 Android 和 iOS来实现都很简单,例如 Android 的 ViewPager 嵌套 ViewPager 默认就支持嵌套滚动。\\n当子 ViewPager 滑动到最后一页再滑动就会触发父 ViewPager 的滑动,非常的丝滑。
\\n就算一些复杂的场景我们可以自行实现NestedScrollingParent和NestedScrollingChild来自定义嵌套滚动,而 Flutter 中并没有很好的嵌套滚动自定义实现,只能用它自带的 NestedScrollView 控件来实现。
\\n以下图为示例
\\n如果只是想要做到 PageVie 嵌套 PageView 的场景好像只能自己手撕嵌套滚动的事件处理。
\\n如果我们尝试做一个 PageView 嵌套 PageView 的示例其实很简单,我们会发现事件基本上一直都在子 PageView 上在移动到最后一页的时候无法触发父 PageView 的事件。
\\n如果你问 AI 他会告诉你用 GestureDetector 或者 NotificationListener 来实现监听,这都是行不通的。
\\n因为 PageView 内部已经处理了事件不能再监听事件,如果只是监听索引也只能修改布局的属性无法达到嵌套滚动的效果。
\\n嵌套滑动判断优先级应以最先接收到事件的控件先做响应,比如说如果我先收到的是子 PageView 的滑动事件,在无法滑动或者事件处理完之前,外部的 PageView 都应该是无法响应的。
\\n那么在子 PageView 上在移动到最后一页的时候如何把事件传递给父 PageView 呢?
\\n如果要自定义就需要按照 NestedScrollerView 中_NestedScrollCoordinator 的做法,创建一个类,继承 ScrollActivityDelegate 和 ScrollHoldController,并修改 position 的方法,将其中drag,hold等需要协调处理的方法交由这个新类实现,而不是像默认的 NestedScrollerView 中根据滑动方向判断优先级的方式。
\\n在applyUserOffset 方法中,判断子 Page 的 position ,其 pixels 加上滑动距离是否大于 maxScrollExtent ,通过这个简单判断一下是不是 overScroll ,并将事件根据结果分发给子 PageView 或者父 PageView 在 goBallistic 方法中,我是根据滑动方向判断子 PageView 是否会 overScroll。
\\n所以整体的逻辑就是由最底层的子 PageView 负责计算,当遇到需要嵌套滑动的时候,计算出滑动结果并调用父 PageView 的 controller,其实就是判断一下是否是过度滑动,在需要的情况下交给父 ScrollerController 处理,基本的动画、复位算法,已经有现成方法实现,过渡滑动则需要 controller 所绑定的 position 中携带的pixel、maxScrollExtent、minScrollExtent等信息来判断。
\\n子 PageVie 怎么拿到父 PageView 的 controller 进而操作父 PageView 的滑动则需要参考 NestedScrollerView-PrimaryScrollerController 的 controller 传递方式,父 PageView 只需要将自己的controller 放入 PrimaryScrollerController 这个 InHeritedWidget 即可。
\\n具体代码在此【传送门】 感谢大佬的开源版本。
\\n这个版本比较老,支持的Flutter的版本还需要我们自己修改一下代码。不过总的来说效果还是不能满足我的需求,有时候划着划着就丢失了Page,滑动不够丝滑并且滑动的惯性也没有处理。
\\n再然后我到 pub 社区也没有找到比较好的方案,只能看后期能不能官方支持一下了。
\\n我突然想到既然 PageView 的嵌套有坑,那我不嵌套了,直接铺平用一层的 PageView 不就好了吗?
\\n监听 PageView 的滚动根据索引的变化切换顶部的 Tab 不就可以了吗?
\\nimport \'package:cpt_payment/modules/payment/payment_view_model.dart\';\\nimport \'package:cs_resources/generated/assets.dart\';\\nimport \'package:cs_resources/generated/l10n.dart\';\\nimport \'package:cs_resources/theme/app_colors_theme.dart\';\\nimport \'package:cs_resources/theme/theme_config.dart\';\\nimport \'package:flutter/material.dart\';\\nimport \'package:auto_route/auto_route.dart\';\\nimport \'package:flutter/services.dart\';\\nimport \'package:hooks_riverpod/hooks_riverpod.dart\';\\nimport \'package:router/ext/auto_router_extensions.dart\';\\nimport \'package:shared/utils/log_utils.dart\';\\nimport \'package:widgets/ext/ex_widget.dart\';\\nimport \'package:widgets/my_appbar.dart\';\\nimport \'package:widgets/my_load_image.dart\';\\nimport \'package:widgets/my_text_view.dart\';\\n\\nimport \'../../../router/page/payment_page_router.dart\';\\n\\n@RoutePage()\\nclass PaymentPage extends HookConsumerWidget {\\n const PaymentPage({Key? key}) : super(key: key);\\n\\n //启动当前页面\\n static void startInstance({BuildContext? context}) {\\n if (context != null) {\\n context.router.push(const PaymentPageRoute());\\n } else {\\n appRouter.push(const PaymentPageRoute());\\n }\\n }\\n\\n @override\\n Widget build(BuildContext context, WidgetRef ref) {\\n final viewModel = ref.read(paymentViewModelProvider.notifier);\\n int selectedInnerIndex = 1; // 1-3索引记录当前的值\\n\\n return Scaffold(\\n appBar: MyAppBar.appBar(\\n context,\\n S.current.facility,\\n backgroundColor: context.appColors.whiteBG,\\n ),\\n backgroundColor: context.appColors.backgroundDark,\\n body: AutoTabsRouter.pageView(\\n routes: const [\\n InfoPageRoute(),\\n CondoPaymentPageRoute(),\\n CondoActivePageRoute(),\\n CondoHistoryPageRoute(),\\n ManagePageRoute(),\\n ],\\n builder: (context, child, pageController) {\\n final tabsRouter = AutoTabsRouter.of(context);\\n\\n //监听赋值内部的选中索引\\n pageController.addListener(() {\\n if (tabsRouter.activeIndex >= 1 && tabsRouter.activeIndex <= 3) {\\n selectedInnerIndex = tabsRouter.activeIndex;\\n }\\n });\\n\\n return Column(\\n children: [\\n Container(\\n color: context.appColors.whiteBG,\\n height: 120,\\n child: Row(\\n mainAxisAlignment: MainAxisAlignment.spaceAround,\\n children: [\\n _buildTopCategory(\\n context,\\n Assets.paymentInfoIcon,\\n 34,\\n 41,\\n \\"Info\\",\\n tabsRouter.activeIndex == 0,\\n ).onTap(\\n () {\\n tabsRouter.setActiveIndex(0);\\n },\\n ),\\n _buildTopCategory(\\n context,\\n Assets.paymentCondoIcon,\\n 48,\\n 43,\\n \\"Condo\\",\\n tabsRouter.activeIndex == 1 || tabsRouter.activeIndex == 2 || tabsRouter.activeIndex == 3,\\n ).onTap(\\n () {\\n tabsRouter.setActiveIndex(selectedInnerIndex);\\n },\\n ),\\n _buildTopCategory(\\n context,\\n Assets.paymentManageIcon,\\n 52,\\n 46.5,\\n \\"Manage\\",\\n tabsRouter.activeIndex == 4,\\n ).onTap(\\n () {\\n tabsRouter.setActiveIndex(4);\\n },\\n ),\\n ],\\n ),\\n ),\\n Expanded(\\n child: Column(\\n children: [\\n if (tabsRouter.activeIndex >= 1 && tabsRouter.activeIndex <= 3)\\n Row(\\n mainAxisAlignment: MainAxisAlignment.spaceAround,\\n children: [\\n _buildInnerTab(\\n context,\\n S.current.payment,\\n tabsRouter.activeIndex == 1,\\n ).onTap(() {\\n tabsRouter.setActiveIndex(1);\\n }),\\n _buildInnerTab(\\n context,\\n S.current.facility_active,\\n tabsRouter.activeIndex == 2,\\n ).onTap(() {\\n tabsRouter.setActiveIndex(2);\\n }),\\n _buildInnerTab(\\n context,\\n S.current.history,\\n tabsRouter.activeIndex == 3,\\n ).onTap(() {\\n tabsRouter.setActiveIndex(3);\\n }),\\n ],\\n ).marginOnly(top: 14, bottom: 17),\\n Expanded(\\n child: child,\\n ),\\n ],\\n ),\\n ),\\n ],\\n );\\n },\\n ),\\n );\\n }\\n\\n //顶部的Tab布局\\n Widget _buildTopCategory(BuildContext context, String iconPath, double iconWidth, double iconHeight, String title, bool isSelected) {\\n return Column(\\n mainAxisAlignment: MainAxisAlignment.center,\\n crossAxisAlignment: CrossAxisAlignment.center,\\n children: <Widget>[\\n Container(\\n width: 70,\\n height: 70,\\n decoration: BoxDecoration(\\n color: context.appColors.lightBlueBg, // 设置圆形背景颜色\\n shape: BoxShape.circle, // 设置为圆形\\n boxShadow: isSelected\\n ? [\\n BoxShadow(\\n color: context.appColors.tabLightBlueShadow, // 设置阴影颜色\\n blurRadius: 5, // 设置模糊半径\\n spreadRadius: 0.05, // 控制阴影扩散\\n offset: const Offset(0, 4), // 设置阴影偏移量\\n ),\\n ]\\n : [], // 未选中时无阴影,\\n ),\\n child: Center(\\n child: MyAssetImage(iconPath, width: iconWidth, height: iconHeight),\\n ),\\n ),\\n const SizedBox(height: 7),\\n MyTextView(\\n title,\\n fontSize: 15,\\n isFontMedium: true,\\n textColor: isSelected ? context.appColors.tabTextSelectedDefault : context.appColors.tabTextUnSelectedDefault,\\n ),\\n ],\\n );\\n }\\n\\n //内部的Tab布局\\n Widget _buildInnerTab(BuildContext context, String title, bool isSelected) {\\n return MyTextView(\\n title,\\n fontSize: 16,\\n isFontMedium: true,\\n textColor: isSelected ? Colors.white : context.appColors.tabTextUnSelectedDefault,\\n backgroundColor: isSelected ? context.appColors.btnBgDefault : Colors.transparent,\\n cornerRadius: 16.5,\\n paddingLeft: 20,\\n paddingRight: 20,\\n paddingTop: 8,\\n paddingBottom: 8,\\n );\\n }\\n}\\n\\n
\\n由于我是用的 AutoRouter 这里使用的 AutoRouter 的子路由控件,之前有讲过【传送门】
\\n路由表的定义如下:
\\n CustomRoute(\\n page: PaymentPageRoute.page,\\n path: RouterPath.payment,\\n transitionsBuilder: applySlideTransition,\\n children: [\\n AutoRoute(page: InfoPageRoute.page, path: \'info\'),\\n AutoRoute(page: CondoPaymentPageRoute.page, path: \'payment\'),\\n AutoRoute(page: CondoActivePageRoute.page, path: \'active\'),\\n AutoRoute(page: CondoHistoryPageRoute.page, path: \'history\'),\\n AutoRoute(page: ManagePageRoute.page, path: \'manage\'),\\n ],\\n ),\\n\\n
\\n这里没有嵌套 PageView 了只是一层,当然了这是 AutoRouter 的用法,你完全可以自己实现 PageView 的嵌套自己实现监听也是一样的效果。
\\n问题:由于是有的父PageView没有子Tab,所以导致的是子Tab有显示隐藏的动画效果,显得稍微有点突兀,如果是父布局全部的子页面都有子Tab,那么就显得好一些:
\\n本文记录了 PageView 嵌套的两种方案,都只是权宜之计,并不是很完美的实现。
\\n关于自定义控件的方案还需要完善细节并且实现physics的动画效果,还需要处理惯性的滚动,这一点也难做,关于铺平 PageView 的思路主要是在 子PageView 切换到 父 PageView 的时候动画效果的缺失导致的突兀效果。
\\n后期的优化思路其实可以参考PageView的滚动Transform进度进而对子 PageView 上的 TabView 进行对应的动画操作从而达到平滑的过渡效果。
\\n就目前来说我们也只能用第二种方案了,后期在进行优化一下,当然如果你有其他的更多的更好的实现方式,也希望大家能评论区交流一起学习进步。
\\nOK,那么今天的分享就到这里啦,如果我的文章有错别字,不通顺的,或者代码、注释、有错漏的地方,同学们都可以指出修正。
\\n如果感觉本文对你有一点的启发和帮助,还望你能点赞
支持一下,你的支持对我真的很重要。
这一期就此完结了。
\\nflutter项目用到了ffmpeg-kit,但是2025年年初因为版权问题,官方放弃维护并且删除了部分二进制文件,导致现在项目编译不过,升级到最新版本6.0可以暂时规避这个问题,但是很快也会被删除。
\\n代码仓库地址:github.com/arthenica/f…
\\nffmpeg_kit_flutter: 5.1.0
\\n执行pod install时报错(如果不报错,可能是因为本地有缓存,先执行:pod cache clear --all)
\\n[!] Error installing ffmpeg-kit-ios-https\\n[!] /usr/bin/curl -f -L -o /var/folders/mk/9gycp1yd0w9dz33s9tf0mm4w0000gn/T/d20250318-13472-ix6xgk/file.zip https://github.com/arthenica/ffmpeg-kit/releases/download/v5.1/ffmpeg-kit-https-5.1-ios-xcframework.zip --create-dirs --netrc-optional --retry 2 -A \'CocoaPods/1.15.2 cocoapods-downloader/2.1\'\\n
\\nfork原仓库,但是部分依赖是动态下载的,即使是fork了仓库代码,仍然会找不到这个文件
\\nPod::Spec.new do |s|\\n s.name = \'ffmpeg_kit_flutter\'\\n s.version = \'6.0.3\'\\n s.summary = \'FFmpeg Kit for Flutter\'\\n s.description = \'A Flutter plugin for running FFmpeg and FFprobe commands.\'\\n s.homepage = \'https://github.com/arthenica/ffmpeg-kit\'\\n s.license = { :file => \'../LICENSE\' }\\n s.author = { \'ARTHENICA\' => \'open-source@arthenica.com\' }\\n\\n s.platform = :ios\\n s.requires_arc = true\\n s.static_framework = true\\n\\n s.source = { :path => \'.\' }\\n s.source_files = \'Classes/**/*\'\\n s.public_header_files = \'Classes/**/*.h\'\\n\\n s.default_subspec = \'https\'\\n\\n s.dependency \'Flutter\'\\n s.pod_target_xcconfig = { \'DEFINES_MODULE\' => \'YES\', \'EXCLUDED_ARCHS[sdk=iphonesimulator*]\' => \'i386\' }\\n\\n s.subspec \'min\' do |ss|\\n ss.source_files = \'Classes/**/*\'\\n ss.public_header_files = \'Classes/**/*.h\'\\n ss.dependency \'ffmpeg-kit-ios-min\', \\"6.0\\"\\n ss.ios.deployment_target = \'12.1\'\\n end\\n\\n s.subspec \'min-lts\' do |ss|\\n ss.source_files = \'Classes/**/*\'\\n ss.public_header_files = \'Classes/**/*.h\'\\n ss.dependency \'ffmpeg-kit-ios-min\', \\"6.0.LTS\\"\\n ss.ios.deployment_target = \'10\'\\n end\\n\\n s.subspec \'min-gpl\' do |ss|\\n ss.source_files = \'Classes/**/*\'\\n ss.public_header_files = \'Classes/**/*.h\'\\n ss.dependency \'ffmpeg-kit-ios-min-gpl\', \\"6.0\\"\\n ss.ios.deployment_target = \'12.1\'\\n end\\n\\n s.subspec \'min-gpl-lts\' do |ss|\\n ss.source_files = \'Classes/**/*\'\\n ss.public_header_files = \'Classes/**/*.h\'\\n ss.dependency \'ffmpeg-kit-ios-min-gpl\', \\"6.0.LTS\\"\\n ss.ios.deployment_target = \'10\'\\n end\\n\\n s.subspec \'https\' do |ss|\\n ss.source_files = \'Classes/**/*\'\\n ss.public_header_files = \'Classes/**/*.h\'\\n ss.dependency \'ffmpeg-kit-ios-https\', \\"6.0\\"\\n ss.ios.deployment_target = \'12.1\'\\n end\\n\\n s.subspec \'https-lts\' do |ss|\\n ss.source_files = \'Classes/**/*\'\\n ss.public_header_files = \'Classes/**/*.h\'\\n ss.dependency \'ffmpeg-kit-ios-https\', \\"6.0.LTS\\"\\n ss.ios.deployment_target = \'10\'\\n end\\n\\n s.subspec \'https-gpl\' do |ss|\\n ss.source_files = \'Classes/**/*\'\\n ss.public_header_files = \'Classes/**/*.h\'\\n ss.dependency \'ffmpeg-kit-ios-https-gpl\', \\"6.0\\"\\n ss.ios.deployment_target = \'12.1\'\\n end\\n\\n s.subspec \'https-gpl-lts\' do |ss|\\n ss.source_files = \'Classes/**/*\'\\n ss.public_header_files = \'Classes/**/*.h\'\\n ss.dependency \'ffmpeg-kit-ios-https-gpl\', \\"6.0.LTS\\"\\n ss.ios.deployment_target = \'10\'\\n end\\n\\n s.subspec \'audio\' do |ss|\\n ss.source_files = \'Classes/**/*\'\\n ss.public_header_files = \'Classes/**/*.h\'\\n ss.dependency \'ffmpeg-kit-ios-audio\', \\"6.0\\"\\n ss.ios.deployment_target = \'12.1\'\\n end\\n\\n s.subspec \'audio-lts\' do |ss|\\n ss.source_files = \'Classes/**/*\'\\n ss.public_header_files = \'Classes/**/*.h\'\\n ss.dependency \'ffmpeg-kit-ios-audio\', \\"6.0.LTS\\"\\n ss.ios.deployment_target = \'10\'\\n end\\n\\n s.subspec \'video\' do |ss|\\n ss.source_files = \'Classes/**/*\'\\n ss.public_header_files = \'Classes/**/*.h\'\\n ss.dependency \'ffmpeg-kit-ios-video\', \\"6.0\\"\\n ss.ios.deployment_target = \'12.1\'\\n end\\n\\n s.subspec \'video-lts\' do |ss|\\n ss.source_files = \'Classes/**/*\'\\n ss.public_header_files = \'Classes/**/*.h\'\\n ss.dependency \'ffmpeg-kit-ios-video\', \\"6.0.LTS\\"\\n ss.ios.deployment_target = \'10\'\\n end\\n\\n s.subspec \'full\' do |ss|\\n ss.source_files = \'Classes/**/*\'\\n ss.public_header_files = \'Classes/**/*.h\'\\n ss.dependency \'ffmpeg-kit-ios-full\', \\"6.0\\"\\n ss.ios.deployment_target = \'12.1\'\\n end\\n\\n s.subspec \'full-lts\' do |ss|\\n ss.source_files = \'Classes/**/*\'\\n ss.public_header_files = \'Classes/**/*.h\'\\n ss.dependency \'ffmpeg-kit-ios-full\', \\"6.0.LTS\\"\\n ss.ios.deployment_target = \'10\'\\n end\\n\\n s.subspec \'full-gpl\' do |ss|\\n ss.source_files = \'Classes/**/*\'\\n ss.public_header_files = \'Classes/**/*.h\'\\n ss.dependency \'ffmpeg-kit-ios-full-gpl\', \\"6.0\\"\\n ss.ios.deployment_target = \'12.1\'\\n end\\n\\n s.subspec \'full-gpl-lts\' do |ss|\\n ss.source_files = \'Classes/**/*\'\\n ss.public_header_files = \'Classes/**/*.h\'\\n ss.dependency \'ffmpeg-kit-ios-full-gpl\', \\"6.0.LTS\\"\\n ss.ios.deployment_target = \'10\'\\n end\\n\\nend\\n
\\niOS中, ffmpeg_kit_flutter.podspec 文件可以看出来,ffmpeg_kit_flutter 依赖了 ffmpeg-kit-ios-xxx
\\n其中xxx可以是min,https等,默认是https,正好对应了下载报错的ffmpeg-kit-https-5.1-ios-xcframework.zip 文件,所以只需要把这个依赖改成本地,就可以解决报错问题
\\n修改依赖
\\n # s.default_subspec = \'https\'\\n # FFmpegKit has been officially retired.Place iOS dependent libraries locally to solve compilation problems\\n s.default_subspec = \'ffmpeg_kit_ios_local\'\\n\\n s.subspec \'ffmpeg_kit_ios_local\' do |ss|\\n ss.vendored_frameworks = \'Frameworks/ffmpeg-kit-ios-https/ffmpegkit.xcframework\', \'Frameworks/ffmpeg-kit-ios-https/libavdevice.xcframework\', \'Frameworks/ffmpeg-kit-ios-https/libavcodec.xcframework\', \'Frameworks/ffmpeg-kit-ios-https/libavfilter.xcframework\', \'Frameworks/ffmpeg-kit-ios-https/libavformat.xcframework\', \'Frameworks/ffmpeg-kit-ios-https/libavutil.xcframework\', \'Frameworks/ffmpeg-kit-ios-https/libswresample.xcframework\', \'Frameworks/ffmpeg-kit-ios-https/libswscale.xcframework\'\\n end\\n
\\n增加本地依赖
\\n获取版本依赖包,如果是未删除的,可以从pod install --verbose 的安装信息中拿到下载地址,如果已经被删除了,并且本地之前有安装过,可以从本地缓存获取
\\n把缓存文件放到代码目录:/code-path/ffmpeg-kit/flutter/flutter/ios/Frameworks/ffmpeg-kit-ios-https
\\n此时目录下包含ffmpeg-kit及ffmpeg的依赖
\\n编译运行,控制台输入信息,成功
\\n Loading ffmpeg-kit.\\n Loaded ffmpeg-kit-https-arm64-5.1-20220929.\\n flutter: Loaded ffmpeg-kit-flutter-ios-https-arm64-5.1.0.\\n
\\n定制分支,不同分支对应不同版本,方便业务使用
\\n以上修改已经放到fork的仓库中:github.com/carl-design…
\\n使用方式:
\\n之前的使用代码:
\\nffmpeg_kit_flutter: 5.1.0\\nor\\nffmpeg_kit_flutter: 6.0.3\\n
\\n新的使用代码:
\\nffmpeg_kit_flutter:\\n git:\\n url: git@github.com:carl-designlibro/ffmpeg-kit.git\\n path: flutter/flutter\\n ref: flutter_fix_retired_v6.0.3 # For version 6.0.3 \\n # ref: flutter_fix_retired_v5.1.0 # For version 5.1.0 \\n
\\n只处理了5.1.0和6.0.3的https版本
\\n安卓端暂时未报错,推测安卓是aar,aar内部集成了so,托管在maven平台的,所以正常
","description":"背景 flutter项目用到了ffmpeg-kit,但是2025年年初因为版权问题,官方放弃维护并且删除了部分二进制文件,导致现在项目编译不过,升级到最新版本6.0可以暂时规避这个问题,但是很快也会被删除。\\n\\n代码仓库地址:github.com/arthenica/f…\\n\\n分析\\n依赖\\n\\nffmpeg_kit_flutter: 5.1.0\\n\\n错误信息\\n\\n执行pod install时报错(如果不报错,可能是因为本地有缓存,先执行:pod cache clear --all)\\n\\n[!] Error installing ffmpeg-kit-ios-https\\n[!]…","guid":"https://juejin.cn/post/7482753184654721074","author":"白话666","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-18T05:49:34.914Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/33a590e920224638b796d2505d43ab71~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55m96K-dNjY2:q75.awebp?rk3s=f64ab15b&x-expires=1742885798&x-signature=wJ6g50KoRJHRTA3KDaUxf4sWzUo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/aa9586b2d55b4ee686af16d5dbfa40e0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg55m96K-dNjY2:q75.awebp?rk3s=f64ab15b&x-expires=1742885798&x-signature=8sZF5kMY7%2BQQPC5dUg7rbxl0glM%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["前端","Flutter","FFmpeg"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 工程——核心概念","url":"https://juejin.cn/post/7482723688673640474","content":"欢迎来到激动人心的 Flutter 工程世界!本章将探讨构成 Flutter 成功软件开发基础的基本原则和核心概念。通过本章的学习,您将深入了解 Flutter 工程的独特视角和方法,这些特点使其区别于传统编程模式,并帮助您掌握构建高效且持久应用的关键知识。
\\n在我的软件工程职业生涯中,拥抱 Flutter 标志着我在技术方法上的重大演进。它不仅仅是一项新技能的掌握,更是一种涵盖整个软件开发生命周期的全面策略——从设计、开发到测试与维护。
\\n我在多种技术领域的经验,使我能够以更广阔的视角来看待 Flutter。我认为它不仅是一个技术工具,更是一种推动软件开发创新和创造力的方式。Flutter 工程采用整体性方法,在用户体验、时间管理效率、可扩展性考量以及影响深远的软件开发权衡之间,精心保持平衡。
\\nFlutter 的多平台架构使开发者能够专注于打造卓越的用户体验,而无需深入处理各个平台的细节。与原生开发不同,原生开发强调遵循平台规范,而 Flutter 更加关注品牌一致性和用户体验。这种方法鼓励开发者优先考虑通用的可用性,而非受限于平台差异,从而培养更以用户为中心的思维方式。
\\n在 Flutter 工程中,用户体验(UX)是评估每个项目的重要视角。我经常自问:“用户如何看待这个功能?它是提升了用户体验,还是增加了复杂度?” 例如,在开发基于 Flutter 的教育应用时,回答这些问题有助于使设计直观且富有吸引力,使学习者能够轻松上手。平衡美观与功能性,是确保应用既实用又愉悦用户的关键挑战。用户体验的核心目标是将用户的需求与 Flutter 的技术能力相结合,确保应用既易于交互,又具备强大的功能性。
\\n时间是软件开发中的关键资源。它是有限的约束,需要被高效管理,以满足项目的交付期限并为利益相关者创造价值。然而,时间管理不仅仅是按时完成任务,在 Flutter 开发中,它还是一个多维度的动态因素。我经常思考:“我的 Flutter 代码的生命周期有多长?这个应用会存续多久?是一年,还是十年?我们的交付截止日期是什么?” 这些问题不仅关乎项目进度,还涉及应用的长期可维护性和可持续发展。
\\n例如,在开发基于 Flutter 的智能家居应用时,我不仅关注其短期发布,还要考虑其对未来物联网(IoT)趋势和技术演进的适应性。这种方法确保应用能够随时间推移保持相关性,并适应用户行为和技术变化。此外,它提醒我在设计时预留升级 Flutter 及其他第三方依赖的机制,以确保应用的长期稳定性和可维护性。
\\n在 Flutter 工程中,可扩展性的概念既复杂又深思熟虑。每当开启一个新项目时,我都会思考:“有多少人参与这个项目?他们在开发和维护中扮演什么角色?未来会有多少终端用户使用这个应用?” 这些问题在大型项目中尤为重要,比如开发一个基于 Flutter 的综合物流应用。在这样的项目中,挑战在于如何管理庞大的代码库,并协调一个由不同领域专家组成的团队,以确保跨平台和多设备上的开发高效且一致。
\\nFlutter 工程中的取舍涉及在多个项目因素之间做出战略性决策。例如,我经常面临这样的选择:“我是否应该实现一个高级但资源密集的功能,它能提升用户体验,但可能会影响某些设备的性能?” 例如,在开发一款游戏应用时,需要在高分辨率图形与流畅性能之间做出权衡;或者,在选择实现一个复杂的动画效果来增强用户体验,还是保持应用轻量、快速加载之间做出决定。这类权衡不仅是技术性的,同时也需要与项目的整体目标和用户期望保持一致。
\\n在我的经验中,使用 Flutter 进行软件工程是一项精细的工作,旨在打造兼具适应性和可扩展性的解决方案,并能够真正触达终端用户。这一过程融合了技术能力、战略规划和创造性的解决问题思维,目标是在快速发展的数字化世界中构建既功能强大,又具有吸引力和可持续性的应用。
\\n为了全面理解这些概念,我们将从核心软件工程原则的角度来探讨 Flutter 应用开发。
\\n在软件开发中,各种思想和方法论指导着系统的构建。这些指导原则,即开发范式,提供了不同的视角,使开发者能够以独特的方式构思和塑造软件。
\\n不同的编程语言通常与特定的开发范式相关,而编程语言的选择会影响开发者思考问题和解决问题的方式。例如,某些语言严格遵循面向对象(OOP)或函数式编程(FP),而 Dart 这样的语言则支持多种开发范式,使开发者能够灵活选择最适合的方式来构建应用。
\\n纵观计算机发展的历史,出现了多种著名的开发范式,每种范式都在软件工程领域留下了深远的影响。其中包括 过程式编程(Procedural Programming)、面向对象编程(OOP)、函数式编程(Functional Programming)、敏捷开发(Agile Development)、事件驱动编程(Event-Driven Programming)、命令式编程(Imperative Programming)和声明式编程(Declarative Programming) 等。
\\n然而,这些编程范式很少是彼此孤立的。Flutter 采用多范式编程环境,在不同场景下灵活运用各种编程技术,以发挥它们的最大优势。让我们深入探讨这一多元方法中的几个关键要素:
\\nFlutter 设计的核心在于 组合(Composition) 。这种方法通过组合简单的组件(widgets)来构建复杂的 UI 结构。例如,TextButton
组件本质上是由 Material
、InkWell
和 Padding
等多个组件组合而成的。
可以将你的应用想象成一座巨大的 乐高积木(Lego) 作品。每个组件(如文本、按钮、图片)都是一个小巧而专业的积木块,彼此拼接在一起,构建出复杂的界面。这种 强组合(aggressive composition) 的方法,使得 UI 具有高度的 可定制性(customizability) 和 灵活性(flexibility) 。关于这一点,我们将在 第 2 章 进行更深入的探讨。
\\n在 Flutter 中,布局系统采用了一种 约束编程(Constraint Programming) 来确定 UI 元素的几何结构。父组件会向子组件传递 大小约束(size constraints) ,例如最小宽度、最大高度等,而子组件则会根据这些约束调整自身尺寸,从而适应整体布局。Flutter 的这一布局机制通常可以在 单次遍历(single pass) 中完成整个 UI 的计算,从而提高布局效率。这种方法确保了应用在不同设备上都能呈现出 自适应(responsive) 且 一致(consistent) 的界面体验。
\\n在 Flutter 中,命令式编程(Imperative Programming) 适用于需要直接控制执行步骤的场景。例如,移动应用的业务逻辑通常涉及 顺序执行(sequences of steps)、条件判断(conditions) 和 循环(loops) 。命令式编程使开发者能够以自然的方式表达这些逻辑,从而提高代码的可读性和可维护性。
\\n以下是一个典型的 命令式风格(imperative style) 的函数示例,包含条件判断语句:
\\nbool isPositive(int x) {\\n if (x > 0) {\\n print(\'x is positive\');\\n return true;\\n }\\n print(\'x is negative or zero\');\\n return false;\\n}\\n
\\n在 Flutter 中,单元测试(unit testing) 也是命令式编程的一个常见应用。例如,以下代码展示了如何使用 testWidgets
进行 Flutter 组件测试:
testWidgets(\'CustomButton displays a label\', (WidgetTester tester) async {\\n // 加载测试组件\\n await tester.pumpWidget(MaterialApp(home: CustomButton(label: \'Test\')));\\n\\n // 断言测试结果\\n expect(find.text(\'Test\'), findsOneWidget);\\n\\n // 触发交互事件\\n await tester.tap(find.byType(CustomButton));\\n await tester.pump();\\n});\\n
\\n在该测试中,开发者 逐步控制(step-by-step control) 组件的加载、交互和 UI 响应,这正是命令式编程的典型特征。
\\n声明式编程是 Flutter 框架的一个关键特性,尤其体现在小部件(widgets)的构建方式上。在 Flutter 中,UI 通常使用 Dart 的声明式语法来定义,其中小部件的 build
方法由单一的表达式和嵌套的构造函数组成。
考虑以下 ListView
小部件的示例:
ListView(\\n children: [\\n ListTile(title: Text(\'Item 1\')),\\n ListTile(title: Text(\'Item 2\')),\\n // 其他列表项\\n ],\\n)\\n
\\n在这个例子中,ListView
及其子项定义得简洁而富有表现力。
这种方法使开发者能够描述 UI 应该呈现的样子,而不是像命令式编程那样逐步构建它。Flutter 中的声明式风格简化了构建复杂 UI 的过程,提升了代码的可读性和可维护性。此外,这种方法可以与命令式编程无缝结合,适用于那些纯声明式方法可能受限的场景,从而为构建更动态和交互式的 UI 提供灵活性。
\\n看这段代码,你可以看到应用程序包含一个 AppBar
和一个居中的文本。它并没有包含指定 UI 如何构建的逻辑,而只是声明用户将看到的内容:
class MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n home: Scaffold(\\n appBar: AppBar(\\n title: Text(\'Declarative Programming in Flutter\'),\\n ),\\n body: Center(\\n child: Text(\'Hello, Flutter!\'),\\n ),\\n ),\\n );\\n }\\n}\\n
\\n函数式编程的核心概念之一是“纯函数”(pure function)。纯函数是指,在相同的输入下,它总是产生相同的输出,并且没有可观察的副作用。纯函数的结果仅取决于其输入参数,并且不会修改任何外部状态,这大大简化了维护工作并为许多优化提供了可能。
\\nFlutter 也采纳了函数式编程,特别是在 StatelessWidget
中,它们类似于纯函数。例如,Icon
小部件可以看作是一个将其参数映射到视觉输出的函数。
Flutter 强调不可变数据结构。整个 Widget
类层次结构以及像 Rect
和 TextStyle
这样的支持类都采用了这种不可变性,使得 UI 保持稳定可靠。
Dart 的 Iterable
API 也是其函数式编程特性的一个例子。还记得你在 Dart 中使用的那些方便的函数吗,如 map
、where
和 reduce
?这些就是框架中常用于处理值列表的函数式风格的例子。
Flutter 的框架同时融合了类继承和动态原型。核心 API 是通过类层次结构构建的,其中像 RenderObject
这样的基类定义了高层次的功能,而像 RenderBox
这样的子类则专注于这些功能,采用笛卡尔坐标系统来处理几何形状。但这不仅仅是静态的继承——ScrollPhysics
类允许你在运行时动态地链接实例,例如将分页物理效果与特定平台的差异进行组合,所有这些都不需要预先选择平台。这种继承与动态灵活性的结合赋予了 Flutter 应用前所未有的适应性和进化能力!
你将在第 3 章中学习更多关于 Dart 中面向对象编程(OOP)的内容。
\\n抽象和封装是软件工程中的基本原则,Flutter 在其以小部件为中心的架构中有效地利用了这两个原则。
\\n抽象是将复杂的系统简化为更易于管理的模型,而封装则是将数据及其相关操作组合到类中,保护数据的完整性,并防止不当访问。
\\n抽象通过简化复杂的 UI 元素为可管理的小部件,专注于核心属性和功能。例如,Flutter 中的 ListView
小部件将一个可滚动列表的复杂功能抽象为一个易于使用的组件。
在 Flutter 中,封装应用于小部件的开发,这个概念在实现 Container
小部件时尤为明显。Container
小部件封装了定义其外观和行为的各种属性或特性。这些属性包括宽度、高度、颜色、内边距、外边距等。开发者通过一组明确定义的属性和方法与 Container
进行交互。封装确保了 Container
如何管理这些属性的内部细节对外界是隐藏的。
抽象与封装共同作用于 Flutter 框架,使得复杂的 UI 设计被简化为可管理的组件,且小部件的内部状态得到良好的保护,从而提高了可用性和可维护性。你将在第 3 章中学习更多关于这些主题的内容。
\\nFlutter 中的用户交互是通过事件驱动的方式处理的。
\\n一个典型的例子是 Flutter 中使用的 Listenable
类。这个类是 Flutter 动画系统的基础,其中动画状态的变化被视为事件。Listenable
提供了一个订阅模型,使多个监听器能够注册回调函数,这些回调函数会在特定事件触发时被调用。这个机制确保了 UI 的各个部分能够保持更新,并与底层数据或状态变化保持同步,反映了框架的响应式特性。
此外,像 GestureDetector
这样的控件和状态管理工具也利用事件来响应用户输入,展示了框架中的事件驱动编程。你将在本书第 2 部分了解更多关于这一点的内容。
在 Flutter 中,响应式编程是推动 UI 开发动态特性的一个关键概念。这个范式体现在控件如何对变化作出反应,更新其状态和外观,以响应用户交互或内部数据变化。
\\n在 Flutter 的响应式系统中,任何在控件构造函数中提供的新输入都会立即触发该控件的重新构建,将变化传播到控件树的下方。相反,底层控件中的变化可以通过事件处理器和状态更新传播到树的上方。
\\nFlutter 利用 Dart 对流的支持,提供了响应式编程模型,StreamBuilder
是该范式中扮演重要角色的一个控件: 响应式编程是一种编程范式,围绕变化的传播和处理异步数据流展开。
Flutter 使用泛型来提高类型安全性并减少错误。这在像 DropdownButton<T>
这样的控件中得以体现,其中 T 代表数据源的类型,或者在像 State<T>
和 GlobalKey<T>
这样的类中,T 代表它们所关联的控件或状态的类型。
Flutter 通过 Dart 的异步特性(如 Futures 和 Streams)来处理并发。这在诸如从网络获取数据或处理长时间运行的任务等场景中至关重要。
\\n你将在第 8 章中进一步了解并发和并行编程。
\\n在软件工程中,内聚性和耦合性是决定系统可维护性和效率的基本原则。
\\n内聚性描述了模块的内部强度,以及模块的元素与其核心目的之间的紧密关系。理想情况下,模块应表现出高内聚性,其中组件共同朝着一个目标工作。而耦合性则关注模块之间的相互依赖程度。追求低耦合性可以确保模块的交互最小化,从而在做出更改时最小化涟漪效应。
\\n在 Flutter 的世界中,两个基本原则定义了可维护的杰作:低耦合和高内聚。让我们来看看这两个原则在 Flutter 中的体现:
\\n高内聚性
\\nFlutter 通过设计专注于特定功能的控件来实现高内聚性。例如,Text
控件仅负责显示一个带有基本样式的文本字符串。它的职责清晰且明确,使其具有很高的内聚性。另一个例子是 Image
控件,它专门用于显示图片,并且不会与非图片功能交织在一起。
低耦合性
\\nFlutter 通过允许控件独立工作,最小化彼此之间的依赖来维持低耦合性。例如,Scaffold
控件提供基础的 Material Design 视觉布局结构,独立于用于操作按钮的 FloatingActionButton
控件。对 FloatingActionButton
的修改,如更改图标或颜色,并不会影响 Scaffold
的布局或功能,展示了这两个组件之间的低耦合性。
你可能会问关于 Theme
的问题。主题主要影响视觉样式,将功能与样式分离。不同层级的可自定义主题加强了低耦合性,确保更改不会将控件紧密绑定。
通常,控件应依赖于已建立的通信渠道,如回调和事件,尽量减少一个控件变化时的级联效应。
\\n在使用 Flutter 开发时,整合“低耦合、高内聚”的原则对于构建一个强大的应用架构至关重要。创建独立操作的控件;例如,PaymentProcessing
控件不应与 UserDashboard
控件紧密关联,体现低耦合性。同时,设计每个控件时要确保其具有专注的角色,比如 ChatScreen
控件专门处理消息功能,确保高内聚性。
在开发过程中,定期问自己:“更改一个控件是否不必要地影响到其他控件?”以及“每个控件的目的和功能是否定义明确且自包含?”思考这些问题将帮助你创建一个更高效、结构更清晰的 Flutter 应用。
\\n关注点分离(SoC)和模块化是软件工程中的基础概念,它们显著提高了代码的组织性、可维护性和可扩展性。
\\n关注点分离是一种设计原则,它涉及将软件应用程序拆分为不同的部分,每部分处理一个特定的方面或关注点。这种方法有助于简化程序的复杂性,使开发者能够专注于一个领域,而不会被其他部分所困扰。它有助于减少相互依赖,从而使应用程序更加灵活且易于维护。而模块化则是指将软件系统分割为独立、可替换的模块,每个模块封装了一个特定的功能。这种设计方法促进了单个组件的测试、调试和更新,从而构建出更加健壮和适应性强的系统。
\\nFlutter 的基于控件的架构本质上是模块化的,每个控件封装了特定的 UI 或功能方面。这与 SoC 原则相一致,在这一架构中,用户界面、业务逻辑和数据管理等关注点是分开的。
\\n作为 Flutter 开发者,你可以利用这些原则来创建稳健和高效的应用。例如,在一个基于 Flutter 的待办应用中,你可以通过将任务显示的 UI 层与 TaskListWidget
控件分开,从而实现 SoC。业务逻辑可以封装在处理任务相关操作的 TaskManager
类中。同时,数据处理可以通过一个负责存储和检索任务数据的 DatabaseService
来管理。
模块化可以通过创建可重用的组件来实现,例如用于用户身份验证的 LoginService
。这些组件可以在应用的不同部分甚至在其他项目中复用,通常存放在 lib
文件夹中,或被提取出来作为独立的 pub 包。
值得一提的是,在 Flutter 中,模块化的概念通常与“按功能划分的包”架构相交织,其独特之处在于模块通常可以以控件的形式存在。这种方法根据特定功能将应用程序组织成多个模块,在许多情况下,每个模块,或者说每个控件,都代表了应用的一个独立功能。
\\n你将在本书的第二部分学习更多关于此内容,我将深入讲解架构。
\\n软件工程中的设计模式是针对常见设计问题的成熟解决方案。它们作为模板,可以应用于软件设计中重复出现的问题,例如管理对象创建、促进对象间的通信以及组织复杂的交互,从而帮助你编写出以下特点的代码:
\\nFlutter 并没有强制要求使用特定的设计模式;其核心功能和架构本身就自然适应了多种设计模式。Flutter 框架中一个非常好的设计模式示例是 建造者模式(Builder Pattern) 。在 Flutter 中,ListView.builder
控件就是建造者模式的一个常见应用。这种模式经常出现在 Flutter 的控件创建过程中。建造者模式将复杂对象的构建与其表示分离,从而使同样的构建过程能够创建不同的表示。
你将在第五章中学习更多关于设计模式的内容。
\\n在软件工程中,尤其是在 Flutter 开发中,理解和应对效率、可扩展性以及权衡的复杂性是至关重要的。这些概念关注的是应用程序如何利用资源(效率)、如何适应增长(可扩展性)以及如何管理竞争需求之间的微妙平衡(权衡)。这些选择不仅仅涉及财务方面,还涵盖了资源分配、人员投入等多种因素。
\\n超越“因为大家都这么做”的思维方式,转向一种优先考虑合理、具体情境决策的共识驱动方法至关重要。在 Flutter 中,这种思维方式尤其重要,特别是在权衡如状态管理技术或集成外部包的选择时。
\\n例如,选择使用 setState
因为它简单易用,可能会导致可扩展性问题;而像 BLoC 这样的高级方法,虽然最初更为复杂,但在可扩展性和可维护性方面带来了长期的好处。同样,使用 cached_network_image
可以提高效率和用户体验,但也带来了复杂性,比如额外的依赖关系,可能影响长期维护和与 Flutter 更新的兼容性。
根据我的经验,“这取决于”在软件工程中尤其重要,尤其是在使用 Flutter 时。这突出了理解每项技术选择的具体背景的重要性。作为开发人员,我不断地在易用性、可扩展性和未来可维护性之间做平衡。这些决策不仅仅关乎短期的效果;它们决定了项目的长期健康。这需要深入的批判性分析和前瞻性思维,强调了在软件开发这个快速发展的领域中,做出明智且可持续决策的重要性。
\\n软件工程中的验证与确认模型是一个用于确保系统满足所有规格并实现预期目标的过程。“验证”(Verification)涉及检查系统是否正确构建,并是否符合指定的要求,这通常被称为“静态测试”。另一方面,“确认”(Validation)检查构建的是正确的系统,并且是否满足用户需求,这被称为“动态测试”。该模型对于确保软件系统的质量和可靠性至关重要。
\\n在 Flutter 的上下文中,验证与确认(V&V)模型可以根据其生态系统进行如下定制:
\\n相应的测试阶段如下:
\\nV&V 模型确保在每个阶段,从需求到验收测试,Flutter 应用程序都能够正确开发并符合设计需求。
\\n软件开发中的“向左转”(Shifting Left)概念,尤其在 Flutter 中,意味着在开发生命周期的早期阶段——例如设计、开发和初步测试——投入时间,比在管道后期处理问题更具成本效益。问题发现得越接近引入的时间点(通常是在开发时间线的左侧),解决问题的成本就越低。这是因为在像预发布或生产环境这样的阶段发现的问题(右侧)可能会因为调试的复杂性以及对用户体验的潜在影响而变得更加昂贵和耗时。
\\n在 Flutter 中,“向左转”实际上意味着采用静态代码分析等实践,及早捕捉语法错误和潜在的 bug。代码审查对于确保质量并捕捉自动化工具可能遗漏的问题至关重要。将自动化集成到持续集成(CI)管道中,可以一致地执行单元测试、小部件测试和集成测试,确保新代码在合并前符合质量标准。此外,使用功能标志和 A/B 测试可以使开发人员在生产环境中选择性地测试新功能,从而降低广泛问题的风险。
\\n通过在 Flutter 开发过程中早期和整个过程中嵌入这些实践,团队可以降低风险,减少后期缺陷修复的成本,并高效地交付高质量、稳健的应用程序。
\\n“向左转”概念强调在开发周期的早期阶段集成这些流程。对于 Flutter 开发者来说,这意味着从最初阶段就开始进行测试和质量检查。这种早期干预有助于及时发现和解决问题,减少通常与后期调试相关的成本和时间。
\\n在 Flutter 中实施这些实践能够提高代码质量,增强应用程序的可靠性和用户体验。
\\n在Flutter和软件开发中,知情决策通常涉及将可量化的因素与更细致、无法量化的方面进行权衡。例如,开发者可能需要在选择使用如BLoC等状态管理解决方案之间做出决策,BLoC提供了可扩展性,但也增加了复杂性;与此相比,setState等更直接的选项更容易实现,但可能无法满足大型应用的扩展需求。
\\n此外,Flutter中的决策有时不仅仅涉及可度量的元素。考虑实现一个自定义小部件与使用现有的第三方小部件之间的选择。这个决策不仅涉及即时功能,还包括长期维护、第三方包的可靠性以及其与应用不断变化需求的契合度等因素。
\\n平衡这些方面需要仔细考虑可量化的影响,以及那些不易量化但同样重要的、对Flutter开发决策产生长期影响的因素。
\\n在将其应用于Flutter开发之前,我们先来了解一下软件开发生命周期(SDLC)。SDLC是一种结构化框架,定义了构建和交付软件应用程序的一系列阶段。它为开发人员和利益相关者提供了一个路线图,确保在整个开发过程中质量、效率和可预测性。
\\nSDLC有多种模型,每种模型都有其特定的阶段和重点。一些常见的模型包括:
\\n瀑布模型:这种线性、顺序的模型遵循严格的阶段门控方法,每个阶段必须在进入下一个阶段之前完成。它适用于明确的需求和受控环境。
\\n敏捷模型:这种迭代和增量的模型强调灵活性和适应性。它将开发过程分解为更小的周期(冲刺),实现持续反馈和可交付的软件。
\\n螺旋模型:这种以风险为驱动的模型结合了敏捷的迭代特性和瀑布模型的控制。它在整个开发周期中进行风险评估,适用于高风险项目。
\\n无论选择哪种模型,SDLC的核心阶段通常包括:
\\n在将软件开发生命周期(SDLC)适应于Flutter开发时,有一些特定的考虑因素需要发挥框架的独特特性。在需求分析阶段,移动优先的方法是关键,但也要关注潜在的扩展到Web和桌面的可能性,得益于Flutter的多功能性。Flutter的热重载功能有助于快速原型设计和迭代反馈,而动画和响应性的性能要求对各种设备的兼容性至关重要。
\\n随着过程进入系统设计、开发、测试和部署阶段,选择适当的小部件和状态管理解决方案以适应应用的复杂性变得至关重要。Dart语言特性,如空安全和小部件层次结构与代码组织中的最佳实践,确保了清晰性和效率。
\\n测试是一个关键阶段,涵盖单元测试、小部件测试和集成测试,以确保稳定性和用户友好性,并进行性能测试以优化应用在不同设备上的表现。最后,部署阶段得益于Flutter能够通过CI/CD管道共享跨平台的代码库,从而实现高效的多平台发布。
\\n请记住,您的具体调整将取决于项目的规模、复杂性和需求。选择最适合您的开发团队和应用目标的工具和实践。
\\n到目前为止,我们已经探讨了软件工程的各个方面,在这一阶段,您应该对这个主题有了更全面的理解。然而,我还想进一步阐述并分享我的观点。
\\n在软件开发中,“Flutter工程”与“编程”代表着项目中的不同角色和职责。编程主要涉及编写代码以实现特定功能,专注于代码实现和问题解决。程序员负责将设计和需求转化为可执行的代码。与此不同,Flutter工程涵盖了更广泛的角色。Flutter工程师不仅编写代码,还设计系统架构和用户界面,并做出战略性的项目结构和可扩展性决策。他们关注代码质量、项目管理和创新,在开发过程中扮演着关键角色。理解这些区别对于有效管理Flutter项目并组建合适的团队至关重要。
\\n在本章结束时,我想表达我对Flutter在技术不断发展的格局中的位置的看法。Flutter在技术世界中占据了一个真正独特且令人兴奋的位置。
\\n在多平台开发日益重要的时代,Flutter作为一个真正的行业颠覆者应运而生。它能够无缝地帮助开发者在不同平台上打造高质量、视觉吸引力强的应用程序,代表了一种范式的转变。该框架对生产力、创造力和效率的关注革新了我们对应用开发的方式,使得开发者和各种规模的企业都能轻松获得应用开发的机会。那句“无论哪里有像素,Flutter都能找到”已经从根本上改变了我们从单一代码库构建跨平台软件的看法。
\\n在这种背景下,Flutter既是一个技术赋能者,也是开发者成长的催化剂。它鼓励开发者拓宽他们的知识和技能,涵盖各种具有独特属性的平台。这种方法提升了应用程序的质量,改善了用户体验,并促使开发者成长为领域内的资深专家。
\\n此外,Flutter在更广泛技术生态系统中的角色具有重要意义。它简化了跨平台开发的复杂性,通过促进创建视觉吸引力强、响应迅速且始终卓越的用户体验,催化了创新。随着我们在接下来的章节中深入探讨Flutter开发,可以明显看出,Flutter不仅仅是一个工具;它是推动软件开发中可实现边界的驱动力。
\\nFlutter在多平台开发中的创新方法使开发者能够专注于创造用户体验,而不是具体的技术或平台。此外,充满活力的Flutter社区在塑造这项技术方面发挥了关键作用,推动了更大的需求和创新。Flutter作为同类技术的杰出典范,继续发展并在软件开发领域留下深远的印记,成为更广泛技术行业的榜样。
\\nFlutter在当下依然相关,并将在未来发挥重要作用。
\\n本章全面探讨了驱动高质量Flutter开发的基本原则和独特理念。我们深入研究了关键的范式,包括抽象、封装、设计模式以及效率和可扩展性的考量。通过早期验证和验证的“左移”概念被强调,为整个开发生命周期中的知情决策奠定了基础。
\\n此外,我们强调了编程与工程之间的区别,展示了Flutter如何促进模块化、关注点分离和深思熟虑的权衡分析。我们还将Flutter放置于更广泛的技术格局中,揭示了与其他跨平台方法相比,Flutter的优势和潜在影响。
\\n在本章结束时,我们必须提出以下问题:我们如何利用这些基础性见解来创建高性能、可维护且具有美学感的Flutter应用?这个问题将成为我们接下来的指南,帮助我们在实践中应用这些原则,并在接下来的章节中打造出色的Flutter体验。
","description":"欢迎来到激动人心的 Flutter 工程世界!本章将探讨构成 Flutter 成功软件开发基础的基本原则和核心概念。通过本章的学习,您将深入了解 Flutter 工程的独特视角和方法,这些特点使其区别于传统编程模式,并帮助您掌握构建高效且持久应用的关键知识。 1.1 使用 Flutter 进行软件工程\\n\\n在我的软件工程职业生涯中,拥抱 Flutter 标志着我在技术方法上的重大演进。它不仅仅是一项新技能的掌握,更是一种涵盖整个软件开发生命周期的全面策略——从设计、开发到测试与维护。\\n\\n我在多种技术领域的经验,使我能够以更广阔的视角来看待 Flutter…","guid":"https://juejin.cn/post/7482723688673640474","author":"数据智能老司机","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-18T05:38:41.536Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/405a69152433437baa480815e2ebe530~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pWw5o2u5pm66IO96ICB5Y-45py6:q75.awebp?rk3s=f64ab15b&x-expires=1742881121&x-signature=e6zmSPrTgzjrIF%2BzWLEBZLA0bME%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5f5e90ee3cfa41c39049d4cfc6c15740~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pWw5o2u5pm66IO96ICB5Y-45py6:q75.awebp?rk3s=f64ab15b&x-expires=1742881121&x-signature=u%2BByLoAvThxpFhbUcDj58b7fGjY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b4940a49e2354397a78d96611d7f94b6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pWw5o2u5pm66IO96ICB5Y-45py6:q75.awebp?rk3s=f64ab15b&x-expires=1742881121&x-signature=tdDtuhL%2FQ4H5o3wydhEL%2FKP6Xec%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/8bb81da7e9b64f17b1f153c447f47987~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pWw5o2u5pm66IO96ICB5Y-45py6:q75.awebp?rk3s=f64ab15b&x-expires=1742881121&x-signature=sxJ8fPrkk1gZodHjlIZze6vg1A0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/45ed48c382e14d9e808596b1849c0af0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pWw5o2u5pm66IO96ICB5Y-45py6:q75.awebp?rk3s=f64ab15b&x-expires=1742881121&x-signature=bhy5eCi7TkmqfqhMaOn0ROdXVeA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2a483e781dfd47259990bf21233da51f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pWw5o2u5pm66IO96ICB5Y-45py6:q75.awebp?rk3s=f64ab15b&x-expires=1742881121&x-signature=6nTa6nV2139sqzg211SujVZwXNg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/97f5d277cdcf43c7a5c06d740f6f3b4f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pWw5o2u5pm66IO96ICB5Y-45py6:q75.awebp?rk3s=f64ab15b&x-expires=1742881121&x-signature=dl4EmYi1jv3OzK4RuuNyUzd7IYg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/82400d005a9a4c8f9ba38a63d9029fdb~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pWw5o2u5pm66IO96ICB5Y-45py6:q75.awebp?rk3s=f64ab15b&x-expires=1742881121&x-signature=%2BLv2BxlkohpMzG2RgUbRBBU8x2A%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["前端","Flutter","前端工程化","架构"],"attachments":null,"extra":null,"language":null},{"title":"Flutter默认字体坑了我们整整一年","url":"https://juejin.cn/post/7482692656989978676","content":"如果你用Flutter做iOS应用,默认的系统字体是 SF Pro Text,但是!它在显示中文字的时候,不支持 w500(中等加粗)!
\\n也就是说,你在代码里设置 FontWeight.w500,中文部分可能根本不会变粗!肉眼看几乎跟普通字重没区别…… 这坑我整整一年才发现!😤
\\n对于Together App来说,最典型的就是我们每个页面的标题,我们其实都用了w500,但是默认情况下,标题并不会变粗一点点。
\\n\\n解决方案:
在主题里面手动指定字体\'PingFang SC\',这样中文也能自动能应用上w500.
\\nMaterialApp(\\n title: \\"Together\\",\\n theme: ThemeData(\\n fontFamily: \\"PingFang SC\\",\\n //....\\n ),\\n)\\n
\\n加上这个以后,标题自然就能变成恰当的粗细了,当然其他用了w500的地方也一样。
\\n为了解决这个问题,我们当时还特地引入了阿里的普惠体,导致包体积无辜增大了好几个M,真是,无发可说!
\\n总结:
\\n👉 如果你在 iOS 上发现中文 w500 没有变粗,要手动指定 PingFang SC!
\\n👉 别被 Flutter 默认字体坑了!这东西可能让你的 UI 一年都不对劲!🔥🔥🔥
\\n这坑了我一年,别让它坑了你!⚠️ 如果你也遇到过 Flutter 的坑问题,一起在评论区交流一下!🗣️
","description":"如果你用Flutter做iOS应用,默认的系统字体是 SF Pro Text,但是!它在显示中文字的时候,不支持 w500(中等加粗)! 也就是说,你在代码里设置 FontWeight.w500,中文部分可能根本不会变粗!肉眼看几乎跟普通字重没区别…… 这坑我整整一年才发现!😤\\n\\n对于Together App来说,最典型的就是我们每个页面的标题,我们其实都用了w500,但是默认情况下,标题并不会变粗一点点。\\n\\n解决方案:\\n\\n在主题里面手动指定字体\'PingFang SC\',这样中文也能自动能应用上w500.\\n\\nMaterialApp(\\n title:…","guid":"https://juejin.cn/post/7482692656989978676","author":"小创","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-18T04:21:03.528Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d9ba5aa752c748148cab42b44198b565~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bCP5Yib:q75.awebp?rk3s=f64ab15b&x-expires=1742876463&x-signature=7Z%2BOfJmQuaUYBcg20oQVMLCKMtI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/af9e9122e3be40b0a478a64cddadcea2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bCP5Yib:q75.awebp?rk3s=f64ab15b&x-expires=1742876463&x-signature=EmfZ1Q%2F3qsGz25JBL%2FpVNnFNWSU%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["iOS","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"用Compose撸一个CoordinatorLayout 🔥🔥🔥","url":"https://juejin.cn/post/7482670776481349673","content":"Android中CoordinatorLayout
是一个很常用的布局,一些特殊交互如吸顶,用它实现非常简单,但Compose中目前没有这个组件。
如果只是单纯吸顶的交互,可以用LazyColumn
的stickyHeader
来实现,非常简单。但如果要控制吸顶的位置,如下面的效果图,stickyHeader
就爱莫能助了,而且LazyColumn
监听滑动的进度也比较麻烦。
于是决定撸一个Compose版本的CoordinatorLayout 💪🏻💪🏻💪🏻
\\n\\n使用起来很简单,具体可查看完整代码。
\\n // collapsable + pin + LazyColumn\\n@Composable\\nfun SimpleScreen2() {\\n Column(\\n Modifier\\n .fillMaxSize()\\n .background(Color.White)\\n .systemBarsPadding()\\n ) {\\n val coroutineScope = rememberCoroutineScope()\\n val lazyListState = rememberLazyListState()\\n val coordinatorState = rememberCoordinatorState()\\n var uiState by remember { mutableStateOf(DemoState()) }\\n\\n Box(\\n modifier = Modifier\\n .height(50.dp)\\n .fillMaxWidth()\\n .padding(horizontal = 20.dp),\\n contentAlignment = Alignment.CenterStart\\n ) {\\n DemoTitle()\\n }\\n\\n HorizontalDivider(color = AppColors.Divider)\\n\\n CoordinatorLayout(\\n nestedScrollableState = { lazyListState },\\n state = coordinatorState,\\n modifier = Modifier.fillMaxSize(),\\n collapsableContent = {\\n Column(Modifier.fillMaxWidth()) {\\n Image(\\n painter = painterResource(id = R.mipmap.img_1),\\n contentDescription = null,\\n modifier = Modifier.fillMaxWidth(),\\n contentScale = ContentScale.FillWidth\\n )\\n }\\n },\\n pinContent = {\\n TabBar(\\n tabList = uiState.tabList,\\n selectedTabIndex = uiState.selectedTab,\\n ) {\\n // 吸顶\\n coroutineScope.launch {\\n uiState = uiState.copy(selectedTab = it)\\n coordinatorState.animateToCollapsed()\\n }\\n\\n }\\n },\\n ) {\\n LazyColumn(Modifier.fillMaxSize(), state = lazyListState) {\\n items(30) {\\n Box(\\n Modifier\\n .fillMaxWidth()\\n .height(50.dp)\\n .padding(horizontal = 15.dp),\\n contentAlignment = Alignment.CenterStart\\n ) {\\n Text(\\n text = \\"Item $it\\",\\n textAlign = TextAlign.Center,\\n\\n )\\n HorizontalDivider(\\n thickness = 0.7.dp,\\n color = AppColors.Divider,\\n modifier = Modifier.align(Alignment.BottomStart)\\n )\\n\\n }\\n }\\n\\n }\\n }\\n }\\n\\n}\\n
\\n接下来,着重讲一下实现过程。一起体验一下Compose的丝滑😄😄😄
\\n先附上CoordinatorState
的完整代码。
@Composable\\nfun rememberCoordinatorState(): CoordinatorState {\\n return rememberSaveable(saver = CoordinatorState.Saver) { CoordinatorState() }\\n}\\n\\n@Stable\\nclass CoordinatorState {\\n // 已折叠的高度\\n var collapsedHeight: Float by mutableFloatStateOf(0f)\\n private set\\n\\n var isFullyCollapsed by mutableStateOf(false)\\n private set\\n\\n private var _maxCollapsableHeight = mutableFloatStateOf(Float.MAX_VALUE)\\n\\n // 最大可折叠高度\\n var maxCollapsableHeight: Float\\n get() = _maxCollapsableHeight.floatValue\\n internal set(value) {\\n if (value.isNaN()) return\\n _maxCollapsableHeight.floatValue = value\\n Snapshot.withoutReadObservation {\\n if (collapsedHeight >= value) {\\n collapsedHeight = value\\n isFullyCollapsed = true\\n } else if (isFullyCollapsed){\\n collapsedHeight = value\\n }\\n }\\n\\n }\\n\\n val scrollableState = ScrollableState { // 向上滑动,为负的,\\n val newValue = (collapsedHeight - it).coerceIn(0f, maxCollapsableHeight)\\n val consumed = collapsedHeight - newValue\\n collapsedHeight = newValue\\n isFullyCollapsed = newValue == maxCollapsableHeight\\n consumed\\n }\\n\\n // animTo 完全折叠状态\\n suspend fun animateToCollapsed(\\n animationSpec: AnimationSpec<Float> = tween(\\n 100,\\n easing = LinearEasing\\n )\\n ) {\\n animateScrollBy(-(maxCollapsableHeight - collapsedHeight), animationSpec)\\n }\\n\\n suspend fun animateScrollBy(\\n value: Float, animationSpec: AnimationSpec<Float> = tween(\\n 100,\\n easing = LinearEasing\\n )\\n ) {\\n scrollableState.animateScrollBy(value, animationSpec)\\n }\\n\\n private fun consume(available: Offset): Offset {\\n val consumedY = scrollableState.dispatchRawDelta(available.y)\\n return available.copy(y = consumedY)\\n }\\n\\n\\n internal val nestedScrollConnection = object : NestedScrollConnection {\\n override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {\\n // 水平方向不消耗\\n if (available.x != 0f) return Offset.Zero\\n // 向上滑动,如果没有达到最大可折叠高度,则自己先消耗\\n if (available.y < 0 && collapsedHeight < maxCollapsableHeight) {\\n return consume(available)\\n }\\n\\n return Offset.Zero\\n }\\n\\n override fun onPostScroll(\\n consumed: Offset,\\n available: Offset,\\n source: NestedScrollSource\\n ): Offset {\\n if (available.y > 0) {\\n return consume(available)\\n }\\n return Offset.Zero\\n }\\n }\\n\\n companion object {\\n val Saver: Saver<CoordinatorState, *> = Saver(\\n save = { listOf(it.collapsedHeight, it.maxCollapsableHeight) },\\n restore = {\\n CoordinatorState().apply {\\n collapsedHeight = it[0]\\n maxCollapsableHeight = it[1]\\n isFullyCollapsed = collapsedHeight >= maxCollapsableHeight\\n }\\n }\\n )\\n }\\n}\\n
\\nCoordinatorState
中定义了已折叠高度collapsedHeight
以及最大可折叠高度maxCollapsableHeight
。
创建一个ScrollableState
用于滑动,更新collapsedHeight
。
val scrollableState = ScrollableState { // 向上滑动,为负的,\\n val newValue = (collapsedHeight - it).coerceIn(0f, maxCollapsableHeight)\\n val consumed = collapsedHeight - newValue\\n collapsedHeight = newValue\\n isFullyCollapsed = newValue == maxCollapsableHeight\\n consumed\\n}\\n
\\nCompose处理滑动冲突非常简单,核心类NestedScrollConnection
。这里我们用到了onPreScroll
和onPostScroll
。下面简单说明下这两个方法,熟悉的同学可以跳过。
onPreScroll
方法用于在子视图即将滚动时预先消费部分或全部滚动事件。
方法签名:
\\nfun onPreScroll(available: Offset, source: NestedScrollSource): Offset\\n
\\n参数说明:
\\navailable: Offset
:表示当前可用的滚动偏移量,包含x和y方向的值。source: NestedScrollSource
:表示滚动事件的来源,例如Drag
或Fling
。返回值:
\\nOffset
,表示当前组件消费的滚动偏移量。如果不想消费任何滚动事件,可以返回Offset.Zero
。onPostScroll
方法用于在子组件已经消费了滚动事件之后,处理剩余的滚动偏移量。
方法签名:
\\nfun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset\\n
\\n参数说明:
\\nconsumed: Offset
:表示子组件已经消费的滚动偏移量。available: Offset
:表示当前剩余的可用滚动偏移量。source: NestedScrollSource
:表示滚动事件的来源。返回值:
\\nOffset
,表示当前组件消费的滚动偏移量。如果不想消费任何滚动事件,可以返回 Offset.Zero
。回到我们的代码。首先看一下我们的CoordinatorLayout
的结构图。
CoordinatorLayout
由3部分组成,CollapsableContent
(以下简称Collapsable
)可折叠的内容,PinContent
(以下简称Pin
),吸顶的内容,Content
底部区域。
页面向上滑动时,如果Collapsable
没有完全折叠,会优先响应滚动,直到完全折叠状态,Pin
吸顶。然后继续上滑,由Content
来响应滑动。下拉时相反。
好了,接下来我们详细说一下这两个方法中的实现。
\\nprivate fun consume(available: Offset): Offset {\\n val consumedY = scrollableState.dispatchRawDelta(available.y)\\n return available.copy(y = consumedY)\\n}\\n\\n\\ninternal val nestedScrollConnection = object : NestedScrollConnection {\\n override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {\\n // 水平方向不消耗\\n if (available.x != 0f) return Offset.Zero\\n // 向上滑动,如果没有达到最大可折叠高度,则自己先消耗\\n if (available.y < 0 && collapsedHeight < maxCollapsableHeight) {\\n return consume(available)\\n }\\n\\n return Offset.Zero\\n }\\n\\n override fun onPostScroll(\\n consumed: Offset,\\n available: Offset,\\n source: NestedScrollSource\\n ): Offset {\\n if (available.y > 0) {\\n return consume(available)\\n }\\n return Offset.Zero\\n }\\n}\\n
\\nonPreScroll
中,首先我们不需要消费水平方向的滑动,为了避免影响水平方向的滑动事件,这里判断如果有水平偏移(available.x != 0f
),直接不返回Offset.Zero
不消费。如果是向上滑动,需要判断Collapsable
是否已经完全折叠,如果没有,则优先自己消费,否则不消费。
onPostScroll
方法回调,意味着Content
已经消费了滚动事件。比如Content
是个LazyColumn
,这里只需要关注向下滑动。该方法回调意味着,LazyColumn
已经滑动到最顶部了,此时只需要消费的剩余的滚动偏移量,让Collapsable
和Pin
往下移动即可。
是的,你没看错,就是这么简单!!!😄😄😄
\\n接下来,我们分析下CoordinatorLayout
的实现。同样,先贴出完整的代码。
/**\\n * @param collapsableContent 可折叠的Content\\n * @param pinContent 要吸顶的Content,默认为空的\\n * @param content 底部的Content\\n * @param nonCollapsableHeight 不允许折叠的高度,至少为0\\n * @param nestedScrollableState 用于collapsableContent和pinContent快速滑动,完全折叠后,剩余Fling交给content来响应。如果不设置,完全折叠后,content不能响应剩余Fling\\n *\\n */\\n@Composable\\nfun CoordinatorLayout(\\n nestedScrollableState: () -> ScrollableState?,\\n collapsableContent: @Composable () -> Unit,\\n modifier: Modifier = Modifier,\\n pinContent: @Composable () -> Unit = {},\\n state: CoordinatorState = rememberCoordinatorState(),\\n nonCollapsableHeight: Int = 0,\\n content: @Composable () -> Unit\\n) {\\n check(nonCollapsableHeight >= 0) {\\n \\"nonCollapsableHeight is at least 0!\\"\\n }\\n\\n val flingBehavior = ScrollableDefaults.flingBehavior()\\n Layout(\\n content = {\\n collapsableContent()\\n pinContent()\\n content()\\n }, modifier = modifier\\n .clipToBounds()\\n .fillMaxSize()\\n .scrollable(\\n state = state.scrollableState,\\n orientation = Orientation.Vertical,\\n enabled = !state.isFullyCollapsed,\\n flingBehavior = remember {\\n object : FlingBehavior {\\n override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {\\n val remain = with(flingBehavior) {\\n performFling(initialVelocity)\\n }\\n // 外层响应Fling后,剩余的交给nestedScrollableState来处理\\n if (remain < 0 && nestedScrollableState() != null) { // 向上滑动,剩余的Fling交给nestedScrollableState消费\\n nestedScrollableState()!!.scroll {\\n with(flingBehavior){\\n performFling(-remain)\\n }\\n }\\n return 0f\\n }\\n return remain\\n }\\n }\\n },\\n )\\n .nestedScroll(state.nestedScrollConnection)\\n ) { measurables, constraints ->\\n check(constraints.hasBoundedHeight)\\n val height = constraints.maxHeight\\n val collapsablePlaceable = measurables[0].measure(\\n constraints.copy(minHeight = 0, maxHeight = Constraints.Infinity)\\n )\\n val collapsableContentHeight = collapsablePlaceable.height\\n val pinPlaceable: Placeable? = if (measurables.size == 3) {\\n measurables[1].measure(\\n constraints.copy(minHeight = 0, maxHeight = Constraints.Infinity)\\n )\\n } else null\\n val pinContentHeight = pinPlaceable?.height ?: 0\\n val safeNonCollapsableHeight = nonCollapsableHeight.coerceAtMost(collapsableContentHeight)\\n val nestedScrollPlaceable = measurables[measurables.lastIndex].measure(\\n constraints.copy(\\n minHeight = 0,\\n maxHeight = (height - pinContentHeight - safeNonCollapsableHeight).coerceAtLeast(0)\\n )\\n )\\n state.maxCollapsableHeight =\\n (collapsablePlaceable.height - safeNonCollapsableHeight).toFloat().coerceAtLeast(0f)\\n layout(constraints.maxWidth, height) {\\n val collapsedHeight = state.collapsedHeight.roundToInt()\\n nestedScrollPlaceable.placeRelative(0, collapsableContentHeight + pinContentHeight - collapsedHeight)\\n collapsablePlaceable.placeRelative(0, -collapsedHeight)\\n pinPlaceable?.placeRelative(0, collapsableContentHeight - collapsedHeight)\\n }\\n }\\n}\\n \\n
\\n这里我们先不需要关注flingBehavior,下面会单独解释。
\\n这里可以看到,我们给Layout
设置了scrollable
,并传入CoordinatorState
中的ScrollableState
并指明orientation,
来使得我们的CoordinatorLayout
支持竖直方向可滑动。
nestedScroll
用于处理嵌套滑动。传入我们CoordinatorState
中的nestedScrollConnection
。
解释measure之前,先看下check(constraints.hasBoundedHeight)
的作用。CoordinatorLayout
的高度有一个明确的上限。
先测量3个Content
的尺寸,由于Pin
可以没有,所以判断一下。 可以注意到测量和Collapsable
和Pin
时传入的maxHeight = Constraints.Infinity
,这意味着子组件的高度没有明确的限制,子组件可以根据自身内容或布局逻辑自由扩展高度,这是因为我们的CoordinatorLayout
是内容可滑动的。
设置不进行折叠的高度,当剩余的可折叠高度达到这个值的时候,再继续滑动,Collapsable
和Pin
便不再跟随滑动了,从而实现,在指定的位置吸顶,常用语吸附在标题栏下方,如前面的效果图。这个可以根据测量的结果动态设置,具体见demo。
我们希望Content
撑满剩余的高度,所以测量的时候,constraints
的是maxHeight
设置的是:
(height - pinContentHeight - safeNonCollapsableHeight).coerceAtLeast(0))
最大可折叠高度maxCollapsableHeight
就是collapsable
的高度 - 不可折叠的高度safeNonCollapsableHeight
前面测量了3个Content
的尺寸,这里layout
就很简单了。只需要根据当前折叠的高度collapsedHeight
,摆放即可,这里很好理解,不做过多的解释了。
通过上面的步骤,我们已经实现了基本的功能。但其中仍然有一些问题,需要处理。 比如Android
的CoordinatorLayout
中存在的一个问题,在AppBarLayout
上快速向上滑动到吸顶后,底部的nestedContent
无法继续响应Fling
等,我们下面一一解决。
val flingBehavior = ScrollableDefaults.flingBehavior()\\nflingBehavior = remember {\\n object : FlingBehavior {\\n override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {\\n val remain = with(flingBehavior) {\\n performFling(initialVelocity)\\n }\\n // scrollable消费Fling后,剩余的交给nestedScrollableState来处理\\n if (remain < 0 && nestedScrollableState() != null) { // 向上滑动,剩余的Fling交给nestedScrollableState消费\\n nestedScrollableState()!!.scroll {\\n with(flingBehavior){\\n performFling(-remain)\\n }\\n }\\n return 0f\\n }\\n return remain\\n }\\n }\\n}\\n \\n
\\nscrollable
默认的flingBehavior
是ScrollableDefaults.flingBehavior()
。这里我们为 scrollable
设置一个自定义的FlingBehavior
,在performFling
方法中首先让默认的flingBehavior
去执行performFling
方法,去让scrollable
消费Fling
。scrollable
消费完后,判断如果是向上(Content
只需要关注向上的Fling),则交由Content
的nestedScrollableState
去消费即可。
目前collapsableContent
的滑动效果比较简单,只支持跟随滑动。
大家有什么优化建议、bug反馈,欢迎大家指出、反馈 👏🏻👏🏻👏🏻 → droyue@163.com
","description":"前言 Android中CoordinatorLayout是一个很常用的布局,一些特殊交互如吸顶,用它实现非常简单,但Compose中目前没有这个组件。\\n\\n如果只是单纯吸顶的交互,可以用LazyColumn的stickyHeader来实现,非常简单。但如果要控制吸顶的位置,如下面的效果图,stickyHeader就爱莫能助了,而且LazyColumn监听滑动的进度也比较麻烦。\\n\\n于是决定撸一个Compose版本的CoordinatorLayout 💪🏻💪🏻💪🏻\\n\\nGithub地址\\n\\n效果图\\n\\n使用\\n\\n使用起来很简单,具体可查看完整代码。\\n\\n点击查看完…","guid":"https://juejin.cn/post/7482670776481349673","author":"左小左","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-18T02:19:21.319Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a989affdcc3c4968b4f89538c7a532c2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bem5bCP5bem:q75.awebp?rk3s=f64ab15b&x-expires=1742869161&x-signature=fy3EWNL6sxyKOoIqtPUySb7%2BT%2BI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/382dae0f5311459a84cd66e710edfc3b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bem5bCP5bem:q75.awebp?rk3s=f64ab15b&x-expires=1742869161&x-signature=Dkn%2B0iblhTkWwXcYGFaRTVBpZsk%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Composer","Android Jetpack"],"attachments":null,"extra":null,"language":null},{"title":"Android Vulkan 官宣转正并统一渲染堆栈 ,这对 Flutter 又有什么影响?","url":"https://juejin.cn/post/7482671750209191936","content":"虽然从 2016 年的 7.0 开始 Android 就已经支持 Vulkan ,并且在之后 Vulkan 逐步作为首选 GPU 接口,但是现在从 2025 开始, Vulkan 将正式官宣成为 Android 上的官方图形 API 。
\\n那「转正」后和之前有什么区别?核心就是 Vulkan 将正式作为 Android 的唯一 GPU 硬件抽象层 (HAL),Google 会要求所有应用和游戏都必须基于 Vulkan 来实现,包括:游戏引擎、middleware 和 HWUI/Skia/WebGPU/ANGLE 等 layered API:
\\n这里就不得不说 ANGLE (Almost Native Graphics Layer Engine) ,虽然 Google 打算强制开发者使用 Vulkan ,但是让大家全部迁移明显不现实,毕竟 Vulkan 和 OpenGLES 的差异还是很大的,而这时候 ANGLE 就开始体现它作用。
\\nANGLE 作为兼容层,它支持让 GLES 应用运行到 Vulkan ,ANGLE 通过将 GLES API 调用翻译为 Vulkan API 调用,从而让 GLES 能够兼容运行到 Vulkan ,而事实上 ANGLE 在 Android 10 开始就开始尝试支持 Vulkan,但是从 Android 15 之后,新的 Android 设备将开始转为仅通过 ANGLE 支持 OpenGL :
\\n其实做出这个决定也挺合理,从数据上看,目前超过 85% 的活跃 Android 手机都已经支持 Vulkan,而基于 Unity 引擎构建的新 Android 游戏里超过 45% 使用 Vulkan,所以 Vulkan only 确实是一个必然的选择。
\\n那为什么 Google 会想要替换到 OpenGL?主要也是因为 OpenGL 存在很多历史问题,例如:
\\n而对于即将到来的 Android 版本:
\\n也就是 Android 17 开始,除了特定列表中的 App 外,所有应用都需要使用 ANGLE 。
\\n另外,虽然 Vlukan 的一致性比起 OpenGL 好很多,但是为了进一步提高 Android 上的 Vulkan 实际可用功能一致性,Google 推出了适用于 Android 的 Vulkan 配置文件 (VPA)。
\\n\\n\\nVPA 是芯片组必须支持的 Vulkan 功能的集合,只有适配了,才能通过 Google 针对特定 Android 版本的认证要求,例如 Android 16 的 VPA 要求芯片组至少支持这些 Vulkan 功能:github.com/KhronosGrou… 。
\\n
通过强制支持较新版本的 Vulkan,也会加速淘汰较旧的 GPU 设备,因为使用这些较旧 GPU 的设备会不允许更新到较新的 Android 版本,这也意味着随着时间的推移, Vulkan 在未来的一致性会越来越高。
\\n除此之外,Google 还和 Unity Technologies 合作,让 Vulkan 和 Unity 游戏引擎的集成变得更加容易,从而进一步降低 PC 游戏移植到 Android 的难度。
\\n另外,谷歌还与联发科达成合作,为联发科芯片提供 Android 动态性能框架 (ADPF),支持让开发人员根据设备的热状态实时调整游戏的性能需求:
\\n最后,Java/Kotlin 开发者后续可能无需切换到 C/C++ 和 NDK 来直接访问 Vulkan,未来 Java/Kotlin 开发者也许可以通过 Java/Kotlin 下利用 WebGPU 来“直接”体验 Vulkan 场景,从而做到相对 “直接” 的 GPU 访问:
\\n所以,到这里有没有一种感觉:就像当年 Apple 全面转向 Metal 的感觉,现在 Google 终于开始也跟上这个步伐。
\\n那么这些 Flutter 有什么影响?答案肯定就是 All In Impeller ,这是 Flutter 正在做的事情,当然这也是一个「充满坎坷」的过程,因为 Android 的碎片化让 Impeller 在 Android 的落地比 iOS 复杂很多。
\\n比如 Flutter 3.29 才发布了 Android 平台正式全面启用 Impeller ,但是 3.29.2 版本就开始「回退」,原因是 Impeller 在某些不支持 Vlukan 的低版本设备上使用 Impeller GLES 作兼容时会 Crash ,所以只能暂时再次转为 Skia GLES :
\\n\\n\\n其实这也一定程度体现了 OpenGL 在兼容上的难度。。。。。
\\n
甚至在之前我们聊过的 《全新 PlatformView 实现 HCPP》 支持上,也可能需要考虑针对 OpenGLES 增加一个 AHB swapchain 来帮助没有 Vulkan 的设备支持 HCPP :
\\n甚至在 Vulkan 相关的 swapchain 支持上, AHBSwapchainVK 实现也并非在所有 Android 版本上都可用,如果不支持还需要会回退到 KHR swapchain ,例如:
\\n所以可以看到,就算存在 Vulkan 场景,在 Android 上 Impeller 也需要根据实际场景使用不同支持,说到这里可能大家就有点懵,swapchain 是什么?AHB 又是什么?这里顺便简单介绍下。
\\n什么是 swapchain ? swapchain 简单说就是一种用于管理多个缓冲区的机制,从而确保平滑渲染和显示画面,进而防止画面撕裂,比如 swapchain 通常会有双缓冲或三缓冲,通过实现类似一个缓冲区显示的同时,另一个缓冲区正在准备渲染一下帧。
\\n\\n\\n通俗又不严谨的说法:现在的 GPU 渲染效率很高,而系统显示的速度跟不上 GPU 渲染的速度,所有可以通过多重 buffer 的作用,提前在 GPU 渲染画面,等待提交,而提交给系统显示的过程中,就是在 buffer 之间进行交换 (Swap)。
\\n
所以也可以理解为:swapchain 是一系列图像队列,队列会顺序交替将图像提交到系统显示。
\\n所以,其实当你在 Android 启用 Impeller 后会发现,如果在特定设备上出现如下图所示这种画面撕裂的问题,一般首先会怀疑 swapchain 问题,这里其实就是 Impeller 使用了 AHB swapchain 的 bug 导致:
\\n那么 AHB(Android Hardware Buffer) 又是什么?简单说,AHB 是 Android 上一种高效共享缓冲区的机制,属于进程间高效共享缓冲区的场景,支持零拷贝操作,而 AHB 可以绑定到 EGL/OpenGL 和 Vulkan ,从而适合跨进程图形数据共享。
\\n前面我们说过,swapchain 是一系列图像队列,队列会顺序交替将图像提交到系统显示,而如果配合 AHB ,就可以起到性能优化的作用,因为 AHB 可以做到零拷贝意味着数据在进程间共享时无需复制 ,也就是在高帧率应用里,渲染进程生成的帧可以直接由显示进程使用而无需额外拷贝。
\\n\\n\\n也就是,当应用渲染新帧时,AHB 确保显示进程能立即访问。
\\n
那么回到 Vulkan,对于 Vulkan 来说 swapchain 就是它的标准实现,所以它本来就是基于 swapchain 模式工作,但是由于 Android 平台存在 AHB ,所以是否使用 AHB 来加速性能对于 Impeller 就是一个需要衡量的情况:
\\n\\n\\n至少目前看来 AHB 确实给 Impeller 带来不少问题。
\\n
所以简单总结下,对于 Vulkan 而言:
\\n而对于 OpenGL 来说,它并没有标准的 swapchain 实现 ,所以如果 HCPP 模式想要支持 AHB swapchain,也就是需要上面所说的自定义来完成。
\\n那么从 Flutter 角度看,Vulkan 和 OpenGL 的同时存在,确实也让 Impeller 在 Android 上的稳定性成本大大提高,那么未来 ANGLE 的强势介入,也许就不在会再有这种问题存在。
\\n也许到那个时候,Flutter GPU 和 sence 场景,也能开始正式落地。
\\nwindows安装命令如下:
\\n@\\"%SystemRoot%\\\\System32\\\\WindowsPowerShell\\\\v1.0\\\\powershell.exe\\" -NoProfile -InputFormat None -ExecutionPolicy Bypass -Command \\"[System.Net.ServicePointManager]::SecurityProtocol = 3072; iex ((New-Object System.Net.WebClient).DownloadString(\'https://community.chocolatey.org/install.ps1\'))\\" && SET \\"PATH=%PATH%;%ALLUSERSPROFILE%\\\\chocolatey\\\\bin\\"\\nForcing web requests to allow TLS v1.2 (Required for requests to Chocolatey.org)\\n
\\n使用如下命令:
\\ndart pub global activate fvm\\n
\\nflutter sdk 里面也包含 dart sdk 的配置,下载任意版本的flutter sdk。
\\n配置好 dart sdk 环境变量:
\\n同样使用如下命令:
\\ndart pub global activate fvm\\n
\\nfvm --version\\n
\\n我这里安装 3.29.2
\\nfvm install 3.29.2\\n
\\nflutter use 3.29.2\\n
\\nfvm list\\n
\\n标题 | |
---|---|
fvm config | 对 fvm 进行配置 |
fvm flutter | 对Flutter 的命令进行代理 |
fvm install | 安装 Flutter 版本 |
fvm list | 查看已安装的 Flutter 版本 |
fvm releases | 查看 Flutter sdk 都有哪些发布的版本 |
fvm remove | 删除已安装的某个 Flutter 的版本 |
fvm use | 选择你要使用的版本 |
fvmversion | 查看安装 fvm 的版本 |
fvm -h,fvm –help | 可以查看更多使用信息 |
fvm –global | 将这个版本设置为全局版本 |
fvm –force | 跳过执行 Flutter 项目检查命令 |
最近体验了Cursor这款编辑器,用它实现了一个\\"汉斯打怪兽\\"的Flutter小游戏。整个开发过程比较顺畅,今天分享一下使用心得,以及与团队去年引入的Copilot相比的一些体验差异。
\\nCursor是一个集成了AI功能的代码编辑器。它基于VS Code,内置了AI助手,可以帮你写代码、解决问题、回答问题。与普通编辑器的主要区别是,你可以用自然语言与它对话,不需要记一堆命令。
\\n开始新Flutter项目时,通常需要花时间思考项目结构,然后一个个手动创建文件夹和文件。这个过程不仅繁琐,还容易遗漏重要的模块。
\\n用Cursor时,我只需要输入:
\\n\\"帮我创建一个Flutter飞机射击游戏的项目结构\\"
\\n它会给出一个目录结构建议,我最终实现的项目结构如下:
\\nlib/\\n├── main.dart\\n├── screens/\\n│ ├── home_page.dart\\n│ ├── game_screen.dart\\n│ └── splash_screen.dart\\n└── game/\\n ├── models/\\n │ ├── player.dart\\n │ └── monster.dart\\n ├── utils/\\n │ └── collision_detector.dart\\n └── widgets/\\n ├── starry_background.dart\\n └── game_controls.dart\\nassets/\\n└── images/\\n ├── app_logo.svg\\n ├── player_ship.svg\\n └── monster.svg\\n
\\n这个结构分离了UI、游戏逻辑和资源。点击确认后,Cursor会创建这些文件和文件夹,省去了手动操作的时间。将游戏相关的代码放在单独的game
目录下,使项目结构更清晰,便于维护。
在开发过程中,Cursor帮助我设计了游戏的架构。下面是项目架构图:
\\ngraph TD\\n A[main.dart] --\x3e B[MyApp 根Widget]\\n B --\x3e C[SplashScreen 启动页]\\n C --\x3e D[HomePage 主页面]\\n D --\x3e E[游戏设置]\\n D --\x3e F[开始游戏按钮]\\n D --\x3e G[GameScreen 游戏界面]\\n \\n G --\x3e H[StarryBackground 星空背景]\\n G --\x3e I[游戏主循环逻辑]\\n G --\x3e J[GameControls 游戏控制]\\n \\n I --\x3e K[玩家飞船]\\n I --\x3e L[怪兽]\\n K --\x3e M[碰撞检测 & 计分]\\n L --\x3e M\\n
\\n这个架构图展示了游戏的组件层次和数据流向。从主应用入口到各个屏幕,再到游戏核心逻辑,每个部分都有明确的职责和关系。
\\n文件结构也可以用Mermaid图表展示:
\\ngraph LR\\n A[lib] --\x3e B[main.dart]\\n A --\x3e C[screens]\\n A --\x3e D[game]\\n \\n C --\x3e C1[home_page.dart]\\n C --\x3e C2[game_screen.dart]\\n C --\x3e C3[splash_screen.dart]\\n \\n D --\x3e D1[models]\\n D --\x3e D2[utils]\\n D --\x3e D3[widgets]\\n \\n D1 --\x3e D1A[player.dart]\\n D1 --\x3e D1B[monster.dart]\\n \\n D2 --\x3e D2A[collision_detector.dart]\\n \\n D3 --\x3e D3A[starry_background.dart]\\n D3 --\x3e D3B[game_controls.dart]\\n \\n E[assets] --\x3e F[images]\\n F --\x3e F1[app_logo.svg]\\n F --\x3e F2[player_ship.svg]\\n F --\x3e F3[monster.svg]\\n
\\nFlutter游戏开发中,素材往往是个问题。要么去网上找现成的(可能有版权问题),要么需要设计工具自己画(需要设计技能)。
\\n用Cursor时,我可以描述需要的图标:
\\n\\"帮我创建一个太空飞船的SVG图标,简洁风格,有点未来感\\"
\\n它会生成SVG代码:
\\n<svg width=\\"100\\" height=\\"100\\" viewBox=\\"0 0 100 100\\" xmlns=\\"http://www.w3.org/2000/svg\\">\\n <path d=\\"M50 20C35 20 25 35 25 50C25 65 35 75 50 75C65 75 75 65 75 50C75 35 65 20 50 20Z\\" fill=\\"#3498db\\"/>\\n <path d=\\"M50 30C40 30 35 40 35 50C35 60 40 65 50 65C60 65 65 60 65 50C65 40 60 30 50 30Z\\" fill=\\"#2980b9\\"/>\\n <rect x=\\"45\\" y=\\"10\\" width=\\"10\\" height=\\"15\\" rx=\\"2\\" fill=\\"#95a5a6\\"/>\\n <rect x=\\"35\\" y=\\"75\\" width=\\"10\\" height=\\"15\\" rx=\\"2\\" fill=\\"#e74c3c\\"/>\\n <rect x=\\"55\\" y=\\"75\\" width=\\"10\\" height=\\"15\\" rx=\\"2\\" fill=\\"#e74c3c\\"/>\\n</svg>\\n
\\n保存为.svg文件后就能在Flutter中使用。这比在网上搜索素材或自己设计要快一些,且可以根据需求调整。
\\n在Cursor的帮助下,我实现了游戏的核心组件。下面是一些实际代码片段:
\\nimport \'package:flutter/material.dart\';\\nimport \'screens/splash_screen.dart\';\\n\\nvoid main() {\\n runApp(const MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n const MyApp({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n title: \'汉斯打怪兽\',\\n theme: ThemeData(\\n primarySwatch: Colors.blue,\\n visualDensity: VisualDensity.adaptivePlatformDensity,\\n fontFamily: \'Microsoft YaHei\', // 使用微软雅黑字体\\n ),\\n home: const SplashScreen(), // 使用启动页面作为首页\\n debugShowCheckedModeBanner: false, // 移除调试标签\\n );\\n }\\n}\\n
\\n这段代码是应用入口,设置了应用主题、字体和初始页面。我描述\\"我想要一个中文游戏,使用微软雅黑字体,并且有启动页面\\",Cursor就生成了这样的代码。
\\nimport \'dart:math\';\\nimport \'package:flutter/material.dart\';\\n\\nclass Star {\\n final double x;\\n final double y;\\n final double size;\\n final double opacity;\\n \\n Star({required this.x, required this.y, required this.size, required this.opacity});\\n \\n static Star random() {\\n final random = Random();\\n return Star(\\n x: random.nextDouble() * 400,\\n y: random.nextDouble() * 800,\\n size: 1 + random.nextDouble() * 2,\\n opacity: 0.3 + random.nextDouble() * 0.7,\\n );\\n }\\n}\\n\\nclass StarryBackground extends StatefulWidget {\\n const StarryBackground({Key? key}) : super(key: key);\\n\\n @override\\n _StarryBackgroundState createState() => _StarryBackgroundState();\\n}\\n\\nclass _StarryBackgroundState extends State<StarryBackground> with TickerProviderStateMixin {\\n late List<Star> stars;\\n late AnimationController _controller;\\n \\n @override\\n void initState() {\\n super.initState();\\n // 生成随机星星\\n stars = List.generate(100, (_) => Star.random());\\n // 设置动画控制器\\n _controller = AnimationController(\\n vsync: this,\\n duration: const Duration(milliseconds: 5000),\\n )..repeat();\\n }\\n \\n @override\\n void dispose() {\\n _controller.dispose();\\n super.dispose();\\n }\\n \\n @override\\n Widget build(BuildContext context) {\\n return AnimatedBuilder(\\n animation: _controller,\\n builder: (context, child) {\\n return CustomPaint(\\n painter: StarPainter(stars, _controller.value),\\n child: child,\\n );\\n },\\n child: Container(),\\n );\\n }\\n}\\n\\nclass StarPainter extends CustomPainter {\\n final List<Star> stars;\\n final double animationValue;\\n \\n StarPainter(this.stars, this.animationValue);\\n \\n @override\\n void paint(Canvas canvas, Size size) {\\n for (var star in stars) {\\n // 计算闪烁效果\\n final flicker = sin((animationValue * 10) + (star.x + star.y)) * 0.3 + 0.7;\\n final paint = Paint()\\n ..color = Colors.white.withOpacity(star.opacity * flicker);\\n \\n canvas.drawCircle(\\n Offset(star.x % size.width, star.y % size.height),\\n star.size,\\n paint,\\n );\\n }\\n }\\n \\n @override\\n bool shouldRepaint(StarPainter oldDelegate) => true;\\n}\\n
\\n这段代码实现了一个动态的星空背景,包含随机生成的星星和闪烁效果。当我向Cursor描述\\"我想要一个有星星闪烁的太空背景\\"时,它生成了基本的Widget结构,并考虑了性能优化和动画效果。
\\nimport \'package:flutter/material.dart\';\\nimport \'package:flutter/services.dart\';\\n\\nclass Player {\\n Rect rect;\\n double speed;\\n int lives;\\n List<Rect> bullets;\\n DateTime lastShootTime;\\n \\n Player({\\n required this.rect,\\n this.speed = 5.0,\\n this.lives = 3,\\n }) : \\n bullets = [],\\n lastShootTime = DateTime.now();\\n \\n void move(double dx) {\\n rect = rect.translate(dx * speed, 0);\\n }\\n \\n void shoot() {\\n final now = DateTime.now();\\n // 限制射击频率\\n if (now.difference(lastShootTime).inMilliseconds > 300) {\\n bullets.add(\\n Rect.fromCenter(\\n center: Offset(rect.center.dx, rect.top),\\n width: 5,\\n height: 10,\\n ),\\n );\\n lastShootTime = now;\\n // 添加振动反馈\\n HapticFeedback.lightImpact();\\n }\\n }\\n \\n void updateBullets() {\\n // 移动子弹\\n for (int i = bullets.length - 1; i >= 0; i--) {\\n bullets[i] = bullets[i].translate(0, -10);\\n \\n // 移除超出屏幕的子弹\\n if (bullets[i].bottom < 0) {\\n bullets.removeAt(i);\\n }\\n }\\n }\\n \\n void render(Canvas canvas) {\\n // 绘制飞船\\n final paint = Paint()\\n ..color = Colors.blue;\\n canvas.drawRect(rect, paint);\\n \\n // 绘制子弹\\n final bulletPaint = Paint()\\n ..color = Colors.red;\\n for (var bullet in bullets) {\\n canvas.drawRect(bullet, bulletPaint);\\n }\\n }\\n}\\n
\\n这段代码定义了玩家飞船的数据模型,包含位置、移动、射击和渲染逻辑。Cursor还添加了振动反馈功能,这是我在描述需求时提到的增强游戏体验的细节。
\\nimport \'dart:math\';\\nimport \'package:flutter/material.dart\';\\nimport \'../models/player.dart\';\\nimport \'../models/monster.dart\';\\n\\nclass CollisionDetector {\\n static bool checkCollision(Rect rect1, Rect rect2) {\\n return rect1.overlaps(rect2);\\n }\\n \\n static void detectCollisions(Player player, List<Monster> monsters, Function(int) onScoreChanged) {\\n int score = 0;\\n \\n // 检测子弹与怪兽的碰撞\\n for (int i = player.bullets.length - 1; i >= 0; i--) {\\n for (int j = monsters.length - 1; j >= 0; j--) {\\n if (checkCollision(player.bullets[i], monsters[j].rect)) {\\n // 处理碰撞逻辑\\n player.bullets.removeAt(i);\\n monsters[j].health--;\\n \\n if (monsters[j].health <= 0) {\\n monsters.removeAt(j);\\n score += 10;\\n // 提供振动反馈\\n HapticFeedback.mediumImpact();\\n }\\n \\n // 更新分数\\n onScoreChanged(score);\\n break;\\n }\\n }\\n }\\n \\n // 检测玩家与怪兽的碰撞\\n for (int i = monsters.length - 1; i >= 0; i--) {\\n if (checkCollision(player.rect, monsters[i].rect)) {\\n player.lives--;\\n monsters.removeAt(i);\\n // 提供强烈振动反馈\\n HapticFeedback.heavyImpact();\\n break;\\n }\\n }\\n }\\n}\\n
\\n这段代码实现了游戏中的碰撞检测逻辑,包括子弹与怪兽的碰撞和玩家与怪兽的碰撞。Cursor实现了基本的碰撞检测算法,并添加了不同强度的振动反馈,以区分不同类型的碰撞。
\\nimport \'package:flutter/material.dart\';\\n\\nclass GameControls extends StatelessWidget {\\n final Function(double) onMove;\\n final VoidCallback onShoot;\\n \\n const GameControls({\\n Key? key,\\n required this.onMove,\\n required this.onShoot,\\n }) : super(key: key);\\n \\n @override\\n Widget build(BuildContext context) {\\n return Stack(\\n children: [\\n // 左右移动控制区\\n Positioned.fill(\\n child: GestureDetector(\\n onPanUpdate: (details) {\\n onMove(details.delta.dx);\\n },\\n ),\\n ),\\n \\n // 射击按钮\\n Positioned(\\n right: 20,\\n bottom: 20,\\n child: GestureDetector(\\n onTap: onShoot,\\n child: Container(\\n width: 60,\\n height: 60,\\n decoration: BoxDecoration(\\n color: Colors.red.withOpacity(0.5),\\n shape: BoxShape.circle,\\n ),\\n child: const Icon(\\n Icons.flash_on,\\n color: Colors.white,\\n size: 30,\\n ),\\n ),\\n ),\\n ),\\n ],\\n );\\n }\\n}\\n
\\n这段代码实现了游戏的控制界面,包括左右移动和射击按钮。Cursor考虑到了移动设备的触摸特性,使用GestureDetector
来捕获用户的滑动和点击操作。
游戏的数据流向如下:
\\nflowchart LR\\n A[用户输入] --\x3e B[GameControls]\\n B --\x3e C[Player模型]\\n C --\x3e D[游戏状态更新]\\n D --\x3e E[碰撞检测]\\n E --\x3e F[游戏逻辑]\\n F --\x3e G[Monster模型]\\n G --\x3e H[渲染更新]\\n H --\x3e I[屏幕显示]\\n
\\n这种数据流向在代码中的体现是:
\\n// GameScreen中的一部分代码\\nvoid _updateGame() {\\n // 更新玩家状态\\n _player.updateBullets();\\n \\n // 更新怪兽状态\\n for (var monster in _monsters) {\\n monster.update();\\n }\\n \\n // 检测碰撞\\n CollisionDetector.detectCollisions(\\n _player, \\n _monsters, \\n (score) {\\n setState(() {\\n _score += score;\\n });\\n }\\n );\\n \\n // 生成新怪兽\\n _generateMonsters();\\n \\n // 检查游戏结束条件\\n if (_player.lives <= 0) {\\n _gameOver();\\n }\\n \\n // 触发重绘\\n setState(() {});\\n}\\n
\\n用户输入通过GameControls传递给Player模型,触发游戏状态更新,然后通过碰撞检测影响Monster模型,最终通过setState触发重绘,反映到屏幕显示上。
\\n去年团队引入了GitHub Copilot,它提高了编码效率,但Cursor在某些方面提供了不同的体验:
\\nCopilot:主要是行内代码补全和注释驱动的代码生成。你写一个Widget名或注释,它会尝试补全剩余代码。
\\nCursor:提供对话界面,可以进行多轮交流。你可以描述一个Flutter UI需求,它会生成解决方案,然后你可以继续提问或要求修改。
\\nCopilot:擅长理解当前文件和上下文,对整个项目结构的理解有限。
\\nCursor:可以理解项目结构,处理多个文件,帮助创建项目框架。
\\nCopilot:通常一次生成几行到几十行代码,适合单个Widget实现。
\\nCursor:可以生成完整的类或文件,包括相关的辅助类和控制器。
\\n不需要特定格式,直接描述:\\"我想实现一个Flutter计分板,可以记录玩家得分\\"。
\\n编译报错时,把错误信息粘贴给Cursor,它通常能理解问题并给出解决方案。
\\n当Widget变得复杂时,可以让Cursor帮忙重构:\\"这个StatefulWidget太长了,能帮我拆分成更小的组件吗?\\"
\\n想了解Flutter中某个widget怎么用?直接问Cursor:\\"如何使用AnimatedBuilder实现动画?\\",它会给出示例代码和解释。
\\nCursor和Copilot在Flutter开发中各有优势。Copilot像是一个随时准备补全代码的助手,而Cursor更像是一个可以讨论项目的合作伙伴。
\\n对于\\"汉斯打怪兽\\"这样的新Flutter项目,Cursor帮助我搭建了项目结构,生成了游戏素材,实现了核心功能。在日常维护和小Widget开发中,Copilot的即时补全可能更方便。
\\n理想的Flutter开发工作流可能是结合两者:用Cursor进行项目规划和复杂UI实现,用Copilot处理日常编码和小改动。
\\n你用过Cursor或Copilot开发Flutter应用吗?对这两种AI编程助手有什么看法?欢迎分享你的体验!
","description":"用Cursor开发Flutter游戏:AI编辑器提升编程效率 最近体验了Cursor这款编辑器,用它实现了一个\\"汉斯打怪兽\\"的Flutter小游戏。整个开发过程比较顺畅,今天分享一下使用心得,以及与团队去年引入的Copilot相比的一些体验差异。\\n\\nCursor是什么\\n\\nCursor是一个集成了AI功能的代码编辑器。它基于VS Code,内置了AI助手,可以帮你写代码、解决问题、回答问题。与普通编辑器的主要区别是,你可以用自然语言与它对话,不需要记一堆命令。\\n\\nFlutter游戏开发体验\\n从零创建项目结构\\n\\n开始新Flutter项目时,通常需要花时间思考项目结构…","guid":"https://juejin.cn/post/7482462767262826535","author":"Hans_April","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-17T07:49:50.895Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/8d952e421e5343f78cb08b858bf0aafc~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgSGFuc19BcHJpbA==:q75.awebp?rk3s=f64ab15b&x-expires=1742803368&x-signature=mqy3H2qQ20zn4KFImVZ66JT29LM%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["开发工具","AI 编程","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter Dart 面向对象编程全面解析","url":"https://juejin.cn/post/7482037724398387238","content":"在 Flutter 开发中,Dart 作为其编程语言,采用了面向对象的编程范式。面向对象编程(OOP)将数据和操作数据的方法封装在一起,形成对象,以提高代码的可维护性、可扩展性和可重用性。本文将详细介绍 Dart 面向对象编程的核心概念,包括类、对象、继承、多态、抽象类和接口等,并结合代码示例进行说明。
\\n类是对象的蓝图,它定义了对象的属性和方法。对象是类的实例,通过类可以创建多个不同的对象。
\\n// 定义一个 Person 类\\nclass Person {\\n // 定义属性\\n String name;\\n int age;\\n\\n // 定义构造函数\\n Person(this.name, this.age);\\n\\n // 定义方法\\n void introduce() {\\n print(\'我叫 $name,今年 $age 岁。\');\\n }\\n}\\n\\nvoid main() {\\n // 创建 Person 类的对象\\n Person person1 = Person(\'张三\', 20);\\n Person person2 = Person(\'李四\', 25);\\n\\n // 调用对象的方法\\n person1.introduce();\\n person2.introduce();\\n}\\n
\\nclass Person
定义了一个名为 Person
的类。String name
和 int age
是 Person
类的属性,用于存储对象的状态。Person(this.name, this.age)
是构造函数,用于初始化对象的属性。void introduce()
是 Person
类的方法,用于打印对象的信息。main
函数中,使用 Person(\'张三\', 20)
和 Person(\'李四\', 25)
创建了两个 Person
类的对象 person1
和 person2
,并调用它们的 introduce
方法。继承是面向对象编程的重要特性之一,它允许一个类(子类)继承另一个类(父类)的属性和方法。子类可以扩展父类的功能,也可以重写父类的方法。
\\n// 定义父类\\nclass Animal {\\n String name;\\n\\n Animal(this.name);\\n\\n void makeSound() {\\n print(\'动物发出声音\');\\n }\\n}\\n\\n// 定义子类\\nclass Dog extends Animal {\\n Dog(String name) : super(name);\\n\\n @override\\n void makeSound() {\\n print(\'汪汪汪\');\\n }\\n}\\n\\nvoid main() {\\n Dog dog = Dog(\'旺财\');\\n dog.makeSound();\\n}\\n
\\nclass Animal
是父类,包含一个属性 name
和一个方法 makeSound
。class Dog extends Animal
表示 Dog
类继承自 Animal
类。Dog(String name) : super(name)
是 Dog
类的构造函数,使用 super(name)
调用父类的构造函数。@override
注解表示重写父类的方法,Dog
类重写了 makeSound
方法,输出 \'汪汪汪\'
。main
函数中,创建了一个 Dog
类的对象 dog
,并调用其 makeSound
方法。多态是指同一个方法调用可以根据对象的不同类型表现出不同的行为。在 Dart 中,多态主要通过继承和方法重写来实现。
\\n// 定义父类\\nclass Shape {\\n void draw() {\\n print(\'绘制图形\');\\n }\\n}\\n\\n// 定义子类\\nclass Circle extends Shape {\\n @override\\n void draw() {\\n print(\'绘制圆形\');\\n }\\n}\\n\\nclass Square extends Shape {\\n @override\\n void draw() {\\n print(\'绘制正方形\');\\n }\\n}\\n\\nvoid main() {\\n Shape shape1 = Circle();\\n Shape shape2 = Square();\\n\\n shape1.draw();\\n shape2.draw();\\n}\\n
\\nclass Shape
是父类,定义了一个 draw
方法。class Circle extends Shape
和 class Square extends Shape
分别定义了两个子类,并重写了 draw
方法。main
函数中,将 Circle
和 Square
类的对象赋值给 Shape
类型的变量 shape1
和 shape2
。shape1.draw()
和 shape2.draw()
时,会根据对象的实际类型调用相应的 draw
方法,体现了多态性。抽象类是一种不能被实例化的类,它主要用于定义一些通用的属性和方法,供子类继承和实现。抽象方法是在抽象类中声明但没有实现的方法,子类必须实现这些抽象方法。
\\n// 定义抽象类\\nabstract class Vehicle {\\n // 抽象方法\\n void start();\\n\\n // 普通方法\\n void stop() {\\n print(\'车辆停止\');\\n }\\n}\\n\\n// 定义子类\\nclass Car extends Vehicle {\\n @override\\n void start() {\\n print(\'汽车启动\');\\n }\\n}\\n\\nvoid main() {\\n Car car = Car();\\n car.start();\\n car.stop();\\n}\\n
\\nabstract class Vehicle
定义了一个抽象类 Vehicle
。void start()
是抽象方法,没有具体的实现,子类必须实现该方法。void stop()
是普通方法,有具体的实现。class Car extends Vehicle
表示 Car
类继承自 Vehicle
类,并实现了 start
方法。main
函数中,创建了一个 Car
类的对象 car
,并调用其 start
和 stop
方法。在 Dart 中,接口的概念与抽象类类似,但接口只包含抽象方法,不包含属性和具体实现的方法。类可以实现一个或多个接口。
\\n// 定义接口\\nabstract class Flyable {\\n void fly();\\n}\\n\\nabstract class Swimmable {\\n void swim();\\n}\\n\\n// 定义实现类\\nclass Duck implements Flyable, Swimmable {\\n @override\\n void fly() {\\n print(\'鸭子飞起来了\');\\n }\\n\\n @override\\n void swim() {\\n print(\'鸭子在游泳\');\\n }\\n}\\n\\nvoid main() {\\n Duck duck = Duck();\\n duck.fly();\\n duck.swim();\\n}\\n
\\nabstract class Flyable
和 abstract class Swimmable
定义了两个接口,分别包含一个抽象方法 fly
和 swim
。class Duck implements Flyable, Swimmable
表示 Duck
类实现了 Flyable
和 Swimmable
两个接口,并实现了接口中的抽象方法。main
函数中,创建了一个 Duck
类的对象 duck
,并调用其 fly
和 swim
方法。封装是将数据和操作数据的方法捆绑在一起,并隐藏对象的内部实现细节,只提供公共的访问接口。在 Dart 中,可以使用访问修饰符来实现封装。
\\nclass BankAccount {\\n // 私有属性\\n double _balance = 0;\\n\\n // 公共方法,用于存款\\n void deposit(double amount) {\\n if (amount > 0) {\\n _balance += amount;\\n print(\'存款 $amount 元,当前余额: $_balance 元\');\\n } else {\\n print(\'存款金额必须大于 0\');\\n }\\n }\\n\\n // 公共方法,用于取款\\n void withdraw(double amount) {\\n if (amount > 0 && amount <= _balance) {\\n _balance -= amount;\\n print(\'取款 $amount 元,当前余额: $_balance 元\');\\n } else {\\n print(\'取款失败,余额不足或取款金额无效\');\\n }\\n }\\n\\n // 公共方法,用于查询余额\\n double getBalance() {\\n return _balance;\\n }\\n}\\n\\nvoid main() {\\n BankAccount account = BankAccount();\\n account.deposit(1000);\\n account.withdraw(500);\\n print(\'当前余额: ${account.getBalance()} 元\');\\n}\\n
\\ndouble _balance
是私有属性,使用下划线 _
开头表示该属性只能在类的内部访问。deposit
、withdraw
和 getBalance
是公共方法,用于对私有属性 _balance
进行操作和访问。main
函数中,创建了一个 BankAccount
类的对象 account
,并调用其公共方法进行存款、取款和查询余额操作。Dart 的面向对象编程提供了丰富的特性,包括类和对象、继承、多态、抽象类和接口、封装等。这些特性可以帮助开发者编写更加模块化、可维护和可扩展的代码。在 Flutter 开发中,合理运用面向对象编程的思想,可以提高开发效率和代码质量。
","description":"引言 在 Flutter 开发中,Dart 作为其编程语言,采用了面向对象的编程范式。面向对象编程(OOP)将数据和操作数据的方法封装在一起,形成对象,以提高代码的可维护性、可扩展性和可重用性。本文将详细介绍 Dart 面向对象编程的核心概念,包括类、对象、继承、多态、抽象类和接口等,并结合代码示例进行说明。\\n\\n1. 类和对象\\n\\n类是对象的蓝图,它定义了对象的属性和方法。对象是类的实例,通过类可以创建多个不同的对象。\\n\\n代码示例\\n// 定义一个 Person 类\\nclass Person {\\n // 定义属性\\n String name;\\n int age…","guid":"https://juejin.cn/post/7482037724398387238","author":"顾林海","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-17T02:47:23.240Z","media":null,"categories":["Android","前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"系统化掌握Flutter开发之导航器(Navigator)(一):页面跳转的“指挥官”","url":"https://juejin.cn/post/7482236799269109800","content":"\\n在移动应用开发中,页面跳转如同城市交通的
路线规划
,而Flutter
的路由系统就是你的智能导航
。你是否遇到过页面堆栈混乱
、参数传递丢失
、返回逻辑失控
的困境?
许多新手开发者因为缺乏对路由系统的系统认知,导致应用后期出现难以维护的\\"面条式跳转\\"
。路由不仅是页面切换的工具,更是构建清晰应用架构的核心。理解其底层机制,就像掌握了应用空间的拓扑学,能让你游刃有余地管理复杂的页面流。
本章将带你以系统化思维拆解Flutter
路由,从底层原理到高阶技巧,构建完整的导航知识体系。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\nNavigator
是管理页面(Route
)堆栈的核心组件,负责控制页面的跳转
、返回
和生命周期
。它不仅决定用户当前看到哪个页面,还通过堆栈(Stack
)机制记住用户走过的路径。例如:
Navigator
会将详情页“压入”
(push
)堆栈顶部;Navigator
会将当前页面“弹出”
(pop
)堆栈,回到上一个页面。1、页面隔离性:
\\n每个页面(Route
)独立存在于堆栈中,彼此之间互不影响。这避免了传统多页面开发中可能出现的状态污染问题。
2、堆栈控制:
\\n通过“先进后出”
(FILO
)的堆栈结构,Navigator
天然支持符合用户直觉的导航逻辑。例如:
3、统一路由管理:
\\nNavigator
提供了一套标准化的导航方案,无论是简单的页面跳转,还是复杂的动态路由(如根据参数生成不同页面
),都能通过一致的API
实现。例如:
Named Route
)统一管理页面路径,避免硬编码。onGenerateRoute
处理动态路由逻辑,实现灵活跳转。4、跨平台一致性:
\\nNavigator
屏蔽了Android
和iOS
的导航差异,开发者无需分别处理“返回按钮”
和“手势返回”
,只需调用pop()
方法即可兼容两端。
\\n\\n小结:
\\n通过堆栈机制,以
\\n符合直觉
的方式管理页面导航,同时提供标准化
、跨平台
的解决方案。
堆栈结构的本质是 “时间线” :
\\nPush
) :记录用户向前的操作(如进入新页面
)。Pop
) :回退到过去的某个状态(如返回上一页
)。这种机制天然适合移动端应用的导航场景,因为它符合用户对“层级递进”
和“逐步返回”
的预期。
Web
开发:浏览器的历史记录(History API
)类似Navigator
的堆栈,但Navigator
更轻量且完全可控。Android
的Activity
和iOS
的ViewController
需要手动管理生命周期,而Flutter
的Navigator
通过堆栈自动处理。Push/Pop
)Navigator
最基础且最常用的功能,通过push
和pop
方法实现页面间的切换。
push
方法:跳转到新页面,新页面被压入堆栈顶部
。
// 1、匿名路由:直接传递页面对象 \\nNavigator.push(context, MaterialPageRoute(builder: (context) => DetailPage()));\\n \\n// 2、命名路由:通过路由名称跳转(需提前在`MaterialApp`中配置`routes`)\\nNavigator.pushNamed(context, \'/detail\');\\n
\\npop
方法:关闭当前页面,返回上一页(弹出堆栈顶部的页面
)。
Navigator.pop(context); // 返回上一页\\nNavigator.pop(context, \'返回值\'); // 返回上一页并传递数据\\n
\\n其他跳变种方法:
\\n// 1、pushReplacement:替换当前页面(常用于登录后跳转主页,销毁登录页) \\nNavigator.pushReplacement(context, MaterialPageRoute(builder: (context) => HomePage()));\\n\\n// 2、pushAndRemoveUntil:跳转到新页面并清空堆栈历史(如跳转到主页并清除所有登录流程页面) \\nNavigator.pushAndRemoveUntil(\\n context,\\n MaterialPageRoute(builder: (context) => HomePage()),\\n (route) => false, // 清空所有历史路由\\n);\\n
\\nNavigator
通过堆栈结构管理所有页面(Route
),确保页面切换符合“先进后出”
的逻辑。
堆栈操作:
\\nNavigator.of(context).widget.pages
获取当前所有页面。removeRoute
:从堆栈中移除指定页面。replace
:替换堆栈中的某个页面。典型场景:
\\nDialog
):弹窗本质是一个透明的页面,压入堆栈后覆盖在当前页面上。IndexedStack
保持多个页面的状态,避免重复重建。通过arguments
传递参数:
// 跳转时传递参数\\nNavigator.pushNamed(context, \'/detail\', arguments: \'user123\');\\n\\n// 目标页面接收参数\\nfinal userId = ModalRoute.of(context)!.settings.arguments as String;\\n
\\n通过构造函数传递(推荐
):
// 跳转时直接构造页面并传参\\nNavigator.push(context, MaterialPageRoute(\\n builder: (context) => DetailPage(userId: \'user123\'),\\n));\\n\\n// 目标页面定义构造函数\\nclass DetailPage extends StatelessWidget {\\n final String userId;\\n const DetailPage({required this.userId});\\n ...\\n}\\n
\\nNavigator
支持自定义页面切换动画,适应不同平台风格(如Android
的Material
和iOS
的Cupertino
)。
内置动画:
\\nMaterialPageRoute
:Android
风格的上下滑动渐变动画。CupertinoPageRoute
:iOS
风格的右侧滑入动画。自定义动画:
\\n通过PageRouteBuilder
实现完全自定义的动画效果。
Navigator.push(\\n context,\\n PageRouteBuilder(\\n transitionDuration: Duration(seconds: 1),\\n pageBuilder: (context, animation, secondaryAnimation) => DetailPage(),\\n transitionsBuilder: (context, animation, secondaryAnimation, child) {\\n return RotationTransition(\\n turns: animation, // 使用旋转动画\\n child: child,\\n );\\n },\\n ),\\n);\\n
\\n通过onGenerateRoute
和onUnknownRoute
实现路由拦截和权限控制。
场景示例:用户未登录时跳转到登录页。
\\nMaterialApp(\\n onGenerateRoute: (settings) {\\n if (settings.name == \'/profile\' && !isLoggedIn) {\\n return MaterialPageRoute(builder: (context) => LoginPage());\\n }\\n return MaterialPageRoute(builder: (context) => HomePage());\\n },\\n);\\n
\\n在无上下文(如Service
类)中跳转页面,需使用GlobalKey<NavigatorState>
。
// 定义全局Key\\nfinal GlobalKey<NavigatorState> navigatorKey = GlobalKey();\\n\\nMaterialApp(\\n navigatorKey: navigatorKey,\\n ...\\n);\\n\\n// 在任意位置跳转页面\\nnavigatorKey.currentState?.pushNamed(\'/detail\');\\n
\\n特征 | 说明 |
---|---|
声明式与命令式结合 | 既支持静态路由表(routes ),也支持动态跳转(push /pop )。 |
跨平台一致性 | 统一Android 和iOS 的导航逻辑,减少适配成本。 |
灵活性 | 支持参数传递、动画自定义、堆栈动态操作等复杂场景。 |
可扩展性 | 通过onGenerateRoute 和全局Key 实现路由拦截、无上下文跳转等高级功能。 |
build
方法中直接跳转:build
中调用push
可能导致页面重复跳转(因build
可能被多次调用)。Navigator.of(context)
的context
来自当前页面(如使用Scaffold
的context
)。MaterialApp
中路由相关属性详解MaterialApp
作为Flutter
应用的根组件,通过以下属性统一管理全局路由规则和导航行为:
属性 | 作用描述 | 示例场景 | 注意事项 |
---|---|---|---|
navigatorKey | 全局导航键(用于在无上下文时操作导航,如Service类中跳转页面)。 | 全局弹窗或登录状态管理。 | 需定义为GlobalKey<NavigatorState> 类型,并在MaterialApp初始化时赋值。 |
home | 应用的默认首页(未配置routes 或initialRoute 时生效)。 | 简单应用的唯一页面。 | 与initialRoute 冲突时,后者优先级更高。 |
routes | 定义静态命名路由表(路由名称 → 页面构造器)。 | 固定页面(如主页、登录页)。 | 路由名称建议定义为常量,避免硬编码。 |
initialRoute | 设置应用启动时的初始页面(需是routes 中已定义的路由名称)。 | 根据用户状态跳转到引导页或主页。 | 若同时设置home 属性,initialRoute 优先级更高。 |
onGenerateRoute | 动态生成路由(处理未在routes 中定义的路径,常用于带参数的页面跳转)。 | 动态详情页(如根据ID加载不同商品)。 | 必须返回一个Route 对象,否则会触发onUnknownRoute 。 |
onUnknownRoute | 处理未知路由(当所有路由规则均未匹配时调用)。 | 显示404错误页。 | 未实现此属性时,默认会抛出异常。 |
navigatorObservers | 导航观察器:监听导航事件(如路由跳转、页面生命周期),用于埋点统计、日志记录或权限拦截。 | 用户行为分析、页面停留统计、路由拦截。 | 需继承NavigatorObserver 类并重写方法(如didPush 、didPop )。 |
路由相关配置:
\\n// 定义全局Key \\nfinal GlobalKey<NavigatorState> navigatorKey = GlobalKey(); \\n\\nMaterialApp( \\n navigatorKey: navigatorKey, \\n routes: { \\n \'/home\': (context) => HomePage(), \\n \'/detail\': (context) => DetailPage(), \\n }, \\n initialRoute: \'/home\', \\n onGenerateRoute: (settings) { \\n if (settings.name == \'/mine\') { \\n return MaterialPageRoute(builder: (context) => MinePage(user: settings.arguments)); \\n } \\n return MaterialPageRoute(builder: (context) => NotFoundPage()); \\n }, \\n navigatorObservers: [MyObserver()],\\n); \\n\\n// 自定义观察者 \\nclass MyObserver extends NavigatorObserver { \\n @override \\n void didPush(Route route, Route? previousRoute) { \\n print(\'页面进入: ${route.settings.name}\'); \\n // 埋点:统计页面打开事件 \\n Analytics.trackPageView(route.settings.name); \\n } \\n\\n @override \\n void didPop(Route route, Route? previousRoute) { \\n print(\'页面退出: ${route.settings.name}\'); \\n // 埋点:统计页面关闭事件 \\n Analytics.trackPageClose(route.settings.name); \\n } \\n} \\n
\\nNavigator
核心属性详解Navigator
作为管理页面堆栈的组件,提供以下属性直接控制页面导航行为:
属性 | 作用描述 | 示例场景 | 注意事项 |
---|---|---|---|
pages | 直接定义页面堆栈(需与Page 类配合使用,适用于声明式导航)。 | 动态调整页面堆栈(如根据权限显示页面)。 | 需配合Navigator.pages API 使用,与命令式导航(push/pop )互斥。 |
onPopPage | 自定义页面返回逻辑(当用户触发返回操作时调用)。 | 拦截返回操作(如表单未保存提示)。 | 需返回bool 值,true 表示允许返回,false 表示拦截。 |
reportsRouteUpdateToEngine | 是否将路由变化通知底层引擎(适用于Web 或桌面端)。 | 在Web 应用中同步浏览器地址栏URL 。 | 默认值为true ,除非需要手动控制路由更新,否则无需修改。 |
transitionDelegate | 自定义页面切换动画的调度策略(如调整页面进入/退出的顺序)。 | 实现复杂动画效果(如共享元素过渡)。 | 需要继承TransitionDelegate 类并重写方法,适合高级场景。 |
声明式导航:
\\n// 声明式导航(通过pages属性) \\nNavigator( \\n pages: [ \\n MaterialPage(child: HomePage()), \\n if (showProfile) MaterialPage(child: ProfilePage()), \\n ], \\n onPopPage: (route, result) { \\n // 拦截返回操作 \\n if (route.didPop(result)) { \\n showProfile = false; \\n return true; \\n } \\n return false; \\n }, \\n); \\n
\\nMaterialApp
的路由属性:
路由表
、初始页面
、动态路由规则
。适用于整个应用
,提供静态和动态路由的基础设施
。Navigator
的属性:
特定子树中管理页面堆栈
(如底部导航栏
、侧边栏
)。直接操作页面堆栈
或自定义返回逻辑
。核心原则:
\\nMaterialApp
配置全局路由,简化跳转逻辑。多导航器嵌套
)中使用局部Navigator
,独立管理子页面堆栈。Navigator.push(context, MaterialPageRoute(builder: (context) => DetailPage())); \\n
\\n灵活性高
,但不利于统一管理。Navigator.pushNamed(context, \'/detail\'); \\n
\\nroutes
中的名称跳转。避免硬编码
。// 跳转时传参 \\nNavigator.pushNamed(context, \'/detail\', arguments: \'来自首页的参数\'); \\n\\n// 接收参数 \\nfinal args = ModalRoute.of(context)?.settings.arguments; \\n
\\n技术要点:
\\narguments
传递任意类型数据(如对象
、字符串
)。ModalRoute.of(context)
获取参数。// 跳转并等待结果 \\nfinal result = await Navigator.push(context, MaterialPageRoute(builder: (context) => LoginPage())); \\n\\n// 关闭页面时返回数据 \\nNavigator.pop(context, \'登录成功\'); \\n
\\n表单提交
、用户选择
等需要双向通信的情况。async/await
或then
处理异步返回。Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => SettingsPage())); \\n
\\n登录后跳转主页
,销毁登录页
)。[旧页面] → [新页面]
。Navigator.pushAndRemoveUntil( \\n context, \\n MaterialPageRoute(builder: (context) => LoginPage()), \\n (route) => false, // 清空所有历史路由 \\n); \\n
\\n(route) => false
表示移除所有现有路由。// 定义全局Key \\nfinal GlobalKey<NavigatorState> navigatorKey = GlobalKey(); \\n\\n// 绑定到MaterialApp \\nMaterialApp(navigatorKey: navigatorKey); \\n\\n// 在任意位置跳转 \\nnavigatorKey.currentState?.pushNamed(\'/detail\'); \\n
\\nService
、工具类
)中跳转页面。// 在onGenerateRoute中拦截未登录 \\nif (settings.name == \'/profile\' && !isLoggedIn) { \\n return MaterialPageRoute(builder: (context) => LoginPage()); \\n} \\n\\n// 拦截物理返回键(如Android返回按钮) \\nPopScope(\\n // 允许物理返回键返回\\n canPop: true,\\n child: ... \\n) \\n
\\n权限控制
、表单未保存提示
。需求描述:在大型企业级应用中,路由管理需要满足以下需求:
\\n避免代码臃肿
。动态控制路由访问权限
。跳转
、传参
、拦截
等逻辑。路由名称常量化
、类型安全传参
。lib/\\n├── routes/\\n│ ├── app_routes.dart # 路由名称常量\\n│ ├── route_config.dart # 全局路由配置\\n│ └── guards/ # 路由守卫\\n│ └── auth_guard.dart\\n├── modules/\\n│ ├── auth/ # 认证模块\\n│ ├── home/ # 主页模块\\n│ └── profile/ # 个人中心模块\\n└── main.dart\\n
\\napp_routes.dart
)/// 路由名称常量化,避免硬编码\\nclass AppRoutes {\\n static const String splash = \'/\';\\n static const String login = \'/login\';\\n static const String home = \'/home\';\\n static const String profile = \'/profile\';\\n static const String settings = \'/settings\';\\n static const String auth = \'/auth\';\\n}\\n
\\nauth_guard.dart
)import \'package:flutter/material.dart\';\\n\\nimport \'../app_routes.dart\';\\n\\n/// 路由守卫\\nclass AuthGuard {\\n /// 检测登录状态\\n static bool check({required BuildContext context}) {\\n // 假设从全局状态获取登录状态(如Provider、Riverpod)\\n final isLoggedIn = false; // 替换为实际状态检查\\n if (!isLoggedIn) {\\n // 未登录跳转到登录页\\n Navigator.pushNamed(context, AppRoutes.login);\\n return false;\\n }\\n return true;\\n }\\n}\\n
\\nroute_config.dart
)import \'package:flutter/material.dart\';\\nimport \'package:flutter_demo/modules/auth/auth_page.dart\';\\nimport \'../modules/not_found_page.dart\';\\nimport \'../modules/settings/settings_page.dart\';\\nimport \'../route/route_demo1.dart\';\\nimport \'../splash_page.dart\';\\nimport \'app_routes.dart\';\\nimport \'guards/auth_guard.dart\';\\n\\nclass RouteConfig {\\n /// 全局路由表\\n static Map<String, WidgetBuilder> routes = {\\n AppRoutes.splash: (context) => SplashPage(),\\n AppRoutes.login: (context) => LoginPage(),\\n AppRoutes.home: (context) => HomePage(),\\n AppRoutes.settings: (context) => SettingsPage(id: \\"\\",),\\n AppRoutes.profile: (context) => ProfilePage(),\\n AppRoutes.auth: (context) => AuthPage(),\\n };\\n\\n /// 动态路由生成逻辑(带权限控制)\\n static Route<dynamic>? onGenerateRoute(RouteSettings settings) {\\n // 权限拦截示例:访问个人中心需登录\\n if (settings.name == AppRoutes.profile) {\\n if (!AuthGuard.check(context: settings.arguments as BuildContext)) {\\n return null; // 已被路由守卫拦截\\n }\\n return MaterialPageRoute(builder: (_) => ProfilePage());\\n }\\n\\n // 动态参数路由示例:带ID的设置页\\n if (settings.name == AppRoutes.settings) {\\n final args = settings.arguments as Map<String, dynamic>;\\n return MaterialPageRoute(\\n builder: (_) => SettingsPage(id: args[\'id\']),\\n );\\n }\\n\\n // 其他未知路由跳转到404\\n return MaterialPageRoute(builder: (_) => NotFoundPage());\\n }\\n}\\n
\\nnot_found_page.dart
)import \'package:flutter/material.dart\';\\n\\nimport \'../routes/app_routes.dart\';\\n\\nclass NotFoundPage extends StatelessWidget {\\n final String? routeName;\\n\\n const NotFoundPage({super.key, this.routeName});\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n body: Center(\\n child: Column(\\n mainAxisAlignment: MainAxisAlignment.center,\\n children: [\\n const Text(\'404 - 页面未找到\', style: TextStyle(fontSize: 24)),\\n if (routeName != null) Text(\'请求路径: $routeName\'),\\n ElevatedButton(\\n onPressed: () =>\\n Navigator.pushReplacementNamed(context, AppRoutes.home),\\n child: const Text(\'返回首页\'),\\n ),\\n ],\\n ),\\n ),\\n );\\n }\\n}\\n
\\nMaterialApp
class RouteDemo2 extends StatefulWidget {\\n const RouteDemo2({super.key});\\n\\n @override\\n State<RouteDemo2> createState() => _MyAppState();\\n}\\n\\nclass _MyAppState extends State<RouteDemo2> {\\n /// 全局路由观察者\\n final RouteObserver<ModalRoute> routeObserver = RouteObserver<ModalRoute>();\\n\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n navigatorKey: GlobalKey<NavigatorState>(),\\n initialRoute: AppRoutes.splash,\\n routes: RouteConfig.routes,\\n onGenerateRoute: RouteConfig.onGenerateRoute,\\n onUnknownRoute: (settings) => MaterialPageRoute(\\n builder: (context) => NotFoundPage(routeName: settings.name),\\n ),\\n navigatorObservers: [routeObserver], // 可添加日志、埋点观察者\\n );\\n }\\n}\\n
\\nAppRoutes
)引用。高频访问页面
(如主页
)使用PageStorageKey
保持状态。Lazy Loading
)非核心模块页面。onUnknownRoute
中统一处理未知路由,跳转至友好错误页。try-catch
包裹可能抛出异常的路由跳转逻辑。路由系统是Flutter
应用的脉络体系,理解其堆栈机制如同掌握城市交通的枢纽控制
。系统化学习要抓住三个关键维度:
导航树结构
决定跳转范围。通信格式
。状态驱动
体现声明式精髓。开发者应建立\\"路由即状态\\"
的认知,将导航操作转化为路由堆栈的状态变更
。优秀的导航设计要让代码自己讲述页面流转的故事。建议通过绘制路由拓扑图辅助设计,先用好原生Navigator
再拓展路由库,最终达到\\"手中无路由,心中有堆栈\\"
的境界。
\\n","description":"前言 在移动应用开发中,页面跳转如同城市交通的路线规划,而Flutter的路由系统就是你的智能导航。你是否遇到过页面堆栈混乱、参数传递丢失、返回逻辑失控的困境?\\n\\n许多新手开发者因为缺乏对路由系统的系统认知,导致应用后期出现难以维护的\\"面条式跳转\\"。路由不仅是页面切换的工具,更是构建清晰应用架构的核心。理解其底层机制,就像掌握了应用空间的拓扑学,能让你游刃有余地管理复杂的页面流。\\n\\n本章将带你以系统化思维拆解Flutter路由,从底层原理到高阶技巧,构建完整的导航知识体系。\\n\\n操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。\\n\\n一、基础认知\\n1.1…","guid":"https://juejin.cn/post/7482236799269109800","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-17T02:12:56.058Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/66b7e550ec8d4eee9ff877aa00724c00~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1742796773&x-signature=9YUc%2FEl5%2BNfxYOZqTmFrQXktM%2Bk%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"Flutter&Flame 游戏实践#22 | 全平台游戏盒#1","url":"https://juejin.cn/post/7482037724397223974","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
该系列是 [张风捷特烈] 的 Flame 游戏开发教程。Flutter 作为 全平台 的 原生级 渲染框架,兼具 全端
跨平台和高性能的特点。目前官方对休闲游戏的宣传越来越多,以 Flame 游戏引擎为基础,Flutter 有游戏方向发展的前景。本系列教程旨在让更多的开发者了解 Flutter 游戏开发。
\\n\\n第一季:30 篇短文章,快速了解 Flame 基础。
\\n[已完结]
\\n第二季:从休闲游戏实践,进阶 Flutter&Flame 游戏开发。
两季知识是独立存在的,第二季 不需要 第一季作为基础。本系列教程源码地址在 【toly1994328/toly_game】,系列文章列表可在《文章总集》 或 【github 项目首页】 查看。
\\n距离上一篇已经七个多月了,Flame 最新版本已经来到 1.26.0,之前用的还是 1.18.0
。以前我们已经通过Flame 搭建了不少小游戏,接下来打算做一个 全平台游戏盒 ,将之前的东西整合到一个全平台的 App 里:
TolyGameBox 游戏盒计划有几个好处:
\\n游戏盒将采用模块化的设计,每个游戏会作为独立的模块维护,每个模块可以独立运行,以便单独维护开发。新版代码将在 toly_game/game_box
分支进行开发。 Github 开源地址 (多多Star 哦~)
TolyGameBox 将先在桌面端开发,后续适配移动端。本文主要目的是:
\\nTolyGameBox 将使用 tolyui 和 fx_framework 进行构建。其中:
\\n相较而言, tolyui 中的组件随意在任何 Flutter 项目中使用,是非常灵活无侵入的。而 fx_framework 则是希望制定一套 App 开发的流程,便于快速开发,但必须遵循 fx 的使用方式。fx_framework 目前处于尝试阶段 api 并不稳定,感兴趣的朋友可以体验一下:
\\n---\x3e[pubspec.yaml]---\\ndependencies:\\n tolyui: 0.0.4+8\\n fx_framework: 0.0.1\\n
\\n对于一个应用程序而言,如何优雅地启动是个问题。比如加载资源的时机、确定异常检测、成功时跳转等。\\nfx_framework 中的 fx_boot_starter 模块封装了启动流程,它规定了在哪里写初始化的逻辑、如何监听启动的状态。
\\n如下所示,进入应用时展示 Splash 界面,资源加载成功后跳转到首页:
启动的代码如下,主要在 starter
文件夹下,其中自定义了 TolyGameBoxApp
启动器,在 main 中调用启动器的 run
方法即可工作:
----\x3e[lib/main.dart]----\\nvoid main(List<String> args) => const TolyGameBoxApp().run(args);\\n
\\n启动器要指定一个泛型作为初始化的结果数据;需要提供 app 组件作为应用的入口,以及 AppStartRepository 仓储来处理异步的初始化任务。在整个启动流程中中,基于 onLoaded
、onStartSuccess
、onStartError
可以定制不同的业务逻辑; onGlobalError 可以监听全局的异常。
\\n这里在 onStartSuccess 时跳转到 gameCenter 主界面:
class TolyGameBoxApp with FxStarter<AppConfig> {\\n const TolyGameBoxApp();\\n\\n @override\\n Widget get app => const TolyGameBox();\\n\\n @override\\n AppStartRepository<AppConfig> get repository => const TolyGameBoxRepo();\\n \\n \\n @override\\n void onGlobalError(Object error, StackTrace stack) { }\\n\\n @override\\n void onLoaded(BuildContext context, int cost, AppConfig state) {}\\n\\n @override\\n void onStartError(BuildContext context, Object error, StackTrace trace) {}\\n\\n @override\\n void onStartSuccess(BuildContext context, AppConfig state) {\\n context.go(AppRoute.gameCenter.url);\\n }\\n\\n}\\n
\\n启动器的仓储单独通过一个类来维护,是考虑到将应用初始化的逻辑强行剥离出来,引导开发者做好逻辑的隔离。这里在其中设置了桌面端的窗口尺寸。后期可以定制一些 App 的配置参数,在这里进行加载初始化:
\\nclass TolyGameBoxRepo implements AppStartRepository<AppConfig>{\\n\\n const TolyGameBoxRepo();\\n\\n @override\\n Future<AppConfig> initApp() async {\\n WindowSizeAdapter.setSize();\\n // TODO 加载资源,创建 AppConfig 对象\\n return AppConfig();\\n }\\n\\n}\\n
\\n应用导航基于官方的 go_router, 导航 2.0 可以很方便地实现局部嵌套路由,如下所示,界面切换的过程中可以保持左侧的导航栏不懂,右侧的面板内容进行局部导航。
\\n导航相关的代码在 navigation
文件夹下,其中:
app_route 中定义路由树的根节点: 注意要在初始路由界面组件中加上 AppStartListener
来监听应用的启动事件,比如这里启动的首屏是 splash
界面:
----\x3e[lib/navigation/router/app_route.dart]----\\nRouteBase get appRoute {\\n return GoRoute(\\n path: AppRoute.home.path,\\n redirect: (_, __) => null,\\n routes: [\\n GoRoute(\\n path: AppRoute.splash.path,\\n builder: (_, __) => const AppStartListener<AppConfig>(child: Splash()),\\n ),\\n if(kAppEnv.isDesktopUI)\\n deskHomeRoute,\\n ],\\n );\\n}\\n
\\n为了更好的维护全局通知、主题,TolyUI 对 MaterialApp
进行了全量的封装,提供了 TolyUiApp
,使用方式和 MaterialApp 完全一致:
deskHomeRoute 是桌面端的嵌套路由,通过 ShellRoute
定义,在 DeskNavigation
组件的局部区域进行导航:这里通过 pageBuilder 指定 NoTransitionPage 可以让局部路由切换时立刻改变,不进行动画。当然你喜欢动画效果的话,也可以定制路由动画:
RouteBase get deskHomeRoute => ShellRoute(\\n builder: (_, __, Widget child) => DeskNavigation(content: child),\\n routes: [\\n GoRoute(\\n path: AppRoute.gameCenter.path,\\n pageBuilder: (_, __) => const NoTransitionPage(child: GameCenterPage()),\\n ),\\n GoRoute(\\n path: AppRoute.save.path,\\n pageBuilder: (_, __) => const NoTransitionPage(child: SavePage()),\\n ),\\n GoRoute(\\n path: AppRoute.collect.path,\\n pageBuilder: (_, __) => const NoTransitionPage(child: CollectPage()),\\n ),\\n GoRoute(\\n path: AppRoute.mine.path,\\n pageBuilder: (_, __) => const NoTransitionPage(child: MinePage()),\\n ),\\n GoRoute(\\n path: AppRoute.settings.path,\\n pageBuilder: (_, __) => const NoTransitionPage(child: SettingsPage()),\\n ),\\n ],\\n );\\n
\\n另外,路由相关的固定字符串,通过 AppRoute
枚举统一维护:
enum AppRoute {\\n home(\'/\', url: \'/\'),\\n splash(\'splash\', url: \'/splash\'),\\n startError(\'start_error\', url: \'/start_error\'),\\n globalError(\'404\', url: \'/404\'),\\n gameCenter(\'game_center\', url: \'/game_center\'),\\n save(\'save\', url: \'/save\'),\\n collect(\'collect\', url: \'/collect\'),\\n mine(\'mine\', url: \'/mine\'),\\n settings(\'settings\', url: \'/settings\'),\\n ;\\n\\n final String path;\\n final String url;\\n\\n const AppRoute(this.path, {required this.url});\\n}\\n
\\nDeskNavigation 组件负责构建桌面端整体视图,它呈左右结构,左侧是导航菜单, 右侧整体是局部导航区:
\\nclass DeskNavigation extends StatelessWidget {\\n final Widget content;\\n\\n const DeskNavigation({super.key, required this.content});\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n backgroundColor: Colors.white,\\n body: Container(\\n decoration: const BoxDecoration(gradient: bgGradient),\\n child: Row(\\n children: [\\n const DeskNavigationRail(),\\n const VerticalDivider(),\\n Expanded(child: content),\\n ],\\n ),\\n ),\\n );\\n }\\n}\\n\\nconst Gradient bgGradient = LinearGradient(\\n colors: [Color(0xFF0A0A12), Color(0xFF1A1A2C)],\\n stops: [0.3, 0.8],\\n begin: Alignment.topLeft,\\n end: Alignment.bottomRight,\\n);\\n
\\n左侧导航使用 tolyui 中的 TolyRailMenuBar 组件构建,首先准备侧栏菜单的数据 MenuMeta 列表:
\\n----\x3e[lib/navigation/view/desktop/rail_navigation.dart]---\\nclass DeskNavigationRail extends StatelessWidget {\\n const DeskNavigationRail({super.key});\\n\\n List<MenuMeta> get navMenus => [\\n MenuMeta(\\n icon: TolyGameIcon.game_center,\\n label: \\"首页\\",\\n router: AppRoute.gameCenter.url\\n ),\\n MenuMeta(\\n icon: TolyGameIcon.save,\\n label: \\"存档\\",\\n router: AppRoute.save.url,\\n ),\\n MenuMeta(\\n icon: TolyGameIcon.collect,\\n label: \\"收藏\\",\\n router: AppRoute.collect.url,\\n ),\\n MenuMeta(\\n icon: TolyGameIcon.mine,\\n label: \\"我的\\",\\n router: AppRoute.mine.url,\\n ),\\n ];\\n\\n @override\\n Widget build(BuildContext context) {\\n // TODO 构建侧栏\\n }\\n}\\n
\\n视图构建逻辑如下:
\\nGoRouterState.of
可以通过上下文得到 当前激活路由
,以此得到激活路径为 activeId 赋值,这样界面路由变化时,就可以重新构建激活对应的索引;context.go
就可以在点击时跳转到对应菜单的路由地址;GameMenuCell
组件实现悬浮时颜色渐变的菜单项效果:@override\\nWidget build(BuildContext context) {\\n final String activePath = GoRouterState.of(context).uri.toString();\\n final bool isSetting = activePath == AppRoute.settings.url;\\n return DragToMoveWrapper(\\n child: TolyRailMenuBar(\\n width: 68,\\n gap: 10,\\n padding: const EdgeInsets.symmetric(horizontal: 6),\\n cellBuilder: GameMenuCell.create,\\n animationConfig: const AnimationConfig(type: AnimTickType.hove),\\n leading: (type) => const TolyGameLogo(),\\n menus: navMenus,\\n activeId: activePath,\\n backgroundColor: Colors.transparent,\\n onSelected: context.go,\\n tail: (_) => SettingButton(active: isSetting),\\n ),\\n );\\n}\\n
\\n此时导航区就可以支持切换了,本文主要关注首页追踪游戏中心的展示,其他几个界面后面有时间再继续完善:
\\n现在希望在游戏中心展示展示之前完成的五个游戏,并且在点击时进入对应的游戏界面。这样就完成了 Flutter 应用和 Flutter 游戏的整合,我们要牢记一点,Flame 的游戏视图本质上也是一个 Widget,所以可以无缝地集成到任何的 Flutter 界面中:
\\n游戏中心界面目前就是一个很简单的 GridView 网格列表,以下面的数据来填充界面。这里就不展开介绍了,感兴趣的可以参见源码 GameCenterPage :
\\n[\\n {\\n \\"title\\": \\"经典扫雷\\",\\n \\"id\\":\\"sweeper\\",\\n \\"image\\": \\"assets/images/cover/sweeper.webp\\",\\n \\"create_at\\": \\"2024-05-07\\"\\n },\\n {\\n \\"title\\": \\"恐龙快跑\\",\\n \\"id\\":\\"trex\\",\\n \\"image\\": \\"assets/images/cover/trex.webp\\",\\n \\"create_at\\": \\"2024-03-04\\"\\n },\\n {\\n \\"title\\": \\"经典打砖块\\",\\n \\"id\\":\\"brick\\",\\n \\"image\\": \\"assets/images/cover/brick.webp\\",\\n \\"create_at\\": \\"2024-03-15\\"\\n },\\n {\\n \\"title\\": \\"生命游戏\\",\\n \\"id\\":\\"life_game\\",\\n \\"image\\": \\"assets/images/cover/life_game.webp\\",\\n \\"create_at\\": \\"2024-03-15\\"\\n },\\n {\\n \\"title\\": \\"贪吃蛇\\",\\n \\"id\\":\\"snake\\",\\n \\"image\\": \\"assets/images/cover/snake.webp\\",\\n \\"create_at\\": \\"2024-08-19\\"\\n }\\n]\\n
\\n这里重点介绍一下,一个游戏如何以独立的模块存在;一个游戏模块具有它所依赖的所有资源,外界只需要引入就可以访问该游戏界面。这里拿扫雷来说,通过如下命令可以创建一个 sweeper
模块包:
\\n\\nflutter create --template=package sweeper
\\n
扫雷相关的所有代码都在这里维护,包括游戏的图片资源。把之前的代码全部拷过来,就完成了 99% 的迁移工作:
\\n对于模块包来说,最需要注意的一点是,资源使用时需要加上包名前缀:完整路径为:
\\n\\n\\npackages/<模块包名称>/<资源在包内的相对路径>
\\n
之前加载资源使用的是自定义的资源加载器 TextureLoader, 现在需要对其稍加改造,让它支持指定模块加载资源。扫雷中的资源都是 svg ,从源码中可以看出,load 资源时可以传入 AssetsCache
,它可以指定资源的前缀:
现在将 TextureLoader
修改如下,构造时可以传入 package
参数,表示当前模块。构造时如果 package 非空,创建指定包名的前缀,另外 Images 对象是 flame 中加载普通图片资源的类。加载资源时使用 cache 对象即可:
class TextureLoader {\\n final String? package;\\n AssetsCache? cache;\\n Images imageCache = Flame.images;\\n\\n TextureLoader({this.package}) {\\n if (package != null) {\\n cache = AssetsCache(prefix: \'packages/$package/assets/\');\\n imageCache = Images(prefix: \'packages/$package/assets/\');\\n }\\n }\\n
\\n对于扫雷游戏来说, Flame 1.18.0 -> 1.26.0 间并没什么破坏性的更新。迁移后,游戏运转正常。嵌入到当前的游戏盒中也非常简单,添加一个跳转的 route 即可,点击时,推入路由:
\\n其中 SweeperPage
使用 sweeper 模块提供的 SweeperGamePanel
视图,作为界面的一部分,你还可以自定义一些其他的信息,让游戏和应用完美融合:
class SweeperPage extends StatelessWidget {\\n const SweeperPage({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n backgroundColor: Colors.transparent,\\n body: Column(\\n children: [\\n CustomDeskTopBar(title: \'经典扫雷\',leading: BackButton(\\n onPressed: context.pop,\\n ),),\\n const Expanded(child: SweeperGamePanel()),\\n ],\\n ),\\n );\\n }\\n}\\n
\\n到这里,就完成了一个最简单的游戏展示和点击打开的游戏盒。后面继续把其他几个小游戏也按照模块的方式集成进去即可。以后再写什么小游戏,就可以在这里安家了。如果有朋友写了什么好玩的游戏,也可以放一个模块进来,让 TolyGameBox 不断壮大。那本文就到这里,后续更多精彩内容,敬请期待 ~
\\n更多文章和视频知识资讯,大家可以关注我的公众号、掘金和 B 站 。
\\n","description":"Flutter&Flame 游戏开发系列前言: 该系列是 [张风捷特烈] 的 Flame 游戏开发教程。Flutter 作为 全平台 的 原生级 渲染框架,兼具 全端 跨平台和高性能的特点。目前官方对休闲游戏的宣传越来越多,以 Flame 游戏引擎为基础,Flutter 有游戏方向发展的前景。本系列教程旨在让更多的开发者了解 Flutter 游戏开发。\\n\\n第一季:30 篇短文章,快速了解 Flame 基础。[已完结] \\n 第二季:从休闲游戏实践,进阶 Flutter&Flame 游戏开发。\\n\\n两季知识是独立存在的,第二季 不需要 第一季作为基础…","guid":"https://juejin.cn/post/7482037724397223974","author":"张风捷特烈","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-16T23:12:43.002Z","media":[{"url":"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/54722ac795b141b99dffaba659f4cab0~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=2480&h=1486&s=115730&e=webp&b=2c0b06","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ac8d4b56219d40238072fe4a5eef0eb5~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1019&h=193&s=20812&e=png&b=ffffff","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/16a19368770042aea9f2c90c5c757e86~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1546&h=633&s=76405&e=png&b=ffffff","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/91420b7b375e4a18af6ee7554fa294e4~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1364&h=1011&s=474247&e=png&b=11111e","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8c1e3f98019a442398bac25b669fd788~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=957&h=708&s=475522&e=gif&f=68&b=15111c","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d33c335090524f29a1cc4bde857d6167~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=887&h=353&s=31123&e=png&b=ffffff","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/11e181f90f2146d590903265e9b88b9f~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=957&h=708&s=328921&e=gif&f=143&b=141424","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/aa580ab857004cf5bc595c5cb8da04bc~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=586&h=308&s=12508&e=png&b=f4f6fa","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/37b8e89475ee4a96be9c271cad939ae8~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=731&h=312&s=43134&e=png&b=fffefe","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e5cac391af7f46e1a7d84ccc8c2c20a8~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1364&h=1011&s=470780&e=png&b=11111e","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/11e181f90f2146d590903265e9b88b9f~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=957&h=708&s=328921&e=gif&f=143&b=141424","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/596ea0fbfecb48389bcbb117d93d2b07~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=957&h=708&s=372793&e=gif&f=128&b=141424","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b9072f5ad83e4be0b2d0f558b7095096~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=721&h=259&s=9688&e=png&b=f4f5f8","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a3d637e3a3754c0180de8cfbe58a9e4c~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=700&h=243&s=31782&e=png&b=ffffff","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c76ffe3ceeb545ef8a17e854ebe11877~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=684&h=257&s=33638&e=png&b=fffefe","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/54f1a08013984e08b374451bbc215233~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=682&h=87&s=11643&e=png&b=ffffff","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","游戏开发"],"attachments":null,"extra":null,"language":null},{"title":"Android PC 要来了?Android 16 Beta3 出现 Enable desktop experience features 选项","url":"https://juejin.cn/post/7482013975077896227","content":"在之前的 《Android 桌面窗口新功能推进》 我们就聊过,Google 就一直在努力改进 Android 的内置桌面模式,例如添加了适当的窗口标题、捕捉窗口的能力、悬停选项、窗口大小调整、最小化支持、app-to-web 等。
\\n比如在搭载 Android 15 QPR 1 Beta 2 的 Pixel 平板,可以通过 Settings > Developer options > Enable freeform windows 体验到桌面窗口的新功能:
\\nAndroid 15 QPR1 Beta 1 下,可以在 Settings > Developer options > Enable freeform windows 看到一个 Enable desktop mode on secondary display,也就是在辅助显示器上启用桌面模式的支持,它取代了以前版本的 Android 中旧的 Force desktop mode 切换:
\\n还有对应桌面窗口模式下的多任务支持、多实例支持、最小化按钮等等:
\\n现在同步还有之前的 《Linux 终端可能登陆 Android 平台》 ,目前已经在 Pixel 上推出了 Debian Linux 终端 App ,官方称相应 App 将在安卓 16 正式开放,第一次使用需要下载 500MB 左右的 Debian 系统,终端 App 中支持用户调节磁盘空间、设置网络端口等。
\\n并且在 Android 16 Beta 3 里面,Linux Terminal 支持多 Tab 添加,具有 Title、关闭按钮和打开新选项卡的按钮等,甚至还能通过 Display 进一步打开图形化的 Linux 应用:
\\n\\n\\n目前 Linux 终端使用的是 Android 虚拟化框架 (AVF) 解压缩并在虚拟机中。
\\n
而在 Android 16 Beta 3 时,androidauthority 发现了相关的 Enable desktop experience features
字符串,目前该功能命名为 Desktop View ,只是功能目前还未上线,但是从这一点可以看到,Android 桌面化的步伐正在一步步完善,androidauthority 表示桌面视图将是适用于 Android 手机的成熟桌面模式体验 :
<string name=\\"enable_desktop_experience_features\\">Enable desktop experience features</string>\\n<string name=\\"enable_desktop_experience_features_summary_with_desktop\\">Enable Desktop View on the device and on secondary displays.</string>\\n<string name=\\"enable_desktop_experience_features_summary_without_desktop\\">Enable Desktop View on secondary displays.</string>\\n
\\n同时,为了让 Android 更好适配传统 PC 显示场景,在前面 Android 15 的辅助显示器上启用桌面模式基础上,Google 正在 Android 16 中测试新的外部显示管理工具:
\\n在 Google Pixel 手机连接到外部显示器时,可以选择镜像屏幕,也可以通过开发者选项改为扩展屏幕,但是,目前这个实现存在一些问题:
\\n而为了解决这两个问题:
\\n可以看到,Android 开始支持排列窗口去匹配每个显示器的实际边界,另外,用户还可以通过切换 “mirror built-in display” 在镜像和扩展屏幕之间切换,甚至调整外部显示器上文本和图标的大小,与内置显示器的大小分开等。
\\n\\n\\n暂时还缺少控制外接显示器刷新率的能力。
\\n
目前在 Android 16 Beta 里可以发现这些尚未发布的能力,虽然不知道它是否会在即将到来的 Android 16 稳定版本中推出,但是目前我们可以看到,Android 桌面化已经集齐:
\\n可以联想,其实 Android 的桌面版本完全体应该并不远了,将 Chrome OS 过渡到基于 Android 也许真的是 Google 的目标之一,过去传闻 Google 正在 Android 上重建 ChromeOS 并非空穴来分,同时 ChromeOS 也开始使用一些 Android 能力,所以未来的 Chromebook 会完全运行 Android 系统这种可能性很高。
\\n那么,未来出门办公就带一台 Android 手机作主机,也许也会是一种不错的场景。
\\n\\n\\n版权声明:本人文章仅在掘金平台发布,请勿抄袭搬运,转载请注明作者及原文链接 🦉
\\n阅读提示:网页版带有主题和代码高亮,阅读体验更佳 🍉
\\n
废话不多说,先来一波截图纯享。
\\n很早之前就有自己做一个听歌 app 的想法,觉得各大音乐 app 广告过于烦人。特别是某易云,页面臃肿丑陋,广告不胜其烦。
\\n很早就关注了一个知名音乐 API 的 nodejs 后端项目,准备着什么时候大显身手。不过该知名项目已经被抬了,虽然有 fork 项目,但是不可避免会存在法律风险,就算是自己做着玩,也不想沾染是非,索性重新构思和寻找合适的方式开发。
\\n一方面是思考使用的技术栈,一方面是寻找合适的接口服务,P2P 的方式也很危险,而且需要架设服务器,也不合适,就这样一直寻找和等待。
\\n年前,无意中发现了一款听歌 app,叫哔哔音乐,当时先在某社区发现的软件,号称免费听歌,所以先使用的软件,用下来就发现,果然可以免费收听很多付费音乐,且曲库还挺全,热门歌曲基本都在。
\\n我发现搜索的结果基本都带小破站的标识,发现其基本上就是小破站搜索的结果,只是转换成音频流了。
\\n此时,我虽然对此有点感受,但是毕竟是人家开发的 app,很好奇人家的实现,但是自己确实没办法。直到有一天,我想着这个 app 没准也是掘金哪个好心肠的 giegie~ 开发的,可能还写了文章,我闲来无事就搜了一下,果不其然。在大佬 阿炸克斯 的文章下面发现了哔哔音乐的踪迹,顺藤摸瓜找到了 github 仓库。
\\n于是,我的基础条件就满足了最重要的一个:歌源。
\\n有了歌源实现,我最大的工作量就是重写 UI 而已。但是对于技术栈,我其实考虑了一番,在 react-native 和 flutter 之间犹豫了一下。
\\n原因无非就是,rn 更贴近我 web 的开发习惯,很多东西我不用再学。而 flutter 我需要重新学习语言、框架、开发模式等等,进程阻碍不小。但是经过一番研究和思考,我还是选择了 flutter,理由如下:
\\n1.rn 已经不同于传统 web 开发了,要学习的东西一点不少
\\n2.rn 毕竟是 web 技术,有更大的局限性,坑是一点不少
\\n3.rn 在不同系统的表现,也就是 UI 一致性上,不如 flutter
\\n4.本着来都来了的心态,不如大胆一试,尝试下全新的领域
\\n所以技术栈就定了 flutter。
\\n于是,开发这样一个免费听歌、无广告的音乐 app 所需要的基本条件,我就准备好了。
\\n紧接着就是紧锣密鼓地学习 flutter,这是对于我来说完全陌生的领域,不过,好在现在学习资源很丰富。掘金上关于 flutter 最权威的教学,应该是 张风捷特烈 出的一系列小册和文章。我花了几天时间过了 dart 语法,简单写了官网案例,迅速过了一遍大佬的项目后,就准备开发了。
\\n将 阿炸克斯 的项目 fork 并 clone 下来后,我却迟迟敲不下第一行代码。
\\n这里有两个最大的难点,是功能设计和页面设计。
\\n可能会有人说,不是有 AI 吗?页面设计交给 AI 就好了。
\\n我当初也试了各大 AI,但是出图效果惨不忍睹。
\\n好在我有点设计功底,当初的摄影、ppt 设计都没白学,审美还在,再说了,实在没法设计就去抄呗,那么多 app 的设计案例摆着,够用了。
\\n然后就是功能设计,毕竟先设计功能才能设计 UI,想要哪些功能,这个得考虑清楚。
\\n第一个,我需要的就是某狗音乐的播放队列功能,就是,我可以将想听的歌都临时放进一个列表,优先播放列表里的歌曲,这个功能对我来说非常喜欢,这也是我一直使用某狗的原因。
\\n举个 🌰,其它 app 里可能就是提供一个歌单给你,你往里面添加歌曲,你每次听这个列表,可能就是列表循环。但是实际上,我在某些时候,可能就只想听某些歌,单曲循环不合适,但是那些歌我都想听,或者我想先听 3 遍薛之谦的演员,5 遍周杰伦的夜曲。那么播放队列这个功能就很合适。
\\n某狗音乐的交互,在老版本,是列表中歌名左侧有一个 +,点击 + 就会添加到待播放列表,并且支持重复添加,任意顺序。现在是取消 +,直接点击歌名左侧封面。
\\n这个功能是我想要的。
\\n除此之外,导入本地下载歌曲,这是核心功能,如果你不能导入本地歌曲,基本可以说这个 app 就是个鸡肋,总不能还要来回切换 app 听歌。
\\n还有就是歌单,哔哔音乐最大问题就是不能在本地新建歌单,那么需要实现一个歌单的增删改查。
\\n所以目前就明确了 app 的核心功能:
\\n1.支持导入本地下载的音乐
\\n2.支持自由新建歌单(不过歌单建多了也不方便,所以限制最多 20 个)
\\n3.支持搜索互联网歌曲,免费收听,且曲库够大(这点哔哔音乐已经实现了,可以直接使用)
\\n4.支持待播放队列
\\n目前这些功能都已支持,另外在设置页还支持了主题色更换,后续可能会考虑暗黑模式,桌面端适配等。不过这都是后话,我自己不对此承诺。
\\n再聊回技术。
\\n首先我觉得最大的阻碍是搭建环境,开发安卓需要安装 jdk、android studio 等,这个过程会很难受,因为你可能会好几天都跑不起来项目,不是这里错就是那里错,好在现在有了 AI,这些问题不再那么恶心了。
\\n再一个,flutter 的生态问题,flutter 确实已经有不少很好用的库了,基本你想要的都有,但是很多库的维护是一个大问题,有些库你去看,可能最近一次更新是在三年前。这就是flutter生态现状,flutter 使用群体和生态远没有 web 那么繁荣。flutter 库的开发基本不会带来什么收益,哪怕说在 github 挣两颗星星,也要你是明星项目才行,加上使用群体本就不大(相较于 web),所以很难让人持续维护。
\\n比如,知名的 flutter 库 getx,其作者由于个人原因,近两年没有更新,基本是一些零散 commit,有个老哥每天在 issue 区抱怨为什么还没有升级到 5 版本,为什么还没有适配最新的 flutter。
\\n这就是现状。
\\n但是别慌,基本你常用的,要用的那些库都在稳定更新,那些不知名的小库,停更就停更吧,反正也指望不上。
\\n在开发这个项目的过程中,我用到了下面的库:
\\n1.just_audio:这是用于音乐播放的知名库
\\n2.bot_toast:toast 提示库,挺好用的
\\n3.provider:知名状态管理库,同作者还有一个 riverpod,上面提到的 getx 也是一个强大的状态管理库,不过getx 除了状态管理还有响应式数据、简洁路由等,下次开发我必然用上 getx
\\n4.dio:知名 http 请求库,用就完事了。不过这部分我没怎么涉及,因为涉及网络请求的地方,哔哔音乐基本都做了
\\n5.sqflite:重量级嘉宾,由于没有后端服务,想要实现列表存储、下一首、上一首等功能,就必须使用到本地数据库,简单的本地存储是没法应对大量的数据的,所以 sqflite 是必然使用的库
\\n6.event_bus:跨页面调用方法会用到,也是挺方便的
\\n7.flutter_easyloading:简单好看的 loading 效果库
\\n8.audio_metadata_reader:音频元信息读取,例如本地音频扫出来后,需要读取音频的时长、作者等信息,就需要这个,不过这里应该不是最佳方案,因为其无法读取封面等信息,存在一定的缺陷,好在我这里并不是很需要封面,用用也没事,最主要的是这个库在稳定维护
\\n在开发过程中,我都在大量使用 AI,特别是本地音频扫描以及播放器部分,没有 AI 我可能都做不出来本地音频扫描这个功能,当初这块功能甚至让我熬夜到凌晨都没解决,好在综合多个 AI 后,在 deepseek 的帮助下,在查找了一些资料后还是解决了。
\\n这里不得不说,这方面的 flutter 生态比较薄弱,我在这里卡壳也是因为 AI 提供的代码示例里使用的第三方库,基本都断更几年了,要么不支持 dart3,要么就是不适配最新 flutter 了,编译会有问题。
\\n甚至 AI 会让你编写原生代码实现,但是我并不想写原生代码,硬是死磕,最后还是被我解决了。只是目前的方案只适配安卓,其它端并不支持。
\\n项目从一月中开始开发,到现在历时两个多月,基本没断过,过年的每一天也是在开发,app 已经接近稳定了。
\\n想下载体验的朋友请访问百度网盘:啵啵音乐,提取码:6w7y。
\\n想看源码的朋友请访问 github 仓库:bobomusic。喜欢的话给个 star,谢谢。
\\n 搭建一个快速开发油猴脚本的前端工程 18+
👍🏻 28+
💚
金九银十招聘季,IT 打工人,该怎么识别烂公司好公司? 70+
👍🏻 80+
💚
别人休息我努力,悄悄写个 cli 工具,必须提升效率,skr~ 60+
👍🏻 110+
💚
一文掌握 eslint,再也不怕项目报错 20+
👍🏻 30+
💚
开发一个 npm 库应该做哪些工程配置? 40+
👍🏻 50+
💚
分享我在前端学习与开发中用到的神仙网站和工具 40+
👍🏻 110+
💚
uniapp 踩坑记录(二) 130+
👍🏻 150+
💚
闲来无事,摸鱼时让 chatgpt 帮忙,写了一个 console 样式增强库并发布 npm 100+
👍🏻 110+
💚
uniapp 初体验踩坑记录 30+
👍🏻 60+
💚
两小时学会 JS 正则表达式,终身不忘 50+
👍🏻
\\n在移动应用开发中,路由系统如同应用的
导航中枢
,决定着用户在不同界面间的流转体验。Flutter
通过精巧的类层次设计和分层抽象机制,构建了一套灵活高效的路由管理体系。
本文从路由(Route
)与导航器(Navigator
)的基础概念切入,深入剖析MaterialPageRoute
、CupertinoPageRoute
等核心类的继承关系与协作原理,解读路由堆栈模型的生命周期控制策略,并通过Hero
动画、全局路由监听
等进阶案例,揭示如何利用TransitionBuilder
实现跨平台风格切换。
无论你是刚接触Flutter
的新手,还是希望优化复杂页面跳转逻辑的资深开发者,本文都将为你打开路由系统的核心黑匣子。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\nRoute
:路由表示一个独立的页面(或界面
),封装了页面内容(Widget
)和页面切换的逻辑(如动画
、传参
)。换言之,即对应用内页面(Screen/Page
)的抽象,定义了路由的基本行为和属性。其核心职责包含:
buildPage
方法构建目标Widget
树。Navigator
:导航器push
(添加新页面
)和 pop
(移除当前页面
)操作控制页面跳转。Navigator
,可以通过 Navigator.of(context)
访问。路由以堆栈(Stack
) 结构管理,遵循“后进先出”
(LIFO
)规则。
例如:
\\n压入堆栈顶部
。顶部页面会被弹出
。类层次结构:
\\nObject\\n ↳ Route (抽象类)\\n ↳ OverlayRoute (抽象类)\\n ↳ TransitionRoute (抽象类,处理过渡动画)\\n ↳ ModalRoute (抽象类,模态路由)\\n ↳ PageRoute (抽象类,全屏路由)\\n ↳ MaterialPageRoute / CupertinoPageRoute\\n
\\nRoute
:抽象类所有路由的基类,定义路由的基本行为和属性
。其核心职责包含:
install
、dispose
等生命周期方法,管理路由的创建与销毁。isActive
)、是否遮挡其他路由(opaque
)。navigator
属性绑定到所属的 Navigator
。OverlayRoute
(抽象类)核心职责:
\\nOverlayEntry
将路由内容插入全局 Overlay
层叠视图。OverlayEntry
的可见性和层级关系。核心属性:
\\nfinal OverlayEntry _overlayEntry = OverlayEntry(builder: _buildModal); // 内容入口\\n
\\n实现原理:
\\ninstall
方法中将 _overlayEntry
加入 Overlay
。dispose
方法中移除 _overlayEntry
。应用场景:需要将内容渲染到全局层叠视图的路由(如弹窗
、全屏页面
)。
TransitionRoute
:抽象类核心职责:
\\nanimation
)和退出(secondaryAnimation
)的动画过程。buildTransitions
方法定义动画逻辑。核心属性与方法:
\\nAnimationController get animation => _animation; // 进入动画控制器\\nAnimationController get secondaryAnimation => _secondaryAnimation; // 退出动画控制器\\n\\n@override\\nWidget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {\\n return FadeTransition(opacity: animation, child: child); // 默认淡入淡出\\n}\\n
\\n应用场景:需要动画过渡效果的路由(如页面切换
、弹窗出现
)。
ModalRoute
:抽象类核心职责:
\\nBarrier
),阻止下层路由的交互。RouteSettings
管理路由名称和参数。核心属性:
\\nColor barrierColor = Colors.black54; // 遮罩颜色\\nbool barrierDismissible = true; // 点击遮罩是否可关闭路由\\nRouteSettings? settings; // 路由配置(名称、参数)\\n
\\n生命周期扩展:
\\n@override\\nvoid didChangeNext(Route? nextRoute) { \\n // 当下一个路由变化时触发(如新路由压栈)\\n}\\n
\\n应用场景:需要模态交互的组件(如对话框
、底部弹窗
)。
PageRoute
:抽象类核心职责:
\\n全屏页面的标准行为
,适配不同平台风格。fullscreenDialog
标记为全屏对话框(如 iOS
风格)。核心属性:
\\nbool fullscreenDialog = false; // 是否为全屏对话框\\n@override\\nDuration get transitionDuration => const Duration(milliseconds: 300); // 动画时长\\n
\\n实现方法:
\\n@override\\nWidget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {\\n return const MyPage(); // 返回页面内容\\n}\\n
\\n应用场景:全屏页面导航
(如主页跳转到详情页
)。
MaterialPageRoute
与 CupertinoPageRoute
:实现类核心职责:
\\nMaterial Design
和 iOS
风格的页面切换动画。差异对比:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n类名 | 过渡动画风格 | 应用场景 |
---|---|---|
MaterialPageRoute | 水平滑动(右进左出) | Android 应用、Material 风格 |
CupertinoPageRoute | 水平滑动(右进左出) + 缩放效果 | iOS 应用、Cupertino 风格 |
实现代码:
\\n// MaterialPageRoute 的过渡动画\\n@override\\nWidget buildTransitions(...) {\\n if (fullscreenDialog) {\\n return FadeTransition(opacity: animation, child: child); // 全屏对话框使用淡入\\n }\\nreturn SlideTransition(position: Tween<Offset>(begin: Offset(1.0, 0.0), end: Offset.zero).animate(animation),child: child,);\\n}\\n
\\nPageRouteBuilder
:工具类非继承链成员:通过组合方式
快速创建自定义路由。
\\n核心作用:允许开发者直接定义 pageBuilder
和 transitionsBuilder
。
使用示例:
\\nNavigator.push(\\n context,\\n PageRouteBuilder(\\n pageBuilder: (context, animation, secondaryAnimation) => const DetailPage(),\\n transitionsBuilder: (context, animation, secondaryAnimation, child) {\\n return RotationTransition(\\n turns: animation,\\n child: child,\\n );\\n },\\n ),\\n);\\n
\\nRoute
到 MaterialPageRoute
,每一层通过继承逐步添加特性(如渲染
、动画
、模态行为
)。Route
管理生命周期,OverlayRoute
处理渲染,TransitionRoute
实现动画。ModalRoute
和 PageRoute
分别扩展模态和全屏逻辑。MaterialPageRoute
)或组合(如 PageRouteBuilder
)支持扩展,而非修改基类。Flutter
内置多种 Route
类型,满足不同场景需求:
类型 | 特点与适用场景 |
---|---|
PageRoute | 全屏路由基类,支持自定义进入/退出动画(如 MaterialPageRoute )。 |
DialogRoute | 模态对话框,自带半透明遮罩层,阻止下层交互(如 showDialog 使用的路由)。 |
PopupRoute | 弹出式组件(如菜单 、Snackbar ),通常覆盖在页面局部区域。 |
RawDialogRoute | 无默认样式的对话框,需完全自定义内容。 |
TransparentRoute | 透明背景路由,用于叠加在现有页面上(如引导层)。 |
设计模式:
\\nRoute
通过组合 OverlayEntry
实现内容渲染。TransitionBuilder
动态注入(如 PageRouteBuilder
)。Route
↔ Navigator
Navigator
是 Route
的容器,维护一个 List<Route>
堆栈。
关键操作:
\\npush
:压入新 Route
,触发 didPush
。pop
:弹出当前 Route
,触发 didPop
。replace
:替换当前 Route
,触发 didReplace
。状态同步:Navigator
通过 Overlay
更新界面,确保路由堆栈变化反映到 UI
。
Route
↔ Overlay
Overlay
是一个全局的层叠视图,所有 Route
的内容通过 OverlayEntry
插入其中。Route
创建 OverlayEntry
,将其添加到 Overlay
。Overlay
根据 Entry
的 opaque
属性决定是否遮挡下层内容。Route
被销毁时,移除对应的 OverlayEntry
。Route
↔ Widget
树Route
的内容(Widget
)独立于父 Widget
树,通过 Overlay
渲染。Route
的 BuildContext
来自 Navigator
,无法直接访问父页面的上下文。RouteSettings
:参数配置用于存储路由的元数据:
\\nRouteSettings(\\n name: \'/detail\', // 路由名称(用于命名路由)\\n arguments: {\'id\': 123}, // 传递的参数\\n)\\n
\\n在路由内通过 ModalRoute.of(context).settings
获取配置。
方式 | 特点 |
---|---|
构造函数传参 | 直接向目标页面 Widget 传递数据,类型安全但需手动处理。 |
RouteSettings | 通过 settings.arguments 传递动态数据(需类型转换 )。 |
InheritedWidget | 跨路由共享数据,但需处理依赖关系。 |
状态管理 | 使用 Provider 、Riverpod 等库实现全局状态共享。 |
每个 TransitionRoute
内置两个 AnimationController
:
animation
:控制当前路由的进入动画。secondaryAnimation
:控制上一个路由的退出动画。通过 PageRouteBuilder
快速实现:
Navigator.push(\\n context,\\n PageRouteBuilder(\\n pageBuilder: (context, animation, secondaryAnimation) => const DetailPage(),\\n transitionsBuilder: (context, animation, secondaryAnimation, child) {\\n return RotationTransition(\\n turns: animation,\\n child: child,\\n );\\n },\\n ),\\n);\\n
\\nHero
动画基于 Route
的共享元素过渡:
// 页面 A\\nHero(tag: \'image\', child: Image.network(url));\\n\\n// 页面 B\\nHero(tag: \'image\', child: Image.network(url));\\n
\\n原理:Hero
组件在路由切换时,通过 Overlay
实现跨页面动画。
Route
生命周期状态转换图:
stateDiagram-v2\\n [*] --\x3e creation\\n creation --\x3e staging : \\"创建路由对象\\\\n(_RouteEntry实例化)\\"\\n \\n staging --\x3e pushing : \\"pushReplace/<font color=#ff0000>push*</font>\\\\n(压栈操作触发)\\"\\n staging --\x3e adding : \\"<font color=#ff0000>add*</font>\\\\n(动态添加路由)\\"\\n staging --\x3e idle : \\"<font color=#ff0000>replace*</font>\\\\n(直接替换当前路由)\\"\\n \\n pushing --\x3e idle : \\"动画完成/\\\\n路由可见\\"\\n adding --\x3e idle : \\"路由插入完成\\"\\n \\n idle --\x3e popping : \\"<font color=#ff0000>pop*</font>\\\\n(用户返回/出栈)\\"\\n idle --\x3e removing : \\"<font color=#ff0000>remove*</font>\\\\n(主动移除路由)\\"\\n idle --\x3e disposed : \\"<font color=#ff0000>complete*</font>\\\\n(标记完成状态)\\"\\n \\n popping --\x3e finalizeRoute : \\"执行路由清理\\\\n(释放动画资源)\\"\\n removing --\x3e finalizeRoute\\n finalizeRoute --\x3e disposing : \\"准备释放对象\\\\n(Dispose阶段)\\"\\n \\n disposing --\x3e disposed : \\"<font color=#ff0000>dispose*</font>\\\\n(对象资源回收)\\"\\n \\n disposed --\x3e [*]\\n \\n note left of creation\\n **Creation 阶段**:\\n - 初始化路由参数\\n - 创建Route对象\\n - 绑定页面Widget\\n end note\\n \\n note right of idle\\n **Idle 状态**:\\n - 路由可见且活跃\\n - 可响应用户输入\\n - 等待下一步操作\\n end note\\n
\\nRoute
的生命周期方法由 Navigator
驱动,每个阶段对应不同的状态和行为:
生命周期方法/事件 | 触发时机 | 用途/典型场景 |
---|---|---|
createState() | 当创建 PageRoute (如 MaterialPageRoute )时 | 生成与路由关联的 RouteState 对象(仅用于 PageRoute 子类) |
install() | 路由被插入到导航器(Navigator )时 | 初始化路由的依赖关系(如添加 Overlay Entry ) |
didPush() | 路由被推入导航器栈(Navigator.push )并完成动画后 | 处理路由完全可见后的逻辑(如数据加载) |
didAdd() | 路由被直接添加到导航器栈(如初始路由)时 | 处理无动画场景的路由初始化 |
didPop() | 路由被弹出导航器栈(Navigator.pop )并完成动画前 | 处理路由弹出前的逻辑(返回 false 可阻止弹出) |
didComplete() | 路由被完全弹出导航器栈后(动画完成时) | 清理路由的残留资源 |
didReplace() | 当前路由被另一个路由替换时(如 Navigator.replace ) | 处理路由替换时的状态转移 |
didPopNext() | 当上层路由被弹出,当前路由重新变为活动状态时 | 返回当前路由时的数据刷新(如从子页面返回) |
didPushNext() | 当新路由被推入到当前路由上方时 | 当前路由被覆盖时的暂停逻辑(如暂停视频播放) |
deactivate() | 路由从导航器栈中移除时(可能在 dispose 前多次调用) | 标记路由为未激活状态 |
dispose() | 路由被永久销毁时 | 释放所有资源(如关闭流、移除 Overlay Entry ) |
监听Route
的生命周期:
import \'package:flutter/material.dart\';\\n\\n/// 全局路由观察者\\nfinal RouteObserver<ModalRoute> routeObserver = RouteObserver<ModalRoute>();\\n\\nclass RouteDemo extends StatelessWidget {\\n const RouteDemo({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n // 注册路由观察者\\n navigatorObservers: [routeObserver],\\n home: HomePage(),\\n );\\n }\\n}\\n\\n/// 主页\\nclass HomePage extends StatefulWidget {\\n @override\\n _HomePageState createState() => _HomePageState();\\n}\\n\\nclass _HomePageState extends State<HomePage> with RouteAware {\\n @override\\n void didChangeDependencies() {\\n super.didChangeDependencies();\\n // 订阅路由观察\\n routeObserver.subscribe(this, ModalRoute.of(context)!);\\n }\\n\\n @override\\n void dispose() {\\n routeObserver.unsubscribe(this);\\n super.dispose();\\n }\\n\\n // 以下为路由生命周期方法\\n @override\\n void didPush() {\\n print(\'HomePage - didPush\');\\n }\\n\\n @override\\n void didPopNext() {\\n print(\'HomePage - didPopNext (从子页面返回)\');\\n }\\n\\n @override\\n void didPushNext() {\\n print(\'HomePage - didPushNext (跳转到子页面)\');\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\'主页\')),\\n body: Center(\\n child: ElevatedButton(\\n child: Text(\'打开子页面\'),\\n onPressed: () => Navigator.push(\\n context,\\n MaterialPageRoute(builder: (context) => ChildPage()),\\n ),\\n ),\\n ),\\n );\\n }\\n}\\n\\n/// 子页面\\nclass ChildPage extends StatefulWidget {\\n @override\\n _ChildPageState createState() => _ChildPageState();\\n}\\n\\nclass _ChildPageState extends State<ChildPage> with RouteAware {\\n @override\\n void didChangeDependencies() {\\n super.didChangeDependencies();\\n routeObserver.subscribe(this, ModalRoute.of(context)!);\\n }\\n\\n @override\\n void dispose() {\\n routeObserver.unsubscribe(this);\\n super.dispose();\\n }\\n\\n @override\\n void didPush() {\\n print(\'ChildPage - didPush\');\\n }\\n\\n @override\\n void didPop() {\\n print(\'ChildPage - didPop (即将被关闭)\');\\n super.didPop();\\n }\\n\\n @override\\n void didPopNext() {\\n print(\'ChildPage - didPopNext (不会触发,因为子页面在栈顶)\');\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\'子页面\')),\\n body: Center(\\n child: ElevatedButton(\\n child: Text(\'返回主页\'),\\n onPressed: () => Navigator.pop(context),\\n ),\\n ),\\n );\\n }\\n}\\n
\\n关键生命周期方法说明:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n方法 | 触发场景 | 典型用途 |
---|---|---|
didPush | 路由被推入导航栈后 | 初始化页面数据 |
didPop | 路由即将被弹出时 | 保存表单数据/释放临时资源 |
didPopNext | 当上层路由被弹出,本路由重新激活时 | 刷新页面数据(如从详情页返回列表页) |
didPushNext | 当新路由被推入到本路由上方时 | 暂停动画/视频播放 |
注意事项:
\\ndidChangeDependencies
中订阅,在 dispose
中取消订阅,避免内存泄漏。ModalRoute
:MaterialPageRoute
/CupertinoPageRoute
,自定义路由需继承 ModalRoute
。Widget
生命周期分离:Widget
的 initState
/dispose
,专注于导航栈的变化。如果需要进一步自定义路由行为(如拦截返回键
),可以通过 WillPopScope
或重写 Route
的 willPop
方法实现。
路由系统通过Route
抽象层与Navigator
容器的协同,实现了页面堆栈的精准管控。从OverlayEntry
的全局渲染机制到ModalRoute
的模态遮罩策略,从PageRouteBuilder
的动画自由度到RouteObserver
的全生命周期监听,每一处设计都彰显着框架\\"组合优于继承\\"
的理念。
开发者通过掌握TransitionRoute
的动画协调原理、理解RouteSettings
的参数传递本质,不仅能轻松实现Material
与Cupertino
风格的无缝切换,更能基于WillPopScope
等扩展点打造深度定制的导航体验。路由系统作为连接业务模块的脉络,其设计优劣直接影响着应用的流畅度与可维护性,值得每一位Flutter
开发者投入精力深挖。
\\n","description":"前言 在移动应用开发中,路由系统如同应用的导航中枢,决定着用户在不同界面间的流转体验。Flutter通过精巧的类层次设计和分层抽象机制,构建了一套灵活高效的路由管理体系。\\n\\n本文从路由(Route)与导航器(Navigator)的基础概念切入,深入剖析MaterialPageRoute、CupertinoPageRoute等核心类的继承关系与协作原理,解读路由堆栈模型的生命周期控制策略,并通过Hero动画、全局路由监听等进阶案例,揭示如何利用TransitionBuilder实现跨平台风格切换。\\n\\n无论你是刚接触Flutter的新手…","guid":"https://juejin.cn/post/7481858719830097971","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-16T11:29:47.636Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/66b7e550ec8d4eee9ff877aa00724c00~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1742729386&x-signature=EkKVAfVDOtH%2BTZb2zFOrOg1jlUM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/91356f2781f74948b6290e9d01a9f9c6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1742729386&x-signature=oLosLZH9ydxYjz%2F0r8IHinsNfTs%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"从0到1掌握Flutter(三)Dart语法","url":"https://juejin.cn/post/7481861001190473768","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
本文接上篇:从0到1掌握Flutter(二)环境搭建与认识工程
\\nDart 语言基础是 Flutter 开发必须掌握的核心知识。本文将讲解变量与常量的声明、Dart 内置类型体系及其用法、运算符的应用场景三大模块。
\\n对于具备 Java/Kotlin 背景的学习者,可以通过对比学习法快速定位知识缺口,理解语法的共性。
\\n💡 万物皆对象
\\n在 Dart 的类型系统中,变量本质上是指对象的引用,这一设计符合面向对象语言的核心特性。
\\n无论是自定义类型还是基本数字类型,Dart 中一切均以对象形式存在。
\\n通过查看 int 类型的源码实现可以发现:int 本身是一个类,其实例自然也是对象:
\\n所以基于此设计,所有未显式初始化的变量默认值均为 null,这是 Dart 统一对象模型的直接体现。
\\nint? uninitializedInt;\\ndouble? uninitializedDouble;\\nString? uninitializedString;\\nbool? uninitializedBool;\\n\\nprint(uninitializedInt); // null\\nprint(uninitializedDouble); // null\\nprint(uninitializedString); // null\\nprint(uninitializedBool); // null\\n
\\n💡 类型声明
\\n在 Dart 中有三种变量的声明方式:Object、var 与 dynamic。与 Java 的强类型约束不同,Dart 的声明方式更灵活,平衡了类型安全与开发效率。
\\nObject obj = \\"hello\\";\\nobj = 50; // 合法但会丢失类型信息\\n
\\n延续了 Java 的基类传统,与 Java 一样 Object 是所有类的基类,Object 声明的变量可以是任意类型。当使用Object时,虽然支持任意赋值,但每次访问都需要显式类型转换——这种设计保留了 Java 的严谨性,却与 Dart 推崇的简洁理念存在冲突。
\\nvar a = 1;\\na = \\"a\\";//编译时报错\\n\\nvar b;\\nb = 1;\\nb = \\"a\\";//正确\\n
\\nDart 中的 var ,与 kotlin 中的 var 类似,用于声明变量并通过初始化值自动推断类型。二者存在一下差异:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n特性 | Dart 的 var | Kotlin 的 var |
---|---|---|
类型锁定 | 初始化后类型固定 | 声明时类型固定 |
默认值 | 未初始化时为 null | 必须显式初始化 |
动态类型支持 | 未初始化的 var 类似 dynamic | 需显式使用 Any 或可空类型 |
语法灵活性 | 允许延迟初始化并改变类型 | 必须立即初始化且类型不可变 |
Dart 中的 var 可以不初始化,若未初始化,变量类型默认为 dynamic,可以任意赋值,编译器不做类型检测(后续会提到)。初始化后类型锁定不可更改。简化了代码并保持编译时类型安全,适用于局部变量或类型明确的场景。
\\ndynamic dy = 10; // 初始化为整型\\ndy = \\"text\\"; // 合法操作,允许运行时改变类型\\ndy.nonExistFunc(); // 编译通过,但运行时会抛出 NoSuchMethodError\\n
\\ndynamic 类型声明是 Dart 中灵活处理类型变化的特殊机制,它的作用是在保留运行时类型检查的前提下关闭静态类型检查。该类型具有三个特征:
\\n与 Java 的 Object 类型对比时,主要差异体现在类型操作层面:Java 必须通过显式类型转换才能调用目标类型方法,而 dynamic 可以直接操作。但这种便利性会带来更高的风险,比如以下 Java 代码会在编译阶段就暴露错误:
\\nObject obj = 10;\\nobj = \\"text\\";\\n // obj.nonExistFunc(); // Java 编译报错:Object 类没有该方法\\n // ((String)obj).nonExistFunc(); // 强制转换后仍会在编译期报错\\n
\\n虽然它提供了编码灵活性,但过度使用会破坏 Dart 的类型系统保护机制。建议仅在处理 JSON 数据解析或与 JavaScript 互操作等特定场景下使用,常规业务逻辑中应优先考虑类型安全方案。
\\n💡 final 与 const
\\n在 Dart 中,当我们确定某个变量不需要修改时,final 和 const 这两个关键字都能帮我们实现不可变性。不过它们的底层机制和应用场景有所不同,我们通过几个关键点来理解它们的差异。
\\n先看这段基础用法示例:
\\nfinal userName = \'name\'; // 运行时确定值\\nconst maxCount = 100; // 编译时确定值\\n
\\n虽然表面上看两者用法相似,但它们的本质区别在于确定值的时机,这种差异直接影响了它们的使用场景。比如下面这个例子中:
\\n // 合法:运行时获取当前时间\\nfinal currentTime = DateTime.now().second;\\n // 非法:编译时无法确定具体值\\n // const fixedTime = DateTime.now().second; \\n
\\n在类成员变量使用时,要注意它们的声明方式:
\\nclass Config {\\n final int id; // 正确:通过构造函数初始化\\n static const version = 1.0; // 必须声明为静态\\n Config(this.id);\\n}\\n
\\n由于类的实例化是运行时行为,而 const 需要编译时确定值,因此类中的 const 常量必须通过 static 声明为类级别常量。而 final 变量则可以通过构造函数灵活初始化,每个实例可以拥有不同的 final 值。
\\n另外,final 是运行时常量,而 const 是编译器常量,它的值在编译期就可以确定,能够让代码运行更高效。
\\n下面来看看 Dart 语言中的基础类型设计,和 Java 直接内置 8 种基本数据类型不同,Dart 内置支持下面这些类型:
\\n💡 Numbers
\\n在数字类型方面,Dart 使用了一个层次分明的结构。最顶层的 num 类型作为数字类型的抽象父类,它有两个具体的子类实现:int 表示整型数值,double 则专门处理浮点数。这种继承关系意味着当我们在代码中声明一个 num 类型的变量时,实际上可以接收整数或小数两种形式的赋值。
\\n// 可以接收整型\\nnum age = 25;\\n// 也可以接收浮点型\\nnum price = 9.99;\\n
\\n这种设计带来的优势是:开发者既可以使用具体类型来确保数值精度,也可以通过父类 num 来编写更通用的数值处理方法。
\\n💡 String
\\n在 Dart 中处理字符串,有几个实用特性。创建字符串时,单引号和双引号是等效的,我们可以根据场景灵活选择,并且单引号和双引号互相嵌套可以不使用转义符号``:
\\n// 单双引号嵌套可避免转义符号的使用\\nvar quote = \'He said \\"Hello!\\" without hesitation\'; \\nvar nested = \\"It\'s a beautiful day\\";\\n
\\n当需要将变量值嵌入字符串时,Dart 提供了便捷的插值语法。注意当引用简单变量时可以直接使用$
符号,而执行表达式时则需要用花括号包裹,与 kotlin 类似:
var score = 90;\\nvar report = \\"Final score: $score\\"; // 简单变量引用\\nvar detail = \\"Score status: ${score >= 60 ? \'Pass\' : \'Fail\'}\\"; // 表达式计算\\n
\\n字符串拼接有两种常用方式。除了传统的+
操作符,将相邻字符串写在一起也能实现自动拼接,这在格式化长字符串时特别有用:
var message1 = \'Hello \' + name + \'!\'; // 使用加号拼接\\nvar message2 = \'Welcome to \' \'Dart string \' \'tutorial\'; // 自动拼接\\n
\\n处理多行文本时,三引号语法可以保留原始换行和缩进格式。这在创建格式化的长文本(如 SQL 语句或文档模板)时非常实用,与 kotlin 类似:
\\nvar sql = \'\'\'\\nSELECT name, email \\nFROM users\\nWHERE status = \'active\'\\n\'\'\';\\n
\\n当需要处理原始字符串(不解析转义字符)时,只需在引号前添加r
前缀。这在处理正则表达式或文件路径时尤为重要:
print(r\\"换行符保留:\\\\n\\"); // 输出:换行符保留:\\\\n\\nprint(\\"正常转义:\\\\n\\"); // 输出:正常转义:\\\\n\\n
\\n💡 Booleans
\\nDart 的布尔值与 Java 等静态类型语言相似。条件表达式必须严格返回布尔值:
\\n// 基础布尔变量声明\\nvar isLogin = true;\\nbool hasPermission = false;\\n\\nif (isLogin) {\\n print(\'显示用户仪表盘\');\\n} else {\\n print(\'跳转到登录页\');\\n}\\n\\n// 以下写法会导致编译错误(非布尔值不能用于条件判断)\\nvar count = 0;\\n// if (count) { ... } // 错误:int 不能隐式转换为 bool\\n
\\n💡 List
\\n在 Dart 中,List 是最基础且使用频率最高的数据结构之一。我们可以将其理解为其他语言中的数组,但具备更灵活的特性:
\\nDart 提供了直观的字面量声明方式,用方括号包裹元素并用逗号分隔。元素的索引体系从 0 开始,与大多数编程语言一致:
\\n//使用new(new可以省去) \\n var list = new List.filled(1, 0);\\n list[0] = 2;\\n\\n// 基础列表声明\\nvar numbers = [10, 20, 30]; \\n\\n// 访问最后一个元素的两种方式\\nprint(numbers[numbers.length - 1]); // 方式1:通过长度计算\\nprint(numbers.last); // 方式2:使用内置属性(更推荐)\\n
\\n通过 const
关键字可以创建编译时常量列表,这种列表在内存中只会存在一份实例,且完全不可修改:
// 创建不可变列表\\nconst immutableList = [\'A\', \'B\', \'C\'];\\n\\n// 以下操作均会触发运行时错误\\nimmutableList[0] = \'X\'; // 错误:禁止修改元素\\nimmutableList.add(\'D\'); // 错误:禁止改变长度\\n
\\n\\n// List 遍历\\nvar colors = [\'Red\', \'Green\', \'Blue\'];\\n//增强型 for-in\\nfor (var color in colors) {\\n print(\'颜色值: $color\');\\n}\\n\\n// 函数式 forEach\\ncolors.asMap().forEach((index, color) {\\n print(\'索引$index -> $color\');\\n});\\n
\\n💡 Map
\\nMap 和其他语言中的 Map 类似
\\n我们可以用花括号 {}
直接创建 Map,键值对之间用逗号分隔:
// 创建包含国家代码的 Map\\nvar countryCodes = {\\n \'China\': 86,\\n \'USA\': 1,\\n \'Japan\': 81\\n};\\n
\\n这种写法非常直观,就像列清单一样把键值对依次排列出来。
\\n当我们需要创建编译时就确定的常量 Map 时,可以像这样在花括号前添加 const 关键字:
\\n// 定义不可修改的常量 Map\\nconst constantMap = {\\n 2: \'helium\',\\n 10: \'neon\',\\n 18: \'argon\',\\n};\\n
\\n这个 Map 在程序运行期间将始终保持不变,任何修改它的尝试都会导致运行时错误。这种特性对于需要保证数据不被意外修改的场景非常有用。
\\n我们可以用两种方式查看每个键值对:
\\n// 使用 forEach 遍历\\nconstantMap.forEach((country, code) {\\n print(\'$country 的国际区号是 $code\');\\n});\\n\\n// 或者用 for-in 遍历\\nfor (var entry in constantMap.entries) {\\n print(\'${entry.key} -> ${entry.value}\');\\n}\\n
\\n💡 Runes
\\n在 Dart 中处理 Unicode 字符时,Runes 类是一个重要工具。当我们需要处理特殊字符或 32 位的 Unicode 编码时,传统的字符串表示方法可能不够用,这时就要用到 Runes 了。举个例子:
\\n假设我们要处理一个「💡」表情符号,它使用 16 进制表示为\\"U+1F4A1\\",转为二进制是 111110010100001,这是一个 21 位的二进制数。
\\n而 String 实际上是使用的是 UTF-16 编码,它的最小码元是 16 位,所以「💡」就需要两个码元来表示,这个时候获取字符串\'💡\'的长度,就会的到 2:
\\nvar emoji = \'💡\';\\nprint(emoji.length); // 输出 2(占用2个UTF-16代码单元)\\nprint(emoji.codeUnits); // 输出 [55357, 56399]\\n
\\n使用 Runes 则不会出现这个问题,Runes 使用的是 UTF-32 编码,它的最小码元是 32 位,足以表示「💡」。
\\nRunes runes = Runes(\'💡\');\\nprint(runes.length);// 输出 1(占用1个UTF-32代码单元)\\nprint(runes.toList()); // 输出 [128161] \\n
\\n最后我们看一个组合使用 Runes 的典型场景,处理包含多种 Unicode 符号的字符串:
\\nRunes input = Runes(\\n \'\\\\u2665 \\\\u{1f605} \\\\u{1f60e} \\\\u{1f47b} \\\\u{1f596} \\\\u{1f44d}\');\\nprint(String.fromCharCodes(input)); \\n// 输出:♥ 😅 😎 👻 🖖 👍\\n
\\nString 与 Runes 对比
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n特性 | 字符串(String) | Runes |
---|---|---|
存储方式 | UTF-16 代码单元序列 | UTF-32 代码单元序列 |
长度计算 | .length 返回代码单元数量 | .length 返回实际字符数量 |
适用场景 | 常规文本操作 | 需要精确处理 Unicode 字符的场景 |
特殊字符处理 | 可能拆分为代理对(如:\'👏\'.length=2) | 始终保留完整代码点(如:Runes(\'👏\').length=1) |
💡 Symbols
\\nSymbol 是一种特殊的标识符,来看一个具体的应用示例:
\\nvoid main() {\\n const targetSymbol = #A; // 声明编译时常量符号\\n print(\'当前符号值: $targetSymbol\');\\n switch(targetSymbol) {\\n case #A: // 符号匹配判断\\n print(\\"匹配到A符号\\");\\n break;\\n case #B:\\n print(\\"匹配到B符号\\");\\n break;\\n }\\n\\n final createdSymbol = Symbol(\'created\'); // 通过构造函数创建符号\\n print(#created == createdSymbol); // 输出 true,两种创建方式等价\\n}\\n
\\n💡 赋值操作符
\\n在 Dart 的赋值操作符中,除了 =(基础赋值)、+=(累加)、/=(除法赋值)、*=(乘法赋值)这些常规操作符之外,还有一个比较实用的空安全赋值操作符 ??=。
\\n它的作用可以用是,当且仅当变量值为 null 时执行赋值操作:
\\nint? score; // 声明一个可空的整型变量\\n\\n// 使用空安全赋值操作符的等效写法\\nscore ??= 60; // 此时 score 的值为 60\\n\\n// 传统写法需要显式判空\\nif (score == null) {\\n score = 60;\\n}\\n
\\n当第二次调用时:
\\nscore ??= 90; // 由于 score 已经是 60(非空),赋值操作不会执行\\nprint(score); // 输出仍然是 60\\n
\\n💡 判定操作符
\\nDart 提供了三个运算符确保类型安全:
\\n使用方法与 kotlin 类似:
\\nvoid process(dynamic data) {\\n // 先进行类型检查\\n if (data is String) {\\n print(\'字符串长度:${data.length}\'); // 这里会自动提升为 String 类型\\n }\\n \\n try {\\n final numData = data as num; // 强制类型转换\\n print(\'数值平方:${numData * numData}\');\\n } catch (e) {\\n print(\'类型转换失败:$e\');\\n }\\n}\\n
\\n在这个示例中,注意几个点需要注意:
\\n• 使用 is
进行类型检查后,Dart 会自动将变量提升为该类型,后续代码可直接使用该类型的属性和方法
• as
运算符需要谨慎使用,当转换不兼容的类型时会抛出 CastError
,因此建议先用 is
检查或配合 try-catch
💡 安全操作符
\\n在 Dart 语言中,可以使用安全操作符(?)处理空指针异常问题。
\\n同 Kotlin 的(?)操作符一样,例如当试图访问一个可能为空的字符串长度时,传统的做法需要写大量判空代码。使用使用安全操作符(?)可以更优雅的处理类似问题:
\\nString? sb; // 声明为可空字符串类型\\nprint(sb.length); // 触发空指针异常\\nprint(sb?.length); // 安全访问,输出 null\\n
\\n💡 件表达式
\\nDart 有条件表达式,与 Java 与 kotlin 的条件表达式很相似。
\\n// Dart示例\\nvar status = isPublic ? \'公开\' : \'私密\';\\n
\\n这个写法和 Java 的三目运算符完全一致。不过 Kotlin 需要用完整的 if 表达式来实现相同效果
\\n// 当userName为空时,使用\'匿名用户\'\\nvar name = userName ?? \'匿名用户\';\\n
\\n??
操作符相当于 Kotlin 的?:
操作符。
💡 级联操作符
\\n级联操作符(..)是一个让代码更优雅的语法糖。帮你保持对象操作连贯性的工具。
\\n当我们需要对同一个对象进行多次操作时,它可以避免反复书写对象变量名。比如在构建字符串时,传统写法需要不断重复变量名:
\\nfinal buffer = StringBuffer();\\nbuffer.write(\'Hello\');\\nbuffer.write(\' \');\\nbuffer.write(\'Dart!\');\\n
\\n而使用级联操作符后,代码会变得行云流水:
\\nfinal buffer = StringBuffer()\\n ..write(\'Hello\')\\n ..write(\' \')\\n ..write(\'Dart!\');\\n
\\n这种写法特别适合配置复杂对象的场景,比如构建 UI 组件时连续设置多个属性,或是处理集合元素时进行链式操作。它让代码呈现出清晰的步骤感,就像在讲述一个对象被创建的过程。
\\n本文系统梳理了 Flutter 开发中的基础语法,从变量声明到集合类型,从基础运算符到级联表达式。这些语法是构建应用的基石。掌握这些语法,后面我们继续学习 Dart 的方法、类。
","description":"引言 本文接上篇:从0到1掌握Flutter(二)环境搭建与认识工程\\n\\nDart 语言基础是 Flutter 开发必须掌握的核心知识。本文将讲解变量与常量的声明、Dart 内置类型体系及其用法、运算符的应用场景三大模块。\\n\\n对于具备 Java/Kotlin 背景的学习者,可以通过对比学习法快速定位知识缺口,理解语法的共性。\\n\\n一、变量与常量\\n1.1 变量\\n\\n💡 万物皆对象\\n\\n在 Dart 的类型系统中,变量本质上是指对象的引用,这一设计符合面向对象语言的核心特性。\\n\\n无论是自定义类型还是基本数字类型,Dart 中一切均以对象形式存在。\\n\\n通过查看 int…","guid":"https://juejin.cn/post/7481861001190473768","author":"A0微声z","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-16T10:00:13.968Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6d11542c67a243c184fd528905c34737~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQTDlvq7lo7B6:q75.awebp?rk3s=f64ab15b&x-expires=1742724014&x-signature=LDaeddiYrfjq7CQYHcZJVjtBpm8%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter Dart 面向对象编程全面解析","url":"https://juejin.cn/post/7481561911711154227","content":"在 Flutter 开发中,Dart 作为其编程语言,采用了面向对象的编程范式。面向对象编程(OOP)将数据和操作数据的方法封装在一起,形成对象,以提高代码的可维护性、可扩展性和可重用性。本文将详细介绍 Dart 面向对象编程的核心概念,包括类、对象、继承、多态、抽象类和接口等,并结合代码示例进行说明。
\\n类是对象的蓝图,它定义了对象的属性和方法。对象是类的实例,通过类可以创建多个不同的对象。
\\n// 定义一个 Person 类\\nclass Person {\\n // 定义属性\\n String name;\\n int age;\\n\\n // 定义构造函数\\n Person(this.name, this.age);\\n\\n // 定义方法\\n void introduce() {\\n print(\'我叫 $name,今年 $age 岁。\');\\n }\\n}\\n\\nvoid main() {\\n // 创建 Person 类的对象\\n Person person1 = Person(\'张三\', 20);\\n Person person2 = Person(\'李四\', 25);\\n\\n // 调用对象的方法\\n person1.introduce();\\n person2.introduce();\\n}\\n
\\nclass Person
定义了一个名为 Person
的类。String name
和 int age
是 Person
类的属性,用于存储对象的状态。Person(this.name, this.age)
是构造函数,用于初始化对象的属性。void introduce()
是 Person
类的方法,用于打印对象的信息。main
函数中,使用 Person(\'张三\', 20)
和 Person(\'李四\', 25)
创建了两个 Person
类的对象 person1
和 person2
,并调用它们的 introduce
方法。继承是面向对象编程的重要特性之一,它允许一个类(子类)继承另一个类(父类)的属性和方法。子类可以扩展父类的功能,也可以重写父类的方法。
\\n// 定义父类\\nclass Animal {\\n String name;\\n\\n Animal(this.name);\\n\\n void makeSound() {\\n print(\'动物发出声音\');\\n }\\n}\\n\\n// 定义子类\\nclass Dog extends Animal {\\n Dog(String name) : super(name);\\n\\n @override\\n void makeSound() {\\n print(\'汪汪汪\');\\n }\\n}\\n\\nvoid main() {\\n Dog dog = Dog(\'旺财\');\\n dog.makeSound();\\n}\\n
\\nclass Animal
是父类,包含一个属性 name
和一个方法 makeSound
。class Dog extends Animal
表示 Dog
类继承自 Animal
类。Dog(String name) : super(name)
是 Dog
类的构造函数,使用 super(name)
调用父类的构造函数。@override
注解表示重写父类的方法,Dog
类重写了 makeSound
方法,输出 \'汪汪汪\'
。main
函数中,创建了一个 Dog
类的对象 dog
,并调用其 makeSound
方法。多态是指同一个方法调用可以根据对象的不同类型表现出不同的行为。在 Dart 中,多态主要通过继承和方法重写来实现。
\\n// 定义父类\\nclass Shape {\\n void draw() {\\n print(\'绘制图形\');\\n }\\n}\\n\\n// 定义子类\\nclass Circle extends Shape {\\n @override\\n void draw() {\\n print(\'绘制圆形\');\\n }\\n}\\n\\nclass Square extends Shape {\\n @override\\n void draw() {\\n print(\'绘制正方形\');\\n }\\n}\\n\\nvoid main() {\\n Shape shape1 = Circle();\\n Shape shape2 = Square();\\n\\n shape1.draw();\\n shape2.draw();\\n}\\n
\\nclass Shape
是父类,定义了一个 draw
方法。class Circle extends Shape
和 class Square extends Shape
分别定义了两个子类,并重写了 draw
方法。main
函数中,将 Circle
和 Square
类的对象赋值给 Shape
类型的变量 shape1
和 shape2
。shape1.draw()
和 shape2.draw()
时,会根据对象的实际类型调用相应的 draw
方法,体现了多态性。抽象类是一种不能被实例化的类,它主要用于定义一些通用的属性和方法,供子类继承和实现。抽象方法是在抽象类中声明但没有实现的方法,子类必须实现这些抽象方法。
\\n// 定义抽象类\\nabstract class Vehicle {\\n // 抽象方法\\n void start();\\n\\n // 普通方法\\n void stop() {\\n print(\'车辆停止\');\\n }\\n}\\n\\n// 定义子类\\nclass Car extends Vehicle {\\n @override\\n void start() {\\n print(\'汽车启动\');\\n }\\n}\\n\\nvoid main() {\\n Car car = Car();\\n car.start();\\n car.stop();\\n}\\n
\\nabstract class Vehicle
定义了一个抽象类 Vehicle
。void start()
是抽象方法,没有具体的实现,子类必须实现该方法。void stop()
是普通方法,有具体的实现。class Car extends Vehicle
表示 Car
类继承自 Vehicle
类,并实现了 start
方法。main
函数中,创建了一个 Car
类的对象 car
,并调用其 start
和 stop
方法。在 Dart 中,接口的概念与抽象类类似,但接口只包含抽象方法,不包含属性和具体实现的方法。类可以实现一个或多个接口。
\\n// 定义接口\\nabstract class Flyable {\\n void fly();\\n}\\n\\nabstract class Swimmable {\\n void swim();\\n}\\n\\n// 定义实现类\\nclass Duck implements Flyable, Swimmable {\\n @override\\n void fly() {\\n print(\'鸭子飞起来了\');\\n }\\n\\n @override\\n void swim() {\\n print(\'鸭子在游泳\');\\n }\\n}\\n\\nvoid main() {\\n Duck duck = Duck();\\n duck.fly();\\n duck.swim();\\n}\\n
\\nabstract class Flyable
和 abstract class Swimmable
定义了两个接口,分别包含一个抽象方法 fly
和 swim
。class Duck implements Flyable, Swimmable
表示 Duck
类实现了 Flyable
和 Swimmable
两个接口,并实现了接口中的抽象方法。main
函数中,创建了一个 Duck
类的对象 duck
,并调用其 fly
和 swim
方法。封装是将数据和操作数据的方法捆绑在一起,并隐藏对象的内部实现细节,只提供公共的访问接口。在 Dart 中,可以使用访问修饰符来实现封装。
\\nclass BankAccount {\\n // 私有属性\\n double _balance = 0;\\n\\n // 公共方法,用于存款\\n void deposit(double amount) {\\n if (amount > 0) {\\n _balance += amount;\\n print(\'存款 $amount 元,当前余额: $_balance 元\');\\n } else {\\n print(\'存款金额必须大于 0\');\\n }\\n }\\n\\n // 公共方法,用于取款\\n void withdraw(double amount) {\\n if (amount > 0 && amount <= _balance) {\\n _balance -= amount;\\n print(\'取款 $amount 元,当前余额: $_balance 元\');\\n } else {\\n print(\'取款失败,余额不足或取款金额无效\');\\n }\\n }\\n\\n // 公共方法,用于查询余额\\n double getBalance() {\\n return _balance;\\n }\\n}\\n\\nvoid main() {\\n BankAccount account = BankAccount();\\n account.deposit(1000);\\n account.withdraw(500);\\n print(\'当前余额: ${account.getBalance()} 元\');\\n}\\n
\\ndouble _balance
是私有属性,使用下划线 _
开头表示该属性只能在类的内部访问。deposit
、withdraw
和 getBalance
是公共方法,用于对私有属性 _balance
进行操作和访问。main
函数中,创建了一个 BankAccount
类的对象 account
,并调用其公共方法进行存款、取款和查询余额操作。Dart 的面向对象编程提供了丰富的特性,包括类和对象、继承、多态、抽象类和接口、封装等。这些特性可以帮助开发者编写更加模块化、可维护和可扩展的代码。在 Flutter 开发中,合理运用面向对象编程的思想,可以提高开发效率和代码质量。
","description":"引言 在 Flutter 开发中,Dart 作为其编程语言,采用了面向对象的编程范式。面向对象编程(OOP)将数据和操作数据的方法封装在一起,形成对象,以提高代码的可维护性、可扩展性和可重用性。本文将详细介绍 Dart 面向对象编程的核心概念,包括类、对象、继承、多态、抽象类和接口等,并结合代码示例进行说明。\\n\\n1. 类和对象\\n\\n类是对象的蓝图,它定义了对象的属性和方法。对象是类的实例,通过类可以创建多个不同的对象。\\n\\n代码示例\\n// 定义一个 Person 类\\nclass Person {\\n // 定义属性\\n String name;\\n int age…","guid":"https://juejin.cn/post/7481561911711154227","author":"顾林海","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-15T05:33:55.551Z","media":null,"categories":["Android","前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"从gitee上的鸿蒙开源Flutter停止更新说起","url":"https://juejin.cn/post/7481467343572205595","content":"哈喽,我是老刘
\\n2月底的时候,gitee 上的开源鸿蒙下的Flutter相关项目全部停止更新,仓库显示已关闭。
\\n
\\n后来发现不是项目停了,只是从gitee切换到GitCode。
\\n原因大概率是因为GitCode是基于华为云的。
\\n
\\n但是今天老刘不是想说切换托管平台的事,而是想在这里说说Flutter之于鸿蒙。
\\n\\n“主要功能已对齐,但部分附加特性或需等待后续更新。”
\\n
结论:现阶段的纯血鸿蒙手机仍然无法成为满足大多数人主力手机端需求。
\\n站在厂商的角度,肯定希望所有开发者都来开发鸿蒙原生应用。甚至在纯血鸿蒙刚推出时不惜动用市场之外的力量。
\\n站在开发者的角度,多一个平台就多了一份开发成本,当前的经济形势让开发团队支付这个成本实在太重了。
\\n站在用户的角度,同一个APP在不同设备、系统上的体验最好一致,否则使用起来就很难受。比如我在手机上用微信读书看书,回家后再ipad上继续看,肯定希望操作、体验都是一样的。
\\n这就形成了一个不可能三角。
\\n如何在这里面找到一个最优解呢?
\\n老刘觉得Flutter这样的跨平台开发框架就是破局的关键。
首先来看Flutter的特点:
\\n站在开发者的角度
\\n以中型应用为例,传统双端(iOS+Android)开发需12人月,Flutter可压缩至8人月。
\\n若新增鸿蒙适配,原生开发需额外增加4人月。
\\n而基于Flutter仅需1人月处理平台特定API(例如系统原生的各种功能调用)。
\\n这种边际成本递减效应显著提升开发者支持鸿蒙的动力。
站在用户的角度
\\nFlutter在渲染层面是独立于平台的,因此可以认为除非开发者特意针对不同平台设计不同的交互。
\\n否则在不同平台上,比如Android、iOS和鸿蒙上,用户体验基本是一致的。
站在鸿蒙的角度
\\n本身鸿蒙的设计就借鉴了Flutter的很多东西。
\\n同时基于Flutter的分层设计理念,兼容新的系统需要的工作是比较清晰且独立的。
\\n因此在鸿蒙上支持Flutter相对来说的成本并不高。
\\n而且从开发者留存的角度看,能最大程度的让现有的客户端开发者去兼容鸿蒙。
所以老刘认为以平台的角色去支持Flutter等跨平台框架是鸿蒙系统最优选择。
\\n事实上鸿蒙方面也是这样做的,所以才会有文章开头看到的那些开源项目。
总的来说,鸿蒙对Flutter的支持已进入可用但需优化阶段:基础功能适配成熟,但生态完整性和性能调优仍需时间。
\\n具体来说Flutter支持鸿蒙需要以下几个几个工作:
\\n1、Flutter框架在鸿蒙系统的原生app外壳中能运行起来
\\n2、Flutter应用能打包成鸿蒙原生应用
\\n3、三方库对鸿蒙系统的支持
其中 1 和 2 因为前面说的原因,已经可以很好的运行起来了。(这是实现起来最简单的部分)
\\n但是有一点需要注意,Flutter没发布一个新的版本,就需要鸿蒙方面去适配和跟进。
\\n这方面目前看和Flutter的官方版本有一定的进度落后。
第3点需要分两种情况来看:
\\n对于纯Dart库
\\n理论上不需要开发者做任何操作,应该能在鸿蒙版Flutter上正常运行。
\\n这部分排除底层实现的bug和性能方面的问题,应该是问题不大的。
对于Flutter + 原生混合开发的插件
\\n这种混合开发的插件目前是最麻烦的地方。
\\n因为需要这些库的开发者去主动适配鸿蒙系统,否则无法正常运行。
\\n但是对这些广大的中小开发者来说,目前看没有特别的动力去推进开发适配工作。
\\n假设我是一个开源的Flutter三方插件的开发者,如果要去适配鸿蒙,我需要先学习鸿蒙原生开发。
\\n然后还需要拉一个单独的分支专门针对鸿蒙版Flutter进行适配。
\\n最关键的是一目前鸿蒙的开发者体量,似乎对我的开源项目没有什么帮助。
所以老刘觉得如果想要开发者有动力去进行这方面的适配工作,可能钞能力是要用上的。
\\n能看到鸿蒙方面也在积极的推进这方面的工作。
\\n比如老刘最近就收到过几次写鸿蒙开发文章的邀请,文章数据好会有一些奖励。
简单总结一下。
\\n纯血鸿蒙本身目前无法平替Android或者iOS手机。应用生态的缺口仍然比较大。
\\n开发原生鸿蒙应用对开发团队来说成本很高。
\\n跨平台框架是鸿蒙应用生态快速发展的破局之道。
\\n目前鸿蒙已经支持Flutter框架。
\\n但是对于Flutter + 原生模式的三方插件需要开发者主动适配鸿蒙系统,目前数量较少。
\\n最后作为一个开发者,希望写一套代码可以运行在所有平台上,不用熬夜加班去适配。
如果看到这里的同学对客户端开发或者Flutter开发感兴趣,欢迎联系老刘,我们互相学习。
\\n点击免费领老刘整理的《Flutter开发手册》,覆盖90%应用开发场景。
\\n可以作为Flutter学习的知识地图。
\\n覆盖90%开发场景的《Flutter开发手册》
在 Flutter 开发中,使用 Dart 语言进行编程时,异常处理是至关重要的一环。异常是程序在运行过程中出现的错误或意外情况,若不进行妥善处理,可能会导致程序崩溃。本文将详细介绍 Dart 中的异常处理机制,包括异常的抛出、捕获以及不同类型的异常,并结合代码示例进行说明。
\\n在 Dart 中,异常是指程序在执行过程中遇到的错误情况。当出现异常时,程序的正常执行流程会被打断,若没有合适的异常处理机制,程序就会崩溃。Dart 中的异常可以是预定义的异常类型,也可以是自定义的异常类型。
\\n在 Dart 中,可以使用 throw
关键字来抛出异常。throw
后面可以跟任何对象,但通常会抛出一个继承自 Exception
或 Error
的对象。
void divide(int a, int b) {\\n if (b == 0) {\\n throw Exception(\'除数不能为零\');\\n }\\n print(\'结果: ${a / b}\');\\n}\\n\\nvoid main() {\\n divide(10, 0);\\n}\\n
\\ndivide
函数,用于计算两个整数的除法。b
是否为零。如果为零,则使用 throw
关键字抛出一个 Exception
对象,并附带错误信息 \'除数不能为零\'
。main
函数中调用 divide(10, 0)
,由于除数为零,会抛出异常,程序会终止执行。为了避免程序因异常而崩溃,可以使用 try - catch
语句来捕获并处理异常。
void divide(int a, int b) {\\n if (b == 0) {\\n throw Exception(\'除数不能为零\');\\n }\\n print(\'结果: ${a / b}\');\\n}\\n\\nvoid main() {\\n try {\\n divide(10, 0);\\n } catch (e) {\\n print(\'捕获到异常: $e\');\\n }\\n print(\'程序继续执行\');\\n}\\n
\\nmain
函数中,使用 try
块包裹可能会抛出异常的代码 divide(10, 0)
。try
块中的代码抛出异常,程序会立即跳转到 catch
块中执行。catch
块中的参数 e
表示捕获到的异常对象,通过 print(\'捕获到异常: $e\');
可以将异常信息输出。catch
块后面的代码 print(\'程序继续执行\');
。可以在 catch
语句中指定要捕获的异常类型,这样可以针对不同类型的异常进行不同的处理。
void divide(int a, int b) {\\n if (b == 0) {\\n throw ArgumentError(\'除数不能为零\');\\n }\\n print(\'结果: ${a / b}\');\\n}\\n\\nvoid main() {\\n try {\\n divide(10, 0);\\n } on ArgumentError catch (e) {\\n print(\'捕获到参数错误异常: ${e.message}\');\\n } catch (e) {\\n print(\'捕获到其他异常: $e\');\\n }\\n}\\n
\\ndivide
函数中,抛出了一个 ArgumentError
类型的异常。main
函数的 try - catch
语句中,首先使用 on ArgumentError catch (e)
来捕获 ArgumentError
类型的异常,并输出异常信息。ArgumentError
类型,则会被第二个 catch
块捕获并处理。finally
语句用于定义无论是否发生异常都会执行的代码块。通常用于释放资源,如关闭文件、网络连接等。
void divide(int a, int b) {\\n if (b == 0) {\\n throw Exception(\'除数不能为零\');\\n }\\n print(\'结果: ${a / b}\');\\n}\\n\\nvoid main() {\\n try {\\n divide(10, 0);\\n } catch (e) {\\n print(\'捕获到异常: $e\');\\n } finally {\\n print(\'无论是否发生异常,这里的代码都会执行\');\\n }\\n}\\n
\\ntry - catch
语句中添加了 finally
块。try
块中的代码是否抛出异常,finally
块中的代码 print(\'无论是否发生异常,这里的代码都会执行\');
都会被执行。除了使用 Dart 预定义的异常类型,还可以自定义异常类来满足特定的需求。
\\n// 自定义异常类\\nclass CustomException implements Exception {\\n final String message;\\n\\n CustomException(this.message);\\n\\n @override\\n String toString() {\\n return \'CustomException: $message\';\\n }\\n}\\n\\nvoid checkNumber(int number) {\\n if (number < 0) {\\n throw CustomException(\'数字不能为负数\');\\n }\\n print(\'数字合法: $number\');\\n}\\n\\nvoid main() {\\n try {\\n checkNumber(-5);\\n } catch (e) {\\n if (e is CustomException) {\\n print(\'捕获到自定义异常: ${e.message}\');\\n } else {\\n print(\'捕获到其他异常: $e\');\\n }\\n }\\n}\\n
\\nCustomException
,它实现了 Exception
接口,并包含一个 message
字段用于存储异常信息。toString
方法,以便在输出异常信息时能更清晰地显示。checkNumber
函数中,检查传入的数字是否为负数,如果是,则抛出 CustomException
异常。main
函数的 try - catch
语句中,捕获异常并判断是否为 CustomException
类型,然后进行相应的处理。rethrow
关键字用于在捕获异常后重新抛出该异常,通常用于在处理部分异常信息后,将异常传递给调用者继续处理。
void divide(int a, int b) {\\n if (b == 0) {\\n throw Exception(\'除数不能为零\');\\n }\\n print(\'结果: ${a / b}\');\\n}\\n\\nvoid wrapperFunction() {\\n try {\\n divide(10, 0);\\n } catch (e) {\\n print(\'在 wrapperFunction 中捕获到异常: $e\');\\n rethrow;\\n }\\n}\\n\\nvoid main() {\\n try {\\n wrapperFunction();\\n } catch (e) {\\n print(\'在 main 函数中捕获到重新抛出的异常: $e\');\\n }\\n}\\n
\\nwrapperFunction
中,使用 try - catch
语句捕获 divide
函数抛出的异常,并输出异常信息。rethrow
关键字将捕获到的异常重新抛出。main
函数中,再次捕获重新抛出的异常并输出异常信息。Dart 中的异常处理机制提供了强大的工具来应对程序运行过程中出现的错误情况。通过合理使用 throw
、try - catch
、finally
、自定义异常和 rethrow
等特性,可以使程序更加健壮,避免因异常而崩溃。在实际开发中,应根据具体的业务需求和场景,选择合适的异常处理方式。
鸿蒙系统作为华为自主研发的分布式操作系统,在中国市场占有重要地位。根据最新数据,截至2023年底,鸿蒙系统设备已超过7亿台,覆盖手机、平板、智能穿戴、智慧屏等多种终端。对于 Flutter 开发者而言,适配鸿蒙系统不仅能够扩大应用的受众群体,还能充分利用鸿蒙系统的分布式能力、超级终端等特性,为用户提供更加流畅、智能的体验。
\\n在深入适配工作前,我们需要了解 Flutter 与鸿蒙系统的技术架构差异:
\\nFlutter与鸿蒙系统在技术架构上有以下主要差异:
\\n渲染引擎:
\\n编程语言:
\\nUI框架:
\\n线程模型:
\\n跨平台策略:
\\n组件系统:
\\n布局系统:
\\n\\n\\n深入理解:Flutter 与鸿蒙的渲染机制差异
\\nFlutter 使用自己的 Skia 渲染引擎直接绘制 UI,不依赖于平台的原生组件,这使得 Flutter 应用在各平台上具有高度一致的外观和行为。而鸿蒙系统则采用了自研的图形渲染栈,包括图形引擎、合成器和渲染服务等组件,支持多种渲染模式。
\\n这种架构差异导致 Flutter 应用在鸿蒙系统上运行时,需要通过适配层将 Flutter 的渲染指令转换为鸿蒙图形 API 的调用。目前,这种适配主要通过鸿蒙的 Android 兼容层实现,但由于技术的发展,未来可能会出现直接基于鸿蒙图形栈的 Flutter 引擎实现。
\\n
在开始 Flutter 应用适配鸿蒙之前,我们需要准备以下开发环境:
\\n\\n\\n技术探讨:为什么需要同时安装 DevEco Studio 和 Android Studio?
\\nFlutter 的工具链主要基于 Android 工具链构建,而鸿蒙系统虽然有自己的开发工具 DevEco Studio,但在适配 Flutter 应用时,我们仍需要 Android Studio 提供的 Gradle 构建系统和部分 Android SDK 工具。这种双工具链的配置反映了当前 Flutter 适配鸿蒙的过渡特性 - Flutter 团队正在努力使 Flutter 直接支持鸿蒙系统,但目前仍需要通过 Android 兼容层来实现部分功能。
\\n从开发者角度看,这意味着你需要同时掌握两套工具的基本操作,但大部分 Flutter 开发工作仍可以在你熟悉的环境中完成。DevEco Studio 主要用于鸿蒙特有功能的开发和测试,如分布式能力、超级终端等。
\\n
# 打开 DevEco Studio\\n# 进入 Settings > Appearance & Behavior > System Settings > HarmonyOS SDK\\n# 下载所需版本的 SDK(建议选择 API 9 或更高版本)\\n
\\n鸿蒙模拟器是测试应用的重要工具,相比真机调试,模拟器具有启动快速、配置灵活的优势。
\\n创建鸿蒙模拟器:
\\n# 在 DevEco Studio 中\\n# 点击 Tools > Device Manager\\n# 点击 Create Device 创建新模拟器\\n# 选择设备类型(手机/平板/智能手表等)\\n# 选择系统版本(建议 API 9 或更高)\\n
\\n模拟器性能优化:
\\n\\n\\n深入分析:鸿蒙模拟器与 Android 模拟器的区别
\\n鸿蒙模拟器基于 QEMU 虚拟化技术,与 Android 模拟器有相似之处,但在系统架构、API 实现和性能特性上存在显著差异:
\\n\\n
\\n- \\n
\\n系统架构:鸿蒙模拟器模拟的是鸿蒙系统架构,包括其微内核、分布式软总线等特有组件,而非 Android 的 Linux 内核和服务。
\\n- \\n
\\nAPI 实现:鸿蒙模拟器提供了鸿蒙特有 API 的完整实现,如分布式能力、超级终端 API 等,这些在 Android 模拟器中不存在。
\\n- \\n
\\n多设备协同:鸿蒙模拟器支持模拟多设备协同场景,可以创建多个虚拟设备并模拟它们之间的互联互通,这是测试鸿蒙分布式应用的关键功能。
\\n- \\n
\\n性能特点:在 Flutter 渲染方面,由于 Flutter 引擎对 Android 图形栈的优化更为成熟,鸿蒙模拟器上的 Flutter 应用可能在某些复杂 UI 场景下性能略低。
\\n作为 Flutter 开发者,你会发现鸿蒙模拟器的操作逻辑与 Android 模拟器类似,但在测试分布式功能时,鸿蒙模拟器提供了更丰富的选项和更真实的环境模拟。
\\n
flutter create --platforms=android,ios harmony_flutter_app\\ncd harmony_flutter_app\\n
\\n\\n\\n注意:目前 Flutter 官方尚未直接支持
\\nharmony
作为平台参数,上述命令创建的是支持 Android 和 iOS 的项目,我们将在后续步骤中添加鸿蒙支持。
在项目创建后,我们需要检查 Flutter 版本兼容性:
\\nflutter doctor\\n
\\n确保所有依赖项都已正确安装,特别是 Android 工具链,因为鸿蒙适配目前仍依赖于部分 Android 构建工具。
\\n在 pubspec.yaml
文件中添加鸿蒙平台支持:
flutter:\\n uses-material-design: true\\n platform:\\n harmony: # 添加鸿蒙平台配置\\n package: com.example.harmony_flutter_app\\n label: Harmony Flutter App\\n icon: assets/harmony_icon.png # 鸿蒙应用图标\\n min_sdk_version: 9 # 最低支持的鸿蒙 API 版本\\n target_sdk_version: 10 # 目标鸿蒙 API 版本\\n
\\n同时,我们需要在 android/app/build.gradle
文件中添加鸿蒙相关配置:
android {\\n // 现有 Android 配置...\\n \\n // 添加鸿蒙兼容配置\\n harmonyOS {\\n compileSdkVersion 10\\n minSdkVersion 9\\n targetSdkVersion 10\\n }\\n}\\n\\ndependencies {\\n // 现有依赖...\\n \\n // 添加鸿蒙兼容库\\n implementation \'com.huawei.hms:base:6.6.0.300\'\\n implementation \'com.huawei.hms:hmscoreinstaller:6.6.0.300\'\\n}\\n
\\n\\n\\n技术探讨:Flutter 项目如何同时支持 Android 和鸿蒙?
\\n鸿蒙系统提供了与 Android 的兼容层,使得大多数 Android 应用可以在鸿蒙系统上运行。Flutter 应用适配鸿蒙时,主要通过两种方式:
\\n\\n
\\n- \\n
\\n兼容层适配:利用鸿蒙的 Android 兼容层,使 Flutter 应用以 Android 应用的形式运行在鸿蒙系统上。这种方式实现简单,但无法充分利用鸿蒙系统特性。
\\n- \\n
\\n原生适配:通过 Flutter 的平台通道(Platform Channels)调用鸿蒙原生 API,实现对鸿蒙特有功能的支持。这种方式开发成本较高,但能够提供更好的用户体验和性能。
\\n在实际项目中,通常采用混合策略,基础功能通过兼容层实现,而关键特性则使用原生适配方式。
\\n对于 Flutter 开发者来说,理解这两种适配方式的区别非常重要:
\\n\\n
\\n- \\n
\\n兼容层适配就像是让你的 Flutter 应用穿着 Android 的外衣在鸿蒙系统上运行,它可以正常工作,但无法展现鸿蒙系统的特色。
\\n- \\n
\\n原生适配则是让你的 Flutter 应用直接与鸿蒙系统对话,可以使用分布式能力、超级终端等特性,但需要编写更多平台特定代码。
\\n大多数 Flutter 开发者会从兼容层适配开始,然后逐步添加原生适配的功能,这样可以平衡开发效率和用户体验。
\\n
创建平台特定的代码目录:
\\nlib/\\n ├── main.dart\\n └── platform/\\n ├── harmony/\\n │ ├── platform_features.dart\\n │ └── harmony_services.dart\\n ├── android/\\n │ └── platform_features.dart\\n ├── ios/\\n │ └── platform_features.dart\\n └── platform_features.dart\\n
\\n实现平台特定功能:
\\n// lib/platform/platform_features.dart\\nimport \'dart:io\' show Platform;\\nimport \'package:flutter/foundation.dart\' show kIsWeb;\\n\\n/// 平台特性抽象类,定义所有平台共用的接口\\nabstract class PlatformFeatures {\\n /// 获取平台名称\\n String getPlatformName();\\n \\n /// 检查是否支持特定功能\\n bool supportsFeature(String featureName);\\n \\n /// 调用平台特定API\\n Future<dynamic> invokePlatformApi(String apiName, Map<String, dynamic> params);\\n \\n /// 工厂构造函数,根据当前运行平台返回对应实现\\n static PlatformFeatures getInstance() {\\n if (kIsWeb) {\\n throw UnsupportedError(\'Web平台暂不支持鸿蒙特性\');\\n } else if (Platform.isHarmonyOS) { // 注意:这里的Platform.isHarmonyOS是假设的API,实际Flutter SDK尚未提供\\n // 导入鸿蒙平台实现\\n return HarmonyFeatures();\\n } else if (Platform.isAndroid) {\\n // 导入Android平台实现\\n return AndroidFeatures();\\n } else if (Platform.isIOS) {\\n // 导入iOS平台实现\\n return IOSFeatures();\\n } else {\\n throw UnsupportedError(\'不支持的平台: ${Platform.operatingSystem}\');\\n }\\n }\\n}\\n\\n// lib/platform/harmony/platform_features.dart\\nimport \'../platform_features.dart\';\\nimport \'package:flutter/services.dart\';\\n\\n/// 鸿蒙平台特性实现\\nclass HarmonyFeatures implements PlatformFeatures {\\n // 定义与鸿蒙原生代码通信的平台通道\\n static const MethodChannel _channel = MethodChannel(\'com.example.harmony_flutter_app/harmony_features\');\\n \\n @override\\n String getPlatformName() => \'HarmonyOS\';\\n \\n @override\\n bool supportsFeature(String featureName) {\\n // 鸿蒙特有功能支持检查\\n switch (featureName) {\\n case \'distributed_capability\': // 分布式能力\\n case \'super_terminal\': // 超级终端\\n case \'atomic_service\': // 原子化服务\\n return true;\\n default:\\n return false;\\n }\\n }\\n \\n @override\\n Future<dynamic> invokePlatformApi(String apiName, Map<String, dynamic> params) async {\\n try {\\n // 通过平台通道调用鸿蒙原生API\\n return await _channel.invokeMethod(apiName, params);\\n } catch (e) {\\n print(\'调用鸿蒙API失败: $apiName, 错误: $e\');\\n rethrow;\\n }\\n }\\n \\n /// 检测当前设备是否为鸿蒙系统\\n /// \\n /// 由于Flutter SDK尚未原生支持鸿蒙系统检测,\\n /// 我们需要通过平台通道自行实现检测逻辑\\n static Future<bool> isHarmonyOS() async {\\n try {\\n final result = await _channel.invokeMethod(\'checkHarmonyOS\');\\n return result == true;\\n } catch (e) {\\n print(\'检测鸿蒙系统失败: $e\');\\n return false;\\n }\\n }\\n \\n /// 获取鸿蒙系统版本\\n Future<String> getHarmonyOSVersion() async {\\n try {\\n final version = await _channel.invokeMethod(\'getHarmonyOSVersion\');\\n return version.toString();\\n } catch (e) {\\n print(\'获取鸿蒙系统版本失败: $e\');\\n return \'unknown\';\\n }\\n }\\n \\n /// 调用鸿蒙分布式能力\\n /// \\n /// 分布式能力是鸿蒙系统的核心特性之一,允许应用跨设备协同工作\\n Future<Map<String, dynamic>> callDistributedCapability(String capabilityName, Map<String, dynamic> params) async {\\n try {\\n final result = await _channel.invokeMethod(\'callDistributedCapability\', {\\n \'capability\': capabilityName,\\n \'params\': params\\n });\\n return Map<String, dynamic>.from(result);\\n } catch (e) {\\n print(\'调用鸿蒙分布式能力失败: $capabilityName, 错误: $e\');\\n return {\'error\': e.toString()};\\n }\\n }\\n \\n // 请求多个权限\\n static Future<Map<String, bool>> requestPermissions(List<String> permissions) async {\\n try {\\n final result = await _channel.invokeMethod(\'requestPermissions\', {\\n \'permissions\': permissions,\\n });\\n return Map<String, bool>.from(result);\\n } catch (e) {\\n print(\'请求多个权限失败: $permissions, 错误: $e\');\\n return permissions.fold<Map<String, bool>>({}, (map, permission) {\\n map[permission] = false;\\n return map;\\n });\\n }\\n }\\n \\n // 检查权限状态\\n static Future<bool> checkPermission(String permission) async {\\n try {\\n final result = await _channel.invokeMethod(\'checkPermission\', {\\n \'permission\': permission,\\n });\\n return result == true;\\n } catch (e) {\\n print(\'检查权限状态失败: $permission, 错误: $e\');\\n return false;\\n }\\n }\\n}\\n
\\n\\n\\nFlutter与鸿蒙平台检测的差异:
\\n在Flutter中,我们习惯使用
\\nPlatform.isAndroid
或Platform.isIOS
来检测平台,但目前Flutter SDK尚未提供Platform.isHarmonyOS
这样的API。这是因为Flutter官方尚未将鸿蒙作为一个独立平台看待,而是通过Android兼容层来支持。对于Flutter开发者来说,这意味着我们需要自行实现鸿蒙系统的检测逻辑。上面的代码展示了一种可能的实现方式:通过平台通道调用原生代码来检测当前设备是否运行鸿蒙系统。
\\n在实际开发中,你可能会遇到这样的情况:
\\nPlatform.isAndroid
在鸿蒙设备上返回true
,这是因为鸿蒙的Android兼容层使得系统在某些API调用上会表现为Android系统。因此,更准确的做法是结合原生代码检测,如上面的HarmonyFeatures.isHarmonyOS()
方法所示。
为了支持上述平台通道调用,我们需要在鸿蒙原生代码中实现对应的功能。以下是一个简化的Java实现示例:
\\n// android/app/src/main/java/com/example/harmony_flutter_app/HarmonyFeaturesPlugin.java\\npackage com.example.harmony_flutter_app;\\n\\nimport androidx.annotation.NonNull;\\nimport io.flutter.embedding.engine.plugins.FlutterPlugin;\\nimport io.flutter.plugin.common.MethodCall;\\nimport io.flutter.plugin.common.MethodChannel;\\nimport io.flutter.plugin.common.MethodChannel.MethodCallHandler;\\nimport io.flutter.plugin.common.MethodChannel.Result;\\n\\n// 鸿蒙相关导入\\nimport ohos.system.DeviceInfo;\\nimport ohos.app.Context;\\n\\npublic class HarmonyFeaturesPlugin implements FlutterPlugin, MethodCallHandler {\\n private MethodChannel channel;\\n private Context context;\\n\\n @Override\\n public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {\\n channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), \\"com.example.harmony_flutter_app/harmony_features\\");\\n channel.setMethodCallHandler(this);\\n // 获取上下文\\n // 注意:在实际代码中,你需要根据鸿蒙API获取正确的上下文\\n // context = flutterPluginBinding.getApplicationContext();\\n }\\n\\n @Override\\n public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {\\n switch (call.method) {\\n case \\"checkHarmonyOS\\":\\n // 检测是否为鸿蒙系统\\n // 注意:这里使用的是假设的API,实际代码需要根据鸿蒙SDK调整\\n boolean isHarmonyOS = checkIfHarmonyOS();\\n result.success(isHarmonyOS);\\n break;\\n case \\"getHarmonyOSVersion\\":\\n // 获取鸿蒙系统版本\\n String version = getHarmonyVersion();\\n result.success(version);\\n break;\\n case \\"callDistributedCapability\\":\\n // 调用分布式能力\\n String capability = call.argument(\\"capability\\");\\n Map<String, Object> params = call.argument(\\"params\\");\\n Map<String, Object> response = invokeDistributedCapability(capability, params);\\n result.success(response);\\n break;\\n default:\\n result.notImplemented();\\n break;\\n }\\n }\\n\\n // 检测是否为鸿蒙系统\\n private boolean checkIfHarmonyOS() {\\n try {\\n // 鸿蒙系统检测逻辑\\n // 这里是简化的示例,实际代码需要根据鸿蒙SDK调整\\n return System.getProperty(\\"os.name\\").toLowerCase().contains(\\"harmony\\");\\n // 或者使用鸿蒙特有API:\\n // return DeviceInfo.getOSFullName().contains(\\"HarmonyOS\\");\\n } catch (Exception e) {\\n e.printStackTrace();\\n return false;\\n }\\n }\\n\\n // 获取鸿蒙系统版本\\n private String getHarmonyVersion() {\\n try {\\n // 获取鸿蒙系统版本的逻辑\\n // 这里是简化的示例,实际代码需要根据鸿蒙SDK调整\\n return System.getProperty(\\"os.version\\");\\n // 或者使用鸿蒙特有API:\\n // return DeviceInfo.getOSFullName();\\n } catch (Exception e) {\\n e.printStackTrace();\\n return \\"unknown\\";\\n }\\n }\\n\\n // 调用分布式能力\\n private Map<String, Object> invokeDistributedCapability(String capability, Map<String, Object> params) {\\n Map<String, Object> result = new HashMap<>();\\n try {\\n // 根据不同的分布式能力执行不同的逻辑\\n switch (capability) {\\n case \\"device_discovery\\":\\n // 设备发现逻辑\\n result.put(\\"devices\\", discoverNearbyDevices());\\n break;\\n case \\"file_transfer\\":\\n // 文件传输逻辑\\n String filePath = (String) params.get(\\"file_path\\");\\n String targetDeviceId = (String) params.get(\\"target_device_id\\");\\n boolean success = transferFile(filePath, targetDeviceId);\\n result.put(\\"success\\", success);\\n break;\\n // 其他分布式能力...\\n default:\\n result.put(\\"error\\", \\"Unsupported capability: \\" + capability);\\n break;\\n }\\n } catch (Exception e) {\\n e.printStackTrace();\\n result.put(\\"error\\", e.toString());\\n }\\n return result;\\n }\\n\\n // 发现附近设备\\n private List<Map<String, Object>> discoverNearbyDevices() {\\n // 实现设备发现逻辑\\n // 这里是简化的示例,实际代码需要根据鸿蒙SDK调整\\n List<Map<String, Object>> devices = new ArrayList<>();\\n // ... 设备发现代码 ...\\n return devices;\\n }\\n\\n // 文件传输\\n private boolean transferFile(String filePath, String targetDeviceId) {\\n // 实现文件传输逻辑\\n // 这里是简化的示例,实际代码需要根据鸿蒙SDK调整\\n // ... 文件传输代码 ...\\n return true;\\n }\\n\\n @Override\\n public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {\\n channel.setMethodCallHandler(null);\\n }\\n}\\n
\\n\\n\\nFlutter开发者视角:鸿蒙原生代码与Android原生代码的区别
\\n对于熟悉Flutter+Android开发的开发者来说,上面的鸿蒙原生代码看起来非常类似于Android代码,这是因为:
\\n\\n
\\n- \\n
\\n鸿蒙系统提供了与Android兼容的API,使得大部分Android代码可以在鸿蒙系统上运行。
\\n- \\n
\\n目前Flutter适配鸿蒙主要通过Android兼容层实现,因此插件开发方式与Android插件类似。
\\n但需要注意的关键区别:
\\n\\n
\\n- \\n
\\n包名和导入:鸿蒙系统的包名通常以
\\nohos
开头,而不是Android的android
。- \\n
\\nAPI差异:虽然有兼容层,但鸿蒙系统的某些API与Android存在差异,特别是系统服务、权限管理等方面。
\\n- \\n
\\n分布式能力:鸿蒙系统提供了分布式软总线、设备虚拟化等Android没有的特性,这些需要使用鸿蒙特有API。
\\n作为Flutter开发者,你不需要深入了解所有鸿蒙原生开发细节,但掌握基本的平台通道使用和鸿蒙特有功能调用方式是必要的。
\\n
鸿蒙系统有其独特的设计语言和交互模式,在适配时需要注意以下几点:
\\n状态栏与导航栏:鸿蒙系统的状态栏和导航栏与Android有细微差别,特别是在手势导航和通知显示方面。
\\n圆角设计:鸿蒙系统UI普遍采用更大的圆角设计,Flutter应用在适配时应当调整组件的borderRadius值。
\\n动效体验:鸿蒙系统强调\\"流体动效\\",动画过渡更加平滑自然,Flutter应用可以通过自定义动画曲线来匹配这种体验。
\\n字体与排版:鸿蒙系统使用HarmonyOS Sans字体,与Flutter默认使用的字体有所不同,需要在应用中引入并设置。
\\n\\n\\nFlutter开发者视角:Material Design与鸿蒙设计语言的差异
\\n作为Flutter开发者,你可能习惯了使用Material Design组件构建UI。在适配鸿蒙系统时,需要注意以下设计语言差异:
\\n
Material Design与鸿蒙设计语言在以下几个关键设计元素上存在显著差异:
\\n主色调风格:
\\n圆角设计:
\\n阴影效果:
\\n动画特性:
\\n图标风格:
\\n\\n\\n这些差异并不意味着你需要完全重构UI,而是通过调整主题参数、动画曲线和组件样式,让Flutter应用在鸿蒙系统上看起来更加\\"原生\\"。
\\n
为了让Flutter应用在鸿蒙系统上有更好的视觉体验,我们可以创建一个鸿蒙特定的主题:
\\n// lib/theme/harmony_theme.dart\\nimport \'package:flutter/material.dart\';\\nimport \'../platform/harmony/platform_features.dart\';\\n\\nclass HarmonyTheme {\\n // 鸿蒙系统默认主题色\\n static const Color harmonyPrimaryColor = Color(0xFF007DFF);\\n static const Color harmonySecondaryColor = Color(0xFF00C3FF);\\n \\n // 鸿蒙系统亮色主题\\n static final ThemeData lightTheme = ThemeData.light().copyWith(\\n primaryColor: harmonyPrimaryColor,\\n colorScheme: const ColorScheme.light().copyWith(\\n primary: harmonyPrimaryColor,\\n secondary: harmonySecondaryColor,\\n ),\\n cardTheme: CardTheme(\\n shape: RoundedRectangleBorder(\\n borderRadius: BorderRadius.circular(16.0),\\n ),\\n ),\\n elevatedButtonTheme: ElevatedButtonThemeData(\\n style: ElevatedButton.styleFrom(\\n shape: RoundedRectangleBorder(\\n borderRadius: BorderRadius.circular(20.0),\\n ),\\n elevation: 1,\\n backgroundColor: harmonyPrimaryColor,\\n ),\\n ),\\n // 其他组件样式调整...\\n );\\n \\n // 鸿蒙系统暗色主题\\n static final ThemeData darkTheme = ThemeData.dark().copyWith(\\n primaryColor: harmonyPrimaryColor,\\n colorScheme: const ColorScheme.dark().copyWith(\\n primary: harmonyPrimaryColor,\\n secondary: harmonySecondaryColor,\\n ),\\n // 暗色主题其他样式...\\n );\\n \\n // 根据平台选择主题\\n static ThemeData getThemeForCurrentPlatform(bool isDarkMode) {\\n return isDarkMode ? darkTheme : lightTheme;\\n }\\n \\n // 获取当前系统主题模式\\n static Future<ThemeMode> getSystemThemeMode() async {\\n try {\\n // 检测是否为鸿蒙系统\\n bool isHarmony = await HarmonyFeatures.isHarmonyOS();\\n if (!isHarmony) {\\n // 非鸿蒙系统使用系统默认主题模式\\n return ThemeMode.system;\\n }\\n \\n // 通过平台通道获取鸿蒙系统当前主题模式\\n final HarmonyFeatures features = HarmonyFeatures();\\n final result = await features.invokePlatformApi(\'getSystemThemeMode\', {});\\n final bool isDark = result[\'isDark\'] ?? false;\\n \\n return isDark ? ThemeMode.dark : ThemeMode.light;\\n } catch (e) {\\n print(\'获取系统主题模式失败: $e\');\\n return ThemeMode.system;\\n }\\n }\\n}\\n
\\n在应用的主入口使用这个主题:
\\n// lib/main.dart\\nimport \'package:flutter/material.dart\';\\nimport \'theme/harmony_theme.dart\';\\n\\nvoid main() {\\n runApp(MyApp());\\n}\\n\\nclass MyApp extends StatefulWidget {\\n @override\\n _MyAppState createState() => _MyAppState();\\n}\\n\\nclass _MyAppState extends State<MyApp> {\\n ThemeMode _themeMode = ThemeMode.system;\\n bool _isDarkMode = false;\\n \\n @override\\n void initState() {\\n super.initState();\\n _loadThemeMode();\\n }\\n \\n Future<void> _loadThemeMode() async {\\n final themeMode = await HarmonyTheme.getSystemThemeMode();\\n setState(() {\\n _themeMode = themeMode;\\n _isDarkMode = themeMode == ThemeMode.dark;\\n });\\n }\\n \\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n title: \'Flutter Harmony App\',\\n theme: HarmonyTheme.lightTheme,\\n darkTheme: HarmonyTheme.darkTheme,\\n themeMode: _themeMode,\\n home: HomePage(isDarkMode: _isDarkMode),\\n );\\n }\\n}\\n
\\n\\n\\n深入理解:Flutter主题与鸿蒙主题的桥接
\\nFlutter的主题系统基于Material Design,而鸿蒙系统有自己的设计规范。上面的代码展示了如何在两者之间建立桥接:
\\n\\n
\\n- \\n
\\n颜色适配:鸿蒙系统的主色调通常更加柔和,我们通过自定义ColorScheme来匹配。
\\n- \\n
\\n组件样式:调整卡片、按钮等组件的圆角和阴影,使其符合鸿蒙设计语言。
\\n- \\n
\\n主题模式同步:通过平台通道获取鸿蒙系统的当前主题模式(亮色/暗色),并同步到Flutter应用。
\\n这种适配方式保持了Flutter应用的跨平台一致性,同时又能在鸿蒙系统上呈现更加\\"原生\\"的视觉效果。
\\n
鸿蒙系统的手势交互与Android、iOS有所不同,特别是在以下几个方面:
\\n返回手势:鸿蒙系统支持从屏幕左右两侧向中间滑动返回上一级界面,这与iOS类似但又有区别。
\\n多任务手势:鸿蒙系统使用上滑并停留来调出多任务界面,这需要在Flutter应用中避免与此冲突的手势。
\\n分屏手势:鸿蒙系统支持特定的分屏手势,Flutter应用需要正确响应分屏状态变化。
\\n以下是适配鸿蒙系统手势的示例代码:
\\n// lib/widgets/harmony_gesture_detector.dart\\nimport \'package:flutter/material.dart\';\\nimport \'../platform/harmony/platform_features.dart\';\\n\\nclass HarmonyGestureDetector extends StatefulWidget {\\n final Widget child;\\n final Function()? onBack;\\n \\n const HarmonyGestureDetector({\\n Key? key,\\n required this.child,\\n this.onBack,\\n }) : super(key: key);\\n \\n @override\\n _HarmonyGestureDetectorState createState() => _HarmonyGestureDetectorState();\\n}\\n\\nclass _HarmonyGestureDetectorState extends State<HarmonyGestureDetector> {\\n late HarmonyFeatures _harmonyFeatures;\\n bool _isHarmonyOS = false;\\n \\n @override\\n void initState() {\\n super.initState();\\n _initPlatform();\\n }\\n \\n Future<void> _initPlatform() async {\\n _isHarmonyOS = await HarmonyFeatures.isHarmonyOS();\\n if (_isHarmonyOS) {\\n _harmonyFeatures = HarmonyFeatures();\\n // 注册鸿蒙系统手势回调\\n await _harmonyFeatures.invokePlatformApi(\'registerGestureCallback\', {});\\n }\\n }\\n \\n @override\\n Widget build(BuildContext context) {\\n // 在鸿蒙系统上使用特定的手势检测\\n if (_isHarmonyOS) {\\n return WillPopScope(\\n onWillPop: () async {\\n if (widget.onBack != null) {\\n widget.onBack!();\\n return false;\\n }\\n return true;\\n },\\n child: widget.child,\\n );\\n } else {\\n // 在其他平台上使用普通的手势检测\\n return widget.child;\\n }\\n }\\n \\n @override\\n void dispose() {\\n if (_isHarmonyOS) {\\n // 取消注册鸿蒙系统手势回调\\n _harmonyFeatures.invokePlatformApi(\'unregisterGestureCallback\', {});\\n }\\n super.dispose();\\n }\\n}\\n
\\n\\n\\nFlutter开发者须知:鸿蒙系统的手势处理
\\n对于Flutter开发者来说,鸿蒙系统的手势处理可能是最容易被忽视的适配点之一。由于Flutter有自己的手势系统,它可能与鸿蒙系统的原生手势产生冲突,特别是在以下场景:
\\n\\n
\\n- \\n
\\n边缘滑动返回:如果你的Flutter应用使用了水平滑动手势(如PageView),可能会与鸿蒙系统的边缘返回手势冲突。解决方法是使用
\\nWillPopScope
结合平台通道来协调两者。- \\n
\\n分屏适配:鸿蒙系统的分屏功能会改变应用窗口大小,Flutter应用需要正确响应这些变化。可以通过监听
\\nMediaQuery
的变化来适配不同屏幕尺寸。- \\n
\\n多窗口支持:鸿蒙系统支持应用多窗口,这对Flutter应用是一个挑战,因为Flutter默认假设一个应用只有一个窗口。解决方法是使用平台通道监听窗口状态变化,并相应地调整UI。
\\n在实际开发中,你可能不需要处理所有这些情况,但了解这些差异有助于解决可能出现的问题。
\\n
鸿蒙系统的权限模型与Android类似,但有一些细微差别。以下是处理鸿蒙系统权限的示例代码:
\\n// lib/utils/harmony_permissions.dart\\nimport \'package:flutter/services.dart\';\\n\\nclass HarmonyPermissions {\\n static const MethodChannel _channel = MethodChannel(\'com.example.harmony_flutter_app/permissions\');\\n \\n // 权限类型枚举\\n static const String CAMERA = \'camera\';\\n static const String LOCATION = \'location\';\\n static const String STORAGE = \'storage\';\\n static const String MICROPHONE = \'microphone\';\\n static const String CONTACTS = \'contacts\';\\n \\n // 请求单个权限\\n static Future<bool> requestPermission(String permission) async {\\n try {\\n final result = await _channel.invokeMethod(\'requestPermission\', {\\n \'permission\': permission,\\n });\\n return result == true;\\n } catch (e) {\\n print(\'请求权限失败: $permission, 错误: $e\');\\n return false;\\n }\\n }\\n \\n // 请求多个权限\\n static Future<Map<String, bool>> requestPermissions(List<String> permissions) async {\\n try {\\n final result = await _channel.invokeMethod(\'requestPermissions\', {\\n \'permissions\': permissions,\\n });\\n return Map<String, bool>.from(result);\\n } catch (e) {\\n print(\'请求多个权限失败: $permissions, 错误: $e\');\\n return permissions.fold<Map<String, bool>>({}, (map, permission) {\\n map[permission] = false;\\n return map;\\n });\\n }\\n }\\n \\n // 检查权限状态\\n static Future<bool> checkPermission(String permission) async {\\n try {\\n final result = await _channel.invokeMethod(\'checkPermission\', {\\n \'permission\': permission,\\n });\\n return result == true;\\n } catch (e) {\\n print(\'检查权限状态失败: $permission, 错误: $e\');\\n return false;\\n }\\n }\\n}\\n
\\n\\n\\nFlutter与鸿蒙权限模型对比:
\\n对于Flutter开发者来说,理解鸿蒙系统与Android权限模型的差异非常重要:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n
权限特性 Android 鸿蒙系统 权限分类 普通、危险、特殊权限 普通、用户授权、系统授权权限 运行时权限 需要动态申请 需要动态申请,但UI和交互有差异 权限组 相关权限归为一组 权限更加细分,粒度更小 权限撤销 用户可在设置中撤销 用户可在设置中撤销,且有更详细的使用记录 后台权限 需要特殊处理 更严格的后台权限控制 在实际开发中,虽然通过Android兼容层可以使用Android的权限API,但为了更好的用户体验和更高的应用商店通过率,建议使用鸿蒙原生的权限API。
\\n
鸿蒙系统最大的特色之一是其分布式能力,允许应用跨设备协同工作。以下是集成鸿蒙分布式能力的示例:
\\n// lib/services/harmony_distributed_service.dart\\nimport \'package:flutter/services.dart\';\\nimport \'../platform/harmony/platform_features.dart\';\\n\\nclass HarmonyDistributedService {\\n final HarmonyFeatures _harmonyFeatures = HarmonyFeatures();\\n \\n // 检查是否支持分布式能力\\n Future<bool> isDistributedCapabilitySupported() async {\\n try {\\n final result = await _harmonyFeatures.invokePlatformApi(\\n \'checkDistributedCapability\', \\n {}\\n );\\n return result[\'supported\'] == true;\\n } catch (e) {\\n print(\'检查分布式能力支持失败: $e\');\\n return false;\\n }\\n }\\n \\n // 发现附近设备\\n Future<List<Map<String, dynamic>>> discoverDevices() async {\\n try {\\n final result = await _harmonyFeatures.callDistributedCapability(\\n \'device_discovery\',\\n {\'timeout\': 10000} // 10秒超时\\n );\\n \\n if (result.containsKey(\'error\')) {\\n throw Exception(result[\'error\']);\\n }\\n \\n return List<Map<String, dynamic>>.from(result[\'devices\'] ?? []);\\n } catch (e) {\\n print(\'发现设备失败: $e\');\\n return [];\\n }\\n }\\n \\n // 连接到设备\\n Future<bool> connectToDevice(String deviceId) async {\\n try {\\n final result = await _harmonyFeatures.callDistributedCapability(\\n \'connect_device\',\\n {\'deviceId\': deviceId}\\n );\\n \\n return result[\'connected\'] == true;\\n } catch (e) {\\n print(\'连接设备失败: $e\');\\n return false;\\n }\\n }\\n \\n // 跨设备启动组件\\n Future<bool> startRemoteAbility(String deviceId, String bundleName, String abilityName) async {\\n try {\\n final result = await _harmonyFeatures.callDistributedCapability(\\n \'start_remote_ability\',\\n {\\n \'deviceId\': deviceId,\\n \'bundleName\': bundleName,\\n \'abilityName\': abilityName\\n }\\n );\\n \\n return result[\'success\'] == true;\\n } catch (e) {\\n print(\'启动远程组件失败: $e\');\\n return false;\\n }\\n }\\n \\n // 分布式数据同步\\n Future<bool> syncData(String deviceId, Map<String, dynamic> data) async {\\n try {\\n final result = await _harmonyFeatures.callDistributedCapability(\\n \'sync_data\',\\n {\\n \'deviceId\': deviceId,\\n \'data\': data\\n }\\n );\\n \\n return result[\'success\'] == true;\\n } catch (e) {\\n print(\'数据同步失败: $e\');\\n return false;\\n }\\n }\\n}\\n
\\n使用分布式服务的示例:
\\n// lib/pages/distributed_demo_page.dart\\nimport \'package:flutter/material.dart\';\\nimport \'../services/harmony_distributed_service.dart\';\\n\\nclass DistributedDemoPage extends StatefulWidget {\\n @override\\n _DistributedDemoPageState createState() => _DistributedDemoPageState();\\n}\\n\\nclass _DistributedDemoPageState extends State<DistributedDemoPage> {\\n final HarmonyDistributedService _service = HarmonyDistributedService();\\n bool _isSupported = false;\\n List<Map<String, dynamic>> _devices = [];\\n bool _isLoading = false;\\n String? _selectedDeviceId;\\n \\n @override\\n void initState() {\\n super.initState();\\n _checkSupport();\\n }\\n \\n Future<void> _checkSupport() async {\\n final isSupported = await _service.isDistributedCapabilitySupported();\\n setState(() {\\n _isSupported = isSupported;\\n });\\n }\\n \\n Future<void> _discoverDevices() async {\\n if (!_isSupported) return;\\n \\n setState(() {\\n _isLoading = true;\\n });\\n \\n final devices = await _service.discoverDevices();\\n \\n setState(() {\\n _devices = devices;\\n _isLoading = false;\\n });\\n }\\n \\n Future<void> _connectToDevice(String deviceId) async {\\n setState(() {\\n _isLoading = true;\\n });\\n \\n final success = await _service.connectToDevice(deviceId);\\n \\n setState(() {\\n if (success) {\\n _selectedDeviceId = deviceId;\\n }\\n _isLoading = false;\\n });\\n \\n if (success) {\\n ScaffoldMessenger.of(context).showSnackBar(\\n SnackBar(content: Text(\'成功连接到设备\'))\\n );\\n } else {\\n ScaffoldMessenger.of(context).showSnackBar(\\n SnackBar(content: Text(\'连接设备失败\'))\\n );\\n }\\n }\\n \\n Future<void> _syncData() async {\\n if (_selectedDeviceId == null) return;\\n \\n setState(() {\\n _isLoading = true;\\n });\\n \\n final success = await _service.syncData(\\n _selectedDeviceId!,\\n {\'message\': \'Hello from Flutter!\', \'timestamp\': DateTime.now().millisecondsSinceEpoch}\\n );\\n \\n setState(() {\\n _isLoading = false;\\n });\\n \\n ScaffoldMessenger.of(context).showSnackBar(\\n SnackBar(content: Text(success ? \'数据同步成功\' : \'数据同步失败\'))\\n );\\n }\\n \\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\'分布式能力演示\')),\\n body: !_isSupported\\n ? Center(child: Text(\'当前设备不支持鸿蒙分布式能力\'))\\n : _buildContent(),\\n );\\n }\\n \\n Widget _buildContent() {\\n return Padding(\\n padding: const EdgeInsets.all(16.0),\\n child: Column(\\n crossAxisAlignment: CrossAxisAlignment.stretch,\\n children: [\\n ElevatedButton(\\n onPressed: _isLoading ? null : _discoverDevices,\\n child: Text(\'发现附近设备\'),\\n ),\\n SizedBox(height: 16),\\n if (_isLoading)\\n Center(child: CircularProgressIndicator())\\n else if (_devices.isEmpty)\\n Center(child: Text(\'未发现设备\'))\\n else\\n Expanded(\\n child: ListView.builder(\\n itemCount: _devices.length,\\n itemBuilder: (context, index) {\\n final device = _devices[index];\\n final deviceId = device[\'deviceId\'] as String;\\n final deviceName = device[\'deviceName\'] as String;\\n final isSelected = deviceId == _selectedDeviceId;\\n \\n return ListTile(\\n title: Text(deviceName),\\n subtitle: Text(deviceId),\\n trailing: isSelected\\n ? Icon(Icons.check_circle, color: Colors.green)\\n : null,\\n onTap: () => _connectToDevice(deviceId),\\n );\\n },\\n ),\\n ),\\n if (_selectedDeviceId != null) ...[ \\n SizedBox(height: 16),\\n ElevatedButton(\\n onPressed: _isLoading ? null : _syncData,\\n child: Text(\'同步数据到选中设备\'),\\n ),\\n ],\\n ],\\n ),\\n );\\n }\\n}\\n
\\n\\n","description":"为什么需要适配鸿蒙系统? 鸿蒙系统作为华为自主研发的分布式操作系统,在中国市场占有重要地位。根据最新数据,截至2023年底,鸿蒙系统设备已超过7亿台,覆盖手机、平板、智能穿戴、智慧屏等多种终端。对于 Flutter 开发者而言,适配鸿蒙系统不仅能够扩大应用的受众群体,还能充分利用鸿蒙系统的分布式能力、超级终端等特性,为用户提供更加流畅、智能的体验。\\n\\nFlutter 与鸿蒙的技术架构对比\\n\\n在深入适配工作前,我们需要了解 Flutter 与鸿蒙系统的技术架构差异:\\n\\nFlutter与鸿蒙系统在技术架构上有以下主要差异:\\n\\n渲染引擎:\\n\\nFlutter…","guid":"https://juejin.cn/post/7481291950034288690","author":"西辰Knight","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-14T05:58:25.117Z","media":null,"categories":["Android","HarmonyOS","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter进阶:Cusor + claude-3.7 AI编程实战效果研究","url":"https://juejin.cn/post/7481238697552674850","content":"Flutter开发者视角:理解鸿蒙分布式能力
\\n对于Flutter开发者来说,鸿蒙系统的分布式能力可能是最陌生但也是最有价值的特性。这里有几个关键概念需要理解:
\\n\\n
\\n- \\n
\\n设备虚拟化:鸿蒙系统将多个设备虚拟化为一个超级终端,应用可以无缝地跨设备运行。这与Flutter的跨平台理念有相似之处,但范围扩展到了多设备协同。
\\n- \\n
\\n分布式软总线:这是鸿蒙系统实现设备互联的底层技术,提供了设备发现、连接和通信的能力。在Flutter中,我们通过平台通道调用这些能力。
\\n- \\n
\\n分布式数据管理:允许应用在多设备间同步和共享数据,这对于需要跨设备协同的应用非常有用。
\\n- \\n
\\n分布式任务调度:允许应用将任务分发到不同设备执行,充分利用多设备的计算资源。
\\n在实际开发中,你可能不需要使用所有这些分布式能力,但了解它们的存在和基本用法,可以帮助你设计出更符合鸿蒙生态的应用。
\\n
最新代码圈有被 claude-3.7 刷屏的情况出现,感觉 AI 代码指导编程的临界点悄然已至。有点类似 Swift4.2、Flutter2.2版本的推出,搞app开发的朋友懂这些关键版本节点的重要性,它代表了从一小部分极客玩家到大众参与的裂变点。如果有还在等待AI辅助编程的朋友,那么可以加入了。最近花了两周多时间体验了 Cusor + claude-3.7 AI编程实战效果。
\\n所有代码都是通过多次谈话自动生成!
这是一个简单的健康调查问卷生成效果:
\\n.\\n├── lib\\n│ ├── controllers\\n│ │ ├── survey_controller.dart (调查表控制器)\\n│ ├── models\\n│ │ └── survey_model.dart (调查模型)\\n│ ├── pages\\n│ │ ├── survey_page.dart (调查表单)\\n│ └── widgets\\n│ └── questions\\n│ ├── question_container.dart (每个组件外边的修饰盒子)\\n│ ├── question_image_upload.dart (上传图片组件)\\n│ ├── question_multi_choice.dart (多选组件)\\n│ ├── question_rating.dart (星星评价组件)\\n│ ├── question_single_choice.dart (单选组件)\\n│ └── question_text.dart (输入框组件)\\n
\\nimport \'package:flutter/material.dart\';\\nimport \'package:get/get.dart\';\\nimport \'../controllers/survey_controller.dart\';\\nimport \'../widgets/questions/question_container.dart\';\\nimport \'../widgets/questions/question_text.dart\';\\nimport \'../widgets/questions/question_single_choice.dart\';\\nimport \'../widgets/questions/question_multi_choice.dart\';\\nimport \'../widgets/questions/question_image_upload.dart\';\\nimport \'../widgets/questions/question_rating.dart\';\\n\\nclass SurveyPage extends StatefulWidget {\\n const SurveyPage({super.key});\\n\\n @override\\n State<SurveyPage> createState() => _SurveyPageState();\\n}\\n\\nclass _SurveyPageState extends State<SurveyPage> {\\n late final SurveyController controller;\\n final RxInt selectedQuestionIndex = (-1).obs;\\n\\n @override\\n void initState() {\\n super.initState();\\n controller = Get.find<SurveyController>();\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n backgroundColor: Colors.white,\\n appBar: _buildAppBar(),\\n body: SingleChildScrollView(\\n child: Column(\\n crossAxisAlignment: CrossAxisAlignment.start,\\n children: [\\n _buildHeader(),\\n _buildQuestionList(),\\n ],\\n ),\\n ),\\n bottomNavigationBar: _buildSubmitButton(),\\n );\\n }\\n\\n PreferredSizeWidget _buildAppBar() {\\n return AppBar(\\n backgroundColor: Colors.white,\\n elevation: 0,\\n leading: IconButton(\\n icon: const Icon(Icons.arrow_back_ios, size: 20),\\n onPressed: () => Get.back(),\\n ),\\n title: const Text(\\n \'量表\',\\n style: TextStyle(\\n fontSize: 18,\\n fontWeight: FontWeight.w500,\\n ),\\n ),\\n centerTitle: true,\\n );\\n }\\n\\n Widget _buildHeader() {\\n return Container(\\n width: double.infinity,\\n padding: const EdgeInsets.all(16.0),\\n decoration: const BoxDecoration(\\n color: Colors.white,\\n borderRadius: BorderRadius.only(\\n bottomLeft: Radius.circular(4),\\n bottomRight: Radius.circular(4),\\n ),\\n ),\\n child: const Text(\\n \'住院病人调查统计表\',\\n style: TextStyle(\\n fontSize: 20,\\n fontWeight: FontWeight.bold,\\n ),\\n ),\\n );\\n }\\n\\n Widget _buildQuestionList() {\\n return Padding(\\n padding: const EdgeInsets.all(16.0),\\n child: Obx(() => Column(\\n children: [\\n QuestionContainer(\\n title: \'1.近期运动情况,请详细描述\',\\n child: Column(\\n crossAxisAlignment: CrossAxisAlignment.start,\\n children: [\\n QuestionText(\\n initialValue: controller.textAnswer.value,\\n onChanged: (value) {\\n controller.updateTextAnswer(value);\\n selectedQuestionIndex.value = 0;\\n },\\n tip: controller.getTip(\'1\'),\\n ),\\n ],\\n ),\\n ),\\n const SizedBox(height: 16),\\n QuestionContainer(\\n title: \'2.住院期间对医院服务总体感觉\',\\n child: Column(\\n crossAxisAlignment: CrossAxisAlignment.start,\\n children: [\\n QuestionSingleChoice(\\n choices: const [\\n Choice(\'好\', \'(52)\'),\\n Choice(\'一般\', \'(49)\'),\\n Choice(\'差\', \'(32)\'),\\n Choice(\'其他\', \'(12)\'),\\n ],\\n initialValue: controller.singleChoiceAnswer.value,\\n onChanged: controller.updateSingleChoice,\\n onSelect: (_) => selectedQuestionIndex.value = 1,\\n tip: controller.getTip(\'2\'),\\n ),\\n ],\\n ),\\n ),\\n const SizedBox(height: 16),\\n QuestionContainer(\\n title: \'3.您有以下哪些症状\',\\n child: Column(\\n crossAxisAlignment: CrossAxisAlignment.start,\\n children: [\\n QuestionMultiChoice(\\n options: const [\\n \'头痛头晕\',\\n \'恶心呕吐\',\\n \'睡眠困难\',\\n \'呼吸困难\',\\n \'晕血症状\',\\n \'四肢发麻\',\\n ],\\n initialValues: controller.multiChoiceAnswers,\\n onChanged: (values) {\\n controller.updateMultiChoice(values);\\n selectedQuestionIndex.value = 2;\\n },\\n tip: controller.getTip(\'3\'),\\n ),\\n ],\\n ),\\n ),\\n const SizedBox(height: 16),\\n QuestionContainer(\\n title: \'4.住院期间您做了哪些辅助检查\',\\n subtitle: \'注:最多可上传10张,每张图片大小不超过2M\',\\n child: Column(\\n crossAxisAlignment: CrossAxisAlignment.start,\\n children: [\\n QuestionImageUpload(\\n initialImages: controller.imageUrls,\\n tip: controller.getTip(\'4\'),\\n onImagesChanged: (images) {\\n selectedQuestionIndex.value = 3;\\n controller.updateImages(images);\\n },\\n ),\\n ],\\n ),\\n ),\\n const SizedBox(height: 16),\\n QuestionContainer(\\n title: \'5.住院期间对医院服务总体感觉是好\',\\n child: Column(\\n crossAxisAlignment: CrossAxisAlignment.start,\\n children: [\\n QuestionSingleChoice(\\n choices: const [\\n Choice(\'是\', \'\'),\\n Choice(\'否\', \'\'),\\n ],\\n initialValue: controller.isGoodService.value ? \'是\' : \'否\',\\n onChanged: (value) {\\n controller.updateServiceSatisfaction(value == \'是\');\\n selectedQuestionIndex.value = 4;\\n },\\n onSelect: (_) => selectedQuestionIndex.value = 4,\\n tip: controller.getTip(\'5\'),\\n ),\\n ],\\n ),\\n ),\\n const SizedBox(height: 16),\\n QuestionContainer(\\n title: \'6.近1个月,晚上上床时间通常在几点?\',\\n child: Column(\\n crossAxisAlignment: CrossAxisAlignment.start,\\n children: [\\n QuestionSingleChoice(\\n choices: const [\\n Choice(\'八点\', \'\'),\\n Choice(\'十一点\', \'\'),\\n ],\\n initialValue: controller.bedTime.value,\\n onChanged: controller.updateBedTime,\\n onSelect: (_) => selectedQuestionIndex.value = 5,\\n tip: controller.getTip(\'6\'),\\n ),\\n ],\\n ),\\n ),\\n const SizedBox(height: 16),\\n QuestionContainer(\\n title: \'7.对医生健康建议满意度\',\\n isRequired: false,\\n child: Column(\\n crossAxisAlignment: CrossAxisAlignment.start,\\n children: [\\n QuestionRating(\\n options: const [\\n RatingOption(\'不满意\', 1),\\n RatingOption(\'一般\', 2),\\n RatingOption(\'非常满意\', 3),\\n ],\\n initialValue: controller.satisfactionRating.value,\\n onChanged: (value) {\\n controller.updateSatisfactionRating(value);\\n selectedQuestionIndex.value = 6;\\n },\\n tip: controller.getTip(\'7\'),\\n ),\\n ],\\n ),\\n ),\\n ],\\n )),\\n );\\n }\\n\\n Widget _buildSubmitButton() {\\n return Container(\\n padding: const EdgeInsets.all(16.0),\\n child: Obx(() => ElevatedButton(\\n onPressed: controller.isSubmitting.value ? null : controller.submitSurvey,\\n style: ElevatedButton.styleFrom(\\n backgroundColor: Theme.of(context).colorScheme.primary,\\n minimumSize: const Size(double.infinity, 48),\\n shape: RoundedRectangleBorder(\\n borderRadius: BorderRadius.circular(4),\\n ),\\n ),\\n child: controller.isSubmitting.value\\n ? const SizedBox(\\n width: 24,\\n height: 24,\\n child: CircularProgressIndicator(\\n strokeWidth: 2,\\n valueColor: AlwaysStoppedAnimation<Color>(Colors.white),\\n ),\\n )\\n : const Text(\\n \'提交\',\\n style: TextStyle(\\n fontSize: 16,\\n fontWeight: FontWeight.w500,\\n ),\\n ),\\n )),\\n );\\n }\\n}\\n
\\nimport \'package:get/get.dart\';\\n\\nclass SurveyController extends GetxController {\\n final RxString textAnswer = \'\'.obs;\\n final RxString singleChoiceAnswer = \'\'.obs;\\n final RxList<String> multiChoiceAnswers = <String>[].obs;\\n final RxList<String> imageUrls = <String>[].obs;\\n final RxBool isGoodService = false.obs;\\n final RxString bedTime = \'\'.obs;\\n final RxInt satisfactionRating = 0.obs;\\n final RxBool isSubmitting = false.obs;\\n final Map<String, String?> _warnings = {};\\n\\n // 文本输入\\n void updateTextAnswer(String value) => textAnswer.value = value;\\n\\n // 单选题\\n void updateSingleChoice(String value) => singleChoiceAnswer.value = value;\\n\\n // 多选题\\n void updateMultiChoice(List<String> values) => multiChoiceAnswers.value = values;\\n\\n // 图片上传\\n void addImage(String url) {\\n if (imageUrls.length < 10) {\\n imageUrls.add(url);\\n }\\n }\\n\\n void removeImage(int index) {\\n if (index >= 0 && index < imageUrls.length) {\\n imageUrls.removeAt(index);\\n }\\n }\\n\\n void updateImages(List<String> images) {\\n imageUrls.clear();\\n imageUrls.addAll(images);\\n }\\n\\n // 服务满意度\\n void updateServiceSatisfaction(bool value) => isGoodService.value = value;\\n\\n // 上床时间\\n void updateBedTime(String value) => bedTime.value = value;\\n\\n // 满意度评分\\n void updateSatisfactionRating(int value) => satisfactionRating.value = value;\\n\\n // 警告消息\\n String? getTip(String questionId) {\\n return _warnings[questionId];\\n }\\n\\n void setTip(String questionId, String? message) {\\n _warnings[questionId] = message;\\n update();\\n }\\n\\n void clearWarningMessage(String questionId) {\\n _warnings.remove(questionId);\\n update();\\n }\\n\\n // 提交问卷\\n Future<void> submitSurvey() async {\\n try {\\n isSubmitting.value = true;\\n\\n // 构建提交数据\\n final Map<String, dynamic> data = {\\n \'textAnswer\': textAnswer.value,\\n \'singleChoiceAnswer\': singleChoiceAnswer.value,\\n \'multiChoiceAnswers\': multiChoiceAnswers,\\n \'imageUrls\': imageUrls,\\n \'isGoodService\': isGoodService.value,\\n \'bedTime\': bedTime.value,\\n \'satisfactionRating\': satisfactionRating.value,\\n };\\n\\n // TODO: 调用API提交数据\\n await Future.delayed(const Duration(seconds: 2)); // 模拟网络请求\\n\\n Get.snackbar(\\n \'提交成功\',\\n \'感谢您的反馈!\',\\n snackPosition: SnackPosition.BOTTOM,\\n );\\n } catch (e) {\\n Get.snackbar(\\n \'提交失败\',\\n \'请稍后重试\',\\n snackPosition: SnackPosition.BOTTOM,\\n );\\n } finally {\\n isSubmitting.value = false;\\n }\\n }\\n\\n // 重置问卷\\n void resetSurvey() {\\n textAnswer.value = \'\';\\n singleChoiceAnswer.value = \'\';\\n multiChoiceAnswers.clear();\\n imageUrls.clear();\\n isGoodService.value = false;\\n bedTime.value = \'\';\\n satisfactionRating.value = 0;\\n _warnings.clear();\\n update();\\n }\\n}\\n
\\nimport \'package:flutter/material.dart\';\\n\\nclass QuestionContainer extends StatelessWidget {\\n final String title;\\n final String? subtitle;\\n final Widget child;\\n final bool isRequired;\\n\\n const QuestionContainer({\\n Key? key,\\n required this.title,\\n this.subtitle,\\n required this.child,\\n this.isRequired = true,\\n }) : super(key: key);\\n\\n @override\\n Widget build(BuildContext context) {\\n return Container(\\n padding: const EdgeInsets.all(16),\\n child: Column(\\n crossAxisAlignment: CrossAxisAlignment.start,\\n children: [\\n Row(\\n crossAxisAlignment: CrossAxisAlignment.start,\\n children: [\\n Expanded(\\n child: Text(\\n title,\\n style: const TextStyle(\\n fontSize: 16,\\n fontWeight: FontWeight.w500,\\n ),\\n ),\\n ),\\n if (isRequired)\\n const Text(\\n \'*\',\\n style: TextStyle(\\n color: Colors.red,\\n fontSize: 16,\\n ),\\n ),\\n ],\\n ),\\n if (subtitle != null) ...[\\n const SizedBox(height: 8),\\n Text(\\n subtitle!,\\n style: TextStyle(\\n fontSize: 14,\\n color: Colors.grey[600],\\n ),\\n ),\\n ],\\n const SizedBox(height: 16),\\n child,\\n ],\\n ),\\n );\\n }\\n}\\n
\\nimport \'package:flutter/material.dart\';\\n\\nclass QuestionText extends StatelessWidget {\\n final String? initialValue;\\n final ValueChanged<String> onChanged;\\n final String? tip;\\n\\n const QuestionText({\\n super.key,\\n this.initialValue,\\n required this.onChanged,\\n this.tip,\\n });\\n\\n @override\\n Widget build(BuildContext context) {\\n return Column(\\n crossAxisAlignment: CrossAxisAlignment.start,\\n children: [\\n TextField(\\n controller: TextEditingController(text: initialValue),\\n onChanged: onChanged,\\n maxLines: 3,\\n decoration: InputDecoration(\\n hintText: \'请输入\',\\n filled: true,\\n fillColor: Colors.grey[50],\\n border: OutlineInputBorder(\\n borderRadius: BorderRadius.circular(4),\\n borderSide: BorderSide.none,\\n ),\\n ),\\n ),\\n if (tip != null)\\n Padding(\\n padding: const EdgeInsets.only(top: 8),\\n child: Row(\\n children: [\\n Icon(\\n Icons.warning_amber_rounded,\\n size: 16,\\n color: Colors.orange[700],\\n ),\\n const SizedBox(width: 4),\\n Expanded(\\n child: Text(\\n tip!,\\n style: TextStyle(\\n fontSize: 12,\\n color: Colors.orange[700],\\n ),\\n ),\\n ),\\n ],\\n ),\\n ),\\n ],\\n );\\n }\\n}\\n
\\nimport \'package:flutter/cupertino.dart\';\\nimport \'package:flutter/material.dart\';\\n\\nclass Choice {\\n final String title;\\n final String? subtitle;\\n\\n const Choice(this.title, [this.subtitle]);\\n}\\n\\nclass QuestionSingleChoice extends StatefulWidget {\\n final List<Choice> choices;\\n final ValueChanged<String>? onChanged;\\n final ValueChanged<bool>? onSelect;\\n final String? initialValue;\\n final String? tip;\\n\\n const QuestionSingleChoice({\\n Key? key,\\n required this.choices,\\n this.onChanged,\\n this.onSelect,\\n this.initialValue,\\n this.tip,\\n }) : super(key: key);\\n\\n @override\\n State<QuestionSingleChoice> createState() => _QuestionSingleChoiceState();\\n}\\n\\nclass _QuestionSingleChoiceState extends State<QuestionSingleChoice> {\\n String? _selectedValue;\\n\\n @override\\n void initState() {\\n super.initState();\\n _selectedValue = widget.initialValue;\\n }\\n\\n @override\\n void didUpdateWidget(QuestionSingleChoice oldWidget) {\\n super.didUpdateWidget(oldWidget);\\n if (widget.initialValue != oldWidget.initialValue) {\\n setState(() {\\n _selectedValue = widget.initialValue;\\n });\\n }\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Column(\\n crossAxisAlignment: CrossAxisAlignment.start,\\n children: [\\n ...widget.choices.map((choice) => _buildChoiceItem(context, choice)).toList(),\\n if (widget.tip != null)\\n Padding(\\n padding: const EdgeInsets.only(top: 8),\\n child: Row(\\n children: [\\n Icon(\\n Icons.warning_amber_rounded,\\n size: 16,\\n color: Colors.orange[700],\\n ),\\n const SizedBox(width: 4),\\n Expanded(\\n child: Text(\\n widget.tip!,\\n style: TextStyle(\\n fontSize: 12,\\n color: Colors.orange[700],\\n ),\\n ),\\n ),\\n ],\\n ),\\n ),\\n ],\\n );\\n }\\n\\n Widget _buildChoiceItem(BuildContext context, Choice choice) {\\n final isSelected = _selectedValue == choice.title;\\n final theme = Theme.of(context);\\n\\n return GestureDetector(\\n onTap: () {\\n setState(() {\\n _selectedValue = choice.title;\\n });\\n widget.onChanged?.call(choice.title);\\n widget.onSelect?.call(true);\\n },\\n child: Container(\\n margin: const EdgeInsets.only(bottom: 8),\\n padding: const EdgeInsets.all(12),\\n decoration: BoxDecoration(\\n color: Colors.grey[50],\\n border: Border.all(\\n color: isSelected ? theme.primaryColor : Colors.transparent,\\n width: isSelected ? 1 : 1,\\n ),\\n borderRadius: BorderRadius.circular(4),\\n ),\\n child: Row(\\n children: [\\n Expanded(\\n child: Text(\\n choice.title,\\n style: TextStyle(\\n fontSize: 16,\\n color: isSelected ? theme.primaryColor : Colors.black87,\\n ),\\n ),\\n ),\\n if (choice.subtitle != null)\\n Text(\\n choice.subtitle!,\\n style: TextStyle(\\n fontSize: 14,\\n color: Colors.grey[600],\\n ),\\n ),\\n const SizedBox(width: 8),\\n Icon(\\n isSelected ? Icons.check_circle : Icons.circle_outlined,\\n color: isSelected ? theme.primaryColor : Colors.grey[400],\\n size: 20,\\n ),\\n ],\\n ),\\n ),\\n );\\n }\\n}\\n
\\nimport \'package:flutter/material.dart\';\\n\\nclass QuestionMultiChoice extends StatefulWidget {\\n final List<String> options;\\n final List<String> initialValues;\\n final ValueChanged<List<String>> onChanged;\\n final String? tip;\\n\\n const QuestionMultiChoice({\\n super.key,\\n required this.options,\\n required this.initialValues,\\n required this.onChanged,\\n this.tip,\\n });\\n\\n @override\\n State<QuestionMultiChoice> createState() => _QuestionMultiChoiceState();\\n}\\n\\nclass _QuestionMultiChoiceState extends State<QuestionMultiChoice> {\\n late List<String> _selectedValues;\\n\\n @override\\n void initState() {\\n super.initState();\\n _selectedValues = List.from(widget.initialValues);\\n }\\n\\n @override\\n void didUpdateWidget(QuestionMultiChoice oldWidget) {\\n super.didUpdateWidget(oldWidget);\\n if (widget.initialValues != oldWidget.initialValues) {\\n setState(() {\\n _selectedValues = List.from(widget.initialValues);\\n });\\n }\\n }\\n\\n void _toggleOption(String option) {\\n setState(() {\\n if (_selectedValues.contains(option)) {\\n _selectedValues.remove(option);\\n } else {\\n _selectedValues.add(option);\\n }\\n widget.onChanged(_selectedValues);\\n });\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Column(\\n crossAxisAlignment: CrossAxisAlignment.start,\\n children: [\\n ...widget.options.map((option) => _buildOptionItem(context, option)).toList(),\\n if (widget.tip != null)\\n Padding(\\n padding: const EdgeInsets.only(top: 8),\\n child: Row(\\n children: [\\n Icon(\\n Icons.warning_amber_rounded,\\n size: 16,\\n color: Colors.orange[700],\\n ),\\n const SizedBox(width: 4),\\n Expanded(\\n child: Text(\\n widget.tip!,\\n style: TextStyle(\\n fontSize: 12,\\n color: Colors.orange[700],\\n ),\\n ),\\n ),\\n ],\\n ),\\n ),\\n ],\\n );\\n }\\n\\n Widget _buildOptionItem(BuildContext context, String option) {\\n final isSelected = _selectedValues.contains(option);\\n final theme = Theme.of(context);\\n\\n return GestureDetector(\\n onTap: () => _toggleOption(option),\\n child: Container(\\n margin: const EdgeInsets.only(bottom: 8),\\n padding: const EdgeInsets.all(12),\\n decoration: BoxDecoration(\\n color: Colors.grey[50],\\n border: Border.all(\\n color: isSelected ? theme.primaryColor : Colors.transparent,\\n width: isSelected ? 1 : 1,\\n ),\\n borderRadius: BorderRadius.circular(8),\\n ),\\n child: Row(\\n children: [\\n Expanded(\\n child: Text(\\n option,\\n style: TextStyle(\\n fontSize: 16,\\n color: isSelected ? theme.primaryColor : Colors.black87,\\n ),\\n ),\\n ),\\n Icon(\\n isSelected ? Icons.check_box : Icons.check_box_outline_blank,\\n color: isSelected ? theme.primaryColor : Colors.grey[400],\\n size: 20,\\n ),\\n ],\\n ),\\n ),\\n );\\n }\\n}\\n
\\nimport \'package:flutter/material.dart\';\\nimport \'package:permission_handler/permission_handler.dart\';\\nimport \'package:wechat_assets_picker/wechat_assets_picker.dart\';\\nimport \'dart:io\';\\nimport \'package:device_info_plus/device_info_plus.dart\';\\n\\nclass QuestionImageUpload extends StatefulWidget {\\n final List<String>? initialImages;\\n final Function(List<String>) onImagesChanged;\\n final String? tip;\\n final int maxCount;\\n final int numOfRow;\\n\\n const QuestionImageUpload({\\n super.key,\\n this.initialImages,\\n required this.onImagesChanged,\\n this.tip,\\n this.maxCount = 10,\\n this.numOfRow = 4,\\n });\\n\\n @override\\n State<QuestionImageUpload> createState() => _QuestionImageUploadState();\\n}\\n\\nclass _QuestionImageUploadState extends State<QuestionImageUpload> {\\n late List<String> _images;\\n\\n @override\\n void initState() {\\n super.initState();\\n _images = widget.initialImages?.toList() ?? [];\\n }\\n\\n @override\\n void didUpdateWidget(QuestionImageUpload oldWidget) {\\n super.didUpdateWidget(oldWidget);\\n if (widget.initialImages != oldWidget.initialImages) {\\n setState(() {\\n _images = widget.initialImages?.toList() ?? [];\\n });\\n }\\n }\\n\\n // 检查并请求权限\\n Future<bool> _checkPermission() async {\\n // 检查平台\\n if (Platform.isIOS) {\\n // iOS 权限检查\\n PermissionStatus photoStatus = await Permission.photos.status;\\n\\n if (!photoStatus.isGranted) {\\n photoStatus = await Permission.photos.request();\\n if (!photoStatus.isGranted) {\\n _showPermissionDeniedDialog(\'相册\');\\n return false;\\n }\\n }\\n\\n // 如果需要相机权限\\n PermissionStatus cameraStatus = await Permission.camera.status;\\n if (!cameraStatus.isGranted) {\\n cameraStatus = await Permission.camera.request();\\n if (!cameraStatus.isGranted) {\\n _showPermissionDeniedDialog(\'相机\');\\n return false;\\n }\\n }\\n\\n return true;\\n } else if (Platform.isAndroid) {\\n // Android 权限检查\\n // 检查 Android 版本\\n if (await _isAndroid13OrAbove()) {\\n // Android 13+ 使用 READ_MEDIA_IMAGES\\n PermissionStatus mediaImagesStatus = await Permission.photos.status;\\n if (!mediaImagesStatus.isGranted) {\\n mediaImagesStatus = await Permission.photos.request();\\n if (!mediaImagesStatus.isGranted) {\\n _showPermissionDeniedDialog(\'相册\');\\n return false;\\n }\\n }\\n } else {\\n // Android 12 及以下使用 READ_EXTERNAL_STORAGE\\n PermissionStatus storageStatus = await Permission.storage.status;\\n if (!storageStatus.isGranted) {\\n storageStatus = await Permission.storage.request();\\n if (!storageStatus.isGranted) {\\n _showPermissionDeniedDialog(\'存储\');\\n return false;\\n }\\n }\\n }\\n\\n // 如果需要相机权限\\n PermissionStatus cameraStatus = await Permission.camera.status;\\n if (!cameraStatus.isGranted) {\\n cameraStatus = await Permission.camera.request();\\n if (!cameraStatus.isGranted) {\\n _showPermissionDeniedDialog(\'相机\');\\n return false;\\n }\\n }\\n\\n return true;\\n }\\n\\n // 其他平台\\n return true;\\n }\\n\\n // 检查是否为 Android 13 或更高版本\\n Future<bool> _isAndroid13OrAbove() async {\\n if (Platform.isAndroid) {\\n final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();\\n final AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;\\n return androidInfo.version.sdkInt >= 33; // Android 13 是 API 33\\n }\\n return false;\\n }\\n\\n // 显示权限被拒绝的对话框\\n void _showPermissionDeniedDialog(String permissionName) {\\n if (!mounted) return;\\n\\n showDialog(\\n context: context,\\n builder: (context) => AlertDialog(\\n title: Text(\'需要$permissionName权限\'),\\n content: Text(\'请在设置中允许应用访问您的$permissionName,以便上传图片\'),\\n actions: [\\n TextButton(\\n onPressed: () => Navigator.pop(context),\\n child: const Text(\'取消\'),\\n ),\\n TextButton(\\n onPressed: () {\\n Navigator.pop(context);\\n openAppSettings();\\n },\\n child: const Text(\'去设置\'),\\n ),\\n ],\\n ),\\n );\\n }\\n\\n // 选择图片\\n Future<void> _pickImage() async {\\n if (_images.length >= widget.maxCount) {\\n ScaffoldMessenger.of(context).showSnackBar(\\n SnackBar(\\n content: Text(\'最多只能上传${widget.maxCount}张图片\'),\\n backgroundColor: Colors.red,\\n ),\\n );\\n return;\\n }\\n\\n // // 检查权限\\n // bool hasPermission = await _checkPermission();\\n // if (!hasPermission) return;\\n\\n // 计算还可以选择的图片数量\\n final int remainingCount = widget.maxCount - _images.length;\\n\\n // 使用wechat_assets_picker选择图片\\n final List<AssetEntity>? result = await AssetPicker.pickAssets(\\n context,\\n pickerConfig: AssetPickerConfig(\\n maxAssets: remainingCount,\\n requestType: RequestType.image,\\n ),\\n );\\n\\n if (result != null && result.isNotEmpty) {\\n // 处理选中的图片\\n for (final AssetEntity asset in result) {\\n final File? file = await asset.file;\\n if (file != null) {\\n setState(() {\\n _images.add(file.path);\\n });\\n }\\n }\\n\\n // 通知父组件图片变化\\n widget.onImagesChanged(_images);\\n }\\n }\\n\\n void _removeImage(int index) {\\n setState(() {\\n _images.removeAt(index);\\n });\\n\\n // 通知父组件图片变化\\n widget.onImagesChanged(_images);\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Column(\\n crossAxisAlignment: CrossAxisAlignment.start,\\n children: [\\n LayoutBuilder(\\n builder: (context, constraints) {\\n // 计算图片项的宽度,基于每行显示的图片数量和可用宽度\\n final availableWidth = constraints.maxWidth;\\n final spacing = 8.0 * (widget.numOfRow - 1); // 图片之间的间距总和\\n final itemWidth = (availableWidth - spacing) / widget.numOfRow;\\n\\n return Wrap(\\n spacing: 8,\\n runSpacing: 8,\\n children: [\\n ..._images.map((path) => _buildImageItem(context, path, itemWidth)).toList(),\\n if (_images.length < widget.maxCount) _buildAddButton(context, itemWidth),\\n ],\\n );\\n },\\n ),\\n if (widget.tip != null)\\n Padding(\\n padding: const EdgeInsets.only(top: 8),\\n child: Row(\\n children: [\\n Icon(\\n Icons.warning_amber_rounded,\\n size: 16,\\n color: Colors.orange[700],\\n ),\\n const SizedBox(width: 4),\\n Expanded(\\n child: Text(\\n widget.tip!,\\n style: TextStyle(\\n fontSize: 12,\\n color: Colors.orange[700],\\n ),\\n ),\\n ),\\n ],\\n ),\\n ),\\n ],\\n );\\n }\\n\\n Widget _buildImageItem(BuildContext context, String path, double width) {\\n final index = _images.indexOf(path);\\n return Stack(\\n children: [\\n Container(\\n width: width.truncateToDouble(),\\n height: width.truncateToDouble(),\\n decoration: BoxDecoration(\\n border: Border.all(color: Colors.grey[300]!),\\n borderRadius: BorderRadius.circular(8),\\n image: DecorationImage(\\n image: path.startsWith(\'http\') ? NetworkImage(path) as ImageProvider : FileImage(File(path)),\\n fit: BoxFit.cover,\\n ),\\n ),\\n ),\\n Positioned(\\n top: 4,\\n right: 4,\\n child: GestureDetector(\\n onTap: () => _removeImage(index),\\n child: Container(\\n padding: const EdgeInsets.all(2),\\n decoration: BoxDecoration(\\n color: Colors.black.withOpacity(0.5),\\n shape: BoxShape.circle,\\n ),\\n child: const Icon(\\n Icons.close,\\n size: 16,\\n color: Colors.white,\\n ),\\n ),\\n ),\\n ),\\n ],\\n );\\n }\\n\\n Widget _buildAddButton(BuildContext context, double width) {\\n return GestureDetector(\\n onTap: _pickImage,\\n child: Container(\\n width: width,\\n height: width,\\n decoration: BoxDecoration(\\n border: Border.all(color: Colors.grey[300]!),\\n borderRadius: BorderRadius.circular(8),\\n ),\\n child: Icon(\\n Icons.add_photo_alternate_outlined,\\n color: Colors.grey[400],\\n size: 32,\\n ),\\n ),\\n );\\n }\\n}\\n
\\nimport \'package:flutter/material.dart\';\\n\\nclass RatingOption {\\n final String text;\\n final int value;\\n\\n const RatingOption(this.text, this.value);\\n}\\n\\nclass QuestionRating extends StatefulWidget {\\n final List<RatingOption> options;\\n final int? initialValue;\\n final ValueChanged<int>? onChanged;\\n final String? tip;\\n\\n const QuestionRating({\\n super.key,\\n required this.options,\\n this.initialValue,\\n this.onChanged,\\n this.tip,\\n });\\n\\n @override\\n State<QuestionRating> createState() => _QuestionRatingState();\\n}\\n\\nclass _QuestionRatingState extends State<QuestionRating> {\\n int? _selectedValue;\\n\\n @override\\n void initState() {\\n super.initState();\\n _selectedValue = widget.initialValue;\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Row(\\n mainAxisAlignment: MainAxisAlignment.spaceBetween,\\n children: widget.options\\n .map((option) => _buildRatingOption(option))\\n .toList(),\\n );\\n }\\n\\n Widget _buildRatingOption(RatingOption option) {\\n final isSelected = _selectedValue == option.value;\\n \\n return GestureDetector(\\n onTap: () {\\n setState(() {\\n _selectedValue = option.value;\\n });\\n widget.onChanged?.call(option.value);\\n },\\n child: Column(\\n children: [\\n Icon(\\n Icons.star,\\n color: isSelected ? Colors.amber : Colors.grey[300],\\n size: 32,\\n ),\\n const SizedBox(height: 4),\\n Text(\\n option.text,\\n style: TextStyle(\\n fontSize: 12,\\n color: isSelected ? Colors.amber : Colors.grey[600],\\n ),\\n ),\\n if (widget.tip != null)\\n Padding(\\n padding: const EdgeInsets.only(top: 8),\\n child: Row(\\n children: [\\n Icon(\\n Icons.warning_amber_rounded,\\n size: 16,\\n color: Colors.orange[700],\\n ),\\n const SizedBox(width: 4),\\n Expanded(\\n child: Text(\\n widget.tip!,\\n style: TextStyle(\\n fontSize: 12,\\n color: Colors.orange[700],\\n ),\\n ),\\n ),\\n ],\\n ),\\n ),\\n ],\\n ),\\n );\\n }\\n} \\n
\\n直接说结论:\\n细节和 UI 差很多,还原度只能达到 50%左右。\\n如果对UI要求不高的自研公司,生成差不多的就可以接受(独立开发者/独立创业者
),那确实是超神器。但如果是追求 UI 100%还原度的公司,只能说见仁见智,任重而道远。
\\n\\n疑问:网上看到一段提示语生成产品原型,效果确实不错。但是生成 flutter 页面代码就差挺多。难道是 web 和 flutter 组件的实现差异?
\\n
1、细节虽然差但是大体框架能用,你可以等它修改差不多时,自己调整细节。这块感觉实际开发中初期提效 20% - 30% 问题不大。
\\n2、你可以让它实现一些复杂组件封装或者实现,比如雷雨、雪花、云动等效果组件实现。以前过于复杂,不敢想的特效和组件,现在和随便想,然后尽可能详细的提示语,让AI帮忙生成。
\\n1、即使你将 UI 截图完全喂给 Claude-3.7,让它自己生成提示语,然后根据提示语生成对应的代码,也无法百分百完全还原。寻找优化方法中
2、Flutter SDK 不支持的不规则组件,很容易直接就不生成,或者随意生成一个一看就不能用的。原本期望是绘制成组件显示出来,可惜不是。
3、生成的代码时你需要告诉它遵守一些规则,比如高内聚低耦合、Flutter最佳实现等,否则代码是平铺的。需要寻找让 AI 学习自己编码风格的解决办法
4、生成速度比较慢,生成之后代码可能和之前的代码有参数匹配错误问题,它不会自己修复,需要你告诉他修复错误。
\\n5、如果感觉效果差不多要及时用 git 进行保存,二次重新生成的代码可能不如第一版,开发人员要注意。
\\n实际开发中 intel i9 + 32g内存配置的 mac book 运行 Cursor 时不时就卡顿,不明所以,有知道原因的朋友可以留言。
\\n\\n","description":"一、需求来源 最新代码圈有被 claude-3.7 刷屏的情况出现,感觉 AI 代码指导编程的临界点悄然已至。有点类似 Swift4.2、Flutter2.2版本的推出,搞app开发的朋友懂这些关键版本节点的重要性,它代表了从一小部分极客玩家到大众参与的裂变点。如果有还在等待AI辅助编程的朋友,那么可以加入了。最近花了两周多时间体验了 Cusor + claude-3.7 AI编程实战效果。\\n\\n二、使用示例\\n所有代码都是通过多次谈话自动生成!\\n\\n这是一个简单的健康调查问卷生成效果:\\n\\n.\\n├── lib\\n│ ├── controllers\\n│ │ …","guid":"https://juejin.cn/post/7481238697552674850","author":"SoaringHeart","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-14T04:01:19.932Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e6b8afcd7aa64a34ab1c07740f438d71~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgU29hcmluZ0hlYXJ0:q75.awebp?rk3s=f64ab15b&x-expires=1742530520&x-signature=lhmv0euTLFBF%2B5qh6EV0ccucfSI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5c2cf298084c4376ae1252521c2a4b79~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgU29hcmluZ0hlYXJ0:q75.awebp?rk3s=f64ab15b&x-expires=1742530520&x-signature=6kTscojR2m4Xx1D1A6fVTUp6YDU%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter截图保存并加水印","url":"https://juejin.cn/post/7481183588781932582","content":"利用RepaintBoundary
组件截屏,RepaintBoundary
能够截图是因为它创建了一个独立的绘制层,Flutter 可以捕获该层的内容并转换为图像。使用时,将屏幕上需要截取的内容用RepaintBoundary
组件包裹,并传入全局变量GlobalKey
。
GlobalKey pictureKey = GlobalKey();\\n\\nRepaintBoundary(\\n key: pictureKey,\\n child: Container(child:...)\\n)\\n
\\n实现截图的步骤
\\nGlobalKey
获取 RepaintBoundary
的引用。RepaintBoundary
的 toImage
方法,Flutter 会将该边界内的内容渲染为 ui.Image
对象。ui.Image
转换为常见的图像格式。path_provider
插件获取app目录并存储图片。import \'dart:ui\' as ui;\\nimport \'dart:typed_data\';\\nimport \'package:flutter/rendering.dart\';\\nimport \'package:path_provider/path_provider.dart\';\\n\\nFuture<void> _captureImage() async {\\n RenderObject? boundary = pictureKey.currentContext?.findRenderObject();\\n ui.Image image = await boundary.toImage();\\n ByteData byteData = await image.toByteData(format: ui.ImageByteFormat.png);\\n Uint8List pngBytes = byteData.buffer.asUint8List();\\n Directory directory = await getTemporaryDirectory();\\n String imagePath =\\n \'${directory.path}/${DateTime.now().microsecondsSinceEpoch}.png\';\\n await File(imagePath).writeAsBytes(pngBytes);\\n}\\n
\\nwebview
,使用RepaintBoundary
是无法截图的,可以使用webview
自带的截图方法,如FLutter中经常使用的flutter_inappwebview
插件,可以使用webController
的takeScreenshot
方法获取截图。Uint8List? uint8List = await webController?.takeScreenshot();\\n
\\n上面已经通过RepaintBoundary
拿到了ui.Image
,可以利用ui.Image
来创建Canvas
画布,利用画布添加水印,可以添加图片、文字,最后,通过ui.Picture
的toImage
方法获取带水印的图片。
Future<void> _captureImage() async {\\n RenderObject? boundary = pictureKey.currentContext?.findRenderObject();\\n ui.Image image = await boundary.toImage();\\n // 创建一个 Canvas 来绘制水印\\n ui.PictureRecorder recorder = ui.PictureRecorder();\\n Canvas canvas = Canvas(recorder);\\n // 将原始图像绘制在canvas上\\n canvas.drawImage(image, Offset.zero, Paint());\\n // 添加图片水印\\n // 从assets中读取水印图片\\n ByteData waterData =\\n await rootBundle.load(\'assets/images/water_mark.png\');\\n ui.Image waterImage =\\n await decodeImageFromList(waterData.buffer.asUint8List());\\n int width = image.width;\\n int height = image.height;\\n canvas.drawImageNine(\\n waterImage,\\n // 要绘制的水印图片区域\\n ui.Rect.fromLTWH(\\n 0, 0, waterImage.width.toDouble(), waterImage.height.toDouble()),\\n // 绘制水印图片的目标区域,如:以原始图片中心点为中心,绘制宽100高100的水印图片\\n ui.Rect.fromCenter(\\n center: Offset(width / 2, height / 2),\\n width: 100,\\n height: 100),\\n Paint());\\n ui.Picture picture = recorder.endRecording();\\n // 获取带水印的图片\\n ui.Image newImg = await picture.toImage(width, height);\\n ...\\n}\\n\\n
\\n如果是整张水印铺满原始图片的情况,直接指定水印图片宽高容易造成水印图片的拉伸变形,所以最好是保持水印图片的宽高比。可以通过计算原始图片的宽高比和水印图片的宽高比来进行判断:
\\n如下图,把整张水印图片铺在原始图片上,为了避免水印图片拉伸变形,需要保持水印图片的宽高比。
\\n// 原始图片宽高比\\ndouble imgRatio = width / height;\\n// 水印图片宽高比\\ndouble waterImgRatio = waterImage.width / waterImage.height;\\ndouble dstWidth = width.toDouble();\\ndouble dstHeight = height.toDouble();\\nif (imgRatio > waterImgRatio) {\\n // 如果原始图片更宽,以原始图片的高为水印图片高,按水印图片宽高比计算出对应的水印图片宽度\\n dstWidth = waterImgRatio * dstHeight;\\n} else {\\n // 反之,水印图片更宽,以原始图片的宽度为水印图片宽,按水印图片宽高比计算出对应的水印图片高度\\n dstHeight = dstWidth / waterImgRatio;\\n}\\ncanvas.drawImageNine(\\n waterImage,\\n ui.Rect.fromLTWH(\\n 0, 0, waterImage.width.toDouble(), waterImage.height.toDouble()),\\n ui.Rect.fromCenter(\\n center: Offset(width / 2, height / 2),\\n width: dstWidth,\\n height: dstHeight),\\n Paint());\\n
\\n除了添加图片,还可以添加文字水印:
\\nfinal textPainter = TextPainter(\\n text: TextSpan(\\n text: \'水印\',\\n style: TextStyle(\\n color: Colors.white.withOpacity(0.5),\\n fontSize: 40,\\n fontWeight: FontWeight.bold,\\n ),\\n ),\\n textDirection: TextDirection.ltr,\\n );\\ntextPainter.layout();\\n// 在原始图片中心绘制水印文字\\ntextPainter.paint(canvas, Offset(\\n (width - textPainter.width) / 2,\\n (height - textPainter.height) / 2)\\n);\\n
\\n上文已经获取到了带水印的图片,一般用于保存或分享,可以借助image_gallery_saver
插件来保存图片到相册。
ByteData? byteData = await newImg.toByteData(format: ui.ImageByteFormat.png);\\nUint8List? pngUint8List = byteData?.buffer.asUint8List();\\nsaveToAlbum(pngUint8List);\\n\\nFuture<String> saveToAlbum(\\n Uint8List imageBytes, {\\n int quality = 100,\\n String? name,\\n}) async {\\n dynamic result = await ImageGallerySaver.saveImage(\\n imageBytes,\\n quality: quality,\\n name: name,\\n isReturnImagePathOfIOS: true,\\n );\\n if (result is! Map) {\\n return Future.value(\'\');\\n }\\n return Future.value(result.[\'filePath\']??\'\'));\\n}\\n
","description":"一、屏幕截图 利用RepaintBoundary组件截屏,RepaintBoundary 能够截图是因为它创建了一个独立的绘制层,Flutter 可以捕获该层的内容并转换为图像。使用时,将屏幕上需要截取的内容用RepaintBoundary组件包裹,并传入全局变量GlobalKey。\\n\\nGlobalKey pictureKey = GlobalKey();\\n\\nRepaintBoundary(\\n key: pictureKey,\\n child: Container(child:...)\\n)\\n\\n\\n实现截图的步骤\\n\\n通过 GlobalKey 获取 Re…","guid":"https://juejin.cn/post/7481183588781932582","author":"风二中","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-14T03:21:22.521Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/bf336dc5d48e4d3399fbad9aa6372a4b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6aOO5LqM5Lit:q75.awebp?rk3s=f64ab15b&x-expires=1742527281&x-signature=cK8rJMJKwJmB0%2B5SD9eEhKJm4NQ%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"系统化掌握Flutter开发之主题(Theme)(一):筑基之旅","url":"https://juejin.cn/post/7480952941508870179","content":"在移动应用开发中,界面风格的统一性如同人的衣装,直接影响用户体验
和应用的专业度
。你是否遇到过这样的困扰:
逐个修改
?深色模式适配
需要重写所有样式?这就是Flutter
主题(Theme
)系统要解决的核心问题。通过主题系统,开发者可以像搭积木一样管理应用样式,实现\\"一次定义,全局生效\\"
的魔法效果。合理使用主题系统能提升界面开发效率,同时降低的样式维护成本。
本文将带你从设计哲学到实战应用,系统化构建主题开发能力,让你的应用轻松实现专业级视觉体验。
\\n操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\n定义本质:
\\nTheme
是通过InheritedWidget
实现的上下文样式管理系统,采用树状继承结构,通过ThemeData
对象集中管理视觉属性。
当调用Theme.of(context)
时:
Widget
树向上查找最近的Theme
组件。差异
。ThemeData
副本。核心价值:
\\n按钮
/文字
/卡片
等组件的默认样式。避免重复
定义相同样式属性。iOS/Android
)的设计特性。// 基础定义示例\\nMaterialApp(\\n theme: ThemeData(\\n primaryColor: Colors.blue, // 主色\\n fontFamily: \'Roboto\', // 全局字体\\n ),\\n darkTheme: ThemeData.dark(), // 暗色主题\\n home: MyApp(),\\n);\\n\\n// 主题系统对商业指标的影响(商业价值)\\nThemeData(\\n primaryColor: BrandColors.primary, // 强化品牌认知\\n textTheme: BrandTextStyles.theme, // 提升视觉一致性\\n buttonTheme: ConversionButtonStyle // 优化转化率按钮\\n)\\n
\\n继承优先级模型:
\\nMaterialApp\\n└── Theme(data: globalTheme) // 全局主题\\n └── PageWidget\\n └── Theme(data: pageTheme) // 页面级覆盖\\n └── CustomWidget\\n └── Theme(data: localTheme) // 组件级覆盖\\n └── Text(\\"示例\\") // 使用Theme.of(context)\\n
\\ncopyWith
方法进行属性级合并。颜色系统工作原理:
\\nThemeData(\\n colorScheme: ColorScheme(\\n primary: Colors.blue,\\n onPrimary: Colors.white, // 主色上的文本颜色\\n secondary: Colors.orange, // 强调色\\n error: Colors.red, // 错误状态色\\n background: Colors.grey[50], // 背景色\\n ),\\n)\\n
\\n核心功能与特征:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n功能特性 | 说明 |
---|---|
上下文继承 | 通过Theme.of(context) 获取最近的父级主题 |
局部覆盖 | 使用Theme 组件包裹子树实现局部样式覆盖 |
自动颜色计算 | primaryColor 会自动生成配套的accentColor 等颜色 |
设计规范支持 | 内置Material Design 2/3 规范,支持useMaterial3 开关 |
典型适用场景:
\\nVI色值
/字体
等注入主题系统。A/B
测试:快速切换不同视觉方案
进行用户测试。// 错误示例:硬编码样式\\nText(\'Hello\', style: TextStyle(color: Colors.red))\\n\\n// 正确示例:主题驱动\\nText(\'Hello\', style: Theme.of(context).textTheme.titleLarge)\\n
\\n四大原则:
\\nThemeData
定义。UI
组件只关注业务逻辑
,不包含样式代码。context
自动获取有效主题。屏幕尺寸
)自动适配。colorScheme
:颜色系统ThemeData(\\n // 现代颜色系统(优先使用)\\n colorScheme: ColorScheme(\\n brightness: Brightness.light, // 亮度模式\\n primary: Colors.blue, // 主品牌色\\n onPrimary: Colors.white, // 主色上的内容色\\n secondary: Colors.orange, // 强调色\\n onSecondary: Colors.black, // 强调色上的内容色\\n error: Colors.red, // 错误状态色\\n background: Colors.grey[50], // 背景色\\n surface: Colors.white, // 表面色(卡片、表单)\\n onSurface: Colors.black87, // 表面上的内容色\\n ),\\n\\n // 传统颜色配置(兼容旧版)\\n primaryColor: Colors.blue, // 被colorScheme.primary取代\\n accentColor: Colors.orange, // 被colorScheme.secondary取代\\n canvasColor: Colors.grey[100], // 画布背景色\\n)\\n
\\n技术要点:
\\ncolorScheme
替代分散的颜色参数。onXxx
颜色表示在对应底色上的内容颜色。ColorScheme.fromSeed()
生成协调的色板。brightness
值。textTheme
:文字系统ThemeData(\\n textTheme: TextTheme(\\n displayLarge: TextStyle(\\n fontSize: 57.0,\\n fontWeight: FontWeight.w400,\\n letterSpacing: -0.25,\\n height: 1.12,\\n ),\\n bodyLarge: TextStyle(\\n fontSize: 16.0,\\n fontWeight: FontWeight.w400,\\n letterSpacing: 0.5,\\n height: 1.5,\\n ),\\n // 完整层级见Material Design规范\\n ),\\n \\n // 东亚文字特殊处理\\n typography: Typography.material2021(\\n englishLike: Typography.blackMountainView,\\n dense: Typography.dense2021,\\n tall: Typography.tall2021,\\n ),\\n \\n // 字体家族配置\\n fontFamily: \'Roboto\',\\n fontFamilyFallback: [\'NotoSansSC\'], // 字体回退链\\n)\\n
\\n实践规范:
\\nTextTheme
定义6
个显示样式 + 6
个正文样式。DefaultTextStyle
组件实现响应式文字。fontFamilyFallback
。MediaQuery.textScaleFactor
处理系统字号设置。shapeScheme
:形状系统ThemeData(\\n shapeScheme: ShapeScheme(\\n // 现代形状系统(Material 3)\\n small: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),\\n medium: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),\\n large: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),\\n ),\\n \\n // 传统形状配置\\n cardTheme: CardTheme(\\n shape: RoundedRectangleBorder(\\n borderRadius: BorderRadius.circular(12.0),\\n side: BorderSide(color: Colors.grey[300]!),\\n ),\\n ),\\n buttonTheme: ButtonThemeData(\\n shape: StadiumBorder(), // 圆形边框\\n ),\\n)\\n
\\n设计原则:
\\nsmall/medium/large
三级圆角半径。4-8px
圆角。8-16px
圆角。StadiumBorder
)。ThemeData(\\n // 按钮系统\\n elevatedButtonTheme: ElevatedButtonThemeData(\\n style: ElevatedButton.styleFrom(\\n padding: EdgeInsets.symmetric(vertical: 14, horizontal: 24),\\n textStyle: TextStyle(fontWeight: FontWeight.w600),\\n ),\\n ),\\n \\n // 输入框系统\\n inputDecorationTheme: InputDecorationTheme(\\n border: OutlineInputBorder(),\\n contentPadding: EdgeInsets.all(16),\\n errorStyle: TextStyle(color: Colors.red[700]),\\n ),\\n \\n // 导航系统\\n navigationBarTheme: NavigationBarThemeData(\\n height: 64,\\n indicatorColor: Colors.blue[100],\\n labelTextStyle: MaterialStateProperty.resolveWith((states) {\\n if (states.contains(MaterialState.selected)) {\\n return TextStyle(color: Colors.blue);\\n }\\n return TextStyle(color: Colors.grey);\\n }),\\n ),\\n)\\n
\\n组件化规范:
\\nXxxThemeData
配置特定组件样式。styleFrom
方法创建样式。MaterialStateProperty
。56px
)。spacing
:间距系统ThemeData(\\n // 间距基准单位\\n spacing: SpacingSystem(\\n baseUnit: 8.0, // 8px网格系统\\n multipliers: {\\n \'xs\': 0.5, // 4px\\n \'sm\': 1.0, // 8px\\n \'md\': 2.0, // 16px\\n \'lg\': 3.0, // 24px\\n },\\n ),\\n \\n // 组件间距继承\\n cardTheme: CardTheme(\\n margin: EdgeInsets.all(Spacing.md), // 16px\\n ),\\n listTileTheme: ListTileThemeData(\\n contentPadding: EdgeInsets.symmetric(\\n horizontal: Spacing.lg, // 24px\\n ),\\n ),\\n)\\n
\\n原子化设计:
\\n8px
基准网格系统。xs=4px
, sm=8px
, md=16px
)。统一管理
。EdgeInsets.all(16)
)。ThemeData(\\n pageTransitionsTheme: PageTransitionsTheme(\\n builders: {\\n TargetPlatform.android: ZoomPageTransitionsBuilder(),\\n TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),\\n },\\n ),\\n transitionsTheme: TransitionsTheme(\\n routeTransitionDuration: Duration(milliseconds: 300),\\n fadeInDuration: Duration(milliseconds: 200),\\n sliderTransition: _CustomSliderTransition(),\\n ),\\n)\\n
\\n性能优化:
\\n页面切换时长
控制在300-400ms
。淡入+缩放
组合。Rive/Lottie
集成。// 自定义主题扩展\\nclass CompanyTheme extends ThemeExtension<CompanyTheme> {\\n final Color brandColor;\\n final Gradient specialGradient;\\n\\n CompanyTheme({required this.brandColor, required this.specialGradient});\\n\\n @override\\n ThemeExtension<CompanyTheme> copyWith() => /*...*/;\\n \\n @override\\n ThemeExtension<CompanyTheme> lerp() => /*...*/;\\n}\\n\\n// 使用扩展\\nThemeData(\\n extensions: <ThemeExtension>[\\n CompanyTheme(\\n brandColor: Colors.purple,\\n specialGradient: LinearGradient(...),\\n )\\n ],\\n)\\n\\n// 获取扩展\\nfinal companyTheme = Theme.of(context).extension<CompanyTheme>()!;\\n
\\n企业级实践:
\\n品牌特殊样式
。避免污染
核心ThemeData
。copyWith/lerp
方法。1、分层配置策略:
\\n// 基础层:颜色/字体/形状\\nThemeData baseTheme = ThemeData(\\n colorScheme: ...,\\n textTheme: ...,\\n);\\n\\n// 组件层:按钮/输入框/卡片\\nThemeData componentTheme = baseTheme.copyWith(\\n elevatedButtonTheme: ...,\\n inputDecorationTheme: ...,\\n);\\n\\n// 业务层:品牌扩展\\nThemeData finalTheme = componentTheme.copyWith(\\n extensions: [CompanyTheme(...)],\\n);\\n
\\n2、动态主题切换架构:
\\n// 状态管理(示例使用Riverpod)\\nfinal themeProvider = StateNotifierProvider<ThemeNotifier, ThemeData>((ref) {\\n return ThemeNotifier();\\n});\\n\\nclass ThemeNotifier extends StateNotifier<ThemeData> {\\n ThemeNotifier() : super(_buildLightTheme());\\n\\n void toggleDarkMode() {\\n state = state.brightness == Brightness.light\\n ? _buildDarkTheme()\\n : _buildLightTheme();\\n }\\n}\\n
\\n3、设计走查工具:
\\n// 主题验证器\\nvoid validateTheme(ThemeData theme) {\\n assert(theme.colorScheme.primary != null);\\n assert(theme.textTheme.bodyLarge?.fontSize != null);\\n // 添加更多验证规则...\\n}\\n
\\nimport \'package:flutter/material.dart\';\\n\\nvoid main() => runApp(ThemeDemoApp());\\n\\nclass ThemeDemoApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n title: \'主题系统演示\',\\n // 全局亮色主题\\n theme: ThemeData(\\n useMaterial3: true,\\n colorScheme: ColorScheme.fromSeed(\\n seedColor: Colors.deepPurple,\\n brightness: Brightness.light,\\n ),\\n textTheme: const TextTheme(\\n headlineMedium: TextStyle(\\n fontSize: 24,\\n fontWeight: FontWeight.bold,\\n color: Colors.black87,\\n ),\\n bodyLarge: TextStyle(\\n fontSize: 16,\\n height: 1.5,\\n color: Colors.grey,\\n ),\\n ),\\n elevatedButtonTheme: ElevatedButtonThemeData(\\n style: ElevatedButton.styleFrom(\\n padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),\\n shape: RoundedRectangleBorder(\\n borderRadius: BorderRadius.circular(8),\\n ),\\n ),\\n ),\\n ),\\n home: ThemeDemoPage(),\\n );\\n }\\n}\\n\\nclass ThemeDemoPage extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\'主题系统演示\')),\\n body: Center(\\n child: Column(\\n mainAxisAlignment: MainAxisAlignment.center,\\n children: [\\n // 自动继承主题样式\\n Text(\'主标题\', style: Theme.of(context).textTheme.headlineMedium),\\n SizedBox(height: 20),\\n Text(\\n \'这是一段说明文字,演示文本主题的自动应用效果\',\\n style: Theme.of(context).textTheme.bodyLarge,\\n ),\\n SizedBox(height: 30),\\n ElevatedButton(\\n onPressed: () {},\\n child: Text(\'主题按钮\'),\\n ),\\n // 局部主题覆盖\\n Theme(\\n data: Theme.of(context).copyWith(\\n elevatedButtonTheme: ElevatedButtonThemeData(\\n style: ElevatedButton.styleFrom(\\n backgroundColor: Colors.red,\\n ),\\n ),\\n ),\\n child: ElevatedButton(\\n onPressed: () {},\\n child: Text(\'局部修改的按钮\'),\\n ),\\n ),\\n ],\\n ),\\n ),\\n );\\n }\\n}\\n
\\n最佳实践:
\\n全局主题
→ 局部Theme
覆盖 → 组件style
参数。Theme.of(context).colorScheme.primary
而非直接颜色值。textTheme
定义层级式文字规范。darkTheme
属性配置,不要手动判断亮度。常见错误:
\\n// 错误1:忽略context层级\\nTheme.of(context) // 必须确保在MaterialApp子树中\\n\\n// 错误2:过度局部覆盖\\n// 应避免超过3层局部主题嵌套\\n\\n// 错误3:硬编码尺寸\\npadding: EdgeInsets.all(16) // 应使用ThemeData的spacing参数统一管理\\n
\\n对比维度 | Theme系统 | 全局变量 | 组件style参数 |
---|---|---|---|
作用域 | 上下文继承 | 全局有效 | 仅影响当前组件 |
维护成本 | 低(集中管理) | 中(需手动同步) | 高(分散在各组件) |
动态切换 | 支持 | 需重启应用 | 不支持 |
适用场景 | 全应用样式规范 | 常量值(如API 地址) | 个别组件的特殊样式 |
需求描述:\\n开发一个具有完整设计系统的新闻类应用,要求:
\\n主色
、辅助色
、语义色
,支持深色模式。标题
、正文
、辅助文字
样式。圆角
、按钮形状
、卡片阴影
。主要按钮
、次要按钮
、文字按钮
样式。图标
和标签
样式。AppBar
统一背景色
和标题
样式。全局边距
、内边距
标准。import \'package:flutter/material.dart\';\\nimport \'package:flutter_demo/theme/theme_utils.dart\';\\nimport \'main_page.dart\';\\n\\nclass NewsApp extends StatefulWidget {\\n const NewsApp({super.key});\\n\\n @override\\n State<NewsApp> createState() => _NewsAppState();\\n}\\n\\nclass _NewsAppState extends State<NewsApp> {\\n ThemeMode _themeMode = ThemeMode.light;\\n final ThemeData lightTheme = ThemeUtils.lightTheme();\\n final ThemeData darkTheme = ThemeUtils.darkTheme();\\n\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n theme: lightTheme,\\n darkTheme: darkTheme,\\n themeMode: _themeMode,\\n home: MainPage(\\n onThemeChanged: (isDark) {\\n setState(() {\\n _themeMode = isDark ? ThemeMode.dark : ThemeMode.light;\\n });\\n },\\n ),\\n );\\n }\\n}\\n
\\nimport \'package:flutter/material.dart\';\\nimport \'package:flutter_demo/theme/tag_theme_extension.dart\';\\n\\n///主题系统工具类\\nclass ThemeUtils {\\n /// 默认浅色模式系统\\n static ThemeData lightTheme() => ThemeData(\\n primaryColor: Colors.blueGrey[800],\\n scaffoldBackgroundColor: Colors.grey[50],\\n colorScheme: colorScheme(),\\n textTheme: textTheme(),\\n cardTheme: cardTheme(),\\n buttonTheme: buttonThemeData(),\\n elevatedButtonTheme: elevatedButtonThemeData(),\\n textButtonTheme: textButtonThemeData(),\\n outlinedButtonTheme: outlinedButtonThemeData(),\\n bottomNavigationBarTheme: bottomNavigationBarThemeData(),\\n appBarTheme: appBarTheme(),\\n extensions: [tagThemeExtension()],\\n );\\n\\n /// 深色色模式系统\\n static ThemeData darkTheme() => ThemeData.dark().copyWith(\\n primaryColor: Colors.blueGrey[300],\\n scaffoldBackgroundColor: Colors.grey[900]!,\\n colorScheme: colorSchemeDark(),\\n textTheme: textThemeDark(),\\n elevatedButtonTheme: elevatedButtonThemeDataDark(),\\n textButtonTheme: textButtonThemeDataDark(),\\n outlinedButtonTheme: outlinedButtonThemeDataDark(),\\n bottomNavigationBarTheme: bottomNavigationBarThemeDataDark(),\\n appBarTheme: appBarThemeDark(),\\n extensions: [tagThemeExtensionDart()],\\n );\\n\\n static TagThemeExtension tagThemeExtension() {\\n return TagThemeExtension(\\n hotTagStyle: TextStyle(\\n color: Colors.white,\\n fontSize: 12,\\n fontWeight: FontWeight.w500,\\n ),\\n hotTagBackground: Colors.red,\\n tagRadius: 4,\\n );\\n }\\n\\n static AppBarTheme appBarTheme() {\\n return AppBarTheme(\\n backgroundColor: Colors.white,\\n elevation: 1,\\n titleTextStyle: TextStyle(\\n color: Colors.black87,\\n fontSize: 20,\\n fontWeight: FontWeight.w600,\\n ),\\n iconTheme: IconThemeData(color: Colors.blueGrey[800]),\\n );\\n }\\n\\n static BottomNavigationBarThemeData bottomNavigationBarThemeData() {\\n return BottomNavigationBarThemeData(\\n selectedItemColor: Colors.blueGrey[800],\\n unselectedItemColor: Colors.grey[600],\\n showUnselectedLabels: true,\\n type: BottomNavigationBarType.fixed,\\n );\\n }\\n\\n static OutlinedButtonThemeData outlinedButtonThemeData() {\\n return OutlinedButtonThemeData(\\n style: OutlinedButton.styleFrom(\\n foregroundColor: Colors.blueGrey[800],\\n side: BorderSide(color: Colors.blueGrey[800]!),\\n ),\\n );\\n }\\n\\n static TextButtonThemeData textButtonThemeData() {\\n return TextButtonThemeData(\\n style: TextButton.styleFrom(\\n foregroundColor: Colors.blueGrey[800],\\n ),\\n );\\n }\\n\\n static ElevatedButtonThemeData elevatedButtonThemeData() {\\n return ElevatedButtonThemeData(\\n style: ElevatedButton.styleFrom(\\n padding: EdgeInsets.symmetric(vertical: 12, horizontal: 24),\\n textStyle: TextStyle(fontWeight: FontWeight.w600),\\n ),\\n );\\n }\\n\\n static ButtonThemeData buttonThemeData() {\\n return ButtonThemeData(\\n shape: RoundedRectangleBorder(\\n borderRadius: BorderRadius.circular(8),\\n ),\\n );\\n }\\n\\n static CardTheme cardTheme() {\\n return CardTheme(\\n elevation: 2,\\n shape: RoundedRectangleBorder(\\n borderRadius: BorderRadius.circular(8),\\n ),\\n );\\n }\\n\\n static TextTheme textTheme() {\\n return TextTheme(\\n displayLarge: TextStyle(\\n fontSize: 24,\\n fontWeight: FontWeight.bold,\\n color: Colors.black87,\\n ),\\n bodyLarge: TextStyle(\\n fontSize: 16,\\n color: Colors.black87,\\n height: 1.5,\\n ),\\n labelSmall: TextStyle(\\n fontSize: 12,\\n color: Colors.grey[600],\\n ),\\n );\\n }\\n\\n static ColorScheme colorScheme() {\\n return ColorScheme.light(\\n primary: Colors.blueGrey[800]!,\\n secondary: Colors.amber[700]!,\\n surface: Colors.white,\\n error: Colors.red[700]!,\\n );\\n }\\n\\n static TagThemeExtension tagThemeExtensionDart() {\\n return TagThemeExtension(\\n hotTagStyle: TextStyle(\\n color: Colors.white,\\n fontSize: 12,\\n fontWeight: FontWeight.w500,\\n ),\\n hotTagBackground: Colors.redAccent,\\n tagRadius: 4,\\n );\\n }\\n\\n static AppBarTheme appBarThemeDark() {\\n return AppBarTheme(\\n backgroundColor: Colors.grey[850]!,\\n elevation: 1,\\n titleTextStyle: TextStyle(\\n color: Colors.white,\\n fontSize: 20,\\n fontWeight: FontWeight.w600,\\n ),\\n iconTheme: IconThemeData(color: Colors.blueGrey[300]),\\n );\\n }\\n\\n static BottomNavigationBarThemeData bottomNavigationBarThemeDataDark() {\\n return BottomNavigationBarThemeData(\\n selectedItemColor: Colors.blueGrey[300],\\n unselectedItemColor: Colors.grey[500],\\n backgroundColor: Colors.grey[850]!,\\n );\\n }\\n\\n static OutlinedButtonThemeData outlinedButtonThemeDataDark() {\\n return OutlinedButtonThemeData(\\n style: OutlinedButton.styleFrom(\\n foregroundColor: Colors.blueGrey[300],\\n side: BorderSide(color: Colors.blueGrey[300]!),\\n ),\\n );\\n }\\n\\n static TextButtonThemeData textButtonThemeDataDark() {\\n return TextButtonThemeData(\\n style: TextButton.styleFrom(\\n foregroundColor: Colors.blueGrey[300],\\n ),\\n );\\n }\\n\\n static ElevatedButtonThemeData elevatedButtonThemeDataDark() {\\n return ElevatedButtonThemeData(\\n style: ElevatedButton.styleFrom(\\n foregroundColor: Colors.black87,\\n backgroundColor: Colors.blueGrey[300]!,\\n padding: EdgeInsets.symmetric(vertical: 12, horizontal: 24),\\n textStyle: TextStyle(fontWeight: FontWeight.w600),\\n ),\\n );\\n }\\n\\n static TextTheme textThemeDark() {\\n return TextTheme(\\n displayLarge: TextStyle(\\n fontSize: 24,\\n fontWeight: FontWeight.bold,\\n color: Colors.white,\\n ),\\n bodyLarge: TextStyle(\\n fontSize: 16,\\n color: Colors.white70,\\n height: 1.5,\\n ),\\n labelSmall: TextStyle(\\n fontSize: 12,\\n color: Colors.grey[400],\\n ),\\n );\\n }\\n\\n static ColorScheme colorSchemeDark() {\\n return ColorScheme.dark(\\n primary: Colors.blueGrey[300]!,\\n secondary: Colors.amber[300]!,\\n surface: Colors.grey[850]!,\\n error: Colors.red[300]!,\\n );\\n }\\n}\\n
\\nimport \'package:flutter/material.dart\';\\nimport \'discover_page.dart\';\\nimport \'home_page.dart\';\\nimport \'mine_page.dart\';\\n\\nclass MainPage extends StatefulWidget {\\n final ValueChanged<bool> onThemeChanged;\\n\\n const MainPage({super.key, required this.onThemeChanged});\\n\\n @override\\n State<MainPage> createState() => _MainPageState();\\n}\\n\\nclass _MainPageState extends State<MainPage> {\\n int _currentIndex = 0;\\n final PageStorageBucket _bucket = PageStorageBucket();\\n final List<Widget> _pages = [\\n HomePage1(key: const PageStorageKey(\'home\')),\\n DiscoverPage(key: const PageStorageKey(\'discover\')),\\n MinePage(key: const PageStorageKey(\'profile\')),\\n ];\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: Text(_getAppBarTitle()),\\n actions: [buildIconButton(context)],\\n ),\\n body: PageStorage(\\n bucket: _bucket,\\n child: _pages[_currentIndex],\\n ),\\n bottomNavigationBar: BottomNavigationBar(\\n currentIndex: _currentIndex,\\n onTap: (index) => setState(() => _currentIndex = index),\\n items: const [\\n BottomNavigationBarItem(\\n icon: Icon(Icons.home),\\n label: \'首页\',\\n ),\\n BottomNavigationBarItem(\\n icon: Icon(Icons.explore),\\n label: \'发现\',\\n ),\\n BottomNavigationBarItem(\\n icon: Icon(Icons.person),\\n label: \'我的\',\\n ),\\n ],\\n ),\\n );\\n }\\n\\n IconButton buildIconButton(BuildContext context) {\\n return IconButton(\\n icon: Icon(Icons.brightness_6),\\n onPressed: () {\\n final isDark = Theme.of(context).brightness == Brightness.dark;\\n widget.onThemeChanged(!isDark);\\n },\\n );\\n }\\n\\n String _getAppBarTitle() {\\n switch (_currentIndex) {\\n case 0:\\n return \'新闻头条\';\\n case 1:\\n return \'发现频道\';\\n case 2:\\n return \'个人中心\';\\n default:\\n return \'新闻应用\';\\n }\\n }\\n}\\n
\\n/// HomePage页面内容\\nimport \'package:flutter/material.dart\';\\nimport \'package:flutter_demo/theme/tag_theme_extension.dart\';\\n\\nclass HomePage extends StatelessWidget {\\n const HomePage({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n final tagTheme = Theme.of(context).extension<TagThemeExtension>()!;\\n\\n return ListView.builder(\\n padding: EdgeInsets.all(16),\\n itemCount: 10,\\n itemBuilder: (context, index) {\\n return Card(\\n child: Padding(\\n padding: EdgeInsets.all(16),\\n child: Column(\\n crossAxisAlignment: CrossAxisAlignment.start,\\n children: [\\n Row(\\n children: [\\n Expanded(\\n child: Text(\\n \'新闻标题 ${index + 1}\',\\n style: Theme.of(context).textTheme.displayLarge,\\n ),\\n ),\\n Container(\\n padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2),\\n decoration: BoxDecoration(\\n color: tagTheme.hotTagBackground,\\n borderRadius: BorderRadius.circular(tagTheme.tagRadius),\\n ),\\n child: Text(\\n \'热\',\\n style: tagTheme.hotTagStyle,\\n ),\\n ),\\n ],\\n ),\\n SizedBox(height: 12),\\n Text(\\n \'新闻内容摘要...\',\\n style: Theme.of(context).textTheme.bodyLarge,\\n ),\\n SizedBox(height: 16),\\n Row(\\n children: [\\n ElevatedButton(\\n onPressed: () {},\\n child: Text(\'阅读全文\'),\\n ),\\n SizedBox(width: 8),\\n TextButton(\\n onPressed: () {},\\n child: Text(\'稍后阅读\'),\\n ),\\n ],\\n ),\\n ],\\n ),\\n ),\\n );\\n },\\n );\\n }\\n}\\n\\n/// 发现页面内容\\nimport \'package:flutter/material.dart\';\\n\\nclass DiscoverPage extends StatelessWidget {\\n const DiscoverPage({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n return Center(\\n child: Text(\\n \'发现频道内容\',\\n style: Theme.of(context).textTheme.displayLarge,\\n ),\\n );\\n }\\n}\\n\\n\\n/// 我的页面内容\\nimport \'package:flutter/material.dart\';\\n\\nclass MinePage extends StatelessWidget {\\n const MinePage({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n return ListView(\\n padding: EdgeInsets.all(16),\\n children: [\\n Card(\\n child: ListTile(\\n leading: Icon(Icons.color_lens),\\n title: Text(\'主题设置\'),\\n subtitle: Text(\'切换浅色/深色模式\'),\\n ),\\n ),\\n Card(\\n child: ListTile(\\n leading: Icon(Icons.settings),\\n title: Text(\'应用设置\'),\\n ),\\n ),\\n Card(\\n child: ListTile(\\n leading: Icon(Icons.info),\\n title: Text(\'关于我们\'),\\n ),\\n ),\\n ],\\n );\\n }\\n}\\n
\\nimport \'dart:ui\';\\nimport \'package:flutter/material.dart\';\\n\\n/// 自定义主题扩展:特殊标记样式\\nclass TagThemeExtension extends ThemeExtension<TagThemeExtension> {\\n final TextStyle hotTagStyle;\\n final Color hotTagBackground;\\n final double tagRadius;\\n\\n const TagThemeExtension({\\n required this.hotTagStyle,\\n required this.hotTagBackground,\\n required this.tagRadius,\\n });\\n\\n @override\\n ThemeExtension<TagThemeExtension> copyWith({\\n TextStyle? hotTagStyle,\\n Color? hotTagBackground,\\n double? tagRadius,\\n }) {\\n return TagThemeExtension(\\n hotTagStyle: hotTagStyle ?? this.hotTagStyle,\\n hotTagBackground: hotTagBackground ?? this.hotTagBackground,\\n tagRadius: tagRadius ?? this.tagRadius,\\n );\\n }\\n\\n @override\\n ThemeExtension<TagThemeExtension> lerp(\\n covariant ThemeExtension<TagThemeExtension>? other, double t) {\\n if (other is! TagThemeExtension) return this;\\n return TagThemeExtension(\\n hotTagStyle: TextStyle.lerp(hotTagStyle, other.hotTagStyle, t)!,\\n hotTagBackground:\\n Color.lerp(hotTagBackground, other.hotTagBackground, t)!,\\n tagRadius: lerpDouble(tagRadius, other.tagRadius, t)!,\\n );\\n }\\n}\\n
\\n归纳总结:
\\nThemeData
统一管理,确保全局一致性。特定按钮单独设置圆角
)。ThemeExtension
实现自定义样式需求,保持代码可维护性。themeMode
轻松切换亮色/暗色
模式。注意事项:
\\n文字颜色
与背景色
的对比度满足无障碍标准。Theme.of(context)
获取当前主题,嵌套主题使用 Theme
组件。Material Design
或产品设计规范。主题系统是Flutter
工程化开发的重要基石。通过本文的系统化学习,我们建立了从设计哲学到实践应用的完整认知框架:
全局/局部
配置方法是核心。ThemeExtensions
处理自定义参数是进阶关键。优秀的主题设计应像精密的瑞士手表
—— 每个零件各司其职又完美协同。建议开发者建立自己的主题规范文档,将颜色
、间距
、字体
等参数标准化。当遇到样式问题时,先思考是否应该通过主题系统解决,而不是直接写死样式值
。这种系统化思维,正是高效Flutter
开发的秘诀所在。
\\n","description":"前言 在移动应用开发中,界面风格的统一性如同人的衣装,直接影响用户体验和应用的专业度。你是否遇到过这样的困扰:\\n\\n按钮颜色要逐个修改?\\n深色模式适配需要重写所有样式?\\n\\n这就是Flutter主题(Theme)系统要解决的核心问题。通过主题系统,开发者可以像搭积木一样管理应用样式,实现\\"一次定义,全局生效\\"的魔法效果。合理使用主题系统能提升界面开发效率,同时降低的样式维护成本。\\n\\n本文将带你从设计哲学到实战应用,系统化构建主题开发能力,让你的应用轻松实现专业级视觉体验。\\n\\n操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。\\n\\n一、基础认知\\n1.1…","guid":"https://juejin.cn/post/7480952941508870179","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-13T07:57:29.132Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/dd2c67cfed7648d1bd190d0a11ac0c7e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1742457449&x-signature=0AcEF95WfIy5rmQf1jcNz6zLch8%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"Reqable项目日志——JSON语法高亮性能200倍提升","url":"https://juejin.cn/post/7480900772386979903","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
好久没有更新Reqable的项目日志了,今天趁着版本发出去的一点点空闲时间,写一篇文章记录下前几天的一个重要性优化。
\\nReqable中JSON语法高亮的性能问题由来已久了,早在2023年9月份的时候,就有用户反馈JSON单个字段超过8055后无法正常高亮,详见 Issue #195,当时评估后不太好解决就挂起了。前段时间,又有用户反馈不到2M的JSON文件高亮渲染性能很差,详见 Issue #1421。这次,我决定把这个问题再次好好捋一捋。
\\n先说说Reqable中是如何实现文本语法高亮的。
\\n早期我在Github上面找到一个非常不错的开源项目highlight.dart,这个项目是基于highlight.js的原理实现的,一个是版本太低,一个是实现逻辑有些问题,因此存在不少Bug。
\\n放弃之后,我们决定参考highlight.dart的思路,将highlight.js用Dart语言严格地完完整整地翻译了一遍(已经开源re-highlight),才总算实现了语法高亮功能。
\\n接下来,我们说说highlight.js这个项目,实现主要分为三个部分语言模式
、处理器
和样式定义
。语言模式
定义了各种语言的语法,例如关键词、变量定义、函数定义等等语法规则,大量利用正则表达式
进行语法定义和约束。处理器
负责根据指定的语言模式对输入文本进行匹配处理,输出这段文本中哪部分是关键词、哪部分是变量、哪部分是函数等等,打上标签。如果在未提前指定语言的情况下,还可以对所有语言进行处理,并输出每个语言的置信度,自动做一个语言检测。样式定义
则是定义了大量的主题样式,比如字体颜色、背景色、字体粗细等等,根据前面处理器输出结果用相应的样式进行渲染,便可以看到语法高亮的效果了。
相信看到这里,大家都能猜测到性能瓶颈在哪里了。没错,就是正则表达式。但是正则表达式效率低归低,但是JSON单个字段超过8055后无法正常高亮又是怎么回事呢?
\\nJSON单个字段超过8055后无法正常高亮这个问题,在用户没有提出来之前,在2023年6月份的时候我们就已经发现了。在Release模式运行时,有些数据JSON高亮会失败,但是在Debug模式下同样的数据没有任何问题。执行语法高亮的逻辑是在一个单独的Isolate中执行的,在Release模式下没有任何输出,仿佛没有执行,一度让我感到困扰。
\\n直到我在Dart仓库下发现这个 Dart compile: StackOverflow when running RegExp.matchAsPrefix ,Dart语言为了性能,AoT下Stack设计得比较小,容易触发StackOverflow。这个问题引来了highlight.js和Dart两边维护者关于正则表达式写法性能的相关讨论。重写正则表达式不可能,重新调参编译Dart VM也挺麻烦,当时又想要不自己重写一套JSON语法高亮解析器算了,因为其他事情要处理,就搁置了。
\\n现在回过头来,看这个问题,比较可行的方案就是单独给JSON写一套语法高亮解析器。输入字符串从头到尾扫描一遍,也就是O(n)的算法复杂度,肯定是不会有性能瓶颈的,JSON节点树深浅,即使是用递归也不会爆栈。问题在于,如何优雅地接入到现在的项目中?能否利用highlight.js本身的机制做到低成本接入?
\\n再次回过头来看highlight.js的代码,直到看到下面这段,心中大喜:
\\n/** @type {BeforeHighlightContext} */\\nconst context = {\\n code,\\n language: languageName\\n};\\n// the plugin can change the desired language or the code to be highlighted\\n// just be changing the object it was passed\\nfire(\\"before:highlight\\", context);\\n\\n// a before plugin can usurp the result completely by providing it\'s own\\n// in which case we don\'t even need to call highlight\\nconst result = context.result\\n ? context.result\\n : _highlight(context.language, context.code, ignoreIllegals);\\n\\nresult.code = context.code;\\n// the plugin can change anything in result to suite it\\nfire(\\"after:highlight\\", result);\\n
\\n原来highlight.js中已经提供了插件API,支持前置处理和后置处理,可以替换掉默认的高亮处理逻辑。所以,我只需要提供一个自定义的JSON解析插件然后注册进去即可,其他任何修改都不需要!
\\n接下来,就是如何在插件里面实现JSON解析逻辑了,像JSON这种严格语法的解析器其实很好写的。不过需要注意的是,和常规的JSON解析不同,我们需要保留全部的空格、换行符、符号等等字符,而不是输出一个Map结构。另外,输出需要按照highlight.js定义的格式,在解析器中还需要做一些特殊处理。终于在Github Copilot的加持下,写完了这个插件,大约300行代码,详见:github.com/reqable/re-…
\\n最后,我们测试下性能,用Issue #195中用户提供的JSON数据(大约1.6M)来实测跑下。优化前的版本在我的Apple M2上跑一遍语法高亮需要30s,优化后跑了一遍我惊呆了,只要150ms,差不多200倍
的提升!
欢迎各位阅读!也欢迎大家来下载和体验 Reqable - 新一代API生产力工具,也欢迎与我一起交流更多Flutter的开发经验和心得!
","description":"好久没有更新Reqable的项目日志了,今天趁着版本发出去的一点点空闲时间,写一篇文章记录下前几天的一个重要性优化。 背景介绍\\n\\nReqable中JSON语法高亮的性能问题由来已久了,早在2023年9月份的时候,就有用户反馈JSON单个字段超过8055后无法正常高亮,详见 Issue #195,当时评估后不太好解决就挂起了。前段时间,又有用户反馈不到2M的JSON文件高亮渲染性能很差,详见 Issue #1421。这次,我决定把这个问题再次好好捋一捋。\\n\\n语法高亮原理\\n\\n先说说Reqable中是如何实现文本语法高亮的。\\n\\n早期我在Github上面找到一个非常不错…","guid":"https://juejin.cn/post/7480900772386979903","author":"MegatronKing","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-13T07:03:38.967Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/839299e2241e44a0bed7010bf2f72ca4~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTWVnYXRyb25LaW5n:q75.awebp?rk3s=f64ab15b&x-expires=1742454218&x-signature=qVzvxiFX6kQ%2Bx6T7n16eepz1d64%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["前端","Flutter","测试"],"attachments":null,"extra":null,"language":null},{"title":"系统化掌握Flutter组件之InteractiveViewer:释放你的双手","url":"https://juejin.cn/post/7480843229099081762","content":"在移动应用中,用户对交互体验的要求越来越高:图片需要双指缩放
、地图需要自由拖拽
、表单需要动态调整布局
……如果让你手动实现这些功能,可能需要处理复杂的手势冲突
、坐标计算
和动画逻辑
。
但Flutter
提供了InteractiveViewer
组件,只需几行代码
即可实现丝滑的平移
、缩放
和边界控制
。
化繁为简
的?\\"交互增强神器\\"
?本文将带你系统化拆解它的核心逻辑,并通过实战案例展示其强大能力,让你轻松掌握交互设计的
关键技巧!
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\n本质定义:
\\nInteractiveViewer
是一个矩阵变换容器,内部通过Matrix4
数学计算实现对子组件的平移
、缩放变换
。它本质上是一个StatefulWidget
,自动管理手势识别
与动画过渡
。
核心价值:
\\n300+
行代码(手势检测
+矩阵计算
+边界约束
+动画
),而InteractiveViewer
仅需10
行。惯性滚动
、弹性边界回弹
等iOS/Android
原生交互体验。iOS/Android/Web
端保持完全一致的手势响应逻辑。底层原理:
\\n\\n\\n用户手势
\\n→
识别为平移/缩放→
计算变换矩阵→
应用矩阵到子组件→
触发重绘
1、自由平移
\\n2、精确缩放
\\nMatrix4
的缩放因子(scale factor
)实现 。3、边界约束
\\nboundaryMargin
控制回弹触发范围 。4、手势冲突处理
\\n缩放优先于平移
)。panEnabled
/scaleEnabled
禁用特定手势。5、矩阵状态控制
\\ntransformationController
实时获取或修改变换状态。\\"重置视角\\"
按钮 。6、性能优化
\\nboundaryMargin
避免过度渲染。推荐使用场景:
\\n✅ 图片/PDF查看器:需支持多级缩放
、自由拖动
的场景。
\\n✅ 地图/画布应用:动态调整视角查看局部
细节 。
\\n✅ 数据可视化面板:展示超宽/超长
内容时局部聚焦 。
\\n✅ 教育类应用:放大查看3D
模型或解剖图。
不推荐场景:
\\n❌ 简单列表滚动:优先使用ListView
/GridView
。
\\n❌ 纯静态缩放:若无需交互,直接使用Transform.scale
。
\\n❌ 超高性能要求:如渲染10万+
个可缩放元素,需自定义RenderObject
。
三大设计原则:
\\nminScale
)定义行为,而非命令式代码。GestureDetector
、Transform
、AnimationController
等基础组件。与传统实现对比(代码片段):
\\n// 传统实现(部分伪代码):\\nGestureDetector(\\n onScaleUpdate: (details) {\\n final matrix = Matrix4.identity()\\n ..translate(details.focalPoint.dx, details.focalPoint.dy)\\n ..scale(details.scale);\\n setState(() => _matrix = matrix);\\n },\\n child: Transform(matrix: _matrix, child: child),\\n)\\n\\n// InteractiveViewer实现:\\nInteractiveViewer(child: child)\\n
\\n维度 | 属性 | 类型 | 默认值 | 深度说明 |
---|---|---|---|---|
缩放控制 | minScale | double | 0.8 | 禁止缩小到小于该值 |
maxScale | double | 2.5 | 禁止放大到超过该值 | |
边界约束 | boundaryMargin | EdgeInsets | EdgeInsets.zero | 边界外扩范围,越大拖拽越自由 |
align | Alignment | Alignment.center | 初始对齐方式 | |
手势行为 | panEnabled | bool | true | 启用平移(单指滑动) |
scaleEnabled | bool | true | 启用缩放(双指捏合) | |
状态控制 | transformationController | TransformationController | null | 获取/修改当前变换矩阵 |
边界约束原理:
\\n[屏幕边缘] \\n│ \\n│ boundaryMargin(如EdgeInsets.all(20)) \\n▼ \\n┌───────────────────┐ \\n│ 实际可拖拽区域 │ \\n└───────────────────┘ \\n
\\nboundaryMargin
的底层计算逻辑:
// 伪代码解释边界检查\\nvoid _applyBoundaryConstraints() {\\n final contentWidth = childSize.width * currentScale;\\n final viewportWidth = viewportSize.width;\\n\\n // 计算水平方向可移动范围\\n minX = (viewportWidth - contentWidth) / 2 - boundaryMargin.left;\\n maxX = (contentWidth - viewportWidth) / 2 + boundaryMargin.right;\\n \\n // 同理计算垂直方向\\n}\\n
\\nchild
中放置超大分辨率图片,应使用Image.network
的frameBuilder
延迟加载。InteractiveViewer
时,使用AbsorbPointer
控制事件传递。boundaryMargin
。transformationController
修改值时,始终先调用value = Matrix4.identity()
重置。组件 | 核心能力 | 学习成本 | 适用场景 |
---|---|---|---|
InteractiveViewer | 平移+缩放+边界控制 | 低 | 通用交互增强 |
PhotoView | 图片专属(旋转、加载指示) | 中 | 专业级图片查看器 |
GestureDetector | 基础手势(点击、拖动) | 高 | 需要完全自定义手势逻辑 |
黄金法则:
\\nInteractiveViewer
满足基础需求。PhotoView
。点击/长按
等简单手势时用GestureDetector
。支持双击缩放
)需求:实现一个支持双指缩放
、双击放大/缩小
的图片查看器 。
import \'package:flutter/material.dart\';\\n\\nclass InteractiveViewerDemo extends StatefulWidget {\\n const InteractiveViewerDemo({super.key});\\n\\n @override\\n State createState() => _InteractiveViewerDemoState();\\n}\\n\\nclass _InteractiveViewerDemoState extends State<InteractiveViewerDemo> {\\n final TransformationController _controller = TransformationController();\\n\\n void _handleDoubleTap() {\\n // 双击切换缩放比例\\n final scale = _controller.value.getMaxScaleOnAxis();\\n _controller.value = scale == 2.0 ? Matrix4.identity() : Matrix4.identity()\\n ..scale(2.0);\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: Text(\\"InteractiveViewer Demo\\"),\\n backgroundColor: Theme.of(context).colorScheme.inversePrimary,\\n ),\\n body: Center(\\n child: Column(\\n children: [\\n GestureDetector(\\n onDoubleTap: _handleDoubleTap,\\n child: InteractiveViewer(\\n transformationController: _controller,\\n boundaryMargin: EdgeInsets.all(10),\\n maxScale: 4,\\n child: Image.asset(\'assets/images/ic_water.webp\'),\\n ),\\n )\\n ],\\n ),\\n ),\\n );\\n }\\n}\\n
\\n技术要点:
\\ntransformationController
主动控制缩放状态。GestureDetector
捕获。boundaryMargin
确保图片缩放后仍可拖拽。InteractiveViewer
的核心价值在于用声明式语法解决交互难题。通过系统化学习其属性体系(缩放约束
、边界控制
、手势开关
),开发者可以快速实现90%
的常见交互需求。值得注意的是,它的设计哲学体现了Flutter
框架\\"组合优于继承\\"
的思想 —— 通过嵌套GestureDetector
、Transform
等底层组件,提供开箱即用的高级功能。
掌握此组件后,建议进一步研究transformationController
与动画的结合,这将为复杂交互(如程序化视角切换)打开新的可能性。优秀的交互设计不是功能的堆砌,而是对用户意图的精准理解,而InteractiveViewer
正是实现这一目标的利器。
\\n","description":"前言 在移动应用中,用户对交互体验的要求越来越高:图片需要双指缩放、地图需要自由拖拽、表单需要动态调整布局……如果让你手动实现这些功能,可能需要处理复杂的手势冲突、坐标计算和动画逻辑。\\n\\n但Flutter提供了InteractiveViewer组件,只需几行代码即可实现丝滑的平移、缩放和边界控制。\\n\\n你是否好奇它是如何化繁为简的?\\n为什么它被称为\\"交互增强神器\\"?\\n\\n本文将带你系统化拆解它的核心逻辑,并通过实战案例展示其强大能力,让你轻松掌握交互设计的关键技巧!\\n\\n操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。\\n\\n一、基础认知\\n1.1…","guid":"https://juejin.cn/post/7480843229099081762","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-13T04:06:39.755Z","media":null,"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 小技巧之通过 MediaQuery 优化 App 性能","url":"https://juejin.cn/post/7480783752483389494","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
许久没更新小技巧系列,温故知新,在两年半前的《 MediaQuery 和 build 优化你不知道的秘密》 我们聊过了在 Flutter 内 MediaQuery 对应 rebuild 机制,由于 MediaQuery 在 MaterialApp
内,并且还是一个 InheritedWidget
, 所以每当你使用一个 MediaQuery.of(context)
,其实就是在向 InheritedWidget
内登记更新绑定 :
具体例子如下图所示:
\\nMyHomePage
使用了 MediaQuery.of(context)
EditPage
EditPage
打开键盘,然后作为上一级页面的 MyHomePage
触发了一些列 rebuild 打印上面的例子很好诠释了 MediaQuery.of(context)
使用不当的后果,特别是当堆栈内页面多的时候,就有很多不必要的开销,而要知道 MediaQuery
涉及 20 来参数,从各种边界到字体大小再到界面比例,可以说在 UI 适配时是经常使用的对象,特别是折叠屏场景更是必不可少,所以合理使用 MediaQuery
就非常重要。
而事实上同样代码,你只需要将 MediaQuery.of(context)
挪到页面 Scaffold
内去使用它的 ctx
,就会发现第二个页面打开键盘时第一个页面不会触发 rebuild 了:
而为什么放到页面 Scaffold
内去使用 context 就好很多?这是因为 Scaffold
内通过「覆盖」MediaQuery
,让他的 body 等 child 部分在 MediaQuery.of(context)
时获取到的是 Scaffold
内的 MediaQueryData
:
另外由于 Scaffold
内部也大量使用 MediaQuery
,在触发 MediaQueryData
更新时,也会触发 Scaffold
的更新, 所以其内部像 body 等参数,也会通过 widget.body
实例等方式,从而避免由于 MediaQuery
更新导致其 child 重复 rebuild 的问题 :
\\n\\n\\n
所以我们知道,使用 MediaQuery
拿的是哪个 context 很重要,如果用错了非 Scaffold
的 context ,那么就很容易造成不必要的性能损耗。
\\n\\n而不同 context 也可能让你拿到不一样的参数结果,比如各种 padding 。
\\n
但是,前面我们说到 MediaQuery
本身带有那么多参数,如果我们只是在意 size
,但是键盘弹出的时候改变的是 viewInsets
,如果这样也导致页面更新,好像也不是很合理,所以后来(3.10) Flutter 更新了 MediaQuery.propertyOf
系列方法。
比如还是一开始的代码,但是我把 MediaQuery.Of(context)
换成 MediaQuery.sizeOf(context)
,入下图所示,在弹出键盘时同样不会触发上一级的 MyHomePage
的 rebuild ,因为此时它关联的是独立的 size 参数:
事实上类似的用法在 Scaffold
内部也用到了,基本上能通过 paddingOf
、sizeOf
、viewInsetsOf
等 propertyOf 方法获取到参数的,就不要直接用 .Of(context)
,这也是 3.10 之后 MediaQuery
上针对性的性能提升:
而之所以 propertyOf 系列参数可以做到约束 MediaQueryData
更新时只触发绑定参数的能力,内部主要还是在 context 登记时,通过 aspect
单独触发 InheritedModel
实现。
每个 InheritedModel
都是一个单独的 InheritedWidget
的实现,而这样 InheritedModel
内部的 InheritedModelElement
就会记录每个子组件依赖的 aspect
,从而形成一个新的独立类型映射,因此 InheritedModel
支持订阅特定模型的变化。
另外,关于 MediaQueryData.fromWindow
,在上古版本内还有 MediaQueryData.fromWindow
这样的 API ,而现在都是 MediaQueryData.fromView
,而之所以这么调整是因为:
\\n\\n起初 Flutter 假定了它只支持一个 Window 的场景,所以会有
\\nSingletonFlutterWindow
这样的 instance window 对象存在,同时window
属性又提供了许多和窗口本身无关的功能,而这种设定在未来多窗口逻辑下会显得很另类。
所以后来开始废除单例 window ,改为 View.of(context)
,也就是可以通过 MediaQueryData.fromView(View.of(context))
这样的方式获取 MediaQueryData
,类似的还有:
/// 3.10 之前\\ndouble dpr = WidgetsBinding.instance.window.devicePixelRatio;\\nLocale locale = WidgetsBinding.instance.window.locale;\\ndouble width =\\n MediaQueryData.fromWindow(WidgetsBinding.instance.window).size.width;\\n\\n\\n/// 3.10 之后\\ndouble dpr = View.of(context).devicePixelRatio;\\nLocale locale = View.of(context).platformDispatcher.locale;\\ndouble width =\\n MediaQueryData.fromView(View.of(context)).size.width;\\n\\n
\\n可以看到,这里的 View
内部肯定也是一个 InheritedWidget
,它将 FlutterView
通过 BuildContext
往下共享,从而提供类似上古时代 「window」 的参数能力,而通过 View.of
获取的参数:
FlutterView
本身的属性值发生变化时,是不会通知绑定的 context
更新,这个行为类似于之前的 WidgetsBinding.instance.window
FlutterView
本身发生变化时,比如 context
绘制到不同的 FlutterView
时,才会触发对应绑定的 context
更新可以看到现在 View.of
这个行为考虑的是「多 FlutterView
」 下的更新场景,如果在单 FlutterView
场景下,它几乎就是静态的,如果你不关心 MediaQuery
动态更新的场景,后者你更应该使用这类「静态获取」的方式。
\\n\\n\\n
好了,今天的小技巧就到这里,温故知新,基本上今天的内容都是过去的片段,把它们放在一起之后,你应该就知道如何使用 MediaQuery
可以让你的 Flutter App 性能有所提升了吧?
在 Flutter 开发中,Dart 语言的流程控制语句起着至关重要的作用。它们允许开发者根据不同的条件执行不同的代码块,或者重复执行特定的代码,从而实现程序的逻辑控制。下面将详细介绍 Dart 中常见的流程控制语句,并结合代码示例进行说明。
\\nif - else
语句用于根据条件的真假来执行不同的代码块。它是最基本的条件控制语句,在很多场景下都会用到。
void main() {\\n int score = 75;\\n\\n if (score >= 90) {\\n print(\'成绩优秀\');\\n } else if (score >= 70) {\\n print(\'成绩良好\');\\n } else if (score >= 60) {\\n print(\'成绩及格\');\\n } else {\\n print(\'成绩不及格\');\\n }\\n}\\n
\\nscore
并赋值为 75。if
和 else if
后面的条件表达式。如果 score >= 90
为 true
,则执行对应的代码块;若为 false
,则继续判断下一个 else if
条件。score >= 70
为 true
时,执行 print(\'成绩良好\');
语句,之后不再继续判断其他条件。if
和 else if
条件都不满足,则执行 else
后面的代码块。switch - case
语句用于根据一个表达式的值来选择执行不同的代码块,通常用于多个固定值的匹配场景。
void main() {\\n String day = \'Monday\';\\n\\n switch (day) {\\n case \'Monday\':\\n print(\'星期一,开始上班啦\');\\n break;\\n case \'Tuesday\':\\n print(\'星期二,继续加油\');\\n break;\\n case \'Wednesday\':\\n print(\'星期三,工作过半啦\');\\n break;\\n case \'Thursday\':\\n print(\'星期四,快到周末啦\');\\n break;\\n case \'Friday\':\\n print(\'星期五,周末在望\');\\n break;\\n case \'Saturday\':\\n print(\'星期六,好好休息\');\\n break;\\n case \'Sunday\':\\n print(\'星期日,放松一下\');\\n break;\\n default:\\n print(\'输入的不是有效的星期\');\\n }\\n}\\n
\\nday
并赋值为 \'Monday\'
。switch
语句会将 day
的值与各个 case
后面的值进行比较。case
时,执行该 case
下的代码块。注意,每个 case
块末尾需要使用 break
语句来跳出 switch
语句,否则会继续执行下一个 case
块的代码。case
,则执行 default
后面的代码块。for
循环用于重复执行一段代码,通常在已知循环次数的情况下使用。
void main() {\\n for (int i = 0; i < 5; i++) {\\n print(\'当前数字是: $i\');\\n }\\n}\\n
\\nfor
循环由三个部分组成:初始化语句 int i = 0
、循环条件 i < 5
和迭代语句 i++
。i
初始化为 0。i < 5
,如果为 true
,则执行循环体中的代码 print(\'当前数字是: $i\');
。i++
,将 i
的值加 1。false
时,循环结束。void main() {\\n List<String> fruits = [\'apple\', \'banana\', \'cherry\'];\\n for (String fruit in fruits) {\\n print(\'我喜欢吃 $fruit\');\\n }\\n}\\n
\\n这里使用 for - in
语法来遍历列表 fruits
中的每个元素,将元素依次赋值给变量 fruit
并执行循环体。
while
循环会在指定条件为 true
时不断执行代码块,直到条件变为 false
。
void main() {\\n int count = 0;\\n while (count < 3) {\\n print(\'当前计数: $count\');\\n count++;\\n }\\n}\\n
\\ncount
并初始化为 0。while
后面的条件 count < 3
,如果为 true
,则执行循环体中的代码 print(\'当前计数: $count\');
和 count++;
。false
时,循环结束。do - while
循环与 while
循环类似,但它会先执行一次循环体,然后再检查条件。
void main() {\\n int num = 0;\\n do {\\n print(\'当前数字: $num\');\\n num++;\\n } while (num < 2);\\n}\\n
\\ndo
后面的循环体,即打印 当前数字: 0
并将 num
的值加 1。while
后面的条件 num < 2
,如果为 true
,则继续执行循环体;否则循环结束。break
语句用于跳出当前的循环或 switch
语句。continue
语句用于跳过当前循环的剩余部分,直接开始下一次循环。void main() {\\n for (int i = 0; i < 5; i++) {\\n if (i == 2) {\\n break; // 当 i 等于 2 时,跳出循环\\n }\\n print(\'当前数字: $i\');\\n }\\n\\n for (int j = 0; j < 5; j++) {\\n if (j == 2) {\\n continue; // 当 j 等于 2 时,跳过本次循环的剩余部分,继续下一次循环\\n }\\n print(\'另一个数字: $j\');\\n }\\n}\\n
\\nfor
循环中,当 i
等于 2 时,执行 break
语句,循环立即结束。for
循环中,当 j
等于 2 时,执行 continue
语句,跳过 print(\'另一个数字: $j\');
语句,直接开始下一次循环。通过合理运用 Dart 中的流程控制语句,如 if - else
、switch - case
、for
循环、while
循环、do - while
循环以及 break
和 continue
语句,开发者可以实现复杂的程序逻辑,使 Flutter 应用更加灵活和强大。在实际开发中,需要根据具体的需求选择合适的流程控制语句来完成相应的任务。
你是否曾惊叹于微信聊天列表的滑动删除
功能?或是疑惑为什么自己的Flutter
应用滑动操作总是不流畅?滑动交互是移动端用户体验的核心之一,而Flutter
的Dismissible
组件正是实现这一能力的\\"幕后英雄\\"
。
但许多初学者在使用时,要么止步于简单删除功能,要么陷入手势冲突、性能卡顿的泥潭。究其根本,是因为缺乏对Dismissible
组件系统化的认知 —— 它不仅仅是一个滑动删除工具,更是一个融合了手势识别
、动画控制
、布局渲染
的综合性交互解决方案。
本文将带你从底层属性到高阶实战,彻底掌握Dismissible
的设计哲学。你将发现:
Dismissible
的深度联动。无论你是刚入门的新手,还是想突破瓶颈的中级开发者,这里都有你需要的答案。
\\n操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\nDismissible
是Flutter
中用于实现滑动交互的核心组件,它允许用户通过滑动手势触发特定操作(如删除
、归档
、标记完成
等),是构建现代移动应用交互体验的“隐形推手”
。与传统的按钮点击不同,Dismissible
将操作隐藏在滑动行为中,既节省界面空间,又符合用户对移动端流畅操作的本能预期,是提升应用专业性的关键细节。
Dismissible
核心属性表属性分类 | 属性名 | 作用 | 必选/可选 |
---|---|---|---|
必选属性 | key | 唯一标识组件,用于状态更新和性能优化 | 必选 |
child | 被滑动的子组件 | 必选 | |
方向控制 | direction | 滑动方向(水平/垂直/多向) | 可选 |
视觉反馈 | background | 滑动时的底层背景(如删除图标) | 可选 |
secondaryBackground | 反向滑动时的另一侧背景(如归档图标) | 可选 | |
行为控制 | confirmDismiss | 滑动完成前的二次确认(如弹窗) | 可选 |
movementDuration | 滑动动画时长控制 | 可选 | |
生命周期回调 | onDismissed | 滑动完成后的回调(如删除数据源) | 可选 |
性能优化 | resizeDuration | 组件收起动画时长 | 可选 |
辅助功能 | crossAxisEndOffset | 控制滑动结束后的横向偏移量(高级布局用) | 可选 |
1、滑动方向控制:
\\n左右
)、垂直(上下
)及多向滑动,灵活适配不同场景需求。左滑删除邮件
,右滑标记为已读
,下拉关闭通知卡片
。2、视觉反馈设计:
\\nbackground
和secondaryBackground
定义滑动时的背景层(如红色删除图标
、绿色完成图标
),直观提示操作含义。渐显
、缩放
),增强交互感知。3、操作安全机制:
\\nconfirmDismiss
属性支持二次确认(如弹窗
),防止误触导致数据丢失。SnackBar
实现操作撤销功能,平衡便捷性
与安全性
。4、数据联动能力:
\\nonDismissed
回调实时更新数据源,确保UI
与状态同步。Provider
、Riverpod
)深度结合,实现复杂业务逻辑。聊天记录
、待办事项
、购物车商品
。右滑
)、任务标记完成(左滑
)。下拉关闭
、设置项的重排序
。key
与child
:必选属性key
:唯一标识组件,确保在列表更新时正确追踪组件状态。
\\n原理:当列表项删除或顺序变化时,通过key
识别组件是否需要重建。
// 错误:列表项删除后key重复导致状态混乱\\nDismissible(\\n key: Key(index.toString()), // 索引作为key可能会导致问题\\n child: ...\\n)\\n// 正确:使用唯一标识符(如item.id)\\nDismissible(\\n key: Key(item.id), // 数据模型中的唯一字段\\n child: ...\\n)\\n
\\nchild
:定义用户可见的可滑动内容,通常为ListTile
或自定义布局。
\\n陷阱:避免在child
中使用过于复杂的布局(如多层嵌套
),可能导致性能问题。
child: ConstrainedBox(\\n constraints: BoxConstraints(maxHeight: 80), // 限制高度提升性能\\n child: ListTile(...),\\n)\\n
\\ndirection
:方向控制枚举值详解:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n值 | 含义 |
---|---|
DismissDirection.startToEnd | 从左向右滑动(左滑) |
DismissDirection.endToStart | 从右向左滑动(右滑) |
DismissDirection.horizontal | 允许左右双向滑动 |
DismissDirection.vertical | 允许上下滑动 |
DismissDirection.up | 仅允许向上滑动 |
DismissDirection.down | 仅允许向下滑动 |
代码示例:实现双向滑动(左右不同操作
)
Dismissible(\\n key: ValueKey(\\"value\\"),\\n direction: DismissDirection.horizontal,\\n background: _buildLeftBackground(),\\n secondaryBackground: _buildRightBackground(),\\n child: Container(\\n width: 200,\\n height: 100,\\n color: Colors.orangeAccent,\\n ),\\n),\\n\\nWidget _buildLeftBackground() {\\n return Container(\\n color: Colors.blue,\\n alignment: Alignment.centerLeft,\\n child: Icon(Icons.archive, color: Colors.white),\\n );\\n}\\n\\nWidget _buildRightBackground() {\\n return Container(\\n color: Colors.red,\\n alignment: Alignment.centerRight,\\n child: Icon(Icons.archive, color: Colors.white),\\n );\\n}\\n
\\n冲突解决:
\\n若在ListView
中同时存在垂直滚动和DismissDirection.vertical
,可能触发手势冲突。
\\n解决方案:限制滑动方向为单一轴或使用AbsorbPointer
控制手势优先级。
background
与secondaryBackground
:视觉反馈设计原则:背景层需直观表达操作意图(如红色代表删除,绿色代表完成
)。
动态效果示例:滑动时图标渐显。
\\nbackground: AnimatedContainer(\\n duration: Duration(milliseconds: 200),\\n color: Colors.red,\\n child: Align(\\n alignment: Alignment.centerLeft,\\n child: Opacity(\\n opacity: _slideProgress, // 根据滑动进度控制透明度\\n child: Icon(Icons.delete),\\n ),\\n ),\\n),\\n
\\n实现思路:通过Dismissible
的onUpdate
回调监听滑动进度:
onUpdate: (details) {\\n setState(() => _slideProgress = details.progress);\\n},\\n
\\nconfirmDismiss
与movementDuration
:行为控制confirmDismiss: (direction) async {\\n if (direction == DismissDirection.endToStart) {\\n // 仅删除操作需要二次确认\\n return await _showDeleteConfirmationDialog();\\n }\\n return true; // 其他方向直接执行\\n},\\nmovementDuration: Duration(milliseconds: 500), // 延长滑动动画时间\\n
\\nonDismissed
:生命周期回调核心逻辑:在滑动完成后更新数据源并触发UI刷新。
\\nonDismissed: (direction) {\\n setState(() {\\n items.removeWhere((item) => item.id == deletedId); // 根据唯一标识删除\\n });\\n}\\n
\\n易错点:
\\n使用唯一ID
)。UI
与数据不一致。import \'package:flutter/material.dart\';\\nclass TodoItem {\\n String id;\\n String title;\\n\\n TodoItem(this.id, this.title);\\n}\\n\\nclass DismissibleDemo extends StatefulWidget {\\n @override\\n State createState() => _DismissibleDemoState();\\n}\\n\\nclass _DismissibleDemoState extends State<DismissibleDemo> {\\n List<TodoItem> items = [];\\n double _slideProgress = 0.0;\\n\\n @override\\n void initState() {\\n super.initState();\\n TodoItem item;\\n for (int i = 0; i < 15; i++) {\\n item = TodoItem(\\"id $i\\", \\"title$i\\");\\n items.add(item);\\n }\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: Text(\\"Dismissible Demo\\"),\\n backgroundColor: Theme.of(context).colorScheme.inversePrimary,\\n ),\\n body: ListView.builder(\\n itemCount: items.length,\\n itemBuilder: (ctx, index) {\\n final item = items[index];\\n return Dismissible(\\n key: Key(item.id),\\n direction: DismissDirection.endToStart,\\n background: _buildDeleteBackground(),\\n onUpdate: (details) =>\\n setState(() => _slideProgress = details.progress),\\n confirmDismiss: (_) => _confirmDelete(item),\\n onDismissed: (_) => _handleDelete(item),\\n movementDuration: Duration(milliseconds: 500), // 延长滑动动画时间\\n child: ConstrainedBox(\\n constraints: BoxConstraints(maxHeight: 80), // 限制高度提升性能\\n child: ListTile(\\n title: Text(item.title),\\n subtitle: Text(\\"滑动删除\\"),\\n ),\\n )\\n ,\\n );\\n },\\n ),\\n );\\n }\\n\\n Widget _buildDeleteBackground() {\\n return AnimatedContainer(\\n duration: Duration(milliseconds: 200),\\n color: Colors.red,\\n child: Align(\\n alignment: Alignment.centerRight,\\n child: Padding(\\n padding: EdgeInsets.only(right: 20),\\n child: Opacity(\\n opacity: _slideProgress.clamp(0.0, 1.0),\\n child: Icon(Icons.delete, size: 30, color: Colors.white),\\n ),\\n ),\\n ),\\n );\\n }\\n\\n Future<bool> _confirmDelete(TodoItem item) async {\\n return await showDialog(\\n context: context,\\n builder: (ctx) => AlertDialog(\\n title: Text(\\"删除 ${item.title}?\\"),\\n actions: [\\n TextButton(\\n onPressed: () => Navigator.pop(ctx, false),\\n child: Text(\\"取消\\"),\\n ),\\n TextButton(\\n onPressed: () => Navigator.pop(ctx, true),\\n child: Text(\\"删除\\"),\\n ),\\n ],\\n ),\\n );\\n }\\n\\n void _handleDelete(TodoItem item) {\\n setState(() => items.removeWhere((i) => i.id == item.id));\\n ScaffoldMessenger.of(context).showSnackBar(\\n SnackBar(content: Text(\\"已删除 ${item.title}\\")),\\n );\\n }\\n}\\n
\\nSnackBar
联动)需求描述:
\\n实现聊天列表左滑删除消息项,删除后底部显示SnackBar
提示,支持3秒内撤销删除操作
。要求:
import \'package:flutter/material.dart\';\\n\\nclass ChatMessage {\\n final String id;\\n final String text;\\n bool isDeleted;\\n\\n ChatMessage({\\n required this.id,\\n required this.text,\\n this.isDeleted = false,\\n });\\n}\\n\\nclass ChatListScreen extends StatefulWidget {\\n const ChatListScreen({super.key});\\n\\n @override\\n State createState() => _ChatListScreenState();\\n}\\n\\nclass _ChatListScreenState extends State<ChatListScreen> {\\n final List<ChatMessage> _messages = List.generate(\\n 15,\\n (i) => ChatMessage(\\n id: \\"id$i\\",\\n text: \'消息 ${i + 1}\',\\n isDeleted: false,\\n ),\\n );\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: Text(\'聊天列表\'),\\n backgroundColor: Theme.of(context).colorScheme.inversePrimary,\\n ),\\n body: ListView.builder(\\n itemCount: _messages.length,\\n itemBuilder: (context, index) {\\n return itemWidget(index);\\n },\\n ),\\n );\\n }\\n\\n Dismissible itemWidget(int index) {\\n return Dismissible(\\n key: ValueKey(_messages[index].id),\\n direction: DismissDirection.endToStart,\\n background: _buildDeleteBackground(),\\n onDismissed: (_) => _handleDismiss(index),\\n child: ConstrainedBox(\\n constraints: BoxConstraints(maxHeight: 80), // 限制高度提升性能\\n child: ListTile(\\n title: Text(_messages[index].text),\\n leading: Icon(Icons.message),\\n ),\\n ),\\n );\\n }\\n\\n Widget _buildDeleteBackground() {\\n return Container(\\n color: Colors.red,\\n alignment: Alignment.centerRight,\\n padding: EdgeInsets.only(right: 20),\\n child: Icon(Icons.delete, color: Colors.white),\\n );\\n }\\n\\n void _handleDismiss(int index) {\\n final deletedItem = _messages[index];\\n setState(() => _messages.removeAt(index));\\n\\n ScaffoldMessenger.of(context).showSnackBar(\\n SnackBar(\\n content: Text(\'已删除 \\"${deletedItem.text}\\"\'),\\n action: SnackBarAction(\\n label: \'撤销\',\\n onPressed: () => setState(() => _messages.insert(index, deletedItem)),\\n ),\\n duration: Duration(seconds: 3),\\n ),\\n );\\n }\\n}\\n
\\n实现技巧:
\\nKey
策略:使用消息id
而非列表索引生成Key
,防止列表更新时出现组件复用错误。removeAt
更新数据,再触发SnackBar
显示,避免界面残留。注意事项:
\\nDismissible
的movementDuration
保证动画流畅。ScaffoldMessenger
而非旧版Scaffold.of
,防止上下文失效。AnimatedList
实现更丝滑的删除动画。需求描述:
\\n在任务管理列表中实现:
import \'package:flutter/material.dart\';\\n\\nclass Task {\\n String id;\\n String title;\\n bool isCompleted;\\n bool isImportant;\\n\\n Task({\\n required this.id,\\n required this.title,\\n this.isCompleted = false,\\n this.isImportant = false,\\n });\\n}\\n\\nclass TaskListScreen extends StatefulWidget {\\n const TaskListScreen({super.key});\\n\\n @override\\n State createState() => _TaskListScreenState();\\n}\\n\\nclass _TaskListScreenState extends State<TaskListScreen> {\\n final List<Task> _tasks = List.generate(\\n 10,\\n (i) => Task(\\n id: \\"id$i\\",\\n title: \'任务 ${i + 1}\',\\n isCompleted: false,\\n isImportant: false,\\n ),\\n );\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: Text(\'多向操作演示\'),\\n backgroundColor: Theme.of(context).colorScheme.inversePrimary,\\n ),\\n body: ListView.builder(\\n itemCount: _tasks.length,\\n itemBuilder: (ctx, index) => Dismissible(\\n key: Key(_tasks[index].id.toString()),\\n direction: _tasks[index].isCompleted\\n ? DismissDirection.up\\n : DismissDirection.horizontal,\\n confirmDismiss: (dir) => _confirmDismiss(dir),\\n onDismissed: (dir) => _handleDismiss(index, dir),\\n background: _buildStartBackground(),\\n secondaryBackground: _buildEndBackground(),\\n child: Container(\\n color: _tasks[index].isImportant ? Colors.amber[100] : null,\\n child: ListTile(\\n title: Text(_tasks[index].title),\\n trailing: _tasks[index].isCompleted\\n ? Icon(Icons.check_circle, color: Colors.green)\\n : null,\\n ),\\n ),\\n ),\\n ),\\n );\\n }\\n\\n Widget _buildStartBackground() => Container(\\n color: Colors.orange,\\n alignment: Alignment.centerLeft,\\n padding: EdgeInsets.only(left: 20),\\n child: Icon(Icons.star, color: Colors.white),\\n );\\n\\n Widget _buildEndBackground() => Container(\\n color: Colors.blue,\\n alignment: Alignment.centerRight,\\n padding: EdgeInsets.only(right: 20),\\n child: Icon(Icons.done_all, color: Colors.white),\\n );\\n\\n Future<bool?> _confirmDismiss(DismissDirection direction) async {\\n final action = {\\n DismissDirection.startToEnd: \'标记重要\',\\n DismissDirection.endToStart: \'完成\',\\n DismissDirection.up: \'删除\'\\n }[direction];\\n\\n return showDialog<bool>(\\n context: context,\\n builder: (ctx) => AlertDialog(\\n title: Text(\'确认操作\'),\\n content: Text(\'确定要$action吗?\'),\\n actions: [\\n TextButton(\\n onPressed: () => Navigator.pop(ctx, false),\\n child: Text(\'取消\'),\\n ),\\n TextButton(\\n onPressed: () => Navigator.pop(ctx, true),\\n child: Text(\'确认\'),\\n ),\\n ],\\n ),\\n );\\n }\\n\\n void _handleDismiss(int index, DismissDirection direction) {\\n final task = _tasks[index];\\n setState(() {\\n switch (direction) {\\n case DismissDirection.endToStart:\\n task.isCompleted = true;\\n break;\\n case DismissDirection.startToEnd:\\n task.isImportant = !task.isImportant;\\n break;\\n case DismissDirection.up:\\n _tasks.removeAt(index);\\n break;\\n default:\\n break;\\n }\\n });\\n }\\n}\\n
\\n避坑指南:
\\nconst
组件。onUpdate
回调中的setState
。iOS
默认支持边缘滑动返回,需在PageView
中禁用冲突手势。Web
端需增加鼠标拖拽支持检测。Dismissible
组件看似简单,实则蕴含多层设计智慧。初学者常陷入的误区是仅将其视为\\"滑动删除工具\\"
,却忽略了它在交互扩展性(如多向操作
)、状态安全性(如撤销机制
)、性能平衡(如列表Key优化
)中的深度价值。
核心公式:Dismissible
= 手势识别 + 动画编排 + 数据驱动。每一次滑动不仅是用户动作的响应,更是对应用状态严谨性的考验。当你下次实现滑动交互时,不妨多问一句:
清晰
?二次确认
?安全
?系统化思考这些问题,你才能真正驾驭Dismissible
,打造出专业级应用体验。
\\n","description":"前言 你是否曾惊叹于微信聊天列表的滑动删除功能?或是疑惑为什么自己的Flutter应用滑动操作总是不流畅?滑动交互是移动端用户体验的核心之一,而Flutter的Dismissible组件正是实现这一能力的\\"幕后英雄\\"。\\n\\n但许多初学者在使用时,要么止步于简单删除功能,要么陷入手势冲突、性能卡顿的泥潭。究其根本,是因为缺乏对Dismissible组件系统化的认知 —— 它不仅仅是一个滑动删除工具,更是一个融合了手势识别、动画控制、布局渲染的综合性交互解决方案。\\n\\n本文将带你从底层属性到高阶实战,彻底掌握Dismissible的设计哲学。你将发现:\\n\\n通过方向控…","guid":"https://juejin.cn/post/7480443517023567907","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-12T03:20:27.012Z","media":null,"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"开源一个两年前写的flutter的K线分时图","url":"https://juejin.cn/post/7480464724094910501","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
两年前做的股票项目,用了一些开源的插件有些数据卡顿,有些滚动卡顿,参考了几个开源的库,具体名字忘了哪些。。股票K线图/分时图/vol图/mac的图/双极图,在币圈的行情下使用的话,有些地方需要修改。已发不到pub.dev\\npub.dev/packages/ac…
\\nimport \'dart:async\';\\n\\nimport \'package:ace_chart/ace_chart.dart\';\\nimport \'package:demo/data.dart\'; // 数据list,文章字数有限制,请从github获取\\nimport \'package:demo/data2.dart\'; // 数据list2,,文章字数有限制,请从github获取\\nimport \'package:flutter/material.dart\';\\n\\nconst Color backgroundColor = Color(0xff151924);\\nconst Color textColor = Colors.white;\\nconst TextStyle style = TextStyle(color: textColor);\\n\\nclass Home extends StatefulWidget {\\n const Home({super.key});\\n\\n @override\\n State<Home> createState() => _HomeState();\\n}\\n\\nint ii = 0;\\n\\nclass _HomeState extends State<Home> {\\n final AceStockMetricController container = AceStockMetricController(\\n useVOLMA: true,\\n useMACD: true,\\n maxLength: list.length,\\n // maDays: [5],\\n pointWidth: 7,\\n useKdj: true,\\n );\\n final AceStockMetricController container3 = AceStockMetricController(\\n useVOLMA: true,\\n useMACD: true,\\n maxLength: list2.length,\\n maDays: [],\\n useKdj: true,\\n );\\n\\n @override\\n void initState() {\\n super.initState();\\n int i = 60;\\n container.addAll(list.sublist(0, i));\\n Timer.periodic(const Duration(milliseconds: 10), (timer) {\\n if (i < list.length) {\\n container.addValue(list[i]);\\n } else {\\n timer.cancel();\\n }\\n i++;\\n });\\n Timer.periodic(const Duration(milliseconds: 10), (timer) {\\n if (ii < list2.length) {\\n container3.addValue(list2[ii]);\\n } else {\\n timer.cancel();\\n }\\n ii++;\\n setState(() {});\\n });\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: const Text(\\n \\"DEMO\\",\\n style: style,\\n ),\\n backgroundColor: backgroundColor,\\n ),\\n backgroundColor: backgroundColor,\\n body: SingleChildScrollView(\\n child: Column(\\n mainAxisAlignment: MainAxisAlignment.start,\\n children: <Widget>[\\n AceContainer(\\n controller: container3,\\n child: Builder(\\n builder: (context) {\\n return Column(\\n children: [\\n SizedBox(\\n height: 150,\\n child: LineChart(\\n transformTime: (time) {\\n return millisToHM(time);\\n },\\n lineColor: const Color(0xff697abc),\\n lastClose: 255.90,\\n gridVerticalGrids: 1,\\n gridHorizontalGrids: 2,\\n paddingTop: 20,\\n centralAxisStyle: const TextStyle(\\n color: Colors.white,\\n fontSize: 8,\\n ),\\n gridLineColor: Colors.white12,\\n crossLineColor: const Color(0xffd13e51),\\n crossTextBgColor: const Color(0xffd13e51),\\n horizontalTextStyle: const TextStyle(\\n color: Color(0xff959FAE),\\n fontSize: 8,\\n ),\\n ),\\n ),\\n Container(\\n height: 10,\\n color: Colors.black,\\n ),\\n const SizedBox(\\n height: 40,\\n child: VolChart(\\n showMaLine: true,\\n showText: false,\\n upperStyle: PaintingStyle.stroke,\\n ),\\n ),\\n Container(\\n height: 10,\\n color: Colors.black,\\n ),\\n const SizedBox(\\n height: 80,\\n child: MacdChart(\\n textStyle: TextStyle(\\n color: Colors.white,\\n fontSize: 8,\\n ),\\n ),\\n ),\\n ],\\n );\\n },\\n ),\\n ),\\n Container(\\n height: 10,\\n margin: const EdgeInsets.symmetric(vertical: 3),\\n color: Colors.black,\\n ),\\n Container(\\n color: Colors.black12,\\n child: AceContainer(\\n controller: container,\\n child: Builder(\\n builder: (context) {\\n return Column(\\n children: [\\n SizedBox(\\n height: 150,\\n child: KChart(\\n highMarkColor: Colors.white,\\n lowMarkColor: Colors.white,\\n crossLineColor: const Color(0xffd13e51),\\n crossTextBgColor: const Color(0xffd13e51),\\n gridTextStyle: const TextStyle(\\n color: Colors.white,\\n fontSize: 8,\\n ),\\n gridLineColor: Colors.white12,\\n horizontalTextStyle: const TextStyle(\\n color: Color(0xff959FAE),\\n fontSize: 8,\\n ),\\n transformTime: (time) {\\n return millisToMD(time);\\n },\\n onCrossChange: (index, alignment) {\\n if (index == -1) {\\n return;\\n }\\n },\\n ),\\n ),\\n Container(\\n height: 10,\\n margin: const EdgeInsets.symmetric(vertical: 3),\\n color: Colors.black,\\n ),\\n const SizedBox(\\n height: 40,\\n child: VolChart(\\n showMaLine: true,\\n showText: false,\\n upperStyle: PaintingStyle.stroke,\\n textStyle: TextStyle(\\n color: Colors.white,\\n fontSize: 8,\\n ),\\n ),\\n ),\\n Container(\\n height: 10,\\n margin: const EdgeInsets.symmetric(vertical: 3),\\n color: Colors.black,\\n ),\\n const SizedBox(\\n height: 80,\\n child: MacdChart(\\n textStyle: TextStyle(\\n color: Colors.white,\\n fontSize: 8,\\n ),\\n ),\\n ),\\n Container(\\n height: 10,\\n margin: const EdgeInsets.symmetric(vertical: 3),\\n color: Colors.black,\\n ),\\n Padding(\\n padding: const EdgeInsets.all(20),\\n child: SizedBox(\\n height: 199,\\n width: 199,\\n child: CircularProgressIndicator(\\n value: (ii + 1) / list2.length,\\n strokeWidth: 10,\\n color: const Color(0xff9096FF),\\n backgroundColor: Colors.white54,\\n ),\\n ),\\n ),\\n ],\\n );\\n },\\n ),\\n ),\\n ),\\n const SizedBox(\\n height: 10,\\n ),\\n ],\\n ),\\n ),\\n );\\n }\\n}\\n\\n
","description":"ace_chart 两年前做的股票项目,用了一些开源的插件有些数据卡顿,有些滚动卡顿,参考了几个开源的库,具体名字忘了哪些。。股票K线图/分时图/vol图/mac的图/双极图,在币圈的行情下使用的话,有些地方需要修改。已发不到pub.dev pub.dev/packages/ac…\\n\\nScreenshot\\n\\nDemo\\nimport \'dart:async\';\\n\\nimport \'package:ace_chart/ace_chart.dart\';\\nimport \'package:demo/data.dart\'; // 数据list,文章字数有限制…","guid":"https://juejin.cn/post/7480464724094910501","author":"strongs","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-12T02:42:02.086Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/68654dbe4dbc4dd1a9d91202138fb7e7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgc3Ryb25ncw==:q75.awebp?rk3s=f64ab15b&x-expires=1742352358&x-signature=FrKwWMVdQIug7AwC%2Fg5TD1UA9wc%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"系统化掌握Flutter组件之Draggable/DragTarget","url":"https://juejin.cn/post/7480434437638324224","content":"你是否曾在移动应用中体验过\\"拖拽文件到文件夹\\"
的丝滑操作?或是在游戏中通过拖动物品完成谜题
的成就感?这种直观的交互背后,是Flutter
组件库中Draggable
与DragTarget
的默契配合。作为现代UI
设计的核心能力之一,拖拽交互不仅提升了用户体验,更考验开发者对组件系统的深刻理解。
但对于初学者来说,面对这两个组件的20+
属性、复杂的生命周期和状态管理,往往会陷入\\"一学就会,一用就废\\"
的困境。
本文将用系统化思维,带你从零构建对拖拽交互的认知框架,通过\\"属性解剖+实战案例+设计哲学\\"
的三重维度,助你彻底掌握这一看似简单却暗藏玄机的交互范式。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\nDraggable
组件详解核心作用:
\\n将子组件变为可拖拽对象,控制拖拽过程中的数据传递
、视觉表现
和交互行为
。
属性 | 类型 | 必填 | 说明 |
---|---|---|---|
data | T | 是 | 拖拽时传递的核心数据对象,决定DragTarget 能否接收的关键标识 |
group | String | 否 | 定义拖拽分组,用于限制同组DragTarget 才能接收(如多级菜单联动场景 ) |
代码示例:
\\nDraggable<String>(\\n data: \'Flutter\', // 拖拽时传递的字符串数据\\n child: Container(...),\\n)\\n
\\n独到见解:
\\ndata
是拖拽系统的\\"身份证\\"
,建议使用不可变对象(如枚举
、唯一ID
)避免状态污染。group
可实现跨屏拖拽的沙盒隔离,比如游戏背包与战场装备栏的独立拖拽逻辑。属性 | 类型 | 必填 | 说明 |
---|---|---|---|
child | Widget | 是 | 默认状态下显示的静态组件 |
feedback | Widget | 否 | 拖拽过程中跟随手指移动的组件(默认使用child 的副本) |
childWhenDragging | Widget | 否 | 拖拽时原始位置占位组件(常用于实现\\"留痕\\"效果) |
feedbackOffset | Offset | 否 | 调整feedback 组件的偏移量(解决手指遮挡问题 ) |
Column buildColumn() {\\n return Column(\\n mainAxisAlignment: MainAxisAlignment.center,\\n children: [\\n // 条件渲染Draggable:未被拖拽时才显示\\n if (!_isDragged) _buildDraggable(),\\n const SizedBox(height: 50),\\n _buildDragTargetZone(),\\n ],\\n );\\n}\\n\\nWidget _buildDraggable() {\\n return Draggable<String>(\\n data: \'Flutter\',\\n //拖拽过程中跟随手指移动的组件(默认使用child的副本)\\n feedback:\\n // _buildDragFeedback(),\\n // 拖拽时放大图标\\n Transform.scale(\\n scale: 1.2,\\n child: _buildSourceItem(),\\n ),\\n // 原位置显示半透明占位\\n childWhenDragging: Opacity(\\n opacity: 0.5,\\n child: _buildSourceItem(),\\n ),\\n // 向上偏移20像素\\n feedbackOffset: Offset(0, -20),\\n onDragCompleted: () {\\n print(\'拖拽完成,数据已被接收\');\\n },\\n onDragEnd: (details) {\\n if (!_isDragged) {\\n // 未被接收时执行回弹动画\\n print(\'拖拽取消,返回原位\');\\n }\\n },\\n child: _buildSourceItem(),\\n );\\n}\\n\\nWidget _buildSourceItem() {\\n return Container(\\n width: 100,\\n height: 100,\\n decoration: BoxDecoration(\\n color: Colors.blue,\\n borderRadius: BorderRadius.circular(16),\\n ),\\n child: const Icon(Icons.ads_click, color: Colors.white),\\n );\\n}\\n\\nWidget _buildDragFeedback() {\\n return Material(\\n child: Container(\\n width: 100,\\n height: 100,\\n decoration: BoxDecoration(\\n color: Colors.blue.withAlpha(220),\\n borderRadius: BorderRadius.circular(16),\\n boxShadow: [\\n BoxShadow(\\n color: Colors.black.withValues(alpha: 0.2),\\n blurRadius: 8,\\n offset: const Offset(0, 4),\\n )\\n ],\\n ),\\n child: const Icon(Icons.ads_click, color: Colors.white),\\n ),\\n );\\n}\\n
\\n视觉设计原则:
\\nchild
与feedback
需保持形态关联(如颜色
、形状
)。feedbackOffset
确保拖拽内容不被手指遮挡
。childWhenDragging
用半透明/灰色暗示\\"已被拖走\\"
。属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
axis | Axis | 无限制 | 限制拖拽方向(Axis.horizontal/vertical ) |
maxSimultaneousDrags | int | 1 | 允许同时拖拽的实例数量(用于实现多指拖拽) |
ignoringFeedbackPointer | bool | true | 是否忽略feedback 组件的点击事件(避免拖拽过程中意外触发其他交互) |
代码示例:
\\nDraggable(\\n axis: Axis.horizontal, // 只能水平拖拽\\n maxSimultaneousDrags: 2, // 允许两个实例同时拖拽\\n ignoringFeedbackPointer: false, // feedback可点击(如拖拽按钮时需保持点击态)\\n)\\n
\\n交互设计陷阱:
\\naxis
限制方向时,垂直方向的拖拽手势会被系统拦截(如ListView
滑动冲突)maxSimultaneousDrags
>1时需考虑多指操作的视觉重叠问题。回调方法 | 触发时机 | 典型应用场景 |
---|---|---|
onDragStarted | 拖拽动作开始时 | 记录初始状态、播放音效 |
onDragUpdate | 拖拽位置更新时 | 实时计算偏移量(如磁贴吸附效果) |
onDragEnd | 拖拽结束且未被DragTarget 接收时 | 执行回弹动画、恢复初始状态 |
onDraggableCanceled | 拖拽被意外终止(如来电打断 ) | 执行异常处理逻辑 |
代码示例:
\\nDraggable(\\n onDragStarted: () => print(\'拖拽开始\'),\\n onDragUpdate: (details) {\\n final offset = details.localPosition;\\n print(\'当前位置:$offset\');\\n },\\n onDragEnd: (details) {\\n if (details.velocity.pixelsPerSecond.dx > 500) {\\n // 快速滑动后执行甩出动画\\n }\\n },\\n)\\n
\\n状态管理要点:
\\nStatefulWidget
或状态管理库中转。onDragUpdate
的高频触发特性要求逻辑必须轻量化(防止界面卡顿
)。DragTarget
组件详解核心作用:定义可接收拖拽数据的区域,处理数据验证
、接收和视觉反馈
。
属性 | 类型 | 说明 |
---|---|---|
onWillAcceptWithDetails | bool Function(DragTargetDetails<T?> details) | 数据进入目标区域时的验证(返回true 才会触发后续接收) |
onAcceptWithDetails | void Function(DragTargetDetails<T?> details) | 数据成功接收时的回调(完成业务逻辑的核心入口) |
onLeave | void Function(T?) | 数据离开目标区域时的回调(常用于重置状态) |
代码示例:
\\nWidget _buildDragTargetZone() {\\n return DragTarget<String>(\\n builder: (context, candidateData, rejectedData) {\\n final isActive = candidateData.isNotEmpty;\\n return Container(\\n width: 150,\\n height: 150,\\n decoration: BoxDecoration(\\n color: isActive ? Colors.blue.shade100 : Colors.grey.shade200,\\n borderRadius: BorderRadius.circular(24),\\n border: Border.all(\\n color: isActive ? Colors.blue : Colors.grey,\\n width: 2,\\n ),\\n ),\\n child: Center(\\n child: _buildContentInTarget(),\\n ),\\n );\\n },\\n // 数据进入目标区域时的验证(返回true才会触发后续接收)\\n onWillAcceptWithDetails: (data) => data.data == \'Flutter\', // 只接受指定数据\\n // 数据成功接收时的回调(完成业务逻辑的核心入口)\\n onAcceptWithDetails: (data) {\\n setState(() {\\n _isDragged = true;\\n _draggedData = data.data;\\n });\\n },\\n //数据离开目标区域时的回调(常用于重置状态)\\n onLeave: (data) {\\n setState(() => _isDragged = false);\\n },\\n );\\n}\\n\\nWidget _buildContentInTarget() {\\n if (_draggedData == null) {\\n return const Text(\'拖拽到此区域\', style: TextStyle(color: Colors.grey));\\n }\\n\\n // 显示接收后的内容\\n return AnimatedSwitcher(\\n duration: const Duration(milliseconds: 300),\\n child: Container(\\n key: ValueKey(_draggedData),\\n width: 100,\\n height: 100,\\n decoration: BoxDecoration(\\n color: Colors.green,\\n borderRadius: BorderRadius.circular(16),\\n ),\\n child: Column(\\n mainAxisAlignment: MainAxisAlignment.center,\\n children: [\\n const Icon(Icons.check_circle, color: Colors.white),\\n const SizedBox(height: 8),\\n Text(_draggedData!, style: const TextStyle(color: Colors.white))\\n ],\\n ),\\n ),\\n );\\n}\\n
\\n验证逻辑设计模式:
\\nonWillAcceptWithDetails
实现数据过滤(如文件格式校验
)。垃圾桶图标中心范围
)。库存已满时禁止拖入
)。属性 | 类型 | 说明 |
---|---|---|
builder | Widget Function(context, List, List) | 动态构建目标区域的UI ,根据候选/拒绝数据改变样式 |
builder
参数解析:
candidateData
:当前悬停在目标区域上方的有效数据列表。rejectedData
:被onWillAcceptWithDetails
拒绝的数据列表。代码示例:
\\nDragTarget<Color>(\\n builder: (context, candidates, rejects) {\\n final isHighlighted = candidates.isNotEmpty;\\n return AnimatedContainer(\\n duration: Duration(milliseconds: 200),\\n color: isHighlighted ? Colors.yellow : Colors.grey,\\n child: Center(child: Text(isHighlighted ? \'释放改变颜色\' : \'拖入颜色\')),\\n );\\n },\\n)\\n
\\n视觉反馈设计原则:
\\n透明度
/颜色变化
暗示可操作区域。微交互
(如震动
、涟漪效果
)增强操作确定性。候选中
、接收成功
、拒绝
三种状态的视觉表现。属性 | 类型 | 说明 |
---|---|---|
hitTestBehavior | HitTestBehavior | 控制点击测试行为(决定哪些区域可触发拖拽接收) |
onMove | void Function(DragTargetDetails) | 数据在目标区域内移动时的细节追踪 |
代码示例:
\\nDragTarget(\\n hitTestBehavior: HitTestBehavior.opaque, // 即使透明区域也可接收\\n onMove: (details) {\\n final localPosition = details.localPosition;\\n _showPositionMarker(localPosition); // 实时显示坐标标记\\n },\\n)\\n
\\n高级交互场景:
\\nonMove
获取坐标实现网格对齐(如日历日程拖拽)。hitTestBehavior
处理多层拖拽目标叠加的情况。// 完整的最小化示例\\nclass DragDemo extends StatefulWidget {\\n @override\\n _DragDemoState createState() => _DragDemoState();\\n}\\n\\nclass _DragDemoState extends State<DragDemo> {\\n String _message = \'拖拽文字到目标区域\';\\n\\n @override\\n Widget build(BuildContext context) {\\n return Column(\\n children: [\\n Draggable<String>(\\n data: \'Hello Flutter!\',\\n child: Chip(label: Text(\'拖拽我\')),\\n feedback: Material(child: Chip(label: Text(\'拖拽中...\'))),\\n ),\\n SizedBox(height: 50),\\n DragTarget<String>(\\n builder: (context, candidates, rejects) {\\n return Container(\\n width: 200,\\n height: 100,\\n color: candidates.isEmpty ? Colors.blueGrey : Colors.blue,\\n alignment: Alignment.center,\\n child: Text(_message),\\n );\\n },\\n onAccept: (data) => setState(() => _message = data),\\n ),\\n ],\\n );\\n }\\n}\\n
\\nDraggable显示``child
,DragTarget
待命。Draggable
显示feedback
,DragTarget
根据candidateData
改变外观。onAccept
更新业务数据,可能需要刷新全局状态。onLeave
,通常伴随视觉回退。现象 | 可能原因 | 解决方案 |
---|---|---|
拖拽过程中UI 闪烁 | feedback 组件尺寸与child 不一致 | 为feedback 设置固定宽高 |
DragTarget 无法接收数据 | data 类型与onWillAcceptWithDetails 验证不匹配 | 检查data 类型和验证逻辑 |
拖拽结束后原位置空白 | 未设置childWhenDragging | 添加占位组件或透明度动画 |
多指拖拽时互相干扰 | maxSimultaneousDrags 设置不合理 | 根据场景调整最大同时拖拽数 |
拖拽区域响应不灵敏 | hitTestBehavior 设置过于严格 | 改为HitTestBehavior.translucent |
三层设计法则:
\\ndata
定义信息实体,group
建立通信规则。feedback
系列属性控制视觉语言的一致性。系统化思维训练:
\\n下次看到任何拖拽交互时,尝试在脑海中拆解:
数据结构
是什么?视觉层级
如何管理?边界条件
?这三大思考维度,将帮助你快速洞悉任何复杂拖拽交互的实现本质。
\\nimport \'package:flutter/material.dart\';\\n\\nclass Product {\\n final String id;\\n final String name;\\n final Color color;\\n\\n Product(this.id, this.name, this.color);\\n}\\n\\nclass ShoppingCartScreen extends StatefulWidget {\\n const ShoppingCartScreen({super.key});\\n\\n @override\\n State createState() => _ShoppingCartScreenState();\\n}\\n\\nclass _ShoppingCartScreenState extends State<ShoppingCartScreen> {\\n final List<Product> _products = [\\n Product(\'1\', \'运动鞋\', Colors.orange),\\n Product(\'2\', \'背包\', Colors.purple),\\n Product(\'3\', \'手表\', Colors.blue),\\n ];\\n final List<Product> _cartItems = [];\\n final GlobalKey _cartKey = GlobalKey();\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: Text(\\"ShoppingCart demo\\"),\\n backgroundColor: Theme.of(context).colorScheme.inversePrimary,\\n ),\\n body: Center(\\n child: buildColumn(),\\n ),\\n );\\n }\\n\\n Column buildColumn() {\\n return Column(\\n children: [\\n Expanded(\\n child: Padding(\\n padding: const EdgeInsets.all(16),\\n child: GridView.builder(\\n gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(\\n crossAxisCount: 3,\\n crossAxisSpacing: 10,\\n mainAxisSpacing: 10,\\n ),\\n itemCount: _products.length,\\n itemBuilder: (context, index) =>\\n _buildProductItem(_products[index]),\\n ),\\n ),\\n ),\\n _buildCartSection(),\\n ],\\n );\\n }\\n\\n Widget _buildProductItem(Product product) {\\n return Draggable<Product>(\\n data: product,\\n feedback: _buildDragFeedback(product),\\n childWhenDragging: Opacity(\\n opacity: 0.5,\\n child: _buildProductCard(product),\\n ),\\n child: _buildProductCard(product),\\n );\\n }\\n\\n Widget _buildProductCard(Product product) {\\n return Material(\\n elevation: 2,\\n borderRadius: BorderRadius.circular(12),\\n child: Container(\\n decoration: BoxDecoration(\\n color: product.color.withValues(alpha: 0.2),\\n borderRadius: BorderRadius.circular(12),\\n border: Border.all(color: product.color),\\n ),\\n child: Column(\\n mainAxisAlignment: MainAxisAlignment.center,\\n children: [\\n Icon(Icons.shopping_bag, color: product.color),\\n const SizedBox(height: 8),\\n Text(product.name),\\n ],\\n ),\\n ),\\n );\\n }\\n\\n Widget _buildDragFeedback(Product product) {\\n return Transform.scale(\\n scale: 1.1,\\n child: Material(\\n elevation: 8,\\n child: Container(\\n width: 80,\\n height: 80,\\n decoration: BoxDecoration(\\n color: Colors.white,\\n borderRadius: BorderRadius.circular(8),\\n border: Border.all(color: product.color),\\n ),\\n child: Icon(Icons.shopping_cart, color: product.color),\\n ),\\n ),\\n );\\n }\\n\\n Widget _buildCartSection() {\\n return DragTarget<Product>(\\n key: _cartKey,\\n builder: (context, candidates, rejects) {\\n final isActive = candidates.isNotEmpty;\\n return buildTargetZone(isActive);\\n },\\n onWillAcceptWithDetails: (product) => !_cartItems.contains(product.data),\\n onAcceptWithDetails: (product) =>\\n setState(() => _cartItems.add(product.data)),\\n );\\n }\\n\\n ///目标区域\\n Container buildTargetZone(bool isActive) {\\n return Container(\\n height: 120,\\n decoration: BoxDecoration(\\n color: isActive ? Colors.green.shade50 : Colors.grey.shade100,\\n border: Border(top: BorderSide(color: Colors.grey.shade300)),\\n ),\\n child: Stack(\\n children: [\\n AnimatedSwitcher(\\n duration: const Duration(milliseconds: 300),\\n child: _cartItems.isEmpty\\n ? Center(\\n child:\\n Text(\'拖拽商品到此区域\', style: TextStyle(color: Colors.grey)))\\n : _buildCartItems(),\\n ),\\n if (isActive)\\n Positioned.fill(\\n child: IgnorePointer(\\n child: AnimatedContainer(\\n duration: const Duration(milliseconds: 200),\\n color: Colors.green.withValues(alpha: 0.1),\\n ),\\n ),\\n ),\\n ],\\n ),\\n );\\n }\\n\\n Widget _buildCartItems() {\\n return Padding(\\n padding: const EdgeInsets.all(8.0),\\n child: Row(\\n mainAxisAlignment: MainAxisAlignment.center,\\n children: _cartItems.map((product) {\\n return Padding(\\n padding: const EdgeInsets.symmetric(horizontal: 8),\\n child: Icon(Icons.shopping_cart, color: product.color),\\n );\\n }).toList(),\\n ),\\n );\\n }\\n}\\n
\\nimport \'package:flutter/material.dart\';\\n\\nclass KanbanBoard extends StatefulWidget {\\n @override\\n _KanbanBoardState createState() => _KanbanBoardState();\\n}\\n\\nclass _KanbanBoardState extends State<KanbanBoard> {\\n final List<String> _tasks = [\\n \'需求分析\',\\n \'UI设计\',\\n \'开发实现\',\\n \'测试验证\',\\n \'上线部署\',\\n ];\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: Text(\\"ShoppingCart demo\\"),\\n backgroundColor: Theme.of(context).colorScheme.inversePrimary,\\n ),\\n body: Center(\\n child: Row(\\n children: [\\n _buildColumn(\'待处理\', Colors.blue),\\n _buildColumn(\'进行中\', Colors.orange),\\n _buildColumn(\'已完成\', Colors.green),\\n ],\\n ),\\n ),\\n );\\n }\\n\\n Widget _buildColumn(String title, Color color) {\\n return Expanded(\\n child: Padding(\\n padding: const EdgeInsets.all(8.0),\\n child: DragTarget<String>(\\n builder: (context, candidates, rejects) {\\n return Container(\\n decoration: BoxDecoration(\\n color: color.withValues(alpha: 0.1),\\n borderRadius: BorderRadius.circular(12),\\n border: Border.all(color: color),\\n ),\\n child: Column(\\n children: [\\n Padding(\\n padding: const EdgeInsets.all(8.0),\\n child: Text(title, style: TextStyle(color: color, fontSize: 16)),\\n ),\\n Expanded(\\n child: ReorderableListView(\\n padding: const EdgeInsets.all(8),\\n onReorder: (oldIndex, newIndex) {\\n setState(() {\\n if (newIndex > _tasks.length) newIndex = _tasks.length;\\n final item = _tasks.removeAt(oldIndex);\\n _tasks.insert(newIndex, item);\\n });\\n },\\n children: _tasks.map((task) => _buildTaskCard(task, color)).toList(),\\n ),\\n ),\\n ],\\n ),\\n );\\n },\\n onAcceptWithDetails: (data) {\\n setState(() {\\n _tasks.remove(data.data);\\n _tasks.add(data.data);\\n });\\n },\\n ),\\n ),\\n );\\n }\\n\\n Widget _buildTaskCard(String task, Color color) {\\n return Draggable<String>(\\n key: ValueKey(task),\\n data: task,\\n feedback: Material(\\n child: Container(\\n width: 200,\\n margin: const EdgeInsets.symmetric(vertical: 4),\\n decoration: BoxDecoration(\\n color: Colors.white,\\n borderRadius: BorderRadius.circular(8),\\n boxShadow: [\\n BoxShadow(\\n color: Colors.black12,\\n blurRadius: 8,\\n offset: Offset(0, 4),\\n )\\n ],\\n ),\\n child: ListTile(\\n title: Text(task),\\n leading: Icon(Icons.drag_indicator, color: color),\\n ),\\n ),\\n ),\\n child: Container(\\n width: 200,\\n margin: const EdgeInsets.symmetric(vertical: 4),\\n decoration: BoxDecoration(\\n color: Colors.white,\\n borderRadius: BorderRadius.circular(8),\\n border: Border.all(color: color),\\n ),\\n child: ListTile(\\n title: Text(task),\\n leading: Icon(Icons.drag_indicator, color: color),\\n ),\\n ),\\n );\\n }\\n}\\n
\\n在Flutter
的交互宇宙中,Draggable
与DragTarget
绝非孤立的两个组件,而是一个完整的拖拽生态系统。从基础属性到跨组件通信,从视觉反馈到性能优化,掌握它们需要建立三层认知:
\\"数据如何流动\\"
。\\"状态如何迁移\\"
。\\"交互如何赋能业务\\"
。优秀的拖拽设计应是\\"看不见的交互\\"
—— 用户感受到的是直觉化的操作,而背后是你精心设计的组件协作网络。当你能够用系统化思维拆解每个onAcceptWithDetails
回调、每个feedback
动画时,真正的Flutter
交互大师之路就此开启。
\\n","description":"前言 你是否曾在移动应用中体验过\\"拖拽文件到文件夹\\"的丝滑操作?或是在游戏中通过拖动物品完成谜题的成就感?这种直观的交互背后,是Flutter组件库中Draggable与DragTarget的默契配合。作为现代UI设计的核心能力之一,拖拽交互不仅提升了用户体验,更考验开发者对组件系统的深刻理解。\\n\\n但对于初学者来说,面对这两个组件的20+属性、复杂的生命周期和状态管理,往往会陷入\\"一学就会,一用就废\\"的困境。\\n\\n本文将用系统化思维,带你从零构建对拖拽交互的认知框架,通过\\"属性解剖+实战案例+设计哲学\\"的三重维度,助你彻底掌握这一看似简单却暗藏玄机的交互范式。…","guid":"https://juejin.cn/post/7480434437638324224","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-11T23:50:32.215Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f2e6229321aa4a6894a32712ed43b3fa~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1742341832&x-signature=bGHoR1wMFrNwWmCnyraCxv6FRlA%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"提高 80% 中文字体加载速度 flutter 3.29 web 终于支持 woff2","url":"https://juejin.cn/post/7480431348487077915","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
在之前的版本中,flutter 加载中文字体需要首先下载整个字体文件,再进行加载和显示。而中文字体动辄几mb甚至十几mb,这最终导致整个页面加载速度缓慢。
\\n这里的问题在于,我们的网页并不需要总是使用到所有的文字,也就是说大部分下载的内容都是浪费的。
\\n\\n\\nWOFF2(Web Open Font Format 2)是一种现代的网页字体文件格式。它是在 WOFF 格式的基础上改进而来的,主要用于在网页上加载和显示字体。相比于传统的 TTF 或 OTF 文件,WOFF2 具有以下优点:
\\n\\n
\\n- 压缩率更高:WOFF2 使用了更先进的压缩技术,使得字体文件比 WOFF 更小,可以显著减少网页的加载时间。
\\n- 更快的网页性能:由于文件更小,使用 WOFF2 的网页可以更快加载,特别是在移动设备和低带宽环境下表现更佳。
\\n- 标准化支持:WOFF2 是由 W3C(万维网联盟)制定的标准,受主流浏览器的广泛支持,包括 Chrome、Firefox、Edge 和 Safari 等。
\\n这种字体格式尤其适合中文等字符较多的语言,可以显著提高网页字体加载速度。
\\n
简单的说,它允许部分下载和加载,按需使用。
\\n在不设置字体的默认情况下(noto sans sc),woff2 是自动生效的。
\\n通过对比可见,虽然数量增加,但需要下载的字体文件显著减小。
\\n众所周知,google 服务在国内连接效果一直不稳定,对此我们有两种解决方案。一是自己部署字体文件。二是尝试更换域名,例如 fonts.gstatic.cn 或者 gstatic.loli.net。
\\n这需要修改一下 index.html 和其中的 js 脚本具体需要参考官方文档 Flutter web app initialization | Flutter
\\n但坑爹的是文档并没有写清楚如何更换字体下载域名,我找遍全网也没有找到相关文档。无奈之下只能自己翻源码,google 你长点心吧。最终代码如下
\\n// web\\\\flutter_bootstrap.js\\n{{flutter_js}}\\n{{flutter_build_config}}\\n\\nconst loading = document.createElement(\'div\');\\ndocument.body.appendChild(loading);\\nloading.textContent = \\"Loading Entrypoint...长时间无响应请尝试更换网络\\";\\n_flutter.loader.load({\\n config: {\\n \'canvasKitBaseUrl\': \'/canvaskit/\',\\n },\\n serviceWorkerSettings: {\\n serviceWorkerVersion: {{flutter_service_worker_version}},\\n },\\n onEntrypointLoaded: async function (engineInitializer) {\\n loading.textContent = \\"Initializing engine...长时间无响应请尝试删除缓存或更换浏览器\\";\\n const appRunner = await engineInitializer.initializeEngine({\\n // *** 改这里的地址 ***\\n \'fontFallbackBaseUrl\': \'https://fonts.gstatic.cn/s/\',\\n });\\n\\n loading.textContent = \\"Running app...\\";\\n await appRunner.runApp();\\n },\\n});\\n
\\n虽说是提升显著,但总归还是没有直接加载系统字体来得快,但据官方说,最快也要年底了。
\\n","description":"背景 在之前的版本中,flutter 加载中文字体需要首先下载整个字体文件,再进行加载和显示。而中文字体动辄几mb甚至十几mb,这最终导致整个页面加载速度缓慢。\\n\\n这里的问题在于,我们的网页并不需要总是使用到所有的文字,也就是说大部分下载的内容都是浪费的。\\n\\nwoff2 是什么?\\n\\nWOFF2(Web Open Font Format 2)是一种现代的网页字体文件格式。它是在 WOFF 格式的基础上改进而来的,主要用于在网页上加载和显示字体。相比于传统的 TTF 或 OTF 文件,WOFF2 具有以下优点:\\n\\n压缩率更高:WOFF2 使用了更先进的压缩技术…","guid":"https://juejin.cn/post/7480431348487077915","author":"p1gd0g","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-11T17:04:17.844Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6dacd71b7f72466ab5c6bfc6f00b6681~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgcDFnZDBn:q75.awebp?rk3s=f64ab15b&x-expires=1742317457&x-signature=MMHmbcaZomIreIwZ6kj1DmdFxqA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b8a8464c0e3f44c2bf8350ead0a1ba2d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgcDFnZDBn:q75.awebp?rk3s=f64ab15b&x-expires=1742317457&x-signature=YwQ8aIMwGrVoP%2BDxwHyWduIR1kE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/054aa94038004554b3977e2c34a7b3ef~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgcDFnZDBn:q75.awebp?rk3s=f64ab15b&x-expires=1742317457&x-signature=iIPRDpR9OTGrPFtIziSj%2BcqVCk0%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"系统化掌握Flutter组件之InkWell(一):筑基之旅","url":"https://juejin.cn/post/7480081951519309824","content":"在移动应用中,一个按钮的点击效果可能决定了用户是否会进行下一步操作。Flutter
的InkWell
组件正是这种微妙交互的幕后英雄 —— 它不仅能实现点击反馈,还能通过水波纹动画
、悬停效果
等细节提升用户体验。但许多初学者仅停留在onTap
的基础使用上,忽略了其背后强大的定制能力。你是否想过:
\\"顺滑\\"
,而有些却显得生硬?InkWell
的属性如何与Material Design
哲学深度绑定?本文将带你从底层属性到高阶实战,系统化掌握InkWell
的设计精髓,让你的Flutter
应用拥有\\"会呼吸\\"
的交互体验。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\nInkWell
?InkWell
是 Flutter
中用于处理 点击交互 的核心组件,它基于 Material Design
设计语言,通过 水波纹动画 和 状态反馈 为用户操作提供视觉响应。与简单的 GestureDetector
不同,InkWell
不仅支持多种手势事件,还内置了符合 Material
规范的交互效果,是构建高质量 UI
交互的基石。
单击
、双击
、长按
、右键点击
等交互事件(如 onTap
、onLongPress
)。splashColor
)、高亮(highlightColor
)、悬停(hoverColor
)等效果,直观反映用户操作状态。Material
生态:Material
组件树实现墨水效果,天然适配 AppBar
、Card
等 Material
组件。Android/iOS
)和桌面端(Windows/macOS/Linux
)自动适配交互细节(如桌面端的悬停效果
)。InkWell
?Material Design
强调操作的 即时反馈,InkWell
通过动画将用户的触摸行为转化为可见的视觉语言,例如:\\nonTapDown
)。onTapUp
)。按钮
、列表项
、卡片
均可通过相似反馈表达可点击性)。GestureDetector
的核心区别特性 | InkWell | GestureDetector |
---|---|---|
视觉反馈 | 内置 Material 水波纹、高亮效果 | 无内置效果,需手动实现 |
使用场景 | 需要符合 Material 规范的交互 | 需要完全自定义交互逻辑 |
性能开销 | 略高(含动画和状态管理 ) | 更低(仅处理原始手势事件) |
依赖关系 | 必须嵌套在 Material 组件内 | 无特殊依赖 |
InkWell
继承自 InkResponse
,其核心是通过 Material
组件树的 Ink
层 绘制水波纹效果。当用户触摸屏幕时:
onTapDown
)。InteractiveInkFeature
对象(默认使用 splashFactory
创建水波纹)。Material
的 Ink
画布上渲染动画效果。Material( // 必须存在 Material 祖先\\n child: InkWell(\\n onTap: () {},\\n child: Text(\'Click Me\'),\\n ),\\n)\\n
\\nElevatedButton
实现自定义样式的点击反馈。ListView
的 item
添加优雅的水波纹效果。Card
组件上叠加可点击区域(如“更多”
按钮)。Material
依赖:Material
组件,会抛出 No Material widget found
错误。Material
的 shape
属性控制(如圆形按钮需设置 shape: CircleBorder()
)。InkWell
,可通过 const
构造函数或缓存状态减少重绘。类别 | 属性名称 | 类型 | 作用描述 |
---|---|---|---|
基础交互 | onTap | GestureTapCallback? | 单击事件的回调函数 |
onDoubleTap | GestureTapCallback? | 双击事件的回调函数 | |
onLongPress | GestureLongPressCallback? | 长按事件的回调函数 | |
二级交互 | onSecondaryTap | GestureTapCallback? | 鼠标右键/触控板双指点击的回调函数 |
onSecondaryTapUp | GestureTapUpCallback? | 二级点击抬起时的回调 | |
onSecondaryTapDown | GestureTapDownCallback? | 二级点击按下时的回调 | |
onSecondaryTapCancel | GestureTapCancelCallback? | 二级点击取消时的回调 | |
按压状态 | onTapDown | GestureTapDownCallback? | 点击按下时的回调(触发按压状态) |
onTapUp | GestureTapUpCallback? | 点击抬起时的回调(结束按压状态) | |
onTapCancel | GestureTapCancelCallback? | 点击取消时的回调(如滑动离开组件区域) | |
视觉反馈 | splashColor | Color? | 水波纹扩散颜色(需配合 Material 组件使用) |
highlightColor | Color? | 按压时的高亮底色(覆盖在组件表面) | |
hoverColor | Color? | 鼠标悬停时的背景色(仅桌面端生效) | |
overlayColor | MaterialStateProperty<Color?> | 动态控制覆盖色(根据组件状态自动切换) | |
radius | double? | 水波纹效果的扩散半径 | |
形状控制 | borderRadius | BorderRadius? | 水波纹的圆角边界(需父级 Material 设置 shape 生效) |
customBorder | ShapeBorder? | 完全自定义水波纹的边界形状(如星形、多边形) | |
焦点控制 | focusColor | Color? | 键盘焦点选中时的覆盖色 |
focusNode | FocusNode? | 管理键盘焦点状态的对象 | |
canRequestFocus | bool? | 是否允许组件获取焦点(默认 true ) | |
autofocus | bool | 是否在组件加载时自动获取焦点(默认 false ) | |
悬停控制 | onHover | ValueChanged<bool>? | 悬停状态变化的回调(bool 参数表示是否悬停) |
hoverDuration | Duration? | 悬停效果从显示到消失的过渡时间(默认 200ms) | |
高级反馈 | splashFactory | InteractiveInkFeatureFactory? | 自定义水波纹效果的工厂类(如实现渐变、异形涟漪) |
enableFeedback | bool? | 是否启用触觉反馈(如安卓的振动,默认 true ) | |
语义控制 | excludeFromSemantics | bool? | 是否从语义树中排除组件(用于无障碍功能,默认 false ) |
状态联动 | statesController | MaterialStatesController? | 外部控制组件状态(如代码强制触发按压效果) |
鼠标交互 | mouseCursor | MouseCursor? | 鼠标悬停时的光标样式(如 SystemMouseCursors.click 显示手指图标) |
控制用户点击行为的回调函数,是InkWell
的核心交互逻辑。
属性 | 类型 | 说明 | 默认行为 |
---|---|---|---|
onTap | VoidCallback | 单击触发,最常用的事件(如按钮点击)。 | 无 |
onDoubleTap | VoidCallback | 双击触发(如放大图片)。 | 无 |
onLongPress | VoidCallback | 长按触发(如弹出菜单)。 | 无 |
onTapCancel | VoidCallback | 点击取消(如手指滑动离开组件区域)。 | 无 |
InkWell(\\n onTap: () => print(\'onTap---\x3e\'),\\n onDoubleTap: () => print(\'onDoubleTap---\x3e\'),\\n onLongPress: () => print(\'onLongPress---\x3e\'),\\n onTapCancel: () => print(\'onTapCancel---\x3e\'),\\n child: buildContainer(),\\n),\\n\\n\\nContainer buildContainer() {\\n return Container(\\n width: 200,\\n height: 200,\\n decoration: BoxDecoration(\\n color: Colors.blueGrey,\\n borderRadius: BorderRadius.circular(15),\\n boxShadow: [BoxShadow(blurRadius: 10)],\\n ),\\n child: Icon(Icons.star, size: 50, color: Colors.white),\\n );\\n}\\n
\\n调整水波纹
、高亮
、悬停
等反馈效果的颜色与动画。
属性 | 类型 | 说明 | 默认值 |
---|---|---|---|
splashColor | Color | 水波纹颜色(触摸瞬间扩散效果)。 | ThemeData.splashColor |
highlightColor | Color | 高亮颜色(按住时的持续底色)。 | ThemeData.highlightColor |
hoverColor | Color | 悬停颜色(桌面端鼠标悬停时的底色)。 | ThemeData.hoverColor |
radius | double | 水波纹扩散的半径(单位:逻辑像素)。 | 根据触摸位置自动计算 |
splashFactory | InkSplash.splashFactory | 控制水波纹样式(如禁用效果)。 | ThemeData.splashFactory |
InkWell(\\n splashColor: Colors.redAccent.withValues(alpha: 0.3),\\n highlightColor: Colors.redAccent.withValues(alpha: 0.1),\\n hoverColor: Colors.redAccent.withValues(alpha: 0.2),\\n radius: 20,\\n onTap: () {},\\n child: Container(\\n width: 200,\\n height: 100,\\n alignment: Alignment.center,\\n child: Text(\'自定义反馈效果\'),\\n ),\\n),\\n//Container可能因为设置了decoration导致看不到点击效果\\nInkWell(\\n onTap: () {},\\n child: Container(\\n width: 200,\\n height: 100,\\n decoration: BoxDecoration(\\n color: Colors.blueGrey,\\n borderRadius: BorderRadius.circular(8),\\n border: Border.all(color: Colors.grey),\\n ),\\n child: const Center(child: Text(\'点击区域\')),\\n ),\\n),\\n\\n//使用Ink代替Container\\nInkWell(\\n onTap: () {},\\n child: Ink(\\n decoration: BoxDecoration(\\n color: Colors.blueGrey,\\n borderRadius: BorderRadius.circular(8),\\n border: Border.all(color: Colors.grey),\\n ),\\n width: 200,\\n height: 100,\\n child: const Center(\\n child: Text(\'点击区域\'),\\n ),\\n ),\\n),\\n
\\n注意事项:
\\nInkWell
嵌套子组件Container
的问题:Container
的 decoration
默认会创建一个新的绘制层,覆盖父级 Material
的画布,导致涟漪效果被压在下方不可见。Ink
组件的优势:Ink
是专门为 Material
设计的装饰容器,直接与 InkWell
的绘制层兼容,不会遮挡涟漪效果。限制水波纹的扩散区域,适配不同形状的组件。
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n属性 | 类型 | 说明 | 默认值 |
---|---|---|---|
customBorder | ShapeBorder | 定义组件的形状边界(如圆形、圆角矩形)。 | RectangleBorder |
excludeFromSemantics | bool | 是否排除辅助功能语义(如屏幕阅读器忽略此组件)。 | false |
focusColor | Color | 获得键盘焦点时的颜色(桌面端Tab键导航)。 | 透明 |
overlayColor | MaterialStateProperty<Color> | 根据组件状态(如按下、禁用)动态设置覆盖色。 | 无 |
Material(\\n shape: CircleBorder(), // 关键:父级Material控制水波纹形状\\n child: InkWell(\\n customBorder: CircleBorder(), // 与父级形状一致\\n onTap: () {},\\n child: Ink(\\n decoration: BoxDecoration(\\n shape: BoxShape.circle,\\n color: Colors.orange,\\n ),\\n width: 100,\\n height: 100,\\n child: Icon(Icons.favorite, color: Colors.white),\\n ),\\n ),\\n)\\n
\\n需求:实现点击时颜色渐变
和图标缩放效果
。
import \'package:flutter/material.dart\';\\n\\nclass AdvancedInkWellDemo extends StatefulWidget {\\n const AdvancedInkWellDemo({super.key});\\n\\n @override\\n State<AdvancedInkWellDemo> createState() => _AdvancedInkWellDemoState();\\n}\\n\\nclass _AdvancedInkWellDemoState extends State<AdvancedInkWellDemo>\\n with SingleTickerProviderStateMixin {\\n bool _isActive = false;\\n late AnimationController _controller;\\n late Animation<double> _scaleAnimation;\\n\\n @override\\n void initState() {\\n super.initState();\\n _controller = AnimationController(\\n vsync: this,\\n duration: const Duration(milliseconds: 200),\\n );\\n _scaleAnimation = Tween<double>(begin: 1.0, end: 1.2).animate(_controller);\\n }\\n\\n @override\\n void dispose() {\\n _controller.dispose();\\n super.dispose();\\n }\\n\\n void _toggleState() {\\n setState(() {\\n _isActive = !_isActive;\\n });\\n _controller.forward().then((_) => _controller.reverse());\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n home: Scaffold(\\n appBar: AppBar(title: const Text(\'动态效果示例\')),\\n body: Center(\\n child: AnimatedBuilder(\\n animation: _scaleAnimation,\\n builder: (context, child) {\\n return buildTransform();\\n },\\n ),\\n ),\\n ),\\n );\\n }\\n\\n Transform buildTransform() {\\n return Transform.scale(\\n scale: _scaleAnimation.value,\\n child: Material(\\n color: Colors.transparent,\\n child: InkWell(\\n onTap: _toggleState,\\n splashColor: _isActive ? Colors.deepPurple : Colors.amber,\\n highlightColor: Colors.transparent,\\n borderRadius: BorderRadius.circular(24),\\n customBorder: const StadiumBorder(),\\n child: buildContainer(),\\n ),\\n ),\\n );\\n }\\n\\n Widget buildContainer() {\\n return Ink(\\n width: 200,\\n height: 60,\\n decoration: BoxDecoration(\\n color: _isActive ? Colors.blue.shade100 : Colors.grey.shade200,\\n borderRadius: BorderRadius.circular(24),\\n border: Border.all(\\n color: _isActive ? Colors.blue : Colors.grey,\\n width: 2,\\n ),\\n ),\\n child: Row(\\n mainAxisAlignment: MainAxisAlignment.center,\\n children: [\\n Icon(\\n Icons.favorite,\\n color: _isActive ? Colors.red : Colors.grey,\\n ),\\n const SizedBox(width: 10),\\n Text(\\n _isActive ? \'已激活\' : \'点击激活\',\\n style: TextStyle(\\n color: _isActive ? Colors.blue.shade900 : Colors.grey.shade800,\\n fontWeight: FontWeight.bold,\\n ),\\n ),\\n ],\\n ),\\n );\\n }\\n}\\n
\\n效果说明:
\\n200ms
完成)。背景色
/边框色
/图标颜色
同步变化。StadiumBorder
实现胶囊状水波纹。splashColor
根据状态改变。防止重复渲染
)import \'package:flutter/material.dart\';\\n\\nclass OptimizedListView extends StatelessWidget {\\n final List<String> items = List.generate(100, (i) => \'Item ${i + 1}\');\\n\\n OptimizedListView({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: const Text(\'性能优化示例\')),\\n body: ListView.builder(\\n itemCount: items.length,\\n itemBuilder: (context, index) => _OptimizedListItem(\\n title: items[index],\\n index: index,\\n ),\\n ),\\n );\\n }\\n}\\n\\nclass _OptimizedListItem extends StatelessWidget {\\n final String title;\\n final int index;\\n\\n const _OptimizedListItem({\\n required this.title,\\n required this.index,\\n });\\n\\n @override\\n Widget build(BuildContext context) {\\n return Padding(\\n padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),\\n child: Material(\\n color: Colors.transparent,\\n child: InkWell(\\n key: ValueKey(\'inkwell_$index\'),// 唯一标识防止重建\\n onTap: () => print(\'点击 $title\'),\\n splashColor: Colors.blue.withValues(alpha: 0.2),\\n highlightColor: Colors.blue.withValues(alpha: 0.1),\\n borderRadius: BorderRadius.circular(12),\\n child: Ink(\\n height: 80,\\n decoration: BoxDecoration(\\n color: _getBackgroundColor(index),\\n borderRadius: BorderRadius.circular(12),\\n ),\\n padding: const EdgeInsets.all(16),\\n child: Row(\\n children: [\\n const FlutterLogo(size: 40),\\n const SizedBox(width: 16),\\n Text(\\n title,\\n style: const TextStyle(\\n fontSize: 18,\\n fontWeight: FontWeight.w500,\\n ),\\n ),\\n ],\\n ),\\n ),\\n ),\\n ),\\n );\\n }\\n\\n Color _getBackgroundColor(int index) {\\n return index.isEven ? Colors.white : Colors.grey.shade100;\\n }\\n}\\n
\\n优化策略:
\\nconst
修饰无状态组件。InkWell
设置唯一 Key
。InkWell
的本质是交互意图的可视化翻译器。初学者常犯的错误是仅将其视为\\"带效果的点击组件\\"
,而忽略了它背后承载的Material Design
交互体系。在进阶应用中,要始终把握\\"状态管理\\"
与\\"动画协调\\"
两个核心命题。
优秀的交互设计不是添加炫酷效果,而是让用户的操作意图得到优雅的视觉回应。当你下次使用InkWell
时,不妨问自己三个问题:
操作预期
?状态
是否清晰可辨?干扰
主流程?系统化思维,才是掌握Flutter
组件的终极密钥。
\\n","description":"前言 在移动应用中,一个按钮的点击效果可能决定了用户是否会进行下一步操作。Flutter的InkWell组件正是这种微妙交互的幕后英雄 —— 它不仅能实现点击反馈,还能通过水波纹动画、悬停效果等细节提升用户体验。但许多初学者仅停留在onTap的基础使用上,忽略了其背后强大的定制能力。你是否想过:\\n\\n为什么有些应用的按钮反馈让人感到\\"顺滑\\",而有些却显得生硬?\\nInkWell的属性如何与Material Design哲学深度绑定?\\n\\n本文将带你从底层属性到高阶实战,系统化掌握InkWell的设计精髓,让你的Flutter应用拥有\\"会呼吸\\"的交互体验。\\n\\n操千…","guid":"https://juejin.cn/post/7480081951519309824","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-11T03:35:48.018Z","media":null,"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"Dart 关键字解析:mixin、abstract、on、with","url":"https://juejin.cn/post/7479994388023623734","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
mixin
是 Dart 提供的一种机制,允许在多个类之间复用代码。与类不同,mixin
不能被实例化,而是用来被其他类混入使用。
mixin Logger {\\n void log(String message) {\\n print(\\"Log: $message\\");\\n }\\n}\\n\\nclass Service with Logger {\\n void doSomething() {\\n log(\\"Service is doing something\\");\\n }\\n}\\n\\nclass AdvancedService extends Service {\\n void doAdvancedWork() {\\n log(\\"Advanced service is working\\"); // ❌ 这里会报错\\n }\\n}\\n\\nvoid main() {\\n Service service = Service();\\n service.doSomething(); // 输出: Log: Service is doing something\\n}\\n
\\nmixin 不会被继承,它只能被直接混入 (with)。如果 Service 作为一个类混入了 Logger,但 Service 的子类 不会 继承 Logger 的功能。
\\nabstract
用于定义抽象类,表示该类不能被直接实例化,只能作为基类被继承。
abstract class Animal {\\n void makeSound(); // 抽象方法,不提供实现\\n}\\n\\nclass Dog extends Animal {\\n @override\\n void makeSound() {\\n print(\\"Woof!\\");\\n }\\n}\\n\\nvoid main() {\\n Dog dog = Dog();\\n dog.makeSound(); // 输出: Woof!\\n}\\n
\\nDart 允许 mixin
声明为 abstract mixin
,表示该 mixin 本身是抽象的,不能直接被使用,必须由子类实现其中的方法。
abstract class BaseLogger {\\n void setup(); // 仅作为对比,展示 abstract class\\n}\\n\\nabstract mixin Logger on BaseLogger { \\n void log(String message);\\n}\\n\\nclass ConsoleLogger extends BaseLogger with Logger { \\n @override\\n void setup() {\\n print(\\"Logger setup completed.\\");\\n }\\n\\n @override\\n void log(String message) {\\n print(\\"Console Log: $message\\");\\n }\\n}\\n\\nvoid main() {\\n ConsoleLogger logger = ConsoleLogger();\\n logger.setup(); // 输出: Logger setup completed.\\n logger.log(\\"Hello, Dart!\\"); // 输出: Console Log: Hello, Dart!\\n}\\n
\\n解释
\\n• 这里 on BaseLogger 规定了 Logger 只能混入 BaseLogger 或其子类。
\\n• 这避免了 Logger 被滥用于任何类,而是强制它只能在 BaseLogger 体系内使用。
\\n• ConsoleLogger 继承了 BaseLogger,并通过 with Logger 混入 Logger,必须实现 log 方法。
\\n• 如果 Logger 只是 abstract class,那么 ConsoleLogger 就需要 extends Logger,这样就无法同时继承 BaseLogger。
\\n• 通过 abstract mixin,我们可以在继承 BaseLogger 的同时引入 Logger 这个功能模块,提高代码复用性。
\\n什么时候应该使用 abstract mixin?
\\n• 需要 mixin 机制:如果你希望将某个功能模块化,并允许多个类通过 with 关键字使用它,而不是强制单一继承。
\\n• 强制约束特定基类:通过 on 关键字,abstract mixin 限制了它的适用范围,避免了错误使用。
\\n• 强制子类实现方法:不像普通 mixin 允许提供默认实现,abstract mixin 要求 with 它的类必须实现它的抽象方法。
\\n结论
\\n虽然 abstract mixin 乍一看与 abstract class 类似,但它适用于更灵活的代码复用场景,特别是当你希望一个类既能继承一个类,又能混入多个功能时,它提供了一种比 abstract class 更优雅的解决方案。
\\non
关键字用于限定 mixin
只能被特定的类或其子类混入。
abstract class Animal {\\n void breathe() {\\n print(\\"Breathing...\\");\\n }\\n}\\n\\nmixin Walker on Animal {\\n void walk() {\\n print(\\"Walking...\\");\\n }\\n}\\n\\nclass Dog extends Animal with Walker {}\\n\\nvoid main() {\\n Dog dog = Dog();\\n dog.breathe(); // 输出: Breathing...\\n dog.walk(); // 输出: Walking...\\n}\\n
\\n注意:如果 Walker
没有 on Animal
限制,则 mixin
可以被任何类混入。
with
关键字用于在类中混入 mixin
,实现代码复用。
mixin Flyer {\\n void fly() {\\n print(\\"Flying...\\");\\n }\\n}\\n\\nclass Bird with Flyer {}\\n\\nvoid main() {\\n Bird bird = Bird();\\n bird.fly(); // 输出: Flying...\\n}\\n
\\nDart 3.0 之后,mixin
允许包含字段,但字段必须是 late
或有初始值。
mixin Counter {\\n int count = 0;\\n void increment() {\\n count++;\\n print(\\"Count: $count\\");\\n }\\n}\\n\\nclass CounterService with Counter {}\\n\\nvoid main() {\\n CounterService service = CounterService();\\n service.increment(); // 输出: Count: 1\\n service.increment(); // 输出: Count: 2\\n}\\n
\\nlate
关键字用于延迟初始化变量,表示变量在首次使用前会被赋值,而不是在声明时初始化。这在 mixin
中尤为重要,因为 mixin
不能有构造函数,不能在初始化时直接赋值。
mixin Configurable {\\n late String config;\\n void setConfig(String value) {\\n config = value;\\n }\\n void printConfig() {\\n print(\\"Config: $config\\");\\n }\\n}\\n\\nclass Service with Configurable {}\\n\\nvoid main() {\\n Service service = Service();\\n service.setConfig(\\"Dark Mode\\");\\n service.printConfig(); // 输出: Config: Dark Mode\\n}\\n
\\nDart 3.0 之后,mixin
允许包含字段,但字段必须是 late
或有初始值。
mixin Counter {\\n int count = 0;\\n void increment() {\\n count++;\\n print(\\"Count: $count\\");\\n }\\n}\\n\\nclass CounterService with Counter {}\\n\\nvoid main() {\\n CounterService service = CounterService();\\n service.increment(); // 输出: Count: 1\\n service.increment(); // 输出: Count: 2\\n}\\n
\\nmixin
不能有构造函数。mixin
不能直接被实例化,必须与 class
结合使用。on
限定 mixin
只能应用于指定的基类。abstract
类不能直接实例化,必须由子类实现抽象方法。with
允许在类中混入多个 mixin
,但顺序可能影响方法解析。mixin
允许包含 late
字段或带默认值的字段,但不允许非 late
且无默认值的字段。mixin
组合使用示例mixin A {\\n void method() => print(\\"A\\");\\n}\\n\\nmixin B {\\n void method() => print(\\"B\\");\\n}\\n\\nclass C with A, B {}\\n\\nvoid main() {\\n C c = C();\\n c.method(); // 输出: B (B 覆盖了 A 的 method)\\n}\\n
\\n这说明 Dart 采用后来的 mixin 优先的解析规则。
\\nDart 通过 mixin
、abstract
、on
和 with
关键字提供了一种强大的代码复用机制。合理使用这些关键字可以提高代码的灵活性和可维护性。
在移动应用开发中,用户与界面之间的手势交互如同人类对话时的肢体语言,是构建自然用户体验的核心要素。GestureDetector
作为Flutter
手势系统的基石组件,其设计哲学在于将复杂的触控事件抽象为语义化的手势回调,让开发者能够用声明式语法捕获用户交互意图。
不同于Android
的View.OnClickListener
或iOS
的UIGestureRecognizer
,它通过分层的事件处理模型和智能手势竞争裁决机制,实现了跨平台手势交互的统一抽象。
掌握该组件不仅能提升界面交互的精细度,更能深入理解Flutter
框架的事件分发体系,这是构建复杂交互应用的关键突破口。当你的手指划过屏幕时,GestureDetector
正在将物理世界的连续动作转化为数字世界的精确语义,这种转化正是人机交互设计的精髓所在。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\nGestureDetector
?GestureDetector
是 Flutter
中用于检测和处理用户手势的核心组件。它本身不渲染任何视觉元素,而是通过包裹子组件(如按钮
、图片
、容器
等),监听用户的触摸
、滑动
、长按
、缩放
等交互行为,并触发相应的回调函数。可以借助它快速实现复杂的交互逻辑,是构建响应式 UI
的基础工具。
Tap
)、双击(Double Tap
)、长按(Long Press
)、拖动(Drag
)、缩放(Scale
)、压力感应(Force Press
)等20+种手势事件,覆盖绝大多数交互场景。onTap
、onVerticalDragUpdate
)精准控制手势的各个阶段(按下
、移动
、释放
、取消
),实现细腻的交互反馈。触屏
、鼠标
、触控笔
、压感设备
等多种输入方式,并提供 supportedDevices
属性限制特定设备的交互。单击
与双击
的优先级),并通过 behavior
属性控制事件传递策略,避免嵌套组件的交互冲突。按钮点击
、图片双击放大
、长按显示菜单
。列表滑动
、元素自由拖拽
、进度条调整
。双指缩放图片
、画布绘图
(结合压感)、多方向滑动导航
。excludeFromSemantics
管理语义树,适配屏幕阅读器。InkWell
:InkWell
提供了 Material Design
的点击涟漪效果,但手势类型较少;GestureDetector
无内置视觉效果,但支持更丰富的手势和精细控制。Listener
监听原始指针事件(如 onPointerDown
)需要手动处理手势逻辑,而 GestureDetector
封装了高级手势识别,开发效率更高。GestureDetector
,避免不必要的性能开销。RawGestureDetector
自定义手势识别器。onDragUpdate
)中执行耗时操作,必要时使用防抖/节流
。事件优先级体系:
\\n垂直拖动
> 水平拖动
> 通用拖动
> 点击事件
。GestureArena
)自动裁决冲突。坐标转换技巧:
\\nonTapDown: (details) {\\n final localPos = details.localPosition; // 相对于子组件的坐标\\n final globalPos = details.globalPosition; // 屏幕绝对坐标\\n}\\n
\\n复合手势策略:
\\n// 同时支持点击和长按\\nGestureDetector(\\n onTap: () => print(\'点击\'),\\n onLongPress: () => print(\'长按\'),\\n // 长按触发时不会触发点击\\n)\\n
\\nTap
:点击属性名称 | 类型 | 作用描述 | 适用场景 |
---|---|---|---|
onTapDown | GestureTapDownCallback | 当手指首次接触屏幕时触发(按下动作)。 | 需要立即响应按下动作(如按钮按下效果)。 |
onTapUp | GestureTapUpCallback | 当手指从屏幕抬起时触发(释放动作)。 | 需要在释放时执行操作(如松开按钮触发提交)。 |
onTap | GestureTapCallback | 点击完成时触发(按下并抬起)。 | 通用的点击交互(如打开页面、提交表单)。 |
onTapCancel | GestureTapCancelCallback | 点击动作被取消时触发(如滑动离开控件)。 | 取消点击反馈(如按钮按下后滑动取消)。 |
onSecondaryTap | GestureTapCallback | 次要按钮点击(如鼠标右键点击)完成时触发。 | 右键菜单、辅助操作(桌面端或触控板场景)。 |
onSecondaryTapDown | GestureTapDownCallback | 次要按钮按下时触发。 | 右键按下时的即时反馈(如高亮右键菜单项)。 |
onSecondaryTapUp | GestureTapUpCallback | 次要按钮抬起时触发。 | 右键释放时的操作(如显示菜单)。 |
onSecondaryTapCancel | GestureTapCancelCallback | 次要按钮点击取消时触发。 | 右键操作取消时恢复状态。 |
onTertiaryTapDown | GestureTapDownCallback | 第三按钮按下时触发(如鼠标中键)。 | 中键按下反馈(如快速滚动或自定义中键功能)。 |
onTertiaryTapUp | GestureTapUpCallback | 第三按钮抬起时触发。 | 中键释放时执行操作。 |
onTertiaryTapCancel | GestureTapCancelCallback | 第三按钮点击取消时触发。 | 中键操作取消时恢复状态。 |
Double Tap
:双击属性名称 | 类型 | 作用描述 | 适用场景 |
---|---|---|---|
onDoubleTapDown | GestureTapDownCallback | 双击时首次按下触发。 | 双击操作的按下反馈(如地图双击放大前的预加载)。 |
onDoubleTap | GestureTapCallback | 双击完成时触发。 | 双击交互(如缩放图片、快速确认操作)。 |
onDoubleTapCancel | GestureTapCancelCallback | 双击动作被取消时触发。 | 双击取消时恢复初始状态。 |
Long Press
:长按属性名称 | 类型 | 作用描述 | 适用场景 |
---|---|---|---|
onLongPressDown | GestureLongPressDownCallback | 长按动作的按下事件触发。 | 长按开始时的即时反馈(如显示提示)。 |
onLongPressCancel | GestureLongPressCancelCallback | 长按动作被取消时触发(如滑动离开控件)。 | 长按中途取消(如拖动取消长按菜单)。 |
onLongPress | GestureLongPressCallback | 长按触发时调用。 | 长按交互(如显示上下文菜单、进入编辑模式)。 |
onLongPressStart | GestureLongPressStartCallback | 长按开始并触发拖动时调用。 | 长按拖动起始点(如列表项拖动排序)。 |
onLongPressMoveUpdate | GestureLongPressMoveUpdateCallback | 长按拖动过程中位置更新时调用。 | 拖动时实时更新位置(如拖拽元素跟随手指移动)。 |
onLongPressUp | GestureLongPressUpCallback | 长按结束并抬起时调用。 | 长按释放后的操作(如完成拖动并保存位置)。 |
onLongPressEnd | GestureLongPressEndCallback | 长按拖动结束时调用。 | 拖动结束后执行逻辑(如触发动画或数据提交)。 |
onSecondaryLongPressDown | GestureLongPressDownCallback | 次要按钮长按按下时触发。 | 右键长按开始时的反馈(如桌面端长按右键)。 |
onSecondaryLongPressCancel | GestureLongPressCancelCallback | 次要按钮长按被取消时触发。 | 右键长按中途取消时的状态恢复。 |
onSecondaryLongPress | GestureLongPressCallback | 次要按钮长按触发时调用。 | 右键长按操作(如自定义右键长按菜单)。 |
onSecondaryLongPressStart | GestureLongPressStartCallback | 次要按钮长按拖动开始时调用。 | 右键长按拖动起始(如特定场景下的辅助拖动)。 |
onSecondaryLongPressMoveUpdate | GestureLongPressMoveUpdateCallback | 次要按钮长按拖动位置更新时调用。 | 右键拖动时实时更新位置。 |
onSecondaryLongPressUp | GestureLongPressUpCallback | 次要按钮长按抬起时调用。 | 右键长按释放后的操作。 |
onSecondaryLongPressEnd | GestureLongPressEndCallback | 次要按钮长按拖动结束时调用。 | 右键拖动结束后的逻辑处理。 |
onTertiaryLongPressDown | GestureLongPressDownCallback | 第三按钮长按按下时触发。 | 中键长按开始时的反馈(如自定义中键长按功能)。 |
onTertiaryLongPressCancel | GestureLongPressCancelCallback | 第三按钮长按被取消时触发。 | 中键长按中途取消时的状态恢复。 |
onTertiaryLongPress | GestureLongPressCallback | 第三按钮长按触发时调用。 | 中键长按操作(如特定设备的中键功能)。 |
onTertiaryLongPressStart | GestureLongPressStartCallback | 第三按钮长按拖动开始时调用。 | 中键长按拖动起始点。 |
onTertiaryLongPressMoveUpdate | GestureLongPressMoveUpdateCallback | 第三按钮长按拖动位置更新时调用。 | 中键拖动时实时更新位置。 |
onTertiaryLongPressUp | GestureLongPressUpCallback | 第三按钮长按抬起时调用。 | 中键长按释放后的操作。 |
onTertiaryLongPressEnd | GestureLongPressEndCallback | 第三按钮长按拖动结束时调用。 | 中键拖动结束后的逻辑处理。 |
Drag
:拖动属性名称 | 类型 | 作用描述 | 适用场景 |
---|---|---|---|
onVerticalDragDown | GestureDragDownCallback | 垂直拖动按下时触发。 | 垂直拖动起始(如上下滑动列表)。 |
onVerticalDragStart | GestureDragStartCallback | 垂直拖动开始时触发。 | 垂直拖动开始时的逻辑(如记录初始位置)。 |
onVerticalDragUpdate | GestureDragUpdateCallback | 垂直拖动位置更新时触发。 | 实时更新垂直位置(如滑动进度条、滚动视图)。 |
onVerticalDragEnd | GestureDragEndCallback | 垂直拖动结束时触发。 | 垂直拖动结束后的操作(如惯性滚动、数据保存)。 |
onVerticalDragCancel | GestureDragCancelCallback | 垂直拖动被取消时触发。 | 垂直拖动中途取消(如被其他手势打断)。 |
onHorizontalDragDown | GestureDragDownCallback | 水平拖动按下时触发。 | 水平拖动起始(如左右滑动切换页面)。 |
onHorizontalDragStart | GestureDragStartCallback | 水平拖动开始时触发。 | 水平拖动开始时的逻辑(如记录初始位置)。 |
onHorizontalDragUpdate | GestureDragUpdateCallback | 水平拖动位置更新时触发。 | 实时更新水平位置(如滑动卡片、横向导航)。 |
onHorizontalDragEnd | GestureDragEndCallback | 水平拖动结束时触发。 | 水平拖动结束后的操作(如页面切换动画)。 |
onHorizontalDragCancel | GestureDragCancelCallback | 水平拖动被取消时触发。 | 水平拖动中途取消(如手势冲突)。 |
onPanDown | GestureDragDownCallback | 平移拖动(无方向限制)按下时触发。 | 自由拖动的起始(如地图拖拽、元素自由移动)。 |
onPanStart | GestureDragStartCallback | 平移拖动开始时触发。 | 自由拖动开始时的逻辑(如记录初始坐标)。 |
onPanUpdate | GestureDragUpdateCallback | 平移拖动位置更新时触发。 | 实时更新拖动位置(如拖拽元素自由移动)。 |
onPanEnd | GestureDragEndCallback | 平移拖动结束时触发。 | 自由拖动结束后的操作(如元素归位或保存位置)。 |
onPanCancel | GestureDragCancelCallback | 平移拖动被取消时触发。 | 自由拖动中途取消(如手势中断)。 |
Scale
:缩放属性名称 | 类型 | 作用描述 | 适用场景 |
---|---|---|---|
onScaleStart | GestureScaleStartCallback | 缩放手势开始时触发(如双指接触屏幕)。 | 双指缩放的起始(如图片缩放、画布放大)。 |
onScaleUpdate | GestureScaleUpdateCallback | 缩放手势更新时触发(如双指移动)。 | 实时更新缩放比例(如动态调整视图大小)。 |
onScaleEnd | GestureScaleEndCallback | 缩放手势结束时触发。 | 缩放结束后的逻辑(如保存缩放比例、重置动画)。 |
Force Press
:压力感应属性名称 | 类型 | 作用描述 | 适用场景 |
---|---|---|---|
onForcePressStart | GestureForcePressStartCallback | 压力感应按下时触发(支持压感设备)。 | 压感设备按下时的反馈(如3D Touch预览)。 |
onForcePressPeak | GestureForcePressPeakCallback | 压力达到峰值时触发。 | 压感峰值操作(如触发快捷菜单)。 |
onForcePressUpdate | GestureForcePressUpdateCallback | 压力值更新时触发。 | 实时响应压力变化(如绘图应用的笔压感应)。 |
onForcePressEnd | GestureForcePressEndCallback | 压力感应结束时触发。 | 压感释放后的操作(如关闭预览或提交数据)。 |
Behavior Control
:行为控制属性名称 | 类型 | 作用描述 | 适用场景 |
---|---|---|---|
behavior | HitTestBehavior? | 控制手势检测的命中测试行为(如是否透传事件)。 | 解决手势冲突(如嵌套可点击控件时的透传策略)。 |
excludeFromSemantics | bool | 是否从语义树中排除,默认false 。 | 无障碍功能适配(如隐藏非交互元素的语义节点)。 |
dragStartBehavior | DragStartBehavior | 拖动开始的触发时机(start 或down ),默认DragStartBehavior.start 。 | 控制拖动灵敏度(如立即响应拖动或延迟触发)。 |
trackpadScrollCausesScale | bool | 是否将触控板滚动事件视为缩放手势,默认false 。 | 适配触控板交互(如触控板双指滚动触发缩放)。 |
trackpadScrollToScaleFactor | double | 触控板滚动转换为缩放的系数,默认kDefaultTrackpadScrollToScaleFactor 。 | 调整触控板缩放的灵敏度。 |
supportedDevices | Set<PointerDeviceKind>? | 指定支持手势的输入设备类型(如鼠标、触控笔)。 | 限制特定设备的交互(如仅响应触控笔或鼠标事件)。 |
onTap: () => print(\'短按触发\'),\\nonDoubleTap: () => print(\'双击触发\'),\\nonLongPress: () => print(\'长按触发\'),\\n
\\nonTapDown
→ onTapUp
→ onTap
(成功点击)。onTapCancel
(中断时触发)。onTapDown
→ onTapUp
→ onTap
→ onDoubleTap
。onLongPress
和onTap
时,长按触发后onTap
不再触发。onPanStart: (d) => print(\'开始拖动\'),\\nonPanUpdate: (d) => print(\'拖动中 delta:${d.delta}\'),\\nonPanEnd: (d) => print(\'拖动结束 velocity:${d.velocity}\'),\\n
\\nonPan
:通用任意方向拖动。onHorizontalDrag
:水平方向专属。onVerticalDrag
:垂直方向专属。delta
:两次事件之间的偏移量。velocity
:释放时的速度向量。global/localPosition
:触点坐标转换。behavior: HitTestBehavior.opaque,\\ndragStartBehavior: DragStartBehavior.down,\\nexcludeFromSemantics: true,\\n
\\n点击测试策略
):\\nopaque
:阻止子树接收事件(默认)。translucent
:允许事件穿透但自身仍响应。deferToChild
:由子组件决定是否响应。拖动触发时机
)。\\ndown
:手指接触屏幕立即触发(更灵敏
)。start
:移动超过阈值才触发(避免误触
)。onScaleStart: (d) => print(\'缩放开始\'),\\nonScaleUpdate: (d) => print(\'缩放比例:${d.scale}\'),\\nonScaleEnd: (d) => print(\'缩放结束\'),\\n
\\nScaleGestureRecognizer
:\\nscale
:当前缩放系数(初始为1.0
)。focalPoint
:双指中心点坐标。rotation
:旋转角度变化量。GestureDetector(\\n onVerticalDragUpdate: (d) => print(\'垂直拖动\'),\\n onPanUpdate: (d) => print(\'通用拖动\'),\\n child: Container(),\\n)\\n
\\n垂直/水平拖动
> 通用拖动
> 点击
。GestureDetector(\\n onTap: () => debugPrintGestureArena(SystemGestureArenaCls.debugPrintActiveArena),\\n)\\n
\\n\\n\\n事件流传递路径:
\\n\\n
PointerEvent
→HitTest
→GestureArena
→Recognizer
→Callback
竞技场生命周期:
\\n1、当第一个
\\nPointerDown
事件发生时,竞技场开启。2、各
\\nGestureRecognizer
声明参与竞争。3、当确定唯一胜出者(如
\\n拖动超过阈值
)或竞技场关闭时触发回调。4、通过
\\nGestureDisposition.accept/reject
控制裁决。
需求:实现列表项的自由拖拽,并通过手势动态调整位置。
\\nimport \'package:flutter/material.dart\';\\n\\nclass DragSortListView extends StatefulWidget {\\n@override\\n_DragSortListViewState createState() => _DragSortListViewState();\\n}\\n\\nclass _DragSortListViewState extends State<DragSortListView> {\\nfinal List<String> _items = [\\n \'Item 1\',\\n \'Item 2\',\\n \'Item 3\',\\n \'Item 4\',\\n \'Item 5\'\\n];\\n\\n@override\\nWidget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: Text(\'ListView拖拽排序\'),\\n backgroundColor: Theme.of(context).colorScheme.inversePrimary,\\n ),\\n body: ReorderableListView(\\n padding: EdgeInsets.all(16),\\n children: [\\n for (int index = 0; index < _items.length; index++)\\n _buildListItem(index),\\n ],\\n onReorder: (oldIndex, newIndex) {\\n // 处理索引越界\\n if (newIndex > _items.length) newIndex = _items.length;\\n if (oldIndex < newIndex) newIndex--;\\n\\n setState(() {\\n final item = _items.removeAt(oldIndex);\\n _items.insert(newIndex, item);\\n });\\n },\\n ),\\n );\\n}\\n\\nWidget _buildListItem(int index) {\\n return Container(\\n key: ValueKey(\'$index\'), // 必须设置唯一key\\n margin: EdgeInsets.only(bottom: 8),\\n decoration: BoxDecoration(\\n color: Colors.blue,\\n borderRadius: BorderRadius.circular(8),\\n boxShadow: [\\n BoxShadow(\\n color: Colors.black12,\\n blurRadius: 4,\\n offset: Offset(0, 2),\\n ),\\n ],\\n ),\\n child: ListTile(\\n contentPadding: EdgeInsets.symmetric(horizontal: 16),\\n leading: Icon(Icons.drag_handle, color: Colors.white), // 拖拽手柄\\n title: Text(\\n _items[index],\\n style: TextStyle(color: Colors.white, fontSize: 16),\\n ),\\n trailing: Icon(Icons.menu, color: Colors.white),\\n ),\\n );\\n}\\n}\\n
\\n技术要点:
\\nReorderableListView
替代普通 ListView
,自动处理拖拽手势:\\nonReorder
:拖拽完成时的回调,自动处理 oldIndex
和 newIndex
。Key
(示例使用 ValueKey
)。Icon(Icons.drag_handle)
)提示可拖拽。阴影
和圆角
提升视觉层次感。onReorder
中处理索引越界问题,确保列表操作安全。需求:支持双指缩放图片,并允许单指拖动查看细节。
\\nimport \'package:flutter/material.dart\';\\n\\nclass ZoomableImage extends StatefulWidget {\\n @override\\n _ZoomableImageState createState() => _ZoomableImageState();\\n}\\n\\nclass _ZoomableImageState extends State<ZoomableImage> {\\n double _scale = 1.0;\\n Offset _offset = Offset.zero;\\n Offset _initialOffset = Offset.zero;\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\'双指缩放\')),\\n body: GestureDetector(\\n onScaleStart: (details) {\\n _initialOffset = _offset;\\n },\\n onScaleUpdate: (details) {\\n setState(() {\\n _scale = details.scale.clamp(1.0, 4.0); // 限制缩放范围\\n _offset = _initialOffset + details.focalPointDelta;\\n });\\n },\\n onDoubleTap: () {\\n setState(() {\\n _scale = _scale == 1.0 ? 2.0 : 1.0; // 双击切换缩放\\n _offset = Offset.zero;\\n });\\n },\\n child: Transform.scale(\\n scale: _scale,\\n child: Transform.translate(\\n offset: _offset,\\n child: Image.network(\'https://picsum.photos/800/600\'),\\n ),\\n ),\\n ),\\n );\\n }\\n}\\n
\\n技术要点:
\\nonScaleUpdate
处理缩放和位移。clamp
限制缩放范围。onDoubleTap
重置缩放状态。需求:长按元素时显示浮动菜单,支持点击菜单项操作。
\\nimport \'package:flutter/material.dart\';\\n\\nclass ContextMenuDemo extends StatefulWidget {\\n @override\\n _ContextMenuDemoState createState() => _ContextMenuDemoState();\\n}\\n\\nclass _ContextMenuDemoState extends State<ContextMenuDemo> {\\n Offset? _tapPosition;\\n bool _showMenu = false;\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\'长按菜单\')),\\n body: GestureDetector(\\n onLongPressStart: (details) {\\n setState(() {\\n _tapPosition = details.globalPosition;\\n _showMenu = true;\\n });\\n _showContextMenu(context, _tapPosition!);\\n },\\n child: Container(\\n color: Colors.grey[200],\\n alignment: Alignment.center,\\n child: FlutterLogo(size: 200),\\n ),\\n ),\\n );\\n }\\n\\n void _showContextMenu(BuildContext context, Offset position) {\\n showMenu(\\n context: context,\\n position: RelativeRect.fromLTRB(\\n position.dx,\\n position.dy,\\n position.dx,\\n position.dy,\\n ),\\n items: [\\n PopupMenuItem(child: Text(\'复制\'), value: \'copy\'),\\n PopupMenuItem(child: Text(\'分享\'), value: \'share\'),\\n PopupMenuItem(child: Text(\'删除\'), value: \'delete\'),\\n ],\\n ).then((value) {\\n if (value != null) {\\n ScaffoldMessenger.of(context)\\n ..hideCurrentSnackBar()\\n ..showSnackBar(\\n SnackBar(\\n content: Text(\'选中: $value\'),\\n ),\\n );\\n }\\n setState(() => _showMenu = false);\\n });\\n }\\n}\\n
\\n实现要点:
\\nonLongPressStart
获取长按位置。showMenu
显示 Material Design
风格菜单。GestureDetector
的本质是Flutter
手势系统的语法糖,其强大之处在于将底层PointerEvent
转化为语义化手势的抽象能力。真正精通的标志是能预见性地处理手势冲突
,并设计出符合人体工学的交互方案
。记住三个黄金法则:
shouldRecognize
参数)。当你能在脑海中构建出从手指触屏到Widget
重绘的完整事件流图谱时,就真正系统化掌握了这一核心交互组件。这不仅是技术的精进
,更是对用户体验本质的深刻理解
。
\\n","description":"前言 在移动应用开发中,用户与界面之间的手势交互如同人类对话时的肢体语言,是构建自然用户体验的核心要素。GestureDetector作为Flutter手势系统的基石组件,其设计哲学在于将复杂的触控事件抽象为语义化的手势回调,让开发者能够用声明式语法捕获用户交互意图。\\n\\n不同于Android的View.OnClickListener或iOS的UIGestureRecognizer,它通过分层的事件处理模型和智能手势竞争裁决机制,实现了跨平台手势交互的统一抽象。\\n\\n掌握该组件不仅能提升界面交互的精细度,更能深入理解Flutter框架的事件分发体系,这是构建复杂…","guid":"https://juejin.cn/post/7479995740031926335","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-10T13:20:09.667Z","media":null,"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"败走麦城——HTTP 请求触发 RST","url":"https://juejin.cn/post/7480037582354989095","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
通过 dio 实现 HTTP 下位机控制功能的场景中,发现对某个控制接口短时间连续发送请求控制指令无法生效,进行抓包排查发现存在 RST 报文;
\\n关键包如下
\\n根据报文数据,关键流程如下(数据时间单位为秒):
\\nsequenceDiagram\\n participant C as 客户端 (10.40.84.77)\\n participant S as 服务器 (10.40.82.75)\\n \\n C->>S: PUT /put/ctrl (Packet 2315)\\n S--\x3e>C: HTTP 200 OK (Packet 2320)\\n C->>S: GET /query/state (Packet 2322)\\n S--\x3e>C: FIN, ACK (Packet 2323)\\n C->>S: ACK (Packet 2324)\\n C->>S: FIN, ACK (Packet 2325)\\n S--\x3e>C: RST (Packet 2326-2328)\\n\\n
\\n主要问题:
\\n客户端在服务器已发 FIN 后仍然发送新的请求(GET 请求)。
\\nTCP 协议规定,一旦一端发送 FIN 表示该端不再发送数据,新的请求不应在该连接上发送。客户端继续使用该连接发送数据,会被服务器视为协议违规,导致服务器立即发送 RST 重置连接。
引发原因:
\\n端口分析:
\\n服务器在包 2323
中已经关闭了写端(发送 FIN),表示不再接收新数据。如果客户端在此后尝试在同一连接上发送 GET 请求,就会违反 TCP 协议,导致服务器直接回复 RST 来拒绝数据。
sequenceDiagram\\n participant C as 客户端 (192.168.0.77, SrcPort:55750)\\n participant S as 服务端 (192.168.0.75, DestPort:8080)\\n\\n C->>S: SYN (建立连接)\\n S--\x3e>C: SYN, ACK\\n C->>S: ACK\\n Note over C,S: 连接建立成功\\n\\n C->>S: PUT /put/ctrl (使用端口 55750)\\n S--\x3e>C: HTTP 200 OK\\n Note over S: 服务器处理完请求后,开始关闭连接\\n S--\x3e>C: FIN, ACK (关闭发送方向) 包 2323\\n\\n Note over C: 客户端应检测到 FIN,停止发送数据并新建连接\\n C->>S: GET /query/state (重用端口 55750) 包 2322\\n \\n Note over S: 服务端检测到异常数据(在半关闭连接上收到数据)\\n S--\x3e>C: RST (重置连接,拒绝异常数据) 包 2326-2328\\n\\n
\\n根据抓包数据,关键步骤如下:
\\nSO_REUSEADDR
,允许服务端直接复用处于 TIME_WAIT 的端口;优化层级 | 措施 | 技术实现 | 适用场景 |
---|---|---|---|
客户端 | 连接池+自动重试 | OkHttp/Dio连接池配置 | 移动端/高并发Web服务 |
服务端 | 日志监控+超时策略 | Prometheus/Grafana监控工具 | 服务器集群/边缘计算节点 |
通过以上策略,可以有效降低 RST 错误的发生率,提升系统的稳定性和用户体验。
\\n主要问题在于服务端本身未支持长链接,而客户端未正确处理服务器发送的 FIN 信号,错误地在半关闭连接通道上继续发送数据,从而引发服务端返回 RST。
\\n优化建议:
\\n这种优化能确保连接管理正确、网络通信稳定,减少因为连接状态异常引发的重置问题,进而提升整体多端智能反馈系统的可靠性。
","description":"HTTP 请求触发 RST 排查 1.问题描述\\n\\n通过 dio 实现 HTTP 下位机控制功能的场景中,发现对某个控制接口短时间连续发送请求控制指令无法生效,进行抓包排查发现存在 RST 报文;\\n\\n问题报文\\n\\n关键包如下\\n\\n根据报文数据,关键流程如下(数据时间单位为秒):\\n\\nPacket 2315(6.016444s):客户端(192.168.0.77)发送 PUT 请求到服务器(192.168.0.75)。\\nPacket 2320(6.036431s):服务器发送 HTTP 200 OK 响应,此时连接仍处于活动状态。\\nPacket 2322(6…","guid":"https://juejin.cn/post/7480037582354989095","author":"人形打码机","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-10T12:36:14.582Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c911927f8e3a4ed9a889c5a4ae46483b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Lq65b2i5omT56CB5py6:q75.awebp?rk3s=f64ab15b&x-expires=1742214973&x-signature=BJ1Mq1KhC1SiT6PpiVJ%2BLFyKhvk%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["后端","网络协议","TCP/IP","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"flutter rust bridge 编译成so 文件 或者 .a文件 依赖到主项目","url":"https://juejin.cn/post/7479808938047373366","content":"安装编译链\\nrustup target add aarch64-linux-android armv7-linux-androideabi\\n\\naarch64-linux-android 用于输出arm64-v8a的.so文件\\narmv7-linux-androideabi 用于输出armeabi-v7a的.so文件\\n您可以通过rustup target list查看所有支持的工具链.\\n
\\n### 安装编译工具\\n\\n`cargo install cargo-ndk`\\n\\n- `cargo-ndk` 用来编译so文件\\n
\\ncargo ndk -t armeabi-v7a -t arm64-v8a build --release\\n
\\n关于cargo ndk
更多用法可以参考: github.com/bbqsrc/carg…
arm64-v8a
平台的so文件输出在target/aarch64-linux-android/release/xxx.so
armeabi-v7a
平台的so文件输出在target/armv7-linux-androideabi/release/xxx.so
参考文章 blog.csdn.net/angcyo/arti…
\\n按照上面的流程 android 的 so文件就可以打包出来了。
\\n提问ai ai给了另一套方式:
\\n# armv7 支持\\nexport CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER=\\"$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin/armv7a-linux-androideabi24-clang\\"\\nexport CC_armv7_linux_androideabi=\\"$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin/armv7a-linux-androideabi24-clang\\"\\nexport AR_armv7_linux_androideabi=\\"$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-ar\\"\\n\\n# x86_64 支持\\nexport CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER=\\"$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin/x86_64-linux-android24-clang\\"\\nexport CC_x86_64_linux_android=\\"$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin/x86_64-linux-android24-clang\\"\\nexport AR_x86_64_linux_android=\\"$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-ar\\"\\n
\\n在 .bash_profile 或者.zprofile 中添加这个
\\n然后通过 这个命令也可以打出so 文件
\\ncargo build --release --target armv7-linux-androideabi\\ncargo build --release --target x86_64-linux-android\\n
\\nso文件弄出来之后 直接Android 引入就可以了。
\\n对于ios 直接用下面的命令 就可以生成 .a 文件 但是我没引入成功 有大佬引入成功 麻烦评论区指点一下
\\ncargo build --release --target aarch64-apple-ios\\ncargo build --release --target x86_64-apple-ios\\nlipo -create \\\\\\n target/aarch64-apple-ios/release/libvideo_compressor.a \\\\\\n target/x86_64-apple-ios/release/libvideo_compressor.a \\\\\\n -output ios/libvideo_compressor.a\\n
","description":"安装编译链 rustup target add aarch64-linux-android armv7-linux-androideabi\\n\\naarch64-linux-android 用于输出arm64-v8a的.so文件\\narmv7-linux-androideabi 用于输出armeabi-v7a的.so文件\\n您可以通过rustup target list查看所有支持的工具链.\\n\\n### 安装编译工具\\n\\n`cargo install cargo-ndk`\\n\\n- `cargo-ndk` 用来编译so文件\\n\\ncargo ndk -t armeabi…","guid":"https://juejin.cn/post/7479808938047373366","author":"火柴就是我","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-10T09:02:53.649Z","media":null,"categories":["Android","Flutter","Rust","iOS"],"attachments":null,"extra":null,"language":null},{"title":"系统化掌握Flutter组件之Transform:空间魔法师","url":"https://juejin.cn/post/7479705953736474676","content":"在Flutter
的视觉王国里,Transform
组件如同一位掌握空间法则的魔术师,它能够突破常规布局的维度限制,让UI
元素在二维平面甚至三维空间中自由变形。作为Flutter
框架中最强大的几何变换工具,Transform
通过矩阵运算实现了平移
、旋转
、缩放
、斜切
等基础变换,更支持自定义矩阵完成复杂形变。
其设计初衷是赋予开发者对UI
元素的绝对控制权,无论是实现微妙的交互动画,还是构建惊艳的3D
效果,Transform
都能游刃有余。掌握Transform
不仅能提升视觉表现力,更能深入理解Flutter
的渲染机制。
本文将通过系统化的知识架构,带你从基础属性认知到高阶实战应用,彻底征服这个布局神器。
\\n操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\n属性类型 | 核心属性 | 数学原理 | 典型场景 |
---|---|---|---|
快捷构造 | Transform.translate | 位移矩阵构造 | 元素位置微调 |
Transform.rotate | 旋转矩阵构造 | 卡片翻转/仪表盘指针 | |
Transform.scale | 缩放矩阵构造 | 按钮点击反馈 | |
基础变换 | transform(Matrix4) | 齐次坐标矩阵 | 自定义复杂形变 |
快捷方法 | origin/alignment | 局部坐标系转换 | 调整变换基准点 |
、Transform.translate
: 平移变换// 平移变换\\nTransform.translate(\\n offset: Offset(50, 30), // X/Y偏移量\\n child: /* 子组件 */\\n)\\n
\\n注意事项:
\\nMatrix4.translationValues
。Positioned
更底层,不受Stack
布局限制。、Transform.rotate
: 旋转变换// 旋转变换\\nTransform.rotate(\\n angle: 0.5, // 旋转弧度(π≈3.14)\\n origin: Offset(25,25),// 旋转中心点\\n child: /* 子组件 */\\n)\\n
\\n注意事项:
\\nXYZ
三轴旋转(默认Z
轴)。pi = 180度
)。、Transform.scale
: 缩放变换// 缩放变换\\nTransform.scale(\\n scale: 0.7, // 缩放比例\\n origin: Offset(0,0), // 缩放原点\\n child: /* 子组件 */\\n)\\n
\\n注意事项:
\\nscaleX
, scaleY
)。可能影响性能
)。、Transform.skew/skewX/skewY
: 斜切变换// 斜切变换\\nTransform(\\n transform: Matrix4.skewX(0.3), // X轴斜切\\n alignment: Alignment.center,\\n child: /* 子组件 */\\n)\\n
\\ntransform
(Matrix4
):基础变换Matrix4
的底层结构Matrix4
是 4x4
的变换矩阵,对应 OpenGL
的矩阵规范:\\n\\n通过
setEntry(row, column, value)
方法修改特定位置的元素值。
透视效果的本质是通过投影矩阵将 3D
坐标映射到 2D
屏幕,其核心公式为:\\nz′=1/(1+d⋅z)
\\n其中:
d
= 透视强度(示例中的 0.005
).z
= 物体在 Z
轴的位置.当 setEntry(3, 2, d)
时,相当于在矩阵第四行第三列(索引从 0
开始)设置透视参数:
通过调整 d
的值可以控制透视强度:
d 值范围 | 视觉效果 |
---|---|
0.001~0.01 | 轻微透视(适合小范围旋转 ) |
0.01~0.1 | 强烈鱼眼效果 |
0 (默认) | 正交投影(无透视变形) |
示例对比:
\\ndart\\n// 无透视效果\\nMatrix4.identity()\\n ..rotateX(0.5)\\n\\n// 有透视效果 \\nMatrix4.identity()\\n ..setEntry(3, 2, 0.005)\\n ..rotateX(0.5)\\n
\\nTransform(\\n transform: Matrix4.identity()\\n ..translate(50.0, 100.0) // 第三步:平移\\n ..rotateZ(pi/4) // 第二步:绕Z轴旋转\\n ..scale(2.0), // 第一步:放大2倍\\n child: FlutterLogo(),\\n)\\n\\n// 3D变换\\nMatrix4.identity()\\n ..setEntry(3, 2, 0.005) // 设置透视效果\\n ..rotateX(0.5) // X轴旋转\\n ..rotateY(0.3) // Y轴旋转\\n\\n// 自定义矩阵\\nMatrix4(\\n 2.0, 0.5, 0.0, 0.0, // 第一列\\n 0.3, 1.0, 0.0, 0.0, // 第二列\\n 0.0, 0.0, 1.0, 0.0, // 第三列\\n 0.0, 0.0, 0.0, 1.0, // 第四列\\n)\\n
\\n变换顺序规则:
\\n缩放
→ 旋转
→ 平移
。注意事项:
\\nMatrix4
实例(创建新对象更安全
)。..setEntry(3, 2, 0.001)
)。HitTest
区域)。origin/alignment
:快捷方法对比解析表:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n参数 | 坐标系类型 | 影响范围 | 数学意义 |
---|---|---|---|
origin | 子组件局部坐标系 | 变换基准点位置 | 相当于先平移再应用变换 |
alignment | 父组件全局坐标系 | 子组件的对齐方式 | 修改父级坐标系原点位置 |
视觉化示例:
\\n// origin示例:围绕Logo左上角旋转\\nTransform.rotate(\\n angle: pi/4,\\n origin: Offset(0, 0),\\n child: FlutterLogo(),\\n)\\n\\n// alignment示例:在父容器中心旋转\\nContainer(\\n alignment: Alignment.center,\\n child: Transform.rotate(\\n angle: pi/4,\\n child: FlutterLogo(),\\n ),\\n)\\n
\\n黄金法则:
\\norigin
。alignment
。3D
卡片翻转效果import \'dart:math\';\\nimport \'package:flutter/material.dart\';\\n\\nclass FlipCardWidget extends StatefulWidget {\\n const FlipCardWidget();\\n\\n @override\\n State<FlipCardWidget> createState() => _FlipCardWidgetState();\\n}\\n\\nclass _FlipCardWidgetState extends State<FlipCardWidget>\\n with SingleTickerProviderStateMixin {\\n late AnimationController _controller;\\n bool _isFront = true;\\n\\n @override\\n void initState() {\\n super.initState();\\n _controller = AnimationController(\\n vsync: this,\\n duration: const Duration(milliseconds: 600),\\n );\\n }\\n\\n void _toggleCard() {\\n if (_isFront) {\\n _controller.forward();\\n } else {\\n _controller.reverse();\\n }\\n _isFront = !_isFront;\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: Text(\\"Transform Demo\\"),\\n backgroundColor: Theme.of(context).colorScheme.inversePrimary,\\n ),\\n body: Center(\\n child: Column(\\n children: [\\n GestureDetector(\\n onTap: _toggleCard,\\n child: AnimatedBuilder(\\n animation: _controller,\\n builder: (context, child) {\\n final angle = _controller.value * pi;\\n return Transform(\\n transform: Matrix4.identity()\\n ..setEntry(3, 2, 0.001) // 透视效果\\n ..rotateY(angle),\\n alignment: Alignment.center,\\n child: IndexedStack(\\n index: _controller.value < 0.5 ? 0 : 1,\\n children: [\\n _CardFace(\\n color: Colors.blue,\\n text: \'Front\',\\n visible: _controller.value < 0.5,\\n ),\\n _CardFace(\\n color: Colors.red,\\n text: \'Back\',\\n visible: _controller.value >= 0.5,\\n ),\\n ],\\n ),\\n );\\n },\\n ),\\n ),\\n ],\\n ),\\n ),\\n );\\n }\\n\\n @override\\n void dispose() {\\n _controller.dispose();\\n super.dispose();\\n }\\n}\\n\\nclass _CardFace extends StatelessWidget {\\n final Color color;\\n final String text;\\n final bool visible;\\n\\n const _CardFace({\\n required this.color,\\n required this.text,\\n required this.visible,\\n });\\n\\n @override\\n Widget build(BuildContext context) {\\n return Visibility(\\n visible: visible,\\n child: Container(\\n width: 200,\\n height: 300,\\n decoration: BoxDecoration(\\n color: color,\\n borderRadius: BorderRadius.circular(16),\\n boxShadow: [\\n BoxShadow(\\n color: Colors.black.withValues(alpha: 0.3),\\n blurRadius: 12,\\n offset: const Offset(4, 6),\\n )\\n ],\\n ),\\n child: Center(\\n child: Text(\\n text,\\n style: const TextStyle(\\n fontSize: 32,\\n color: Colors.white,\\n fontWeight: FontWeight.bold,\\n ),\\n ),\\n ),\\n ),\\n );\\n }\\n}\\n
\\n技术要点:
\\nMatrix4.rotateY
实现绕Y轴旋转。IndexedStack
控制正反面切换时机。setEntry(3, 2, 0.001)
设置透视投影。Visibility
组件优化渲染性能。import \'dart:math\';\\nimport \'package:flutter/material.dart\';\\n\\nclass ComplexAnimation extends StatefulWidget {\\n const ComplexAnimation();\\n\\n @override\\n State<ComplexAnimation> createState() => _ComplexAnimationState();\\n}\\n\\nclass _ComplexAnimationState extends State<ComplexAnimation>\\n with TickerProviderStateMixin {\\n late AnimationController _controller;\\n late Animation<double> _rotate;\\n late Animation<Offset> _translate;\\n late Animation<double> _scale;\\n\\n @override\\n void initState() {\\n super.initState();\\n _controller = AnimationController(\\n vsync: this,\\n duration: const Duration(seconds: 2),\\n )..repeat(reverse: true);\\n\\n _rotate = Tween(begin: 0.0, end: 2 * pi)\\n .animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));\\n\\n _translate = Tween<Offset>(\\n begin: const Offset(-1.5, 0.0),\\n end: const Offset(1.5, 0.0),\\n ).animate(CurvedAnimation(\\n parent: _controller,\\n curve: Curves.fastOutSlowIn,\\n ));\\n\\n _scale = Tween(begin: 0.5, end: 1.5)\\n .animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));\\n }\\n\\n @override\\n void dispose() {\\n _controller.dispose();\\n super.dispose();\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: Text(\\"Transform Demo\\"),\\n backgroundColor: Theme.of(context).colorScheme.inversePrimary,\\n ),\\n body: Center(\\n child: Column(\\n children: [\\n buildAnimatedBuilder(),\\n ],\\n ),\\n ),\\n );\\n }\\n\\n AnimatedBuilder buildAnimatedBuilder() {\\n return AnimatedBuilder(\\n animation: _controller,\\n builder: (context, child) {\\n return Transform.translate(\\n offset: _translate.value * 100,\\n child: Transform(\\n transform: Matrix4.identity()\\n ..rotateZ(_rotate.value)\\n ..scale(_scale.value),\\n alignment: Alignment.center,\\n child: Container(\\n width: 100,\\n height: 100,\\n decoration: BoxDecoration(\\n gradient: LinearGradient(\\n colors: [Colors.blue, Colors.purple],\\n begin: Alignment.topLeft,\\n end: Alignment.bottomRight,\\n ),\\n borderRadius: BorderRadius.circular(20),\\n boxShadow: [\\n BoxShadow(\\n color: Colors.black.withValues(alpha: 0.3),\\n blurRadius: 10,\\n offset: Offset(0, _scale.value * 5),\\n )\\n ],\\n ),\\n child: Icon(\\n Icons.star,\\n color: Colors.amber,\\n size: 40 * _scale.value,\\n ),\\n ),\\n ),\\n );\\n },\\n );\\n }\\n}\\n
\\n技术要点:
\\n旋转
、平移
、缩放
三种动画。CurvedAnimation
实现非线性动画效果。Tween
创建不同属性的动画区间。阴影
和图标尺寸
。AnimatedBuilder
优化局部重建。Transform
的本质是Flutter
世界的空间操纵法则,它赋予开发者突破维度限制的创造自由。系统化掌握其技术脉络需要建立三维思维:在基础层
深入理解矩阵运算原理,在应用层
熟练运用各类快捷方法,在架构层
能将变换逻辑与动画、手势等系统有机结合。真正的精通体现在能预判变换叠加的视觉结果,并合理选择实现路径。
建议开发者将Transform
视为视觉问题的数学解算器,而非简单的布局工具。每一次成功的UI
变形,都是对渲染管线的一次优雅操控。保持对变换顺序的敏感,培养坐标系直觉,你将成为Flutter
世界的空间魔术师。
\\n","description":"前言 在Flutter的视觉王国里,Transform组件如同一位掌握空间法则的魔术师,它能够突破常规布局的维度限制,让UI元素在二维平面甚至三维空间中自由变形。作为Flutter框架中最强大的几何变换工具,Transform通过矩阵运算实现了平移、旋转、缩放、斜切等基础变换,更支持自定义矩阵完成复杂形变。\\n\\n其设计初衷是赋予开发者对UI元素的绝对控制权,无论是实现微妙的交互动画,还是构建惊艳的3D效果,Transform都能游刃有余。掌握Transform不仅能提升视觉表现力,更能深入理解Flutter的渲染机制。\\n\\n本文将通过系统化的知识架构…","guid":"https://juejin.cn/post/7479705953736474676","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-10T07:20:40.309Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/63a4838483514fb1a9e82de28e4ede89~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1742196039&x-signature=YRisWsv3YpFsPqluELo1Kl0S43w%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1262aaa414b14a44af12bf20a49eb840~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1742196039&x-signature=uZ9JoUz1fn8iKY%2BSBuKNZsKvxHI%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"一个多功能的GetX 项目代码生成工具","url":"https://juejin.cn/post/7479625267029458971","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
兄弟们,好久不见,上班啦,但生命不止,学习不停!今天跟大家分享一个flutter开发很好用的拓展插件,能提高开发效率。
\\n目录上右键菜单操作
\\n
请将你的 图片、Svg 放到目录
\\nassets/images/\\nassets/svgs/\\n
\\n准备好你的 assets/images/3.0x 图片\\n
右键点击菜单 Assets: Images x1 x2 Generate\\n
成功生成了 2.0x 文件夹,和 1x 的图片\\n
点击 Assets: Images x1 x2 Generate 同时会生成常量列表文件 files.txt
\\n文件位置
\\nassets/images/files.txt\\nassets/svgs/files.txt\\n
\\n所以你的 图片 svg 要放到指定位置\\n
生成 files.txt 常量列表\\n
如果你把 svg 放到 assets/svgs 这个目录下,也会生成常量列表\\n
自动创建开发目录\\n
只有 controller、view 两个文件\\n
推荐用这种,简单快速,自带自动释放控制器,GetBuilder 方式对性能也好。
\\nview
\\nimport \'package:flutter/material.dart\';\\nimport \'package:get/get.dart\';\\n\\nimport \'index.dart\';\\n\\nclass AbcPage extends GetView<AbcController> {\\n const AbcPage({Key? key}) : super(key: key);\\n\\n Widget _buildView() {\\n return Container();\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return GetBuilder<AbcController>(\\n init: AbcController(),\\n id: \\"abc\\",\\n builder: (_) {\\n return Scaffold(\\n body: SafeArea(\\n child: _buildView(),\\n ),\\n );\\n },\\n );\\n }\\n}\\n
\\n采用 GetBuilder 手动、布局控制刷新,性能好,推荐这种。 注意看这个 id 属性,需要全局唯一
代码清单:
\\ncontroller
\\nimport \'package:get/get.dart\';\\n\\nclass AbcController extends GetxController {\\n AbcController();\\n\\n _initData() {\\n update([\\"abc\\"]);\\n }\\n\\n void onTap() {}\\n\\n // @override\\n // void onInit() {\\n // super.onInit();\\n // }\\n\\n @override\\n void onReady() {\\n super.onReady();\\n _initData();\\n }\\n\\n // @override\\n // void onClose() {\\n // super.onClose();\\n // }\\n\\n // @override\\n // void dispose() {\\n // super.dispose();\\n // }\\n}\\n
\\n常用的生命周期函数也生成了,按需要放开注释 update([\\"abc\\"]); 采用这种方式出发 GetBuilder 的 id属性,进行控制刷新
\\n这种是在 GetBuilder + GetView 的基础上,再加入了 StatefulWidget 包裹,比如你需要 mixin 一些功能的时候需要(AutomaticKeepAliveClientMixin、wantKeepAlive)。
\\n代码清单:
\\ncontroller
\\nimport \'package:get/get.dart\';\\n\\nclass MyController extends GetxController {\\n MyController();\\n\\n _initData() {\\n update([\\"my\\"]);\\n }\\n\\n void onTap() {}\\n\\n // @override\\n // void onInit() {\\n // super.onInit();\\n // }\\n\\n @override\\n void onReady() {\\n super.onReady();\\n _initData();\\n }\\n\\n // @override\\n // void onClose() {\\n // super.onClose();\\n // }\\n\\n // @override\\n // void dispose() {\\n // super.dispose();\\n // }\\n}\\n
\\nview
\\nimport \'package:flutter/material.dart\';\\nimport \'package:get/get.dart\';\\n\\nimport \'index.dart\';\\n\\nclass MyPage extends StatefulWidget {\\n const MyPage({Key? key}) : super(key: key);\\n\\n @override\\n _MyPageState createState() => _MyPageState();\\n}\\n\\nclass _MyPageState extends State<MyPage>\\n with AutomaticKeepAliveClientMixin {\\n @override\\n bool get wantKeepAlive => true;\\n\\n @override\\n Widget build(BuildContext context) {\\n super.build(context);\\n return const _MyViewGetX();\\n }\\n}\\n\\nclass _MyViewGetX extends GetView<MyController> {\\n const _MyViewGetX({Key? key}) : super(key: key);\\n\\n Widget _buildView() {\\n return Container();\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return GetBuilder<MyController>(\\n init: MyController(),\\n id: \\"my\\",\\n builder: (_) {\\n return Scaffold(\\n body: SafeArea(\\n child: _buildView(),\\n ),\\n );\\n },\\n );\\n }\\n}\\n
\\n可以看到 GetX 和 StatefulWidget 的优雅的结合方式,就是作为组件在 StatefulWidget.build 时创建 并不是用了 GetX 就不要 StatefulWidget 了,很多 Mixin 还是需要的
\\n鼠标右键你的视图目录,输入名称生成代码
\\n这种方式,包含了全部的 controller、view、widgets、bindings、state 拆分的很细致
\\n代码清单:
\\ncontroller
\\nimport \'package:get/get.dart\';\\n\\nimport \'index.dart\';\\n\\nclass AccountController extends GetxController {\\n AccountController();\\n\\n final state = AccountState();\\n\\n // tap\\n void handleTap(int index) {\\n Get.snackbar(\\n \\"标题\\",\\n \\"消息\\",\\n );\\n }\\n\\n /// 在 widget 内存中分配后立即调用。\\n @override\\n void onInit() {\\n super.onInit();\\n }\\n\\n /// 在 onInit() 之后调用 1 帧。这是进入的理想场所\\n @override\\n void onReady() {\\n super.onReady();\\n }\\n\\n /// 在 [onDelete] 方法之前调用。\\n @override\\n void onClose() {\\n super.onClose();\\n }\\n\\n /// dispose 释放内存\\n @override\\n void dispose() {\\n super.dispose();\\n }\\n}\\n
\\nview
\\nimport \'package:flutter/material.dart\';\\nimport \'package:get/get.dart\';\\n\\nimport \'index.dart\';\\nimport \'widgets/widgets.dart\';\\n\\nclass AccountPage extends GetView<AccountController> {\\n const AccountPage({Key? key}) : super(key: key);\\n\\n // 内容页\\n Widget _buildView() {\\n return const HelloWidget();\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return GetBuilder<AccountController>(\\n builder: (_) {\\n return Scaffold(\\n body: SafeArea(\\n child: _buildView(),\\n ),\\n );\\n },\\n );\\n }\\n}\\n
\\nbindings
\\n\\nimport \'package:get/get.dart\';\\n\\nimport \'controller.dart\';\\n\\nclass AccountBinding implements Bindings {\\n @override\\n void dependencies() {\\n Get.lazyPut<AccountController>(() => AccountController());\\n }\\n}\\n\\nstate\\nimport \'package:get/get.dart\';\\n\\nclass AccountState {\\n // title\\n final _title = \\"\\".obs;\\n set title(value) => _title.value = value;\\n get title => _title.value;\\n}\\n\\nindex\\nlibrary account;\\n\\nexport \'./state.dart\';\\nexport \'./controller.dart\';\\nexport \'./bindings.dart\';\\nexport \'./view.dart\';\\n\\nwidgets/hello.dart\\nimport \'package:flutter/material.dart\';\\nimport \'package:get/get.dart\';\\n\\nimport \'../index.dart\';\\n\\n/// hello\\nclass HelloWidget extends GetView<AccountController> {\\n const HelloWidget({Key? key}) : super(key: key);\\n\\n @override\\n Widget build(BuildContext context) {\\n return Center(\\n child: Obx(() => Text(controller.state.title)),\\n );\\n }\\n}\\n\\nwidgets/widgets.dart\\nlibrary widgets;\\n\\nexport \'./hello.dart\';\\n
\\ncommon/routers/names.txt common/routers/pages.txt pages/index.txt
lib/common/routes/names.txt
\\nstatic const application = \'/application\';\\nstatic const category = \'/category\';\\nstatic const frameNotfound = \'/frame_notfound\';\\nstatic const frameSignIn = \'/frame_sign_in\';\\nstatic const frameSignUp = \'/frame_sign_up\';\\nstatic const frameWelcome = \'/frame_welcome\';\\nstatic const main = \'/main\';\\n
\\nlib/common/routes/pages.txt
\\n GetPage(\\n name: RouteNames.application,\\n page: () => const ApplicationPage(),\\n ),\\n GetPage(\\n name: RouteNames.category,\\n page: () => const CategoryPage(),\\n ),\\n GetPage(\\n name: RouteNames.frameNotfound,\\n page: () => const FrameNotfoundPage(),\\n ),\\n GetPage(\\n name: RouteNames.frameSignIn,\\n page: () => const FrameSignInPage(),\\n ),\\n GetPage(\\n name: RouteNames.frameSignUp,\\n page: () => const FrameSignUpPage(),\\n ),\\n GetPage(\\n name: RouteNames.frameWelcome,\\n page: () => const FrameWelcomePage(),\\n ),\\n GetPage(\\n name: RouteNames.main,\\n page: () => const MainPage(),\\n ),\\n
\\nlib/pages/index.txt
\\nexport \'application/index.dart\';\\nexport \'category/index.dart\';\\nexport \'frame/notfound/index.dart\';\\nexport \'frame/sign_in/index.dart\';\\nexport \'frame/sign_up/index.dart\';\\nexport \'frame/welcome/index.dart\';\\nexport \'main/index.dart\';\\n
","description":"兄弟们,好久不见,上班啦,但生命不止,学习不停!今天跟大家分享一个flutter开发很好用的拓展插件,能提高开发效率。 功能\\n根据 x3 图片自动生成 x1 x2 图片\\n生成 图片 svg 常量列表 files.txt\\n生成规范目录 common\\n生成 GetBuilder + GetView 的代码\\n生成 StatefulWidget + GetBuilder + GetView 的代码\\n生成 controller、view、widgets、bindings、state、index 完整的代码\\n生成 路由声明定义文件common/routers…","guid":"https://juejin.cn/post/7479625267029458971","author":"晴天学长","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-10T03:14:48.721Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6eaaf18ef6e64cf38a1a812033a84f99~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pm05aSp5a2m6ZW_:q75.awebp?rk3s=f64ab15b&x-expires=1742181288&x-signature=TaMCKBLOcg6d%2BpsfHiiFmnUSj5k%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ed74235480d94f179aff34cba2ff7038~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pm05aSp5a2m6ZW_:q75.awebp?rk3s=f64ab15b&x-expires=1742181288&x-signature=1HuBenfx1Ds4EqVJBAxFOpCRPYc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/aab2433edb5743aba041740e2d07ce62~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pm05aSp5a2m6ZW_:q75.awebp?rk3s=f64ab15b&x-expires=1742181288&x-signature=dK%2BSfGOEGNSa0SxQJwps7lmpeh8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c2461666bc8b4bce8a0e611922476655~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pm05aSp5a2m6ZW_:q75.awebp?rk3s=f64ab15b&x-expires=1742181288&x-signature=uzoBdBfRq16pfXAI9gqPXiVndA0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1eab4109e60d4ee1988f33ea8a52e4b3~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pm05aSp5a2m6ZW_:q75.awebp?rk3s=f64ab15b&x-expires=1742181288&x-signature=Ix9n8TzRcksmuHaUG5ptm3LIbTs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7c807ff385974db5a005ed410447b85e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pm05aSp5a2m6ZW_:q75.awebp?rk3s=f64ab15b&x-expires=1742181288&x-signature=EF8sDy1stuK2t2rgXpDkNskzYp0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4eff27b3c7494e17917bc5abd275ab19~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pm05aSp5a2m6ZW_:q75.awebp?rk3s=f64ab15b&x-expires=1742181288&x-signature=XHx0v98RCjs3hK4Ic3ZsYv2VsNI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0e529145db454bcf996644dc62680e6c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pm05aSp5a2m6ZW_:q75.awebp?rk3s=f64ab15b&x-expires=1742181288&x-signature=QX%2BHXvIpe2rYtITLs9fY45PrzA0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7fa1d3498b594fc2ac5ca5886b7d4f8b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pm05aSp5a2m6ZW_:q75.awebp?rk3s=f64ab15b&x-expires=1742181288&x-signature=QSkwC9zoCX9i5hzu%2Fqkcu5E2rjU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d64807dbb5334a7cb6b65bda20226f34~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pm05aSp5a2m6ZW_:q75.awebp?rk3s=f64ab15b&x-expires=1742181288&x-signature=9OgpLZ73pC3xbgLllMGyqeJ97h0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/60c9894b39fd4487a7a505c48928b406~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pm05aSp5a2m6ZW_:q75.awebp?rk3s=f64ab15b&x-expires=1742181288&x-signature=rRfLIqYQctKDuVEV%2B1Eb8W6YiWU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d070b50e073d403c8e663d952bc0decb~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5pm05aSp5a2m6ZW_:q75.awebp?rk3s=f64ab15b&x-expires=1742181288&x-signature=HRsUh8gkslT98geHU%2Fibp4NGiDc%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","前端"],"attachments":null,"extra":null,"language":null},{"title":"深入理解 Dart 函数:从基础到高阶应用","url":"https://juejin.cn/post/7479434217941631013","content":"Dart 是一种面向对象的、类定义的、单继承的语言,被广泛应用于 Flutter 跨平台开发。在 Dart 编程中,函数占据着核心地位。
\\n函数是代码复用和模块化的基础,能够提高代码的可读性、可维护性和可测试性。本文将详细探讨 Dart 函数相关内容。
\\n函数定义的基本语法包括返回值类型、函数名、参数列表和函数体。
\\nint addNumbers(int num1, int num2) {\\n return num1 + num2;\\n}\\n
\\n该示例定义了一个计算两个整数之和的函数。
\\n通过函数名和实际参数来调用函数,参数的顺序和类型必须与函数定义一致。
\\nvoid main() {\\n int result = addNumbers(3, 5);\\n print(result); // 输出 8\\n}\\n
\\n此代码调用了addNumbers函数并输出结果。
\\n位置参数根据参数在函数定义中的位置来传递值。例如:
\\nint multiply(int a, int b) {\\n return a * b;\\n}\\n// 调用方式:multiply(2, 3);\\n
\\n命名参数允许在调用函数时通过参数名来指定参数值,提高代码可读性。
\\nvoid printUserInfo({String name, int age}) {\\n print(\'Name: $name, Age: $age\');\\n}\\n
\\n调用示例:
\\nvoid main() {\\n printUserInfo(name: \'John\', age: 30);\\n}\\n
\\n默认参数为函数参数提供默认值,当调用函数时未传递该参数值时,将使用默认值。
\\nvoid greet(String message, [String name = \'Guest\']) {\\n print(\'$message, $name!\');\\n}\\n
\\n调用示例:
\\nvoid main() {\\n greet(\'Hello\'); // 输出 Hello, Guest!\\n greet(\'Hello\', \'Alice\'); // 输出 Hello, Alice!\\n}\\n
\\n在 Dart 中,函数可以像其他数据类型一样赋值给变量。
\\nvoid main() {\\n Function add = (int a, int b) => a + b;\\n int result = add(2, 3);\\n print(result); // 输出 5\\n}\\n
\\n函数可以作为参数传递给其他函数,实现回调函数功能。
\\nvoid performOperation(int num1, int num2, Function operation) {\\n int result = operation(num1, num2);\\n print(result);\\n}\\n
\\n调用示例:
\\nvoid main() {\\n performOperation(4, 2, (a, b) => a * b); // 输出 8\\n}\\n
\\n函数可以返回另一个函数。
\\nFunction createAdder(int num) {\\n return (int anotherNum) => num + anotherNum;\\n}\\n
\\n调用示例:
\\nvoid main() {\\n Function addFive = createAdder(5);\\n int result = addFive(3);\\n print(result); // 输出 8\\n}\\n
\\n匿名函数是没有函数名的函数,通常用于临时定义一个简单函数的场景。其语法简洁,无函数名。
\\nvoid main() {\\n List<int> numbers = [1, 2, 3, 4];\\n List<int> squaredNumbers = numbers.map((num) => num * num).toList();\\n print(squaredNumbers); // 输出 [1, 4, 9, 16]\\n numbers.forEach((num) => print(num));\\n}\\n
\\n在列表的map和forEach方法中使用了匿名函数。
\\n箭头函数是匿名函数的一种简化写法,适用于函数体只有一条语句的情况。使用=>连接参数列表和函数体,省略大括号和return关键字(若函数体只有一条返回语句)。
\\nvoid main() {\\n List<int> numbers = [1, 2, 3, 4];\\n List<int> squaredNumbers = numbers.map((num) => num * num).toList();\\n print(squaredNumbers); // 输出 [1, 4, 9, 16]\\n numbers.forEach((num) => print(num));\\n}\\n
\\n上述示例中map和forEach方法内的匿名函数采用了箭头函数形式。
\\n高阶函数是指可以接受一个或多个函数作为参数,或者返回一个函数的函数。在 Dart 函数式编程中具有重要意义。
\\nmap函数对列表中的每个元素应用指定的函数,并返回一个新的列表。
\\nvoid main() {\\n List<String> names = [\'Alice\', \'Bob\', \'Charlie\'];\\n List<int> nameLengths = names.map((name) => name.length).toList();\\n print(nameLengths); // 输出 [5, 3, 7]\\n}\\n
\\nforEach函数用于遍历列表,并对每个元素执行指定的函数,不返回新的列表。
\\nvoid main() {\\n List<int> numbers = [1, 2, 3, 4];\\n numbers.forEach((num) => print(num * 2));\\n}\\n
\\nfilter函数根据指定的条件过滤列表元素,返回符合条件的元素组成的新列表。
\\nvoid main() {\\n List<int> numbers = [1, 2, 3, 4, 5];\\n List<int> evenNumbers = numbers.filter((num) => num % 2 == 0);\\n print(evenNumbers); // 输出 [2, 4]\\n}\\n
\\nreduce函数将列表中的元素按照指定的函数进行累积计算,返回一个单一的值。
\\nvoid main() {\\n List<int> numbers = [1, 2, 3, 4];\\n int sum = numbers.reduce((acc, num) => acc + num);\\n print(sum); // 输出 10\\n}\\n
\\n函数重载指在同一个类中定义多个同名函数,但参数列表不同(参数个数、类型或顺序不同),可提高代码的灵活性和可读性。
\\nDart 本身不直接支持传统意义上的函数重载,但可通过命名参数和可选参数模拟函数重载效果。
\\nclass Calculator {\\n int add(int num1, int num2) {\\n return num1 + num2;\\n }\\n int addAll({int num1 = 0, int num2 = 0, int num3 = 0}) {\\n return num1 + num2 + num3;\\n }\\n}\\n
\\n调用示例:
\\nvoid main() {\\n Calculator calculator = Calculator();\\n int result1 = calculator.add(2, 3);\\n print(result1); // 输出 5\\n int result2 = calculator.addAll(num1 = 1, num2 = 2, num3 = 3);\\n print(result2); // 输出 6\\n}\\n
\\n闭包是一个函数对象,它可以访问其词法作用域之外的变量,即使在函数被调用的地方,这些变量已经超出了其原始作用域。
\\nFunction createCounter() {\\n int count = 0;\\n return () {\\n count++;\\n return count;\\n };\\n}\\n
\\n当createCounter函数被调用时,返回的匿名函数(闭包)捕获了count变量。即使createCounter函数执行完毕,闭包仍可访问和修改count变量。
\\n在 Flutter 中,使用闭包处理按钮点击事件。
\\nimport \'package:flutter/material.dart\';\\nvoid main() => runApp(MyApp());\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n home: Scaffold(\\n appBar: AppBar(title: Text(\'Closure Example\')),\\n body: Center(\\n child: RaisedButton(\\n child: Text(\'Click Me\'),\\n onPressed: () {\\n print(\'Button Clicked!\');\\n },\\n ),\\n ),\\n ),\\n );\\n }\\n}\\n
\\n使用闭包实现延迟执行一段代码。
\\nvoid main() {\\n Future.delayed(Duration(seconds: 2), () {\\n print(\'This is printed after 2 seconds.\');\\n });\\n}\\n
\\n通过闭包实现类的私有变量和方法。
\\nclass MyClass {\\n int _privateVariable = 0;\\n Function getPrivateVariable() {\\n return () => _privateVariable;\\n }\\n Function incrementPrivateVariable() {\\n return () => _privateVariable++;\\n }\\n}\\n
\\n调用示例:
\\nvoid main() {\\n MyClass myClass = MyClass();\\n Function getVar = myClass.getPrivateVariable();\\n Function incrementVar = myClass.incrementPrivateVariable();\\n print(getVar()); // 输出 0\\n incrementVar();\\n print(getVar()); // 输出 1\\n}\\n
\\n本文介绍了 Dart 函数的基础、函数类型、高阶函数、函数重载和闭包等方面。函数在 Dart 编程中处于核心地位,应用广泛。
","description":"一、引言 1.1 Dart 语言简介\\n\\nDart 是一种面向对象的、类定义的、单继承的语言,被广泛应用于 Flutter 跨平台开发。在 Dart 编程中,函数占据着核心地位。\\n\\n1.2 函数的重要性\\n\\n函数是代码复用和模块化的基础,能够提高代码的可读性、可维护性和可测试性。本文将详细探讨 Dart 函数相关内容。\\n\\n二、函数基础\\n2.1 函数定义与声明\\n语法结构\\n\\n函数定义的基本语法包括返回值类型、函数名、参数列表和函数体。\\n\\n示例代码\\nint addNumbers(int num1, int num2) {\\n return num1 + num2;\\n}…","guid":"https://juejin.cn/post/7479434217941631013","author":"顾林海","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-10T01:23:28.449Z","media":null,"categories":["Android","Flutter","前端"],"attachments":null,"extra":null,"language":null},{"title":"Flutter进阶:局部嵌套导航实现 Navigator","url":"https://juejin.cn/post/7479371146657611826","content":"iOS原生是支持局部嵌套导航实现的(半屏导航),就想在flutter中实现同样功能,今天灵光一闪,实现分享给大家。
\\n//\\n// NestedNavigatorDemo.dart\\n// flutter_templet_project\\n//\\n// Created by shang on 2024/9/27 16:14.\\n// Copyright © 2024/9/27 shang. All rights reserved.\\n//\\n\\nimport \'dart:math\';\\n\\nimport \'package:flutter/material.dart\';\\nimport \'package:flutter_templet_project/extension/build_context_ext.dart\';\\nimport \'package:flutter_templet_project/extension/ddlog.dart\';\\nimport \'package:flutter_templet_project/pages/demo/CupertinoTabScaffoldDemo.dart\';\\nimport \'package:flutter_templet_project/routes/APPRouter.dart\';\\nimport \'package:get/get.dart\';\\n\\nclass NestedNavigatorDemo extends StatefulWidget {\\n const NestedNavigatorDemo({\\n super.key,\\n this.arguments,\\n });\\n\\n final Map<String, dynamic>? arguments;\\n\\n @override\\n State<NestedNavigatorDemo> createState() => _NestedNavigatorDemoState();\\n}\\n\\nclass _NestedNavigatorDemoState extends State<NestedNavigatorDemo> {\\n\\n final _scrollController = ScrollController();\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: Text(\\"$widget\\"),\\n ),\\n body: buildBody(),\\n );\\n }\\n\\n Widget buildBody() {\\n return Scrollbar(\\n controller: _scrollController,\\n child: SingleChildScrollView(\\n controller: _scrollController,\\n child: Container(\\n padding: EdgeInsets.all(10),\\n child: Column(\\n // crossAxisAlignment: CrossAxisAlignment.start,\\n children: [\\n buildNavigatorBox(),\\n ],\\n ),\\n ),\\n ),\\n );\\n }\\n\\n Widget buildNavigatorBox() {\\n onNext({required BuildContext context, required String title}) {\\n Navigator.push(\\n context,\\n MaterialPageRoute(\\n builder: (context) {\\n return NestedNavigatorSubpage(\\n appBar: AppBar(\\n centerTitle: true,\\n title: Text(title),\\n actions: [\\n GestureDetector(\\n onTap: () {\\n DLog.d(\\"error\\");\\n },\\n child: Icon(Icons.error_outline),\\n ),\\n ]\\n .map((e) => Container(\\n padding: EdgeInsets.only(right: 8),\\n child: e,\\n ))\\n .toList(),\\n ),\\n child: Column(\\n children: [\\n ElevatedButton(\\n onPressed: () {\\n onNext(context: context, title: title);\\n },\\n child: Text(\'next page\'),\\n ),\\n ElevatedButton(\\n onPressed: () {\\n Navigator.pop(context);\\n },\\n child: Text(\'Go back\'),\\n ),\\n Wrap(\\n spacing: 8,\\n runSpacing: 8,\\n children: [\\n ...List.generate(Random().nextInt(9), (index) {\\n final title = \\"选项_$index\\";\\n\\n return OutlinedButton(\\n style: TextButton.styleFrom(\\n padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),\\n tapTargetSize: MaterialTapTargetSize.shrinkWrap,\\n minimumSize: Size(50, 18),\\n // primary: primary,\\n ),\\n onPressed: () {\\n DLog.d(title);\\n },\\n child: Text(title),\\n );\\n }),\\n ],\\n ),\\n ],\\n ),\\n );\\n },\\n ),\\n );\\n }\\n\\n return Container(\\n height: 400,\\n child: Theme(\\n data: ThemeData(\\n appBarTheme: const AppBarTheme(\\n backgroundColor: Colors.lightBlueAccent,\\n elevation: 0,\\n scrolledUnderElevation: 0,\\n titleTextStyle: TextStyle(\\n fontSize: 18,\\n fontWeight: FontWeight.w500,\\n ),\\n toolbarTextStyle: TextStyle(\\n fontSize: 16,\\n fontWeight: FontWeight.w400,\\n ),\\n iconTheme: IconThemeData(\\n color: Colors.white,\\n size: 24.0,\\n opacity: 0.8,\\n ),\\n actionsIconTheme: IconThemeData(\\n color: Colors.white,\\n size: 24.0,\\n opacity: 0.8,\\n ),\\n ),\\n ),\\n child: Navigator(\\n onGenerateRoute: (RouteSettings settings) {\\n return MaterialPageRoute(\\n builder: (context) {\\n return NestedNavigatorSubpage(\\n appBar: AppBar(\\n centerTitle: true,\\n title: Text(\\"嵌套导航主页面\\"),\\n ),\\n child: Column(\\n children: [\\n ElevatedButton(\\n onPressed: () {\\n onNext(context: context, title: \\"子页面\\");\\n },\\n child: Text(\'next page\'),\\n )\\n ],\\n ),\\n );\\n },\\n );\\n },\\n ),\\n ),\\n );\\n }\\n}\\n\\n/// 嵌套导航子视图\\nclass NestedNavigatorSubpage extends StatefulWidget {\\n const NestedNavigatorSubpage({\\n super.key,\\n this.appBar,\\n required this.child,\\n });\\n\\n final AppBar? appBar;\\n\\n final Widget child;\\n\\n @override\\n State<NestedNavigatorSubpage> createState() => _NestedNavigatorSubpageState();\\n}\\n\\nclass _NestedNavigatorSubpageState extends State<NestedNavigatorSubpage> {\\n final scrollController = ScrollController();\\n\\n @override\\n void didUpdateWidget(covariant NestedNavigatorSubpage oldWidget) {\\n super.didUpdateWidget(oldWidget);\\n\\n if (oldWidget.appBar != widget.appBar || oldWidget.child != widget.child) {\\n setState(() {});\\n }\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: widget.appBar ??\\n AppBar(\\n title: Text(\\"$widget\\"),\\n ),\\n body: buildBody(),\\n );\\n }\\n\\n Widget buildBody() {\\n return Container(\\n alignment: Alignment.center,\\n decoration: BoxDecoration(\\n // color: color,\\n border: Border.all(color: Colors.blue),\\n ),\\n child: Column(\\n children: [\\n // NPickerToolBar(\\n // title: title,\\n // onCancel: onBack,\\n // onConfirm: onNext,\\n // ),\\n Expanded(\\n child: Scrollbar(\\n child: SingleChildScrollView(\\n child: widget.child,\\n ),\\n ),\\n ),\\n ],\\n ),\\n );\\n }\\n}\\n
\\nNavigator
是 Flutter 中管理页面堆栈和路由的核心组件,它允许在应用中进行页面导航(推送、弹出、替换等)。Navigator
通过一个栈来管理活动路由集合。通常当前屏幕显示的页面就是栈顶的路由。最核心的方法就是入栈和出栈:
Future push(BuildContext context, Route route)
将给定的路由入栈(即打开新的页面),返回值是一个Future
对象,用以接收新路由出栈(即关闭)时的返回数据。
bool pop(BuildContext context, [ result ])
将栈顶路由出栈,result
为页面关闭时返回给上一个页面的数据。
1、Navigator
是 Flutter 中管理页面堆栈和路由的核心组件。即使你工作中使用的是第三方导航库,了解它的源码依然能提高你的核心能力。
1)架构是如何设计?
2)如何使用(是否掌握了所有的使用方法)?
3)局限性如何突破?
我曾经困扰于 popUntil 无法传值的问题,期望它可以像 pop 一样返回值。今天突然发现只要加一个参数即可实现
\\nSDK 源码
\\n/// Calls [pop] repeatedly until the predicate returns true.\\n///\\n/// {@macro flutter.widgets.navigator.popUntil}\\n///\\n/// {@tool snippet}\\n///\\n/// Typical usage is as follows:\\n///\\n/// ```dart\\n/// void _doLogout() {\\n/// navigator.popUntil(ModalRoute.withName(\'/login\'));\\n/// }\\n/// ```\\n/// {@end-tool}\\nvoid popUntil(RoutePredicate predicate) {\\n _RouteEntry? candidate = _history.cast<_RouteEntry?>().lastWhere(\\n (_RouteEntry? e) => e != null && _RouteEntry.isPresentPredicate(e),\\n orElse: () => null,\\n );\\n while(candidate != null) {\\n if (predicate(candidate.route)) {\\n return;\\n }\\n pop();\\n candidate = _history.cast<_RouteEntry?>().lastWhere(\\n (_RouteEntry? e) => e != null && _RouteEntry.isPresentPredicate(e),\\n orElse: () => null,\\n );\\n }\\n}\\n
\\nSDK 源码魔改版:
\\nvoid popUntil<T extends Object?>(RoutePredicate predicate, [ T? result ]) {\\n ...\\n pop(result);\\n ...\\n}\\n
\\nPageTwo->PageThree->PageFour->PageFive->PageTwo
\\n//当前页面 PageFive\\nNavigator.of(context).popUntil(ModalRoute.withName(\\"/PageTwo\\"), {\\" PageFive\\": \\"999\\"});\\n
\\n//当前页面 PageTwo\\nfinal result = await Navigator.of(context).push(MaterialPageRoute(\\n builder: (context) => const PageThree(),\\n settings: const RouteSettings(\\n name: \\"/PageThree\\",\\n ),\\n));\\nDLog.d(\\"$widget result: $result\\");\\n//[log] DLog 2025-02-25 10:26:04.382607 PageTwo result: {PageFive: 999}//完美传值\\n
\\n","description":"一、需求来源 iOS原生是支持局部嵌套导航实现的(半屏导航),就想在flutter中实现同样功能,今天灵光一闪,实现分享给大家。\\n\\n二、使用示例\\n\\n三、源码\\n//\\n// NestedNavigatorDemo.dart\\n// flutter_templet_project\\n//\\n// Created by shang on 2024/9/27 16:14.\\n// Copyright © 2024/9/27 shang. All rights reserved.\\n//\\n\\nimport \'dart:math\';\\n\\nimport \'package:flu…","guid":"https://juejin.cn/post/7479371146657611826","author":"SoaringHeart","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-10T01:04:50.327Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/744089c273c5445ebf49afc7c60ff646~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgU29hcmluZ0hlYXJ0:q75.awebp?rk3s=f64ab15b&x-expires=1742173490&x-signature=v%2FaP1sxVac%2FktIOAYCVxQeUU2VY%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Trae&Flutter | 助力 TolyUI 模块管理与发布","url":"https://juejin.cn/post/7479331614620925963","content":"随着 TolyUI 的维护,我遇到了一些模块管理上的问题。比如:
\\n手动处理比较麻烦,所以我打算写个 Dart 脚本,来分析模块依赖结构。完成自动发布和展示当前所有模块信息的功能。刚好最近 Trae 发布了,就用它来帮我分析和书写一些代码:
\\n首先我提前准备了一个 module.json, 问它:
\\n\\n\\n这个文件的作用。
\\n
当编辑器打开一个文件时,对话框里可以识别到 这个文件 指代的是什么。
\\n它的总结还是非常到位的,基本上是我模块化设计的初衷:
\\n这个文件的主要作用是:\\n1. 记录项目中各个模块的版本信息\\n2. 定义模块之间的依赖关系和层级结构\\n3. 便于项目的版本管理和模块更新\\n4. 可能用于自动化构建和依赖管理\\n\\n这种模块化的结构设计有助于:\\n- 更好地组织和管理大型项目\\n- 实现模块的独立开发和维护\\n- 方便模块的复用和版本控制\\n- 清晰地展示项目的整体架构\\n
\\n然后向他索要自动生成这个 module.json 的方式。这一招叫 将欲取之,必先与之
:
\\n\\n用 dart 解析 modlues 下的模块,自动生成该文件。注意模块间的子模块校验方式是: 依赖中含 tolyui_ 的模块。
\\n
然后它给了我一段代码,我执行了一下,完美地满足了我的需求。控制台漂亮地打印出了一个树形结构。从解析 yaml 到树形节点的处理,如果全凭我自己敲,少说也要一个小时的分析调试。而 AI 可以在 1 分钟之内给我期望的结果。
\\n人生是一场修炼,过程远比结果要重要得多。可以学到知识的地方都可以谓之 师
,我将于 AI 老师为我节约的这一个小时,来优化一下代码,并写篇文章仔细分析其过程,汲取其中的养分。而不是得到结果就算完事了。
首先定义了一个 Module
对象,盛纳一个模块的必要信息,目前包括名称、版本和子模块:
import \'dart:convert\';\\nimport \'dart:io\';\\nimport \'package:yaml/yaml.dart\';\\nimport \'package:path/path.dart\' as path;\\n\\nclass Module {\\n final String name;\\n final String version;\\n final List<Module> children;\\n\\n Module({\\n required this.name,\\n required this.version,\\n this.children = const [],\\n });\\n\\n Map<String, dynamic> toJson() => {\\n \'name\': name,\\n \'version\': version,\\n if (children.isNotEmpty) \'children\': children.map((e) => e.toJson()).toList(),\\n };\\n}\\n
\\n解析模块的方法 processModule
代码还不到 30 行,其中传入模块的文件夹路径,并读取其中的 pubspec.yaml ,然后通过 yaml 解析内容,得到名称版本、依赖库列表,当依赖以 tolyui_
开头,则递归地分析子模块,并将解析结果添加到当前模块的 children 中;
Future<Module?> processModule(Directory dir) async {\\n // 1. 读取并解析 pubspec.yaml\\n final pubspecFile = File(path.join(dir.path, \'pubspec.yaml\'));\\n if (!await pubspecFile.exists()) return null;\\n final yaml = loadYaml(await pubspecFile.readAsString());\\n // 2. 创建当前模块\\n final module = Module(\\n name: yaml[\'name\'],\\n version: yaml[\'version\'],\\n children: [],\\n );\\n\\n // 3.扫描依赖\\n YamlMap dependencies = yaml[\'dependencies\'];\\n \\n // 4. 递归处理子模块\\n for (final dep in dependencies.keys) {\\n if (dep.startsWith(\'tolyui_\')) {\\n Directory subModelDir = Directory(path.join(dir.path, \'..\', dep));\\n final childModule = await processModule(subModelDir);\\n if (childModule != null) {\\n module.children.add(childModule);\\n }\\n }\\n }\\n return module;\\n}\\n
\\n最后将解析的 Module 对象通过 JsonEncoder 反序列化为字符串,写入到文件即可:
\\nFuture<void> main() async {\\n String modulesPath = \'d:\\\\\\\\Projects\\\\\\\\Flutter\\\\\\\\Fx\\\\\\\\toly_ui\\\\\\\\modules\';\\n String root = path.join(modulesPath, \'tolyui\');\\n Module? moduleJson = await processModule(Directory(root));\\n if (moduleJson == null) return;\\n String data = const JsonEncoder.withIndent(\' \').convert(moduleJson.toJson());\\n print(data);\\n final outputFile = File(path.join(modulesPath, \'module.json\'));\\n await outputFile.writeAsString(data);\\n}\\n
\\nTrae 还有一点非常惊艳,它可以识别图片。比如我给它一个截图,问:
\\n\\n\\n以这种树形结构,输出 module.json 的核心数
\\n
它会为我在 Module 类中提供 displayTree 方法,获得直观版的树形结构。运行后,完美得到我预期的效果:
\\nString displayTree({String prefix = \'\', bool isLast = true}) {\\n final buffer = StringBuffer();\\n final marker = isLast ? \'└──\' : \'├──\';\\n buffer.writeln(\'$prefix$marker $name ($version)\');\\n if (children.isNotEmpty) {\\n final childPrefix = prefix + (isLast ? \' \' : \'│ \');\\n for (var i = 0; i < children.length; i++) {\\n buffer.write(children[i].displayTree(\\n prefix: childPrefix,\\n isLast: i == children.length - 1,\\n ));\\n }\\n }\\n return buffer.toString();\\n}\\n
\\n如果不太明白算法实现的细节,也可以继续追问,让 AI 详细分析一下:
\\n核心实现解析:
\\nStringBuffer 的使用 [1]
\\n树形标记符号 [2]
\\n当前节点输出 [3]
\\n子节点缩进处理 [4]
\\n递归处理子节点 [5]
\\n返回结果 [6]
\\n\\n\\n基于 module.json,现在我想实现一个功能,通过 Dart 提供一个 update 方法,指定模块名,和版本号,如果模块的版本号增加,将其父级的所有版本后缀 +1 个小版本,形成新的 module.json
\\n
拿当前的版本来说,如果 tolyui_message
有更新,我希望依赖它的上层组件都可以更新一个小版本:
└── tolyui (0.0.4+6)\\n ├── tolyui_rx_layout (1.0.0)\\n ├── tolyui_color (0.0.1)\\n └── tolyui_navigation (0.1.0+3)\\n └── tolyui_feedback (0.3.6+1)\\n └── tolyui_message (0.2.4)\\n\\n
\\n比如 tolyui_message
更新到 0.2.5
, tolyui 版本系列应该如下所示:
└── tolyui (0.0.4+7)\\n ├── tolyui_rx_layout (1.0.0)\\n ├── tolyui_color (0.0.1)\\n └── tolyui_navigation (0.1.0+4)\\n └── tolyui_feedback (0.3.6+2)\\n └── tolyui_message (0.2.5)\\n
\\n为了记录 TolyUI 的每个更新的点滴,我希望保留所有的 tolyui
版本树,让之前的 module.json
编程 tolyui 后面加上版本号:
下面是 AI 给出的版本更新方法,放在 Module
类中,传入待更新模块名、版本号。这个方法会将检查当前当 Module 树种匹配的模块名,更新版本号:
bool updateVersion(String targetName, String newVersion) {\\n if (name == targetName) {\\n version = newVersion;\\n return true;\\n }\\n for (var child in children) {\\n if (child.updateVersion(targetName, newVersion)) {\\n _incrementVersion();\\n return true;\\n }\\n }\\n return false;\\n}\\nvoid _incrementVersion() {\\n final parts = version.split(\'+\');\\n if (parts.length > 1) {\\n // 处理带有 build number 的版本号\\n final buildNumber = int.parse(parts[1]);\\n version = \'${parts[0]}+${buildNumber + 1}\';\\n } else {\\n // 处理普通版本号\\n version += \\"+1\\";\\n }\\n}\\n
\\n我想了一下,也可能一次更新多个模块,并且最好记录一下更新信息。于是继续优化:
\\n\\n\\n优化一下当前类的 updateVersion 方法,我想传入一个更新信息的列表,包括更新模块名、版本号、更新介绍。父级没有更新的模块,版本号自动 +1,并在更新信息上记录所有子模块的更新信息
\\n
期望调用方式如下:
\\nmodule.updateVersion([\\n UpdateInfo(\\n moduleName: \'tolyui_message\',\\n newVersion: \'0.2.5\',\\n changLog: \'修复一些 bug\',\\n ),\\n UpdateInfo(\\n moduleName: \'tolyui_color\',\\n newVersion: \'0.0.2\',\\n changLog: \'优化颜色选择器\',\\n ),\\n]);\\n
\\n如下代码所示,在 Module
类中增加了 changLog 字段,updateVersion 的入参改成了 UpdateInfo 列表。其中会根据入参信息的模块名称和版本号,自动维护 changLog 信息:
final String changLog;\\n\\nbool updateVersion(List<UpdateInfo> updates) {\\n bool hasUpdates = false;\\n StringBuffer childrenChanges = StringBuffer();\\n // 检查当前模块是否在更新列表中\\n final updateInfo = updates.where((u) => u.moduleName == name).firstOrNull;\\n if (updateInfo != null) {\\n version = updateInfo.newVersion;\\n changLog = updateInfo.changLog;\\n hasUpdates = true;\\n }\\n // 检查子模块更新\\n for (var child in children) {\\n if (child.updateVersion(updates)) {\\n if (!hasUpdates) {\\n _incrementVersion();\\n }\\n childrenChanges.writeln(\'* ${child.name} -> ${child.version}\');\\n hasUpdates = true;\\n }\\n }\\n // 如果有子模块更新,将子模块信息添加到父模块的更新记录中\\n if (childrenChanges.isNotEmpty) {\\n if (changLog.isEmpty) {\\n changLog = childrenChanges.toString();\\n } else {\\n changLog += \'\\\\n$childrenChanges\';\\n }\\n }\\n return hasUpdates;\\n}\\n
\\n此时跑一下脚本,完美符合我的预期:tolyui_0.0.4+7
中描述了 tolyui 在 0.0.4+7 版本中的所有信息:
接下来就到了最后一步,根据 tolyui_0.0.4+7
中的描述,修改变化模块的版本信息,以及 CHANGELOG.md 变化记录文件。关于 changelog 的维护,我希望能有一个结构化的东西来组织好,而不是直接 markdown 文本来记录。毕竟文本可以随便写,自动维护时校验比较麻烦。
于是我在每个模块中新加了doc 文件夹,用于放置文档,changelog.json
就是更新的结构化数据来源。
所以根据 tolyui_0.0.4+7.json 中的信息,维护模块下的 doc/changelog.json
即可,然后根据 changelog.json 生成 CHANGELOG.md。
\\n\\n写一个 addVersion(String version, String change) 的方法,为当前文件新增一个版本。注意:当版本号是最新版本的小版本时,添加到 changes 里,否则在最前面添加该版本;没有文件时,自动创建文件,并写入当前版本
\\n
调用 addVersion,传入 0.3.6+3, Fix some bugs 后,就会添加一个记录:
\\nFuture<void> addVersion(String path ,String version, String change) async {\\n final file = File(path);\\n\\n if (!await file.exists()) {\\n // 文件不存在,创建新文件\\n final json = {\\n \\"versions\\": {\\n version: {\\n \\"changes\\": [change],\\n \\"timestamp\\": DateTime.now().toIso8601String()\\n }\\n }\\n };\\n await file.writeAsString(jsonEncode(json));\\n return;\\n }\\n\\n // 读取现有文件\\n final content = await file.readAsString();\\n final json = jsonDecode(content) as Map<String, dynamic>;\\n final versions = json[\'versions\'] as Map<String, dynamic>;\\n\\n // 检查是否为最新版本的小版本更新\\n final latestVersion = versions.keys.first;\\n if (version.startsWith(latestVersion.split(\'+\')[0])) {\\n // 添加到现有版本的 changes\\n List<dynamic> changes = versions[latestVersion][\'changes\'];\\n if(changes.isNotEmpty && changes.first.startsWith(version)) return;\\n changes.insert(0, change);\\n } else {\\n // 添加新版本\\n versions[version] = {\\n \\"changes\\": [change],\\n \\"timestamp\\": DateTime.now().toIso8601String()\\n };\\n // 重新排序版本\\n final sortedVersions = Map.fromEntries(\\n versions.entries.toList()\\n ..sort((a, b) => compareVersions(b.key, a.key))\\n );\\n json[\'versions\'] = sortedVersions;\\n }\\n\\n // 写入文件\\n await file.writeAsString(JsonEncoder.withIndent(\' \').convert(json));\\n}\\n\\nint compareVersions(String v1, String v2) {\\n final v1Parts = v1.split(\'+\')[0].split(\'.\');\\n final v2Parts = v2.split(\'+\')[0].split(\'.\');\\n\\n for (var i = 0; i < 3; i++) {\\n final num1 = int.parse(v1Parts[i]);\\n final num2 = int.parse(v2Parts[i]);\\n if (num1 != num2) return num1.compareTo(num2);\\n }\\n\\n // 比较小版本号\\n final v1Build = v1.contains(\'+\') ? int.parse(v1.split(\'+\')[1]) : 0;\\n final v2Build = v2.contains(\'+\') ? int.parse(v2.split(\'+\')[1]) : 0;\\n return v1Build.compareTo(v2Build);\\n}\\n
\\nTrae 还有个比较好用的功能,可以选择关联的文件/夹, 比如这里让它根据 changelog.json
生成 CHANGELOG.md
import \'dart:convert\';\\nimport \'dart:io\';\\n\\nFuture<void> jsonToMarkdown(String jsonPath,String mdPath) async {\\n \\n final file = File(jsonPath);\\n if (!await file.exists()) return;\\n \\n final content = await file.readAsString();\\n final json = jsonDecode(content) as Map<String, dynamic>;\\n final versions = json[\'versions\'] as Map<String, dynamic>;\\n \\n final StringBuffer buffer = StringBuffer();\\n \\n for (final entry in versions.entries) {\\n final version = entry.key;\\n final data = entry.value as Map<String, dynamic>;\\n final changes = data[\'changes\'] as List;\\n \\n buffer.writeln(\'## $version\\\\n\');\\n for (final change in changes) {\\n // 处理小版本号格式\\n if (change.toString().startsWith(\'+\')) {\\n buffer.writeln(\'* $version$change\');\\n } else {\\n buffer.writeln(\'* $change\');\\n }\\n }\\n buffer.writeln(\'\\\\n\');\\n }\\n \\n await File(mdPath).writeAsString(buffer.toString());\\n}\\n
\\n到这里,基本功能都有了,整理串联一下:
\\n最后来到批量发布环节,我只需要对比当前版本和上个版本有差异的模块,通过命令发布到 pub 上即可。
\\nFuture<List<String>> collect(String oldPath, String newPath) async {\\n String oldContent = await File(oldPath).readAsString();\\n String newContent = await File(newPath).readAsString();\\n\\n Map<String, dynamic> oldJson = jsonDecode(oldContent);\\n Map<String, dynamic> newJson = jsonDecode(newContent);\\n\\n List<String> updates = [];\\n\\n void compareVersions(\\n Map<String, dynamic> oldModule, Map<String, dynamic> newModule) {\\n // 递归比较子模块\\n if (oldModule.containsKey(\'children\') &&\\n newModule.containsKey(\'children\')) {\\n List<dynamic> oldChildren = oldModule[\'children\'];\\n List<dynamic> newChildren = newModule[\'children\'];\\n\\n for (int i = 0; i < oldChildren.length; i++) {\\n compareVersions(oldChildren[i], newChildren[i]);\\n }\\n }\\n\\n // 比较当前模块版本\\n if (oldModule[\'version\'] != newModule[\'version\']) {\\n updates.add(\\n \'${newModule[\'name\']}: ${oldModule[\'version\']} -> ${newModule[\'version\']}\');\\n }\\n }\\n\\n compareVersions(oldJson, newJson);\\n\\n return updates;\\n}\\n
\\n发布 pub 包本质上就是调一下 dart pub publish
的命令,在桌面端可以通过 Process.start
来调用命令。由于发布 pub 要科学上网,可以在 environment 参数中设置网络代理:
Future<void> publishModule(String name) async{\\n Directory dir = Directory.current;\\n String path = p.join(dir.path, \'modules\', name);\\n print(path);\\n await publish(path, port: 7890);\\n}\\n\\nFuture<void> publish(String dir, {required int port}) async {\\n Process process = await Process.start(\\n workingDirectory: dir,\\n \'dart\',\\n [\'pub\', \'publish\', \'--server\', \'https://pub.dartlang.org\', \'-f\'],\\n environment: {\'https_proxy\': \'http://127.0.0.1:$port\'},\\n );\\n\\n process.stdout.listen((e) {\\n String value = utf8.decode(e, allowMalformed: true);\\n print(value);\\n });\\n\\n process.stderr.listen((e) {\\n String value = utf8.decode(e, allowMalformed: true);\\n print(value);\\n });\\n}\\n
\\n到这里,整体的流程就跑通了,从分析模块,构建结构化数据,到记录更新日志、对比更新模块,自动发布。一套下来,可以节约很多手动管理的成本。而到这里,我只用了不到 1 个小时。如果在一年前,从编码到调试,我估计要写上两天的时间。
\\nTrae 在其中起到了很多作用,包括树形结构的各种操作、文件转换等。 AI 确实在真实地改变着编程工作的流程。特别是对于算法不是很好的朋友,他可以为你完成很多以前完不成的事。但是,在此基础上,也不能忘记审视 AI 的内容,好的地方可以学习;不好的地方也要及时更正。它将是你的一把趁手的兵器,而非你的大脑。
\\n相比于 那本文就到这里,以后关于 Trae 帮助我解决实际问题的场景,我也会继续分享,下次再见 ~
","description":"随着 TolyUI 的维护,我遇到了一些模块管理上的问题。比如: 一个子模块版本更新时,我需要把依赖它的父级模块更新一个小版本。\\n我很难直观地看出当前所有模块的版本信息\\nmarkdown 版的CHANGELOG 不太好维护,更新时写起来也比较麻烦\\n\\n手动处理比较麻烦,所以我打算写个 Dart 脚本,来分析模块依赖结构。完成自动发布和展示当前所有模块信息的功能。刚好最近 Trae 发布了,就用它来帮我分析和书写一些代码:\\n\\n1. 让 AI 分析生成 module.json\\n\\n首先我提前准备了一个 module.json, 问它:\\n\\n这个文件的作用。\\n\\n当…","guid":"https://juejin.cn/post/7479331614620925963","author":"张风捷特烈","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-09T23:05:42.024Z","media":[{"url":"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/eaf47e7225b44e9e9acd37805afdf619~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1920&h=1200&s=398049&e=png&b=fefbfb","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/db0306c27e90495e8f622e242ed0eed8~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1791&h=1197&s=278288&e=png&b=ffffff","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/11a803df2ee548a6bfa7dd34bc9a4c39~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1282&h=894&s=58919&e=png&b=ffffff","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ec38898674ca4e228dd8d1850ca36b48~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=791&h=488&s=79027&e=png&b=ffffff","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/81b8c13a7162452dac753a7717cf293e~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=661&h=147&s=14393&e=png&b=ffffff","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/071de049bdd14bc29e53ab2e561d0290~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1067&h=962&s=122168&e=png&b=eff2f4","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/68491fcdc9684fe4aa19faffb00aaae8~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=756&h=144&s=12259&e=png&b=ffffff","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/58806bd542dc490f824e883bd163232f~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1624&h=1245&s=269243&e=png&b=ffffff","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9179e5a0d4d742b29bb500613d8b61dc~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1578&h=1069&s=219491&e=png&b=ffffff","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2bb80de2407d48fba52b45d2e3266f4a~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1091&h=264&s=29544&e=png&b=ffffff","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/03057e5d87944c2eb68c970af55a3667~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1330&h=637&s=60309&e=png&b=ffffff","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fde94abda76743a58664cb479b71259f~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1428&h=494&s=49631&e=png&b=fffbfa","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4fecdf41edf246339a9f0c9cb06dae80~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=870&h=143&s=11519&e=png&b=ffffff","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/eaaf8f7bc5e04b08bc1e412467ef4466~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1049&h=479&s=50429&e=png&b=ffffff","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/01da0a59c87d453f89340c446814f019~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=998&h=339&s=25526&e=png&b=ffffff","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Trae"],"attachments":null,"extra":null,"language":null},{"title":"再聊 Flutter Riverpod ,注解模式下的 Riverpod 有什么特别之处,还有发展方向","url":"https://juejin.cn/post/7479474972849143844","content":"三年前我们通过 《Flutter Riverpod 全面深入解析》 深入理解了 riverpod 的内部实现,而时隔三年之后,如今Riverpod 的主流模式已经是注解,那今天就让我们来聊聊 riverpod 的注解有什么特殊之处。
\\n在此之前,我们需要先回忆一下,riverpod 最明显的特点是将 BuildContext
转换成 WidgetRef
抽象 ,从而让状态管理不直接依赖 BuildContext
,所以对应的 Provider 可以按需写成全局对象,而在 riverpod 里,主要的核心对象有:
InheritedWidget
实现,共享实例的顶层存在,提供一个 ProviderContainer
全局共享Create
函数会在 “Element” 内通过 \\"setState
\\" 调用执行,比如 StateProvider((ref)=> 0)
这里的 ref ,就是内部在 ”Element“ 里通过 setState(_provider.create(this));
\\" 的时候传入的 thisBuildContext
的抽象,内部通过继承 StatefulWidget
实现,作为 BuildContext 的对外替代\\n\\n所以 \\"Provider\\" 里的
\\nRef
和 “Consumer” 的WidgetRef
严格来说是两个不同的东西,只是它们内部都可以获取到ProviderContainer
,从而支持对应 read\\\\watch\\\\refesh 等功能,这也是为什么你在外部直接通过ProviderContainer
也可以全局直接访问到 read\\\\watch\\\\refesh 的原因。
另外,riverpod 内部定义了自己的 「Element」 和 「setState」实现,它们并不是 Flutter 里的 Element 和 setState,所以上面都加了 “”,甚至 riverpod 里的 “Provider” 和 Provider 状态管理库也没有关系, 这么设计是为了贴合 Flutter 本身的 「Element」 和 「setState」概念,所以这也是为什么说 riverpod 是专为 Flutter 而存在的设计。
\\n现在 riverpod 更多提倡使用注解模式,注解模式可以让 riverpod 使用起来更方便且规范,从一定程度也降低了使用难度,但是也对初学者屏蔽了不少过去的手写实现,导致在出现问题时新手也可能会相对更蒙。
\\n首先我们看这个简单的代码,我们在 main.dart
里添加了了一个 @riverpod
给 helloWorld
,然后运行 flutter pub run build_runner build --delete-conflicting-outputs
,可以看到此时生成了对应的 main.g.dart
文件:
import \'package:flutter/material.dart\';\\nimport \'package:flutter_riverpod/flutter_riverpod.dart\';\\nimport \'package:riverpod_annotation/riverpod_annotation.dart\';\\n\\npart \'main.g.dart\';\\n\\n@riverpod\\nString helloWorld(Ref ref) {\\n return \'Hello world\';\\n}\\nclass MyApp extends ConsumerWidget {\\n @override\\n Widget build(BuildContext context, WidgetRef ref) {\\n final String value = ref.watch(helloWorldProvider);\\n\\n return MaterialApp(\\n home: Scaffold(\\n appBar: AppBar(title: const Text(\'Example\')),\\n body: Center(\\n child: Text(value),\\n ),\\n ),\\n );\\n }\\n}\\n
\\n我们看 main.g.dart
文件,可以看到,根据 @riverpod
的规则, helloWorld
会生成一个 helloWorldProvider
实例让我们在使用时 read/watch/refresh :
// GENERATED CODE - DO NOT MODIFY BY HAND\\n\\npart of \'main.dart\';\\n\\n// **************************************************************************\\n// RiverpodGenerator\\n// **************************************************************************\\n\\nString _$helloWorldHash() => r\'9abaa5ab530c55186861f2debdaa218aceacb7eb\';\\n\\n/// See also [helloWorld].\\n@ProviderFor(helloWorld)\\nfinal helloWorldProvider = AutoDisposeProvider<String>.internal(\\n helloWorld,\\n name: r\'helloWorldProvider\',\\n debugGetCreateSourceHash:\\n const bool.fromEnvironment(\'dart.vm.product\') ? null : _$helloWorldHash,\\n dependencies: null,\\n allTransitiveDependencies: null,\\n);\\n\\n@Deprecated(\'Will be removed in 3.0. Use Ref instead\')\\n// ignore: unused_element\\ntypedef HelloWorldRef = AutoDisposeProviderRef<String>;\\n// ignore_for_file: type=lint\\n// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package\\n\\n
\\n通过生成的代码,我们可以看到:
\\n _$helloWorldHash()
:它主要是用于提供一个唯一标识,用于追踪 Provider 的来源和状态,它是被 debugGetCreateSourceHash
所使用,例如在 Debug 模式下 hotload 时,riverpod 会用这个值来判断当前 provider 是否需要重建,比如当你重新生成的时候 hash 值就会出现变化。helloWorldProvider
: AutoDisposeProvider
的实例,也就是默认情况下 @riverpod
生成的都是自动销毁的 Provider ,\\n\\n这里默认使用
\\nAutoDisposeProvider
,也是为了更好的释放内存和避免不必需要的内存泄漏等场景,AutoDisposeProvider
内部,在每次read
、invalidate
、页面退出、ProviderContainer
销毁等场景会自动调用 dispose 。
接着,如果给 helloWorld
增加 async ,那么我们得到一个 AutoDisposeFutureProvider
,同理,如果是 async*
就会生成一个 AutoDisposeStreamProvider
:
@riverpod\\nFuture<String> helloWorld(Ref ref) async{\\n return \'Hello world\';\\n}\\n\\n------------------------------GENERATED CODE---------------------------------\\n\\n@ProviderFor(helloWorld)\\nfinal helloWorldProvider = AutoDisposeFutureProvider<Object?>.internal(\\n helloWorld,\\n name: r\'helloWorldProvider\',\\n debugGetCreateSourceHash:\\n const bool.fromEnvironment(\'dart.vm.product\') ? null : _$helloWorldHash,\\n dependencies: null,\\n allTransitiveDependencies: null,\\n);\\n
\\n当然,在返回结果使用上会有些差别, 异步的 Provider 会返回一个 AsyncValue
,或者需要 .value
获取一个非空安全的对象:
@override\\nWidget build(BuildContext context, WidgetRef ref) {\\n final AsyncValue<String> asyncValue = ref.watch(helloWorldProvider);\\n final String? value = ref.watch(helloWorldProvider).value;\\n return MaterialApp(\\n home: Scaffold(\\n appBar: AppBar(title: const Text(\'Example\')),\\n body: Center(\\n child: Text(asyncValue.when(\\n data: (v) => v,\\n error: (_, __) => \\"error\\",\\n loading: () => \\"loading\\")),\\n ),\\n ),\\n );\\n}\\n
\\n当你需要给 helloWorld
增加参数的时候,此时的 helloWorldProvider
就不再是一个 AutoDisposeFutureProvider
实例,它将变成 HelloWorldFamily
,它是一个 Family
的实现:
@riverpod\\nFuture<String> helloWorld(Ref ref, String value, String type) async {\\n return \'Hello world $value $type\';\\n}\\n\\n@override\\nWidget build(BuildContext context, WidgetRef ref) {\\n final AsyncValue<String> asyncValue = ref.watch(helloWorldProvider(\\"1\\", \\"2\\"));\\n final String? value = ref.watch(helloWorldProvider(\\"1\\", \\"2\\")).value;\\n}\\n\\n------------------------------GENERATED CODE---------------------------------\\n\\n/// See also [helloWorld].\\nclass HelloWorldFamily extends Family<AsyncValue<String>> {\\n /// See also [helloWorld].\\n const HelloWorldFamily();\\n\\n /// See also [helloWorld].\\n HelloWorldProvider call(\\n String value,\\n String type,\\n ) {\\n return HelloWorldProvider(\\n value,\\n type,\\n );\\n }\\n\\n @override\\n HelloWorldProvider getProviderOverride(\\n covariant HelloWorldProvider provider,\\n ) {\\n return call(\\n provider.value,\\n provider.type,\\n );\\n }\\n\\n static const Iterable<ProviderOrFamily>? _dependencies = null;\\n\\n @override\\n Iterable<ProviderOrFamily>? get dependencies => _dependencies;\\n\\n static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;\\n\\n @override\\n Iterable<ProviderOrFamily>? get allTransitiveDependencies =>\\n _allTransitiveDependencies;\\n\\n @override\\n String? get name => r\'helloWorldProvider\';\\n}\\n\\n
\\n\\n\\n在 Dart 中,call 方法是一个特殊的方法,它可以让一个类的实例像函数一样调用。
\\n
说到 Family
, 它的作用是主要就是支持使用额外的参数构建 Provider ,因为前面 helloWorld
需要传递参数,所以 HelloWorldFamily
的主要作用,就是提供创建和覆盖需要参数的 Provider,例如前面的:
final AsyncValue<String> asyncValue = ref.watch(helloWorldProvider(\\"1\\", \\"2\\"));\\n final String? value = ref.watch(helloWorldProvider(\\"1\\", \\"2\\")).value;\\n
\\n当然,这里你需要注意,不同与前面的 helloWorldProvider
实例,需要参数的 Provider 需要你每次使用时通过参数构建,而此时你每次调用如 helloWorldProvider(\\"1\\", \\"2\\")
都是创建了一个全新实例,如果你需要同一个数据源下 read/watch ,那么你应该在调用时共用一个全局 helloWorldProvider(\\"1\\", \\"2\\")
实例。
如果是不同 Provider 实例,那么你获取到的参数其实是不一样的,因为内部 map 登记的映射关系就是基于 Provider 实例为 key :
\\n不过对比之下,过去你使用 FutureProvider.family
只能覆带一个 Arg
参数,虽然可以通过语法糖传递多个参数,但是终究还是比注解生成的麻烦:
final helloWorldFamily =\\n FutureProvider.family<String, (String, String)>((value, type) async {\\n return \'Hello world $value $type\';\\n});\\n
\\n另外,注解生成时,还会动态生成一个对应的 \\"Element\\" ,让 Element 支持获取 Provider 的参数,并实现对应 build
方法,也就是通过 ref 可以获取到相关参数:
mixin HelloWorldRef on AutoDisposeFutureProviderRef<String> {\\n /// The parameter `value` of this provider.\\n String get value;\\n\\n /// The parameter `type` of this provider.\\n String get type;\\n}\\n\\nclass _HelloWorldProviderElement\\n extends AutoDisposeFutureProviderElement<String> with HelloWorldRef {\\n _HelloWorldProviderElement(super.provider);\\n\\n @override\\n String get value => (origin as HelloWorldProvider).value;\\n @override\\n String get type => (origin as HelloWorldProvider).type;\\n}\\n
\\n最后,带参数之后,生成的 _SystemHash
也会根据参数动态变化,从而支持 hotload 等场景:
class _SystemHash {\\n _SystemHash._();\\n\\n static int combine(int hash, int value) {\\n // ignore: parameter_assignments\\n hash = 0x1fffffff & (hash + value);\\n // ignore: parameter_assignments\\n hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));\\n return hash ^ (hash >> 6);\\n }\\n\\n static int finish(int hash) {\\n // ignore: parameter_assignments\\n hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));\\n // ignore: parameter_assignments\\n hash = hash ^ (hash >> 11);\\n return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));\\n }\\n}\\n
\\n接着,我们看 @riverpod
除了可以注解函数之后,还可以直接注解 class ,只是 class 需要继承 _$***
一个子类:
@riverpod\\nclass HelloWorld extends _$HelloWorld {\\n @override\\n String build() {\\n return \'Hello world\';\\n }\\n changeValue(String value) {\\n state = value;\\n }\\n}\\n\\n@override\\nWidget build(BuildContext context, WidgetRef ref) {\\n final String asyncValue = ref.watch(helloWorldProvider);\\n ref.read(helloWorldProvider.notifier).changeValue(\\"next\\");\\n}\\n
\\n通过生成代码可以看到,此时生成的是 AutoDisposeNotifierProvider
,也就是在读取时,可以通过 read(****Provider.notifier)
去改变状态:
String _$helloWorldHash() => r\'52966cfeefb6334e736061e19443e4c8b94160d8\';\\n\\n/// See also [HelloWorld].\\n@ProviderFor(HelloWorld)\\nfinal helloWorldProvider =\\n AutoDisposeNotifierProvider<HelloWorld, String>.internal(\\n HelloWorld.new,\\n name: r\'helloWorldProvider\',\\n debugGetCreateSourceHash:\\n const bool.fromEnvironment(\'dart.vm.product\') ? null : _$helloWorldHash,\\n dependencies: null,\\n allTransitiveDependencies: null,\\n);\\n\\ntypedef _$HelloWorld = AutoDisposeNotifier<String>;\\n
\\n也就是,通过 @riverpod
注解的 class ,是带有 state 状态的 NotifierProvider ,这是对比注解函数最明显的差异。
而如果注解 class 需要携带参数,那么可以在 build
上添加需要的参数,最终同样和函数一样会生成一个对应的 HelloWorldFamily
:
@riverpod\\nclass HelloWorld extends _$HelloWorld {\\n @override\\n String build(String value, String type) {\\n return \'Hello world\';\\n }\\n\\n changeValue(String value) {\\n state = value;\\n }\\n}\\n
\\n同理,如果你给 build 增加了 async,那么就会生成一个 AutoDisposeAsyncNotifierProviderImpl
的相关实现:
@riverpod\\nclass HelloWorld extends _$HelloWorld {\\n @override\\n Future<String> build(String value, String type) async {\\n return \'Hello world\';\\n }\\n\\n changeValue(String value) {\\n final currentValue = state.valueOrNull ?? \\"\\";\\n state = AsyncData(currentValue + value);\\n }\\n \\n removeString(String value) {\\n final currentValue = state.valueOrNull ?? \\"\\";\\n state = state.copyWithPrevious(AsyncData(currentValue.replaceAll(value, \\"\\")));\\n }\\n}\\n
\\n可以看到,在注解 class 下可操作空间是在 build ,并且需要注意的是,当你调用 refresh
的时候,State 是会被清空,并且重新调用 build。
那么我们前面说的都是 AutoDispose ,如果我不想他被释放呢?那就是需要用到大写字母开头的 @Riverpod
,给参数配置上 keepAlive: true
:
@Riverpod(keepAlive: true)\\nclass HelloWorld extends _$HelloWorld \\n
\\n然后再看输出文件,你就会看到此时 HelloWorldProvider
继承的是 AsyncNotifierProviderImpl
而不是 AutoDispose
了:
class HelloWorldProvider extends AsyncNotifierProviderImpl<HelloWorld, String> {\\n /// See also [HelloWorld].\\n HelloWorldProvider(\\n String value,\\n String type,\\n ) : this._internal(\\n
\\n另外 @Riverpod
还有另外一个可配置参数 dependencies
,从名字上理解起来是依赖的意思,但是其实它更多用于「作用域」相关的处理。
在 riverpod 里,框架的设计是支持多个 ProviderContainer 的场景,并且每个容器可以覆盖(override)某些 Provider 的数据,例如我只是添加了一个 dependencies: []
,此时无论列表是否为空,它都可以被认为是一个具有作用域支持的 Provider,从而实现根据上下文进行数据隔离,另外不为空时还可以看作声明 Provider 在作用域内的依赖关系。
@Riverpod(dependencies: [])\\n
\\n但是,不是你加了 dependencies 它就自动产生作用域隔离了,不为空时也不会自动追加依赖,它只是一个声明作用,后续还是需要代码配合。
\\n如下代码所示,这里简单的声明了一个带有 dependencies
的 Counter
,然后:
ref.watch(counterProvider)
监听了 Counterref2.watch(counterProvider)
监听了 Counter@Riverpod(dependencies: [])\\nclass Counter extends _$Counter {\\n @override\\n int build() => 0;\\n\\n void update(int count) {\\n state = count;\\n }\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return ProviderScope(\\n child: MaterialApp(\\n home: Consumer(builder: (ctx, ref, __) {\\n final count = ref.watch(counterProvider);\\n return Scaffold(\\n appBar: AppBar(),\\n body: Column(\\n mainAxisAlignment: MainAxisAlignment.start,\\n children: [\\n Text(\'Counter: $count\'),\\n ElevatedButton(\\n onPressed: () {\\n showDialog(\\n context: ctx,\\n builder: (context) => AlertDialog(\\n title: Text(\'Dialog\'),\\n content: Consumer(builder: (_, ref2, __) {\\n final count2 = ref2.watch(counterProvider);\\n return InkWell(\\n onTap: () {\\n ref2\\n .read(counterProvider.notifier)\\n .update(count2 + 1);\\n },\\n child: Text(\'Dialog Counter: $count2\'),\\n );\\n })),\\n );\\n },\\n child: Text(\'Open Dialog\'),\\n ),\\n ],\\n ),\\n floatingActionButton: FloatingActionButton(onPressed: () {\\n ref.read(counterProvider.notifier).update(count + 1);\\n }),\\n );\\n }),\\n ),\\n );\\n }\\n}\\n\\n
\\n结果最后运行发现,Dialog 和主页的 Counter 其实还是共享的, dependencies
并没有起到作用:
之所以这样,原因在于没有增加新的 ProviderScope
,如下代码所示,只要将上面的 showDialog
部分修改为如下代码所示:
ProviderScope
overrides
指定对应 counterProvider
showDialog(\\n context: ctx,\\n builder: (context) => ProviderScope(\\n overrides: [\\n counterProvider,\\n ///你还可以 overrideWith 覆盖修改\\n //counterProvider.overrideWith(()=>Counter())\\n ],\\n child: AlertDialog(\\n title: Text(\'Dialog\'),\\n content: Consumer(builder: (_, ref2, __) {\\n final count2 = ref2.watch(counterProvider);\\n return InkWell(\\n onTap: () {\\n ref2\\n .read(counterProvider.notifier)\\n .update(count2 + 1);\\n },\\n child: Text(\'Dialog Counter: $count2\'),\\n );\\n })),\\n ),\\n);\\n
\\n以上条件缺一不可以,运行后如下图所示,可以看到此时 counterProvider
在主页和 Dialog 之间被有效分割开:
其实原因从源码里也可以看出来,在 ProviderContainer
内部源码我们可以看到,要产生一个独立的作用域,你需要:
ProviderContainer
dependencies
且 ProviderContainer
的 override 不为空,也就是 dependencies
不为 null 就行,但是 override 必须有 Provider_StateReader
用于提供状态数据所以,从这里就可以看出,dependencies
只是一个先置条件,具体它是不是局部作用域,还得是你用的时候怎么用。
同理依赖也是,比如你写了一个 @Riverpod(dependencies: [maxCountProvider])
,但是你还是需要对应写上 ref.watch(maxCountProvider)
,不然它也并不起作用:
@Riverpod(dependencies: [maxCountProvider])\\nint limitedCounter(LimitedCounterRef ref) {\\n final max = ref.watch(maxCountProvider); // 监听 \\n return 0.clamp(0, max); \\n}\\n
\\n\\n\\nPS ,如果你只是正常监听,不需要作用域的场景,其实直接写
\\nref.watch
而不需要dependencies: [maxCountProvider]
也是可以的。
如果我们从输出端看,可以看到有没有 dependencies
,主要就是 _dependencies
和 _allTransitiveDependencies
是否为空的区别:
最后也有一些注意事项,例如:
\\n通过注解生成的 Provider 好不要依赖非生成的 Provider,比如这里的 example
是注解,它监听了一个非注解生成的 depProvider
,这样并不规范:
final depProvider = Provider((ref) => 0);\\n\\n@riverpod\\nvoid example(Ref ref) {\\n // Generated providers should not depend on non-generated providers\\n ref.watch(depProvider);\\n}\\n
\\n有作用域时,如果监听了某个 Provider ,那么 dependencies 里必须写上依赖 Provider,以下写法就不合规:
\\n@Riverpod(dependencies: [])\\nvoid example(Ref ref) {\\n // scopedProvider is used but not present in the list of dependencies\\n ref.watch(scopedProvider);\\n}\\n
\\nProvider 里不应该接收 BuildContext
:
// Providers should not receive a BuildContext as a parameter.\\n@riverpod\\nint fn(Ref ref, BuildContext context) => 0;\\n\\n@riverpod\\nclass MyNotifier extends _$MyNotifier {\\n int build() => 0;\\n\\n // Notifiers should not have methods that receive a BuildContext as a parameter.\\n void event(BuildContext context) {}\\n}\\n
\\n其实类型的注意事项在 riverpod_lint 里都声明了,只是 Custom lint rules 不会直接展示在 dart analyze ,所以需要用户在添加完 riverpod_lint 后,执行对应的 dart run custom_lint
:
可以看到,通过注解模式,riverpod 可以让开发者少些很多代码,在整体设计理念没有变化的情况下,模版生成的代码会更规范,并且在上层屏蔽了许多复杂度和工作量。
\\n另外通过 dependencies
我们可以可以看到 riverpod 在存储管理上它是统一的,但是在组合上它是分散的的设计理念。
而 Flutter 状态管理一直以来也是「是非之地」,比如近期就出现说 riverpod 在基准性能测试表示不如 signals 的情况,但是作者也回应了该测试属于「春秋笔法」之流:
\\n另外,由于Dart 宏功能推进暂停 ,而 build runner 与数据类的优化还没落地,作者也在探索没有 codegen 下如何也可以便捷使用 riverpod ,比如让 family 支持多个参数:
\\n当然,从作者的维护体验上看,貌似作者又有停滞 codegen 的倾向,看起来左右摇摆的状态还会持续一段时间:
\\n那么, 2025 年 riverpod 还是你状态管理的首选吗?
","description":"三年前我们通过 《Flutter Riverpod 全面深入解析》 深入理解了 riverpod 的内部实现,而时隔三年之后,如今Riverpod 的主流模式已经是注解,那今天就让我们来聊聊 riverpod 的注解有什么特殊之处。 前言\\n\\n在此之前,我们需要先回忆一下,riverpod 最明显的特点是将 BuildContext 转换成 WidgetRef 抽象 ,从而让状态管理不直接依赖 BuildContext ,所以对应的 Provider 可以按需写成全局对象,而在 riverpod 里,主要的核心对象有:\\n\\nProviderScop…","guid":"https://juejin.cn/post/7479474972849143844","author":"恋猫de小郭","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-09T22:36:21.067Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d2600410141343508671ab8e4c3e5dff~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1742164580&x-signature=ozSONChTbZH4zcmG0h8sz6mhZuI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5feeeb9dea9e4c95850503a108af92cd~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1742164580&x-signature=2vOTn7NjoSRlgtsPCkPeyiK9lDU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/16118f916b1146ad9a5173622c21cf45~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1742164580&x-signature=55vxDDUh%2FZlhv11dz5N6SRnDgDc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fdaf0fdb233f4b7a8c33d4cbbf03a120~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1742164580&x-signature=%2Fjt760B5%2BlLwRPbdU7bmfcXukTQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/02fd8d633ced4293ac392c22b630e154~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1742164580&x-signature=U0RkTWvfc1NEKnb5Y19DrhMUNag%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2a065703002c440385900b78591da6aa~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1742164580&x-signature=f%2FxLmQGcjUkeFFI%2BpDml7ZpBJ14%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/41221ca7a00a42f49883457414e906f9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1742164580&x-signature=pirRStVtDvPWU%2FRvpiFA%2B4ek5sQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3d618e7e9e2b40ea8154e93745d24229~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1742164580&x-signature=Li5lWr5zr1GXGV5mGiXuf7OfvBk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/370763d7e6ec4b9c8270878c5623d93e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1742164580&x-signature=6yvadB7o8PHhlq%2Fpzcv7caS2is8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c23e7bd4a6664f2aa49a08f1147b212e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1742164580&x-signature=yBAwNLJzyQRIWQVwIueaerZkRD4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/47cfb0c419e04f2ebf4ccf7c73005a0c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1742164580&x-signature=nGPdO6hF9twybv%2B%2B8VTZZcFWS5A%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d815a613d9c2486589edaa37f76adc40~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1742164580&x-signature=G1dpOrKlGt4uQBYGlkeUtk%2BzAg8%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"从0到1掌握Flutter(二)环境搭建与认识工程","url":"https://juejin.cn/post/7479284918784557092","content":"接上篇从0到1掌握Flutter(一)Flutter与移动端跨平台
\\n本文基于Windows系统进行演示,绍了 Flutter 开发环境的配置过程,包括安装依赖、下载 Flutter SDK、配置环境变量以及检查环境等步骤,认识了 Flutter 工程的目录结构和主要文件,搭建跨平台应用的基础框架。
\\n💡 Git
\\nFlutter SDK版本管理与依赖更新需要依赖Git工具实现,是构建开发环境的必要前置条件。在新设备配置开发环境时,可以按照以下流程进行安装验证:
\\n访问Git官方下载中心下载安装包,并安装。
\\n安装后,命令行输入(Windows CMD/PowerShell 或 macOS 终端通用)git --version
如果提示git version 2.39.2 (Apple Git-143)
表示Git环境已正确安装并配置。
💡 Android Studio
\\nAndroid Studio是Google 官方推荐的开发工具, 提供完整的 Flutter 开发支持,包括插件管理、模拟器调试、原生代码编译等。
\\n要求安装Android Studio 2024.1.1 (Koala) 或更高版本来调试和编译 Android 的 Java 或 Kotlin 代码。
\\n可以访问Android Studio 中国区镜像站 或 国际版官网 下载安装。
\\n💡 Flutter插件
\\n安装完成后,在启动首页点击 Plugins,在Marketplace中搜索Flutter插件并安装。
\\n该插件可以提升 Flutter 开发效率,简化复杂操作。以下是其核心功能:
\\n快速生成标准 Flutter 工程结构(含基础应用、模块化应用、包/插件项目模板)
\\n语法高亮,区分 Widget、Dart 关键字、字符串等代码元素、自动补全、语法诊断等
\\n热重载:修改代码后保存即可实时刷新运行中的应用
\\n设备与模拟器控制:支持多设备并行调试,同时连接 Android 手机、iOS 模拟器、Web 浏览器
\\nUI 层级分析,可视化查看 Widget 树结构
\\n注意:需要启用Android APK Support插件,如果没有开启,Android Studio 将没有创建Flutter工程的入口。
\\n💡 SDK包获取
\\n访问Flutter官网安装指南选择自己设备的SDK下载并解压。Flutter SDK 包含了 Flutter 开发所需的工具、库和文档。
\\n不要将 Flutter 安装到以下情况的目录或路径中:
\\n路径包含特殊字符或空格。
\\n路径需要较高的权限。
\\nSDK安装完成后,需要将 Flutter 添加到 PATH
环境变量后,才能在 PowerShell 中运行 Flutter。指假定你在 %USERPROFILE%\\\\dev\\\\flutter
中安装了 Flutter SDK。
%USERPROFILE%\\\\dev\\\\flutter\\\\bin
。Path
。在 变量值 框中,输入 %USERPROFILE%\\\\dev\\\\flutter\\\\bin
。当环境配好以后,可以通过下面的方式验证环境配置是否正确
\\n命令行输入执行:
\\nflutter --version\\n
\\n如果输出了版本信息表示Flutter环境配置成功
\\n然后我们可以使用flutter doctor 检查其他环境是否完善。这个命令会检查系统上的各种依赖是否安装正确,并给出相应的提示信息。如果存在问题,按照提示信息进行相应的解决即可。
\\n命令行输入执行:
\\nflutter doctor\\n
\\n典型输出结构如下:
\\n[!] Android toolchain - develop for Android devices \\n ✗ Android licenses not accepted \\n[✓] Chrome - develop for the web \\n[!] Android Studio (version 2022.3) \\n ✗ Unable to find bundled Java version \\n[✓] Connected device (1 available) \\n
\\n可以看到有三种不同的输出状态:
\\n✓ 绿色对勾:组件已就绪,保持现状即可
\\n! 黄色感叹号:存在可优化项,选择性处理,影响开发体验
\\n✗ 红色叉号:必须修复的阻断性问题,影响编译能力
\\n对于阻断性问题,系统会输出详细的诊断日志,结构如下:
\\n[组件类别] • [检查项] (状态) [具体描述] [修复建议]
\\n[X] Visual Studio - develop for Windows\\n X Visual Studio not installed; this is necessary for Windows development.\\n Download at https://visualstudio.microsoft.com/downloads/.\\n Please install the \\"Desktop development with C++\\" workload, including all of its default components\\n
\\n当我们在Android Studio中完成Kotlin插件配置后(详见1.1节),IDE顶部工具栏多了一个\\"New Flutter Project\\"按钮,这就是创建Flutter工程的入口。点击他就可以创建一个Flutter工程。
\\n首次创建工程时需手动指定Flutter SDK路径,选择flutter/bin上级目录即可。(如/Users/name/flutter)选择后点击Next。
\\n最后然后输入工程名、包名,注意名称中不能包含大写字母。选择语言,以及需要支持的端,这里选择Android和iOS,点击Creat,等待IDE创建工程即可。
\\n此时IDE会自动打开main.dart文件,开发者可直接运行到设备查看初始界面。
\\n创建好 Flutter 工程后,让我们来了解一下它的目录结构。 在Android Studio的工程面板中,有这几个主要的部分。
\\n目录/文件 | 作用描述 |
---|---|
android/ | 安卓原生工程文件 |
ios/ | iOS原生工程文件 |
lib/ | Dart源码目录(主开发区域) |
pubspec.yaml | 依赖管理文件 |
test/ | 单元测试代码 |
💡 主要文件
\\n“main.dart”文件是 Flutter 应用的入口点。它包含一个名为“main”的顶级函数,这个函数是应用启动时首先执行的代码。在“main”函数中,我们通常会调用“runApp”函数,并传入一个根 Widget。例如IDE自动为我们创建的默认Demo:
\\nimport \'package:flutter/material.dart\';\\n\\nvoid main() {\\n runApp(const MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n const MyApp({super.key});\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n title: \'Flutter Demo\',\\n theme: ThemeData(\\n primarySwatch: Colors.blue,\\n ),\\n home: const MyHomePage(title: \'Flutter Demo Home Page\'),\\n );\\n }\\n}\\n\\n//....\\n
\\n我们在顶部设备栏选择需要运行的设备,然后直接点击运行按钮,就可以运行。可以在看各个平台的效果。
\\n页面顶部显示着\'Flutter Demo\'标题栏,中央区域包含提示文字和一个醒目的数字计数器,右下角的蓝色圆形按钮格外显眼。每次点击按钮计数器上的数字就会+1。
\\n可以看到在手机、网页等不同设备上,按钮的点击反馈、文字的排版布局、数字的放大效果都保持高度统一,无需任何额外设置即可实现多端一致的显示效果。
\\n下面是在web上运行与在Android手机上运行的效果:
\\n\\n
如前文所述,“pubspec.yaml”文件用于管理项目的依赖和元数据。下面是IDE自动为我们创建的“pubspec.yaml”文件示例:
\\nname: my_flutter_app \\ndescription: A new Flutter application. \\nversion: 1.0.0 \\nenvironment: \\n sdk: \\">=2.10.0 <3.0.0\\" \\ndependencies: \\n flutter: \\n sdk: flutter \\n cupertino_icons: ^1.0.2 \\ndev_dependencies: \\n flutter_test: \\n sdk: flutter \\nflutter: \\n uses-material-design: true \\n
\\n这个文件中定义了项目的名称、描述、版本等信息。在“dependencies”部分,声明了项目依赖的 Flutter 库和插件,这里依赖了“cupertino_icons”插件用于在 iOS 风格的应用中使用图标。在“dev_dependencies”部分,声明了开发过程中使用的依赖,例如“flutter_test”用于编写测试代码。最后,“flutter”部分配置了应用使用 Material Design 风格。
\\n通过以上内容,我们了解了 Flutter 开发环境的配置过程,包括安装依赖、下载 Flutter SDK、配置环境变量以及检查环境等步骤,认识了 Flutter 工程的目录结构和主要文件。
\\n在后续的文章中,我们将基于这个配置好的环境和工程结构,进一步学习 Flutter。让我们一起期待在 Flutter 开发的道路上不断前。
","description":"引文 接上篇从0到1掌握Flutter(一)Flutter与移动端跨平台\\n\\n本文基于Windows系统进行演示,绍了 Flutter 开发环境的配置过程,包括安装依赖、下载 Flutter SDK、配置环境变量以及检查环境等步骤,认识了 Flutter 工程的目录结构和主要文件,搭建跨平台应用的基础框架。\\n\\n一、环境搭建\\n1.1 开发工具\\n\\n💡 Git\\n\\nFlutter SDK版本管理与依赖更新需要依赖Git工具实现,是构建开发环境的必要前置条件。在新设备配置开发环境时,可以按照以下流程进行安装验证:\\n\\n访问Git官方下载中心下载安装包,并安装。\\n\\n安装…","guid":"https://juejin.cn/post/7479284918784557092","author":"A0微声z","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-09T14:37:33.038Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/8852e632f37f4c66bac86e035451b554~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQTDlvq7lo7B6:q75.awebp?rk3s=f64ab15b&x-expires=1742135853&x-signature=6Nm70DVBCLAO3cciHsu4G89kCO8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/34c10ee1e2d5424cba20d2a7ac0acc02~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQTDlvq7lo7B6:q75.awebp?rk3s=f64ab15b&x-expires=1742135853&x-signature=nf0ob3YJQ5QCWcvZUt%2F3wMS1BUg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2a8978efa18c4f72babe8a64595d9d11~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQTDlvq7lo7B6:q75.awebp?rk3s=f64ab15b&x-expires=1742135853&x-signature=WYt%2B9qGQRQXjRiwq%2FLowJmQakXQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7f2a6fbabc734ad190d44fc526eddde5~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQTDlvq7lo7B6:q75.awebp?rk3s=f64ab15b&x-expires=1742135853&x-signature=3ttVXvdmGW1kB6Pzow7prQorIbI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/df2d9cfc91ac4e4d84da7520804a62a1~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQTDlvq7lo7B6:q75.awebp?rk3s=f64ab15b&x-expires=1742135853&x-signature=iGnjMDCSfAE770U8ZUIbeosxqjA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/41eb6210b7ed4e7fb7bf11b81e1b1fa8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQTDlvq7lo7B6:q75.awebp?rk3s=f64ab15b&x-expires=1742135853&x-signature=0hZgFArrQt8OSfW2lyGgO6pS0Zg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e89c2537debd43e5a96e024166a586eb~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQTDlvq7lo7B6:q75.awebp?rk3s=f64ab15b&x-expires=1742135853&x-signature=xGO6tuuNUfiYURC%2B%2BeIRxG%2Fz9Nc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a264033ffe514f9283ed1bb9fd859172~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQTDlvq7lo7B6:q75.awebp?rk3s=f64ab15b&x-expires=1742135853&x-signature=X8%2BmWHfmUXjiXW2lSxA59GTeVs8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/82070c6522f247c09b079820756a1f5c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQTDlvq7lo7B6:q75.awebp?rk3s=f64ab15b&x-expires=1742135853&x-signature=vejmNC1S3%2F8dKb2Mb7DNzm74nlw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/347cbf35e2304332a9a85e881a15895f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQTDlvq7lo7B6:q75.awebp?rk3s=f64ab15b&x-expires=1742135853&x-signature=vOVTUByaxNg3SBziTh0zrUnncUo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9047aa0f9eac4287b3f4718c5ab74b12~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQTDlvq7lo7B6:q75.awebp?rk3s=f64ab15b&x-expires=1742135853&x-signature=JClKotFt2ZvVMq3JbS3t%2FjXm8Hg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e771e503c7a547188469cebc1e2eee01~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQTDlvq7lo7B6:q75.awebp?rk3s=f64ab15b&x-expires=1742135853&x-signature=uj5TpOUJZE4z9u33qXoIcJZ5iCs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/edf6b6bde8e84d158c4aedff444c4eb9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQTDlvq7lo7B6:q75.awebp?rk3s=f64ab15b&x-expires=1742135853&x-signature=8nTDOJAXWV7iFV4yYMtWC2lD9fo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1ad954ba7e664cb88038f66c127b21c9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQTDlvq7lo7B6:q75.awebp?rk3s=f64ab15b&x-expires=1742135853&x-signature=AbXVtt1xwfJlMn68lo9V3NXj9Xc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/82f9dd922de245848f57b844c41673ea~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQTDlvq7lo7B6:q75.awebp?rk3s=f64ab15b&x-expires=1742135853&x-signature=JJLMRUc3KfeAYSdp0lTsYIKJ5jQ%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter - iOS编译加速","url":"https://juejin.cn/post/7479399201999683584","content":"\\n\\n欢迎关注微信公众号:FSA全栈行动 👋
\\n
在项目完全重构成纯 Flutter
之后 ,iOS
端在 i7 Mac Mini
构建机上的打包时间差不多在 12分钟
左右,而在升级了 Xcode 16
之后,构建机的打包时间有了质的 “提升”
,来到了 25分钟
,换成 M1
来了也压不住,甚至更久~
这种情况在退回 Xcode 15
是可以解决的,但是这并不是长久之计,因为苹果早晚会强制要求升级的,好在申请了台 M4 Mac Mini
来打包,时间来到了 15 分钟
,不过随着业务功能不断迭代,构建时间也慢慢增加,目前来到了 17、18分钟
,但一旦哪天对 M4
构建机进行维护,让 i7
和 M1
顶上时,再加上多个打包任务并行,完成打包的时间动不动就得 半小时
起步,真的很令人绝望~
这里先给出优化后的打包时间
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n构建机 | 优化前(min) | 优化后(min) |
---|---|---|
i7 | 25+ | 14+ |
M4 | 16+ | 8+ |
Profile
+ 源码依赖
原生插件Release
+ 二进制依赖
原生插件Xcode 16
这里我拿了一个业务组件来做测试,分别使用 Xcode 15
和 Xcode 16
对 Profile
和 Release
两种模式来观察编译用时
版本 | Profile (s) | Release (s) |
---|---|---|
Xcode 15 | 389 | 384.6 |
Xcode 16 | 952.3 | 477.4 |
可以看到升级到 Xcode 16
后,两种模式的编译时间都比使用 Xcode 15
的要久,特别是 Profile
模式下的编译时间更离谱,是 Release
的 2倍
多~
而我们的项目为了方便,是以编译模式进行环境区分的。
\\nProfile
: 测试包使用,对应 kProfileMode
Release
: 上架包使用,对应 kReleaseMode
基于现状,只能调整项目中对环境的区分逻辑,改用 Dart Define
将环境参数传入。
这里使用 --dart-define-from-file
传递文件的方式
fvm spawn 3.24.5 build ipa --release --export-options-plist=path/to/ad_hoc.plist --dart-define-from-file=path/to/test.env\\n
\\ntest.env
文件以键值对的方式设置环境变量
APP_ENV=test\\n
\\n取值方式如下,注意,一定要加上 const
!
/// dart define 环境变量\\nString get appEnv => const String.fromEnvironment(\'APP_ENV\');\\n
\\n判断是否为 release
enum AppBuildMode {\\n release,\\n debug,\\n test,\\n}\\n\\nAppBuildMode? fetchAppEnvType() {\\n switch (appEnv.toLowerCase()) {\\n case \\"debug\\":\\n return AppBuildMode.debug;\\n case \\"test\\":\\n return AppBuildMode.test;\\n case \\"release\\":\\n return AppBuildMode.release;\\n default:\\n return null;\\n }\\n}\\n\\nbool isRelease() {\\n final envType = fetchAppEnvType();\\n if (envType == null) {\\n // 没有使用 dart define 设置环境变量\\n return kReleaseMode;\\n } else {\\n return AppBuildMode.release == envType;\\n }\\n}\\n
\\n当然,我们也可以尝试去探索一下,到底是哪里耗时这么久。
\\n通过 Xcode
自身去查看编译耗时会发现最长的是 Run Script
,其主要负责编译 Flutter
侧的代码。
\\n\\n注:这里的时间是 Xcode 16 + Release 下的
\\n
但是展开详细内容会发现一点有用的信息都没有,无法定位到具体问题。
\\n经过对 flutter_tools
的代码进行阅读后发现,可以通过设置环境变量 VERBOSE_SCRIPT_LOGGING
来使其加上 --verbose
参数,进而将打包过程中的一些信息打印出来。
具体操作: Runner
-> Build Phases
-> Run Script
中补充一句 export VERBOSE_SCRIPT_LOGGING=1
# 补充这一句\\nexport VERBOSE_SCRIPT_LOGGING=1\\n\\n/bin/sh \\"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\\" build\\n
\\n再次编译就可以看到详细的 flutter
命令打包信息,可以将其导出后慢慢查看。
下面是摘出的主要耗时记录和文件大小
\\nProfile
\\n# Xcode 15\\n[ +2 ms] executing: xcrun cc -arch arm64 -miphoneos-version-min=12.0 -isysroot /Applications/Xcode-15.4.0.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS17.5.sdk -c /Users/lxf/.../.dart_tool/flutter_build/80c2b96b2938ac2118bcd57be8744d2f/arm64/snapshot_assembly.S -o /Users/lxf/.../.dart_tool/flutter_build/80c2b96b2938ac2118bcd57be8744d2f/arm64/snapshot_assembly.o\\n[+165207 ms] executing: xcrun clang -arch arm64 -miphoneos-version-min=12.0 -isysroot /Applications/Xcode-15.4.0.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS17.5.sdk -dynamiclib -Xlinker -rpath -Xlinker @executable_path/Frameworks -Xlinker -rpath -Xlinker @loader_path/Frameworks -fapplication-extension -install_name @rpath/App.framework/App -o /Users/lxf/.../.dart_tool/flutter_build/80c2b96b2938ac2118bcd57be8744d2f/arm64/App.framework/App /Users/lxf/.../.dart_tool/flutter_build/80c2b96b2938ac2118bcd57be8744d2f/arm64/snapshot_assembly.o\\n[ +289 ms] ...\\n[ +1 ms] executing: /Users/lxf/fvm/versions/3.24.5/bin/cache/artifacts/engine/ios-profile/gen_snapshot_arm64 --deterministic --snapshot_kind=app-aot-assembly --assembly=/Users/lxf/.../.dart_tool/flutter_build/80c2b96b2938ac2118bcd57be8744d2f/arm64/snapshot_assembly.S /Users/lxf/.../.dart_tool/flutter_build/80c2b96b2938ac2118bcd57be8744d2f/app.dill\\n[+96580 ms] ...\\n\\n\\n# Xcode 16\\n[ ] executing: xcrun cc -arch arm64 -miphoneos-version-min=12.0 -isysroot /Applications/Xcode-16.0.0.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk -c /Users/lxf/.../.dart_tool/flutter_build/503cc01726bac6c42836b48ae4a747ed/arm64/snapshot_assembly.S -o /Users/lxf/.../.dart_tool/flutter_build/503cc01726bac6c42836b48ae4a747ed/arm64/snapshot_assembly.o\\n[+596589 ms] executing: xcrun clang -arch arm64 -miphoneos-version-min=12.0 -isysroot /Applications/Xcode-16.0.0.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk -dynamiclib -Xlinker -rpath -Xlinker @executable_path/Frameworks -Xlinker -rpath -Xlinker @loader_path/Frameworks -fapplication-extension -install_name @rpath/App.framework/App -o /Users/lxf/.../.dart_tool/flutter_build/503cc01726bac6c42836b48ae4a747ed/arm64/App.framework/App /Users/lxf/.../.dart_tool/flutter_build/503cc01726bac6c42836b48ae4a747ed/arm64/snapshot_assembly.o\\n[ +290 ms] ...\\n[ +1 ms] executing: /Users/lxf/fvm/versions/3.24.5/bin/cache/artifacts/engine/ios-profile/gen_snapshot_arm64 --deterministic --snapshot_kind=app-aot-assembly --assembly=/Users/lxf/.../.dart_tool/flutter_build/503cc01726bac6c42836b48ae4a747ed/arm64/snapshot_assembly.S /Users/lxf/.../.dart_tool/flutter_build/503cc01726bac6c42836b48ae4a747ed/app.dill\\n[+92259 ms] ...\\n
\\n# Xcode 15、Xcode 16 一样\\n\\nls -lh\\ntotal 853368\\ndrwxr-xr-x@ 3 lxf staff 96B 3 7 15:48 App.framework\\ndrwxr-xr-x@ 3 lxf staff 96B 3 7 15:48 App.framework.dSYM\\n-rw-r--r--@ 1 lxf staff 323M 3 7 15:38 snapshot_assembly.S\\n-rw-r--r--@ 1 lxf staff 93M 3 7 15:48 snapshot_assembly.o\\n
\\nRelease
\\n# Xcode 15\\n[ ] executing: xcrun cc -arch arm64 -miphoneos-version-min=12.0 -isysroot /Applications/Xcode-15.4.0.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS17.5.sdk -c /Users/lxf/.../.dart_tool/flutter_build/a403cb206ef9086380afa3baff59c37e/arm64/snapshot_assembly.S -o /Users/lxf/.../.dart_tool/flutter_build/a403cb206ef9086380afa3baff59c37e/arm64/snapshot_assembly.o\\n[+92077 ms] executing: xcrun clang -arch arm64 -miphoneos-version-min=12.0 -isysroot /Applications/Xcode-15.4.0.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS17.5.sdk -dynamiclib -Xlinker -rpath -Xlinker @executable_path/Frameworks -Xlinker -rpath -Xlinker @loader_path/Frameworks -fapplication-extension -install_name @rpath/App.framework/App -o /Users/lxf/.../.dart_tool/flutter_build/a403cb206ef9086380afa3baff59c37e/arm64/App.framework/App /Users/lxf/.../.dart_tool/flutter_build/a403cb206ef9086380afa3baff59c37e/arm64/snapshot_assembly.o\\n[ +245 ms] ...\\n[ +1 ms] executing: /Users/lxf/fvm/versions/3.24.5/bin/cache/artifacts/engine/ios-release/gen_snapshot_arm64 --deterministic --snapshot_kind=app-aot-assembly --assembly=/Users/lxf/.../.dart_tool/flutter_build/a403cb206ef9086380afa3baff59c37e/arm64/snapshot_assembly.S /Users/lxf/.../.dart_tool/flutter_build/a403cb206ef9086380afa3baff59c37e/app.dill\\n[+88256 ms] ...\\n\\n# ========== 华丽的分割线 ========== #\\n\\n# Xcode 16\\n[ ] executing: xcrun cc -arch arm64 -miphoneos-version-min=12.0 -isysroot /Applications/Xcode-16.0.0.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk -c /Users/lxf/.../.dart_tool/flutter_build/6294397816f76932cad621f56d6b967b/arm64/snapshot_assembly.S -o /Users/lxf/.../.dart_tool/flutter_build/6294397816f76932cad621f56d6b967b/arm64/snapshot_assembly.o\\n[+246277 ms] executing: xcrun clang -arch arm64 -miphoneos-version-min=12.0 -isysroot /Applications/Xcode-16.0.0.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk -dynamiclib -Xlinker -rpath -Xlinker @executable_path/Frameworks -Xlinker -rpath -Xlinker @loader_path/Frameworks -fapplication-extension -install_name @rpath/App.framework/App -o /Users/lxf/.../.dart_tool/flutter_build/6294397816f76932cad621f56d6b967b/arm64/App.framework/App /Users/lxf/.../.dart_tool/flutter_build/6294397816f76932cad621f56d6b967b/arm64/snapshot_assembly.o\\n[ +237 ms] ...\\n[ +1 ms] executing: /Users/lxf/fvm/versions/3.24.5/bin/cache/artifacts/engine/ios-release/gen_snapshot_arm64 --deterministic --snapshot_kind=app-aot-assembly --assembly=/Users/lxf/.../.dart_tool/flutter_build/6294397816f76932cad621f56d6b967b/arm64/snapshot_assembly.S /Users/lxf/.../.dart_tool/flutter_build/6294397816f76932cad621f56d6b967b/app.dill\\n[+88139 ms] ...\\n
\\n# Xcode 15、Xcode 16 一样\\n\\nls -lh\\ntotal 572808\\ndrwxr-xr-x@ 3 lxf staff 96B 3 7 17:08 App.framework\\ndrwxr-xr-x@ 3 lxf staff 96B 3 7 16:02 App.framework.dSYM\\n-rw-r--r--@ 1 lxf staff 213M 3 7 17:04 snapshot_assembly.S\\n-rw-r--r--@ 1 lxf staff 67M 3 7 17:08 snapshot_assembly.o\\n
\\n你可能会觉得最耗时的是 xcrun clang
,但其实每一行前面的中括号内的时间,是上一行的命令的耗时,即 xcrun cc
最耗时,而其它命令的执行时间是差不多的。
xcrun cc
命令是用于将 Flutter
生成的汇编代码(snapshot_assembly.S
)编译为目标文件(snapshot_assembly.o
),不知道苹果使用的 clang
版本是有什么问题,在 Profile
下的编译时长是 Release
下的 2倍
多,它就是造成编译时间变长的主要原因,到这我就没继续往下研究了,有兴趣的小伙伴可以尝试研究看看。
除此之外,汇编文件 snapshot_assembly.S
的大小相差 100M+
,我们可以在日志中找到生成汇编代码的 gen_snapshot_*
命令,如下所示
executing: /Users/lxf/fvm/versions/3.24.5/bin/cache/artifacts/engine/ios-release/gen_snapshot_arm64 --deterministic --snapshot_kind=app-aot-assembly --assembly=/Users/lxf/.../.dart_tool/flutter_build/0385e340094e836ea63c75553c018e82/arm64/snapshot_assembly.S /Users/lxf/.../.dart_tool/flutter_build/0385e340094e836ea63c75553c018e82/app.dill\\n
\\n给 gen_snapshot_*
命令加上 --trace-compiler
标志并重新运行,让其提供每个函数的编译时间,并记录到 result.txt
中,精简命令如下
gen_snapshot_* --trace-compiler ... app.dill > result.txt 2>&1\\n
\\nresult.txt
中的内容长这个样子
Precompiling optimized function: \'dart:core_StateError_StateError.\' @ token 21950, size 52\\n--\x3e \'dart:core_StateError_StateError.\' entry: 0x108d00090 size: 56 time: 935 us\\nPrecompiling optimized function: \'dart:core_RangeError_RangeError.\' @ token 9976, size 94\\n--\x3e \'dart:core_RangeError_RangeError.\' entry: 0x108d000e0 size: 72 time: 133 us\\n...\\n
\\n根据 result.txt
中的耗时(time
)进行从大到小排序,并输出到 sorted_result.txt
中
grep \'^--\x3e\' result.txt | awk \'{for(i=1;i<=NF;i++) if($i==\\"time:\\") print $(i+1), $0}\' | sort -nr | cut -d\' \' -f2- > sorted_result.txt\\n
\\n排序后我们就可以清晰的知道哪些方法是比较耗时的,大家自行判断是否优化即可。
\\n经过对比两个 sorted_result.txt
后发现,一些方法在 Profile
中存在而 Release
中没有,即发生了 Tree Shaking
。
在 Flutter
中,Tree Shaking
是一种优化技术,用于删除未使用的代码,以减小应用的大小并提高性能。对于不同的构建模式,Tree Shaking
的行为有所不同:
模式 | 描述 |
---|---|
Debug | 不会进行 Tree Shaking 。因为 Debug 模式主要用于开发和调试,保留所有代码和调试信息,以便于开发者进行调试。 |
Profile | 会进行部分 Tree Shaking 。主要用于性能分析,尽可能地优化代码,同时保留一些调试信息,以便开发者能分析性能问题。 |
Release | 会进行全面的 Tree Shaking 。会删除未使用的代码,并进行其他优化,以确保应用的体积尽可能小,并且性能最佳。 |
关于构建模式的详细说明,可以看官方文档 docs.flutter.dev/testing/bui…
\\n\\n\\n因此,如果我们希望最大限度地减少应用的体积并提高性能,建议在
\\nRelease
模式下构建Flutter
应用。
将 Profile
模式切到 Release
模式后的打包时间如下
构建机 | Profile (min) | Release (min) |
---|---|---|
i7 | 25+ | 18+ |
M4 | 16+ | 9+ |
可以看到,切换编译模式已经很大程度地优化了编译时长,不过我们还可以再进一步优化。
\\n二进制依赖
是 iOS
端老生常谈的优化点了,通过直接使用编译好的库或模块,从而避免编译的时间和资源消耗。
\\n\\n因此,原生插件越多,编译速度就越慢,二进制依赖的优化效果越好,二进制依赖的优化效果越好,编译速度就越快,所以编译越慢,编译越快 ~
\\n
在这里我使用的是 Rugby
这个工具。
curl -Ls https://swiftyfinch.github.io/rugby/install.sh | bash\\n
\\n安装完成后输出如下内容
\\n🏈 Rugby has been installed ✓\\n\\n/Users/lxf/.rugby/clt is not in your $PATH\\nAdd it manually to your shell profile.\\nFor example, if you use zsh, run this command:\\n$ echo \'\\\\nexport PATH=$PATH:~/.rugby/clt\' >> ~/.zshrc\\nThan open a new window or tab in the terminal for applying changes.\\n
\\n根据提示,将 rugby
添加到环境变量中。
完成后新开个终端,执行如下命令验证 rugby
是否可以被正常使用
rugby --version\\n\\n# 输出\\n2.10.2\\n
\\n在执行完 pod install
后,再执行 rugby cache
即可将原生插件从源码依赖转成二进制依赖了
rugby cache \\\\\\n --arch arm64 \\\\\\n --sdk ios \\\\\\n --except chat_bottom_container realm dart_native \\\\\\n --config Release\\n
\\n这里通过 --except
将一些不做二进制依赖的包过滤掉了。
当这些参数太多之后,命令会变得很长,不好看,可以将这些参数整理到 plans.yml
文件中
profile:\\n- command: cache\\n sdk: ios\\n config: Profile\\n except:\\n - chat_bottom_container\\n - realm\\n - dart_native\\n\\nrelease:\\n- command: cache\\n sdk: ios\\n config: Release\\n except:\\n - chat_bottom_container\\n - realm\\n - dart_native\\n
\\n然后改为 rugby plan
去执行,并且指定使用 plans.yml
中的 release
rugby plan release -p /User/lxf/.../plans.yml\\n
\\n不过需要注意的是,如果你再次执行 pod install
将会还原为源码依赖!rugby
的修改就会失效~
而我们平时执行的 flutter build ipa
命令,其内部是有可能会去执行 pod install
的,那如何避免呢?
经过 flutter_tools
的源码阅读,发现它会做如下判断
pod_inputs.fingerprint
中的各项 MD5
值Podfile.lock
与 Pods/Manifest.lock
内容pod_inputs.fingerprint
位于 build/ios
目录,内容如下
{\\n \\"files\\": {\\n \\"/Users/lxf/.../ios/Runner.xcodeproj/project.pbxproj\\": \\"21b527dc18081de6eabe26c6a4e851b2\\",\\n \\"/Users/lxf/.../ios/Podfile\\": \\"25baa69590b287fd88a578ae5fa2f964\\",\\n \\".../flutter/packages/flutter_tools/bin/podhelper.rb\\": \\"29abcfc3297c225fc1d1ae2380787cd6\\"\\n }\\n}\\n
\\n所以现在很明确,我们需要调整打包步骤
\\nflutter pub get/upgrade
cd ios && pod install
pod_inputs.fingerprint
Podfile.lock
至 Pods/Manifest.lock
flutter build ipa
其中第 3
~ 第 5
步我已经做了封装在我的 github.com/LinXunFeng/… 项目中,使用如下
condor
brew tap LinXunFeng/tap && brew install condor\\n
\\n设置环境变量 CONDOR_BUILD_MODE
,对应 plans.yml
里的 profile
和 release
export CONDOR_BUILD_MODE=release\\n
\\n也可以使用 --mode
参数来指定模式
condor optimize-build --mode release\\n
\\n进入到 Flutter
项目的根目录,执行如下命令
cd path/to/your/flutter_project\\n\\ncondor optimize-build --config path/to/rugby/plans.yml\\n
\\n如果你想指定 fvm
安装的且非全局默认的 flutter
,则可以加上 --flutter
参数
condor optimize-build --config path/to/rugby/plans.yml --flutter \\"fvm spawn 3.24.5\\"\\n
\\n最后执行打包命令即可。
\\n希望苹果下一个版本的 Xcode
可以解决这个问题吧,不然的话,emmm,我也不会升级电脑的~
\\n","description":"欢迎关注微信公众号:FSA全栈行动 👋 一、前言\\n\\n在项目完全重构成纯 Flutter 之后 ,iOS 端在 i7 Mac Mini 构建机上的打包时间差不多在 12分钟 左右,而在升级了 Xcode 16 之后,构建机的打包时间有了质的 “提升”,来到了 25分钟,换成 M1 来了也压不住,甚至更久~\\n\\n这种情况在退回 Xcode 15 是可以解决的,但是这并不是长久之计,因为苹果早晚会强制要求升级的,好在申请了台 M4 Mac Mini 来打包,时间来到了 15 分钟,不过随着业务功能不断迭代,构建时间也慢慢增加,目前来到了 17、18分钟,但一旦…","guid":"https://juejin.cn/post/7479399201999683584","author":"LinXunFeng","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-09T12:37:56.881Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c936376c02544df681a5f09acdba3d78~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTGluWHVuRmVuZw==:q75.awebp?rk3s=f64ab15b&x-expires=1742128675&x-signature=o10br%2FSKwNpLoZd0y0SclgJq%2Bs0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0d2298b054ed4d47a0db5f6c4e8231fa~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgTGluWHVuRmVuZw==:q75.awebp?rk3s=f64ab15b&x-expires=1742128675&x-signature=g%2B%2BVfhPaTAD7beCcgxTCChhw7%2B0%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["iOS","Flutter","Xcode","Apple"],"attachments":null,"extra":null,"language":null},{"title":"系统化掌握Flutter开发之隐式动画(一):筑基之旅","url":"https://juejin.cn/post/7479331614619975691","content":"如果文章对您有所帮助, 请不吝点击关注一下我的微信公众号:FSA全栈行动, 这将是对我最大的激励. 公众号不仅有
\\niOS
技术,还有Android
,Flutter
,Python
等文章, 可能有你想要了解的技能知识点哦~
在移动应用开发中,动画是用户体验的\\"隐形推手\\"
。它不仅是界面元素的简单位移
,更是用户心智模型的引导工具
—— 通过缓动曲线暗示操作反馈,利用共享元素传递层级关系,借助物理动效强化真实感。
Flutter
的动画体系以Widget
为核心,将数学
、物理
、美术
三大学科融于代码,实现了跨平台一致的高性能表现。但许多初学者陷入\\"调参数改数值\\"
的碎片化误区,忽略了动画作为系统级解决方案的本质。
本文将从认知维度重构学习路径,通过分层递进的案例,揭示如何用系统思维将冰冷数值转化为有温度的用户体验。当你能用动画讲好产品故事时,技术就完成了向艺术的蜕变。
\\n操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\n隐式动画
?定义:
\\n作为Flutter
声明式动画体系的核心方案,隐式动画通过继承ImplicitlyAnimatedWidget
的组件群实现动画自动化,隶属于官方标准动画库。
核心特征:
\\nbegin
)与终止值(end
)。动画引擎自动计算中间帧
,默认采用300ms
线性插值。通过 curve
参数调整动画缓动曲线(如 Curves.easeInOut
)。AnimationController
,消除手动维护状态机的复杂度。与显式动画对比优势:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n维度 | 隐式动画 | 显式动画 |
---|---|---|
开发效率 | ⭐⭐⭐⭐ | ⭐⭐ |
控制粒度 | 自动触发 | 手动控制(controller ) |
代码复杂度 | 简单(声明式 ),代码减少60%+ | 复杂(需完整动画生命周期管理 ) |
适用场景 | 简单属性过渡 | 复杂交互动画/组合动画 |
分类 | 组件名称 | 设计目的 | 注意事项 |
---|---|---|---|
布局属性动画 | AnimatedContainer | 处理宽高、边距、装饰等复合属性变化的过渡动画 | 同时动画属性不宜超过4 个,避免频繁重建装饰对象 |
AnimatedPositioned | 在Stack 中实现绝对定位的平滑过渡 | 父容器必须是Stack ,需明确父容器尺寸 | |
AnimatedPadding | 动态调整内边距时的过渡效果 | 四边同时变化时性能敏感,优先使用Transform 替代 | |
AnimatedAlign | 实现元素在容器内对齐方式变化的动画 | 父容器需明确尺寸,对齐值超出1.0 会溢出 | |
AnimatedSize | 动态调整组件尺寸(宽/高),适应内容变化 | 子组件尺寸变化需稳定 | |
视觉属性动画 | AnimatedOpacity | 实现透明度渐变效果 | 优先使用Opacity 组件替代以提升性能 |
AnimatedTheme | 主题属性(颜色/文本样式)变化的过渡动画 | 需配合InheritedWidget 使用,避免深层嵌套 | |
AnimatedPhysicalModel | 物理效果(阴影/高程)变化的拟真动画 | 消耗较高GPU 资源,移动端慎用 | |
AnimatedRotation | 实现组件旋转动画(角度变化) | 使用弧度单位(2π 为一圈),优先配合Transform.rotate 使用 | |
AnimatedScale | 实现组件缩放动画 | 避免缩放比例过大导致溢出 | |
AnimatedSlide | 实现组件偏移滑动动画 | 偏移量需基于父容器尺寸计算 | |
组件切换动画 | AnimatedSwitcher | 子组件切换时的复合过渡效果 | 子组件需不同Key,避免使用复杂transitionBuilder |
AnimatedCrossFade | 两个子组件交叉淡入淡出的过渡效果 | 需保持两个子组件树稳定,避免频繁重建 | |
AnimatedList | 列表项增删时的布局过渡动画 | 需配合GlobalKey 使用,及时清理不可见元素 |
设计目标:处理Widget
在布局系统中的位置
、尺寸
变化。
\\n实现原理:通过监听布局属性变化自动生成补间动画。
组件名称 | 动画属性 | 实现原理 | 关键参数 | 使用场景 | 注意事项 |
---|---|---|---|---|---|
AnimatedContainer | 宽高、边距、颜色、装饰等 | 比较新旧属性差异,自动生成补间动画 | duration 、curve 、alignment 、decoration | 1. 可展开卡片 2. 主题切换布局调整 | 1. 同时变化的属性不宜超过4个 2. 预定义装饰对象避免重建 |
AnimatedPositioned | 绝对定位(left/top 等) | 基于父Stack 坐标系计算位置插值 | left 、top 、right 、bottom 、duration | 1. 侧边栏滑入滑出 2. 拖拽元素归位 | 1. 父容器需明确尺寸 2. 避免同时设置对立属性(left/right ) |
AnimatedPadding | 内边距(padding ) | 动态插值计算各方向边距 | padding 、duration 、curve | 1. 输入框聚焦扩展间距 2. 菜单展开动画 | 1. 四边同时变化时性能敏感 2. 优先用Transform 替代 |
AnimatedAlign | 对齐坐标(alignment ) | 根据父容器尺寸计算对齐点插值 | alignment 、duration 、curve | 1. 工具栏对齐切换 2. 动态内容居中 | 1. 父容器需确定尺寸 2. 对齐值超出1.0 会导致溢出 |
AnimatedSize | 宽高尺寸(size ) | 监听子组件尺寸变化,自动生成补间动画 | duration 、alignment | 1. 文本展开/折叠 2. 图片加载占位动画 | 1. 子组件尺寸变化需稳定 2. 避免在滚动视图中使用 |
import \'package:flutter/material.dart\';\\n\\nclass AnimationDemo extends StatefulWidget {\\n @override\\n _AnimationDemoState createState() => _AnimationDemoState();\\n}\\n\\nclass _AnimationDemoState extends State<AnimationDemo> {\\n bool _expanded = false;\\n bool _moveRight = false;\\n bool _addPadding = false;\\n int _currentIndex = 0;\\n Alignment _alignment = Alignment.topLeft;\\n final _alignments = [ Alignment.topLeft, Alignment.topRight, Alignment.bottomLeft, Alignment.bottomRight, ];\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: Text(\\"Animation Demo\\"),\\n backgroundColor: Theme.of(context).colorScheme.inversePrimary,\\n ),\\n body: Center(\\n child: Column(\\n children: [\\n buildAnimatedContainer(),\\n buildAnimatedPositioned(context),\\n buildAnimatedPadding(),\\n buildAnimatedAlign(),\\n buildAnimatedSize(),\\n ],\\n ),\\n ),\\n );\\n }\\n\\n GestureDetector buildAnimatedSize() {\\n return GestureDetector(\\n onTap: () => setState(() => _expanded = !_expanded),\\n child: AnimatedSize(\\n duration: const Duration(seconds: 1),\\n curve: Curves.easeInOutBack,\\n child: Container(\\n width: _expanded ? 200 : 100,\\n height: _expanded ? 200 : 100,\\n color: Colors.purple,\\n child: Center(\\n child: Icon(\\n _expanded ? Icons.expand_less : Icons.expand_more,\\n color: Colors.white,\\n size: 40,\\n ),\\n ),\\n ),\\n ),\\n );\\n }\\n\\n GestureDetector buildAnimatedAlign() {\\n return GestureDetector(\\n onTap: () {\\n setState(() {\\n _alignment = _alignments[(++_currentIndex) % 4];\\n });\\n },\\n child: Container(\\n color: Colors.grey[200],\\n child: AnimatedAlign(\\n duration: const Duration(seconds: 1),\\n curve: Curves.elasticOut,\\n alignment: _alignment,\\n child: Container(\\n width: 100,\\n height: 100,\\n color: Colors.green,\\n child: const Icon(Icons.location_on, color: Colors.white),\\n ),\\n ),\\n ),\\n );\\n }\\n\\n GestureDetector buildAnimatedPadding() {\\n return GestureDetector(\\n onTap: () => setState(() => _addPadding = !_addPadding),\\n child: AnimatedPadding(\\n duration: const Duration(seconds: 1),\\n padding: _addPadding ? const EdgeInsets.all(40) : EdgeInsets.zero,\\n curve: Curves.easeInOutQuint,\\n child: Container(\\n width: 100,\\n height: 100,\\n color: Colors.orange,\\n child: const Center(\\n child: Text(\'改变内边距\', style: TextStyle(color: Colors.white)),\\n ),\\n ),\\n ),\\n );\\n }\\n\\n SizedBox buildAnimatedPositioned(BuildContext context) {\\n return SizedBox(\\n width: double.infinity,\\n height: 200,\\n child: Stack(\\n children: [\\n AnimatedPositioned(\\n duration: const Duration(seconds: 1),\\n curve: Curves.easeInOutCirc,\\n left: _moveRight ? MediaQuery.of(context).size.width - 120 : 20,\\n top: 50,\\n child: GestureDetector(\\n onTap: () => setState(() => _moveRight = !_moveRight),\\n child: Container(\\n width: 100,\\n height: 100,\\n color: Colors.blue,\\n child: const Icon(Icons.arrow_forward, color: Colors.white),\\n ),\\n ),\\n ),\\n ],\\n ),\\n );\\n }\\n\\n Widget buildAnimatedContainer() {\\n return GestureDetector(\\n onTap: () => setState(() => _expanded = !_expanded),\\n child: AnimatedContainer(\\n duration: const Duration(seconds: 1),\\n curve: Curves.fastOutSlowIn,\\n width: _expanded ? 200 : 100,\\n height: _expanded ? 200 : 100,\\n decoration: BoxDecoration(\\n color: _expanded ? Colors.blue : Colors.red,\\n borderRadius: BorderRadius.circular(_expanded ? 20 : 8),\\n ),\\n child: Icon(\\n Icons.star,\\n color: Colors.white,\\n size: _expanded ? 48 : 32,\\n ),\\n ),\\n );\\n }\\n}\\n
\\n设计目标:处理Widget
的视觉表现变化。
\\n数学基础:颜色空间转换
、透明度插值计算
。
组件名称 | 动画属性 | 实现原理 | 关键参数 | 使用场景 | 注意事项 |
---|---|---|---|---|---|
AnimatedOpacity | 透明度(opacity ) | 通过RenderObject 实现透明度插值 | opacity (0.0~1.0)、duration 、curve | 1. 弹窗遮罩淡入淡出 2. 内容渐显效果 | 1. 优先用Visibility 控制显示逻辑 2. 低端设备时长≤500ms |
AnimatedTheme | 主题属性(颜色/文本样式) | 通过InheritedWidget 传递主题数据,比较新旧差异 | data (ThemeData )、duration | 1. 日间/夜间模式切换 2. 局部主题高亮 | 1. 使用copyWith 保持稳定 2. 避免深层嵌套 |
AnimatedPhysicalModel | 物理属性(阴影/高程) | 基于RenderPhysicalModel 更新材质效果 | elevation 、shadowColor 、shape 、duration | 1. 按钮点击反馈 2. 卡片浮动效果 | 1. 移动端elevation ≤8.0 2. 禁用复杂多色阴影 |
AnimatedRotation | 旋转角度(turns/angle ) | 通过变换矩阵实现旋转变换 | turns (圈数)、angle (弧度)、duration | 1. 加载指示器旋转 2. 菜单图标展开 | 1. 使用alignment 控制旋转中心 2. 循环动画需手动repeat |
AnimatedScale | 缩放比例(scale ) | 基于变换矩阵实现视觉缩放 | scale 、alignment 、duration | 1. 按钮点击弹性效果 2. 元素聚焦放大 | 1. 缩放值≤1.5防模糊 2. 避免与布局尺寸动画叠加 |
AnimatedSlide | 相对位移(offset ) | 根据父容器尺寸计算偏移量 | offset (如Offset(0.5,0) )、duration | 1. 侧边栏滑入动画 2. 拖拽元素跟随效果 | 1. 偏移量超出1.0会溢出 2. 优先用绝对定位组件(如AnimatedPositioned ) |
bool _visible = true;\\nbool _darkMode = false;\\nbool _pressed = false;\\n\\ndouble _turns = 0.0;\\ndouble _scale = 1.0;\\nbool _showPanel = false;\\n\\nvoid _rotate() {\\n setState(() => _turns += 1.0); // 每点击旋转一圈(360度)\\n}\\n\\nStack buildAnimatedSlide() {\\n return Stack(\\n children: [\\n Positioned.fill(\\n child: Center(\\n child: ElevatedButton(\\n child: Text(_showPanel ? \'隐藏面板\' : \'显示面板\'),\\n onPressed: () => setState(() => _showPanel = !_showPanel),\\n ),\\n ),\\n ),\\n AnimatedSlide(\\n offset: _showPanel ? Offset.zero : const Offset(0, 1.5),\\n duration: const Duration(seconds: 1),\\n curve: Curves.fastOutSlowIn,\\n child: Container(\\n height: 200,\\n decoration: BoxDecoration(\\n color: Colors.green,\\n borderRadius:\\n const BorderRadius.vertical(top: Radius.circular(20)),\\n boxShadow: [\\n BoxShadow(\\n color: Colors.black.withOpacity(0.3),\\n blurRadius: 10,\\n spreadRadius: 2,\\n )\\n ],\\n ),\\n child: Column(\\n children: [\\n const Padding(\\n padding: EdgeInsets.all(16.0),\\n child: Text(\'滑动面板\',\\n style: TextStyle(color: Colors.white, fontSize: 24)),\\n ),\\n Expanded(\\n child: ListView.builder(\\n itemCount: 5,\\n itemBuilder: (context, index) => ListTile(\\n title: Text(\'项目 ${index + 1}\',\\n style: const TextStyle(color: Colors.white)),\\n ),\\n ),\\n ),\\n ],\\n ),\\n ),\\n ),\\n ],\\n );\\n}\\n\\nWidget buildAnimatedScale() {\\n return Column(\\n children: [\\n GestureDetector(\\n onTap: () => setState(() => _scale = _scale == 1.0 ? 1.5 : 1.0),\\n child: AnimatedScale(\\n scale: _scale,\\n duration: const Duration(milliseconds: 300),\\n curve: Curves.easeInOutBack,\\n child: Container(\\n width: 150,\\n height: 150,\\n decoration: BoxDecoration(\\n color: Colors.orange,\\n borderRadius: BorderRadius.circular(24),\\n ),\\n child: const Icon(Icons.star, color: Colors.white, size: 50),\\n ),\\n ),\\n ),\\n const SizedBox(height: 20),\\n Text(\\n _scale > 1.0 ? \'放大状态\' : \'正常状态\',\\n style: const TextStyle(fontSize: 20),\\n ),\\n ],\\n );\\n}\\n\\nWidget buildAnimatedRotation() {\\n return Column(\\n children: [\\n AnimatedRotation(\\n turns: _turns,\\n duration: const Duration(seconds: 1),\\n curve: Curves.elasticOut,\\n child: Container(\\n width: 100,\\n height: 100,\\n decoration: BoxDecoration(\\n color: Colors.blue,\\n borderRadius: BorderRadius.circular(16),\\n ),\\n child: const Icon(Icons.refresh, color: Colors.white, size: 40),\\n ),\\n ),\\n const SizedBox(height: 20),\\n ElevatedButton(\\n onPressed: _rotate,\\n child: const Text(\'旋转\'),\\n ),\\n ],\\n );\\n}\\n\\nGestureDetector buildAnimatedPhysicalModel() {\\n return GestureDetector(\\n onTapDown: (_) => setState(() => _pressed = true),\\n onTapUp: (_) => setState(() => _pressed = false),\\n onTapCancel: () => setState(() => _pressed = false),\\n child: AnimatedPhysicalModel(\\n shape: BoxShape.rectangle,\\n elevation: _pressed ? 12.0 : 4.0,\\n color: Colors.blue,\\n shadowColor: Colors.black,\\n duration: const Duration(milliseconds: 200),\\n child: const SizedBox(\\n width: 150,\\n height: 60,\\n child: Center(\\n child: Text(\'点击我\',\\n style: TextStyle(color: Colors.white, fontSize: 18)),\\n ),\\n ),\\n ),\\n );\\n}\\n\\nAnimatedTheme buildAnimatedTheme() {\\n return AnimatedTheme(\\n data: _darkMode ? ThemeData.dark() : ThemeData.light(),\\n duration: const Duration(seconds: 1),\\n child: Container(\\n padding: const EdgeInsets.all(20),\\n child: Column(\\n children: [\\n SwitchListTile(\\n title: const Text(\'夜间模式\'),\\n value: _darkMode,\\n onChanged: (v) => setState(() => _darkMode = v),\\n ),\\n const SizedBox(height: 20),\\n Card(\\n child: Padding(\\n padding: const EdgeInsets.all(16.0),\\n child: Column(\\n children: [\\n const Text(\'主题示例文字\'),\\n const SizedBox(height: 10),\\n ElevatedButton(\\n child: const Text(\'示例按钮\'),\\n onPressed: () {},\\n ),\\n ],\\n ),\\n ),\\n ),\\n ],\\n ),\\n ),\\n );\\n}\\n\\nWidget buildAnimatedOpacity() {\\n return Column(\\n children: [\\n AnimatedOpacity(\\n opacity: _visible ? 1.0 : 0.0,\\n duration: const Duration(seconds: 1),\\n curve: Curves.easeInOut,\\n child: Container(\\n width: 100,\\n height: 100,\\n color: Colors.blue,\\n child: const Icon(Icons.visibility, color: Colors.white, size: 50),\\n ),\\n ),\\n const SizedBox(height: 20),\\n ElevatedButton(\\n child: Text(_visible ? \\"隐藏\\" : \\"显示\\"),\\n onPressed: () => setState(() => _visible = !_visible),\\n ),\\n ],\\n );\\n}\\n
\\n组件名称 | 动画属性 | 实现原理 | 关键参数 | 使用场景 | 注意事项 |
---|---|---|---|---|---|
AnimatedSwitcher | 子组件切换 | 1. 通过Key 识别新旧组件差异 2. 并行执行退场和入场动画 | transitionBuilder (自定义动画) duration (动画时长) switchInCurve (入场曲线) | 1. 页面切换动画 2. 加载状态变化 | 1. 必须为子组件设置不同Key 2. 避免嵌套复杂组件树 |
AnimatedCrossFade | 双组件交叉淡入淡出 | 1. 同时维护两棵组件树 2. 根据crossFadeState 控制主次组件透明度 | crossFadeState (状态标记) duration firstChild /secondChild | 1. 选项卡切换 2. 登录/注册表单切换 | 1. 保持两组件结构相似 2. 避免频繁切换状态(间隔≥200ms) |
AnimatedList | 列表项增删 | 1. 基于GlobalKey 跟踪列表状态 2. 通过SliverAnimatedList 实现局部刷新 | itemBuilder (项构建器) initialItemCount (初始数量) key (全局Key) | 1. 动态添加/删除列表项 2. 聊天消息流 | 1. 必须配合GlobalKey 使用 2. 及时清理不可见元素 |
bool _toggle = true;\\nbool _first = true;\\n\\nfinal GlobalKey<AnimatedListState> _listKey = GlobalKey();\\nfinal List<String> _items = [];\\n\\nvoid _addItem() {\\n _items.insert(0, \'项目 ${_items.length + 1}\');\\n _listKey.currentState!.insertItem(0);\\n}\\n\\nWidget buildAnimatedList() {\\n return Column(\\n children: [\\n Expanded(\\n child: AnimatedList(\\n key: _listKey,\\n initialItemCount: _items.length,\\n itemBuilder: (context, index, animation) {\\n return FadeTransition(\\n opacity: animation,\\n child: ListTile(title: Text(_items[index])),\\n );\\n },\\n ),\\n ),\\n ElevatedButton(\\n onPressed: _addItem,\\n child: const Text(\'添加项目\'),\\n ),\\n ],\\n );\\n}\\n\\nGestureDetector buildAnimatedCrossFade() {\\n return GestureDetector(\\n onTap: () {\\n setState(() => _first = !_first);\\n },\\n child: AnimatedCrossFade(\\n duration: const Duration(seconds: 1),\\n firstChild: Container(\\n width: 150,\\n height: 100,\\n color: Colors.blueAccent,\\n child: Text(\'第一个组件\', style: TextStyle(fontSize: 24)),\\n ),\\n secondChild: Container(\\n width: 150,\\n height: 100,\\n color: Colors.redAccent,\\n child: Text(\'第二个组件\', style: TextStyle(fontSize: 24)),\\n ),\\n crossFadeState:\\n _first ? CrossFadeState.showFirst : CrossFadeState.showSecond,\\n ),\\n );\\n}\\n\\nGestureDetector buildAnimatedSwitcher() {\\n return GestureDetector(\\n onTap: () {\\n setState(() => _toggle = !_toggle);\\n },\\n child: AnimatedSwitcher(\\n duration: const Duration(seconds: 1),\\n child: _toggle\\n ? Container(\\n key: UniqueKey(), width: 100, height: 100, color: Colors.blue)\\n : Container(\\n key: UniqueKey(), width: 100, height: 100, color: Colors.red),\\n ),\\n );\\n}\\n
\\n动画设计的本质是建立用户心智模型与物理世界的映射关系
。优秀的动画应遵循\\"三阶法则\\"
:
精准的数值计算
)。符合运动规律
)。传递产品性格
)。开发者需要建立\\"参数即意图\\"
的思维 —— 每个curve
的选择都是对用户情绪的引导,每个duration
的设定都是对操作优先级的排序。
动画不是炫技工具,而是用户旅程的无声向导。当你能用Curves.easeInOut
解释产品理念,用Hero
动画传递信息层级时,就真正掌握了系统化设计的精髓。
\\n","description":"前言 在移动应用开发中,动画是用户体验的\\"隐形推手\\"。它不仅是界面元素的简单位移,更是用户心智模型的引导工具 —— 通过缓动曲线暗示操作反馈,利用共享元素传递层级关系,借助物理动效强化真实感。\\n\\nFlutter的动画体系以Widget为核心,将数学、物理、美术三大学科融于代码,实现了跨平台一致的高性能表现。但许多初学者陷入\\"调参数改数值\\"的碎片化误区,忽略了动画作为系统级解决方案的本质。\\n\\n本文将从认知维度重构学习路径,通过分层递进的案例,揭示如何用系统思维将冰冷数值转化为有温度的用户体验。当你能用动画讲好产品故事时,技术就完成了向艺术的蜕变。\\n\\n操千曲而后晓…","guid":"https://juejin.cn/post/7479331614619975691","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-09T10:07:00.903Z","media":null,"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"烽火连营——爆杀 Jank 闪烁卡顿","url":"https://juejin.cn/post/7478949354113253386","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
graph LR\\n classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px\\n classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px\\n classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px\\n \\n A([开始]):::startend --\x3e B(加载预览图列表):::process\\n B --\x3e C{选择预览图}:::decision\\n C --\x3e B\\n C --\x3e D(加载高清图):::process\\n D --\x3e H([结束]):::startend\\n \\n
\\n业务功能要求,需要进行预览图列表及高清图渲染,期间点击切换预览高清图时切换过程有明显闪烁;其中渲染核心逻辑如下
\\n Image(\\n image: AssetImage(\'images/base_widgets/star_black.png\'),\\n fit: BoxFit.fill,\\n )\\n
\\nAssetImage 渲染过程:
\\n造成明显白屏闪烁的原因基本都在进行 GPU 计算,着色阶段,在处理较大资源时涉及到的计算与资源拷贝将占用大量系统资源,且对于资源的创建与销毁同样会占用 GC 线程从而影响整体效率,最终造成卡顿、闪屏发生
\\ngraph TD\\n A[VSync Signal Trigger] --\x3e B[UI Thread Processing]\\n B --\x3e C[Animation Processing]\\n C --\x3e D[Build Three Trees]\\n D --\x3e E[Widget Tree]\\n D --\x3e F[Element Tree]\\n D --\x3e G[RenderObject Tree]\\n G --\x3e H[Layout]\\n H --\x3e I[Calculate Size/Position]\\n G --\x3e J[Paint]\\n J --\x3e K[Generate Layer Tree]\\n K --\x3e L[Submit to GPU Thread]\\n L --\x3e M[Rasterize]\\n M --\x3e N[Compositing]\\n N --\x3e O[Display Refresh]\\n\\n subgraph \\"UI Thread\\"\\n B\\n C\\n D\\n E\\n F\\n G\\n H\\n I\\n J\\n K\\n end\\n\\n subgraph \\"GPU Thread\\"\\n L\\n M\\n N\\n end\\n
\\n借助 Flutter Devtool 查看切换过程的火焰图如下,可明显看出 Jank 在切换图片的渲染周期内频繁出现
\\n对照 GC 情况检测如下
\\n红色三角表示一次 垃圾回收(Garbage Collection, GC) 事件;
\\n蓝色点状标记通常反映内存分配的 波动情况,表示在应用运行过程中,内存分配(或内存使用快照)的瞬间数据点;
\\n从火焰图发现周期渲染内存在多次 Jank,即渲染周期内多次帧率小于 60FPS,这很容易造成视觉上的卡顿,对应 GC 分析器中红色三角形表明在单次渲染周期内有较频繁的内存回收行为,而每次 GC 都会暂停主线程,这可能是导致 Jank 的主要原因之一;
\\n通过检索发现,一般渲染卡顿通常由以下几点造成:
\\n频繁内存分配:如应用中存在大量临时对象或重复创建对象(如图片、列表、Widget 等),导致内存分配频繁,从而触发 GC 的频率随之增加;
\\n不必要的重绘和布局重建:如果 UI 组件频繁重建(例如频繁调用 setState 或组件未正确利用缓存机制),可能会导致不必要的内存分配和 GC 触发;
\\n资源未能有效复用:使用了没有缓存机制的组件(如 Image.memory 直接加载图片而非通过 ImageCache 复用),使得每次加载时都需要重新分配内存和解码,从而增加了 GC 的负担;
\\n垃圾回收策略:如果应用中大量使用短生命周期的对象,垃圾回收器需要频繁启动,导致主线程短暂停顿(STW),从而引起 Jank;
\\n结合当前使用场景,除了图片资源过大造成的内存分配、资源解码问题,还有重复建立临时对象引发频繁的 GC 事件导致;
\\n检索 Jank 的引发因素如下
\\n60FPS:意味着画面每秒更新 60 次,用户感知为“无缝”交互。低于此阈值时,人眼会察觉到帧间间隔增大,表现为卡顿(Jank);UI 线程(Dart)或 GPU 线程(Skia渲染)任一环节超时将导致帧丢失,破坏 60FPS 连续性
\\nJank:lutter 团队及社区通常将 16ms(或16.67ms) 作为性能的一个关键指标。如果某一帧的渲染时间超过这个时间,就可能导致掉帧(Jank),进而影响整体体验
\\n考虑业务对原图有一定格式要求,对于图片资源首先不做更改,可尝试通过缓存机制减少 GC 次数,从而降低 CPU 负载,并,看是否能满足交互要求;
\\nFadeInImage
的工作方式FadeInImage
结合了 占位图 + 目标图淡入动画,核心流程如下:
graph LR\\n classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px\\n classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px\\n classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px\\n \\n A([开始]):::startend --\x3e B(显示占位图):::process\\n B --\x3e C(后台异步解码目标图):::process\\n C --\x3e D{目标图解码完成?}:::decision\\n D --\x3e|否| C\\n D --\x3e|是| E(渐变显示目标图):::process\\n E --\x3e F([结束]):::startend\\n
\\n先显示占位图(如 AssetImage
)。
后台异步解码目标图,避免 UI 主线程阻塞:
\\nSchedulerBinding.instance!.scheduleFrameCallback((_) {\\n _resolveImage(); // 后台线程执行解码\\n});\\n
\\n加载完成后渐变显示目标图(避免视觉跳变):
\\nAnimatedOpacity(\\n opacity: _animation.value, // 过渡透明度\\n child: RawImage(image: _image, fit: BoxFit.cover),\\n)\\n
\\nImage.memory 问题
\\nImage.memory
直接调用 instantiateImageCodec
在主线程上同步解码图片。当图片较大时,这个过程会占用较长时间,直接导致 UI 卡顿,因为主线程在等待图片解码完成之前不能继续其他任务。Uint8List
和 Codec
对象,造成大量临时对象,这会引发频繁的垃圾回收(GC),进一步增加 CPU 负载。FadeInImage 优化
\\n后台解码:
\\nFadeInImage 内部通过将图片解码任务交由后台线程执行(例如利用 SchedulerBinding.instance.scheduleFrameCallback
),使得主线程不必等待耗时的解码过程,从而大幅降低主线程的工作量。
// 调度后台解码任务\\nSchedulerBinding.instance!.scheduleFrameCallback((_) {\\n _resolveImage(); // 后台执行图片解码\\n});\\n
\\n异步解码机制确保了主线程只负责轻量级任务(如动画插值和 UI 更新),从而减少因解码延迟引起的掉帧(Jank)。
\\nImage.memory 问题
\\nFadeInImage 优化
\\nImageCache 复用:
\\nFadeInImage 使用的是 ImageProvider(如 MemoryImage),这个 ImageProvider 通过内部的 ImageCache 机制缓存了解码后的图片对象。后续相同图片的加载可以直接从缓存中获取,避免了重复解码的开销;
//通过 ImageProvider.resolve() 获取 ImageStream,\\n// ImageCache 会尝试找到已缓存的解码图片\\nfinal ImageProvider imageProvider = MemoryImage(imageBytes);\\nfinal ImageStream stream = imageProvider.resolve(ImageConfiguration());\\n
\\n通过缓存复用,FadeInImage 降低了频繁的内存分配和解码操作,从而减少了垃圾回收的触发频率,也降低了 CPU 的整体负载。
\\ngraph LR\\n classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px\\n classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px\\n classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px\\n\\n A([开始]):::startend --\x3e B{组件类型}:::decision\\n B --\x3e|FadeInImage| C(显示占位图):::process\\n B --\x3e|Image.memory| D(直接解码显示):::process\\n C --\x3e E(异步解码目标图):::process\\n E --\x3e F(显示目标图):::process\\n D --\x3e G(显示图片):::process\\n F --\x3e H([结束]):::startend\\n G --\x3e H\\n
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n特性 | Image.memory | FadeInImage |
---|---|---|
解码方式 | 主线程同步解码 | 后台异步解码 |
缓存机制 | 每次加载都重新解码,不复用缓存 | 利用 ImageCache 复用解码后图片 |
动画过渡 | 直接替换,无过渡效果 | 淡入动画过渡,平滑显示 |
CPU 负载 | 高,频繁触发 GC | 低,主线程阻塞减少 |
Jank 频次 | 较高(每帧耗时多,卡顿明显) | 显著下降(Jank 降低 80% 以上) |
红色三角下降 30% :这通常表示垃圾回收(GC)事件的减少。也就是说,经过优化后,由于缓存机制和异步解码,系统触发 GC 的频率有所降低,从而减少了主线程因 GC 暂停而造成的中断。
\\n蓝色点状变化不明显:蓝色点状标记通常反映内存分配事件。它们没有明显下降说明内存分配次数或总量基本保持不变,但这并不意味着内存使用效率低,因为关键在于这些分配对象是否被重复利用,而不是每次都创建新的对象。
\\nJank 频次下降超过 80% :这说明整体的 UI 渲染和动画执行大为改善。尽管内存分配频率没显著变化,但由于 FadeInImage 利用了缓存复用、异步解码和渐入动画等机制,主线程负载显著降低,渲染过程更平滑,从而极大减少了掉帧现象和视觉卡顿。
\\n组件 | 主线程耗时(ms/帧) | GC事件(次/秒) |
---|---|---|
Image.memory | 18.2(Jank率30%) | 45 |
FadeInImage | 12.1(Jank率8%) | 12 |
结果: 对比优化前后,频繁进行预览图切换没有明显卡顿现象输出,并且可以考虑增加渐入切换动画丰富交互。
\\ngraph LR\\n classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px\\n classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px\\n classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px\\n classDef result fill:#FFEBEB,stroke:#E68994,stroke-width:2px\\n\\n A([开始]):::startend --\x3e B(图片加载):::process\\n B --\x3e C{优化机制}:::decision\\n C --\x3e|异步解码| D(将大图解码移至后台线程):::process\\n C --\x3e|缓存复用| E(使用 ImageCache 复用已解码图片):::process\\n D --\x3e F(避免 UI 阻塞):::result\\n D --\x3e G(降低 CPU 负载):::result\\n E --\x3e H(减少重复内存分配):::result\\n E --\x3e I(降低 GC 频率):::result\\n F --\x3e J(提升 UI 流畅度):::result\\n G --\x3e J\\n H --\x3e J\\n I --\x3e J\\n J --\x3e K([结束]):::startend\\n
\\n如果放开对图片资源限制,可以考虑对资源进行优化
\\n图片压缩:将 PNG 转换为 WebP 格式,减少文件大小和解码时间(网页9建议的资源压缩)
\\n按需加载:使用 ResizeImage
指定解码尺寸(如 cacheWidth: 200
),避免加载全尺寸图(网页6的降级策略)
Image(\\n image: ResizeImage(\\n AssetImage(\'images/base_widgets/star_black.webp\'),\\n width: 200,\\n height: 200,\\n ),\\n fit: BoxFit.fill,\\n)\\n
","description":"场景问题 graph LR\\n classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px\\n classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px\\n classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px\\n \\n A([开始]):::startend --\x3e B(加载预览图列表):::process\\n B --\x3e C…","guid":"https://juejin.cn/post/7478949354113253386","author":"人形打码机","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-08T10:11:14.656Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b27cae65574047e3844eb0bb797c6655~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Lq65b2i5omT56CB5py6:q75.awebp?rk3s=f64ab15b&x-expires=1742033541&x-signature=yzwOEw%2FLPGCgtyT4xRR58NS3TpU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d1c131b0b2c34d6e81e609e3a4a096d5~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Lq65b2i5omT56CB5py6:q75.awebp?rk3s=f64ab15b&x-expires=1742033541&x-signature=ZAAcVRd7r9Mh8OFWog5v4BV%2Fkr0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e088521c04fe4a87853f5dbd7ae6a692~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Lq65b2i5omT56CB5py6:q75.awebp?rk3s=f64ab15b&x-expires=1742033541&x-signature=JjCUa9LO9ARuqBAi71bhwVUpJJ8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/30574e00c76741bf8d0add456fba44db~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Lq65b2i5omT56CB5py6:q75.awebp?rk3s=f64ab15b&x-expires=1742033541&x-signature=mmYKu6qjubNLc1SvB02qxOGbpn0%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","性能优化","面试"],"attachments":null,"extra":null,"language":null},{"title":"flutter webview crash 问题","url":"https://juejin.cn/post/7478938109020799011","content":"在 Flutter界面 和 H5界面 之间来回切换(切换10多次),导致 iOS App Crash, flutter 版本 3.22.0;
\\nWebViewPageState 一直没有释放, 通过 DevTool 的 Memory 工具发现, webviewController 一直在持有 WebViewPage, github 上找到相应的 issue 连接 webviewController 不能主动释放
\\n最后的代码链接,里面附上了释放的过程
\\n\\n
功能描述
\\n没有登录/注册功能,只能简单的做一个本地存储,功能和其它壁纸软件也大差不差。不过我最喜欢的还是可以设置本地壁纸和本地视频壁纸的功能。
\\n基础用法都不做介绍了,主要介绍一些可能遇到的问题:
\\nvar file = await DefaultCacheManager().getSingleFile(url);
我用这种方法获取图片缓存,在图片加载成功后,第一次设置壁纸并没有从缓存中直接拿到图片数据,间接导致我使用了两个壁纸设置插件。var cacheData = await getNetworkImageData(imagePath, useCache: true);
这个方法获取图片缓存,图片加载成功后,第一次设置壁纸就能有效的从缓存中拿到图片数据,在把图片存到临时文件夹中,返回自定义的路径即可,但是这种方法使用 async_wallpaper
设置壁纸时在模拟器上没问题,真机上面就失败了,暂时不知道原因,因此用了两个壁纸设置插件。/lib/tools/down_image.dart
\\nimport \'dart:io\';\\nimport \'dart:typed_data\';\\nimport \'package:bot_toast/bot_toast.dart\';\\nimport \'package:extended_image/extended_image.dart\';\\nimport \'package:flutter/material.dart\';\\nimport \'package:flutter_cache_manager/flutter_cache_manager.dart\';\\nimport \'package:image_gallery_saver_plus/image_gallery_saver_plus.dart\';\\nimport \'package:path_provider/path_provider.dart\';\\nimport \'package:permission_handler/permission_handler.dart\';\\nimport \'package:dio/dio.dart\';\\n\\nclass DownImage {\\n /// 获取应用总缓存大小 (单位: MB)\\n static Future<double> getTotalCacheSize() async {\\n double totalSize = 0;\\n final tempDir = await getTemporaryDirectory();\\n totalSize += await _getFolderSize(tempDir);\\n return totalSize;\\n }\\n\\n /// 清理所有缓存\\n static Future<void> clearAllCache() async {\\n // 1. 清理内存中的图片缓存\\n imageCache.clear();\\n imageCache.clearLiveImages();\\n\\n // 2. 清理磁盘中的图片缓存\\n await DefaultCacheManager().emptyCache();\\n await clearExtendedImageCache();\\n\\n // 3. 清理临时目录\\n final tempDir = await getTemporaryDirectory();\\n await _deleteFolder(tempDir);\\n\\n // 4. 清理其他缓存(按需添加)\\n // await SharedPreferences.getInstance().then((prefs) => prefs.clear());\\n // await Hive.deleteFromDisk();\\n }\\n\\n /// 清理 extended_image 特殊缓存\\n static Future<void> clearExtendedImageCache() async {\\n try {\\n final directory = Directory(\\n \'${(await getTemporaryDirectory()).path}/extended_image_cache\');\\n if (directory.existsSync()) {\\n await directory.delete(recursive: true);\\n }\\n } catch (e) {\\n debugPrint(\'清理extended_image缓存失败: $e\');\\n }\\n }\\n\\n /// 计算文件夹大小\\n static Future<double> _getFolderSize(Directory dir) async {\\n if (!dir.existsSync()) return 0;\\n\\n int totalBytes = 0;\\n final files = dir.listSync(recursive: true);\\n\\n await Future.forEach(files, (file) async {\\n if (file is File) {\\n totalBytes += await file.length();\\n }\\n });\\n\\n return totalBytes / (1024 * 1024);\\n }\\n\\n /// 删除文件夹\\n static Future<void> _deleteFolder(Directory dir) async {\\n if (dir.existsSync()) {\\n // ignore: body_might_complete_normally_catch_error\\n await dir.delete(recursive: true).catchError((e) {\\n debugPrint(\'删除文件夹失败: $e\');\\n });\\n }\\n }\\n\\n /// 下载网络图片(先读缓存资源,缓存没有再重新获取资源)\\n static Future<String> downloadNetworkImage(String imagePath) async {\\n var status = await Permission.storage.status;\\n if (!status.isGranted) {\\n //未授予\\n Permission.storage.request();\\n }\\n const String prefix = \'App-Save\';\\n // 获取缓存图片\\n var cacheData = await getNetworkImageData(imagePath, useCache: true);\\n // 获取当前时间戳\\n int timestamp = DateTime.now().millisecondsSinceEpoch;\\n // 将时间戳转换为可读的日期格式\\n String dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp).toString();\\n // 拼接名字\\n String saveName = \'$prefix-$dateTime\';\\n\\n var loadingBot = BotToast.showCustomLoading(\\n backgroundColor: const Color.fromARGB(100, 4, 4, 4),\\n toastBuilder: (cancel) {\\n return Column(\\n mainAxisAlignment: MainAxisAlignment.center,\\n children: [\\n CircularProgressIndicator(),\\n SizedBox(height: 10),\\n Text(\\n \'下载中,请稍后···\',\\n style: TextStyle(color: Colors.white, fontSize: 13),\\n ),\\n ],\\n );\\n });\\n\\n // 下载逻辑\\n late dynamic result;\\n // 如果缓存图片不为空\\n if (cacheData != null) {\\n result = await ImageGallerySaverPlus.saveImage(\\n cacheData,\\n quality: 100,\\n name: saveName,\\n );\\n } else {\\n var response = await Dio()\\n .get(imagePath, options: Options(responseType: ResponseType.bytes));\\n result = await ImageGallerySaverPlus.saveImage(\\n Uint8List.fromList(response.data),\\n quality: 100,\\n );\\n }\\n if (result[\\"isSuccess\\"]) {\\n loadingBot();\\n BotToast.showText(text: \'下载成功\');\\n return result[\\"filePath\\"];\\n } else {\\n loadingBot();\\n BotToast.showText(text: \'下载失败\', contentColor: Colors.red);\\n return \'error\';\\n }\\n }\\n\\n Future<String?> setWallpaper(String url) async {\\n try {\\n final dir = await getTemporaryDirectory();\\n final filename = url.split(\'/\').last;\\n final path = \'${dir.path}/$filename\';\\n // 先读取缓存中的图片 存在缓存则直接返回\\n final cacheData = await getNetworkImageData(url, useCache: true);\\n if (cacheData != null) {\\n await File(path).writeAsBytes(cacheData);\\n return path;\\n } else {\\n final response = await Dio().download(url, path);\\n if (response.statusCode == 200) {\\n return path;\\n }\\n return null;\\n }\\n } catch (e) {\\n print(\'下载失败: $e\');\\n return null;\\n }\\n }\\n}\\n\\n
\\n还有一个问题,获取相册中所有图片时,会存在性能问题:
\\nid == \'isAll\' || name == \'Recent\'
就是最大的相册,我是获取全部图片,就直接获取最大的相册了,没有做细分。final authState = await PhotoManager.requestPermissionExtend();
这个方法获取到的是所有相册。_loadImages()
中 final assets = await album.getAssetListRange(start: start, end: end);
可以获取对应相册中所有图像(图片和视频)的信息,只需要图片,在下方做了判断。刚开始,我是通过album.getAssetListRange(start: start, end: end);
方法一次性获取全部图片,图片多就会存在性能问题和等待时间过长问题,丢个ai优化后就是下面这种分页加载的,用这种方式我自己测试是没啥大问题了,可能是我图片不够多的原因,如果图片过多,依然存在性能问题时建议结合滚动事件,滚动到底部时在获取数据,因为提供了单独选择图片的页面,不存在性能问题,懒得弄滚动事件了就沿用下面这种方式了。
Future<void> _requestPermissionAndLoadAlbums() async {\\n // 请求相册权限\\n final authState = await PhotoManager.requestPermissionExtend();\\n if (authState.isAuth) {\\n // 获取所有相册\\n final albumList = await PhotoManager.getAssetPathList();\\n for (var i = 0; i < albumList.length; i++) {\\n if (albumList[i].id == \'isAll\' || albumList[i].name == \'Recent\') {\\n setState(() {\\n album = albumList[i];\\n });\\n break;\\n }\\n }\\n }\\n _loadImages();\\n }\\n \\n // 加载所有图片\\n Future<void> _loadImages() async {\\n // 清空之前的图片列表\\n setState(() {\\n imageList.clear();\\n currentPage = 0; // 重置页码\\n });\\n\\n // 分页加载图片\\n while (true) {\\n int start = currentPage * pageSize;\\n int end = start + pageSize;\\n\\n // 获取当前页的图片\\n final assets = await album.getAssetListRange(start: start, end: end);\\n\\n // 如果没有更多图片,退出循环\\n if (assets.isEmpty) {\\n break;\\n }\\n\\n // 处理图片并添加到列表\\n for (var asset in assets) {\\n if (asset.type == AssetType.image) {\\n final file = await asset.file;\\n // final compressedFile = await _compressImage(file?.path ?? \'\');\\n setState(() {\\n imageList.add(file!.path);\\n });\\n }\\n }\\n\\n currentPage++;\\n }\\n }\\n\\n
\\n报错内容如下:
\\n[ ] FAILURE: Build failed with an exception.\\n[ +1 ms] * What went wrong:\\n[ ] Execution failed for task \':photo_manager:compileDebugKotlin\'.\\n[ ] > Error while evaluating property \'compilerOptions.jvmTarget\' of task \':photo_manager:compileDebugKotlin\'.\\n[ ] > Failed to calculate the value of property \'jvmTarget\'.\\n[ ] > Unknown Kotlin JVM target: 21\\n[ ] * Try:\\n[ ] > Run with --debug option to get more log output.\\n[ ] > Run with --scan to get full insights.\\n[ ] > Get more help at https://help.gradle.org.\\n[ +1 ms] * Exception is:\\n[ ] org.gradle.api.tasks.TaskExecutionException: Execution failed for task \':photo_manager:compileDebugKotlin\'.\\n[ ] at org.gradle.api.internal.tasks.execution.CatchExceptionTaskExecuter.execute(CatchExceptionTaskExecuter.java:38)\\n[ +36 ms] at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.executeTask(EventFiringTaskExecuter.java:77)\\n[ ] at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:55)\\n[ +1 ms] at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:52)\\n[ ] at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:209)\\n[ ] at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)\\n[ ] at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)\\n[ +1 ms] at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)\\n[ ] at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)\\n[ ] at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)\\n[ ] at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)\\n
\\n到这里就能正常运行项目了,但是又出现了一个新的爆红警告(不影响项目启动):
\\n解决 Flutter 使用 async_wallpaper 插件设置视频壁纸时 Gradle 构建错误的方法
\\n当您在 Flutter 项目中使用 async_wallpaper
插件设置视频壁纸时,可能会遇到以下 Gradle 构建错误:
复制
\\nERROR: Missing classes detected while running R8.\\nPlease add the missing classes or apply additional keep rules that are generated in C:\\\\Users\\\\n03158\\\\Desktop\\\\new_wallpaper\\\\build\\\\app\\\\outputs\\\\mapping\\\\release\\\\missing_rules.txt.\\n
\\n这是因为在构建过程中,R8 检测到某些类缺失。以下是解决此问题的步骤:
\\n检查 missing_rules.txt
文件:
位置:build\\\\app\\\\outputs\\\\mapping\\\\release
,上面的报错截图不全,只能看到部分位置信息,完整报错能看到完整的路径。
在 Gradle 构建输出中,您会看到一个路径指向 missing_rules.txt
文件。该文件包含解决此问题所需的 ProGuard 规则。将其内容复制到您的 proguard-rules.pro
文件中。
在 Flutter 项目中添加 ProGuard 规则以解决 R8 缩减代码时的类缺失问题,具体步骤如下:
\\n步骤 1:找到 proguard-rules.pro
文件
android/app/proguard-rules.pro
路径下(我的项目下面没有这个文件,是我手动创建的)。步骤 2:打开文件并添加规则
\\nmissing_rules.txt
文件中的提示,将相应的规则添加到 proguard-rules.pro
文件中。missing_rules.txt
文件中的内容 copy 到 proguard-rules.pro
文件中。步骤 3:保存并重新构建项目
\\n保存更改后,重新运行 flutter build apk
或 flutter build appbundle
命令以重新构建项目。
通过上述步骤,您可以有效地解决 R8 在代码缩减过程中遇到的类缺失问题。
\\nimgs.isEmpty && !isLoading
逻辑是没有在加载数据中,并且数据为空时显示 Empty() 空组件,有数据时则展示图片。现在的问题是在 Empty() 状态时,下拉刷新不会触发。
@override\\n Widget build(BuildContext context) {\\n super.build(context);\\n return RefreshIndicator(\\n onRefresh: () {\\n setState(() {\\n imgs.clear();\\n results.clear();\\n });\\n getData();\\n return Future.delayed(\\n Duration(milliseconds: OptionsBase().refreshTime));\\n },\\n child: imgs.isEmpty && !isLoading\\n ? SizedBox(\\n height: MediaQuery.of(context).size.height * 0.8,\\n child: Empty(),\\n )\\n : CustomScrollView(\\n controller: scrollController,\\n slivers: [\\n SliverGrid(\\n gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(\\n crossAxisCount: OptionsBase().imageColumns(context),\\n childAspectRatio: widget.sort == \'pc\' ? 1.5 : 0.7,\\n ),\\n delegate: SliverChildBuilderDelegate(\\n (context, index) => buildItem(context, index),\\n childCount: imgs.length,\\n ),\\n ),\\n if (isLoading)\\n SliverToBoxAdapter(\\n child: Center(\\n child: Padding(\\n padding: EdgeInsets.only(\\n top: MediaQuery.of(context).size.height * 0.4),\\n child: CircularProgressIndicator(),\\n ),\\n ),\\n ),\\n ],\\n ),\\n );\\n }\\n
\\nEmpty()组件代码:一个简单的图标和文字描述。
\\nimport \'package:flutter/material.dart\';\\n\\nclass Empty extends StatelessWidget {\\n final double width;\\n const Empty({super.key, this.width = 80});\\n @override\\n Widget build(BuildContext context) {\\n return SizedBox(\\n // 屏幕高度\\n height: MediaQuery.of(context).size.height - 100,\\n child: Center(\\n child: Column(\\n mainAxisAlignment: MainAxisAlignment.center,\\n children: [\\n Icon(\\n Icons.upcoming_outlined,\\n size: width,\\n color: Colors.grey[400],\\n ),\\n Text(\\n \'暂无数据\',\\n style: TextStyle(color: Colors.grey[400], fontSize: 15),\\n )\\n ],\\n ),\\n ),\\n );\\n }\\n}\\n
\\n丢给ai,看看ai的分析:
\\n\\n很显然Empty()就是一个普通的静态小组件,并不具备滚动能力,因此无法触发下拉刷新事件。
根本原因还是对组件的属性不熟导致的
,丢给ai瞬间就能发现问题了。知道问题就好解决了,直接给Empty()包裹在可滚动的组件中即可。
修改后的代码:
\\n @override\\n Widget build(BuildContext context) {\\n super.build(context);\\n return RefreshIndicator(\\n onRefresh: () {\\n setState(() {\\n imgs.clear();\\n results.clear();\\n });\\n getData();\\n return Future.delayed(\\n Duration(milliseconds: OptionsBase().refreshTime));\\n },\\n child: imgs.isEmpty && !isLoading\\n ? SingleChildScrollView(\\n physics: AlwaysScrollableScrollPhysics(),\\n child: Empty(),\\n )\\n : CustomScrollView(\\n controller: scrollController,\\n slivers: [\\n SliverGrid(\\n gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(\\n crossAxisCount: OptionsBase().imageColumns(context),\\n childAspectRatio: widget.sort == \'pc\' ? 1.5 : 0.7,\\n ),\\n delegate: SliverChildBuilderDelegate(\\n (context, index) => buildItem(context, index),\\n childCount: imgs.length,\\n ),\\n ),\\n if (isLoading)\\n SliverToBoxAdapter(\\n child: Center(\\n child: Padding(\\n padding: EdgeInsets.only(\\n top: MediaQuery.of(context).size.height * 0.4),\\n child: CircularProgressIndicator(),\\n ),\\n ),\\n ),\\n ],\\n ),\\n );\\n }\\n\\n
\\n修改点说明:
\\nSingleChildScrollView
和 AlwaysScrollableScrollPhysics()
:
SingleChildScrollView
包裹 Empty()
,并设置 physics: AlwaysScrollableScrollPhysics()
,确保即使内容不足一屏也可以滚动。RefreshIndicator
始终检测到滚动行为,从而支持下拉刷新。软件体验链接: pan.baidu.com/s/1ODDzqQ0R… \\n提取码:68my
\\n项目地址: gitee.com/zsnoin-can/…
","description":"基于flutter的开源壁纸软件 功能描述\\n\\n一款基于flutter的开源壁纸软件,所有接口都是大佬开源的,感谢各位大佬;\\n支持设置下载壁纸;\\n支持简单收藏功能(本地收藏);\\n支持设置壁纸功能;\\n支持获取本地壁纸来设置壁纸;\\n支持扫描本地相册来设置壁纸;\\n支持获取本地视频设置壁纸(只支持mp4格式)。\\n\\n没有登录/注册功能,只能简单的做一个本地存储,功能和其它壁纸软件也大差不差。不过我最喜欢的还是可以设置本地壁纸和本地视频壁纸的功能。\\n\\n1、项目预览图\\n截取了部分图,其它功能有在以前的文章介绍过,懒得在叙述了,有的页面很简单也懒得截了。…","guid":"https://juejin.cn/post/7478870801157324852","author":"Zsnoin能","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-07T08:57:52.251Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/72fb5ca1651f435885f638b2478fc8cd~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgWnNub2lu6IO9:q75.awebp?rk3s=f64ab15b&x-expires=1741943542&x-signature=8BYd0osN7dIp2vouqwGwIcjKUNw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9fdacac458f54488b327d86c1404f8f7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgWnNub2lu6IO9:q75.awebp?rk3s=f64ab15b&x-expires=1741943542&x-signature=1mxAVDS4NL5a76%2Fno8CNv8cBKTU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2d64d7471d5d4864a4a9cfd8a5deccdb~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgWnNub2lu6IO9:q75.awebp?rk3s=f64ab15b&x-expires=1741943542&x-signature=%2Bird7WtzM66JpXgwVh4FOW8XQMg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b0c364d0bc0b4cd0ad0407fc973b33c0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgWnNub2lu6IO9:q75.awebp?rk3s=f64ab15b&x-expires=1741943542&x-signature=Ipe9VZoVKNJ2fgeV6NxyUfdm5Ro%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/34ae4829149041f18a3a9aab35663ff1~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgWnNub2lu6IO9:q75.awebp?rk3s=f64ab15b&x-expires=1741943542&x-signature=HFrq%2BilOVG4Vq8IIwlV4CDFVfH0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/85d1025c20ad4dc09bfed62ee9149b76~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgWnNub2lu6IO9:q75.awebp?rk3s=f64ab15b&x-expires=1741943542&x-signature=tmsdq1EyHLqt0MBPG0qkwJGRgY8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ff1d1e9e53734339b65a0a0864620133~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgWnNub2lu6IO9:q75.awebp?rk3s=f64ab15b&x-expires=1741943542&x-signature=J98Di%2BaPbSq8doqPqMhPg7MdpTU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9ecd6c6c301a49fd9352c47c5228d50e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgWnNub2lu6IO9:q75.awebp?rk3s=f64ab15b&x-expires=1741943542&x-signature=bkD4inJ32t0Pm4Fu1mRnk16pYko%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/49541b2006e04ea0b24008406b0b2571~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgWnNub2lu6IO9:q75.awebp?rk3s=f64ab15b&x-expires=1741943542&x-signature=zWmcZy8OGGIDsKlyKcC22q0poTM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/8ee728d7ca5541279efc9f3cb0584b25~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgWnNub2lu6IO9:q75.awebp?rk3s=f64ab15b&x-expires=1741943542&x-signature=hshOX3n8%2FbQDy2gv8AsclEwtHRM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f21819bca37042b9b74aa9d892a6e21d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgWnNub2lu6IO9:q75.awebp?rk3s=f64ab15b&x-expires=1741943542&x-signature=%2FiG6cvhYO0VWCg9JKYaZxaLn47A%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/bc342a9b4e264153bd7a845335dfc0e8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgWnNub2lu6IO9:q75.awebp?rk3s=f64ab15b&x-expires=1741943542&x-signature=0Pgd0Vv3eoxQTLqjmQlebf3QGF8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/21edb756e43c4d888bb9f155439e4345~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgWnNub2lu6IO9:q75.awebp?rk3s=f64ab15b&x-expires=1741943542&x-signature=ZlBGbUMV6v5kg9wsalWD1wXvTus%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/31547f1052dc41daa3b80fe5367b075a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgWnNub2lu6IO9:q75.awebp?rk3s=f64ab15b&x-expires=1741943542&x-signature=Aq%2FM1sOvDhSTxQZUbHngPtq4kpo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9298bf51efa140d0a639c311d15ec3c8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgWnNub2lu6IO9:q75.awebp?rk3s=f64ab15b&x-expires=1741943542&x-signature=GRvc7CzTYqvWdIMfUw4s53UW4Hc%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"HarmonyOS NEXT-Flutter混合开发之鸿蒙-代码实践","url":"https://juejin.cn/post/7478618857882026011","content":"在 Flutter 三端分离模式下完成纯血鸿蒙混入的过程中,虽然官方文档提供了一定的指导,但实际操作中可能会遇到一些坑。以下是我在适配过程中的一些经验总结,供各位开发者参考 😄 如果有帮助点个赞。
\\n在混入过程中是基于咸鱼团队 flutter_boost(这里不讨论和其他方案的差别) 和自定义 FlutterPlugin 实现的。
\\n主要涉及内容:
\\n准备支持鸿蒙的 Flutter 开发环境,flutter_fluter 仓库基于 Flutter SDK 对于OpenHarmony平台的兼容拓展,可支持 IDE 或者终端使用 Flutter Tools 指令编译和构建 OpenHarmony 应用程序。
\\n不再赘述,上链接。
\\n创建 Fluter 项目
\\nflutter create -t module --org xyz.zhousg demo_fluter \\n
\\n打包 Fluter 项目
\\nflutter build har --debug\\n
\\ndependencies:\\n flutter:\\n sdk: flutter\\n\\n # The following adds the Cupertino Icons font to your application.\\n # Use with the CupertinoIcons class for iOS style icons.\\n cupertino_icons: ^1.0.2\\n fl_chart: ^0.62.0\\n flutter_boost:\\n+ git:\\n+ url: \'https://github.com/alibaba/flutter_boost.git\'\\n+ ref: \'4.6.5\'\\n
\\nimport \'package:flutter/cupertino.dart\';\\nimport \'package:flutter/material.dart\';\\nimport \'package:flutter_boost/flutter_boost.dart\';\\n\\n// 1. 创建一个自定义的Binding,继承和with的关系如下,里面什么都不用写\\nclass CustomFlutterBinding extends WidgetsFlutterBinding\\n with BoostFlutterBinding {}\\n\\nvoid main() {\\n // 2. 这里的CustomFlutterBinding调用务必不可缺少,用于控制Boost状态的resume和pause\\n CustomFlutterBinding();\\n runApp(const MyApp());\\n}\\n\\nclass MyApp extends StatefulWidget {\\n const MyApp({super.key});\\n\\n @override\\n State<MyApp> createState() => _MyAppState();\\n}\\n\\nclass _MyAppState extends State<MyApp> {\\n // 3. 路由表\\n Map<String, FlutterBoostRouteFactory> routerMap = {\\n \'SettingsPage\': (settings, isContainerPage, uniqueId) {\\n return CupertinoPageRoute(\\n settings: settings,\\n builder: (BuildContext ctx) {\\n return const Placeholder();\\n },\\n );\\n },\\n \'DeviceStoragePage\': (settings, isContainerPage, uniqueId) {\\n return CupertinoPageRoute(\\n settings: settings,\\n builder: (BuildContext ctx) {\\n return const Placeholder();\\n },\\n );\\n },\\n \'AboutPage\': (settings, isContainerPage, uniqueId) {\\n return CupertinoPageRoute(\\n settings: settings,\\n builder: (BuildContext ctx) {\\n return const Placeholder();\\n },\\n );\\n },\\n \'/\': (settings, isContainerPage, uniqueId) {\\n return CupertinoPageRoute(\\n settings: settings,\\n builder: (BuildContext ctx) {\\n return const Placeholder();\\n },\\n );\\n },\\n };\\n\\n // 路由工厂函数\\n Route<dynamic> routeFactory(\\n RouteSettings settings, bool isContainerPage, String? uniqueId) {\\n FlutterBoostRouteFactory? fn = routerMap[settings.name];\\n if (fn == null) {\\n throw FlutterError(\\n \'Route \\"${settings.toString()}\\" is not defined in routerMap.\');\\n }\\n return fn(settings, isContainerPage, uniqueId)!;\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n // flutter_boost 接管\\n return FlutterBoostApp(\\n routeFactory,\\n // Flutter 侧直接预览需要,需要使用 Deveco Studio 导入 .ohos 项目进行自动签名预览至鸿蒙设备\\n initialRoute: \'SettingsPage\',\\n appBuilder: (home) {\\n return MaterialApp(\\n builder: (context, child) => home,\\n );\\n },\\n );\\n }\\n}\\n
\\n这里使用的是 router + FlutterPage 方式展示 Flutter 界面, Navigation 后续再说吧~
\\na. 先打包 Fluter 项目,会生成三个产物
\\n.ohos\\n |--har\\n |-- fluter_boost.har\\n |-- fluter_module.har\\n |-- fluter.har\\n
\\nb. 在鸿蒙项目中,引入依赖
\\noh-package.json5
\\"dependencies\\": {\\n \\"@ohos/flutter_module\\": \\"file:../demo_flutter/.ohos/har/flutter_module.har\\",\\n // 下面两个依赖我直接拷贝到了 libs 下\\n \\"@ohos/flutter_ohos\\": \\"file:./libs/flutter.har\\",\\n \\"flutter_boost\\": \\"file:./libs/flutter_boost.har\\"\\n},\\n\\"overrides\\": {\\n \\"@ohos/flutter_ohos\\": \\"file:./libs/flutter.har\\",\\n \\"flutter_boost\\": \\"file:./libs/flutter_boost.har\\"\\n},\\n
\\nc. 初始化 flutter_boost
\\nentryAbility.ets
import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from \'@kit.AbilityKit\';\\nimport { router } from \'@kit.ArkUI\';\\nimport { FlutterManager } from \'@ohos/flutter_ohos\';\\nimport { FlutterBoostDelegate, FlutterBoostRouteOptions, FlutterBoost, FlutterBoostSetupOptionsBuilder } from \'flutter_boost\';\\nimport { GeneratedPluginRegistrant } from \'@ohos/flutter_module\';\\n\\n\\nexport default class EntryAbility extends UIAbility implements FlutterBoostDelegate{\\n pushNativeRoute(options: FlutterBoostRouteOptions): void {\\n // throw new Error(\'Method not implemented.\');\\n }\\n\\n pushFlutterRoute(options: FlutterBoostRouteOptions,): void {\\n // throw new Error(\'Method not implemented.\');\\n }\\n\\n popRoute(options: FlutterBoostRouteOptions): boolean {\\n // throw new Error(\'Method not implemented.\');\\n router.back()\\n return true\\n }\\n\\n async onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {\\n FlutterManager.getInstance().pushUIAbility(this);\\n }\\n\\n onDestroy(): void {\\n FlutterManager.getInstance().popUIAbility(this);\\n }\\n\\n onWindowStageCreate(windowStage: window.WindowStage): void {\\n // Flutter bind in UIAbility\\n FlutterManager.getInstance().pushWindowStage(this, windowStage);\\n // Initial FlutterBoost\\n const optionsBuilder: FlutterBoostSetupOptionsBuilder = new FlutterBoostSetupOptionsBuilder()\\n FlutterBoost.getInstance().setup(this, this.context, (engine) => {\\n GeneratedPluginRegistrant.registerWith(engine)\\n }, optionsBuilder.build())\\n\\n windowStage.loadContent(\'pages/Index\');\\n }\\n\\n onWindowStageDestroy(): void {\\n FlutterManager.getInstance().popWindowStage(this);\\n }\\n\\n onForeground(): void {\\n logger.info(\'Ability onForeground\');\\n }\\n\\n onBackground(): void {\\n logger.info(\'Ability onBackground\');\\n }\\n}\\n
\\n这里部分代码省略了 ~ pushNativeRoute pushFlutterRoute popRoute 具体实现还是参考官方仓库
\\nd. Flutter容器与跳转
\\npages/FluterPage.ets
import { FlutterEntry, FlutterPage, FlutterView} from \'@ohos/flutter_ohos\';\\nimport { FlutterBoost, FlutterBoostEntry } from \'flutter_boost\';\\nimport { router } from \'@kit.ArkUI\';\\n\\n@Entry\\n@Component\\nstruct SettingsPage {\\n private flutterEntry?: FlutterEntry;\\n private flutterView?: FlutterView\\n\\n aboutToAppear() {\\n this.flutterEntry = new FlutterBoostEntry(getContext(this), router.getParams())\\n this.flutterEntry?.aboutToAppear()\\n this.flutterView = this.flutterEntry?.getFlutterView()\\n }\\n\\n aboutToDisappear() {\\n this.flutterEntry?.aboutToDisappear()\\n }\\n\\n onPageShow() {\\n this.flutterEntry?.onPageShow()\\n }\\n\\n onPageHide() {\\n this.flutterEntry?.onPageHide()\\n }\\n\\n onBackPress(): boolean | void {\\n FlutterBoost.getInstance()\\n .getPlugin()?.onBackPressed();\\n return true;\\n }\\n\\n build() {\\n Column() {\\n FlutterPage({ viewId: this.flutterView?.getId() })\\n .width(\'100%\')\\n .height(\'100%\')\\n }\\n .width(\'100%\')\\n .height(\'100%\')\\n }\\n}\\n
\\npages/Index.ets
// uri Flutter Module 中的路由表 KEY params 是传参\\nrouter.pushUrl({ url: \'pages/FlutterPage\', params: { uri: \'DeviceStoragePage\', params: {} } })\\nrouter.pushUrl({ url: \'pages/FlutterPage\', params: { uri: \'SettingPage\', params: {} } })\\nrouter.pushUrl({ url: \'pages/FlutterPage\', params: { uri: \'AboutPage\', params: {} } })\\n
\\n TextButton(\\n onPressed: () {\\n Map<String, String> data = {\'name\': \'jack\'};\\n BoostChannel.instance.sendEventToNative(\'updateUser\', data);\\n },\\n child: const Text(\'发送消息\'),\\n ),\\n
\\npages/FlutterPage.ets
aboutToAppear() {\\n this.flutterEntry = new FlutterBoostEntry(getContext(this), router.getParams())\\n this.flutterEntry?.aboutToAppear()\\n this.flutterView = this.flutterEntry?.getFlutterView()\\n\\n const plugin = FlutterBoost.getInstance()\\n .getPlugin()\\n if (plugin) {\\n // 通信\\n plugin.addEventListener(\'updateUser\', {\\n onEvent: (key, args) => {\\n // logger.debug(`事件名称 ${key}`, JSON.stringify(args))\\n promptAction.showToast({ message: \'Flutter Data: \' + JSON.stringify(args) })\\n }\\n })\\n }\\n
\\nfinal _platform = const MethodChannel(\'xyz.zhousg.interview_success_project\');\\n
\\nTextButton(\\n onPressed: () {\\n _platform.invokeMethod(\'openCamera\').then(\\n (value) => setState(() {\\n // value 鸿蒙侧回传数据\\n }),\\n );\\n },\\n child: Text(\'打开相机),\\n),\\n
\\n定义 Flutter 插件
\\nNativePlugin.ets
import { FlutterPlugin, FlutterPluginBinding, MethodChannel, MethodResult } from \'@ohos/flutter_ohos\';\\n\\nexport class NativePlugin implements FlutterPlugin {\\n private channel?: MethodChannel;\\n\\n getUniqueClassName(): string {\\n return \'CameraPlugin\'\\n }\\n\\n onAttachedToEngine(binding: FlutterPluginBinding): void {\\n this.channel = new MethodChannel(binding.getBinaryMessenger(), \'xyz.zhousg.interview_success_project\')\\n this.channel.setMethodCallHandler({\\n onMethodCall: (call, result) => {\\n switch (call.method) {\\n case \\"openCamera\\":\\n this.openCamera(result)\\n break;\\n default:\\n result.notImplemented()\\n break;\\n }\\n }\\n })\\n }\\n\\n onDetachedFromEngine(binding: FlutterPluginBinding): void {\\n this.channel?.setMethodCallHandler(null);\\n }\\n\\n\\n // native api\\n openCamera (result: MethodResult) {\\n // 回传数据给 Flutter\\n result.success(\'http://test.png\')\\n }\\n}\\n
\\n注册插件
\\nentryAbility.ets
// Initial FlutterBoost\\nconst optionsBuilder: FlutterBoostSetupOptionsBuilder = new FlutterBoostSetupOptionsBuilder()\\nFlutterBoost.getInstance().setup(this, this.context, (engine) => {\\n GeneratedPluginRegistrant.registerWith(engine)\\n // 打开相机\\n engine.getPlugins()?.add(new NativePlugin())\\n}, optionsBuilder.build())\\n
\\n使用 flutter_boost 开发 Flutter混合项目,在鸿蒙这边和混合 Web 组件进行混合开发很相似,搞定Flutter页面栈+鸿蒙页面栈跳转,搞定数据通信和原生调用,基本可以满足一般的开发,以上这里仅供参考哈~
","description":"在 Flutter 三端分离模式下完成纯血鸿蒙混入的过程中,虽然官方文档提供了一定的指导,但实际操作中可能会遇到一些坑。以下是我在适配过程中的一些经验总结,供各位开发者参考 😄 如果有帮助点个赞。 在混入过程中是基于咸鱼团队 flutter_boost(这里不讨论和其他方案的差别) 和自定义 FlutterPlugin 实现的。\\n\\n主要涉及内容:\\n\\n环境搭建\\nFlutter module 创建\\nFutter 引入 flutter_boost\\nHarmony 引入 flutter_boost\\nFlutter 与鸿蒙侧通信\\nFlutter 调用鸿蒙原生\\n环…","guid":"https://juejin.cn/post/7478618857882026011","author":"zhousg","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-07T02:56:51.438Z","media":null,"categories":["前端","HarmonyOS","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"在已有flutter 项目中集成 flutter_rust_bridge","url":"https://juejin.cn/post/7478580065640333375","content":"1 添加依赖
flutter_rust_bridge: 2.3.0\\n
\\n2 安装rust 环境
3 通过cargo 创建flutter_rust 项目
命令 flutter_rust_bridge_codegen create blade
\\n创建完成项目结构图 我们只关注 rust 跟 rust_builder文件夹
\\n4 拷贝 rust 跟 rust_builder 文件夹到已有的flutter项目根目录
5 在yaml文件中引入rust_builder
6 增加 flutter_rust_bridge.yaml
这个文件在第三步创建的项目里面也有 直接拷贝过来也可以。
\\n这里的dart_output 文件夹 需要自己手动创建一下。不然会运行失败。
\\n7 在rust代码中增加方法 然后就可以直接调用了
添加以及生成flutter调用的方法 参考\\njuejin.cn/post/741330…
\\n8 调用
\\n在 lib/src/rust/api 下面能找到在rust中的方法的映射
调用方法之前需要先调用
\\nRustLib.init();\\n\\n
","description":"1 添加依赖 flutter_rust_bridge: 2.3.0\\n\\n\\n2 安装rust 环境\\n\\njuejin.cn/post/741321…\\n\\n3 通过cargo 创建flutter_rust 项目 \\n\\n命令 flutter_rust_bridge_codegen create blade\\n\\n创建完成项目结构图 我们只关注 rust 跟 rust_builder文件夹\\n\\n4 拷贝 rust 跟 rust_builder 文件夹到已有的flutter项目根目录\\n\\n5 在yaml文件中引入rust_builder\\n\\n6 增加 flutter_rust_bridge…","guid":"https://juejin.cn/post/7478580065640333375","author":"火柴就是我","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-07T02:49:06.517Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d67340acaa544d0fbd53edaed3c10783~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg54Gr5p-05bCx5piv5oiR:q75.awebp?rk3s=f64ab15b&x-expires=1741920546&x-signature=wpVxyOXW4aI%2FjPTwIT82puOw4kM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0029bf02c05b4146b2be9ddebe035248~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg54Gr5p-05bCx5piv5oiR:q75.awebp?rk3s=f64ab15b&x-expires=1741920546&x-signature=gaFgyLf%2FnbMXCibWq6OLT7R%2FdWw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f7dba5c233704bc9807ef2d28a59e550~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg54Gr5p-05bCx5piv5oiR:q75.awebp?rk3s=f64ab15b&x-expires=1741920546&x-signature=WL7Z8m6In92T%2FEPnBp1svKtt5P0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9101ca326bf54a72a3c53f8f890b27eb~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg54Gr5p-05bCx5piv5oiR:q75.awebp?rk3s=f64ab15b&x-expires=1741920546&x-signature=FWnfkyxu6cm%2B%2FWoPt%2B%2Fv0TCwDos%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 监听当前页面可见与隐藏状态","url":"https://juejin.cn/post/7478504097394999315","content":"flutter
可以监听 app
进入前台还是后台状态,也可以监听当前某个页面 当前正在显示
还是 隐藏了
。
找一个公共文件初始化一下 路由观察者
,例如:
public.dart
,只要能做成全局对象就行。
/// 路由观察者\\nfinal routeObserver = RouteObserver<PageRoute>();\\n
\\n有了 路由观察者
对象后,注册为导航监听者:
main.dart
/// MaterialApp\\nWidget buildMaterialApp(BuildContext context) {\\n // 这里直接在 MaterialApp 对象进行全局注册,其他模式也有,例如 CupertinoApp、WidgetsAp、Navigator\\n return GetMaterialApp(\\n title: \'Flutter Demo\',\\n // 注册\\n navigatorObservers: [routeObserver],\\n );\\n ...\\n ...\\n ...\\n }\\n}\\n
\\n页面使用,推荐封装作为底层 BaseStateful
使用,其他页面继承,可以随时打开,也可以单个页面使用:
import \'package:flutter/material.dart\';\\nimport \'package:base_project/utils/public.dart\';\\n\\n@immutable\\nclass BaseStatefulController extends StatefulWidget {\\n\\n const BaseStatefulController({super.key});\\n\\n @override\\n State<BaseStatefulController> createState() => BaseStatefulControllerState();\\n}\\n\\nclass BaseStatefulControllerState extends State<BaseStatefulController> with RouteAware {\\n\\n /// 启用路由观察者\\n bool enableRouteObserver = false;\\n\\n @override\\n void initState() {\\n super.initState();\\n // 等待加载\\n WidgetsBinding.instance.addPostFrameCallback((_) {\\n // 初始化上下文完成\\n initStateContext();\\n });\\n }\\n\\n /// 初始化上下文完成,可以在这里做一些需要上下文的初始化操作\\n void initStateContext () {\\n // 注册路由监听\\n if (enableRouteObserver) {\\n final route = ModalRoute.of(context);\\n if (route is PageRoute) {\\n routeObserver.subscribe(this, route);\\n }\\n }\\n }\\n\\n @override\\n void dispose() {\\n // 取消路由监听\\n routeObserver.unsubscribe(this);\\n super.dispose();\\n }\\n\\n @override\\n void didPush() {\\n print(\\"页面被 push 到栈顶,页面可见\\");\\n }\\n @override\\n void didPop() {\\n print(\\"页面被 pop,页面销毁\\");\\n }\\n @override\\n void didPushNext() {\\n print(\\"有新页面 push 进来,当前页面进入不可见状态\\");\\n }\\n @override\\n void didPopNext() {\\n print(\\"上一个页面被 pop,当前页面重新可见\\");\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Container();\\n }\\n}\\n
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n方法 | 作用 |
---|---|
subscribe(routeAware, route) | 订阅某个页面,监听生命周期变化 |
unsubscribe(routeAware) | 取消订阅,避免内存泄漏 |
didPush() | 页面进入可见 |
didPop() | 页面销毁 |
didPushNext() | 当前页面被覆盖,不可见 |
didPopNext() | 上一个页面被 pop ,当前页面重新可见 |
Scaffold
—— 界面设计的脚手架
Scaffold
就像移动应用的\\"房间布局图\\"
,它决定了页面的基本骨架结构。想象布置一个客厅时,Scaffold
就是空间规划师:顶部挂画的位置(AppBar
)、沙发摆放区域(Body
)、墙角的收纳柜(Drawer
)、茶几上的台灯(FloatingActionButton
)。
该组件通过预设的布局模块,让开发者像搭积木一样快速构建标准页面,同时保留充分的定制空间。理解Scaffold
的运作机制,是掌握Flutter
界面开发的关键第一步。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\n属性详解
及分类列表
属性详解:
\\nScaffold({\\n super.key, // 组件唯一标识键\\n this.appBar, // 顶部导航栏(PreferredSizeWidget?)\\n this.body, // 主体内容区域(Widget?)\\n this.floatingActionButton, // 悬浮操作按钮(Widget?)\\n this.floatingActionButtonLocation, // 悬浮按钮位置(FloatingActionButtonLocation?)\\n this.floatingActionButtonAnimator, // 悬浮按钮位置变化动画器(FloatingActionButtonAnimator?)\\n this.persistentFooterButtons, // 底部固定按钮组(List<Widget>?)\\n this.persistentFooterAlignment = AlignmentDirectional.centerEnd, // 底部按钮组对齐方式\\n this.drawer, // 左侧抽屉菜单(Widget?)\\n this.onDrawerChanged, // 抽屉状态变化回调(DrawerCallback?)\\n this.endDrawer, // 右侧抽屉菜单(Widget?)\\n this.onEndDrawerChanged, // 右侧抽屉状态变化回调(DrawerCallback?)\\n this.bottomNavigationBar, // 底部导航栏(Widget?)\\n this.bottomSheet, // 底部附着式面板(Widget?)\\n this.backgroundColor, // 背景颜色(Color?)\\n this.resizeToAvoidBottomInset, // 是否自动避开键盘(bool?,Android默认true,iOS默认false)\\n this.primary = true, // 是否作为主视图处理滚动(bool)\\n this.drawerDragStartBehavior = DragStartBehavior.start, // 抽屉拖动手势起始行为\\n this.extendBody = false, // 是否延伸主体到底部栏后面(bool)\\n this.extendBodyBehindAppBar = false, // 是否延伸主体到AppBar后面(bool)\\n this.drawerScrimColor, // 抽屉打开时遮罩颜色(Color?,默认Colors.black54)\\n this.drawerEdgeDragWidth, // 触发抽屉拖动的边缘区域宽度(double?)\\n this.drawerEnableOpenDragGesture = true, // 是否允许手势打开左侧抽屉(bool)\\n this.endDrawerEnableOpenDragGesture = true, // 是否允许手势打开右侧抽屉(bool)\\n this.restorationId, // 状态恢复标识符(String?)\\n })\\n
\\n核心属性分类列表:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n类别 | 属性名称 | 属性类型 | 作用描述 | 默认值 |
---|---|---|---|---|
结构类 | appBar | PreferredSizeWidget? | 页面顶部的应用栏(包含标题、操作按钮等) | null |
body | Widget? | 页面主体内容区域 | null | |
drawer | Widget? | 左侧抽屉式导航菜单(支持手势滑动打开) | null | |
endDrawer | Widget? | 右侧抽屉式导航菜单(针对RTL布局自动翻转) | null | |
bottomNavigationBar | Widget? | 底部导航栏(通常与PageView配合使用) | null | |
bottomSheet | Widget? | 固定在底部的持久性面板 | null | |
样式类 | backgroundColor | Color? | 整个Scaffold的背景颜色 | null |
extendBody | bool | 是否将底部组件(bottomNavigationBar/bottomSheet)延伸至屏幕底部边缘 | false | |
extendBodyBehindAppBar | bool | 是否让body内容延伸到appBar下方 | false | |
resizeToAvoidBottomInset | bool | 是否自动调整body尺寸以避免被键盘等底部插入物遮挡 | true | |
drawerScrimColor | Color? | 打开抽屉时主内容的遮罩颜色 | Colors.black54 | |
交互类 | drawerEnableOpenDragGesture | bool | 是否允许通过手势滑动打开左侧抽屉 | true |
endDrawerEnableOpenDragGesture | bool | 是否允许通过手势滑动打开右侧抽屉 | true | |
floatingActionButton | Widget? | 悬浮操作按钮(通常用于主要操作) | null | |
floatingActionButtonLocation | FloatingActionButtonLocation? | 悬浮按钮的位置配置(预定义位置或自定义位置) | FabEndTop | |
floatingActionButtonAnimator | FloatingActionButtonAnimator? | 悬浮按钮位置变化的动画控制器 | null | |
状态类 | isDrawerOpen | bool (read-only) | 当前左侧抽屉是否处于打开状态 | - |
isEndDrawerOpen | bool (read-only) | 当前右侧抽屉是否处于打开状态 | - |
appBar
:顶部导航栏,通常位于屏幕顶部,包含页面标题
、导航图标
和操作按钮
等。body
:主体内容区,放置页面的主要内容,如文本
、图片
、列表
等。drawer
:左侧滑出式侧边栏(抽屉菜单
),通过向右滑动
或点击菜单图标
打开。endDrawer
:右侧滑出式侧边栏(与 drawer
方向相反),适用于从右向左布局的语言。FAB
:FloatingActionButton
(浮动操作按钮),通常是一个圆形按钮
,悬浮在界面上
,用于执行主要操作。bottomNavigationBar
:底部导航栏,用于主要视图切换(通常配合3-5
个导航项)。bottomSheet
:底部弹出的固定/临时
面板。\\npersistent
:持续显示(如地图信息栏
) 。modal
:模态弹窗(需要用户操作后关闭
)。这些组件共同构成了一个完整的用户界面布局。
\\n//最小可用结构\\nScaffold(\\n appBar: AppBar(title: Text(\'首页\')),\\n body: Center(child: Text(\'主体内容\')),\\n)\\n\\n//完整布局\\nScaffold(\\n appBar: buildAppBar(context),\\n drawer: buildDrawer(),\\n body: buildBody(),\\n bottomNavigationBar: buildBottomNavigationBar(),\\n floatingActionButton: buildFloatingActionButton(context),\\n bottomSheet: buildContainer(),\\n)\\n
\\nScaffold
:脚手架
解析AppBar
:顶部导航系统作用: ➤ 顶部导航栏,承载标题
、导航按钮
、操作菜单
等。
\\n核心配置:
AppBar(\\n title: Text(\'主页\'), // 主标题\\n leading: IconButton(icon: Icon(Icons.menu)), // 左侧按钮\\n actions: [ // 右侧操作区\\n IconButton(icon: Icon(Icons.search)),\\n PopupMenuButton(itemBuilder: (context) => [...]),\\n ],\\n flexibleSpace: Container(...), // 灵活空间(如渐变背景)\\n bottom: PreferredSize( // 底部附加组件(如TabBar)\\n child: TabBar(tabs: [...]),\\n preferredSize: Size.fromHeight(48),\\n ),\\n)\\n
\\n\\n\\n深入探究可查看:系统化掌握Flutter组件之AppBar(一):筑基之旅
\\n
drawer & endDrawer
:侧边导航系统属性 | 默认方向 | 适用场景 |
---|---|---|
drawer | 左侧滑出 | 主菜单/导航 |
endDrawer | 右侧滑出 | 辅助功能/设置 |
基础抽屉实现:
\\nDrawer buildDrawer() {\\n return Drawer(\\n child: ListView(\\n children: [\\n DrawerHeader(child: Text(\'用户信息\')),\\n ListTile(title: Text(\'个人中心\'), leading: Icon(Icons.person)),\\n ListTile(title: Text(\'设置\'), leading: Icon(Icons.settings))\\n ],\\n ),\\n );\\n}\\n
\\n双抽屉系统:
\\nScaffold(\\n drawer: LeftDrawer(),\\n endDrawer: RightDrawer()\\n)\\n
\\n注意事项:
\\ndrawer
和 endDrawer
导致手势冲突。DrawerHeader
增强视觉层次。bottomNavigationBar
:底部导航体系基础底部栏:
\\nBottomNavigationBar buildBottomNavigationBar() {\\n return BottomNavigationBar(\\n currentIndex: _selectedIndex,\\n items: [\\n BottomNavigationBarItem(icon: Icon(Icons.home), label: \'首页\'),\\n BottomNavigationBarItem(icon: Icon(Icons.settings), label: \'设置\'),\\n ],\\n onTap: (index) => setState(() => _selectedIndex = index),\\n );\\n}\\n
\\n注意事项:
\\n3-5
个,过多会导致布局拥挤。type: BottomNavigationBarType.fixed
防止标签文字隐藏。floatingActionButton
:浮动按钮系统FloatingActionButton buildFloatingActionButton(BuildContext context) {\\n return FloatingActionButton(\\n child: Icon(Icons.add),\\n onPressed: () => showModalBottomSheet(\\n context: context,\\n builder: (ctx) => Container(\\n width: double.infinity,\\n height: 200,\\n alignment: Alignment.center,\\n child: Text(\'新建内容\'),\\n ),\\n ),\\n );\\n}\\n
\\n注意事项:
\\nSpeedDial
等扩展组件。bottomSheet
:底部面板两种模式:
\\n// 持久性面板\\nbottomSheet: Container(\\n height: 40,\\n color: Colors.grey[200],\\n child: Center(child: Text(\'持久性信息栏\')),\\n)\\n\\n// 模态面板(需通过事件触发)\\nshowModalBottomSheet(\\n context: context,\\n builder: (context) => Container(...),\\n);\\n
\\n注意事项:
\\nbottomSheet
中放置过多交互元素。enableDrag: false
禁用拖动关闭)。import \'package:flutter/material.dart\';\\n\\nclass ScaffoldDemo extends StatefulWidget {\\n @override\\n _ScaffoldDemoState createState() => _ScaffoldDemoState();\\n}\\n\\nclass _ScaffoldDemoState extends State<ScaffoldDemo> {\\n int _selectedIndex = 0;\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: buildAppBar(context),\\n drawer: buildDrawer(),\\n body: buildBody(),\\n bottomNavigationBar: buildBottomNavigationBar(),\\n floatingActionButton: buildFloatingActionButton(context),\\n bottomSheet: buildContainer(),\\n );\\n }\\n\\n AppBar buildAppBar(BuildContext context) {\\n return AppBar(\\n title: Text(\'Flutter 布局大全\'),\\n actions: [IconButton(icon: Icon(Icons.share), onPressed: () {})],\\n backgroundColor: Theme.of(context).colorScheme.inversePrimary,\\n );\\n }\\n\\n Drawer buildDrawer() {\\n return Drawer(\\n child: ListView(\\n children: [\\n DrawerHeader(child: Text(\'用户信息\')),\\n ListTile(title: Text(\'个人中心\'), leading: Icon(Icons.person)),\\n ListTile(title: Text(\'设置\'), leading: Icon(Icons.settings))\\n ],\\n ),\\n );\\n }\\n\\n Widget buildBottomNavigationBar() {\\n return BottomNavigationBar(\\n currentIndex: _selectedIndex,\\n type: BottomNavigationBarType.fixed,\\n items: [\\n BottomNavigationBarItem(icon: Icon(Icons.home), label: \'首页\'),\\n BottomNavigationBarItem(icon: Icon(Icons.settings), label: \'设置\'),\\n ],\\n onTap: (index) => setState(() => _selectedIndex = index),\\n );\\n }\\n\\n FloatingActionButton buildFloatingActionButton(BuildContext context) {\\n return FloatingActionButton(\\n child: Icon(Icons.add),\\n onPressed: () => showModalBottomSheet(\\n context: context,\\n builder: (ctx) => Container(\\n width: double.infinity,\\n height: 200,\\n alignment: Alignment.center,\\n child: Text(\'新建内容\'),\\n ),\\n ),\\n );\\n }\\n\\n Center buildBody() {\\n return Center(\\n child: Text(\'当前页面: ${[\'首页\', \'设置\'][_selectedIndex]}\'),\\n );\\n }\\n\\n Container buildContainer() {\\n return Container(\\n height: 40,\\n color: Colors.grey[200],\\n child: Center(child: Text(\'持久性信息栏\')),\\n );\\n }\\n}\\n
\\n企业级应用:动态主题切换
+ 多级导航
import \'package:flutter/material.dart\';\\n\\n/// 全局主题状态管理\\nclass ThemeState extends InheritedWidget {\\n final bool isDark;\\n final VoidCallback toggleTheme;\\n\\n const ThemeState({\\n super.key,\\n required this.isDark,\\n required this.toggleTheme,\\n required super.child,\\n });\\n\\n static ThemeState of(BuildContext context) {\\n return context.dependOnInheritedWidgetOfExactType<ThemeState>()!;\\n }\\n\\n @override\\n bool updateShouldNotify(ThemeState oldWidget) {\\n return isDark != oldWidget.isDark;\\n }\\n}\\n\\nclass ScaffoldDemo1 extends StatefulWidget {\\n const ScaffoldDemo1({super.key});\\n\\n @override\\n State<ScaffoldDemo1> createState() => _ScaffoldDemo1State();\\n}\\n\\nclass _ScaffoldDemo1State extends State<ScaffoldDemo1> {\\n bool _isDark = false;\\n\\n void _toggleTheme() {\\n setState(() => _isDark = !_isDark);\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return ThemeState(\\n isDark: _isDark,\\n toggleTheme: _toggleTheme,\\n child: MaterialApp(\\n theme: _buildTheme(_isDark),\\n home: const MainScreen(),\\n debugShowCheckedModeBanner: false,\\n ),\\n );\\n }\\n\\n ThemeData _buildTheme(bool isDark) {\\n return isDark\\n ? ThemeData.dark().copyWith(\\n colorScheme: const ColorScheme.dark().copyWith(\\n secondary: Colors.cyan[300],\\n ),\\n )\\n : ThemeData.light().copyWith(\\n colorScheme: const ColorScheme.light().copyWith(\\n secondary: Colors.cyan[800],\\n ),\\n );\\n }\\n}\\n\\n// 主页面框架\\nclass MainScreen extends StatefulWidget {\\n const MainScreen({super.key});\\n\\n @override\\n State<MainScreen> createState() => _MainScreenState();\\n}\\n\\nclass _MainScreenState extends State<MainScreen> {\\n int _currentIndex = 0;\\n final List<Widget> _pages = [\\n const HomePage(key: PageStorageKey(\'Home\')),\\n const SettingsPage(key: PageStorageKey(\'Settings\')),\\n ];\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: const Text(\'动态主题示例\'),\\n actions: [\\n IconButton(\\n icon: const Icon(Icons.brightness_6),\\n onPressed: ThemeState.of(context).toggleTheme,\\n )\\n ],\\n ),\\n drawer: const AppDrawer(),\\n body: IndexedStack(\\n index: _currentIndex,\\n children: _pages,\\n ),\\n bottomNavigationBar: BottomNavigationBar(\\n currentIndex: _currentIndex,\\n onTap: (index) => setState(() => _currentIndex = index),\\n items: const [\\n BottomNavigationBarItem(\\n icon: Icon(Icons.home),\\n label: \'首页\',\\n ),\\n BottomNavigationBarItem(\\n icon: Icon(Icons.settings),\\n label: \'设置\',\\n ),\\n ],\\n ),\\n );\\n }\\n}\\n\\n// 抽屉菜单\\nclass AppDrawer extends StatelessWidget {\\n const AppDrawer({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n final themeState = ThemeState.of(context);\\n return Drawer(\\n child: ListView(\\n padding: EdgeInsets.zero,\\n children: [\\n DrawerHeader(\\n decoration: BoxDecoration(\\n color: Theme.of(context).colorScheme.secondary,\\n ),\\n child: const Text(\\n \'菜单\',\\n style: TextStyle(fontSize: 24, color: Colors.white),\\n ),\\n ),\\n SwitchListTile(\\n title: const Text(\'夜间模式\'),\\n value: themeState.isDark,\\n onChanged: (value) => themeState.toggleTheme(),\\n ),\\n ListTile(\\n leading: const Icon(Icons.person),\\n title: const Text(\'用户中心\'),\\n onTap: () {\\n Navigator.pop(context);\\n Navigator.push(\\n context,\\n MaterialPageRoute(builder: (ctx) => const UserProfile()),\\n );\\n },\\n ),\\n ],\\n ),\\n );\\n }\\n}\\n\\n// 页面组件(带状态保持)\\nclass HomePage extends StatefulWidget {\\n const HomePage({super.key});\\n\\n @override\\n State<HomePage> createState() => _HomePageState();\\n}\\n\\nclass _HomePageState extends State<HomePage>\\n with AutomaticKeepAliveClientMixin {\\n int _counter = 0;\\n\\n @override\\n bool get wantKeepAlive => true;\\n\\n @override\\n Widget build(BuildContext context) {\\n super.build(context);\\n return Center(\\n child: Column(\\n mainAxisAlignment: MainAxisAlignment.center,\\n children: [\\n const Text(\'主页计数器\'),\\n Text(\\n \'$_counter\',\\n style: Theme.of(context).textTheme.headlineMedium,\\n ),\\n ElevatedButton(\\n onPressed: () => setState(() => _counter++),\\n child: const Text(\'增加\'),\\n ),\\n ],\\n ),\\n );\\n }\\n}\\n\\nclass SettingsPage extends StatefulWidget {\\n const SettingsPage({super.key});\\n\\n @override\\n State<SettingsPage> createState() => _SettingsPageState();\\n}\\n\\nclass _SettingsPageState extends State<SettingsPage>\\n with AutomaticKeepAliveClientMixin {\\n @override\\n bool get wantKeepAlive => true;\\n\\n @override\\n Widget build(BuildContext context) {\\n super.build(context);\\n return const Center(\\n child: Text(\'设置页面\'),\\n );\\n }\\n}\\n\\n// 二级页面\\nclass UserProfile extends StatelessWidget {\\n const UserProfile({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: const Text(\'用户资料\'),\\n ),\\n body: Center(\\n child: Text(\\n \'用户信息\',\\n style: Theme.of(context).textTheme.headlineSmall,\\n ),\\n ),\\n );\\n }\\n}\\n
\\n代码要点解析:
\\n1、主题管理架构:
\\nInheritedWidget
实现全局主题状态共享。ThemeState
类封装主题切换逻辑。ThemeState.of(context)
获取主题状态。2、状态保持技巧:
\\nIndexedStack
保持页面状态。AutomaticKeepAliveClientMixin
实现页面状态持久化。PageStorageKey
维护滚动位置。3、导航系统:
\\n主页面切换
。跨页面导航
。二级页面的跳转方式
。4、纯 Dart
实现:
Flutter
原生 API
。该示例实现方案完全使用 Flutter
原生功能,能够帮助开发者深入理解 原生的状态管理机制和组件生命周期管理。
Scaffold
的精髓在于将复杂的页面布局
抽象为标准模块的智能组合
。就像优秀的室内设计师需要理解空间结构的基础框架,我们掌握Scaffold
需要建立三层认知:基础层(各区域的独立配置
)、联动层(组件间的状态协同
)、扩展层(自定义布局覆盖
)。
建议通过\\"标准配置→功能叠加→个性化改造\\"
的渐进路径,逐步从基础页面过渡到复杂场景。好的界面设计如同精心布置的家居空间 —— 既遵循人体工学(设计规范),又体现个性风格(产品特色),最终在功能与美学
之间找到完美平衡。
\\n","description":"前言 Scaffold —— 界面设计的脚手架\\n\\nScaffold就像移动应用的\\"房间布局图\\",它决定了页面的基本骨架结构。想象布置一个客厅时,Scaffold就是空间规划师:顶部挂画的位置(AppBar)、沙发摆放区域(Body)、墙角的收纳柜(Drawer)、茶几上的台灯(FloatingActionButton)。\\n\\n该组件通过预设的布局模块,让开发者像搭积木一样快速构建标准页面,同时保留充分的定制空间。理解Scaffold的运作机制,是掌握Flutter界面开发的关键第一步。\\n\\n操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。\\n\\n一、基础认知…","guid":"https://juejin.cn/post/7478507102039474228","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-06T09:16:40.192Z","media":null,"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"解锁Flutter Dart:变量与基本数据类型的深度剖析","url":"https://juejin.cn/post/7478255110020644914","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
在移动应用开发的广阔领域中,Flutter 以其卓越的性能、简洁的开发方式和强大的跨平台能力脱颖而出,成为众多开发者的首选框架。而 Dart 作为 Flutter 的核心编程语言,犹如基石之于高楼,对 Flutter 应用的开发起着至关重要的作用。
\\nDart 语言专为高效构建跨平台应用而设计,它具备简洁的语法、强大的功能以及出色的性能表现。在 Flutter 开发中,Dart 负责驱动整个应用逻辑,从界面的构建、交互的处理,到数据的获取与管理,无一不是 Dart 的用武之地。它不仅能够充分发挥 Flutter 框架的优势,实现流畅的用户界面和高效的应用性能,还为开发者提供了丰富的工具和库,极大地提高了开发效率。
\\n变量和基本数据类型作为 Dart 语言的基础组成部分,是开发者编写代码的基石。变量是存储数据的容器,通过它可以在程序中灵活地操作和管理数据。而基本数据类型则定义了变量能够存储的数据种类,不同的数据类型具有各自独特的特性和使用规则。在实际开发中,对变量和基本数据类型的准确理解和熟练运用,直接关系到代码的质量、可读性和可维护性。例如,在构建一个电商应用时,需要使用不同的数据类型来存储商品的价格(double 类型)、库存数量(int 类型)、商品名称(String 类型)以及商品是否上架的状态(bool 类型)等信息,合理地选择和使用这些数据类型,能够确保程序准确无误地运行,并高效地处理各种业务逻辑。因此,深入学习 Dart 的变量与基本数据类型,是踏入 Flutter 开发世界的关键一步,为后续掌握更复杂的编程概念和技术奠定坚实的基础。
\\n在 Dart 中,声明变量有多种方式,每种方式都有其独特的用途和特点。
\\nvar message;\\nmessage = \'Hello, Dart!\'; // 此时message被推断为String类型\\nmessage = 123; // 可以重新赋值为其他类型\\n
\\n但如果在声明时进行了初始化,Dart 会根据初始化的值推断变量的类型,之后该变量就只能被赋予相同类型的值。如下方代码,number变量在声明时被初始化为整数类型,后续若再赋值为其他类型(如字符串),就会导致编译错误。
\\nvar number = 10;\\n// number = \'ten\'; // 这行代码会报错,因为类型不匹配\\n
\\nint count = 5;\\ndouble price = 9.99;\\nString name = \'John\';\\nbool isAvailable = true;\\n
\\n在这种方式下,变量只能接受指定类型的值,有助于在开发过程中尽早发现类型相关的错误。
\\nObject obj = \'initial value\';\\nobj = 42; // 可以重新赋值为不同类型\\n// print(obj.length); // 这行代码会报错,因为Object类没有length属性\\n
\\ndynamic关键字声明的变量同样可以存储任何类型的值,并且在编译阶段不会进行类型检查,这意味着可以调用任何方法或访问任何属性,即使该方法或属性实际上并不存在于变量的运行时类型中,这可能会导致运行时错误。
\\ndynamic data = \'Hello\';\\ndata = 100;\\ndata.someMethod(); // 编译时不会报错,但运行时会出错,因为String和int类型都没有someMethod方法\\n
\\n变量的赋值是将一个值存储到变量所代表的内存空间中。在 Dart 中,赋值操作使用等号(=)。例如:
\\nint num1 = 5;\\ndouble num2 = 3.14;\\nString str = \'Hello\';\\n
\\n当对已声明的变量进行更新时,需要注意类型兼容性。如果变量的类型已经确定,那么只能将兼容类型的值赋给它。例如:
\\nint count = 10;\\n// count = 3.14; // 这行代码会报错,因为double类型不能赋值给int类型\\ncount = 15; // 正确,将int类型的值赋给int类型的变量\\n
\\n对于var声明且未初始化的变量,或者dynamic和Object声明的变量,可以更灵活地进行赋值和更新,因为它们的类型在运行时才确定或不进行严格的类型检查。
\\nvar value;\\nvalue = \'first value\';\\nvalue = 20; // 可以,因为var声明的未初始化变量类型是动态的\\ndynamic dynamicValue = \'initial\';\\ndynamicValue = 4.5; // 可以,dynamic类型在编译时不检查类型\\n
\\n变量的作用域决定了变量在程序中的可见性和可访问性。在 Dart 中,主要有以下几种作用域:
\\nint globalVar = 100;\\nvoid main() {\\n print(globalVar); // 可以访问全局变量\\n anotherFunction();\\n}\\nvoid anotherFunction() {\\n print(globalVar); // 在其他函数中也可以访问全局变量\\n}\\n
\\nvoid main() {\\n int localVar = 20;\\n print(localVar); // 可以访问局部变量\\n}\\n// print(localVar); // 这行代码会报错,因为localVar超出了作用域\\n
\\nclass MyClass {\\n int instanceVar; // 实例变量\\n static int staticVar = 5; // 静态变量\\n void instanceMethod() {\\n instanceVar = 10; // 可以在实例方法中访问和修改实例变量\\n print(instanceVar);\\n print(staticVar); // 也可以在实例方法中访问静态变量\\n }\\n static void staticMethod() {\\n // instanceVar = 15; // 这行代码会报错,静态方法中不能直接访问实例变量\\n staticVar = 20; // 可以在静态方法中访问和修改静态变量\\n print(staticVar);\\n }\\n}\\nvoid main() {\\n MyClass myObject = MyClass();\\n myObject.instanceMethod();\\n MyClass.staticMethod();\\n}\\n
\\nfinal和const关键字都用于声明不可变的变量,但它们之间存在一些重要的区别。
\\nimport \'dart:math\';\\nvoid main() {\\n final randomNumber = Random().nextInt(100); // 运行时生成随机数并赋值给final变量\\n print(randomNumber);\\n // randomNumber = 50; // 这行代码会报错,final变量不能被重新赋值\\n}\\n
\\n在类中,final修饰的实例变量必须在构造函数中初始化。
\\nclass Person {\\n final String name;\\n final int age;\\n Person(this.name, this.age);\\n}\\n
\\nconst pi = 3.14159;\\nconst square = 2 * 2;\\n
\\n多个相同的const常量在内存中共享同一个实例,而final变量即使值相同,在内存中也是不同的实例。
\\nconst list1 = [1, 2, 3];\\nconst list2 = [1, 2, 3];\\nprint(identical(list1, list2)); // 输出true,说明list1和list2共享同一实例\\nfinal list3 = [1, 2, 3];\\nfinal list4 = [1, 2, 3];\\nprint(identical(list3, list4)); // 输出false,说明list3和list4是不同的实例\\n
\\nlate关键字用于延迟初始化变量,主要有以下两种使用场景:
\\nclass MyData {\\n late String message;\\n void setMessage(String value) {\\n message = value;\\n }\\n void printMessage() {\\n print(message);\\n }\\n}\\nvoid main() {\\n MyData data = MyData();\\n data.setMessage(\'Hello, late initialized!\');\\n data.printMessage();\\n}\\n
\\n在上述代码中,message变量使用late声明,允许在后续的方法中进行初始化。
\\nclass HeavyResource {\\n HeavyResource() {\\n print(\'Initializing HeavyResource...\');\\n }\\n}\\nclass MyClass {\\n late final HeavyResource resource = HeavyResource();\\n void useResource() {\\n print(\'Using resource: $resource\');\\n }\\n}\\nvoid main() {\\n MyClass myObject = MyClass();\\n // 此时resource尚未初始化\\n // 当调用useResource方法时,resource才会被初始化\\n myObject.useResource();\\n}\\n
\\n需要注意的是,使用late关键字时,必须确保在使用变量之前已经对其进行了初始化,否则会抛出LateInitializationError异常 。
\\n在 Dart 中,int类型用于表示整数,其取值范围在不同平台上有所不同。在 Dart 虚拟机(Dart VM)中,int类型的取值范围与运行机器的位数相关,例如在 64 位平台上,int类型的范围为 -2^63 (-9223372036854775808) 到 2^63-1 (9223372036854775807)。int类型具有以下特性:
\\n以下是声明和使用int类型变量的代码示例:
\\nvoid main() {\\n // 声明并初始化int变量\\n int age = 25;\\n int count = 100;\\n int result;\\n // 进行整数运算\\n result = age + count;\\n print(\'加法运算结果: $result\'); // 输出:加法运算结果: 125\\n result = count - age;\\n print(\'减法运算结果: $result\'); // 输出:减法运算结果: 75\\n result = age * count;\\n print(\'乘法运算结果: $result\'); // 输出:乘法运算结果: 2500\\n result = count ~/ age;\\n print(\'整除运算结果: $result\'); // 输出:整除运算结果: 4\\n result = count % age;\\n print(\'取余运算结果: $result\'); // 输出:取余运算结果: 0\\n}\\n
\\ndouble类型用于表示带有小数部分的浮点数,它遵循 IEEE 754 标准,是 64 位双精度浮点数,其取值范围非常广泛,大约在 -2^1024 到 2^1024 之间。double类型的特性如下:
\\n通过下面的代码示例,可以更直观地了解double类型的声明和运算:
\\nvoid main() {\\n // 声明并初始化double变量\\n double price = 9.99;\\n double pi = 3.14159;\\n double result;\\n // 进行浮点数运算\\n result = price + pi;\\n print(\'加法运算结果: $result\'); // 输出:加法运算结果: 13.13159\\n result = pi - price;\\n print(\'减法运算结果: $result\'); // 输出:减法运算结果: -6.84841\\n result = price * pi;\\n print(\'乘法运算结果: $result\'); // 输出:乘法运算结果: 31.3844841\\n result = price / pi;\\n print(\'除法运算结果: $result\'); // 输出:除法运算结果: 3.1800700219639074\\n // 注意浮点数运算的精度问题\\n double num1 = 0.1;\\n double num2 = 0.2;\\n print(\'0.1 + 0.2 的结果: ${num1 + num2}\'); // 输出:0.1 + 0.2 的结果: 0.30000000000000004\\n}\\n
\\nnum类型是int和double的父类,这意味着num类型的变量既可以存储整数,也可以存储浮点数。当使用num声明变量时,Dart 会根据赋值自动推断其具体类型,这体现了num类型的灵活性。例如:
\\nvoid main() {\\n num number1 = 10; // 此时number1被推断为int类型\\n num number2 = 3.14; // 此时number2被推断为double类型\\n print(\'number1的类型: ${number1.runtimeType}\'); // 输出:number1的类型: int\\n print(\'number2的类型: ${number2.runtimeType}\'); // 输出:number2的类型: double\\n // num类型变量可以进行基本的数学运算\\n num result1 = number1 + number2;\\n print(\'运算结果: $result1\'); // 输出:运算结果: 13.14\\n // num类型变量也可以重新赋值为不同类型\\n number1 = 5.5;\\n print(\'重新赋值后number1的类型: ${number1.runtimeType}\'); // 输出:重新赋值后number1的类型: double\\n}\\n
\\n在实际开发中,num类型通常用于需要处理不同数值类型的通用场景,或者在类型不确定的情况下使用。但需要注意的是,由于num类型的灵活性,在进行类型相关的操作时,可能需要进行额外的类型检查,以确保程序的正确性。例如,可以使用is关键字来检查num类型变量的实际类型:
\\nvoid main() {\\n num value = 10;\\n if (value is int) {\\n print(\'这是一个整数\');\\n } else if (value is double) {\\n print(\'这是一个浮点数\');\\n }\\n}\\n
\\n在 Dart 中,字符串是一系列 UTF - 16 代码单元,用于表示文本数据。声明字符串有以下几种方式:
\\nString singleQuotedString = \'This is a single - quoted string.\';\\n
\\nString doubleQuotedString = \\"This is a double - quoted string.\\";\\n
\\nString multiLineString1 = \'\'\'\\nThis is a multi - line\\nstring created with three single quotes.\\n\'\'\';\\nString multiLineString2 = \\"\\"\\"\\nThis is another multi - line\\nstring created with three double quotes.\\n\\"\\"\\";\\n
\\nDart 提供了多种字符串拼接的方法,以满足不同的需求:
\\nString str1 = \'Hello\';\\nString str2 = \'World\';\\nString result1 = str1 + \', \' + str2;\\nprint(result1); // 输出:Hello, World\\n
\\nString result2 = \'Hello\' \', \' \'World\';\\nprint(result2); // 输出:Hello, World\\n
\\n\\\\){表达式}的形式,可以将表达式的结果插入到字符串中,表达式可以是变量、函数调用等。如果表达式是一个简单的变量,可以省略{},直接使用$ 变量名
,例如:int count = 5;\\nString message = \'There are ${count} apples.\';\\nprint(message); // 输出:There are 5 apples.\\nString name = \'John\';\\nString greeting = \'Hello, $name!\';\\nprint(greeting); // 输出:Hello, John!\\n
\\nDart 的String类提供了丰富的方法,用于处理字符串,以下是一些常见方法的示例:
\\nString str = \'Flutter is awesome\';\\nprint(\'字符串长度: ${str.length}\'); // 输出:字符串长度: 16\\n
\\nString lowerCaseStr = \'hello\';\\nString upperCaseStr = lowerCaseStr.toUpperCase();\\nprint(upperCaseStr); // 输出:HELLO\\n
\\nString upperCaseStr = \'WORLD\';\\nString lowerCaseStr = upperCaseStr.toLowerCase();\\nprint(lowerCaseStr); // 输出:world\\n
\\nString str = \'Dart programming\';\\nbool containsResult = str.contains(\'programming\');\\nprint(containsResult); // 输出:true\\n
\\nString str = \'Hello, World\';\\nbool startsWithResult = str.startsWith(\'Hello\');\\nprint(startsWithResult); // 输出:true\\n
\\nString str = \'Hello, World\';\\nbool endsWithResult = str.endsWith(\'World\');\\nprint(endsWithResult); // 输出:true\\n
\\nString str = \'apple,banana,orange\';\\nList<String> fruits = str.split(\',\');\\nprint(fruits); // 输出:[apple, banana, orange]\\n
\\nString str = \'I like apples\';\\nString newStr = str.replaceAll(\'apples\', \'bananas\');\\nprint(newStr); // 输出:I like bananas\\n
\\n布尔类型(bool)在 Dart 中只有两个值:true和false,用于表示逻辑上的真和假。布尔类型在条件判断、循环控制等场景中起着至关重要的作用。在 Dart 中,布尔类型是强类型检查的,只有bool类型的值true才被认为是真,只有false才被认为是假,不存在其他类型的值会被自动转换为布尔值的情况。例如,在条件语句中,必须使用明确的bool类型值来控制程序的流程:
\\nvoid main() {\\n bool isSunny = true;\\n bool isRaining = false;\\n if (isSunny) {\\n print(\'It\'s a sunny day.\');\\n } else {\\n print(\'It\'s not a sunny day.\');\\n }\\n if (!isRaining) {\\n print(\'It\'s not raining.\');\\n } else {\\n print(\'It\'s raining.\');\\n }\\n}\\n
\\n上述代码中,通过if语句根据isSunny和isRaining的布尔值来输出不同的信息。在条件判断中,!运算符用于对布尔值取反,即!true为false,!false为true。这种严格的布尔类型检查有助于避免在编程中出现逻辑错误,提高代码的可读性和可靠性。
\\n在 Dart 中,List用于存储一组有序的元素,列表中的元素可以是任何数据类型,包括基本数据类型、对象、甚至其他列表。以下是几种常见的声明和初始化列表的方式:
\\n// 存储整数的列表\\nList<int> intList = [1, 2, 3, 4, 5];\\n// 存储字符串的列表\\nList<String> stringList = [\'apple\', \'banana\', \'cherry\'];\\n// 存储不同类型元素的列表(使用dynamic类型)\\nList<dynamic> mixedList = [1, \'two\', 3.0, true];\\n
\\n// 创建一个空的整数列表\\nList<int> emptyIntList = List<int>();\\n// 创建一个指定初始容量为10的字符串列表\\nList<String> stringListWithCapacity = List<String>.filled(10, \'\');\\n
\\n// 创建一个长度为5,每个元素都是0的整数列表\\nList<int> filledIntList = List.filled(5, 0);\\n// 创建一个长度为3,每个元素都是\'unknown\'的字符串列表\\nList<String> filledStringList = List.filled(3, \'unknown\');\\n
\\nDart 的List类提供了丰富的方法来操作列表中的元素,以下是一些常见的操作示例:
\\nList<int> numbers = [1, 2, 3];\\nnumbers.add(4);\\nprint(numbers); // 输出:[1, 2, 3, 4]\\nList<int> moreNumbers = [5, 6];\\nnumbers.addAll(moreNumbers);\\nprint(numbers); // 输出:[1, 2, 3, 4, 5, 6]\\n
\\nList<String> fruits = [\'apple\', \'banana\', \'cherry\'];\\nfruits.remove(\'banana\');\\nprint(fruits); // 输出:[apple, cherry]\\nfruits.removeAt(1);\\nprint(fruits); // 输出:[apple]\\n
\\nList<int> numbers = [1, 2, 3];\\nnumbers[1] = 10;\\nprint(numbers); // 输出:[1, 10, 3]\\n
\\nList<int> numbers = [1, 2, 3, 4, 5];\\n// 使用for循环遍历\\nfor (int i = 0; i < numbers.length; i++) {\\n print(numbers[i]);\\n}\\n// 使用for - in循环遍历\\nfor (int number in numbers) {\\n print(number);\\n}\\n// 使用forEach()方法遍历\\nnumbers.forEach((number) {\\n print(number);\\n});\\n
\\n在 Dart 中,Map是一种键值对的集合,每个键都是唯一的,通过键可以快速访问对应的值。Map中的键和值可以是任何数据类型。以下是声明和初始化Map的常见方式:
\\n// 键为字符串,值为整数的Map\\nMap<String, int> ageMap = {\'Alice\': 25, \'Bob\': 30, \'Charlie\': 35};\\n// 键为整数,值为字符串的Map\\nMap<int, String> idMap = {1: \'Alice\', 2: \'Bob\', 3: \'Charlie\'};\\n
\\n// 创建一个空的Map,键和值类型为动态\\nMap<dynamic, dynamic> emptyMap = Map<dynamic, dynamic>();\\n
\\nDart 的Map类提供了丰富的方法来操作键值对,以下是一些常见的操作示例:
\\nMap<String, int> ageMap = {\'Alice\': 25};\\nageMap[\'Bob\'] = 30;\\nprint(ageMap); // 输出:{Alice: 25, Bob: 30}\\nageMap[\'Alice\'] = 26; // 更新Alice的年龄\\nprint(ageMap); // 输出\\n## 四、类型转换与断言\\n### 4.1 类型转换\\n在Dart开发中,经常需要在不同的数据类型之间进行转换,以满足各种业务逻辑的需求。下面介绍几种常见的数据类型转换方法,并结合代码示例进行展示。\\n#### 4.1.1 数值与字符串之间的转换\\n- **字符串转换为数值**:Dart提供了`int.parse()`和`double.parse()`方法,用于将字符串转换为对应的数值类型。例如:\\n```dart\\nString strInt = \'123\';\\nint numInt = int.parse(strInt);\\nprint(numInt is int); // 输出:true\\nString strDouble = \'3.14\';\\ndouble numDouble = double.parse(strDouble);\\nprint(numDouble is double); // 输出:true\\n
\\n需要注意的是,如果字符串无法解析为有效的数值,会抛出FormatException异常。为了避免这种情况,可以使用int.tryParse()和double.tryParse()方法,它们在解析失败时会返回null,而不是抛出异常。例如:
\\nString invalidStr = \'abc\';\\nint? num = int.tryParse(invalidStr);\\nprint(num); // 输出:null\\n
\\nint number = 100;\\nString str = number.toString();\\nprint(str is String); // 输出:true\\ndouble pi = 3.14159;\\nString piStr = pi.toString();\\nprint(piStr is String); // 输出:true\\n
\\n对于double类型,还可以使用toStringAsFixed()方法指定小数的位数,使用toStringAsPrecision()方法指定有效数字的位数。例如:
\\ndouble num = 3.14159;\\nString fixedStr = num.toStringAsFixed(2); // 保留两位小数\\nprint(fixedStr); // 输出:3.14\\nString precisionStr = num.toStringAsPrecision(3); // 保留三位有效数字\\nprint(precisionStr); // 输出:3.14\\n
\\nSet<int> numberSet = {1, 2, 3, 4, 5};\\nList<int> numberList = List.from(numberSet);\\nprint(numberList); // 输出:[1, 2, 3, 4, 5]\\n
\\nList<String> strList = [\'1\', \'2\', \'3\'];\\nList<int> intList = strList.map(int.parse).toList();\\nprint(intList); // 输出:[1, 2, 3]\\n
\\nList<List<dynamic>> keyValuePairs = [[\'name\', \'Alice\'], [\'age\', 25]];\\nMap<String, dynamic> personMap = Map.fromIterables(keyValuePairs.map((e) => e[0]), keyValuePairs.map((e) => e[1]));\\nprint(personMap); // 输出:{name: Alice, age: 25}\\n
\\nMap<String, dynamic> originalMap = {\'2024-10-01\': \'Value 1\', \'2024-10-02\': \'Value 2\'};\\nMap<DateTime, dynamic> convertedMap = {};\\noriginalMap.forEach((key, value) {\\n DateTime newKey = DateTime.parse(key);\\n convertedMap[newKey] = value;\\n});\\nprint(convertedMap);\\n
\\n断言(assert)是 Dart 中用于在开发过程中检查条件是否为真的一种机制,它在调试和测试阶段非常有用,可以帮助开发者快速发现代码中的潜在问题。断言的基本语法是assert(condition, optionalMessage),其中condition是一个布尔表达式,optionalMessage是一个可选的字符串,用于在断言失败时提供更多的错误信息。当condition为false时,断言失败,程序会抛出一个AssertionError异常,并显示optionalMessage(如果提供了的话),程序会立即停止执行;当condition为true时,断言通过,程序继续正常执行。
\\n以下是一个使用断言的简单示例:
\\nvoid calculateArea(int length, int width) {\\n assert(length > 0, \'长度必须大于0\');\\n assert(width > 0, \'宽度必须大于0\');\\n int area = length * width;\\n print(\'面积为: $area\');\\n}\\nvoid main() {\\n calculateArea(5, 3); // 正常情况,断言通过\\n calculateArea(0, 3); // 断言失败,抛出AssertionError异常,显示错误信息\'长度必须大于0\'\\n}\\n
\\n在上述示例中,calculateArea函数用于计算矩形的面积,通过断言来确保传入的长度和宽度都大于 0。如果传入的参数不满足条件,断言会失败,程序会立即停止并提示错误信息,这样可以在开发阶段尽早发现错误,避免在后续的复杂计算中出现难以调试的问题。
\\n需要注意的是,在生产环境中,断言默认是禁用的,因为断言主要用于开发和调试,不应该影响生产环境下程序的性能和行为。如果希望在生产环境中也启用断言,可以在运行程序时使用--enable-asserts标志,但这通常不是推荐的做法,因为它可能会影响程序的性能,并且断言中的错误信息可能会暴露给用户。因此,在实际开发中,应该合理使用断言,将其作为开发和调试的工具,而不是用于处理生产环境中的异常情况。
\\nDart 变量和基本数据类型是 Flutter 开发的基石,它们为构建高效、可靠的应用程序提供了坚实的基础。通过深入学习变量的声明、赋值、作用域以及final、const、late等关键字的使用,我们能够更加灵活、准确地管理数据。而熟练掌握数值类型(int、double、num)、字符串类型(String)、布尔类型(bool)、列表类型(List)和映射类型(Map)等基本数据类型,以及它们之间的类型转换和断言机制,能够帮助我们处理各种复杂的业务逻辑,确保程序的正确性和稳定性。
\\n在 Flutter 开发中,对变量和基本数据类型的深入理解和运用,直接关系到应用的性能、可读性和可维护性。无论是简单的界面交互,还是复杂的数据处理,都离不开这些基础知识的支撑。因此,鼓励读者在实际开发中不断实践,加深对 Dart 变量和基本数据类型的理解,探索更多的应用场景和技巧,从而提升自己的 Flutter 开发能力,创造出更加优秀的移动应用。
","description":"一、引言 在移动应用开发的广阔领域中,Flutter 以其卓越的性能、简洁的开发方式和强大的跨平台能力脱颖而出,成为众多开发者的首选框架。而 Dart 作为 Flutter 的核心编程语言,犹如基石之于高楼,对 Flutter 应用的开发起着至关重要的作用。\\n\\nDart 语言专为高效构建跨平台应用而设计,它具备简洁的语法、强大的功能以及出色的性能表现。在 Flutter 开发中,Dart 负责驱动整个应用逻辑,从界面的构建、交互的处理,到数据的获取与管理,无一不是 Dart 的用武之地。它不仅能够充分发挥 Flutter 框架的优势…","guid":"https://juejin.cn/post/7478255110020644914","author":"顾林海","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-06T03:53:10.976Z","media":null,"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter性能优化细节","url":"https://juejin.cn/post/7478246730586898466","content":"使用const
构造函数\\n对静态Widget使用const
,减少重复构建:
const Text(\'Hello World\'), // ✅ 编译时即确定,不会重复创建\\nText(\'Hello World\'), // ❌ 每次build都会新建实例\\n
\\n分离动画与子组件\\n使用AnimatedBuilder
避免动画导致整个子树重建:
AnimatedBuilder(\\n animation: _animation,\\n builder: (context, child) => Transform.rotate(\\n angle: _animation.value,\\n child: child, // ✅ 复用child,不重复构建\\n ),\\n child: const HeavyWidget(), // 静态子组件\\n)\\n
\\n用RepaintBoundary
包裹频繁重绘的组件(如游戏角色):
RepaintBoundary(\\n child: CustomPaint(painter: MyDynamicPainter()),\\n)\\n
\\n// ❌ 错误:每次build都新建\\nWidget build() {\\n final logger = Logger();\\n return ...;\\n}\\n\\n// ✅ 正确:提前创建或使用const\\nstatic const _logger = Logger();\\nWidget build() => ...;\\n
\\n动态高度布局导致重复计算
\\n优化前:\\nColumn(\\n children: [\\n const HeaderWidget(),\\n ListView( // ❌ ListView在Column中会引发布局冲突\\n children: items.map((e) => ItemWidget(e)).toList(),\\n ),\\n ],\\n)\\n\\n优化后:\\nColumn(\\n children: [\\n const HeaderWidget(),\\n Expanded( // ✅ 使用Expanded约束ListView高度\\n child: ListView.builder(\\n itemCount: items.length,\\n itemBuilder: (ctx, i) => ItemWidget(items[i]),\\n ),\\n ),\\n ],\\n)\\n
\\n复杂页面导致单帧渲染时间过长
\\n优化前:\\nWidget build(BuildContext context) {\\n return Scaffold(\\n body: Column(\\n children: [\\n // 100行嵌套布局...\\n ],\\n ),\\n );\\n}\\n\\n优化后:\\nWidget build(BuildContext context) {\\n return Scaffold(\\n body: Column(\\n children: [\\n const HeaderSection(), // 拆分为独立组件\\n const _ContentSection(), // 使用private组件\\n _buildFooter(), // 提取方法\\n ],\\n ),\\n );\\n}\\n\\n// 拆分成独立的组件或方法\\nWidget _buildFooter() => ... ;\\n\\n
\\nOpacity
使用优化前\\nOpacity(\\n opacity: 0.5,\\n child: ComplexWidgetTree(), // ❌ 整个子树都会参与混合计算\\n)\\n\\n优化后:\\nContainer(\\n color: Colors.black.withOpacity(0.5), // ✅ 仅背景透明\\n child: ComplexWidgetTree(),\\n)\\n
\\nShaderMask
替代复杂遮罩ShaderMask(\\n blendMode: BlendMode.modulate,\\n shaderCallback: (Rect bounds) => LinearGradient(\\n colors: [Colors.red, Colors.blue],\\n ).createShader(bounds),\\n child: Image.network(\'...\'),\\n)\\n
\\n使用AnimatedWidget
替代setState
优化前:\\n\\nAnimationController _controller;\\nWidget build() {\\n return Transform.rotate(\\n angle: _controller.value,\\n child: Button(\\n onPressed: () => setState(() {}), // ❌ 触发整个页面重建\\n ),\\n );\\n}\\n\\n\\n优化后:\\n\\nclass _RotatingButton extends AnimatedWidget {\\n const _RotatingButton({required Animation<double> animation})\\n : super(listenable: animation);\\n\\n @override\\n Widget build(BuildContext context) {\\n final animation = listenable as Animation<double>;\\n return Transform.rotate(\\n angle: animation.value,\\n child: const Button(),\\n );\\n }\\n}\\n\\n
\\n长列表必须使用ListView.builder
,避免一次性构建所有子项:
ListView.builder(\\n itemCount: 1000,\\n itemBuilder: (ctx, i) => ListTile(title: Text(\'Item $i\')),\\n)\\n
\\n使用AutomaticKeepAliveClientMixin
保持Tab页状态:
class _TabPageState extends State<TabPage> with AutomaticKeepAliveClientMixin {\\n @override\\n bool get wantKeepAlive => true; // ✅ 切换Tab不重新加载\\n // build方法...\\n}\\n
\\n明确设置itemExtent
提升滚动流畅度:
ListView.builder(\\n itemExtent: 80, // 每个列表项高度固定为80\\n // ...\\n)\\n
\\nSliver
实现高性能复杂列表CustomScrollView(\\n slivers: [\\n SliverAppBar(...), // 可折叠的AppBar\\n SliverPersistentHeader(...), // 固定Header\\n SliverList(\\n delegate: SliverChildBuilderDelegate(\\n (ctx, i) => ListItem(data[i]),\\n childCount: data.length,\\n ),\\n ),\\n SliverGrid(...), // 混合网格布局\\n ],\\n)\\n
\\n使用cached_network_image
缓存网络图片:
CachedNetworkImage(\\n imageUrl: \'https://example.com/image.jpg\',\\n placeholder: (ctx, url) => CircularProgressIndicator(),\\n)\\n
\\n加载本地图片时指定尺寸:
\\nImage.asset(\\n \'assets/large_image.png\',\\n width: 200,\\n height: 200, // ✅ 避免解码原始大图\\n)\\n
\\n在dispose()
中释放控制器、监听器
late final ScrollController _controller;\\n@override\\nvoid dispose() {\\n _controller.dispose(); // ✅ 防止内存泄漏\\n super.dispose();\\n}\\n
\\n将非必要插件延迟到首帧后加载:
\\nvoid main() async {\\n WidgetsFlutterBinding.ensureInitialized();\\n runApp(MyApp());\\n // 首帧渲染后初始化\\n await Future.delayed(Duration.zero);\\n await ThirdPartyPlugin.init();\\n}\\n
\\n// 首页不使用相机功能,延迟加载\\nvoid onProfilePageOpen() async {\\n final cameraPlugin = await CameraPlugin.load();\\n // 使用插件...\\n}\\n
\\n使用compute
函数执行密集计算:
void _processData() async {\\n final result = await compute(heavyCalculation, data);\\n // ...\\n}\\n
\\nProvider中使用Consumer
或Selector
避免全局刷新:
Selector<AppState, String>(\\n selector: (_, state) => state.username,\\n builder: (_, username, __) => Text(username),\\n)\\n
\\n使用rxdart
控制频繁触发的事件:
searchInput.onTextChanged\\n .debounceTime(Duration(milliseconds: 500)) // 500ms内只取最后一次\\n .listen((text) => fetchData(text));\\n
\\nvoid main() {\\n debugProfileBuildsEnabled = true; // 启用构建分析\\n debugProfilePaintsEnabled = true; // 查看重绘区域\\n runApp(MyApp());\\n}\\n
\\nvoid main() {\\n runApp(SplashScreen()); // 极简启动屏\\n \\n Future.wait([\\n _warmupEngine(),\\n _preloadCriticalData(),\\n ]).then((_) => _enterMainApp());\\n}\\n\\nFuture<void> _warmupEngine() async {\\n // 后台初始化非必要引擎模块\\n await Firebase.initializeApp();\\n await Hive.initFlutter();\\n}\\n
\\nDart Wasm
预编译# pubspec.yaml\\nflutter:\\n module:\\n web:\\n wasm: true\\n
\\nflutter build web --wasm # 生成WebAssembly版本\\n
\\nImpeller
渲染引擎flutter run --enable-impeller # 启用下一代渲染引擎\\n
","description":"一、渲染性能优化 1、减少Widget重建\\n\\n使用const构造函数 对静态Widget使用const,减少重复构建:\\n\\nconst Text(\'Hello World\'), // ✅ 编译时即确定,不会重复创建\\nText(\'Hello World\'), // ❌ 每次build都会新建实例\\n\\n\\n分离动画与子组件 使用AnimatedBuilder避免动画导致整个子树重建:\\n\\nAnimatedBuilder(\\n animation: _animation,\\n builder: (context, child) => Transform.rota…","guid":"https://juejin.cn/post/7478246730586898466","author":"SunshineBrother","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-06T03:52:29.512Z","media":null,"categories":["iOS","Flutter","Android"],"attachments":null,"extra":null,"language":null},{"title":"系统化掌握Flutter组件之AppBar(一):筑基之旅","url":"https://juejin.cn/post/7478288498673270799","content":"在Flutter
界面设计中,导航栏如同城市的路标系统
,直接影响用户的操作体验与产品专业度。其精妙的设计思想:从动态响应式布局到平台风格适配,从基础文本显示到复杂交互动画,它承载着界面控制
、状态管理
、用户引导
等十余项关键职责。
flexibleSpace
与flexibleHeight
的区别?SliverAppBar
实现视差滚动效果?iOS
和Android
上表现不同?本文将带你以系统工程的视角,逐层拆解AppBar
核心属性,通过亲手编码实现从标准配置到企业级定制的跨越,揭开这个\\"最熟悉的陌生人\\"
的技术本质。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\nAppBar
官方图示:
属性详解
及分类列表
属性详解:
\\nAppBar({\\n super.key, // 组件唯一标识\\n this.leading, // 左侧组件(如返回按钮或抽屉菜单图标)\\n this.automaticallyImplyLeading = true, // 是否自动生成leading(当leading为null时)\\n this.title, // 中间标题组件\\n this.actions, // 右侧操作按钮列表\\n this.flexibleSpace, // 在工具栏和底部之间堆叠的灵活空间\\n this.bottom, // 底部组件(如TabBar)\\n this.elevation, // 阴影高度(Material Design Z轴值)\\n this.scrolledUnderElevation, // 当内容滚动到AppBar下方时的阴影高度(Material 3)\\n this.notificationPredicate = defaultScrollNotificationPredicate, // 过滤滚动通知的条件\\n this.shadowColor, // 阴影颜色\\n this.surfaceTintColor, // Material 3中表面着色效果颜色\\n this.shape, // 自定义形状(如圆角边框)\\n this.backgroundColor, // 背景颜色(覆盖ThemeData.primaryColor)\\n this.foregroundColor, // 前景色(图标/文字颜色,覆盖ThemeData.primaryTextTheme)\\n this.iconTheme, // 图标主题(颜色/大小/透明度)\\n this.actionsIconTheme, // 右侧操作按钮的独立图标主题\\n this.primary = true, // 是否延伸到状态栏下方\\n this.centerTitle, // 标题是否居中(null时根据平台自动判断)\\n this.excludeHeaderSemantics = false, // 是否排除标题语义(无障碍功能相关)\\n this.titleSpacing, // 标题与两侧组件的间距\\n this.toolbarOpacity = 1.0, // 工具栏部分的透明度(0.0-1.0)\\n this.bottomOpacity = 1.0, // 底部组件的透明度(0.0-1.0)\\n this.toolbarHeight, // 工具栏高度(覆盖默认56.0)\\n this.leadingWidth, // leading组件的宽度(覆盖默认56.0)\\n this.toolbarTextStyle, // 工具栏文本样式(如actions中的文本)\\n this.titleTextStyle, // 标题文本样式(覆盖ThemeData.textTheme.titleLarge)\\n this.systemOverlayStyle, // 系统状态栏/导航栏样式(颜色/亮度)\\n this.forceMaterialTransparency = false, // 强制启用材质透明效果(Material 3)\\n this.clipBehavior, // 内容裁剪方式(如Clip.none, Clip.antiAlias)\\n })\\n
\\n分类列表:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n类别 | 属性名称 | 属性类型 | 作用描述 | 默认值 |
---|---|---|---|---|
布局与内容 | leading | Widget? | 左侧组件(如返回按钮或抽屉菜单图标) | null |
title | Widget? | 中间标题组件 | null | |
actions | List<Widget>? | 右侧操作按钮列表 | null | |
flexibleSpace | Widget? | 在工具栏和底部之间堆叠的灵活空间(如 CollapsingToolbar ) | null | |
bottom | PreferredSizeWidget? | 底部组件(如 TabBar ) | null | |
clipBehavior | Clip? | 内容裁剪方式(如 Clip.none 或 Clip.antiAlias ) | Clip.none | |
样式与外观 | backgroundColor | Color? | 背景颜色(覆盖 ThemeData.primaryColor ) | ThemeData.primaryColor |
elevation | double? | 静态阴影高度 | 4.0 | |
scrolledUnderElevation | double? | 当内容滚动到 AppBar 下方时的动态阴影高度(Material 3 特性) | null | |
shadowColor | Color? | 阴影颜色 | ThemeData.shadowColor | |
surfaceTintColor | Color? | Material 3 中表面着色效果的颜色 | null | |
shape | ShapeBorder? | 自定义形状(如圆角边框) | null | |
iconTheme | IconThemeData? | 控制图标颜色、大小等属性 | ThemeData.primaryIconTheme | |
actionsIconTheme | IconThemeData? | 右侧操作按钮的独立图标主题 | ThemeData.primaryIconTheme | |
foregroundColor | Color? | 前景色(图标/文字颜色,覆盖 ThemeData.primaryTextTheme ) | null | |
titleTextStyle | TextStyle? | 标题文本样式(覆盖 ThemeData.textTheme.titleLarge ) | ThemeData.textTheme.titleLarge | |
toolbarTextStyle | TextStyle? | 工具栏文本样式(如 actions 中的文本按钮样式) | null | |
systemOverlayStyle | SystemUiOverlayStyle? | 系统状态栏/导航栏样式(颜色和亮度,需配合 AnnotatedRegion 使用) | null | |
forceMaterialTransparency | bool | 强制启用材质透明效果(Material 3 中默认透明,设为 false 禁用) | false | |
交互与行为 | automaticallyImplyLeading | bool | 是否自动生成 leading (当 leading 为 null 时) | true |
notificationPredicate | ScrollNotificationPredicate | 过滤滚动通知的条件(用于触发 scrolledUnderElevation ) | defaultScrollNotificationPredicate | |
excludeHeaderSemantics | bool | 是否排除标题的语义节点(无障碍功能相关) | false | |
位置与间距 | centerTitle | bool? | 标题是否居中(null 时根据平台自动判断) | null (Android 左对齐,iOS 居中) |
titleSpacing | double? | 标题与左右组件的间距 | NavigationToolbar.kMiddleSpacing (16.0) | |
尺寸与约束 | toolbarHeight | double? | 工具栏高度(覆盖默认 kToolbarHeight ) | kToolbarHeight (56.0) |
leadingWidth | double? | 精确控制 leading 组件的宽度(覆盖默认 56.0) | null | |
toolbarOpacity | double | 工具栏部分的透明度(0.0 到 1.0 ) | 1.0 | |
bottomOpacity | double | 底部组件(bottom )的透明度 | 1.0 |
补充说明:
\\n1、Material 3
特性:
scrolledUnderElevation
和 forceMaterialTransparency
需在启用 Material 3
主题时生效。surfaceTintColor
替代旧版的 backgroundColor
着色逻辑。2、平台差异:
\\ncenterTitle
的默认居中行为因平台而异(Android
左对齐,iOS
居中)。systemOverlayStyle
可动态适配亮/暗模式的状态栏图标颜色。3、默认值依赖:
\\niconTheme
、titleTextStyle
)继承自 ThemeData
,实际值可能因主题配置变化。4、无障碍支持:
\\nexcludeHeaderSemantics
用于特殊场景下优化无障碍阅读体验。此表格适用于 Flutter 3.x
及以上版本,具体细节请以官方文档为准。
AppBar
是Material Design
应用的核心导航组件,作为Scaffold
的顶级子组件,它与body
、drawer
等共同构成页面骨架。其典型应用场景如下:
@override\\nWidget build(BuildContext context) {\\n return DefaultTabController(\\n length: 3,\\n child: Scaffold(\\n appBar: AppBar(\\n leading: buildLeading(),\\n title: buildTitle(),\\n actions: buildActions(),\\n flexibleSpace: buildFlexibleSpace(),\\n bottom: buildTabBar(),\\n ),\\n body: buildTabBarView(),\\n ),\\n );\\n}\\n
\\n生命周期与状态管理
\\nAppBar
的状态与所属Scaffold
绑定,当页面切换时自动重建。若需保持状态(如展开的搜索栏
),需结合AutomaticKeepAliveClientMixin
:
class _KeepAliveAppBarState extends State<KeepAliveAppBar> \\n with AutomaticKeepAliveClientMixin {\\n @override\\n bool get wantKeepAlive => true;\\n\\n @override\\n Widget build(BuildContext context) {\\n super.build(context);\\n return AppBar(...);\\n }\\n}\\n
\\nleading
:左侧控制区默认显示返回按钮(当有上级页面时
),可完全自定义:
IconButton buildLeading() {\\n return IconButton(\\n icon: Icon(Icons.menu),\\n onPressed: () => print(\'打开侧边栏\'),\\n );\\n}\\n
\\n典型场景实现
\\n确认弹窗
)。// 自定义返回拦截\\nAppBar(\\n leading: BackButton(\\n onPressed: () async {\\n final confirm = await showExitConfirmDialog(context);\\n if (confirm) Navigator.pop(context);\\n },\\n ),\\n)\\n
\\ntitle
:标题区支持任意Widget
,但推荐使用布局约束组件
:
Row buildTitle() {\\n return Row(\\n children: [\\n FlutterLogo(size: 28),\\n SizedBox(width: 12),\\n Text(\'综合新闻\'),\\n ],\\n );\\n}\\n
\\n响应式布局技巧
\\n通过LayoutBuilder
实现标题自适应:
LayoutBuilder(\\n builder: (context, constraints) {\\n final isWide = constraints.maxWidth > 600;\\n return AppBar(\\n title: isWide \\n ? Text(\'宽屏完整标题\')\\n : Text(\'简版\'),\\n );\\n },\\n)\\n
\\nactions
:右侧操作区建议使用IconButton
组件,注意操作项不宜超过5
个:
List<Widget> buildActions() {\\n return [\\n IconButton(\\n icon: Icon(Icons.notifications_none),\\n onPressed: () => print(\'通知\'),\\n ),\\n PopupMenuButton<String>(\\n itemBuilder: (context) => [\\n PopupMenuItem(value: \'night\', child: Text(\'夜间模式\')),\\n PopupMenuItem(value: \'font\', child: Text(\'字体设置\')),\\n ],\\n ),\\n ];\\n}\\n
\\n操作项状态管理
\\n通过ValueNotifier
实现按钮动态效果:
final isFavorite = ValueNotifier(false);\\n\\nAppBar(\\n actions: [\\n ValueListenableBuilder<bool>(\\n valueListenable: isFavorite,\\n builder: (ctx, value, _) => IconButton(\\n icon: Icon(value ? Icons.star : Icons.star_border),\\n onPressed: () => isFavorite.value = !value,\\n ),\\n ),\\n ],\\n)\\n
\\nFlexibleSpace
:弹性空间功能比较弱小,推荐使用更强大的SliverAppBar
系列实现。
Container buildFlexibleSpace() {\\n return Container(\\n decoration: BoxDecoration(\\n gradient: LinearGradient(\\n colors: [Colors.blue, Colors.purple],\\n ),\\n ),\\n );\\n}\\n
\\nbottom
:底部扩展区常用于集成TabBar
、搜索框
等组件:
TabBar buildTabBar() {\\n return TabBar(\\n tabs: [\\n Tab(text: \'热点\'),\\n Tab(text: \'国际\'),\\n Tab(text: \'科技\'),\\n ],\\n );\\n}\\n\\nTabBarView buildTabBarView() {\\n return TabBarView(\\n children: [\\n Center(child: Text(\'热点内容\')),\\n Center(child: Text(\'国际新闻\')),\\n Center(child: Text(\'科技前沿\')),\\n ],\\n );\\n}\\n
\\nAppBar架构树\\n├── 布局控制层\\n│ ├── toolbarHeight:控制操作栏高度\\n│ ├── flexibleSpace:弹性扩展区域\\n│ └── bottom:底部扩展组件\\n├── 视觉表现层\\n│ ├── backgroundColor:背景颜色/渐变色\\n│ ├── elevation:投影深度\\n│ └── shadowColor:投影颜色\\n└── 交互逻辑层\\n ├── leading:导航控制\\n ├── actions:功能操作\\n └── automaticallyImplyLeading:智能推断逻辑\\n
\\nimport \'package:flutter/material.dart\';\\n\\nvoid main() => runApp(AppBarDemo());\\n\\nclass AppBarDemo extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n debugShowCheckedModeBanner: false,\\n home: DefaultTabController(\\n length: 3,\\n child: Scaffold(\\n appBar: AppBar(\\n title: Row(\\n children: [\\n FlutterLogo(size: 28),\\n SizedBox(width: 12),\\n Text(\'综合新闻\'),\\n ],\\n ),\\n leading: IconButton(\\n icon: Icon(Icons.menu),\\n onPressed: () => print(\'打开侧边栏\'),\\n ),\\n actions: [\\n IconButton(\\n icon: Icon(Icons.notifications_none),\\n onPressed: () => print(\'通知\'),\\n ),\\n PopupMenuButton<String>(\\n itemBuilder: (context) => [\\n PopupMenuItem(value: \'night\', child: Text(\'夜间模式\')),\\n PopupMenuItem(value: \'font\', child: Text(\'字体设置\')),\\n ],\\n ),\\n ],\\n bottom: TabBar(\\n tabs: [\\n Tab(text: \'热点\'),\\n Tab(text: \'国际\'),\\n Tab(text: \'科技\'),\\n ],\\n ),\\n flexibleSpace: Container(\\n decoration: BoxDecoration(\\n gradient: LinearGradient(\\n colors: [Colors.blue, Colors.purple],\\n ),\\n ),\\n ),\\n ),\\n body: TabBarView(\\n children: [\\n Center(child: Text(\'热点内容\')),\\n Center(child: Text(\'国际新闻\')),\\n Center(child: Text(\'科技前沿\')),\\n ],\\n ),\\n ),\\n ),\\n );\\n }\\n}\\n
\\nAppBar
+ 滚动联动需求:滚动列表时,AppBar
背景色从透明渐变到蓝色,标题渐现。
import \'package:flutter/material.dart\';\\n\\nclass DynamicColorAppBar extends StatefulWidget {\\n const DynamicColorAppBar({super.key});\\n\\n @override\\n State<DynamicColorAppBar> createState() => _DynamicColorAppBarState();\\n}\\n\\nclass _DynamicColorAppBarState extends State<DynamicColorAppBar> {\\n final ScrollController _scrollController = ScrollController();\\n double _scrollProgress = 0.0;\\n\\n @override\\n void initState() {\\n super.initState();\\n _scrollController.addListener(() {\\n setState(() {\\n _scrollProgress = (_scrollController.offset / 200).clamp(0.0, 1.0);\\n });\\n });\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n final color = Color.lerp(Colors.transparent, Colors.blue, _scrollProgress)!;\\n return Scaffold(\\n appBar: AppBar(\\n backgroundColor: color,\\n title: Opacity(\\n opacity: _scrollProgress,\\n child: const Text(\'滚动变色标题\'),\\n ),\\n flexibleSpace: Container(\\n decoration: BoxDecoration(\\n gradient: LinearGradient(\\n colors: [Colors.purple, Colors.blue],\\n stops: [0.0, _scrollProgress],\\n begin: Alignment.topLeft,\\n end: Alignment.bottomRight,\\n ),\\n ),\\n ),\\n ),\\n body: ListView.builder(\\n controller: _scrollController,\\n itemCount: 50,\\n itemBuilder: (_, index) => Container(\\n height: 100,\\n color: Colors.primaries[index % 18],\\n alignment: Alignment.center,\\n child: Text(\\"Item $index\\"),\\n ),\\n ),\\n );\\n }\\n}\\n
\\nAppBar
中可展开的搜索栏需求:点击搜索图标时,标题区域动态展开为搜索输入框。
\\nimport \'package:flutter/material.dart\';\\n\\nclass ExpandableSearchAppBar extends StatefulWidget {\\n const ExpandableSearchAppBar({super.key});\\n\\n @override\\n State<ExpandableSearchAppBar> createState() => _ExpandableSearchAppBarState();\\n}\\n\\nclass _ExpandableSearchAppBarState extends State<ExpandableSearchAppBar>\\n with SingleTickerProviderStateMixin {\\n late AnimationController _controller;\\n bool _isExpanded = false;\\n\\n @override\\n void initState() {\\n super.initState();\\n _controller = AnimationController(\\n vsync: this,\\n duration: const Duration(milliseconds: 300),\\n );\\n }\\n\\n void _toggleSearch() {\\n setState(() {\\n _isExpanded = !_isExpanded;\\n _isExpanded ? _controller.forward() : _controller.reverse();\\n });\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: _isExpanded\\n ? AnimatedBuilder(\\n animation: _controller,\\n builder: (_, __) {\\n return SizedBox(\\n width: MediaQuery.of(context).size.width * 0.8,\\n child: TextField(\\n decoration: const InputDecoration(\\n hintText: \'搜索...\',\\n border: InputBorder.none,\\n ),\\n style: const TextStyle(color: Colors.white),\\n ),\\n );\\n },\\n )\\n : const Text(\'点击搜索\'),\\n leading: IconButton(\\n icon: const Icon(Icons.arrow_back),\\n onPressed: () {},\\n ),\\n actions: [\\n IconButton(\\n icon: AnimatedIcon(\\n icon: AnimatedIcons.search_ellipsis,\\n progress: _controller,\\n ),\\n onPressed: _toggleSearch,\\n ),\\n ],\\n backgroundColor: Theme.of(context).colorScheme.inversePrimary,\\n ),\\n body: ListView.builder(\\n itemCount: 50,\\n itemBuilder: (_, index) => Container(\\n height: 100,\\n color: Colors.primaries[index % 18],\\n alignment: Alignment.center,\\n child: Text(\\"Item $index\\"),\\n ),\\n ), // 内容列表\\n );\\n }\\n}\\n
\\n掌握AppBar
需建立三维知识体系:
Material
规范到像素渲染的完整链路。Scaffold
、TabBar
等组件的交互。遵循\\"属性解析→组合实验→场景适配\\"
的学习路径,将标准组件转化为精准的界面控制工具。优秀的AppBar
设计如同交响乐指挥 —— 既要精确控制每个元素的位置,又要统筹整体的和谐美感。
\\n","description":"前言 在Flutter界面设计中,导航栏如同城市的路标系统,直接影响用户的操作体验与产品专业度。其精妙的设计思想:从动态响应式布局到平台风格适配,从基础文本显示到复杂交互动画,它承载着界面控制、状态管理、用户引导等十余项关键职责。\\n\\n你是否真正理解flexibleSpace与flexibleHeight的区别?\\n能否用SliverAppBar实现视差滚动效果?\\n为什么同样的代码在iOS和Android上表现不同?\\n\\n本文将带你以系统工程的视角,逐层拆解AppBar核心属性,通过亲手编码实现从标准配置到企业级定制的跨越,揭开这个\\"最熟悉的陌生人\\"的技术本质…","guid":"https://juejin.cn/post/7478288498673270799","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-06T03:18:26.088Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b103923ab1a542d9b0ca773cfc20feb7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1741835906&x-signature=RpbrQYUG8qNqs4C8g6MoWJXX%2Fkw%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"跟🤡杰哥一起学Flutter (三十二、玩转 Flutter 版本控制💨)","url":"https://juejin.cn/post/7478412386153168930","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
2024.2.29 日 Apple 发布了《关于 App Store 提交的隐私更新》,明确要求 新上架或更新的APP 必须包含 「PrivacyInfo.xcprivacy」 隐私清单文件,否则可能被拒审,部分第三方SDK若以二进制形式集成,还需附带签名。😶 对此组里的 iOS小伙伴 抛出疑问:
\\nFlutter 3.18.0 后才内置了PrivacyInfo.xcprivacy,现在我们的版本是3.7.12,要不要 升级下Flutter版本?不升的话就先按照这次提交改一下:Add xcprivacy privacy manifest to iOS framework
\\n😶 当初搞 Flutter 的一个原因就是 适配鸿蒙,但 openharmony-sig/flutter_flutter 有 flutter 版本限制,所以组里一直没更新,看提交记录,前阵子合并了一个 3.22.0 的分支:
\\n😄 反正早晚要踩坑,还是升级一波吧,「本地 Flutter SDK」切换到特定版本的命令如下:
\\n# 导航到 Flutter SDK\\ncd path/to/flutter_sdk\\n\\n# 拉取最新 Flutter SDK 版本\\ngit pull\\n\\n# 查看可用的 Flutter 版本\\ngit tag\\n\\n# 切换到特定版本,如3.22.0\\ngit checkout 3.22.0\\n\\n# 升级 Flutter 到该版本\\nflutter upgrade\\n\\n# 版本验证,是否成功切换到该版本\\nflutter --version\\n
\\n😳 升级过程中卡住不动大概率就是 网络问题,可以设置下 国内镜像环境变量 (任选其一):
\\n# 清华源\\nexport FLUTTER_STORAGE_BASE_URL=https://mirrors.cnnic.cn/flutter\\nexport PUB_HOSTED_URL=https://mirrors.cnnic.cn/dart-pub\\n\\n# 腾讯源\\nexport FLUTTER_STORAGE_BASE_URL=https://mirrors.cloud.tencent.com/flutter\\nexport PUB_HOSTED_URL=https://mirrors.cloud.tencent.com/dart-pub\\n
\\n配置后执行下 source 命令或重启终端生效后,再重新升级。当然,你有 科学上网 的话,配下 终端代理 亦可:
\\n# 后面的端口号取决于你的✈️\\nexport http_proxy=http://127.0.0.1:1087\\nexport https_proxy=http://127.0.0.1:1087\\n
\\n😳 如果出现「Waiting for another flutter command to release the startup lock」的提示,把 Android Studio 关掉,打开任务管理器 杀掉所有dart进程,接着到 Flutter SDK 目录的 /bin/cache 下删掉名为 lockfile 的文件后,再尝试升级。🙃 当然,也可能有其它莫名其妙的问题,实在搞不定或者不想折腾,直接删了,去 Flutter官网 下载对应版本的SDK,重新配置下就好了:
\\n🤔 升级完我就在想,Flutter 有没有类似于 Python中的版本管理工具,如:venv (轻量级隔离,依赖系统Python版本)、pipenv (依赖锁定+虚拟环境)、pyenv (多版本Python切换)、conda (环境+包管理+跨语言依赖)。如果存在 需要管理多个Flutter版本的需求 (A项目要用a版本,B项目要用b版本,互不干扰),有一个 版本管理工具 还是挺方便的,搜了下,还真有 → FVM (Flutter Version Management),用法非常简单,顺手记录下~
\\n官方文档《FVM Installation》中详细列出了系统环境的安装方式:
\\n# MacOS 和 Linux\\ncurl -fsSL https://fvm.app/install.sh | bash\\nbrew tap leoafarias/fvm\\nbrew install fvm\\n\\n# Windows\\nchoco install fvm\\n
\\n😶 杰哥用的 Windows,得先装下 Chocolatey,它是 Windows 上的 包管理器,类似于 Linux 中的 apt 或 yum,允许用户通过命令行来安装、更新和管理软件包。Chocolatey 使用 NuGet-包管理器 和 PowerShell-脚本 来自动化软件安装过程,简化软件管理。以「管理员身份」运行 PowerShell,依次键入下述命令:
\\n# 输入下述命令回车后,输入 Y\\nSet-ExecutionPolicy RemoteSigned\\n\\n# 安装 Chocolatey\\niwr https://chocolatey.org/install.ps1 -UseBasicParsing | iex\\n\\n# 安装完,键入下述命令查询版本,确认是否安装成功\\nchoco -v\\n
\\n然后再执行 choco install fvm 安装fvm,中途会让你输入,直接输入 Y:
\\n需要 科学上网,偶尔可能抽风下一半就失败,切换不同的代理依旧失败,大概率服务器的问题,建议 换个时间 安装,杰哥昨天下午就是一直不行,今早再试就好了,而且速度快多了🤷♀️。安装完 fvm, Flutter SDK 默认缓存路径为「 ~/fvm/versions」,😳 就C盘,如果不想放这里,执行下述命令指定下缓存地址:
\\nfvm config --cache-path D:\\\\Coding\\\\fvm\\n
\\n然后配置下环境变量「FVM_CACHE_PATH」
\\n😶 官方还给出一个 pub 安装的方式,不过想用 FVM全局管理Flutter版本,建议还是走独立安装~
\\ndart pub global activate fvm\\n
\\n😄 版本管理工具一般都支持 全局控制版本 和 单一项目控制版本,FVM 亦是如此,依次讲讲相关用法~
\\n执行「fvm releases」进行查看:
\\n渠道默认是 stable(稳定版) ,如果想查看其它渠道 (beta、main),可以添加 --channel 参数指定:
\\n执行「fvm install 版本号」进行安装:
\\n安装完,可以键入「fvm list」查看已安装的Flutter版本列表:
\\n此时 Flutter Version 那里显示 Need setup,表示该版本还未完全设置或初始化。
\\n执行「fvm global 版本号」进行设置 (第一次安装完必须先执行,设置一个默认的版本)
\\n设置完,对应目录下会多这个 default 快捷方式:
\\n接着执行「fvm flutter doctor」来检查和完成设置:
\\n如果一直卡住不动,可以尝试添加下述两个 环境变量 来指定 镜像源:
\\n等待设置结束:
\\n完成后,此时再键入「fvm list」
\\n后续执行 flutter、dart 相关的命令,在前面加上 fvm 就好了,比如「fvm flutter --version」
\\n😄 试下装个当前最新的 3.29.0 的版本:
\\n再次执行「fvm flutter --version」查看当前 Flutter 版本:
\\n👏 切换起来还是非常方便的,有时可能会出现这样的提示:
\\nCan\'t load Kernel binary: Invalid kernel binary format version.\\nfvm as globally activated doesn\'t support Dart 3.4.0.\\n\\ntry:\\n`dart pub global activate fvm` to reactivate.\\n
\\n😄 执行下「dart pub global activate fvm」命令 全局激活fvm工具 即可。最后,在 PATH 环境中加上「fvm路径\\\\default\\\\bin」,fvm 切换 Flutter 版本,直接执行 flutter 命令也会是切换后的版本啦~
\\n执行「fvm use 版本号」为当前项目设置一个 特定的Flutter版本,后面没跟版本号的话,会让你选:
\\n运行后会在当前目录下生成下述两个东西~
\\n用于 记录当前项目使用的Flutter 版本 的简单配置文件:
\\n支持的配置如下:
\\nflutter
: 要使用的 Flutter SDK 版本,如果未明确设置,则回退到 flutter 值。cachePath
: 定义项目缓存目录的路径。useGitCache
: (默认值:true)指示是否使用 Git 缓存来处理依赖项。gitCachePath
: 设置 Git 缓存目录的路径,适用于 useGitCache
为 true 的情况。flutterUrl
: 指定 Flutter SDK 仓库的 URL。privilegedAccess
: (默认值:true)确定是否启用需要提升权限的配置。flavors
: 定义不同配置的自定义项目风格的映射。updateVscodeSettings
: (默认值:true)标志是否在配置更改时自动更新 VS Code 设置。updateGitIgnore
: (默认值:true)指示是否根据项目配置自动更新 .gitignore
文件。runPubGetOnSdkChanges
: (默认值:true)在 Flutter SDK 版本更改时自动触发 flutter pub get
。用于 存储和管理 Flutter SDK 的目录:
\\n对应作用:
\\n\\n\\nTips:从 3.0 及以上版本开始,建议将 .fvm 目录添加 .gitignore 文件中。如果 updateGitIgnore 设置为 true,当你将某个版本固定到项目时,它会自动添加到 .gitignore 文件中。
\\n
😄 测试下 全局 和 项目 的 Flutter版本是否隔离,键入「fvm use 3.29.0」切换项目的 Flutter版本。
\\n键入「fvm list」可以看到,Local 处亮起绿灯:
\\n接着分别在 当前目录 和 上级目录 执行 「fvm flutter --version」看看对应的 Flutter 版本:
\\n👍 牛皮!
\\n打开 设置,搜索 Flutter,修改 Flutter SDK path 指向 fvm/default 目录:
\\n不想采用 全局设置 的话而是 单一项目设置,路径指向当前项目的 fvm\\\\flutter_sdk 即可。
\\n使用 Ctrl+Shift+P 或者点击左下角齿轮图标,选择 命令面板,输入 settings.json 进行搜索,打开 Workspace (工作区) 的设置文件:
\\n添加 dart.flutterSdkPaths 的配置,值为 fvm目录\\\\versions:
\\n保存后,Ctrl+Shift+P 输入 Change SDK,即可选择需要的版本直接切换:
\\n切换完,settings.json 会自动生成 dart.flutterSdkPath 的配置:
\\n右下角会弹出版本切换提醒,点下 pub upgrade 更新依赖就行啦~
\\nspawn 是以 指定Flutter版本 直接运行命令,示例:
\\n# 指定要使用的 Flutter 版本\\nfvm spawn --version 2.2.3 flutter doctor\\n\\n# 指定环境变量\\nfvm spawn --env ENV_VAR=value\\n\\n# 指定当前工作目录\\nfvm spawn --cwd /path/to/project flutter build apk\\n
\\nexec 则是基于 当前项目配置 的 Flutter版本运行命令 (🤔直接执行fvm也是一样的效果吧...)
\\n删除本地存储的指定Flutter版本,示例:fvm remove 2.2.3
\\n删除FVM安装的所有Flutter版本和相关数据,重置FVM环境,示例:fvm destroy
\\nFVM 支持项目 flavors(多渠道) ,你可以为不同构建配置指定的不同的Flutter版本,如:
\\n# 为 test 的渠道设置 3.16.9 的 Flutter SDK\\nfvm use 3.16.9 --flavor dev\\n
\\n🤷♀️ 网上搜了下,有人很多人说直接执行 fvm flavor 可以查看所有 flavor 名称,实测并不行:
\\n想看的话,可以打开 .fvmrc 文件:
\\n如果想 切换特定的flavor 可以执行下述命令:
\\n# 其实等价于 fvm use 3.29.0\\nfvm flavor main\\n
\\n不知道误操作了啥,执行fvm的命令一直报这个错:
\\n在 [Beta] flutter upgrade has broken flutter command 找到了临时解决方法:
\\n执行 fvm list 报错:
\\nerror: unable to normalize alternate object path: D:/Coding/fvm/cache.git/.git/objects\\nfatal: Not a valid commit name HEAD\\nError: Unable to determine engine version...\\n
\\n当前版本的 flutter sdk 的 Git 目录损坏,执行「fvm global 版本号」重新下载即可。
\\n","description":"1. 引言 2024.2.29 日 Apple 发布了《关于 App Store 提交的隐私更新》,明确要求 新上架或更新的APP 必须包含 「PrivacyInfo.xcprivacy」 隐私清单文件,否则可能被拒审,部分第三方SDK若以二进制形式集成,还需附带签名。😶 对此组里的 iOS小伙伴 抛出疑问:\\n\\nFlutter 3.18.0 后才内置了PrivacyInfo.xcprivacy,现在我们的版本是3.7.12,要不要 升级下Flutter版本?不升的话就先按照这次提交改一下:Add xcprivacy privacy manifest to…","guid":"https://juejin.cn/post/7478412386153168930","author":"coder_pig","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-06T01:38:01.160Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0d3e5a698ddb48ee9c7cd1ab53347ea5~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1741829880&x-signature=fCjRoAe7VjBVLUH1I8JZ%2BIHYHR0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b8681222dcb2487d96011653137d3fb6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1741829880&x-signature=zgS4Y95Ln7YPPjXIfo6%2FBgt5cIM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5f89e85286cf40e1ab7a48263619f16a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1741829880&x-signature=jobBtVE7QX%2FlrSbT1VaGgoHfGUQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b48497af80774b66a3ac1c66297816b9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1741829880&x-signature=noR544OOUmG5SRnN2afo4%2BuG35I%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/93b0df1102834809be6f0da367329aba~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1741829880&x-signature=QJciVYWS35OVlGDKrx1nFmj0HvI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7937357e9b6e4f0aa794a1211650105a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1741829880&x-signature=%2FnyvERCIeM%2F%2Fy7nr8wCyn919PtI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/481d334e31e044fdb08bfe22cddf4019~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1741829880&x-signature=6dnKF0Fvogi8rgg3G%2BupIEe9HZM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/db09e6be8422417c8e2f3b1ac700f202~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1741829880&x-signature=hgX0SXbOPMf0m4w4CrcljQYFmmA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b18978bc1ca648709abd594da44283e1~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1741829880&x-signature=If5IPgjEKT2i5gDEpP3NONPFZTM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c2ba34fc942e4c229c00964e7b7940ce~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1741829880&x-signature=MDplQDL9Fj%2B%2F8wSNmi4RshSpuo8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6fc9f47637e748da8f06defe5d6d5c0c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1741829880&x-signature=wRCcXfRzMxQDIHRhHlOl1fZ3qbs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3f1d1274988e4e2684f17ed97b7498b3~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1741829880&x-signature=5DVMNd9w27RN%2F2r4xEMt2MFhIgA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7378c38c85504390bc5c3a7ddb3722da~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1741829880&x-signature=SjBsyvBYvEE%2Fhepevb5eFGMbguo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/dada0590c885450489808df9ba64680a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1741829880&x-signature=DUAPV%2BKZSq2DBQYjujbx6C%2FMU%2FI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2d6e9ff01bb548349cc7ffce21817c80~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1741829880&x-signature=gNxwpGy%2FyS3%2BdGEiV%2FnxqRmqkxU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/08c291390e2b410b8150f431159f5069~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1741829880&x-signature=%2BVOui2GVB86CTIEuFgMnl%2FIe1JM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7029a26e601b49d183480eeebc941c37~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1741829880&x-signature=zqYRL6uCSJswQsvszgOIRedIcew%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/da6e3d1725b84d3b9a424101c8db964e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1741829880&x-signature=UF3JMmV6EKhKNsYJZviJvd7MzTs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/df1cd527775c4f388b0d966580c93954~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1741829880&x-signature=K3jU4onmBioJ1E4W7OIZLj4JNmk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/960fed6bf0bd4216a2855c940f9c5701~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1741829880&x-signature=iEmlDhAt%2BR8LPrJogLqycMt5mpE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4a6606804b7340a6ac53cf5aaf83b5f6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1741829880&x-signature=dST4DAce%2BFfShxHc9ADFS7QS2Dg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0efd6c6299104592bce5392eae412b56~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1741829880&x-signature=PCigXWMNoPxjYddgQq877XSjUF0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4be687cb88ec48a883236924125439c7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1741829880&x-signature=TFwkZPERw8Ydvrr%2BRhyLHi7MMkc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/8f54f580409a40a0bc13783bec8f68db~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1741829880&x-signature=cGlNh7PfCdxdJ8aOFZDs18fZTUw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/74323b6522cc4e4aa587c4de3ef528c9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1741829880&x-signature=1p6uD1c53ar%2BK%2BBa8BWuE7xtVIg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ff249227a5304a49aeb89c7db38d8a6e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1741829880&x-signature=AREWh%2BMJyKva56f5Yw3EcW7Bb64%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c45cf8db1e36405aa47475c463787a08~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1741829880&x-signature=nOow77AvtiWBARrf5g6OB%2FF2L4w%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/93dd9a75d9ca4882a3b605eeb38c8715~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1741829880&x-signature=HuBLf%2FZXWUTM%2FJEYUuOTfZ2iYw0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4b61d2e2cd74404c8b0b05abb22eaf67~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1741829880&x-signature=lzMz7yGFgpFCMM4%2BAcTesQ9RvOc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d580fcc31de947d885fb8c574f5fa510~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1741829880&x-signature=QMPBPY%2Fb4nDp3DiErdj8N8iJ5Ws%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d4491d0e3ccb494db04fe2080bdbf2e0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1741829880&x-signature=m1rifBy21RWRYJQSOWr8uS6yBmw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3e9db463ff734fa78885f6e6c5043279~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1741829880&x-signature=p511j0CHBKSGG4DxeiYV%2FL%2FjJ3E%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/91047e8acc444812ab065c5f7ff8415e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1741829880&x-signature=pYxSI9OLAjcM0ybYTN7cL5JzpRA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/031abaa1a0f741af980247c24454ebc3~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1741829880&x-signature=d7JVbLFIjf1HEMCWJ3qdm5jzpvo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a6db69f2bfef4117b83c0a1801458da3~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1741829880&x-signature=kji2GwNy8iKrWIlsAfMV0x%2Bnm2E%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","前端","Flutter","HarmonyOS"],"attachments":null,"extra":null,"language":null},{"title":"Flutter开发者 3.29版本关注要点","url":"https://juejin.cn/post/7478135128100536329","content":"哈喽,我是老刘
\\nFlutter 3.29已经发布有一段时间了,老刘写Flutter代码已经6年多,对这种例行的Flutter版本更新基本上已经是内心毫无波澜。\\n不过最近有不少朋友问要不要更新到最新版,这里罗列一下从开发者角度看比较重要的更新,大家自己判断。
\\n组件库的更新是例行的,基本每个发布版本都会有。
\\n但是通常老刘不建议使用最新的Flutter版本,所以这部分就略过了,感兴趣的同学可以去看官方的发布文档。\\n不建议紧跟最新版主要有两个原因:
\\nflutter_markdown
、palette_generator
等 6 个包将于 2025 年 4 月 30 日后停止支持,需寻找社区分叉或替代方案。调试工具增强也属于是例行更新,大家可以每过几个大版本后系统了解一下最新的调试工具。
\\nThemeData.dialogBackgroundColor
,迁移至 DialogThemeData.backgroundColor
,可通过 dart fix
自动修复。该版本涉及较多底层变更(如线程模型、渲染引擎),可能对现有项目产生兼容性影响。建议:
\\ndart fix
处理弃用 API。dart fix
这个工具还是建议大家利用起来,能很好的提升升级的效率,IDE会有自动化提示,很方便。总的来说对开发者影响最大的可能是Dart 代码现直接在 Android/iOS 的主线程运行。开发者如果升级一定要做好测试覆盖。
\\n其它的功能更多是例行的优化与升级。
\\n从最近几个版本的升级来看,Flutter这个项目已经进入相对稳定的阶段。
\\n比较少出现重量级功能的变化,更多的以bug修复和功能优化为主。
\\n这对开发者来说是非常好的事情。
\\n而对于观望者来说,通过每个发布版本的更新情况,大致能估算团队的投入程度。
\\n基于这些数据去判断比可以减少很多不必要的担心与忧虑。
如果看到这里的同学对客户端开发或者Flutter开发感兴趣,欢迎联系老刘,我们互相学习。\\n点击免费领老刘整理的《Flutter开发手册》,覆盖90%应用开发场景。\\n可以作为Flutter学习的知识地图。\\n覆盖90%开发场景的《Flutter开发手册》
","description":"哈喽,我是老刘 Flutter 3.29已经发布有一段时间了,老刘写Flutter代码已经6年多,对这种例行的Flutter版本更新基本上已经是内心毫无波澜。 不过最近有不少朋友问要不要更新到最新版,这里罗列一下从开发者角度看比较重要的更新,大家自己判断。\\n\\n一、架构与性能优化\\nDart 代码执行线程调整\\n Dart 代码现直接在 Android/iOS 的主线程运行,减少了线程切换开销,但需注意可能加剧平台 UI 线程的负载,需通过性能分析工具监控卡顿问题。\\n渲染引擎变更\\n • iOS 平台完全移除了 Skia 渲染引擎,可能影响依赖 Skia 特性的应用…","guid":"https://juejin.cn/post/7478135128100536329","author":"程序员老刘","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-05T08:57:43.940Z","media":null,"categories":["开发工具","Flutter","客户端"],"attachments":null,"extra":null,"language":null},{"title":"flutter 流(Stream)介绍&结合RxDart使用","url":"https://juejin.cn/post/7477921821285433384","content":"在 Flutter 中,Stream
是一种用于处理异步数据序列的机制。它允许你以异步的方式接收和处理一系列的数据,类似于事件流。下面将详细介绍 Flutter 中 Stream
的使用,包括创建、监听、转换和控制等方面。
在 Flutter 中有多种方式可以创建 Stream
,下面是几种常见的创建方式:
StreamController
StreamController
是创建 Stream
的最常用方式,它允许你手动添加数据到流中。
import \'dart:async\';\\n\\nvoid main() {\\n // 创建一个 StreamController\\n final streamController = StreamController<int>();\\n\\n // 获取 Stream\\n final stream = streamController.stream;\\n\\n // 向流中添加数据\\n streamController.sink.add(1);\\n streamController.sink.add(2);\\n streamController.sink.add(3);\\n\\n // 监听流\\n stream.listen((data) {\\n print(\'Received data: $data\');\\n });\\n\\n // 关闭 StreamController\\n streamController.close();\\n}\\n
\\nStream.fromIterable
Stream.fromIterable
可以将一个可迭代对象(如 List
)转换为 Stream
。
import \'dart:async\';\\n\\nvoid main() {\\n final list = [1, 2, 3, 4, 5];\\n final stream = Stream.fromIterable(list);\\n\\n stream.listen((data) {\\n print(\'Received data: $data\');\\n });\\n}\\n
\\nStream.periodic
Stream.periodic
可以创建一个按指定时间间隔重复发送数据的 Stream
。
import \'dart:async\';\\n\\nvoid main() {\\n final stream = Stream.periodic(Duration(seconds: 1), (count) => count);\\n\\n final subscription = stream.listen((data) {\\n print(\'Received data: $data\');\\n if (data >= 5) {\\n // 取消订阅\\n subscription.cancel();\\n }\\n });\\n}\\n
\\n在上述代码中,创建了一个每隔 1 秒发送一次数据的 Stream
,数据从 0 开始递增。当接收到的数据大于等于 5 时,取消订阅。
使用 listen
方法可以监听 Stream
中的数据,listen
方法接受一个回调函数,当有新的数据到达时,该回调函数将被调用。
import \'dart:async\';\\n\\nvoid main() {\\n final stream = Stream.fromIterable([1, 2, 3]);\\n\\n stream.listen(\\n (data) {\\n print(\'Received data: $data\');\\n },\\n onError: (error) {\\n print(\'Error: $error\');\\n },\\n onDone: () {\\n print(\'Stream is done\');\\n },\\n );\\n}\\n
\\n在上述代码中,listen
方法接受三个可选参数:
onData
:当有新的数据到达时调用。onError
:当流中出现错误时调用。onDone
:当流关闭时调用可以使用 Stream
的各种转换方法对数据进行处理,例如 map
、where
等。
map
转换数据import \'dart:async\';\\n\\nvoid main() {\\n final stream = Stream.fromIterable([1, 2, 3]);\\n\\n final transformedStream = stream.map((data) => data * 2);\\n\\n transformedStream.listen((data) {\\n print(\'Transformed data: $data\');\\n });\\n}\\n
\\nwhere
过滤数据import \'dart:async\';\\n\\nvoid main() {\\n final stream = Stream.fromIterable([1, 2, 3, 4, 5]);\\n\\n final filteredStream = stream.where((data) => data % 2 == 0);\\n\\n filteredStream.listen((data) {\\n print(\'Filtered data: $data\');\\n });\\n}\\n
\\n可以使用 StreamSubscription
对象来控制 Stream
的监听,例如暂停、恢复和取消订阅。
import \'dart:async\';\\n\\nvoid main() {\\n final stream = Stream.periodic(Duration(seconds: 1), (count) => count);\\n\\n final subscription = stream.listen((data) {\\n print(\'Received data: $data\');\\n if (data == 2) {\\n // 暂停订阅\\n subscription.pause();\\n Future.delayed(Duration(seconds: 3), () {\\n // 恢复订阅\\n subscription.resume();\\n });\\n }\\n if (data >= 5) {\\n // 取消订阅\\n subscription.cancel();\\n }\\n });\\n}\\n
\\nStream可以简单的处理数据流,但遇到更复杂的需求时,发现原生Stream的操作符不够用。这个时候我们就可以借助于RxDart。RxDart可以提供更多的操作符的链式调用、错误处理、流的组合。
\\nclass RxStream<T> {\\n final BehaviorSubject<T> _subject = BehaviorSubject<T>();\\n\\n Stream<T> get stream => _subject.stream;\\n\\n // 添加数据\\n void add(T value) => _subject.sink.add(value);\\n\\n // 链式操作符示例:防抖 + 过滤空值\\n Stream<T> debounceAndFilter(Duration duration) {\\n return stream\\n .debounceTime(duration) // 防抖\\n .where((value) => value != null); // 过滤空值\\n }\\n\\n // 合并多个流(例如:搜索输入 + 筛选条件)\\n static Stream<R> combineStreams<A, B, R>(\\n Stream<A> streamA,\\n Stream<B> streamB,\\n R Function(A, B) combiner,\\n ) {\\n return Rx.combineLatest2(streamA, streamB, combiner);\\n }\\n\\n // 关闭资源\\n void dispose() => _subject.close();\\n}\\n \\n
\\nclass SearchService {\\n final RxStream<String> _searchStream = RxStream();\\n Stream<String> get searchResults => _searchStream.debounceAndFilter(Duration(milliseconds: 300));\\n void onSearchTextChanged(String text) {\\n _searchStream.add(text);\\n }\\n void dispose() => _searchStream.dispose();\\n}\\n\\n// 使用示例\\nfinal searchService = SearchService();\\nsearchService.searchResults.listen((text) {\\n// 发起搜索请求(防抖后)\\nprint(\'Searching for: $text\');\\n});\\n
\\nclass _MyHomePageState extends State<MyHomePage> {\\n late EnhancedRxStream<String> searchStream;\\n late EnhancedRxStream<String> filterStream;\\n late Stream<String> combinedStream;\\n\\n @override\\n void initState() {\\n super.initState();\\n searchStream = EnhancedRxStream<String>();\\n filterStream = EnhancedRxStream<String>();\\n\\n // 使用 combineStreams 方法合并两个流\\n combinedStream = EnhancedRxStream.combineStreams(\\n searchStream.stream,\\n filterStream.stream,\\n (searchTerm, filterTerm) {\\n return \'搜索词: $searchTerm, 筛选条件: $filterTerm\';\\n },\\n );\\n }\\n\\n @override\\n void dispose() {\\n searchStream.dispose();\\n filterStream.dispose();\\n super.dispose();\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: const Text(\'Combine Streams Example\'),\\n ),\\n body: Center(\\n child: Column(\\n mainAxisAlignment: MainAxisAlignment.center,\\n children: <Widget>[\\n Padding(\\n padding: const EdgeInsets.all(16.0),\\n child: TextField(\\n onChanged: (text) {\\n searchStream.add(text);\\n },\\n decoration: const InputDecoration(\\n hintText: \'输入搜索词\',\\n ),\\n ),\\n ),\\n Padding(\\n padding: const EdgeInsets.all(16.0),\\n child: TextField(\\n onChanged: (text) {\\n filterStream.add(text);\\n },\\n decoration: const InputDecoration(\\n hintText: \'输入筛选条件\',\\n ),\\n ),\\n ),\\n const SizedBox(height: 20),\\n StreamBuilder<String>(\\n stream: combinedStream,\\n builder: (context, snapshot) {\\n if (snapshot.hasData) {\\n return Text(\\n snapshot.data!,\\n style: Theme.of(context).textTheme.headline6,\\n );\\n }\\n return const Text(\'暂无数据\');\\n },\\n ),\\n ],\\n ),\\n ),\\n );\\n }\\n}\\n\\n
","description":"在 Flutter 中,Stream 是一种用于处理异步数据序列的机制。它允许你以异步的方式接收和处理一系列的数据,类似于事件流。下面将详细介绍 Flutter 中 Stream 的使用,包括创建、监听、转换和控制等方面。 Flutter 流(Stream)介绍\\n1、创建 Stream\\n\\n在 Flutter 中有多种方式可以创建 Stream,下面是几种常见的创建方式:\\n\\n使用 StreamController\\n\\nStreamController 是创建 Stream 的最常用方式,它允许你手动添加数据到流中。\\n\\nimport \'dart:async\';\\n\\nvo…","guid":"https://juejin.cn/post/7477921821285433384","author":"SunshineBrother","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-05T07:35:11.421Z","media":null,"categories":["iOS","Flutter","Android"],"attachments":null,"extra":null,"language":null},{"title":"系统化掌握Flutter组件之PageView(一):筑基之旅","url":"https://juejin.cn/post/7477937313391411235","content":"在移动应用开发中,页面滑动交互占据着核心地位。但你是否思考过:
\\n抖音的短视频
切换如此丝滑?电商App
的商品轮播如何做到无缝衔接?新闻客户端
的栏目切换为何能精准响应?这一切的背后,都离不开PageView
组件的精妙设计。作为Flutter
布局体系的滑动容器,它承载着页面生命周期管理
、手势冲突协调
、性能优化
等多重使命。
本文将带你穿透表象,直击本质,通过3
个实战案例,彻底掌握如何用PageView
构建企业级复杂滚动界面。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\nPageView
核心原理剖析1、页面容器本质
\\nPageView
是一个特殊的ScrollView
子类,其核心在于管理多个\\"页面维度\\"
的视图。与ListView
的线性排列不同,PageView
采用视口(Viewport
)机制,每个子元素占据完整的视口区域,通过滑动切换实现页面跳转。
PageView(\\n children: [\\n Container(color: Colors.red), // 页面1\\n Container(color: Colors.green),// 页面2\\n Container(color: Colors.blue), // 页面3\\n ],\\n)\\n
\\n2、坐标系与布局流程
\\n视口坐标系决定了子组件的布局方式:
PageController.offset
动态计算。3、缓存机制
\\n默认缓存当前页面
及其相邻页面
(通过cacheCount
控制),这是通过SliverFillViewport
实现的。当使用PageView.builder
时,缓存机制与ListView
类似,但以页面为单位进行回收。
属性 | 类型 | 深度解析 | 典型应用场景 |
---|---|---|---|
controller | PageController | 控制页面跳转的核心枢纽,需手动dispose 。关键方法:jumpToPage() 、animateToPage() | 实现程序控制页面跳转 |
physics | ScrollPhysics | 控制滑动行为的物理引擎: • ClampingScrollPhysics (安卓风格)• BouncingScrollPhysics (iOS 风格) | 平台适配/自定义滑动效果 |
scrollDirection | Axis | 滑动轴方向,垂直滑动时需注意键盘弹出问题 | 竖屏阅读器/垂直轮播 |
allowImplicitScrolling | bool | 允许子组件捕获滑动事件,解决嵌套滚动冲突的关键 | 页面内包含可滚动组件时 |
padEnds | bool | 当页面不足视口大小时是否填充空白区域(默认true ) | 小尺寸页面布局优化 |
restorationId | String? | 页面滚动位置恢复标识符,与RestorationMixin 配合使用 | 状态恢复场景 |
clipBehavior | Clip | 内容裁剪方式,影响性能表现(默认Clip.hardEdge ) | 实现圆角页面效果时需调整 |
场景描述:构建一个带指示器的水平轮播图。
\\nimport \'package:flutter/material.dart\';\\n\\nclass BasicPageViewDemo extends StatefulWidget {\\n @override\\n _BasicPageViewDemoState createState() => _BasicPageViewDemoState();\\n}\\n\\nclass _BasicPageViewDemoState extends State<BasicPageViewDemo> {\\n final PageController _controller = PageController(viewportFraction: 0.85);\\n int _currentPage = 0;\\n\\n final List<Color> _pages = [\\n Colors.blue.shade200,\\n Colors.green.shade200,\\n Colors.orange.shade200,\\n ];\\n\\n @override\\n void dispose() {\\n _controller.dispose(); // 必须手动释放控制器\\n super.dispose();\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: Text(\\"PageView Demo\\"),\\n backgroundColor: Theme.of(context).colorScheme.inversePrimary,\\n ),\\n body: buildColumn(),\\n );\\n }\\n\\n Column buildColumn() {\\n return Column(\\n children: [\\n // 页面视图区域\\n Expanded(\\n child: buildPageView(),\\n ),\\n // 指示器区域\\n Padding(\\n padding: EdgeInsets.symmetric(vertical: 20),\\n child: buildRow(),\\n ),\\n ],\\n );\\n }\\n\\n PageView buildPageView() {\\n return PageView.builder(\\n controller: _controller,\\n itemCount: _pages.length,\\n onPageChanged: (index) {\\n setState(() => _currentPage = index);\\n },\\n itemBuilder: (context, index) {\\n return AnimatedContainer(\\n duration: Duration(milliseconds: 300),\\n margin: EdgeInsets.all(10),\\n decoration: BoxDecoration(\\n color: _pages[index],\\n borderRadius: BorderRadius.circular(20),\\n boxShadow: [\\n BoxShadow(\\n color: Colors.black12,\\n blurRadius: _currentPage == index ? 10 : 5,\\n spreadRadius: _currentPage == index ? 2 : 1,\\n )\\n ],\\n ),\\n child: Center(\\n child: Text(\\n \'Page ${index + 1}\',\\n style: TextStyle(fontSize: 32, color: Colors.white),\\n ),\\n ),\\n );\\n },\\n );\\n }\\n\\n Row buildRow() {\\n return Row(\\n mainAxisAlignment: MainAxisAlignment.center,\\n children: _pages.asMap().entries.map((entry) {\\n return Container(\\n width: 12,\\n height: 12,\\n margin: EdgeInsets.symmetric(horizontal: 4),\\n decoration: BoxDecoration(\\n shape: BoxShape.circle,\\n color: _currentPage == entry.key\\n ? Colors.blue\\n : Colors.grey.withValues(alpha: 0.5),\\n ),\\n );\\n }).toList(),\\n );\\n }\\n}\\n
\\n图示:
\\n代码解析:
\\nPageView.builder
实现懒加载,适合动态页面场景
。PageController
的viewportFraction
属性实现页面\\"预览\\"
效果。AnimatedContainer
实现页面切换时的平滑过渡动画。状态实时同步
。生命周期管理
(dispose)。class _KeepAlivePage extends StatefulWidget {\\n @override\\n _KeepAlivePageState createState() => _KeepAlivePageState();\\n}\\n\\nclass _KeepAlivePageState extends State<_KeepAlivePage> \\n with AutomaticKeepAliveClientMixin {\\n \\n @override\\n bool get wantKeepAlive => true; // 保持页面状态\\n \\n @override\\n Widget build(BuildContext context) {\\n super.build(context); // 必须调用父类方法\\n return Container(...);\\n }\\n}\\n
\\n最佳实践:
\\nAutomaticKeepAliveClientMixin
和PageStorageKey
。PageController(\\n viewportFraction: 0.8, // 相邻页面可见20%\\n)\\n
\\n视觉效果对比:
\\n1.0
:标准全屏模式。0.8
:电影海报墙效果。1.2
:实现缩放视差效果。PageView.builder(\\n itemCount: 1000,\\n itemBuilder: (context, index) {\\n return HeavyWidget(index: index);\\n },\\n)\\n
\\n优化策略:
\\nbuilder
构造函数实现懒加载。const
构造函数。RepaintBoundary
减少重绘区域。cacheExtent
合理控制预加载范围。问题1:页面滑动卡顿
\\n使用性能面板分析
)。itemBuilder
中进行耗时操作。KeepAlive
减少重复构建。问题2:嵌套滚动冲突
\\nPageView(\\n physics: NeverScrollableScrollPhysics(), // 禁用自身滚动\\n children: [\\n SingleChildScrollView(...), // 子组件处理滚动\\n ],\\n)\\n
\\n或使用NotificationListener
进行精细控制。
问题3:页面跳转异常
\\n_controller.animateToPage(\\n 2,\\n duration: Duration(milliseconds: 500),\\n curve: Curves.easeInOut,\\n);\\n
\\n确保在WidgetsBinding
实例可用后再执行跳转操作。
PageView三要素模型:
\\n ┌─────────────┐ ┌─────────────┐ ┌─────────────┐\\n │ │ │ │ │ │\\n │ 视口系统 │───────▶ 控制器 │───────▶ 页面管理 │\\n │ (Viewport) │ │(Controller) │ │ (Children) │\\n └─────────────┘ └─────────────┘ └─────────────┘\\n ▲ ▲ ▲\\n │ │ │\\n 布局约束处理 交互与动画控制 状态与生命周期\\n
\\n核心算法:
\\nimport \'package:flutter/material.dart\';\\nimport \'dart:async\';\\n\\nclass InfiniteCarousel extends StatefulWidget {\\n const InfiniteCarousel({super.key});\\n\\n @override\\n _InfiniteCarouselState createState() => _InfiniteCarouselState();\\n}\\n\\nclass _InfiniteCarouselState extends State<InfiniteCarousel> {\\n final PageController _pageController = PageController(initialPage: 10000);\\n final List<String> _imageUrls = [\\n \'https://picsum.photos/300/200?image=10\',\\n \'https://picsum.photos/300/200?image=20\',\\n \'https://picsum.photos/300/200?image=30\',\\n ];\\n int _currentPage = 0;\\n Timer? _timer;\\n\\n @override\\n void initState() {\\n super.initState();\\n _startAutoPlay();\\n _pageController.addListener(_updateIndicator);\\n }\\n\\n void _startAutoPlay() {\\n _timer = Timer.periodic(const Duration(seconds: 3), (_) {\\n if (_pageController.hasClients) {\\n _pageController.nextPage(\\n duration: const Duration(milliseconds: 500),\\n curve: Curves.easeInOut,\\n );\\n }\\n });\\n }\\n\\n void _updateIndicator() {\\n final newPage = _getRealIndex(_pageController.page!.round());\\n if (newPage != _currentPage) {\\n setState(() => _currentPage = newPage);\\n }\\n }\\n\\n int _getRealIndex(int position) {\\n return position % _imageUrls.length;\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: Text(\\"PageView Demo\\"),\\n backgroundColor: Theme.of(context).colorScheme.inversePrimary,\\n ),\\n body: Column(\\n children: [\\n _buildCarousel(),\\n _buildIndicator(),\\n ],\\n ),\\n );\\n }\\n\\n Widget _buildCarousel() {\\n return AspectRatio(\\n aspectRatio: 16 / 9,\\n child: PageView.builder(\\n controller: _pageController,\\n onPageChanged: (index) => _updateIndicator(),\\n itemBuilder: (context, index) {\\n return Container(\\n margin: const EdgeInsets.symmetric(horizontal: 8),\\n decoration: BoxDecoration(\\n borderRadius: BorderRadius.circular(12),\\n boxShadow: [\\n BoxShadow(\\n color: Colors.black.withValues(alpha: 0.3),\\n blurRadius: 8,\\n spreadRadius: 2,\\n )\\n ],\\n ),\\n child: ClipRRect(\\n borderRadius: BorderRadius.circular(12),\\n child: Image.network(\\n _imageUrls[_getRealIndex(index)],\\n fit: BoxFit.cover,\\n loadingBuilder: (context, child, loadingProgress) {\\n if (loadingProgress == null) return child;\\n return Center(\\n child: CircularProgressIndicator(\\n value: loadingProgress.expectedTotalBytes != null\\n ? loadingProgress.cumulativeBytesLoaded /\\n loadingProgress.expectedTotalBytes!\\n : null,\\n ),\\n );\\n },\\n ),\\n ),\\n );\\n },\\n ),\\n );\\n }\\n\\n Widget _buildIndicator() {\\n return Padding(\\n padding: const EdgeInsets.symmetric(vertical: 16),\\n child: Row(\\n mainAxisAlignment: MainAxisAlignment.center,\\n children: _imageUrls.asMap().entries.map((entry) {\\n return Container(\\n width: 10,\\n height: 10,\\n margin: const EdgeInsets.symmetric(horizontal: 4),\\n decoration: BoxDecoration(\\n shape: BoxShape.circle,\\n color: _currentPage == entry.key\\n ? Colors.blue\\n : Colors.grey.withOpacity(0.5),\\n ),\\n );\\n }).toList(),\\n ),\\n );\\n }\\n\\n @override\\n void dispose() {\\n _pageController.dispose();\\n _timer?.cancel();\\n super.dispose();\\n }\\n}\\n
\\n图示:
\\n实现要点解析:
\\n1、无限循环策略:
\\nPageController(initialPage: 10000) // 设置大数初始位置\\nint _getRealIndex(int position) => position % _imageUrls.length // 取余实现循环\\n
\\n2、自动播放控制:
\\nTimer.periodic() // 定时器控制自动播放\\nnextPage() // 平滑翻页\\ndispose()中取消定时器 // 防止内存泄漏\\n
\\n3、性能优化:
\\nPageView.builder // 懒加载机制\\nloadingBuilder // 图片加载进度指示\\nBoxDecoration缓存 // 复用渲染对象\\n
\\n4、交互增强:
\\nBoxShadow添加投影 // 提升视觉效果\\nBorderRadius圆角 // 现代设计风格\\n
\\n5、状态同步:
\\naddListener(_updateIndicator) // 实时同步页码\\nonPageChanged双重保障 // 处理边界情况\\n
\\n手势冲突的和平协议
混合滑动方案:
\\nimport \'package:flutter/material.dart\';\\n\\nclass ScrollConflictDemo extends StatefulWidget {\\n @override\\n _ScrollConflictDemoState createState() => _ScrollConflictDemoState();\\n}\\n\\nclass _ScrollConflictDemoState extends State<ScrollConflictDemo> {\\n final ScrollController _verticalController = ScrollController();\\n final PageController _horizontalController = PageController();\\n bool _lockVerticalScroll = false;\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: Text(\\"PageView Demo\\"),\\n backgroundColor: Theme.of(context).colorScheme.inversePrimary,\\n ),\\n body: NotificationListener<ScrollUpdateNotification>(\\n onNotification: _handleScrollUpdate,\\n child: ListView.builder(\\n controller: _verticalController,\\n itemCount: 5,\\n itemBuilder: (context, index) {\\n if (index == 2) {\\n return _buildHorizontalScrollSection();\\n }\\n return Container(\\n height: 200,\\n color: Colors.primaries[index % Colors.primaries.length],\\n child: Center(\\n child: Text(\'垂直列表项 $index\',\\n style: const TextStyle(fontSize: 24, color: Colors.white)),\\n ),\\n );\\n },\\n ),\\n ),\\n );\\n }\\n\\n Widget _buildHorizontalScrollSection() {\\n return SizedBox(\\n height: 300,\\n child: PageView.builder(\\n controller: _horizontalController,\\n scrollDirection: Axis.horizontal,\\n itemCount: 5,\\n itemBuilder: (context, index) {\\n return Container(\\n margin: const EdgeInsets.all(10),\\n decoration: BoxDecoration(\\n color: Colors.grey[800],\\n borderRadius: BorderRadius.circular(15),\\n ),\\n child: Center(\\n child: Text(\'水平页面 $index\',\\n style: const TextStyle(fontSize: 24, color: Colors.white)),\\n ),\\n );\\n },\\n ),\\n );\\n }\\n\\n bool _handleScrollUpdate(ScrollUpdateNotification notification) {\\n final scrollDelta = notification.scrollDelta;\\n\\n if (scrollDelta == null) return false;\\n\\n // 判断滑动方向\\n final isHorizontal = (notification.dragDetails?.delta.dx.abs() ?? 0) >\\n (notification.dragDetails?.delta.dy.abs() ?? 0);\\n\\n if (isHorizontal) {\\n // 水平滑动优先处理PageView\\n if (!_lockVerticalScroll) {\\n setState(() => _lockVerticalScroll = true);\\n _verticalController.jumpTo(_verticalController.offset);\\n }\\n return true;\\n } else {\\n // 垂直滑动时恢复ListView滚动\\n if (_lockVerticalScroll) {\\n setState(() => _lockVerticalScroll = false);\\n }\\n return false;\\n }\\n }\\n\\n @override\\n void dispose() {\\n _verticalController.dispose();\\n _horizontalController.dispose();\\n super.dispose();\\n }\\n}\\n
\\n图示:
\\n核心解决逻辑解析:
\\n1、手势方向判断:
\\n// 通过比较X/Y轴的滑动距离差值判断方向\\nfinal isHorizontal = (delta.dx.abs() > delta.dy.abs());\\n
\\n2、滚动锁定机制:
\\n// 锁定垂直滚动时保持ListView位置不变\\n_lockVerticalScroll = true;\\n_verticalController.jumpTo(_verticalController.offset);\\n
\\n3、物理特性动态切换:
\\n// 通过NotificationListener动态控制是否拦截事件\\nonNotification: _handleScrollUpdate\\n
\\n实现效果说明:
\\n1、水平滑动优先:
\\n2、垂直滑动恢复:
\\n3、边界处理:
\\n// 通过jumpTo保持位置稳定\\n_verticalController.jumpTo(_verticalController.offset);\\n
\\nPageView
组件就像Flutter
世界的传送门,连接着静态布局与动态交互
的两个维度。通过系统化的学习路径,我们不仅掌握了基础属性的基因序列,更揭开了企业级应用的神秘面纱。
记住这三个核心法则:控制器是大脑
、物理特性是骨架
、构建器是血脉
。当面临复杂场景时,不妨回归到\\"滑动本质=数据驱动+状态管理+性能优化\\"
的黄金三角。
真正的精通,在于将PageView
的每个参数都转化为解决实际问题的武器。现在,带着这份系统化的认知地图,去创造属于你的滑动奇迹吧!
\\n","description":"前言 在移动应用开发中,页面滑动交互占据着核心地位。但你是否思考过:\\n\\n为什么抖音的短视频切换如此丝滑?\\n电商App的商品轮播如何做到无缝衔接?\\n新闻客户端的栏目切换为何能精准响应?\\n\\n这一切的背后,都离不开PageView组件的精妙设计。作为Flutter布局体系的滑动容器,它承载着页面生命周期管理、手势冲突协调、性能优化等多重使命。\\n\\n本文将带你穿透表象,直击本质,通过3个实战案例,彻底掌握如何用PageView构建企业级复杂滚动界面。\\n\\n操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。\\n\\n一、基础认知\\n1.1、PageView核心原理剖析\\n\\n1…","guid":"https://juejin.cn/post/7477937313391411235","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-05T06:18:45.533Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/41dc0a94e4174f62b3093ebd6d568107~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1741760325&x-signature=5lLVZI2k%2FFE%2B3odIN%2Bdnig38TlI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/57cbe4fc73994c31a333a16b2d90a802~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1741760325&x-signature=sfuvCEaa6vstQppcJujP36Gb3lg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ddc8e3bf202e4ff7994011d53369d23c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1741760325&x-signature=aneaGXeT8fYLoIbZ00AfLKAFmsQ%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"Android Studio Meerkat | 2024.3.1 更新,快来看看有什么新功能吧","url":"https://juejin.cn/post/7478167090499764259","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
又到了认识全新 Android Studio 动物的时候,本次更新的版本是猫鼬:
\\nCompose 又又又迎来了新的增强,这次是大家非常关心的 Preview 增强,本次更新处理了:
\\n\\n\\n可以看到现在 Android Studio 基本都没有 XML 传统 View 的支持的更新,基本更新都在 Compose 。
\\n
随着 Fleet 不再作为 KMP 的专用框架 , Android Studio 现在可以使用 KMP (Kotlin MultiPlatform) 给 App 添加共享逻辑,前提是:
\\nbuild.gradle.kts
文件添加对共享模块的依赖项Platform.android.kt
文件并添加 actual fun platform() = \\"Android from Shared KMP Module\\"
MainActivity.kt
文件将其修改为从共享模块调用 platform()
函数通过 Device Manager 的 + 按钮,然后选择 Create Virtual Device 或 Select Remote Devices ,就可以轻松添加对应设备,现在会有全新的筛选条件和建议:
\\nAndroid Studio Meerkat 引入了 Gemini 的新功能,要使用这些功能,需要在当前项目中启用与 Gemini 共享代码上下文:
\\n\\n\\n你别说,以上两个场景使用 AI 是再合适不过了。
\\n
Feature Drop 属于接下来要发布的功能版本,目前还是 Canary 。
\\nMeerkat Feature Drop 版本现在可以在 Android Studio 中将图像直接添加到 Gemini ,从而生成相应的代码框架:
\\n另外, Android Studio 中的 Gemini 新增了提示库功能,可以让开发者保存和管理常用的提示,可以在 Settings > Gemini > Prompt Library 访问提示库从而存储和检索 prompt,还可以右键聊天中的 prompt 保存备用。
\\n要应用已保存的 prompt ,可以在 Editor 中右键并导航到 Gemini > Prompt Library 应用:
\\n最后,还可以使用 Gemini 生成 Compose Preview ,这属于 Android Studio 目前的实验性功能,需要在 Compose 代码处右键选择: Gemini > Generate \\"\\" Preview 打开,如果当然文件没有预览,可以通过 Gemini > Generate Compose Preview 启用:
\\n\\n\\nAI 生成预览也是一个有趣的方向。
\\n
Android Studio Meerkat Feature Drop 提供了为应用生成备份并将其恢复到其他设备的方法,这对于测试在设备之间或云备份还原应用数据时应用是否按预期运行非常有用,或者在直接使用开发和调试的数据进行设置测试的场景也很有用。
\\n一般情况下,可以将应用 Debug 运行到连接的设备:
\\n需要恢复时通过 Run > Edit Configurations 完成选择对应的数据即可:
\\n为了帮助开发者在 Android 13 开发者选项中启用 Theme icons,Android Studio Meerkat Feature Drop 现在允许开发者预览使用新主题时查看图标的外观。
\\n为了完全控制图标的外观,开发者需要提供自己的主题图标,并添加自定义 monochromatic layer (res/mipmap-anydpi-v26/ic_launcher.xml
):
<?xml version=\\"1.0\\" encoding=\\"utf-8\\"?>\\n...\\n<adaptive-icon xmlns:android=\\"http://schemas.android.com/apk/res/android\\">\\n <background android:drawable=\\"@drawable/ic_launcher_background\\" />\\n <foreground android:drawable=\\"@drawable/ic_launcher_foreground\\" />\\n\\n // Starting with Android 13 (API level 33), you can opt-in to providing a\\n // <monochrome> drawable.\\n <monochrome android:drawable=\\"@drawable/ic_launcher_monochrome\\" />\\n</adaptive-icon>\\n...\\n
\\n当然,就算你不这样做,仍然可以使用这个新的预览工具去预览图标的外观,并查看颜色和对比度等问题:
\\n\\n\\n有多少人适配过这个?
\\n
从 Meerkat Feature Drop Canary 2 开始,Android Studio 在 Canary、Beta 版和稳定版中使用相同的用户配置,现在 Android Studio会在 config 目录路径中添加了一个 micro 版本,例如使用 AndroidStudio2024.3.2
而不是 AndroidStudio2024.3
。
\\n\\n可以删除旧配置释放空间。
\\n
Android Studio Meerkat Feature Drop 开始支持开发人员使用 Jetpack XR 构建支持:
\\n使用 Compose Preview 屏幕截图测试工具测试 Compose 界面,新工具可直接生成 HTML 报告,从而直观地检测对应用 UI 的任何更改 ,例如
\\n@Preview
参数(例如 uiMode
或 fontScale
)和多预览功能,帮助扩大测试规模。screenshotTest
源代码集将测试模块化。\\n\\n\\n
随着 Android Studio 的动物越来越多,Android Studio 动物园也是越来越丰富,除了图片里的这些,过去的还有白狐狸、海豚、电鳗、火烈鸟、长颈鹿、刺猬、花栗鼠等没出镜,但是没关系,未来动物园肯定越来越壮硕,而随着 Fleet 不在支持 KMP ,未来 KMP 肯定会越来越高度集成会 Android Studio 。
\\n那么,少年,开始吃螃蟹了~
","description":"又到了认识全新 Android Studio 动物的时候,本次更新的版本是猫鼬: Compose 预览增强\\n\\nCompose 又又又迎来了新的增强,这次是大家非常关心的 Preview 增强,本次更新处理了:\\n\\n支持更好的缩放效果,预览的缩放效果更流程和灵敏\\n预览组可折叠,减少混乱\\n可以从 Grid mode 切换到 Gallery mode,删除了 List 模式\\n\\n可以看到现在 Android Studio 基本都没有 XML 传统 View 的支持的更新,基本更新都在 Compose 。\\n\\nKMP Shared Module 和 Android…","guid":"https://juejin.cn/post/7478167090499764259","author":"恋猫de小郭","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-05T03:53:30.217Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f7450392b7204b03983070d9aec944ce~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1741751609&x-signature=ncDLFyQRaQcCADmJZaJEnwt4wm0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/81d739c4219c48dc971820629bb80151~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1741751609&x-signature=GoUmil7%2FosjTZM0U5UKwm3%2BBvK4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/355af916268648bcb42c97585a0affab~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1741751609&x-signature=6rvLTRb4QRzYi%2FhnV9v8YD6vpDY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/cfadcb04a5174fd3a881396c4639a3fe~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1741751609&x-signature=8Zyu1OLEuWEi9VCLR3c1scVhQlE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5f37511eece44ceebe586e0aa75eadf7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1741751609&x-signature=WsMOmN6pMp%2FjwqzMU3WmtdUsRVM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/bd95b6488fc9459b80385162ecda4850~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1741751609&x-signature=Ims%2FgQJV7sri%2Fanr18s%2FbETRzhM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/07a26a96363f4ebe8ca887b2bf206077~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1741751609&x-signature=ej39WdAlaH2Ag53Eq9EDBePGR1A%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2000025e9f0746759f7a464cba223174~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1741751609&x-signature=YaXOcF%2FuSNS6GNaxUGZ3bnhuY2I%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2e86c4f5ce5546c7849a7deb1c3ef6f7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1741751609&x-signature=zgU79SvVU0Fyc52RH5vBkvLVDxA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fc039b8731a14a48a35fe119af6ed5d6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1741751609&x-signature=MdhRNzcrsxQirkNpmHvb0uioBI0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/bde4ffbb1e794583bc330a723fa1b8ab~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1741751609&x-signature=UroByYhQrpNyfs9HFvdKlzjpg2I%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fd888130f3f24002904b855c79a1b7cc~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1741751609&x-signature=WieJR9QpTrkTzkdbgezlMpmCxcs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0acd590dcbcf450390f73b6208a00207~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1741751609&x-signature=CSr5OuCxRDUHjP67Pgg6OqMXs%2FQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5b4a27a714284a8188b7caaf9c665508~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1741751609&x-signature=k04o3vvC7vQIsajqtDI%2BIKAY3%2FE%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"系统化掌握Flutter组件之NestedScrollView(一):筑基之旅","url":"https://juejin.cn/post/7477869412277403648","content":"滚动布局是用户交互的核心场景之一,但当遇到多层级嵌套滚动(如顶部导航栏吸顶
、下拉刷新与分页加载结合
)时,开发者往往陷入手势冲突
、滚动错位
的困境。ListView
或CustomScrollView
虽然强大,却难以协调多个滚动区域的联动关系。
NestedScrollView
如同一名精密的\\"指挥家\\"
,能够优雅地协调SliverAppBar
、TabBarView
、ListView
等组件的协作。但许多初学者因缺乏系统性认知,仅停留在基础API
调用层面,未能发挥其真正威力。
本文将带你穿透表象,直击本质,通过3
个实战案例,彻底掌握如何用NestedScrollView
构建企业级复杂滚动界面。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\nNestedScrollView
核心原理核心定位:
\\nNestedScrollView
是 Flutter
中用于协调多个独立滚动区域的容器组件。其核心原理是通过 NestedScrollViewCoordinator
协调两个滚动控制器:
Header
):由 headerSliverBuilder
定义的 Sliver
组件组成(如 SliverAppBar
),受 PrimaryScrollController
控制。Body
):由 body
定义的普通滚动组件(如 ListView
),拥有独立的 ScrollController
。滚动联动规则:
\\nheaderSliverBuilder
:构建顶部浮动层作用:
\\n定义顶部的 Sliver
组件集合(如吸顶导航
、可折叠头图
),支持动态响应滚动状态。
NestedScrollView(\\n headerSliverBuilder: (context, innerBoxIsScrolled) {\\n return [\\n _buildSliverAppBar(innerBoxIsScrolled),\\n _buildSliverPersistentHeader(),\\n ];\\n },\\n body: _buildBody(),\\n)\\n\\nSliverAppBar _buildSliverAppBar(bool innerBoxIsScrolled) {\\n return SliverAppBar(\\n expandedHeight: 200,\\n pinned: true,\\n floating: true,\\n snap: true,\\n flexibleSpace: FlexibleSpaceBar(\\n title: Text(innerBoxIsScrolled ? \'标题吸顶\' : \'展开状态\'),\\n background:\\n Image.asset(\'assets/images/product.webp\', fit: BoxFit.cover),\\n ),\\n );\\n}\\n\\nSliverPersistentHeader _buildSliverPersistentHeader() {\\n return SliverPersistentHeader(\\n pinned: true,\\n delegate: _StickyTabBarDelegate(\\n child: TabBar(\\n tabs: [Tab(text: \'商品\'), Tab(text: \'评论\')],\\n ),\\n ),\\n );\\n}\\n
\\n关键参数解析:
\\ninnerBoxIsScrolled
:布尔值,表示内层滚动是否已触发(可用于动态更新UI)SliverAppBar
的 pinned
/floating
/snap
:控制吸顶、快速展开等行为SliverPersistentHeader
:创建自定义固定头组件(需实现 SliverPersistentHeaderDelegate
)body
:主体滚动区域作用:
\\n定义主要的滚动内容区域,通常与 TabBarView
或 ListView
结合使用。
Widget _buildBody() {\\n return TabBarView(\\n children: [\\n CustomScrollView(\\n slivers: [\\n SliverPadding(\\n padding: EdgeInsets.all(16),\\n sliver: SliverList(\\n delegate: SliverChildBuilderDelegate(\\n (ctx, index) => itemWidget(index),\\n childCount: 15,\\n ),\\n ),\\n ),\\n ],\\n ),\\n ListView.builder(\\n padding: EdgeInsets.zero,\\n itemCount: 15,\\n itemBuilder: (context, index) {\\n return itemWidget(index);\\n },\\n )\\n ],\\n );\\n}\\n\\nContainer itemWidget(int index) {\\n return Container(\\n height: 100,\\n color: Colors.primaries[index % 18],\\n alignment: Alignment.center,\\n child: Text(\\"Item $index\\"),\\n );\\n}\\n
\\n避坑指南:
\\nListView
可能导致滚动冲突CustomScrollView
+ SliverList
保证滚动一致性floatHeaderSlivers
:控制浮动行为作用:
\\n决定 Header
中的 Sliver
是否浮动在 Body
内容之上(默认 false
)。
场景对比:
\\nNestedScrollView(\\n floatHeaderSlivers: true, // Header始终覆盖Body\\n headerSliverBuilder: [/*...*/],\\n body: [/*...*/],\\n)\\n
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\nfloatHeaderSlivers | 效果 |
---|---|
false (默认) | Header 滚动时与Body 内容联动 |
true | Header 始终悬浮在Body 上方 |
clipBehavior
:裁剪优化作用:
\\n控制滚动内容溢出时的裁剪方式,影响性能与视觉效果。
NestedScrollView(\\n clipBehavior: Clip.hardEdge, // 使用硬件加速裁剪\\n headerSliverBuilder: [/*...*/],\\n body: [/*...*/],\\n)\\n
\\n可选值对比:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n参数 | 性能 | 视觉效果 |
---|---|---|
Clip.none | 高 | 内容可能溢出 |
Clip.hardEdge | 中 | 精确裁剪(推荐) |
Clip.antiAlias | 低 | 抗锯齿边缘 |
场景:实现一个电商详情页,包含可折叠头图
、吸顶Tab导航
、商品列表
。
import \'package:flutter/material.dart\';\\n\\nclass NestedScrollDemo extends StatefulWidget {\\n const NestedScrollDemo({super.key});\\n\\n @override\\n State<NestedScrollDemo> createState() => _NestedScrollDemoState();\\n}\\n\\nclass _NestedScrollDemoState extends State<NestedScrollDemo> {\\n @override\\n Widget build(BuildContext context) {\\n return DefaultTabController(\\n length: 2,\\n child: Scaffold(\\n body: NestedScrollView(\\n headerSliverBuilder: (context, innerBoxIsScrolled) {\\n return [\\n _buildSliverAppBar(innerBoxIsScrolled),\\n _buildSliverPersistentHeader(),\\n ];\\n },\\n body: _buildBody(),\\n ),\\n ),\\n );\\n }\\n\\n SliverAppBar _buildSliverAppBar(bool innerBoxIsScrolled) {\\n return SliverAppBar(\\n expandedHeight: 200,\\n pinned: true,\\n floating: true,\\n snap: true,\\n flexibleSpace: FlexibleSpaceBar(\\n title: Text(innerBoxIsScrolled ? \'标题吸顶\' : \'展开状态\'),\\n background:\\n Image.asset(\'assets/images/product.webp\', fit: BoxFit.cover),\\n ),\\n );\\n }\\n\\n SliverPersistentHeader _buildSliverPersistentHeader() {\\n return SliverPersistentHeader(\\n pinned: true,\\n delegate: _StickyTabBarDelegate(\\n child: TabBar(\\n tabs: [Tab(text: \'商品\'), Tab(text: \'评论\')],\\n ),\\n ),\\n );\\n }\\n\\n Widget _buildBody() {\\n return TabBarView(\\n children: [\\n CustomScrollView(\\n slivers: [\\n SliverPadding(\\n padding: EdgeInsets.all(16),\\n sliver: SliverList(\\n delegate: SliverChildBuilderDelegate(\\n (ctx, index) => itemWidget(index),\\n childCount: 15,\\n ),\\n ),\\n ),\\n ],\\n ),\\n ListView.builder(\\n padding: EdgeInsets.zero,\\n itemCount: 15,\\n itemBuilder: (context, index) {\\n return itemWidget(index);\\n },\\n )\\n ],\\n );\\n }\\n\\n Container itemWidget(int index) {\\n return Container(\\n height: 100,\\n color: Colors.primaries[index % 18],\\n alignment: Alignment.center,\\n child: Text(\\"Item $index\\"),\\n );\\n }\\n}\\n\\n// 自定义SliverPersistentHeaderDelegate\\nclass _StickyTabBarDelegate extends SliverPersistentHeaderDelegate {\\n final Widget child;\\n\\n _StickyTabBarDelegate({required this.child});\\n\\n @override\\n Widget build(context, shrinkOffset, overlapsContent) {\\n return Container(\\n // color: Colors.redAccent,\\n child: child,\\n );\\n }\\n\\n @override\\n double get maxExtent => 48;\\n\\n @override\\n double get minExtent => 48;\\n\\n @override\\n bool shouldRebuild(covariant _StickyTabBarDelegate oldDelegate) {\\n return child != oldDelegate.child;\\n }\\n}\\n
\\n问题1:TabBarView
内容不滚动 。
\\n原因:直接使用 ListView
未包裹在 CustomScrollView
中
// 错误写法\\nbody: TabBarView(children: [ListView(), ListView()])\\n\\n// 正确写法\\nbody: TabBarView(\\n children: [\\n CustomScrollView(slivers: [SliverList(...)]),\\n CustomScrollView(slivers: [SliverList(...)])\\n ]\\n)\\n
\\n问题2:头部折叠时出现空白区域 。
\\n原因:SliverAppBar
的 expandedHeight
与内容高度不匹配。
\\n方案:使用 LayoutBuilder
动态计算高度:
SliverAppBar(\\n flexibleSpace: LayoutBuilder(\\n builder: (ctx, constraints) {\\n final height = constraints.maxHeight;\\n return AnimatedOpacity(\\n duration: Duration(milliseconds: 300),\\n opacity: height > 100 ? 1 : 0,\\n child: /*...*/,\\n );\\n },\\n ),\\n)\\n
\\n\\n\\n系统化认知框架:
\\n\\n
\\n- 组件关系:理解
\\nNestedScrollView
本质是协调两个独立的滚动控制器。- 布局原则:Header 使用
\\nSliver
组件,Body 使用CustomScrollView
保证一致性。- 性能关键:优先使用
\\nSliverChildBuilderDelegate
实现懒加载。- 调试技巧:通过
\\ndebugPaintSizeEnabled = true
可视化查看滚动区域边界 。
复杂吸顶导航
+ 分页懒加载
+ 滚动状态联动
场景:实现新闻资讯类App
,包含可折叠头图
、吸顶导航
(带二级分类
)、分页加载列表
、滚动时隐藏/显示浮动按钮
。
import \'package:flutter/material.dart\';\\nimport \'package:flutter/rendering.dart\';\\n\\nclass AdvancedCase1 extends StatefulWidget {\\n @override\\n _AdvancedCase1State createState() => _AdvancedCase1State();\\n}\\n\\nclass _AdvancedCase1State extends State<AdvancedCase1>\\n with SingleTickerProviderStateMixin {\\n final ScrollController _scrollController = ScrollController();\\n late TabController tabController;\\n bool _showFab = true;\\n int _page = 1;\\n List<String> _data = List.generate(20, (i) => \'初始数据 $i\');\\n\\n @override\\n void initState() {\\n super.initState();\\n _scrollController.addListener(_onScroll);\\n tabController = TabController(length: 3, vsync: this);\\n }\\n\\n void _onScroll() {\\n final maxScroll = _scrollController.position.maxScrollExtent;\\n final currentScroll = _scrollController.position.pixels;\\n\\n // 分页加载逻辑\\n if (currentScroll > maxScroll * 0.8) {\\n _loadMoreData();\\n }\\n\\n // 浮动按钮显示逻辑\\n final isScrollingDown = _scrollController.position.userScrollDirection ==\\n ScrollDirection.forward;\\n setState(() => _showFab = !isScrollingDown);\\n }\\n\\n Future<void> _loadMoreData() async {\\n await Future.delayed(Duration(seconds: 2));\\n setState(() {\\n _data.addAll(List.generate(10, (i) => \'新增数据 ${_page * 10 + i}\'));\\n _page++;\\n });\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n floatingActionButton: AnimatedOpacity(\\n opacity: _showFab ? 1 : 0,\\n duration: Duration(milliseconds: 300),\\n child: FloatingActionButton(\\n child: Icon(Icons.add),\\n onPressed: () {},\\n ),\\n ),\\n body: NestedScrollView(\\n controller: _scrollController,\\n headerSliverBuilder: (ctx, innerBoxIsScrolled) => [\\n SliverAppBar(\\n expandedHeight: 200,\\n pinned: true,\\n flexibleSpace: FlexibleSpaceBar(\\n background: Image.network(\\n \'https://picsum.photos/2000\',\\n fit: BoxFit.cover,\\n ),\\n ),\\n ),\\n SliverPersistentHeader(\\n pinned: true,\\n delegate: _StickyHeaderDelegate(\\n child: Container(\\n color: Colors.white,\\n child: TabBar(\\n controller: tabController,\\n tabs: [\'要闻\', \'科技\', \'财经\'].map((e) => Tab(text: e)).toList(),\\n ),\\n ),\\n ),\\n ),\\n ],\\n body: TabBarView(\\n controller: tabController,\\n children: [\\n _buildNewsList(\'news_list\'),\\n _buildNewsList(\'tech_list\'),\\n _buildNewsList(\'finance_list\'),\\n // _buildTechList(),\\n // _buildFinanceList(),\\n ],\\n ),\\n ),\\n );\\n }\\n\\n Widget _buildNewsList(String key) {\\n return CustomScrollView(\\n key: PageStorageKey(key),\\n slivers: [\\n SliverList(\\n delegate: SliverChildBuilderDelegate(\\n (ctx, i) => ListTile(\\n title: Text(_data[i]),\\n subtitle: Text(\'2023-09-20\'),\\n ),\\n childCount: _data.length,\\n ),\\n ),\\n SliverToBoxAdapter(\\n child:\\n _data.length > 100 ? Text(\'没有更多数据\') : CircularProgressIndicator(),\\n )\\n ],\\n );\\n }\\n}\\n\\nclass _StickyHeaderDelegate extends SliverPersistentHeaderDelegate {\\n final Widget child;\\n\\n _StickyHeaderDelegate({required this.child});\\n\\n @override\\n Widget build(ctx, shrinkOffset, overlapsContent) => child;\\n\\n @override\\n double get maxExtent => 48;\\n\\n @override\\n double get minExtent => 48;\\n\\n @override\\n bool shouldRebuild(covariant _StickyHeaderDelegate old) => false;\\n}\\n
\\n图示:
\\n嵌套横向滚动
+ 手势缩放
+ 视差效果
场景:实现图片社交App
的详情页,支持头部图片缩放
、横向滑动切换图片
、下方关联内容滚动
。
import \'package:flutter/material.dart\';\\n\\nclass AdvancedCase2 extends StatefulWidget {\\n @override\\n _AdvancedCase2State createState() => _AdvancedCase2State();\\n}\\n\\nclass _AdvancedCase2State extends State<AdvancedCase2> {\\n final PageController _pageController = PageController();\\n double _scale = 1.0;\\n double _prevScale = 1.0;\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n body: NestedScrollView(\\n headerSliverBuilder: (ctx, innerBoxIsScrolled) => [\\n SliverAppBar(\\n expandedHeight: 300,\\n flexibleSpace: GestureDetector(\\n onScaleStart: (d) => _prevScale = _scale,\\n onScaleUpdate: (d) => setState(() => _scale = _prevScale * d.scale),\\n child: Transform.scale(\\n scale: _scale,\\n child: PageView(\\n controller: _pageController,\\n children: [\\n Image.network(\'https://picsum.photos/2001\', fit: BoxFit.cover),\\n Image.network(\'https://picsum.photos/2002\', fit: BoxFit.cover),\\n Image.network(\'https://picsum.photos/2003\', fit: BoxFit.cover),\\n ],\\n ),\\n ),\\n ),\\n ),\\n ],\\n body: CustomScrollView(\\n slivers: [\\n SliverList(\\n delegate: SliverChildListDelegate([\\n _buildParallaxSection(),\\n _buildRelatedContent(),\\n ]),\\n )\\n ],\\n ),\\n ),\\n );\\n }\\n\\n Widget _buildParallaxSection() {\\n return LayoutBuilder(\\n builder: (ctx, constraints) {\\n return NotificationListener<ScrollUpdateNotification>(\\n onNotification: (notification) {\\n final scrollProgress = notification.metrics.pixels / 200;\\n // 实现视差动画逻辑\\n return true;\\n },\\n child: Container(\\n height: 400,\\n color: Colors.blue[100],\\n alignment: Alignment.center,\\n child: Text(\'视差效果区域\', style: TextStyle(fontSize: 24)),\\n ),\\n );\\n },\\n );\\n }\\n\\n Widget _buildRelatedContent() {\\n return ListView.builder(\\n physics: NeverScrollableScrollPhysics(),\\n shrinkWrap: true,\\n itemCount: 20,\\n itemBuilder: (ctx, i) => Card(\\n child: ListTile(title: Text(\'关联内容 $i\')),\\n ),\\n );\\n }\\n}\\n
\\n图示:
\\n动态浮动操作栏
+ 滚动到顶部按钮
+ 交互动画
场景:实现长文阅读页面,包含根据滚动位置变化的操作栏、智能显示返回顶部按钮。
\\nimport \'package:flutter/material.dart\';\\n\\nclass AdvancedCase3 extends StatefulWidget {\\n @override\\n _AdvancedCase3State createState() => _AdvancedCase3State();\\n}\\n\\nclass _AdvancedCase3State extends State<AdvancedCase3> {\\n final ScrollController _scrollController = ScrollController();\\n bool _showBackTop = false;\\n double _appBarOpacity = 1.0;\\n\\n @override\\n void initState() {\\n super.initState();\\n _scrollController.addListener(() {\\n final offset = _scrollController.offset;\\n setState(() {\\n // 顶部渐变逻辑\\n _appBarOpacity = (100 - offset.clamp(0, 100)) / 100;\\n });\\n // 显示返回顶部按钮\\n print(\\"---\x3e$offset\\");\\n setState(() {\\n _showBackTop = offset > 200;\\n });\\n });\\n }\\n\\n void _scrollToTop() {\\n _scrollController.animateTo(\\n 0,\\n duration: Duration(milliseconds: 600),\\n curve: Curves.easeInOut,\\n );\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n body: NestedScrollView(\\n controller: _scrollController,\\n headerSliverBuilder: (ctx, innerBoxIsScrolled) => [\\n SliverAppBar(\\n expandedHeight: 180,\\n pinned: true,\\n flexibleSpace: AnimatedOpacity(\\n opacity: _appBarOpacity,\\n duration: Duration(milliseconds: 200),\\n child: FlexibleSpaceBar(\\n title: Text(\\n \\"复杂交互置顶\\",\\n style: TextStyle(color: Colors.redAccent),\\n ),\\n background: Image.network(\\n \'https://picsum.photos/2004\',\\n fit: BoxFit.cover,\\n ),\\n ),\\n ),\\n ),\\n ],\\n body: CustomScrollView(\\n slivers: [\\n SliverList(\\n delegate: SliverChildBuilderDelegate(\\n (ctx, index) => Container(\\n height: 100,\\n color: Colors.primaries[index % 18],\\n alignment: Alignment.center,\\n child: Text(\\"Item $index\\"),\\n ),\\n childCount: 15,\\n ),\\n )\\n ],\\n ),\\n ),\\n floatingActionButton: AnimatedSlide(\\n duration: Duration(milliseconds: 300),\\n offset: _showBackTop ? Offset.zero : Offset(0, 2),\\n child: FloatingActionButton(\\n onPressed: _scrollToTop,\\n child: Icon(\\n Icons.arrow_upward,\\n color: Colors.redAccent,\\n ),\\n ),\\n ),\\n );\\n }\\n}\\n
\\n图示:
\\nNestedScrollView
的精髓在于将复杂问题模块化:顶部Sliver
负责\\"浮动层\\"
的精致交互,Body
区域专注主体内容的高效渲染。开发者需牢记两个黄金法则:
Header
处理动态布局,Body
处理数据驱动。SliverChildBuilderDelegate
的lazy
加载。正如乐高积木的拼接艺术,掌握NestedScrollView
的核心原理后,你便能游刃有余地组合出任何天马行空的滚动交互设计。
\\n\\n系统性认知 > 零散技巧,这才是突破
\\nFlutter
进阶瓶颈的关键。
\\n","description":"前言 滚动布局是用户交互的核心场景之一,但当遇到多层级嵌套滚动(如顶部导航栏吸顶、下拉刷新与分页加载结合)时,开发者往往陷入手势冲突、滚动错位的困境。ListView或CustomScrollView虽然强大,却难以协调多个滚动区域的联动关系。\\n\\nNestedScrollView如同一名精密的\\"指挥家\\",能够优雅地协调SliverAppBar、TabBarView、ListView等组件的协作。但许多初学者因缺乏系统性认知,仅停留在基础API调用层面,未能发挥其真正威力。\\n\\n本文将带你穿透表象,直击本质,通过3个实战案例,彻底掌握如何用NestedScrol…","guid":"https://juejin.cn/post/7477869412277403648","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-05T00:58:22.155Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/05c22c2a90ed431591cd0e7c1d682ee7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1741741102&x-signature=lYhz4u9KRi610HPs9huo5BSSSPo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3655cbd492a84e38b954ef62c5ecf670~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1741741102&x-signature=t99foBlNofHxIegjJuLin4mTsKI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3eb6d0c6feaf447b9bba81d9327ed735~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1741741102&x-signature=XF3Vwu3ht3S92dKKeWJgQsbdXJs%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"系统化掌握Flutter组件之TabBar","url":"https://juejin.cn/post/7477601578830413824","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
在移动应用开发中,超过83%的App
采用标签栏导航,但开发者常常陷入\\"能跑就行\\"
的思维陷阱。当你的TabBar
出现指示器抖动
、滑动不同步
、样式混乱
时,是否意识到这源于对组件体系理解的碎片化?传统教学往往孤立讲解属性参数,却忽视了TabBar
与TabBarView
的联动机制、滑动冲突解决方案
等关键系统化认知。
本文将带你经历从原子属性到复杂交互
的完整认知跃迁,让你不仅掌握参数配置,更能驾驭企业级复杂场景的实现。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\nTabBar
属性详解const TabBar({\\n super.key,\\n required this.tabs, // 必需参数,定义标签集合(通常使用Tab组件数组)\\n this.controller, // 选项卡控制器,用于同步TabBar和TabBarView的状态\\n this.isScrollable = false, // 是否启用横向滚动(false=等分宽度,true=自适应宽度+滚动)\\n this.padding, // 整个TabBar容器的内边距(控制整体布局位置)\\n this.indicatorColor, // 指示器默认颜色(当indicator未指定时生效)\\n this.automaticIndicatorColorAdjustment = true, // 是否自动调整指示器颜色(默认为true,根据主题色自动适配)\\n this.indicatorWeight = 2.0, // 指示器线条厚度(单位:逻辑像素)\\n this.indicatorPadding = EdgeInsets.zero, // 指示器与标签的内边距(微调指示器位置)\\n this.indicator, // 自定义指示器装饰(可替代默认的线条样式)\\n this.indicatorSize, // 指示器尺寸策略(TabBarIndicatorSize.tab/label)\\n this.dividerColor, // 标签之间的分隔线颜色\\n this.dividerHeight, // 分隔线高度(默认与标签高度相同)\\n this.labelColor, // 选中状态标签颜色(优先级高于默认主题色)\\n this.labelStyle, // 选中状态文本样式(覆盖默认TextStyle)\\n this.labelPadding, // 标签内部文字/图标的边距(微调内容布局)\\n this.unselectedLabelColor, // 未选中状态标签颜色\\n this.unselectedLabelStyle, // 未选中状态文本样式\\n this.dragStartBehavior = DragStartBehavior.start, // 拖拽行为配置(处理触摸事件细节)\\n this.overlayColor, // 水波纹/高亮覆盖色(Material状态效果)\\n this.mouseCursor, // 鼠标悬停时的指针样式(跨平台兼容性处理)\\n this.enableFeedback, // 是否启用触觉/音效反馈(Android/iOS平台特性)\\n this.onTap, // 标签点击事件回调(返回点击的索引位置)\\n this.physics, // 滚动行为控制器(如ClampingScrollPhysics等)\\n this.splashFactory, // 自定义水波纹效果实现(继承InteractiveInkFeatureFactory)\\n this.splashBorderRadius, // 水波纹效果的圆角边界(需配合splashFactory使用)\\n this.tabAlignment, // 标签对齐方式(仅isScrollable=false时生效)\\n this.textScaler, // 字体缩放比例(支持系统字体大小设置)\\n this.indicatorAnimation, // 指示器切换动画控制器(高级自定义动画使用)\\n })\\n
\\n关键参数补充说明:
\\ncontroller
:必须与关联的TabBarView
共享同一个控制器,且需通过TickerProviderStateMixin
管理生命周期。indicator
:当自定义装饰时,推荐使用BoxDecoration
或继承Decoration
实现高级动画效果。labelStyle/unselectedLabelStyle
:设置fontSize
时需注意双平台设计规范(iOS
通常比Android
大1pt
)。physics
:在嵌套滚动场景中建议设置为NeverScrollableScrollPhysics
避免手势冲突。splashBorderRadius
:需要与InkRipple.splashFactory
配合使用才能生效。indicatorAnimation
:用于实现非线性的指示器切换动画(需配合自定义动画曲线使用
)。TabBar
由状态控制器与视觉描述组件共同驱动,理解其技术实现层级是掌握用法的关键:
TabBar(\\n controller: _tabController, // 状态控制核心\\n tabs: _buildTabs(), // 子组件集合\\n // 其他视觉参数...\\n)\\n
\\n核心组件解析:
\\ncontroller
:管理选项卡切换状态
、动画驱动
和生命周期
。tabs
:定义选项卡的静态结构
,支持动态更新
。颜色
、尺寸
、指示器
等外观特征。(1)controller
:状态管理
late TabController _controller;\\n\\n@override\\nvoid initState() {\\n super.initState();\\n _controller = TabController(\\n length: 3, \\n vsync: this, // 需混入TickerProviderStateMixin\\n );\\n}\\n
\\ndispose()
释放资源。addListener()
监听选项卡切换事件。最佳实践:
\\n// 同步TabBar与TabBarView\\nTabBarView(\\n controller: _controller,\\n children: [...],\\n)\\n
\\n(2)isScrollable
:布局模式
isScrollable: true // 启用横向滚动\\n
\\n布局差异:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n模式 | 实现原理 | 适用场景 |
---|---|---|
false | 等分父容器宽度 | 固定少量选项卡 |
true | 内容自适应+横向滚动 | 动态/多选项卡 |
源码实现逻辑:
\\n// 简化后的布局逻辑\\nif (isScrollable) {\\n return SingleChildScrollView(...);\\n} else {\\n return Row(\\n children: tabs.map((tab) => Expanded(...)).toList()\\n );\\n}\\n
\\n(3)indicator
:指示器样式
indicator: BoxDecoration(\\n border: Border(bottom: BorderSide(width: 2)),\\n color: Colors.blue,\\n)\\n
\\n动态指示器示例:
\\nAnimationController _animationController;\\n\\nindicator: Decoration(\\n color: Colors.blue.withOpacity(_animationController.value),\\n)\\n
\\n(4)physics
:滚动行为
physics: BouncingScrollPhysics() // iOS风格弹性滚动\\n
\\nClampingScrollPhysics
:Android
风格。NeverScrollableScrollPhysics
:禁用滚动。PageScrollPhysics
:分页效果。错误1:控制器未同步
\\n// 错误:TabBar与TabBarView使用不同controller\\nTabBar(controller: _ctrl1)\\nTabBarView(controller: _ctrl2)\\n\\n// 修正:共享同一控制器\\nfinal _controller = TabController(length: 3, vsync: this);\\n
\\n错误2:动态更新处理不当
\\n// 错误:直接修改tabs数量不更新controller\\nsetState(() => tabs = newTabs);\\n\\n// 正确流程:\\nvoid updateTabs() {\\n _controller.dispose();\\n _controller = TabController(length: newTabs.length, vsync: this);\\n setState(() => tabs = newTabs);\\n}\\n
\\n错误3:布局约束冲突
\\n// 错误:未限定父容器宽度\\nContainer(\\n child: TabBar(isScrollable: false) // 需要明确宽度约束\\n)\\n\\n// 修正方案:\\nSizedBox(\\n width: MediaQuery.of(context).size.width,\\n child: TabBar(...)\\n)\\n
\\nTabBar
实现DefaultTabController(\\n length: 3,\\n child: Scaffold(\\n appBar: AppBar(\\n title: const Text(\'基础TabBar示例\'),\\n bottom: const TabBar(\\n tabs: [\\n Tab(icon: Icon(Icons.cloud), text: \\"天气\\"),\\n Tab(icon: Icon(Icons.message), text: \\"消息\\"),\\n Tab(icon: Icon(Icons.settings), text: \\"设置\\"),\\n ],\\n indicatorColor: Colors.red,\\n labelColor: Colors.redAccent,\\n unselectedLabelColor: Colors.blueGrey,\\n ),\\n ),\\n body: const TabBarView(\\n children: [\\n Center(child: Text(\'天气页面\')),\\n Center(child: Text(\'消息页面\')),\\n Center(child: Text(\'设置页面\')),\\n ],\\n ),\\n ),\\n)\\n
\\nimport \'package:flutter/material.dart\';\\n\\nclass TabBarDemo extends StatefulWidget {\\n const TabBarDemo({super.key});\\n\\n @override\\n State<TabBarDemo> createState() => _TabBarDemoState();\\n}\\n\\nclass _TabBarDemoState extends State<TabBarDemo>\\n with SingleTickerProviderStateMixin {\\n late TabController _controller;\\n\\n @override\\n void initState() {\\n _controller = TabController(\\n length: 4,\\n vsync: this,\\n );\\n _controller.addListener(_handleTabChange);\\n }\\n\\n void _handleTabChange() {\\n debugPrint(\'当前索引: ${_controller.index}\');\\n }\\n\\n @override\\n void dispose() {\\n _controller.dispose();\\n super.dispose();\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: Text(\\"自定义控制器示例\\"),\\n backgroundColor: Theme.of(context).colorScheme.inversePrimary,\\n bottom: TabBar(\\n controller: _controller,\\n tabs: tabs(),\\n indicatorColor: Colors.red,\\n labelColor: Colors.redAccent,\\n unselectedLabelColor: Colors.blueGrey,\\n ),\\n ),\\n body: TabBarView(\\n controller: _controller,\\n children: const [\\n Center(child: Text(\'首页内容\')),\\n Center(child: Text(\'发现内容\')),\\n Center(child: Text(\'通知列表\')),\\n Center(child: Text(\'个人中心\')),\\n ],\\n ),\\n );\\n }\\n\\n List<Widget> tabs() {\\n return const [\\n Tab(text: \'首页\'),\\n Tab(text: \'发现\'),\\n Tab(text: \'通知\'),\\n Tab(text: \'我的\'),\\n ];\\n }\\n}\\n
\\nTab
生成系统import \'package:flutter/material.dart\';\\n\\n// 模拟远程配置数据模型\\nclass RemoteTabConfig {\\n final String title;\\n final IconData icon;\\n\\n RemoteTabConfig(this.title, this.icon);\\n}\\n\\nclass DynamicTabDemo extends StatefulWidget {\\n const DynamicTabDemo({super.key});\\n\\n @override\\n State<DynamicTabDemo> createState() => _DynamicTabDemoState();\\n}\\n\\nclass _DynamicTabDemoState extends State<DynamicTabDemo>\\n with TickerProviderStateMixin {\\n late TabController _controller;\\n List<RemoteTabConfig> _tabs = [];\\n\\n @override\\n void initState() {\\n super.initState();\\n _loadTabs();\\n }\\n\\n Future<void> _loadTabs() async {\\n // 模拟网络请求\\n await Future.delayed(const Duration(seconds: 1));\\n final newTabs = [\\n RemoteTabConfig(\'新闻\', Icons.article),\\n RemoteTabConfig(\'视频\', Icons.video_camera_back),\\n RemoteTabConfig(\'音乐\', Icons.music_note),\\n RemoteTabConfig(\'直播\', Icons.live_tv),\\n ];\\n\\n if (mounted) {\\n setState(() {\\n _tabs = newTabs;\\n _controller = TabController(\\n length: _tabs.length,\\n vsync: this,\\n );\\n });\\n }\\n }\\n\\n @override\\n void dispose() {\\n _controller.dispose();\\n super.dispose();\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: const Text(\'动态Tab示例\'),\\n bottom: _tabs.isEmpty\\n ? null\\n : TabBar(\\n controller: _controller,\\n // isScrollable: true,\\n tabs: _tabs\\n .map((tab) => Tab(\\n icon: Icon(tab.icon),\\n text: tab.title,\\n ))\\n .toList(),\\n ),\\n ),\\n body: _tabs.isEmpty\\n ? const Center(child: CircularProgressIndicator())\\n : TabBarView(\\n controller: _controller,\\n children: _tabs\\n .map(\\n (tab) => KeepAlivePage(\\n title: tab.title,\\n ),\\n )\\n .toList(),\\n ),\\n );\\n }\\n}\\n\\nclass KeepAlivePage extends StatefulWidget {\\n final String title;\\n\\n const KeepAlivePage({super.key, required this.title});\\n\\n @override\\n State<KeepAlivePage> createState() => _KeepAlivePageState();\\n}\\n\\nclass _KeepAlivePageState extends State<KeepAlivePage>\\n with AutomaticKeepAliveClientMixin {\\n @override\\n bool get wantKeepAlive => true;\\n\\n @override\\n Widget build(BuildContext context) {\\n super.build(context);\\n return Center(\\n child: Text(\'${widget.title}页面内容\'),\\n );\\n }\\n}\\n
\\nNestedScrollView+TabBar
)import \'package:flutter/material.dart\';\\n\\nclass NestedDemo extends StatelessWidget {\\n const NestedDemo({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n return DefaultTabController(\\n length: 3,\\n child: Scaffold(\\n body: NestedScrollView(\\n headerSliverBuilder: (context, innerBoxIsScrolled) => [\\n SliverOverlapAbsorber(\\n handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),\\n sliver: SliverAppBar(\\n title: const Text(\\n \'嵌套滚动示例\',\\n style: TextStyle(color: Colors.white),\\n ),\\n floating: true,\\n pinned: true,\\n snap: true,\\n expandedHeight: 200,\\n flexibleSpace: FlexibleSpaceBar(\\n background: Image.network(\\n \'https://picsum.photos/600/400?random=1\',\\n fit: BoxFit.cover,\\n ),\\n ),\\n bottom: const TabBar(\\n tabs: [\\n Tab(text: \'热门\'),\\n Tab(text: \'推荐\'),\\n Tab(text: \'最新\'),\\n ],\\n ),\\n ),\\n ),\\n ],\\n body: TabBarView(\\n children: [\\n _buildScrollPage(\'热门内容\'),\\n _buildScrollPage(\'推荐内容\'),\\n _buildScrollPage(\'最新内容\'),\\n ],\\n ),\\n ),\\n ),\\n );\\n }\\n\\n Widget _buildScrollPage(String title) {\\n return Builder(\\n builder: (context) {\\n return CustomScrollView(\\n slivers: [\\n SliverOverlapInjector(\\n handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),\\n ),\\n SliverList(\\n delegate: SliverChildBuilderDelegate(\\n (context, index) => ListTile(\\n title: Text(\'$title - 项目 $index\'),\\n ),\\n childCount: 50,\\n ),\\n ),\\n ],\\n );\\n },\\n );\\n }\\n}\\n
\\nTabBar
不是孤立的存在,而是Flutter
组件生态的微缩宇宙。真正的精通不在于记忆参数,而是建立\\"声明式配置-控制器驱动-渲染管线\\"
三位一体的思维模型。当你下次面对复杂交互需求时,所有视觉表现都是数据的函数,所有交互都是状态流转的具象化。带着这种系统思维去解构其他组件,你将获得指数级的学习加速度。
\\n","description":"前言 在移动应用开发中,超过83%的App采用标签栏导航,但开发者常常陷入\\"能跑就行\\"的思维陷阱。当你的TabBar出现指示器抖动、滑动不同步、样式混乱时,是否意识到这源于对组件体系理解的碎片化?传统教学往往孤立讲解属性参数,却忽视了TabBar与TabBarView的联动机制、滑动冲突解决方案等关键系统化认知。\\n\\n本文将带你经历从原子属性到复杂交互的完整认知跃迁,让你不仅掌握参数配置,更能驾驭企业级复杂场景的实现。\\n\\n操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。\\n\\n一、基础认知:\\n1.1、TabBar属性详解\\nconst TabBar({…","guid":"https://juejin.cn/post/7477601578830413824","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-04T08:59:50.941Z","media":null,"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"腾讯 TDF 即将开源 Kuikly 跨端框架,Kotlin 支持全平台","url":"https://juejin.cn/post/7477601578829627392","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
今天,在腾讯的 Shiply 平台看 Flutter 动态化自研框架 Conch 时,在侧边栏看到了有「跨端开发框架」的介绍,点开发现有两个产品:
\\n关于 Hippy 在之前就大概了解过,属于 Web 开发体验的开源的跨端开发框架,但是 Kuikly 又是什么?
\\n通过查找,在 openhippy 的官网可以看到,原来 Kuikly 是基于 Kotlin KMM(Kotlin Multiplatform Mobile) 技术实现的客户端友好跨端方案,可以使用 Kotlin 原生开发语言创建 Android 、iOS、、H5、小程序和 PC 应用,属于 TDF (Tencent Device-oriented Framework)的全新跨平台方案。
\\n而从目前已有的产品介绍看,Kuikly 是采用类 Compose 和 SwiftUI 的声明式+响应式的开发模式,框架输出的产物有:
\\n运行时会映射到系统原生控件渲染,跟系统原生控件体验完全一致,最重要的是,Android 平台实现了基于dex 动态下发支持,iOS平台基于 JS 动态下发,也就是完全支持热更新,动态话能力可以依托腾讯自家的 Shiply 。
\\n\\n\\n看起来为了实现动态,在 iOS 平台使用的是 Kotlin/JS 。
\\n
同时,App 极度的轻量化,使用 Kuikly 的安装包增量仅 300K,运行时额外的内存占用几乎为零,从这点看大小和内存基本不会是 Kuikly 的门槛。
\\n在查阅资料后才发现,2023 年的时候,「腾讯技术工程」团队就在知乎分享过 Kuikly 的实现,Kuikly(Kotlin UI Kit,发音同 quickly),项目就是使用 Kotlin 开发了自己的一套声明式 UI 框架,同时映射到系统原生控件做渲染,最终用 KMM(Kotlin Multiplatform Mobile)实现跨端。
\\n而对于 Kuikly ,它从业务代码、UI 框架、布局层以及渲染层全部使用 Kotlin 语言(iOS 渲染层是 OC),其中Android 端通过 KMM 编译成 SO 文件,而,iOS 端可以编译成 JS,不过那也是两年前的情况。
\\n\\n\\n可以看到当时腾讯几乎是用了自己的 UI 框架实现而非直接使用了 Compose MultiPlatform,不知道现在是否还是如此?
\\n
而从现在看来,依托 KMP 项目的成熟,目前 Kuikly 很大可能已经支持可 Kotlin Native? 并且从预告看,已经支持了鸿蒙平台,那么大概率不是 Kotlin/JS 就是 Kotlin Native 。
\\n\\n\\n如果是为了动态化,可能还是 Kotlin/JS 的概率大一些?
\\n
如下图所示,是过去 Kuikly 过去在知乎发过的代码编写情况,看起来基本上有着浓浓的 Compose 的熟悉味道:
\\n\\n\\n这时候可能路过的 iOS 表示:为什么大厂弄跨平台甚至直至鸿蒙都是 Kotlin 不是 Swift ?
\\n
而 Kuikly 表示,其核心的设计思路是让 native 的渲染层尽量的薄,所以他们把布局和复杂 UI 控件封装都放在了跨端的 Kotlin 侧,native 层只有对原生基础控件的简单映射,这样也能尽量减少因为两端代码不一致导致的功能和体验不一致问题。
\\n这是两年前 Kuikly 提供的数据对比,基本和原始开发保持一致:
\\n另外,通过代码量对比,腾讯技术工程团队表示:同一个页面使用 Kotlin 和 OC 开发两端的代码量,是使用 Kuikly 跨端开发的代码量的 3 倍,同时腾讯还发布了 Kuikly 与类 RN 和 Flutter 的对比:
\\n那么 2025 年的今天,Kuikly 是否还是使用全自研发的 UI 框层?还是已经接入 Compose MultiPlatform ? 从渲染实现上考虑,看起来还是映射的可能性更大?毕竟还有考虑动态化支持,具体还是要等项目正式开源后才知道了。
\\nOverlay 创建-》展示-》移除
\\nOverlayEntry? _compressDeleteOverLay;\\nvoid showCompressOverlay() {\\n // 创建OverlayEntry\\n _compressDeleteOverLay = OverlayEntry(\\n builder: (context) =>\\n Positioned(\\n bottom: 0,\\n left: 0,\\n right: 0,\\n child: Obx(()=>ClickListener(\\n onTap: (){\\n jumpCompressFileDeletePage();\\n },\\n child: CommonDeleteView(\\n count: totalCompressDeleteCount.value,\\n size: totalCompressDeleteSize.value,\\n initState: true,\\n ),\\n )),\\n ),\\n );\\n Overlay.of(navigatorKey.currentContext!).insert(_compressDeleteOverLay!);\\n}\\n\\nvoid removeCompressDeleteOverlay(){\\n _compressDeleteOverLay?.remove();\\n _compressDeleteOverLay = null;\\n}\\n
\\nOverlayPortal 创建-》展示-》隐藏(通过OverlayPortalController 控制)
\\nOverlayPortal(\\n controller: controller.overlayPortalController, //OverlayPortalController\\n overlayChildBuilder: (BuildContext context) {\\n return Positioned(\\n right: 0,\\n bottom: 0,\\n left: 0,\\n child: Obx(()=>ClickListener(\\n onTap: (){\\n controller.jumpCompressFileDeletePage();\\n },\\n child: CommonDeleteView(count: controller.totalCompressDeleteCount.value,\\n size: controller.totalCompressDeleteSize.value,\\n initState: true,\\n controller: controller.fileCompressButtonController),\\n )),\\n );\\n }\\n)\\n
\\n简单可以理解为:Overlay 做全局弹窗 OverlayPortal 做单页面内部弹窗
\\n","description":"Overlay 创建-》展示-》移除 OverlayEntry? _compressDeleteOverLay;\\nvoid showCompressOverlay() {\\n // 创建OverlayEntry\\n _compressDeleteOverLay = OverlayEntry(\\n builder: (context) =>\\n Positioned(\\n bottom: 0,\\n left: 0,\\n right: 0,\\n child: Obx(()=>Cl…","guid":"https://juejin.cn/post/7477541534638161959","author":"火柴就是我","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-04T05:23:28.906Z","media":null,"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"系统化掌握Flutter组件之CustomScrollView:“一统江湖”的秘密武器","url":"https://juejin.cn/post/7477688695286628406","content":"在Flutter
开发中,你是否曾因简单的ListView
无法实现复杂嵌套滚动而抓狂?是否在面对需要动态切换网格与列表布局时感到束手无策?或者,当产品经理提出\\"视差滚动+吸顶导航+动态加载\\"
的组合需求时,你的代码逐渐失控?
这一切的答案,都藏在CustomScrollView
的魔法盒子里。作为Flutter
滚动系统的终极武器,它通过Sliver
协议将布局原子化,赋予开发者无限的可能性。但为何许多开发者对它望而却步?因为它不仅需要你理解RenderObject
的底层逻辑,更需要一种全新的\\"分形布局思维\\"
。
本文将通过六维知识体系,深入剖析其底层机制,揭秘如何通过Sliver
协议构建弹性滚动体系,并通过企业级最佳实践案例,让你真正掌握这个\\"一统江湖\\"
的滚动组件。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\nSliver
组件全解析SliverAppBar
属性全解const SliverAppBar({\\n super.key,\\n this.leading, 左侧操作按钮,通常是返回键\\n this.automaticallyImplyLeading = true, // 是否自动隐藏导航栏返回箭头(当存在leading时)\\n this.title, // 导航栏中央标题\\n this.actions, // 右侧操作按钮组\\n this.flexibleSpace, // 展开/折叠时的弹性空间组件(通常使用FlexibleSpaceBar实现视差滚动)\\n this.bottom, // 底部固定组件(通常为TabBar)\\n this.elevation, // 导航栏阴影高度\\n this.scrolledUnderElevation, // 导航栏滚动至屏幕下方时的阴影高度\\n this.shadowColor, // 阴影颜色\\n this.surfaceTintColor, // 导航栏下方区域的表面色(滚动时覆盖颜色)\\n this.forceElevated = false, // 强制显示阴影(即使导航栏在顶部)\\n this.backgroundColor, // 导航栏背景色\\n this.foregroundColor, // 导航栏文字颜色\\n this.iconTheme, // 导航栏图标主题(如返回箭头样式)\\n this.actionsIconTheme, // 右侧操作按钮的主题\\n this.primary = true, // 是否为主滚动视图的一部分(影响滚动手势优先级)\\n this.centerTitle, // 标题是否居中对齐\\n this.excludeHeaderSemantics = false, // 是否排除导航栏的语义标签(影响辅助功能)\\n this.titleSpacing, // 标题与左侧控件间的水平间距\\n this.collapsedHeight, // 折叠状态下的导航栏高度\\n this.expandedHeight, // 展开状态下的导航栏高度\\n this.floating = false, // 下拉时是否自动展开\\n this.pinned = false, // 折叠后是否保持固定在屏幕顶部\\n this.snap = false, // 松手时是否吸附到展开/折叠状态\\n this.stretch = false, // 是否支持拉伸超过边界滚动\\n this.stretchTriggerOffset = 100.0, // 触发拉伸行为的滚动偏移阈值\\n this.onStretchTrigger, // 拉伸触发时的回调函数\\n this.shape, // 导航栏形状(如圆角矩形)\\n this.toolbarHeight = kToolbarHeight, // 导航栏高度(默认平台特定值)\\n this.leadingWidth, // leading控件的宽度(如返回按钮)\\n this.toolbarTextStyle, // 导航栏工具栏区域的文本样式\\n this.titleTextStyle, // 标题文本的特定样式\\n this.systemOverlayStyle, // 系统覆盖层样式(如状态栏颜色)\\n this.forceMaterialTransparency = false, // 是否强制使用透明材料样式\\n this.clipBehavior, // 剪裁行为(如剪裁超出部分)\\n})\\n
\\nFlexibleSpaceBar
属性全解const FlexibleSpaceBar({\\n super.key,\\n this.title, // 标题文本或组件(居中显示在弹性空间区域)\\n this.background, // 背景组件(通常为Image或渐变层)\\n this.centerTitle = true, // 标题是否居中对齐(默认true)\\n this.titlePadding, // 标题区域的额外内边距(默认无)\\n this.collapseMode = CollapseMode.parallax, // 折叠动画模式:\\n // - parallax(视差滚动,背景缓慢移动)\\n // - fade(背景淡入淡出)\\n // - scroll(背景随滚动平滑移动)\\n this.stretchModes = const <StretchMode>[StretchMode.zoomBackground], // 背景拉伸模式(可组合使用):\\n // - zoomBackground:背景缩放\\n // - blurBackground:背景高斯模糊\\n // - none:无拉伸效果\\n this.expandedTitleScale = 1.5, // 展开状态下标题的缩放比例(默认1.5倍)\\n})\\n
\\nSliver
组件全景图组件类型 | 核心功能 | 典型场景 |
---|---|---|
基础布局类 | SliverAppBar 、SliverList 、SliverGrid | 首屏导航、商品列表、瀑布流 |
辅助渲染类 | SliverToBoxAdapter 、SliverPadding | 嵌入普通组件、统一边距 |
高级交互类 | SliverPersistentHeader 、SliverAnimatedList | 悬浮头部、动态加载列表 |
性能优化类 | SliverFillRemaining 、SliverOffstage | 填充剩余空间、组件懒加载 |
视觉控制类 | SliverOpacity 、SliverVisibility | 动态透明度、条件渲染 |
嵌套滚动类 | SliverOverlapInjector 、SliverMainAxisGroup | 多视图嵌套、跨轴布局 |
SliverAppBar
:智能折叠导航栏SliverAppBar(\\n leading: Icon(Icons.arrow_back),\\n expandedHeight: 200,\\n pinned: true, // 折叠后保持可见\\n flexibleSpace: FlexibleSpaceBar(\\n title: Text(\\n \'SliverAppBar\',\\n style: TextStyle(color: Colors.green),\\n ),\\n background: Image.asset(\\"assets/images/product.webp\\", fit: BoxFit.cover),\\n ),\\n),\\n
\\nSliverPersistentHeader
:悬浮固定头部class StickyHeaderDelegate extends SliverPersistentHeaderDelegate {\\n @override\\n Widget build(\\n BuildContext context, double shrinkOffset, bool overlapsContent) {\\n return Container(\\n height: 60,\\n color: Colors.white,\\n child: Row(\\n children: [\\n Expanded(child: Text(\'消息\')),\\n IconButton(\\n icon: Icon(Icons.search),\\n onPressed: () {},\\n ),\\n ],\\n ),\\n );\\n }\\n\\n @override\\n double get maxExtent => 60;\\n\\n @override\\n double get minExtent => 60;\\n\\n @override\\n bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {\\n return oldDelegate.maxExtent != maxExtent &&\\n oldDelegate.minExtent != minExtent;\\n }\\n}\\n\\n// 使用方式\\nSliverPersistentHeader(\\n delegate: StickyHeaderDelegate(),\\n pinned: true,\\n floating: false,\\n),\\n
\\nSliverList/SliverFixedExtentList
:高性能列表布局// 动态高度列表\\nSliverList(\\n delegate: SliverChildBuilderDelegate(\\n (_, index) => Container(\\n height: 100,\\n color: Colors.primaries[index % 18],\\n alignment: Alignment.center,\\n child: Text(\\"Item $index\\"),\\n ),\\n childCount: 20,\\n ),\\n),\\n\\n//固定高度列表(性能更优)\\nSliverFixedExtentList(\\n itemExtent: 80, // 明确指定高度\\n delegate: SliverChildBuilderDelegate(\\n (_, index) => Container(\\n height: 80,\\n color: Colors.primaries[index % 18],\\n alignment: Alignment.center,\\n child: Text(\\"Item $index\\"),\\n ),\\n childCount: 20,\\n ),\\n),\\n
\\nSliverAnimatedList
:动态列表动画final GlobalKey<SliverAnimatedListState> _listKey = GlobalKey();\\n\\n// 动态添加新消息\\nvoid _addMessage() {\\n _listKey.currentState!.insertItem(0);\\n}\\n\\n// 动画列表\\nSliverAnimatedList(\\n key: _listKey,\\n itemBuilder: (_, index, animation) {\\n return SizeTransition(\\n axis: Axis.vertical,\\n sizeFactor: animation,\\n child: ListTile(title: Text(\'消息 $index\')),\\n );\\n },\\n)\\n
\\nSliverGrid
:高性能网格布局SliverGrid(\\n gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(\\n maxCrossAxisExtent: 200,\\n mainAxisSpacing: 10,\\n crossAxisSpacing: 10,\\n childAspectRatio: 0.8,\\n ),\\n delegate: SliverChildBuilderDelegate(\\n (_, index) => Container(\\n height: 80,\\n color: Colors.primaries[index % 18],\\n alignment: Alignment.center,\\n child: Text(\\"Item $index\\"),\\n ),\\n childCount: 20,\\n ),\\n),\\n
\\nSliverToBoxAdapter
:普通Widget
转Sliver
SliverToBoxAdapter(\\n child: Container(\\n height: 200,\\n color: Colors.blue,\\n child: Text(\'非滑动区域\'),\\n ),\\n),\\n
\\nSliverAppBar
的颜色import \'package:flutter/material.dart\';\\n\\nclass DynamicColorSliverAppBarPage extends StatefulWidget {\\n const DynamicColorSliverAppBarPage({super.key});\\n\\n @override\\n _DynamicColorSliverAppBarPageState createState() =>\\n _DynamicColorSliverAppBarPageState();\\n}\\n\\nclass _DynamicColorSliverAppBarPageState\\n extends State<DynamicColorSliverAppBarPage> {\\n Color _appBarColor = Colors.transparent;\\n ScrollController _scrollController = ScrollController();\\n final String url =\\n \\"https://img1.sycdn.imooc.com/szimg/650413e409dc4db412000676-360-202.jpg\\";\\n\\n @override\\n void initState() {\\n super.initState();\\n _scrollController.addListener(() {\\n double offset = _scrollController.offset;\\n if (offset > 100) {\\n setState(() {\\n _appBarColor = Colors.blue;\\n });\\n } else {\\n setState(() {\\n _appBarColor = Colors.transparent;\\n });\\n }\\n });\\n }\\n\\n @override\\n void dispose() {\\n _scrollController.dispose();\\n super.dispose();\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n body: CustomScrollView(\\n controller: _scrollController,\\n slivers: [\\n SliverAppBar(\\n expandedHeight: 200.0,\\n floating: false,\\n pinned: true,\\n backgroundColor: _appBarColor,\\n flexibleSpace: FlexibleSpaceBar(\\n title: const Text(\'Dynamic Color SliverAppBar\'),\\n background: Image.network(\\n url,\\n fit: BoxFit.cover,\\n ),\\n ),\\n ),\\n SliverList(\\n delegate: SliverChildBuilderDelegate(\\n (BuildContext context, int index) {\\n return ListTile(\\n title: Text(\'Item $index\'),\\n );\\n },\\n childCount: 20,\\n ),\\n ),\\n ],\\n ),\\n );\\n }\\n}\\n
\\n列表/网格
)import \'package:flutter/material.dart\';\\n\\nenum LayoutType { list, grid }\\n\\nclass DynamicLayoutPage extends StatefulWidget {\\n @override\\n _DynamicLayoutPageState createState() => _DynamicLayoutPageState();\\n}\\n\\nclass _DynamicLayoutPageState extends State<DynamicLayoutPage> {\\n final ValueNotifier<LayoutType> _layoutType = ValueNotifier(LayoutType.list);\\n final ScrollController _scrollController = ScrollController();\\n final List<Color> _colors = List.generate(\\n 100, (i) => Color.lerp(Colors.blue, Colors.green, i / 100)!);\\n\\n @override\\n void dispose() {\\n _scrollController.dispose();\\n super.dispose();\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(body: _buildContent());\\n }\\n\\n Widget _buildLayoutSwitch() {\\n return ValueListenableBuilder<LayoutType>(\\n valueListenable: _layoutType,\\n builder: (_, type, __) => SegmentedButton<LayoutType>(\\n segments: const [\\n ButtonSegment(\\n value: LayoutType.list,\\n icon: Icon(Icons.list),\\n ),\\n ButtonSegment(value: LayoutType.grid, icon: Icon(Icons.grid_on)),\\n ],\\n selected: {type},\\n onSelectionChanged: (Set<LayoutType> newSelection) {\\n _layoutType.value = newSelection.first;\\n },\\n ),\\n );\\n }\\n\\n Widget _buildContent() {\\n return ValueListenableBuilder<LayoutType>(\\n valueListenable: _layoutType,\\n builder: (_, type, __) {\\n return CustomScrollView(\\n controller: _scrollController,\\n slivers: [\\n SliverAppBar(\\n title: Text(\'Dynamic Layout\'),\\n pinned: true,\\n actions: [\\n Padding(\\n padding: EdgeInsets.only(right: 20),\\n child: _buildLayoutSwitch(),\\n )\\n ],\\n ),\\n _buildSliverList(type),\\n ],\\n );\\n },\\n );\\n }\\n\\n Widget _buildSliverList(LayoutType type) {\\n switch (type) {\\n case LayoutType.list:\\n return SliverList(\\n delegate: SliverChildBuilderDelegate(\\n (_, i) => _ListItem(color: _colors[i], index: i),\\n childCount: _colors.length,\\n ),\\n );\\n case LayoutType.grid:\\n return SliverGrid(\\n gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(\\n crossAxisCount: 3,\\n mainAxisSpacing: 8,\\n crossAxisSpacing: 8,\\n ),\\n delegate: SliverChildBuilderDelegate(\\n (_, i) => _GridItem(color: _colors[i], index: i),\\n childCount: _colors.length,\\n ),\\n );\\n }\\n }\\n}\\n\\n// 子组件实现\\nclass _ListItem extends StatelessWidget {\\n final Color color;\\n final int index;\\n\\n const _ListItem({required this.color, required this.index});\\n\\n @override\\n Widget build(BuildContext context) {\\n return Container(\\n height: 80,\\n color: color,\\n child: Center(\\n child: Text(\\n \'Item $index\',\\n style: TextStyle(\\n color: Colors.white,\\n fontSize: 20,\\n ),\\n ),\\n ),\\n );\\n }\\n}\\n\\nclass _GridItem extends StatelessWidget {\\n final Color color;\\n final int index;\\n\\n const _GridItem({required this.color, required this.index});\\n\\n @override\\n Widget build(BuildContext context) {\\n return Container(\\n color: color,\\n child: Center(\\n child: Text(\\n \'Grid $index\',\\n style: TextStyle(color: Colors.white, fontSize: 16),\\n ),\\n ),\\n );\\n }\\n}\\n
\\nimport \'package:flutter/material.dart\';\\n\\nclass ParallaxScrollPage extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n body: CustomScrollView(\\n slivers: [\\n SliverAppBar(\\n expandedHeight: 250,\\n flexibleSpace: FlexibleSpaceBar(\\n title: const Text(\'Parallax Demo\'),\\n background: Image.network(\\n \'https://picsum.photos/1200/800\',\\n fit: BoxFit.cover,\\n ),\\n ),\\n pinned: true,\\n ),\\n _buildStickyHeader(\'Section 1\'),\\n _buildParallaxList(5),\\n _buildStickyHeader(\'Section 2\'),\\n _buildParallaxList(15),\\n ],\\n ),\\n );\\n }\\n\\n SliverPersistentHeader _buildStickyHeader(String text) {\\n return SliverPersistentHeader(\\n pinned: true,\\n delegate: _StickyHeaderDelegate(\\n child: Container(\\n color: Colors.blueGrey[800],\\n padding: EdgeInsets.all(16),\\n child: Text(text,\\n style: TextStyle(\\n color: Colors.white,\\n fontSize: 18,\\n fontWeight: FontWeight.bold)),\\n ),\\n ),\\n );\\n }\\n\\n SliverList _buildParallaxList(int count) {\\n return SliverList(\\n delegate: SliverChildBuilderDelegate(\\n (_, index) => _ParallaxListItem(index: index),\\n childCount: count,\\n ),\\n );\\n }\\n}\\n\\nclass _StickyHeaderDelegate extends SliverPersistentHeaderDelegate {\\n final Widget child;\\n\\n _StickyHeaderDelegate({required this.child});\\n\\n @override\\n Widget build(\\n BuildContext context, double shrinkOffset, bool overlapsContent) {\\n return SizedBox.expand(child: child);\\n }\\n\\n @override\\n double get maxExtent => 50;\\n\\n @override\\n double get minExtent => 50;\\n\\n @override\\n bool shouldRebuild(covariant _StickyHeaderDelegate oldDelegate) {\\n return child != oldDelegate.child;\\n }\\n}\\n\\nclass _ParallaxListItem extends StatelessWidget {\\n final int index;\\n\\n const _ParallaxListItem({required this.index});\\n\\n @override\\n Widget build(BuildContext context) {\\n return Container(\\n height: 200,\\n margin: EdgeInsets.symmetric(vertical: 8, horizontal: 16),\\n decoration: BoxDecoration(\\n borderRadius: BorderRadius.circular(12),\\n image: DecorationImage(\\n image: NetworkImage(\\n \'https://picsum.photos/600/400?random=$index\'),\\n fit: BoxFit.cover,\\n ),\\n ),\\n child: Stack(\\n children: [\\n Positioned.fill(\\n child: DecoratedBox(\\n decoration: BoxDecoration(\\n borderRadius: BorderRadius.circular(12),\\n gradient: LinearGradient(\\n begin: Alignment.bottomCenter,\\n end: Alignment.topCenter,\\n colors: [\\n Colors.black.withOpacity(0.7),\\n Colors.transparent,\\n ],\\n ),\\n ),\\n ),\\n ),\\n Align(\\n alignment: Alignment.bottomLeft,\\n child: Padding(\\n padding: EdgeInsets.all(16),\\n child: Text(\'Parallax Item $index\',\\n style: TextStyle(\\n color: Colors.white,\\n fontSize: 20,\\n fontWeight: FontWeight.bold)),\\n ),\\n )\\n ],\\n ),\\n );\\n }\\n}\\n
\\n关键优化策略:
\\nSliverChildBuilderDelegate(\\n (context, index) => HeavyItem(data[index]),\\n childCount: 100000,\\n // 内存优化三剑客\\n addAutomaticKeepAlives: false, // 禁用自动保持状态\\n addRepaintBoundaries: false, // 关闭重绘边界\\n findChildIndexKey: (key) { // 自定义索引查找\\n final ValueKey valueKey = key as ValueKey;\\n return int.parse(valueKey.value.toString());\\n },\\n)\\n
\\n深度解析:
\\naddAutomaticKeepAlives
的误用会导致OOM
:当列表项包含大量图片时,KeepAlive
会阻止GC
回收。15%
的布局计算开销。二分查找
替代线性遍历,时间复杂度从O(n)降到O(log n)
。内存泄漏检测方案:
\\nvoid _detectMemoryLeaks() {\\n WidgetsBinding.instance.addPostFrameCallback((_) {\\n final renderObject = _scrollController.position.context.storageContext;\\n _checkRetainedRenderObjects(renderObject);\\n });\\n}\\n\\nvoid _checkRetainedRenderObjects(RenderObject node) {\\n if (node is RenderSliver && node.child != null) {\\n assert(node.debugDisposed!, \'Sliver未正确释放: ${node.runtimeType}\');\\n _checkRetainedRenderObjects(node.child!);\\n }\\n}\\n
\\n性能压测数据:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n优化策略 | FPS 提升 | 内存下降 | 冷启动时间 |
---|---|---|---|
预计算Sliver 几何尺寸 | 42% | 18% | 300ms → 210ms |
分级缓存策略 | 37% | 29% | - |
增量式布局更新 | 55% | 12% | - |
实战代码:动态缓存策略
\\nclass SmartCacheSliver extends SliverPersistentHeaderDelegate {\\n @override\\n Widget build(context, shrinkOffset, overlapsContent) {\\n final cacheExtent = context.dependOnInheritedWidgetOfExactType<CacheConfig>()?.extent;\\n \\n return LayoutBuilder(\\n builder: (_, constraints) {\\n // 根据设备性能动态调整缓存\\n final isLowEnd = MediaQuery.of(context).platformBrightness == Brightness.light;\\n final dynamicExtent = isLowEnd ? cacheExtent! * 0.5 : cacheExtent! * 1.2;\\n \\n return _CachedHeader(extent: dynamicExtent);\\n }\\n );\\n }\\n}\\n
\\nSliver
核心布局流程源码分析:
// flutter/lib/src/rendering/sliver.dart\\nvoid performLayout() {\\n // 阶段1:几何约束计算\\n final SliverConstraints constraints = ...;\\n final SliverGeometry geometry = getGeometry(constraints);\\n\\n // 阶段2:布局位置分配\\n double scrollOffset = 0.0;\\n for (final child in children) {\\n child.layout(\\n constraints.copyWith(\\n scrollOffset: scrollOffset,\\n overlap: calculateOverlap(),\\n ),\\n parentUsesSize: true,\\n );\\n scrollOffset += child.geometry.scrollExtent;\\n }\\n\\n // 阶段3:绘制指令生成\\n if (geometry.visible) {\\n paint(context, Offset.zero);\\n }\\n}\\n
\\n源码级性能优化点:
\\nSliverGeometry.visible
为false
时,相关RenderObject
会被标记为可回收。SliverConstraints.cacheExtent
影响Sliver
是否复用之前的布局计算结果。SliverHitTestResult.addWithPaintOffset
实现精准的重绘区域计算。Sliver
协议的三重境界:
1、几何层: 将布局抽象为SliverGeometry
的数学模型。
geometry = SliverGeometry(\\n scrollExtent: 100.0, // 滚动占据的空间\\n paintExtent: 80.0, // 实际绘制高度 \\n maxPaintExtent: 150.0, // 最大可能高度\\n layoutExtent: 70.0, // 布局有效高度\\n);\\n
\\n2、协议层:通过SliverConstraints
传递布局约束。
3、渲染层:RenderSliver
实现具体的绘制逻辑。
安全滚动架构方案:
\\nclass SafeScrollView extends StatefulWidget {\\n final List<Widget> slivers;\\n\\n const SafeScrollView({required this.slivers});\\n\\n @override\\n _SafeScrollViewState createState() => _SafeScrollViewState();\\n}\\n\\nclass _SafeScrollViewState extends State<SafeScrollView> with AutomaticKeepAliveClientMixin {\\n final _controller = ScrollController();\\n final _scrollNotifier = ValueNotifier(0.0);\\n\\n @override\\n void initState() {\\n super.initState();\\n _controller.addListener(() {\\n _scrollNotifier.value = _controller.offset;\\n });\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n super.build(context);\\n return NotificationListener<ScrollNotification>(\\n onNotification: (notification) {\\n if (notification is ScrollStartNotification) {\\n // 暂停后台任务\\n AppState.pauseBackgroundTasks();\\n }\\n return false;\\n },\\n child: CustomScrollView(\\n controller: _controller,\\n slivers: [\\n _buildScrollSyncHeader(),\\n ...widget.slivers,\\n _buildPerformanceMonitor(),\\n ],\\n ),\\n );\\n }\\n\\n Widget _buildScrollSyncHeader() {\\n return SliverToBoxAdapter(\\n child: ValueListenableBuilder<double>(\\n valueListenable: _scrollNotifier,\\n builder: (_, offset, __) => Opacity(\\n opacity: offset > 100 ? 1.0 : 0.0,\\n child: const HeaderWidget(),\\n ),\\n ),\\n );\\n }\\n}\\n
\\n企业级代码规范:
\\n1、滚动控制器生命周期:
\\n@override\\nvoid dispose() {\\n _controller.dispose(); // 必须显式释放\\n _scrollNotifier.dispose();\\n super.dispose();\\n}\\n
\\n2、内存安全检测:
\\nvoid _validateMemorySafety() {\\n assert(() {\\n if (kDebugMode) {\\n final leakDetector = MemoryAllocations.instance;\\n return leakDetector.detect(\\n this,\\n maxAllowed: 5, // 允许最多5个RenderObject泄漏\\n onLeak: (leaks) => reportError(leaks),\\n );\\n }\\n return true;\\n }());\\n}\\n
\\n3、性能监控体系:
\\nWidget _buildPerformanceMonitor() {\\n return SliverToBoxAdapter(\\n child: PerformanceOverlay(\\n options: const PerformanceOverlayOption(\\n rasterizerThreshold: 16, // 16ms/frame\\n checkerboardRasterCacheImages: true,\\n ),\\n ),\\n );\\n} \\n
\\nCustomScrollView
的本质是一个滚动布局的元编程框架,它通过Sliver
协议将布局元素转化为可组合的数学函数。真正的高手需要掌握三个维度:
SliverGeometry
如何将布局抽象为几何方程。ScrollController
如何协调多层级滚动动画。RenderSliver
的绘制管线以降低系统熵增。当你能用CustomScrollView
实现这些设计时,你构建的已不仅是界面,而是一个自组织的滚动生态系统:
这要求开发者突破传统Widget
的思维定式,转而用物理引擎的思维来思考滚动系统的能量流动与形态变换。
\\n\\n优秀的滚动体验不是设计出来的,而是通过精密的数学计算演化生成的。
\\n
\\n","description":"前言 在Flutter开发中,你是否曾因简单的ListView无法实现复杂嵌套滚动而抓狂?是否在面对需要动态切换网格与列表布局时感到束手无策?或者,当产品经理提出\\"视差滚动+吸顶导航+动态加载\\"的组合需求时,你的代码逐渐失控?\\n\\n这一切的答案,都藏在CustomScrollView的魔法盒子里。作为Flutter滚动系统的终极武器,它通过Sliver协议将布局原子化,赋予开发者无限的可能性。但为何许多开发者对它望而却步?因为它不仅需要你理解RenderObject的底层逻辑,更需要一种全新的\\"分形布局思维\\"。\\n\\n本文将通过六维知识体系,深入剖析其底层机制…","guid":"https://juejin.cn/post/7477688695286628406","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-04T00:20:29.906Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/06079c3b667c42e6b39ca12610b051cb~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1741652429&x-signature=NTU90chL6%2F743EdSLmz9tGWZuk0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f7b779eb39ee4831a5ab480de4e33e1b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1741652429&x-signature=Tg0J7O8sckytoI5kW0L90EMpR2E%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"深入聊聊 Flutter 里最接近官方的热更新方案:Shorebrid","url":"https://juejin.cn/post/7477147173537366068","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
热更新一直都是 Flutter 里的热门话题,毕竟 Flutter 的「先天属性」让它不像 RN 一样有 code push 这样的业内通用方案,不过这么多年下来 Flutter 也发展出了一些热更新的产品路线,比如:
\\n它们都在不同场景有着各自的优劣,而今天我们要聊的 Shorebrid 就比较特殊,因为它是 Flutter 前创始人 Eric 的商业项目,从目前来看,它是 Flutter 业内最接近 RN code push 的存在,或者说 Shorebrid 更懂 Flutter 在 code push 领域的产品体验。
\\n目前大家在聊 Flutter 的热更新时,关注的核心主要有三点:
\\n而从这几个纬度考虑的话,对于 Shorebrid 而言我们可以先有一个简单的结论:
\\n那么 Shorebrid 在技术上又是如何做到以上三点的?这就不得不提 Shorebrid 对于 Flutter Engine 和 Dart VM 的“魔改” ,或者你可以理解为,Shorebrid 对 Flutter 进行了一定程度的分叉。
\\n简单来说,你需要在构建 Flutter 项目时把 flutter build
换成 shorebird build
,那么看到这里,或者有人会觉得 Shorebrid 这样的实现「侵入性」或者「接入成本」不是很高?毕竟连 cli 都换了。
但是事实上并非如此,使用 Shorebrid 时虽然需要通过 shorebird build
等 cli 在构建时接入 Shorebrid 分叉过的 Flutter Engine 和 Dart VM ,但是日常开发里你依然可以使用原本的 flutter build
和 flutter run
去开发和构建。
\\n\\n因为 Shorebrid 的 cache 目录下会内置自己的 flutter lib,和你本地的 flutter 工程是可以直接区分开来的,只要你保证它们版本一致,例如通过
\\nshorebird flutter versions list
切换到支持的 flutter 版本。
对于 Shorebrid, 你一般只需要在发布或者构建 patch 的时候接入即可,其他时候和你日常开发 Flutter 并没有区别,这主要也是因为 Shorebrid 虽然“魔改”了 Engine 和 VM ,但是它并没有进行破坏性的功能变动,只是“新增了支持”,基本不影响你在 Flutter 和 Shorebrid 之间切换。
\\n\\n\\n甚至 Shorebrid 在 Flutter Engine 的“魔改”只有几百行代码,而真正的核心部份其实是在于 Dart VM 的定制逻辑。
\\n
那 Shorebrid 又是如何进行热更新的?答案就是下发“二进制”的 patch 文件,可能这时候你会觉得诧异,下发“二进制”能合规吗?没事,我们后面解释这个问题。
\\n首先我们先了解 Shorebrid 的实现,在 Shorebrid 里,如果在不存在任何 patch 更新的情况下,它和官方的 Flutter 是没有任何区别的,也就是从性能上和原有 Flutter 保持一致。
\\n而当存在热更新的 patch 文件时,根据平台又会有不同的情况:
\\n举个例子,我们先看 Android ,当你使用 shorebird build
构建完 Android 之后,cli 会将构建完的版本提交到 Shorebrid 集中的托管平台:
然后当你需要创建一个 patch 的时候,Shorebrid 会将你发布的版本下载下来,然后进行对比,创建出一个最小差异的二进制 patch 进行发布:
\\n如果这时候你去看这个文件,就会发现它是一个大小只有几十 K 到几百 K 的 dlc.vmcode
二进制文件:
在 Android 上,Shorebrid 会通过下发这个文件来,最终在 Dart VM 层面实现二进制部分替换,从而完成动态化热更新的支持,所以 Android 上 Shorebrid 一直是 AOT 的运行模式,性能基本没有变化。
\\n那为什么 Android 上可以采用这样的更新方式?这就不得不说 Google Play 的政策条件,如下图所示,官方在提及更新可执行文件时有一个例外要求:限制不适用于在虚拟机或解释器中运行的代码:
\\n也就是虽然 Shorebrid 下发的是 AOT 的二进制代码,但是它不能直接在 Android 或者 JVM 上运行,它需要 Dart VM 才能运行。
\\n\\n\\n这里可能有人会有疑问,都是编译成机械码了,为什么还需要 VM ?这是因为系统没有提供对应的运行时支持,Dart 的 AOT 代码同样需要有相应的库实现和垃圾回收的 runtime 环境支持,就像 C 语言编译后还是需要依赖于运行时来提供 C 标准库的实现,又比如 so 库可以使用 Android 环境提供的精简版 libstdc++ 等。
\\n
但是这在 iOS 上不适合,因为 App Store 上任何可执行代码都不允许动态下载,所以在做热更新时,iOS 的开发人员只能使用解释器(interpreter) 来实现,但是完全使用解释器运行会导致 App 性能极低,所以在 iOS 上,Shorebrid 最终实现了:未更改的代码依然在 CPU 上通过 AOT 运行,而更改的 patch 代码则通过 Shorebrid 实现的解释器运行到 Dart VM 上。
\\n这样的实现可以尽可能让 Flutter 在 iOS 上贴近原有的性能,如果你的项目没有任何 patch ,那么它就不会有性能损耗,如果存在 patch ,那么就只会在运行到对应 patch 代码的时候才会有相应的性能损耗,当然这部分涉及到了 Dart VM 的“魔改”支持:Shorebrid 在 VM 上增加了一个解释器和全新的 Linker。
\\n解释器当然就是解析代码让其可以变为 JIT 的模式运行到 Dart VM 上,这得益于 Dart 本身就同时具有 JIT 和 AOT 编译器两个工作流程,同时 Dart 本身在 JIT 模式下通常会保留有关源代码的信息,这些信息是 JIT 优化 hot code 的关键,同时也是 Shorebrid 让 Dart VM 能够实现“混合模式”运行的关键。
\\n而这里 Linker 可能和我们在 iOS 上理解的链接器不大一样,它不会对代码进行签名,它更多是通过在生成 patch 文件时判断具体函数是否直接在 CPU 上运行的作用。
\\n前面说过,iOS 不允许动态下发任何可执行文件,所以 Shorebrid 不会下发任何可以直接在 CPU 上运行的东西,甚至是 Dart 编译后 JIT 文件都不行(还涉及签名问题), Linker 的作用就是通过分析原有的 AOT 文件,然后通过对比快照 diff 在指令级别找到最小差异部分,而在没改变部分尽可能的在原有 AOT 文件下保留。
\\n\\n\\n所有这里的 Linker 更像一个缝合器 ,主就是对比出不能在 CPU 运行的函数,然后剥离出来后续通过解释器运行。
\\n
这里插一个题外话,为什么 Dart 编译后的 JIT 文件都不行?因为从 Dart 2.0 开始,Dart VM 就不能直接从原始代码解释执行 Dart ,VM 现在需要的是一个包含序列化的 Kernel AST 文件(dill 二进制文件),Dart 源码会通过前端 CFE 被处理为内核 AST :
\\n\\n\\n二进制不等于就是可执行 Machine Code
\\n
而在 JIT 模式中,Flutter 并不会直接处理 Dart 本身的解析,而时通过另外一个 frontend_server
的进程一起工作,从而实现 Flutter 上众所周知的 hotload 的效果,而内核二进制文件加载到 VM 后,对应的程序实体(类、对象)解析都是 Lazy 的,起初只加载有关库和类的基本信息,仅当运行时需要时,才会完全反序列化有关类的信息:
\\n\\n这也导致了 Flutter Debug 在 iOS 18.4 beta 无法运的问题,因为 iOS 18.4 beta 系统不再允许未经代码签名的二进制文件通过 JIT 编译直接执行,之前可以是因为这是一个“安全漏洞”,因为之前的机制允许开发者在真机上绕过某些签名要求,而现在通过 mprotect 在运行时动态修改内存读写权限的方式不再支持。
\\n
所以,在 Dart 里的 JIT 和一般意义上的代码解释运行还有区别,同时这也导致了在 Flutter 社区一直有一个误区,那就是 debug 模式下 Flutter 性能很差是因为 JIT ,其实这并不完全正确。
\\n实际上导致慢的主要原因是因为 Flutter 框架里有着许多一致性检查/断言,而这些导致性能极具下降的检查/断言仅在 debug 模式下启用,这才是缓慢的主要来源。
\\n\\n\\nJIT 也会有慢的情况,但是不会像 flutter run debug 那么卡顿。
\\n
JIT 模式的主要差别是需要预热,所以程序可能需要一定的时间才能达到最佳性能,同时需要更多内存占用,但是从理论峰值性能考虑,其实并不会输于 AOT。
\\n而 AOT 的特点是启动速度非常快,无需预热就可以达到最佳性能,所以 AOT 非常适合 UI 场景,因为 UI 无法容忍 JIT 的不可预测性和预热时间。
\\n所以回到 Shorebrid 热更新 iOS 上:
\\n当然,从整体运行性能上考虑,在有热更新 patch 的情况下,也能接近满血性能的 90% 以上,因为大多数时候在使用热更新场景,需要 patch 的代码并不会很多。
\\n因为 Flutter 上最耗时的布局计算等部分是直接在 CPU 上 AOT 运行,而真正需要 patch 往往不会很大,因为如果你真的有很大规模的更改,那么用热更新也不合适,从合规场景考虑,大规模变动的场景还是通过平台 update 更合理:
\\n所以,在 iOS 场景下,好理解又不太精确的解释,大概会是:
\\n\\n\\nDart VM 拥有两个 snapshtos ,一个是已经签名所以可以在 CPU 上运行的;另外一个是 patch snapshot,是无法直接在 CPU 上运行的,需要通过解释器运行,解释器就想是一个 fake CPU。
\\n
这也是 Shorebird “魔改” Engine 的原因,就是为了让 Engine 允许在运行时使用 Dart 虚拟机运行更新的代码更改,然后让 Dart 支持在生产模式下运行对应的解释器:
\\n\\n\\n在 JIT 模式运行时 Dart 函数可能具有不同的编译表示形式,例如简单编译和针对同一函数的优化编译,Shorebird 正是利用 Dart 架构的这一特点,插入了一个新的解释器作为函数执行的替代机制,从而能够在运行时有效地替换应用的某些部分,而无需在设备上编译新代码。
\\n
说人话大概就是,Dart 有时候会在 function 仍在运行时将执行从未优化的代码切换到优化的代码:
\\n而在代码层面,我们可以看到,在 Flutter 初始化时,引擎会被插入初始化一个 ConfigureShorebird
,这就是一切“魔法”的源头:
而配置的目的主要还是读取到 patch 文件的相关信息,如 vm_snapshot
和 isolate_snapshot
相关的类信息,全局变量,函数指针、堆、指令等内容:
例如,如果在 iOS 上不是 App.framework/App
而是 foo.vmcode
时,就需要从中提取符号,然后将符号读入静态变量并保留,同时我们也可以看到,作为 patch 的 vmcode 文件其实就是带有 shorebird 链接器标头前缀的 elf 文件 :
而最终读取到的 patch 会在 shorebird_init
变成 FileCallbacks ,接下来就去到 updater 的 rust 代码:
在 Shorebrid 里,updater 库是通过 rust 编写,用于在 Flutter 中更新和管理 patch 代码的存在,它是作为静态库构建的,例如在构建 Android 时会链接到 Flutter 引擎中 libflutter.so
当然,最后 patch 后运行部分肯定是去到了“魔改”的 Dart VM ,这部分才是 Shorebrid 的灵魂核心,但是遗憾的是,这部分目前并没有开源。
\\n所以到这里,我们基本全面了解了 Shorebrid 的实现原理,那么最后我们需要聊聊它的局限:
\\n首先 Shorebrid 是不能更新任何 Native 代码 ,因为更新 Native 代码是不合规,Shorebrid 的目的是修复 Dart 代码
\\n无法跨 Flutter 版本使用 patch,就算是小版本,因为 Shorebrid 需要通过已发布应用文件去对比得到最小差异 patch ,所以基本保证每次构建 Flutter 都是在同一个固定版本
\\n最低支持版本至少 3.10 :
\\n服务器稳定性问题,因为 Shorebird 现在使用的是 CloudFlare CDN,有时候在一些特殊区域还是稳定性问题。
\\n\\n\\n其实在此之前 shorebird 是托管在 Google 的,但是基于国内用户的强烈要求才做的迁移到 CloudFlare,甚至 Discord 还有一个中文子区。
\\n
不支持自托管部署
\\n另外,使用这种框架最怕的就是 Flutter 版本跟进的速度,不过这对于 Shorebird 来说基本不是问题,Shorebird 在 Flutter 版本跟进上的速度可以说是几乎同步,甚至连前段时间的 Flutter 的 monorepo 迁移也能快速同步,所以在这一问题上基本不需要担心。
\\n最后,Shorebird 的退出机制几乎无损, 在不想使用的时候,只需要删号然后切换回 flutter build
即可。
前段时间,我基于deepseek制作了一个基于小红书的自动推文生成发送工作流。然而,先前制作的windows端的工作流到小红书发布时显得异常繁琐,原先的思路是在手机接收到验证码后进入系统进行人为输入,这显然太麻烦了。同时,这一问题当部署到linux服务器上时显得尤为突出,这与自动化的理念显然有些背道而驰。因此,我决定基于flutter制作一个验证码提取转发应用,将手机短信验证码提取出来,通过http接口转发给工作流,从而实现自动化的工作流。
\\nflutter中存在大量不错的第三方短信处理库,例如flutter_sms_inbox, sms_v2, sms_receiver等,但经过测试,许多库在当前开发环境下存在许多问题,因此我最终选择了sms_advanced库进行短信处理。
\\nsms_advanced提供了querySms()这样一个方法,这个方法可以根据条件进行短信查询。以下是方法源码:
\\n/// Query a list of SMS\\nFuture<List<SmsMessage>> querySms({\\n int? start,\\n int? count,\\n String? address,\\n int? threadId,\\n List<SmsQueryKind> kinds = const [SmsQueryKind.Inbox],\\n bool sort = true}) async {\\n List<SmsMessage> result = [];\\n for (var kind in kinds) {\\n result.addAll(await _querySmsWrapper(\\n start: start,\\n count: count,\\n address: address,\\n threadId: threadId,\\n kind: kind,\\n ));\\n}\\nif (sort == true) {\\n result.sort((a, b) => a.compareTo(b));\\n}\\nreturn (result);\\n}\\n
\\n可以看到,querySms()方法可以接受多个参数,其中address参数可以指定短信发送者的手机号,kinds参数默认为SmsQueryKind.Inbox
,即从收件箱获取短信,从而实现短信提取。然后使用正则表达式对短信内容进行匹配,提取出验证码。
if (messages.isNotEmpty) {\\n // 获取第一条短信\\n SmsMessage firstMessage = messages.first;\\n String? messageBody = firstMessage.body;\\n\\n\\n // 使用正则表达式匹配验证码,假设验证码是 6 位数字\\n RegExp regex = RegExp(r\'\\\\d{6}\');\\n Match? match = regex.firstMatch(messageBody!);\\n\\n\\n if (match != null) {\\n String smsCode = match.group(0)!;\\n // 发送验证码到 API\\n result = await _sendCodeToAPI(smsCode);\\n } else {\\n result = \'未在短信中找到验证码\';\\n }\\n} else {\\n result = \'未找到短信\';\\n}\\n
\\n但后面我发现小红书的验证码发送者手机号并非固定,因此我选择制作一个多条件筛选器。在条件筛选中,我选择先根据手机号做一次短信筛选,如果没有找到,则根据短信内容做一次筛选,如果还是没有找到,则返回未找到短信。这样用户就可以在仅知道验证码发送应用名称的情况下,不填写发送者手机号,获取到短信并提取到验证码。实现代码如下:
\\nSmsQuery query = SmsQuery();\\nList<SmsMessage>? messages = await query.querySms(\\n address: _phoneNumber,\\n kinds: [SmsQueryKind.Inbox],\\n); // 获取收件箱中的短信\\nif (messages.isEmpty) {\\n List<SmsMessage>? messages = await query.querySms(\\n kinds: [SmsQueryKind.Inbox],\\n ); // 根据条件二进行查询\\n for (SmsMessage message in messages) {\\n if (message.body?.contains(_targetApp) ?? false) {\\n final code = _extractCode(message.body);\\n if (code != null) {\\n return await _sendCodeToAPI(code);\\n }\\n }\\n }\\n}\\n
\\n验证码转发是将提取到的验证码通过http接口转发给工作流。这里我选择使用http库进行http请求,实现代码如下:
\\nFuture<String?> _sendCodeToAPI(String code) async {\\n try {\\n final response = await http.post(\\n Uri.parse(_apiEndpoint),\\n headers: {\'Content-Type\': \'application/json\'},\\n body: jsonEncode({\'code\': code}),\\n );\\n if (response.statusCode != 200) {\\n return (\'Failed to send code: ${response.statusCode}\');\\n } else {\\n return \'Successfully Code sent \';\\n }\\n } catch (e) {\\n return (\'Error sending code: $e\');\\n }\\n}\\n
\\n由于验证码提取是一个耗时操作,因此我选择将其放在一个子线程中执行,以避免阻塞主线程。这里我选择使用flutter的Isolate进行线程通信。同时,为了更新监控状态并控制监控开始和停止,设计了两个Port,分别是mainpPort和isolatePort。mainpPort用于向接收子线程的监控状态消息,实现监控状态的实时更新;isolatePort用于接收主线程发来的启停信息,当点击停止监控后,由父线程告知子线程停止作业。实现代码如下:
\\nMainPort:
\\n// 在主isolate的接收端口监听中添加状态更新\\nvoid _initReceivePort() {\\n_receivePort = ReceivePort();\\n_receivePort.listen((message) {\\n if (message is String) {\\n if (message == \'isolate_stopped\') {\\n // 处理isolate退出通知\\n if (mounted) {\\n setState(() {\\n _isolate = null;\\n isMonitoring = false;\\n buttonText = \'开始监控\';\\n });\\n }\\n } else {\\n setState(() => response = message);\\n if(message == \'Successfully Code sent\') {\\n _stopIsolate();\\n }\\n }\\n } else if (message is SendPort) {\\n _isolateSendPort = message;\\n }\\n});\\n}\\n
\\nIsolatePort:
\\nstatic void _monitorSmsInBackground(List<dynamic> args) async {\\n final rootIsolateToken = args[4] as RootIsolateToken;\\n BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken);\\n\\n\\n final apiEndpoint = args[0] as String;\\n final targetApp = args[1] as String;\\n final phoneNumber = args[2] as String;\\n final mainSendPort = args[3] as SendPort;\\n final smsHandler = SMSHandler(apiEndpoint, targetApp, phoneNumber);\\n final controlPort = ReceivePort();\\n mainSendPort.send(controlPort.sendPort);\\n\\n\\n final stopCompleter = Completer<void>();\\n controlPort.listen((message) {\\n if (message == \'stop\') {\\n stopCompleter.complete();\\n }\\n });\\n\\n\\n try {\\n while (!stopCompleter.isCompleted) {\\n final value = await smsHandler.initSMSListener().timeout(\\n const Duration(seconds: 1),\\n onTimeout: () => null,\\n );\\n print(value);\\n mainSendPort.send(value);\\n\\n\\n if (stopCompleter.isCompleted) break;\\n }\\n } finally {\\n controlPort.close();\\n mainSendPort.send(\'isolate_stopped\'); // 添加退出通知\\n }\\n}\\n
\\nStopIsolate:
\\n// 修改 _stopIsolate 方法,仅发送停止信号,不强制终止Isolate\\nvoid _stopIsolate() {\\n if (_isolate != null) {\\n _isolateSendPort?.send(\'stop\');\\n }\\n}\\n
\\n总体来说,整体项目还是挺简单的。主要就是利用flutter的插件进行短信的监听,然后通过正则表达式提取验证码,最后通过http接口将验证码发送给工作流。但因为初次学习flutter,许多地方没有做详细的优化,仅仅实现了整体功能。工程代码放在github上,有兴趣的可以看看:verify_code_app
","description":"验证码提取转发应用 1. 前言\\n\\n前段时间,我基于deepseek制作了一个基于小红书的自动推文生成发送工作流。然而,先前制作的windows端的工作流到小红书发布时显得异常繁琐,原先的思路是在手机接收到验证码后进入系统进行人为输入,这显然太麻烦了。同时,这一问题当部署到linux服务器上时显得尤为突出,这与自动化的理念显然有些背道而驰。因此,我决定基于flutter制作一个验证码提取转发应用,将手机短信验证码提取出来,通过http接口转发给工作流,从而实现自动化的工作流。\\n\\n2.开发环境\\nIDE:VSCode\\n语言:Dart 3.7.0\\n框架…","guid":"https://juejin.cn/post/7477088942962622516","author":"yeffky","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-03T05:06:22.003Z","media":null,"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"不要升级,Flutter Debug 在 iOS 18.4 beta 无法运行,提示 mprotect failed: Permission denied","url":"https://juejin.cn/post/7476743827202736143","content":"近期如果有开发者的 iOS 真机升级到 18.4 beta,大概率会发现在 debug 运行时会有 Permission denied
的相关错误提示,其实从 log 可以很直观看出来,就是 Dart VM 在初始化时,对内核文件「解释运行(JIT)」时出现权限不足的问题:
../../../flutter/third_party/dart/runtime/vm/virtual_memory_posix.cc: 428: error: mprotect failed: 13 (Permission denied)\\nversion=3.6.0 (stable) (Thu Dec 5 07:46:24 2024 -0800) on \\"ios_arm64\\"\\npid=3252, thread=259, isolate_group=vm-isolate(0x107205400), isolate=vm-isolate(0x107369000)\\nos=ios, arch=arm64, comp=no, sim=no\\nisolate_instructions=108e375a0, vm_instructions=108e375a0\\nfp=16bb19560, sp=16bb19540, pc=109889864\\n pc 0x0000000109889864 fp 0x000000016bb19560 Dart_DumpNativeStackTrace+0x18\\n pc 0x000000010943aeb8 fp 0x000000016bb19580 dart::Assert::Fail(char const*, ...) const+0x30\\n pc 0x0000000109536100 fp 0x000000016bb19a30 dart::Code::FinalizeCode(dart::FlowGraphCompiler*, dart::compiler::Assembler*, dart::Code::PoolAttachment, bool, dart::CodeStatistics*)+0x82c\\n pc 0x00000001095f51c8 fp 0x000000016bb1a040 dart::StubCode::Init()+0x31c\\n pc 0x0000000109485c30 fp 0x000000016bb1ab00 dart::Dart::DartInit(Dart_InitializeParams const*)+0x2a9c\\n pc 0x0000000109870310 fp 0x000000016bb1ab20 Dart_Initialize+0x3c\\n pc 0x0000000108f1aaf4 fp 0x000000016bb1b0f0 flutter::DartVM::Create(flutter::Settings const&, fml::RefPtr<flutter::DartSnapshot const>, fml::RefPtr<flutter::DartSnapshot const>, std::_fl::shared_ptr<flutter::IsolateNameServer>)+0x1d60\\n pc 0x00000001093f17dc fp 0x000000016bb1b850 flutter::Shell::Create(flutter::PlatformData const&, flutter::TaskRunners const&, flutter::Settings, std::_fl::function<std::_fl::unique_ptr<flutter::PlatformView, std::_fl::default_delete<flutter::PlatformView>> (flutter::Shell&)> const&, std::_fl::function<std::_fl::unique_ptr<flutter::Rasterizer, std::_fl::default_delete<flutter::Rasterizer>> (flutter::Shell&)> const&, bool)+0x310\\n pc 0x0000000108e3b060 fp 0x000000016bb1c5c0 -[FlutterEngine createShell:libraryURI:initialRoute:]+0x934\\n pc 0x0000000108e42c4c fp 0x000000016bb1c630 -[FlutterViewController sharedSetupWithProject:initialRoute:]+0x1cc\\n pc 0x0000000108e42a58 fp 0x000000016bb1c660 -[FlutterViewController awakeFromNib]+0x58\\n
\\n具体原理就是在于:从目前 iOS 18.4 beta 上看,iOS 加强了对应用运行时修改内存权限的限制,也就是上面出现 mprotect failed: 13 (Permission denied)
的原因。
\\n\\nmprotect 全称是 \\"memory protect\\" ,可以用于修改内存页的保护属性,让 App 可以动态调整某块内存的访问权限,例如将 RX 只读执行权限切换为 RW 可读写权限。
\\n
而为什么 Flutter 在 Debug 时需要 mprotect ?其实这就要说到 Dart VM ,虽然在 Debug 模式下 Dart VM 是通过 JIT 模式解释执行的,但是从 Dart 2.0 之后就不再支持直接从源码运行,对于 Dart 代码现在会统一编译成一种「预处理」形式的二进制 dill 文件,我们一般称它会 Kernel AST 文件:
\\n也就是如今在 Dart 里,就算你是 JIT 运行,那么你也是跑着一个二进制的 Kernel dill ,只是 Kernel AST 不包含解析和优化:
\\n\\n\\n简单说,它仅仅是对源码进行了二进制加工转化, 让 Dart 代码从高级语法转换为统一且平台无关的中间格式。
\\n
所以 Flutter 在 debug 运行时, JIT 运行的是一个未签名的二进制文件,并且需要直接 hotload ,也就是需要 Dart VM 在运行时根据 Kernel 二进制文件生成机械码,并且在可以接受 hotload 的热更新,所以它是通过 VM 来“解释”和“生成“,所以它会需要 mprotect 的系统调用。
\\n\\n\\n比如上面的 StubCode 相关部分,在当前的 kernel JIT 模式下就极度依赖 VM 运行时的动态生成。
\\n
当然,这个过程依赖于 get-task-allow
,get-task-allow
可以允许其他进程 (如调试器) 附加到当前 App 上,让额外的进程获取到当前应用的任务端口,从而让它们可以执行诸如在内存上写入和读取内容之类的行为,最终达到 hotload 的目的。
那为什么在 release/profile 就不会有问题呢?很简单,代码已经被完全打包成机械码,并且需要生成的代码都包括在 snapshot 内,所以并不需要上述这些“魔法加持” 。
\\n那么回过头来,从 iOS 18.4 开始, 系统加强了对应用运行时修改内存权限的限制,具体来说就是:
\\n\\n\\n系统不再允许未经代码签名的二进制文件通过 JIT 编译直接执行,之前可以是因为这是一个“安全漏洞”,因为之前的机制允许开发者在真机上绕过某些签名要求,也就是 iOS 18.4 的新安全策略禁止了这种未经签名的动态代码生成支持。
\\n
那么到这里你应该大概了解了问题的原因,目前 Flutter 官方表示:在他们热修复此问题之前,尽可能先请不要升级到 iOS 18.4 beta。
\\n而目前官方修复的思路主要大概是:
\\ndart:ffi
而目前暂时评估的方向有:
\\n其实这里的第三点「混合模式执行」很有趣,因为这是 Flutter 热更新框架 shorebird 在 iOS 上目前的热更新方案:App 整体通过 AOT 运行,只有热更新 patch 存在的时候,针对该部分进行解释执行 ,也就是 shorebird 针对 Dart VM 自己“魔改”并“插入”了一个解释器,所以可以看到 shorebird 的 Eric (Flutter 前创始人) 针对和这个也和 Dart/Flutter 团队进行了密切的沟通:
\\n事实上,Eric 对于 Dart VM 这部分工作还是很“担心的”,毕竟 shorebird 作为分支方,这种修改合并无疑会给他们带来许多工作量,而如果 Dart 团队的方案能尽可能贴近 shorebird ,那就最好不过了:
\\n目前来说,好消息在于,只要你的真机不升级到 iOS 18.4 beta ,那么就不会有影响,而 Flutter/Dart 团队大概率会在 iOS 18.4 正式发布前修复这个问题,毕竟方向都有了。
\\n当然,这也体现了“利用漏洞”完成需求的可靠性很低,因为你不知道哪天平台就把后门关闭了。
","description":"近期如果有开发者的 iOS 真机升级到 18.4 beta,大概率会发现在 debug 运行时会有 Permission denied 的相关错误提示,其实从 log 可以很直观看出来,就是 Dart VM 在初始化时,对内核文件「解释运行(JIT)」时出现权限不足的问题: ../../../flutter/third_party/dart/runtime/vm/virtual_memory_posix.cc: 428: error: mprotect failed: 13 (Permission denied)\\nversion=3.6.0 (stabl…","guid":"https://juejin.cn/post/7476743827202736143","author":"恋猫de小郭","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-02T07:06:44.310Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/96a053174d66498f90562016452818b2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1741504004&x-signature=ObGHl3yrKMohCOKCUxYrzGFTFnY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/8191aa10466e4745b8a5d1e84a2fbe73~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1741504004&x-signature=o9A1X9mQ0nTTCkcXE5DQ%2BTaQK5s%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a63853fd1bd649b28336699a3b0c0b0a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1741504004&x-signature=gIQwr85%2FJ44bmdzM6DcdmX3OHWI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/70e3e4e47cb849b6904a14f8535c611e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1741504004&x-signature=wL9isoKB%2FKF9xfLq0jCDzSfwkXM%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["iOS","前端","Flutter","Android"],"attachments":null,"extra":null,"language":null},{"title":"系统化掌握Flutter组件之SingleChildScrollView:重识滚动容器的本质","url":"https://juejin.cn/post/7476651892145651723","content":"在Flutter
中,滚动行为如同呼吸般自然存在
。当我们在Flutter
框架中处理内容溢出问题时,SingleChildScrollView
组件展现出独特的价值。与传统ListView
等滚动容器不同,它专为单一子组件的滚动场景设计,在表单布局
、长文本展示
、复杂嵌套UI
等场景中表现出卓越的适应性。
本文将通过六维知识体系,深入解剖这个看似简单却暗藏玄机
的组件。通过系统化的知识梳理,我们将掌握如何正确选择和使用滚动容器,避免常见的性能陷阱,并深入理解Flutter
渲染机制的精妙之处。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\nconst SingleChildScrollView({\\n super.key,\\n this.scrollDirection = Axis.vertical,\\n this.reverse = false,\\n this.padding,\\n this.primary,\\n this.physics,\\n this.controller,\\n this.child,\\n this.dragStartBehavior = DragStartBehavior.start,\\n this.clipBehavior = Clip.hardEdge,\\n this.hitTestBehavior = HitTestBehavior.opaque,\\n this.restorationId,\\n this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,\\n})\\n
\\n类别 | 属性 | 类型 | 默认值 | 设计意图 |
---|---|---|---|---|
布局控制 | scrollDirection | Axis | vertical | 建立滚动维度坐标系 |
padding | EdgeInsets | null | 构建安全滚动区域 | |
reverse | bool | false | 实现倒序布局的革命性设计 | |
滚动行为 | controller | ScrollController | null | 赋予滚动状态控制权 |
physics | ScrollPhysics | 平台自适应 | 构建物理滚动模型 | |
primary | bool | null | 主滚动视图的智能识别 | |
交互优化 | keyboardDismissBehavior | ScrollViewKeyboardDismissBehavior | manual | 软键盘交互的优雅处理 |
scrollDirection
:滚动方向控制Axis.vertical
(垂直滚动)与Axis.horizontal
(水平滚动)决定了滚动视图的主轴方向。垂直滚动时,子组件的高度可以无限延伸
,但宽度受父容器约束
;水平滚动则相反。
///垂直滚动\\nSingleChildScrollView buildVertical() {\\n return SingleChildScrollView(\\n scrollDirection: Axis.vertical,\\n child: Column(\\n children: buildList(double.infinity),\\n ),\\n );\\n}\\n\\n/// 水平滚动\\nSingleChildScrollView buildHorizontal() {\\n return SingleChildScrollView(\\n scrollDirection: Axis.horizontal,\\n child: Row(\\n children: buildList(100),\\n ),\\n );\\n}\\n\\nList<Widget> buildList(double width) {\\n return List.generate(\\n 10,\\n (index) => Container(\\n width: width,\\n height: 100,\\n color: Colors.primaries[index % Colors.primaries.length],\\n child: Center(\\n child: Text(\'Item $index\'),\\n ),\\n ),\\n );\\n}\\n
\\n与ListView
的差异:
\\nListView
基于Sliver机制动态渲染子项。而SingleChildScrollView
一次性渲染所有内容。在子组件高度不确定时,垂直滚动需结合LayoutBuilder
动态计算:
LayoutBuilder(\\n builder: (context, constraints) {\\n return SingleChildScrollView(\\n child: ConstrainedBox(\\n constraints: BoxConstraints(minHeight: constraints.maxHeight),\\n child: ...,\\n ),\\n );\\n },\\n)\\n
\\nreverse
:反向滚动的本质当reverse: true
时,滚动起点从右下角开始(垂直滚动
)或右上角开始(水平滚动
)。这实质是修改了Viewport
的anchor
属性(0.0→1.0
)。
/// 反向滚动:从末尾开始滚动\\nSingleChildScrollView buildReverse() {\\n return SingleChildScrollView(\\n reverse: true,\\n child: Column(\\n children: buildList(double.infinity),\\n ),\\n );\\n}\\n
\\n与ScrollController
的联动:
\\n反向滚动时,ScrollController.initialScrollOffset
的逻辑会变化。若需编程跳转到底部,需计算正确的偏移量:
void scrollToBottom() {\\n final maxOffset = _controller.position.maxScrollExtent;\\n _controller.jumpTo(reverse ? 0 : maxOffset);\\n}\\n
\\npadding
:边距的深层逻辑padding
属性在滚动视图中承担着双重职责:\\nUI
遮挡)。SingleChildScrollView(\\n padding: EdgeInsets.all(16),\\n child: Column(\\n children: buildList(),\\n ),\\n)\\n
\\n与Margin
的本质区别:
\\npadding
作用于Viewport
内部,相当于在滚动区域周围添加缓冲区
,不影响子组件的布局约束。而Margin
属于子组件的布局属性,可能导致约束冲突。
MediaQuery.removePadding
移除系统默认的内边距(如状态栏遮挡
):\\nMediaQuery.removePadding(\\n context: context,\\n removeTop: true,\\n child: SingleChildScrollView(...),\\n)\\n
\\nphysics
:滚动物理的定制平台自适应方案 :
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n平台 | 对应ScrollPhysics | 核心特性 |
---|---|---|
iOS | BouncingScrollPhysics | 弹性越界回弹 |
Android | ClampingScrollPhysics | 无弹性效果 |
iOS 、Android | AlwaysScrollableScrollPhysics | 始终可以滚动 |
iOS 、Android | NeverScrollableScrollPhysics | 禁止滚动 |
高级用法:通过ScrollPhysics
自动匹配平台风格:
physics: Platform.isIOS ? const BouncingScrollPhysics() : const ClampingScrollPhysics()\\n
\\n自定义物理效果:\\n实现视差滚动效果需继承ScrollPhysics
:
class ParallaxScrollPhysics extends ScrollPhysics {\\n @override\\n double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {\\n return offset * 0.5; // 滚动速度为正常的一半\\n }\\n}\\n
\\ncontroller
:滚动控制的核心/// 控制滚动位置\\nColumn buildController() {\\n return Column(\\n children: [\\n ElevatedButton(\\n onPressed: () {\\n // 滚动到指定位置\\n _controller.animateTo(\\n 500,\\n duration: Duration(seconds: 1),\\n curve: Curves.easeInOut,\\n );\\n },\\n child: Text(\'Scroll to 500\'),\\n ),\\n Expanded(\\n child: SingleChildScrollView(\\n controller: _controller,\\n child: Column(\\n children: buildList(double.infinity),\\n ),\\n ),\\n ),\\n ],\\n );\\n}\\n
\\n四大核心能力:
\\noffset
)。positions
)。animateTo/jumpTo
)。生命周期管理规范:必须在State
的dispose
中销毁自定义控制器:
@override\\nvoid dispose() {\\n _controller.dispose();\\n super.dispose();\\n}\\n
\\n高阶动画技巧:实现分段滚动动画
:
void _scrollToSection(int index) {\\n final double offset = index * 100;\\n _controller.animateTo(\\n offset,\\n duration: Duration(seconds: 1),\\n curve: Curves.easeInOut,\\n );\\n}\\n
\\nprimary
:主滚动控制器当primary=true
时,滚动视图会:
controller
。平台差异处理策略:
\\nprimary: kIsWeb ? false : null, // Web平台特殊处理\\n
\\n与AppBar
的自动联动:
\\n当primary: true
且滚动方向为垂直时,Flutter
自动关联PrimaryScrollController
,使得AppBar
的滚动指示器生效。但需注意:
primary: true
的滚动视图。NestedScrollView
嵌套时可能失效。源码级验证:
\\n查看ScrollView
源码可见,primary
属性实际控制是否使用PrimaryScrollController
:
ScrollController get controller => primary \\n ? PrimaryScrollController.of(context) \\n : _controller;\\n
\\nkeyboardDismissBehavior
:软键盘的优雅退场两种模式的本质区别:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n模式 | 触发条件 | 适用场景 |
---|---|---|
onDrag | 滚动开始瞬间触发 | 搜索列表等即时反馈场景 |
manual | 需要明确滑动操作才会触发 | 表单输入等敏感操作场景 |
// 智能键盘处理方案\\nSingleChildScrollView(\\n keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,\\n child: TextField(\\n decoration: InputDecoration(\\n hintText: \'输入后滑动收起键盘\',\\n ),\\n ),\\n)\\n
\\nclipBehavior
:裁剪策略可视化对比:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n模式 | 性能 | 视觉效果 |
---|---|---|
Clip.none | 最高 | 内容可能溢出 |
Clip.hardEdge | 高 | 锯齿明显 |
Clip.antiAlias | 中 | 平滑边缘 |
Clip.antiAliasWithSaveLayer | 低 | 完美裁剪但消耗内存 |
内存泄露陷阱 :
\\n使用Clip.antiAliasWithSaveLayer
时会创建离屏缓冲区,在长列表滚动中可能导致OOM
,需通过RepaintBoundary
隔离绘制区域。
属性A | 属性B | 互斥关系 | 解决方案 |
---|---|---|---|
primary=true | controller | 不能共存 | 使用ScrollController时设置primary=false |
reverse=true | anchor=0.0 | 逻辑冲突 | 使用Alignment代替anchor定位 |
physics=NeverScrollableScrollPhysics | controller | 功能矛盾 | 需要滚动时禁用NeverScrollable模式 |
开发时需要特别注意这些隐性的互斥规则。
\\nStack(\\n children: [\\n // 背景图像\\n AnimatedBuilder(\\n animation: _controller,\\n builder: (context, child) {\\n double scrollOffset = _controller.hasClients\\n ? _controller.offset\\n : 0;\\n return Transform.translate(\\n offset: Offset(0, scrollOffset * 0.5),\\n child: Image.asset(\\n \'assets/images/product.webp\',\\n fit: BoxFit.cover,\\n width: double.infinity,\\n height: MediaQuery.of(context).size.height * 2,\\n ),\\n );\\n },\\n ),\\n SingleChildScrollView(\\n controller: _controller,\\n child: Column(\\n children: buildList(200),\\n ),\\n ),\\n ],\\n),\\n
\\n横向+纵向滚动联动
/// 嵌套滚动与滑动切换\\nPageView buildPageView() {\\n return PageView(\\n children: [\\n // 第一个页面\\n SingleChildScrollView(\\n child: Column(\\n children: buildList(double.infinity),\\n ),\\n ),\\n // 第二个页面\\n SingleChildScrollView(\\n child: Column(\\n children: buildList(double.infinity),\\n ),\\n ),\\n ],\\n );\\n}\\n
\\n动态显示 / 隐藏
元素class SingleChildScrollViewDemo extends StatefulWidget {\\n @override\\n _SingleChildScrollViewState createState() => _SingleChildScrollViewState();\\n}\\n\\nclass _SingleChildScrollViewState extends State<SingleChildScrollViewDemo> {\\n final ScrollController _controller = ScrollController();\\n\\n bool _isButtonVisible = true;\\n double _previousOffset = 0;\\n\\n @override\\n void initState() {\\n super.initState();\\n _controller.addListener(() {\\n double currentOffset = _controller.offset;\\n if (currentOffset > _previousOffset) {\\n // 向下滚动,隐藏按钮\\n if (_isButtonVisible) {\\n setState(() {\\n _isButtonVisible = false;\\n });\\n }\\n } else {\\n // 向上滚动,显示按钮\\n if (!_isButtonVisible) {\\n setState(() {\\n _isButtonVisible = true;\\n });\\n }\\n }\\n _previousOffset = currentOffset;\\n });\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: Text(\\"SingleChildScrollView Demo\\"),\\n centerTitle: true,\\n backgroundColor: Theme.of(context).colorScheme.inversePrimary,\\n ),\\n body: SingleChildScrollView(\\n controller: _controller,\\n child: Column(\\n children: buildList(double.infinity),\\n ),\\n ),\\n floatingActionButton: _isButtonVisible\\n ? FloatingActionButton(\\n onPressed: () {},\\n child: Icon(Icons.add),\\n )\\n : null,\\n );\\n }\\n
\\n核心逻辑:
\\nScrollController
的 addListener
方法监听滚动事件。比较当前滚动偏移量和上一次的滚动偏移量
,判断用户是向上滚动还是向下滚动。setState
方法动态更新 _isButtonVisible
变量的值,从而控制浮动按钮的显示和隐藏。1、约束传递优化
\\nSingleChildScrollView
的子组件可能因无限高度导致重复布局。LayoutBuilder(\\n builder: (context, constraints) {\\n return ConstrainedBox(\\n constraints: constraints.copyWith(maxHeight: double.infinity),\\n child: ...,\\n );\\n },\\n)\\n
\\n50%
以上的布局计算次数
。2、绘制边界控制
\\nRepaintBoundary
黄金法则:在以下位置插入:\\nRepaintBoundary
增加约5%
的内存占用。3、组件构建优化
\\nWidget
重建时的差异比对时间。Visibility
控制子组件的显隐生命周期。30%-70%
。1、图像资源优化
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n策略 | 实现方式 | 内存降幅 |
---|---|---|
预加载 | precacheImage(context, Image.network(url).image) | 20%-40% |
懒加载 | visibility_detector + Placeholder | 30%-50% |
分辨率适配 | MediaQuery.size + Image.network的width/height参数 | 15%-25% |
2、列表项复用
\\nCustomScrollView(\\n slivers: [\\n SliverFixedExtentList(\\n itemExtent: 100,\\n delegate: SliverChildBuilderDelegate(\\n (context, index) => _buildItem(index),\\n childCount: 1000,\\n ),\\n ),\\n ],\\n)\\n
\\nSingleChildScrollView
内存降低80%
。3、滚动位置持久化
\\nPageStorage.of(context)?.writeState(context, _controller.offset);\\n// 恢复时\\nfinal savedOffset = PageStorage.of(context)?.readState(context) as double?;\\n_controller.jumpTo(savedOffset ?? 0);\\n
\\n1、火焰图实战分析
\\nGesture → ScrollActivity → Layout → Paint
2、内存快照对比技巧
\\nflutter build apk --analyze-size\\nflutter run --profile\\n
\\nDart VM
内存 < 200MB
100MB
50
3、自动化监控方案
\\nvoid _startMonitoring() {\\n WidgetsBinding.instance.addTimingsCallback((List<FrameTiming> timings) {\\n final frameTime = timings.last.totalSpan.inMilliseconds;\\n if (frameTime > 16) {\\n _reportJank(frameTime);\\n }\\n });\\n}\\n
\\n1、继承体系解密
\\n@immutable\\nclass SingleChildScrollView extends ScrollView {\\n // 关键源码片段:\\n Widget build(BuildContext context) {\\n return Scrollable(\\n controller: controller,\\n physics: physics,\\n viewportBuilder: (context, offset) {\\n return Viewport(\\n offset: offset,\\n slivers: [SliverToBoxAdapter(child: child)],\\n );\\n },\\n );\\n }\\n}\\n
\\n组合模式实现功能复用
。2、RenderObject
布局流程
Parent → Viewport
)。Viewport → Sliver
)。Sliver
布局算法)。ScrollPosition
)。3、坐标转换系统
\\n// 关键坐标计算公式:\\nfinal double paintOffset = offset.pixels + viewportDimension - layoutExtent;\\n
\\n1、手势识别链
\\nPointerDownEvent → GestureDetector → DragGestureRecognizer → ScrollActivity
2、物理模拟引擎
\\nclass _BallisticSimulation extends Simulation {\\n // 核心算法:\\n double x(double time) => initialVelocity * time - 0.5 * deceleration * time * time;\\n}\\n
\\niOS
的BouncingScrollPhysics
弹性系数为0.15
,Android
的ClampingScrollPhysics
阻尼系数为0.3
。3、帧同步机制
\\nVSync
信号处理:通过SchedulerBinding
同步到屏幕刷新率。60fps
时自动插值。1、组合式架构
\\nScrollable
处理交互。Viewport
处理布局。Sliver
体系处理渲染。2、可扩展性设计
\\nScrollPhysics
子类实现不同效果。Viewport
:支持ShrinkWrappingViewport
等变种。3、平台适配策略
\\n// 平台检测逻辑:\\nswitch (defaultTargetPlatform) {\\n case TargetPlatform.android:\\n return ClampingScrollPhysics();\\n case TargetPlatform.iOS:\\n return BouncingScrollPhysics();\\n}\\n
\\n1、场景适用性
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n场景 | 推荐组件 | 理由 |
---|---|---|
简单表单 | SingleChildScrollView | 开发效率高 |
长列表 | ListView.builder | 内存优化 |
嵌套滚动 | NestedScrollView | 手势协调 |
复杂布局 | CustomScrollView | 灵活性高 |
2、性能边界条件
\\n50
个时应考虑虚拟化。20MB
需优化。3、开发者体验优先
\\nAPI
设计原则1、正交性检验
\\nscrollDirection/padding
)。physics/controller
)。clipBehavior/restorationId
)。2、渐进式复杂度
\\n// 基础用法\\nSingleChildScrollView(child: ...)\\n\\n// 进阶用法\\nSingleChildScrollView(\\n controller: _controller,\\n physics: CustomScrollPhysics(),\\n ...\\n)\\n
\\n3、版本兼容策略
\\n至少两个大版本
。mixin
实现API
扩展。1、Impeller
引擎优化
20%-40%
。Skia
相关的绘制逻辑。2、声明式滚动API
// 提案中的新语法:\\nScrollView.animated(\\n target: 500,\\n curve: Curves.easeInOut,\\n child: ...,\\n)\\n
\\n3、跨平台统一性
\\nWeb
端:优化滚动惯性算法。Desktop
:支持精确触控板滚动。Embedded
:低内存模式开发。LayoutBuilder(\\n builder: (context, constraints) {\\n return SingleChildScrollView(\\n controller: _controller,\\n physics: const AlwaysScrollableScrollPhysics(),\\n padding: const EdgeInsets.symmetric(vertical: 16),\\n child: ConstrainedBox(\\n constraints: constraints.copyWith(\\n minHeight: constraints.maxHeight,\\n maxHeight: double.infinity,\\n ),\\n child: IntrinsicHeight(\\n child: Column(\\n crossAxisAlignment: CrossAxisAlignment.stretch,\\n children: [\\n _buildHeader(),\\n _buildContent(),\\n _buildFooter(),\\n ],\\n ),\\n ),\\n ),\\n );\\n },\\n)\\n
\\n布局组合技解析:
\\nLayoutBuilder
:获取父级真实约束。ConstrainedBox
:确保最小高度填满可视区域。IntrinsicHeight
:解决Column
子组件高度依赖问题。CrossAxisAlignment.stretch
:实现横向撑满布局。这种组合方案完美解决了以下常见问题:
\\n1、键盘安全布局
\\nSingleChildScrollView(\\n padding: EdgeInsets.only(\\n bottom: MediaQuery.of(context).viewInsets.bottom + 16,\\n ),\\n child: TextField(...),\\n)\\n
\\niOS
需要额外处理键盘动画曲线。2、嵌套滚动协调
\\nPrimaryScrollController(\\n controller: _mainController,\\n child: NestedScrollView(\\n body: SingleChildScrollView(\\n controller: _subController,\\n physics: const ClampingScrollPhysics(),\\n ),\\n ),\\n)\\n
\\n3、平台自适应
\\nphysics: Platform.isIOS \\n ? const BouncingScrollPhysics() \\n : const ClampingScrollPhysics()\\n
\\n1、滚动卡顿四步排查法
\\nOpacity
。DevTools Timeline
)。Memory Tab
)。Widget Inspector
)。2、布局溢出解决方案
\\n// 错误示例:\\nSingleChildScrollView(child: Row(children: [...])) \\n\\n// 修正方案:\\nSingleChildScrollView(\\n scrollDirection: Axis.horizontal,\\n child: IntrinsicWidth(child: Row(...)),\\n)\\n
\\n3、手势冲突处理
\\nRawGestureDetector(\\n gestures: {\\n AllowMultipleGestureRecognizer: \\n GestureRecognizerFactoryWithHandlers(...)\\n },\\n child: SingleChildScrollView(...),\\n)\\n
\\nSingleChildScrollView
作为Flutter
滚动系统的基石组件,其设计体现了框架对开发者体验的深刻理解。通过本文的系统化梳理,我们不仅掌握了属性配置的细节
,更深入理解了滚动机制的本质
。从源码实现到性能优化,从设计哲学到实践技巧,构建了多维度的知识网络。
值得注意的是,在复杂场景中往往需要结合CustomScrollView
、NestedScrollView
等其他组件形成解决方案。我们应当根据实际需求选择最合适的滚动容器,在性能与功能间找到最佳平衡点。未来随着Flutter
引擎的持续演进,对滚动系统的深度理解将成为构建高质量应用的关键竞争力。
\\n","description":"前言 在Flutter中,滚动行为如同呼吸般自然存在。当我们在Flutter框架中处理内容溢出问题时,SingleChildScrollView组件展现出独特的价值。与传统ListView等滚动容器不同,它专为单一子组件的滚动场景设计,在表单布局、长文本展示、复杂嵌套UI等场景中表现出卓越的适应性。\\n\\n本文将通过六维知识体系,深入解剖这个看似简单却暗藏玄机的组件。通过系统化的知识梳理,我们将掌握如何正确选择和使用滚动容器,避免常见的性能陷阱,并深入理解Flutter渲染机制的精妙之处。\\n\\n操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。\\n\\n一…","guid":"https://juejin.cn/post/7476651892145651723","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-02T01:41:30.149Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9a454aab50dc45c6bdbb7cca20dfde65~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1741484489&x-signature=uFtHpM3FJ6MMdLEPX9m23EZZIAM%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"Flutter插件中引用本地framework","url":"https://juejin.cn/post/7476489350823018548","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
最近搞了搞Flutter plugin,发现iOS端已经默认使用SPM了,但是SPM目前也比较少,而我拿到的Framework又是纯本地的,不需要发布到远程仓库,但始终无法正确添加。本身不是什么难题,只是因为我不太了解SPM。
\\n如果有误,望指正。
\\n由于对方给我的是一堆.framework
文件,而SPM不支持.framework
,所以要先把.framework
变成.xcframework
:
xcodebuild -create-xcframework -output SampleFramework.xcframework -framework path/to/a.framework -framework path/to/b.framework \\n\\n
\\n完成之后你会得到一个Sample.xcframework
.
假设你的插件名字叫作plugin_name
,并且你要集成的xcframework
名字叫作SampleFramework.xcframework
。
首先,把xcframework
复制到ios/plugin_name
。然后找Package.swift
, 然后把SampleFramework.xcframework
添加进去。
import PackageDescription\\n\\nlet package = Package(\\n name: \\"plugin_name\\",\\n platforms: [\\n .iOS(\\"12.0\\")\\n ],\\n products: [\\n .library(name: \\"plugin_name\\", targets: [\\"plugin_name\\",\\"SampleFramework\\"]) // 这里添加你的framework名称\\n ],\\n dependencies: [\\n .package(path: \\"Sources/plugin_name/SampleFramework.xcframework\\") // 这里添加你的framework\\n ],\\n targets: [\\n .target(\\n name: \\"plugin_name\\",\\n dependencies: [\\n .byName(name: \\"SampleFramework\\") // 这里添加你的framework\\n ],\\n resources: []\\n \\n ),\\n \\n .binaryTarget(name: \\"SampleFramework\\", path: \\"SampleFramework.xcframework\\") //这里添加你的framework\\n\\n ]\\n)\\n\\n
","description":"前言 最近搞了搞Flutter plugin,发现iOS端已经默认使用SPM了,但是SPM目前也比较少,而我拿到的Framework又是纯本地的,不需要发布到远程仓库,但始终无法正确添加。本身不是什么难题,只是因为我不太了解SPM。\\n\\n如果有误,望指正。\\n\\n.framework转成.xcframework\\n\\n由于对方给我的是一堆.framework文件,而SPM不支持.framework,所以要先把.framework变成.xcframework:\\n\\nxcodebuild -create-xcframework -output SampleFramework…","guid":"https://juejin.cn/post/7476489350823018548","author":"JarvanMo","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-03-01T13:55:34.435Z","media":null,"categories":["前端","Flutter","Swift"],"attachments":null,"extra":null,"language":null},{"title":"最新Flutter导航拦截PopScope使用","url":"https://juejin.cn/post/7476388533738635264","content":"以下是基于 Flutter 最新版本的 PopScope
使用示例及说明,结合官方文档和社区实践:
import \'package:flutter/material.dart\';\\n\\nclass HomePage extends StatefulWidget {\\n const HomePage({super.key});\\n\\n @override\\n State<HomePage> createState() => _HomePageState();\\n}\\n\\nclass _HomePageState extends State<HomePage> {\\n bool _canPop = true; // 控制是否允许返回\\n final TextEditingController _textController = TextEditingController();\\n\\n @override\\n void dispose() {\\n _textController.dispose();\\n super.dispose();\\n }\\n\\n Future<bool> _handlePop() async {\\n if (_textController.text.isEmpty) return true; // 允许直接返回\\n\\n // 弹出二次确认对话框\\n final confirm = await showDialog<bool>(\\n context: context,\\n builder: (context) => AlertDialog(\\n title: const Text(\'确认退出?\'),\\n content: const Text(\'输入内容未保存,确定要离开吗?\'),\\n actions: [\\n TextButton(\\n onPressed: () => Navigator.pop(context, false),\\n child: const Text(\'取消\'),\\n ),\\n TextButton(\\n onPressed: () => Navigator.pop(context, true),\\n child: const Text(\'确定\'),\\n ),\\n ],\\n ),\\n );\\n return confirm ?? false; // 用户点击确定时返回true\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return PopScope(\\n canPop: _canPop, // 动态控制返回手势是否生效 [[6]][[7]]\\n onPopInvoked: (didPop) async {\\n if (didPop) return; // 如果已经弹出则直接返回\\n final allowed = await _handlePop();\\n if (allowed && mounted) {\\n Navigator.pop(context); // 手动触发返回操作 [[2]][[4]]\\n }\\n },\\n child: Scaffold(\\n appBar: AppBar(title: const Text(\'PopScope 示例\')),\\n body: Padding(\\n padding: const EdgeInsets.all(16.0),\\n child: Column(\\n children: [\\n TextField(\\n controller: _textController,\\n onChanged: (value) {\\n // 输入内容变化时更新canPop状态\\n setState(() => _canPop = value.isEmpty);\\n },\\n ),\\n ],\\n ),\\n ),\\n ),\\n );\\n }\\n}\\n
\\n替代 WillPopScope
\\nPopScope
在 Flutter 3.16+ 完全替代 WillPopScope
,通过 canPop
和 onPopInvoked
实现返回拦截 [[3]][[6]]。
canPop
动态控制
canPop: true
时,系统返回手势(如 Android 物理按键/滑动返回)直接生效 [[7]]。canPop: false
时,需通过 onPopInvoked
手动处理返回逻辑 [[8]]。异步确认弹窗
\\n在 onPopInvoked
中可通过 showDialog
弹出确认框,结合 async/await
实现阻塞式交互 [[1]][[4]]。
与导航器的配合
\\nPopScope
需包裹在 Navigator
的上层组件中(如 MaterialApp
的子组件),才能正确拦截返回事件 [[2]][[3]]。
根据最新 Flutter 版本(v3.22+)的变更,onPopInvoked
已被弃用,需改用 onPopInvokedWithResult
。以下是修改后的代码示例及关键点说明:
class _HomePageState extends State<HomePage> {\\n // ... 其他代码保持不变\\n\\n @override\\n Widget build(BuildContext context) {\\n return PopScope(\\n canPop: _canPop,\\n onPopInvokedWithResult: (didPop, result) async { // 修改为 onPopInvokedWithResult [[1]][[2]]\\n if (didPop) return; // 已处理弹出则直接返回\\n\\n final allowed = await _handlePop();\\n if (allowed && mounted) {\\n Navigator.pop(context); // 手动触发返回\\n }\\n },\\n child: Scaffold(\\n // ... 原有 Scaffold 内容\\n ),\\n );\\n }\\n}\\n
\\n替换回调名称
\\n将 onPopInvoked
改为 onPopInvokedWithResult
,这是 Flutter 3.22+ 的强制要求 [[1]][[2]]。
参数兼容性处理
\\n新回调的参数 (didPop, result)
中:
didPop
表示路由是否已弹出(与旧版行为一致)[[1]]。result
是导航返回时传递的可选数据(如 Navigator.pop(context, result)
)[[6]]。若无需处理返回值,可直接忽略 result
参数。
保持原有逻辑
\\n_handlePop()
方法的异步确认逻辑无需修改,只需确保在 allowed
为 true
时手动调用 Navigator.pop
[[4]][[6]]。
pubspec.yaml
中 Flutter SDK 版本不低于 3.22.0-12.0.pre
,否则会因 API 不兼容导致编译失败 [[1]]。Navigator.pop(context, result)
传递数据,可通过 onPopInvokedWithResult
的 result
参数接收 [[6]]。通过上述修改,代码将符合最新 Flutter API 规范,同时保持原有的返回拦截功能(如未保存内容的确认弹窗)。
","description":"以下是基于 Flutter 最新版本的 PopScope 使用示例及说明,结合官方文档和社区实践: 基础用法示例\\nimport \'package:flutter/material.dart\';\\n\\nclass HomePage extends StatefulWidget {\\n const HomePage({super.key});\\n\\n @override\\n State太想进步了,最近准备搞搞升级。
\\n从 Flutter 2.10.5 升级到 3.27.4 版本将带来显著的框架特性增强、性能优化及开发工具改进。以下是主要提升点:
\\nCupertino 组件高保真优化
\\nCupertinoCheckbox
、CupertinoRadio
和CupertinoSwitch
的视觉细节(如大小、颜色、交互效果),新增语义标签、鼠标光标支持及tinted
按钮构造函数,使 iOS 风格组件更贴近原生体验。CupertinoNavigationBar
支持透明背景动态适配,实现折叠与展开状态的平滑颜色过渡。Material 设计改进
\\nRow
和Column
的间距布局逻辑,减少手动调整成本。平台适配扩展
\\n官网下载3.27.4的Flutter sdk放置后之前安装目录,解压并更新环境变量。结果无论运行什么flutter指令都会报错。
\\n\\n经过多次检查环境变量,显示引入
flutter_templete_images: ^5.0.0
也无法解决这个问题。清理掉所有缓存包重新下载也解决不了。
不得已打开flutter3.27.4源码研究才发现,里面工具包flutter_tools
本身在pubspec.yaml中的版本引入和flutter3.27.4不兼容,修正了一下终于好了。
\\n在Vscode中使用时需要在settings.json中配置一下
dart.flutterSdkPath
的指定路径,由flutter2.10.5切到flutter3.27.4。
回到业务上来看,首先用AI刷一遍pubspec.yaml中第三方包版本,不得不说效率很高。
\\n\\n刷完后运行发现有些第三方包作者没有继续维护,意味着没有对应版本号来适配。
text_to_speech
拉取到本地自己维护一下引入也能兼容futter_my_picker
研究了一下,修改成本太高,不得不用flutter_datetime_picker替换掉qiniu
的sdk和dio
冲突了,dio升级到最新版本flutter_datetime_picker引入后发现插件本身也报错。\\n\\n不得已又拉到本地,修改了一下命名冲突问题,并增加了清空功能重新引入。
启动继续报错,gradle必须升级成classpath \'com.android.tools.build:gradle:7.4.0\'
,Java8也得配合着升级到Java11。
第一次下载的是压缩包格式,解压完配置环境变量重启电脑发现没切过来。直接下载exe格式安装再重启才把Java应用版本切到11。
\\n调用deepseek api全项目检查了一遍语法问题,同步修正了一下。剩余的主要是:
\\nTheme.of(context).canvasColor
主题色的应用出现差异,手动修正一下Refresh Token
逻辑项目中虽然存在一些弃用标记的语法,不过也能使用就没进一步升级修正。
\\nAPP的加载速度有一定提高,部分控件的展示风格有些变化(例如:Tabs
)。Text
原生控件的默认文本颜色由之前的黑色变成紫色。画质感觉清晰了一些,也可能是错觉。大体上已经能完全跑业务,一些细节问题可能还未发现。
整个升级过程主要解决Flutter3.27.4 SDK集成问题和第三方包兼容问题,对原有业务代码部分影响不是很大,整体升级过程还算顺利。
","description":"背景 太想进步了,最近准备搞搞升级。\\n\\n从 Flutter 2.10.5 升级到 3.27.4 版本将带来显著的框架特性增强、性能优化及开发工具改进。以下是主要提升点:\\n\\n一、框架特性增强\\n\\nCupertino 组件高保真优化\\n\\n更新了CupertinoCheckbox、CupertinoRadio和CupertinoSwitch的视觉细节(如大小、颜色、交互效果),新增语义标签、鼠标光标支持及tinted按钮构造函数,使 iOS 风格组件更贴近原生体验。\\nCupertinoNavigationBar支持透明背景动态适配,实现折叠与展开状态的平滑颜色过渡…","guid":"https://juejin.cn/post/7476117595462320139","author":"机器瓦力","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-28T07:50:42.111Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/82842077ad434be98831d7ea805fd4ec~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5py65Zmo55Om5Yqb:q75.awebp?rk3s=f64ab15b&x-expires=1741333842&x-signature=W6aNwc2CuOiiMEc3svFve1GiBps%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/63ab8e9188a74bbb885874b4b024ba5b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5py65Zmo55Om5Yqb:q75.awebp?rk3s=f64ab15b&x-expires=1741333842&x-signature=beGqtZIm9xwLzRS4VH56kd%2FQReo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ed0b4467337f45b3824440a9892fc457~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5py65Zmo55Om5Yqb:q75.awebp?rk3s=f64ab15b&x-expires=1741333842&x-signature=ZvzH6KTpBu3jV382X15DmZR2ZcU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/bca94e42d940460d97a39282a0a03964~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5py65Zmo55Om5Yqb:q75.awebp?rk3s=f64ab15b&x-expires=1741333842&x-signature=LcDB%2FqGEQ1SQrraCbD6UFtohno8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b9cacf863f4c47fda86918e11b66f992~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5py65Zmo55Om5Yqb:q75.awebp?rk3s=f64ab15b&x-expires=1741333842&x-signature=KkRcDZWA1J8V2pzO80PaviV7iCw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/adb5e9cf8d2149bfa21cfd1d315779ed~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5py65Zmo55Om5Yqb:q75.awebp?rk3s=f64ab15b&x-expires=1741333842&x-signature=hHnNRWgB9WNR3jguLD26i4Mt%2B74%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"系统化掌握Flutter表单组件之Radio、Checkbox、Switch","url":"https://juejin.cn/post/7476142699994906639","content":"在移动应用开发中,表单控件是与用户交互的核心元素。Flutter
提供的Radio
(单选按钮)、Checkbox
(复选框)和Switch
(开关)组件,是实现选择逻辑的三大支柱工具。这三个组件看似简单,实则蕴含着丰富的设计哲学和技术细节:
Radio
体现排他选择。Checkbox
处理多重选择。Switch
呈现二元状态切换。它们共同构建了现代应用中最基础的选择体系。本文将以系统化视角深入剖析这三个组件,从基础属性到高级应用,揭示其内在设计原理与工程实践中的精妙之处。
\\n通过本文,你不仅能掌握标准用法,还将学习到如何通过属性组合实现定制化交互,并深入理解Flutter
框架下表单控件状态管理的本质。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\nRadio
组件value
与groupValue
(动态类型)值匹配实现单选逻辑
。枚举推荐方案
)。// 字符串类型示例\\nString? _selected = \\"A\\";\\nRadio<String>(\\n value: \\"A\\",\\n groupValue: _selected,\\n onChanged: (v) => setState(() => _selected = v)\\n)\\n\\n// 数值类型示例\\nint? _selectedNumber = 1;\\nRadio<int>(\\n value: 1,\\n groupValue: _selectedNumber,\\n onChanged: (v) => setState(() => _selectedNumber = v)\\n)\\n
\\nonChanged(ValueChanged<T?>)
用户交互
→ 触发回调
→ 更新groupValue
。null
时组件不可交互。\\nRadio<String?>(\\n value: null,\\n groupValue: null,\\n onChanged: _isEditable ? (v) {} : null, // 动态禁用\\n)\\n
\\ntoggleable
(布尔型)Radio
。Radio
时设置groupValue
为null
。\\"不确定\\"
选项。Radio<String>(\\n value: \\"A\\",\\n groupValue: _selected,\\n toggleable: _isToggleable,\\n onChanged: (v) => setState(() => _selected = v),\\n)\\n
\\n属性 | 作用范围 | 优先级 | 特性 |
---|---|---|---|
fillColor | 选中状态填充色 | 最高 | 支持状态交互颜色变化 |
activeColor | 激活状态主色 | 次高 | 统一设置选中颜色 |
ThemeData 属性 | 全局默认颜色 | 最低 | 保持应用视觉一致性 |
动态颜色实现方案:
\\nRadio<String>(\\n value: \\"A\\",\\n groupValue: _selected,\\n toggleable: _isToggleable,\\n fillColor: WidgetStateProperty.resolveWith<Color>(\\n (Set<WidgetState> states) {\\n if (states.contains(WidgetState.disabled)) {\\n return Colors.grey.withValues(alpha: 0.5);\\n }\\n if (states.contains(WidgetState.selected)) {\\n return Colors.blueAccent;\\n }\\n return Colors.grey;\\n },\\n ),\\n onChanged: (v) => setState(() => _selected = v),\\n)\\n
\\nmaterialTapTargetSize
Material Design
触摸目标最小48x48px
。MaterialTapTargetSize.shrinkWrap
(紧凑模式)。MaterialTapTargetSize.padded
(标准模式)。触控区域满足可访问性要求
。Transform.scale
Transform.scale
可以对其子组件进行缩放操作,通过设置缩放比例来间接改变 Radio
的大小。
Transform.scale(\\n scale: 2.0, // 缩放比例,这里将 Radio 放大两倍\\n child: Radio<int>(\\n value: 1,\\n groupValue: 1,\\n onChanged: (int? value) {\\n // 处理选中事件\\n },\\n ),\\n),\\n
\\nRadio
样式可以自定义 Radio
样式,通过绘制一个圆形来模拟 Radio
的外观,然后根据自己的需求设置其宽高。
import \'package:flutter/material.dart\';\\n\\nclass CustomRadio extends StatelessWidget {\\n final bool isSelected;\\n final double size;\\n final VoidCallback? onTap;\\n\\n const CustomRadio({\\n Key? key,\\n required this.isSelected,\\n this.size = 24,\\n this.onTap,\\n }) : super(key: key);\\n\\n @override\\n Widget build(BuildContext context) {\\n return GestureDetector(\\n onTap: onTap,\\n child: Container(\\n width: size,\\n height: size,\\n decoration: BoxDecoration(\\n shape: BoxShape.circle,\\n border: Border.all(\\n color: Colors.grey,\\n width: 2,\\n ),\\n ),\\n child: isSelected\\n ? Center(\\n child: Container(\\n width: size * 0.6,\\n height: size * 0.6,\\n decoration: BoxDecoration(\\n shape: BoxShape.circle,\\n color: Colors.blue,\\n ),\\n ),\\n )\\n : null,\\n ),\\n );\\n }\\n}\\n
\\n上述示例代码图示如下:
\\nCheckbox
组件值 | 含义 | 显示形态 |
---|---|---|
true | 选中状态 | ✓ |
false | 未选中状态 | □ |
null | 不确定状态 | -(需设置tristate ) |
状态流转控制:
\\nCheckbox(\\n tristate: true,\\n value: _checkState,\\n onChanged: (bool? value) {\\n setState(() {\\n _checkState = value ?? false;\\n });\\n },\\n)\\n
\\n属性 | 类型 | 效果 |
---|---|---|
shape | ShapeBorder | 控制整体外框形状 |
side | BorderSide | 边框样式 |
checkColor | Color | 勾选标记颜色 |
圆角+边框样式示例:
\\nCheckbox(\\n value: _checkState,\\n shape: RoundedRectangleBorder(\\n borderRadius: BorderRadius.circular(8),\\n ),\\n side: BorderSide(color: Colors.blue, width: 2),\\n checkColor: Colors.white,\\n fillColor: WidgetStateProperty.all(Colors.blue),\\n visualDensity: VisualDensity(horizontal: -2, vertical: -2),\\n // visualDensity: VisualDensity.adaptivePlatformDensity,\\n onChanged: (bool? value) {\\n setState(() {\\n _checkState = value ?? false;\\n });\\n },\\n)\\n
\\nCheckbox(\\n visualDensity: VisualDensity.adaptivePlatformDensity,\\n // 或指定具体值\\n // visualDensity: VisualDensity(horizontal: -2, vertical: -2)\\n)\\n
\\n-4
(最紧凑)到+4
(最宽松)。Switch
组件// 自动适配平台风格\\nSwitch.adaptive(\\n value: _isActive,\\n onChanged: (v) => setState(() => _isActive = v),\\n),\\n\\n// 强制Material风格\\nSwitch(\\n value: _isActive,\\n materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,\\n onChanged: (bool value) {},\\n),\\n\\n// 强制Cupertino风格\\nCupertinoSwitch(\\n value: _isActive,\\n onChanged: (v) => setState(() => _isActive = v),\\n)\\n
\\n颜色体系架构:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n属性 | 作用部位 | 优先级 |
---|---|---|
thumbColor | 滑块颜色 | 最高 |
activeColor | 激活状态主色 | 次高 |
trackColor | 轨道背景色 | 第三 |
ThemeData.switchThumb | 主题默认颜色 | 最低 |
动态颜色配置:
\\nSwitch(\\n value: _isActive,\\n activeTrackColor: Colors.blueAccent,\\n inactiveTrackColor: Colors.grey[300],\\n thumbColor: WidgetStateProperty.resolveWith<Color>(\\n (Set<WidgetState> states) {\\n if (states.contains(WidgetState.disabled)) {\\n return Colors.grey;\\n }\\n return _isDarkMode ? Colors.black : Colors.white;\\n },\\n ),\\n onChanged: (v) => setState(() {\\n _isActive = v;\\n }),\\n)\\n
\\n图像化滑块:
\\nSwitch(\\n activeThumbImage: AssetImage(\'assets/sun.png\'),\\n inactiveThumbImage: AssetImage(\'assets/moon.png\'),\\n)\\n
\\n20x20
像素。thumbColor
为透明。pubspec.yaml
。交互状态颜色:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n属性 | 触发条件 | 默认值来源 |
---|---|---|
hoverColor | 鼠标悬停 | ThemeData.hoverColor |
focusColor | 键盘焦点 | ThemeData.focusColor |
overlayColor | 按压覆盖色 | WidgetStateProperty |
自定义状态反馈:
\\nRadio<String>(\\n value: \\"A\\",\\n groupValue: _selected,\\n toggleable: _isToggleable,\\n activeColor: Colors.red,\\n focusColor: Colors.blue.withValues(alpha: 0.2),\\n hoverColor: Colors.blue.withValues(alpha: 0.1),\\n overlayColor: WidgetStateProperty.all(\\n Colors.blue.withValues(alpha: 0.3)),\\n onChanged: (v) => setState(() => _selected = v),\\n),\\n
\\n关键配置项:
\\nCheckbox(\\n semanticLabel: \\"同意用户协议\\", // 屏幕阅读器标签\\n autofocus: true, // 自动获取焦点\\n mouseCursor: SystemMouseCursors.click, // 鼠标指针样式\\n)\\n
\\n热区优化方案:
\\nRadio(\\n materialTapTargetSize: MaterialTapTargetSize.padded,\\n // 扩展点击区域\\n autofocus: true,\\n)\\n
\\nFlutter
的SemanticsDebugger
。48x48
像素可触摸区域。// 与FormField集成示例\\nForm(\\n child: Column(\\n children: [\\n RadioListTile<String>(\\n title: Text(\'选项A\'),\\n value: \'A\',\\n groupValue: _groupValue,\\n onChanged: (v) => setState(() => _groupValue = v),\\n ),\\n CheckboxListTile(\\n title: Text(\'同意协议\'),\\n value: _isAgreed,\\n onChanged: (v) => setState(() => _isAgreed = v),\\n ),\\n SwitchListTile(\\n title: Text(\'启用功能\'),\\n value: _isEnabled,\\n onChanged: (v) => setState(() => _isEnabled = v),\\n ),\\n ],\\n ),\\n)\\n
\\n实现目标:
\\nRadio
选择控制Checkbox
可选状态。Checkbox
组合验证逻辑。Switch
控制整个表单可用性。状态管理架构:
\\nclass FormState with ChangeNotifier {\\n bool _formEnabled = true;\\n String? _userType;\\n Set<String> _permissions = {};\\n\\n // 状态验证逻辑\\n bool get isValid {\\n if (_userType == \'admin\' && !_permissions.contains(\'admin\')) return false;\\n return _formEnabled && _userType != null;\\n }\\n}\\n
\\n关联逻辑实现:
\\nConsumer<FormState>(\\n builder: (context, state, _) {\\n return RadioListTile(\\n value: \'admin\',\\n groupValue: state.userType,\\n onChanged: state.formEnabled ? (v) => state.updateType(v) : null,\\n );\\n }\\n)\\n
\\nRadio
、Checkbox
、Switch
这三个基础组件构成了Flutter
选择体系的核心三角。通过系统化分析,我们揭示了其设计本质:
Radio
实现互斥选择。Checkbox
处理复合选择。Switch
专注二元状态。值得注意的是,这三个组件都遵循Material Design
规范,但通过属性组合可以突破默认样式限制。开发者应深入理解WidgetStateProperty
机制,这是实现交互状态联动的关键。最后,牢记表单控件的可访问性原则
,合理设置热区大小
和视觉反馈
,才能打造出专业级的移动应用体验。
\\n","description":"前言 在移动应用开发中,表单控件是与用户交互的核心元素。Flutter提供的Radio(单选按钮)、Checkbox(复选框)和Switch(开关)组件,是实现选择逻辑的三大支柱工具。这三个组件看似简单,实则蕴含着丰富的设计哲学和技术细节:\\n\\nRadio体现排他选择。\\nCheckbox处理多重选择。\\nSwitch呈现二元状态切换。\\n\\n它们共同构建了现代应用中最基础的选择体系。本文将以系统化视角深入剖析这三个组件,从基础属性到高级应用,揭示其内在设计原理与工程实践中的精妙之处。\\n\\n通过本文,你不仅能掌握标准用法,还将学习到如何通过属性组合实现定制化交互,并深…","guid":"https://juejin.cn/post/7476142699994906639","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-28T07:23:07.918Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/18c81483da5846778158390d884b2532~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1741332186&x-signature=jfyC%2Fv%2BxVD9DrIKQBg%2BMPACPMWw%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"系统化掌握Flutter组件之“核按钮系统探秘”","url":"https://juejin.cn/post/7476031198117347355","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
在移动应用开发中,按钮(Button
)是用户交互的核心组件之一。Flutter
通过丰富的按钮类型和灵活的定制能力,为开发者提供了构建高效
、美观
交互体验的工具。然而,许多开发者仅停留在基础使用层面,对按钮的底层原理
、性能优化
和设计哲学
缺乏系统化认知。
本文将通过六维知识体系,全面剖析Flutter
中的按钮组件。无论你是想解决按钮点击卡顿问题,还是想定制独特的按钮动画,这篇文章都将提供系统化的解决方案。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\n深度对比
)在深入属性前,需明确不同按钮的核心设计目标:
\\nElevatedButton
:强调主要操作(如表单提交
),默认带有背景色
和阴影
。TextButton
:用于次要操作(如取消
)。OutlinedButton
:介于前两者之间,通过边框表达层级
。IconButton
:专为图标设计的紧凑型按钮
。FloatingActionButton
:圆形悬浮按钮(Material Design
特殊场景)。CupertinoButton
:iOS
风格按钮(透明背景+按压高亮
) 。FlatButton
, RaisedButton
(已过时按钮)。将按钮属性划分为三大类:
\\n行为与状态
。颜色
、形状
、动画
)。尺寸
、边距
等空间关系。属性分类 | 属性名 | 类型 | 关键作用点 |
---|---|---|---|
交互控制 | onPressed | VoidCallback? | 点击回调 |
onLongPress | VoidCallback? | 长按回调 | |
enabled | bool | 全局禁用 | |
样式定制 | style | ButtonStyle? | 综合样式入口 |
backgroundColor | WidgetStateProperty<Color?> | 背景色动态控制 | |
foregroundColor | WidgetStateProperty<Color?> | 前景色控制 | |
elevation | WidgetStateProperty<double?> | 阴影层级 | |
布局约束 | padding | WidgetStateProperty<EdgeInsetsGeometry?> | 内边距 |
minimumSize | WidgetStateProperty<Size?> | 最小尺寸 | |
辅助功能 | autofocus | bool | 初始焦点控制 |
mouseCursor | WidgetStateProperty<MouseCursor?> | 鼠标样式 |
onPressed
:核心交互逻辑onPressed: () { \\n // 点击触发逻辑\\n}\\n
\\nnull
时,按钮自动进入禁用状态(视觉灰化
)。onLongPress
的优先级:长按仅在未定义onPressed
时生效。// 异步操作时禁用按钮\\nbool _isLoading = false;\\n\\nonPressed: _isLoading ? null : () async {\\n setState(() => _isLoading = true);\\n await fetchData();\\n setState(() => _isLoading = false);\\n}\\n
\\nonLongPress
:长按交互onLongPress: () {\\n // 长按触发逻辑(如弹出菜单)\\n}\\n
\\n500ms
。onPressed
的关系:\\nonPressed
,长按不会触发。TextButton
中实现\\"按压预览\\"
效果。enabled
:显式状态控制enabled: false // 强制禁用按钮\\n
\\nonPressed: null
的区别:\\nenabled: false
会同时禁用所有交互事件(包括hover/focus
)。onPressed: null
仅禁用点击,仍可接收其他状态事件
。style
//方式1: 使用ButtonStyle\\nstyle: ButtonStyle(\\n backgroundColor: WidgetStateProperty.all(Colors.red),\\n)\\n// 方式2 :使用对应的styleFrom构造函数\\nstyle:ElevatedButton.styleFrom(...)\\n
\\nWidgetStateProperty
动态状态管理\\nenum WidgetState implements WidgetStatesConstraint {\\n hovered, // 鼠标悬停\\n focused, // 获得焦点(如键盘导航)\\n pressed, // 按压中\\n dragged, // 拖拽\\n selected, // 选中状态\\n scrolledUnder, // 由 [AppBar] 使用,用于指示主要可滚动视图的内容已向上滚动并位于应用栏后面。 \\n disabled, // 禁用状态\\n error, // 错误状态(需手动触发)\\n}\\n
\\n// 单值覆盖\\nWidgetStateProperty.all(Colors.red)\\n\\n// 条件判断\\nWidgetStateProperty.resolveWith<Color?>(\\n (Set<WidgetState> states) {\\n if (states.contains(WidgetState.pressed)) {\\n return Colors.blue;\\n }\\n return Colors.grey;\\n },\\n)\\n\\n// 多状态组合\\nWidgetStateProperty.all<Color>(\\n Colors.blue.withOpacity(states.contains(WidgetState.disabled) ? 0.5 : 1.0)\\n)\\n
\\n属性名 | 作用范围 | 默认值规则 |
---|---|---|
backgroundColor | 按钮背景色 | 根据按钮类型变化 |
foregroundColor | 文字/图标颜色 | 对比背景色自动计算 |
overlayColor | 按压/悬停叠加色 | ThemeData.splashColor |
shadowColor | 阴影颜色 | ThemeData.shadowColor |
surfaceTintColor | 材质表面色调(ElevatedButton ) | Colors.transparent |
代码示例:
\\nElevatedButton(\\n style: ButtonStyle(\\n backgroundColor: WidgetStateProperty.resolveWith<Color>(\\n (states) => states.contains(WidgetState.pressed) \\n ? Colors.deepPurple \\n : Colors.purple,\\n ),\\n foregroundColor: WidgetStateProperty.all(Colors.white),\\n overlayColor: WidgetStateProperty.all(\\n Colors.white.withOpacity(0.2)\\n ),\\n ),\\n)\\n
\\nButtonStyle(\\n shape: WidgetStateProperty.all<RoundedRectangleBorder>(\\n RoundedRectangleBorder(\\n borderRadius: BorderRadius.circular(20),\\n side: BorderSide(color: Colors.black),\\n ),\\n ),\\n elevation: WidgetStateProperty.all(8),\\n)\\n
\\nshape
:控制按钮外形(圆角/边框
)\\nRoundedRectangleBorder
, StadiumBorder
, CircleBorder
。elevation
:阴影高度(Material Design
层级表达)elevation: WidgetStateProperty.resolveWith<double>(\\n (states) {\\n if (states.contains(WidgetState.pressed)) return 12;\\n if (states.contains(WidgetState.hovered)) return 8;\\n return 4;\\n }\\n)\\n
\\nTextButton(\\n style: ButtonStyle(\\n textStyle: WidgetStateProperty.all<TextStyle>(\\n TextStyle(fontSize: 18, fontWeight: FontWeight.bold),\\n ),\\n ),\\n)\\n
\\ntextStyle
:字体样式。alignment
:子组件对齐方式。iconColor
/iconSize
:独立控制图标样式。padding
:内边距控制ButtonStyle(\\n padding: WidgetStateProperty.all<EdgeInsets>(\\n EdgeInsets.symmetric(horizontal: 24, vertical: 16),\\n ),\\n)\\n
\\nElevatedButton
:EdgeInsets.symmetric(horizontal: 16)
。TextButton
:EdgeInsets.symmetric(horizontal: 8)
。48x48
(Material Accessibility
指南)。minimumSize/maximumSize
:最小/最大尺寸minimumSize: WidgetStateProperty.all(Size(200, 60))\\nmaximumSize: WidgetStateProperty.all(Size.infinite)\\n
\\nfixedSize
的区别:\\nfixedSize
:严格固定尺寸。minimumSize
:允许超过设定值。alignment
:子组件对齐alignment: Alignment.centerLeft\\n
\\nRow(\\n mainAxisSize: MainAxisSize.min,\\n children: [Text(\'Text\'), Icon(Icons.arrow_right)],\\n)\\n
\\nColumn(\\n children: [\\n ElevatedButton(\\n onPressed: () {},\\n style: ButtonStyle(\\n //单值覆盖\\n // backgroundColor: WidgetStateProperty.all(Colors.red),\\n // 条件判断1\\n // backgroundColor: WidgetStateProperty.resolveWith<Color?>(\\n // (Set<WidgetState> states) {\\n // if (states.contains(WidgetState.pressed)) {\\n // return Colors.blue;\\n // }\\n // return Colors.grey;\\n // },\\n // ),\\n // 条件判断2\\n backgroundColor: WidgetStateProperty.resolveWith<Color>(\\n (states) => states.contains(WidgetState.pressed)\\n ? Colors.deepPurple\\n : Colors.purple,\\n ),\\n // 多状态组合\\n // backgroundColor: WidgetStateProperty.resolveWith<Color>(\\n // (states) => Colors.blue.withValues(\\n // alpha: states.contains(WidgetState.disabled) ? 0.5 : 1.0),\\n // ),\\n\\n //颜色相关\\n foregroundColor: WidgetStateProperty.all(Colors.white),\\n overlayColor: WidgetStateProperty.all(\\n Colors.white.withValues(alpha: 0.2)),\\n\\n //形状与装饰\\n shape: WidgetStateProperty.all<RoundedRectangleBorder>(\\n RoundedRectangleBorder(\\n borderRadius: BorderRadius.circular(20),\\n side: BorderSide(color: Colors.black),\\n ),\\n ),\\n elevation: WidgetStateProperty.all(10),\\n\\n textStyle: WidgetStateProperty.all<TextStyle>(\\n TextStyle(fontSize: 18, fontWeight: FontWeight.bold),\\n ),\\n\\n // 布局约束\\n // padding: WidgetStateProperty.all<EdgeInsets>(\\n // EdgeInsets.symmetric(horizontal: 24, vertical: 16),\\n // ),\\n minimumSize: WidgetStateProperty.all(Size(200, 60)),\\n // maximumSize: WidgetStateProperty.all(Size(300, 60)),\\n\\n //子组件对齐\\n // alignment: Alignment.centerLeft,\\n // mouseCursor: WidgetStateProperty.all(SystemMouseCursors.click),\\n // animationDuration: Duration(milliseconds: 200),\\n ),\\n child: Text(\'提交\'),\\n // Row(\\n // mainAxisSize: MainAxisSize.min,\\n // children: [Text(\'Text\'), Icon(Icons.arrow_right)],\\n // ),\\n ),\\n SizedBox(height: 10),\\n TextButton(\\n onPressed: () {},\\n style: TextButton.styleFrom(\\n padding: EdgeInsets.symmetric(vertical: 16, horizontal: 30),\\n backgroundColor: Colors.purple,\\n // foregroundColor: Colors.red,\\n shape: RoundedRectangleBorder(\\n borderRadius: BorderRadius.circular(10),\\n ),\\n textStyle: TextStyle(\\n fontSize: 16,\\n fontWeight: FontWeight.w600,\\n ),\\n // splashFactory: NoSplash.splashFactory, // 禁用波纹效果\\n ),\\n child: Text(\\n \'删除记录\',\\n style: TextStyle(\\n color: Colors.white,\\n fontSize: 18,\\n fontWeight: FontWeight.bold,\\n ),\\n ),\\n ),\\n SizedBox(height: 10),\\n OutlinedButton(\\n onPressed: () {},\\n style: OutlinedButton.styleFrom(\\n foregroundColor: Colors.red,\\n backgroundColor: Colors.yellow,\\n // shape: CircleBorder(),\\n side: BorderSide(\\n color: Colors.blue,\\n width: 2.0,\\n style: BorderStyle.solid,\\n ),\\n // 配置按下时的背景色\\n overlayColor: Colors.blue,\\n ),\\n child: Text(\'蓝色边框按钮\'),\\n ),\\n SizedBox(height: 10),\\n IconButton(\\n onPressed: () {},\\n icon: Icon(Icons.star),\\n ),\\n IconButton(\\n icon: Icon(Icons.share),\\n onPressed: () {},\\n padding: EdgeInsets.all(20),\\n ),\\n IconButton(\\n icon: Icon(Icons.add),\\n onPressed: () {},\\n visualDensity: VisualDensity.compact,\\n ),\\n IconButton(\\n icon: Icon(Icons.info),\\n onPressed: () {},\\n tooltip: \'这是一个提示信息\',\\n ),\\n FloatingActionButton(\\n onPressed: () {},\\n child: Icon(Icons.add),\\n ),\\n ],\\n)\\n
\\n图示:
\\n需求:电商应用“秒杀按钮”
实现
解决方案:
\\nclass FlashSaleButton extends StatefulWidget {\\n @override\\n _FlashSaleButtonState createState() => _FlashSaleButtonState();\\n}\\n\\nclass _FlashSaleButtonState extends State<FlashSaleButton> {\\n bool _isSoldOut = false;\\n bool _isProcessing = false;\\n int _countdown = 10;\\n\\n @override\\n void initState() {\\n super.initState();\\n _startCountdown();\\n }\\n\\n void _startCountdown() {\\n Timer.periodic(Duration(seconds: 1), (timer) {\\n if (_countdown == 0) {\\n timer.cancel();\\n setState(() => _isSoldOut = true);\\n } else {\\n setState(() => _countdown--);\\n }\\n });\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: Text(\\"Container Demo\\"),\\n centerTitle: true,\\n backgroundColor: Theme.of(context).colorScheme.inversePrimary,\\n ),\\n body: ElevatedButton.icon(\\n icon: _isProcessing ? CircularProgressIndicator() : Icon(Icons.flash_on),\\n label: Text(_isSoldOut ? \'已售罄\' : \'立即抢购 ($_countdown秒)\'),\\n onPressed: _isSoldOut || _isProcessing\\n ? null\\n : () async {\\n setState(() => _isProcessing = true);\\n await _purchaseItem();\\n setState(() => _isProcessing = false);\\n },\\n ),\\n );\\n }\\n\\n _purchaseItem() {\\n Future.delayed(Duration(milliseconds: 500), () {\\n // 延迟执行的代码\\n });\\n }\\n}\\n
\\n图示:
\\n// 错误示例:匿名函数导致重建 \\nElevatedButton( \\n onPressed: () => _handleClick(), \\n child: Text(\'Click\'), \\n) \\n\\n// 正确方式:使用预定义方法 \\nfinal VoidCallback _onPressed = _handleClick; \\n\\nElevatedButton( \\n onPressed: _onPressed, \\n child: const Text(\'Click\'), // 使用const \\n) \\n
\\nRepaintBoundary( \\n child: ElevatedButton( \\n // 高频更新的动画按钮 \\n style: _animatedStyle, \\n ), \\n) \\n
\\n作用:
\\nShader _createGradient(Size size) { \\n return LinearGradient( \\n colors: [Colors.cyan, Colors.indigo], \\n ).createShader(Rect.fromLTWH(0, 0, size.width, size.height)); \\n} \\n\\n@override \\nvoid paint(Canvas canvas, Size size) { \\n final paint = Paint()..shader = _createGradient(size); \\n canvas.drawRRect(..., paint); \\n} \\n
\\n关键点:
\\npaint
方法中动态创建着色器。build
阶段预计算渐变。// 核心继承链 \\nButtonStyleButton (abstract) \\n ├─ ElevatedButton \\n ├─ TextButton \\n └─ OutlinedButton \\n\\n// 实现关键接口 \\n- ToggleableStateMixin (用于保持按压状态) \\n- ThemeExtension<ButtonStyle> (主题扩展能力) \\n
\\n状态流转图:
\\n[Enabled] \\n │ \\n ├─ onHover → Hovered \\n ├─ onTapDown → Pressed \\n ├─ onFocus → Focused \\n └─ onDisable → Disabled \\n\\n[Disabled] \\n └─ (无事件响应) \\n
\\n源码关键路径:
\\n// 按钮状态更新入口 \\nvoid _handleStateUpdate(MaterialState newState) { \\n if (_states == newState) return; \\n setState(() => _states = newState); \\n // 触发样式重新计算 \\n _updateStyle(); \\n} \\n
\\n渲染管线:
\\nGestureDetector \\n → InkWell (处理点击反馈) \\n → Material (绘制背景/阴影) \\n → AnimatedContainer (状态过渡动画) \\n
\\n核心渲染逻辑:
\\nvoid _paintInkEffects(Canvas canvas, Offset offset) { \\n for (final effect in _inkEffects) { \\n effect.paint( \\n canvas, \\n size, \\n // 动态计算波纹半径 \\n _calculateClipRRect(offset), \\n ); \\n } \\n} \\n
\\n统一配置入口:
\\nButtonStyle
作为所有Material
按钮的样式基类。WidgetStateProperty
统一管理多状态样式。设计考量:
\\nAPI
适配不同按钮类型。全局主题即可影响所有按钮
。iOS vs Android
设计哲学:
维度 | Material Design | Cupertino Style |
---|---|---|
反馈形式 | 波纹扩散 + 阴影变化 | 透明度变化 + 缩放动画 |
交互时长 | 300ms 标准动画 | 200ms 快速响应 |
视觉层级 | 通过Elevation 表达 | 通过边框和颜色 对比表达 |
Flutter
的解决方案:
CupertinoButton
独立实现。ThemeData.platform
自动切换样式。自动处理机制:
\\nFocus
节点。Semantics
标签生成语音提示。自动调整颜色方案
。开发者扩展点:
\\nSemantics( \\n label: \'重要操作按钮\', \\n hint: \'双击可执行订单提交\', \\n child: ElevatedButton(...), \\n) \\n
\\n// 层级1:全局主题 (theme.dart) \\nThemeData( \\n elevatedButtonTheme: ElevatedButtonThemeData( \\n style: ElevatedButton.styleFrom(...), \\n ), \\n) \\n\\n// 层级2:组件库样式 (button_styles.dart) \\nclass AppButtonStyles { \\n static final rounded = ElevatedButton.styleFrom( \\n shape: RoundedRectangleBorder(...), \\n ); \\n} \\n\\n// 层级3:局部覆盖 (具体页面) \\nElevatedButton( \\n style: AppButtonStyles.rounded.copyWith(...), \\n) \\n
\\n禁止模式:
\\n// 错误:直接修改按钮状态 \\nonPressed: () { \\n setState(() => _buttonColor = Colors.red); \\n} \\n
\\n推荐模式:
\\n// 正确:通过状态机管理 \\nenum ButtonState { normal, loading, disabled } \\n\\nValueNotifier<ButtonState> state = ValueNotifier(ButtonState.normal); \\n\\nValueListenableBuilder( \\n valueListenable: state, \\n builder: (_, value, __) => ElevatedButton( \\n style: _getStyle(value), \\n onPressed: _getHandler(value), \\n ), \\n) \\n
\\nabstract class ThrottleButton extends StatefulWidget { \\n @override \\n _ThrottleButtonState createState() => _ThrottleButtonState(); \\n} \\n\\nclass _ThrottleButtonState extends State<ThrottleButton> { \\n bool _isProcessing = false; \\n DateTime? _lastClickTime; \\n\\n Future<void> _safeClick() async { \\n if (_isProcessing) return; \\n if (_lastClickTime != null && \\n DateTime.now().difference(_lastClickTime!) < \\n Duration(seconds: 2)) { \\n return; \\n } \\n\\n setState(() => _isProcessing = true); \\n _lastClickTime = DateTime.now(); \\n await widget.onPressed?.call(); \\n setState(() => _isProcessing = false); \\n } \\n} \\n
\\n使用Riverpod
实现全局状态管理:
final buttonStateProvider = StateNotifierProvider<ButtonStateNotifier, Map<String, bool>>( \\n (ref) => ButtonStateNotifier(), \\n); \\n\\nclass ButtonStateNotifier extends StateNotifier<Map<String, bool>> { \\n ButtonStateNotifier() : super({}); \\n\\n void setProcessing(String buttonId, bool value) { \\n state = {...state, buttonId: value}; \\n } \\n} \\n\\n// 使用 \\nConsumer( \\n builder: (context, ref, _) { \\n final isProcessing = ref.watch( \\n buttonStateProvider.select((s) => s[\'submitBtn\'] ?? false) \\n ); \\n return ElevatedButton( \\n onPressed: isProcessing ? null : _submit, \\n ); \\n }, \\n) \\n
\\nFlutter
的按钮系统是一个融合了Material Design
规范、高性能渲染
和灵活扩展能力
的复杂体系。通过深入理解其属性分类
、源码实现
和设计哲学
,我们可以:
系统化掌握按钮开发,不仅是学习一个组件,更是理解Flutter
设计思想的窗口。希望本文能成为你深入Flutter
世界的一块基石。
\\n","description":"前言 在移动应用开发中,按钮(Button)是用户交互的核心组件之一。Flutter通过丰富的按钮类型和灵活的定制能力,为开发者提供了构建高效、美观交互体验的工具。然而,许多开发者仅停留在基础使用层面,对按钮的底层原理、性能优化和设计哲学缺乏系统化认知。\\n\\n本文将通过六维知识体系,全面剖析Flutter中的按钮组件。无论你是想解决按钮点击卡顿问题,还是想定制独特的按钮动画,这篇文章都将提供系统化的解决方案。\\n\\n操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。\\n\\n一、基础认知\\n1.1、按钮类型(深度对比)\\n\\n在深入属性前,需明确不同按钮的核心设计目标:…","guid":"https://juejin.cn/post/7476031198117347355","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-28T00:23:30.673Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ce07abef2aa44139ae4e1f4dfe40b3e0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1741307010&x-signature=YEKmiv1MKpZ8dFZahLqNxUmOO6Q%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/bf41ac734f9345899ca48b21049b4853~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1741307010&x-signature=zzViVqp58pgdQKr6YZ46kbIr1Zw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/011c913cc5c141a684136dbb551eb27c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1741307010&x-signature=9zEhQuqWMowg%2Fl9cd2KDyKaOogE%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"Frenda:一个专注于简化Dart实体生成的工具","url":"https://juejin.cn/post/7475779230644568073","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
\\n\\n在Flutter开发中,处理JSON序列化、对象拷贝等操作是很常见的需求。现有的方案各有特点,但在使用过程中也存在一些不便,这促使我开发了Frenda —— 一个致力于简化这些操作的代码生成工具。
\\n
目前Flutter生态中比较常用的代码生成方案有:
\\njson_serializable:
\\nfreezed:
\\nFrenda的目标是通过简单的类定义自动生成必要的代码。让我们通过一个例子来说明:
\\nimport \'package:frenda/frenda.dart\';\\n\\npart \'simple.dart\';\\n\\n// 注解标注,类名必须以 $ 进行开头\\n@frenda\\nclass $Simple {\\n // 定义一个字段\\n late String firstField;\\n \\n // 带默认值的final字段\\n late final int secondField = 10;\\n \\n // 自定义JSON序列化时的字段名\\n // 生成时会使用\'third_field\'作为JSON的key\\n @Filed(\'third_field\')\\n late bool? thirdField;\\n}\\n
\\n运行代码生成后,Frenda会自动生成如下实现:
\\n// 统一实现了Serializable接口\\nclass Simple implements Serializable {\\n String firstField;\\n final int secondField;\\n bool? thirdField;\\n\\n // 构造函数,required标记必填字段\\n Simple({\\n required this.firstField, \\n this.secondField = 10, \\n this.thirdField\\n });\\n\\n // JSON序列化\\n Map<String, dynamic> toJson() => {\\n \'firstField\': firstField,\\n \'secondField\': secondField,\\n \'third_field\': thirdField, // 使用自定义的字段名\\n };\\n\\n // JSON反序列化\\n factory Simple.fromJson(Map<String, dynamic> json) => Simple(\\n firstField: json[\'firstField\'] as String,\\n secondField: json[\'secondField\'] as int,\\n thirdField: json[\'third_field\'] as bool?,\\n );\\n\\n // 对象拷贝,保持不可变性\\n Simple copyWith({String? firstField, int? secondField, bool? thirdField}) => \\n Simple(/*...*/);\\n\\n // 清晰的字符串表示\\n @override\\n String toString() => \'Simple(firstField: $firstField, /*...*/}\';\\n\\n // 正确的相等性比较\\n @override\\n bool operator ==(Object other) => /*...*/;\\n\\n // 配套的hashCode实现\\n @override\\n int get hashCode => Object.hash(firstField, secondField, thirdField);\\n}\\n
\\n注重简洁
\\n保持灵活
\\ndependencies:\\n frenda: any\\n\\ndev_dependencies:\\n build_runner: any\\n
\\ntargets:\\n $default:\\n builders:\\n frenda: \\n options:\\n prefix: \\"$\\" # 类名前缀,默认为$\\n
\\nFrenda基于Dart的代码生成体系构建,主要使用了:
\\nbuild
: 代码生成框架source_gen
: Dart代码生成工具analyzer
: Dart代码分析器生成过程包括:
\\nFrenda是一个实用的代码生成工具,它的目标是减少重复工作,帮助开发者专注于业务逻辑。
\\n目前还在持续改进中,欢迎社区的朋友们试用并提出宝贵意见。
\\n\\n如果项目对你有帮助,欢迎Star支持。有任何建议或问题,也欢迎通过Issue交流!
","description":"在Flutter开发中,处理JSON序列化、对象拷贝等操作是很常见的需求。现有的方案各有特点,但在使用过程中也存在一些不便,这促使我开发了Frenda —— 一个致力于简化这些操作的代码生成工具。 现状分析\\n\\n目前Flutter生态中比较常用的代码生成方案有:\\n\\njson_serializable:\\n\\n优点:生成JSON序列化相关代码,使用广泛\\n不足:需要额外编写copyWith等实用方法\\n\\nfreezed:\\n\\n优点:提供了较为完整的功能支持\\n不足:\\n需要额外依赖json_serializable\\n生成的泛型类缺少统一的接口约束\\n需要手动编写部…","guid":"https://juejin.cn/post/7475779230644568073","author":"谕酱","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-27T07:40:35.012Z","media":null,"categories":["前端","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"Flutter dio 手把手教你封装一个实用网络工具","url":"https://juejin.cn/post/7475651131449819136","content":"实现功能
\\n在 Flutter 中,dio
是一个强大的 HTTP 客户端,用于发送各种网络请求,如 GET、POST、PUT、DELETE 等。
在 pubspec.yaml
文件中添加 dio
依赖:
dependencies:\\n dio: ^5.3.2\\n
\\nimport \'package:dio/dio.dart\';\\n\\nvoid fetchData() async {\\n try {\\n // 创建 Dio 实例\\n Dio dio = Dio();\\n // 发送 GET 请求\\n Response response = await dio.get(\'https://jsonplaceholder.typicode.com/posts/1\');\\n // 打印响应数据\\n print(response.data);\\n } catch (e) {\\n // 打印错误信息\\n print(\'请求出错: $e\');\\n }\\n}\\n
\\nimport \'package:dio/dio.dart\';\\n\\nvoid postData() async {\\n try {\\n Dio dio = Dio();\\n // 定义请求数据\\n Map<String, dynamic> data = {\\n \'title\': \'foo\',\\n \'body\': \'bar\',\\n \'userId\': 1\\n };\\n // 发送 POST 请求\\n Response response = await dio.post(\'https://jsonplaceholder.typicode.com/posts\', data: data);\\n print(response.data);\\n } catch (e) {\\n print(\'请求出错: $e\');\\n }\\n}\\n
\\n因为我们项目中可能需要自定义header
,设置mockUrl
等需求,我们这个时候可以自定义一个RequestOptions,帮组我们来统一管理Request
/// 请求方式\\nenum MyRequestMethod { get, post }\\n\\nclass MyRequestOptions {\\n /// 请求方式\\n MyRequestMethod method = MyRequestMethod.get;\\n\\n /// 基础url\\n String baseUrl =\\n \\"https://mockapi.eolink.com/uvemJdBf6d6fe15694c6ce211778969e0cfaacf4f97f262\\";\\n\\n /// 请求路径\\n String urlPath = \\"\\";\\n\\n /// 参数\\n Map<String, dynamic> params = Map<String, dynamic>();\\n\\n /// HTTP 请求头。\\n Map<String, dynamic> headers = Map<String, dynamic>();\\n\\n /// 连接服务器超时时间.\\n Duration connectTimeout = Duration(seconds: 10);\\n\\n /// 接收数据的超时设置。\\n ///\\n /// 这里的超时对应的时间是:\\n /// - 在建立连接和第一次收到响应数据事件之前的超时。\\n /// - 每个数据事件传输的间隔时间,而不是接收的总持续时间。\\n ///\\n /// 超时时会抛出类型为 [DioExceptionType.receiveTimeout] 的\\n /// [DioException]。\\n ///\\n /// `null` 或 `Duration.zero` 即不设置超时。\\n Duration receiveTimeout = Duration(seconds: 10);\\n\\n MyRequestOptions({required String url, Map<String, dynamic>? paramsMap}) {\\n urlPath = url;\\n if (paramsMap != null) {\\n params.addAll(paramsMap);\\n }\\n\\n // 设置默认header\\n _addDefaultHeader();\\n }\\n\\n /// 设置Mockurl\\n void setMockUrl({required String mockUrl}) {\\n if (kDebugMode) {\\n baseUrl = mockUrl;\\n }\\n }\\n\\n String getMethod() {\\n if (method == MyRequestMethod.get) {\\n return \\"get\\";\\n }\\n return \\"post\\";\\n }\\n\\n /// 设置默认header\\n void _addDefaultHeader() {\\n Map<String, dynamic> defaultHeader = {\\n \\"Content-Type\\": \\"application/json; charset=utf-8\\",\\n \\"Accept\\": \\"application/json\\"\\n };\\n headers.addAll(defaultHeader);\\n }\\n\\n /// 设置header\\n void setHeader({required Map<String, dynamic> headerMap}) {\\n headers.addAll(headerMap);\\n }\\n}\\n
\\n在发起请求以后,我们可能会因为各种情况需要管理这个请求,如取消某一个请求,或者取消全部请求等等,这个时候我们最好有一个管理工具类
\\nvoid cancelMultipleRequests() async {\\n Dio dio = Dio();\\n CancelToken cancelToken = CancelToken();\\n\\n try {\\n // 第一个请求\\n dio.get(\\n \'https://jsonplaceholder.typicode.com/posts/1\',\\n cancelToken: cancelToken,\\n );\\n } on DioException catch (e) {\\n if (e.type == DioExceptionType.cancel) {\\n print(\'请求已取消: ${e.message}\');\\n } else {\\n print(\'请求出错: ${e.message}\');\\n }\\n }\\n\\n // 取消所有使用该 CancelToken 的请求\\n cancelToken.cancel(\'批量取消请求\');\\n}\\n
\\n我们为每一个请求都创建一个cancelToken来管理,cancelToken的生成是根据MyRequestOptions
来生成的
获取CancelTokenKey
\\nString getCancelTokenKey({required MyRequestOptions options}) {\\n String url = options.baseUrl + options.urlPath;\\n String paramString = options.params.toString();\\n String cancelTokenKey = (url + paramString).md5Hash();\\n return cancelTokenKey;\\n }\\n
\\n获取CancelToken
\\nCancelToken getCancelToken({required MyRequestOptions options}) {\\n String cancelTokenKey = getCancelTokenKey(options: options);\\n CancelToken? cancelToken;\\n cancelToken = cancelTokens[cancelTokenKey];\\n if (cancelToken == null) {\\n cancelToken = CancelToken();\\n cancelTokens[cancelTokenKey] = cancelToken;\\n }\\n return cancelToken;\\n }\\n
\\n我们使用一个单例来管理我们的CancelToken
\\nclass MyDioManager {\\n final Map<String, CancelToken> cancelTokens = {};\\n // 静态私有实例,初始值为 null\\n static MyDioManager? _instance;\\n // 私有构造函数\\n MyDioManager._privateConstructor();\\n\\n // 静态工厂方法,用于获取单例实例\\n static MyDioManager get instance {\\n // 创建一个锁对象\\n final Lock lock = Lock();\\n lock.synchronized(() {\\n _instance ??= MyDioManager._privateConstructor();\\n });\\n return _instance!;\\n }\\n\\n // 取消某个请求\\n void cancelRequest(String cancelTokenKey) {\\n final cancelToken = cancelTokens[cancelTokenKey];\\n if (cancelToken != null) {\\n cancelToken.cancel(\'Request cancelled by user\');\\n cancelTokens.remove(cancelTokenKey);\\n }\\n }\\n\\n // 取消全部请求\\n void cancelAllRequests() {\\n cancelTokens.forEach((key, cancelToken) {\\n cancelToken.cancel(\'All requests cancelled by user\');\\n });\\n cancelTokens.clear();\\n }\\n}\\n
\\nDio是基于Dart的http包开发的,但Dart本身在标准库中不提供这些底层的网络指标。我们可以通过一些自定义的方式来实现这些统计
\\nDNS解析通常发生在建立TCP连接之前。Dart的Socket类在连接时会解析DNS,但是dio并没有暴露相关信息,我们需要自己实现一个自定义的连接器,比如继承自Dio的CustomHttpClientAdapter
,然后重写一些方法,在发起请求时记录时间。
对于DNS时间,可以在打开连接时记录开始时间,当Socket连接建立时,DNS解析已经完成,这时候可以计算DNS耗时
\\n // DNS解析开始时间\\n final dnsStartTime = DateTime.now();\\n // 创建HttpClient\\n final httpClient = HttpClient();\\n httpClient.badCertificateCallback =\\n (X509Certificate cert, String host, int port) => true;\\n // 解析DNS\\n final uri = options.uri;\\n final addresses = await InternetAddress.lookup(uri.host);\\n // DNS解析结束时间\\n final dnsEndTime = DateTime.now();\\n final dnsTime = dnsEndTime.difference(dnsStartTime).inMilliseconds;\\n
\\nTCP连接的时间是从开始连接到连接成功的时间差。同样需要在发起连接的时候记录开始时间,连接成功后记录结束时间,计算差值
\\n// TCP 连接耗时\\n final tcpStart = DateTime.now();\\n var socket = await Socket.connect(\\n addresses.first,\\n options.uri.port ?? (options.uri.scheme == \'https\' ? 443 : 80),\\n );\\n timings[\'tcpTime\'] = DateTime.now().difference(tcpStart).inMilliseconds;\\n\\n
\\nSSL握手时间的话,如果是HTTPS请求,在建立TCP连接之后会进行SSL握手。这时候可以在SecureSocket.connect的时候记录时间,计算SSL握手的时间差。这需要覆盖处理HTTPS的部
\\n // SSL握手开始时间(如果是HTTPS)\\n final sslStartTime = DateTime.now();\\n SecureSocket? secureSocket;\\n if (uri.scheme == \'https\') {\\n secureSocket = await SecureSocket.secure(\\n socket,\\n host: uri.host,\\n onBadCertificate: (cert) => true,\\n );\\n }\\n // SSL握手结束时间\\n final sslEndTime = DateTime.now();\\n
\\n即从请求发送到接收到第一个响应包的时间。这个可以通过拦截器来记录。在发送请求前记录时间,然后在接收到响应时记录第一个字节到达的时间,计算差值
\\n// 首包时间开始记录\\n final firstPacketStartTime = DateTime.now();\\n // 使用默认适配器发送请求\\n final response = await _defaultAdapter.fetch(\\n options,\\n requestStream,\\n cancelFuture,\\n );\\n // 首包时间结束记录\\n final firstPacketEndTime = DateTime.now();\\n final firstPacketTime =\\n firstPacketEndTime.difference(firstPacketStartTime).inMilliseconds;\\n
\\nimport \'dart:async\';\\nimport \'dart:io\';\\nimport \'dart:typed_data\';\\nimport \'package:dio/dio.dart\';\\nimport \'package:dio/io.dart\';\\n\\nclass CustomHttpClientAdapter implements HttpClientAdapter {\\n final HttpClientAdapter _defaultAdapter = DefaultHttpClientAdapter();\\n\\n @override\\n Future<ResponseBody> fetch(\\n RequestOptions options,\\n Stream<Uint8List>? requestStream,\\n Future? cancelFuture,\\n ) async {\\n final timings = <String, num>{};\\n\\n // 记录开始时间\\n final startTime = DateTime.now();\\n\\n // DNS解析开始时间\\n final dnsStartTime = DateTime.now();\\n\\n // 创建HttpClient\\n final httpClient = HttpClient();\\n httpClient.badCertificateCallback =\\n (X509Certificate cert, String host, int port) => true;\\n\\n // 解析DNS\\n final uri = options.uri;\\n final addresses = await InternetAddress.lookup(uri.host);\\n // DNS解析结束时间\\n final dnsEndTime = DateTime.now();\\n final dnsTime = dnsEndTime.difference(dnsStartTime).inMilliseconds;\\n\\n // TCP连接开始时间\\n final tcpStartTime = DateTime.now();\\n final socket = await Socket.connect(\\n addresses.first,\\n uri.port,\\n timeout: const Duration(seconds: 10),\\n );\\n // TCP连接结束时间\\n final tcpEndTime = DateTime.now();\\n final tcpTime = tcpEndTime.difference(tcpStartTime).inMilliseconds;\\n\\n // SSL握手开始时间(如果是HTTPS)\\n final sslStartTime = DateTime.now();\\n SecureSocket? secureSocket;\\n if (uri.scheme == \'https\') {\\n secureSocket = await SecureSocket.secure(\\n socket,\\n host: uri.host,\\n onBadCertificate: (cert) => true,\\n );\\n }\\n // SSL握手结束时间\\n final sslEndTime = DateTime.now();\\n final sslTime = sslEndTime.difference(sslStartTime).inMilliseconds;\\n // 首包时间开始记录\\n final firstPacketStartTime = DateTime.now();\\n // 使用默认适配器发送请求\\n final response = await _defaultAdapter.fetch(\\n options,\\n requestStream,\\n cancelFuture,\\n );\\n // 首包时间结束记录\\n final firstPacketEndTime = DateTime.now();\\n final firstPacketTime =\\n firstPacketEndTime.difference(firstPacketStartTime).inMilliseconds;\\n // 总耗时\\n final totalTime = DateTime.now().difference(startTime).inMilliseconds;\\n\\n // 打印统计信息\\n timings[\'dns\'] = dnsTime;\\n timings[\'tcp\'] = tcpTime;\\n timings[\'ssl\'] = sslTime;\\n timings[\'first_packet\'] = firstPacketTime;\\n timings[\'totalTime\'] = totalTime;\\n\\n // print(\'DNS解析耗时: $dnsTime ms\');\\n // print(\'TCP三次握手耗时: $tcpTime ms\');\\n // print(\'SSL握手耗时: $sslTime ms\');\\n // print(\'首包时间: $firstPacketTime ms\');\\n // print(\'总耗时: $totalTime ms\');\\n\\n // 将耗时数据存入请求配置的 extra 字段\\n options.extra.addAll(timings);\\n return response;\\n }\\n\\n @override\\n void close({bool force = false}) {\\n _defaultAdapter.close(force: force);\\n }\\n}\\n\\n
\\n使用时
\\n _dio.httpClientAdapter = CustomHttpClientAdapter();\\n
\\n我们可以把将耗时数据存入请求配置的 extra 字段,方便我们使用日志拦截器时,打印整个请求详细的信息
\\njson转model我是借助于json_annotation
实现的,我定义了两个基类model,用于解析普通类型MyBaseModel
和数组类型MyBaseListModel
@JsonSerializable(genericArgumentFactories: true, converters: [SafeNumConverter()])\\nclass MyBaseModel<T> extends SafeConvertModel {\\n @JsonKey(name: \'code\')\\n num? code;\\n @JsonKey(name: \'message\')\\n String? message;\\n T? data;\\n \\n /// 是否成功\\n bool isSucess() {\\n bool result = this.code?.toInt() == 0;\\n return result;\\n }\\n}\\nclass MyBaseListModel<T> {\\n @JsonKey(name: \'code\')\\n num? code;\\n @JsonKey(name: \'message\')\\n String? message;\\n List<T>? data;\\n /// 是否成功\\n bool isSucess() {\\n bool result = this.code == 0;\\n return result;\\n }\\n}\\n
\\n在NetworkService
中封装json转model
Future<MyBaseModel<T>> get<T>(\\n {required MyRequestOptions options,\\n required T Function(Object? json) fromJsonT}) async {\\n // 发起请求\\n MyResopnseModel response = await _request(options: options);\\n if (response.isHttpSucess() == true) {\\n try {\\n return MyBaseModel.fromJson(\\n response.data,\\n fromJsonT,\\n );\\n } catch (e, stackTrace) {\\n print(\'json转model失败: $e\');\\n throw e;\\n }\\n } else {\\n throw _handleError(resopnse: response);\\n }\\n }\\n
\\n使用示例
\\ntry {\\n // 发起 GET 请求获取用户信息\\n MyBaseModel<User> result = await networkService.get<User>(\\n \'/user\',\\n fromJsonT: (json) => User.fromJson(json as Map<String, dynamic>),\\n );\\n\\n if (result.isSucess()) {\\n print(\'User name: ${result.data?.name}\');\\n print(\'User age: ${result.data?.age}\');\\n } else {\\n print(\'Request failed: ${result.message}\');\\n }\\n } catch (e) {\\n print(\'Error: $e\');\\n }\\n
\\n如果T
是基础类型
MyBaseModel<int> model = MyBaseModel.fromJson(\\n jsonMap,\\n (json) => json as int, // 直接将 JSON 值转换为 int\\n );\\n
\\nInterceptor
(拦截器)Interceptor
是 dio
库中的一个抽象类,它允许你在请求发送前、响应返回后以及请求发生错误时插入自定义逻辑。通过实现 Interceptor
类的方法,你可以对请求和响应进行拦截和修改。
Interceptor
类有三个主要的方法,分别用于处理请求、响应和错误:
onRequest
作用:在请求发送之前被调用,可用于修改请求选项,如添加请求头、修改请求参数等。
\\nclass AuthInterceptor extends Interceptor {\\n @override\\n void onRequest(RequestOptions options, RequestInterceptorHandler handler) {\\n // 添加授权头\\n options.headers[\'Authorization\'] = \'Bearer your_token\';\\n handler.next(options);\\n }\\n}\\n
\\nonResponse
作用:在响应返回之后被调用,可用于处理响应数据,如解析数据、缓存数据等。
\\nclass DataParserInterceptor extends Interceptor {\\n @override\\n void onResponse(Response response, ResponseInterceptorHandler handler) {\\n // 解析响应数据\\n if (response.data is Map) {\\n // 处理 Map 类型的数据\\n }\\n handler.next(response);\\n }\\n}\\n
\\nonError
作用:在请求发生错误时被调用,可用于统一处理错误,如重试请求、显示错误信息等。
\\nclass RetryInterceptor extends Interceptor {\\n int maxRetries = 3;\\n\\n @override\\n void onError(DioException err, ErrorInterceptorHandler handler) async {\\n int retryCount = 0;\\n while (retryCount < maxRetries) {\\n try {\\n // 重试请求\\n Response response = await err.requestOptions.createDio().fetch(err.requestOptions);\\n handler.resolve(response);\\n return;\\n } catch (e) {\\n retryCount++;\\n }\\n }\\n handler.next(err);\\n }\\n}\\n
\\ndio
中的拦截器是按照添加的顺序依次执行的。在请求阶段,拦截器按照添加顺序依次处理请求;在响应阶段,拦截器按照相反的顺序依次处理响应。例如:
dio.interceptors.add(Interceptor1());\\ndio.interceptors.add(Interceptor2());\\n
\\n请求处理顺序:Interceptor1
-> Interceptor2
\\n响应处理顺序:Interceptor2
-> Interceptor1
根据以往的业务需求,我定义了下面缓存策略
\\n/// 缓存策略\\nenum MyNetworkCachePolicy {\\n /// 不用缓存\\n none,\\n /// 先用缓存,在请求网络,得到网络数据后覆盖缓存\\n firstCache,\\n /// 先请求网络,失败后再返回缓存\\n firstRequest,\\n}\\n
\\n缓存管理类主要任务
\\nclass MyNetworkCacheManager {\\n /// 缓存策略\\n final MyNetworkCachePolicy cachePolicy = MyNetworkCachePolicy.none;\\n /// 缓存过期时间(单位:秒)\\n final int cacheExpirationTime = 24 * 60 * 60;\\n\\n /// 获取缓存\\n Future<String?> getCacheData(RequestOptions options) async {\\n final filePath = _getFilePath(options);\\n final fileUtils = FileUtils();\\n String? jsonString = await fileUtils.getFile(filePath);\\n\\n if (jsonString != null) {\\n Map<String, dynamic> jsonMap = jsonString.toMap();\\n int timestamp = jsonMap[\'timestamp\'];\\n // 检查缓存是否过期\\n if (DateTime.now().millisecondsSinceEpoch - timestamp < cacheExpirationTime * 1000) {\\n return jsonMap[\'data\'];\\n }\\n // 若缓存过期,删除缓存\\n await _remove(options);\\n return null;\\n }\\n return null;\\n }\\n\\n /// 保存缓存\\n Future<void> saveCache(RequestOptions options, String data) async {\\n final filePath = _getFilePath(options);\\n final fileUtils = FileUtils();\\n Map<String, dynamic> cachedData = {\\n \'timestamp\': DateTime.now().millisecondsSinceEpoch,\\n \'data\': data\\n };\\n final jsonString = json.encode(cachedData);\\n fileUtils.writeFile(filePath, jsonString);\\n }\\n\\n\\n Future<void> _remove(RequestOptions options) async{\\n final filePath = _getFilePath(options);\\n final fileUtils = FileUtils();\\n fileUtils.removeFilePath(filePath);\\n }\\n\\n /// 获取文件路径\\n String _getFilePath(RequestOptions options) {\\n String url = options.uri.toString();\\n String paramJsonString = options.queryParameters.toString();\\n String method = options.method;\\n return (method + url + paramJsonString).md5Hash();\\n }\\n}\\n\\n
\\nfirstCache
缓存策略下,如果我们能够拿到缓存可以直接执行handler.resolve(response);
handler.next(options);
@override\\n void onRequest(\\n RequestOptions options, RequestInterceptorHandler handler) async {\\n final MyNetworkCachePolicy cachePolicy = cacheManager.cachePolicy;\\n if (cachePolicy == MyNetworkCachePolicy.firstCache) {\\n final cacheJsonString = await cacheManager.getCacheData(options);\\n if (cacheJsonString != null) {\\n // 有缓存数据,先返回缓存响应\\n final response = Response(\\n requestOptions: options,\\n data: json.decode(cacheJsonString),\\n statusCode: 200,\\n );\\n handler.resolve(response);\\n return;\\n }\\n }\\n\\n // 继续请求网络\\n handler.next(options);\\n }\\n
\\n只缓存GET请求成功响应以及缓存策略是none
@override\\n void onResponse(Response response, ResponseInterceptorHandler handler) {\\n // 只缓存GET请求成功响应\\n if (response.requestOptions.method == \'GET\' &&\\n response.statusCode == 200 &&\\n cacheManager.cachePolicy != MyNetworkCachePolicy.none) {\\n try {\\n String data = json.encode(response.data);\\n cacheManager.saveCache(response.requestOptions, data);\\n } catch (e) {}\\n }\\n\\n handler.next(response);\\n }\\n
\\nonError
\\n@override\\n Future<void> onError(\\n DioException err, ErrorInterceptorHandler handler) async {\\n if (cacheManager.cachePolicy == MyNetworkCachePolicy.firstRequest) {\\n final cacheJsonString =\\n await cacheManager.getCacheData(err.requestOptions);\\n if (cacheJsonString != null) {\\n // 有缓存数据,先返回缓存响应\\n final response = Response(\\n requestOptions: err.requestOptions,\\n data: json.decode(cacheJsonString),\\n statusCode: 200,\\n );\\n // 返回正确的响应\\n return handler.resolve(response);\\n }\\n }\\n\\n // 继续传递错误\\n handler.next(err);\\n }\\n
\\n根据以往业务需求,我希望token续租拦截器有能够实现一下功能
\\n基于以上要求,我可以借助QueuedInterceptorsWrapper
拦截器帮我们实现相关功能
QueuedInterceptorsWrapper
是 dio
库中的一个拦截器包装器。
QueuedInterceptorsWrapper
的主要作用是确保多个拦截器按照队列的顺序依次执行。它会将多个拦截器的处理逻辑包装起来,使得每个拦截器的处理逻辑依次执行,并且可以处理异步操作。
QueuedInterceptorsWrapper
会接收多个拦截器作为参数,并将它们存储在一个列表中。
import \'package:dio/dio.dart\';\\n\\nclass QueuedInterceptorsWrapper extends InterceptorsWrapper {\\n final List<Interceptor> _interceptors;\\n\\n QueuedInterceptorsWrapper({required List<Interceptor> interceptors})\\n : _interceptors = interceptors;\\n\\n // 其他方法...\\n}\\n
\\n在 onRequest
方法中,QueuedInterceptorsWrapper
会依次调用每个拦截器的 onRequest
方法。如果某个拦截器返回 RequestOptions
或者 Response
,则会终止后续拦截器的执行。
@override\\nFuture<void> onRequest(\\n RequestOptions options, RequestInterceptorHandler handler) async {\\n for (var interceptor in _interceptors) {\\n var shouldContinue = await _handleInterceptor(\\n interceptor.onRequest,\\n options,\\n (newOptions) {\\n if (newOptions is RequestOptions) {\\n options = newOptions;\\n } else if (newOptions is Response) {\\n handler.resolve(newOptions);\\n return false;\\n }\\n return true;\\n },\\n );\\n if (!shouldContinue) {\\n return;\\n }\\n }\\n handler.next(options);\\n}\\n\\nFuture<bool> _handleInterceptor(\\n FutureOr<dynamic> Function(RequestOptions, RequestInterceptorHandler)\\n interceptorFunction,\\n RequestOptions options,\\n bool Function(dynamic) resultHandler) async {\\n try {\\n var result = await interceptorFunction(options, RequestInterceptorHandler());\\n return resultHandler(result);\\n } catch (e) {\\n return false;\\n }\\n}\\n
\\n在 onResponse
方法中,QueuedInterceptorsWrapper
会依次调用每个拦截器的 onResponse
方法。
@override\\nFuture<void> onResponse(\\n Response response, ResponseInterceptorHandler handler) async {\\n for (var interceptor in _interceptors) {\\n var shouldContinue = await _handleInterceptor(\\n (options, _) => interceptor.onResponse(response, ResponseInterceptorHandler()),\\n response.requestOptions,\\n (newResponse) {\\n if (newResponse is Response) {\\n response = newResponse;\\n }\\n return true;\\n },\\n );\\n if (!shouldContinue) {\\n return;\\n }\\n }\\n handler.next(response);\\n}\\n
\\n在 onError
方法中,QueuedInterceptorsWrapper
会依次调用每个拦截器的 onError
方法。
@override\\nFuture<void> onError(DioError err, ErrorInterceptorHandler handler) async {\\n for (var interceptor in _interceptors) {\\n var shouldContinue = await _handleInterceptor(\\n (options, _) => interceptor.onError(err, ErrorInterceptorHandler()),\\n err.requestOptions,\\n (newError) {\\n if (newError is DioError) {\\n err = newError;\\n }\\n return true;\\n },\\n );\\n if (!shouldContinue) {\\n return;\\n }\\n }\\n handler.next(err);\\n}\\n
\\nclass CsrfTokenInterceptor extends QueuedInterceptor {\\n String? _csrfToken;\\n bool _isFetchingToken = false;\\n\\n @override\\n Future<void> onRequest(\\n RequestOptions options,\\n RequestInterceptorHandler handler,\\n ) async {\\n // 1. 检查是否需要添加 CSRF Token(根据实际需求调整条件)\\n if (options.path.startsWith(\'/secure/\')) {\\n // 2. 如果没有 token 且不在获取中\\n if (_csrfToken == null && !_isFetchingToken) {\\n _isFetchingToken = true;\\n \\n try {\\n // 3. 获取新 token\\n final newToken = await _fetchCsrfToken();\\n _csrfToken = newToken;\\n _isFetchingToken = false;\\n \\n // 4. 更新当前请求的 header\\n options.headers[\'X-CSRF-TOKEN\'] = newToken;\\n \\n // 5. 放行当前请求\\n handler.next(options);\\n } catch (e) {\\n // 6. 获取 token 失败,终止请求链\\n _isFetchingToken = false;\\n handler.reject(DioException(\\n requestOptions: options,\\n error: \'Failed to get CSRF token: $e\',\\n ));\\n }\\n } \\n // 7. 如果 token 正在获取中,等待直到获取完成\\n else if (_isFetchingToken) {\\n // 延迟重试逻辑\\n Future.delayed(Duration(milliseconds: 100), () {\\n onRequest(options, handler);\\n });\\n }\\n // 8. 已有 token 直接添加\\n else {\\n options.headers[\'X-CSRF-TOKEN\'] = _csrfToken;\\n handler.next(options);\\n }\\n } else {\\n // 不需要 CSRF token 的请求直接放行\\n handler.next(options);\\n }\\n }\\n\\n Future<String> _fetchCsrfToken() async {\\n print(\'开始获取 CSRF Token...\');\\n // 模拟网络请求延迟\\n await Future.delayed(Duration(seconds: 1));\\n \\n // 模拟获取 token(实际应该发送真实请求)\\n final mockToken = \'csrf_token_${DateTime.now().millisecondsSinceEpoch}\';\\n print(\'获取到 CSRF Token: $mockToken\');\\n \\n return mockToken;\\n }\\n\\n @override\\n void onError(DioException err, ErrorInterceptorHandler handler) {\\n // 401 状态码时清除 token(示例逻辑)\\n if (err.response?.statusCode == 401) {\\n _csrfToken = null;\\n print(\'CSRF Token 已失效,已清除\');\\n }\\n handler.next(err);\\n }\\n}\\n
\\nQueuedInterceptor
确保所有请求按顺序进入拦截器_isFetchingToken
标志位防止重复请求onError
中处理 401 未授权情况,自动清除失效 tokenloading拦截器实现比较简单:就一个参数是否显示loading
\\nclass LoadingInterceptor extends Interceptor {\\n /// 是否显示loading\\n final bool isShowLoading;\\n LoadingInterceptor({required this.isShowLoading});\\n\\n @override\\n void onRequest(RequestOptions options, RequestInterceptorHandler handler) {\\n // 在请求发起时显示加载提示\\n if (isShowLoading) {\\n _showLoading();\\n }\\n super.onRequest(options, handler);\\n }\\n\\n @override\\n void onError(DioError err, ErrorInterceptorHandler handler) {\\n // 在请求出错时隐藏加载提示\\n if (isShowLoading) {\\n _hideLoading();\\n }\\n super.onError(err, handler);\\n }\\n\\n @override\\n void onResponse(Response response, ResponseInterceptorHandler handler) {\\n // 在请求成功响应后隐藏加载提示\\n if (isShowLoading) {\\n _hideLoading();\\n }\\n\\n super.onResponse(response, handler);\\n }\\n\\n /// 弹窗\\n void _showLoading() {\\n ToastUtil.showLoading();\\n }\\n\\n /// 隐藏弹窗\\n void _hideLoading() {\\n ToastUtil.dismiss();\\n }\\n}\\n
\\nMap<String, String> _errorCodeMessage = {\\n \\"400\\": \\"状态码:400 请求参数错误\\",\\n \\"401\\": \\"状态码:401 身份验证错误\\",\\n \\"403\\": \\"状态码:403 服务器拒绝请求\\",\\n \\"404\\": \\"状态码:404 找不到服务器地址\\",\\n \\"407\\": \\"状态码:407 需要代理授权\\",\\n \\"408\\": \\"状态码:408 请求超时\\",\\n \\"500\\": \\"状态码:500 服务器内部错误\\",\\n \\"501\\": \\"状态码:501 尚未实施\\",\\n \\"502\\": \\"状态码:502 错误网关\\",\\n \\"503\\": \\"状态码:503 服务不可用\\",\\n \\"504\\": \\"状态码:504 网关超时\\",\\n \\"505\\": \\"HTTP 版本不受支持\\",\\n \\"-1000\\": \\"解析不到数据\\"\\n};\\n\\n/*\\n * 特殊状态code处理的拦截器,\\n * 401 弹出弹窗提示用户重新登录\\n */\\nclass ErrorHandleInterceptor extends Interceptor {\\n /// 是否显示http网络请求错误\\n final bool isShowHttpErrorMsg;\\n /// 响应code不为0异常\\n final bool isShowDataErrorMsg;\\n\\n ErrorHandleInterceptor(\\n {required this.isShowHttpErrorMsg, required this.isShowDataErrorMsg});\\n\\n @override\\n void onError(DioError error, ErrorInterceptorHandler handler) {\\n // 自定义错误处理逻辑\\n if (isShowHttpErrorMsg) {\\n _handleHttpError(error);\\n }\\n handler.next(error);\\n }\\n\\n @override\\n void onResponse(Response response, ResponseInterceptorHandler handler) {\\n if (isShowDataErrorMsg) {\\n _handleDataError(response);\\n }\\n super.onResponse(response, handler);\\n }\\n\\n /// 网络异常\\n void _handleHttpError(DioError error) {\\n String errorMsg = \\"网络异常\\";\\n switch (error.type) {\\n case DioExceptionType.connectionTimeout:\\n errorMsg = \'连接超时\';\\n case DioExceptionType.sendTimeout:\\n errorMsg = \'发送超时\';\\n case DioExceptionType.receiveTimeout:\\n errorMsg = \'接受超时\';\\n case DioExceptionType.badCertificate:\\n errorMsg = \'无效证书\';\\n case DioExceptionType.badResponse:\\n errorMsg = \'无效响应\';\\n case DioExceptionType.cancel:\\n errorMsg = \'请求取消\';\\n case DioExceptionType.connectionError:\\n errorMsg = \'链接错误\';\\n case DioExceptionType.unknown:\\n errorMsg = \'未知错误\';\\n }\\n\\n int? code = error.response?.statusCode;\\n if (code != null) {\\n String codeString = code.toString();\\n errorMsg = _errorCodeMessage[codeString] ?? \\"网络异常\\";\\n }\\n\\n if (isShowHttpErrorMsg) {\\n if (kDebugMode) {\\n errorMsg = \\"网络异常\\";\\n }\\n ToastUtil.showToast(msg: errorMsg);\\n }\\n }\\n\\n /// 网络异常\\n void _handleDataError(Response response) {\\n Map<String, dynamic> _data = {};\\n if (response.data is Map) {\\n _data = response.data as Map<String, dynamic>;\\n } else if (response.data is String) {\\n _data = response.data.toMap();\\n }\\n final num? code = JsonTypeAdapter.safeParseNumber(_data[\'code\']);\\n // 检查 code 是否为 0\\n if (code != null && code.toInt() != 0) {\\n final String message = _data[\'message\'] as String? ?? \'未知错误\';\\n ToastUtil.showToast(msg: message);\\n }\\n }\\n}\\n
\\n主要打印整个请求过程中的各个参数&状态&耗时
\\nclass CustomLogInterceptor extends Interceptor {\\n // 用于存储每个请求的开始时间\\n Map<RequestOptions, int> requestStartTimeMap = {};\\n\\n @override\\n void onRequest(RequestOptions options, RequestInterceptorHandler handler) {\\n // 记录请求开始时间\\n requestStartTimeMap[options] = MyDateTimeUtil.getTimeStamp();\\n handler.next(options);\\n }\\n\\n @override\\n void onResponse(Response response, ResponseInterceptorHandler handler) {\\n // 记录响应日志\\n _logResponse(response);\\n handler.next(response);\\n }\\n\\n @override\\n void onError(DioError error, ErrorInterceptorHandler handler) {\\n // 记录错误日志\\n _logError(error);\\n handler.next(error);\\n }\\n\\n // 记录响应日志\\n void _logResponse(Response response) {\\n // 获取请求开始时间\\n int startTime = requestStartTimeMap[response.requestOptions] ?? 0;\\n // 当前时间戳\\n int entTime = MyDateTimeUtil.getTimeStamp();\\n // 从 extra 中读取耗时指标\\n final timings = response.requestOptions.extra as Map<String, dynamic>;\\n final options = response.requestOptions;\\n Log.error(\'\'\'\\n \\n 请求方式: ${options.method}\\n 请求URL: ${options.uri}\\n 请求Headers: ${options.headers}\\n 网络请求耗时:${entTime - startTime} ms\\n DNS: ${timings[\'dns\']}ms\\n TCP: ${timings[\'tcp\']}ms\\n SSL: ${timings[\'ssl\']}ms \\n 首包: ${timings[\'first_packet\']}ms\\n 响应状态码: ${response.statusCode}\\n 响应头:${response.headers}\\n 响应: ${response.data}\\n \'\'\');\\n }\\n\\n // 记录错误日志\\n void _logError(DioError error) {\\n // 获取请求开始时间\\n int startTime = requestStartTimeMap[error.requestOptions] ?? 0;\\n // 当前时间戳\\n int entTime = MyDateTimeUtil.getTimeStamp();\\n // 从 extra 中读取耗时指标\\n final timings =\\n error.response?.requestOptions.extra as Map<String, dynamic>;\\n final options = error.requestOptions;\\n Log.error(\'\'\'\\n 网络请求错误:\\n 请求方式: ${options.method}\\n 请求URL: ${options.uri}\\n 请求Headers: ${options.headers}\\n 网络请求耗时:${entTime - startTime} 毫秒\\n DNS: ${timings[\'dns\']}ms\\n TCP: ${timings[\'tcp\']}ms\\n SSL: ${timings[\'ssl\']}ms \\n 首包: ${timings[\'first_packet\']}ms\\n ${error.toString()}\\n \'\'\');\\n }\\n}\\n\\n
\\n数据转换拦截器将请求或响应的数据在发送或接收时进行转换,例如将 JSON 数据转换为自定义的数据模型,或者对数据进行加密 / 解密。可以确保数据的格式和安全性符合应用的要求
\\nclass DataTransformInterceptor extends Interceptor {\\n @override\\n void onRequest(RequestOptions options, RequestInterceptorHandler handler) {\\n if (options.data != null && options.data is Map<String, dynamic>) {\\n options.data = jsonEncode(options.data);\\n }\\n handler.next(options);\\n }\\n\\n @override\\n void onResponse(Response response, ResponseInterceptorHandler handler) {\\n if (response.data is String) {\\n response.data = jsonDecode(response.data);\\n }\\n handler.next(response);\\n }\\n}\\n\\n
","description":"代码github地址 实现功能\\n\\n1、get、post请求\\n2、自定义RequestOptions\\n3、dio请求管理队列,用于统一管理请求\\n4、HttpClient链接管理,用于获取解析DNS时间、TCP连接开始时间、SSL握手开始时间(如果是HTTPS)、首包时间\\n5、json转model\\n6、缓存管理\\n7、日志管理拦截器\\n8、数据转换管理拦截器\\n9、loading拦截器\\n10、token续租拦截器\\n11、错误处理拦截器\\n1、基础使用\\n\\n在 Flutter 中,dio 是一个强大的 HTTP 客户端,用于发送各种网络请求,如 GET、POST…","guid":"https://juejin.cn/post/7475651131449819136","author":"SunshineBrother","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-27T02:14:43.180Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3b524788f99b4aacbae349a7f14bb543~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgU3Vuc2hpbmVCcm90aGVy:q75.awebp?rk3s=f64ab15b&x-expires=1741227374&x-signature=nQywFZPar2vVtq5Q2lYLmHJ4YgU%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["iOS","Flutter","Android"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 中 PluginRegistry.Registrar 的移除与迁移指南","url":"https://juejin.cn/post/7475675805389979699","content":"在 Flutter 3.29 版本中,PluginRegistry.Registrar
已被完全移除。这一变化意味着所有基于旧 API 的插件都需要迁移到新的 FlutterPlugin
接口。
自 Flutter 1.12 版本开始,Flutter 团队就引入了新的 Android 插件 API,以替代旧的 PluginRegistry.Registrar
。旧 API 的主要问题是它依赖于 BuildContext
,在某些情况下(如 Flutter 尚未附加到任何 Activity 时)可能会返回 null
,导致运行时错误。新的 FlutterPlugin
接口则提供了更简洁、更稳定的生命周期管理方式。
在 Flutter 3.29 中,旧的 PluginRegistry.Registrar
已被完全移除。如果你的项目或依赖的插件仍在使用旧 API,可能会导致编译失败或运行时错误。例如,以下是一个典型的错误信息:
error: cannot find symbol\\npublic static void registerWith(PluginRegistry.Registrar registrar) {\\n ^\\nsymbol: class Registrar\\nlocation: interface PluginRegistry\\n
\\n这种错误表明代码中仍然使用了已被移除的 Registrar
类。
为了确保插件与 Flutter 3.29 兼容,你需要将旧的插件代码迁移到新的 FlutterPlugin
接口。以下是迁移的步骤和示例代码:
registerWith
方法旧的插件注册方式是通过 registerWith
方法实现的,例如:
public class MyPlugin implements MethodCallHandler {\\n private final Registrar registrar;\\n\\n private MyPlugin(Registrar registrar) {\\n this.registrar = registrar;\\n }\\n\\n public static void registerWith(Registrar registrar) {\\n final MethodChannel channel = new MethodChannel(registrar.messenger(), \\"my_plugin\\");\\n channel.setMethodCallHandler(new MyPlugin(registrar));\\n }\\n}\\n
\\n在新的 API 中,你需要实现 FlutterPlugin
接口,并在 onAttachedToEngine
方法中初始化 MethodChannel
:
public class MyPlugin implements FlutterPlugin, MethodCallHandler {\\n private MethodChannel channel;\\n\\n @Override\\n public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {\\n channel = new MethodChannel(binding.getBinaryMessenger(), \\"my_plugin\\");\\n channel.setMethodCallHandler(this);\\n }\\n\\n @Override\\n public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {\\n channel.setMethodCallHandler(null);\\n channel = null;\\n }\\n\\n @Override\\n public void onMethodCall(MethodCall call, @NonNull Result result) {\\n // Handle method calls\\n }\\n}\\n
\\n如果你的插件需要与 Android 的 Activity
交互,还需要实现 ActivityAware
接口。例如:
public class MyPlugin implements FlutterPlugin, MethodCallHandler, ActivityAware {\\n private MethodChannel channel;\\n private Activity activity;\\n\\n @Override\\n public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {\\n channel = new MethodChannel(binding.getBinaryMessenger(), \\"my_plugin\\");\\n channel.setMethodCallHandler(this);\\n }\\n\\n @Override\\n public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {\\n channel.setMethodCallHandler(null);\\n channel = null;\\n }\\n\\n @Override\\n public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {\\n activity = binding.getActivity();\\n }\\n\\n @Override\\n public void onDetachedFromActivityForConfigChanges() {\\n activity = null;\\n }\\n\\n @Override\\n public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) {\\n activity = binding.getActivity();\\n }\\n\\n @Override\\n public void onDetachedFromActivity() {\\n activity = null;\\n }\\n\\n @Override\\n public void onMethodCall(MethodCall call, @NonNull Result result) {\\n // Handle method calls\\n }\\n}\\n
\\n在迁移完成后,确保删除所有与 PluginRegistry.Registrar
相关的代码。例如,移除以下方法:
public static void registerWith(Registrar registrar) {\\n // Old registration code\\n}\\n
\\nFlutter 3.29 中移除了旧的 PluginRegistry.Registrar
API,这是为了提供更稳定、更高效的插件生命周期管理。通过上述迁移步骤,你可以轻松将旧的插件代码更新为新的 FlutterPlugin
接口。这一变化不仅提高了插件的稳定性,还为未来的 Flutter 开发奠定了更好的基础。
如果你在迁移过程中遇到问题,可以参考官方文档或社区讨论。
","description":"在 Flutter 3.29 版本中,PluginRegistry.Registrar 已被完全移除。这一变化意味着所有基于旧 API 的插件都需要迁移到新的 FlutterPlugin 接口。 背景与原因\\n\\n自 Flutter 1.12 版本开始,Flutter 团队就引入了新的 Android 插件 API,以替代旧的 PluginRegistry.Registrar。旧 API 的主要问题是它依赖于 BuildContext,在某些情况下(如 Flutter 尚未附加到任何 Activity 时)可能会返回 null,导致运行时错误。新的 Flutt…","guid":"https://juejin.cn/post/7475675805389979699","author":"Hans_April","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-27T01:49:34.570Z","media":null,"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 状态管理:Provider vs Riverpod","url":"https://juejin.cn/post/7475658259938279474","content":"在 Flutter 开发中,状态管理是构建复杂应用的核心部分。良好的状态管理可以提高代码的可维护性、可读性和可扩展性。Provider
和 Riverpod
是 Flutter 中两种非常流行的状态管理方案,它们都基于依赖注入(Dependency Injection)的思想,但在实现和使用上存在一些差异。本文将详细介绍 Provider
和 Riverpod
的区别,帮助你在项目中做出更合适的选择。
Provider
是 Flutter 团队提供的一个状态管理库,它基于 InheritedWidget
实现,通过上下文(BuildContext
)将数据传递给子组件。Provider
的设计目标简单是易用,同时提供足够的灵活性来满足大多数应用的需求。
ChangeNotifier
配合使用。以下是一个简单的 Provider
示例,展示如何使用 Provider
管理计数器的状态:
import \'package:flutter/material.dart\';\\nimport \'package:provider/provider.dart\';\\n\\nvoid main() {\\n runApp(\\n ChangeNotifierProvider(\\n create: (context) => CounterModel(),\\n child: MyApp(),\\n ),\\n );\\n}\\n\\nclass CounterModel with ChangeNotifier {\\n int _count = 0;\\n\\n int get count => _count;\\n\\n void increment() {\\n _count++;\\n notifyListeners(); // 通知依赖的组件更新\\n }\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n home: Scaffold(\\n appBar: AppBar(title: Text(\'Provider Example\')),\\n body: Center(\\n child: Consumer<CounterModel>(\\n builder: (context, counter, child) {\\n return Text(\'Count: ${counter.count}\');\\n },\\n ),\\n ),\\n floatingActionButton: FloatingActionButton(\\n onPressed: () => context.read<CounterModel>().increment(),\\n child: Icon(Icons.add),\\n ),\\n ),\\n );\\n }\\n}\\n
\\nProvider
的 API 设计简洁,易于上手。Provider
与 Flutter 的生命周期和上下文管理无缝结合。ValueNotifierProvider
、ChangeNotifierProvider
等。Provider
的使用依赖于 BuildContext
,这可能导致代码的可读性降低,尤其是在复杂的组件树中。Provider.of<T>
时,如果类型不匹配,可能会导致运行时错误。Provider
的生命周期管理可能不够直观,尤其是在涉及异步操作时。Riverpod
是由 Provider
的作者 Remi Rousselet 开发的下一代状态管理库,旨在解决 Provider
的一些局限性。Riverpod
的核心思想是将状态管理与 BuildContext
解耦,从而提高代码的可读性和可维护性。
Provider
是一个独立的类,用于定义和管理状态。以下是一个简单的 Riverpod
示例,展示如何使用 Riverpod
管理计数器的状态:
import \'package:flutter/material.dart\';\\nimport \'package:flutter_riverpod/flutter_riverpod.dart\';\\n\\nfinal counterProvider = StateNotifierProvider<CounterModel, int>((ref) {\\n return CounterModel();\\n});\\n\\nclass CounterModel extends StateNotifier<int> {\\n CounterModel() : super(0);\\n\\n void increment() {\\n state++;\\n }\\n}\\n\\nvoid main() {\\n runApp(ProviderScope(child: MyApp()));\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n home: Scaffold(\\n appBar: AppBar(title: Text(\'Riverpod Example\')),\\n body: Center(\\n child: Consumer(\\n builder: (context, ref, child) {\\n final count = ref.watch(counterProvider);\\n return Text(\'Count: $count\');\\n },\\n ),\\n ),\\n floatingActionButton: FloatingActionButton(\\n onPressed: () => context.read(counterProvider.notifier).increment(),\\n child: Icon(Icons.add),\\n ),\\n ),\\n );\\n }\\n}\\n
\\nBuildContext
,这使得状态管理更加清晰和独立。FutureProvider
和 StreamProvider
。Provider
更复杂,需要一定的时间来学习和理解。Provider
丰富。特性 | Provider | Riverpod |
---|---|---|
依赖上下文 | 是 | 否 |
类型安全 | 有限 | 高 |
生命周期管理 | 依赖 BuildContext | 独立于 BuildContext |
异步状态支持 | 需手动实现 | 原生支持 |
学习曲线 | 低 | 中等 |
社区支持 | 高 | 中 |
选择 Provider
还是 Riverpod
取决于你的项目需求和个人偏好:
Provider
,并且团队对它熟悉,那么继续使用 Provider
是一个合理的选择。BuildContext
的依赖,那么 Riverpod
是一个更好的选择。Riverpod
,因为它的设计理念更先进,能够为你的项目带来更好的可维护性和可扩展性。Provider
和 Riverpod
都是 Flutter 中非常优秀的状态管理方案,但它们在设计理念和实现上有显著差异。Provider
更适合简单的项目和快速开发,而 Riverpod
更适合复杂的应用和对代码质量有较高要求的场景。无论你选择哪种方案,重要的是理解它们的核心思想,并根据项目需求灵活运用。
最近几天随着 Claude 3.7 Sonnet 的发布,朋友圈几乎都被各种刷屏,各种内容总结下来一句话: Claude 3.7 Sonnet 是迄今为止「最智能」且首款支持「混合推理」的模型。
\\n刚好这段时间一直在使用 Cursor 和 Trae ,并且目前 Cursor 的跟进速度也相当感人,如下图这两天已经在使用 Claude 3.7 Sonnet 了 ,恰逢最近在搞一些项目的框架迁移,正好借此机会通过实际需求对比下 Cursor 和 Trae 的 AI 体验,而本次体验下来,只能说是一言难尽。。。
\\n本次需求的核心是让 AI 帮我在一个 Flutter 项目里把状态管理框架从 redux 迁移到 riverpod ,相信 Flutter 开发应该会理解,这其实不算是一个简单的需求,因为在状态管理逻辑的实现和 API 使用上,redux 和 riverpod 可以说是“南辕北辙”,具体体现在于:
\\n而对于 riverpod,它支持分散式状态管理,状态和存储都可以按需定义和组合,核心是依赖 Ref 和各种 Provider ,并且不需要传递 BuildContext
。
另外,状态管理框架在项目里本身就会涉及很多代码模块,所以可以看出来,这样一个迁移需求无疑是一个「吃力不讨好」的活。
\\n那么,首先从 Trae 开始,目前版本下 Trae 的 Builder 支持的是 Claude-3.5-Sonnet
免费使用,一开始我只是用简单一句话来说明需求 :
可以看到虽然我说的不多,但是起初 Trae 看起来理解的还不错,只是目前体验下, Trae 的思考速度还是略慢,而在等了一段时间之后,Trae 告诉我改完了,结果我定睛一看,好家伙,只创建了一个新的 gsy_state_provider.dart
,然后改了 app,dart
和 pubspec.yaml
两个文件:
很明显这才哪到哪,所以我让它继续迁移,然后·····它又帮我改了两个文件,然后告诉我改好了:
\\n到这时候,我开始思考大概是我的描述有问题?然后在经过几次尝试后,我 checkout 了项目,然后重新开了一个会话,并且增加了一下明确的路径和引用描述:
\\n而这一次的结果相对好了一些,但是依旧还是只改了几个文件,并且我在 review 代码的时候,发现了不少“一言难尽”的提交,比如:
\\n\\n\\n在项目里有一些 mixin 的通用 State 基类,但是由于 Trae 在迁移到 riverpod 时,会让 Widget 直接继承
\\nConsumerStatefulWidget
,从而对应的 State 也需要继承ConsumerState
,然后“基类们”就出现冲突了,甚至有时候它还会把 mixin 的基类改成 abstract ,然后 mixin 就报错····
尽管看起来 Trae 直接使用 ConsumerState
也许大概可能是为了更好全局获取 Ref
,但是实际迁移过程中,使用 Consumer
才能最低程度降低冲突,所以我又不得不 checkout 之后重开个新的会话。
另外由于 Trae 还会有一些不合规的地方修改 riverpod 的参数,所以又继续丰富相关的任务提示词:
\\n而后 Trae 的修改又转好一丢丢,但是貌似也就好了那么一丢丢,甚至虽然你让它不要用 ConsumerStatefulWidget
,但是它还是在某些地方「坚定」的认为需要使用 ConsumerStatefulWidget
,最后我终于“醒悟”:Trae 它真的没办法一次性帮自动帮我完成框架迁移。
就算后续我在提示词增加了各种使用到 redux 的地方,并且标注上它们的业务逻辑作用,但是 Trae 一次最多就只会帮我修改那么几个文件,并且还是会残留不少“大坑”等我去填,比如:
\\n在某个地方业务上是通过一个 int
的index
去判断获取哪个 Color
传递给 Theme 主题,从而生成新的 ThemeData
,然后 Trae 修改后让函数的参数直接变成了 Color
, 我直接把对应报错复制给它处理,结果它的处理方式是:
\\n\\n将
\\nindex
通过toInt()
的方式转为整形,然后它就是一个正常的Color
?
后面让它再多修改几次,它确实也能将错误解决,但是解决的方式是大概是类似 setThemeData(Theme.of(context))
,嗯,错误是没了,但是这代码也没有意义了:获取了当前主题设置给当前主题。
\\n\\n所以在使用 AI 工具修改代码的时候,审查很重要, 有时候它真的就没报错,但是它可能直接帮你屏蔽了一个需求。
\\n
类似的还有,在不合适的地方去修改 riverpod 内的状态是不合规的,然后针对这个问题,Trae 的解决方案就是:
\\n\\n\\n加个
\\nFuture
。
然后运行后继续报错,这种也是比较“恶心”,编译过程没问题,然后运行才出现的“埋坑”,又一次体现了审查的重要性,并且还需要你有对应的认知能力:
\\n最离谱的还有几次,它在宣称「迁移完成」后,甚至都没往 pubspec.yml
内添加过 riverpod 的依赖:
再之后,也不知道是不是因为我「骚操作」太多,Trae 就开始进入「红温」,进入了频繁不可用状态:
\\n自此我放弃在 Trae 下通过 AI 自动完成迁移的可能,然后我就开始转战 Cursor 的 Claude 3.7 Sonnet
,当我以为会有不一样的体验时,它确实给了我不一样的体验,因为它在简单了几个文件后告诉我:
\\n\\n我需要手动逐步完成。
\\n
不信邪的我又喂了一份非常详细的迁移计划,然后 Cursor 继续告诉我:这不是可以一步到“胃”的事情,饭要一口一口的吃。
\\n到这里我突然就领悟到自媒体在说 Claude 3.7 Sonnet
是我们正在迈向自动编程重要的一步跨越,嗯,目前它还在迈向这个过程,所以大家还是需要开「手动挡」。
当然 Cursor 也会写“奇奇怪怪”的代码,比如我让 Cursor 帮我生成一个翻译后的代码文件,然后加载顺序是 3 日语,4 韩语:
\\n但是之后 Cursor 给我生成的选项顺序是 3 韩语 4 日语,然后运行时点击「切换到日语」时就发现界面变成了韩语:
\\n另外,在 Cursor 上开 agent 或者 Thinking 模式下整体效果会更好一些,但是刚改了个开头就被强行结束的体验,也确实很难接受:
\\n\\n\\n从 Cursor 谨慎的角度看,某种程度体验上还真不如 Trae ,但是 Cursor 在 Claude 3.7 下的生成速度和思考能力确实强了不少。
\\n
当然,这也和模型在整个「上下文窗口数量」还有「单次响应」的 Token 有关系,也就是你需要处理的代码越复杂,量越大,就越不好用,AI 理解上更容易出现「断章取义」的情况。
\\n当然,吐槽了这么多,其实有了 Cursor 和 Trae 之后,对工作效率提升上还是很有帮助的,比如多语言翻译,还有修复某些具体编译错误时,AI 给出的建议和自动化能力,确实能很好提升开发体验,前提是需要注意一些关键点:
\\n最后,聊个题外话,在找资料和具体问题建议上,DeepSeek 的深度思考确实不错,另外 Grok 3 的体验也让我很惊喜,有时候我怕“幻觉”的时候,就在两个平台上同时问后对比,大部分时候两者的答案水平都不相上下,而 Grok 3 开了 DeepSearch 后,往往结果更好一些:
\\n当然,我在 Grok 3 几乎每天都被限制 DeepSearch ,毕竟 Grok 3 的价格感人, 还是继续用用免费额度好了:
\\n在有了 DeepSeek 和 Grok 3 之后,找资料看问题真的比直接翻文档确实来的方便,虽然它们在代码建议上还是会瞎编 API ,但是思路参考还是挺不错的,对比之下,现在的 ChatGPT 的推理搜索虽然出结果很快,但是貌似除了快之外,答案的可用性并不是很好。
\\n这大概就是我这段时间完整的 AI 辅助编码体验,它们确实有很大的帮助,但是也没有各种文章中提到那么强力,而根据 Anthropic 的发展图景:它们希望是在 2025 年 Claude 可以成为独立自主工作数小时的专家级智能体,而到 2027 年能够解决人工团队花费数年才能解决的挑战性难题。
\\n所以,或者留给我们的时间只有两年了?
","description":"最近几天随着 Claude 3.7 Sonnet 的发布,朋友圈几乎都被各种刷屏,各种内容总结下来一句话: Claude 3.7 Sonnet 是迄今为止「最智能」且首款支持「混合推理」的模型。 刚好这段时间一直在使用 Cursor 和 Trae ,并且目前 Cursor 的跟进速度也相当感人,如下图这两天已经在使用 Claude 3.7 Sonnet 了 ,恰逢最近在搞一些项目的框架迁移,正好借此机会通过实际需求对比下 Cursor 和 Trae 的 AI 体验,而本次体验下来,只能说是一言难尽。。。\\n\\n本次需求的核心是让 AI 帮我在一个…","guid":"https://juejin.cn/post/7475648534009675812","author":"恋猫de小郭","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-26T15:54:26.332Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9f327f9b450b421386bd81c91b266956~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1741190066&x-signature=aPh9seLijU10eTIxCeL01jKadoo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/67a2e2bf5ff74580bfffda81e7cc0614~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1741190066&x-signature=4RTqMXhQDVHJcs14BN24%2FZnBbtM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f9e3f093b4cc4e63b2646cdcc8acdc8a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1741190066&x-signature=n109IoX26G%2BonaB0x89Vq2UDyxs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4859e641cff347f985cb6dfb6e5cc2d3~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1741190066&x-signature=ardlC6eEDj65n6LUU0KgyACukNk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3d88eacf856d44ab919c54fe26e964db~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1741190066&x-signature=%2BaAOiwQ8SlbbU60SVUPue76T%2BzY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6f78ce6cfb2e41168347c8d888409656~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1741190066&x-signature=kTmafaJOKjVRfFoGNBXm%2FAhQpRs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4a504d9a8144498e85ca205cc697235c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1741190066&x-signature=Cv6BXlmrvRsEHr8XsBS0BNpwJKw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e51f7dd908b64307a1dd721838053b9a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1741190066&x-signature=OVxm1lDGnoFyvHd8dC%2Bgzd2YYv4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/54dc511cfd9244ef94bd89dd97a41860~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1741190066&x-signature=oKBHXQ%2BR9y3hA6HfUG91SD4DTFc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ebe71cedc9334b12a191ed14454fd8c4~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1741190066&x-signature=9Vihpf7p0VznnuGc%2B4pWp7DSs5M%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2cfb4fa76a27478c99ef11f03955b6af~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1741190066&x-signature=HAMtzdKvSAeQW6orKs4Y18JTNGI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ef287e3b32934e87a9b191d0a2fb9af1~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1741190066&x-signature=%2Fd%2BUy%2F%2Fub49ET6hluWE4x3vFXi4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/406657f686e5482cacd5eb085d634faf~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1741190066&x-signature=68b1btcKe87f5x%2FZgegTE2dHcwk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2aaf392d88cf4bec98b6593b3a403812~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1741190066&x-signature=yVzRMiP8w%2FoFKdQIIj530ipZ1OE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/399fc3aee9e34062b2d82fbb42d73ae1~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1741190066&x-signature=PAhvFv%2BT7s4jcfXzZshNeAS0Wis%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e2c6065e080a4e099ce039b4051ed741~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1741190066&x-signature=DWyQ02x7T2oemMG8jr9fO23dbr0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c1555c0cbfd346ebba33db11a4f9d35a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1741190066&x-signature=Lj02enoqPsB51rgolXJlNORIoDI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/79233f7e158a4774834ebb199602b691~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1741190066&x-signature=%2BMmp1SUP2YEGc8dYcC1yWZETVt4%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 教程(三)状态管理","url":"https://juejin.cn/post/7475587818895704075","content":"在上一篇文章 Flutter 教程(二)Flutter 组件中,我们了解了在 Flutter 中构建UI的方式。可以看到,它们和在 Android 传统构建 UI 的方式完全不同。这是因为 Flutter 是基于声明式构建UI的,而在 Android ,是命令式构建UI的。
\\n以 Flutter 默认的计数器例子为例:
\\n在 Android 中,实现上面的效果的代码如下所示:
\\n// 一、定义展示的内容\\nprivate int mCount = 0;\\n \\n// 二、中间展示数字的控件 TextView\\nprivate TextView mTvCount;\\n \\n// 三、关联 TextView 与 xml 中的组件\\nmTvCount = findViewById(R.id.tv_count)\\n \\n// 四、点击按钮控制组件更新\\nprivate void increase( ){ \\nmCount++;\\nmTvCounter.setText(mCount.toString()); \\n}\\n
\\n可以看到在Android中实现计数更新的功能,需要获取对应的UI控件对象,然后设置该对象的值。而在 Flutter 中,声明 UI 布局之后,只需要通过 setState
来刷新对应的界面即可,而不需要进行繁琐的控制,代码示例如下:
// 一、声明变量\\nint _counter =0; \\n\\n// 二、展示变量 \\nText(\'$_counter\')\\n\\n// 三、变量增加,更新界面\\nsetState(() {\\n _counter++; \\n});\\n
\\n让开发者摆脱组件的繁琐控制,聚焦于状态处理
\\n在开发 Android 原生时,你会发现当多个组件之间相互关联时,对于 View 的控制非常麻烦。而在 Flutter 中我们只需要处理好状态即可 (复杂度转移到了状态 -> UI 的映射,也就是 Widget 的构建)。
\\n使用声明式开发主要遇到的问题有三个:
\\n一开始业务不复杂的时候,所有的代码都直接写到 widget 中,随着业务迭代,文件越来越大,其他开发者很难直观地明白里面的业务逻辑。这就导致了逻辑和页面 UI 耦合,导致无法复用/单元测试、修改混乱等问题。
\\n这个问题在 Android 原生上同样存在,现在一般 MVP、MVVM、MVI等设计模式的思路去解决。
\\n如上图所示,在 Widget 结构中,一个子组件想要展示父组件中的 name
字段,可能需要层层进行传递。又或者是要在两个页面之间共享筛选数据,并没有一个很优雅的机制去解决这种跨页面的数据访问。
setState
会触发对你当前所在的小组件的重建。如果你的整个应用程序只包含一个widget,那么整个widget将被重建,这将使你的应用程序变得缓慢。
前面我们提到了声明式UI会造成的三个问题,而状态管理的目的就是解决「声明式」开发带来的问题。 Flutter 中常用的状态管理框架有 Get 和 Provider。
\\n这两个框架各有优缺点,如果你或者你的团队刚接触 Flutter,使用 Provider 能帮助你们更快理解 Flutter 的核心机制。而如果已经对 Flutter 的原理有了解,Get 丰富的功能和简洁的 API,则能帮助你很好地提高开发效率。
\\n在介绍 Provider 和 Get 状态管理框架前,我们需要先了解一下组件的基础知识。这样才方便理解状态管理实现的原理。
\\nFlutter 的渲染是通过三棵树实现的,三棵树分别为:
\\n对一个 Element 配置的描述
,也就是说,widget 只是一个配置的描述,并不是真正的渲染对象,就相当于是 Android 里面的 xml,只是描述了一下属性,但他并不是真正的 View。在Flutter开发中,一切皆组件,我们展示给用户的界面也是一个组件。而组件 Widget 主要被划分为 StatelessWidget 和 StatefulWidget 两大类。StatelessWidget 就是一个无状态组件。由 StatelessWidget 设计出来的界面内容是无法使用setState()方法改变的。StatelessWidget 的代码示例如下:
\\nclass MyStateLessWidget extends StatelessWidget{\\n final String title;\\n MyStateLessWidget({\\n Key key,\\n this.title,\\n });\\n @override\\n Widget build(BuildContext context){\\n return new ...;\\n }\\n}\\n
\\n而StatefulWidget 是有状态组件。当创建一个StatefulWidget组件的时候,肯定也会创建一个State对象。通过这个对象,我们可以与用户交互并刷新界面。StatefulWidget 的代码示例如下:
\\n// 主体部分\\nclass MyHomePage extends StatefulWidget {\\n MyHomePage({Key key, this.title}) : super(key: key);\\n\\n final String title;\\n\\n @override\\n _MyHomePageState createState() => _MyHomePageState();\\n}\\n// State 部分\\nclass _MyHomePageState extends State<MyHomePage> {\\n ...\\n @override\\n Widget build(BuildContext context) {\\n return new ...;\\n }\\n}\\n
\\n我们需要改变StatefulWidget组件的界面内容,就需要使用setState(...),这个服务由Flutter框架层控制来更新UI。
\\n从上面的代码示例,可以看到在实现 StatelessWidget 和 StatefulWidget 来创建组件时,会使用到 BuildContext
。而 BuildContext
就是 widget 对应的 Element
。BuildContext 的代码示例如下:
Theme.of(context) //获取主题\\nNavigator.push(context, route) //入栈新路由\\nLocalizations.of(context, type) //获取Local\\ncontext.size //获取上下文大小\\ncontext.findRenderObject() //查找当前或最近的一个祖先RenderObject\\n
\\n\\n\\n为什么不直接传入 Element,而是传入 BuildContext;这是因为 BuildContext 是 Element 的接口,传入 BuildContext 可以限制直接操作 Element
\\n
我们将State的生命周期分为3个部分:第一部分是启动 App 的运行流程;第二部分是热重载的运行流程;第三部分是界面销毁时的运行流程。
\\nState的生命周期中的几个非常重要的方法如下:
\\n在实现 StatelessWidget 和 StatefulWidget 时,都会使用一个 Key。它代表 Widget 的唯一标识。这个唯一标识在 build/rendering 阶段由框架定义。
\\n关于 Key 的详情可以看 Flutter | Key 的原理和使用概述
\\nInheritedWidget 是 Flutter 提供的一种在 widget 树中从上到下共享数据的方式,即在父widget 中通过InheritedWidget共享了一个数据,那么在任意子widget都能获取该共享的数据。
\\n前面提到的 Provider 框架就是根据 InheritedWidget 来实现状态管理的
\\nProvider 的使用具体看Flutter状态管理之 Provider 使用详解
\\nGetX 的使用具体看Flutter GetX使用
\\n在Material Design
设计体系中,Card
(卡片)作为信息容器占据着特殊地位。它不仅是一个简单的矩形框,更是承载复杂内容交互的原子单元。Flutter
框架通过Card
组件将这一设计理念工程化,提供了从基础布局到高级动效的全套解决方案。
这种设计范式之所以经久不衰,源于其三大特性:聚焦性(内容层级分明
)、隔离性(逻辑单元独立
)和可操作性(交互行为明确
)。
本文将通过六维知识体系,深度解构Card
容器,揭示其隐藏的关键布局规则,并配合企业级案例代码,帮助开发者从\\"会用\\"
到\\"精通\\"
,最终达到\\"手中无卡,心中有卡\\"
的开发境界。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\nconst Card({\\n super.key,\\n this.color,\\n this.shadowColor,\\n this.surfaceTintColor,\\n this.elevation,\\n this.shape,\\n this.borderOnForeground = true,\\n this.margin,\\n this.clipBehavior,\\n this.child,\\n this.semanticContainer = true,\\n})\\n
\\ncolor
与surfaceTintColor
用于控制卡片背景色(color
)和 Material 3
表面着色(surfaceTintColor
),两者通过 alpha
合成算法叠加。
Card(\\n color: Colors.red.withValues(alpha: 0.1),\\n surfaceTintColor: Colors.blue,\\n child: Padding(\\n padding: EdgeInsets.all(10),\\n child: Text(\\n \'Card内容\',\\n style: TextStyle(color: Colors.white),\\n ),\\n ),\\n),\\n
\\n图示:
\\nelevation
控制阴影深度(0-24 dp
),影响视觉层级和立体感
。
Card(\\n color: Colors.blue,\\n elevation: 10.0,\\n shadowColor: Colors.black,\\n child: Padding(\\n padding: EdgeInsets.all(30),\\n child: Text(\\n \'Card内容\',\\n style: TextStyle(color: Colors.white),\\n ),\\n ),\\n),\\n
\\n图示:
\\n注意事项:
\\nelevation * 0.5 + 2.0
(单位 dp
)。iOS
需调整 shadowColor
透明度(Cupertino 设计规范
)。elevation
建议 ≤ 6
(避免过度绘制
)。shape
定义卡片几何形状(几何造型系统
),支持 15+
种 ShapeBorder
类型。
Card(\\n shape: ContinuousRectangleBorder(\\n // 流体圆角\\n borderRadius: BorderRadius.circular(28),\\n side: BorderSide(\\n color: Colors.blue.shade300,\\n width: 1.2,\\n ),\\n ),\\n clipBehavior: Clip.antiAlias, // 必须启用抗锯齿裁剪\\n child: Padding(\\n padding: EdgeInsets.all(30),\\n child: Text(\\n \'Card内容\',\\n style: TextStyle(color: Colors.red),\\n ),\\n ),\\n)\\n
\\n图示:
\\n注意事项:
\\nclipBehavior: Clip.antiAlias
防止子组件溢出。≥ 8 dp
(Material 3 标准
)。margin
与padding
的黄金分割控制卡片外间距(margin
)和内边距(padding
)。
LayoutBuilder(\\n builder: (context, constraints) {\\n final spacing = constraints.maxWidth * 0.1;\\n return Card(\\n margin: EdgeInsets.all(spacing),\\n child: Padding(\\n padding: EdgeInsets.symmetric(\\n horizontal: spacing * 1.5,\\n vertical: spacing,\\n ),\\n child: Text(\\n \'Card内容\',\\n style: TextStyle(color: Colors.red),\\n ),\\n ),\\n );\\n },\\n)\\n
\\n图示:
\\n注意事项:
\\nmargin
建议使用百分比而非固定值(适配多屏幕
)SliverList
中设置 margin: EdgeInsets.zero
提升性能。MediaQuery.padding
处理刘海屏/下巴屏
。shadowColor
与 surfaceTintColor
精细化控制阴影颜色和表面着色效果。
\\nCard(\\n elevation: 10,\\n shadowColor: Colors.blueAccent, // 霓虹阴影\\n surfaceTintColor: isDarkMode\\n ? Colors.blueGrey[800]\\n : Colors.indigo[100], // 动态主题\\n child: Padding(\\n padding: EdgeInsets.all(30),\\n child: Text(\\n \'Card内容\',\\n style: TextStyle(color: Colors.red),\\n ),\\n ),\\n)\\n
\\n图示:
\\n注意事项:
\\nMediaQuery.highContrast
调整颜色。elevation
动画同步(使用 AnimatedContainer
)。Material 3
中 surfaceTintColor
的透明度应 ≤ 0.2
。borderOnForeground
控制边框是否绘制在内容上层(默认 true
)
Card(\\n borderOnForeground: false, // 边框在底层\\n shape: RoundedRectangleBorder(\\n side: BorderSide(\\n color: Colors.deepPurple,\\n width: 1,\\n ),\\n ),\\n child: Container(\\n width: 150,\\n height: 150,\\n color: Colors.red,\\n ),\\n)\\n
\\n图示:
\\n注意事项:
\\nfalse
时可实现「边框作为背景
」效果。谨慎在列表中使用
)。InkWell
点击效果同时使用(导致视觉冲突
)。Card
的嵌套使用是构建复杂界面布局的有效手段。通过在一个 Card
中嵌套多个 Card
,可以创建出层次分明的布局结构。
Card(\\n margin: EdgeInsets.all(16.0),\\n child: Padding(\\n padding: EdgeInsets.all(16),\\n child: Column(\\n children: [\\n Card(\\n child: Image.asset(\\n \\"assets/images/product.webp\\",\\n fit: BoxFit.contain,\\n ),\\n ),\\n Card(\\n child: Padding(\\n padding: EdgeInsets.all(8.0),\\n child: Text(\'商品价格:${99.99}\'),\\n ),\\n ),\\n Card(\\n child: Padding(\\n padding: EdgeInsets.all(8.0),\\n child: Text(\'商品描述:这是一款非常优质的商品,具有多种功能...\'),\\n ),\\n )\\n ],\\n ),\\n ),\\n),\\n
\\n图示:
\\n利用 Flutter
强大的动画库,为 Card
添加动画效果能够显著增强用户界面的交互性。比如,可以为 Card
的显示添加淡入动画,使其在出现时更加自然流畅,吸引用户的注意力。在AnimatedOpacity
组件中包裹 Card
,通过控制opacity
值随时间的变化来实现淡入效果。
class CardDemo extends StatefulWidget {\\n @override\\n _CardDemoState createState() => _CardDemoState();\\n}\\n\\nclass _CardDemoState extends State<CardDemo>\\n with SingleTickerProviderStateMixin {\\n late AnimationController _controller;\\n late Animation<double> _opacityAnimation;\\n\\n @override\\n void initState() {\\n super.initState();\\n _controller = AnimationController(\\n duration: const Duration(milliseconds: 1000),\\n vsync: this,\\n );\\n _opacityAnimation =\\n Tween<double>(begin: 0.0, end: 1.0).animate(_controller);\\n _controller.forward();\\n }\\n\\n @override\\n void dispose() {\\n _controller.dispose();\\n super.dispose();\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: Text(\\"Card Demo\\"),\\n centerTitle: true,\\n backgroundColor: Theme.of(context).colorScheme.inversePrimary,\\n ),\\n body: AnimatedOpacity(\\n opacity: _opacityAnimation.value,\\n duration: const Duration(milliseconds: 1000),\\n child: Card(\\n color: Colors.red,\\n child: Padding(\\n padding: EdgeInsets.all(16.0),\\n child: Text(\'带有淡入动画的Card\',style: TextStyle(color: Colors.white),),\\n ),\\n ),\\n ),\\n );\\n }\\n}\\n
\\nViewport
动态缓存策略CustomScrollView(\\n cacheExtent: 2000, // 预渲染区域扩展\\n slivers: [\\n SliverGrid(\\n delegate: SliverChildBuilderDelegate(\\n (context, index) => _buildCard(index),\\n childCount: 100000,\\n addAutomaticKeepAlives: false, // 手动控制生命周期\\n addRepaintBoundaries: false, // 自定义重绘边界\\n ),\\n gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(\\n crossAxisCount: 2,\\n ),\\n ),\\n ],\\n)\\n
\\n优化策略:
\\n活跃池
(可视区域)+ 休眠池
(缓存区域)。cacheExtent
。WeakReference
持有卡片状态。RepaintBoundary(\\n key: ValueKey(\'card_$index\'),\\n child: Card(\\n child: ...,\\n ),\\n)\\n
\\n性能对比:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n策略类型 | 内存占用 | FPS | 适用场景 |
---|---|---|---|
无缓存 | 120MB | 45 | 简单卡片 |
RepaintBoundary | 95MB | 58 | 中等复杂度 |
OffscreenLayer | 80MB | 60+ | 动态特效卡片 |
LayerLink
组合拳final layerLink = LayerLink();\\n\\nCompositedTransformTarget(\\n link: layerLink,\\n child: Card(...),\\n);\\n\\nCompositedTransformFollower(\\n link: layerLink,\\n child: PopupMenuButton(...), // 悬浮菜单\\n)\\n
\\n优化原理:
\\nGPU
纹理切换。RasterCachePolicy.enabled
加速静态内容渲染。class StatelessCard extends StatelessWidget {\\n const StatelessCard({super.key});\\n \\n @override\\n Widget build(BuildContext context) {\\n return Provider.of<CardState>(context, listen: false).isVisible\\n ? const _CardContent() // 使用const构造\\n : const SizedBox.shrink();\\n }\\n}\\n
\\n黄金法则:
\\nfinal/const
修饰组件。build
方法内创建闭包。Widget
树结构解析// 源码路径:flutter/lib/src/material/card.dart\\n@override\\nWidget build(BuildContext context) {\\n return Material(\\n type: MaterialType.card,\\n elevation: elevation,\\n color: color,\\n shadowColor: shadowColor,\\n surfaceTintColor: surfaceTintColor,\\n shape: shape,\\n clipBehavior: clipBehavior,\\n child: Ink(\\n decoration: _buildInkDecoration(),\\n child: ConstrainedBox(\\n constraints: const BoxConstraints(minWidth: 88.0, minHeight: 88.0),\\n child: child,\\n ),\\n ),\\n );\\n}\\n
\\n架构亮点:
\\nMaterial
(样式层)→ Ink
(交互层)→ ConstrainedBox
(布局层)。≥48dp
(Material规范
)。Ink
组件实现水波纹与高亮效果。// 主题继承优先级\\nfinal CardTheme cardTheme = CardTheme.of(context);\\nfinal MaterialType materialType = MaterialType.card;\\n\\nreturn Material(\\n color: widget.color ?? cardTheme.color ?? Theme.of(context).colorScheme.surface,\\n elevation: widget.elevation ?? cardTheme.elevation ?? _defaultElevation,\\n // ...其他属性类似\\n);\\n
\\n继承顺序:
\\n最高优先级
)。CardTheme
配置。ThemeData
。Paint
阶段关键步骤:
drawShadow
(skia
引擎)。canvas.drawPath
(根据shape
计算路径)。paint.strokeWidth > 0
时执行。Ink
效果:overlay?.paint
方法。黄金比例公式:
\\n\\n\\n每卡片信息单元数 =
\\nmax(3, min(7, 屏幕高度(mm)/15))
Flutter
实现:
LayoutBuilder(\\n builder: (context, constraints) {\\n final density = constraints.maxHeight / 15; // 15mm基准单位\\n return Card(\\n child: Column(\\n children: [\\n _buildPrimaryContent(),\\n if (density > 5) _buildSecondaryContent(),\\n if (density > 7) _buildTertiaryContent(),\\n ],\\n ),\\n );\\n },\\n)\\n
\\n点击热区优化:
\\nCard(\\n shape: RoundedRectangleBorder(\\n borderRadius: BorderRadius.circular(8),\\n ),\\n margin: EdgeInsets.all(8),\\n child: MergeSemantics(\\n child: InkWell(\\n borderRadius: BorderRadius.circular(8), // 匹配卡片圆角\\n onTap: () {},\\n child: ...,\\n ),\\n ),\\n)\\n
\\n设计准则:
\\n≥8dp
保证可操作区域隔离。Semantics
合并点击区域语义。材质运动曲线:
\\nconst _cardEnterCurve = Cubic(0.4, 0.0, 0.2, 1.0); // 标准缓入\\nconst _cardExitCurve = Cubic(0.4, 0.0, 0.2, 1.0); // 标准缓出\\n\\nAnimatedCard(\\n curve: _isInserting ? _cardEnterCurve : _cardExitCurve,\\n duration: const Duration(milliseconds: 250),\\n)\\n
\\n运动类型:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n动效类型 | 曲线函数 | 应用场景 |
---|---|---|
容器变换 | FastOutSlowIn | 卡片展开/折叠 |
子项入场 | LinearOutSlowIn | 列表插入动画 |
共享元素过渡 | SlowMiddle | 跨页面卡片跳转 |
class CardBreakpoints {\\n static double get compactWidth => 600;\\n static double get mediumWidth => 840;\\n \\n static CardLayoutType getLayoutType(double width) {\\n if (width < compactWidth) return CardLayoutType.compact;\\n if (width < mediumWidth) return CardLayoutType.medium;\\n return CardLayoutType.expanded;\\n }\\n}\\n\\nLayoutBuilder(\\n builder: (context, constraints) {\\n final layoutType = CardBreakpoints.getLayoutType(constraints.maxWidth);\\n return switch (layoutType) {\\n CardLayoutType.compact => _buildCompactCard(),\\n CardLayoutType.medium => _buildMediumCard(),\\n CardLayoutType.expanded => _buildExpandedCard(),\\n };\\n },\\n)\\n
\\nclass AppCardTheme extends ThemeExtension<AppCardTheme> {\\n final Gradient? backgroundGradient;\\n final BoxBorder? customBorder;\\n \\n const AppCardTheme({this.backgroundGradient, this.customBorder});\\n \\n @override\\n ThemeExtension<AppCardTheme> lerp(ThemeExtension<AppCardTheme>? other, double t) {\\n // 实现渐变插值逻辑\\n }\\n}\\n\\nCard(\\n shape: Theme.of(context).extension<AppCardTheme>()?.customBorder ?? defaultShape,\\n decoration: BoxDecoration(\\n gradient: Theme.of(context).extension<AppCardTheme>()?.backgroundGradient,\\n ),\\n)\\n
\\nCard
组件的深度掌握,本质上是对Flutter
设计哲学的具象化理解。通过本文的系统化拆解,我们不仅收获了参数配置的技巧,更重要的是建立了四维认知框架:
布局系统
。动画原理
。组件生态
。无障碍准则
。当开发者能游刃有余地在这些维度间切换视角,卡片便不再是简单的UI
元素,而成为构建数字体验的基本粒子。这种认知跃迁的价值,将在复杂应用开发
、跨平台方案设计
、性能调优
等场景中持续释放。
记住:优秀的Flutter
开发者不是参数的搬运工,而是用户体验的炼金术士。
\\n","description":"前言 在Material Design设计体系中,Card(卡片)作为信息容器占据着特殊地位。它不仅是一个简单的矩形框,更是承载复杂内容交互的原子单元。Flutter框架通过Card组件将这一设计理念工程化,提供了从基础布局到高级动效的全套解决方案。\\n\\n这种设计范式之所以经久不衰,源于其三大特性:聚焦性(内容层级分明)、隔离性(逻辑单元独立)和可操作性(交互行为明确)。\\n\\n本文将通过六维知识体系,深度解构Card容器,揭示其隐藏的关键布局规则,并配合企业级案例代码,帮助开发者从\\"会用\\"到\\"精通\\",最终达到\\"手中无卡,心中有卡\\"的开发境界。\\n\\n操千曲而后晓声…","guid":"https://juejin.cn/post/7475555271991001127","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-26T09:10:07.550Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fe9d5db3dcde4740a83ae463ffaebd05~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1741225962&x-signature=OCiG4aPKmk3hyBVswcFKaIDfolk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e2883cfc34984d8a8d5820e0ab52384b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1741225962&x-signature=i9%2BI%2FU6blpOGyMSeas3CQKkpKgo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/691f558be8064fc9bb7a5f986147605d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1741225962&x-signature=oD7groaujIXT9U8gqku7U3N6uoc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/24fb5169f7854667b510a67116beccba~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1741225962&x-signature=PDhVK%2BymMFHgeAfJga%2FpK6jM6ko%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/972bca9580ca4185858a937f610b9d67~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1741225962&x-signature=Ia5G8WTDPiAT%2Bxc1F6dhD7ymwJU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a3c5be4403c7478bba839a8a34943b6a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1741225962&x-signature=8R9FmoM7bpZRu9Ooimq6ew3Cndg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9be84be94e924baa8f48056f0da3cbc2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1741225962&x-signature=05x47A5dulat81Pl3jTtxu%2FiS7g%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3bf46e3f59434d7184c43b2a9eb94309~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1741225962&x-signature=gXTgikrDMZqOQtHUZ0Xs8%2BPjEcI%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"FlutterWeb实战:07-自动化部署","url":"https://juejin.cn/post/7475555271990755367","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
\\n\\nFlutter Web 开发打包后,可以手动发布到服务器上,通过 nginx 来托管静态页面。本文将介绍如何将这一过程自动化。
\\n
使用脚本自动化构建,然后使用 Docker 打包成镜像,最后部署在服务器上。
\\n这里使用 GitLab-CI 来自动化构建。
\\n整个流水线分为四步,分别是前端构建、Flutter Web 构建、Docker 镜像打包、以及部署。
\\nbuild-js:\\n image: zacksleo/node:19\\n stage: .pre\\n script: |-\\n CI_COMMIT_TAG=${CI_COMMIT_TAG:-latest}\\n cd packages/apps/web\\n yarn install\\n sed -i \\"s/main.dart.js/main.dart.js?v=$CI_COMMIT_SHORT_SHA/g\\" src/index.js\\n sed -i \\"s/flutter.js/flutter.js?v=$CI_COMMIT_SHORT_SHA/g\\" public/index.html\\n yarn build\\n artifacts:\\n paths:\\n - packages/apps/web/web\\n expire_in: 60 mins\\n
\\n这里使用了 flutter build web
命令来构建 Flutter Web 应用,构建后批量对文件重命名,统一增加 Commit Hash 后缀,以解决缓存问题。
build-web:\\n stage: build\\n script: |-\\n CI_COMMIT_TAG=${CI_COMMIT_TAG:-latest}\\n echo $(git log -1 --pretty=%s | tail -1) > ./release.log\\n cd packages/apps/web\\n echo -e \'\\\\nHIDE_APP_BAR=true\' >> env\\n flutter build web --pwa-strategy none --build-number=$VERSION_CODE --build-name=$TAG --web-renderer html --base-href /webapp/\\n cp .deploy/flutter.js build/web\\n # 对part文件重命名,以解决缓存问题\\n sed -i \\"s/.part.js/.$CI_COMMIT_SHORT_SHA.part.js/g\\" build/web/main.dart.js\\n sed -i \\"s/.part.js/.$CI_COMMIT_SHORT_SHA.part.js/g\\" build/web/flutter_service_worker.js\\n for file in build/web/main.dart.js_* ; do mv $file ${file//part/$CI_COMMIT_SHORT_SHA.part} ; done\\n needs: [\\"build-js\\"]\\n dependencies:\\n - build-js\\n artifacts:\\n paths:\\n - packages/apps/web/build/web\\n - ./release.log\\n expire_in: 120 mins\\n
\\n这里使用 Docker 来打包镜像,然后推送到 Docker 镜像仓库。打包时,替换了压缩版本的字体图标文件。
\\nDockerfile 文件配置
\\nFROM nginx:alpine\\nCOPY .deploy/nginx.conf /etc/nginx/nginx.conf\\nCOPY build/web /usr/share/nginx/html\\n
\\nGitLab-CI 文件配置
\\ndockerize:\\n stage: dockerize\\n image: docker:latest\\n needs: [\\"build-web\\"]\\n dependencies:\\n - build-web\\n before_script: []\\n script:\\n - if [[ -z \\"$CI_COMMIT_TAG\\" ]];then\\n - CI_COMMIT_TAG=\\"latest\\"\\n - fi\\n - cd packages/apps/web\\n - cp build/web/fonts/MaterialIcons-Regular.otf build/web/assets/fonts\\n - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY\\n - docker build --build-arg DEPLOY_ENV=$DEPLOY_ENV -t $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG .\\n - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG\\n - docker rmi $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG || true\\n
\\n这里使用了 rsync 来同步部署文件到服务器上,然后使用 docker-compose 拉取镜像和来启动服务。
\\nprod-web:\\n image: zacksleo/node\\n stage: release\\n needs: [\\"dockerize\\"]\\n variables:\\n DEPLOY_SERVER: \\"10.10.10.10\\"\\n SSH_PORT: 22\\n script:\\n - cd packages/apps/web/.deploy\\n - CI_COMMIT_TAG=${CI_COMMIT_TAG:-latest}\\n - SSH_PORT=${SSH_PORT:-22}\\n - rsync -rtvhze \\"ssh -p $SSH_PORT\\" . root@$DEPLOY_SERVER:/data/$CI_PROJECT_NAME --stats\\n - ssh -p $SSH_PORT root@$DEPLOY_SERVER \\"docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY\\"\\n - ssh -p $SSH_PORT root@$DEPLOY_SERVER \\"export COMPOSE_HTTP_TIMEOUT=120 && export DOCKER_CLIENT_TIMEOUT=120 && cd /data/$CI_PROJECT_NAME && echo -e \'\\\\nTAG=$CI_COMMIT_TAG\' >> .env && docker-compose pull $MODULE && docker-compose stop $MODULE && docker-compose rm -f $MODULE && docker-compose up -d $MODULE\\"\\n needs: [\\"dockerize\\"]\\n
\\nuser nginx;\\nworker_processes 1;\\n\\nerror_log /var/log/nginx/error.log warn;\\npid /var/run/nginx.pid;\\n\\nevents {\\n worker_connections 1024;\\n}\\n\\nhttp {\\n include /etc/nginx/mime.types;\\n default_type application/octet-stream;\\n log_format main \'$remote_addr - $remote_user [$time_local] \\"$request\\" \'\\n \'$status $body_bytes_sent \\"$http_referer\\" \'\\n \'\\"$http_user_agent\\" \\"$http_x_forwarded_for\\"\';\\n\\n access_log /var/log/nginx/access.log main;\\n sendfile on;\\n keepalive_timeout 65;\\n\\n gzip on;\\n gzip_min_length 1k;\\n gzip_comp_level 5;\\n gzip_vary on;\\n gzip_static on;\\n gzip_types text/plain text/html text/css application/javascript application/x-javascript text/xml application/xml application/xml application/json;\\n\\n client_max_body_size 2M;\\n\\n server {\\n listen 80;\\n root /usr/share/nginx/html;\\n location /webapp/ {\\n rewrite ^/webapp(/.*)$ $1 last;\\n index index.html index.htm\\n try_files $uri $uri/index.html $uri/ =404;\\n }\\n }\\n}\\n\\n
\\n这里面我们使用 js
库来实现 JS 调用 Dart,首先添加依赖:
dependencies:\\n+ js: ^0.6.4\\n
\\n在 Dart 侧定义调用方法
\\n@JS()\\n@anonymous\\nclass WxConfigOption {\\n /// 开启调试模式,调用的所有 api 的返回值会在客户端 alert 出来,若要查看传入的参数,可以在 pc 端打开,参数信息会通过 log 打出,仅在 pc 端时才会打印。\\n external bool? debug;\\n\\n /// 公众号的唯一标识\\n external String appId;\\n\\n /// 生成签名的时间戳\\n external num timestamp;\\n\\n /// 生成签名的随机串\\n external String nonceStr;\\n\\n /// 签名\\n external String signature;\\n\\n /// 需要使用的 JS 接口列表\\n external List<String> jsApiList;\\n\\n /// 需要跳转的标签类型\\n external List<String> openTagList;\\n\\n external factory WxConfigOption({\\n bool? debug,\\n required String appId,\\n required num timestamp,\\n required String nonceStr,\\n required String signature,\\n required List<String> jsApiList,\\n required List<String> openTagList,\\n });\\n}\\n
\\n@JS()\\nclass Promise<T> {\\n external Promise(void Function(void Function(T result) resolve, Function reject) executor);\\n external Promise then(void Function(T result) onFulfilled, [Function onRejected]);\\n}\\n\\n/// 声明调用方法\\n@JS(\\"flutterWeb\\")\\nclass FlutterWeb {\\n /// 配置js-sdk\\n external static Promise<void> configJsSdk(WxConfigOption options);\\n\\n /// 上传图片\\n external static Promise<String> uploadImage();\\n}\\n\\n
\\n在《FlutterWeb实战:04-集成微信JS-SDK提供丰富体验》中,我们介绍了如何封装微信的 JS-SDK 方法,供 Flutter 调用。
\\n最后在 JS 侧导出了被调用方法:
\\nimport * as flutterWeb from \\"./index.js\\";\\n\\nwindow.flutterWeb = flutterWeb;\\n
\\n这样就可以在 Dart 侧调用 JS 方法了:
\\nFuture resolveSdkSign() {\\n final completer = Completer<void>();\\n FlutterWeb.configJsSdk(WxConfigOption(\\n appId: appId,\\n timestamp: timestamp,\\n nonceStr: nonceStr,\\n signature: signature,\\n jsApiList: [\'chooseImage\',\'uploadImage\'],\\n openTagList: [\\n \'wx-open-launch-app\',\\n \'wx-open-launch-weapp\'\\n ]\\n )).then(allowInterop(completer.complete),\\n allowInterop(completer.completeError));\\n return completer.future;\\n}\\n
\\n可以以这种形式调用:
\\n配置 JS-SDK
\\nresolveSdkSign().then((_) {})\\n
\\n上传图片
\\nFlutterWeb.uploadImage()\\n .then(allowInterop(completer.complete), allowInterop(completer.completeError));\\n
\\nwindow.jsOnEvent(\\"events.page.active\\");\\n
\\n@JS(\'jsOnEvent\')\\nexternal set _jsOnEvent(void Function(dynamic event) f);\\n\\nclass PlatformCallWebPlugin {\\n static void registerWith(Registrar registrar) {\\n final MethodChannel channel = MethodChannel(\\n \'nicestwood.com/forest\', const StandardMethodCodec(), registrar);\\n channel.setMethodCallHandler(handleMethodHandler);\\n\\n //Sets the call from JavaScript handler\\n _jsOnEvent = allowInterop((dynamic event) {\\n //\\n if (event == \'events.page.active\') {\\n // do something\\n }\\n });\\n }\\n}\\n\\n
\\n在前面的文章《FlutterWeb实战:04-集成微信JS-SDK提供丰富体验》中,我们介绍了如何集成微信 JS-SDK,实现与微信 H5 交互。
\\n如果 H5 在微信小程序中打开,还可以调用 JSSDK 提供的小程序相关的 API。以下是可调用的API
\\nwx.miniProgram.navigateTo\\nwx.miniProgram.navigateBack\\nwx.miniProgram.switchTab\\nwx.miniProgram.reLaunch\\nwx.miniProgram.redirectTo\\n#向小程序发送消息\\nwx.miniProgram.postMessag\\n#获取当前环境\\nwx.miniProgram.getEnv\\n
\\n一种常用的场景是将部分页面以 H5 形式内嵌到小程序的 Webview 提供次级页面服务。这里面涉及到账号打通的问题。
\\n我们希望当用户在小程序中打开 Webview 页面,不需要登录、授权,就可以直接在 H5 中继续相应的操作。这里有一种方式,可以通过 设置 Cookie 来共享登录状态。
\\n服务端提供一个API接口,或者称为一个URL地址,形如
\\nhttps://xxx.com/app/redirect?accessToken={accessToken}&to={to}
这个接口接收两个参数,accessToken
代表用户的 Token,to
表示要跳转的页面地址(为确保正确解析,使用urlencode编码)。
假设我们使用 flutter 编写了一个订单页面,其路由为 /order/index
,那么这个页面的 URL 为 https://xxx.com/webapp/#/order/index
, 这里面我们使用二级目录托管 Flutter Web 页面,让他与 API 使用相同域名。
当用户在小程序中打开 Webivew 的页面,我们希望用户打开 https://xxx.com/webapp/#/order/index
页面,但为了保持登录状态,我们不直接打开这个页面,而是需要通过统一跳转接口中转,
也就是用户打开的是 https://xxx.com/app/redirect?accessToken={accessToken}&to=/webapp/#/order/index
,在这个接口中,服务端接收两个参数,并向客户端设置 Cookie,同时向客户端发起一个301临时重定向,\\n小程序的Webview在收到响应后,要自动进行跳转,最终也就跳转到了我们的目的页面,同时本地Cookie中保存的AccessToken,这样也就实现了登录状态共享。
服务端的代码类似如下实现:
\\n// 定义跳转接口\\n@GetMapping(\\"/app/redirect\\")\\npublic ResponseEntity<Void> redirectToTargetPage(\\n @RequestParam(\\"accessToken\\") String accessToken,\\n @RequestParam(\\"to\\") String targetUrl,\\n HttpServletResponse response) throws IOException {\\n\\n // 创建 Cookie 并设置值\\n Cookie accessTokenCookie = new Cookie(\\"accessToken\\", accessToken);\\n // 设置 Cookie 的路径,保证整个站点有效\\n accessTokenCookie.setPath(\\"/\\");\\n // 设置 Cookie 的过期时间,单位是秒,这里设置为 1小时\\n accessTokenCookie.setMaxAge(3600);\\n\\n // 将 Cookie 添加到响应中\\n response.addCookie(accessTokenCookie);\\n\\n // 设置 301 临时重定向\\n HttpHeaders headers = new HttpHeaders();\\n headers.add(\\"Location\\", targetUrl);\\n\\n // 返回 301 状态码和 Location 头部,触发客户端重定向\\n return new ResponseEntity<>(headers, HttpStatus.MOVED_PERMANENTLY);\\n}\\n
\\n\\n\\n这里需要注意的是,接口域名必须和跳转页面的域名一致,否则无法共享 Cookie。
\\n
在Flutter
的布局体系中,Container
是最基础且功能最丰富的组件之一。它既可以作为简单的视觉容器,也能通过组合多种属性(如尺寸
、边距
、对齐
、装饰
等)实现复杂的布局效果。然而,许多开发者对Container
的认知仅停留在表层,未能深入其设计哲学与性能优化细节,导致代码冗余或性能问题。
本文将通过六维知识体系,深度解构Container
容器,揭示其隐藏的关键布局规则,并配合企业级案例代码,帮助开发者从\\"会用\\"
到\\"精通\\"
,最终实现精准控制像素级布局的能力。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\nContainer({\\n super.key,\\n this.alignment,\\n this.padding,\\n this.color,\\n this.decoration,\\n this.foregroundDecoration,\\n double? width,\\n double? height,\\n BoxConstraints? constraints,\\n this.margin,\\n this.transform,\\n this.transformAlignment,\\n this.child,\\n this.clipBehavior = Clip.none,\\n})\\n
\\nwidth
和height
Container(\\n width: 200.0,\\n height: 100.0,\\n color: Colors.blue,\\n),\\n
\\n图示:
\\n\\n\\n注意事项:\\n如果不指定
\\nwidth
和height
,Container
会尽可能地占据其父Widget
允许的空间。例如,在一个Column
中,如果不设置width
,Container
会自动填充Column
的整个宽度。
constraints
Container(\\n constraints: BoxConstraints(\\n minWidth: 100,\\n maxWidth: 300,\\n minHeight: 50,\\n maxHeight: 200,\\n ),\\n color: Colors.green,\\n),\\n
\\n图示:
\\n\\n\\n注意事项:\\n如果同时设置了
\\nwidth
和height
以及constraints
,width
和height
会优先于constraints
中的对应设置。比如,设置了width: 150
同时constraints
中minWidth: 200
,那么最终Container
的宽度还是150
像素 。
padding
:内边距用于设置 Container
内部子 Widget
与 Container
边缘的距离,接受一个 EdgeInsets
对象。
Container(\\n padding: EdgeInsets.symmetric(vertical: 15.0, horizontal: 20.0),\\n color: Colors.yellow,\\n child: Text(\'Hello, Flutter!\'),\\n),\\n
\\n图示:
\\nmargin
:外边距用于设置 Container
与周围 Widget
之间的距离,它接受一个 EdgeInsets
对象。EdgeInsets
可以分别设置上、下、左、右边距。
Container(\\n height: 100,\\n margin: EdgeInsets.all(10.0),\\n color: Colors.red,\\n),\\n
\\n效果图:
\\n使用 alignment
属性设置 Container
内部子 Widget
的对齐方式,它接受一个 AlignmentGeometry
对象
Container(\\n width: 200.0,\\n height: 200.0,\\n color: Colors.grey,\\n alignment: Alignment.center,\\n child: Text(\'Center\'),\\n),\\n
\\n图示:
\\ncolor
用于设置 Container
的背景颜色,它接受一个 Color
对象。
Container(\\n height: 100,\\n color: Colors.purple,\\n),\\n
\\n图示:
\\n互斥性:color
是decoration
的简写属性,二者不可共存。
decoration
用于更复杂的装饰设置,比如背景图片
、渐变
、边框
等,它接受一个 BoxDecoration
对象。
Container(\\n width: 200,\\n height: 200,\\n decoration: BoxDecoration(\\n color: Colors.blue,\\n boxShadow: [\\n BoxShadow(\\n color: Colors.black12,\\n blurRadius: 6,\\n offset: Offset(0, 2),\\n ),\\n ],\\n borderRadius: BorderRadius.all(Radius.circular(20)),\\n border: Border.all(\\n color: Colors.black,\\n width: 2.0,\\n ),\\n gradient: LinearGradient(\\n colors: [Colors.orange, Colors.pink],\\n begin: Alignment.topLeft,\\n end: Alignment.bottomRight,\\n ),\\n image: DecorationImage(\\n image: AssetImage(\'assets/images/ic_launcher.png\'),\\n fit: BoxFit.cover,\\n ),\\n ),\\n),\\n
\\n图示:
\\nforegroundDecoration
用于设置 Container
的前景装饰,它也接受一个 BoxDecoration
对象,不过它是绘制在子 Widget
之上的。
Container(\\n width: 200,\\n height: 200,\\n foregroundDecoration: BoxDecoration(\\n color: Colors.black.withOpacity(0.5),\\n ),\\n child: Image.asset(\'assets/images/ic_launcher.png\'),\\n),\\n
\\n图示:
\\ntransform
用于对 Container
进行矩阵变换,比如平移
、旋转
、缩放
等,它接受一个 Matrix4
对象。
Container(\\n width: 100,\\n height: 100,\\n color: Colors.green,\\n transform: Matrix4.rotationZ(0.5),\\n),\\nContainer(\\n width: 100,\\n height: 100,\\n color: Colors.blue,\\n transform: Matrix4.diagonal3Values(1.5, 1.5, 1.0),\\n),\\n
\\n图示:
\\nMatrix4.rotationZ(0.5)
表示 Container
绕 Z
轴旋转 0.5
弧度(约 28.65
度)。Matrix4.diagonal3Values(1.5, 1.5, 1.0)
表示在 X
和 Y
方向上进行 1.5
倍的缩放,Z
方向不变。clipBehavior
用于指定当子 Widget
超出 Container
边界时的裁剪行为,它接受一个 Clip
枚举值。
Container(\\n width: 100,\\n height: 100,\\n decoration: BoxDecoration(\\n color: Colors.blue,\\n boxShadow: [\\n BoxShadow(\\n color: Colors.black12,\\n blurRadius: 6,\\n offset: Offset(0, 2),\\n ),\\n ],\\n ),\\n clipBehavior: Clip.antiAlias,\\n child: Image.asset(\'assets/images/ic_launcher.png\'),\\n),\\n
\\n图示:
\\nContainer(\\n width: 300.0,\\n height: 300.0,\\n decoration: BoxDecoration(\\n border: Border.all(\\n color: Colors.blue,\\n width: 2.0,\\n ),\\n ),\\n child: Container(\\n margin: EdgeInsets.all(10.0),\\n decoration: BoxDecoration(\\n border: Border.all(\\n color: Colors.red,\\n width: 1.5,\\n ),\\n ),\\n child: Container(\\n padding: EdgeInsets.all(15.0),\\n color: Colors.lightGreen,\\n child: Text(\'Nested Containers\'),\\n ),\\n ),\\n)\\n
\\n图示:
\\nAnimatedContainer
是Container
的动画版本,它可以在属性值发生变化时自动生成过渡动画。比如,实现一个点击按钮改变 Container
大小和颜色的动画效果。
class ContainerDemo extends StatefulWidget {\\n @override\\n _ContainerDemoState createState() => _ContainerDemoState();\\n}\\n\\nclass _ContainerDemoState extends State<ContainerDemo> {\\n double _width = 100.0;\\n double _height = 100.0;\\n Color _color = Colors.red;\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: Text(\'Animated Container Demo\'),\\n ),\\n body: Center(\\n child: Column(\\n mainAxisAlignment: MainAxisAlignment.center,\\n children: [\\n AnimatedContainer(\\n duration: Duration(milliseconds: 500),\\n width: _width,\\n height: _height,\\n color: _color,\\n ),\\n TextButton(\\n child: Text(\'Animate\'),\\n onPressed: () {\\n setState(() {\\n _width = _width == 100.0? 200.0 : 100.0;\\n _height = _height == 100.0? 200.0 : 100.0;\\n _color = _color == Colors.red? Colors.blue : Colors.red;\\n });\\n },\\n ),\\n ],\\n ),\\n ),\\n );\\n }\\n}\\n
\\nWidget
Container
可以作为构建自定义 Widget
的基础,通过封装常用的样式和布局,提高代码的复用性。例如,创建一个具有特定样式的卡片 Widget
。
class CustomCard extends StatelessWidget {\\n final String title;\\n final String description;\\n\\n CustomCard({required this.title, required this.description});\\n\\n @override\\n Widget build(BuildContext context) {\\n return Container(\\n width: double.infinity,\\n margin: EdgeInsets.symmetric(vertical: 10.0, horizontal: 15.0),\\n decoration: BoxDecoration(\\n color: Colors.white,\\n borderRadius: BorderRadius.circular(8.0),\\n boxShadow: [\\n BoxShadow(\\n color: Colors.grey.withOpacity(0.5),\\n spreadRadius: 2,\\n blurRadius: 5,\\n offset: Offset(0, 3),\\n ),\\n ],\\n ),\\n child: Padding(\\n padding: EdgeInsets.all(15.0),\\n child: Column(\\n crossAxisAlignment: CrossAxisAlignment.start,\\n children: [\\n Text(\\n title,\\n style: TextStyle(\\n fontSize: 18.0,\\n fontWeight: FontWeight.bold,\\n ),\\n ),\\n SizedBox(height: 8.0),\\n Text(description),\\n ],\\n ),\\n ),\\n );\\n }\\n}\\n\\n调用方式:\\nCustomCard(\\n title: \'Card Title\',\\n description: \'This is a sample description for the card.\',\\n)\\n
\\n图示:
\\n在 Flutter
中,当 Widget
的状态发生变化时,会触发重建。频繁的重建会影响性能,特别是对于包含大量子 Widget
的 Container
。
const
构造函数使用const
修饰,在构建时会将其识别为常量,不会因为父 Widget
的重建而重新构建,从而节省性能。
const Container(\\n color: Colors.blue,\\n padding: EdgeInsets.all(8),\\n),\\n
\\n合理使用状态管理机制,避免不必要的状态更新导致 Container
重建。
例如,使用InheritedWidget
或状态管理库(如 Provider
、Bloc
等),确保只有真正需要更新的部分才会触发重建。
检测工具:DevTools
的Layer Snapshot
。
优化方法:
\\ndecoration
的绘制层级(如避免多层边框叠加)。ClipRect
或ClipPath
限制绘制区域。问题根源:transform
或复杂decoration
可能导致额外的Layer
生成。
解决方案:
\\nTransform
而非Container.transform
。关键代码(简化版
):
class Container extends StatelessWidget {\\n // 构造函数参数...\\n Widget build(BuildContext context) {\\n Widget current = child;\\n if (child != null) {\\n if (padding != null) {\\n current = Padding(padding: padding, child: current);\\n }\\n if (alignment != null) {\\n current = Align(alignment: alignment, child: current);\\n }\\n }\\n if (constraints != null) {\\n current = ConstrainedBox(constraints: constraints, child: current);\\n }\\n // 应用decoration、transform等...\\n return current;\\n }\\n}\\n
\\n核心结论:Container
本质是多个布局组件的组合(如Padding
+ Align
+ ConstrainedBox
)。
constraints
;padding
和alignment
;decoration
;transform
。问题背景:传统UI
框架(如Android
的View
)通过继承实现功能扩展,导致类层次臃肿。
Flutter
的选择:通过组合单一职责的小组件(如Padding
、Align
)实现复杂功能。
优势:
\\n职责单一
,代码更易测试和复用。Container
的哲学启示Unix
哲学:“Do One Thing and Do It Well”
。“全能上帝类”
。Container
?推荐场景:
\\n尺寸
、边距
、装饰
的容器;替代方案:
\\nPadding
;SizedBox
;DecoratedBox
。反面案例:
\\nContainer(\\n child: Container(\\n margin: EdgeInsets.all(8),\\n child: Container(\\n padding: EdgeInsets.all(4),\\n child: Text(\'过度嵌套\'),\\n ),\\n ),\\n)\\n
\\n优化方案:
\\nContainer(\\n margin: EdgeInsets.all(8),\\n padding: EdgeInsets.all(4),\\n child: Text(\'简化版\'),\\n)\\n
\\n问题:复杂的BoxDecoration
导致代码臃肿。
方案:
\\n将装饰配置抽取为常量:
\\nconst _kCardDecoration = BoxDecoration(\\n color: Colors.white,\\n borderRadius: BorderRadius.circular(8),\\n boxShadow: [BoxShadow(blurRadius: 4)],\\n);\\n
\\n使用DecoratedBox
封装复用逻辑。
Container
作为Flutter
布局系统的核心组件,其强大之处在于通过组合简单属性实现复杂效果。开发者需深入理解其属性优先级(如父约束优先于子constraints
)、性能陷阱(如过度绘制
与图层合成
)以及背后的设计哲学(组合优于继承
)。
在实际开发中,遵循以下原则:
\\n单一职责组件
(如Padding
);避免嵌套过深
,善用const
和AnimatedContainer
;注释或常量命名解释复杂装饰的意图
。通过系统化掌握Container
,开发者能够编写出既高效又易维护的Flutter
代码,真正驾驭这一布局利器的全部潜力。
\\n","description":"前言 在Flutter的布局体系中,Container是最基础且功能最丰富的组件之一。它既可以作为简单的视觉容器,也能通过组合多种属性(如尺寸、边距、对齐、装饰等)实现复杂的布局效果。然而,许多开发者对Container的认知仅停留在表层,未能深入其设计哲学与性能优化细节,导致代码冗余或性能问题。\\n\\n本文将通过六维知识体系,深度解构Container容器,揭示其隐藏的关键布局规则,并配合企业级案例代码,帮助开发者从\\"会用\\"到\\"精通\\",最终实现精准控制像素级布局的能力。\\n\\n操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。\\n\\n一、基础认知\\nContai…","guid":"https://juejin.cn/post/7475377684882276406","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-26T03:25:56.037Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/219af6c8347947038f199fdfe4ac6894~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1741145156&x-signature=K2QHDAYNMiGeBH%2FngYfa6nXGHuw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1cd795201f5141d09ebefb04c3de75f1~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1741145156&x-signature=1v0p1v5DvjiNVXYKUJXBmRwYjjg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7d8571967e2a4ee89e6ea2d5da781442~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1741145156&x-signature=6gtss72vJJDLlb1XIPNz29g0mBM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/dc54e096cde549f68175e527062691ea~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1741145156&x-signature=xV3h8wenVRXd%2BPLrsXbI%2FkYMMu8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/80f68fe72dc44a94bf8cb0cdef4eee44~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1741145156&x-signature=wU7L6UPSs1abrZ%2BbM70LnT3kV9s%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c56ba341ef834b06981a41f33ae0c3f9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1741145156&x-signature=RGHFR4gzDb4oDhH7492WEqnwmFc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9a5c2af8544e40b79816b83bb47b4822~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1741145156&x-signature=cjfP2Yh22AX1zm9tPSqKuvyv0Lk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7ebfb92138e44e129f9bc4512fb02654~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1741145156&x-signature=olpRnVzuINb%2F6jLflDwqyemQh3k%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d20c3381ad004498b36cda7837fd06c1~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1741145156&x-signature=1KmewiFuHeS6tOT4yx%2Fi51dvz2Y%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/dce934e99b2942f0b21940ce7936b505~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1741145156&x-signature=xbDthmmf4DSwEei6%2F2QCvhLYyrE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/87637c8cbaf445dea5530536694217a5~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1741145156&x-signature=HgU09hkMxQ4iMrlBqSWN%2FQj9gy4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/8542bdcbb4484946a10c4c0be21ccb45~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1741145156&x-signature=NxysavFqfX4Q9PUYShP5iaVQtwI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f2eaa51f2be9489ba64099975f9d11a7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1741145156&x-signature=%2BN5gJgvBq7c%2Fqlc43Q3OUTPGw%2Fo%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 组件集录 | SliverFloatingHeader @3.27.0","url":"https://juejin.cn/post/7475288228229808182","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
SliverFloatingHeader 是 3.27.0 中新增组件之一,它可以让开发者更容易地实现头部浮动的功能。先通过本文两个案例,看一下它的效果,SliverFloatingHeader 的特点是:
\\n\\n\\n向上滑动时内容组件会移出屏幕,向下滑动时,会展示出来。
\\n
FlutterUnit 之前也有类似的效果,是通过自定义 SliverPersistentHeader 代理器实现的,比较麻烦。SliverFloatingHeader 支持之后,可以轻松完成同样的功能:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n官方案例 | FlutterUnit 首页 |
---|---|
SliverFloatingHeader 组件在使用上非常简单,它和其他的 Sliver 组件一样,只有用于滑动视口中。比如这里通过 CustomScrollView
盛纳多个 Sliver 滑片:
CustomScrollView(\\n slivers: <Widget>[\\n SliverFloatingHeader(\\n child: ListHeader(\\n text:\\n \'SliverFloatingHeader\\\\nScroll down a little to show\\\\nScroll up a little to hide\',\\n ),\\n snapMode: FloatingHeaderSnapMode.overlay,\\n ),\\n ItemList(),\\n ],\\n),\\n
\\nclass ItemList extends StatelessWidget {\\n const ItemList({super.key, this.itemCount = 50});\\n\\n final int itemCount;\\n\\n @override\\n Widget build(BuildContext context) {\\n final ColorScheme colorScheme = Theme.of(context).colorScheme;\\n return SliverList(\\n delegate: SliverChildBuilderDelegate((BuildContext context, int index) {\\n return Card(\\n color: colorScheme.onSecondary,\\n child: ListTile(textColor: colorScheme.secondary, title: Text(\'Item $index\')),\\n );\\n }, childCount: itemCount),\\n );\\n }\\n}\\n
\\n这样就实现了 ListHeader 头部内容的上述滚动效果。
\\nFlutterUnit 首页头部的搜索标题栏,会随着下滑消失、上滑出现。有了 SliverFloatingHeader 之后,只需要如下非常简单地处理即可:其中头部栏的构建交由 StandardHomeSearch
组件负责,可以无缝衔接:
总得来说,SliverFloatingHeader 的功能非常简单,只要有想让头部栏下滑消失、上滑出现的场景,用 SliverFloatingHeader 套一下就可以了。
\\n进入 SliverFloatingHeader 源码中的构造函数中瞥一眼可以看出,它还有两个可配置的参数:
\\n其中 snapMode
类型是 FloatingHeaderSnapMode 枚举,默认是 overlay
模式,两者区别在于:
overlay | scroll |
---|---|
enum FloatingHeaderSnapMode {\\n overlay,\\n scroll,\\n}\\n
\\nanimationStyle
的类型是 AnimationStyle
, 用于动画样式的配置,包括正反动画时长、曲线:
class AnimationStyle with Diagnosticable {\\n /// Creates an instance of Animation Style class.\\n AnimationStyle({\\n this.curve,\\n this.duration,\\n this.reverseCurve,\\n this.reverseDuration,\\n });\\n
\\n下面来简单看一下 SliverFloatingHeader 的源码实现,从中汲取一些有用的知识。从上面的构造中可以看出 SliverFloatingHeader
是一个 StatefulWidget
, 它会依赖 _SliverFloatingHeaderState
状态类完成构建组件的任务。
\\n有状态组件的必要性是,该组件需要进行动画效果,状态类混入了 SingleTickerProviderStateMixin
。并将自身作为 Ticker 创建器传给 _SliverFloatingHeader
组件:
_SliverFloatingHeader
是一个单子渲染组件,维护 _RenderSliverFloatingHeader
渲染对象。SliverFloatingHeader 的两个属性,也是用于该渲染对象中的:
同步标题的内容通过 _SnapTrigger
构建,这里值得学习一下普通组件如何获取滚动的位置,进行联动。如下所示: 在 didChangeDependencies
中,通过 Scrollable.maybeOf(context)
可以获取 ScrollableState 滑动状态类, 对其中的 position#isScrollingNotifier
进行监听,可以得到是否正在滑动的监听,
正在滑动中时,触发 isScrollingListener
回调,其中通过上下文寻找 _RenderSliverFloatingHeader
渲染对象,触发它的 isScrollingUpdate
更新位置。
关于 _RenderSliverFloatingHeader
实现的具体细节,感兴趣的朋友可以自己研究一下。总得来说 SliverFloatingHeader 组件对于使用者来说,简单有强大,可以方便地解决场景中的痛点。希望本文可以帮到你,更多的组件介绍分享,敬请期待 ~
更多文章和视频知识资讯,大家可以关注我的公众号、掘金和 B 站 。
\\n","description":"1. 前言 SliverFloatingHeader 是 3.27.0 中新增组件之一,它可以让开发者更容易地实现头部浮动的功能。先通过本文两个案例,看一下它的效果,SliverFloatingHeader 的特点是:\\n\\n向上滑动时内容组件会移出屏幕,向下滑动时,会展示出来。\\n\\nFlutterUnit 之前也有类似的效果,是通过自定义 SliverPersistentHeader 代理器实现的,比较麻烦。SliverFloatingHeader 支持之后,可以轻松完成同样的功能:\\n\\n官方案例\\tFlutterUnit 首页\\t\\n1…","guid":"https://juejin.cn/post/7475288228229808182","author":"张风捷特烈","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-26T01:40:22.482Z","media":[{"url":"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4c7893ad268442e8af6e6fa8a44b977b~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1920&h=1200&s=388663&e=png&b=fefbfb","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3f64cefa2c3547568261129c98d92539~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=400&h=890&s=387054&e=webp&f=57&b=fdfdfb","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/762ffc4c6d7e4692b90b400849df37b3~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=400&h=890&s=770390&e=webp&f=35&b=fcfcfa","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1b5f34cbc8974139b9ee12068415da75~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1051&h=328&s=58508&e=png&b=fffefe","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1101bc26916540f28ccdaa32eb8cb33a~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1056&h=260&s=31060&e=png&b=ffffff","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dc253290ef43421283a9ee7b5ad38904~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=640&h=680&s=690281&e=gif&f=50&b=fefefc","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/32724413f4ab4b0d910b0be6972b41ad~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=640&h=680&s=729980&e=gif&f=42&b=fefefc","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f9b928a71eb3491395caf76cd54cb626~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=974&h=468&s=66880&e=png&b=ffffff","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8086926b21f441b79a9cc179e10547fd~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1060&h=461&s=78929&e=png&b=ffffff","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ecb28913b9854d70b6e1d5cbcbfc4196~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1069&h=727&s=102611&e=png&b=ffffff","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e3e2533411c349308393a6ca2303ba6c~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1044&h=211&s=36275&e=png&b=ffffff","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"通义千问2.5-Max + Roo Code Cline 插件:实现 AI Agents 自动编程。基准测试超过 DeepSeek v3。","url":"https://juejin.cn/post/7475266130786713637","content":"\\n\\n\\n
首先得出结论:除了 Cursor 工具,我们还有许多其他选择。例如,今天提到的 Roo Code 作为一个 AI Agents 自动编码的工具,是一个 VSCode 插件,并在千问大模型 qwen-max-2025-01-25 发布时使用。目前,猫哥的主流选择仍然是:Cursor 进行代码生成,配合 GitHub Copilot 提供代码提示。同时,我们也在研究使用 Roo Code、Cline 以及各大模型平台。
\\n通义千问2.5-Max, Roo Code Cline插件, AI自动编程, 基准测试, DeepSeek v3, AI编程性能, 代码生成
\\nArena-Hard:通常指一种难度较高的测试环境或基准,用于评估模型在复杂任务中的表现。
\\nLiveBench:可能指一种实时基准测试,旨在评估模型在实际应用中的表现,尤其是在动态或变化环境中。
\\nLiveCodeBench:类似于LiveBench,但可能更专注于代码的实时执行和评估,特别是在软件开发和代码优化方面。
\\nGPQA-Diamond:可能指某种特定的基准测试或评估指标,特别是在自然语言处理或问答系统中,GPQA通常与生成式问答(Generative Question Answering)相关,\\"Diamond\\"可能是该基准的一个特定版本或变体。
\\n开通 api keys
\\nbailian.console.aliyun.com/?apiKey=1#/…
\\n测试模型是否可用
\\ncurl -X POST https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions \\\\\\n-H \\"Authorization: Bearer $DASHSCOPE_API_KEY\\" \\\\\\n-H \\"Content-Type: application/json\\" \\\\\\n-d \'{\\n \\"model\\": \\"qwen-max-2025-01-25\\",\\n \\"messages\\": [\\n {\\n \\"role\\": \\"system\\",\\n \\"content\\": \\"You are a helpful assistant.\\"\\n },\\n {\\n \\"role\\": \\"user\\", \\n \\"content\\": \\"你是谁?\\"\\n }\\n ]\\n}\'\\n
\\n\\n\\n替换其中的 api key 和 model 模型名称
\\n
模型名称如下:
\\n\\n\\n\\nAPI Provider :OpenAI Compatible
\\nBase URL: dashscope.aliyuncs.com/compatible-…
\\nModel: qwen-max-2025-01-25
\\n启用流方式 Enable Streaming
\\n
我的主语言是简体中文,所以请用简体中文回答我,与我交流。\\n\\n您是一名高级 Dart 程序员,具有 Flutter 框架的经验,并偏好干净的编程和设计模式。\\n\\n生成符合基本原则和命名规范的代码、修正和重构。\\n\\n## Dart 一般指南\\n\\n### 基本原则\\n\\n- 所有代码和文档使用中文。\\n- 始终声明每个变量和函数的类型(参数和返回值)。\\n - 避免使用任何类型(any)。\\n - 创建必要的类型。\\n- 不要在函数内部留空行。\\n- 每个文件只导出一个。\\n\\n### 命名规范\\n\\n- 类使用 PascalCase。\\n- 变量、函数和方法使用 camelCase。\\n- 文件和目录名称使用 underscores_case。\\n- 环境变量使用 UPPERCASE。\\n - 避免魔法数字,定义常量。\\n- 每个函数以动词开头。\\n- 布尔变量使用动词,例如:isLoading、hasError、canDelete 等。\\n- 使用完整单词而非缩写,并确保拼写正确。\\n - 除了标准缩写,如 API、URL 等。\\n - 除了众所周知的缩写:\\n - i、j 用于循环\\n - err 用于错误\\n - ctx 用于上下文\\n - req、res、next 用于中间件函数参数\\n\\n### 函数\\n\\n- 在此上下文中,函数的定义同样适用于方法。\\n- 编写短小的函数,功能单一。指令数少于 20 条。\\n- 用动词和其他内容命名函数。\\n - 如果返回布尔值,使用 isX 或 hasX、canX 等。\\n - 如果不返回任何内容,使用 executeX 或 saveX 等。\\n- 避免嵌套块:\\n - 提前检查并返回。\\n - 提取到工具函数中。\\n- 使用高阶函数(map、filter、reduce 等)来避免函数嵌套。\\n - 对于简单函数(少于 3 条指令)使用箭头函数。\\n - 对于非简单函数使用具名函数。\\n- 使用默认参数值,而不是检查 null 或 undefined。\\n- 通过 RO-RO 减少函数参数:\\n - 使用对象传递多个参数。\\n - 使用对象返回结果。\\n - 为输入参数和输出声明必要的类型。\\n- 使用单一的抽象级别。\\n\\n### 数据\\n\\n- 不要滥用原始类型,将数据封装在复合类型中。\\n- 避免在函数中进行数据验证,使用具有内部验证的类。\\n- 优先使用不可变数据。\\n - 对于不变的数据使用 readonly。\\n - 对于不变的字面量使用 const。\\n\\n### 类\\n\\n- 遵循 SOLID 原则。\\n- 优先使用组合而非继承。\\n- 声明接口以定义契约。\\n- 编写小型类,功能单一。\\n - 指令数少于 200。\\n - 公共方法少于 10 个。\\n - 属性少于 10 个。\\n\\n### 异常\\n\\n- 使用异常处理您不期望的错误。\\n- 如果捕获异常,应该是为了:\\n - 修复预期的问题。\\n - 添加上下文。\\n - 否则,使用全局处理程序。\\n\\n## 特定于 Flutter\\n\\n### 基本原则\\n\\n- 使用干净的架构。\\n - 如果需要将代码组织为模块,请参见模块。\\n - 如果需要将代码组织为控制器,请参见控制器。\\n - 如果需要将代码组织为服务,请参见服务。\\n - 如果需要将代码组织为存储库,请参见存储库。\\n - 如果需要将代码组织为实体,请参见实体。\\n- 使用存储库模式进行数据持久化。\\n - 如果需要缓存数据,请参见缓存。\\n- 使用控制器模式与 GetX 处理业务逻辑。\\n- 使用 GetX 管理状态。\\n - 如果需要保持状态,请参见 keepAlive。\\n- 使用 GetX 管理 UI 状态。\\n- 控制器始终接受方法作为输入,并更新影响 UI 的 UI 状态。\\n- 使用扩展管理可重用代码。\\n- 使用 ThemeData 管理主题。\\n- 使用 AppLocalizations 管理翻译。\\n- 使用常量管理常量值。\\n- 当小部件树变得过深时,可能导致更长的构建时间和更高的内存使用。Flutter 需要遍历整个树来呈现 UI,因此更平坦的结构提高了效率。\\n- 更平坦的小部件结构使理解和修改代码更容易。可重用组件也促进了更好的代码组织。\\n- 避免在 Flutter 中深度嵌套小部件。深度嵌套的小部件可能会对 Flutter 应用的可读性、可维护性和性能产生负面影响。旨在将复杂的小部件树拆分为更小的可重用组件。这不仅使您的代码更清晰,还通过减少构建复杂性来增强性能。\\n- 深度嵌套的小部件可能使状态管理变得更加困难。通过保持树的扁平化,更容易管理状态并在小部件之间传递数据。\\n- 将大型小部件拆分为更小、更专注的小部件。\\n- 尽可能使用 const 构造函数以减少重建次数。\\n\\n### 性能优化\\n\\n- 在可能的情况下使用 const 组件以优化重建。\\n- 实现列表视图优化(例如:ListView.builder)。\\n\\n### UI 和样式\\n\\n- 使用 Flutter 内置组件并创建自定义组件。\\n- 使用 LayoutBuilder 或 MediaQuery 实现响应式设计。\\n- 使用主题以保持应用一致的样式。\\n\\n### 参考\\n\\n- 界面视图库 [ducafe_ui_core packages](https://pub.dev/packages/ducafe_ui_core)\\n\\n### 代码生成\\n\\n- 使用 build_runner 从注解生成代码(Freezed、Riverpod、JSON 序列化)。\\n- 在修改注解类后运行 \'flutter pub run build_runner build --delete-conflicting-outputs\'。\\n\\n### 文档\\n\\n- 文档应复杂逻辑和非显而易见的代码决策。\\n- 遵循官方 Flutter 文档以获取最佳实践。\\n
\\n您是一位在 VSCode 中的专家 AI 编程助手,主要专注于生成清晰、可读的代码。\\n您考虑周到,提供细致的答案,并在推理方面表现出色。您仔细提供准确、事实性、深思熟虑的答案,并在推理方面表现出色。\\n\\n请仔细遵循用户的要求。\\n首先逐步思考——详细描述您要构建的内容的伪代码。\\n确认后再写代码!\\n始终编写正确、最新、无错误、功能完整、工作正常、安全、高效的代码。\\n关注可读性,而不是性能。\\n全面实现所有请求的功能。\\n不留任何待办事项、占位符或缺失部分。\\n确保代码完整!彻底验证最终结果。\\n包括所有必要的导入,并确保关键组件的命名正确。\\n简明扼要,尽量减少其他文字。\\n如果您认为可能没有正确答案,请明确指出。如果您不知道答案,请直接说出,而不是猜测。\\n
\\n写一个 unsplash 图片墙,提示词
\\n在 lib/pages/msg/msg_index/view.dart 界面中实现,unsplash.com 图片墙功能\\n
\\n\\n\\n分别在控制器和视图中加入了代码,后期还需要进一步的优化。
\\n
产出效果
\\n通义千问2.5-Max与Roo Code Cline插件的结合,为AI自动编程领域带来了全新的解决方案。通过基准测试,这一组合在性能上成功超越了DeepSeek v3,展现了强大的代码生成能力和智能化水平。无论是开发者还是企业用户,都可以借助这一技术提升开发效率,降低人工成本。未来,随着AI编程工具的不断进化,通义千问2.5-Max有望成为行业标杆,推动AI编程技术迈向新高度。
\\n感谢阅读本文
\\n如果有什么建议,请在评论中让我知道。我很乐意改进。
\\n© 猫哥\\nducafecat.com
\\nend
","description":"通义千问2.5-Max + Roo Code Cline 插件:实现 AI Agents 自动编程。基准测试超过 DeepSeek v3。 视频\\n\\nwww.bilibili.com/video/BV1s5…\\n\\nyoutu.be/TpOCkbbLOS8\\n\\n前言\\n\\n原文 通义千问2.5-Max + Roo Code Cline插件:AI自动编程新突破\\n\\n首先得出结论:除了 Cursor 工具,我们还有许多其他选择。例如,今天提到的 Roo Code 作为一个 AI Agents 自动编码的工具,是一个 VSCode 插件,并在千问大模型 qwen-max…","guid":"https://juejin.cn/post/7475266130786713637","author":"独立开发者_猫哥","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-25T12:43:27.068Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d8b17f8b33714395914c9f0ca0d70948~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg54us56uL5byA5Y-R6ICFX-eMq-WTpQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741092207&x-signature=ghlqKASmjpN9eEPon3LD5SwWnFw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/74048d63369c4c7a8080a557adcc1e4e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg54us56uL5byA5Y-R6ICFX-eMq-WTpQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741092207&x-signature=TpwmV0uk7KgTuHzZdMqbbm0cVdM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b7e34018a3304cfea83b11a2c547005f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg54us56uL5byA5Y-R6ICFX-eMq-WTpQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741092207&x-signature=lOUilvVmWWJUa1Ycav4lKEKfgtU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1e6678f1b8ac4eb1b8c2bd60803082de~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg54us56uL5byA5Y-R6ICFX-eMq-WTpQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741092207&x-signature=3NKuxpoyJKt8VenHfDtmoAAMYkE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2b14d943d61b46aa946d7a6c9225e6de~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg54us56uL5byA5Y-R6ICFX-eMq-WTpQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741092207&x-signature=PxLzBES64S5yBM14i6SUOVSl53w%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f25587e185f9445c98cc4d987dabf51d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg54us56uL5byA5Y-R6ICFX-eMq-WTpQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741092207&x-signature=aSbnONOtd6lNu7bUscmcDvRZEwQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0ab5c76a4b1146179f44722db97f15f1~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg54us56uL5byA5Y-R6ICFX-eMq-WTpQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741092207&x-signature=B7twhJTHQ3exnm9ooDo4Zh6BAwM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9b256039a47947d2b2686427613c80a4~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg54us56uL5byA5Y-R6ICFX-eMq-WTpQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741092207&x-signature=e5LuqFnSc%2FDt2zdKctjq4UxY01k%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/73f653dc40c74010b458cc51bd505632~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg54us56uL5byA5Y-R6ICFX-eMq-WTpQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741092207&x-signature=7wsKYfB29S95TWfvp7Em%2Bo6jfvI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/367bc8b127954a9c880472536d2f077c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg54us56uL5byA5Y-R6ICFX-eMq-WTpQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741092207&x-signature=YQnKrtmbwKd0Q0XxgmWy2zy%2BCho%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 中使用 Mixin 优化逻辑与功能","url":"https://juejin.cn/post/7475238092305530931","content":"\\n\\n\\n
本文详细介绍了 Flutter 中的 mixin 概念,包括其特点、使用场景以及如何有效地在多个类之间共享代码。了解如何利用 mixin 实现功能模块化,避免代码重复,提升开发效率。
\\nmixin 是一种特殊的类,允许您在多个类之间共享代码。它提供了一种方法来实现多重继承的效果,使得一个类可以同时使用多个 mixin 中定义的功能,而不需要通过传统的继承方式。
\\nwith
关键字:在使用 mixin 时,您需要在类定义中使用 with
关键字来指定要混入的 mixin。处理在线登录、离线登录、注销等逻辑。
\\n mixin AuthenticationMixin {\\n // 在线登录\\n Future<void> login(String username, String password) async {\\n // 实现在线登录逻辑,例如调用 API\\n // 如果成功,保存用户状态\\n }\\n \\n // 离线登录\\n Future<void> offlineLogin(String username) async {\\n // 实现离线登录逻辑,通常是从本地存储中获取用户信息\\n // 检查用户是否存在并有效\\n }\\n \\n // 登出\\n void logout() {\\n // 实现登出逻辑,例如清除用户状态\\n }\\n \\n // 检查用户是否已登录\\n bool isLoggedIn() {\\n // 检查用户登录状态\\n return false; // 示例,实际需要根据用户状态实现\\n }\\n }\\n
\\n例如添加、删除商品等。
\\n mixin CartMixin {\\n void addToCart(Product product) {\\n // 添加商品到购物车逻辑\\n }\\n \\n void removeFromCart(Product product) {\\n // 从购物车移除商品逻辑\\n }\\n }\\n
\\n在您的 Widget 或类中使用 mixins 来组合功能。例如,一个 ShoppingCartPage
可以同时使用 CartMixin
和 AuthenticationMixin
:
加购物车
\\n class ShoppingCartPage extends StatelessWidget with CartMixin, AuthenticationMixin {\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\\"购物车\\")),\\n body: Center(\\n child: ElevatedButton(\\n onPressed: () {\\n addToCart(product); // 使用 CartMixin 的方法\\n },\\n child: Text(\\"添加到购物车\\"),\\n ),\\n ),\\n );\\n }\\n }\\n
\\n用户登录
\\n class LoginPage extends StatelessWidget with AuthenticationMixin {\\n final TextEditingController usernameController = TextEditingController();\\n final TextEditingController passwordController = TextEditingController();\\n \\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\\"登录\\")),\\n body: Padding(\\n padding: EdgeInsets.all(16.0),\\n child: Column(\\n children: [\\n TextField(\\n controller: usernameController,\\n decoration: InputDecoration(labelText: \\"用户名\\"),\\n ),\\n TextField(\\n controller: passwordController,\\n decoration: InputDecoration(labelText: \\"密码\\"),\\n obscureText: true,\\n ),\\n ElevatedButton(\\n onPressed: () async {\\n await login(usernameController.text, passwordController.text);\\n // 登录后处理,例如导航到主页面\\n },\\n child: Text(\\"登录\\"),\\n ),\\n ElevatedButton(\\n onPressed: () async {\\n await offlineLogin(usernameController.text);\\n // 离线登录后处理,例如导航到主页面\\n },\\n child: Text(\\"离线登录\\"),\\n ),\\n ],\\n ),\\n ),\\n );\\n }\\n }\\n
\\n在 Dart 中为 AuthenticationMixin
添加约束,以确保它只能与特定的类或类型一起使用。
可以通过使用 on
关键字在 mixin 声明中进行约束。
假设 AuthenticationMixin
只能与具有 User
属性的类一起使用,这样定义 mixin:
定义用户类。
\\n class User {\\n String username;\\n String password;\\n \\n User(this.username, this.password);\\n }\\n
\\n使用 on 关键字
\\n mixin AuthenticationMixin on User {\\n void login() {\\n print(\\"Logging in user: $username\\");\\n // 登录逻辑\\n }\\n \\n void logout() {\\n print(\\"$username has logged out.\\");\\n // 登出逻辑\\n }\\n }\\n
\\n // 使用 AuthenticationMixin 的类\\n class UserService extends User with AuthenticationMixin {\\n UserService(String username, String password) : super(username, password);\\n }\\n \\n void main() {\\n var userService = UserService(\\"Alice\\", \\"password123\\");\\n userService.login();\\n userService.logout();\\n }\\n
\\n\\n\\n约束声明:
\\nmixin AuthenticationMixin on User
表示AuthenticationMixin
只能与User
类或其子类一起使用。继承与组合:
\\nUserService
继承自User
类,并使用AuthenticationMixin
。这样,UserService
拥有了User
类的属性和AuthenticationMixin
中定义的登录和登出方法。
并不是所有的功能都要放在 mixin ,比如加密、日期格式化、正则工具类 等等。
\\n我是建议把一些业务、用例可以放在 mixin 中,方便复用和梳理(基于业务)。
\\nmixin 和 工具类(Utility Class) 是两种不同的代码组织方式,各自有不同的特性和使用场景。
\\n以下是它们之间的主要区别和适用场景:
\\nwith
关键字:在类定义中使用 with
来混入多个 mixin。 mixin LoggingMixin {\\n void log(String message) {\\n print(\\"Log: $message\\");\\n }\\n }\\n \\n class MyService with LoggingMixin {\\n void performAction() {\\n log(\\"Action performed\\");\\n }\\n }\\n
\\nconst
构造函数使其不可实例化。class StringUtils {\\n static String capitalize(String input) {\\n if (input.isEmpty) return input;\\n return input[0].toUpperCase() + input.substring(1);\\n }\\n}\\n\\n// 使用工具类\\nvoid main() {\\n String capitalized = StringUtils.capitalize(\\"hello\\");\\n print(capitalized); // 输出 \\"Hello\\"\\n}\\n
\\n特性 | Mixin | 工具类 |
---|---|---|
实例化 | 不可实例化 | 通常不可实例化 |
方法访问 | 可以访问实例属性和方法 | 只能访问静态方法 |
适用场景 | 共享功能、实现多重继承 | 提供无状态的通用工具方法 |
组合方式 | 使用 with 关键字 | 通过类名直接调用静态方法 |
通过理解这两者的特点和使用场景,您可以在 Flutter 开发中更有效地组织和管理代码。
\\n通过合理设计和使用 mixins,您可以提高电商应用的可维护性和可重用性。确保 mixins 功能单一、代码组织清晰,并为其编写文档和测试,以便于团队合作和后续开发。
\\n感谢阅读本文
\\n如果有什么建议,请在评论中让我知道。我很乐意改进。
\\n© 猫哥 ducafecat.com
\\nend
","description":"Flutter 中使用 Mixin 优化逻辑与功能 视频\\n\\nyoutu.be/xyHd7gUbBo4\\n\\nwww.bilibili.com/video/BV1qD…\\n\\n前言\\n\\n原文 Flutter Mixins的规范设计与应用实例\\n\\n本文详细介绍了 Flutter 中的 mixin 概念,包括其特点、使用场景以及如何有效地在多个类之间共享代码。了解如何利用 mixin 实现功能模块化,避免代码重复,提升开发效率。\\n\\n参考\\ndocs.flutter.dev/\\ndart.dev/language/mi…\\ndart.dev/language#mi…\\ndart.dev…","guid":"https://juejin.cn/post/7475238092305530931","author":"独立开发者_猫哥","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-25T12:22:20.662Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/52c2857934b14217a474a0d1d5b16c04~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg54us56uL5byA5Y-R6ICFX-eMq-WTpQ==:q75.awebp?rk3s=f64ab15b&x-expires=1741090940&x-signature=fVXFFNZsguj6KSMKvVMRgspV18c%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"深度理解Flutter开发之Text组件","url":"https://juejin.cn/post/7475192745327231015","content":"Text
组件是 Flutter 中用于显示文本的基本组件,提供了丰富的样式和布局选项。通过合理使用其属性,可以轻松实现各种文本显示效果。
//直接显示\\n const Text(\\"这里是文本内容\\")\\n \\n //变量传入,注意考虑非空\\n Text(text ?? \\"\\")\\n
\\n属性名 | 类型 | 说明 |
---|---|---|
data | String | 必须属性,指定要显示的文本内容。 |
style | TextStyle? | 定义文本的样式,包括字体大小、颜色、字体粗细等。 |
textAlign | TextAlign? | 设置文本的对齐方式,如TextAlign.left 、TextAlign.center 等。 |
overflow | TextOverflow? | 设置文本溢出时的处理方式,如TextOverflow.ellipsis (显示省略号)。 |
maxLines | int? | 设置文本的最大行数,超出部分根据overflow 属性处理。 |
softWrap | bool | 是否自动换行,默认为true 。 |
textScaleFactor | double | 文本缩放因子,用于调整文本大小。 |
locale | Locale? | 指定文本的语言环境,用于字体选择和文本方向等。 |
textDirection | TextDirection? | 设置文本的书写方向,如TextDirection.ltr (从左到右)或TextDirection.rtl (从右到左)。 |
strutStyle | StrutStyle? | 定义行高和字体间距等。 |
textWidthBasis | TextWidthBasis? | 定义文本宽度的计算方式,如TextWidthBasis.parent 或TextWidthBasis.longestLine 。 |
textHeightBehavior | TextHeightBehavior? | 定义文本高度的计算方式,用于调整行间距等。 |
常用代码示例:
\\nText(\\n //文本内容\\n \\"Hello, Flutter\\",\\n //定义文本样式\\n style: TextStyle(fontSize: 20, color: Colors.blue, fontWeight: FontWeight.bold),\\n //定义文本对齐方式\\n textAlign: TextAlign.center,\\n //定义文本最大行数\\n maxLines: 2,\\n //定义文本溢出处理\\n overflow: TextOverflow.ellipsis,\\n)\\n
\\nmaxLines
限制文本显示的最大行数结合overflow
属性,处理文本溢出。Container
限制文本宽度结合textAlign
属性将文本对齐。属性名 | 类型 | 说明 |
---|---|---|
color | Color? | 文本颜色。 |
fontSize | double? | 字体大小。 |
fontWeight | FontWeight? | 字体粗细,如FontWeight.bold (加粗)或FontWeight.normal (正常)。 |
fontStyle | FontStyle? | 字体样式,如FontStyle.italic (斜体)或FontStyle.normal (正常)。 |
letterSpacing | double? | 字母间距,单位为逻辑像素。 |
wordSpacing | double? | 单词间距,单位为逻辑像素。 |
textBaseline | TextBaseline? | 文本基线类型,如TextBaseline.alphabetic 或TextBaseline.ideographic 。 |
height | double? | 行高,相对于字体大小的倍数(例如1.5表示行高为字体大小的1.5倍)。 |
decoration | TextDecoration? | 文本装饰,如TextDecoration.underline (下划线)、TextDecoration.lineThrough (删除线)等。 |
decorationColor | Color? | 文本装饰的颜色。 |
decorationStyle | TextDecorationStyle? | 文本装饰的样式,如TextDecorationStyle.solid (实线)、TextDecorationStyle.dashed (虚线)等。 |
decorationThickness | double? | 文本装饰的厚度。 |
fontFamily | String? | 字体家族名称,指定字体样式。 |
fontFamilyFallback | List<String>? | 字体家族的备用列表,当主字体不可用时使用。 |
fontFeatures | List<FontFeature>? | 字体特性,用于控制字体的特殊行为,如连字等。 |
shadows | List<Shadow>? | 文本的阴影列表,可以设置多个阴影效果。 |
background | Paint? | 文本背景的画笔,用于绘制背景图案。 |
常用代码示例:
\\nText(\'这里是文字\', //Text必须有值\\n style: TextStyle( //Text控制字体样式\\n backgroundColor: Colors.red, //‘设置背景颜色为红色’\\n color: Colors.black, //‘设置文字颜色为黑色’\\n fontSize: 30, //‘字体大小’\\n fontWeight: FontWeight.bold, //‘字体粗细<img src=\\"’\\" alt=\\"\\" width=\\"50%\\" />\\n ),\\n textAlign: TextAlign.center, //字体居中展示\\n)\\n
\\ncolor/backgroundColor
与foreground/background
不可同时使用,优先选color/backgroundColor
实现。height
可能破坏原有行高设计,谨慎使用值 | 描述 |
---|---|
TextAlign.left | 文本靠左对齐。这是默认值。 |
TextAlign.right | 文本靠右对齐。 |
TextAlign.center | 文本居中对齐。 |
TextAlign.justify | 文本两端对齐(类似于报纸排版)。每一行的左右两端都会对齐,通过调整单词间距或字符间距实现。最后一行可以设置为左对齐或右对齐(取决于textDirection )。 |
TextAlign.start | 文本对齐到容器的起始方向。如果textDirection 是TextDirection.ltr (从左到右),则等同于TextAlign.left ;如果是TextDirection.rtl (从右到左),则等同于TextAlign.right 。 |
TextAlign.end | 文本对齐到容器的结束方向。与TextAlign.start 相反,具体对齐方式取决于textDirection 。 |
常用代码示例:
\\nText(\'right:1111111111111111111111111111111111111111111111111111\',\\n style: TextStyle(\\n color: Colors.black,\\n fontSize: 15,\\n fontWeight: FontWeight.bold,\\n ),\\n textAlign: TextAlign.right, //控制文本对齐方式\\n),\\n
\\nTextAlign.justify
可能不会对最后一行进行两端对齐。如果需要对最后一行也进行两端对齐,可以使用 TextPainter
或其他自定义逻辑。TextAlign.start
或 TextAlign.end
,以确保对齐方式符合语言习惯。Overflow 属性值 | 描述 |
---|---|
TextOverflow.ellipsis | 当文本溢出时,在末尾显示省略号(...)。 |
TextOverflow.clip | 直接裁剪文本,不显示省略号。 |
TextOverflow.fade | 文本末尾使用渐变效果来裁剪文本,适用于单行文本。 |
TextOverflow.visible | 允许文本溢出其容器,不进行任何处理。 |
常用代码示例:
\\nText(\\n \'这是一段很长的文本,我们希望通过 maxLines 属性限制它的显示行数,并且通过 overflow 属性定义超出部分的显示方式。如果文本内容超出了指定的行数,我们希望它以省略号的形式显示,而不是溢出容器。\',\\n maxLines: 3, // 限制文本显示的最大行数\\n overflow: TextOverflow.ellipsis, // 超出部分以省略号显示\\n style: TextStyle(fontSize: 16),\\n),\\n\\n
\\nRichText
通常用于需要在同一个文本块中显示不同样式文本的场景,例如,显示带有强调的单词或不同颜色的链接。RichText
的构造函数接受一个 TextSpan
对象作为参数,这个 TextSpan
可以包含多个子 TextSpan
,每个都可以有自己的样式。
常用代码示例:
\\nRichText(\\n text: const TextSpan(\\n children: <TextSpan>[\\n TextSpan(\\n text: \'This is a \',\\n style: TextStyle(color: Colors.black, fontSize: 20),\\n ),\\n TextSpan(\\n text: \'special \',\\n style: TextStyle(color: Colors.red, fontSize: 20, fontWeight: FontWeight.bold),\\n ),\\n TextSpan(\\n text: \'text with \',\\n style: TextStyle(color: Colors.blue, fontSize: 20),\\n ),\\n TextSpan(\\n text: \'rich formatting.\',\\n style: TextStyle(backgroundColor: Colors.black, fontSize: 20),\\n ),\\n ],\\n ),\\n);\\n
\\n理解 Flutter 中 Text
组件的核心在于掌握信息展示的设计理念:
我们应该构建一个分层次的认知体系:
\\nText
组件的基本属性。通过“标准化配置—场景定制—性能优化”的逐步实践,可以将 Text
组件从简单的信息展示工具转变为提升用户体验的关键因素。
在Flutter
的布局体系中,Stack
如同一个魔法容器,允许开发者以自由而精确的方式叠加视图元素。这种能力使得它成为实现复杂界面效果(如悬浮按钮
、视差滚动
、自定义进度条
等)的核心工具。但自由往往伴随着责任 —— 错误使用Stack
可能导致布局失控
、性能下降
甚至渲染异常
。
本文将从基础属性
解析到源码实现
,从设计哲学到实战经验,以系统化视角全面剖析Stack
布局。无论你是刚接触Flutter
的新手,还是寻求进阶突破的中级开发者,都将在这篇深度指南中找到关键认知提升点。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\nStack(\\n alignment: AlignmentDirectional.topStart,\\n textDirection: TextDirection.ltr,\\n fit: StackFit.loose,\\n clipBehavior: Clip.hardEdge,\\n children: [...],\\n)\\n
\\nalignment
1、本质作用:
\\nPositioned
组件未显式指定的定位参数。2、坐标系详解:
\\nAlignment
:笛卡尔坐标系(中心点(0,0)
)。Alignment(-1, -1) → 左上角 \\nAlignment(1, 1) → 右下角\\n
\\nAlignmentDirectional
:考虑文本方向(RTL/LTR
)。AlignmentDirectional.topStart → LTR时为左上角,RTL时为右上角\\n
\\n布局计算公式:
\\n\\n\\n子元素位置 =
\\n父容器尺寸
×alignment
系数 +偏移量
例:父容器宽
\\n200
,alignment.x=0.5
→ 横向偏移100px
4、黄金法则:
\\nPositioned
定位时,alignment
失效。Positioned
的left/right
等参数共用时可能产生意外偏移。5、基本用法
\\nWidget testStack1() {\\n return Column(\\n children: [\\n Row(\\n children: [\\n buildStack(Alignment.topLeft),\\n SizedBox(width: 5),\\n buildStack(Alignment.topCenter),\\n SizedBox(width: 5),\\n buildStack(Alignment.topRight),\\n ],\\n ),\\n SizedBox(height: 5),\\n Row(\\n children: [\\n buildStack(Alignment.centerLeft),\\n SizedBox(width: 5),\\n buildStack(Alignment.center),\\n SizedBox(width: 5),\\n buildStack(Alignment.centerRight),\\n ],\\n ),\\n SizedBox(height: 5),\\n Row(\\n children: [\\n buildStack(Alignment.bottomLeft),\\n SizedBox(width: 5),\\n buildStack(Alignment.bottomCenter),\\n SizedBox(width: 5),\\n buildStack(Alignment.bottomRight),\\n ],\\n )\\n ],\\n );\\n}\\n\\nWidget buildStack(Alignment alignment) {\\n return Stack(\\n alignment: alignment,\\n children: <Widget>[\\n Container(\\n width: 120,\\n height: 120,\\n color: Colors.red,\\n ),\\n Container(\\n width: 50,\\n height: 50,\\n color: Colors.green,\\n ),\\n ],\\n );\\n}\\n
\\n效果图:
\\ntextDirection
textDirection: TextDirection.ltr // 默认值\\n
\\n1、深层影响:
\\nstart/end
系参数的方向解析(如AlignmentDirectional.topStart
)。Positioned
的left/right
在RTL语言中的映射关系。2、典型场景
\\nRTL
布局)。3、动态切换技巧
\\nBuilder(\\n builder: (context) {\\n final dir = Directionality.of(context);\\n return Stack(\\n textDirection: dir == TextDirection.rtl ? TextDirection.ltr : null,\\n // ...\\n );\\n }\\n)\\n
\\n4、常见陷阱
\\ntextDirection
时依赖系统默认值导致布局错乱。Directionality
组件引发方向冲突。fit
fit: StackFit.loose // 默认值\\n
\\n1、模式对比:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n模式 | 约束条件 | 典型场景 |
---|---|---|
StackFit.loose | 子元素最大尺寸不超过父容器 | 需要自适应大小的元素(如图标) |
StackFit.expand | 强制子元素填满父容器 | 全屏背景/遮罩层 |
StackFit.passthrough | 继承父级约束(需自定义RenderObject ) | 高级自定义布局 |
2、尺寸计算流程:
\\nStack
。Stack
根据fit
模式调整自身尺寸。3、边界条件处理:
\\nwidth: 100
)时,fit
参数可能失效。expand
模式与Positioned
定位参数冲突时的优先级规则。4、基本使用
\\nStack(\\n fit: StackFit.expand,\\n children: <Widget>[\\n Container(\\n color: Colors.red,\\n ),\\n Container(\\n width: 200,\\n height: 200,\\n color: Colors.green,\\n ),\\n ],\\n)\\n
\\n效果图:
\\nclipBehavior
clipBehavior: Clip.hardEdge // 默认值\\n
\\n1、四种模式对比:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n模式 | 性能消耗 | 视觉效果 | 适用场景 |
---|---|---|---|
Clip.none | 最低 | 允许子元素溢出 | 需要极致性能的静态布局 |
Clip.hardEdge | 低 | 锯齿状裁剪边缘 | 多数常规场景(默认选择) |
Clip.antiAlias | 中 | 平滑边缘但有半透明像素 | 需要美观裁剪的动画元素 |
Clip.antiAliasWithSaveLayer | 高 | 完美抗锯齿但内存消耗大 | 复杂叠加的透明元素 |
2、性能优化策略
\\nhardEdge
,仅在必要时升级裁剪等级。antiAliasWithSaveLayer
。RepaintBoundary
隔离高消耗的裁剪区域。3、内存泄漏案例\\nStack(\\nclipBehavior: Clip.none,\\nchildren: [\\nPositioned(\\nleft: -1000, // 超出屏幕范围的元素\\nchild: Image.asset(\'assets/images/ic_launcher.png\'),\\n),\\n],\\n)
\\nPositioned
组件深度解析Positioned({\\n double? left, \\n double? top,\\n double? right,\\n double? bottom,\\n double? width,\\n double? height,\\n})\\n
\\nStack buildStack3() {\\n return Stack(\\n children: <Widget>[\\n Positioned(\\n top: 50,\\n left: 50,\\n child: Container(\\n width: 150,\\n height: 150,\\n color: Colors.red,\\n ),\\n ),\\n Positioned(\\n top: 100,\\n left: 100,\\n child: Container(\\n width: 150,\\n height: 150,\\n color: Colors.green,\\n ),\\n ),\\n Positioned(\\n top: 150,\\n left: 150,\\n child: Container(\\n width: 150,\\n height: 150,\\n color: Colors.blue,\\n ),\\n ),\\n ],\\n );\\n}\\n
\\n效果图:
\\n横向布局:
\\nleft
和right
→ width = 父宽度 - left - right
。left
+ width
→ right
自动计算。left/right
为准,忽略width
。纵向布局:
\\n逻辑同上,替换为top/bottom/height
。
公式推导:\\n// 横向计算逻辑\\nif (left != null && right != null) {\\nwidth = parentWidth - left - right;\\n} else if (left != null && width != null) {\\nright = parentWidth - left - width;\\n}\\n// 纵向同理
\\nbool _isMoved = false;\\n\\nStack(\\n children: <Widget>[\\n AnimatedPositioned(\\n duration: Duration(seconds: 2),\\n left: _isMoved ? 200 : 50,\\n top: _isMoved ? 200 : 50,\\n child: GestureDetector(\\n onTap: () {\\n setState(() {\\n _isMoved = !_isMoved;\\n });\\n },\\n child: Container(\\n width: 100,\\n height: 100,\\n color: Colors.blue,\\n ),\\n ),\\n ),\\n ],\\n),\\n
\\nStack buildStack4() {\\n return Stack(\\n children: <Widget>[\\n // 背景图片\\n Positioned.fill(\\n child: Image.network(\\n url,\\n fit: BoxFit.cover,\\n ),\\n ),\\n // 中心文本\\n Center(\\n child: Text(\\n \'Hello Flutter!\',\\n style: TextStyle(\\n fontSize: 36,\\n color: Colors.white,\\n fontWeight: FontWeight.bold,\\n ),\\n ),\\n ),\\n // 右下角悬浮按钮\\n Positioned(\\n bottom: 30,\\n right: 30,\\n child: FloatingActionButton(\\n onPressed: () {},\\n child: Icon(Icons.add),\\n ),\\n ),\\n ],\\n );\\n}\\n
\\n效果图
\\nStack(\\n children: [\\n RepaintBoundary( // 隔离高频变化的元素\\n child: AnimatedLogo(),\\n ),\\n Positioned(\\n child: const StaticText(), // 无需重绘的静态元素\\n ),\\n ],\\n)\\n
\\n优化指标:
\\ndebugDumpRenderTree()
分析。Performance
工具监测。DevTools
内存分析器对比。Positioned(\\n child: PhysicalModel(\\n elevation: 10,\\n color: Colors.white,\\n child: BackdropFilter( // 使用硬件加速的滤镜\\n filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),\\n child: Container(...),\\n ),\\n ),\\n)\\n
\\n关键技术:
\\nRenderObject.alwaysNeedsCompositing
。saveLayer
的使用场景。ImageCache
管理图片资源。Stack(\\n clipBehavior: Clip.hardEdge, // 严格控制溢出\\n children: [\\n Visibility( // 替代Offstage避免内存驻留\\n visible: _isVisible,\\n child: HeavyWidget(),\\n ),\\n Positioned.fill(\\n child: ListView.builder( // 列表项复用\\n itemBuilder: (context, index) => Item(index),\\n ),\\n ),\\n ],\\n)\\n
\\n优化手段:
\\nPositioned
组件。ScrollNotification
动态加载。MemoryAllocations
插件。// 简化的performLayout伪代码\\nvoid performLayout() {\\n size = constraints.constrain(computeSize());\\n \\n for (var child in children) {\\n if (child is Positioned) {\\n // 计算定位参数\\n child.layout(positionedConstraints);\\n positionChild(child);\\n } else {\\n // 应用alignment\\n child.layout(unpositionedConstraints);\\n alignChild(child);\\n }\\n }\\n}\\n
\\n关键方法:
\\ncomputeDryLayout()
:预测布局尺寸。applyPosition()
:处理定位偏移。paintStack()
:处理绘制顺序。enum StackLayoutState {\\n Initial,\\n Sizing,\\n Positioning,\\n Completed,\\n}\\n
\\n状态转换流程:
\\n// 源码中的性能优化点\\nif (childParentData.isPositioned) {\\n // 使用快速路径计算\\n child.layout(constraints.loosen(), parentUsesSize: true);\\n} else {\\n // 完整约束计算\\n child.layout(constraints, parentUsesSize: true);\\n}\\n
\\n优化策略:
\\nmeasure pass
次数。// 典型组合模式示例\\nStack(\\n children: [\\n Positioned(child: BaseWidget()),\\n Align(alignment: Alignment.center),\\n Transform(transform: Matrix4.rotationZ(0.1)),\\n ],\\n)\\n
\\n设计原则:
\\nWidget
只做一件事。组合扩展功能
。避免隐式行为
。// 状态驱动布局示例\\nStack(\\n children: [\\n if (_showBackground) BackgroundWidget(),\\n PrimaryContent(),\\n if (_hasError) ErrorOverlay(),\\n ],\\n)\\n
\\n核心优势:
\\nLayoutBuilder(\\n builder: (context, constraints) {\\n if (constraints.maxWidth > 600) {\\n return DesktopLayout();\\n } else {\\n return MobileLayout();\\n }\\n },\\n)\\n
\\n适配方案:
\\nOrientationBuilder
动态调整。AnimatedBuilder(\\n animation: _animationController,\\n builder: (context, child) {\\n return Stack(\\n children: [\\n Positioned(\\n left: _animation.value * 100,\\n child: child!,\\n ),\\n ],\\n );\\n },\\n child: const Icon(Icons.star),\\n)\\n
\\n关键要点:
\\nAnimatedWidget
替代setState
。child
参数优化重建。Animation Curve
。class SafePositioned extends StatefulWidget {\\n @override\\n _SafePositionedState createState() => _SafePositionedState();\\n}\\n\\nclass _SafePositionedState extends State<SafePositioned> {\\n @override\\n void dispose() {\\n _controller?.dispose(); // 必须手动释放资源\\n super.dispose();\\n }\\n}\\n
\\n防护策略:
\\nflutter_bloc
等状态管理库。DevTools
内存检测。通过本文的深度探索,我们不仅掌握了Stack
的基础用法,更建立起从源码实现到设计哲学的全维度认知体系。优秀的Flutter
开发者应具备:
Widget
树、RenderObject
、Layer
三个层面分析问题。Stack
布局的掌握程度,往往折射出一个Flutter
开发者的综合能力水平。希望本文能成为你通往高阶开发的阶梯,在复杂UI
的实现中游刃有余,在性能优化的战场上所向披靡。
\\n","description":"前言 在Flutter的布局体系中,Stack如同一个魔法容器,允许开发者以自由而精确的方式叠加视图元素。这种能力使得它成为实现复杂界面效果(如悬浮按钮、视差滚动、自定义进度条等)的核心工具。但自由往往伴随着责任 —— 错误使用Stack可能导致布局失控、性能下降甚至渲染异常。\\n\\n本文将从基础属性解析到源码实现,从设计哲学到实战经验,以系统化视角全面剖析Stack布局。无论你是刚接触Flutter的新手,还是寻求进阶突破的中级开发者,都将在这篇深度指南中找到关键认知提升点。\\n\\n操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。\\n\\n一、基础认知\\n1.1…","guid":"https://juejin.cn/post/7475172600936169508","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-25T07:07:25.485Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7491fd1f98904519acf937081b30dbc5~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1741072045&x-signature=4Ya0Sw3TBkreuVSGo2xN7hYWObk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/dafc0750aaa74d93add3089bbb921344~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1741072045&x-signature=w4rwGkqm%2B32uTb3IEnGh2tyz58o%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ba014d90028f421f99987414fc2fecec~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1741072045&x-signature=lpRjUk9jmeu2rTqOoEvracLl5dU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a68df3f2012e4bbcb8f013780ad70e01~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1741072045&x-signature=4dNcez3LHeGsTKsL0r11ozb9qBA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2d8435b57c5847c5978cb5acaa21ec70~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1741072045&x-signature=ZELgIVEfgvnGm7VNipNuq%2BO0Z1o%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"【Flutter入门】3.深入理解Flutter的核心概念:架构、渲染与生命周期","url":"https://juejin.cn/post/7475001582797062180","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
\\n\\n想要成为一名出色的Flutter开发者,就像建造一座精美的高楼大厦,需要深入理解其架构基础、渲染工艺和生命周期管理。
\\n
想象Flutter的架构就像一座精美设计的三层宝塔,每一层都有其独特的职责和魅力。通过这种分层架构设计,Flutter成功实现了高性能的跨平台开发能力。让我们来看看这座宝塔的独特之处:
\\nFlutter的分层架构不仅仅是静态的结构,更像是一个充满活力的有机整体,层与层之间的数据流动就像血液循环一样,保持整个系统的生机活力。从上到下分为三层,每一层都通过明确的接口与其他层进行优雅的交互:
\\nFramework 层就像一个精心打造的乐高积木世界,使用 Dart 语言实现,为开发者提供了丰富的开发工具。它的分层设计就像一个精妙的俄罗斯套娃,从外到内依次为:
\\nMaterial/Cupertino:你的设计语言守护者
\\nWidgets:UI积木的基石
\\nRendering:布局的魔法师
\\nAnimation:动画的编舞者
\\nPainting:绘制的艺术家
\\nFoundation:坚实的地基
\\nEngine 层就像一座现代化的引擎室,为整个应用提供强劲动力。这一层使用 C++ 实现,负责最核心的渲染工作:
\\nSkia:绘图引擎的心脏
\\nDart Runtime:运行时的指挥官
\\nPlatform Channels:平台沟通的桥梁
\\nEmbedder 层就像一位熟练的工匠,负责将Flutter的内核与各个平台完美适配:
\\n平台适配:因地制宜的匠人
\\n渲染适配:画布的搭建者
\\n事件处理:信使的传递者
\\n在 Flutter 中,各层之间的通信就像一场精心编排的芭蕾舞,每一个动作都优雅而准确:
\\n// 层间通信示例\\nclass MyWidget extends StatefulWidget {\\n @override\\n _MyWidgetState createState() => _MyWidgetState();\\n}\\n\\nclass _MyWidgetState extends State<MyWidget> {\\n // Framework层到Engine层的通信\\n Future<void> loadImage() async {\\n // 通过Platform Channel调用原生API\\n final ByteData imageData = await rootBundle.load(\'assets/image.png\');\\n // Engine层处理图片加载\\n final ui.Image image = await decodeImageFromList(imageData.buffer.asUint8List());\\n setState(() {\\n // 更新UI\\n });\\n }\\n\\n // Engine层到Framework层的通信\\n @override\\n void initState() {\\n super.initState();\\n // 监听平台消息\\n SystemChannels.lifecycle.setMessageHandler((msg) async {\\n if (msg == AppLifecycleState.resumed.toString()) {\\n // 处理应用恢复事件\\n }\\n return null;\\n });\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Container();\\n }\\n}\\n
\\n让我们通过一个综合示例,看看Flutter各层是如何协同工作的,就像一场精彩的交响乐演出:
\\nclass ComplexWidget extends StatefulWidget {\\n @override\\n _ComplexWidgetState createState() => _ComplexWidgetState();\\n}\\n\\nclass _ComplexWidgetState extends State<ComplexWidget> with SingleTickerProviderStateMixin {\\n late AnimationController _controller;\\n late Animation<double> _animation;\\n final MethodChannel _channel = MethodChannel(\'example_channel\');\\n\\n @override\\n void initState() {\\n super.initState();\\n \\n // Animation层:创建动画指挥家\\n _controller = AnimationController(\\n duration: Duration(seconds: 1),\\n vsync: this,\\n );\\n \\n // Animation层:设计动画旋律\\n _animation = CurvedAnimation(\\n parent: _controller,\\n curve: Curves.easeInOut,\\n );\\n\\n // Engine层:搭建平台桥梁\\n _setupPlatformChannel();\\n }\\n\\n Future<void> _setupPlatformChannel() async {\\n try {\\n // Engine层:与原生世界对话\\n final result = await _channel.invokeMethod(\'getPlatformData\');\\n setState(() {\\n // Framework层:刷新UI画布\\n });\\n } catch (e) {\\n print(\'Platform channel error: $e\');\\n }\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n // Framework层:编织UI交响乐\\n return AnimatedBuilder(\\n animation: _animation,\\n builder: (context, child) {\\n return Transform.scale(\\n scale: _animation.value,\\n child: CustomPaint(\\n // Painting层:绘制艺术\\n painter: MyCustomPainter(),\\n child: GestureDetector(\\n onTap: () {\\n // Animation层:开始动画表演\\n _controller.forward(from: 0.0);\\n // Engine层:与平台互动\\n _channel.invokeMethod(\'onUserInteraction\');\\n },\\n child: Container(\\n // Rendering层:布局编排\\n width: 200,\\n height: 200,\\n child: Center(\\n child: Text(\'Flutter架构示例\'),\\n ),\\n ),\\n ),\\n ),\\n );\\n },\\n );\\n }\\n\\n @override\\n void dispose() {\\n _controller.dispose();\\n super.dispose();\\n }\\n}\\n\\n// Painting层:自定义画笔艺术\\nclass MyCustomPainter extends CustomPainter {\\n @override\\n void paint(Canvas canvas, Size size) {\\n final paint = Paint()\\n ..color = Colors.blue\\n ..style = PaintingStyle.stroke\\n ..strokeWidth = 2.0;\\n\\n canvas.drawRect(\\n Rect.fromLTWH(0, 0, size.width, size.height),\\n paint,\\n );\\n }\\n\\n @override\\n bool shouldRepaint(CustomPainter oldDelegate) => false;\\n}\\n
\\n这个交响乐般的示例展示了:
\\nFramework层交互:就像乐团的总谱
\\nEngine层通信:如同乐团与观众的互动
\\n渲染层协作:犹如舞台灯光和布景
\\nFlutter 的渲染过程就像一场精心编排的四重奏,每个步骤都至关重要:
\\n构建(Build):就像搭建舞台布景
\\n布局(Layout):精确定位每个演员的位置
\\n绘制(Paint):为舞台增添色彩和光影
\\n合成(Composite):将所有元素和谐统一
\\n让我们通过一个具体的例子来理解这个过程:
\\nclass RenderingExample extends StatefulWidget {\\n @override\\n _RenderingExampleState createState() => _RenderingExampleState();\\n}\\n\\nclass _RenderingExampleState extends State<RenderingExample> {\\n @override\\n Widget build(BuildContext context) {\\n // 构建阶段:创建Widget树\\n return Container(\\n // 布局阶段:设置约束\\n constraints: BoxConstraints.expand(),\\n child: CustomPaint(\\n // 绘制阶段:自定义绘制\\n painter: MyCustomPainter(),\\n child: Center(\\n child: Text(\'渲染示例\'),\\n ),\\n ),\\n );\\n }\\n}\\n\\nclass MyCustomPainter extends CustomPainter {\\n @override\\n void paint(Canvas canvas, Size size) {\\n // 绘制阶段:执行具体的绘制操作\\n final paint = Paint()\\n ..color = Colors.blue\\n ..style = PaintingStyle.stroke\\n ..strokeWidth = 2.0;\\n\\n // 绘制一个矩形边框\\n canvas.drawRect(\\n Rect.fromLTWH(0, 0, size.width, size.height),\\n paint,\\n );\\n\\n // 绘制一些装饰性的圆点\\n paint.style = PaintingStyle.fill;\\n canvas.drawCircle(Offset(size.width / 4, size.height / 4), 10, paint);\\n canvas.drawCircle(Offset(size.width * 3 / 4, size.height / 4), 10, paint);\\n canvas.drawCircle(Offset(size.width / 2, size.height * 3 / 4), 10, paint);\\n }\\n\\n @override\\n bool shouldRepaint(CustomPainter oldDelegate) => false;\\n}\\n
\\n在这个例子中:
\\n构建阶段:
\\n布局阶段:
\\n绘制阶段:
\\n合成阶段:
\\nFlutter中的Widget生命周期就像一场精心编排的舞台剧,每个阶段都有其特定的角色和任务。让我们通过一个完整的示例来详细了解这个过程:
\\nclass LifecycleDemoWidget extends StatefulWidget {\\n @override\\n _LifecycleDemoWidgetState createState() => _LifecycleDemoWidgetState();\\n}\\n\\nclass _LifecycleDemoWidgetState extends State<LifecycleDemoWidget> with WidgetsBindingObserver {\\n @override\\n void initState() {\\n super.initState();\\n // 注册应用生命周期观察者\\n WidgetsBinding.instance.addObserver(this);\\n print(\'1. initState: Widget初始化\');\\n // 适合进行:\\n // - 初始化数据\\n // - 订阅事件通知\\n // - 创建动画控制器\\n }\\n\\n @override\\n void didChangeDependencies() {\\n super.didChangeDependencies();\\n print(\'2. didChangeDependencies: 依赖更新\');\\n // 适合进行:\\n // - 获取InheritedWidget数据\\n // - 处理主题或语言变化\\n // - 注意:此方法可能被多次调用\\n }\\n\\n @override\\n void didUpdateWidget(LifecycleDemoWidget oldWidget) {\\n super.didUpdateWidget(oldWidget);\\n print(\'3. didUpdateWidget: Widget更新\');\\n // 适合进行:\\n // - 比较新旧widget的属性变化\\n // - 重新初始化某些资源\\n // - 注意:保持状态的连续性\\n }\\n\\n @override\\n void deactivate() {\\n print(\'4. deactivate: Widget暂时移除\');\\n super.deactivate();\\n // 适合进行:\\n // - 暂停动画\\n // - 保存临时状态\\n // 注意:widget可能被重新插入到其他位置\\n }\\n\\n @override\\n void dispose() {\\n print(\'5. dispose: Widget永久移除\');\\n WidgetsBinding.instance.removeObserver(this);\\n super.dispose();\\n // 适合进行:\\n // - 取消事件订阅\\n // - 释放资源(动画控制器等)\\n // - 关闭数据流\\n }\\n\\n @override\\n void didChangeAppLifecycleState(AppLifecycleState state) {\\n // 监听应用生命周期状态变化\\n switch (state) {\\n case AppLifecycleState.resumed:\\n print(\'应用进入前台\');\\n // 恢复数据刷新、动画等\\n break;\\n case AppLifecycleState.inactive:\\n print(\'应用暂时失去焦点\');\\n // 暂停部分UI更新\\n break;\\n case AppLifecycleState.paused:\\n print(\'应用进入后台\');\\n // 保存重要数据、暂停耗资源的操作\\n break;\\n case AppLifecycleState.detached:\\n print(\'应用被挂起\');\\n // 执行关键数据保存\\n break;\\n }\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n print(\'build: 构建Widget\');\\n // build方法可能被多次调用,应该:\\n // - 保持方法纯粹,避免副作用\\n // - 优化性能,避免不必要的重建\\n return Container(\\n child: Text(\'生命周期示例\'),\\n );\\n }\\n}\\n
\\nclass BestPracticeWidget extends StatefulWidget {\\n @override\\n _BestPracticeWidgetState createState() => _BestPracticeWidgetState();\\n}\\n\\nclass _BestPracticeWidgetState extends State<BestPracticeWidget> {\\n late StreamController _controller;\\n late Future<void> _initFuture;\\n\\n @override\\n void initState() {\\n super.initState();\\n // 1. 使用late关键字延迟初始化\\n _controller = StreamController();\\n \\n // 2. 异步初始化使用Future\\n _initFuture = _initializeData();\\n }\\n\\n Future<void> _initializeData() async {\\n // 异步初始化逻辑\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return FutureBuilder(\\n future: _initFuture,\\n builder: (context, snapshot) {\\n if (snapshot.connectionState == ConnectionState.waiting) {\\n return LoadingWidget();\\n }\\n return YourWidget();\\n },\\n );\\n }\\n\\n @override\\n void dispose() {\\n // 3. 确保资源释放\\n _controller.close();\\n super.dispose();\\n }\\n}\\n
\\nclass StateManagementExample extends StatefulWidget {\\n @override\\n _StateManagementExampleState createState() => _StateManagementExampleState();\\n}\\n\\nclass _StateManagementExampleState extends State<StateManagementExample> {\\n late final ValueNotifier<int> _counter;\\n\\n @override\\n void initState() {\\n super.initState();\\n _counter = ValueNotifier<int>(0);\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return ValueListenableBuilder<int>(\\n valueListenable: _counter,\\n builder: (context, value, child) {\\n return Text(\'计数: $value\');\\n },\\n );\\n }\\n\\n @override\\n void dispose() {\\n _counter.dispose();\\n super.dispose();\\n }\\n}\\n
\\nmixin LifecycleLoggerMixin<T extends StatefulWidget> on State<T> {\\n @override\\n void initState() {\\n super.initState();\\n print(\'${widget.runtimeType} - initState\');\\n }\\n\\n @override\\n void dispose() {\\n print(\'${widget.runtimeType} - dispose\');\\n super.dispose();\\n }\\n}\\n\\n// 使用混入\\nclass DebuggableWidget extends StatefulWidget {\\n @override\\n _DebuggableWidgetState createState() => _DebuggableWidgetState();\\n}\\n\\nclass _DebuggableWidgetState extends State<DebuggableWidget> with LifecycleLoggerMixin {\\n @override\\n Widget build(BuildContext context) {\\n return Container();\\n }\\n}\\n
","description":"想要成为一名出色的Flutter开发者,就像建造一座精美的高楼大厦,需要深入理解其架构基础、渲染工艺和生命周期管理。 Flutter 架构原理:精心设计的层级大厦\\n整体架构概览:Flutter的三层宝塔\\n\\n想象Flutter的架构就像一座精美设计的三层宝塔,每一层都有其独特的职责和魅力。通过这种分层架构设计,Flutter成功实现了高性能的跨平台开发能力。让我们来看看这座宝塔的独特之处:\\n\\n分层设计:就像建筑的地基-主体-装饰,Flutter从上到下分为三层,每层各司其职,相互配合\\n跨平台一致性:通过自绘引擎,就像一位技艺精湛的画师…","guid":"https://juejin.cn/post/7475001582797062180","author":"西辰Knight","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-25T05:56:03.589Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c3b767066d514fc8bcff2f158d9397de~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6KW_6L6wS25pZ2h0:q75.awebp?rk3s=f64ab15b&x-expires=1741067763&x-signature=vuV%2Bj4iym87RuNlqEhcZ7tG9%2BuA%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 混合架构方案:多引擎","url":"https://juejin.cn/post/7474995524337500212","content":"在 Flutter 中,单引擎和多引擎是两种不同的应用开发模式,它们在性能、资源管理和使用场景等方面存在差异。
\\n单引擎模式是指在一个 Flutter 应用中只使用一个 Flutter 引擎实例。这个引擎负责处理所有 Flutter 界面的渲染、动画、输入事件等。大多数简单的 Flutter 应用默认采用单引擎模式。
\\n优点
\\n资源占用少:由于只使用一个引擎实例,内存和 CPU 等系统资源的占用相对较少,有助于提高应用的性能和电池续航能力。
\\n状态管理简单:整个应用的状态管理相对简单,因为所有的 Flutter 界面都在同一个引擎上下文中运行,数据共享和状态同步更加容易。
\\n启动速度快:单引擎模式下,应用启动时只需初始化一个引擎实例,启动速度相对较快。
\\n缺点
\\n灵活性差:如果应用需要在不同的界面或模块之间进行复杂的隔离和交互,单引擎模式可能无法满足需求。
\\n稳定性受影响:一旦引擎出现问题,整个应用可能会受到影响。
\\n多引擎模式是指在一个应用中使用多个 Flutter 引擎实例。每个引擎实例可以独立运行,负责不同的 Flutter 界面或模块。多引擎模式通常用于复杂的应用场景,如在一个原生应用中嵌入多个独立的 Flutter 页面。
\\n优点
\\n隔离性好:不同的引擎实例之间相互隔离,一个引擎出现问题不会影响其他引擎的运行,提高了应用的稳定性。
\\n灵活性高:可以根据需要独立控制每个引擎的生命周期、状态和资源,实现更复杂的界面和交互逻辑。
\\n与原生集成更方便:在原生应用中嵌入多个独立的 Flutter 页面时,多引擎模式可以更好地与原生代码进行集成。
\\n缺点
\\n资源占用多:每个引擎实例都需要占用一定的系统资源,多个引擎实例会增加内存和 CPU 的负担,可能影响应用的性能和电池续航能力。
\\n状态管理复杂:不同引擎实例之间的数据共享和状态同步相对复杂,需要额外的机制来实现。
\\n使用 FlutterEngineGroup
(推荐)或手动创建多个 FlutterEngine
实例。
// 使用 FlutterEngineGroup 创建引擎\\nval engineGroup = FlutterEngineGroup(context)\\nval engine1 = engineGroup.createAndRunEngine(\\n context, DartExecutor.DartEntrypoint.createDefault()\\n)\\nval engine2 = engineGroup.createAndRunEngine(\\n context, DartExecutor.DartEntrypoint.createDefault()\\n)\\n\\n// 将引擎绑定到 FlutterView\\nval flutterView1 = FlutterView(context).apply {\\n attachToFlutterEngine(engine1)\\n}\\nval flutterView2 = FlutterView(context).apply {\\n attachToFlutterEngine(engine2)\\n}\\n
\\n使用 FlutterEngineGroup
(iOS 13+)或手动创建多个 FlutterEngine
。
// 创建引擎组\\nlet engineGroup = FlutterEngineGroup(name: \\"group\\", project: nil)\\n\\n// 生成引擎实例\\nlet engine1 = engineGroup.makeEngine(withEntrypoint: \\"main\\", libraryURI: nil)\\nlet engine2 = engineGroup.makeEngine(withEntrypoint: \\"main\\", libraryURI: nil)\\n\\n// 绑定到 FlutterViewController\\nlet flutterVC1 = FlutterViewController(engine: engine1, nibName: nil, bundle: nil)\\nlet flutterVC2 = FlutterViewController(engine: engine2, nibName: nil, bundle: nil)\\n
\\n每个引擎默认独立运行,需确保入口点(main()
)支持多实例:
void main() {\\n runApp(MyApp()); // 确保无全局静态状态冲突\\n}\\n
\\nGeneratedPluginRegistrant
)。若插件依赖原生上下文(如相机),需确保多引擎下的兼容性。destroy()
释放引擎MethodChannel
或 EventChannel
)。class FlutterEnginePool {\\n // MARK: - 单例\\n static let shared = FlutterEnginePool()\\n private init() {}\\n \\n // MARK: - 核心属性\\n private let engineGroup = FlutterEngineGroup(name: \\"flutter_engine_pool\\", project: nil)\\n private var activeEngines: [String: FlutterEngine] = [:] // 使用中的引擎 [路由标识: 引擎]\\n private var idleEngines: [String: FlutterEngine] = [:] // 闲置引擎池\\n private let queue = DispatchQueue(label: \\"com.flutter.engine.pool.lock\\") // 线程安全队列\\n \\n // MARK: - 配置参数\\n private let maxIdleCount = 3 // 最大闲置引擎数\\n private let maxIdleTime: TimeInterval = 300 // 闲置超时时间(秒)\\n private var timer: Timer? // 闲置检测定时器\\n \\n // MARK: - 公开方法\\n /// 获取引擎(按路由标识)\\n func getEngine(for route: String) -> FlutterEngine {\\n return queue.sync {\\n // 1. 查找可用闲置引擎\\n if let engine = idleEngines.removeValue(forKey: route) {\\n activeEngines[route] = engine\\n return engine\\n }\\n \\n // 2. 创建新引擎\\n let engine = engineGroup.makeEngine(\\n withEntrypoint: \\"main\\",\\n libraryURI: nil,\\n initialRoute: route\\n )\\n GeneratedPluginRegistrant.register(with: engine)\\n activeEngines[route] = engine\\n \\n // 3. 启动闲置检测\\n startIdleCheckTimer()\\n \\n return engine\\n }\\n }\\n \\n /// 归还引擎到池中\\n func recycleEngine(for route: String) {\\n queue.async {\\n guard let engine = self.activeEngines.removeValue(forKey: route) else { return }\\n \\n // 1. 超过最大闲置数时销毁最旧的引擎\\n if self.idleEngines.count >= self.maxIdleCount, let firstKey = self.idleEngines.keys.first {\\n self.idleEngines.removeValue(forKey: firstKey)?.destroyContext()\\n }\\n \\n // 2. 记录闲置时间戳(用于LRU回收)\\n let metadata = [\\"recycleTime\\": Date()]\\n engine.setMetadata(metadata)\\n \\n // 3. 存入闲置池\\n self.idleEngines[route] = engine\\n }\\n }\\n \\n /// 强制销毁所有引擎(用于内存警告)\\n func purgeAllEngines() {\\n queue.async {\\n self.activeEngines.values.forEach { $0.destroyContext() }\\n self.idleEngines.values.forEach { $0.destroyContext() }\\n self.activeEngines.removeAll()\\n self.idleEngines.removeAll()\\n }\\n }\\n \\n // MARK: - 私有方法\\n private func startIdleCheckTimer() {\\n guard timer == nil else { return }\\n \\n timer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { [weak self] _ in\\n self?.checkIdleEngines()\\n }\\n }\\n \\n private func checkIdleEngines() {\\n queue.async {\\n let now = Date()\\n \\n // 清理超时引擎\\n self.idleEngines = self.idleEngines.filter { route, engine in\\n guard let metadata = engine.metadata as? [String: Any],\\n let recycleTime = metadata[\\"recycleTime\\"] as? Date else {\\n return false\\n }\\n \\n if now.timeIntervalSince(recycleTime) > self.maxIdleTime {\\n engine.destroyContext()\\n return false\\n }\\n return true\\n }\\n }\\n }\\n}\\n\\n// MARK: - 内存警告扩展\\nextension FlutterEnginePool {\\n func setupMemoryWarningObserver() {\\n NotificationCenter.default.addObserver(\\n self,\\n selector: #selector(handleMemoryWarning),\\n name: UIApplication.didReceiveMemoryWarningNotification,\\n object: nil\\n )\\n }\\n \\n @objc private func handleMemoryWarning() {\\n purgeAllEngines()\\n }\\n}\\n\\n
\\n// AppDelegate.swift\\n@main\\nclass AppDelegate: FlutterAppDelegate {\\n override func application(\\n _ application: UIApplication,\\n didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?\\n ) -> Bool {\\n // 初始化引擎池并启动内存监听\\n FlutterEnginePool.shared.setupMemoryWarningObserver()\\n return super.application(application, didFinishLaunchingWithOptions: launchOptions)\\n }\\n}\\n
\\nclass NativeViewController: UIViewController {\\n func navigateToFlutterPage(route: String) {\\n // 获取引擎\\n let engine = FlutterEnginePool.shared.getEngine(for: route)\\n let flutterVC = FlutterViewController(engine: engine, nibName: nil, bundle: nil)\\n \\n // 跳转页面\\n navigationController?.pushViewController(flutterVC, animated: true)\\n }\\n \\n func onReturnFromFlutter() {\\n // 归还引擎(通常在 viewDidDisappear 或 Flutter 侧触发返回时调用)\\n FlutterEnginePool.shared.recycleEngine(for: \\"your_route_identifier\\")\\n }\\n}\\n
\\n// 在 Flutter 页面中添加返回按钮逻辑\\nElevatedButton(\\n onPressed: () {\\n Navigator.pop(context); // 退出当前页面\\n // 通知 iOS 归还引擎\\n const channel = MethodChannel(\'com.example/engine_channel\');\\n channel.invokeMethod(\'recycleEngine\');\\n },\\n child: Text(\'返回原生\'),\\n)\\n\\n// iOS 端监听(在 AppDelegate 中)\\nMethodChannel(name: \\"com.example/engine_channel\\", binaryMessenger: engine.binaryMessenger)\\n .setMethodCallHandler { call, _ in\\n if call.method == \\"recycleEngine\\" {\\n FlutterEnginePool.shared.recycleEngine(for: \\"your_route_identifier\\")\\n }\\n }\\n
","description":"在 Flutter 中,单引擎和多引擎是两种不同的应用开发模式,它们在性能、资源管理和使用场景等方面存在差异。 单引擎模式\\n\\n单引擎模式是指在一个 Flutter 应用中只使用一个 Flutter 引擎实例。这个引擎负责处理所有 Flutter 界面的渲染、动画、输入事件等。大多数简单的 Flutter 应用默认采用单引擎模式。\\n\\n优点\\n\\n资源占用少:由于只使用一个引擎实例,内存和 CPU 等系统资源的占用相对较少,有助于提高应用的性能和电池续航能力。\\n\\n状态管理简单:整个应用的状态管理相对简单,因为所有的 Flutter…","guid":"https://juejin.cn/post/7474995524337500212","author":"SunshineBrother","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-25T03:43:38.247Z","media":null,"categories":["iOS","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"ABI 与 API 究竟有什么区别?程序如何与操作系统交互?","url":"https://juejin.cn/post/7474900530251579411","content":"ABI(Application Binary Interface)和 API(Application Programming Interface)都是接口的概念,但它们的关注点和应用场景有本质区别:
\\nAPI(应用程序编程接口)
\\nJava 的 List 接口
、Python 的 requests 库
、或 RESTful Web API
。.h
)、文档或代码示例暴露功能。ABI(应用程序二进制接口)
\\n场景 | API 变化 | ABI 变化 |
---|---|---|
是否需要重新编译 | 是 | 否(但已编译的二进制文件可能崩溃) |
兼容性破坏表现 | 编译错误(如函数签名不匹配) | 运行时错误(如内存访问越界、栈损坏) |
典型例子 | 删除一个函数或修改参数类型 | 修改结构体字段顺序或调整调用约定 |
API 的典型场景
\\nOpenCV
的图像处理函数)。GET /api/users
)。Linux 的 read()
或 Windows 的 CreateFileW()
)。ABI 的典型场景
\\nctypes
调用(需保证数据布局一致)。API 示例(C 语言)
\\n// math.h 中声明的 API\\nint add(int a, int b);\\n
\\n开发者调用 add(2, 3)
时,只需知道函数名和参数类型。
ABI 示例
\\n假设上述 add
函数的 ABI 规定:
eax
和 ebx
传递。ecx
。维度 | API | ABI |
---|---|---|
层级 | 源代码级(逻辑) | 二进制级(物理) |
变化代价 | 需重新编译 | 需重新编译并替换所有依赖二进制文件 |
主要维护者 | 开发者 | 编译器、操作系统、硬件厂商 |
稳定性需求 | 高(但可通过版本控制管理) | 极高(二进制分发场景必须稳定) |
简单记忆:
\\n程序与操作系统的交互是计算机运行的核心机制之一,主要通过以下方式实现:
\\nint 0x80
)或专用指令(如 syscall
/sysenter
)。open()
打开文件,fork()
创建进程,send()
发送网络数据。libc
)封装了系统调用,提供更易用的接口。\\nprintf()
内部调用 write()
系统调用。malloc()
)可能组合多个系统调用(如 brk()
或 mmap()
)。SIGSEGV
、SIGINT
)通知程序异常事件,程序可注册处理函数响应。pipe()
创建管道,shmget()
创建共享内存。mmap()
或 malloc()
申请内存,触发缺页异常由操作系统分配物理内存。程序通过系统调用直接与操作系统交互,库函数简化调用过程,中断/异常处理突发事件,IPC实现协作,操作系统最终控制硬件和资源权限。这种分层机制保证了系统的安全性和稳定性。
\\nsyscall
接口(Linux示例):#include <unistd.h>\\n#include <sys/syscall.h>\\n\\nint main() {\\n // 直接调用 write 系统调用(标准输出)\\n const char msg[] = \\"Hello via syscall!\\\\n\\";\\n syscall(SYS_write, STDOUT_FILENO, msg, sizeof(msg)-1);\\n return 0;\\n}\\n
\\n编译运行:gcc -o syscall_example syscall_example.c && ./syscall_example
#include <stdio.h>\\n#include <stdlib.h>\\n\\nint main() {\\n FILE *fp = fopen(\\"test.txt\\", \\"w\\");\\n if (fp == NULL) {\\n perror(\\"fopen failed\\");\\n exit(1);\\n }\\n // 使用库函数 fprintf(底层调用 write 系统调用)\\n fprintf(fp, \\"Hello via libc!\\\\n\\");\\n fclose(fp);\\n return 0;\\n}\\n
\\n说明:fopen
和 fprintf
封装了 open
和 write
系统调用。
#include <stdio.h>\\n#include <signal.h>\\n#include <unistd.h>\\n\\nvoid sigint_handler(int signum) {\\n write(STDOUT_FILENO, \\"\\\\nSIGINT caught!\\\\n\\", 14);\\n _exit(0); // 直接退出,避免未定义行为\\n}\\n\\nint main() {\\n // 注册 SIGINT 信号处理函数\\n signal(SIGINT, sigint_handler);\\n while(1) {\\n printf(\\"Press Ctrl+C to interrupt...\\\\n\\");\\n sleep(1);\\n }\\n return 0;\\n}\\n
\\n运行效果:按下 Ctrl+C
触发自定义处理逻辑。
#include <stdio.h>\\n#include <unistd.h>\\n#include <sys/wait.h>\\n\\nint main() {\\n int pipefd[2];\\n pipe(pipefd); // 创建管道(系统调用)\\n\\n if (fork() == 0) { // 子进程\\n close(pipefd[0]); // 关闭读端\\n write(pipefd[1], \\"Hello from child!\\", 17);\\n close(pipefd[1]);\\n } else { // 父进程\\n close(pipefd[1]); // 关闭写端\\n char buf[20];\\n read(pipefd[0], buf, sizeof(buf));\\n printf(\\"Parent received: %s\\\\n\\", buf);\\n close(pipefd[0]);\\n wait(NULL);\\n }\\n return 0;\\n}\\n
\\n#include <stdio.h>\\n#include <sys/mman.h>\\n#include <fcntl.h>\\n#include <unistd.h>\\n\\nint main() {\\n int fd = open(\\"mmap_example.txt\\", O_RDWR | O_CREAT, 0644);\\n ftruncate(fd, 1024); // 扩展文件大小\\n\\n // 将文件映射到内存\\n char *mem = mmap(NULL, 1024, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);\\n sprintf(mem, \\"Data written via mmap!\\");\\n\\n munmap(mem, 1024); // 解除映射\\n close(fd);\\n return 0;\\n}\\n
\\n说明:mmap
直接操作虚拟内存,由操作系统处理物理内存分配。
#include <stdio.h>\\n#include <errno.h>\\n#include <string.h>\\n\\nint main() {\\n FILE *fp = fopen(\\"/root/protected_file\\", \\"r\\");\\n if (fp == NULL) {\\n // 操作系统拒绝访问后检查错误类型\\n if (errno == EACCES) {\\n printf(\\"Permission denied: %s\\\\n\\", strerror(errno));\\n }\\n }\\n return 0;\\n}\\n
\\n#include <unistd.h>\\n\\nint main() {\\n // 直接使用 write 系统调用(由 glibc 包装)\\n write(STDOUT_FILENO, \\"Hello via write()!\\\\n\\", 19);\\n return 0;\\n}\\n
\\n#include <stdio.h>\\n#include <signal.h>\\n\\nvoid sigfpe_handler(int signum) {\\n printf(\\"Caught division by zero!\\\\n\\");\\n _exit(1);\\n}\\n\\nint main() {\\n signal(SIGFPE, sigfpe_handler);\\n int a = 10 / 0; // 触发 SIGFPE 信号\\n return 0;\\n}\\n
\\n#include <stdio.h>\\n#include <sys/mman.h>\\n#include <fcntl.h>\\n#include <unistd.h>\\n\\nint main() {\\n int fd = shm_open(\\"/my_shm\\", O_CREAT | O_RDWR, 0644);\\n ftruncate(fd, 1024);\\n\\n char *mem = mmap(NULL, 1024, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);\\n sprintf(mem, \\"Shared memory data\\");\\n\\n munmap(mem, 1024);\\n shm_unlink(\\"/my_shm\\");\\n return 0;\\n}\\n
\\n编译:需添加 -lrt
参数:gcc -o shm_example shm_example.c -lrt
syscall
或 glibc 包装函数(如 write
)调用。fopen
)隐藏底层细节。signal
注册回调函数处理异常。mmap
、malloc
依赖操作系统分配资源。所有示例均需在支持 POSIX 的系统(如 Linux)下编译运行。
\\n注: POSIX(Portable Operating System Interface,可移植操作系统接口)是一套由 IEEE 制定的操作系统接口标准,目标是让不同类Unix系统(如Linux、macOS、BSD等)的软件能跨平台兼容。简单来说,它定义了操作系统应该提供哪些功能接口(如文件操作、进程管理、信号处理等),开发者遵循这些接口写代码,程序就能在多个系统上运行。
","description":"ABI 与 API 究竟有什么区别? ABI(Application Binary Interface)和 API(Application Programming Interface)都是接口的概念,但它们的关注点和应用场景有本质区别:\\n\\n1. 定义与抽象层级\\n\\nAPI(应用程序编程接口)\\n\\n源代码级接口:定义的是高层功能调用规范,开发者通过函数、类、参数、返回值等与代码库或服务交互。\\n关注逻辑交互:例如,Java 的 List 接口、Python 的 requests 库、或 RESTful Web API。\\n开发者直接使用:通过头文件(如 .h)、…","guid":"https://juejin.cn/post/7474900530251579411","author":"庄周梦了个蝶","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-24T15:07:33.277Z","media":null,"categories":["代码人生","程序员","iOS","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter framework之Element","url":"https://juejin.cn/post/7474870279387578383","content":"最近正在写一个思维导图Widget,但是遇到了一个很诡异的bug,猜测问题出在自定义的Element
上。然而看了好几天掘金的文章也没看明白,最后还是决定亲自分析一下。
\\n\\n本文基于flutter 3.29.0
\\n
Element class - widgets library - Dart API
\\n很多文章都会说
\\n\\n\\n\\n
Element
是Flutter
的树中特定位置的Widget
的实例化。
这句话真的很官方,因为它就是从官方注释直接翻译过来的。但是我觉得大多数人看到这个定义时,可能和看到同济高数里那些难懂的定义一样,仍然不太清楚Element
到底有什么用。
我自己分析了一下,认为Element
Widget
、管理RenderObject
(如果有),负责把二者关联起来Element
来实现的。_parent
Element
按树形结构被组织起来,除根节点外,每个element
对象都要保存其父节点
_widget
创建这个Element
时用到的widget
对象。有些文章会说widget
负责配置Element
,这句话其实不太准确。
widget
负责配置Element
主要体现在:widget
会根据自己类型的不同创建不同类型的Element
子类。 比如StatefulWidget
会创建StatefulElement
,RenderObjectWidget
会创建RenderObjectElement
。
通常来讲,我们在写widget
时用到的那些属性(比如Text
里的字符串和textStyle
,Column
里的mainAxisSize
和spacing
...)是用来配置RenderObject
的。Element
大多数时候不会用到widget
,只会把它储存起来,这点自定义过RenderBox
的同学可能比较清楚。
_depth
Element按树形结构被组织起来,代表该element
对象在树中的深度。刷新时会按从父到子刷新。
_slot
有些文章会说slot
代表该element
在父节点中的位置信息,这句话其实不太准确。
slot
的具体值是没有意义的,当slot
相对旧值发生改变时,说明它的位置变化了,具体变到哪和slot
无关。
就像hashCode或md5,a
和b
的hashCode
不相同说明它们在内存中的位置不同,但是hashCode
这串数字没有意义。另外,slot
对element
其实没什么意义,它的作用是在自己更新时触发renderObject.move
方法,最终调用markNeedsLayout()
去重新计算布局。
_owner
和 buildScope
(不是重点)_owner
是BuildOwner
实例,全局唯一。它负责
BuildScope完成
)Element
对象,以在合适的时机将它们回收GlobalKey
和其绑定的Element
,以在合适的时机复用它们buildScope
是BuildScope
实例,子element通常会使用父节点的buildScope
,但是有例外(比如LayoutBuilder
类)。buildScope
实际上负责管理该element
下需要刷新的节点
enum _ElementLifecycle { initial, active, inactive, defunct }\\n
\\nstateDiagram-v2\\n[*] --\x3e initial: widget.createElement\\ninitial --\x3e active: Element.mount()\\nactive --\x3e inactive: Element.deactivate()\\ninactive --\x3e active: Element.active()\\ninactive --\x3e defunct: Element.unmount()\\ndefunct --\x3e [*]\\n
\\n1.initial
:flutter framework 通过Widget.createElement()
实例化了一个Element
对象,是Element
生命周期的开始。
2.active
:对于组合型Element
,也就是StatelessWidget
和StatefulWidget
对应的Element
类型,表示这个Element
对应的widget
对象的子widget
均已被展开(inflate)成element
。对于渲染型Element
,也就是RenderObjectWidget
对应的Element
,代表自己创建的RenderObject
已经被创建并添加到渲染树。此时这个widget
随时都可能显示在屏幕上。
3.inactive
:由于各种原因(通常是widget树的变化),该Element
从树中移除。这个Element将不会显示在屏幕上,并存活到当前帧结束。
4.defunct
:该帧结束时此element
对象没有被重新添加到树中,需要被卸载。这个Element
的生命周期正式结束。
之前提到过主要有两种Element
:组合型ComponentElement
和渲染型RenderObjectElement
,这两种Element在执行生命周期方法时的工作也有所不同,这里我们分类讨论
parent.inflateWidget(Widget newWidget, Object? newSlot)
Element inflateWidget(Widget newWidget, Object? newSlot) {\\n\\n final Key? key = newWidget.key;\\n // 这里会对使用了globalKey的Widget做特殊处理,尝试复用,省略\\n ....\\n // 父element创建子widget对应的Element,此时为initial状态\\n final Element newChild = newWidget.createElement();\\n \\n // 将其挂载到Element树上,此时为active状态\\n // 下一节会讨论\\n newChild.mount(this, newSlot);\\n return newChild;\\n}\\n
\\nflutter中,Element
按树形结构被组织起来,那创建子节点的任务自然就交给了父节点来完成。
在父节点
\\nmount
方法被调用时widget
被插入到树中,父Element
调用updateChild
方法更新自己的孩子时父节点parent
会通过Element.inflateWidget(Widget newWidget, Object? newSlot)
方法,递归的将子Widget
\'膨胀\'成Element
对象并挂载。
父节点调用inflateWidget
方法创建、挂载mount
子节点,而子节点被挂载mount
的过程中又会调用自己的inflateWidget
方法去挂载自己的孩子节点......当自己的孩子节点全部被挂载完成时自己才算完。
mount(Element? parent, Object? newSlot)
Element
类的mount方法// 父节点的inflateWidget方法,在这里挂载子节点\\nElement inflateWidget(Widget newWidget, Object? newSlot) {\\n final Element newChild = newWidget.createElement();\\n // this就是自己,对于newChild而言就是parent element\\n newChild.mount(this, newSlot);\\n assert(newChild._lifecycleState == _ElementLifecycle.active);\\n return newChild;\\n}\\n
\\n在mount
方法中,需要
_parent
和位置_slot
,更新自己的深度active
,表示被激活,随时都可能被显示BuildOwner
和BuildScope
InherientWidget
,并将自己关联到通知树上attachNotificationTree
void mount(Element? parent, Object? newSlot) {\\n _parent = parent;\\n _slot = newSlot;\\n // 标记自己的状态为active\\n _lifecycleState = _ElementLifecycle.active;\\n _depth = 1 + (_parent?.depth ?? 0);\\n if (parent != null) {\\n _owner = parent.owner;\\n _parentBuildScope = parent.buildScope;\\n }\\n assert(owner != null);\\n final Key? key = widget.key;\\n if (key is GlobalKey) {\\n owner!._registerGlobalKey(key, this);\\n }\\n _updateInheritance();\\n attachNotificationTree();\\n}\\n
\\n我自己看到这段代码的时候都是懵逼的,赋值就完事了?但是mount
方法干的事就是这么简单。其实仔细想想,flutter中真正负责测量、绘制的是RenderObject
,而Element
的最大作用就是根据Widget树尝试复用已有的RenderObject
,所以并不需要什么复杂的操作。
ComponmentElement
的mount方法StatelessElement
和StatefulElement
都是ComponmentElement
的子类,会触发一次performRebuild
方法,performRebuild
又会调用widget
的build
方法,创建出自己的widget
并挂载他们。
@override\\nvoid mount(Element? parent, Object? newSlot) {\\n super.mount(parent, newSlot);\\n _firstBuild();\\n assert(_child != null);\\n}\\n\\nvoid _firstBuild() {\\n // StatefulElement overrides this to also call state.didChangeDependencies.\\n rebuild(); // This eventually calls performRebuild.\\n}\\n
\\nRenderObjectElement
的mount方法RenderObjectElement
额外做了几件事情:
Widget.createRenderObject
方法,创建RenderObject
RenderObjectElement
,调用它的insertRenderObjectChild
方法,把自己创建的RenderObject插入到树中。对应RenderObject
的insert
和setChild
方法@override\\nvoid mount(Element? parent, Object? newSlot) {\\n super.mount(parent, newSlot);\\n // 创建了RenderObject对象\\n _renderObject = (widget as RenderObjectWidget).createRenderObject(this);\\n // 把RenderObject对象插入到RenderObject树中\\n // 这个方法在ComponentElement是没有的,只有RenderObjectElement才有这个方法\\n attachRenderObject(newSlot);\\n super.performRebuild(); // _dirty = false; clears the \\"dirty\\" flag\\n}\\n
\\n@override\\nvoid attachRenderObject(Object? newSlot) {\\n _slot = newSlot;\\n // 找到上级离自己最近的一个RenderObjectElement\\n _ancestorRenderObjectElement = _findAncestorRenderObjectElement();\\n // 调用上级Element的insertRenderObjectChild方法\\n _ancestorRenderObjectElement?.insertRenderObjectChild(renderObject, newSlot);\\n \\n final List<ParentDataElement<ParentData>> parentDataElements =\\n _findAncestorParentDataElements();\\n for (final ParentDataElement<ParentData> parentDataElement in parentDataElements) {\\n // 调用parentDataWidget.applyParentData,将Widget携带的信息传递给RenderObject\\n _updateParentData(parentDataElement.widget as ParentDataWidget<ParentData>);\\n }\\n}\\n
\\ndeactivate()
@mustCallSuper\\nvoid deactivate() {\\n if (_dependencies?.isNotEmpty ?? false) {\\n // 将自己从之前订阅的InheritedElement中移除,不再收听变化\\n for (final InheritedElement dependency in _dependencies!) {\\n dependency.removeDependent(this);\\n }\\n }\\n _inheritedElements = null;\\n // 标记自己为inactive状态\\n _lifecycleState = _ElementLifecycle.inactive;\\n}\\n
\\nunmount()
unmount
void unmount() {\\n final Key? key = _widget?.key;\\n if (key is GlobalKey) {\\n owner!._unregisterGlobalKey(key, this);\\n }\\n _widget = null;\\n _dependencies = null;\\n _lifecycleState = _ElementLifecycle.defunct;\\n}\\n
\\n对于StatefulElement
,还会调用其state
的dispose
方法
RenderObjectElement
的unmount
方法会额外调用自己管理的RenderObject
的dispose
方法
@override\\nvoid unmount() {\\n final RenderObjectWidget oldWidget = widget as RenderObjectWidget;\\n super.unmount();\\n oldWidget.didUnmountRenderObject(renderObject);\\n _renderObject!.dispose();\\n _renderObject = null;\\n}\\n
\\n推荐两篇写的比较好的文章:# 纷争再起:Flutter-UI绘制解析 &\\n# Flutter 必知必会系列—— Element 的更新复用机制
\\nmarkNeedsRebuild()
:主动触发更新// setState的背后也是markNeedsBuild\\nvoid markNeedsBuild() {\\n if (_lifecycleState != _ElementLifecycle.active) {\\n return;\\n }\\n if (dirty) {\\n return;\\n }\\n // 标脏\\n _dirty = true;\\n // owner是BuildOwner对象,在mount方法被赋值,和parent相同\\n // 可以简单理解成全局只有一个BuildOwner实例\\n owner!.scheduleBuildFor(this);\\n}\\n
\\n最终会调用BuildScope._scheduleBuildFor(element)
// BuildOwner类\\nvoid scheduleBuildFor(Element element) {\\n final BuildScope buildScope = element.buildScope;\\n \\n if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {\\n _scheduledFlushDirtyElements = true;\\n // onBuildScheduled是个回调,在BuildOwner创建时被赋值\\n // 在下一帧时刷新element树\\n onBuildScheduled!();\\n }\\n buildScope._scheduleBuildFor(element);\\n}\\n
\\nBuildScope._scheduleBuildFor(element)
会将自己添加到BuildScope
的脏列表_dirtyElements
中等待刷新。并且如果是本帧内第一次被调用,还会执行一次scheduleRebuild
回调。
// BuildScope类\\nvoid _scheduleBuildFor(Element element) {\\n assert(identical(element.buildScope, this));\\n if (!element._inDirtyList) {\\n _dirtyElements.add(element);\\n element._inDirtyList = true;\\n }\\n if (!_buildScheduled && !_building) {\\n _buildScheduled = true;\\n scheduleRebuild?.call();\\n }\\n if (_dirtyElementsNeedsResorting != null) {\\n _dirtyElementsNeedsResorting = true;\\n }\\n}\\n\\n
\\n结束后,请求被刷新的element
对象会被存储到本对象的buildScope._dirtyElements
中。
对于markNeedsBuild()
是如何注册回调的,详见\\n# Flutter中setState原理及更新机制。最终会回调到BuildOwner.buildScope
方法。
BuildOwner.buildScope
flutter会在WidgetFlutterBindings
初始化时创建一个rootElement
。通过WidgetsBinding.drawFrame
调用BuildOwner.buildScope
时,接收的context
参数就是rootElement
。我们在这里假设buildScope
全局唯一。
// WidgetsBinding.drawFrame\\nvoid drawFrame() {\\n buildOwner!.buildScope(rootElement!);\\n super.drawFrame();\\n buildOwner!.finalizeTree();\\n}\\n
\\n// BuildOwner.buildScope\\nvoid buildScope(Element context, [VoidCallback? callback]) {\\n final BuildScope buildScope = context.buildScope;\\n if (callback == null && buildScope._dirtyElements.isEmpty) {\\n return;\\n }\\n try {\\n _scheduledFlushDirtyElements = true;\\n buildScope._building = true;\\n if (callback != null) {\\n callback();\\n }\\n buildScope._flushDirtyElements(debugBuildRoot: context);\\n } finally {\\n buildScope._building = false;\\n _scheduledFlushDirtyElements = false;\\n }\\n}\\n
\\nelement
,也没有回调要执行,就跳过过程callback
。回调可能会重新将一部分Element
标记为需要刷新的状态(可以参考ListView
的源码)buildScope._flushDirtyElements
方法,刷新所有需要刷新的Element
\\n\\n题外话:对于
\\nbuildScope
这种末尾接受一个lambda作为参数的方法,kotlin的语法是这样的:\\nfun buildScope(element: Element){\\n // callback\\n}\\n
现在想想,kotlin的语法是真好看啊
\\n
BuildScope._flushDirtyElements
从子节点到父节点,依次调用element
对象的rebuild
方法
@pragma(\'vm:notify-debugger-on-exception\')\\nvoid _flushDirtyElements({required Element debugBuildRoot}) {\\n // 按element._depth排序,浅的在前\\n _dirtyElements.sort(Element._sort);\\n _dirtyElementsNeedsResorting = false;\\n try {\\n for (int index = 0; index < _dirtyElements.length; index = _dirtyElementIndexAfter(index)) {\\n final Element element = _dirtyElements[index];\\n // 如果element的buildScope和本buildScope是同一个,我们暂时将其视为true\\n if (identical(element.buildScope, this)) {\\n // tryRebuild会调用element的Rebuild方法\\n _tryRebuild(element);\\n }\\n }\\n } finally {\\n for (final Element element in _dirtyElements) {\\n if (identical(element.buildScope, this)) {\\n element._inDirtyList = false;\\n }\\n }\\n _dirtyElements.clear();\\n _dirtyElementsNeedsResorting = null;\\n _buildScheduled = false;\\n }\\n}\\n\\nvoid _tryRebuild(Element element) {\\n element.rebuild();\\n}\\n
\\nelement
对象按深度排序,保证先刷新父节点,再刷新子节点element.buildScope
和本buildScope
是同一个对象,执行_tryRebuild
方法,刷新自己。_tryRebuild
会调用element
的rebuild
方法element
的状态,表示他们已经不在脏列表中BuildScope
的刷新工作已完成rebuild
& update
rebuild
和performRebuild
rebuild
方法只是简单的调用了performRebuild
方法,Element
的子类会重写这个方法。还是只看最常用的两个Element
子类。
简单来说,rebuild
方法执行完后,Widget
树已经被更新到最新的状态,同时RenderObject
树也被更新完毕
ComponentElement
:更新widget树,并更新对应的ElementComponentElement.build()
方法创建出新的child。对于StatelessElement
,build方法就是widget.build
;对于StatefulElement
就是state.build()
updateChild(_child, built, slot)
更新Elementvoid performRebuild() {\\n // 这个built,就是stless.build()方法 或state.build()方法返回的Widget\\n Widget built = build();\\n // 注意,如果是StatefulElement,didChangeDependencies也是在这里被调用的\\n super.performRebuild(); // _dirty = false;\\n // _child就是本ComponentElement的唯一子Element。updateChild会更新widget树,并更新对应的Element\\n _child = updateChild(_child, built, slot);\\n}\\n
\\nRenderObjectElement
:更新RenderObject调用widget
的updateRenderObject()
,以设置RenderObject的各个属性
@override\\nvoid performRebuild() {\\n _performRebuild(); // calls widget.updateRenderObject()\\n}\\n\\nvoid _performRebuild() {\\n (widget as RenderObjectWidget).updateRenderObject(this, renderObject);\\n super.performRebuild(); // _dirty = false;\\n}\\n
\\nupdateChild
这个方法是更新子Element
的核心方法,根据旧element
和新widget
,决定复用、更新或新建Element
对象
child != null, newWidget == null
:不再使用子elementchild.widget
和newWidget
是相同的对象:只尝试更新slotchild.widget
和newWidget
不是同一个对象,但是类型和key
相同,执行child.update
方法更新child.widget
和newWidget
均不为空,但类型或key
不同:无法更新,移除旧child
,创建新element
child == null, newWidget != null
:创建一个新的elementElement? updateChild(Element? child, Widget? newWidget, Object? newSlot) {\\n if (newWidget == null) {\\n if (child != null) {\\n // 情况1,将子树添加到BuildOwner的_inactiveElements列表中\\n deactivateChild(child);\\n }\\n return null;\\n }\\n\\n final Element newChild;\\n if (child != null) {\\n \\n if (child.widget == newWidget) {\\n // 情况2:widget对象未发生变化,如果有需要(child的slot和新的slot不相同),更新slot\\n if (child.slot != newSlot) {\\n updateSlotForChild(child, newSlot);\\n }\\n newChild = child;\\n } else if (Widget.canUpdate(child.widget, newWidget)) {\\n // 情况3:两个widget类型和key相同,不是同一实例:更新slot,调用update方法\\n if (child.slot != newSlot) {\\n updateSlotForChild(child, newSlot);\\n }\\n child.update(newWidget);\\n newChild = child;\\n } else {\\n // 情况4:两个widget类型或key不同,无法复用\\n deactivateChild(child);\\n newChild = inflateWidget(newWidget, newSlot);\\n }\\n } else {\\n // 情况5:widget!= null, child == null:创建新的element\\n newChild = inflateWidget(newWidget, newSlot);\\n }\\n\\n return newChild;\\n}\\n
\\nElement.update(Widget)
方法当flutter framework决定更改这个element
对应的widget时会使用这个方法。Element
种类不同,执行过程也不相同
StatelessElement
和StatefulElement
会直接触发自己的rebuild()方法
。调用performRebuild
方法更新自己的子element
,这又会触发下级element
去更新,从而从上到下的更新完element树。
这里的调用链就是 parent.performRebuild
-> child.rebuild
-> child.performRebuild
-> ....
RenderObjectElement
及其子类RenderObjectElement
会调用widget.updateRenderObject
,更新RenderObject
的信息
// RenderObjectElement\\n@override\\nvoid update(covariant RenderObjectWidget newWidget) {\\n super.update(newWidget);\\n (widget as RenderObjectWidget).updateRenderObject(this, renderObject);\\n}\\n
\\nSingleChildRenderObjectWidget
会接受一个widget
参数作为child
,需要尝试更新它
@override\\nvoid update(SingleChildRenderObjectWidget newWidget) {\\n super.update(newWidget);\\n _child = updateChild(_child, (widget as SingleChildRenderObjectWidget).child, null);\\n}\\n
\\nMultiChildRenderObjectWidget
会接受一个List<Widget> children
作为参数,需要用updateChildren
更新所有的孩子
@override\\nvoid update(MultiChildRenderObjectWidget newWidget) {\\n super.update(newWidget);\\n final MultiChildRenderObjectWidget multiChildRenderObjectWidget =\\n widget as MultiChildRenderObjectWidget;\\n _children = updateChildren(\\n _children,\\n multiChildRenderObjectWidget.children,\\n forgottenChildren: _forgottenChildren,\\n );\\n _forgottenChildren.clear();\\n}\\n
\\n会直接调用renderObject.markNeedsLayout()
@override\\nvoid updateSlot(Object? newSlot) {\\n final Object? oldSlot = slot;\\n _ancestorRenderObjectElement?.moveRenderObjectChild(renderObject, oldSlot, slot);\\n}\\n\\n@override\\nvoid moveRenderObjectChild(\\n RenderObject child,\\n IndexedSlot<Element?> oldSlot,\\n IndexedSlot<Element?> newSlot,\\n) {\\n final ContainerRenderObjectMixin<RenderObject, ContainerParentDataMixin<RenderObject>>\\n renderObject = this.renderObject;\\n renderObject.move(child, after: newSlot.value?.renderObject);\\n}\\n\\n
\\nvoid move(ChildType child, {ChildType? after}) {\\n final ParentDataType childParentData = child.parentData! as ParentDataType;\\n if (childParentData.previousSibling == after) {\\n return;\\n }\\n _removeFromChildList(child);\\n _insertIntoChildList(child, after: after);\\n markNeedsLayout();\\n}\\n
\\n// todo
\\n其实RenderBox
部分的布局逻辑倒是没问题,问题确实是出在Element
部分。performLayout
不是会无条件被调用的。flutter framework看来Element
没有发生变化,对应的RenderObject
也不会重新布局,在layout
阶段请求重新构建的代码也自然不会执行。
解决方法: 1. 更换slot,把slot从节点id更换为节点的数据类;2. 及时删掉不用的widget
\\nRenderBox中布局的逻辑
\\n为什么动一下屏幕,被删除的节点就不再显示了?
\\nBuildScope
不相同的情况是怎么处理的?见 4.2.2,只有使用LayoutBuilder
时会出现这种情况。
LayoutBuilder
在RenderLayoutBuilder
的performLayout
阶段调用updateChild
方法
\\n@override\\nvoid mount(Element? parent, Object? newSlot) {\\n super.mount(parent, newSlot); // Creates the renderObject.\\n renderObject.updateCallback(_rebuildWithConstraints);\\n}\\n\\nvoid _rebuildWithConstraints(ConstraintType constraints) {\\n void updateChildCallback() {\\n Widget built;\\n built = (widget as ConstrainedLayoutBuilder<ConstraintType>).builder(this, constraints);\\n _child = updateChild(_child, built, null);\\n _needsBuild = false;\\n _previousConstraints = constraints;\\n }\\n }\\n\\n final VoidCallback? callback =\\n _needsBuild || (constraints != _previousConstraints) ? updateChildCallback : null;\\n owner!.buildScope(this, callback);\\n}\\n
\\n断断续续写了一周多,我是真能拖啊。。。就这么多吧,希望没啥问题
","description":"最近正在写一个思维导图Widget,但是遇到了一个很诡异的bug,猜测问题出在自定义的Element上。然而看了好几天掘金的文章也没看明白,最后还是决定亲自分析一下。 本文基于flutter 3.29.0\\n\\nElement class - widgets library - Dart API\\n\\n1.Element的职责\\n\\n很多文章都会说\\n\\nElement是Flutter的树中特定位置的Widget的实例化。\\n\\n这句话真的很官方,因为它就是从官方注释直接翻译过来的。但是我觉得大多数人看到这个定义时,可能和看到同济高数里那些难懂的定义一样,仍然不太清楚Ele…","guid":"https://juejin.cn/post/7474870279387578383","author":"嘿嘿嘿呼呼嘿","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-24T13:33:25.836Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/31d7621cf59c441c93ea33def89e797c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zi_5Zi_5Zi_5ZG85ZG85Zi_:q75.awebp?rk3s=f64ab15b&x-expires=1741011084&x-signature=9RYd1FrMqF8ZmniVdPLCWXkErZ0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/038ecdeabd114b39b877b5214b5353de~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zi_5Zi_5Zi_5ZG85ZG85Zi_:q75.awebp?rk3s=f64ab15b&x-expires=1741011084&x-signature=UWaWdFSi1H%2FdUJpiMslljImrFKA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/380dd6e7442f4184b06aee4b46825853~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zi_5Zi_5Zi_5ZG85ZG85Zi_:q75.awebp?rk3s=f64ab15b&x-expires=1741011084&x-signature=dklmGeljzTbjumpfDIhw20uC0xU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c1b45fdfee16440da296a2e174d6bd1d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zi_5Zi_5Zi_5ZG85ZG85Zi_:q75.awebp?rk3s=f64ab15b&x-expires=1741011084&x-signature=hhLwJa4hTlylrM%2BIgYjraOJnysk%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"气定神闲 —— Flutter Event Loop","url":"https://juejin.cn/post/7474817237884682251","content":"事件循环是 Dart 语言和 Flutter 框架中处理异步操作的核心机制。通过调度和执行异步任务,确保程序能够高效地响应用户输入、网络请求等异步事件。以下是关于事件循环的详细解释。
\\n事件循环的主要功能是管理和调度两个队列中的任务:事件队列(Event Queue)
和微任务队列(Microtask Queue)
。运行过程将按照一定的顺序处理这些任务,保证程序的顺利运行。
事件队列(Event Queue):
\\n微任务队列(Microtask Queue):
\\nFuture.microtask
调度的任务和 scheduleMicrotask
调度的任务。对比维度 | 事件队列 | 微任务队列 |
---|---|---|
任务来源 | 来自异步操作回调,如浏览器中setTimeout 、setInterval 、DOM事件(鼠标点击、键盘输入等),Node.js中的文件读写、网络请求等异步I/O操作完成后的回调函数 | 来自JavaScript语言本身异步操作,如Promise 的.then() 、.catch() 、.finally() 方法回调,async/await 相关回调,MutationObserver 回调函数 |
执行时机 | 调用栈为空且微任务队列所有任务执行完毕后执行 | 每次事件循环迭代开始,先检查并执行微任务队列所有任务,直到队列为空,才处理事件队列任务 |
执行顺序 | 先进先出(FIFO) | 先进先出(FIFO),且优先于事件队列执行 |
用途和场景 | 处理相对耗时、不紧急的异步操作,如网络请求、文件读写,避免阻塞主线程 | 用于当前任务完成后需尽快执行的操作,如更新UI状态、处理依赖异步操作结果的后续逻辑 |
事件循环的每次迭代遵循以下顺序:
\\n执行微任务队列中的所有任务:
\\n执行事件队列中的第一个任务:
\\n重复以上步骤:
\\ngraph LR\\n classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px\\n classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px\\n classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px\\n\\n A([开始事件循环迭代]):::startend --\x3e B{微任务队列是否为空?}:::decision\\n B -- 否 --\x3e C(执行微任务队列中的所有任务):::process\\n C --\x3e B\\n B -- 是 --\x3e D{事件队列是否为空?}:::decision\\n D -- 否 --\x3e E(执行事件队列中的第一个任务):::process\\n E --\x3e A\\n D -- 是 --\x3e F([本次迭代结束]):::startend\\n F -.-> A[开始事件循环迭代]:::startend\\n\\n style A stroke-dasharray: 5 5\\n style F stroke-dasharray: 5 5\\n
\\n下面是一个简单的示例,展示了事件循环如何处理微任务和事件任务。
\\nimport \'dart:async\';\\n\\nvoid main() {\\n print(\'Start\');\\n\\n // 添加一个事件任务到事件队列\\n Timer(Duration(seconds: 1), () {\\n print(\'Event: Timer 1\');\\n });\\n\\n // 添加一个微任务到微任务队列\\n scheduleMicrotask(() {\\n print(\'Microtask 1\');\\n });\\n\\n // 添加另一个事件任务到事件队列\\n Timer(Duration(seconds: 1), () {\\n print(\'Event: Timer 2\');\\n });\\n\\n // 添加另一个微任务到微任务队列\\n scheduleMicrotask(() {\\n print(\'Microtask 2\');\\n });\\n\\n print(\'End\');\\n}\\n
\\n输出结果:
\\nStart\\nEnd\\nMicrotask 1\\nMicrotask 2\\nEvent: Timer 1\\nEvent: Timer 2\\n
\\n解释:
\\nprint(\'Start\')
和 print(\'End\')
是同步任务,首先执行。Microtask 1
和 Microtask 2
被添加到微任务队列中,并在同步任务完成后立即执行。Timer 1
和 Timer 2
被添加到事件队列中,并在微任务完成后依次执行。同步任务:这些任务直接执行,不经过事件循环的队列。
\\n微任务:微任务被添加到微任务队列后,会在当前事件循环迭代中尽快执行,优先于事件队列中的任务。
\\n事件任务:当微任务队列为空时,事件队列中的任务(如定时器回调)才会被处理。
\\n在 Flutter 中,事件循环在处理各种异步操作(如用户输入、动画、网络请求等)时起着关键作用。理解事件循环的工作原理,可以更好地编写高效的异步代码,避免常见性能问题。
\\n例如,在 Flutter 中使用 Future
和 Stream
来处理异步数据时,可以通过合理使用微任务和事件任务,确保 UI 更新及时,提升用户体验。
void main() async {\\n print(\'Start\');\\n\\n // 使用 Future.microtask 调度微任务\\n Future.microtask(() {\\n print(\'Microtask\');\\n });\\n\\n // 使用 Future.delayed 调度事件任务\\n Future.delayed(Duration(seconds: 1), () {\\n print(\'Event: Future.delayed\');\\n });\\n\\n print(\'End\');\\n}\\n
\\n输出结果:
\\nStart\\nEnd\\nMicrotask\\nEvent: Future.delayed\\n
\\nText 是文本控件,类似于 Android 中 TextView。Text的常用属性如下图所示:
\\n代码示例如下:
\\nText(\' Flutter是谷歌推出的移动端跨平台开发框架,使用的编程语言是Dart\',\\n maxLines: 2,\\n overflow:TextOverflow.ellipsis ,\\n style: TextStyle(\\n color: Colors.red,\\n fontSize: 32,\\n decoration:TextDecoration.underline\\n ),\\n ),\\n
\\nFlutter 没有直接给出 Button 组件,而是提供了各种样式的 Button 组件。常用的 Button 组件有:
\\n代码示例如下:
\\nRaisedButton(\\n child: Text(\'RaisedButton\'),\\n color: Colors.blue,\\n textColor: Colors.red,\\n onPressed: ()=>{},\\n),\\n\\nFlatButton(\\n child: Text(\'FlatButton\'),\\n textColor: Colors.red,\\n onPressed: ()=>{},\\n),\\n\\nIconButton(\\n icon: Icon(Icons.close),\\n onPressed: ()=>{},\\n),\\n\\nOutlineButton(\\n child: Text(\'OutlineButton\'),\\n color: Colors.blue,\\n textColor: Colors.red,\\n onPressed: ()=>{},\\n \\nfloatingActionButton: FloatingActionButton(\\n child: Icon(Icons.add),\\n onPressed: ()=>{},\\n tooltip: \'你点击的是FloatingActionButton\',\\n), \\n
\\n如果 Button 组件无法满足需求,可以使用 shape 属性来设置按钮的形状,代码示例如下:
\\nOutlineButton(\\n child: Text(\\"我是自定义按钮\\"),\\n shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20.0)),\\n),\\n
\\nIcon 是图标组件,通常使用系统内置的图标。代码示例如下:
\\nIcon(\\n Icons.favorite,\\n size: 48,\\n color: Colors.red,\\n)\\n
\\nImage 组件可以加载和显示各种类型的图像,包括网络图片、本地图片等。代码示例如下:
\\n// 加载本地图片\\nImage(\\n image: AssetImage(\\"images/press.jpg\\"),\\n width: 500,\\n)\\n// 加载网络图片\\nImage(\\n image: NetworkImage(\\"\\"),\\n width: 200,\\n),\\n// 加载手机图片\\nImage.file(\\n File(\'/storage/emulated/0/Download/one.jpg\'),\\n width: 200,\\n),\\n
\\n需要注意,加载本地图片必须在 pubspec.yaml
文件中配置本地图片的位置。代码示例如下:
flutter:\\n assets:\\n - images/press.jpg\\n
\\n常用的 Iamge 属性如下图所示:
\\nfit
属性主要用于 Image
组件,用于控制图像在 Image
组件中的缩放和裁剪方式。有三种,分别是:
BoxFit.fill
:将图像拉伸或压缩以填充整个 Image
组件的边界,不保持图像的原始宽高比。BoxFit.contain
:将图像等比例缩放,使图像能够完整地显示在 Image
组件的边界内,并且尽可能地占据最大空间,可能会留下空白区域。BoxFit.cover
:将图像等比例缩放,使图像能够完全覆盖 Image
组件的边界,可能会裁剪掉部分图像。是一个显示 Flutter logo 的组件,一般用于开发时占位。代码示例如下:
\\nFlutterLogo(\\n size:100.0,\\n color:Colors.blue,\\n),\\n
\\nTextField 是文本框组件。代码示例如下:
\\nTextEditingController userController=new TextEditingController();\\nTextEditingController passwordController=new TextEditingController();\\nbody: Center(\\n child: Column(\\n children: <Widget>[\\n Padding(\\n padding: EdgeInsets.all(10),\\n child: TextField(\\n controller: _userController,\\n autofocus: false,\\n decoration: InputDecoration(\\n labelText: \'请输入邮箱地址\',\\n icon: Icon(Icons.email),\\n errorText: \'邮箱地址输入错误\',\\n ),\\n keyboardType: TextInputType.emailAddress,\\n readOnly: false,\\n maxLines: 1,\\n minLines: 1,\\n onChanged: (String text){\\n print(text);\\n },\\n onSubmitted: (String text){\\n print(\'你在文本框中输入了\'+text);\\n },\\n cursorWidth: 10,\\n cursorColor: Colors.red,\\n cursorRadius: Radius.circular(5),\\n ),\\n ),\\n ],\\n ),\\n),\\n
\\nTextField 的属性如下图所示:
\\nTextFormField 基于TextField组件封装了一层,能够做到数据的前置校验,也能够设置其默认值。代码示例如下:
\\nTextFormField(\\n decoration: InputDecoration(\\n labelText: \'密码\',\\n ),\\n initialValue: \'23365+989+8+98\',\\n obscureText: true,\\n validator: (value) {\\n if (value.isEmpty) {\\n return \'请输入密码\';\\n }\\n return null;\\n },\\n),\\n
\\n单一子元素(single-child)组件,顾名思义就是只能包含一个子组件的组件,常见的有 Container、Padding、Align、Center、FittedBox、AspectRatio、SingleChildScrollView、FractionallySizedBox、ConstrainedBox和Baseline等。下面分别介绍。
\\nContainer 是最常用的单一子元素(single-child)组件。代码示例如下
\\nContainer(\\n // 设置子组件的位置为居中\\n alignment: Alignment.center,\\n // 默认 Container 是占据整个屏幕的,constraints 属性可以控制 Container 的大小\\n constraints: BoxConstraints.expand(width: 100,height: 80),\\n //装饰器\\n decoration: BoxDecoration(\\n // 边框:黄色、大小为5的实线边框\\n border: Border.all(color: Colors.yellowAccent, style: BorderStyle.solid, width: 5),\\n // 背景图\\n image: new DecorationImage(\\n image: AssetImage(\'images/phone.jpg\'),\\n ),\\n // 边框圆角\\n borderRadius: BorderRadius.all(Radius.circular(30)),\\n //阴影效果\\n boxShadow: [\\n BoxShadow(\\n color: Colors.redAccent,//阴影颜色\\n offset: Offset(20, 20),//阴影相偏移量\\n blurRadius: 10,//高斯模糊数值\\n ),\\n ],\\n),\\n //设置旋转角度\\n transform: Matrix4.rotationZ(.3),\\n child: Text(\'\'),\\n),\\n
\\nPadding 用于设置内边距。代码示例如下:
\\nContainer(\\n width: 200.0,\\n height: 200.0,\\n color: Colors.blue,\\n child: Padding(\\n child: Text(\'我是文本\'),\\n padding: const EdgeInsets.all(10.0),\\n ),\\n),\\n
\\nPadding的布局分为以下两种情况。
\\nEdgeInsets 是用于填充各个方向的空白像素,有三种,分别是:
\\nAlign 用于设置其对齐方式,例如居中、居左、居右等。Align 有两个常用的属性——widthFactor与heightFactor。当Align不设置widthFactor与heightFactor属性的时候,Align只会跟随alignment属性调整其位置。当Align设置这两个属性后,Align会随着这两个属性改变自己的尺寸。代码示例如下:
\\nnew Align(\\n child: Text(\'我是一个Align\'),\\n heightFactor: 2.0,\\n alignment: Alignment.center,\\n),\\n
\\nCenter 组件继承自 Align,表示一个居中的组件。代码示例如下:
\\nnew Center(\\n child: Text(\'我是一个Align\'),\\n heightFactor: 2.0,\\n),\\n
\\nFittedBox 是用于给子组件三种缩放方式的组件。其类似于 Android ImageView 的 scaleType 属性。代码示例如下:
\\nnew Column(\\n children: <Widget>[\\n Container(\\n width: 100.0,\\n height: 100.0,\\n color: Colors.blue,\\n child: FittedBox(\\n child: Text(\'BoxFit.contain\',style: TextStyle(fontSize: 32),),\\n fit: BoxFit.contain,\\n ),\\n ),\\n Container(\\n width: 100.0,\\n height: 100.0,\\n color: Colors.red,\\n child: FittedBox(\\n child: Text(\'BoxFit.cover\',style: TextStyle(fontSize: 32),),\\n fit: BoxFit.cover,\\n ),\\n ),\\n Container(\\n width: 100.0,\\n height: 100.0,\\n color: Colors.yellow,\\n child: FittedBox(\\n child: Text(\'BoxFit.fill\',style: TextStyle(fontSize: 32),),\\n fit: BoxFit.fill,\\n ),\\n ),\\n Container(\\n width: 100.0,\\n height: 100.0,\\n color: Colors.orange,\\n child: FittedBox(\\n child: Text(\'BoxFit.scaleDown\',style: TextStyle(fontSize: 32),),\\n fit: BoxFit.scaleDown,\\n ),\\n ),\\n Container(\\n width: 100.0,\\n height: 100.0,\\n color: Colors.indigo,\\n child: FittedBox(\\n child: Text(\'BoxFit.fitHeight\',style: TextStyle(fontSize: 32),),\\n fit: BoxFit.fitHeight,\\n ),\\n ),\\n ],\\n),\\n
\\nAspectRatio的作用是根据设置调整子组件 child 的宽高比。代码示例如下:
\\nnew Container(\\n width: 200.0,\\n color: Colors.blue,\\n child: AspectRatio(\\n // aspectRatio 子组件表示宽高比\\n aspectRatio: 2.0/1.0, \\n child: Container(\\n color: Colors.yellow,\\n ),\\n ),\\n),\\n
\\nSingleChildScrollView 的作用是一个只能嵌套一个组件的滚动布局。虽然SingleChildScrollView只能有一个子组件,但是其子组件可以是一个多元素组件。代码示例如下:
\\nnew SingleChildScrollView(\\n child:Column(\\n children: <Widget>[...其他子组件],\\n )\\n),\\n
\\nSingleChildScrollView 默认滚动方向是垂直的,可以通过 scrollDirection属性设置为 scrollDirection属性设置为 Axis.horizontal 改成水平滚动。也可以根据reverse属性设置阅读顺序。
\\nFractionallySizedBox的用途是基于宽度缩放因子和高度缩放因子来调整布局大小,和FittedBox一样,子组件都有可能超出父组件设置的范围。代码示例如下:
\\nContainer(\\n color: Colors.yellow,\\n height: 50.0,\\n width: 50.0,\\n child: FractionallySizedBox(\\n alignment: Alignment.topLeft,\\n widthFactor: 2.0,\\n heightFactor: 1.0,\\n child: new Container(\\n width: 200.0,//(1)\\n color: Colors.blue,\\n ),\\n ),\\n),\\n
\\nConstrainedBox是一种有约束性的组件。例如,子组件无论如何都不能超出设置的约定范围。代码示例如下:
\\nConstrainedBox(\\n constraints: BoxConstraints(\\n minWidth: 100.0,\\n minHeight: 100.0,\\n maxWidth: 200.0,\\n maxHeight: 200.0,\\n ),\\n child: Container(\\n color: Colors.blue,\\n width: 100.0,\\n height: 50.0,\\n ),\\n),\\n
\\n\\n\\nConstrainedBox组件必须设置其constraints属性值,如果不设置的话,虽然编译器不会报错,但是运行之后,App会崩溃并提示错误
\\n
Baseline是一个基线组件,它可以把不相关的几个组件设置在同一条水平线上进行对齐。代码示例如下:
\\nbody:new Row(\\n children: <Widget>[\\n Baseline(\\n child: FlutterLogo(\\n size: 100.0,\\n colors: Colors.yellow,\\n ),\\n baseline: 100.0,\\n baselineType: TextBaseline.alphabetic,\\n ),\\n Baseline(\\n child: FlutterLogo(\\n size: 100.0,\\n colors: Colors.blue,\\n ),\\n baseline: 100.0,\\n baselineType: TextBaseline.alphabetic,\\n ),\\n Baseline(\\n child: FlutterLogo(\\n size: 100.0,\\n colors: Colors.indigo,\\n ),\\n baseline: 100.0,\\n baselineType: TextBaseline.alphabetic,\\n ),\\n ],\\n),\\n
\\nDrawer 是侧滑菜单组件。代码示例如下:
\\n//main.dart文件\\nimport \'package:flutter/material.dart\';\\nimport \'onePage.dart\';\\nvoid main() => runApp(MyApp());\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n title: \'Flutter Demo\',\\n theme: ThemeData(\\n primarySwatch: Colors.blue,\\n ),\\n routes: {\\n \'/page1\':(context)=>RightPage(\\"我的主页\\",),\\n \'/page2\':(context)=>RightPage(\\"我的相册\\",),\\n \'/page3\':(context)=>RightPage(\\"我的文件\\",),\\n \'/page4\':(context)=>RightPage(\\"我的游戏\\",),\\n },\\n home: DrawerDemo(title: \'侧滑菜单\'),\\n );\\n }\\n}\\n\\nclass DrawerDemo extends StatefulWidget {\\n DrawerDemo({Key key, this.title}) : super(key: key);\\n\\n final String title;\\n\\n @override\\n _DrawerDemoState createState() => _DrawerDemoState();\\n}\\n\\nclass _DrawerDemoState extends State<DrawerDemo> {\\n @override\\n Widget build(BuildContext context) {\\n return new Scaffold(\\n appBar: new AppBar(\\n title: new Text(\'侧滑菜单\'),\\n ),\\n drawer: Drawer(\\n child: ListView(\\n children: <Widget>[\\n UserAccountsDrawerHeader(\\n accountName: Text(\'liyuanjinglyj\'),\\n accountEmail: Text(\'liyuanjinglyj@163.com\'),\\n currentAccountPicture: GestureDetector(\\n child: new CircleAvatar(\\n backgroundImage: AssetImage(\'images/header.png\'),\\n )\\n ),\\n decoration: BoxDecoration(\\n color: Colors.blue,\\n ),\\n ),\\n ListTile(\\n title: Text(\'我的主页\'),\\n leading: Icon(Icons.description),\\n trailing: Icon(Icons.arrow_forward_ios),\\n onTap: (){\\n Navigator.pushNamed(context, \\"/page1\\");\\n },\\n ),\\n ListTile(\\n title: Text(\'我的相册\'),\\n leading: Icon(Icons.image),\\n trailing: Icon(Icons.arrow_forward_ios),\\n onTap: (){\\n Navigator.pushNamed(context, \\"/page2\\");\\n },\\n ),\\n ListTile(\\n title: Text(\'我的文件\'),\\n leading: Icon(Icons.insert_drive_file),\\n trailing: Icon(Icons.arrow_forward_ios),\\n onTap: (){\\n Navigator.pushNamed(context, \\"/page3\\");\\n },\\n ),\\n new Divider(),//分割线\\n ListTile(\\n title: Text(\'我的游戏\'),\\n leading: Icon(Icons.videogame_asset),\\n trailing: Icon(Icons.arrow_forward_ios),\\n onTap: (){\\n Navigator.pushNamed(context, \\"/page4\\");\\n },\\n ),\\n ],\\n ),\\n ),\\n body:Center(\\n child: Text(\'主页面\',style: TextStyle(fontSize: 50),),\\n ),\\n );\\n }\\n}\\n//onePage.dart文件\\nimport \'package:flutter/material.dart\';\\n\\nclass RightPage extends StatelessWidget {\\n final String title;\\n\\n RightPage(this.title);\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: Text(this.title),\\n ),\\n body: Center(\\n child: Text(this.title,style: TextStyle(fontSize: 50),),\\n ),\\n );\\n }\\n}\\n
\\nSwiper 是用于轮播广告或者资讯的组件。但是 Swiper组件并不是官方提供的,所以我们需要引入这个组件。Flutter 提供的是 PageView,但是这个使用比较复杂。代码示例如下:
\\n引入组件:
\\ndependencies:\\nflutter_swiper: ^1.1.6\\n
\\n实现轮播效果:
\\nbody:Container(\\n height: 200,\\n child: Swiper(\\n scrollDirection: Axis.horizontal,//设置横向\\n itemCount: 4,//数量为4\\n autoplay: true,//自动翻页\\n itemBuilder: (BuildContext context,int index){\\n return Image.network(imgLists[index]);//返回图片\\n },\\n pagination: SwiperPagination(//创建圆形分页指示\\n alignment: Alignment.bottomCenter,//分页指示位置底部中间\\n margin: const EdgeInsets.fromLTRB(0, 0, 20, 10),//间距\\n builder: DotSwiperPaginationBuilder(//圆形,选中为白色圆点,没选中为黑色圆点\\n color: Colors.black54,\\n activeColor: Colors.white\\n ),\\n ),\\n ),\\n),\\n
\\nSliverPersistentHeaderDelegate 用于实现自定义折叠的效果。代码示例如下:
\\nclass MySliverPersistentHeaderDelegate implements SliverPersistentHeaderDelegate{\\n @override\\n //折叠前大小\\n double maxExtent;\\n\\n @override\\n //折叠后大小\\n double minExtent;\\n\\n MySliverPersistentHeaderDelegate({this.maxExtent,this.minExtent});\\n\\n @override\\n Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {\\n return Stack(\\n fit: StackFit.expand,//大小与父组件一样\\n children: <Widget>[\\n Image.asset(\\n \'images/phone.jpg\',\\n fit: BoxFit.cover,//尽可能小,同时覆盖整个目标\\n ),\\n Positioned(//层叠组件\\n left: 20,\\n bottom: 20,\\n right: 20,\\n child: Text(\\n \'我的相册\',\\n style: TextStyle(\\n fontSize: 30,\\n color: Colors.red\\n ),\\n ),\\n ),\\n ],\\n );\\n }\\n\\n @override\\n bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {\\n return true;\\n }\\n\\n @override\\n // TODO: implement snapConfiguration\\n FloatingHeaderSnapConfiguration get snapConfiguration => null;\\n\\n @override\\n // TODO: implement stretchConfiguration\\n OverScrollHeaderStretchConfiguration get stretchConfiguration => null;\\n\\n}\\n
\\n多子元素(multi-child)组件,顾名思义就是可以包含多个子组件的组件。其中常用的有:Scaffold、AppBar、Row、Column、ListView、GridView、CustomScrollView、CustomMultiChildLayout、stack、IndexedStack、Table、Flex、Wrap、Flow等。下面一一介绍:
\\nScaffold(脚手架),是基于 Material Design 可视化布局的结构,也是Flutter提供的标准化布局容器。它集成了AppBar(顶部导航栏)、body(界面内容)、bottomNavigationBar(底部菜单)、floatingActionButton(右下角按钮)以及drawer(侧滑菜单)。Scaffold 的结构如下:
\\nReturn Scaffold(\\n AppBar://...\\n body://...\\n bottomNavigationBar://...\\n floatingActionButton://...\\n drawer://...\\n);\\n
\\nAppBar是顶部导航栏,它的结构如下图所示:
\\n使用示例如下:
\\nhome: new Scaffold(\\n appBar: new AppBar(\\n leading: IconButton(\\n icon: Icon(Icons.add_to_photos),\\n onPressed: ()=>{},\\n ),\\n title: new Text(\'AppBar\'),\\n actions: <Widget>[\\n IconButton(\\n icon: Icon(Icons.add),\\n tooltip: \'添加\',\\n onPressed: ()=>{},\\n ),\\n IconButton(\\n icon: Icon(Icons.delete),\\n tooltip: \'删除\',\\n onPressed: ()=>{},\\n ),\\n IconButton(\\n icon: Icon(Icons.search),\\n tooltip: \'查询\',\\n onPressed: ()=>{},\\n ),\\n ],\\n ),\\n),\\n
\\n\\n\\n一般来说,在Flutter开发中,actions最多放3个
\\n
Row和Column属于线性布局组件。其中Row是行多子元素组件,Column是列多子元素组件。它们的属性都是类似的,这里以 Row 为例,代码示例如下:
\\nbody: new Row(\\n mainAxisAlignment: MainAxisAlignment.spaceAround,//(1)\\n crossAxisAlignment: CrossAxisAlignment.stretch,//(2)\\n mainAxisSize: MainAxisSize.min,//(3)\\n textDirection: TextDirection.rtl,//(4)\\n children: <Widget>[\\n Container(\\n width: 100.0,\\n height: 100.0,\\n color: Colors.yellow,\\n alignment: Alignment.center,\\n child: Text(\'1\',style: TextStyle(fontSize: 20),),\\n ),\\n Container(\\n width: 100.0,\\n height: 100.0,\\n color: Colors.deepOrange,\\n alignment: Alignment.center,\\n child: Text(\'2\',style: TextStyle(fontSize: 20),),\\n ),\\n Container(\\n width: 100.0,\\n height: 100.0,\\n color: Colors.green,\\n alignment: Alignment.center,\\n child: Text(\'3\',style: TextStyle(fontSize: 20),),\\n ),\\n ],\\n),\\n
\\n常见的Row的属性如下图所示:
\\nFlutter中的ListView与Android开发中的ListView、RecycleView有些类似,都是线性列表组件。代码示例如下:
\\n// 创建 ListView 的方式一\\nListView(\\n itemExtent:30.0, // 子组件的高度范围\\n children:<widget>[\\n Text(\'1\'),\\n Text(\'2\'),\\n Text(\'3\'),\\n Text(\'4\'),\\n ]\\n),\\n\\n// 方式二\\nListView.builder(\\n itemExtent:30.0,\\n itemCount:4, // 如果不设置,那么列表是无限的\\n itemBuilder:(context,position){\\n return Text(\'$position\');\\n }\\n),\\n\\n// 方式三,separated 方法创建的列表可以加上分割符\\nListView.separated(\\n itemBuilder:(context,position){\\n return Text(\'$position\');\\n }\\n separatorBuilder:(context,position){\\n return Container(\\n width:500,\\n height:20,\\n color:Colors.red,\\n );\\n }\\n itemCount:10,\\n),\\n\\n
\\n\\n\\n如果想要实现自定义的功能,可以使用 ListView.custom
\\n
GridView 是实现网格结构列表的组件。代码示例如下:
\\nbody: GridView.count(\\n crossAxisCount: 2,\\n mainAxisSpacing: 10.0,\\n crossAxisSpacing: 10.0,\\n children: <Widget>[\\n Container(\\n width: 200,\\n height: 200,\\n color: Colors.yellow,\\n ),\\n //省略N个Container\\n ],\\n),\\nbody: GridView.extent(\\n maxCrossAxisExtent: 130.0,\\n mainAxisSpacing: 10,\\n crossAxisSpacing: 10,\\n children: <Widget>[\\n Container(\\n width: 200,\\n height: 200,\\n color: Colors.yellow,\\n ),\\n ],\\n),\\n
\\nCustomScrollView是可以包裹ListView与GridView的集合组件。主要用于,单个界面并不包含一个滚动列表组件时。代码示例如下:
\\nbody: CustomScrollView(\\n slivers: <Widget>[\\n SliverGrid(\\n gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(\\n maxCrossAxisExtent: 200,\\n mainAxisSpacing: 10,\\n crossAxisSpacing: 10,\\n childAspectRatio: 4\\n ),\\n delegate: SliverChildBuilderDelegate((BuildContext context,int index){\\n return Container(\\n alignment: Alignment.center,\\n color: Color.fromARGB(255, 255-index*6, 255-index*20, index*20),\\n child: Text(\'gridview$index\'),\\n );\\n },childCount: 10),\\n ),\\n SliverFixedExtentList(\\n itemExtent: 50,\\n delegate: SliverChildBuilderDelegate((BuildContext context,int index){\\n return Container(\\n alignment: Alignment.center,\\n color: Colors.teal[100*(index%5)],\\n child: Text(\'listview$index\'),\\n );\\n }),\\n ),\\n ],\\n),\\n
\\nCustomMultiChildLayout是一个多节点、自定义布局组件,通过提供的delegate可以实现控制节点的位置以及尺寸,其具体的布局行为如下:
\\n代码示例如下:
\\nclass _MyLayoutDelegate extends MultiChildLayoutDelegate{\\n static const String layoutTitle=\'layout_bar\';\\n static const String body=\'body\';\\n //布局规则\\n @override\\n void performLayout(Size size) {\\n //布局layout,并返回它的大小,方便后续放body组件\\n Size layoutSize=layoutChild(layoutTitle ,new BoxConstraints(maxHeight: size. \\n height,maxWidth: size.width));\\n //将layoutTitle放在顶部(0.0,0.0)处\\n positionChild(layoutTitle, Offset(0.0,0.0));\\n //布局body,约束为剩下的空间\\n layoutChild(body, BoxConstraints.tight(Size(size.width,size.height)));\\n //将body放在距离layoutTitle下方layoutSize.height处\\n positionChild(body, Offset(0.0,layoutSize.height));\\n }\\n //是否需要重新布局\\n @override\\n bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) {\\n return false;\\n }\\n}\\nbody: CustomMultiChildLayout(\\n delegate: _MyLayoutDelegate(),\\n children: <Widget>[\\n LayoutId(\\n id: _MyLayoutDelegate.layoutTitle,\\n child: Text(\'这是Title\'),\\n ),\\n LayoutId(\\n id: _MyLayoutDelegate.body,\\n child: Text(\'这是body\'),\\n ),\\n ],\\n),\\n
\\nStack 是一个绝对布局的组件。代码示例如下:
\\nbody:Center(\\n child: Stack(\\n alignment: Alignment(0.1,1),\\n children: <Widget>[\\n CircleAvatar(\\n backgroundImage: AssetImage(\'images/press.jpg\'),\\n radius: 100,\\n ),\\n Container(\\n decoration: BoxDecoration(\\n color: Colors.black45,\\n ),\\n child: Text(\\n \'人民邮电出版社\',\\n style: TextStyle(\\n fontSize: 20,\\n fontWeight: FontWeight.bold,\\n color: Colors.white\\n ),\\n ),\\n ),\\n ],\\n ),\\n),\\n
\\nIndexedStack继承Stack,通过IndexedStack的index属性,可以直接切换它的子组件。代码示例如下:
\\nbody:Center(\\n child: IndexedStack(\\n index: 2,\\n alignment: Alignment.center,\\n children: <Widget>[\\n Text(\\"第一层\\"),\\n Text(\\"第二层\\"),\\n Text(\\"第三层\\"),\\n ],\\n ),\\n),\\n
\\nTable是一个表格组件。Table组件通过TableRow子属性逐行设置数据,同时通过columnWidths属性设置列宽,通过border属性设置表格边框样式等。代码示例如下:
\\nbody:Center(\\n child: Table(\\n columnWidths: const {\\n 0: FixedColumnWidth(100.0),\\n 1: FixedColumnWidth(200.0),\\n 2: FixedColumnWidth(50.0),\\n },\\n border: TableBorder.all(\\n color: Colors.blue,\\n width: 2,\\n style: BorderStyle.solid\\n ),\\n children: [\\n TableRow(\\n decoration: BoxDecoration(\\n color: Colors.yellow\\n ),\\n children: [\\n Text(\'姓名\'),\\n Text(\'职业\'),\\n Text(\'年龄\'),\\n ],\\n ),\\n TableRow(\\n decoration: BoxDecoration(\\n color: Colors.yellow\\n ),\\n children: [\\n Text(\'张三\'),\\n Text(\'产品经理\'),\\n Text(\'30\'),\\n ],\\n ),\\n TableRow(\\n decoration: BoxDecoration(\\n color: Colors.yellow\\n ),\\n children: [\\n Text(\'李四\'),\\n Text(\'软件工程师\'),\\n Text(\'27\'),\\n ],\\n ),\\n TableRow(\\n decoration: BoxDecoration(\\n color: Colors.yellow\\n ),\\n children: [\\n Text(\'王五\'),\\n Text(\'执行总裁\'),\\n Text(\'55\'),\\n ],\\n ),\\n ],\\n ),\\n),\\n
\\nFlex 借鉴了前端的Flex布局方式,代码示例如下:
\\nbody:Column(\\n children: <Widget>[\\n Container(\\n height: 200,\\n child: Flex(\\n direction: Axis.horizontal,\\n children: <Widget>[\\n Expanded(\\n flex: 1,\\n child: Container(\\n color: Colors.yellow,\\n ),\\n ),\\n Expanded(\\n flex: 2,\\n child: Container(\\n color: Colors.blue,\\n ),\\n ),\\n ],\\n ),\\n ),\\n ],\\n),\\n
\\nWrap组件能代替Row组件,当一行显示不全的时候,会自动进行换行处理。代码示例如下:
\\nbody:Wrap(\\n spacing: 10,\\n runSpacing: 1,\\n children: <Widget>[\\n FlatButton(\\n child: Text(\'Flutter技术开发\'),\\n ),\\n FlatButton(\\n child: Text(\'Python\'),\\n ),\\n FlatButton(\\n child: Text(\'Vue\'),\\n ),FlatButton(\\n child: Text(\'Android Studio\'),\\n ),\\n FlatButton(\\n child: Text(\'Django\'),\\n ),\\n FlatButton(\\n child: Text(\'C/C++\'),\\n ),\\n FlatButton(\\n child: Text(\'Qt5\'),\\n ),\\n FlatButton(\\n child: Text(\'Weex\'),\\n ),\\n ],\\n),\\n
\\nWrap的属性如下图所示:
\\n代码示例如下:
\\nclass NightFlowDelegate extends FlowDelegate{\\n EdgeInsets margin=EdgeInsets.zero;//默认为0\\n\\n NightFlowDelegate({this.margin});\\n @override\\n void paintChildren(FlowPaintingContext context) {\\n var left=margin.left;\\n var top=margin.top;\\n for(int i=0;i<context.childCount;i++){\\n var childWidth=context.getChildSize(i).width+left+margin.right;//子组件的长度\\n if(childWidth<context.size.width){\\n context.paintChild(\\n i,\\n transform: new Matrix4.compose(Vector.Vector3(left,top,0.0), \\n Vector.Quaternion(0.0,0.0,0.0,0.0), Vector.Vector3(1.0,1.0,1.0)));\\n left = childWidth + margin.left;//确定下一个位置的坐标\\n }else{\\n left = margin.left;\\n top += context.getChildSize(i).height + margin.top + margin.bottom;\\n //绘制子组件(有优化)\\n context.paintChild(i,\\n transform: Matrix4.translationValues(left, top, 0.0) //位移\\n );\\n left += context.getChildSize(i).width + margin.left + margin.right;\\n }\\n }\\n }\\n\\n getSize(BoxConstraints constraints) {\\n //指定Flow组件的大小\\n return Size(double.infinity, double.infinity);\\n }\\n\\n @override\\n bool shouldRepaint(FlowDelegate oldDelegate) {\\n return oldDelegate != this;\\n }\\n}\\nbody:Flow(\\n delegate: NightFlowDelegate(margin: EdgeInsets.all(1)),\\n children: <Widget>[\\n Container(color: Colors.yellow,width: 100,height: 100),\\n Container(color: Colors.blue,width: 100,height: 100),\\n Container(color: Colors.orange,width: 100,height: 100),\\n Container(color: Colors.red,width: 100,height: 100),\\n Container(color: Colors.deepPurpleAccent,width: 100,height: 100),\\n Container(color: Colors.indigoAccent,width: 100,height: 100),\\n Container(color: Colors.lightGreenAccent,width: 100,height: 100),\\n Container(color: Colors.greenAccent,width: 100,height: 100),\\n Container(color: Colors.yellow,width: 100,height: 100),\\n ],\\n),\\n
\\n最近在研究一个验证码转发的app,原理是尝试读取手机中对应应用的验证码进行自动转发。本次尝试用flutter开发,因为之前没有flutter开发的经验,遇到了诸多环境方面的问题,汇总一些常见的问题如下。希望帮助到入门的flutter开发者,避免踩坑。
\\nFAILURE: Build failed with an exception.\\n\\n\\n* What went wrong:\\nExecution failed for task \':gradle:compileGroovy\'.\\n> BUG! exception in phase \'semantic analysis\' in source unit \'C:\\\\dev\\\\flutter\\\\packages\\\\flutter_tools\\\\gradle\\\\src\\\\main\\\\groovy\\\\app_plugin_loader.groovy\' Unsupported class file major version 65\\n\\n\\n* Try:\\n> Run with --stacktrace option to get the stack trace.\\n> Run with --info or --debug option to get more log output.\\n> Run with --scan to get full insights.\\n\\n\\n* Get more help at https://help.gradle.org\\n\\n\\nBUILD FAILED in 31s\\nRunning Gradle task \'assembleDebug\'... 32.3s\\n\\n\\n┌─ Flutter Fix ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ \\n│ [!] Your project\'s Gradle version is incompatible with the Java version that Flutter is using for Gradle. │ \\n│ │\\n│ If you recently upgraded Android Studio, consult the migration guide at https://flutter.dev/to/java-gradle-incompatibility. │ \\n│ │ \\n│ Otherwise, to fix this issue, first, check the Java version used by Flutter by running `flutter doctor --verbose`. │ \\n│ │ \\n│ Then, update the Gradle version specified in D:\\\\Project\\\\Verify_Code_App\\\\verify_code_app\\\\android\\\\gradle\\\\wrapper\\\\gradle-wrapper.properties to be │ \\n│ compatible with that Java version. See the link below for more information on compatible Java/Gradle versions: │ \\n│ https://docs.gradle.org/current/userguide/compatibility.html#java │ \\n│ │ \\n│ │ \\n└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘\\n
\\n执行flutter doctor --verbose
发现
Java binary at: D:\\\\Android\\\\Android Studio\\\\jbr\\\\bin\\\\java\\n
\\n说明java地址指向不对,要使用flutter config --jdk-dir <jdk目录>
来指定java目录
配置gradle plugin国内镜像源时,使用了
\\npluginManagement {\\n repositories {\\n maven { url \'https://plugins.gradle.org/m2/\' }\\n maven { url \'https://maven.aliyun.com/nexus/content/repositories/google\' }\\n maven { url \'https://maven.aliyun.com/nexus/content/groups/public\' }\\n maven { url \'https://maven.aliyun.com/nexus/content/repositories/jcenter\'}\\n gradlePluginPortal()\\n google()\\n mavenCentral()\\n }\\n}\\n
\\n报错如下:
\\nLaunching lib\\\\main.dart on sdk gphone64 x86 64 in debug mode...\\ne: D:\\\\Project\\\\Verify_Code_App\\\\verify_code_app\\\\android\\\\settings.gradle.kts:14:25: Too many characters in a character literal \'\'https://maven.aliyun.com/nexus/content/repositories/google\'\'\\ne: D:\\\\Project\\\\Verify_Code_App\\\\verify_code_app\\\\android\\\\settings.gradle.kts:15:25: Too many characters in a character literal \'\'https://maven.aliyun.com/nexus/content/groups/public\'\'\\ne: D:\\\\Project\\\\Verify_Code_App\\\\verify_code_app\\\\android\\\\settings.gradle.kts:16:25: Too many characters in a character literal \'\'https://maven.aliyun.com/nexus/content/repositories/jcenter\'\'\\n\\n\\nFAILURE: Build failed with an exception.\\n\\n\\n* Where:\\nSettings file \'D:\\\\Project\\\\Verify_Code_App\\\\verify_code_app\\\\android\\\\settings.gradle.kts\' line: 14\\n\\n\\n* What went wrong:\\nScript compilation errors:\\n\\n\\n Line 14: maven { url=uri(\'https://maven.aliyun.com/nexus/content/repositories/google\') }\\n ^ Too many characters in a character literal \'\'https://maven.aliyun.com/nexus/content/repositories/google\'\' \\n\\n\\n Line 15: maven { url=uri(\'https://maven.aliyun.com/nexus/content/groups/public\') }\\n ^ Too many characters in a character literal \'\'https://maven.aliyun.com/nexus/content/groups/public\'\'\\n\\n\\n Line 16: maven { url=uri(\'https://maven.aliyun.com/nexus/content/repositories/jcenter\')}\\n ^ Too many characters in a character literal \'\'https://maven.aliyun.com/nexus/content/repositories/jcenter\'\' \\n\\n\\n3 errors\\n
\\n网上的教程大多是按build.gradle文件来配置的,然而本项目采用了build.gradle.kts,所以需要修改为
\\n修改为
\\nrepositories {\\n maven { url=uri(\\"https://plugins.gradle.org/m2/\\") }\\n maven { url=uri(\\"https://maven.aliyun.com/nexus/content/repositories/google\\") }\\n maven { url=uri(\\"https://maven.aliyun.com/nexus/content/groups/public\\") }\\n maven { url=uri(\\"https://maven.aliyun.com/nexus/content/repositories/jcenter\\")}\\n gradlePluginPortal()\\n google()\\n mavenCentral()\\n}\\n
\\n运行flutter项目时报错,提示找不到第三方库的命名空间:
\\nNamespace not specified. Specify a namespace in the module\'s build file\\n
\\n在build.gradle.kts
文件中添加如下代码,来对第三方库进行命名空间指定:
subprojects {\\n afterEvaluate {\\n if (this is org.gradle.api.Project && (plugins.hasPlugin(\\"com.android.library\\") || plugins.hasPlugin(\\"com.android.application\\"))) {\\n val androidExtension = extensions.findByType<com.android.build.gradle.BaseExtension>()\\n androidExtension?.let { android ->\\n val currentNamespace = android.namespace\\n println(\\"project: ${this.name} Namespace get: $currentNamespace\\")\\n\\n\\n val packageName = currentNamespace\\n ?: android.defaultConfig.applicationId\\n ?: android.sourceSets.getByName(\\"main\\").manifest.srcFile.readText().let { manifestText ->\\n val regex = Regex(\\"package=\\"([^\\"]*)\\"\\")\\n regex.find(manifestText)?.groupValues?.get(1)\\n }\\n ?: group.toString()\\n\\n\\n android.namespace = packageName\\n println(\\"Namespace set to: $packageName for project: ${this.name}\\")\\n\\n\\n val manifestFile = android.sourceSets.getByName(\\"main\\").manifest.srcFile\\n if (manifestFile.exists()) {\\n var manifestText = manifestFile.readText()\\n if (manifestText.contains(\\"package=\\")) {\\n manifestText = manifestText.replace(Regex(\\"package=\\"[^\\"]*\\"\\"), \\"\\")\\n manifestFile.writeText(manifestText)\\n println(\\"Package attribute removed in AndroidManifest.xml for project: ${this.name}\\")\\n } else {\\n println(\\"No package attribute found in AndroidManifest.xml for project: ${this.name}\\")\\n }\\n } else {\\n println(\\"AndroidManifest.xml not found for project: ${this.name}\\")\\n }\\n }\\n }\\n }\\n}\\n
\\n在编写flutter项目时,引入了sms_advanced第三方库,但是在运行时,出现了如下错误:
\\n* What went wrong:\\nThe Android Gradle plugin supports only Kotlin Gradle plugin version 1.5.20 and higher.\\nThe following dependencies do not satisfy the required version:\\nproject \':sms_advanced\' -> org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.50\\n
\\npubspec.yml中引入的依赖:
\\ndependencies:\\n flutter:\\n sdk: flutter\\n http: ^1.3.0\\n shared_preferences: ^2.5.2\\n\\n\\n # The following adds the Cupertino Icons font to your application.\\n # Use with the CupertinoIcons class for iOS style icons.\\n cupertino_icons: ^1.0.8\\n sms_advanced: ^1.1.0\\n
\\n网上大多数解决方案是修改android/build.gradle
文件,将kotlin
版本修改为1.5.20以上,这个方案需要对sms_advanced
依赖中的文件进行操作。我尝试前往对应插件库的github项目地址,发现作者最新的更新仅上传了github并未上传到pub,因此无法通过修改版本号的方式解决问题。
因此我尝试修改dependencies,直接从git仓库中引入依赖,如下:
\\ndependencies:\\n flutter:\\n sdk: flutter\\n http: ^1.3.0\\n shared_preferences: ^2.5.2\\n\\n\\n # The following adds the Cupertino Icons font to your application.\\n # Use with the CupertinoIcons class for iOS style icons.\\n cupertino_icons: ^1.0.8\\n sms_advanced:\\n git:\\n url: git@github.com:EddieKamau/sms_advanced.git\\n
\\n最终得以解决。
","description":"最近在研究一个验证码转发的app,原理是尝试读取手机中对应应用的验证码进行自动转发。本次尝试用flutter开发,因为之前没有flutter开发的经验,遇到了诸多环境方面的问题,汇总一些常见的问题如下。希望帮助到入门的flutter开发者,避免踩坑。 problems\\n1. running failed\\n1.1. Bug Description\\nFAILURE: Build failed with an exception.\\n\\n\\n* What went wrong:\\nExecution failed for task \':gradle…","guid":"https://juejin.cn/post/7474812310444965923","author":"yeffky","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-24T09:40:26.207Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/74b3f200afda4c899195891aa9a5070b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgeWVmZmt5:q75.awebp?rk3s=f64ab15b&x-expires=1740994826&x-signature=GZXcN%2B6%2F4tq71U9px96WttjDy04%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"FlutterWeb实战:04-集成微信JS-SDK提供丰富体验","url":"https://juejin.cn/post/7474826148514938943","content":"\\n\\n微信的 JS-SDK 提供了很多调用微信能力的 API,H5 页面也经常用到。本文以文件上传为为例,介绍了如何在 Flutter Web 项目集成微信 JS-SDK。
\\n
首先,我们需要对微信 JS-SDK 进行初始化配置。以下代码将原本的初始化方法封装为 Promise 形式,便于后续调用。
\\n/**\\n * 配置js\\n * @param {*} options\\n * @returns\\n */\\nexport async function configJsSdk(options) {\\n return new Promise((resolve, reject) => {\\n wx.config(options);\\n wx.ready(function () {\\n resolve();\\n });\\n wx.error(function () {\\n reject();\\n });\\n });\\n}\\n
\\n通过这种方式,我们可以在配置完成后执行后续操作,确保 JS-SDK 的正确初始化。
\\n接下来,我们实现图片上传功能。微信 JS-SDK 提供了 chooseImage 和 uploadImage 两个 API,分别用于选择图片和上传图片。我们将这两个 API 封装为 Promise 形式,方便在 Flutter Web 中调用。
\\n/**\\n * 上传图片\\n * @returns\\n */\\nexport async function uploadImage() {\\n return upload([\'album\', \'camera\']);\\n}\\n\\n/**\\n * 拍照上传\\n * @returns\\n */\\nexport async function uploadCamera() {\\n return upload([\'camera\']);\\n}\\n\\nexport async function upload(sourceType) {\\n return new Promise((resolve, reject) => {\\n wx.chooseImage({\\n // 默认9\\n count: 1,\\n // 可以指定是原图还是压缩图,默认二者都有\\n sizeType: [\'original\', \'compressed\'],\\n // 可以指定来源是相册还是相机,默认二者都有\\n sourceType: sourceType,\\n // 返回选定照片的本地 ID 列表,localId可以作为 img 标签的 src 属性显示图片\\n success: function (res) {\\n wx.uploadImage({\\n // 需要上传的图片的本地ID,由 chooseImage 接口获得\\n localId: res.localIds[0],\\n // 默认为1,显示进度提示\\n isShowProgressTips: 1,\\n // 返回图片的服务器端ID\\n success: function (res) {\\n resolve(res.serverId);\\n },\\n fail: function (res) {\\n reject(res);\\n }\\n })\\n }\\n });\\n })\\n}\\n
\\n通过封装,我们可以轻松实现从相册或相机选择图片并上传的功能。
\\n在客户端上传图片后,服务端需要接收微信返回的 mediaId,并调用微信 API 下载文件。以下是服务端的实现步骤。
\\n首先,在 pom.xml 中引入 wx-java-mp 依赖包:
\\n <!-- 微信公众号 --\x3e\\n <dependency>\\n <groupId>com.github.binarywang</groupId>\\n <artifactId>wx-java-mp-spring-boot-starter</artifactId>\\n <version>4.4.0</version>\\n </dependency>\\n
\\n在服务端,我们可以通过 WxMpService 提供的 API 下载文件:
\\n\\n @Autowired\\n private WxMpService wxMpService;\\n\\n File file = wxMpService.getMaterialService().mediaDownload(encode(request.getMediaId()));\\n
\\n通过 mediaDownload 方法,我们可以根据 mediaId 下载文件,并进行后续处理。
\\n为了方便在 Flutter Web 项目中调用,我们将上述功能封装为一个对象,并导出到全局作用域:
\\nimport * as flutterWeb from \\"./app/index.js\\";\\n\\nwindow.flutterWeb = flutterWeb;\\n
\\n这里可以把 flutterWeb 改为你希望使用的名字。
\\n在 Web 中,可以通过 flutterWeb 对象调用相关方法,我们将在后续文章介绍,如何在 Flutter Web 中调用 Web 中的 API。
\\n本文详细介绍了如何在 Flutter Web 项目中集成微信 JS-SDK,并实现图片上传功能。通过封装 JS-SDK 的 API 和服务端处理逻辑,我们可以轻松实现与微信的深度集成,为用户提供更丰富的功能体验。希望本文能为开发者提供有价值的参考。
\\n\\n\\n前端有非常多的框架、工具、库,这些都要比 Dart Web 成熟、丰富。所以在将 Fluttter 编译成 Web 以后,若能使用现有的前端技术实现 web 端的特殊需求,肯定事半功倍。
\\n
在开始之前,确保你已经安装好了 node 和 npm
\\n首先使用 create-react-app 创建一个前端项目
\\nnpx create-react-app flutter_web\\n
\\n这些创建以下文件
\\n .eslintrc.js\\n build/\\n node_modules/\\n package.json\\n public/\\n src/\\n yarn.lock\\n
\\n这是一个标准的前端项目,不过不用担心,我们不会使用任何 react 技术。
\\n为了能自定义 webpack 打包配置,需要安装一个名为 react-app-rewired
的插件,以替换 react-scripts
脚本
安装 react-app-wired
\\nnpm install -g react-app-rewired\\n
\\n在根目录创建 config-overrides.js
文件,增加以下内容
const HtmlWebpackPlugin = require(\'html-webpack-plugin\');\\n\\nmodule.exports = function override(config, env) {\\n // Remove the default HtmlWebpackPlugin\\n config.plugins = config.plugins.filter(\\n (plugin) => !(plugin instanceof HtmlWebpackPlugin)\\n );\\n\\n // Add your own HtmlWebpackPlugin instance with your own options\\n config.plugins.push(\\n new HtmlWebpackPlugin({\\n template: \'public/index.html\',\\n minify: {\\n removeComments: false,\\n collapseWhitespace: false,\\n removeRedundantAttributes: true,\\n useShortDoctype: true,\\n removeEmptyAttributes: true,\\n removeStyleLinkTypeAttributes: true,\\n keepClosingSlash: true,\\n minifyJS: false,\\n minifyCSS: true,\\n minifyURLs: true,\\n },\\n })\\n );\\n\\n return config;\\n};\\n
\\n这里面引入一个名为 html-webpack-plugin
的插件,配置了需要压缩的内容。
替换 package.json 中 scripts 部分
\\n \\"scripts\\": {\\n- \\"start\\": \\"react-scripts start\\",\\n+ \\"start\\": \\"react-app-rewired start\\",\\n- \\"build\\": \\"react-scripts build\\",\\n+ \\"build\\": \\"react-app-rewired build\\",\\n- \\"test\\": \\"react-scripts test\\",\\n+ \\"test\\": \\"react-app-rewired test\\",\\n \\"eject\\": \\"react-scripts eject\\"\\n}\\n
\\n在当前项目目录下,执行以下命令初始化 Flutter 项目
\\nflutter create --platforms web .\\n
\\n这将创建一个 Flutter 项目,并添加了 web 平台支持。
\\n以下目录由 flutter 创建
\\nRecreating project ....\\n pubspec.yaml (created)\\n lib/main.dart (created)\\n web/\\n analysis_options.yaml (created)\\n
\\n现在,虽然两个项目共用一个目录,但我们需要修改一些配置,才将 flutter 项目与前端项目集成在一起工作。
\\n编辑 package.json 文件中scripts/build
处的内容,改为
\\"rm -rf build && rm -rf web && react-app-rewired build && mv build web\\",
\\n同时删除不需要的依赖, 增加 react-app-rewired
依赖
\\"dependencies\\": {\\n- \\"cra-template\\": \\"1.2.0\\",\\n- \\"react\\": \\"^19.0.0\\",\\n- \\"react-dom\\": \\"^19.0.0\\",\\n \\"react-scripts\\": \\"5.0.1\\"\\n },\\n \\"eslintConfig\\": {\\n \\"extends\\": [\\n- \\"react-app\\",\\n- \\"react-app/jest\\"\\n ]\\n },\\n+ \\"devDependencies\\": {\\n+ \\"react-app-rewired\\": \\"^2.2.1\\"\\n+ }\\n
\\n运行 npm install
或 yarn install
, 更新依赖。
这行命令的作用是,构建时先清理当前项目目录 build 和 web 目录,构建完成后将前端构建目录改名为 web,以提供给 flutter 进一步构建使用。
\\n最终,经过一番折腾, package.json
文件中的内容如下面所示
{\\n \\"name\\": \\"flutter_web\\",\\n \\"version\\": \\"0.1.0\\",\\n \\"private\\": true,\\n \\"dependencies\\": {\\n \\"react-scripts\\": \\"5.0.1\\"\\n },\\n \\"scripts\\": {\\n \\"start\\": \\"react-app-rewired start\\",\\n \\"build\\": \\"rm -rf build && rm -rf web && react-app-rewired build && mv build web\\",\\n \\"test\\": \\"react-app-rewired test\\",\\n \\"eject\\": \\"react-app-rewired eject\\"\\n },\\n \\"eslintConfig\\": {\\n \\"extends\\": [\\n ]\\n },\\n ...\\n \\"devDependencies\\": {\\n \\"react-app-rewired\\": \\"^2.2.1\\"\\n }\\n}\\n
\\n接下来我们复制 web 目录并替换掉 public 目录。
\\nrm -rf public\\ncp -r web public\\n
\\n运行 npm run build
, 如果能成功生成 web 目录,代表集成成功
前面的准备工作完成以后,就可以愉快的开发了!
\\n进入 src 目录,这里面就可以编写我们的前端代码了,也可以使用 npm 的任何 js 库。
\\n为了统一维护 js,我们把 flutter web 的初始化代码从 html 中移到这里。
\\n首先清空 src 目录中的文件,然后新建一个 index.js
, 添加以下内容
window.flutterWebRenderer = \\"html\\";\\nwindow.addEventListener(\'load\', function(ev) {\\n // Download main.dart.js\\n _flutter.loader.loadEntrypoint({\\n entrypointUrl: \'main.dart.js\',\\n serviceWorker: {\\n serviceWorkerVersion: serviceWorkerVersion,\\n }\\n }).then(function(engineInitializer) {\\n return engineInitializer.initializeEngine();\\n }).then(function(appRunner) {\\n return appRunner.runApp();\\n });\\n});\\n
\\n# 构建前端\\nnpm run build\\n# 构建Flutter\\nflutter clean && flutter build web --build-number=$VERSION_CODE --build-name=$TAG --web-renderer html --profile --base-href /webapp/\\n
\\n上述命令中, 和 都是环境变量,需要提前设置好
\\nexport VERSION_CODE=1\\nexport TAG=1.0.0\\n
\\n这里需要注意的是,如果你不希望通过子目录访问 Flutter web 应用,那么需要将 base-href
设置为 /
,或者移除该选项
flutter clean && flutter build web --build-number=$VERSION_CODE --build-name=$TAG --web-renderer html --profile\\n
\\n默认情况下,Flutter 打包 web 以后,首次打开页面需要加载大量的资源,这就需要做首屏加载优化。
\\n通过分析,canvaskit 和 skwasm 需要加载较大的引擎包,很难优化,目前选择 3.22 版本,故选择 HTML Render 引擎
\\n\\n\\nFlutter Web 计划在 2025 开始弃用 HTML Render。如果是 2025 年的新版本,可以考虑使用 skwasm 引擎。
\\n
体积裁剪,通过 bulid apk shaking icon,得到一个裁剪后的字体库,替换调 Flutter Web 打包的对应字体产物
\\n先在 App 项目构建 apk:
\\nflutter build apk --tree-shake-icons\\n
\\n找到 build/host/intermediates/assets/release/mergeReleaseAssets/flutter_assets/fonts/MaterialIcons-Regular.otf
\\n将该文档复制到 web/fonts/
文件夹
文件采样 | 压缩前 | 压缩后 | 压缩率 |
---|---|---|---|
MaterialIcons-Regular.otf | 1.5M | 2k | 1% |
使用延迟加载拆分文件,当前页面不需要的使用的代码延迟加载
\\nDart 中提供了 defered 关键词,用于延迟加载组件。
\\n参考下方实现一个 DeferredWidget 组件
\\nimport \'dart:async\';\\nimport \'package:ealing_widget/common/common_color.dart\';\\nimport \'package:flutter/material.dart\';\\n\\ntypedef LibraryLoader = Future<void> Function();\\ntypedef DeferredWidgetBuilder = Widget Function();\\n\\n///延迟加载组件\\nclass DeferredWidget extends StatefulWidget {\\n DeferredWidget(this.libraryLoader, this.createWidget, {Key? key, Widget? placeholder}) : placeholder = placeholder ?? Container(color: CommonColors.color_widget_background), super(key: key);\\n\\n final LibraryLoader libraryLoader;\\n final DeferredWidgetBuilder createWidget;\\n final Widget placeholder;\\n // 存储 libraryLoader 对应的 future 数据\\n static final Map<LibraryLoader, Future<void>> _moduleLoaders = {};\\n // 存储已经预加载过了的 libraryLoader\\n static final Set<LibraryLoader> _loadedModules = {};\\n\\n static Future<void>? preload(LibraryLoader loader) {\\n if (!_moduleLoaders.containsKey(loader)) {\\n _moduleLoaders[loader] = loader().then((dynamic _) {\\n _loadedModules.add(loader);\\n });\\n }\\n return _moduleLoaders[loader];\\n }\\n\\n @override\\n _DeferredWidgetState createState() => _DeferredWidgetState();\\n}\\n\\nclass _DeferredWidgetState extends State<DeferredWidget> {\\n Widget? _loadedChild;\\n\\n @override\\n void initState() {\\n if (DeferredWidget._loadedModules.contains(widget.libraryLoader)) {\\n _onLibraryLoaded();\\n } else {\\n DeferredWidget.preload(widget.libraryLoader)?.then((dynamic _) => _onLibraryLoaded());\\n }\\n super.initState();\\n }\\n\\n void _onLibraryLoaded() {\\n setState(() {\\n _loadedChild = widget.createWidget();\\n });\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return _loadedChild ?? widget.placeholder;\\n }\\n}\\n
\\n然后在 GoRouter 路由配置处, 以这种形式使用:
\\n\\nimport \'../screens/home/index.dart\' deferred as home;\\n\\nfinal _router = GoRouter(\\n routes: [\\n GoRoute(\\n path: \'/\',\\n builder: (context, state) => ppDeferredWidget(libraryLoader: home.loadLibrary, builder: (() => home.HomeIndexScreen())),\\n ),\\n ],\\n);\\n
\\n经过以上配置, Flutter Web 打包后,将对 js 文件分割,只有在当前页面打开时,才会加载对应的 js 文件,这就实现了页面组件资源的延迟加载。
\\n经过加载对比可以看到,首屏加载时,原本 2M 左右的 main.dart.js 大小,减小到了 1M 左右,显著提升了首屏静态资源大小。
\\n增加过渡动画,在资源加载过程中使用一个加载动画,优化用户体验。
\\n这里使用 flutter_native_splash 插件,在 app 启动时,显示一个加载动画,在 app 加载完成后,隐藏加载动画。
\\n<body>\\n <picture id=\\"splash\\">\\n <img class=\\"center\\" width=\\"95\\" height=\\"100\\" aria-hidden=\\"true\\" src=\\"loading.gif\\" alt=\\"\\">\\n </picture>\\n <script type=\\"text/javascript\\" src=\\"splash/splash.js\\"></script>\\n</body>\\n
\\n增加以下 css 样式
\\nhtml { height: 100% }\\n\\nbody {\\n margin: 0;\\n min-height: 100%;\\n background-size: 100% 100%;\\n -webkit-text-size-adjust: 100% !important;\\n text-size-adjust: 100% !important;\\n -moz-text-size-adjust: 100% !important;\\n}\\n\\n.center {\\n margin: 0;\\n position: absolute;\\n top: 50%;\\n left: 50%;\\n -ms-transform: translate(-50%, -50%);\\n transform: translate(-50%, -50%);\\n}\\n
\\nsplash/splash.js 的内容如下:
\\nfunction removeSplashFromWeb() {\\n document.getElementById(\\"splash\\")?.remove();\\n document.getElementById(\\"splash-branding\\")?.remove();\\n document.body.style.background = \\"transparent\\";\\n}\\n
\\n在 Flutter main.dart 中,配置加载动画保持, 我们将在后面手动移除。
\\nvoid main() {\\n FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);\\n}\\n
\\n在 AppDefere 中,移除加载动画
\\nFlutterNativeSplash.remove();\\n
\\n最终效果参考下图展示:
\\n开启gzip,压缩静态资源文件。
\\n gzip on;\\n gzip_min_length 1k;\\n gzip_comp_level 5;\\n gzip_vary on;\\n gzip_static on;\\n gzip_types text/plain text/html text/css application/javascript application/x-javascript text/xml application/xml application/xml application/json;\\n
\\n这里配置了压缩文件类型,如 text/plain, html,css, javascript json 等。
\\nGzip 压缩开启之后,可以在浏览器的开发者工具中,打开网络面板,查看响应头中,有一个 Content-Encoding: gzip 的字段,表示该文件已经被压缩。
\\n经过下表中的采样对比可以看到,压缩率还是很高的。
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n文件采样 | 压缩前 | 压缩后 | 压缩率 |
---|---|---|---|
main.dart.js | 3.1M | 903k | 28% |
vendor.js | 2.6M | 667k | 25% |
app.js | 1M | 185k | 18% |
也可以将静态资源放到 CDN 上,如阿里云等,通过 OSS 存储,然后配置 CDN 加速。需要注意的事,这要做好版本控制,否则会出现缓存问题。
\\n在Flutter
的布局体系中,Row
组件与Column
组件共同构成了Flex布局的核心双翼。这个看似简单的水平排列组件,实际上承载着界面设计的三大核心命题:空间分配策略
、动态布局管理
和跨平台适配方案
。
本文将通过六维知识体系,深度解构Row
布局,揭示其隐藏的关键布局规则,并配合企业级案例代码,帮助开发者从\\"会用\\"
到\\"精通\\"
,最终实现精准控制像素级布局的能力。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\n核心差异对比表
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n特性 | Row | Column |
---|---|---|
主轴方向 | 水平(X轴 ) | 垂直(Y轴 ) |
默认主轴尺寸 | max (充满宽度) | max (充满高度) |
交叉轴对齐基线 | 垂直方向基线 | 水平方向基线 |
Row(\\n textDirection: TextDirection.rtl, // 从右向左排列\\n verticalDirection: VerticalDirection.up, // 子组件底部对齐\\n children: [\\n Icon(Icons.star),\\n Text(\'Hello Flutter\'),\\n ],\\n)\\n
\\n效果图:
\\nmainAxisAlignment
控制子组件在主轴方向的排列方式:
\\nstart
:左对齐(默认)。center
:水平居中。end
:右对齐。spaceBetween
:首尾贴边,中间等距。spaceAround
:每个子组件上下等距。spaceEvenly
:所有间距相等。Column buildColumn1() {\\n return Column(\\n children: [\\n Row(\\n mainAxisAlignment: MainAxisAlignment.start,\\n children: [buildContainer(\\"start\\")],\\n ),\\n buildDivider(),\\n Row(\\n mainAxisAlignment: MainAxisAlignment.center,\\n children: [buildContainer(\\"center\\")],\\n ),\\n buildDivider(),\\n Row(\\n mainAxisAlignment: MainAxisAlignment.end,\\n children: [buildContainer(\\"end\\")],\\n ),\\n buildDivider(),\\n Row(\\n mainAxisAlignment: MainAxisAlignment.spaceBetween,\\n children: [\\n buildContainer(\\"spaceBetween\\"),\\n buildContainer(\\"spaceBetween\\"),\\n buildContainer(\\"spaceBetween\\"),\\n ],\\n ),\\n buildDivider(),\\n Row(\\n mainAxisAlignment: MainAxisAlignment.spaceAround,\\n children: [\\n buildContainer(\\"spaceBetween\\"),\\n buildContainer(\\"spaceBetween\\"),\\n buildContainer(\\"spaceBetween\\"),\\n ],\\n ),\\n buildDivider(),\\n Row(\\n mainAxisAlignment: MainAxisAlignment.spaceEvenly,\\n children: [\\n buildContainer(\\"spaceBetween\\"),\\n buildContainer(\\"spaceBetween\\"),\\n buildContainer(\\"spaceBetween\\"),\\n ],\\n ),\\n buildDivider(),\\n ],\\n );\\n}\\n\\nContainer buildContainer(String text) {\\n return Container(\\n width: 80,\\n height: 50,\\n color: Colors.blue,\\n alignment: Alignment.center,\\n child: Text(\\n text,\\n style: TextStyle(color: Colors.white, fontSize: 20),\\n ),\\n );\\n}\\n\\nDivider buildDivider() => Divider(height: 5, thickness: 3, color: Colors.red);\\n
\\n效果图:
\\ncrossAxisAlignment
控制子组件在垂直方向(交叉轴
)的对齐方式:
start
:顶部对齐。center
:垂直居中(默认)。end
:底部对齐。stretch
:垂直拉伸。baseline
:文字基线对齐(需设置textBaseline
)。Column buildColumn2() {\\n return Column(\\n children: [\\n SizedBox(\\n height: 80,\\n child: Row(\\n crossAxisAlignment: CrossAxisAlignment.start,\\n children: [buildContainer(\\"start\\")],\\n ),\\n ),\\n buildDivider(),\\n SizedBox(\\n height: 80,\\n child: Row(\\n crossAxisAlignment: CrossAxisAlignment.center,\\n children: [buildContainer(\\"center\\")],\\n ),\\n ),\\n buildDivider(),\\n SizedBox(\\n height: 80,\\n child: Row(\\n crossAxisAlignment: CrossAxisAlignment.end,\\n children: [buildContainer(\\"end\\")],\\n ),\\n ),\\n buildDivider(),\\n SizedBox(\\n height: 80,\\n child: Row(\\n crossAxisAlignment: CrossAxisAlignment.stretch,\\n children: [buildContainer(\\"stretch\\")],\\n ),\\n ),\\n buildDivider(),\\n SizedBox(\\n height: 80,\\n child: Row(\\n crossAxisAlignment: CrossAxisAlignment.baseline,\\n textBaseline: TextBaseline.alphabetic, // 必须指定基线类型\\n children: [\\n Text(\'Flutter\',\\n style: TextStyle(color: Colors.red, fontSize: 20)),\\n Text(\'Dart\', style: TextStyle(color: Colors.blue, fontSize: 16)),\\n ],\\n ),\\n ),\\n buildDivider(),\\n ],\\n );\\n}\\n
\\n效果图:
\\nFlex
子组件详解Expanded
:强制占据剩余空间Row(\\n children: [\\n Expanded(\\n flex: 2, // 空间分配权重\\n child: Container(color: Colors.red),\\n ),\\n Expanded(\\n flex: 1,\\n child: Container(color: Colors.blue),\\n ),\\n ],\\n)\\n
\\n效果图:
\\nFlexible
:弹性空间适配Flexible(\\n // fit: FlexFit.tight, // 强制填充(同Expanded)\\n fit: FlexFit.loose, // 根据内容自适应\\n child: Container(color: Colors.blue),\\n)\\n
\\n效果图:
\\nSpacer
:空白间距控制Row(\\n children: [\\n Icon(Icons.home),\\n Spacer(flex: 1), // 弹性空白\\n Icon(Icons.settings),\\n ],\\n)\\n
\\n效果图:
\\nFlex
组件对比表组件 | 特性 | 适用场景 |
---|---|---|
Expanded | 强制填满剩余空间 | 等分空间布局 |
Flexible | 根据内容自适应 | 动态收缩内容 |
Spacer | 占位空间分配 | 元素间距控制 |
CustomScrollView buildCustomScrollView() {\\n return CustomScrollView(\\n slivers: [\\n SliverToBoxAdapter(\\n child: Row(\\n mainAxisAlignment: MainAxisAlignment.spaceEvenly,\\n children: [\\n Container(\\n width: 100,\\n height: 50,\\n color: Colors.blue,\\n alignment: Alignment.center,\\n child: Text(\\n \\"Hello Flutter\\",\\n style: TextStyle(color: Colors.white),\\n ),\\n ),\\n Container(\\n width: 150,\\n height: 50,\\n color: Colors.red,\\n alignment: Alignment.center,\\n child: Text(\\n \\"Hello Dart\\",\\n style: TextStyle(color: Colors.white),\\n ),\\n ),\\n ],\\n ),\\n ),\\n SliverList(\\n delegate: SliverChildBuilderDelegate(\\n (_, index) => ListTile(title: Text(\'Item $index\'))),\\n )\\n ],\\n );\\n}\\n
\\n效果图:
\\nAnimatedBuilder(\\n animation: _animationController,\\n builder: (context, child) {\\n return Row(\\n children: [\\n SlideTransition(\\n position: Tween<Offset>(\\n begin: Offset(-1, 0),\\n end: Offset.zero,\\n ).animate(_animationController),\\n ),\\n ScaleTransition(\\n scale: _animationController,\\n child: Icon(Icons.menu),\\n ),\\n ],\\n );\\n },\\n),\\n
\\n优化前后对比表
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n场景 | 优化前(ms) | 优化后(ms) |
---|---|---|
100个动态子组件 | 48.2 | 12.5 |
复杂嵌套布局 | 76.8 | 22.3 |
优化策略:
\\nRow(\\n children: [\\n const HeaderWidget(), // 使用const避免重建\\n Expanded(\\n child: ListView.builder( // 分页加载\\n itemCount: _data.length,\\n itemBuilder: (_, index) => _buildItem(_data[index]),\\n ),\\n ),\\n if (_showFooter) // 条件渲染\\n const FooterWidget(),\\n ],\\n)\\n
\\n@override\\nvoid dispose() {\\n _animationController.dispose(); // 及时释放控制器\\n _scrollController.dispose();\\n super.dispose();\\n}\\n\\n// 图片内存优化\\nCachedNetworkImage(\\n imageUrl: \'https://example.com/image.jpg\',\\n memCacheWidth: 300,\\n maxHeight: 200,\\n)\\n
\\nRenderFlex
布局算法void performLayout() {\\n // 步骤1:确定主轴可用空间\\n final double maxMainSize = constraints.maxWidth;\\n \\n // 步骤2:计算flex空间分配\\n double allocatedSize = 0.0;\\n for (RenderBox child in _children) {\\n if (child is Flexible) {\\n totalFlex += child.flex;\\n }\\n }\\n \\n // 步骤3:二次遍历定位子组件\\n double childPosition = 0.0;\\n for (RenderBox child in _children) {\\n final FlexParentData childParentData = child.parentData;\\n child.layout(constraints, parentUsesSize: true);\\n childParentData.offset = Offset(childPosition, 0);\\n childPosition += child.size.width;\\n }\\n}\\n
\\n场景 | 时间复杂度 | 空间复杂度 |
---|---|---|
固定尺寸子组件 | O(n) | O(1) |
混合Flex 布局 | O(n^2) | O(n) |
动态尺寸计算 | O(n log n) | O(n) |
Row
通过组合多种布局策略实现灵活性:
Row(\\n textDirection: TextDirection.ltr,\\n verticalDirection: VerticalDirection.down,\\n mainAxisAlignment: MainAxisAlignment.start,\\n crossAxisAlignment: CrossAxisAlignment.center,\\n children: [...],\\n)\\n
\\nLayoutBuilder(\\n builder: (context, constraints) {\\n return Row(\\n children: constraints.maxWidth > 600 \\n ? _buildDesktopLayout()\\n : _buildMobileLayout(),\\n );\\n },\\n)\\n
\\nRow(\\n crossAxisAlignment: CrossAxisAlignment.center,\\n children: [\\n IconButton(\\n icon: Icon(Icons.menu),\\n onPressed: _openDrawer,\\n ),\\n Expanded(\\n child: Text(\\n \'Dashboard\',\\n style: TextStyle(fontSize: 20),\\n textAlign: TextAlign.center,\\n ),\\n ),\\n IconButton(\\n icon: Icon(Icons.search),\\n onPressed: _startSearch,\\n ),\\n ],\\n)\\n
\\n效果图:
\\nRow(\\n children: [\\n Expanded(\\n flex: 2,\\n child: Text(\'Product Name\', style: headerStyle),\\n ),\\n Expanded(\\n flex: 1,\\n child: Text(\'Price\', style: headerStyle),\\n ),\\n Expanded(\\n flex: 1,\\n child: Text(\'Stock\', style: headerStyle),\\n ),\\n ],\\n)\\n
\\nLayoutBuilder(\\n builder: (context, constraints) {\\n return constraints.maxWidth > 800\\n ? Row(\\n crossAxisAlignment: CrossAxisAlignment.start,\\n children: [\\n Expanded(child: _buildFormSection()),\\n VerticalDivider(width: 20),\\n Expanded(child: _buildPreviewSection()),\\n ],\\n )\\n : Column(\\n children: [\\n _buildFormSection(),\\n Divider(height: 20),\\n _buildPreviewSection(),\\n ],\\n );\\n },\\n)\\n
\\nRow
布局作为Flutter
水平布局的核心组件,其深度掌握需要系统化的知识构建:从基础属性的精确控制
到复杂场景的灵活应对
,从性能瓶颈
的突破到源码原理的透彻理解
,每个维度都构成完整知识体系的关键拼图。
开发者需要特别注意三个核心差异点:
\\nColumn
的垂直布局存在本质区别。textDirection
与verticalDirection
的组合使用带来的国际化适配能力。Flex
弹性布局与约束传递的相互作用机制。建议在实际开发中结合Performance
工具进行实时布局分析,通过Widget Inspector
可视化调试布局边界,并建立布局组件的性能评估指标体系。最终,通过将Row
布局与ScrollView
、AnimatedBuilder
等组件的深度整合,开发者能够构建出既美观又高性能的现代化用户界面。
\\n","description":"前言 在Flutter的布局体系中,Row组件与Column组件共同构成了Flex布局的核心双翼。这个看似简单的水平排列组件,实际上承载着界面设计的三大核心命题:空间分配策略、动态布局管理和跨平台适配方案。\\n\\n本文将通过六维知识体系,深度解构Row布局,揭示其隐藏的关键布局规则,并配合企业级案例代码,帮助开发者从\\"会用\\"到\\"精通\\",最终实现精准控制像素级布局的能力。\\n\\n操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。\\n\\n一、基础认知\\n1.1、坐标系与方向定义\\n\\n核心差异对比表\\n\\n特性\\tRow\\tColumn主…","guid":"https://juejin.cn/post/7474782353463164964","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-24T06:55:04.308Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/561ca4f179534f169f29d14163a50f7f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1740984904&x-signature=2x5P79zS4YkP91h3bn4Rftxl7vg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7d8cfc0e727f4214a4383b3b1bdb9745~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1740984904&x-signature=yLjc4HaMN3JK33T2wghcBThhcP8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/45991df8623b41ca8ae461687092b657~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1740984904&x-signature=IM800p6C5oUlRSIk5VYt0%2FojyfQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4917cbb1b0ab495a9de3bcd8dfb005de~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1740984904&x-signature=dL6Yof0PxsNsO2P9FqDvDzwWsU0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b82d47c895134e8b88c2a246d266dd09~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1740984904&x-signature=kbn4w12oy8aJYLwUZ21sMyWEuMA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/73ebc889882141bf899075ab2637887a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1740984904&x-signature=lKJiQqpSrgngzK1lYtWXyGY7OjU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b248a147fa524dbea731cb0128a65d15~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1740984904&x-signature=VkFr38URgkk47tKVmRgp7cATWVQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4a992fb9e13142d7a7f3a5958a6a7f4f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1740984904&x-signature=A5aFPbEQHMnyfV7djF6DCnXsuh0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1d2a142b9ba04469bfa218025373f329~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1740984904&x-signature=wHTQdKNmTi9YbzRNPQlgs3Sig0I%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 教程(一)Flutter 简介","url":"https://juejin.cn/post/7474780149554610212","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
Flutter是谷歌公司推出的一套跨平台的开源用户界面(User Interface,UI)框架,同时支持Android App与iOS App开发。
\\n目前业界有很多成熟的跨平台技术,但是这些技术几乎都存在一定的缺陷,例如:
\\n所以,到目前为止,开发人员依然需要准备两套代码,分别运行在Android端与iOS端,这样不仅增加了开发的成本,还要维护两端代码,耗时、耗力。而Flutter的出现,让这些开发问题有所改善。
\\n如上图所示,Flutter 架构分成两部分,一部分是框架,另一部分是引擎。
\\n框架是由纯Dart语言实现的,包括UI、文本、图片和按钮等Widgets,以及Rendering(渲染)、Animation(动画)、Gestures(手势)等层。各层的作用如下:
\\nFlutter引擎是由纯C++实现的SDK,主要包括Skia、Dart和Text。Framework层中所有的UI库都会调用引擎层。各层的作用如下:
\\nFlutter 项目的创建可以看这篇文章。项目创建完成后的目录结构如下图所示:
\\n各个目录及其文件的作用如下:
\\nflutter3.x-hotel:2025实战-跨端Flutter3.27+Dart3.6+Getx
仿携程/同程旅游app酒店客房预约查询系统。实现了首页、预订酒店搜索模块、酒店列表/详情、动态、订单、聊天消息、我的等功能。
酒店预订模块包含热门城市地址/位置品牌选择、入住离店日期区间选择、价格/星级等功能。
\\n筛选下拉框采用自定义组件实现功能,使用SizeTransition
和FadeTransition
组件实现下拉动画效果。
flutter3-trip:2025自研Flutter3.27民宿预约酒店App系统 - bilibili
\\n项目还增加了动态/探索模块,支持tab滑动切换,保留页面滚动位置状态。
\\n如果想要了解更多的技术实现细节可以去看看下面这篇分享文章。
\\n\\n往期热门Flutter3.x跨平台实战项目实例。
\\nFlutter3.27实战抖音App商城|flutter3+getX短视频+直播+聊天实例
\\nflutter3-winchat桌面端exe聊天实例|Flutter3+Dart3+Getx仿微信Exe程序
\\nflutter3+dart3聊天室|Flutter3跨平台仿微信App语音聊天/朋友圈
","description":"flutter3.x-hotel:2025实战-跨端Flutter3.27+Dart3.6+Getx仿携程/同程旅游app酒店客房预约查询系统。实现了首页、预订酒店搜索模块、酒店列表/详情、动态、订单、聊天消息、我的等功能。 酒店预订模块包含热门城市地址/位置品牌选择、入住离店日期区间选择、价格/星级等功能。\\n\\n筛选下拉框采用自定义组件实现功能,使用SizeTransition和FadeTransition组件实现下拉动画效果。\\n\\nflutter3-trip:2025自研Flutter3.27民宿预约酒店App系统 - bilibili\\n\\n项目框架目录…","guid":"https://juejin.cn/post/7474532273327144994","author":"xiaoyan2015","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-24T02:25:12.439Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2b85e0b2968540ebb5d98844e2b2be09~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgeGlhb3lhbjIwMTU=:q75.awebp?rk3s=f64ab15b&x-expires=1740968712&x-signature=gyX8End%2F4uMYckExQ0g3zZVYfXI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/cb62c7f22a10463ca60d4ebaa4cee3dd~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgeGlhb3lhbjIwMTU=:q75.awebp?rk3s=f64ab15b&x-expires=1740968712&x-signature=CcHLB1gq5sUeBy2%2B2QBSVKOryLU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7e68600c548f467da7272776f48f460d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgeGlhb3lhbjIwMTU=:q75.awebp?rk3s=f64ab15b&x-expires=1740968712&x-signature=gTEHuETakQyxur6oVQEY%2F6uSWlQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/cd85aa0b32cb444b82a10547b7a3b4c0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgeGlhb3lhbjIwMTU=:q75.awebp?rk3s=f64ab15b&x-expires=1740968712&x-signature=saeSA%2FMCOoAX5nCbzeeYZz6eSHM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/8e72b8b1ffb546bfbd6ce24634b6944f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgeGlhb3lhbjIwMTU=:q75.awebp?rk3s=f64ab15b&x-expires=1740968712&x-signature=xEMUKgoHBkUFzWD2MDGba0OxE%2F0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/80da2230867047238911a67c5580fb53~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgeGlhb3lhbjIwMTU=:q75.awebp?rk3s=f64ab15b&x-expires=1740968712&x-signature=GFdy8ieTAtg%2B51SN2zc7YwhAbCU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/878fc293df8a4a45b39a29a67dcd87da~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgeGlhb3lhbjIwMTU=:q75.awebp?rk3s=f64ab15b&x-expires=1740968712&x-signature=p3LPtasqHU1sgV1B8fYNDx%2BqDkc%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["前端","Flutter","Dart","Android"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 上的 Platform 和 UI 线程合并是怎么回事?它会带来什么?","url":"https://juejin.cn/post/7474503566154219560","content":"Flutter 在 3.29 发布了一个「重大」调整:从 3.29 开始,Android 和 iOS 上的 Flutter 将在应用的主线程上执行 Dart 代码,并且不再有单独的 Dart UI 线程
\\n也许一些人对于这个概念还比较陌生,有时间可以看看以前发过的 《深入理解 Dart 异步实现机制》 的相关内容,这里面主要涉及 isolate、 Thread、Runner 等概念。
\\n简单说就是:
\\nmain
就是运行在 root isolate 里,也是我们 Dart 代码的「主线程」而在 Android 和 iOS 上,以前会为 UI,GPU,IO 分别创建一个线程,其中 UI Task Runner 就是 Dart root isolate,也就是 Dart 主线程, Platform Runner 其实就是设备平台自己的主线程。
\\n所以,在过去 Flutter 的 UI Runner 和 Android/iOS 平台的 Platform Runner 是处于不同线程,其中 Dart 的 root isolate 会在被关联到 UITaskRunner 上。
\\n所以在过去 Flutter 里会有异步 platform channels 的存在,因为 UI Runner 和 Platform Runner 分属不同线程,所以 Dart 和 Native 互相调用时需要序列化和异步消息传递。
\\n而在 3.29 里,作为改进移动平台上 Native 和 Dart 互操作系列调整中的一部分,两个线程被合并了,说人话就是: UI Runner = Platform Runner
:
是的,默认情况下现在 merged_platform_ui_thread
会是 true
,也就是 UI Runner 现在等同于 Platform Runner ,那么自然 Dart 的 root isolate 就关联到 Platform Runner 上:
另外,过去 Dart 的 root isolate 是在 SetMessageHandlingTaskRunner
的时候关联上 UI Runner 的,而现在是直接 post_directly_to_runner :
那为什么可以这样简单切换?实际上就是我们前面讲过的, Engine 并不在乎 Runner 具体跑在哪个线程,对于 Flutter Engine 而言,它可以往 Runner 里面提交 Task,只要最终有执行的地方就行了。
\\n对于 Dart 来说,在内部 VM 会使用 dart::ThreadPool
这样的线程池来管理系统线程,并且代码是围绕 dart::ThreadPool::Task
概念构建的,而不是围绕系统线程:
\\n\\n例如用于处理 isolate message 的 event loop 的默认实现,实际就是没有一个专用的事件循环线程,而是在有新消息到达时将
\\ndart::MessageHandlerTask
发布到线程池。
同时,由于过去 UI 和 Platform 线程是分开的,那时的 UI Runner 都是通过独立的 MessageLoopTaskQueues
来处理 microtask 的,而现在线程合并后,UI Runner 变成了 Platform Runner ,自然也就没有关联的任务队列,所以需要在运行任务后需要手动刷新 microtask 。
\\n\\nmicrotask 就是 isolate 事件循环队列任务的一种,具有更高优先级。
\\n
另外,基本上所有 PostTask 都变成了 RunNowOrPostTask ,主要也是通过判断 MessageLoop 的初始化情况来判断执行位置:
\\n这里再结合前面我们 merged 两个线程时 platform runner 的初始化逻辑,可以看到 MessageLoop 不会是空,所以 IsInitializedForCurrentThread
会是 true
,也就是在当前线程直接运行 task()
:
另外在 iOS 上也是同样道理,直接用了当前的 MessageLoop :
\\n\\n\\n其实合并线程后,Flutter 单独的光栅线程还是在的,所以一般来说,并不用担心 Flutter 的动画会「直接」影响到 Native UI 线程造成卡顿。
\\n
那么合并线程的好处是什么?最直接的就是 iOS 可以做到支持渲染 PlatformView 而无需合并光栅线程。
\\n另外一个情况就是文本输入,因为在此之前都是需要通过 Platform Channel 进行通信,这个异步行为造成了许多问题,例如;
\\n\\n\\n在 iOS 上的 IME 生成快速事件序列,然后在 UI 线程处理事件并发送回平台线程之前读取文本,很多时候逻辑上是需要同步响应,但是由于 Platform Channel 的限制,最终需要通过一些额外成本来达成这个需求(不断在事件处理中抽取 CFRunLoop)。
\\n
Platform Channel 的核心在于异步,当平台的文本输入需要某些东西(选择坐标、当前文本)时,它需要接口可以立即给出答案,而通过 Platform Channel 只能是主动将所有状态推送给客户,以便在需要时能够用到。
\\n而如果合并到一个线程上,那么 FFI 就可以同步执行平台交互,可以简单地调用 dart 代码并立即返回答案,甚至在文本输入上可以更好保留住某些平台差异的效果,而不是像现在一样只能在 Channel 抽象出统一的文本输入 API。
\\n\\n\\n还可以减少文本和状态在内存里的多处缓存的情况。
\\n
另外还有在 Android WebView 的拦截响应上,如 shouldOverrideUrlLoading
需要「直接」同步响应返回一个结果的情况变得简单。
当然,也许这个改动会带来一些负面影响,例如插件如果没适配好,可能会导致某些行为对平台线程造成 ANR 等极端情况,所以如果你希望延迟这个逻辑,可以增加以下配置:
\\n<meta-data\\n android:name=\\"io.flutter.embedding.android.DisableMergedPlatformUIThread\\"\\n android:value=\\"true\\" />\\n
\\n这个配置会执行 --no-enable-merged-platform-ui-thread
,从而修改 settings.merged_platform_ui_thread
的标志位为 false 。
当然,在整个 Flutter 团队的目标里,完全剔除 platform/message channels 是必然的方向,未来整个异步 channel 肯定会被彻底“消灭” ,所以合并线程对于 Flutter 来说是大势所趋,和 RN 一样,同步调用和互操作是跨平台的趋势。
\\n学习一门新的语言,最好的方法就是从自己熟悉的语言中找不同。这篇文章就从基本语法、逻辑判断和控制、属性、函数、接口、抽象类、类、集合、IO、泛型、异常、多线程、反射角度看 Kotlin 与 Dart 的区别。
\\n在 Dart 中,变量需要显示声明类型,代码如下所示:
\\n // 整型 age,表示年龄的数值\\n int age = 2;\\n // 浮点型 weight,表示体重的数值\\n double weight = 4.5;\\n
\\n如果想要主动推动类型,可以使用关键字 var
,它和 kotlin 中的 var
的作用是一致的。在 Dart 中还有一个关键字 dynamic
,它是一种特殊的类型,使用 dynamic
声明的变量可以在运行时存储任何类型的值,并且可以随时改变其存储的值的类型。代码示例如下:
void main() {\\n // 使用 var 声明变量,初始值为字符串,Dart 推断其类型为 String\\n var message = \'Hello, Dart!\';\\n print(\'message 的类型: ${message.runtimeType}\');\\n\\n // 使用 dynamic 声明变量\\n dynamic value;\\n\\n // 赋值为字符串\\n value = \'Hello\';\\n print(\'value 的类型: ${value.runtimeType}\');\\n\\n // 赋值为整数\\n value = 123;\\n print(\'value 的类型: ${value.runtimeType}\');\\n\\n // 调用 value 的方法,编译时不会检查,运行时根据实际类型调用\\n if (value is int) {\\n print(value + 1); \\n }\\n}\\n
\\n在 Dart 中,函数的声明和 Java 类似,代码示例如下:
\\ndouble bmi(double height, double wight) {\\n // 具体算法\\n double result = wight / (height * height);\\n return result;\\n}\\n
\\nkotlin 中支持给函数的参数设置默认值,调用时也可以使用形参传入数值。在 Dart 中通过命名参数实现,命名参数需要加上 {}
,代码示例如下:
double bmi({\\n required double height, // required 关键字表示该入参必须传入;\\n double weight = 65, // 可以用 `=` 提供参数的默认值\\n}) {\\n // 具体算法\\n double result = weight / (height * height);\\n return result;\\n}\\n\\nvoid main() {\\n // 调用时通过 : 设置参数值\\n double toly = bmi(weight: 70, height: 1.8);\\n}\\n
\\n在 Dart 中还有一种参数是位置参数,它需要加上 []
,代码示例如下:
// 位置参数,必须要有默认值\\ndouble bmi([double height = 1.79, double weight = 65]) {\\n // 具体算法\\n double result = weight / (height * height);\\n return result;\\n}\\n\\nvoid main() {\\n double toly = bmi(1.8,70);\\n}\\n
\\nDart 的 if 和 Java 一样,无法获取 if 表达式的返回值。代码示例如下:
\\nvoid main() {\\n double height = 1.18;\\n // 布尔值可以通过运算获得\\n bool free = height < 1.2;\\n if(free){\\n print(\\"可免费入园\\");\\n }else{\\n print(\\"请购买门票\\");\\n }\\n}\\n
\\nDart 的 switch 也和 Java 一样,代码示例如下:
\\nvoid main() {\\n String mark = \'A\';\\n switch (mark) {\\n case \'A\':\\n print(\\"优秀\\");\\n break;\\n case \'B\':\\n print(\\"良好\\");\\n break;\\n case \'C\':\\n print(\\"普通\\");\\n break;\\n case \'D\':\\n print(\\"较差\\");\\n break;\\n case \'E\':\\n print(\\"极差\\");\\n break;\\n default:\\n print(\\"未知等级\\");\\n }\\n}\\n
\\nDart 中 for 的语法也和 Java 中一样,代码示例如下:
\\nvoid main() {\\n int sum = 0;\\n for (int i = 0; i < 5; i = i + 1) {\\n sum = sum + i;\\n print(\\"第 $i 次执行,sum = $sum\\");\\n }\\n}\\n\\n----\x3e[输出结果]----\\n第 0 次执行,sum = 0\\n第 1 次执行,sum = 1\\n第 2 次执行,sum = 3\\n第 3 次执行,sum = 6\\n第 4 次执行,sum = 10\\n
\\nDart 中的 while 语法也和 Java 中一样,代码示例如下:
\\nvoid main() {\\n int i = 0;\\n while (i < 10) {\\n double bmiValue = bmi(1.75, 65); // 示例:身高1.75m,体重65kg\\n print(\\"BMI: $bmiValue\\\\n\\");\\n i++;\\n }\\n}\\n
\\nDart 中也同样支持 break
和 continue
,它们的作用和在 Java 中是一样的。
Dart 中的加减乘除的运算符和 Java 一样,不同的是 Dart 增加了一个 ~/
运算符,用来求商,代码示例如下:
void main() {\\n print(10 % 3);//1 余\\n print(10 ~/ 3);//3 商\\n}\\n
\\nDart 的逻辑运算符 &&
、||
、!
也和 Java 一样,代码示例如下:
void main() {\\n // 公园是否开放\\n bool open = true;\\n // 是否免费\\n bool free = false;\\n\\n // 公园是否免费进入\\n bool freeEnter = open && free;\\n}\\n
\\n默认情况下,==
可以判断是否指向同一个实例,但是大部分数据类型都会重写该方法,让其判断值是否相同。因此Dart 提供了 identical
方法来判断两个对象是否指向同一个实例。
在 Dart 中,左移、右移的运算符 也和 Java 一样,代码示例如下:
\\n在 Dart 中,没有 public、private 等访问控制符。在 Dart 中,默认情况下,所有的类、变量、方法、函数等都是公开的,即可以在整个项目的任何地方访问。如果你想要限制访问,则需要在在标识符(类、变量、方法等)前加下划线 _
来表示该成员是私有的,私有成员只能在定义它们的库(文件)内部访问。代码示例如下:
// 定义一个包含私有成员的类\\nclass PrivateExample {\\n // 私有变量\\n String _privateVariable = \'This is a private variable\';\\n\\n // 私有方法\\n void _privateMethod() {\\n print(\'This is a private method\');\\n }\\n\\n // 公开方法,用于在类内部访问私有成员\\n void accessPrivateMembers() {\\n print(_privateVariable);\\n _privateMethod();\\n }\\n}\\n
\\n在 Dart 中可以使用 final
和 const
定义常量。const与final的共同点是初始化后都无法更改。两者的区别是,const值在编译时会检查值,而final值在运行时才检查值。因此不能给const定义的常量赋值为不确定的值。代码示例如下:
const time=\'2020-05-03\';\\nconst time=DataTime.now();//这行代码在编译器中会报错\\nfinal time=\'2020-05-03\';\\nfinal time=DataTime.now();//这行代码不会报错\\n
\\nDart 和 Kotlin 一样,一切都是对象,没有 Java 中的基本数据类型。但是 Dart 的数据类型与 Kotlin 有所不同。
\\nDart 和 Kotlin 类似,也支持可空类型。不同的是,在 Dart 中使用 !
来强制把可空类型转换为非可空类型;同时使用 ??
来为可空类型提供一个默认值。代码示例如下:
int? nullableInt = null;\\n// 使用安全调用操作符,当 nullableInt 为 null 时,不会调用方法,直接返回 null\\nint? result = nullableInt?.toDouble()?.toInt();\\nprint(\'result 的值: $result\');\\n\\nnullableInt = nullableInt ?? 0;\\nprint(\'nullableInt 的值: $nullableInt\');\\n
\\n需要注意的是,Dart 的 runtimeType
方法获取的是实际运行时的类型。比如对于 int? a
变量,它只会返回 int
类型或者 Null
类型。
在 Dart 语言中,数字有两大类型: 整型(整数) int 和浮点型(小数) double ,没有 float 类型。定义变量的语法是 类型名 变量名 = 值,代码示例如下:
\\nvoid main(){\\n // 整型 age,表示年龄的数值\\n int age = 2;\\n // 浮点型 weight,表示体重的数值\\n double weight = 4.5;\\n}\\n
\\n\\n\\n虽然 int、double 看上去像是 java 中的基本数据类型,但实际上它们都是继承 num 类。
\\n
Dart 的布尔类型使用 bool 表示,代码示例如下:
\\nvoid main() {\\n // 直接赋值\\n bool enable = true;\\n double height = 1.18;\\n // 布尔值可以通过运算获得\\n bool free = height < 1.2;\\n}\\n
\\n在 Dart 中有3种创建字符串的方式:
\\n代码示例如下:
\\nvoid main() {\\n // 使用双引号创建字符串\\n String doubleQuotedString = \\"Hello, Dart!\\";\\n print(doubleQuotedString);\\n\\n // 使用三个单引号创建多行字符串\\n String multiLineSingleQuoted = \'\'\'\\n 这是一个多行字符串示例。\\n 它可以包含多行文本。\\n 这里是第三行。\\n \'\'\';\\n print(multiLineSingleQuoted);\\n\\n // 普通字符串,需要对反斜杠进行转义\\n String normalString = \'这是一个包含路径的字符串:C:\\\\\\\\Users\\\\\\\\Documents\';\\n print(normalString);\\n\\n // 原始字符串,反斜杠无需转义\\n String rawString = r\'这是一个包含路径的原始字符串:C:\\\\Users\\\\Documents\';\\n print(rawString);\\n\\n}\\n
\\n同时 String 还可以通过 +
拼接,也可以使用 $变量名
在字符串内插入变量值,这个和 kotlin 的语法是一样的。代码示例如下:
void main() {\\n String str1 = \\"Hello\\" + \\"World\\";\\n String str2 = \\"$str1 Dart\\";\\n}\\n
\\n在 Dart 中,列表表示集合,也表示数组,代码示例如下:
\\nvoid main() {\\n String str1 = \\"Hello\\" + \\"World\\";\\n String str2 = \\"$str1 Dart\\";\\n}\\n
\\n列表中的常用方法有:add()、length()、remove()、insert()、indexOf()、sublist()、forEach()、shuffle()等,代码示例如下:
\\nvar list=[\\"apple\\",\\"banana\\",\\"cherry\\"];\\nprint(list.length);//输出列表的长度\\nlist.add(\\"bayberry\\");//末尾添加\\"bayberry字符串\\nprint(list);\\nlist.remove(\\"apple\\")//删除apple字符串\\nprint(list);\\nlist.insert(1, \'dates\');//在1索引插入dates字符串\\nprint(list);\\nprint(list.indexOf(\\"cherry\\"));//获取cherry字符串所在位置\\nprint(list.sublist(2));//去除前两个元素后的新的列表\\nlist.forEach(print);//遍历并输出列表\\nlist.shuffle();//打乱列表顺序\\nprint(list);\\n
\\n示例如下:
\\nMap<int, String> numMap = {\\n 0: \'zero\',\\n 1: \'one\',\\n 2: \'two\',\\n};\\nprint(numMap);\\nnumMap.remove(1);\\nprint(numMap);\\n\\n----\x3e[控制台输出]----\\n{0: zero, 1: one, 2: two}\\n{0: zero, 2: two}\\n\\n
\\n示例如下:
\\nSet<int> numSet = {1, 9, 9, 4};\\nprint(numSet);\\n\\n----\x3e[控制台输出]----\\n{1, 9, 4}\\n
\\nSet 最重要的特征是可以进行集合间的运算,这点 List 列表是无法做到的。两个集合间通过 difference
、union
、intersection
方法可以分别计算差集、并集、交集。计算的结果也是一个集合,代码示例如下:
Set<int> a = {1, 9, 4};\\nSet<int> b = {1, 9, 3};\\nprint(a.difference(b));// 差集\\nprint(a.union(b)); // 并集\\nprint(a.intersection(b)); // 交集\\n\\n----\x3e[控制台输出]----\\n{4}\\n{1, 9, 4, 3}\\n{1, 9}\\n
\\n在 Dart中,runtimeType
可以获取对象类型;is
和 is!
可以判断类型;而 as
可以强制把类型转换。代码示例如下:
void main() {\\n var obj1 = \'Hello, Dart!\';\\n var obj = \'Hello\';\\n \\n // 判断 obj1 是否为 String 类型\\n bool isString = obj1 is String;\\n print(\'obj1 is String: $isString\');\\n\\n // 判断 obj 是否不是 int 类型\\n bool isNotInt = obj is! int;\\n print(\'obj is not int: $isNotInt\');\\n}\\n
\\n在 Dart 中,静态变量和静态方法和 Java 一样都被 static
修饰,代码示例如下:
class Cat{\\n // 静态变量\\n static String TAG = \'Cat\';\\n \\n static String eatFish(){\\n print(\'猫天生喜欢吃鱼!\');\\n }\\n}\\n
\\nDart 的构造函数和 Java 类似,代码示例如下:
\\nclass Human {\\n String name = \'\';\\n double weight = 0;\\n double height = 0;\\n\\n Human(String name,double weight,double height){\\n this.name = name;\\n this.weight = weight;\\n this.height = height;\\n }\\n \\n // 上面的方法可以简写成下面这样\\n Human(this.name, this.weight, this.height)\\n}\\n
\\n由于 Dart 不支持函数重载,因此推出了命名构造函数来解决这个问题。代码示例如下:
\\nclass Human {\\n String name = \'\';\\n double weight = 0;\\n double height = 0;\\n int age = 0;\\n\\n Human(String name, double weight, double height) {\\n this.name = name;\\n this.weight = weight;\\n this.height = height;\\n }\\n\\n Human.withAge(this.name, this.weight, this.height, this.age);\\n}\\n
\\n在构造函数中还可以加上 factory
关键字,这表示工厂构造函数。工厂构造函数最大的特点是可以手动返回一个对象。代码示例如下:
class Person {\\n String name;\\n\\n static final Map<String, Person> cache = {};\\n\\n Person(this.name);\\n\\n factory Person.getSingle(String name) {\\n if (cache.containsKey(name)) {\\n return cache[name] as Person;\\n } else {\\n cache[name] = new Person(name);\\n return cache[name] as Person;\\n }\\n }\\n}\\n
\\n和 Kotlin 类型,Dart 也可以把函数作为变量来传递,代码示例如下:
\\n// 定义一个高阶函数,接受一个函数作为参数\\nint calculate(int a, int b, int Function(int, int) operation) {\\n return operation(a, b);\\n}\\n\\n// 定义加法函数\\nint add(int a, int b) {\\n return a + b;\\n}\\n\\n// 定义减法函数\\nint subtract(int a, int b) {\\n return a - b;\\n}\\n\\nvoid main() {\\n int num1 = 10;\\n int num2 = 5;\\n\\n // 调用 calculate 函数并传递 add 函数\\n int sum = calculate(num1, num2, add);\\n print(\'加法结果: $sum\');\\n\\n // 调用 calculate 函数并传递 subtract 函数\\n int difference = calculate(num1, num2, subtract);\\n print(\'减法结果: $difference\');\\n}\\n
\\n在 Dart 中,使用 =>
用来表示 {}
,常用于单行函数中,代码示例如下:
int squareWithArrow(int num) => num * num;\\n
\\n级联运算符 (.., ?..) 可以让你在同一个对象上连续调用多个对象的变量或方法,它类似于 Kotlin 中的 apply{}
和 ?.apply{}
。代码示例如下:
class Person {\\n String name = \'\';\\n int age = 0;\\n\\n void introduce() {\\n print(\'我叫 $name,今年 $age 岁。\');\\n }\\n}\\n\\nvoid main() {\\n Person person = Person()\\n ..name = \'张三\'\\n ..age = 25\\n ..introduce();\\n}\\n
\\n在 Dart 中,没有 interface
关键字来定义接口。但是每一个类(除了 int、String 这些类型类不行)外都可以通过 implements
来作为接口来实现。
在 Dart 中,抽象类也是用 abstract
来声明的,抽象类可以用 implements
作为接口来实现;或者使用 extends
来作为父类来继承。
代码示例如下:
\\n// 定义一个抽象类作为接口\\nabstract class Shape {\\n // 没有实现的是抽象方法,不需要加 abstract 修饰\\n double area();\\n \\n void draw() {\\n print(\'绘制一个形状\');\\n }\\n}\\n\\n// implements 表示子类是作为接口来实现的\\n// 因此不能使用 Shape 中的任何属性和方法,同时必须重写所有的属性和方法\\nclass Circle implements Shape {\\n double radius;\\n\\n Circle(this.radius);\\n\\n @override\\n double area() {\\n return 3.14 * radius * radius;\\n }\\n\\n // 必须实现\\n @override\\n void draw() {\\n print(\'绘制一个半径为 $radius 的圆\');\\n }\\n}\\n\\n// extends 表示子类是作为父类来实现的\\n// 因此只需要实现抽象方法,同时可以使用Shape 中的属性和方法\\nclass Rectangle extends Shape {\\n double width;\\n double height;\\n\\n Rectangle(this.width, this.height);\\n\\n @override\\n double area() {\\n return width * height;\\n }\\n \\n}\\n
\\n在 Kotlin 中,创建对象不需要 new
关键字,而在 Dart 中,new
是可选的。
在 Dart 中,一般的继承和 Java 一样。它们都是使用 extends
继承父类,使用 super
调用父类的方法。代码示例如下:
// 定义父类\\nclass Animal {\\n String name;\\n\\n // 父类构造函数\\n Animal(this.name);\\n\\n // 父类方法\\n void eat() {\\n print(\'$name 正在进食\');\\n }\\n}\\n\\n// 定义子类,继承自 Animal 类\\nclass Dog extends Animal {\\n // 子类构造函数,调用父类构造函数\\n Dog(String name) : super(name);\\n\\n // 子类特有的方法\\n void bark() {\\n print(\'$name 正在汪汪叫\');\\n }\\n}\\n\\nvoid main() {\\n // 创建 Dog 类的实例\\n Dog dog = Dog(\'旺财\');\\n // 调用从父类继承的方法\\n dog.eat();\\n // 调用子类特有的方法\\n dog.bark();\\n}\\n
\\n在 Kotlin 中,我们使用继承是为了复用父类的代码。而在 Dart 中,除了继承这种方式外,还提供了 mixin(混合)来让你在不使用继承的情况下,将一组方法和属性添加到多个类中。mixin
主要用于实现代码的复用和组合,避免了多重继承带来的复杂性和问题。代码示例如下:
// 定义一个 mixin\\nmixin CanFly {\\n void fly() {\\n print(\'正在飞行\');\\n }\\n}\\n\\n// 定义另一个 mixin\\nmixin CanSwim {\\n void swim() {\\n print(\'正在游泳\');\\n }\\n}\\n\\n// 定义一个类,使用 with 关键字应用 mixin\\nclass Bird with CanFly {\\n void chirp() {\\n print(\'鸟儿在叽叽喳喳叫\');\\n }\\n}\\n\\n// 定义另一个类,应用多个 mixin\\nclass Duck with CanFly, CanSwim {\\n void quack() {\\n print(\'鸭子在嘎嘎叫\');\\n }\\n}\\n\\nvoid main() {\\n Bird bird = Bird();\\n bird.fly();\\n bird.chirp();\\n\\n Duck duck = Duck();\\n duck.fly();\\n duck.swim();\\n duck.quack();\\n}\\n
\\n需要注意,被 mixins 的类有如下限制:
\\n和 Kotlin 不同,Dart 不支持内部类。
\\n在 Dart 中没有 Kotlin 中的 object
关键字来直接声明单例。Dart 中的单例声明如下:
class Singleton {\\n // 静态私有变量,用于存储单例实例\\n static final Singleton _instance = Singleton._internal();\\n\\n // 私有构造函数,防止外部直接创建实例\\n Singleton._internal();\\n\\n // 静态方法,用于获取单例实例\\n static Singleton get instance => _instance;\\n\\n void doSomething() {\\n print(\'单例对象正在执行操作...\');\\n }\\n}\\n\\nvoid main() {\\n Singleton singleton1 = Singleton.instance;\\n Singleton singleton2 = Singleton.instance;\\n\\n // 验证两个实例是否相同\\n print(\'singleton1 和 singleton2 是否为同一个实例: ${identical(singleton1, singleton2)}\');\\n\\n singleton1.doSomething();\\n}\\n
\\nDart 中的枚举的定义和 Kotlin 类似,代码示例如下:
\\n// 定义一个枚举类型表示一周的天数\\nenum Weekday {\\n monday(name: \\"星期一\\"),\\n tuesday(name: \\"星期二\\"),\\n wednesday(name: \\"星期三\\"),\\n thursday(name: \\"星期四\\"),\\n friday(name: \\"星期五\\"),\\n saturday(name: \\"星期六\\"),\\n sunday(name: \\"星期天\\");\\n\\n const Weekday({required this.name});\\n final String name;\\n}\\n
\\n更详细的用法可以看 Dart 的枚举类型的高阶用法Dart 的枚举你知道多少?
\\n不同于 Kotlin 通过import导入各种类型的开发包,Dart 将这些导入的开发包称为库,每段Dart程序都是由被称为库的模块化单元组成的。在 Dart 中也是通过 import
关键字来导入的,代码示例如下:
import \'dart:math\'; // 引入内置 math 库,对于 Dart 内置的库,使用 `dart:xxxxxx` 的形式\\nimport \'package:test/test.dart\'; // 引入包管理器中的库\\nimport \'lib/test.dart\'; // 引入自己写的库\\n
\\n在 Dart 中的 IO 操作是使用的 dart:io
库,代码示例如下:
import \'dart:io\';\\n\\nvoid main() async {\\n try {\\n // 创建一个 File 对象,指定要读取的文件路径\\n File file = File(\'example.txt\');\\n // 以 UTF-8 编码读取文件的全部内容\\n String content = await file.readAsString();\\n print(\'文件内容:$content\');\\n } catch (e) {\\n print(\'读取文件时出错:$e\');\\n }\\n}\\n
\\n更多IO操作可以看 Flutter系列之Dart文件IO操作_dart.io delete file-CSDN博客
\\nDart 中泛型也是使用 <T>
来表示,也可以通过 extends
来限制。但是不同于 Kotlin,没有星投影、\\n逆变等操作。代码示例如下:
// 定义一个抽象类\\nabstract class Shape {\\n void draw();\\n}\\n\\n// 定义一个矩形类,继承自 Shape\\nclass Rectangle extends Shape {\\n @override\\n void draw() {\\n print(\'绘制矩形\');\\n }\\n}\\n\\n// 定义一个圆形类,继承自 Shape\\nclass Circle extends Shape {\\n @override\\n void draw() {\\n print(\'绘制圆形\');\\n }\\n}\\n\\n// 定义一个泛型类,限制类型参数必须是 Shape 或其子类\\nclass ShapeContainer<T extends Shape> {\\n T shape;\\n\\n ShapeContainer(this.shape);\\n\\n void drawShape() {\\n shape.draw();\\n }\\n}\\n\\nvoid main() {\\n Rectangle rectangle = Rectangle();\\n ShapeContainer<Rectangle> rectangleContainer = ShapeContainer(rectangle);\\n rectangleContainer.drawShape();\\n\\n Circle circle = Circle();\\n ShapeContainer<Circle> circleContainer = ShapeContainer(circle);\\n circleContainer.drawShape();\\n\\n // 下面这行代码会报错,因为 int 不是 Shape 或其子类\\n // ShapeContainer<int> intContainer = ShapeContainer(123);\\n}\\n
\\nDart是一门单线程的语言,不支持多线程。具体可以看 Flutter-Dart中的异步和多线程讲解
\\n和 Java 异常处理类似,Dart 使用 throw 抛出异常、try-catch-finally 来处理异常。不同的是,在 Dart 中,我们需要使用 on
关键字来指定异常的类型;也可以使用 rethrow
关键字在我们捕获异常时,再把这个异常抛出。代码示例如下:
try {\\n // throw Error();\\n throw Exception(\'this is exception error\');\\n } on Exception catch (e) {\\n print(\'this is Unknown exception $e\');\\n } catch (e,s) { // catch 方法有两个参数,第一个参数是抛出的异常对象,第二个参数是栈信息\\n print(\'No specified type, handles all error $e\');\\n print(\'Stack trace:\\\\n $s\');\\n }\\n\\n
\\n在 Dart 中,注解的样式和 Kotlin 一样都是 @注解名
。不同的是,Dart 中自定义注解需要创建一个类。类名通常以Annotation
为后缀。自定义注解类可以包含任意数量的参数,这些参数可以在使用注解时传入。要定义自定义注解,需要创建一个带有const
构造函数的类。这个类可以有任意数量的属性,但它们必须是编译时常量。代码示例如下:
class MyAnnotation {\\n final String description;\\n const MyAnnotation(this.description);\\n}\\n\\n// 使用自定义注解注解类\\n@MyAnnotation(\'这是一个注解类的元数据\')\\nclass MyClass {\\n // ...\\n}\\n// 使用自定义注解注解函数\\n@MyAnnotation(\'这是一个注解函数的元数据\')\\nvoid myFunction() {\\n // ...\\n}\\n// 使用自定义元素据注解变量\\n@MyAnnotation(\'这是一个注解变量的元数据\')\\nint myVariable;\\n
\\n在这个示例中,我们定义了一个名为MyAnnotation
的自定义注解。这个注解有一个属性description
,用于存储与注解关联的文本描述。我们还为这个类提供了一个const
构造函数,以便在使用注解时可以创建编译时常量。
Dart 的反射具体可以看 Dart基础4-反射 - 简书
\\nflutter pub get
。 # 国际化支持\\n flutter_localizations:\\n sdk: flutter\\n intl: ^0.19.0\\n
\\nvscode 点击顶部输入框输入命令
\\n输入 >Flutter Intl: Initialize
后点击回车,等待初始化完成
初始化成功会默认创建如下两个文件夹
\\ngenerated文件夹不需要修改
\\nl10n文件夹默认只有 intl_en.arb 文件,即默认英文
通过命令添加其它语言 >Flutter Intl: Add locale
,回车继续输入 zh_CN
回车确认即可
执行完命令就会创建新的 arb文件
\\nS.of(context).home
即可拿到,注意导入相关的包和确保 context
能安全的拿到。{\\n \\"home\\": \\"Home\\",\\n \\"setting\\": \\"Setting\\",\\n \\"video\\": \\"Video\\",\\n \\"food\\": \\"Food\\",\\n \\"internationalization\\": \\"Internationalization\\",\\n \\"theme\\": \\"Theme\\",\\n \\"icon\\": \\"Icon\\",\\n \\"icons\\": \\"Icons\\",\\n \\"about\\": \\"About\\",\\n \\"login\\": \\"Login\\"\\n}\\n
\\nimport \'package:flutter/material.dart\';\\nimport \'package:shared_preferences/shared_preferences.dart\';\\n\\nclass LocalizationInfo with ChangeNotifier {\\n String _localeKey = \'zh_CN\';\\n String get localeKey => _localeKey;\\n \\n // 初始化\\n LocalizationInfo() {\\n initLanguage();\\n }\\n\\n // 设置语言\\n void setLocaleKey(String locale) async {\\n final prefs = await SharedPreferences.getInstance();\\n _localeKey = locale;\\n prefs.setString(\'_localeKey\', locale);\\n notifyListeners(); // 通知监听者\\n }\\n\\n // 初始化语言\\n void initLanguage() async {\\n final prefs = await SharedPreferences.getInstance();\\n _localeKey = prefs.getString(\'_localeKey\') ?? \'zh_CN\';\\n notifyListeners();\\n }\\n}\\n\\n
\\n_getLocale
方法通过不同的值返回不一样的 Locale。_getLocale
封装的比较简单粗暴,可自行封装成通用的方法。 Locale _getLocale(String localeKey) {\\n switch (localeKey) {\\n case \'zh_CN\':\\n return const Locale(\'zh\', \'CN\');\\n case \'en\':\\n return const Locale(\'en\');\\n default:\\n return const Locale(\'zh\', \'CN\');\\n }\\n }\\n
\\nlocalizations_page.dart
页面来设置语言import \'package:flutter/material.dart\';\\nimport \'package:provider/provider.dart\';\\nimport \'package:wallpaper/components/appbar_base.dart\';\\nimport \'package:wallpaper/generated/l10n.dart\';\\nimport \'package:wallpaper/themes/localization_info.dart\';\\n\\nclass LocalizationsPage extends StatefulWidget {\\n const LocalizationsPage({super.key});\\n\\n @override\\n State<LocalizationsPage> createState() => _LocalizationsPageState();\\n}\\n\\n// ignore: camel_case_types\\nclass localItem {\\n final String title;\\n final String localeKey;\\n const localItem(this.title, this.localeKey);\\n}\\n\\nclass _LocalizationsPageState extends State<LocalizationsPage> {\\n @override\\n Widget build(BuildContext context) {\\n final localization = Provider.of<LocalizationInfo>(context);\\n List<localItem> localList = [\\n const localItem(\'中文\', \'zh_CN\'),\\n const localItem(\'English\', \'en\'),\\n ];\\n return Scaffold(\\n appBar: AppbarBase(title: S.of(context).internationalization),\\n body: Padding(\\n padding: const EdgeInsets.all(8.0),\\n child: Flex(\\n direction: Axis.vertical,\\n spacing: 8,\\n children: [\\n for (var item in localList)\\n Material(\\n color: Theme.of(context).colorScheme.primaryContainer,\\n borderRadius: BorderRadius.circular(8),\\n clipBehavior: Clip.antiAlias,\\n child: ListTile(\\n title: Text(item.title),\\n // 右侧图标\\n trailing: localization.localeKey == item.localeKey\\n ? Icon(\\n Icons.check,\\n color: Theme.of(context).colorScheme.primary,\\n )\\n : null,\\n onTap: () {\\n localization.setLocaleKey(item.localeKey);\\n },\\n )),\\n ],\\n ),\\n ),\\n );\\n }\\n}\\n\\n
\\nlocalization.setLocaleKey()
设置语言就会通知 main.dart
重新加载语言,并且会做持久化处理,下次打开软件时会自动加载设置好的语言。自定义主题文件 theme.dart
,colorScheme
还有很多属性都能自定义,可以自己查一下,基本上下面这些属性已经完全够用了,我们可以通过 Theme.of(context).colorScheme.primaryContainer
使用定义好的颜色。
import \'package:flutter/material.dart\';\\n\\nThemeData lightMode = ThemeData(\\n brightness: Brightness.light,\\n colorScheme: ColorScheme.light(\\n surface: Colors.grey.shade200,\\n onSurface: Colors.grey.shade900,\\n primary: const Color.fromARGB(255, 0, 94, 255),\\n primaryContainer: Colors.white,\\n secondary: Colors.grey.shade300,\\n inversePrimary: Colors.grey.shade800,\\n shadow: const Color.fromARGB(112, 80, 80, 80),\\n ),\\n);\\n\\nThemeData darkMode = ThemeData(\\n brightness: Brightness.dark,\\n colorScheme: ColorScheme.dark(\\n surface: Colors.grey.shade900,\\n onSurface: Colors.grey.shade100,\\n primary: Colors.deepPurpleAccent,\\n primaryContainer: const Color.fromARGB(255, 23,23,23),\\n secondary: const Color.fromARGB(107, 66, 66, 66),\\n inversePrimary: Colors.grey.shade200,\\n shadow: const Color.fromARGB(62, 75, 75, 75)),\\n);\\n\\n
\\n创建 theme_provider.dart
文件设置主题,里面注释已经很详细了,也没啥难的逻辑,我就直接介绍怎么使用了。
import \'package:flutter/material.dart\';\\nimport \'package:shared_preferences/shared_preferences.dart\';\\nimport \'package:wallpaper/themes/theme.dart\';\\n\\nclass ThemeProvider extends ChangeNotifier {\\n ThemeData _themeData = lightMode;\\n bool _followSystem = false;\\n bool _isInitialized = false;\\n VoidCallback? _brightnessListener;\\n\\n ThemeData get themeData => _themeData;\\n bool get isDarkMode => _themeData == darkMode;\\n bool get followSystem => _followSystem;\\n\\n ThemeProvider() {\\n _initTheme();\\n }\\n\\n // 初始化主题(异步)\\n Future<void> _initTheme() async {\\n if (_isInitialized) return;\\n\\n final prefs = await SharedPreferences.getInstance();\\n _followSystem = prefs.getBool(\'followSystem\') ?? false;\\n\\n if (_followSystem) {\\n _themeData = _getSystemTheme();\\n } else {\\n final isDark = prefs.getBool(\'isDark\') ?? false;\\n _themeData = isDark ? darkMode : lightMode;\\n }\\n\\n _addSystemThemeListener();\\n _isInitialized = true;\\n notifyListeners();\\n }\\n\\n // 系统主题监听\\n void _addSystemThemeListener() {\\n _brightnessListener = () {\\n if (_followSystem) {\\n _themeData = _getSystemTheme();\\n notifyListeners();\\n }\\n };\\n\\n WidgetsBinding.instance.platformDispatcher.onPlatformBrightnessChanged =\\n _brightnessListener;\\n }\\n\\n // 清理资源\\n @override\\n void dispose() {\\n WidgetsBinding.instance.platformDispatcher.onPlatformBrightnessChanged =\\n null;\\n super.dispose();\\n }\\n\\n // 统一保存配置\\n Future<void> _savePreferences() async {\\n final prefs = await SharedPreferences.getInstance();\\n await prefs.setBool(\'isDark\', isDarkMode);\\n await prefs.setBool(\'followSystem\', _followSystem);\\n }\\n\\n // 切换主题\\n void toggleTheme() {\\n _followSystem = false;\\n _themeData = isDarkMode ? lightMode : darkMode;\\n _savePreferences();\\n notifyListeners();\\n }\\n\\n // 设置浅色主题\\n void setLightTheme() {\\n _followSystem = false;\\n _themeData = lightMode;\\n _savePreferences();\\n notifyListeners();\\n }\\n\\n // 设置深色主题\\n void setDarkTheme() {\\n _followSystem = false;\\n _themeData = darkMode;\\n _savePreferences();\\n notifyListeners();\\n }\\n\\n // 跟随系统主题\\n void setFollowSystem() {\\n _followSystem = true;\\n _themeData = _getSystemTheme();\\n _savePreferences();\\n notifyListeners();\\n }\\n\\n // 获取系统主题\\n ThemeData _getSystemTheme() {\\n final brightness =\\n WidgetsBinding.instance.platformDispatcher.platformBrightness;\\n return brightness == Brightness.dark ? darkMode : lightMode;\\n }\\n}\\n\\n
\\n在 main.dart
中注入ThemeProvider
\\n
直接在 main.dart
中使用主题
因为上面国际化时已经使用了 Consumer
,设置主题可以采用 Provider.of<ThemeProvider>(context).themeData
的方式直接设置,详细用法见官网。\\n
创建一个theme_page.dart
页面来设置主题
import \'package:flutter/material.dart\';\\nimport \'package:provider/provider.dart\';\\nimport \'package:wallpaper/components/appbar_base.dart\';\\nimport \'package:wallpaper/themes/theme_provider.dart\';\\n\\nclass ThemePage extends StatefulWidget {\\n const ThemePage({super.key});\\n\\n @override\\n State<ThemePage> createState() => _ThemePageState();\\n}\\n\\nclass _ThemePageState extends State<ThemePage> {\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppbarBase(title: \'主题设置\'),\\n body: Consumer<ThemeProvider>(builder: (context, provider, child) {\\n return Padding(\\n padding: const EdgeInsets.all(8.0),\\n child: Flex(\\n direction: Axis.vertical,\\n spacing: 10,\\n children: [\\n SizedBox(\\n child: Material(\\n borderRadius: BorderRadius.circular(10),\\n clipBehavior: Clip.antiAlias,\\n color: Theme.of(context).colorScheme.primaryContainer,\\n child: InkWell(\\n onTap: () {\\n provider.setLightTheme();\\n },\\n child: Row(\\n children: [\\n Radio(\\n value: !provider.isDarkMode &&\\n !provider.followSystem,\\n groupValue: true,\\n onChanged: (value) {\\n provider.setLightTheme();\\n }),\\n Text(\'浅色模式\')\\n ],\\n ),\\n ),\\n ),\\n ),\\n Material(\\n borderRadius: BorderRadius.circular(10),\\n clipBehavior: Clip.antiAlias,\\n color: Theme.of(context).colorScheme.primaryContainer,\\n child: InkWell(\\n onTap: () {\\n provider.setDarkTheme();\\n },\\n child: Row(\\n children: [\\n Radio(\\n value:\\n provider.isDarkMode && !provider.followSystem,\\n groupValue: true,\\n onChanged: (value) {\\n provider.setDarkTheme();\\n }),\\n Text(\'暗色模式\')\\n ],\\n ),\\n ),\\n ),\\n Material(\\n borderRadius: BorderRadius.circular(10),\\n clipBehavior: Clip.antiAlias,\\n color: Theme.of(context).colorScheme.primaryContainer,\\n child: InkWell(\\n onTap: () {\\n provider.setFollowSystem();\\n },\\n child: Row(\\n children: [\\n Radio(\\n value: provider.followSystem,\\n groupValue: true,\\n onChanged: (value) {\\n provider.setFollowSystem();\\n }),\\n Text(\'跟随系统\')\\n ],\\n ),\\n ),\\n ),\\n ],\\n ),\\n );\\n }));\\n }\\n}\\n
\\n视频播放器UI,没啥大的变化,稍微调整了边距。
\\n扫码功能,美化UI,简单添加提示和刷新功能。
\\n问题:
\\n解决:
\\nInkWell
举例,水波纹需要 Material
或者 Scaffold
来承载,如果使用 Container
等包裹,水波纹可能就会失效。 Navigator.push(\\n context,\\n PageRouteBuilder(\\n settings: RouteSettings(name: \'\'), // 保留路由名称\\n pageBuilder: (_, animation, __) =>\\n list[i].route!, // 替换为你的目标页面\\n transitionsBuilder: (_, animation, __, child) {\\n // 示例:从右向左滑动动画\\n return SlideTransition(\\n position: Tween<Offset>(\\n begin: const Offset(1.0, 0.0),\\n end: Offset.zero,\\n ).animate(CurvedAnimation(\\n parent: animation,\\n curve: Curves.easeInOut,\\n )),\\n child: child,\\n );\\n },\\n transitionDuration:\\n const Duration(milliseconds: 300), // 动画时长\\n ),\\n );\\n
\\ngradle-8.3-all.zip
使用七牛云Flutter的SDK qiniu_flutter_sdk
在Flutter物流项目中主要用于车辆相关页面的图片上传功能,包括车辆照片、行驶证照片、商业险照片和道路运输证照片的上传。通过该SDK,实现了图片的快速上传和管理。
在使用SDK之前,需要进行必要的初始化配置:
\\nimport \'package:qiniu_flutter_sdk/qiniu_flutter_sdk.dart\';\\n\\nvoid initQiniuSDK() {\\n // 配置SDK\\n Config config = Config()\\n ..connectTimeout = 30000\\n ..responseTimeout = 30000\\n ..retryMax = 3\\n ..retryInterval = 1000;\\n \\n // 初始化SDK\\n Qiniu.init(config);\\n}\\n
\\nvehicle_add_form_page.dart
)vehicle_auth_form_page.dart
)vehicle_update_form_page.dart
)使用 Storage
类进行文件上传,通过 PutController
控制上传过程,并配置 PutOptions
参数。
Storage storage = Storage();\\nPutController putController = PutController();\\n// qiniuPath生成规则说明:\\n// 1. uploadPolicy?.filePath:获取上传策略中的基础路径\\n// 2. imageUseType:图片用途类型(如vehicle_photo, license_photo等)\\n// 3. DateTime.now().millisecondsSinceEpoch:当前时间戳,确保唯一性\\n// 4. p.extension(path):获取文件扩展名\\n// 5. 增加用户ID和设备ID,进一步确保路径唯一性\\nString qiniuPath = \\"${uploadPolicy?.filePath}${imageUseType}_${userId}_${deviceId}_${DateTime.now().millisecondsSinceEpoch}${p.extension(path)}\\";\\nputController.addProgressListener((percent) {});\\nputController.addStatusListener((status) {\\n if (status == StorageStatus.Success) {\\n // 处理上传成功后的逻辑\\n } else if (status == StorageStatus.Error) {\\n // 处理上传失败的情况\\n }\\n});\\nstorage.putFile(File(path), uploadPolicy!.uploadToken!, options: PutOptions(controller: putController, key: qiniuPath));\\n
\\n通过调用OSS接口删除已上传的图片。
\\nawait G.req.oss.delFile(_path, \\"vehicle\\").then((value) {\\n G.loading.hide(context);\\n G.toast(\'移除图片成功\');\\n}).catchError((err) {\\n G.loading.hide(context);\\n});\\n
\\n当需要更新已上传的图片时,按照以下步骤进行:
\\n// 更新图片示例\\nFuture<void> updateImage(String oldPath, String newPath) async {\\n try {\\n // 1. 删除旧图片\\n await G.req.oss.delFile(oldPath, \\"vehicle\\");\\n \\n // 2. 上传新图片\\n String newQiniuPath = generateQiniuPath(newPath);\\n await storage.putFile(File(newPath), uploadPolicy!.uploadToken!,\\n options: PutOptions(key: newQiniuPath));\\n \\n // 3. 更新数据库\\n await updateImagePathInDatabase(oldPath, newQiniuPath);\\n \\n G.toast(\'图片更新成功\');\\n } catch (e) {\\n G.toast(\'图片更新失败: $e\');\\n }\\n}\\n\\n// 生成新的qiniu路径\\nString generateQiniuPath(String path) {\\n return \\"${uploadPolicy?.filePath}${imageUseType}_${userId}_${deviceId}_${DateTime.now().millisecondsSinceEpoch}${p.extension(path)}\\";\\n}\\n
\\n在文件上传之前,需要先通过认证获取上传策略。使用 G.req.oss.getUploadPolicy
接口获取上传策略对象 OssUploadPolicy
。
// 获取上传策略示例\\nOssUploadPolicy? uploadPolicy;\\ntry {\\n uploadPolicy = await G.req.oss.getUploadPolicy(\\n bucket: \\"vehicle-images\\", // 存储空间名称\\n fileType: \\"image\\", // 文件类型\\n userId: userId, // 用户ID\\n deviceId: deviceId // 设备ID\\n );\\n} catch (e) {\\n // 处理获取上传策略失败的情况\\n print(\\"获取上传策略失败: $e\\");\\n}\\n\\nif (uploadPolicy == null) {\\n // 处理上传策略为空的情况\\n return;\\n}\\n
\\ntry {\\n // 上传操作\\n} on NetworkException catch (e) {\\n // 处理网络异常\\n print(\'网络异常: ${e.message}\');\\n} on StorageException catch (e) {\\n // 处理存储异常\\n print(\'存储异常: ${e.message}\');\\n} catch (e) {\\n // 处理其他异常\\n print(\'未知异常: $e\');\\n}\\n
\\nOssUploadPolicy
)Storage
和 PutController
PutOptions
)storage.putFile
)qiniu_flutter_sdk 为项目提供了稳定可靠的图片上传功能,极大地简化了图片管理的复杂度,提升了用户体验。通过合理的配置和优化,可以充分发挥SDK的性能,为应用提供高效的文件存储解决方案。
","description":"概述 使用七牛云Flutter的SDK qiniu_flutter_sdk 在Flutter物流项目中主要用于车辆相关页面的图片上传功能,包括车辆照片、行驶证照片、商业险照片和道路运输证照片的上传。通过该SDK,实现了图片的快速上传和管理。\\n\\nSDK初始化配置\\n\\n在使用SDK之前,需要进行必要的初始化配置:\\n\\nimport \'package:qiniu_flutter_sdk/qiniu_flutter_sdk.dart\';\\n\\nvoid initQiniuSDK() {\\n // 配置SDK\\n Config config = Config…","guid":"https://juejin.cn/post/7473730128070950953","author":"机器瓦力","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-21T10:03:12.199Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/db94a94513aa4205976a71e8b6696022~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5py65Zmo55Om5Yqb:q75.awebp?rk3s=f64ab15b&x-expires=1740736992&x-signature=q9Z%2FnoO9saFzUtpTVRg22Yy3%2BiM%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"一个Flutter跨4端开发的案例","url":"https://juejin.cn/post/7473712922389839898","content":"哈喽,我是老刘
\\n老刘做Flutter开发6年多了,在手机端的案例见到很多,PC端的确实比较少。\\nFlutter也的确不是PC端开发的首选。\\n不过前段时间无意见发现一个Flutter跨Windows、Mac、Android和iOS四个端的典型例子,百度输入法。
\\n事情是这样的,春节前一段时间,搜狗输入法上线了一个AI助手,我觉得挺烦人的还很难彻底关掉,就决定换一个输入法,于是下载了百度输入法。
\\n万万没想到,这个浓眉大眼的家伙也有AI助手。\\n
可是装都装了,想着先试试能不能把AI助手禁用。\\n就在进程管理器里面看到了Flutter\\n
但是只是名字可能是巧合,所以还需要确认一下是否真的使用了Flutter进行开发。
\\n看有没有相关文件
\\n打开百度输入法的安装目录,可以很容易找到Flutter相关的文件\\n\\n
\\n
\\n这回Flutter的基础文件都有了,如果不放心还可以看看flutter_windows.dll的内容\\n
到这里基本上可以判断百度输入法的这个AI助手是Flutter开发的了。
\\n但是我又产生了一个疑惑:为啥要选择Flutter?
\\n因为在pc端我们有太多的选择了,而对于AI助手这类以聊天界面为主的UI,其实一个基于Web技术的开发框架是很不错的选择。
\\n既然如此,那选择Flutter一定就是为了跨平台考虑。
\\n于是我去百度输入法官网看了下,确实是一个跨平台的App:\\n
又去下载了一个Android端的安装包,在里面确实也找到了Flutter的相关文件。
\\n因此我推测百度输入法本身应该是在各个平台采用原生技术开发的,但是新增的这个AI助手,采用了Flutter作为主要的开发框架。
\\n其实前面那些动作大多出于一个Flutter开发者的好奇心,但是仔细想想,这确实是Flutter的典型应用场景。
\\n单纯看PC端,我觉得开发一个AI助手用Web框架是不错的选择。
\\n但是考虑百度输入法的场景,需要同时开发手机和PC两种共四个平台。
\\n另外这不是一个全新的App,还需要和原有的输入法一定程度进行结合。
\\n因此开发框架的选择就需要满足以下三个条件:
\\n足够好的跨平台能力。要能提供非常高的多端一致性体验。
\\n足够好的性能。出于AI助手的定位,未来很有可能需要增加文档分析、画布甚至生成图片的能力。在PC端可能问题不大,但是考虑到手机端,就需要开发框架能提供非常高的性能支撑。
\\n和原生App的无缝融合。如果后续这个AI助手做大做强很有可能会和输入法本身进一部融合,这时开发框架和原生无缝衔接的能力就会显得无比重要了。
\\n基于上面三个条件要求,特别是多端一致性和手机端性能的要求,相信留给开发者的选择已经不多了。
\\n而Flutter恰恰是能比较理想的满足这三个条件的开发框架。
\\n考虑到未来这个App可能的变化,即使后续这个AI助手需要部署端侧的轻量级模型,Flutter的原生融合能力和性能也能提供有效的支撑。
\\n所以我说这是一个典型的Flutter跨四端开发的应用场景。
\\n无意中发现这样一个Flutter的典型应用场景,相信对一些选择困难症的朋友有帮助。
\\n另外对于希望在AI应用上做一些尝试的开发者,这也是一个很好的案例。
\\n不管是在已有的应用中增加AI相关的功能还是开发全新的应用,都可以考虑Flutter作为核心开发框架。
\\n另外说一下百度输入法的使用体验:
\\n输入便利性上略微比搜狗差一点,但是差距不大,总体比微软拼音好用不少。
\\n从性能上,微软拼音最好,百度和搜狗差不多。偶尔百度输入法会有卡顿的场景个人分析可能是后台在更新。
\\n如果看到这里的同学对客户端开发或者Flutter开发感兴趣,欢迎联系老刘,我们互相学习。\\n点击免费领老刘整理的《Flutter开发手册》,覆盖90%应用开发场景。\\n可以作为Flutter学习的知识地图。
\\n","description":"哈喽,我是老刘 老刘做Flutter开发6年多了,在手机端的案例见到很多,PC端的确实比较少。 Flutter也的确不是PC端开发的首选。 不过前段时间无意见发现一个Flutter跨Windows、Mac、Android和iOS四个端的典型例子,百度输入法。\\n\\n事情是这样的,春节前一段时间,搜狗输入法上线了一个AI助手,我觉得挺烦人的还很难彻底关掉,就决定换一个输入法,于是下载了百度输入法。\\n\\n万万没想到,这个浓眉大眼的家伙也有AI助手。\\n\\n可是装都装了,想着先试试能不能把AI助手禁用。 就在进程管理器里面看到了Flutter\\n\\n但是只是名字可能是巧合…","guid":"https://juejin.cn/post/7473712922389839898","author":"程序员老刘","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-21T09:39:41.799Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/aa75ee6c73fa45e0ae2bbc59c65672e5~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg56iL5bqP5ZGY6ICB5YiY:q75.awebp?rk3s=f64ab15b&x-expires=1740735581&x-signature=MWLlFDv69gxuebr4MK4Tat%2F%2FlOk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a2163f09bc0144ee8a5a80e27045f4a5~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg56iL5bqP5ZGY6ICB5YiY:q75.awebp?rk3s=f64ab15b&x-expires=1740735581&x-signature=SZkX44T7pFe3lcsutcrIey9Cj5Q%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/23aeac2fa7d3434cb28991b440991bd6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg56iL5bqP5ZGY6ICB5YiY:q75.awebp?rk3s=f64ab15b&x-expires=1740735581&x-signature=%2Fx%2FmrpO3moZoyLWybtGKLf6DXf4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6844efce6af040f3bf279bdf36505da0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg56iL5bqP5ZGY6ICB5YiY:q75.awebp?rk3s=f64ab15b&x-expires=1740735581&x-signature=YLhVmjsP5H3RImX8SbqYlJ77Fec%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/305c9721213d49e2a2d74e972bb749af~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg56iL5bqP5ZGY6ICB5YiY:q75.awebp?rk3s=f64ab15b&x-expires=1740735581&x-signature=NupSZI4YeiRQdf5VJgMWMjGKYNI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7498971a978e4346aecab20b4547f383~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg56iL5bqP5ZGY6ICB5YiY:q75.awebp?rk3s=f64ab15b&x-expires=1740735581&x-signature=t16uNSiqSLsWU7wQWR6avFbJKjQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5da735d4dda54651a5bd3c45d8816b8f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg56iL5bqP5ZGY6ICB5YiY:q75.awebp?rk3s=f64ab15b&x-expires=1740735581&x-signature=Ol8c9I7%2B8fjwvSu5Pm%2FeQtL646s%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/bfbd94245c664fc9890a786e78cdc6b8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg56iL5bqP5ZGY6ICB5YiY:q75.awebp?rk3s=f64ab15b&x-expires=1740735581&x-signature=BMvHLP5bDQsvAwIUmG5OOvIYEmk%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["开发工具","Flutter","AIGC"],"attachments":null,"extra":null,"language":null},{"title":"老司机快来!2分钟搞懂flutter的动画:)","url":"https://juejin.cn/post/7473712922389790746","content":"\\n\\nflutter中的动画千变万化,但是始终不过2种:‘隐式动画’与‘显示动画’。
\\n
不需要手动控制的动画。所谓的隐式
就是指控制动画的过程由系统帮你完成了。可以给它比作太阳,它天生就能动(其实底层也是通过显示动画
那套实现的)。\\n它的特点是:
AnimatedContainer
、AnimatedOpacity
等以 Animated
开头的组件上代码:
\\nAnimatedSize( //第一句。\\n duration: Duration(seconds: 1), // 第二句。\\n child: Container(\\n width: 100, // 首次运行后改动这个值为200,然后按保存按钮,就会看见动画\\n height: 100,// 首次运行后改动这个值为200,然后按保存按钮,就会看见动画\\n color: Colors.blue,\\n ),\\n)\\n
\\n当然,实际中肯定不是按保存按钮执行动画!不过其实也差别不大,只是你自己用代码改变child的大小而已,这个动画就会自己动起来。(由于太过简单,不需要再多解释了)
\\n需要手动控制动画。所谓的显示
就是指需要自己搞一个AnimationController
来控制动画。可以给它比作一个汽车,需要引擎来让轮子转起来。\\n它的特点是:
AnimationController
显式控制;AnimatedBuilder
、AnimatedWidget
上代码:
\\nclass _MyAnimState extends State<MyAnim> with SingleTickerProviderStateMixin {\\n late AnimationController _controller;\\n late Animation<double> _animation;\\n\\n @override\\n void initState() {\\n super.initState();\\n // 创建动画控制器\\n _controller = AnimationController(\\n duration: Duration(seconds: 1),\\n vsync: this,\\n );\\n\\n // 创建动画\\n _animation = Tween<double>(\\n begin: 0,\\n end: 200,\\n ).animate(_controller);\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Column(\\n children: [\\n AnimatedBuilder(\\n animation: _animation,\\n builder: (context, child) {\\n return Container(\\n width: _animation.value,\\n height: _animation.value,\\n color: Colors.blue,\\n );\\n },\\n ),\\n ElevatedButton(\\n onPressed: () {\\n // 手动控制动画\\n if (_controller.status == AnimationStatus.completed) {\\n _controller.reverse();\\n } else {\\n _controller.forward();\\n }\\n },\\n child: Text(\'触发动画\'),\\n ),\\n ],\\n );\\n }\\n}\\n
\\n代码里的with SingleTickerProviderStateMixin
是什么呢?
还是用前面提到的汽车来举例:我们知道,系统是一直在运行的,它本身就相当于一个引擎。当我们with
上这个SingleTickerProviderStateMixin
之后,就相当于给汽车挂了个档(开机就是打火,引擎其实早就启动了),相当于告诉操作系统:你运行的动力我们也需要用到了,也可以说是给操作系统的引擎运动函数加了个回调而已。
所以,我们的方向盘就是AnimationController
(注:vsync
就是连接了SingleTickerProviderStateMixin
),我们现在有了AnimationController
可以控制动画了,它本身也有驱动的动力了。
现在我们有了第一个需求:启动
\\ncontroller.forward(); // 启动\\n//顺便学会其他几个\\ncontroller.reverse(); // 倒车 \\ncontroller.stop(); // 停车 \\ncontroller.reset(); // 前面不算,重来\\n
\\n光会前进是不够的,我们还要会:转弯与加减速
\\n// 添加加速减速的效果。比真实开车更强的是它的加减速有好多预设模式:Curves.bounceOut、Curves.slowMiddle等;\\nfinal curvedAnimation = CurvedAnimation( parent: controller, curve: Curves.bounceOut);\\n
\\n比如Curves.bounceOut
的样子是这样:\\n
我们知道,挂上1档
与5档
的车速其实是不一样的,那在flutter
里是怎么换档
的?
那就是Tween
:
// 连接上前面的controller\\n// 当 controller.value 是 0.0 时,输出 0 \\n// 当 controller.value 是 1.0 时,输出 200 \\nTween<double> tween = Tween<double>( begin: 0, end: 200);\\n
\\n其实到这里也说得差不多了,最终的动画是通过AnimatedBuilder
之类的应用到实际页面上面的。
现在就剩最后一个问题:这4个有什么关系?
\\n我们可以通过下面这个图来解释一下:\\n通过Controller
提供动力和操控(Controller
的动力来源是它与SingleTickerProviderStateMixin
连接起来的),通过Curve
来调节方向与速度,通过Tween
来换档,最后通过Animation
把最终的效果应用到实际以达到页面按需动起来的目的。\\n
补充:
\\nController
有lowerBound
和lowerBound
2个构造参数来把范围作一下调整,这样简单的话(简单是指单个动画,其实还有多个动画串联和并联)可以把Tween
给省略掉(直接使用controller.value
);Tween
不仅仅可以把Controller
的数字转换为数字,还可以是颜色等;SawTooth
、Interval
等等格式花哨的Curve
,实在不满足就自己自定义也行。针对各种手机的屏幕自适应是APP开发的刚性需求,本项目使用了 flutter_screenutil
版本 5.5.3+2
来实现屏幕适配,确保应用在不同尺寸的设备上都能保持良好的UI效果。
在 main.dart
中,使用 ScreenUtilInit
包裹整个应用,并设置设计稿尺寸为 750x1334
:
return ScreenUtilInit(\\n designSize: Size(750, 1334),\\n builder: (context, child) {\\n return GetMaterialApp(\\n // 应用配置\\n );\\n });\\n
\\n在 global.dart
中封装了 rpx()
方法,用于将设计稿中的尺寸转换为实际像素:
static double rpx(double rpx) {\\n return ScreenUtil().setWidth(2 * rpx);\\n}\\n
\\n封装了多个边距处理方法,方便快速设置各种边距:
\\nstatic EdgeInsets edge(double left, double top, double right, double bottom) {\\n return EdgeInsets.fromLTRB(rpx(left), rpx(top), rpx(right), rpx(bottom));\\n}\\n\\nstatic EdgeInsets edgeAll(double value) {\\n return EdgeInsets.all(rpx(value));\\n}\\n\\nstatic EdgeInsets edgeOnly({double left = 0.0, double top = 0.0, double right = 0.0, double bottom = 0.0}) {\\n return EdgeInsets.only(left: rpx(left), top: rpx(top), right: rpx(right), bottom: rpx(bottom));\\n}\\n\\nstatic EdgeInsets edgeSymmetric({double vertical = 0.0, double horizontal = 0.0}) {\\n return EdgeInsets.symmetric(vertical: rpx(vertical), horizontal: rpx(horizontal));\\n}\\n
\\nContainer(\\n width: G.rpx(375), // 设计稿中宽度为375rpx\\n height: G.rpx(200), // 设计稿中高度为200rpx\\n)\\n
\\nPadding(\\n padding: G.edgeAll(20), // 四周20rpx边距\\n child: Text(\'Hello World\'),\\n)\\n
\\n特性 | flutter_screenutil | flutter_screen | sizer |
---|---|---|---|
支持设计稿尺寸 | ✔️ | ✔️ | ✔️ |
字体适配 | ✔️ | ✔️ | ✔️ |
边距处理 | ✔️ | ✔️ | ✔️ |
状态栏高度适配 | ✔️ | ✔️ | ✔️ |
横竖屏切换支持 | ✔️ | ✔️ | ✔️ |
多语言支持 | ✔️ | ❌ | ❌ |
社区活跃度 | 高 | 中 | 低 |
通过 flutter_screenutil
的合理使用,本项目实现了:
相比其他方案,flutter_screenutil
具有以下优势:
在移动应用开发中,图片是用户界面最直观的视觉元素
之一。无论是社交媒体的动态展示
、电商平台的商品列表
,还是新闻应用的内容呈现
,图片的高效加载、渲染和管理都直接影响用户体验。
Flutter
的Image
组件作为UI体系的核心部分,不仅支持本地资源
、网络图片
、文件系统
和二进制数据
等多种加载方式,还通过高度灵活的API
和底层优化机制(如缓存
、分辨率适配
、懒加载
)确保了性能与视觉效果的平衡。然而,许多开发者仅停留在基础使用层面,对Image
的底层原理、性能优化策略和设计哲学缺乏系统化认知,导致应用中频繁出现内存泄漏
、图片闪烁
、加载卡顿
等问题。
本文将从六大核心维度,全面解析Image
组件的技术细节,帮助开发者构建高性能
、高可维护
的图片处理体系。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\nImage的核心属性与基本使用
1、Image.asset
:加载本地资源\\nImage.asset(\\n \'assets/logo.png\', \\n width: 100, \\n height: 100,\\n)\\n
\\npubspec.yaml
中声明资源路径,支持多级目录(如assets/images/
)。1.0x
、2.0x
、3.0x
文件夹自动匹配设备像素密度。Unable to load asset
异常。2、Image.network
:加载网络图片\\nImage.network(\\n \'https://example.com/image.jpg\', \\n fit: BoxFit.cover,\\n)\\n
\\nHTTP/HTTPS
协议,可添加自定义请求头(如鉴权
)。errorBuilder
显示占位图。cacheWidth
和cacheHeight
优化内存占用。3、Image.file
:加载本地文件\\nImage.file(\\n File(\'/path/to/image.png\'), \\n color: Colors.blue.withOpacity(0.5), // 颜色叠加\\n filterQuality: FilterQuality.high, // 抗锯齿级别\\n)\\n
\\n相册图片
、临时下载文件
。4、Image.memory
:加载二进制数据
\\nImage.memory(\\n Uint8List.fromList(bytes), \\n scale: 2.0, // 缩放系数(适配高分辨率设备)\\n)\\n
\\n数据库
或加密存储
中读取二进制图片数据。1、width
与height
:尺寸控制的双刃剑\\n// 自适应尺寸(按原图比例)\\nImage.network(url, fit: BoxFit.contain)\\n\\n// 固定尺寸(可能导致拉伸或压缩)\\nImage.asset(\'logo.png\', width: 200, height: 200)\\n
\\nBoxFit
控制填充方式,而非强制尺寸。2、fit
:7
种填充模式的本质区别
填充模式 | 行为描述 | 典型场景 |
---|---|---|
BoxFit.fill | 拉伸图片以完全填满容器,忽略原始宽高比,可能导致图片变形。 | 背景图(需完全覆盖且不关心比例) |
BoxFit.contain | 保持宽高比缩放图片,完整显示图片内容,但可能在容器内留白(上下或左右)。 | 产品详情图、需完整展示的图标 |
BoxFit.cover | 保持宽高比缩放图片,完全覆盖容器并裁剪超出部分,无留白。 | 用户头像、封面图(需充满容器) |
BoxFit.fitWidth | 保持宽高比缩放图片宽度,使宽度填满容器,高度可能不足(下方留白)或超出(裁剪)。 | 横向滑动横幅(固定宽度,高度自适应) |
BoxFit.fitHeight | 保持宽高比缩放图片高度,使高度填满容器,宽度可能不足(左右留白)或超出(裁剪)。 | 纵向滚动海报(固定高度,宽度自适应) |
BoxFit.scaleDown | 仅在图片原始尺寸大于容器时缩小,行为与 contain 一致但不会放大图片,可能留白。 | 动态内容(避免小图被意外放大) |
BoxFit.none | 不缩放图片,以原始尺寸居中显示,可能溢出容器或被裁剪(取决于 clipBehavior )。 | 像素级精确显示(如游戏贴图、小图标) |
直观理解(以 4:3
图片放入 1:1
容器为例):
fill
:拉伸为正方形 → 变形contain
:保持 4:3 比例,上下留黑边 → 完整显示cover
:保持 4:3 比例,裁剪左右 → 无黑边none
:居中显示原始尺寸 → 溢出或被裁剪3、color
与colorBlendMode
:颜色混合的魔法\\nImage.asset(\\n \'icon.png\', \\n color: Colors.red, \\n colorBlendMode: BlendMode.srcIn, // 保留原图Alpha通道,叠加颜色\\n)\\n
\\n4、alignment
:精准控制图片对齐\\nImage.network(\\n url, \\n alignment: Alignment.topCenter, // 顶部居中\\n repeat: ImageRepeat.repeatX, // 水平平铺\\n)\\n
\\n5、filterQuality
:抗锯齿级别控制\\nImage.asset(\\n \'graph.png\', \\n filterQuality: FilterQuality.high, // 高质量缩放(适合矢量图)\\n)\\n
\\n父级约束的影响:
\\nImage
默认遵循父级约束,若父容器未指定尺寸,图片可能溢出。LayoutBuilder
动态计算尺寸或包裹SizedBox
。AspectRatio
的妙用:
AspectRatio(\\n aspectRatio: 16 / 9, // 强制宽高比\\n child: Image.network(url),\\n)\\n
\\nerrorBuilder
:优雅降级\\nImage.network(\\n url,\\n errorBuilder: (context, error, stackTrace) => \\n Container(\\n color: Colors.grey, \\n child: Icon(Icons.broken_image),\\n)\\n
\\nframeBuilder
:渐进式加载动画\\nImage.network(\\n url,\\n frameBuilder: (ctx, child, frame, wasSynchronouslyLoaded) {\\n if (wasSynchronouslyLoaded) return child; // 同步加载(如缓存命中)\\n return AnimatedOpacity(\\n child: child,\\n opacity: frame == null ? 0 : 1, // 渐显效果\\n duration: const Duration(milliseconds: 300),\\n );\\n },\\n)\\n
\\nAssetImage
的自动分辨率选择:
image.png
(1x)、image@2x.png
(2x)、image@3x.png
(3x)。devicePixelRatio
自动选择最佳资源。ExactAssetImage
的强制分辨率:
Image(image: ExactAssetImage(\'assets/icon.png\', scale: 2.0))\\n
\\n格式 | 特点 | 适用场景 |
---|---|---|
PNG | 无损压缩、支持透明度 | 图标、透明背景图 |
JPEG | 有损压缩、文件小 | 照片、背景图 |
WebP | 高压缩率、支持动画 | 网络传输优先 |
动态加载、滤镜与动画集成
devicePixelRatio
)的实战应用:\\nLayoutBuilder(\\n builder: (context, constraints) {\\n final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;\\n return Image.network(\\n \'$url?w=${constraints.maxWidth * devicePixelRatio}\', // 动态请求合适分辨率\\n );\\n },\\n)\\n
\\nShaderMask
实现渐变蒙版:\\nShaderMask(\\n shaderCallback: (Rect bounds) => LinearGradient(\\n colors: [Colors.transparent, Colors.black],\\n ).createShader(bounds),\\n blendMode: BlendMode.darken,\\n child: Image.asset(\'landscape.jpg\'),\\n)\\n
\\nBackdropFilter
实现高斯模糊:\\nBackdropFilter(\\n filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),\\n child: Image.network(url),\\n)\\n\\nImageFiltered(\\n imageFilter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),\\n child: Image.network(url, fit: BoxFit.cover),\\n),\\n
\\nAnimatedSwitcher
实现图片切换动画:\\nAnimatedSwitcher(\\n duration: Duration(milliseconds: 500),\\n child: Image.network(_currentUrl),\\n transitionBuilder: (child, animation) => \\n FadeTransition(opacity: animation, child: child),\\n)\\n
\\nimage_picker
和image
库:\\nfinal img = decodeImage(fileBytes);\\nfinal cropped = copyCrop(img, x: 100, y: 100, width: 200, height: 200);\\nImage.memory(encodePng(cropped));\\n
\\nInteractiveViewer
实现手势控制:\\nInteractiveViewer(\\n child: Image.network(url),\\n boundaryMargin: EdgeInsets.all(20),\\n minScale: 0.1,\\n maxScale: 4.0,\\n)\\n
\\n从加载速度到内存管理
Flutter
的图片缓存机制:
ImageCache
类管理,默认最大100张图片或100MB。\\n// 调整缓存大小\\nPaintingBinding.instance.imageCache.maximumSizeBytes = 200 << 20; // 200MB\\n
\\ncached_network_image
)。缓存穿透与雪崩问题:
\\nLRU
淘汰策略。cacheWidth
与cacheHeight
的黄金法则:
Image.network(\\n url, \\n cacheWidth: (300 * MediaQuery.of(context).devicePixelRatio).toInt(),\\n)\\n
\\nWebP与AVIF格式的优势:
\\nCDN
图片转换为WebP
格式,体积减少30%~70%
。precacheImage
的四种使用场景:\\n// 预加载到内存\\nprecacheImage(NetworkImage(url), context);\\n\\n// 预加载但不显示\\nprecacheImage(NetworkImage(url), context, onError: (_, __) {});\\n
\\nListView.builder(\\n itemCount: 100,\\n itemBuilder: (ctx, index) => Visibility(\\n visible: _isItemVisible(index), // 根据滚动位置判断\\n child: Image.network(urls[index]),\\n ),\\n)\\n
\\n常见泄漏场景:
\\nImageStream
未正确释放。解决方案:
\\nclass _MyWidgetState extends State<MyWidget> {\\n ImageStream? _imageStream;\\n\\n @override\\n void dispose() {\\n _imageStream?.removeListener(ImageStreamListener(_updateImage));\\n super.dispose();\\n }\\n}\\n
\\nImageProvider
与渲染管线ImageProvider
的加载流程核心类关系图:
\\nImage -> ImageProvider -> ImageStream -> ImageStreamCompleter -> ImageInfo\\n
\\n关键源码解析:
\\nImageProvider.resolve
方法触发加载流程。obtainKey
生成唯一缓存键。loadBuffer
实现具体加载逻辑。Decoder
)的工作机制GIF/WebP
)的处理:\\nvoid decodeMultiFrame(Uint8List bytes) async {\\n final codec = await instantiateImageCodec(bytes);\\n for (int i = 0; i < codec.frameCount; i++) {\\n final frame = await codec.getNextFrame();\\n // 处理每一帧\\n }\\n}\\n
\\nImageProvider
实战class EncryptedImageProvider extends ImageProvider<EncryptedImageProvider> {\\n final String encryptedData;\\n \\n @override\\n ImageStreamCompleter loadBuffer(..., DecoderCallback decode) {\\n final decrypted = _decrypt(encryptedData); // 解密逻辑\\n return MemoryImage(decrypted).loadBuffer(..., decode);\\n }\\n}\\n
\\nFlutter
图片系统的架构思想API
的设计精髓React Native
:需手动管理图片状态。Flutter
:通过Image
组件自动处理生命周期。Skia
引擎的统一渲染:\\nAndroid
还是iOS
,最终通过Skia
将图片数据渲染为GPU
指令。高可用图片处理方案
void _loadImage() {\\n final startTime = DateTime.now();\\n final image = Image.network(url);\\n image.image.resolve(ImageConfiguration()).addListener(\\n ImageStreamListener((info, _) {\\n final loadTime = DateTime.now().difference(startTime);\\n _reportMetric(\'load_time\', loadTime.inMilliseconds);\\n }),\\n );\\n}\\n
\\nCDN
优化CDN
最佳实践:\\nURL
参数如?dpr=2
)。HTTP/2
协议提升并发加载速度。3
次)。Flutter
的Image
组件是一个多层次的系统,从基础属性到渲染管线,从内存管理到跨平台适配,每个环节都需精心设计。开发者需以性能为导向,结合cacheWidth
、预加载
和格式优化
,同时利用errorBuilder
和frameBuilder
提升用户体验。
深入源码理解ImageProvider
机制,能帮助定制高级功能(如加密图片
),而设计哲学(如声明式API
)则指导架构决策。最终,通过监控
、动态化
与团队协作规范
,构建高可用、高扩展的图片处理体系。
\\n","description":"前言 在移动应用开发中,图片是用户界面最直观的视觉元素之一。无论是社交媒体的动态展示、电商平台的商品列表,还是新闻应用的内容呈现,图片的高效加载、渲染和管理都直接影响用户体验。\\n\\nFlutter的Image组件作为UI体系的核心部分,不仅支持本地资源、网络图片、文件系统和二进制数据等多种加载方式,还通过高度灵活的API和底层优化机制(如缓存、分辨率适配、懒加载)确保了性能与视觉效果的平衡。然而,许多开发者仅停留在基础使用层面,对Image的底层原理、性能优化策略和设计哲学缺乏系统化认知,导致应用中频繁出现内存泄漏、图片闪烁、加载卡顿等问题。\\n\\n本文将从六大…","guid":"https://juejin.cn/post/7473463198534074422","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-21T02:52:56.320Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b096434ad6244b36b0b886601b37a594~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1740715227&x-signature=qVMUsp1gT5UWe5rQNTJQ%2FuN6WbU%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"[Flutter] 嵌套滚动与弹性嵌套滚动","url":"https://juejin.cn/post/7473440825101680691","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
如果你真的按照上一篇文章的内容:[Flutter] 多层级嵌套滚动
\\n中,那样去实现了这样的多层级嵌套滑动的视图,你会发现当你手指一直按着屏幕的时候,滚动是正常的,每个层级都可以被很好地协调,并且正常地滚动,但是如果你仔细看第二段,你就会发现滚动整体非常「涩」,因为虽然手指在ListView3的滚动确实可以很好地传递到CustomScrollView3/2/1上,但是,当你手指离开屏幕之后,ListView3会触发一个「惯性滚动」,惯性滚动只会发生在ListView3上,一旦ListView3滚动到顶,惯性滚动就停了,即使这个惯性滚动还有很大的余量,他也不会导致上层的CustomScrollView3被惯性滚动所推动。
\\n原因很简单------惯性滚动丢失了。
\\n在 Flutter 中,惯性滚动是指当用户在可滚动组件(如 ListView、GridView 等)上进行快速滑动操作后,即使手指已经离开屏幕,组件仍会基于之前的滑动速度继续滚动一段距离,然后逐渐减速直至停止的效果。
\\n划重点:手指离开屏幕、继续滚动。
\\n既然手指离开了屏幕需要如何继续滚动呢?滚动量从哪里来?答案是动画。惯性滚动本质上是在滚动后,手指离开屏幕之后,根据此前的速度(velocity),生成一段动画,动画会模拟阻尼,计算当前的速度在多久之后会停止,就好比你在一个桌子上往前推一个方块,由于摩擦力的原因,他可能会在1~3秒之内停止滑动,这个过程中方块可能会移动1米的距离。
\\n这个1~3秒和1米就是惯性滚动动画的两个非常重要的属性:distance和duration,前者描述了本次惯性滚动一共要滚多远,后者描述了多久会停止。如果是线性动画,那么1米的距离1秒内要完成,那么这就意味着动画每次触发的时候会对单位时间生成均匀的偏移量,然后在某个时间节点上附加到可滚动视图上。
\\n这个过程就是动画生成的,在Flutter中,Simulation就是用来生成偏移量的模拟类,然后将生成的偏移量或者动画值交给AnimationController,例如ClampingScrollSimulation,它会在传入的position数值的基础上生成新的position数值:
\\n\\n@override\\ndouble x(double time) {\\nfinal double t = clampDouble (time / _duration, 0.0 , 1.0 );\\n return position + _distance * ( 1.0 - math. pow ( 1.0 - t, _kDecelerationRate ));\\n}\\n
\\n然后交给AnimationController,在每次tick时,AnimationController会访问Simulation,取出最新的数值:
\\n\\nvoid _tick(Duration elapsed) {\\n ...\\n _value = clampDouble(_simulation!.x(elapsedInSeconds), lowerBound, upperBound);\\n ...\\n}\\n
\\n在手指停止操作视图之后,Scrollable会处理DragEndDetails事件(scrollable.dart),然后会调用drag.end的一个回调,而这个end方法的实现最终可以追溯到:Drag的实现类:ScrollDragController中,其中的end方法中调用了delegate.goBallistic(velocity);
,这行调用的含义是:我滚动完成了,然后告诉我对应的ScrollActivityDelegate
,你将要开始一个BallisticScroll的流程,也就是开始惯性滚动。
对于goBallistic
比较常见的实现,可以参考ScrollPositionWithSingleContext:
@override\\nvoid goBallistic(double velocity) {\\n assert(hasPixels);\\n final Simulation? simulation = physics.createBallisticSimulation(this, velocity);\\n if (simulation != null) {\\n beginActivity(BallisticScrollActivity(\\n this,\\n simulation,\\n context.vsync,\\n activity?.shouldIgnorePointer ?? true,\\n ));\\n } else {\\n goIdle();\\n }\\n}\\n
\\n其中,创建了Simulation,然后开始滚动,前面我们有提到,Simulation其实是惯性滚动动画的数值生成器,本质上是在处理时刻t和视图最新偏移量的关系。
\\n在BallisticScrollActivity
中,有对应的AnimationController,然后再动画开始之后,会不断地去执行_tick回调,这里面其实就是在不断地去操作视图滚动了:
void _tick() {\\n if (!delegate.setPixels(value).abs() < precisionErrorTolerance) {\\n delegate.goIdle();\\n }\\n}\\n
\\n这个_tick会随着动画的执行和剩余滚动量的消耗有两种结果:
\\n此时AnimationController会走对应的_end
回调:
void _end() {\\ndelegate. goBallistic ( 0.0 );\\n}\\n
\\n这个0.0的Velocity,最终会走到ClampingScrollPhysics的createBallisticSimulation,然后认为速度太小了,就返回一个null,所以最终这里创建出来的Simulation就是null,走了goIdle逻辑。
\\ngoIdle会用一个IdleScrollActivity替换掉现在正在执行的BallisticScrollActivity,BallisticScrollActivity会立即被dispose掉,对应的AnimationController也会dispose,因此惯性滚动动画也会停止。
\\n\\n\\n类似直接替换ScrollActivity来改变滚动行为的操作,在惯性滚动,然后手指突然按住屏幕停止惯性滚动的地方也有用到。
\\n
此时会命中tick方法的if内部,最终也是利用一个goIdle去停止惯性滚动。
\\n由此我们得到了一个重要的参考,用**goIdle
**来停止一个惯性滚动动画。
\\n\\n在后续的实践过程中,我们其实会发现Scroll体系内有大量的断言会校验当前的组件或者ScrollActivity状态,例如你调用滚动方法的时候,可能会校验当前的ScrollActivity是否是处于isScrolling状态;这就要求我们不仅仅要维护滚动量,我还需要正确地去维护对应的ScrollActivity。
\\n
究其根本,只是惯性滚动量(BallisticScroll)它没有像我们之前实现的手指滚动(FingerScroll)那样,将盈余的滚动量传递出去, 我们要解决的正是这个问题。
\\n如果你参考当前惯性滚动事件,你会发现这里的BallisticScrollActivity、AnimationController、Simulation和ScrollView是一对一处理的。
\\n要传递滚动量不是一件简单的事情,前面我们提到了,如果在当前组件A处理完了自己需要的滚动量之后,BallsticScrollActivity会被一个IdleScrollActivity替代掉。AnimationController会立即被dispose,以至于动画终止。
\\n因此,我们自然要保证动画在此时不停止,我们就要维持着ScrollView对应BallisticScrollActivity状态,然后动画仍然在运行,对应的动画偏移量的应用要替换到新的目标View上,假设此时惯性滚动量需要从ListView3向上传递,传递到CustomScrollView3上,这个流程会是:
\\n这个过程中存在的问题:
\\n本质上还是因为滚动滚动的动画与ListView是一对一的关系,导致我们难以处理这些问题。
\\n我们知道,根据滚动方向的不同,滚动量的消费顺序其实不同,一个CustomScrollView从收缩状态展开的时候,其实是祖先节点优先消费,最后才是触摸事件发生的控件节点消费。在手指触发的滚动中,我们可以通过ElementTree去做向上查询,然后找到对应的目标节点,然后进行消费,并且将多余的滚动量直接通过函数调用栈返回给下一层的节点。
\\n这件事情在惯性滚动量的传递中,是很难做到的,因为惯性滚动本质上是在启动动画,动画生成新的偏移量,偏移量去修改可滚动视图的偏移量。而动画变量生成的时候,产生偏移量更新视图的过程,其实已经在事件循环的下一个事件中了,无法直接通过函数调用栈进行返回。
\\n这会对我们的传递造成一定的困难。
\\n如果我们要跳脱出现有的Scroll体系,去设计一个支持多层级嵌套滚动的组件,经过总结上面的问题,我们知道它必须要满足两个条件:
\\nScrollPositionDelegate
**;(通俗点说就是动画会陆续操作多个层级的可滚动视图的偏移量)解决这两个问题,我们回到惯性滚动的起点,也就是发生DragEnd事件的Scrollable组件本身,它通过delegate.goBallistic(velocity)
来启动一次惯性滚动,这里的delegate,就是ScrollPositionDelegate,也就是我们所说的滚动的「后端」,用于描述滚动偏移量的类。我们要在之前实现的ScrollViewExPosition
中,重写它的goBallistic
方法,处理velocity的值,然后提交给我们之前定义的ScrollViewExCoordinator:
@override\\nvoid goBallistic(double velocity) {\\n goIdle();\\n if (velocity.abs() < precisionErrorTolerance) {\\n return;\\n }\\n coordinator.onBallisticSubmit(velocity);\\n}\\n
\\n此前的goBallistic就不能再使用ScrollPhysics来获取父布局定义的Simulation了,因为后续我们需要通过Coordinator创建统一的、定制化的自定义的Simulation,因此我们这里按照ClampingScrollSimulation中的逻辑简单处理:
\\nCoordinator
处理。Coordinator中,我们在生成动画之前,先沿着Element Tree进行一次向上的递归搜索,这个和手指滚动的传递类似,只不过是在搜集所有可能发生滚动的节点,构成一个双端队列:
\\n//// 搜集可滚动的视图\\nvoid assembleToQueue(ScrollViewExBallisticQueue queue) {\\n ScrollViewExCoordinator? current = this;\\n while (current != null) {\\n queue.push(current);\\n current = current.parentCoordinator;\\n }\\n T.i(\\"(FlutterSourceCode)[coordinator.dart]->assemble pha:${queue.list.map((e) => e.key)}\\");\\n}\\n
\\n在这个assembleToQueue之后,对于这样的一个视图,你的queue队列中,应该存储了所有的绿色节点,这些绿色结点都是一个可能被惯性滚动动画所滚动的视图:
\\n因此,你此刻会有如下的队列:[ListView1、CustomScrollView#3、CustomScrollView#2、CustomScrollView#1]。
\\n由于我们动画控制器AnimationController会变成一对多的关系,一个动画控制器会对应着这队列中的所有组件,因此我们直接在队列中处理这个动画和Simulation,为Queue对应的类增加如下的方法和变量
\\n///// Animation\\nlate AnimationController _controller;\\nlate TickerProvider vsync;\\nlate ExClampingScrollSimulation _simulation;\\n\\nvoid start() {\\n if (size == 0) {\\n return;\\n }\\n _simulation = ExClampingScrollSimulation(velocity: velocity);\\n _controller = AnimationController.unbounded(\\n debugLabel: kDebugMode\\n ? objectRuntimeType(this, \'ExBallisticScrollActivity\')\\n : null,\\n vsync: vsync,\\n )\\n ..addListener(_tick)\\n ..animateWith(_simulation).whenComplete(_end);\\n T.i(\\"(FlutterSourceCode)[queue.dart]->velocity:$velocity\\");\\n}\\n
\\n其中的_tick是动画量生成的回调方法,而_end是动画量消耗完之后的回调方法:
\\nvoid _tick() {\\n if (consuming == null) {\\n _controller.stop();\\n return;\\n }\\n double controllerValue = _controller.value;\\n if (!_applyToTarget(consuming!, controllerValue)) {\\n pop();\\n }\\n}\\n\\nbool _applyToTarget(ScrollViewExCoordinator target, double value) {\\n target. goBallisticFragment(velocity, true);\\n return target.onBallisticDispatch(value: value, velocity: velocity) <\\n precisionErrorTolerance;\\n}\\n\\nvoid _end() {\\n consuming?.goIdle();\\n}\\n
\\n_tick的逻辑很简单,AnimationController生成的动画量交给当前正在执行的consuming对象(它表示的是当前正在接受AnimationController控制的Coordinator),如果consuming已经滚动到顶(底)了之后,就调用pop(),更换节点。
\\n由于传播本身是有方向性的,方向不同会影响是队头结点优先消费,还是队尾结点优先消费,因此,我们需要根据velocity这个矢量来判断从哪一个出口获取当前正在消费的结点,pop方法也是如此,需要决定哪个出口的节点出队。
\\nScrollViewExCoordinator ? get consuming => list. isEmpty ? null : velocity < 0 ? list. first : list. last ;\\n \\n \\nvoid pop () {\\n ScrollViewExCoordinator ? coordinator;\\n if (velocity < 0 ) {\\ncoordinator = list. removeAt ( 0 );\\n} else {\\ncoordinator = list. removeLast ();\\n}\\ncoordinator. goIdle ();\\n} \\n
\\n出队后的节点置为idle状态即可。
\\n这样一来,所维护的队列的四个结点,就在同一个Simulation、AnimationController的驱动下完成了惯性滚动量的传递,前面我们提到Simulation我们也要去定制,这是因为普通的ClampScrollSimulation会根据它绑定ScrollPositionDelegate的偏移量去生成只属于这个ScrollPositionDelegate的数值:
\\n@override\\ndouble x(double time) {\\n final double t = clampDouble(time / _duration, 0.0, 1.0);\\n return position + _distance * (1.0 - math.pow(1.0 - t, _kDecelerationRate));\\n}\\n
\\nposition在多个可滚动视图中传递的过程中是会发生变更的。
\\n我这里的思路其实非常简单粗暴,如果position会有影响,偏移量 + 后面的一串数值 = 新的偏移量,那么后面的这一串代码就是动画生成的偏移量数值,也就是当前X相较于初始状态0的偏移量,我们只要拿到每一个tick发生时对应的tick即可,我们记录lastX,然后新的偏移量 - 旧的偏移量即可得到当前tick和上一个tick中的deltaX:
\\ndouble lastX = 0;\\n\\n@override\\ndouble x(double time) {\\n final double t = clampDouble(time / _duration, 0.0, 1.0);\\n double newX = _distance * (1.0 - pow(1.0 - t, _kDecelerationRate));\\n double deltaX = newX - lastX;\\n lastX = newX;\\n return deltaX;\\n}\\n
\\n这样每次返回的deltaX就是合理的。
\\ntarget.goBallisticFragment的实现,让当前正在操作的consuming节点进入滚动状态而已,否则Scrollable会校验当前activity的isScrolling是否为true,然后大量地抛出断言异常。
\\n\\nvoid goBallisticFragment(double velocity, bool shouldIgnorePointer) {\\n _currentPosition.beginActivity(ExBallisticAnimateScrollActivity(\\n _currentPosition, velocity, shouldIgnorePointer));\\n}\\n
\\n将上面的代码组装起来,就能实现相关的功能:
\\n\\n
总的流程如下:
\\n在认识惯性滚动之前,我们需要认识一下Flutter Scrollable组件的滚动实现,即手指触摸到屏幕再到内容滚动是如何发生的。
\\nScrollActivity是一个抽象类,现实中的含义是表示当前可滚动控件的滚动活动类型,比如在一次手指滚动过程中,会创建如下的几种实例对象:
\\nHoldScrollActivity\\nHoldScrollActivity\\nHoldScrollActivity\\nIdleScrollActivity\\nIdleScrollActivity\\nDragScrollActivity\\nBallisticScrollActivity\\nIdleScrollActivity\\n
\\n以手指在ListView上滚动为例:
\\nHoldScrollActivity
对应手指按下时刻ListView的滚动活动;IdleScrollActivity
则对应停止滚动的ListView的滚动活动;DragScrollActivity
意指手指开始拖动时的ListView的滚动活动;BallisticScrollActivity
**则对应惯性滚动开始
时ListView的滚动活动;这其中IdleScrollActivity
我们可以猜到它代表的就是ListView的禁止状态;而DragScrollActivity
代表的就是手指滚动状态,BallisticScrollActivity
则是惯性滚动状态,从这些现实意义进行切入,我们就可以去看看它们的实现了。
\\n\\n注意,我们暂时不太需要关注它们的实现细节,更需要关注它们的现实意义,Activity对应的就是一种造成滑动的场景。
\\n
IdleScrollActivity
和HoldScrollActivity
IdleScrollActivity的注释就说明了一切:
\\n\\n\\nA scroll activity that does nothing.
\\n
它就是一个什么也不做的ScrollActivity,它对所有参数、方法的实现也没有任何复杂的逻辑:
\\nclass IdleScrollActivity extends ScrollActivity {\\n IdleScrollActivity( super .delegate);\\n\\n @override\\n void applyNewDimensions() {\\n delegate.goBallistic( 0.0 );\\n }\\n\\n @override\\nbool get shouldIgnorePointer = > false ;\\n\\n @override\\nbool get isScrolling = > false ;\\n\\n @override\\n double get velocity = > 0.0 ;\\n}\\n
\\n而对比之下,HoldScrollActivity
则比IdleScrollActivity多了一个感知HoldEvent
的能力,当手指Hold在一个Scrollable上时,该Scrollable
便会切换到HoldScrollActivity
状态,代码就不贴出来了。
\\n\\nThe activity a scroll view performs when the user drags their finger across the screen.
\\n当用户在屏幕上拖动手指时,滚动视图所执行的活动。
\\n
通过注释我们就可以看出来用户手指在屏幕上滚动时就会切换到DragScrollActivity
,其中主要是对一些滚动相关的事件做了分发处理:
@override\\nvoid dispatchScrollStartNotification(ScrollMetrics metrics, BuildContext? context) {\\n final dynamic lastDetails = _controller!.lastDetails;\\n assert(lastDetails is DragStartDetails);\\n ScrollStartNotification(metrics: metrics, context: context, dragDetails: lastDetails as DragStartDetails).dispatch(context);\\n}\\n\\n@override\\nvoid dispatchScrollUpdateNotification(ScrollMetrics metrics, BuildContext context, double scrollDelta) {\\n final dynamic lastDetails = _controller!.lastDetails;\\n assert(lastDetails is DragUpdateDetails);\\n ScrollUpdateNotification(metrics: metrics, context: context, scrollDelta: scrollDelta, dragDetails: lastDetails as DragUpdateDetails).dispatch(context);\\n}\\n\\n@override\\nvoid dispatchOverscrollNotification(ScrollMetrics metrics, BuildContext context, double overscroll) {\\n final dynamic lastDetails = _controller!.lastDetails;\\n assert(lastDetails is DragUpdateDetails);\\n OverscrollNotification(metrics: metrics, context: context, overscroll: overscroll, dragDetails: lastDetails as DragUpdateDetails).dispatch(context);\\n}\\n\\n@override\\nvoid dispatchScrollEndNotification(ScrollMetrics metrics, BuildContext context) {\\n // We might not have DragEndDetails yet if we\'re being called from beginActivity.\\n final dynamic lastDetails = _controller!.lastDetails;\\n ScrollEndNotification(\\n metrics: metrics,\\n context: context,\\n dragDetails: lastDetails is DragEndDetails ? lastDetails : null,\\n ).dispatch(context);\\n}\\n
\\n例如:
\\nScrollStartNotification(metrics: metrics, context: context, dragDetails: lastDetails as DragStartDetails).dispatch(context); \\n
\\n就是从当前Scrollable组件对应的BuildContext向外派发了一个ScrollStartNotification
事件。
\\n\\n再温习一下ListView和它背后的Scrollable是如何被滚动的。
\\n
除此之外,ScrollDragActivity会创建一个非常重要的对象:ScrollDragController。
\\n它是手指在滚动Scrollable时,不断产生Update事件的控制器,一旦Scrollable开始一次Drag行为,一个ScrollDragController对象就会被创建,然后关联到ScrollableState上,后续这个State对应的Element在不断地接收到来自系统的滚动事件时,就直接会派发到这个ScrollDragController之上,而ScrollDragController会调用相关的方法来设置ScrollPosition的偏移数值,然后更新RenderObject:
\\n一旦一个DragScrollActivity完成了它的使命之后,随之而来的就是这个实例被废弃,比如在DragScrollActivity变更到IdleScrollActivity时,在ScrollPositionWithSingleContext中beginActivity的实现中就可以看到对Drag行为的置空操作:
\\n\\n\\n总结一下
\\n
DragScrollActivity
两个作用:
派发滚动Notification
创建ScrollDragController
对象,并交给ScrollableState所引用,然后ScrollableState收到滚动事件时传递给ScrollDragController
,调用ScrollPosition的pixels变更,然后通知RenderObject更新视图的偏移量。
在新的ScrollActivity被begin之后,Scrollable的_drag
会立即被置为null,并dispose掉既有的ScrollActivity。
\\n\\nAn activity that animates a scroll view based on a physics [Simulation].
\\n一项基于物理 [模拟] 使滚动视图具有动画效果的活动。
\\n
Simulation,也就是模拟,在我们处理惯性滚动时,我们需要根据手指Drag的时长
、距离
和预设的阻尼系数
等参数来估算接下来惯性滚动的速度,说人话就是一个基于时间的动画数值计算函数,基于时间t产生deltaX的一个函数:
deltaX = simulation(t) \\n
\\n惯性滚动,很大程度上就是根据动画自动地去决定手指离开屏幕之后接下来Scrollable的变更偏移量,因此,BallisticScrollActivity比其他的ScrollActivity会多一个AnimationController
,需要实现动画所以它需要感知系统的Vsync信号,自然它的构造函数:
BallisticScrollActivity(\\n super.delegate,\\n Simulation simulation,\\n TickerProvider vsync,\\n this.shouldIgnorePointer,\\n) {\\n _controller = AnimationController.unbounded(\\n debugLabel: kDebugMode ? objectRuntimeType(this, \'BallisticScrollActivity\') : null,\\n vsync: vsync,\\n )\\n ..addListener(_tick)\\n ..animateWith(simulation)\\n .whenComplete(_end); // won\'t trigger if we dispose _controller first\\n}\\n
\\n其次,它多了一个_tick
方法,结合对Vsync信号的理解,和上面BallisticScrollActivity
构造函数中对于AnmationController的创建,我们可以猜测,AnimationController最终会通过这个方法引用来将最新的动画数据,也就是最新时刻对应的t生成的deltaX交付给BallisticScrollActivity来产生动画位移:
void _tick() {\\n if (!applyMoveTo(_controller.value)) {\\n delegate.goIdle();\\n }\\n}\\n
\\n而applyMoveTo则就是拿着ScrollPosition去设置位移量了:
\\n@protected\\nbool applyMoveTo(double value) {\\n return delegate.setPixels(value).abs() < precisionErrorTolerance;\\n}\\n
\\n这里的value
,是BallisticScrollActivity对应的AnimationController动画中ClampingScrollSimulation
基于当前时间t生成的动画数值。
\\n\\n\\n
ClampingScrollSimulation
数值生成由两个部分构成:\\n
其中的position数值是当前ScrollActivity对应的可滚动组件(Scrollable)的偏移量,也就是对应的ScrollPosition的pixels变量;而绿色部分则是delta,即根据t simulate出来的数值simulate出来的数值,这俩个数值相加返回的数值其实是ScrollPosition在当前惯性滚动动画下的newPixels。
\\n所以在applyMoveTo方法中,直接将返回的double类型数值调用
\\ndelegate.setPixels(double)
赋值给了当前的可滚动视图,也就是当前视图的最新偏移量。
ScrollPosition其实我们之前已经有过介绍了,他就是用来维护当前Scrollable组件偏移量的一个代理对象,目前来说我们需要知道它有三个非常重要的数值:
\\n\\n\\n它直接维护着一个可滚动视图的滚动状态,这与我们之前介绍的ScrollActivity其实是密不可分的,因为ScrollActivity在被begin之后,如果需要修改可滚动视图的偏移量就必然需要操作ScrollPosition。
\\n而Flutter使用了一种前后端分离的模型来处理这二者之间的关系,以适配不同的ScrollActivity和不同的ScrollPosition之间的关系。
\\n这就是ScrollActivity的分离模型。
\\n
ScrollActivity作为前端。
\\n而ScrollActivity需要操作ScrollPosition来变更可滚动视图的偏移量,这个操作可能是直接的,也可能是间接的。
\\n比如ListView的ScrollActivity作为前端,就会直接操作ScrollPostition来完成偏移量的修改,进而完成滚动;
\\n而NestedScrollView产生的ScrollActivity并不会直接操作ScrollPosition。
\\n\\n\\n为什么呢?
\\n
因为NestedScrollView会有两个ScrollPosition,外层的CustomScrollView对应着outScrollPosition
,而内层的PrimaryScrollController对应着另一个innerScrollPosition
:
暂时无法在飞书文档外展示此内容
\\n因此,ScrollActivity作为前端,不能直接将ScrollPosition作为后端,而是需要找到各个接受ScrollActivity对象之间的最大公约数,因此ScrollActivityDelegate就诞生了:
\\n这里的delegate的实现类就是用来实际控制视图位移的类,一般是ScrollPosition,而注释中把ScrollActivityDelegate这一类东西称为ScrollActivity的后端(backend),我们就可以大致上确定Scrollable在滚动时大致上的模型就是:
\\nScrollActivityDelegate本身实现了多种不同事件的灵活切换。
\\n\\n\\n比如BallisticScrollActivity生效时,这时手指突然按在屏幕上,此时会用一个HoldScrollAtivity去替换掉生效的activity,能够立即快速地终止BallisticScrollActivity的惯性滚动效果。
\\n
对于ScrollActivityDelegate来说,他就约定了前后端之间需要交流的几件事情:
\\nabstract class ScrollActivityDelegate {\\nAxisDirection get axisDirection;\\ndouble setPixels(double pixels);\\nvoid applyUserOffset(double delta);\\nvoid goIdle();\\nvoid goBallistic(double velocity);\\n}\\n
\\n它的实现类:ScrollPosition要对如上的内容进行实现:
\\n _outerPosition!.axisDirection
;这五个内容都是要由ScrollActivityDelegate去重写的,我们今天的主要内容其实就是惯性滚动,例如ScrollPositionWithSingleContext对goBallistic的实现如下:
\\n@override\\nvoid goBallistic(double velocity) {\\n assert (hasPixels);\\n final Simulation? simulation = physics.createBallisticSimulation( this , velocity);\\n if (simulation != null) {\\n beginActivity(BallisticScrollActivity(\\n this ,\\nsimulation,\\ncontext.vsync,\\nactivity?.shouldIgnorePointer ?? true ,\\n));\\n } else {\\n goIdle();\\n }\\n}\\n
\\n主要的内容就两件事情:
\\n在BallisticScrollActivity开始之后,BallisticScrollActivity所创建的AnimationController就开始工作了,然后根据当前事件t,不断地输出当前的动画数值,然后回调BallisticScrollActivity#_tick->applyMoveTo方法,再调用后端ScrollPositionWithSingleContext设置pixels的数值:
\\nNestedScrollView对应的NestedScrollCoordinator会直接配合ScrollActivity来消费滚动活动,因此它也实现elScrollActivityDelegate:
\\n这就意味着它重写的setPixels方法的返回值永远会返回0,因为Coordinator本身并不维护偏移量,直接做滚动没有任何意义:
\\n@override\\ndouble setPixels(double newPixels) {\\n assert ( false );\\n return 0.0;\\n}\\n
\\n理论上永远调用不到setPixels方法,因为applyUserOffset中,就将具体的滚动量分发到outScrollPosition和innerScrollPostion中去了:
\\n@override\\nvoid applyUserOffset(double delta) {\\n updateUserScrollDirection(\\n delta > 0.0 ? ScrollDirection.forward : ScrollDirection.reverse,\\n );\\n assert(delta != 0.0);\\n if (_innerPositions.isEmpty) {\\n _outerPosition!.applyFullDragUpdate(delta);\\n // 此处省略一万个字\\n
\\n而其他ScrollActivityDelegate的实现类,比如ScrollPositionWithSingleContext,就会直接在ScrollActivityDelegate#applyUserOffset中调用setPixels来处理偏移量的变更和根据delta的正负来改变滚动轴的方向记录
\\n@override\\nvoid applyUserOffset(double delta) {\\n updateUserScrollDirection(delta > 0.0 ? ScrollDirection.forward : ScrollDirection.reverse);\\n setPixels(pixels - physics.applyPhysicsToUserOffset(this, delta));\\n}\\n
","description":"一、ScrollActivity 在认识惯性滚动之前,我们需要认识一下Flutter Scrollable组件的滚动实现,即手指触摸到屏幕再到内容滚动是如何发生的。\\n\\nScrollActivity是一个抽象类,现实中的含义是表示当前可滚动控件的滚动活动类型,比如在一次手指滚动过程中,会创建如下的几种实例对象:\\n\\nHoldScrollActivity\\nHoldScrollActivity\\nHoldScrollActivity\\nIdleScrollActivity\\nIdleScrollActivity\\nDragScrollActivity…","guid":"https://juejin.cn/post/7473440825101533235","author":"开中断","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-21T02:11:24.187Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2cea476b1e9249c7b28d61724c21e8d5~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5byA5Lit5pat:q75.awebp?rk3s=f64ab15b&x-expires=1740708683&x-signature=sknAgci3RDnCpi01BEmejy3Jcgs%3D","type":"photo"},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fcb85821105940c591c7017f285a8cd3~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5byA5Lit5pat:q75.awebp?rk3s=f64ab15b&x-expires=1740708683&x-signature=VqRfUwIF0%2BbtumRycdHRkcvTAe8%3D","type":"photo"},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/91cc5425f99140c7a86de3756dcdf3b6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5byA5Lit5pat:q75.awebp?rk3s=f64ab15b&x-expires=1740708683&x-signature=QI3uYJysKordpLVdUU%2FwH1SIyuQ%3D","type":"photo"},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c5286663c28d457482db57faea4792ff~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5byA5Lit5pat:q75.awebp?rk3s=f64ab15b&x-expires=1740708683&x-signature=%2B4fZ5cQXBaRYUSWQHhjF76qbPA8%3D","type":"photo"},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1541be05b50344ac9ea26ddd5f66454e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5byA5Lit5pat:q75.awebp?rk3s=f64ab15b&x-expires=1740708683&x-signature=eKf%2BU1%2BkSvmxx8N72NwyRToNFuY%3D","type":"photo"}],"categories":["Android","前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"[Flutter] 多层级嵌套滚动","url":"https://juejin.cn/post/7473431148958957578","content":"[Flutter] 多层级嵌套滚动
\\n在[Flutter] NestedScrollView与嵌套滚动、多子Widget的嵌套滚动Flutter使用sliver - 掘金中,我们介绍另一种SliverCompat的实现方式,以实现一对多场景下的嵌套滚动,其核心逻辑是在父组件中声明一个SliverCompat对象,然后通过给外层的CustomScrollController提供一个majorScrollController,给内部嵌套的ListView提供一个minorScrollController来区分内层、外层的ScrollController。
\\n并且在各自ScrollController接收到滚动事件之后,优先递交给SliverCompat对象进行处理,根据滚动的方向来判断将滚动量交给谁去消费,比如初始状态手指向下滑动,此时滑动量应先给CustomScrollView决定要不要消费,剩余的滚动量才交给子Widget消费,这样就能实现滑动量从CustomScrollView到ListView的丝滑过渡:
\\n这种实现能够满足一般的使用场景:即单级嵌套(例如CustomScrollView + 同一层级的ListView)滚动的场景,如果此时有多个CustomScrollView嵌套,这种实现就无法满足如下的场景了:
\\n理想情况下,它滚动起来应该是这样的,TabBar下的所有内容均支持按层级嵌套的多级嵌套滚动:
\\n\\n\\n1、其中的最上层的AppBar这里特殊处理过了,不参与嵌套滚动。
\\n2、遇到这东西建议先去和产品与UED Battle一下能不能换种实现方式,这篇文章仅是对上一篇文章的实现优化。
\\n
其实原理我们大致上已经知道了,要有一个统一的结构去处理滚动量
,而现有的内容结构大致如下:
这里的主要内容以TabView中的内容为主,一共是三个CustomScrollView为主体,然后:
\\n第一层CustomScrollView对应的左侧有两个ListView,而右侧是第二层CustomScrollView;
\\n第二层CustomScrollView左侧是一个FittedBox + Text组件,右侧是第三CustomScrollView;
\\n第三层CustomScrollView简单嵌套了一个ListView;
\\n如果单纯地去按照Major、Minor去设置ScrollController,那么这里会有非常复杂的嵌套关系,比如第二层CustomScrollView既是第一层CustomScrollView的MinorScrollController又是第三层的MajorScrollController,嵌套层级一多就近乎无法处理。
\\n如果跳脱出这种单纯的主、副
思维来看,嵌套滚动量在Widget Tree上的传递其实根本的是两个方向:
这就构成了很简单的树上的路径搜索算法,不过我们需要基于Flutter提供的结构树去处理我们的嵌套滚动量,我们要做的就是搜集所有可滚动组件(ListView、CustomScrollView等等)的滚动量,然后交给统一的结构:SliverCompat去处理,交给谁呢?
\\n我们可以简单地画这个图:
\\n其中黄色部分的就是三级CustomScrollView的嵌套,而红色部分则是ListView,无论是黄色还是红色,这两种结点都是可以独立接收滑动事件的,换句话说,我们就是要去控制它们的滚动事件的统一处理。
\\n\\n\\n我明明手指按在AppBar上可以滑动啊,为什么AppBar不算在其中?
\\n其实手指在AppBar上滚动的时候,也是CustomScrollView接收的滚动事件,然后CustomScrollView来处理SliverAppBar的一些尺寸变化,这也正是CustomScrollView和ListView不完全兼容的原因,它们的ScrollController是隔离的,换言之NestedScrollView中只有外层的CustomScrollView和内层的PrimaryScrollController#child是可以滚动的,header部分的sliver组件一般都是不直接滚动的 。
\\n
我们按照ScrollDirection的规则,规定两个滚动方向:forward和reverse,其中forward方向表示列表在初始状态下,手指向上拖动时,列表整体向下滚动的操作;reverse则相反,表示列表向上滚动的操作。
\\n如果此时进行forward操作,手指在ListView3上滚动,ListView3在收到滚动事件时,不应该由自己去消费,而是应该交给CustomScrollView3去消费。但是CustomScrollView3其实又是CustomScrollView2的子Widget,因此CustomScrollView3需要将滚动量提交给CustomScrollView2,让它决定是否去消费,直到CustomScrollView1中。它对于这一套滚动系统来说是一个隔离的根节点,因此而不需要再向上传递了,换句话说我们的滚动应该先交给顶层的CustomScrollView去消费,这样我们就可以得到这样的一条完整的传递路线,沿着下图的绿色结点自底向上地传递:
\\n其中有几个节点是可以处理滚动事件的,分别是:
\\nListView3自己、CustomScrollView3、CustomScrollView2和最顶层的CustomScrollView1。
\\n不难看出我们要控制着四者的协调滚动,上一版本的SliverCompat仅仅区分了主、副,无法很好地实现这个需求,对应的四个节点都需要:
\\n1、自己可滚动
\\n2、可向上提交滚动量
\\n3、可向下返回剩余的滚动量
\\n\\n\\n备注:
\\n\\n
\\n- 如果是Reverse方向滚动,那么这个滚动量就应该是ListView3先消费,然后盈余的滚动量再给父布局消费了。
\\n- 这个方向和消费顺序不光是嵌套滚动的消费顺序,还是后续惯性动画的消费顺序,如果你要实现多层级的惯性动画消费那么也必须考虑这个顺序问题。
\\n
可接受滚动事件的结点它们各自都应该成为一个SliverCompat的处理结点,为了和上一篇文章中的普通的SliverCompat区分,我们叫他ScrollViewExCoordinator
。
\\n对于ListView3,它会和一个
ScrollViewExCoordinator
做绑定,ScrollViewExCoordinator
提供一个ScrollViewExController
的实例,交给ListView
,用于给ListView
的controller
字段赋值。\\nListView的滚动就完全交给ScrollViewExCoordinator
、ScrollViewExController
和ScrollViewExPosition
来处理。
如何接受滚动量在上文我们已经提过了:需要自己去实现我们的ScrollController,然后重写插件ScrollPosition的方法:
\\nclass ScrollViewExController extends ScrollController {\\n final Key? key;\\n\\n ScrollViewExController(this.key);\\n\\n late ScrollViewExCoordinator? _coordinator;\\n\\n ScrollViewExCoordinator get coordinator => _coordinator!;\\n\\n attachToCoordinator(ScrollViewExCoordinator coordinator) {\\n _coordinator = coordinator;\\n }\\n\\n detachFromCoordinator() {\\n _coordinator = null;\\n }\\n\\n @override\\n ScrollPosition createScrollPosition(ScrollPhysics physics,\\n ScrollContext context, ScrollPosition? oldPosition) {\\n return ScrollViewExPosition(\\n physics: physics, context: context, coordinator: coordinator);\\n }\\n\\n @override\\n void dispose() {\\n super.dispose();\\n _coordinator?.dispose();\\n detachFromCoordinator();\\n }\\n}\\n
\\n而ScrollViewExPosition
的整体实现:
class ScrollViewExPosition extends ScrollPositionWithSingleContext {\\n ScrollViewExPosition(\\n {required super.physics,\\n required super.context,\\n required this.coordinator});\\n\\n @override\\n String toString() {\\n return \\"${super.toString()},<key:[${coordinator.key}]>\\";\\n }\\n\\n ScrollViewExCoordinator coordinator;\\n\\n bool get shouldIgnorePointer => activity?.shouldIgnorePointer ?? false;\\n\\n @override\\n void applyUserOffset(double delta) {\\n double fingerOverscroll = coordinator.applyUserFingerScrolling(delta);\\n }\\n\\n double applyClampedDragUpdate(double delta) {\\n assert(delta != 0.0);\\n final double minValue =\\n delta < 0.0 ? -double.infinity : min(minScrollExtent, pixels);\\n final double maxValue = delta > 0.0\\n ? double.infinity\\n : pixels < 0.0\\n ? 0.0\\n : max(maxScrollExtent, pixels);\\n final double oldPixels = pixels;\\n final double newPixels = clampDouble(pixels - delta, minValue, maxValue);\\n final double clampedDelta = newPixels - pixels;\\n if (clampedDelta == 0.0) {\\n return delta;\\n }\\n final double overscroll = physics.applyBoundaryConditions(this, newPixels);\\n final double actualNewPixels = newPixels - overscroll;\\n final double offset = actualNewPixels - oldPixels;\\n if (offset != 0.0) {\\n forcePixels(actualNewPixels);\\n didUpdateScrollPositionBy(offset);\\n }\\n return delta + offset;\\n }\\n\\n ScrollActivity? get currentScrollingActivity => activity;\\n}\\n
\\n其中的applyUserOffset
方法,用于直接从Drag事件处接收滚动量,如果你要统筹管理滚动量,那么肯定要在applyUserOffset处进行上报Coordinator进行处理。因此,重中之重就是Coordinator的实现了:
\\n//// 可滚动结点\\nclass ScrollViewExCoordinator {\\n ///// FIELDS or GETTER /////\\n late ScrollViewExController _currentNodeScrollController;\\n final BuildContext context;\\n final Key? key;\\n\\n ScrollViewExCoordinator(this.context, this.key);\\n\\n ScrollViewExController get currentScrollController =>\\n _currentNodeScrollController;\\n\\n ScrollViewExPosition get _currentPosition =>\\n currentScrollController.position as ScrollViewExPosition;\\n\\n ScrollViewExCoordinator? get parentCoordinator =>\\n ScrollViewExWidgetBuilderState.getCoordinator(context);\\n\\n ///// BUILD /////\\n /// 创建对应的ScrollController\\n ScrollViewExController createScrollViewExController(Key? key) {\\n _currentNodeScrollController = ScrollViewExController(key);\\n _currentNodeScrollController.attachToCoordinator(this);\\n return _currentNodeScrollController;\\n }\\n\\n void dispose() {\\n _currentNodeScrollController.detachFromCoordinator();\\n onFingerOverScrollingListener = null;\\n }\\n\\n ///// FINGER SCROLLING /////\\n /// 应用用户的滚动量,返回盈余滚动量\\n double applyUserFingerScrolling(double delta) {\\n if (delta < 0) {\\n // pass_to_top\\n double? overscroll;\\n if (parentCoordinator == null) {\\n overscroll = delta;\\n } else {\\n overscroll = parentCoordinator?.applyUserFingerScrolling(delta);\\n }\\n\\n if (overscroll == 0) {\\n return 0;\\n }\\n // consume by self\\n double remaining = _currentPosition.applyClampedDragUpdate(overscroll!);\\n return remaining;\\n } else {\\n if (delta < precisionErrorTolerance) {\\n return 0;\\n }\\n // consume by self\\n double overscroll = _currentPosition.applyClampedDragUpdate(delta);\\n if (overscroll < precisionErrorTolerance) {\\n return 0;\\n }\\n // pass_to_top\\n double? remaining = ScrollViewExWidgetBuilderState.getCoordinator(context)\\n ?.applyUserFingerScrolling(overscroll);\\n\\n return remaining ?? overscroll;\\n }\\n }\\n}\\n
\\n重点可以看applyUserFingerScrolling,外层if的两个分支分别代表了两个方向,由于forward和reverse滚动方向不同,所以消费事件的顺序也不同,所以要分开处理。\\n这里通过类似InheritedWidget向上查找的思路,直接传递滚动量,然后将消费完的剩余滚动量逐层返回,以实现嵌套滚动
\\n每个可滚动节点都应该是一个ScrollViewExCoordinator,因此我们需要一个代理Widget来完成这个构建SliverViewExCoordinator的工作:
\\n\\n\\n当然,你在每个地方手动声明也不是不可以,就是会很麻烦
\\n
\\ntypedef ScrollViewExBuilder = Function(\\n ScrollViewExController scrollViewExController);\\n\\nclass ScrollViewExWidgetBuilder extends StatefulWidget {\\n final ScrollViewExBuilder builder;\\n\\n const ScrollViewExWidgetBuilder(\\n {required this.builder, super.key});\\n\\n @override\\n State<ScrollViewExWidgetBuilder> createState() =>\\n ScrollViewExWidgetBuilderState();\\n}\\n\\nclass ScrollViewExWidgetBuilderState extends State<ScrollViewExWidgetBuilder> {\\n late ScrollViewExCoordinator _coordinator;\\n\\n @override\\n void initState() {\\n _coordinator = ScrollViewExCoordinator(context, widget.key);\\n super.initState();\\n }\\n\\n static ScrollViewExCoordinator? getCoordinator(BuildContext context) {\\n return context\\n .findAncestorStateOfType<ScrollViewExWidgetBuilderState>()\\n ?._coordinator;\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return widget\\n .builder(_coordinator.createScrollViewExController(widget.key));\\n }\\n}\\n
\\n使用时:
\\nScrollViewExWidgetBuilder(builder:\\n (controller) {\\n return ListView(\\n controller:controller\\n ...\\n )\\n})\\n
\\n这样就将一个ListView和一个ScrollViewExWidgetBuilder提供的ScrollViewExCoordinator/ScrollViewExController连接起来了,如果我们需要在组件间传递滚动量只需要在当前Coordinator节点对应的BuildContext
即可。
以ListView3为例,它拿到的coordinaotr就会是CustomScrollView3对应的ScrollViewExCoordinator,这样ListView3就可以调用它的方法向CustomScrollView3提交滚动量了,CustomScrollView3也需要走一样的流程,先向上查找更上层的ScrollViewExCoordinator的实例,如果找不到,说明自己就已经是根节点了,可以尝试着去消费对应的滚动量;如果找得到那么就继续向上传递,以初始情况列表展开为例,手指向上滑动时,代码如下:
\\nif (delta < 0) {\\n // pass_to_top\\n double? overscroll;\\n \\n if (parentCoordinator == null) {\\n overscroll = delta;\\n } else {\\n overscroll = parentCoordinator?.applyUserFingerScrolling(delta);\\n }\\n\\n if (overscroll == 0) {\\n return 0;\\n }\\n // consume by self\\n double remaining = _currentPosition.applyClampedDragUpdate(overscroll!);\\n return remaining;\\n}\\n
\\n以两个注释为例,代码被分为了三个部分:
\\n向外传递,ListView3会先把滚动量向上传递,如果父布局的Coordinator不为空则表示是一个有效的滚动结点,因此将滚动量直接交给CustomScrollView3进行消费;
\\nCustomScrollView3
递归地重复这个过程,直到某个节点的父布局Coordinator为空,此时认为它是一个顶层节点。顶层节点意味着递归的「回归过程」开始,顶层节点尝试调用applyClampedDragUpdate
去消费滚动量。
剩余的滚动量会逐渐回归到子组件,或者滚动量在中途被完全消耗。
\\n和NestedScrollView使用NestedScrollCoordinator统一管理所有的ScrollController、ScrollPosition不一样,我们在这里实现的ScrollViewExCoordinator则是将需要管理的结点单独成一个结点,然后依赖BuildContext Tree来处理它的上下级关系,这样做的好处很明显就是可以更加自由地在多个不同层级的Widget树中嵌套滚动量,缺点在于复杂视图结构可能难以直接维护层级关系。
","description":"[Flutter] 多层级嵌套滚动 一、单级嵌套滚动\\n\\n在[Flutter] NestedScrollView与嵌套滚动、多子Widget的嵌套滚动Flutter使用sliver - 掘金中,我们介绍另一种SliverCompat的实现方式,以实现一对多场景下的嵌套滚动,其核心逻辑是在父组件中声明一个SliverCompat对象,然后通过给外层的CustomScrollController提供一个majorScrollController,给内部嵌套的ListView提供一个minorScrollController来区分内层…","guid":"https://juejin.cn/post/7473431148958957578","author":"开中断","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-21T02:10:57.970Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/039f1835bd9d4ef484bfab232abbda4f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5byA5Lit5pat:q75.awebp?rk3s=f64ab15b&x-expires=1740708657&x-signature=8R0fivsTfMEYmZR8f7iy4dycJVM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4a039c925314457e929cd8d494013ee3~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5byA5Lit5pat:q75.awebp?rk3s=f64ab15b&x-expires=1740708657&x-signature=Mybo6zKdTOT89%2BMRlIb%2ByFb0JLg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7b87ad08ae564e42a66db0c480dce090~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5byA5Lit5pat:q75.awebp?rk3s=f64ab15b&x-expires=1740708657&x-signature=Mq%2FheKVKGG9N9dN12ijfsghKGTw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/133a3d2b2dcc49359e3313c8abea659f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5byA5Lit5pat:q75.awebp?rk3s=f64ab15b&x-expires=1740708657&x-signature=8agWvUo%2BW3%2BdbNZQV7Z3Z%2B%2F9xCQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/59a6da5791d546679f1af86107dd285f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5byA5Lit5pat:q75.awebp?rk3s=f64ab15b&x-expires=1740708657&x-signature=UHp05pj9TW15SxLii9hJ6%2BZ8AyI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0d31e5cad8504944820500765349b8e9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5byA5Lit5pat:q75.awebp?rk3s=f64ab15b&x-expires=1740708657&x-signature=xPfmV5BMeatf8C9u8h%2Bt2Pa1W70%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6ccf5c0e45e64b118937b80fd877ae58~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5byA5Lit5pat:q75.awebp?rk3s=f64ab15b&x-expires=1740708657&x-signature=L86sGqEFkLRKcj7kNopGm0v742A%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/de459c9ceccf44e6a1ec95c3e0f34446~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5byA5Lit5pat:q75.awebp?rk3s=f64ab15b&x-expires=1740708657&x-signature=VE0ySD6ry0MZEoEcOeEw6DOYmg0%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","前端"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 组件集录 | TweenAnimationBuilder 补间动画构造器","url":"https://juejin.cn/post/7473406723489284135","content":"当你想要让一个组件在出现时就具有动画效果,又懒得使用动画控制器,还懒得触发属性更新使用隐式动画。那么 TweenAnimationBuilder 将可以很好地帮助你完成动画效果。该组件已收录到 FlutterUnit,感兴趣的朋友可以参阅。
\\n下面案例中,组件在刚出现时,就会伴随背景由 蓝 到 红
的渐变动画。这里没有使用动画控制器来主动修改颜色属性;也没有通过隐式动画通过修改属性重新构建触发动画。
\\nTweenAnimationBuilder 组件在构造时通过 tween
字段,指定补间过渡的首尾属性;在 builder 回调中可以感知补间动画当前帧对应的值。根据该值就可以控制每个属性的动画效果。比如下面在 builder 中根据 value 值设置 Container 的颜色:
import \'package:flutter/material.dart\';\\n\\nclass TweenAnimationBuilderDemo extends StatelessWidget {\\n const TweenAnimationBuilderDemo({Key? key}) : super(key: key);\\n\\n @override\\n Widget build(BuildContext context) {\\n return TweenAnimationBuilder(\\n tween: ColorTween(begin: Colors.blue, end: Colors.red),\\n duration: const Duration(milliseconds: 800),\\n builder: (BuildContext context, Color? color, Widget? child) {\\n return Container(\\n width: 40,\\n height: 40,\\n child: child,\\n decoration: BoxDecoration(\\n color: color,\\n borderRadius: BorderRadius.circular(5)\\n ),\\n );\\n },\\n child: const Icon( Icons.android_outlined, color: Colors.white),\\n );\\n }\\n}\\n
\\n隐式动画的特点是不需要用开发者主动创建动画控制器,只需要修改属性,重新构建就可以触发动画变换。我们可以控制 tween 的起止值,达到动态控制动画的效果。比如下面案例中,每次点击时背景都会动画变换到另一种颜色,依次是红、橙、黄、绿、蓝、靛、紫 :
\\n由于需要修改起止的颜色值,进行重新构建,所以这里使用了 StatefulWidget
其中定义了:
colors
,_activeIndex
,class _TweenAnimationBuilderDemoState extends State<TweenAnimationBuilderDemo> {\\n List<Color> get colors => const [\\n Colors.red,\\n Colors.orange,\\n Colors.yellow,\\n Colors.green,\\n Colors.blue,\\n Colors.indigo,\\n Colors.purple\\n ];\\n\\n int _activeIndex = 0;\\n\\n Color get begin => colors[_activeIndex % colors.length];\\n\\n Color get end => colors[(_activeIndex + 1) % colors.length];\\n
\\n在 TweenAnimationBuilder 构造时取用 begin 和 end 构建新的颜色补间即可,点击时触发 nextColor
更新到写一个颜色索引。
void nextColor(){\\n _activeIndex++;\\n setState(() {});\\n}\\n
\\n这样就完成了上面的效果,总得来说 TweenAnimationBuilder 和常规的隐式动画用法类似,只不过它的属性是个 Tween 补间对象。而且可以在组件初始化时就立刻执行补间动画。
\\nTweenAnimationBuilder 继承自 ImplicitlyAnimatedWidget ,说明它的底层确实是补间动画的一套东西,
\\nTweenAnimationBuilder 可以在初始化时执行动画的原因,可以在源码中窥见本质。如下所示,在初始化状态时,如果补间对象的起止值不同,就会触发动画控制器的 forward
方法执行动画:
而动画控制器封装在父类 ImplicitlyAnimatedWidgetState
中,
当外界触发更新时,ImplicitlyAnimatedWidgetState 中的 didUpdateWidget 会触发,其中执行 forEachTween
的抽象方法,该方法将有子类实现,处理补间的具体更新逻辑:
从子类 _TweenAnimationBuilderState
的实现中可以看到,它会基于新补间参数的结尾和当前补间的值形成一个新的补间对象,继续进行动画。这也就是 TweenAnimationBuilder 在补间连续变化时,可以保持连贯性的秘密。
希望等你想做动画效果时 TweenAnimationBuilder 可以帮助到你,比如菜单展开时的动画,用 TweenAnimationBuilder 就很合适。那本文就到这里,更多的组件介绍分享,敬请期待 ~
","description":"当你想要让一个组件在出现时就具有动画效果,又懒得使用动画控制器,还懒得触发属性更新使用隐式动画。那么 TweenAnimationBuilder 将可以很好地帮助你完成动画效果。该组件已收录到 FlutterUnit,感兴趣的朋友可以参阅。 1. 一个案例简单了解 TweenAnimationBuilder\\n\\n下面案例中,组件在刚出现时,就会伴随背景由 蓝 到 红 的渐变动画。这里没有使用动画控制器来主动修改颜色属性;也没有通过隐式动画通过修改属性重新构建触发动画。\\n TweenAnimationBuilder 组件在构造时通过 tween 字段…","guid":"https://juejin.cn/post/7473406723489284135","author":"张风捷特烈","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-21T01:42:44.530Z","media":[{"url":"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b1631bc7df6f4ded9659c9ebe6bf96a9~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1920&h=1200&s=394246&e=png&b=fefbfb","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/212a5bf8cdf44668978e3e1cc27ed5bf~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1358&h=1002&s=191140&e=png&b=fdfdfd","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/13af729bfcf24ec893e663cfa1c83677~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1028&h=152&s=40654&e=gif&f=58&b=fefefc","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6ea1133d9ab44610b80a91444e99a331~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1028&h=152&s=277072&e=gif&f=268&b=fefefc","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ccbfeb3db6c9456780c8da8d242282f9~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=539&h=174&s=25891&e=png&b=fffefe","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/47c138ce2b4648c7b12605fcb28ec7c9~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=713&h=220&s=25917&e=png&b=ffffff","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/849c8dbc50d94e649a4616309576fce0~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=638&h=266&s=33292&e=png&b=ffffff","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1a0fb93786334792bfac7f343efaf1e9~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=707&h=203&s=33442&e=png&b=fffefe","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a62d19fd85684455b55430a9af0ad27e~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=589&h=234&s=30595&e=png&b=fffefe","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e7dcbb3873514e3389a5be301dd310c7~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=643&h=150&s=19815&e=png&b=fffefe","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"系统化掌握Flutter开发之Icon深度解析","url":"https://juejin.cn/post/7473309339295760420","content":"在移动应用的视觉体系中,图标(Icon
)是用户交互的核心元素
之一。Flutter
通过高度灵活的Icon
组件,为开发者提供了跨平台
、矢量化
的图标解决方案。
本文将以系统化视角,从基础认知到源码实现,全方位解析Flutter
图标技术。涵盖六大核心要点:基础属性解析
、进阶交互实现
、性能调优策略
、底层源码逻辑
、设计原则思考
、工程最佳实践
,以及图标体系的未来演进。
通过深度剖析,结合代码案例,帮助开发者构建完整的图标知识体系,解决实际开发中的疑难问题。
\\n操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\nIcon组件的本质与核心属性
Icon
核心属性详解Icon(\\n Icons.star, // 必选参数:图标数据\\n size: 24.0, // 逻辑像素尺寸(非物理像素)\\n color: Colors.amber, // 颜色覆盖机制\\n semanticLabel: \'收藏\', // 无障碍标签\\n textDirection: TextDirection.ltr, // 绘制方向\\n shadows: [ // 阴影效果\\n Shadow(\\n color: Colors.black54,\\n offset: Offset(2.0, 2.0),\\n ],\\n)\\n
\\n属性深度解析表:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n属性 | 类型 | 默认值 | 作用域 | 性能影响 |
---|---|---|---|---|
icon | IconData | 必填 | 图标定义 | 低 |
size | double? | 24.0 | 渲染尺寸 | 中 |
color | Color? | 主题色 | 颜色覆盖 | 低 |
semanticLabel | String? | null | 无障碍系统 | 无 |
textDirection | TextDirection? | 环境继承 | 复杂图标方向 | 低 |
shadows | List<Shadow>? | null | 视觉效果 | 高 |
// Material Design图标(默认)\\nIcon(Icons.home)\\n\\n// Cupertino风格图标(需单独引入包)\\nIcon(CupertinoIcons.gear)\\n\\n// 自定义字体图标\\nIcon(MyIcons.customIcon)\\n\\n// SVG图标(需通过第三方库转换)\\nSvgPicture.asset(\'assets/icon.svg\')\\n
\\n矢量图标 vs 位图图标:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n特性 | 矢量图标 | 位图图标 |
---|---|---|
缩放质量 | 无限缩放无锯齿 | 放大后模糊 |
文件大小 | 较小(字符编码) | 较大(像素数据) |
颜色修改 | 运行时动态修改 | 需预处理 |
多分辨率适配 | 自动适配 | 需多版本文件 |
适用场景 | UI 控制图标 | 复杂图形/照片 |
// 主题继承示例\\nIconTheme(\\n data: IconThemeData(color: Colors.blue),\\n child: Icon(Icons.star), // 实际颜色为蓝色\\n)\\n\\n// 局部覆盖示例\\nIcon(\\n Icons.star,\\n color: Colors.red, // 覆盖主题色\\n)\\n
\\n颜色优先级规则:
\\n\\n\\n\\n
Widget
局部color
> 局部IconTheme
> 全局IconTheme
> 默认主题色
动态图标与深度交互
class _AnimatedIconState extends State<AnimatedIconDemo> \\n with SingleTickerProviderStateMixin {\\n late AnimationController _controller;\\n\\n @override\\n void initState() {\\n _controller = AnimationController(\\n vsync: this,\\n duration: Duration(seconds: 1),\\n )..repeat();\\n super.initState();\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return AnimatedIcon(\\n icon: AnimatedIcons.view_list,\\n progress: _controller,\\n size: 48,\\n );\\n }\\n}\\n
\\n内置动画图标类型:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n动画类型 | 效果描述 |
---|---|
play_pause | 播放/暂停切换动画 |
menu_arrow | 菜单箭头展开动画 |
add_event | 添加事件的\\"+\\"号旋转动画 |
ellipsis_search | 搜索框省略号波动动画 |
IconButton(\\n icon: Icon(_isLiked ? Icons.favorite : Icons.favorite_border),\\n color: _isLiked ? Colors.red : Colors.grey,\\n onPressed: () {\\n setState(() {\\n _isLiked = !_isLiked;\\n });\\n },\\n)\\n
\\n交互优化技巧:
\\nScaleTransition
实现点击缩放。AbsorbPointer
防止重复点击。semanticLabel
的上下文语义。图标渲染的极致调优
void main() {\\n // 预加载字体(减少首次渲染延迟)\\n FontLoader(\'MaterialIcons\')\\n ..addFont(rootBundle.load(\'fonts/MaterialIcons-Regular.ttf\'))\\n ..load();\\n runApp(MyApp());\\n}\\n
\\n字体分包加载方案:
\\n# pubspec.yaml配置示例\\nflutter:\\n fonts:\\n - family: MaterialIcons\\n fonts:\\n - asset: fonts/MaterialIcons-Core.ttf\\n characters: \\"\\\\uE000-\\\\uF8FF\\" # 仅加载基础图标\\n - family: MaterialIcons-Ext\\n fonts:\\n - asset: fonts/MaterialIcons-Extension.ttf\\n
\\nsize
:优先使用布局约束控制尺寸。shadows
属性:投影效果会触发离屏渲染。const
构造函数优化:对静态图标使用const Icon()
。Icon组件的底层逻辑
Icon (Widget)\\n │\\n └─> IconData (不可变数据类)\\n │\\n └─> FontMetadata (字体加载管理)\\n │\\n └─> ParagraphBuilder (文本布局引擎)\\n
\\n// Icon Widget的build方法核心逻辑\\n@override\\nWidget build(BuildContext context) {\\n final IconThemeData iconTheme = IconTheme.of(context);\\n \\n return RichText(\\n textDirection: textDirection,\\n text: TextSpan(\\n style: DefaultTextStyle.of(context).style.copyWith(\\n fontSize: size ?? iconTheme.size,\\n color: color ?? iconTheme.color,\\n fontFamily: icon?.fontFamily,\\n package: icon?.fontPackage,\\n ),\\n children: [TextSpan(text: String.fromCharCode(icon!.codePoint))],\\n ),\\n );\\n}\\n
\\n渲染流程解析:
\\nIconTheme
获取上下文样式。RichText
实现矢量绘制。Unicode
字符转换为图形显示。Flutter图标体系的思考
Material/Cupertino
双体系兼容。自定义图标
接入方案。矢量渲染
方案。Icon(\\n Theme.of(context).platform == TargetPlatform.iOS\\n ? CupertinoIcons.gear\\n : Icons.settings,\\n)\\n
\\n平台差异处理方案:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n平台特性 | Android 方案 | iOS 方案 |
---|---|---|
返回按钮 | Icons.arrow_back | CupertinoIcons.back |
菜单图标 | Icons.more_vert | CupertinoIcons.ellipsis |
弹窗样式 | Icons.info_outline | CupertinoIcons.info |
大型项目的图标管理
// 图标集中管理类\\nclass AppIcons {\\n static const IconData home = Icons.home;\\n static const IconData profile = CupertinoIcons.person;\\n static const IconData payment = FontAwesomeIcons.creditCard;\\n \\n // 统一尺寸配置\\n static const double defaultSize = 24.0;\\n}\\n\\n// 使用示例\\nIcon(\\n AppIcons.home,\\n size: AppIcons.defaultSize,\\n)\\n
\\n// 单元测试示例\\ntestWidgets(\'确保图标正确显示\', (tester) async {\\n await tester.pumpWidget(\\n MaterialApp(home: Icon(AppIcons.home)),\\n );\\n \\n final icon = tester.widget<Icon>(find.byType(Icon));\\n expect(icon.icon, equals(AppIcons.home));\\n});\\n
\\nFlutter
的图标系统通过精妙的架构设计,在跨平台适配
、动态样式控制
、性能优化
等方面展现出强大能力。重点掌握:图标资源的全生命周期管理
、渲染性能的量化评估方法
、平台特性的智能适配策略
。
未来可关注:图标动态加载技术
、与Lottie动画的深度融合
、基于AI的智能图标推荐系统
。系统化掌握这些知识,将助力开发者构建出既美观又高效的移动应用界面。
\\n","description":"前言 在移动应用的视觉体系中,图标(Icon)是用户交互的核心元素之一。Flutter通过高度灵活的Icon组件,为开发者提供了跨平台、矢量化的图标解决方案。\\n\\n本文将以系统化视角,从基础认知到源码实现,全方位解析Flutter图标技术。涵盖六大核心要点:基础属性解析、进阶交互实现、性能调优策略、底层源码逻辑、设计原则思考、工程最佳实践,以及图标体系的未来演进。\\n\\n通过深度剖析,结合代码案例,帮助开发者构建完整的图标知识体系,解决实际开发中的疑难问题。\\n\\n操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。\\n\\n一、基础认知:Icon组件的本质与核心属性…","guid":"https://juejin.cn/post/7473309339295760420","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-20T08:01:01.263Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2235532f9dd9423ab2b71b1e4524beab~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1740643260&x-signature=AP2NfgkGqLCWQScC1v3tyJUCOQg%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"系统化掌握Flutter开发之Text组件:文字的力量","url":"https://juejin.cn/post/7473085303982309415","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
为什么Text
组件值得深入探索?。
在Flutter
应用中,Text
组件是最基础、最高频使用的UI
元素之一。它看似简单,却承载着用户交互
、信息展示
、多语言适配
等核心功能。许多开发者对Text
的认知停留在设置文字颜色、字体大小的表面层级
,却忽略了其背后复杂的布局逻辑
、性能优化点
以及高度可定制化
的能力。例如:
优雅截断
?阿拉伯语
从右到左的排版如何处理?避免不必要的重绘
?渐变文字
、自定义字体
或复杂阴影效果
?操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\nText
的基本用法Text
组件是Flutter
中最基础的文本展示控件,其核心属性为data
(文本内容)和style
(样式)
Text
的基本用法data
:显示文本内容
的基础数据源。
// 简单文本显示\\nText(\'Hello Flutter\')\\n\\n// 直接拼接动态内容时,未处理`null`值可能导致崩溃,推荐使用空字符串兜底\\nText(description ?? \'\')\\n
\\n效果图:
\\n注意事项:
\\ntextSpan
互斥)。直接字符串
或通过变量传入
。特殊字符需转义处理
(如\\\\n
换行)。style
: 精美视觉生产商通过TextStyle
类,可以精细控制文本的视觉效果,常用三剑客(颜色
、字体大小
、字重
(粗细)):
Text(\\n \\"Hello Flutter!\\",\\n style: TextStyle(\\n color: Colors.blue,\\n fontSize: 22,\\n fontWeight: FontWeight.bold,\\n ),\\n),\\n
\\n效果图:
\\nText
的属性说明属性 | 类型 | 说明 | 默认值 |
---|---|---|---|
data | String | 要显示的文本内容,是必传参数。 | 无(必须传入) |
key | Key | widget 树中唯一标识 | 无 |
style | TextStyle? | 用于指定文本样式,如颜色、大小、粗细等。 | 从父级继承的 TextStyle |
strutStyle | StrutStyle? | 控制文本的行距、字体大小等样式 | null |
textAlign | TextAlign? | 指定文本在水平方向上的对齐方式 | null |
textDirection | TextDirection? | 指定文本的阅读方向 | null ,系统自动推断方向 |
locale | Locale? | 指定文本的语言环境 | null ,使用系统默认 |
softWrap | bool? | 是否自动换行 | true |
overflow | TextOverflow? | 当文本超出可用空间且 softWrap 为 false 时,指定处理方式,如截断、显示省略号、渐变透明等。 | TextOverflow.clip |
textScaler | TextScaler? | 设置字体缩放 | noScaling 不缩放 |
maxLines | int? | 限制文本显示的最大行数。当文本行数超过该限制时,会根据 overflow 属性的设置进行处理。 | null ,即不限制行数 |
semanticsLabel | String? | 无障碍服务处理 | null |
textWidthBasis | TextWidthBasis? | 指定计算文本宽度的基础 | null |
textHeightBehavior | TextHeightBehavior? | 控制文本行的高度行为 | null |
selectionColor | Color? | 当文本可选择时,指定选中文本的高亮颜色。 | null |
关键说明:
\\ndata
与textSpan
不可同时使用,优先选textSpan
实现富文本。Widget
中创建复杂TextStyle
。textDirection
和locale
对多语言应用至关重要。semanticsLabel
。textScaler
。strutStyle
可能破坏原有行高设计,谨慎使用。TextStyle
的属性说明属性 | 属性类型 | 作用 | 默认值 |
---|---|---|---|
inherit | bool | 决定 TextStyle 是否继承父级的样式 | true |
color | Color? | 设置文本的颜色。 | null |
backgroundColor | Color? | 设置文本的背景颜色。 | null |
fontSize | double? | 设置文本的字体大小,单位为逻辑像素。 | null |
fontWeight | FontWeight? | 设置文本的字体粗细 | null |
fontStyle | FontStyle? | 设置文本的字体样式 | null |
letterSpacing | double? | 设置字符之间的间距 | null |
wordSpacing | double? | 设置单词之间的间距,对英文等有单词分隔的文本有效。 | null |
textBaseline | TextBaseline? | 指定文本的基线,用于垂直对齐 | null |
height | double? | 设置行高,是字体大小的倍数 | null |
leadingDistribution | TextLeadingDistribution? | 定义行间距在文本行上下的分布方式 | null |
locale | Locale? | 指定文本的语言环境 | null |
foreground | Paint? | 设置文本的前景装饰 | null |
background | Paint? | 设置文本的背景装饰 | null |
shadows | List<Shadow>? | 为文本添加阴影效果 | null |
fontFeatures | List<FontFeature>? | 用于启用或禁用字体的特定特性 | null |
fontVariations | List<FontVariation>? | 允许对字体的可变特性进行细粒度控制 | null , |
decoration | TextDecoration? | 为文本添加装饰线 | null |
decorationColor | Color? | 指定文本装饰线的颜色。 | null |
decorationStyle | TextDecorationStyle? | 定义文本装饰线的样式 | null |
decorationThickness | double? | 设置文本装饰线的粗细。 | null |
overflow | TextOverflow? | 当文本超出可用空间时,指定处理方式 | null |
fontFamily | String? | 指定文本使用的字体家族名称。 | null |
fontFamilyFallback | List<String>? | 字体列表,当首选不可用时,按顺序使用其他字体 | null |
package | String? | 当使用来自其他包的字体时,指定字体所在的包名。 | null |
关键说明:
\\ncolor/backgroundColor
与foreground/background
不可同时使用,优先选color/backgroundColor
实现。Widget
中创建复杂TextStyle
。locale
对多语言应用至关重要。height
可能破坏原有行高设计,谨慎使用。//1、基础用法\\nText(\\"Hello Flutter!\\"),\\n//2、样式三剑客 颜色 大小 字重(粗细)\\nText(\\n \\"Hello Flutter!\\",\\n style: TextStyle(\\n color: Colors.blue,\\n fontSize: 22,\\n fontWeight: FontWeight.bold,\\n ),\\n),\\n//3、字体背景色\\nText(\\n \\"Hello Flutter!\\",\\n style: TextStyle(\\n color: Colors.blue,\\n fontSize: 22,\\n fontWeight: FontWeight.bold,\\n backgroundColor: Colors.red,\\n ),\\n),\\n//4、斜体\\nText(\\n \\"Hello Flutter!\\",\\n style: TextStyle(\\n color: Colors.blue,\\n fontSize: 22,\\n fontWeight: FontWeight.bold,\\n fontStyle: FontStyle.italic,\\n ),\\n),\\n//5、字符间距\\nText(\\n \\"Hello Flutter!\\",\\n style: TextStyle(\\n color: Colors.blue,\\n fontSize: 22,\\n fontWeight: FontWeight.bold,\\n letterSpacing: 5,\\n ),\\n),\\n//6、单词间距\\nText(\\n \\"Hello Flutter!\\",\\n style: TextStyle(\\n color: Colors.blue,\\n fontSize: 22,\\n fontWeight: FontWeight.bold,\\n wordSpacing: 10,\\n ),\\n),\\n//7、行高\\nText(\\n \\"Hello Flutter!\\",\\n style: TextStyle(\\n color: Colors.blue,\\n fontSize: 22,\\n fontWeight: FontWeight.bold,\\n height: 2,\\n ),\\n),\\n//8、前景装饰\\nText(\\n \\"Hello Flutter!\\",\\n style: TextStyle(\\n // color: Colors.blue,\\n // backgroundColor: Colors.red,\\n fontSize: 22,\\n fontWeight: FontWeight.bold,\\n foreground: Paint()..color = Colors.yellow,\\n background: Paint()..color = Colors.orange\\n ),\\n),\\n//9、添加阴影\\nText(\\n \\"Hello Flutter!\\",\\n style: TextStyle(\\n color: Colors.blue,\\n fontSize: 22,\\n fontWeight: FontWeight.bold,\\n shadows: [\\n Shadow(\\n color: Colors.red, // 阴影颜色\\n offset: Offset(1.0, 1.0), // 阴影偏移量\\n blurRadius: 10.0, // 阴影的模糊半径\\n ),\\n Shadow(\\n color: Colors.red, // 阴影颜色\\n offset: Offset(1.0, 1.0), // 阴影偏移量\\n blurRadius: 10.0, // 阴影的模糊半径\\n ),\\n ],\\n ),\\n),\\n//10、添加线\\nText(\\n \\"Hello Flutter!\\",\\n style: TextStyle(\\n color: Colors.blue,\\n fontSize: 22,\\n fontWeight: FontWeight.bold,\\n decoration: TextDecoration.lineThrough,\\n decorationColor: Colors.red,\\n decorationStyle: TextDecorationStyle.solid,\\n decorationThickness: 2\\n ),\\n),\\n//11、溢出处理\\nContainer(\\n width: 200,\\n child: Text(\\n \\"Hello Flutter!Hello Flutter!Hello Flutter!Hello Flutter!Hello Flutter!Hello Flutter!\\",\\n style: TextStyle(\\n color: Colors.blue,\\n fontSize: 22,\\n fontWeight: FontWeight.bold,\\n ),\\n maxLines: 2,\\n overflow: TextOverflow.ellipsis,\\n ),\\n),\\n//11、strutStyle样式\\nText(\\n \\"Hello Flutter!\\",\\n style: TextStyle(\\n color: Colors.blue,\\n fontSize: 22,\\n fontWeight: FontWeight.bold,\\n ),\\n strutStyle: StrutStyle(\\n fontSize: 22,\\n height: 2\\n ),\\n),\\n//11、textAlign 文本对齐方式\\nContainer(\\n color: Colors.red,\\n width: 200,\\n height: 100,\\n child: Text(\\n \\"Hello Flutter!\\",\\n style: TextStyle(\\n color: Colors.blue,\\n fontSize: 22,\\n fontWeight: FontWeight.bold,\\n ),\\n textAlign: TextAlign.justify,\\n textScaler: TextScaler.linear(1.2)\\n ),\\n)\\n
\\n效果图:
\\ntextAlign
:对齐方式作用:控制文本在水平方向的对齐
\\n类型:TextAlign
\\n可选值:
left
/right
:物理方向对齐。start
/end
:逻辑方向(受textDirection
影响)。center
:居中对齐。justify
:两端对齐(英文效果好)。场景对比:
\\n// 商品价格右对齐\\nText(\'¥199.00\', textAlign: TextAlign.end)\\n\\n// 多语言居中\\nText(\'Hello 你好\', textAlign: TextAlign.center)\\n
\\n注意事项:
\\njustify
对中文支持有限(需手动添加空格)。RTL
语言(如阿拉伯语
)使用start
自动适配。maxLines
与overflow
:溢出处理1、组合使用场景:
\\nText(\\n \'这是一段非常长的商品描述文本这是一段非常长的商品描述文本这是一段非常长的商品描述文本\',\\n style: TextStyle(\\n color: Colors.blue,\\n fontSize: 22,\\n fontWeight: FontWeight.bold,\\n ),\\n maxLines: 1, // 最多显示2行\\n overflow: TextOverflow.ellipsis, // 超出部分显示...\\n)\\n
\\n效果图:
\\n2、溢出处理模式:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n模式 | 效果 | 适用场景 |
---|---|---|
clip | 硬截断 | 固定布局 |
fade | 渐变透明 | 带背景色的列表项 |
ellipsis | 末尾显示省略号 | 标题/摘要 |
visible | 允许溢出父容器 | 特殊设计需求 |
3、动态截断进阶:
\\nString longText = \\"这是一段非常长的商品描述文本这是一段非常长的商品描述文本这是一段非常长的商品描述文本这是一段非常长的商品描述文本这是一段非常长的商品描述文本\\";\\nLayoutBuilder(\\n builder: (context, constraints) {\\n final painter = TextPainter(\\n text: TextSpan(text: longText),\\n maxLines: 2,\\n textDirection: TextDirection.ltr\\n );\\n painter.layout(maxWidth: constraints.maxWidth);\\n return Text(\\n painter.didExceedMaxLines ? \'${longText.substring(0, 50)}...\' : longText\\n );\\n }\\n),\\n
\\n效果图:
\\n优化策略:
\\nconst Text( // 使用const构造\\n \'静态文本\',\\n style: TextStyle(color: Colors.black),\\n)\\n
\\n避免的陷阱:
\\nTextStyle
。const
导致重复构建。TextSpan
嵌套结构。风险场景:
\\n10,000
字符)。解决方案:
\\nListView.builder(\\n itemBuilder: (context, index) {\\n return FutureBuilder(\\n future: _loadChunk(index),\\n builder: (_, snapshot) => Text(snapshot.data ?? \'\')\\n );\\n }\\n)\\n
\\nRichText
:富文本支持富文本(包含不同样式
、链接
、图片
等混合内容),可以使用 RichText
组件来创建富文本。
RichText
组件通过 TextSpan
对象来构建富文本内容,每个 TextSpan
可以有自己的样式。以下是一个简单的示例:
RichText(\\n text: TextSpan(\\n text: \'Hello \',\\n style: TextStyle(\\n fontSize: 20,\\n color: Colors.black,\\n ),\\n children: <TextSpan>[\\n TextSpan(\\n text: \'Flutter!\',\\n style: TextStyle(\\n fontWeight: FontWeight.bold,\\n color: Colors.blue,\\n ),\\n ),\\n ],\\n ),\\n),\\n
\\n效果图:
\\nTextSpan
嵌套TextSpan
可以嵌套,以创建更复杂的富文本效果。例如:
RichText(\\n text: TextSpan(\\n text: \'This is a \',\\n style: TextStyle(\\n fontSize: 20,\\n color: Colors.black,\\n ),\\n children: <TextSpan>[\\n TextSpan(\\n text: \'nested \',\\n style: TextStyle(\\n fontWeight: FontWeight.bold,\\n color: Colors.red,\\n ),\\n children: <TextSpan>[\\n TextSpan(\\n text: \'text span\',\\n style: TextStyle(\\n fontStyle: FontStyle.italic,\\n color: Colors.green,\\n ),\\n ),\\n ],\\n ),\\n ],\\n ),\\n),\\n
\\n效果图:
\\n可以为 TextSpan
添加点击事件,通过 GestureRecognizer
实现。
RichText(\\n text: TextSpan(\\n text: \'Click \',\\n style: TextStyle(\\n fontSize: 20,\\n color: Colors.black,\\n ),\\n children: <TextSpan>[\\n TextSpan(\\n text: \'here\',\\n style: TextStyle(\\n color: Colors.blue,\\n decoration: TextDecoration.underline,\\n ),\\n recognizer: TapGestureRecognizer()\\n ..onTap = () {\\n // 处理点击事件\\n print(\'Clicked on \\"here\\"\');\\n },\\n ),\\n ],\\n ),\\n),\\n
\\n效果图:
\\n要在富文本中显示图片,可以使用 WidgetSpan
。
RichText(\\n text: TextSpan(\\n text: \'This is an image: \',\\n style: TextStyle(\\n fontSize: 20,\\n color: Colors.black,\\n ),\\n children: <InlineSpan>[\\n WidgetSpan(\\n child: Image.asset(\\n \'assets/images/ic_launcher.png\',\\n width: 20,\\n height: 20,\\n ),\\n ),\\n ],\\n ),\\n),\\n
\\n效果图:
\\n文字设计的系统方法论
掌握Text
组件的本质是理解信息呈现的工程哲学:
我们应建立三层认知体系:基础层(属性配置
)、逻辑层(布局计算
)、体验层(性能与无障碍
)。通过\\"标准配置-场景适配-性能调优\\"
的渐进实践,将Text
从信息载体升级为体验引擎。
记住,优秀的文字设计如同空气般自然存在 —— 用户不会注意它的存在,但拙劣的排版会立刻破坏产品质感。在Flutter
的渲染体系中,Text
组件正是这种\\"无形的艺术\\"
的最佳实践场域。
\\n","description":"前言 为什么Text组件值得深入探索?。\\n\\n在Flutter应用中,Text组件是最基础、最高频使用的UI元素之一。它看似简单,却承载着用户交互、信息展示、多语言适配等核心功能。许多开发者对Text的认知停留在设置文字颜色、字体大小的表面层级,却忽略了其背后复杂的布局逻辑、性能优化点以及高度可定制化的能力。例如:\\n\\n1、文本溢出:长文本在不同屏幕尺寸下如何优雅截断?\\n2、多语言适配:阿拉伯语从右到左的排版如何处理?\\n3、性能陷阱:频繁更新的动态文本如何避免不必要的重绘?\\n4、深度定制:如何实现渐变文字、自定义字体或复杂阴影效果?\\n\\n操千曲而后晓声,观千剑…","guid":"https://juejin.cn/post/7473085303982309415","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-20T02:19:18.781Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4c82507b257743dca4280b2f918279b3~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1740626079&x-signature=KG6vW9%2Fpu7nRUTpESWtGNkmSWKY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9b88c9504306443a8a07339ce647cefa~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1740626079&x-signature=jEl9TN713CUwEB%2FnZthtiBmF2Og%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ced0d78fc0e4499f9fa0a7352df425fa~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1740626079&x-signature=ZGDNnDPKd2LX2RFQbnZEUdC%2FRU4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/20699d556763479ea152f294c8c4d56a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1740626079&x-signature=59d%2FdWCc9gqrBGW8nMb75NM34%2FA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c48746fca28145a9be14b61a98d90110~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1740626079&x-signature=GGsIi743kJSehUdvjxGiX9VVgbo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2ba38a5df3a346a5b33e450ab9384cfb~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1740626079&x-signature=nvvT8d%2FAYY7EPBUc6AKPQtHsbzU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4dabcba990464e129c6d0400a878417a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1740626079&x-signature=7QK0stbgUQ3T%2FB52MZnR8hD2pV0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/dc884002f375430793ece7f06bb3e394~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1740626079&x-signature=mP0rmmVbgZUMxkU%2B6cY0lX3jL%2BE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/848a8f4926ae4f48a159e539a956019e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1740626079&x-signature=yn3y0eRvXpriY4SGnuOppVRVnUs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5daedd3ae0724ebf9da29b9f4d48b044~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1740626079&x-signature=xRTg7Oppc%2FkmH4lDRkVBFOw%2Fu70%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"flutter扫描二维码或者条形码","url":"https://juejin.cn/post/7473050566987825171","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
1、mobile_scanner,该插件同时支持二维码和条形码,但是使用捆绑版本会使Android打包变大 3-10M,非捆版版本会增加 600kb左右,具体可前往官网查看。
\\nimport \'package:flutter/material.dart\';\\nimport \'package:mobile_scanner/mobile_scanner.dart\';\\nimport \'package:wallpaper/components/scan_code_animation%20.dart\';\\n\\nclass ScanCode extends StatefulWidget {\\n final void Function(String result) onScanCompleted;\\n const ScanCode({super.key, required this.onScanCompleted});\\n\\n @override\\n State<ScanCode> createState() => _ScanCode();\\n}\\n\\nclass _ScanCode extends State<ScanCode> {\\n Barcode? _barcode;\\n final MobileScannerController controller = MobileScannerController();\\n\\n void _handleBarcode(BarcodeCapture barcodes) async {\\n if (mounted) {\\n final barcode = barcodes.barcodes.firstOrNull;\\n if (barcode != null) {\\n // 更新状态\\n setState(() {\\n _barcode = barcode;\\n });\\n\\n // 调用回调函数\\n widget.onScanCompleted(barcode.displayValue ?? \'\');\\n\\n // 停止扫描并返回\\n await controller.stop();\\n // ignore: use_build_context_synchronously\\n Navigator.pop(context);\\n }\\n }\\n }\\n\\n @override\\n void dispose() {\\n controller.dispose();\\n super.dispose();\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: const Text(\'扫描二维码或者条形码\')),\\n backgroundColor: Colors.black,\\n body: Stack(\\n children: [\\n MobileScanner(\\n controller: controller,\\n onDetect: _handleBarcode,\\n ),\\n // 条形码扫描动画\\n QrCodeScanAnimation(),\\n ],\\n ),\\n );\\n }\\n}\\n\\n
\\n InkWell(\\n onTap: () {\\n Navigator.push(\\n context,\\n MaterialPageRoute(\\n builder: (context) => ScanCode(onScanCompleted: (value) {\\n print(value);\\n })));\\n },\\n child: Icon(\\n Icons.qr_code_scanner,\\n color: Theme.of(context).colorScheme.onSurface,\\n size: 28,\\n ),\\n )\\n
\\n到这里,简单的扫码功能就实现了,能够直接扫描二维码和条形码,具体支持哪些格式可自行查看官网。
\\nimport \'package:flutter/material.dart\';\\n\\nclass QrCodeScanAnimation extends StatefulWidget {\\n const QrCodeScanAnimation({super.key});\\n @override\\n // ignore: library_private_types_in_public_api\\n _QrCodeScanAnimation createState() => _QrCodeScanAnimation();\\n}\\n\\nclass _QrCodeScanAnimation extends State<QrCodeScanAnimation>\\n with SingleTickerProviderStateMixin {\\n late AnimationController _controller;\\n\\n @override\\n void initState() {\\n super.initState();\\n _controller = AnimationController(\\n duration: const Duration(seconds: 2),\\n vsync: this,\\n )..repeat();\\n }\\n\\n @override\\n void dispose() {\\n _controller.dispose();\\n super.dispose();\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Center(\\n child: CustomPaint(\\n painter: ScannerPainter(_controller),\\n size: Size(250, 250),\\n ),\\n );\\n }\\n}\\n\\nclass ScannerPainter extends CustomPainter {\\n final Animation<double> animation;\\n\\n ScannerPainter(this.animation) : super(repaint: animation);\\n\\n @override\\n void paint(Canvas canvas, Size size) {\\n final width = size.width;\\n final height = size.height;\\n final paintColor = Colors.blue;\\n final borderPaint = Paint()\\n ..color = paintColor\\n ..style = PaintingStyle.stroke\\n ..strokeWidth = 3;\\n\\n // 绘制四角边框\\n final cornerLength = 25.0;\\n\\n // 左上角\\n canvas.drawLine(Offset.zero, Offset(cornerLength, 0), borderPaint);\\n canvas.drawLine(Offset.zero, Offset(0, cornerLength), borderPaint);\\n\\n // 右上角\\n canvas.drawLine(\\n Offset(width, 0), Offset(width - cornerLength, 0), borderPaint);\\n canvas.drawLine(Offset(width, 0), Offset(width, cornerLength), borderPaint);\\n\\n // 左下角\\n canvas.drawLine(\\n Offset(0, height), Offset(cornerLength, height), borderPaint);\\n canvas.drawLine(\\n Offset(0, height), Offset(0, height - cornerLength), borderPaint);\\n\\n // 右下角\\n canvas.drawLine(Offset(width, height), Offset(width - cornerLength, height),\\n borderPaint);\\n canvas.drawLine(Offset(width, height), Offset(width, height - cornerLength),\\n borderPaint);\\n\\n // 绘制扫描线\\n final linePaint = Paint()\\n ..shader = LinearGradient(\\n colors: [Colors.transparent, paintColor, Colors.transparent],\\n stops: [0.1, 0.5, 0.9],\\n ).createShader(Rect.fromLTWH(0, 0, width, 3))\\n ..strokeWidth = 3;\\n\\n final lineY = height * animation.value;\\n canvas.drawLine(\\n Offset(0, lineY),\\n Offset(width, lineY),\\n linePaint,\\n );\\n }\\n\\n @override\\n bool shouldRepaint(covariant CustomPainter oldDelegate) => true;\\n}\\n\\n
\\n大家好。我用 typescript 语言编写了一个命令行工具,有如下功能
\\njson
生成 class 代码。json
数据格式。json
,也可以使用 json5
。ref
语法,引用已经定义的结构,生成递归
类型dart
、arkTs
语言,后续支持其他语言也不是问题json_serializable
实在觉得有点麻烦,用起来需要定义一堆东西,还需要按约定的规则对应好属性。如果跟 http 请求结合起来使用,需要手写的模版代码就更多了。下面我介绍下这个工具如何使用。这是我第一次写开源工具,难免有所考虑不到的地方,欢迎大家给我提出意见或建议。
\\n因为这是个命令行工具,所以需要从命令行开始。我以 npx
这个工具开始。其他执行方式基本类似,可以在上面的 工具地址 的文档中找到使用方法。
npx json2class build -l dart@3\\n
\\n-l
是 --language
的缩写,用于指定需要构建的语言。目前已经支持 dart@3
和 arkTs@12
。很显然,@
后面的数字代表版本号
执行该条命令后,默认会在执行命令所在的目录去查找所有的 .json(5)
文件,也支持文件夹查找,但最多支持三层。
如果一切没有问题,默认就会在执行命令所在的文件夹生成一个构建好的代码文件。如果是 dart
生成的就是 json2class.dart
。如果是 arkTs
,生成的就是 json2class.ets
。
可以通过 -s
或 --search
指定一个查找 .json(5)
的目录,支持绝对路径和相对路径
可以通过 -o
或 --output
指定一个输出目录。
推荐将生成的文件 json2class.*
加入 git 忽略。
只要你的 json 是合法的,就是一个有效的配置,就可以完成代码的构建
\\n// 文件名: test.json5\\n{\\n a: 1,\\n b: \\"bbb\\",\\n c: true,\\n o: {\\n o1: 100,\\n o2: \'ooo\',\\n o3: false,\\n },\\n}\\n
\\n生成的代码 dart
代码大致如下
class test {\\n num a = 0;\\n String b = \'\';\\n bool c = false;\\n testo o = testo();\\n}\\n\\nclass testo {\\n num o1 = 0;\\n String o2 = \'\';\\n bool o3 = false;\\n}\\n
\\n没错,我使用了文件名
+ 属性名
直接拼接的方式当做 class 类名。这在使用上完全没有问题,既保证了命名规范从一而终的一致性、也保留了充分的可识别性。
从生成的代码中,你可以看到属性类型
和属性默认值
与JSON的大致关系。亦或是如果不想设置属性默认值
,这些在上面的 工具地址 中都有详细描述,这里不再赘述。
之前有提到,只要你的 json 是合法的,构建就不会有问题。但合法的 json 并不能一定保证能生成我需要的类型。
\\nclass test {\\n num a = 0;\\n String b = \'\';\\n bool c = false;\\n testo o = testo();\\n}\\nclass testo {\\n num o1 = 0;\\n String o2 = \'\';\\n bool o3 = false;\\n testo? child; // 类型递归\\n}\\n\\n
\\n像这样的一个递归类型,只使用 json 就无法描述,我们需要约定特殊语法。
\\n// test.json5\\n{\\n a: 123,\\n b: \'abc\',\\n c: true,\\n o: {\\n o1: 100,\\n o2: \'ooo\',\\n o3: false,\\n child: { $meta: { ref: \'/test#/o\' } }\\n },\\n}\\n
\\nref 引用
用 #
分为两个部分:
/test
是文件名,这里当然也可以是其他文件名,只要存在于你的 json 查找目录中的文件。如果是多层目录,需要带上文件夹层级。
/o
就是你需要引用的字段对应的类型,在这个例子中就是 testo
。如果你需要引用的类型就是当前文件的,可以省略前半部分,{ ref: \'#/o\' }
,此写法与当前例子等效,但需要注意的是 #
一定是必须的。
目前 $meta
下 ref
是唯一支持的配置。如果大家在使用过程中,有更好的想法可以告诉我,可以通过 $meta
加入更多特性的支持。
按类名
通过 文件名
+ 字段名
拼接的方式,在一些极端情况下,会产生类型冲突的情况,此时命令行工具会给出提示,你只需要修改文件名即可消除冲突。
修改文件名会产生新的类名,这里再次强调,类名具体是什么其实真的不太重要,重要的是
\\n如果字段中,有特殊的字符,这些字符是属于该语言的关键字,命令行工具在识别到以后,会通过特定算法做转换,不影响属性的使用,也不会影响序列化后的字段名。
\\nimport \'json2class.dart\';\\n\\nmain() {\\n final t = testo();\\n t.fromJson({\\n \\"test\\": {\\n \\"o1\\": 100,\\n \\"o2\\": \'ooo\',\\n \\"o3\\": true,\\n }\\n });\\n print(t.toJson());\\n}\\n
\\n生成的代码最核心功能就是序列化
与反序列化
。
fromJson
:反序列化toJson
:序列化为了方便字符串格式的JSON数据转换,还提供了一个 fromAny
,他可以接收任意参数,尝试转换成 Map 后在调用 fromJson
实现反序列化。
完整的生成代码的使用文档可以参见上面的 工具地址,这里不再赘述。
\\n可以预见,该工具生成的代码,最佳的使用场景是在获取网络数据。在后端没有准备好联调时,我们就需要构造假数据进行开发了。我们在拿到后端接口返回定义时就可以同时配置好 mock 数据
// test.json\\n{\\n \\"result\\": {\\n \\"statusCode\\": \\"\\",\\n \\"statusMessage\\": \\"\\",\\n \\"data\\": {\\n \\"userName\\": \\"json2class\\",\\n \\"userPhone\\": \\"13888888888\\",\\n \\"userAvator\\": \\"http://xxx.yyy.com/avator.png\\"\\n }\\n }\\n}\\n
\\n\\nmain() {\\n final result = testresult();\\n result.fromPreset();\\n setState(() {});\\n}\\n\\n
\\n通过 fromPreset
方法,就可以将 json 中配置的数据填充到对象中,去完成业务逻辑的编码。当后面后端具备联调条件时,只需要将 fromPreset
,替换成真实的接口调用即可。当然你也可以把这里封装一下,通过泛型,实现所有接口统一入口,通过一个参数实现 mock 数据
的一键切换。
为什么会设计填充规则
这个玩意?因为前端在与后端交互时,为了保证前端代码的健壮性,一定不能信任任何后端数据,这也是为什么我们拿到后端的 json 数据后,要做反序列化、类型化的原因。
那么在真实的业务场景下,如果接口返回数据的结构、类型与我们定义的不一致,怎么办?直接报错?这肯定不行。当发生结构或类型与定义不一致时,填充 null
应该可行,填充该类型的一个 默认值
似乎也不错。为了满足不同的需求,所以设计了 填充规则
。
很好理解。无非就三种
情况。
DiffType
枚举值决定如何填充。MissKey
枚举值决定如何填充。这个应该也容易理解,输入的数组长度与带填充的数组长度一样,那么按元素位置逐个填充,如果发生对应位置类型不匹配的情况,按 DiffType
处理。
针对长度超出的那部分元素,按如下规则处理。
\\nMoreIndex
:输入数组长度 > 原数组枚举值 | 效果 |
---|---|
Fill | 按输入值插入,类型不一致时,根据字段是否可选,设置默认值 / null |
Drop | 丢弃多余的输入数据,数组长度与原数组长度一致 |
Null | 多出的数据填充为 null 值(非可选字段强制 Null 等同 Fill ) |
MissIndex
:输入数组长度 < 原数组枚举值 | 效果 |
---|---|
Fill | 填充默认值,多维数组会递归填充 |
Drop | 丢弃多余的原始数据,数组长度与输入数组长度一致 |
Null | 多出的数据填充为 null 值(非可选字段强制 Null 等同 Fill ) |
Skip | 原数组中多余的数据不做处理,保留原值 |
一般情况下,我们在接收后端数组数据的时候,一般会 new 一个新对象,那么接收数组长度为 0
,即 输入数组长度 > 原数组
,受枚举值 MoreIndex
影响,而默认配置的 MoreIndex
是 MoreIndex.Fill
,所以最后表现的行为是将数据逐个填充到原数组中,符合预期。
这就是全部内容,感谢您看到这里。该项目也是在多年的实际项目中逐渐迭代、优化、提炼而形成的。非常希望大家能在各自的项目中用起来,如果在使用过程中遇到问题,或者有任何改进的建议,欢迎通过如下方式反馈:
\\n\\n\\n\\n
在本文中,我们深入探讨了在Flutter开发中使用SizedBox进行间距设置的不足之处,并提供了多种更有效的替代方案。这些替代方法包括使用Padding、Spacer、Flexible和Wrap等小部件,以实现更灵活和响应式的布局设计。
\\nFlutter, SizedBox, 布局优化, 间距设置, 响应式设计
\\n虽然 SizedBox 是添加间距的简单方法,但它在响应式或复杂布局中存在局限性。以下是一些考虑因素:
\\nSizedBox
可以增加清晰度,但在更复杂的布局中,其目的可能会变得模糊。如果没有解释就放置它,可能不清楚它是用于填充、边距还是对齐。然而,用 const
标记 SizedBox
可以使它更高效并稍微提高可读性。SizedBox
灵活性较低,不适应响应式布局。对于灵活的布局,您可能更倾向于选择 Spacer
、 Padding
或 Flexible
,以实现更响应的行为。SizedBox
都会向小部件树添加一个额外的节点,这可能会引入效率低下问题,尤其是在深层嵌套布局中过度使用时。\\n\\n更新:社区反馈表明,虽然
\\nSizedBox
确实是一个额外的节点,但使用Container
或Padding
可能会在小部件树中添加一个额外的层,这有类似的影响。尽可能将SizedBox
标记为const
可以缓解一些性能问题。然而,在需要响应式间距的情况下,仍然更优选Flexible
或Spacer
。
使用 Padding
和 Container
来控制小部件周围的间距。这些提供比固定大小的盒子更明确的意图和更大的灵活性。
示例:使用填充
\\nPadding(\\n padding: const EdgeInsets.symmetric(vertical: 10.0),\\n child: Text(\'Hello World\'),\\n)\\n
\\n示例:带容器的边距
\\nContainer(\\n margin: const EdgeInsets.only(top: 20),\\n child: Text(\'Hello World\'),\\n)\\n
\\nSpacer
小部件提供了灵活的动态间距,可以根据可用空间进行调整。它可以在 Row
和 Column
小部件中使用。
示例:使用 Spacer 实现动态间距
\\nRow(\\n children: [\\n Text(\'Left\'),\\n Spacer(),\\n Text(\'Right\'),\\n ],\\n)\\n
\\n在这个示例中, Spacer
根据屏幕尺寸动态调整 Text
小部件之间的间距,使布局具有响应性。
Flexible
允许小部件占用可用空间的比例,使其非常适合响应式布局。
示例:灵活布局
\\nRow(\\n children: [\\n Flexible(flex: 2, child: Container(color: Colors.blue, height: 50)),\\n Flexible(flex: 1, child: Container(color: Colors.red, height: 50)),\\n ],\\n)\\n
\\nFlexible
小部件确保蓝色的 Container
占用红色的两倍空间,按比例调整布局。
Wrap
小部件会根据可用空间自动处理小部件对齐和间距,无需手动创建 SizedBox
实例。
示例:使用带有间距的 Wrap
\\nWrap(\\n spacing: 10,\\n runSpacing: 20,\\n children: [\\n Chip(label: Text(\'Chip 1\')),\\n Chip(label: Text(\'Chip 2\')),\\n Chip(label: Text(\'Chip 3\')),\\n ],\\n)\\n
\\nWrap
是一个理想的选择,当你希望小部件能够自然流动并根据可用屏幕空间调整位置时。
虽然 IntrinsicHeight
和 IntrinsicWidth
可以用于精确对齐小部件,但应谨慎使用。Flutter 文档指出,这些小部件可能会影响性能,特别是在复杂或频繁重建的布局中,因为它们需要额外的遍历来测量每个子元素。避免在频繁重建或深层布局中使用 IntrinsicHeight
和 IntrinsicWidth
。
示例:用于对齐的内在高度
\\nIntrinsicHeight(\\n child: Row(\\n children: [\\n Text(\'First Line\'),\\n VerticalDivider(width: 10),\\n Text(\'Second Line\'),\\n ],\\n ),\\n)\\n
\\nIntrinsicHeight
确保小部件之间的垂直对齐一致,减少了手动调整间距的需求。
虽然 SizedBox
在简单布局中很高效,但在大型应用程序中过度使用可能会导致:
每个 SizedBox
都会在小部件树中添加一个节点。在复杂的 UI 中,这会使树结构更深,可能导致布局遍历时间变长。
在动态布局(其中小部件经常变化)中频繁使用 SizedBox
会导致 Flutter 引擎在重建期间需要评估的小部件数量增加。
SizedBox
引入了固定间距,这在响应式布局中可能不够灵活。像 Spacer
或 Flexible
这样的替代方案能更好地适应不同的屏幕尺寸,提供更高效的布局。
通过避免过度使用SizedBox,开发者可以提升应用的性能和可维护性,创建更高效的用户界面。掌握这些技巧,将为您的Flutter项目带来显著的改进。
\\n感谢阅读本文
\\n如果有什么建议,请在评论中让我知道。我很乐意改进。
\\n© 猫哥\\nducafecat.com
\\nend
","description":"Flutter 中 SizedBox 的替代布局方式 视频\\n\\nyoutu.be/xd7FAS68nLI\\n\\nwww.bilibili.com/video/BV1vs…\\n\\n前言\\n\\n原文 停止在Flutter中使用SizedBox进行间距设置的最佳实践\\n\\n在本文中,我们深入探讨了在Flutter开发中使用SizedBox进行间距设置的不足之处,并提供了多种更有效的替代方案。这些替代方法包括使用Padding、Spacer、Flexible和Wrap等小部件,以实现更灵活和响应式的布局设计。\\n\\nFlutter, SizedBox, 布局优化, 间距设置, 响应式设计…","guid":"https://juejin.cn/post/7472780631279468570","author":"独立开发者_猫哥","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-19T03:48:25.718Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/63b78e5cd4a7491bb2e66e4405ae2797~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg54us56uL5byA5Y-R6ICFX-eMq-WTpQ==:q75.awebp?rk3s=f64ab15b&x-expires=1740541704&x-signature=c3E%2B0tvCgB3iNcjL2lnu01vkbxo%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"通过外部链接启动 Flutter App(详细介绍及示例)","url":"https://juejin.cn/post/7472666325137276968","content":"详细介绍 通过外部链接启动flutter App 的使用及示例
\\n在我们的APP中,经常有点击链接启动并进入APP的需求(如果未安装跳转到应用商店)。Android通过deep link或者app link(是deep link 的增强版),iOS通过 url schema,可以打开对应的app,因此我们需要对我们的app进行对应的配置。下面将会详细介绍两种方式。\\n推荐使用app_link
\\n该三方服务提供了 生成分享链接、通过链接启动跳转到指定页面的方法,通知支持 未安装App的时候跳转其他的链接(例如跳转到应用商店),重定向逻辑由 Firebase 服务自动处理,并且支持短链接。
\\n实现步骤:
\\n在 pubspec.yaml 文件中添加:
\\ndependencies:\\n flutter:\\n sdk: flutter\\n firebase_dynamic_links: ^6.0.5\\n
\\n在 android/app/src/main/AndroidManifest.xml 文件中,添加 intent-filter 配置,以便在 Android 中处理 App Links。
\\n<activity android:name=\\".MainActivity\\">\\n <intent-filter>\\n <action android:name=\\"android.intent.action.VIEW\\" />\\n\\n <category android:name=\\"android.intent.category.DEFAULT\\" />\\n <category android:name=\\"android.intent.category.BROWSABLE\\" />\\n\\n <data\\n android:host=\\"i89trillion.page.link\\"\\n android:scheme=\\"https\\" />\\n </intent-filter>\\n</activity>\\n\\n
\\n// 初始化动态链接\\nawait FirebaseDynamicLinkService.initDynamicLink();\\n
\\n\\nimport \\"dart:async\\";\\nimport \\"dart:io\\";\\n\\nimport \\"package:easy_localization/easy_localization.dart\\";\\nimport \\"package:firebase_dynamic_links/firebase_dynamic_links.dart\\";\\n\\nclass FirebaseDynamicLinkService {\\n static Duration maxDuration = Duration(seconds: 5);\\n\\n static Future<void> initDynamicLink() async {\\n // 未启动的时候监听\\n final PendingDynamicLinkData? initialLink =\\n await FirebaseDynamicLinks.instance.getInitialLink();\\n if (initialLink != null) {\\n final Uri deepLink = initialLink.link;\\n FirebaseAnalyticsService.logEvent(\\n FirebaseAnalyticsEvent.link_join_guild_finish, \'0\');\\n _handleDeepLink(deepLink);\\n }\\n\\n // 应用程序在后台启动时有效\\n FirebaseDynamicLinks.instance.onLink.listen((dynamicLinkData) {\\n final Uri deepLink = dynamicLinkData.link;\\n FirebaseAnalyticsService.logEvent(\\n FirebaseAnalyticsEvent.link_join_guild_finish, \'1\');\\n _handleDeepLink(deepLink);\\n }, onError: (e) {\\n LogUtil.error(e.toString());\\n });\\n }\\n\\n static void _handleDeepLink(Uri deepLink) async {\\n var isJoinGuildLink = deepLink.pathSegments.contains(\'joinServer\');\\n if (isJoinGuildLink) {\\n String? id = deepLink.queryParameters[\'id\'];\\n if (id != null) {\\n LogUtil.info(\\"deepLink serverId: $id\\");\\n }\\n }\\n }\\n\\n // 创建一个群邀请链接\\n static Future<String> createJoinServerDynamicLink(\\n bool short, int guildID) async {\\n if (!Platform.isAndroid && !Platform.isIOS) {\\n return \\"该功能只在 Android 和 iOS 上可用\\";\\n }\\n\\n String _linkMessage;\\n\\n final DynamicLinkParameters parameters = DynamicLinkParameters(\\n uriPrefix: \\"https://xxx.page.link\\",\\n link: Uri.parse(\'https://xxx.page.link/joinServer?id=$guildID\'),\\n androidParameters: AndroidParameters(\\n // 未安装应用程序打开的链接\\n fallbackUrl: Uri.parse(\\n \'https://play.google.com/store/apps/details?id=xxx\'),\\n packageName: \'xxx\',\\n ),\\n );\\n\\n Uri url;\\n if (short) {\\n final ShortDynamicLink shortLink = await FirebaseDynamicLinksPlatform\\n .instance\\n .buildShortLink(parameters);\\n url = shortLink.shortUrl;\\n } else {\\n url = await FirebaseDynamicLinksPlatform.instance.buildLink(parameters);\\n }\\n _linkMessage = url.toString();\\n return _linkMessage;\\n }\\n}\\n\\n
\\n在生成链接的时候参数 fallbackUrl 为未安装App时打开的链接,firebase 服务自己做了重定向。
\\n该三方库 也支持 通过链接启动App,同时解析链接参数,跳转指定页面,但是不支持App未安装的时候跳转其他链接(需要自己的服务处理重定向)\\n官方文档见:developer.android.com/studio/writ…
\\n实现步骤:
\\n在 pubspec.yaml 文件中添加:
\\ndependencies:\\n flutter:\\n sdk: flutter\\n app_links: ^6.0.1 # 用于处理动态链接\\n
\\n在 android/app/src/main/AndroidManifest.xml 文件中,添加 intent-filter 配置,以便在 Android 中处理 App Links。
\\n<activity android:name=\\".MainActivity\\">\\n <!-- Add the intent-filter for handling app links --\x3e\\n <intent-filter android:autoVerify=\\"true\\">\\n <action android:name=\\"android.intent.action.VIEW\\" />\\n <category android:name=\\"android.intent.category.DEFAULT\\" />\\n <category android:name=\\"android.intent.category.BROWSABLE\\" />\\n <data android:scheme=\\"https\\" android:host=\\"yourapp.page.link\\" android:pathPrefix=\\"/joinServer\\"/>\\n </intent-filter>\\n</activity>\\n\\n
\\n注意: 这里注意下,开启autoVerify的activity中的<intent-filter..>的action必须为android.intent.action.VIEW,category必须包含android.intent.category.BROWSABLE,data的scheme必须包含http/https,否则不生效,而且AppLinks必须在Android 6.0 以上的手机才可生效。\\nandroid:host 为域名,android:pathPrefix为地址前缀,根据实际需求配置
\\n// 初始化动态链接\\n await AppLinkService.initializeDynamicLinks();\\n
\\nimport \'package:app_links/app_links.dart\';\\nimport \\"package:beehive/utils/logger.dart\\";\\nimport \\"dart:async\\";\\n\\nclass AppLinkService {\\n static final AppLinks appLinks = AppLinks();\\n\\n // 初始化动态链接\\n static Future<void> initializeDynamicLinks() async {\\n print(\'Initializing dynamic links...\');\\n // 监听动态链接流\\n appLinks.uriLinkStream.listen((Uri? uri) {\\n print(\\"appLink listen: $uri\\");\\n if (uri != null) {\\n _handleDeepLink(uri);\\n }\\n });\\n }\\n\\n // 处理动态链接,跳转到相应页面\\n static void _handleDeepLink(Uri deepLink) async {\\n var isJoinGuildLink = deepLink.pathSegments.contains(\'joinServer\');\\n if (!isJoinGuildLink){\\n return;\\n }\\n String? id = deepLink.queryParameters[\'id\'];\\n LogUtil.info(\\"appLink serverId: $id\\");\\n }\\n\\n // 生成分享链接\\n static Future<String> generateShareLink(int serverId) async {\\n final String deepLinkUrl = \'https://yourapp.page.link/joinServer?id=$serverId\';\\n\\n final Uri dynamicLink = Uri.parse(deepLinkUrl);\\n return dynamicLink.toString(); // 返回生成的动态链接\\n }\\n}\\n\\n
\\n需要在域名<https://yourapp.page.link> 根目录下面 放一个JSON文件:.well-known/assetlinks.json 。该JSON文件就是用于识别进入App的,如果是谷歌商店的App,可以直接在管理中心生成,如果是测试,就自己生成。\\n内容格式如下:\\n
\\n[\\n {\\n \\"relation\\": [\\"delegate_permission/common.handle_all_urls\\"],\\n \\"target\\": {\\n \\"namespace\\": \\"android_app\\",\\n \\"package_name\\": \\"xxxx\\",\\n \\"sha256_cert_fingerprints\\":\\n [\\"7F:48:F9:...\\"]\\n }\\n }\\n]\\n
\\napp link没有实现未安装跳转其他URL,需要自己在自己的服务域名下面进行重定向处理。(例如重定向到应用商店)\\n快速的方法:将 一个重定向的HTML也放到域名根目录下面。客户端 识别链接就是这个html,可以在链接中加参数。分享链接就是:yourapp.page.link/applink.htm…\\n例如:applink.html
\\n<!DOCTYPE HTML PUBLIC \\"-//W3C//DTD HTML 4.01 Transitional//EN\\"\\n\\"http://www.w3.org/TR/html4/loose.dtd\\">\\n<html>\\n<head>\\n<title>App Link</title>\\n</head>\\n<script>\\nwindow.onload = function() {\\nif (/(android)/i.test(navigator.userAgent)) {\\n// 用户使用的是 Android 设备\\nwindow.location.href = \\"https://play.google.com/store/apps/details?id=xxx\\";\\n} else if (/(iphone|ipad|ipod|ios)/i.test(navigator.userAgent)) {\\n// 用户使用的是 iOS 设备\\nwindow.location.href = \\"https://itunes.apple.com/app/idxxx\\";\\n} else {\\n// 用户使用的是其他设备\\nconsole.log(\\"This device is not supported.\\");\\n}\\n};\\n</script>\\n<body>\\n\\n</body>\\n</html>\\n
\\n也可以在该域名对应的服务中对某个接口实现重定向逻辑:\\n例如:(go语言 gframe框架为例)
\\nfunc (ctrl *linkCtrl) LinkRedirect(r *ghttp.Request) {\\nuserAgent := r.Header.Get(\\"User-Agent\\")\\nlog.LogRuntime(log.LEVEL_INFO, \\"[LinkRedirect] userAgent:\\", userAgent)\\nif strings.Contains(strings.ToLower(userAgent), \\"android\\") {\\n// 用户使用的是 Android 设备\\nr.Response.RedirectTo(define.GooglePlayAdd)\\n} else {\\n// 用户使用的是其他设备\\nr.Response.Write(\\"This device is not supported.\\")\\n}\\n}\\n
\\n或者也可以直接在NGINX上面重定向跳转.
","description":"通过外部链接启动 Flutter App(firebase_dynamic_links 和 app_links) 详细介绍 通过外部链接启动flutter App 的使用及示例\\n\\n在我们的APP中,经常有点击链接启动并进入APP的需求(如果未安装跳转到应用商店)。Android通过deep link或者app link(是deep link 的增强版),iOS通过 url schema,可以打开对应的app,因此我们需要对我们的app进行对应的配置。下面将会详细介绍两种方式。 推荐使用app_link\\n\\n一、firebase_dynamic_links…","guid":"https://juejin.cn/post/7472666325137276968","author":"飞川001","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-18T16:02:27.991Z","media":null,"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"深入理解Dart中集合类型:Map","url":"https://juejin.cn/post/7472597127489126436","content":"Map 是键值对(key-value pairs)的集合,具有以下特性:
\\n//空 map\\nMap map = {};\\n//明确类型声明\\nMap<int,String> map = {};\\n
\\n//空 map\\nMap map = Map();\\n//明确类型声明\\nMap<int,String> map = Map<int,String>();\\n
\\nList<int> list1 = [1,2,3,4,5];\\nList<String> list2 = [\\"one\\",\\"two\\",\\"three\\",\\"fore\\",\\"five\\"];\\nMap<int,String> map1 = Map.fromIterables(list1,list2);\\nprint(map1);\\n//输出:{1: one, 2: two, 3: three, 4: fore, 5: five}\\n\\n\\nList<MapEntry<String,String>> list = [\\n MapEntry(\'我\', \'张三\'), \\n MapEntry(\'你\', \'李四\'),\\n MapEntry(\'他\', \'王五\')\\n];\\nMap map = Map.fromEntries(list);\\nprint(map);\\n//输出:{我: 张三, 你: 李四, 他: 王五}\\n
\\nMap<String,String> map = {};\\nmap[\'我\'] = \'张三\'; // 添加\\nmap[\'我\'] = \'小明\'; // 更新\\n
\\nprint(map[\'我\']); // 输出: 小明\\nprint(map[\'你\']); // 输出: null(键不存在)\\n
\\nmap.remove(\'我\'); // 删除指定键\\nmap.clear(); // 清空所有条目\\n
\\nbool haskey = map.containsKey(\'我\');\\nbool hasvalue = map.containsValue(\'小明\');\\n
\\nmap.forEach((key, value) {\\n print(\'$key :$value\');\\n});\\n
\\nfor (var entry in map.entries) {\\n print(\'${entry.key} : ${entry.value}\');\\n}\\n
\\n键唯一性:重复赋值会覆盖旧值
\\n空安全:使用空安全操作符(Dart 2.12+)
\\n性能考虑:HashMap 的查询时间复杂度为 O(1)
\\n类型安全:推荐使用显式类型声明避免运行时错误
\\nDart 中的 Map 是一种键值对集合,键唯一且无序,值可重复。通过字面量(如 {\'key\': \'value\'}
)或构造函数(如 Map()
)创建,支持添加、更新、删除和查询操作。适合用于缓存、配置管理等场景。Map 支持类型化声明(如 Map<String, int>
)和不可变操作(如 Map.unmodifiable
),同时兼容空安全特性,是 Dart 中处理键值数据的核心工具。
本文结合实际代码讲述如何改造现有 Flutter 项目,适配鸿蒙平台。
\\n通过模块化,鸿蒙壳工程,结合 FVM 管理多版本 Flutter SDK,保持原 Flutter 代码纯净,最小化修改,完成鸿蒙化的适配示例。
\\n安装 FVM,更多安装方式参考 fvm 官方文档
\\ncurl -fsSL https://fvm.app/install.sh | bash\\n
\\n分别安装官方的Flutter版本(我这里用的比较旧,版本为 3.3.10),以及鸿蒙社区的 3.22.0 版本
\\n提示:安装鸿蒙定制版本的 Flutter SDK,建议使用 git clone
命令行(我一开始直接 download zip,出现下载不全的现象)
完成准备工作后,调整现有 Flutter 的工程结构,简化后大致如下:
\\n.\\n├── apps #该目录用于存放各端应用壳工程 (示例如下)\\n│ ├── app #对应Android、IOS、Windows等原Flutter框架支持的平台\\n│ └── app_ohos #对应HarmonyOS平台\\n│\\n├── modules #该目录用于存放各业务模块 (示例如下)\\n│ ├── home_module #首页\\n│ ├── trade_module #交易结算\\n│ └── memeber_module #会员管理\\n│\\n├── common # 该目录用于存放公共库 (示例如下)\\n│ ├── network \\n│ ├── tools \\n│ └── widgets \\n│\\n└── README.md\\n\\n
\\n如上所示, /apps/app
为我们现有的 Flutter 壳工程目录,modules
及common
目录是我们项目开发中封装的业务模块库及公共组件库。(当然,不一定要放在同一目录下,这里也为方便大家观察学习)
统一布局
及三方库依赖
放在下层模块中例如,在home_module
模块中添加外部三方库,封装后再提供给壳工程使用。
在 /modules/home_module/pubspec.yaml
中添加几个典型的三方库作为示例
name: home_module\\ndescription: home_module\\nversion: 0.0.1\\nhomepage:\\n\\nenvironment:\\n sdk: \'>=2.18.6 <3.0.0\'\\n flutter: \\">=1.17.0\\"\\n\\ndependencies:\\n flutter:\\n sdk: flutter\\n flutter_bloc: ^7.3.0 // 演示纯 Dart 实现的三方状态管理库\\n dio: ^4.0.6 // 演示纯 Dart 实现的三方网络库\\n fluttertoast: 8.1.2 // 演示依赖系统底层实现的三方库(需要鸿蒙化适配)\\n
\\n封装组件 HomePage
提供给壳工程,用于显示主页
在 /apps/app/pubspec.yaml
中对各个业务模块及公共组件库进行依赖项添加
dependencies:\\n flutter:\\n sdk: flutter\\n cupertino_icons: ^1.0.6\\n home_module:\\n path: \'../../module/home_module\'\\n ... 省略其他 \\n
\\n通过依赖 home_module
显示封装的首页组件
点击运行,效果如下:
\\n首先,我们需要在 /apps
目录下创建一个鸿蒙壳工程
fvm use custom_3.22.0\\n
\\n/apps
目录,使用命令行创建 app_ohos
项目fvm flutter create --template app --platforms ohos --org com.rex.flutter app_ohos\\n
\\ndependencies:\\n flutter:\\n sdk: flutter\\n cupertino_icons: ^1.0.6\\n home_module:\\n path: \'../../module/home_module\'\\n ... 省略其他 \\n
\\n通过 dependency_overrides 来替换鸿蒙化的三方库,以组件内用到的 fluttertoast
为例
dependency_overrides:\\n fluttertoast:\\n git:\\n url: \\"https://gitee.com/openharmony-sig/flutter_fluttertoast.git\\"\\n ref: \\"master\\"\\n
\\n\\n\\napp_ohos/pubsec.yaml 中的 dependency_overrides, 仅添加需要鸿蒙化的三方库\\n如何判断三方库是否需要鸿蒙化,简而言之,如果三方库由纯 Dart 实现,则不需要单独适配,直接使用;如果三方库依赖系统底层实现,则需要鸿蒙化适配。\\n三方库的适配情况,可以查询 Gitee/Github,或者查阅表格 Flutter三方库适配计划
\\n
用 Deveco 打开apps/app_ohos/ohos 目录。
\\n在 Deveco 左上角 打开 File -> Project Structure..., 点击 Siging Configs, 勾选 Automatically generate signature, 对鸿蒙项目签名。
\\n在 ohos_app 目录下,使用 fvm flutter run,或者点击运行按钮,运行flutter项目。
\\nPS1:注意添加应用权限
\\nPS2:没有真机的同学可以使用模拟器运行
\\n运行效果如下:
\\ndependency_overrides
配置,逐个替换鸿蒙化的三方库DEMO 示例已上传: github.com/liyufengrex…
","description":"引言 本文结合实际代码讲述如何改造现有 Flutter 项目,适配鸿蒙平台。\\n\\n通过模块化,鸿蒙壳工程,结合 FVM 管理多版本 Flutter SDK,保持原 Flutter 代码纯净,最小化修改,完成鸿蒙化的适配示例。\\n\\n准备工作\\n1. 安装 FVM\\n\\n安装 FVM,更多安装方式参考 fvm 官方文档\\n\\ncurl -fsSL https://fvm.app/install.sh | bash\\n\\n2.使用 FVM 安装 Flutter SDK\\n\\n分别安装官方的Flutter版本(我这里用的比较旧,版本为 3.3.10),以及鸿蒙社区的 3.22.0 版本\\n\\n提示…","guid":"https://juejin.cn/post/7472593190920912934","author":"李小轰_Rex","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-18T08:41:12.980Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f1ac0225c96a4a01ab16bf70c81370de~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5p2O5bCP6L2wX1JleA==:q75.awebp?rk3s=f64ab15b&x-expires=1740472871&x-signature=eU0Oa4qEqaC6Spg49a8Q0RJjBXk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5c5c753bbbaa46eb8d35094e8c749753~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5p2O5bCP6L2wX1JleA==:q75.awebp?rk3s=f64ab15b&x-expires=1740472871&x-signature=IEJWjqcOTG8CoB%2BStqaxdHEr0as%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/70aa92a10acb4b1c9429c73ddab6916e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5p2O5bCP6L2wX1JleA==:q75.awebp?rk3s=f64ab15b&x-expires=1740472871&x-signature=DJte%2F9GkxZ0amVKl8o04LSEGNDw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fde0fc83382a456c8476c59776cfff09~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5p2O5bCP6L2wX1JleA==:q75.awebp?rk3s=f64ab15b&x-expires=1740472871&x-signature=M0SNboVN5p%2BWSDtR8gCWbMRBot8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/adfb0d39c3224bbba6812391df34a509~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5p2O5bCP6L2wX1JleA==:q75.awebp?rk3s=f64ab15b&x-expires=1740472871&x-signature=L%2B0GsqmjGCeC8hcjUV%2BjZH34gI4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2d621051603f416e94754205b75e0ac6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5p2O5bCP6L2wX1JleA==:q75.awebp?rk3s=f64ab15b&x-expires=1740472871&x-signature=5uBbM5JUreP6pZxVk0OHwZuHunI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e787dae95de24cc9898cc4e89d9d9029~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5p2O5bCP6L2wX1JleA==:q75.awebp?rk3s=f64ab15b&x-expires=1740472871&x-signature=QSJnwAuhQ8IPDQMee0RwUPF3CKo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2b3e16d1d0614f4eadc46ffdd1274564~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5p2O5bCP6L2wX1JleA==:q75.awebp?rk3s=f64ab15b&x-expires=1740472871&x-signature=LTHieKpeF%2B4dtYmjkd69KfUzjoM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ab0c1ea8853e4b58bfaaa9bd98f091b3~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5p2O5bCP6L2wX1JleA==:q75.awebp?rk3s=f64ab15b&x-expires=1740472871&x-signature=WcR25oYj2YYQjbTBMK60ZfFmm6k%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["前端","Flutter","HarmonyOS"],"attachments":null,"extra":null,"language":null},{"title":"深入解析 Flutter GetX","url":"https://juejin.cn/post/7472551695365521458","content":"GetX
是 Flutter 中一个轻量级且功能强大的状态管理、路由管理和依赖注入框架。它以简单、快速、高效著称,适合从小型到大型项目的开发需求。GetX
的设计理念是一体化解决方案,通过一个框架解决状态管理、路由管理和依赖注入的问题。
BuildContext
,支持命名路由和动态路由。Get.put
、Get.lazyPut
等方法实现依赖注入。BuildContext
,可以在任何地方访问状态和路由。Rx
)和简单状态管理(GetBuilder
)。Get.put
、Get.lazyPut
等方法管理依赖。Rx
或 Controller
声明状态。Obx
或 GetBuilder
监听状态变化并更新 UI。Get.to
或 Get.off
实现页面跳转。Get.put
提供依赖,在任何地方访问。import \'package:flutter/material.dart\';\\nimport \'package:get/get.dart\';\\n\\n// 定义控制器\\nclass CounterController extends GetxController {\\n var count = 0.obs; // 响应式变量\\n\\n void increment() {\\n count++;\\n }\\n}\\n\\nvoid main() {\\n runApp(MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return GetMaterialApp(\\n home: CounterHomePage(),\\n );\\n }\\n}\\n\\nclass CounterHomePage extends StatelessWidget {\\n final CounterController controller = Get.put(CounterController()); // 注入控制器\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\\"GetX 示例\\")),\\n body: Center(\\n child: Obx(() => Text(\\"点击次数:${controller.count}\\")), // 监听状态变化\\n ),\\n floatingActionButton: FloatingActionButton(\\n onPressed: controller.increment,\\n child: Icon(Icons.add),\\n ),\\n );\\n }\\n}\\n
\\nRx
(如 0.obs
)声明响应式变量。Obx
监听状态变化并更新 UI。Get.put
提供控制器实例。class CounterController extends GetxController {\\n int count = 0;\\n\\n void increment() {\\n count++;\\n update(); // 通知监听者更新\\n }\\n}\\n\\nclass CounterHomePage extends StatelessWidget {\\n final CounterController controller = Get.put(CounterController());\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\\"GetBuilder 示例\\")),\\n body: Center(\\n child: GetBuilder<CounterController>(\\n builder: (controller) => Text(\\"点击次数:${controller.count}\\"),\\n ),\\n ),\\n floatingActionButton: FloatingActionButton(\\n onPressed: controller.increment,\\n child: Icon(Icons.add),\\n ),\\n );\\n }\\n}\\n
\\nGetBuilder
监听状态变化并更新 UI。update
方法通知监听者更新。class HomePage extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\\"首页\\")),\\n body: Center(\\n child: ElevatedButton(\\n onPressed: () {\\n Get.to(DetailsPage()); // 跳转到详情页\\n },\\n child: Text(\\"跳转到详情页\\"),\\n ),\\n ),\\n );\\n }\\n}\\n\\nclass DetailsPage extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\\"详情页\\")),\\n body: Center(\\n child: ElevatedButton(\\n onPressed: () {\\n Get.back(); // 返回上一页\\n },\\n child: Text(\\"返回首页\\"),\\n ),\\n ),\\n );\\n }\\n}\\n
\\nGet.to
跳转到新页面。Get.back
返回上一页。void main() {\\n runApp(MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return GetMaterialApp(\\n initialRoute: \'/\',\\n getPages: [\\n GetPage(name: \'/\', page: () => HomePage()),\\n GetPage(name: \'/details\', page: () => DetailsPage()),\\n ],\\n );\\n }\\n}\\n\\nclass HomePage extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\\"首页\\")),\\n body: Center(\\n child: ElevatedButton(\\n onPressed: () {\\n Get.toNamed(\'/details\'); // 跳转到详情页\\n },\\n child: Text(\\"跳转到详情页\\"),\\n ),\\n ),\\n );\\n }\\n}\\n\\nclass DetailsPage extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\\"详情页\\")),\\n body: Center(\\n child: ElevatedButton(\\n onPressed: () {\\n Get.back(); // 返回上一页\\n },\\n child: Text(\\"返回首页\\"),\\n ),\\n ),\\n );\\n }\\n}\\n
\\nGetPage
配置命名路由。Get.toNamed
跳转到命名路由。class AuthMiddleware extends GetMiddleware {\\n @override\\n RouteSettings? redirect(String? route) {\\n final isLoggedIn = false; // 模拟登录状态\\n if (!isLoggedIn) {\\n return RouteSettings(name: \'/login\');\\n }\\n return null;\\n }\\n}\\n\\nvoid main() {\\n runApp(MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return GetMaterialApp(\\n initialRoute: \'/\',\\n getPages: [\\n GetPage(name: \'/\', page: () => HomePage()),\\n GetPage(name: \'/details\', page: () => DetailsPage(), middlewares: [AuthMiddleware()]),\\n GetPage(name: \'/login\', page: () => LoginPage()),\\n ],\\n );\\n }\\n}\\n
\\nGetMiddleware
实现路由拦截。redirect
方法中检查登录状态,未登录时跳转到登录页。class ApiService {\\n String fetchData() {\\n return \\"数据加载完成\\";\\n }\\n}\\n\\nclass HomeController extends GetxController {\\n final ApiService apiService = Get.find();\\n\\n String getData() {\\n return apiService.fetchData();\\n }\\n}\\n\\nvoid main() {\\n Get.put(ApiService()); // 注入依赖\\n runApp(MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return GetMaterialApp(\\n home: HomePage(),\\n );\\n }\\n}\\n\\nclass HomePage extends StatelessWidget {\\n final HomeController controller = Get.put(HomeController());\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\\"依赖注入示例\\")),\\n body: Center(\\n child: Text(controller.getData()),\\n ),\\n );\\n }\\n}\\n
\\nGet.put
提供依赖。Get.find
获取依赖实例。class ProductController extends GetxController {\\n var cart = <String>[].obs;\\n\\n void addToCart(String product) {\\n cart.add(product);\\n }\\n}\\n\\nvoid main() {\\n runApp(MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return GetMaterialApp(\\n home: ProductListPage(),\\n );\\n }\\n}\\n\\nclass ProductListPage extends StatelessWidget {\\n final ProductController controller = Get.put(ProductController());\\n\\n @override\\n Widget build(BuildContext context) {\\n final products = [\\"商品 1\\", \\"商品 2\\", \\"商品 3\\"];\\n return Scaffold(\\n appBar: AppBar(\\n title: Text(\\"商品列表\\"),\\n actions: [\\n IconButton(\\n icon: Icon(Icons.shopping_cart),\\n onPressed: () {\\n Get.to(CartPage());\\n },\\n ),\\n ],\\n ),\\n body: ListView.builder(\\n itemCount: products.length,\\n itemBuilder: (context, index) {\\n final product = products[index];\\n return ListTile(\\n title: Text(product),\\n trailing: ElevatedButton(\\n onPressed: () {\\n controller.addToCart(product);\\n },\\n child: Text(\\"添加到购物车\\"),\\n ),\\n );\\n },\\n ),\\n );\\n }\\n}\\n\\nclass CartPage extends StatelessWidget {\\n final ProductController controller = Get.find();\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\\"购物车\\")),\\n body: Obx(() => ListView.builder(\\n itemCount: controller.cart.length,\\n itemBuilder: (context, index) {\\n return ListTile(\\n title: Text(controller.cart[index]),\\n );\\n },\\n )),\\n );\\n }\\n}\\n
\\nGetX
的状态管理和路由管理。Bloc
(Business Logic Component)是 Flutter 中一个强大的状态管理工具,基于事件驱动的架构设计,适合管理复杂的业务逻辑和状态。Bloc
的核心理念是将业务逻辑与 UI 分离,通过事件(Event)和状态(State)来驱动应用的变化。
Bloc
:\\nEvent
:\\nState
:\\nBlocProvider
:\\nBlocBuilder
:\\nimport \'package:flutter/material.dart\';\\nimport \'package:flutter_bloc/flutter_bloc.dart\';\\n\\n// 定义事件\\nabstract class CounterEvent {}\\n\\nclass IncrementEvent extends CounterEvent {}\\n\\n// 定义状态\\nclass CounterState {\\n final int count;\\n\\n CounterState(this.count);\\n}\\n\\n// 定义 Bloc\\nclass CounterBloc extends Bloc<CounterEvent, CounterState> {\\n CounterBloc() : super(CounterState(0)) {\\n on<IncrementEvent>((event, emit) {\\n emit(CounterState(state.count + 1));\\n });\\n }\\n}\\n\\nvoid main() {\\n runApp(\\n BlocProvider(\\n create: (context) => CounterBloc(),\\n child: MyApp(),\\n ),\\n );\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n home: CounterHomePage(),\\n );\\n }\\n}\\n\\nclass CounterHomePage extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n final counterBloc = BlocProvider.of<CounterBloc>(context);\\n return Scaffold(\\n appBar: AppBar(title: Text(\\"Bloc 示例\\")),\\n body: Center(\\n child: BlocBuilder<CounterBloc, CounterState>(\\n builder: (context, state) {\\n return Text(\\"点击次数:${state.count}\\");\\n },\\n ),\\n ),\\n floatingActionButton: FloatingActionButton(\\n onPressed: () {\\n counterBloc.add(IncrementEvent());\\n },\\n child: Icon(Icons.add),\\n ),\\n );\\n }\\n}\\n
\\nIncrementEvent
,表示增加计数的操作。CounterState
,表示计数器的状态。on
方法处理事件并生成新的状态。BlocBuilder
监听状态变化并更新界面。// 定义事件\\nabstract class LoginEvent {}\\n\\nclass LoginSubmitted extends LoginEvent {\\n final String username;\\n final String password;\\n\\n LoginSubmitted(this.username, this.password);\\n}\\n\\n// 定义状态\\nabstract class LoginState {}\\n\\nclass LoginInitial extends LoginState {}\\n\\nclass LoginLoading extends LoginState {}\\n\\nclass LoginSuccess extends LoginState {}\\n\\nclass LoginFailure extends LoginState {\\n final String error;\\n\\n LoginFailure(this.error);\\n}\\n\\n// 定义 Bloc\\nclass LoginBloc extends Bloc<LoginEvent, LoginState> {\\n LoginBloc() : super(LoginInitial()) {\\n on<LoginSubmitted>((event, emit) async {\\n emit(LoginLoading());\\n await Future.delayed(Duration(seconds: 2)); // 模拟网络请求\\n if (event.username == \\"admin\\" && event.password == \\"1234\\") {\\n emit(LoginSuccess());\\n } else {\\n emit(LoginFailure(\\"用户名或密码错误\\"));\\n }\\n });\\n }\\n}\\n\\nvoid main() {\\n runApp(\\n BlocProvider(\\n create: (context) => LoginBloc(),\\n child: MyApp(),\\n ),\\n );\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n home: LoginPage(),\\n );\\n }\\n}\\n\\nclass LoginPage extends StatelessWidget {\\n final TextEditingController usernameController = TextEditingController();\\n final TextEditingController passwordController = TextEditingController();\\n\\n @override\\n Widget build(BuildContext context) {\\n final loginBloc = BlocProvider.of<LoginBloc>(context);\\n return Scaffold(\\n appBar: AppBar(title: Text(\\"登录\\")),\\n body: BlocListener<LoginBloc, LoginState>(\\n listener: (context, state) {\\n if (state is LoginSuccess) {\\n ScaffoldMessenger.of(context).showSnackBar(\\n SnackBar(content: Text(\\"登录成功\\")),\\n );\\n } else if (state is LoginFailure) {\\n ScaffoldMessenger.of(context).showSnackBar(\\n SnackBar(content: Text(state.error)),\\n );\\n }\\n },\\n child: BlocBuilder<LoginBloc, LoginState>(\\n builder: (context, state) {\\n if (state is LoginLoading) {\\n return Center(child: CircularProgressIndicator());\\n }\\n return Padding(\\n padding: const EdgeInsets.all(16.0),\\n child: Column(\\n children: [\\n TextField(\\n controller: usernameController,\\n decoration: InputDecoration(labelText: \\"用户名\\"),\\n ),\\n TextField(\\n controller: passwordController,\\n decoration: InputDecoration(labelText: \\"密码\\"),\\n obscureText: true,\\n ),\\n SizedBox(height: 20),\\n ElevatedButton(\\n onPressed: () {\\n final username = usernameController.text;\\n final password = passwordController.text;\\n loginBloc.add(LoginSubmitted(username, password));\\n },\\n child: Text(\\"登录\\"),\\n ),\\n ],\\n ),\\n );\\n },\\n ),\\n ),\\n );\\n }\\n}\\n
\\nLoginSubmitted
,表示提交登录表单的操作。LoginInitial
、LoginLoading
、LoginSuccess
、LoginFailure
)。LoginSubmitted
事件,模拟网络请求并生成状态。BlocListener
监听状态变化,显示提示信息。BlocBuilder
渲染不同的状态。Cubit
简化代码// 定义 Cubit\\nclass CounterCubit extends Cubit<int> {\\n CounterCubit() : super(0);\\n\\n void increment() => emit(state + 1);\\n}\\n\\nvoid main() {\\n runApp(\\n BlocProvider(\\n create: (context) => CounterCubit(),\\n child: MyApp(),\\n ),\\n );\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n home: CounterHomePage(),\\n );\\n }\\n}\\n\\nclass CounterHomePage extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n final counterCubit = BlocProvider.of<CounterCubit>(context);\\n return Scaffold(\\n appBar: AppBar(title: Text(\\"Cubit 示例\\")),\\n body: Center(\\n child: BlocBuilder<CounterCubit, int>(\\n builder: (context, state) {\\n return Text(\\"点击次数:$state\\");\\n },\\n ),\\n ),\\n floatingActionButton: FloatingActionButton(\\n onPressed: counterCubit.increment,\\n child: Icon(Icons.add),\\n ),\\n );\\n }\\n}\\n
\\nCubit
:\\nBloc
的简化版本,只处理状态,不需要事件。// 商品事件\\nabstract class ProductEvent {}\\n\\nclass AddToCart extends ProductEvent {\\n final String product;\\n\\n AddToCart(this.product);\\n}\\n\\n// 商品状态\\nabstract class ProductState {}\\n\\nclass ProductInitial extends ProductState {}\\n\\nclass ProductAdded extends ProductState {\\n final List<String> cart;\\n\\n ProductAdded(this.cart);\\n}\\n\\n// 商品 Bloc\\nclass ProductBloc extends Bloc<ProductEvent, ProductState> {\\n final List<String> _cart = [];\\n\\n ProductBloc() : super(ProductInitial()) {\\n on<AddToCart>((event, emit) {\\n _cart.add(event.product);\\n emit(ProductAdded(List.from(_cart)));\\n });\\n }\\n}\\n\\nvoid main() {\\n runApp(\\n BlocProvider(\\n create: (context) => ProductBloc(),\\n child: MyApp(),\\n ),\\n );\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n home: ProductListPage(),\\n );\\n }\\n}\\n\\nclass ProductListPage extends StatelessWidget {\\n final List<String> products = [\\"商品 1\\", \\"商品 2\\", \\"商品 3\\"];\\n\\n @override\\n Widget build(BuildContext context) {\\n final productBloc = BlocProvider.of<ProductBloc>(context);\\n return Scaffold(\\n appBar: AppBar(\\n title: Text(\\"商品列表\\"),\\n actions: [\\n IconButton(\\n icon: Icon(Icons.shopping_cart),\\n onPressed: () {\\n Navigator.push(\\n context,\\n MaterialPageRoute(builder: (context) => CartPage()),\\n );\\n },\\n ),\\n ],\\n ),\\n body: ListView.builder(\\n itemCount: products.length,\\n itemBuilder: (context, index) {\\n final product = products[index];\\n return ListTile(\\n title: Text(product),\\n trailing: ElevatedButton(\\n onPressed: () {\\n productBloc.add(AddToCart(product));\\n },\\n child: Text(\\"添加到购物车\\"),\\n ),\\n );\\n },\\n ),\\n );\\n }\\n}\\n\\nclass CartPage extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\\"购物车\\")),\\n body: BlocBuilder<ProductBloc, ProductState>(\\n builder: (context, state) {\\n if (state is ProductAdded) {\\n return ListView.builder(\\n itemCount: state.cart.length,\\n itemBuilder: (context, index) {\\n return ListTile(\\n title: Text(state.cart[index]),\\n );\\n },\\n );\\n }\\n return Center(child: Text(\\"购物车为空\\"));\\n },\\n ),\\n );\\n }\\n}\\n
\\nCubit
简化代码。Bloc
管理复杂的业务逻辑。Bloc
和依赖注入,构建模块化的状态管理体系。Provider
是 Google 官方推荐的 Flutter 状态管理工具,基于 InheritedWidget
构建,简化了状态共享和管理的代码。它是 Flutter 开发中最常用的状态管理方案之一,适合从小型到中型项目的状态管理需求。
本篇博客将详细分析 Provider
的核心原理、常见用法,并结合实际场景进行实战演示,帮助你全面掌握 Provider
的使用。
Provider
是一个封装了 InheritedWidget
的状态管理工具。ChangeNotifier
和 Consumer
实现状态的监听和更新。InheritedWidget
,代码更简洁。ChangeNotifier
、ValueNotifier
、Stream
等)。ChangeNotifier
或其他状态类声明状态。ChangeNotifierProvider
或其他 Provider 提供状态。Consumer
或 Provider.of
获取状态并更新 UI。ChangeNotifier
:\\nChangeNotifierProvider
:\\nChangeNotifier
类型的状态。Consumer
:\\nProvider.of
:\\nimport \'package:flutter/material.dart\';\\nimport \'package:provider/provider.dart\';\\n\\n// 定义状态类\\nclass Counter with ChangeNotifier {\\n int _count = 0;\\n\\n int get count => _count;\\n\\n void increment() {\\n _count++;\\n notifyListeners(); // 通知监听者更新\\n }\\n}\\n\\nvoid main() {\\n runApp(\\n ChangeNotifierProvider(\\n create: (context) => Counter(),\\n child: MyApp(),\\n ),\\n );\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n home: CounterHomePage(),\\n );\\n }\\n}\\n\\nclass CounterHomePage extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n final counter = Provider.of<Counter>(context); // 获取状态\\n return Scaffold(\\n appBar: AppBar(title: Text(\\"Provider 示例\\")),\\n body: Center(\\n child: Column(\\n mainAxisAlignment: MainAxisAlignment.center,\\n children: [\\n Text(\\"点击次数:${counter.count}\\"),\\n ElevatedButton(\\n onPressed: counter.increment,\\n child: Text(\\"增加计数\\"),\\n ),\\n ],\\n ),\\n ),\\n );\\n }\\n}\\n
\\nChangeNotifier
定义状态类 Counter
。ChangeNotifierProvider
提供 Counter
状态。Provider.of
获取状态并更新 UI。Consumer
class CounterHomePage extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\\"Consumer 示例\\")),\\n body: Center(\\n child: Column(\\n mainAxisAlignment: MainAxisAlignment.center,\\n children: [\\n Consumer<Counter>(\\n builder: (context, counter, child) {\\n return Text(\\"点击次数:${counter.count}\\");\\n },\\n ),\\n ElevatedButton(\\n onPressed: () => context.read<Counter>().increment(),\\n child: Text(\\"增加计数\\"),\\n ),\\n ],\\n ),\\n ),\\n );\\n }\\n}\\n
\\nConsumer
:\\nConsumer
包裹的部分,性能更高。context.read
:\\nclass Product with ChangeNotifier {\\n final String name;\\n final double price;\\n\\n Product(this.name, this.price);\\n}\\n\\nclass Cart with ChangeNotifier {\\n final List<Product> _items = [];\\n\\n List<Product> get items => _items;\\n\\n void addItem(Product product) {\\n _items.add(product);\\n notifyListeners();\\n }\\n\\n void removeItem(Product product) {\\n _items.remove(product);\\n notifyListeners();\\n }\\n}\\n\\nvoid main() {\\n runApp(\\n MultiProvider(\\n providers: [\\n ChangeNotifierProvider(create: (context) => Cart()),\\n ChangeNotifierProvider(create: (context) => Product(\\"商品 1\\", 10.0)),\\n ],\\n child: MyApp(),\\n ),\\n );\\n}\\n
\\nMultiProvider
:\\nProvider.of
或 Consumer
获取不同的状态。Selector
优化性能Consumer
会监听整个状态对象的变化,可能导致不必要的刷新。Selector
只监听状态的某个字段。class CounterHomePage extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\\"Selector 示例\\")),\\n body: Center(\\n child: Column(\\n mainAxisAlignment: MainAxisAlignment.center,\\n children: [\\n Selector<Counter, int>(\\n selector: (context, counter) => counter.count,\\n builder: (context, count, child) {\\n return Text(\\"点击次数:$count\\");\\n },\\n ),\\n ElevatedButton(\\n onPressed: () => context.read<Counter>().increment(),\\n child: Text(\\"增加计数\\"),\\n ),\\n ],\\n ),\\n ),\\n );\\n }\\n}\\n
\\nSelector
:\\nProxyProvider
动态依赖状态class User with ChangeNotifier {\\n final String name;\\n\\n User(this.name);\\n}\\n\\nclass UserProfile with ChangeNotifier {\\n final User user;\\n\\n UserProfile(this.user);\\n\\n String get profile => \\"用户:${user.name}\\";\\n}\\n\\nvoid main() {\\n runApp(\\n MultiProvider(\\n providers: [\\n ChangeNotifierProvider(create: (context) => User(\\"张三\\")),\\n ProxyProvider<User, UserProfile>(\\n update: (context, user, previous) => UserProfile(user),\\n ),\\n ],\\n child: MyApp(),\\n ),\\n );\\n}\\n
\\nProxyProvider
:\\nimport \'package:flutter/material.dart\';\\nimport \'package:provider/provider.dart\';\\n\\nclass Product {\\n final String name;\\n final double price;\\n\\n Product(this.name, this.price);\\n}\\n\\nclass Cart with ChangeNotifier {\\n final List<Product> _items = [];\\n\\n List<Product> get items => _items;\\n\\n void addItem(Product product) {\\n _items.add(product);\\n notifyListeners();\\n }\\n\\n void removeItem(Product product) {\\n _items.remove(product);\\n notifyListeners();\\n }\\n}\\n\\nvoid main() {\\n runApp(\\n ChangeNotifierProvider(\\n create: (context) => Cart(),\\n child: MyApp(),\\n ),\\n );\\n}\\n\\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n home: ProductListPage(),\\n );\\n }\\n}\\n\\nclass ProductListPage extends StatelessWidget {\\n final List<Product> products = [\\n Product(\\"商品 1\\", 10.0),\\n Product(\\"商品 2\\", 20.0),\\n Product(\\"商品 3\\", 30.0),\\n ];\\n\\n @override\\n Widget build(BuildContext context) {\\n final cart = Provider.of<Cart>(context);\\n return Scaffold(\\n appBar: AppBar(\\n title: Text(\\"商品列表\\"),\\n actions: [\\n IconButton(\\n icon: Icon(Icons.shopping_cart),\\n onPressed: () {\\n Navigator.push(\\n context,\\n MaterialPageRoute(builder: (context) => CartPage()),\\n );\\n },\\n ),\\n ],\\n ),\\n body: ListView.builder(\\n itemCount: products.length,\\n itemBuilder: (context, index) {\\n final product = products[index];\\n return ListTile(\\n title: Text(product.name),\\n subtitle: Text(\\"价格:¥${product.price}\\"),\\n trailing: ElevatedButton(\\n onPressed: () => cart.addItem(product),\\n child: Text(\\"添加到购物车\\"),\\n ),\\n );\\n },\\n ),\\n );\\n }\\n}\\n\\nclass CartPage extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n final cart = Provider.of<Cart>(context);\\n return Scaffold(\\n appBar: AppBar(title: Text(\\"购物车\\")),\\n body: ListView.builder(\\n itemCount: cart.items.length,\\n itemBuilder: (context, index) {\\n final product = cart.items[index];\\n return ListTile(\\n title: Text(product.name),\\n subtitle: Text(\\"价格:¥${product.price}\\"),\\n trailing: ElevatedButton(\\n onPressed: () => cart.removeItem(product),\\n child: Text(\\"删除\\"),\\n ),\\n );\\n },\\n ),\\n );\\n }\\n}\\n
\\nChangeNotifierProvider
和 Consumer
。MultiProvider
和 Selector
优化性能。ProxyProvider
动态管理状态。PerformanceOverlay
监控性能瓶颈:详细分析与实战在开发 Flutter 应用时,性能问题可能会导致用户体验下降,比如页面卡顿、掉帧、内存泄漏等。为了定位和解决这些问题,Flutter 提供了强大的性能监控工具:Flutter DevTools 和 PerformanceOverlay
。
本篇文章将详细分析如何使用这两种工具监控性能瓶颈,并结合实际场景提供具体的使用方法。
\\nFlutter DevTools 是一个基于 Web 的调试和性能分析工具,提供了以下功能:
\\n确保你的应用正在运行在模拟器或真机上:
\\nflutter run\\n
\\nflutter pub global activate devtools\\nflutter pub global run devtools\\n
\\nhttp://127.0.0.1:9100
。class PerformanceExample extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\\"性能监控示例\\")),\\n body: ListView.builder(\\n itemCount: 1000,\\n itemBuilder: (context, index) {\\n return ListTile(\\n title: Text(\\"Item $index\\"),\\n );\\n },\\n ),\\n );\\n }\\n}\\n
\\nclass MemoryLeakExample extends StatefulWidget {\\n @override\\n _MemoryLeakExampleState createState() => _MemoryLeakExampleState();\\n}\\n\\nclass _MemoryLeakExampleState extends State<MemoryLeakExample> {\\n late Timer _timer;\\n\\n @override\\n void initState() {\\n super.initState();\\n _timer = Timer.periodic(Duration(seconds: 1), (timer) {\\n print(\\"计时器运行中...\\");\\n });\\n }\\n\\n @override\\n void dispose() {\\n // 如果忘记释放计时器,会导致内存泄漏\\n _timer.cancel();\\n super.dispose();\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n body: Center(child: Text(\\"内存泄漏示例\\")),\\n );\\n }\\n}\\n
\\nTimer
或 Stream
。dispose
方法中释放资源。class DeepWidgetTreeExample extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n body: Column(\\n children: [\\n for (int i = 00; i < 10; i++)\\n Padding(\\n padding: EdgeInsets.all(8.0),\\n child: Container(\\n color: Colors.blue,\\n child: Text(\\"Item $i\\"),\\n ),\\n ),\\n ],\\n ),\\n );\\n }\\n}\\n
\\nconst
构造函数优化静态 Widget。PerformanceOverlay
PerformanceOverlay
?PerformanceOverlay
是 Flutter 提供的内置性能监控工具,用于实时显示帧率和渲染性能。
PerformanceOverlay
在 MaterialApp
或 CupertinoApp
中启用性能叠加:
MaterialApp(\\n debugShowCheckedModeBanner: false,\\n showPerformanceOverlay: true,\\n home: MyApp(),\\n);\\n
\\nPerformanceOverlay
,实时监控帧率。class ScrollPerformanceExample extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n debugShowCheckedModeBanner: false,\\n showPerformanceOverlay: true,\\n home: Scaffold(\\n appBar: AppBar(title: Text(\\"滚动性能示例\\")),\\n body: ListView.builder(\\n itemCount: 1000,\\n itemBuilder: (context, index) {\\n return ListTile(\\n title: Text(\\"Item $index\\"),\\n );\\n },\\n ),\\n ),\\n );\\n }\\n}\\n
\\nPerformanceOverlay
检查动画的渲染性能。class AnimationPerformanceExample extends StatefulWidget {\\n @override\\n _AnimationPerformanceExampleState createState() =>\\n _AnimationPerformanceExampleState();\\n}\\n\\nclass _AnimationPerformanceExampleState\\n extends State<AnimationPerformanceExample>\\n with SingleTickerProviderStateMixin {\\n late AnimationController _controller;\\n\\n @override\\n void initState() {\\n super.initState();\\n _controller = AnimationController(\\n duration: Duration(seconds: 2),\\n vsync: this,\\n )..repeat();\\n }\\n\\n @override\\n void dispose() {\\n _controller.dispose();\\n super.dispose();\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n debugShowCheckedModeBanner: false,\\n showPerformanceOverlay: true,\\n home: Scaffold(\\n body: Center(\\n child: AnimatedBuilder(\\n animation: _controller,\\n builder: (context, child) {\\n return Transform.rotate(\\n angle: _controller.value * 2 * 3.14159,\\n child: Container(\\n width: 100,\\n height: 100,\\n color: Colors.blue,\\n ),\\n );\\n },\\n ),\\n ),\\n ),\\n );\\n }\\n}\\n
\\nPerformanceOverlay
的使用场景PerformanceOverlay
:实时监控帧率,快速定位性能问题。通过本篇博客,你应该能够熟练使用 Flutter DevTools 和 PerformanceOverlay
监控性能瓶颈,并在实际项目中灵活应用这些工具,构建高性能的 Flutter 应用!
Flutter 是一个高性能的跨平台框架,但在开发复杂应用时,性能问题仍然可能出现。性能优化是开发高质量 Flutter 应用的关键。本篇博客将从 Flutter 的渲染原理出发,结合实际场景,详细分析如何优化 Flutter 应用的性能,涵盖布局优化、绘制优化、内存优化、网络优化等多个方面。
\\n在优化性能之前,我们需要理解 Flutter 的渲染原理和性能瓶颈。
\\nFlutter 的渲染过程分为以下几个阶段:
\\nStream
、Timer
)导致内存占用增加。setState
,都会触发整个 Widget 树的重建。StatefulBuilder
或 ValueListenableBuilder
只更新局部状态。class CounterApp extends StatefulWidget {\\n @override\\n _CounterAppState createState() => _CounterAppState();\\n}\\n\\nclass _CounterAppState extends State<CounterApp> {\\n int _counter = 0;\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\\"性能优化示例\\")),\\n body: Column(\\n children: [\\n // 静态部分\\n Text(\\"静态内容\\"),\\n // 动态部分\\n StatefulBuilder(\\n builder: (context, setState) {\\n return Column(\\n children: [\\n Text(\\"计数器:$_counter\\"),\\n ElevatedButton(\\n onPressed: () {\\n setState(() {\\n _counter++;\\n });\\n },\\n child: Text(\\"增加计数\\"),\\n ),\\n ],\\n );\\n },\\n ),\\n ],\\n ),\\n );\\n }\\n}\\n
\\nRepaintBoundary
隔离重绘RepaintBoundary
将需要重绘的部分隔离,避免影响整个 Widget 树。class RepaintBoundaryExample extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n body: Column(\\n children: [\\n // 不需要频繁重绘的部分\\n Text(\\"静态内容\\"),\\n // 需要频繁重绘的部分\\n RepaintBoundary(\\n child: ListView.builder(\\n itemCount: 1000,\\n itemBuilder: (context, index) {\\n return ListTile(\\n title: Text(\\"动态内容 $index\\"),\\n );\\n },\\n ),\\n ),\\n ],\\n ),\\n );\\n }\\n}\\n
\\nconst
构造函数优化静态 Widget。Flutter Inspector
检查 Widget 树的深度。// 优化前\\nColumn(\\n children: [\\n Padding(\\n padding: EdgeInsets.all(8.0),\\n child: Container(\\n color: Colors.blue,\\n child: Text(\\"内容\\"),\\n ),\\n ),\\n ],\\n);\\n\\n// 优化后\\nPadding(\\n padding: EdgeInsets.all(8.0),\\n child: Container(\\n color: Colors.blue,\\n child: Text(\\"内容\\"),\\n ),\\n);\\n
\\nCustomPainter
优化复杂绘制CustomPainter
直接绘制图形,减少 Widget 的数量。class CirclePainter extends CustomPainter {\\n @override\\n void paint(Canvas canvas, Size size) {\\n final paint = Paint()\\n ..color = Colors.blue\\n ..style = PaintingStyle.fill;\\n\\n canvas.drawCircle(Offset(size.width / 2, size.height / 2), 50, paint);\\n }\\n\\n @override\\n bool shouldRepaint(CustomPainter oldDelegate) => false;\\n}\\n\\nclass CustomPainterExample extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n body: CustomPaint(\\n size: Size(200, 200),\\n painter: CirclePainter(),\\n ),\\n );\\n }\\n}\\n
\\nImage
缓存优化图片加载CachedNetworkImage
插件缓存图片。dependencies:\\n cached_network_image: ^3.0.0\\n
\\nimport \'package:cached_network_image/cached_network_image.dart\';\\n\\nclass ImageCacheExample extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n body: CachedNetworkImage(\\n imageUrl: \\"https://example.com/image.jpg\\",\\n placeholder: (context, url) => CircularProgressIndicator(),\\n errorWidget: (context, url, error) => Icon(Icons.error),\\n ),\\n );\\n }\\n}\\n
\\nStream
、Timer
)会导致内存泄漏。dispose
方法中释放资源。class TimerExample extends StatefulWidget {\\n @override\\n _TimerExampleState createState() => _TimerExampleState();\\n}\\n\\nclass _TimerExampleState extends State<TimerExample> {\\n late Timer _timer;\\n\\n @override\\n void initState() {\\n super.initState();\\n _timer = Timer.periodic(Duration(seconds: 1), (timer) {\\n print(\\"计时器运行中...\\");\\n });\\n }\\n\\n @override\\n void dispose() {\\n _timer.cancel(); // 释放计时器\\n super.dispose();\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n body: Center(child: Text(\\"计时器示例\\")),\\n );\\n }\\n}\\n
\\nIsolate
处理耗时任务Isolate
将耗时任务移到后台线程。import \'dart:async\';\\nimport \'dart:isolate\';\\n\\nFuture<void> runHeavyTask() async {\\n final receivePort = ReceivePort();\\n await Isolate.spawn(_heavyTask, receivePort.sendPort);\\n\\n receivePort.listen((message) {\\n print(\\"任务完成:$message\\");\\n receivePort.close();\\n });\\n}\\n\\nvoid _heavyTask(SendPort sendPort) {\\n // 模拟耗时任务\\n int result = 0;\\n for (int i = 0; i < 1000000000; i++) {\\n result += i;\\n }\\n sendPort.send(result);\\n}\\n
\\ndio
优化网络请求dio
插件实现高效的网络请求。dependencies:\\n dio: ^5.0.0\\n
\\nimport \'package:dio/dio.dart\';\\n\\nclass NetworkExample {\\n final Dio _dio = Dio();\\n\\n Future<void> fetchData() async {\\n try {\\n final response = await _dio.get(\\"https://example.com/api\\");\\n print(response.data);\\n } catch (e) {\\n print(\\"网络请求失败:$e\\");\\n }\\n }\\n}\\n
\\nPerformanceOverlay
MaterialApp(\\n debugShowCheckedModeBanner: false,\\n showPerformanceOverlay: true,\\n home: MyApp(),\\n);\\n
\\n布局优化:
\\nRepaintBoundary
隔离重绘。绘制优化:
\\nCustomPainter
优化复杂图形。内存优化:
\\nIsolate
处理耗时任务。网络优化:
\\ndio
)。性能监控:
\\nPerformanceOverlay
监控性能瓶颈。go_router
和 auto_route
实现复杂路由与拦截在 Flutter 中,随着应用规模的增长,路由管理变得越来越复杂。简单的 Navigator
和命名路由可能难以满足需求,比如嵌套路由、动态路由参数、路由守卫(如登录验证)等。为了解决这些问题,Flutter 社区提供了强大的第三方路由库,如 go_router
和 auto_route
。
本篇博客将深入探讨如何使用 go_router
和 auto_route
实现复杂路由管理,并结合实际场景实现路由拦截(如登录验证)。
go_router
实现复杂路由管理go_router
?go_router
是一个轻量级的路由库,专为 Flutter 设计,支持嵌套路由、动态路由参数和路由守卫。
go_router
在 pubspec.yaml
中添加依赖:
dependencies:\\n go_router: ^6.0.0\\n
\\n运行以下命令安装依赖:
\\nflutter pub get\\n
\\n使用 GoRouter
配置路由表:
import \'package:flutter/material.dart\';\\nimport \'package:go_router/go_router.dart\';\\n\\nvoid main() {\\n runApp(MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n final GoRouter _router = GoRouter(\\n initialLocation: \'/\',\\n routes: [\\n GoRoute(\\n path: \'/\',\\n builder: (context, state) => HomePage(),\\n ),\\n GoRoute(\\n path: \'/details/:id\',\\n builder: (context, state) {\\n final id = state.params[\'id\']!;\\n return DetailsPage(id: id);\\n },\\n ),\\n ],\\n );\\n\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp.router(\\n routerConfig: _router,\\n );\\n }\\n}\\n\\nclass HomePage extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\\"首页\\")),\\n body: ListView(\\n children: [\\n ListTile(\\n title: Text(\\"详情页 1\\"),\\n onTap: () => context.go(\'/details/1\'),\\n ),\\n ListTile(\\n title: Text(\\"详情页 2\\"),\\n onTap: () => context.go(\'/details/2\'),\\n ),\\n ],\\n ),\\n );\\n }\\n}\\n\\nclass DetailsPage extends StatelessWidget {\\n final String id;\\n\\n DetailsPage({required this.id});\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\\"详情页\\")),\\n body: Center(\\n child: Text(\\"详情页 ID: $id\\"),\\n ),\\n );\\n }\\n}\\n
\\nGoRouter
:定义路由表,配置路径和页面。state.params
获取动态参数。context.go
:实现页面跳转。在电商应用中,商品详情页可能包含多个子页面(如商品信息、评论、推荐)。
\\nfinal GoRouter _router = GoRouter(\\n initialLocation: \'/\',\\n routes: [\\n GoRoute(\\n path: \'/\',\\n builder: (context, state) => HomePage(),\\n routes: [\\n GoRoute(\\n path: \'details/:id\',\\n builder: (context, state) {\\n final id = state.params[\'id\']!;\\n return DetailsPage(id: id);\\n },\\n routes: [\\n GoRoute(\\n path: \'reviews\',\\n builder: (context, state) => ReviewsPage(),\\n ),\\n ],\\n ),\\n ],\\n ),\\n ],\\n);\\n
\\ncontext.go(\'/details/1/reviews\');\\n
\\n某些页面(如购物车、订单)需要登录后才能访问。
\\nbool isLoggedIn = false;\\n\\nfinal GoRouter _router = GoRouter(\\n initialLocation: \'/\',\\n routes: [\\n GoRoute(\\n path: \'/\',\\n builder: (context, state) => HomePage(),\\n ),\\n GoRoute(\\n path: \'/cart\',\\n builder: (context, state) => CartPage(),\\n redirect: (context, state) {\\n if (!isLoggedIn) {\\n return \'/login\';\\n }\\n return null;\\n },\\n ),\\n GoRoute(\\n path: \'/login\',\\n builder: (context, state) => LoginPage(),\\n ),\\n ],\\n);\\n
\\nredirect
:在路由跳转前检查登录状态。auto_route
实现复杂路由管理auto_route
?auto_route
是一个功能强大的路由库,支持代码生成、嵌套路由和路由守卫。
auto_route
在 pubspec.yaml
中添加依赖:
dependencies:\\n auto_route: ^5.0.0\\n auto_route_generator: ^5.0.0\\ndev_dependencies:\\n build_runner: ^2.0.0\\n
\\n运行以下命令安装依赖:
\\nflutter pub get\\n
\\n使用 @MaterialAutoRouter
注解定义路由表:
import \'package:auto_route/auto_route.dart\';\\nimport \'package:flutter/material.dart\';\\n\\n@MaterialAutoRouter(\\n routes: <AutoRoute>[\\n AutoRoute(page: HomePage, initial: true),\\n AutoRoute(page: DetailsPage),\\n ],\\n)\\nclass $AppRouter {}\\n
\\n运行以下命令生成路由代码:
\\nflutter pub run build_runner build\\n
\\nfinal _appRouter = AppRouter();\\n\\n@override\\nWidget build(BuildContext context) {\\n return MaterialApp.router(\\n routerDelegate: _appRouter.delegate(),\\n routeInformationParser: _appRouter.defaultRouteParser(),\\n );\\n}\\n
\\nclass AuthGuard extends AutoRouteGuard {\\n final bool isLoggedIn;\\n\\n AuthGuard(this.isLoggedIn);\\n\\n @override\\n void onNavigation(NavigationResolver resolver, StackRouter router) {\\n if (isLoggedIn) {\\n resolver.next(true); // 允许导航\\n } else {\\n router.push(LoginRoute()); // 跳转到登录页面\\n }\\n }\\n}\\n
\\n@MaterialAutoRouter(\\n routes: <AutoRoute>[\\n AutoRoute(page: HomePage, initial: true),\\n AutoRoute(page: CartPage, guards: [AuthGuard]),\\n AutoRoute(page: LoginPage),\\n ],\\n)\\nclass $AppRouter {}\\n
\\ngo_router
实现bool isLoggedIn = false;\\n\\nfinal GoRouter _router = GoRouter(\\n initialLocation: \'/\',\\n routes: [\\n GoRoute(\\n path: \'/\',\\n builder: (context, state) => HomePage(),\\n ),\\n GoRoute(\\n path: \'/details/:id\',\\n builder: (context, state) {\\n final id = state.params[\'id\']!;\\n return DetailsPage(id: id);\\n },\\n ),\\n GoRoute(\\n path: \'/cart\',\\n builder: (context, state) => CartPage(),\\n redirect: (context, state) {\\n if (!isLoggedIn) {\\n return \'/login\';\\n }\\n return null;\\n },\\n ),\\n GoRoute(\\n path: \'/login\',\\n builder: (context, state) => LoginPage(),\\n ),\\n ],\\n);\\n
\\ngo_router
和 auto_route
的对比
go_router
:轻量级,适合快速开发。auto_route
:功能强大,适合复杂项目。高级功能
\\n项目实战
\\n在 Flutter 中,路由与导航是构建多页面应用的核心功能。无论是简单的页面跳转,还是复杂的多级路由管理,掌握路由与导航的使用是开发高质量 Flutter 应用的关键。本篇博客将从基础到高级,详细介绍 Flutter 的路由与导航功能,并结合实际项目场景,探讨如何使用第三方路由库(如 go_router
和 auto_route
)实现高效的路由管理。
Navigator
是 Flutter 提供的内置导航管理器,用于管理页面的堆栈。Flutter 提供了 Navigator.push
和 Navigator.pop
方法,用于实现页面的跳转和返回。
// 页面 A\\nclass PageA extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\\"页面 A\\")),\\n body: Center(\\n child: ElevatedButton(\\n onPressed: () {\\n Navigator.push(\\n context,\\n MaterialPageRoute(builder: (context) => PageB()),\\n );\\n },\\n child: Text(\\"跳转到页面 B\\"),\\n ),\\n ),\\n );\\n }\\n}\\n\\n// 页面 B\\nclass PageB extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\\"页面 B\\")),\\n body: Center(\\n child: ElevatedButton(\\n onPressed: () {\\n Navigator.pop(context);\\n },\\n child: Text(\\"返回页面 A\\"),\\n ),\\n ),\\n );\\n }\\n}\\n
\\nNavigator.push
:\\nMaterialPageRoute
定义页面的路由。Navigator.pop
:\\n命名路由通过字符串标识页面,适合管理多页面应用。
\\nvoid main() {\\n runApp(MaterialApp(\\n initialRoute: \'/\',\\n routes: {\\n \'/\': (context) => PageA(),\\n \'/pageB\': (context) => PageB(),\\n },\\n ));\\n}\\n\\n// 页面 A\\nclass PageA extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\\"页面 A\\")),\\n body: Center(\\n child: ElevatedButton(\\n onPressed: () {\\n Navigator.pushNamed(context, \'/pageB\');\\n },\\n child: Text(\\"跳转到页面 B\\"),\\n ),\\n ),\\n );\\n }\\n}\\n\\n// 页面 B\\nclass PageB extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\\"页面 B\\")),\\n body: Center(\\n child: ElevatedButton(\\n onPressed: () {\\n Navigator.pop(context);\\n },\\n child: Text(\\"返回页面 A\\"),\\n ),\\n ),\\n );\\n }\\n}\\n
\\ninitialRoute
:定义应用的初始路由。routes
:定义路由表,映射路由名称到页面。在页面跳转时,可以通过路由传递参数。
\\n// 页面 A\\nclass PageA extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\\"页面 A\\")),\\n body: Center(\\n child: ElevatedButton(\\n onPressed: () {\\n Navigator.push(\\n context,\\n MaterialPageRoute(\\n builder: (context) => PageB(data: \\"Hello from Page A\\"),\\n ),\\n );\\n },\\n child: Text(\\"跳转到页面 B\\"),\\n ),\\n ),\\n );\\n }\\n}\\n\\n// 页面 B\\nclass PageB extends StatelessWidget {\\n final String data;\\n\\n PageB({required this.data});\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\\"页面 B\\")),\\n body: Center(\\n child: Text(data),\\n ),\\n );\\n }\\n}\\n
\\ngo_router
go_router
是一个轻量级的路由库,支持嵌套路由和动态路由。
dependencies:\\n go_router: ^6.0.0\\n
\\nimport \'package:flutter/material.dart\';\\nimport \'package:go_router/go_router.dart\';\\n\\nvoid main() {\\n runApp(MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n final GoRouter _router = GoRouter(\\n initialLocation: \'/\',\\n routes: [\\n GoRoute(\\n path: \'/\',\\n builder: (context, state) => PageA(),\\n ),\\n GoRoute(\\n path: \'/pageB\',\\n builder: (context, state) => PageB(data: state.queryParams[\'data\'] ?? \'\'),\\n ),\\n ],\\n );\\n\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp.router(\\n routerConfig: _router,\\n );\\n }\\n}\\n\\n// 页面 A\\nclass PageA extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\\"页面 A\\")),\\n body: Center(\\n child: ElevatedButton(\\n onPressed: () {\\n context.go(\'/pageB?data=Hello%20from%20Page%20A\');\\n },\\n child: Text(\\"跳转到页面 B\\"),\\n ),\\n ),\\n );\\n }\\n}\\n\\n// 页面 B\\nclass PageB extends StatelessWidget {\\n final String data;\\n\\n PageB({required this.data});\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\\"页面 B\\")),\\n body: Center(\\n child: Text(data),\\n ),\\n );\\n }\\n}\\n
\\nGoRouter
:定义路由表和初始路由。queryParams
获取路由参数。context.go
:实现页面跳转。auto_route
auto_route
是一个功能强大的路由库,支持代码生成和路由守卫。
dependencies:\\n auto_route: ^5.0.0\\n auto_route_generator: ^5.0.0\\ndev_dependencies:\\n build_runner: ^2.0.0\\n
\\n定义路由表:
\\nimport \'package:auto_route/auto_route.dart\';\\nimport \'package:flutter/material.dart\';\\n\\n@MaterialAutoRouter(\\n routes: <AutoRoute>[\\n AutoRoute(page: PageA, initial: true),\\n AutoRoute(page: PageB),\\n ],\\n)\\nclass $AppRouter {}\\n
\\n生成路由代码:
\\nflutter pub run build_runner build\\n
\\n使用路由:
\\nfinal _appRouter = AppRouter();\\n\\n@override\\nWidget build(BuildContext context) {\\n return MaterialApp.router(\\n routerDelegate: _appRouter.delegate(),\\n routeInformationParser: _appRouter.defaultRouteParser(),\\n );\\n}\\n
\\n/
:首页/product/:id
:商品详情页/cart
:购物车页go_router
实现路由import \'package:flutter/material.dart\';\\nimport \'package:go_router/go_router.dart\';\\n\\nvoid main() {\\n runApp(MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n final GoRouter _router = GoRouter(\\n initialLocation: \'/\',\\n routes: [\\n GoRoute(\\n path: \'/\',\\n builder: (context, state) => HomePage(),\\n ),\\n GoRoute(\\n path: \'/product/:id\',\\n builder: (context, state) {\\n final id = state.params[\'id\']!;\\n return ProductPage(productId: id);\\n },\\n ),\\n GoRoute(\\n path: \'/cart\',\\n builder: (context, state) => CartPage(),\\n ),\\n ],\\n );\\n\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp.router(\\n routerConfig: _router,\\n );\\n }\\n}\\n\\n// 首页\\nclass HomePage extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\\"首页\\")),\\n body: ListView(\\n children: [\\n ListTile(\\n title: Text(\\"商品 1\\"),\\n onTap: () => context.go(\'/product/1\'),\\n ),\\n ListTile(\\n title: Text(\\"商品 2\\"),\\n onTap: () => context.go(\'/product/2\'),\\n ),\\n ],\\n ),\\n );\\n }\\n}\\n\\n// 商品详情页\\nclass ProductPage extends StatelessWidget {\\n final String productId;\\n\\n ProductPage({required this.productId});\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\\"商品详情\\")),\\n body: Center(\\n child: Text(\\"商品 ID: $productId\\"),\\n ),\\n );\\n }\\n}\\n\\n// 购物车页\\nclass CartPage extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\\"购物车\\")),\\n body: Center(\\n child: Text(\\"购物车为空\\"),\\n ),\\n );\\n }\\n}\\n
\\n基础路由:
\\nNavigator
实现页面跳转和返回。第三方路由库:
\\ngo_router
实现嵌套路由和动态路由。auto_route
提供代码生成和路由守卫功能。项目实战:
\\n在 Flutter 中,Widget 树是构建 UI 的核心概念。每个 UI 元素都是一个 Widget,Widget 树决定了应用的布局和交互方式。本篇博客将从实际场景出发,详细解析如何使用 GridView
、ListView
和 Stack
构建复杂布局,并探讨如何通过性能优化(如 RepaintBoundary
和避免不必要的 setState
)提升应用的流畅度。
MaterialApp
或 CupertinoApp
。Row
、Column
、Stack
。Text
、Image
、Button
。电商首页通常包含以下内容:
\\nGridView
和 ListView
构建布局import \'package:flutter/material.dart\';\\n\\nclass EcommerceHomePage extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: Text(\\"电商首页\\"),\\n backgroundColor: Colors.blue,\\n ),\\n body: Column(\\n children: [\\n // 搜索栏\\n Padding(\\n padding: const EdgeInsets.all(8.0),\\n child: TextField(\\n decoration: InputDecoration(\\n hintText: \\"搜索商品\\",\\n prefixIcon: Icon(Icons.search),\\n border: OutlineInputBorder(\\n borderRadius: BorderRadius.circular(8.0),\\n ),\\n ),\\n ),\\n ),\\n // 分类网格\\n Expanded(\\n flex: 1,\\n child: GridView.builder(\\n gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(\\n crossAxisCount: 4, // 每行显示4个分类\\n crossAxisSpacing: 8.0,\\n mainAxisSpacing: 8.0,\\n ),\\n itemCount: 8, // 假设有8个分类\\n itemBuilder: (context, index) {\\n return Container(\\n decoration: BoxDecoration(\\n color: Colors.blue[100],\\n borderRadius: BorderRadius.circular(8.0),\\n ),\\n child: Center(\\n child: Text(\\n \\"分类 ${index + 1}\\",\\n style: TextStyle(fontSize: 14),\\n ),\\n ),\\n );\\n },\\n ),\\n ),\\n // 商品列表\\n Expanded(\\n flex: 2,\\n child: ListView.builder(\\n itemCount: 10, // 假设有10个商品\\n itemBuilder: (context, index) {\\n return ListTile(\\n leading: Container(\\n width: 50,\\n height: 50,\\n color: Colors.blue[200],\\n child: Icon(Icons.shopping_bag),\\n ),\\n title: Text(\\"商品名称 ${index + 1}\\"),\\n subtitle: Text(\\"商品描述 ${index + 1}\\"),\\n trailing: Text(\\"¥${(index + 1) * 10}\\"),\\n );\\n },\\n ),\\n ),\\n ],\\n ),\\n );\\n }\\n}\\n\\nvoid main() {\\n runApp(MaterialApp(\\n home: EcommerceHomePage(),\\n ));\\n}\\n
\\n搜索栏:
\\nTextField
实现搜索输入框。prefixIcon
和 OutlineInputBorder
提升视觉效果。分类网格:
\\nGridView.builder
动态生成分类项。SliverGridDelegateWithFixedCrossAxisCount
控制网格布局。商品列表:
\\nListView.builder
动态生成商品项。ListTile
提供标准的列表布局。在电商首页中,可能需要一个悬浮按钮(如购物车按钮)叠加在页面上。
\\nStack
实现布局class FloatingButtonExample extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n body: Stack(\\n children: [\\n // 背景内容\\n ListView.builder(\\n itemCount: 20,\\n itemBuilder: (context, index) {\\n return ListTile(\\n title: Text(\\"商品 ${index + 1}\\"),\\n subtitle: Text(\\"商品描述 ${index + 1}\\"),\\n );\\n },\\n ),\\n // 悬浮按钮\\n Positioned(\\n bottom: 20,\\n right: 20,\\n child: FloatingActionButton(\\n onPressed: () {\\n print(\\"购物车按钮点击\\");\\n },\\n child: Icon(Icons.shopping_cart),\\n ),\\n ),\\n ],\\n ),\\n );\\n }\\n}\\n
\\nStack
:
Positioned
:
bottom
和 right
属性将按钮放置在右下角。RepaintBoundary
优化复杂布局在复杂布局中,某些部分频繁重绘会影响性能。
\\n使用 RepaintBoundary
将需要重绘的部分隔离,避免影响整个 Widget 树。
class RepaintBoundaryExample extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n body: Column(\\n children: [\\n // 不需要频繁重绘的部分\\n Text(\\"静态内容\\"),\\n // 需要频繁重绘的部分\\n RepaintBoundary(\\n child: ListView.builder(\\n itemCount: 1000,\\n itemBuilder: (context, index) {\\n return ListTile(\\n title: Text(\\"动态内容 $index\\"),\\n );\\n },\\n ),\\n ),\\n ],\\n ),\\n );\\n }\\n}\\n
\\nsetState
重绘在 StatefulWidget
中,调用 setState
会触发整个 Widget 树的重建,可能导致性能问题。
StatefulBuilder
或 ValueListenableBuilder
只更新局部状态。class AvoidSetStateExample extends StatefulWidget {\\n @override\\n _AvoidSetStateExampleState createState() => _AvoidSetStateExampleState();\\n}\\n\\nclass _AvoidSetStateExampleState extends State<AvoidSetStateExample> {\\n int _counter = 0;\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\\"避免不必要的 setState\\")),\\n body: Column(\\n children: [\\n // 静态部分\\n Text(\\"静态内容\\"),\\n // 动态部分\\n StatefulBuilder(\\n builder: (context, setState) {\\n return Column(\\n children: [\\n Text(\\"计数器:$_counter\\"),\\n ElevatedButton(\\n onPressed: () {\\n setState(() {\\n _counter++;\\n });\\n },\\n child: Text(\\"增加计数\\"),\\n ),\\n ],\\n );\\n },\\n ),\\n ],\\n ),\\n );\\n }\\n}\\n
\\nWidget 树与布局:
\\nGridView
和 ListView
构建电商首页。Stack
实现悬浮按钮和叠加布局。性能优化:
\\nRepaintBoundary
隔离重绘区域。setState
重绘,提升局部更新效率。你是否需要将数据以键值对的形式关联,例如快速查询商品信息,或是实现高效的数据索引?在Dart中,Map
正是为此而设计的核心数据结构。与List
和Set
不同,Map
通过唯一的键(Key)关联值(Value),既能像字典一样直观检索,又能以接近O(1)的时间复杂度实现快速查找,成为处理复杂数据关系的“神器”。现在就让我们一起来了解一下这把处理复杂数据关系的“神器”怎么使用吧。
映射(Map)是无序的键值对集合。包含键和值两个部分,其中键具有唯一性,即不同键可以对应相同的值。
\\n如图所示,每一个键(唯一)都对应一个值(可以重复)。
\\nDart中映射(Map)和列表(List)集合(Set)一样,不仅支持指定元素为同一类型,也支持元素为不同类型(通过动态类型声明dynamic
)。
映射(Map)和集合(Set)一样可通过大括号({}
)直接创建。
示例:
\\nvoid main() {\\n Map<String,int> scoresMap = {\'John\':2,\'Michael\':23,\'David\':23,\'James\':54,\'Daniel\':8,\'Sophia\':12,\'Emma\':2};\\n}\\n
\\n示例:
\\nvoid main() {\\n Map<String,int> emptyMap = <String,int>{};\\n print(emptyMap); // 输出:{}\\n Map<String,int> fromMap = Map<String,int>.from({\'John\':2,\'Michael\':23,\'David\':23});\\n print(fromMap); // 输出:{John: 2, Michael: 23, David: 23}\\n}\\n
\\n映射(Map)和集合(Set)类似都可通过 of()
构造函数进行创建。
示例:
\\nvoid main() {\\n Map<String,int> ofMap = Map.of({\'John\':2,\'Michael\':23,\'David\':23});\\n print(ofMap); // 输出:{John: 2, Michael: 23, David: 23}\\n}\\n
\\n示例:
\\nvoid main() {\\n List<String> keyList = [\'John\',\'Michael\',\'David\'];\\n List<int> valueList = [2,23,23];\\n Map<String,int> fromIterablesMap = Map.fromIterables(keyList,valueList);\\n print(fromIterablesMap); // 输出:{John: 2, Michael: 23, David: 23}\\n // 创建MapEntry对象的列表entries。\\n List<MapEntry<String,int>> entries = [MapEntry(\'John\', 2),MapEntry(\'Michael\', 23),MapEntry(\'David\', 23)];\\n Map<String,int> fromEntriesMap = Map.fromEntries(entries);\\n print(fromEntriesMap); // 输出:{John: 2, Michael: 23, David: 23}\\n}\\n
\\nDart映射(Map)与集合(Set)、列表(List)相同,都支持定义不可修改的对象。映射(Map)使用Map.unmodifiable()构造函数创建。
\\n示例:
\\nvoid main() {\\n Map<String,int> unmodifiableMap = Map.unmodifiable({\'John\':2,\'Michael\':23,\'David\':23});\\n print(unmodifiableMap); // 输出:{John: 2, Michael: 23, David: 23}\\n // 尝试修改内容\\n unmodifiableMap.addAll({\'Daniel\':8,\'Sophia\':12,\'Emma\':2}); \\n // Unsupported operation: Cannot modify unmodifiable map\\n}\\n
\\n运行时报错:
\\n示例:
\\nvoid main() {\\n Map<String,int> scoresMap = {\'John\':2,\'Michael\':23,\'David\':23,\'James\':54,\'Daniel\':8,\'Emma\':2};\\n print(scoresMap.length); // 输出:6\\n print(scoresMap.isEmpty); // 输出:false\\n print(scoresMap.keys); // 输出:(John, Michael, David, James, Daniel, Emma)\\n print(scoresMap.values); // 输出:(2, 23, 23, 54, 8, 2)\\n print(scoresMap.entries);\\n // 输出:(MapEntry(John: 2), MapEntry(Michael: 23), MapEntry(David: 23), ..., MapEntry(Daniel: 8), MapEntry(Emma: 2))\\n}\\n
\\n示例:
\\nvoid main() {\\n Map<String,int> scoresMap = {\'John\':2,\'Michael\':23,\'David\':23,\'James\':54,\'Daniel\':8,\'Emma\':2};\\n print(scoresMap.containsKey(\'John\')); // 包含键John 输出:true\\n print(scoresMap.containsValue(1200)); // 不包含值1200 输出:false\\n}\\n
\\n映射(Map)除了不仅支持遍历,还支持通过键访问键值对的值。
\\n映射中每个键与每个值一一对应。因此可以通过键来访问值(就像列表中使用下标访问元素)。
\\n示例:
\\nvoid main() {\\n Map<String,int> scoresMap = {\'John\':2,\'Michael\':23,\'David\':23,\'James\':54,\'Daniel\':8,\'Emma\':2};\\n print(scoresMap[\'John\']); // 输出:2\\n}\\n
\\n将映射中的元素访问一遍。
\\nentries
属性。示例: for-in遍历
\\nvoid main() {\\n Map<String,int> scoresMap = {\'John\':2,\'Michael\':23,\'David\':23,\'James\':54,\'Daniel\':8,\'Emma\':2};\\n for (var entry in scoresMap.entries){\\n print(\'${entry.key} : ${entry.value}\');\\n }\\n}\\n
\\n示例: 高阶函数forEach()遍历
\\nvoid main() {\\n Map<String,int> scoresMap = {\'John\':2,\'Michael\':23,\'David\':23,\'James\':54,\'Daniel\':8,\'Emma\':2};\\n scoresMap.forEach((key,value) => print(\'$key : $value\'));\\n}\\n
\\n示例: 通过键添加
\\nvoid main() {\\n Map<String,int> scoresMap = {\'John\':2,\'Michael\':23,\'David\':23,\'Emma\':2};\\n scoresMap[\'Emma\'] = 1200;\\n print(scoresMap); // 更新scoresMap中Emma的值 输出:{John: 2, Michael: 23, David: 23, Emma: 1200}\\n scoresMap[\'Daniel\'] = 234;\\n print(scoresMap); // 输出:{John: 2, Michael: 23, David: 23, Emma: 1200, Daniel: 234}\\n}\\n
\\n示例: addAll()与addEntries()添加
\\nvoid main() {\\n Map<String,int> scoresMap = {\'John\':2,\'Michael\':23,\'David\':23,\'Emma\':2};\\n scoresMap.addAll({\'Daniel\': 234});\\n print(scoresMap); // 输出:{John: 2, Michael: 23, David: 23, Emma: 1200, Daniel: 234}\\n scoresMap.addEntries([MapEntry(\'Michael\',23),MapEntry(\'Sophia\',12)]);\\n print(scoresMap); // 输出:{John: 2, Michael: 23, David: 23, Emma: 2, Daniel: 234, Sophia: 12}\\n}\\n
\\n通过键访问到元素,再进行修改。
\\n示例:
\\nvoid main() {\\n Map<String,int> scoresMap = {\'John\':2,\'Michael\':23,\'David\':23,\'Emma\':2};\\n scoresMap[\'John\'] = 1200;\\n print(scoresMap); // 输出:{John: 1200, Michael: 23, David: 23, Emma: 2}\\n}\\n
\\n示例:
\\nvoid main() {\\n Map<String,int> scoresMap = {\'John\':2,\'Michael\':23,\'David\':23,\'Emma\':2};\\n scoresMap.remove(\'John\');\\n print(scoresMap); // 输出:{Michael: 23, David: 23, Emma: 2}\\n // 移除Michael键值对。\\n scoresMap.removeWhere((key,value) => key==\'Michael\');\\n print(scoresMap); // 输出:{David: 23, Emma: 2}\\n scoresMap.clear();\\n print(scoresMap); // 输出:{}\\n}\\n
\\n本小节归纳总结如下:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n属性或方法 | |
---|---|
创建和初始化 | 1、使用{} 直接创建2、 Map<String,int>.from() 3、 Map<String,int>.fromIterables() 4、 Map<Sring,int>.fromEntries() 5、 Map.of() |
不可变映射 | 使用Map.unmodifiable() 创建 |
常用属性和方法 | 1、length 属性 2、isEmpty 属性 3、keys 属性4、 values 属性 5、Entries 属性1、 containsKey() 方法 2、containsValue() 方法 |
访问元素 | 1、使用键访问 2、 for-in 循环遍历(结合Entries属性)3、高阶函数 forEach() 遍历 |
添加元素 | 1、使用键添加 2、 addAll() 3、 addEntries() |
修改元素 | 先访问,再修改 |
移除元素 | 1、remove() 移除键值对2、 removeWhere() 移除符合条件的键值对 3、 clear() 清空映射中所有键值对 |
go_router
是一个用于 Flutter 应用的第三方路由管理库,它简化了应用内的路由导航逻辑,提供了声明式的路由配置方式,同时对 URL 有很好的支持,在 Web、移动端和桌面端都能表现出色。开始了解以前,你可以先看一下原生路由导航:Flutter 路由与导航
GoRouter
是 go_router
库的核心类,用于配置路由信息。
GoRouter 构造函数
\\nGoRouter({\\n required List<RouteBase> routes,\\n GoRouterRedirect? redirect,\\n List<NavigatorObserver>? observers,\\n GlobalKey<NavigatorState>? navigatorKey,\\n String? initialLocation,\\n bool? debugLogDiagnostics,\\n RouteInformationParser<Uri>? routeInformationParser,\\n RouterDelegate<Uri>? routerDelegate,\\n BackButtonDispatcher? backButtonDispatcher,\\n String restorationScopeId,\\n})\\n
\\nroutes
:必填参数,用于定义应用的路由列表。redirect
:可选参数,用于在导航时进行重定向。observers
:可选参数,用于监听导航事件的观察者列表。navigatorKey
:可选参数,用于访问底层的 Navigator
实例。initialLocation
:可选参数,指定应用启动时的初始路由。debugLogDiagnostics
:可选参数,是否在调试模式下打印路由诊断信息。使用示例
\\nfinal GoRouter _router = GoRouter(\\n routes: <RouteBase>[\\n GoRoute(\\n path: \'/\',\\n builder: (BuildContext context, GoRouterState state) {\\n return const HomePage();\\n },\\n ),\\n GoRoute(\\n path: \'/details\',\\n builder: (BuildContext context, GoRouterState state) {\\n return const DetailsPage();\\n },\\n ),\\n ],\\n);\\n
\\nGoRoute 路由定义
\\nGoRoute({\\n required String path,\\n required WidgetBuilder builder,\\n List<RouteBase>? routes,\\n GoRouterRedirect? redirect,\\n LocalKey? key,\\n String? name,\\n PageBuilder? pageBuilder,\\n bool maintainState = true,\\n bool fullscreenDialog = false,\\n})\\n
\\nrequired String path
:这是一个必填参数,用于指定该路由对应的路径。路径可以是固定的字符串,也可以包含路径参数。路径参数使用冒号 :
开头,用于匹配动态的值。
required WidgetBuilder builder
:同样是必填参数,它是一个函数,用于构建该路由对应的 Widget
。该函数接收两个参数:BuildContext context
和 GoRouterState state
,并返回一个 Widget
。
GoRoute(\\n path: \'/\',\\n builder: (BuildContext context, GoRouterState state) {\\n return const HomePage();\\n },\\n)\\n
\\nList<RouteBase>? routes
:可选参数,用于定义该路由的子路由列表。子路由可以进一步细分当前路由的导航结构。
GoRoute(\\n path: \'/settings\',\\n builder: (context, state) => SettingsPage(),\\n routes: [\\n GoRoute(\\n path: \'notifications\',\\n builder: (context, state) => NotificationSettingsPage(),\\n ),\\n ],\\n)\\n
\\n/settings
是父路由,/settings/notifications
是它的子路由。
GoRouterRedirect? redirect
:是一个重定向函数。该函数接收 BuildContext context
和 GoRouterState state
作为参数,并返回一个字符串或 null
。如果返回一个字符串,则表示重定向到该路径;如果返回 null
,则正常导航到当前路由。
GoRoute(\\n path: \'/secret\',\\n redirect: (context, state) {\\n // 假设 isUserAuthenticated 是一个检查用户是否认证的函数\\n if (!isUserAuthenticated()) {\\n return \'/login\';\\n }\\n return null;\\n },\\n builder: (context, state) => SecretPage(),\\n)\\n
\\n如果用户未认证,访问 /secret
路径时会重定向到 /login
路径。
LocalKey? key
:于给该路由的 Widget
提供一个唯一的键。键可以帮助 Flutter 更准确地识别和更新 Widget
。
String? name
:为该路由指定一个名称。通过名称可以更方便地进行导航,而不需要记住具体的路径。
pageBuilder
:是一个用于构建 Page
对象的函数。与 builder
不同,pageBuilder
可以自定义页面的过渡效果等。如果同时提供了 builder
和 pageBuilder
,pageBuilder
会优先使用。
GoRoute(\\n path: \'/details\',\\n pageBuilder: (context, state) {\\n return MaterialPage(\\n key: state.pageKey,\\n child: DetailsPage(),\\n );\\n },\\n)\\n
\\nmaintainState = true
:表示当该路由离开导航栈时,是否保留其状态。如果设置为 true
,当再次返回该路由时,会恢复之前的状态;如果设置为 false
,每次进入该路由都会重新创建 Widget
。
fullscreenDialog = false
: 表示该路由是否以全屏对话框的形式显示。如果设置为 true
,在 Android 上会以全屏对话框的样式显示,在 iOS 上会有不同的过渡效果。
GoRouter
的每一个路由都通过 GoRoute
对象来配置,我们可以在构建 GoRoute
对象时来配置路由参数。路由参数典型的就是路径参数,比如 /path/:{路径参数}
,这个时候 GoRoute
的路径参数和很多 Web 框架的路由是一样的,通过一个英文冒号加参数名称就可以配置,之后我们可以在回调方法中通过 GoRouterState
对象获取路径参数,这个参数就可以传递到路由跳转目的页面。
路径参数用于在路由路径中传递动态值,例如用户 ID、文章 ID 等。在定义路由时,使用冒号 :
来标记路径参数。
import \'package:flutter/material.dart\';\\nimport \'package:go_router/go_router.dart\';\\n\\n// 定义一个接收路径参数的页面\\nclass UserPage extends StatelessWidget {\\n final String userId;\\n\\n const UserPage({Key? key, required this.userId}) : super(key: key);\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\'User $userId\')),\\n body: Center(\\n child: Text(\'User ID: $userId\'),\\n ),\\n );\\n }\\n}\\n\\n// 配置 GoRouter,定义包含路径参数的路由\\nfinal GoRouter _router = GoRouter(\\n routes: <RouteBase>[\\n GoRoute(\\n path: \'/users/:id\',\\n builder: (BuildContext context, GoRouterState state) {\\n // 从 GoRouterState 中获取路径参数\\n final String userId = state.params[\'id\']!;\\n return UserPage(userId: userId);\\n },\\n ),\\n ],\\n);\\n\\nvoid main() {\\n runApp(\\n MaterialApp.router(\\n routerConfig: _router,\\n ),\\n );\\n}\\n
\\n导航到包含路径参数的路由
\\n// 导航到包含路径参数的路由\\nGoRouter.of(context).go(\'/users/123\');\\n
\\n查询参数用于在 URL 中传递额外的信息,通常用于过滤、排序等操作。查询参数以问号 ?
开头,多个参数之间用 &
分隔。
import \'package:flutter/material.dart\';\\nimport \'package:go_router/go_router.dart\';\\n\\n// 定义一个接收查询参数的页面\\nclass SearchPage extends StatelessWidget {\\n final String? query;\\n\\n const SearchPage({Key? key, this.query}) : super(key: key);\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: const Text(\'Search\')),\\n body: Center(\\n child: Text(\'Search query: ${query ?? \'No query\'}\'),\\n ),\\n );\\n }\\n}\\n\\n// 配置 GoRouter,定义接收查询参数的路由\\nfinal GoRouter _router = GoRouter(\\n routes: <RouteBase>[\\n GoRoute(\\n path: \'/search\',\\n builder: (BuildContext context, GoRouterState state) {\\n // 从 GoRouterState 中获取查询参数\\n final String? query = state.queryParams[\'q\'];\\n return SearchPage(query: query);\\n },\\n ),\\n ],\\n);\\n\\nvoid main() {\\n runApp(\\n MaterialApp.router(\\n routerConfig: _router,\\n ),\\n );\\n}\\n
\\n导航到包含查询参数的路由
\\n// 导航到包含查询参数的路由\\nGoRouter.of(context).go(\'/search?q=flutter\');\\n
\\n// 定义带名称的路由,包含路径参数\\nfinal GoRouter _router = GoRouter(\\n routes: <RouteBase>[\\n GoRoute(\\n name: \'user\',\\n path: \'/users/:id\',\\n builder: (BuildContext context, GoRouterState state) {\\n final String userId = state.params[\'id\']!;\\n return UserPage(userId: userId);\\n },\\n ),\\n ],\\n);\\n\\n// 通过名称导航并传递路径参数\\nGoRouter.of(context).goNamed(\'user\', params: {\'id\': \'456\'});\\n\\n// 通过名称导航并传递查询参数\\nGoRouter.of(context).goNamed(\'search\', queryParams: {\'q\': \'dart\'});\\n
\\ncontext.go
直接导航到指定的路由,会替换当前的路由栈,即当前页面会被新页面替换。
\\ncontext.go(\'/details\');\\n
\\ncontext.push
将新的路由页面推送到路由栈中,当前页面不会被替换,用户可以通过返回操作回到上一个页面。
\\ncontext.push(\'/details\');\\n
\\ncontext.pop
从路由栈中弹出当前页面,返回到上一个页面。
\\ncontext.pop();\\n
\\ncontext.goNamed
和 context.pushNamed
当你为路由配置了名称时,可以使用这两个方法通过名称进行导航。
\\nfinal GoRouter _router = GoRouter(\\n routes: <RouteBase>[\\n GoRoute(\\n name: \'home\',\\n path: \'/\',\\n builder: (BuildContext context, GoRouterState state) {\\n return const HomePage();\\n },\\n routes: <RouteBase>[\\n GoRoute(\\n name: \'details\',\\n path: \'details\',\\n builder: (BuildContext context, GoRouterState state) {\\n return const DetailsPage();\\n },\\n ),\\n ],\\n ),\\n ],\\n);\\n\\n// 使用名称导航\\ncontext.goNamed(\'details\');\\ncontext.pushNamed(\'details\');\\n
\\n在 go_router
中,嵌套导航允许你在应用的某个部分实现独立的路由管理,例如在底部导航栏或者侧边栏的不同标签页内进行各自的路由切换。以下是关于 go_router
嵌套导航的详细介绍和示例代码。
ShellRoute(\\n builder: (context, state, child) => Scaffold(\\n body: child,\\n bottomNavigationBar: BottomNavBar(),\\n ),\\n routes: [\\n GoRoute(path: \'/books\', ...),\\n GoRoute(path: \'/movies\', ...),\\n ],\\n)\\n
\\nShellRoute
:ShellRoute
是 go_router
中用于实现嵌套导航的关键组件,它可以包裹子路由,提供一个共享的布局。import \'package:flutter/material.dart\';\\nimport \'package:go_router/go_router.dart\';\\n\\n// 定义页面\\nclass HomePage extends StatelessWidget {\\n const HomePage({Key? key}) : super(key: key);\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: const Text(\'Home\')),\\n body: Center(\\n child: Column(\\n mainAxisAlignment: MainAxisAlignment.center,\\n children: [\\n ElevatedButton(\\n onPressed: () {\\n context.go(\'/books\');\\n },\\n child: const Text(\'Go to Books\'),\\n ),\\n ElevatedButton(\\n onPressed: () {\\n context.go(\'/movies\');\\n },\\n child: const Text(\'Go to Movies\'),\\n ),\\n ],\\n ),\\n ),\\n );\\n }\\n}\\n\\nclass BooksPage extends StatelessWidget {\\n const BooksPage({Key? key}) : super(key: key);\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: const Text(\'Books\')),\\n body: Center(\\n child: ElevatedButton(\\n onPressed: () {\\n context.go(\'/books/details\');\\n },\\n child: const Text(\'Go to Book Details\'),\\n ),\\n ),\\n );\\n }\\n}\\n\\nclass BookDetailsPage extends StatelessWidget {\\n const BookDetailsPage({Key? key}) : super(key: key);\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: const Text(\'Book Details\')),\\n body: const Center(child: Text(\'This is the book details page.\')),\\n );\\n }\\n}\\n\\nclass MoviesPage extends StatelessWidget {\\n const MoviesPage({Key? key}) : super(key: key);\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: const Text(\'Movies\')),\\n body: const Center(child: Text(\'This is the movies page.\')),\\n );\\n }\\n}\\n\\n// 配置路由\\nfinal GoRouter _router = GoRouter(\\n routes: [\\n GoRoute(\\n path: \'/\',\\n builder: (context, state) => const HomePage(),\\n ),\\n ShellRoute(\\n builder: (context, state, child) {\\n return Scaffold(\\n body: child,\\n );\\n },\\n routes: [\\n GoRoute(\\n path: \'/books\',\\n builder: (context, state) => const BooksPage(),\\n routes: [\\n GoRoute(\\n path: \'details\',\\n builder: (context, state) => const BookDetailsPage(),\\n ),\\n ],\\n ),\\n GoRoute(\\n path: \'/movies\',\\n builder: (context, state) => const MoviesPage(),\\n ),\\n ],\\n ),\\n ],\\n);\\n\\nvoid main() {\\n runApp(\\n MaterialApp.router(\\n routerConfig: _router,\\n ),\\n );\\n}\\n
\\n1、外层路由
\\n/
,对应 HomePage
,这是应用的起始页面。2、ShellRoute
ShellRoute
包裹了两个子路由 /books
和 /movies
,它的 builder
方法返回一个 Scaffold
,用于提供一个共享的布局。
子路由的页面会显示在 Scaffold
的 body
中。
内层路由
\\n/books
路由下还有一个子路由 /books/details
,对应 BookDetailsPage
,实现了在 BooksPage
内部的嵌套导航。// 路由重定向函数\\nString? redirectLogic(GoRouterState state) {\\n final bool isGoingToLogin = state.matchedLocation == \'/login\';\\n // 如果用户未登录且不是前往登录页,则重定向到登录页\\n if (!isUserLoggedIn && !isGoingToLogin) {\\n return \'/login\';\\n }\\n // 如果用户已登录且正在前往登录页,则重定向到仪表盘页\\n if (isUserLoggedIn && isGoingToLogin) {\\n return \'/dashboard\';\\n }\\n return null; // 允许导航到目标路由\\n}\\n\\n// 配置路由\\nfinal GoRouter _router = GoRouter(\\n redirect: redirectLogic,\\n routes: [\\n GoRoute(\\n path: \'/login\',\\n builder: (context, state) => const LoginPage(),\\n ),\\n GoRoute(\\n path: \'/dashboard\',\\n builder: (context, state) => const DashboardPage(),\\n ),\\n ],\\n);\\n\\n
\\nisUserLoggedIn
布尔变量来模拟用户的登录状态。redirectLogic
函数\\nGoRouterState
对象,该对象包含了当前导航的相关信息,如目标路由的路径。/login
,将用户重定向到登录页。/dashboard
,将用户重定向到仪表盘页。null
,允许用户正常导航到目标路由。GoRouter
配置:在 GoRouter
的构造函数中传入 redirect
参数,将其设置为 redirectLogic
函数,这样每次导航前都会执行该函数的逻辑。final GoRouter _router = GoRouter(\\n routes: [\\n GoRoute(\\n path: \'/\',\\n builder: (context, state) => const HomePage(),\\n ),\\n ],\\n errorBuilder: (context, state) {\\n return ErrorPage(error: state.error);\\n },\\n);\\n
\\nerrorBuilder
参数指定了一个函数,当导航出错时会调用这个函数。该函数接收 context
和 state
两个参数,state.error
包含了具体的错误信息,我们将其传递给 ErrorPage
来显示。上述示例主要处理了导航到不存在路由的情况,也就是 404 错误。当用户尝试访问未在 routes
中定义的路由时,go_router
会触发 errorBuilder
来显示错误页面。
除了 404 错误,在实际开发中还可能遇到其他类型的错误,比如在路由的 builder
函数中抛出异常。这些错误同样会触发 errorBuilder
,你可以根据 state.error
的具体类型进行不同的处理,例如:
errorBuilder: (context, state) {\\n if (state.error is SomeSpecificException) {\\n // 处理特定类型的异常\\n return SpecificErrorPage(error: state.error);\\n }\\n return ErrorPage(error: state.error);\\n}\\n
\\n在 GoRoute
的 builder
或 pageBuilder
函数中,会传入一个 GoRouterState
对象,你可以通过它来获取路由的相关信息。
GoRoute(\\n path: \'details/:id\',\\n builder: (BuildContext context, GoRouterState state) {\\n // 获取路径参数\\n final String id = state.pathParameters[\'id\']!;\\n\\n // 获取查询参数\\n final String? queryParam = state.queryParameters[\'param\'];\\n\\n // 获取完整的 URI\\n final Uri uri = state.uri;\\n\\n return DetailsPage(id: id);\\n },\\n);\\n
\\nclass MyWidget extends StatelessWidget {\\n const MyWidget({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n final GoRouterState state = GoRouterState.of(context);\\n final String? currentPath = state.matchedLocation;\\n\\n return Text(\'Current path: $currentPath\');\\n }\\n}\\n
\\nGoRouter
的 refreshListenable
GoRouter
提供了 refreshListenable
选项,你可以传入一个 Listenable
对象,当路由发生变化时,GoRouter
会通知这个 Listenable
,进而触发更新。
/ 创建一个 ValueNotifier 作为可监听对象\\nfinal ValueNotifier<String> routeNotifier = ValueNotifier<String>(\'\');\\n\\n// 定义路由\\nfinal GoRouter _router = GoRouter(\\n refreshListenable: routeNotifier,\\n routes: <GoRoute>[\\n GoRoute(\\n path: \'/\',\\n builder: (BuildContext context, GoRouterState state) {\\n // 更新路由信息\\n routeNotifier.value = state.matchedLocation;\\n return const HomePage();\\n },\\n routes: <GoRoute>[\\n GoRoute(\\n path: \'details/:id\',\\n builder: (BuildContext context, GoRouterState state) {\\n // 更新路由信息\\n routeNotifier.value = state.matchedLocation;\\n final String id = state.pathParameters[\'id\']!;\\n return DetailsPage(id: id);\\n },\\n ),\\n ],\\n ),\\n ],\\n);\\n\\n\\n\\n// 监听路由变化的 Widget\\nclass RouteListenerWidget extends StatelessWidget {\\n const RouteListenerWidget({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n return ValueListenableBuilder<String>(\\n valueListenable: routeNotifier,\\n builder: (BuildContext context, String value, Widget? child) {\\n return Text(\'Current Route: $value\');\\n },\\n );\\n }\\n}\\n\\nvoid main() {\\n runApp(\\n MaterialApp.router(\\n routerConfig: _router,\\n home: Column(\\n children: [\\n const RouteListenerWidget(),\\n Expanded(\\n child: Router(\\n routerConfig: _router,\\n ),\\n ),\\n ],\\n ),\\n ),\\n );\\n}\\n
\\nValueNotifier<String>
类型的 routeNotifier
作为可监听对象,并将其传递给 GoRouter
的 refreshListenable
属性。builder
函数中,更新 routeNotifier
的值为当前匹配的路径。RouteListenerWidget
,使用 ValueListenableBuilder
监听 routeNotifier
的变化,并在路由变化时更新显示的路由信息。GoRouter
的 observers
class RouterObserver extends NavigatorObserver {\\n void log(value) => debugPrint(\'MyNavObserver:$value\');\\n\\n /// 当一个新的路由被推送到导航栈时,此方法会被调用。\\n @override\\n void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {\\n log(\'新的路由被推送到导航栈: ${route.toString()}, previousRoute= ${previousRoute?.toString()}\');\\n }\\n\\n /// 当一个路由从导航栈中弹出时,此方法会被调用。route 参数表示被弹出的路由,previousRoute 参数\\n @override\\n void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {\\n log(\'路由从导航栈中弹出: ${route.toString()}, previousRoute= ${previousRoute?.toString()}\');\\n }\\n\\n /// 当一个路由从导航栈中被移除时,此方法会被调用。移除路由和弹出路由不同,移除操作可以移除导航栈中任意位置的路由,而弹出操作只能移除栈顶的路由。\\n /// route 参数表示被移除的路由,previousRoute 参数表示在该路由移除后,其下一个路由(如果存在的话)。\\n @override\\n void didRemove(Route<dynamic> route, Route<dynamic>? previousRoute) {\\n log(\'didRemove: ${route.toString()}, previousRoute= ${previousRoute?.toString()}\');\\n }\\n\\n @override\\n void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) {\\n log(\'didReplace: new= ${newRoute?.toString()}, old= ${oldRoute?.toString()}\');\\n }\\n\\n /// 当用户开始进行一个导航手势(如在 iOS 上从屏幕边缘向左滑动返回上一页)时,此方法会被调用。\\n /// route 参数表示当前正在操作的路由,previousRoute 参数表示在手势操作后可能会显示的前一个路由(如果存在的话)。\\n @override\\n void didStartUserGesture(\\n Route<dynamic> route, Route<dynamic>? previousRoute) {\\n log(\'didStartUserGesture: ${route.toString()}, \'\\n \'previousRoute= ${previousRoute?.toString()}\');\\n }\\n\\n /// 用户结束导航手势时,此方法会被调用。无论手势是否成功完成导航操作,只要手势结束,就会触发这个方法。\\n @override\\n void didStopUserGesture() {\\n log(\'didStopUserGesture\');\\n }\\n}\\n\\n
\\nrouter.refresh()
(常用于登录状态变化后强制重定向)。final location = router.location();
final routeName = router.routeInformationProvider.value.name;
go_router是一个声明式的路由库,支持深度链接和导航,适合复杂的路由场景。用户可能已经了解了基础的路由配置,但现在需要将路由配置进行封装,以提高代码的可维护性和扩展性。
\\n将路由路径和名称统一管理,避免硬编码:
\\n// lib/routes/app_routes.dart\\n\\nabstract class AppRoutes {\\n static const home = \'/\';\\n static const details = \'/details\';\\n static const profile = \'/profile\';\\n static const settings = \'/settings\';\\n}\\n
\\n将不同模块的路由定义拆分到独立文件中:
\\n// lib/routes/home_route.dart\\nimport \'package:go_router/go_router.dart\';\\nimport \'../pages/home_page.dart\';\\n\\nGoRoute get homeRoute => GoRoute(\\n path: AppRoutes.home,\\n pageBuilder: (context, state) => MaterialPage(\\n key: state.pageKey,\\n child: const HomePage(),\\n ),\\n);\\n\\n// lib/routes/details_route.dart\\nimport \'package:go_router/go_router.dart\';\\nimport \'../pages/details_page.dart\';\\n\\nGoRoute get detailsRoute => GoRoute(\\n path: AppRoutes.details,\\n pageBuilder: (context, state) => MaterialPage(\\n key: state.pageKey,\\n child: const DetailsPage(),\\n ),\\n);\\n
\\n统一整合所有模块路由:
\\n// lib/routes/router_config.dart\\nimport \'package:go_router/go_router.dart\';\\nimport \'home_route.dart\';\\nimport \'details_route.dart\';\\n\\nfinal appRouter = GoRouter(\\n initialLocation: AppRoutes.home,\\n routes: [\\n homeRoute,\\n detailsRoute,\\n // 添加更多路由...\\n ],\\n);\\n
\\n实现全局路由守卫(例如登录验证):
\\n// lib/routes/route_guard.dart\\nimport \'package:go_router/go_router.dart\';\\n\\nclass RouteGuard {\\n static bool isLoggedIn = false;\\n\\n static FutureOr<String?> authGuard(\\n BuildContext context,\\n GoRouterState state,\\n ) {\\n final isLoginPage = state.location == AppRoutes.login;\\n \\n if (!isLoggedIn && !isLoginPage) {\\n return AppRoutes.login; // 跳转登录页\\n }\\n \\n if (isLoggedIn && isLoginPage) {\\n return AppRoutes.home; // 已登录时禁止返回登录页\\n }\\n \\n return null; // 允许导航\\n }\\n}\\n\\n// 在路由配置中启用\\nfinal appRouter = GoRouter(\\n redirect: RouteGuard.authGuard,\\n // ...其他配置\\n);\\n
\\n封装无需 context
的跳转方法:
// lib/utils/navigation_service.dart\\nimport \'package:flutter/material.dart\';\\nimport \'package:go_router/go_router.dart\';\\n\\nclass NavigationService {\\n static final GlobalKey<NavigatorState> navigatorKey = \\n GlobalKey<NavigatorState>();\\n\\n static BuildContext get context => \\n navigatorKey.currentState!.context;\\n\\n static void pushNamed(String routeName, {Object? extra}) {\\n context.pushNamed(routeName, extra: extra);\\n }\\n\\n static void goNamed(String routeName, {Object? extra}) {\\n context.goNamed(routeName, extra: extra);\\n }\\n\\n static void pop() => context.pop();\\n}\\n\\n// 在 MaterialApp 中注入\\nMaterialApp.router(\\n routerConfig: appRouter,\\n navigatorKey: NavigationService.navigatorKey,\\n);\\n
\\n定义统一参数传递模型
\\n// lib/models/route_args.dart\\nclass DetailsPageArgs {\\n final String id;\\n final String? source;\\n\\n DetailsPageArgs({\\n required this.id,\\n this.source,\\n });\\n}\\n\\n// 使用示例\\nNavigationService.pushNamed(\\n AppRoutes.details,\\n extra: DetailsPageArgs(id: \'123\', source: \'home\'),\\n);\\n\\n// 在页面中获取参数\\nfinal args = state.extra as DetailsPageArgs;\\n
\\n统一404页面处理:
\\nfinal appRouter = GoRouter(\\n errorPageBuilder: (context, state) => MaterialPage(\\n child: Scaffold(\\n body: Center(\\n child: Text(\'页面不存在: ${state.location}\'),\\n ),\\n ),\\n ),\\n // ...其他配置\\n);\\n
\\nlib/\\n├── main.dart\\n├── routes/\\n│ ├── app_routes.dart # 路由路径常量\\n│ ├── router_config.dart # 路由配置入口\\n│ ├── home_route.dart # 首页路由配置\\n│ ├── details_route.dart # 详情页路由配置\\n│ └── route_guard.dart # 路由守卫\\n├── models/\\n│ └── route_args.dart # 路由参数模型\\n├── utils/\\n│ └── navigation_service.dart # 导航服务\\n└── pages/\\n ├── home_page.dart\\n └── details_page.dart\\n
","description":"go_router 是一个用于 Flutter 应用的第三方路由管理库,它简化了应用内的路由导航逻辑,提供了声明式的路由配置方式,同时对 URL 有很好的支持,在 Web、移动端和桌面端都能表现出色。开始了解以前,你可以先看一下原生路由导航:Flutter 路由与导航 go_router特性\\nGoRouter的配置(routes, redirect, errorBuilder)\\n导航方法(go, push, pop)\\n命名路由(goNamed, pushNamed)\\n路由参数传递(queryParams, extra)\\n路由守卫(redirect函数)…","guid":"https://juejin.cn/post/7472230420470759424","author":"SunshineBrother","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-17T09:37:12.663Z","media":null,"categories":["iOS","Flutter","Android"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 视频播放器UI封装","url":"https://juejin.cn/post/7472191058070011967","content":"flutter 官方推荐的视频插件 video_player 是非常简陋的,除了能播放视频,其它啥功能都没有。为了满足需求,在 video_player 的基础上进行封装,封装的功能包括:
\\n包含了播放器的一些常见功能,其它功能可以根据需求自行添加。
\\n可以看到,封装后的效果还是很ok的,UI可以根据自己的喜欢进行调整。
\\n播放器就选择官方推荐的 video_player 就ok了、屏幕旋转可以使用插件,也可以使用flutter提供的api、亮度调整推荐是要 screen_brightness 插件,可以调整软件整体的亮度或者调整系统的亮度(需要配置相关权限)、状态管理我使用的是 provider ,你也可以根据自己习惯,选择其它状态管理插件。这些插件使用都很简单,可以直接看官方文档或者查一下资料,代码中稍微提一下,不做详细介绍。
\\n1、如果你是直接克隆的项目,是无法直接运行的,因为项目的 gradle 是用的本地。
\\n2、auto_orientation 插件运行时可能会出现下方错误:
\\n解决办法:在 build.gradle 中添加如下代码
\\n添加的代码,Android 打包问题我也看不懂,都是网上查的解决办法
\\n// set-namespace for information about setting the namespace.\\nsubprojects {\\n afterEvaluate { project ->\\n if (project.hasProperty(\'android\')) {\\n project.android {\\n if (namespace == null) {\\n namespace project.group\\n }\\n }\\n }\\n }\\n}\\n
\\n3、可能会出现以下问题,AGP版本低于 8.2.1 会出错
\\n解决办法:我是手动调整到 8.2.1 版本,在运行就没问题了。
\\n到这里,准备工作就完成了,接下来就是代码部分了。
\\nz_video\\n assembly -- 小组件\\n zsn_bar.dart -- 音量和亮度控制条\\n zsn_header.dart -- 顶部标题栏\\n zsn_pause.dart -- 暂停播放图标\\n zsn_progess.dart -- 进度条\\n zsn_class.dart -- 一些基础属性和方法\\n zsn_fullScreen.dart -- 全屏组件\\n zsn_providers.dart -- 状态管理\\n
\\n void initializeVideo(String videoUrl) async {\\n try {\\n // 判断是否已经初始化过\\n if (zProvider.videoInitialized) {\\n _videoController.dispose();\\n _videoController.removeListener(() {}); // 移除监听器\\n zProvider.videoInitialized = false;\\n }\\n\\n _videoController = VideoPlayerController.networkUrl(Uri.parse(videoUrl));\\n // 获取重定向后的视频链接\\n // final redirectedUrl = await getRedirectedVideoUrl(videoUrl);\\n // _videoController =\\n // VideoPlayerController.networkUrl(Uri.parse(redirectedUrl));\\n _videoController.setVolume(zProvider.currentVolume);\\n await _videoController.initialize();\\n await _videoController.play();\\n await _videoController.setLooping(true);\\n\\n _videoController.setVolume(zProvider.currentVolume);\\n zProvider.setPause(false);\\n zProvider.resetState();\\n zProvider.setVideoDuration(_videoController.value.duration);\\n zProvider.videoInitialized = true;\\n\\n // 实时监听视频播放进度\\n _videoController.addListener(() {\\n // 调整视频进度时,没有预加载的视频会重新加载,如何监听视频重新加载和加载完成\\n if (_videoController.value.isBuffering) {\\n zProvider.setBuffering(true);\\n } else if (_videoController.value.isInitialized) {\\n zProvider.setBuffering(false);\\n }\\n zProvider.setVideoCurrentDuration(_videoController.value.position);\\n });\\n } catch (e) {\\n print(\'视频初始化失败: $e\');\\n zProvider.setErrorMessage(\'Failed to load video!\'); // 设置错误信息\\n }\\n }\\n\\n
\\n @override\\n void didUpdateWidget(ZsnVideo oldWidget) {\\n super.didUpdateWidget(oldWidget);\\n // 比较旧的 videoUrl 和新的 videoUrl\\n if (oldWidget.videoUrl != widget.videoUrl) {\\n zProvider.errorMessage = null;\\n zProvider.videoDuration = const Duration();\\n zProvider.videoCurrentDuration = const Duration();\\n // 重新初始化视频\\n initializeVideo(widget.videoUrl);\\n }\\n }\\n\\n @override\\n void dispose() {\\n _videoController.dispose();\\n _videoController.removeListener(() {}); // 移除监听器\\n ZsnClass().resetApplicationBrightness(); // 重置应用亮度\\n super.dispose();\\n if (timer != null) timer!.cancel();\\n }\\n
\\nChangeNotifier
notifyListeners()
通知import \'package:flutter/material.dart\';\\n\\nclass ZsnProviders extends ChangeNotifier {\\n bool videoInitialized = false; // 视频是否初始化\\n bool pause = false; // 是否暂停\\n bool isBuffering = false; // 是否缓冲\\n bool showUI = false; // 是否显示UI\\n Duration videoDuration = const Duration(); // 视频总时长\\n Duration videoCurrentDuration = const Duration(); // 视频当前播放时长\\n double second = 0; // 快退 快进的秒数\\n Duration beforeProgress = const Duration(); // 快退 快进前的进度\\n Duration afterProgress = const Duration(); // 快退 快进后的进度\\n double currentVolume = 1.0; // 当前音量\\n bool volumeUI = false; // 音量控制UI\\n double currentBrightness = 1.0; // 当前亮度\\n bool brightnessUI = false; // 亮度控制UI\\n bool isLock = false; // 是否锁定\\n String? errorMessage; // 添加错误信息属性\\n\\n // 数据重置\\n ZsnProviders() {\\n resetState();\\n }\\n void resetState() {\\n isLock = false;\\n showUI = false;\\n errorMessage = null;\\n notifyListeners();\\n }\\n \\n void changePause() {\\n pause = !pause;\\n notifyListeners();\\n }\\n\\n void setPause(bool isPause) {\\n pause = isPause;\\n notifyListeners();\\n }\\n ···\\n 其它方法\\n ···\\n}\\n
\\nimport \'package:flutter/material.dart\';\\nimport \'package:provider/provider.dart\';\\nimport \'package:videodemo/home.dart\';\\nimport \'package:videodemo/http/dio_instance.dart\';\\nimport \'package:videodemo/z_video/zsn_providers.dart\';\\n\\nvoid main() {\\n // dio初始化\\n DioInstance.instance().initDio(baseUrl: \'\');\\n // 注入 provider\\n runApp(MultiProvider(\\n providers: [ChangeNotifierProvider(create: (contex) => ZsnProviders())],\\n child: const MyApp()));\\n}\\n\\nclass MyApp extends StatelessWidget {\\n const MyApp({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n debugShowCheckedModeBanner: false,\\n title: \'Flutter Demo\',\\n theme: ThemeData(\\n colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),\\n useMaterial3: true,\\n ),\\n home: Home(),\\n );\\n }\\n}\\n\\n
\\n1、结合 Consumer
就能直接使用了
@override\\n Widget build(BuildContext context) {\\n return Consumer<ZsnProviders>(\\n builder: (context, provider, child) {\\n return Text(provider.参数名);\\n ···\\n
\\n2、定义一个全局的参数,方便在函数中调用。在 didChangeDependencies
中赋予初始值,这样才能安全的拿到 context,在需要使用的地方 zProvider.参数名
或者 zProvider.方法名()
。
class _VideoBaseState extends State<ZsnVideo> {\\n late VideoPlayerController _videoController;\\n\\n Timer? timer; // 创建个延时器\\n ZsnProviders zProvider = ZsnProviders(); // 获取provider\\n ···\\n @override\\n void didChangeDependencies() {\\n super.didChangeDependencies();\\n zProvider = Provider.of<ZsnProviders>(context, listen: false);\\n }\\n ···\\n
\\nformatDuration
把 Duration 时间转换成 02:25:23
或 02:25
格式volumeIcon
、brightnessIcon
根据亮度和音量的大小返回特定图标applicationBrightness
、setApplicationBrightness
、resetApplicationBrightness
设置应用亮度calculateTopPosition
计算图标的位置import \'package:flutter/material.dart\';\\nimport \'package:screen_brightness/screen_brightness.dart\';\\n\\nclass ZsnClass {\\n Color baseColor = const Color.fromARGB(255, 0, 110, 255);\\n ZsnClass();\\n get baseColor1 => baseColor;\\n // 计算时分秒\\n String formatDuration(Duration duration) {\\n String twoDigits(int n) => n.toString().padLeft(2, \\"0\\");\\n String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60));\\n String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60));\\n return duration.inHours > 0\\n ? \\"${twoDigits(duration.inHours)}:$twoDigitMinutes:$twoDigitSeconds\\"\\n : \\"$twoDigitMinutes:$twoDigitSeconds\\";\\n }\\n\\n Icon volumeIcon(double volume) {\\n if (volume == 0) {\\n return Icon(Icons.volume_off, color: Colors.grey);\\n } else if (volume < 0.5) {\\n return Icon(Icons.volume_down, color: Colors.blue);\\n } else {\\n return Icon(Icons.volume_up, color: Colors.blue);\\n }\\n }\\n\\n Icon brightnessIcon(double brightness) {\\n if (brightness == 0) {\\n return Icon(Icons.brightness_low, color: Colors.grey);\\n } else if (brightness < 0.5) {\\n return Icon(Icons.brightness_medium, color: Colors.blue);\\n } else {\\n return Icon(Icons.brightness_high, color: Colors.blue);\\n }\\n }\\n\\n // 获取应用亮度\\n Future<double> get applicationBrightness async {\\n try {\\n return await ScreenBrightness.instance.application;\\n } catch (e) {\\n throw \'Failed to get application brightness\';\\n }\\n }\\n\\n // 设置应用亮度\\n Future<void> setApplicationBrightness(double brightness) async {\\n try {\\n await ScreenBrightness.instance\\n .setApplicationScreenBrightness(brightness);\\n } catch (e) {\\n debugPrint(e.toString());\\n throw \'Failed to set application brightness\';\\n }\\n }\\n\\n // 重置应用亮度\\n Future<void> resetApplicationBrightness() async {\\n try {\\n await ScreenBrightness.instance.resetApplicationScreenBrightness();\\n } catch (e) {\\n debugPrint(e.toString());\\n throw \'Failed to reset application brightness\';\\n }\\n }\\n\\n // 计算锁屏UI、亮度UI、音量UI的位置\\n // top = 元素高度的一半 - 自身高度的一半(默认为按钮高度的一半)\\n double calculateTopPosition(BuildContext context, {double height = 48.0}) {\\n final screenHeight = MediaQuery.of(context).size.height;\\n return (screenHeight / 2) - (height / 2);\\n }\\n}\\n
\\n基本函数、基本参数、基本用法介绍完毕。
\\n过于简单,代码就不贴了,可自行查看,不然文章太长。
\\n同上
\\nimport \'dart:math\';\\n\\nimport \'package:flutter/material.dart\';\\nimport \'package:videodemo/z_video/zsn_class.dart\';\\n\\nclass ZsnBar extends StatelessWidget {\\n final double value;\\n final double height;\\n final bool isVolume; // 默认音量图标\\n const ZsnBar({\\n super.key,\\n required this.value,\\n required this.height,\\n this.isVolume = true,\\n });\\n @override\\n Widget build(BuildContext context) {\\n return Positioned(\\n right: 0,\\n top: (height - 30) / 2,\\n height: 30,\\n width: 100,\\n child: Transform.rotate(\\n angle: -pi / 2,\\n child: Row(\\n children: [\\n Transform.rotate(\\n angle: pi / 2,\\n child: isVolume\\n ? ZsnClass().volumeIcon(value)\\n : ZsnClass().brightnessIcon(value),\\n ),\\n SizedBox(\\n width: 3,\\n height: 3,\\n ),\\n Expanded(\\n child: LinearProgressIndicator(\\n backgroundColor: Colors.grey[200],\\n valueColor: AlwaysStoppedAnimation(Colors.blue),\\n value: value,\\n ),\\n )\\n ],\\n ),\\n ),\\n );\\n }\\n}\\n\\n
\\nimport \'package:flutter/material.dart\';\\nimport \'package:video_player/video_player.dart\';\\nimport \'package:videodemo/z_video/zsn_class.dart\';\\n\\n\\nclass ZsnProgess extends StatelessWidget {\\n final VideoPlayerController controller;\\n const ZsnProgess({super.key, required this.controller});\\n\\n @override\\n Widget build(BuildContext context) {\\n return Expanded(\\n child: VideoProgressIndicator(\\n controller,\\n allowScrubbing: true,\\n padding: EdgeInsets.all(5),\\n colors: VideoProgressColors(\\n backgroundColor: Colors.white,\\n playedColor: ZsnClass().baseColor,\\n bufferedColor: Colors.grey[500]!,\\n ),\\n ),\\n );\\n }\\n}\\n\\n
\\n // 底部控制条 UI\\n if (provider.showUI && !provider.isLock)\\n Positioned(\\n bottom: 0,\\n left: 0,\\n child: Container(\\n padding: EdgeInsets.all(5),\\n width: MediaQuery.of(context).size.width,\\n // ignore: deprecated_member_use\\n color: Colors.black.withOpacity(0.1),\\n child: Row(\\n mainAxisAlignment: MainAxisAlignment.spaceBetween,\\n // 上下居中\\n crossAxisAlignment: CrossAxisAlignment.center,\\n children: [\\n // 播放暂停按钮\\n ZsnPause(\\n pause: provider.pause, playVideo: playVideo),\\n // 播放进度\\n Text(\\n ZsnClass().formatDuration(\\n provider.videoCurrentDuration),\\n style: TextStyle(color: Colors.white),\\n ),\\n SizedBox(\\n width: 10,\\n ),\\n // 进度条\\n ZsnProgess(controller: _videoController),\\n SizedBox(\\n width: 10,\\n ),\\n // 播放进度\\n Text(\\n ZsnClass()\\n .formatDuration(provider.videoDuration),\\n style: TextStyle(color: Colors.white),\\n ),\\n // 全屏按钮\\n IconButton(\\n icon: Icon(\\n Icons.fullscreen,\\n color: Colors.white,\\n ),\\n onPressed: () {\\n // 跳转全屏\\n Navigator.push(\\n context,\\n MaterialPageRoute(\\n builder: (context) => ZsnFullscreen(\\n title: widget.title,\\n providers: provider,\\n controller: _videoController,\\n pauseEvent: pauseEvent,\\n onTap: onTap,\\n onDoubleTap: onDoubleTap,\\n onLongPress: onLongPress,\\n playVideo: playVideo,\\n onVerticalDragUpdate:\\n onVerticalDragUpdate,\\n onVerticalDragEnd:\\n onVerticalDragEnd,\\n onHorizontalDragUpdate:\\n onHorizontalDragUpdate,\\n onHorizontalDragEnd:\\n onHorizontalDragEnd,\\n onLongPressEnd: onLongPressEnd,\\n )));\\n },\\n ),\\n ]),\\n )),\\n
\\n // 锁屏按钮\\n if (provider.showUI)\\n Positioned(\\n top: widget.height / 2 - 25,\\n left: 10,\\n child: IconButton(\\n style: ButtonStyle(\\n backgroundColor: WidgetStateProperty.all(\\n const Color.fromARGB(68, 158, 158, 158)),\\n ),\\n onPressed: () {\\n provider.changeLock();\\n },\\n icon: Icon(\\n provider.isLock ? Icons.lock : Icons.lock_open,\\n color: Colors.white,\\n size: 25,\\n ))),\\n
\\n // 播放暂停\\n void playVideo() {\\n zProvider.changePause();\\n if (zProvider.pause) {\\n _videoController.pause();\\n } else {\\n _videoController.play();\\n }\\n }\\n\\n // 控制亮度(模拟)\\n void _controlBrightness(double delta) {\\n double newBrightness = zProvider.currentBrightness + delta;\\n newBrightness = newBrightness.clamp(0.0, 1.0); // 亮度限制在 0-1\\n zProvider.setBrightness(newBrightness);\\n ZsnClass().setApplicationBrightness(newBrightness);\\n }\\n\\n // 控制音量\\n void _controlVolume(double delta) {\\n double newVolume = zProvider.currentVolume + delta;\\n newVolume = newVolume.clamp(0.0, 1.0); // 音量限制在 0-1\\n zProvider.setVolume(newVolume);\\n _videoController.setVolume(newVolume);\\n }\\n\\n
\\n void onTap() {\\n // 点击时显示或隐藏控制栏\\n zProvider.changeShowUI();\\n timer?.cancel();\\n timer = Timer(Duration(milliseconds: 5000), () {\\n zProvider.setShowUI(false);\\n });\\n }\\n
\\n void onDoubleTap() {\\n // 双击时暂停或播放视频\\n zProvider.changePause();\\n if (zProvider.pause) {\\n _videoController.pause();\\n } else {\\n _videoController.play();\\n }\\n }\\n\\n
\\n // 长按开始\\n void onLongPress() {\\n if (zProvider.isLock) return;\\n // 长按改变视频倍速\\n _videoController.setPlaybackSpeed(3.0);\\n }\\n
\\n void onLongPressEnd(details) {\\n if (zProvider.isLock) return;\\n // 长按结束时取消视频倍速改变\\n _videoController.setPlaybackSpeed(1.0);\\n }\\n
\\nvoid onVerticalDragUpdate(details) {\\n if (zProvider.isLock) return;\\n // 判断是否是上下滑动\\n if (details.delta.dy.abs() > details.delta.dx.abs()) {\\n final double screenWidth = MediaQuery.of(context).size.width;\\n double positionX = details.localPosition.dx;\\n if (positionX < screenWidth / 2) {\\n // 手势在左侧 控制视频亮度 保留小数点后一位\\n zProvider.setBrightnessUI(true);\\n _controlBrightness(-details.delta.dy / 100);\\n } else {\\n // 手势在右侧 控制视频音量\\n zProvider.setVolumeUI(true);\\n _controlVolume(-details.delta.dy / 100);\\n }\\n }\\n }\\n\\n
\\n void onVerticalDragEnd(details) {\\n if (zProvider.isLock) return;\\n zProvider.setBrightnessUI(false);\\n zProvider.setVolumeUI(false);\\n }\\n
\\n void onHorizontalDragUpdate(details) {\\n if (zProvider.isLock) return;\\n if (details.delta.dx.abs() > details.delta.dy.abs()) {\\n // 记录滑动开始时视频的播放进度\\n if (zProvider.beforeProgress == Duration.zero) {\\n zProvider.setBeforeProgress(zProvider.videoCurrentDuration);\\n }\\n // 根据滑动距离设置快进秒数\\n zProvider.setSecond(details.delta.dx);\\n zProvider.calculateAfterProgress();\\n }\\n }\\n
\\n void onHorizontalDragEnd(details) {\\n if (zProvider.isLock) return;\\n _videoController.seekTo(zProvider.afterProgress);\\n zProvider.setBeforeProgress(Duration.zero);\\n zProvider.setAfterProgress(Duration.zero);\\n zProvider.resetSecond();\\n }\\n
\\n bool isLand = false; // 是否横屏\\n \\n @override\\n void initState() {\\n super.initState();\\n\\n // 通过视频比例确定是否需要全屏\\n if (widget.controller.value.aspectRatio < 1) {\\n // 竖屏模式\\n SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);\\n AutoOrientation.portraitUpMode();\\n } else {\\n // 横屏模式\\n SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);\\n AutoOrientation.landscapeRightMode();\\n isLand = true;\\n }\\n }\\n
\\n @override\\n void dispose() {\\n if (isLand == true) {\\n AutoOrientation.portraitUpMode();\\n }\\n // 恢复系统状态栏\\n SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,\\n overlays: SystemUiOverlay.values);\\n super.dispose();\\n }\\n
\\n(/ω\),淦,终于写完了,写的不是很全,详情见代码。
\\n代码地址: https://gitee.com/zsnoin-can/z_video
WidgetsFlutterBinding.ensureInitialized()
方法的主要作用是确保 Flutter 的 WidgetsBinding
被初始化。WidgetsBinding
是 Flutter 框架的核心绑定类之一,它负责将 Flutter 的 widget 层与 Flutter 引擎连接起来,使得 Flutter 应用能够与底层的渲染、输入、生命周期管理等系统进行交互。
在 Flutter 应用启动时,需要初始化一系列的底层系统,如渲染系统、输入系统、手势识别系统等。WidgetsFlutterBinding.ensureInitialized()
会确保这些系统被正确初始化,为后续的 widget 构建、渲染和交互提供基础。
WidgetsBinding
还负责管理应用的生命周期,例如应用进入后台、回到前台、暂停、恢复等状态。通过调用 WidgetsFlutterBinding.ensureInitialized()
,可以确保应用的生命周期管理系统正常工作,开发者可以监听这些生命周期事件并做出相应的处理。
许多 Flutter 插件和服务依赖于 WidgetsBinding
的初始化。在调用这些插件或服务之前,必须确保 WidgetsBinding
已经初始化,否则可能会导致异常或错误。
当你需要在应用启动时执行一些异步操作,如读取本地存储、初始化第三方服务等,通常需要先调用 WidgetsFlutterBinding.ensureInitialized()
。例如:
import \'package:flutter/material.dart\';\\nimport \'package:flutter/services.dart\';\\n\\nvoid main() async {\\n // 确保 WidgetsBinding 被初始化\\n WidgetsFlutterBinding.ensureInitialized();\\n\\n // 执行异步操作,如设置系统导航栏颜色\\n SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(\\n statusBarColor: Colors.transparent,\\n ));\\n\\n runApp(const MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n const MyApp({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n title: \'Flutter Demo\',\\n theme: ThemeData(\\n primarySwatch: Colors.blue,\\n ),\\n home: const Scaffold(\\n body: Center(\\n child: Text(\'Hello, World!\'),\\n ),\\n ),\\n );\\n }\\n}\\n
\\n如果你使用了一些需要初始化的 Flutter 插件,也需要先调用 WidgetsFlutterBinding.ensureInitialized()
。例如,使用 shared_preferences
插件来读写本地存储:
import \'package:flutter/material.dart\';\\nimport \'package:shared_preferences/shared_preferences.dart\';\\n\\nvoid main() async {\\n // 确保 WidgetsBinding 被初始化\\n WidgetsFlutterBinding.ensureInitialized();\\n\\n // 获取 SharedPreferences 实例\\n final prefs = await SharedPreferences.getInstance();\\n\\n runApp(const MyApp());\\n}\\n\\nclass MyApp extends StatelessWidget {\\n const MyApp({super.key});\\n\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n title: \'Flutter Demo\',\\n theme: ThemeData(\\n primarySwatch: Colors.blue,\\n ),\\n home: const Scaffold(\\n body: Center(\\n child: Text(\'Hello, World!\'),\\n ),\\n ),\\n );\\n }\\n}\\n
\\n在 Flutter 中,绑定(Binding)是连接 Flutter 框架和底层系统的桥梁,它负责处理各种系统事件(如输入、渲染、生命周期等)并将其传递给 Flutter 应用的各个部分。WidgetsFlutterBinding
是其中一个重要的绑定类,它继承自多个基础绑定类,综合了多种功能,将 widget 层与 Flutter 引擎连接起来。
WidgetsFlutterBinding
使用了单例模式,确保在整个应用的生命周期中只有一个 WidgetsFlutterBinding
实例。这是通过静态属性 _instance
来实现的,代码示例如下:
class WidgetsFlutterBinding extends BindingBase with GestureBinding, SchedulerBinding, ServicesBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {\\n static WidgetsFlutterBinding? _instance;\\n\\n /// Returns an instance of the [WidgetsBinding], creating and initializing it if necessary.\\n static WidgetsFlutterBinding ensureInitialized() {\\n if (_instance == null) {\\n WidgetsFlutterBinding();\\n }\\n return _instance!;\\n }\\n\\n /// Initializes this [WidgetsFlutterBinding].\\n WidgetsFlutterBinding() {\\n assert(_instance == null);\\n _instance = this;\\n initInstances();\\n initServiceExtensions();\\n SystemChannels.lifecycle.setMessageHandler(_handleLifecycleMessage);\\n readInitialLifecycleStateFromNativeWindow();\\n }\\n \\n}\\n
\\n在 ensureInitialized()
方法中,首先检查 _instance
是否为 null
。如果为 null
,则创建一个新的 WidgetsFlutterBinding
实例;否则,直接返回现有的实例。
当 _instance
为 null
时,会调用 WidgetsFlutterBinding
的构造函数。在构造函数中,会执行一系列的初始化操作:
initInstances()
:这是一个重要的方法,它会调用父类的 initInstances()
方法,依次初始化各个绑定类的实例。例如,GestureBinding
负责处理手势事件,SchedulerBinding
负责调度任务,ServicesBinding
负责处理平台消息等。这些绑定类的初始化确保了 Flutter 应用能够与底层系统进行交互。initServiceExtensions()
:该方法用于初始化 Flutter 的服务扩展,服务扩展提供了一些额外的功能,如性能监控、调试工具等。SystemChannels.lifecycle.setMessageHandler(_handleLifecycleMessage)
,可以监听应用的生命周期事件(如进入后台、回到前台等),并在 _handleLifecycleMessage
方法中进行相应的处理。readInitialLifecycleStateFromNativeWindow()
方法用于从原生窗口读取应用的初始生命周期状态,确保应用在启动时能够正确处理生命周期事件。由于内网无法正常访问到谷歌相关资源站点,导致canvaskit.js和字体资源(fonts.gstatic.com)无法被加载,项目部署后显示白屏。将资源提前下载好后,通过修改资源引用地址,来解决这个问题。canvaskit.js相关资源是打包后就存在了的,但是项目仍然会从网络获取,所以只需要更改下引用地址就好,但是文字资源就需要自己手动下载了。
\\n1.修改canvaskit.js的引用,从网络获取改为本地获取
\\n①在项目的根目录的web目录下,新建flutter_bootstrap.js文件
\\n{{flutter_js}}\\n{{flutter_build_config}}\\n{{flutter_service_worker_version}}\\n\\n_flutter.loader.load({\\n config: {\'canvasKitBaseUrl\': \'canvaskit/\'}\\n});\\n
\\n②将index.html文件中flutter_bootstrap.js的引用方式改为下面的方式:
\\n<script>\\n {{flutter_bootstrap_js}}\\n</script>\\n
\\n2.flutter build web 打包,生成的web项目位于项目中的/build/web目录内
\\n3.修改字体资源的引用,从网络获取改为本地获取
\\n①将需要用到的字体文件从网络提前下载好,集中放入/build/web/fonts文件夹。
\\n②修改打包后的main.dart.js,修改资源引用地址。由于需要修改的地方较多,所以写了一段脚本,脚本文件位于项目根目录
\\nimport \'dart:io\';\\n\\n/*\\n* 脚本适用于flutter 3.29.0\\n* 执行flutter build web后,执行脚本。将字体资源的引用改为使用本地资源\\n* 在Android Studio的终端中执行命令: dart run .\\\\script_for_web.dart\\n* */\\n\\nFuture<void> main() async {\\n final targetFile = File(\'./build/web/main.dart.js\');\\n if (!targetFile.existsSync()) {\\n print(\'❌ /build/web/main.dart.js文件不存在,请打包出web项目后执行\');\\n return;\\n }\\n try {\\n var content = await targetFile.readAsString();\\n var replaceStr = \'https://fonts.gstatic.com/s/\';\\n if (content.contains(replaceStr)) {\\n content = content.replaceAll(replaceStr, \'\');\\n print(\'将$replaceStr替换成了空字符串\');\\n }\\n replaceStr = \'A.h1().gPC()+\\"roboto/v32/KFOmCnqEu92Fr1Me4GZLCzYlKw.woff2\\"\';\\n if (content.contains(replaceStr)) {\\n content = content.replaceAll(replaceStr, \'\\"/fonts/KFOmCnqEu92Fr1Me4GZLCzYlKw.woff2\\"\');\\n print(\'将$replaceStr替换成了\\"/fonts/KFOmCnqEu92Fr1Me4GZLCzYlKw.woff2\\"\');\\n }\\n for (int i = 1; i < 120; i++) {\\n var oldStr = \'notosanssc/v37/k3kCo84MPvpLmixcA63oeAL7Iqp5IZJF9bmaG9_FnYkldv7JjxkkgFsFSSOPMOkySAZ73y9ViAt3acb8NexQ2w.$i.woff2\';\\n var newStr = \'/fonts/k3kCo84MPvpLmixcA63oeAL7Iqp5IZJF9bmaG9_FnYkldv7JjxkkgFsFSSOPMOkySAZ73y9ViAt3acb8NexQ2w.$i.woff2\';\\n if (content.contains(oldStr)) {\\n content = content.replaceAll(oldStr, newStr);\\n print(\'将$oldStr替换成了$newStr\');\\n }\\n }\\n await targetFile.writeAsString(content);\\n print(\'🎉 文件修改成功\');\\n }catch (e){\\n print(\'❌ ${e.toString()}\');\\n }\\n}\\n
\\n至此,修改完成,可使用python3开启web服务进行验证!(python -m http.server 8000)
\\n参考资料:docs.flutter.cn/platform-in…
","description":"由于内网无法正常访问到谷歌相关资源站点,导致canvaskit.js和字体资源(fonts.gstatic.com)无法被加载,项目部署后显示白屏。将资源提前下载好后,通过修改资源引用地址,来解决这个问题。canvaskit.js相关资源是打包后就存在了的,但是项目仍然会从网络获取,所以只需要更改下引用地址就好,但是文字资源就需要自己手动下载了。 1.修改canvaskit.js的引用,从网络获取改为本地获取\\n\\n①在项目的根目录的web目录下,新建flutter_bootstrap.js文件\\n\\n{{flutter_js}}\\n{{flutter_build…","guid":"https://juejin.cn/post/7472174331280703514","author":"billy_huang","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-17T06:10:56.097Z","media":null,"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"系统化掌握Dart编程之映射(Map)(二):键值对的时空交响曲","url":"https://juejin.cn/post/7471979172115595300","content":"Map
—— 键值对的时空交响曲
。
在Dart
的编程宇宙中,Map
如同精密的时空转换器,用键(Key
)与值(Value
)的量子纠缠,构建出高效的数据存取网络。从用户配置缓存到路由参数传递,从API
响应解析到状态管理枢纽,Map
以O(1)
的魔法时间复杂度,在移动端开发中扮演着数据高速公路的角色。
但在这优雅的API
背后,隐藏着哈希碰撞的量子风暴
、负载因子的平衡艺术
、红黑树的自我修复魔法
。理解Map
的实现本质,不仅关乎数据结构的选用智慧,更是打开高性能Flutter
开发之门的密钥。
本文将带你穿透表面语法,直击Map
的量子核心,揭示其如何通过哈希算法
、冲突解决策略
和内存优化技术
,在有限的内存空间里编织无限可能。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\nMap的量子态特性
Map
的时空法则键值纠缠原理:
\\nKey
与Value
的绑定关系(Entry
结构体实现)。时间复杂度图谱:
\\nfinal map = {\'a\':1, \'b\':2};\\nmap[\'c\'] = 3; // O(1) 平均插入时间\\nmap.containsKey(\'b\'); // O(1) 哈希查找\\nmap.remove(\'a\'); // O(1) 删除操作\\n
\\n类型 | 数据结构 | 特性 | 内存密度(万条目) |
---|---|---|---|
HashMap | 哈希表 | 最高效通用实现 | 3.2MB |
LinkedHashMap | 哈希表+双向链表 | 保持插入顺序 | 3.8MB |
SplayTreeMap | 伸展树 | 自动平衡有序映射 | 4.5MB |
基础操作演示:
\\n// 声明与初始化\\nfinal config = <String, dynamic>{\\n \'theme\': \'dark\',\\n \'fontSize\': 14.0,\\n \'notifications\': true\\n};\\n\\n// 级联操作\\nfinal modified = config..[\'locale\'] = \'zh-CN\'..remove(\'theme\');\\n
\\n量子纠缠的七十二变
// JSON自动展平工具\\nMap<String, dynamic> flattenMap(Map<String, dynamic> map, {String prefix = \'\'}) {\\n return map.entries.fold<Map<String, dynamic>>({}, (result, entry) {\\n final key = prefix.isEmpty ? entry.key : \'$prefix.${entry.key}\';\\n if (entry.value is Map) {\\n result.addAll(flattenMap(entry.value as Map, prefix: key));\\n } else {\\n result[key] = entry.value;\\n }\\n return result;\\n });\\n}\\n\\n// 输入:{\'user\': {\'profile\': {\'name\':\'Alice\'}, \'age\':30}}\\n// 输出:{\'user.profile.name\':\'Alice\', \'user.age\':30}\\n
\\nclass ReactiveMap<K, V> extends ChangeNotifier {\\n final Map<K, V> _storage = {};\\n \\n V operator [](K key) => _storage[key]!;\\n \\n void operator []=(K key, V value) {\\n if (_storage[key] != value) {\\n _storage[key] = value;\\n notifyListeners();\\n }\\n }\\n \\n // 自动同步到SharedPreferences\\n void persist() => _storage.forEach((k,v) => SharedPreferences.set(k, v));\\n}\\n
\\n突破量子隧穿效应
键类型 | 默认哈希质量 | 优化方案 | 碰撞率下降 |
---|---|---|---|
长字符串 | 一般 | 预计算哈希值缓存 | 75% |
复合对象 | 较差 | Jenkins混合哈希算法 | 92% |
浮点数 | 精度问题 | 定点数转换 | 100% |
优化代码示例:
\\nimport \'package:quiver/core.dart\'; // 需要添加quiver依赖\\n\\nclass HighPerfKey {\\n final String id;\\n final DateTime timestamp;\\n\\n HighPerfKey(this.id, this.timestamp);\\n\\n @override\\n int get hashCode => JenkinsHash.run([\\n id.hashCode, \\n timestamp.microsecondsSinceEpoch\\n ]);\\n\\n @override\\n bool operator ==(Object other) {\\n if (identical(this, other)) return true;\\n return other is HighPerfKey &&\\n other.id == id &&\\n other.timestamp == timestamp;\\n }\\n\\n // 可选:添加toString()方便调试\\n @override\\n String toString() => \'HighPerfKey($id, ${timestamp.toIso8601String()})\';\\n}\\n
\\n// 使用WeakMap实现缓存自动回收\\nfinal _imageCache = Expando<Uint8List>();\\n\\nvoid cacheImage(String url, Uint8List data) {\\n _imageCache[url] = data; // 当内存不足时自动释放\\n}\\n\\n// 内存实测对比(加载100张1MB图片):\\n// 传统Map:102MB WeakMap:58MB\\n
\\nDart的量子引擎
HashMap
的哈希矩阵存储结构全息图:
\\n索引槽:[•→] [→Entry@1→Entry@5] [→Entry@2]...\\n │ │ │\\n 空 数据 哈希碰撞链\\n
\\n核心源码解析(sdk/lib/internal/hash_map.dart
):
class _HashMap<K, V> {\\n List<_HashMapEntry<K, V>?> _buckets;\\n int _elementCount = 0;\\n \\n void operator []=(K key, V value) {\\n final hashCode = _computeHash(key);\\n final index = hashCode & (_buckets.length - 1);\\n var entry = _buckets[index];\\n while (entry != null) {\\n if (entry._equals(key)) {\\n entry.value = value; // 更新已有键\\n return;\\n }\\n entry = entry.next;\\n }\\n _buckets[index] = _HashMapEntry(key, value, hashCode, _buckets[index]);\\n _elementCount++;\\n if (_shouldRehash) _rehash();\\n }\\n}\\n
\\nvoid _rehash() {\\n final newCapacity = _buckets.length * 2;\\n final newBuckets = List<_HashMapEntry<K, V>?>.filled(newCapacity, null);\\n \\n for (var entry in _buckets) {\\n while (entry != null) {\\n final newIndex = entry.hashCode & (newCapacity - 1);\\n final nextEntry = entry.next;\\n entry.next = newBuckets[newIndex];\\n newBuckets[newIndex] = entry;\\n entry = nextEntry;\\n }\\n }\\n _buckets = newBuckets;\\n}\\n
\\n混沌中的秩序之美
0.75
:空间利用率与操作速度的完美平衡点。负载因子>0.75
时,哈希碰撞概率呈指数级上升。跨语言实现对比:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n语言 | 初始容量 | 扩容阈值 | 冲突解决方案 |
---|---|---|---|
Dart | 8 | 0.75 | 链地址法(单向链表) |
Java | 16 | 0.75 | 红黑树转换 |
Map
的量子冻结// 使用const创建编译期常量\\nconst final config = const {\\n \'maxRetries\': 3,\\n \'timeout\': Duration(seconds: 30)\\n};\\n\\n// 底层实现:采用共享的不可变哈希结构\\n// 源码节选(sdk/lib/_internal/immutable_map.dart)\\nclass _ImmutableHashMap<K, V> {\\n final _data = _compactHashArray; // 共享内存存储\\n final _hashMask; // 位掩码优化\\n}\\n
\\n量子跃迁实战手册
class BigDataIndexer {\\n final List<HashMap<String, List<int>>> _shards = List.generate(16, (_) => HashMap());\\n \\n void addDocument(String id, List<String> keywords) {\\n final shard = _shards[id.hashCode % 16];\\n for (final word in keywords) {\\n shard.update(word, (list) => list..add(id), ifAbsent: () => [id]);\\n }\\n }\\n \\n List<int> search(String keyword) {\\n return _shards.expand((shard) => shard[keyword] ?? []).toList();\\n }\\n}\\n\\n// 性能测试(百万文档):\\n// 索引构建时间:2.8s 查询响应时间:<10ms\\n
\\nclass Router {\\n static final _routeMap = LinkedHashMap<String, WidgetBuilder>(\\n equals: _routeEquals, // 自定义路径匹配\\n hashCode: _routeHash // 哈希优化\\n );\\n \\n static void register(String path, WidgetBuilder builder) {\\n _routeMap[_normalizePath(path)] = builder;\\n }\\n \\n static Widget build(String url) {\\n final route = _findBestMatch(url);\\n return _routeMap[route]!(context);\\n }\\n \\n // 支持参数化路由:/user/:id\\n static String _normalizePath(String path) => ... \\n}\\n
\\nMap的量子编程启示
Map
的设计展现了计算机科学中矛盾统一的哲学:
这种设计哲学不仅造就了高效的键值存储系统,更为开发者提供了应对复杂性的思维模型 —— 通过合理的哈希设计将混沌数据转化为有序结构,利用自动扩容机制平衡资源消耗。
\\n在Flutter
开发中,Map
不仅是数据容器
,更是状态管理
、路由配置
、缓存系统
的基石。随着Dart
语言的演进,如Records
类型的引入,Map
将与模式匹配
等新特性深度融合,开启更强大的数据建模能力。掌握Map
的量子本质,将使你在面对性能优化、架构设计等挑战时,能够像量子计算机般并行思考,找到最优解。
\\n","description":"前言 Map —— 键值对的时空交响曲。\\n\\n在Dart的编程宇宙中,Map如同精密的时空转换器,用键(Key)与值(Value)的量子纠缠,构建出高效的数据存取网络。从用户配置缓存到路由参数传递,从API响应解析到状态管理枢纽,Map以O(1)的魔法时间复杂度,在移动端开发中扮演着数据高速公路的角色。\\n\\n但在这优雅的API背后,隐藏着哈希碰撞的量子风暴、负载因子的平衡艺术、红黑树的自我修复魔法。理解Map的实现本质,不仅关乎数据结构的选用智慧,更是打开高性能Flutter开发之门的密钥。\\n\\n本文将带你穿透表面语法,直击Map的量子核心,揭示其如何通过哈希算法、…","guid":"https://juejin.cn/post/7471979172115595300","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-17T03:01:11.965Z","media":null,"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"系统化掌握Dart编程之异步编程(五):Isolate的\\"平行宇宙\\"哲学","url":"https://juejin.cn/post/7471979172115578916","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
Isolate
—— Dart
的“平行宇宙”
哲学。
在单线程的Dart
世界中,事件循环模型如同精密的传送带,高效处理着I/O
任务。但当遇到图像处理
、复杂计算
等CPU
密集型任务时,这条“传送带”
就会陷入拥堵。此时,Isolate
犹如平行宇宙般的存在,允许开发者创建多个独立运行的计算空间,每个空间拥有自己的内存堆
、事件循环
和垃圾回收机制
。
这种设计既避免了传统多线程的共享内存陷阱
,又实现了真正的并发计算
。理解Isolate
的底层逻辑,不仅是掌握Dart
高性能编程的关键,更是窥探
现代语言设计如何在安全与效率之间找到平衡点的绝佳窗口。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\nIsolate的本质与特性
Isolate
?Isolate
拥有独立的堆内存(Heap
)、栈内存(Stack
)和事件循环(Event Loop
)。Race Condition
)。2MB
内存(iOS
实测数据),适合移动端场景。与传统线程的差异:
\\n// 对比Java线程共享内存\\nclass Counter {\\n int value = 0; // 多线程访问需加锁\\n}\\n\\n// Dart Isolate通过消息传递\\nisolate.sendPort.send({\'action\': \'increment\'});\\n
\\nAPI
全景解析// 基础启动方式\\nIsolate.spawn(_entryPoint, sendPort);\\n\\n// 完整参数控制\\nfinal isolate = await Isolate.spawn<SendPort>(\\n _entryPoint,\\n sendPort,\\n debugName: \'ImageProcessor\', // 调试标识\\n errorsAreFatal: false, // 错误处理策略\\n onExit: _handleExit, // 退出回调\\n onError: _handleError // 错误回调\\n);\\n
\\n关键参数深度解读:
\\npauseOnStart
:用于调试的暂停机制。packageConfig
:隔离环境的依赖包配置。errorsAreFatal
:错误传播链设计原理。通信模式与架构设计
// 主Isolate\\nfinal receivePort = ReceivePort();\\nisolate.sendPort = receivePort.sendPort;\\n\\n// 子Isolate\\nvoid _entryPoint(SendPort mainSendPort) {\\n final childReceivePort = ReceivePort();\\n mainSendPort.send(childReceivePort.sendPort);\\n \\n childReceivePort.listen((message) {\\n // 处理逻辑\\n });\\n}\\n
\\n通信协议设计技巧:
\\nMap
封装type
和data
。Completer
实现请求-响应模式。Isolate
池架构设计class IsolatePool {\\n final List<Isolate> _workers = [];\\n final Queue<_Task> _taskQueue = [];\\n \\n Future<T> execute<T>(ComputeCallback<T> task) async {\\n final completer = Completer<T>();\\n _taskQueue.add(_Task(task, completer));\\n _scheduleTask();\\n return completer.future;\\n }\\n \\n void _scheduleTask() {\\n if (_workers.isNotEmpty) {\\n final worker = _workers.removeLast();\\n final task = _taskQueue.removeFirst();\\n worker.send(task);\\n }\\n }\\n}\\n
\\n负载均衡策略:
\\nWork Stealing
)。Isolate
的CPU
使用率)。Heap-based Priority Queue
)。突破通信瓶颈
数据类型 | 传统方式 | Transferable | 优化原理 |
---|---|---|---|
100MB 图片字节流 | 120ms | 15ms | 零拷贝内存转移 |
结构化JSON 数据 | 45ms | 38ms | 序列化算法优化 |
二进制协议 | 28ms | 5ms | FlatBuffers编码 |
优化手段:
\\n// 使用TransferableTypedData\\nfinal transferable = TransferableTypedData.fromList([data.buffer]);\\nsendPort.send(transferable);\\n
\\nvm_service
监听内存压力。Dart VM的并发魔法
Isolate
启动流程Dart VM
执行链:
Dart_IsolateCreate\\n → Isolate::Init\\n → Thread::EnterIsolate\\n → Dart_RunLoop\\n → MessageHandler::Run\\n
\\n关键数据结构:
\\nclass Isolate {\\n private:\\n Thread* mutator_thread_; // 执行线程\\n Heap* heap_; // 独立堆内存\\n MessageHandler* handler_; // 消息处理器\\n // ...\\n};\\n
\\n序列化过程:
\\nMessageSerializer
进行对象图遍历。C++
层:通过Dart_Post
写入共享内存区。MessageDeserializer
重建对象树。零拷贝实现:
\\nvoid Message::AddObject(RawObject* raw_obj) {\\n if (raw_obj->IsTypedData()) {\\n AddTypedData(TypedData::Cast(raw_obj)); // 直接传递内存指针\\n }\\n}\\n
\\n安全与效率的平衡术
Android
的JNI Crash
)。JavaScript Worker
模型对齐。Actor
模型的取舍优势:
\\n代价:
\\n与Go Channel
的对比:
// Go共享内存通信\\nch := make(chan int)\\ngo func() { ch <- 42 }()\\n\\n// Dart消息传递\\nsendPort.send(42); \\n
\\n复杂场景下的最佳实践
// 使用Isolate链式处理\\nfinal downloadIsolate = await _spawnDownloader();\\nfinal processIsolate = await _spawnProcessor();\\nfinal uploadIsolate = await _spawnUploader();\\n\\ndownloadIsolate.pipe(processIsolate).pipe(uploadIsolate);\\n
\\n性能数据:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n方案 | 1080P图片处理耗时 | 内存峰值 |
---|---|---|
主Isolate | 3200ms | 480MB |
单Isolate | 2100ms | 210MB |
Isolate 管道 | 1500ms | 180MB |
问题1:消息循环未关闭导致内存泄漏
// 正确关闭姿势\\nvoid _entryPoint(SendPort port) {\\n final receivePort = ReceivePort();\\n receivePort.listen((_) {}, cancelOnError: true); // 必须设置取消监听\\n \\n // ...\\n receivePort.close(); // 显式关闭\\n}\\n
\\n问题2:大消息阻塞事件循环
解决方案:
\\nChunking
)Stream API
)Isolate
的未来与开发者之道Isolate
的设计体现了Dart
团队对移动端开发痛点的深刻洞察:在保证内存安全的前提下,通过精巧的架构设计实现高效并发
。其价值不仅在于技术实现,更在于启发开发者建立边界思维 —— 通过合理的任务划分与通信设计
,将复杂问题分解到独立的计算单元中。
随着Dart FFI
(外部函数接口)的成熟,Isolate
与原生代码的协作能力将持续增强,在图像处理
、科学计算
等领域的应用将更加广泛。理解Isolate
的底层逻辑,将使我们在面对Flutter
性能优化时,拥有更高维度的系统化思考能力。
\\n","description":"前言 Isolate —— Dart的“平行宇宙”哲学。\\n\\n在单线程的Dart世界中,事件循环模型如同精密的传送带,高效处理着I/O任务。但当遇到图像处理、复杂计算等CPU密集型任务时,这条“传送带”就会陷入拥堵。此时,Isolate犹如平行宇宙般的存在,允许开发者创建多个独立运行的计算空间,每个空间拥有自己的内存堆、事件循环和垃圾回收机制。\\n\\n这种设计既避免了传统多线程的共享内存陷阱,又实现了真正的并发计算。理解Isolate的底层逻辑,不仅是掌握Dart高性能编程的关键,更是窥探现代语言设计如何在安全与效率之间找到平衡点的绝佳窗口。\\n\\n操千曲而后晓声,观…","guid":"https://juejin.cn/post/7471979172115578916","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-17T02:58:54.705Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b957860350f14f7990189ef0531e4599~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1740365934&x-signature=BsY7i7Zi4GWbu0Y%2BJoVeOPg6OEg%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"深入理解面向对象之抽象类以及混入","url":"https://juejin.cn/post/7472012159179096083","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
探索抽象类与混入的力量
\\n抽象类作为定义共享属性和行为的蓝图,不提供具体的实现细节,而是为子类设定了一个明确的契约。这种设计不仅增强了系统的可维护性,还极大地提升了代码的复用性和一致性。与此同时,混入(mixin)作为 Dart 语言特有的轻量级多重继承形式,使得多个类的功能可以无缝组合到一个新类中,无需构建复杂的继承层次结构。通过混入,代码片段得以轻松重用,功能模块的自由组合构建了高度灵活且可扩展的系统。
\\n抽象类,一个无法被实例化的类,主要作为其他类的模板或契约存在。
\\n特点:
\\n使用 abstract
关键字定义抽象类:
// 定义一个抽象类 Animal\\nabstract class Animal {\\n String name; \\n// 构造方法\\n Animal(this.name);\\n \\n// 具体实现方法:获取动物的名称\\n String getName() {\\n return \\"My name is $name.\\";\\n }\\n \\n // 抽象方法:定义如何发出声音,子类需要实现这个方法\\n void makeSound();\\n}\\n
\\n说明:Animal
抽象类定义了所有动物共有的行为(如 getName()
),以及每个具体动物必须实现的行为(如 makesound()
)。
通过继承并实现抽象类中的抽象方法来创建具体子类:
\\n// 定义一个具体类 Cat,继承自抽象类 Animal\\nclass Cat extends Animal {\\n Cat(String name) : super(name);\\n \\n // 实现抽象方法:定义 Cat 如何发出声音\\n @override\\n void makeSound() {\\n print(\\"$name says: Meow!\\");\\n }\\n}\\n \\nvoid main() {\\n // 创建 Cat 实例\\n Animal cat = Cat(\\"Whiskers\\");\\n print(cat.getName()); // 输出: My name is Whiskers.\\n cat.makeSound(); // 输出: Whiskers says: Meow!\\n}\\n}\\n
\\n说明:Animal
抽象类定义了一个具体方法 getName()
和一个抽象方法 makeSound()
。Cat
类继承了 Animal
,实现了 makeSound()
方法,并直接使用了 getName()
方法。
多继承的二义性,即菱形问题,是面向对象编程中多继承语言可能面临的困境。当一个类从多个基类派生,而这些基类又共同继承自同一个祖先类时,方法或属性的重复继承可能导致编译器无法确定调用哪个版本,从而产生二义性。
\\n菱形继承结构:
\\n在这种结构下,一个子类同时继承两个父类,这两个父类又刚好继承同一个祖先类,当子类想实现祖先类中的某个方法时,不知道是继承父类1还是父类2中的方法。这就导致歧义。
\\n解决方法:
\\nMixin 是 Dart 提供的一种轻量级多重继承形式,允许将多个类的功能组合到一个新类中,无需复杂的继承结构。
\\n特点:
\\n使用 mixin
关键字定义 Mixin:
// 定义一个 mixin,用于添加行走功能\\nmixin Walkable {\\n void walk() {\\n print(\'Walking...\');\\n }\\n}\\n \\n// 定义一个 mixin,用于添加说话功能\\nmixin Talkable {\\n void talk() {\\n print(\'Hello, I can talk!\');\\n }\\n}\\n \\n// 使用 Mixin 的类,表示一个具有行走和说话能力的角色\\nclass people with Walkable, Talkable {\\n void introduce() {\\n print(\'Hi, I am people in this story.\');\\n }\\n}\\n \\nvoid main() {\\n // 创建 Character 实例\\n Character character = Character();\\n character.introduce(); // 输出: Hi, I am a character in this story.\\n character.walk(); // 输出: Walking...\\n character.talk(); // 输出: Hello, I can talk!\\n}\\n
\\n说明:可以看到 mixin
如何被用来为类添加额外的行为,而无需通过多重继承(Dart 不支持多重继承,但支持 mixin
)来复制代码。每个 mixin
都封装了一组相关的方法,这些方法可以在多个类之间共享。
on
关键字为 Mixin 指定约束条件,确保只有满足条件的类才能使用。Dart 中的抽象类和 Mixin 各具特色,共同助力开发者构建健壮、灵活且易于维护的系统。抽象类通过定义公共接口和提供默认实现,确保代码的一致性和可预测性;而 Mixin 通过灵活组合功能,提升代码的复用性和扩展性。理解并合理运用这两个特性,将使应用程序更加模块化、易于维护及扩展。
","description":"前言 学习之路需深耕细作,切勿轻视任一知识点,因其存在必蕴含深意\\n\\n探索抽象类与混入的力量\\n\\n抽象类作为定义共享属性和行为的蓝图,不提供具体的实现细节,而是为子类设定了一个明确的契约。这种设计不仅增强了系统的可维护性,还极大地提升了代码的复用性和一致性。与此同时,混入(mixin)作为 Dart 语言特有的轻量级多重继承形式,使得多个类的功能可以无缝组合到一个新类中,无需构建复杂的继承层次结构。通过混入,代码片段得以轻松重用,功能模块的自由组合构建了高度灵活且可扩展的系统。\\n\\n一、抽象类\\n1.1 基本概念\\n\\n抽象类,一个无法被实例化的类…","guid":"https://juejin.cn/post/7472012159179096083","author":"科昂","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-17T02:50:10.254Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/810baf2cc7df4aff8946c46d1dc8555b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg56eR5piC:q75.awebp?rk3s=f64ab15b&x-expires=1740365409&x-signature=gWmQtVDgvZw2WRfzhg2%2BEiEz2f0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/cdca58882be04342bfea4bd40becc50e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg56eR5piC:q75.awebp?rk3s=f64ab15b&x-expires=1740365409&x-signature=Mev30Epu%2FCITCM18%2F6xPZ%2FBczg4%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Dart","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 正在推进全新 PlatformView 实现 HCPP, 它又用到了 Android 上的什么黑科技","url":"https://juejin.cn/post/7471979172115152932","content":"跨平台开发里的 PlatformView 实现一直是一个经久不衰的话题,在之前的 《深入 Flutter 和 Compose 的 PlatformView 实现对比》 我们就详细聊过 Flutter 和 Compose 在 PlatformView 实现上的异同之处,也聊到了 Compose 为什么在相同实现上对比 Flutter 会更有优势的原因。
\\n那么随着 3.29 的发布,恰好关注到其实 Flutter 在 Android 的 PlatformView 上其实正在落地另外一种实现,而这种实现目前看来可以做到在 HC 的基础上得到更好的性能,所以也被暂时称为 HCPP。
\\n在聊 HCPP 之前我们再简单回顾下 Flutter 在 Android 上的 PlatformView 实现模式:
\\nVirtualDisplay
的相关支持在内存实现原生控件的模拟绘制和纹理提取FlutterView
上,然后通过新的 FlutterImageView
提供新的 Surface 来实现控件 UI 堆叠合成FlutterView
上,但是中间利用通过 parent 替代掉 child 的 canvas,让原生控件绘制的对应的 surface.lockHardwareCanvas
上\\n\\n有点抽象?,没关系,后面有简单的直观例子。
\\n
在这个过程中几种模式各有优劣,比如:
\\nSurfaceView
等控件,因为 SurfaceView
有自己独立的 Surface 和 Canva ,它的 Surface 直接来自 SurfaceFlinger
,也就是当前 Window 下invalidate
才能保证正确渲染,例如 textureView
的 onSurfaceTextureUpdated
调用 mapView.invalidate
所以目前这三种模式是协同工作,例如:
\\ninitAndroidView
: 默认会使用最新的模式,目前就是 TLHC,如果遇到不支持的就降级到 VDinitSurfaceAndroidView
: 默认会使用最新的模,目前就是 TLHC,如果遇到不支持的就降级到 HCinitExpensiveAndroidView
:直接强制使用 HC 模式那么,回到本次的主题 ,针对全新的 HCPP 实现,Flutter 提供了全新的 API initHybridAndroidView
,可以看到,它需要 Vulkan 和 API 34 的环境才支持使用,如果从这点看,它的通用性又相对较低:
那为什么它需要 API 34 呢?这和它直接使用 SurfaceControl
的 API 逻辑有很大关系,另外,从 Engine 的判断逻辑上可以看到,目前除了判断 Vulkan 和 API 之后,还需要配置对应的 EnableSurfaceControl
才可以测试 HCPP,也就是在 AndroidManifest
增加:
<meta-data\\n android:name=\\"io.flutter.embedding.android.EnableSurfaceControl\\"\\n android:value=\\"true\\" />\\n
\\n接着就让我们来看看 HCPP 和其他几种模式有什么区别,其实主要就是和 HC 和 TLHC 进行比较,这里首先做一个容器 Demo ,主要是通过混合 Flutter 和原生控件的效果来区分它们的实现,让 platformView
渲染在两个 Flutter Widget 之间:
return MaterialApp(\\n debugShowCheckedModeBanner: false,\\n home: Stack(\\n alignment: AlignmentDirectional.center,\\n children: <Widget>[\\n ///200x200的绿色 Flutter 方块\\n TextButton(\\n key: const ValueKey<String>(\'AddOverlay\'),\\n onPressed: _togglePlatformView,\\n child: const SizedBox(width: 190, height: 190, child: ColoredBox(color: Colors.green)),\\n ),\\n \\n ///200x200的原生控件,这里用的是一个红色的原生方块\\n if (showPlatformView) ...<Widget>[\\n SizedBox(width: 200, height: 200, child: widget.platformView),\\n \\n \\n ///黄色 Flutter 条\\n TextButton(\\n key: const ValueKey<String>(\'RemoveOverlay\'),\\n onPressed: _togglePlatformView,\\n child: const SizedBox(\\n width: 800,\\n height: 25,\\n child: ColoredBox(color: Colors.yellow),\\n ),\\n ),\\n ],\\n ],\\n ),\\n);\\n
\\n之后我们可以通过 initExpensiveAndroidView
强制 PlatformView 使用 HC 模式,可以看到,在 HC 模式下出现很多经典的原生层,特别是多了 FlutterImageView
的转换还有它的子类 PlatformOverlayView
:
我们通过 3D 图可以看到,红色的原生 BoxPlatformView
正常被渲染,然后在其之上的 Flutter 控件(一部分黄色条),是通过 FlutterImageView
的子类 PlatformOverlayView
提供的 Surface 独立渲染:
然后我们再通过 initAndroidView
来使用 TLHC 模式,可以看到此时是通过 PlatformViewWrapper
这个 parent 作为容器来承载,而 PlatformViewWrapper
会替换掉原生 BoxPlatformView
的 Canvas,让原生控件的内容渲染到指定 Surface 上 :
我们通过原生 3D 图可以看到,此时的 BoxPlatformView
其实在原生层并没有绘制任何东西,因为其 Canvas 是被替换到内存的 SurfaceTexture
上:
说到 SurfaceTexture
,这个插个题外话,对于 THLC 和 VD 而言,现在创建纹理时是会根据 Android API 来使用不同实现,其中 SurfaceProducer
比较特殊:
因为在此之前,Android 上的 Flutter 引擎支持两个外部渲染源:SurfaceTexture (OpenGLES 纹理)和 ImageReader(GPU-ready buffer),其中 Image.getHardwareBuffer
需要 API 28 支持。
而为了适配 Impeller 团队提出了 SurfaceProducer
概念,让 Android 在运行时选择“最佳”渲染 Surface,除了 PlatformView 场景,在外界纹理场景也需要适配的情况:
- TextureRegistry.SurfaceTextureEntry entry = textureRegistry.createSurfaceTexture();\\n+ TextureRegistry.SurfaceProducer producer = textureRegistry.createSurfaceProducer();\\n\\n- Surface surface = new Surface(entry.surfaceTexture());\\n+ Surface surface = producer.getSurface();\\n
\\n那么我们看 HCPP,通过 initHybridAndroidView
我们启用了 HCPP 模式,可以看到,此时 UI 的层级结构类似 TLHC, 但是 parent 使用的是 HC 模式中的 FlutterMutatorView
:
然后我们看 3D 效果,原生控件 BoxPlatformView
其实可以被完整被渲染,证明其 Canvas 并没有被替代,那么这里就有一个神奇的问题了:Flutter 的黄色控件,是如何渲染到红色的 BoxPlatformView
之上的?
这就不得不提 PlatformViewsController2
,作为一个 HCPP 的临时对象,它的实现里有一个关键的对象 SurfaceControl
,并且在事务提交时通过 setLayer
设置了 z 轴为 1000
:
我们可以看提交更改里,基本上全新的 PlatformViewsController2
核心逻辑都在于操作 SurfaceControl
:
在 Android 里,SurfaceControl
是一种用于管理和操作与显示系统相关的图形资源的类,简单说就是与 Surface
相关的操作,SurfaceControl
可以用于创建和管理 Surface
,它是和SurfaceFlinger
交互的一个主要接口,交互的方式则是通过 Transaction
。
而在 HCPP 里,我们可以看到,此时的 Surface
正是通过一个全新的 SurfaceControl
创建得到,而这个 SurfaceControl
的 Transaction
来自 FlutterView
:
也就是,在 HCPP 模式里,Flutter 通过 SurfaceControl.Transaction
构造了一个全新的 Surface
用于 SurfaceFlinger
合成,并且还通过 setLayer
将 Surface 的 z 轴设置到了 1000 ,而这个 1000 就是黄色 Flutter 控件可以渲染到原生红色方块之上的原因。
举个例子,我们将 SurfaceControl
这部分代码复制到一个简单的纯原生项目里,并且同样对创建的 Surface
设置 1000 和绘制红色:
protected void onCreate(Bundle savedInstanceState) {\\n super.onCreate(savedInstanceState);\\n \\n ········\\n \\n // 获取 FrameLayout\\n FrameLayout rootView = findViewById(R.id.container);\\n rootView.postDelayed(new Runnable() {\\n @Override\\n public void run() {\\n final SurfaceControl.Builder surfaceControlBuilder = new SurfaceControl.Builder();\\n surfaceControlBuilder.setBufferSize(500, 500);\\n surfaceControlBuilder.setFormat(PixelFormat.RGBA_8888);\\n surfaceControlBuilder.setName(\\"Flutter Overlay Surface\\");\\n surfaceControlBuilder.setOpaque(false);\\n surfaceControlBuilder.setHidden(false);\\n final SurfaceControl surfaceControl = surfaceControlBuilder.build();\\n final SurfaceControl.Transaction tx =\\n binding.container.getRootSurfaceControl().buildReparentTransaction(surfaceControl);\\n tx.setLayer(surfaceControl, 1000);\\n tx.apply();\\n surface1 = new Surface(surfaceControl);\\n surfaceControl1 = surfaceControl;\\n\\n // 在 SurfaceView 上绘制一些内容\\n drawOnSurface(surface1, Color.RED); \\n }\\n }, 2000);\\n\\n}\\n\\nprivate void drawOnSurface(Surface surface, int color) {\\n Canvas canvas = surface.lockCanvas(null);\\n if (canvas != null) {\\n canvas.drawColor(color);\\n surface.unlockCanvasAndPost(canvas);\\n }\\n}\\n
\\n然后我们看最终绘制的效果,可以看到绿色背景的 FrameLayout
是在 WebView
下方的,但是通过 container.getRootSurfaceControl()
创建的 Surface
因为 z 轴为 1000 的原因,最终红色方块会绘制到 WebView
之上:
另外,在目前逻辑中,Engine 如果判断当前帧如果不存在 PlatformView ,并且上一帧存在 PlatformView,那么就会调用 hideOverlaySurface2
从而直接触发 Java 层面的platformViewsController2.hideOverlaySurface()
,进而隐藏不需要的 Layer :
if (!FrameHasPlatformLayers()) {\\n frame->Submit();\\n // If the previous frame had platform views, hide the overlay surface.\\n if (previous_frame_view_count_ > 0) {\\n jni_facade_->hideOverlaySurface2();\\n }\\n jni_facade_->applyTransaction();\\n return;\\n }\\n
\\n所以可以看到,HCPP 主要就是通过 SurfaceControl
来构造一个高层级的 Surface
从而实现最终绘制时混合覆盖的问题,这和我们之前聊 《深入 Flutter 和 Compose 的 PlatformView 实现对比》 里 Compose 可以在 PlatformView 里直接使用 SurfaceView
的道理类似,都是 SurfaceFlinger
合成时的层级操作。
至于为什么需要 API 34, 主要也是 SurfaceControl 对应的一些 API 需要的版本都很高,另外我依稀记得, Android 14 在通过 SurfaceControl 实现低延迟绘图时,可以更好支持 Canvas API 通过硬件加速绘制到 HardwareBuffer :
\\n如果对于 Engine 部份逻辑感兴趣的,也可以看 external_view_embedder_2#SubmitFlutterView
这部分逻辑里如何通过 GetLayer 去创建 FlutterOverlaySurface
。
目前 HCPP 还处于 main 分之的 beta 状态,如果后续正式落地,那对于 Android PlatformView 实现将会是存在 4 种组合模式,相比较 iOS 端多个 CALayer 的合成模式,Android 的 PlatformView 可以说是一路坎坷至今。
\\n最后,你觉得 HCPP 会成为落地为全新的 PlatformView 支持吗?
\\n\\n\\nPS :
\\nio.flutter.embedding.android.EnableSurfaceControl
标识还用于控制 Impeller 内部使用 Vulkan swapchain 或者 Android SurfaceControl (AHB swapchain),在 Android SurfaceControl 模式下,Java 端创建的 Transaction 会链接到 AHB swapchain。当然, AHBSwapchainVK 交换链实现并非在所有 Android 版本上都可用,一般不支持的话,会回退到 KHR swapchain。
\\n
2025 让我最震惊的消息之一就是:Google 将取消 Kotlin GDE,也就是 2025 年开始不再有 Kotlin 类目的 GDE :
\\n从目前消息看,Kotlin 类目开发者大概率回归 JetBrains ,JetBrains 应该会在2025 年初开始将 Kotlin 纳入 JetBrains 社区贡献。
\\n\\n\\n所以 Google 和 JetBrains 在亲密合作的同时,也开始亲兄弟明算帐?
\\n
而近日,JetBrains 也宣布停止了之前 2025 年的 Roadmap 之一:为 KMP 定制独立的 IDE,这个话题在去年的 《Kotlin Multiplatform 未来将采用基于 JetBrains Fleet 定制的独立 IDE》 我们详细聊过。
\\n而从 JetBrains 的声明可以看出,由于大多数用户更希望在原有在 IntelliJ 平台上提供更好的 KMP 支持,所以在未来三个月内,JetBrains将弃用 Fleet 中对 KMP 的支持,并且不再发布 KMP 的独立 IDE。
\\n\\n\\n所以 Fleet 又一次凉了?
\\n
其实一直以来,我们可以看到 JetBrains 的开源和框架发展基本都是围绕自家的 IDE 产品展开,而 JetBrains,目前已经在 Kotlin 之上构建了一个完整的技术生态系统,包括 KMP、Ktor、Exposed、Amper 等许多项目,再加上社区创建的大量库和框架,JetBrains 认为这是一个新的机会,他们也许可以通过可以利用 AI 来进一步提供开发者的体验,例如 Junie 。
\\n最后,就像之前聊过的 《Jetpack Compose 和 Compose Multiplatform 还有 KMP 的关系》 一样,Kotlin 生态乃至 KMP 和 CMP ,它们都会更加回归到 JetBrains 的怀抱里,从目前看它们的未来在于 JetBrains。
\\n\\n\\n但是在 Android Studio 上实现类似 Fleet 一样的 Kotlin - Swfit 无缝跳转和 debug 大概会是一件比较有挑战的事情,还有 CMP 预览等。
\\n
而对于 Kotlin ,也许 Google 是觉得“功成身退”了,可以放心回归 JetBrains 的怀抱?
\\nFlutter 主要使用 Dart 语言,也就是说我们只要掌握了 Dart 语言调用 C/C++ 的方法,就知道了如何在 Flutter 中调用 C/C++ 编写的功能了。
\\nDart 的移动端、命令行和服务端应用所运行的 Dart 原生平台,均可以使用 dart:ffi 库调用原生的 C 语言 API,用于读、写、分配和销毁原生内存。 FFI(Foreign Function Interface)指的是外部函数接口。
\\n用于与 C 编程语言互操作的外部函数接口。
\\nAbi 应用程序二进制接口 (ABI)。
\\nAbiSpecificInteger 所有 Abi-specific 整数类型的 supertype(超类型)。
\\nAbiSpecificIntegerMapping 映射 AbiSpecificInteger
的 subtype(子类型)。
Allocator 管理 native 堆上的内存。
\\nNativeType NativeType
的子类型表示 C 中的 native 类型。
Array<T extends NativeType> 固定大小的数组 T
。
Dart_CObject 不透明,不暴露其成员。
\\nDartRepresentationOf 表示与 NativeType
对应的 Dart 类型。
DefaultAsset 指定当前库的默认 asset ID 的注解。
\\nDynamicLibrary 动态加载的 native 库。
\\nFinalizable 标记不应过早销毁对象的接口。
\\nHandle 代表 Dart_Handle 来自 C 的 dart_api.h
。
Native<T> 将外部声明绑定到其 native 实现的注解。
\\nNativeApi 用于从 Dart 代码或通过 C 代码(使用 dart_api_dl.h
)访问 Dart VM API。
NativeCallable<T extends Function> 一个 native 可调用对象,用于监听对 native 函数的调用。
\\nNativeFinalizer 可以附着到 Dart 对象的 native finalizer。
\\nNativeFunction<T extends Function> 表示 C 中的函数类型。
\\nOpaque Opaque
的 subtype 表示 C 中的不透明类型。
Packed 用于在 Struct
subtype 上指定注解,以表明其成员需要进行打包。
Pointer<T extends NativeType> 表示指向 native C 内存的指针,无法扩展。
\\nSizedNativeType 具有已知大小的 NativeType
。
Struct 所有 FFI 结构体类型的 supertype。
\\nUnion 所有 FFI 联合体类型的 supertype。
\\nVarArgs<T extends Record> C 中传递的可变参数的类型。
\\nBool 表示 C 中的 native 布尔值。
\\nChar 表示 C 中的 native char
型。
Double 表示 C 中的 native 64 位双精度浮点数。
\\nFloat 表示 C 中的 native 32 位单精度浮点数。
\\nInt 表示 C 中的 native int
型。
Int8 表示 C 中的 native 有符号 8 位整数。
\\nInt16 表示 C 中的 native 有符号 16 位整数。
\\nInt32 表示 C 中的 native 有符号 32 位整数。
\\nInt64 表示 C 中的 native 有符号 64 位整数。
\\nUint8 表示 C 中的 native 无符号 8 位整数。
\\nUint16 表示 C 中的 native 无符号 16 位整数。
\\nUint32 表示 C 中的 native 无符号 32 位整数。
\\nUint64 表示 C 中的 native 无符号 64 位整数。
\\nUintPtr 表示 C 中的 native uintptr_t
型(指针)。
IntPtr 表示 C 中的 native intptr_t
型(指针)。
Long 表示 C 中的 native long int
型,又称 long
类型。
LongLong 表示 C 中的 native long long
类型。
Short 表示 C 中的 native short
型。
SignedChar 表示 C 中的 native signed char
型。
Size 表示 C 中的 native size_t
型。
Void 表示 C 中的 native void
类型。
WChar 表示 C 中的 native wchar_t
类型。
UnsignedChar 表示 C 中的 native unsigned char
类型。
UnsignedShort 表示 C 中的 native unsigned short
类型。
UnsignedInt 表示 C 中的 native unsigned int
类型。
UnsignedLong 表示 C 中的 native unsigned long int
类型,又称 unsigned long
类型。
UnsignedLongLong 表示 C 中的 native unsigned long long
类型。
AbiSpecificIntegerArray on Array<T> 对 AbiSpecificInteger
数组(Array
)的索引方法进行边界检查。
AbiSpecificIntegerPointer on Pointer<T> 针对类型参数 AbiSpecificInteger
的指针(Pointer
)扩展。
AllocatorAlloc on Allocator 扩展 Allocator
以提供 NativeType
的分配。
ArrayAddress on Array<T> 为 Array<T>
添加通过 address
property 获取底层数据的内存地址的扩展。
ArrayArray on Array<Array<T>> 对数组(Array
)的数组(Array
)进行索引方法的边界检查。
BoolAddress on bool 为 bool
添加通过 address
property 获取底层数据的内存地址的扩展。
BoolArray on Array<Bool> Bool
数组(Array
)的边界检查索引方法。
BoolPointer on Pointer<Bool> 针对类型参数 Bool
的指针(Pointer
)扩展。
DoubleAddress on double 为 double
添加通过 address
property 获取底层数据的内存地址的扩展。
DoubleArray on Array<Double> Double
数组(Array
)的边界检查索引方法。
DoublePointer on Pointer<Double> 针对类型参数 Double
的指针(Pointer
)扩展。
DynamicLibraryExtension on DynamicLibrary 不能动态调用的方法。
\\nFloat32ListAddress on Float32List 为 Float32List
添加通过 address
property 获取底层数据的内存地址的扩展。
Float64ListAddress on Float64List 为 Float64List
添加通过 address
property 获取底层数据的内存地址的扩展。
Int8ListAddress on Int8List 为 Int8List
添加通过 address
property 获取底层数据的内存地址的扩展。
Int16ListAddress on Int16List 为 Int16List
添加通过 address
property 获取底层数据的内存地址的扩展。
Int32ListAddress on Int32List 为 Int32List
添加通过 address
property 获取底层数据的内存地址的扩展。
Int64ListAddress on Int64List 为 Int64List
添加通过 address
property 获取底层数据的内存地址的扩展。
FloatArray on Array<Float> Float
数组(Array
)的边界检查索引方法。
FloatPointer on Pointer<Float> 针对类型参数 Float
的指针(Pointer
)扩展。
Int8Array on Array<Int8> Int8
数组(Array
)的边界检查索引方法。
Int16Array on Array<Int16> Int16
数组(Array
)的边界检查索引方法。
Int32Array on Array<Int32> Int32
数组(Array
)的边界检查索引方法。
Int32Array on Array<Int64> Int64
数组(Array
)的边界检查索引方法。
IntAddress on int 为 int
添加通过 address
property 获取底层数据的内存地址的扩展。
Int8Pointer on Pointer<Int8> 针对类型参数 Int8
的指针(Pointer
)扩展。
Int16Pointer on Pointer<Int16> 针对类型参数 Int16
的指针(Pointer
)扩展。
Int32Pointer on Pointer<Int32> 针对类型参数 Int32
的指针(Pointer
)扩展。
Int64Pointer on Pointer<Int64> 针对类型参数 Int64
的指针(Pointer
)扩展。
NativeFunctionPointer on Pointer<NativeFunction<NF>> 针对类型参数 NativeFunction
的指针(Pointer
)扩展。
NativePort on SendPort Dart_Port
从 SendPort
检索 native 的扩展。
PointerArray on Array<Pointer<T>> Pointer
数组(Array
)的边界检查索引方法。
PointerPointer on Pointer<Pointer<T>> 针对类型参数 Pointer
的指针(Pointer
)扩展。
Uint8Array on Array Uint8
数组(Array
)的边界检查索引方法。
Uint16Array on Array Uint16
数组(Array
)的边界检查索引方法。
Uint32Array on Array Uint32
数组(Array
)的边界检查索引方法。
Uint64Array on Array Uint64
数组(Array
)的边界检查索引方法。
Uint8Pointer on Pointer 针对类型参数 Int8
的指针(Pointer
)扩展。
Uint16Pointer on Pointer 针对类型参数 Int16
的指针(Pointer
)扩展。
Uint32Pointer on Pointer 针对类型参数 Int32
的指针(Pointer
)扩展。
Uint64Pointer on Pointer 针对类型参数 Int64
的指针(Pointer
)扩展。
Uint8ListAddress on Uint8List 为 Uint8List
添加通过 address
property 获取底层数据的内存地址的扩展。
Uint16ListAddress on Uint16List 为 Uint16List
添加通过 address
property 获取底层数据的内存地址的扩展。
Uint32ListAddress on Uint32List 为 Uint32List
添加通过 address
property 获取底层数据的内存地址的扩展。
Uint64ListAddress on Uint64List 为 Uint64List
添加通过 address
property 获取底层数据的内存地址的扩展。
StructAddress on T 为 T(T extends Struct)
添加通过 address
property 获取底层数据的内存地址的扩展。
StructArray on Array<T> Struct
数组(Array
)的边界检查索引方法。
StructPointer on Pointer<T> 针对类型参数 Struct
的指针(Pointer
)扩展。
UnionAddress on T T(T extends Union)
添加通过 address
property 获取底层数据的内存地址的扩展。
UnionArray on Array Union
联合体(Array
)的边界检查索引方法。
UnionPointer on Pointer 针对类型参数 Union
的指针(Pointer
)扩展。
nullptr → Pointer 表示指向 C 语言 native 内存中对应于“NULL
”的指针,例如地址为 0 的指针。
sizeOf() → int 由 native 类型 T
占用的字节数。
Dart_NativeMessageHandler = Void Function(Int64, Pointer<Dart_CObject>) Dart 和 native 之间通信的一个关键组件,主要用于处理 Dart 和 native 之间的消息传递。\\nNativeFinalizerFunction = NativeFunction<Void Function(Pointer token)> NativeFinalizer 的 native 函数类型。
\\n接下来先通过一个 Hello World 开始学习 FFI 如何使用,接下来会继续介绍函数入参中使用指针、函数返回值返回指针、结构体和回调函数四种使用场景。
\\ndart:ffi
import \'package:ffi/ffi.dart\';\\n
\\nFFI
类型签名定义一个 typedef
。typedef HelloWorldFunc = Void Function();\\n
\\ntypedef
。typedef HelloWorldDart = void Function();\\n
\\n // 打开动态库\\n final DynamicLibrary nativeLib = Platform.isAndroid\\n ? DynamicLibrary.open(\\"libnativelib.so\\")\\n : DynamicLibrary.process();\\n
\\ntypedef
,以及步骤 4 定义的动态库变量。 // 查找 C 函数 hello_world\\n final HelloWorldDart hello = nativeLib\\n .lookup<NativeFunction<HelloWorldFunc>>(\'hello_world\')\\n .asFunction();\\n
\\n对应的 libnativelib.so 内 native 实现如下:
\\n#include <android/log.h>\\n\\n#define ATTRIBUTES extern \\"C\\" __attribute__((visibility(\\"default\\"))) __attribute__((used))\\n#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, \\"DemoNative\\", __VA_ARGS__)\\n\\nATTRIBUTES void hello_world() {\\n LOGD(\\"Hello world from native!!!\\");\\n}\\n
\\nhello_world
函数使用 LOGD
打印,实际上是使用了 android 平台的 native __android_log_print
函数。
关于 ATTRIBUTES 宏的说明
\\n这段 #define
预处理指令定义了一个名为 ATTRIBUTES
的宏,它展开后是 extern \\"C\\" __attribute__((visibility(\\"default\\"))) __attribute__((used))
。下面来详细解释一下这些部分的含义:
(1) extern \\"C\\"
:这是 C++ 语言中的声明,用于指定接下来的函数或变量使用 C 语言的链接规范。在 C++ 中,函数名在编译时会进行名称修饰(name mangling),以支持函数重载等特性。而 C 语言没有名称修饰的概念。使用 extern \\"C\\"
可以确保在 C++ 代码中调用 C 语言编写的函数,或者让 C 语言代码能够调用 C++ 中用 extern \\"C\\"
声明的函数。
(2) __attribute__((visibility(\\"default\\")))
:这是 GCC 编译器特有的属性(attribute),用于指定符号(函数、变量等)的可见性。visibility(\\"default\\")
表示该符号具有默认的可见性,即对整个程序可见。这在构建共享库时特别有用,可以控制哪些符号可以被外部代码访问。例如,在共享库中,某些函数可能不希望被外部直接调用,就可以通过设置不同的可见性属性来隐藏它们。
(3) __attribute__((used))
:同样是 GCC 编译器特有的属性,它告诉编译器即使某个符号看起来没有被使用(例如,某个函数没有在代码中被显式调用),也不要对其进行优化(例如,不要将其从目标文件中移除)。这在某些情况下很有用,比如当你需要确保某个函数或变量在目标文件中存在,即使它的调用是在运行时通过动态链接或其他间接方式实现的。
hello();\\n
\\n打印结果:
\\nD/DemoNative(12514): Hello world from native!!!\\n
\\n我们先来看 native 部分代码,简单的实现了加法和减法,native 函数入参都是普通 int
类型。
ATTRIBUTES int add_from_native(int a, int b) {\\n return a + b;\\n}\\n\\nATTRIBUTES int sub_from_native(int a, int b) {\\n return a - b;\\n}\\n
\\n现在回到 Flutter 的 Dart 代码中。首先为调用 C 函数的变量定义一个 NativeIntFunc
,接着为调用 C 函数的变量定义一个 DartIntFunc
。在 fromNative
函数中打开动态库,查找 C 函数 add_from_native
和 sub_from_native
,最后调用它们 dart 层的代表。
typedef NativeIntFunc = Int32 Function(Int32 a, Int32 b);\\ntypedef DartIntFunc = int Function(int a, int b);\\n\\nstatic void fromNative(int a, int b) {\\n // 打开动态库\\n final DynamicLibrary nativeLib = Platform.isAndroid\\n ? DynamicLibrary.open(\\"libnativelib.so\\")\\n : DynamicLibrary.process();\\n \\n // 查找 C 函数 add_from_native\\n final DartIntFunc add = nativeLib\\n .lookup<NativeFunction<NativeIntFunc>>(\\"add_from_native\\")\\n .asFunction();\\n\\n final DartIntFunc sub = nativeLib\\n .lookup<NativeFunction<NativeIntFunc>>(\\"sub_from_native\\")\\n .asFunction();\\n int result = add(a, b);\\n print(\'dart -> add_from_native result=$result\');\\n result = sub(a, b);\\n print(\'dart -> sub_from_native result=$result\');\\n}\\n
\\n打印结果:
\\nI/flutter (12514): dart -> add_from_native result=1005\\nI/flutter (12514): dart -> sub_from_native result=995\\n
\\nnative 函数第一个入参函数我们使用了指针。
\\nATTRIBUTES int div_from_native(int *a, int b) {\\n return (*a) / b;\\n}\\n
\\n唯一的主要区别在于在 dart 层创建一个指针,这调用了 calloc
,import
语句多了一条(import \'package:ffi/ffi.dart\'
),当然指针在使用完后还要释放(free
)。
import \'dart:ffi\';\\nimport \'package:ffi/ffi.dart\';\\n\\ntypedef DivFuncNative = Int32 Function(Pointer<Int32> a, Int32 b);\\ntypedef DivDart = int Function(Pointer<Int32> a, int b);\\n\\nstatic void fromNative(int a, int b) {\\n // 打开动态库\\n final DynamicLibrary nativeLib = Platform.isAndroid\\n ? DynamicLibrary.open(\\"libnativelib.so\\")\\n : DynamicLibrary.process();\\n \\n final div = nativeLib\\n .lookupFunction<DivFuncNative, DivDart>(\'div_from_native\');\\n\\n // 创建一个指针\\n final p = calloc<Int32>();\\n // 在地址中放置一个值\\n p.value = 20;\\n result = div(p, 5);\\n // 释放已分配的内存\\n calloc.free(p);\\n print(\'dart -> div_from_native = $result\');\\n}\\n
\\n打印结果:
\\nI/flutter (12514): dart -> div_from_native = 4\\n
\\nnative 函数的第一个入参我们使用了指针,并在函数体内调用了 malloc
分配内存,所以需要 dart 再次调用到 native 去释放这块内存。
ATTRIBUTES int *mul_from_native(int a, int b) {\\n // Allocates native memory in C.\\n int *mult = (int *) malloc(sizeof(int));\\n *mult = a * b;\\n return mult;\\n}\\n\\nATTRIBUTES void free_pointer(int *int_pointer) {\\n // Free native memory in C which was allocated in C.\\n free(int_pointer);\\n}\\n
\\n区别在于我们获取返回结果的方式,从一个 Pointer
中去取。另外我们还需要释放这块 native 申请的内存,通过调用 free_pointer
实现。
typedef MultiplyFuncNative = Pointer<Int32> Function(Int32 a, Int32 b);\\ntypedef MultiplyDart = Pointer<Int32> Function(int a, int b);\\n\\ntypedef FreePointerFuncNative = Void Function(Pointer<Int32> a);\\ntypedef FreePointerDart = void Function(Pointer<Int32> a);\\n\\nstatic void fromNative(int a, int b) {\\n // 打开动态库\\n final DynamicLibrary nativeLib = Platform.isAndroid\\n ? DynamicLibrary.open(\\"libnativelib.so\\")\\n : DynamicLibrary.process();\\n \\n final multiply = nativeLib\\n .lookupFunction<MultiplyFuncNative, MultiplyDart>(\'mul_from_native\');\\n final resultPointer = multiply(3, 5);\\n // 到指定地址获取结果\\n result = resultPointer.value;\\n print(\'dart -> mul_from_native = $result\');\\n\\n // 释放已分配的内存,因为内存是在C语言中分配的\\n final freePointerPointer =\\n nativeLib.lookup<NativeFunction<FreePointerFuncNative>>(\'free_pointer\');\\n final freePointer = freePointerPointer.asFunction<FreePointerDart>();\\n freePointer(resultPointer);\\n}\\n
\\n打印结果:
\\nI/flutter (12514): dart -> mul_from_native = 15\\n
\\nnative 函数创建了一个 Coordinate
结构体并返回。
typedef struct Coordinate {\\n double latitude;\\n double longitude;\\n} Coordinate;\\n\\nATTRIBUTES Coordinate create_coordinate(double latitude, double longitude) {\\n Coordinate coordinate;\\n coordinate.latitude = latitude;\\n coordinate.longitude = longitude;\\n return coordinate;\\n}\\n
\\ndart 层定义了一个对等的结构体 Coordinate
,这里值得注意的是这种实现方式无需在 dart 调用释放内存的操作。和 native 栈上分配是一致的,无需手动释放。
final class Coordinate extends Struct {\\n @Double()\\n external double latitude;\\n\\n @Double()\\n external double longitude;\\n}\\n\\ntypedef CreateCoordinateNative = Coordinate Function(\\n Double latitude, Double longitude);\\ntypedef CreateCoordinateDart = Coordinate Function(\\n double latitude, double longitude);\\n\\nstatic void fromNative() {\\n // 打开动态库\\n final DynamicLibrary nativeLib = Platform.isAndroid\\n ? DynamicLibrary.open(\\"libnativelib.so\\")\\n : DynamicLibrary.process();\\n \\n final createCoordinate =\\n nativeLib.lookupFunction<CreateCoordinateNative, CreateCoordinateDart>(\\n \'create_coordinate\');\\n final coordinate = createCoordinate(3.5, 4.6);\\n print(\'dart -> Coordinate is lat ${coordinate.latitude}, long ${coordinate.longitude}\');\\n}\\n
\\n打印结果:
\\nI/flutter (12514): dart -> Coordinate is lat 3.5, long 4.6\\n
\\nnative 创建了一个接收函数指针 Callback 作为入参的函数。
\\ntypedef int (*Callback)(int);\\n\\nATTRIBUTES int native_cb(Callback cb) {\\n int value = 100;\\n int result = cb(value);\\n LOGD(\\"Native function received result from callback: %d\\\\n\\", result);\\n return result;\\n}\\n
\\nint dartCallback(int value) {...}
:定义了一个 Dart 回调函数,接收一个整数并将其乘以 2 后返回。
int result = nativeFunction(Pointer.fromFunction<NativeCallback>(dartCallback, 0).address);
:将 Dart 回调函数转换为指针,并传递给 nativeCb
,这里的 0
作为异常返回值,当调用出现异常时会返回这个值。
typedef NativeCallback = Int32 Function(Int32);\\ntypedef DartCallback = int Function(int);\\n\\n// 定义 Dart 中的回调函数\\nint dartCallback(int value) {\\n print(\'Dart callback received value: $value\');\\n return value * 2;\\n}\\n\\nstatic void fromNative() {\\n // 打开动态库\\n final DynamicLibrary nativeLib = Platform.isAndroid\\n ? DynamicLibrary.open(\\"libnativelib.so\\")\\n : DynamicLibrary.process();\\n \\n final nativeCb = nativeLib\\n .lookupFunction<NativeCallback, DartCallback>(\'native_cb\');\\n // 调用动态库中的函数,并传递回调函数\\n int resultVal = nativeCb(Pointer.fromFunction<NativeCallback>(dartCallback, 0).address);\\n print(\'dart -> Result from native function: $resultVal\');\\n}\\n
\\n打印结果:
\\nI/flutter (15141): Dart callback received value: 100\\nD/DemoNative(15141): Native function received result from callback: 200\\nI/flutter (15141): dart -> Result from native function: 200\\n
\\nnative 函数的入参使用了 char 指针,指向了一个字符串,函数内部做了字符串反转的动作。
\\nATTRIBUTES char *reverse_native(char *str, int length)\\n{\\n // Allocates native memory in C.\\n char *reversed_str = (char *)malloc((length + 1) * sizeof(char));\\n for (int i = 0; i < length; i++)\\n {\\n reversed_str[length - i - 1] = str[i];\\n }\\n reversed_str[length] = \'\\\\0\';\\n return reversed_str;\\n}\\n\\nATTRIBUTES void free_string_native(char *str)\\n{\\n // Free native memory in C which was allocated in C.\\n free(str);\\n}\\n
\\n首先将 hello
字符串转化为 native Utf8
,接着传递给 reverse
,这里需要注意使用 calloc free
去释放 helloStrUtf8
。由于 reverse
返回的 Pointer<Utf8>
是个指针,需要 dart 层调用 toDartString
转换为 dart 的 String
。最后要控制从 dart 层调用去释放 native 的内存,和上一小结是类似的。
typedef ReverseFuncNative = Pointer<Utf8> Function(Pointer<Utf8> str, Int32 length);\\ntypedef ReverseDart = Pointer<Utf8> Function(Pointer<Utf8> str, int length);\\n\\ntypedef FreeStringFuncNative = Void Function(Pointer<Utf8> str);\\ntypedef FreeStringDart = void Function(Pointer<Utf8> str);\\n\\nstatic void fromNative() {\\n // 打开动态库\\n final DynamicLibrary nativeLib = Platform.isAndroid\\n ? DynamicLibrary.open(\\"libnativelib.so\\")\\n : DynamicLibrary.process();\\n \\n final reverse = nativeLib\\n .lookupFunction<ReverseFuncNative, ReverseDart>(\'reverse_native\');\\n final helloStr = \'hello\';\\n final helloStrUtf8 = helloStr.toNativeUtf8();\\n final reversedMessageUtf8 = reverse(helloStrUtf8, helloStr.length);\\n final reversedMessage = reversedMessageUtf8.toDartString();\\n\\n calloc.free(helloStrUtf8);\\n\\n print(\'dart -> $helloStr reversed is $reversedMessage\');\\n\\n final freeString =\\n nativeLib.lookupFunction<FreeStringFuncNative, FreeStringDart>(\\n \'free_string_native\');\\n freeString(reversedMessageUtf8);\\n}\\n
\\n打印结果:
\\nI/flutter (12514): dart -> hello reversed is olleh\\n
","description":"Flutter 主要使用 Dart 语言,也就是说我们只要掌握了 Dart 语言调用 C/C++ 的方法,就知道了如何在 Flutter 中调用 C/C++ 编写的功能了。 Dart 的移动端、命令行和服务端应用所运行的 Dart 原生平台,均可以使用 dart:ffi 库调用原生的 C 语言 API,用于读、写、分配和销毁原生内存。 FFI(Foreign Function Interface)指的是外部函数接口。\\n\\n一、dart:ffi\\n\\n用于与 C 编程语言互操作的外部函数接口。\\n\\n1.1 Class\\n\\nAbi 应用程序二进制接口 (ABI)。\\n\\nAbiSp…","guid":"https://juejin.cn/post/7471923435384881163","author":"天涯一角","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-16T23:57:24.099Z","media":null,"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"FlutterUnit 周边 | 组件数据国际化完成","url":"https://juejin.cn/post/7471873856875282451","content":"上一篇介绍了通过 Deepseek 完成 FlutterUnit 组件数据的 10 国语言翻译,本篇将解析这些数据,并且重新设计数据库,让 FlutterUnit 在应用层的数据支持多国语言。
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n英文列表 | 英文详情 |
---|---|
中文列表 | 中文详情 |
---|---|
之前的数据库没有考虑到需要进行数据的国际化,所以介绍信息也是直接作为表中的字段,拿 widget 表来说,承载了所有界面表现需要的字段,
\\n现在要将国际化的数据通过表记录,考虑到后续的可拓展性,比如还有添加其他的语言,或者增加其他需要国际化的字段。这里打算保证 widget 表数据的唯一性,增加 widget_desc
表记录组件国际化相关的数据,通过 widget_id
和 widget 表进行关联。如下是 Container 组件在数据库中的存储形式:
node 表记录组件介绍节点,一个组件对应多个 node 数据,节点的介绍信息也是同理,拆分出由 node_desc
表维护组件节点的介绍信息:
数据表定义完毕,接下来的目标收集上一篇章,Deepseek AI 接口生成的翻译文件。将他们整合在一起,录入数据库:
\\n对于数据的收集,这里定义了 3 个对象:
\\n其中一个 WidgetData 持有若干个国际化描述,以及国际化和组件节点的映射数据:
\\nclass WidgetData {\\n final int id;\\n final String name;\\n final List<WidgetDesc> desc;\\n final int family;\\n final double lever;\\n final int deprecated;\\n final String linkWidget;\\n final Map<String, List<NodeData>> nodes;\\n ...\\n}\\n\\nclass WidgetDesc {\\n final String locale;\\n final String name;\\n final String info;\\n ...\\n}\\n\\nclass NodeData {\\n final int id;\\n final int widgetId;\\n final String name;\\n final String desc;\\n final String locale;\\n final int priority;\\n final String filepath;\\n ...\\n}\\n
\\n定义 WidgetParser 解析类维护同步解析逻辑,在构造时传入组件的总目录:
\\nclass WidgetParser {\\n final String widgetPackage;\\n\\n WidgetParser(this.widgetPackage);\\n\\n Future<WidgetData> parserWidget(String widgetDir) async {\\n // TODO 解析组件文件夹,收录信息\\n }\\n \\n}\\n
\\n和上篇处理组件翻译类似,这里通过 parserWidget 解析指定路径的组件。其中的过程包括:
\\n_findDescFiles
: 寻找组件文件夹下的国际化描述数据文件。_parserNodes
: 解析描述中的节点数据,构造节点列表和语言的映射。Iterable<String> _findDescFiles(String dir){\\n Directory directory = Directory(dir);\\n List<FileSystemEntity> files = directory.listSync();\\n return files\\n .where((e) => (e is File) && p.basename(e.path).startsWith(\'desc_\'))\\n .map((e) => e.path);\\n}\\n\\nFuture<WidgetData> parserWidget(String widgetDir) async {\\n Iterable<String> descPathList = _findDescFiles(widgetDir);\\n int id = 0;\\n String name = \'\';\\n List<WidgetDesc> desc = [];\\n int family = 0;\\n double lever = 0;\\n int deprecated = 0;\\n String linkWidget = \'\';\\n Map<String, List<NodeData>> nodesMap = {};\\n \\n for (String descPath in descPathList) {\\n String filename = p.basenameWithoutExtension(descPath);\\n String locale = filename.replaceAll(\'desc_\', \'\');\\n locale = locale.replaceAll(\'_\', \'-\').toLowerCase();\\n dynamic descMap = json.decode(await File(descPath).readAsString());\\n descMap[\'locale\'] = locale;\\n if (filename.contains(\'zh-CN\')) {\\n id = descMap[\'id\'] ?? 0;\\n name = descMap[\'name\'] ?? \'\';\\n family = descMap[\'family\'] ?? 0;\\n lever = descMap[\'lever\'].toDouble() ?? 0;\\n deprecated = descMap[\'deprecated\'] ?? 0;\\n linkWidget = descMap[\\"linkIds\\"].join(\',\') ?? \'\';\\n }\\n desc.add(WidgetDesc.fromMap(descMap));\\n nodesMap[locale] = _parserNodes(widgetDir, descMap);\\n }\\n return WidgetData(\\n id: id,\\n name: name,\\n desc: desc,\\n family: family,\\n lever: lever,\\n linkWidget: linkWidget,\\n deprecated: deprecated,\\n nodes: nodesMap,\\n );\\n}\\n
\\n解析完毕后,得到的 WidgetData 对象,就持有了实现数据库国际化的所有数据。接下来的工作是如何把这个对象录入到数据库中。
\\nList<NodeData> _parserNodes(String directory, dynamic descMap) {\\n List<NodeData> nodes = [];\\n int priority = 0;\\n for (dynamic node in descMap[\'nodes\']) {\\n nodes.add(NodeData(\\n id: -1,\\n widgetId: descMap[\'id\'],\\n locale: descMap[\'locale\'],\\n name: node[\'name\'],\\n filepath: p.join(directory, node[\'file\']),\\n desc: node[\'desc\'].join(\'\\\\n\'),\\n priority: priority,\\n ));\\n priority++;\\n }\\n return nodes;\\n}\\n
\\n这里定义 WidgetSync 类来同步数据库内容,其中:
\\nWidgetParser
解析得到 WidgetData
对象insertWidget
方法负责将 WidgetData 中的数据录入数据库。class WidgetSync {\\n final String project;\\n\\n WidgetSync(this.project);\\n\\n late WidgetParser parser = WidgetParser(project);\\n \\n WidgetDao dao = WidgetDao();\\n \\n Future<void> saveWidget(String widgetDir) async {\\n WidgetData data = await parser.parserWidget(widgetDir);\\n await dao.insertWidget(data);\\n }\\n}\\n
\\n数据的录入主要就是通过 sql 语句,将对象记录的数据插入到 widget
、widget_desc
、node
、node_desc
四张表里。以供 app 实现组件信息的展示功能。
Future<void> insertWidget(WidgetData data) async {\\n final db = await FlutterDb.db.database;\\n String insertWidget = \\"\\"\\"\\nINSERT INTO \\n widget \\n (id,name,family,lever,deprecated,linkWidget) \\nVALUES \\n (?,?,?,?,?,?);\\n \\"\\"\\";\\n List<Object?> args = [data.id, data.name, data.family, data.lever, data.deprecated, data.linkWidget];\\n await db.rawInsert(insertWidget, args);\\n\\n String insertWidgetDesc = \\"\\"\\"\\nINSERT INTO \\n widget_desc \\n (widget_id,name,info,locale) \\nVALUES \\n (?,?,?,?);\\n \\"\\"\\";\\n\\n for (WidgetDesc desc in data.desc) {\\n List<Object?> args = [data.id, desc.name, desc.info, desc.locale];\\n await db.rawInsert(insertWidgetDesc, args);\\n }\\n await insertNodes(data.nodes);\\n }\\n
\\nWidgetData 是一个组件的全量数据,包含介绍的节点列表,所以插入过程中可以同时处理 node 表数据的插入:
\\n Future<int> insertNode(NodeData node) async {\\n int id = flake.id();\\n final db = await FlutterDb.db.database;\\n String insertWidget = \\"\\"\\"\\nINSERT INTO node \\n(id,widgetId,priority,code) \\nVALUES (?,?,?,?);\\n \\"\\"\\";\\n String code = await File(node.filepath).readAsString();\\n List<Object?> args = [id, node.widgetId, node.priority, code];\\n await db.rawInsert(insertWidget, args);\\n return id;\\n }\\n\\n Future<void> insertNodeDesc(int id, NodeData node) async {\\n final db = await FlutterDb.db.database;\\n String insertWidget = \\"\\"\\"\\nINSERT INTO node_desc \\n(node_id,name,subtitle,locale) \\nVALUES (?,?,?,?);\\n \\"\\"\\";\\n List<Object?> args = [id, node.name, node.desc, node.locale];\\n await db.rawInsert(insertWidget, args);\\n }\\n
\\n此时调用 WidgetSync#saveWidget
就可以完成一个组件的所有数据录入。最后,只需要遍历所有的组件,触发 saveWidget
即可。这样就得到了对 FlutterUnit 组件展示来说至关重要的数据库: flutter.db
final Map<int, String> familyMap = {\\n 0: \'StatelessWidget\',\\n 1: \'StatefulWidget\',\\n 2: \'SingleChildRenderObjectWidget\',\\n 3: \'MultiChildRenderObjectWidget\',\\n 4: \'Sliver\',\\n 5: \'ProxyWidget\',\\n 6: \'Other\',\\n};\\n\\nFuture<void> syncAll() async {\\n for (String family in familyMap.values) {\\n await syncFamily(family);\\n }\\n}\\n\\nFuture<void> syncFamily(String family) async {\\n String dir = p.join(project, \'modules\', \'widget_system\', \'widgets\', \'lib\');\\n String familyDir = p.join(dir, family);\\n Directory directory = Directory(familyDir);\\n List<FileSystemEntity> entity = directory.listSync();\\n for (FileSystemEntity e in entity) {\\n if (e is Directory) {\\n await saveWidget(e.path);\\n }\\n }\\n}\\n
\\n数据库目前已经内含 10 国语言的数据,接下来需要升级应用层的查询处理。之前介绍过,FlutterUnit 采用模块化的设计:
\\n当前增加数据库多语言查询,其实对业务逻辑和视图构建没有太大的影响。只需要处理 widget_repository
中数据库查询操作即可:比如 WidgetDao
中, queryWidgetByName 方法根据组件名查询组件,现在增加 locale
参数表示查询的语言,通过 widget 和 widget_desc 联表查询,就可以得到之前的数据结构。上层的模型层、视图层都不用任何变化:
Future<Map<String, dynamic>?> queryWidgetByName(String name, {String? locale}) async {\\n String querySql = \\"\\"\\"\\nSELECT \\n widget.id, \\n widget.name,\\n widget.family,\\n widget.linkWidget,\\n widget.lever,\\n widget_desc.name AS nameCN, \\n widget_desc.info\\nFROM widget\\nINNER JOIN widget_desc \\n ON widget.id = widget_desc.widget_id\\nWHERE \\nwidget.name = ? AND \\nwidget_desc.locale = ? \\n;\\n\\"\\"\\";\\n List<Map<String, Object?>> result = await database.rawQuery(querySql, [name,locale??\'zh-cn\']);\\n if (result.isNotEmpty) {\\n return result.first;\\n }\\n return null;\\n }\\n
\\n这就是模块化以及合理划分层次的好处,新的需求来临时,只需要修改和它相关模块的相关层级即可。外界只在乎它提供的能力,不在意其具体的功能实现,这样就可以局部地升级代码,不会对整个系统产生其他影响。最后在查询时,业务层逻辑的事件,只需要额外承载 locale
参数,作为查询的参数,就可以将 旧的螺丝钉 换成 支持十国语言的新螺丝钉。
extension WidgetContext on BuildContext{\\n\\n void switchWidgetFamily(WidgetFamily family){\\n Locale locale = read<AppConfigBloc>().state.language.locale;\\n String lang = \'${locale.languageCode}-${locale.countryCode}\'.toLowerCase();\\n read<WidgetsBloc>().add(EventTabTap(family,locale: lang));\\n }\\n}\\n
\\n本文简单介绍了一下 FlutterUnit 支持十国语言的过程,具体细节可以自己查阅开源的项目源码。在 AI 大模型的能力加持下,普通的开发者可以调度更加强大的力量,完成在之前很难实现的功能。包括目前正在进行的 Flutter 组件 Logo 设计,也有 AI 帮忙设计和优化。大家敬请期待 FlutterUnit 的逐步完善,感谢支持 ~
\\n你是否遇到过需要快速去重或高效查找元素的场景?比如统计一组用户ID的唯一性,或是检查某个关键词是否已存在?在Dart中,Set
正是为解决这类问题而生的利器。与List
不同,Set
天生具备元素唯一性和无序性,其底层基于哈希表实现,使得查找、插入和删除操作的时间复杂度接近O(1),性能优势显著。相信你已经很期待Set利器的使用方式了,那接下来就跟随我们一起走近Set,掌握Set这把瑞士军刀。
集合(Set)又称为无序集合,是一个无序且不允许重复的元素集合。和数学中集合概念一样其具有数学中集合的特点和操作(并、交、差)。
\\nDart中集合(Set)和列表一样可以支持不同的数据类型。通过动态类型声明。
\\n通过大括号({}
)直接创建。
示例:
\\nvoid main() {\\n Set<int> intSet = {2,23,3,14,6,90};\\n Set<dynamic> dynamicSet = {23,true,\'apple\',12.5,\'peach\',\'pear\'};\\n}\\n
\\n<String>{}
:创建空集合。Set<int>.from()
:创建具有初始值的集合。示例:
\\nvoid main() {\\n Set<String> emptySet = <String>{};\\n Set<int> fromSet = Set<int>.from([2,23,3,14,6,90]);\\n}\\n
\\nSet.of()可以从任何可迭代对象创建集合(Set)。
\\n示例:
\\nvoid main() {\\n Set<int> ofSet = Set.of([2,23,3,14,6,90]);\\n}\\n
\\nDart中支持创建一个不可变的集合,使用Set.unmodifiable()创建。
\\n示例:
\\nvoid main() {\\n Set<int> unmodifiableSet = Set.unmodifiable([2,23,3,14,6,90]);\\n // 尝试添加内容。\\n unmodifiableSet.add(1200); // Unsupported operation: Cannot change an unmodifiable set\\n}\\n
\\n运行时报错:
\\n示例:
\\nvoid main() {\\n Set<int> intSet = {2,23,3,14,6,90};\\n Set<dynamic> dynamicSet = {23,true,\'apple\',12.5,\'peach\',\'pear\'};\\n print(intSet.length); // 输出:6\\n print(intSet.isEmpty); // 输出:false\\n print(intSet.first); // 输出:2\\n print(intSet.last); // 输出:90\\n}\\n
\\n注意:union()、intersection()、difference()会创建一个新集合。
\\n示例:
\\nvoid main() {\\n Set<int> intSet = {2,23,3,14,6,90};\\n Set<dynamic> dynamicSet = {23,true,\'apple\',12.5,\'peach\',\'pear\'};\\n Set<int> intSetA = {1,5,9};\\n Set<int> intSetB = {2,5,8};\\n print(intSetA.contains(1200)); // 输出:false\\n // 并集\\n print(intSetA.union(intSetB));// 输出:{1, 5, 9, 2, 8}\\n // 交集\\n print(intSetA.intersection(intSetB)); // 输出:{5}\\n // 差集\\n print(intSetA.difference(intSetB)); // 输出:{1, 9}\\n // 查找元素中所有大于20的元素。\\n Set<int> whereSet = intSet.where((element) => element>20).toSet();\\n print(whereSet); // 输出:{23, 90}\\n // 判断集合中是否有大于五十的数返回bool类型。\\n bool hasAnyBool = intSet.any((element) => element > 50);\\n print(hasAnyBool);// 输出:true\\n // 判断集合中是否所有元素都大于五十,返回bool类型。\\n bool hasEveryBool = intSet.every((element) => element > 50);\\n print(hasEveryBool);// 输出:false\\n}\\n
\\n集合(Set)可通过遍历的方式访问元素。其与列表不同,集合是无序的,不支持所有关于下标的操作。
\\n将集合中的所有元素依次访问一遍。
\\n示例: for循环遍历
\\nvoid main() {\\n Set<dynamic> dynamicSet = {23,true,\'apple\',12.5,\'peach\',\'pear\'};\\n List<dynamic> dynamicList = dynamicSet.toList(); // 转换为列表进行遍历\\n for (int i=0;i<dynamicList.length;i++){\\n print(dynamicList[i]);\\n }\\n}\\n
\\n示例: for-in循环遍历
\\nvoid main() {\\n Set<dynamic> dynamicSet = {23,true,\'apple\',12.5,\'peach\',\'pear\'};\\n for (var element in dynamicSet){\\n print(element);\\n }\\n}\\n
\\n示例: 高阶函数forEach()遍历
\\nvoid main() {\\n Set<dynamic> dynamicSet = {23,true,\'apple\',12.5,\'peach\',\'pear\'};\\n dynamicSet.forEach((element) => print(element));\\n}\\n
\\n注意:add()、addAll()都是原地添加,即在原来基础上进行添加。
\\n示例:
\\nvoid main() {\\n Set<int> intSet = {2,23,3,14,6,90};\\n intSet.add(1200); // 原地操作,在原来的基础上进行添加。\\n print(intSet); // 输出:{2, 23, 3, 14, 6, 90, 1200}\\n intSet.addAll([1,2,3,1899,1899]); \\n print(intSet); // 去除重复元素,输出:{2, 23, 3, 14, 6, 90, 1200, 1, 1899}\\n}\\n
\\nDart集合(Set)不支持通过下标进行元素的访问和修改,这就意味着我们不能通过如同列表那样去修改元素。当然也可以将其转为列表(List)再进行修改,但通常我们采取的是移除要修改的元素后再添加修改后的元素。
\\n示例:
\\nvoid main() {\\n Set<int> intSet = {2,23,3,14,6,90};\\n Set<dynamic> dynamicSet = {23,true,\'apple\',12.5,\'peach\',\'pear\'};\\n // 修改元素apple为1200\\n dynamicSet.remove(\'apple\'); // 1、移除元素apple\\n print(dynamicSet); // 输出:{23, true, 12.5, peach, pear}\\n dynamicSet.add(1200); // 2、添加元素1200\\n print(dynamicSet); // 输出:{23, true, 12.5, peach, pear, 1200}\\n}\\n
\\n示例:
\\nvoid main() {\\n Set<int> intSet = {2,23,3,14,6,90};\\n Set<dynamic> dynamicSet = {23,true,\'apple\',12.5,\'peach\',\'pear\'};\\n dynamicSet.remove(\'apple\');\\n print(dynamicSet); // 输出:{23, true, 12.5, peach, pear}\\n dynamicSet.removeAll([23,true]);\\n print(dynamicSet); // 输出:{12.5, peach, pear}\\n dynamicSet.removeWhere((element) => element==12.5);// 移除等于12.5的元素\\n print(dynamicSet); // 输出:{peach, pear}\\n dynamicSet.clear(); \\n print(dynamicSet); // 输出:{}\\n}\\n
\\n本小节归纳总结如下:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n方法或属性 | |
---|---|
创建和初始化 | 1、使用{} 创建。2、 <int>{} 创建空集合。3、 Set<int>.from() 4、 Set.of() |
不可变集合 | 使用Set.unmodifiable() 创建。 |
常用属性和方法 | 1、length 属性。 2、isEmpty 属性。 3、first 属性。 4、last 属性。 1、 contains() 方法。 2、union() 方法。 3、intersection() 方法。 4、 difference() 方法。 5、高阶函数map() 等。 |
访问元素 | 1、for 遍历(转换为列表)。 2、 for-in 遍历。3、高阶函数 forEach() 遍历。 |
添加元素 | 1、add() 添加单个元素。 2、 addAll() 添加多个元素。 |
修改元素 | 1、移除要修改的元素。 2、添加修改后的内容。 |
移除元素 | 1、remove() 移除单个元素。 2、 removeAll() 批量移除元素。3、 removeWhere() 移除符合条件的元素。 4、 clear() 清空列表。 |
如果你是刚刚踏入编程世界的新手,可能会好奇:如何在代码中高效管理一组数据?答案就是列表(List) ——它就像现实生活中的“购物清单”或“待办事项表”,能帮你将多个元素有序地组织在一起,轻松完成增删改查等操作。相信你已经开始好奇列表是什么啦?接下来我们将一起从零开始认识Dart列表这把利器。
\\n列表(List)也称为有序集合,是集合类型中的一种,其是基于动态数组实现的数据结构,可以存储相同类型或不同类型的元素。每个元素都有固定的位置索引,通过索引可以方便地访问到元素。
\\n如下图所示蓝色部分为列表内容,红色部分为列表内容对应的位置索引。
\\n泛型支持是指列表中的元素类型可以声明为动态类型。如上图中的列表元素类型分别为int类型、字符串类型、浮点类型、布尔类型。
\\n使用中括号([])快速创建。
\\n示例:
\\nvoid main() {\\n List<int> intList = [2,23,23,54,8,12,2];\\n List<dynamic> dynamicList = [23,23,\'apple\',8,\'peach\',12.5,true];\\n}\\n
\\n使用List类的构造函数创建。
\\nList<int>.empty()
:创建空列表。List<int>.filled(5,1)
:创建固定长度的列表。示例:
\\nvoid main() {\\n List<int> emptyList = List<int>.empty(growable: true);\\n List<int> fixedLenList = List<int>.filled(5,1);\\n}\\n
\\nlist.generate()可以根据生成器函数创建列表。第一个参数为列表长度,第二个参数为生成器函数。
\\n示例:
\\nvoid main() {\\n List<int> generateList = List.generate(5, (index)=>index*2);\\n print(generateList); // 输出:[0, 2, 4, 6, 8]\\n}\\n
\\nDart中支持创建一个不可进行更改的列表,使用 List.unmodifiable() 创建。
\\n示例:
\\nvoid main() {\\n List<int> unmodifiableList = List.unmodifiable([2,23,23,54,8,12,2]);\\n unmodifiableList.add(4); // Unsupported operation: Cannot add to an unmodifiable list\\n}\\n
\\n运行时报错:\\n
示例:
\\nvoid main() {\\n List<int> intList = [2,23,23,54,8,12,2];\\n print(intList.length); // 输出:7\\n print(intList.isEmpty); // 输出:false\\n print(intList.first); // 输出:2\\n print(intList.last);// 输出:2\\n}\\n
\\n示例:
\\nvoid main() {\\n List<int> intList = [2,23,23,54,8,12,2];\\n print(intList.indexOf(2)); // 输出:0\\n print(intList.contains(1220)); // 输出:false\\n intList.sort();\\n print(intList); // 输出:[2, 2, 8, 12, 23, 23, 54]\\n List<int> mapIntList = intList.map((n)=>n*2).toList();\\n print(mapIntList); // 输出:[4, 4, 16, 24, 46, 46, 108]\\n List<int> whereIntList = intList.where((n)=>n%2==0).toList();\\n print(whereIntList); // 输出:[2, 2, 8, 12, 54]\\n}\\n
\\n列表支持下标访问元素和遍历元素。
\\n通过位置索引访问元素。
\\n注意:索引从0开始。
\\n示例:
\\nvoid main() {\\n List<dynamic> dynamicList = [23,23,\'apple\',8,\'peach\',12.5,true];\\n print(dynamicList[3]); // 输出:8\\n}\\n
\\n将列表中的所有元素访问一遍。
\\n示例:for循环遍历
\\nvoid main() {\\n List<dynamic> dynamicList = [23,23,\'apple\',8,\'peach\',12.5,true];\\n for(int i=0;i<dynamicList.length;i++){ // 通过下标依次访问\\n print(dynamicList[i]);\\n }\\n}\\n
\\n示例:for-in遍历
\\nvoid main() {\\n List<dynamic> dynamicList = [23,23,\'apple\',8,\'peach\',12.5,true];\\n for(var element in dynamicList){\\n print(element);\\n }\\n}\\n
\\n示例:高阶函数forEach()遍历
\\nvoid main() {\\n List<dynamic> dynamicList = [23,23,\'apple\',8,\'peach\',12.5,true];\\n dynamicList.forEach((element)=>print(element));\\n}\\n
\\n示例:
\\nvoid main() {\\n List<int> intList = [2,23,23,54,8,12,2];\\n intList.add(5); \\n print(intList);// 输出:[2, 23, 23, 54, 8, 12, 2, 5]\\n intList.insert(4, 1200);\\n print(intList);// 输出:[2, 23, 23, 54, 1200, 8, 12, 2, 5]\\n intList.addAll([12,43,9]);\\n print(intList); // 输出:[2, 23, 23, 54, 1200, 8, 12, 2, 5, 12, 43, 9]\\n}\\n
\\n修改元素分为两步,第一步访问到需要修改的元素,第二步进行修改。
\\n示例:
\\nvoid main() {\\n List<int> intList = [2,23,23,54,8,12,2];\\n intList[4] = 88888;\\n print(intList); // 输出:[2, 23, 23, 54, 88888, 12, 2]\\n}\\n
\\n从列表中删除元素。
\\n示例:
\\nvoid main() {\\n List<dynamic> dynamicList = [23,23,\'apple\',8,\'peach\',12.5,true];\\n dynamicList.remove(\'apple\');\\n print(dynamicList); // 输出:[23, 23, 8, peach, 12.5, true]\\n dynamicList.removeLast();\\n print(dynamicList); // 输出:[23, 23, 8, peach, 12.5]\\n dynamicList.clear();\\n print(dynamicList); // 输出:[]\\n}\\n
\\n本小节归纳总结如下:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n方法或属性 | |
---|---|
创建和初始化 | 1、使用[] 创建;2、 List<int>.empty() ;3、 list<int>filled() ;4、 List.generate() ; |
不可变列表 | List.unmodifiable() 创建。 |
常用属性和方法 | 1、length 属性; 2、isEmpty 属性;3、first 属性; 4、last 属性;1、 indexOf() 方法; 2、contains() 方法;3、sort() 方法; 4、map() 等高阶函数; |
访问元素 | 1、下标访问 ; 2、 for 循环遍历;3、 for-in 遍历;4、 forEach() 遍历; |
添加元素 | 1、add() ;2、 addAll() ;3、 insert() ; |
修改元素 | 通过下标找到元素,然后进行修改。 |
移除元素 | 1、remove() ;2、 removeLast() ;3、 clear() ; |
\\n\\nBuildScript Unsupported class file major version 65
\\n
表示你尝试运行一个使用了Java 19(Java SE 19的类文件主要版本号为65)编译的类文件,但在一个不支持该版本的Java虚拟机(JVM)上运行。
\\n\\n\\n通过gradle.properties 文件配置 jdk的安装路径
\\n
\\n\\n大概意思就是:对应的依赖位置和项目编译后位置不一致导致
\\n
\\n\\n添加 环境变量 PUB_CACHE 位置和项目同一个磁盘即可
\\n
\\n\\n设置完环境变量(最好重启一下电脑)
\\n
\\n\\n关闭 Android Studio,打开dos窗口(cmd),进入项目根目录(我没重启是这么执行的,不然在Studio Studio里面执行命令依赖位置还在原来文件里)分别执行如下命令
\\n
flutter clean\\n
\\n\\nflutter pub get\\n
\\n\\n\\n执行完命令后,进到 android目录,执行如下命令:
\\n
./gradlew clean\\n
\\n\\n./gradlew build\\n
\\n\\n\\n大概意思就是:Android工具链-为Android设备开发(Android SDK版本35.0.1)\\n! 一些Android许可证不被接受。要解决这个问题,运行:flutter doctor -android-licenses
\\n
执行如下命令
\\nflutter doctor --android-licenses\\n
\\nflutter doctor\\n
\\n下载谷歌浏览器 并安装
\\nflutter doctor\\n
\\n出现如下错误:
\\nVisual Studio is missing necessary components. Please re-run the Visual Studio installer for the \\"Desktop development with C++\\" workload, and include these components:
\\n
flutter doctor\\n
\\n修改 flutter sdk 下面的flutter.groovy 文件
\\n在编程世界中,函数是构建逻辑的核心单元,而Dart作为一门现代化的多范式语言,其函数设计既灵活又强大,能够同时满足面向对象和函数式编程的需求。无论是开发Flutter应用,还是编写服务端脚本,深入理解Dart函数都能显著提升代码的可读性与效率。
\\n一个函数由函数签名+函数体组成。
\\n函数通常由函数返回类型 + 函数名 + 参数 + 函数体组成。
\\n示例:
\\nint addOne(int a){ \\n a++;\\n return a;\\n}\\n
\\n注:函数签名包括函数返回类型、函数名、参数。
\\nDart中函数的参数支持多种方式。分别为:
\\n必须参数示例:
\\n// name必须参数,调用hello函数时必须传入。\\nvoid hello(String name) {\\n print(\'hello,$name!\');\\n}\\nvoid main() {\\n hello(\'Dart\'); // 输出:hello,Dart!\\n}\\n
\\n可选参数示例:用大括号({})进行包裹。
\\n// age为可空类型,意味着可以不进行传入。\\nvoid introduce(String name,{int? age,required sex}){\\n print(\'hello,my name is $name\');\\n if (sex){\\n print(\'I am a boy\');\\n }else{\\n print(\'I am a girl\');\\n }\\n if (age!=null) {\\n print(\'I am $age years old this year\');\\n }\\n}\\nvoid main() {\\n introduce(\'小明\',sex:true); // 没有传入age,则age为空,结果不打印。\\n // 输出:\\n // hello,my name is 小明\\n // I am a boy\\n}\\n
\\n位置参数示例:用中括号([])进行包裹。
\\nvoid introduce(String name,[int? age,sex]){\\n print(\'hello,my name is $name\');\\n if (sex!=null){\\n print(\'I am a $sex\');\\n }\\n if (age!=null) {\\n print(\'I am $age years old this year\');\\n }\\n}\\nvoid main() {\\n introduce(\'小明\'); // 输出:hello,my name is 小明\\n // 输出:\\n // hello,my name is 小明\\n // I am a boy\\n // I am 18 years old this year\\n}\\n
\\n默认参数示例:为可选参数提供默认值。
\\n// age为默认值参数,在调用函数时不传入age参数时age为18\\nvoid introduce(String name,[age=18,sex]){\\n print(\'hello,my name is $name\');\\n if (sex!=null){\\n print(\'I am a $sex\');\\n }\\n if (age!=null) {\\n print(\'I am $age years old this year\');\\n }\\n}\\nvoid main() {\\n introduce(\'小明\');\\n // 输出:\\n // hello,my name is 小明\\n // I am 18 years old this year\\n}\\n
\\n注意:可选参数,位置参数的数据类型会进行自动推断,意味着定义函数时可以不进行类型声明。
\\nDart中函数的返回值可以是没有返回值。\\n也可以是返回单个值或多个值(以列表或映射形式返回)。
\\n示例:返回单个值。
\\nint addOne(int a){ \\n a++;\\n return a;\\n}\\n
\\n示例:返回多个值。
\\nList<int> getList(int a, int b){\\n return [a,a*2];\\n}\\nMap<String,int> getMap(String a, int b){\\n return {a:b};\\n}\\n
\\n匿名函数就如同其名字一样是没有名字的函数。这是一种简化定义函数的方式,通常在函数作为参数时传参使用。
\\n由圆括号()+大括号{}组成
\\n示例:
\\nvoid executeFunc(void Function() func){\\n func(); // 调用传入的函数\\n}\\nvoid main() {\\n executeFunc(\\n () {print(\'hello,Dart!\'); // 匿名函数\\n }); // 输出:hello,Dart!\\n}\\n
\\n箭头函数,一种特殊的匿名函数,用于执行只有一行的代码,自动返回结果。
\\n由 => 声明
\\n示例:
\\nvoid main() {\\n var add = (int a, int b) => a + b;\\n print(add.runtimeType); // 输出:(int, int) => int\\n print(add(2,4));// 输出:6\\n}\\n
\\n闭包可以理解为函数中嵌套函数,这样做可以带来的好处是内部的函数可以访问外部的作用域。常用于捕获外部作用域的值。
\\n示例:
\\nFunction outerFunc(){\\n int outerFuncData = 1; // 外部函数的作用域\\n int innerFunc(){\\n return ++outerFuncData; // 访问外部函数作用域的变量outerFuncData\\n }\\n return innerFunc;\\n}\\nvoid main() {\\n var a = outerFunc(); // a为outerFunc函数返回的结果,其结果为函数类型。\\n print(a.runtimeType); // 输出:() => int\\n print(a()); // 输出:2\\n print(a()); // 输出:3\\n}\\n
\\n高阶函数是可以接受函数作为参数或返回值的函数。如同上面的匿名函数示例将函数作为参数进行传递。
\\n示例:
\\n// executeFunc函数为高阶函数,其接受传入参数为函数类型。\\nvoid executeFunc(void Function() func){\\n func(); // 调用传入的函数\\n}\\nvoid main() {\\n executeFunc(\\n () {print(\'hello,Dart!\'); \\n }); // 输出:hello,Dart!\\n}\\n
\\n回调函数是作为参数传递给另一个函数的函数,就和其名字一样往回调用函数。其场景是在将函数作为参数时,在调用函数内部执行传入的函数参数。
\\n示例:
\\nvoid executeFunc(void Function() func){\\n func(); // 执行回调函数\\n}\\nvoid main() {\\n executeFunc(\\n () {print(\'hello,Dart!\'); // 匿名函数,也是回调函数。其作为参数传递给另一个函数。\\n }); // 输出:hello,Dart!\\n}\\n
\\n递归函数可以理解为函数自己调用自己。因其执行过程包括递和归两个过程,故称其为递归函数。
\\n注意:递归函数必须有一个明确的终止条件,否则会导致无限递归。
\\n示例:
\\nint factorial(int n) {\\n if (n == 0 || n == 1) {\\n return 1;\\n } else {\\n return n * factorial(n - 1);\\n }\\n}\\n\\nvoid main() {\\n print(factorial(5));// 输出:120\\n}\\n\\n//详细执行流程说明\\n//1.初始执行\\nint factorial(int 5) {\\n if (n == 0 || n == 1) {\\n return 1;\\n } else {\\n //2.第一次递归调用\\n return 5*factorial(int 4) {\\n if (n == 0 || n == 1) {\\n return 1;\\n } else {\\n //3.第二次递归调用\\n return 4*factorial(int 3) {\\n if (n == 0 || n == 1) {\\n return 1;\\n } else {\\n //4.第三次递归调用\\n return 3*factorial(int 2) {\\n if (n == 0 || n == 1) {\\n return 1;\\n } else {\\n //5.第四次递归调用\\n return 2*factorial(int 1) {\\n if (n == 0 || n == 1) {\\n return 1;\\n } else {\\n return n* factorial(n-1);\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n}\\n
\\n本小节从基础函数定义出发,逐步解析Dart中的高阶函数、匿名函数、闭包等核心概念,并通过实例演示如何实现代码结构。相信通过本小节的了解,我们将会很快的掌握Dart中的函数。
","description":"前言 在编程世界中,函数是构建逻辑的核心单元,而Dart作为一门现代化的多范式语言,其函数设计既灵活又强大,能够同时满足面向对象和函数式编程的需求。无论是开发Flutter应用,还是编写服务端脚本,深入理解Dart函数都能显著提升代码的可读性与效率。\\n\\n一、函数的定义\\n\\n一个函数由函数签名+函数体组成。\\n\\n1.1、基本语法\\n\\n函数通常由函数返回类型 + 函数名 + 参数 + 函数体组成。\\n\\n函数返回类型:需与return 返回的数据类型保持一致。\\n函数名:遵循标识符命名规范,一般使用小驼峰命名(首字母小写,后续首字母大写)。\\n参数…","guid":"https://juejin.cn/post/7471178644770439183","author":"好的佩奇","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-14T13:52:12.080Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/50d6a589c6f14e1cbcc4cadd2e339442~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5aW955qE5L2p5aWH:q75.awebp?rk3s=f64ab15b&x-expires=1740146027&x-signature=zxf4jyK6O%2Bbirq7QKqz6Z0dDFuk%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["前端","Flutter","Dart","Android"],"attachments":null,"extra":null,"language":null},{"title":"详细解释Flutter中 async/await 的工作原理","url":"https://juejin.cn/post/7470912831412912191","content":"在 Flutter(Dart)中,async/await
是 异步编程 的核心机制。Dart 使用 单线程的事件循环 进行任务调度,而 async/await
提供了一种更直观的方式来管理异步任务。
本篇文章将深入解析 async/await
的 执行原理,包括:
Flutter 的 UI 渲染依赖于 主线程(UI 线程) ,如果任务执行时间过长,会导致界面卡顿(掉帧)。为了保证流畅性,Flutter 采用 异步模型 处理耗时任务,比如:
\\nDart 采用 单线程事件循环,与 JavaScript 类似:
\\n事件按照 先进先出(FIFO) 执行
\\n任务分为:
\\nPromise.then
)在 Dart 中,Future
代表 一个异步操作的结果:
dart\\n复制编辑\\nFuture<String> fetchData() {\\n return Future.delayed(Duration(seconds: 2), () => \\"数据加载完成\\");\\n}\\n
\\n使用 .then()
处理:
dart\\n复制编辑\\nfetchData().then((data) {\\n print(data);\\n});\\n
\\n但 .then()
方式 嵌套太多,会变得难以阅读(回调地狱):
dart\\n复制编辑\\nfetchData().then((data) {\\n process(data).then((result) {\\n save(result).then((_) {\\n print(\\"完成\\");\\n });\\n });\\n});\\n
\\n为了解决这个问题,Dart 引入了 async/await
。
dart\\n复制编辑\\nFuture<void> loadData() async {\\n String data = await fetchData();\\n print(data);\\n}\\n
\\n等价于:
\\ndart\\n复制编辑\\nfetchData().then((data) {\\n print(data);\\n});\\n
\\nasync/await 只是 Future
的语法糖,它不会改变 Future
本身的行为,而是让异步代码像同步代码一样编写。
Flutter 运行时采用 事件循环(Event Loop) 处理异步任务:
\\nDart 采用 单线程事件循环 处理所有任务,执行顺序
\\n示例:
\\ndart\\n复制编辑\\nvoid main() {\\n print(\\"1\\");\\n Future(() => print(\\"2\\"));\\n scheduleMicrotask(() => print(\\"3\\"));\\n print(\\"4\\");\\n}\\n
\\n执行顺序:
\\nprint(\\"1\\")
print(\\"4\\")
(同步任务执行完毕)print(\\"3\\")
(微任务队列执行)print(\\"2\\")
(事件任务执行)await
并不会阻塞线程,而是将代码拆分为多个 Future 回调,然后利用 状态机 进行调度。
dart\\n复制编辑\\nFuture<void> main() async {\\n print(\\"A\\");\\n await Future.delayed(Duration(seconds: 1));\\n print(\\"B\\");\\n}\\n
\\ndart\\n复制编辑\\nFuture<void> main() {\\n print(\\"A\\");\\n return Future.delayed(Duration(seconds: 1)).then((_) {\\n print(\\"B\\");\\n });\\n}\\n
\\n执行过程
\\nprint(\\"A\\")
Future.delayed
被加入 事件队列Future
print(\\"B\\")
await
关键字 会拆分成多个 Future 回调,每个 await
相当于创建了一个新的 Future.then。
在 Dart 源码中,async
代码会被编译成 Future
回调链,并使用 状态机 控制执行。
查看 dart:async
中的 Future
代码:
dart\\n复制编辑\\nFuture<T> async<T>(FutureOr<T> Function() computation) {\\n return Future<T>(() {\\n return computation();\\n });\\n}\\n
\\nDart 运行时会自动将 async
方法转换成 Future
状态机。
示例
\\ndart\\n复制编辑\\nFuture<int> compute() async {\\n int result = await Future.value(10);\\n return result;\\n}\\n
\\n实际执行时,相当于:
\\ndart\\n复制编辑\\nFuture<int> compute() {\\n return Future.value(10).then((result) {\\n return result;\\n });\\n}\\n
\\nDart 运行时 通过状态机调度 await
关键字,使其执行时不会阻塞主线程。
await
阻塞 UI错误示例:
\\ndart\\n复制编辑\\nvoid main() async {\\n await Future.delayed(Duration(seconds: 3)); // UI 会卡顿\\n runApp(MyApp());\\n}\\n
\\n正确方式:
\\ndart\\n复制编辑\\nvoid main() {\\n runApp(MyApp());\\n Future.delayed(Duration(seconds: 3), () {\\n print(\\"初始化完成\\");\\n });\\n}\\n
\\n使用 try-catch
捕获 async/await
异常:
dart\\n复制编辑\\nFuture<void> fetchData() async {\\n try {\\n String data = await Future.error(\\"网络异常\\");\\n print(data);\\n } catch (e) {\\n print(\\"捕获异常: $e\\");\\n }\\n}\\n
\\n避免未处理的异常导致应用崩溃。
\\nawait
错误示例:
\\ndart\\n复制编辑\\nFuture<void> loadData() async {\\n String data1 = await fetchData1();\\n String data2 = await fetchData2();\\n}\\n
\\n优化:
\\ndart\\n复制编辑\\nFuture<void> loadData() async {\\n var future1 = fetchData1();\\n var future2 = fetchData2();\\n String data1 = await future1;\\n String data2 = await future2;\\n}\\n
\\n这样 fetchData1()
和 fetchData2()
并行执行,提高性能。
async/await
只是 Future
的语法糖,本质是 状态机 + Future.then()await
不会阻塞 UI 线程await
,避免 UI 阻塞理解 async/await
的底层原理,有助于编写更高效、可维护的 Flutter 代码
Flutter 的异常处理机制包括 同步异常 和 异步异常,涉及多个层面,如 Dart 语言级别的异常捕获、Flutter 框架的错误处理机制、自定义异常处理 以及 错误上报。
\\nDart 主要使用 try-catch
进行异常捕获,并区分 同步异常 和 异步异常 的处理方式。
同步代码中的异常可以通过 try-catch
捕获:
dart\\n复制编辑\\nvoid main() {\\n try {\\n int result = 10 ~/ 0; // 除以 0,会触发异常\\n } catch (e, stackTrace) {\\n print(\\"捕获异常: $e\\");\\n print(\\"堆栈信息: $stackTrace\\");\\n }\\n}\\n
\\ne
捕获异常对象stackTrace
获取异常发生的堆栈信息,方便调试常见异常类型:
\\nFormatException
:数据格式错误RangeError
:数组越界TypeError
:类型转换错误StateError
:非法状态操作ArgumentError
:参数错误Flutter 使用 异步编程(Future 和 Stream) ,因此要特别注意 Future
和 Stream
的异常处理。
异步 Future
任务的异常需要使用 .catchError()
或 try-catch
捕获:
dart\\n复制编辑\\nFuture<void> asyncTask() async {\\n try {\\n await Future.delayed(Duration(seconds: 1));\\n throw Exception(\\"异步任务异常\\");\\n } catch (e) {\\n print(\\"捕获 Future 异常: $e\\");\\n }\\n}\\n\\nvoid main() {\\n asyncTask();\\n}\\n
\\n另一种方式是 .catchError()
:
dart\\n复制编辑\\nFuture<void> asyncTask() {\\n return Future.delayed(Duration(seconds: 1))\\n .then((_) => throw Exception(\\"异步任务异常\\"))\\n .catchError((e) {\\n print(\\"Future 捕获异常: $e\\");\\n });\\n}\\n
\\nStream
中的异常不会自动抛出,需要使用 handleError()
或 try-catch
捕获:
dart\\n复制编辑\\nvoid main() {\\n Stream<int> stream = Stream.periodic(Duration(seconds: 1), (count) {\\n if (count == 2) throw Exception(\\"Stream 发生错误\\");\\n return count;\\n }).take(5);\\n\\n stream.listen(\\n (data) => print(\\"收到数据: $data\\"),\\n onError: (error) => print(\\"捕获 Stream 异常: $error\\"),\\n );\\n}\\n
\\nFlutter 提供了一套全局异常处理机制,包括:
\\nFlutterError.onError
PlatformDispatcher.instance.onError
runZonedGuarded
FlutterError.onError
)Flutter 框架内部的异常(例如 setState()
期间的异常)会由 FlutterError.onError
处理:
dart\\n复制编辑\\nvoid main() {\\n FlutterError.onError = (FlutterErrorDetails details) {\\n print(\\"Flutter 框架错误: ${details.exception}\\");\\n print(\\"错误详情: ${details.stack}\\");\\n };\\n\\n runApp(MyApp());\\n}\\n
\\n应用场景
\\nbuild()
方法中的错误PlatformDispatcher.instance.onError
)PlatformDispatcher.instance.onError
可用于捕获 Flutter Engine 层的异常:
dart\\n复制编辑\\nvoid main() {\\n PlatformDispatcher.instance.onError = (error, stack) {\\n print(\\"捕获 Native 层异常: $error\\");\\n return true; // 阻止异常冒泡\\n };\\n\\n runApp(MyApp());\\n}\\n
\\n适用场景
\\nrunZonedGuarded
异常Dart 允许在 runZonedGuarded
中执行代码,确保 Future
或 Stream
中的异常不会导致程序崩溃:
dart\\n复制编辑\\nvoid main() {\\n runZonedGuarded(() {\\n runApp(MyApp());\\n }, (error, stackTrace) {\\n print(\\"runZonedGuarded 捕获到异常: $error\\");\\n });\\n}\\n
\\n适用场景
\\n在生产环境中,异常通常需要上报到日志系统(如 Sentry、Firebase Crashlytics)。
\\nFlutterError.onError
进行上报dart\\n复制编辑\\nvoid main() {\\n FlutterError.onError = (FlutterErrorDetails details) {\\n // 发送错误信息到服务器\\n reportError(details.exception, details.stack);\\n };\\n\\n runApp(MyApp());\\n}\\n\\nvoid reportError(Object error, StackTrace? stack) {\\n // 发送到 Sentry 或 Firebase\\n print(\\"上报错误: $error\\");\\n}\\n
\\n有时,我们希望在 UI 层面防止崩溃,比如 build()
发生异常时显示默认 UI。
ErrorWidget
Flutter 允许替换 ErrorWidget
,在 Widget
构建失败时显示自定义错误页面:
dart\\n复制编辑\\nvoid main() {\\n ErrorWidget.builder = (FlutterErrorDetails details) {\\n return Center(child: Text(\\"发生错误: ${details.exceptionAsString()}\\"));\\n };\\n\\n runApp(MyApp());\\n}\\n
\\n机制 | 作用 | 适用场景 |
---|---|---|
try-catch | 捕获同步异常 | 同步代码异常 |
catchError() | 处理 Future 异常 | 异步异常 |
handleError() | 处理 Stream 异常 | Stream 发生错误 |
FlutterError.onError | 捕获 Flutter 框架错误 | Widget build() 失败 |
PlatformDispatcher.instance.onError | 监听 Engine 级错误 | Flutter Engine 发生崩溃 |
runZonedGuarded | 保护全局异步代码 | 防止 Future 未捕获异常导致崩溃 |
ErrorWidget.builder | 自定义 UI 错误页面 | Widget 级别错误 |
catchError()
handleError()
FlutterError.onError
统一收集 UI 相关异常runZonedGuarded
防止未捕获的异步异常ErrorWidget.builder
让 UI 崩溃可视化PlatformDispatcher.instance.onError
监听 Native 层错误Flutter 的异常处理涉及多个层面,从 Dart 语言层、Flutter 框架层到 UI 组件层,掌握这些机制能有效提升 App 的稳定性,减少崩溃情况。
","description":"Flutter 的异常处理机制包括 同步异常 和 异步异常,涉及多个层面,如 Dart 语言级别的异常捕获、Flutter 框架的错误处理机制、自定义异常处理 以及 错误上报。 1. Dart 语言级别的异常处理\\n\\nDart 主要使用 try-catch 进行异常捕获,并区分 同步异常 和 异步异常 的处理方式。\\n\\n1.1. 同步异常\\n\\n同步代码中的异常可以通过 try-catch 捕获:\\n\\ndart\\n复制编辑\\nvoid main() {\\n try {\\n int result = 10 ~/ 0; // 除以 0,会触发异常\\n } catch (e,…","guid":"https://juejin.cn/post/7470880599202185279","author":"东大街","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-14T03:10:20.321Z","media":null,"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"系统化掌握Dart编程之异步编程(四):Stream的江河水利工程","url":"https://juejin.cn/post/7470879836724527156","content":"Stream
—— 数据流动的永恒命题
。
在异步编程的世界里,如果说Future
是点对点的快递包裹,那么Stream
就是奔流不息的江河。它承载着持续的数据流,从点击事件的涓涓细流,到网络传输的滔滔江海,构成了现代应用的血脉系统。理解Stream
的本质,就是掌握在不确定的时间维度上构建确定性数据管道的艺术。
本文将带你从水文学的角度解构Stream
:我们将观察数据流的发源地(Source
),修筑河道(Transform
),建设水库(Buffer
),最终构建完整的水利系统。通过揭示StreamController
的闸门原理、Subscription
的河道治理方案,你将获得驾驭数据洪流的核心能力。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\n认识数据流域
每条Stream
都由三大要素构成:
Source
) :事件产生的源头(用户输入
、定时器
、网络
等)。Pipe
) :数据流转的路径(listen
、transform
)。Sink
) :数据最终归宿(UI渲染
、存储
等)。// 创建山泉水源(定时数据源)\\nfinal mountainStream = Stream.periodic(\\n Duration(seconds: 1), \\n (count) => \'水滴$count\'\\n);\\n\\n// 开凿河道\\nfinal subscription = mountainStream.listen(\\n (drop) => print(\'收到水滴:$drop\'),\\n onDone: () => print(\'河流干涸\')\\n);\\n\\n// 人工截流(5秒后停止)\\nFuture.delayed(Duration(seconds: 5), () => subscription.cancel());\\n
\\nStream
具有区别于Future
的独特性质:
特性 | 类比解释 | 代码表现 |
---|---|---|
多值性 | 持续流动的活水 | stream.listen((data){...}) |
异步性 | 水流速度不可控 | 数据到达时间不确定 |
可中断性 | 人工修建水闸 | subscription.cancel() |
方向性 | 单向流动的河道 | 数据只能从源头到终点 |
创建Stream
的三种典型方式:
// 方式1:山泉(预置水源)\\nStream<int> spring = Stream.fromIterable([1,2,3]);\\n\\n// 方式2:人工湖(控制器)\\nfinal lakeController = StreamController<String>();\\nStream<String> lake = lakeController.stream;\\n\\n// 方式3:瀑布(异步生成器)\\nStream<int> waterfall() async* {\\n for (int i = 0; i < 5; i++) {\\n await Future.delayed(Duration(seconds: 1));\\n yield i;\\n }\\n}\\n
\\nStream
的河道治理Transform
)使用transform
方法进行数据加工:
// 原始水源\\nfinal source = Stream.periodic(Duration(milliseconds: 500), (i) => i);\\n\\n// 建造水处理厂\\nfinal filtered = source.where((i) => i % 2 == 0); // 过滤奇数\\nfinal transformed = filtered.map((i) => \'处理后的数据$i\'); \\nfinal buffered = transformed.transform(StreamTransformer.fromHandlers(\\n handleData: (data, sink) {\\n sink.add(\'[$data]\'); // 添加包装\\n }\\n));\\n\\n// 最终输出示例:[处理后的数据0] → [处理后的数据2] → ...\\n
\\nCombine
)合并多个数据流:
\\nfinal streamA = Stream.periodic(Duration(seconds: 1), (i) => \'A$i\');\\nfinal streamB = Stream.periodic(Duration(seconds: 2), (i) => \'B$i\');\\n\\n// 建造合流水坝\\nStreamZip([streamA, streamB]).listen(\\n (combined) => print(\'${combined[0]}-${combined[1]}\')\\n);\\n// 输出:A0-B0 → A1-B1 → A2-B1...\\n
\\nBack Pressure
)处理数据生产消费速度失衡:
\\n// 快速生产的数据流\\nfinal fastProducer = Stream.periodic(Duration(milliseconds: 100), (i) => i);\\n\\n// 慢速消费者\\nfastProducer\\n .bufferCount(10) // 建造缓冲水库\\n .listen((batch) {\\n print(\'处理批量数据:$batch\');\\n await Future.delayed(Duration(seconds: 1));\\n });\\n
\\nStreamController
源码解析StreamController
)分析dart:async
包中的核心类结构:
class StreamController<T> implements StreamSink<T> {\\n final _StreamImpl<T> _stream; // 主河道\\n final _SyncStreamControllerDispatch<T> _state; // 调度中心\\n \\n void add(T data) { // 开闸放水\\n _state._sendData(data);\\n }\\n \\n Future close() { // 关闭水源\\n _state._close();\\n }\\n}\\n
\\nSubscription
)订阅对象的底层实现:
\\nclass _BufferingStreamSubscription<T> \\n implements StreamSubscription<T> {\\n final _PendingEvents _pending; // 待处理事件队列\\n _HandleData<T> _onData; // 数据处理回调\\n bool _isPaused = false; // 暂停状态\\n \\n void pause([Future<void>? resumeSignal]) { // 暂停监测\\n _isPaused = true;\\n resumeSignal?.whenComplete(resume);\\n }\\n}\\n
\\n事件循环集成
)Stream
与事件循环的交互机制:
事件派发流程:\\n1. 数据到达StreamController\\n2. 检查当前是否在事件循环中\\n - 是:立即派发事件\\n - 否:将事件包装为微任务\\n3. 执行监听器回调\\n4. 处理暂停/恢复状态\\n
\\n异常处理与资源管理
异常处理
)多层级错误捕获方案:
\\nfinal riskyStream = Stream.error(Exception(\'污染事件\'));\\n\\nriskyStream\\n .handleError((e) => print(\'初步处理:$e\')) // 过滤网\\n .transform(StreamTransformer.fromHandlers(\\n handleError: (e, st, sink) { // 深度处理\\n sink.addError(ProcessedError(e));\\n }))\\n .listen(\\n print,\\n onError: (e) => print(\'最终处理:$e\') // 终端处理\\n);\\n
\\n资源释放
)防止内存泄漏的三种方式:
\\n// 方式1:显式取消订阅\\nfinal subscription = stream.listen(...);\\n@override\\nvoid dispose() {\\n subscription.cancel();\\n}\\n\\n// 方式2:使用DisposeBag(第三方库)\\nfinal disposeBag = DisposeBag();\\nstream.listen(...).disposedBy(disposeBag);\\n\\n// 方式3:自动关闭(whenComplete)\\nstream.listen(\\n print,\\n onDone: () => print(\'自动清理完成\')\\n);\\n
\\n实战架构设计
基于Stream
的完整架构:
// 消息处理核心\\nclass ChatEngine {\\n final _controller = StreamController<Message>();\\n late final Stream<Message> publicStream;\\n \\n ChatEngine() {\\n publicStream = _controller.stream\\n .transform(_createMessageTransformer())\\n .asBroadcastStream();\\n }\\n \\n void send(Message msg) => _controller.add(msg);\\n \\n StreamTransformer<Message, Message> _createMessageTransformer() {\\n return StreamTransformer.fromHandlers(\\n handleData: (msg, sink) {\\n if (msg.isValid) {\\n sink.add(msg.withTimestamp());\\n }\\n });\\n }\\n}\\n
\\nBLoC
模式的Stream
实现:
class CounterBloc {\\n final _countController = StreamController<int>();\\n final _actionsController = StreamController<Function>();\\n \\n CounterBloc() {\\n _actionsController.stream\\n .scan<int>((sum, func, _) => func(sum), 0)\\n .pipe(_countController);\\n }\\n \\n Stream<int> get count => _countController.stream;\\n Sink<Function> get action => _actionsController.sink;\\n \\n void dispose() {\\n _countController.close();\\n _actionsController.close();\\n }\\n}\\n\\n// 使用示例\\nbloc.action.add((prev) => prev + 1);\\nbloc.count.listen(print); // 输出:1 → 2 → 3...\\n
\\n成为数据流域的治理专家
掌握Stream
的本质,就是理解数据流动的时间艺术。从最基础的listen
监听,到复杂的背压处理;从表面的事件处理,到底层的StreamController
实现,我们构建了完整的认知体系。
记住:每个Stream
都是独立的水系,Subscription
是控制水闸的钥匙,StreamTransformer
是净化水质的关键设施。当你能自如地设计Stream
管道、处理数据洪峰
、预防内存泄漏
时,就具备了构建复杂异步系统的基础能力。未来的响应式编程、实时系统、物联网应用,都将建立在这套数据水利系统之上。现在,是时候将理论付诸实践,在代码世界中开凿属于自己的数字运河
了。
\\n","description":"前言 Stream —— 数据流动的永恒命题。\\n\\n在异步编程的世界里,如果说Future是点对点的快递包裹,那么Stream就是奔流不息的江河。它承载着持续的数据流,从点击事件的涓涓细流,到网络传输的滔滔江海,构成了现代应用的血脉系统。理解Stream的本质,就是掌握在不确定的时间维度上构建确定性数据管道的艺术。\\n\\n本文将带你从水文学的角度解构Stream:我们将观察数据流的发源地(Source),修筑河道(Transform),建设水库(Buffer),最终构建完整的水利系统。通过揭示StreamController的闸门原理、Subscription…","guid":"https://juejin.cn/post/7470879836724527156","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-14T03:02:56.665Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c9ea1e3df64e4f2d9968f43fe8c87973~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1740106975&x-signature=X4Vwk3WurpPios3gMcf%2F9KVIeoY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/25ee2122d765428f8e18fc2a91a6250f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1740106975&x-signature=bnDcMHPI2L9p8EPwBiOZtGN1t68%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"系统化掌握Dart编程之列表(List)(二):从容器到高性能的进阶之路","url":"https://juejin.cn/post/7470879836724297780","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
List
—— 数据流动的容器哲学
。
在Dart
语言构筑的编程世界中,List
不仅仅是简单的数据容器,它是动态集合的终极表达
,是算法落地的物理载体,更是内存与CPU
对话的微观剧场。从渲染Flutter
界面时的Widget
树管理,到处理百万级数据的科学计算,List
的身影无处不在。这个看似平凡的线性结构,实则是Dart
运行时最精妙的工程杰作 —— 它既保持着JavaScript
数组般的开发友好性,又具备Java ArrayList
级别的性能控制力。
本文将带你穿越List
的多维宇宙,从基础API
的巧用到虚拟机堆内存的布局,从迭代器模式的实现到SIMD
优化的前沿,构建完整的List
认知体系。我们将揭示:
[]
符号背后,蕴含着动态语言与静态类型系统的完美妥协;List
的底层机制,让Flutter
应用的滚动性能提升一个数量级;Dart 3.0
的空安全革命中,List
类型系统如何重塑开发者的集合编程思维。操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\nList
基础架构解构Dart
的List
在虚拟机层面表现为连续内存块
:
// 内存布局示意图\\n+---------+---------+---------+\\n| index 0 | index 1 | index 2 | ... \\n+---------+---------+---------+\\n
\\n类型推断或指定
)baseAddress(首地址) + index * elementSize(元素所占内存宽度)
Dart
通过类型参数约束元素类型:
List<int> numbers = [1, 2, 3]; // 显式类型\\nvar dynamicList = []; // 动态类型(List<dynamic>)\\n
\\n// Dart SDK核心实现\\nabstract class List<E> implements EfficientLengthIterable<E> {\\n // 抽象接口\\n}\\n\\nclass _GrowableList<E> extends ListBase<E> {\\n // 动态扩容实现\\n}\\n\\nclass _FixedLengthList<E> extends ListBase<E> {\\n // 定长实现\\n}\\n
\\nList
的六种范式var list1 = [1, 2, 3]; // 类型推断为List<int>\\nvar list2 = <dynamic>[]; // 动态类型列表\\nvar list3 = [if (condition) 4]; // 条件化构造\\n
\\nvar fixedList = List.filled(3, \'\'); // 定长填充\\nvar unmodifiable = List.unmodifiable([1,2]);// 不可变\\nvar generateList = List.generate(3, (i) => i*2);// 生成式\\n
\\nvar byteData = Uint8List(1024); // 指定元素类型\\nvar matrix = Float64List(9); // 高性能数值计算\\n
\\n// 索引操作伪代码实现\\nE operator [](int index) {\\n if (index < 0 || index >= _length) throw RangeError();\\n return _elementData[index];\\n}\\n
\\nclass _ListIterator<E> implements Iterator<E> {\\n final List<E> _list;\\n int _index = -1;\\n \\n E get current => _index >= 0 ? _list[_index] : null;\\n \\n bool moveNext() {\\n _index++;\\n return _index < _list.length;\\n }\\n}\\n
\\nList
内存管理机制// _GrowableList 扩容实现\\nvoid _grow(int newLength) {\\n if (newLength > _capacity) {\\n int newCapacity = _capacity * 2 + 1;\\n if (newCapacity < newLength) newCapacity = newLength;\\n _setCapacity(newCapacity);\\n }\\n _length = newLength;\\n}\\n
\\n初始容量 | 添加元素数 | 扩容次数 | 总拷贝元素量 |
---|---|---|---|
0 | 1000 | 11 | 2047 |
100 | 1000 | 5 | 1300 |
void main() {\\n var objects = [Object(), Object()];\\n objects.clear(); // 显式释放引用\\n // GC触发时回收内存\\n}\\n
\\nvar original = List.generate(1e6.toInt(), (i) => i);\\nvar sublist = original.sublist(100, 200); // 共享内存块\\n
\\nList
工程实践// 错误示范\\nfinal list = [];\\nfor (var i = 0; i < 1e6; i++) {\\n list.add(i); // 多次扩容\\n}\\n\\n// 优化方案\\nfinal list = List<int>.filled((1e6).toInt(), 0);\\nfor (var i = 0; i < 1e6; i++) {\\n list[i] = i;\\n}\\n
\\n// 低效方式\\nlist.addAll([a, b, c]);\\n\\n// 高效方式\\nlist.length += 3;\\nlist[list.length - 3] = a;\\nlist[list.length - 2] = b;\\nlist[list.length - 1] = c;\\n
\\n// 主Isolate\\nfinal sendPort = ...;\\nfinal sharedData = Uint8List(1024).buffer;\\nsendPort.send(sharedData);\\n\\n// 子Isolate\\nreceivePort.listen((message) {\\n final data = ByteData.view(message as ByteBuffer);\\n});\\n
\\nfinal immutableList = List.unmodifiable([1, 2, 3]);\\nfinal fastCopy = List.of(original); // 快速拷贝\\n
\\nList
设计哲学与系统思维List
设计的核心矛盾:
vs
运行时效率
vs
动态灵活
vs
稀疏存储
在Dart
集合体系中的定位:
Collection\\n├── List(有序可重复)\\n├── Set(无序唯一)\\n└── Map(键值对)\\n
\\n特性 | Dart | Java | Python |
---|---|---|---|
动态扩容 | 自动增长 | ArrayList 扩容 | list 自动扩展 |
内存布局 | 连续存储 | 数组实现 | 指针数组 |
类型安全 | 强类型可选 | 泛型擦除 | 动态类型 |
并发安全 | Isolate 隔离 | 同步锁 | GIL 全局锁 |
List
的深层理解是打开Dart
高性能编程之门的金钥匙。从虚拟机层面的连续内存布局,到语言特性中的扩展操作符;从看似简单的[]
操作符重载,到复杂的扩容策略算法,每个设计细节都体现着工程智慧。
在Flutter
开发实践中,List
的高效使用直接影响Widget
重建性能、滚动流畅度和内存占用。当开发者真正掌握List
的内存模型,就能预判List.generate
与普通循环创建的性能差异;当理解类型化数组的SIMD
优化潜力,就能在图像处理等场景释放硬件算力。
List
不仅是数据的容器,更是算法思维的训练场 —— 它教会我们如何在空间与时间、安全与效率、抽象与具象之间找到最佳平衡点。这种系统化认知,将帮助我们在面对复杂业务场景时,做出最符合Dart
语言哲学的设计决策。
\\n","description":"前言 List —— 数据流动的容器哲学。\\n\\n在Dart语言构筑的编程世界中,List不仅仅是简单的数据容器,它是动态集合的终极表达,是算法落地的物理载体,更是内存与CPU对话的微观剧场。从渲染Flutter界面时的Widget树管理,到处理百万级数据的科学计算,List的身影无处不在。这个看似平凡的线性结构,实则是Dart运行时最精妙的工程杰作 —— 它既保持着JavaScript数组般的开发友好性,又具备Java ArrayList级别的性能控制力。\\n\\n本文将带你穿越List的多维宇宙,从基础API的巧用到虚拟机堆内存的布局,从迭代器模式的实现到SIM…","guid":"https://juejin.cn/post/7470879836724297780","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-14T02:49:35.666Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9b40336a16d342c7b02e930016a553fe~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1740106175&x-signature=4MIxzq%2BFk137YXGkPVtL5IMG7n0%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"结合Riverpod的源码,详细解释工作实现原理","url":"https://juejin.cn/post/7470871481988497419","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
Riverpod 是一个 响应式的、基于依赖关系管理的 状态管理库,旨在解决 Provider 及其他状态管理方式中的一些问题,如:
\\nBuildContext
依赖在解读源码之前,我们可以先了解一下 Riverpod 的核心理念:
\\nProviderContainer
负责。watch()
时自动追踪依赖,确保状态更新时能正确通知相关组件。Provider
、StateProvider
、FutureProvider
等所有 Provider 都继承自 ProviderBase
,它是 Riverpod 状态管理的基石。
dart\\n复制编辑\\nabstract class ProviderBase<State> {\\n const ProviderBase();\\n\\n State create(ProviderElement<State> ref);\\n}\\n
\\n这个基类定义了 Provider 的基本行为:
\\ncreate()
方法:用于生成 State
。Provider
会通过 create()
定义自己的状态。dart\\n复制编辑\\nclass Provider<T> extends ProviderBase<T> {\\n Provider(this._create) : super();\\n\\n final Create<T, ProviderRef<T>> _create;\\n\\n @override\\n T create(ProviderElement<T> ref) {\\n return _create(ref);\\n }\\n}\\n
\\n_create
是用户定义的回调函数,用于创建 T
类型的状态。create()
只有在 ProviderContainer
请求该 Provider 的状态 时才会执行(懒加载)。dart\\n复制编辑\\nclass ProviderContainer {\\n final _providers = <ProviderBase, ProviderElement>{};\\n\\n T read<T>(ProviderBase<T> provider) {\\n return _readElement(provider).state;\\n }\\n\\n ProviderElement<T> _readElement<T>(ProviderBase<T> provider) {\\n return _providers.putIfAbsent(provider, () {\\n final element = provider.createElement();\\n element.mount();\\n return element;\\n }) as ProviderElement<T>;\\n }\\n}\\n
\\n_providers
:存储所有 Provider
及其 ProviderElement
(持有状态)。
read()
:返回 Provider
的状态,不会监听状态变化。
_readElement()
:
Provider
还未创建,则调用 createElement()
创建 ProviderElement
。mount()
使其进入运行状态(支持自动销毁)。总结:
\\nProviderContainer
维护 所有 Provider 的生命周期。ProviderContainer
通过 _providers
缓存已创建的 ProviderElement
,避免重复创建。在 Riverpod 组件(如 ConsumerWidget
)中,ref.watch()
用于监听 Provider
的状态。
dart\\n复制编辑\\nclass ConsumerWidget extends StatelessWidget {\\n @override\\n Widget build(BuildContext context, WidgetRef ref) {\\n final count = ref.watch(counterProvider);\\n return Text(\'Counter: $count\');\\n }\\n}\\n
\\n源码解析:
\\ndart\\n复制编辑\\nT watch<T>(ProviderBase<T> provider) {\\n final element = _readElement(provider);\\n element.addListener(_markNeedsNotify);\\n return element.state;\\n}\\n
\\nwatch()
会在 ProviderElement
上注册监听器,当 state
变化时:
build()
以更新 UI。ProviderElement
触发 notifyListeners()
。read()
用于获取 Provider
的值,但不会监听其变化。
dart\\n复制编辑\\nT read<T>(ProviderBase<T> provider) {\\n return _readElement(provider).state;\\n}\\n
\\n区别:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n方法 | 作用 | 是否监听变化 |
---|---|---|
watch() | 监听 Provider,UI 变化时重建 | ✅ |
read() | 仅获取当前值,不触发更新 | ❌ |
适用于: 存储简单的可变状态,如计数器。
\\ndart\\n复制编辑\\nfinal counterProvider = StateProvider<int>((ref) => 0);\\n
\\nStateProvider
返回 StateController<T>
,它封装了可变状态。源码:
\\ndart\\n复制编辑\\nclass StateProvider<T> extends ProviderBase<StateController<T>> {\\n StateProvider(this._create) : super();\\n\\n final Create<T, StateProviderRef<T>> _create;\\n\\n @override\\n StateController<T> create(ProviderElement<StateController<T>> ref) {\\n return StateController(_create(ref));\\n }\\n}\\n\\nclass StateController<T> {\\n StateController(this._state);\\n\\n T _state;\\n final _listeners = <void Function(T)>[];\\n\\n T get state => _state;\\n\\n set state(T newState) {\\n if (_state == newState) return;\\n _state = newState;\\n for (final listener in _listeners) {\\n listener(_state);\\n }\\n }\\n}\\n
\\nStateProvider
包装了 StateController<T>
,它:
_state
(实际存储的值)。state
变化时,通知所有监听者。Riverpod 会自动追踪 Provider 之间的依赖,如:
\\ndart\\n复制编辑\\nfinal providerA = Provider<int>((ref) => 10);\\nfinal providerB = Provider<int>((ref) => ref.watch(providerA) * 2);\\n
\\nproviderB
依赖 providerA
。providerA
变化时,providerB
也会自动更新。内部实现(简化版):
\\ndart\\n复制编辑\\nclass ProviderElement<T> {\\n final _listeners = <void Function(T)>[];\\n\\n void addListener(void Function(T) listener) {\\n _listeners.add(listener);\\n }\\n\\n void notifyListeners() {\\n for (final listener in _listeners) {\\n listener(state);\\n }\\n }\\n}\\n
\\nRiverpod 自动回收未使用的 Provider,防止内存泄漏。
\\ndart\\n复制编辑\\nvoid maybeDispose() {\\n if (_listeners.isEmpty) {\\n _dispose();\\n }\\n}\\n\\nvoid _dispose() {\\n state.dispose();\\n}\\n
\\nRiverpod 还支持异步数据流,如:
\\ndart\\n复制编辑\\nfinal futureProvider = FutureProvider<String>((ref) async {\\n await Future.delayed(Duration(seconds: 2));\\n return \'Data Loaded\';\\n});\\n
\\n底层维护 AsyncValue<T>
类型:
dart\\n复制编辑\\nclass AsyncValue<T> {\\n final T? value;\\n final bool isLoading;\\n final Object? error;\\n}\\n
\\n这样,UI 组件可以直接处理:
\\ndart\\n复制编辑\\nfinal asyncValue = ref.watch(futureProvider);\\nasyncValue.when(\\n data: (data) => Text(data),\\n loading: () => CircularProgressIndicator(),\\n error: (e, _) => Text(\'Error: $e\'),\\n);\\n
\\nProviderContainer
管理所有 Provider。这就是 Riverpod 的底层实现原理,它提供了一种安全、灵活、高效的状态管理方案。
","description":"Riverpod 是一个 响应式的、基于依赖关系管理的 状态管理库,旨在解决 Provider 及其他状态管理方式中的一些问题,如: 全局状态管理的安全性\\n懒加载与自动销毁\\n无 BuildContext 依赖\\n良好的组合性\\n更强的类型安全\\n异步支持\\n\\n在解读源码之前,我们可以先了解一下 Riverpod 的核心理念:\\n\\nProvider 只是一个工厂,它本身不会存储状态,状态的实际管理由 ProviderContainer 负责。\\nProvider 之间的依赖关系由 Riverpod 维护,在 watch() 时自动追踪依赖…","guid":"https://juejin.cn/post/7470871481988497419","author":"东大街","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-14T02:09:31.239Z","media":null,"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"详细介绍Riverpod 的使用","url":"https://juejin.cn/post/7470848139168596031","content":"Riverpod 是一个用于 Flutter 的状态管理库,它的设计理念和功能与 Provider(Flutter 之前的流行状态管理库)有相似之处,但 Riverpod 在实现上更为灵活、健壮,并且具备更好的扩展性。
\\nBuildContext
:与 Provider 不同,Riverpod 允许你在不依赖 BuildContext
的情况下访问和管理状态,这简化了许多常见的 Flutter 状态管理问题。Provider:
\\nProvider
有多种类型,可以提供不同的功能,如 StateProvider
、FutureProvider
、StreamProvider
等。Consumer:
\\nConsumer
是一个监听并重建 widget 的方法。当某个 Provider 的值发生变化时,Consumer
会自动重新构建依赖于该值的 widget。使用 Consumer
可以方便地将状态绑定到 widget 树中。Scoped Provider:
\\nStateNotifier:
\\nStateNotifier
是一个特殊的对象,可以用来封装和管理复杂的状态逻辑。它通常与 StateNotifierProvider
配合使用,来创建可变状态(例如改变计数器的值)。StateProvider:
\\nStateProvider
是用于创建基本的状态管理提供者,适合简单的状态管理场景。例如一个计数器或切换开关的状态管理。AsyncProvider:
\\nAsyncProvider
用于处理异步数据(如通过 HTTP 请求获取数据或执行其他异步操作)。它可以管理加载、成功、错误等状态,帮助你更好地处理异步任务。首先,创建一个 Riverpod Provider,并在 Widget 中使用它:
\\ndart\\n复制编辑\\nimport \'package:flutter_riverpod/flutter_riverpod.dart\';\\nimport \'package:flutter/material.dart\';\\n\\n// 定义一个Provider来管理一个整数状态\\nfinal counterProvider = StateProvider<int>((ref) => 0);\\n\\nvoid main() {\\n runApp(ProviderScope(child: MyApp()));\\n}\\n\\nclass MyApp extends ConsumerWidget {\\n @override\\n Widget build(BuildContext context, WidgetRef ref) {\\n // 获取Provider的值\\n final counter = ref.watch(counterProvider);\\n\\n return MaterialApp(\\n home: Scaffold(\\n appBar: AppBar(title: Text(\'Riverpod Example\')),\\n body: Center(\\n child: Text(\'$counter\', style: TextStyle(fontSize: 40)),\\n ),\\n floatingActionButton: FloatingActionButton(\\n onPressed: () {\\n // 更新Provider的状态\\n ref.read(counterProvider.notifier).state++;\\n },\\n child: Icon(Icons.add),\\n ),\\n ),\\n );\\n }\\n}\\n
\\n在这个简单的例子中,StateProvider<int>
管理了一个整数状态。当用户点击按钮时,状态增加 1,UI 会自动更新。
StateNotifier
来管理更复杂的状态dart\\n复制编辑\\n// 定义一个更复杂的状态管理\\nclass CounterNotifier extends StateNotifier<int> {\\n CounterNotifier() : super(0);\\n\\n void increment() => state++;\\n void decrement() => state--;\\n}\\n\\nfinal counterNotifierProvider = StateNotifierProvider<CounterNotifier, int>((ref) {\\n return CounterNotifier();\\n});\\n\\nclass MyApp extends ConsumerWidget {\\n @override\\n Widget build(BuildContext context, WidgetRef ref) {\\n final counter = ref.watch(counterNotifierProvider);\\n\\n return MaterialApp(\\n home: Scaffold(\\n appBar: AppBar(title: Text(\'Riverpod with StateNotifier\')),\\n body: Center(\\n child: Text(\'$counter\', style: TextStyle(fontSize: 40)),\\n ),\\n floatingActionButton: Row(\\n mainAxisAlignment: MainAxisAlignment.center,\\n children: [\\n FloatingActionButton(\\n onPressed: () => ref.read(counterNotifierProvider.notifier).decrement(),\\n child: Icon(Icons.remove),\\n ),\\n SizedBox(width: 20),\\n FloatingActionButton(\\n onPressed: () => ref.read(counterNotifierProvider.notifier).increment(),\\n child: Icon(Icons.add),\\n ),\\n ],\\n ),\\n ),\\n );\\n }\\n}\\n
\\n在这个例子中,StateNotifier
用于管理计数器状态,通过 StateNotifierProvider
提供给 UI。这样可以让状态更新的逻辑更加清晰和封装。
如果需要处理异步数据,可以使用 FutureProvider
或 StreamProvider
:
dart\\n复制编辑\\nfinal fetchDataProvider = FutureProvider<String>((ref) async {\\n await Future.delayed(Duration(seconds: 2));\\n return \'Data loaded!\';\\n});\\n\\nclass MyApp extends ConsumerWidget {\\n @override\\n Widget build(BuildContext context, WidgetRef ref) {\\n final asyncValue = ref.watch(fetchDataProvider);\\n\\n return MaterialApp(\\n home: Scaffold(\\n appBar: AppBar(title: Text(\'Riverpod Async Example\')),\\n body: Center(\\n child: asyncValue.when(\\n data: (data) => Text(data),\\n loading: () => CircularProgressIndicator(),\\n error: (error, stack) => Text(\'Error: $error\'),\\n ),\\n ),\\n ),\\n );\\n }\\n}\\n
\\nFutureProvider
用于处理异步任务,当数据加载完成时,UI 会自动更新。
Riverpod 的实现基于一个 ProviderContainer,它是所有 provider 的容器。ProviderContainer
会保存所有的 provider 的状态,并且负责通知相应的依赖项发生变化。每个 Provider
在 ProviderContainer
中都会有一个独立的状态。
状态管理:
\\nProviderContainer
是 Riverpod 的核心,用于存储 provider 的所有状态。每次请求一个 provider 时,Riverpod 会从容器中获取对应的值。依赖注入:
\\n状态清理:
\\n不可变状态:
\\nRiverpod 提供了一个非常强大和灵活的状态管理方案,它不仅支持 Flutter,还可以在 Dart 的任何应用中使用。通过提供类型安全、自动清理和强大的依赖管理机制,Riverpod 提升了 Flutter 应用的可维护性和可扩展性。
","description":"Riverpod 是一个用于 Flutter 的状态管理库,它的设计理念和功能与 Provider(Flutter 之前的流行状态管理库)有相似之处,但 Riverpod 在实现上更为灵活、健壮,并且具备更好的扩展性。 Riverpod 的主要特点\\n独立于 Flutter 框架:Riverpod 不依赖于 Flutter,理论上可以与任何 Dart 应用一起使用。它的核心完全独立于 Flutter,因此可以轻松地在不同的环境中使用,比如命令行应用、服务器端应用等。\\n类型安全:Riverpod 完全支持 Dart 的类型系统…","guid":"https://juejin.cn/post/7470848139168596031","author":"东大街","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-14T01:22:59.825Z","media":null,"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"系统化掌握Dart编程之异步编程(三):Future的快递包裹哲学","url":"https://juejin.cn/post/7470635421505536040","content":"Future
—— 程序世界的物流系统
。
想象你是一位忙碌的网购达人,每天要处理数十个快递包裹。在Dart
的异步世界里,Future
就像一个个等待派送的快递包裹:有的正在运输中(Pending
),有的已签收(Completed
),有的可能丢失(Error
)。理解Future
的运作规律,就像掌握快递公司的物流系统 —— 你知道包裹不会立即到达,但可以通过订单号追踪状态,设置签收后的自动操作,甚至处理异常情况。本文将用快递系统的运行逻辑,带你揭开Dart
异步编程的神秘面纱,从日常使用到底层原理,建立完整的认知体系。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\nFuture
基础:快递包裹的生命周期每个Future
都像快递包裹一样经历完整生命周期:
Pending
) :创建后等待处理
,如同商家打包商品。Completed
) :成功送达并返回结果
,对应包裹正常签收。Error
) :派送失败产生异常
,如同包裹丢失或损坏。// 下单购买商品(创建Future)\\nFuture<String> delivery = Future(() => \\"Dart编程指南\\");\\n\\n// 设置签收后的操作\\ndelivery.then((package) => print(\\"已签收:$package\\"));\\n\\n// 处理运输异常\\ndelivery.catchError((err) => print(\\"包裹异常:$err\\"));\\n
\\n多个then()
调用形成物流流水线,每个环节处理完的包裹会传递给下一站:
Future.value(\\"raw_package\\")\\n .then((pkg) => pkg + \\"_checked\\") // 质量检测站\\n .then((pkg) => pkg + \\"_wrapped\\") // 包装站\\n .then(print); // 最终输出:raw_package_checked_wrapped\\n
\\n当某个物流环节出错时,异常会沿着处理链向后传递,直到被捕获:
\\nFuture.error(\\"交通事故\\")\\n .then((_) => print(\\"此路段不会执行\\"))\\n .catchError((e) => print(\\"物流中心捕获异常:$e\\"))\\n .then((_) => print(\\"继续后续处理\\"));\\n
\\nDart
的事件循环就像24
小时运作的快递分拣中心,其处理流程分为两个优先级通道:
void showSortingProcess() {\\n print(\\"[早8:00] 分拣开始\\");\\n \\n // 普通包裹(事件队列)\\n Future(() => print(\\"[午12:00] 常规包裹派送\\"));\\n \\n // 加急包裹(微任务队列)\\n Future.microtask(() => print(\\"[早8:05] 加急包裹处理\\"));\\n \\n scheduleMicrotask(() => print(\\"[早8:03] 特快专递\\"));\\n \\n print(\\"[早8:01] 分拣结束\\");\\n}\\n\\n/* 输出结果揭示处理顺序:\\n[早8:00] 分拣开始\\n[早8:01] 分拣结束\\n[早8:03] 特快专递\\n[早8:05] 加急包裹处理\\n[午12:00] 常规包裹派送\\n*/\\n
\\n分拣中心严格遵守作业流程:
\\n分拣中心工作手册:\\n1. 优先处理所有加急包裹(微任务队列)\\n2. 每次只处理一个普通包裹(事件队列)\\n3. 重复上述过程直到所有包裹处理完毕\\n
\\n这种机制保证了UI渲染
等关键任务优先执行。假设每60帧
需要渲染一次界面(约16ms/帧
),微任务处理必须在一个帧间隔内完成,否则会导致界面卡顿。
Future
进阶:物流系统深度解析当创建Future
时,Dart
会生成一张物流订单:
void createDelivery() {\\n // 相当于生成订单号\\n Future<String> order = Future(() {\\n // 模拟商品生产耗时\\n return \\"定制商品\\"; \\n });\\n \\n // 添加物流追踪\\n order.then((goods) => print(\\"商品已生产:$goods\\"));\\n}\\n
\\n实际执行流程:
\\n事件队列
)。同步代码
)。then
回调)。查看Dart SDK
源码中的_Future
实现:
// 简化的内部状态管理\\nclass _Future<T> implements Future<T> {\\n static const int _stateIncomplete = 0;\\n static const int _stateComplete = 1;\\n static const int _stateError = 2;\\n \\n int _state = _stateIncomplete;\\n Object? _result;\\n List<_FutureListener>? _listeners;\\n}\\n
\\n状态转换示意图:
\\n 开始派送\\n ↓\\n 待处理(_stateIncomplete)\\n ↙ ↘\\n完成(_stateComplete) 异常(_stateError)\\n
\\n当调用then()
时,实际是在创建物流追踪节点:
void _then(_Future<T> parent, Function callback) {\\n final child = _Future<T>();\\n parent._listeners ??= [];\\n parent._listeners!.add(_FutureListener(callback, child));\\n}\\n
\\n每个_FutureListener
相当于物流转运站,负责将处理结果传递给下一站。当父Future
完成时,会依次通知所有监听器:
void _complete(T value) {\\n _state = _stateComplete;\\n _result = value;\\n for (var listener in _listeners!) {\\n _propagate(listener); // 触发下游处理\\n }\\n}\\n
\\nCompleter
)Completer
相当于自建物流公司,允许手动控制包裹状态:
void customDelivery() {\\n final completer = Completer<String>();\\n completer.future.then(print); // 注册签收回调\\n \\n // 模拟异步操作\\n Timer(Duration(seconds: 2), () {\\n completer.complete(\\"特别快递\\"); // 手动标记完成\\n });\\n}\\n
\\nFuture.wait
)同时处理多个包裹
时,使用集合运输策略:
Future<void> batchProcessing() async {\\n final deliveries = [\\n fetchUserInfo(),\\n loadProductList(),\\n checkInventory()\\n ];\\n \\n final results = await Future.wait(deliveries);\\n print(\'全部包裹已抵达:$results\');\\n}\\n
\\n对异常包裹进行智能重发:
\\nFuture<String> retryDelivery(int retries) async {\\n for (int i = 0; i < retries; i++) {\\n try {\\n return await fetchData();\\n } catch (e) {\\n print(\\"第${i+1}次派送失败,等待重试...\\");\\n await Future.delayed(Duration(seconds: 1));\\n }\\n }\\n throw \\"所有重试失败\\";\\n}\\n
\\n微任务队列相当于VIP
通道,滥用会导致普通包裹积压:
// 错误示例:加急包裹无限递归\\nvoid microtaskFlood() {\\n scheduleMicrotask(() {\\n processOrder();\\n microtaskFlood(); // 导致分拣中心瘫痪\\n });\\n}\\n
\\n减少不必要的包装中转:
\\n// 低效写法:多次中转\\nFuture<String> deliverGoods() {\\n return getAddress()\\n .then((addr) => findRoute(addr))\\n .then((route) => arrangeDelivery(route));\\n}\\n\\n// 高效写法:直达路线\\nFuture<String> deliverGoods() async {\\n final addr = await getAddress();\\n final route = await findRoute(addr);\\n return await arrangeDelivery(route);\\n}\\n
\\nZone
)建立全局监控中心:
\\nrunZonedGuarded(() {\\n Future.error(Exception(\\"包裹丢失\\"));\\n}, (error, stack) {\\n print(\\"监控中心捕获异常:$error\\");\\n reportToServer(error);\\n});\\n
\\n理解Future
就像掌握现代物流系统的运营法则。我们拆解了从包裹创建
、状态管理
到分拣派送
的完整流程,揭示了事件循环作为分拣中心的核心作用。通过Completer
实现自定义派送方案,利用Future.wait
优化批量运输,借助async/await
打造直达路线,这些技巧都能显著提升代码效率。
记住:微任务队列是VIP通道不可滥用
,错误处理是物流保险不可或缺
。当你能在脑海中清晰勾勒出Dart
的\\"物流网络\\"时,就具备了处理复杂异步场景的系统化思维能力。继续深入实践,您将成为驾驭异步编程的物流大师,让代码如同高效运转的智慧物流系统,精准可靠地抵达目标。
\\n","description":"前言 Future —— 程序世界的物流系统。\\n\\n想象你是一位忙碌的网购达人,每天要处理数十个快递包裹。在Dart的异步世界里,Future就像一个个等待派送的快递包裹:有的正在运输中(Pending),有的已签收(Completed),有的可能丢失(Error)。理解Future的运作规律,就像掌握快递公司的物流系统 —— 你知道包裹不会立即到达,但可以通过订单号追踪状态,设置签收后的自动操作,甚至处理异常情况。本文将用快递系统的运行逻辑,带你揭开Dart异步编程的神秘面纱,从日常使用到底层原理,建立完整的认知体系。\\n\\n操千曲而后晓声,观千剑而后识器…","guid":"https://juejin.cn/post/7470635421505536040","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-13T03:53:21.000Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/52fa46a35e7d4f57b10b53abbfddf367~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1740023601&x-signature=VddcPbyLSO62aw%2B4jud%2BSIPNCFU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/447bab7e06c841e3befe34107f131b9e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1740023601&x-signature=VrS2IESTJ3rL9yESZ4hIfOgcPcw%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"Flutter Gradle 命令式插件正式移除,你迁移旧版 Gradle 配置了吗?","url":"https://juejin.cn/post/7470457106844860455","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
在 Flutter 3.29 版本里官方正式移除了 Flutter Gradle Apply 插件,其实该插件自 3.19 起已被弃用,同时 Flutter 团队后续也打算把 Flutter Gradle 从 Groovy 转换为 Kotlin,并将其迁移到使用 AGP(Android Gradle Plugin)的公共 API,所以这个改动有望降低在发布新 AGP 版本时损坏的频率,并减少基于构建的回归。
\\n\\n\\n从这里也可以看出来,Flutter 团队也为 AGP 升级迭代适配感到“头痛”。
\\n
所以如果你的项目是在 3.16 版本之前创建,但一直尚未迁移,那么在 3.29 版本下肯定会受到直接影响,比如之前 Flutter 工具在构建项目时有警告:“You are applying Flutter\'s main Gradle plugin imperatively
”,那么基本可以确定会 3.29 版本会无法正常运行,开发者需要手动进行迁移。
首先要说一些额外前置关系,和本文没直接关联,适合在迁移时还有升级需求的,如果不感兴趣可以直接看第二部分,因为在 Android Gradle 里,AGP 相关升级可以说是 Android 开发者最头疼的问题之一,这里面除了涉及 JDK 、Gradle、AGP、Kotlin、KGP(Kotlin Gradle Plugin)等版本之外,甚至还和 Android Studio 的版本有关系,而 Android Studio 正式版又刚刚度过 10 周年, 种种因素之下,想要不那么难受的升级迁移,或者你需要简单理清下他们的版本对应关系。
\\n\\n\\n比如之前就出现过,由于某些官方的 androidx 开始升级到了 JDK 21 ,但是官方在旧版 AGP 中没有正确处理,从而引发如
\\nD8 Cannot invoke \\"String.length()\\" because \\"<parameter1>
等相关 issue 。
首先我们以 JDK 作为视角,简单看看 Android 构建中的 JDK 关系,大概可以知道 JDK 在 Gradle、Kotlin、Android Studio 和 AGP 里的角色:
\\n然后,我们再简单看看,在不同 Android Studio 里,默认自带的 JDK 版本是什么:
\\n接着,我们再看看 Android Studio 和 AGP 版本之间的对应关系:
\\n然后我们再看 Java version 和 Gradle 之间的版本对应关系:
\\n最后是 AGP 和 Gradle 版本之间的关系:
\\n到这里,我们可以直观知道,Gradle 版本其实和 Java 版本有关系的,而不同 Android Studio 默认自带的 JDK 版本是不同的,所以在迁移过程中,你需要确定:
\\n只有这四者之间版本范围合适,你才可以减少在迁移升级版本的过程中冲突踩坑,当然 Android Studio 内置的 JDK 版本是支持手动切换的 ,你可以在设置里手动下载想要的 JDK 版本:
\\n\\n\\n当然,如果你不用 Andriod Studio ,只用 VSCode 的话,那么就可以减少考虑 Android Studio 版本和内置 JDK 的问题。
\\n
接着,其实还有 KGP 、 Kotlin 和 AGP 的版本对应关系问题,因为在 Flutter 里,各种 Plugin 和主工程都可能有不同的 kotlin versoin:
\\n关于 KGP 、Gradle 和 AGP 的对应关系:
\\n可以看到,在选择对应 KGP 的时候,最好是在合适 AGP 范围内,不然编译可能也会出现意料之外的报错。
\\n从 Flutter 3.16 开始,官方就增加了使用 Gradle 的声明式插件 {} 块(也称为插件 DSL)应用插件的支持,而 DSL 会要求静态定义插件,这也是 plugins {}
块机制和传统 apply()
的差异之一,例如:
plugins{}
只能在项目的构建脚本 build.gradle(.kts)
和 settings.gradle(.kts)
文件中使用,并且它必须出现在任何其他块之前,同时不能在 script plugins 或 init 脚本中使用plugins {}
块不支持任意代码,它必须是无副作用,每次都产生相同的结果plugins{}
必须是构建脚本中的顶级语句,它不能嵌套在另一个结构中(如 if 语句或 for 循环)所以,迁移时,我们首先需要找到项目当前使用的 Android Gradle Plugin (AGP) 和 Kotlin 的值,一般都在 /android/build.gradle
文件的 buildscript
里,比如这里的 kotlin_version
和 com.android.tools.build:gradle
:
buildscript {\\n ext.kotlin_version = \'1.7.10\'\\n repositories {\\n google()\\n mavenCentral()\\n }\\n\\n dependencies {\\n classpath \'com.android.tools.build:gradle:7.3.0\'\\n classpath \\"org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version\\"\\n }\\n}\\n\\nallprojects {\\n repositories {\\n google()\\n mavenCentral()\\n }\\n}\\n\\nrootProject.buildDir = \'../build\'\\nsubprojects {\\n project.buildDir = \\"${rootProject.buildDir}/${project.name}\\"\\n}\\nsubprojects {\\n project.evaluationDependsOn(\':app\')\\n}\\n\\ntasks.register(\\"clean\\", Delete) {\\n delete rootProject.buildDir\\n}\\n
\\n接下来,需要将项目下 /android/settings.gradle
的内容替换为以下内容,这里的 {agpVersion}
和 {kotlinVersion}
就是前面原本的数值 :
pluginManagement {\\n def flutterSdkPath = {\\n def properties = new Properties()\\n file(\\"local.properties\\").withInputStream { properties.load(it) }\\n def flutterSdkPath = properties.getProperty(\\"flutter.sdk\\")\\n assert flutterSdkPath != null, \\"flutter.sdk not set in local.properties\\"\\n return flutterSdkPath\\n }()\\n\\n includeBuild(\\"$flutterSdkPath/packages/flutter_tools/gradle\\")\\n\\n repositories {\\n google()\\n mavenCentral()\\n gradlePluginPortal()\\n }\\n}\\n\\nplugins {\\n id \\"dev.flutter.flutter-plugin-loader\\" version \\"1.0.0\\"\\n id \\"com.android.application\\" version \\"{agpVersion}\\" apply false\\n id \\"org.jetbrains.kotlin.android\\" version \\"{kotlinVersion}\\" apply false\\n}\\n\\ninclude \\":app\\"\\n
\\n如果你还有一些其他参数配置,需要确保将它们放在 pluginManagement {}
和 plugins {}
块之后,正如前面所说, Gradle 强制要求不能将其他代码放在这些块之前。
接着,从 /android/build.gradle
中删除整个 buildscript
块:
默认情况下,android/build.gradle
文件应该只剩下这个样子:
allprojects {\\n repositories {\\n google()\\n mavenCentral()\\n }\\n}\\n\\nrootProject.buildDir = \'../build\'\\nsubprojects {\\n project.buildDir = \\"${rootProject.buildDir}/${project.name}\\"\\n}\\nsubprojects {\\n project.evaluationDependsOn(\':app\')\\n}\\n\\ntasks.register(\\"clean\\", Delete) {\\n delete rootProject.buildDir\\n}\\n
\\n接着你还需要对代码 android/app/build.gradle
进行一些调整,例如删除以下 2 个使用旧版命令式 apply 方法的代码块:
然后再次添加对应的插件,但这次使用 Plugin DSL 语法,同样需要在文件的最顶部:
\\nplugins {\\n id \\"com.android.application\\"\\n id \\"kotlin-android\\"\\n id \\"dev.flutter.flutter-gradle-plugin\\"\\n}\\n
\\n最后,如果您的 dependencies
块包含对 \\"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version\\"
的依赖项,还需要删除该依赖项:
以上是属于官方默认最简配置下的迁移,如果你还是用了其他 classpath
和 apply
模块,那么你还需要将他们都移除:
然后将它们添加到应用 android/settings.gradle
文件的 plugins
块里面:
plugins {\\n id \\"dev.flutter.flutter-plugin-loader\\" version \\"1.0.0\\"\\n id \\"com.android.application\\" version \\"{agpVersion}\\" apply false\\n id \\"org.jetbrains.kotlin.android\\" version \\"{kotlinVersion}\\" apply false\\n /// 这个\\n id \\"com.google.gms.google-services\\" version \\"4.4.0\\" apply false\\n /// 这个\\n id \\"com.google.firebase.crashlytics\\" version \\"2.9.9\\" apply false\\n}\\n
\\n并且在 android/app/build.gradle
同步添加:
plugins {\\n id \\"com.android.application\\"\\n id \\"dev.flutter.flutter-gradle-plugin\\"\\n id \\"org.jetbrains.kotlin.android\\"\\n /// 这个\\n id \\"com.google.gms.google-services\\"\\n /// 这个\\n id \\"com.google.firebase.crashlytics\\"\\n}\\n
\\n最后,以下是一个简单迁移后的 git diff patch 参考:
\\nIndex: android/app/build.gradle\\nIDEA additional info:\\nSubsystem: com.intellij.openapi.diff.impl.patch.CharsetEP\\n<+>UTF-8\\n===================================================================\\ndiff --git a/android/app/build.gradle b/android/app/build.gradle\\n--- a/android/app/build.gradle(revision 69dfe7ed0d762bfd35e470fc31d2aebf1e1690bf)\\n+++ b/android/app/build.gradle(revision 1adf2a436b02e7af99121553eb67d7880ad91571)\\n@@ -1,3 +1,9 @@\\n+plugins {\\n+ id \\"com.android.application\\"\\n+ id \\"kotlin-android\\"\\n+ id \\"dev.flutter.flutter-gradle-plugin\\"\\n+}\\n+\\n def localProperties = new Properties()\\n def localPropertiesFile = rootProject.file(\'local.properties\')\\n if (localPropertiesFile.exists()) {\\n@@ -6,14 +12,6 @@\\n }\\n }\\n \\n-def flutterRoot = localProperties.getProperty(\'flutter.sdk\')\\n-if (flutterRoot == null) {\\n- throw new GradleException(\\"Flutter SDK not found. Define location with flutter.sdk in the local.properties file.\\")\\n-}\\n-\\n-apply plugin: \'com.android.application\'\\n-apply plugin: \'kotlin-android\'\\n-apply from: \\"$flutterRoot/packages/flutter_tools/gradle/flutter.gradle\\"\\n apply from: \\"exported.gradle\\"\\n \\n android {\\n@@ -31,9 +29,9 @@\\n // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).\\n applicationId \\"com.shuyu.gsygithub.gsygithubappflutter\\"\\n minSdkVersion 21\\n- targetSdkVersion 31\\n+ targetSdkVersion 33\\n versionCode 54\\n- versionName \\"4.0.1\\"\\n+ versionName \\"5.0.0\\"\\n testInstrumentationRunner \\"androidx.test.runner.AndroidJUnitRunner\\"\\n }\\n \\n@@ -70,9 +68,4 @@\\n source \'../..\'\\n }\\n \\n-dependencies {\\n- implementation \\"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version\\"\\n- testImplementation \'junit:junit:4.12\'\\n- androidTestImplementation \'androidx.test:runner:1.1.1\'\\n- androidTestImplementation \'androidx.test.espresso:espresso-core:3.1.1\'\\n-}\\n+dependencies {}\\nIndex: android/build.gradle\\nIDEA additional info:\\nSubsystem: com.intellij.openapi.diff.impl.patch.CharsetEP\\n<+>UTF-8\\n===================================================================\\ndiff --git a/android/build.gradle b/android/build.gradle\\n--- a/android/build.gradle(revision 69dfe7ed0d762bfd35e470fc31d2aebf1e1690bf)\\n+++ b/android/build.gradle(revision 1adf2a436b02e7af99121553eb67d7880ad91571)\\n@@ -1,16 +1,3 @@\\n-buildscript {\\n- ext.kotlin_version = \'1.8.10\'\\n- repositories {\\n- google()\\n- jcenter()\\n- }\\n-\\n- dependencies {\\n- classpath \\"com.android.tools.build:gradle:7.0.3\\"\\n- classpath \\"org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version\\"\\n- }\\n-}\\n-\\n allprojects {\\n repositories {\\n google()\\nIndex: android/settings.gradle\\nIDEA additional info:\\nSubsystem: com.intellij.openapi.diff.impl.patch.CharsetEP\\n<+>UTF-8\\n===================================================================\\ndiff --git a/android/settings.gradle b/android/settings.gradle\\n--- a/android/settings.gradle(revision 69dfe7ed0d762bfd35e470fc31d2aebf1e1690bf)\\n+++ b/android/settings.gradle(revision 1adf2a436b02e7af99121553eb67d7880ad91571)\\n@@ -1,15 +1,25 @@\\n-include \':app\'\\n+pluginManagement {\\n+ def flutterSdkPath = {\\n+ def properties = new Properties()\\n+ file(\\"local.properties\\").withInputStream { properties.load(it) }\\n+ def flutterSdkPath = properties.getProperty(\\"flutter.sdk\\")\\n+ assert flutterSdkPath != null, \\"flutter.sdk not set in local.properties\\"\\n+ return flutterSdkPath\\n+ }()\\n \\n-def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()\\n+ includeBuild(\\"$flutterSdkPath/packages/flutter_tools/gradle\\")\\n \\n-def plugins = new Properties()\\n-def pluginsFile = new File(flutterProjectRoot.toFile(), \'.flutter-plugins\')\\n-if (pluginsFile.exists()) {\\n- pluginsFile.withReader(\'UTF-8\') { reader -> plugins.load(reader) }\\n+ repositories {\\n+ google()\\n+ mavenCentral()\\n+ gradlePluginPortal()\\n+ }\\n }\\n \\n-plugins.each { name, path ->\\n- def pluginDirectory = flutterProjectRoot.resolve(path).resolve(\'android\').toFile()\\n- include \\":$name\\"\\n- project(\\":$name\\").projectDir = pluginDirectory\\n+plugins {\\n+ id \\"dev.flutter.flutter-plugin-loader\\" version \\"1.0.0\\"\\n+ id \\"com.android.application\\" version \\"7.0.3\\" apply false\\n+ id \\"org.jetbrains.kotlin.android\\" version \\"1.8.10\\" apply false\\n }\\n+\\n+include \\":app\\"\\n\\\\ No newline at end of file\\n\\n
\\n在聊 Dart 3.7 发布的之前,就不得不提 Dart 宏功能推进暂停 ,在春节期间,Dart 团队决定,由于宏的性能具体目标还太遥远,团队决定把当前的实现回归到编辑(例如静态分析和代码完成)和增量编译(热重载的第一步)上。
\\n关于数据支持,具体在于重新投资Dart 中的数据实现,因为这也是Dart & Flutter issue 里请求最多的问题,事实上一开始 Dart 对宏支持的主要动机,也是为了提供更好的数据序列化和反序列化,但是目前看来,通过更多定制语言功能来实现这一点更加实际。
\\n另外,Dart 团队目前已经在研究改进 build_runner 性能和推出 augmentations language feature (可能以略有不同的形式)支持,最终目的是找到更直接和方便的方法,来支持建模数据以及处理序列化和反序列化适配。
\\n回到 Dart 3.7 的功能,在聊 Dart 3.6 Digit separators 支持的时候我们提到过,Dart 3.6 允许使用下划线 (_) 作为数字分隔符,这有助于使长数字的字面量更具可读性,例如多个连续的下划线表示更高级别的分组:
\\n1__000_000__000_000__000_000\\n0x4000_0000_0000_0000\\n0.000_000_000_01\\n0x00_14_22_01_23_45\\n
\\n而 (_) 在 Dart 3.7 的局部变量和参数会变成非绑定的状态,因此可以在同一个范围中声明它们任意多次,而不会发生冲突:
\\n在 Dart 中,如果回调的主体实际上不需要使用参数,一般大家都会使用 _
作为回调参数的名称,但是如果回调有多个不需要使用的参数,过去可能会命名为 _
、__
、___
等,否则名称会发生冲突。
而在 Dart 3.7 中, _
的参数和局部变量实际上不会再创建变量,因此不存在名称冲突的可能性,当然,下面这种写法也就不生效了:
var [1, 2, 3].map((_) {\\n return _.toString();\\n // ^ Error! Reference to unknown variable.\\n});\\n
\\n\\n\\n类似的在 patterns 也是同样的道理:
\\nvar [_, _, third, _, _] = [1, 2, 3, 4, 5];
Dart 3.7 包含了重新编写的 Dart 格式化(dart format
),并采用了新的格式样式。
新样式看起来类似于向参数列表添加尾随逗号时获得的样式,不同之处在于现在格式化程序将为开发者添加和删除这些逗号:
\\n// Old style:\\nvoid writeArgumentList(\\n Token leftBracket, List<AstNode> elements, Token rightBracket) {\\n writeList(\\n leftBracket: leftBracket,\\n elements,\\n rightBracket: rightBracket,\\n allowBlockArgument: true);\\n}\\n\\n// New style:\\nvoid writeArgumentList(\\n Token leftBracket,\\n List<AstNode> elements,\\n Token rightBracket,\\n) {\\n writeList(\\n leftBracket: leftBracket,\\n elements,\\n rightBracket: rightBracket,\\n allowBlockArgument: true,\\n );\\n}\\n\\n
\\n而目前,对于格式样式的处理取决于要格式化的代码的语言版本:
\\n另外,为了确定格式化的每个文件的语言版本,dart format
会寻找一个 package_config.json
文件,这意味着开发者需要在格式化 package 代码之前运行 dart pub get
。
\\n\\n在未来,当大多数生态系统都在 3.7 或更高版本上时,对旧样式的支持将被删除。
\\n
同时,dart format 对于新样式包含了一些期待已久的功能,例如:
\\n项目范围的页面宽度配置 ,现在开发者可以在 analysis_options.yaml
文件中,配置项目范围内的首选格式页面宽度:
formatter:\\n page_width: 123\\n
\\n从格式设置中选择退出代码区域,开发者可以使用一对特殊标记注释将代码区域从自动格式化中剔除:
\\n main() {\\n // dart format off\\n no + formatting + here;\\n // dart format on\\n }\\n
\\n另外,格式化程序不再支持 dart format --fix
,相反会使用 dart fix
可以直接触发 dart format
处理所有修复。
在 Dart 3.7 中增加了新的 lints ,一个值得注意的新增功能是 unnecessary_underscores
这个 lint,它支持新的通配符变量功能。
另外,在 Dart 3.7 包含的大量新的快速修复和帮助支持,包括如缺少 await
关键字、不正确的导入前缀以及违反 lint 规则(如 cascade_invocations
)等。
还有一些方便代码重构辅助的工具,例如将 else
块转换为 else if
,以及使用 Expanded
或 Flexible
包装 Flutter widget 等。
\\n\\n\\n
由于现在主要的 JavaScript 库是 dart:js_interop
,而对于浏览器 API 是 package:web
,所以在 Dart 3.7 版本里,有 7 个 Dart SDK 库被弃用:
dart:html
dart:indexed_db
dart:js
dart:js_util
dart:web_audio
dart:web_gl
去年12 月 Dart 在 pub.dev 上启动了 package 的下载计数,而本次调整扩展了这一功能,现在支持查看每个 package 版本的下载计数。
\\n通过这些数据,可以提示包的使用者中有多少人已升级到最新版本(或仅升级到最新的主要版本),旨在帮助包作者衡量将修复程序向后移植到包的较旧主要版本的价值。
\\n同时 pub.dev 正式支持暗模式:
\\n另外,针对 topic 搜索支持,现在 pub.dev 上的搜索关键词推出了一个类似 IDE 的自动完成器,用户可以通过按 ctrl+space 来触发它,也可以在键入匹配的前缀(如 “topic:” 或 “license:”)时自动触发它。
\\n目前来看,Dart 3.7 属于“平平无奇”,和“带着大坑”的 Flutter 3.29 不同,升不升级影响不大,最多也只是能不能体验全新的格式化支持而已。
\\nFlutter 3.29 正式发布,如果不出意外,这将是一个带着“大坑”到来的版本,因为该版本带来了很多「重大调整或者弃用」 ,所以如果你想升级到 3.29,还需要多慎重了解这次升级到底更新了什么。
\\n之所以说带着“大坑”,主要是 3.29 更新带来了太多“意料之外”的东西,例如:
\\n相信大家从上述描述里应该可以感受到 3.29 潜在的“威能”,那么下面就让我们看看 3.29 给我们带来了什么更新吧。
\\n和 3.27 那会一样, Flutter iOS 的 PM 回归后, 每次更新都会包含不少 Cupertino 的身影,而本次 3.29 开始 CupertinoNavigationBar
和 CupertinoSliverNavigationBar
将支持 bottom widget 配置,一般会用于搜索字段或分段控件的场景。
例如在 CupertinoSliverNavigationBar
里现在可以使用 bottomMode 属性配置底部 Widget,从而支持自动调整大小直到隐藏,或者始终在导航栏滚动时显示:
\\n\\nFlutter 在 Cupertino 的高保真支持上力度上,从近来的几个版本都可以明显感受到。
\\n
其他导航栏的更新包括:
\\n当部分滚动时,CupertinoSliverNavigationBar
可以在展开和折叠状态之间对齐
新的 CupertinoNavigationBar.large
构造函数支持静态导航栏显示大标题
Cupertino popups 窗口支持更生动的背景模糊效果,从而提高了 Cupertino 风格的保真度:
\\n新的 CupertinoSheetRoute
可以使用拖动手势将其移除,同时还提供了新的 showCupertinoSheet
:
CupertinoAlertDialog
改进了在深色模式下的 native 保真度:
在文本选择时,反转选择后,Flutter 的文本选择手柄在 iOS 上会交换它们的顺序,并且文本选择放大镜的边框颜色现在会和当前主题匹配:
\\n\\n不得不说最近几个版本都有关于文本选择的更新内容,也许不久之后,Flutter 文本处理能力的短板会被补齐。
\\n
谷歌对于 Material 设计规范的跟进依然还是「迷之聚焦」,这对于国内开发者而言不知是福是祸, 3.29 提供了新的 Material 3 页面过渡构建器 FadeForwardsPageTransitionsBuilder
,主要是为了匹配 Android 的最新页面过渡行为。
在页面跳转过渡期间,页面会从右向左滑动淡入,退出页面会从右向左滑动淡出,这个新过渡还解决了以前由 ZoomPageTransitionsBuilder
导致的性能问题:
此外,3.29 还更新了 CircularProgressIndicator
和 LinearProgressIndicator
,从而适配 Material 3 规范,如果要使用更新的样式,需要将 year2023
属性设置为 false
,或者将 ProgressIndicatorThemeData.year2023 设置为 false
:
\\n\\n\\n
year2023
属于将 Deprecated 的字段。
在 3.29 还引入了 M3 最新的 Slider
设计样式,Slider
默认为以前的 Material 3 样式,如果要启用最新设计,同样需要将 year2023
设置为 false
,或将 SliderThemeData.year2023
设置为 false
:
最后,3.29 里还包含了 Material library 的多个错误修复和功能增强,例如:
DropdownMenu.onSelected
回调TabBar
弹性动画 RangeSlider
的 thumb 对齐方式,包括 divisions 、padding 和圆角mouseCursor
属性已添加到 Chip
、Tooltip
和 ReorderableListView
里面,从而允许在悬停时自定义鼠标光标近来几个版本 Framework 更新都会包含文本选择的更新,比如之前各种手势和鼠标触发的选择效果等,而 3.29 现在通过 SelectionListener
和 SelectionListenerNotifier
提供了有关 SelectionArea
或 SelectableRegion
下的文本选择信息:
SelectionDetails
(通过 SelectionListenerNotifier
获得)提供了所选内容的开始和结束偏移量(相对于 wrapped 的子树),并提示所选内容是否存在以及是否已折叠:
另外还可以通过 SelectableRegionSelectionStatusScope
获取 SelectionArea
或 SelectableRegion
状态的信息,例如通过使用 SelectableRegionSelectionStatusScope.maybeOf(context)
检查SelectableRegionSelectionStatus
状态:
class MySelectableText extends StatefulWidget {\\n const MySelectableText({super.key, required this.selectionNotifier, required this.onChanged});\\n\\n final SelectionListenerNotifier selectionNotifier;\\n final ValueChanged<SelectableRegionSelectionStatus> onChanged;\\n\\n @override\\n State<MySelectableText> createState() => _MySelectableTextState();\\n}\\n\\nclass _MySelectableTextState extends State<MySelectableText> {\\n ValueListenable<SelectableRegionSelectionStatus>? _selectableRegionScope;\\n\\n void _handleOnSelectableRegionChanged() {\\n if (_selectableRegionScope == null) {\\n return;\\n }\\n widget.onChanged.call(_selectableRegionScope!.value);\\n }\\n\\n @override\\n void didChangeDependencies() {\\n super.didChangeDependencies();\\n _selectableRegionScope?.removeListener(_handleOnSelectableRegionChanged);\\n _selectableRegionScope = SelectableRegionSelectionStatusScope.maybeOf(context);\\n _selectableRegionScope?.addListener(_handleOnSelectableRegionChanged);\\n }\\n\\n @override\\n void dispose() {\\n _selectableRegionScope?.removeListener(_handleOnSelectableRegionChanged);\\n _selectableRegionScope = null;\\n super.dispose();\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return SelectionListener(\\n selectionNotifier: widget.selectionNotifier,\\n child: const Text(\'This is some text under a SelectionArea that can be selected.\'),\\n );\\n }\\n}\\n
\\n3.29 改进了多个 Material 控件里 Accessibility 的用户体验:
\\n在去年发布 wasm 时,Flutter 在 Web 上的 WebAssembly (wasm) 支持要求开发者使用特殊的 HTTP 响应 headers 来托管 Flutter 应用,而现在相关要求在 3.29 开始放宽。
\\n现在使用默认 headers 可以允许应用使用 wasm 运行,但仅限于单个线程,而更新 headers 可以让 wasm 构建的 Flutter Web 应用使用多个线程运行。
\\n同时 3.29 修复了 WebGL 后端上图像的几个问题:
\\n\\n另外补充一点,关于 Flutter Web 在 WebAssembly 上的 SEO 优化支持,官方正在研究一种使用Flutter 语义树来适配的场景 :
\\nFlutter Web 在 3.29 还允许开发者更好地控制图像在 Web 上的显示方式,过去 Image 会在发生 CORS 错误时自动使用 <img>
元素来显示来自 URL 的图像,这可能会导致出现一些不一致的行为。
而现在 webHtmlElementStrategy
标志允许开发者选择何时使用 <img>
元素,虽然默认情况下自动回退处于禁用状态,不过仍然可以标志启用回退,甚至根据应用场景确定 <img>
元素的优先级。
3.29 修复了不少 Vulkan 问题,包活:
\\n在 3.29 里,没有 Vulkan 驱动的 Android 设备将回退到在 OpenGLES 上运行的 Impeller,而不是使用 Skia,默认情况下该行为处于启用状态,无需配置。
\\n\\n\\n这样 Flutter 将实现 100% 支持 Android 上的 Impeller。
\\n
和去年提到的 roadmap 一样,现在 iOS 后端已删除 Skia 支持,并且 FLTEnableImpeller
选择退出标志不再有效,随着 Flutter 开始从 iOS 版本中删除 Skia ,预计在未来版本中会进一步减小二进制文件大小。
从 3.29 开始,显示多个背景滤镜的应用现在可以使用新的 BackdropGroup
和新的 BackdropFilter.grouped
,通过这些 Widget 可以提高多个模糊的性能:
Widget build(BuildContext context) {\\n return BackdropGroup(\\n child: ListView.builder(\\n itemCount: 60,\\n itemBuilder: (BuildContext context, int index) {\\n return ClipRect(\\n child: BackdropFilter.grouped(\\n filter: ui.ImageFilter.blur(\\n sigmaX: 40,\\n sigmaY: 40,\\n ),\\n child: Container(\\n color: Colors.black.withOpacity(0.2),\\n height: 200,\\n child: const Text(\'Blur item\'),\\n ),\\n ),\\n );\\n }\\n ),\\n );\\n\\n
\\n\\n\\n上述代码展示了如何使用 BackdropGroup 让每个列表项都有一个有效的模糊效果,并且引擎将只执行一次背景模糊,但结果在视觉上与多次模糊相同。
\\n
在 3.29 里,如果这些 Backdrop filter 控件都共享一个共同的 BackdropKey,那么 Flutter 引擎就可以将多个背景过滤器组合到一个渲染操作中。
\\nBackdrop Key 可以唯一标识背景过滤器的输入,当共享时表示执行一次过滤,这可以显著减少在场景中使用多个背景滤镜的开销。
\\n对应的 Key 可以通过 backdropKey
构造函数参数手动提供,也可以通过 .grouped
构造函数从[BackdropGroup] 中查找。
新的 ImageFilter
构造函数允许将自定义着色器应用于任何子 Widget ,这和 package:flutter_shaders
中的 AnimatedSampler
功能类似,不同之处在于它还适用于 backdrop filters 。
众所周知,一直以来 Flutter 的 Dart UI Thread 和 Android/iOS 平台的 UI Thread 是不同线程,这在 《深入理解 Dart 异步实现机制》 和过去我们聊 background isolate 得时候聊过,而独立的 Dart UI 线程的主要目的之一防止阻塞平台 UI 线程。
\\n但是由于 Flutter 是在与 native 主线程不同的 Thread(UI 线程)上执行 Dart 代码,所以会出现 Dart 和平台互相调用时需要序列化和异步消息传递,这意味着:
\\n\\n\\nFlutter 和 Native 之间需要使用 platform channels 来封装对平台线程的调用,而不是能够直接从 Dart 调用 API(即通过 FFI),这让一些简单的同步调用增加了性能损耗和无意义的异步行为。
\\n
而从 3.29 开始,Android 和 iOS 上的 Flutter 将在应用的主线程上执行 Dart 代码,并且不再有单独的 Dart UI 线程。
\\n\\n\\n这是改进移动平台上 Native 和 Dart 互操作系列调整中的第一部分,因为它将允许对平台进行同步调用和从平台进行同步调用,并且不会产生序列化和消息传递的开销。
\\n
而当双方处于同一个线程下时,同步响应和调用可以更好处理一些平台事件处理、文本输入、插件调用和辅助功能等。
\\n\\n\\n特别是在对于
\\nPlatformView
混合渲染等场景,如果处于同一线程之上,那么一些场景下的PlatformView
由于不同线程导致的闪烁或者同步问题或者也可以得到改善。
当然, Eric 也在 150525#issuecomment-2652547816 提到合并线程后的忧虑,比如 Dart 和 Native 平台同一线程之后,那么「滚动/动画」是否会因此出现相互影响,特别是第三方插件处理不当的时候,反而更加卡顿的情况。
\\n当然,在整个 Flutter 团队的目标里,完全剔除 platform/message channels 是必然的方向,未来整个异步 channel 肯定会被彻底“消灭” 。
\\n默认情况下,3.29 下所有用户都启用了新的 DevTools inspector,新的 inspector 具有一个精简的 Widget Tree 和一个全新的 Widget 属性视图,以及一个自动更新以响应热重载和导航事件的选项。
\\n通过全新的 inspector ,开发者可以更直观地调试布局问题和诊断布局问题:
\\n如果你想关闭它,目前可以从 Inspector settings 对话框中禁用它:
\\n\\n\\n\\n
从 DevTools 检查器启用 widget 选择模式后,设备上的任何选择都被视为 widget 选择。
\\n以前在初始 Widget 选择后,开发者需要单击设备上的 Select widget 按钮,然后选择另一个小组件,而现在有一个设备上的按钮,可用于快速退出 Widget 选择模式:
\\nDevTools 中的 Logging 工具在 3.29 更新中得到了一些改进:
\\n本次重大更改和弃用影响范围还是比较大的。
\\n以下过去由官方提供的 package ,计划在 2025 年 4 月 30 日后停止支持,关于这些包后续可由第三方协调建立和维护分叉:
\\n3.29 移除了 Flutter Gradle 插件,这个在很久之前就提到了,该插件其实自 3.19 起已被弃用,后续将把 Flutter Gradle 插件从 Groovy 转换为 Kotlin,并将其迁移到使用 AGP 公共 API,这个改动有望降低发布新 AGP 版本时损坏的频率,并减少基于构建的回归。
\\n\\n\\n不得不说,现在 Android 平台自己的 AGP 兼容问题越来越麻烦,坑越来越多。
\\n
如果是在 3.16 之前创建但尚未迁移的项目可能会受到影响,比如 Flutter 工具在构建项目时会有警告:“You are applying Flutter\'s main Gradle plugin imperatively
”,则基本可以确定会受到这个变动的影响,开发者需要根据 docs.flutter.dev 上提供的方式进行迁移。
\\n\\n\\n
这个在之前的 《Flutter Web 正式移除 HTML renderer,只支持 CanvasKit 和 SkWasm》 已经聊到过,而从 3.29 开始, HTML renderer 就被正式移除了。
\\n同时正如前面所说的,一些由于 WebAssembly 带来的缺失也逐步完善,例如 CORS images 和通过语义树来适配的 SEO 等场景都在补齐:
\\n作为 Material 中正在进行的主题规范化项目的一部分,3.29 弃用 ThemeData.dialogBackgroundColor
并迁移到了 DialogThemeData.backgroundColor
,开发者可以使用 dart fix
命令迁移受影响的代码。
同样在 Material 中,ButtonStyleButton
iconAlignment
属性在添加到 ButtonStyle
和关联的 styleFrom
方法后已被弃用。
最后,不得不提提及 PC 端多窗口支持的进展,在去年的 《Flutter PC 多窗口新进展,已在 Ubuntu/Canonical 展示》 官方已经向我们展示了多窗口的可行性,而从 #142845 看目前推进的进度还可以:
\\n而从 #30701 可以看到,不久之后对应的 PR 草稿将正式开始进行审查,所以相信今年应该可以看到期盼已久的多窗口落地:
\\n关于 Dart 宏支持部份,正如 《Flutter 新春第一弹,Dart 宏功能推进暂停,后续专注定制数据处理支持》 所说,由于宏的性能具体目标还太遥远,Flutter 团队决定把当前的实现回归到编辑(例如静态分析和代码完成)和增量编译(热重载的第一步)上,并且具体在于重新投资Dart 中的数据支持,从聚焦于而优化数据序列化和反序列化问题。
\\n可以看到 ,Flutter 3.29 带来了不少新功能的同时,也引入了不少大变动,所以如果你想将生产项目升级到 3.29 ,那么在「稳定」和「可控」评估上就需要更加谨慎,至少也要等到 3.29.3
再行动不迟。
那么,勇士们准备好吃螃蟹了吗?
\\n异步模型 —— 单线程下实现并发的核心机制
。
在传统认知中,多线程似乎是并发的唯一解,但Dart
却以单线程+事件循环的设计,实现了媲美多线程的高效异步。这看似“反直觉”
的方案背后,隐藏着精妙的设计哲学:通过有序的任务调度替代无序的资源竞争
。单线程避免了多线程的内存隔离
、锁机制
和上下文切换成本
,却带来了新的思考——如何用一根“单线”
串起网络请求
、UI渲染
、用户交互
等海量事件?
本章将揭开Dart
事件循环的神秘面纱,你将会看到:微任务如何“插队”
实现即时响应,事件队列如何像传送带
般有序运转,以及单线程模型下如何避免“卡顿陷阱”
。理解这些机制,才能真正写出既高效又优雅的异步代码。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\nDart
主线程(Isolate
)仅有一个执行线程,但通过以下机制实现异步:
I/O
操作:所有 I/O
操作(文件
、网络
)均委托给操作系统异步执行
。Event Loop
) :持续监控任务队列,调度异步任务执行
。任务主动让出控制权,避免长时间阻塞
。与Java
的多线程模型不同,Dart
选择单线程的考量在于:
复杂的锁机制
。线程上下文切换
的成本消失。共享状态同步
。Dart
的单线程模型通过事件循环(Event Loop
)实现并发,其核心结构如下:
[微任务队列] -> [事件队列]\\n ↓ ↓\\n (全部处理) (每次处理一个)\\n ↖_________↙\\n
\\nMicrotask Queue
) :存放需要立即处理的紧急任务,如状态更新
。Event Queue
) :处理I/O
、计时器
、用户交互
等常规事件。事件循环是异步模型的核心
,其工作原理如下图所示:
代码示例:
\\nvoid eventLoop() {\\n while (true) {\\n if (微任务队列.isNotEmpty) {\\n 执行所有微任务();\\n } else if (事件队列.isNotEmpty) {\\n 处理一个事件();\\n } else {\\n 等待新事件();\\n }\\n }\\n}\\n
\\n队列类型 | 优先级 | 典型用例 |
---|---|---|
微任务队列 | 最高 | Future.then , scheduleMicrotask |
事件队列 | 次高 | I/O 回调、Timer 、手势事件 |
┌───────────────────────┐\\n│ 同步代码 │ ← 立即执行,可阻塞线程\\n├───────────────────────┤\\n│ 微任务队列 │ ← 每轮事件循环优先清空\\n├───────────────────────┤\\n│ Animation回调 │ ← Flutter渲染管线专用\\n├───────────────────────┤\\n│ 事件队列 │ ← I/O、Timer、用户事件\\n├───────────────────────┤\\n│ Isolate通信任务 │ ← 跨线程消息传递\\n└───────────────────────┘\\n
\\n代码示例:
\\nvoid main() {\\n print(\'Main Start\');\\n\\n // 事件队列任务\\n Future(() => print(\'Event Task 1\'));\\n \\n // 微任务队列任务\\n scheduleMicrotask(() => print(\'Microtask 1\'));\\n\\n Future(() => print(\'Event Task 2\'))\\n .then((_) => print(\'Microtask 2\'));\\n\\n print(\'Main End\');\\n}\\n\\n输出顺序为:\\nMain Start\\nMain End\\nMicrotask 1\\nMicrotask 2\\nEvent Task 1\\nEvent Task 2\\n
\\n执行规律:
\\n\\n\\n1、同步代码优先执行。
\\n2、微任务队列全部清空。
\\n3、每次处理一个
\\n事件队列任务
。4、每个事件任务完成后
\\n再次检查微任务队列
。
Future
的深度解析\\n\\n未完成(
\\nUncompleted
) → 已完成(Completed
) (成功或失败
)
状态转换通过 Completer
控制:
final completer = Completer<String>();\\ncompleter.complete(\'Result\'); // 成功\\ncompleter.completeError(\'Error\'); // 失败\\n
\\nFuture(() => print(\'Event Queue 1\'));\\nscheduleMicrotask(() => print(\'Microtask 1\'));\\nFuture.microtask(() => print(\'Microtask 2\'));\\n
\\n输出顺序:
\\nAsync/Await
的编译转换代码转换示例
\\n原始代码:
\\nFuture<String> fetchData() async {\\n var data = await http.get(\'url\');\\n return process(data);\\n}\\n
\\n等效转换:
\\nFuture<String> fetchData() {\\n return http.get(\'url\').then((data) {\\n return process(data);\\n });\\n}\\n
\\n关键实现细节
\\nawait
处生成隐式 then()
调用。隐式状态机
保存局部变量和执行位置。多层异步错误处理:
\\nFuture<void> deepAsync() async {\\n try {\\n await Future.error(\'Deep error\');\\n } catch (e) {\\n print(\'Inner catch: $e\');\\n throw \'Wrapped error\';\\n }\\n}\\n\\ndeepAsync().catchError((e) => print(\'Outer catch: $e\'));\\n
\\n错误处理策略:
\\nZone
机制:创建错误处理上下文。runZonedGuarded
捕获未处理异常。Future
链向后传递。void processLargeData(List data) {\\n if (data.isEmpty) return;\\n \\n processChunk(data.take(100));\\n scheduleMicrotask(() => processLargeData(data.skip(100)));\\n}\\n
\\nTimer.run
替代 Future((){})
。Stream
的 debounce
/throttle
。async/await
消除回调// 传统回调写法\\ngetUserData((user) {\\n getOrders(user.id, (orders) {\\n showOrders(orders);\\n });\\n});\\n\\n// 现代写法\\nvoid loadData() async {\\n var user = await getUserData();\\n var orders = await getOrders(user.id);\\n showOrders(orders);\\n}\\n
\\n// 传统方式\\nfetchData().then(handleSuccess).catchError(handleError);\\n\\n// 现代方式\\ntry {\\n var data = await fetchData();\\n handleSuccess(data);\\n} catch (e) {\\n handleError(e);\\n}\\n
\\n// 顺序执行:总耗时 1+2=3秒\\nvar a = await task1(); // 1秒\\nvar b = await task2(); // 2秒\\n\\n// 并行执行:总耗时 max(1,2)=2秒\\nvar a = task1();\\nvar b = task2();\\nawait Future.wait([a, b]);\\n
\\nIsolate
的协作模式虽然事件循环擅长I/O
密集型任务,但面对图像处理等CPU
密集型操作时,需要使用Isolate
:
// 主 Isolate\\nawait Isolate.spawn(heavyComputation, data)\\n .then((result) => handleResult(result));\\n\\n// 计算 Isolate\\nvoid heavyComputation(SendPort port) {\\n // ... 复杂计算 ...\\n port.send(result);\\n}\\n
\\n每个Isolate
拥有独立内存堆,通过消息传递通信,既保持并发能力,又避免共享状态问题。
FSM
)模式// 示例1 \\nenum DownloadState { idle, loading, completed }\\nfinal stateNotifier = ValueNotifier<DownloadState>(DownloadState.idle);\\n\\nFuture<void> downloadFile() async {\\n stateNotifier.value = DownloadState.loading;\\n try {\\n await _startDownload();\\n stateNotifier.value = DownloadState.completed;\\n } catch (e) {\\n stateNotifier.value = DownloadState.idle;\\n }\\n}\\n\\n// 示例2\\nenum TaskState { idle, loading, success, failure }\\n\\nclass AsyncTask {\\n final _state = ValueNotifier<TaskState>(TaskState.idle);\\n \\n Future<void> execute() async {\\n _state.value = TaskState.loading;\\n try {\\n await _doWork();\\n _state.value = TaskState.success;\\n } catch (_) {\\n _state.value = TaskState.failure;\\n }\\n }\\n}\\n
\\nfinal taskQueue = StreamController<Function>.broadcast();\\nfinal workerSubscription = taskQueue.stream\\n .asyncMap((task) => task())\\n .listen((_) {});\\n\\n// 提交任务\\ntaskQueue.add(() => processData());\\n
\\n问题现象 | 根本原因 | 解决方案 |
---|---|---|
UI 卡顿 | 同步阻塞事件循环 | 分解任务为异步步骤 |
内存泄漏 | 未取消 Stream 订阅 | 使用 StreamSubscription 管理 |
状态不一致 | 异步操作时序不可控 | 使用 StateNotifier 统一管理 |
理解Dart
的异步模型是掌握异步编程的基础:
微任务
与事件任务
的使用场景。任务类型
选择合适并发方案。记住:好的异步代码就像优秀的交通管制系统
,让各种任务有序高效地运行,既不让UI
列车晚点,也不让数据货运停滞。掌握这些底层原理,才能写出既高效又健壮的Flutter
应用。
\\n","description":"前言 异步模型 —— 单线程下实现并发的核心机制。\\n\\n在传统认知中,多线程似乎是并发的唯一解,但Dart却以单线程+事件循环的设计,实现了媲美多线程的高效异步。这看似“反直觉”的方案背后,隐藏着精妙的设计哲学:通过有序的任务调度替代无序的资源竞争。单线程避免了多线程的内存隔离、锁机制和上下文切换成本,却带来了新的思考——如何用一根“单线”串起网络请求、UI渲染、用户交互等海量事件?\\n\\n本章将揭开Dart事件循环的神秘面纱,你将会看到:微任务如何“插队”实现即时响应,事件队列如何像传送带般有序运转,以及单线程模型下如何避免“卡顿陷阱”。理解这些机制…","guid":"https://juejin.cn/post/7470417707582636083","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-12T09:57:45.061Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/aadc24b28fa54e7291dbc19cebd0ddc8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1739959065&x-signature=u4kv1SF2tEBkE%2BYaEZsaJNHRF%2BQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e8b4fc1425bb4794a3d2bb2d82b4896e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1739959065&x-signature=Zceb%2BQW3VDx2rHFsiz%2B87PY%2BWbw%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"flutter中创建单例的方式","url":"https://juejin.cn/post/7470308848676896779","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
在Flutter中实现单例模式的方式有多种,每种方式各自有其优缺点,适用于不同的场景。下面我将介绍常见的几种单例实现方式,并分析它们的优缺点。
\\nfactory
构造函数实现单例这种方式是Flutter中最常见的单例实现方式,利用factory
构造函数来确保单例对象只会被实例化一次。
dart\\n复制编辑\\nclass Singleton {\\n static Singleton? _instance;\\n\\n Singleton._internal();\\n\\n factory Singleton() {\\n _instance ??= Singleton._internal();\\n return _instance!;\\n }\\n\\n void sayHello() {\\n print(\\"Hello, Singleton!\\");\\n }\\n}\\n
\\nfactory
构造函数实现单例模式,代码清晰易懂。??=
操作符是原子的,能确保线程安全。static
变量实现单例通过使用静态变量来持有单例实例,确保该实例是全局唯一的。
\\ndart\\n复制编辑\\nclass Singleton {\\n static final Singleton _instance = Singleton._internal();\\n\\n Singleton._internal();\\n\\n static Singleton get instance => _instance;\\n\\n void sayHello() {\\n print(\\"Hello from static variable Singleton!\\");\\n }\\n}\\n
\\ngetter
来返回单例实例,代码简洁易懂。factory
构造函数一样,无法控制单例的销毁时机。get_it
包(依赖注入)实现单例get_it
是一个依赖注入(DI)库,常用于Flutter中管理对象的生命周期,可以用来实现单例模式。
dart\\n复制编辑\\nimport \'package:get_it/get_it.dart\';\\n\\nclass Singleton {\\n void sayHello() {\\n print(\\"Hello from Singleton using GetIt!\\");\\n }\\n}\\n\\nvoid main() {\\n final getIt = GetIt.instance;\\n getIt.registerSingleton<Singleton>(Singleton());\\n\\n var instance1 = getIt<Singleton>();\\n var instance2 = getIt<Singleton>();\\n\\n print(instance1 == instance2); // 输出: true\\n}\\n
\\nget_it
是一个功能强大的依赖注入库,可以用于管理应用中的所有单例对象,不仅仅是单例。get_it
能够管理不同类型的对象,可以在项目中统一管理所有的单例对象,增强了依赖关系的管理能力。get_it
),如果项目对外部依赖有严格要求,这可能不是最佳选择。Lazy Singleton
(延迟单例)延迟单例通过延迟加载的方式,确保对象只有在第一次使用时才被创建。适用于懒加载的场景。
\\ndart\\n复制编辑\\nclass LazySingleton {\\n static LazySingleton? _instance;\\n\\n LazySingleton._();\\n\\n static LazySingleton get instance {\\n _instance ??= LazySingleton._();\\n return _instance!;\\n }\\n\\n void sayHello() {\\n print(\\"Hello from Lazy Singleton!\\");\\n }\\n}\\n
\\n??=
运算符会确保线程安全。factory
构造函数方式类似,这种方式没有提供单例销毁机制。StreamController
实现单例通过StreamController
来实现单例,可以在需要管理异步流的场景中使用。
dart\\n复制编辑\\nimport \'dart:async\';\\n\\nclass Singleton {\\n static final _controller = StreamController<Singleton>.broadcast();\\n\\n Singleton._();\\n\\n static Stream<Singleton> get instance async* {\\n yield Singleton._();\\n }\\n\\n void sayHello() {\\n print(\\"Hello from Singleton with Stream!\\");\\n }\\n}\\n
\\nStreamController
来管理单例比其他方式更加复杂,对于简单场景来说可能显得过于繁琐。InheritedWidget
实现单例InheritedWidget
是Flutter框架中用于在 Widget 树中传递数据的机制。通过将单例对象存储在InheritedWidget
中,可以实现跨多个Widget共享单例的功能。
dart\\n复制编辑\\nclass Singleton {\\n void sayHello() {\\n print(\\"Hello from Singleton with InheritedWidget!\\");\\n }\\n}\\n\\nclass SingletonProvider extends InheritedWidget {\\n final Singleton singleton;\\n\\n SingletonProvider({Key? key, required this.singleton, required Widget child}) \\n : super(key: key, child: child);\\n\\n static Singleton of(BuildContext context) {\\n final SingletonProvider? result = context.dependOnInheritedWidgetOfExactType<SingletonProvider>();\\n return result!.singleton;\\n }\\n\\n @override\\n bool updateShouldNotify(covariant InheritedWidget oldWidget) {\\n return false;\\n }\\n}\\n\\nvoid main() {\\n runApp(\\n SingletonProvider(\\n singleton: Singleton(),\\n child: MaterialApp(\\n home: Scaffold(\\n body: Builder(\\n builder: (context) {\\n final singleton = SingletonProvider.of(context);\\n singleton.sayHello(); // 输出: Hello from Singleton with InheritedWidget!\\n return Container();\\n },\\n ),\\n ),\\n ),\\n ),\\n );\\n}\\n
\\nInheritedWidget
是Flutter框架中推荐的跨组件共享数据的方式,特别适用于小型项目和依赖注入。InheritedWidget
可以通过扩展来传递多个单例。InheritedWidget
只能用于Widget树中的数据传递,无法用于其他场景。InheritedWidget
可能会增加代码复杂性。实现方式 | 优点 | 缺点 |
---|---|---|
factory 构造函数 | 简单、延迟加载、线程安全 | 无法显式销毁单例 |
static 变量 | 简洁、线程安全 | 无法懒加载、无法显式销毁单例 |
get_it (依赖注入) | 灵活、全局管理单例对象 | 需要额外依赖、需要理解依赖注入概念 |
Lazy Singleton | 懒加载、线程安全 | 无法显式销毁、可能带来性能开销 |
StreamController | 支持异步流、多次监听 | 复杂、可能导致内存泄漏 |
InheritedWidget | 适合Widget树中的数据传递 | 仅适用于Widget树、增加代码复杂性 |
每种实现方式都有其适用场景,选择适合自己项目的单例实现方式能够让代码更加简洁高效。
","description":"在Flutter中实现单例模式的方式有多种,每种方式各自有其优缺点,适用于不同的场景。下面我将介绍常见的几种单例实现方式,并分析它们的优缺点。 1. 使用 factory 构造函数实现单例\\n\\n这种方式是Flutter中最常见的单例实现方式,利用factory构造函数来确保单例对象只会被实例化一次。\\n\\n代码示例:\\ndart\\n复制编辑\\nclass Singleton {\\n static Singleton? _instance;\\n\\n Singleton._internal();\\n\\n factory Singleton() {\\n _instance ?…","guid":"https://juejin.cn/post/7470308848676896779","author":"东大街","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-12T07:40:24.902Z","media":null,"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter App 图标与启动图自动生成","url":"https://juejin.cn/post/7470310543279882277","content":"在 Flutter
中配置应用图标和启动图时,选择适当的尺寸对于确保应用在不同设备和分辨率下的显示效果非常重要。下面是图标和启动图的尺寸建议:
当然,也是可以自己手动到原生工程中自己配置的,如果想简单点可以通过自动生成的方式会比较节省时间。
\\n对于应用图标,通常需要提供不同的分辨率来适应不同的屏幕密度。以下是建议的尺寸和对应的资源文件夹:
\\nAppIcon:
\\n建议图标的最小尺寸是 512x512 px,这样可以确保生成其他尺寸时保持清晰,符合 Android 和 iOS 平台的要求。
\\n启动图的尺寸需要根据设备的分辨率和屏幕尺寸来进行调整,以下是常见的建议尺寸:
\\n应用图标:
\\n512x512 px
。你可以准备好这张图,然后使用 flutter_launcher_icons
插件来生成不同分辨率的图标。启动图:
\\n1242x2436 px
的图片,作为启动图源。flutter_native_splash
插件会根据它自动为不同分辨率的设备生成合适的启动图。在 pubspec.yaml
中配置图标路径并运行命令:
flutter_icons:\\n android: true\\n ios: true\\n image_path: \\"assets/icon/app_icon.png\\" # 512x512 px 图标\\n
\\n然后运行:
\\n$ flutter pub get\\n$ flutter pub run flutter_launcher_icons:main\\n
\\n在 pubspec.yaml
中配置启动图路径并运行命令:
flutter_native_splash:\\n image: \\"assets/images/splash.png\\" # 推荐图像尺寸:1242x2436 px\\n color: \\"#42a5f5\\" # 背景色\\n android: true\\n ios: true\\n
\\n然后运行:
\\n$ flutter pub get\\n$ flutter pub run flutter_native_splash:create\\n
\\n512x512 px
的图标图像,然后使用 flutter_launcher_icons
生成不同尺寸的图标。1242x2436 px
的启动图图像,使用 flutter_native_splash
插件生成不同尺寸的启动图。\\n\\n在移动应用开发中,日期和时间选择是非常常见的需求。无论是设置日程、安排提醒,还是筛选数据,我们都离不开日期的选择功能。尤其是当用户需要进行灵活的日期选择、筛选或编辑时,我们希望能够通过一个统一的组件来满足这些需求,而不是每次都重新开发。
\\n
确定好这个方向后,我们就开始着手准备做一个通用时间选择组件。
\\n假设正在开发一个待办事项应用,用户可能需要进行以下操作:
\\n总结
\\n\\n\\n这一块主要还是和产品去沟通,看看是否符合自己公司的开发需求,我这里只是陈述我得到的需求,可以将我的实现方法做一个思路去扩展到自己的项目中~
\\n
需要在同一个组件中实现三种不同的使用模式:
\\neditDate
) :在这个模式下,用户需要选择一个日期并进行编辑。这类似于我们日常生活中在日历上选择某天来安排事件或修改事件时间。filterDate
) :这个模式主要用于筛选特定日期范围或者周期。用户可以选择按“周”、“月”或“年”来筛选,这个需求常见于数据筛选或任务过滤等场景。editTime
) :这个模式允许用户选择一个具体的时间(包括小时和分钟)。比如在设置提醒的时候,用户需要指定提醒的具体时间,这时一个精确的时间编辑器就变得尤为重要。日期选择器需要能够灵活地接受不同的配置项,以满足多种使用场景:
\\ninitialDateTime
) :支持传入一个初始日期时间(DateTime
类型),以便用户能基于某个默认时间来进行操作。如果没有传入该值,则默认为当前时间。mode
):支持传入一个mode
(String
类型),来区别展示哪种时间选择器。filterType
) :用户可以选择筛选的时间粒度(周、月、年)。通过这个配置,用户可以根据不同的需求选择不同的筛选方式。yearsBack
) :在filterType == week
时,可能有时需要设定过去若干年内的日期进行选择。这一配置项可以帮助限制可选择的日期范围。为了提高组件的复用性,我们需要通过回调函数来传递用户的选择结果。这个组件将暴露以下回调:
\\nonChange
:每当用户选择一个新的日期或时间时触发,能够让外部接收到最新的选择结果。onConfirm
:用户确认选择时触发,组件将返回最终选择的日期或时间。onCancel
:用户取消操作时触发,关闭日期选择弹窗。为了让日期选择器在页面上方以弹窗的形式展示,我们使用了 Overlay
来插入浮层。通过 OverlayEntry
动态插入日期选择器,使得日期选择器能够在不影响页面其他内容的情况下展示,并且支持自定义关闭操作。
这样,用户可以通过点击空白区域来关闭弹窗,或者点击按钮来确认或取消。
\\n根据不同的模式,选择器的界面会有所变化。\\n在实现时,我们将不同模式的UI分离成单独的小组件(如 EditDate
、EditTime
、FilterDate
),这样可以根据用户的需求动态加载不同的组件。每个组件都有自己特定的行为,确保在各种场景下都能提供最佳的用户体验。
通过这些设计,保证了组件的灵活性和可定制性的同时,也能够根据自己的需求选择性地使用日期选择器,满足各种功能场景。
\\nCustomDatePicker
\\n\\n是一个静态类,用于展示日期选择器,接受多个配置项,如上下文、初始化时间、筛选类型、回调函数等。
\\nshow
方法负责插入弹窗并展示不同的选择器。
class CustomDatePicker {\\n static void show({\\n required BuildContext context,\\n required String mode,\\n String? filterType,\\n DateTime? initialDateTime,\\n int? yearsBack,\\n required Function(dynamic) onChange,\\n required Function(dynamic) onConfirm,\\n required Function() onCancel,\\n }) {\\n OverlayState overlayState = Overlay.of(context);\\n late OverlayEntry overlayEntry;\\n overlayEntry = OverlayEntry(\\n builder: (context) {\\n return Material(\\n color: Colors.transparent,\\n child: DatePickerOverlay(\\n mode: mode,\\n initialDateTime: initialDateTime,\\n onChange: onChange,\\n filterType: filterType,\\n yearsBack: yearsBack,\\n onConfirm: (selectedTime) {\\n onConfirm(selectedTime);\\n overlayEntry.remove();\\n },\\n onCancel: () {\\n onCancel();\\n overlayEntry.remove();\\n },\\n ),\\n );\\n },\\n );\\n overlayState.insert(overlayEntry);\\n }\\n}\\n
\\nDatePickerOverlay
\\n\\n具体的日期选择器UI组件,根据不同的模式展示不同的日期选择界面,如
\\nEditDate
、EditTime
、FilterDate
。\\n
_buildPicker()
方法来根据传入的mode
显示不同的选择器。\\n
_buildButtons
:在底部展示两个按钮:“取消”和“确定”,点击后执行相应的回调
class DatePickerOverlay extends StatefulWidget {\\n final String mode;\\n final DateTime? initialDateTime;\\n final String? filterType;\\n final int? yearsBack;\\n final Function(dynamic) onChange;\\n final Function(dynamic) onConfirm;\\n final Function() onCancel;\\n\\n const DatePickerOverlay({\\n Key? key,\\n required this.mode,\\n this.initialDateTime,\\n this.filterType,\\n this.yearsBack = 2,\\n required this.onChange,\\n required this.onConfirm,\\n required this.onCancel,\\n }) : super(key: key);\\n @override\\n _DatePickerOverlayState createState() => _DatePickerOverlayState();\\n}\\nclass _DatePickerOverlayState extends State<DatePickerOverlay> {\\n late DateTime selectedDateTime;\\n late String filterType;\\n Map<String ,dynamic> filterDate = {};\\n @override\\n void initState() {\\n super.initState();\\n filterType = widget.filterType ?? \\"week\\";\\n selectedDateTime = widget.initialDateTime ?? DateTime.now();\\n }\\n void _onMomentChange(DateTime date) {\\n setState(() {\\n selectedDateTime = DateTime(\\n date.year,\\n date.month,\\n date.day,\\n date.hour,\\n date.minute\\n );\\n });\\n widget.onChange(selectedDateTime);\\n }\\n void _onEditTimeChange(dynamic date){\\n setState(() {\\n filterDate = date;\\n });\\n widget.onChange(date);\\n }\\n void _onFilterDateChange(dynamic date){\\n setState(() {\\n filterDate = date;\\n });\\n widget.onChange(date);\\n }\\n void _onConfirm(){\\n if(widget.mode == \'editDate\'){\\n widget.onConfirm(selectedDateTime);\\n }\\n if(widget.mode == \'filterDate\'){\\n widget.onConfirm(filterDate);\\n }\\n if(widget.mode == \'editTime\'){\\n widget.onConfirm(filterDate);\\n }\\n }\\n @override\\n Widget build(BuildContext context) {\\n return Stack(\\n children: [\\n Positioned.fill(\\n child: GestureDetector(\\n onTap: widget.onCancel,\\n child: Container(color: Colors.black54),\\n ),\\n ),\\n Positioned(\\n bottom: MediaQuery.of(context).padding.bottom + 20,\\n left: 0,\\n right: 0,\\n child: Container(\\n padding: const EdgeInsets.symmetric(vertical: 24 , horizontal: 24),\\n margin: const EdgeInsets.symmetric(horizontal: 12),\\n decoration: BoxDecoration(\\n color: Colors.white,\\n borderRadius: BorderRadius.circular(24),\\n ),\\n child: Column(\\n children: [\\n Padding(padding: const EdgeInsets.only(bottom: 16) ,\\n child: Text(getPickerTitle(widget.mode), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold , color: Colors.black),),\\n ),\\n _buildPicker(),\\n _buildButtons(),\\n ],\\n ),\\n ),\\n ),\\n ],\\n );\\n }\\n Widget _buildPicker() {\\n switch (widget.mode) {\\n case \\"editDate\\":\\n return _buildDateTime(initialDate: widget.initialDateTime ?? DateTime.now() );\\n case \\"filterDate\\":\\n return _buildFilterPicker(\\n filterType: filterType,\\n initialDate: widget.initialDateTime?? DateTime.now() ,\\n yearsBack: widget.yearsBack\\n );\\n case \\"editTime\\":\\n return _buildTimePicker(initialDate: widget.initialDateTime ?? DateTime.now() );\\n default:\\n return Container();\\n }\\n }\\n Widget _buildDateTime({\\n required DateTime initialDate\\n }){\\n return EditDate(\\n initialDate: initialDate,\\n onDateChanged: _onMomentChange,\\n );\\n }\\n Widget _buildTimePicker({\\n required DateTime initialDate\\n }) {\\n return EditTime(\\n initialDate: initialDate,\\n onDateChanged: _onEditTimeChange\\n );\\n }\\n Widget _buildFilterPicker({\\n required DateTime initialDate,\\n required String filterType,\\n int? yearsBack,\\n }){\\n return FilterDate(\\n type:filterType,\\n initialDate: initialDate,\\n yearsBack: yearsBack,\\n onDateChanged: _onFilterDateChange\\n );\\n }\\n Widget _buildButtons() {\\n return Padding(padding: EdgeInsets.only(top: 12) ,\\n child: Row(\\n children: [\\n Expanded(\\n child: TextButton(\\n style: ButtonStyle(\\n backgroundColor: MaterialStateProperty.all<Color>(\\n const Color.fromRGBO(240, 240, 240, 1),\\n ),\\n padding: MaterialStateProperty.all<EdgeInsets>(\\n const EdgeInsets.symmetric(vertical: 14, horizontal: 0),\\n ),\\n ),\\n onPressed: widget.onCancel,\\n child: const Text(\\"取消\\", style: TextStyle(color: Colors.black , fontWeight: FontWeight.w600)),\\n ),\\n ),\\n const SizedBox(width: 12,),\\n Expanded(\\n child: TextButton(\\n style: ButtonStyle(\\n backgroundColor: MaterialStateProperty.all<Color>(\\n const Color.fromRGBO(52, 130, 255, 1),\\n ),\\n padding: MaterialStateProperty.all<EdgeInsets>(\\n const EdgeInsets.symmetric(vertical: 14, horizontal: 0),\\n ),\\n ),\\n onPressed: _onConfirm,\\n child: const Text(\\"确定\\", style: TextStyle(color: Colors.white , fontWeight: FontWeight.w600)),\\n ),\\n ),\\n ],\\n )\\n );\\n }\\n}\\n
\\nCustomPicker
...待补充
","description":"前言 在移动应用开发中,日期和时间选择是非常常见的需求。无论是设置日程、安排提醒,还是筛选数据,我们都离不开日期的选择功能。尤其是当用户需要进行灵活的日期选择、筛选或编辑时,我们希望能够通过一个统一的组件来满足这些需求,而不是每次都重新开发。\\n\\n确定好这个方向后,我们就开始着手准备做一个通用时间选择组件。\\n\\n需求\\n\\n假设正在开发一个待办事项应用,用户可能需要进行以下操作:\\n\\n编辑日期:用户希望选择某个具体日期来编辑待办事项的日期,类似于日历上的日期选择。\\n筛选日期:用户可能需要在某些场景下筛选出特定的日期范围,例如选择某一周、一月或一年内的任务。\\n编辑提醒…","guid":"https://juejin.cn/post/7470167570274385920","author":"你听得到11","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-12T02:49:22.558Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b9a98295398541778a95c7d4a6678dde~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5L2g5ZCs5b6X5YiwMTE=:q75.awebp?rk3s=f64ab15b&x-expires=1739933362&x-signature=Femch%2FGNnswVqJqxrG49llna9HQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/78c56ce41754471183cc9f3d248f8f03~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5L2g5ZCs5b6X5YiwMTE=:q75.awebp?rk3s=f64ab15b&x-expires=1739933362&x-signature=hIXSTbe8L3N3pz8iw2UWmQOanjo%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["前端","Flutter","Android"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 的 Widget Key 提议大调整?深入聊一聊 Key 的作用","url":"https://juejin.cn/post/7470191899124596748","content":"在 Flutter 里,Key 对象存在的目的主要是区分和维持 Widget 的状态,它是控件在渲染树里的「复用」标识之一,这一点在之前的《深入 Flutter 和 Compose 在 UI 渲染刷新时 Diff 实现对比》 聊到过,可以说 Key 的存在关乎了 Flutter 的性能,因为它的作用就是提高 Element Tree 的复用效率,例如减少匹配阶段所需的 Widget 比较次数。
\\n另外通过 Key 还可以提高如 AnimatedList
、ListView
里重新排序时对应 Item widget 的效率,通过将 Key 分配给 Item ,Flutter 可以更有效地识别何时添加、删除或更新列表并执行动画,在这个时候, Key 可以确保每个 Item 即使在对列表进行排序时也保持其状态。
大多数情况下,无状态的 Widget 是不需要 Key,而默认情况下,我们在不主动配置 Key 的时候,它会是 null :
\\n也就是在没有 Key 的情况下,framewok 一般只判断 runtimeType 去决定是否「复用」,举个很老的官方例子,如下图片代码里的 StatelessColorfulTile
所示,它是一个无状态的 StatelessWidget ,显示了一个随机颜色的 200x200 大小的正方形,通过点击右下角按键,每次调整两个方块的位置,可以看到方块可以正常切换:
因为此时没有 Key ,在 Element Tree 只需要判断 runtimeType ,明显此时 Element 符合复用条件,而代码里又是直接使用 StatelessColorfulTile
的 Widget 实例对象进行 tiles.insert(1, tiles.removeAt(0))
,所以在 Widget 切换位置之后,Element 和 RenderObject 只需要 update 一下新位置 Widget 实例的颜色即可:
但是,如果我们修改为 StatefulWidget ,此时我们再点击右下角按键,可以看到此时颜色方块不会切换了:
\\n因为此时颜色 color 被保存在 State 下,在 Widget 切换位置之后,因为 runtimeType 符合条件,所以 Element 复用,但是颜色被保存在 State 下,State 又是保存在 Element 里,从而导致颜色并没有按照需求被更新切换:
\\n但是,如果这时候我们给两个 StatefulWidget 添加上 Key ,就可以看到它们可以被切换了,因为 canUpdate
判断条件会增加 Key 判断:
也就是,在有了 Key 之后,新 Widget 的 key 就可以在老 Element 列表里进行匹配,从而更新 Element 的位置并刷新 RenderObject,两个 Element 在状态保留的情况下,被 Tree 里调换了位置进行更新,从而实现了切换的效果:
\\n所以,从这个简单的例子,可以直观看到 Key 在有状态的情况下能够发挥的作用,当然,目前在 Flutter 里的 Key 类型很丰富,但是大致可以简单分为两类: Local Keys 和 Global Keys 。
\\n顾名思义就是它的作用范围,举个例子,如果我们给 StatelessColorfulTile
增加了一个 Padding ,再点击切换按键,可以看到此时点击后 Element 一直被重构:
因为此时在 Row
里面,此时处于“一级”位置 children 是两个 Padding,而 Padding 没有 Key,所以它在 runtimeType 条件的情况下,是直接被复用:
而对于 StatelessColorfulTile
而言,它处于 Padding 之下,Padding 不是一个 Multi Child 的控件,所以在 canUpdate 为 false 的时候,Flutter 内部会认为它需要被重新创建:
从这里我们就可以很直观体验到 Local Keys 这个概念:它只作用于标识同一父 Widget 中的 Widget,不能用于识别其父 Widget 之外的 Widget。
\\n同时,我们也可以是直观感受到:Multi Child 和 Single Child 的 Element 对于 Diff 更新时的策略差别。
\\n另外,我们还可以感受到 Widget 作为「配置文件」的存在,要知道,代码里我们操作的一直都是 tiles.insert(1, tiles.removeAt(0));
,也就是 Widget 的实例化都的对象,虽然 Widget 实例没变,但是 Element 层面还是会根据情况「重新创建」对应的 Element ,由于颜色是在 State 里,所以也就会跟着 Element 重新随机变化。
最后如下图所示,对于 Local Keys 来说,左侧这样的写法是可以的,而右侧这样的写法是违规的:
\\n所以,在 Widget 的 Key 注释里也有这样一句描述:通常情况下,作为另一个 widget 的唯一子项的 widget 不需要显式 Key。
\\n那么,除开 Local Keys ,Flutter 里还有一个特殊的 GlobalKey,允许开发者在 Widget 树里去「唯一」标识 Widget,并提供 BuildContext(Element)/State 的全局访问:
\\n\\n\\n这里的「唯一」更多体现在当前这一帧里的「唯一」。
\\n
比如前面的例子,我们只需要把对应的 Local Keys 换成 GlobalKey ,就可以看到,虽然 Key 所在的 StatelessColorfulTile
还是在 Padding 下的“二级” child ,但是现在点击切换时,它不会被「重新创建」导致颜色发生变化:
这是因为,虽然在 updateChild
的时候,逻辑依然会走到 inflateWidget
去创建 Element ,但是由于是 GlobalKey,所以会从全局保存的 Map 里获取到当前 GlobalKey 绑定的 Element ,从而 retake 复用:
\\n\\n从这里可以看出来, 如果 Element 在同一帧中移动或者删除,并且它具有 GlobalKey,那么它仍然可能被重新激活使用。
\\n
所以 GlobalKey 不仅可以作为 Key 区分 Widget ,帧内还可以在 BuildOwner 里“全局”保持住 Element 、State 和关联 RenderObject 的“状态”,即使它出现移动或者删除。
\\n同时,通过 GlobalKey ,我们也可以访问对应的 BuildContext 和 State 数据,甚至是直接给 MaterialApp
添加 GlobalKey 来操作导航:
那么 GlobalKey 这么好用,它又存在什么问题呢?其实在注释里已经有对应说明:
\\n\\n\\nGlobalKey 在使用的过程中可能会出现需要重新设置 [Element] 父级的情况,而这个操作会触发对关联的 [State] 及其所有后代 [State.deactivate] 的调用,还会强制重建所有依赖于 [InheritedWidget] 的控件。
\\n
具体就体现在这下面两段代码:
\\n_retakeInactiveElement
内可能会触发所有关联 State 的 deactivate
_activateWithParent
会触发 Element 的 activate
,从而通过 didChangeDependencies
强制重建所有依赖于 [InheritedWidget] 的控件当然,GlobalKey 也有一些注意事项,例如:
\\n\\n\\n使用 GlobalKey 不能频繁创建,通常应该是让它和 State 对象拥有类似的“生命长度”,因为新的 GlobalKey 会丢弃与旧 Key 关联的子树的状态,并为新键创建一个新的子树,频繁创建会导致状态丢失和性能损耗。
\\n
前面我们主要介绍了 Key 的作用和分类下的职能,而本次 PR 提议的调整,则是在于打算简化 Local Keys 相关的实现上,可以看到在以往的实现里,关于 LocalKey 的实现有好几种类型,但是其中一些职能其实「相对重复」:
\\n在 #159225 的 PR 里,将打算把 Key 对象切换到 Object ,从而“消灭”过往这些 Local Keys 的“重叠”,让 Key API 更加灵活:
\\n另外,除了灵活和简化之外,针对目前存在的 Local Keys ,它和 Dart 的 Extension Types 不同,比如使用 ValueKey() 多多少少会有一点点点点点点 wrapper 成本,而如果这个提议合并后,大概会是如下所示的情况,或多或少对性能还是有那么一点点点点点点帮助:
\\n\\n\\n事实上对于 LocalKey ,大多数人应该都只会使用到
\\nValueKey
居多。
当然,这个 PR 整体来说还是属于底层大调整,而目前看起来提议应该是暂时搁置了,不过就算推进落地,相信对于大多数上层 Flutter 开发者来说,应该也不会有明显的感知,毕竟大多数时候 Flutter 开发者对 Key 并不敏感:
\\n所以,你是喜欢现在的 Local Keys 分类还是提议里的 Object ?
\\n在 Flutter 开发中,组件库是提高开发效率、保证 UI 一致性的重要工具。然而,现有的 Flutter 组件库(如 Material Components、Bruno 等)虽然功能强大,但在实际企业开发中,往往无法完全满足定制化需求。本文将分享我们在 Flutter 组件库开发中的实践经验,介绍如何基于 Bruno 封装一套符合企业需求的 UI 组件库,并探讨未来的开源计划。
\\n在开发 Flutter 应用时,我们遇到了以下问题:
\\nElevatedButton
、TextButton
等),导致代码风格不一致。为了解决这些问题,尽量少的开发成本,我们决定在 Bruno 组件库的基础上进行二次封装,扩展功能并统一开发风格。
\\n在 Web 和 H5 小程序开发中,组件库生态非常丰富,例如:
\\n这些组件库的成功经验为我们提供了以下启示:
\\n借鉴这些经验,我们在 Flutter 组件库开发中,也注重统一性、配置化和扩展性。
\\n我们封装了一个 XFButton
组件,通过参数控制不同类型的按钮(如主按钮、次按钮、虚线按钮等),并支持自定义样式。以下是一个简单的示例:
通过主题(Theme)和样式扩展,我们封装了符合公司设计规范的组件库。支持全局主题配置和局部主题覆盖。
\\n我们基于现有插件封装了地图和图表组件,提供更易用的 API 和统一的风格。以下是一个地图组件示例:
\\n我们封装了一个 XFForm
组件,支持通过 JSON 或 Dart 配置生成表单页,减少重复代码。以下是一个表单配置示例:
我们计划将组件库开源,并欢迎更多开发者参与。以下是我们的后续计划:
\\n通过封装统一的 Flutter 组件库,我们不仅解决了企业定制化需求,还提高了开发效率和代码质量。希望我们的实践经验能够为 Flutter 开发者提供参考,也期待更多开发者加入我们的开源项目,共同推动 Flutter 生态的发展!
\\n相关链接:
\\n\\n如果你对我们的组件库感兴趣,欢迎一起讨论和贡献!
","description":"引言 在 Flutter 开发中,组件库是提高开发效率、保证 UI 一致性的重要工具。然而,现有的 Flutter 组件库(如 Material Components、Bruno 等)虽然功能强大,但在实际企业开发中,往往无法完全满足定制化需求。本文将分享我们在 Flutter 组件库开发中的实践经验,介绍如何基于 Bruno 封装一套符合企业需求的 UI 组件库,并探讨未来的开源计划。\\n\\n背景与问题\\n\\n在开发 Flutter 应用时,我们遇到了以下问题:\\n\\nButton 实现不统一:Flutter 提供了多种 Button 实现(如 ElevatedBut…","guid":"https://juejin.cn/post/7469994189310328895","author":"东东同学","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-11T09:35:30.439Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4576204893c441e69781dfc3d87c416a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Lic5Lic5ZCM5a2m:q75.awebp?rk3s=f64ab15b&x-expires=1739871330&x-signature=hhLEyVqW3MQVra%2F%2BYCS5brh2OfM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f160f41f9f1d4212a18cc34119f5c6a7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Lic5Lic5ZCM5a2m:q75.awebp?rk3s=f64ab15b&x-expires=1739871330&x-signature=r1zJdqhEjiWuVPstDp24NXx0OZY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e8e73d94c42b4d668314030970b24a34~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Lic5Lic5ZCM5a2m:q75.awebp?rk3s=f64ab15b&x-expires=1739871330&x-signature=K9iGaSwpfxyrkGz7xU%2BK8%2BKmNK8%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["前端","Flutter","架构"],"attachments":null,"extra":null,"language":null},{"title":"系统化掌握Dart编程之异步编程(一):提升对任务的认知","url":"https://juejin.cn/post/7469978782674829338","content":"任务(Task
) —— 构建非阻塞
、高效
程序的基石。
在 Dart
的异步编程模型中,任务(Task
) 是构建非阻塞
、高效
程序的基石。无论是处理网络请求
、文件操作
,还是复杂的计算逻辑
,任务的设计和管理直接影响程序的并发性
、效率
以及资源利用
。
Dart
作为单线程语言,通过事件循环(Event Loop
) 和任务队列的机制实现并发,而提升对任务的认知是系统化掌握异步编程的核心内容。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\n独立的工作单元:任务代表一个逻辑上独立的操作
,例如计算
、I/O请求
、数据处理
等。
可并发/并行执行:任务可以被多个线程
、进程
或协程
同时处理(取决于硬件和编程模型)。
资源占用:任务可能需要内存
、CPU
时间、文件句柄
等资源。
目标导向:任务通常有明确的输入
、处理
和输出
阶段。
常见形式 | 详细说明 |
---|---|
线程(Thread ) | 操作系统调度的最小执行单元,共享进程资源。 |
进程(Process ) | 独立运行的实例,拥有独立内存空间。 |
协程(Coroutine ) | 用户态轻量级线程,通过协作式多任务切换(如Kotlin中的Coroutine )。 |
异步任务(Async Task ) | 非阻塞操作,通过回调或Future/Promise 实现(如JavaScript 的Promise 、Dart 中的Future 、Android 中的AsyncTask 等)。 |
作业(Job ) | 后台运行的任务(如服务器处理请求)。 |
核心特性 | 详细说明 |
---|---|
优先级(Priority ) | 决定任务调度的顺序(如实时系统)。 |
状态(State ) | 包括就绪(Ready )、运行(Running )、阻塞(Blocked )、完成(Finished )等。 |
依赖关系 | 任务之间可能存在先后顺序(如任务B 需要任务A 的结果)。 |
超时与重试 | 任务可能需要设置超时机制 或失败后的重试策略 。 |
调度器(Scheduler
) :负责分配CPU
时间给任务,常见策略:
FCFS
)Round-Robin
)Priority-based
)并发模型:
\\nJava
的ExecutorService
)。Node.js
、Dart
的异步模型)。Actor
模型:任务通过消息传递通信(如Erlang
、Akka
框架)。同步机制:确保多个任务有序执行:
\\nLock
) :防止资源竞争(如互斥锁)。Semaphore
) :控制资源访问数量。Condition Variable
) :任务间的状态通知。通信方式:
\\nPython
的multiprocessing.Queue
)。I/O
:任务在等待I/O
时释放CPU
(如网络请求)。Callback
) :任务完成后触发特定函数
。Future/Promise
:代表异步操作的最终结果(如Java
的CompletableFuture
、Dart
的Future
)。async/await
:语法糖简化异步代码(如Python
、Dart
、JavaScript
)。CPU
时间运行。I/O
或资源等待暂停。关闭文件
、内存回收
)。内部处理错误
或向上层传递
。asyncio.wait_for
)。CancellationToken in C#
)。Web
服务器:每个HTTP
请求作为一个任务。MapReduce
)。AI
逻辑分任务执行。GUI
应用:后台任务避免界面卡顿。任务是编程中组织和管理复杂操作的基石
,理解任务的调度
、并发
和同步机制
是构建高效、可靠系统的关键。不同的编程范式(多线程
、异步
、分布式
)对任务的处理方式各异,需根据场景选择合适模型。
\\n","description":"前言 任务(Task) —— 构建非阻塞、高效程序的基石。\\n\\n在 Dart 的异步编程模型中,任务(Task) 是构建非阻塞、高效程序的基石。无论是处理网络请求、文件操作,还是复杂的计算逻辑,任务的设计和管理直接影响程序的并发性、效率以及资源利用。\\n\\nDart 作为单线程语言,通过事件循环(Event Loop) 和任务队列的机制实现并发,而提升对任务的认知是系统化掌握异步编程的核心内容。\\n\\n操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。\\n\\n一、任务的基本定义\\n\\n独立的工作单元:任务代表一个逻辑上独立的操作,例如计算、I/O请求、数据处理等。…","guid":"https://juejin.cn/post/7469978782674829338","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-11T07:12:22.637Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f3cebdfe3a0947d9a0809a3fe256af8f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1739862742&x-signature=hfPx%2FlxpBMKrvsHdsN6sDhZ6CUE%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"【全网最细】Flutter入门必读:90%人不知道的Dart核心基础技巧,免费解锁80%开发效率提升!》[^全网最细]","url":"https://juejin.cn/post/7469771546828374053","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
dart是一种强大的脚本语言,可以不预先定义变量的类型,dart会自动类型推导
\\n可以通过var的关键字声明变量,也可通过显式类型来声明
\\n如:
\\nvar str = \'i am dart\';\\n\\nstring str = \'i am dart\';\\n\\nvar str =123;\\n\\nint str =123;\\n
\\n注意: var关键字和具体类型不要同时写,如:var int str =123; 报错
\\nDart的命名规则\\n1).变量名称必须有数字,字母,下划线,和美元符号($)组成
\\n2).标识符的开头不能是数字开头\\n\\n3).标识符不能使用关键字和保留字\\n\\n4).变量的名称是会区分大小写的\\n\\n5).定义标识符的时候意思要尽量明朗,通常变量的名称使用名词,方法的名称使用动词\\n
\\nDart的常量使用final 和const修饰
\\nconst修饰的常量在一开始的时候就需要赋值(编译的时候就已经赋好值了)
\\nfinal修饰的常量可以在一开始的时候不赋值,但同样只能赋值一次(惰性赋值,运行时第一次使用时赋值)
\\n//const常量\\nconst PI=3.14159;\\n\\nPI=3; //错误,常量的值不能修改了\\n\\n//final常量\\n\\nfinal a=new DateTime.now(); //给a运行时赋值\\n
\\n常用的数据类型:
\\n1).Numbers(数值):int,double
\\n2).Strings(字符串) : String
\\n3).Booleans(布尔) : bool
\\n4).List(数组) : 在Dart中数组是列表对象
\\n5).Maps(字典) : Map为键值对相关对象
\\n\\n\\nint a = 1;
\\n
\\n\\ndouble b = 2.5 ;
\\ndouble b = 2 ; // 给浮点型变量赋值整型不会报错,但输出的结果会自动补小数点后的0
\\n
运算符有加减乘除余,相对应 +,-,*,/,%
\\n1)可以用单引号,也可以用双引号
\\nvar str1 = \' i am str1\';\\n\\n或 String str1 = \' i am str1\';\\n\\nvar str2 = \\"i am str2\\";\\n\\n或 String str2 = \\"i am str2\\";\\n
\\n2)三引号
\\n使用三引号定义字符串可以换行
\\nString str3=\'\'\'\\n\\ni am str1\\n\\ni am str2\\n\\ni am str3\'\'\';\\n
\\n或:
\\nString str3=\\"\\"\\"\\n\\ni am str1\\n\\ni am str2\\n\\ni am str3\\"\\"\\";\\n
\\n3)字符串拼接
\\nprint(\\"$str1 $str2\\");\\n\\nprint(str1 + str2);\\n\\n\\n
\\n5.3.1 bool
\\nbool b1 = true;\\n\\nbool b2 = false;\\n
\\n5.3.2 条件判断语句
\\nvar b1=true;\\n\\nif(b1){\\n\\nprint(\'true\');\\n\\n}else{\\n\\nprint(\'false\');\\n\\n}\\n
\\nvar l1=[\'a\',\'b\',\'c\'];\\n\\nprint(l1); // 输出[a, b, c]\\nprint(l1.length); // 输出3\\nprint(l1[0]); // 输出a\\n
\\nvar l2= new List();\\nl2.add(\'one\');\\nl2.add(\'two\');\\nl2.add(\'three\');\\nprint(l2); // 输出[one, two, three]\\nprint(l2.length); // 输出3 \\nprint(l2[0]); // 输出one\\n
\\n\\n\\nvar l2= new List();
\\n
var persion={\\n \\"name\\":\\"Dart\\",\\n \\"age\\":\\"8\\"\\n};\\nprint(persion); //输出 {name: Dart, age: 8}\\nprint(persion[\'name\']); //输出 Dart\\nprint(persion[\'age\']); //输出 8\\n
\\nvar persion1=new Map();\\npersion1[\\"name\\"]=\\"张三\\";\\npersion1[\\"age\\"]=\\"9\\";\\nprint(persion1); //输出 {name: 张三, age: 9}\\nprint(persion1[\'name\']); //输出 张三\\nprint(persion1[\'age\']); //输出 9\\n
\\n var str = \'111\';\\n if (str is String) {\\n print(\\"str is String type\\");\\n } else if (str is int) {\\n print(\\"str is int type\\");\\n } else {\\n print(\\"str is other type\\");\\n }\\n}\\n结果 : 输出 str is String type\\n
\\n6.运算符\\n1)算数运算符
\\n2)关系运算符
\\n3)逻辑运算符
\\n4)赋值运算符
\\n5)条件表达式
\\n6)类型转换
\\nint a=5;\\nint b=4;\\n \\nprint(a+b); //加\\nprint(a-b); //减\\nprint(a*b); //乘\\nprint(a/b); //除\\nprint(a%b); //取余\\nprint(a~/b); //取整\\n结果:\\n\\n9\\n1\\n20\\n1.25\\n1\\n1\\n
\\nint a=5;\\nint b=4;\\n \\nprint(a==b); //是否相等\\nprint(a!=b); //是否不相等\\nprint(a>b); //是否大于\\nprint(a<b); //是否小于\\nprint(a>=b); //是否大于或者等于\\nprint(a<=b); //是否小于或者等于\\n结果:\\n\\nfalse\\ntrue\\ntrue\\nfalse\\ntrue\\nfalse\\n
\\nvar b =false;\\nprint(!b); //输出为true\\n
\\n当且仅当所有的值都为true的时候,结果才为true,否则为false\\n\\nvar a = true;\\nvar b = false;\\nprint(b && a); // 输出false\\n
\\n只要有一个值为true,则结果为true\\n\\nvar a = true;\\nvar b = false;\\nprint(b || a); // 输出true\\n
\\n int b = 6;\\n
\\nint b;\\n\\nb??=6; //当 b在这之前没有被赋值,则在这行代码中会被赋值 \\n
\\nint b = 6;\\nb += 10;\\nprint(b); //输出 16\\n
\\nvar b = true;\\nif (b) {\\n print(\\"true\\");\\n} else {\\n print(\\"false\\");\\n}\\n结果: true\\n
\\nvar sex = \\"boy\\";\\nswitch(sex){\\n case \\"boy\\" :\\n print(\\"boy\\");\\n break;\\n case \\"girl\\":\\n print(\\"girl\\");\\n break;\\n default:\\n print(\\"传入的参数错误\\");\\n break; \\n}\\n结果 : boy\\n
\\n解释:当等号=后的变量为true时,给变量赋值 : 前面的值,当flag为false时,给 变量赋值 : 后面的值\\n \\nbool flag = true;\\nString b = flag ? \\"I am a boy \\" : \\" I am a girl\\";\\nprint(b);\\n结果 : I am a boy\\n\\n解释:flag 为true,赋值I am a boy\\n
\\n解释:当a为null时,赋值??符号后的10给a,然后将a赋值给b.\\n 当a不为null时,直接将a赋值给b\\n \\nvar a;\\nvar b = a ?? 10;\\nprint(b); // 输出10\\nvar a=20;\\nvar b = a ?? 10;\\nprint(b); //输出20\\n
\\nNumber 转换为 String使用toString();
\\nString转换为 Number 使用parse();
\\n例:
\\n//将字符串转换为整型\\nString str=\'111\';\\nvar myNum=int.parse(str); //输出 111\\n//将整型转换为字符串\\nvar myNum = 18;\\nvar str = myNum.toString();\\nprint(str is String); //输出 true\\n//注意转换时报异常\\n\\nString str = \'\'; //字符串为空,转换为整型会报错\\ntry {\\n var myNum = int.parse(str);\\n print(\\"myNum\\");\\n} catch (err){\\n print(\\"转换错误\\");\\n}\\n
\\nfor (int i = 0; i < 5; i++) {\\n print(i);\\n}\\n结果:\\n\\n0\\n1\\n2\\n3\\n4\\n
\\nwhile : 先判断条件在进行操作
\\nvar i = 0;\\nwhile (i < 5) {\\n print(i);\\n i++;\\n}\\n结果:\\n\\n0\\n1\\n2\\n3\\n4\\n
\\ndo while: 先进行操作再判断条件
\\ndo {\\n print(i);\\n i++;\\n} while (i < 5);\\n结果:\\n\\n0\\n1\\n2\\n3\\n4\\n
\\nbreak : 1.在switch语句中跳出switch结构\\n\\n 2.在循环语句中跳出当前循环语句(注意只能向外跳出一层循环)\\n\\ncontinue:1.只能在循环语句中使用\\n\\n 2.跳出当前的此次循环,继续进行下一次循环\\n
\\n自定义方法的格式:
\\n\\n返回类型 方法名称(参数1,参数2,......){\\n\\n 方法体\\n\\n return 返回值;\\n\\n }\\n
\\n注:方法中可以定义一个内部方法并调用这个内部方法,该内部方法不能在方法外部调用
\\n在参数中用中括号[ ] 包含的参数为位置可选参数:(调用的时候可以带上[ ]中的参数,也可不带,也可以带上其中几个)
\\nString getPersionInfo(String name, [int age, String sex]) {\\n return \\"name : $name ; age : $age ; sex : $sex\\";\\n}\\nprint(getPersionInfo(\\"name\\")); // 输出name : name ; age : null ; sex : null\\nprint(getPersionInfo(\\"name\\", 8)); //输出name : name ; age : 8 ; sex : null\\n
\\nString getPersionInfo(String name, [int age, String sex = \\"man\\"]) {\\n return \\"name : $name ; age : $age ; sex : $sex\\";\\n}\\n
\\n当调用方法时没有传入参数sex,则sex默认被赋值为\\"man\\"。
\\n在参数中用{ }包含的参数为命名可选参数
\\nString getPersionInfo(String name, {int age, String sex = \\"man\\"}) {\\n return \\"name : $name ----- age : $age ----- sex : $sex\\";\\n}\\n \\nprint(getPersionInfo(\\"Dart\\", age: 8)); //输出 name : Dart ----- age : 8 ----- sex : man\\nprint(getPersionInfo(\\"Dart\\", sex: \\"girl\\")); //输出 name : Dart ----- age : null ----- sex :girl\\nprint(getPersionInfo(\\"Dart\\", sex: \\"girl\\", age: 8)); //输出 name : Dart ----- age : 8 ----- sex : girl\\n
\\n解释:在{ }外的参数为必传参数,而{ }里的参数为可选参数,即可传可不传,可以不按照{ }里的参数顺序传参,当传{ }中的参数时,应该用:
\\n参数名:参数值
\\n的方式传递。
\\n//方法,可作为参数\\nmethod1() {\\n print(\\"I am method one\\");\\n}\\n \\n//参数为方法的函数\\nmethod2(f()) {\\n f();\\n}\\n//调用方法method2\\nmethod2(method1) ; \\n结果:\\n\\nI am method one\\n \\n\\n解释:和匿名方法一样:\\n\\nvar fn=(){\\n print(\\"一个匿名方法\\");\\n};\\nfn是一个变量,代表着这个(){ print(\\"一个匿名方法\\"); };方法,可以直接当作参数传递。\\n
\\n当函数体的内容只有一句时,可用箭头指向表示。\\n\\n例如以下函数体可以改为用箭头=>指向:\\n\\n使用箭头表示前:\\n\\n// 当数组中的元素大于5,则返回5\\nList list = [2, 4, 6, 5, 8];\\nvar newList = list.map((e) {\\n if (e > 5) {\\n return 5;\\n }\\n return e;\\n});\\nprint(newList.toList());\\n\\n结果:[2, 4, 5, 5, 5]\\n\\n使用箭头表示后:\\n\\n//使用三目运算,将函数体简化成一句话,输出的结果和上面的例子是一样的\\n \\nList list = [2, 4, 6, 5, 8];\\nvar newList1 = list.map((e) => e > 5 ? 5 : e);\\nprint(newList1.toList());\\n//结果:[2, 4, 5, 5, 5]\\n
\\n匿名方法,顾名思义,即是没有名字的方法,匿名方法可以赋值给一个变量,例如:
\\nvar fun = () { //定义了一个变量fun(),这个变量的值是后面的方法体(匿名方法)\\n print(\\"我是匿名方法\\");\\n};\\nfun(); //在这里fun()是一个变量,在这里使用了这个变量\\n结果: 我是匿名方法\\n\\n注:这个匿名方法也可以带参数。\\n\\n如:\\n\\nvar fun = (int i) { //定义了一个变量fun,使用这个变量的时候需要带上一个参数\\n print(\\"我是匿名方法 :$i\\");\\n};\\n \\nfun(2); //这里使用fun(2)这一个变量 \\n结果:\\n\\n我是匿名方法 :2 \\n
\\n匿名方法的常用场景:
\\n1)将一个匿名函数赋值给一个变量;
\\n2)在传参的时候,把匿名函数作为参数传递。
\\n顾名思义,自执行方法,即指不需要主动的调用该方法,当程序启动的时候会自动执行该段代码;
\\n\\n 格式:\\n\\n((){\\n //这里输入代码内容 \\n})();\\n \\n \\n例子:\\n\\n(() {\\n print(\\"这是一段自执行代码!\\");\\n})();\\n结果: 这是一段自执行代码!\\n\\n在括号里可以定义传入的参数,在第一个括号里定义形参,在最后一个括号里传入实参,如:\\n\\n((int i) {\\n print(i);\\n print(\\"这是一段自执行代码!\\");\\n})(50);\\n结果:\\n\\n50\\n这是一段自执行代码!\\n
\\n\\n\\n1)面向对象编程语言的三个基本特征是:封装,继承,多态
\\n封装:封装是对象和类的主要特征。封装,把客观事物封装成抽象的类,并且把自己的部分属性和方法提供给其他对象使用。
\\n
\\n\\n继承: 面向对象编程语言的一个主要功能是“继承”。继承是指该实例化的对象能够使用现有类,以及这个类所继承的类的所有的变量和方法.
\\n
\\n\\n多态:多态性是允许你将父对象设置成为一个或更多的他的子对象相等的技术。
\\n2)Dart所有的东西都是对象,所有的对象都是继承自Object类。Dart是单继承的面向对象语言,所有的对象都是类的实例,并且所有的类都是Object的子类。
\\n3)一个类通常由属性和方法组成。
\\n
构造函数有两种:
\\n1)默认构造函数:
\\n当实例化对象的时候,会自动调用的函数,构造函数的名称和类的名称相同,在一个类中默认构造函数只能由一个。
\\n2)命名构造函数:
\\n当通过指定的命名构造函数实例化对象时,会调用改命名构造函数,命名构造函数可以有多个。
\\nclass Persion { //persion为类名\\n String name; //属性\\n int age;\\n \\n Persion(this.name, this.age); //默认构造函数,当实例化一个对象时,会自动调用到该函数\\n \\n Persion.now() {\\n print(\\"这是一个命名构造函数\\");\\n }\\n \\n getInfo() { //方法\\n print(\\"name : $name age : $age\\");\\n }\\n}\\n \\nvoid main() {\\n Persion man = new Persion(\\"ShenZhen\\", 40); //实例化对象(调用了默认构造函数)\\n man.getInfo();\\n \\n Persion man2 = new Persion.now(); //实例化对象的时候调用了命名构造函数\\n}\\n结果 :\\n\\nname : ShenZhen age : 40\\n这是一个命名构造函数\\n
\\n\\n\\njava等语言中有private,public,proteccted关键字表示属性或者方法的私有性,而在Dart语言中使用下划线_表示该方法或属性为私有的。
\\n注意:只有当类定义在其他独立的文件上时\\"_\\"表示私有性才是有效的,若和主入口函数main()在同一个文件下,私有性不会生效。
\\n
如果想要使用类的私有方法或者私有属性,可以通过类中公有方法返回私有属性。
\\n如私有属性(私有方法同理):
\\n\\nclass Persion {\\n String _name; //私有属性\\n \\n Persion(this._name); //公有方法,返回私有属性\\n \\n getName() {\\n return _name;\\n }\\n}\\n \\nvoid main() {\\n Persion man = new Persion(\\"Dart\\");\\n String myName = man.getName(); //Persion类的实例对象通过Persion类的公有方法getName()获取类中的私有属性\\n print(myName);\\n}\\n结果:Dart\\n
\\n类中用get修饰的方法块,使用的时候通过调用属性的方式使用。
\\n如:
\\nclass Persion {\\n String _name;\\n \\n Persion(this._name);\\n \\n get getName {\\n return _name;\\n }\\n \\n set setName(value) {\\n _name = value;\\n }\\n}\\n \\nvoid main() {\\n Persion man = new Persion(\\"深圳\\"); //实例化一个Persion对象\\n print(man.getName); //和调用类的属性的方式一样。通过“对象.属性”的方式调用get修饰的方法体\\n \\n man.setName = \\"惠州\\"; //通过“对象.属性 = 值”的方式调用set修饰的方法体\\n print(man.getName);\\n}\\n结果:\\n\\n深圳\\n惠州\\n
\\n\\nclass Persion {\\n static String name = \\"深圳\\"; //name为static修饰的静态变量\\n \\n static void show() {\\n print(\\"name : $name\\");\\n }\\n}\\n \\nvoid main() {\\n print(Persion.name); //使用name这个属性时直接通过“类名.属性”的方式 \\n}\\n//结果:深圳\\n
\\n ? 条件运算符\\n\\n as 类型转换\\n\\n is 类型判断\\n\\n .. 级联操作\\n
\\n在对象的后面使用?判断该对象是否是null.
\\nclass Persion {\\n String name = \\"深圳\\";\\n Persion(this.name);\\n void show() {\\n print(\\"name : $name\\");\\n }\\n}\\n \\nvoid main() {\\n Persion man; //这里只是定义了一个Persion的对象man,但是没有给man赋值\\n print(man?.name); //这里会报错,使用了条件运算符?判断man是一个空值,故不会打印也不会报错\\n}\\n
\\n 使用 is 判断该变量是什么数据类型\\n\\nPersion man=new Persion(\\"name\\");\\nif(man is Persion){ //判断man是否是Persion类型\\n print(\\"true\\");\\n}\\n结果:\\n\\ntrue\\n
\\n使用as进行类型的转换
\\n man as Persion //将对象man转换为Persion对象\\n
\\n在对象的后面使用级联符号“..”加属性或方法,会返回对象的本身,类似于java中的Builde建造者模式\\n\\nclass Persion {\\n String name ;\\n int age ;\\n \\n Persion(this.name,this.age);\\n \\n void show() {\\n print(\\"name : $name and age : $age\\");\\n }\\n}\\n \\nvoid main() {\\n Persion man = new Persion(\\"深圳\\",40);\\n man..name = \\"惠州\\" //使用..name后返回的还是man对象,可以进行接下来..age的操作\\n ..age=50\\n ..show();\\n}\\n结果:\\n\\nname : 惠州 and age : 50\\n
\\n1)一个子类继承自一个父类,那么这个子类的实例化对象直接可以使用这个父类的属性或方法。继承使用关键字extent 。
\\n格式:
\\n子类 extent 父类
\\n如:
\\nclass Persion {\\n String name ;\\n int age ;\\n Persion(this.name,this.age);\\n void show() {\\n print(\\"name : $name and age : $age\\");\\n }\\n}\\n \\nclass Superman extends Persion{ //Superman继承Persion\\n \\n Superman(String name, int age) : super(name, age); //super()里的参数是要传递给父类的参数\\n}\\n \\nvoid main() {\\n Superman man = new Superman(\\"深圳\\",40); //Superman实例化对象\\n man.show(); //Superman实例化的对象可以直接使用父类Persion的方法show();\\n}\\n结果:\\n\\nname : 深圳 and age : 40\\n
\\n2)在子类中不仅仅可以扩展父类中的属性或者方法,还能重写父类中的方法
\\nclass Persion {\\n String name;\\n \\n int age;\\n \\n Persion(this.name, this.age);\\n \\n void show() {\\n print(\\"name : $name and age : $age\\");\\n }\\n}\\n \\nclass Superman extends Persion {\\n Superman(String name, int age) : super(name, age);\\n \\n void show() { //在子类中复写了父类中的show方法\\n print(\\"姓名: $name----年龄:$age\\");\\n }\\n}\\n \\nvoid main() {\\n Superman man = new Superman(\\"深圳\\", 40); \\n man.show(); //通过子类的对象调用的是子类中复写的方法\\n} \\n
\\n结果:
\\n姓名: 深圳----年龄:40
\\n3)可以通过super关键字调用父类的方法
\\nclass Persion {\\n String name;\\n \\n int age;\\n \\n Persion(this.name, this.age);\\n \\n void show() {\\n print(\\"name : $name and age : $age\\");\\n }\\n}\\n \\nclass Superman extends Persion {\\n Superman(String name, int age) : super(name, age);\\n \\n void show() {\\n super.show(); //子类的show()方法通过super.show()的形式调用父类的方法\\n }\\n}\\n \\nvoid main() {\\n Superman man = new Superman(\\"深圳\\", 40);\\n man.show();\\n}\\n
\\n结果:
\\nname : 深圳 and age : 40
\\nDart中的抽象类:Dart中的抽象类主要用于定义标准,子类可以继承抽象类,也可以实现抽象类接口。
\\n1.抽象类通过abstract关键字来定义;
\\n2.Dart中的抽象类不能通过abstract声明,Dart中没有方法体的方法我们称之为抽象方法;
\\n3.如果子类继承抽象类必须实现里面的抽象方法;
\\n4.如果把抽象类当作接口实现的话必须得实现抽象类里面定义的所有属性和方法;
\\n5.抽象类不能被实例化,只有继承它的子类可以。
\\nextends抽象类 和 implement 的区别:
\\n1.如果要复用抽象类里面的方法,并且要用抽象方法约束自类的话我们就用extend继承抽象类
\\n2.如果只是把抽象类当作标准的话我们就用implement实现抽象类
\\n如:
\\nabstract class Animal { //Animal 为抽象类\\n eat(); //没有实现方法体,默认是一个抽象方法\\n}\\nclass Dog extends Animal{\\n @override\\n eat() { //如果在Dog类中没有定义eat()方法,将会报错\\n // do something\\n }\\n}\\n 下面直接通过抽象类进行初始化,会报错\\n\\nAnimal a = new Animal(); //会报错\\n
\\nAnimal d = new Dog(); //使用d.eat()的时候会调用Dog类中复写的eat()方法\\nAnimal c = new Cat(); //使用c.eat()的时候会调用Cat类中复写的eat()方法\\n
\\n\\n\\n和java一样,Dart中也有接口,但是和java有区别。
\\n在Java 中用interface关键字定义接口,而在Dart语言中普通的类或者抽象类都可以作为接口被实现。同样是通过使用implement关键字实现。
\\n
注意:
\\nDart中如果使用普通类或者抽象类做接口类,实现这个接口类的时候要覆写这个接口类所有属性和方法。抽象类中可以定义抽象方法,故建议使用抽象类定义接口。(接口通常是定义规范)
如:
\\nabstract class Animal { //抽象类,用作接口\\n String size;\\n eat() {\\n //do something\\n }\\n}\\n \\nclass Dog implements Animal { //implements 用于实现接口\\n @override // `@Override` 是 Java 的一种 **注解**,表明子类中的方法重写了父类中的方法或者实现了接口中的方法。\\n String size; //需要重新定义属性size\\n \\n @override\\n eat() { //需要重新定义方法eat()\\n // do something\\n }\\n}\\n
\\n实现多个接口,通过逗号“,”分隔
\\nabstract class A {\\n String name;\\n doA(){}\\n}\\n \\nabstract class B {\\n String name;\\n doB() {}\\n}\\n \\nclass C implements A,B { //要实现两个类,通过“,”分隔\\n @override\\n String name;\\n @override\\n doA() {} //不覆写doA()会报错\\n @override\\n doB() {} //不覆写doB()会报错\\n}\\n
\\nmixins的中文意思是混入,就是在类中混入其他功能。
\\n在Dart中可以使用minxins实现类似多继承的功能。
\\nmixins的使用条件随着Dart的版本不断更新而有所改变,此处讲的是Dart2.x中使用minxins的条件:
\\n1.作为minxins的类只能继承自Object,不能继承其他类\\n\\n2.作为minxins的类不能有构造函数\\n\\n3.一个类可以minxins多个minxins类\\n\\n4.minxins绝不是继承,也不是接口,而是一种全新的特性\\n
\\n如:
\\n\\nclass A { //A作为minxins类,只能继承自Object\\n doA() {\\n print(\\"I am A\\");\\n }\\n}\\n \\nclass B { //B作为minxins类,只能继承自Object\\n doB() { \\n print(\\"I am B\\");\\n }\\n}\\n \\nclass C with A, B {} //C混合了A类和B类,类似继承,C的实例化类可以使用A类以及B类中的方法 \\n \\nmain() {\\n C c = new C();\\n c.doA();\\n c.doB();\\n}\\n
\\n结果:
\\nI am A\\nI am B
\\n疑问:当两个混合类A和B中有相同的方法,那么C类中调用这个方法会产生什么样的结果呢?
\\nclass A {\\n run() { //A类中run()方法\\n print(\\"run A\\"); \\n }\\n \\n doA() {\\n print(\\"I am A\\");\\n }\\n}\\n \\nclass B {\\n run() { //B类中run()方法\\n print(\\"run B\\");\\n }\\n doB() {\\n print(\\"I am B\\");\\n }\\n}\\n \\nclass C with A, B {}\\n \\nmain() {\\n C c = new C();\\n c.doA();\\n c.doB();\\n c.run();\\n}\\n结果:\\n\\nI am A\\nI am B\\nrun B\\n
\\n解释:当两个混合类A和B中有相同的方法,那么C类调用该方法的时候会调用with关键字上最靠后的混合类的方法
\\n先看一个例子:
\\ngetData(int value) {\\n return value * 2;\\n}\\n \\nprint(getData(3));\\n
\\n结果:6
\\n当我们想要返回一个数字类型的数据,可以调用getDate()方法,传入数字类型,返回数字类型
\\n但是当我们想要返回一个String类型的数据的时候,此时是需要定义一个返回String类型的方法的。
\\n如:
\\ngetData(String value) {\\n return value;\\n}\\n \\nprint(getData(\\"惠州\\"));\\n
\\n结果:惠州
\\n以下“T”为不固定的传入类型:
\\nT getData<T>(T value) { //传入的实参是什么类型,则“T”就代表该类型\\n return value;\\n}\\n \\nprint(getData<String>(\\"深圳\\")); //<String>中的String为检验传的参数是否是String类型\\n
\\n结果:深圳
\\nclass ListClass<T> { //定义泛型类\\n List list = new List<T>();\\n \\n void printInfo() {\\n for (var i = 0; i < this.list.length; i++) {\\n print(this.list[i]);\\n }\\n }\\n \\n void add(T value) {\\n this.list.add(value);\\n }\\n}\\n \\nmain() {\\n ListClass list = new ListClass(); //实例化一个泛型类(这里没有指定类型T的实际类型,因此没有类型校验,传各种类型)\\n list.add(1); \\n list.add(2);\\n list.add(3); //添加int类型数据\\n list.add(\\"深圳\\"); //添加String类型数据,不会报错\\n list.printInfo(); \\n}\\n结果:\\n\\n1\\n2\\n3\\n深圳\\n
\\n当实例化泛型类的时候传入了指定的类型,那么在调用其中该泛型类中的方法时会进行类型校验,只能使用指定的类型。否则将将会报错。
\\n\\nmain() {\\n ListClass list = new ListClass<int>(); //指定了实例化ListClass 类时传入的类型为int类型\\n list.add(1);\\n list.add(2);\\n list.add(3);\\n list.add(\\"深圳\\"); //报错\\n list.printInfo();\\n} \\n
\\n在具体的类实现了泛型接口后,实例化该类需指定传入的类型:
\\n如:
\\n\\nabstract class Cache<T> { //抽象类Cache,此处做接口使用\\n getByKey(String key);\\n \\n setByKey(String key, T value);\\n}\\n \\nclass FileCache<T> implements Cache<T> { //Cache类的实现类\\n @override\\n getByKey(String key) {\\n }\\n \\n @override\\n setByKey(String key, T value) {}\\n}\\n \\nmain() {\\n FileCache fileCache = new FileCache<String>(); //实例化FileCache对象的时候,指定\\"T\\"的类型\\n fileCache.setByKey(\\"name\\", \\"深圳\\");\\n fileCache.setByKey(\\"name\\", 123); //报错,指定setByKey()的第二个参数为String类型,但是这里传入了int类型\\n}\\n
\\n在Dart中,库的使用通过import关键字引入。
\\nlibrary指令可以创建一个库,每个Dart文件都是一个库,即使没有使用library指令来指定。
\\nDart中的库有三种:
\\n1)自定义的库
\\n2)系统内置库
\\n3)Pub包管理系统中的库(第三方库)
\\n格式:
\\n\\n\\nimport \'lib/xxx.dart\';
\\n
当一个类的内容过多时,若把这个类与main()主方法或与其他类写在同一个文件中,将会导致这个文件过大而不便于管理,此时我们可以把这个类独立成一个文件,当另一个类需要使用这个独立成文件的类,通过以上的引用格式,就可以使用这个独立类里的方法了。
\\n格式:
\\n\\n\\nimport \'dart:math\';
\\n
在math库中有许多数学操作方法
\\n例如以下截图中的求最大值和最小值:
\\n使用:
\\nmin(20, 10); //返回10\\nmax(20, 10); //返回20\\n
\\n格式:
\\n\\n\\nimport \'dart:io\';
\\n
async 和 await
\\n在Dart中async 和 await关键字
\\nasync 是让方法变成异步方法。
\\nawait是等待异步方法执行完毕。
\\n1)只有async 方法才能使用await关键字去调用方法
\\n2)如果调用别的async方法必须使用 await关键字
\\n如:
\\nimport \'dart:io\';\\n\\nmain() { \\n test1(); //调用test1()方法,要使用await关键字,否则若有返回值会报错\\n test2(); //调用test2()方法\\n print(\\"----over-----\\");\\n}\\n\\ntest1() async { //此方法为async异步方法\\n print(\\"test---1\\");\\n await test3(); //调用了异步方法test3(),(注意如果test3()有返回值,这里必须用await关键字调用)\\n print(\\"test---1.1\\");\\n}\\n\\ntest2() {\\n print(\\"test---2\\");\\n}\\n\\ntest3() async { //声明test3()方法为异步方法\\n print(\\"test---3\\");\\n}\\n结果:\\n\\ntest---1\\ntest---3\\ntest---2\\n----over-----\\ntest---1.1\\n
\\n解释:在main()方法中依次调用了test1 ( )方法和test2( )方法,test1()方法为异步方法,故不需要test1()方法执行完才开始执行test2()方法,在test1()方法中调用了异步方法test3( ),这里使用了await关键字来调用test3( )方法,此时test2( )方法同时在执行。
\\n例如从 pub.dev/packages 获取http第三方库
\\n1).从下面网址中找到要用的库(以http库为例):
\\n\\n2).打开项目中的pubspec.yaml文件,找到 dependencies:,在dependencies下面添加内容:
\\nhttp: ^0.12.2\\n3).打开AndroidStudio的命令工作台:运行pub get 命令 ,获取远程库
\\n4).引用库
\\n当两个库中有相同名称的标识符时,我们不能辨别我们要使用到的标识符是属于哪一个库里面的,在java中通常是通过导入完整的包名路径来指定使用哪一个库里的标识符,在Dart语言中我们要使用到库的重命名的方法。
\\n如下:
\\n在当前类中引用了两个库文件,Persion1.dart和Persion2.dart,这两个库文件中都有对Perdsion类的定义,当前类中有一个main()方法,这个方法实例化了一个Persion类,但是这个时候回报错,因为IDE不清楚调用的是哪一个库文件里定义的Persion类。
\\nimport \'package:flutter_app_demo14/Persion1.dart\';\\nimport \'package:flutter_app_demo14/Persion2.dart\';\\n\\nmain() {\\n Persion p = new Persion(); //报错,IDE不清楚调用的是哪一个库文件里定义的Persion类\\n}\\n
\\n解决办法:
\\n使用 as 关键字给引用到的库重命名:
\\n格式:
\\n库名 as XXX
\\nimport \'package:flutter_app_demo14/Persion1.dart\';\\nimport \'package:flutter_app_demo14/Persion2.dart\' as lib; // as关键字给库重命名为lib\\n\\nmain() {\\n Persion p = Persion(); //这里Persion使用的是Persion1.dart里的\\n lib.Persion p1 = new lib.Persion(); //这里Persion使用的是Persion2.dart里的\\n}\\n
\\n部分导入的的两种模式:
\\n模式一:只导入需要的部分,使用show关键字,如下:\\n\\nimport \'Persion1.dart\' show getName; //此时可以使用Persion1.dart库文件中的getName()方法\\n 模式二:隐藏不需要的部分,使用hide关键字,如下:\\n\\nimport \'Persion1.dart\' hide getName; //此时不可以使用Persion1.dart库文件中的getName()方法\\n\\n\\n
\\n最近在写纯净flutter的相机功能,但是发现flutter官方的相机库还是需要很多需要封装的东西,所以就写成了一个开源库。
\\nPS:拍照加水印的功能正在筹备开发ing
\\n首先引入依赖
\\nflutter_camerax:\\n git:\\n url: https://github.com/cgztzero/FlutterCameraX.git\\n
\\n1.第一步创建一个相机Controller,之后相机所有的操作都是通过Controller来执行
\\nfinal CameraXController _controller = CameraXController();\\n
\\n2.第二步在页面中创建相机预览widget,并和Controller绑定
\\nCameraPreviewWidget(\\n cameraController: _controller,\\n cameraOption: CameraOption(camera: CameraType.back),//相机参数,可以不设置\\n width: 100,//预览的宽高,基本上可以不设置,规则跟Container一样\\n height: 100,\\n cameraCallBack: (int? code, String? message) {\\n //相机错误的回调,可以不设置 \\n },\\n //切换摄像头,初始化等耗时操作的loading,可以自定义 \\n loadingWidget: const SizedBox(\\n width: 50,\\n height: 50,\\n child: CircularProgressIndicator(),\\n ), \\n)\\n
\\n3.之后就可以通过Controller进行相关操作了,Controller可以通过引用在任何widget中进行操作\\n拍照功能
\\n_button(\\n text: \'TakePicture\',\\n onTap: () async {\\n final image = await _controller.takePicture();\\n debugPrint(\'image file path:${image?.path}\');\\n },\\n)\\n
\\n录像功能
\\nif (_controller.isRecording()) {\\n _controller.stopRecording();\\n} else {\\n _controller.startVideoRecording(\\n max: 60,//视频录制最长时间,单位秒,不设置则没有限制 \\n onRecordFinish: (file) => debugPrint(\'video file path:${file.path}\'),\\n );\\n}\\n
\\n切换摄像头
\\n_button(text: \'switch\', onTap: () => _controller.switchCamera()),\\n
\\n相机一些初始化参数,基础开发可以不传,可以使用默认的就行
\\nCameraOption(\\n camera: CameraType.back,//摄像头类型 默认back\\n resolutionPresetType: ResolutionPresetType.veryHigh,//图片质量 默认high\\n enableAudio: true,//是否可以录音 默认false\\n flashType: FlashType.auto,//闪光灯 默认auto\\n)\\n
\\n扫描二维码功能只需要在widget中新增一个回调即可
\\nCameraPreviewWidget(\\n cameraController: _controller,\\n onScanSuccess: (list) {\\n //遍历List即可,因为一张图片里可能有多个二维码\\n for (var barcode in list) {\\n //value是二维码的内容,boundingBox是二维码图片的坐标\\n debugPrint(\'barcode value:${barcode.value} - box:${barcode.boundingBox}\');\\n }\\n },\\n)\\n
\\n也可以用Controller暂停/开启预览
\\n_controller.pausePreview();\\n_controller.resumePreview();\\n
\\n封装后flutter相机开发的代码量瞬间下降了不少,而且Controller和Widget进行了拆分,使得功能和布局也解耦了。
\\n最后是GitHub地址:github.com/cgztzero/Fl…
\\n欢迎各位多多交流,多多提bug和需求~~
\\n寒冬中抱团取暖,如果北京有合适的flutter或者Android岗位麻烦各位同学可以帮忙推荐~
","description":"最近在写纯净flutter的相机功能,但是发现flutter官方的相机库还是需要很多需要封装的东西,所以就写成了一个开源库。 PS:拍照加水印的功能正在筹备开发ing\\n\\n首先引入依赖\\n\\nflutter_camerax:\\n git:\\n url: https://github.com/cgztzero/FlutterCameraX.git\\n\\n\\n1.第一步创建一个相机Controller,之后相机所有的操作都是通过Controller来执行\\n\\nfinal CameraXController _controller = CameraXController…","guid":"https://juejin.cn/post/7469619471508144143","author":"没有机器猫的大雄","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-10T08:17:07.922Z","media":null,"categories":["前端","Flutter","GitHub"],"attachments":null,"extra":null,"language":null},{"title":"Flutter3.x深度融合短视频+直播+聊天app实例","url":"https://juejin.cn/post/7469337867589287987","content":"原创自研2025版Flutter3.27+Dart3.6
实战一款仿抖音短视频+直播+聊天功能为一体的商城app项目。
flutter3-mall实现了类似于抖音app首页联动滚动效果,左右滚动切换页面模块、上下滚动切换短视频,效果非常丝滑~~
\\n【手机app版】原创Flutter3.27仿抖音App商城|直播+短视频+聊天实例 - bilibili
\\n由于深度融合了短视频+直播+聊天三个大模块,整个项目涉及到的知识点非常多。
\\n如果想要了解更多的技术实现细节可以去看看下面这篇分享文章。
\\n\\n另外分享两篇flutter3实战微信聊天项目实例。
\\nflutter3-winchat桌面端exe聊天实例|Flutter3+Dart3+Getx仿微信Exe程序
\\nflutter3+dart3聊天室|Flutter3跨平台仿微信App语音聊天/朋友圈
","description":"原创自研2025版Flutter3.27+Dart3.6实战一款仿抖音短视频+直播+聊天功能为一体的商城app项目。 flutter3-mall实现了类似于抖音app首页联动滚动效果,左右滚动切换页面模块、上下滚动切换短视频,效果非常丝滑~~\\n\\n【手机app版】原创Flutter3.27仿抖音App商城|直播+短视频+聊天实例 - bilibili\\n\\n项目结构\\n\\n由于深度融合了短视频+直播+聊天三个大模块,整个项目涉及到的知识点非常多。\\n\\n如果想要了解更多的技术实现细节可以去看看下面这篇分享文章。\\n\\njuejin.cn/post/746813…\\n\\n另外分享两…","guid":"https://juejin.cn/post/7469337867589287987","author":"xiaoyan2015","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-10T01:42:08.282Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/95d9feceafcb49cf837b9bd0cb2496cd~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgeGlhb3lhbjIwMTU=:q75.awebp?rk3s=f64ab15b&x-expires=1739756527&x-signature=MuWsM3Reww%2FGG5qOnRbuW%2BZesJI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/84045aac0373453d82e1de92aeb96c0c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgeGlhb3lhbjIwMTU=:q75.awebp?rk3s=f64ab15b&x-expires=1739756527&x-signature=MLdlkDrAn9fqyrPeW8FxJtqtQec%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/63333b6b369847c6a68f4e19479d0207~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgeGlhb3lhbjIwMTU=:q75.awebp?rk3s=f64ab15b&x-expires=1739756527&x-signature=yDfN4frfFmpVgwD4iE03SUKXLh0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3d9199155afd4e5599881114248af6cb~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgeGlhb3lhbjIwMTU=:q75.awebp?rk3s=f64ab15b&x-expires=1739756527&x-signature=qikmoa30S4koWd6KdISncS5E0tg%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["前端","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"Flutter Widget 复用完整总结(终极版)","url":"https://juejin.cn/post/7469352181100199947","content":"StatelessWidget
( stl
)是否会复用?*� 结论: StatelessWidget
实例不会复用,但 Element
可能会复用!
StatelessWidget
在 setState()
时的行为StatelessWidget
,每次 setState()
后都会重新创建 Widget
实例StatelessWidget
的 Element
并不会被销毁,而是复用**StatelessWidget
来替换 Element
中的 oldWidget
✅ 示例
\\nclass StaticText extends StatelessWidget {\\n StaticText() {\\n print(\\"❗StaticText 被创建了!\\");\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n print(\\"🔄 StaticText.build() 被调用了!\\");\\n return const Text(\\"Hello World\\");\\n }\\n}\\n
\\n🚀 点击按钮 setState()
setState(() {});\\n
\\n📢 日志输出
\\n❗StaticText 被创建了! <-- `StatelessWidget` 重新创建\\n🔄 StaticText.build() 被调用了! <-- `build()` 重新执行\\n
\\n📌 StatelessWidget
本身被销毁并重新创建,但 Element
不变!
StatefulWidget
( stf
)如何复用?*� 结论: StatefulWidget
不会复用,但 State
和 Element
****可以 复用!
StatefulWidget
在 setState()
时的行为StatefulWidget
本身会被销毁并重新创建State
不会被销毁,它会在 Element
中复用State
的 build()
重新执行,不会创建新的 State
✅ 示例
\\nclass Counter extends StatefulWidget {\\n Counter() {\\n print(\\"❗Counter 被创建了!\\");\\n }\\n\\n @override\\n _CounterState createState() {\\n print(\\"🎯 CounterState 被创建了!\\");\\n return _CounterState();\\n }\\n}\\n\\nclass _CounterState extends State<Counter> {\\n int count = 0;\\n\\n @override\\n Widget build(BuildContext context) {\\n print(\\"🔄 Counter.build() 被调用了!\\");\\n return Column(\\n children: [\\n Text(\\"计数器: $count\\"),\\n ElevatedButton(\\n onPressed: () {\\n setState(() {\\n count++;\\n });\\n },\\n child: const Text(\\"增加\\"),\\n ),\\n ],\\n );\\n }\\n}\\n
\\n🚀 点击按钮 setState()
🔄 Counter.build() 被调用了!\\n
\\n📢 日志输出
\\n❗Counter 被创建了!\\n🎯 CounterState 被创建了! <-- `State` 只会创建一次\\n🔄 Counter.build() 被调用了! <-- `build()` 重新执行\\n
\\n📌 StatefulWidget
本身被销毁并重新创建,但 State
复用了!
Element
在 StatefulWidget
和 StatelessWidget
中的作用Element
在 setState()
时如何复用Element
树来管理 Widget
,而不是 Widget 树
runtimeType
和 Key
没有变,Flutter 会复用 Element
Widget
类型发生了变化,Flutter 会销毁旧 Element
,创建新 Element
✅ 示例
\\n@override\\nWidget build(BuildContext context) {\\n return toggle ? Container() : SizedBox(); // `runtimeType` 变了,Element 无法复用!\\n}\\n
\\n📢 点击 setState()
触发切换
Container() → SizedBox() <-- `runtimeType` 不同,不能复用 `Element`\\n
\\n✅ 正确写法(保证 Element
复用)
@override\\nWidget build(BuildContext context) {\\n return Container(\\n key: ValueKey(\\"same-widget\\"), // 🌟 添加 Key,保证 `Element` 复用\\n child: Text(\\"Hello\\"),\\n );\\n}\\n
\\n📢 现在即使 setState()
,Flutter 也不会销毁 Element
,而是复用它!
runtimeType
和 Key
如何帮助复用?*� 结论:Flutter 复用 Widget 主要依赖 runtimeType
和 Key
runtimeType
相同,Flutter 可以 复用Key
相同,即使 Widget
位置变化,Flutter 仍然 可以 复用runtimeType
不同,Flutter 无法 复用 Element
,会创建新的✅ 示例
\\n@override\\nWidget build(BuildContext context) {\\n return Column(\\n children: [\\n toggle ? Container() : SizedBox(), // ❌ `runtimeType` 不同,不能复用 `Element`\\n ],\\n );\\n}\\n
\\nKey
让 Widget
复用,即使位置发生变化@override\\nWidget build(BuildContext context) {\\n return Column(\\n children: toggle\\n ? [MyWidget(\\"A\\", key: ValueKey(\\"A\\")), MyWidget(\\"B\\", key: ValueKey(\\"B\\"))]\\n : [MyWidget(\\"B\\", key: ValueKey(\\"B\\")), MyWidget(\\"A\\", key: ValueKey(\\"A\\"))], \\n );\\n}\\n
\\n📢 加 Key
后,Flutter 仍然可以复用 Element
,提高性能!
行为 | StatelessWidget (stl ) | StatefulWidget (stf ) |
---|---|---|
Widget 是否会复用? | ❌ 不会,每次 setState() 触发,新建 Widget | ❌ 不会,每次 setState() 触发,新建 Widget |
Element 是否会复用? | ❌ 通常不会,除非 const | ✅ 会复用 |
State 是否会复用? | ❌ 没有 State ,无法复用 | ✅ 会复用,Flutter 只会执行 build() 重新渲染 |
setState() 触发时的行为 | 创建新 Widget ,替换 Element | 创建新 Widget ,但 State 仍然保持, Element 复用 |
如何优化? | ✅ const | ✅ Key |
📢 最重要的知识点
\\n1️⃣ StatelessWidget
不会复用,每次 setState()
重新创建,它的 Element
也通常被替换
\\n2️⃣ StatefulWidget
不会复用 Widget
本身,但它的 State
会被 Flutter 复用,Element
也尽可能复用
\\n3️⃣ Flutter 复用 Widget 依赖于
runtimeType 相同,并且
Key 也相同 4️⃣ 加
const 可以让
StatelessWidget` 彻底不变,进一步优化性能
✨ *� 这就是 Flutter Widget 复用的完整解析!!! 🚀🚀🚀
","description":"*� Flutter Widget 复用完整总结(终极版) 🎯 1. StatelessWidget ( stl )是否会复用?\\n\\n*� 结论: StatelessWidget 实例不会复用,但 Element 可能会复用!\\n\\n*� StatelessWidget 在 setState() 时的行为\\nFlutter 不会复用 StatelessWidget ,每次 setState() 后都会重新创建 Widget 实例\\n但是**StatelessWidget 的 Element 并不会被销毁,而是复用**\\nFlutter 只是用新的 Stateles…","guid":"https://juejin.cn/post/7469352181100199947","author":"呆萌呆萌怪兽","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-09T16:24:53.721Z","media":null,"categories":["iOS","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"一文教你更简单的使用event_bus","url":"https://juejin.cn/post/7468898013546512384","content":"一文教你更简单的使用event_bus,不再写繁杂的代码,手动调用取消订阅的方法。
\\nevent_bus
是 Dart 中一个非常实用的事件总线库,它基于发布 - 订阅模式,允许应用程序的不同部分之间进行松耦合的通信。
首先,你需要定义不同类型的事件,每个事件通常是一个简单的类。例如:
\\n// 定义事件类\\nclass UserLoggedInEvent {\\n final String username;\\n UserLoggedInEvent(this.username);\\n}\\n
\\nEventBus
实例在应用程序中创建一个 EventBus
实例,通常将其作为单例使用,以便在整个应用中共享。
import \'package:event_bus/event_bus.dart\'; \\n// 创建 EventBus 单例 \\nEventBus eventBus = EventBus();\\n
\\n使用 eventBus.on<T>()
方法来订阅特定类型的事件。这个方法返回一个 Stream
,你可以监听这个流来处理事件。
// 订阅 UserLoggedInEvent 事件\\nvar userLoggedInSubscription = eventBus.on<UserLoggedInEvent>().listen((event) {\\n print(\'User ${event.username} logged in.\');\\n});\\n\\n// 订阅 UserLoggedOutEvent 事件\\nvar userLoggedOutSubscription = eventBus.on<UserLoggedOutEvent>().listen((event) {\\n print(\'User logged out.\');\\n});\\n \\n
\\n使用 eventBus.fire()
方法来发布事件。
// 发布 UserLoggedInEvent 事件\\neventBus.fire(UserLoggedInEvent(\'JohnDoe\'));\\n\\n// 发布 UserLoggedOutEvent 事件\\neventBus.fire(UserLoggedOutEvent());\\n
\\n当你不再需要监听某个事件时,应该取消订阅以避免内存泄漏。
\\n// 取消订阅\\nuserLoggedInSubscription.cancel();\\nuserLoggedOutSubscription.cancel();\\n
\\n封装event_bus的目的可能有几个方面:简化API,添加日志或错误处理,支持单例模式,或者整合其他功能如依赖注入。我们希望有一个更友好的接口,比如通过注解自动订阅事件,或者统一管理事件监听器的生命周期,防止内存泄漏。
\\nevent_bus包本身提供了基本的发布和订阅功能,但需要我们手动管理订阅的取消。封装的时候,可以考虑自动取消订阅,比如结合Flutter的Widget生命周期,或者在Dispose时自动取消。或者,我们可能希望支持多种事件类型,或者对事件进行过滤和处理。可能还需要添加日志功能,方便调试事件触发和监听的情况。
\\n考虑错误处理。原生的event_bus可能在事件处理函数抛出异常时导致整个应用崩溃,封装时可以添加try-catch块,捕获异常并记录,避免应用崩溃。同时,允许我们自定义错误处理逻辑。
\\n另外,可能还需要支持不同的执行方式,比如同步和异步事件处理。原生的event_bus可能默认是同步的,但有时候异步处理更合适,封装时可以提供一个选项来选择执行方式。
\\n关于订阅管理,我不希望手动调用取消订阅的方法,而是希望通过某种方式自动管理。例如,在Flutter中,可以在StatefulWidget的dispose方法中自动取消订阅。这时候,封装类可能需要提供一个简便的方法,比如在注册事件监听时返回一个订阅对象,并在dispose时调用取消。
\\n接下来,需要添加一些高级功能,比如事件过滤,只接收特定条件下的事件,或者转换事件数据。或者支持多个不同的EventBus实例,以便在不同模块中使用独立的事件总线。
\\n现在,结合这些思考,我设计一个封装类的结构
\\n以下是针对 event_bus
的封装实现,提供 单例管理、自动取消订阅、日志跟踪、错误处理 等功能,并支持与 Flutter Widget 生命周期无缝集成:
\\n/// 封装后的高级事件总线\\nclass AppEventBus {\\n static final EventBus _instance = EventBus();\\n\\n // 私有构造,确保单例\\n AppEventBus._internal();\\n\\n /// 获取单例实例\\n static EventBus get instance => _instance;\\n\\n /// 发送事件\\n static void sendEvent<T>(T event) {\\n if (kDebugMode) {\\n print(\'[EventBus] Firing event: ${event.runtimeType}\');\\n }\\n instance.fire(event);\\n }\\n\\n /// 订阅事件,返回可取消的订阅对象\\n static StreamSubscription<T> on<T>(void Function(T event) handler, {\\n bool handleError = true,\\n ErrorCallback? onError,\\n }) {\\n final subscription = instance.on<T>().listen((event) {\\n if (kDebugMode) {\\n print(\'[EventBus] Received event: ${event.runtimeType}\');\\n }\\n _safeRun(() => handler(event), onError: onError);\\n }, onError: handleError ? (error, stack) {\\n _safeRun(() => onError?.call(error, stack));\\n } : null);\\n\\n return subscription;\\n }\\n\\n static void _safeRun(void Function() action, {ErrorCallback? onError}) {\\n try {\\n action();\\n } catch (e, s) {\\n if (kDebugMode) {\\n print(\'[EventBus] Handler error: $e\\\\n$s\');\\n }\\n onError?.call(e, s);\\n }\\n }\\n}\\n\\n/// Flutter Widget 集成扩展\\nmixin EventBusMixin<T extends StatefulWidget> on State<T> {\\n final List<StreamSubscription> _eventSubscriptions = [];\\n\\n /// 安全订阅事件,自动管理生命周期\\n void subscribe<Event>(void Function(Event event) handler, {\\n bool handleError = true,\\n ErrorCallback? onError,\\n }) {\\n _eventSubscriptions.add(\\n AppEventBus.on<Event>(handler, handleError: handleError, onError: onError)\\n );\\n }\\n\\n @override\\n void dispose() {\\n for (final sub in _eventSubscriptions) {\\n sub.cancel();\\n }\\n if (kDebugMode) {\\n print(\'[EventBus] Canceled ${_eventSubscriptions.length} subscriptions\');\\n }\\n super.dispose();\\n }\\n}\\n\\ntypedef ErrorCallback = void Function(Object error, StackTrace stackTrace);\\n
\\nimport \'package:flutter/material.dart\';\\n// 定义事件类\\nclass UserLoggedInEvent {\\n final String username;\\n UserLoggedInEvent(this.username);\\n}\\n\\n// 定义另一个事件类\\nclass DataUpdatedEvent {\\n final String data;\\n DataUpdatedEvent(this.data);\\n}\\n
\\n\\nclass EventBusPage extends StatefulWidget {\\n const EventBusPage({super.key});\\n\\n @override\\n State<EventBusPage> createState() => _EventBusPageState();\\n}\\n\\nclass _EventBusPageState extends State<EventBusPage> with EventBusMixin {\\n @override\\n void initState() {\\n super.initState();\\n print(\'initState\');\\n // 自动管理订阅\\n subscribe<UserLoggedInEvent>(handleLogin);\\n subscribe<DataUpdatedEvent>(dataUpdated);\\n }\\n\\n void handleLogin(UserLoggedInEvent event) {\\n print(\'User logged in: ${event.username}\');\\n\\n }\\n void dataUpdated(DataUpdatedEvent event) {\\n print(\\"更新数据:${event.data}\\");\\n }\\n\\n @override\\n void dispose() {\\n print(\'dispose---EventBusPage\');\\n super.dispose();\\n }\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: CustomAppBar(\\n title: \'EventBus\',\\n actions: [\\n ],\\n onBackPressed: () {\\n // Handle back button press, if needed\\n Navigator.pop(context);\\n },\\n ),\\n body: Container(\\n alignment: Alignment.center,\\n child: Column(\\n children: [\\n SizedBox(height: 50),\\n Container(\\n height: 50,\\n width: 200,\\n color: Colors.red,\\n child:\\n TextButton(onPressed: () {\\n // 发送事件\\n AppEventBus.sendEvent(UserLoggedInEvent(\'Alice\'));\\n }, child: Text(\\"用户昵称按钮\\"))),\\n SizedBox(height: 50),\\n Container(\\n height: 50,\\n width: 200,\\n color: Colors.red,\\n child:\\n TextButton(onPressed: () {\\n // 发送事件\\n AppEventBus.sendEvent(DataUpdatedEvent(\\"kkkkk\\"));\\n }, child: Text(\\"普通更新按钮\\"))),\\n ],\\n ),\\n ),\\n );\\n }\\n}\\n
\\nAppEventBus.instance
访问核心功能EventBusMixin
自动取消订阅StreamSubscription
便于管理虽然 FlutterUnit 应用支持了国际化,但是组件相关介绍信息的数据仍是中文。在新版的 FlutterUnit 组件数据设计中已经为组件数据的国际化埋下了伏笔。但是摆在面前的一座大山是:
\\n\\n\\n354 个组件的数据,如何翻译为多国语言。
\\n
DeepSeek 给了我一个可以基于 AI 批量完成翻译的机会。不得不说,DeepSeek 的 api 调用已经砍到白菜价了,普通的开发者自己玩玩也可以承受:
\\n另外登录 DeepSeek 后,在 控制台 可以看到会赠送 10 元的余额,所以不用白不用。本文我将基于 DeepSeek 的开发 api ,通过 Dart 的代码调用,完成 354 个组件 数据的 10 国 语言翻译:
\\n中文: zh_CN \\n英文: en_US\\n俄语: ru_RU\\n法语: fr_FR\\n韩文: ko_KR\\n德语: de_DE\\n日语: js_JP\\n意大利: it_IT\\n葡萄牙语: pt_PT\\n西班牙语: es_ES\\n
\\n首先,需要到 DeepSeek 的控制台中生成一个 API key 作为调用接口的令牌。
\\n这里通过 dio 进行网络请求,简单封装一个 AiChatApi 类维护 ai 聊天的逻辑处理:
\\nimport \'package:dio/dio.dart\';\\n\\nclass AiChatApi {\\n final String apiUrl = \'https://api.deepseek.com\';\\n final String apiKey = \'你的 api key\';\\n late Dio dio = Dio(BaseOptions(baseUrl: apiUrl));\\n}\\n
\\n这里简单写一个 translation 方法,输入待翻译的文本和对应的语言。后面有时间可以细致地封装一下,提供一个通用的 ai 会话功能。\\n官方的对话补全文档 中详细介绍了各个参数的作用。对当前的翻译需求,定制 env 提示词:
\\nFuture<String> translation(String src, String locale) async {\\n\\n String env = \\"你是一个翻译专家, \\"\\n \\"只需要将传入的 json 中文文本翻译为指定的 locale 语言即可,\\"\\n \\"输入中的数字和英文不用翻译。\\"\\n \\"不用返回 json 文本以外及任何额外的信息。\\";\\n \\n final Map<String, dynamic> data = {\\n \\"messages\\": [\\n {\\"content\\": env, \\"role\\": \\"system\\"},\\n {\\"content\\": \\"将 {$src} 翻译为 $locale\\", \\"role\\": \\"user\\"}\\n ],\\n \\"model\\": \\"deepseek-chat\\",\\n \\"frequency_penalty\\": 0,\\n \\"max_tokens\\": 2048,\\n \\"presence_penalty\\": 0,\\n \\"response_format\\": {\\"type\\": \\"text\\"},\\n \\"stop\\": null,\\n \\"stream\\": false,\\n \\"stream_options\\": null,\\n \\"temperature\\": 1.3,\\n \\"top_p\\": 1,\\n \\"tools\\": null,\\n \\"tool_choice\\": \\"none\\",\\n \\"logprobs\\": false,\\n \\"top_logprobs\\": null\\n };\\n \\n //接口调用见下:\\n}\\n
\\n会话补全使用 /chat/completions
接口,发生普通的 post 请求即可,在响应的数据中,choices 字段携带着我们需要的内容:
try {\\n final response = await dio.post(\\n \'/chat/completions\',\\n options: Options(headers: {\\n \'Content-Type\': \'application/json; charset=UTF-8\',\\n \'Accept\': \'application/json; charset=UTF-8\',\\n \'Authorization\': \'Bearer $apiKey\',\\n }),\\n data: data,\\n );\\n final dynamic repData = response.data;\\n if (repData != null) {\\n String result = repData[\'choices\'].first[\'message\'][\'content\'];\\n return result;\\n } else {\\n return \'\';\\n }\\n } catch (e) {\\n return \'Error: $e\';\\n }\\n
\\n另外 usage 字段中记录着当前请求消耗 token 的情况:
\\n有了翻译的方法,接下来只需要读取指定组件中的 desc_zh-CN.json
内容,将它交给 AI 翻译处理,在得到结果后,将内容写入到对应语言的文件里即可:
Future<void> translation(AiChatApi api, String widgetDir, String locale) async{\\n String inputName = r\'desc_zh-CN.json\';\\n String content = await File(p.join(widgetDir,inputName)).readAsString();\\n String ret = await api.translation(content, locale);\\n String outputName = \'desc_$locale.json\';\\n File distFile = File(p.join(widgetDir,outputName));\\n if(ret.isNotEmpty){\\n await distFile.writeAsString(ret);\\n print(\\"翻译成功:[${p.basename(widgetDir)}]::$locale\\");\\n }else{\\n print(\\"翻译失败:[${p.basename(widgetDir)}]::$locale\\");\\n }\\n}\\n
\\n比如下面的俄语,通过 WidgetDataL10nTool 进行处理后,即可将结果写入到 desc_ru_RU.json
文件中
main() async {\\n String widgetDir = r\'D:\\\\Projects\\\\Flutter\\\\Github\\\\FlutterUnit\\\\modules\\\\widget_system\\\\widgets\\\\lib\\\\StatelessWidget\\\\CloseButtonIcon\';\\n WidgetDataL10nTool tool = WidgetDataL10nTool();\\n String targetLocale = \'ru_RU\';\\n await tool.translation(widgetDir, targetLocale);\\n}\\n
\\n一个组件的一种语言处理完毕,接下来只需要遍历待翻译的九种语言,依次调用 translation
方法即可:
Future<void> translationWidget(String widgetDir) async {\\n List<String> locales = [\\n \'en_US\', \'ru_RU\', \'fr_FR\', \'ko_KR\', \'de_DE\',\\n \'js_JP\', \'it_IT\', \'pt_PT\', \'es_ES\'\\n ];\\n for (String locale in locales) {\\n await translation(widgetDir, locale);\\n }\\n }\\n
\\n运行一次之后,FlutterUnit 对应的组件就完成了 10 国语言的原始数据积累:
\\n一个文件的生成,通过遍历可以生成一批文件;同理,一个组件完成了目标,通过遍历可以完成全部的组件翻译任务。 在 FlutterUnit 的设计上,所有组件分为七大家族,每个家族的组件都放置在对应的文件夹中。所以可以先提供一个翻译家族的方法 translationFamily
\\nFuture<void> translationFamily(String family) async {\\n String familyDir = p.join(dataDir, family);\\n Directory directory = Directory(familyDir);\\n List<FileSystemEntity> entity = directory.listSync();\\n for (FileSystemEntity e in entity) {\\n if (e is Directory) {\\n await translationWidget(e.path);\\n }\\n }\\n}\\n
\\n一个家族的组件处理完毕,遍历七大家族即可完成全部组件的翻译任务,也就是下面 translationAll
方法。到这里就完成了 FlutterUnit 组件数据国际化的可行性。目前只是最简单的 \\"能用\\"
, 后期还可以强化一下任务过程中的日志、错误收集处理,甚至给出 UI 反馈翻译进度等功能。
final Map<int, String> familyMap = {\\n 0: \'StatelessWidget\',\\n 1: \'StatefulWidget\',\\n 2: \'SingleChildRenderObjectWidget\',\\n 3: \'MultiChildRenderObjectWidget\',\\n 4: \'Sliver\',\\n 5: \'ProxyWidget\',\\n 6: \'Other\',\\n};\\n\\nFuture<void> translationAll() async{\\n for(String family in familyMap.values){\\n await translationFamily(family);\\n }\\n}\\n
\\n不得不说,AI 的存在让很多不可能的事成为了可能。如果是传统翻译,中文介绍中的英文、数字很难控制其不处理。对于 AI 来说,调用者可以通过自然语言来描述翻译过程中的细节,AI 会推理理解,从而完美完成任务:
\\n试想一下,如果没有 AI 的支持,FlutterUnit 中 354 个组件的 10 国语言翻译,那就是 3540 个文件的翻译工作。这需要花费巨大的人力、财力、时间来处理,还不一定能翻译地到 AI 的水平。
\\n而接入 Deepseek 支持之后,我只是轻轻地敲了 一个小时 的代码,顺便还水了一篇文章,一边等待翻译任务的执行。预计也就 2 元左右的接口调用费,而且还是免费赠送的余额。
最近 deepseek 接口白天非常慢甚至不能用,晚上和清晨会好很多。等 FlutterUnit 仓库中全部组件数据的国际化后,会继续完善,通过 FlutterUnitTool 解析为多国语言,另外数据库也需要增加数据国际化的支持。巧妇难为无米之炊,现在米已经有了,那么离一席大餐也不远了,敬请期待真正 10 国语言支持的 FlutterUnit 。
","description":"0. 前言 虽然 FlutterUnit 应用支持了国际化,但是组件相关介绍信息的数据仍是中文。在新版的 FlutterUnit 组件数据设计中已经为组件数据的国际化埋下了伏笔。但是摆在面前的一座大山是:\\n\\n354 个组件的数据,如何翻译为多国语言。\\n\\nDeepSeek 给了我一个可以基于 AI 批量完成翻译的机会。不得不说,DeepSeek 的 api 调用已经砍到白菜价了,普通的开发者自己玩玩也可以承受:\\n\\n另外登录 DeepSeek 后,在 控制台 可以看到会赠送 10 元的余额,所以不用白不用。本文我将基于 DeepSeek 的开发 api…","guid":"https://juejin.cn/post/7468627492447240255","author":"张风捷特烈","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-08T01:10:43.839Z","media":[{"url":"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/670043e42df94ba79b7d8746d9cda1e8~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1920&h=1200&s=441803&e=png&b=fefbfb","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/53b24399aaa84e929fe3d2d3e5144b0c~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1012&h=404&s=52106&e=png&b=fffefe","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/be28b8da7c184961af1135d93393401a~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1829&h=773&s=128525&e=png&b=fdfdfd","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a9f768d271d046b1bca01286e6da159d~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1249&h=254&s=24099&e=png&b=fefdfd","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d40980f9ac074d7ea9b66288e2c53349~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=2125&h=502&s=78920&e=png&b=fdfdfd","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d66355dde479476eac6de0d241713850~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1033&h=646&s=92625&e=png&b=f3f4f7","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8fe6f1840cdf444a9645a842d95e81b2~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=626&h=215&s=15732&e=png&b=f2f5fa","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3d090762ab66439d885d5144c648af22~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=993&h=395&s=51425&e=png&b=f5f6f9","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/36730f2587c648908b7656b12270059e~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1037&h=551&s=75896&e=png&b=fffefe","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/899a8692a1bf434f91f6af40fd1958bc~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/27366569f38745ffaf9ba6d3445e5b87~tplv-k3u1fbpfcp-jj-mark:3024:0:0:0:q75.awebp#?w=1052&h=140&s=22848&e=png&b=fffefe","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","DeepSeek"],"attachments":null,"extra":null,"language":null},{"title":"【Cursor初体验】AI开发 Flutter 应用:一款快速预览SVGA的桌面小工具","url":"https://juejin.cn/post/7468627492446928959","content":"平时想预览SVGA动画,首先得打开预览网页,再把文件丢进去才能看到。
\\n这样实在太麻烦了😫,所以一直以来都想开发一款可以快速SVGA动画的桌面应用,可以直接双击文件就能打开应用进行预览,并带上动画信息。
\\n但是从没开发过桌面应用,对于我这种移动端开发,最好的选择是用Flutter
去开发,跨平台而且有SVGA组件库。不过由于太久没写Flutter
,生疏了,上手有点费神🤯。
所幸听闻 Cursor 很火热,AI编程耶,正好来一次初体验。
\\n其实开发过程没什么好说的,首先创建好Flutter
工程,然后用Cursor
打开,在COMPOSER
中给出自己的需求:
帮我开发一个可以预览SVGA动画的MacOS应用,以下是应用的需求说明: \\n\\n1. 主要功能 \\n - 可以预览SVGA动画\\n - 解析SVGA文件中的所有图片,组成列表预览,可以切换选择其中一张进行展示\\n - 需要展示SVGA的帧率、时间、内存占用等信息 \\n - 能够双击打开、拖拽放入、Finder选择SVGA文件\\n \\n2. UI需求 \\n - 左侧为图片列表,可以点击选择其中一张在右侧下半区进行展示\\n - 右侧上半为动画预览区,并带有动画信息(帧率、时间、内存占用)\\n - 右侧下半为图片列表中的单张图片展示区及其尺寸大小\\n
\\n接下来就等Cursor
去编辑代码了,当然不可能一步到位的,期间会有很多的小问题,发现后叫Cursor
去解决,如此循环。但是在这个过程中,我基本没写过一句代码,全程都是口语化交流,有种当老板在指导员工干活的感觉😏。
就这样,仅仅一个下午的时间,Cursor
就完成了我的需求,不得不感叹现在AI的强大🙇♂️。
最后就简单介绍一下该应用吧~
\\nsvga_previewer,一款快速预览SVGA的桌面小工具。
\\nFlutter
+Cursor
开发,主要支持MacOS平台(代码质量有待优化,毕竟用的AI快速开发🤖)。Github地址:github.com/Rogue24/JPS…
","description":"缘由 平时想预览SVGA动画,首先得打开预览网页,再把文件丢进去才能看到。\\n\\n这样实在太麻烦了😫,所以一直以来都想开发一款可以快速SVGA动画的桌面应用,可以直接双击文件就能打开应用进行预览,并带上动画信息。\\n\\n但是从没开发过桌面应用,对于我这种移动端开发,最好的选择是用Flutter去开发,跨平台而且有SVGA组件库。不过由于太久没写Flutter,生疏了,上手有点费神🤯。\\n\\n所幸听闻 Cursor 很火热,AI编程耶,正好来一次初体验。\\n\\n启动\\n\\n其实开发过程没什么好说的,首先创建好Flutter工程,然后用Cursor打开,在COMPOSER中给出自己的…","guid":"https://juejin.cn/post/7468627492446928959","author":"健了个平_24","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-07T17:29:10.076Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fa91e80a414d47bc8135c343e9fb752d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5YGl5LqG5Liq5bmzXzI0:q75.awebp?rk3s=f64ab15b&x-expires=1739554510&x-signature=FN5dAma%2BdYw7r8Z1AljBTbIM6vc%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["人工智能","Flutter","iOS","前端"],"attachments":null,"extra":null,"language":null},{"title":"独立开发项目阶段总结-需求只有自己有","url":"https://juejin.cn/post/7468564769030324251","content":"halo,大家好,俺是132,好久不贱
\\n过年了,精打细算又是一年,互联网从2022年泡沫破裂,之后一年比一年急剧坠落,到2024年已经惨不忍睹了,这还只是个开始,未来几年必定惨绝人寰
\\n这背后的原因并不是大家技术不到位,实际上前端后端安卓ios也就那点东西,老油条基本都全通了,技术门槛是极低的
\\n主要还是需求太少了,招聘太少了,诺大的上海就没几家在招的好坑,基本都是挂羊头卖狗肉,也就是明明没有需求,招点人勉强背锅苟活
\\n既然外面的世界没有需求,那就自己制造需求
\\n也就是独立开发啦,俺今年的独立开发项目主要还是两个 APP,接下来一一道来
\\nc站是一款动漫app,成立于七年前,每次都觉得已经很完善了的时候,过阵子就会冒出来新需求
\\n\\nc站的主体没什么好说的,它就是一个普通的动漫视频软件,就都差不多,今年主要是新增了一些小需求,如下载,外站源同步,等
除了主体,今年的一个比较大的更新是,里世界复活了
\\n里世界的前身是acg和谐区,acgzone,老二次元都知道,是c站之前俺搞得一个动漫资源网站
\\n现在里世界的复活,意味着重启二次元种草,大家都可以继续分享本子,galgame啥的了
\\n除了c站,今年还开了一个新坑,也就是广播剧软件,它整体上是一个音乐软件,只不过它没有“歌词”但是有“弹幕”
\\n值得一提的是,c站是flutter,但新软件没有使用 flutter,而是 kotlin 原生写的,主要是 flutter 感觉快要倒闭了,而且c站也有很多 flutter 做不了的事情,不得不写原生插件,所以我决定,还是得多写写原生
\\n音频软件总体上是比视频软件要简单不少的,但因为用户群体完全变成了女性向,这块肉也不见得吃相好看
\\n另外,新软件俺可以挂一个slogan:【前洗马员工开发的新fm软件】,啊哈哈哈
\\n想当年校招的时候,俺去洗马是本着广播剧去的,结果去了才发现,大家都不知道广播剧是啥
\\n除此之外,不得不提一下个人支付
\\nc站的支付是使用的支付宝当面付,但这个接口现在只能生成二维码,不能h5直接跳转支付宝的url schema了,可以说非常尴尬了,当面付是唯一一个对个人开放的支付接口
\\n广播剧这边,我将采用一种新的方式,也就是【兑换码】
\\n类似原神兑换码机制,在淘宝或闲鱼卖兑换码,然后在app内兑换即可√
\\n这两种机制可以共存,也是目前个人免费情况下,唯一的支付方案了(总比对接各种第三方支付sdk容易维护)
\\n一些新的 idea
\\n很多人都说独立开发作者都只敢说表面数据,就是不敢说具体是啥产品
\\n很多人都不知道独立开发应该做什么
\\n其实很简单,你平时喜欢用什么,就做什么,特别需要关注的是,你用过,但倒闭的,产品
\\n比如我自己喜欢动漫,游戏,小说,广播剧,等等
\\n比如我用过半次元,白熊阅读,tape,这些都倒闭了
\\n接下来就说一些我平时用到的一些新 idea 吧
\\n大家应该都用过提瓦特小助手这类手游助手,用来分析抽卡记录,角色练度等
\\n但有没有一种可能,不用再去艰难的获取抽卡链接了?
\\n答案是有的,也就是 VPN 抓包
\\n原理就是安卓自己开一个VPN,自己开一个tcp server,自己将手机流量转发到自己的server,最终自己代理自己
\\n这玩意真是心心念念了很久,也只有原生可以做,flutter 也只能调原生插件
\\n参考文章:www.jianshu.com/p/ae4d43359…
\\n众所周知,俺是小程序框架专家,但是微信这套小程序框架还是太重了,本质是一个 hybrid 多 webview 框架
\\n我想要一个更简单的 webview 方案,此时 telegram miniapp 出现了,这玩意就类似微信公众号
\\n网址:docs.telegram-mini-apps.com/platform/ab…
\\n就还是比较简单,准备实现一套这个的 api 给 c站,让 tg 的小程序可以直接跑到 c站
\\n这个先不急,类似 tape,因为 tape 也符合俺刚刚说的,自己用过倒闭的软件,这个设定
\\n也是心心念念,目前c站的弹幕是类似b站那种满屏弹幕
\\n这种弹幕的体验不太好,默认看番都是直接关掉,俺比较喜欢腾讯视频的“单行弹幕”
\\n这种一行弹幕就非常舒服,只有一行字幕在上面飘,体验非常棒
\\n当然了,之所以要重写弹幕,还有很大的原因是隔壁广播剧
\\n广播剧的台词,歌词,都需要利用底部弹幕来做,这也是很重要的,这一整套逻辑需要重新整理了
\\n\\n以上
\\n总结
\\n新的一年里,主要目标还得是广播剧app,c站已经基本定型,啥功能都不缺了,也基本可以说探索出了flutter中视频的最佳实践,而隔壁广播剧,则是重新探索,kotlin compose 中,音频/弹幕等的最佳实践
","description":"halo,大家好,俺是132,好久不贱 过年了,精打细算又是一年,互联网从2022年泡沫破裂,之后一年比一年急剧坠落,到2024年已经惨不忍睹了,这还只是个开始,未来几年必定惨绝人寰\\n\\n这背后的原因并不是大家技术不到位,实际上前端后端安卓ios也就那点东西,老油条基本都全通了,技术门槛是极低的\\n\\n主要还是需求太少了,招聘太少了,诺大的上海就没几家在招的好坑,基本都是挂羊头卖狗肉,也就是明明没有需求,招点人勉强背锅苟活\\n\\n既然外面的世界没有需求,那就自己制造需求\\n\\n也就是独立开发啦,俺今年的独立开发项目主要还是两个 APP,接下来一一道来\\n\\nc站\\n\\nc站是一款动漫…","guid":"https://juejin.cn/post/7468564769030324251","author":"132","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-07T12:34:59.508Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a01eca168217483aadc74a01bf4b4db9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgMTMy:q75.awebp?rk3s=f64ab15b&x-expires=1739536498&x-signature=oiWtlTjzlBBIUolBCL38h5Mo8AQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/50965e9742e740ff9fded3feef64bd60~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgMTMy:q75.awebp?rk3s=f64ab15b&x-expires=1739536498&x-signature=y%2Fx8WmOA5GQ0t%2BJVtebCuFCpwIE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/8d4249bf0b3f4efd868f1de9334cfd70~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgMTMy:q75.awebp?rk3s=f64ab15b&x-expires=1739536498&x-signature=Xm8IeTYrKpTa05iyGTp1OBVFNsk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/26d2ba7183274744bdb00e7b5af2bc90~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgMTMy:q75.awebp?rk3s=f64ab15b&x-expires=1739536498&x-signature=pv7Fp1dzIhpyiSAu32%2FzSjAWJqk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c7e6e879acd54f9b9ffd437fb81c2af2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgMTMy:q75.awebp?rk3s=f64ab15b&x-expires=1739536498&x-signature=DMbIKwiFOV39HyZZEP4LmjuvEiw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/633cb078acd1412797a1e051fe1dc04c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgMTMy:q75.awebp?rk3s=f64ab15b&x-expires=1739536498&x-signature=dUvpWdtk8IICyDX4hN94Z3g9Mvw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b83550afff8840a3ba9937374bc6b91d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgMTMy:q75.awebp?rk3s=f64ab15b&x-expires=1739536498&x-signature=RwWmnehkg4Zrf6T8Gb31zp%2Ba%2FHU%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["前端","Flutter","Android"],"attachments":null,"extra":null,"language":null},{"title":"flutter程序上架app store出现4.3?看这","url":"https://juejin.cn/post/7468142769661313076","content":"最近很多人上架应用程序出现了4.3,而且被打回的速度非常快,这大概率是被机器检测到代码重复了,有可能是你用了小众的插件,而这个插件还没被应用商店记录,不过一般热门的插件都记录了,所以要解决这部分问题大家可以选择使用热门插件,比如getx,provider等官方的。
\\n一些第三方库也可能被检测,比如融云极光和腾讯云等库,这种就需要主动声明下使用场景,一般都能过审,之前还有些乐子说flutter3.7开始无论是什么app,只要提交就4.3,我不知道你们遇到过没,我反正没遇到。
\\n现在来说说重点,有些有隐藏包需求的企业可能会想着隐藏些内容不被发现,然后上架成功之后就开启跳转正常的app,具体例子我就不说了,不管海内外到处都有已经实现的,不过他们都是自己手动重写,成本巨大,如果只是小公司根本承担不起,除非小个体开发者用时间磨,这样也可以,但是有没有更简单的方式呢?
\\n有!我目前上架了5个海外程序,只有小部分界面用的不一样,其内部程序开启开放模式之后内容完全一致,也上架成功了,具体逻辑大家可以看开源地址:
\\n\\nflutter代码混淆工具,解决iOS上架4.3问题,代码完全更改为另一份,不影响运行效果,模型字段一键混淆、图片等文件一键混淆压缩、整个项目混淆、解决上架问题、解决审核拘审代码重复问题,正在更新中。
","description":"前言 最近很多人上架应用程序出现了4.3,而且被打回的速度非常快,这大概率是被机器检测到代码重复了,有可能是你用了小众的插件,而这个插件还没被应用商店记录,不过一般热门的插件都记录了,所以要解决这部分问题大家可以选择使用热门插件,比如getx,provider等官方的。\\n\\n一些第三方库也可能被检测,比如融云极光和腾讯云等库,这种就需要主动声明下使用场景,一般都能过审,之前还有些乐子说flutter3.7开始无论是什么app,只要提交就4.3,我不知道你们遇到过没,我反正没遇到。\\n\\n重点\\n\\n现在来说说重点,有些有隐藏包需求的企业可能会想着隐藏些内容不被发现…","guid":"https://juejin.cn/post/7468142769661313076","author":"CrazyQ1","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-06T09:09:45.474Z","media":null,"categories":["Android","Flutter","iOS"],"attachments":null,"extra":null,"language":null},{"title":"Flutter进阶:全局音视频流进入后台停止播放","url":"https://juejin.cn/post/7467570649960628250","content":"商业级 Flutter 应用中因为场景的多样性,很难避免其中包含多个音频和视频播放器(例如 video_player、just_audio、audioplayers、audio_session)。当应用生命周期发生变化时(例如,应用转入后台或分离),需要停止或暂停所有正在运行的媒体流,而无需手动管理每个音频或视频实例。
\\n在每个播放组件的 didChangeAppLifecycleState() 中暂停每个实力。使用生命周期钩子进行手动管理,但是很麻烦。如果要二次调整停止流媒体时机,每个地方都需要修改,维护性差。
\\n通过订阅模式持有方法指针,全局统一管理,完美解决问题。
\\n1、实现音视频管理单例类
\\nimport \'dart:io\';\\n\\nimport \'package:audio_session/audio_session.dart\';\\nimport \'package:flutter/foundation.dart\';\\n\\n/// AudioSession 音视频管理类\\nclass AudioSessionManager {\\n static final AudioSessionManager _instance = AudioSessionManager._();\\n AudioSessionManager._();\\n factory AudioSessionManager() => _instance;\\n\\n\\n /// 监听列表(实现音频统一管理)\\n final List<AudioSessionSoundPlayerModel> _listeners = [];\\n\\n // 添加监听\\n void addListener(AudioSessionSoundPlayerModel cb) {\\n if (_listeners.contains(cb)) {\\n return;\\n }\\n _listeners.add(cb);\\n }\\n\\n // 移除监听\\n void removeListener(AudioSessionSoundPlayerModel cb) {\\n _listeners.remove(cb);\\n }\\n\\n // 通知所有监听器\\n Future<void> notifyListeners(Future<void> Function(AudioSessionSoundPlayerModel e) action) async {\\n for (var ltr in _listeners) {\\n await action(ltr);\\n }\\n }\\n\\n void clearListeners() {\\n _listeners.clear();\\n }\\n}\\n\\nclass AudioSessionSoundPlayerModel {\\n AudioSessionSoundPlayerModel({\\n this.data,\\n this.onPlay,\\n this.onStop,\\n });\\n\\n /// 唯一值\\n Map<String, dynamic>? data;\\n\\n /// 播放\\n Future<void> Function()? onPlay;\\n\\n /// 停止播放\\n Future<void> Function()? onStop;\\n\\n AudioSessionSoundPlayerModel.fromJson(Map<String, dynamic>? json) {\\n if (json == null) {\\n return;\\n }\\n data = json[\'data\'] ?? {};\\n onPlay = json[\'onPlay\'];\\n onStop = json[\'onStop\'];\\n }\\n\\n Map<String, dynamic> toJson() {\\n final data = Map<String, dynamic>();\\n data[\'data\'] = data;\\n data[\'onPlay\'] = onPlay.hashCode;\\n data[\'onStop\'] = onStop.hashCode;\\n return data;\\n }\\n\\n @override\\n bool operator ==(Object other) {\\n if (identical(this, other)) {\\n return true;\\n }\\n\\n final isEqual = other is AudioSessionSoundPlayerModel &&\\n runtimeType == other.runtimeType &&\\n mapEquals(toJson(), other.toJson());\\n return isEqual;\\n }\\n\\n @override\\n int get hashCode => data.hashCode ^ onPlay.hashCode ^ onStop.hashCode;\\n}\\n
\\n2、注册到 AudioSessionManager 示例:
\\nclass SoundPlayerAndRecorderState extends State<SoundPlayerAndRecorder>\\n with AutomaticKeepAliveClientMixin, SafeSetStateMixin {\\n \\n /// current audio model\\n AudioSessionSoundPlayerModel get audioSessionSoundPlayerModel => AudioSessionSoundPlayerModel(\\n data: widget.model?.toJson(),\\n onPlay: onPlay,\\n onStop: onStop,\\n );\\n\\n...\\n\\n Future<void> onPlay() async {\\n await AudioSessionManager().notifyListeners((e) async {\\n await e.onStop?.call();\\n });\\n\\n AudioSessionManager().addListener(audioSessionSoundPlayerModel);\\n\\n //your audio play codes\\n \\n }\\n\\n\\n Future<void> onStop() async {\\n //your audio stop play codes\\n\\n AudioSessionManager().removeListener(audioSessionSoundPlayerModel);\\n }\\n\\n ...\\n}\\n
\\n3、监听app生命周期回调方法中
\\n switch (state) {\\n case AppLifecycleState.inactive:\\n {\\n AudioSessionManager().notifyListeners((e) async {\\n await e.onStop?.call();// 停止所有音视频播放\\n });\\n }\\n break;\\n default:\\n debugPrint(\\"$state\\");\\n }\\n
\\n在项目开发中,因为场景众多,很难避免各种疑难问题。这时候就需要我们发散思维,结合现有知识跳出窠臼,提出创造性的解决办法,维护时会省时省力。方法1虽然也能解决问题,但维护性较差,方法2才是兼顾维护性和扩展性的最佳方法。
\\n","description":"应用程序生命周期改变全局停止所有正在运行的音频和视频流? 商业级 Flutter 应用中因为场景的多样性,很难避免其中包含多个音频和视频播放器(例如 video_player、just_audio、audioplayers、audio_session)。当应用生命周期发生变化时(例如,应用转入后台或分离),需要停止或暂停所有正在运行的媒体流,而无需手动管理每个音频或视频实例。\\n\\n解决办法 1:\\n\\n在每个播放组件的 didChangeAppLifecycleState() 中暂停每个实力。使用生命周期钩子进行手动管理,但是很麻烦。如果要二次调整停止流媒体时机…","guid":"https://juejin.cn/post/7467570649960628250","author":"SoaringHeart","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-05T04:55:04.213Z","media":null,"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"系统化掌握 Dart 编程之异常处理(二):从防御到艺术的进阶之路","url":"https://juejin.cn/post/7466363839857721370","content":"系统化思维 —— 像建筑师一样设计代码的“安全网”
编程中的异常处理,就像建筑师在设计大楼时考虑的“抗震结构”
。
try-catch
防止程序崩溃,如同给大楼装上灭火器
。输入校验
)、精准捕获(类型匹配
)、优雅恢复(用户提示
)、全局兜底(日志监控
)四个维度构建多层防御体系,如同设计防火隔离带、逃生通道和智能报警系统。本文将通过三个阶段(基础防御
→异步战场
→全局设计
)、四大模块(语法
、异步
、自定义
、哲学
),带你用系统化思维掌握Dart
异常处理的完整知识框架。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\n基础防御 —— 掌握异常处理的“语法武器库”
1、敌人类型:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n异常类型 | 典型场景 | 应对策略 |
---|---|---|
Exception | 文件不存在、网络超时 | 捕获后重试或提示用户 |
Error | 内存溢出、栈溢出 | 记录日志并终止程序 |
自定义异常 | 业务规则校验失败(如密码强度) | 精准提示用户具体原因 |
2、关键工具:
\\n// 精准捕获:按类型分类处理\\ntry { ... }\\non FileSystemException catch (e) { ... } // 处理文件异常\\non FormatException { ... } // 处理数据格式错误\\ncatch (e, s) { ... } // 兜底捕获所有异常\\n
\\nfinally
的哲学)→
内存泄漏。File file;\\ntry {\\n file = File(\'data.txt\');\\n await file.readAsString();\\n} catch (e) { ... }\\nfinally {\\n await file?.close(); // 无论是否异常,必须释放资源\\n}\\n
\\n Dart
的await
与finally
结合,确保异步操作中资源释放的原子性
。异步战场 —— 征服Future
、Stream
与Isolate
的异常
Future
的异常链式反应Future
嵌套时,异常可能被中间环节“吞没”
。fetchUserData()\\n.then((data) => process(data)) // 成功→处理数据\\n.catchError((e) => handleError(e)) // 统一捕获所有环节的异常\\n.whenComplete(() => cleanUp()); // 等效于finally\\n
\\nDart
的Future
异常会沿着调用链向上冒泡,直到被catchError
捕获。Stream
的异常洪流控制Stream
持续产生数据时,某个数据包引发异常 →
整个流终止。stream\\n.handleError((e) => log(e)) // 处理异常但继续接收数据\\n.listen((data) => ...);\\n
\\nStreamTransformer
包装异常处理逻辑,实现复用:final safeTransformer = StreamTransformer<int, String>.fromHandlers(\\n handleData: (value, sink) => ...,\\n handleError: (e, s, sink) => sink.add(\'错误: $e\'),\\n);\\ninputStream.transform(safeTransformer).listen(...);\\n
\\nIsolate
的异常隔离与通信Isolate
中未捕获的异常 → 导致整个Isolate
崩溃,主线程无感知。// 主Isolate\\nvoid main() async {\\n final receivePort = ReceivePort();\\n final isolate = await Isolate.spawn(\\n _worker,\\n receivePort.sendPort,\\n onError: receivePort.sendPort, // 错误发送到主端口\\n );\\n\\n receivePort.listen((message) {\\n if (message is List) { // 错误信息格式:[错误, 堆栈]\\n print(\'Worker异常: ${message[0]}\\\\n堆栈: ${message[1]}\');\\n }\\n });\\n}\\n\\n// 子Isolate\\nvoid _worker(SendPort sendPort) {\\n try { ... }\\n catch (e, s) { Isolate.current.kill(priority: Isolate.immediate); }\\n}\\n
\\n全局设计 —— 从异常处理到代码哲学
“三层金字塔”
模型try-catch
(工具
)规则
)系统
)// 全局捕获未处理异常(Flutter示例)\\nvoid main() {\\n FlutterError.onError = (details) => reportError(details.exception);\\n \\n runZonedGuarded(\\n () => runApp(MyApp()),\\n (error, stack) => reportError(error, stack),\\n );\\n}\\n
\\n1、反模式:
\\nUI
层直接处理异常 →
业务逻辑与错误提示耦合。2、系统化方案:
\\nPaymentFailedException
)。// 领域层\\nclass PaymentFailedException implements Exception { ... }\\n\\n// 应用层\\nvoid pay() async {\\n try { ... }\\n on PaymentFailedException catch (e) {\\n return PaymentResult.failure(e.message); // 转换为结果对象\\n }\\n}\\n\\n// UI层\\nElevatedButton(\\n onPressed: () async {\\n final result = await pay();\\n if (result.isFailure) showToast(result.message); // 无try-catch\\n },\\n)\\n
\\nObservability
)1、黄金指标:
\\nError Rate
)按类型、模块
)异常发生的页面、设备
)2、实现方案:
\\nvoid reportError(dynamic error, StackTrace stack) {\\n final errorInfo = {\\n \'type\': error.runtimeType.toString(),\\n \'message\': error.toString(),\\n \'stack\': stack.toString(),\\n \'user\': currentUser.id,\\n \'page\': currentRoute.name,\\n };\\n AnalyticsService.log(\'error\', errorInfo); // 上报到监控平台\\n}\\n
\\n系统化思维的胜利 —— 让异常处理成为代码的“免疫系统”
通俗总结:
\\n“皮肤屏障”
,异步处理是“白细胞巡逻”
,全局监控是“淋巴网络”
,三者协同构建代码的自我修复能力。80%
的异常用基础try-catch
防御,15%
用自定义异常精准拦截,5%
的未知错误交给全局监控兜底。深度思考:
\\n系统化的异常处理,本质是对“不确定性”的管理:
网络波动
、硬件故障
、用户误操作
永远存在。分层处理
,将不确定性限制在可控范围内。监控异常数据
→
分析高频错误
→
迭代防御策略,形成闭环
。当你不再把异常处理视为“边角料”
,而是作为代码的核心架构来设计时,便是系统化思维的真正觉醒。
\\n","description":"前言 系统化思维 —— 像建筑师一样设计代码的“安全网”\\n\\n编程中的异常处理,就像建筑师在设计大楼时考虑的“抗震结构”。\\n\\n初级开发者:仅知道用try-catch防止程序崩溃,如同给大楼装上灭火器。\\n系统化思维者:会从异常预防(输入校验)、精准捕获(类型匹配)、优雅恢复(用户提示)、全局兜底(日志监控)四个维度构建多层防御体系,如同设计防火隔离带、逃生通道和智能报警系统。\\n\\n本文将通过三个阶段(基础防御→异步战场→全局设计)、四大模块(语法、异步、自定义、哲学),带你用系统化思维掌握Dart异常处理的完整知识框架。\\n\\n操千曲而后晓声,观千剑而后识器。虐它千…","guid":"https://juejin.cn/post/7466363839857721370","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-02T03:47:38.117Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/573a3197eee640849859971dc277d706~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1739072858&x-signature=betI1EW9iX1nOBOuxUqjOhfNiRw%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"从0到1掌握Flutter(一)Flutter与移动端跨平台","url":"https://juejin.cn/post/7465994044336275482","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
Flutter是一款由 Google 开发供支持的跨平台开源框架。
\\n在2018年发布时Flutter主要被用于移动端跨平台开发,Flutter3.0发布以后,支持在六个平台上进行应用程序开发:iOS、Android、Web、Windows、MacOS 和 Linux。
\\n尽管多平台适配能力不断提升,iOS和Android仍是其核心应用场景:
\\n本文也将以移动端视角展开讨论
\\nFlutter无论从开发效率、性能表现,还是从企业成本的角度来看,Flutter 都提供了独特的解决方案。接下来我们来看一下为什么 Flutter 能够成为开发者的首选框架,以及它如何在实际项目中创造价值。
\\n在传统的原生开发中,通常我们需要维护Android和iOS两个开发团队,导致版本迭代时的人力和测试成本加大。对一线的开发者来说,也许并不太关注这个问题。
\\n但是,如果从公司的角度出发,有一套可以直接开发出Android的apk和iOS的代码,意味着公司只需要一个团队就能维护这套代码。这样就能在人力成本上节省开支。而在开发方面,这套代码能有效实现复用,简化测试,并统一UI风格。
\\n而Flutter就是这样一个能帮我们实现跨平台开发的框架,它可以帮助你:
\\n提高开发效率
\\n同一份代码开发iOS和Android
\\n用更少的代码做更多的事情
\\n轻松迭代
\\n创建美观,高度定制的用户体验
\\n受益于使用Flutter框架提供的丰富的Material Design和Cupertino(iOS风格)的widget
\\n实现定制、美观、品牌驱动的设计,而不受原生控件的限制
\\nFlutter 的这些优势不仅体现在技术层面,更在实际应用中得到了广泛验证。
\\n随着越来越多的企业和开发者选择 Flutter,它在行业中的地位和影响力也日益凸显。
\\n\\n\\n“Apptopia 跟踪 Apple AppStore 和 Google Play Store 中的数百万个应用,并分析和检测哪些开发人员 SDK 用于创建这些应用,Flutter 是跟踪的最受欢迎的 SDK 之一:在 Apple AppStore 中 它的使用量从 2021 年所有跟踪免费应用的 10% 左右稳步增长到 2024 年所有跟踪免费应用的近 30%!
\\n
Flutter官方最新统计数据显示:
\\n开发者基数:月活跃开发者突破100万大关
\\n市场渗透率:支撑近30%新发布iOS应用(数据源: Apptopia Inc. )
\\n社区规模:覆盖全球60+国家/地区,累计9万开发者参与本地社区活动
\\n另外,根据行业调研显示,国内头部互联网企业已普遍将Flutter应用于核心业务场景:
\\n字节跳动(今日头条)、腾讯(微信生态工具)、阿里巴巴(闲鱼)、美团等企业的关键产品线均已深度集成Flutter框架。这些技术选型决策印证了Flutter在实现高性能渲染、保持跨端UI一致性、支持动态化更新等核心能力上的技术优势。
\\nFlutter 的崛起并非偶然,而是移动开发技术演进的必然结果。要理解 Flutter 的价值,我们需要回溯跨平台技术的发展历程。这段进化史不仅揭示了技术发展的内在逻辑,也为我们理解 Flutter 的设计理念提供了重要背景。
\\n原生开发作为移动开发的\\"黄金标准\\",其不可替代的核心优势主要体现在:
\\n极致性能:极致流畅体验
\\n硬件支持:深度系统集成
\\n新特性支持:直接支持5G/蓝牙5.0/NFC等硬件能力
\\n传感器访问:精准获取陀螺仪、气压计等数据
\\n相机控制:支持手动对焦、RAW格式等专业模式
\\n权限管理:细粒度控制硬件访问权限
\\nGPU加速:Metal/Vulkan图形API直接调用
\\n然而,如果把移动开发比作造车,早期原生开发就像同时维护两条独立生产线——Android和iOS工程师如同分别制造燃油车和电动车,每个螺丝都要拧两遍。产品经理说:\'这个动效Android和iOS能不能做得一模一样?\' 就像要求油车和电车引擎声要同步轰鸣。
\\n有一个典型痛点场景,当产品提出\\"修改全局圆角半径\\"需求时:
\\n这些痛点促使开发者们寻求更高效的解决方案,跨平台技术正是在这样的背景下应运而生,开启了移动开发的新阶段。
\\n当Android与iOS还在争夺移动市场时,Adobe的PhoneGap(后开源为Cordova)率先给出跨平台方案。PhoneGap是一个快速开发平台,允许开发者使用HTML、CSS和JavaScript等Web技术来创建跨平台的移动应用程序。这个方案的核心思路颇具巧思——将Web技术嵌入原生容器。其架构可拆解为三个关键层:
\\nWeb层:HTML/CSS构建界面,JS处理逻辑
\\n桥接层:JsBridge实现双向通信
\\n原生层:通过插件机制扩展设备能力
\\n这套看似简单的架构,却带来了三重变革性优势
\\n开发效率飞跃:复用Web技术栈,前端开发者转型使成本大幅降低
\\n动态更新能力:服务端热更新HTML,紧急修复无需发版审核
\\n跨平台一致性:统一CSS适配方案,双端UI差异率低
\\n尽管PhoneGap/Cordova提供了跨平台的便利,但也面临一些性能上的挑战。
\\nAndroid的WebView组件渲染效率相对较低
\\nJavaScript作为解释型语言,其执行性能也不如编译型语言。这意味着在执行复杂任务时,性能可能会受到限制。
\\n由于Android系统自身的内存管理问题,WebView在使用过程中消耗的内存无法在不需要时及时回收,这可能导致内存泄漏和最终的OOM。
\\n因此,尽管PhoneGap/Cordova为跨平台开发提供了极大的便利,但在实际应用中仍需注意其性能瓶颈和内存管理问题,以确保应用的稳定性和用户体验。
\\n这意味着需要更彻底的架构革新,而不是将Web嵌入原生,而是让原生组件能被动态编排。正是这种认知转变,为React Native的出现铺平了道路。
\\n当WebView方案的性能天花板愈发明显时,Facebook在React Web框架大获成功的背景下,推出了移动端革命性方案——React Native(RN)。
\\n在RN的工作机制中,开发者编写的仍然是JavaScript代码。这些代码首先被用来构建一个Virtual DOM,随后通过Bridge将这个虚拟DOM传递给原生层进行UI的创建和渲染。
\\n这一流程与原生开发中的XML布局解析过程颇为相似:
\\n// JSX声明\\n<View style={styles.container}>\\n <Text>Hello World</Text>\\n</View>\\n\\n// 实际映射过程\\n// Android端:\\nJS View → android.view.ViewGroup\\nJS Text → android.widget.TextView\\n\\n// iOS端: \\nJS View → UIView\\nJS Text → UILabel\\n
\\n两种开发方式对比:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n开发方式 | Android原生开发 | React Native |
---|---|---|
UI编码 | XML布局文件 | JSX组件树 |
解析引擎 | LayoutInflater | Yoga布局引擎 |
组件创建 | 反射实例化View对象 | 映射为各平台的组件 |
更新机制 | 直接修改View属性 | 虚拟DOM Diff后批量通过Bridge更新 |
然而,与原生开发中的静态XML布局不同,RN中的UI更新需要通过Bridge再次进行数据传输。同样地,在RN中调用原生API也需要通过Bridge进行传递。这种频繁的跨桥调用无疑增加了通信成本,使得RN的性能无法完全媲美原生应用。这推动着跨平台技术向无桥接架构进化,而Flutter正是这个进化方向的终极答案...
\\n当React Native还在通过Bridge与原生组件交互时,Flutter选择了一条更彻底的技术路径。与RN类似,Flutter也旨在解决跨平台开发中的性能瓶颈和代码复用问题,但它采取了一种截然不同的策略——完全自主的渲染体系。
\\n这种方案的核心在于:
\\n语言革新:采用Dart语言,通过Dart虚拟机(DVM)实现JIT(即时编译)与AOT(预编译)的双模式支持,兼顾开发效率与运行性能。
\\n渲染自主:基于高度优化的2D渲染引擎——Skia直接控制像素绘制,无需依赖平台控件,确保跨平台渲染一致性。
\\n架构统一:从UI描述到渲染执行的全链路控制,DVM作为核心执行引擎,负责Dart代码的高效运行和资源管理。
\\nFlutter采用清晰的两层架构设计:
\\nFramework层
\\nEngine层
\\nFlutter的工作流程可概括为以下关键步骤:
\\nDart语言与Skia这一组合使得Flutter能够以一种几乎接近原生的方式,在iOS和Android平台上呈现UI界面,同时保持了代码的完全一致性和高效性。
\\nFlutter的出现是跨平台技术发展的必然结果。传统方案如React Native受限于Bridge的性能瓶颈,难以突破性能与体验的天花板。而Flutter通过创新的架构设计,在开发阶段利用JIT编译支持热重载,显著提升开发效率;在发布时通过AOT编译为原生机器码、无桥接架构实现媲美原生的运行性能。
\\n从设计层面看,Flutter提供了完善的Widget体系,开发者可以轻松实现Material Design和Cupertino风格的UI效果,同时保持高度的定制灵活性。
\\n作为Google官方力推的跨平台方案,Flutter拥有更规范的生态和更强的平台支持。这种官方背景不仅确保了技术的长期演进,也降低了因平台政策变动带来的风险(如iOS热更新限制)。
\\n总之,Flutter 的出现为跨平台开发提供了一种不错的解决方案。它在开发效率、性能表现和跨端一致性之间找到了较好的平衡,同时通过丰富的组件库和活跃的社区支持,为开发者提供了更多可能性。随着技术的不断成熟,Flutter 正在成为跨平台开发领域的一个重要选择。
","description":"一、Flutter介绍 1.1 什么是flutter?\\n\\nFlutter是一款由 Google 开发供支持的跨平台开源框架。\\n\\n在2018年发布时Flutter主要被用于移动端跨平台开发,Flutter3.0发布以后,支持在六个平台上进行应用程序开发:iOS、Android、Web、Windows、MacOS 和 Linux。\\n\\n尽管多平台适配能力不断提升,iOS和Android仍是其核心应用场景:\\n\\n移动端支持最成熟\\n社区资源最丰富\\n性能优化最彻底\\n\\n本文也将以移动端视角展开讨论\\n\\nFlutter无论从开发效率、性能表现,还是从企业成本的角度来看…","guid":"https://juejin.cn/post/7465994044336275482","author":"A0微声z","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-02-01T07:46:03.969Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f4ea7d6ea05749399b68edd2c4e45012~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQTDlvq7lo7B6:q75.awebp?rk3s=f64ab15b&x-expires=1739000763&x-signature=9K6qKHhhIEvlQB0IXn8ws5eklMY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/88855004f0b04a06b64b360ebdc6f977~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQTDlvq7lo7B6:q75.awebp?rk3s=f64ab15b&x-expires=1739000763&x-signature=eVwI%2B93JSeK965bkPjTyWCeqPro%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/62da84e927a44d96b38a5f5d298a05a5~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQTDlvq7lo7B6:q75.awebp?rk3s=f64ab15b&x-expires=1739000763&x-signature=rsZbP%2BKIAkPGVvPN3rVg%2BliFwN0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fb2a8dbea7d642ceb27b3cddc7258d41~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQTDlvq7lo7B6:q75.awebp?rk3s=f64ab15b&x-expires=1739000763&x-signature=171MRXPHOMRT4ITtFUFBqZbG3H4%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter pub.dev 插件源码工程位置(本地使用、调整插件原工程)","url":"https://juejin.cn/post/7465711902415241227","content":"flutter
插件镜像地址指令
\\n$ flutter doctor -v\\n
\\n[✓] Flutter (Channel stable, 3.24.5, on macOS 13.5.2 22G91 darwin-arm64, locale zh-Hans-CN)\\n • Flutter version 3.24.5 on channel stable at\\n /Users/dengzemiao/Desktop/Project/flutter/Flutter/flutter\\n • Upstream repository https://github.com/flutter/flutter.git\\n • Framework revision dec2ee5c1f (3 个月前), 2024-11-13 11:13:06 -0800\\n • Engine revision a18df97ca5\\n • Dart version 3.5.4\\n • DevTools version 2.37.3\\n • Pub download mirror https://pub.flutter-io.cn\\n • Flutter download mirror https://storage.flutter-io.cn\\n
\\nPub download mirror
作用:这是针对 Dart 包管理工具 Pub 的镜像源。
\\n用途:当在 Flutter 项目中运行 flutter pub get
或者 dart pub get
时,Pub 会从这个镜像下载你项目中所需的 Dart 包(如第三方库)。
该镜像源主要是为了加速 Dart 包的下载。
\\n例子:使用了 http
、provider
等库,flutter pub get
就会从 Pub download mirror
获取这些包。
Flutter download mirror
flutter upgrade
或下载 SDK 更新),或者在新安装时,它会从这个镜像源下载 Flutter SDK 和相关工具链(如编译器、构建工具等)。flutter doctor
或 flutter upgrade
时,它会从 Flutter download mirror
获取 SDK 更新。总结:
\\nPub download mirror
:用于下载 Dart 包,加速第三方库的获取。Flutter download mirror
:用于下载 Flutter SDK 和其工具链,帮助加速 Flutter 相关资源的下载。看这行 Pub download mirror https://pub.flutter-io.cn
表示当前配置的 Dart 包管理器(Pub)的镜像地址是 https://pub.flutter-io.cn
,这是国内的镜像源,替代了官方的 https://pub.dev
,目的是加速包的下载。
其他配置:
\\nFlutter SDK
下载时使用的镜像地址,当前配置的是 https://storage.flutter-io.cn
,也就是国内镜像源,替代了官方的 https://storage.googleapis.com
。Flutter
插件通常存储在项目中的 pubspec.yaml
文件指定的依赖部分。当通过 flutter pub get
安装插件时,Flutter
会将插件存储在本地缓存中,具体位置如下:
macOS/Linux: ~/.pub-cache/hosted/
使用的什么镜像就到 ~/.pub-cache/hosted/
目录下的哪个镜像文件夹中去找对应的插件以及版本,拷贝出来即可。例如 ~/.pub-cache/hosted/pub.dev/插件
、~/.pub-cache/hosted/pub.flutter-io.cn/插件
,本地镜像是 pub.flutter-io.cn
就去 pub.flutter-io.cn
文件夹中找。
Windows: C:\\\\Users<YourUsername>\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted
将插件工程拷贝出来到任意文件存储
\\n在 pubspec.yaml
中使用
# 原来是这样的\\ndependencies:\\n flutter_qiyu: ^0.1.2\\n\\n# 调整后,两者运行结果是一样的,但是这样可以调整插件工程的配置,有些插件必须要这样调整\\ndependencies:\\n flutter_qiyu:\\n path: /Users/dengzemiao/Desktop/flutter_qiyu-0.1.2\\n
\\nAndroid Studio 1.0 宣发于 2014 年 12 月,而现在时间来到 2025 ,不知不觉间 Android Studio 已经陪伴 Androider 走过十年历程。
\\nAndroid Studio 10 周年,也代表着了我的职业生涯也超十年,现在回想起来依然觉得「唏嘘」,还记得十多年前从嵌入式转入安卓的时候,搭建的工作环境还是 Eclipse + ADT , JDK 也还是甲骨文的 Java,而现在 Android Studio 都内置可切换的 OpenJDK 版本了。
\\n在 Eclipse 时代,搭建 Android 开发环境相当麻烦,比如需要单独下载 JDK,然后下载 Eclipse,然后用更新中心配置它指向 Android,之后安装适用于 Android 的 Eclipse 插件,然后将该插件配置为指向 Android SDK 安装。
\\n而相比较 Eclipse IDE 使用的 Ant CI 构建,Android Studio 在 2013 年 Google 大会发布后就宣布采用 Gradle 作为构建系统,不知道大家是否还记得 0.1 版本尝鲜时的这个界面:
\\n随着 Android Studio 1.0 的正式发布,Gradle 也开始正式进入 Android 开发者们的视野里,相信起初不少开发者对于 Gradle 是抗拒的,因为真的会有各种各样的原理导致项目跑不起来,更别说迁移了。
\\n\\n\\n相信我,现在的 AGP 再怎么坑,都比当年靠谱很多。
\\n
我还记得,2015 年的时候我去了一家新公司工作的第一件事,就是把它们历史的 Eclipse 工程迁移到 Android Studio ,并让团队接受 Gradle 开发环境。
\\n当然,Android Studio 刚出来那会,它内置的模拟器依然是一种“狗都不用”的情况,相比现在的模拟器,当年的模拟器真的一言难尽:
\\n而 Google 选择 IntelliJ 作为构建 Android Studio 的平台,也是 Google 和 JetBrains 深度合作的开始,至此,Android 就和 JetBrains/Gradle 开始了十年的蜜月,然后就是大家熟知的 Kotlin、Kotlin Multiplatform、Compose Multiplatform:
\\n而从这 10 年 Android Studio logo 的变化,你是否也从这些熟悉的 logo 里看到曾经「每日早八」的回忆:
\\n从 2014 年到 2022 年,Android Studio 图标就通过不同的背景颜色来区分版本,黄色代表 Canary ,绿色 (2014 - 2019) 和白色 (2020-2022) 代表稳定版本。
\\n而现在,除了颜色之外,新设计还采用了一种辅助编码方法,还以轮廓 A 表示 Canary,以实心 A 表示 Stable。
\\n而除了 Logo 之外,从 Android Studio 4.1 之后, Android Studio Arctic Fox 变更了版本号规则,开始与 IntelliJ IDEA 更新保持一致,并且每个大版本增加一种「生物」,可以看到现在启动图也变得多彩起来:
\\n之后,从 Android Studio Koala 开始,Studio 版本所使用的版本号都遵循着: <IntelliJ 版本年份>.<IntelliJ 主版本>.<Studio 主版本> 这样的格式,其中初始的动物版本发布将带有 \\".1\\" 的 Android Studio 主版本号,用于引入更新的 IntelliJ 平台版本号,而随后的功能更新将把 Android Studio 的主版本号提升到 \\".2\\",聚焦于引入更多特定于 Android 的功能,统称为 Feature Drop 。
\\n自此, Android Studio 正式走过了十年,现在的 Android Studio 不仅聚焦 AI Gemini ,更打通了 Firebase 整套支持,同时还支持 K2 模式,全面无缝地适配 Jetpack Compose ,拥有更优秀的测试和部署支持等。
\\n可以看到,十年过去,Android Studio 确实越来越优秀了,尽管它现在还是有着这样那样的问题,但是,我们还是期待它能迈向下一个十年~
\\n在去年春节,Flutter 官方发布了宏(Macros)编程的原型支持, 同年的 5 月份在 Google I/O 发布的 Dart 3.4 宣布了宏的实验性支持,但是对于 Dart 内部来说,从启动宏编程实验开始已经过去了几年,但是从目前的推进趋势看,完全的宏功能支持并不理想,结论大概是:
\\n\\n\\n能用是能用,但是质量和性能都达不到一开始的预期。
\\n
具体原来在于 Dart 的静态语言提前编译和有状态的热重载等方面,对于元编程而言,需要建立在强大的内省基础支持之上,但是对于 Dart 目前来说,运行时内省(例如反射)会让 tree-shaking 优化变得困难 ,而 tree-shaking 优化是 Dart 在二进制优化的重要指标之一。
\\n一开始 Dart 的目标是构建一个完整的宏系统,从而让该系统支持在编译时对程序进行深度语义内省,但是在实现语义内省时引入了大量的编译时成本,而这让有状态的热重载保持变得困难。
\\n\\n\\n目前的宏编程还让 Flutter 开发时的 IDE 编辑与补全体验下降。
\\n
同时带来的还有依赖项里的宏循环依赖等问题,例如在 IDE 中输入“.foo” 可能需要重新处理所有宏,从而执行正确的代码,目前来看要么处理得太频繁,要么给出的结果不正确。
\\n\\n\\n在过去的测试里,宏在小型库上的性能非常好,但是在真实应用的大周期开发里,会让 Dart 的体验变得很差,例如在顶层编辑(声明、方法头、字段等)时,基本上每次键入都需要重新运行整个宏构建。
\\n
而针对当前宏支持采用缓存的提议,也存在宏生成的代码的整个版本适配问题,例如:
\\n\\n\\n现在有一个依赖于 foo 和 bar 的 my_app 包,如果你只在 foo 上运行 pub get,解析器可能会给你 bar 1.2.3;而当你在 my_app上运行 pub get 时,也许会得到 bar 2.3.4,大概可能是 @doStuff 宏内省的 type from bar 在这些版本之间不同。
\\n
虽然也可以通过限制内省来避免这种深层依赖,但带来的一些其他负面,例如你可能正在为 foo 生成 JSON 序列化代码,并且宏正在尝试判断其类型来自 bar 的字段是否支持 JSON 序列化,甚至前面提到的循环依赖问题。
\\n当然针对和这个可能还有其他解决方案,相比较目前带来的编译时间、静态分析和整个程序的优化问题,对于 Dart 来说运行时方法并不现实。
\\n所以最终 Dart 团队决定,由于宏的性能具体目标还太遥远,团队决定把当前的实现回归到编辑(例如静态分析和代码完成)和增量编译(热重载的第一步)上。
\\n具体在于重新投资Dart 中的数据支持,因为这也是Dart & Flutter issue 里请求最多的问题,事实上一开始 Dart 对宏支持的主要动机也是提供更好的数据序列化和反序列化,但是目前看来,通过更多定制语言功能来实现这一点更加实际。
\\n另外通过缩短构建时间和整体代码生成体验来弥补宏的确实,也是未来目标之一,目前 Dart 已经确定了 build_runner 的改进支持。
\\n另外还计划提供 augmentations 功能,这是作为宏的一部分制作原型的功能,例如增加修饰符 augment
作为扩充声明,而该功能也是独立的部份,并将改进现有的代码生成。
\\n\\n通过 augment 实现将一个功能部署到多个文件里,同时可以添加新的顶级声明,将新成员注入类,并将函数和变量包装在其他代码中。
\\n
相信宏支持停止这个消息会让大家感到失望,尽管从长远来看 Dart 仍然对通用元编程感兴趣,因为它在数据之外还有许多潜在的用例,但是在短期之内,Dart 应该是不会发布宏支持。
\\n对于包开发者来说,比如之前的 equatable 在 3.0.0-dev.1
就发布过宏的实验性版本,体验还不错,但是现在看来只能继续“实验”下去。
最后,祝大家 2025 新春快乐~
\\nValueKey
ValueKey
用于基于某个值(例如字符串、数字等)来唯一标识一个组件。适合切换相似视图的场景,例如在 ListView
中区分列表项,或动态切换输入框。
if (loginMode == LoginMode.pwd)\\n CustomInputField(\\n key: ValueKey(\'passwordInputField\'), // 用唯一字符串标识\\n hintText: \'请输入密码\',\\n obscureText: true,\\n ),\\nif (loginMode == LoginMode.code)\\n CustomInputField(\\n key: ValueKey(\'codeInputField\'), // 用唯一字符串标识\\n hintText: \'请输入验证码\',\\n keyboardType: TextInputType.number,\\n ),\\n
\\nListView.builder(\\n itemCount: items.length,\\n itemBuilder: (context, index) {\\n return ListTile(\\n key: ValueKey(items[index].id), // 使用唯一 ID 标识\\n title: Text(items[index].name),\\n );\\n },\\n);\\n
\\nUniqueKey
UniqueKey
总是生成一个完全唯一的 Key,即使传入相同的值,每次调用都不同。它适合在动态添加或删除组件时,确保组件在重新排列时强制销毁和重建。
if (loginMode == LoginMode.pwd)\\n CustomInputField(\\n key: UniqueKey(), // 每次都会生成不同的 Key,确保重新渲染\\n hintText: \'请输入密码\',\\n obscureText: true,\\n ),\\nif (loginMode == LoginMode.code)\\n CustomInputField(\\n key: UniqueKey(), // 每次都会生成不同的 Key\\n hintText: \'请输入验证码\',\\n keyboardType: TextInputType.number,\\n ),\\n
\\nReorderableListView(\\n onReorder: (oldIndex, newIndex) {\\n setState(() {\\n final item = items.removeAt(oldIndex);\\n items.insert(newIndex, item);\\n });\\n },\\n children: items.map((item) {\\n return ListTile(\\n key: UniqueKey(), // 确保拖动后不会复用旧组件\\n title: Text(item.name),\\n );\\n }).toList(),\\n);\\n
\\nObjectKey
ObjectKey
使用一个对象实例作为唯一标识。这在动态数据(如模型对象)对应组件时非常有用,适合复杂对象关联的场景。
class Item {\\n final int id;\\n final String name;\\n\\n Item(this.id, this.name);\\n}\\n\\nList<Item> items = [\\n Item(1, \'Item 1\'),\\n Item(2, \'Item 2\'),\\n];\\n\\nListView(\\n children: items.map((item) {\\n return ListTile(\\n key: ObjectKey(item), // 使用对象作为 Key\\n title: Text(item.name),\\n );\\n }).toList(),\\n);\\n
\\nCustomInputField(\\n key: ObjectKey(loginMode), // 以 loginMode 对象为 Key\\n hintText: loginMode == LoginMode.pwd ? \'请输入密码\' : \'请输入验证码\',\\n obscureText: loginMode == LoginMode.pwd,\\n);\\n
\\nGlobalKey
GlobalKey
是功能最强的 Key,可以跨组件访问状态,或者从父级调用组件的方法。它适合复杂交互或需要直接操作子组件的场景。
final GlobalKey<FormFieldState> passwordFieldKey = GlobalKey<FormFieldState>();\\n\\nCustomInputField(\\n key: passwordFieldKey,\\n hintText: \'请输入密码\',\\n);\\n\\n// 在父组件中调用子组件方法\\nvoid clearPasswordField() {\\n passwordFieldKey.currentState?.reset();\\n}\\n
\\nfinal GlobalKey<FormState> formKey = GlobalKey<FormState>();\\n\\nForm(\\n key: formKey,\\n child: Column(\\n children: [\\n TextFormField(\\n decoration: InputDecoration(hintText: \'用户名\'),\\n validator: (value) => value!.isEmpty ? \'用户名不能为空\' : null,\\n ),\\n ElevatedButton(\\n onPressed: () {\\n if (formKey.currentState!.validate()) {\\n // 验证通过\\n }\\n },\\n child: Text(\'提交\'),\\n ),\\n ],\\n ),\\n);\\n
\\nKey 类型 | 使用场景 | 示例 |
---|---|---|
ValueKey | 动态切换视图或标识列表项 | 列表项的唯一 ID 或动态视图的唯一值:ValueKey(\'passwordInputField\') |
UniqueKey | 确保强制销毁并重新创建组件 | 每次视图切换都重新渲染:UniqueKey() |
ObjectKey | 基于对象实例动态关联组件 | 用对象本身标识组件:ObjectKey(item) |
GlobalKey | 跨组件访问状态或需要直接调用子组件的方法 | 表单验证、滚动控制:GlobalKey<FormState>() |
根据实际需求选择合适的 Key 类型,比如视图切换建议用 ValueKey
或 ObjectKey
,而动态列表可能需要 UniqueKey
来避免组件混淆。
抽象类提供了一种更加方便的代码复用形式,为继承的子类提供了一组共享的属性和方法(可以不具体实现)。\\n混入是为解决Dart中只支持单继承而带来的局限性而引入的一种轻量级多重继承形式。抽象类与混入有许多相似的地方,因此这里将二者放在一起进行介绍。接下来,我们一起去了解一下什么是抽象类与混入。
\\n抽象类是类之上的抽象,其为类定义提供了一个可修改的模版或契约。
\\n抽象类是不能被实例化的类。其具有以下特点:
\\n抽象类使用 abstract
关键字定义。
由abstract关键字 + class关键字 + 抽象类类名 + 大括号({})组成。
\\n示例:
\\n/// 定义一个Animal的抽象类\\nabstract class Animal{\\n String name; // 抽象类的属性\\n Animal(this.name); // 为子类提供的构造函数\\n void introduce(){ // 具体方法,实现函数的具体细节\\n print(\'我是${this.name}\');\\n }\\n void skill(); // 抽象方法,不实现函数的具体细节,强制其子类实现。\\n}\\n
\\n如果其子类不实现抽象方法则会出现如下图的错误。
\\n\\n实现抽象方法后:
/// 定义一个Animal的抽象类\\nabstract class Animal{\\n String name; // 抽象类的属性\\n Animal(this.name); // 为子类提供的构造函数\\n void introduce(){ // 具体方法,实现函数的具体细节\\n print(\'我是${this.name}\');\\n }\\n void skill(); // 抽象方法,不实现函数的具体细节,强制其子类实现。\\n}\\n\\nclass Bird extends Animal{\\n Bird(super.name);\\n void skill(){\\n print(\'${this.name}会飞!\');\\n }\\n}\\nvoid main() {\\n Bird parrot = Bird(\'鹦鹉\');\\n parrot.introduce(); // 输出:我是鹦鹉\\n parrot.skill(); // 输出:鹦鹉会飞!\\n}\\n
\\n混入(Mixin)是Dart提供的一种轻量级多重继承形式,其弥补了Dart单继承带来的缺陷。你可能会有一个疑问,Dart为什么不直接支持多继承呢?这不是更快捷吗?答案是多继承会带来一个难以抉择的局面,即菱形问题。下面我们先一起来看看什么是菱形问题。
\\n菱形问题是当一个类从两个或多个基类派生,而这些基类又共同继承自同一个祖先类时,可能会出现方法或属性的二义性。
\\n菱形问题又称为钻石问题,它是多继承带来的二义性问题。
\\n注:称其为菱形问题是因其形状如菱形,故称为菱形问题,当出现复杂继承关系时就会出现其形状状如钻石。
\\n\\n如图所示,菱形问题即当一个子类继承多个父类的方法(或属性),而继承的这些方法(或属性)又同时继承自同一个父类。当这个子类去继承这两个父类的相同方法(或属性)时,会出现这个子类不知道选择那个父类的方法(或属性)去继承。
出现了问题,当然就要解决问题,不同的编程语言提出了不同的解决方法,Java中通过接口来实现类似于多继承的效果,避免了直接多继承带来的菱形问题。而在Dart中则是通过 Mixin
(混入)来实现类似的效果。
混入(Mixin)Dart中解决菱形问题的一种方案,通过混入可以组合不同的类的功能到一个类中,而不需要复杂的类继承结构。其具有如下优势:
\\n混入(Mixin)使用 mixin
关键字定义。
由 mixin 关键字 + 类名 + 大括号({})组成。
\\n注意:混入没有构造函数
\\n示例:
\\n/// 使用关键字mixin定义一个混入类Fly\\nmixin Fly{\\n void canfly(){\\n print(\'会飞!\');\\n }\\n}\\n
\\n混入(Mixin)通过 with
关键字来使用。
示例: 通过混入实现单个功能的组合。
\\n/// 使用关键字mixin定义一个混入类Fly\\nmixin Fly{\\n void canFly(){\\n print(\'我会飞!\');\\n }\\n}\\n// 使用 Mixin 的类\\nclass Bird with Fly{\\n String name;\\n Bird(this.name);\\n void introduce(){\\n print(\'我是${this.name}!\');\\n }\\n}\\nvoid main() {\\n Bird parrot = Bird(\'鹦鹉\');\\n parrot.introduce(); // 输出:我是鹦鹉!\\n parrot.canFly(); // 输出:我会飞!\\n}\\n
\\n示例: 通过混入实现多个功能的组合。
\\n/// 定义混入类Fly、Roar\\nmixin Fly{\\n void canFly(){\\n print(\'我会飞!\');\\n }\\n}\\nmixin Roar{\\n void canRoar(){\\n print(\'我会叫!\');\\n }\\n}\\n// 使用 Mixin 的类\\nclass Bird with Fly, Roar{\\n String name;\\n Bird(this.name);\\n void introduce(){\\n print(\'我是${this.name}!\');\\n }\\n}\\nvoid main() {\\n Bird parrot = Bird(\'鹦鹉\');\\n parrot.introduce(); // 输出:我是鹦鹉!\\n parrot.canFly(); // 输出:我会飞!\\n parrot.canRoar(); // 输出:我会叫!\\n}\\n
\\n混入(Mixin)可以约束哪些类可以进行混入。也就是说只有满足约束的类才可以使用with关键字进行使用。
\\n混入约束条件使用 on
关键字定义。
示例:
\\n/// 定义Animal类\\nclass Animal{\\n String name;\\n Animal(this.name);\\n}\\n/// 定义Mixin类Fly并限定只能Animal派生类使用\\nmixin Fly on Animal {\\n void canFly(){\\n print(\'我会飞!\');\\n }\\n}\\nmixin Roar{\\n void canRoar(){\\n print(\'我会叫!\');\\n }\\n}\\n// 使用 Mixin 的类\\nclass Bird extends Animal with Fly, Roar{\\n Bird(super.name);\\n void introduce(){\\n print(\'我是${this.name}!\');\\n }\\n}\\nvoid main() {\\n Bird parrot = Bird(\'鹦鹉\');\\n parrot.introduce(); // 输出:我是鹦鹉!\\n parrot.canFly(); // 输出:我会飞!\\n parrot.canRoar(); // 输出:我会叫!\\n}\\n
\\n如果不是Animal子类则会出现下图中的错误(编译器报错)。
\\n混入的约束具有执行顺序,越靠近 with
关键字的先进行混入。
示例:
\\nmixin A {\\n void detailA() {\\n print(\'我是Mixin: A 的方法\');\\n }\\n}\\nmixin B {\\n void detailB() {\\n print(\'我是Mixin: B 的方法\');\\n }\\n}\\n// 通过on关键字约束,若要混入C必须是先混入A,B\\nmixin C on A, B {\\n void detailC() {\\n print(\'我是Mixin: C 的方法\');\\n super.detailA();\\n super.detailB();\\n }\\n}\\n/// 定义MyClass 类\\nclass MyClass with A, B, C { // 先混入A,B在混入C\\n void myMethod() {\\n detailA();\\n detailB();\\n detailC();\\n }\\n}\\n\\nvoid main() {\\n MyClass myClass = MyClass();\\n myClass.myMethod();\\n}\\n// 输出:\\n我是Mixin: A 的方法\\n我是Mixin: B 的方法 // 混入 A、B的结果\\n我是Mixin: C 的方法 // 本行及下面的为混入 C 的结果\\n我是Mixin: A 的方法\\n我是Mixin: B 的方法\\n
\\n若定义MyClass类,混入A,B,C时不先混入A,B则会出现下图所示错误。
\\n本小节介绍了Dart中的抽象类、混入,其中在抽象类部分介绍了抽象类的一些优势及定义。在混入部分,首先介绍了多继承的二义性问题,其次为解决此问题接着介绍了混入的概念,最后介绍了Dart中混入的定义及混入的约束。
","description":"前言 抽象类提供了一种更加方便的代码复用形式,为继承的子类提供了一组共享的属性和方法(可以不具体实现)。 混入是为解决Dart中只支持单继承而带来的局限性而引入的一种轻量级多重继承形式。抽象类与混入有许多相似的地方,因此这里将二者放在一起进行介绍。接下来,我们一起去了解一下什么是抽象类与混入。\\n\\n一、抽象类\\n\\n抽象类是类之上的抽象,其为类定义提供了一个可修改的模版或契约。\\n\\n1.1、抽象类概念\\n\\n抽象类是不能被实例化的类。其具有以下特点:\\n\\n不能被实例化:抽象类不能被实例化为对象。\\n没有构造函数:抽象类不能被实例化为对象,也就不需要构造函数。\\n支持抽象方法:抽…","guid":"https://juejin.cn/post/7464247481225904164","author":"好的佩奇","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-27T09:17:58.695Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d5259aad91ae42568d8aabf283073cc7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5aW955qE5L2p5aWH:q75.awebp?rk3s=f64ab15b&x-expires=1738574278&x-signature=z6CRYWmm%2Bz2xYuvLfTAVA5ayvAo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d77caf65e097420385184c37f4c1f586~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5aW955qE5L2p5aWH:q75.awebp?rk3s=f64ab15b&x-expires=1738574278&x-signature=4vsMAEHLjrLXMPg9U03kPTwLGpo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/80f811415cb54fa2a25d7bd22ba9485f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5aW955qE5L2p5aWH:q75.awebp?rk3s=f64ab15b&x-expires=1738574278&x-signature=oi74Ach5dNrHGqmcD4eJyBFjWE4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4b575efb8de248efaf3ac30ca134a3dd~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5aW955qE5L2p5aWH:q75.awebp?rk3s=f64ab15b&x-expires=1738574278&x-signature=%2BuQkYZ3yOIoBF9FphtDE%2B7SESbA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/cd132f78565d4d609c0464de77adcb8f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5aW955qE5L2p5aWH:q75.awebp?rk3s=f64ab15b&x-expires=1738574278&x-signature=6hNZWBEffU52GkuVQ0d61PkSYLs%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"系统化掌握 Dart 编程之异常处理","url":"https://juejin.cn/post/7464145318285377577","content":"异常(Exception
)是指程序执行过程中发生的意外情况,可能导致程序崩溃
或无法正常工作
。Dart
提供了强大的异常处理机制,帮助开发者优雅地捕获和处理这些异常,确保程序的稳定性
和可靠性
。为了系统化地掌握 Dart
的异常处理,我们将从理论基础
、具体实现
、实践应用
到最佳实践
四个层面进行详细讲解。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\n理论基础 —— 理解异常的本质
与分类
异常是程序执行过程中出现的错误
或意外情况
。当程序遇到异常时,如果不加以处理,可能会导致程序终止或产生不正确的结果。Dart
使用对象来表示异常,并通过 throw
关键字抛出异常,使用 try
、catch
和 finally
语句来捕获和处理异常。
代码控制异常
的发生和处理。自定义异常类型
,满足不同场景的需求。清晰的异常处理逻辑
使代码更易于理解和维护。Dart
中的异常主要分为两类:
Error
\\n内存不足
、堆栈溢出
等。不可恢复
,程序应立即终止
。Exception
\\n用户输入错误
、文件未找到
等。异常处理机制捕获并处理
这些异常,使程序继续运行。具体实现 —— 掌握异常处理的关键语法
可以使用 throw
关键字抛出一个异常对象。这个对象可以是任何类型的对象,但通常是一个继承自 Exception
或 Error
的类实例。
void checkAge(int age) {\\n if (age < 0) {\\n throw Exception(\'年龄不能为负数\');\\n }\\n}\\n\\nvoid main() {\\n try {\\n checkAge(-5);\\n } catch (e) {\\n print(e); // 输出: Exception: 年龄不能为负数\\n }\\n}\\n
\\n创建自己的异常类,继承自 Exception
类,以便更好地描述特定类型的错误。
class NegativeAgeException implements Exception {\\n final String message;\\n\\n NegativeAgeException(this.message);\\n\\n @override\\n String toString() => \'NegativeAgeException: $message\';\\n}\\n\\nvoid checkAge(int age) {\\n if (age < 0) {\\n throw NegativeAgeException(\'年龄不能为负数\');\\n }\\n}\\n\\nvoid main() {\\n try {\\n checkAge(-5);\\n } catch (e) {\\n print(e); // 输出: NegativeAgeException: 年龄不能为负数\\n }\\n}\\n
\\n使用 try-catch
结构捕获并处理异常。
void main() {\\n try {\\n int result = divide(10, 0);\\n print(\'结果是: $result\');\\n } on NegativeAgeException catch (e) {\\n print(\'年龄异常: $e\');\\n } on Exception catch (e) {\\n print(\'一般异常: $e\');\\n } catch (e) {\\n print(\'未知异常: $e\');\\n } finally {\\n print(\'清理操作...\');\\n }\\n}\\n\\nint divide(int a, int b) {\\n if (b == 0) {\\n throw Exception(\'除数不能为零\');\\n }\\n return a ~/ b;\\n}\\n
\\n实践应用 —— 处理真实世界的问题
如果一个函数抛出了异常且没有被捕获,该异常会向上层调用栈传播,直到被某个 catch
块捕获或最终导致程序终止。
void main() {\\n try {\\n performOperation();\\n } catch (e) {\\n print(\'主函数捕获到异常: $e\');\\n }\\n}\\n\\nvoid performOperation() {\\n riskyFunction();\\n}\\n\\nvoid riskyFunction() {\\n throw Exception(\'风险操作失败\');\\n}\\n
\\n异步操作(如 Future
)也可能抛出异常。可以使用 await
和 try-catch
来捕获这些异常。
import \'dart:async\';\\n\\nFuture<int> fetchUserAge() async {\\n await Future.delayed(Duration(seconds: 1));\\n throw Exception(\'用户信息获取失败\');\\n}\\n\\nvoid main() async {\\n try {\\n int age = await fetchUserAge();\\n print(\'用户年龄: $age\');\\n } catch (e) {\\n print(\'发生异常: $e\');\\n }\\n}\\n
\\n枯燥中来点乐趣
:
尽量使用具体的异常类型,而不是泛型的 Exception
,这有助于更精确地处理不同类型的错误。
class NegativeValueException implements Exception {\\n final String message;\\n\\n NegativeValueException(this.message);\\n\\n @override\\n String toString() => \'NegativeValueException: $message\';\\n}\\n\\nvoid checkValue(int value) {\\n if (value < 0) {\\n throw NegativeValueException(\'值不能为负数\');\\n }\\n}\\n\\nvoid main() {\\n try {\\n checkValue(-5);\\n } on NegativeValueException catch (e) {\\n print(e); // 输出: NegativeValueException: 值不能为负数\\n } catch (e) {\\n print(\'其他异常: $e\');\\n }\\n}\\n
\\n捕获异常后应进行适当的处理,不要简单地忽略它们
。
void readFile(String path) {\\n try {\\n // 模拟文件读取操作\\n throw Exception(\'模拟文件读取失败\');\\n } catch (e) {\\n // 错误处理逻辑\\n print(\'无法读取文件: $e\');\\n // 记录日志或通知用户\\n }\\n}\\n\\nvoid main() {\\n readFile(\'nonexistent_file.txt\');\\n}\\n
\\nfinally
确保关键资源的释放
和清理
操作,即使发生异常也要保证程序的稳定性
。
import \'dart:io\';\\n\\nvoid writeFile(String content) {\\n File file = File(\'example.txt\');\\n RandomAccessFile? raFile;\\n\\n try {\\n raFile = file.openSync(mode: FileMode.write);\\n raFile.writeStringSync(content);\\n } catch (e) {\\n print(\'写入文件时出错: $e\');\\n } finally {\\n // 确保关闭文件资源\\n raFile?.closeSync();\\n print(\'文件已关闭\');\\n }\\n}\\n\\nvoid main() {\\n writeFile(\'Hello, World!\');\\n}\\n
\\n不要在不必要的地方捕获异常,保持异常处理逻辑的简洁性
和可读性
。
void divideNumbers(int a, int b) {\\n if (b == 0) {\\n throw Exception(\'除数不能为零\');\\n }\\n print(a ~/ b);\\n}\\n\\nvoid main() {\\n try {\\n divideNumbers(10, 0);\\n } catch (e) {\\n print(\'捕获到异常: $e\');\\n }\\n\\n // 正常情况下不捕获异常\\n divideNumbers(10, 2);\\n}\\n
\\n对于生产环境中的异常,建议记录详细的日志信息
,便于后续排查
和分析
。
void divideNumbers(int a, int b) {\\n if (b == 0) {\\n throw Exception(\'除数不能为零\');\\n }\\n print(a ~/ b);\\n}\\n\\nvoid main() {\\n try {\\n divideNumbers(10, 0);\\n } catch (e) {\\n print(\'捕获到异常: $e\');\\n }\\n\\n // 正常情况下不捕获异常\\n divideNumbers(10, 2);\\n}\\n
\\n枯燥中来点乐趣
:
通过上述四个层面 —— 理论基础
、具体实现
、实践应用
和最佳实践
,我们系统化地掌握了 Dart
的异常处理机制。以下是各部分的核心要点:
本质
、分类
及其在 Dart
中的表现形式。抛出
、捕获
和处理
异常,包括自定义异常
类的创建。异常情况
,特别是异步操作中的异常处理
。健壮
和可靠
的异常处理策略。\\n","description":"前言 异常(Exception)是指程序执行过程中发生的意外情况,可能导致程序崩溃或无法正常工作。Dart 提供了强大的异常处理机制,帮助开发者优雅地捕获和处理这些异常,确保程序的稳定性和可靠性。为了系统化地掌握 Dart 的异常处理,我们将从理论基础、具体实现、实践应用到最佳实践四个层面进行详细讲解。\\n\\n操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。\\n\\n一、理论基础\\n\\n理论基础 —— 理解异常的本质与分类\\n\\n1.1、定义\\n\\n异常是程序执行过程中出现的错误或意外情况。当程序遇到异常时,如果不加以处理,可能会导致程序终止或产生不正确的结果。Dart 使…","guid":"https://juejin.cn/post/7464145318285377577","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-27T00:35:19.416Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/eb9c2998b2ca4884b95a6f099e93cf95~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1738542918&x-signature=7Plp3s78MwZOeH1R8MZn%2Fn4GtHg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f6cb0b8f97924a9d828e84bb2fb842ad~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1738542918&x-signature=6RFrR4uJPBZff3eDV9ud7vCLMaQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/bf612eafebf4473782540f0d7d87a112~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1738542918&x-signature=E2mPB7Pn3XGffioV%2BY70cSvdCKw%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"Flutter使用Flavor实现切换环境和多渠道打包","url":"https://juejin.cn/post/7463828441873662003","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
在Android开发中通常我们使用flavor进行多渠道打包,flutter开发中同样有这种方式,不过需要在原生中配置
\\n具体方案其实flutter官网个了相关示例(docs.flutter.dev/deployment/…),我这里记录一下自己的操作
\\n如下图,这里是android的配置比
2.找到Configuration进行相关设置\\n
3.根据不同的flavor可以设置不同的bundleId和产品名称
\\n4.另外根据不同的flavor还可以进行设置咱们的开发环境,比如debug,test,release
\\n5.在ios原生端自定义flavor字段
\\n然后进行相关渠道的设置
\\n这样就可以根据统一的渠道进行在flutter端开发相关代码
\\nandroid端原生代码
\\nios端原生代码\\n
项目运行执行命令可用
\\nflutter run --flavor freetest\\n
\\n如果开发工具是vscode可以进行相关配置\\n穿件.vscode目录\\n然后创建launch.json文件。里面添加如下配置
\\n有了以上代码可以根据不同的flavor进行设置不同的代码,还可以多渠道打包
\\n记录到此^_^
","description":"在Android开发中通常我们使用flavor进行多渠道打包,flutter开发中同样有这种方式,不过需要在原生中配置 具体方案其实flutter官网个了相关示例(docs.flutter.dev/deployment/…),我这里记录一下自己的操作\\n\\nAndroid\\n\\n如下图,这里是android的配置比\\n\\niOS\\n先创建一个新的Scheme\\n\\n2.找到Configuration进行相关设置\\n\\n3.根据不同的flavor可以设置不同的bundleId和产品名称\\n\\n4.另外根据不同的flavor还可以进行设置咱们的开发环境,比如debug,test…","guid":"https://juejin.cn/post/7463828441873662003","author":"零點壹度ideality","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-26T07:16:31.592Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/909af269ef3347f0866b8e2bb2dda7e7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6Zu26bue5aO55bqmaWRlYWxpdHk=:q75.awebp?rk3s=f64ab15b&x-expires=1738480591&x-signature=%2Fw3EEYkvx6e4Jvr7x4gIL67ZOWw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3b3f90e066334992ad8aa8c85ad821ed~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6Zu26bue5aO55bqmaWRlYWxpdHk=:q75.awebp?rk3s=f64ab15b&x-expires=1738480591&x-signature=8LdRPTAde2kyHVojL3CCkYsDDwE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/84cd2c09e7a24834a413f2966b669a1c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6Zu26bue5aO55bqmaWRlYWxpdHk=:q75.awebp?rk3s=f64ab15b&x-expires=1738480591&x-signature=fBvGJXbrERcHIlmpC0PSCZIS7iQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/15253163ce9a489fb5b14edf547f7026~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6Zu26bue5aO55bqmaWRlYWxpdHk=:q75.awebp?rk3s=f64ab15b&x-expires=1738480591&x-signature=sW9Re%2Fh03P57Cjf9IAN%2FspKywek%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/942fb0a6edc2423cba82e34fd94a1fc3~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6Zu26bue5aO55bqmaWRlYWxpdHk=:q75.awebp?rk3s=f64ab15b&x-expires=1738480591&x-signature=SQqcTJh%2F%2BdN8pWrkdVs4KK5c9fI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/07636b5caf0b4e8aa4b30cbb3c7a8c1b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6Zu26bue5aO55bqmaWRlYWxpdHk=:q75.awebp?rk3s=f64ab15b&x-expires=1738480591&x-signature=%2Fvzxr8XigX9XWyZ%2FUXEehKASx%2FU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/8d97d4ab31e940499fd697ddb2f9dd52~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6Zu26bue5aO55bqmaWRlYWxpdHk=:q75.awebp?rk3s=f64ab15b&x-expires=1738480591&x-signature=GvJiJHBM48UXsNduX2VW2tYWOvw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/018bb2a18fc54095940e89cef89f843f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6Zu26bue5aO55bqmaWRlYWxpdHk=:q75.awebp?rk3s=f64ab15b&x-expires=1738480591&x-signature=CE9hZI7OmUvz0pO2FilypNElmbI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/92f6334396c64b9682a46cec5df42f2d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6Zu26bue5aO55bqmaWRlYWxpdHk=:q75.awebp?rk3s=f64ab15b&x-expires=1738480591&x-signature=uoXFHml%2BkEJEmi3QYJ2qY%2BzeTVc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b11109d0393742b2a2ba91b6ab1f24c5~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6Zu26bue5aO55bqmaWRlYWxpdHk=:q75.awebp?rk3s=f64ab15b&x-expires=1738480591&x-signature=69QPiTAaCFdVQ%2FR6zLW3adKtEoI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/56df62f8d2c24d48a70363a518d79f32~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6Zu26bue5aO55bqmaWRlYWxpdHk=:q75.awebp?rk3s=f64ab15b&x-expires=1738480591&x-signature=B4oBG2aRxk9dDyDmCB5mj3Z5eSM%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter Map 常用的函数与使用场景","url":"https://juejin.cn/post/7463827687872086070","content":"Map.entries
Map
的所有键值对,以 Iterable<MapEntry<K, V>>
形式表示。Map
的所有键值对时,可以使用 entries
来访问每个 MapEntry
对象,方便提取键和值。Map<String, String> fruits = {\'a\': \'Apple\', \'b\': \'Banana\'};\\n\\nfruits.entries.forEach((entry) {\\n print(\'Key: ${entry.key}, Value: ${entry.value}\');\\n});\\n
\\n输出:
\\nKey: a, Value: Apple\\nKey: b, Value: Banana\\n
\\nMap.from
Map
,并将现有的 Map
内容复制过来。Map
并进行修改时,可以使用 Map.from
。Map<String, int> original = {\'apple\': 1, \'banana\': 2};\\nMap<String, int> copy = Map.from(original);\\ncopy[\'orange\'] = 3; // 修改复制后的 Map\\n\\nprint(original); // 输出: {apple: 1, banana: 2}\\nprint(copy); // 输出: {apple: 1, banana: 2, orange: 3}\\n
\\nMap.addAll
Map
的键值对添加到当前 Map
。Map
,并且希望将一个 Map
的内容添加到另一个 Map
中,可以使用 addAll
。Map<String, int> map1 = {\'a\': 1, \'b\': 2};\\nMap<String, int> map2 = {\'c\': 3, \'d\': 4};\\n\\nmap1.addAll(map2);\\nprint(map1); // 输出: {a: 1, b: 2, c: 3, d: 4}\\n
\\nMap.containsKey
和 Map.containsValue
Map
是否包含指定的键或值。Map
是否包含某个键或某个值时,可以使用这两个方法。Map<String, int> map = {\'apple\': 1, \'banana\': 2};\\n\\nprint(map.containsKey(\'apple\')); // 输出: true\\nprint(map.containsValue(3)); // 输出: false\\n
\\nMap.remove
Map
中指定键的键值对。Map
中移除某个键值对时,使用 remove
。Map<String, int> map = {\'apple\': 1, \'banana\': 2};\\n\\nmap.remove(\'apple\');\\nprint(map); // 输出: {banana: 2}\\n
\\nMap.update
Map
中指定键的值。如果该键不存在,可以插入一个新的键值对。update
。Map<String, int> map = {\'apple\': 1, \'banana\': 2};\\n\\nmap.update(\'apple\', (value) => value + 1, ifAbsent: () => 3);\\nprint(map); // 输出: {apple: 2, banana: 2}\\n
\\nMap.addEntries
MapEntry
的集合添加到 Map
中。MapEntry
批量添加。Map<String, int> map = {\'apple\': 1, \'banana\': 2};\\n\\nmap.addEntries([\\n MapEntry(\'orange\', 3),\\n MapEntry(\'grape\', 4),\\n]);\\nprint(map); // 输出: {apple: 1, banana: 2, orange: 3, grape: 4}\\n
\\nMap.updateAll
Map
中所有元素的值。Map
中的每个值进行某种操作时,可以使用 updateAll
。Map<String, int> map = {\'apple\': 1, \'banana\': 2};\\n\\nmap.updateAll((key, value) => value * 2);\\nprint(map); // 输出: {apple: 2, banana: 4}\\n
\\nMap.forEach
Map
中的所有键值对。Map
并执行某些操作时,使用 forEach
方法。Map<String, int> map = {\'apple\': 1, \'banana\': 2};\\n\\nmap.forEach((key, value) {\\n print(\'Key: $key, Value: $value\');\\n});\\n
\\n输出:
\\nKey: apple, Value: 1\\nKey: banana, Value: 2\\n
\\nMap.putIfAbsent
Map
中不存在指定的键,则插入指定的键值对。Map
中,并希望在不存在的情况下插入一个默认值,可以使用 putIfAbsent
。Map<String, int> map = {\'apple\': 1};\\n\\nmap.putIfAbsent(\'banana\', () => 2);\\nmap.putIfAbsent(\'apple\', () => 3); // 不会替换已有的值\\n\\nprint(map); // 输出: {apple: 1, banana: 2}\\n
\\nMap.asMap()
List
转换为 Map
,其中键为列表元素的索引,值为列表中的元素。List
转换为 Map
时,可以使用 asMap()
,它会将每个列表元素的索引作为键。List<String> list = [\'apple\', \'banana\', \'cherry\'];\\n\\nMap<int, String> map = list.asMap();\\nmap.forEach((index, value) {\\n print(\'Index: $index, Value: $value\');\\n});\\n
\\n输出:
\\nIndex: 0, Value: apple\\nIndex: 1, Value: banana\\nIndex: 2, Value: cherry\\n
\\nMap.fromIterable
List
或 Set
)创建一个 Map
,可以指定键和值。Iterable
对象并想根据一定的规则构建一个 Map
,可以使用 fromIterable
。List<String> list = [\'apple\', \'banana\', \'cherry\'];\\n\\nMap<int, String> map = Map.fromIterable(\\n list,\\n key: (item) => list.indexOf(item), // 使用索引作为键\\n value: (item) => item, // 使用元素作为值\\n);\\n\\nprint(map); // 输出: {0: apple, 1: banana, 2: cherry}\\n
\\nentries
: 用于遍历 Map
的键值对,适用于直接操作 MapEntry
。asMap()
: 用于将 List
转换为 Map
,适用于需要根据索引访问列表的情况。addAll
: 用于将另一个 Map
的内容添加到当前 Map
。containsKey
和 containsValue
: 用于判断 Map
是否包含指定的键或值。remove
: 用于移除指定键的键值对。update
: 用于更新指定键的值。addEntries
: 用于批量添加多个键值对。updateAll
: 用于更新 Map
中所有值。forEach
: 用于遍历整个 Map
。putIfAbsent
: 用于插入一个不存在的键值对。fromIterable
: 用于从 Iterable
创建一个 Map
。这些方法和属性提供了多种灵活的方式来操作 Map
类型的数据。选择使用哪个方法,取决于要完成的具体任务。
面向对象编程(OOP) 作为编程范式的重要里程碑,彻底改变了我们构建软件的方式。它引入对象的概念,强调以对象为核心,以一种更接近现实世界的方式建模问题域,通过封装、继承、多态等特性,实现了代码的模块化、复用性和灵活性的显著提升。接下来让我们一起在Dart世界中探索OOP的奥秘。
\\n注: 对象是数据和作用于数据的操作的封装体。
\\n类似对象编程(OOP)是相对于面向过程来说的,是一种编程范式。它引入了对象的概念,以一种更接近现实世界的方式建模问题域。其核心思想在于封装、继承、多态和抽象。
\\n记忆方法: 一二三四五 二十三
\\n一个类:类的相关概念。
\\n两个世界:现实世界、计算机世界。
\\n三大特性:封装、继承、多态。
\\n四大支柱:封装、继承、多态、抽象。
\\n五大原则:单一职责原则、开放封闭原则、接口隔离原则、依赖倒置原则、里式替换原则。
\\n二十三种设计模式(后续文章介绍)。
\\n类:创建对象的模版或蓝图。
\\n类实例生成(由构造函数完成)对象,对象抽象为类。\\n
注意: 在设计与实现时,我们首先接触的不是对象而是类和类层次结构。
\\n由class关键字+类名+类体(大括号{})定义。
\\nclass
关键字进行定义。示例:
\\n/// 使用class关键字定义一个类名为Animal的类。\\nclass Animal{\\n String name; // 属性\\n Animal(this.name); // 构造函数\\n void roar(){ // 方法\\n print(\'${this.name} 会叫\');\\n }\\n}\\n
\\n继承是子类与父类共享属性与方法的一种构造。一般是用类来体现继承关系。如下图中子类(鸟类、鱼类) 继承自父类(动物类)。
\\n\\n子类与父类的继承关系构成了类层次结构。\\n当执行子类的实例生成方法时,其执行顺序为:
重置或覆盖:子类重新定义父类中继承过来的方法。其基本思想是动态绑定的支持。
\\n示例:
\\n/// 使用class关键字定义一个类名为Animal的类。\\nclass Animal{\\n String name; // 属性\\n Animal(this.name); // 构造函数\\n void skill(){ // 方法\\n print(\'${this.name} 会叫!\');\\n }\\n}\\n/// 定义一个Bird类继承自Animal类\\nclass Bird extends Animal{\\n Bird(super.name); // 子类继承父类时,必须实例化父类\\n @override\\n void skill(){ // 重置了父类中的skil()方法\\n print(\'${this.name} 会飞!\');\\n }\\n \\n}\\nvoid main() {\\n Animal parrot_a = Animal(\'鹦鹉\');\\n parrot_a.skill();\\n Bird parrot_b = Bird(\'鹦鹉\');\\n parrot_b.skill();\\n}\\n// 输出:\\n鹦鹉 会叫!\\n鹦鹉 会飞!\\n
\\nDart 支持显式定义访问或修改私有属性。
\\n示例:
\\n/// 使用class关键字定义一个类名为Animal的类。\\nclass Animal{\\n String _name; // 属性\\n Animal(this._name); // 构造函数\\n // Getter\\n String get name => _name; \\n // Setter \\n set name(String value) { \\n if (value.isNotEmpty) {\\n _name = value;\\n } else { \\n throw ArgumentError(\'Name cannot be empty\');\\n } \\n }\\n void skill(){ // 方法\\n print(\'${this._name} 会叫!\');\\n }\\n}\\n\\nvoid main() {\\n Animal parrot_a = Animal(\'鹦鹉\');\\n parrot_a.skill();\\n parrot_a.name = \'喜鹊\'; // 修改私有属性\\n parrot_a.skill();\\n}\\n// 输出:\\n鹦鹉 会叫!\\n喜鹊 会叫!\\n
\\n对象自身引用是编程语言中的一种特有的结构。Dart中使用this
关键字。
示例:
\\n/// 使用class关键字定义一个类名为Animal的类。\\nclass Animal{\\n String name; \\n Animal(this.name); // this.name对象自身引用\\n void skill(){ \\n print(\'${this.name}会叫!\'); // this.name对象自身引用\\n }\\n}\\n\\nvoid main() {\\n Animal parrot_a = Animal(\'鹦鹉\');\\n parrot_a.skill();\\n}\\n// 输出:\\n鹦鹉会叫!\\n
\\n对象是数据和作用于数据的操作的封装体。
\\n对象是类的实例化。
\\n示例:
\\n/// 使用class关键字定义一个类名为Animal的类。\\nclass Animal{\\n String name; \\n Animal(this.name); \\n void skill(){ \\n print(\'${this.name}会叫!\');\\n }\\n}\\n\\nvoid main() {\\n Animal parrot_a = Animal(\'鹦鹉\'); // 实例化Animal类\\n parrot_a.skill(); // parrot_a 为实例化的对象。\\n}\\n// 输出:\\n鹦鹉会叫!\\n
\\n对象之间进行通信的构造。通常包含接收者,接收者方法,接收者参数、返回值(可选)。
\\n示例:
\\n/// 使用class关键字定义一个类名为Animal的类。\\nclass Animal{\\n String name; \\n Animal(this.name); \\n void skill(){ \\n print(\'${this.name}会叫!\'); \\n }\\n}\\nvoid main() {\\n Animal parrot_a = Animal(\'鹦鹉\'); // 实例化Animal类\\n parrot_a.skill(); // 消息传递,向parrot_a 对象发送消息\\n}\\n// 输出:\\n鹦鹉会叫!\\n
\\n构造函数名与类名相同。
\\n示例:
\\n/// 使用class关键字定义一个类名为Animal的类。\\nclass Animal{\\n String name; \\n Animal(this.name); // 构造函数。函数名与类名相同。\\n}\\n
\\nDart中允许有多个构造函数,并且可以给构造函数起一个别名。
\\n示例:
\\n/// 使用class关键字定义一个类名为Animal的类。\\nclass Animal{\\n String name;\\n Animal(this.name); // 构造函数。函数名与类名相同。\\n Animal.getname(this.name); // 命名构造函数,getname为重新命的名。\\n void skill(){\\n print(\'$name会叫\');\\n }\\n}\\nvoid main(){\\n Animal parrot_a = Animal(\'鹦鹉\');\\n parrot_a.skill(); // 输出:鹦鹉会叫\\n Animal parrot_b = Animal.getname(\'鹦鹉\');\\n parrot_b.skill(); // 输出:鹦鹉会叫\\n}\\n
\\n在Dart中可以在构造函数后加冒号(:
)表示为初始化列表,初始化列表内容可以是表达式或方法调用。
示例:
\\n/// 使用class关键字定义一个类名为Animal的类。\\nclass Animal{\\n String name; // 属性\\n double wingspan;\\n double height;\\n Animal(this.name, this.wingspan, this.height); // 默认构造函数\\n Animal.getinfo(this.name,this.wingspan):this.height=wingspan + 1; // 命名构造函数\\n void info(){\\n print(\'${this.name}高:${this.height},翅膀长:${this.wingspan}\');\\n }\\n}\\nvoid main(){\\n Animal parrot_a = Animal(\'鹦鹉\',10,11);\\n parrot_a.info(); // 输出:鹦鹉高:11.0,翅膀长:10.0\\n Animal parrot_b = Animal.getinfo(\'鹦鹉\',10);\\n parrot_b.info(); // 输出:鹦鹉高:11.0,翅膀长:10.0\\n}\\n
\\n继承时子类调用父类的构造函数。
\\n示例:
\\n/// 使用class关键字定义一个类名为Animal的类。\\nclass Animal{\\n String name; \\n Animal(this.name); \\n void skill(){\\n print(\'${this.name} 会叫!\');\\n }\\n}\\n/// 定义一个Bird类继承自Animal类\\nclass Bird extends Animal{\\n Bird(super.name); // 调用父类的构造函数\\n @override\\n void skill(){ \\n print(\'${this.name} 会飞!\');\\n }\\n \\n}\\nvoid main() {\\n Animal parrot_a = Animal(\'鹦鹉\');\\n parrot_a.skill();\\n Bird parrot_b = Bird(\'鹦鹉\');\\n parrot_b.skill();\\n}\\n// 输出:\\n鹦鹉 会叫!\\n鹦鹉 会飞!\\n
\\n工厂构造函数用于返回现有的实例或不同类型的对象,而不必每次都创建新的实例。
\\n使用 factory 关键字
\\n示例:
\\n/// 使用class关键字定义一个类名为Animal的类。\\nclass Animal{\\n static Map<String,Animal> _cahe = <String, Animal>{};\\n final String name;\\n Animal._factory(this.name); // 只能使用命名构造函数\\n factory Animal(String name){ // 工厂构造函数固定写法,函数名与类名相同\\n if (_cahe.containsKey(name)){\\n return _cahe[name]!; // 必须断言其不为空,否则编译器报错\\n }else{\\n final animal = Animal._factory(name);\\n _cahe[name] = animal;\\n return animal;\\n }\\n }\\n void skill(){\\n print(\'${this.name} 会叫!\');\\n }\\n}\\nvoid main() {\\n Animal parrot_a = Animal(\'鹦鹉\');\\n Animal parrot_b = Animal(\'鹦鹉\');\\n print(parrot_a == parrot_b); // 输出:true\\n}\\n
\\n通过工厂构造函数可以简洁的返回不同类型的对象。
\\n示例:
\\n/// 使用class关键字定义一个类名为Animal的类。\\nclass Animal{\\n String name;\\n Animal(this.name);\\n void roar(){\\n print(\'${this.name} 会叫!\');\\n }\\n}\\n/// 定义一个Bird类继承自Animal类\\nclass Bird extends Animal{\\n Bird(super.name);\\n @override\\n void roar(){\\n print(\'${this.name}叽叽喳喳!\');\\n }\\n\\n}\\n/// 定义一个Dog类继承自Animal类\\nclass Dog extends Animal{\\n Dog(super.name);\\n @override\\n void roar(){\\n print(\'${this.name}汪汪!\');\\n }\\n}\\n/// 工厂模式的类\\nclass AnimalFactory{\\n // 静态工厂方法\\n static Animal createFactory(String name){ // Animal为返回的函数类型\\n switch(name){\\n case \'小狗\':\\n return Dog(name);\\n case \'小鸟\':\\n return Bird(name);\\n default:\\n throw ArgumentError(\'Unknown Animal name: $name\');\\n }\\n }\\n}\\nvoid main() {\\n Animal bird = AnimalFactory.createFactory(\'小鸟\');\\n bird.roar(); // 输出:小鸟叽叽喳喳!\\n Animal dog = AnimalFactory.createFactory(\'小狗\');\\n dog.roar(); // 输出:小狗汪汪!\\n}\\n
\\n本小节聚焦于面向对象方法的应用,首先阐述了面向对象编程的核心理念,随后详细展示了在Dart语言中如何定义类与对象的过程,并最终详细介绍了类定义中不可或缺的构造函数部分。
","description":"前言 面向对象编程(OOP) 作为编程范式的重要里程碑,彻底改变了我们构建软件的方式。它引入对象的概念,强调以对象为核心,以一种更接近现实世界的方式建模问题域,通过封装、继承、多态等特性,实现了代码的模块化、复用性和灵活性的显著提升。接下来让我们一起在Dart世界中探索OOP的奥秘。\\n\\n注: 对象是数据和作用于数据的操作的封装体。\\n\\n一、面向对象编程概述\\n\\n类似对象编程(OOP)是相对于面向过程来说的,是一种编程范式。它引入了对象的概念,以一种更接近现实世界的方式建模问题域。其核心思想在于封装、继承、多态和抽象。\\n\\n记忆方法: 一二三四五 二十三\\n\\n一个类…","guid":"https://juejin.cn/post/7463688772560748555","author":"好的佩奇","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-25T13:19:19.537Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e68771ad436c40a4aac510f1d12c67df~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5aW955qE5L2p5aWH:q75.awebp?rk3s=f64ab15b&x-expires=1738415958&x-signature=rnyVcDiL32PXrUDRT2b5N%2Bk3Ffc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/04918bb847b2467d8352a069070575eb~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5aW955qE5L2p5aWH:q75.awebp?rk3s=f64ab15b&x-expires=1738415958&x-signature=BUQDypEgpYUqSTJh%2BOsoGLJk46A%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/8eff09406c834bfd84ff8d9460465da0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5aW955qE5L2p5aWH:q75.awebp?rk3s=f64ab15b&x-expires=1738415958&x-signature=A8%2BkVwreSnfDz6E7wmyhFCIMOGo%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"轻松组织了我的 Flutter 项目代码!","url":"https://juejin.cn/post/7462934560944865331","content":"日常开发Flutter项目过程中,我一直都在寻找一种方法来组织我的代码。维护多个项目从来不容易,代码越干净,维护起来就越容易。
\\n这是我在所有项目中使用的结构,这意味着从一个项目跳到另一个项目很容易,我不必去挖掘东西在哪里。与我一起工作的团队也习惯了这种结构,这有助于最大限度地减少我们的生产,从而在几天内制作应用程序。
\\n让我们进一步讨论结构:
\\nScreen:这是应用程序的所有屏幕通常所在的文件夹。正如屏幕截图上看到的,每个屏幕都有自己的文件夹。在这个特定的示例中,应用程序有一个主屏幕,它使用底部导航栏和4个主要屏幕(战争、新闻、major_orders和游戏)。我稍后会详细讨论这部分。
\\nWidgets:这是应用程序的所有可重用小部件通常所在的文件夹。在这个文件夹中,我保存了像我的custom_scaffold和所有“回收器”或列表项小部件这样的文件。
\\nServices:这基本上是我制作的应用程序中使用的所有外部服务,也称为我的DIO文件夹。这里的类通常包括连接到向应用程序提供数据的REST API。
\\nModels:在服务文件夹之后,我总是将我的模型(对象类)保存在一个名为模型的单独文件夹中。对于过去使用Flex或AIR的人来说,这曾经是我的ValueObjects文件夹。字典术语的模型示例:
\\nclass TermVO { \\n int? id; \\n String? title; \\n String? description; \\n String? language; \\n \\n \\n TermVO({this.id, this.title, this.description, this.language}); \\n \\n TermVO.fromJson(Map<String, dynamic> json) { \\n id = json[\'id\']; \\n title = json[\'title\']; \\n description = json[\'description\']; \\n language = json[\'language\']; \\n }\\n}\\n
\\nUtils:此文件夹是外部API或本地库的所有“助手”文件的所在地。在这里,我将保留像Firebase助手类、OneSignal助手类甚至我自己的StringHelper类这样的东西,它包括在我们的项目中格式化字符串的函数。
\\nStyles:这很简单,因为它包括应用程序的主题和样式。
\\nCommon. dart:这是一个文件,我们将其用作应用程序中使用的所有内容的主要“导入”。这样您只需在文件中导入一个文件,这也有助于快速重构、更改或添加库。
\\n有了这种结构,你甚至可以让大项目组织得很好,让你的生活更轻松。
","description":"日常开发Flutter项目过程中,我一直都在寻找一种方法来组织我的代码。维护多个项目从来不容易,代码越干净,维护起来就越容易。 这是我在所有项目中使用的结构,这意味着从一个项目跳到另一个项目很容易,我不必去挖掘东西在哪里。与我一起工作的团队也习惯了这种结构,这有助于最大限度地减少我们的生产,从而在几天内制作应用程序。\\n\\n让我们进一步讨论结构:\\n\\nScreen:这是应用程序的所有屏幕通常所在的文件夹。正如屏幕截图上看到的,每个屏幕都有自己的文件夹。在这个特定的示例中,应用程序有一个主屏幕,它使用底部导航栏和4个主要屏幕(战争、新闻、major…","guid":"https://juejin.cn/post/7462934560944865331","author":"BUG集结者","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-23T09:04:23.701Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ade49254a50a49d7ac481b45f6c558c2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQlVH6ZuG57uT6ICF:q75.awebp?rk3s=f64ab15b&x-expires=1738228279&x-signature=PvlKC06togmY2ePQ6Cy8bhPMLuM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5087f11a398442bd944b24f4ecd84044~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQlVH6ZuG57uT6ICF:q75.awebp?rk3s=f64ab15b&x-expires=1738228279&x-signature=NRbI6FyifLLz5f9vxj17JYWCXxI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0dd78fb1cb324115b9e656565037ceb5~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgQlVH6ZuG57uT6ICF:q75.awebp?rk3s=f64ab15b&x-expires=1738228279&x-signature=q6H4Q1YlGfFx7EL4C0RQpBljySo%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"Flutter Tabbar 切换不要每次初始化","url":"https://juejin.cn/post/7462924455313096743","content":"Tabbar
切换不要每次初始化方式一:用 IndexedStack
就行了,比如 IndexedStack
管理着 a b c
页面,然后 a
跳转到 d
页面,d
不会被缓存。因为 IndexedStack
只控制显示的页面,而不直接控制页面内的导航栈(Navigator
)。
方式二:AutomaticKeepAliveClientMixin
则是只针对 StatefulWidget
且只希望在页面切换时保持部分页面的状态,使用 AutomaticKeepAliveClientMixin
更加灵活且节省内存。
IndexedStack
适用于你希望在多个页面之间切换时,保留所有页面的状态(即每个页面只初始化一次,后续切换时不会重新构建页面)。如果你有多个标签页,且每个标签页都有独立的状态,使用 IndexedStack
可以确保所有页面都保持在内存中,不会丢失数据。
IndexedStack
会创建一个包含所有页面的堆栈,并根据当前的索引选择显示哪个页面。每次切换 Tab 时,之前的页面会保持在堆栈中,而不会被销毁或重新创建。适用于希望在切换页面时保存单个页面的状态(如滚动位置、表单输入等),特别是在需要跨页面切换但又不希望重新加载或重置页面状态时。常用于 ListView
、GridView
等有滚动或其他状态的页面。
PageStorage
可以在页面切换时保存和恢复页面的状态,使用 PageStorageKey
为每个页面分配唯一的标识符。Flutter 会自动保存每个页面的状态(如滚动位置),并在页面重新显示时恢复。PageStorageKey
,且不适用于整个页面的状态保存,更多是适用于特定的组件或视图。适用于在 StatefulWidget
中,当你希望在页面切换时保持每个页面的状态(如表单输入、动画状态等)。如果你有多个页面,并且希望每个页面的状态都被保留,可以将该 mixin 应用于每个页面的 State
类。
AutomaticKeepAliveClientMixin
,并将 wantKeepAlive
设置为 true
,你可以确保页面的状态在切换时保持不变。这个方法通常用于控制多个页面状态是否应该被保持。StatefulWidget
中保持状态的场景。State
类中实现并正确配置,可能会增加一些代码复杂度。适用于你希望在每个 Tab 页中保持独立的导航历史栈的情况。通常在需要保持每个 Tab 页面内部的导航状态(比如你在 Home
Tab 下有一个跳转到 Detail
页的需求)时,可以在每个页面使用 Navigator
。
Navigator
允许每个 Tab 页面拥有独立的导航栈。这使得每个页面都可以进行独立的页面跳转,而不影响其他页面的导航历史。Navigator
时,可能需要一些额外的逻辑。适用于使用 TabBar
和 TabBarView
进行页面切换的场景,尤其是在你希望保持页面状态时(比如滚动位置、表单状态等)。通常用于带有选项卡(Tab)的界面,Tab 内部的页面通常是独立的,且需要保持状态。
TabController
用于管理 Tab 页的状态(当前选中的 Tab)。TabBarView
是用于显示 Tab 页内容的组件,它会自动与 TabController
配合工作来显示相应的页面。AutomaticKeepAliveClientMixin
,可以在切换 Tab 时保留页面的状态。TabController
配合使用时,可以轻松管理 Tab 页的切换。AutomaticKeepAliveClientMixin
一起使用,页面会在切换时被销毁。StatefulWidget
时。AutomaticKeepAliveClientMixin
可以保持状态。选择的方法主要取决于你需要管理的页面状态的范围和复杂度。如果只是需要在 Tab 页面间切换并保持状态,IndexedStack
或 TabController
配合 AutomaticKeepAliveClientMixin
是不错的选择。如果页面复杂并且需要独立的导航栈,Navigator
可能更适合。
今天刚好看到官方发布了一篇文章,用于讨论 Compose Multiplatform 和 Jetpack Compose 之间的区别,突然想起之前评论区经常看到说 “Flutter 和 CMP 对于 Google 来说项目重叠的问题”,刚好可以放一起聊一聊。
\\n\\n\\n最近写的几篇内容写的太干,刚好要过年,大家也放假了,今天写篇水的。
\\n
实际上很多时候大家在讨论 Compose 的时候,会下意识把 Jetpack Compose 和 Compose Multiplatform 当成一个东西 ,但是实际上其实并合适,同样的情况也经常发生在 Kotlin Multiplatform (KMP) 和 Compose Multiplatform 之间。
\\n这里其实需要搞清楚一个项目“归属”问题,就像是 JetBrains 自己发的这个 :
\\n所以,你如果从实际项目归属看,其实严格意义上说 Compose Multiplatform 是属于 JetBrains 开发的「拓展」支持,本质上并不是直接归属 Google 项目,属于合作性质,所以从内部项目来说,它和 Flutter 并不直接重叠。
\\n\\n\\n只是,由于 Compose Multiplatform 是基于 Jetpack Compose 开发,因此使用这些框架的体验非常相似,同时两者都由 Compose 内部的 compiler 和 runtime 进行支持,所以有相同的核心概念,可以用类似的 API 来构建 UI,包括
\\n@Composable
函数、状态处理 API(如remember
)、UI 组件(如Row
和Column
)、修饰符、动画 API 等。
比如 JetBrains 提到,Jetpack 包含的 first-party libraries,例如 Foundation 和 Material 等,这些都是 Google 为 Android 发布的,而为了使这些库提供的 API 可从通用代码中使用,JetBrains 维护了这些库的多平台版本,这些库是为 Android 以外的目标发布的。
\\n\\n\\n所以其实整个社区生态也是 JetBrains 在维护。
\\n
类似的还有 2024 Google I/O 上正式官宣的 Kotlin Multiplatform,它也是 Google Workspace 团队的一项长期「投资」项目,由 JetBrains 开发维护和开源的项目,简单来说,JetBrains 主导投入,Google Workspace 投资并提供技术支持。
\\n所以本质上你看 Compose Multiplatform 和 Kotlin Multiplatform 的资料,它都是在 JetBrains 相关的网站发布,属于 JetBrains 的项目,甚至托管 Package 的 klibs.io 平台,也是属于 JetBrains 管理和发布。
\\n当然,你要说和 Google 完全没关系肯定是不可能的,毕竟 Kotlin 、KMP、CMP 都属于 Google 和 JetBrains 深度合作项目,但是你要说是完全「亲生儿子」,又不是十分恰当,就像 JetBrains 提到的:
\\n\\n\\nCompose Multiplatform 是基于 Google 发布的代码和版本构建,虽然 Google 的重点是适用于 Android 的 Jetpack Compose,但 Google 和 JetBrains 之间也密切合作以实现 Compose Multiplatform。
\\n
从这里理解,就可以大概理清楚:
\\n是的,事实上 Kotlin Multiplatform 和 Compose Multiplatform 还需要分开看待,Kotlin Multiplatform 属于是 Kotlin 的「拓展」功能,它和 Compose Multiplatform 其实并没有“必然” 的关系:
\\n\\n\\n你不用 Compose Multiplatform ,也可以使用 Kotlin Multiplatform ,它是支持独立运行的存在 。
\\n
如果硬是要举例,那就是 Kotlin Multiplatform 是可以直接用于编写跨平台共享业务逻辑的,甚至曾经就有些项目是 Flutter 写 UI ,然后 Kotlin Multiplatform 写业务的情况。
\\n只是现在有了 Compose Multiplatform , 所以 Kotlin Multiplatform 可以作为 Compose Multiplatform 的插件和底层跨平台支撑。
\\n反过来看,也可以认为 Compose Multiplatform 作为 Kotlin Multiplatform 项目中的 UI 支持,它不是 Kotlin Multiplatform 本身的一部分,只是一个通过启用共享 UI 来补充 KMP 的 SDK。
\\n\\n\\n就像是,你想在鸿蒙上兼容 KMP 和 Compose Multiplatform ,那其实是两个工作量。
\\n
所以,很多时候我们在提 Compose 的时候,会直接潜意识的把 Jetpack Compose、Compose Multiplatform 和 Kotlin Multiplatform 都当成一个整体和归属讨论,当时实际上,它们之间还是需要区分,也有必要做一些区分。
\\nwww.jetbrains.com/help/kotlin…
","description":"今天刚好看到官方发布了一篇文章,用于讨论 Compose Multiplatform 和 Jetpack Compose 之间的区别,突然想起之前评论区经常看到说 “Flutter 和 CMP 对于 Google 来说项目重叠的问题”,刚好可以放一起聊一聊。 最近写的几篇内容写的太干,刚好要过年,大家也放假了,今天写篇水的。\\n\\n实际上很多时候大家在讨论 Compose 的时候,会下意识把 Jetpack Compose 和 Compose Multiplatform 当成一个东西 ,但是实际上其实并合适,同样的情况也经常发生在 Kotlin…","guid":"https://juejin.cn/post/7462776363734564916","author":"恋猫de小郭","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-23T06:47:33.507Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ef70c6c5d7eb439c917bb8fa13e3727a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1738219652&x-signature=31TxsALVsLRkR1M5tCPawrWsngA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/bd75d03f192146969009b637afb57891~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oGL54yrZGXlsI_pg60=:q75.awebp?rk3s=f64ab15b&x-expires=1738219652&x-signature=GirvICbwuEiXQ1igmOjxRUDmPhI%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"系统化掌握Dart编程之映射(Map)","url":"https://juejin.cn/post/7462671646630281266","content":"集合 —— 操作批量数据
的核心工具
在 Dart
中,Map
是一种非常强大的数据结构,它允许存储键值对(key-value pairs
),其中每个键都是唯一的。Map
类似于现实世界中的字典
或电话簿
——通过一个唯一的标识符(键
)来查找对应的信息(值
)。本章节将深入理解 Dart
中的 Map
,系统化掌握其创建
、操作
和优化
方法。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\n姓名 | 电话 |
---|---|
Alice | 19999999999 |
Bob | 18888888888 |
Charlie | 17777777777 |
如上图所示,有一个班级的学生电话簿,左边是人名
,右边是对应的电话号码
。想找某个人的电话时,只需要知道名字就可以快速找到电话号码。这就是 Map
的工作方式 —— 它由键(Key
)和值(Value
)组成,通过键来查找对应的值
。
Map
是一个无序的键值对集合,其中每个键是唯一的。可以通过键快速查找对应的值,非常适合用于关联数据
的场景,如配置文件解析
、用户信息管理
等。
Map<String, String> infos = {\'Alice\':\\"19999999999\\", \'Bob\': \\"18888888888\\", \'Charlie\': \\"17777777777\\"};\\n
\\n一次
。键
可以对应相同的值
。Map
中的元素没有固定的顺序。 // 学生电话薄\\n Map<String, String> infos = {\'Alice\':\\"19999999999\\", \'Bob\': \\"18888888888\\", \'Charlie\': \\"17777777777\\"};\\n\\n// 查找某个学生的电话\\n print(infos[\\"Alice\\"]); // 输出: 19999999999\\n\\n// 添加新的学生信息\\n infos[\\"David\\"] = \'16666666666\';\\n\\n// 输出学生信息\\n print(infos); // 输出: {Alice: 19999999999, Bob: 18888888888, Charlie: 17777777777, David: 16666666666}\\n
\\nDart
的 Map
支持泛型,允许指定映射中元素的具体类型
,从而提高代码的类型安全性
和可读性
。
最简单的方式是直接在花括号{}
中列出键值对
。
Map<String, int> ages = {\'Alice\': 30, \'Bob\': 25, \'Charlie\': 35};\\n
\\n使用 Map
类的构造函数来创建一个空Map
或具有初始键值对的Map
。
// 创建一个空 Map\\nMap<String, int> emptyMap = <String, int>{};\\n\\n// 创建一个包含初始键值对的 Map\\nMap<int, String> numbersToWords = Map<int, String>.from({\\n 1: \'one\',\\n 2: \'two\',\\n 3: \'three\'\\n});\\n
\\nMap.of
构造函数Map.of
可以从另一个 Map
创建一个新的 Map
,确保键值对的独立性
。
Map<String, String> original = {\'apple\': \'苹果\', \'banana\': \'香蕉\'};\\nMap<String, String> copy = Map.of(original);\\n
\\nMap.fromIterable
和 Map.fromEntries
Map.fromIterable
:根据可迭代对象创建 Map
,并指定键和值生成器。Map.fromEntries
:根据可迭代的 MapEntry
对象创建 Map
。\\nMap.of
可以从另一个 Map
创建一个新的 Map
,确保键值对的独立性
。List<String> keys = [\'apple\', \'banana\'];\\nList<String> values = [\'苹果\', \'香蕉\'];\\n\\nMap<String, String> fruits = Map.fromIterables(keys, values);\\n\\nList<MapEntry<String, String>> entries = [\\n MapEntry(\'apple\', \'苹果\'),\\n MapEntry(\'banana\', \'香蕉\')\\n];\\nMap<String, String> fruitsFromEntries = Map.fromEntries(entries);\\n
\\n使用键
来访问对应的值
。
Map<String, int> ages = {\'Alice\': 30, \'Bob\': 25, \'Charlie\': 35};\\nprint(ages[\'Alice\']); // 输出: 30\\n
\\n可以通过键
直接修改
对应的值
。
ages[\'Alice\'] = 31;\\nprint(ages[\'Alice\']); // 输出: 31\\n
\\n如果键不存在
,则添加新的键值对
;如果键存在
,则更新对应的值
。
ages[\'David\'] = 28;\\nprint(ages); // 输出: {Alice: 31, Bob: 25, Charlie: 35, David: 28}\\n
\\nremove
方法。ages.remove(\'Bob\');\\nprint(ages); // 输出: {Alice: 31, Charlie: 35, David: 28}\\n
\\nMap
:使用clear
方法。ages.clear();\\nprint(ages); // 输出: {}\\n
\\nforEach
forEach
方法允许为 Map
中的每个键值对执行一个回调函数。
ages.forEach((key, value) => print(\'$key: $value\'));\\n
\\nentries
属性结合for-in
循环for (var entry in ages.entries) {\\n print(\'${entry.key}: ${entry.value}\');\\n}\\n
\\n// length:获取 Map 的大小(键值对的数量)\\nprint(ages.length); \\n// isEmpty 和 isNotEmpty:检查 Set 是否为空。\\nprint(emptySet.isEmpty);\\n
\\n// 1、使用 containsKey 检查某个键是否存在。\\nprint(ages.containsKey(\'Alice\')); // 输出: true\\n\\n// 2、使用 containsValue 检查某个值是否存在。\\nprint(ages.containsValue(30)); // 输出: true\\n
\\n// 1、使用 `keys` 属性获取所有键。\\nprint(ages.keys); // 输出: (Alice, Bob, Charlie)\\n\\n// 2、使用 values 属性获取所有值。\\nprint(ages.values); // 输出: (30, 25, 35)\\n
\\nMap
:\\n// 使用 addAll 方法将另一个 Map 的键值对添加到当前 Map\\nMap<String, int> moreAges = {\'Eve\': 32};\\nages.addAll(moreAges);\\nprint(ages); // 输出: {Alice: 30, Bob: 25, Charlie: 35, Eve: 32}\\n
\\nMap
(Map.unmodifiable
)有时需要确保一个 Map
不会被修改。可以使用 Map.unmodifiable
来创建一个不可变的 Map
。
Map<String, int> immutableAges = Map.unmodifiable({\'Alice\': 30, \'Bob\': 25});\\n\\n// 下面这行代码会抛出异常,因为 immutableAges 是不可变的\\n// immutableAges[\'Charlie\'] = 35;\\n
\\nMap
提供了一种强大而灵活的方式来处理键值对
数据。通过合理利用 Map
的特性,可以编写出更加简洁
、高效和易于维护
的代码。
\\n","description":"前言 集合 —— 操作批量数据的核心工具\\n\\n在 Dart 中,Map 是一种非常强大的数据结构,它允许存储键值对(key-value pairs),其中每个键都是唯一的。Map 类似于现实世界中的字典或电话簿——通过一个唯一的标识符(键)来查找对应的信息(值)。本章节将深入理解 Dart 中的 Map,系统化掌握其创建、操作和优化方法。\\n\\n操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。\\n\\n一、基本概念\\n1.1、图像表示\\n姓名\\t电话Alice\\t19999999999\\nBob\\t18888888888\\nCharlie\\t17…","guid":"https://juejin.cn/post/7462671646630281266","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-23T01:10:27.577Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3d727c407d0c43c4900e564adf8f5e03~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1738199427&x-signature=yXVockDKDh2V0go0rYCuvn3oD34%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"Flutter GetX 使用 Camera 实现拍照功能","url":"https://juejin.cn/post/7462622176270614562","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
用前置摄像头采集用户头像
\\n运行 flutter pub add
将Camera模块其添加为依赖:
flutter pub add camera\\n
\\n使用相机首先要获取可用相机列表。
\\n// Ensure that plugin services are initialized so that `availableCameras()`\\n// can be called before `runApp()`\\nWidgetsFlutterBinding.ensureInitialized();\\n\\n// Obtain a list of the available cameras on the device.\\nfinal cameras = await availableCameras();\\n\\n// Get a specific camera from the list of available cameras.\\nfinal firstCamera = cameras.first;\\n
\\nCameraController
在选择了一个相机后,你需要创建并初始化 CameraController
。在这个过程中,与设备相机建立了连接并允许你控制相机并展示相机的预览帧流。
在使用相机前,请确保控制器已经完成初始化。因此,要等待前一个步骤创建 initialize()
执行完毕才去展示 CameraPreview
。
Future<void> _initCamera() async {\\n List<CameraDescription> cameras = await availableCameras();\\n CameraDescription frontCamera = cameras[0];\\n for (var camera in cameras) {\\n if (camera.lensDirection == CameraLensDirection.front) {\\n frontCamera = camera;\\n break;\\n }\\n }\\n controller = CameraController(frontCamera, ResolutionPreset.max, enableAudio: false);\\n try {\\n // 初始化\\n await controller.initialize();\\n isCameraInitialized.value = true;\\n applyEnabled.value = true;\\n } on CameraException catch (e) {\\n print(e);\\n }\\n}\\n\\n@override\\nvoid onInit() {\\n _initCamera();\\n super.onInit();\\n}\\n\\n// 需要销毁Controller\\n@override\\nvoid onClose() {\\n controller.dispose();\\n super.onClose();\\n}\\n\\n// 组件\\nWidget _buildCamera() {\\n return Obx(() {\\n if (!controller.isCameraInitialized.value) {\\n return Container(\\n color: Colors.black54,\\n child: Center(\\n child: CircularProgressIndicator(),\\n ),\\n );\\n } else {\\n return CameraPreview(controller.controller);\\n }\\n });\\n}\\n
\\n\\n\\n注意
\\n如果你没有初始化
\\nCameraController
,你就 不能 使用相机预览和拍照。
CameraController
拍照接着就可以调用controller.takePicture()来拍照,它获得一个XFile
对象,直接就可以通过path
展示出来。
Future<void> takePicture() async {\\n if (!isCameraInitialized.value) {\\n return;\\n }\\n applyEnabled.value = false;\\n try {\\n final XFile file = await controller.takePicture();\\n imagePath.value = file.path;\\n });\\n } on CameraException catch (e) {\\n applyEnabled.value = true;\\n }\\n}\\n\\n// 展示图片\\nWidget _buildShowPicture() {\\n return Obx(\\n () => controller.imagePath.value.isEmpty\\n ? SizedBox()\\n : Image.file(\\n File(controller.imagePath.value),\\n width: 100,\\n fit: BoxFit.fitWidth,\\n ),\\n );\\n}\\n\\n// 拍照\\nFloatingActionButton(\\n // Provide an onPressed callback.\\n onPressed: () async {\\n controller.apply();\\n },\\n child: const Icon(Icons.camera_alt),\\n)\\n
\\n以上已经可以拍照了,但是会发现预览镜头是横向的,拍摄出来的图片是正常竖屏的,所以需要将预览镜头进行旋转。
\\nWidget _buildCamera() {\\n return Obx(() {\\n if (!controller.isCameraInitialized.value) {\\n return Container(\\n color: Colors.black54,\\n child: Center(\\n child: CircularProgressIndicator(),\\n ),\\n );\\n } else {\\n return Transform.rotate(\\n angle: -pi / 2, // 将预览画面旋转 90 度\\n child: Center(\\n child: AspectRatio(\\n aspectRatio: controller.controller.value.aspectRatio,\\n child: CameraPreview(controller.controller),\\n ),\\n ),\\n );\\n }\\n });\\n}\\n
\\n同样,在预览中有可能会出现拉伸现象,所以需要进行缩放。
\\nWidget _buildCamera() {\\n return Obx(() {\\n if (!controller.isCameraInitialized.value) {\\n return Container(\\n color: Colors.black54,\\n child: Center(\\n child: CircularProgressIndicator(),\\n ),\\n );\\n } else {\\n return Transform.rotate(\\n angle: -pi / 2, // 将预览画面旋转 90 度\\n child: Transform.scale(\\n scale: Get.pixelRatio,\\n child: Center(\\n child: AspectRatio(\\n aspectRatio: controller.controller.value.aspectRatio,\\n child: CameraPreview(controller.controller),\\n ),\\n ),\\n ),\\n );\\n }\\n });\\n}\\n
\\n这里有个疑问,采用final scale = Get.window.physicalSize / controller.value.previewSize.width
计算出来的 scale = 1, 但展示其实只有设备的一半左右,实际像素和展示像素不一致,故而采用Get.pixelRatio
计算缩放。
本文主要涉及Flutter 性能相关的概念,如渲染、包体积管理、懒加载、线程相关、以及并发操作使用的Dart隔离概念。
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n
在Flutter中,性能优化是提升用户体验的关键,它涉及多个方面,包括流畅度、内存管理、应用大小和功耗。以下是对这四个方面的详细分析:
\\n流畅度是指应用程序在用户交互时的响应速度和界面更新的平滑程度。为了提升Flutter应用的流畅度,可以采取以下措施:
\\n内存管理是指应用程序在运行过程中如何分配、使用和释放内存。为了优化Flutter应用的内存管理,可以采取以下措施:
\\n应用大小是指应用程序安装包的大小,它直接影响到用户的下载和安装体验。为了优化Flutter应用的大小,可以采取以下措施:
\\n功耗是指应用程序在运行过程中消耗的电量或能量。为了优化Flutter应用的功耗,可以采取以下措施:
\\n在Flutter开发中,Impeller 是一个相对较新的渲染引擎后端,旨在替代现有的 Skia 渲染引擎,以提供更高的性能和更好的跨平台一致性。
\\n1、更高效的图形处理:Impeller 使用了更现代的图形 API 和技术,如 Vulkan 和 Metal,这些 API 通常比 Skia 使用的 OpenGL 或 Direct3D 提供了更高的性能和更好的硬件加速能力。
\\n2、减少渲染开销:Impeller 通过优化渲染管道和减少不必要的渲染调用,降低了渲染开销。这有助于减少 CPU 和 GPU 的负担,从而提高应用程序的响应速度和流畅度。
\\n3、更好的跨平台一致性:Impeller 旨在提供一个更加一致的渲染体验,无论你是在 Android、iOS 还是其他平台上运行 Flutter 应用程序。这有助于减少因平台差异而导致的渲染问题,并提高应用程序的兼容性和稳定性。
\\n4、自定义渲染管道:Impeller 允许开发者更加灵活地自定义渲染管道,以满足特定的性能需求或视觉效果。这有助于开发者在保持应用程序性能的同时,实现更加独特和吸引人的视觉效果。
\\n5、可预测的性能(Predictable performance)
\\n6、可仪器化(Instrumentable)
\\n7、可移植性(Portable)
\\n8、利用现代图形 API(Leverages modern graphics APIs)
\\n9、利用并发性(Leverages concurrency)
\\n确保你的 Flutter 环境是最新的,并且可能还需要一些额外的配置步骤(这些步骤可能会随着 Flutter 的更新而变化)
\\n默认启用 Impeller:
\\n在 Flutter 的最新版本中,对于 iOS 平台,Impeller 渲染引擎是默认启用的。这意味着,当您在 iOS 设备或模拟器上运行 Flutter 应用程序时,它会自动使用 Impeller 进行渲染。
\\n调试时禁用 Impeller:
\\n如果您在调试过程中希望禁用 Impeller,可以通过向 flutter run
命令传递 --no-enable-impeller
参数来实现。这样做可以让您的应用程序在调试期间使用旧的渲染引擎(通常是 Skia),而不是 Impeller。
flutter run --no-enable-impeller\\n
\\n部署时禁用 Impeller:
\\n当您准备将 Flutter 应用程序部署到生产环境时,如果您希望禁用 Impeller,可以在应用程序的 Info.plist
文件中添加特定的键值对。这样做可以确保您的应用程序在发布版本中使用旧的渲染引擎。
在 Info.plist
文件中,您需要找到或添加顶层的 <dict>
标签,并在其内部添加以下 XML 代码:
<key>FLTEnableImpeller</key>\\n<false />\\n
\\n您提供的信息是关于 Flutter 在 Android 平台上 Impeller 渲染引擎的默认启用行为以及如何禁用它的详细说明。以下是对您所提供信息的整理和解释:
\\n默认启用 Impeller
\\n在 Flutter 的最新版本中,对于 Android 平台,Impeller 渲染引擎同样是默认启用的。这意味着,当您在 Android 设备或模拟器上运行 Flutter 应用程序时,它会自动尝试使用 Impeller 进行渲染。
\\nVulkan 和 OpenGL 的兼容性
\\n调试时禁用 Impeller
\\n如果您在调试过程中希望禁用 Impeller,可以通过向 flutter run
命令传递 --no-enable-impeller
参数来实现。这样做可以让您的应用程序在调试期间使用旧的渲染引擎(如 OpenGL),而不是 Impeller。
flutter run --no-enable-impeller\\n
\\n部署时禁用 Impeller
\\n当您准备将 Flutter 应用程序部署到生产环境时,如果您希望禁用 Impeller,可以在 Android 项目的 AndroidManifest.xml
文件中添加特定的 <meta-data>
标签。
在 <application>
标签内部添加以下 XML 代码:
<meta-data\\n android:name=\\"io.flutter.embedding.android.EnableImpeller\\"\\n android:value=\\"false\\" />\\n
\\n在 Flutter 开发中,理解帧的构建和渲染时间对于优化应用性能至关重要
\\n使用最新版本的Flutter
\\n最小化小部件的重建,分拆和封装庞大的widget
\\nshouldRebuild
方法来确定小部件是否需要重建。build()
方法返回的widget过于庞大或复杂,应该考虑将其分拆成更小的、可复用的widget。使用无状态小部件,使用StatelessWidget
而不是函数
StatelessWidget
而不是函数。StatelessWidget
提供了更丰富的生命周期管理和状态管理功能,同时也更容易与Flutter的widget系统集成。StatelessWidget
提供了更好的封装性和可测试性。使用const
关键字
const
关键字可以创建编译时常量,优化小部件的渲染和布局。const
可以避免不必要的重绘和内存分配。const
构造函数来创建widget实例。flutter_lints
包中的推荐lints可以帮助自动提醒使用const
。优化列表渲染
\\nListView.builder
小部件来延迟创建和显示列表项。利用widget树的遍历优化
\\nFlutter框架在构建widget树时,会检查每个widget是否与前一帧相同。如果相同(使用operator==
进行比较),则不会遍历其后代widget。
这意味着,如果两个widget实例在逻辑上是相同的(即它们的状态和属性没有改变),那么它们的后代widget也不会被重建。
\\n因此,应该尽量保持widget实例的稳定性和可比较性,以利用这一优化。
\\ncached_network_image
包来缓存图像,减少网络请求次数。AnimatedBuilder
小部件而不是AnimatedWidget
小部件。HttpClient
、Dio
或http
。使用Offstage
来隐藏组件
使用RepaintBoundary
RepaintBoundary
中,减少布局和绘制的工作量。使用LayoutBuilder
和ConstrainedBox
避免在build
方法中执行耗时操作
在initState
中进行耗时操作,避免在build
方法中频繁重绘和重建。
build()
方法是Flutter中widget构建的核心方法。当widget的状态改变或父widget重建时,build()
方法可能会被频繁调用。
因此,应避免在build()
方法中执行任何耗时或复杂的计算。这些操作应该放在initState()
、didUpdateWidget()
等生命周期方法中,或者在单独的函数或类中处理,并通过状态管理传递给widget。
在 Flutter 开发中,saveLayer()
是一个强大的功能,它允许你在绘制操作中创建一个离屏缓冲区(off-screen buffer),这个缓冲区可以在后续的绘制操作中被复用或修改。然而,saveLayer()
的使用需要谨慎,因为它可能会影响性能,特别是在频繁调用或在大面积绘制时使用。
saveLayer()
的原因saveLayer()
会创建一个新的图层,这需要在 GPU 上分配额外的内存和可能的渲染开销。saveLayer()
可能会导致掉帧或性能瓶颈。saveLayer()
的场景saveLayer()
来确保这些变换独立于其他绘制操作。saveLayer()
可以帮助创建一个独立的图层,以便在这些图层上应用遮罩或裁剪效果。saveLayer()
可以将绘制过程分解为多个独立的步骤,每个步骤在一个离屏缓冲区上进行,从而简化绘制逻辑。saveLayer()
的调用次数。如果可以通过其他方式(如使用组合变换、遮罩等)实现相同的效果,优先考虑这些方法。saveLayer()
覆盖的区域大小。只覆盖必要的区域,避免创建过大的离屏缓冲区。saveLayer()
调用覆盖的区域相同或重叠,考虑将它们合并为一个图层,以减少内存使用和渲染开销。RepaintBoundary
\\nRepaintBoundary
替代 saveLayer()
。RepaintBoundary
会触发一个独立的渲染树节点,但它通常用于更复杂的场景,如捕获图像或处理滚动。saveLayer()
对性能的影响。通过分析渲染帧时间和内存使用情况,找到性能瓶颈并进行优化。saveLayer()
性能的优化。AnimatedOpacity
或 FadeInImage
代替该操作检测应用体积是确保应用优化和用户体验的重要步骤
\\nFlutter 提供了一些构建命令,可以帮助开发者分析应用的体积。这些命令会生成包含应用体积详细信息的报告文件。
\\nflutter build apk
或 flutter build ios
命令构建出发布包,该包可以大概评估出用户要下载的包大小。flutter build ios --analyze-size
命令来查看安装包大小,并生成包含详细信息的报告文件。flutter build appbundle --release
命令)可以减小应用体积,因为 App Bundle 会根据用户设备信息动态下发不同的资源包。Flutter 官方提供了应用体积工具,用于分析 Flutter 应用的体积信息。
\\nflutter build <your target platform> --analyze-size
命令构建应用,并生成体积分析文件。在检测应用体积后,开发者可以采取一些方法来优化应用体积,提高用户体验。
\\nflutter analyze
命令来查找未使用的代码。android/app/build.gradle
文件中配置 minifyEnabled
和 shrinkResources
为 true,并配置 ProGuard 规则文件。flutter_image_compress
。flutter_deferred_components
包,按需加载特定模块,而非一次性加载整个应用。pubspec.yaml
文件中的依赖,删除未使用的第三方库。flutter pub deps --style=compact
命令分析依赖包大小,并优化依赖关系。延迟加载组件(也称为懒加载或按需加载)是一种优化应用性能和资源使用的有效方法。通过延迟加载,应用可以在需要时才加载特定的组件或资源,从而减少初始加载时间和内存占用
\\nAndroid平台:
\\nandroid/app/build.gradle
文件中添加Play Core依赖:dependencies {\\n implementation \'com.google.android.play:core:版本号\' // 替换为实际版本号\\n}\\n
\\nAndroidManifest.xml
文件,将application
标签的android:name
属性设置为io.flutter.embedding.android.FlutterPlayStoreSplitApplication
(如果已使用FlutterPlayStoreSplitApplication
,则无需更改)。iOS平台:
\\nWeb平台:
\\n.js
文件,并在需要时动态加载。pubspec.yaml
文件中声明延迟组件在pubspec.yaml
文件的flutter
部分下添加deferred-components
声明,用于指定哪些组件是延迟加载的。例如:
flutter:\\n # ... 其他配置 ...\\n deferred-components:\\n my_deferred_component:\\n path: lib/my_deferred_component # 延迟组件的路径\\n
\\n创建延迟组件:
\\nlib/my_deferred_component
目录下创建一个名为my_deferred_component.dart
的文件。在需要时加载延迟组件:
\\ndeferred as
关键字导入延迟组件,并在需要时调用loadLibrary()
函数来加载它。例如:import \'package:flutter/material.dart\';\\n \\n// 延迟加载组件的导入\\ndeferred import \'my_deferred_component.dart\' as my_deferred_component;\\n \\nvoid main() {\\n runApp(MyApp());\\n}\\n \\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n home: Scaffold(\\n appBar: AppBar(\\n title: Text(\'Delayed Loading Demo\'),\\n ),\\n body: Center(\\n child: ElevatedButton(\\n onPressed: () async {\\n // 加载延迟组件\\n await my_deferred_component.loadLibrary();\\n // 创建并使用延迟组件的Widget\\n Navigator.push(\\n context,\\n MaterialPageRoute(builder: (context) => my_deferred_component.MyDeferredComponentWidget()),\\n );\\n },\\n child: Text(\'Load Deferred Component\'),\\n ),\\n ),\\n ),\\n );\\n }\\n}\\n
\\nflutter build appbundle
(针对Android)或flutter build ios
(针对iOS)命令构建应用。对于Web平台,使用flutter build web
命令。initState
方法)在加载时才会执行。Flutter性能视图(Performance view)是Flutter DevTools的一部分,它对于开发者来说是一个非常重要的工具,因为它可以帮助开发者分析和优化Flutter应用的性能。以下是对Flutter性能视图的详细解释:
\\n性能视图可以记录并分析Dart应用程序的性能,帮助开发者找到应用程序的性能瓶颈。它主要关注于UI的流畅性,即确保应用在每16毫秒(对于60帧每秒的帧率)内能够渲染一帧,从而避免卡顿现象。此外,性能视图还可以分析Dart代码中的性能问题,以及I/O或网速等其他性能指标(尽管本文主要聚焦于UI流畅性)。
\\n打开DevTools:首先,需要在VS Code、Android Studio或IntelliJ等IDE中打开Flutter DevTools。
\\n运行应用:确保应用在以profile模式运行。在VS Code中,可以通过修改launch.json文件设置flutterMode属性为profile来运行应用。在Android Studio和IntelliJ中,则可以通过Run菜单选择Flutter Run main.dart in Profile Mode选项。在命令行中,可以使用flutter run --profile参数来运行应用。
\\n显示性能图层:一旦应用运行在分析模式下,就可以打开性能图层来分析应用的性能。性能图层可以通过Flutter Inspector或直接在DevTools中打开。
\\n分析图表:它用两张图表显示应用的耗时信息,一张显示raster线程的性能情况(在上方),另一张显示UI线程的性能情况(在下方)。
\\n图表中的绿色条代表当前帧,而白线则代表16毫秒的增量。如果白线在图表中都没有被超过,说明应用的运行帧率低于60Hz。\\n
红图表中色竖条则表示当前帧的渲染和绘制都很耗时。如果红色竖条出现在UI图表中,则表明Dart代码消耗了大量资源;如果红色竖条出现在GPU图表中,则意味着场景太复杂导致无法快速渲染,就要开始对 UI 线程 (Dart VM) 进行诊断了\\n
在进行Flutter web应用的性能分析时,确实需要Flutter版本3.14或更高版本。这是因为从该版本开始,Flutter框架提供了更强大的性能分析工具和功能。
\\nFlutter框架在运行时会发出时间线事件,这些事件涵盖了帧的构建、场景的绘制以及垃圾回收等其他活动。这些事件对于调试和优化应用性能至关重要。在Chrome浏览器中,你可以使用Chrome DevTools性能面板来查看这些事件。通过该面板,你可以直观地看到应用的性能瓶颈,进而采取相应的优化措施。
\\n关于如何优化Flutter web应用的加载速度,你可以参考Medium上的免费文章《优化Flutter Web加载速度的最佳实践》。这篇文章提供了许多实用的建议,帮助你提升应用的加载速度和整体性能。
\\n除了Flutter框架自动生成的时间线事件外,你还可以使用dart:developer
包中的Timeline和TimelineTask API来自定义时间线事件。这对于深入分析应用的特定部分或功能非常有用。通过自定义事件,你可以更精确地了解应用在不同阶段的行为和性能表现。
Flutter 插件提供的 Flutter inspector,只需单击 Performance Overlay 按钮,即可在正在运行的应用程序上切换图层
\\n使用 P 参数触发性能图层
\\n在Flutter中,并发操作主要通过Dart语言的Isolate机制来实现
\\ncompute
函数可以简便地创建并执行一个Isolate中的函数。compute
函数接受一个函数和一个参数列表,返回一个Future
,该Future
将在Isolate执行完成后完成。sendPort.send(message)
,接收消息则通过监听ReceivePort
来实现。Isolates在Flutter中主要用于执行需要并发处理的任务,以避免阻塞主线程(UI线程)。常见的用例包括:
\\n每个隔离都有自己的内存和自己的事件循环。事件循环按照事件添加到事件队列的顺序处理事件。在主隔离上,这些事件可以是任何事情,从处理用户在UI中的点击,到执行一个函数,再到在屏幕上绘制一个框架。
\\n下图显示了一个示例事件队列,其中有3个事件等待处理:
\\n当你应该使用隔离时,只有一个硬性规则,那就是当大型计算导致你的Flutter应用程序出现UI阻塞时。当有任何计算需要比Flutter的帧间隙更长的时间时,就会出现这种情况。
\\n如下图 Tap handler 操作时间过长导致阻塞:
\\nIsolates之间通过消息传递进行通信。每个Isolate都有自己的内存空间和事件循环,它们之间不能直接共享内存。因此,需要使用SendPort
和ReceivePort
来发送和接收消息。发送方通过SendPort
发送消息,接收方通过ReceivePort
接收消息。
短生命周期的Isolates通常用于执行一次性任务,如计算某个值或处理一次网络请求。这些Isolates在完成任务后会被销毁,以释放资源。使用短生命周期的Isolates可以提高应用程序的性能和响应性。
\\n在Flutter中将进程移动到隔离的最简单方法是使用isolate .run方法。此方法生成一个隔离,向生成的隔离传递一个回调以启动某些计算,从计算返回一个值,然后在计算完成时关闭隔离。这一切都与主隔离并发发生,并且不会阻塞它:
\\n与短生命周期的Isolates相比,有状态、长生命周期的Isolates通常用于执行需要持续运行的任务,如后台服务或定时任务。这些Isolates在创建后会一直运行,直到被显式销毁。它们可以维护自己的状态,并在需要时与其他Isolates进行通信。
\\nReceivePort
和SendPort
是Dart中用于Isolate间通信的两个关键类。ReceivePort
用于接收消息,而SendPort
用于发送消息。当创建一个新的Isolate时,会返回一个SendPort
对象,该对象可以用于向新Isolate发送消息。同时,新Isolate也会创建一个ReceivePort
对象来接收消息。
在Flutter中,平台插件允许应用程序与原生平台(如iOS和Android)进行交互。然而,在Isolates中使用平台插件是有限制的。由于Isolates是独立的执行环境,它们没有直接访问原生平台的能力。因此,通常需要在主Isolate中初始化平台插件,并通过消息传递机制将插件的功能暴露给其他Isolates。
\\n尽管Isolates提供了强大的并发处理能力,但它们也有一些限制:
\\nrootBundle
或dart:ui
方法。在Web平台上,Flutter使用Web Workers来实现Isolates的功能。Web Workers允许在后台线程中执行脚本,从而不会阻塞主线程。然而,由于Web Workers的限制,一些在Dart VM中可用的Isolate特性在Web平台上可能不可用。
\\n在Isolates中,无法直接访问rootBundle
或dart:ui
方法。rootBundle
通常用于加载应用程序的资源文件,而dart:ui
提供了与UI相关的功能。由于Isolates是独立的执行环境,它们没有访问这些资源的权限。因此,需要在主Isolate中加载资源或执行UI相关操作,并通过消息传递机制将结果传递给其他Isolates。
在Flutter中,从主机平台(如iOS或Android)到Flutter应用程序的插件消息传递是有限制的。这通常是由于安全原因和平台限制。为了确保应用程序的稳定性和安全性,Flutter对插件消息传递进行了严格的控制。因此,在开发过程中需要注意这些限制,并遵循最佳实践来确保消息传递的可靠性和安全性。
","description":"本文主要涉及Flutter 性能相关的概念,如渲染、包体积管理、懒加载、线程相关、以及并发操作使用的Dart隔离概念。 笔记1:介绍一下flutter\\n\\n笔记2-了解Flutter UI 页面和跳转\\n\\n笔记3- 常用 Widget 整理\\n\\n笔记4- dart 语法快速学习\\n\\n笔记5- dart 编码规范\\n\\n笔记6- 网络请求、序列化、平台通道介绍\\n\\n笔记7- 状态管理、数据持久化\\n\\n笔记8- package、插件、主题、国际化\\n\\n笔记9 - 架构、调试、打包部署\\n\\n笔记10- Widget 构建、渲染流程和原理、布局算法优化\\n\\n笔记11- 性能、渲染、包体积、懒加载、线程…","guid":"https://juejin.cn/post/7462566390845079586","author":"捡芝麻丢西瓜","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-22T09:43:14.012Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/64728af93679478395e947343871de85~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5o2h6Iqd6bq75Lii6KW_55Oc:q75.awebp?rk3s=f64ab15b&x-expires=1738147317&x-signature=6aKtqQ9Av2XC%2BpUT5CxpcRvGCC4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/791801465c914152aeeaa839cb84575c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5o2h6Iqd6bq75Lii6KW_55Oc:q75.awebp?rk3s=f64ab15b&x-expires=1738147317&x-signature=%2BDhbe%2FTJRqvsWqZS6j78W1ISxRw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/bbc526d77d5446d496473d556fc733ec~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5o2h6Iqd6bq75Lii6KW_55Oc:q75.awebp?rk3s=f64ab15b&x-expires=1738147317&x-signature=MgOJ5yYuLzP8U5ikF1T3AnaxLts%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f3dd6ed6c8d94e018c4a99c20486cb80~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5o2h6Iqd6bq75Lii6KW_55Oc:q75.awebp?rk3s=f64ab15b&x-expires=1738147317&x-signature=TDb%2BWGTnR%2FE%2FxzNtAZ2EnPD9Nvk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fbf2faf333424b1790c4d16b4701329b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5o2h6Iqd6bq75Lii6KW_55Oc:q75.awebp?rk3s=f64ab15b&x-expires=1738147317&x-signature=xuXQSEPLaq3hQgC8EQy8HPy17Gc%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["iOS","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"系统化掌握Dart编程之集合(Set)","url":"https://juejin.cn/post/7462280326913376265","content":"集合 —— 操作批量数据
的核心工具
在 Dart
中,Set
是一种特殊的集合类型,它确保每个元素都是唯一的,不允许重复。就像一个精心整理的工具箱
,每种工具只出现一次
,方便快速查找和使用。Set
适用于需要去重
、检查成员资格
或进行数学集合操作
(如并集
、交集
)的场景。通过 Set
,可以高效地管理和操作不重复的数据项,为应用程序带来简洁性
和高性能
。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\n姓名 | 参加的活动 |
---|---|
Alice | 短跑100米 |
Bob | 短跑200米 |
Charlie | 长跑5000米 |
如上图所示,有一个班级的学生名册,确保每个学生的名字只出现一次
,不论他们参加了多少个活动。这就是 Set
的工作方式 —— 它是一个无序且不允许重复元素的集合。
Set
是一个无序且不允许重复元素的集合。它类似于数学中的集合概念,提供了一种方便的方式来处理唯一
的数据项。
// 创建一个 Set 班级学生名册\\nSet<String> studentRoster = {\'Alice\', \'Bob\', \'Charlie\'};\\n
\\n一次
。// 尝试添加重复的名字\\nstudentRoster.add(\'Alice\'); // 不会添加,因为已经存在\\n\\n// 输出名册\\nprint(studentRoster); // 输出: {Alice, Bob, Charlie}\\n
\\nDart
的 Set
支持泛型,允许指定列表中元素的具体类型
,从而提高代码的类型安全性
和可读性
。
// 创建一个泛型为int类型的Set\\nSet<int> numbers = {1, 2, 3, 4, 5};\\n// 创建一个泛型为String类型的list\\nSet<String> names = {\'Alice\', \'Bob\', \'Charlie\'};\\n// 创建一个泛型为dynamic类型的list\\nSet<dynamic> list = {\'Alice\', 1, false};\\n
\\n最简单的方式是直接在花括号{}
中列出元素。
Set<int> numbers = {1, 2, 3, 4, 5};\\n
\\n使用 Set
类的构造函数来创建一个空集合
或具有初始元素的集合
。
// 创建一个空集合\\nSet<String> emptySet = <String>{};\\n\\n// 创建一个包含初始元素的集合\\nSet<int> initialNumbers = Set<int>.from([1, 2, 3, 4, 5]);\\n
\\n Set.of
构造函数Set.of
可以从任何可迭代对象(如 List
)创建一个 Set
。
Set<int> fromList = Set.of([1, 2, 3, 4, 5]);\\n
\\n由于 Set
本身是无序的,它并不支持通过索引访问元素(如 List
那样)。然而,Dart
提供了多种方式来访问 Set
中的元素,主要包括遍历
和查找特定元素
。下面对应的目录中会有介绍,在此不做过多叙述。
由于 Set
的特性,直接修改某个特定元素并不是像 List
那样简单,因为 Set
不支持通过索引访问和修改元素。可以通过以下几种方法来实现对 Set
中元素的“修改”
。
void main() {\\n Set<String> colors = {\'红色\', \'蓝色\', \'绿色\'};\\n \\n // 修改 \\"红色\\" 为 \\"粉红色\\"\\n if (colors.remove(\'红色\')) {\\n colors.add(\'粉红色\');\\n }\\n \\n print(colors); // 输出: {蓝色, 绿色, 粉红色}\\n}\\n
\\nmap
方法创建新的 Set
:可以使用 map
方法将每个元素映射到新值,然后将结果转换回 Set
。void main() {\\n Set<int> numbers = {1, 2, 3, 4, 5};\\n \\n // 将所有数字加 10\\n Set<int> updatedNumbers = numbers.map((number) => number + 10).toSet();\\n \\n print(updatedNumbers); // 输出: {11, 12, 13, 14, 15}\\n}\\n
\\nremoveWhere
和 addAll
)void main() {\\n Set<String> fruits = {\'苹果\', \'香蕉\', \'橙子\'};\\n \\n // 移除以 \\"苹\\" 开头的水果,并添加 \\"草莓\\"\\n fruits.removeWhere((fruit) => fruit.startsWith(\'苹\'));\\n fruits.add(\'草莓\');\\n \\n print(fruits); // 输出: {香蕉, 橙子, 草莓}\\n}\\n
\\n使用 add
或addAll
方法向 Set
中添加新元素。如果元素已经存在,则不会添加。
numbers.add(6);\\nprint(numbers); // 输出: {1, 2, 3, 4, 5, 6\\n\\n// 使用addAll批量添加元素\\nnumbers.addAll({7, 8, 9});\\nprint(numbers); // 输出: {1, 2, 3, 4, 5, 6, 7, 8, 9}\\n
\\nremove
方法。numbers.remove(5);\\nprint(numbers); // 输出: {1, 2, 3, 4, 6, 7, 8, 9}\\n
\\nclear
方法。numbers.clear();\\nprint(numbers); // 输出: {}\\n
\\nfor
循环如果确实需要按顺序访问元素,可以先将 Set
转换为 List
,然后通过索引访问。
Set<String> colors = {\'红色\', \'蓝色\', \'绿色\'};\\n List<String> colorList = colors.toList();\\n \\nfor (int i = 0; i < colorList.length; i++) {\\n print(\'颜色 ${i + 1}: ${colorList[i]}\');\\n}\\n
\\n注意:由于 Set
是无序的,转换后的 List
可能不会保持原来的插入顺序。
forEach
forEach
方法允许为 Set
中的每个元素执行一个回调函数。
Set<String> colors = {\'红色\', \'蓝色\', \'绿色\'};\\ncolors.forEach((color) => print(color));\\n
\\nfor-in
循环for-in
循环提供了一种简洁的方式遍历 Set
中的每个元素。
Set<int> numbers = {1, 2, 3, 4, 5};\\n \\n for (var number in numbers) {\\n print(number);\\n} \\n
\\n// length:获取 Set 的大小(元素数量)\\nprint(numbers.length); // 输出: 5\\n// isEmpty 和 isNotEmpty:检查 Set 是否为空。\\nprint(emptySet.isEmpty); // 输出: true\\n
\\n1、查找特定元素:
\\ncontains
方法检查某个元素是否存在于 Set
中。print(numbers.contains(5)); // 输出: true\\n
\\nwhere
方法与 first
、last
或 single
来查找符合条件的第一个或最后一个元素,或者唯一符合条件的元素。void main() {\\n Set<int> numbers = {1, 2, 3, 4, 5};\\n\\n // 查找第一个大于 3 的元素\\n int firstGreaterThanThree = numbers.where((number) => number > 3).first;\\n print(\'第一个大于 3 的数字是: $firstGreaterThanThree\');\\n\\n // 查找唯一等于 4 的元素\\n int onlyFour = numbers.singleWhere((number) => number == 4, orElse: () => -1);\\n print(\'唯一的数字 4 是: $onlyFour\');\\n}\\n
\\nany
和 every
方法。void main() {\\n Set<int> numbers = {1, 2, 3, 4, 5};\\n\\n bool hasEvenNumber = numbers.any((number) => number % 2 == 0);\\n print(\'Set 中是否有偶数: $hasEvenNumber\');\\n\\n bool allPositive = numbers.every((number) => number > 0);\\n print(\'所有数字是否都是正数: $allPositive\');\\n}\\n
\\n2、集合操作:
\\nunion
方法计算两个 Set
的并集。Set<int> setA = {1, 2, 3};\\nSet<int> setB = {3, 4, 5};\\nSet<int> unionSet = setA.union(setB);\\nprint(unionSet); // 输出: {1, 2, 3, 4, 5}\\n
\\nintersection
方法计算两个 Set
的交集。Set<int> intersectionSet = setA.intersection(setB);\\nprint(intersectionSet); // 输出: {3}\\n
\\ndifference
方法计算两个 Set
的差集。Set<int> differenceSet = setA.difference(setB);\\nprint(differenceSet); // 输出: {1, 2}\\n
\\nSet
(Set.unmodifiable
)有时需要确保一个 Set
不会被修改。可以使用 Set.unmodifiable
来创建一个不可变的 Set
。
Set<int> immutableNumbers = Set.unmodifiable({1, 2, 3, 4, 5});\\n\\n// 下面这行代码会抛出异常,因为 immutableNumbers 是不可变的\\n// immutableNumbers.add(6);\\n
\\nSet
提供了一种强大而灵活的方式来处理唯一
的数据项。通过合理利用 Set
的特性,可以编写出更加简洁
、高效和易于维护
的代码。
\\n","description":"前言 集合 —— 操作批量数据的核心工具\\n\\n在 Dart中,Set 是一种特殊的集合类型,它确保每个元素都是唯一的,不允许重复。就像一个精心整理的工具箱,每种工具只出现一次,方便快速查找和使用。Set 适用于需要去重、检查成员资格或进行数学集合操作(如并集、交集)的场景。通过 Set,可以高效地管理和操作不重复的数据项,为应用程序带来简洁性和高性能。\\n\\n操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。\\n\\n一、基本概念\\n1.1、图像表示\\n姓名\\t参加的活动Alice\\t短跑100米\\nBob\\t短跑200米\\nCharlie\\t长跑…","guid":"https://juejin.cn/post/7462280326913376265","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-22T01:07:04.218Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e3035e0ac60a48e3892351174aba0be4~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1738112823&x-signature=GzA28Hm1Fc3nhoKpebafbF4TTfU%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart","前端"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 设计百科全书 - 主题、样式、颜色篇","url":"https://juejin.cn/post/7462390806582935561","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
这篇文章的部分内容我从来没有在互联网上看到过
\\n(至少中文互联网没有 > 😂)
\\n(至少百度搜不到 > 😂)
\\n\\n点名批评
\\n
\\n”谁会在意这个,这个东西(颜色)没人注意到,就这样吧“
\\n”设计师(实习生)怎么设计,我就怎么做,最后不好看跟我前端没关系“
颜色 Color 颜色的设计
\\n\\n\\n阴影的设计\\n阴影的设计
\\n
主题 Theme 主题设计
\\n\\n\\nBrightness\\nColorScheme\\nThemeExtension
\\n
样式 Style
\\n\\n\\nButtonStyle
\\n
\\n\\n其他组件的Style
\\n
\\n\\nTextStyle
\\n
\\n\\nTextSize 、 Color 、 TextStyle 冲突的问题
\\n
\\n\\nGetX 中如何使用Theme Style?
\\n
\\n\\n一个支持黑白主题的程序完整源码
\\n
虽然这篇文章主要讲的是主题样式,但是颜色却是最先要注意的地方。
\\n前面2个按钮的问题显而易见,为什么第三个按钮也有问题?
\\n问题就是,不应该用黄色作为按钮的背景色。
看看谷歌设计的最早的阴影设计,
\\n还有目前MD3的 Card(Flutter 自带组件)卡片质感,
\\n猜猜看,为什么大家拒绝使用谷歌的规范,还不是这东西真太low了。
\\n不是瞎吹,国内设计圈虽然起步慢,抄袭也多,但是不得不说,
\\n对比欧美的 大圆角设计,真的好太多了。
\\n\\n咱们是顶尖的上限不够高,但是平均水平比国外强太多
\\n
弱化阴影的设计,让你的阴影没那么突兀,
\\n可以将投影的颜色设置为无限接近于#FFFFFFFF,
\\n常见参考值 可以使用 EE,FE,EF 等,235-245 区间。
\\n范围可以控制在 5-10px像素
BoxDecoration(\\n color: Colors.white,\\n borderRadius: BorderRadius.circular(10),\\n boxShadow: const [\\n BoxShadow(\\n color: Color(0xFFEFEFEF),\\n blurRadius: 5,\\n blurStyle: BlurStyle.outer,\\n ),\\n ],\\n ),\\n
\\n越来越多的设计 使用“无边界”,“无卡片”,“无阴影” 的设计。
\\n这种设计 非常的简洁,非常考验 布局能力。
\\n做法就是 使用背景色和卡片 色差,产生阶梯感。
backgroundColor: const Color(0xFFFAFAFA), //背景白色偏灰\\ncolor: Colors.white, // 卡片白色\\n
\\n谷歌的MD设计其实只考虑了少部分的按钮和配色,
\\n但是他们没想到,国内的设计师,设计程序时,
\\n能搞出10几种按钮 和 文字 大小颜色 😂
\\n谷歌都气炸了。
所以在中文互联网上,你搜索Flutter 主题时,这块的内容多数是复制粘贴党,
\\n并不是批评 到处复制粘贴的人,只是这种行为 让真正有独特理解的内容 更难被人看到。
在MaterialApp下面有常用的三个主题相关字段,
\\ntheme:白色主题
\\ndarkTheme:黑色主题
\\nthemeMode:主题模式
enum ThemeMode {\\n system,// 跟随系统变化\\n light,// 强制白色主题\\n dark,// 强制黑色主题\\n}\\n
\\n初始代码默认只设置了theme,不设置darkTheme,任何时候都不能切换到黑色模式,始终会显示白色主题。
\\n当设置为system模式时,系统主题切换,App会跟随系统变化,这是一个比较优秀的做法。
MaterialApp的theme,是ThemeData,其中的ColorScheme,是Flutter 中的颜色配置信息,\\n默认拥有ColorScheme.light,ColorScheme.dark,ColorScheme.fromSeed,三种快速配置,比较适合不在意颜色的APP快速使用。
\\n\\n\\n推荐白色主题使用 ColorScheme.light
\\n
\\n推荐黑色主题使用 ColorScheme.dark
enum Brightness {\\n /// 例如,颜色可能是深灰色,需要白色文本。\\n dark,\\n /// 例如,颜色可能是亮白色,需要黑色文本\\n light,\\n}\\n
\\nFlutter中黑白相关的区分,都可以使用Brightness,\\n但是需要注意的是,有些地方Brightness可能是相反并且反直觉的。\\n比如在系统顶部状态栏中的Brightness和IconBrightness就是一个容易搞混的设置
\\nSystemUiOverlayStyle(\\n statusBarBrightness: Brightness.light,//状态栏的主题\\n statusBarIconBrightness: Brightness.dark,//状态栏的图标主题,需要跟状态栏主题相反\\n)\\n
\\n默认的颜色设计,如果严格按照这种规范来,
\\n程序中 只会有 几种颜色。
Flutter 颜色设置名称非常多 非常难以 运用。
\\n\\n\\n如果你的程序颜色配置较少可以使用上面的关键字。\\n如果你的程序颜色配置较多,推荐使用ThemeData 的 extensions属性,类型为 ThemeExtension
\\n
我们先来讲默认的颜色配置,如果想看扩展主题,请跳到下方扩展主题章节。
\\n我们来设计一套兼容黑白主题的主题吧,这是一个最简单的案例。
我们先给MaterialApp定义一个白色主题theme.
\\n如果我们定义appBarTheme,代表App会在白色主题下 使用这个Appbar的主题设置。
\\n同样的,如果我们在darkTheme中定义appBarTheme,那么黑色主题会使用它。
return MaterialApp(\\n title: \'Flutter Demo\',\\n theme: ThemeData(\\n appBarTheme: const AppBarTheme(\\n backgroundColor: Colors.white,\\n foregroundColor: Colors.black,\\n elevation: 0,\\n scrolledUnderElevation: 0,\\n surfaceTintColor: Colors.black,\\n systemOverlayStyle: SystemUiOverlayStyle(\\n statusBarColor: Colors.transparent,\\n statusBarBrightness: Brightness.light,\\n statusBarIconBrightness: Brightness.dark,\\n systemNavigationBarColor: Colors.white,\\n systemNavigationBarDividerColor: Colors.black,\\n ),\\n ),\\n
\\n接着是颜色配置
\\ncolorScheme: const ColorScheme(\\n brightness: Brightness.dark,\\n primary: AppColors.themeColor, // 主要颜色,一般是按钮上面会用\\n onPrimary: AppColors.themeTextColor, // 在主要颜色 上面,就是文字的颜色,我们用白色\\n secondary: Colors.grey, // 次要颜色,用灰色\\n onSecondary: Colors.white,// 在次要颜色 上面,就是文字的颜色,我们用白色色\\n error: Colors.red,//错误颜色\\n onError: Colors.white,// 错误颜色上面的文字颜色\\n surface: Colors.black, // 表面颜色\\n onSurface: Colors.white,// 表面上的文字\\n),\\n
\\n\\n\\n主题和Style不建议使用静态字段(将它们写到默认Theme或 ThemeExtension 扩展内部),但是颜色和数值类型可以考虑使用 静态字段,在 theme 和 style中引用这些静态变量达到复用的目的,比如
\\n
class AppColors {\\n /// 主题色\\n static const Color themeColor = Color(0xff00CC74);\\n\\n /// 文字颜色\\n static const Color themeTextColor = Color(0xff5FB23F);\\n}\\n\\nclass AppSizes {\\n /// 按钮大小\\n static const int buttonSize = 48;\\n}\\n
\\n到这里,已经完成了颜色配置,flutter默认的组件style都会从里面取出颜色使用
\\n /// 按钮会自动使用主题色,除非你定义了ButtonTheme中的ButtonStyle并修改了背景色\\n FilledButton(\\n onPressed: () {},\\n child: const Text(\\"FilledButton\\"),\\n ),\\n Switch(\\n value: false,\\n onChanged: (value) {},\\n ),\\n Row(\\n children: [\\n const Chip(\\n label: Text(\\"Chip\\"),\\n ),\\n const ChoiceChip(label: Text(\\"ChoiceChip\\"), selected: true),\\n Radio(\\n value: 1,\\n groupValue: 1,\\n onChanged: (value) {},\\n )\\n ],\\n )\\n
\\n效果如下(图片和上方颜色代码不一致,仅供参考):
\\n当然我们也可以直接取到这个颜色值使用。
\\n/// 获取默认ThemeData下面的主题色\\nTheme.of(context).colorScheme.primary\\n\\n/// 直接使用默认 Theme中的颜色\\n/// 当前 我建议你使用textTheme,而不是直接使用颜色值,具体内容查看本章内容\\nbody: Text(\\n \\"body\\",\\n style: TextStyle(\\n color: Theme.of(context).colorScheme.primary,\\n ),\\n )\\n
\\n\\n\\n如何使用主题的颜色完成自定义样式的组件呢? -> 具体使用方法查看本章Style模块,
\\n
从上面的内容上你也看出来了,如果你的程序 颜色 非常单一,谷歌这套设计也是能够满足需求的,
\\n可惜国内的设计环境就不是这样,各种颜色,各种尺寸,
\\n这里有3种方案,
第一种是 跟设计师商量,减少 部分颜色 和 样式设计,让最终的按钮大概就是 2-3种 ,文字颜色 也是 2-3 种。
\\n第二种是 全局静态Style
\\n在静态类中,写固定函数,返回固定组件的样式和颜色,比如定义一个 BlackButton,在函数中申明它的背景色为黑色。这种做法它的缺陷就是无法跟随主题变化,如果你的程序只有一种主题,那么它也是一种可行的方案。
第三种是 使用扩展 ThemeExtension,查看下方。
\\n/// 自定义的主题扩展\\nclass DiyTheme extends ThemeExtension<DiyTheme> {\\n const DiyTheme({\\n required this.brandColor,\\n required this.danger,\\n required this.dangerTextStyle,\\n });\\n\\n final Color? brandColor;\\n final Color? danger;\\n final TextStyle? dangerTextStyle;\\n\\n @override\\n DiyTheme copyWith({Color? brandColor, Color? danger}) {\\n return DiyTheme(\\n brandColor: brandColor ?? this.brandColor,\\n danger: danger ?? this.danger,\\n dangerTextStyle : dangerTextStyle ?? this.dangerTextStyle,\\n );\\n }\\n\\n @override\\n DiyTheme lerp(DiyTheme? other, double t) {\\n if (other is! DiyTheme) {\\n return this;\\n }\\n return DiyTheme(\\n brandColor: Color.lerp(brandColor, other.brandColor, t),\\n danger: Color.lerp(danger, other.danger, t),\\n dangerTextStyle : *** 省略,\\n );\\n }\\n\\n @override\\n String toString() => \'DiyTheme(brandColor: $brandColor, danger: $danger)\';\\n}\\n\\n//创建方式\\ntheme: ThemeData.light().copyWith(\\n extensions: <ThemeExtension<dynamic>>[\\n const DiyTheme(\\n brandColor: Color(0xFF1E88E5),\\n danger: Color(0xFFE53935),\\n dangerTextStyle : *** 省略\\n ),\\n ],\\n ),\\n\\n// 使用扩展颜色的方式\\nfinal DiyTheme diy = Theme.of(context).extension<DiyTheme>()!;\\nColor brandColor = diy.brandColor;\\n\\n// 使用扩展主题中的样式\\nText(\\n \\"自定义样式\\",\\n style: diy.dangerTextStyle,\\n),\\n
\\n区分一下 Theme 和 Style
\\n在Flutter中,主题是一些列Style的组合。
\\n所以 并不是换一个颜色 就是一个主题换肤(😀)。
\\n\\n让Theme获取颜色和属性配置,
\\n
\\n\\n从Theme中获取Style给组件。
\\n
在Flutter的主题设计中,你需要为最常用的按钮风格设置默认的主题,
\\n总不可能 每写一个按钮 是使用 DiyTheme.buttonStyle吧?这样多麻烦不是吗?
FilledButton(\\n style: Theme.of(context).extension<DiyTheme>()!.buttonStyle,\\n onPressed: () {},\\n child: Text(\\"按钮\\"),\\n )\\n
\\n如果所有的按钮都像上面这样写,开发者会疯掉,
\\n我这里分享我推荐的做法,它不一定是最优,但是是我目前想到的比较好的解决方案。
比如:
\\n// 亮色 主题\\n theme: ThemeData(\\n colorScheme: const ColorScheme.light(),\\n appBarTheme: const AppBarTheme(),\\n textButtonTheme: TextButtonThemeData(\\n style: ButtonStyle(\\n splashFactory: NoSplash.splashFactory,\\n overlayColor: WidgetStatePropertyAll(Colors.transparent),\\n foregroundColor: WidgetStateProperty.resolveWith((state) {\\n if (state.contains(WidgetState.disabled)) {\\n return Colors.black26;\\n }\\n return AppColors.themeColor;\\n }),\\n ),\\n ),\\n filledButtonTheme: FilledButtonThemeData(\\n style: ButtonStyle(\\n shape: WidgetStatePropertyAll(\\n RoundedRectangleBorder(borderRadius: 8.borderRadius),\\n ),\\n minimumSize: const WidgetStatePropertyAll(Size.fromHeight(60)),\\n backgroundColor: WidgetStateProperty.resolveWith((state) {\\n if (state.contains(WidgetState.disabled)) {\\n return AppColors.themeColor.withAlpha(80);\\n }\\n return AppColors.themeColor;\\n }),\\n foregroundColor: const WidgetStatePropertyAll(Colors.white),\\n ),\\n ),\\n ),\\n
\\n上面的代码中,我们为 常用的2种按钮定义了外观,在我们日常使用中,
\\n直接使用按钮 即可获得对应的外观
TextButton(\\n onPressed: () {},\\n child: Text(\\"TextButton\\"),\\n),\\nFilledButton(\\n onPressed: () {},\\n child: Text(\\"FilledButton\\"),\\n),\\n
\\n如果目前的按钮类型, 不足以满足你App的按钮需求,再根据对应的特性,将它们 扩展到相应按钮Style上,写入ThemeExtension中,参考上方ThemeExtension方法,使用时稍微麻烦一些,但是总体来说这不失为一种合理高效的方案。
\\nFilledButton(\\n style: Theme.of(context).extension<DiyTheme>()!.buttonStyle,\\n onPressed: () {},\\n child: Text(\\"FilledButton\\"),\\n),\\n
\\nbuttonStyle 这个名字可以自己取,当然,需要和你团队定义规范,你们需要在各自使用的时候,从theme中提取样式使用,而不是自己写到行内或者自己单独定义到文件中。
\\n\\n\\n你需要跟你的开发同伴 一起定义规范,大家都遵守这种规范。
\\n
\\n\\n想起了上次我给同事说要做这个事情,结果被他怼,”你想怎么写就怎么写,咱们各写各的,我们之前都这样的“,我竟无言以对。
\\n
//输入框参考:\\n theme: ThemeData(\\n inputDecorationTheme: InputDecorationTheme(\\n filled: true,\\n fillColor: const Color(0xFFEEEEEE),\\n hintStyle: const TextStyle(\\n height: 1,\\n fontSize: 18,\\n wordSpacing: 1,\\n letterSpacing: 1,\\n fontWeight: FontWeight.normal,\\n color: Color(0xFF999999),\\n ),\\n border: OutlineInputBorder(\\n borderRadius: 8.borderRadius,\\n borderSide: BorderSide.none,\\n ),\\n enabledBorder: OutlineInputBorder(\\n borderRadius: 8.borderRadius,\\n borderSide: BorderSide.none,\\n ),\\n prefixIconConstraints: const BoxConstraints(\\n minWidth: 40,\\n ),\\n suffixIconConstraints: const BoxConstraints(\\n minWidth: 50,\\n ),\\n ),\\n ),\\n\\n //Tabbar参考:\\n tabBarTheme: TabBarTheme(\\n dividerHeight: 0,\\n splashFactory: NoSplash.splashFactory,\\n overlayColor: WidgetStatePropertyAll(Colors.transparent),\\n labelStyle: TextStyle(fontWeight: FontWeight.w600),\\n //unselectedLabelStyle: TextStyle(),\\n ), \\n
\\n上面都是默认的主题样式,当一种主题样式不够使用时,将其他样式写入扩展类型中。
\\n\\n\\n几乎所有的组件都支持Style编写!!!
\\n
Flutter 中的TextStyle 可以说是最难理解和使用的参数了,如果说colorScheme复杂,至少你还能大概知道它会在什么地方出现,但是TextStyle,那就得一个个的尝试才知道它最终会在什么地方使用到,下面的内容可以收藏一波,基本上每一个项目可能都会用到这部分的文档内容,因为它太难记了!
\\nTextTheme(\\n /// 最大的显示样式 ,作为屏幕上最大的文本 重要的文字或数字。\\n displayLarge: TextStyle(),\\n displayMedium: TextStyle(),\\n displaySmall: TextStyle(),\\n \\n /// 标题样式 (小于display)\\n headlineLarge: TextStyle(),\\n headlineMedium: TextStyle(),\\n headlineSmall: TextStyle(),\\n \\n /// 标题样式(小于headline)\\n titleLarge: TextStyle(), // AppBar Title (默认字体)\\n titleMedium: TextStyle(),\\n titleSmall: TextStyle(),\\n \\n /// 正文样式\\n bodyLarge: TextStyle(), // 大 - 输入框 (默认字体)\\n bodyMedium: TextStyle(), // 字体 默认\\n bodySmall: TextStyle(), // 小 (部分水印提示信息)\\n \\n /// 标签样式 标签样式是较小的实用样式,用于UI区域 例如组件内部的文本 或 内容正文,如标题。\\n labelLarge: TextStyle(), // 按钮 (默认字体)\\n labelSmall: TextStyle(),\\n labelMedium: TextStyle(),\\n)\\n
\\n同样的,为了简单,设计师 应该尽量将 字体的尺寸设计为 上方 15种之一,这样我们就方便使用了(想太多。。。哈哈) 😀
\\n当然如果上面的文字样式 使用起来过于复杂难以记忆,我们依然可以将textStyle放入扩展Theme,使用自定义的名字,就会简单很多。
\\n但是我们遇到修改 某些 组件的字体样式时,先看看默认的textTheme是否能够很好的修改到它(这么做优先级比自定义高)。
关于textTheme 的一些使用案例:
\\n/// 特定的区域中使用时,不需要申明对应的Style\\n\\nappBar: AppBar(\\n title: Text(\\"appBar\\"),// 自动使用textTheme.titleLarge\\n),\\n\\nbody: Text(\\"body\\"), // 自动使用textTheme.bodyMedium\\n\\n/// 你想使用更大字体时样式时 \\nbody: Text(\\n \\"body\\",\\n style: Theme.of(context).textTheme.bodyLarge, // 申明使用textTheme.bodyLarge\\n),\\n
\\n仔细阅读之前的文章,再经过思考后,你就会发现,我们定义了textTheme,ButtonTheme又可以定义\\ntextStyle,这个时候不是冲突了吗?
\\n其实的确有冲突,但是Flutter讲的是就近原则,也就是说 按钮内的text在寻找样式时,如果你写了行内Style,它直接就用了,如果没有,那它会先寻找buttonTheme中的textStyle,如果没有定义,它才会去找textTheme中对应的textStyle,textStyle如果你没有修改,它就用的初始值,整体理解起来也是非常容易。
\\n所以,在按钮的主题Style中,定义文字颜色时,应该放到foregroundColor,而不是定义TextStyle中的Color
\\n你没有在按钮中定义文字大小,它也会去textTheme中获取 默认的文字大小。
\\n\\n\\n不建议在textTheme(或扩展Theme)之外的区域定义文字大小和颜色属性
\\n
在Getx中,你可以通过Get快速访问theme对象。
\\n\\n\\nGet.theme.colorScheme
\\n
\\n\\nGet.theme.extensions
\\n
需要注意的是,如果你使用system模式的主题模式,系统主题发生变化时,Get使用的context并不能从当前的主题变化中获得消息,导致界面不会重新渲染(猜测的😔)。
\\n所以建议使用BuildContext context来进行theme获取,这样你可能需要在页面的函数之间传递这个context 或 传递 themeData,如果读到这里,你有更好的方案,请在评论区留言讨论。
\\n(有可能你在阅读本文章时,这个源码还没有编写完成,但是你可以先关注一波。)\\ngithub.com/DMSkin/flut…
","description":"Flutter 设计百科全书 主题、样式、颜色篇\\n前言\\n\\n这篇文章的部分内容我从来没有在互联网上看到过\\n (至少中文互联网没有 > 😂)\\n (至少百度搜不到 > 😂)\\n\\n点名批评\\n ”谁会在意这个,这个东西(颜色)没人注意到,就这样吧“\\n ”设计师(实习生)怎么设计,我就怎么做,最后不好看跟我前端没关系“\\n\\n本章内容\\n\\n颜色 Color 颜色的设计\\n\\n阴影的设计 阴影的设计\\n\\n主题 Theme 主题设计\\n\\nBrightness ColorScheme ThemeExtension\\n\\n样式 Style\\n\\nButtonStyle\\n\\n其他组件的Style…","guid":"https://juejin.cn/post/7462390806582935561","author":"DreamMachine","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-21T18:15:39.957Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3414c7d7218349b99315c6f0421f43db~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRHJlYW1NYWNoaW5l:q75.awebp?rk3s=f64ab15b&x-expires=1738088138&x-signature=Q85BHihH5Pekuwr1MfOuauWQFrM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1669d7cf7dd542fc8861214956403a1a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRHJlYW1NYWNoaW5l:q75.awebp?rk3s=f64ab15b&x-expires=1738088138&x-signature=l9aPICqc%2BJlXC2FiP12jyBnStvw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/81650d6a9c39489c92c4438e851831d6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRHJlYW1NYWNoaW5l:q75.awebp?rk3s=f64ab15b&x-expires=1738088138&x-signature=6pR%2Bv7xq7w8Pi7U3rpapOw2LLfI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/08f64de2efd848b88c20c250a251a051~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRHJlYW1NYWNoaW5l:q75.awebp?rk3s=f64ab15b&x-expires=1738088138&x-signature=aPVIy0QoNQCu88E23b%2FQqx8CP1U%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1bda2090dff6477aade5d20b7f14a842~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRHJlYW1NYWNoaW5l:q75.awebp?rk3s=f64ab15b&x-expires=1738088138&x-signature=oDPYa19EobFXJvt7v5Hz8xYnlQY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d261456e79574ed2bea8ec6019afba31~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRHJlYW1NYWNoaW5l:q75.awebp?rk3s=f64ab15b&x-expires=1738088138&x-signature=U%2BUECGlB9rWXFfDjtSTKe6fTTIY%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["前端","Flutter","设计"],"attachments":null,"extra":null,"language":null},{"title":"flutter自学笔记10- Widget 构建、渲染流程和原理、布局算法优化","url":"https://juejin.cn/post/7462202297000460303","content":"本文梳理了从 Widget 基本构建流程、平台差异、渲染流程和机制、布局和算法的优化等内容
\\n层级/组件 | 描述 | 涉及技术/语言 |
---|---|---|
底层操作系统 | - Flutter应用与底层OS交互的接口 | - |
嵌入层 | 提供程序入口,协调与OS的服务,管理事件循环队列 | |
- Flutter代码可以集成到现有应用或作为主体 | - | |
Flutter引擎 | - Flutter核心,使用C++编写 | C++ |
功能 | - 栅格化场景,提供核心API的底层实现(当需要绘制新一帧的内容时,引擎将负责对需要合成的场景进行栅格化) | |
- 图形(在 iOS 和 Android 上通过 Impeller,在其他平台上通过 Skia),文本布局,文件及网络IO,辅助功能,插件架构 | ||
- Dart运行环境及编译环境的工具链 | ||
dart:ui | - 引擎将底层 C++ 代码包装成 Dart 代码,通过 dart:ui 暴露给 Flutter 框架层 | |
Flutter框架层 | - 开发者交互层,现代响应式框架,使用Dart编写 | Dart |
foundational | - 基础类及构建块服务,如animation、painting、gestures | |
渲染层 | - 提供操作布局的抽象,构建渲染对象树 | |
widget层 | - 组合抽象,每个渲染对象对应widgets层的一个类,响应式编程模型 | |
Material和Cupertino库 | - 提供Material和iOS设计规范的widgets组合 | |
附加软件包 | - 更高层级功能,拆分为不同软件包 | Dart, Flutter核心库,平台插件,与平台无关的功能,生态系统中的软件包。【其中包括平台插件,例如 camera 和 webview;与平台无关的功能,例如 characters、 http 和 animations。还有一些软件包来自于更为宽泛的生态系统中,例如 应用内支付、 Apple 认证 和 Lottie 动画。】 |
通过 flutter create
命令创建的应用的结构概览:
组件 | 描述 | 备注 |
---|---|---|
Dart 应用 | ||
widget 合成 | 将 widget 合成预期的 UI | 由应用开发者进行管理 |
业务实现 | 实现对应的业务逻辑 | 由应用开发者进行管理 |
框架(源代码) | ||
API 封装 | 提供上层的 API 封装,用于构建高质量的应用(例如 widget、触摸检测、手势竞技、无障碍和文字输入) | |
Scene 构建 | 将应用的 widget 树构建至一个 Scene 中 | |
引擎(源代码) | ||
Scene 栅格化 | 将已经合成的 Scene 进行栅格化 | |
核心 API 封装 | 对 Flutter 的核心 API 进行了底层封装(例如图形图像、文本布局和 Dart 的运行时) | |
dart:ui API | 将其功能通过 dart:ui API 暴露给框架 | |
嵌入层 API | 使用嵌入层 API 与平台进行整合 | |
嵌入层(源代码) | ||
操作系统服务协调 | 协调底层操作系统的服务,例如渲染层、无障碍和输入 | |
事件循环管理 | 管理事件循环体系 | |
平台 API 暴露 | 将特定平台的 API 暴露给应用集成嵌入层 | |
运行器 | ||
应用包合成 | 将嵌入层暴露的平台 API 合成为目标平台可以运行的应用包 | 部分内容由 flutter create 生成,由应用开发者进行管理 |
Flutter应用会在一个VM(程序虚拟机)中运行,这一机制是Flutter开发框架的一个重要组成部分。以下是对这一现象的详细解释:
\\nFlutter是一款移动应用程序跨平台框架,它允许开发者使用Dart语言编写代码,然后生成高性能、高保真的iOS和Android应用程序。在这个过程中,Flutter应用会在一个虚拟机(VM)中运行。这个VM提供了有状态的热重新加载功能,使得开发者可以在不完全重新编译应用的情况下,快速预览代码更改的效果。
\\nFlutter应用在一个VM中运行是其开发框架的一个重要组成部分。这个VM提供了强大的语言支持、性能优化和跨平台能力,使得Flutter应用能够在多种设备和操作系统上高效、一致地运行。同时,VM中的热刷新功能也大大提高了开发效率。
\\n在 Flutter 里,widgets(类似于 React 中的组件)是用来配置对象树的不可变类,每个 widget 都是一部分不可变的 UI 声明。
\\n这些 widgets 会管理单独的布局对象树,接着参与管理合成的布局对象树。
\\nFlutter 的核心就是一套高效的遍历树的变动的机制,它会将对象树转换为更底层的对象树,并在树与树之间传递更改。
\\nFlutter 拥有其自己的 UI 控制实现,而不是由系统自带的方法进行托管:
\\nWidget可以表示屏幕上的绘制、布局(位置和大小)、用户交互、状态管理、主题、动画及导航等
\\n比如:
\\n动画层:Animation和
Tween
渲染层:RenderObject
用来描述布局、绘制、触摸判断及可访问性
没有视觉内容功能:包含了布局、绘制、定位和大小的功能的Container 是由 LimitedBox
、 ConstrainedBox
、 Align
、 Padding
、 DecoratedBox
和 Transform
组成的
通过重写 build()
方法,返回一个新的元素树定义视觉UI
build()
是将状态转化为 UI 的方法,widget 通过重写该方法来声明 UI 的构造:
UI = f(state)\\n
\\nbuild()
方法在框架需要时都可以被调用(每个渲染帧可能会调用一次),从设计角度来看,它应当能够快速执行且没有额外影响的。
这样的实现设计依赖于语言的运行时特征(特别是对象的快速实例化和清除)。幸运的是,Dart 非常适合这份工作。
\\n每个渲染帧,Flutter 都可以根据变化的状态,调用 build()
方法重建部分 UI,确保build 方法轻量且能快速返回 widget 是非常关键的,繁重的计算工作应该通过一些异步方法完成,并存储在状态中,在 build 方法中使用
InheritedWidget:
\\n通过 build()
方法可以确保子 widget 使用其所需的数据进行实例化,随着 widget 树层级逐渐加深,依赖树形结构上下传递状态信息会变得十分麻烦。这时需要用到InheritedWidget。
主题:主题是典型的状态共享示例,调用 of(context)
会根据当前构建的上下文(即当前 widget 位置的句柄)逐级向上一级遍历直到找到对应的状态:
Container(\\n color: Theme.of(context).secondaryHeaderColor,\\n child: Text(\\n \'Text with a background color\',\\n style: Theme.of(context).textTheme.titleLarge,\\n ),\\n);\\n
\\n更高级的InheritedWidget封装 provider 用于状态管理更方便。
\\n示例代码:
\\nContainer(\\n color: Colors.yellow,\\n child: Row(\\n children: [\\n Image.network(\'xxx.png\'),\\n const Text(\'风景图\'),\\n ],\\n ),\\n);\\n
\\n代码绘制流程:
\\n1、调用 build()
方法构建 widget 子树(返回一棵基于当前应用状态来绘制 UI 的 widget 子树)
2、插入节点 Container 的 Element
\\n3、判断背景 color
属性不为空,ColoredBox
(处理颜色)会被加入
4、插入节点Row (对应child)
\\n5、开始处理child的子树 Image和 Text
\\n7、插入子节点 RawImage
和 RichText
(分别对应Image
和 Text
)
整个流程(widget 子树)如下图左:
\\n其中左图:
\\n1、蓝色实线圆:表示 UI 生命周期 的Element 宿主(用户可见)
\\n2、灰色虚线圆:参与布局或绘制阶段的Element(用户不可见)
\\n而图左的本质是图右:包含 ComponentElement和RenderObjectElement
\\n1、ComponentElement
\\n2、RenderObjectElement
\\nFlutter中的大部分widget都是通过继承自RenderBox
的类来渲染的。RenderBox
是Flutter渲染树中的一个基础类,它提供了一个盒子模型(box model),这个模型定义了widget在二维笛卡尔空间(即屏幕)中的位置和大小。
RenderBox 和 盒子模型
\\nRenderBox
提供了一个简单的盒子模型,其中每个盒子(即widget的渲染表示)都有一个位置、大小以及可选的边距(margin)、边框(border)、内边距(padding)和内容区域。这个模型与Web开发中常用的盒子模型非常相似。RenderBox
都有一个父坐标系中的位置(通常是通过其左上角的坐标来定义的)和一个固定的大小(宽度和高度)。这些属性共同决定了盒子在屏幕上的可见区域。RenderBox
还允许为其关联的widget设置最小和最大的宽度和高度约束。这些约束在布局过程中非常重要,因为它们告诉父widget如何调整子widget的大小以满足布局要求。在Flutter中,widget、Element和RenderObject这三个核心概念各自扮演着不同的角色,但它们共同构建了一个灵活的UI框架,允许开发者以高度定制化的方式创建用户界面。
\\nWidget是Flutter中的UI构建块,它们描述了UI的结构和外观。大多数widget都有一个或多个子widget,这些子widget通过child或children属性暴露出来。Widget本身并不直接参与布局和渲染,而是作为UI蓝图存在。
\\nElement是widget在Flutter框架中的实例化表示。当widget树被构建时,每个widget都会对应一个Element。Element负责在运行时管理widget的状态和生命周期。虽然开发者通常不直接与Element打交道,但它们是Flutter框架内部实现UI更新和状态管理的重要部分。
\\nRenderObject是负责布局和渲染的具体实现类。它们与Element相关联,但直接与底层的渲染引擎交互。RenderObject定义了如何在屏幕上绘制UI元素以及它们如何相互布局。
\\nFlutter允许每个RenderObject对适用于该对象的子模型进行定制化。这意味着不同的RenderObject可以有不同的方式来管理它们的子节点。例如,RenderTable使用二维数组来存储子节点,以适应表格布局的需求。
\\nRenderParagraph是一个特殊的RenderObject,它的子节点是TextSpan对象,而不是其他RenderObject。这意味着在RenderParagraph的边界内,RenderObject树被转换为TextSpan树。这种情况展示了Flutter框架的灵活性,允许开发者以最适合特定UI元素的方式来组织子节点。
\\nFlutter还提供了一些琐碎的widget,如Expanded、SizedBox和Visibility,它们封装了常见的UI模式,使开发者能够更容易地实现特定的UI效果。这些widget的存在简化了开发过程,让开发者能够更快地找到并解决问题。
\\n在Flutter中,RenderObject树和Element树是同构的,但RenderObject树实际上是Element树的一个子集。这意味着每一个RenderObject在Element树中都有一个对应的Element节点,但并非每一个Element节点都会有一个对应的RenderObject。这种设计允许Flutter在处理布局和绘制时更加灵活和高效。
\\n分离的好处:
\\n在Flutter中,RenderObject
树的根节点是RenderView
。这个根节点代表了整个渲染树的输出,它是连接Flutter框架和底层渲染引擎(如Skia)的桥梁。RenderView
负责协调整个渲染过程,确保每一帧的内容都能够正确地显示在屏幕上。
当平台需要渲染新的一帧内容时,这通常是由一些外部事件触发的,比如垂直同步信号(vsync)或者纹理的更新完成。在这些事件发生时,Flutter框架会调用RenderView
的compositeFrame()
方法。
compositeFrame()
方法是渲染过程的核心,它负责创建一个SceneBuilder
对象。SceneBuilder
是一个辅助类,它用于构建和描述当前要渲染的场景(即一帧的内容)。在这个过程中,SceneBuilder
会遍历整个RenderObject
树,收集所有需要渲染的信息,比如各个widget的位置、大小、颜色、纹理等。
一旦SceneBuilder
完成了对当前场景的描述,它就会将这些信息传递给dart:ui
库中的Window.render()
方法。Window
是Flutter与底层操作系统和硬件进行交互的接口,它提供了与屏幕、输入设备等进行交互的能力。
Window.render()
方法接收SceneBuilder
构建的场景信息,并将其传递给GPU进行渲染。GPU是专门用于图形处理的硬件加速器,它能够高效地处理复杂的图形计算任务,并将渲染结果输出到屏幕上。
总的来说,RenderView
、SceneBuilder
和Window.render()
共同构成了Flutter的渲染管道。这个管道负责将Flutter框架中的widget树转换为屏幕上的像素,从而实现了用户界面的动态更新和渲染。
UIViewController
(iOS)和NSViewController
(macOS)载入到嵌入层。FlutterEngine
,作为Dart VM和Flutter运行时的宿主。FlutterViewController
关联对应的FlutterEngine
,传递UIKit(iOS)或Cocoa(macOS)的输入事件到Flutter。Activity
加载到嵌入层中。FlutterView
进行控制。Flutter允许开发者创建自定义的嵌入层。例如,已经存在通过VNC风格的帧缓冲区支持远程Flutter的例子,以及支持树莓派运行的Flutter例子。这些自定义嵌入层展示了Flutter在不同环境和设备上的灵活性和可扩展性。
\\nFlutter通过引入平台widget(AndroidView
和UiKitView
)确实解决了在不同平台上显示原生视图的问题。这两个widget允许Flutter应用在需要时嵌入和显示原生平台的视图组件,从而充分利用平台特定的功能和特性。
AndroidView
\\nAndroidView
允许在Flutter应用中嵌入和显示Android的原生视图。这对于需要在Flutter应用中集成特定的Android组件或服务(如地图、视频播放器等)时非常有用。AndroidView
通过Flutter的平台通道与Android原生代码进行通信。它会在Flutter的渲染树中创建一个占位符,并在后台创建一个对应的Android视图。这个视图会被嵌入到Flutter应用的界面中,并且可以通过平台通道与Flutter代码进行交互。UIKitView
\\nAndroidView
类似,UiKitView
允许在Flutter应用中嵌入和显示iOS的原生视图。这对于需要在Flutter应用中集成特定的iOS组件或服务时非常有用。UiKitView
的实现原理与AndroidView
相似,也是通过Flutter的平台通道与iOS原生代码进行通信。它会在Flutter的渲染树中创建一个占位符,并在后台创建一个对应的iOS视图。这个视图同样会被嵌入到Flutter应用的界面中,并且可以通过平台通道与Flutter代码进行交互。性能开销:
\\n嵌入原生视图可能会引入一定的性能开销,特别是在频繁更新或动画效果较多的情况下。因此,开发者需要在使用时权衡性能和功能之间的平衡。
\\nFlutter引擎原本是用C++编写的,主要用于与底层操作系统进行交互。然而,在Web平台上,由于不存在直接的操作系统API访问,Flutter需要重新实现其引擎部分。Flutter在Web上使用了浏览器的标准API来重新实现引擎的功能,这使得Flutter应用能够在Web浏览器上运行。
\\nDart语言从设计之初就支持直接编译成JavaScript,这为Flutter在Web上的运行提供了基础。Flutter框架本身是用Dart编写的,因此将其编译成JavaScript并在Web浏览器上运行是相对简单的。Dart编译器会生成高效的JavaScript代码,从而确保Flutter应用在Web上的性能
\\nWeb上的呈现选项
\\n在Web平台上,Flutter提供了两种呈现内容的选项:HTML和WebGL。
\\n在开发 Flutter Web 应用时,dartdevc
(Dart Development Compiler)是主要的编译器。这个编译器支持增量编译,这意味着它只会重新编译发生变化的代码部分,而不是整个应用。这个特性对于提升开发体验至关重要,因为它大大减少了每次代码修改后的编译时间。
dartdevc
编译器的快速编译能力。当准备好将 Flutter Web 应用部署到生产环境时,dart2js
(Dart to JavaScript Compiler)是首选的编译器。dart2js
将 Dart 代码深度优化并编译成高效的 JavaScript 代码,这是部署到浏览器环境的标准格式。
dart2js
会对 Dart 代码进行深度优化,包括代码混淆(minification)、死代码消除(dead code elimination)和其他多种优化技术,以确保生成的 JavaScript 代码尽可能小且运行速度快。一旦使用 dart2js
编译了 Flutter Web 应用,生成的文件就可以部署到任何能够托管静态文件的服务器上。这包括云服务器、内容分发网络(CDN)以及像 Firebase Hosting、GitHub Pages 这样的托管服务
Flutter 的渲染机制确实遵循了简单快速的首要原则,并且它通过一系列高效的步骤确保用户界面的流畅和响应性。
\\n这个流程确保了 Flutter 应用能够快速响应用户输入,同时保持高质量的视觉效果。Flutter 的这种渲染机制得益于其底层架构,特别是其使用 Dart 语言和 Skia 、Impeller图形库,共同为 Flutter 提供了高性能和跨平台的渲染能力
\\nFlutter 的界面构建、布局、合成和绘制全都由 Flutter 自己完成,而不是转换为对应平台系统的原生组件
\\nImpeller是Flutter的新一代渲染引擎,其核心职责是绘制应用的界面,这包括布局计算、纹理映射、动画处理等一系列任务。它负责将代码转换为像素、颜色和形状,因此会直接影响应用的性能和渲染效果。以下是对Impeller渲染引擎的详细介绍:
\\n尽管Skia是一个优秀的通用2D图形库,被广泛应用于Google Chrome、Android、Firefox等设备,但由于其通用性,它无法专门针对Flutter的要求进行优化。Skia附带的许多功能超出了Flutter的需求,其中一些可能会导致不必要的开销,导致渲染时间变慢。因此,Skia的通用性给Flutter带来了性能瓶颈。相比之下,Impeller是专门为Flutter设计的,旨在优化Flutter架构的渲染过程。
\\nImpeller作为Flutter的新一代渲染引擎,在性能、渲染效果和跨平台能力等方面都表现出色。随着Flutter团队的不断优化和完善,Impeller有望成为未来Flutter应用的默认渲染引擎。
\\n布局过程:
\\n1、父widget会向其子widget提供一组布局约束(通常是最小和最大宽度和高度的限制)。
\\n2、子widget然后根据这些约束来决定自己的大小,并通过调用父widget的layout
方法来告知父widget自己的最终大小。
这个过程会递归地在整个widget树中进行,直到所有的widget都被正确地布局。
\\n遍历节点:
\\n1、Flutter 会以 DFS(深度优先遍历)方式遍历渲染树,并 将限制以自上而下的方式 从父节点传递给子节点。
\\n2、子节点若要确定自己的大小,则 必须 遵循父节点传递的限制。子节点的响应方式是在父节点建立的约束内 将大小以自下而上的方式 传递给父节点。
\\n3、遍历完一次树后,每个对象都通过父级约束而拥有了明确的大小,随时可以通过调用 paint()
进行渲染
4、盒子限制模型对象布局的时间复杂度是 O(n)
\\nFlutter 的目标是实现布局的线性性能初始化,以及在更新现有布局时的次线性性能。这意味着,在大多数情况下,布局操作应该比对象渲染更快。为了达到这一目标,Flutter 采用了高效的布局算法,这些算法在单次传递中完成布局,从而避免了多次测量和布局传递的开销。时间复杂度是 O(n)
\\nFlutter 对每一帧执行一次布局操作,且这个操作在单个传递中完成。在这个过程中,父节点向下传递约束信息,子节点根据这些约束递归地执行布局操作,并返回几何信息给父节点。这种策略确保了每个渲染对象在布局过程中最多被访问两次:一次在向下传递约束时,一次在向上传递几何信息时。
\\nRenderBox 是 Flutter 中最常用的布局模型,它使用二维笛卡尔坐标进行运算。在 RenderBox 布局中,约束以最小和最大宽高的形式传递给子节点。子节点根据这些约束选择自己的大小,并在布局完成后返回给父节点。父节点随后根据子节点返回的大小信息来确定子节点在父坐标系中的位置。
\\n为了优化布局性能,Flutter 采用了多种策略:
\\n通常实现无限滚动列表是比较困难的。Flutter 支持基于 构造器 模式实现的简单无限滚动列表界面,该功能需要 视窗感知布局 及 按需构建 widget 的
\\nViewport
\\nViewport是可滚动widget的外部容器,它提供了一个可以滚动的视窗口。Viewport本身并不直接渲染内容,而是包含一个或多个sliver,这些sliver负责实际的布局和渲染工作。Viewport的大小通常与屏幕大小相匹配,但它的内部空间可以远大于屏幕,从而允许用户滚动查看更多内容。
\\nSliver
\\nSliver是实现了视窗感知协议(Viewport-aware protocol)的RenderObject子类。与RenderBox不同,sliver不是直接填充其父容器的整个空间,而是根据Viewport提供的可见空间来进行布局。这意味着sliver可以处理超出视窗口边界的内容,并根据用户的滚动操作来动态地显示或隐藏这些内容。
\\nSliver的布局协议
\\n在sliver布局协议中,父节点(通常是Viewport)向下传递给子节点(即sliver)一组约束信息,这些约束信息描述了视窗口的大小、滚动位置以及滚动方向等。子节点(sliver)根据这些约束信息来计算自己的布局,并返回一组几何信息来描述自己的位置和大小。
\\n与盒子布局(如RenderBox)不同,sliver布局协议中的约束和几何数据更加复杂,因为它们需要考虑滚动和视窗口的变化。例如,一个sliver可能需要知道它还有多少可见空间来继续布局子节点,或者它是否已经滚动到了视窗口的底部。
\\nSliver的组合
\\nFlutter允许开发者通过组合不同的sliver来创建复杂的滚动布局和效果。例如,一个Viewport可以包含一个折叠标题sliver、一个线性列表sliver和一个网格sliver。这些sliver将按照sliver布局协议进行协作,共同填充Viewport提供的可见空间。
\\n由于sliver知道还有多少可见空间可用,它们可以智能地生成有限的子节点,即使这些子节点在理论上可能是无限的(例如,一个无限长的列表)。这种能力使得Flutter能够高效地处理大量数据,并为用户提供流畅的滚动体验。
\\n构建与布局的交叉执行
\\n在Flutter中,构建(build)阶段通常用于创建widget树,而布局(layout)阶段则用于计算widget的位置和大小。然而,在处理无限滚动列表等场景时,如果严格按照构建到布局再到绘制的顺序执行,可能会导致性能问题,因为只有在布局阶段才能确定视窗口的可用空间,而这时再构建用于填充空间的widget可能已经太迟了。
\\n为了解决这个问题,Flutter采用了构建和布局交叉执行的方式。这意味着在布局阶段的任意时刻,只要这些widget是当前布局的渲染对象的子节点,框架就可以按需构建新的widget。这种方式允许Flutter在布局过程中动态地调整widget树,以适应用户滚动和视图变化的需求。
\\n消息传递和算法控制
\\n为了确保构建和布局的交叉执行能够正确进行,Flutter严格控制了构建及布局中消息传播的算法。在构建过程中,消息只能沿构建树向下传递,以确保每个widget都能够正确地接收其父widget的状态和配置。同时,在布局遍历过程中,渲染对象不会访问其子树的构建状态,以避免在布局计算过程中使已构建的widget失效。
\\n此外,一旦布局从某个渲染对象返回,在当前布局过程中,该渲染对象将不会被再次访问。这意味着后续布局计算生成的任何信息都不会影响已经构建的渲染对象的子树。这种设计确保了布局的确定性和一致性。
\\n线性协调和树结构优化
\\n在处理滚动和动态内容加载时,线性协调和树结构优化也是至关重要的。线性协调允许Flutter在滚动过程中有效地更新element树,以确保只有视窗口内的内容被重新构建和布局。这有助于减少不必要的计算和渲染,提高应用的性能。
\\n同时,树结构优化也是提高滚动性能的关键。通过优化widget树的结构,Flutter可以减少不必要的节点和渲染工作,从而进一步提高滚动效率。
\\nFlutter使用一种高效的机制来构建和管理其界面,这种机制依赖于widget、element和state等关键概念。
\\nWidget:在Flutter中,widget是界面构建的基本单元。它们是不可变的,这意味着一旦创建,它们的属性就不能改变。由于这种不可变性,Flutter框架可以高效地重用和比较widget,从而优化构建过程。
\\nElement:Element是widget在界面树中的实例化表示。每个widget在构建时都会创建一个对应的element。Element树保留了用户页面的逻辑结构,并且是动态更新的。与widget不同,element可以记住与其他element的父或子关系,并且可以变脏(即需要更新)。
\\nState:对于Stateful widget,它们的状态(state)是与特定的element实例相关联的。当widget的状态发生变化时,可以通过调用setState()方法来通知框架该widget需要重新构建。
\\n构建过程:当某个element变脏时,Flutter框架会将其添加到脏element列表中。在构建过程中,框架会遍历这个列表,并跳过干净的element,只更新脏的element。这种机制使得构建过程非常高效,因为每个element在构建阶段最多只会被访问一次。
\\n优化策略:
\\nWidget比较:由于widget是不可变的,因此可以通过比较widget对象的引用来确定它们是否相同。如果父节点使用相同的widget来重新构建element,并且该element没有将自己标记为脏,那么可以直接从构建中返回,切断构建的向下传递。
\\n投影模式:开发者可以利用widget的不可变性和构建过程的优化来实现投影模式。在这种模式下,widget可以包含预先构建的子widget作为成员变量,从而在构建过程中避免不必要的重复工作。
\\nInheritedWidget:为了避免父链的遍历,Flutter框架使用InheritedWidget来向下传递信息。通过在每个element上维护一个InheritedWidget哈希表,框架可以高效地访问和更新这些信息,从而避免O(N²)的复杂度。
\\n在Flutter中,element是widget树中的一个实例,它持有Stateful widget的状态对象以及底层的渲染对象。当框架能够重用element时,这意味着它不需要销毁和重新创建这些对象,从而保留了用户界面的逻辑状态信息和之前计算的布局信息。这不仅可以避免不必要的遍历整棵子树,还可以显著提高应用的性能,特别是在处理大量动态内容时。
\\n关于全局树更新和GlobalKey的使用:
\\n开发者广泛使用全局key和全局树更新来实现各种高级效果,如hero transition(英雄动画)和导航等。这些效果通常需要在不同的widget树之间共享状态和布局信息,而全局key和全局树更新提供了一种高效且灵活的方式来实现这一点。
\\n在Flutter中,当需要更新列表中的widget时,传统的做法可能是对整个列表进行树差异比较,这种方法的复杂度通常是O(N^2),其中N是列表中的widget数量。然而,Flutter采用了一种更高效的算法,其复杂度为O(N),这种算法通过独立地检查每个element的子节点来决定是否重用该element。
\\n这种子列表协调算法针对几种特定情况进行了优化:
\\n子列表协调算法的具体实现通常涉及以下步骤:
\\n这种算法的优点是能够高效地处理列表的更新,特别是在列表项的顺序频繁变化时。它避免了不必要的重建和重绘,从而提高了应用的性能和响应速度。同时,通过使用key来匹配widget,开发者可以更好地控制列表项的更新行为,并保持状态的连续性。
\\n子模型无关性:
\\nFlutter的渲染树设计得不会记住特定的子模型,这意味着它不会依赖于具体的子列表结构。例如,RenderBox
类有一个抽象的visitChildren()
方法,而不是具体的firstChild
和nextSibling
接口。这种设计允许子类以更高效的方式处理其子项,特别是当子类只支持单个子节点时(如RenderPadding
)。这种灵活性使得Flutter能够根据不同的布局需求进行优化,而不必受限于固定的子项结构。
视觉渲染树与Widget逻辑树的分离:
\\nFlutter中的渲染树在与设备无关的视觉坐标系中运行,而Widget树则在逻辑坐标中运行。这种分离使得布局和绘制计算可以更加高效地进行,因为渲染树中的这些计算比Widget到渲染树的切换更加频繁。此外,逻辑坐标到视觉坐标的转换是在Widget树和渲染树之间的切换中完成的,这避免了重复的坐标转换,从而提高了性能。
\\n专门的渲染对象处理文本:
\\nFlutter使用专门的渲染对象RenderParagraph
来处理文本布局。这种设计使得文本布局可以更加高效地进行,因为RenderParagraph
作为渲染树中的一个叶子节点,可以避免在父节点提供相同布局约束下的重复计算。此外,开发者可以通过组合形式将文本并入到用户界面中,而不必使用文本感知渲染对象进行子类化,这进一步简化了文本处理流程。
可观察对象在渲染树中的应用:
\\nFlutter使用模型观察及响应设计模式,但在某些叶子节点的数据结构上使用了可观察对象。例如,Animation
对象会在值发生变化时通知观察者列表。Flutter将这些可观察对象从Widget树转移到渲染树中,使得渲染树可以直接监听这些对象的变化,并在它们改变时仅重绘管道的相关阶段。这种设计减少了不必要的重建和重绘,提高了应用的性能。
OpenHarmony-SIG/flutter_flutter
\\n\\n# 适配安卓开发环境\\nexport ANDROID_SDK_ROOT=/Users/用户名/Library/Android/sdk\\nexport PATH=$ANDROID_SDK_ROOT/emulator:$ANDROID_SDK_ROOT/tools:$ANDROID_SDK_ROOT/tools/bin:$ANDROID_SDK_ROOT/platform-tools:$PATH\\n\\n# Flutter 快捷命令\\nalias cleanFlutterLockFile=\\"rm -rf /Users/用户名/fvm/default/bin/cache/lockfile\\"\\nalias killFlutter=\\"killall -9 dart\\"\\n\\n# OpenJDK 配置\\nexport PATH=\\"/opt/homebrew/opt/openjdk/bin:$PATH\\"\\n\\n# 国内镜像\\nexport PUB_HOSTED_URL=https://pub.flutter-io.cn\\nexport FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn\\n\\n# 根据当前使用的 Flutter 版本设置 PUB_CACHE 目录和 Flutter 路径\\nfunction set_flutter_version() {\\n local version=$1\\n export PUB_CACHE=$HOME/.pub-cache-$version\\n export PATH=\\"$PUB_CACHE/bin:$HOME/fvm/versions/$version/bin:$PATH\\"\\n}\\n\\n# 使用Flutter官方稳定版本的flutter和dart版本\\nalias use_flutter_release=\\"set_flutter_version 3.24.5 && export PATH=$HOME/fvm/versions/3.24.5/bin:$PATH\\"\\n\\n# 使用鸿蒙突击版本的flutter和dart版本\\nalias use_ohos_dev=\\"set_flutter_version ohos_dev && export PATH=$HOME/fvm/versions/ohos_dev/bin:$PATH\\"\\n\\n# 使用鸿蒙稳定版本的flutter和dart版本\\nalias use_ohos_release=\\"set_flutter_version ohos_release && export PATH=$HOME/fvm/versions/ohos_release/bin:$PATH\\"\\n\\n# 默认使用Flutter官方稳定版本的flutter和dart版本\\nalias use_flutter_default=\\"set_flutter_version 3.24.5 && export PATH=$HOME/fvm/versions/3.24.5/bin:$PATH\\"\\n\\n# HamonyOS SDK\\nexport TOOL_HOME=/Applications/DevEco-Studio.app/Contents # mac环境\\nexport DEVECO_SDK_HOME=$TOOL_HOME/sdk # command-line-tools/sdk\\nexport PATH=$TOOL_HOME/tools/ohpm/bin:$PATH # command-line-tools/ohpm/bin\\nexport PATH=$TOOL_HOME/tools/hvigor/bin:$PATH # command-line-tools/hvigor/bin\\nexport PATH=$TOOL_HOME/tools/node/bin:$PATH # command-line-tools/tool/node/bin\\n\\n# Java 配置\\nexport JAVA_HOME=/opt/homebrew/Cellar/openjdk@17/17.0.13/libexec/openjdk.jdk/Contents/Home\\nexport PATH=$JAVA_HOME/bin:$PATH\\n\\n#配置自定义命令\\nalias run_ios_mini_sdk=\'/Users/用户名/bashShell/update_ios_deployment_target.sh\'\\nalias run_android_mini_sdk=\'/Users/用户名/bashShell/update_android_deployment_target.sh\'\\n
\\n使用的时候 根据命令去切换脚本
\\n使用fvm 去管理版本
\\nios:
\\n#!/bin/bash\\n\\n# 设置最低支持的 iOS 版本\\nMIN_IOS_VERSION=\\"13.0\\"\\n\\n# 更新 Podfile\\necho \\"Updating Podfile...\\"\\nsed -i \'\' \\"s/platform :ios, \'[0-9.]*\'/platform :ios, \'$MIN_IOS_VERSION\'/\\" ios/Podfile\\n\\n# 更新 Xcode 项目文件\\necho \\"Updating Xcode project files...\\"\\nplutil -replace MinimumOSVersion -string $MIN_IOS_VERSION ios/Runner/Info.plist\\n\\n# 更新所有 Pod 项目的 deployment target\\necho \\"Updating Pod deployment targets...\\"\\ncd ios\\npod install\\n\\n# 使用 xcodeproj 工具更新所有 target 的 deployment target\\nif ! command -v xcodeproj &> /dev/null\\nthen\\n echo \\"xcodeproj could not be found, installing...\\"\\n gem install xcodeproj\\nfi\\n\\nruby -e \\"\\nrequire \'xcodeproj\'\\nproject_path = \'Runner.xcodeproj\'\\nproject = Xcodeproj::Project.open(project_path)\\nproject.targets.each do |target|\\n target.build_configurations.each do |config|\\n config.build_settings[\'IPHONEOS_DEPLOYMENT_TARGET\'] = \'$MIN_IOS_VERSION\'\\n end\\nend\\nproject.save\\n\\"\\n\\necho \\"Done! Minimum iOS deployment target set to $MIN_IOS_VERSION.\\"\\n
\\n安卓:
\\n#!/bin/bash\\n\\n# 检查是否在 Flutter 项目根目录中\\nif [ ! -f \\"pubspec.yaml\\" ]; then\\n echo \\"Error: This script must be run from the root of a Flutter project.\\"\\n exit 1\\nfi\\n\\n# 设置 Android 项目的 minSdkVersion\\nMIN_SDK_VERSION=23\\n\\n# 更新 android/app/build.gradle 文件\\necho \\"Forcing minSdkVersion to $MIN_SDK_VERSION in android/app/build.gradle...\\"\\n\\n# 强制设置 minSdkVersion 为 23\\nsed -i \'\' \\"s/minSdkVersion [0-9]+/minSdkVersion $MIN_SDK_VERSION/\\" android/app/build.gradle\\n\\n# 如果不存在 minSdkVersion 配置项,则添加\\nif ! grep -q \\"minSdkVersion\\" android/app/build.gradle; then\\n sed -i \'\' \\"/defaultConfig {/a\\\\\\n minSdkVersion $MIN_SDK_VERSION\\n \\" android/app/build.gradle\\nfi\\n\\n# 确保 minSdkVersion 设置为 23\\nsed -i \'\' \\"/defaultConfig {/!b;n;c\\\\\\n minSdkVersion $MIN_SDK_VERSION\\n\\" android/app/build.gradle\\n\\necho \\"Done! Android minSdkVersion set to $MIN_SDK_VERSION.\\"\\n
\\n可以使用 FVM 管理本地 Flutter 的版本。
\\ngithub地址:github.com/leoafarias/…
\\n你可以使用 Homebrew package manager 在 Mac OS X 中安装 FVM。
\\n安装 FVM:
\\nbrew tap leoafarias/fvm\\nbrew install fvm\\n
\\n卸载 FVM:
\\nbrew uninstall fvm\\nbrew untap leoafarias/fvm\\n
\\n在海外V5.5.0+版本中,可以安装 Flutter 2.2.0 版本
\\n安装 Flutter:
\\nfvm install 3.3.7\\nfvm global 3.3.7 # 可以用 global\\nflutter doctor -v # 按照提示安装各种插件,直到没有提示错误\\n
\\n如果需要切换本地已安装的 Flutter 版本,用 fvm use命令(或 fvm global 命令),例如 切换至 Flutter 2.0.4:
\\nfvm global 2.0.4 # 如果提示 use 用不了,可以用 global\\n
\\n在hw_flutter目录下,安装第三方库,执行如下命令:
\\nflutter pub get\\n
\\n问题:
\\n cmdline-tools component is missing\\n Run `path/to/sdkmanager --install \\"cmdline-tools;latest\\"`\\n See https://developer.android.com/studio/command-line for more details.\\n ✗ Android license status unknown.\\n Run `flutter doctor --android-licenses` to accept the SDK licenses.\\n See https://flutter.dev/docs/get-started/install/macos#android-setup for more details.\\n
\\n解决方法:
\\n集合 —— 操作批量数据
的核心工具
在日常生活中,我们经常遇到需要批量
处理的数据,例如一个班级里有很多学生
、每个班级有学生的花名册
、学生考试后有成绩表
等。如何高效地管理和维护这些结构相似的批量数据是编程中非常重要的课题。在编程术语中,可以称这样的数据为集合类型或复合数据类型。在 Dart
中,最常用的三种集合类型分别是:列表(List
)、集合(Set
) 和 映射(Map
)。
接来下来我们一起开启探索列表(List
)的神奇之旅。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\n序号 | 姓名 |
---|---|
1 | Alice |
2 | Bob |
3 | Charlie |
4 | David |
如上图所示,有一个班级的学生名单,按学号顺序排列。可以轻松找到第一个、第二个,甚至最后一个学生的名字。这就是 List
的工作方式 —— 它是一个有序
的元素序列,可以重复存储相同的元素
,并且可以根据位置(索引
)快速访问或修改任何元素
。
List
是一个基于动态数组实现的数据结构,它可以存储相同类型
或不同类型
的元素。List
是有序的
,这意味着每个元素都有一个固定的索引位置,可以通过索引来快速访问或修改这些元素(随机访问
)。
// 创建一个 List\\nList<String> names = [\'Alice\', \'Bob\', \'Charlie\',\'David\'];\\n
\\n顺序是固定的
。重复出现在名单中
。// 学生名单\\nList<String> studentNames = [\'Alice\', \'Bob\', \'Charlie\', \'Alice\'];\\n\\n// 添加新学生\\nstudentNames.add(\'David\');\\n\\n// 输出名单\\nprint(studentNames); // 输出: [Alice, Bob, Charlie, Alice, David]\\n
\\nDart
的 List
支持泛型,允许指定列表中元素的具体类型
,从而提高代码的类型安全性
和可读性
。
// 创建一个泛型为int类型的list\\nList<int> numbers = [1, 2, 3, 4, 5];\\n// 创建一个泛型为String类型的list\\nList<String> names = [\'Alice\', \'Bob\', \'Charlie\', \'David\'];\\n// 创建一个泛型为dynamic类型的list\\nList<dynamic> list = [\'Alice\', 1, false];\\n
\\n最简单的方式是直接在方括号中列出元素
。
List<int> numbers = [1, 2, 3, 4, 5];\\n
\\n使用 List
类的构造函数来创建一个空列表
或具有固定长度的列表
。
// 创建一个空列表\\nList<String> emptyList = List<String>.empty(growable: true);\\n\\n// 创建一个固定长度的列表,所有元素初始为 null\\nList<int> fixedLengthList = List<int>.filled(5, 0);\\n
\\nList.generate
List.generate
构造函数可以根据提供的生成器函数创建一个列表
。
List<int> generatedNumbers = List.generate(5, (index) => index * 2);\\nprint(generatedNumbers); // 输出: [0, 2, 4, 6, 8]\\n
\\n可以通过索引访问 List
中的元素,索引从 0
开始。
List<String> names = [\'Alice\', \'Bob\', \'Charlie\'];\\nprint(names[0]); // 输出: Alice\\n
\\n同样,可以通过索引修改 List
中的元素。
names[1] = \'Bobby\';\\nprint(names); // 输出: [Alice, Bobby, Charlie]\\n
\\nadd
方法。names.add(\'David\');\\nprint(names); // 输出: [Alice, Bobby, Charlie, David]\\n
\\ninsert
方法。names.insert(1, \'Betty\');\\nprint(names); // 输出: [Alice, Betty, Bobby, Charlie, David]\\n
\\nremove
方法。names.remove(\'Betty\');\\nprint(names); // 输出: [Alice, Bobby, Charlie, David]\\n
\\nremoveLast
方法。names.removeLast();\\nprint(names); // 输出: [Alice, Bobby, Charlie]\\n
\\nclear
方法。names.clear();\\nprint(names); // 输出: []\\n
\\nfor
循环使用传统的for
循环来遍历List
。
for (int i = 0; i < numbers.length; i++) {\\n print(numbers[i]);\\n}\\n
\\nforEach
使用forEach
来遍历List
,简化遍历操作。
numbers.forEach((number) => print(number));\\n
\\nfor-in
循环for-in
循环提供了一种简洁
的遍历方式。
for (var number in numbers) {\\n print(number);\\n}\\n
\\n// length:获取 List 的长度\\nprint(numbers.length); // 输出: 5\\n// isEmpty 和 isNotEmpty:检查 List 是否为空。\\nprint(emptyList.isEmpty); // 输出: true\\n
\\nindexOf
或contains
方法。print(numbers.indexOf(3)); // 输出: 2\\nprint(numbers.contains(5)); // 输出: true\\n
\\nsort
方法。numbers.sort(); // 默认按升序排序\\nprint(numbers); // 输出: [1, 2, 3, 4, 5]\\n
\\nmap
、where
等高阶函数。List<int> doubledNumbers = numbers.map((n) => n * 2).toList();\\nprint(doubledNumbers); // 输出: [2, 4, 6, 8, 10]\\n\\nList<int> evenNumbers = numbers.where((n) => n % 2 == 0).toList();\\nprint(evenNumbers); // 输出: [2, 4]\\n
\\nList.unmodifiable
)有时需要确保一个 List
不会被修改。可以使用 List.unmodifiable
来创建一个不可变的 List
。
List<int> immutableNumbers = List.unmodifiable([1, 2, 3, 4, 5]);\\n\\n// 下面这行代码会抛出异常,因为 immutableNumbers 是不可变的\\n// immutableNumbers.add(6);\\n
\\nDart
的 List
提供了丰富的功能和灵活的操作方式,使得处理有序数据变得非常方便。通过合理利用 List
的各种特性,可以编写出更加简洁
、高效
和易于维护
的代码。
\\n","description":"前言 集合 —— 操作批量数据的核心工具\\n\\n在日常生活中,我们经常遇到需要批量处理的数据,例如一个班级里有很多学生、每个班级有学生的花名册、学生考试后有成绩表等。如何高效地管理和维护这些结构相似的批量数据是编程中非常重要的课题。在编程术语中,可以称这样的数据为集合类型或复合数据类型。在 Dart 中,最常用的三种集合类型分别是:列表(List)、集合(Set) 和 映射(Map)。\\n\\n接来下来我们一起开启探索列表(List)的神奇之旅。\\n\\n操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。\\n\\n一、基本概念\\n1.1、图像表示…","guid":"https://juejin.cn/post/7462019042634989568","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-21T03:57:45.880Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/cd747c7e4b4f4d8aa00c734e766dd2a8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1738036664&x-signature=4x4TB7g3MxA5Ic4vctdee7nK3%2FY%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 设计百科全书 - 布局规范篇","url":"https://juejin.cn/post/7461886222153187365","content":"欢迎一键四连(
\\n关注
+点赞
+收藏
+评论
)
其实我是有点鄙视那些人,他们说。
\\n\\n\\n”界面写的好看有什么用,你要把后面的逻辑代码写好了才是本事“
\\n
\\n\\n”界面写得好没用,用户只在乎功能和稳定“
\\n
需要解释的几个点就是:
\\n本章会介绍各类常用的尺寸设计,以及布局思路。
\\n本章部分内容跟flutter_screenutil自适应尺寸无关,学习的是思路,而不是具体的数值。
AppBar 其实是2个部分组成,上方是StatusBar 状态栏,由系统控制高度
\\n下方是ToolBar,Flutter 定义了一部分 尺寸常量,比如toolbar的默认高度kToolbarHeight = 56,
\\n其他常用参数你可以从下面文件找到:
\\n\\nflutter/packages/flutter/lib/src/material/constants.dart
\\n
/// 手指交互尺寸\\nconst double kMinInteractiveDimension = 48.0;\\n\\n/// [AppBar] toolbar 默认高度\\nconst double kToolbarHeight = 56.0;\\n\\n/// 底部导航栏 bar 高度\\nconst double kBottomNavigationBarHeight = 56.0;\\n\\n/// 默认的水波纹圆角值\\nconst double kRadialReactionRadius = 20.0;\\n\\n/// Tab 选项卡水平内边距\\nconst EdgeInsets kTabLabelPadding = EdgeInsets.symmetric(horizontal: 16.0);\\n\\n/// 列表子项的内边距\\nconst EdgeInsets kMaterialListPadding = EdgeInsets.symmetric(vertical: 8.0);\\n\\n// 默认亮色图标的颜色\\nfinal Color kDefaultIconLightColor = Color(0xFFFFFFFF);\\n\\n// 默认暗色图标的颜色\\nfinal Color kDefaultIconDarkColor = Color(0xDD000000);\\n
\\nAppbar 包含三个区域,按照设计规范来说,\\n左侧leading 的触控极限应该是48*48,\\n右侧Action是一个组件数组,如果你放入交互按钮,应该 增加右侧 5-15 的边距,这样按钮才不会太贴边
\\n如果你增加的按钮是没有内边距的,你需要将边距设置为10甚至15。
\\nIconButton默认是有内边距的,如果增加10的边距,就会特别空,所以可以考虑使用5。
\\n(ps:设置action之后,title不会居中了,记得根据软件的设计风格 设置centerTitle)
/// action区域的右侧边距设计\\nactions: [\\n Button(\\n onPressed: () {\\n _showPushAction();\\n },\\n child: Image.asset(\\n \\"assets/images/photo.png\\",\\n width: 24,\\n height: 24,\\n ),\\n ),\\n 5.horizontalSpace,//增加5-15个尺寸的边距\\n],\\n
\\n多个IconButton放入action,并不会特别难看,但是你放入多个自定义按钮,可能就会特别挤 或者 特别空,考虑增加它们之间的间距 工具栏看起来更舒服。\\n(关于自定义按钮尺寸和Icon尺寸请看下方章节细节解析)
\\n作为页面中最常见的区域,body一般需要考虑 内边距和安全区域,
\\n页面整体内边距
\\n一般body的内边距考虑设计成四边 10 - 20 的内边距,
\\n如果你的页面是List列表类型,建议是给List增加内边距,\\n如果你使用了extendBodyBehindAppBar,修改ListView的Padding会导致ListView默认的SafePadding失效,你需要配合ViewPadding来避免安全区域。
Item的尺寸和内边距设计
\\n参考谷歌给MD定义的触控尺寸为48px,那么Item大概率是比48px高一些的
\\n\\nItem的 水平间距建议设计为 15 - 20px ,上下间距 建议设计为 10px - 20px。\\n不建议给Item设定固定的高度,而是让Item自适应高度,除非你能准确的计算内部高度,不然会出现布局溢出情况。(itemExtent设置后据说会提升性能?)
\\n
\\nBottomBar 的默认高度也是56(存疑),
\\n这个参数一般不建议修改,比如使用BottomAppBar时,建议使用自适应高度,而不是固定尺寸。
\\n底部一般有三个可用组件
谷歌从很早开始就在MD设计中申明,手指触碰区域 是48px,\\n所以按钮的点击区域大概围绕这个参数设计。
\\n大多数按钮文字 都是 12-14px,所以按钮的上下边距大概是 15px - 20px,
\\n大多数时候Icon 被设计成几个 固定值,
\\n当它与文字并排时,大概是 10-16px的size,
\\n当它作为按钮图标是,大多数时候是 24px 28px 32px
\\nDialog 弹窗一般设计都是固定宽高的,
\\n这时候需要注意极限宽高的设计,部分小屏幕手机,宽度可能低于320像素,高度低于500像素,\\n如果使用固定宽高的尺寸设计,可能会出现溢出警告。
\\n弹窗部分的尺寸设计,尽量使用最小化尺寸设计,也就是内容+边距的设计思路。
\\n建议使用ConstrainedBox设置一个最小值240,内部边距参考20px,让整个弹窗自适应高宽。
\\n官方的Dialog 就是限制的最小宽度
constraints: const BoxConstraints(minWidth: 280.0),\\n
\\n\\nBottomSheet 底部弹出层,可以考虑 设计为屏幕的百分比高度。例如30% 50% 60-70%
\\n可以使用固定像素值,建议不要超过 450px。
MediaQuery.of(context).size\\n
\\n使用size可以拿到屏幕的尺寸
\\nMediaQuery.of(context).viewPadding.top\\nMediaQuery.of(context).viewPadding.bottom\\n
\\n通过mediaquery可以拿到屏幕安全区域的尺寸,通过在List上下设置,可以让List避开非显示区域。
\\n查看ListView内部安全区域实现代码,看看官方怎么实现的。
final MediaQueryData? mediaQuery = MediaQuery.maybeOf(context);\\n if (mediaQuery != null) {\\n // Automatically pad sliver with padding from MediaQuery.\\n final EdgeInsets mediaQueryHorizontalPadding =\\n mediaQuery.padding.copyWith(top: 0.0, bottom: 0.0);\\n final EdgeInsets mediaQueryVerticalPadding =\\n mediaQuery.padding.copyWith(left: 0.0, right: 0.0);\\n // Consume the main axis padding with SliverPadding.\\n effectivePadding = scrollDirection == Axis.vertical\\n ? mediaQueryVerticalPadding\\n : mediaQueryHorizontalPadding;\\n // Leave behind the cross axis padding.\\n sliver = MediaQuery(\\n data: mediaQuery.copyWith(\\n padding: scrollDirection == Axis.vertical\\n ? mediaQueryHorizontalPadding\\n : mediaQueryVerticalPadding,\\n ),\\n child: sliver,\\n );\\n }\\n
\\n当你为ListView设置Padding时,就把safearea功能给覆盖了,\\n你可以参考上面的代码,自己再实现它。
\\nListTile 作为快速演示效果组件,非常不错,
\\n但是推荐自己用更简单的Row组件实现它,因为你自己写的结构会更简单,也更轻量化。
\\n\\n我见过SingleChildScrollView + Column实现滚动效果的。
\\n
\\n我更推荐使用ListView(children: [])
最近的Flutter SDK 3.27+ 增加了 spacing 作为间隔
\\n在之前的版本使用SizedBox 作为间隔
如果能使用
\\n实现的对齐或者排序方式,优先考虑,而不是使用Spacer
\\n大多数时候如果仅有边距需求,优先选择Padding组件。\\n大多数时候,padding 应该在最外层
\\nPadding(\\n padding: EdgeInsets.all(10),\\n child: Row(),\\n
\\nStack 堆叠布局,一般用于多个组件叠放,
\\n使用fit: StackFit.expand 铺满父容器,而不是用double.infinity 再次😀。
面向对象方法(OOM)的核心思想是通过引入对象的概念,将现实世界中的事物、事件、规则和概念进行抽象,以一种更接近现实世界的视角建模问题域。而面向对象分析(OOA)是面向对象方法中的第一阶段,这一阶段将决定后续阶段的实现。
\\n面向对象分析是将现实世界中的问题抽象为对象的过程。
\\n面向对象分析主要包含以下几个活动。\\n
示例:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n对象 | 动物园系统中的实体或概念 |
---|---|
动物 | 如老虎、狮子、大象、长颈鹿等,它们是动物园的主要展示对象。 |
游客 | 来动物园参观的人群,他们与动物进行互动,如观看动物表演、喂食等。 |
饲养员 | 负责动物的日常饲养、照顾和清洁工作的人员。 |
管理员 | 负责动物园的整体运营和管理,包括动物引进、园区维护、游客安全等。 |
示例:
\\n注:我们这里只举例老虎这个动物。
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n对象/类名称 | 组织关系与说明 |
---|---|
动物类(基类) | 作为所有动物种类的基类,包含共同属性和方法(如体重、年龄、叫声等)。 |
老虎类 | 继承自动物基类,包含老虎特有的属性和方法(如条纹、捕猎行为等)。 |
游客类 | 包含游客的个体信息(如姓名、年龄等)和游客行为(如购票、参观等)。 |
饲养员类 | 包含饲养员的个人信息(如姓名、负责动物种类等)和饲养行为(如喂食、清洁等)。 |
管理员类 | 包含管理员的个人信息(如姓名、管理职责等)和管理行为(如动物引进等)。 |
示例:
\\n注:我们这里只举例老虎这个动物。
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n相互关系 | 示例 |
---|---|
游客与动物 | 观看、拍照、喂食(允许时)。 |
饲养员与动物 | 喂食、清洁笼舍、观察健康状况。 |
管理员与饲养员 | 分配任务、检查工作、沟通需求。 |
管理员与游客 | 维护秩序、处理投诉、传达信息。 |
动物与动物 | 社交互动、竞争关系(如领地争夺)。 |
示例:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n对象/类 | 操作/方法 |
---|---|
动物类 | makeSound() :发声 eat() :进食 rest() :休息 interact() :与游客互动 |
游客类 | purchaseTicket() :购票enter() :入园 visit() :参观动物 |
饲养员类 | feed() :喂食动物 clean() :清洁笼舍monitorHealth() :监控健康 |
管理员类 | introduceAnimal() :引进动物 maintainPark() :维护园区 |
示例:
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n对象/类 | 内部信息/属性 |
---|---|
动物类 | name :动物名称species :动物种类 age :年龄 healthStatus :健康状况 |
游客类 | name :游客姓名ticket :门票信息(类型、购买时间等) |
饲养员类 | name :饲养员姓名 assignedAnimals :负责的动物列表 |
管理员类 | name :管理员姓名 parkStatus :园区状态(开放、关闭、维护中) |
本小节我们从动物园的例子出发,分别介绍了面向对象分析里面的几个重要活动,包括认定对象、组织对象、描述对象之间的相互作用、确定对象的操作、定义对象的内部信息。
","description":"前言 面向对象方法(OOM)的核心思想是通过引入对象的概念,将现实世界中的事物、事件、规则和概念进行抽象,以一种更接近现实世界的视角建模问题域。而面向对象分析(OOA)是面向对象方法中的第一阶段,这一阶段将决定后续阶段的实现。\\n\\n一、面向对象分析概述\\n\\n面向对象分析是将现实世界中的问题抽象为对象的过程。\\n\\n目的:通过对问题的分析建立分析模型,也就是将现实世界中的事物转换为抽象模型。\\n方法:将数据和功能结合为一个对象来考虑,将系统的行为和信息间的关系表示为迭代构造特征。可以理解为其构造过程是迭代进行的,而不是从一而终的。\\n主要活动:认定对象、组织对象…","guid":"https://juejin.cn/post/7461899974197510178","author":"好的佩奇","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-20T12:58:42.377Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/811bf5c17a354fe385a9ce8add669e68~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5aW955qE5L2p5aWH:q75.awebp?rk3s=f64ab15b&x-expires=1737982721&x-signature=2q2qdIxTPK8lfyWjcI2A95UEhSI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e389843d3dff444783c85c90da21830b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5aW955qE5L2p5aWH:q75.awebp?rk3s=f64ab15b&x-expires=1737982721&x-signature=pQfST58Vv680L9m52TEPa3772Lg%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Dart","Flutter","前端"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 设计百科全书 - Flutter、Dart 的语法和代码编写规范篇","url":"https://juejin.cn/post/7461825527058563110","content":"我最喜欢的2句话,
\\n前者是 ”闻道有先后,术业有专攻“
\\n后者是 ”学无先后,达者为师“
因为受到这两句话的影响,我这个人也是非常尊重分享知识的人。\\n同时也因为如此,有时候会跟别人产生争执,讨论某某技术是否足够好。
\\n但永远围绕的是知识而不是人。但是有些人攻击你,不仅攻击你,还要攻击你的知识。
\\n\\n”好为人师一直是我的问题“
\\n
\\n”永远不要试图说服每一个人“本章内容充满了个人认知与理解,如果你有不同意见完全可以直接跳过。
\\n
late id;\\nconst SizedBox();\\nfinal pi = 3.1415926;\\nint? id;\\n
\\n_pageIndex;\\n_buildBody()\\n_initData()\\n
\\n // 视图页面\\n 参数;\\n 事件和触发函数\\n Widget build(BuildContext context)\\n Widget _buildList()\\n Widget _buildItem()\\n\\n // 逻辑页面\\n 参数;\\n oninit()\\n _loadData()\\n _onTapUserInfo()\\n
\\nprint()❌\\ndebugPrint()✅\\n
\\nimport \'../../../routes/app_pages.dart\';❌\\nimport \'package:**/routes/app_pages.dart\';✅\\n
\\nFuture<T> work();\\n
\\nColor(0xFF336699)\\n
\\n static Color c_FF381F = const Color(0xFFFF381F); ❌\\n static Color c_DB3A34 = const Color(0xFFDB3A34); ❌\\n static Color c_666666 = const Color(0xFF666666); ❌\\n static Color c_999999 = const Color(0xFF999999); ❌\\n static Color c_FFFFFF = const Color(0xFFFFFFFF); ❌\\n static Color c_18E875 = const Color(0xFF18E875); ❌\\n static Color c_F0F2F6 = const Color(0xFFF0F2F6); ❌\\n
\\nconst Container() ❌\\n\\nconst DecoratedBox() ✅\\n
\\nContainer(\\n padding: padding) ❌\\n\\nPadding(\\n padding: padding) ✅\\n
\\nconst Container() ❌\\n\\nconst SizedBox() ✅\\n
\\ndouble.infinity\\n
\\n一般情况下是不需要明文使用double.infinity的,\\n常见的可能会设置double.infinity的情况\\n> body 的组件无法铺满,使用SizedBox.expand铺满(内部是double.infinity)\\n\\n> Column 无法铺满,设置crossAxisAlignment:CrossAxisAlignment.stretch 左右铺满\\n\\n> Row 无法铺满,设置crossAxisAlignment:CrossAxisAlignment.stretch 上下铺满\\n\\n> Stack 无法铺满,设置fit: StackFit.expand 铺满\\n
\\n暂时只想到这些组件有铺满问题,所以项目中不需要用到明文的double.infinity。
\\nSizedBox(height: 3.1415926 * pi * input) ❌ // 应该将 计算值放到实体类 中\\nSizedBox(height: 16 / 7) // 可以用,问题不大\\n
\\nSafaArea\\n
\\n多数情况下,在安卓设计中,沉浸式状态栏都是一种非常漂亮的设计,\\n如果错误的使用SafeArea会丢失这种效果,\\n并且有时候SafeArea会导致无法预料的布局,所以谨慎使用。
\\n ❌ 错误的示范\\n logic.currentGender.value == 1\\n ? xxx\\n : xxx\\n\\n✅\\n/// 实名状态\\nenum Status {\\n //已认证\\n passed(1),\\n //审核中\\n submitted(2),\\n //审核未通过\\n unpassed(3),\\n //成功\\n successed(4);\\n\\n final int code;\\n const Status(this.code);\\n\\n static Status convert(int code) {\\n return Status.values.firstWhere((p) => p.code == code);\\n }\\n\\n @override\\n String toString() {\\n switch (this) {\\n case Status.passed:\\n return \\"已认证\\";\\n case Status.submitted:\\n return \\"审核中\\";\\n case Status.unpassed:\\n return \\"审核未通过\\";\\n case Status.successed:\\n return \\"成功\\";\\n default:\\n return \\"\\";\\n }\\n }\\n}\\n\\n// 判断状态\\nlogic.status == Status.successed\\n ? xxx\\n : xxx\\n\\n// 直接跟源数据类型判断\\nlogic.statusData == Status.successed.code\\n\\n// 显示状态的描述\\nStatus.convert(4).toString() // 输出成功\\n
","description":"Flutter 设计百科全书 Flutter、Dart 的语法和代码编写规范篇\\n前言\\n\\n我最喜欢的2句话,\\n 前者是 ”闻道有先后,术业有专攻“\\n 后者是 ”学无先后,达者为师“\\n\\n因为受到这两句话的影响,我这个人也是非常尊重分享知识的人。 同时也因为如此,有时候会跟别人产生争执,讨论某某技术是否足够好。\\n 但永远围绕的是知识而不是人。但是有些人攻击你,不仅攻击你,还要攻击你的知识。\\n\\n”好为人师一直是我的问题“\\n ”永远不要试图说服每一个人“\\n\\n本章内容充满了个人认知与理解,如果你有不同意见完全可以直接跳过。\\n\\n本章内容\\n推荐和不推荐的,做法、代码写法和组件\\n后续…","guid":"https://juejin.cn/post/7461825527058563110","author":"DreamMachine","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-20T07:41:34.061Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/768d747d2d89499b87d989cf95e72f50~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRHJlYW1NYWNoaW5l:q75.awebp?rk3s=f64ab15b&x-expires=1737963694&x-signature=bY9p%2FUiWWpljQRLtyO909dwrxAI%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"跟🤡杰哥一起学Flutter (三十一、UI实战-用户操作引导组件简易封装👣)","url":"https://juejin.cn/post/7461626575207612442","content":"🤡 组员对「Flutter自定义绘制」不太熟悉,不知从何下手整这个「用户操作引导组件」,所以这个活就落到杰哥身上了。
\\n👻 它是 App 中一个很常见的组件,用于 帮助用户 快速熟悉和掌握App的使用方法 (首次使用 & 新功能) ,提高用户体验和操作效率。表现形式通常为这两者的组合:
\\n具体UI效果如下 (图摘自:《APP UI结构:用户引导&提示》):
\\n💁♂️ 不难看出其中的关键技术难点为「如何实现特定组件的高亮」,思路有两:
\\n前者一天就不靠谱,搞起来麻烦而且不好复用,那就只有思路二咯,「混合模式」在《二十九、🖌玩转自定义绘制三部曲[上]》中已经详细讲解过了,这里就不复读了,😄 不了解Flutter自定义绘制的建议移步做下前置阅读:
\\n😄 接着由浅入深,先用 CustomPainter 写个最简单的Demo 实现 组件高亮,然后再延伸封装和扩展。
\\n💁♂️ 开始编写具体代码前,得先定下 弹窗 的方式,em... 选 Route(路由) 还是 Overlay(浮窗) ?😄 都可以,在《二十八、UI实战-玩转自定义弹窗💥》提到过,Flutter弹窗 实现的主要思路有两种:Stack 和 Overlay,Route 本质上也是基于 Overlay 实现的。🤡 但直接用 Overlay 有个小坑需要注意,它会置于 最顶层,在上面弹出窗口反而会显示到了它的下方:
\\n😶 这个坑的解法:
\\n\\n\\n给弹出的浮层内容视图 套一个Navigator,showDialog() 传参 useRootNavigator:false,查找 最近的 Navigator 而不是 根Navigator。
\\n
😄 个人建议:对于需要 一直处于页面最上层 的弹窗才用 Overlay,其它情况都用 Route。而在这里,用户引导操作页位于顶层没毛病,所以这里直接用 Overlay 来弹窗 😆。
\\n😶 最常规的获取方式:
\\n\\n\\n先为 高亮Widget 的 key 属性设置一个 GlobalKey,然后通过它访问Widget的 BuildContext,从而获得 RenderBox,进而获取到 Widget 的 尺寸和位置信息。
\\n
获取代码示例:
\\n😀 如果你不了解Flutter中的各种Key,可以先看下《十、进阶-玩转各种Key🔑》,GlobalKey 能够在 Widget树 中唯一标识一个Widget,通过它无需依赖于 Widget 的位置和层级结构,就能方便地访问到特定Widget。不 过有两点需要注意:
\\n😏 此方法 注册的回调 会在 当前帧绘制完毕后立即执行,简单点说 →「Widget 🌳 渲染完、屏幕刷新后」执行,可以在这里拿到「渲染后的布局信息」。有时我们还会在这里进行「状态更新」,这样做的好处是避免在 Widget 构建过程中做不必要的渲染或更新。
\\n😶 避免在 build() 中调用此方法,因为 build() 可能会被多次调用,推荐在 initState() 或其它不高频的回调中调用,如:
\\n获取代码实例:
\\n😀 然后,拿到 子Widget的尺寸和位置信息,一般都是需要向上传递给 父Widget 的,一种常见玩法 →「构造方法向下逐层传递回调」,具体代码示例:
\\n😅 可以是可以,但这只适合「嵌套层次较少」的场景,如果「嵌套了很多层」,写起来就巨麻烦,每一层Widget 都要定义一个这样的 回调属性,然后 构造方法 里传递这个值,🙃 耦合严重,改起来也头疼。
\\n😀 两种更好的做法是「向上发送通知」或「使用状态管理工具」,说下前者,用到 Flutter 提供的「Notification」机制,用法非常简单,具体代码示例:
\\n① 自定义 Notification 类
\\n② 子Widget发送通知
\\n③ 接收通知的父Widget套一个 NotificationListener 来监听指定类型的通知:
\\n运行后,父组件如约收到子组件发送的通知:
\\n上面 onNotification() 返回 true,表示消费调当前通知,不再继续向上传递。如果返回 false,通知还会继续传递,直到找到一个处理该通知的组件 (回调返回true) 或到达 根节点。😶 第二种状态管理就不用说了,可选项有很多,如:内置的 InheritedWidget 和 Provider、Riverpod、Bloc、Redux、GetX 等。
\\n核心:在 performLayout() 中通过 WidgetsBinding.instance.addPostFrameCallback() 添加回调监听。
\\n① 自定义 RenderProxyBox 类
\\n② 自定义 SingleChildRenderObjectWidget
\\n③ 需要获取尺寸和位置信息的子Widget套上:
\\n运行后,Flutter 布局确定组件大小和位置时会调用 performLayout(),然后获取到子组件的信息:
\\n😀 弄到高亮组件的尺寸和位置信息,接下来的绘制就简单了,根据这参数生成 Path(矩形) :
\\n然后创建一个混合模式为 BlendMode.dstOut 画笔,依次绘制半透明背景,再绘制高亮区域:
\\n然后这里用到了 canvas 的 saveLayer() 而非 save() ,说下两者的区别:
\\n😄 如果你这里不用 saveLayer() 你会发现不是高亮反而是 变黑,接着加一个弹窗方法,其中传递一个 移除浮层的回调,以便点击时关闭引导:
\\n最后加上 GlobalKey 并传递给 需要高亮的Widget:
\\n运行效果如下:
\\n👏 非常简单就实现了组件高亮的基本效果啦,源码【---\x3ec31/d2/custom_painter_demo.dart<---】。另外,除了 CustomPaint 组件支持 BlendMode (混合模式) 能实现高亮效果外,ShaderMask、ColorFiltered#ColorFilter.mode、DecoratedBox#BoxDecoration 等组件也可以,不过在实现用户操作引导组件这个场景,个人感觉支持 复杂自定义绘制 的 CustomPaint 更合适,灵活而且稳定可控。
\\n关键技术难点解决了,接着就是 封装,这里不太好做统一封装,毕竟不同APP想要的引导效果可能不太一样。众口难调🤷♀️,所以,这里只是 抛砖引玉,以我司项目为例,进行简单封装,读者可以借鉴思路,契合实际业务 自行扩展或者封装。通用套路:
\\nStack作为父容器 ,先盖一层 CustomPaint绘制高亮区域,然后就是 按需添加其它组件,通过在组件的外层套一个 Positioned 来调整组件的具体摆放位置。
\\n😶 通过观察,发现公司项目里用到的用户引导组件 非常简单,长这样:
\\n有 多个引导页,每个都是 一组三要素:高亮组件 + 文字说明 + 操作按钮 (上下一页),然后顺序只有上面的两种。😀 这完全可以用 纯自定义绘制 来实现,接着具体实现一波~
\\n暴露两个方法来调用 State 中切换上下一页的方法:
\\n属性为对应的 三要素 需要的参数,高亮加了「内边距」和「高亮形状」的支持 (矩形、圆角矩形、圆形),控制按钮传递一个控制器的回调,方便外部调用:
\\n除了定义一个 GuidePage 的引导页列表外,还定义了一个 引导结束的回调,毕竟,有时有引导完成执行相关操作的需求 (如弹窗)。State 中定义了两个 ValueNotifier,分别用于 保存当前引导页的索引 (当前第几页) 和 用户点击位置坐标 (判断按钮点击位置用到)。初始化了一个 UserGuideController 实例,定义了切换上/下一页的方法,build() 返回的Widget 套了一个 ValueListenableBuilder,当引导页索引变化时会触发Widget的刷新:
\\n依次是:
\\n再往下就是具体的自定义绘制逻辑了,定义一些用需要用的参数,将绘制过程拆解成三个方法:
\\n跟简单例子那里一样:
\\n这部分涉及到计算,会复杂一些,拆解为三个部分,① 文字相关值的计算:
\\n② 绘制文字标签的三角形:
\\n③ 绘制圆角背景 & 文字,返回一个y轴的坐标,后面绘制按钮要用到:
\\n这一部分同样涉及到计算:
\\n😶 还要判断下点击位置,触发对应按钮的回调:
\\n走的 Overlay(浮窗) ,传入回调中移除 OverlayEntry(浮层) :
\\n源码【---\x3ec31/d3/test_user_guide.dart<---】
\\n运行效果如下:
\\n👏 实现起来还是比较简单的,主要是计算需要花点时间,这里的 悬浮文字 和 按钮 也可以自己叠组件算。😀 活干完了,接着整下活,找几个效果写来玩玩~
\\nGithub仓库:kpaxian7/feature_guider
\\n🤔 就是 从当前高亮区域 切换到 上/下一个高亮区域 的过渡效果,用到动画,State 混入 SingleTickerProviderStateMixin,定义两个属性保存切换前后的 Path,初始化 动画控制器 和 动画曲线。
\\n切换上/下一页时 重置和启动动画:
\\n将动画通过构造方法传递到 LightPainter 中:
\\n写一个 插值两个Path 的方法 (矩形中心点 + 宽高变化):
\\n绘制高亮区域那里调下上面的方法 生成插值Path 再绘制:
\\n运行效果如下:
\\n😳 矩形 → 矩形 还好,但从 矩形 → 圆形或圆角矩形 (反过来也是) 最后的过渡明显是有些 突兀 的,因为动画的插值过程都是 绘制矩形,动画完成直接绘制结束Path。🤔 这里把动画执行时间拆解为 前后半段:
\\n修改后的代码:
\\n运行效果如下:
\\n👏 Nice,有个圆角变化的效果,丝滑了不少,源码【---\x3ec31/d4/a1/test_user_guide.dart<---】
\\nGithub 仓库:SimformSolutionsPvtLtd/flutter_showcaseview
\\n💁♂️ 加个循环执行的 AnimationController,在绘制文字时获取动画值,添加不断变化的偏移就好。关键代码:
\\n运行效果如下:
\\n👏 so easy,源码【---\x3ec31/d4/a2/test_user_guide.dart<---】
\\n🤡 本节,杰哥带着大伙手把手实现了「用户操作引导组件」的 简易封装,总体来说还是 非常简单 的。核心的技术难点无非「特定组件的高亮」,通过「混合模式-BlendMode」就能实现这样的效果,剩下就是一些自定义绘制的计算。例子里是 CustomPainter 一把梭,更贴合日常开发的通用套路:
\\nStack 作为父容器 ,先盖一层 CustomPaint 绘制高亮区域,然后按需添加 其它组件,通过在组件的外层套一个 Positioned 来调整组件的具体摆放位置。
\\n🤏 赶紧自己动手试试吧,年前最后一更,提前祝各位读者:春节快乐,所愿皆所成,多喜乐、长安宁🎉。
\\n本节配套源码:coder-pig/cp_study_flutter_demo
","description":"1. 引言 🤡 组员对「Flutter自定义绘制」不太熟悉,不知从何下手整这个「用户操作引导组件」,所以这个活就落到杰哥身上了。\\n\\n👻 它是 App 中一个很常见的组件,用于 帮助用户 快速熟悉和掌握App的使用方法 (首次使用 & 新功能) ,提高用户体验和操作效率。表现形式通常为这两者的组合:\\n\\n「高亮指引」→ 通过遮罩层或光标聚集突出界面的关键区域.\\n「悬浮提示」→ 特定功能或控件上显示简单的文字说明。\\n\\n具体UI效果如下 (图摘自:《APP UI结构:用户引导&提示》):\\n\\n💁♂️ 不难看出其中的关键技术难点为「如何实现特定组件的高亮」,思…","guid":"https://juejin.cn/post/7461626575207612442","author":"coder_pig","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-20T03:05:53.724Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/875d02e286324644ae89c99239fe81a8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=UfhGeyRCa5hsoGrJNtx3jupBmSU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a43c0e583c9047d59d629c3c195d37d4~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=1Iyi2MNnpwgB%2FQDLW8cRabxTaRo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2d52f66b97944bdab82d988e35b99716~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=FqUuV3z0ODH84qj0NJf1%2FZSoxek%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/9fe1fb9410e44732ac5bedba16a1459c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=JioOD6yoUNe7Ak0olPQ0gAuaWhw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/934b5fd0bbfc40908527849404f1e620~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=uLo5ekBeBb%2BCHIB2%2Blo6Olj%2Fl3Y%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/894a23bdb80849b08df07d051d4eb747~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=%2FQLpU3pQqkCrELyFB30cJGJWK%2Fw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a6c04d423f894593ba373bc5453675f7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=VInE%2BxfnH736SApJonW5d4MYUJI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/50840ef2ee1e4989b1bba53cdb22e6e3~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=dIa3O19c20CGCoyRPXtQakW4CA8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2c0448ab7fbf4fbdbbabe697f8609ed6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=FYS4ihwrQolmfHzXgZeJOHTp99g%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/bc21aa2b8e574167bfc3708f5934d22a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=f2piw96LTcdMTa4SNf8P5wum4yk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b606a524de7342fb8e7d3ea10fe1bb91~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=lg%2BV6d6L78Dhx3wFV8W5fRADoI4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/05da8470361a406fad172dd4a8e75d82~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=YMGKJQYMObtFW%2F98uwuTg6onqRY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e4c97e2cc9f84630a1571c02fe5322ba~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=yyR69fCR7ZzLSQuzk8x9OIicZ14%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/57493887b91544818100b605d59b7ef6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=Rm%2FuOGXqocouuKSVGRw8bFAuqsY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fab4aaa92dad40689c8a34f36d2ce022~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=sXcNyy%2B%2FSIc8ORXiyEml5QpEpiM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/94b5d37c57f54ebc88ab2d19edcf0e2a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=h%2BuUrvYanFE2cHldjoUf5wVnSyQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fbf5218558294b059f5d1b050c64017d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=5U%2FwPJduPINrlvILxabxm71r4bQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2b996ecadf3243c48acaa25de26e5d3b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=Q1zwmWRGkGyBNxfzNRjw89SyjJM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ebcec0b807434255b2bfbd845e79a63a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=zh4wRLDSvoc7uGng9GyaUQ2htpo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4f9d0dbb7d3c432a997305417dd16196~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=eV8jrfonqvoSYEXQQsRzCeZ1icI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5c4c12d751c84e8cb474cb331e7b44af~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=hpDaxs22Xnc9MH5%2FzBHlIAjMYaY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/8ab57228d33d48078ef293b42e87e2f5~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=4dLyHi1vGzGmLRuGv3UllX%2FnqEc%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b1f8a51d54974d56969725571eac2069~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=%2FJYUPvkJwn%2FnvCm7KsxHvrKo1gI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/813093a84cad483598c56dc31ecd907d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=9gYIeN5BjQycOQmpR4HVoDcGyHM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/09af88dd62584b65b60443f586bba711~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=EwWSkONyTGto%2B14e8tovzgwnhT8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e2159f9dac8f4235bfa29c3a1227bfe8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=PjIr%2Fj4PdwzbWwyGmDuns5jNU1A%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0cf944e7ff8c4c84ac0ad7acb160e797~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=jeeV40exgUQSnBJPRfTXa%2BX9Ob4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/06ff2fb3052c42afb4abf16bd4394e4e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=D%2FRoi76swG2l%2BhCFXUJRD8qbFfY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/00b93747e01b49c79c9232f2754f8703~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=5GRDMR2pr3Cwc6RuJmqfH7%2Bv6Vs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/861b74bb50e446a283450f8eedf16b85~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=VcyaU2YPawzq4e7aY64Nc5Qc1As%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2b4fd0a841c74dfe8c6450aed1336366~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=s%2B0dnGdByGLHNRomZ%2Fv1Qv7WlYw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4733876b0ef94e80adb6f04d2d3b3e67~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=7Eg10%2B3CH67xX%2BkcIpav1XLBBc0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/11c6cdfa9bbc4490a77649b9eb692908~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=7RJM%2FiaAkiPuOJ35x1Mh4AgbUZY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c34077560a4b424c93280e097db103a1~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=T7zXiOKDcNHMgKpHVi%2FDsl7DK9g%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/97e4b8f3b4fa40f1874f6178567cf078~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=L1S1Y%2FT0QGy5pY74t8SU5afKaAE%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e19bb37a2c68435582a01fbbcdefc7e3~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=Se3Ivj28OYubXYF3gek2gpH4mbI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5fb065fabca843fdb1c87446de2b4812~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=LIWqGCv8mAfteuu1W5SAex5Dees%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/186e438cb9774e5fadfa3c8c89508b23~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=FHxa0EEhQ%2BjzPsz82%2FjhQTS7juI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/f2ee3a5cbbbb4fa9b3837c7c7cc4cbcf~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=LFTmpgtM%2BDMxIoKHwxB%2FIllImL8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c076c7931a4d4eb48a69e4af16e1219a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=qL0rpiGrcI9rzKM8C25Mj9g2tmI%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1193fa29d0124faca4822f247249891a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=2L6lCxSlHgTB1OSTvkbEZNAGZ6E%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/25b9ed910c5141c1af015befd44c59a5~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=6zvD0YXf0hUrQNG%2B8Fqlnv87XGw%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/99eb6768cc864207b23fd279ebf89580~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=mfPSmK1cXZDT4kYCDq2BFhGjPgQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/0b28d7c3f99e4836bdba05de81179486~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=hhO9tTAknGOTpKkyqQzc0R9xmao%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/dcf99d6528454eef914c80665e766eec~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=z9K5xr6tgDiABMHYfz8r%2BLVTms0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d46e7435eb434aa9a12a0019fe613f41~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=nte6BSGZ8m2Bq7zOP%2BJ85bfPT38%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5f318b6ea5e141cf8b42be3ee7bc6345~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=K%2FAaBWEegXKdEg4wCdGkhwM47aY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/edfbbd7eda8d4daba685e593b4fb7344~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=Vk5by6Q%2BQg1yN6%2FnduqStztSkrk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/db55c723046c4ee3b40e301352bd307b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=9Ro5zEoiheZVSrOEFVktpHgFLJU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/963d74f2e87446ceb090db0d95abf51f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=pkxjVN6%2B%2Fg4KWAV4fJtgLvh9Fjo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/feb7139900e145ff9d55e610fa4ad201~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=oD44U8Mea7v%2FHsgz58pvX3k%2FLgo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1c455bb018034b8598573ca53baeea07~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=XjeJlRQ1I3l0JyecuEcQT2pMZ9A%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/186348e41bc94cc0b89708e1471ab002~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgY29kZXJfcGln:q75.awebp?rk3s=f64ab15b&x-expires=1737947152&x-signature=R6wNu1SqLu47qfYStmvLSZTaBzU%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["前端","Flutter","Android","HarmonyOS"],"attachments":null,"extra":null,"language":null},{"title":"系统化掌握Dart编程之函数","url":"https://juejin.cn/post/7461629792724631567","content":"函数 —— 编程世界的多功能工具
\\n函数就像是一个工具箱里的工具
。你有一把螺丝刀(函数
),它有一个特定的任务——拧螺丝(执行特定代码
)。每次你需要拧螺丝时,你不需要重新发明或制造一把新的螺丝刀,而是直接使用这把现成的螺丝刀。
在Dart
中,函数是一等公民
,它们能被保存在变量
中,能作为参数传递
及作为函数的返回值
。与所有Dart
运行的值一样,函数同样是对象
。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\n在Dart
中,函数是用于执行特定任务
的可重复使用
的代码块。一个函数由函数签名
和函数体
组成。
数据类型
。名字
。变量列表
,可以为空
。代码块
,可以有多条语句
。return
语句返回值。返回类型 函数名(参数类型1 参数名1, 参数类型2 参数名2, ...) {\\n // 函数体\\n // 可以有多条语句\\n // 可以使用 return 语句返回值\\n}\\n
\\n必需参数是调用函数时必须提供
的参数。
void greet(String name) {\\n print(\'Hello, $name!\');\\n}\\n\\ngreet(\'Alice\'); // 输出: Hello, Alice!\\n
\\n必填的
或可选的
。参数列表尾部
并用{}
包裹,并且按名称传递
,可以指定默认值但必须是编译时常量
。出现在命名参数前面
。void describePerson(String name, {int? age, String occupation = \'unknown\'}) {\\n print(\'Name: $name, Age: $age, Occupation: $occupation\');\\n}\\n\\ndescribePerson(\'Bob\', occupation: \'Engineer\', age: 30);\\n// 输出: Name: Bob, Occupation: Engineer, Age: 30\\n\\ndescribePerson(\'Charlie\', occupation: \'Artist\');\\n// 输出: Name: Charlie, Occupation: Artist, Age: null\\n
\\n必填的
或可选的
。参数列表尾部
并用 []
包裹,并且按位置传递
,可以指定默认值但必须是编译时常量
。出现在可选参数前面
。void describePerson(String name, [String occupation = \'unknown\', int? age]) {\\n print(\'Name: $name, Occupation: $occupation, Age: $age\');\\n}\\n\\ndescribePerson(\'David\', \'Developer\', 25);\\n// 输出: Name: David, Occupation: Developer, Age: 25\\n\\ndescribePerson(\'Eve\');\\n// 输出: Name: Eve, Occupation: unknown, Age: null\\n
\\n可为命名参数和位置参数设置默认值
,当未指定参数时,使用函数中指定的默认值
。
void describePerson(String name, {String occupation = \'unknown\', int age = 0}) {\\n print(\'Name: $name, Occupation: $occupation, Age: $age\');\\n}\\n\\ndescribePerson(\'Frank\');\\n// 输出: Name: Frank, Occupation: unknown, Age: 0\\n
\\n无返回值时返回类型为void
。
void greet(String name) {\\n print(\'Hello, $name!\');\\n}\\n
\\n函数通过 return
关键字返回一个值。如果没有显式返回值
,默认返回 null
。
int add(int a, int b) {\\n return a + b;\\n}\\n\\nprint(add(3, 4)); // 输出: 7\\n
\\n虽然 Dart
不直接支持返回多个值,但可以通过返回列表
或映射
来实现类似的效果。
List<int> addAndMultiply(int a, int b) {\\n return [a + b, a * b];\\n}\\n\\nvar result = addAndMultiply(3, 4);\\nprint(\'Sum: ${result[0]}, Product: ${result[1]}\'); // 输出: Sum: 7, Product: 12\\n
\\n匿名函数是没有名字的函数
,通常用于作为参数传递给其他函数
或立即执行
。它们非常适合用于一次性的任务
或回调函数
。
void executeFunction(void Function() func) {\\n func();\\n}\\n\\nexecuteFunction(() {\\n print(\'This is an anonymous function.\');\\n});\\n
\\n对于只有一行代码
的匿名函数,Dart
提供了更简洁的箭头语法(=>
),它自动返回表达式的值
。
/// 使用箭头语法\\nvar add = (int a, int b) => a + b;\\nprint(add(3, 4)); // 输出: 7\\n
\\n匿名函数可以接受参数,并且这些参数可以在函数体内使用。
\\n// 带参数的匿名函数\\nvoid greetPeople(List<String> names, void Function(String) greet) {\\n for (var name in names) {\\n greet(name);\\n }\\n}\\n\\ngreetPeople([\'Alice\', \'Bob\'], (name) {\\n print(\'Hello, $name!\');\\n});\\n
\\n匿名函数还可以作为另一个函数的返回值
,这在构建高阶函数时非常有用。
// 返回匿名函数\\nFunction createMultiplier(int multiplier) {\\n return (int number) => number * multiplier;\\n}\\n\\nvar doubleIt = createMultiplier(2);\\nprint(doubleIt(5)); // 输出: 10\\n
\\n闭包是指一个函数对象,它可以记住并访问其创建时的外部作用域中的变量
,即使这个函数在其外部作用域之外被调用。换句话说,闭包“捕获”
了其定义时的环境状态。
// 创建一个闭包\\nFunction makeCounter() {\\n var count = 0; // 外部作用域中的变量\\n return () {\\n count++; // 访问并修改外部变量\\n print(count);\\n };\\n}\\n\\nvar counter = makeCounter();\\ncounter(); // 输出: 1\\ncounter(); // 输出: 2\\ncounter(); // 输出: 3\\n
\\n闭包的一个关键特性是它可以捕获并保存外部作用域中的变量
。这意味着即使外部函数已经返回
,闭包仍然可以访问这些变量
。
// 捕获多个外部变量\\nFunction createAdder(int base) {\\n return (int addend) => base + addend;\\n}\\n\\nvar addTen = createAdder(10);\\nprint(addTen(5)); // 输出: 15\\nprint(addTen(8)); // 输出: 18\\n
\\n匿名函数可以成为闭包
,当它们引用了外部作用域中的变量时。这种组合使得代码更加简洁和强大。
// 匿名函数作为闭包\\nvoid executeWithDelay(void Function() action, int delay) {\\n Future.delayed(Duration(seconds: delay), action);\\n}\\n\\nexecuteWithDelay(() {\\n print(\'Executed after delay\');\\n}, 2); // 两秒后输出: Executed after delay\\n
\\n异步操作
、事件处理
等场景中,闭包常用于定义回调函数。Future<void> fetchData() async {\\n print(\'Fetching data...\');\\n await Future.delayed(Duration(seconds: 2));\\n print(\'Data fetched.\');\\n}\\n\\nvoid main() async {\\n fetchData().then((_) {\\n print(\'Processing data...\');\\n });\\n}\\n
\\n参数传递
给高阶函数,或者作为高阶函数的返回值
。void performOperation(int a, int b, Function operation) {\\n print(operation(a, b));\\n}\\n\\nperformOperation(3, 4, (x, y) => x + y); // 输出: 7\\n
\\n私有变量和方法
,因为外部无法直接访问闭包内部的变量。class Counter {\\n int _count = 0;\\n\\n void Function() get increment => () {\\n _count++;\\n print(_count);\\n };\\n}\\n\\nvoid main() {\\n var counter = Counter();\\n var increment = counter.increment;\\n increment(); // 输出: 1\\n increment(); // 输出: 2\\n}\\n
\\n高阶函数是指可以接受函数作为参数或返回值
的函数。这种特性使得代码更加简洁
和抽象
。
void performOperation(int a, int b, Function operation) {\\n print(operation(a, b));\\n}\\n\\nperformOperation(3, 4, (x, y) => x + y); // 输出: 7\\nperformOperation(3, 4, (x, y) => x * y); // 输出: 12\\n
\\nDart
提供了许多内置的高阶函数,如 map
、where
和 reduce
,用于处理集合数据。
List<int> numbers = [1, 2, 3, 4, 5];\\n\\n// 使用 map 将每个元素加倍\\nvar doubled = numbers.map((n) => n * 2).toList();\\nprint(doubled); // 输出: [2, 4, 6, 8, 10]\\n\\n// 使用 where 过滤偶数\\nvar evenNumbers = numbers.where((n) => n % 2 == 0).toList();\\nprint(evenNumbers); // 输出: [2, 4]\\n\\n// 使用 reduce 计算总和\\nvar sum = numbers.reduce((sum, element) => sum + element);\\nprint(sum); // 输出: 15\\n
\\n异步编程中,允许编写非阻塞
的代码。使用 async
和 await
关键字可以使异步代码看起来像同步代码一样简单。
Future<void> fetchData() async {\\n print(\'Fetching data...\');\\n await Future.delayed(Duration(seconds: 2));\\n print(\'Data fetched.\');\\n}\\n\\nvoid main() async {\\n fetchData();\\n print(\'Doing other work...\');\\n}\\n
\\nStream
类型用于处理一系列异步事件
,如文件读取
或网络请求
。
import \'dart:async\';\\n\\nvoid listenToStream() {\\n Stream<int> stream = Stream.periodic(Duration(seconds: 1), (count) => count)\\n .take(5);\\n\\n stream.listen((value) {\\n print(\'Received value: $value\');\\n }, onDone: () {\\n print(\'Stream completed.\');\\n });\\n}\\n\\nvoid main() {\\n listenToStream();\\n}\\n
\\n回调函数是一个作为参数传递给另一个函数
的函数,并在这个函数执行过程中或之后被调用。它允许你指定一段代码,在特定条件满足时执行。回调函数通常用于异步操作
、事件处理
和数据处理
中。
示例代码:
\\n// 简单的回调函数示例\\nvoid executeFunction(void Function() callback) {\\n print(\'Executing function...\');\\n callback(); // 调用回调函数\\n}\\n\\nexecuteFunction(() {\\n print(\'Callback executed!\');\\n});\\n
\\n实际用途
\\n可以将一个函数作为参数传递
给另一个函数,这样可以在需要的时候调用这个函数。
// 定义一个接受回调函数的函数\\nvoid fetchData(String url, void Function(String) onData) {\\n // 模拟网络请求\\n print(\'Fetching data from $url...\');\\n String data = \'Some data\';\\n onData(data); // 当数据准备好时调用回调函数\\n}\\n\\n// 使用回调函数处理数据\\nfetchData(\'https://example.com\', (data) {\\n print(\'Received data: $data\');\\n});\\n
\\n在异步操作中,回调函数通常用于处理操作完成后的工作。Dart
提供了 Future
和 async/await
来简化异步编程。
import \'dart:async\';\\n\\n// 模拟异步操作\\nFuture<void> fetchDataAsync(String url, void Function(String) onData) async {\\n print(\'Fetching data from $url...\');\\n await Future.delayed(Duration(seconds: 2)); // 模拟网络延迟\\n String data = \'Some data\';\\n onData(data);\\n}\\n\\nvoid main() async {\\n fetchDataAsync(\'https://example.com\', (data) {\\n print(\'Received data: $data\');\\n });\\n}\\n
\\n回调函数不仅可以处理成功的情况,还可以处理错误。通过传递多个回调函数
来分别处理成功
和失败
的情况。
void fetchDataWithErrorHandling(\\n String url, void Function(String) onData, void Function(String) onError) {\\n print(\'Fetching data from $url...\');\\n bool success = false; // 模拟失败情况\\n\\n if (success) {\\n String data = \'Some data\';\\n onData(data);\\n } else {\\n String error = \'Failed to fetch data\';\\n onError(error);\\n }\\n}\\n\\nvoid main() {\\n fetchDataWithErrorHandling(\\n \'https://example.com\',\\n (data) => print(\'Received data: $data\'),\\n (error) => print(\'Error: $error\'),\\n );\\n}\\n
\\n取决于同一类问题的更小子集
。函数调用自身
的函数。递归函数必须有一个明确的终止条件
,否则会导致无限递归
。解决方案是一致的
(有规律的)。缩减
(子集
),而且最后会缩减至无需递归
。内层函数
调用(子集处理
)完成
,外层函数才能算调用完成
。递
归
局部变量
(以及方法参数
)并未消失
,归的时候可以用到。int factorial(int n) {\\n if (n == 0 || n == 1) {\\n return 1;\\n } else {\\n return n * factorial(n - 1);\\n }\\n}\\n\\nvoid main() {\\n int num = 5;\\n print(\'Factorial of $num is ${factorial(num)}\');\\n}\\n\\n//详细执行流程说明\\n//1.初始执行\\nint factorial(int 5) {\\n if (n == 0 || n == 1) {\\n return 1;\\n } else {\\n //2.第一次递归调用\\n return 5*factorial(int 4) {\\n if (n == 0 || n == 1) {\\n return 1;\\n } else {\\n //3.第二次递归调用\\n return 4*factorial(int 3) {\\n if (n == 0 || n == 1) {\\n return 1;\\n } else {\\n //4.第三次递归调用\\n return 3*factorial(int 2) {\\n if (n == 0 || n == 1) {\\n return 1;\\n } else {\\n //5.第四次递归调用\\n return 2*factorial(int 1) {\\n if (n == 0 || n == 1) {\\n return 1;\\n } else {\\n return n* factorial(n-1);\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n}\\n\\n
\\n1、初始调用:factorial(5)
;
2、第一次递归调用:return 5 * factorial(4)
;
3、第二次递归调用:return 4 * factorial(3)
;
4、第三次递归调用:return 3 * factorial(2)
;
5、第四次递归调用:return 2 * factorial(1)
;
6、基本终止条件:return 1
;
7、回溯计算:
\\nfactorial(2)
返回 2 * 1 = 2。
factorial(3)
返回 3 * 2 = 6。
factorial(4)
返回 4 * 6 = 24。
factorial(5)
返回 5 * 24 = 120。
8、最终输出: Factorial of 5 is 120
factorial
函数通过递归调用自身
来计算阶乘。n
为 0
或 1
时,递归终止
。Dart
中的函数是一个强大且灵活的工具,支持多种特性,包括参数管理
、匿名函数
、闭包
、高阶函数
和异步编程
等。通过合理利用这些特性,有助于我们编写出更加简洁
、高效
、易维护及扩展
的代码。
\\n","description":"前言 函数 —— 编程世界的多功能工具\\n\\n函数就像是一个工具箱里的工具。你有一把螺丝刀(函数),它有一个特定的任务——拧螺丝(执行特定代码)。每次你需要拧螺丝时,你不需要重新发明或制造一把新的螺丝刀,而是直接使用这把现成的螺丝刀。\\n\\n在Dart中,函数是一等公民,它们能被保存在变量中,能作为参数传递及作为函数的返回值。与所有Dart运行的值一样,函数同样是对象。\\n\\n操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。\\n\\n一、函数的基本概念\\n1.1、定义\\n\\n在Dart中,函数是用于执行特定任务的可重复使用的代码块。一个函数由函数签名和函数体组成。\\n\\n1…","guid":"https://juejin.cn/post/7461629792724631567","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-20T02:58:16.582Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fd0f21ff7e6a4a08b5c2fa00f5e36c3f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1737946696&x-signature=DOcH%2BqD7d178kN%2B4h2f7oIoQx40%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a4bbdbe17598485f83c852aa3cf67fc3~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1737946696&x-signature=kQYiIO9zGjIavmwEHTZlRgwg5rU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2b990bea7c864036aa67e271df1e6117~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1737946696&x-signature=WWMfTHSjLrH3UjfiLW5g5GQkjMk%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"深入 Flutter 和 Compose 的 PlatformView 实现对比,它们是如何接入平台控件","url":"https://juejin.cn/post/7461597205342928936","content":"码字不易,记得 关注 + 点赞 + 收藏 + 评论
\\n
在上一篇《深入 Flutter 和 Compose 在 UI 渲染刷新时 Diff 实现对比》发布之后,收到了大佬的“催稿”,想了解下 Flutter 和 Compose 在 PlatformView
实现上的对比,恰好过去写过不少 Flutter 上对于 PlatformView
的实现,这次恰好可以用来和 Compose 做个简单对比:
其实 Flutter 在 Android 上的 PlatformView
实现过去已经聊过好多次了,Flutter 作为完全脱离平台渲染树的独立 UI 库,它在混合开发的 PlatformView
实现可以说是“历经沧桑” 。
\\n\\n既然前面我们讲过很多次,这里主要就是简单介绍下,方便和 Compose 做个对比,感兴趣的可以去看后面的详细链接。
\\n
在 Flutter 上是通过 AndroidView
接入平台控件,目前活跃在 Android 平台的 PlatformView
支持主要有以下三种:
为什么会有这么多不同模式支持?因为主要是随着技术推进和适配场景,PlatformView
的适配需求都在更新,但是新来的又不能完全提前之前的方案,所以就导致实现都并存下来。
VD简单来说就是使用 VirtualDisplay 渲染原生控件到内存,然后利用 id 在 Flutter 界面上占用一个相应大小的位置,最后通过 id 关联到 Flutter Texture 里进行渲染。
\\n问题也很明显,因为控件不会真实存在渲染的位置,可以不严谨理解,它只是内存里 UI 的“镜像”显示,或者说“副屏镜像”,所以此时的点击和对原生控件的操作,其实都是需要由 Flutter 这个 View 进行二次转发到原生再回到 Flutter 。
\\n另外因为控件是渲染在内存里,所以和键盘交互需要通过二级代理处理,容易产生各种键盘输入和交互的异常问题,特别是 WebView
场景。
\\n\\n当然,现在的 VD 已经比初始的时候好很多,并且还在兼容“服役”。
\\n
1.2 版本开始支持 HC 模式,这个版本就是直接把原生控件「覆盖」在 FlutterView 上进行堆叠,简单来说就是 HC 模式会直接把原生控件通过 addView
添加到 FlutterView
上 。如果出现 Flutter Widget 需要渲染在 Native Widget 上,就采用新的 FlutterImageView
来承载新图层。
比如在 Layout Inspector,HC 模式可以看出来各种原生布局的边界绘制:
\\n而如下图所示,其中蓝色的文本是原生的 TextView
,红色的文本是 Flutter 的 Text
控件,在中间 Layout Inspector 的 3D 图层下可以清晰看到:
TextView
是被添加在 FlutterView
之上,并且把没有背景色的红色 RE 遮挡住了TextView
之上,所以这时候多一个 FlutterImageView
,它用于承载需要显示在 Native 控件之上的纹理,从而达 Flutter 控件“真正”和原生控件混合堆叠的效果。\\n\\n这里的
\\nFlutterImageView
,其实还有一个作用,就是为了解决动画同步和渲染。
当然,这样带来了一个问题,因为此时原生控件是直接渲染,所以需要在原生的平台线程上执行,纯在 Flutter 的 UI 线程就存在线程同步问题,所以在此之前一些场景下会有画面闪烁 bug 。
\\n虽然这个问题最后也通过类似线程同步实现解决,但是也带来一定程度的性能开销,另外在 Android 10 之前还会存在 GPU->CPU->GPU的性能损耗,所以 HC 属于会性能开销较大,又需要原生控件特性的场景。
\\n3.0 版本开始支持 TLHC 模式,最初的目的是取代上面这两种模式,可惜最终共存下来,该模式下控件虽然在还是布局在该有的位置上,但是其实是通过一个 FrameLayout
代理 onDraw
然后替换掉 child 原生控件的 Canvas
来实现混合绘制。
\\n\\n所以看到此时上图
\\nTextView
里没有了内容,因为TextView
里的Canvas
被替换成 Flutter 在内存里创建的Canvas
。
其实 TLHC 流程上和 VD 基本一样,简单对比 VirtualDisplay 和 TextureLayer 的实现差异,可以看到主要还是在于原生控件纹理的提取方式上 :
\\n从上图我们可以得知:
\\n从 VD 到 TLHC, Plugin 的实现是可以无缝切换,因为主要修改的地方在于底层对于纹理的提取和渲染逻辑;
\\n以前 Flutter 中会将 AndroidView
需要渲染的内容绘制到 VirtualDisplays
,然后在 VirtualDisplay
对应的内存中,绘制的画面就可以通过其 Surface
获取得到;现在 AndroidView
需要的内容,会通过 View 的 draw
方法被绘制到 SurfaceTexture
里,然后同样通过 TextureId
获取绘制在内存的纹理 ;
从这个简单流程上看,这里面的关键就在于 super.draw(surfaceCanvas);
,给 Android 的 View “模拟” 出来工作环境,然后通过“替换” Canvas 让 View 绘制需要的 Surface 上合成:
那 TLHC 有什么问题?因为它是通过“替换” Canvas 来得到 UI ,但是这种实现天然不支持 SurfaceView
等场景,因为 SurfaceView
是自己独立的 Surface 和 Canvas,所以通过 parent 替换 Canvas
的实现并不支持。
所以目前的 PlatformVIew
支持上的结果:
SurfaceView
,那么就会“降级”使用 VD 来适配initExpensiveAndroidView
接口强行使用 HCCompose 的 PlatformView 原理这里可以详细聊聊,这个目前的资料不多,比较有聊的价值。
\\n众所周知,Jetpack Compose 虽然是 Android 平台的全新 UI 开发框架,但是它的 UI 渲染树和「传统 xml View 控件」是“不直接兼容”的,Compose 属于独立的 UI 库,它的 UI 模式更接近 Flutter ,但是 @Composable 函数又不是和 Flutter 一样 return ,在实际工作中,Compose 代码在编译时会给 @Composable 函数添加 Composer
参数 ,而实际的 UI Node Tree 等的创建,都是从“隐藏”的 Composer
开始:
\\n\\n\\n
所以,一旦你需要在 Jetpack Compose 里接入一个原生控件,你就需要用到 PlatformView 的相关实现,PlatformView 本质上就是把「传统 xml View 控件」渲染进 Compose 渲染树里,而在 Compose 在 Android 平台,使用的就是 AndroidView
:
@Composable\\nfun CustomView() {\\n var selectedItem by remember { mutableStateOf(\\"Hello from View\\") }\\n\\n // Adds view to Compose\\n AndroidView(modifier = Modifier.fillMaxSize(), // Occupy the max size in the Compose UI tree\\n factory = { context ->\\n // Creates view\\n TextView(context).apply {\\n text = \\"Hello from View\\"\\n textSize = 30f\\n textAlignment = TextView.TEXT_ALIGNMENT_CENTER\\n }\\n }, update = { view ->\\n // View\'s been inflated or state read in this block has been updated\\n // Add logic here if necessary\\n\\n // As selectedItem is read here, AndroidView will recompose\\n // whenever the state changes\\n // Example of Compose -> View communication\\n view.text = selectedItem\\n })\\n}\\n
\\n如上代码所示,通过 AndroidView
我们可以把一个 Android 传统的 TextView
添加到 Compose 里,当然这没什么实际意义,只是作为一个简单例子。
渲染之后,我们可以看到在 Layout Inspector 的 Component tree 里并没有 TextView
,因为它只是被渲染到 Compose 里,但是它其实并不是 “直接” 存在于 Compose 的 LayoutNode ,它只是“依附”在 AndroidView
。
想知道 AndroidView
的工作原理,我们需要看它的 factory
实现,从源码我们可以看到,它主要是通过 ViewFactoryHolder
创建了一个代理 layoutNode
来“进入” Compose 渲染树:
而 Android 上 ViewFactoryHolder
的实现主要在它基类 AndroidViewHolder
,这里可以看到 AndroidViewHolder
那可是一个“实实在在”的传统 ViewGroup
实现 :
\\n\\n我们可以假设,我们前面的传统
\\nTextView
,在AndroidView
内部实际上就是被添加到AndroidViewHolder
这个ViewGroup
里 ,而且这里还有一个Owner
,从命名上也很“关键”。
带着这两个问题,我们继续看,首先我们在 AndroidViewHolder
里可以看到有 layoutNode
的实现,也就是其实这个 Holder ,它既是传统 ViewGroup
,又具备 Compose 里的 layoutNode
实现:
通过查看 layoutNode
的实现,我们可以看到:
layoutNode
被 onAttach
到 Compose 布局里的时候,会执行 addAndroidView
,其实这里的 addAndroidView
内部,就是一个 ViewGroup
的 addView
操作onDetach
时执行 removeAndroidView
,内部也就是 ViewGroup
的removeViewInLayout
MeasurePolicy
处理布局,简单说就是将 Compose 的布局状态同步到 AndroidViewHolder
这个 ViewGroup
去布局,给 「传统 XML View」“模拟” 布局环境。所以我们可以看到,AndroidViewHolder
类似一个“中转站”,它将 Compose UI 的生命周期和测绘布局状态同步到传统 ViewGroup 控件,从而给添加进来的 TextView
“模拟” 出布局和绘制环境,大概可以总结:
\\n\\n\\n
AndroidViewHolder
类似于 Compose 代理 Node,它 Compose 中的 UI 环境“模拟”到ViewGroup
中,通过控制ViewGroup
的绘制与布局来控制我们的「传统 xml View 控件」。
那么 AndroidViewHolder
肯定就是从 onAttach
开始进入 Compose 的 LayoutNode 体系工作,这里关键在于 AndroidComposeView
的这个操作:
(owner as? AndroidComposeView)?.addAndroidView(this, layoutNode)\\n
\\n这里又冒出来一个新对象 AndroidComposeView
,它就是我们前面所说的 owner
,那它又是什么?
我们看 AndroidComposeView
的源码,可以看到 AndroidComposeView
同样是一个 ViewGroup
,它的内部主要是有一个 AndroidViewsHandler
的 ViewGroup
在处理 AndroidViewHolder
,比如前面的 addAndroidView
就是将 Holder 添加到 Handler :
那到这里就有三个东西:
\\nAndroidComposeView
AndroidViewsHandler
AndroidViewHolder
它们都是传统 ViewGroup
的实现,且关系大概如下所示:
那么到这里,流程上我们应该就清晰了,我们只需要搞清楚 AndroidComposeView
是什么,来自哪里,然后往下,大概就可以理清它的实现。
我们通过 AndroidComposeView
内部有个 root
节点的实现,可以猜测它应该是一个顶层节点,所以我们直接从顶部开始找:
我们从 Activity 开始往下找,经过几个简单调整,就可以在 AbstractComposeView.setContent
找到创建 AndroidComposeView
的地方:
因为 AbstractComposeView
的实现是 ComposeView
,所以可以看到:
\\n\\n\\n
AndroidComposeView
是在初始时被ComposeView
创建并addView
,然后 Composition 里 UiApplier 的 root 节点就是AndroidComposeView
。
所以这就是为什么前面我们那个 owner
为什么是 AndroidComposeView
的来源,然后往下就是 AndroidViewsHandler
,它主要就是持有所有 Holder ,然后根据调用给它的 children 执行各种布局和绘制操作:
所以我们就知道了:
\\nAndroidComposeView
,它是一个 root LayoutNodeAndroidComposeView
内部的 AndroidViewsHandler
会通过一个 hashMap 去触发和管理 children Holder 的布局和重绘AndroidViewHolder
是一个代理 LayoutNode ,同时它将 Compose UI 的生命周期和测绘布局状态同步到传统 ViewGroup 控件大概会是下面这样的结构,但是它虽然被 addView
到 ViewGroup
里,但是它并不会直接渲染在 ViewGroup
里 ,而是「被代理渲染」到 LayoutNode 对应的 Scope 里 :
比如我们接入了两个 SurfaceView
到 Compose ,如果我们打印传统布局结构,大概可以看到这样的一个结果,:
\\n\\n这里举例的
\\nSurfaceView
后面会顺便聊聊 。
最后就是绘制,知道流程后,我们直接看回 AndroidViewHolder
里的 layoutNode 实现,在这里有一个来自 drawBehind
的 canvas
:
一般情况下,drawBehind
修饰符可以想任何可组合函数后面绘制内容时,例如:
Text(\\n \\"Hello Compose!\\",\\n modifier = Modifier\\n .drawBehind {\\n drawRoundRect(\\n Color(0xFFBBAAEE),\\n cornerRadius = CornerRadius(10.dp.toPx())\\n )\\n }\\n .padding(4.dp)\\n)\\n
\\n而这里的 Canvas 是来自 DrawScope
, DrawScope
属于一个针对 Canvas 接口的高级封装,内部 Canvas 的底层支持还是原生平台的 Canvas ,因为 Compose 有多平台支持,而 Android 平台对应的就是 AndroidCanvas
对象,这里是通过 canvas.nativeCanvas
获取到的,就是 android.graphics.Canvas
对象,也就是传入了一个 Android 原生 Canvas 。
流程上如下图所示,这里的核心其实就是:将 Compose 里 drawBehind
的 Canvas 传递给「传统 XML View」,这样在绘制时用的就是来自 Compose 体系 drawBehind
的 Canvas 链条:
所以这里可以看到,在绘制的时候,采用的其实就是通过 AndroidViewHolder 这个 ViewGroup
作为 Parent 来 “替换” 掉作为 child 的传统 View 的 Canvas ,让 View 的内容通过 Compose 的 Canvas 绘制到它所在的 LayoutNode 上。
另外, pointerInteropFilter
也会处理手势事件,用户在当前 LayoutNode 交互的手势,会被发送到 AndroidViewHolder
这个 ViewGroup
,从而触发传统 Androd 控件的点击等效果。
最后,在 navigate 切换的时候, AndroidViewHolder
也会相对应的被 add/remove 。
\\n\\n从这角度看,Compose 的 PlatformView 实现和 Flutter 的 TextureLayer 理念很接近,都是通过“替换” Canvas 和“模拟”布局环境来实现 View 接入,但是,它们又有本质不同,这个不同就体现在
\\nSurfaceView
。
因为 SurfaceView
是有自己独立的 Surface 和 Canvas ,所以它是无法被 Parent 的 Canvas “替换” ,这也是 Flutter 里 TLHC 的问题,但是在 Compose 里,你会发现 SurfaceView
在 AndroidView
里可以正常工作:
\\n@Composable\\nfun ContentExample() {\\n Box() {\\n ComposableSurfaceView(Modifier.size(100.dp))\\n Text(\\"Compose\\", modifier = Modifier\\n .drawBehind {\\n drawRoundRect(\\n color = Color(0x9000FFFF), cornerRadius = CornerRadius(10.dp.toPx())\\n )\\n }\\n .padding(vertical = 30.dp))\\n }\\n}\\n\\n@Composable\\nfun ComposableSurfaceView(modifier: Modifier = Modifier) {\\n AndroidView(factory = { context ->\\n SurfaceView(context).apply {\\n layoutParams = ViewGroup.LayoutParams(\\n ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT\\n )\\n holder.addCallback(MySurfaceCallback())//添加回调\\n }\\n\\n }, modifier = modifier)\\n}\\n\\nclass MySurfaceCallback : SurfaceHolder.Callback {\\n private var _canvas: Canvas? = null\\n override fun surfaceCreated(p0: SurfaceHolder) {\\n _canvas = p0.lockCanvas()\\n _canvas?.drawColor(android.graphics.Color.GRAY)//设置背景颜色\\n _canvas?.drawCircle(100f, 100f, 50f, Paint().apply {\\n color = android.graphics.Color.YELLOW\\n })//绘制一个红色的图像\\n p0.unlockCanvasAndPost(_canvas)\\n }\\n}\\n
\\n可以看到,上面代码的 SurfaceView
灰色的背景和黄色的圆都被渲染出来,另外 Text
的 Compose 文本也正常带着背景色覆盖显示在 SurfaceView
上:
有没有觉得奇怪,为什么 SurfaceView
的 Canvas
没有被替换,但是 SurfaceView
的内容和层级却又正常渲染在了 Compose UI 树里?
其实道理很简单,虽然 Compose 和「传统 XML View」 是两套 UI 框架,但是 Compose 的本质还是 Android 里面的 View ,也就是它依旧在 View 体系的范畴内:
\\n\\n\\n依赖 Android 的 Surface、Window、SurfaceFlinger 体系去渲染。
\\n
我们简单回忆下 SurfaceView
是怎么工作的?
Android 里控件基本都是以 View
为基类,所有可见 View 对象都会渲染到一个 Surface ,这个 Surface 来自 SurfaceFlinger ,也就是当前 Window 下。
尽管 SurfaceView
继承自类View,但是它有自己独立的 Surface,是直接提交到 SurfaceFlinger
这也是 SurfaceView
会有自己独立 Canvas 的原因,简单说它是一个可以绘制到 Surface 并直接输出到 SurfaceFlinger 的视图。
一般情况下, SurfaceView
在其 Window 层上始终是一个透明的 Rect,类似于 SurfaceView
在其窗口中打了一个洞 , 并且默认情况下,SurfaceView
的 Z 顺序始终低于其附加的 Window 层,也就是 SurfaceView
的 Surface 是在默认 Surface 的下面。
而最终渲染时,SurfaceFlinger 会将 SurfaceView
的图像层和 Window 的图像层叠加在一起。
那么回到 Compose,Compose 的底层还是一个传统的 View ,所以它还是依赖 View 的 Surface 和 SurfaceFlinger,也就是:
\\n\\n\\nCompose 和「传统 View」 共用同一个 Window 和
\\nDecorView
,AndroidView
作为一个桥接节点,将「传统 View」 “插入” 到 Compose 的布局树中,虽然SurfaceView
绘制内容是独立的,但在屏幕上是共享一个Window
,SurfaceFlinger
依然会统一管理窗口合成。
如给上方 SurfaceView
的代码加上 setZOrderOnTop(true)
,就会看到 Compose 的 Text
看不到了,因为此时的 Z 层面发生了变化:
这就是 Compose 和 Flutter 在 AndroidView
上最大的区别:
\\n\\nFlutter 是完全脱离了渲染体系,但是 Compose 还是在 View 体系内,所以
\\nSurfaceView
不会是问题,甚至官方还推出了SurfaceView
对应的 Compose 封装 AndroidExternalSurfaceScope 。
只是说,在 「传统 XML View」 体系中,每个 View 会有一个 RenderNode,而 Compose 中“一般”只有 ComposeView 一个 RenderNode,也就是传说的单页面状态,而 Compose 内部最终就是将自己的 LayoutNode 通过 Composer 组合完成后塞到 RenderNode 里面。
\\n可以看到,在 Android 平台上, Flutter 和 Compose 在最终实现思路很接近,大家都叫 AndroidView
,理念都是“模拟”环境和“替换” Canvas ,但是在 Android 平台上 Compose 有着原生 View 体系的优势,所以它对 SurfaceView
的支持更友好。
本章内容需要结合我其他Theme相关文章观看。
\\n自定义按钮需要考虑手指触控面积和透明穿透设置。
GestureDetector(\\n behavior: HitTestBehavior.opaque,\\n onTap: () {\\n \\n },\\n child: const Padding(\\n padding: EdgeInsets.all(8.0),\\n child: Text(\\"手势捕获\\"),\\n ),\\n);\\n\\n///机器翻译\\n///如何在命中测试中表现。\\nenum HitTestBehavior {\\n///尊重孩子的目标会在自己的范围内接受事件\\n///只有当他们的一个孩子被命中测试击中时。\\n deferToChild,\\n\\n///不透明的目标可以被命中测试击中,导致它们都受到\\n///事件在其范围内,并防止其背后的目标从视觉上\\n///也接收事件。\\n opaque,\\n\\n///半透明目标都在其范围内接收事件并允许\\n///他们身后的目标也可以接收事件。\\n translucent,\\n}\\n
\\nIconButton(\\n icon: Image.asset(\\"图标.png\\"),\\n onPressed: () {},\\n),\\n
\\nGestureDetector(\\n behavior: HitTestBehavior.opaque,\\n child: Image.asset(\\"按钮.png\\"),\\n),\\n
\\n纯文字的按钮,默认是有内边距的(不推荐 GestureDetector + Text 实现)
\\n(不推荐使用 GestureDetector + Container + Text 实现)
\\n比FilledButton增了一个高度elevation,不推荐使用,除非你要实现MD设计风格。
\\n外边框按钮,外面一条线。
\\n自定义图标按钮 推荐 IconButton + Image(svg等)
\\n(不推荐使用 GestureDetector + Image 实现,除非你的贴图自带Padding)
Theme 和 ThemeData 请查看我另外的文章
\\ntextButtonTheme: TextButtonThemeData(\\n style: ButtonStyle(\\n shape: \\n padding: \\n elevation: \\n minimumSize: \\n overlayColor: \\n foregroundColor: \\n backgroundColor: \\n )\\n)\\n
\\n\\n\\n常用属性(基本上每一种按钮都拥有这些属性,但是选择特性最符合你需求的按钮去修改样式能够事半功倍。)
\\n
// 五角星\\nStarBorder\\n// 圆型\\nCircleBorder\\n// 胶囊按钮\\nStadiumBorder \\n// 圆角按钮\\nRoundedRectangleBorder(\\n borderRadius: BorderRadius.circular(10)\\n)\\n
\\npadding 按钮的内边距,可以设置为EdgeInsets.zero
\\nelevation 按钮的高度,估计只有ElevatedButton有效(存疑)
\\nminimumSize 按钮的最小体积,按钮都有一个默认值,如果你让按钮变得很小,那么你需要修改这个字段以及tapTargetSize字段,注意 按钮较小时,可能会导致内部Text无法 正常渲染。
\\noverlayColor 水波纹颜色
\\nforegroundColor 前景色,通过这个属性可以设置 按钮中的Text 颜色,而不是在每一个 Button的Text组件中设置 颜色。
\\nbackgroundColor 背景色,设置按钮的背景色。
\\nFlutter 中按钮都是具有状态的,在上方你尝试设置背景时,发现按钮竟然不能直接设置背景色,这是因为按钮背景色的设置处于一个多种状态的叠加态中,你可以参考下方来编写按钮样式设置。
\\nbackgroundColor: WidgetStateProperty.resolveWith((state){\\n if (state.contains(WidgetState.disabled)) {\\n return Colors.grey;\\n }\\n return Colors.blue;\\n})\\n\\n/// 组件状态\\nenum WidgetState implements WidgetStatesConstraint {\\n hovered,\\n focused,\\n pressed,\\n dragged,\\n selected,\\n scrolledUnder,\\n disabled,\\n error;\\n}\\n
\\n除了按钮以为,其他组件也同样具有组件状态,大多数时候设置ThemeStyle都需要从此下手。
\\nFilledButton(\\n style: ButtonStyle(\\n shape: WidgetStatePropertyAll(\\n RoundedRectangleBorder(\\n borderRadius: BorderRadius.circular(10)\\n ),\\n ),\\n),\\n onPressed: () {},\\n child: Text(\\"圆角按钮\\"),\\n),\\n
","description":"Flutter 设计百科全书 按钮篇\\n前言\\n\\n本章内容需要结合我其他Theme相关文章观看。\\n 自定义按钮需要考虑手指触控面积和透明穿透设置。\\n\\n本章内容\\nInkWell 和 GestureDetector 如何自定义按钮\\n图标按钮案例\\n图片按钮案例\\n常用 Button\\nButtonTheme 和 按钮自定义样式\\nWidgetStateProperty\\n如何自定义按钮 InkWell 和 GestureDetector\\n如果你需要给 自定义组件增加 点击事件, 请使用 GestureDetector\\n如果你需要给 这个组件增加 水波纹涟漪效果, 请使用…","guid":"https://juejin.cn/post/7461463147540234278","author":"DreamMachine","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-19T14:08:47.699Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/320d50fd4ea54285a16149b5860cd252~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRHJlYW1NYWNoaW5l:q75.awebp?rk3s=f64ab15b&x-expires=1737900527&x-signature=k2cIoMrSmhRxwBtDT6F1RtPaBeU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/39d29bcfe01647498dd961a058719709~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRHJlYW1NYWNoaW5l:q75.awebp?rk3s=f64ab15b&x-expires=1737900527&x-signature=Vd1d9bBXlRgIx8M6zZI6fuvDMU8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/80f06607598f40df9257d775cfda10b8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRHJlYW1NYWNoaW5l:q75.awebp?rk3s=f64ab15b&x-expires=1737900527&x-signature=sxzvmUfrCW9hQPeGM1fLzvovXbY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/eecebc7a3d274caca2a3fcfb921e1d64~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRHJlYW1NYWNoaW5l:q75.awebp?rk3s=f64ab15b&x-expires=1737900527&x-signature=63%2FTYVrMfTb6t5Shy5B3kzRxUg8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/423f6dd377b54a1bb8b29b09bc67f0e7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRHJlYW1NYWNoaW5l:q75.awebp?rk3s=f64ab15b&x-expires=1737900527&x-signature=9A7ocUwtf3%2BhCtj7oZqmtPcSQdo%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/559f058370e3476db055bb319a4f87e7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRHJlYW1NYWNoaW5l:q75.awebp?rk3s=f64ab15b&x-expires=1737900527&x-signature=iHK7wQonqqVZK2erJndrbZj1q7U%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["前端","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"深入理解面向对象三大特性","url":"https://juejin.cn/post/7461480100501585920","content":"学习之路需深耕细作,切勿轻视任一知识点,因其存在必蕴含深意
\\n面向对象的三大特性指的是封装、继承和多态。这些特性使得面向对象编程(OOP)成为一种强大且灵活的编程范式。面向对象编程的这三大特性相互协作,使得开发者能够创建更加模块化、复用性强和易于维护的代码。通过封装,可以隐藏对象的内部实现细节;通过继承,可以实现代码的重用和扩展;通过多态,可以实现更加灵活和动态的行为。
\\n直接上图
\\n将插线板比作对象,它由一块电板(属性)以及复杂的电路(方法)组成,我们不用去搞懂他的电路构造(内部细节),只需要电工(开发人员)将其封装好,我们就可以通过插孔(接口)直接使用了。
\\nclass Person {\\n String name = \\"二狗\\";\\n int age = 20;\\n\\n @override\\n String toString() {\\n return \'姓名: $name, age: $age}\';\\n }\\n}\\nvoid main() {\\n Person person = Person();\\n print(person.age);\\n print(person.toString());\\n //输出:\\n //20\\n //姓名: 二狗, 年龄: 20}\\n
\\n直接上图
\\n图中的Person
类是Teacher
类和Student
类的父类(基类),反之Teacher
类和Student
类是Person
类的子类(派生类)
2.继承的使用
\\n在Dart中,继承同样是一种强大的面向对象编程特性,它允许一个类(子类)继承另一个类(父类)的属性和方法。子类可以重用父类的代码,并且可以添加新的属性、方法或重写父类中的方法。
\\nclass Animal {\\n String name;\\n int age;\\n\\n Animal(this.name, this.age);\\n\\n void speak() {\\n }\\n\\n void info() {\\n print(\'Name: $name, 年龄: $age岁\');\\n }\\n}\\n\\n// 定义派生类(子类) Dog 继承自 Animal\\nclass Dog extends Animal {\\n String breed;\\n Dog(super.name,super.age,this.breed);\\n @override\\n void speak() {\\n print(\'$name 在汪汪叫!\');\\n }\\n}\\n\\nclass Cat extends Animal {\\n String color;\\n Cat(super.name,super.age,this.color);\\n\\n @override\\n void speak() {\\n print(\'$name 在喵喵叫\');\\n }\\n}\\n\\n\\nvoid main() {\\n Dog dog = Dog(\'小狗\', 3, \'边牧\');\\n dog.speak();\\n dog.info();\\n\\n Cat cat = Cat(\'小猫\', 2, \'黑色\');\\n cat.speak();\\n cat.info();\\n
\\n代码重用:
\\n继承允许子类复用父类的代码。这意味着如果多个类有共享的功能或属性,你可以将这些共享的部分放在一个父类中,然后让子类继承这个父类。这样,你就不需要在每个子类中重复编写相同的代码了。
扩展性:
\\n通过继承,子类可以在父类的基础上添加新的功能或属性,从而扩展父类的能力。这种扩展性使得代码更加灵活,能够适应不断变化的需求。
多态性:
\\n继承是实现多态性的基础。多态性允许你使用父类类型的引用来指向子类对象,并根据实际对象的类型来调用相应的方法。这使得代码更加通用和灵活。
多态(Polymorphism)是面向对象编程中的一个核心概念,它允许一个接口被多个不同的类实现。相同的操作或函数可以在作用于不同的对象时,产生不同的解释和不同的执行结果。
\\n直接上图
\\n图中打印机中的打印方法被彩色打印机和黑白打印机实现,但具体的实现方式各有不同,\\n同一种方法被不同对象使用时会有不同的表现形式。
\\n2.多态的使用
\\nabstract class Flyable {\\n void fly();\\n} \\nclass Bird implements Flyable { \\n @override void \\n fly() { \\n print(\\"I\'m flying!\\"); \\n } \\n} \\nvoid main() { \\nFlyable flyable = Bird();\\nflyable.fly();\\n// 输出: I\'m flying! \\n}\\n
\\n在这个例子中,Bird
类实现了Flyable
接口,并提供了fly
方法的具体实现。这样,Bird
类的对象就可以被赋值给Flyable
类型的变量,并通过这个变量来调用fly
方法。
3.多态的优点
\\n本章内容可能在Android 6/7 之前的系统中,有部分不兼容,因为年代久远不做考虑。
\\n文章中的设置同时对Android / Ios 都能生效,有时候不能同时支持,但是为了兼容需要设置它们。
\\n\\n推荐把Scaffold作为每个页面的基础组件,并为它搭配上AppBar组件。
\\n
\\n原因:AppBar中的SystemUiOverlayStyle,是设置手机 系统状态栏和系统导航栏的重要属性。如果不增加该配置,状态栏跟页面颜色可能会冲突
\\n不推荐Scaffold嵌套使用,在App的导航页面会这样用。
常用的属性
\\nextendBody: true, // 扩展到底部导航栏下面\\nextendBodyBehindAppBar: true,// 扩展到顶部AppBar下面\\nPreferredSizeWidget? appBar;\\nWidget? body;\\nWidget? floatingActionButton;\\nWidget? bottomNavigationBar;\\n
\\n关于页面全景背景图或渐变背景的做法
\\n\\n\\n一种是用Stack将Scaffold包起来(个人感觉代码看起来不够美观)
\\n
\\n另外一种是使用extendBodyBehindAppBar,然后将body 的根节点包含一个DecoratedBox作为背景层,
\\n需要注意的是如果当前页面没有AppBar,那么需要设置toolbarHeight:0;内容才能正常从顶部开始布局。
关于底部按钮或其他组件
\\n\\n\\n推荐底部按钮放入bottomNavigationBar中(另外一个做法是Column中,上方Expanded,下方自适应高度的按钮)
\\n
本章不介绍主题Theme相关的基础知识,有需要请看我另外专门介绍Theme的篇章。
\\nAppBarTheme(\\n backgroundColor: Colors.white,// 背景色\\n foregroundColor: Colors.black,// 前景色\\n elevation: 0, // 高度\\n toolbarHeight: kToolbarHeight,\\n scrolledUnderElevation: 0, // 滚动高度\\n surfaceTintColor: Colors.black, //叠加色\\n systemOverlayStyle: \\n SystemUiOverlayStyle(\\n statusBarColor: Colors.transparent, // 状态栏背景色\\n statusBarBrightness: Brightness.light, // 状态栏主题\\n statusBarIconBrightness: Brightness.dark, // 状态栏图标主题\\n systemNavigationBarColor: Colors.white, // 导航栏背景色\\n systemNavigationBarDividerColor: Colors.black //导航栏线条色\\n ),\\n);\\n
\\nAppBar是顶部的组件,它包含Toolbar 和 StatusBar,
\\nStatusBar 属于系统区域了,里面会显示时间和WIFI信号和GPS等图标。
\\ntoolbarHeight 设置为0的时候,工具栏就不能显示了,这时候返回按钮和标题不能正常工作,一般页面都不会这样设置。
Toolbar 中包含 leading(默认会生成返回按钮),title(一般放Text显示标题),action(一般放右上角的按钮,是一个数组,当设置action时,title就不居中了,如果需要title居中可以设置centerTitle)
\\nSystemUiOverlayStyle 非常重要,它是每个页面状态栏的属性设置,但是这个字段网上讲的不太细致
\\n不要在AppBar内的Text 和 Button 中设置 color,而是通过AppbarTheme的foregroundColor 自动设置文本的颜色
\\ntoolbarHeight 默认设置是一个常量,double kToolbarHeight = 56.0,一般情况不要在 全局设置中设置为0或者其他值,保持它的默认值就好。
\\nelevation Z-Index 高度,会产生阴影和叠加色。
\\nscrolledUnderElevation 滚动高度,是body内list组件滚动时就会产生的高度。
\\nsystemNavigationBarColor 这个属性只有安卓有效(存疑),它是设置安卓底部小黑条后面的背景色
\\nstatusBarColor !!! 再也不用去改安卓原生代码实现沉浸式状态栏了,直接这里就可以设置沉浸式状态栏(存疑:安卓老版本可能有兼容问题)
\\n安全区域指的是 排除刘海屏,水滴屏和圆角屏幕的边缘的区域。\\nListView 默认是有一个SafeArea区域Padding的,当你为ListView设置Padding时,这个Safe边距将会失效。
\\nSafeArea //不包括AppBar 和 BottomNavigationBar 区域。\\nMediaQuery.of(context).padding //屏幕安全边距,会变化,比如键盘弹起,底部的边距将为0.\\nMediaQuery.of(context).viewPadding //屏幕的安全边距,不会变化。\\nMediaQuery.of(context).viewInsets //键盘的高度\\n
\\n通过viewPadding字段可以取到屏幕四边的安全区域尺寸,
\\nviewPadding.top 代表顶部安全区域
通过设置extendBodyBehindAppBar之后,可以让body的内容跑到Appbar和状态栏后方,\\n但是可以通过 SafeArea 或 viewPadding.top 让部分内容 不被Appbar 覆盖。
\\nAppBarTheme(\\n systemOverlayStyle: \\n SystemUiOverlayStyle(\\n statusBarColor: Colors.transparent, // 状态栏背景色\\n statusBarBrightness: Brightness.light, // 状态栏主题\\n statusBarIconBrightness: Brightness.dark, // 状态栏图标主题\\n systemNavigationBarColor: Colors.white, // 导航栏背景色\\n systemNavigationBarDividerColor: Colors.black //导航栏线条色\\n ),\\n);\\n
\\n设置Scaffold的extendBodyBehindAppBar,可以轻松的实现沉浸式状态。
\\n另外一个高斯模糊的AppBar方案可以看我另外的文章。
\\n掘金
\\n安卓是可以打开底部三大金刚导航栏的,开启后,屏幕底部显示三个按钮,这个时候如果你使用了extendBody,需要做好适配,不然内容会沉浸进去,导致显示异常。
systemNavigationBarDividerColor\\n在部分安卓手机上,底部系统导航栏下方存在一根黑色的条状物(手势导航线),可以设置它的颜色等。
\\nstatusBarIconBrightness
\\nsystemNavigationBarDividerColor
这个两个属性在部分安卓手机上可能出现无效的情况,大概率是小米等安卓厂商强制覆写了该字段,因为默认会开启 强制颜色反转属性,。\\n设置systemNavigationBarContrastEnforced 无效,我猜测这个字段是关闭反转的,但是无效。
\\n不用过于担心,因为小黑条自动设置颜色了,大多数情况下 都是正常的。
\\n但是为了兼容IOS,你依然需要按照预期去设置这些参数。
安装插件,注意:裁剪库
需要额外配置
# 相机、相册库\\n$ flutter pub add image_picker\\n# 裁剪库\\n$ flutter pub add image_cropper\\n
\\n每次修改了配置,需要 clean
在 get
重新安装,要不然会使用编译好的缓存运行,所以避免问题,从根本解决问题:
# 先清空\\n$ flutter clean\\n# 再安装\\n$ flutter pub get\\n# 运行\\n$ flutter run\\n
\\nAndroid
权限配置,文件路径 android/app/src/main/AndroidManifest.xml
<!-- 请求相机权限 --\x3e\\n<uses-permission android:name=\\"android.permission.CAMERA\\"/>\\n<!-- 请求读取存储权限(读取相册) --\x3e\\n<uses-permission android:name=\\"android.permission.READ_EXTERNAL_STORAGE\\"/>\\n<!-- 请求写入存储权限(选择和保存相册中的图片)--\x3e\\n<uses-permission android:name=\\"android.permission.WRITE_EXTERNAL_STORAGE\\"/>\\n<!-- 如果使用了访问相册,且是 Android 10(API 29)及以上版本,还需要配置 --\x3e\\n<uses-permission android:name=\\"android.permission.ACCESS_MEDIA_LOCATION\\" />\\n
\\niOS
权限配置,文件路径 ios/Runner/Info.plist
<key>Localization native development region</key>\\n<string>zh_CN</string>\\n<key>NSCameraUsageDescription</key>\\n<string>我们需要访问您的相机</string>\\n<key>NSPhotoLibraryUsageDescription</key>\\n<string>我们需要访问您的照片库</string>\\n
\\nLocalization native development region
会使相机、相册打开后显示中文。附:# Flutter iOS 调起相机、相册显示英文,需要改成中文。
image_cropper
安卓问题处理不需要裁剪功能,就不用看这段了。
\\nimage_cropper
在 iOS
上没问题,但是在安卓上需要将 compileSdk
修改为 35
,文件路径 android/app/build.gradle
未来最新的版本可能不是 35
,可能更高,按着报错改就行。
不修改会报错:
\\nYour project is configured to compile against Android SDK 34, but the following plugin(s) require to be compiled against a higher Android SDK version:\\n- image_cropper compiles against Android SDK 35\\nFix this issue by compiling against the highest Android SDK version (they are backward compatible).\\nAdd the following to /Users/dengzemiao/Desktop/Project/flutter/flutter-edu-app/android/app/build.gradle:\\n\\n android {\\n compileSdk = 35\\n ...\\n }\\n\\n\\nFAILURE: Build failed with an exception.\\n\\n* What went wrong:\\nExecution failed for task \':app:processDebugResources\'.\\n> A failure occurred while executing com.android.build.gradle.internal.res.LinkApplicationAndroidResourcesTask$TaskAction\\n > Android resource linking failed\\n com.example.testbaseproject.app-mergeDebugResources-42:/values-v35/values-v35.xml:4: error: style attribute \'android:attr/windowOptOutEdgeToEdgeEnforcement\' not found.\\n error: failed linking references.\\n\\n\\n* Try:\\n> Run with --stacktrace option to get the stack trace.\\n> Run with --info or --debug option to get more log output.\\n> Run with --scan to get full insights.\\n> Get more help at https://help.gradle.org.\\n\\nBUILD FAILED in 9s\\n
\\n修改后,还不行,需要确定 Android Studio
安装好了 35
的 sdk
,注意安装包支持的 CPU
类型,安装好了就行了。
nonono
,再次运行,可以正常使用了,但是在拍好照片进行裁剪的还会报错,可能还会报错,一大串的,可以搜搜里面有没有 com.yalantis.ucrop.UCropActivity
,就是缺少了 activity
的申明,所以还需要配置下,文件路径 android/app/src/main/AndroidManifest.xml
在 application
里面再加一个 activity
申明:
<activity android:name=\\"com.yalantis.ucrop.UCropActivity\\"\\n android:theme=\\"@style/Theme.AppCompat.Light.NoActionBar\\"/>\\n
\\n到这里就真的 ok
了,可以放心使用裁剪了,如果不需要裁剪后面这些配置可以不用配置,只要配置权限即可。
使用
\\n// 导入\\nimport \'package:base_project/utils/camera_utils.dart\';\\n\\n// 使用\\nCustomBottomSheet.show(\\n context: context,\\n list: [\'拍照\', \'打开相册\'],\\n onConfirm: (index) async {\\n if (index == 0) {\\n // 打开相机\\n await camera.showCamera(\\n cropping: true,\\n onChanged: (value) => {\\n print(\'图片路径: $value\')\\n }\\n );\\n } else {\\n // 打开相册\\n await camera.showAlbum(\\n cropping: true,\\n onChanged: (value) => {\\n print(\'图片路径: $value\')\\n }\\n );\\n }\\n },\\n);\\n
\\ncamera_utils.dart
import \'package:flutter/material.dart\';\\nimport \'package:image_picker/image_picker.dart\';\\nimport \'package:image_cropper/image_cropper.dart\';\\n\\nclass CameraUtils {\\n\\n // 静态变量存储单例\\n static final CameraUtils _instance = CameraUtils._internal();\\n // 静态方法获取单例实例\\n factory CameraUtils() => _instance;\\n // 私有构造函数,确保只能通过工厂方法获取实例\\n CameraUtils._internal();\\n\\n // 静态的 _picker 变量,确保只会初始化一次\\n final ImagePicker _picker = ImagePicker();\\n\\n // 打开相机\\n Future<void> showCamera({\\n bool? cropping,\\n ValueChanged<String>? onChanged\\n }) async {\\n try {\\n final XFile? file = await _picker.pickImage(source: ImageSource.camera);\\n if (file != null) {\\n if (cropping == true) {\\n await _cropImage(\\n path: file.path,\\n onChanged: onChanged\\n );\\n } else {\\n if (onChanged != null) { onChanged(file.path); }\\n }\\n } else {\\n if (onChanged != null) { onChanged(\'\'); }\\n }\\n } catch (e) {\\n if (onChanged != null) { onChanged(\'\'); }\\n // print(\'图片选择出错: $e\');\\n }\\n }\\n\\n // 打开相册\\n Future<void> showAlbum({\\n bool? cropping,\\n ValueChanged<String>? onChanged\\n }) async {\\n try {\\n final XFile? file = await _picker.pickImage(source: ImageSource.gallery);\\n if (file != null) {\\n if (cropping == true) {\\n await _cropImage(\\n path: file.path,\\n onChanged: onChanged\\n );\\n } else {\\n if (onChanged != null) { onChanged(file.path); }\\n }\\n } else {\\n if (onChanged != null) { onChanged(\'\'); }\\n }\\n } catch (e) {\\n // print(\'图片选择出错: $e\');\\n if (onChanged != null) { onChanged(\'\'); }\\n }\\n }\\n\\n // 图片裁剪方法\\n Future<void> _cropImage({\\n required String path,\\n ValueChanged<String>? onChanged\\n }) async {\\n try {\\n // 使用 image_cropper 进行裁剪\\n final file = await ImageCropper().cropImage(\\n sourcePath: path,\\n uiSettings: [\\n AndroidUiSettings(\\n toolbarTitle: \'裁剪图片\',\\n toolbarColor: Colors.deepOrange,\\n toolbarWidgetColor: Colors.white,\\n initAspectRatio: CropAspectRatioPreset.square,\\n lockAspectRatio: false,\\n ),\\n IOSUiSettings(\\n minimumAspectRatio: 1.0,\\n aspectRatioLockEnabled: false\\n ),\\n ]\\n );\\n // 如果裁剪成功\\n if (file != null && onChanged != null) {\\n // 图片裁剪成功,返回裁剪后的图片\\n onChanged(file.path);\\n }\\n } catch (e) {\\n // print(\'裁剪出错: $e\');\\n if (onChanged != null) { onChanged(\'\'); }\\n }\\n }\\n}\\n
\\nflutter
相机使用的是 image_picker
插件,打开中文的问题跟原生开发解决思路是一样的。
在开发相机、相册功能时,默认调起展示的英文,可以通过原生工程修改 info
配置,添加:
Localization native development region
-> String
-> zh_CN
Localized resources can be mixed
-> Boolean
-> YES
注意:推荐从原工程 Target
中进入修改,直接修改 info.plist
文件有概率不生效,可能是同步问题,理论上来说都可以,如果遇上了这个问题了可以尝试下。下面也提供了 info.plist
配置方式。
配置细节了解:
\\n通常配置 Localization native development region
配置后就生效了,而 Localized resources can be mixed
属于可选,可以加一个尝试一下再追加。
如果两个配置都设置了,还没有生效,那么就还需要添加一下资源包:
\\n加了资源包后,重新运行看下有没有问题,按理论上基本就不会存在问题了,如果还有还可以强制设置一下默认:
\\n到这还不行,重新找文档吧。
\\ninfo.plist
配置与介绍总结Localization native development region
zh_CN
),并且没有设置其他区域相关的资源,它将会使你的应用在默认情况下使用该区域的语言和格式。<key>Localization native development region</key>\\n<string>zh_CN</string>\\n
\\nLocalized resources can be mixed
作用:这个设置允许混合使用不同区域的资源。如果启用了它,资源文件(例如 strings
、图片等)可以不严格依赖于区域设置,这意味着可以在多个区域中共享资源,而不需要为每个区域单独维护一组资源。
效果:即使没有明确为每个区域提供本地化资源,这个设置也会让应用根据开发设置的默认语言进行显示。
\\n<key>Localized resources can be mixed</key>\\n<true/>\\n
\\n\\nLocalization native development region
设置正确,应用会优先使用该区域的语言。例如,zh_CN
会让系统默认使用简体中文。Localized resources can be mixed
,这样即使没有为所有地区提供资源,应用也可以正常运行。Localization native development region
为 zh_CN
就足够了。Localized resources can be mixed
。change_app_package_name
是一个 Flutter 社区插件,旨在帮助开发者更方便地修改 Flutter 项目的包名。它能够自动处理大部分的包名更改,避免了手动更新 Android 和 iOS 配置文件中的多个位置。
change_app_package_name
插件可以快速修改项目的 Android 和 iOS 部分的包名。它会:
AndroidManifest.xml
和 build.gradle
文件中的包名。Info.plist
和 Runner.xcodeproj/project.pbxproj
文件中的 Bundle Identifier。在项目的根目录下找到 pubspec.yaml
文件。
将以下依赖添加到 dev_dependencies
中:
dev_dependencies:\\n change_app_package_name: ^1.0.0\\n
\\n在终端中运行以下命令安装依赖:
\\n$ flutter pub get\\n
\\n或命令安装到 dev_dependencies
$ flutter pub add --dev change_app_package_name\\n
\\n安装完插件后,你可以使用以下命令来修改包名:
\\n$ flutter pub run change_app_package_name:main com.yourname.new_project_name\\n
\\ncom.yourname.new_project_name
是你要使用的新包名。确保这个包名是合法的,并符合命名规范(通常采用倒装域名形式,如 com.example.myapp
)。运行此命令后,插件会自动做如下操作:
\\nAndroidManifest.xml
、build.gradle
等文件中的 package
和 applicationId
。Info.plist
和 project.pbxproj
文件中的 Bundle Identifier。如果你在开发过程中需要修改包名(例如,你决定修改公司名或应用名称),可以使用 change_app_package_name
插件快速修改。
例如,开发者使用了 com.example.myapp
作为初始包名,后来决定改成 com.mycompany.newapp
,就可以直接使用该插件进行修改。
在发布应用时,尤其是当你需要更换包名时,change_app_package_name
插件可以帮助你自动完成这些更改,而不必手动去修改多个文件。
如果你有多个不同版本的应用(例如测试版、正式版),change_app_package_name
插件可以帮助你轻松地切换不同版本的包名。
尽管 change_app_package_name
插件提供了便捷的包名更改方式,但有时你仍然需要手动修改包名,特别是在以下场景:
flutter clean
后重新构建项目,有时缓存可能导致修改未生效。Runner
的 Bundle Identifier。change_app_package_name
插件是一个方便的工具,可以帮助你自动修改 Flutter 项目中的 Android 和 iOS 包名,减少手动修改配置文件的繁琐。它特别适用于修改现有项目的包名,尤其是在发布应用时,节省大量时间。然而,在某些复杂的情况下,手动修改包名可能更为合适。
Flutter 项目中偶尔会遇到一些杀死 app 冷启动会遇到的一些调试问题。或者需要读写一些数据到沙盒。都需要将沙盒文件透明化,简单来说就是文件可以随时访问分享出来的沙盒文件浏览器。
\\n读取文件成功示例:\\n
class _AppSandboxFileDirectoryState extends State<AppSandboxFileDirectory> with DebugBottomSheetMixin {\\n var directorys = <PathProviderDirectory>[];\\n\\n final cacheUserMapVN = ValueNotifier(<String, dynamic>{});\\n\\n @override\\n void initState() {\\n super.initState();\\n\\n WidgetsBinding.instance.addPostFrameCallback((_) async {\\n directorys = await PathProviderDirectory.initail();\\n directorys = directorys.where((e) => e.custom?[\\"dir\\"] != null).toList();\\n DLog.d(\\"directorys: ${directorys.length}\\");\\n setState(() {});\\n cacheUserMapVN.value = await getCacheUserMap();\\n });\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: GestureDetector(\\n onLongPress: onDebugSheet,\\n child: Text(\\"$widget\\"),\\n ),\\n ),\\n body: buildBody(),\\n );\\n }\\n\\n Widget buildBody() {\\n return Container(\\n padding: EdgeInsets.symmetric(horizontal: 16, vertical: 16),\\n child: Column(\\n crossAxisAlignment: CrossAxisAlignment.stretch,\\n children: [\\n ...[\\n NButton(\\n constraints: BoxConstraints(maxHeight: 35),\\n title: \\"DocumentsDirectory\\",\\n onPressed: () async {\\n final directory = await getApplicationDocumentsDirectory();\\n Get.to(() => FileBrowserPage(directory: directory));\\n },\\n ),\\n buildChooseDir(),\\n ].map(\\n (e) => Padding(padding: EdgeInsets.only(bottom: 8), child: e),\\n ),\\n ],\\n ),\\n );\\n }\\n\\n /// 改变并跳转目录\\n buildChooseDir() {\\n return NMenuAnchor<PathProviderDirectory>(\\n dropItemPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),\\n values: directorys,\\n initialItem: PathProviderDirectory.applicationDocumentsDirectory,\\n onChanged: (val) {\\n DLog.d(\\"NMenuAnchor: $val\\");\\n final directory = val.custom?[\\"dir\\"];\\n final exception = val.custom?[\\"exception\\"] as String?;\\n if (exception?.isNotEmpty == true) {\\n ToastUtil.show(exception ?? \\"\\");\\n return;\\n }\\n Get.to(() => FileBrowserPage(directory: directory));\\n },\\n equal: (a, b) => a.name == b?.name,\\n cbName: (e) => e?.name ?? \\"-\\",\\n );\\n }\\n}\\n
\\n/// 文件管理类\\nclass FileManager {\\n static final FileManager _instance = FileManager._();\\n FileManager._();\\n factory FileManager() => _instance;\\n static FileManager get instance => _instance;\\n\\n ///获取缓存目录路径\\n Future<Directory> getCacheDir() async {\\n var directory = await getTemporaryDirectory();\\n return directory;\\n }\\n\\n ///获取文件缓存目录路径\\n Future<Directory> getFilesDir() async {\\n var directory = await getApplicationSupportDirectory();\\n return directory;\\n }\\n\\n ///获取文档存储目录路径\\n Future<Directory> getDocumentsDir() async {\\n var directory = await getApplicationDocumentsDirectory();\\n return directory;\\n }\\n\\n /// 文件创建\\n ///\\n /// - fileName 文件名\\n ///\\n /// - ext 文件扩展\\n ///\\n /// - content 文件内容\\n ///\\n /// - dir 保存文件夹\\n Future<File> createFile({\\n required String fileName,\\n String ext = \\"dart\\",\\n required String content,\\n Directory? dir,\\n bool cover = false,\\n }) async {\\n // final dateStr = \\"${DateTime.now()}\\".split(\\".\\").first ?? \\"\\";\\n\\n /// 本地文件目录\\n Directory tempDir = dir ?? await getApplicationCacheDirectory();\\n if (Platform.isMacOS) {\\n final downloadsDir = await getDownloadsDirectory();\\n if (downloadsDir != null) {\\n tempDir = downloadsDir;\\n }\\n }\\n\\n final fileNameNew = fileName.contains(\\".\\") ? fileName : \'$fileName.$ext\';\\n assert(fileNameNew.contains(\\".\\"), \\"文件类型不能为空\\");\\n\\n var path = \'${tempDir.path}/$fileNameNew\';\\n debugPrint(\\"$this $fileNameNew: $path\\");\\n var file = File(path);\\n if (cover && file.existsSync()) {\\n file.deleteSync();\\n }\\n file.createSync();\\n file.writeAsStringSync(content);\\n return file;\\n }\\n\\n /// 文件读取\\n ///\\n /// - fileName 文件名\\n ///\\n /// - ext 文件扩展\\n ///\\n /// - content 文件内容\\n ///\\n /// - dir 保存文件夹\\n Future<File> readFile({\\n required String fileName,\\n String ext = \\"dart\\",\\n Directory? dir,\\n }) async {\\n /// 本地文件目录\\n var tempDir = dir ?? await getApplicationCacheDirectory();\\n\\n final fileNameNew = fileName.contains(\\".\\") ? fileName : \'$fileName.$ext\';\\n assert(fileNameNew.contains(\\".\\"), \\"文件类型不能为空\\");\\n\\n var path = \'${tempDir.path}/$fileNameNew\';\\n debugPrint(\\"$this $fileNameNew: $path\\");\\n var file = File(path);\\n return file;\\n }\\n\\n /// 存储 map\\n ///\\n /// - fileName 文件名\\n ///\\n /// - ext 文件类型, 默认 txt\\n ///\\n /// - map 要存储的字典\\n Future<File> saveJson({\\n required String fileName,\\n String ext = \\"dart\\",\\n Directory? dir,\\n required Map<String, dynamic> map,\\n }) async {\\n final content = jsonEncode(map);\\n final file = await FileManager().createFile(fileName: fileName, ext: ext, content: content, dir: dir);\\n return file;\\n }\\n\\n /// 读取 map\\n ///\\n /// - fileName 文件名\\n ///\\n /// - ext 文件类型, 默认 txt\\n ///\\n /// - dir 目标文件夹\\n Future<Map<String, dynamic>?> readJson({\\n required String fileName,\\n String ext = \\"dart\\",\\n Directory? dir,\\n }) async {\\n final file = await FileManager().readFile(\\n fileName: fileName,\\n ext: ext,\\n dir: dir,\\n );\\n final fileExists = file.existsSync();\\n if (!fileExists) {\\n debugPrint(\\"❌ $this $fileName.$ext: 文件不存在\\");\\n return null;\\n }\\n final content = await file.readAsString();\\n return jsonDecode(content);\\n }\\n}\\n
\\nclass CacheController {\\n CacheController();\\n\\n /// 从沙盒读取数据\\n Future<Map<String, dynamic>> readFromDisk({required String cacheKey}) async {\\n try {\\n final dir = await FileManager().getDocumentsDir();\\n final mapNew = await FileManager().readJson(fileName: cacheKey, dir: dir);\\n return mapNew ?? {};\\n } catch (e) {\\n DLog.d(\\"$runtimeType readFromDisk $e\\");\\n }\\n return {};\\n }\\n\\n /// 保存数据到沙盒\\n Future<File?> saveToDisk({required String cacheKey, required Map<String, dynamic> map}) async {\\n try {\\n final dir = await FileManager().getDocumentsDir();\\n final file = await FileManager().saveJson(fileName: cacheKey, map: map, dir: dir);\\n DLog.d(\\"$runtimeType saveToDisk file: $file\\");\\n return file;\\n } catch (e) {\\n DLog.d(\\"$runtimeType saveToDisk $e\\");\\n }\\n return null;\\n }\\n\\n /// 保存数据到沙盒\\n Future<File?> readFile({required String cacheKey}) async {\\n try {\\n final dir = await FileManager().getDocumentsDir();\\n final file = await FileManager().readFile(fileName: cacheKey, dir: dir);\\n DLog.d(\\"$runtimeType readFile file: $file\\");\\n return file;\\n } catch (e) {\\n DLog.d(\\"$runtimeType readFile $e\\");\\n }\\n return null;\\n }\\n}\\n
\\n/// 基于 path_provider 沙盒文件路径获取\\nclass PathProviderDirectory {\\n static PathProviderDirectory get temporaryDirectory => PathProviderDirectory(\\n name: \\"temporaryDirectory\\",\\n func: getTemporaryDirectory,\\n desc: \\"临时目录,系统可以随时清空的缓存文件夹\\",\\n );\\n\\n static PathProviderDirectory get applicationSupportDirectory => PathProviderDirectory(\\n name: \\"applicationSupportDirectory\\",\\n func: getApplicationSupportDirectory,\\n desc: \\"应用程序支持目录,用于不想向用户公开的文件,也就是你不想给用户看到的文件可放置在该目录中,系统不会清除该目录,只有在删除应用时才会消失。\\",\\n );\\n\\n static PathProviderDirectory get libraryDirectory => PathProviderDirectory(\\n name: \\"libraryDirectory\\",\\n func: getLibraryDirectory,\\n desc: \\"应用程序持久文件目录,主要存储持久文件的目录,并且不会对用户公开,常用于存储数据库文件,比如sqlite.db等。\\",\\n );\\n\\n static PathProviderDirectory get applicationDocumentsDirectory => PathProviderDirectory(\\n name: \\"applicationDocumentsDirectory\\",\\n func: getApplicationDocumentsDirectory,\\n desc: \\"文档目录,用于存储只能由该应用访问的文件,系统不会清除该目录,只有在删除应用时才会消失。\\",\\n );\\n\\n static PathProviderDirectory get applicationCacheDirectory => PathProviderDirectory(\\n name: \\"applicationCacheDirectory\\",\\n desc: \\"应用程序可以在其中放置特定于应用程序的目录的路径 cache 文件。如果此目录不存在,则会自动创建该目录。\\",\\n func: getApplicationCacheDirectory,\\n );\\n\\n static PathProviderDirectory get externalStorageDirectory => PathProviderDirectory(\\n name: \\"externalStorageDirectory\\",\\n desc: \\"外部存储目录, 应用程序可以访问顶级存储的目录的路径。\\",\\n func: getExternalStorageDirectory,\\n );\\n\\n static PathProviderDirectory get externalCacheDirectories => PathProviderDirectory(\\n name: \\"externalCacheDirectories\\",\\n desc: \\"外部存储缓存目录\\",\\n func: getExternalCacheDirectories,\\n );\\n\\n static PathProviderDirectory get externalStorageDirectories => PathProviderDirectory(\\n name: \\"externalStorageDirectories\\",\\n desc: \\"可根据类型获取外部存储目录,如SD卡、单独分区等,和外部存储目录不同在于他是获取一个目录数组。但iOS不支持外部存储目录,目前只有Android才支持。\\",\\n func: getExternalStorageDirectories,\\n );\\n\\n static PathProviderDirectory get downloadsDirectory => PathProviderDirectory(\\n name: \\"downloadsDirectory\\",\\n desc: \\"桌面程序下载目录,主要用于存储下载文件的目录,只适用于Linux、MacOS、Windows,Android和iOS平台无法使用。\\",\\n func: getDownloadsDirectory,\\n );\\n\\n static List<PathProviderDirectory> get values => [\\n temporaryDirectory,\\n applicationSupportDirectory,\\n libraryDirectory,\\n applicationDocumentsDirectory,\\n applicationCacheDirectory,\\n externalStorageDirectory,\\n externalCacheDirectories,\\n externalStorageDirectories,\\n downloadsDirectory,\\n ];\\n\\n PathProviderDirectory({\\n required this.name,\\n required this.func,\\n required this.desc,\\n this.custom,\\n });\\n\\n final String name;\\n /// 获取文件目录的异步方法\\n final Function func;\\n final String desc;\\n\\n /// 自定义参数,用来存储获取到的文件目录路径及异常信息\\n Map<String, dynamic>? custom;\\n\\n /// 初始化目录路径\\n static Future<List<PathProviderDirectory>> initail() async {\\n var list = <PathProviderDirectory>[];\\n for (final e in PathProviderDirectory.values) {\\n e.custom ??= {};\\n try {\\n final result = await e.func();\\n e.custom?[\\"dir\\"] = result;\\n } catch (exception) {\\n debugPrint(\\"获取目录失败 $exception\\");\\n e.custom?[\\"exception\\"] = exception.toString();\\n continue;\\n } finally {\\n list.add(e);\\n }\\n }\\n // debugPrint(\\"list: ${list.length}\\");\\n return list;\\n }\\n}\\n
\\n//\\n// FileBrowserPage.dart\\n// projects\\n//\\n// Created by shang on 2025/1/6 17:48.\\n// Copyright © 2025/1/6 shang. All rights reserved.\\n//\\n\\n/// 查看本地缓存文件\\nclass FileBrowserPage extends StatefulWidget {\\n const FileBrowserPage({\\n super.key,\\n required this.directory,\\n });\\n\\n final Directory? directory;\\n\\n @override\\n State<FileBrowserPage> createState() => _FileBrowserPageState();\\n}\\n\\nclass _FileBrowserPageState extends State<FileBrowserPage> with DebugBottomSheetMixin {\\n final _scrollController = ScrollController();\\n\\n Directory? currentDirectory;\\n List<FileSystemEntity> files = [];\\n\\n @override\\n void initState() {\\n super.initState();\\n _loadInitialDirectory();\\n }\\n\\n Future<void> _loadInitialDirectory() async {\\n Directory directory = widget.directory ?? await getApplicationDocumentsDirectory();\\n currentDirectory = directory;\\n files = currentDirectory!.listSync();\\n setState(() {});\\n }\\n\\n @override\\n void didUpdateWidget(covariant FileBrowserPage oldWidget) {\\n super.didUpdateWidget(oldWidget);\\n if (oldWidget.directory != widget.directory) {\\n setState(() {});\\n }\\n }\\n\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: Text(\\"$widget\\"),\\n ),\\n body: Column(\\n children: [\\n Text(\\n currentDirectory?.path ?? \\"\\",\\n style: const TextStyle(fontSize: 14),\\n ),\\n Expanded(child: buildBody()),\\n ],\\n ),\\n );\\n }\\n\\n Widget buildBody() {\\n final dirExsit = currentDirectory?.existsSync() == true;\\n if (!dirExsit) {\\n return const Center(\\n child: Text(\\n \\"目录不存在\\",\\n style: TextStyle(fontSize: 14),\\n ),\\n );\\n }\\n\\n return Scrollbar(\\n child: ListView.separated(\\n itemCount: files.length,\\n itemBuilder: (context, index) {\\n FileSystemEntity entity = files[index];\\n final isDir = entity is Directory;\\n\\n final statSync = entity.statSync();\\n final modifiedStr = statSync.modified.toString().substring(0, 19);\\n\\n return ListTile(\\n dense: true,\\n leading: Icon(\\n isDir ? Icons.folder : Icons.insert_drive_file,\\n color: isDir ? primary : null,\\n ),\\n title: Text(entity.path.split(\'/\').last),\\n subtitle: Row(\\n children: [\\n Expanded(child: Text(modifiedStr)),\\n Text(statSync.size.fileSizeDesc),\\n ],\\n ),\\n onTap: () {\\n if (isDir) {\\n Navigator.push(\\n context,\\n MaterialPageRoute(\\n builder: (context) => FileBrowserPage(directory: entity),\\n ),\\n );\\n } else if (entity is File) {\\n _openFile(entity);\\n }\\n },\\n );\\n },\\n separatorBuilder: (_, index) {\\n return const Divider(height: 1, color: lineColor);\\n },\\n ),\\n );\\n }\\n\\n void _openFile(File file) {\\n final path = file.path;\\n final title = path.split(\'/\').last;\\n var content = \'\';\\n try {\\n content = file.readAsStringSync();\\n final obj = jsonDecode(content);\\n if (obj is Object) {\\n content = obj.formatedString();\\n }\\n } catch (e) {\\n debugPrint(\\"$this $e\\");\\n content = \\"文件读取失败: $e\\";\\n }\\n\\n onDebugBottomSheet(\\n title: title,\\n confirmTitle: Platform.isIOS ? \\"分享\\" : \\"下载\\",\\n onConfirm: () {\\n Share.shareXFiles([XFile(path)]);\\n },\\n content: Text(content),\\n );\\n }\\n}\\n
\\nimport \'package:flutter/material.dart\';\\nimport \'package:get/get.dart\';\\n\\n/// Debug 底部弹窗封装\\nmixin DebugBottomSheetMixin<T extends StatefulWidget> on State<T> {\\n /// 弹窗展示类型\\n Future<R?> onDebugBottomSheet<R>({\\n required String title,\\n required Widget content,\\n String confirmTitle = \\"确定\\",\\n VoidCallback? onConfirm,\\n }) {\\n return Get.bottomSheet<R>(\\n FractionallySizedBox(\\n heightFactor: 0.7,\\n child: Column(\\n crossAxisAlignment: CrossAxisAlignment.stretch,\\n children: [\\n SizedBox(\\n height: 50,\\n child: NavigationToolbar(\\n leading: InkWell(\\n onTap: () {\\n Navigator.of(context).pop();\\n },\\n child: Container(\\n padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 15),\\n child: const Text(\\n \\"取消\\",\\n style: TextStyle(\\n fontWeight: FontWeight.w500,\\n fontSize: 15.0,\\n color: Color(0xff737373),\\n ),\\n ),\\n ),\\n ),\\n middle: Text(\\n title,\\n style: const TextStyle(\\n fontWeight: FontWeight.w500,\\n fontSize: 15.0,\\n color: Color(0xff181818),\\n ),\\n textAlign: TextAlign.center,\\n ),\\n trailing: InkWell(\\n onTap: onConfirm,\\n child: Container(\\n padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 15),\\n child: Text(\\n confirmTitle,\\n style: const TextStyle(\\n fontWeight: FontWeight.w500,\\n fontSize: 16.0,\\n color: Colors.blue,\\n ),\\n ),\\n ),\\n ),\\n ),\\n ),\\n Expanded(\\n child: Scrollbar(\\n child: SingleChildScrollView(\\n child: Container(\\n padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 15),\\n child: content,\\n ),\\n ),\\n ),\\n ),\\n const SizedBox(height: 34),\\n ],\\n ),\\n ),\\n isScrollControlled: true,\\n backgroundColor: Colors.white,\\n );\\n }\\n}\\n
\\n1、实现一个简易的文件浏览器之后,你就随时读取存储在沙盒的文件排查问题。尤其在排查 app杀退冷启动时的 debug 日志文件,极大的提高了排查疑难问题的效率。
\\n2、通过 share_plus 实现分享
\\nShare.shareXFiles([XFile(path)]);\\n
\\n","description":"一、思路来源 Flutter 项目中偶尔会遇到一些杀死 app 冷启动会遇到的一些调试问题。或者需要读写一些数据到沙盒。都需要将沙盒文件透明化,简单来说就是文件可以随时访问分享出来的沙盒文件浏览器。\\n\\n读取文件成功示例:\\n\\n二、文件浏览器示例\\nclass _AppSandboxFileDirectoryState extends State@override\\nWidget build(BuildContext context) {\\n return ScreenUtilInit(\\n designSize: const Size(375, 812), // 配置设计图尺寸大小,单位:dp\\n builder: (context, child) => GetMaterialApp(\\n title: \'Flutter Demo\',\\n theme: ThemeData(\\n ),\\n builder: EasyLoading.init(),\\n initialBinding: InitBinding(),\\n // 初始路由会被 InitBinding 覆盖\\n initialRoute: AppRoutes.initialRoute,\\n getPages: AppRoutes.getPages,\\n )\\n );\\n}\\n
\\n.w
:宽度适配使用场景:
\\ndp
单位来设置控件的宽度时,使用 .w
来适配屏幕宽度。实际场景: 假设设计图中的按钮宽度是 200dp
,你想在不同的屏幕尺寸上保持一致的宽度。
Container(\\n width: 200.w, // 宽度根据屏幕宽度自动缩放\\n height: 50.h, // 高度根据屏幕高度自动缩放\\n color: Colors.blue,\\n child: Center(child: Text(\'Button\')),\\n)\\n
\\n在大屏设备上,按钮的宽度会更大,而在小屏设备上,宽度会更小,确保适配。
\\n.h
:高度适配使用场景:
\\ndp
单位来设置控件的高度时,使用 .h
来适配屏幕高度。实际场景: 假设设计图中的按钮高度是 50dp
,你希望在不同设备的屏幕上高度适配。
Container(\\n width: 100.w, \\n height: 50.h, // 高度适配屏幕高度\\n color: Colors.blue,\\n child: Center(child: Text(\'Button\')),\\n)\\n
\\n.sp
:字号适配使用场景:
\\nsp
)在设计图中通常是一个重要的参数,需要根据不同设备的屏幕密度进行缩放。实际场景: 假设设计图中的字体大小是 16sp
,你需要根据设备的屏幕密度来调整字号:
Text(\\n \'Hello World\',\\n style: TextStyle(\\n fontSize: 16.sp, // 字号根据屏幕适配\\n ),\\n)\\n
\\n这样,字体在不同的屏幕上显示的大小会一致,确保用户体验的一致性。
\\n.r
:圆角适配使用场景:
\\n实际场景: 假设设计图中卡片的圆角半径是 8dp
,你可以使用 .r
来自动适配屏幕:
Container(\\n width: 200.w,\\n height: 100.h,\\n decoration: BoxDecoration(\\n color: Colors.white,\\n borderRadius: BorderRadius.circular(8.r), // 自动适配圆角半径\\n ),\\n child: Center(child: Text(\'Card\')),\\n)\\n
\\n这样,圆角在所有设备上看起来都是一致的,不会因为屏幕尺寸的不同而显得过大或过小。
\\n.textScaleFactor
:字体缩放因子使用场景:
\\ntextScaleFactor
会根据这个设置来调整字体大小。实际场景: 如果用户在系统设置中增加了字体大小,你可以使用 textScaleFactor
来自动适配文本:
Text(\\n \'Hello World\',\\n style: TextStyle(\\n fontSize: 16.sp * ScreenUtil().textScaleFactor, // 适配字体缩放因子\\n ),\\n)\\n
\\n这样,文本会根据用户的设置自动调整字体大小,而不会被强制放大或缩小。
\\n.setWidth()
和 .setHeight()
:宽高自定义适配使用场景:
\\n实际场景: 假设你希望设置一个控件的宽度为屏幕宽度的 70%,而高度为屏幕高度的 10%。
\\nContainer(\\n width: 100.setWidth(0.7), // 设置为屏幕宽度的 70%\\n height: 200.setHeight(0.1), // 设置为屏幕高度的 10%\\n color: Colors.blue,\\n child: Center(child: Text(\'Custom Size\')),\\n)\\n
\\n.radius
:圆角半径适配(另一种方式)使用场景:
\\n实际场景:
\\nContainer(\\n width: 200.w,\\n height: 100.h,\\n decoration: BoxDecoration(\\n color: Colors.white,\\n borderRadius: BorderRadius.all(Radius.circular(8.radius)), // 另一种圆角适配方式\\n ),\\n child: Center(child: Text(\'Card\')),\\n)\\n
\\n.w
和 .h
用于宽高适配,确保控件的尺寸根据屏幕的尺寸自动缩放。.sp
用于字号适配,确保字体大小在不同设备上的一致性,并且能适应用户的字体缩放设置。.r
用于圆角适配,确保圆角在不同屏幕上显示一致。.textScaleFactor
用于调整字体大小,适应系统字体大小的调整。.setWidth()
和 .setHeight()
用于根据屏幕宽度或高度比例自定义控件尺寸。抽象类的力量
\\n抽象类允许定义一组共享的属性和行为
,但不提供具体的实现细节
。这为子类提供了一个清晰的契约,确保所有子类都遵循相同的接口。抽象类包含具体的方法实现
和没有实现的抽象方法
,后者必须由子类来实现。这种方式不仅增强了系统的可维护性
,还提高了代码的复用性
和一致性
。
混入的魅力
\\n混入(mixin
)则是 Dart
提供的一种轻量级多重继承形式,它允许将多个类的功能组合到一个新的类中,而不需要复杂的继承层次结构。混入可以包含方法
和属性
,但不能有构造函数
。通过混入,可以轻松地重用代码片段
,并将不同的功能模块组合在一起,从而构建出高度灵活和可扩展的系统。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\n抽象类是不能被实例化
的类,主要用于定义其他类的模板
或契约
。
特点:
\\n默认行为
。子类必须实现
这些方法。使用abstract
关键字来定义抽象类;
abstract class Animal {\\n // 成员变量\\n String name;\\n\\n // 构造函数\\n Animal(this.name);\\n\\n // 具体方法:所有动物都可以吃东西\\n void eat() {\\n print(\'$name is eating.\');\\n }\\n\\n // 抽象方法:不同动物发出的声音不同\\n void sound(); // 没有实现\\n}\\n
\\n详细说明:Animal
是一个抽象类,它定义了所有动物都有的行为(如 eat()
),以及每个具体动物必须实现
的行为(如 sound()
)。
要使用抽象类,必须通过继承并实现其抽象方法
来创建具体的子类:
class Dog extends Animal {\\n // 调用父类构造函数\\n Dog(super.name);\\n\\n // 实现抽象方法\\n @override\\n void sound() {\\n print(\'$name barks.\');\\n }\\n}\\n\\nvoid main() {\\n // 不能直接实例化抽象类 提示 Abstract classes can\'t be instantiated.\\n // var animal = Animal(\'Generic Animal\'); // 这行代码会报错\\n\\n // 实例化具体子类\\n var dog = Dog(\'Buddy\');\\n dog.eat(); // 输出: Buddy is eating.\\n dog.sound(); // 输出: Buddy barks.\\n}\\n
\\n详细说明:Animal
是一个抽象类,它定义了一个具体方法 eat()
和一个抽象方法 sound()
。可以创建 Dog
的实例, 并实现 sound()
方法,同时可以直接使用 eat()
方法。
属性
和行为
,确保所有子类遵循相同的结构。提供默认实现
,减少重复代码
。实现特定的行为
。Diamond Problem
)多继承的二义性,也称为“菱形问题”
或“钻石问题”
,是面向对象编程中一个多继承语言可能遇到的问题。当一个类从两个或多个基类派生
,而这些基类又共同继承自同一个祖先类时
,可能会出现方法或属性的二义性
,即编译器无法确定应该使用哪一个版本的方法或属性
。
菱形继承结构
\\n考虑以下经典的菱形继承结构
:
A\\n / \\\\\\n B C\\n \\\\ /\\n D\\n
\\nA
是最顶层的基类
。B
和 C
分别继承自 A
。D
同时继承自 B
和 C
。在这种结构下,如果类 A
中有一个方法 method()
,那么类 D
会通过 B
和 C
继承两次 method()
。这会导致编译器无法确定应该调用哪一个版本的 method()
,从而产生二义性问题
。
代码示例:
\\nclass A {\\npublic:\\n void method() {\\n std::cout << \\"Method from A\\" << std::endl;\\n }\\n};\\n\\nclass B : public A {\\npublic:\\n // B does not override method()\\n};\\n\\nclass C : public A {\\npublic:\\n // C does not override method()\\n};\\n\\nclass D : public B, public C {\\npublic:\\n // What happens here?\\n};\\n\\n
\\n上述示例中,D
类同时继承了 B
和 C
,而 B
和 C
都继承了 A
。因此,D
类实际上有两个 A
的副本,每个副本都有自己的 method()
方法。当尝试创建 D
的实例并调用 method()
时,编译器不知道应该调用哪个版本的 method()
,这就导致了二义性。
int main() {\\n D d;\\n d.method(); // 编译错误:二义性\\n}\\n
\\n解决方法:
\\n不同编程语言有不同的机制来解决这个二义性问题。以下是几种常见的解决方案:
\\nVirtual Inheritance
):\\nC++
中,可以通过声明虚基类
来确保只有一个 A
的副本被继承。 class B : virtual public A {};\\n class C : virtual public A {};\\n
\\nJava
和 Dart
等语言中,类不能多重继承
,但可以通过接口
和 Mixin
来实现类似的效果,避免直接的多继承带来的二义性问题。Java 8+
),如果接口中有默认方法冲突,子类必须显式地覆盖并提供实现,以消除二义性。小结:
\\n多继承的二义性问题是由于在一个类从多个基类继承
时,这些基类又共享同一个祖先类,导致方法
或属性
的重复继承和不确定调用路径。不同的编程语言有不同的机制来解决这个问题,包括虚基类
、接口
与 Mixin
的使用、以及明确指定调用路径等。
Mixin
(混入
)是 Dart
提供的一种轻量级多重继承形式
,允许将多个类的功能组合到一个新的类中,而不需要复杂的继承层次结构
。
特点:
\\nMixin
来组合多种功能。复杂性
和二义性
问题。不改变现有类层次结构的情况下添加新功能
。Mixin
在 Dart
中,定义 Mixin
使用 mixin
关键字:
// 定义Mixin\\nmixin Flyable {\\n void fly() {\\n print(\'Flying...\');\\n }\\n}\\n\\n// 定义Mixin\\nmixin Swimmable {\\n void swim() {\\n print(\'Swimming...\');\\n }\\n}\\n\\n// 使用 Mixin 的类\\nclass Bird with Flyable, Swimmable {\\n void chirp() {\\n print(\'Chirp chirp!\');\\n }\\n}\\n\\nvoid main() {\\n var bird = Bird();\\n bird.chirp(); // 输出: Chirp chirp!\\n bird.fly(); // 输出: Flying...\\n bird.swim(); // 输出: Swimming...\\n}\\n
\\n详细说明:Bird
类通过 with
关键字引入了两个 Mixin
:Flyable
和 Swimmable
。这样,Bird
类不仅拥有自己的方法 chirp()
,还获得了来自 Mixin
的 fly()
和 swim()
方法。
Mixin
的优势Dart
不支持传统意义上的多重继承,但 Mixin
提供了一种安全且简单
的方式来实现类似的效果。Mixin
中,然后在需要的地方轻松引入。Mixin
不涉及复杂的继承关系,因此不会使类层次结构变得混乱。Mixin
的限制Mixin
不能定义构造函数,这意味着它们不能在初始化时执行特定的逻辑。Mixin
不能直接访问其父类的成员变量或方法
,除非这些成员是通过接口可见的
。Mixin
的高级用法1、Mixin
组合:可以将多个 Mixin 组合在一起,创建更复杂的行为组合。\\n如上述3.2
中的代码示例。
2、Mixin
约束条件(On Clause
):可以为 Mixin
指定约束条件,确保只有满足这些条件的类才能使用该 Mixin
,通过 on
关键字来实现。
// 定义一个 Mixin,并指定它只能应用于实现了特定接口的类\\nmixin CanEat on Animal {\\n void eat() {\\n print(\'$name is eating.\');\\n }\\n}\\n// 抽象类 Animal\\nabstract class Animal {\\n String name;\\n\\n // 构造函数\\n Animal(this.name);\\n\\n // 抽象方法\\n void makeSound();\\n}\\n\\n// Dog 类继承自 Animal 并使用 CanEat Mixin\\nclass Dog extends Animal with CanEat {\\n Dog(super.name);\\n\\n @override\\n void makeSound() {\\n print(\'$name barks.\');\\n }\\n}\\n\\n// Cat 类继承自 Animal 并使用 CanEat Mixin\\nclass Cat extends Animal with CanEat {\\n Cat(super.name);\\n\\n @override\\n void makeSound() {\\n print(\'$name meows.\');\\n }\\n}\\n\\nvoid main() {\\n var dog = Dog(\'Buddy\');\\n dog.makeSound(); // 输出: Buddy barks.\\n dog.eat(); // 输出: Buddy is eating.\\n\\n var cat = Cat(\'Alice\');\\n cat.makeSound(); // 输出: Alice meows.\\n cat.eat(); // 输出: Alice is eating.\\n}\\n
\\n详细说明:CanEat Mixin
只能被实现了 Animal
接口的类使用。如果尝试将其应用于未实现 Animal
的类,编译器会报错
。
3、Mixin
组合、约束及继承顺序:可以将组合
、约束及继承
一起创建更复杂的实现。
mixin A {\\n void methodA() {\\n print(\'Method A from Mixin A\');\\n }\\n}\\nmixin B {\\n void methodB() {\\n print(\'Method B from Mixin B\');\\n }\\n}\\n\\nmixin C on A, B {\\n void methodC() {\\n print(\'Method C from Mixin C\');\\n super.methodA();\\n super.methodB();\\n }\\n}\\n\\nclass MyClass with A, B, C {\\n void myMethod() {\\n methodA();\\n methodB();\\n methodC();\\n }\\n}\\n\\nvoid main() {\\n var obj = MyClass();\\n obj.myMethod();\\n}\\n\\n输出:\\nMethod A from Mixin A\\nMethod B from Mixin B\\nMethod C from Mixin C\\nMethod A from Mixin A\\nMethod B from Mixin B\\n
\\n详细说明:
\\n首先,有 mixin A
和 mixin B
,它们分别定义了 methodA
和 methodB
方法,用于输出相应的信息。
mixin C
定义了 methodC
方法,并且使用 on A, B
表示 C
可以混入的类必须已经包含 A
和 B
的特性。在 methodC
中,使用 super.methodA()
和 super.methodB()
来调用 A
和 B
中的方法,确保调用的是父类的方法,避免混淆。
MyClass
现在先混入 A
和 B
,然后混入 C
,这样就满足了 C
的混入条件。MyClass
还定义了 myMethod
方法,用于调用 methodA
、methodB
和 methodC
。
在 main
函数中,创建 MyClass
的实例 obj
,并调用 myMethod
,该方法会依次调用 methodA
、methodB
和 methodC
并输出相应的信息。
Dart
中的抽象类和 Mixin
各自提供了独特的优势,帮助开发者构建健壮
、灵活且易于维护
的系统。抽象类通过定义公共接口和提供默认实现,确保了代码的一致性和可预测性;而 Mixin
通过灵活地组合功能,提升了代码的复用性和扩展性。理解并合理运用这两个特性,可以使应用程序更加模块化
、易维护及扩展
。
\\n","description":"前言 抽象类的力量\\n\\n抽象类允许定义一组共享的属性和行为,但不提供具体的实现细节。这为子类提供了一个清晰的契约,确保所有子类都遵循相同的接口。抽象类包含具体的方法实现和没有实现的抽象方法,后者必须由子类来实现。这种方式不仅增强了系统的可维护性,还提高了代码的复用性和一致性。\\n\\n混入的魅力\\n\\n混入(mixin)则是 Dart 提供的一种轻量级多重继承形式,它允许将多个类的功能组合到一个新的类中,而不需要复杂的继承层次结构。混入可以包含方法和属性,但不能有构造函数。通过混入,可以轻松地重用代码片段,并将不同的功能模块组合在一起,从而构建出高度灵活和可扩展的系统。…","guid":"https://juejin.cn/post/7460631925275901987","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-17T03:52:04.119Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/80bd36578ab44fedb83306e5533a765c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1737691435&x-signature=xqYX2NjYXAE9jZkrDKYhiGyNuH4%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"Dart 之消息、继承、多态","url":"https://juejin.cn/post/7460460021703065640","content":"码字不易,记得 关注 + 点赞 + 收藏 + 评论
\\n
消息是沟通的桥梁,继承是智慧的传递,多态则是变化的魔法。今天,我们将一起深入探索Dart中的这些奇妙特性——消息、继承和多态。
\\n消息是对象之间进行通信的一种构造。这种构造可以是一个指令,也可以是一个请求。一般是发送者对象发送一个指令或请求,接收者对象对这个请求进行处理。
\\n在面向对象编程中,消息通常包含以下几个元素:
\\n消息传递是对象间进行通信和协作的桥梁。
\\n如何传递?
\\n消息传递示例:
\\n/// Receiver 类,用于接收消息\\nclass Receiver {\\n void receiveMessage(String message) {\\n print(\\"$message\\");\\n }\\n}\\n\\n/// Sender 类,用于发送消息\\nclass Sender {\\n void sendMessage(Receiver receiver,String message) {\\n receiver.receiveMessage(message);\\n }\\n}\\n\\nvoid main() {\\n Receiver receiver = Receiver();\\n Sender sender = Sender();\\n // 发送消息\\n sender.sendMessage(receiver,\\"Hello,Dart!\\"); // 输出:Hello,Dart!\\n}\\n
\\n继承是子类和父类共享数据和方法的一种机制。可以说是一种智慧的传递,如同父亲将其所拥有的财富(父类的数据或方法)传递给儿子。
\\n注意: 子类一旦继承了父类,就拥有了父类的所有非私有的方法和属性。\\n
继承代码示例:
\\n/// 定义一个Animal类\\nclass Animal{\\n String name;\\n Animal(this.name);\\n void roar(){\\n print(\'The ${this.name} is roaring\');\\n }\\n}\\n\\n/// 定义狗这个类,继承自Animal类\\nclass Dog extends Animal{\\n Dog(super.name); // 拥有Animal的属性name\\n // 在继承的基础上重写父类的roar()方法\\n @override\\n void roar(){\\n print(\'The ${super.name} is running!\'); // 定制子类需要的内容\\n }\\n}\\nvoid main() {\\n Animal dog_a = Animal(\'dog\');\\n dog_a.roar(); // The dog is roaring\\n Dog dog_b = Dog(\'dog\');\\n dog_b.roar(); // The dog is running!\\n}\\n
\\n多态是变化的魔法,其在同一个接口下可以有不同的实现形式。可以理解为多态就是同一个接口,使用不同的实例而执行不同操作。如下图中的打印机一样,同一个接口传入不同的值得到不同的实例,不同的实例呈现出不同的结果(黑白、彩色)。
\\n/// 定义一个Animal类\\nclass Animal{\\n String name;\\n Animal(this.name);\\n void roar(){\\n print(\'The ${this.name} is roaring\');\\n }\\n}\\n\\n/// 定义狗这个类继承自Animal类\\nclass Dog extends Animal{\\n Dog(super.name); // 拥有Animal的属性name\\n // 在继承的基础上重写父类的roar()方法\\n @override\\n void roar(){\\n print(\'${super.name} 汪汪\'); // 定制子类需要的内容\\n }\\n}\\n/// 定义猫这个类继承自Animal类\\nclass Cat extends Animal{\\n Cat(super.name); // 拥有Animal的属性name\\n // 在继承的基础上重写父类的roar()方法\\n @override\\n void roar(){\\n print(\'${super.name} 喵喵\'); // 定制子类需要的内容\\n }\\n}\\nvoid main() {\\n List<Animal> animals = [Dog(\'小狗\'),Cat(\'小猫\')];\\n for(var animal in animals){\\n animal.roar();\\n }\\n}\\n// 输出:\\n//小狗 汪汪\\n//小猫 喵喵\\n
\\n消息作为沟通基石,促进了信息流通;继承承载智慧,实现了代码复用与扩展;多态如同变化魔法,让对象行为灵活多变。
","description":"前言 消息是沟通的桥梁,继承是智慧的传递,多态则是变化的魔法。今天,我们将一起深入探索Dart中的这些奇妙特性——消息、继承和多态。\\n\\n一、消息\\n\\n1.1、消息的定义\\n\\n消息是对象之间进行通信的一种构造。这种构造可以是一个指令,也可以是一个请求。一般是发送者对象发送一个指令或请求,接收者对象对这个请求进行处理。\\n\\n在面向对象编程中,消息通常包含以下几个元素:\\n\\n发送者:发起消息的对象。它希望与另一个对象进行通信,以便执行某些操作或获取某些信息。\\n接收者:接收消息的对象。它根据消息的内容执行相应的操作或返回信息。\\n操作:消息中指定的操作或方法…","guid":"https://juejin.cn/post/7460460021703065640","author":"好的佩奇","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-16T15:24:10.368Z","media":[{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7a4a05582c9e440fa776bd5a67b70258~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5aW955qE5L2p5aWH:q75.awebp?rk3s=f64ab15b&x-expires=1737645850&x-signature=68n%2FpdTSJRDzY0zJ8WAz5cQc6q4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/60868d5f2bd9431e85865db7283f61b6~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5aW955qE5L2p5aWH:q75.awebp?rk3s=f64ab15b&x-expires=1737645850&x-signature=SMfSGx0APxeLu0v808YAuakrs4Q%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2446d064a6ed483e84f2809f9bf4540e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5aW955qE5L2p5aWH:q75.awebp?rk3s=f64ab15b&x-expires=1737645850&x-signature=RJ9UubXT2hykegk5fW2sF3yCSDU%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6e0acfe34945413caae5aecd30f9155a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5aW955qE5L2p5aWH:q75.awebp?rk3s=f64ab15b&x-expires=1737645850&x-signature=a4W1%2FM1jcLC1sJ7QV8zvwRVzRtA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7b9a0b50f8a2451788a9b2fb6bf35bd7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5aW955qE5L2p5aWH:q75.awebp?rk3s=f64ab15b&x-expires=1737645850&x-signature=RmhUoejsuvAcidNTpvSyt4R7m50%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"系统化掌握Dart编程之面向对象编程","url":"https://juejin.cn/post/7460158378968645667","content":"面向对象
编程 —— 构建灵活、可维护软件的基石。
理想中的OOP
:
实际中的OOP
:
\\n面向对象编程(
Object-Oriented Programming
, OOP
)是一种以“对象”
为核心概念的编程范式。它鼓励我们将现实世界中的实体
抽象为计算机世界中的对象
,每个对象都是数据和作用于数据操作的封装体。通过这种方式,OOP
使我们可以用更直观
、更贴近自然
的方式描述问题域,并且能够轻松地模拟复杂的交互关系
。
OOP
的核心思想在于封装
、继承
、多态
和 抽象
四大支柱。
除了上述特性外,OOP
还提倡遵循一些设计原则,如 SOLID
原则,它们指导我们编写易于维护和扩展
的代码。此外,各种设计模式也为解决常见的设计挑战提供了经过验证的解决方案
。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\n在学习OOP
时,先是引入对象的概念,再对对象进行抽象的角度引入类的概念。但当设计和实现时,首先接触的不是对象,而是类和类层次结构
。
类具有实例化功能
,包括实例生成(由类的构造函数(Constructor
)完成)。类的实例化功能决定了类及其实例具有如下特征:
数据结构
,承受的是同一方法集合所定义的操作
,因而具有规律相同的行为
。持有不同的值
,因而可以具有不同的状态
。初值
)可以在实例化时确定。class
关键字来定义的。唯一标识
,遵循标识符的命名规则。{}
表示,其内部包含了类的定义内容,同时限定了类的作用范围(作用域
)。成员变量
)和方法(成员函数
),以及构造函数
等。代码示例:
\\n///定义Dart类,所有的类都继承自Object\\nclass Person {\\n // 属性 (成员变量)\\n String name;\\n int age;\\n\\n // 构造函数\\n Person(this.name, this.age);\\n\\n // 方法 (成员函数)\\n void introduce() {\\n print(\'Hello, my name is $name and I am $age years old.\');\\n }\\n}\\n\\nvoid main() {\\n var person = Person(\'Alice\', 30);\\n person.introduce(); // 输出: Hello, my name is Alice and I am 30 years old.\\n}\\n
\\n谁知道这是一棵什么树?
\\n继承一般通过定义类之间的关系
来体现。在面向对象系统中,子类和父类之间的继承关系构成了这个系统的类层次结构,可以用树(对应于单继承
)这样的图来描述。
沿继承路径上溯
至它的一个基类,然后自顶向下
执行该子类所有父类的实例生成方法。顺序恰好相反
。class Student extends Person{\\n Student(super.name, super.age);\\n}\\n
\\n对象是类的实例
。其表现形式与一般数据类型相似,本质区别在于:
\\n\\n对象之间是通过
\\n消息传递方式进行通信的
。
在OOP
中,消息传递是对象间交互
的主要方式。对象被视为通过消息互相通信的实体
,可以接收
或拒绝
外界的消息。
接收对象
和消息
名称(通常是对象中的一个方法名
)。参数
,用于向对象传递额外信息
。简而言之,对象通过消息传递进行通信,消息包括目标对象
、方法名
和必要的参数
。这使得对象能够以一种解耦的方式互动,增强了系统的灵活性
和可维护性
。
class Student extends Person{\\n Student(super.name, super.age);\\n\\n void learn(String subject){\\n print(\'$name is learning $subject\');\\n }\\n}\\n\\nvoid main() {\\n var person = Person(\'Alice\', 30);\\n person.introduce(); // 输出: Hello, my name is Alice and I am 30 years old.\\n \\n var student = Student(\\"zhangsan\\", 18);\\n student.learn(\\"English\\");//输出:zhangsan is learning English\\n}\\n
\\n对象自身引用(self-Reference
)是编程语言中的一种特有的结构。不同编程语言中的名称不同,如C++
、Java
、Dart
中称为this
,在Object-C
中称为self
。
示例代码:
\\nclass Person {\\n // 属性 (成员变量)\\n String name;\\n int age;\\n\\n // 构造函数\\n Person(this.name, this.age);\\n}\\n
\\n重置
或覆盖
(Overriding
)是在子类中重新定义
父类中已经定义的方法,基本思想是通过一种动态绑定机制的支持,使得子类在继承父类接口定义的前提下用适合自己要求的实现去置换
父类中的相应实现。
代码示例:
\\n///定义Dart类,所有的类都继承自Object\\nclass Person {\\n // 属性 (成员变量)\\n String name;\\n int age;\\n\\n // 构造函数\\n Person(this.name, this.age);\\n \\n /// 重写父类的方法, 多态的最主要的表现形式\\n @override\\n String toString() {\\n return \'Person{name: $name, age: $age}\';\\n }\\n}\\n\\nvoid main() {\\n var person = Person(\'Alice\', 30);\\n person.toString(); \\n}\\n
\\ngetter
和 setter
Dart
支持显式定义 getter
和 setter
方法来访问
和修改
私有属性。
class Person {\\n String _name; // 私有属性,以下划线开头\\n\\n // 构造函数\\n Person(this._name);\\n\\n // Getter\\n String get name => _name;\\n\\n // Setter\\n set name(String value) {\\n if (value.isNotEmpty) {\\n _name = value;\\n } else {\\n throw ArgumentError(\'Name cannot be empty\');\\n }\\n }\\n\\n void introduce() {\\n print(\'Hello, my name is $_name.\');\\n }\\n}\\n\\nvoid main() {\\n var person = Person(\'Alice\');\\n person.introduce(); // 输出: Hello, my name is Alice.\\n\\n person.name = \'Bob\';\\n person.introduce(); // 输出: Hello, my name is Bob.\\n}\\n
\\n工厂构造函数用于返回现有的实例
或不同类型的对象
,而不必每次都创建新的实例。
///实现方式1\\nclass Logger {\\n static final Map<String, Logger> _cache = <String, Logger>{};\\n\\n final String name;\\n\\n Logger._internal(this.name);\\n\\n factory Logger(String name) {\\n if (_cache.containsKey(name)) {\\n return _cache[name]!;\\n } else {\\n final logger = Logger._internal(name);\\n _cache[name] = logger;\\n return logger;\\n }\\n }\\n}\\n\\n///实现方式2\\n// 定义抽象基类\\nabstract class Shape {\\n void draw();\\n}\\n\\n// 定义具体的子类\\nclass Circle implements Shape {\\n @override\\n void draw() {\\n print(\'Drawing a circle.\');\\n }\\n}\\n\\nclass Rectangle implements Shape {\\n @override\\n void draw() {\\n print(\'Drawing a rectangle.\');\\n }\\n}\\n\\nclass Square implements Shape {\\n @override\\n void draw() {\\n print(\'Drawing a square.\');\\n }\\n}\\n\\n// 使用工厂模式的类\\nclass ShapeFactory {\\n // 静态工厂方法\\n static Shape createShape(String type) {\\n switch (type.toLowerCase()) {\\n case \'circle\':\\n return Circle();\\n case \'rectangle\':\\n return Rectangle();\\n case \'square\':\\n return Square();\\n default:\\n throw ArgumentError(\'Unknown shape type: $type\');\\n }\\n }\\n}\\n\\nvoid main() {\\n //方式1\\n var logger1 = Logger(\'main\');\\n var logger2 = Logger(\'main\');\\n\\n print(logger1 == logger2); // 输出: true\\n \\n //方式2\\n // 创建不同类型的对象\\n try {\\n Shape shape1 = ShapeFactory.createShape(\'circle\');\\n shape1.draw(); // 输出: Drawing a circle.\\n\\n Shape shape2 = ShapeFactory.createShape(\'rectangle\');\\n shape2.draw(); // 输出: Drawing a rectangle.\\n\\n Shape shape3 = ShapeFactory.createShape(\'square\');\\n shape3.draw(); // 输出: Drawing a square.\\n } catch (e) {\\n print(e);\\n }\\n}\\n
\\n命名构造函数允许为类定义多个构造函数
,每个都有不同的名字
。
class Rectangle {\\n double width;\\n double height;\\n\\n // 默认构造函数\\n Rectangle(this.width, this.height);\\n\\n // 命名构造函数\\n Rectangle.square(double side) : width = side, height = side;\\n}\\n\\nvoid main() {\\n var rect = Rectangle(20, 15);\\n var square = Rectangle.square(10);\\n\\n print(\'Rectangle: ${rect.width}x${rect.height}\');\\n print(\'Square: ${square.width}x${square.height}\');\\n}\\n
\\n在 Dart
中,命名构造函数后面的冒号(:
)用于初始化列表(initializer list
)。初始化列表允许你在构造函数体执行之前初始化类的成员变量
。这是确保对象在创建时就处于有效状态
的一种方式。
初始化列表的作用:
\\n直接为成员变量赋值
。调用父类的构造函数
。简单的表达式
或方法调用
。class Person {\\n String name;\\n int age;\\n // 命名构造函数,使用初始化列表初始化成员变量\\n Person.guest(this.name, this.age);\\n}\\n
\\n在上述例子中,this.name
和 this.age
是初始化列表
的一部分,它们直接将构造函数参数赋值给类的成员变量。
class Rectangle {\\n double width;\\n double height;\\n double area;\\n\\n // 命名构造函数,使用初始化列表计算面积\\n Rectangle.square(double side) : width = side, height = side, area = side * side;\\n\\n void display() {\\n print(\'Rectangle: ${width}x${height}, Area: $area\');\\n }\\n}\\n\\nvoid main() {\\n var square = Rectangle.square(5);\\n square.display(); // 输出: Rectangle: 5.0x5.0, Area: 25.0\\n}\\n
\\n在上述例子中,area = side * side
是一个表达式,在初始化列表中计算并赋值给 area
成员变量。
class Animal {\\n String name;\\n\\n Animal(this.name);\\n\\n void sound() {\\n print(\'$name makes a sound.\');\\n }\\n}\\n\\nclass Dog extends Animal {\\n bool isTrained;\\n\\n // 命名构造函数,使用初始化列表调用父类构造函数\\n Dog(super.name, this.isTrained);\\n\\n @override\\n void sound() {\\n print(\'$name barks.\');\\n }\\n}\\n\\nvoid main() {\\n var dog = Dog(\'Buddy\', true);\\n dog.sound(); // 输出: Buddy barks.\\n}\\n
\\n在上述例子中,super(name)
在初始化列表中调用了父类 Animal
的构造函数,以确保父类的成员变量也被正确初始化。
OOP不仅仅是一组技术或规则
,更是一种思维方式。它教会我们如何将复杂的现实世界映射到软件中
,同时保持系统的清晰结构和良好的可维护性
。对于现代软件开发而言,掌握 OOP
是一项至关重要的技能,它为我们打开了通往高效
、优雅
编程的大门。
\\n","description":"前言 面向对象编程 —— 构建灵活、可维护软件的基石。\\n\\n理想中的OOP:\\n\\n实际中的OOP:\\n\\n面向对象编程(Object-Oriented Programming, OOP)是一种以“对象”为核心概念的编程范式。它鼓励我们将现实世界中的实体抽象为计算机世界中的对象,每个对象都是数据和作用于数据操作的封装体。通过这种方式,OOP 使我们可以用更直观、更贴近自然的方式描述问题域,并且能够轻松地模拟复杂的交互关系。\\n\\nOOP 的核心思想在于封装、继承、多态 和 抽象 四大支柱。\\n\\n除了上述特性外,OOP 还提倡遵循一些设计原则,如 SOLID 原则…","guid":"https://juejin.cn/post/7460158378968645667","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-16T03:51:27.377Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1b14b0dee3ff4e989602c57d7bb0d68e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1737604286&x-signature=D7RyAGCdC5ydyDl01007EenFlzA%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a3d17f8e0ce24c6fb928cdcbb762cb9a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1737604286&x-signature=T8mMbFXRVSqlXZFch4%2FCRZAOwLk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/dfcfb2f3e15d43e4a4843ce70f0359bd~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1737604286&x-signature=GMZ2hTFkoiGzRh%2Bwnv4nfq43%2BYQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ee9d4adbb53a46c4b0f283cb98112728~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1737604286&x-signature=6T7Rq73SDSGRxvcBUyOckfklCa0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7a3a9f4c031d497185224176aa8a66f8~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1737604286&x-signature=R44Gxqks43ynYBbZrUO852cpuMw%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"Flutter 常用滚动组件使用场景","url":"https://juejin.cn/post/7460002800464216102","content":"码字不易,记得 关注 + 点赞 + 收藏 + 评论
\\n
SingleChildScrollView
如果页面的内容较少并且你希望整体可滚动,可以使用 SingleChildScrollView
来包裹页面的所有内容。
SingleChildScrollView(\\n child: Column(\\n children: [\\n Container(\\n height: 200,\\n color: Colors.blue,\\n child: const Center(child: Text(\'部分内容\')),\\n ),\\n Container(\\n height: 200,\\n color: Colors.green,\\n child: const Center(child: Text(\'其他内容\')),\\n ),\\n // 可以继续添加更多内容\\n ],\\n ),\\n)\\n
\\nListView
如果你的内容是一个列表项,ListView
是最适合的滚动组件。ListView
会根据列表项的数量自动滚动,可以通过 ListView.builder
来动态构建列表项。
ListView(\\n children: [\\n Container(\\n height: 200,\\n color: Colors.blue,\\n child: const Center(child: Text(\'部分内容\')),\\n ),\\n Container(\\n height: 200,\\n color: Colors.green,\\n child: const Center(child: Text(\'其他内容\')),\\n ),\\n // 可以继续添加更多内容\\n ],\\n)\\n
\\n你可以根据页面布局需求,结合 SingleChildScrollView
和 ListView
等进行嵌套使用。
SingleChildScrollView(\\n child: Column(\\n children: [\\n Container(\\n height: 200,\\n color: Colors.blue,\\n child: const Center(child: Text(\'顶部内容\')),\\n ),\\n ListView.builder(\\n shrinkWrap: true, // 设置为 true 让 ListView 在嵌套滚动时不占据全部空间\\n physics: NeverScrollableScrollPhysics(), // 禁止内嵌的 ListView 滚动\\n itemCount: 10,\\n itemBuilder: (context, index) {\\n return Container(\\n height: 100,\\n color: Colors.orange,\\n child: Center(child: Text(\'列表项 $index\')),\\n );\\n },\\n ),\\n Container(\\n height: 200,\\n color: Colors.green,\\n child: const Center(child: Text(\'底部内容\')),\\n ),\\n ],\\n ),\\n)\\n
\\n如果页面有一个动态的顶部栏(例如 SliverAppBar
),可以通过使用 CustomScrollView
来实现与滚动内容的配合。
CustomScrollView(\\n slivers: [\\n SliverAppBar(\\n expandedHeight: 200.0,\\n floating: true,\\n pinned: true,\\n flexibleSpace: FlexibleSpaceBar(\\n title: Text(\'标题\'),\\n background: Image.network(\'https://example.com/image.jpg\', fit: BoxFit.cover),\\n ),\\n ),\\n SliverList(\\n delegate: SliverChildBuilderDelegate(\\n (context, index) {\\n return Container(\\n height: 100,\\n color: Colors.orange,\\n child: Center(child: Text(\'列表项 $index\')),\\n );\\n },\\n childCount: 50,\\n ),\\n ),\\n ],\\n)\\n
\\nSingleChildScrollView
:适用于页面内容较少的场景。ListView
:适用于有多个列表项且需要滚动的场景。CustomScrollView
:适用于结合 SliverAppBar
或自定义滚动效果的场景。Flutter DevTools是Flutter开发者用于调试和性能分析的重要工具。随着Flutter SDK的更新,DevTools也会不断引入新功能和改进。以下是一些DevTools的重要更新(注意:由于DevTools通常与Flutter SDK版本紧密相关,因此以下更新可能并不完全对应于特定的DevTools版本号,而是与Flutter SDK版本相关联):
\\n??
(if null operator)、??=
(null-aware assignment)、x?.p
(null-aware access)和x?.m()
(null-aware method invocation)等,这些特性使得在处理可能为null的变量时更加安全和便捷。Flutter packages 分为几种类型,主要包括:
\\n使用 Flutter CLI 创建一个新的 Dart package:
\\nflutter create --template=package my_dart_package\\n
\\n在 lib
目录下编写 Dart 代码,定义你的包的功能。例如,一个简单的数学运算库:
// lib/my_dart_package.dart\\nlibrary my_dart_package;\\n \\nclass MathUtils {\\n static int add(int a, int b) {\\n return a + b;\\n }\\n}\\n
\\nFlutter 插件通常包含 Dart 代码和平台特定代码。Dart 代码作为插件的接口,而平台特定代码则实现具体功能。
\\n在 pubspec.yaml
中指定支持的平台:
flutter:\\n plugin:\\n platforms:\\n android:\\n package: com.example.myplugin\\n pluginClass: MyPlugin\\n ios:\\n pluginClass: MyPlugin\\n
\\n使用 Flutter CLI 创建一个新的插件:
\\nflutter create --template=plugin my_plugin\\n
\\n在 android
和 ios
目录下实现平台特定代码。例如,在 Android 中:
// android/src/main/java/com/example/myplugin/MyPlugin.java\\npackage com.example.myplugin;\\n \\nimport io.flutter.embedding.engine.plugins.FlutterPlugin;\\nimport io.flutter.embedding.engine.plugins.activity.ActivityAware;\\nimport io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;\\nimport io.flutter.plugin.common.MethodCall;\\nimport io.flutter.plugin.common.MethodChannel;\\nimport io.flutter.plugin.common.MethodChannel.MethodCallHandler;\\nimport io.flutter.plugin.common.MethodChannel.Result;\\nimport android.app.Activity;\\nimport android.content.Context;\\n \\npublic class MyPlugin implements FlutterPlugin, MethodCallHandler, ActivityAware {\\n private MethodChannel channel;\\n private Context applicationContext;\\n private Activity activity;\\n \\n @Override\\n public void onAttachedToEngine(FlutterPluginBinding flutterPluginBinding) {\\n channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), \\"my_plugin\\");\\n channel.setMethodCallHandler(this);\\n applicationContext = flutterPluginBinding.getApplicationContext();\\n }\\n \\n @Override\\n public void onMethodCall(MethodCall call, Result result) {\\n if (call.method.equals(\\"getPlatformVersion\\")) {\\n String version = android.os.Build.VERSION.RELEASE;\\n result.success(version);\\n } else {\\n result.notImplemented();\\n }\\n }\\n \\n @Override\\n public void onDetachedFromEngine(FlutterPluginBinding binding) {\\n channel.setMethodCallHandler(null);\\n }\\n \\n @Override\\n public void onAttachedToActivity(ActivityPluginBinding binding) {\\n activity = binding.getActivity();\\n }\\n \\n @Override\\n public void onDetachedFromActivityForConfigChanges() {\\n activity = null;\\n }\\n \\n @Override\\n public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) {\\n activity = binding.getActivity();\\n }\\n \\n @Override\\n public void onDetachedFromActivity() {\\n activity = null;\\n }\\n}\\n
\\n如果你已经有一个 Dart package,并想添加平台支持,可以手动添加 android
和 ios
目录,并按照上述方式实现平台代码。
Dart 代码通过 MethodChannel
与平台代码通信。例如:
// lib/my_plugin.dart\\nimport \'package:flutter/services.dart\';\\n \\nclass MyPlugin {\\n static const MethodChannel _channel = const MethodChannel(\'my_plugin\');\\n \\n static Future<String?> get platformVersion async {\\n final String? version = await _channel.invokeMethod(\'getPlatformVersion\');\\n return version;\\n }\\n}\\n
\\n在 example
目录下创建一个 Flutter 应用,用于测试你的插件。
使用 Flutter CLI 创建一个新的 FFI 插件(虽然 Flutter CLI 目前没有直接支持 FFI 插件的模板,但你可以手动创建):
\\nflutter create --template=package my_ffi_plugin\\n
\\n编写本地(C/C++)代码,并编译成共享库(如 .so
、.dylib
、.dll
)。
使用 Dart 的 dart:ffi
库绑定本地代码。例如:
// lib/my_ffi_plugin.dart\\nimport \'dart:ffi\';\\nimport \'package:ffi/ffi.dart\';\\n \\ntypedef NativeAddFunc = NativeFunction<Int32 Function(Int32, Int32)>;\\ntypedef AddFunc = int Function(int, int);\\n \\nclass MyFFIPlugin {\\n static DynamicLibrary _openLibrary() {\\n if (Platform.isAndroid) {\\n return DynamicLibrary.open(\\"libmy_ffi_native.so\\");\\n } else if (Platform.isLinux) {\\n return DynamicLibrary.open(\\"libmy_ffi_native.so\\");\\n } else if (Platform.isMacOS) {\\n return DynamicLibrary.open(\\"libmy_ffi_native.dylib\\");\\n } else if (Platform.isWindows) {\\n return DynamicLibrary.open(\\"my_ffi_native.dll\\");\\n } else {\\n throw UnsupportedError(\\"This platform is not supported\\");\\n }\\n }\\n \\n static AddFunc? _addFunc;\\n \\n static int add(int a, int b) {\\n _addFunc ??= _openLibrary()\\n .lookup<NativeFunction<Int32 Function(Int32, Int32)>>(\'add\')\\n .asFunction<AddFunc>();\\n return _addFunc!(a, b);\\n }\\n}\\n
\\n在你的 Dart 代码中调用绑定的函数:
\\nvoid main() {\\n print(MyFFIPlugin.add(3, 4)); // 输出 7\\n}\\n
\\n在 lib
目录下使用 DartDoc 生成 API 文档。例如,在 my_dart_package.dart
文件顶部添加文档注释:
/// 一个简单的数学运算库。\\nlibrary my_dart_package;\\n \\n/// 提供基本的数学运算功能。\\nclass MathUtils {\\n /// 返回两个整数的和。\\n static int add(int a, int b) {\\n return a + b;\\n }\\n}\\n
\\n运行 flutter pub publish --dry-run
检查并生成文档。
确保你的包包含一个 LICENSE
文件,描述你的包的许可证类型。
使用 flutter pub publish
命令将你的包发布到 pub.dev。
对于不同平台,你可能需要处理特定的依赖。例如,在 android/build.gradle
中添加 Android 依赖,在 ios/Podfile
中添加 iOS 依赖,或在 web/
目录下添加 Web 特定的依赖。
// android/build.gradle\\ndependencies {\\n implementation \'com.google.android.material:material:1.4.0\'\\n}\\n# ios/Podfile\\nplatform :ios, \'10.0\'\\n \\ntarget \'Runner\' do\\n use_frameworks!\\n pod \'SomeiOSLibrary\', \'~> 1.0\'\\nend\\n
\\npackage:flutter_lints
是Flutter官方推荐的一个linting包,用于鼓励良好的编码实践。以下是package:flutter_lints
的详细使用流程:
确保你的Flutter开发环境已经正确配置,并且你的项目是基于Flutter框架构建的。
\\npubspec.yaml
:在项目的根目录下找到pubspec.yaml
文件,并添加flutter_lints
作为开发依赖(dev_dependency)。dev_dependencies:\\n flutter_lints: ^最新版本号\\n
\\n请注意,你应该使用flutter_lints
的最新稳定版本。你可以在pub.dev上找到最新版本号。
flutter pub get
:在终端中运行flutter pub get
命令,以安装新添加的依赖。analysis_options.yaml
analysis_options.yaml
文件,你需要创建一个。analysis_options.yaml
文件中,添加以下内容来激活flutter_lints
中的推荐lint规则。yaml复制代码\\ninclude: package:flutter_lints/flutter.yaml\\n
\\n如果你的项目已经有一个自定义的analysis_options.yaml
文件,你只需要在文件的顶部添加上述include:
指令即可。
如果你想自定义lint规则,你可以在analysis_options.yaml
文件中添加或修改linter:
部分下的rules:
子部分。例如:
linter:\\n rules:\\n avoid_print: false # 禁用avoid_print规则\\n prefer_single_quotes: true # 启用prefer_single_quotes规则\\n
\\n你可以根据需要启用或禁用特定的lint规则。所有可用的lint规则及其文档可以在Dart Linter规则索引上找到。
\\nflutter analyze
命令来手动调用lint分析器。在终端中运行此命令,lint分析器将扫描你的项目代码,并输出任何识别到的问题。根据lint分析器识别到的问题,你需要对代码进行相应的修复。修复可能包括重构代码、添加缺失的注释、修复类型错误等。
\\n如果你的项目使用了持续集成(CI)服务,你可以配置CI服务在每次代码更改时自动运行flutter analyze
命令。这将确保你的代码始终符合良好的编码实践,并在问题出现时及时得到通知。
Flutter中的主题(Theme)是应用于整个应用程序、屏幕或视图层次结构的样式集合,它可以帮助开发者统一应用的整体风格,提高用户体验。以下是对Flutter主题的详细介绍:
\\n在Flutter中,主题是通过ThemeData
类来定义的。ThemeData
类包含了应用的颜色、字体、图标等样式信息。通过设置主题,可以实现应用的整体风格统一。
定义主题:
\\n在Flutter应用中,可以通过MaterialApp
组件的theme
属性来设置全局主题。例如:
void main() {\\n runApp(MaterialApp(\\n home: MyAppHome(),\\n theme: ThemeData(\\n primarySwatch: Colors.blue,\\n accentColor: Colors.blueAccent,\\n fontFamily: \'Arial\',\\n ),\\n ));\\n}\\n
\\n在上面的代码中,我们定义了一个全局主题,其中主色调为蓝色,辅助色调为蓝色点缀色,字体系列为Arial。
\\n使用主题:
\\n在Flutter组件中,可以通过Theme.of(context)
方法来获取当前主题,并使用主题中的样式。例如:
Text(\'Hello World\', style: Theme.of(context).textTheme.headline6)\\n
\\n在上面的代码中,我们获取了当前主题中的headline6
文本样式,并将其应用于Text
组件。
Flutter提供了丰富的自定义和扩展主题的功能,以满足不同开发者的需求。
\\n自定义颜色:
\\n可以通过在ThemeData
中设置colorScheme
属性来自定义颜色方案。例如:
ThemeData(\\n colorScheme: ColorScheme.fromSeed(\\n seedColor: Colors.green,\\n background: Colors.white,\\n error: Colors.red,\\n onTertiary: Colors.orange,\\n ),\\n)\\n
\\n在上面的代码中,我们定义了一个基于绿色种子颜色的颜色方案,并设置了背景色、错误色和次要文字颜色等。
\\n自定义文本样式:
\\n可以通过在ThemeData
中设置textTheme
属性来自定义文本样式。例如:
ThemeData(\\n textTheme: TextTheme(\\n headline6: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),\\n bodyText2: TextStyle(fontSize: 16, color: Colors.grey),\\n ),\\n)\\n
\\n在上面的代码中,我们自定义了headline6
和bodyText2
两种文本样式。
自定义AppBar样式:
\\n可以通过在ThemeData
中设置appBarTheme
属性来自定义AppBar的样式。例如:
ThemeData(\\n appBarTheme: AppBarTheme(\\n elevation: 0,\\n color: Colors.white,\\n iconTheme: IconThemeData(color: Colors.black),\\n textTheme: TextTheme(\\n headline6: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),\\n ),\\n ),\\n)\\n
\\n在上面的代码中,我们自定义了AppBar的高度、背景色、图标颜色和标题样式。
\\n自定义按钮样式:
\\n可以通过在ThemeData
中设置buttonTheme
属性来自定义按钮的样式。例如:
ThemeData(\\n buttonTheme: ButtonThemeData(\\n shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),\\n buttonColor: Colors.blue,\\n textTheme: ButtonTextTheme.primary,\\n ),\\n)\\n
\\n在上面的代码中,我们自定义了按钮的形状、颜色和文本样式。
\\nFlutter还支持动态主题切换功能,可以根据用户的偏好或系统设置自动切换主题。
\\n实现动态主题切换:
\\n可以通过使用ThemeMode
枚举来定义主题模式(亮色模式、深色模式或系统模式),并通过在MaterialApp
组件中设置themeMode
属性来实现动态主题切换。例如:
class MyApp extends StatefulWidget {\\n @override\\n _MyAppState createState() => _MyAppState();\\n}\\n \\nclass _MyAppState extends State<MyApp> {\\n ThemeMode _themeMode = ThemeMode.system;\\n \\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n title: \'Flutter Dynamic Theme\',\\n theme: ThemeData.light(),\\n darkTheme: ThemeData.dark(),\\n themeMode: _themeMode,\\n home: HomePage(\\n onThemeChanged: (ThemeMode themeMode) {\\n setState(() {\\n _themeMode = themeMode;\\n });\\n },\\n ),\\n );\\n }\\n}\\n \\nclass HomePage extends StatelessWidget {\\n final Function(ThemeMode) onThemeChanged;\\n \\n const HomePage({super.key, required this.onThemeChanged});\\n \\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(title: Text(\'动态主题切换\')),\\n body: Column(\\n children: [\\n ElevatedButton(\\n onPressed: () => onThemeChanged(ThemeMode.light),\\n child: Text(\'切换到亮色模式\'),\\n ),\\n ElevatedButton(\\n onPressed: () => onThemeChanged(ThemeMode.dark),\\n child: Text(\'切换到深色模式\'),\\n ),\\n ElevatedButton(\\n onPressed: () => onThemeChanged(ThemeMode.system),\\n child: Text(\'跟随系统设置\'),\\n ),\\n ],\\n ),\\n );\\n }\\n}\\n
\\n在上面的代码中,我们定义了一个支持动态主题切换的Flutter应用。通过点击按钮,可以在亮色模式、深色模式和系统模式之间切换。
\\n自定义主题:
\\n除了支持动态主题切换外,Flutter还支持自定义主题。可以通过在MaterialApp
组件中设置theme
属性为自定义的ThemeData
对象来实现自定义主题。例如:
ThemeData _customTheme() {\\n return ThemeData(\\n scaffoldBackgroundColor: Colors.yellow,\\n // 其他自定义样式...\\n );\\n}\\n \\n// 在MaterialApp中使用自定义主题\\nMaterialApp(\\n title: \'Flutter Custom Theme\',\\n theme: _customTheme(),\\n // 其他属性...\\n)\\n
\\n在上面的代码中,我们定义了一个自定义主题,并将其应用于MaterialApp
组件中。
默认主题变更
\\nuseMaterial3
标志默认为true
,这意味着应用程序将自动采用Material 3设计规范的主题。回退到Material 2
\\nuseMaterial3
默认为true
,但开发者仍然可以通过将useMaterial3
设置为false
来暂时回退到Material 2的行为。然而,这只是一个临时解决方案,Material 2的实现和useMaterial3
标志最终将被移除。颜色方案的更新
\\nThemeData.colorScheme
的默认值已更新以匹配Material 3设计规范。ColorScheme.fromSeed
构造函数,它可以根据给定的种子颜色生成一个符合Material 3设计系统要求的颜色方案。ColorScheme.fromSeed
生成的颜色方案来确保UI的正确显示。背景色和表面着色
\\nColorScheme.surfaceTint
,它表示一个提升(elevated)组件的着色。surfaceTint
和shadowColor
来表示提升效果。文字主题的更新
\\nThemeData.textTheme
的默认值已更新以匹配Material 3的默认设置。组件的迁移
\\nBottomNavigationBar
已被替换为新的NavigationBar
,Drawer
已被替换为NavigationDrawer
。应用栏的变化
\\nColorScheme.surfaceTint
颜色来与内容创建分隔。选项卡栏的变化
\\nTabBar
组件:主要(primary)和次要(secondary)。分段按钮
\\nSegmentedButton
是ToggleButtons
的更新版本,具有完全圆角、不同的布局高度和大小,并使用DartSet
来确定选定的项目。迁移代码示例
\\nFlutter的国际化与本地化功能使得应用能够在不同的语言和地区环境中运行,满足不同用户的文化和语言需求。以下是对Flutter应用本地化的全面介绍,涵盖从配置到高级操作的各个方面。
\\n安装依赖
\\npubspec.yaml
文件中添加flutter_localizations
依赖。配置本地化资源
\\nlib/l10n
目录下创建ARB(Application Resource Bundle)文件,用于存储不同语言的翻译资源。messages_xx.arb
(xx为语言代码,如en、zh等)的文件。配置MaterialApp
\\nMaterialApp
的构造函数中,通过localizationsDelegates
和supportedLocales
参数配置本地化支持。localizationsDelegates
参数指定了哪些本地化代理将被用于加载本地化资源。supportedLocales
参数定义了应用支持的语言环境列表。通过更改MaterialApp
的locale
属性,可以实现语言的动态切换。这通常涉及到一个状态管理,以便在应用的不同部分之间共享当前的语言设置。
创建ARB文件
\\nlib/l10n
目录下的相应语言子目录中,创建或编辑ARB文件,添加或更新翻译资源。使用占位符
\\n处理复数和选项
\\n在编写ARB文件时,需要注意避免语法解析错误。确保文件的格式正确,键和值都使用双引号括起来,并且遵循ARB文件的规范。
\\n在本地化过程中,需要特别注意数字和货币的格式。这些格式可能因地区而异,因此需要根据目标地区的习惯进行调整。
\\n日期和时间的格式也是本地化过程中需要重点关注的内容。Flutter提供了相应的API来处理不同地区的日期和时间格式。
\\n对于iOS应用,除了配置Flutter的本地化资源外,还需要更新iOS app bundle中的相关信息,以确保应用能够正确显示本地化的内容。
\\n高级语言环境定义
\\n获取语言环境
\\nLocale
类和Localizations
Widget可以获取当前的语言环境信息。l10n.yaml
文件是Flutter Intl插件用于生成和管理本地化资源的配置文件。通过配置这个文件,可以方便地添加、删除和更新语言环境。
Flutter的国际化系统基于ARB文件和flutter_localizations
包实现。在运行时,Flutter会根据当前的语言环境加载相应的ARB文件,并使用其中的翻译资源来替换应用中的文本。
在Flutter应用中,可以使用Localizations.of<T>(context)
方法获取当前上下文的本地化对象,并通过该对象访问翻译后的字符串。
为了更方便地管理本地化资源,可以定义一个类来封装与本地化相关的逻辑和数据。这个类可以包含加载本地化资源、获取翻译字符串等方法。
\\n要添加对新的语言的支持,只需在lib/l10n
目录下创建相应的语言子目录和ARB文件,并在pubspec.yaml
文件中添加相应的语言代码即可。然后运行flutter gen-l10n
命令来生成新的本地化资源文件。
除了使用ARB文件和flutter_localizations
包进行国际化外,Flutter还支持其他国际化方法,如使用JSON或XML文件存储翻译资源等。这些方法可以根据项目的具体需求进行选择。
在某些情况下,可能需要使用替代类来管理应用程序的本地化资源。这些替代类可以提供更灵活或更强大的本地化功能,以满足特定项目的需求。
\\n首先,在pubspec.yaml
文件中添加必要的依赖:
dependencies:\\n flutter:\\n sdk: flutter\\n flutter_localizations:\\n sdk: flutter\\n intl: ^0.19.0 # 或者最新版本\\n flutter_intl:\\n enabled: true\\n # 其他配置参数...\\n
\\n然后,运行flutter pub get
命令来安装这些依赖。
在lib/l10n
目录下创建两个ARB文件,分别用于存储英文和中文的翻译资源。
intl_en.arb
){\\n \\"home\\": \\"Home\\",\\n \\"settingLanguage\\": \\"Set Language\\",\\n \\"languageName_en\\": \\"English\\",\\n \\"languageName_zh\\": \\"Simplified Chinese\\",\\n \\"login_pageName\\": \\"Login Page\\",\\n \\"login_title\\": \\"Test App Demo\\",\\n \\"login_userName\\": \\"Username\\",\\n \\"login_userName_empty\\": \\"Username can\'t be empty!\\",\\n \\"login_password\\": \\"Password\\",\\n \\"login_password_empty\\": \\"Password can\'t be empty!\\",\\n \\"login_btn\\": \\"Login\\"\\n}\\n
\\nintl_zh.arb
){\\n \\"home\\": \\"首页\\",\\n \\"settingLanguage\\": \\"语言设置\\",\\n \\"languageName_en\\": \\"英语\\",\\n \\"languageName_zh\\": \\"简体中文\\",\\n \\"login_pageName\\": \\"登录页\\",\\n \\"login_title\\": \\"App测试\\",\\n \\"login_userName\\": \\"用户名\\",\\n \\"login_userName_empty\\": \\"用户名不能为空!\\",\\n \\"login_password\\": \\"密码\\",\\n \\"login_password_empty\\": \\"密码不能为空!\\",\\n \\"login_btn\\": \\"登录\\"\\n}\\n
\\n在main.dart
文件中,配置MaterialApp
以支持国际化:
import \'package:flutter/material.dart\';\\nimport \'package:flutter_localizations/flutter_localizations.dart\';\\nimport \'package:your_app_name/l10n.dart\'; // 导入生成的本地化资源文件\\n \\nvoid main() {\\n runApp(MyApp());\\n}\\n \\nclass MyApp extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return MaterialApp(\\n title: \'Flutter Demo\',\\n theme: ThemeData(\\n // ... 主题配置\\n ),\\n localizationsDelegates: const [\\n S.delegate, // 本地化的代理类\\n GlobalMaterialLocalizations.delegate,\\n GlobalCupertinoLocalizations.delegate,\\n GlobalWidgetsLocalizations.delegate,\\n ],\\n supportedLocales: S.delegate.supportedLocales, // 应用支持的语言环境列表\\n locale: Locale(\'zh\'), // 默认语言环境,可以根据需要更改为其他语言\\n home: MyHomePage(),\\n );\\n }\\n}\\n
\\n在应用的各个页面中,可以使用S.of(context).xxx
的方式来获取国际化后的字符串。例如:
import \'package:flutter/material.dart\';\\nimport \'package:your_app_name/l10n.dart\'; // 导入生成的本地化资源文件\\n \\nclass MyHomePage extends StatelessWidget {\\n @override\\n Widget build(BuildContext context) {\\n return Scaffold(\\n appBar: AppBar(\\n title: Text(S.of(context).home), // 使用国际化后的字符串\\n ),\\n body: Center(\\n child: Column(\\n mainAxisAlignment: MainAxisAlignment.center,\\n children: <Widget>[\\n Text(S.of(context).login_title),\\n TextField(\\n decoration: InputDecoration(\\n labelText: S.of(context).login_userName,\\n ),\\n ),\\n TextField(\\n decoration: InputDecoration(\\n labelText: S.of(context).login_password,\\n ),\\n obscureText: true,\\n ),\\n ElevatedButton(\\n onPressed: () {\\n // 按钮点击事件处理\\n },\\n child: Text(S.of(context).login_btn),\\n ),\\n ],\\n ),\\n ),\\n );\\n }\\n}\\n
\\n为了实现语言的动态切换,可以在应用中添加一个语言选择器,并在选择语言时更新MaterialApp
的locale
属性。这通常涉及到一个状态管理,如使用Provider
、GetX
或Riverpod
等状态管理库。
原生集成Flutter存在一些限制,这些限制主要源于Flutter与原生平台(如Android和iOS)之间的差异以及Flutter自身的架构设计。以下是一些主要的限制:
\\n移动端不支持多视图模式(仅限多引擎)。
\\n移动端不支持将多个 Flutter 库(Flutter 模块)同时打包进一个应用。
\\n移动端不支持 FlutterPlugin
的插件如果在 add-to-app 进行一些不合理的假设(例如假设 Flutter 的 Activity
始终存在),可能会出现意外行为。
Android 平台的 Flutter 模块仅支持适配了 AndroidX 的应用。
\\nWeb 端不支持多引擎模式(仅限多视图)。
\\nWeb 端无法完全“关闭” Flutter 引擎。应用程序可以移除所有 FlutterView 对象,并确保所有数据通过 Dart 常规的垃圾回收机制被清理。然而,即使引擎不再渲染任何内容,它仍会保持预热状态。
\\n本段落示例代码收集自Flutter官方文档
\\n为了在iOS应用中展示Flutter页面,需要启动FlutterEngine和FlutterViewController。FlutterEngine充当Dart VM和Flutter运行时的主机,而FlutterViewController则依附于FlutterEngine,传递UIKit的输入事件,并展示被FlutterEngine渲染的每一帧画面。
\\n创建FlutterEngine的位置取决于宿主类型。以下示例在SwiftUI项目中创建了一个FlutterEngine对象:
\\nimport SwiftUI\\nimport Flutter\\n// 导入Flutter插件与iOS平台代码的连接库\\nimport FlutterPluginRegistrant\\n \\n@ObservableObject class FlutterDependencies {\\n let flutterEngine = FlutterEngine(name: \\"my flutter engine\\")\\n init() {\\n // 运行默认的Dart入口点和Flutter路由\\n flutterEngine.run()\\n // 将插件与iOS平台代码连接到此应用\\n GeneratedPluginRegistrant.register(with: self.flutterEngine)\\n }\\n}\\n \\n@main\\nstruct MyApp: App {\\n // 通过视图环境注入FlutterDependencies\\n @StateObject var flutterDependencies = FlutterDependencies()\\n var body: some Scene {\\n WindowGroup {\\n ContentView().environmentObject(flutterDependencies)\\n }\\n }\\n}\\n
\\n以下示例展示了如何创建一个FlutterViewControllerRepresentable来代表FlutterViewController,并通过视图环境注入FlutterEngine:
\\nimport SwiftUI\\nimport Flutter\\n \\nstruct FlutterViewControllerRepresentable: UIViewControllerRepresentable {\\n // 通过视图环境获取FlutterDependencies\\n @EnvironmentObject var flutterDependencies\\n \\n func makeUIViewController(context: Context) -> some UIViewController {\\n return FlutterViewController(engine: flutterDependencies.flutterEngine, nibName: nil, bundle: nil)\\n }\\n \\n func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {\\n // 无需在此处进行更新操作\\n }\\n}\\n \\nstruct ContentView: View {\\n var body: some View {\\n NavigationStack {\\n NavigationLink(\\"My Flutter Feature\\") {\\n FlutterViewControllerRepresentable()\\n }\\n }\\n }\\n}\\n
\\n虽然推荐预热一个“长寿”的FlutterEngine以提高性能,但在某些情况下(如Flutter页面很少被展示时),可以选择让FlutterViewController隐式创建自己的FlutterEngine:
\\nstruct FlutterViewControllerRepresentable: UIViewControllerRepresentable {\\n func makeUIViewController(context: Context) -> some UIViewController {\\n return FlutterViewController(project: nil, nibName: nil, bundle: nil)\\n }\\n \\n func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {\\n // 无需在此处进行更新操作\\n }\\n}\\n
\\n推荐让应用的UIApplicationDelegate继承FlutterAppDelegate,以利用其功能(如传递openURL回调到Flutter插件)。以下是在SwiftUI项目中创建FlutterAppDelegate子类的示例:
\\nimport SwiftUI\\nimport Flutter\\nimport FlutterPluginRegistrant\\n \\n@ObservableObject class AppDelegate: FlutterAppDelegate {\\n let flutterEngine = FlutterEngine(name: \\"my flutter engine\\")\\n \\n override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {\\n flutterEngine.run()\\n // 连接插件(如果插件包含iOS平台代码)\\n GeneratedPluginRegistrant.register(with: self.flutterEngine)\\n return true\\n }\\n}\\n \\n@main\\nstruct MyApp: App {\\n // 告诉SwiftUI使用AppDelegate类作为应用委托\\n @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate\\n \\n var body: some Scene {\\n WindowGroup {\\n ContentView().environmentObject(appDelegate)\\n }\\n }\\n}\\n \\n// 在FlutterViewControllerRepresentable中使用AppDelegate的FlutterEngine\\nstruct FlutterViewControllerRepresentable: UIViewControllerRepresentable {\\n // 通过视图环境获取AppDelegate\\n @EnvironmentObject var appDelegate\\n \\n func makeUIViewController(context: Context) -> some UIViewController {\\n return FlutterViewController(engine: appDelegate.flutterEngine, nibName: nil, bundle: nil)\\n }\\n \\n func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {\\n // 无需在此处进行更新操作\\n }\\n}\\n
\\n如果AppDelegate不能直接继承FlutterAppDelegate,可以让其实现FlutterAppLifeCycleProvider协议,以确保Flutter插件接收到必要的回调:
\\nimport Foundation\\nimport Flutter\\n \\n@ObservableObject class AppDelegate: UIResponder, UIApplicationDelegate, FlutterAppLifeCycleProvider {\\n private let lifecycleDelegate = FlutterPluginAppLifeCycleDelegate()\\n let flutterEngine = FlutterEngine(name: \\"my flutter engine\\")\\n \\n // 实现UIApplicationDelegate方法,并通过lifecycleDelegate转发回调\\n // ...(此处省略了具体实现方法)\\n \\n func add(_ delegate: FlutterApplicationLifeCycleDelegate) {\\n lifecycleDelegate.add(delegate)\\n }\\n}\\n
\\n可以通过指定Dart入口、库和路由来定制Flutter运行时:
\\nrun
方法时,默认会调用lib/main.dart
文件中的main
函数。也可以使用runWithEntrypoint
方法并指定另一个Dart入口。通过以上步骤和示例,可以轻松地在iOS项目中集成Flutter页面,并根据实际需求选择最佳集成方式。
\\n本指南介绍如何向现有的 Android 应用中添加 FlutterFragment
。FlutterFragment
允许开发者在任何使用常规 Fragment
的地方呈现 Flutter 的内容。通过 FlutterFragment
,开发者可以控制 Flutter 的初始路由、Dart 入口、背景透明度等细节。
实例化并绑定 FlutterFragment
\\n在 Activity
的 onCreate()
方法中,实例化 FlutterFragment
并将其添加到 Activity
中。
class MyActivity : FragmentActivity() {\\n companion object {\\n private const val TAG_FLUTTER_FRAGMENT = \\"flutter_fragment\\"\\n }\\n \\n private var flutterFragment: FlutterFragment? = null\\n \\n override fun onCreate(savedInstanceState: Bundle?) {\\n super.onCreate(savedInstanceState)\\n setContentView(R.layout.my_activity_layout)\\n \\n val fragmentManager: FragmentManager = supportFragmentManager\\n flutterFragment = fragmentManager.findFragmentByTag(TAG_FLUTTER_FRAGMENT) as FlutterFragment?\\n \\n if (flutterFragment == null) {\\n val newFlutterFragment = FlutterFragment.createDefault()\\n flutterFragment = newFlutterFragment\\n fragmentManager.beginTransaction()\\n .add(R.id.fragment_container, newFlutterFragment, TAG_FLUTTER_FRAGMENT)\\n .commit()\\n }\\n }\\n}\\n
\\n处理系统回调
\\n为了使 FlutterFragment
如预期一样正常工作,需要将系统回调从 Activity
传递到 FlutterFragment
。
class MyActivity : FragmentActivity() {\\n override fun onPostResume() {\\n super.onPostResume()\\n flutterFragment!!.onPostResume()\\n }\\n \\n override fun onNewIntent(intent: Intent) {\\n flutterFragment!!.onNewIntent(intent)\\n }\\n \\n override fun onBackPressed() {\\n flutterFragment!!.onBackPressed()\\n }\\n \\n override fun onRequestPermissionsResult(\\n requestCode: Int,\\n permissions: Array<out String>,\\n grantResults: IntArray\\n ) {\\n flutterFragment!!.onRequestPermissionsResult(requestCode, permissions, grantResults)\\n }\\n \\n override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {\\n super.onActivityResult(requestCode, resultCode, data)\\n flutterFragment!!.onActivityResult(requestCode, resultCode, data)\\n }\\n \\n override fun onUserLeaveHint() {\\n flutterFragment!!.onUserLeaveHint()\\n }\\n \\n override fun onTrimMemory(level: Int) {\\n super.onTrimMemory(level)\\n flutterFragment!!.onTrimMemory(level)\\n }\\n}\\n
\\n为了减少 FlutterFragment 的初始化时间,可以使用已存在的、预热的 FlutterEngine
。
在应用启动时预热 FlutterEngine
\\nclass MyApplication : Application() {\\n lateinit var flutterEngine: FlutterEngine\\n \\n override fun onCreate() {\\n super.onCreate()\\n flutterEngine = FlutterEngine(this)\\n flutterEngine.navigationChannel.setInitialRoute(\\"your/route/here\\")\\n flutterEngine.dartExecutor.executeDartEntrypoint(\\n DartExecutor.DartEntrypoint.createDefault()\\n )\\n FlutterEngineCache.getInstance().put(\\"my_engine_id\\", flutterEngine)\\n }\\n}\\n
\\n在 FlutterFragment 中使用预热的 FlutterEngine
\\nval flutterFragment = FlutterFragment.withCachedEngine(\\"my_engine_id\\").build()\\n
\\n指定初始路由
\\n使用 FlutterFragment.Builder
指定初始路由(仅适用于新的 FlutterEngine)。
val flutterFragment = FlutterFragment.withNewEngine()\\n .initialRoute(\\"your/custom/route\\")\\n .build()\\n
\\n注意:当使用已预热的 FlutterEngine 时,指定的初始路由无效。
\\n指定 Dart 入口
\\n使用 FlutterFragment.Builder
指定 Dart 入口(仅适用于新的 FlutterEngine)。
val flutterFragment = FlutterFragment.withNewEngine()\\n .dartEntrypoint(\\"mySpecialEntrypoint\\")\\n .build()\\n
\\n注意:当使用已预热的 FlutterEngine 时,指定的 Dart 入口无效。
\\n选择渲染模式
\\n默认使用 SurfaceView
渲染,性能较好,但无法插入到 Android 的 View 层级中。如需使用 TextureView
,可以指定渲染模式。
val flutterFragment = FlutterFragment.withNewEngine()\\n .renderMode(FlutterView.RenderMode.texture)\\n .build()\\n
\\n设置透明度
\\n默认背景不透明,可以设置为透明。
\\nval flutterFragment = FlutterFragment.withNewEngine()\\n .transparencyMode(FlutterView.TransparencyMode.transparent)\\n .build()\\n
\\n使用 shouldAttachEngineToActivity()
方法决定 FlutterFragment 是否应该控制宿主 Activity。
val flutterFragment = FlutterFragment.withNewEngine()\\n .shouldAttachEngineToActivity(false) // 防止 Flutter 控制 Activity 的系统 UI\\n .build()\\n
\\n通过以上步骤,您可以成功地将 FlutterFragment 集成到现有的 Android 应用中,并根据需要配置各种场景
\\n在 Swift 中使用 FlutterEngineGroup
可以帮助你在 iOS 应用中高效地管理和复用多个 Flutter 引擎实例。下面是一个关于如何在 Swift 中使用 FlutterEngineGroup
的详细指南,包括各种代码示例。
首先,确保你的 iOS 项目已经集成了 Flutter。这通常涉及将 Flutter 模块作为依赖添加到你的原生 iOS 项目中,并配置好相关的 build 设置。
\\n你通常会在应用的入口点(如 AppDelegate
)中初始化 FlutterEngineGroup
。
import UIKit\\nimport Flutter\\n \\n@UIApplicationMain\\nclass AppDelegate: UIResponder, UIApplicationDelegate {\\n \\n var window: UIWindow?\\n var flutterEngineGroup: FlutterEngineGroup?\\n \\n func application(_ application: UIApplication,\\n didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {\\n \\n // 初始化 FlutterEngineGroup\\n flutterEngineGroup = FlutterEngineGroup(name: \\"my_engine_group\\", project: nil)\\n \\n // 其他配置...\\n \\n return true\\n }\\n}\\n
\\n当你需要展示一个 Flutter 页面时,你可以从 FlutterEngineGroup
中获取或创建一个 FlutterEngine
实例。
import UIKit\\nimport Flutter\\n \\nclass SomeViewController: UIViewController {\\n \\n var flutterEngine: FlutterEngine?\\n var flutterViewController: FlutterViewController?\\n \\n override func viewDidLoad() {\\n super.viewDidLoad()\\n \\n guard let appDelegate = UIApplication.shared.delegate as? AppDelegate,\\n let flutterEngineGroup = appDelegate.flutterEngineGroup else {\\n return\\n }\\n \\n // 从 FlutterEngineGroup 中获取或创建一个 FlutterEngine 实例\\n flutterEngine = flutterEngineGroup.makeEngine(withEntrypoint: nil, libraryURI: nil)\\n \\n // 使用 FlutterEngine 实例初始化 FlutterViewController\\n flutterViewController = FlutterViewController(engine: flutterEngine!, nibName: nil, bundle: nil)\\n \\n // 将 FlutterViewController 添加到当前视图控制器中\\n addChild(flutterViewController!)\\n view.addSubview(flutterViewController!.view)\\n flutterViewController!.didMove(toParent: self)\\n }\\n}\\n
\\n在上面的代码中,flutterViewController
已经被添加到 SomeViewController
的视图中。你可以根据需要将 SomeViewController
添加到你的应用中的任何位置,比如作为根视图控制器、子视图控制器或模态呈现。
当 FlutterViewController
不再需要时,你应该确保正确地清理资源。这通常涉及移除 FlutterViewController
的视图并从父视图控制器中移除它。
override func viewWillDisappear(_ animated: Bool) {\\n super.viewWillDisappear(animated)\\n \\n flutterViewController?.willMove(toParent: nil)\\n flutterViewController?.view.removeFromSuperview()\\n flutterViewController?.removeFromParent()\\n \\n // 如果这是最后一个使用这个 FlutterEngine 的地方,你可以考虑销毁它\\n // 注意:通常不建议手动销毁 FlutterEngine,因为 FlutterEngineGroup 会管理它们的生命周期\\n // flutterEngine = nil // 这不会真正销毁 FlutterEngine,只是将其置为 nil\\n}\\n
\\n注意:在大多数情况下,你不应该手动销毁 FlutterEngine
实例。FlutterEngineGroup
会负责管理和缓存 FlutterEngine
实例的生命周期。如果你不再需要某个 FlutterEngine
,并且确信没有其他地方在使用它,你可以将其从缓存中移除(尽管 Flutter SDK 可能不提供直接的方法来做到这一点),但通常这是不必要的,因为 FlutterEngine
的内存管理是由 Flutter 框架自动处理的。
Info.plist
文件中包含了必要的 Flutter 配置。FlutterEngineGroup
时,你可以通过传递相同的组名来确保多个地方共享相同的 FlutterEngine
实例(尽管这通常不是必需的,因为 FlutterEngineGroup
会为你管理这些实例)。FlutterEngine
和 FlutterViewController
是否为 nil
,以避免在它们尚未初始化时访问它们的属性或方法。在 Kotlin 中使用 FlutterEngineGroup
是为了在 Android 应用中高效地管理和复用 Flutter 引擎实例。这对于需要在多个地方嵌入 Flutter 视图的应用特别有用,因为它可以减少内存占用并提高性能。
以下是在 Kotlin 中使用 FlutterEngineGroup
的详细指南,包括各种代码示例。
首先,确保你的 Android 项目已经集成了 Flutter。这通常涉及将 Flutter 模块作为依赖添加到你的原生 Android 项目中,并配置好相关的 build.gradle 文件。
\\n你通常会在应用的入口点(如 Application
类)中初始化 FlutterEngineGroup
。
import android.app.Application\\nimport io.flutter.embedding.engine.FlutterEngine\\nimport io.flutter.embedding.engine.FlutterEngineGroup\\nimport io.flutter.embedding.android.FlutterActivity\\n \\nclass MyApplication : Application() {\\n \\n private lateinit var flutterEngineGroup: FlutterEngineGroup\\n \\n override fun onCreate() {\\n super.onCreate()\\n \\n // 初始化 FlutterEngineGroup\\n flutterEngineGroup = FlutterEngineGroup(this, \\"my_engine_id\\")\\n \\n // 可以在这里预创建 FlutterEngine 实例,以便更快地显示 Flutter 视图\\n // val precreatedEngine = flutterEngineGroup.makeEngine(null)\\n }\\n \\n // 提供一个全局访问点来获取 FlutterEngineGroup 实例\\n fun getFlutterEngineGroup(): FlutterEngineGroup {\\n return flutterEngineGroup\\n }\\n}\\n
\\n别忘了在 AndroidManifest.xml
中将你的 Application
类设置为应用的入口点。
当你需要展示一个 Flutter 页面时,你可以从 FlutterEngineGroup
中获取或创建一个 FlutterEngine
实例,并使用它来启动一个 FlutterActivity
或 FlutterFragment
。
import android.content.Context\\nimport android.content.Intent\\nimport io.flutter.embedding.android.FlutterActivity\\nimport io.flutter.embedding.engine.FlutterEngine\\n \\n// 在某个 Activity 或 Fragment 中\\nfun showFlutterScreen(context: Context) {\\n val application = context.applicationContext as MyApplication\\n val flutterEngineGroup = application.getFlutterEngineGroup()\\n \\n // 从 FlutterEngineGroup 中获取或创建一个 FlutterEngine 实例\\n val flutterEngine: FlutterEngine = flutterEngineGroup.makeEngine(null)\\n \\n // 配置 FlutterActivity 的启动参数\\n val flutterActivityIntent = FlutterActivity\\n .withCachedEngine(\\"my_engine_id\\") // 使用相同的 ID 来复用 FlutterEngine 实例\\n .build(context)\\n \\n // 启动 FlutterActivity\\n context.startActivity(flutterActivityIntent)\\n}\\n
\\n注意:在上面的代码中,withCachedEngine
方法的参数应该是你在 FlutterEngineGroup
构造函数中使用的相同 ID。但是,如果你只是想从 FlutterEngineGroup
中获取一个新的或现有的引擎,并不关心是否缓存了特定的引擎,你可以省略这个 ID(尽管这通常不是最佳实践,因为它会失去使用 FlutterEngineGroup
的主要优势)。实际上,你应该使用 flutterEngineGroup.makeEngine(null)
来创建一个新的引擎(如果还没有缓存的话),并通过其他机制(如传递额外的启动参数)来区分不同的 Flutter 视图。
然而,由于 FlutterActivity.withCachedEngine
方法期望一个已经存在的、通过特定 ID 缓存的引擎,因此上面的代码示例可能并不完全准确。在大多数情况下,你可能不需要显式地指定引擎 ID 来复用引擎,因为 FlutterEngineGroup
会自动管理引擎的生命周期和缓存。相反,你可以简单地调用 flutterEngineGroup.makeEngine(null)
来获取一个新的或现有的引擎,并使用 FlutterActivity.withNewEngine(flutterEngine)
来启动一个带有新引擎的 FlutterActivity
。但是,由于 FlutterActivity
的 API 可能会随着 Flutter 的更新而变化,因此请务必查阅最新的 Flutter 文档以获取准确的信息。
上面的代码示例已经展示了如何通过 FlutterActivity
来展示 Flutter 页面。如果你更喜欢使用 FlutterFragment
,你可以类似地配置并添加到你的 Activity 中。
通常,你不需要手动清理 FlutterEngine
实例,因为 FlutterEngineGroup
会负责管理和缓存它们。但是,如果你确定某个 FlutterEngine
不再需要,并且想要释放它占用的资源,你可以调用 destroy()
方法来销毁它(尽管这通常不是必需的)。然而,请注意,直接销毁 FlutterEngine
可能会导致未定义的行为,因为 Flutter 框架可能仍然在使用它。因此,在大多数情况下,你应该允许 FlutterEngineGroup
来管理 FlutterEngine
的生命周期。
build.gradle
文件中包含了必要的 Flutter 依赖。FlutterEngineGroup
时,确保在需要展示 Flutter 视图的地方正确地获取和使用 FlutterEngine
实例。FlutterEngine
和其他相关对象是否为 null
,以避免在它们尚未初始化时访问它们的属性或方法。关于控制加载顺序以优化性能与内存的部分,对于add-to-app场景下的性能优化尤为重要。以下是加载Flutter UI时的关键步骤梳理:
\\ndart-obfuscate
工具dart-obfuscate
是一个流行的 Dart 代码混淆工具。你可以通过以下步骤使用它:
安装 Dart SDK:确保你已经安装了 Dart SDK。
\\n安装 dart-obfuscate
:
dart pub global activate dart_obfuscation\\n
\\n混淆你的 Dart 代码:
\\ndart-obfuscate input.dart --output output.dart\\n
\\n这里 input.dart
是你的源代码文件,output.dart
是混淆后的代码文件。
通常是在Flutter项目中进行的,因为Flutter支持使用Dart语言开发跨平台应用,包括iOS。以下是在iOS工程中配置Dart代码混淆的步骤:
\\n修改构建配置
\\nios
文件夹,然后进入Flutter
文件夹,再找到Release.xcconfig
文件。Release.xcconfig
文件中,添加以下行来启用Dart代码混淆:EXTRA_GEN_SNAPSHOT_OPTIONS=--obfuscate\\n
\\n这告诉Flutter在构建iOS应用时使用混淆选项。
\\n构建应用
\\nflutter build ios --release --obfuscate --split-debug-info=/<project-name>/<directory>\\n
\\n这里的--obfuscate
选项启用代码混淆,--split-debug-info
选项指定了Flutter输出调试文件的目录。请确保替换/<project-name>/<directory>
为你的实际项目名称和目录。
保存符号表文件
\\n如果你需要调试混淆后的应用创建的堆栈跟踪,可以使用flutter symbolize
命令和符号文件来解析堆栈跟踪。具体步骤如下:
flutter symbolize
命令提供堆栈跟踪(存储在文件中)和符号文件。例如:flutter symbolize -i <stack trace file> -d /path/to/symbols\\n
\\n这里的<stack trace file>
是堆栈跟踪文件,/path/to/symbols
是符号文件的路径。
面向对象设计(Object-Oriented Design
, OOD
)是软件工程中的一种设计方法,它通过将现实世界中的实体抽象为对象来简化复杂系统的建模。
OOD
阶段在分析模型基础上进行应用软件的系统设计
、对象设计
,从而得到设计模型,该模型包含了解决问题的方案
和策略
。是确定问题具体解决方案
的过程。
OOD
不仅依赖于基本的概念如类
、对象
、封装
、继承
和多态
,还依赖于一系列指导原则来确保代码的质量。以下是五个重要的OOD
原则,并通过具体例子详细解释每个原则的应用。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
\\nSingle Responsibility Principle
,SRP
)\\n\\n1、定义: 一个类应该
\\n只有一个引起它变化的原因
。2、通俗理解:如果一个类做了
\\n太多
的事情,当其中一部分需求发生变化
时,可能会影响到其他部分的功能。因此,应该让每个类专注于完成一个特定的任务
,减少耦合
和依赖
。即低耦合和高内聚。
在图书馆管理系统中,Book
类只负责管理书籍的信息(如书名
、作者
等),而不处理借阅逻辑
或会员信息
。这样,如果需要修改
书籍信息的存储方式,不会影响到借阅记录或会员管理。
代码示例:
\\nclass Book {\\n String title;\\n String author;\\n ///ISBN编号是指国际标准书号,它是识别图书的唯一标识符。\\n ///ISBN由一串数字组成,有时也包含破折号,用于分隔不同部分,\\n ///例如出版国家、出版社和书名等信息。ISBN有助于图书管理、销售及库存控制。\\n String isbn;\\n\\n Book(this.title, this.author, this.isbn);\\n}\\n
\\nOpen/Closed Principle
, OCP
)\\n\\n1、定义: 软件实体(
\\n类
、模块
、函数
等)应该对扩展开放,对修改封闭
。2、通俗理解: 这意味着可以在
\\n不修改现有代码的情况下添加新功能
。这有助于保持系统的稳定性,同时允许新的特性被轻松集成
。
在图书馆系统中,如果我们想支持不同类型书籍(如 Textbook
, Novel
),可以通过继承 Book
类来实现,而不需要修改 Book
类本身。
代码示例:
\\n// 定义抽象类 Book\\nabstract class Book {\\n String title;\\n String author;\\n String isbn;\\n\\n // 构造函数\\n Book(this.title, this.author, this.isbn);\\n\\n // 抽象方法,用于显示书籍的详细信息\\n void displayDetails();\\n}\\n\\n// 定义 Textbook 类,继承自 Book 类\\nclass Textbook extends Book {\\n String subject;\\n\\n // Textbook 类的构造函数\\n Textbook(String title, String author, String isbn, this.subject) : super(title, author, isbn);\\n\\n // 实现抽象方法 displayDetails\\n @override\\n void displayDetails() {\\n print(\'Textbook: $title, Author: $author, Subject: $subject\');\\n }\\n}\\n\\n// 定义 Novel 类,继承自 Book 类\\nclass Novel extends Book {\\n String genre;\\n\\n // Novel 类的构造函数\\n Novel(String title, String author, String isbn, this.genre) : super(title, author, isbn);\\n\\n // 实现抽象方法 displayDetails\\n @override\\n void displayDetails() {\\n print(\'Novel: $title, Author: $author, Genre: $genre\');\\n }\\n}\\n\\nvoid main() {\\n Textbook textbook = Textbook(\'Dart Programming\', \'John Doe\', \'123456789\', \'Computer Science\');\\n textbook.displayDetails();\\n\\n Novel novel = Novel(\'费曼学习法\', \\"尹红心\\", \'987654321\', \'学习方法\');\\n novel.displayDetails();\\n}\\n
\\nInterface Segregation Principle
, ISP
)\\n\\n1、定义:
\\n客户端不应该依赖于它们不使用的接口
。2、通俗理解:避免创建
\\n大而全
的接口,而是创建多个小
而具体的接口,使每个接口只包含一组紧密相关的操作
。
在图书馆系统中,我们可以为不同的角色(如 Member
, Librarian
)定义不同的接口
,而不是让所有角色都实现一个庞大的通用接口
。
代码示例:
\\n// 定义一个表示书籍的类\\nclass Book {\\n String title;\\n String author;\\n\\n Book(this.title, this.author);\\n}\\n\\n// 定义可借阅的接口\\nabstract class Borrowable {\\n void borrow(Book book);\\n}\\n\\n// 定义可归还的接口\\nabstract class Returnable {\\n void returnBook(Book book);\\n}\\n\\n// 定义可管理的接口\\nabstract class Manageable {\\n void addBook(Book book);\\n void removeBook(Book book);\\n}\\n\\n// 会员类,实现 Borrowable 和 Returnable 接口\\nclass Member implements Borrowable, Returnable {\\n @override\\n void borrow(Book book) {\\n print(\'会员正在借阅书籍:${book.title} 作者:${book.author}\');\\n }\\n\\n @override\\n void returnBook(Book book) {\\n print(\'会员正在归还书籍:${book.title} 作者:${book.author}\');\\n }\\n}\\n\\n// 图书管理员类,实现 Borrowable、Returnable 和 Manageable 接口\\nclass Librarian implements Borrowable, Returnable, Manageable {\\n @override\\n void borrow(Book book) {\\n print(\'图书管理员正在借阅书籍:${book.title} 作者:${book.author}\');\\n }\\n\\n @override\\n void returnBook(Book book) {\\n print(\'图书管理员正在归还书籍:${book.title} 作者:${book.author}\');\\n }\\n\\n @override\\n void addBook(Book book) {\\n print(\'图书管理员正在添加书籍:${book.title} 作者:${book.author}\');\\n }\\n\\n @override\\n void removeBook(Book book) {\\n print(\'图书管理员正在移除书籍:${book.title} 作者:${book.author}\');\\n }\\n}\\n\\n\\nvoid main() {\\n Book book1 = Book(\'Dart 入门\', \'张三\');\\n Member member = Member();\\n Librarian librarian = Librarian();\\n\\n member.borrow(book1);\\n member.returnBook(book1);\\n\\n librarian.borrow(book1);\\n librarian.returnBook(book1);\\n librarian.addBook(book1);\\n librarian.removeBook(book1);\\n}\\n
\\nDependency Inversion Principle
, DIP
)\\n\\n1、定义:高层模块不应该依赖于低层模块,二者都应该
\\n依赖于抽象
;抽象不应该依赖于细节
,细节应该依赖于抽象
。2、通俗理解:尽量让代码依赖于接口或抽象类,
\\n而不是具体的实现类
。这提高了代码的灵活性
和可测试性
。
在图书馆系统中,Library
类不应该直接依赖于具体的 Book
类型,而是依赖于 Book
接口。这样,即使将来引入新的书籍类型,也不会影响 Library
类。
代码示例:
\\n// 定义一个表示书籍的接口\\nabstract class Book {\\n void displayDetails();\\n}\\n\\n// 图书馆类\\nclass Library {\\n List<Book> books = [];\\n\\n // 构造函数\\n Library() {\\n // 初始化书籍列表\\n }\\n\\n // 添加书籍的方法\\n void addBook(Book book) {\\n books.add(book);\\n }\\n\\n // 列出所有书籍的方法\\n void listBooks() {\\n for (Book book in books) {\\n book.displayDetails();\\n }\\n }\\n}\\n\\n// 教科书类,实现 Book 接口\\nclass Textbook implements Book {\\n String title;\\n String author;\\n String isbn;\\n String subject;\\n\\n Textbook(this.title, this.author, this.isbn, this.subject);\\n\\n @override\\n void displayDetails() {\\n print(\'Textbook: $title by $author, ISBN: $isbn, Subject: $subject\');\\n }\\n}\\n\\n// 小说类,实现 Book 接口\\nclass Novel implements Book {\\n String title;\\n String author;\\n String isbn;\\n String genre;\\n\\n Novel(this.title, this.author, this.isbn, this.genre);\\n\\n @override\\n void displayDetails() {\\n print(\'Novel: $title by $author, ISBN: $isbn, Genre: $genre\');\\n }\\n}\\n\\nvoid main() {\\n Library library = Library();\\n Textbook textbook = Textbook(\\"Java Programming\\", \\"John Doe\\", \\"1234567890\\", \\"Computer Science\\");\\n Novel novel = Novel(\'费曼学习法\', \\"尹红心\\", \'987654321\', \'学习方法\');\\n library.addBook(textbook);\\n library.addBook(novel);\\n library.listBooks();\\n}\\n
\\nLiskov Substitution Principle
,LSP
)\\n\\n1、定义:子类应当能够替换它们的基类而不影响程序的正确性。
\\n2、通俗理解:子类应该可以无缝地替代父类使用,而不改变程序的行为。这确保了继承关系的合理性。
\\n
在图书馆系统中,Textbook
和 Novel
都是 Book
的子类,任何接受 Book
对象的地方都可以无差别地接受 Textbook
或 Novel
对象。
代码示例:
\\n// 打印书籍详细信息的函数\\nvoid printBookDetails(Book book) {\\n book.displayDetails();\\n}\\n\\nvoid main() {\\n Textbook textbook = Textbook(\\"Java Programming\\", \\"John Doe\\", \\"1234567890\\", \\"Computer Science\\");\\n Novel novel = Novel(\'费曼学习法\', \\"尹红心\\", \'987654321\', \'学习方法\');\\n // 使用示例\\n printBookDetails(textbook);\\n printBookDetails(novel);\\n}\\n
\\n通过上述五个面向对象设计原则(SRP
, OCP
, ISP
, DIP
,LSP
),我们可以构建出更加健壮
、灵活且易于维护
的软件系统。这些原则不仅仅是理论上的概念,而是实际可行且非常有效的设计方法
。希望通过上述深入的讲解
和详细案例
能帮助你更好地掌握面向对象设计的原则。
\\n","description":"前言 面向对象设计(Object-Oriented Design, OOD)是软件工程中的一种设计方法,它通过将现实世界中的实体抽象为对象来简化复杂系统的建模。\\n\\nOOD阶段在分析模型基础上进行应用软件的系统设计、对象设计,从而得到设计模型,该模型包含了解决问题的方案和策略。是确定问题具体解决方案的过程。\\n\\nOOD不仅依赖于基本的概念如类、对象、封装、继承和多态,还依赖于一系列指导原则来确保代码的质量。以下是五个重要的OOD原则,并通过具体例子详细解释每个原则的应用。\\n\\n操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。\\n\\n一、单一职责原则(Singl…","guid":"https://juejin.cn/post/7459951561692397587","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-15T07:13:44.480Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/543473ad74394ac98d0a5cf93e89ae6c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1737530023&x-signature=g6BJfepYMb286uHbMV3J74YxOMw%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"Dart 之空安全","url":"https://juejin.cn/post/7459767548911304743","content":"码字不易,记得 关注 + 点赞 + 收藏 + 评论
\\n
Dart 是一门现代化编程语言,自2.12版本开始引入空安全的特性,从根本上解决了因变量或对象引用为null而引发的问题。简单来说空安全就是做了一个提前的预防措施,将运行时可能存在的潜在空值引用错误提前到了编译期检查,便于开发者进行修改。
\\n空值在某些场景下是必要的,但空值的引用不当可能会导致程序崩溃或导致产生难以调试的错误。如在访问或引用 null 的属性或方法时会导致空指针异常。
\\n空值的产生:
\\n空安全是一种编程语言特性,用于防止因变量或对象引用为null而出现的错误。能够在编译期捕捉到潜在的空指针异常,从而减少运行时错误的发生。
\\n需要注意:
\\n在声明变量时,默认是不可为空的,除非将变量显示声明为可空类型,否则它一定是非空的。
\\n示例:\\n
Dart 的空安全是非常可靠的。如果类型系统推断出某个变量不为空,那么它 永远 不为空。
\\n引入前:
\\n从图中可以看出,Null 类型是所有类型的子类,意味着所有类型都可以为Null。
\\n引入后:
\\n引入后可以明显的发现Null类型不再是其它类型的子类,意味着所有类型都不可为空。
\\n当左侧表达式为 null 时,使用右侧表达式的值作为默认值。
\\n示例:
\\nvoid main() {\\n int? a;\\n print(a); // 输出:null\\n print(a.runtimeType); // 输出:Null\\n int b = a ?? 9; // 左侧表达式为空,b应该为9\\n print(b); // 输出:9\\n}\\n
\\n只有在左侧表达式为 null 时,才执行赋值操作。
\\n示例:
\\nvoid main() {\\n int? a;\\n print(a); // 输出:null\\n print(a.runtimeType); // 输出:Null\\n a = a ?? 9; // 左侧表达式为空,进行赋值操作\\n print(a); // 输出:9\\n}\\n
\\n允许安全地访问对象的属性或调用方法,如果对象为 null ,则返回 null。
\\n示例:
\\nvoid main() {\\n // 空感知方法调用\\n GetData c = GetData();\\n print(c.runtimeType); // GetData\\n feelNull(c); // 传入 GetData,输出:2\\n GetData? d;\\n print(d.runtimeType); // 输出:Null\\n feelNull(d); // 传入null,输出:null\\n}\\n\\nvoid feelNull (GetData? data) {\\n data?.name;\\n // 空感知方法调用,如果data为空,则返回null,否则调用getNum() 函数\\n var result = data?.getNum(1);\\n print(result);\\n}\\n\\nclass GetData {\\n String? name;\\n int getNum(int i){\\n return i+1;\\n }\\n}\\n
\\n用于给表达式断言,断言其不能为空,若为空则会在运行时报错。
\\n用于延时初始化变量,若未进行初始化,则会运行时报错。
\\n适用场景: 无法在定义时进行初始化,并且又想避免使用?.
required关键字主要用于防止忘记设置必要的参数。常用于函数参数与构造函数参数,确保必须传入这些参数。
\\n用于在调用函数时必须要求传入参数,如果不传入提前报错提示。
\\nvoid main() {\\n hello(i: 520); // 输出:hello,Dart! 520\\n}\\nvoid hello({required int i}){\\n String name = \'Dart\';\\n print(\'hello,$name! $i\');\\n}\\n
\\n不传入时:
\\n class Car{\\n void introduce({required String name, String color = \'yellow\'}){\\n print(\'$name is $color\');\\n }\\n }\\n void main() {\\n Car truck = Car();\\n truck.introduce(name: \'truck\'); // 输出:truck is yellow \\n }\\n
\\n未传入参数时:\\n
本小节从空值问题出发,首先介绍了空值的影响,其次介绍了空安全的定义与原则,然后介绍了空安全的运算符,最后介绍了延迟初始化(late)与关键字required的使用。
","description":"前言 Dart 是一门现代化编程语言,自2.12版本开始引入空安全的特性,从根本上解决了因变量或对象引用为null而引发的问题。简单来说空安全就是做了一个提前的预防措施,将运行时可能存在的潜在空值引用错误提前到了编译期检查,便于开发者进行修改。\\n\\n一、空值问题\\n\\n空值在某些场景下是必要的,但空值的引用不当可能会导致程序崩溃或导致产生难以调试的错误。如在访问或引用 null 的属性或方法时会导致空指针异常。\\n\\n空值的产生:\\n\\n声明变量时未进行初始化\\n引用不存在的对象\\n赋了null值\\n二、空安全的定义\\n\\n空安全是一种编程语言特性…","guid":"https://juejin.cn/post/7459767548911304743","author":"好的佩奇","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-15T04:49:51.589Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/aef3e8c31d144aa7b63fc81cec2a9bc9~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5aW955qE5L2p5aWH:q75.awebp?rk3s=f64ab15b&x-expires=1737526605&x-signature=m6JE0QpXnQAOolpPhY2RNIRCtzM%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/04c07534d55b4bb692be9c4fbde664f0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5aW955qE5L2p5aWH:q75.awebp?rk3s=f64ab15b&x-expires=1737526605&x-signature=nDWJZsWcWgQYCIpWVwyHmpM0xsY%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e0065b89cb6b48a295b546be2f3cf66e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5aW955qE5L2p5aWH:q75.awebp?rk3s=f64ab15b&x-expires=1737526605&x-signature=XF%2FB7siodCM3vXF1fgvlpIolIT4%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e520d8e5f6b847369438215c2ce784c3~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5aW955qE5L2p5aWH:q75.awebp?rk3s=f64ab15b&x-expires=1737526605&x-signature=PZUxzIicw7d6sntUerMReMBqeZs%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a18f1ae01bd845c4b842ec9cf9444a79~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5aW955qE5L2p5aWH:q75.awebp?rk3s=f64ab15b&x-expires=1737526605&x-signature=xNHSrj%2F%2FKnvDponHF4SEjA33Z8w%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6fc926c1499f404abdc577a2e1ea788a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5aW955qE5L2p5aWH:q75.awebp?rk3s=f64ab15b&x-expires=1737526605&x-signature=gBJN%2BTth%2FGVXzw8VomQjgf1kGXQ%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/fc0f6279218d41dbb147cfd1e79ae0cf~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5aW955qE5L2p5aWH:q75.awebp?rk3s=f64ab15b&x-expires=1737526605&x-signature=QBTTTwbuCMR7nDFYb%2F7By9mvvC8%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3f1e8d95e4cc4fa5a09fb32689c59b49~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5aW955qE5L2p5aWH:q75.awebp?rk3s=f64ab15b&x-expires=1737526605&x-signature=eqfaaVCM9vs1u7myiMVvRJ3rZe0%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d11693d4a3774d748788dcc45e5b8665~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5aW955qE5L2p5aWH:q75.awebp?rk3s=f64ab15b&x-expires=1737526605&x-signature=Xy8KuRhc8JZijA9vkQmD5R97jyw%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Dart","Flutter"],"attachments":null,"extra":null,"language":null},{"title":null,"url":null,"content":null,"description":null,"guid":"undefined","author":null,"authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-15T03:00:49.329Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"系统化掌握Dart编程之面向对象分析","url":"https://juejin.cn/post/7459762467288743999","content":"面向对象分析(Object-Oriented Analysis
, OOA
)是将现实世界的问题抽象为软件系统中对象的过程,其任务和目的是通过对问题空间的分析
,建立\\n系统的分析模型。
OOA
方法将数据
和功能
结合在一起作为一个综合对象来考虑。OOA
技术可将系统的行为
和信息间的关系
表示为迭代构造特征。换言之,通过迭代的方式,逐步构建出系统的行为
和信息之间
的关系模型,使得系统的结构更加清晰
、易于理解和维护
。
交互
、执行任务
或响应事件
。关联
和依赖
。行为
和信息
的关系不是一次性设计完成的,而是通过多次迭代逐步完善和细化的
。OOA
包含5个活动:认定对象
、组织对象
、描述对象间的相互作用
、确定对象的操作及定义对象的内部信息
。
为了深入理解 OOA
,我们将通过一个具体的例子——图书馆管理系统——来详细探讨其五个核心活动
,并展示每个阶段的具体步骤
和成果
。
需求文档
或需求讨论
中提取名词。至关重要
。在图书馆管理系统中,可能会识别出以下对象:
\\nBook
:书籍,包括书名
、作者
、出版日期
等信息。Member
:会员,包括姓名
、ID
、联系方式
等信息。Loan
:借阅记录,包括借阅日期
、归还日期
等信息。Librarian
:管理员,负责管理图书
和会员
的操作。Library
:图书馆本身,作为所有资源的容器
。对象列表
,明确了哪些元素对于解决问题至关重要
。关系
和层次结构
。“是一种”
(Is-a
)关系,建立泛化或继承关系。“有一个”
(Has-a
)关系,使用组合或聚合来组织对象。Book
类可以有多个子类如 Textbook
, Novel
来表示不同类型的书籍。Library
包含多个 Book
和 Member
实例,形成了一种“有一个”
的关系。Loan
对象关联了 Member
和 Book
,体现了它们之间的交互。类图
,展示了对象之间的关联
和层次结构
,确保了合理的依赖关系
。不同角色之间的互动
。对象间的消息传递顺序
。当 Member
请求借阅一本书时,过程如下:
Member
:requestLoan(Book book)
。Librarian
:checkAvailability(Book book)
。Book
:isAvailable()
。Loan
:createRecord(Member member, Book book)
。Book
的状态为不可用。对象间的交互方式
,确保它们能够有效合作
。具体操作
,包括它们的参数
、返回值
以及必要的内部状态管理
。参数类型
和返回类型
)。公开
,哪些应该私有
。Loan
类的 createRecord(Member member, Book book)
方法需要传入 Member
和 Book
的引用,并返回一个唯一的借阅 ID
。Loan
可能维护一些内部状态,如借阅日期
和预计归还日期
,这些信息应该是私有的
,仅通过公共方法访问
。信息
和方法
来正确执行其任务,同时保持良好的封装性
。当确定对象的操作
后,再定义对象的内部信息
,内部信息包括其内部数据信息
、信息存储方法
、继承关系
等。
OOA
是一个迭代的过程,随着对领域理解的加深,最初的模型可能会不断调整
和完善
。通过认定对象
、组织对象
、描述对象间的相互作用
、确定对象的操作
和定义内部信息
这五个活动,可以构建出既贴近业务逻辑
又具备强大技术支撑
的软件系统。
\\n","description":"前言 面向对象分析(Object-Oriented Analysis, OOA)是将现实世界的问题抽象为软件系统中对象的过程,其任务和目的是通过对问题空间的分析,建立 系统的分析模型。\\n\\nOOA方法将数据和功能结合在一起作为一个综合对象来考虑。OOA技术可将系统的行为和信息间的关系表示为迭代构造特征。换言之,通过迭代的方式,逐步构建出系统的行为和信息之间的关系模型,使得系统的结构更加清晰、易于理解和维护。\\n\\n系统的行为:指的是系统中各个组件或对象如何交互、执行任务或响应事件。\\n信息间的关系:指的是系统中不同数据或对象之间的关联和依赖。\\n迭代构造特征:意味…","guid":"https://juejin.cn/post/7459762467288743999","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-15T02:27:17.448Z","media":[{"url":"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ec85a4fb405f400bb10f326f0fb3fbf5~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Zyw54ux5YuH5aOr:q75.awebp?rk3s=f64ab15b&x-expires=1737512837&x-signature=iH57Q7nIOzWVWYIc6f4Hc5r1eOc%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter","Dart"],"attachments":null,"extra":null,"language":null},{"title":"深入理解Dart中空安全","url":"https://juejin.cn/post/7459660996770512959","content":"深入理解Dart中空安全","description":"深入理解Dart中空安全","guid":"https://juejin.cn/post/7459660996770512959","author":"科昂","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-14T14:40:10.830Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Flutter-通过Native和外接纹理加载图片","url":"https://juejin.cn/post/7459603316324106267","content":"码字不易,记得 关注 + 点赞 + 收藏 + 评论
\\n
\\n
\\n
在Native和Flutter混合开发时,如果Flutter的业务面逐渐增多,且对性能指标有所要求,那么势必会遇到一个问题:使用Flutter加载图片的内存以及缓存文件,和Native环境下出现叠加导致APP的内存占用、存储占用上升的问题。 因此打通Flutter和Native的缓存机制,是非常有必要的。
\\n而在2年前,阿里就已经推出了PowerImage,文章里面也对比了Flutter原生方案和Texture以及FFI的性能指标,只是这个库后面再继续维护了。
\\n所以参考阿里的代码,去掉过度设计,抽象加载过程,重新设计Texture内存管理机制,实现一个低成本、易对接拓展的图片加载框架。
\\n虽然默认情况下Texture
方案会存在GPU->CPU->GPU的问题,但考虑到目前的设备的整体性能,还是直接使用Texture
的方案,基本满足了大部分的场景需要,然后就是管理好Texture的创建、缓存和销毁即可。
使用TextureRegistry
来创建SurfaceTexture
:
TextureRegistry.SurfaceTextureEntry entry = textureRegistry.createSurfaceTexture();\\nmTextureId = entry.id();\\nmSurfaceTexture = entry.surfaceTexture();\\nmSurface = new Surface(mSurfaceTexture);\\n
\\n有了textureId
,Flutter层的Texture
Widget就能和Native的Surface进行绑定了:
Texture(textureId: _textureId);\\n
\\n有了Surface
就能使用Canvas
进行绘制渲染了:
Canvas canvas = mSurface.lockCanvas(null);\\n...\\nmSurface.unlockCanvasAndPost(canvas);\\n
\\n这也就是实际将Native加载的图片绘制到Flutter的地方。
\\n然后在不再需要时进行销毁:
\\nmSurfaceTexture.release();\\nentry.release();\\n
\\niOS需要注意的是,只有在主线程调用textureRegistry.textureFrameAvailable(textureId)
,才能使给copyPixelBuffer
设置返回的纹理数据生效,Flutter层才能显示出最新的纹理。
只要Native层的SurfaceTexture
没有被销毁,那么通过textureId
就能显示对应的图片,这个也就能算是Flutter层的图片纹理缓存。
这里需要使用2层缓存的设计,一层TextureManager
用来管理存在外部引用的Texture
,然后使用LruCache
来作为没有引用的Texture
缓存管理。
当一个Texture
被mount时,对应引用计数+1,unmount时计数-1,当计数为0时,意味着外部没有引用,则将这个Texture
加入LruCache
;当LruCache
中缓存的Texture
会mount时,则将其从LruCache
中移除,并引用计数+1。这就意味着LruCache
中保存的Texture
一定是外部没有引用的。当LruCache
满了,新的Texture
加入,老的Texture
移除,就需要调用channel,通知Native对移除的Texture
进行销毁。
stateDiagram-v2\\n\\n加载Uri对应的Key --\x3e TextureManager\\nTextureManager --\x3e 返回textureId,计数加1:找到缓存\\nTextureManager --\x3e Lrucache:找不到缓存\\nLrucache --\x3e 返回textureId,并从Lrucache中移除: 找到缓存\\n返回textureId,并从Lrucache中移除 --\x3e 将缓存加入TextureManager,计数加1\\nLrucache --\x3e 通知Native加载:找不到缓存\\n通知Native加载 --\x3e 加入TextureManager,计数加1:加载成功\\n
\\nstateDiagram-v2\\n\\nTexture.dispose --\x3e TextureManager:Key\\nTextureManager --\x3e 计数减1:找到对应记录\\n计数减1 --\x3e 从TextureManager移出,加入Lrucache:计数为0\\n
\\n为了适配各种不同的图片加载框架,因此将图片实际的加载过程抽象出来,由Native对接实现。不需要太多的接口,只需要
\\nstatic Future<int?> createTexture();\\nstatic Future<NImageInfo> loadImage(LoadRequest request);\\nstatic void destroyTexture(int textureId)\\n
\\n这里对于Flutter和Native通信的MethodChannel
不做说明。
没什么好说的,只需要Native创建一个Texture,然后把textureId
返回给Flutter即可。
同时,这里定义一个ImageTextureView
的类,持有Texture,并且将<textureId, ImageTextureView>的对应关系记录下来,方便后面的流程函数,通过textureId
能够方便的拿到对应的ImageTextureView
,后面加载逻辑,就都在ImageTextureView
中实现。
class ImageTextureView {\\n private final TextureRegistry.SurfaceTextureEntry entry;\\n private final SurfaceTexture mSurfaceTexture;\\n private final Surface mSurface;\\n\\n public ImageTextureView(@NonNull TextureRegistry.SurfaceTextureEntry surfaceTextureEntry) {\\n entry = surfaceTextureEntry;\\n mTextureId = surfaceTextureEntry.id();\\n mSurfaceTexture = surfaceTextureEntry.surfaceTexture();\\n mSurface = new Surface(mSurfaceTexture);\\n }\\n}\\n\\n
\\n在Flutter层都不再需要这个纹理内存时,通知Native销毁对应的Texture
。
public void destroy() {\\n try {\\n mSurfaceTexture.release();\\n } catch (Exception | Error ignore) {\\n }\\n try {\\n entry.release();\\n } catch (Exception | Error ignore) {\\n }\\n}\\n
\\n将图片加载所需要的参数塞入LoadRequest
中,以Map的形式传给Native。
class LoadRequest {\\n /// id of the texture created by native\\n int textureId;\\n\\n /// image uri\\n String? uri;\\n\\n int width;\\n\\n int height;\\n\\n BoxFit fit;\\n}\\n
\\nNative在channel中收到调用后,解析出对应的参数,根据textureId
获取到持有对应Texture
的ImageTextureView
对象,并执行它的loadImage
方法:
\\nclass ImageTextureView {\\n private final SurfaceTexture mSurfaceTexture;\\n \\n public void loadImage(LoadRequest loadRequest, MethodChannel.Result result) {\\n }\\n}\\n
\\n然后在有了加载结果之后,将最终加载好的图片的尺寸返回给Flutter。
\\n为了能够对接各种Native的加载框架,这里将loadImage
的实际加载任务通过代理的方式抛出去实现,因此简单定义几个代理接口类:
public class ImageLoader {\\n\\n private static ILoaderProxy mProxy;\\n\\n public static ILoaderProxy getProxy() {\\n return mProxy;\\n }\\n\\n public static void setProxy(ILoaderProxy proxy) {\\n mProxy = proxy;\\n }\\n}\\n
\\npublic interface ILoaderProxy<T> {\\n\\n T loadImage(Context appCtx, LoadRequest request, ILoadCallback target);\\n\\n void cancelLoad(T task);\\n}\\n
\\npublic interface ILoadCallback {\\n\\n void onLoadSuccess(Drawable drawable);\\n\\n void onLoadFailed(String error);\\n}\\n
\\n这里在Android
上选择Drawable
作为加载结果的载体,而iOS上,则使用UIImage
。所以只要通过ILoaderProxy
实现真正的加载,然后ImageTextureView
作为ILoadCallback
的实现者,就能打通外部的图片加载框架和ImageTextureView
了。
class ImageTextureView implements ILoadCallback {\\n private final SurfaceTexture mSurfaceTexture;\\n private MethodChannel.Result mResult;\\n private Drawable mDrawable;\\n \\n public void loadImage(LoadRequest loadRequest, MethodChannel.Result result) {\\n mResult = result;\\n ImageLoader.getProxy().loadImage(mContext, loadRequest, this);\\n }\\n \\n @Override\\n public void onLoadSuccess(Drawable result) {\\n //接收到图片加载的结果\\n mDrawable = result;\\n }\\n \\n @Override\\n public void onLoadFailed(String error) {\\n //图片加载失败\\n }\\n
\\nAndroid上以Glide
库为示例,对接ILoaderProxy:
public class GlideLoader implements ILoaderProxy<FutureTarget<Drawable>> {\\n\\n @Override\\n public FutureTarget<Drawable> loadImage(Context appCtx, LoadRequest request,\\n ILoadCallback target) {\\n String uri = request.uri;\\n int width = request.width;\\n int height = request.height;\\n RequestBuilder<Drawable> builder = Glide.with(appCtx).asDrawable()\\n .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC).load(uri).addListener(new RequestListener<Drawable>() {\\n @Override\\n public boolean onLoadFailed(@Nullable GlideException e, Object model,\\n Target<Drawable> t,\\n boolean isFirstResource) {\\n if (e != null) {\\n target.onLoadFailed(e.getMessage());\\n } else {\\n target.onLoadFailed(\\"load failed.\\");\\n }\\n return true;\\n }\\n\\n @Override\\n public boolean onResourceReady(Drawable resource, Object model,\\n Target<Drawable> t,\\n DataSource dataSource,\\n boolean isFirstResource) {\\n if (resource instanceof GifDrawable) {\\n ((GifDrawable) resource).start();\\n }\\n target.onLoadSuccess(resource);\\n return true;\\n }\\n });\\n if (width > 0 && height > 0) {\\n return builder.submit(width, height);\\n }\\n return builder.submit(Target.SIZE_ORIGINAL, FutureTarget.SIZE_ORIGINAL);\\n }\\n\\n @Override\\n public void cancelLoad(FutureTarget<Drawable> task) {\\n if (task != null) {\\n if (!task.isDone()) {\\n task.cancel(true);\\n }\\n }\\n }\\n}\\n
\\niOS上以SDWebImage
为示例:
class SDWebImageLoader: NSObject, ILoaderProxy {\\n \\n typealias T = SDWebImageCombinedOperation?\\n \\n func loadImage(from request: LoadRequest, callback: ILoadCallback) -> SDWebImageCombinedOperation? {\\n guard let uri = request.uri else {\\n callback.onFailure(error: \\"Missing image URI\\")\\n return nil\\n }\\n \\n // 使用 SDWebImage 加载图片\\n return SDWebImageManager.shared.loadImage(with: URL(string: uri), options: [], progress: nil) { (image, data, error, cacheType, finished, url) in\\n if let error = error {\\n callback.onFailure(error: error.localizedDescription)\\n } else {\\n guard let image = image else {\\n callback.onFailure(error: \\"Failed to load image\\")\\n return\\n }\\n callback.notifyUIImage(image: image)\\n let imageInfo = NImageInfo()\\n imageInfo.uri = uri\\n imageInfo.imageWidth = Int(image.size.width)\\n imageInfo.imageHeight = Int(image.size.height)\\n callback.onSuccess(imageInfo: imageInfo)\\n }\\n }\\n }\\n \\n func cancelLoad(task: Any) {\\n (task as! SDWebImageCombinedOperation).cancel()\\n }\\n}\\n
\\n既然已经拿到了加载结果,那么就需要将其绘制到Surface上。
\\n在绘制Drawable之前,需要设置SurfaceTexture
的BuferSize,可以直接设置为Flutter层Texture Widget的宽高,也就是loadImage
传过来的LoadRequest
中的宽高:
//mSurfaceW = loadRequest.width;\\n//mSurfaceH = loadRequest.height;\\nmSurfaceTexture.setDefaultBufferSize(mSurfaceW, mSurfaceH);\\n
\\n在Canvas上绘制Drawable非常的简单,没有什么特殊处理的话,基本就是下面这样
\\ncanvas = mSurface.lockCanvas(null);\\nif (canvas != null) {\\n mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));\\n canvas.drawPaint(mPaint);\\n mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));\\n drawable.draw(canvas);\\n mSurface.unlockCanvasAndPost(canvas);\\n}\\n
\\n为了充分利用Drawable
的特性,以及为了后面对动图的支持,这里让ImageTextureView
去实现Drawable.Callback
:
public interface Callback {\\n\\n void invalidateDrawable(@NonNull Drawable who);\\n\\n void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when);\\n\\n void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what);\\n}\\n
\\n在invalidateDrawable
中,实现在Canvas上绘制Drawable。这样的话,也就只需要在onLoadSuccess
中调用invalidateDrawable
或者drawable.invalidateSelf()
触发绘制即可:
@Override\\n public void onLoadSuccess(Drawable result) {\\n //接收到图片加载的结果\\n mDrawable = result;\\n mDrawable.setCallback(this);\\n invalidateDrawable(mDrawable); //mDrawable.invalidateSelf();\\n }\\n
\\niOS上只要把UIImage
转成CVPixelBuffer
即可:
func imageToPixelBuffer(uiimage: UIImage) -> CVPixelBuffer? {\\n let image = uiimage.cgImage\\n let imageWidth = image.width\\n let imageHeight = image.height\\n \\n var pixelBuffer: CVPixelBuffer?\\n let options: [String: Any] = [\\n kCVPixelBufferIOSurfacePropertiesKey as String: [:],\\n kCVPixelBufferCGImageCompatibilityKey as String: false,\\n kCVPixelBufferCGBitmapContextCompatibilityKey as String: false\\n ]\\n \\n let status = CVPixelBufferCreate(kCFAllocatorDefault, imageWidth, imageHeight, kCVPixelFormatType_32BGRA, options as CFDictionary, &pixelBuffer)\\n \\n if status != kCVReturnSuccess {\\n return nil\\n }\\n \\n CVPixelBufferLockBaseAddress(pixelBuffer!, CVPixelBufferLockFlags(rawValue: 0))\\n let data = CVPixelBufferGetBaseAddress(pixelBuffer!)\\n \\n let colorSpace = CGColorSpaceCreateDeviceRGB()\\n let context = CGContext(data: data, width: imageWidth, height: imageHeight, bitsPerComponent: 8, bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer!), space: colorSpace, bitmapInfo: kCGBitmapByteOrder32Host.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue)\\n \\n context?.draw(image, in: CGRect(x: 0, y: 0, width: imageWidth, height: imageHeight))\\n \\n CVPixelBufferUnlockBaseAddress(pixelBuffer!, CVPixelBufferLockFlags(rawValue: 0))\\n \\n return pixelBuffer\\n}\\n
\\n能够将图片绘制到纹理上之后,就该处理正确显示图片效果
的问题。也就是将图片的哪一部分区域,绘制到画布的哪个区域里
,实际就是Flutter中BoxFit
的显示效果。
在Android上处理Fit的效果,主要是依靠canvas.concat(Matrix)
,这个的具体实现,可以直接偷懒,Android的ImageView
,支持各种scaleType
,图片载体也是Drawable
,所以直接将它的configureBounds
和onDraw
函数拷贝出来直接用就可以了。
在iOS上,则是采用转换成CopyPixelBuffer
之前,对加载得到的UIImage
进行处理,重新生成一个Surface尺寸的UIImage
,这个UIImage的效果就是BoxFit后的效果。
private func fitTransform(_ image: UIImage) -> UIImage {\\n let viewSize = CGSize(width: self.loadRequest!.width!, height: self.loadRequest!.height!)\\n let imageSize = image.size\\n switch self.loadRequest?.fit {\\n case .none?:\\n // 填充模式:裁剪图像以填充整个视图\\n // 创建一个新的图像上下文\\n UIGraphicsBeginImageContextWithOptions(CGSize(width: viewSize.width, height: viewSize.height), false, image.scale)\\n \\n // 绘制图像到新的上下文\\n image.draw(in: CGRect(x: (viewSize.width - imageSize.width) / 2, y: (viewSize.height - imageSize.height) / 2, width: imageSize.width, height: imageSize.height))\\n \\n // 从上下文获取新的图像\\n let newImage = UIGraphicsGetImageFromCurrentImageContext()\\n \\n // 结束图像上下文\\n UIGraphicsEndImageContext()\\n \\n return newImage ?? image\\n case .contain:\\n // 包含模式:缩放图像以完全显示在视图中\\n // 计算缩放比例\\n let scaleWidth = viewSize.width / imageSize.width\\n let scaleHeight = viewSize.height / imageSize.height\\n let scale = min(scaleWidth, scaleHeight)\\n \\n // 计算缩放后的图像尺寸\\n let newWidth = imageSize.width * scale\\n let newHeight = imageSize.height * scale\\n \\n // 创建一个新的图像上下文\\n UIGraphicsBeginImageContextWithOptions(CGSize(width: viewSize.width, height: viewSize.height), false, image.scale)\\n \\n // 绘制图像到新的上下文\\n image.draw(in: CGRect(x: (viewSize.width - newWidth) / 2, y: (viewSize.height - newHeight) / 2, width: newWidth, height: newHeight))\\n \\n // 从上下文获取新的图像\\n let newImage = UIGraphicsGetImageFromCurrentImageContext()\\n \\n // 结束图像上下文\\n UIGraphicsEndImageContext()\\n \\n return newImage ?? image\\n case .cover:\\n // 覆盖模式:缩放图像以覆盖整个视图,可能会裁剪部分图像\\n // 计算缩放比例\\n let scaleWidth = viewSize.width / imageSize.width\\n let scaleHeight = viewSize.height / imageSize.height\\n let scale = max(scaleWidth, scaleHeight)\\n \\n // 计算裁剪后的图像尺寸\\n let newWidth = imageSize.width * scale\\n let newHeight = imageSize.height * scale\\n \\n // 创建一个新的图像上下文\\n UIGraphicsBeginImageContextWithOptions(CGSize(width: viewSize.width, height: viewSize.height), false, image.scale)\\n \\n // 绘制图像到新的上下文\\n image.draw(in: CGRect(x: (viewSize.width - newWidth) / 2, y: (viewSize.height - newHeight) / 2, width: newWidth, height: newHeight))\\n \\n // 从上下文获取新的图像\\n let newImage = UIGraphicsGetImageFromCurrentImageContext()\\n \\n // 结束图像上下文\\n UIGraphicsEndImageContext()\\n \\n return newImage ?? image\\n case .fitWidth:\\n // 适应宽度模式:缩放图像以适应视图的宽度,高度可能会超出视图\\n // 计算缩放比例\\n let scaleWidth = viewSize.width / imageSize.width\\n let scale = scaleWidth\\n \\n // 计算缩放后的图像尺寸\\n let newWidth = imageSize.width * scale\\n let newHeight = imageSize.height * scale\\n \\n // 创建一个新的图像上下文\\n UIGraphicsBeginImageContextWithOptions(CGSize(width: viewSize.width, height: viewSize.height), false, image.scale)\\n \\n // 绘制图像到新的上下文\\n image.draw(in: CGRect(x: (viewSize.width - newWidth) / 2, y: (viewSize.height - newHeight) / 2, width: newWidth, height: newHeight))\\n \\n // 从上下文获取新的图像\\n let newImage = UIGraphicsGetImageFromCurrentImageContext()\\n \\n // 结束图像上下文\\n UIGraphicsEndImageContext()\\n \\n return newImage ?? image\\n case .fitHeight:\\n // 适应高度模式:缩放图像以适应视图的高度,宽度可能会超出视图\\n // 计算缩放比例\\n let scaleHeight = viewSize.height / imageSize.height\\n let scale = scaleHeight\\n \\n // 计算缩放后的图像尺寸\\n let newWidth = imageSize.width * scale\\n let newHeight = imageSize.height * scale\\n \\n // 创建一个新的图像上下文\\n UIGraphicsBeginImageContextWithOptions(CGSize(width: viewSize.width, height: viewSize.height), false, image.scale)\\n \\n // 绘制图像到新的上下文\\n image.draw(in: CGRect(x: (viewSize.width - newWidth) / 2, y: (viewSize.height - newHeight) / 2, width: newWidth, height: newHeight))\\n \\n // 从上下文获取新的图像\\n let newImage = UIGraphicsGetImageFromCurrentImageContext()\\n \\n // 结束图像上下文\\n UIGraphicsEndImageContext()\\n \\n return newImage ?? image\\n case .scaleDown:\\n // 缩小模式:如果图像大于视图,则缩小图像以适应视图\\n // 计算缩放比例\\n let scaleWidth = viewSize.width / imageSize.width\\n let scaleHeight = viewSize.height / imageSize.height\\n let scale = min(scaleWidth, scaleHeight)\\n \\n // 如果缩放比例小于1,则进行缩放\\n if scale < 1 {\\n // 计算缩放后的图像尺寸\\n let newWidth = imageSize.width * scale\\n let newHeight = imageSize.height * scale\\n \\n // 创建一个新的图像上下文\\n UIGraphicsBeginImageContextWithOptions(CGSize(width: viewSize.width, height: viewSize.height), false, image.scale)\\n \\n // 绘制图像到新的上下文\\n image.draw(in: CGRect(x: (viewSize.width - newWidth) / 2, y: (viewSize.height - newHeight) / 2, width: newWidth, height: newHeight))\\n \\n // 从上下文获取新的图像\\n let newImage = UIGraphicsGetImageFromCurrentImageContext()\\n \\n // 结束图像上下文\\n UIGraphicsEndImageContext()\\n \\n return newImage ?? image\\n } else {\\n // 如果图像小于或等于视图\\n // 创建一个新的图像上下文\\n UIGraphicsBeginImageContextWithOptions(CGSize(width: viewSize.width, height: viewSize.height), false, image.scale)\\n \\n // 绘制图像到新的上下文\\n image.draw(in: CGRect(x: (viewSize.width - imageSize.width) / 2, y: (viewSize.height - imageSize.height) / 2, width: imageSize.width, height: imageSize.height))\\n \\n // 从上下文获取新的图像\\n let newImage = UIGraphicsGetImageFromCurrentImageContext()\\n \\n // 结束图像上下文\\n UIGraphicsEndImageContext()\\n \\n return newImage ?? image\\n }\\n default:\\n // 如果 fit 模式是 .fill则直接返回原始图像\\n return image\\n }\\n}\\n
\\n既然绘制都是由Native自己来了,那么对动图的支持其实就是不停的绘制最新的帧到纹理
上即可。
在Android上,Native对于动图的Drawable
,有子类AnimationDrawable
进行支持。因为我们将ImageTextureView
绑定作为Drawable.Callback
,所以只需要调用AnimationDrawable.start
,就能直接支持动图的绘制,不需要做其他处理。
而在iOS上,UIImage
中有images
和duration
来记录动图的所有帧和动图播放总时长。但是更新绘制,需要自己来实现。这里采用Timer.scheduledTimer
来实现一个定时触发的逻辑即可:
private func showNextFrame() {\\n if (self.animatedPlayTimer == nil) {\\n self.animatedPlayTimer = Timer.scheduledTimer(withTimeInterval: self.frameInterval!, repeats: true) { timer in\\n self.index += 1\\n if (self.index >= self.animatedImages!.count) {\\n self.index = 0\\n }\\n let frame = self.animatedImages![self.index]\\n let fitImage = self.fitTransform(frame)\\n if let cgImage = fitImage.cgImage {\\n let p = self.imageToPixelBuffer(image: cgImage)\\n self.notifyTextureUpdate(pixelBuffer: p!)\\n }\\n }\\n }\\n}\\n\\nprivate func notifyTextureUpdate(pixelBuffer: CVPixelBuffer) {\\n self.pixelBuffer = pixelBuffer\\n //判断是否是主线程,如果不是,则切换到主线程调用\\n if (Thread.isMainThread) {\\n guard let textureId = self.textureId else {\\n return\\n }\\n self.textureRegistry.textureFrameAvailable(textureId)\\n } else {\\n DispatchQueue.main.async {\\n guard let textureId = self.textureId else {\\n return\\n }\\n self.textureRegistry.textureFrameAvailable(textureId)\\n }\\n }\\n}\\n
\\n既然支持的动图,那肯定需要考虑,何时触发动图的播放和停止。因为动图本身的载体还是Native的Drawable
或UIImage
,且Native的图片加载框架,肯定也会有内存缓存,那么Flutter层多个相同纹理,很可能使用的是Native中同一个Drawable
或UIImage
对象。那么如果停止播放,势必会导致Flutter层使用这个纹理的地方,都停止了动图的播放,就出问题了。
这里采用最简单的处理办法:
\\n因此,Flutter和Native的交互通道,新增两个方法:
\\nvoid setVisible(int textureId);\\nvoid setInVisible(int textureId);\\n
\\nNative对接这两个方法,然后进行start
和stop
即可。
shadertoy 中的实现是webgl 的写法,转成flutter 可以用的glsl,需要做如下修改。
\\n1 main方法
\\n在shadertoy中 一般是以mainImage 开始
\\nvoid mainImage( out vec4 fragColor, in vec2 fragCoord ){}\\n
\\n在flutter glsl中意main方法开始
\\nvoid main(){}\\n
\\n2 fragColor 与 fragCoord
\\n2.1 fragColor
\\n在shadertoy的mainImage方法中 一般在最后给fragColor赋值。也就是最终输出的颜色
\\n\\n在flutter glsl中main方法中没有fragColor,需要全局声明一个fragColor变量。
out vec4 fragColor;\\n
\\n2.2 fragCoord
\\nfragCoord 是屏幕上每一个像素的坐标。glsl是一个像素一个像素的处理,fragCoord就代表的是当前处理的像素的坐标。在shadertoy中fragCoord在mainImage方法中获取。
\\n在flutter glsl通过 FlutterFragCoord().xy 获取。
\\n3 iTime 一般涉及到随时间变化的都会有这个变量
\\n在shadertoy中,iTime是内置的变量,代表从开始运行到当前的时间。
\\n在Flutter glsl中需要自己定义输入变量,由开发者外部传入。
\\n4 iResolution
\\n在shadertoy中,iResolution存储的是屏幕的尺寸信息。iResolution是个内置的变量。
\\n在flutter glsl中需要外部传入。也就是画布的大小。
\\n外部传入的一般写法:
\\n var width = size.width;\\n var height = size.height;\\n shader.setFloat(0, width);\\n shader.setFloat(1, height);\\n shader.setFloat(2, currentTime);\\n
\\n5 for 循环
\\n这样写是不可以的
\\nfor( int i=0; i<70 && t<tmax; i++ )\\n
\\n需要这样
\\nfor (int i = 0; i < 70; i++) {\\n if (t >= tmax) {\\n break;\\n }\\n // 循环体中的其余代码\\n}\\n
","description":"shadertoy 中的实现是webgl 的写法,转成flutter 可以用的glsl,需要做如下修改。 1 main方法\\n\\n在shadertoy中 一般是以mainImage 开始\\n\\nvoid mainImage( out vec4 fragColor, in vec2 fragCoord ){}\\n\\n\\n在flutter glsl中意main方法开始\\n\\nvoid main(){}\\n\\n\\n2 fragColor 与 fragCoord\\n\\n2.1 fragColor\\n\\n在shadertoy的mainImage方法中 一般在最后给fragColor赋值。也就是最终输出的颜色…","guid":"https://juejin.cn/post/7457736454561382426","author":"火柴就是我","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-09T09:44:08.493Z","media":[{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/71a65d2cc1364015bc8f4c22f3616f10~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg54Gr5p-05bCx5piv5oiR:q75.awebp?rk3s=f64ab15b&x-expires=1737024729&x-signature=GMjS5M%2FLK%2F0hdVwlATH%2F43y2VHg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/38f5daa6b9f64139bda78ea0375014bf~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg54Gr5p-05bCx5piv5oiR:q75.awebp?rk3s=f64ab15b&x-expires=1737024729&x-signature=v2Z7qK47S24E9inM9Y7Xo9QGsdg%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6658d76e86cd4b4bb09cc967221a163c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg54Gr5p-05bCx5piv5oiR:q75.awebp?rk3s=f64ab15b&x-expires=1737024729&x-signature=%2BmpMARFBR3mwo5PWM8QSeZ6wPNk%3D","type":"photo","width":0,"height":0,"blurhash":""},{"url":"https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3b5c89aa597c45638db5d4a03b830dd4~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg54Gr5p-05bCx5piv5oiR:q75.awebp?rk3s=f64ab15b&x-expires=1737024729&x-signature=DVEoo5b8P8BKwFkWLl1E5SXCt%2BA%3D","type":"photo","width":0,"height":0,"blurhash":""}],"categories":["Android","Flutter"],"attachments":null,"extra":null,"language":null},{"title":"系统化掌握Dart编程之流程控制","url":"https://juejin.cn/post/7457418235580891199","content":"流程控制——构建灵活、智能且高效应用程序的基石。它决定了程序的执行路径,使得代码可以根据不同的条件和输入做出响应,从而实现复杂而多变的逻辑需求。掌握流程控制结构不仅是编写功能丰富的应用的基础","description":"流程控制——构建灵活、智能且高效应用程序的基石。它决定了程序的执行路径,使得代码可以根据不同的条件和输入做出响应,从而实现复杂而多变的逻辑需求。掌握流程控制结构不仅是编写功能丰富的应用的基础","guid":"https://juejin.cn/post/7457418235580891199","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-08T07:26:55.742Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Flutter应用开发:定位上报","url":"https://juejin.cn/post/7457339608240291891","content":"市面上主流APP都存在定位上报的诉求,例如抖音、美团等。不过也存在简单和复杂的区别,本文主要从需求开始一步一步分析落地。","description":"市面上主流APP都存在定位上报的诉求,例如抖音、美团等。不过也存在简单和复杂的区别,本文主要从需求开始一步一步分析落地。","guid":"https://juejin.cn/post/7457339608240291891","author":"机器瓦力","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-08T03:13:49.214Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"系统化掌握Dart编程之运算符与表达式","url":"https://juejin.cn/post/7457343141042290742","content":"系统化掌握Dart编程之运算符与表达式","description":"系统化掌握Dart编程之运算符与表达式","guid":"https://juejin.cn/post/7457343141042290742","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-08T02:52:53.541Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"系统化掌握Dart编程之变量与常量","url":"https://juejin.cn/post/7456977898379329599","content":"变量与常量——编程世界的基石 在编程中,数据如同流动的河流,时而平静,时而激荡。为了驾驭这股力量,程序员们发明了两种基本工具:变量和常量。它们就像是编程语言中的螺丝钉和齿轮,看似简单却不可或缺。","description":"变量与常量——编程世界的基石 在编程中,数据如同流动的河流,时而平静,时而激荡。为了驾驭这股力量,程序员们发明了两种基本工具:变量和常量。它们就像是编程语言中的螺丝钉和齿轮,看似简单却不可或缺。","guid":"https://juejin.cn/post/7456977898379329599","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-07T04:09:26.948Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"GetX框架里容易被忽略的那些小知识(五)","url":"https://juejin.cn/post/7456872771847553034","content":"GetX框架里容易被忽略的那些小知识(五)","description":"GetX框架里容易被忽略的那些小知识(五)","guid":"https://juejin.cn/post/7456872771847553034","author":"一名普通的程序员","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-07T03:03:48.793Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"系统化掌握Dart编程之数据类型","url":"https://juejin.cn/post/7456857517029965860","content":"系统化掌握Dart编程之数据类型","description":"系统化掌握Dart编程之数据类型","guid":"https://juejin.cn/post/7456857517029965860","author":"地狱勇士","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-07T02:35:37.407Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Dart学习之注释","url":"https://juejin.cn/post/7456769287350517779","content":"注释就像一篇文章的精髓摘要,不仅要令自己一目了然,更要确保他人亦能迅速领悟。它如同指引方向的明灯,照亮代码的深层逻辑与意图。","description":"注释就像一篇文章的精髓摘要,不仅要令自己一目了然,更要确保他人亦能迅速领悟。它如同指引方向的明灯,照亮代码的深层逻辑与意图。","guid":"https://juejin.cn/post/7456769287350517779","author":"科昂","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-06T14:00:42.110Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"PAG 提示java 版本问题 一直提示 1.7","url":"https://juejin.cn/post/7456657044947795977","content":"可能你需要添加可能你需要添加可能你需要添加可能你需要添加可能你需要添加可能你需要添加可能你需要添加可能你需要添加可能你需要添加可能你需要添加可能你需要添加可能你需要添加可能你需要添加可能你需要添加可能","description":"可能你需要添加可能你需要添加可能你需要添加可能你需要添加可能你需要添加可能你需要添加可能你需要添加可能你需要添加可能你需要添加可能你需要添加可能你需要添加可能你需要添加可能你需要添加可能你需要添加可能","guid":"https://juejin.cn/post/7456657044947795977","author":"火柴就是我","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-06T07:09:18.428Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Dart async/await 和 Kotlin suspend 有什么区别?顺带看看 Oppo ColorOS 上的 Flutter “彩蛋”","url":"https://juejin.cn/post/7456407906634825755","content":"在之前闲聊的《Kotlin 协程能够完全替代线程吗?》的内容里,有人提了这样的问题:Dart async/await 和 Kotlin suspend 还有 JS 的异步有什么区别? 实际上不管是 a","description":"在之前闲聊的《Kotlin 协程能够完全替代线程吗?》的内容里,有人提了这样的问题:Dart async/await 和 Kotlin suspend 还有 JS 的异步有什么区别? 实际上不管是 a","guid":"https://juejin.cn/post/7456407906634825755","author":"恋猫de小郭","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-06T00:13:58.626Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Dart 3.6.0 入门教程(二)","url":"https://juejin.cn/post/7456354304100171788","content":"Dart 3.6.0 入门教程(二)","description":"Dart 3.6.0 入门教程(二)","guid":"https://juejin.cn/post/7456354304100171788","author":"xvch","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-05T13:39:37.476Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"【Flutter入门】2. 快速掌握Dart语言 - 从 Java、JavaScript 转型必看","url":"https://juejin.cn/post/7455702717189881897","content":"【Flutter入门】2. 快速掌握Dart语言 - 从 Java、JavaScript 转型必看","description":"【Flutter入门】2. 快速掌握Dart语言 - 从 Java、JavaScript 转型必看","guid":"https://juejin.cn/post/7455702717189881897","author":"西辰Knight","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-04T02:41:15.653Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"一个常见面试问题:Kotlin 协程能够完全取代线程吗?为什么?","url":"https://juejin.cn/post/7455576220374368282","content":"某种程度上考虑「Kotlin 协程确实足够直接取代线程」,但是「协程能够完全取代线程」的说法其实不太准确,毕竟协程是必须基于线程,所以线程肯定是需要存在的,更准确的说,应该是 Kotlin 协程在 A","description":"某种程度上考虑「Kotlin 协程确实足够直接取代线程」,但是「协程能够完全取代线程」的说法其实不太准确,毕竟协程是必须基于线程,所以线程肯定是需要存在的,更准确的说,应该是 Kotlin 协程在 A","guid":"https://juejin.cn/post/7455576220374368282","author":"恋猫de小郭","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-03T09:04:46.087Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"flutter开发的适合程序员的笔记软件","url":"https://juejin.cn/post/7455498800837574694","content":"个人认为适合程序员的软件有几个特征: 体积小,使用方便 本地存储笔记,文件夹管理笔记 快速搜索 无需登录,无需验证 支持富文本 ok,那一定要试试温知文档,采用flutter开发的跨平台软件。","description":"个人认为适合程序员的软件有几个特征: 体积小,使用方便 本地存储笔记,文件夹管理笔记 快速搜索 无需登录,无需验证 支持富文本 ok,那一定要试试温知文档,采用flutter开发的跨平台软件。","guid":"https://juejin.cn/post/7455498800837574694","author":"果冻橙橙君","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-03T04:51:11.273Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Dart 3.6.0 入门教程(一)","url":"https://juejin.cn/post/7455491124564901907","content":"Dart 3.6.0 入门教程(一)。-------------------------------","description":"Dart 3.6.0 入门教程(一)。-------------------------------","guid":"https://juejin.cn/post/7455491124564901907","author":"xvch","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-03T03:45:59.006Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Flutter封装:对 Flex.spacing 不满意,所以封装了NFlexSeparated","url":"https://juejin.cn/post/7455269259671126028","content":"一、思路来源 Flutter 3.27 会出一个 Flex.spacing 可以添加子项 spacing间距,但是仅仅添加间距不支持 Widget 所以我封装了NFlexSeparated。 Flut","description":"一、思路来源 Flutter 3.27 会出一个 Flex.spacing 可以添加子项 spacing间距,但是仅仅添加间距不支持 Widget 所以我封装了NFlexSeparated。 Flut","guid":"https://juejin.cn/post/7455269259671126028","author":"SoaringHeart","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-02T15:02:01.535Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"flutter 开发笔记(八):本地化","url":"https://juejin.cn/post/7455190390229598208","content":"flutter 开发笔记(八):本地化","description":"flutter 开发笔记(八):本地化","guid":"https://juejin.cn/post/7455190390229598208","author":"张二三","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-02T08:05:50.873Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"FlutterUnit 3.1.0 | 桌面端应用内更新、全局搜索","url":"https://juejin.cn/post/7454579652629561353","content":"光阴似箭,新的一年又开始啦 ~ 大家元旦快乐 最近我用 Rust 设计并搭建了一套后端服务,后续 FlutterUnit 将会有更多网络请求的数据啦。目前来小试牛刀的就是对于一个野生 App 而言至关","description":"光阴似箭,新的一年又开始啦 ~ 大家元旦快乐 最近我用 Rust 设计并搭建了一套后端服务,后续 FlutterUnit 将会有更多网络请求的数据啦。目前来小试牛刀的就是对于一个野生 App 而言至关","guid":"https://juejin.cn/post/7454579652629561353","author":"张风捷特烈","authorUrl":null,"authorAvatar":null,"publishedAt":"2025-01-01T09:24:00.126Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Flutter 开发速成(三)——开发一个 TodoMVC 应用","url":"https://juejin.cn/post/7454413392637476890","content":"Flutter 开发速成(三)——开发一个 TodoMVC 应用","description":"Flutter 开发速成(三)——开发一个 TodoMVC 应用","guid":"https://juejin.cn/post/7454413392637476890","author":"Winwin","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-31T07:19:45.673Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"轻量开源Flutter 热更新库 MicroDart使用指南","url":"https://juejin.cn/post/7454411502053965876","content":"轻量开源Flutter 热更新库 MicroDart,食用指南。 下载地址:https://github.com/lancexin/micro_dart","description":"轻量开源Flutter 热更新库 MicroDart,食用指南。 下载地址:https://github.com/lancexin/micro_dart","guid":"https://juejin.cn/post/7454411502053965876","author":"孤鸿玉","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-31T06:47:09.877Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Flutter 2024 年度回顾总结,致敬这精彩的一年","url":"https://juejin.cn/post/7454397827955867658","content":"2024 年的最后一天,就让我们快速回顾下这一年里 Flutter 给我们带来了哪些变化,当然 2024 肯定少不了鸿蒙的身影。 这一年里 Flutter 主要发布了 3.19、3.22、3.24 和","description":"2024 年的最后一天,就让我们快速回顾下这一年里 Flutter 给我们带来了哪些变化,当然 2024 肯定少不了鸿蒙的身影。 这一年里 Flutter 主要发布了 3.19、3.22、3.24 和","guid":"https://juejin.cn/post/7454397827955867658","author":"恋猫de小郭","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-31T04:02:39.961Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"flame forge2d 实现随机小怪以及飞镖射中爆炸","url":"https://juejin.cn/post/7454105683702005770","content":"flame forge2d 实现随机小怪以及飞镖射中爆炸","description":"flame forge2d 实现随机小怪以及飞镖射中爆炸","guid":"https://juejin.cn/post/7454105683702005770","author":"火柴就是我","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-30T09:29:07.010Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Flutter应用开发:返回列表刷新并保持原始操作位置","url":"https://juejin.cn/post/7454045994272538633","content":"Flutter应用开发:返回列表刷新并保持原始操作位置","description":"Flutter应用开发:返回列表刷新并保持原始操作位置","guid":"https://juejin.cn/post/7454045994272538633","author":"机器瓦力","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-30T07:35:15.971Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Flutter 组件集录 | Focus 组件 - 我就是焦点~","url":"https://juejin.cn/post/7454006358603153427","content":"Flutter 组件集录 | Focus 组件 - 我就是焦点~","description":"Flutter 组件集录 | Focus 组件 - 我就是焦点~","guid":"https://juejin.cn/post/7454006358603153427","author":"张风捷特烈","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-30T01:38:27.359Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"记录一次flutter项目更新到3.27","url":"https://juejin.cn/post/7453354379168268325","content":"本人小白,正在学习flutter,之前看见一个大佬发的joker fun(段子乐)Flutter仿写段子乐app来咯~Flutter仿写段子乐app,项目整体基于GetX实现路由跳转、依赖注入、状态管","description":"本人小白,正在学习flutter,之前看见一个大佬发的joker fun(段子乐)Flutter仿写段子乐app来咯~Flutter仿写段子乐app,项目整体基于GetX实现路由跳转、依赖注入、状态管","guid":"https://juejin.cn/post/7453354379168268325","author":"用户2683036895597","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-28T13:27:38.331Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"HarmonyOS NEXT | 一文搞懂 华为账号登录(获取UnionID/OpenID)","url":"https://juejin.cn/post/7453304066451275795","content":"前言 随着HarmonyOS NEXT的逐渐完善,越来越多的开发者开始加入这一平台。很多时候开发者开发的相关应用都有账号系统,往往是需要用户先注册,填写邮箱电话等,复杂而繁琐。 刚开始上架了一款Har","description":"前言 随着HarmonyOS NEXT的逐渐完善,越来越多的开发者开始加入这一平台。很多时候开发者开发的相关应用都有账号系统,往往是需要用户先注册,填写邮箱电话等,复杂而繁琐。 刚开始上架了一款Har","guid":"https://juejin.cn/post/7453304066451275795","author":"Jalor","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-28T08:42:42.149Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"《Flutter性能优化全攻略:从首屏渲染到性能监测,附案例代码详解》","url":"https://juejin.cn/post/7452988212803502115","content":"性能优化(App)随着移动端开发的发展,用户对 App 性能的要求越来越高,首屏加载的流畅度、长列表的加载速度、动画的统一性、甚至异常日志的监控,都是开发中需要关注的重点。","description":"性能优化(App)随着移动端开发的发展,用户对 App 性能的要求越来越高,首屏加载的流畅度、长列表的加载速度、动画的统一性、甚至异常日志的监控,都是开发中需要关注的重点。","guid":"https://juejin.cn/post/7452988212803502115","author":"你听得到11","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-27T09:58:00.288Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"flutter flame forge2d 实现任务发射回旋镖","url":"https://juejin.cn/post/7452913993570009139","content":"上一次使用 forge2d 实现了人物移动 边界碰撞 以及 场景建筑物的添加 https://juejin.cn/post/7442512509283860534 这次我们实现人物能发射回旋镖。 在实","description":"上一次使用 forge2d 实现了人物移动 边界碰撞 以及 场景建筑物的添加 https://juejin.cn/post/7442512509283860534 这次我们实现人物能发射回旋镖。 在实","guid":"https://juejin.cn/post/7452913993570009139","author":"火柴就是我","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-27T06:07:07.947Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"flutter自学笔记7- 状态管理","url":"https://juejin.cn/post/7452720059641282560","content":"flutter自学笔记7- 状态管理","description":"flutter自学笔记7- 状态管理","guid":"https://juejin.cn/post/7452720059641282560","author":"捡芝麻丢西瓜","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-27T02:00:57.122Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"flutter自学笔记6- 网络请求、序列化、平台通道介绍","url":"https://juejin.cn/post/7452753943610294272","content":"flutter自学笔记6- 网络请求、序列化、平台通道介绍","description":"flutter自学笔记6- 网络请求、序列化、平台通道介绍","guid":"https://juejin.cn/post/7452753943610294272","author":"捡芝麻丢西瓜","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-27T01:53:00.080Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Flutter是如何处理一次点击事件","url":"https://juejin.cn/post/7452641558639165474","content":"Flutter是如何处理一次点击事件","description":"Flutter是如何处理一次点击事件","guid":"https://juejin.cn/post/7452641558639165474","author":"laterlater","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-26T12:20:16.478Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"flutter 开发笔记(七):音视频","url":"https://juejin.cn/post/7452610281720299520","content":"flutter 开发笔记(七):音视频","description":"flutter 开发笔记(七):音视频","guid":"https://juejin.cn/post/7452610281720299520","author":"张二三","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-26T10:00:15.937Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"鸿蒙原生开发手记:04-一个完整元服务案例","url":"https://juejin.cn/post/7452592478694686720","content":"影院热映 简介 整个元服务分为 4-5 个页面,首页为列表页,展示了当前影院热门的电影,点开是一个详情介绍页,里面有影片详情,演职表,相关影片推荐等,热门海报。","description":"影院热映 简介 整个元服务分为 4-5 个页面,首页为列表页,展示了当前影院热门的电影,点开是一个详情介绍页,里面有影片详情,演职表,相关影片推荐等,热门海报。","guid":"https://juejin.cn/post/7452592478694686720","author":"zacksleo","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-26T09:16:15.625Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"【Flutter入门】1. 从零开始的flutter跨平台开发之旅(概述、环境搭建、第一个Flutter应用)","url":"https://juejin.cn/post/7452547194539687955","content":"想快速掌握现代移动应用开发吗?本文带你走进Google推出的开源UI工具包——Flutter的世界。通过简洁明了的步骤,你将学会如何在Windows和macOS上搭建开发环境,并创建一个简单的app应","description":"想快速掌握现代移动应用开发吗?本文带你走进Google推出的开源UI工具包——Flutter的世界。通过简洁明了的步骤,你将学会如何在Windows和macOS上搭建开发环境,并创建一个简单的app应","guid":"https://juejin.cn/post/7452547194539687955","author":"西辰Knight","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-26T06:23:34.289Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Flutter Warning: SDK processing. This version only understands SDK XML versions","url":"https://juejin.cn/post/7452538597196546111","content":"一、简介 执行 flutter run 的时候,提示警告: Warning: SDK processing. This version only understands SDK XML version","description":"一、简介 执行 flutter run 的时候,提示警告: Warning: SDK processing. This version only understands SDK XML version","guid":"https://juejin.cn/post/7452538597196546111","author":"卡尔特斯","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-26T05:57:41.786Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Flutter 组件集录 | KeyboardListener + CallbackShortcuts","url":"https://juejin.cn/post/7452337927328071743","content":"在桌面端开发中,键盘的交互在所难免。Flutter 框架中有 KeyboardListener 和 CallbackShortcuts 组件,可以让开发者非常方便地 监听键盘事件 以及 处理组合快捷键","description":"在桌面端开发中,键盘的交互在所难免。Flutter 框架中有 KeyboardListener 和 CallbackShortcuts 组件,可以让开发者非常方便地 监听键盘事件 以及 处理组合快捷键","guid":"https://juejin.cn/post/7452337927328071743","author":"张风捷特烈","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-26T01:16:55.653Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Flutter中如何实现RN的hitSlop扩大热区功能","url":"https://juejin.cn/post/7452224395895193640","content":"前言: react-native hitSlop介绍:这一属性定义了按钮的外延范围 为了方便用户使用,公司的自研跨端框架描述语言前期对齐了react-native,最近研发的同学疯狂push框架提供对","description":"前言: react-native hitSlop介绍:这一属性定义了按钮的外延范围 为了方便用户使用,公司的自研跨端框架描述语言前期对齐了react-native,最近研发的同学疯狂push框架提供对","guid":"https://juejin.cn/post/7452224395895193640","author":"laterlater","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-25T09:55:26.208Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"程序员焦虑症之「没用过」和「不知道」,码农的「拧螺丝」之道","url":"https://juejin.cn/post/7451964967165231104","content":"许久没扯淡,今天就写点没营养的内容。 前几天和朋友聊天,其中一个话题很有意思,那就是「没用过」和「不知道」是他日常焦虑症的来源,因为他在一家传统企业做开发,技术栈一直很保守,很多框架代码可能一两年都不","description":"许久没扯淡,今天就写点没营养的内容。 前几天和朋友聊天,其中一个话题很有意思,那就是「没用过」和「不知道」是他日常焦虑症的来源,因为他在一家传统企业做开发,技术栈一直很保守,很多框架代码可能一两年都不","guid":"https://juejin.cn/post/7451964967165231104","author":"恋猫de小郭","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-25T00:13:30.433Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Flutter Plugin Desktop 示例","url":"https://juejin.cn/post/7451849205838725130","content":"Flutter Plugin Desktop 示例","description":"Flutter Plugin Desktop 示例","guid":"https://juejin.cn/post/7451849205838725130","author":"清凉夏日","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-24T07:10:03.714Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"原来Flutter背后的布局原理是这样的","url":"https://juejin.cn/post/7451575054796734479","content":"如果你是一名web开发者应该对于元素的布局不陌生,直接给目标元素定义尺寸就可以了,如css的width/height 、android的layout_width等等,但在flutter中同样的尺寸定义","description":"如果你是一名web开发者应该对于元素的布局不陌生,直接给目标元素定义尺寸就可以了,如css的width/height 、android的layout_width等等,但在flutter中同样的尺寸定义","guid":"https://juejin.cn/post/7451575054796734479","author":"大卫talk","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-24T00:02:52.111Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"这么详细的Flutter Hooks源码分析,你确定不看看?","url":"https://juejin.cn/post/7451555953450532874","content":"这么详细的Flutter Hooks源码分析,你确定不看看?","description":"这么详细的Flutter Hooks源码分析,你确定不看看?","guid":"https://juejin.cn/post/7451555953450532874","author":"siapapa123","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-23T16:13:47.949Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"📝小记:Ubuntu 部署 Jenkins 打包 Flutter APK","url":"https://juejin.cn/post/7451524631734419510","content":"📝小记:Ubuntu 部署 Jenkins 打包 Flutter APK","description":"📝小记:Ubuntu 部署 Jenkins 打包 Flutter APK","guid":"https://juejin.cn/post/7451524631734419510","author":"coder_pig","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-23T12:31:42.113Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"flutter自学笔记5- dart 编码规范","url":"https://juejin.cn/post/7451498663500644392","content":"本文仅梳理了dart.cn文档的 Dart语言编写代码时应该遵循的风格规范,包括标识符命名、Linter规则应用、库和文件命名、代码格式化等方面的内容。 简化了一些内容,方便以后复习使用","description":"本文仅梳理了dart.cn文档的 Dart语言编写代码时应该遵循的风格规范,包括标识符命名、Linter规则应用、库和文件命名、代码格式化等方面的内容。 简化了一些内容,方便以后复习使用","guid":"https://juejin.cn/post/7451498663500644392","author":"捡芝麻丢西瓜","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-23T09:24:31.196Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"聊聊 Flutter & Dart 里的内存泄漏和优化,也许没你想的那么复杂","url":"https://juejin.cn/post/7451203358395039796","content":"聊聊 Flutter & Dart 里的内存泄漏和优化,也许没你想的那么复杂","description":"聊聊 Flutter & Dart 里的内存泄漏和优化,也许没你想的那么复杂","guid":"https://juejin.cn/post/7451203358395039796","author":"恋猫de小郭","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-22T23:18:44.340Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Flutter/Dart:日志模块Logger Easier","url":"https://juejin.cn/post/7450808124247490586","content":"Flutter/Dart:日志模块Logger Easier","description":"Flutter/Dart:日志模块Logger Easier","guid":"https://juejin.cn/post/7450808124247490586","author":"jcLee95","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-21T19:34:36.224Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Flutter 使用 Pigeon 集成飞书移动应用登录(Android/iOS)","url":"https://juejin.cn/post/7450710706234884146","content":"用 Flutter 开发 Android、iOS 双端 APP 并集成飞书移动应用登录能力,实现拉起飞书 APP 并授权登录后回跳","description":"用 Flutter 开发 Android、iOS 双端 APP 并集成飞书移动应用登录能力,实现拉起飞书 APP 并授权登录后回跳","guid":"https://juejin.cn/post/7450710706234884146","author":"StringBean","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-21T10:16:02.230Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Flutter疑难杂症:安卓手机键盘焦点丢失问题解决办法","url":"https://juejin.cn/post/7450416441347588115","content":"Flutter疑难杂症:安卓手机键盘焦点丢失问题解决办法","description":"Flutter疑难杂症:安卓手机键盘焦点丢失问题解决办法","guid":"https://juejin.cn/post/7450416441347588115","author":"SoaringHeart","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-20T12:53:10.131Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Flutter应用开发:蓝牙打印","url":"https://juejin.cn/post/7450399578283376703","content":"做的是物流相关的业务,蓝牙打印属于核心环节。主要涉及蓝牙连接打印设备,构建打印模板,构建打印队列,打印配置。","description":"做的是物流相关的业务,蓝牙打印属于核心环节。主要涉及蓝牙连接打印设备,构建打印模板,构建打印队列,打印配置。","guid":"https://juejin.cn/post/7450399578283376703","author":"机器瓦力","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-20T10:39:32.588Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"dart if case 的简单实用","url":"https://juejin.cn/post/7450360978359615499","content":"1 List操作 2 对象操作1 List操作 2 对象操作1 List操作 2 对象操作1 List操作 2 对象操作1 List操作 2 对象操作","description":"1 List操作 2 对象操作1 List操作 2 对象操作1 List操作 2 对象操作1 List操作 2 对象操作1 List操作 2 对象操作","guid":"https://juejin.cn/post/7450360978359615499","author":"火柴就是我","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-20T07:49:39.107Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"flutter 学习笔记(六):持久化存储","url":"https://juejin.cn/post/7450024356874731520","content":"在 Flutter 应用中,有时我们需要将数据保存到设备的本地存储中,以便在应用重启后能够恢复这些数据。在这篇文章中,我们将介绍如何在 Flutter 中使用持久化存储 安装 shared_prefe","description":"在 Flutter 应用中,有时我们需要将数据保存到设备的本地存储中,以便在应用重启后能够恢复这些数据。在这篇文章中,我们将介绍如何在 Flutter 中使用持久化存储 安装 shared_prefe","guid":"https://juejin.cn/post/7450024356874731520","author":"张二三","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-19T10:37:59.324Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"【译】Flutter Router (Navigator 2.0)","url":"https://juejin.cn/post/7449971044549656603","content":"【译】Flutter Router (Navigator 2.0)","description":"【译】Flutter Router (Navigator 2.0)","guid":"https://juejin.cn/post/7449971044549656603","author":"wangruofeng","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-19T08:23:59.880Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Kotlin Multiplatform 的春天, klibs.io 发布,还有官方支持鸿蒙的想法","url":"https://juejin.cn/post/7449965819360411685","content":"Kotlin Multiplatform 的春天, klibs.io 发布,还有官方支持鸿蒙的想法","description":"Kotlin Multiplatform 的春天, klibs.io 发布,还有官方支持鸿蒙的想法","guid":"https://juejin.cn/post/7449965819360411685","author":"恋猫de小郭","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-19T07:44:24.938Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Android 16 Baklava 来了,来看看开发者预览版给我们带来了什么","url":"https://juejin.cn/post/7449932238583513138","content":"Android 15 估计很多人还没用上,Android 16 这就要来了,官方 2025 年将发布两个大 Android API 版本 : 而对于 Android 16 ,尽管它还是保留了以甜点为主","description":"Android 15 估计很多人还没用上,Android 16 这就要来了,官方 2025 年将发布两个大 Android API 版本 : 而对于 Android 16 ,尽管它还是保留了以甜点为主","guid":"https://juejin.cn/post/7449932238583513138","author":"恋猫de小郭","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-19T03:54:52.964Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"什么?Flutter 可能会被 SwiftUI/ArkUI 化?全新的 Flutter Roadmap","url":"https://juejin.cn/post/7449699023768600627","content":"在刚刚过去的 FlutterInProduction 活动里,Flutter 官方除了介绍「历史进程」和「用户案例」之外,也着重提及了未来相关的 roadmap ,其中就有 3.27 里的 Swift","description":"在刚刚过去的 FlutterInProduction 活动里,Flutter 官方除了介绍「历史进程」和「用户案例」之外,也着重提及了未来相关的 roadmap ,其中就有 3.27 里的 Swift","guid":"https://juejin.cn/post/7449699023768600627","author":"恋猫de小郭","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-18T14:28:24.755Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"flutter 开发笔记(五):表单","url":"https://juejin.cn/post/7449634653282271247","content":"在移动应用开发中,表单是一个非常常见的用户界面元素,它允许用户输入和提交信息。在这篇文章中,我们将介绍如何在 Flutter 中使用表单 需求 我们将从一个基本的用户登录表单开始,该表单包含电子邮件输","description":"在移动应用开发中,表单是一个非常常见的用户界面元素,它允许用户输入和提交信息。在这篇文章中,我们将介绍如何在 Flutter 中使用表单 需求 我们将从一个基本的用户登录表单开始,该表单包含电子邮件输","guid":"https://juejin.cn/post/7449634653282271247","author":"张二三","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-18T09:42:16.217Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Flutter Isolate 基本使用","url":"https://juejin.cn/post/7449639780408115254","content":"``` //创建ReceivePort var receiver = ReceivePort(); //rootIsolateToken 主要是为了在非主Isolate中调用Channel RootI","description":"``` //创建ReceivePort var receiver = ReceivePort(); //rootIsolateToken 主要是为了在非主Isolate中调用Channel RootI","guid":"https://juejin.cn/post/7449639780408115254","author":"火柴就是我","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-18T09:17:03.772Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"网页跳转App,Universal Links(iOS)和 App Links(Android) 如何设置","url":"https://juejin.cn/post/7449624180289667091","content":"要在 iOS 和 Android 上使用 Universal Links 和 App Links 进行跳转,您需要按照不同平台的具体要求进行配置。","description":"要在 iOS 和 Android 上使用 Universal Links 和 App Links 进行跳转,您需要按照不同平台的具体要求进行配置。","guid":"https://juejin.cn/post/7449624180289667091","author":"shankss","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-18T08:07:28.094Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"flutter 实现点击区域放大的一种方式","url":"https://juejin.cn/post/7449590309024596022","content":"flutter 实现点击区域放大的一种方式","description":"flutter 实现点击区域放大的一种方式","guid":"https://juejin.cn/post/7449590309024596022","author":"火柴就是我","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-18T07:36:08.406Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"鸿蒙 Flutter 实战:现有 Flutter 项目支持鸿蒙 II","url":"https://juejin.cn/post/7449563222698573864","content":"鸿蒙 Flutter 实战:现有 Flutter 项目支持鸿蒙 II","description":"鸿蒙 Flutter 实战:现有 Flutter 项目支持鸿蒙 II","guid":"https://juejin.cn/post/7449563222698573864","author":"zacksleo","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-18T05:00:26.072Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"FlutterInProduction ,2024 年末,让我们看看 Flutter 现在的生态数据","url":"https://juejin.cn/post/7449373647255535666","content":"Flutter 从立项到现在也有 10 年了,从 2014 年作为代号为 “Sky” 的 Google 实验框架开始,而 2018 年 Flutter 正式推出 1.0 版本, 到现在也有六年: 而在","description":"Flutter 从立项到现在也有 10 年了,从 2014 年作为代号为 “Sky” 的 Google 实验框架开始,而 2018 年 Flutter 正式推出 1.0 版本, 到现在也有六年: 而在","guid":"https://juejin.cn/post/7449373647255535666","author":"恋猫de小郭","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-18T01:40:49.879Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"手把手教你入门Fair之脚本篇","url":"https://juejin.cn/post/7449184399294873615","content":"本文介绍了Fair4.0提供的4个脚本的使用方法,能够帮助读者快速上手Fair,简化开发流程,降低使用成本。","description":"本文介绍了Fair4.0提供的4个脚本的使用方法,能够帮助读者快速上手Fair,简化开发流程,降低使用成本。","guid":"https://juejin.cn/post/7449184399294873615","author":"冷心市民火同学","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-17T06:10:04.788Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"卓易通:鸿蒙Next系统的蜜糖还是毒药?","url":"https://juejin.cn/post/7448842151700332555","content":"本文介绍了纯血鸿蒙版本卓易通和出境易两款应用中运行Android App的体验和运行原理。 同时分析了在这个虚拟环境下运行Android应用对于鸿蒙生态的一些看法。","description":"本文介绍了纯血鸿蒙版本卓易通和出境易两款应用中运行Android App的体验和运行原理。 同时分析了在这个虚拟环境下运行Android应用对于鸿蒙生态的一些看法。","guid":"https://juejin.cn/post/7448842151700332555","author":"程序员老刘","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-16T08:24:47.831Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"flutter 命令记录","url":"https://juejin.cn/post/7448816635886501951","content":"1 单独创建单个平台的文件夹 1 单独创建单个平台的文件夹 1 单独创建单个平台的文件夹 1 单独创建单个平台的文件夹","description":"1 单独创建单个平台的文件夹 1 单独创建单个平台的文件夹 1 单独创建单个平台的文件夹 1 单独创建单个平台的文件夹","guid":"https://juejin.cn/post/7448816635886501951","author":"火柴就是我","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-16T06:14:05.741Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"现代化Flutter架构-Riverpod应用层","url":"https://juejin.cn/post/7448645894976946195","content":"在构建复杂的应用程序时,我们可能会发现自己编写的逻辑: 依赖于多个数据源或Repository 需要被多个Widget使用(共享) 在这种情况下,很容易将逻辑放在已有的类(Widget或Reposit","description":"在构建复杂的应用程序时,我们可能会发现自己编写的逻辑: 依赖于多个数据源或Repository 需要被多个Widget使用(共享) 在这种情况下,很容易将逻辑放在已有的类(Widget或Reposit","guid":"https://juejin.cn/post/7448645894976946195","author":"xuyisheng","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-16T03:00:08.126Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"《吐血整理》高级系列教程-吃透Fiddler抓包教程(31)-Fiddler如何抓取Android系统中Flutter应用程序的包","url":"https://juejin.cn/post/7447475284718452748","content":"Flutter是谷歌的移动UI框架,可以快速在iOS和Android上构建高质量的原生用户界面。Flutter应用程序是用Dart编写的,这是一种由Google在7年多前创建的语言。","description":"Flutter是谷歌的移动UI框架,可以快速在iOS和Android上构建高质量的原生用户界面。Flutter应用程序是用Dart编写的,这是一种由Google在7年多前创建的语言。","guid":"https://juejin.cn/post/7447475284718452748","author":"北京_宏哥","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-13T01:07:16.209Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"flutter 开发笔记(四):http 请求","url":"https://juejin.cn/post/7447424970390978612","content":"flutter 开发笔记(四):http 请求","description":"flutter 开发笔记(四):http 请求","guid":"https://juejin.cn/post/7447424970390978612","author":"张二三","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-12T10:17:53.882Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Flutter 动态化 - Fair 4.0编译原理及优化","url":"https://juejin.cn/post/7447421809756127283","content":"Fair 团队在今年6月份发布了Fair 4.0。本文通过对比Fair 4.0 及之前版本的编译流程,详细介绍了Fair 4.0的编译原理及优化过程。","description":"Fair 团队在今年6月份发布了Fair 4.0。本文通过对比Fair 4.0 及之前版本的编译流程,详细介绍了Fair 4.0的编译原理及优化过程。","guid":"https://juejin.cn/post/7447421809756127283","author":"bing7720","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-12T10:08:18.813Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Flutter 动画篇 (Hero 动画)","url":"https://juejin.cn/post/7447180058127204390","content":"Hero 动画 介绍 你可能经常遇到 hero 动画。比如,页面上显示的代售商品列表。选择一件商品后,应用会跳转至包含更多细节以及“购买”按钮的新页面。在 Flutter 中,图像从当前页面转到另一个","description":"Hero 动画 介绍 你可能经常遇到 hero 动画。比如,页面上显示的代售商品列表。选择一件商品后,应用会跳转至包含更多细节以及“购买”按钮的新页面。在 Flutter 中,图像从当前页面转到另一个","guid":"https://juejin.cn/post/7447180058127204390","author":"心安事随","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-12T05:00:43.834Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Dart 3.6 发布,workspace 和 Digit separators","url":"https://juejin.cn/post/7447134640466886683","content":"workspace 之前我们就聊过 Flutter 正在切换成 Monorepo 和支持 workspaces ,Dart 3.6 开始,Pub 现在正式支持 monorepo 或 workspace","description":"workspace 之前我们就聊过 Flutter 正在切换成 Monorepo 和支持 workspaces ,Dart 3.6 开始,Pub 现在正式支持 monorepo 或 workspace","guid":"https://juejin.cn/post/7447134640466886683","author":"恋猫de小郭","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-12T02:00:09.385Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Flutter 3.27 发布啦,快来看有什么更新吧","url":"https://juejin.cn/post/7447097960011923506","content":"Flutter 3.27 悄悄的就来了,该版本包含了大量更新,包括: Cupertino 相关组件的大量优化 Material 下的一些主题和控件调整,包括之前我们聊过的 Row and Column","description":"Flutter 3.27 悄悄的就来了,该版本包含了大量更新,包括: Cupertino 相关组件的大量优化 Material 下的一些主题和控件调整,包括之前我们聊过的 Row and Column","guid":"https://juejin.cn/post/7447097960011923506","author":"恋猫de小郭","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-12T00:59:36.175Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Flutter App 官方架构指南","url":"https://juejin.cn/post/7447055521327317030","content":"本指南概述了构建 Flutter 应用的架构最佳实践,重点包括职责分离、单向数据流、MVVM 模式、不可变数据模型、依赖注入等核心原则。","description":"本指南概述了构建 Flutter 应用的架构最佳实践,重点包括职责分离、单向数据流、MVVM 模式、不可变数据模型、依赖注入等核心原则。","guid":"https://juejin.cn/post/7447055521327317030","author":"hamber","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-11T12:14:55.660Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"flutter 开发笔记(三):JSON","url":"https://juejin.cn/post/7447049768679637026","content":"flutter 开发笔记(三):JSON","description":"flutter 开发笔记(三):JSON","guid":"https://juejin.cn/post/7447049768679637026","author":"张二三","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-11T10:16:18.944Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"flutter getx 路由侧滑返回 PopScope 拦截失效的探究与解决","url":"https://juejin.cn/post/7446998671886286900","content":"flutter getx 路由侧滑返回 PopScope 拦截失效的探究与解决","description":"flutter getx 路由侧滑返回 PopScope 拦截失效的探究与解决","guid":"https://juejin.cn/post/7446998671886286900","author":"佚名啊","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-11T08:52:40.124Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"flutter 开发笔记(二):路由","url":"https://juejin.cn/post/7446996523870289954","content":"路由是一个应用程序中不可或缺的功能,它负责管理页面的导航和切换。本文将简要介绍 Flutter 的路由功能,并提供一个可复现的示例代码,能更好地理解和使用路由 简介 首先介绍一个概念,应用构建器(ap","description":"路由是一个应用程序中不可或缺的功能,它负责管理页面的导航和切换。本文将简要介绍 Flutter 的路由功能,并提供一个可复现的示例代码,能更好地理解和使用路由 简介 首先介绍一个概念,应用构建器(ap","guid":"https://juejin.cn/post/7446996523870289954","author":"张二三","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-11T08:38:57.539Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"现代化Flutter架构-Riverpod表现层","url":"https://juejin.cn/post/7446964447463981094","content":"在编写 Flutter 应用程序时,将业务逻辑与 UI 代码分离是非常重要的。 这将使我们的代码更易于测试和推理,当我们的应用程序变得越来越复杂时,这一点尤为重要。 为了实现这一点,我们可以使用设计模","description":"在编写 Flutter 应用程序时,将业务逻辑与 UI 代码分离是非常重要的。 这将使我们的代码更易于测试和推理,当我们的应用程序变得越来越复杂时,这一点尤为重要。 为了实现这一点,我们可以使用设计模","guid":"https://juejin.cn/post/7446964447463981094","author":"xuyisheng","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-11T06:06:38.354Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Flutter 环境搭建、常用指令、开发细节","url":"https://juejin.cn/post/7446774405143986226","content":"Flutter 环境搭建、常用指令、开发细节","description":"Flutter 环境搭建、常用指令、开发细节","guid":"https://juejin.cn/post/7446774405143986226","author":"卡尔特斯","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-11T03:25:50.420Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"跟🤡杰哥一起学Flutter (三十、🖌玩转自定义绘制三部曲[中])","url":"https://juejin.cn/post/7446634545867046922","content":"🐶 断断续续写了两周,水技术文章这么久以来耗时最多的一篇,毕竟 用(li)料(zi)扎(chao)实(duo) ,算是用 Flutter自定义绘制 完成了当年没怎么写 Android自定义View","description":"🐶 断断续续写了两周,水技术文章这么久以来耗时最多的一篇,毕竟 用(li)料(zi)扎(chao)实(duo) ,算是用 Flutter自定义绘制 完成了当年没怎么写 Android自定义View","guid":"https://juejin.cn/post/7446634545867046922","author":"coder_pig","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-10T09:48:07.362Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"震惊!!Flutter竟然可以完美复刻微信媒体操作","url":"https://juejin.cn/post/7446651689430237218","content":"在移动应用开发中,文件上传和预览功能是几乎所有社交应用都需要具备的重要功能,尤其是在涉及图片和视频上传时。用户对于这些功能的期望,通常要求界面流畅、操作简便。而在众多社交软件中,微信无疑是最受欢迎的","description":"在移动应用开发中,文件上传和预览功能是几乎所有社交应用都需要具备的重要功能,尤其是在涉及图片和视频上传时。用户对于这些功能的期望,通常要求界面流畅、操作简便。而在众多社交软件中,微信无疑是最受欢迎的","guid":"https://juejin.cn/post/7446651689430237218","author":"程序张","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-10T09:35:22.168Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"🚀 Flutter动画大爆炸:打造炫酷动画的秘籍与案例","url":"https://juejin.cn/post/7446600698681212938","content":"在Flutter的世界里,动画不仅仅是装饰,它们是用户体验的灵魂。本文将带你穿梭于Flutter动画的奇妙世界,探索如何制作一系列令人惊叹的动画效果。","description":"在Flutter的世界里,动画不仅仅是装饰,它们是用户体验的灵魂。本文将带你穿梭于Flutter动画的奇妙世界,探索如何制作一系列令人惊叹的动画效果。","guid":"https://juejin.cn/post/7446600698681212938","author":"Hans_April","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-10T07:42:10.142Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Flutter Web 正式移除 HTML renderer,只支持 CanvasKit 和 SkWasm","url":"https://juejin.cn/post/7446613741627736091","content":"关于这个话题其实聊过很多次,在 2023 年初的时候,谷歌就提出了调整 Flutter Web 路线的方向,而今年年初的时候,Flutter 官方也正式官宣弃用 HTML renderer 的计划,而","description":"关于这个话题其实聊过很多次,在 2023 年初的时候,谷歌就提出了调整 Flutter Web 路线的方向,而今年年初的时候,Flutter 官方也正式官宣弃用 HTML renderer 的计划,而","guid":"https://juejin.cn/post/7446613741627736091","author":"恋猫de小郭","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-10T07:22:11.820Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"(开源)MicroDart:一种Dart代码解释器及在 Flutter动态化中的应用","url":"https://juejin.cn/post/7446402625716863002","content":"(开源)MicroDart:一种Dart代码解释器及在 Flutter动态化中的应用","description":"(开源)MicroDart:一种Dart代码解释器及在 Flutter动态化中的应用","guid":"https://juejin.cn/post/7446402625716863002","author":"孤鸿玉","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-10T03:18:57.152Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"现代化Flutter架构-Riverpod领域层","url":"https://juejin.cn/post/7446240175641182262","content":"你是否曾将用户界面、业务逻辑和网络代码混杂在一起,成为一捆乱七八糟的意大利面代码? 我知道我曾这样做过。 ✋ 毕竟,真实世界的应用程序开发是很困难的。 领域驱动设计(DDD)等书籍就是为了帮助我们开发","description":"你是否曾将用户界面、业务逻辑和网络代码混杂在一起,成为一捆乱七八糟的意大利面代码? 我知道我曾这样做过。 ✋ 毕竟,真实世界的应用程序开发是很困难的。 领域驱动设计(DDD)等书籍就是为了帮助我们开发","guid":"https://juejin.cn/post/7446240175641182262","author":"xuyisheng","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-09T09:39:52.844Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"glsl 效果一","url":"https://juejin.cn/post/7446285461242298409","content":"``` // original shader https://www.shadertoy.com/view/fsGXWG precision highp float; #include uniform","description":"``` // original shader https://www.shadertoy.com/view/fsGXWG precision highp float; #include uniform","guid":"https://juejin.cn/post/7446285461242298409","author":"火柴就是我","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-09T09:28:36.238Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"手把手教你如何使用Fair 4.0","url":"https://juejin.cn/post/7446116873710993460","content":"手把手教你如何使用Fair 4.0","description":"手把手教你如何使用Fair 4.0","guid":"https://juejin.cn/post/7446116873710993460","author":"Blues9611","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-09T06:36:19.571Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"GetX框架里容易被忽略的那些小知识(四)","url":"https://juejin.cn/post/7446011856039378982","content":"GetX框架里容易被忽略的那些小知识(四)","description":"GetX框架里容易被忽略的那些小知识(四)","guid":"https://juejin.cn/post/7446011856039378982","author":"一名普通的程序员","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-09T02:08:58.093Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Flutter进阶:基于 MLKit 的 自动翻译功能实现","url":"https://juejin.cn/post/7445500872187871268","content":"一、需求来源 无意中发现了很棒的翻译(支持英转中的)第三方库 google_mlkit_translation ,分享给大家。 二、使用示例 三、使用指南 google_mlkit_translati","description":"一、需求来源 无意中发现了很棒的翻译(支持英转中的)第三方库 google_mlkit_translation ,分享给大家。 二、使用示例 三、使用指南 google_mlkit_translati","guid":"https://juejin.cn/post/7445500872187871268","author":"SoaringHeart","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-07T05:09:54.063Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Flutter/Dart中Mixin的深入理解与原理解析","url":"https://juejin.cn/post/7445492949599338496","content":"什么是Mixin? Mixin是Dart中一种代码复用的机制,它允许在不使用继承的情况下,将一个类的代码复用到多个类层次结构中。与传统的继承相比,Mixin提供了更灵活的代码复用方式。 Mixin的基","description":"什么是Mixin? Mixin是Dart中一种代码复用的机制,它允许在不使用继承的情况下,将一个类的代码复用到多个类层次结构中。与传统的继承相比,Mixin提供了更灵活的代码复用方式。 Mixin的基","guid":"https://juejin.cn/post/7445492949599338496","author":"parcool","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-07T03:20:41.392Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"一件Flutter升级到最新版本所引发的血案","url":"https://juejin.cn/post/7445465796300324905","content":"由于第三方依赖库的较新问题,我们务必把flutter升级到最新版本,也就是3.24.5,否则无法引入第三方库 升级后debug开发调试还是非常松弛和happy的,直到我给产品打正式包时。。。。 编译崩","description":"由于第三方依赖库的较新问题,我们务必把flutter升级到最新版本,也就是3.24.5,否则无法引入第三方库 升级后debug开发调试还是非常松弛和happy的,直到我给产品打正式包时。。。。 编译崩","guid":"https://juejin.cn/post/7445465796300324905","author":"小虎牙007","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-07T03:12:40.142Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"flutter 绘制渐变圆环","url":"https://juejin.cn/post/7445194859348721705","content":"``` canvas.save(); canvas.translate(width / 2, height / 2); canvas.rotate( -pi / 2); canvas.drawArc(","description":"``` canvas.save(); canvas.translate(width / 2, height / 2); canvas.rotate( -pi / 2); canvas.drawArc(","guid":"https://juejin.cn/post/7445194859348721705","author":"火柴就是我","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-06T10:42:52.878Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"flutter Canvas 的一些操作 加深理解","url":"https://juejin.cn/post/7445184021603024935","content":"1 canvas的scale操作。 在画布放大之后,所有绘制的坐标值都会放大。比如绘制的圆心为(10,10),最后在restore之后显示的位置是(20,20) 2 canvas的rotate, tr","description":"1 canvas的scale操作。 在画布放大之后,所有绘制的坐标值都会放大。比如绘制的圆心为(10,10),最后在restore之后显示的位置是(20,20) 2 canvas的rotate, tr","guid":"https://juejin.cn/post/7445184021603024935","author":"火柴就是我","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-06T10:35:25.098Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"flutter 实现word-break 效果","url":"https://juejin.cn/post/7445150409096003638","content":"``` extension TextOverflowUtil on String { /// 将flutter系统默认的单词截断模式转换成字符截断模式 /// 通过向文本中插入宽度为0的空格实现 St","description":"``` extension TextOverflowUtil on String { /// 将flutter系统默认的单词截断模式转换成字符截断模式 /// 通过向文本中插入宽度为0的空格实现 St","guid":"https://juejin.cn/post/7445150409096003638","author":"火柴就是我","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-06T08:01:06.614Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Android API Server 开源分享","url":"https://juejin.cn/post/7445095166749425690","content":"Android API Server 开源分享","description":"Android API Server 开源分享","guid":"https://juejin.cn/post/7445095166749425690","author":"梦魇兽","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-06T06:23:35.338Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Flutter 组件集录 | 隐式动画组件 * 4","url":"https://juejin.cn/post/7444922158285275175","content":"最近盘点 Flutter 中的内置组件,发现隐式动画(ImplicitlyAnimated) 中没有收录的四个组件,可能是后期版本增加的。本文介绍一下它们的使用,并加入到 FlutterUnit 中。","description":"最近盘点 Flutter 中的内置组件,发现隐式动画(ImplicitlyAnimated) 中没有收录的四个组件,可能是后期版本增加的。本文介绍一下它们的使用,并加入到 FlutterUnit 中。","guid":"https://juejin.cn/post/7444922158285275175","author":"张风捷特烈","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-06T01:43:56.988Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"记录一个Flutter 3.24单元测试点击事件bug","url":"https://juejin.cn/post/7444733608139685897","content":"记录一个Flutter 3.24单元测试点击事件bug","description":"记录一个Flutter 3.24单元测试点击事件bug","guid":"https://juejin.cn/post/7444733608139685897","author":"程序员老刘","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-05T08:19:03.347Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"小谈Flutter中的文件管理","url":"https://juejin.cn/post/7444566019122905098","content":"小谈Flutter中的文件管理","description":"小谈Flutter中的文件管理","guid":"https://juejin.cn/post/7444566019122905098","author":"Hans_April","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-05T03:09:53.562Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"GetX框架里容易被忽略的那些小知识(三)","url":"https://juejin.cn/post/7444515626633003034","content":"GetStorage 成为小型数据存储的优秀选择,与 GetX 框架的其他模块无缝集成,为使用者带来了极大的便利。","description":"GetStorage 成为小型数据存储的优秀选择,与 GetX 框架的其他模块无缝集成,为使用者带来了极大的便利。","guid":"https://juejin.cn/post/7444515626633003034","author":"一名普通的程序员","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-05T01:32:20.897Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Flutter表格SfDataGrid进阶使用","url":"https://juejin.cn/post/7444475403605999616","content":"Flutter表格SfDataGrid进阶使用","description":"Flutter表格SfDataGrid进阶使用","guid":"https://juejin.cn/post/7444475403605999616","author":"菜牙买菜","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-04T15:16:05.596Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"鸿蒙 Next 可兼容运行 Android App,还支持出海 GMS?","url":"https://juejin.cn/post/7444454304973635595","content":"最近 「出境易」和 「卓易通」 应该算是鸿蒙和 Android 开发圈“突如其来”的热门话题,而 「出境易」可能更高频一些,主要也是 Next 5.0 被大家发现刚上架了一个名为「出境易」的应用,通过","description":"最近 「出境易」和 「卓易通」 应该算是鸿蒙和 Android 开发圈“突如其来”的热门话题,而 「出境易」可能更高频一些,主要也是 Next 5.0 被大家发现刚上架了一个名为「出境易」的应用,通过","guid":"https://juejin.cn/post/7444454304973635595","author":"恋猫de小郭","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-04T09:24:40.779Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Get lazyPut 与 put 的区别","url":"https://juejin.cn/post/7444382893471039538","content":"Get lazyPut 与 put 的区别","description":"Get lazyPut 与 put 的区别","guid":"https://juejin.cn/post/7444382893471039538","author":"火柴就是我","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-04T06:40:10.080Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Flutter 中 视频封面 视频的压缩 上传 播放","url":"https://juejin.cn/post/7444110366675681318","content":"Flutter 中 视频封面 视频的压缩 上传 播放","description":"Flutter 中 视频封面 视频的压缩 上传 播放","guid":"https://juejin.cn/post/7444110366675681318","author":"心安事随","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-03T14:28:02.278Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Flutter解压文件并解析数据","url":"https://juejin.cn/post/7444008868445126706","content":"Flutter解压文件并解析数据","description":"Flutter解压文件并解析数据","guid":"https://juejin.cn/post/7444008868445126706","author":"我码玄黄","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-03T08:22:20.274Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"史上最强大的文本溢出效果,强得飞起 !","url":"https://juejin.cn/post/7443784332164775971","content":"史上最强大的文本溢出效果,强得飞起 ! 一直没人模仿, 从未被超越!能超越的文本组件的只有糖果的 ExtendedText!","description":"史上最强大的文本溢出效果,强得飞起 ! 一直没人模仿, 从未被超越!能超越的文本组件的只有糖果的 ExtendedText!","guid":"https://juejin.cn/post/7443784332164775971","author":"法的空间","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-02T23:24:11.055Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"现代化Flutter架构-Riverpod数据层","url":"https://juejin.cn/post/7443576983705829388","content":"设计模式是帮助我们解决软件设计中常见问题的有用模板。 说到应用程序架构,结构设计模式可以帮助我们决定如何组织应用程序的不同部分。 在这种情况下,我们可以使用Repository模式从各种来源(如后端 ","description":"设计模式是帮助我们解决软件设计中常见问题的有用模板。 说到应用程序架构,结构设计模式可以帮助我们决定如何组织应用程序的不同部分。 在这种情况下,我们可以使用Repository模式从各种来源(如后端","guid":"https://juejin.cn/post/7443576983705829388","author":"xuyisheng","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-02T02:05:01.317Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"【Flutter】Riverpod中普通Provider与特殊Provider的分类、用法与使用场景","url":"https://juejin.cn/post/7443576983705108492","content":"【Flutter】Riverpod中普通Provider与特殊Provider的分类、用法与使用场景","description":"【Flutter】Riverpod中普通Provider与特殊Provider的分类、用法与使用场景","guid":"https://juejin.cn/post/7443576983705108492","author":"Newki","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-02T00:57:20.708Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"【动画图解】这个方法,把 Flutter 遍历树变动的高效性体现得淋漓尽致","url":"https://juejin.cn/post/7443104727075176500","content":"前面的文章我们为了简化问题,一直都只局限于讨论单个节点的更新。但实际上,在 Flutter 中,多个节点的差异化更新更具挑战性,也更能体现 Flutter 遍历树变动的机制的高效性。 我们已经知道,当","description":"前面的文章我们为了简化问题,一直都只局限于讨论单个节点的更新。但实际上,在 Flutter 中,多个节点的差异化更新更具挑战性,也更能体现 Flutter 遍历树变动的机制的高效性。 我们已经知道,当","guid":"https://juejin.cn/post/7443104727075176500","author":"星际码仔","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-12-01T11:45:06.854Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"用最简单的方式说清楚flutter中get框架里update()方法为什么能刷新UI?","url":"https://juejin.cn/post/7442953704418230310","content":"用最简单的方式说清楚flutter中get框架里update()方法为什么能刷新UI?","description":"用最简单的方式说清楚flutter中get框架里update()方法为什么能刷新UI?","guid":"https://juejin.cn/post/7442953704418230310","author":"parcool","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-11-30T10:12:10.998Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Futter for iOS 开发者线程和异步","url":"https://juejin.cn/post/7442606223956754495","content":"Futter for iOS 开发者线程和异步","description":"Futter for iOS 开发者线程和异步","guid":"https://juejin.cn/post/7442606223956754495","author":"用户9855130974287","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-11-29T10:16:50.538Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"什么?!居然用200行代码就实现了flutter放大镜功能!","url":"https://juejin.cn/post/7442584156494774308","content":"什么?!居然用200行代码就实现了flutter放大镜功能!","description":"什么?!居然用200行代码就实现了flutter放大镜功能!","guid":"https://juejin.cn/post/7442584156494774308","author":"7_bit","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-11-29T08:54:10.352Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"【Flutter】AutoRoute的子路由如何使用?你可能不了解的那些配合子路由的控件","url":"https://juejin.cn/post/7442532787130007588","content":"【Flutter】AutoRoute的子路由如何使用?你可能不了解的那些配合子路由的控件","description":"【Flutter】AutoRoute的子路由如何使用?你可能不了解的那些配合子路由的控件","guid":"https://juejin.cn/post/7442532787130007588","author":"Newki","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-11-29T06:53:01.316Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Flutter 组件集录 | CupertinoCheckbox 复选框","url":"https://juejin.cn/post/7442523453321740323","content":"Flutter 中的 Checkbox 属于 Material 风格,这对于桌面端很不友好。为此 Flutter 中增加了 CupertinoCheckbox 组件,它是一个 macOS 方格的复选框","description":"Flutter 中的 Checkbox 属于 Material 风格,这对于桌面端很不友好。为此 Flutter 中增加了 CupertinoCheckbox 组件,它是一个 macOS 方格的复选框","guid":"https://juejin.cn/post/7442523453321740323","author":"张风捷特烈","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-11-29T04:46:04.415Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"flame_forge2d BodyComponent 使用","url":"https://juejin.cn/post/7442394610558074934","content":"BodyComponent 的使用一般实现两个方法 第一个方法 createBody 创建物体在二维世界的一些属性 restitution 回弹系数。用于控制碰撞后的反弹效果。恢复系数的值在 0 到 ","description":"BodyComponent 的使用一般实现两个方法 第一个方法 createBody 创建物体在二维世界的一些属性 restitution 回弹系数。用于控制碰撞后的反弹效果。恢复系数的值在 0 到","guid":"https://juejin.cn/post/7442394610558074934","author":"火柴就是我","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-11-29T03:27:09.519Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"flame_forge2d BodyType 记录","url":"https://juejin.cn/post/7442495430322536487","content":"刚体类型 说明 Static 静态刚体,零质量,零速度,即不会受到重力或速度影响,但是可以设置他的位置来进行移动。该类型通常用于制作场景 Dynamic 动态刚体,有质量,可以设置速度,会受到重力影响","description":"刚体类型 说明 Static 静态刚体,零质量,零速度,即不会受到重力或速度影响,但是可以设置他的位置来进行移动。该类型通常用于制作场景 Dynamic 动态刚体,有质量,可以设置速度,会受到重力影响","guid":"https://juejin.cn/post/7442495430322536487","author":"火柴就是我","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-11-29T03:13:23.698Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"dart 记录 padLeft 与 padRight","url":"https://juejin.cn/post/7442434008128307212","content":"dart 记录 padLeft 与 padRight","description":"dart 记录 padLeft 与 padRight","guid":"https://juejin.cn/post/7442434008128307212","author":"火柴就是我","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-11-29T02:46:33.373Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"flutter 简化处理RichText","url":"https://juejin.cn/post/7442379154442059839","content":"flutter 简化处理RichText","description":"flutter 简化处理RichText","guid":"https://juejin.cn/post/7442379154442059839","author":"火柴就是我","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-11-29T02:41:12.631Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"辕门射戟——基本的滚动视图优化","url":"https://juejin.cn/post/7442264305812389899","content":"使用 ListView.builder 导致高度计算困难。手动设定高度需要处理滚动冲突。 ListView.build 又会导致渲染效率显著降低。","description":"使用 ListView.builder 导致高度计算困难。手动设定高度需要处理滚动冲突。 ListView.build 又会导致渲染效率显著降低。","guid":"https://juejin.cn/post/7442264305812389899","author":"人形打码机","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-11-28T11:54:54.300Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"非常优雅简单的isolate,一行代码轻松实现Isolate复用与异步任务执行","url":"https://juejin.cn/post/7441987735735517194","content":"Dart 本身也支持多线程编程,Isolate 作为一种类似线程的概念,提供了多任务并行的能力,但其使用相对复杂,且创建和销毁 Isolate 的过程较为繁重,对性能也会造成一定的负担。","description":"Dart 本身也支持多线程编程,Isolate 作为一种类似线程的概念,提供了多任务并行的能力,但其使用相对复杂,且创建和销毁 Isolate 的过程较为繁重,对性能也会造成一定的负担。","guid":"https://juejin.cn/post/7441987735735517194","author":"小小毛毛虫","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-11-28T03:16:37.255Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Dart中mixin为什么能解决钻石问题?","url":"https://juejin.cn/post/7441974406301351988","content":"Dart中mixin为什么能解决钻石问题?","description":"Dart中mixin为什么能解决钻石问题?","guid":"https://juejin.cn/post/7441974406301351988","author":"努力奋斗的iOSer","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-11-28T02:51:32.574Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Android 桌面窗口新功能推进,聊一聊 Android 桌面化的未来","url":"https://juejin.cn/post/7441865024613646345","content":"Android 桌面化支持可以说是 Android 15 里被多次提及的 new features,例如在 Android 15 QPR1 Beta 2 里就提到为 Pixel 平板引入了桌面窗口支持","description":"Android 桌面化支持可以说是 Android 15 里被多次提及的 new features,例如在 Android 15 QPR1 Beta 2 里就提到为 Pixel 平板引入了桌面窗口支持","guid":"https://juejin.cn/post/7441865024613646345","author":"恋猫de小郭","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-11-27T09:48:24.525Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"【Flutter】新手向,多主题与国际化/本地化的配置与使用","url":"https://juejin.cn/post/7441848779521884171","content":"在如今的移动应用开发中,用户体验的优劣直接影响着应用的受欢迎程度和用户留存率。而主题和国际化是提升用户体验的重要方面。多主题支持使得用户能够根据自己的喜好选择合适的界面风格,而国际化则确保了应用能够满","description":"在如今的移动应用开发中,用户体验的优劣直接影响着应用的受欢迎程度和用户留存率。而主题和国际化是提升用户体验的重要方面。多主题支持使得用户能够根据自己的喜好选择合适的界面风格,而国际化则确保了应用能够满","guid":"https://juejin.cn/post/7441848779521884171","author":"Newki","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-11-27T09:20:00.388Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Dart 脚本:一键整合,国际化语种翻译文件(.arb) 转 Excel","url":"https://juejin.cn/post/7441608252038381620","content":"Dart 脚本:一键整合,国际化语种翻译文件(.arb) 转 Excel","description":"Dart 脚本:一键整合,国际化语种翻译文件(.arb) 转 Excel","guid":"https://juejin.cn/post/7441608252038381620","author":"李小轰_Rex","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-11-27T02:11:24.660Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Flutter 组件集录 | 组件案例是怎么展示的?","url":"https://juejin.cn/post/7441592985875316771","content":"本文将从 FlutterUnit 如何展示一个组件信息为线索,展开介绍 FlutterUnit 对组件管理的处理方式。 - - 1. 案例组件模块 FlutterUnit 按照需求划分功能模块,其中 ","description":"本文将从 FlutterUnit 如何展示一个组件信息为线索,展开介绍 FlutterUnit 对组件管理的处理方式。 - - 1. 案例组件模块 FlutterUnit 按照需求划分功能模块,其中","guid":"https://juejin.cn/post/7441592985875316771","author":"张风捷特烈","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-11-27T00:57:19.036Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"我在成都教人用Flutter写TDD(上)——为啥要搞TDD?","url":"https://juejin.cn/post/7441479625971318835","content":"我在成都教人用Flutter写TDD(上)——为啥要搞TDD?","description":"我在成都教人用Flutter写TDD(上)——为啥要搞TDD?","guid":"https://juejin.cn/post/7441479625971318835","author":"程序员老刘","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-11-26T09:19:32.988Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Flutter求职、面试20+面试官总结:Flutter篇","url":"https://juejin.cn/post/7441457433257541667","content":"Flutter性能优化实践 Flutter作为一款高性能、高质量的移动开发框架,在开发过程中仍然需要进行一些性能优化,以确保应用的流畅性和响应速度。以下是一些关键的Flutter性能优化策略: 1、","description":"Flutter性能优化实践 Flutter作为一款高性能、高质量的移动开发框架,在开发过程中仍然需要进行一些性能优化,以确保应用的流畅性和响应速度。以下是一些关键的Flutter性能优化策略: 1、","guid":"https://juejin.cn/post/7441457433257541667","author":"SunshineBrother","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-11-26T07:43:41.356Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Flutter求职、面试20+面试官总结:Dart篇","url":"https://juejin.cn/post/7441435383688232994","content":"Flutter求职、面试20+面试官总结:Dart篇","description":"Flutter求职、面试20+面试官总结:Dart篇","guid":"https://juejin.cn/post/7441435383688232994","author":"SunshineBrother","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-11-26T07:16:45.632Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Flutter Web 与原生交互 相关","url":"https://juejin.cn/post/7441377202589220899","content":"在 Flutter 中编写的 Dart 代码,经过 Web 编译后,本质上会被转译成 JavaScript。为了实现与 Android 原生的交互,可以通过 Flutter Dart 代码调用 Jav","description":"在 Flutter 中编写的 Dart 代码,经过 Web 编译后,本质上会被转译成 JavaScript。为了实现与 Android 原生的交互,可以通过 Flutter Dart 代码调用 Jav","guid":"https://juejin.cn/post/7441377202589220899","author":"如此风景","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-11-26T03:07:57.608Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"一文聊聊Flutter多业务混合工程实践","url":"https://juejin.cn/post/7441137135535980578","content":"本篇主要介绍Flutter多业务模块在既有的项目中是如何落地的,这个过程中我们踩过哪些坑,后续是如何推进优化的。","description":"本篇主要介绍Flutter多业务模块在既有的项目中是如何落地的,这个过程中我们踩过哪些坑,后续是如何推进优化的。","guid":"https://juejin.cn/post/7441137135535980578","author":"半山居士","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-11-25T12:46:00.994Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Flutter 终于正式规划 IDE Widget 预览支持,基础技术架构公布","url":"https://juejin.cn/post/7441006286765064218","content":"2024 了, Flutter 终于\\"醒悟\\",开始规划 Widget Previews#159342 ,在 Jetpack Compose 和 SwiftUI 都支持 IDE Preview 的情况下","description":"2024 了, Flutter 终于\\"醒悟\\",开始规划 Widget Previews#159342 ,在 Jetpack Compose 和 SwiftUI 都支持 IDE Preview 的情况下","guid":"https://juejin.cn/post/7441006286765064218","author":"恋猫de小郭","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-11-25T03:47:49.761Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"Flutter - 子部件任意位置观察滚动数据","url":"https://juejin.cn/post/7440718747491647525","content":"在 scrollview_observer 的 1.23.0 中,新增了允许对观察结果进行监听的功能,也就是说你可以不需要在固定的观察结果回调(如:onObserve、onObserveAll)","description":"在 scrollview_observer 的 1.23.0 中,新增了允许对观察结果进行监听的功能,也就是说你可以不需要在固定的观察结果回调(如:onObserve、onObserveAll)","guid":"https://juejin.cn/post/7440718747491647525","author":"LinXunFeng","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-11-24T11:37:49.520Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"FlutterBasic - 私有化组件仓库unpub最完整指南 - Jinkey原创","url":"https://juejin.cn/post/7440501970463342655","content":"买一台阿里云服务器 Centos7 2核2G 40G硬盘 香港 安装 Docker 和 docker-compose 参考 安装指南 克隆仓库拉取容器镜像 访问域名 浏览器访问:http://你的IP","description":"买一台阿里云服务器 Centos7 2核2G 40G硬盘 香港 安装 Docker 和 docker-compose 参考 安装指南 克隆仓库拉取容器镜像 访问域名 浏览器访问:http://你的IP","guid":"https://juejin.cn/post/7440501970463342655","author":"Jinkey","authorUrl":null,"authorAvatar":null,"publishedAt":"2024-11-24T06:14:49.360Z","media":null,"categories":null,"attachments":null,"extra":null,"language":null},{"title":"FlutterBasic - GetBuilder、Obx、GetX