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

基于Flutter BLoC实现未保存表单跳转时的弹窗确认功能

Flutter BLoC模式下表单页面未保存时的离开确认实现

问题描述

我正在用BLoC模式开发Flutter应用,其中一个页面包含表单。需要实现:当用户未保存表单就尝试跳转到其他页面时,弹出确认弹窗,提供保存更改的选项。目前已有检查表单保存状态的基础架构,下面是维护保存状态的Cubit代码:

class AppbarSaveActionCubit extends Cubit<AppbarSaveActionState> 
{
AppbarSaveActionCubit() : super(AppbarSaveActionState.initial());

// 从底部栏触发子组件保存操作
void saveAction(){
final newState = state.copyWith(saveAction: true);
emit(newState);
// 重置Cubit状态
emit(AppbarSaveActionState.initial());
}

// 初始化保存状态
void saveActionInitState(){
final newState = state.copyWith(saveAction: false);
emit(newState);
}

// 从底部栏触发子组件返回操作
void backPageAction(){
final newState = state.copyWith(backAction: true);
emit(newState);
// 重置Cubit状态
emit(AppbarSaveActionState.initial());
 }
}

实现方案

1. 完善状态与Cubit逻辑

首先需要在状态中添加未保存修改标记,用来追踪表单是否有未提交的更改,同时调整Cubit方法,避免状态被立即重置导致监听失效:

修改状态类

class AppbarSaveActionState {
  final bool hasUnsavedChanges; // 新增:标记是否有未保存修改
  final bool saveAction;
  final bool backAction;

  AppbarSaveActionState({
    required this.hasUnsavedChanges,
    required this.saveAction,
    required this.backAction,
  });

  // 初始状态:无未保存修改,无动作触发
  static AppbarSaveActionState initial() {
    return AppbarSaveActionState(
      hasUnsavedChanges: false,
      saveAction: false,
      backAction: false,
    );
  }

  AppbarSaveActionState copyWith({
    bool? hasUnsavedChanges,
    bool? saveAction,
    bool? backAction,
  }) {
    return AppbarSaveActionState(
      hasUnsavedChanges: hasUnsavedChanges ?? this.hasUnsavedChanges,
      saveAction: saveAction ?? this.saveAction,
      backAction: backAction ?? this.backAction,
    );
  }
}

更新Cubit方法

class AppbarSaveActionCubit extends Cubit<AppbarSaveActionState> {
  AppbarSaveActionCubit() : super(AppbarSaveActionState.initial());

  // 标记表单有未保存修改(表单输入时调用)
  void markAsDirty() {
    emit(state.copyWith(hasUnsavedChanges: true));
  }

  // 标记表单已保存(保存成功后调用)
  void markAsSaved() {
    emit(state.copyWith(hasUnsavedChanges: false));
  }

  // 触发保存操作
  void triggerSaveAction() {
    emit(state.copyWith(saveAction: true));
    // 保存完成后调用markAsSaved重置状态
  }

  // 触发返回操作
  void triggerBackAction() {
    emit(state.copyWith(backAction: true));
  }

  // 重置动作状态
  void resetActionState() {
    emit(state.copyWith(saveAction: false, backAction: false));
  }
}

2. 拦截页面离开操作

针对两种离开场景分别处理:系统返回键、自定义导航按钮。

处理系统返回键(WillPopScope)

在表单页面的外层包裹WillPopScope,拦截系统返回事件:

class FormPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocConsumer<AppbarSaveActionCubit, AppbarSaveActionState>(
      listener: (context, state) {
        // 监听保存动作,执行实际保存逻辑
        if (state.saveAction) {
          _performSave(context);
          context.read<AppbarSaveActionCubit>().resetActionState();
        }
        // 监听返回动作,执行导航
        if (state.backAction) {
          Navigator.of(context).pop();
          context.read<AppbarSaveActionCubit>().resetActionState();
        }
      },
      builder: (context, state) {
        return WillPopScope(
          onWillPop: () async {
            // 检查是否有未保存修改
            if (state.hasUnsavedChanges) {
              return _showLeaveConfirmationDialog(context);
            }
            return true; // 允许直接返回
          },
          child: Scaffold(
            appBar: AppBar(
              leading: IconButton(
                icon: Icon(Icons.arrow_back),
                onPressed: () => _handleCustomBack(context),
              ),
            ),
            body: _buildForm(context),
          ),
        );
      },
    );
  }

  // 构建表单,输入时调用markAsDirty
  Widget _buildForm(BuildContext context) {
    return TextField(
      onChanged: (value) {
        context.read<AppbarSaveActionCubit>().markAsDirty();
      },
      // 其他表单字段...
    );
  }

  // 处理自定义返回按钮
  void _handleCustomBack(BuildContext context) {
    final cubit = context.read<AppbarSaveActionCubit>();
    if (cubit.state.hasUnsavedChanges) {
      _showLeaveConfirmationDialog(context).then((shouldLeave) {
        if (shouldLeave) {
          cubit.triggerBackAction();
        }
      });
    } else {
      cubit.triggerBackAction();
    }
  }

  // 执行表单保存逻辑
  void _performSave(BuildContext context) {
    // 这里写实际的表单保存代码(比如提交API、数据库存储)
    // 保存成功后标记为已保存
    context.read<AppbarSaveActionCubit>().markAsSaved();
  }

  // 显示离开确认弹窗
  Future<bool> _showLeaveConfirmationDialog(BuildContext context) async {
    final result = await showDialog<bool>(
      context: context,
      builder: (context) => AlertDialog(
        title: Text('确认离开?'),
        content: Text('您有未保存的更改,是否保存后再离开?'),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(context).pop(false), // 取消,不离开
            child: Text('取消'),
          ),
          TextButton(
            onPressed: () {
              // 保存并离开
              context.read<AppbarSaveActionCubit>().triggerSaveAction();
              Navigator.of(context).pop(true);
            },
            child: Text('保存并离开'),
          ),
          TextButton(
            onPressed: () => Navigator.of(context).pop(true), // 不保存直接离开
            child: Text('不保存离开'),
          ),
        ],
      ),
    );
    return result ?? false;
  }
}

3. 关键逻辑说明

  • 状态追踪:通过hasUnsavedChanges字段实时记录表单修改状态,用户输入时调用markAsDirty(),保存成功后调用markAsSaved()。
  • 导航拦截:WillPopScope处理系统返回键,自定义按钮则在点击时先检查状态。
  • 弹窗交互:弹窗提供三个选项,根据用户选择执行保存后离开、直接离开或取消操作,确保用户确认后再执行导航。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 08:35:37