本文適合使用Flutter開發過一段時間的開發者閱讀,旨在分享一種避免
Flutter
的UI代碼嵌套太深問題的方法。若是對本文內容或觀點有相關疑問,歡迎在評論中指出。
優化效果(縮略圖):html
距離我接觸Flutter已通過去了九個月,在Flutter代碼編寫的過程當中,不少開發者都遇到了「回調地獄」的問題。在Flutter
中,稱之爲回調並不許確,準確的說,是由於衆多Widget
互相嵌套在一塊兒,致使反括號部分堆積嚴重,極度影響代碼可讀性。前端
本文將介紹一種代碼編寫風格,最大限度減小嵌套對代碼閱讀的影響。segmentfault
咱們先來簡單看一下,Flutter
的UI代碼:網絡
build
方法Flutter
的Widget
使用build
方法來建立UI組件,而後經過注入child
屬性的方式爲組件添加子組件,子組件能夠繼續包含child
,經過調用每個child
的build
方法,就造成了相似DOM結構的組件樹,而後由渲染引擎渲染圖形。 閉包
一個常見的定義組件的例子以下:框架
class DeleteText extends StatelessWidget { // 咱們在build方法中渲染自定義Widget @override Widget build(BuildContext context) { return Text('Delete'); } }
final
要在Flutter
中定義(繼承)一個Widget,則它的屬性必須都是final
的。final
意味着屬性必須在構造函數中就被初始化完成,不接受提早定義,也不接受更改。因此,在生命週期中動態的改變Widget
對象的屬性是不可能的,必須使用框架的build
方法來爲構造函數動態指定參數,從而達到改變組件屬性的功能。less
class Avatar extends StatelessWidget { // 若是url屬性不是final的,編譯器會報出警告 final String url; // 這個構造方法很長,可是主要你寫了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:自動建立構造方法,只要是構造方法沒有的final屬性,點擊「快速修復」,就能夠自動生成構造方法。
嵌套正是DOM樹的特色,正如HTML
其實也會無限嵌套同樣(大多數前端可能看HTML看習慣了,都忘了HTML其實也常常會寫成嵌套很深的形式),Flutter
的UI代碼嵌套本質是不可避免的,這正是Flutter
UI代碼的編寫特色——一次成型,而不是經過addView
之類的方法來手動管理每個視圖的生命週期。在此基礎上,Flutter
能夠高效的反覆重建Widget
,在渲染效率上展示出了很是大的優點。異步
<!-- html的嵌套其實也很深 --> <div> <div> <div> <div> <article> <h1></h1> <li></li> </article> </div> </div> </div> </div>
當咱們評判一串代碼的時候,一個顯而易見的點,就是代碼距離左邊的距離,若是一行代碼距離左邊達到了十多個tab,可想而知它被嵌套在了多麼深的位置。ide
來看看這個Widget
,這個Widget
很簡單,左邊有一個正文和一個附屬文本,附屬文本在正文下方,右邊有一組按鈕,表明這一行的操做,咱們再給他嵌套一個動畫的漸現效果,處理好字體。那麼他的代碼應該以下所示:函數
// 一個簡單的嵌套的狀況 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: <Widget>[ Expanded( child: Container( padding: EdgeInsets.fromLTRB(6, 8, 8, 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ /* 超級長的左邊距 */Text( 'Title', style: TextStyle(fontSize: 16), ), Container( padding: EdgeInsets.only(top: 4), child: Text( 'Desc', style: TextStyle(fontSize: 12), ), ), ], ), ), ), Row( children: <Widget>[ 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'); },// 往下數,足足11個反括號 ), ), ], ) ], ), ), ); } }
此種代碼,只要是開發過Flutter
的開發者必定不會陌生,它能夠完美運行,可是十分難以閱讀。反括號的數量常常會達到一個更誇張的級別,致使部份內容被頂到過於右邊,在閱讀時形成了很是大的困難。
就讓咱們以這串代碼爲例子,來優化他的嵌套,使其能夠輕鬆的從上到下閱讀。
new
Dart2
已經能夠徹底不寫new
了,但有的開發者還在寫new
。去掉new
以後,代碼會變得更加乾淨。
在這裏,咱們能夠抽取部分嵌套很深的Widget,將其定義成變量,從而減小它與左邊的距離。
讀一下代碼,咱們很容易就能發現,左邊的Expanded部分中,兩個文字的相關代碼距離左邊太遠了,咱們將他們抽出來做爲一個獨立的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: <Widget>[ Text( /* 短多了啊*/'Title', style: TextStyle(fontSize: 16), ), Container( padding: EdgeInsets.only(top: 4), child: Text( 'Desc', style: TextStyle(fontSize: 12), ), ), ], ), ); // 右邊同理 Widget right = Row( children: <Widget>[ 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: <Widget>[ Expanded( /*這裏仍是太長*/child: left, ), right, ],// 如今有六個反括號 ), ), ); } }
如今,咱們的程序彷佛有了一個均勻的左邊距,看起來不會那麼可怕了。
在嵌套很複雜時,也可使用這種處理方法,把修飾用的UI與主體功能分離。不少時候爲了實現設計圖咱們會嵌套不少的Center和Padding,將他們與真正起做用的UI分離開,有利於咱們第一時間找到目標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: <Widget>[ Text( 'Title', style: TextStyle(fontSize: 16), ), Container( padding: EdgeInsets.only(top: 4), child: Text( 'Desc', style: TextStyle(fontSize: 12), ), ), ], ), ); Widget right = Row( children: <Widget>[ 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: <Widget>[ 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; } }
有時候,在數據不一樣時,咱們但願組件按不一樣的方式嵌套。將組件寫成一整坨固然作不到如此靈活,從google的AppBar的源碼中,我學習了一套寫法,經過反覆利用同一個Widget,優雅的處理了條件渲染的問題。
在這個例子裏,咱們但願作到一個效果,若是沒有傳入onEdit與onDelete方法,就不渲染右邊的部分,應該如何寫呢?這個時候,嵌套任何組件都顯得複雜,咱們只須要一個if就搞定了。
// 如今看起來就好多啦 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 = Container( padding: EdgeInsets.fromLTRB(6, 8, 8, 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ 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'), ); // 只有傳入方法,右邊纔會出現按鈕 if (onEdit != null || onDelete != null) { right = Row( children: <Widget>[ 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: <Widget>[ Expanded( child: left, ), right, ], ); row = Container( color: Colors.white, margin: EdgeInsets.symmetric(vertical: 1), padding: EdgeInsets.symmetric(horizontal: 20), child: row, ); return row; } }
很顯然上面的代碼屬於比較簡單的UI代碼,咱們一般會把代碼寫的更大更復雜,這時候抽取組件就十分有必要,在上面的代碼中,咱們以爲left仍是有點複雜的,試着把它抽出來,做爲一個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: <Widget>[ 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: <Widget>[ 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; } } // 不必優化抽離後的小Widget,畢竟只須要知道他負責顯示兩行字就行了 // 看上去代碼不少,可是都是自動生成的 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: <Widget>[ Text( title, style: TextStyle(fontSize: 16), ), Container( padding: EdgeInsets.only(top: 4), child: Text( desc, style: TextStyle(fontSize: 12), ), ), ], ), ); } }
如此一來咱們的優化就完成了,對比一下代碼,是否是看起來更好了呢?
優化前:
優化後:
不少開發者會有以下誤區。實際上,Google
的部分UI源碼也存在以下這些問題,致使閱讀困難,可是有部分官方Widget
的代碼質量明顯更好,咱們固然能夠學習更好的寫法。
在編寫UI代碼時,請避免以下行爲:
function
來建立Widget
沒必要使用function
來建立Widget
,你應當把組件提取成StatelessWidget
,而後將屬性或事件傳遞給這個Widget
。
使用function
的問題是,你能夠在function
中向Widget傳遞閉包,該閉包包含了當前的做用域,卻又不在build
方法中,同時你也能夠在function
中作其餘無關的事情。
因此當咱們過一段時間回頭閱讀代碼的時候,build
中夾雜的function
顯得很是的混亂不堪,沒有條理,UI應當是聚合在一塊兒的,而數據與事件,應當與UI分離開來。如此才能夠閱讀一次build方法,就基本理解當前Widget的功能與目的。
// function建立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的, // 可是如今這兩個屬性根本就不在build方法裏出現(他們去哪兒了?), // 因此使用function來build組件可能會丟失一些關鍵信息,打斷代碼閱讀的順序。 Widget editButton = buildEditButton(); Widget deleteButton = buildDeleteButton(); right = Row( children: <Widget>[ editButton, deleteButton, ], ); } Widget row = Row( children: <Widget>[ // Expanded( // child: left, // ), right, ], ); row = Container( color: Colors.white, margin: EdgeInsets.symmetric(vertical: 1), padding: EdgeInsets.symmetric(horizontal: 20), child: row, ); return row; } }
這個固然不是強制的,甚至很多Google的例子也採用這種寫法,可是經過閱讀大量的源碼來進行對比,這種寫法是很難通順閱讀的,老是須要在不一樣的function
中切來切去,屬性引用沒有任何章法可言。
而StatelessWidget
會強制全部屬性都是final
的,這意味着,你必須把可變的屬性寫在build方法裏(而不是其餘地方),大多數時候,這很是有利於代碼閱讀。
由於
final
的特性,你也沒機會把變量寫到其餘地方了,這樣看起來更整潔,畢竟整個頁面的數據一般也只有那麼幾個。
StatefulWidget
這裏其實說的是,不要嵌套不少StatefulWidget
,事實上大部分Widget均可以是Stateless
的:例如官方的Switch
組件,竟然也是Stateless
的。一般按照咱們的經驗,Switch
彷佛須要維護本身的開關狀態,在Flutter實際應用中,並不須要如此,任何狀態均可以交給父組件管理,從而減小一個StatefulWidget
,也就減小了一個State
,大大減小了UI代碼的複雜程度。
從我目前的經驗來看,只有不多部分Widget須要寫成Stateful
的:
Scaffold
的Widget
都寫成Stateful
的initState
中觸發方法,例如從網絡請求數據,開啓藍牙搜索等異步操做。同時StatefulWidget
不該緊密嵌套在一塊兒,只須要把數據都放在上一級的state
裏就好,維護state
實際上會多出很是多的無用代碼,過多嵌套會直接致使代碼混亂不堪。
做者:馬嘉倫
日期:2019/07/14
平臺:Segmentfault獨家,勿轉載
個人其餘文章:
【開發經驗】淺談flutter的優勢與缺點
【Flutter工具】fmaker:自動生成倍率切圖/自動更換App圖標
【開發經驗】在Flutter中使用dart的單例模式
本文是對Flutter的一種編碼風格的歸納,主要的意義在於減小代碼嵌套層數,加強代碼可讀性。本文大部分經驗其實來自Google
本身的組件源碼,是經過對比大量源碼得出的一個較優寫法,若是你對上述觀點,建議,代碼,風格有疑問或者發現了文章中的問題,請直接留下你的評論,我會直接在評論中進行回覆。