Key的原理

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

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 {
  List 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 {
  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 {
  List 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 {
  List 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 {
  List 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 {
  @override
  Widget build(BuildContext context) {
    return Container(
      width: 100,
      height: 100,
      child: Text(widget.title),
      color: widget.color,
    );
  }
}
运行效果
点击右下角按钮删除数组数据

StatefulWidgetStatelessWidget的区别是什么?为什么StatelessWidget类型的小部件不受影响?

  • StatelessWidgetcolor属性属于Widget对象,而StatefulWidgetcolor属性属于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删除流程图
  • Widget树与Element树是一一对应的,当Widget树被创建时,同时会有一个Element树被创建;
  • 当把第一个Widget删除时,同时对应的Element树就会去调用canUpdate方法,查看之前保留的Widget与现在的Widget是否一样;它的判断是按照顺序判断的,Element1就会与Widget2进行对比,发现类型相同,为了高效进行复用,从而Element1Widget2进行绑定;
  • Widget删除的时候,并不会删除ElementElement树中保存了很多数据,而State对象就保存在Element树中;上面Element1指向Widget2Element2指向Widget3Element3没有Widget可指向了,从而释放掉;最终出现了数据被删除,而背景色依然在的问题。

下面尝试添加StfulItem,验证Element树是否会重新创建?

class _KeyDemoState extends State {
  List 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 {
  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获取到ChildPageState属性,然后更改State中的属性值。

Key的原理小结
*   Key本身是一个抽象类,有一个工厂构造方法(创建 ValueKey)。
*    直接子类主要有:LocalKey 和 GlobalKey
*    GlobalKey:帮助我们访问某个Widget的信息。
*   LocalKey:它用来区别哪个Element要保留,哪个Element要删除;diff算法的核心所在。
    *   ValueKey:以值作为参数(数字、字符串等)
    *   ObjectKey:以对象作为参数
    *   UniqueKey:(创建唯一标识)

Flutter调用原生页面

混合开发的两种情况
  • Flutter项目调用原生的功能
  • 原生项目嵌入Flutter,(不推荐,Flutter嵌入会导致包的体积增大,比较重)

下面我们来学习Flutter项目调用原生的功能,打开我们前面开发的wechat_demo项目,在我的页面实现更换用户头像的功能

  • State中添加交互Channel,用户头像添加点击事件并给原生发送消息

class _MinePageState extends State {
  // 用于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
      )
    ),
  ),
),
  • 原生接收消息并跳转相册页

@interface AppDelegate : FlutterAppDelegate
@property(nonatomic, strong) FlutterMethodChannel* methodChannel;
@end


- (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

// 遵守相册协议,以获取相册图片
@interface AppDelegate : FlutterAppDelegate
@property(nonatomic, strong) FlutterMethodChannel* methodChannel;
@end


// imageVc设置代理
imageVc.delegate = self;

// 获取相册图片,传递给flutter
-(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
    [picker dismissViewControllerAnimated:YES completion:^{
        // 获取相册图片资源路径
        NSString * imagePath = [NSString stringWithFormat:@"%@",info[@"UIImagePickerControllerImageURL"]];
        // 把图片资源路径传递给flutter
        [self.methodChannel invokeMethod:@"imagePath" arguments:imagePath];
    }];
}
  • Flutter处理原生传过来的图片数据
class _MinePageState extends State {

  // 定义头像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来更换头像

image.png
  • pubspec.yaml文件配置image_picker,并点击Pub get
引入image_picker
  • Xcode配置相册、相机权限
配置权限
  • 使用image_picker
class _MinePageState extends State {
  // 定义头像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

image.png

你可能感兴趣的:(Key的原理)