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

Flutter中同时显示两个Alert Dialog时焦点异常问题求助

Flutter 多弹窗焦点异常问题

我在Flutter中尝试同时显示两个AlertDialog,打开第二个Dialog时,焦点没有落在第二个Dialog内的Widget上,反而停留在第一个已打开的Dialog的Widget上。想请教这是框架问题,还是Flutter本就不允许同时打开两个Dialog?

问题演示

问题演示图

相关代码

第一个Dialog代码

showDialog(
  context: context,
  builder: (_) => AlertDialog(
    title: Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: [
        Row(
          children: [
            Container(
              width: 16,
              height: 32,
              decoration: BoxDecoration(
                color: AppColors.colorFFBC99,
                borderRadius: BorderRadius.circular(4),
              ),
            ),
            const SizedBox(width: 16),
            Text(
              S.addaRecipe,
              style: Theme.of(context).textTheme.headlineSmall!.copyWith(
                    fontWeight: FontWeight.w600,
                    color: Colors.black,
                  ),
            ),
          ],
        ),
        SizedBox(
          width: 300,
          child: Row(
            children: [
              Text(
                S.recipeBy,
                style: Theme.of(context)
                    .textTheme
                    .labelLarge!
                    .copyWith(fontWeight: FontWeight.w500),
              ),
              const SizedBox(width: 8),
              Expanded(child: CustomMyDropDownButton(label: 'dsfsdsf')),
            ],
          ),
        )
      ],
    ),
    content: Form(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Expanded(
            child: SingleChildScrollView(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  const SizedBox(height: 16),
                  Text(
                    S.addPhotos,
                    style: Theme.of(context)
                        .textTheme
                        .labelLarge!
                        .copyWith(fontWeight: FontWeight.w500),
                  ),
                  const SizedBox(height: 8),
                  Container(
                    width: double.infinity,
                    color: Colors.white,
                    child: Wrap(
                      children: [
                        for (int i = 0; i < 10; i++)
                          Padding(
                            padding: const EdgeInsets.all(8.0),
                            child: SizedBox(
                              width: 200,
                              height: 200,
                              child: ClipRRect(
                                borderRadius: BorderRadius.circular(8),
                                child: Image.network(
                                  S.recipeNetworkImage,
                                  fit: BoxFit.cover,
                                ),
                              ),
                            ),
                          ),
                        Padding(
                          padding: const EdgeInsets.all(8.0),
                          child: InkWell(
                            onTap: () {},
                            child: Container(
                              decoration: BoxDecoration(
                                color: Colors.grey.shade100,
                                borderRadius: BorderRadius.circular(8),
                              ),
                              width: 200,
                              height: 200,
                              child: const Center(child: Icon(Icons.add, size: 32)),
                            ),
                          ),
                        ),
                      ],
                    ),
                  ),
                  const SizedBox(height: 16),
                  CustomTextField(
                    showLabel: true,
                    label: S.recipeName,
                    validator: (v) {
                      if (v!.isEmpty) return 'This field is required';
                      return null;
                    },
                  ),
                  const SizedBox(height: 16),
                  CustomTextField(
                    showLabel: true,
                    label: S.description,
                    validator: (v) {
                      if (v!.isEmpty) return 'This field is required';
                      return null;
                    },
                  ),
                  const SizedBox(height: 32),
                  Row(
                    children: [
                      Text(
                        S.nutritionalInfo,
                        style: Theme.of(context)
                            .textTheme
                            .labelLarge!
                            .copyWith(fontWeight: FontWeight.w500),
                      ),
                      IconButton(
                        onPressed: () {
                          showDialog(
                            context: context,
                            builder: (_) => AlertDialog(
                              title: const Text(S.addNutrition),
                              content: Column(
                                mainAxisSize: MainAxisSize.min,
                                children: const [
                                  CustomTextField(showLabel: true, label: S.name),
                                  CustomTextField(showLabel: true, label: S.value),
                                ],
                              ),
                              actions: [
                                CustomOutlinedButton(
                                    name: S.close, onPress: () => context.pop()),
                                CustomElevatedButton(name: S.add, onPress: () {}),
                              ],
                            ),
                          );
                        },
                        icon: const Icon(Icons.add),
                      )
                    ],
                  ),
                  const SizedBox(height: 8),
                  Container(
                    padding: const EdgeInsets.all(8),
                    color: Colors.grey.shade100,
                    child: Text("Nutrition Info...."),
                  ),
                  const SizedBox(height: 32),
                  Text(
                    S.steps,
                    style: Theme.of(context)
                        .textTheme
                        .labelLarge!
                        .copyWith(fontWeight: FontWeight.w500),
                  ),
                  const SizedBox(height: 8),
                  Container(
                    color: AppColors.colorF4F4F4,
                    height: 300,
                    width: MediaQuery.of(context).size.width * .8,
                    child: HtmlEditor(
                      controller: HtmlEditorController(),
                      htmlEditorOptions: const HtmlEditorOptions(hint: "Steps...."),
                      otherOptions: const OtherOptions(
                          height: 400, decoration: BoxDecoration(color: Colors.red)),
                    ),
                  ),
                  const SizedBox(height: 32),
                ],
              ),
            ),
          ),
          const SizedBox(height: 32),
          Stack(
            children: [
              Text(
                '',
                maxLines: 3,
                overflow: TextOverflow.ellipsis,
                style: Theme.of(context).textTheme.bodyMedium!.copyWith(color: Colors.red),
              ),
              Text(
                '',
                maxLines: 3,
                overflow: TextOverflow.ellipsis,
                style: Theme.of(context).textTheme.bodyMedium!.copyWith(color: Colors.green),
              ),
            ],
          ),
        ],
      ),
    ),
    actions: [
      CustomOutlinedButton(name: S.close, onPress: () => context.pop()),
      CustomElevatedButton(name: S.add, onPress: () {}),
    ],
  ),
);

第二个Dialog代码

showDialog(
  context: context,
  builder: (_) => AlertDialog(
    title: const Text(S.addNutrition),
    content: Column(
      mainAxisSize: MainAxisSize.min,
      children: const [
        CustomTextField(showLabel: true, label: S.name),
        CustomTextField(showLabel: true, label: S.value),
      ],
    ),
    actions: [
      CustomOutlinedButton(name: S.close, onPress: () => context.pop()),
      CustomElevatedButton(name: S.add, onPress: () {}),
    ],
  ),
);

CustomTextField代码

import 'package:flutter/material.dart';

class CustomTextField extends StatefulWidget {
  final String? label;
  final TextEditingController? controller;
  final String? Function(String?)? validator;
  final Function(String?)? onSaved;
  final bool showLabel;
  final bool enableSuggestions;
  final bool autocorrect;
  final TextInputType? keyBoardType;
  final Function(String)? onChange;
  final bool showSuffixIcon;

  const CustomTextField({
    Key? key,
    this.label,
    this.controller,
    this.validator,
    this.onSaved,
    this.enableSuggestions = true,
    this.autocorrect = true,
    this.keyBoardType,
    this.onChange,
    this.showSuffixIcon = false,
    this.showLabel = false,
  }) : super(key: key);

  @override
  State<CustomTextField> createState() => _CustomTextFieldState();
}

class _CustomTextFieldState extends State<CustomTextField> {
  final textFieldFocusNode = FocusNode();

  bool _obscured = false;

  @override
  void initState() {
    if (widget.showSuffixIcon) {
      _obscured = true;
    }
    super.initState();
  }

  void _toggleObscured() {
    setState(() {
      _obscured = !_obscured;
      if (textFieldFocusNode.hasPrimaryFocus) return;
      textFieldFocusNode.canRequestFocus = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        widget.showLabel
            ? Text(
                widget.label!,
                style: Theme.of(context)
                    .textTheme
                    .labelLarge!
                    .copyWith(fontWeight: FontWeight.w500),
              )
            : const SizedBox(),
        SizedBox(height: widget.showLabel ? 8 : 0),
        TextFormField(
          controller: widget.controller,
          validator: widget.validator,
          onSaved: widget.onSaved,
          onChanged: widget.onChange,
          focusNode: textFieldFocusNode,
          obscureText: _obscured,
          enableSuggestions: widget.enableSuggestions,
          autocorrect: widget.autocorrect,
          keyboardType: widget.keyBoardType,
          decoration: InputDecoration(
            isDense: true,
            enabledBorder: const OutlineInputBorder(
              borderRadius: BorderRadius.all(Radius.circular(8)),
              borderSide: BorderSide(color: Colors.black12),
            ),
            focusedBorder: const OutlineInputBorder(
              borderRadius: BorderRadius.all(Radius.circular(8)),
              borderSide: BorderSide(color: Colors.black12),
            ),
            errorBorder: const OutlineInputBorder(
              borderRadius: BorderRadius.all(Radius.circular(8)),
              borderSide: BorderSide(color: Colors.red),
            ),
            border: const OutlineInputBorder(
              borderRadius: BorderRadius.all(Radius.circular(8)),
              borderSide: BorderSide(color: Colors.black12),
            ),
            labelText: widget.showLabel ? null : widget.label,
            suffixIcon: widget.showSuffixIcon
                ? Padding(
                    padding: const EdgeInsets.fromLTRB(0, 0, 4, 0),
                    child: GestureDetector(
                      onTap: _toggleObscured,
                      child: Icon(
                        _obscured ? Icons.visibility_rounded : Icons.visibility_off_rounded,
                        size: 24,
                        color: Colors.grey,
                      ),
                    ),
                  )
                : null,
          ),
        ),
      ],
    );
  }
}

问题解答

Flutter 默认允许叠加显示多个Dialog,但焦点管理逻辑是焦点会保留在最先获取焦点的元素上,除非手动处理焦点转移。你遇到的问题并非框架Bug,而是未对第二个Dialog的焦点进行主动控制导致的。

解决方法

1. 给第二个Dialog的输入框添加自动聚焦

修改CustomTextField,新增autofocus参数并传递给内部的TextFormField:

// CustomTextField参数新增
final bool autofocus;

const CustomTextField({
  // ...其他参数
  this.autofocus = false,
}) : super(key: key);

// TextFormField中添加
TextFormField(
  // ...其他属性
  autofocus: widget.autofocus,
)

然后在第二个Dialog中给第一个输入框设置autofocus: true:

CustomTextField(
  showLabel: true,
  label: S.name,
  autofocus: true,
),

2. 打开第二个Dialog前清除旧焦点

在触发第二个Dialog的按钮点击事件中,先清除当前焦点:

IconButton(
  onPressed: () {
    FocusManager.instance.primaryFocus?.unfocus();
    showDialog(/* 第二个Dialog代码 */);
  },
  icon: const Icon(Icons.add),
)

3. 手动请求焦点

在CustomTextField的initState中延迟请求焦点:

@override
void initState() {
  super.initState();
  if (widget.showSuffixIcon) {
    _obscured = true;
  }
  WidgetsBinding.instance.addPostFrameCallback((_) {
    FocusScope.of(context).requestFocus(textFieldFocusNode);
  });
}

额外说明

虽然Flutter支持多Dialog叠加,但从用户体验角度,不建议同时展示多个弹窗,易造成界面混乱。更优方案是改用底部Sheet、侧边栏,或把子弹窗的功能整合到父弹窗的内容区域中。


内容的提问来源于stack exchange,提问作者Konark Shivam

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 17:55:11