共计 8565 个字符,预计需要花费 22 分钟才能阅读完成。
前言一:接下来一段时间我会陆续更新一些列 Flutter 文字教程
更新进度: 每周至少两篇;
更新地点: 首发于公众号,第二天更新于掘金、思否、开发者头条等地方;
更多交流: 可以添加我的微信 372623326,关注我的微博:coderwhy
希望大家可以 帮忙转发,点击在看,给我更多的创作动力。
一. StatefulWidget
在开发中,某些 Widget 情况下我们展示的数据并不是一层不变的:
比如 Flutter 默认程序中的计数器案例,点击了 + 号按钮后,显示的数字需要 +1;
比如在开发中,我们会进行下拉刷新、上拉加载更多,这时数据也会发生变化;
而 StatelessWidget 通常用来展示哪些数据固定不变的,如果数据会发生改变,我们使用 StatefulWidget;
1.1. 认识 StatefulWidget
1.1.1. StatefulWidget 介绍
如果你有阅读过默认我们创建 Flutter 的示例程序,那么你会发现它创建的是一个 StatefulWidget。
为什么选择 StatefulWidget 呢?
- 因为在示例代码中,当我们点击按钮时,界面上显示的数据会发生改变;
- 这时,我们需要一个
变量
来记录当前的状态,再把这个变量显示到某个 Text Widget 上; - 并且每次
变量
发生改变时,我们对应的 Text 上显示的内容也要发生改变;
但是有一个问题,我之前说过定义到 Widget 中的数据都是不可变的,必须定义为 final,为什么呢?
- 这次因为 Flutter 在设计的时候就决定了一旦 Widget 中展示的数据发生变化,就重新构建整个 Widget;
- 下一个章节我会讲解 Flutter 的渲染原理,Flutter 通过一些机制来限定定义到 Widget 中的
成员变量
必须是final
的;
Flutter 如何做到我们在开发中定义到 Widget 中的数据一定是 final 的呢?
我们来看一下 Widget 的源码:
@immutable
abstract class Widget extends DiagnosticableTree {// ... 省略代码}
这里有一个很关键的东西 @immutable
- 我们似乎在 Dart 中没有见过这种语法,这实际上是一个
注解
,这设计到 Dart 的元编程,我们这里不展开讲; - 这里我就说明一下这个 @immutable 是干什么的;
实际上官方有对 @immutable 进行说明:
- 来源: https://api.flutter.dev/flutt…
- 说明: 被 @immutable 注解标明的类或者子类都必须是不可变的
结论: 定义到 Widget 中的数据一定是不可变的,需要使用 final 来修饰
1.1.2. 如何存储 Widget 状态?
既然 Widget 是不可变,那么 StatefulWidget 如何来存储可变的状态呢?
- StatelessWidget 无所谓,因为它里面的数据通常是直接定义玩后就不修改的。
- 但 StatefulWidget 需要有状态(可以理解成变量)的改变,这如何做到呢?
Flutter 将 StatefulWidget 设计成了两个类:
- 也就是你创建 StatefulWidget 时必须创建两个类:
- 一个类继承自 StatefulWidget,作为 Widget 树的一部分;
- 一个类继承自 State,用于记录 StatefulWidget 会变化的状态,并且根据状态的变化,构建出新的 Widget;
创建一个 StatefulWidget,我们通常会按照如下格式来做:
- 当 Flutter 在构建 Widget Tree 时,会获取
State 的实例
,并且它调用 build 方法去获取 StatefulWidget 希望构建的 Widget; - 那么,我们就可以将需要保存的状态保存在 MyState 中,因为它是可变的;
class MyStatefulWidget extends StatefulWidget {
@override
State<StatefulWidget> createState() {
// 将创建的 State 返回
return MyState();}
}
class MyState extends State<MyStatefulWidget> {
@override
Widget build(BuildContext context) {return < 构建自己的 Widget>;}
}
思考:为什么 Flutter 要这样设计呢?
这是因为在 Flutter 中,只要数据改变了 Widget 就需要重新构建(rebuild)
1.2. StatefulWidget 案例
1.2.1. 案例效果和分析
我们通过一个案例来练习一下 StatefulWidget,还是之前的计数器案例,但是我们按照自己的方式进行一些改进。
案例效果以及布局如下:
- 在这个案例中,有很多布局对于我们来说有些复杂,我们后面会详细学习,建议大家根据我的代码一步步写出来来熟悉 Flutter 开发模式;
- Column 小部件:之前我们已经用过,当有垂直方向布局时,我们就使用它;
- Row 小部件:之前也用过,当时水平方向布局时,我们就使用它;
- RaiseButton 小部件:可以创建一个按钮,并且其中有一个
onPress 属性
是传入一个回调函数
,当按钮点击时被回调;
1.2.2. 创建 StatefulWidget
下面我们来看看代码实现:
- 因为当点击按钮时,数字会发生变化,所以我们需要使用一个 StatefulWidget,所以我们需要创建两个类;
- MyCounterWidget 继承自 StatefulWidget,里面需要实现 createState 方法;
- MyCounterState 继承自 State,里面实现 build 方法,并且可以定义一些成员变量;
class MyCounterWidget extends StatefulWidget {
@override
State<StatefulWidget> createState() {
// 将创建的 State 返回
return MyCounterState();}
}
class MyCounterState extends State<MyCounterWidget> {
int counter = 0;
@override
Widget build(BuildContext context) {
return Center(child: Text("当前计数:$counter", style: TextStyle(fontSize: 30),),
);
}
}
1.2.3. 实现按钮的布局
class MyCounterState extends State<MyCounterWidget> {
int counter = 0;
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
color: Colors.redAccent,
child: Text("+1", style: TextStyle(fontSize: 18, color: Colors.white),),
onPressed: () {},
),
RaisedButton(
color: Colors.orangeAccent,
child: Text("-1", style: TextStyle(fontSize: 18, color: Colors.white),),
onPressed: () {},
)
],
),
Text("当前计数:$counter", style: TextStyle(fontSize: 30),)
],
),
);
}
}
1.2.4. 按钮点击状态改变
我们现在要监听状态的改变,当状态改变时要修改counter 变量
:
- 但是,直接修改变量可以改变界面吗?不可以。
- 这是因为 Flutter 并不知道我们的数据发生了改变,需要来重新构建我们界面中的 Widget;
如何可以让 Flutter 知道我们的状态发生改变了,重新构建我们的 Widget 呢?
- 我们需要调用一个 State 中默认给我们提供的 setState 方法;
- 可以在其中的回调函数中修改我们的变量;
onPressed: () {setState(() {counter++;});
},
这样就可以实现想要的效果了:
1.3. StatefulWidget 生命周期
1.3.1. 生命周期的理解
什么是生命周期呢?
- 客户端开发:iOS 开发中我们需要知道 UIViewController 从创建到销毁的整个过程,Android 开发中我们需要知道 Activity 从创建到销毁的整个过程。以便在不同的生命周期方法中完成不同的操作;
- 前端开发中:Vue、React 开发中组件也都有自己的生命周期,在不同的生命周期中我们可以做不同的操作;
Flutter 小部件的生命周期:
- StatelessWidget 可以由父 Widget 直接传入值,调用 build 方法来构建,整个过程非常简单;
- 而 StatefulWidget 需要通过 State 来管理其数据,并且还要监控状态的改变决定是否重新 build 整个 Widget;
- 所以,我们主要讨论 StatefulWidget 的生命周期,也就是它从创建到销毁的整个过程;
1.3.2. 生命周期的简单版
在这个版本中,我讲解那些常用的方法和回调,下一个版本中我解释一些比较复杂的方法和回调
那么 StatefulWidget 有哪些生命周期的回调呢?它们分别在什么情况下执行呢?
- 在下图中,灰色部分的内容是 Flutter 内部操作的,我们并不需要手动去设置它们;
- 白色部分表示我们可以去监听到或者可以手动调用的方法;
我们知道 StatefulWidget 本身由两个类组成的:StatefulWidget
和State
,我们分开进行分析
首先,执行 StatefulWidget 中相关的方法:
- 1、执行 StatefulWidget 的构造函数(Constructor)来创建出 StatefulWidget;
- 2、执行 StatefulWidget 的 createState 方法,来创建一个维护 StatefulWidget 的 State 对象;
其次,调用 createState 创建 State 对象时,执行 State 类的相关方法:
- 1、执行 State 类的构造方法(Constructor)来创建 State 对象;
-
2、执行 initState,我们通常会在这个方法中执行一些数据初始化的操作,或者也可能会发送网络请求;
- 注意:这个方法是重写父类的方法,必须调用 super,因为父类中会进行一些其他操作;
- 并且如果你阅读源码,你会发现这里有一个注解(annotation):@mustCallSuper
- 3、执行 didChangeDependencies 方法,这个方法在两种情况下会调用
-
- 情况一:调用 initState 会调用;
- 情况二:从其他对象中依赖一些数据发生改变时,比如前面我们提到的 InheritedWidget(这个后面会讲到);
- 4、Flutter 执行 build 方法,来看一下我们当前的 Widget 需要渲染哪些 Widget;
- 5、当前的 Widget 不再使用时,会调用 dispose 进行销毁;
- 6、手动调用 setState 方法,会根据最新的状态(数据)来重新调用 build 方法,构建对应的 Widgets;
- 7、执行 didUpdateWidget 方法是在当父 Widget 触发重建(rebuild)时,系统会调用 didUpdateWidget 方法;
我们来通过代码进行演示:
import 'package:flutter/material.dart';
main(List<String> args) {runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text("HelloWorld"),
),
body: HomeBody(),),
);
}
}
class HomeBody extends StatelessWidget {
@override
Widget build(BuildContext context) {print("HomeBody build");
return MyCounterWidget();}
}
class MyCounterWidget extends StatefulWidget {MyCounterWidget() {print("执行了 MyCounterWidget 的构造方法");
}
@override
State<StatefulWidget> createState() {print("执行了 MyCounterWidget 的 createState 方法");
// 将创建的 State 返回
return MyCounterState();}
}
class MyCounterState extends State<MyCounterWidget> {
int counter = 0;
MyCounterState() {print("执行 MyCounterState 的构造方法");
}
@override
void initState() {super.initState();
print("执行 MyCounterState 的 init 方法");
}
@override
void didChangeDependencies() {
// TODO: implement didChangeDependencies
super.didChangeDependencies();
print("执行 MyCounterState 的 didChangeDependencies 方法");
}
@override
Widget build(BuildContext context) {print("执行执行 MyCounterState 的 build 方法");
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
color: Colors.redAccent,
child: Text("+1", style: TextStyle(fontSize: 18, color: Colors.white),),
onPressed: () {setState(() {counter++;});
},
),
RaisedButton(
color: Colors.orangeAccent,
child: Text("-1", style: TextStyle(fontSize: 18, color: Colors.white),),
onPressed: () {setState(() {counter--;});
},
)
],
),
Text("当前计数:$counter", style: TextStyle(fontSize: 30),)
],
),
);
}
@override
void didUpdateWidget(MyCounterWidget oldWidget) {super.didUpdateWidget(oldWidget);
print("执行 MyCounterState 的 didUpdateWidget 方法");
}
@override
void dispose() {super.dispose();
print("执行 MyCounterState 的 dispose 方法");
}
}
打印结果如下:
flutter: HomeBody build
flutter: 执行了 MyCounterWidget 的构造方法
flutter: 执行了 MyCounterWidget 的 createState 方法
flutter: 执行 MyCounterState 的构造方法
flutter: 执行 MyCounterState 的 init 方法
flutter: 执行 MyCounterState 的 didChangeDependencies 方法
flutter: 执行执行 MyCounterState 的 build 方法
// 注意:Flutter 会 build 所有的组件两次(查了 GitHub、Stack Overflow,目前没查到原因)flutter: HomeBody build
flutter: 执行了 MyCounterWidget 的构造方法
flutter: 执行 MyCounterState 的 didUpdateWidget 方法
flutter: 执行执行 MyCounterState 的 build 方法
当我们改变状态,手动执行 setState 方法后会打印如下结果:
flutter: 执行执行 MyCounterState 的 build 方法
1.3.3. 生命周期的复杂版(选读)
我们来学习几个前面生命周期图中提到的属性,但是没有详细讲解的
1、mounted 是 State 内部设置的一个属性,事实上我们不了解它也可以,但是如果你想深入了解它,会对 State 的机制理解更加清晰;
- 很多资料没有提到这个属性,但是我这里把它列出来,是内部设置的,不需要我们手动进行修改;
2、dirty state 的含义是脏的 State
- 它实际是通过一个 Element 的东西(我们还没有讲到 Flutter 绘制原理)的属性来标记的;
- 将它标记为 dirty 会等待下一次的重绘检查,强制调用 build 方法来构建我们的 Widget;
- (有机会我专门写一篇关于 StatelessWidget 和 StatefulWidget 的区别,讲解一些它们开发中的选择问题);
3、clean state 的含义是干净的 State
- 它表示当前 build 出来的 Widget,下一次重绘检查时不需要重新 build;
二. Flutter 的编程范式
这个章节又讲解一些理论的东西,可能并不会直接讲授 Flutter 的知识,但是会对你以后写任何的代码,都具备一些简单的知道思想;
2.1. 编程范式的理解
编程范式 对于初学编程的人来说是一个虚无缥缈的东西,但是却是我们日常开发中都在默认遵循的 一些模式和方法;
比如我们最为熟悉的 面向对象编程
就是一种编程范式,与之对应或者结合开发的包括:面向过程编程、函数式编程、面向协议编程;
另外还有两个对应的编程范式:命令式编程
和 声明式编程
- 命令式编程: 命令式编程非常好理解,就是一步步给计算机命令,告诉它我们想做什么事情;
- 声明式编程: 声明式编程通常是描述目标的性质,你应该是什么样的,依赖哪些状态,并且当依赖的状态发生改变时,我们通过某些方式通知目标作出相应;
上面的描述还是太笼统了,我们来看一些具体点的例子;
2.2. 前端的编程范式
下面的代码没有写过前端的可以简单看一下
下面的代码是在前端开发中我写的两个 demo,作用都是点击按钮后修改 h2 标签的内容:
- 左边代码: 命令式编程,一步步告诉浏览器我要做什么事情;
- 右边代码: 声明式编程,我只是告诉 h2 标签中我需要显示 title,当 title 发生改变的时候,通过一些机制自动来更新状态;
2.3. Flutter 的编程范式
从 2009 年开始(数据来自维基百科),声明式编程就开始流行起来,并且目前在 Vue、React、包括 iOS 中的 SwiftUI 中以及 Flutter 目前都采用了声明式编程。
现在我们来开发一个需求:显示一个 Hello World,之后又修改成了 Hello Flutter
如果是传统的命令式编程,我们开发 Flutter 的模式很可能是这样的:(注意是想象中的伪代码)
- 整个过程,我们需要一步步告诉 Flutter 它需要做什么;
final text = new Text();
var title = "Hello World";
text.setContent(title);
// 修改数据
title = "Hello Flutter";
text.setContent(title);
如果是声明式编程,我们通常会维护一套数据集:
- 这个数据集可能来自己父类、来自自身 State 管理、来自 InheritedWidget、来自统一的状态管理的地方;
- 总之,我们知道有这么一个数据集,并且告诉 Flutter 这些数据集在哪里使用;
var title = "Hello World";
Text(title); // 告诉 Text 内部显示的是 title
// 数据改变
title = "Hello Flutter";
setState(() => null); // 通知重新 build Widget 即可
上面的代码过于简单,可能不能体现出 Flutter 声明式编程的优势所在,但是在以后的开发中,我们都是按照这种模式在进行开始,我们一起来慢慢体会;
备注:所有内容首发于公众号,之后除了 Flutter 也会更新其他技术文章,TypeScript、React、Node、uniapp、mpvue、数据结构与算法等等,也会更新一些自己的学习心得等,欢迎大家关注