Flutter问题统计
1、Flutter 单线程模式。
参考文献
Flutter中文网 : book.flutterchina.club/chapter2/th…
介绍:
Dart在单线程中是以消息循环机制来运行的,其中包含两个任务队列,一个是“微任务队列” microtask queue,另一个叫做“事件队列” event queue。微任务队列的执行优先级高于事件队列。
Dart 中事件的执行顺序:Main > MicroTask > EventQueue
在事件循环中,当某个任务发生异常并没有被捕获时,程序并不会退出,而直接导致的结果是当前任务的后续代码就不会被执行了,也就是说一个任务中的异常是不会影响其它任务执行的。
通常使用 scheduleMicrotask(…)或者Future.microtask(…)方法向微任务队列插入一个任务。
通常使用 Future 向 EventQueue加入事件,也可以使用 async 和 await 向 EventQueue 加入事件。
在Dart中,所有的外部事件任务都在事件队列中,如IO、计时器、点击、以及绘制事件等,而微任务通常来源于Dart内部,并且微任务非常少,之所以如此,是因为微任务队列优先级高,如果微任务太多,执行时间总和就越久,事件队列任务的延迟也就越久,对于GUI应用来说最直观的表现就是比较卡,所以必须得保证微任务队列不会太长。
- Flutter 单线程是怎么执行异步程序的?
2、强类型语言还是弱类型语言?
-
什么是强类型语言、什么是弱类型语言 ?有哪些优缺点
强类型语言是一种强制类型定义的语言,一旦某一个变量被定义类型,如果不经过强制转换,则它永远就是该数据 类型了,强类型语言包括Java、.net 、Python、C++等语言。 弱类型语言是一种弱类型定义的语言,某一个变量被定义类型,该变量可以根据环境变化自动进行转换,不需要经 过显性强制转换。弱类型语言包括vb 、PHP、javascript等语言。 强类型语言在速度上略逊于弱类型语言,但是强类型定义语言带来的严谨性又能避免不必要的错误, 复制代码
- 强类型的语言可以在编译过程中发现源代码的错误,从而保证程序更加健壮。
- 强类型定义语言带来的严谨性又能避免不必要的错误。
- 强类型语言在速度上略逊于弱类型语言
-
dart属于哪种。Swift属于哪种、OC属于哪种
Dart 本身是一个强类型语言,任何变量都是有确定类型的,在 Dart 中,当用`var`声明一个变量后,Dart 在编译时会根据第一次赋值数据的类型来推断其类型,编译结束后其类型就已经被确定,而 JavaScript 是纯 粹的弱类型脚本语言,var 只是变量的声明方式而已。 复制代码
所以无论是 OC 还是 Swift 都是强类型语言。 JS是弱类型语言。
- 所有的变量必须先声明,后使用;
- 指定类型的变量只能接收类型与之匹配的值。
强类型变量两方面的含义。
3、Dart 关键字
-
dynamic 和 Object 区别
Object
是 Dart 所有对象的根基类,也就是说在 Dart 中所有类型都是Object
的子类(包括Function和Null)dynamic
与Object
不同的是dynamic
声明的对象编译器会提供所有可能的组合,而Object
声明的对象只能使用Object
的属性与方法, 否则编译器会报错(可以使用强制累心转换)dynamic a; Object b = ""; main() { a = ""; printLengths(); } printLengths() { // 正常 print(a.length); // 报错 The getter 'length' is not defined for the class 'Object' print(b.length); } 复制代码
但是这里的 a.xx (xx是a对象没有的属性)。编译也是不会报错的。
反而。b.xx 也可以使用强制类型转换来操作 (b as String).length
-
final 和 const 区别
如果您从未打算更改一个变量,那么使用 final 或 const,不是var,也不是一个类型。 一个 final 变量只能被设置一次,两者区别在于:const 变量是一个编译时常量(编译时直接替换为常量值),final变量在第一次使用时被初始化。 复制代码
-
函数可选参数、命名参数
用[]标记为可选的位置参数,并放在参数列表的最后面
{} 标记可选命名参数
4、Future Stream
Future
随着 ECMAScript 标准发布后,这个问题得到了非常好的解决,而解决回调地狱的两大神器正是 ECMAScript6 引入了Promise,以及ECMAScript7 中引入的async/await。 而在 Dart 中几乎是完全平移了 JavaScript 中的这两者:Future相当于Promise,而async/await连名字都没改。接下来我们看看通过Future和async/await如何消除上面示例中的嵌套问题。
Future与JavaScript中的Promise非常相似,表示一个异步操作的最终完成(或失败)及其结果值的表示。简单来说,它就是用于处理异步操作的,异步处理成功了就执行成功的操作,异步处理失败了就捕获错误或者停止后续操作。一个Future只会对应一个结果,要么成功,要么失败。
Future.delayed(Duration(seconds: 2),(){
//return "hi world!";
throw AssertionError("Error");
}).then((data){
//执行成功会走到这里
print("success");
}).catchError((e){
//执行失败会走到这里
print(e);
}).whenComplete((){
//无论成功或失败都会走到这里
});
复制代码
有些时候,我们需要等待多个异步任务都执行结束后才进行一些操作,比如我们有一个界面,需要先分别从两个网络接口获取数据.Future.wait
,它接受一个Future
数组参数,只有数组中所有Future
都执行成功后,才会触发then
的成功回调
Future.wait([
// 2秒后返回结果
Future.delayed(Duration(seconds: 2), () {
return "hello";
}),
// 4秒后返回结果
Future.delayed(Duration(seconds: 4), () {
return " world";
})
]).then((results){
print(results[0]+results[1]);
}).catchError((e){
print(e);
});
复制代码
Stream.fromFutures([
// 1秒后返回结果
Future.delayed(Duration(seconds: 1), () {
return "hello 1";
}),
// 抛出一个异常
Future.delayed(Duration(seconds: 2),(){
throw AssertionError("Error");
}),
// 3秒后返回结果
Future.delayed(Duration(seconds: 3), () {
return "hello 3";
})
]).listen((data){
print(data);
}, onError: (e){
print(e.message);
},onDone: (){
});
复制代码
async
用来表示函数是异步的,定义的函数会返回一个Future
对象,可以使用 then 方法添加回调函数。await
后面是一个Future
,表示等待该异步任务完成,异步完成后才会往下走;await
必须出现在async
函数内部。
Stream
Stream
也是用于接收异步事件数据,和 Future
不同的是,它可以接收多个异步操作的结果(成功或失败)。 也就是说,在执行异步任务时,可以通过多次触发成功或失败事件来传递结果数据或错误异常。 Stream
常用于会多次读取数据的异步任务场景,如网络内容下载、文件读写等。举个例子:
Stream.fromFutures([
// 1秒后返回结果
Future.delayed(Duration(seconds: 1), () {
return "hello 1";
}),
// 抛出一个异常
Future.delayed(Duration(seconds: 2),(){
throw AssertionError("Error");
}),
// 3秒后返回结果
Future.delayed(Duration(seconds: 3), () {
return "hello 3";
})
]).listen((data){
print(data);
}, onError: (e){
print(e.message);
},onDone: (){
});
复制代码
I/flutter (17666): hello 1
I/flutter (17666): Error
I/flutter (17666): hello 3
复制代码
-
区别。
上面可以看出。 Future 虽然可以处理多个回调。 但是是等多个回调,都完成了在执行then。Stream则是每个都执行。
上面的代码依次会输出:
5、Flutter 的 四棵树
在 Flutter 中, widget 的功能是“描述一个UI元素的配置信息、Widgt是immutable的这会 限制Widget 中定义的属性(即配置信息)必须是不可变的(final),为什么不允许 Widget 中定义的属性变化呢?这是因为,Flutter 中如果属性发生则会重新构建Widget树,即重新创建新的 Widget 实例来替换旧的 Widget 实例,所以允许 Widget 的属性变化是没有意义的。
DiagnosticableTree 诊断树 <= Widget
渲染过程的四棵树 Widget树 => 生成 Element树 => 生成Render树 => layer 树
既然 Widget 只是描述一个UI元素的配置信息,那么真正的布局、绘制是由谁来完成的呢?Flutter 框架的的处理流程是这样的:
- 根据 Widget 树生成一个 Element 树,Element 树中的节点都继承自
Element
类。 - 根据 Element 树生成 Render 树(渲染树),渲染树中的节点都继承自
RenderObject
类。 - 根据渲染树生成 Layer 树,然后上屏显示,Layer 树中的节点都继承自
Layer
类。
真正的布局和渲染逻辑在 Render 树中,Element 是 Widget 和 RenderObject 的粘合剂,可以理解为一个中间代理。我们通过一个例子来说明,假设有如下 Widget 树:
Container( // 一个容器 widget
color: Colors.blue, // 设置容器背景色
child: Row( // 可以将子widget沿水平方向排列
children: [
Image.network('https://www.example.com/1.png'), // 显示图片的 widget
const Text('A'),
],
),
);
复制代码
如果 Container 设置了背景色,Container 内部会创建一个新的 ColoredBox 来填充背景
而 Image 内部会通过 RawImage 来渲染图片、Text 内部会通过 RichText 来渲染文本,所以最终的 Widget树、Element 树、渲染树结构如下:
这里需要注意:
三棵树中,Widget 和 Element 是一一对应的,但并不和 RenderObject 一一对应。比如 StatelessWidget 和 StatefulWidget 都没有对应的 RenderObject。
渲染树在上屏前会生成一棵 Layer 树,这个我们将在后面原理篇介绍,在前面的章节中读者只需要记住以上三棵树就行。
6、Context
它是BuildContext
类的一个实例,表示当前 widget 在 widget 树中的上下文。实际上,context
是当前 widget 在 widget 树中位置中执行”相关操作“的一个句柄(handle),比如它提供了从当前 widget 开始向上遍历 widget 树以及按照 widget 类型查找父级 widget 的方法。下面是在子树中获取父级 widget 的一个示例:
class ContextRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Context测试"),
),
body: Container(
child: Builder(builder: (context) {
// 在 widget 树中向上查找最近的父级`Scaffold` widget
Scaffold scaffold = context.findAncestorWidgetOfExactType<Scaffold>();
// 直接返回 AppBar的title, 此处实际上是Text("Context测试")
return (scaffold.appBar as AppBar).title;
}),
),
);
}
}
复制代码
7、State生命周期
一个 StatefulWidget 类会对应一个 State 类,State表示与其对应的 StatefulWidget 要维护的状态,State 中的保存的状态信息可以:
- 在 widget 构建时可以被同步读取。
- 在 widget 生命周期中可以被改变,当State被改变时,可以手动调用其
setState()
方法通知Flutter 框架状态发生改变,Flutter 框架在收到消息后,会重新调用其build
方法重新构建 widget 树,从而达到更新UI的目的。
State 中有两个常用属性:
widget
,它表示与该 State 实例关联的 widget 实例,由Flutter 框架动态设置。注意,这种关联并非永久的,因为在应用生命周期中,UI树上的某一个节点的 widget 实例在重新构建时可能会变化,但State实例只会在第一次插入到树中时被创建,当在重新构建时,如果 widget 被修改了,Flutter 框架会动态设置State. widget 为新的 widget 实例。context
。StatefulWidget对应的 BuildContext,作用同StatelessWidget 的BuildContext。
-
State生命周期
下面我们来看看各个回调函数:
-
initState
:当 widget 第一次插入到 widget 树时会被调用,对于每一个State对象,Flutter 框架只会调用一次该回调,所以,通常在该回调中做一些一次性的操作,如状态初始化、订阅子树的事件通知等。不能在该回调中调用BuildContext.dependOnInheritedWidgetOfExactType
(该方法用于在 widget 树上获取离当前 widget 最近的一个父级InheritedWidget
,关于InheritedWidget
我们将在后面章节介绍),原因是在初始化完成后, widget 树中的InheritFrom widget
也可能会发生变化,所以正确的做法应该在在build()
方法或didChangeDependencies()
中调用它。 -
didChangeDependencies()
:当State对象的依赖发生变化时会被调用;例如:在之前build()
中包含了一个InheritedWidget
,然后在之后的build()
中Inherited widget
发生了变化,那么此时Inherited widget
的子 widget 的didChangeDependencies()
回调都会被调用。典型的场景是当系统语言 Locale 或应用主题改变时,Flutter 框架会通知 widget 调用此回调。 -
build()
:此回调读者现在应该已经相当熟悉了,它主要是用于构建 widget 子树的,会在如下场景被调用:- 在调用
initState()
之后。 - 在调用
didUpdateWidget()
之后。 - 在调用
setState()
之后。 - 在调用
didChangeDependencies()
之后。 - 在State对象从树中一个位置移除后(会调用deactivate)又重新插入到树的其它位置之后。
- 在调用
-
reassemble()
:此回调是专门为了开发调试而提供的,在热重载(hot reload)时会被调用,此回调在Release模式下永远不会被调用。 -
didUpdateWidget ()
:在 widget 重新构建时,Flutter 框架会调用widget.canUpdate
来检测 widget 树中同一位置的新旧节点,然后决定是否需要更新,如果widget.canUpdate
返回true
则会调用此回调。正如之前所述,widget.canUpdate
会在新旧 widget 的key
和runtimeType
同时相等时会返回true,也就是说在在新旧 widget 的key和runtimeType同时相等时didUpdateWidget()
就会被调用。 -
deactivate()
:当 State 对象从树中被移除时,会调用此回调。在一些场景下,Flutter 框架会将 State 对象重新插到树中,如包含此 State 对象的子树在树的一个位置移动到另一个位置时(可以通过GlobalKey 来实现)。如果移除后没有重新插入到树中则紧接着会调用dispose()
方法。 -
dispose()
:当 State 对象从树中被永久移除时调用;通常在此回调中释放资源。
-
8、StatefulWidget 、为什么要设计出一个state。
Flutter 设计的 Widget 和 State 就是分开的, 一个负责渲染、一个负责生命周期、热重载、刷新等功能。 他只能是帮页面和状态分开设计。
但是State里面依然有两个比较重要的属性。
widget
,它表示与该 State 实例关联的 widget 实例,由Flutter 框架动态设置。注意,这种关联并非永久的,因为在应用生命周期中,UI树上的某一个节点的 widget 实例在重新构建时可能会变化,但State实例只会在第一次插入到树中时被创建,当在重新构建时,如果 widget 被修改了,Flutter 框架会动态设置State. widget 为新的 widget 实例。context
。StatefulWidget对应的 BuildContext,作用同StatelessWidget 的BuildContext。
9、为什么build方法放在 State 中?
主要是为了提高开发的灵活性。如果将build()
方法在StatefulWidget
中则会有两个问题:
1、状态访问不便。
我们的StatefulWidget
有很多状态,而每次状态改变都要调用build
方法。如果放在StatefulWidget中不方便调用。
2、承StatefulWidget
不便。
如果 build 在 Widget 中。 他的 build 方法、 必须帮 state 传过去。 那他的子类就只能使用父类的 state 这样设计就不太合理了。
@override
Widget build(BuildContext context, State state){
//由于子类要用到AnimatedWidget 的状态对象_animatedWidgetState,
//所以AnimatedWidget 必须通过某种方式将其状态对象_animatedWidgetState
//暴露给其子类
super.build(context, _animatedWidgetState)
}
复制代码
10、子 widget 获取父级的State
book.flutterchina.club/chapter2/fl…
-
1、通过Context获取
context
对象有一个findAncestorStateOfType()
方法,该方法可以从当前节点沿着 widget 树向上查找指定类型的 StatefulWidget 对应的 State 对象。一般来说,如果 StatefulWidget 的状态是私有的(不应该向外部暴露),那么我们代码中就不应该去直接获取其 State 对象;如果StatefulWidget的状态是希望暴露出的(通常还有一些组件的操作方法),我们则可以去直接获取其State对象。但是通过
context.findAncestorStateOfType
获取 StatefulWidget 的状态的方法是通用的,我们并不能在语法层面指定 StatefulWidget 的状态是否私有,所以在 Flutter 开发中便有了一个默认的约定:如果 StatefulWidget 的状态是希望暴露出的,应当在 StatefulWidget 中提供一个
of
静态方法来获取其 State 对象,开发者便可直接通过该方法来获取;如果 State不希望暴露,则不提供
of
方法这个约定在 Flutter SDK 里随处可见。所以,上面示例中的
Scaffold
也提供了一个of
方法,我们其实是可以直接调用它的:// 直接通过of静态方法来获取ScaffoldState ScaffoldState _state=Scaffold.of(context); static ScaffoldState of(BuildContext context) { assert(context != null); final ScaffoldState? result = context.findAncestorStateOfType<ScaffoldState>(); if (result != null) return result; ); } 复制代码
-
2、 通过GlobalKey
Flutter还有一种通用的获取
State
对象的方法——通过GlobalKey来获取! 步骤分两步:-
给目标
StatefulWidget
添加GlobalKey
。//定义一个globalKey, 由于GlobalKey要保持全局唯一性,我们使用静态变量存储 static GlobalKey<ScaffoldState> _globalKey= GlobalKey(); ... Scaffold( key: _globalKey , //设置key ... ) 复制代码
-
通过
GlobalKey
来获取State
对象_globalKey.currentState.openDrawer() 复制代码
注意:使用 GlobalKey 开销较大,如果有其他可选方案,应尽量避免使用它。另外,同一个 GlobalKey 在整个 widget 树中必须是唯一的,不能重复 复制代码
GlobalKey 是 Flutter 提供的一种在整个 App 中引用 element 的机制。如果一个 widget 设置了
GlobalKey
,那么我们便可以通过globalKey.currentWidget
获得该 widget 对象、globalKey.currentElement
来获得 widget 对应的element对象,如果当前 widget 是StatefulWidget
,则可以通过globalKey.currentState
来获得该 widget 对应的state对象。
-
* 11、pubspec.yaml
一、逐一解释一下各个字段的意义:
-
name
:应用或包名称。 -
description
: 应用或包的描述、简介。 -
version
:应用或包的版本号。 -
dependencies
:应用或包依赖的其它包或插件。 -
dev_dependencies
:开发环境依赖的工具包(而不是flutter应用本身依赖的包)。 -
flutter
:flutter相关的配置选项。
二、依赖
-
最常用的pub仓库包。。 依赖方式
english_words: ^4.0.0 复制代码
直接依赖便可
-
依赖本地包
如果我们正在本地开发一个包,包名为pkg1,我们可以通过下面方式依赖:
dependencies: pkg1: path: ../../code/pkg1 复制代码
路径可以是相对的,也可以是绝对的。
-
依赖git包
dependencies: pkg1: git: url: git://github.com/xxx/pkg1.git 复制代码
dependencies: package1: git: url: git://github.com/flutter/packages.git path: packages/package1 复制代码
上面假定包位于Git存储库的根目录中。如果不是这种情况,可以使用path参数指定相对位置,例如:
12、assets
Flutter APP 安装包中会包含代码和 assets(资源)两部分。Assets 是会打包到程序安装包中的,可在运行时访问。常见类型的 assets 包括静态数据(例如JSON文件)、配置文件、图标和图片等。
在构建期间,Flutter 将 asset 放置到称为 asset bundle 的特殊存档中,应用程序可以在运行时读取它们 (但不能修改) 。
-
assets 变体
flutter: assets: - graphics/background.png 复制代码
那么这两个
graphics/background.png
和graphics/dark/background.png
都将包含在您的 asset bundle中。前者被认为是main asset (主资源),后者被认为是一种变体(variant)。 -
加载本地 assets
-
通过
rootBundle
(opens new window)对象加载:每个Flutter应用程序都有一个rootBundle
(opens new window)对象, 通过它可以轻松访问主资源包,直接使用package:flutter/services.dart
中全局静态的rootBundle
对象来加载asset即可。 -
通过
DefaultAssetBundle
(opens new window)加载:建议使用DefaultAssetBundle
(opens new window)来获取当前 BuildContext 的AssetBundle。 这种方法不是使用应用程序构建的默认 asset bundle,而是使父级 widget 在运行时动态替换的不同的 AssetBundle,这对于本地化或测试场景很有用。通常,可以使用
DefaultAssetBundle.of()
在应用运行时来间接加载 asset(例如JSON文件),而在widget 上下文之外,或其它AssetBundle
句柄不可用时,可以使用rootBundle
直接加载这些 asset,例如:import 'dart:async' show Future; import 'package:flutter/services.dart' show rootBundle; Future<String> loadAsset() async { return await rootBundle.loadString('assets/config.json'); } 复制代码
-
-
设置特地平台的,开机图片和app图标。
-
加载图片
类似于原生开发,Flutter也可以为当前设备加载适合其分辨率的图像。
- …/my_icon.png
- …/2.0x/my_icon.png
- …/3.0x/my_icon.png
AssetImage('graphics/background.png')
-
设置特地平台的,开机图片和app图标。 book.flutterchina.club/chapter2/fl…
... 更新中