摘要:意味著屬性必須在構(gòu)造函數(shù)中就被初始化完成,不接受提前定義,也不接受更改。所以,在生命周期中動態(tài)的改變對象的屬性是不可能的,必須使用框架的方法來為構(gòu)造函數(shù)動態(tài)指定參數(shù),從而達(dá)到改變組件屬性的功能。
本文適合使用Flutter開發(fā)過一段時間的開發(fā)者閱讀,旨在分享一種避免Flutter的UI代碼嵌套太深問題的方法。如果對本文內(nèi)容或觀點(diǎn)有相關(guān)疑問,歡迎在評論中指出。
優(yōu)化效果(縮略圖):
距離我接觸Flutter已經(jīng)過去了九個月,在Flutter代碼編寫的過程中,很多開發(fā)者都遇到了“回調(diào)地獄”的問題。在Flutter中,稱之為回調(diào)并不準(zhǔn)確,準(zhǔn)確的說,是因?yàn)楸姸?b>Widget互相嵌套在一起,導(dǎo)致反括號部分堆積嚴(yán)重,極度影響代碼可讀性。
本文將介紹一種代碼編寫風(fēng)格,最大限度減少嵌套對代碼閱讀的影響。
初步介紹我們先來簡單看一下,Flutter的UI代碼:
使用build方法Flutter的Widget使用build方法來創(chuàng)建UI組件,然后通過注入child屬性的方式為組件添加子組件,子組件可以繼續(xù)包含child,通過調(diào)用每一個child的build方法,就形成了類似DOM結(jié)構(gòu)的組件樹,然后由渲染引擎渲染圖形。
一個常見的定義組件的例子如下:
class DeleteText extends StatelessWidget { // 我們在build方法中渲染自定義Widget @override Widget build(BuildContext context) { return Text("Delete"); } }組件屬性必須為final
要在Flutter中定義(繼承)一個Widget,則它的屬性必須都是final的。final意味著屬性必須在構(gòu)造函數(shù)中就被初始化完成,不接受提前定義,也不接受更改。所以,在生命周期中動態(tài)的改變Widget對象的屬性是不可能的,必須使用框架的build方法來為構(gòu)造函數(shù)動態(tài)指定參數(shù),從而達(dá)到改變組件屬性的功能。
class Avatar extends StatelessWidget { // 如果url屬性不是final的,編譯器會報出警告 final String url; // 這個構(gòu)造方法很長,但是主要你寫了final屬性,VSCode就會幫我們自動生成 const Avatar({Key key, this.url}) : super(key: key); @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), ), child: Image.network(url), ); } }
Tips:自動創(chuàng)建構(gòu)造方法,只要是構(gòu)造方法沒有的final屬性,點(diǎn)擊“快速修復(fù)”,就可以自動生成構(gòu)造方法。Flutter語法與HTML/CSS
嵌套正是DOM樹的特點(diǎn),正如HTML其實(shí)也會無限嵌套一樣(大多數(shù)前端可能看HTML看習(xí)慣了,都忘了HTML其實(shí)也經(jīng)常會寫成嵌套很深的形式),Flutter的UI代碼嵌套本質(zhì)是不可避免的,這正是Flutter UI代碼的編寫特點(diǎn)——一次成型,而不是通過addView之類的方法來手動管理每一個視圖的生命周期。在此基礎(chǔ)上,Flutter可以高效的反復(fù)重建Widget,在渲染效率上展現(xiàn)出了非常大的優(yōu)勢。
嵌套代碼難以閱讀
當(dāng)我們評判一串代碼的時候,一個顯而易見的點(diǎn),就是代碼距離左邊的距離,如果一行代碼距離左邊達(dá)到了十多個tab,可想而知它被嵌套在了多么深的位置。
來看看這個Widget,這個Widget很簡單,左邊有一個正文和一個附屬文本,附屬文本在正文下方,右邊有一組按鈕,代表這一行的操作,我們再給他嵌套一個動畫的漸現(xiàn)效果,處理好字體。那么他的代碼應(yīng)該如下所示:
// 一個簡單的嵌套的情況 class ActionRow extends StatelessWidget { @override Widget build(BuildContext context) { return AnimatedOpacity( opacity: 1, duration: Duration(milliseconds: 800), child: Container( color: Colors.white, margin: EdgeInsets.symmetric(vertical: 1), padding: EdgeInsets.symmetric(horizontal: 20), child: Row( children:[ Expanded( child: Container( padding: EdgeInsets.fromLTRB(6, 8, 8, 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ /* 超級長的左邊距 */Text( "Title", style: TextStyle(fontSize: 16), ), Container( padding: EdgeInsets.only(top: 4), child: Text( "Desc", style: TextStyle(fontSize: 12), ), ), ], ), ), ), Row( children: [ Container( padding: EdgeInsets.fromLTRB(6, 8, 8, 8), child: MaterialButton( color: Colors.orange, child: Text("Edit"), /* 超級長的左邊距 */onPressed: () { print("Handle Edit"); }, ), ), Container( padding: EdgeInsets.fromLTRB(6, 8, 8, 8), child: MaterialButton( color: Colors.red, child: Text("Delete"), onPressed: () { print("Handle Delete"); },// 往下數(shù),足足11個反括號 ), ), ], ) ], ), ), ); } }
此種代碼,只要是開發(fā)過Flutter的開發(fā)者一定不會陌生,它可以完美運(yùn)行,但是十分難以閱讀。反括號的數(shù)量經(jīng)常會達(dá)到一個更夸張的級別,導(dǎo)致部分內(nèi)容被頂?shù)竭^于右邊,在閱讀時造成了非常大的困難。
就讓我們以這串代碼為例子,來優(yōu)化他的嵌套,使其可以輕松的從上到下閱讀。
解決方法 不寫newDart2已經(jīng)可以完全不寫new了,但有的開發(fā)者還在寫new。去掉new之后,代碼會變得更加干凈。
定義變量以減少反括號在這里,我們可以抽取部分嵌套很深的Widget,將其定義成變量,從而減少它與左邊的距離。
讀一下代碼,我們很容易就能發(fā)現(xiàn),左邊的Expanded部分中,兩個文字的相關(guān)代碼距離左邊太遠(yuǎn)了,我們將他們抽出來作為一個獨(dú)立的Widget變量,右邊的兩個按鈕也是同理:
class ActionRow extends StatelessWidget { @override Widget build(BuildContext context) { // 將左邊的抽出來作為變量 Widget left = Container( padding: EdgeInsets.fromLTRB(6, 8, 8, 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children:[ Text( /* 短多了啊*/"Title", style: TextStyle(fontSize: 16), ), Container( padding: EdgeInsets.only(top: 4), child: Text( "Desc", style: TextStyle(fontSize: 12), ), ), ], ), ); // 右邊同理 Widget right = Row( children: [ Container( padding: EdgeInsets.fromLTRB(6, 8, 8, 8), child: MaterialButton( color: Colors.orange, /* 短多了啊*/child: Text("Edit"), onPressed: () { print("Do something here"); }, ), ), Container( padding: EdgeInsets.fromLTRB(6, 8, 8, 8), child: MaterialButton( color: Colors.red, child: Text("Delete"), onPressed: () { print("Do something here"); }, ), ), ], ); return AnimatedOpacity( opacity: 1, duration: Duration(milliseconds: 800), child: Container( color: Colors.white, margin: EdgeInsets.symmetric(vertical: 1), padding: EdgeInsets.symmetric(horizontal: 20), child: Row( children: [ Expanded( /*這里還是太長*/child: left, ), right, ],// 現(xiàn)在有六個反括號 ), ), ); } }
現(xiàn)在,我們的程序似乎有了一個均勻的左邊距,看起來不會那么可怕了。
反復(fù)利用變量,處理復(fù)雜嵌套在嵌套很復(fù)雜時,也可以使用這種處理方法,把修飾用的UI與主體功能分離。很多時候?yàn)榱藢?shí)現(xiàn)設(shè)計(jì)圖我們會嵌套很多的Center和Padding,將他們與真正起作用的UI分離開,有利于我們第一時間找到目標(biāo)Widget:
class ActionRow extends StatelessWidget { @override Widget build(BuildContext context) { // 這里看起來非常清晰,我們就不需要繼續(xù)抽離變量了 Widget left = Container( padding: EdgeInsets.fromLTRB(6, 8, 8, 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children:反復(fù)利用變量完成條件渲染[ Text( "Title", style: TextStyle(fontSize: 16), ), Container( padding: EdgeInsets.only(top: 4), child: Text( "Desc", style: TextStyle(fontSize: 12), ), ), ], ), ); Widget right = Row( children: [ Container( padding: EdgeInsets.fromLTRB(6, 8, 8, 8), child: MaterialButton( color: Colors.orange, child: Text("Edit"), onPressed: () { print("Do something here"); }, ), ), Container( padding: EdgeInsets.fromLTRB(6, 8, 8, 8), child: MaterialButton( color: Colors.red, child: Text("Delete"), onPressed: () { print("Do something here"); }, ), ), ], ); // 定義變量 Widget row = Row( children: [ Expanded( child: left, ), right, ], ); // 然后在外面嵌套修飾的Container,注意,這里把row嵌套給了自己 row = Container( color: Colors.white, margin: EdgeInsets.symmetric(vertical: 1), padding: EdgeInsets.symmetric(horizontal: 20), child: row, ); // 我突然覺得這一層Widget暫時不需要,使用注釋就可以將其去掉 // 如果這里是嵌套的寫法,是不能快速注釋一個Widget的 // row = AnimatedOpacity( // opacity: 1, // duration: Duration(milliseconds: 800), // child: row, // ); return row; } }
有時候,在數(shù)據(jù)不同時,我們希望組件按不同的方式嵌套。將組件寫成一整坨當(dāng)然做不到如此靈活,從google的AppBar的源碼中,我學(xué)習(xí)了一套寫法,通過反復(fù)利用同一個Widget,優(yōu)雅的處理了條件渲染的問題。
在這個例子里,我們希望做到一個效果,如果沒有傳入onEdit與onDelete方法,就不渲染右邊的部分,應(yīng)該如何寫呢?這個時候,嵌套任何組件都顯得復(fù)雜,我們只需要一個if就搞定了。
// 現(xiàn)在看起來就好多啦 class ActionRow extends StatelessWidget { final String title; final String desc; final VoidCallback onEdit; final VoidCallback onDelete; // 如上文所述,這里是自動生成的,然后添加一下默認(rèn)值 const ActionRow({ Key key, this.title: "title", this.desc: "desc", this.onEdit, this.onDelete, }) : super(key: key); @override Widget build(BuildContext context) { Widget left = Container( padding: EdgeInsets.fromLTRB(6, 8, 8, 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children:提取組件——Stateful與Stateless[ Text( title, style: TextStyle(fontSize: 16), ), Container( padding: EdgeInsets.only(top: 4), child: Text( desc, style: TextStyle(fontSize: 12), ), ), ], ), ); Widget right = Container( alignment: Alignment.center, child: Text("No Function Here"), ); // 只有傳入方法,右邊才會出現(xiàn)按鈕 if (onEdit != null || onDelete != null) { right = Row( children: [ Container( padding: EdgeInsets.fromLTRB(6, 8, 8, 8), child: MaterialButton( color: Colors.orange, child: Text("Edit"), onPressed: onEdit ?? () {}, ), ), Container( padding: EdgeInsets.fromLTRB(6, 8, 8, 8), child: MaterialButton( color: Colors.red, child: Text("Delete"), onPressed: onDelete ?? () {}, ), ), ], ); } Widget row = Row( children: [ Expanded( child: left, ), right, ], ); row = Container( color: Colors.white, margin: EdgeInsets.symmetric(vertical: 1), padding: EdgeInsets.symmetric(horizontal: 20), child: row, ); return row; } }
很顯然上面的代碼屬于比較簡單的UI代碼,我們通常會把代碼寫的更大更復(fù)雜,這時候抽取組件就十分有必要,在上面的代碼中,我們覺得left還是有點(diǎn)復(fù)雜的,試著把它抽出來,作為一個StatelessWidget:
想想:為什么不是Stateful的Widget?
這一步也有快捷操作哦:
抽離后的代碼:
class ActionRow extends StatelessWidget { final String title; final String desc; final VoidCallback onEdit; final VoidCallback onDelete; const ActionRow({ Key key, this.title: "title", this.desc: "desc", this.onEdit, this.onDelete, }) : super(key: key); @override Widget build(BuildContext context) { // 這個就很少了 Widget left = TextGroup(title: title, desc: desc); Widget right = Container( alignment: Alignment.center, child: Text("No Function Here"), ); if (onEdit != null || onDelete != null) { right = Row( children:[ Container( padding: EdgeInsets.fromLTRB(6, 8, 8, 8), child: MaterialButton( color: Colors.orange, child: Text("Edit"), onPressed: onEdit ?? () {}, ), ), Container( padding: EdgeInsets.fromLTRB(6, 8, 8, 8), child: MaterialButton( color: Colors.red, child: Text("Delete"), onPressed: onDelete ?? () {}, ), ), ], ); } Widget row = Row( children: [ Expanded( child: left, ), right, ], ); row = Container( color: Colors.white, margin: EdgeInsets.symmetric(vertical: 1), padding: EdgeInsets.symmetric(horizontal: 20), child: row, ); // row = AnimatedOpacity( // opacity: 1, // duration: Duration(milliseconds: 800), // child: row, // ); return row; } } // 沒必要優(yōu)化抽離后的小Widget,畢竟只需要知道他負(fù)責(zé)顯示兩行字就好了 // 看上去代碼很多,但是都是自動生成的 class TextGroup extends StatelessWidget { const TextGroup({ Key key, @required this.title, @required this.desc, }) : super(key: key); final String title; final String desc; @override Widget build(BuildContext context) { return Container( padding: EdgeInsets.fromLTRB(6, 8, 8, 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: TextStyle(fontSize: 16), ), Container( padding: EdgeInsets.only(top: 4), child: Text( desc, style: TextStyle(fontSize: 12), ), ), ], ), ); } }
如此一來我們的優(yōu)化就完成了,對比一下代碼,是不是看起來更好了呢?
優(yōu)化完成,看看縮略圖:優(yōu)化前:
優(yōu)化后:
很多開發(fā)者會有如下誤區(qū)。實(shí)際上,Google的部分UI源碼也存在如下這些問題,導(dǎo)致閱讀困難,但是有部分官方Widget的代碼質(zhì)量明顯更好,我們當(dāng)然可以學(xué)習(xí)更好的寫法。
在編寫UI代碼時,請避免如下行為:
使用function來創(chuàng)建Widget不必使用function來創(chuàng)建Widget,你應(yīng)當(dāng)把組件提取成StatelessWidget,然后將屬性或事件傳遞給這個Widget。
使用function的問題是,你可以在function中向Widget傳遞閉包,該閉包包含了當(dāng)前的作用域,卻又不在build方法中,同時你也可以在function中做其他無關(guān)的事情。
所以當(dāng)我們過一段時間回頭閱讀代碼的時候,build中夾雜的function顯得非常的混亂不堪,沒有條理,UI應(yīng)當(dāng)是聚合在一起的,而數(shù)據(jù)與事件,應(yīng)當(dāng)與UI分離開來。如此才可以閱讀一次build方法,就基本理解當(dāng)前Widget的功能與目的。
// function創(chuàng)建Widget可能會破壞Widget樹的可讀性 class ActionRow extends StatelessWidget { final String title; final String desc; final VoidCallback onEdit; final VoidCallback onDelete; const ActionRow({ Key key, this.title: "title", this.desc: "desc", this.onEdit, this.onDelete, }) : super(key: key); Widget buildEditButton() { return Container( padding: EdgeInsets.fromLTRB(6, 8, 8, 8), child: MaterialButton( color: Colors.orange, child: Text("Edit"), onPressed: onEdit ?? () {}, ), ); } Widget buildDeleteButton() { return Container( padding: EdgeInsets.fromLTRB(6, 8, 8, 8), child: MaterialButton( color: Colors.red, child: Text("Delete"), onPressed: onDelete ?? () {}, ), ); } @override Widget build(BuildContext context) { // Widget left = TextGroup(title: title, desc: desc); Widget right = Container( alignment: Alignment.center, child: Text("No Function Here"), ); if (onEdit != null || onDelete != null) { // 本來這里要傳入onDelete和onEdit的, // 但是現(xiàn)在這兩個屬性根本就不在build方法里出現(xiàn)(他們?nèi)ツ膬毫耍浚? // 所以使用function來build組件可能會丟失一些關(guān)鍵信息,打斷代碼閱讀的順序。 Widget editButton = buildEditButton(); Widget deleteButton = buildDeleteButton(); right = Row( children:[ editButton, deleteButton, ], ); } Widget row = Row( children: [ // Expanded( // child: left, // ), right, ], ); row = Container( color: Colors.white, margin: EdgeInsets.symmetric(vertical: 1), padding: EdgeInsets.symmetric(horizontal: 20), child: row, ); return row; } }
這個當(dāng)然不是強(qiáng)制的,甚至不少Google的例子也采用這種寫法,但是通過閱讀大量的源碼來進(jìn)行對比,這種寫法是很難通順閱讀的,總是需要在不同的function中切來切去,屬性引用沒有任何章法可言。
而StatelessWidget會強(qiáng)制所有屬性都是final的,這意味著,你必須把可變的屬性寫在build方法里(而不是其他地方),大多數(shù)時候,這非常有利于代碼閱讀。
因?yàn)?b>final的特性,你也沒機(jī)會把變量寫到其他地方了,這樣看起來更整潔,畢竟整個頁面的數(shù)據(jù)通常也只有那么幾個。寫太多StatefulWidget
這里其實(shí)說的是,不要嵌套很多StatefulWidget,事實(shí)上大部分Widget都可以是Stateless的:例如官方的Switch組件,居然也是Stateless的。通常按照我們的經(jīng)驗(yàn),Switch似乎需要維護(hù)自己的開關(guān)狀態(tài),在Flutter實(shí)際應(yīng)用中,并不需要如此,任何狀態(tài)都可以交給父組件管理,從而減少一個StatefulWidget,也就減少了一個State,大大減少了UI代碼的復(fù)雜程度。
從我目前的經(jīng)驗(yàn)來看,只有很少部分Widget需要寫成Stateful的:
頁面,推薦每一個返回Scaffold的Widget都寫成Stateful的
需要在initState中觸發(fā)方法,例如從網(wǎng)絡(luò)請求數(shù)據(jù),開啟藍(lán)牙搜索等異步操作。
需要維護(hù)自己的動畫狀態(tài)的。
同時StatefulWidget不應(yīng)緊密嵌套在一起,只需要把數(shù)據(jù)都放在上一級的state里就好,維護(hù)state實(shí)際上會多出非常多的無用代碼,過多嵌套會直接導(dǎo)致代碼混亂不堪。
總結(jié)作者:馬嘉倫
日期:2019/07/14
平臺:Segmentfault獨(dú)家,勿轉(zhuǎn)載
我的其他文章:
【開發(fā)經(jīng)驗(yàn)】淺談flutter的優(yōu)點(diǎn)與缺點(diǎn)
【Flutter工具】fmaker:自動生成倍率切圖/自動更換App圖標(biāo)
【開發(fā)經(jīng)驗(yàn)】在Flutter中使用dart的單例模式
本文是對Flutter的一種編碼風(fēng)格的概括,主要的意義在于減少代碼嵌套層數(shù),增強(qiáng)代碼可讀性。本文大部分經(jīng)驗(yàn)其實(shí)來自Google自己的組件源碼,是通過對比大量源碼得出的一個較優(yōu)寫法,如果你對上述觀點(diǎn),建議,代碼,風(fēng)格有疑問或者發(fā)現(xiàn)了文章中的問題,請直接留下你的評論,我會直接在評論中進(jìn)行回復(fù)。
本文禁止任何轉(zhuǎn)載,需轉(zhuǎn)載授權(quán)可直接聯(lián)系我文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/105633.html
摘要:是啥是谷歌推出的一套視覺設(shè)計(jì)語言。比如有的可以換皮膚,而每一套皮膚就是一種設(shè)計(jì)語言,有古典風(fēng)呀炫酷風(fēng)呀極簡風(fēng)呀神馬的,而就是谷歌風(fēng),有興趣的同學(xué)可以學(xué)習(xí)了解一下官方原版和中文翻譯版,這是每一個產(chǎn)品經(jīng)理的必修教材。 flutter環(huán)境和運(yùn)行環(huán)境搭建好之后,可以開始擼碼了,然而當(dāng)你打開VScode,在打開項(xiàng)目文件夾后,擺在你面前的是main.dart被打開的樣子,里面七七八八的已經(jīng)寫好了一...
摘要:體驗(yàn)熱更新帶來的開發(fā)周期加速。學(xué)會使用有狀態(tài)控件,增強(qiáng)了應(yīng)用的交互。使用和創(chuàng)建了一個支持懶加載的無限滾動列表。了解如何使用主題更改應(yīng)用的外觀。 接著上一篇,我們做一個這樣的APP:showImg(https://segmentfault.com/img/remote/1460000013672700); 開始之前,我發(fā)現(xiàn)了一個好玩的東西,每次我們在終端中輸入命令: flutter ru...
摘要:繼上一篇關(guān)于的介紹,是仿照微信界面,因?yàn)樽鳛榍岸碎_發(fā),有一定的基礎(chǔ),所有寫起來,也不是很吃力。班門弄斧之作,若有大神見到,敬請指教,有不對不合理之處,敬請指出我是邇伶貳環(huán)境準(zhǔn)備以系統(tǒng)為例。 flutter的入門記錄 0.前言: flutter 的入門demo 已經(jīng)寫好一個星期了,只不過一直都沒有整理出博客來。收拾好心情,來整理一下。繼上一篇關(guān)于react-native-wx的介紹,是仿...
摘要:是谷歌的移動框架,可以快速在和上構(gòu)建高質(zhì)量的原生用戶界面。在全世界好了這些,大家早就知道了,來點(diǎn)實(shí)在的話說隔壁師兄,閑魚是最早一批與谷歌展開合作,并在重要的商品詳情頁中使用技術(shù)上線的。一切皆來自的組件皆來自。是狀態(tài)不可變的稱為無狀態(tài)。 前言 要說2018年最火的跨端技術(shù),當(dāng)屬于 Flutter 莫屬,應(yīng)該沒人質(zhì)疑吧。一個新的技術(shù)的趨勢,最明顯的特征,就是它一定想把前浪拍死在沙灘上。這個...
閱讀 1356·2021-10-09 09:44
閱讀 1439·2021-09-28 09:36
閱讀 15925·2021-09-22 15:55
閱讀 1238·2021-09-22 15:45
閱讀 2198·2021-09-02 09:48
閱讀 2783·2019-08-29 17:19
閱讀 2296·2019-08-29 10:54
閱讀 906·2019-08-23 18:40