概念
UI界面设计合理的动画,可以让用户觉得更加流畅、直观,可以极大提高和改善用户体验
从直观来说动画是:
1. 动画就是动起来的画面
2. 视觉暂留:画面经视神经传入大脑后,不会立即消失,会存留一段时间
3. 帧(Frame):单个的画面,在学术上叫帧
4. 每秒中暂时的帧数简称 fps(Frame per Second)
针对动画的性质,分为两大类:
- 补间(Tween)动画
- 在补间动画中我们定义开始点和结束点、时间线以及定义转换时间和速度曲线,
然后由系统计算,从开始点运动到结束点。从而形成效果 - 例如:透明度从 0 到 1,颜色值从 0 到 255
- 拟物动画
- 拟物动画是对真实世界的行为进行建模,使动画效果类似于现实中的物理效果
- 例如:弹簧,阻尼,重力,抛物线等
AnimationController 动画控制器 是一个类,是Animation的重要实现类,
主要是完成控制动画的各种操作,包括动画的启动(forward)、暂停(stop)、回滚(reverse)、以及反复(repeat)
它可设置在指定时间内,将组件属性值由初始值演变到终止值,从而形成动画效果
AnimationController 参数
- duration 动画的执行时间
- reverseDuration 动画反向执行时间
- lowerBound = 0.0 动画最小值
- upperBound = 1.0 动画最大值
- value 动画初始值 默认是 lowerBound
- vsync (TickerProvider 类型 的对象,用来创建 Ticker对象)
当创建一个 AnimationController 时,需要传递一个 vsync 参数
- vaync 的作用是:防止屏幕外动画(动画页面切换到后台时)消耗不必要的资源(管理Ticker更新)
(淡入淡出的过程是一个数值变化的过程,Ticker 在每一步骤上都会通知 UI组件 进行界面更新) - 通过将 singleTickerProviderStateMixin 添加到类定义中,可以将 stateful 对象作为 vsync 的值
AnimationController 具有控制动画的方法
- forward() 可以正向执行动画
- reverse() 可以反向执行动画
- dispose() 用来释放动画资源(在不使用时需要调用该方法,否则会造成资源泄露)
- stop() 用来停止动画
class _AnimateAppState extends State<AnimateApp>
with SingleTickerProviderStateMixin {
AnimationController? controller;
@override
void initState() {
super.initState();
// 创建 AnimationController 对象
controller = AnimationController(
/*
this 指的是当前组件对象
因为使用了混入 SingleTickerProviderStateMixin 类
因此会自动调用 cerateTicker()方法
去创建 Ticker 用于通知组件去一帧一帧创建动画
*/
vsync: this,
// 持续时间 {毫秒:1000}
duration: const Duration(milliseconds: 2000),
);
...
// 执行动画
controller!.forward();
}
}
animationController 动画生成值的默认区间是 0.0 到 1.0,如果希望使用不同的区间,
或者不同的数据类型,需要使用 Tween(补间动画)
实际上它也可以对 AnimationController 默认区值 的补充,可以设置新的阔值
animation = CurvedAnimation(
// 动画控制器来源
parent: controller!,
// 动画曲线类型
curve: Curves.bounceIn,
);
// 通过 Tween 对象 创建 Animation 对象
animation = Tween(begin: 50.0, end: 400.0).animate(controller!)
..addListener(() {
// 注意:这句不能少,否则 widget 不会重绘,也就看不到动画效果
setState(() {});
});
// 执行动画
controller!.forward();
- Tween 的唯一职责就是定义哦那个输入范围到输出范围的映射
- 例如:颜色区间是 0 到 255
Tween 组件中有:
1. IntTween(int 类型的动画
2. ColorTween(颜色渐变动画)
3. ReverseTween(反转动画)
4. SizeTween(size类型的动画)等
>>更多内容
针对不同的 Tween 组件,对应的写入参数也不同,例如:
- Tween(begin:起始值,end:终止值);
- ColorTween(begin:Colors.withe,end:Colors.black);
定义动画曲线的组件为 CurvedAnimation组件,它可控制动画从 开始到结束 的速度曲线
参数介绍:
- parent 动画控制器对象
- curve 正向执行的动画曲线
- reverseCurve 反向执行的动画曲线
>>>更多内容
Animation<double>? animation;
@override
void initState() {
...
super.initState();
// 声明动画曲线
animation = CurvedAnimation(
// 动画控制器来源
parent: controller!,
// 动画曲线类型
curve: Curves.bounceIn,
);
};
当组件切换到后台时,我们理应当停止动画样式的继续渲染
使用 dispose() 用来释放动画资源 (在不使用时应调用该方法,否则会造成资源泄露)
@override
// 当前组件 切换到后台
void dispose() {
// TODO: implement dispose
super.dispose();
// 动画控制器 释放资源
controller!.dispose();
}
Dart 创建动画 一般创建动画需要以下几个步骤
- 创建动画控制器
- controller = AnimationController(duration,vsync);
- 创建动画
- 动画曲线(CurvedAnimation)
- 补间动画(Tween)
- 监听动画
- addListener 监听动画生成值
- addStatusListener 监听动画状态
- 执行动画
- controller.forward() 正向执行
- controller.reverse() 反向执行
下方为完整代码
import 'package:flutter/material.dart';
class Home extends StatelessWidget {
const Home({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('动画'),
centerTitle: true,
elevation: 0.0,
leading: const Icon(Icons.menu),
actions: const [
Icon(Icons.search),
],
backgroundColor: Colors.blue,
),
body: AnimateApp(),
);
}
}
class AnimateApp extends StatefulWidget {
const AnimateApp({Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() {
return _AnimateAppState();
}
}
class _AnimateAppState extends State<AnimateApp>
with SingleTickerProviderStateMixin {
AnimationController? controller;
Animation<double>? animation;
@override
void initState() {
super.initState();
// 创建 AnimationController 对象
controller = AnimationController(
/*
this 指的是当前组件对象
因为使用了混入 SingleTickerProviderStateMixin 类
因此会自动调用 cerateTicker()方法
去创建 Ticker 用于通知组件去一帧一帧创建动画
*/
vsync: this,
// 持续时间 {毫秒:1000}
duration: const Duration(milliseconds: 2000),
);
// 声明动画曲线
animation = CurvedAnimation(
// 动画控制器来源
parent: controller!,
// 动画曲线类型
curve: Curves.bounceIn,
);
// 通过 Tween 对象 创建 Animation 对象
animation = Tween(begin: 50.0, end: 400.0).animate(controller!);
// 监听 动画值变化过程
animation!.addListener(() {
// 注意:这句不能少,否则 widget 不会重绘,也就看不到动画效果
setState(() {});
});
// 执行动画
controller!.forward();
}
@override
Widget build(BuildContext context) {
return SizedBox(
width: double.infinity,
height: double.infinity,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
/*
获取动画的值赋给 widget 的宽高
当值发生变化时 会自动通知 Ticker 重绘界面
*/
width: animation!.value,
height: animation!.value,
decoration: const BoxDecoration(
color: Colors.redAccent,
),
child: Center(
child: Text(
'${animation!.value}',
style: const TextStyle(
fontSize: 50,
fontWeight: FontWeight.w800,
color: Colors.white,
),
)),
),
],
),
);
}
@override
void dispose() {
// 组件 切换到后台
super.dispose();
// 动画 释放资源
controller!.dispose();
}
}
动画循环就是让动画一直播放
如果需要让动画持续播放,可在监听动画结束状态时反向执行动画,并在动画回到开始状态时候正向执行。
// 3. 让动画反复执行
animation!.addStatusListener((status) {
// 如果动画 已经 播放完整
if (status == AnimationStatus.completed) {
print(AnimationStatus.completed);
// 反向执行动画
controller!.reverse();
// 如果动画 状态 为初始状态
} else if (status == AnimationStatus.dismissed) {
// 正向执行动画
controller!.forward();
}
});
交织动画是由多个单一动画叠加而成的复杂动画
例如:组件变化可能涉及高度、宽度、颜色、透明度、位置等
方法:需要给每个动画设置时间间隔 (Interval) ,并依次在规定时间里执行。
* 注意,它们都需要同时绑定在同一个动画控制器中 *
// 2. 创建动画
animation = CurvedAnimation(
// 动画控制器
parent: controller!,
// 播放区间
curve: const Interval(0.0, 0.5),
)..addListener(() {
setState(() {});
});
例如,下方这段代码,在一个动画周期过程中声明了多个 动画参数
// 2. 创建动画
animation = CurvedAnimation(
// 动画控制器
parent: controller!,
// 播放区间
curve: const Interval(0.0, 0.5),
)
// 动画执行期间 通知组件渲染界面
..addListener(() {
// 只是监听 不执行任何操作
setState(() {});
});
// 4.设置其他变化
sizeAnimation = Tween(begin: 0.0, end: 200.0).animate(CurvedAnimation(
// 动画控制器
parent: controller!,
// 播放区间
curve: const Interval(0.0, 0.5),
));
colorAnimation = ColorTween(begin: Colors.yellow, end: Colors.red).animate(
CurvedAnimation(
// 动画控制器
parent: controller!,
// 播放区间
curve: const Interval(0.5, 0.8, curve: Curves.bounceIn),
),
);
rotationAnimation = Tween(begin: 0.0, end: 2 * Math.pi).animate(
CurvedAnimation(
// 动画控制器
parent: controller!,
// 动画控制器
curve: const Interval(0.8, 1.0, curve: Curves.easeIn),
),
);
}
声明完成,再将各个 Animation动画组件 的 value 参数绑定到对应的位置,实现动画效果
// 透明组件
Opacity(
// opacity 为 0 时,使组件透明
opacity: controller!.value,
// 子组件 旋转组件
child: Transform.rotate(
// 旋转角度
angle: rotationAnimation!.value,
// 子组件 容器组件
child: Container(
// 容器 宽度
width: sizeAnimation!.value,
// 容器 高度
height: sizeAnimation!.value,
// 容器 颜色
color: colorAnimation!.value,
),
),
),
完整代码
import 'package:flutter/material.dart';
import 'dart:math' as Math;
class Home extends StatelessWidget {
const Home({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Stagger Animation'),
centerTitle: true,
elevation: 0.0,
leading: const Icon(Icons.menu),
actions: const [
Icon(Icons.search),
],
backgroundColor: Colors.blue,
),
body: const AnimationDemo(),
);
}
}
class AnimationDemo extends StatefulWidget {
const AnimationDemo({super.key});
@override
State<AnimationDemo> createState() => _AnimationDemoState();
}
class _AnimationDemoState extends State<AnimationDemo>
with SingleTickerProviderStateMixin {
// 定义动画 控制器
AnimationController? controller;
// 声明 动画
CurvedAnimation? animation;
Animation? sizeAnimation;
Animation? colorAnimation;
Animation? rotationAnimation;
@override
// 组件初始化 状态
void initState() {
// TODO: implement initState
super.initState();
// 1. 创建 AnimationController
controller = AnimationController(
/*
this 指的是当前组件对象
因为使用了混入 SingleTickerProviderStateMixin 类
因此会自动调用 cerateTicker()方法
去创建 Ticker 用于通知组件去一帧一帧创建动画
*/
vsync: this,
// 动画执行时间 3秒
duration: const Duration(seconds: 3),
);
// 2. 创建动画
animation = CurvedAnimation(
// 动画控制器
parent: controller!,
// 播放区间
curve: const Interval(0.0, 0.5),
)
// 动画执行期间 通知组件渲染界面
..addListener(() {
// 只是监听 不执行任何操作
setState(() {});
});
// 3. 让动画反复执行
animation!.addStatusListener((status) {
// 如果动画 已经 播放完整
if (status == AnimationStatus.completed) {
print(AnimationStatus.completed);
// 反向执行动画
controller!.reverse();
// 如果动画 状态 为初始状态
} else if (status == AnimationStatus.dismissed) {
// 正向执行动画
controller!.forward();
}
});
// 4.设置其他变化
sizeAnimation = Tween(begin: 0.0, end: 200.0).animate(CurvedAnimation(
// 动画控制器
parent: controller!,
// 播放区间
curve: const Interval(0.0, 0.5),
));
colorAnimation = ColorTween(begin: Colors.yellow, end: Colors.red).animate(
CurvedAnimation(
// 动画控制器
parent: controller!,
// 播放区间
curve: const Interval(0.5, 0.8, curve: Curves.bounceIn),
),
);
rotationAnimation = Tween(begin: 0.0, end: 2 * Math.pi).animate(
CurvedAnimation(
// 动画控制器
parent: controller!,
// 动画控制器
curve: const Interval(0.8, 1.0, curve: Curves.easeIn),
),
);
}
@override
Widget build(BuildContext context) {
return Center(
child: Column(children: [
ElevatedButton(
onPressed: () {
// animation!.addStatusListener((status) {
// // 如果动画 已经 播放完整
// if (status == AnimationStatus.completed) {
// // 反向执行动画
// controller!.reverse();
// } else {
// // 正向执行动画
// controller!.forward();
// }
// });
// 正向执行动画
controller!.forward();
},
child: const Text('重复')),
ElevatedButton(
onPressed: () {
// animation!.addStatusListener((status) {
// // 如果动画 已经 播放完整
// if (status == AnimationStatus.completed) {
// // 反向执行动画
// controller!.reverse();
// } else {
// // 正向执行动画
// controller!.forward();
// }
// });
// 正向执行动画
controller!.reverse();
},
child: const Text('返回')),
ElevatedButton(
onPressed: () {
// 停止动画
controller!.stop();
},
child: const Text('停止'),
),
// 透明组件
Opacity(
// opacity 为 0 时,使组件透明
opacity: controller!.value,
// 子组件 旋转组件
child: Transform.rotate(
// 旋转角度
angle: rotationAnimation!.value,
// 子组件 容器组件
child: Container(
// 容器 宽度
width: sizeAnimation!.value,
// 容器 高度
height: sizeAnimation!.value,
// 容器 颜色
color: colorAnimation!.value,
),
),
),
]),
);
}
@override
// 当前组件 切换到后台
void dispose() {
// TODO: implement dispose
super.dispose();
// 动画控制器 释放资源
controller!.dispose();
}
}
Hero动画 用来实现跨页面的动画效果
原理:
由于共享组件在不同页面中的位置、外观等不同,使用hero作为父级,路由切换时,两者之间形成动画效果
在两个组件同时定义 Hero 组件,使用 相同的 tag 的值 ( tag就是Hero控件的一个标签,用来判定唯一性)
实现:
- 在页面 A 中定义 起始 Hero组件 (source hero),声明 tag
- 在页面 B 中定义目标 Hero 组件 (destination hero),绑定相同的 tag
- 页面跳转时,通过 Navigator,传递 tag
hero 参数:
- tag (路由切换时,共享组件的标记)
- child (声明子组件)
代码演示
import 'package:flutter/material.dart';
class Home extends StatelessWidget {
const Home({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Hero'),
leading: const Icon(Icons.menu),
actions: const [
Icon(Icons.settings),
],
elevation: 0.0,
// 居中标题
centerTitle: true,
// AppBar 背景色
backgroundColor: Colors.green,
),
body: HeroAnimation(),
);
}
}
class HeroAnimation extends StatelessWidget {
const HeroAnimation({super.key});
@override
Widget build(BuildContext context) {
return Container(
// 网格布局盒子
child: GridView.extent(
// 必填 副轴的最大长度
maxCrossAxisExtent: 202.0,
// 主轴边距
mainAxisSpacing: 20,
// 子元素 设定 20个成员 依次对每个下标成员布局
children: List.generate(20, (index) {
// 网络图片
String imageUrl = 'https://picsum.photos/id/$index/300/400';
// 返回 交互组件
return GestureDetector(
// 单击事件
onTap: () {
// 匿名路由
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext ctx) {
return ImageDetail(imageUrl);
},
),
);
},
// Hero 动画组件
child: Hero(
tag: imageUrl,
child: Image.network(imageUrl),
),
);
}),
),
);
}
}
class ImageDetail extends StatelessWidget {
final String imageUrl;
ImageDetail(this.imageUrl);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Center(
child: GestureDetector(
onTap: () {
// 返回上一个组件
Navigator.pop(context);
},
// Hero 动画组件
child: Hero(
tag: imageUrl,
child: Image.network(
imageUrl,
width: double.infinity,
fit: BoxFit.cover,
),
),
),
),
);
}
}
在 Flutter组件 初始化到销毁之间,这个过程是组件的生命周期,每个状态都对应下方的钩子函数
钩子函数 | 描述 |
---|---|
initState() | 组件对象插入到元素树中时 |
didChangeDependencies() | 当前状态对象的依赖改变时 |
build() | 组件渲染时 |
setState() | 组件对象的内部状态变更时 |
didUpdateWidget() | 组件配置更新时 |
deactivate() | 组件对象在元素树中 暂时移除时 (切换界面) |
dispose() | 组件对象在元素树中 永远被移除时 (关闭App时) |
statefulWidget(建立有状态组件) → createElement(创建元素对象) → widget.createState(创建状态组件)
→ initState(初始化状态组件) → didChangeDependencies(公共数据依赖发生变更?) → dirty(脏状态[列表]) → build(构建组件列表) → clean(完成构建)
→ deactivate(切换页面) → dispose(关闭App)
[同步] clean(完成构建) → setState(页面发生改变) → dirty(脏状态[列表])
[同步] clean(完成构建) → didUpdateWidget(组件配置更新时) → dirty(脏状态[列表])
下方为完整代码演示
import 'package:flutter/material.dart';
class Home extends StatelessWidget {
const Home({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('life Cycle'),
leading: const Icon(Icons.menu),
actions: const [
Icon(Icons.settings),
],
elevation: 0.0,
centerTitle: true,
),
body: MyState(),
);
}
}
class MyState extends StatefulWidget {
MyState({Key? key}) : super(key: key);
@override
State<MyState> createState() => _MyStateState();
}
class _MyStateState extends State<MyState> {
// 延时初始化
late int _num;
String _str = '';
// 生命周期 初始化状态组件
@override
void initState() {
// ignore: todo
// TODO: implement initState
super.initState();
// ignore: avoid_print
print('initState 初始化状态组件');
_num = 1;
}
// 生命周期 公共数据依赖发生变更?
@override
void didChangeDependencies() {
// ignore: todo
// TODO: implement didChangeDependencies
super.didChangeDependencies();
// ignore: avoid_print
print('didChangeDependencies 公共数据依赖发生变更?');
}
// 生命周期 组件配置更新时
@override
void didUpdateWidget(covariant MyState oldWidget) {
// ignore: todo
// TODO: implement didUpdateWidget
super.didUpdateWidget(oldWidget);
// ignore: avoid_print
print('didUpdateWidget 组件配置更新');
}
@override
void deactivate() {
// ignore: todo
// TODO: implement deactivate
super.deactivate();
// ignore: avoid_print
print('deactivate 切换界面');
}
@override
void dispose() {
// ignore: todo
// TODO: implement dispose
super.dispose();
// ignore: avoid_print
print('dispose APP关闭');
}
void _increment() {
setState(() {
// ignore: avoid_print
print('setState 页面发生改变');
_num++;
_str += 'setState 页面发生改变';
});
}
void _decrement() {
setState(() {
// ignore: avoid_print
print('setState 页面发生改变');
_num--;
_str += 'setState 页面发生改变';
});
}
@override
Widget build(BuildContext context) {
// ignore: avoid_print
print('build 页面重新构建');
return Center(
child: Column(
children: [
ElevatedButton(
onPressed: _decrement,
child: const Text('-'),
),
Padding(
padding: const EdgeInsets.all(20),
child: Text('$_num'),
),
ElevatedButton(
onPressed: _increment,
child: const Icon(Icons.add),
),
Container(
width: double.infinity,
height: 300,
decoration: BoxDecoration(
border: Border.all(color: Colors.red, width: 1),
),
child: SingleChildScrollView(
// ignore: unnecessary_string_interpolations
child: Text('$_str'),
),
),
],
),
);
}
}
inheritedWidget组件 提供了沿树向下,共享数据的功能。
解决了 组件传参 过程中多余的步骤,从沿树而上 的方式到直接 一对一绑定的方式传递(类似 Vue)
用途
- 依赖构造函数传递数据的方式不能满足业务需求
- 需要一个新的、更好的跨组件数据传输方式
使用:
BuildContext.dependOnInheritedWidgetOfExactType<MyInheritedWidget>()
下方为一个完整代码,演示 inheritedWidget 如何作为参数传递方传递参数
import 'package:flutter/material.dart';
class Home extends StatelessWidget {
const Home({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
// 头部区域
appBar: AppBar(
// 标题
title: const Text('DataTable'),
// 左侧图标
leading: const Icon(Icons.assessment),
// 右侧图标组
actions: const <Widget>[
Icon(Icons.home),
Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Icon(Icons.move_down),
),
],
// 下部阴影
elevation: 0.0,
// 居中元素
centerTitle: true,
),
body: ListView(
children: [
Card(
child: MyState(),
),
],
),
);
}
}
class MyState extends StatefulWidget {
MyState({Key? key}) : super(key: key);
@override
State<MyState> createState() => _MyStateState();
}
class _MyStateState extends State<MyState> {
int _num = 0;
void _increment() {
setState(() {
_num++;
});
}
void _decrement() {
setState(() {
_num--;
});
}
@override
Widget build(BuildContext context) {
/*
InheritedWidget 可以存储需要的数据,可共享给该组件下的所有组件的数据并统一在该组件中存放
如若需要子组件传给父组件传数据时候,可以使用 InheritedWidget
该组件解决了传参过程中多余的从上到下的传递参数方式 节约了传递的消耗和拥堵
*/
return ShareDataWidfet(
// 存入 num 属性值
num: _num,
child: Center(
child: Column(children: [
ElevatedButton(
onPressed: _increment,
child: const Text('-'),
),
Padding(
padding: const EdgeInsets.all(20),
// child: Text('$_num'),
// 跨组件访问数据
child: MyCounter(),
),
ElevatedButton(
onPressed: _decrement,
child: const Icon(Icons.add),
),
/*
在InheritedWidget 下的所有子级组件都可以直接使用共享的数据内容
*/
Padding(
padding: const EdgeInsets.all(20),
child: TextDemo(),
),
]),
),
);
}
}
class TextDemo extends StatefulWidget {
TextDemo({Key? key}) : super(key: key);
@override
State<TextDemo> createState() => _TextDemoState();
}
class _TextDemoState extends State<TextDemo> {
@override
Widget build(BuildContext context) {
return Center(
child: MyCounter(),
);
}
}
// 返回 Text组件 并使用 inheritedWidget 进行存值赋值
class MyCounter extends StatefulWidget {
MyCounter({Key? key}) : super(key: key);
@override
State<MyCounter> createState() => _MyCounterState();
}
class _MyCounterState extends State<MyCounter> {
@override
Widget build(BuildContext context) {
// 使用 InheritedWidget 中的共享数据 从中读取成员属性 num
return Text(ShareDataWidfet.of(context)!.num.toString());
}
}
// 创建数据共享组件
class ShareDataWidfet extends InheritedWidget {
final int num;
final Widget child;
ShareDataWidfet({Key? key, required this.child, required this.num})
: super(key: key, child: child);
static ShareDataWidfet? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<ShareDataWidfet>();
}
@override
bool updateShouldNotify(ShareDataWidfet oldWidget) {
// 每次值发生改变都传递
return true;
}
}
该组件为 第三方包,需要在 pubspec.yaml 配置安装
配置 pubspec.yaml
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
provider: ^4.3.3
说明
provider 是对 InheritedWidget 的封装 文档地址
provider是第三方包 它有一个 ChangeNotifierProvider 组件 存储了 ChangeNotifier 的 数据
当调用数据的组件 ChangeNotifier 中的数据值发生改变 ChangeNotifierProvider 会通知 其他调用同样数据的Ui组件 将旧数据进行更新
优点
- 简化资源的分配和处置
- 懒加载
provider 组成
- 数据模型 ChangeNotigier [被观察者]
- 提供者(对组件提供数据) ChangeNotifierProvier [观察者]
- 使用数据的组件 的 不同称呼 Producer [生产者] Listener(Consumer) [消费者]
业务逻辑上看 分为 数据层-存放各组件数据 [Model] 观察层-监控数据变化并通知Ui层更新 [ViewModel] 视图层-各个Flutter组件 [View]
probider 使用
- 安装 Provider 第三方库
- 创建数据模型 T extends ChangeNotifier
- 创建 Provider 注册数据模型
1. Probider() 不会被要求随着变动而变动
2. ChangeNotifierProVider() 随着某些数据改变而被通知更新 - 获取数据模型并更新UI
1. 通过上下文 BuildContext
2. 通过静态方法 Provider.of(context)
下方是完整代码,实现由 Probider 管理数据
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class Home extends StatelessWidget {
const Home({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
// 2. 创建 Provider对象(注册数据模型) 赋值上下文内容
create: (BuildContext context) => LikesModel(),
child: Scaffold(
appBar: AppBar(
title: const Text('Provider'),
leading: const Icon(Icons.menu),
actions: const [
Icon(Icons.settings),
],
elevation: 0.0,
centerTitle: true,
),
body: const MyHomePage(),
),
);
}
}
// 1. 创建数据模型
class LikesModel extends ChangeNotifier {
// 初始值
int _counter = 0;
// get 方法 取私有成员值方法
int get counter => _counter;
incrementCounter() {
// 累加
_counter++;
// 通知UI更新函数 类似 setstate
notifyListeners();
}
}
/*
probider 负责页面的更新后 因此即使无状态组件也可以更新页面
因为 依赖 Probider组件 后 更新的任务不是委派给 setstate()
而是第三方组件负责 数据的观察 数据的更新 和UI的重新绘制 等操作
*/
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});
@override
Widget build(BuildContext context) {
return SizedBox(
width: double.infinity,
height: double.infinity,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// 获取并使用 数据模型 中的 私有属性值 (依据 get函数)
Text('${context.watch<LikesModel>().counter}'),
TextButton(
// 获取并使用 数据模型中的 方法
onPressed: Provider.of<LikesModel>(context).incrementCounter,
child: const Icon(Icons.thumb_up),
),
],
),
);
}
}
- Route 一个路由是一个屏幕或页面的抽象
- Navigator 管理路由的组件
1. Navigator 可以通过路由入栈和出栈来实现页面之间的跳转
2. 常用属性- initialRoute 初始路由 即默认页面
- onGenerateRoute (根据规则,匹配动态路由) // 根据传过来的路径进行动态判断
- onUnknownRoute 未知路由 也就是 404
- routes 路由集合
在组件中 直接使用 Navigator.push() 跳转至目标组件
Navigator
- push (跳转到指定组件)
Navigator.push(
context,MaterialPageRoute(builder:(context)=>组件名称()
);
- pop (回退)
Navigator.pop(context);
下方是完整代码,演示组件之间的跳转
import 'package:flutter/material.dart';
class Home extends StatelessWidget {
const Home({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('匿名路由'),
leading: const Icon(Icons.menu),
actions: const [
Icon(Icons.settings),
],
elevation: 0.0,
centerTitle: true,
),
body: const HomePage(),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return SizedBox(
width: double.infinity,
child: Center(
child: ElevatedButton(
onPressed: () {
Navigator.push(
context,
// 跳转到页面
MaterialPageRoute(builder: (context) => const Product()),
);
},
child: const Text('跳转到商品页面'),
)),
);
}
}
class Product extends StatelessWidget {
const Product({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('商品页面'),
),
body: SizedBox(
width: double.infinity,
child: Center(
child: ElevatedButton(
// 返回到上一个页面
onPressed: () => Navigator.pop(context),
child: const Text('回退'),
)),
),
);
}
}
一般在根组件注册常用路由,并给路由设置命名,使用 Navigator.pushNamed(context,'RouteName'); 跳转。
声明路由
- routes 路由表 (Map类型)
- initialRoute (初始路由)
- onUnknownRoute (未知路由 -404)
跳转操作
Navigator.pushNamed(context,'RouteName');
定义操作
MaterialApp(
// ..... //
routes:{ // 注册路由
'first': (context) => FirstPage(),
'second': (context) => SecondPage(),
},
initialRoute: 'first', // 初始路由
onUnknownRoute: (RouteSettings setting) => MaterialPageRoute(
builder: (context) => UnknownPage(), // 未知路由
)
)
使用命名路由操作
import 'package:flutter/material.dart';
class Home extends StatelessWidget {
const Home({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('首页'),
leading: const Icon(Icons.menu),
actions: const [
Icon(Icons.settings),
],
elevation: 0.0,
centerTitle: true,
),
body: Center(
child: Column(
children: [
ElevatedButton(
// 命名路由跳转
onPressed: () => Navigator.pushNamed(context, 'first'),
child: const Text('第一个组件'),
),
ElevatedButton(
// 命名路由跳转
onPressed: () => Navigator.pushNamed(context, 'second'),
child: const Text('第二个组件'),
),
ElevatedButton(
// 未定义的命名路由跳转
onPressed: () => Navigator.pushNamed(context, 'smm'),
child: const Text('未知路由'),
),
],
),
),
);
}
}
概念
多语言 internationalization (简称 i18n),在终端 (手机) 系统使用不同国家语言的文字时候,
应用理所应当切换为该终端 (手机) 系统使用的国家的语言
该组件为 官方包,需要在 pubspec.yaml 配置安装
介绍
终端(手机) 系统语言切换时候,Flutter应用的跟随切换,
- 组件 widget 国际化 (已封装在 flutter 中)
- 例如:日历,弹窗等常用组件的国际化
- 需要依赖 flutter_localizations 包
配置第三方包
在 flutter 项目根目录中的 pubspec.yaml 中,对应的位置引用包
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
在 flutter 的入口组件 main.dart 中,启用 多语言组件
import 'package:flutter/material.dart';
// 多语言组件
import 'package:flutter_localizations/flutter_localizations.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: '初号机',
home: Home(),
// 国际化 多语言组件
localizationsDelegates: [
// 本地化代理 多语言组件
GlobalMaterialLocalizations.delegate, // 安卓组件
GlobalWidgetsLocalizations.delegate, // 基础组件
GlobalWidgetsLocalizations.delegate, // IOS风格组件
],
// 支持的语言 (按需添加) 多语言组件
supportedLocales: [
Locale('en', 'US'), // 美国英语
Locale('zh', 'CN'), // 简体中文
],
);
}
}
配置完成后,localizationsDelegates 项对应的所有组件,一旦被 main.dart 所引用,就有了多语言的效果
效果展示
概念
向服务器地址发起请求,当返回结果后执行对应的操作
演示
http包 为 第三方包,需要在 pubspec.yaml 配置安装
使用 http 第三方包 在 pubspec.yaml 中配置 引用包
dependencies:
flutter:
sdk: flutter
http: ^0.12.0
在 dart 相关组件项目中实际使用