通过Android Studio创建的Flutter应用模板,了解Flutter项目结构,分析Flutter工程与原生Android和iOS工程有哪些联系,体验一个有着基本功能的Flutter应用是如何运转的,从而加深你对构建Flutter应用的关键概念和技术的理解。
Dart只要理解基本编程概念(比如,类型、变量、函数和面向对象),并具备一定前端基础(了解View是什么、页面基本布局等基础),就可以和我一起完成计数器示例工程分析
Android Studio创建Flutter工程应用flutter_app。Flutter会根据自带的应用模板,自动生成简单计数器示例应用Demo。我们先运行此示例,效果如下:
每点击一次右下角带“+”号的悬浮按钮,就可以看到屏幕中央的数字随之+1。
1 工程结构
了解Flutter工程与原生Android和iOS工程关系及这些关系是如何确保Flutter程序最终运行在Android和iOS。
除Flutter本身的代码、资源、依赖和配置,Flutter工程包含Android和iOS工程目录。因为Flutter虽然是跨平台开发方案,但却需要一个容器最终运行到Android和iOS平台,所以 Flutter工程实际是同时内嵌Android和iOS原生子工程的父工程:在lib目录进行Flutter代码开发,某些特殊场景原生功能,则在对应Android和iOS工程提供相应代码实现,供对应Flutter代码引用。
Flutter会将相关依赖和构建产物注入这两个子工程,集成到各自项目。而我们开发Flutter代码,最终以原生工程形式运行。
2 工程代码
Flutter自带的应用模板,即计数器示例,对初学者是极好入门范例。从基础的组件、布局到手势的监听,再到状态的改变,Flutter最核心思想在这60余行代码。
为便学习理解,删掉核心流程无关组件配置代码及布局逻辑,不影响示例功能的情况下对代码进行改写,并分两部分:
- 应用入口、应用结构以及页面结构,理解构建Flutter程序的基本结构和套路
- 页面布局、交互逻辑及状态管理,理解Flutter页面是如何构建、如何响应交互,以及如何更新
3 第一部分代码,应用的整体结构
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。
3.1 Widget
组件视觉效果的封装,UI界面的载体,因此还要为它提供一个方法,告诉Flutter框架如何构建UI界面,即build。
3.1.1 build方法
通过对基础Widget进行相应UI配置或组合各类基础Widget进行UI定制化。如MyApp通过MaterialApp这个Flutter App框架设置应用首页,即MyHomePage。
3.2 MaterialApp类
是对构建material设计风格应用的组件封装框架,有很多可配置属性,如应用主题、应用名称、语言标识符、组件路由等,可参考Flutter官方的 API文档,了解MaterialApp框架的其他配置能力。
3.3 MyHomePage
应用首页,继承自StatefulWidget类,有状态Widget(Stateful Widget), _MyHomePageState就是其状态。
虽然MyHomePage类也是Widget,但与MyApp类不同,它没有build方法返回Widget,而是多个createState方法返回_MyHomePageState对象,而build方法包含在这 _MyHomePageState 类中。
3.4 StatefulWidget V.S StatelessWidget的接口设计
因为Widget要依据数据才能完成构建,对StatefulWidget来说,其依赖的数据在Widget生命周期中可能频繁变化。由State创建Widget,以数据驱动视图更新,而非直接操作UI更新视觉属性,代码表达更精炼,逻辑更清晰。
4 第二部分-页面布局及交互逻辑
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,页面导航栏,直接将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,若不调用setState,Flutter框架也不会感知到状态变化,因此界面也不会有任何改变。
图计数器示例的代码流程示意图,把这实例的整个代码流程串起:
MyApp为Flutter应用的运行实例,在main函数中调用runApp函数实现程序的入口。
应用首页则为MyHomePage,一个拥有_MyHomePageState状态的StatefulWidget。_MyHomePageState通过调用build方法以相应数据配置完成包括导航栏、文本及按钮的页面视图的创建。
而当按钮被点击之后,其关联的控件函数_incrementCounter会触发调用。在这个函数中,通过调用setState更新_counter属性同时,也通知Flutter框架其状态发生变化。随后,Flutter重新调用build方法以新数据配置重建_MyHomePageState的UI,最终完成页面重新渲染。
Widget只是视图的“配置信息”,是数据的映射,“只读”。对StatefulWidget,当数据改变时,需重建Widget去更新界面,即Widget创建销毁会很频繁。
为此,Flutter对此机制
5 优化
其框架9内部会通过一个中间层收敛上层UI配置对底层真实渲染的改动,从而最大程度降低对真实渲染视图的修改,提高渲染效率,而不是上层UI配置变就要销毁整个渲染视图树重建。
这样Widget仅是一个轻量级的数据配置存储结构,它的重新创建速度非常快,所以我们可放心重建任何需更新的视图,无需分别修改各子Widget特定样式。
6 总结
先通过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编程方式好处是,可以让我们把复杂的视图操作细节交给框架去完成,这样一来不仅可以提高我们的效率,也可专注整个应用和页面的结构和功能。
7 FAQ
示例项目代码在_MyHomePageState类中,直接在build函数里以内联的方式完成了Scaffold页面元素的构建,这样做的好处是什么呢?在实现同样功能的情况下,如果将Scaffold页面元素的构建封装成一个新Widget类,我们该如何处理?
将Scaffold页面元素的构建封装成一个新Widget类的好处是可以提高代码的可复用性和可维护性。这样可以将Scaffold的构建逻辑封装到一个独立的组件中,方便在其他地方重复使用,也方便后续进行修改和维护。
如果要将Scaffold页面元素的构建封装成一个新Widget类,可以创建一个新的StatelessWidget或StatefulWidget类,然后在该类的build方法中返回Scaffold组件的代码。这样就可以在其他地方使用该组件来构建Scaffold页面元素。例如:
class MyScaffold extends StatelessWidget { final Widget body; const MyScaffold({Key? key, required this.body}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('My App'), ), body: body, ); } }
然后在其他地方可以通过MyScaffold
来构建页面元素,例如:
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MyScaffold( body: Center( child: Text('Hello, World!'), ), ); } }