Flutter | Key的原理和使用(下)

简介: Flutter | Key的原理和使用(下)

栗子:

在切换屏幕方向的时候改变布局排列方式,并且保证状态不会重置


Center(
  child: MediaQuery.of(context).orientation == Orientation.portrait
      ? Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Counter(),
            Container(child: Counter(key: _globalKey)),
          ],
        )
      : Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Counter(),
            Container(child: Counter(key: _globalKey)),
          ],
        ),
)


上面是最开始写的代码,


通过上面的动图就会发现,第二个 Container 的状态是正确的,第一个则不对,因为第一个没有使用 GlobalKey,所以需要给第一个也加上 GlobalKey,如下:


Center(
        child: MediaQuery.of(context).orientation == Orientation.portrait
            ? Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  Counter(key: _globalKey1),
                  Counter(key: _globalKey2)
                ],
              )
            : Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  Counter(key: _globalKey1),
                  Container(child: Counter(key: _globalKey2))
                ],
              ),
      )


但是这样的写法确实有些 low,并且这种需求我们其实不需要 GlobalKey 也可以实现,代码如下:


Center(
  child: Flex(
    direction: MediaQuery.of(context).orientation == Orientation.portrait
        ? Axis.vertical
        : Axis.horizontal,
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[Counter(), Counter()],
  ),
)


使用了 Flex 之后,在 build 的时候 Flex 没有发生改变,所以就会重新找到 Element,所以状态也就不会丢失了。


但是如果内部的 Container 在屏幕切换的过程中会重新嵌套,那还是需要使用 GlobalKey,原因就不需要多说了吧!


GlobalKey 的第二种用法

Flutter 属于声明式编程,如果页面中某个组件的需要更新,则会将更新的值提取到全局,在更新的时候修改全局的值,并进行 setState。这就是最推荐的做法。如果这个状态需要在两个 widget 中共同使用,就把状态向上提升,毫无疑问这也是正确的做法。


但是通过 GlobalKey 我们可以直接在别的地方进行更新,获取状态,widget中数据等操作。前提是我们需要拿到 GlobalKey 对象,其实就类似于 Android 中的 findViewById 拿到对应的控件,但是相比 GlobalKey,GlobalKey 可以获取到 State,Widget,RenderObject 等。


下面我们看一下栗子:


final _globalKey = GlobalKey();
@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text(widget.title),
    ),
    body: Center(
      child: Flex(
        direction: MediaQuery.of(context).orientation == Orientation.portrait
            ? Axis.vertical
            : Axis.horizontal,
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Counter(key: _globalKey),
        ],
      ),
    ),
    floatingActionButton: FloatingActionButton(
      onPressed: () {},
      tooltip: 'Increment',
      child: Icon(Icons.add),
    ),
  );


和之前的例子差不多,现在只剩了一个 Counter 了。现在我们需要做的就是在点击 FloatingActionButton 按钮的时候,使这个 Counter 中的计数自动增加,并且获取到他的一些属性,代码如下:


floatingActionButton: FloatingActionButton(
    onPressed: () {
      final state = (_globalKey.currentState as _CounterState);
      state.setState(() => state._count++);
      final widget = (_globalKey.currentWidget as Counter);
      final context = _globalKey.currentContext;
      final render =
          (_globalKey.currentContext.findRenderObject() as RenderBox);
      ///宽高度  
      print(render.size);
      ///距离左上角的像素
      print(render.localToGlobal(Offset.zero));
    },
    child: Icon(Icons.add),
  ),
);


I/flutter (29222): Size(88.0, 82.0)
I/flutter (29222): Offset(152.4, 378.6)


可以看到上面代码中通过 _globakKey 获取到了 三个属性,分别是 state,widget 和 context。


其中使用了 state 对 _count 进行了自增。


而 widget 则就是 Counter 了。


但是 context 又是什么呢,我们点进去源码看一下:


Element? get _currentElement => _registry[this];
BuildContext? get currentContext => _currentElement;


通过上面两句代码就可以看出来 context 其实就是 Element 对象,通过查看继承关系可知道,Element 是继承自 BuildContext 的。


通过这个 context 的 findRenderObject 方法可以获取到 RenderObject ,这个 RenderObject 就是最终显示到屏幕上的东西,通过 RenderObject 我们可以获取到一一些数据,例如 widget 的宽高度,距离屏幕左上角的位置等等。


RenderObject 有很多种类型,例如 RenderBox 等,不同的 Widget 用到的可能并不相同,这里需要注意一点


实例


这个例子我们写一个小游戏,一个列表中有很多不同颜色的小方块,通过拖动这些方块来进行颜色的重排序。


通过点击按钮来打乱顺序,然后长按方框拖动进行重新排序;


下面我们来写一下代码:


final boxes = [
  Box(Colors.red[100], key: UniqueKey()),
  Box(Colors.red[300], key: UniqueKey()),
  Box(Colors.red[500], key: UniqueKey()),
  Box(Colors.red[700], key: UniqueKey()),
  Box(Colors.red[900], key: UniqueKey()),
];
  _shuffle() {
    setState(() => boxes.shuffle());
  }
 @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        ///可重排序的列表
        child: Container(
          child: ReorderableListView(
              onReorder: (int oldIndex, newIndex) {
                if (newIndex > oldIndex) newIndex--;
                final box = boxes.removeAt(oldIndex);
                boxes.insert(newIndex, box);
              },
              children: boxes),
          width: 60,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _shuffle(),
        child: Icon(Icons.refresh),
      ),
    );
  }


ReorderableListView:可重排序的列表,支持拖动排序


onReorder:拖动后的回调,会给出新的 index 和 旧的 index,通过这两个参数就可以对位置就行修改,如上所示

scrollDirection:指定横向或者竖向

还有一个需要注意的是 ReorderableListView 的 Item 必须需要一个 key,否则就会报错。


class Box extends StatelessWidget {
  final Color color;
  Box(this.color, {Key key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return UnconstrainedBox(
      child: Container(
        margin: EdgeInsets.all(5),
        width: 50,
        height: 50,
        decoration: BoxDecoration(
            color: color, borderRadius: BorderRadius.circular(10)),
      ),
    );
  }
}


上面是列表中 item 的 widget,需要注意的是里面使用到了 UnconstrainedBox,因为在 ReorderableListView 中可能使用到了尺寸限制,导致在 item 中设置的宽高无法生效,所以使用了 UnconstrainedBox。


体验了几次之后就发现了一些问题,


比如拖动的时候只能是一维的,只能上下或者左右,

拖动的时候是整个 item 拖动,并且会有一些阴影效果等,

必须是长按才能拖动

因为 ReorderableListView 没有提供属性去修改上面的这些问题,所以我们可以自己实现一个类似的效果。如下:


class _MyHomePageState extends State<MyHomePage> {
  final colors = [
    Colors.red[100],
    Colors.red[300],
    Colors.red[500],
    Colors.red[700],
    Colors.red[900],
  ];
  _shuffle() {
    setState(() => colors.shuffle());
  }
  int _slot;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Listener(
        onPointerMove: (event) {
          //获取移动的位置
          final x = event.position.dx;
          //如果大于抬起位置的下一个,则互换
          if (x > (_slot + 1) * Box.width) {
            if (_slot == colors.length - 1) return;
            setState(() {
              final temp = colors[_slot];
              colors[_slot] = colors[_slot + 1];
              colors[_slot + 1] = temp;
              _slot++;
            });
          } else if (x < _slot * Box.width) {
            if (_slot == 0) return;
            setState(() {
              final temp = colors[_slot];
              colors[_slot] = colors[_slot - 1];
              colors[_slot - 1] = temp;
              _slot--;
            });
          }
        },
        child: Stack(
          children: List.generate(colors.length, (i) {
            return Box(
              colors[i],
              x: i * Box.width,
              y: 300,
              onDrag: (Color color) => _slot = colors.indexOf(color),
              key: ValueKey(colors[i]),
            );
          }),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _shuffle(),
        child: Icon(Icons.refresh),
      ),
    );
  }
}
class Box extends StatelessWidget {
  final Color color;
  final double x, y;
  static final width = 50.0;
  static final height = 50.0;
  static final margin = 2;
  final Function(Color) onDrag;
  Box(this.color, {this.x, this.y, this.onDrag, Key key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return AnimatedPositioned(
      child: Draggable(
        child: box(color),
        feedback: box(color),
        onDragStarted: () => onDrag(color),
        childWhenDragging: box(Colors.transparent),
      ),
      duration: Duration(milliseconds: 100),
      top: y,
      left: x,
    );
  }
  box(Color color) {
    return Container(
      width: width - margin * 2,
      height: height - margin * 2,
      decoration:
          BoxDecoration(color: color, borderRadius: BorderRadius.circular(10)),
    );
  }
}


可以看到上面我们将 ReorderableListView 直接改成了 Stack , 这是因为在 Stack 中我们可以再 子元素中通过 Positioned 来自由的控制其位置。并且在 Stack 外面套了一层 Listener,这是用来监听移动的事件。


接着我们看 Box,Box 就是可以移动的小方块。在最外层使用了 带动画的 Positioned,在 Positioned 的位置发生变化之后就会产生平移的动画效果。


接着看一下 Draggable组件,Draggable 是一个可拖拽组件,常用的属性如下:


feedback:跟随拖拽的组件

childWhenDragging:拖拽时 chilid 子组件显示的样式

onDargStarted:第一次按下的回调

上面的代码工作流程如下:


1,当手指按住 Box 之后,计算 Box 的 index 。


2,当手指开始移动时通过移动的位置和按下时的位置进行比较。


3,如果大于,则 index 和 index +1 进行互换,小于则 index 和 index-1互换。


4,进行判决处理,如果处于第一个或最后一个时直接 return。


需要注意的是上面并没有使用 UniqueKey,因为 UniqueKey 是惟一的,在重新 build 的时候 因为 key 不相等,之前的状态就会丢失,导致 AnimatedPositioned 的动画无法执行,所以这里使用 ValueKey。这样就能保证不会出现状态丢失的问题。


当然也可以给每一个 Box 创建一个惟一的 UniqueKey 也可以。


由于是 gif 图,所以就会显得比较卡顿。


问题



其实在上面最终完成的例子中,还是有一些问题,例如只能是横向的,如果是竖着的,就需要重新修改代码。


并且 x 的坐标是从 0 开始计算的,如果在前面还有一些内容就会出现问题了。例如如果是竖着的,在最上面有一个 appbar,则就会出现问题。


修改代码如下所示:


class _MyHomePageState extends State<MyHomePage> {
 ///...
  int _slot;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Listener(
        onPointerMove: (event) {
          //获取移动的位置
          final y = event.position.dy;
          //如果大于抬起位置的下一个,则互换
          if (y > (_slot + 1) * Box.height) {
            if (_slot == colors.length - 1) return;
            setState(() {
              final temp = colors[_slot];
              colors[_slot] = colors[_slot + 1];
              colors[_slot + 1] = temp;
              _slot++;
            });
          } else if (y < _slot * Box.height) {
            if (_slot == 0) return;
            setState(() {
              final temp = colors[_slot];
              colors[_slot] = colors[_slot - 1];
              colors[_slot - 1] = temp;
              _slot--;
            });
          }
        },
        child: Stack(
          children: List.generate(colors.length, (i) {
            return Box(
              colors[i],
              x: 300,
              y: i * Box.height,
              onDrag: (Color color) => _slot = colors.indexOf(color),
              key: ValueKey(colors[i]),
            );
          }),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _shuffle(),
        child: Icon(Icons.refresh),
      ),
    );
  }
}class _MyHomePageState extends State<MyHomePage> {
 ///...
  int _slot;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Listener(
        onPointerMove: (event) {
          //获取移动的位置
          final y = event.position.dy;
          //如果大于抬起位置的下一个,则互换
          if (y > (_slot + 1) * Box.height) {
            if (_slot == colors.length - 1) return;
            setState(() {
              final temp = colors[_slot];
              colors[_slot] = colors[_slot + 1];
              colors[_slot + 1] = temp;
              _slot++;
            });
          } else if (y < _slot * Box.height) {
            if (_slot == 0) return;
            setState(() {
              final temp = colors[_slot];
              colors[_slot] = colors[_slot - 1];
              colors[_slot - 1] = temp;
              _slot--;
            });
          }
        },
        child: Stack(
          children: List.generate(colors.length, (i) {
            return Box(
              colors[i],
              x: 300,
              y: i * Box.height,
              onDrag: (Color color) => _slot = colors.indexOf(color),
              key: ValueKey(colors[i]),
            );
          }),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _shuffle(),
        child: Icon(Icons.refresh),
      ),
    );
  }
}


在上面代码中将原本横着的组件变成了竖着的,然后在拖动就会发现问题,如向上拖动的时候需要拖动两格才能移动,这就是因为y轴不是从0开始的,在最上面会有一个 appbar,我们没有将他的高度计算进去,所以就出现了这个问题。


这个时候我们就可以使用 GlobalKey 来解决这个问题:


final _globalKey = GlobalKey();
double _offset;
@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text(widget.title),
    ),
    body: Column(
      children: [
        SizedBox(height: 30),
        Text("WelCome", style: TextStyle(fontSize: 28, color: Colors.black)),
        SizedBox(height: 30),
        Expanded(
            child: Listener(
          onPointerMove: (event) {
            //获取移动的位置
            final y = event.position.dy - _offset;
            //如果大于抬起位置的下一个,则互换
            if (y > (_slot + 1) * Box.height) {
              if (_slot == colors.length - 1) return;
              setState(() {
                final temp = colors[_slot];
                colors[_slot] = colors[_slot + 1];
                colors[_slot + 1] = temp;
                _slot++;
              });
            } else if (y < _slot * Box.height) {
              if (_slot == 0) return;
              setState(() {
                final temp = colors[_slot];
                colors[_slot] = colors[_slot - 1];
                colors[_slot - 1] = temp;
                _slot--;
              });
            }
          },
          child: Stack(
            key: _globalKey,
            children: List.generate(colors.length, (i) {
              return Box(
                colors[i],
                x: 180,
                y: i * Box.height,
                onDrag: (Color color) {
                  _slot = colors.indexOf(color);
                  final renderBox = (_globalKey.currentContext
                      .findRenderObject() as RenderBox);
                  //获取距离顶部的距离
                  _offset = renderBox.localToGlobal(Offset.zero).dy;
                },
                key: ValueKey(colors[i]),
              );
            }),
          ),
        ))
      ],
    ),
    floatingActionButton: FloatingActionButton(
      onPressed: () => _shuffle(),
      child: Icon(Icons.refresh),
    ),
  );
}


解决的思路非常简单,


通过 GlobalKey 获取到当前 Stack 距离顶部的位置,然后用dy减去这个位置即可。


优化细节


经过上面的操作,基本的功能都实现了,最后我们优化一下细节,如随机颜色,固定第一个颜色,添加游戏成功检测等。


最终代码如下:


class _MyHomePageState extends State<MyHomePage> {
  MaterialColor _color;
  List<Color> _colors;
  initState() {
    super.initState();
    _shuffle();
  }
  _shuffle() {
    _color = Colors.primaries[Random().nextInt(Colors.primaries.length)];
    _colors = List.generate(8, (index) => _color[(index + 1) * 100]);
    setState(() => _colors.shuffle());
  }
  int _slot;
  final _globalKey = GlobalKey();
  double _offset;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(widget.title), actions: [
        IconButton(
          onPressed: () => _shuffle(),
          icon: Icon(Icons.refresh, color: Colors.white),
        )
      ]),
      body: Column(
        children: [
          SizedBox(height: 30),
          Text("WelCome", style: TextStyle(fontSize: 28, color: Colors.black)),
          SizedBox(height: 30),
          Container(
            width: Box.width - Box.margin * 2,
            height: Box.height - Box.margin * 2,
            decoration: BoxDecoration(
                color: _color[900], borderRadius: BorderRadius.circular(10)),
            child: Icon(Icons.lock, color: Colors.white),
          ),
          SizedBox(height: Box.margin * 2.0),
          Expanded(
              child: Center(
            child: Listener(
              onPointerMove: event,
              child: SizedBox(
                width: Box.width,
                child: Stack(
                  key: _globalKey,
                  children: List.generate(_colors.length, (i) {
                    return Box(
                      _colors[i],
                      y: i * Box.height,
                      onDrag: (Color color) {
                        _slot = _colors.indexOf(color);
                        final renderBox = (_globalKey.currentContext
                            .findRenderObject() as RenderBox);
                        //获取距离顶部的距离
                        _offset = renderBox.localToGlobal(Offset.zero).dy;
                      },
                      onEnd: _checkWinCondition,
                    );
                  }),
                ),
              ),
            ),
          ))
        ],
      ),
    );
  }
  _checkWinCondition() {
    List<double> lum = _colors.map((e) => e.computeLuminance()).toList();
    bool success = true;
    for (int i = 0; i < lum.length - 1; i++) {
      if (lum[i] > lum[i + 1]) {
        success = false;
        break;
      }
    }
    print(success ? "成功" : "");
  }
  event(event) {
    //获取移动的位置
    final y = event.position.dy - _offset;
    //如果大于抬起位置的下一个,则互换
    if (y > (_slot + 1) * Box.height) {
      if (_slot == _colors.length - 1) return;
      setState(() {
        final temp = _colors[_slot];
        _colors[_slot] = _colors[_slot + 1];
        _colors[_slot + 1] = temp;
        _slot++;
      });
    } else if (y < _slot * Box.height) {
      if (_slot == 0) return;
      setState(() {
        final temp = _colors[_slot];
        _colors[_slot] = _colors[_slot - 1];
        _colors[_slot - 1] = temp;
        _slot--;
      });
    }
  }
}
class Box extends StatelessWidget {
  final double x, y;
  final Color color;
  static final width = 200.0;
  static final height = 50.0;
  static final margin = 2;
  final Function(Color) onDrag;
  final Function onEnd;
  Box(this.color, {this.x, this.y, this.onDrag, this.onEnd})
      : super(key: ValueKey(color));
  @override
  Widget build(BuildContext context) {
    return AnimatedPositioned(
      child: Draggable(
        child: box(color),
        feedback: box(color),
        onDragStarted: () => onDrag(color),
        onDragEnd: (drag) => onEnd(),
        childWhenDragging: box(Colors.transparent),
      ),
      duration: Duration(milliseconds: 100),
      top: y,
      left: x,
    );
  }
  box(Color color) {
    return Container(
      width: width - margin * 2,
      height: height - margin * 2,
      decoration:
          BoxDecoration(color: color, borderRadius: BorderRadius.circular(10)),
    );
  }
}
相关文章
|
1月前
|
存储 移动开发 安全
Flutter加固原理及加密处理
Flutter加固原理及加密处理
39 0
|
3月前
|
JSON Dart 安全
Flutter App混淆加固、保护与优化原理
Flutter App混淆加固、保护与优化原理
50 0
|
3月前
|
存储 安全 数据安全/隐私保护
Flutter应用程序的加固原理
Flutter应用程序的加固原理
44 0
|
1月前
|
存储 安全 数据安全/隐私保护
Flutter应用程序的加固原理
Flutter应用程序的加固原理
25 0
|
3月前
|
Web App开发 Ubuntu 应用服务中间件
Flutter笔记:Web支持原理与实践
Flutter笔记:Web支持原理与实践
103 0
|
3月前
|
容器
Flutter笔记:Box协议的布局约束原理与应用
Flutter笔记:Box协议的布局约束原理与应用
48 0
|
3月前
|
存储 移动开发 安全
Flutter加固原理及加密处理
Flutter加固原理及加密处理
56 0
|
4月前
|
JSON Dart 安全
Flutter App混淆加固、保护与优化原理
在移动应用程序开发中,保护应用程序的代码和数据安全至关重要。本文将探讨如何对Flutter应用程序进行混淆、优化和保护,以提高应用程序的安全性和隐私。
|
4月前
|
存储 安全 数据安全/隐私保护
🚀Flutter应用程序的加固原理
在移动应用开发中,Flutter已经成为一种非常流行的技术选项,可以同时在Android和iOS平台上构建高性能、高质量的移动应用程序。但是,由于其跨平台特性,Flutter应用程序也面临着一些安全风险,例如反编译、代码泄露、数据泄露等问题。为了保护Flutter应用程序的安全性,开发者需要进行加固,提供更加安全的应用程序给用户使用。
|
4月前
|
存储 移动开发 安全
Flutter加固原理及加密处理
为了保护Flutter应用免受潜在的漏洞和攻击威胁,加固是必不可少的措施之一。Flutter加固原理主要包括代码混淆、数据加密、安全存储、反调试与反分析、动态加载和安全通信等多个方面。通过综合运用这些措施,可以提高Flutter应用的安全性,保护应用不受潜在的安全威胁和攻击。