dart 语言学习日记(3)
dart 语言学习日记(3)
前言
基于第一篇学习日记,继续学习 dart 语言
其主要思路是通过 flutter 中的一个实例,讲一讲 flutter 中构建一个 app 的主要思路
- ui 构建
- 事件处理
参考资料:
主要内容
代码准备
首先找一个地方放我们的代码, 并初始化一个 flutter 应用,
> cd ~/Study/flutterStudy/
> flutter create flutter_example
> cd flutter_example
> flutter run
根据上述的步骤, 我们创建了一个简单的 flutter 应用, 使用 flutter run 命令即可启动这个简单的应用
接下来, 我们需要在 lib/main.dart 中清空示例代码 我也不知道这样子到底对不对啊
.
├── analysis_options.yaml
├── android
├── build
├── flutter_example.iml
├── ios
├── lib
├── linux
├── macos
├── pubspec.lock
├── pubspec.yaml
├── README.md
├── test
├── web
└── windows
10 directories, 5 files
> tree -L1 ./lib
./lib
└── main.dart # 这个就是我们需要修改的文件
> rm ./lib/main.dart
> touch ./lib/main.dart
接下来, 我们需要在 lib/main.dart 中添加一个简单的代码, 这个代码是一个简单的 flutter 应用
import 'package:flutter/material.dart';
// 定义一个 product 类
class Product {
const Product({required this.name});
final String name;
}
typedef CartChangedCallback = Function(Product product, bool inCart);
class ShoppingListItem extends StatelessWidget {
ShoppingListItem({
required this.product,
required this.inCart,
required this.onCartChanged,
}) : super(key: ObjectKey(product));
final Product product;
final bool inCart;
final CartChangedCallback onCartChanged;
// 根据 inCart 的数值来决定颜色
Color _getColor(BuildContext context) {
// The theme depends on the BuildContext because different
// parts of the tree can have different themes.
// The BuildContext indicates where the build is
// taking place and therefore which theme to use.
return inCart //
? Colors.black54
: Theme.of(context).primaryColor;
}
// 根据 inCart 的数值来决定文字样式
TextStyle? _getTextStyle(BuildContext context) {
if (!inCart) return null;
return const TextStyle(
color: Colors.black54,
decoration: TextDecoration.lineThrough,
);
}
@override
Widget build(BuildContext context) {
return ListTile(
// 当用户点击 ListTile 时候,会调用 onCartChanged 回调函数
onTap: () {
// onCartChanged 传递给 ShoppingListItem 的回调函数
onCartChanged(product, inCart);
},
leading: CircleAvatar(
backgroundColor: _getColor(context),
child: Text(product.name[0]),
),
title: Text(product.name, style: _getTextStyle(context)),
);
}
}
// 继承 StatefulWidget 并抽象出一个 State 对象
class ShoppingList extends StatefulWidget {
const ShoppingList({required this.products, super.key});
final List<Product> products;
// The framework calls createState the first time
// a widget appears at a given location in the tree.
// If the parent rebuilds and uses the same type of
// widget (with the same key), the framework re-uses
// the State object instead of creating a new State object.
// 创建一个 State 对象,用于管理 ShoppingList 的状态
@override
State<ShoppingList> createState() => _ShoppingListState();
}
// 继承 State 并实现其抽象方法
class _ShoppingListState extends State<ShoppingList> {
final _shoppingCart = <Product>{};
void _handleCartChanged(Product product, bool inCart) {
setState(() {
// When a user changes what's in the cart, you need
// to change _shoppingCart inside a setState call to
// trigger a rebuild.
// The framework then calls build, below,
// which updates the visual appearance of the app.
if (!inCart) {
_shoppingCart.add(product);
} else {
_shoppingCart.remove(product);
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Shopping List')),
body: ListView(
padding: const EdgeInsets.symmetric(vertical: 8),
children:
widget.products.map((product) {
return ShoppingListItem(
product: product,
inCart: _shoppingCart.contains(product),
onCartChanged: _handleCartChanged,
);
}).toList(),
),
);
}
}
void main() {
runApp(
const MaterialApp(
title: 'Shopping App',
// home 默认路由的入口,存一个 widget
home: ShoppingList(
products: [
Product(name: 'Eggs'),
Product(name: 'Flour'),
Product(name: 'Chocolate chips'),
],
),
),
);
}
接下来, 尝试跑一下上面的应用
> flutter run
然后我们就可以看到一个简单的应用, 如下图所示

该应用实现了如下图功能

总结:
该章节准备了一个简单的 flutter 应用,并且顺利的运行了起来
代码结构
接下来,我们分析一下代码的结构,在上文的示例中,我们从 ui 以及事件处理两个方面来分析这个应用
ui
我们先看一看这个应用的 ui 由哪些部分组成

如上图所示,该 app 由两个部分组成,1部分 是 app 的标题,2部分 是 app 的主体内容
我们先聊一聊这两部分的 ui 该如何构建
关于第一部分,我们可以用以下代码实现
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(title: 'Shopping List', home: ShoppingList()));
}
class ShoppingList extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Shopping List')),
body: const Center(child: Text('Hello World')),
);
}
}
使用 flutter run 命令即可得到如下 app

上文代码由一个 main 函数,一个 ShoppingList 类组成, 我你自己看
flutter 的核心概念为使用组件构建自己的 ui , 组件通常会定义其外观以及事件
在上述代码的 main 函数, 将MaterialApp (一个组件), 传入 runApp 方法中,从而构建了一个 app
runApp 提供了 flutter 组件树的根组件,同时传入 MaterialApp 作为根组件下的子组件
Calling [runApp] again will detach the previous root widget from the view and
attach the given widget in its place.
runApp 的函数原型为 Type: void Function(Widget)
MaterialApp 则创建了一个 WidgetApp 的实例,runApp 接受了该实例,从而返回一个组件树,如下图所示
而对于 MaterialApp 而言,也有其必须传入的参数
At least one of [home], [routes], [onGenerateRoute], or [builder] must be non-null.
在本章节代码中,我们传入了 String? title 以及 Widget? home 两个参数
title 参数如字面意思理解即可,而 home 参数则要求我们传入一个 Widget 实例
所以我们创建了一个继承 StatelessWidget 的 ShoppingList 类
StatelessWidget 类在 flutter 中通常用于构建用户 UI
我们首先查看要实现相关的类需要哪些东西
- Minimize the number of nodes transitively created by the build method and
any widgets it creates.- Use
constwidgets where possible, and provide aconstconstructor for
the widget so that users of the widget can also do so.
我们需要一个 build 方法来构建一个 Widget ,同时我们也需要一个 const 方法将这个Widget 实例化
build 方法的原型为 Widget build(BuildContext context), 其描述如下
Describes the part of the user interface represented by this widget.
也就是说,该方法用于定义 Widget 的 UI, 该方法返回一个 Widget 类型,并需要传入一个 BuildContext 类型参数
在 UI 方面,我们构建了一个 Scaffold 作为 Widget 的脚手架
在这个脚手架中我们定义了 appBar 以及 body 两个成员
appBar 详情可参考appBar Class
其中的 UI 定义如下图所示

在上述示例代码中,我们定义了 title 为 Shopping List, 也就是上文所提到的 1部分
body 详情可参考body in Scaffold
body 的默认位置在 appBar 下方的左上角,但是我们额外定义了 const Center(child: Text('Hello World'))
所以如 2部分 所示,Hello World 字样在 app 中间
同时我们看一看 build 方法所传入的参数 BuildContext context
The framework calls this method when this widget is inserted into the tree
in a given [BuildContext] and when the dependencies of this widget change
...
The given [BuildContext] contains information about the location in the
tree at which this widget is being built.
在上述文档中,我们可以看到,当 flutter 需要用到 widget 的时候,会自动插入 BuildContext
而 BuildContext 通常用于传入在父节点中定义的 WidgetTree 位置
通过上述代码,我们解释了在 flutter 中 UI 是如何构建的,接下来我们打开 flutter devtool 验证我们的解释是否合理
在 nvim 普通模式中输入 :FlutterDevTools 并打开弹出的 URL, 如下图所示

我们可以看到其中的 widgettree 构成,进入 Select Widget Mode, 点击我们的应用,我们可以看到组件的组织方式


至此,针对上述代码,我们做了比较详尽的解释,而关于 ShoppingListItem 的 UI 构建,参考上文以及文档即可
另外一个重要的概念,就是 widgettree, 我觉得我讲的不够好,可以参考以下文章
Understanding the Flutter Widget Tree: A Comprehensive Guide
Flutter’s Widget Tree: A Deep Dive into the World of Flutter UI
Widget Class
把握几个核心的点即可
-
flutter 的 UI 以 Widget 的形式构建,包括
ColumnRowTextStack- 组件都是不可变的状态,如果需要根据用户行为改变组件,则需要进行事件的传递,然后由 flutter 进行重新渲染
-
Widget 之间通过树关系进行组织
-
通过改变 Widget 里的位置 尺寸等关系来构建 UI 界面
事件结构
参考资料:
在我们的代码中,除了 UI 的构建,还涉及到 UI 随着用户操作而改变.
所以深入了解 flutter 事件是有必要的
整体的逻辑为,用户操作 -> 改变组件状态 -> flutter 重新渲染 UI
其中 state 的生命周期为
- flutter 使用 StatefulWidget.createState 创建一个 State 对象
- State 与 BuildContext 进行关联,这个关联是永久性的
- 在 flutter 启动的时候会对 State 进行初始化
- State 会通过 setState 方法重新构建组件树,并调用
didUpdateWidget方法,从而重新构建整个 app
上述代码是个简单的 app ,并不涉及到 state 在不同组件间的流转,所以只涉及到局部 state 的构建
我们首先定义一个能够通过成员属性定义自身 layout 的 widget
class ShoppingListItem extends StatelessWidget {
ShoppingListItem({
required this.product,
required this.inCart,
required this.onCartChanged,
}) : super(key: ObjectKey(product));
final Product product;
final bool inCart;
final CartChangedCallback onCartChanged;
// 根据 inCart 的数值来决定颜色
Color _getColor(BuildContext context) {
// The theme depends on the BuildContext because different
// parts of the tree can have different themes.
// The BuildContext indicates where the build is
// taking place and therefore which theme to use.
return inCart //
? Colors.black54
: Theme.of(context).primaryColor;
}
// 根据 inCart 的数值来决定文字样式
TextStyle? _getTextStyle(BuildContext context) {
if (!inCart) return null;
return const TextStyle(
color: Colors.black54,
decoration: TextDecoration.lineThrough,
);
}
@override
Widget build(BuildContext context) {
return ListTile(
// 当用户点击 ListTile 时候,会调用 onCartChanged 回调函数
onTap: () {
// onCartChanged 传递给 ShoppingListItem 的回调函数
onCartChanged(product, inCart);
},
leading: CircleAvatar(
backgroundColor: _getColor(context),
child: Text(product.name[0]),
),
title: Text(product.name, style: _getTextStyle(context)),
);
}
}
首先这是一个 StatelessWidget 组件,这个组件是静态的,是不会动态改变的
同时,该组件有两种形态,随着 inCart 的值不同,该组件会呈现不同的 UI 状态
也就是说,随着该组件渲染时所传入的 inCart 参数不同,该组件会呈现不同的 UI 状态
观察我们的 App ,我们不只需要一个 ShoppingListItem ,我们还需要一个 list 来存储多个 item 对象
// 继承 StatefulWidget 并抽象出一个 State 对象
class ShoppingList extends StatefulWidget {
const ShoppingList({required this.products, super.key});
final List<Product> products;
// The framework calls createState the first time
// a widget appears at a given location in the tree.
// If the parent rebuilds and uses the same type of
// widget (with the same key), the framework re-uses
// the State object instead of creating a new State object.
// 创建一个 State 对象,用于管理 ShoppingList 的状态
@override
State<ShoppingList> createState() => _ShoppingListState();
}
// 继承 State 并实现其抽象方法
class _ShoppingListState extends State<ShoppingList> {
final _shoppingCart = <Product>{};
void _handleCartChanged(Product product, bool inCart) {
setState(() {
// When a user changes what's in the cart, you need
// to change _shoppingCart inside a setState call to
// trigger a rebuild.
// The framework then calls build, below,
// which updates the visual appearance of the app.
if (!inCart) {
_shoppingCart.add(product);
} else {
_shoppingCart.remove(product);
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Shopping List')),
body: ListView(
padding: const EdgeInsets.symmetric(vertical: 8),
children:
widget.products.map((product) {
return ShoppingListItem(
product: product,
inCart: _shoppingCart.contains(product),
onCartChanged: _handleCartChanged,
);
}).toList(),
),
);
}
}
上述代码一共涉及三个类,类与类之间的关系如下如所示
可以看到,在 ShoppingList 中,我们定义了一个 products 的成员变量, 该变量存储了多个 Product 对象
同时我们还定义了一个 createState 方法, 该方法返回一个 State<ShoppingList> 对象
通过定义该方法, flutter 会自动创建一个 _ShoppingListState 对象
_ShoppingListState 继承了 State<ShoppingList> 类
该类是一个抽象类, 该类的作用是管理 ShoppingList 的状态
并且_ShoppingListState同时管理 ShoppingListItem 的状态,
通过调用 _handleCartChanged 方法, 我们可以改变 ShoppingListItem 的状态
下面我们描述一下在整个事件中发生了什么事情
-
用户点击
ShoppingListItem组件:\ -
用户点击了
ShoppingListItem,触发了其内部的onTap方法。 -
调用
onCartChanged回调:\ -
在
ShoppingListItem的实现中,onTap方法调用了onCartChanged回调函数,并将当前的Product对象作为参数传递。 -
事件传递到
_ShoppingListState的_handleCartChanged方法:\ -
onCartChanged回调实际上是_ShoppingListState中的_handleCartChanged方法 \
这个方法接收了Product和inCart参数,用于处理购物车状态的变化。 -
更新
_shoppingCart集合:\ -
_handleCartChanged方法根据inCart的值决定是将Product添加到_shoppingCart集合中,还是从中移除。 -
调用
setState更新 UI:\ -
_handleCartChanged方法在修改_shoppingCart后调用了setState。\
setState通知 Flutter 框架需要重新构建 UI。 -
重新构建 UI:
Flutter 框架调用_ShoppingListState的build方法,重新生成整个ShoppingList的 UI。\
此时,ShoppingListItem的inCart属性会根据_shoppingCart的状态更新,从而反映最新的购物车状态。
其流程图为
所以在本文中,状态处理组件的核心职责为
-
接收回调函数:
父组件(或子组件)通过回调函数将用户交互事件传递给_ShoppingListState。 -
定义状态处理方法:
_ShoppingListState定义了一个方法(如_handleCartChanged),用于处理状态的变化。 -
调用
setState更新状态:
在状态处理方法中,通过调用setState来更新组件的状态。 -
重新构建 UI:
调用setState后,Flutter 框架会重新调用build方法,生成新的 UI 树,从而反映最新的状态。
这实际也是 flutter 组件该做的事情,这种设计模式通常被称为 lifting state up,
其核心内容在于
- 避免使用子组件控制 state 相反的,将 state 的管理放于父组件中
- 父组件通过使用回调函数控制子组件的状态
同时可以查看以下资料
Lifting State Up & Callbacks in Flutter/
这篇博客比较详细的描述了 状态提升 的定义
Lifting State Up in Flutter: A Smarter Approach to Navigation Management
这篇博客举了一个比较简单例子,也可以进行进一步的参考
关于状态管理的官方文档
总结
本文在提供了一个简单的代码示例的同时,对以下两个方面进行了讲解
- 如何构建基础的 flutter UI
- 如何在简单的应用中对 flutter state 进行管理

浙公网安备 33010602011771号