Flutter
一. Dart声明类型
Dart是一个强类型语言,完全面向对象的一种编程语言
所以它跟js不同的是,dart在声明变量时,需要先声明类型
1. Object类型
dart也具有var的声明方式(虽然没有声明类型,但是var会自动根据后面内容,自动生成类型)
对dart来讲,所有的对象都是继承于object类型
import 'package:Dart_study/Dart_study.dart' as Dart_study;
void main(List<String> arguments) {
print('Hello world!');
// dart声明变量
var str= 'hello';
//var 的声明方式,会自动识别后面的类型
//所以再赋值别的类型是不可以的
//str=123;
print(str);
//显示声明了这个password是个object类型
//所以给它赋值成字符串或数字或其他就都是可以的
//因为这些都属于object的子类型
Object password =123;
password='obj';
}
上面只是举个例子
- 但其实,你声明object其实是没什么意义的,因为你把类型声明的这么大,其实也就相当于没有声明类型
2. dynamic动态类型
跟object有点像的是,它也是一个很大的类型
但是与object不同的是:
- object变量在点属性的时候,只有object拥有的方法
- 但是dynamic可以推断出所有可能的属性或方法(点属性)
- 它避免了一些编译器在编译阶段的检查,但是也就存在会引入bug的情况
- 因为有可能你点出来的这个方法它并不具备
- 所以一般还是不用这个
Object a='obj';
dynamic b='dynamic';
print(a.length);//会有提示表示object没有这个属性
print(b.length);
3. 常量的声明方式
- final
- const
区别:(看需求使用)
- const叫做编译时常量
- 意思是它在编译时就已经存在了,即使还没有运行到它,但是它已经存在了
- 在编译阶段就已经申请好内存了
- final是只有运行到时才会被初始化
- 也就是它什么时候用,什么时候才会申请一段内存空间
4. 变量类型
Number类型——int、double
import 'package:Dart_study/Dart_study.dart' as Dart_study;
// List<String> arguments
void main() {
int a=111;
double b=222.333;
// 把字符串转成整型
int c =int.parse("333");
// 把字符串转成double型
double d =double.parse("444.555");
print(a);
print(b);
print(c);
print(d);
}
String
//toString可以转成字符串
String a=123.toString();
//四舍五入
// 保留两位小数
String b=123.666.toStringAsFixed(2);
print(a);
print(b);
Bool
bool result=123 > 110;
bool result2=1 =='1'; // 如果类型不相同,直接就是false,不会再进行值的比较了
print(result);
print(result2);
List数组类型
List list=[1,3,5,7,9];
//也可以声明一个空数组
List newList=new List();
newList.add(1);
newList.addAll([2,3,4]);
print(newList);
print(newList.length);//数组长度
print(newList.first);//第一个元素
print(newList.last);//最后一个元素
print(newList[2]);//根据下标打印出某个值
// 其他还有很多,不一一列举,直接点可以看到
Map对象类型
本质上就是key value的键值对
有两种声明方式:
- 字面量的形式,直接大括号
- (书写上比较严格的是,需要用单引号或双引号包裹key)
- 直接实例化一个(也就是new一个)
//1. 字面量的形式声明
Map obj={'x':1,'y':2,'z':3};
//2. 或者直接实例化一个(也就是new一个)
Map obj2=new Map();
obj2['x']=1;
obj2['y']=2;
print(obj);
print(obj2);
print(obj.containsKey('x'));// 判断是否包含某一个key值
obj.remove('x');// 删除某个键值对
print(obj);
function
函数
例如 :
- main是入口函数,void是这个函数的返回类型(而void意思是不需要返回值的类型)
- String是需要返回的是字符串类型,里面参数意思是需要传递一个整型参数
int addAge(int age1,[int age2]){}
这个中括号意思是,这个参数是可传可不传的(可选参数)
- 但是并不建议这样写,因为如果这样写然后函数里又用到了这个参数,就必须给这个参数加三目运算
- 所以可以把中括号的方式替换成 赋初值 的方式(但是这种给初始值的方式,需要额外做的是,给参数外面括上大括号,不然报错)
int addAge({int age1,int age2=0}){}
- 建议用赋初值(默认值) 而不是中括号
void main() {
String userName=getUserName();
String personInfo=getPersonInfo(111);
print(userName);
print(personInfo);
}
String getUserName(){
return 'hello world!';
}
String getPersonInfo(int useId){
Map userInfo={
'111':'zhangsan',
'222':'lisi'
};
//return userInfo[useId];易错,类型不匹配
return userInfo[useId.toString()];
}
匿名函数
不具备方法名的函数,往往在某个方法里的回调里经常被使用
比如:
var list=[123,456,789];
list.forEach((number){
print(number);
});
二. Dart 类
1. 继承——单继承
- 类——接收参数
void main() {
var a=new Person(18,'zhangsan');
// 可以像下面这样写,但是我们希望实例化的时候就已经传好了,而不是实例化完再传
//所以就用到了构造函数
// a.age=123;
print(a.age);
}
// 类
class Person{
int age;
String name;
// 构造函数,也就是跟类同名的一个方法
Person(int age,String name){
//this指向的就是实例,person实例化之后的实例
this.age=age;
this.name=name;
}
}
- 类——成员函数
void main() {
var a=new Person(18,'zhangsan');
// 直接调方法
a.sayHello();
}
// 类
class Person{
int age;
String name;
// 构造函数,也就是跟类同名的一个方法
Person(int age,String name){
//this指向的就是实例,person实例化之后的实例
this.age=age;
this.name=name;
}
// 成员函数
void sayHello() {
print('my name is' + this.name);
}
}
- 继承
void main() {
//父
var a=new Person(18,'zhangsan');
// 直接调方法
a.sayHello();
//子
var b=new Worker(18, 'lisi', 3500);
b.sayHello();
}
// 类
class Person{
int age;
String name;
// 构造函数,也就是跟类同名的一个方法
Person(int age,String name){
//this指向的就是实例,person实例化之后的实例
this.age=age;
this.name=name;
}
// 成员函数
void sayHello() {
print('my name is ' + this.name);
}
}
// 继承
class Worker extends Person{
// 构造器
// 相当于woker这个构造函数,调了一个super,就是一个父类构造器
int salary;// 我们扩展的成员变量
Worker(int age, String name,int salary) : super(age, name){
// 这里面是我们要扩展的一些方法体
this.salary=salary;
}
//如果想写一个同名的方法的话,默认意思是重写(覆盖)
// 这里为了增强可读性,可以在前面写一个@override
@override
void sayHello(){
// 在子类里也可以调父类的
super.sayHello();
print("重写了");
print('my salart is '+ this.salary.toString());
}
}
注意:
- dart里面只有单继承,也就是说extends不能同时继承多个父类
- 如果想实现多继承的话,就要用到混合mixin (with)👇
2. mixin混合——多继承
混合:可以同时具备多个类的方法
class Eat {
void eat(){
print('eat');
}
}
class Sleep{
void sleep(){
print('sleep');
}
}
class Person with Eat,Sleep{
}
注意:
- 如果类里面本身就有方法,混合中也有同名方法的话,优先执行类里面的,其他就不执行了
- 如果类里面没有,多个混合里有,优先混合的执行最后一个的这个方法
3. 抽象类 abstract
就是在类的前面加上一个关键词
abstract class Animal{
// 只定义而没有实现,是抽象方法(也就是先把想法定义出来)
void baby();
// 方法实现了,所以不是抽象方法
void have_a_baby(){
print('have a baby');
}
}
定义好抽象方法后,可以交给继承这个的类去实现
...
class Person extends Animal with Eat,Sleep{
//(baby按住tab键可以补全)
@override
void baby() {
// TODO: implement baby
}
}
abstract class Animal{
// 只定义而没有实现,是抽象方法(也就是先把想法定义出来)
void baby();
}
注意:
- 抽象类不能被实例化
- 原因:因为它里面还有没实现的方法,调用的话会报错
三. Dart 使用库
1. 使用自己的库

main.dart
import 'package:Dart_study/Dart_study.dart' as Dart_study;
import 'ku/Calc.dart';
void main() {
int result=add(3,4);
print(result);
var c=new Calc(4,3);
c.minus();
}
ku/Calc.dart
int add(int x,int y){
return x+y;
}
class Calc{
int x;
int y;
Calc(int x,int y){
this.x=x;
this.y=y;
}
minus(){
print(this.x-this.y);
}
}
2. 用别人的库
从pub网站上拿我们需要的依赖包
配置文件👇

网站👇
The official repository for Dart and Flutter packages. (pub.dev)
import 'package:Dart_study/Dart_study.dart' as Dart_study;
import 'package:http/http.dart'as http;
import 'ku/Calc.dart';
void main () async{
int result=add(3,4);
print(result);
var c=new Calc(4,3);
c.minus();
var url = Uri.https('example.com', 'whatsit/create');
var response = await http.post(url, body: {'name': 'doodle', 'color': 'blue'});
print('Response status: ${response.statusCode}');
print('Response body: ${response.body}');
print(await http.read('https://www.baidu.com/'));
}
3. 使用dart自身的包
比如:
// 数学
import 'dart:math';
// 生成一个随机数
var r =new Random();
print(r.nextInt(10));
4. 延时加载(按需加载)
import 'dart:math' deferred as math;
// 告诉dart,开始加载这个库了
math.loadLibrary();
var r= new math.Random();
print(r.nextInt(10));
四.Dart的异步处理
- Future
void main () {
print("say hi");
//异步
Future.delayed(new Duration(seconds: 5),(){
print('chibaole');
});
print('play game');
}
- 异步调用,同步写法——async await
void main () async{
print("say hi");
//异步
//也就是会等待这个异步执行完成之后,才会继续往下执行
await Future.delayed(new Duration(seconds: 5),(){
print('chibaole');
});
print('play game');
}
Future.wait([])
的方法,可以让多个异步同时进行,都完成之后,才会继续往下
Future.wait([
Future.delayed(new Duration(seconds: 1),(){
print('001');
}),
Future.delayed(new Duration(seconds: 2),(){
print('002');
}),
Future.delayed(new Duration(seconds: 3),(){
print('003');
})
]).then((List results){
print('over');
});
- 不加await,三个异步同时进行(例如下面代码:3s)
- 加wait,一个执行完下一个(例如:6s)
Future.delayed(new Duration(seconds: 1),(){
print('001');
}),
Future.delayed(new Duration(seconds: 2),(){
print('002');
}),
Future.delayed(new Duration(seconds: 3),(){
print('003');
})
五. Flutter——widgets
flutter中几乎所有的对象都是一个widgets
flutter系列之:widgets,构成flutter的基石_为什么根widget要使用stateless-CSDN博客
1. 组件——文本,字体
children: <Widget>[
const Text(
'hello bodyhello bodyhello bodyhello bodyhello bodyhello bodyhello bodyhello bodyhello bodyhello bodyhello bodyhello bodyhello body',
textAlign: TextAlign.right,
maxLines: 2,// 最多显示两行
overflow: TextOverflow.ellipsis,//ellipsis省略号
textScaleFactor: 3 ,//字体放大三倍,
),
也可以设置统一的,可以多次使用
分别设置的话,可以用Text.rich
2. 组件——按钮
- RaiseButton
- FlatButton
- OutlineButton
- 自定义
3. 图片的使用和加载 和 图标
加载分为
- 加载本地资源
- 加载网络资源
本地资源
比如我们想放一个本地图片
body:Column(
children:<Widget> [
Image.asset("路径")
],
)
但是现在并不能实现,还需要加一个东西(在pubspec.yaml
文件里)
相当于把本地资源在这里注册进去
// 把本地路径补充上去
assets:
- images/a_dot_burr.jpeg
网络资源
Image.network("https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png",
width: 150,)
属性
fit:BoxFit.cover
colorBlendMode:BlendMode.difference
混合- ...
图标
icon:Icon(Icons....)
这个可以选择自带的- 还可以使用iconfont图标库(学会使用字体库 p13集)
4. 下拉框
5. 单选框和多选框
6. 输入框
六. flutter——布局
1. 线性布局
例子:
body:Row(
children: <Widget>[
Container(
width: 200,
height: 350,
color: Colors.blue,
),
Container(
width: 300,
height: 400,
color: Colors.red,
)
],
),
效果:
出错原因:
- 盒子超出了row的范围
- (如果想做等分——用弹性布局来做(后面会讲到))
横向对齐方式
// 首先关键
body:Row()
crossAxisAlignment:CrossAxisAlignment.start ,// 靠上对齐
crossAxisAlignment:CrossAxisAlignment.end,// 靠下对齐
crossAxisAlignment:CrossAxisAlignment.center ,// 默认状态,中线对齐
垂直对齐方式
// 首先关键
body:Column()
// 跟上面代码样子是一样的
crossAxisAlignment:CrossAxisAlignment.start ,// 靠左对齐
crossAxisAlignment:CrossAxisAlignment.end,// 靠右对齐
crossAxisAlignment:CrossAxisAlignment.center ,// 默认状态,中线对齐
2. 弹性布局
最常见的应用场景: n等分,一个宽度固定其它灵活分配
// 首先关键
body:Flex(
direction:Axis.horizontal,// 是什么方向,flex布局必须要有的(horizontal这个是横向的)
)
举个例子:
body:Flex(
direction:Axis.horizontal,// 是什么方向,flex布局必须要有的(horizontal这个是横向的)
children: <Widget>[
Expanded(
flex: 1,//占一份
child: Container(
height: 300,
color: Colors.red,
),
),
Expanded(
flex: 1,//占一份
child: Container(
height: 300,
color: Colors.blue,
),
),
Expanded(
flex: 1,//占一份
child: Container(
height: 300,
color: Colors.yellow,
),
)
],
)
下面代码多写了一个单独的Container,意思是这个盒子固定宽度占50,剩下区域三等分
body:Flex(
direction:Axis.horizontal,// 是什么方向,flex布局必须要有的(horizontal这个是横向的)
children: <Widget>[
Container(
width: 50,
height: 300,
color: Colors.black,
),
Expanded(
flex: 1,//占一份
child: Container(
height: 300,
color: Colors.red,
),
),
Expanded(
flex: 1,//占一份
child: Container(
height: 300,
color: Colors.blue,
),
),
Expanded(
flex: 1,//占一份
child: Container(
height: 300,
color: Colors.yellow,
),
)
],
)
direction:Axis.vertical
是垂直方向,同理
3. 流式布局
比如当我们在布局时,超过了最大行宽,但是我们并不想它报错,而是希望它超过后就换行,那么就要用到流式布局
body:Wrap(
)
流式布局中还有几个属性👇
// 比如
spacing:25 // 横向盒子与盒子之间的间距
runSpacing:10 // 垂直方向间距
alignment:WrapAlignment.center// 对齐方式
七. flutter——定位
有个特点:可以层叠覆盖
// 这个SizedBox是修饰子组件大小的(也就是child)
body:SizedBox(
// 这个宽高修饰的是这个Stack的大小
width: 300,
height: 300,
child: Stack(
children: <Widget>[
// 定位
Positioned(
// 这个定位最多写两个!!
left: 15,
top: 30,
// 需要定位的这个child盒子
child: Container(
height: 100,
width: 100,
color: Colors.yellow,
),
),
Positioned(
right: 10,
top: 100,
child: Container(
width: 200,
height: 200,
color: Colors.blue,
),
)
],
),
)
这个属性也可以设置定位👇
alignment:Alignment.topRight ,//设置左对齐还是右对齐
八. flutter——内边距
body:Container(
width: 400,
height: 400,
color: Colors.red,
//all代表四个方向都是一样的
child: Padding(padding: EdgeInsets.all(30),
child: Container(
color: Colors.blue,
)),
)
EdgeInsets.all()
,是四个方向.fromlTRB()
,表示四个方向的值可以单独控制(顺时针,左上右下)only(top:10)
,代表只有上边距,别的方向同理
九. flutter——布局限制类容器
就是用来限制他的子容器,也就是子元素的
body:ConstrainedBox(
//expand默认填充整个剩余空间
constraints:BoxConstraints.expand(),
child: Container(
color: Colors.red,
),
)
- 也可以自己定义
body:ConstrainedBox(
//expand默认填充整个剩余空间
constraints:BoxConstraints(
maxHeight: 200,
maxWidth: 100,
),
child: Container(
color: Colors.red,
),
)
- 想要一个方向占满
maxHeight:double.infinity,//意思是最大高度是无限大
十. flutter——集容器
1. 装饰容器 Decoration
在绘制内容之前,可以先绘制一些装饰的内容
相当于对child装饰
body:DecoratedBox(decoration: BoxDecoration(
gradient: LinearGradient(colors: [Colors.yellow,Colors.greenAccent]),//渐变
borderRadius: BorderRadius.all(Radius.circular(3.0)),//圆角
boxShadow: [
//阴影
BoxShadow(
color: Colors.black,
offset: Offset(3,3),
blurRadius: 4.0
)
]
),
//FlatButton已经被弃用了
child:TextButton(onPressed:null,child:Text("hello"))
)
2. 变换transform
改变的是表象的空间,内容实际占的还是原来的空间
拉伸skew
body: Column(
children: <Widget>[
Container(
color: Colors.red,
child: Transform(
// 在x轴上进行拉伸
transform: new Matrix4.skewY(0.5),
child: Container(
color: Colors.blue,
child:Text("hello")
)),
)
],
),
放大scale
body: Column(
children: <Widget>[
Container(
color: Colors.red,
child: Transform.scale(
// 放大
// 但是所占的空间(大小)不变,放大的是里面的内容
scale: 4,//1 就是原尺寸
child: Container(
color: Colors.blue,
child:Text("hello")
)),
)
],
),
旋转routate
body: Column(
children: <Widget>[
Container(
color: Colors.red,
child: Transform.rotate(
// 旋转
angle: math.pi/2,//旋转90度,math需要import
child: Container(
color: Colors.blue,
child:Text("hello")
)),
)
],
),
平移translate
body: Column(
children: <Widget>[
Container(
color: Colors.red,
child: Transform.translate(
// 平移
offset: Offset(200,50),//像右移动了200,向下移动了50
child: Container(
color: Colors.blue,
child:Text("hello")
)),
)
],
),
3. Container
4. 可滚动的组件
- 内容超出容器后,它就会自动出现一个滚动条
body:Scrollbar(
child: SingleChildScrollView(
child: Container(
height: 3000,
color: Colors.red,
),
),
)
- 有固定表头,还有容器高度——ListView
body: Column(
children: <Widget>[
ListTile(title: Text('固定的表头')),
Container(
height: 400,
child: ListView.builder(
itemCount: 50,
itemExtent: 50,
itemBuilder:(BuildContext context,int index){
return Text("列表内容"+index.toString());
}
),
)
],
),
- 格子布局
body:GridView(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,// 每行有几格
childAspectRatio: 1 // 横纵比
),
children: <Widget>[
Text("1"),
Text("2"),
Text("3"),
Text("4"),
Text("5"),
Text("6"),
Text("7"),
Text("8"),
Text("9"),
],
)
十一. flutter——脚手架Scaffold
- 基本结构
- 顶部导航
- 底部导航
- 侧边抽屉
十二. flutter——事件
绑事件需要在最外层加个listener
1. 指针事件
绑事件需要在最外层加个listener
从子到父冒泡
(tip:在flutter中没有阻止冒泡这个功能)
body:Listener(
onPointerDown: (e){
print("down");
},
onPointerUp: (e){
print("up");
},
onPointerMove: (e){
print("move");
},
onPointerCancel: (e){
print("cancel");
},
child: Container(
width:200,
height: 200,
color: Colors.red,
),
)
2. 手势事件
可用于交互
绑事件需要在最外层加个
body: GestureDetector(
child: Container(
width: 200,
height: 200,
color: Colors.greenAccent,
),
//on找到事件
// onTap
onTap: (){
print("tap");
},
// 双击
onDoubleTap: (){
print("doubleTap");
},
// 长按
onLongPress: (){
print("long press");
},
// 还有很多不一一列举,详情需要查询api
// 拖动,缩放...
),
十二. flutter——网络请求
点击之后👇
在flutter中如何访问一个HTTP接口
使用第三方库dio
- stf快捷创建一个有状态的组件
- 在pubspec.yaml文件里添加dio依赖,然后在命令行flutter pub get下载—— dio | Dart package (pub.dev)
- 然后页面中import
import 'package:dio/dio.dart';
- 添加mypage
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
//这里添加MyPage
body: MyPage(),
);
}
}
- 完整stful
// 一个有状态的组件
//这样在初始化的时候就会显示这个组件
class MyPage extends StatefulWidget {
const MyPage({super.key});
@override
State<MyPage> createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
String _data="";
@override
Widget build(BuildContext context) {
return Container(
child: Column(
children:<Widget> [
TextButton(onPressed:()async{
Dio dio=new Dio();
//async await 处理了这句的报错,相当于异步处理变成了同步调用
Response res= await dio.get('https://www.baidu.com');
print("result"+res.data.toString());
// 通知页面刷新
setState(() {
_data=res.data.toString();
});
},
child: Text("发起http请求"),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all<Color>(Colors.red),
),
),
Scrollbar(
child: Container(
height: 400,
child: SingleChildScrollView(
child: Text(_data),
),
))
],
),
);
}
}
上面是以get距离,请求的是百度的地址
当然也可以传参
p33 实战
十三. flutter——路由
1. 从A页面跳转到B页面
-
- 快捷创建两个stl
-
- 分别写....
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
//这里添加MyPage
body: FirstPage(),
);
}
}
//1. ————————————————————————————————————————
class FirstPage extends StatelessWidget {
const FirstPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("第一个页面"),
),
body: Column(
children: <Widget>[
TextButton(
onPressed: (){
// 跳(第一个参数:上下文,第二个参数:往哪跳)
Navigator.push(context, new MaterialPageRoute(builder: (route)=>new SecondPage()));
},
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all<Color>(Colors.red),
),
child:Text("跳转到第二个页面"))
],
),
);
}
}
//2. ——————————————————————————————————————————
class SecondPage extends StatelessWidget {
const SecondPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("第二个页面"),
),
body: Column(
children: <Widget>[
TextButton(
onPressed: (){
Navigator.pop(context);
},
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all<Color>(Colors.blue),
),
child:Text("返回到第一个页面"))
],
),
);
}
}
2. 页面间传递数据
- 页面向另一个页面传递参数
- 另一个页面还可以返回给第一个页面
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
//这里添加MyPage
body: ListPage(),
);
}
}
class ListPage extends StatelessWidget {
// const ListPage({super.key});
List _data=[
{"id":1,"name":"oppo"},
{"id":2,"name":"huawei"},
{"id":3,"name":"xiaomi"},
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
height: 400,
child: ListView.builder(
itemCount: _data.length,
itemBuilder: (context,index){
return new ListTile(
title:Text(_data[index]["name"]),
onTap: ()async{
String result=await Navigator.push(context, new MaterialPageRoute(builder: (context)=>new DetailPage(_data[index]["id"],_data[index]["name"])));
print("接收到的返回值:"+result);
},
);
}
),
),
);
}
}
class DetailPage extends StatelessWidget {
// const DetailPage({super.key});
int goods_id=0;
String goods_name="";
// 第二个页面通过构造函数来请求参数
DetailPage(this.goods_id,this.goods_name);
@override
Widget build(BuildContext context) {
return Container(
child: Column(
children: <Widget>[
Text("id"+this.goods_id.toString()),
Text("name"+this.goods_name),
TextButton(
onPressed: (){
Navigator.pop(context,"这是来自第二个页面的问候"+this.goods_name);
},
child: Text("返回"))
],
),
);
}
}
十四. flutter——调用原生native代码
- 引两个包
import 'dart:async';
import 'package:flutter/services.dart';
转载自:https://juejin.cn/post/7367009718835920931