第一章: 初识Flutter
1.1 Flutter简介
Flutter 是 Google 推出并开源的移动应用开发框架,主打跨平台、高保真、高性能。开发者可以通过 Dart 语言开发 App,一套代码同时运行在 iOS 和 Android平台。 Flutter 提供了丰富的组件、接口,开发者可以很快地为 Flutter 添加 Native 扩展。
跨平台自绘引擎
Flutter 与用于构建移动应用程序的其它大多数框架不同,因为 Flutter 既不使用 WebView,也不使用操作系统的原生控件。 相反,Flutter 使用自己的高性能渲染引擎来绘制 Widget(组件)。这样不仅可以保证在 Android 和iOS 上 UI 的一致性,也可以避免对原生控件依赖而带来的限制及高昂的维护成本。
高性能
Flutter 高性能主要靠两点来保证:
第一:Flutter APP 采用 Dart 语言开发。Dart 在 JIT(即时编译)模式下,执行速度与 JavaScript 基本持平。但是 Dart 支持 AOT,当以 AOT模式运行时,JavaScript 便远远追不上了。执行速度的提升对高帧率下的视图数据计算很有帮助。
第二:Flutter 使用自己的渲染引擎来绘制 UI ,布局数据等由 Dart 语言直接控制,所以在布局过程中不需要像 RN 那样要在 JavaScript 和 Native 之间通信,这在一些滑动和拖动的场景下具有明显优势,因为在滑动和拖动过程往往都会引起布局发生变化,所以 JavaScript 需要和 Native 之间不停的同步布局信息,这和在浏览器中JavaScript 频繁操作 DOM 所带来的问题是类似的,都会导致比较可观的性能开销。
采用Dart语言开发
1、开发效率高。
Dart 运行时和编译器支持 Flutter 的两个关键特性的组合:
基于 JIT 的快速开发周期:Flutter 在开发阶段采用,采用 JIT 模式,这样就避免了每次改动都要进行编译,极大的节省了开发时间;
基于 AOT 的发布包: Flutter 在发布时可以通过 AOT 生成高效的机器码以保证应用性能。而 JavaScript 则不具有这个能力。
2、高性能。
Flutter 旨在提供流畅、高保真的的 UI 体验。为了实现这一点,Flutter 中需要能够在每个动画帧中运行大量的代码。这意味着需要一种既能提供高性能的语言,而不会出现会丢帧的周期性暂停,而 Dart 支持 AOT,在这一点上可以做的比 JavaScript 更好。
3、类型安全和空安全。
由于 Dart 是类型安全的语言,且 2.12 版本后也支持了空安全特性,所以 Dart 支持静态类型检测,可以在编译前发现一些类型的错误,并排除潜在问题,这一点对于前端开发者来说可能会更具有吸引力。与之不同的,JavaScript 是一个弱类型语言,也因此前端社区出现了很多给 JavaScript 代码添加静态类型检测的扩展语言和工具,如:微软的 TypeScript 以及Facebook 的 Flow。相比之下,Dart 本身就支持静态类型,这是它的一个重要优势。
1.2 Flutter开发环境搭建
1.2.1 安装Flutter
去flutter官网下载其最新可用的安装包,下载地址:https://flutter.dev/docs/development/tools/sdk/releases 。
注意,Flutter的渠道版本会不停变动,请以Flutter官网为准。另外,在中国大陆地区,要想正常获取安装包列表或下载安装包,可能需要翻墙,读者也可以去Flutter github项目下去下载安装包,地址:https://github.com/flutter/flutter/releases 。
由于在国内访问Flutter有时可能会受到限制,Flutter官方为中国开发者搭建了临时镜像,将如下环境变量加入到用户环境变量中:
export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
注意: 此镜像为临时镜像,并不能保证一直可用,读者可以参考https://flutter.dev/community/china 以获得有关镜像服务器的最新动态。
1.2.2 环境变量更新
将Flutter添加到PATH中,可以在任何终端会话中运行flutter命令。如Mac系统一般默认shell是bash_shell,需要在bash_profile中配置
export FLUTTER=/Users/mac-jfyd/flutter/bin
export PATH=$FLUTTER:$PATH
运行 source $HOME/.bash_profile 刷新当前终端窗口。
1.2.3 运行 flutter doctor命令
flutter doctor
该命令检查你的环境并在命令行窗口中显示报告。Dart SDK已经在打包在Flutter SDK里了,没有必要单独安装Dart。 仔细检查命令行输出以获取可能需要安装的其他软件或进一步需要执行的任务。
1
第一次运行flutter命令(如flutter doctor)时,它会下载它自己的依赖项并自行编译。以后再运行就会快得多。缺失的依赖需要安装一下,安装完成后再运行flutter doctor命令来验证是否安装成功。
1.2.4 创建Flutter APP
1、用Android Studio创建
2、终端
flutter create 项目名称
也可以选择一些配置,如开发语言
flutter create -i objc -a java 项目名称
第二章 Widget
2.1 Widget介绍
在Flutter中几乎所有的对象都是一个 widget 。与原生开发中“控件”不同的是,Flutter 中的 widget 的概念更广泛,它不仅可以表示UI元素,也可以表示一些功能性的组件如:用于手势检测的 GestureDetector 、用于APP主题数据传递的 Theme 等等,而原生开发中的控件通常只是指UI元素。由于 Flutter 主要就是用于构建用户界面的,所以,在大多数时候,读者可以认为 widget 就是一个控件。
Flutter 中是通过 Widget 嵌套 Widget 的方式来构建UI和进行实践处理的,所以记住,Flutter 中万物皆为Widget。
在 Flutter 中, widget 的功能是“描述一个UI元素的配置信息”,就是说, Widget 其实并不是表示最终绘制在设备屏幕上的显示元素,所谓的配置信息就是 Widget 接收的参数,比如对于 Text 来讲,文本的内容、对齐方式、文本样式都是它的配置信息。如下:
Text(
'发现',
style: TextStyle(fontSize: 15,color: Colors.black),
textAlign: TextAlign.center,
),
2.2 无状态的StatelessWidget
StatelessWidget相对比较简单,它继承自widget类,重写了createElement()法,该方法用于构建Element树,在下面的渲染原理篇会详细介绍
StatelessWidget用于不需要维护状态的场景,它通常在build方法中通过嵌套其它 widget 来构建UI,在构建过程中会递归的构建其嵌套的 widget ,如
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
highlightColor: Color.fromRGBO(1, 0, 0, 0.0),
splashColor: Color.fromRGBO(1, 0, 0, 0.0),
cardColor: Color.fromRGBO(1, 1, 1, 0.65),//有透明层叠视图的设置
primarySwatch: Colors.blue,
appBarTheme: AppBarTheme(iconTheme: IconThemeData(color: Colors.black))
),
home: RootPage(),
);
}
}
2.3 有状态的StatefulWidget
和StatelessWidget一样,StatefulWidget也是继承自widget类,并重写了createElement()方法,不同的是返回的Element 对象并不相同;另外StatefulWidget类中添加了一个新的接口createState()。
createState() 用于创建和 StatefulWidget 相关的状态,它在StatefulWidget 的生命周期中可能会被多次调用。例如,当一个 StatefulWidget 同时插入到 widget 树的多个位置时,Flutter 框架就会调用该方法为每一个位置生成一个独立的State实例,其实,本质上就是一个StatefulElement对应一个State实例。
class RootPage extends StatefulWidget {
const RootPage({Key? key}) : super(key: key);
@override
_RootPageState createState() => _RootPageState();
}
2.4 State
一个 StatefulWidget 类会对应一个 State 类,State表示与其对应的 StatefulWidget 要维护的状态,State 中的保存的状态信息可以:
1、在 widget 构建时可以被同步读取。
2、在 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。
class _RootPageState extends State<RootPage> {
int _currentIndex = 0;
List <Widget> _pages = [ChatPage(),FriendsPage(),DiscoverPage(),MinePage()];
final PageController _controller = PageController(initialPage: 0);
@override
Widget build(BuildContext context) {
return Scaffold(
body: PageView( //通过pageView 来保持状态
physics: NeverScrollableScrollPhysics(), //不允许滚动
children: _pages,
controller: _controller,
),
bottomNavigationBar: BottomNavigationBar(
onTap: (index){
setState(() {
_currentIndex = index;
});
_controller.jumpToPage(index);
},
selectedFontSize: 12,
unselectedFontSize: 12,
currentIndex: _currentIndex,
fixedColor: Colors.green,
type: BottomNavigationBarType.fixed,
items: [
BottomNavigationBarItem(
icon: Image.asset('images/tabbar_chat.png',height: 25,width: 25),
activeIcon: Image.asset('images/tabbar_chat_hl.png',height: 25,width: 25,),
label: '微信'
),
BottomNavigationBarItem(
icon: Image.asset('images/tabbar_friends.png',height: 25,width: 25,),
activeIcon: Image.asset('images/tabbar_friends_hl.png',height: 25,width: 25,),
label: '通讯录'
),
BottomNavigationBarItem(
icon: Image.asset('images/tabbar_discover.png',height: 25,width: 25,),
activeIcon: Image.asset('images/tabbar_discover_hl.png',height: 25,width: 25,),
label: '发现'
),
BottomNavigationBarItem(
icon: Image.asset('images/tabbar_mine.png',height: 25,width: 25,),
activeIcon: Image.asset('images/tabbar_mine_hl.png',height: 25,width: 25,),
label: '我'
),
],
),
);
}
}
2.5 State生命周期
理解State的生命周期对flutter开发非常重要,下面以一个计算器的例子来介绍,实现一个计数器 CounterWidget 组件 ,点击它可以使计数器加1
class CounterDemo extends StatefulWidget {
const CounterDemo({this.initValue = 0});
final int initValue;
@override
_CounterDemoState createState() => _CounterDemoState();
}
class _CounterDemoState extends State<CounterDemo> {
late int _counter;
@override
void initState() {
// TODO: implement initState
_counter = widget.initValue;
super.initState();
print("initState");
}
@override
Widget build(BuildContext context) {
print("build");
return Scaffold(
body: Center(
child: FlatButton(
child: Text('$_counter'),
//点击后计数器自增
onPressed:()=>setState(()=> ++_counter,
),
),
),
);
}
@override
void didUpdateWidget(CounterDemo oldWidget) {
super.didUpdateWidget(oldWidget);
print("didUpdateWidget");
}
//当state对象从渲染树中移出的时候,就会调用,即将销毁
@override
void deactivate() {
super.deactivate();
print("deactive");
}
//销毁,类似iOS原生dealloc,或c++的析构函数
@override
void dispose() {
super.dispose();
print("dispose");
}
//重新组装,点击热重载后
@override
void reassemble() {
super.reassemble();
print("reassemble");
}
//依赖关系改变的时候
@override
void didChangeDependencies() {
super.didChangeDependencies();
print("didChangeDependencies");
}
}
下面我们来看看各个回调函数:
-
initState:当 widget 第一次插入到 widget 树时会被调用,对于每一个State对象,Flutter 框架只会调用一次该回调。
-
didChangeDependencies():当State对象的依赖发生变化时会被调用;例如:在之前build() 中包含了一个Inherited widget,然后在之后的build() 中Inherited widget发生了变化,那么此时Inherited widget的子 widget 的didChangeDependencies()回调都会被调用。典型的场景是当系统语言 Locale 或应用主题改变时,Flutter 框架会通知 widget 调用此回调。
-
build():它主要是用于构建 widget 子树的,会在如下场景被调用:
1、在调用initState()之后。
2、在调用didUpdateWidget()之后。
3、 在调用setState()之后。
4、在调用didChangeDependencies()之后。
5、在State对象从树中一个位置移除后(会调用deactivate)又重新插入到树的其它位置之后。 -
reassemble():此回调是专门为了开发调试而提供的,在热重载(hot reload)时会被调用,此回调在Release模式下永远不会被调用。
-
didUpdateWidget ():在 widget 重新构建时,Flutter 框架会调用widget.canUpdate来检测 widget 树中同一位置的新旧节点,然后决定是否需要更新,如果widget.canUpdate返回true则会调用此回调。
-
deactivate():当 State 对象从树中被移除时,会调用此回调。在一些场景下,Flutter 框架会将 State 对象重新插到树中,如包含此 State 对象的子树在树的一个位置移动到另一个位置时(可以通过GlobalKey 来实现)。如果移除后没有重新插入到树中则紧接着会调用dispose()方法。
-
dispose():当 State 对象从树中被永久移除时调用;通常在此回调中释放资源。
2.6 渲染原理
Flutter 中的三棵树Widget 树、Element 树、Render 树。三棵树中,Widget 和 Element 是一一对应的,但并不和 RenderObject 一一对应。比如 StatelessWidget 和 StatefulWidget 都没有对应的 RenderObject。
2
2.7 Key
key本身是一个抽象类,包含LocalKey:用作Element和Widget比较,增量式渲染。GlobalKey:可以获取到对应的Widget的State对象!用来局部渲染
- LocalKey: Widget构造方法中使用,用来标记,目的是为了防止渲染出错
class KeyDemo extends StatefulWidget {
const KeyDemo({Key? key}) : super(key: key);
@override
_KeyDemoState createState() => _KeyDemoState();
}
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
- GlobalKey: 为了更好的优化性能,我们在布局的时候不应该整个使用StatefulWidget,如果整个页面只有某个Widget是有状态的,可以使用GlobalKey实现局部更新。
class GlobalKeyDemo extends StatelessWidget {
GlobalKeyDemo({Key? key}) : super(key: key);
final GlobalKey<_ChildPageState> _globalKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('GlobalkeyDemo'),),
body: ChildPage(key: _globalKey,),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: (){
_globalKey.currentState!.count++;
_globalKey.currentState!.data = 'hello' + (_globalKey.currentState!.count).toString();
_globalKey.currentState!.setState(() {
});
},
),
);
}
}
class ChildPage extends StatefulWidget {
const ChildPage({Key? key}) : super(key: key);
@override
_ChildPageState createState() => _ChildPageState();
}
class _ChildPageState extends State<ChildPage> {
int count = 0;
String data = 'hello';
@override
Widget build(BuildContext context) {
return Center(
child: Column(
children: <Widget>[
Text(count.toString()),
Text(data)
],
),
);
}
}
2.8 Widget组件
Widget组件众多如:
- 基础组件Image、Text等;
- 布局类组件:理解主轴和纵轴的概念
1、线性布局Row(横向)、Column(纵向)
2、流式布局Wrap、Flow
3、层叠布局 Stack、Positioned
4、弹性布局Expanded
、、、等 - 容器类组件
填充(Padding)、限制类容器SizedBox、容器Container、导航Scaffold等 - 可滚动组件
ListView、Page View、TabBarView等
、、、、
还有许多常用的组件,组件具体是如使用可参考Flutter中文网中《Flutter实战》
第三章 网络请求
3.1 事件循环机制
在开发中,我们经常会遇到一些耗时的操作需要完成,比如网络请求、文件读取等等;
如何处理耗时的操作?
针对如何处理耗时的操作,不同的语言有不同的处理方式。
-
处理方式一: 多线程,比如Java、iOS,我们普遍的做法是开启一个新的线程(Thread),在新的线程中完成这些异步的操作,再通过线程间通信的方式,将拿到的数据传递给主线程。
-
处理方式二: 单线程+事件循环,比如JavaScript、Dart都是基于单线程加事件循环来完成耗时操作的处理
Dart事件循环(Event Loop)
事件循环是什么呢?
- 事件循环并不复杂,它就是将需要处理的一系列事件(包括点击事件、IO事件、网络事件)放在一个事件队列(Event Queue)中。
- 不断的从事件队列(Event Queue)中取出事件,并执行其对应需要执行的代码块,直到事件队列清空位置
- 事件队列(Event Queue)和微任务队列(Microtask Queue),微任务队列优先级高于事件队列
代码的执行顺序是怎样的?
3.png
Dart的异步操作
通过Future函数来创建一个异步操作,Future中通常有两个函数执行体:
- Future构造函数传入的函数体,默认会放到事件队列中
- then的函数体(catchError等同看待)
那么它们是加入到什么队列中的呢?
- Future构造函数传入的函数体放在事件队列中
- then的函数体要分成三种情况:
- 情况一:Future没有执行完成(有任务需要执行),那么then会直接被添加到Future的函数执行体后;
- 情况二:如果Future执行完后就then,该then的函数体被放到如微任务队列,当前Future执行完后执行微任务队列;
- 情况三:如果Future世链式调用,意味着then未执行完,下一个then不会执行;
//微任务队列的优先级高于事件队列
void testFuture() {
print('外部代码1');
Future x1 = Future(() => null); //事件队列
x1.then((value) {
print('D');
scheduleMicrotask(() => print('E')); //这里只是创建微任务,所以先执行then
}).then((value) => print('C'));
Future(() => print('A')).then((value) => print('A结束'));
Future(() => print('B')).then((value) => print('B结束'));
//微任务队列
scheduleMicrotask(() {
print('微任务结束');
});
print('外部代码2');
}
执行顺序如何
4.png
await实现同步
//异步方法,请求数据
Future<List<Chat>> getDatas() async {
final response = await http.get(
'http://rap2api.taobao.org/app/mock/290366/api/chat/list',
timeOut: 1000); //等待回调,返回一个Future
if (response.statusCode == 200) {
//获取数据,并转成Map类型
print(response.data); //response.data本身就是map
final chatListMap = response.data['chat_list'];
//遍历转模型,map遍历的结果返回去,箭头函数相等于 (item) {return Chat.fromJson(item)}
List<Chat> chatList =
chatListMap.map<Chat>((item) => Chat.fromJson(item)).toList();
return chatList;
} else {
throw Exception('statusCode: ${response.statusCode}');
}
}
3.2 多线程
Isolate的理解
Dart是单线程的,这个线程有自己可以访问的内存空间以及需要运行的事件循环,看起来更像是进程。我们可以将这个空间系统称之为是一个Isolate,比如Flutter中就有一个Root Isolate,负责运行Flutter的代码,比如UI渲染、用户交互等等。在 Isolate 中,资源隔离做得非常好,每个 Isolate 都有自己的 Event Loop 与 Queue,Isolate 之间不共享任何资源,只能依靠消息机制通信,因此也就没有资源抢占问题。但是,如果只有一个Isolate,那么意味着我们只能永远利用一个线程,这对于多核CPU来说,是一种资源的浪费。
Isolate创建和通信
如果在开发中,我们有非常多耗时的计算,完全可以自己创建Isolate,在独立的Isolate中完成想要的计算操作。
int a = 10;
Future<void> test1() async {
print('外部代码1');
//多线程,看起来更像进程,因为有独立的内存空间,好处是不用担心多线程的资源抢夺
Isolate.spawn(func2, 100);
//通信,创建port
ReceivePort port = ReceivePort();
Isolate iso = await Isolate.spawn(func3, port.sendPort);
//监听
port.listen((message) {
a = message;
print('--a现在是$a');
port.close();
iso.kill();
});
print('回来之后的a是$a'); //这里a还是10,证明了Isolate空间是独立的
print('外部代码2');
}
void func2(int count) {
a = count;
print('第二个来了!');
print('a现在是$a');
}
void func3(SendPort port) {
port.send(1000);
}
compute函数
上面的通信只是单向通信,如果需要双向通信呢?事实上双向通信的代码会比较麻烦。Flutter提供了支持并发计算的compute 函数,它内部封装了Isolate的创建和双向通信,使用起来也非常简单;
Future<void> computeTest() async {
print('外部代码1');
//compute可接受返回值,不用通过port
int x = await compute(func4,10);
print('x: $x');
print('外部代码2');
}
int func4(int count) {
return 10000;
}
3.3 网络请求组件
-
http官方组件
5.png
打开Dart和Flutter应用程序的官方软件包库,搜索框中输入http后如下,具体使用参照官方
-
Dio三方组件
Dio封装的更好,使用更方便,也可在官方上搜索具体用法,下面利用Dio 组件封装了请求类
class HttpManager {
static final Dio dio = Dio();
static Future<Response> request(
String url, {
String method = 'get',
Map<String, String>? headers,
int? timeOut,
Map<String, dynamic>? queryParameters,
}) {
//创建配置
final options = Options(method: method, headers: headers,receiveTimeout: timeOut);
// 发送请求
return dio.request(url,
options: options,
queryParameters: queryParameters);
}
}
//get方法
Future<Response> get(String url,
{Map<String, String>? headers, Map<String, dynamic>? queryParameters,int? timeOut}) {
return HttpManager.request(url,
method: 'get', headers: headers, queryParameters: queryParameters,timeOut: timeOut);
}
网友评论