一切皆是Widget淮椰,但是不要把所有內(nèi)容都放置在一個(gè)Widget中
這是一篇翻譯文章:
原文:Everything is a Widget, but don’t put everything in one Widget!
作者:Romain Rastel
讀后感:
移動(dòng)端原生Android、iOS通常使用一種命令式的編程風(fēng)格來完成UI編程,這可能是大家最熟悉的風(fēng)格。Flutter則不同宠叼,讓開發(fā)人員只是描述當(dāng)前的UI狀態(tài)妈橄,并將轉(zhuǎn)換交給框架。在Flutter開發(fā)中寫的最多的就是各式各樣的Widget了富蓄,想必大家都有寫過很長(zhǎng)的Widget的經(jīng)驗(yàn),在同一個(gè)build函數(shù)里慢逾,Widget可以通過child屬性一層層的嵌套立倍,在相同的作用域下共享變量,寫起來很方便侣滩。但是當(dāng)需要狀態(tài)刷新的時(shí)候口注,則會(huì)刷新整個(gè)層級(jí),導(dǎo)致性能損失君珠,想必大家也想過該怎么組織Flutter代碼寝志,這篇文章將會(huì)給你帶來一些參考。
這篇文章在medium上有800多贊,對(duì)于Flutter社群是相當(dāng)高的數(shù)量了材部,說明大家比較認(rèn)可這種方式毫缆。文章以一個(gè)UI頁面為例,介紹了怎么組織代碼乐导,為什么要這樣組織代碼苦丁,以及如何通過代碼塊進(jìn)一步提升開發(fā)效率與性能。
正文:
作為一個(gè)Flutter開發(fā)者物臂,我肯定在你的職業(yè)生涯中至少聽過一次這樣的話:"一切都是Widget"旺拉,這是Flutter的一種口頭禪,也揭示出了這個(gè)優(yōu)秀的SDK的內(nèi)在力量棵磷。
當(dāng)我們深入進(jìn)catalog這個(gè)Widget中蛾狗,我們可以看到有很多Widget做著單一的工作,比如Padding
仪媒、Align
沉桌、 SizedBox
等,我們通過組合這些小的Widge來創(chuàng)建其他Widget规丽,我發(fā)現(xiàn)這種方式可擴(kuò)展蒲牧、功能強(qiáng)大且容易理解。
但是當(dāng)我閱讀一些在網(wǎng)上找到的源碼或者新手編寫的代碼后赌莺,我發(fā)現(xiàn)一件事情使我震驚:build 構(gòu)造方法有越來越大的趨勢(shì),并且在其中實(shí)例化了很多Widget
松嘶,我發(fā)現(xiàn)這樣很難閱讀艘狭、理解和維護(hù)。
···
作為軟件開發(fā)人員翠订,我們必須記住巢音,軟件的生命起始于當(dāng)它第一次發(fā)布給其他用戶。該軟件的源碼也將會(huì)由他人(包括將來的你自己)來閱讀尽超、維護(hù)官撼,這就是為什么我們的代碼應(yīng)該保持簡(jiǎn)單、易讀和理解的非常重要的原因似谁。
···
我們能在Flutter官網(wǎng)上找到一個(gè)一切皆是Widget
的例子傲绣,本教程的目的就是展示如何構(gòu)建此布局:
下面的代碼達(dá)到了展示如何簡(jiǎn)單的創(chuàng)建上述布局
目的:正如我們看到的,代碼里面甚至有一些變量和方法巩踏,可以為布局的各個(gè)部分賦予語義秃诵,這點(diǎn)做得很好,因?yàn)樗苁勾a更容易理解塞琼。
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
Widget titleSection = Container(
padding: const EdgeInsets.all(32),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
'Oeschinen Lake Campground',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
),
Text(
'Kandersteg, Switzerland',
style: TextStyle(
color: Colors.grey[500],
),
),
],
),
),
Icon(
Icons.star,
color: Colors.red[500],
),
Text('41'),
],
),
);
Color color = Theme.of(context).primaryColor;
Widget buttonSection = Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildButtonColumn(color, Icons.call, 'CALL'),
_buildButtonColumn(color, Icons.near_me, 'ROUTE'),
_buildButtonColumn(color, Icons.share, 'SHARE'),
],
),
);
Widget textSection = Container(
padding: const EdgeInsets.all(32),
child: Text(
'Lake Oeschinen lies at the foot of the Blüemlisalp in the Bernese '
'Alps. Situated 1,578 meters above sea level, it is one of the '
'larger Alpine Lakes. A gondola ride from Kandersteg, followed by a '
'half-hour walk through pastures and pine forest, leads you to the '
'lake, which warms to 20 degrees Celsius in the summer. Activities '
'enjoyed here include rowing, and riding the summer toboggan run.',
softWrap: true,
),
);
return MaterialApp(
title: 'Flutter layout demo',
home: Scaffold(
appBar: AppBar(
title: Text('Flutter layout demo'),
),
body: ListView(
children: [
Image.asset(
'images/lake.jpg',
width: 600,
height: 240,
fit: BoxFit.cover,
),
titleSection,
buttonSection,
textSection,
],
),
),
);
}
Column _buildButtonColumn(Color color, IconData icon, String label) {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: color),
Container(
margin: const EdgeInsets.only(top: 8),
child: Text(
label,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
color: color,
),
),
),
],
);
}
}
實(shí)際上菠净,代碼可以寫的更糟糕。下面是是我所不喜歡的典型的,所有代碼在一個(gè)Widget中的版本:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
Color color = Theme.of(context).primaryColor;
return MaterialApp(
title: 'Flutter layout demo',
home: Scaffold(
appBar: AppBar(
title: Text('Flutter layout demo'),
),
body: ListView(
children: [
Image.asset(
'images/lake.jpg',
width: 600,
height: 240,
fit: BoxFit.cover,
),
Container(
padding: const EdgeInsets.all(32),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
'Oeschinen Lake Campground',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
),
Text(
'Kandersteg, Switzerland',
style: TextStyle(
color: Colors.grey[500],
),
),
],
),
),
Icon(
Icons.star,
color: Colors.red[500],
),
Text('41'),
],
),
),
Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.call, color: color),
Container(
margin: const EdgeInsets.only(top: 8),
child: Text(
'CALL',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
color: color,
),
),
),
],
),
Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.near_me, color: color),
Container(
margin: const EdgeInsets.only(top: 8),
child: Text(
'ROUTE',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
color: color,
),
),
),
],
),
Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.share, color: color),
Container(
margin: const EdgeInsets.only(top: 8),
child: Text(
'SHARE',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
color: color,
),
),
),
],
),
],
),
),
Container(
padding: const EdgeInsets.all(32),
child: Text(
'Lake Oeschinen lies at the foot of the Blüemlisalp in the Bernese '
'Alps. Situated 1,578 meters above sea level, it is one of the '
'larger Alpine Lakes. A gondola ride from Kandersteg, followed by a '
'half-hour walk through pastures and pine forest, leads you to the '
'lake, which warms to 20 degrees Celsius in the summer. Activities '
'enjoyed here include rowing, and riding the summer toboggan run.',
softWrap: true,
),
),
],
),
),
);
}
}
在第二段代碼中毅往,我們書寫的這個(gè)Widget有一個(gè)大大的build
方法牵咙,這樣很難閱讀、理解和維護(hù)攀唯。
現(xiàn)在洁桌,讓我們看看如何將其重寫吧:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter layout demo',
home: const HomePage(),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter layout demo'),
),
body: ListView(
children: [
const _Header(),
const _SubHeader(),
const _Buttons(),
const _Description(),
],
),
);
}
}
class _Header extends StatelessWidget {
const _Header({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Image.asset(
'images/lake.jpg',
width: 600,
height: 240,
fit: BoxFit.cover,
);
}
}
class _SubHeader extends StatelessWidget {
const _SubHeader({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(32),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const _Title(),
const _SubTitle(),
],
),
),
const _Likes(),
],
),
);
}
}
class _Title extends StatelessWidget {
const _Title({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
'Oeschinen Lake Campground',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
);
}
}
class _SubTitle extends StatelessWidget {
const _SubTitle({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Text(
'Kandersteg, Switzerland',
style: TextStyle(
color: Colors.grey[500],
),
);
}
}
class _Likes extends StatelessWidget {
const _Likes({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Icon(
Icons.star,
color: Colors.red[500],
),
Text('41'),
],
);
}
}
class _Buttons extends StatelessWidget {
const _Buttons({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
const _Button(icon: Icons.call, text: 'CALL'),
const _Button(icon: Icons.share, text: 'ROUTE'),
const _Button(icon: Icons.share, text: 'SHARE'),
],
),
);
}
}
class _Button extends StatelessWidget {
const _Button({
Key key,
@required this.icon,
@required this.text,
}) : assert(icon != null),
assert(text != null),
super(key: key);
final IconData icon;
final String text;
@override
Widget build(BuildContext context) {
Color color = Theme.of(context).primaryColor;
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: color),
Container(
margin: const EdgeInsets.only(top: 8),
child: Text(
text,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
color: color,
),
),
),
],
);
}
}
class _Description extends StatelessWidget {
const _Description({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(32),
child: Text(
'Lake Oeschinen lies at the foot of the Blüemlisalp in the Bernese '
'Alps. Situated 1,578 meters above sea level, it is one of the '
'larger Alpine Lakes. A gondola ride from Kandersteg, followed by a '
'half-hour walk through pastures and pine forest, leads you to the '
'lake, which warms to 20 degrees Celsius in the summer. Activities '
'enjoyed here include rowing, and riding the summer toboggan run.',
softWrap: true,
),
);
}
}
您不覺得這樣更具有可讀性嗎?
這樣寫有什么好處革答?
我理解為什么教程不經(jīng)常請(qǐng)這樣做:它需要更多行的代碼(在這個(gè)示例中多了100行)战坤,并且人們可能想知道為什么要?jiǎng)?chuàng)建這么多的Widget。由于教程旨在專注于一個(gè)概念残拐,這樣編寫可能和他們的目標(biāo)適得其反途茫。這樣教學(xué)的后果就是,新手可能傾向于在他們的build
方法中放置一個(gè)大的Widget樹溪食,讓我們看看對(duì)布局的每個(gè)部分囊卜,使用一個(gè)單獨(dú)的Widget有什么好處:
可讀性
我們?yōu)椴季值拿總€(gè)部分創(chuàng)建一個(gè)Widget,每個(gè)Widget有他們自己的一個(gè)較小的build
方法错沃。由于您不必滾動(dòng)到小部件的末尾才能看到所有代碼栅组,因此這樣更易閱讀。
易理解
每個(gè)Widget都有與之角色相匹配的名字枢析,這被稱為語義命名
玉掸。這樣,當(dāng)我們?cè)陂喿x代碼時(shí)醒叁,能夠更容易的在腦海中映射出代碼的哪一部分和我們?cè)贏PP中所看到的內(nèi)容相匹配司浪。這里,我看到了兩個(gè)在易理解性方面的改進(jìn):
1.當(dāng)我們閱讀到某處引用這種Widget的地方時(shí)把沼,我們幾乎無需知道其實(shí)現(xiàn)啊易,即可知道它的功能。
2.在閱讀使用語義命名的Widget的build方法前饮睬,我們已經(jīng)對(duì)它的內(nèi)容有了一個(gè)大致了解租谈。
可維護(hù)性
假如你必須更換一個(gè)組件或者更改某個(gè)部分,則該組件將僅位于一個(gè)地方捆愁,由于每個(gè)組件都得到了很好的定義割去,與其他小組件分開,因此更改不易出錯(cuò)牙瓢。在你APP中的另一個(gè)頁面甚至另一個(gè)APP中·共享布局組件劫拗,也將變得更加容易。
性能
前面的所有原因應(yīng)該都足夠讓您采用這種方式來創(chuàng)建Flutter應(yīng)用程序矾克,除此之外還有一個(gè)優(yōu)勢(shì):提升了應(yīng)用程序的性能页慷,因?yàn)槊總€(gè)Widget都可以獨(dú)立進(jìn)行rebuild
(在之前的示例中憔足,如果我們僅用方法method隔離布局的部分,則不會(huì)這樣)酒繁。例如滓彰,假設(shè)我們點(diǎn)擊圖中紅色星星時(shí)需要增加數(shù)字,在重構(gòu)版本中州袒,我們可以將_Likes
制作成一個(gè)StatefulWidget
揭绑,并且在其內(nèi)部處理數(shù)字增加,當(dāng)我們點(diǎn)擊星星時(shí)郎哭,只有_Likes
Widget會(huì)被rebuild
他匪。而在第一個(gè)版本中,假如將MyApp
制作成一個(gè)StatefulWidget
夸研,整個(gè)Widget都將會(huì)被rebuild
邦蜜。
Flutter官方文檔中也對(duì)最佳實(shí)線做了說明。
在
State
中調(diào)用setState()
時(shí)亥至,所有子節(jié)點(diǎn)都將會(huì)被重建悼沈。因此將setState()
的調(diào)用放置到真正需要改變UI的子樹上。如果只更改包含子樹的一小部分姐扮,避免在子樹的上層調(diào)用setState()
絮供。When setState() is called on a State, all descendent widgets will rebuild. Therefore, localize the setState() call to the part of the subtree whose UI actually needs to change. Avoid calling setState() high up in the tree if the change is contained to a small part of the tree.
另一個(gè)優(yōu)勢(shì)是可以更多的使用const
關(guān)鍵字的功能,Widgets可以被被緩存和重用茶敏,如Flutetr文檔所述:
與重新創(chuàng)建新的Widget(配置相同)相比壤靶,重用Widget效率要高的多。
It is massively more efficient for a widget to be re-used than for a new (but identically-configured) widget to be created.
如何進(jìn)一步提升生產(chǎn)力
如您所見惊搏,在布局的每個(gè)語義部分創(chuàng)建一個(gè)Widget萍肆,我們編寫了很多代碼。我們可以使用Visual Studio Code中Dart擴(kuò)展提供的stless
和 stful
代碼段胀屿,但是它們不會(huì)生成const
構(gòu)造函數(shù)。
為了滿足我自己的需求包雀,我創(chuàng)建了新的代碼段宿崭,稱之為sless
和 sful
,這樣我的生產(chǎn)力比以往任何時(shí)候都更高才写。如果你想要在Visual Studio Code中使用它們葡兑,則必須遵從此文檔并添加一下內(nèi)容:
{
"Flutter stateless widget": {
"scope": "dart",
"prefix": "sless",
"description": "Insert a StatelessWidget",
"body": [
"class $1 extends StatelessWidget {",
" const $1({",
" Key key,",
" }) : super(key: key);",
"",
" @override",
" Widget build(BuildContext context) {",
" return Container(",
" $2",
" );",
" }",
"}"
]
},
"Flutter stateful widget": {
"scope": "dart",
"prefix": "sful",
"description": "Insert a StatefulWidget",
"body": [
"class $1 extends StatefulWidget {",
" const $1({",
" Key key,",
" }) : super(key: key);",
"",
" @override",
" _$1State createState() => _$1State();",
"}",
"",
"class _$1State extends State<$1> {",
" @override",
" Widget build(BuildContext context) {",
" return Container(",
" $2",
" );",
" }",
"}"
]
},
}
如何將這種做法與狀態(tài)管理結(jié)合起來?
如您所知赞草,在Flutter中有很多種狀態(tài)管理解決方案讹堤。我并不會(huì)列出哪些可以和這種編碼方式相結(jié)合的很好,而是會(huì)列出您在選擇最適合您的狀態(tài)管理方案時(shí)應(yīng)該知道的一些關(guān)鍵概念厨疙。
- 狀態(tài)對(duì)于Widget來說應(yīng)該是可直接獲取的洲守,而不應(yīng)該通過它的構(gòu)造函數(shù)來傳遞。否則,你將不得不通過一些原本不應(yīng)該關(guān)注的Widget來傳遞它梗醇。
- 僅當(dāng)與當(dāng)前Widget有關(guān)的狀態(tài)發(fā)生改變時(shí)知允,當(dāng)前Widget才應(yīng)該被重新rebuild。如果不是這種情況叙谨,Widget可能會(huì)被重建太多次温鸽,可能會(huì)有損性能。
我認(rèn)為手负,效果最好的解決方案是基于InheritedWidget或者相同概念的解決方案涤垫。例如,你可以參考Provider+X(X是能夠通知狀態(tài)更改的類)或者Maestro竟终。
結(jié)論
我堅(jiān)信這是編寫Flutter應(yīng)用程序的好方法蝠猬,希望您也堅(jiān)信。如果不是這樣衡楞,我對(duì)你的意見很感興趣吱雏!
從現(xiàn)在開始,記住下面口頭禪:“一切皆是Widget瘾境,但是不要把所有內(nèi)容都放置在一個(gè)Widget中歧杏!”。