👨🏻‍💻's 博客

慢品人间烟火色,闲观万事岁月长

0%

Flutter - 概览

Hello world

⌘ + shift + p

2025-05-01 20.05.16.png

选择 Empty Application 模板

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
// 导入Material风格的组件包
// 位置在flutter安装目录/packages/flutter/lib/material.dart
import 'package:flutter/material.dart';

void main() {
// runApp函数接收MainApp组件并将这个Widget作为根节点
runApp(const MainApp());
}

class MainApp extends StatelessWidget {
const MainApp({super.key});

@override
// Describes the part of the user interface represented by this widget.

// 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
// (e.g., an [InheritedWidget] referenced by this widget changes).
// This method can potentially be called in every frame and should not have any
// side effects beyond building a widget.
Widget build(BuildContext context) {
/// An application that uses Material Design.
/// 使用Material设计的组件,home代表默认页
return const MaterialApp(
/// The Scaffold is designed to be a top level container for
/// a [MaterialApp]. This means that adding a Scaffold
/// to each route on a Material app will provide the app with
/// Material's basic visual layout structure.
/// Scaffold,MateriaApp组件的顶层容器,规范样式之类的
home: Scaffold(
body: Center( /// 局中显示Hello World
child: Text('Hello World'),
),
),
);
}
}

build方法用于描述Widget的展示效果,当被添加到上下文的树Widget发生变化时会触发这个方法。因为这个方法是高频操作所以不应该有副作用。

热重载(Hot reload)

Flutter支持热重载,无需重启启动应用的情况下去重新刷新页面。通过将更新代码注入到运行的Dart虚拟机来实现热重载。在虚拟机使用新的字段和函数更新类后,Flutter框架自动重新构建widget。

修改后直接保存/点击调试那里的闪电图标能直接刷新

optimized.gif

有状态(StatefulWidget) + 无状态(StatelessWidget)

Flutter中的一切都是Widget,Widget分为有状态和无状态两种,在 Flutter 中每个页面都是一帧,无状态就是保持在那一帧,而有状态的 Widget 当数据更新时,其实是创建了新的 Widget,只是 State 实现了跨帧的数据同步保存。

比如上面的MainApp是无状态的Widget,而Scaffold是有状态的Widget

1
2
3
4
5
6
7
class MainApp extends StatelessWidget {
...
}

class Scaffold extends StatefulWidget {
...
}

创建新组件时继承有状态还是无状态的Widget取决于是否要管理状态

基础组件

Text

Text 是现实单一样式的文本字符串组件。字符串可能跨多行中断,也可能全部显示在同一行上,取决于布局约束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Widget build(BuildContext context) {
return MaterialApp(
home:Scaffold(
body:Center(
// 设置宽度限制为100点
child:Container(
width: 100,
height:30,
// 边框
decoration: BoxDecoration(border: Border.all()),
// TextOverflow.ellipsis 超过部分用...
// TextOverflow.clip -- Clip the overflowing text to fix its container. 超出部分换下一行,外部容器会被遮挡
// TextOverflow.visible -- Render overflowing text outside of its container. 超出容器部分能渲染
child: Text(overflow:TextOverflow.ellipsis, 'Hello world, how are you?'))
))
);
}

TextOverflow.ellipsis的效果

2025-05-02 14.56.13.png

TextOverflow.clip 的效果

2025-05-02 14.55.32.png

TextOverflow.visible 的效果

2025-05-02 15.06.07.png

maxLines 控制最大行数
softWrap 控制是否换行

overflowTextOverflow.visible

softWrap: false

2025-05-02 15.04.59.png

softWrap: true

2025-05-02 15.07.44.png

Text.rich

使用Text.rich构造器,Text组件可以在一个段落中展示不同的样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: const Text.rich(
TextSpan(
text: 'Hello', // default text style
children: <TextSpan>[
TextSpan(
text: ' beautiful ',
style: TextStyle(fontStyle: FontStyle.italic),
),
TextSpan(
text: 'world',
style: TextStyle(fontWeight: FontWeight.bold),
),
],
),
),
),
),
);
}

2025-05-02 15.16.39.png

关于Text的交互

GestureDetector widget包装Text,然后在GestureDetector.onTap中处理点击事件。或者使用TextButton来代替

Row,Column,Stack,Container

  • Container: 只有一个子 Widget。默认充满,包含了padding、margin、color、宽高、decoration 等配置
  • Row: 可以有多个子 Widget。水平布局。
  • Column: 可以有多个子 Widget。垂直布局。
  • Stack: 可以有多个子 Widget。 子Widget堆叠在一起。
  • Center: 只有一个子 Widget。只用于居中显示,常用于嵌套child,给child设置居中。
  • Padding: 只有一个子 Widget。只用于设置Padding,常用于嵌套child,给child设置padding。
  • Expanded: 只有一个子 Widget。在 Column 和 Row 中充满。
  • ListView: 可以有多个子Widget,列表布局
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 水平布局
Row(
children: [
// 图标
const IconButton(
icon: Icon(Icons.menu),
tooltip: 'Navigation menu',
onPressed: null, // null disables the button
),
// Expanded expands its child
// to fill the available space.
// 填充满2个图标之间的空间
Expanded(child: title),
// 查询图标
const IconButton(
icon: Icon(Icons.search),
tooltip: 'Search',
onPressed: null,
),
],
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 垂直布局
Column(
children: [
MyAppBar(
title: Text(
'示例标题',
style:
Theme.of(context) //
.primaryTextTheme.titleLarge,
),
),
const Expanded(child: Center(child: Text('容器'))),
],
)

2025-05-02 16.10.27.png

要使用material中这些预定义图标,需要将工程中的pubspec.yaml文件里的uses-material-design字段设置为true

使用Material组件

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
import 'package:flutter/material.dart';

void main() {
runApp(const MaterialApp(title: 'Flutter Tutorial', home: TutorialHome()));
}

class TutorialHome extends StatelessWidget {
const TutorialHome({super.key});

@override
Widget build(BuildContext context) {
// Scaffold is a layout for
// the major Material Components.
return Scaffold(
appBar: AppBar(
leading: const IconButton(
icon: Icon(Icons.menu),
tooltip: 'Navigation menu',
onPressed: null,
),
title: const Text('Material Components'),
actions: const [
IconButton(
icon: Icon(Icons.search),
tooltip: 'Search',
onPressed: null,
),
],
),
// body is the majority of the screen.
body: const Center(child: Text('Material!')),
floatingActionButton: const FloatingActionButton(
tooltip: 'Add', // used by assistive technologies
onPressed: null,
child: Icon(Icons.add),
),
);
}
}

2025-05-02 16.31.29.png

使用ScaffoldAppBar替换原来自定义的MyScaffoldMyAppBar

手势处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: GestureDetector(
child: Text('Hello world',overflow: TextOverflow.ellipsis,),
onTap: ()=> {
// 生产环境不要用print
print("123")
},)
),
),
);
}

2025-05-02 15.38.53.png

更改小组件以响应输入

UI通常需要对用户的输入进行响应,比如点外卖时根据用户选择菜品计算最后的价格, Flutter中使用StatefulWidgets来处理这种场景。

继承StatefulWidget,重写createState方法

1
2
3
4
5
6
7
8
class Counter extends StatefulWidget {
const Counter({super.key});

// 继承StatefulWidget的类要重写createState()方法,内容返回是_CounterState对象
// ``=>`` (胖箭头)简写语法用于仅包含一条语句的函数。该语法在将匿名函数作为参数传递时非常有用
@override
State<Counter> createState() => _CounterState();
}

所有的类都隐式定义成了一个接口。因此,任意类都可以作为接口被实现,定义一个继承State并实现Counter类的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/// [State] objects are created by the framework by calling the
/// [StatefulWidget.createState] method when inflating a [StatefulWidget] to
/// insert it into the tree.
/// 在这里当Counter组件被添加到渲染树时,因为也实现了Counter类,所以会调用对应的createState方法。
class _CounterState extends State<Counter> {
int _counter = 0;

// _ 代表私有方法
void _increment() {
// 调用setState()通知Flutter状态变化了,然后重新执行build方法实现实时刷新的效果
setState(() {
_counter++;
});
}

2025-05-02 17.45.53.png

合并的示例

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
113
114
115
116
117
import 'package:flutter/material.dart';

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;

Color _getColor(BuildContext context) {
return inCart //
? Colors.black54
: Theme.of(context).primaryColor;
}

TextStyle? _getTextStyle(BuildContext context) {
if (!inCart) return null;

return const TextStyle(
color: Colors.black54,
decoration: TextDecoration.lineThrough,
);
}

@override
Widget build(BuildContext context) {
return ListTile(
// 5. 点击Item时调用传入 onCartChanged 回调,并传入一开始接收的出参数
// 比如一开始在订单内inCart传true
onTap: () {
onCartChanged(product, inCart);
},
leading: CircleAvatar(
backgroundColor: _getColor(context),
child: Text(product.name[0]),
),
// 4.显示产品订单的样式
// 10.根据新参数重新显示样式
title: Text(product.name, style: _getTextStyle(context)),
);
}
}

class ShoppingList extends StatefulWidget {
// 要求传入 products属性
const ShoppingList({required this.products, super.key});

final List<Product> products;

// 2. 调用_ShoppingListState创建状态对象
@override
State<ShoppingList> createState() => _ShoppingListState();
}

class _ShoppingListState extends State<ShoppingList> {
final _shoppingCart = <Product>{};
// 6. 点击触发回调
void _handleCartChanged(Product product, bool inCart) {
setState(() {
// 7. 根据入参进行判断,如果一开始是true,则移除,否则添加,即取反操作
if (!inCart) {
_shoppingCart.add(product);
} else {
_shoppingCart.remove(product);
}
// 8. 通知Flutter 重新执行_ShoppingListState对象的build方法
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Shopping List')),
body: ListView(
padding: const EdgeInsets.symmetric(vertical: 8),
children:
// 3. 根据products属性创建ShoppingListItem,并传入产品信息,回调
// 9. 再次调用ShoppingListItem并传入新参数
widget.products.map((product) {
return ShoppingListItem(
product: product,
inCart: _shoppingCart.contains(product),
onCartChanged: _handleCartChanged,
);
}).toList(),
),
);
}
}

void main() {
runApp(
const MaterialApp(
title: 'Shopping App',
// 1. 创建ShoppingList对象,并传入Product参数
home: ShoppingList(
products: [
Product(name: 'Eggs'),
Product(name: 'Flour'),
Product(name: 'Chocolate chips'),
],
),
),
);
}

optimized.gif

响应组件的生命周期相关事件

Flutter调用createState方法后,会将state对象添加到渲染树并且调用state对象的initState(),可以重写这个方法中配置动画或准备订阅平台相关的服务,重写方法开始要先调用super.initState。当state对象不再需要时,Flutter会调用对象的dispose方法来执行清理操作,比如取消定时器,取消订阅,同样在重写方法中也要先调用super.dispose

其它

包缓存地址

1
2
$ flutter pub get 
# 命令下载的包在~/.pub-cache/hosted

参考

  1. Flutter
  2. Flutter-UI
  3. Flutter - 我给官方提PR,解决run命令卡住问题 😃
  4. Day16 - Flutter - 屏幕适配