Flutter - 初识 Flutter

初步了解 Flutter 应用可以通过分析 Android Studio 创建的 Flutter 应用模板来了解。
打开 Android Studio,创建一个 Flutter 工程应用 flutter_app。Flutter 会根据自带的应用模板,自动生成一个简单的计数器示例应用 Demo。
该工程的代码如下:

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}

class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);

// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.

// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".

final String title;

@override
_MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;

void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
}

@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Invoke "debug painting" (press "p" in the console, choose the
// "Toggle Debug Paint" action from the Flutter Inspector in Android
// Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
// to see the wireframe for each widget.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}

标准模版分析

应用入口、应用结构以及页面结构

第一部分是应用入口、应用结构以及页面结构,可以帮助你理解构建 Flutter 程序的基本结构和套路;第二部分则是页面布局、交互逻辑及状态管理,能够帮你理解 Flutter 页面是如何构建、如何响应交互,以及如何更新的。

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 设计风格应用的组件封装框架,里面还有很多可配置的属性,比如应用主题、应用名称、语言标识符、组件路由等。

MyHomePage 是应用的首页,继承自 StatefulWidget 类。这代表着它是一个有状态的 Widget(Stateful Widget),而 _MyHomePageState 就是它的状态。

虽然 MyHomePage 类也是 Widget,但与 MyApp 类不同的是,它并没有一个 build 方法去返回 Widget,而是多了一个 createState 方法返回 _MyHomePageState 对象,而 build 方法则包含在这个 _MyHomePageState 类当中。

Widget 需要依据数据才能完成构建,而对于 StatefulWidget 来说,其依赖的数据在 Widget 生命周期中可能会频繁地发生变化。由 State 创建 Widget,以数据驱动视图更新,而不是直接操作 UI 更新视觉属性,代码表达可以更精炼,逻辑也可以更清晰。

页面布局、交互逻辑及状态管理,Flutter 页面是如何构建、如何响应交互,以及如何更新的。

_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 方法,根据新的状态重新构建界面。

Widget 只是视图的“配置信息”,是数据的映射,是“只读”的。对于 StatefulWidget 而言,当数据改变的时候,需要重新创建 Widget 去更新界面,这也就意味着 Widget 的创建销毁会非常频繁。
为此,Flutter 对这个机制做了优化,其框架内部会通过一个中间层去收敛上层 UI 配置对底层真实渲染的改动,从而最大程度降低对真实渲染视图的修改,提高渲染效率,而不是上层 UI 配置变了就需要销毁整个渲染视图树重建。

生命周期

  • StatefulWidget 生命周期
    • created:
      已创建[State]对象。 此时调用[State.initState]
    • initialized:
      已调用[State.initState]方法,但[State]对象尚未准备好构建。 此时调用[State.didChangeDependencies]。
    • ready:
      [State]对象已准备好构建
    • defunct:
      已调用[State.dispose]方法,并且[State]对象不再能够构建。
  • State 生命周期
    • initState:
      当插入渲染树的时候调用,这个函数在生命周期中只调用一次
    • didChangeDependencies:
      当 StatefulWidget 第一次创建的时候,didChangeDependencies 方法会在 initState 方法之后立即调用,之后当 StatefulWidget 刷新的时候,就不会调用了,除非你的 StatefulWidget 依赖的 InheritedWidget 发生变化之后,didChangeDependencies 才会调用,所以 didChangeDependencies 有可能会被调用多次。
    • build:
      初始化之后开始绘制界面,当setState触发的时候会再次被调用
    • didUpdateWidget:
    • deactivate:
      当 State 被暂时从视图树中移除时,会调用这个函数。
      页面切换时,也会调用它,因为此时 State 在视图树中的位置发生了变化,需要先暂时移除后添加。
    • dispose:
      当 State 被永久的从视图树中移除,Framework 会调用该函数。
      在销毁前触发,我们可以在这里进行最终的资源释放。
      在调用这个函数之前,总会先调用deactivate。