0%

ListView

inheritance 继承关系

Object > DiagnosticableTree > Widget > StatelessWidget > ScrollView > BoxScrollView > ListView

BoxScrollView

BoxScrollView 只有一个成员padding,根据滚动方向来添加padding,将另一方向的padding置为0。

BoxScrollView 需要传入的参数有很多,几乎多有传入参数均传给了其父类ScrollView

ScrollView

提供滚动(Scrollable)组件、viewport。Scrollable在滚动中回调函数viewportBuilder,提供给viewport偏移量,返回显示视图。

统一处理滚动过程中键盘处理——键盘失去焦点,通过FocusScope.of(context)获取当前焦点状态,调用unfocus方法房键盘消失。

ListView总结

在查看集成链路源码中,通过抽象方法划分功能模块。

  • ScrollView: 负责滚动相关功能

    • 集成Scrollable,实现滚动能力。通过集成Scrollable的返回的偏移量,管理自己的viewport。
    • 视口创建,Scrollable的回调方法viewportBuilder也很好的解耦了滚动功能和视口创建,视图功能交给使用者ScrollView, 由ScrollView的buildViewport方法实现。
    • 视图内容交给子类实现,在buildViewport方法中需要传入具体sliver,而sliver方法是ScrollView的抽象方法,交给子类实现(如BoxScrollView)
  • BoxScrollView: 负责处理滚动边距问题

    • 实现父类ScrollView的buildSlivers方法,处理边距问题
    • 具体sliver内容通过抽象方法buildChildLayout,交由子类实现
  • ListView: 实现buildChildLayout方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @override
    Widget buildChildLayout(BuildContext context) {
    if (itemExtent != null) {
    return SliverFixedExtentList(
    delegate: childrenDelegate,
    itemExtent: itemExtent!,
    );
    } else if (prototypeItem != null) {
    return SliverPrototypeExtentList(
    delegate: childrenDelegate,
    prototypeItem: prototypeItem!,
    );
    }
    return SliverList(delegate: childrenDelegate);
    }

GirdView

inheritance

Object > DiagnosticableTree > Widget > StatelessWidget > ScrollView > BoxScrollView > GridView

GridView和ListView都是BoxScrollView子类。边距处理、滚动功能、视口创建均由父类实现。他们均需要实现buildChildLayout方法,去管理具体silver内容

GirdView

有两个成员girdDelegate和childrenDelegate,这两个类均默认在GirdView构造函数中创建。

  • girdDelegate成员是SliverGridDelegate类型,作用是管理sliver布局约束,SliverGridDelegate是抽象类,它有两个子类,一般我们使用也是使用这两个子类:
    • SliverGridDelegateWithFixedCrossAxisCount,通过交叉轴sliver个数
    • SliverGridDelegateWithMaxCrossAxisExtent, 通过sliver在交叉轴最大尺寸
  • childrenDelegate成员是SliverChildDelegate类型,girdView布局管理,给SliverGrid提供最大滚动距离。SliverChildDelegate也是抽象类,也是两个子类,一个用于有builder回调的,一个用于列表:
    • SliverChildBuilderDelegate
    • SliverChildListDelegate

实现buildChildLayout方法

1
2
3
4
5
6
7
@override
Widget buildChildLayout(BuildContext context) {
return SliverGrid(
delegate: childrenDelegate,
gridDelegate: gridDelegate,
);
}

GirdView总结

GirdView主要是通过两个成员变量来管理sliver布局和尺寸——childrenDelegate和gridDelegate,这两个成员的类型均是抽象类,也就是说我们完全可以通过集成相应抽象类,去具体实现我们想要的布局,让GirdView灵活性变得大很多。

flutter布局的核心

Constraints go down, Sizes go up, Parent sets position

  • 父节点向子节点传约束
  • 子节点向父节点上传大小
  • 最后由父节点决定位置

不是按照直接约束显示

问题代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Scaffold(
body: Center(
child: ConstrainedBox(
constraints: BoxConstraints.tight(const Size(300, 300)),
child: ColoredBox(
color: Colors.yellow,
child: ConstrainedBox(
constraints: BoxConstraints.tight(const Size(100, 200)),
child: const ColoredBox(
color: Colors.red,
child: SizedBox(
width: 100,
height: 100,
child: ColoredBox(color: Colors.teal),
),
),
),
),
),
),
);

显示效果

最终只显示了蓝绿色,而且没有按照我们对蓝绿色组件直接的约束大小显示。

错误分析

  • 这段代码,我们逐步移出蓝绿色、黄色代码,发现黄色和红色均显示为300*300
  • 我们想显示的蓝绿色、红色均没有按照我们直接的约束显示,都被黄色父节点约束覆盖了。
  • 而黄色可以理解为按照起父级约束显示。

我们发现黄色的父级约束盒子的父级是Center, 这个就是重点

  1. Center的继承关系是:SingleChildRenderObjectWidget>Align>Center
  2. ConstrainedBox的继承关系是:SingleChildRenderObjectWidget>ConstrainedBox
  3. SizedBox的继承关系是:SingleChildRenderObjectWidget>SizedBox

这里先插入一个知识,我们在屏幕上看到的UI都是通过RenderObjectWidget实现的。而RenderObjectWidget中有个creatRenderObject方法生成RenderObject对象,RenderObject实际负责layout()和paint()。

其中Align的creatRenderObject方法返回的是RenderPositionedBox;

SizedBox ConstrainedBox的creatRenderObject方法返回的是RenderConstrainedBox;

RenderPositionedBox 和 RenderConstrainedBox

RenderPositionedBoxRenderConstrainedBox 最终集成的都是RenderBox

我们先来对比一下用于计算布局的performLayout方法

RenderPositionedBox

这里在子节点渲染时,修改了布局约束,改为松约束。
其中constraints.loosen()是把min约束给删除,可以理解为置零

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
@override
void performLayout() {
final BoxConstraints constraints = this.constraints;
final bool shrinkWrapWidth = _widthFactor != null || constraints.maxWidth == double.infinity;
final bool shrinkWrapHeight = _heightFactor != null || constraints.maxHeight == double.infinity;

if (child != null) {
// 这里在子节点渲染时,修改了布局约束,改为松约束。
// 其中constraints.loosen()是把min约束给删除,可以理解为置零
child!.layout(constraints.loosen(), parentUsesSize: true);
// 根据子节点大小计算自己的大小
// 默认是没设置Factor 那么自身size就是最大值double.infinity
size = constraints.constrain(Size(
shrinkWrapWidth ? child!.size.width * (_widthFactor ?? 1.0) : double.infinity,
shrinkWrapHeight ? child!.size.height * (_heightFactor ?? 1.0) : double.infinity,
));
// 根据自身size,子节点size,来绘制子节点位置
alignChild();
} else {
size = constraints.constrain(Size(
shrinkWrapWidth ? 0.0 : double.infinity,
shrinkWrapHeight ? 0.0 : double.infinity,
));
}
}

RenderConstrainedBox

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@override
void performLayout() {
final BoxConstraints constraints = this.constraints;
if (child != null) {
// 直接把自己的约束传递给子节点,并获取子节点size
child!.layout(_additionalConstraints.enforce(constraints), parentUsesSize: true);
// 将子节点的size赋值给自己
size = child!.size;
// 自己和子节点的size相同,所以不需要进行align子节点
} else {
size = _additionalConstraints.enforce(constraints).constrain(Size.zero);
}
}

结论

我们可以得到结论,当直接父节点是CenterAlign等约束Widget,他们的creatRenderObject返回的是RenderPositionedBox, 而RenderPositionedBox计算会先计算子节点大小,然后结算自己的大小,最后根据约束子组件偏移,子组件是可以显示其自身约束,前提是在RenderPositionedBox的松约束下。

而ConstrainedBox、SizeBox等最终的creatRenderObject返回的是RenderConstrainedBox,其直接把父节点约束传递给子节点,当父节点的约束是紧约束,即是expand显示,所有creatRenderObject返回的是RenderConstrainedBox的子节点均会继承约束,即自身约束并不生效。

混编打包遇到环境不在有效范围内,爬坑记录

1
2
3
4
5
6
7
8
ERROR: Unknown FLUTTER_BUILD_MODE: distribution.
Valid values are 'Debug', 'Profile', or 'Release' (case insensitive).
This is controlled by the FLUTTER_BUILD_MODE environment variable.
If that is not set, the CONFIGURATION environment variable is used.

You can fix this by either adding an appropriately named build
configuration, or adding an appropriate value for FLUTTER_BUILD_MODE to the
.xcconfig file for the current build configuration (Distribution).

方案一(不推荐)

根据提示的路径找到对应的flutter_export_environment.sh文件和Generated.xcconfig文件,打开添加以下代码:

export "FLUTTER_BUILD_MODE=release"

注:这两个文件是自动生成的,每次生成会覆盖,所以这个方案算是临时方案,协同工作很不方便,极其不推荐

方案二

打开flutter的package地址:xxx/Library/Developer/flutter/packages/flutter_tools/bin/xcode_backend.dart

我们可以看到在选择模式的时候,先查找set中FLUTTER_BUILD_MODE配置,如果没有取CONFIGURATION,可以直接修改package代码,添加判断如下:

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
String parseFlutterBuildMode() {
// Use FLUTTER_BUILD_MODE if it's set, otherwise use the Xcode build configuration name
// This means that if someone wants to use an Xcode build config other than Debug/Profile/Release,
// they _must_ set FLUTTER_BUILD_MODE so we know what type of artifact to build.
final String? buildMode =
(environment['FLUTTER_BUILD_MODE'] ?? environment['CONFIGURATION'])
?.toLowerCase();

if (buildMode != null) {
// 修改的地方
if (buildMode.contains('release') || buildMode.contains('xxx')) {
return 'release';
}
if (buildMode.contains('profile')) {
return 'profile';
}
if (buildMode.contains('debug')) {
return 'debug';
}
}
echoError(
'========================================================================');
echoError('ERROR: Unknown FLUTTER_BUILD_MODE: $buildMode.');
echoError(
"Valid values are 'Debug', 'Profile', or 'Release' (case insensitive).");
echoError(
'This is controlled by the FLUTTER_BUILD_MODE environment variable.');
echoError(
'If that is not set, the CONFIGURATION environment variable is used.');
echoError('');
echoError('You can fix this by either adding an appropriately named build');
echoError(
'configuration, or adding an appropriate value for FLUTTER_BUILD_MODE to the');
echoError(
'.xcconfig file for the current build configuration (${environment['CONFIGURATION']}).');
echoError(
'========================================================================');
exitApp(-1);
}

方案三(推荐★★★)

在方案二实施过程中,我们发现这个选择mode过程,会优先选择FLUTTER_BUILD_MODE,这个配置默认是没有的,方案一强制性设置为release会导致其他环境运行出错。
我们可以添加自定义配置项FLUTTER_BUILD_MODE,将你的环境指定对应的mode

flutter数据共享系列——随记

Provider

InheritedWidget 解决了数据共享问题。迎面也带来数据刷新导致的组件不必要更新问题。Provider基于InheritedWidget实现数据共享,数据更新,定向通知组件更新等。

接下来我们先从Provider使用开始切入,逐步分析Provider的实现,以及对组件的应用进行熟悉。

就拿官方文档开始:

新建一个模型Counter

1
2
3
4
5
6
7
8
9
10
class Counter with ChangeNotifier {
int _count = 0;

int get count => _count;

void increment() {
_count++;
notifyListeners();
}
}

在合适的位置初始化

这里我们选择main方法:

1
2
3
4
5
6
7
8
9
10
11
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => Counter()),
],
child: const MyApp(),
),
);
}

使用并修改数据

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return const MaterialApp(
home: MyHomePage(),
);
}
}

class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Example'),
),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: const <Widget>[
Text('You have pushed the button this many times:'),

Extracted as a separate widget for performance optimization.
As a separate widget, it will rebuild independently from [MyHomePage].

This is totally optional (and rarely needed).
Similarly, we could also use [Consumer] or [Selector].
Count(),
],
),
),
floatingActionButton: FloatingActionButton(
key: const Key('increment_floatingActionButton'),

Calls `context.read` instead of `context.watch` so that it does not rebuild
when [Counter] changes.
onPressed: () => context.read<Counter>().increment(),
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}

class Count extends StatelessWidget {
const Count({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return Text(

Calls `context.watch` to make [Count] rebuild when [Counter] changes.
'${context.watch<Counter>().count}',
key: const Key('counterState'),
style: Theme.of(context).textTheme.headline4);
}
}

使用注意事项

1. 共享数据定义为私有属性,提供get方法和update方法

这样可以有效的保护数据结构,统一修改入口和获取方法。

2. 适当隔离会进行rebuild的组件,常用的方式有三种:

  • 单独封装组件
  • 通过Consumer包裹
  • 通过Selector包裹,selector可以在某些值不变的情况下,防止rebuild。常用的地方是针对列表中个别数据进行修改。

3. 区分watchread的使用

watchread是Provider框架内部对BuildContext的扩展类。用户获取父级组件指定数据入口。区别在于是否添加了linsten,这个关系到是否需要实时刷新。
简单区分两种场景:

  • watch:界面监听数据,更新页面
  • read:响应业务交互,去操作更新数据

两种方法的源码也很简单,只是为了方便生成的拓展类:

1
2
3
4
5
6
7
8
9
10
11
12
13
 Exposes the [read] method.
extension ReadContext on BuildContext {
T read<T>() {
return Provider.of<T>(this, listen: false);
}
}

Exposes the [watch] method.
extension WatchContext on BuildContext {
T watch<T>() {
return Provider.of<T>(this);
}
}

这里我们来看看Provider.of源码:

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
  static T of<T>(BuildContext context, {bool listen = true}) {
// 移出部分不必要代码

final inheritedElement = _inheritedElementOf<T>(context);

if (listen) {
context.dependOnInheritedElement(inheritedElement);
}
return inheritedElement.value;
}

static _InheritedProviderScopeElement<T> _inheritedElementOf<T>(
BuildContext context,
) {
// 移出部分不必要代码
_InheritedProviderScopeElement<T>? inheritedElement;

if (context.widget is _InheritedProviderScope<T>) {
context.visitAncestorElements((parent) {
inheritedElement = parent.getElementForInheritedWidgetOfExactType<
_InheritedProviderScope<T>>() as _InheritedProviderScopeElement<T>?;
return false;
});
} else {
inheritedElement = context.getElementForInheritedWidgetOfExactType<
_InheritedProviderScope<T>>() as _InheritedProviderScopeElement<T>?;
}

if (inheritedElement == null) {
throw ProviderNotFoundException(T, context.widget.runtimeType);
}

return inheritedElement!;
}

原来加不加listen的区别在于获取数据的方式是getElementForInheritedWidgetOfExactTypeordependOnInheritedElementdependOnInheritedElement会新增一个注册,这个注册会调用在数据变更后,调用消费者的didChangeDependencies 。稍微具体点分析可以查看我之前的文章——
记InheritedWidget使用思考

ChangeNotifier

实现Listenable接口的一个简单类,官方给的说明很简单:

A class that can be extended or mixed in that provides a change notification

可以扩展或混合的类,提供更改通知

实现了算法复杂度为O(1)去添加监听,O(N)去移除监听。对数据更新高效通知页面去刷新。provider的数据模型均得继承与它。

ChangeNotifierProvider

有了数据模型,接下来就开始创建我们的ChangeNotifier,就要用到ChangeNotifierProvider

先说一个错误的示例,错误的示例,错误的示例,在build中通过ChangeNotifierProvider.value去创建:

1
2
3
4
ChangeNotifierProvider.value(
value: new MyChangeNotifier(),
child: ...
)

这样会造成内存泄露和潜在的bug——参考

当然,这个方法存在肯定是有他的意义的——如果你已经有了ChangeNotifier实例,就可以通过ChangeNotifierProvider.value进行构造。而不是选用create

正确的方法是通过creat方法去构建:

1
2
3
4
ChangeNotifierProvider(
create: (_) => new MyChangeNotifier(),
child: ...
)

不要传入变量去构建ChangeNotifier,这样的话,当变量更新,ChangeNotifier是不会去更新的。

1
2
3
4
5
6
int count;

ChangeNotifierProvider(
create: (_) => new MyChangeNotifier(count),
child: ...
)

如果你确实需要传入变量,请使用ChangeNotifierProxyProvider。具体ChangeNotifierProxyProvider使用,这里就不探讨了。

记InheritedWidget使用思考

InheritedWidget 是项目中必不可少的组件,用户数据共享。老生常谈的Provider框架也是基于InheritedWidget实现的

简介

InheritedWidget组件是功能性组局,实现了由上向下共享数据的功能。即子组件通过BuildContext.dependOnInheritedWidgetOfExactType方法从父组件获取数据。

值得提一下,这种由上向下提供书共享数据的方式和Notification传递方向正好相反。两者相同点是:都是由子组件发起的。InheritedWidget是又子组件通过content向树上方查找数据,Notification是由子组件向上发起通知。

特点

  1. 子组件通过[BuildContext.dependOnInheritedWidgetOfExactType]方法查找对应最近的InheritedWidget并获取数据。
  2. InheritedWidget根据习惯或者协议,提供of静态方法,便于子组件获取数据。该方法并非一定返回该继承的widget,也可以返回其他数据。
  3. 调用of方法的组件,必须要在InheritedWidget内。如果在同一个widget中使用,可以通过Builder将需要使用的组件包裹起来。

作用

在使用中我们发现,InheritedWidget作用是父组件提供数据,子组件可以由下往上查找对应的共享数据,这个是它直观的作用。

常用的错误更新数据方式

**setState**: 我们发现网上的有很多文章在介绍InheritedWidget的使用,并更新数据。但是这个只是体现了该组件的作用。绝大多数是在InheritedWidget组件中通过final申明共享数据。然后在父组件中通过setState强制刷新父组件,同时也刷新了InheritedWidget组件,当然其多层子组件都会跟着一起rebuild,造成了很大的非必要消耗。

如何去优化?

InheritedWidget提供了数据的共享,避免了数据由上到下层层申明传递。我们使用数据的地方也很明确。接下来要做的是,数据刷新后,让指定组件去更新。这个时候我们就想到了ValueNotifier:**InheritedWidget在共享数据的时候通过ValueNotifier包裹,子组件通过of方法获取的数据类型也是ValueNotifier,在使用的地方通过ValueListenableBuilder实现**。

进一步思考

【老孟Flutter】源码分析系列之InheritedWidget一文中提到了,修改of方法:

  • dependOnInheritedWidgetOfExactType

  • getElementForInheritedWidgetOfExactType().widget

    两者的区别:调用dependOnInheritedWidgetOfExactType() 和 getElementForInheritedWidgetOfExactType()的区别就是前者会注册依赖关系,而后者不会,所以在调用dependOnInheritedWidgetOfExactType()时,InheritedWidget和依赖它的子孙组件关系便完成了注册,之后当InheritedWidget发生变化时,就会更新依赖它的子孙组件,也就是会调这些子孙组件的didChangeDependencies()方法和build()方法。而当调用的是 getElementForInheritedWidgetOfExactType()时,由于没有注册依赖关系,所以之后当InheritedWidget发生变化时,就不会更新相应的子孙Widget。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    @override
    T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>(
    {Object? aspect}) {
    assert(_debugCheckStateIsActiveForAncestorLookup());
    final InheritedElement? ancestor =
    _inheritedWidgets == null ? null : _inheritedWidgets![T];
    // 多出部分
    if (ancestor != null) {
    assert(ancestor is InheritedElement);
    return dependOnInheritedElement(ancestor, aspect: aspect) as T;
    }
    _hadUnsatisfiedDependencies = true;
    return null;
    }

    @override
    InheritedElement?
    getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
    assert(_debugCheckStateIsActiveForAncestorLookup());
    final InheritedElement? ancestor =
    _inheritedWidgets == null ? null : _inheritedWidgets![T];
    return ancestor;
    }

很明显修改为getElementForInheritedWidgetOfExactType<T>().widget,就会少一个非必要注册,也算是好事。

进一步思考产生疑问

既然通过了ValueNotifierInheritedWidget结合,我们通过修改ValueNotifier的value来改变页面时,InheritedWidget组件没有重新构建并不会触发updateShouldNotify,也就是在使用数据的子组件中didChangeDependencies方法也不会因为value的改变而调用。
所以of方法具体用什么去实现,取决于具体业务设计。

文中的图片均借用于【老孟Flutter】源码分析系列之InheritedWidget

Welcome to my blog, enter password to read.
Read more »

Flutter实现自旋转的伪3D球

一个自动旋转的仿3D球体。基于https://github.com/al4fun/3DBall,进行二次封装。

特性

  • 支持手动/自动转动
  • 支持暂停/继续转动控制
  • 支持高亮处理部分标签

思路

  • 建立了一个虚拟的3d坐标系:坐标原点位于球心,Z轴垂直于屏幕并指向屏幕外。根据球面方程将需要绘制的点均匀散布到整个球面上。
  • 由点坐标可以得到点在XY平面上的投影位置;根据点到球体最前方垂直切面的距离,计算点的尺寸和颜色值(距离越远,尺寸越小,颜色越暗淡)。
  • 由球体旋转的方向和位移可以得到球体的旋转轴矢量和旋转角,进而可由罗德里格旋转公式计算出旋转后点的坐标。

Getting Started

1
2
3
4
5
6
7
8
9
10
RBallView(
isAnimate: provider.curIndex == 0,
isShowDecoration: false,
mediaQueryData: MediaQuery.of(context),
keywords: snapshot.data,
highlight: [snapshot.data[0]],
onTapRBallTagCallback: (RBallTagData data) {
print('点击回调:${data.tag}');
},
),

源码和Demo地址

https://github.com/rzrobert/RBallView

Here's something encrypted, password is required to continue reading.
Read more »

Here's something encrypted, password is required to continue reading.
Read more »

客户端需要不定期更新token,同时得保证在发送更新token同时其他网络请求挂起,否则会报token失效。待新token回来之后,重新发起挂起的请求。

应服务端要求,更新token请求时,老token立刻失效

Read more »