原文地址:How refactoring improve readability, maintainability and performance optimization of your Flutter application
原作者:Jonathan Monga
讀后感:
這篇文章是關(guān)于如何組織代碼結(jié)構(gòu)的,如何編寫Flutter代碼,才能使代碼有更好的可讀性、可維護(hù)性,并且?guī)砀玫男阅軈龋耙卜g過一篇相似的文章Flutter Widget瘦身,兩篇文章看完,想必會(huì)給你帶一些收益。
前言
我們都同意widget 樹是你在UI中所獲得的東西,并且同意它完全是關(guān)于Flutter widget的,因此你可以將你的widget相互嵌套。無論你的UI是簡單還是復(fù)雜,當(dāng)你的UI簡單時(shí),即使幾周后回來閱讀你的代碼,它也很容易閱讀,并且性能很好,因?yàn)樗故镜膬?nèi)容很少。但是當(dāng)你的應(yīng)用界面比較復(fù)雜時(shí),這會(huì)促使你嵌套大量的widget,代碼的可讀性、可維護(hù)性降低,程序的效率也會(huì)降低。
我知道,對(duì)于初學(xué)者來說,很容易沒有重構(gòu)代碼的文化,一旦注意力轉(zhuǎn)移到其他事情上,初學(xué)者就會(huì)滿足于widget的嵌套、嵌套、嵌套,這就是產(chǎn)生很深的widget樹的原因。對(duì)于像我這樣的新手Flutter開發(fā)者來說,這是很常見的現(xiàn)象,好吧,既然問題已經(jīng)暴露出來了,我們?cè)趺幢苊猓咳绾我砸环N不陷入非常深的widget樹的方式進(jìn)行編碼吶?
在我之前已經(jīng)有不少人探討過這個(gè)問題了,但我認(rèn)為還是值得在花點(diǎn)時(shí)間再談?wù)撘幌隆_@個(gè)經(jīng)常困擾我們的問題的答案就是代碼重構(gòu)。既然你已經(jīng)得到了答案,那么就不要再拖延重構(gòu)你的代碼啦。下面我將用不同的技術(shù),向你展示如何進(jìn)行代碼重構(gòu)。
在向你展示如何重構(gòu)代碼之前,讓我們使用此UI的代碼:
這個(gè)很漂亮的UI來自于https://github.com/JideGuru/weather_neumorphism_ui,這里并沒有惡意,我不認(rèn)為我比Olusegun Festus Babajide更厲害,以至于我有權(quán)利對(duì)他的代碼做點(diǎn)評(píng)。同樣你如果找到一些我的代碼,我相信,你也會(huì)發(fā)現(xiàn)很多值得抱怨的地方。
不,這不是下流或者傲慢的行為,我將要做的無非只是專業(yè)的評(píng)論,這是我們都應(yīng)該樂于做的事情,當(dāng)完成時(shí),我們應(yīng)該歡迎它。通過這樣發(fā)表評(píng)論,可以促使我們學(xué)習(xí),醫(yī)生這樣做、飛行員這樣做、律師這樣做,我們程序員也應(yīng)該學(xué)習(xí)這樣做。補(bǔ)充一點(diǎn):Olusegun Festus Babajide不僅是一位很好的Flutter開發(fā)人員,并且有勇氣和善意,愿意將他的代碼免費(fèi)提供給整個(gè)社區(qū),他把它提供給所有人看,并邀請(qǐng)公眾使用和監(jiān)督,這樣做很贊。
這是現(xiàn)在的代碼:
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Container(
height: 40,
width: 40,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Theme.of(context).primaryColor,
boxShadow: [
BoxShadow(
offset: Offset(3, 3),
color: Colors.black12,
blurRadius: 5,
),
BoxShadow(
offset: Offset(-3, -3),
color: Colors.white,
blurRadius: 5,
)
],
),
child: Icon(
Icons.arrow_back_ios,
size: 14,
),
),
],
),
centerTitle: true,
elevation: 0,
title: Text(
"${Constants.appName}",
style: TextStyle(
fontSize: 25,
fontWeight: FontWeight.w900,
),
),
),
body: ListView(
padding: EdgeInsets.symmetric(horizontal: 20),
children: <Widget>[
Container(
height: 100,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Container(
height: 70,
width: MediaQuery.of(context).size.width,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Theme.of(context).primaryColor,
boxShadow: [
BoxShadow(
offset: Offset(3, 3),
color: Colors.black12,
blurRadius: 5,
),
BoxShadow(
offset: Offset(-3, -3),
color: Colors.white,
blurRadius: 5,
)
],
),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 20),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(
"Period",
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).textTheme.caption.color,
),
),
SizedBox(width: 30,),
Text(
"Last 30 days",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
],
),
Container(
height: 40,
width: 40,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Theme.of(context).primaryColor,
boxShadow: [
BoxShadow(
offset: Offset(3, 3),
color: Colors.black12,
blurRadius: 5,
),
BoxShadow(
offset: Offset(-3, -3),
color: Colors.white,
blurRadius: 5,
)
],
),
child: Icon(
Icons.arrow_forward_ios,
size: 14,
),
),
],
),
),
),
],
),
),
SizedBox(height: 20,),
Container(
height: 300,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Container(
height: 280,
width: MediaQuery.of(context).size.width,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).primaryColor,
boxShadow: [
BoxShadow(
offset: Offset(6, 6),
color: Colors.black12,
blurRadius: 5,
),
BoxShadow(
offset: Offset(-6, -6),
color: Colors.white,
blurRadius: 5,
)
],
),
child: Stack(
children: <Widget>[
Align(
alignment: Alignment.center,
child: Icon(
Feather.loader,
size: 250,
color: Theme.of(context).accentColor,
),
),
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Container(
height: 200,
width: MediaQuery.of(context).size.width,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).primaryColor,
boxShadow: [
BoxShadow(
offset: Offset(3, 3),
color: Colors.black12,
blurRadius: 5,
),
BoxShadow(
offset: Offset(-3, -3),
color: Colors.white,
blurRadius: 5,
)
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Icon(
Feather.thermometer,
color: Theme.of(context).accentColor,
size: 40,
),
SizedBox(height: 20,),
Text(
"7°C",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 22,
color: Theme.of(context).accentColor,
),
),
],
),
),
],
),
],
),
),
],
),
),
SizedBox(height: 20,),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Container(
height: 150,
width: 130,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Theme.of(context).primaryColor,
boxShadow: [
BoxShadow(
offset: Offset(3, 3),
color: Colors.black12,
blurRadius: 5,
),
BoxShadow(
offset: Offset(-3, -3),
color: Colors.white,
blurRadius: 5,
),
],
),
child: Padding(
padding: EdgeInsets.all(15),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Icon(
Feather.cloud_snow,
size: 40,
color: Theme.of(context).accentColor,
),
Text(
"Cool",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 22,
),
),
],
),
),
),
Neumorphic(
height: 150,
width: 130,
status: NeumorphicStatus.convex,
decoration: NeumorphicDecoration(
borderRadius: BorderRadius.circular(10),
),
child: Padding(
padding: EdgeInsets.all(15),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Icon(
Feather.sun,
size: 40,
color: Colors.deepOrange,
),
Text(
"Warm",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 22,
),
),
],
),
),
),
],
),
SizedBox(height: 20,),
Neumorphic(
status: NeumorphicStatus.convex,
height: 50,
decoration: NeumorphicDecoration(
borderRadius: BorderRadius.circular(10),
),
child: Center(
child: Text(
"Update Settings",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: Theme.of(context).accentColor,
),
),
),
),
],
),
);
}
}
那么讓我們看看如何使這一切井然有序。
1、使用方法重構(gòu)
我想你在某些地方已經(jīng)看到了這種技術(shù)而沒有意識(shí)到。該技術(shù)只是將widget作為方法調(diào)用的返回值,進(jìn)行封裝使用。假設(shè)在Flutter中一切都是widget,那么任何參與組成UI的類都繼承自Widget類,該方法的返回值可能是任何一個(gè)widget類或者一些特定的類,例如容器類container、row、column等。
繼續(xù)往下看,方法中的Widget可以依賴父widget的BuildContext
實(shí)例或?qū)ο蟆_@就是問題的來源,記住BuildContext
對(duì)象知道widget在widget tree中的位置。既然此方法依賴于主BuildContext
,那么當(dāng)父widget重繪時(shí),此方法也將強(qiáng)制重新組裝、重新創(chuàng)建或者重繪其內(nèi)部的widget。或者如果該方法也調(diào)用了其他依賴于父widget的BuildContext
的方法,也會(huì)帶來副作用,所有方法繪制他們的widget的次數(shù)將會(huì)和繪制父widget的次數(shù)一樣多。無論哪種情況,這都不是我們重構(gòu)后所期望的行為。
使用這種方法,我們將widget分割開來,這當(dāng)然能夠帶來可讀性及可維護(hù)性的提升,但是對(duì)于性能優(yōu)化,并沒有什么用處。當(dāng)widget數(shù)量增加時(shí),我們UI的性能在配置更改期間將會(huì)下降,例如屏幕旋轉(zhuǎn)。
下面是兩個(gè)方法的示例:
Column _buildLeadingColumn(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Container(
height: 40,
width: 40,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Theme.of(context).primaryColor,
boxShadow: [
BoxShadow(
offset: Offset(3, 3),
color: Colors.black12,
blurRadius: 5,
),
BoxShadow(
offset: Offset(-3, -3),
color: Colors.white,
blurRadius: 5,
)
],
),
child: Icon(
Icons.arrow_back_ios,
size: 14,
),
),
],
);
}
Widget _buildRow(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(
"Period",
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).textTheme.caption.color,
),
),
SizedBox(
width: 30,
),
Text(
"Last 30 days",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
],
);
}
我們使用Visual Studio Code
作為代碼編輯器(AS也一樣),并按照以下步驟進(jìn)行重構(gòu):
1.打開任何.dart文件
2.將光標(biāo)放在第一個(gè)widget上,然后右擊,在我的場(chǎng)景中,是在Row、Container或者Column上。
3.選中Refactor >Extract Method
4.在提取方法的彈窗中,輸入_buildRow
作為方法名,注意方法前的下劃線,讓Dart知道這是一個(gè)私有方法。
5.Row widget現(xiàn)在替換為了_b方法uildRow()
,滾動(dòng)到代碼底部,方法和widget都得到了很好的重構(gòu)。
6.繼續(xù)重構(gòu)其他的Rows、Columns、Containers和Stack Widget。
這種方式增加了代碼的可讀性,widget樹的主要組成部分被分割成了非常簡單的方法,這種方式的好處是純粹和簡單的代碼可讀性和可維護(hù)性,作為回報(bào)失去了優(yōu)化性能,如果你想看更多內(nèi)容,請(qǐng)轉(zhuǎn)到底部的引用部分。
2、使用局部變量重構(gòu)
和第一種重構(gòu)方式有些相識(shí),只不過這里使用局部變量,包括使用final變量初始化widget。在這里一樣是將widget樹的主要部分分割成多個(gè),這增加了代碼的可讀性和可維護(hù)性。
在這種情況下,雖然我們的widget使用final來初始化變量,但是仍然使用的是父widget的BuildContext,當(dāng)框架重繪父widget時(shí),局部變量也將會(huì)被重繪。這增加了可讀性和可維護(hù)性,你的widget樹將會(huì)變淺,但是不會(huì)優(yōu)化性能。
下面是一個(gè)帶有常量的的重構(gòu)代碼示例:
final rowConstant = Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(
"Period",
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).textTheme.caption.color,
),
),
SizedBox(
width: 30,
),
Text(
"Last 30 days",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
],
);
我們使用Visual Studio Code
作為代碼編輯器(AS也一樣),并按照以下步驟進(jìn)行重構(gòu):
1.打開任何.dart文件
2.將光標(biāo)放在第一個(gè)widget上,然后右擊,在我的場(chǎng)景中,是在Row、Container或者Column上。
3.選擇 Refactor > Extract Local Varialble
4.在我們的例子中,將局部變量命名為rowConstant
,注意我們使用final進(jìn)行修飾,告訴Dart這是一個(gè)常量。
5.Row widget替換為了rowConstant
最終變量。滾動(dòng)帶代碼頂部,局部變量和widget都得到了很好的重構(gòu)。
6.繼續(xù)重構(gòu)其他的Rows、Columns、Containers和Stack Widget。
3、使用widget class重構(gòu)
這種方式允許你使用繼承自StatelessWidget
或者StatefullWidget
的類,來隔離widget子樹,還允許你創(chuàng)建可重用的widget,并且可以將它們分布在相同或不同的dart文件中,這樣你就可以在程序的任何地方引入或者使用這些文件。警告!這些類的構(gòu)造函數(shù)必須以const
關(guān)鍵字開頭,再次感謝Dart,以const
開頭聲明的構(gòu)造函數(shù),會(huì)告訴Dart緩存和重用這些widget,與此相反的是其它widget將會(huì)被重繪。
當(dāng)你要?jiǎng)?chuàng)建此類的對(duì)象時(shí),不要忘記使用const
關(guān)鍵字。通過這樣做,當(dāng)其他widget在widget樹中更改狀態(tài)時(shí),此widget將不會(huì)被重建。如果遺漏了const
關(guān)鍵字,父widget重繪多少次,我們的widget也將會(huì)跟著重繪多少次,因此需要留心。
這樣的widget類依賴它自己的BuildContext
,而不是像重構(gòu)成方法或者變量的那樣依賴于父widget的。BuildContext
負(fù)責(zé)管理widget在widget樹中的位置。
現(xiàn)在讓我們看看使用這種方式的小例子:
class PaddingWidget extends StatelessWidget {
const PaddingWidget({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 20),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(
"Period",
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).textTheme.caption.color,
),
),
SizedBox(
width: 30,
),
Text(
"Last 30 days",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
],
),
Container(
height: 40,
width: 40,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Theme.of(context).primaryColor,
boxShadow: [
BoxShadow(
offset: Offset(3, 3),
color: Colors.black12,
blurRadius: 5,
),
BoxShadow(
offset: Offset(-3, -3),
color: Colors.white,
blurRadius: 5,
)
],
),
child: Icon(
Icons.arrow_forward_ios,
size: 14,
),
),
],
),
);
}
}
我們使用Visual Studio Code
作為代碼編輯器(AS也一樣),并按照以下步驟進(jìn)行重構(gòu):
1.打開任何.dart文件
2.將光標(biāo)放在第一個(gè)widget上,然后右擊,在我的場(chǎng)景中,是在Row、Container或者Column上。
3.選擇 Refactor > Extract Widget
4.在我們的例子中,將類名命名為PaddingWidget
。
5.Padding widget替換為了PaddingWidget
類。滾動(dòng)帶代碼底部,類和widget都得到了很好的重構(gòu)。
6.繼續(xù)并重構(gòu)其他Padding(PaddingWidgets class)、Rows(RowsAndColumnWidget class)widget。
抱歉,有太多內(nèi)容需要消化,我總結(jié)一下:你不僅在可讀性和可維護(hù)性上有所收獲,并且性能也會(huì)有很大提升。因?yàn)楫?dāng)父widget重繪時(shí),并不是所有widget都會(huì)被重繪,他們只構(gòu)建一次。
結(jié)論
在這篇文章中,你了解到了widget樹是widget嵌套的結(jié)果,隨著widget的增加,widget樹會(huì)迅速擴(kuò)展并且降低代碼的可讀性以及可管理性,這被稱之為整個(gè)widget樹。為了提高代碼的可讀性和可管理性,你可以將widget分割成獨(dú)立的widget類,創(chuàng)建一個(gè)淺的widget樹。在每個(gè)程序中,你都應(yīng)該盡量保持widget樹層級(jí)淺。通過使用widget類的重構(gòu)方式,你可以在Flutter子樹的重構(gòu)中獲益,這將會(huì)提升性能。
感謝閱讀我的文章,歡迎進(jìn)行評(píng)論。
Refactoring a Flutter Project -- a story about progression and decisions