原文地址:How refactoring improve readability, maintainability and performance optimization of your Flutter application
原作者:Jonathan Monga
读后感:
这篇文章是关于如何组织代码结构的,如何编写Flutter代码,才能使代码有更好的可读性、可维护性,并且带来更好的性能呐,之前也翻译过一篇相似的文章Flutter Widget瘦身,两篇文章看完,想必会给你带一些收益。
前言
我们都同意widget 树是你在UI中所获得的东西,并且同意它完全是关于Flutter widget的,因此你可以将你的widget相互嵌套。无论你的UI是简单还是复杂,当你的UI简单时,即使几周后回来阅读你的代码,它也很容易阅读,并且性能很好,因为它展示的内容很少。但是当你的应用界面比较复杂时,这会促使你嵌套大量的widget,代码的可读性、可维护性降低,程序的效率也会降低。
我知道,对于初学者来说,很容易没有重构代码的文化,一旦注意力转移到其他事情上,初学者就会满足于widget的嵌套、嵌套、嵌套,这就是产生很深的widget树的原因。对于像我这样的新手Flutter开发者来说,这是很常见的现象,好吧,既然问题已经暴露出来了,我们怎么避免?如何以一种不陷入非常深的widget树的方式进行编码呐?
在我之前已经有不少人探讨过这个问题了,但我认为还是值得在花点时间再谈论一下。这个经常困扰我们的问题的答案就是代码重构。既然你已经得到了答案,那么就不要再拖延重构你的代码啦。下面我将用不同的技术,向你展示如何进行代码重构。
在向你展示如何重构代码之前,让我们使用此UI的代码:
Weather Stats.png
这个很漂亮的UI来自于https://github.com/JideGuru/weather_neumorphism_ui,这里并没有恶意,我不认为我比Olusegun Festus Babajide更厉害,以至于我有权利对他的代码做点评。同样你如果找到一些我的代码,我相信,你也会发现很多值得抱怨的地方。
不,这不是下流或者傲慢的行为,我将要做的无非只是专业的评论,这是我们都应该乐于做的事情,当完成时,我们应该欢迎它。通过这样发表评论,可以促使我们学习,医生这样做、飞行员这样做、律师这样做,我们程序员也应该学习这样做。补充一点:Olusegun Festus Babajide不仅是一位很好的Flutter开发人员,并且有勇气和善意,愿意将他的代码免费提供给整个社区,他把它提供给所有人看,并邀请公众使用和监督,这样做很赞。
这是现在的代码:
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,
),
),
),
),
],
),
);
}
}
wow.png
那么让我们看看如何使这一切井然有序。
1、使用方法重构
我想你在某些地方已经看到了这种技术而没有意识到。该技术只是将widget作为方法调用的返回值,进行封装使用。假设在Flutter中一切都是widget,那么任何参与组成UI的类都继承自Widget类,该方法的返回值可能是任何一个widget类或者一些特定的类,例如容器类container、row、column等。
继续往下看,方法中的Widget可以依赖父widget的BuildContext
实例或对象。这就是问题的来源,记住BuildContext
对象知道widget在widget tree中的位置。既然此方法依赖于主BuildContext
,那么当父widget重绘时,此方法也将强制重新组装、重新创建或者重绘其内部的widget。或者如果该方法也调用了其他依赖于父widget的BuildContext
的方法,也会带来副作用,所有方法绘制他们的widget的次数将会和绘制父widget的次数一样多。无论哪种情况,这都不是我们重构后所期望的行为。
使用这种方法,我们将widget分割开来,这当然能够带来可读性及可维护性的提升,但是对于性能优化,并没有什么用处。当widget数量增加时,我们UI的性能在配置更改期间将会下降,例如屏幕旋转。
下面是两个方法的示例:
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也一样),并按照以下步骤进行重构:
1.打开任何.dart文件
2.将光标放在第一个widget上,然后右击,在我的场景中,是在Row、Container或者Column上。
3.选中Refactor >Extract Method
4.在提取方法的弹窗中,输入_buildRow
作为方法名,注意方法前的下划线,让Dart知道这是一个私有方法。
5.Row widget现在替换为了_b方法uildRow()
,滚动到代码底部,方法和widget都得到了很好的重构。
6.继续重构其他的Rows、Columns、Containers和Stack Widget。
这种方式增加了代码的可读性,widget树的主要组成部分被分割成了非常简单的方法,这种方式的好处是纯粹和简单的代码可读性和可维护性,作为回报失去了优化性能,如果你想看更多内容,请转到底部的引用部分。
2、使用局部变量重构
和第一种重构方式有些相识,只不过这里使用局部变量,包括使用final变量初始化widget。在这里一样是将widget树的主要部分分割成多个,这增加了代码的可读性和可维护性。
在这种情况下,虽然我们的widget使用final来初始化变量,但是仍然使用的是父widget的BuildContext,当框架重绘父widget时,局部变量也将会被重绘。这增加了可读性和可维护性,你的widget树将会变浅,但是不会优化性能。
下面是一个带有常量的的重构代码示例:
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也一样),并按照以下步骤进行重构:
1.打开任何.dart文件
2.将光标放在第一个widget上,然后右击,在我的场景中,是在Row、Container或者Column上。
3.选择 Refactor > Extract Local Varialble
4.在我们的例子中,将局部变量命名为rowConstant
,注意我们使用final进行修饰,告诉Dart这是一个常量。
5.Row widget替换为了rowConstant
最终变量。滚动带代码顶部,局部变量和widget都得到了很好的重构。
6.继续重构其他的Rows、Columns、Containers和Stack Widget。
3、使用widget class重构
这种方式允许你使用继承自StatelessWidget
或者StatefullWidget
的类,来隔离widget子树,还允许你创建可重用的widget,并且可以将它们分布在相同或不同的dart文件中,这样你就可以在程序的任何地方引入或者使用这些文件。警告!这些类的构造函数必须以const
关键字开头,再次感谢Dart,以const
开头声明的构造函数,会告诉Dart缓存和重用这些widget,与此相反的是其它widget将会被重绘。
当你要创建此类的对象时,不要忘记使用const
关键字。通过这样做,当其他widget在widget树中更改状态时,此widget将不会被重建。如果遗漏了const
关键字,父widget重绘多少次,我们的widget也将会跟着重绘多少次,因此需要留心。
这样的widget类依赖它自己的BuildContext
,而不是像重构成方法或者变量的那样依赖于父widget的。BuildContext
负责管理widget在widget树中的位置。
现在让我们看看使用这种方式的小例子:
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也一样),并按照以下步骤进行重构:
1.打开任何.dart文件
2.将光标放在第一个widget上,然后右击,在我的场景中,是在Row、Container或者Column上。
3.选择 Refactor > Extract Widget
4.在我们的例子中,将类名命名为PaddingWidget
。
5.Padding widget替换为了PaddingWidget
类。滚动带代码底部,类和widget都得到了很好的重构。
6.继续并重构其他Padding(PaddingWidgets class)、Rows(RowsAndColumnWidget class)widget。
抱歉,有太多内容需要消化,我总结一下:你不仅在可读性和可维护性上有所收获,并且性能也会有很大提升。因为当父widget重绘时,并不是所有widget都会被重绘,他们只构建一次。
结论
在这篇文章中,你了解到了widget树是widget嵌套的结果,随着widget的增加,widget树会迅速扩展并且降低代码的可读性以及可管理性,这被称之为整个widget树。为了提高代码的可读性和可管理性,你可以将widget分割成独立的widget类,创建一个浅的widget树。在每个程序中,你都应该尽量保持widget树层级浅。通过使用widget类的重构方式,你可以在Flutter子树的重构中获益,这将会提升性能。
感谢阅读我的文章,欢迎进行评论。
Refactoring a Flutter Project -- a story about progression and decisions
网友评论