Flutter 小白入门教程Flutter 小白入门教程
首页
学习指南
项目实战
Flutter 官网
编程指南
首页
学习指南
项目实战
Flutter 官网
编程指南
  • 入门基础

    • 📚 基础教程
    • 第1章 - 认识 Flutter
    • 第2章 - 环境搭建
    • 第3章 - Dart 语言基础
    • 第4章 - 第一个 Flutter 应用
    • 第5章 - Widget 基础
    • 第6章 - 布局系统
    • 第7章 - 状态管理入门
    • 第8章 - 页面导航
    • 第9章 - 资源管理
  • 进阶开发

    • 第10章 - 网络请求
    • 第11章 - 本地存储
    • 第12章 - 对话框与反馈
    • 第13章 - 列表进阶
    • 第14章 - 主题定制
    • 第15章 - 状态管理进阶
    • 第16章 - 动画入门
    • 第17章 - 常用第三方包
  • 调试与发布

    • 第18章 - 调试与性能优化
    • 第19章 - 打包与发布
  • 附录

    • 附录A - UI 框架与组件库推荐
    • 附录B - 项目结构最佳实践
    • 附录C - 国际化配置
    • 附录D - 权限处理

第12章 - 对话框与反馈

嗨,朋友!我是长安。

好的用户体验离不开及时的反馈。用户点击按钮后,要让他知道发生了什么;操作成功了,要给个提示;需要确认了,要弹个对话框。这一章,我们来学习 Flutter 中的对话框和反馈机制。

📢 SnackBar - 底部提示

SnackBar 是一个轻量级的提示组件,从屏幕底部滑出,适合显示简短的提示信息。

基本用法

ScaffoldMessenger.of(context).showSnackBar(
  const SnackBar(
    content: Text('保存成功!'),
  ),
);

带操作按钮

ScaffoldMessenger.of(context).showSnackBar(
  SnackBar(
    content: const Text('已删除 1 条记录'),
    action: SnackBarAction(
      label: '撤销',
      onPressed: () {
        // 撤销删除
      },
    ),
  ),
);

自定义样式

ScaffoldMessenger.of(context).showSnackBar(
  SnackBar(
    content: const Row(
      children: [
        Icon(Icons.check_circle, color: Colors.white),
        SizedBox(width: 10),
        Text('操作成功'),
      ],
    ),
    backgroundColor: Colors.green,
    behavior: SnackBarBehavior.floating,  // 浮动样式
    margin: const EdgeInsets.all(16),
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(10),
    ),
    duration: const Duration(seconds: 2),
  ),
);

封装 SnackBar 工具类

class SnackBarUtil {
  static void show(BuildContext context, String message) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(message),
        behavior: SnackBarBehavior.floating,
        margin: const EdgeInsets.all(16),
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(8),
        ),
      ),
    );
  }

  static void showSuccess(BuildContext context, String message) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Row(
          children: [
            const Icon(Icons.check_circle, color: Colors.white),
            const SizedBox(width: 10),
            Text(message),
          ],
        ),
        backgroundColor: Colors.green,
        behavior: SnackBarBehavior.floating,
        margin: const EdgeInsets.all(16),
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(8),
        ),
      ),
    );
  }

  static void showError(BuildContext context, String message) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Row(
          children: [
            const Icon(Icons.error, color: Colors.white),
            const SizedBox(width: 10),
            Text(message),
          ],
        ),
        backgroundColor: Colors.red,
        behavior: SnackBarBehavior.floating,
        margin: const EdgeInsets.all(16),
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(8),
        ),
      ),
    );
  }
}

// 使用
SnackBarUtil.showSuccess(context, '保存成功');
SnackBarUtil.showError(context, '网络连接失败');

💬 AlertDialog - 警告对话框

AlertDialog 用于显示重要提示或确认操作。

基本用法

showDialog(
  context: context,
  builder: (context) => AlertDialog(
    title: const Text('提示'),
    content: const Text('确定要退出登录吗?'),
    actions: [
      TextButton(
        onPressed: () => Navigator.pop(context),
        child: const Text('取消'),
      ),
      TextButton(
        onPressed: () {
          Navigator.pop(context);
          // 执行退出登录
        },
        child: const Text('确定'),
      ),
    ],
  ),
);

获取对话框返回值

Future<void> _showDeleteConfirm() async {
  final result = await showDialog<bool>(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('确认删除'),
      content: const Text('删除后无法恢复,确定要删除吗?'),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context, false),
          child: const Text('取消'),
        ),
        TextButton(
          onPressed: () => Navigator.pop(context, true),
          style: TextButton.styleFrom(foregroundColor: Colors.red),
          child: const Text('删除'),
        ),
      ],
    ),
  );

  if (result == true) {
    // 执行删除操作
    print('用户确认删除');
  }
}

自定义对话框内容

showDialog(
  context: context,
  builder: (context) => AlertDialog(
    title: const Text('选择头像'),
    content: SizedBox(
      width: double.maxFinite,
      child: GridView.count(
        shrinkWrap: true,
        crossAxisCount: 4,
        children: List.generate(8, (index) {
          return GestureDetector(
            onTap: () {
              Navigator.pop(context, index);
            },
            child: CircleAvatar(
              backgroundImage: NetworkImage(
                'https://i.pravatar.cc/100?img=$index',
              ),
            ),
          );
        }),
      ),
    ),
  ),
);

禁止点击外部关闭

showDialog(
  context: context,
  barrierDismissible: false,  // 点击外部不关闭
  builder: (context) => AlertDialog(
    title: const Text('重要提示'),
    content: const Text('请阅读并同意用户协议'),
    actions: [
      TextButton(
        onPressed: () => Navigator.pop(context),
        child: const Text('我已阅读并同意'),
      ),
    ],
  ),
);

📋 SimpleDialog - 简单选择对话框

SimpleDialog 适合展示选项列表。

Future<void> _showLanguageDialog() async {
  final result = await showDialog<String>(
    context: context,
    builder: (context) => SimpleDialog(
      title: const Text('选择语言'),
      children: [
        SimpleDialogOption(
          onPressed: () => Navigator.pop(context, '中文'),
          child: const Text('中文'),
        ),
        SimpleDialogOption(
          onPressed: () => Navigator.pop(context, 'English'),
          child: const Text('English'),
        ),
        SimpleDialogOption(
          onPressed: () => Navigator.pop(context, '日本語'),
          child: const Text('日本語'),
        ),
      ],
    ),
  );

  if (result != null) {
    print('选择了: $result');
  }
}

📝 输入对话框

带有输入框的对话框:

Future<void> _showInputDialog() async {
  final controller = TextEditingController();
  
  final result = await showDialog<String>(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('添加备注'),
      content: TextField(
        controller: controller,
        autofocus: true,
        decoration: const InputDecoration(
          hintText: '请输入备注内容',
          border: OutlineInputBorder(),
        ),
        maxLines: 3,
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('取消'),
        ),
        TextButton(
          onPressed: () => Navigator.pop(context, controller.text),
          child: const Text('保存'),
        ),
      ],
    ),
  );

  if (result != null && result.isNotEmpty) {
    print('备注内容: $result');
  }
}

📅 日期时间选择器

日期选择器

Future<void> _selectDate() async {
  final DateTime? picked = await showDatePicker(
    context: context,
    initialDate: DateTime.now(),
    firstDate: DateTime(2020),
    lastDate: DateTime(2030),
    locale: const Locale('zh'),  // 需要配置本地化
    helpText: '选择日期',
    cancelText: '取消',
    confirmText: '确定',
  );

  if (picked != null) {
    print('选择的日期: ${picked.year}-${picked.month}-${picked.day}');
  }
}

时间选择器

Future<void> _selectTime() async {
  final TimeOfDay? picked = await showTimePicker(
    context: context,
    initialTime: TimeOfDay.now(),
    helpText: '选择时间',
    cancelText: '取消',
    confirmText: '确定',
  );

  if (picked != null) {
    print('选择的时间: ${picked.hour}:${picked.minute}');
  }
}

日期范围选择器

Future<void> _selectDateRange() async {
  final DateTimeRange? picked = await showDateRangePicker(
    context: context,
    firstDate: DateTime(2020),
    lastDate: DateTime(2030),
    initialDateRange: DateTimeRange(
      start: DateTime.now(),
      end: DateTime.now().add(const Duration(days: 7)),
    ),
  );

  if (picked != null) {
    print('开始: ${picked.start}, 结束: ${picked.end}');
  }
}

📄 BottomSheet - 底部弹出面板

模态底部面板

void _showBottomSheet() {
  showModalBottomSheet(
    context: context,
    builder: (context) => Container(
      padding: const EdgeInsets.all(20),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          ListTile(
            leading: const Icon(Icons.camera_alt),
            title: const Text('拍照'),
            onTap: () {
              Navigator.pop(context);
              // 打开相机
            },
          ),
          ListTile(
            leading: const Icon(Icons.photo_library),
            title: const Text('从相册选择'),
            onTap: () {
              Navigator.pop(context);
              // 打开相册
            },
          ),
          ListTile(
            leading: const Icon(Icons.cancel),
            title: const Text('取消'),
            onTap: () => Navigator.pop(context),
          ),
        ],
      ),
    ),
  );
}

可滚动的底部面板

void _showScrollableBottomSheet() {
  showModalBottomSheet(
    context: context,
    isScrollControlled: true,  // 允许全屏
    shape: const RoundedRectangleBorder(
      borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
    ),
    builder: (context) => DraggableScrollableSheet(
      initialChildSize: 0.5,
      minChildSize: 0.25,
      maxChildSize: 0.9,
      expand: false,
      builder: (context, scrollController) {
        return Container(
          padding: const EdgeInsets.all(16),
          child: ListView.builder(
            controller: scrollController,
            itemCount: 50,
            itemBuilder: (context, index) {
              return ListTile(
                title: Text('选项 $index'),
                onTap: () => Navigator.pop(context, index),
              );
            },
          ),
        );
      },
    ),
  );
}

圆角底部面板

showModalBottomSheet(
  context: context,
  shape: const RoundedRectangleBorder(
    borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
  ),
  builder: (context) => Container(
    padding: const EdgeInsets.all(20),
    child: Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        // 拖动指示器
        Container(
          width: 40,
          height: 4,
          margin: const EdgeInsets.only(bottom: 20),
          decoration: BoxDecoration(
            color: Colors.grey[300],
            borderRadius: BorderRadius.circular(2),
          ),
        ),
        // 内容
        const Text('底部面板内容'),
      ],
    ),
  ),
);

⏳ Loading 加载状态

简单加载对话框

void _showLoading() {
  showDialog(
    context: context,
    barrierDismissible: false,
    builder: (context) => const Center(
      child: CircularProgressIndicator(),
    ),
  );
}

// 关闭加载
Navigator.pop(context);

带文字的加载对话框

void _showLoadingWithText() {
  showDialog(
    context: context,
    barrierDismissible: false,
    builder: (context) => AlertDialog(
      content: Row(
        children: const [
          CircularProgressIndicator(),
          SizedBox(width: 20),
          Text('加载中...'),
        ],
      ),
    ),
  );
}

封装 Loading 工具类

class LoadingUtil {
  static bool _isShowing = false;

  static void show(BuildContext context, {String? message}) {
    if (_isShowing) return;
    _isShowing = true;

    showDialog(
      context: context,
      barrierDismissible: false,
      builder: (context) => WillPopScope(
        onWillPop: () async => false,  // 禁止返回键关闭
        child: Center(
          child: Container(
            padding: const EdgeInsets.all(20),
            decoration: BoxDecoration(
              color: Colors.white,
              borderRadius: BorderRadius.circular(10),
            ),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                const CircularProgressIndicator(),
                if (message != null) ...[
                  const SizedBox(height: 16),
                  Text(message),
                ],
              ],
            ),
          ),
        ),
      ),
    );
  }

  static void hide(BuildContext context) {
    if (!_isShowing) return;
    _isShowing = false;
    Navigator.pop(context);
  }
}

// 使用示例
Future<void> _submitForm() async {
  LoadingUtil.show(context, message: '提交中...');
  
  try {
    await Future.delayed(const Duration(seconds: 2));  // 模拟网络请求
    LoadingUtil.hide(context);
    SnackBarUtil.showSuccess(context, '提交成功');
  } catch (e) {
    LoadingUtil.hide(context);
    SnackBarUtil.showError(context, '提交失败');
  }
}

🔔 Toast 提示(使用第三方库)

fluttertoast 提供类似原生的 Toast 提示:

dependencies:
  fluttertoast: ^8.2.4
import 'package:fluttertoast/fluttertoast.dart';

Fluttertoast.showToast(
  msg: "这是一条 Toast 消息",
  toastLength: Toast.LENGTH_SHORT,
  gravity: ToastGravity.BOTTOM,
  backgroundColor: Colors.black87,
  textColor: Colors.white,
  fontSize: 16.0,
);

🎯 实战:完整的反馈示例

class FeedbackDemoPage extends StatefulWidget {
  const FeedbackDemoPage({super.key});

  @override
  State<FeedbackDemoPage> createState() => _FeedbackDemoPageState();
}

class _FeedbackDemoPageState extends State<FeedbackDemoPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('反馈示例')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          ElevatedButton(
            onPressed: () => _showSnackBar(),
            child: const Text('显示 SnackBar'),
          ),
          const SizedBox(height: 10),
          ElevatedButton(
            onPressed: () => _showAlertDialog(),
            child: const Text('显示确认对话框'),
          ),
          const SizedBox(height: 10),
          ElevatedButton(
            onPressed: () => _showInputDialog(),
            child: const Text('显示输入对话框'),
          ),
          const SizedBox(height: 10),
          ElevatedButton(
            onPressed: () => _showBottomSheet(),
            child: const Text('显示底部面板'),
          ),
          const SizedBox(height: 10),
          ElevatedButton(
            onPressed: () => _showDatePicker(),
            child: const Text('选择日期'),
          ),
          const SizedBox(height: 10),
          ElevatedButton(
            onPressed: () => _simulateSubmit(),
            child: const Text('模拟提交(带 Loading)'),
          ),
        ],
      ),
    );
  }

  void _showSnackBar() {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: const Text('这是一条提示消息'),
        action: SnackBarAction(
          label: '撤销',
          onPressed: () {},
        ),
        behavior: SnackBarBehavior.floating,
      ),
    );
  }

  Future<void> _showAlertDialog() async {
    final result = await showDialog<bool>(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('确认删除'),
        content: const Text('确定要删除这条数据吗?'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context, false),
            child: const Text('取消'),
          ),
          TextButton(
            onPressed: () => Navigator.pop(context, true),
            style: TextButton.styleFrom(foregroundColor: Colors.red),
            child: const Text('删除'),
          ),
        ],
      ),
    );

    if (result == true && mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('已删除')),
      );
    }
  }

  Future<void> _showInputDialog() async {
    final controller = TextEditingController();
    final result = await showDialog<String>(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('输入备注'),
        content: TextField(
          controller: controller,
          autofocus: true,
          decoration: const InputDecoration(
            hintText: '请输入内容',
          ),
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('取消'),
          ),
          TextButton(
            onPressed: () => Navigator.pop(context, controller.text),
            child: const Text('确定'),
          ),
        ],
      ),
    );

    if (result != null && result.isNotEmpty && mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('输入内容: $result')),
      );
    }
  }

  void _showBottomSheet() {
    showModalBottomSheet(
      context: context,
      shape: const RoundedRectangleBorder(
        borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
      ),
      builder: (context) => Container(
        padding: const EdgeInsets.all(20),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Container(
              width: 40,
              height: 4,
              decoration: BoxDecoration(
                color: Colors.grey[300],
                borderRadius: BorderRadius.circular(2),
              ),
            ),
            const SizedBox(height: 20),
            ListTile(
              leading: const Icon(Icons.share),
              title: const Text('分享'),
              onTap: () => Navigator.pop(context),
            ),
            ListTile(
              leading: const Icon(Icons.link),
              title: const Text('复制链接'),
              onTap: () => Navigator.pop(context),
            ),
            ListTile(
              leading: const Icon(Icons.report),
              title: const Text('举报'),
              onTap: () => Navigator.pop(context),
            ),
          ],
        ),
      ),
    );
  }

  Future<void> _showDatePicker() async {
    final date = await showDatePicker(
      context: context,
      initialDate: DateTime.now(),
      firstDate: DateTime(2020),
      lastDate: DateTime(2030),
    );

    if (date != null && mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('选择了: ${date.year}-${date.month}-${date.day}')),
      );
    }
  }

  Future<void> _simulateSubmit() async {
    showDialog(
      context: context,
      barrierDismissible: false,
      builder: (context) => const Center(
        child: Card(
          child: Padding(
            padding: EdgeInsets.all(20),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                CircularProgressIndicator(),
                SizedBox(height: 16),
                Text('提交中...'),
              ],
            ),
          ),
        ),
      ),
    );

    await Future.delayed(const Duration(seconds: 2));

    if (mounted) {
      Navigator.pop(context);
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Row(
            children: [
              Icon(Icons.check, color: Colors.white),
              SizedBox(width: 10),
              Text('提交成功!'),
            ],
          ),
          backgroundColor: Colors.green,
        ),
      );
    }
  }
}

📝 小结

这一章我们学习了:

  • ✅ SnackBar - 底部提示消息
  • ✅ AlertDialog - 警告/确认对话框
  • ✅ SimpleDialog - 简单选择对话框
  • ✅ 日期时间选择器
  • ✅ BottomSheet - 底部弹出面板
  • ✅ Loading 加载状态
  • ✅ 封装工具类提高开发效率

用户反馈最佳实践

  1. 操作后给反馈 - 让用户知道操作结果
  2. 重要操作需确认 - 删除等不可逆操作要二次确认
  3. 加载状态要显示 - 网络请求时显示加载中
  4. 错误信息要友好 - 不要显示技术性错误信息

💪 练习题

  1. 创建一个删除确认对话框,带「再也不提示」选项
  2. 实现一个图片选择底部面板(拍照/相册/取消)
  3. 封装一个通用的 Loading 工具类

🚀 下一步

掌握了对话框与反馈后,下一章我们来学习 列表进阶,实现下拉刷新和无限滚动!


由 编程指南 提供

最近更新: 2026/2/3 16:24
Contributors: 王长安
Prev
第11章 - 本地存储
Next
第13章 - 列表进阶