每个Flutter开发者都应该知道的16个Dart技巧和窍门

247 阅读4分钟

在本教程中,我分享了我的顶级Dart技巧和窍门,这些技巧将改善你作为Flutter开发者的编码风格。

您可以使用它们来编写更简洁有效的代码,并充分利用Dart语言。

准备好了吗?让我们开始吧!

1.你知道吗?Dart支持字符串乘法。

这里有一个简单的程序,展示了如何用字符串乘法打印一棵圣诞树。

void main() {
  for (var i = 1; i <= 5; i++) {
    print('🎄' * i);
  }
}
// Output:
// 🎄
// 🎄🎄
// 🎄🎄🎄
// 🎄🎄🎄🎄
// 🎄🎄🎄🎄🎄

很酷吧? 😉

你可以用它来检查一个长的字符串在一个Text widget里面是怎样的。

Text('You have pushed the button this many times:' * 5)

2.需要同时执行多个Future?使用Future.wait。

考虑一下这个模拟的API类,它告诉我们COVID案例的最新数字。

// Mock API class
class CovidAPI {
  Future<int> getCases() => Future.value(1000);
  Future<int> getRecovered() => Future.value(100);
  Future<int> getDeaths() => Future.value(10);
}

要并发地执行所有这些期货,请使用Future.wait 。这需要一个列表或期货,并返回一个列表的未来

final api = CovidAPI();
final values = await Future.wait([
    api.getCases(),
    api.getRecovered(),
    api.getDeaths(),
]);
print(values); // [1000, 100, 10]

当这些期货是独立的,并且它们不需要按顺序执行时,这是理想的做法。

3.在你的Dart类中实现一个 "调用 "方法,使它们像函数一样可以调用。

下面是一个例子:PasswordValidator 类。

class PasswordValidator {
  bool call(String password) {
    return password.length > 10;
  }
}

因为该方法被命名为call ,我们可以声明一个类实例,并使用方法一样使用它。

final validator = PasswordValidator();
// can use it like this:
validator('test');
validator('test1234');
// no need to use it like this:
validator.call('not-so-frozen-arctic');

4.需要调用一个回调,但只有在它不为空的情况下才能调用?使用"?.call() "语法。

假设我们有一个自定义的widget类,当某个事件发生时应该调用一个onDragCompleted 回调。

class CustomDraggable extends StatelessWidget {
  const CustomDraggable({Key key, this.onDragCompleted}) : super(key: key);
  final VoidCallback? onDragCompleted;

  void _dragComplete() {
    // TODO: Implement me
  }
  @override
  Widget build(BuildContext context) {/*...*/}
}

为了调用该回调,我们可以写这样的代码。

  void _dragComplete() {
    if (onDragCompleted != null) {
      onDragCompleted();
    }
  }

但是有一个更简单的方法(注意使用?. )。

  Future<void> _dragComplete() async {
    onDragCompleted?.call();
  }

5.使用匿名函数和作为参数的函数

在Dart中,函数是一流的公民,可以作为参数传递给其他函数。

这里有一些代码,定义了一个匿名函数,并将其分配给一个sayHi 变量。

void main() {
  final sayHi = (name) => 'Hi, $name';
  welcome(sayHi, 'Andrea');
}

void welcome(String Function(String) greet,
             String name) {
  print(greet(name));
  print('Welcome to this course');
}

然后,sayHi 被传递给一个welcome 函数,该函数接收一个Function 参数并使用它来问候用户。

String Function(String) 是一个函数类型,它接受一个 参数并返回一个 。因为上面的匿名函数有相同的String String签名,它可以直接作为参数传递,也可以通过 变量传递。sayHi


这种编码方式在使用函数运算符(如map,where, 和reduce )时很常见。

例如,这里有一个简单的函数来计算一个数字的平方。

int square(int value) {
  // just a simple example
  // could be a complex function with a lot of code
  return value * value;
}

给定一个值的列表,我们可以通过映射来获得平方。

const values = [1, 2, 3];

values.map(square).toList();

这里我们把square 作为一个参数,因为它的签名正是map运算符所期望的。这意味着我们不需要用一个匿名函数来扩展它。

values.map((value) => square(value)).toList();

6.你可以在列表、集合和地图中使用 collection-if 和 spreads

当你把你的用户界面写成代码时,collection-if和spreads是非常有用的。

但是你知道吗,你也可以在地图中使用它们?

考虑一下这个例子。

const addRatings = true;
const restaurant = {
  'name' : 'Pizza Mario',
  'cuisine': 'Italian',
  if (addRatings) ...{
    'avgRating': 4.3,
    'numRatings': 5,
  }
};

这里我们声明了一个restaurant 地图,并且只在addRatingstrue 的情况下添加avgRatingnumRatings 的键值对。因为我们要添加一个以上的键值对,所以我们需要使用传播操作符(...)。

7.需要以空安全的方式遍历一个地图?使用`. entries`。

假设你有这个地图。

const timeSpent = <String, double>{
  'Blogging': 10.5,
  'YouTube': 30.5,
  'Courses': 75.2,
};

这里你可以写一个循环,使用所有的键值对运行一些代码。

for (var entry in timeSpent.entries) {
  // do something with keys and values
  print('${entry.key}: ${entry.value}');
}

通过在entries 变量上的迭代,你可以以空安全的方式访问所有的键值对。

这比这更简明,更不容易出错。

for (var key in timeSpent.keys) {
  final value = timeSpent[key]!;
  print('$key: $value');
}

上面的代码在读取数值时需要使用断言操作符(!),因为Dart不能保证某个键的数值存在。

8.使用命名的构造函数和初始化器列表以获得更符合人体工程学的API。

假设你想声明一个代表温度值的类。

你可以通过两个命名的构造函数使你的类的API不含糊,并同时支持摄氏和华氏温度。

class Temperature {
  Temperature.celsius(this.celsius);
  Temperature.fahrenheit(double fahrenheit)
    : celsius = (fahrenheit - 32) / 1.8;
  double celsius;
}

这个类只需要一个存储变量来表示温度,并使用一个初始化器列表来将华氏温度转换为摄氏温度。

这意味着你可以像这样声明温度值。

final temp1 = Temperature.celsius(30);
final temp2 = Temperature.fahrenheit(90);

9.获取器和设置器

在上面的Temperature 类中,celsius 被声明为一个存储变量。

但是用户可能更喜欢用华氏温度来获取设置温度。

这很容易通过getters和setters来实现,它们允许你定义计算变量。下面是更新后的类。

class Temperature {
  Temperature.celsius(this.celsius);
  Temperature.fahrenheit(double fahrenheit)
    : celsius = (fahrenheit - 32) / 1.8;
  double celsius;
  double get fahrenheit
    => celsius * 1.8 + 32;
  set fahrenheit(double fahrenheit)
    => celsius = (fahrenheit - 32) / 1.8;
}

这使得用华氏温度或摄氏温度来获取或设置温度变得很容易。

final temp1 = Temperature.celsius(30);
print(temp1.fahrenheit);
final temp2 = Temperature.fahrenheit(90);
temp2.celsius = 28;

一句话:使用命名的构造函数、getters和setters来改善你的类的设计。

10.对未使用的函数参数使用下划线

在Flutter中,我们经常使用需要函数参数的小部件。一个常见的例子是ListView.builder

class MyListView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemBuilder: (context, index) => ListTile(
        title: Text('all the same'),
      ),
      itemCount: 10,
    );
  }
}

(context, index) 在这种情况下,我们没有使用itemBuilder 中的参数。因此我们可以用下划线代替它们。

ListView.builder(
  itemBuilder: (_, __) => ListTile(
    title: Text('all the same'),
  ),
  itemCount: 10,
)

注意:这两个参数是不同的(___),因为它们是独立的标识符

11.需要一个只能被实例化一次的类(又称单子)?使用一个静态实例变量和一个私有构造函数。

单子的最重要的属性是在你的整个程序中只能有一个实例。这对于像文件系统这样的东西的建模很有用。

// file_system.dart
class FileSystem {
  FileSystem._();
  static final instance = FileSystem._();
}

要在Dart中创建一个单子,你可以声明一个命名的构造函数,并使用_ 语法将其私有化。

然后你可以用它来创建你的类的一个静态最终实例。

因此,其他文件中的任何代码都只能通过instance 变量来访问这个类。

// some_other_file.dart
final fs = FileSystem.instance;
// do something with fs

注意:如果你不小心,单子会导致很多问题。在使用它们之前,请确保你了解其弊端。

12.需要一个独特项目的集合?使用一个集合而不是一个列表。

Dart中最常用的集合类型是List

但是列表可以有重复的项目,有时这不是我们想要的。

const citiesList = [
  'London',
  'Paris',
  'Rome',
  'London',
];

只要我们需要一个唯一值的集合,我们就可以使用一个Set (注意使用final )。

// set is final, compiles
final citiesSet = {
  'London',
  'Paris',
  'Rome',
  'London', // Two elements in a set literal shouldn't be equal
};

上面的代码产生了一个警告,因为London 被包含了两次。如果我们尝试用一个const 集合做同样的事情,我们会得到一个错误,我们的代码不能编译。

// set is const, doesn't compile
const citiesSet = {
  'London',
  'Paris',
  'Rome',
  'London', // Two elements in a constant set literal can't be equal
};

当我们使用集合时,我们可以访问有用的API,如union,difference, 和intersection

citiesSet.union({'Delhi', 'Moscow'});
citiesSet.difference({'London', 'Madrid'});
citiesSet.intersection({'London', 'Berlin'});

一句话:当你创建一个集合时,问问你自己是否希望它的项目是唯一的,并考虑使用一个集合。

13.如何使用try, on, catch, rethrow, finally

try 和 ,在处理基于Future的API时是非常理想的,这些API可能在出错时抛出一个异常。catch

这里有一个完整的例子,展示了如何充分利用它们的作用。

Future<void> printWeather() async {
  try {
    final api = WeatherApiClient();
    final weather = await api.getWeather('London');
    print(weather);
  } on SocketException catch (_) {
    print('Could not fetch data. Check your connection.');
  } on WeatherApiException catch (e) {
    print(e.message);
  } catch (e, st) {
    print('Error: $e\nStack trace: $st');
    rethrow;
  } finally {
    print('Done');
  }
}

一些注意事项。

  • 你可以添加多个on 子句来处理不同类型的异常。
  • 你可以有一个后备的catch 子句来处理所有不符合上述任何类型的异常。
  • 你可以使用rethrow 语句将当前的异常抛到调用堆栈中**,同时保留堆栈跟踪**。
  • 你可以使用finally ,在Future 完成后运行一些代码,不管它是成功还是失败。

如果你正在使用或设计一些基于Future的API,请确保根据需要处理异常。

14.常见的Future构造函数

DartFuture 类带有一些方便的工厂构造函数。Future.delayed,Future.valueFuture.error

我们可以使用Future.delayed 来创建一个等待一定延迟的Future 。第二个参数是一个(可选)匿名函数,你可以用它来完成一个值或抛出一个错误。

await Future.delayed(Duration(seconds: 2), () => 'Latte');

但有时我们想创建一个立即完成的Future

await Future.value('Cappuccino');
await Future.error(Exception('Out of milk'));

我们可以使用Future.value 来成功完成一个值,或者使用Future.error 来完成一个错误。

你可以使用这些构造函数来模拟来自你的基于Future的API的响应。在你的测试代码中编写模拟类时,这很有用。

15.常见的流构造函数

Stream类还附带了一些方便的构造函数。下面是最常见的。

Stream.fromIterable([1, 2, 3]);
Stream.value(10);
Stream.empty();
Stream.error(Exception('something went wrong'));
Stream.fromFuture(Future.delayed(Duration(seconds: 1), () => 42));
Stream.periodic(Duration(seconds: 1), (index) => index);
  • 使用Stream.fromIterable ,从一个值的列表中创建一个Stream
  • 如果你只有一个值,请使用Stream.value
  • 使用Stream.empty 来创建一个空流。
  • 使用Stream.error ,创建一个包含错误值的流。
  • 使用Stream.fromFuture 创建一个只包含一个值的流,并且该值在未来完成时可以使用。
  • 使用Stream.periodic 来创建一个周期性的事件流。你可以指定一个Duration 作为事件之间的时间间隔,并指定一个匿名函数来生成流中的每个值的索引。

16.同步和异步生成器

在Dart中,我们可以将同步生成器定义为一个返回Iterable 的函数。

Iterable<int> count(int n) sync* {
  for (var i = 1; i <= n; i++) {
    yield i;
  }
}

这使用了sync* 语法。在这个函数中,我们可以 "生成 "或yield 多个值。当函数完成时,这些值将作为一个Iterable 返回。


另一方面,一个异步生成器是一个返回Stream 的函数。

Stream<int> countStream(int n) async* {
  for (var i = 1; i <= n; i++) {
    yield i;
  }
}

这使用了这种async* 语法。在函数内部,我们可以像在同步情况下一样,yield 值。

但如果我们愿意,我们可以在基于Future的API上await ,因为这是一个异步发生器。

Stream<int> countStream(int n) async* {
  for (var i = 1; i <= n; i++) {
    // dummy delay - this could be a network request
    await Future.delayed(Duration(seconds: 1));
    yield i;
  }
}

结语

我希望您喜欢我为Flutter开发者提供的顶级Dart技巧和窍门。

我对您的挑战是什么?

编码愉快!