Flutter:如何正确使用GlobalKey以避免‘多个Widget使用同一GlobalKey’错误
我看了你的代码,立刻发现了问题所在——你在WordsToPress组件里犯了一个很容易忽略的错误:同一个GlobalKey被同时赋值给了WordsToPress本身和它内部的Container。Flutter要求每个GlobalKey必须唯一对应一个Widget,这就是你报错的直接原因。
接下来咱们一步步解决这个问题,同时优化你的实现:
第一步:修复重复使用GlobalKey的问题
在你的WordsToPress代码里,你把传入的key同时给了组件本身和子Container:
const WordsToPress( {required this.title, required this.key, required this.onPressed}) : super(key: key); // 然后在build里又给Container设了同一个key child: Container( key: key, ... )
这就导致同一个Key被两个Widget绑定,直接触发了重复Key的错误。你只需要去掉Container的key赋值,让WordsToPress自己持有这个Key就够了,因为你需要获取的是这个组件的位置信息。
第二步:正确创建独立的GlobalKey实例
虽然你在build里给每个WordsToPress传了GlobalKey(),但更好的做法是在State类里提前初始化这些Key,避免build方法重复创建(虽然Flutter会处理,但这样更规范)。修改_LeftSideWidgetState:
class _LeftSideWidgetState extends State<LeftSideWidget> { double thumbWidth = 70.0; var position = 0.0; // 为每个WordsToPress创建独立的GlobalKey final key1 = GlobalKey(); final key2 = GlobalKey(); final key3 = GlobalKey(); changePosition(GlobalKey key) { final RenderObject? object = key.currentContext!.findRenderObject(); final RenderBox renderBox = object as RenderBox; final size = object.semanticBounds; position = renderBox.localToGlobal(Offset.zero).dy; setState(() { position = (size.center.dy + position - (thumbWidth / 2)); print(position.toString()); }); } @override Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.max, children: [ Spacer(), WordsToPress( title: 'TEST LINE 1', key: key1, // 使用提前初始化的key onPressed: (GlobalKey key) => changePosition(key), ), Spacer(), WordsToPress( title: 'TEST LINE 2', key: key2, onPressed: (GlobalKey key) => changePosition(key), ), Spacer(), WordsToPress( title: 'TEST LINE 3', key: key3, onPressed: (GlobalKey key) => changePosition(key), ), Spacer(), ], ); } }
第三步:优化方案——不用GlobalKey也能获取中间位置
其实如果你的需求只是获取点击区域的中间位置,完全可以不用GlobalKey,直接利用GestureDetector的onTapUp回调里的TapUpDetails来获取位置,这样更轻量:
修改WordsToPress的onPressed参数,改为传递TapUpDetails和组件的BuildContext:
class WordsToPress extends StatelessWidget { final String title; final Function(TapUpDetails, BuildContext) onPressed; const WordsToPress( {required this.title, required this.onPressed, Key? key}) : super(key: key); @override Widget build(BuildContext context) { return GestureDetector( onTapUp: (details) => onPressed(details, context), child: RotatedBox( quarterTurns: -1, child: Text( title, ), ), ); } }
然后修改_LeftSideWidgetState的changePosition方法:
changePosition(TapUpDetails details, BuildContext context) { final RenderBox renderBox = context.findRenderObject() as RenderBox; final globalOffset = renderBox.localToGlobal(Offset.zero); final size = renderBox.size; setState(() { // 计算组件中心的全局y坐标,再调整thumb的位置 position = globalOffset.dy + size.center.dy - (thumbWidth / 2); print(position.toString()); }); } // 调用的时候改成: WordsToPress( title: 'TEST LINE 1', onPressed: (details, context) => changePosition(details, context), ),
这个方案完全不需要GlobalKey,代码更简洁,也避免了Key相关的问题。
最终修复后的完整代码(使用GlobalKey的版本)
import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( home: Home(), ); } } class Home extends StatelessWidget { final scaffoldKey = GlobalKey<ScaffoldState>(); @override Widget build(BuildContext context) { return Scaffold( key: scaffoldKey, body: SafeArea( child: Row( mainAxisSize: MainAxisSize.max, children: [LeftSideWidget()], ), ), ); } } class DisplayCircle extends StatelessWidget { final Color color; DisplayCircle({Key? key, required this.color}) : super(key: key); @override Widget build(BuildContext context) { return Container( width: 120, height: 120, clipBehavior: Clip.antiAlias, decoration: BoxDecoration( shape: BoxShape.circle, ), child: CircleAvatar( backgroundColor: color, ), ); } } class LeftSideWidget extends StatefulWidget { const LeftSideWidget({ Key? key, }) : super(key: key); @override _LeftSideWidgetState createState() => _LeftSideWidgetState(); } class _LeftSideWidgetState extends State<LeftSideWidget> { double thumbWidth = 70.0; var position = 0.0; // 初始化独立的GlobalKey final key1 = GlobalKey(); final key2 = GlobalKey(); final key3 = GlobalKey(); changePosition(GlobalKey key) { final RenderObject? object = key.currentContext!.findRenderObject(); final RenderBox renderBox = object as RenderBox; final size = object.semanticBounds; position = renderBox.localToGlobal(Offset.zero).dy; setState(() { position = (size.center.dy + position - (thumbWidth / 2)); print(position.toString()); }); } @override Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.max, children: [ Spacer(), WordsToPress( title: 'TEST LINE 1', key: key1, onPressed: (GlobalKey key) => changePosition(key), ), Spacer(), WordsToPress( title: 'TEST LINE 2', key: key2, onPressed: (GlobalKey key) => changePosition(key), ), Spacer(), WordsToPress( title: 'TEST LINE 3', key: key3, onPressed: (GlobalKey key) => changePosition(key), ), Spacer(), ], ); } } class WordsToPress extends StatelessWidget { final String title; final GlobalKey key; final Function(GlobalKey) onPressed; const WordsToPress( {required this.title, required this.key, required this.onPressed}) : super(key: key); @override Widget build(BuildContext context) { return GestureDetector( onTapUp: (_) => onPressed(key), child: RotatedBox( quarterTurns: -1, child: Text( title, ), ), ); } }
这样修改后,你就不会再遇到重复GlobalKey的错误,同时也能正确获取到点击Widget的中间位置了。
内容的提问来源于stack exchange,提问作者George Lee

