美文网首页
Key的原理

Key的原理

作者: 浅墨入画 | 来源:发表于2021-12-24 17:19 被阅读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的问题,下面进行分析解决?
    • 方案一:给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,
        );
      }
    }
    
    运行效果 点击右下角按钮删除数组数据

    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<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获取到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,用户头像添加点击事件并给原生发送消息
    <!-- 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来更换头像

    image.png
    • pubspec.yaml文件配置image_picker,并点击Pub get
    引入image_picker
    • 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

    image.png

    相关文章

      网友评论

          本文标题:Key的原理

          本文链接:https://www.haomeiwen.com/subject/ygfofrtx.html