最近打算自己做一个电商 App 调研之后选择技术栈为 Flutter 因此打算梳理一下 Flutter 的所有知识点,做个预热。
- flutter 中的动画 flutter 中的动画可以分成隐式和显式两种,又可以分成用户自定义的和组件库内置的两种。
确保项目中已经安装了如下依赖
- cupertino_icons@^1.0.2
- google_fonts@^4.0.3
- transparent_image@^2.0.1
- flutter_riverpod@^2.3.2
The following adds the Cupertino Icons font to your application. Use with the CupertinoIcons class for i0s style icons.
以下是关于显式(Explicit)与隐式(Implicit)动画的整理信息:
显式动画(Explicit Animations)
- 控制权:你完全控制动画的整个过程。
- 复杂性:由于控制权在你,因此动画的实现会更加复杂。
- 避免使用:可以通过使用预构建的 Widgets 来避免显式动画的复杂性。
隐式动画(Implicit Animations)
- 控制权:Flutter 框架控制动画的播放。
- 复杂性:由于 Flutter 框架接管了动画控制,因此动画的实现会相对简单,复杂性较低。
- 使用建议:尽可能多地使用预构建的动画 Widgets 来实现隐式动画。
使用动画,我们一般采用的是可变组件
- 使用动画的一般步骤
class CategoriesScreenstate extends State<CategoriesScreen> with SingleTickerProviderStateMixin {
// 声明一个AnimationController类型的变量,用于控制动画
late AnimationController animationController;
// 使用override关键字来覆盖基类的initState方法
@override
void initState() {
// 首先调用基类的initState方法,以确保基类的初始化逻辑被执行
super.initState();
// 创建一个AnimationController实例,它需要一个vsync参数来确保动画的流畅性
animationController = AnimationController(
// vsync参数用于提供动画的同步信号,确保动画的流畅性
vsync: this, // 提供当前State对象作为vsync参数
duration: const Duration(milliseconds: 300), // 设置动画的持续时间为300毫秒
lowerBound: 0, // 设置动画的最小值,通常用于控制动画的起始位置
upperBound: 1, // 设置动画的最大值,通常用于控制动画的结束位置
);
}
}
class CategoriesScreenstate extends State<CategoriesScreen> with SingleTickerProviderStateMixin {:定义一个名为CategoriesScreenstate的类,它继承自State<CategoriesScreen>并混入SingleTickerProviderStateMixin,用于管理 Flutter 中的动画。late AnimationController animationController;:声明一个AnimationController类型的变量animationController,使用late关键字表示这个变量稍后会被初始化。@override:这是一个注解,表示接下来的成员函数将覆盖基类中的同名函数。void initState() {:定义initState方法,它是State类的一个生命周期方法,用于在组件初始化时执行一些操作。super.initState();:调用基类的initState方法,以确保基类的初始化逻辑被执行。animationController = AnimationController(:创建一个AnimationController实例并赋值给animationController变量。vsync: this,:为AnimationController提供vsync参数,这个参数用于确保动画的同步和流畅性,this表示当前的State对象。
如果我们的组件只具有单一的动画,那么我们 with SingleTickerProviderStateMixin 就好了,但是如果组件中具有复杂动画,那么我们需要 with 的就是 TickerProviderstateMixin 了。
- 关于 with 有如下信息
在 Dart 语言中,with 关键字用于实现 mixin(混入)功能。Mixin 是一种代码复用技术,允许将多个类的功能组合到一个类中,而不需要使用继承。这使得代码更加模块化,并且可以减少继承层次结构的复杂性。
以下是 with 关键字的一些关键点:
-
- 代码复用:
with允许一个类继承多个类的非私有成员(包括方法、属性和构造函数),而不需要成为它们的子类。
- 代码复用:
-
- 不涉及继承:与继承不同,
with关键字创建的是一个新的子类,这个子类拥有所有混入类(mixin)的成员,但不拥有它们的继承关系。
- 不涉及继承:与继承不同,
-
- 非侵入性:使用
with的类不需要修改其代码来适应混入的类,这使得混入非常灵活。
- 非侵入性:使用
-
- 接口和实现的分离:混入通常用于提供接口(方法签名),而实现则由混入的类提供。
-
- 限制:混入的类不能有构造函数,因为它们没有自己的实例。它们只能添加方法和属性,不能添加字段(字段必须在最终的类中声明)。
-
- 冲突解决:如果混入的类和最终的类有同名成员,那么最终的类必须显式地解决这些冲突,通常是通过覆盖(override)或者使用
noSuchMethod机制。
- 冲突解决:如果混入的类和最终的类有同名成员,那么最终的类必须显式地解决这些冲突,通常是通过覆盖(override)或者使用
-
- 与接口(interfaces)和抽象类(abstract classes)的比较:混入更像是接口和抽象类的混合体,它们可以包含方法的实现,而不仅仅是签名。
-
- 使用场景:混入通常用于添加一些通用的功能,比如生命周期管理、事件处理等,这些功能可以在多个不同的类中使用。
下面是一个简单的使用 with 的例子:
class MusicPlayer {
void play() {
print("Playing music");
}
}
mixin Portable {
void carry() {
print("Carrying the device");
}
}
class MobileMusicPlayer with Portable {
void usePlayer() {
play(); // From MusicPlayer
carry(); // From Portable mixin
}
}
void main() {
MobileMusicPlayer player = MobileMusicPlayer();
player.usePlayer();
}
在这个例子中,MobileMusicPlayer 类通过 with Portable 混入了 Portable 类的功能,使得 MobileMusicPlayer 可以调用 carry 方法,而这个方法实际上是定义在 Portable 混入中的。这样,MobileMusicPlayer 就拥有了播放音乐和携带设备的能力。
简单来说,with 就相当于是 class 级别的 hook.
- 使用动画控制器,并在组件销毁之前销毁控制器
是不是组件中所有的控制器都需要在组件销毁之前同步销毁掉?
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
lowerBound: 0,
upperBound: 1,
)
@overide
void dispose() {
_animationController.dispose();
super.dispose();
}
}
- 在组件的 build 函数中使用动画控制器
和之前所有的组件都一样,我们对目标组件使用动画,其从过程来看就是使用特定的组件来包裹目标组件,如下面的代码所示:
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationController,
builder: () => TargetWidget,
)
}
目标组件被写在 builder 函数的返回值中了。
- 同时接受命名参数和位置参数的自定义组件
如下例子所示:
import 'package:flutter/material.dart';
// 自定义按钮组件
class CustomButton extends StatelessWidget {
// 位置参数:按钮的颜色
final Color color;
// 命名参数:按钮的文本
final String? text;
// 构造函数,接收位置参数和命名参数
CustomButton(this.color, {this.text});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
print('Button pressed!');
},
style: ElevatedButton.styleFrom(
primary: color, // 使用传入的颜色参数
),
child: Text(text ?? 'Default Text'), // 使用传入的文本参数,如果没有提供则使用默认文本
);
}
}
void main() {
runApp(MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Custom Button Example')),
body: Center(
child: CustomButton(
Colors.blue, // 位置参数:按钮颜色
text: 'Click Me', // 命名参数:按钮文本
),
),
),
));
}
简单来说,位置参数在前,命名参数在最后并统一放在一个花括号中。
- 一个比较完整的使用动画的例子
class CategoriesScreen extends StatefulWidget {
@override
_CategoriesScreenState createState() => _CategoriesScreenState();
}
class _CategoriesScreenState extends State<CategoriesScreen> with SingleTickerProviderStateMixin {
AnimationController animationController;
@override
void initState() {
super.initState();
animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
animationController.forward(); // 开始动画
}
@override
void dispose() {
animationController.dispose(); // 释放资源
super.dispose();
}
@override
Widget build(BuildContext context) {
final availableCategories = [...]; // 假设这是一个类别列表
return AnimatedBuilder(
animation: animationController, // 关联动画控制器
builder: (context, child) {
return Padding(
padding: EdgeInsets.only(
top: animationController.value * 100, // 随动画值变化的顶部内边距
),
child: child,
);
},
child: GridView(
padding: const EdgeInsets.all(24),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 3 / 2,
crossAxisSpacing: 20,
mainAxisSpacing: 20,
),
children: List.generate(
availableCategories.length,
(index) => CategoryGridItem(
category: availableCategories[index],
onSelectCategory: () {
selectCategory(context, availableCategories[index]);
},
),
).toList(),
),
);
}
}
class CategoryGridItem extends StatelessWidget {
final Category category;
final VoidCallback onSelectCategory;
CategoryGridItem({Key? key, required this.category, required this.onSelectCategory}) : super(key: key);
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onSelectCategory,
child: Card(
child: Center(
child: Text(category.name), // 假设Category有一个name属性
),
),
);
}
}
void selectCategory(BuildContext context, Category category) {
// 处理选择类别的逻辑
}
class Category {
final String name;
Category(this.name);
}
注意!builder 的第二个形参指的是同级配置项 child 指向的组件,表示动画的不可变部分。
接下来,详细解释AnimatedBuilder中的各个设置项的作用:
-
animation:这是AnimatedBuilder的必需参数,它接受一个Animation对象,通常是AnimationController。这个动画对象控制着builder函数何时被调用。 -
builder:这是一个函数,它接收两个参数:context和child。context是当前的BuildContext,child是AnimatedBuilder的子组件。builder函数根据动画的当前状态构建 UI,并且每当动画值变化时都会被调用。context:用于访问当前的构建上下文。child:AnimatedBuilder的子组件,它是一个不变的组件,不随动画变化。
-
child:这是AnimatedBuilder的子组件,它是一个不变的组件,不随动画变化。在这个例子中,child是一个GridView,它显示了一个类别列表。
SliverGridDelegateWithFixedCrossAxisCount的相关设置项:
-
crossAxisCount:设置在交叉轴(这里是水平轴)上的子组件数量。 -
childAspectRatio:设置子组件的宽高比。 -
crossAxisSpacing:设置子组件在交叉轴方向上的间距。 -
mainAxisSpacing:设置子组件在主轴(这里是垂直轴)方向上的间距。
在builder函数中,我们根据animationController的值动态地改变Padding的top值,从而实现一个随动画变化的顶部内边距效果。这样,GridView会随着动画的进行上下移动。
- 动画控制器实例上的方法
在 Flutter 中,AnimationController是一个特殊的Animation对象,它可以用来控制动画的播放。AnimationController通常与TickerProvider(如SingleTickerProviderStateMixin或AutomaticKeepAliveClientMixin)一起使用,以确保动画的生命周期得到妥善管理。以下是AnimationController的一些关键方法:
-
forward():
- 开始动画,从动画的开始位置向结束位置播放。
-
reverse():
- 反向播放动画,从动画的结束位置向开始位置播放。
-
animateTo(double value):
- 将动画平滑地过渡到指定的值。
-
stop():
- 停止动画,但不改变动画的当前值。
-
stop(canceled: bool):
- 停止动画,并可选地取消动画,如果
canceled为true,则动画的当前值会被设置为AnimationController的lowerBound或upperBound,取决于动画是向前还是向后播放。
- 停止动画,并可选地取消动画,如果
-
reset():
- 重置动画到开始位置。
-
dispose():
- 释放与
AnimationController相关的资源,通常在State对象被销毁时调用。
- 释放与
-
addListener(void listener(AnimationController animation)):
- 添加一个监听器,当动画的值发生变化时,会调用这个监听器。
-
removeListener(void listener(AnimationController animation)):
- 移除之前添加的监听器。
-
addStatusListener(AnimationStatusListener listener):
- 添加一个状态监听器,当动画开始、结束、停止或被取消时,会调用这个监听器。
-
removeStatusListener(AnimationStatusListener listener):
- 移除之前添加的状态监听器。
-
getValues():
- 返回一个包含当前动画值和其他相关值的对象,这些值可以用于构建动画。
AnimationController还有一些重要的属性,如value,它返回动画的当前值,以及duration,它定义了动画的总持续时间。通过这些方法和属性,你可以精确地控制动画的播放、暂停、反向播放和结束等行为。
- 动画的触发时机
动画通常在新页面入栈时触发,而页面更新或移除其上面覆盖页面时不会自动触发动画。
-
-
页面入栈(Push):
- 当你使用
Navigator.push将一个新页面推送到路由栈时,可以指定一个PageRouteBuilder来创建一个带有动画的页面过渡效果。
-
-
-
页面出栈(Pop):
- 当你从路由栈中移除页面时(例如,用户按下返回键或调用
Navigator.pop),可以为被移除的页面指定一个退出动画。
-
-
-
页面替换(Replace):
- 如果你使用
Navigator.pushReplacement替换当前页面,通常不会有动画,因为新页面立即覆盖了旧页面。
-
-
-
页面遮盖(Overlay):
- 如果你使用
Overlay或类似的机制在页面上添加覆盖层,这些层的添加和移除通常不会触发页面路由级别的动画。
-
-
- 页面更新(Update):
- 如果页面本身的内容发生变化,但页面没有被推送或弹出,那么页面上的动画需要手动触发,例如,通过改变
AnimationController的值。
- 一个页面出现时从下滑动到应该位置的动画效果
builder: (context, child) => Padding(
padding: EdgeInsets.only(
top: 100 - _animationController.value * 100,
),
child: child,
)
_animationController是一个AnimationController实例,它的value属性表示动画的当前状态,其值从 0.0 变化到 1.0,表示动画从开始到结束的完整周期。
builder方法中的Padding widget 使用了_animationController.value来动态计算top属性的值。这里的计算方式是100 - _animationController.value * 100,意味着:
- 当
_animationController.value为 0.0 时(动画开始),top的值为 100,表示childwidget 将距离顶部 100 像素。 - 随着
_animationController.value从 0.0 增加到 1.0(动画进行),top的值会从 100 减少到 0,使得childwidget 向下移动,直到紧贴顶部。 - 当
_animationController.value达到 1.0 时(动画结束),top的值为 0,表示childwidget 已经移动到了顶部边缘。
因此,_animationController.value控制了child widget 在垂直方向上的动态位置,实现了一个从下向上进入屏幕的动画效果。
也就是说 _animationController.value 表示动画进行的进度的百分比。
- 使用内置动画
以 SlideTransition 为例:
builder: (context, child) => SlideTransition(
position: animationController.drive(
Tween<Offset>(
begin: const Offset(0, 0.3),
end: const Offset(0, 0),
),
),
child: child,
)
下面是对代码逐行的详细解释:
-
builder: (context, child) =>
这一行定义了AnimatedBuilder的builder方法,它是一个函数,接受两个参数:context和child。context是当前的构建上下文,child是AnimatedBuilder的子组件。这个函数返回一个新的 Widget,这里是SlideTransition。 -
SlideTransition(
这是SlideTransitionWidget 的构造函数调用,它是一个用于创建滑动动画的 Flutter 组件。 -
position: animationController.drive(
position属性设置了SlideTransition的滑动位置,它接收一个Animation<Offset>对象。animationController.drive方法用于创建这个Animation<Offset>对象,它将根据AnimationController的值来驱动动画。 -
Tween<Offset>(
Tween是一个用于计算两个值之间插值的工具。在这里,它用于计算两个Offset值之间的插值,Offset是一个包含dx和dy的 Flutter 数据类型,用于表示二维空间中的点或向量。 -
begin: const Offset(0, 0.3),
begin属性指定了Tween动画的起始位置。const Offset(0, 0.3)表示动画开始时,SlideTransition的垂直位置偏移是其原始高度的 30%(因为dy值是 0.3,而dx值是 0,表示没有水平偏移)。 -
end: const Offset(0, 0),
end属性指定了Tween动画的结束位置。const Offset(0, 0)表示动画结束时,SlideTransition的垂直位置偏移为 0,即完全进入视图。 -
child: child,
child属性设置了SlideTransition内部显示的 Widget,这里使用的是AnimatedBuilder传递进来的child参数。
这段代码创建了一个随animationController驱动的滑动动画,动画从垂直方向偏移 30%的高度开始,滑动到没有偏移的位置,同时child Widget 会在这个过程中跟随滑动。
其中 4 5 6 比较难以理解,特别是 Tween 对 Offset 类型进行插值计算。
- 对比两种动画的写法 见此链接
对比两种写法,说说它们之间的不同:
builder: (context, child) => SlideTransition(
position: animationController.drive(
Tween<Offset>(
begin: const Offset(0, 0.3),
end: const Offset(0, 0),
),
),
child: child,
)
和
builder: (context, child) => SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 0.3),
end: const Offset(0, 0),
).animate(CurvedAnimation(
parent: animationController,
curve: Curves.easeInOut,
)),
child: child,
)
后面一种的写法提供了对动画更加复杂的控制,所以怎么写需要根据复杂程度。
两种写法的主要区别在于如何将Tween与AnimationController结合来创建动画:
-
-
使用
animationController.drive方法:
- 第一种写法中,
animationController.drive直接作用于Tween<Offset>。 drive方法创建了一个Animation<Offset>,它将Tween的开始和结束值与AnimationController关联起来。- 这种方法通常用于不需要额外曲线(curve)或者父母动画(parent animation)的场景,它将
Tween的值直接与AnimationController的值关联。
-
-
- 使用
Tween.animate方法:
- 第二种写法中,
Tween<Offset>与CurvedAnimation结合使用。 animate方法创建了一个Animation<Offset>,它将Tween与一个CurvedAnimation包装器关联起来,这个包装器又与AnimationController关联。- 这种方法允许你指定一个曲线(curve),例如
Curves.easeInOut,来控制动画的加速和减速,使得动画更加平滑。
- 使用
第一种写法更简单直接,适用于线性动画,而第二种写法提供了更多的控制,允许你为动画添加非线性的曲线效果。在实际应用中,选择哪种写法取决于你是否需要对动画的速度曲线进行更细致的控制。
- 切换按钮时候的动画
我们以点击收藏按钮时候设置动画为例。
icon: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (child, animation) {
return RotationTransition(
turns: animation,
child: child,
);
},
child: Icon(isFavorite ? Icons.star : Icons.star_border),
)
上面的动画实际上并不会生效,原因很简单,那就是 flutter 对于变化前后还是同一个组件类型的情况(如上面代码中 child 属性的值),在进行 diff 的时候并不会触发动画。
如果我们想要触发动画,那么就必须特别的告诉 flutter,通常我们给其增加一个 key 就可以了,通常使用的是 ValueKey。
icon: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (child, animation) {
return RotationTransition(
turns: animation,
child: child,
);
},
child: Icon(
isFavorite ? Icons.star : Icons.star_border,
key: ValueKey(isFavorite),
),
)
- 配置 RotationTransition
既然叫 RotationTransition 那么我们当然可以通过配置设置其旋转角度和速度,如下所示:
transitionBuilder: (child, animation) {
return RotationTransition(
turns: Tween<double>(begin: 0.5, end: 1).animate(animation),
child: child,
);
},
child: Icon(
isFavorite ? Icons.star: Icons.star_border,
key: ValueKey(isFavorite),
)
- 使用 Hero 组件完成多动画
基本原则就是使用 Hero 组件将目标组件包裹起来,然后再为其设置一个 tag 属性,多个组件都使用 Hero 包裹并设置相同的 tag 这样就可以完成多动画了。
Hero(
tag: meal.id,
child: FadeInImage(
placeholder: MemoryImage(kTransparentImage),
image: NetworkImage(meal.imageUrl),
fit: BoxFit.cover,
height: 200,
width: double.infinity,
),
)
Hero 组件通过相同的 tag 值实现多个动画之间的共享效果。
下面是一个简单的例子,展示如何使用相同的tag值在多个Hero组件之间实现动画共享效果:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: FirstPage(),
);
}
}
class FirstPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Hero Animation Example')),
body: Center(
child: GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondPage()),
);
},
child: Hero(
tag: 'hero-tag', // 相同的tag值
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
),
),
),
);
}
}
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Second Page')),
body: Center(
child: Hero(
tag: 'hero-tag', // 相同的tag值
child: Container(
width: 200,
height: 200,
color: Colors.blue,
),
),
),
);
}
}
说明
-
第一个页面(FirstPage):
- 包含一个
Hero组件,tag值为'hero-tag',显示一个蓝色的方块。 - 点击方块时,导航到第二个页面。
- 包含一个
-
第二个页面(SecondPage):
- 也包含一个
Hero组件,tag值同样为'hero-tag',显示一个更大的蓝色方块。
- 也包含一个
动画效果
当用户点击第一个页面的方块时,方块会平滑地从第一个页面过渡到第二个页面,形成一个动画效果。这是因为两个Hero组件使用了相同的tag值,从而实现了动画的共享。
- ListView 和 ListTile 以及小尾巴 trailing
先看代码:
body: ListView.builder(
itemCount: groceryItems.length,
itemBuilder: (ctx, index) => ListTile(
title: Text(groceryItems[index].name),
leading: Container(
width: 24,
height: 24,
color: groceryItems[index].category.color,
), // Container
trailing: Text(
groceryItems[index].quantity.toString(),
), // Text
), // ListTile
) // ListView.builder
-
ListView在 Flutter 中类似于 HTML 中的<ul>(无序列表),它用于创建一个可以滚动的列表视图,可以垂直或水平展示一系列子组件。 -
ListTile在 Flutter 中类似于 HTML 中的<li>(列表项),它代表列表中的单个条目,通常包含一些文本、图标或其他小部件,并且可以响应用户的点击事件。 -
trailing可以方便的在每一个列表元素的后面设置一个 addon.
- 回归复习
现在,复习可变组件的创建过程:
import 'package:flutter/material.dart';
class NewItem extends StatefulWidget {
const NewItem({super.key});
@override
State<NewItem> createState() {
return _NewItemState();
}
}
class _NewItemState extends State<NewItem> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Add a new item'),
),
)
}
}
- context 在可变或者不可变组件中的传递
这段代码定义了一个名为addItem的函数,该函数使用Navigator来推送一个新的路由到应用的导航堆栈上。这个路由是由MaterialPageRoute构建的,它将显示NewItem组件。
在可变组件(StatefulWidget)中
在可变组件(StatefulWidget)中,context通常指的是组件的BuildContext,它可以通过this.context获得。在这种情况下,context是与组件的状态和生命周期相关联的。当你在可变组件的方法中调用Navigator.of(context)时,它通常能够正确地找到当前组件的上下文,并用于导航。context在这里指向的是当前组件树中正确的位置,允许Navigator知道从哪里弹出或推送新的路由。
在不可变组件(StatelessWidget)中
在不可变组件(StatelessWidget)中,context通常是指传递给build方法的BuildContext。在这种情况下,context不包含状态信息,因为它是与组件的构建相关联的。如果你在不可变组件的build方法外部直接调用Navigator.of(context),可能会抛出异常,因为context必须是与组件树中的一个具体位置相关联的,而不可变组件不包含这样的状态信息。
传递context的不同
- 可变组件:
context可以直接用来与Navigator交互,因为它包含了组件的状态和位置信息。 - 不可变组件:在
build方法外部使用context可能会导致问题,因为context不包含足够的信息来确定组件在导航树中的位置。如果需要在不可变组件中进行导航,通常需要将context从上层组件传递下来,或者使用其他方式获取正确的context。
解决方案
如果你需要在不可变组件中进行导航,你可以考虑以下解决方案:
- 将
context作为参数传递:让调用addItem的上层组件传递context作为参数。 - 使用
Builder包裹:使用Builderwidget 包裹不可变组件,并在Builder中访问context。 - 回调函数:将
addItem函数作为回调传递给不可变组件,同时传递context。
总之,context的使用取决于组件的类型和它在组件树中的位置。在可变组件中,context通常可以直接使用,而在不可变组件中,则需要特别注意context的来源和使用方式。