0%

Flutter 日常开发小技巧

收集了一些日常开发中会用到技巧或者语法糖,简化代码,提升开发效率。

函数扩展

Dart 中,我们可以为扩展任何数据类型,而任何函数都能转换成数据类型,所以,我们也可以在 Dart 中扩展任何类型的函数,函数扩展是通过扩展方法的方式实现的,通过使用 extension 关键字。其目的就是为了提高代码的可读性和可维护性。

如下面的例子,为 String 类型添加了一个名为 capitalize() 的方法,用于将字符串的首字母大写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
extension StringExtensions on String {
String capitalize() {
if (isEmpty) {
return this;
}
return substring(0, 1).toUpperCase() + substring(1);
}
}

class DartTipsPage extends StatefulWidget {
const DartTipsPage({Key? key}) : super(key: key);

@override
State<DartTipsPage> createState() => _DartTipsPageState();
}

class _DartTipsPageState extends State<DartTipsPage> {
@override
void initState() {
super.initState();

String text = "hello";
print(text.capitalize()); // 输出 "Hello"
}
}

还可以直接给函数类型创建 extension,在函数之上添加功能。如下例,为函数类型添加延迟调用的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
extension on VoidCallback {
Future<void> delayedCall(
Duration duration,
) =>
Future<void>.delayed(duration, this);
}

class DartTipsPage extends StatefulWidget {
const DartTipsPage({Key? key}) : super(key: key);

@override
State<DartTipsPage> createState() => _DartTipsPageState();
}

class _DartTipsPageState extends State<DartTipsPage> {
@override
void initState() {
super.initState();

_click.delayedCall(const Duration(seconds: 1));
}

_click(){
print("delayedCall");
}
}

这里的 VoidCallback 是一种已经转换为数据类型的函数类型,所以我们也可以为其添加扩展。

安全数组

Flutter 开发时,碰到数组越界或者访问数组中不存在的元素情况时,会引起运行时错误,如:

1
2
List<int> numbers = [1, 2, 3, 4, 5];
print(numbers[5]);

尝试访问索引为 5 的元素,但数组长度只有 5 个元素,就会触发数组越界错误,Flutter 报错信息:

1
RangeError (index): Index out of range: index should be less than 5: 5

当然也可以使用 try-catch 来捕获异常,但使用起来有些繁琐,很多时候问题没法儿及时发现和修复,那有没有更好的办法呢?当然有,自定义一个继承自 ListBase 的类:SafeList,其代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import 'dart:collection';

class SafeList<T> extends ListBase<T> {
final List<T> _list;
// 调用 set 方法修改 length 的时候,若大于当前数组的长度时,就使用 defaultValue 填充
final T defaultValue;

// 在数组中查询不存在的值时,即数组越界时,返回 absentValue
final T absentValue;

SafeList({
required this.defaultValue,
required this.absentValue,
List<T>? values,
}) : _list = values ?? [];

@override
T operator [](int index) => index < _list.length ? _list[index] : absentValue;

@override
void operator []=(int index, T value) => _list[index] = value;

@override
int get length => _list.length;

@override
T get first => _list.isNotEmpty ? _list.first : absentValue;

@override
T get last => _list.isNotEmpty ? _list.last : absentValue;

@override
set length(int newValue) {
if (newValue < _list.length) {
_list.length = newValue;
} else {
_list.addAll(List.filled(newValue - _list.length, defaultValue));
}
}
}

调用代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const notFound = 'Value Not Found';
const defaultString = '';

final SafeList<String> myList = SafeList(
defaultValue: defaultString,
absentValue: notFound,
values: ['Flutter', 'iOS', 'Android'],
);

print(myList[0]); // Flutter
print(myList[1]); // iOS
print(myList[2]); // Android
print(myList[3]); // Value Not Found

myList.length = 5;
print(myList[4]); // ''

myList.length = 0;
print(myList.first); // Value Not Found
print(myList.last); // Value Not Found

获取图片宽高比

获取图片的宽高比在 Flutter 开发中是一个很常见的需求,可以帮助我们更加精准的地控制UI布局,特别是需要设置图片宽高的时候,做响应式布局的时候也需要根据图片的宽高比动态调整UI布局。

ImageStreamaddListener 回调中使用的 Completer,用来处理Future对象的完成和结果传递。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import 'dart:async' show Completer;
import 'package:flutter/material.dart' as material
show Image, ImageConfiguration, ImageStreamListener;

extension GetImageAspectRatio on material.Image {
Future<double> getAspectRatio() {
final completer = Completer<double>();
image.resolve(const material.ImageConfiguration()).addListener(
material.ImageStreamListener(
(imageInfo, synchronousCall) {
final aspectRatio = imageInfo.image.width / imageInfo.image.height;
imageInfo.image.dispose();
completer.complete(aspectRatio);
},
),
);
return completer.future;
}
}

调用代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class _DartTipsPageState extends State<DartTipsPage> {
@override
void initState() {
super.initState();
_getImageAspectRatio(); // 打印结果:2.8160919540229883
}

_getImageAspectRatio() async {
Image wxIcon = Image.asset("images/wx_icon.png");
var aspectRatio = await wxIcon.getAspectRatio();
print(aspectRatio);
}

@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xffffffff),
);
}
}

自动添加间距

FlutterRow 或者 Column 需要在元素之间添加相同间距的时候经常会有这样的代码:

1
2
3
4
5
6
7
8
9
10
11
12
Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
_hasFirstSpacing ? SizedBox(width: 20,) : Container(),
Text("Flutter"),
SizedBox(width: 20,),
Text("ReactNative"),
SizedBox(width: 20,),
Text("uni-app"),
],
),

而且有的情况是第一个间距是否添加还需要根据条件来判断,这样重复的代码太多,可读性也比较差,有没有一种更优雅的方式呢?来看看下面的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class RowWithSpacing extends Row {
RowWithSpacing({
super.key,
double spacing = 8,
// 是否需要在开头添加间距
bool existLeadingSpace = false,
super.mainAxisAlignment,
super.mainAxisSize,
super.crossAxisAlignment,
super.textDirection,
super.verticalDirection,
super.textBaseline,
List<Widget> children = const [],
}) : super(
children: [
...existLeadingSpace ? [SizedBox(width: spacing)] : <Widget>[],
...children.expand(
(w) => [
w,
SizedBox(width: spacing),
],
)
],
);
}

其实就是继承 Row ,在自定义类 RowWithSpacing 内部自动添加 spacing,将上面的例子改成使用 RowWithSpacing ,代码是不是也清爽多了。

1
2
3
4
5
6
7
8
9
10
11
RowWithSpacing(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
existLeadingSpace: true,
spacing: 20,
children: [
Text("Flutter"),
Text("ReactNative"),
Text("uniapp"),
],
),

以上这些技巧代码量都不多,简单轻便,拿来就用,日常开发中也能带来不少方便,代码和测试的 Demo 放在了老地方,不知道的先关注公众号后,回复源码就能看到。原创不易,您的关注是我更新下去最大的动力。

Flutter技术实践