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

Flutter Web:如何为Firebase上传的每张图片显示ProgressIndicator

实现Flutter Web单图/多图上传带进度指示器(Firebase + FilePicker)

1. 依赖配置

先在pubspec.yaml中添加所需依赖:

dependencies:
  flutter:
    sdk: flutter
  firebase_core: ^2.15.0
  firebase_storage: ^11.2.6
  file_picker: ^5.5.0

执行flutter pub get后,在Firebase控制台配置Web项目,将初始化代码添加到web/index.html中。

2. 定义上传任务模型

创建类管理单个文件的上传状态:

class UploadItem {
  final XFile file;
  String fileName;
  double progress;
  bool isCompleted;
  String? error;

  UploadItem({
    required this.file,
    required this.fileName,
    this.progress = 0.0,
    this.isCompleted = false,
    this.error,
  });
}

3. 主页面完整实现

用StatefulWidget管理上传列表与状态:

import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:file_picker/file_picker.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: FirebaseOptions(
      apiKey: "YOUR_API_KEY",
      authDomain: "YOUR_AUTH_DOMAIN",
      projectId: "YOUR_PROJECT_ID",
      storageBucket: "YOUR_STORAGE_BUCKET",
      messagingSenderId: "YOUR_SENDER_ID",
      appId: "YOUR_APP_ID",
    ),
  );
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '图片上传进度示例',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const UploadPage(),
    );
  }
}

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

  @override
  State<UploadPage> createState() => _UploadPageState();
}

class _UploadPageState extends State<UploadPage> {
  List<UploadItem> _uploadItems = [];
  bool _isPicking = false;

  // 选择图片并初始化上传项
  Future<void> _pickImages() async {
    if (_isPicking) return;
    setState(() => _isPicking = true);

    final result = await FilePicker.platform.pickFiles(
      allowMultiple: true,
      type: FileType.image,
      withData: true,
    );

    if (result != null) {
      final newItems = result.files.map((file) {
        return UploadItem(
          file: file,
          fileName: file.name,
        );
      }).toList();

      setState(() => _uploadItems.addAll(newItems));
      _startUploads(newItems);
    }

    setState(() => _isPicking = false);
  }

  // 批量启动上传
  void _startUploads(List<UploadItem> items) {
    for (var item in items) {
      _uploadFile(item);
    }
  }

  // 单个文件上传逻辑
  Future<void> _uploadFile(UploadItem item) async {
    try {
      final storageRef = FirebaseStorage.instance
          .ref()
          .child('uploads/${DateTime.now().millisecondsSinceEpoch}_${item.fileName}');

      final uploadTask = storageRef.putData(
        item.file.bytes!,
        SettableMetadata(contentType: item.file.extension != null ? 'image/${item.file.extension}' : 'image/jpeg'),
      );

      // 监听上传进度
      uploadTask.snapshotEvents.listen((taskSnapshot) {
        switch (taskSnapshot.state) {
          case TaskState.running:
            final progress = taskSnapshot.bytesTransferred / taskSnapshot.totalBytes;
            setState(() {
              item.progress = progress;
            });
            break;
          case TaskState.success:
            setState(() {
              item.isCompleted = true;
            });
            // 可在此获取下载URL:final downloadUrl = await storageRef.getDownloadURL();
            break;
          case TaskState.paused:
          case TaskState.canceled:
            setState(() {
              item.error = '上传暂停或取消';
            });
            break;
          case TaskState.error:
            setState(() {
              item.error = '上传失败';
            });
            break;
        }
      });
    } catch (e) {
      setState(() {
        item.error = '上传出错: ${e.toString()}';
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('图片上传进度')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            ElevatedButton(
              onPressed: _pickImages,
              child: const Text('选择图片'),
            ),
            const SizedBox(height: 20),
            Expanded(
              child: _uploadItems.isEmpty
                  ? const Center(child: Text('暂无上传任务'))
                  : ListView.builder(
                      itemCount: _uploadItems.length,
                      itemBuilder: (context, index) {
                        final item = _uploadItems[index];
                        return Card(
                          margin: const EdgeInsets.only(bottom: 12),
                          child: Padding(
                            padding: const EdgeInsets.all(12),
                            child: Row(
                              crossAxisAlignment: CrossAxisAlignment.start,
                              children: [
                                // 图片预览
                                Container(
                                  width: 80,
                                  height: 80,
                                  decoration: BoxDecoration(
                                    borderRadius: BorderRadius.circular(8),
                                    image: DecorationImage(
                                      image: MemoryImage(item.file.bytes!),
                                      fit: BoxFit.cover,
                                    ),
                                  ),
                                ),
                                const SizedBox(width: 16),
                                Expanded(
                                  child: Column(
                                    crossAxisAlignment: CrossAxisAlignment.start,
                                    children: [
                                      Text(
                                        item.fileName,
                                        style: const TextStyle(fontWeight: FontWeight.w500),
                                        maxLines: 1,
                                        overflow: TextOverflow.ellipsis,
                                      ),
                                      const SizedBox(height: 8),
                                      // 上传中显示进度条
                                      if (!item.isCompleted && item.error == null)
                                        LinearProgressIndicator(value: item.progress),
                                      // 上传完成提示
                                      if (item.isCompleted)
                                        const Text('上传完成', style: TextStyle(color: Colors.green)),
                                      // 错误提示
                                      if (item.error != null)
                                        Text(item.error!, style: TextStyle(color: Colors.red)),
                                    ],
                                  ),
                                ),
                              ],
                            ),
                          ),
                        );
                      },
                    ),
            ),
          ],
        ),
      ),
    );
  }
}

关键细节说明

  • 本地预览:通过FilePicker的withData: true直接获取文件字节数据,用MemoryImage快速展示预览图,避免额外IO操作。
  • 实时进度更新:利用Firebase Storage的snapshotEvents监听上传快照,实时计算进度并通过setState刷新UI。
  • 状态区分:针对上传中、完成、错误三种状态分别展示进度条、完成提示、错误信息,交互逻辑清晰。
  • 文件名去重:上传路径加入时间戳,避免同名文件覆盖。

注意事项

  1. Firebase Storage规则需配置允许上传权限,测试阶段可使用临时规则:
rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
      allow read, write: if true;
    }
  }
}

上线前需修改为安全规则(如仅允许已认证用户上传)。
2. 注意Firebase Storage的单文件大小限制,可在控制台调整。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 02:15:40