Flutter 学习笔记

Introduction

Flutter 采用 Dart 作为主要编程语言,所以应用的入口同样是顶层的 main() 方法,main() 方法中调用了 runApp() 方法,它接收一个 Widget 作为参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import 'package:flutter/material.dart';

void main() {
runApp(MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Demo'),
),
body: Center(
child: Text('Hello World'),
),
),
));
}

UI

Flutter 中的组件分为非容器类组件和容器类组件,非容器类组件一般直接或间接继承自 LeafRenderObjectWidget,而容器类组件则继承自 SingleChildRenderObjectWidgetMultiChildRenderObjectWidget,所以,所有组件的继承关系为 Widget > RenderObjectWidget > (Leaf/SingleChild/MultiChild)RenderObjectWidget,其中,RenderObjectWidget 类中定义了创建、更新 RenderObject 的方法,所有子类必须实现它。

Layout 基础

Flutter 中的 Widget 借鉴了 React 的思想。所有的 UI 都是基于 Widget 的,Widget 定义了视图如何显示(实际对应的是 Element)。如果使用的是 StatefulWidget,当 State 发生变化时 (通过 setState()) 会触发重绘,当然,这种重绘是经过框架计算的,框架会尽量减少重绘的范围。

Flutter 中布局模型有两种,一种是基于 RenderBox 的模型,其约束被称为 BoxContraints,另一种是基于 RenderSliver 的可滚动列表模型,其约束来自于 SliverConstraints。由于开发过程中涉及到的大部分约束相关的问题都来自于 Box 约束,因此这里主要介绍它。

Box 约束

在 Flutter 中,组件是基于 RenderBox 被渲染出来的,而 RenderBox 的约束 (constraints) 来自于父组件,并且会根据约束来调整自身的*大小 (size)*。约束包含最大/最小的宽和高,大小则由具体的宽和高组成。

通常来说,依据组件如何处理它们的约束,Flutter 中可以分为三种 Boxes:

  • 想要尽可能大的 Box,比如 Center、ListView 和未指定大小的 Container
  • 和子组件一样大的 Box,比如 Transform 和 Opacity
  • 固定大小的 Box,比如 Image 和 Text
不受限的约束

在某些场景下,Box 的约束可能是不受限制的,也就是 unbounded,它们的宽或高被设置为 double.INIFINITY。

一个尽可能大的 Box 如果被给予了不受限的约束的话,就会在运行时报错,比如如果你把一个垂直滚动的 ListView 嵌套进一个水平滚动的 ListView,那么,由于 ListView 会在它的交叉方向上寻求最大边界,内部的 ListView 会尝试尽可能填充自己的宽度,而此时宽度又是无限宽,所以会报异常。

Flex Boxes

Flex boxes (比如 Row 或 Column) 在不同的约束下表现不同。在受限的约束下,Flex box 会在该方向上尽可能的大。在不受限的约束下,它们会将子组件尽可能合适地进行排列,但是前提是子组件不能使用大于 0 的 Flex,也就是说使用了 Flex 的 box 不能相互嵌套。比如对于 Column 来说,它的 width 不能是 unbounded;对于 Row 来说,它的高度也不能是 unbounded。

理解约束

Flutter 中有一个基本原则:约束向下传递,大小向上传递,父布局设置位置

  • 组件的约束 (constraints) 来自父组件,约束由 4 个 double 组成:最小/最大宽度,最小/最大高度。
  • 组件会依次测量子组件,得到子组件的大小,并且告诉每个子组件它们各自的约束。
  • 组件会根据子组件的大小和排列方向依次放置子组件。
  • 最后,组件告诉父组件它自身的大小。
约束的限制
  • 组件的大小必须在父组件的约束范围之内,也就是说子组件并不能拥有任何想要的大小
  • 组件无法知晓也决定不了自身在屏幕上的位置,因为它的位置是由父组件决定的。
  • 因为所有组件都在一个组件树之中,所以任何组件都无法在脱离组件树的情况下知道自身的大小和尺寸
  • 当子组件需要一个不同的大小并且父组件没有足够的关于如何排列它的信息时,子组件的大小会被忽略。所以,定义对齐方式时必须足够明确
严密约束和宽松约束

约束也分为 tightloose 的。宽松约束指组件的最大宽高确定,但是最小宽高为零。严密约束是指确定大小的约束,也可以理解为最大宽高和最小宽高相等。比如下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 布局 A
Container(
child: Container(
color: Colors.red,
width: 100,
height: 100,
),
);

// 布局 B
Center(
child: Container(
color: Colors.red,
width: 100,
height: 100,
),
);

同样作为根布局,布局 A 会占满整个屏幕,因为 Container 传递的是 tight 约束,它告诉同样为 Container 的子组件,它的大小是整个屏幕,所以,即使子组件 Container 设置了大小也不会有效果。

而布局 B 中的 Center 作为根布局,它传递的是 loose 约束,所以只要子组件 Contaienr 的大小不超过屏幕大小就可以了,最小多小并不会有限制,所以它设置的宽高可以传递到父布局并设置生效。

内部 Container 的约束分别可以表示为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 布局 A
BoxConstraints.expand({
width = double.infinity,
height = double.infinity,
})
// 等同于:
BoxConstraints(
minWidth: double.infinity,
maxWidth: double.infinity,
minHeight: double.infinity,
maxHeight: double.infinity,
);

// 布局 B
BoxConstraints.loose(Size(100, 100))
// 等同于:
BoxConstraints(
minWidth: 100,
maxWidth: double.infinity,
minHeight: 100,
maxHeight: double.infinity,
);

因此,固定大小的组件 (比如 SizedBox) 也是严密约束的,其约束可以表示为:

1
2
3
BoxConstraints.tightFor(width: 80.0, height: 80.0)
// 等同于:
BoxConstraints(minHeight: 80.0, maxHeight: 80.0, minWidth: 80.0, maxWidth: 80.0)
「去除」父级约束

既然布局约束是向下传递的,有没有什么办法可以去除掉父布局的约束呢?理论上是不可以的,但是实际使用中,我们可以使用 UnconstraintedBox,它可以使得子布局忽略父布局的约束:

1
2
3
4
5
6
7
8
9
ConstrainedBox(
constraints: BoxConstraints(minWidth: 60.0, minHeight: 100.0), // A
child: UnconstrainedBox( // 「去除」父级约束
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: 90.0, minHeight: 20.0), // B
child: redBox,
),
)
)

上面的例子中,最外层的约束 A 不会传递给 B,最终 redBox 的高度会是 20。但这只是表面现象,其实 UnconstrainedBox 只是阻断了约束的传递,自身还是会受到父级约束的限制,因此最终整体布局的高度还是 100。

实际使用中,我们可以利用 UncontrainedBox 去除父组件的约束,比如如果使用 SizedBox 时指定了宽度或者高度,子组件就会有相同的宽度或高度,而如果想要去除这种限制,我们就可以使用 UnconstrainedBox 来包裹子组件。

如何学习组件的布局规则

Flutter 中组件众多,我们很难一下子记住所有的组件到底是如何布局的,所以最好的做法是阅读文档源码。比如,如果你想阅读 Column 组件的源码,首先,我们通过 IDE 跳转到 basic.dart 文件,发现原来 Column 继承的是 Flex 类,在 FlexcreateRenderObject() 方法中发现返回的是 RenderFlex 对象,最后在 flex.dart 文件中找到 performLayout() 方法,这里才是实际测量并计算子组件位置的地方。

常用组件

Basic Widget

MaterialApp, Scaffold, AppBar (material library)

CupertinoApp, CupertinoPageScaffold, CupertinoTabScaffold (cupertino library)

Builder, StatefulBuilder

Text, Icon, Image

TextButton, ElevatedButton, OutlinedButton, IconButton

GestureDetector, InkWell/InkResponse

Layout Widget

Row, Column, Stack/Positioned, ListView, GridView, Scrollbar

SingleChildScrollView, CustomScrollView, SliverList, SliverGrid

Container, Center, Padding

DecoratedBox/BoxDecoration, ClipRect, ClipRRect, Transform

Expanded, Flexible, ConstrainedBox, SizedBox, Spacer

Flex, Wrap, Flow, Align/Alignment/FractionalOffset

Animation Widget

Animation, Tween, AnimationController, Curves, Transition, ImplicitlyAnimatedWidget

Others

其他组件的介绍见:Widget Catalog, Widget of the Week

自定义组件

大多数情况下,我们都可以通过组合创建出所需要的组件,但是对于一些特别复杂的组件,无法通过组合获得,此时就需要通过自定义的方式创造出我们想要的组件。

Flutter 中自定义组件的方式主要依赖于 CustomPaint 组件和自定义画笔 CustomerPainter

CustomPaint

用于使用自定义的画笔,其构造方法如下:

1
2
3
4
5
6
7
8
9
CustomPaint({
Key key,
this.painter,
this.foregroundPainter,
this.size = Size.zero,
this.isComplex = false,
this.willChange = false,
Widget child,
})

其中,painter 最先被绘制,然后是 child,最后是 foregroudnPainter。默认会以 child 作为可绘制区域,如果没有指定 child 则以 size 指定的大小作为可绘制区域,默认值为 Size.zeropainter 默认情况下是可以超出可绘制区域的,如果要改变这种行为,可以使用 ClipRect 组件包裹 CustomerPaint 组件。

注意

为了防止不必要的重绘,可以使用 RepaintBoundary 包裹 CustomPaint 组件。

CustomPainter

实现自定义画笔的内容。主要通过在 Canvas 上绘制一系列图形,通过 Paint 指定画笔风格。

比如画一些常见的几何图形和文字:

1
2
3
4
5
6
canvas.drawPoints(PointMode.points, [p1, p2], paint);
canvas.drawLine(from, to, paint);
canvas.drawRect(rect, paint);
canvas.drawCircle(center, radius, paint);
canvas.drawArc(rect, startAngle, sweepAngle, userCenter, paint);
canvas.drawParagraph(textBuilder.build()..layout(ParagraphConstraints(width: 100)), offset);

Paint

画笔的使用与其它平台中类似,我们可以设置画笔风格(填充 or 描边)、描边宽度、颜色、反锯齿等。

1
2
3
4
5
final paint = Paint()
..color = Colors.red
..style = PaintingStyle.stroke
..isAntiAlias = true
..strokeWidth = 10;

Animation

Flutter 中对动画进行了抽象,主要涉及 Animation、Curve、Controller、Tween 四个部分。

Animation

Animation 是一个抽象类,主要的功能是保存动画的插值状态。在动画的每一帧中,我们可以通过 Animation对象的 value 属性获取动画的当前状态值,也可以在其上添加每一帧的监听器 (addListener) 和状态变化监听器 (addStatusListener)。

AnimationController

AnimationController 用于控制动画,它包含动画的启动 forward()、停止 stop() 、反向播放 reverse()、重复播放 repeat() 等方法。AnimationController 会在动画的每一帧生成一个新的值,范围默认是 [0, 1],可以通过 lowerBoundupperBound 进行指定。

创建 AnimationController 时需要传入一个 vsync 参数,它接收一个 TickerProvider 对象,它负责创建 Ticker,用于在动画的每一帧提供回调。一般可以使用 mixin 类 SingleTickerProviderStateMixin 或者 TickerProviderStateMixin 来实现 TickerProvider 的功能。

Tween

默认情况下,AnimationController 控制的值的范围是数字区间 [0.0, 1.0],如果我们需要构建 UI 的动画值在不同的范围或者使用不同的数据类型,就可以通过 Tween 添加映射来生成不同范围或数据类型的值。

Curve

我们通过 Curve(曲线)来描述动画过程,把匀速动画称为线性 (Curves.linear) 的,非匀速动画称为非线性的。

核心原理

Widget/Element/RenderObject

Flutter 中一切都是 Widget,但是所有 widget 最终的 layout 和渲染都是通过 RenderObject 来完成的,从创建到渲染大致的流程是:根据 Widget 树生成 Element 树,然后创建 RenderObject 并关联到 Element.renderObject 属性上,最后再通过 RenderObject 来完成布局和绘制。^注1

Element 是 Widget 在 UI 树中对应的实例对象,大多数 Element 只有一个 renderObject,除了少部分有多个子节点。最终,所有 Element 的 renderObject 构成了一棵 Render Tree,即渲染树。

即在 Flutter 中主要有三棵树:Widget Tree => Element Tree => RenderObejct Tree(其实 Widget Tree 并不存在,只是我们看到的结果,只有 Element 和 RenderObject 才有上下级相互关联的关系)。

RenderObejct/RenderBox

每一个 Element 对应一个 RenderObject,它负责布局和绘制,但是它并没有定义子节点模型以及坐标系统,实际完成布局的(确定大小和位置)是通过 RenderBox。

BuildContext 即 Element

创建这个接口是为了避免开发者直接操作 Element 对象。

Flutter 应用是如何运行的

我们知道一个 Flutter 应用的入口是 main() 方法中的 runApp(),那么 runApp() 中主要做了哪些操作呢?

首先,通过 WidgetsFlutterBinding 绑定 Flutter 框架(widgets、services、gesture、painting 等)和 Flutter engine(sky_engine),WidgetsFlutterBinding 就像胶水一样,而绑定发生的位置是在 BindingBase 中的 windowwindow 实际是一个 SingletonFlutterWindow 对象,它作为应用的主窗口,继承自 FlutterView,并且提供了设备屏幕相关的信息以及设备设置的回调方法(比如语言、屏幕亮度等)。

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
41
42
/// Inflate the given widget and attach it to the screen.
void runApp(Widget app) {
WidgetsFlutterBinding.ensureInitialized()
..scheduleAttachRootWidget(app)
..scheduleWarmUpFrame();
}

/// This is the glue that binds the framework to the Flutter engine.
class WidgetsFlutterBinding extends BindingBase with GestureBinding, SchedulerBinding, ... {
static WidgetsBinding ensureInitialized() {
if (WidgetsBinding.instance == null)
WidgetsFlutterBinding();
return WidgetsBinding.instance!;
}
}

/// Base class for mixins that provide singleton services (also known as "bindings").
abstract class BindingBase {
ui.SingletonFlutterWindow get window => ui.window;
}

/// A [FlutterWindow] that includes access to setting callbacks and retrieving
/// properties that reside on the [PlatformDispatcher].
class SingletonFlutterWindow extends FlutterWindow {
VoidCallback? get onXxxChanged => platformDispatcher.onXxxChanged;
...
}

/// A top-level platform window displaying a Flutter layer tree drawn from a [Scene].
class FlutterWindow extends FlutterView {
...
}

/// A view into which a Flutter [Scene] is drawn.
abstract class FlutterView {
PlatformDispatcher get platformDispatcher;
ViewConfiguration get viewConfiguration;

double get devicePixelRatio => viewConfiguration.devicePixelRatio;
Rect get physicalGeometry => viewConfiguration.geometry;
...
}

紧接着会调用 WidgetsBinding 中的 attachRootWidget 方法,将根 Widget 添加到 renderViewElement 上,这是一个 RenderView 对象,它作为整个 RenderObejct Tree 的根 RenderObject

1
2
3
4
5
6
7
8
9
void attachRootWidget(Widget rootWidget) {
_readyToProduceFrames = true;
_renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
container: renderView,
debugShortDescription: '[root]',
child: rootWidget,
).attachToRenderTree(buildOwner!, renderViewElement
as RenderObjectToWidgetElement<RenderBox>?);
}

attachRootWidget 之后,会继续调用 scheduleWarmUpFrame 方法,调用该方法之后会通知框架尽早进行一次绘制,而不是等待 flutter 引擎接收到一次 Vsync 信号之后(正常情况下,每次接受到一次信号会触发一次重绘,也就是一帧,此外还会调用 FrameCallback,Flutter 中一共有 3 种这样的帧回调)。另外,这个方法只会被调用一次,第二次调用无效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/// ScheduleBinding
void scheduleWarmUpFrame() {
...
Timer.run(() {
handleBeginFrame(null); // 会执行 transientCallbacks
});
Timer.run(() {
handleDrawFrame(); // 会执行 persistentCallbacks 和 postFrameCallbacks
resetEpoch();
});
// 锁定事件,在首帧绘制结束之前不会触发新的重绘,也不会响应任何事件输入
lockEvents(() async {
await endOfFrame;
Timeline.finishSync();
});
...
}

在创建完 RenderObject 以及首帧绘制任务之后,接下来真正执行渲染和绘制任务的地方是在 RendererBinding 中的 drawFrame() 方法:

1
2
3
4
5
6
7
void drawFrame() {
assert(renderView != null);
pipelineOwner.flushLayout(); // 布局
pipelineOwner.flushCompositingBits(); // 重绘之前的预处理操作
pipelineOwner.flushPaint(); // 重绘
...
}

另外,在首帧绘制结束后,之后的绘制只有被标记为 dirty 的 Element 才会被重新绘制:

1
2
3
4
5
6
7
8
/// framework.dart/BuildOwner
void buildScope(Element context, [ VoidCallback? callback ]) {
int dirtyCount = _dirtyElements.length;
int index = 0;
while (index < dirtyCount) {
_dirtyElements[index].rebuild();
}
}

State Management

Provider

Provider 是基于 InheritedWidget 的状态管理工具,它的优点是便于理解且使用简单,主要由三个部分组成:

  • ChangeNotifier: 提供数据,更新数据并发送通知
  • ChangeNotifierProvider: 连接数据和组件
  • Consumer: 消费事件,监听数据变化

ChangeNotifier

ChangeNotifier 封装了应用状态,使用观察者-被观察者模式,会通知注册处发生的变化。

1
2
3
4
5
6
7
8
9
10
11
class CartModel extends ChangeNotifier {

final List<Item> _items = [];

void add(Item item) {
_items.add(item);

// This call tells the widgets that are listening to this model to rebuild.
notifyListeners();
}
}

ChangeNotifierProvider

ChangeNotifierProvider 用于给子组件提供一个 ChangeNotifier 实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ChangeNotifierProvider(
create: (context) => CartModel(),
child: MyApp(),
)

// 如果有多个 ChangeNotifier 则需要使用 MultiProvider
MultiProvider(
providers: [
Provider(create: (context) => SomeClass()),
ChangeNotifierProvider(create: (context) => SomeModel()),
ChangeNotifierProxyProvider<DataProvider, SomeOtherModel>(
create: (context) => SomeOtherModel(),
update: (context, newData, previousModel) =>
previousModel.updateModel(newData);
),
],
child: MyApp(),
)

Consumer

我们通过 Consumer 组件来消费 ChangeNotifier 产生的事件。

1
2
3
4
5
return Consumer<CartModel>(
builder: (context, cart, child) {
return Text("Total price: ${cart.totalPrice}");
},
);

Consumer 的 build 函数一共接收三个参数,第一个参数是 context,第二个参数是 ChangeNotifier 对象,也就是我们需要监听的目标对象,第三个是 child,它的作用是优化性能,比如当一个组件不是每次状态发生更新时都要更新 UI 时,此时就可以根据状态来决定是否需要重新创建。其次,最好的做法是将 Consumer 组件放在组件树底部,这样可以尽可能减少因状态发生变化而需要的组件更新。

另外,我们也可以使用 Provider 提供的扩展方法 context.watch<T>() 来获得 ChangeNotifier 并触发 rebuild,不过,该方法只能在 build 方法中使用。

Provider.of<T>()

当你只需要访问 ChangeNotifier 中的方法或者属性而不需要更新 UI 时,可以使用 Provider.of

1
Provider.of<CartModel>(context, listen: false).removeAll();

通过这种方式可以访问到 CartModel 中的方法,但是在数据发生变化时不会收到通知。

另外,也可以使用 context.read<T>(),它也能获取到 ChangeNotifier,不过只能在 build 方法之外才能被调用(比如在 onPressed 方法中),所以也不会触发 rebuild,而使用 Provider.of<T>(context, listen: false) 则没有这样的限制。

select<T, R>(R Function(T value) selector)

当我们只需要监听 ChangeNotifier 中部分发生变化的内容时,就可以使用 select 方法。与 watch 方法类似,select 只有在 build 方法中才能被使用。

1
2
3
var isInCart = context.select<CartModel, bool>(
(cart) => cart.items.contains(item),
);

完整例子

使用 Provider 实现一个 Counter 的例子:

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
41
42
43
44
45
46
47
48
void main() {
runApp(CounterWithProvider());
}

class CounterWithProvider extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => Counter(),
// 不能直接使用 CounterWithProvider 的 context
// 因为它的 `BuildContext` 没有 Provider
builder: (context, child) => MaterialApp(
home: Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
// 使用 builder 的 context 才是安全的
var counter = context.read<Counter>();
// 或者使用 Provider.of<T>
// var counter = Provider.of<Counter>(context, listen: false);
counter.add();
},
tooltip: 'Add',
child: Icon(Icons.add),
),
appBar: AppBar(
title: Text('Counter App'),
),
body: Container(
child: Center(
child: Consumer<Counter>(
builder: (_, counter, child) =>
Text('You\'ve clicked ${counter.count} times'),
),
),
)),
),
);
}
}

class Counter extends ChangeNotifier {
int count = 0;

void add() {
count++;
notifyListeners();
}
}

Others

调试

Flutter 提供了丰富的调试工具,比如 Flutter Inspector, Flutter Outline, Flutter Performance 等。

异常捕获

Flutter 中,发生异常时不会导致程序退出,只会导致当前任务的后续代码不再继续执行,也就是说一个任务中的异常是不会影响其它任务的执行的。下面是在 Flutter 中捕获异常的简单演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void main() {
// 提供一个自定义的错误处理回调
FlutterError.onError = (FlutterErrorDetails details) {
reportErrorAndLog(details); // 上报错误和日志
};

// 在指定配置的环境下运行项目
runZonedGuarded(
() => runApp(ErrorReportApp()),
// Zone 中未捕获到的异常处理回调
(Object obj, StackTrace stack) {
var details = makeDetails(obj, stack);
reportErrorAndLog(details);
},
zoneSpecification: ZoneSpecification(
print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
collectLog(line); // 收集日志
},
),
);
}

具体的异常上报的例子见:Report errors to a service

平台相关

Platform Channel

当我们需要执行平台相关的代码时,就需要借助于 platform channels。工作流程如下:

首先需要在项目中创建 MethodChannel

1
static const MethodChannel methodChannel = MethodChannel('method_channel_name');

接下来,在目标平台创建对应的 MethodChannel,并且对方法调用进行处理。以安卓平台为例:

1
2
3
4
5
6
7
8
9
10
methodChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "method_channel_name")
methodChannel.setMethodCallHandler { call, result ->
when (call.method) {
"someMethodCall" -> {
result.success(null)
}
else -> {
}
}
}

假如我们需要监听方法调用结果,则需要通过 EventChannel 和 EventSink,前者用于注册事件监听,后者用于发送事件。

同样的,我们需要先在 dart 部分创建 EventChannel:

1
static const EventChannel eventChannel = EventChannel('event_channel_name');

然后在平台部分创建 EventChannel 并对事件流进行监听并处理,以安卓为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private lateinit var eventChannel: EventChannel
private var eventSink: EventSink? = null

override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPluginBinding) {
// ...
eventChannel = EventChannel(flutterPluginBinding.binaryMessenger,
"flutter_nimplayer_event")
eventChannel.setStreamHandler(object : StreamHandler {
override fun onListen(arguments: Any?, eventSink: EventSink?) {
this.eventSink = eventSink
}

override fun onCancel(arguments: Any?) {
}
})
// ...
}

向 dart 端传递事件调用结果:

1
eventSink?.success(data)

安卓插件安装流程

安卓项目下的 app/build.gradle 中有一行:

1
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"

该文件的位置在:$FLUTTER_HOME/packages/flutter_tools/gradle/flutter.gradle,其中添加项目中的依赖的方法调用路径是:apply() -> addFlutterTasks() -> configurePlugins() -> getPluginList() -> configurePluginProject() { api pluginProject }

关键的一步是在 getPluginList() 中,通过读取项目根目录下生成的 .flutter-plugins 文件获取插件列表,然后把它们添加到 android 项目中。