背景
初入Flutter,都会带有之前开发经验的一种思维定式,如果改变这一现状,就必须搞清楚Flutter的声明式编程的编程思想,,这篇文章我将从编程范式、如何正确使用widget,以及Flutter的最佳实践与技巧三个方便解读,帮助大家写出地道的Flutter的代码。
两种编程范式
命令式编程:命令“机器”如何去做事情(how),这样不管你想要的是什么(what),它都会按照你的命令实现。是函数式编程的意思,更强调流程及步骤。强调的操作细节,即Step by Step,而不是对象之间的关系
声明式编程:告诉“机器”你想要的是什么(what),让机器想出如何去做(how)。更强调语义的层面,而不是细节,不强调具体流程的细节,即更强调What,描述对象之间的关系,而不是具体的操作细节
这样说可能不太容易理解,我们就拿 Andorid 和 IOS 来举个栗子:
-
iOS编程范式
很纯粹的命令式,new view
,addsubview
-
安卓编程范式:
算是半命令式,xml 声明了 UI,这是声明式的部分;但程序运行时如果要修改某个 view,仍是取到这个view,再去命令式地修改。
-
Flutter编程范式:
Flutter 的 UI 框架吸取了 react 的理念,即 UI 是关于状态的函数。
如何正确的使用 widget
-
use smallwidgets
- 构建更加健壮/安全
- 构建更高效,避免不必要的 bulid,提高性能
- widget tree 更具有清晰干净
// like this
const MyPage extends StatelessWidget {
@override
Widget build(context) {
return Column(
children: const [ // <- I am now able to give a const list to the Column
TopBar(),
MyWidget1(),
MyWidget2(),
],
);
}
}
-
build 中减少复杂业务
- 避免在 build 使用过度变量的变量
// Don’t
bool arg1, arg2, arg3
const MyPage extends StatelessWidget {
@override
Widget build(context) {
if (arg1) {
// do something
} else if (arg2) {
// do something
} else if (arg3) {
// do something
} else {
// do something
}
}
}
// Do
enum MyPageState { arg1 , arg2, arg3, default,}
const MyPage extends StatelessWidget {
@override
Widget build(context) {
switch (type) {
case MyPageState.arg1:
// do something
case MyPageState.arg2:
// do something
case MyPageState.arg3:
// do something
case MyPageState.default:
// do something
}
}
}
- 避免在 build 中做复杂/耗时的业务逻辑
// Don’t
Future<bool?> isReady() async{
final cache = await DefaultCacheManager().getFileFromCache(assetUrl);
return cache?.file.path.isNotEmpty == true;
}
const MyPage extends StatelessWidget {
@override
Widget build(context) {
if (isReady()) {
// do something
}
}
}
// Do
bool get isReady {
return cachePath?.isNotEmpty == true && isValid;
}
Future<void> preloadAsset() async {
final cache = await DefaultCacheManager().getFileFromCache(assetUrl);
cachePath = cache?.file.path;
}
const MyPage extends StatelessWidget {
@override
void initState() {
super.initState();
preloadAsset();
}
@override
Widget build(context) {
if (isReady) {
// do something
}
}
}
-
合理使用StatefulWidget
通过区分场景适当选择StatefulWidget/StatelessWidget,避免滥用StatefulWidget
-
使用状态管理框架
- Provider
- Riverpod
-
函数判断优先级
- 子widget逻辑在子widget里判断
// Don’t
const MyPage extends StatelessWidget {
bool isShowTopBar;
@override
Widget build(context) {
return Column(
children: const [
if(isShowTopBar) TopBar(),
MyWidget1(),
MyWidget2(),
],
);
}
}
// Do
const MyPage extends StatelessWidget {
@override
Widget build(context) {
return Column(
children: const [
TopBar(),
MyWidget1(),
MyWidget2(),
],
);
}
}
const TopBar extends StatelessWidget {
@override
Widget build(context) {
return if(model != null) ? Column(
children: const [
MyWidget(),
],
):const SizedBox.shrink();;
}
}
- 组合判断,把简单的放在前面
举一个例子:
为什么红色的要改为绿色的?
对简单的逻辑,我们要放前面,不要混在一起,看似兼容性更强实际降低了部分场景的性能。
Flutter 最佳实践和技巧
-
文件使用相对导入
lib
当同时使用相对和绝对导入时,当同一个类从两种不同的方式导入时,可能会造成混淆。为了避免这种情况,我们应该在文件夹中使用相对路径lib/
。
// Don't
import 'package:demo/src/utils/dialog_utils.dart';
// Do
import '../../../utils/dialog_utils.dart';
-
指定类成员的类型
当成员的值类型已知时,尽可能避免使用var
。
//Don't
var item = 10;
final car = Car();
const timeOut = 2000;
//Do
int item = 10;
final Car bar = Car();
String name = 'john';
const int timeOut = 20;
为什么是已知类别,避免使用var?
对于全局变量、局部变量、静态属性或成员属性,通常可以通过初始值推断出来它们的类型,但是如果没有初始化值的话会导致类型推断失败。
// Don't
var parameters;
if (node is Constructor) {
parameters = node.signature;
} else if (node is Method) {
parameters = node.parameters;
}
//Do
List<AstNode> parameters;
if (node is Constructor) {
parameters = node.signature;
} else if (node is Method) {
parameters = node.parameters;
}
-
避免使用
as
,使用is
运算符
在 dart 中 as 更像是一个断言,如果值类型不匹配 as 会导致运行时异常。通常,as
如果无法进行强制类型转换,则类型转换运算符会引发异常。为避免引发异常,可以使用is
//Don't
(item as Animal).name = 'Lion';
//Do
if (item is Animal)
item.name = 'Lion';
-
使用
??
和?.
//Don't
v = a == null ? b : a;
//Do
v = a ?? b;
//Don't
v = a == null ? null : a.b;
//Do
v = a?.b;
-
使用级联运算符
如果我们想对同一个对象执行一系列操作,那么我们应该使用 Cascades(..) 运算符。
// Don't
var path = Path();
path.lineTo(0, size.height);
path.lineTo(size.width, size.height);
path.lineTo(size.width, 0);
path.close();
// Do
var path = Path()
..lineTo(0, size.height)
..lineTo(size.width, size.height)
..lineTo(size.width, 0)
..close();
-
使用 async/await 过度使用futures回调
异步代码很难阅读和调试。同步代码async/await提高了可读性。
// Don’t
Future<int> countActiveUser() {
return getActiveUser().then((users) {
return users?.length ?? 0;
}).catchError((e) {
log.error(e);
return 0;
});
}
// Do
Future<int> countActiveUser() async {
try {
var users = await getActiveUser();
return users?.length ?? 0;
} catch (e) {
log.error(e);
return 0;
}
}
-
在Widget中使用 Const
Widget在 setState 调用时不会改变时,我们应该将其定义为常量。这样避免不必要的build,从而提高性能。
-
使用 ListView.builder 获取长列表
在使用无限列表或非常大的列表时,通常建议使用ListView
构建器以提高性能。
默认ListView
构造函数一次构建整个列表。ListView.builder
创建一个惰性列表,当用户向下滚动列表时,Flutter 会按需构建小部件。