You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter中SwipableStack滑动后出现Multiple widgets used the same GlobalKey错误

解决SwipableStack中GlobalKey重复使用问题

问题场景

开发了一个Tinder式推荐项页面,使用SwipableStack堆叠带TextFormField的卡片:左滑取消、右滑接受(右滑时需验证输入)。首次构建正常,但接受首个项并移除后,重建页面时反复触发Multiple widgets used the same GlobalKey错误。

错误日志

======== Exception caught by widgets library =======================================================
The following assertion was thrown while finalizing the widget tree:
Multiple widgets used the same GlobalKey.

The key [LabeledGlobalKey<FormState>#9dc26] was used by multiple widgets. The parents of those widgets were:

  • Padding(padding: EdgeInsets.all(15.0), dependencies: [Directionality], renderObject: RenderPadding#386b3)
  • Padding(padding: EdgeInsets.all(15.0), dependencies: [Directionality], renderObject: RenderPadding#0a341)
    A GlobalKey can only be specified on one widget at a time in the widget tree.
    When the exception was thrown, this was the stack:
    #0 BuildOwner._debugVerifyGlobalKeyReservation.<anonymous closure>.<anonymous closure>.<anonymous closure> (package:flutter/src/widgets/framework.dart:3205:13)
    #1 _LinkedHashMapMixin.forEach (dart:collection-patch/compact_hash.dart:633:13)
    #2 BuildOwner._debugVerifyGlobalKeyReservation.<anonymous closure>.<anonymous closure> (package:flutter/src/widgets/framework.dart:3149:20)
    #3 _LinkedHashMapMixin.forEach (dart:collection-patch/compact_hash.dart:633:13)
    #4 BuildOwner._debugVerifyGlobalKeyReservation.<anonymous closure> (package:flutter/src/widgets/framework.dart:3144:36)
    #5 BuildOwner._debugVerifyGlobalKeyReservation (package:flutter/src/widgets/framework.dart:3213:6)
    #6 BuildOwner.finalizeTree.<anonymous closure> (package:flutter/src/widgets/framework.dart:3267:11)
    #7 BuildOwner.finalizeTree (package:flutter/src/widgets/framework.dart:3342:8)
    #8 WidgetsBinding.drawFrame (package:flutter/src/widgets/binding.dart:1169:19)
    #9 RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:468:5)
    #10 SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1397:15)
    #11 SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1318:9)
    #12 SchedulerBinding._handleDrawFrame (package:flutter/src/scheduler/binding.dart:1176:5)
    #13 _invoke (dart:ui/hooks.dart:312:13)
    #14 PlatformDispatcher._drawFrame (dart:ui/platform_dispatcher.dart:419:5)
    #15 _drawFrame (dart:ui/hooks.dart:283:31)
    ====================================================================================================

原因分析

  • 移除首个item后,重新生成的formKeys列表依赖索引绑定,但SwipableStack内部可能残留旧Widget实例(携带旧GlobalKey),新Widget复用相同索引的Key导致冲突。
  • 手动维护recommendationTiles和noOfRemovedItems的逻辑复杂,容易出现索引错位,加剧Key冲突问题。

修复方案

核心改动

  1. 移除手动维护的recommendationTiles和noOfRemovedItems,直接在SwipableStack的builder中根据当前index生成Widget,避免缓存旧实例。
  2. 用item的唯一标识(如label)绑定GlobalKey,替代索引关联,防止索引变化导致Key复用错误。
  3. 简化验证逻辑,通过当前index获取对应Form的Key,不再固定取formKeys[0]。

完整修复代码

import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:../services/formatter.dart';
import 'package:../templates/styling.dart';
import 'package:swipable_stack/swipable_stack.dart';

class RecommendationsScreen extends StatefulWidget {
  static late SwipableStackController controller;

  const RecommendationsScreen({
    super.key,
  });

  @override
  State<RecommendationsScreen> createState() => _RecommendationsScreenState();
}

class _RecommendationsScreenState extends State<RecommendationsScreen> {
  final int swipeActionDuration = 750;
  // 用item的label作为唯一标识,存储对应FormKey
  final Map<String, GlobalKey<FormState>> _formKeys = {};

  List<Map<String, dynamic>> items = [
    {'label': 'ABC'},
    {'label': 'DEF'},
    {'label': 'GHI'},
    {'label': 'JKL'},
  ];

  @override
  void initState() {
    super.initState();
    // 初始化每个item的FormKey
    for (var item in items) {
      _formKeys[item['label']] = GlobalKey<FormState>();
    }
  }

  Widget _buildRecommendationTile(Map<String, dynamic> item) {
    return Container(
      color: kTileBackgroundColor,
      padding: EdgeInsets.all(kDefaultScreenPaddingAllSides),
      child: Form(
        key: _formKeys[item['label']],
        child: TextFormField(
          decoration: InputDecoration(
            label: Text(item['label']),
          ),
          validator: (String? value) {
            final String adjustedValue = removeAllSeparators(
              value,
              Localizations.localeOf(context).toLanguageTag(),
            );

            if (adjustedValue.isEmpty) {
              return 'Please enter a value';
            }

            return null;
          },
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    final AppLocalizations t = AppLocalizations.of(context)!;

    return Container(
      margin: const EdgeInsets.symmetric(vertical: kDefaultScreenPaddingAllSides),
      child: Stack(
        children: [
          items.isNotEmpty
              ? Positioned.fill(
                  child: SwipableStack(
                    allowVerticalSwipe: false,
                    stackClipBehaviour: Clip.none,
                    overlayBuilder: (context, properties) {
                      final opacity = min(properties.swipeProgress, 1.0);
                      final isRight = properties.direction == SwipeDirection.right;

                      return properties.swipeProgress >= 0.15
                          ? Opacity(
                              opacity: opacity,
                              child: Stack(
                                children: [
                                  isRight
                                      ? Positioned(
                                          left: 0,
                                          top: MediaQuery.of(context).size.height / 4,
                                          child: Container(
                                            padding: const EdgeInsets.all(kDefaultScreenPaddingAllSides),
                                            decoration: BoxDecoration(
                                              color: Colors.white,
                                              border: Border.all(
                                                color: kTextInputOutline,
                                                width: 1.0,
                                              ),
                                              borderRadius: BorderRadius.circular(0.0),
                                            ),
                                            child: Text(
                                              t.recommendationGeneralImplementLabel,
                                              style: const TextStyle(
                                                fontSize: kDefaultHeaderSize,
                                                color: kSuccessColor,
                                              ),
                                            ),
                                          ),
                                        )
                                      : Positioned(
                                          right: 0,
                                          top: MediaQuery.of(context).size.height / 4,
                                          child: Container(
                                            padding: const EdgeInsets.all(kDefaultScreenPaddingAllSides),
                                            decoration: BoxDecoration(
                                              color: Colors.white,
                                              border: Border.all(
                                                color: kTextInputOutline,
                                                width: 1.0,
                                              ),
                                              borderRadius: BorderRadius.circular(0.0),
                                            ),
                                            child: Text(
                                              t.recommendationGeneralIgnoreLabel,
                                              style: const TextStyle(
                                                fontSize: kDefaultHeaderSize,
                                                color: kFailureColor,
                                              ),
                                            ),
                                          ),
                                        ),
                                ],
                              ),
                            )
                          : Container();
                    },
                    horizontalSwipeThreshold: 0.8,
                    onWillMoveNext: (index, direction) {
                      bool isLeft = direction == SwipeDirection.left;
                      bool isRight = direction == SwipeDirection.right;
                      // 根据当前index获取对应item的FormKey并验证
                      if (index >= items.length) return isLeft;
                      final formKey = _formKeys[items[index]['label']];
                      bool isValidated = formKey?.currentState?.validate() ?? false;

                      return isLeft || (isRight && isValidated);
                    },
                    onSwipeCompleted: (index, direction) async {
                      if (direction == SwipeDirection.right) {
                        print('Accepted ${items[index]['label']}');
                      } else if (direction == SwipeDirection.left) {
                        print('Declined ${items[index]['label']}');
                      }

                      setState(() {
                        // 移除item并清理对应FormKey
                        final removedItemLabel = items[index]['label'];
                        _formKeys.remove(removedItemLabel);
                        items.removeAt(index);
                      });
                    },
                    itemCount: items.length,
                    builder: (context, index) {
                      return _buildRecommendationTile(items[index]);
                    },
                  ),
                )
              : Container(),
        ],
      ),
    );
  }
}

关键说明

  • Map存储FormKey:通过item的唯一标识绑定Key,确保每个item的Key独立,不会因列表索引变化导致冲突。
  • 直接使用items数据:SwipableStack的itemCount绑定items长度,builder直接根据index生成Widget,消除旧实例残留问题。
  • 同步清理Key:移除item时删除对应FormKey,避免内存泄漏。

内容的提问来源于stack exchange,提问作者AlexanderM.

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 21:14:50