You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

154 lines
12 KiB
Markdown

2 years ago
# 05 | 从标准模板入手体会Flutter代码是如何运行在原生系统上的
你好,我是陈航。
在专栏的第一篇预习文章中我和你一起搭建了Flutter的开发环境并且通过自带的hello\_world示例和你演示了Flutter项目是如何运行在Android和iOS模拟器以及真机上的。
今天我会通过Android Studio创建的Flutter应用模板带你去了解Flutter的项目结构分析Flutter工程与原生Android和iOS工程有哪些联系体验一个有着基本功能的Flutter应用是如何运转的从而加深你对构建Flutter应用的关键概念和技术的理解。
如果你现在还不熟悉Dart语言也不用担心只要能够理解基本的编程概念比如类型、变量、函数和面向对象并具备一定的前端基础比如了解View是什么、页面基本布局等基础知识就可以和我一起完成今天的学习。而关于Dart语言基础概念的讲述、案例分析我会在下一个模块和你展开。
## 计数器示例工程分析
首先我们打开Android Studio创建一个Flutter工程应用flutter\_app。Flutter会根据自带的应用模板自动生成一个简单的计数器示例应用Demo。我们先运行此示例效果如下
![](https://static001.geekbang.org/resource/image/3a/24/3afe6b35238d1e57c8ae6bec9be61624.png)
图1 计数器示例运行效果
每点击一次右下角带“+”号的悬浮按钮,就可以看到屏幕中央的数字随之+1。
### 工程结构
在体会了示例工程的运行效果之后我们再来看看Flutter工程目录结构了解Flutter工程与原生Android和iOS工程之间的关系以及这些关系是如何确保一个Flutter程序可以最终运行在Android和iOS系统上的。
![](https://static001.geekbang.org/resource/image/e7/fc/e7ecbd5c21895e396c14154b2f226dfc.png)
图2 Flutter工程目录结构
可以看到除了Flutter本身的代码、资源、依赖和配置之外Flutter工程还包含了Android和iOS的工程目录。
这也不难理解因为Flutter虽然是跨平台开发方案但却需要一个容器最终运行到Android和iOS平台上所以**Flutter工程实际上就是一个同时内嵌了Android和iOS原生子工程的父工程**我们在lib目录下进行Flutter代码的开发而某些特殊场景下的原生功能则在对应的Android和iOS工程中提供相应的代码实现供对应的Flutter代码引用。
Flutter会将相关的依赖和构建产物注入这两个子工程最终集成到各自的项目中。而我们开发的Flutter代码最终则会以原生工程的形式运行。
### 工程代码
在对Flutter的工程结构有了初步印象之后我们就可以开始学习Flutter的项目代码了。
Flutter自带的应用模板也就是这个计数器示例对初学者来说是一个极好的入门范例。在这个简单示例中从基础的组件、布局到手势的监听再到状态的改变Flutter最核心的思想在这60余行代码中展现得可谓淋漓尽致。
为了便于你学习理解领会构建Flutter程序的大体思路与关键技术而不是在一开始时就陷入组件的API细节中我删掉了与核心流程无关的组件配置代码及布局逻辑在不影响示例功能的情况下对代码进行了改写并将其分为两部分
* 第一部分是应用入口、应用结构以及页面结构可以帮助你理解构建Flutter程序的基本结构和套路
* 第二部分则是页面布局、交互逻辑及状态管理能够帮你理解Flutter页面是如何构建、如何响应交互以及如何更新的。
首先,我们来看看**第一部分的代码**,也就是应用的整体结构:
```
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) => MaterialApp(home: MyHomePage(title: 'Flutter Demo Home Page'));
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
Widget build(BuildContext context) => {...};
}
```
在本例中Flutter应用为MyApp类的一个实例而MyApp类继承自StatelessWidget类这也就意味着应用本身也是一个Widget。事实上在Flutter中Widget是整个视图描述的基础在Flutter的世界里包括应用、视图、视图控制器、布局等在内的概念都建立在Widget之上**Flutter的核心设计思想便是一切皆Widget**。
Widget是组件视觉效果的封装是UI界面的载体因此我们还需要为它提供一个方法来告诉Flutter框架如何构建UI界面这个方法就是build。
在build方法中我们通常通过对基础Widget进行相应的UI配置或是组合各类基础Widget的方式进行UI的定制化。比如在MyApp中我通过MaterialApp这个Flutter App框架设置了应用首页即MyHomePage。当然MaterialApp也是一个Widget。
MaterialApp类是对构建material设计风格应用的组件封装框架里面还有很多可配置的属性比如应用主题、应用名称、语言标识符、组件路由等。但是这些配置属性并不是本次分享的重点如果你感兴趣的话可以参考Flutter官方的[API文档](https://api.flutter.dev/flutter/material/MaterialApp/MaterialApp.html)来了解MaterialApp框架的其他配置能力。
MyHomePage是应用的首页继承自StatefulWidget类。这代表着它是一个有状态的WidgetStateful Widget而\_MyHomePageState就是它的状态。
如果你足够细心的话就会发现虽然MyHomePage类也是Widget但与MyApp类不同的是它并没有一个build方法去返回Widget而是多了一个createState方法返回\_MyHomePageState对象而build方法则包含在这个\_MyHomePageState类当中。
那么,**StatefulWidget与StatelessWidget的接口设计为什么会有这样的区别呢**
这是因为Widget需要依据数据才能完成构建而对于StatefulWidget来说其依赖的数据在Widget生命周期中可能会频繁地发生变化。由State创建Widget以数据驱动视图更新而不是直接操作UI更新视觉属性代码表达可以更精炼逻辑也可以更清晰。
在了解了计数器示例程序的整体结构以后,我们再来看看这个**示例代码的第二部分**,也就是页面布局及交互逻辑部分。
```
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() => setState(() {_counter++;});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(Widget.title)),
body: Text('You have pushed the button this many times:$_counter')),
floatingActionButton: FloatingActionButton(onPressed: _incrementCounter)
);
}
```
\_MyHomePageState中创建的Widget Scaffold是Material库中提供的页面布局结构它包含AppBar、Body以及FloatingActionButton。
* AppBar是页面的导航栏我们直接将MyHomePage中的title属性作为标题使用。
* body则是一个Text组件显示了一个根据\_counter属性可变的文本You have pushed the button this many times:$\_counter
* floatingActionButton则是页面右下角的带“+”的悬浮按钮。我们将\_incrementCounter作为其点击处理函数。
\_incrementCounter的实现很简单使用setState方法去自增状态属性\_counter。setState方法是Flutter以数据驱动视图更新的关键函数它会通知Flutter框架我这儿有状态发生了改变赶紧给我刷新界面吧。而Flutter框架收到通知后会执行Widget的build方法根据新的状态重新构建界面。
**这里需要注意的是状态的更改一定要配合使用setState。**通过这个方法的调用Flutter会在底层标记Widget的状态随后触发重建。于我们的示例而言即使你修改了\_counter如果不调用setStateFlutter框架也不会感知到状态的变化因此界面上也不会有任何改变你可以动手验证一下
下面的图3就是整个计数器示例的代码流程示意图。通过这张图你就能够把这个实例的整个代码流程串起来了
![](https://static001.geekbang.org/resource/image/34/36/3401b99587eafa7c0c1ed446eb936f36.png)
图3 代码流程示意图
MyApp为Flutter应用的运行实例通过在main函数中调用runApp函数实现程序的入口。而应用的首页则为MyHomePage一个拥有\_MyHomePageState状态的StatefulWidget。\_MyHomePageState通过调用build方法以相应的数据配置完成了包括导航栏、文本及按钮的页面视图的创建。
而当按钮被点击之后,其关联的控件函数\_incrementCounter会触发调用。在这个函数中通过调用setState方法更新\_counter属性的同时也会通知Flutter框架其状态发生变化。随后Flutter会重新调用build方法以新的数据配置重新构建\_MyHomePageState的UI最终完成页面的重新渲染。
Widget只是视图的“配置信息”是数据的映射是“只读”的。对于StatefulWidget而言当数据改变的时候我们需要重新创建Widget去更新界面这也就意味着Widget的创建销毁会非常频繁。
为此Flutter对这个机制做了优化其框架内部会通过一个中间层去收敛上层UI配置对底层真实渲染的改动从而最大程度降低对真实渲染视图的修改提高渲染效率而不是上层UI配置变了就需要销毁整个渲染视图树重建。
这样一来Widget仅是一个轻量级的数据配置存储结构它的重新创建速度非常快所以我们可以放心地重新构建任何需要更新的视图而无需分别修改各个子Widget的特定样式。关于Widget具体的渲染过程细节我会在后续的第9篇文章“Widget构建Flutter界面的基石”中向你详细介绍在这里就不再展开了。
## 总结
今天的这次Flutter项目初体验我们就先进行到这里。接下来我们一起回顾下涉及到的知识点。
首先我们通过Flutter标准模板创建了计数器示例并分析了Flutter的项目结构以及Flutter工程与原生Android、iOS工程的联系知道了Flutter代码是怎么运行在原生系统上的。
然后我带你学习了示例项目代码了解了Flutter应用结构及页面结构并认识了构建Flutter的基础也就是Widget以及状态管理机制知道了Flutter页面是如何构建的StatelessWidget与StatefulWidget的区别以及如何通过State的成员函数setState以数据驱动的方式更新状态从而更新页面。
有原生Android和iOS框架开发经验的同学可能更习惯命令式的UI编程风格手动创建UI组件在需要更改UI时调用其方法修改视觉属性。而Flutter采用声明式UI设计我们只需要描述当前的UI状态即State即可不同UI状态的视觉变更由Flutter在底层完成。
虽然命令式的UI编程风格更直观但声明式UI编程方式的好处是可以让我们把复杂的视图操作细节交给框架去完成这样一来不仅可以提高我们的效率也可以让我们专注于整个应用和页面的结构和功能。
所以在这里我非常希望你能够适应这样的UI编程思维方式的转换。
## 思考题
最后,我给你留下一个思考题吧。
示例项目代码在\_MyHomePageState类中直接在build函数里以内联的方式完成了Scaffold页面元素的构建这样做的好处是什么呢
在实现同样功能的情况下如果将Scaffold页面元素的构建封装成一个新Widget类我们该如何处理
欢迎你在评论区给我留言分享你的观点,我会在下一篇文章中等待你!感谢你的收听,也欢迎你把这篇文章分享给更多的朋友一起阅读。