Key的原理

2021-12-24  本文已影响0人  浅墨入画

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的问题,下面进行分析解决?
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值,就能准确定位具体的小部件。

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,
    );
  }
}
运行效果 点击右下角按钮删除数组数据

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

通过Widget源码分析
static bool canUpdate(Widget oldWidget, Widget newWidget) {
  return oldWidget.runtimeType == newWidget.runtimeType
    && oldWidget.key == newWidget.key;
}
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进行传值。

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),
        ],
      ),
    );
  }
}
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项目调用原生的功能,打开我们前面开发的wechat_demo项目,在我的页面实现更换用户头像的功能

<!-- 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];
}
<!-- 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];
    }];
}
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来更换头像

image.png 引入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

image.png
上一篇下一篇

猜你喜欢

热点阅读