超赞的 Flutter 技巧和窍门

提示 1:stless & stful

我们可以输入stlessstful,然后我们就会得到自动完成建议,分别生成无状态的 Flutter Widget 或有状态的 Flutter Widget。

statful

提示 2:If Null 操作符 (??)

?? 检查某项是否为null。如果不是null,它会返回自身的值;但如果它是null,它会返回??之后的值。

return abc??10; //如果 abc 是 null 则返回 10,否则返回其自身的值,

它也有一个简写赋值,当它为 null 时。

abc??=5 //如果 abc 是 null 则将其赋值为 5

testOldMethod() {
  print("NullChecking in old way");
  var abc;
  
  if (abc == null) {
    print("It's null");
  } else {
    print(abc);
  }

  if (abc == null) {
    abc = 5;
  }
}

testIfNullOperator() {
  print("NullChecking with if Null Operator");
  var abc;

  print(abc ?? "It's null");
  abc ??= 5;
  print(abc ?? "It's null");
}

输出

NullChecking in old way
It's null
5
--------------------
NullChecking with if Null Operator
It's null
5

提示 3:内部函数

我们可以在一个函数内部定义另一个函数。

这是为了将内部函数与外部函数之外的所有其他东西隔离开来。

main() {
  String getName() {

    String getFirstName() { return "Laxman"; }

    String getLastName() { return "Bhattarai"; }

    return getFirstName() + " " + getLastName();
  }

  print(getName());
}

输出

Laxman Bhattarai

提示 4:..级联..链式..流式 API

我们可以链接方法/成员调用,而无需从方法()、getter() 和 setter()返回this,可以使用级联操作符 (..)

请在 Dartpad 中尝试

之前

class User {
  String name;
  int age;

  User({this.name = "Foo", this.age = 0});

  User withName(String name) {
    this.name = name;
    return this;
  }

  User withAge(int age) {
    this.age = age;
    return this;
  }

  void printId() => print("$name is $age years old.");
}

main() {
  User()
  .withAge(27)
  .withName("Laxman")
  .printId();
}

可以替换为

class User {
  String name;
  int age;

  void printId() => print("$name is $age years old.");
}

main() {
  User()
  ..age = 27
  ..name = "Laxman"
  ..printId();
}

提示 5:Dart 数据类

Dart 默认不支持数据类,但通过插件,我们可以轻松生成数据类(由工具实现了copyWith()fromMap()toMap()命名构造函数toString()hashCode()equals() 方法)。

?❗️注意❗️?您的光标应位于您想要生成数据类的类内部。

dataclass

下载插件

适用于 Android Studio

适用于 VS Code

提示 6:RichText Widget

如果您想在单个文本中拥有不同样式的文本?不要费心或尝试使用Text()进行 hack,而是使用带有TextSpan()RichText()

在 dartpad 上尝试

观看 YouTube 演示

richtext

提示 7:Spacer Widget

使用带有特定高度/宽度的 Container 在 Widgets 之间创建响应式间距?它在一屏上可能看起来不错,但在不同屏幕尺寸下不会看起来一样。

Spacer Widget 来拯救您。使用Spacer(flex: )代替Container(width: / height: )

我怎么会不知道这个 Widget 呢?这会拯救很多生命?

在 dartpad 上尝试

观看 YouTube 演示

spacer

提示 8:ListView.separated()

如果您像我一样,一直在 ListItem 的底部添加带有maxwidthContainer()来放置分隔线,那么您一直以来都做错了。

Flutter 有ListView.separated,正是为此目的。除了在使用ListView.builder时已经传递的参数外,我们还需要提供separatorBuilder

奖励????:您不必检查项目是否是最后一个,以免在最后一个项目之后绘制分隔线。

在 dartpad 上尝试

ListView.separated(
  itemCount: 25,
  separatorBuilder: (BuildContext context, int index) => Divider(
    thickness: 1,
  ),
  itemBuilder: (BuildContext context, int index) {
    return ListTile(
      title: Text(
        'Index Number $index',
      ),
    );
  },
);

提示 9:将函数作为参数传递

我们可以像传递变量一样,简单地将一个函数作为参数传递。当我们在调用函数时想调用传递的函数,我们只需在其后加上(),如果它接受参数,则一并传递。

在 dartpad 上尝试

void main() {
  f2(f1, 3);
  f2(f1, 4);
  f2(f1, 7);
}

f1(int venOrOdd) {
  print("$evenOrOdd is ${evenOrOdd % 2 == 0 ? "Even" : "Odd"}");
}

f2(Function(int) evenOrOddFunction, int argumentToPass) {
  evenOrOddFunction(argumentToPass);
}

输出

3 is Odd
4 is Even
7 is Odd

提示 10:相对导入:导入我们 lib 包中.dart文件的正确方法

是否曾想过导入自己包中文件的正确方法?

优先使用相对导入而非绝对导入。

为什么?

  • 它更短、更简洁。
  • 我们可以轻松地区分我们的文件和第三方文件。
  • 这很合理,不是吗?
my_package
└─ lib
   ├─ src
   │  └─ utils.dart
   └─ api.dart

如果api.dart想导入utils.dart,它应该这样做:

import 'src/utils.dart';

而不是

import 'package:my_package/src/utils.dart';

提示 11:重用文本样式

厌倦了每次要自定义Text时都要定义textStyle更糟糕的是,如果您有多个主题(深色、浅色、全黑主题等)。

只需使用

Theme.of(context).textTheme.title

其中textTheme中还有其他样式,如title

在带主题的示例中,在 dartpad 上尝试

Text(
  "Title",
  style: Theme.of(context).textTheme.title,
)

提示 12:使用字面量初始化可增长集合

如果我们初始化可增长集合,请使用字面量初始化而不是构造函数。

// Good
var points = [];
var addresses = {};

// Bad
var points = List();
var addresses = Map();

// With type argument

// Good
var points = <Point>[];
var addresses = <String, Address>{};

// Bad
var points = List<Point>();
var addresses = Map<String, Address>();

提示 13:箭头函数

我们可以在 dart 中使用箭头=>成员(函数、getter、setter)。

如果声明不是单行,我不会使用=>。但几行是可以接受的。

在 dartpad 上尝试

void main() {
  User()
    ..firstName = "Laxman"
    ..lastName = " Bhattarai"
    ..age = 18
    ..printUser();
}

class User {
  String firstName;
  String lastName;
  DateTime birthday;

  String get fullName => firstName + lastName;

  set age(int age) =>  birthday = DateTime.now().subtract(Duration(days: age * 365));

  int get age => DateTime.now().year - birthday.year;

  bool get isAdult => age >= 18;

  printUser() => print(fullName + " is a ${isAdult ? "Adult" : "Child"}");
}

提示 14:FractionallySizedBox

是否曾想过让 Widget 的高度和宽度与其屏幕的高度和宽度完全成比例?

FractionallySizedBox 正是为了这种用例而构建的。只需为您需要的高度和宽度指定分数,它就会处理所有其他事情。分数的值将在 0.0 到 1.0 之间。

FractionallySizedBox(
  widthFactor: 0.5,
  heightFactor: 0.5,
  child: Container(color: Colors.green),
)

在 codepen 上尝试

fractionally

提示 15:Flexible vs Expanded

Expanded() 只是 Flexible() 的一个

Flexible (fit: FlexFit.tight) = Expanded()

但是,Flexible 默认使用fit :FlexFit.loose

FlexFit.tight = 想要紧密地适应父级,尽可能多地占用空间。

FlexFit.loose = 想要松散地适应父级,为自己占用尽可能少​​的空间。

flex = 从父级获取的空间的比例。如果使用flex: FlexFit.loose(即Flexible),则通常不完全使用。

flex

如果您仔细阅读以下图片,您将完全理解FlexibleExpanded之间的区别。

class Flexible extends... {
  /// The flex factor to use for this child
  ///
  /// If null or zero, the child is inflexible and determines its own size. If
  /// non-zero, the amount of space the child's can occupy in the main axis is
  /// determined by dividing the free space (after placing the inflexible
  /// children) according to the flex factors of the flexible children.
  final int flex;

  /// How a flexible child is inscribed into the available space.
  ///
  /// If [flex] is non-zero, the [fit] determines whether the child fills the
  /// space the parent makes available during layout. If the fit is
  /// [FlexFit.tight], the child is required to fill the available space. If the
  /// fit is [FlexFit.loose], the child can be at most as large as the available
  /// space (but is allowed to be smaller).
  final FlexFit fit;

  ........
}

class Expanded extends Flexible {
  /// Creates a widget that expands a child of a [Row], [Column], or [Flex]
  /// so that the child fills the available space along the flex widget's
  /// main axis.
  const Expanded({
    Key key,
    int flex = 1,
    @required Widget child,
  }) : super(key: key, flex: flex, fit: FlexFit.tight, child: child);
}

在 codepen 上尝试

提示 16:批量声明

如果您一直单独声明每个成员,那么您可以一次声明相同类型的成员。

我不会一次声明ageshoeSize,因为它们不相关。

能力越大,责任越大,请明智地使用。

class Footballer {

  String firstName = "Lionel";
  String middleName = "Andrés";
  String lastName = "Messi";

  double weightKG;
  double heightCM;

  int goals;
  int assists;
  int tackles;
  int takeons;
  int saves;
  int shots;
}

// The class above can be replaced with:
class Footballer {

  String firstName = "Lionel", middleName = "Andrés", lastName = "Messi";

  double weightKG, heightCM;

  int goals, assists, tackles, takeons, saves, shots;
}

提示 17:SliverAppBar / 可折叠 AppBar / 视差标题

还记得 CollapsableAppBar (android) / ParallaxHeader (ios) 吗?Flutter 中有 SliverAppBar 可以做到这一点。

要使用它,您需要有一个 CustomScrollView 作为父级。

然后您添加两个 sliver。

  1. SliverAppBar
  2. SliverFillRemaining

您可以调整 snap、floating、pinned 等的值以获得所需的效果。

在 dartpad 上尝试

在此处查看各种类型的 SliverAppBars

sliverappbar

提示 18:Key 是什么

keys

是否想过我们为什么需要 GlobalKey(子项:GlobalObjectKey、LabeledGlobalKey)、LocalKey(子项:ObjectKey、ValueKey & UniqueKey)?

它们用于在有状态的 Widget 中访问或恢复状态(如果我们的 Widget 树全是无状态 Widget,我们通常不需要它们)。

目的(括号内使用的 Key)

  • 在有状态 Widget 中突变集合,即从列表中删除/添加/重新排序项目,例如可拖动的待办事项列表,已勾选的项目会被删除(ObjectKey、ValueKey & UniqueKey)。
  • 将 Widget 从一个父级移动到另一个父级,同时保留其状态。(GlobalKey)。
  • 在多个屏幕上显示相同的 Widget 并保持其状态。(GlobalKey)。
  • 验证表单。(GlobalKey)。
  • 您可以在不使用任何数据的情况下提供 Key。(UniqueKey)。
  • 如果您可以使用用户 UUID 等特定数据字段作为唯一 Key。(ValueKey)。
  • 如果您没有可用的唯一字段作为 Key,但对象本身是唯一的。(ObjectKey)。
  • 如果您有多个表单或相同类型的多个 Widget 需要 GlobalKey。(GlobalObjectKey、LabeledGlobalKey,取决于哪种合适,逻辑与 ValueKey 和 ObjectKey 类似)。

在此视频中了解更多

提示 19:强大的库time.dart

如果您厌倦了冗长且啰嗦的 DateTime 和 Duration 计算,time.dart可以为您排忧解难。

//Before
var 3dayLater = DateTime.now().add(Duration(days: 3)).day;

//After
var 3dayLater = 3.days.fromNow.day;

//Before
var duration = Duration(minutes: 10) +Duration(seconds: 15) 
  - Duration(minutes: 3) + Duration(hours: 2;

//After
var duration = 10.minutes + 15.seconds - 3.minutes + 2.hours;

//Before
await  Future.delayed(Duration(seconds: 2))

//After
await 2.seconds.delay

访问 time.dart

提示 20:测试错误

您可以使用expect(actual, expected)在 dart 中简单地测试两个值是否相等。

但如果您想测试错误,请使用抛出错误的函数闭包作为实际值,并使用throwsA<ErrorType>作为预期值进行检查。

void main() {
  group("Exception/Error testing", () {
    test("test method that throws errors", () {
      expect(_testError(fails: false), false);
      expect(() => _testError(fails: true), throwsA(isA<FooError>()));
    });
  });
}

bool _testError({bool fails}) {
  if(fails)throw FooError();
    return fails;
}

class FooError extends Error {}

GitHub

https://github.com/erluxman/awesomefluttertips/