Key的作用
新建key_demo
工程,在main.dart
文件中我们点击查看StatelessWidget
源码,再次点击查看Widget
源码如下
@immutable
abstract class Widget extends DiagnosticableTree {
/// Initializes [key] for subclasses.
const Widget({ this.key });
......
任何一个Widget
都有key
值
StatefulWidget
类型的key
<!-- main.dart文件 -->
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
// 由于key的构造方法添加了const是一个常量对象,这里也要添加const,便于效率
home: const KeyDemo(),
);
}
}
class KeyDemo extends StatefulWidget {
const KeyDemo({Key? key}) : super(key: key);
@override
_KeyDemoState createState() => _KeyDemoState();
}
class _KeyDemoState extends State<KeyDemo> {
List<Widget> items = [
StfulItem('1111'),
StfulItem('2222'),
StfulItem('3333'),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
// 查看Text源码是一个常量对象,这里也要添加const
title: const Text('keyDemo'),
),
body: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: items,
),
// 悬浮按钮
floatingActionButton: FloatingActionButton(
// 查看Icon源码是一个常量对象,这里也要添加const
child: const Icon(Icons.add),
onPressed: () {
setState(() {
items.removeAt(0);
});
},
),
);
}
}
class StfulItem extends StatefulWidget {
// 接收内容
final String title;
StfulItem(this.title, {Key? key}) : super(key: key);
@override
_StfulItemState createState() => _StfulItemState();
}
class _StfulItemState extends State<StfulItem> {
final color = Color.fromRGBO(
Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1.0);
@override
Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
child: Text(widget.title),
color: color,
);
}
}
运行key_demo
工程查看效果
运行StatefulWidget类型demo
我们发现,点击按钮虽然删除了数组的第一条数据
,但是页面背景色却删除的是最后一个item
的背景色。
StatelessWidget
类型的key
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
// 由于key的构造方法添加了const是一个常量对象,这里也要添加const,便于效率
home: const KeyDemo(),
);
}
}
class KeyDemo extends StatefulWidget {
const KeyDemo({Key? key}) : super(key: key);
@override
_KeyDemoState createState() => _KeyDemoState();
}
class _KeyDemoState extends State<KeyDemo> {
List<Widget> items = [
StlItem('1111'),
StlItem('2222'),
StlItem('3333'),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
// 查看Text源码是一个常量对象,这里也要添加const
title: const Text('keyDemo'),
),
body: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: items,
),
// 悬浮按钮
floatingActionButton: FloatingActionButton(
// 查看Icon源码是一个常量对象,这里也要添加const
child: const Icon(Icons.add),
onPressed: () {
setState(() {
items.removeAt(0);
});
},
),
);
}
}
class StlItem extends StatelessWidget {
final String title;
StlItem(this.title, {Key? key}) : super(key: key);
final color = Color.fromRGBO(
Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1.0);
@override
Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
child: Text(title),
color: color,
);
}
}
运行key_demo
工程查看效果
运行StatelessWidget类型demo
我们发现,点击按钮删除了数组的第一条数据
,同时页面背景色也删除的是第一个item的背景色
,这就是我们想要的结果。
针对StatefulWidget
类型demo
的问题,下面进行分析解决?
- 方案一:给
StfulItem
小部件添加key
值
class _KeyDemoState extends State<KeyDemo> {
List<Widget> items = [
StfulItem('1111', key: const ValueKey(111)),
StfulItem('2222', key: const ValueKey(222)),
StfulItem('3333', key: const ValueKey(333)),
];
......
运行效果
点击右下角按钮删除数组数据
StatefulWidget
类型demo
使用key
值,就能准确定位具体的小部件。
- 方案二:把
color
属性放入Widget
中
class _KeyDemoState extends State<KeyDemo> {
List<Widget> items = [
StfulItem('1111'),
StfulItem('2222'),
StfulItem('3333'),
];
......
class StfulItem extends StatefulWidget {
// 接收内容
final String title;
StfulItem(this.title, {Key? key}) : super(key: key);
final color = Color.fromRGBO(
Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1.0);
@override
_StfulItemState createState() => _StfulItemState();
}
class _StfulItemState extends State<StfulItem> {
@override
Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
child: Text(widget.title),
color: widget.color,
);
}
}
运行效果
点击右下角按钮删除数组数据
StatefulWidget
与StatelessWidget
的区别是什么?为什么StatelessWidget
类型的小部件不受影响?
-
StatelessWidget
的color
属性属于Widget
对象,而StatefulWidget
的color
属性属于State
。 - 当删除数组的第一个元素时,
Widget
对象被删除了,而State
对象依然在内存中,被复用指向了第二个Widget
,从而导致页面背景色没有被删除掉;而第三个State
没有Widget
可指向,从而释放掉了。
通过Widget
源码分析
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
-
Flutter
使用的是增量渲染
,只是把改变的内容进行重新渲染,其他未变的内容进行复用;内容是否改变就是通过canUpdate
进行判断的,只有Widget
对象相同并且key
值也相同,才会允许更新; - 使用
StatefulWidget
出现的问题,就是只判断了Widget
对象的类型都是StfulItem
从而进行了更新;
-
Widget
树与Element
树是一一对应的,当Widget
树被创建时,同时会有一个Element
树被创建; - 当把第一个
Widget
删除时,同时对应的Element
树就会去调用canUpdate
方法,查看之前保留的Widget
与现在的Widget
是否一样;它的判断是按照顺序判断的,Element1
就会与Widget2
进行对比,发现类型相同,为了高效进行复用,从而Element1
与Widget2
进行绑定; -
Widget
删除的时候,并不会删除Element
,Element
树中保存了很多数据,而State
对象就保存在Element
树中;上面Element1
指向Widget2
,Element2
指向Widget3
,Element3
没有Widget
可指向了,从而释放掉;最终出现了数据被删除
,而背景色依然在
的问题。
下面尝试添加StfulItem
,验证Element
树是否会重新创建?
class _KeyDemoState extends State<KeyDemo> {
List<Widget> items = [
StfulItem('1111'),
StfulItem('2222'),
StfulItem('3333'),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
// 查看Text源码是一个常量对象,这里也要添加const
title: const Text('keyDemo'),
),
body: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: items,
),
// 悬浮按钮
floatingActionButton: FloatingActionButton(
// 查看Icon源码是一个常量对象,这里也要添加const
child: const Icon(Icons.add),
onPressed: () {
setState(() {
items.removeAt(0);
// 删除数组第一个元素,同时添加一个新的元素
items.add(StfulItem('4444'));
});
},
),
);
}
}
运行效果
点击右下角按钮删除数组数据
删除一个Widget
,同时添加一个新的元素,背景色值并没有发生变化,说明增量渲染的时候发现有空余的Element树
会直接复用,但是并不能证明Element4
没有创建,也有可能是创建Widget4
的同时也创建了Element4
,只是Element4
没有Widget
可以指向,创建完之后又销毁了。
推荐:打断点调试探索原理......
注意:定位小部件的时候,key
的作用非常重要;尤其是在使用StatefulWidget
小部件时key
就是用来标记小部件的。
GlobalKey的作用
StatelessWidget
正常是无法给StatefulWidget
传值的,我们可以借助GlobalKey
进行传值。
- 新建
key_demo.dart
文件,代码如下
import 'package:flutter/material.dart';
class GlobalKeyDemo extends StatelessWidget {
final GlobalKey<_ChildPageState> _globalKey = GlobalKey();
GlobalKeyDemo({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('GlobalKeyDemo'),
),
body: ChildPage(
key: _globalKey,
),
floatingActionButton: FloatingActionButton(
onPressed: () {
_globalKey.currentState!.setState(() {
_globalKey.currentState!.data =
'old:' + _globalKey.currentState!.count.toString();
_globalKey.currentState!.count++;
});
},
child: const Icon(Icons.add),
),
);
}
}
class ChildPage extends StatefulWidget {
const ChildPage({Key? key}) : super(key: key);
@override
_ChildPageState createState() => _ChildPageState();
}
class _ChildPageState extends State<ChildPage> {
int count = 0;
String data = 'hello';
@override
Widget build(BuildContext context) {
return Center(
child: Column(
children: [
Text(count.toString()),
Text(data),
],
),
);
}
}
-
main.dart
文件中使用GlobalKeyDemo
小部件
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: GlobalKeyDemo(),
);
}
}
通过GlobalKey传值
GlobalKeyDemo
通过GlobalKey
获取到ChildPage
的State
属性,然后更改State
中的属性值。
Key的原理小结
* Key本身是一个抽象类,有一个工厂构造方法(创建 ValueKey)。
* 直接子类主要有:LocalKey 和 GlobalKey
* GlobalKey:帮助我们访问某个Widget的信息。
* LocalKey:它用来区别哪个Element要保留,哪个Element要删除;diff算法的核心所在。
* ValueKey:以值作为参数(数字、字符串等)
* ObjectKey:以对象作为参数
* UniqueKey:(创建唯一标识)
Flutter调用原生页面
混合开发的两种情况
-
Flutter
项目调用原生的功能 - 原生项目嵌入
Flutter
,(不推荐,Flutter嵌入会导致包的体积增大,比较重)
下面我们来学习Flutter
项目调用原生的功能,打开我们前面开发的wechat_demo
项目,在我的页面实现更换用户头像的功能
-
State
中添加交互Channel
,用户头像添加点击事件并给原生发送消息
<!-- mine_page.dart文件 -->
class _MinePageState extends State<MinePage> {
// 用于flutter与原生通信
MethodChannel _methodChannel = MethodChannel('mine_page/method');
......
//头像
GestureDetector(
onTap: () {
// flutter给原生发送picture消息
_methodChannel.invokeMapMethod('picture');
},
child: Container(
width: 70,
height: 70,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
image: DecorationImage(
image: AssetImage('images/Hank.png'),
fit: BoxFit.cover
)
),
),
),
- 原生接收消息并跳转相册页
<!-- Appdelegate.h文件 -->
@interface AppDelegate : FlutterAppDelegate
@property(nonatomic, strong) FlutterMethodChannel* methodChannel;
@end
<!-- Appdelegate.m文件 -->
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[GeneratedPluginRegistrant registerWithRegistry:self];
FlutterViewController * vc = (FlutterViewController *)self.window.rootViewController;
// Flutter与原生是通过FlutterMethodChannel进行通信的
self.methodChannel = [FlutterMethodChannel methodChannelWithName:@"mine_page/method" binaryMessenger:vc];
UIImagePickerController * imageVc = [[UIImagePickerController alloc] init];
// 设置监听回调,flutter发送一个invokeMapMethod消息,这里就能够接收到
[self.methodChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult _Nonnull result) {
// flutter发送的消息是 _methodChannel.invokeMapMethod('picture');
// 接收到flutter的picture消息,就跳转相册页
if ([call.method isEqualToString:@"picture"]) {
[vc presentViewController:imageVc animated:YES completion:nil];
}
}];
// Override point for customization after application launch.
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
- 相册选中图片,要把图片回调给
Flutter
<!-- Appdelegate.h文件 -->
// 遵守相册协议,以获取相册图片
@interface AppDelegate : FlutterAppDelegate<UINavigationControllerDelegate,UIImagePickerControllerDelegate>
@property(nonatomic, strong) FlutterMethodChannel* methodChannel;
@end
<!-- Appdelegate.m文件 -->
// imageVc设置代理
imageVc.delegate = self;
// 获取相册图片,传递给flutter
-(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<UIImagePickerControllerInfoKey,id> *)info
{
[picker dismissViewControllerAnimated:YES completion:^{
// 获取相册图片资源路径
NSString * imagePath = [NSString stringWithFormat:@"%@",info[@"UIImagePickerControllerImageURL"]];
// 把图片资源路径传递给flutter
[self.methodChannel invokeMethod:@"imagePath" arguments:imagePath];
}];
}
-
Flutter
处理原生传过来的图片数据
class _MinePageState extends State<MinePage> {
// 定义头像File
File _avatarFile;
// 用于flutter与原生通信
MethodChannel _methodChannel = MethodChannel('mine_page/method');
@override
void initState() {
super.initState();
// 接收原生发送的消息
_methodChannel.setMethodCallHandler((call) {
if (call.method == 'imagePath') {
String imagePath = call.arguments.toString().substring(7);
setState(() {
_avatarFile = File(imagePath);
});
}
return null;
});
}
// Flutter展示相册图片
//头像
GestureDetector(
onTap: () {
_methodChannel.invokeMapMethod('picture');
},
child: Container(
width: 70,
height: 70,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
image: DecorationImage(
image: _avatarFile == null
? AssetImage('images/Hank.png')
: FileImage(_avatarFile),
fit: BoxFit.cover
)
),
),
),
相册选取图片
头像替换成功
image_picker
下面我们使用Flutter
官方框架image_picker来更换头像
-
pubspec.yaml
文件配置image_picker
,并点击Pub get
- Xcode配置相册、相机权限
- 使用
image_picker
class _MinePageState extends State<MinePage> {
// 定义头像File
File _avatarFile;
......
//头像
GestureDetector(
child: Container(
width: 70,
height: 70,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(12),
image: DecorationImage(
image: _avatarFile == null
? AssetImage('images/Hank.png')
: FileImage(_avatarFile),
fit: BoxFit.cover)
),
),
onTap: _pickImage,
),
void _pickImage() async {
try {
// 有可能获取为空,所以要try捕获异常
XFile file = await ImagePicker().pickImage(source: ImageSource.gallery);
setState(() {
_avatarFile = File(file.path);
});
} catch (e) {
print(e.toString());
setState(() {
_avatarFile = null;
});
}
}
注意:如果原生项目引用到image_picker
一类的库,启动的时候需要pod install
网友评论