Flutter Web集成Firebase后有效URL图片无法加载
我使用Android Studio基于Firebase开发本科毕业设计(FYP)的论坛应用,通过file_picker选择并上传图片至Firebase Storage,获取的图片URL有效,但Web端无法加载图片。此前使用命令flutter run -d chrome --web-renderer html可正常显示,但Flutter不再支持该参数后问题复现。
论坛页面代码
import 'package:flutter/material.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_database/firebase_database.dart'; import 'package:firebase_storage/firebase_storage.dart'; import 'package:file_picker/file_picker.dart'; import 'post_detail_page.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:flutter/foundation.dart' show kIsWeb; import 'dart:io'; Future<void> requestPermissions() async { if (kIsWeb) { print("Web does not require storage permissions."); return; // Skip permission request on web } if (Platform.isAndroid) { var status = await Permission.storage.request(); if (status.isGranted) { print("Storage permission granted"); } else { print("Storage permission denied"); } } } class ForumPage extends StatefulWidget { const ForumPage({super.key}); @override _ForumPageState createState() => _ForumPageState(); } class _ForumPageState extends State<ForumPage> { void initState() { super.initState(); requestPermissions(); // Request permission when the page initializes } final FirebaseAuth _auth = FirebaseAuth.instance; final DatabaseReference _dbRef = FirebaseDatabase.instance.ref(); final TextEditingController _titleController = TextEditingController(); final TextEditingController _contentController = TextEditingController(); File? _file; String? _fileUrl; // Declare this variable to hold the file URL // Pick a file (image or other types) from the device Future<void> _pickFile() async { try { final result = await FilePicker.platform.pickFiles( type: FileType.image, withData: kIsWeb, // Use withData for Web ); if (result == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('No file selected.')), ); return; } final file = result.files.single; if (kIsWeb && file.bytes != null) { // Handle file upload for Web (use bytes directly) final fileBytes = file.bytes!; final storageRef = FirebaseStorage.instance.ref().child('forum_files/${DateTime.now().millisecondsSinceEpoch}.jpg'); final uploadTask = storageRef.putData(fileBytes); final snapshot = await uploadTask.whenComplete(() {}); final fileUrl = await snapshot.ref.getDownloadURL(); print('File uploaded successfully. URL: $fileUrl'); setState(() { _fileUrl = fileUrl; }); } else if (!kIsWeb && file.path != null) { // Handle file upload for Android/iOS final fileRef = FirebaseStorage.instance.ref().child('forum_files/${DateTime.now().millisecondsSinceEpoch}.jpg'); final uploadTask = fileRef.putFile(File(file.path!)); final snapshot = await uploadTask.whenComplete(() {}); final fileUrl = await snapshot.ref.getDownloadURL(); print('File uploaded successfully. URL: $fileUrl'); setState(() { _fileUrl = fileUrl; }); } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Failed to access file data.')), ); } } catch (e) { print('Error picking file: $e'); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Failed to pick file: $e')), ); } } // Upload file to Firebase Storage Future<String?> _uploadFile(File file) async { try { final fileRef = FirebaseStorage.instance.ref().child('forum_files/${DateTime.now().millisecondsSinceEpoch}.jpg'); final uploadTask = fileRef.putFile(file); final snapshot = await uploadTask.whenComplete(() {}); final fileUrl = await snapshot.ref.getDownloadURL(); return fileUrl; } catch (e) { print('Error uploading file: $e'); return null; } } // Add a new post with a file to Firebase Realtime Database Future<void> createPost(String title, String content) async { final user = _auth.currentUser; if (user == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('You must be signed in to create a post.')), ); return; } // Fetch the user's profile name from Realtime Database final databaseRef = FirebaseDatabase.instance.ref( 'users/${user.uid}/profile'); String profileName = 'Anonymous'; try { final snapshot = await databaseRef.child('name').get(); if (snapshot.exists) { profileName = snapshot.value as String; } } catch (e) { print('Error fetching profile name: $e'); } // Construct the post data final postData = { 'title': title, 'content': content, 'author': {'id': user.uid, 'name': profileName}, 'timestamp': DateTime .now() .millisecondsSinceEpoch, // Use local timestamp }; // Add the file URL to the post data if it's not null if (_fileUrl != null) { postData['fileUrl'] = _fileUrl!; // Use the '!' operator to assert that it's not null } try { final postRef = FirebaseDatabase.instance.ref('forum/posts').push(); await postRef.set(postData); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Post created successfully!')), ); _titleController.clear(); _contentController.clear(); setState(() { _file = null; // Clear the file after posting _fileUrl = null; // Clear the file URL }); } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Failed to create post: $e')), ); } } // Build the UI for creating a new post Widget _buildCreatePostSection() { return Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Create a Post', style: TextStyle(fontSize: 20)), const SizedBox(height: 10), TextField( controller: _titleController, decoration: const InputDecoration(labelText: 'Title'), ), const SizedBox(height: 10), TextField( controller: _contentController, decoration: const InputDecoration(labelText: 'Content'), maxLines: 5, ), const SizedBox(height: 10), ElevatedButton( onPressed: _pickFile, child: const Text('Select File'), ), if (_file != null) Padding( padding: const EdgeInsets.only(top: 8.0), child: Image.file(_file!, height: 100, width: 100, fit: BoxFit.cover), ), const SizedBox(height: 10), ElevatedButton( onPressed: () { final title = _titleController.text.trim(); final content = _contentController.text.trim(); if (title.isNotEmpty && content.isNotEmpty) { createPost(title, content); } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Title and content cannot be empty.')), ); } }, child: const Text('Post'), ), ], ), ); } // Build the list of posts Widget _buildPostList() { return StreamBuilder<DatabaseEvent>( stream: _dbRef.child('forum/posts').orderByChild('timestamp').onValue, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } if (!snapshot.hasData || snapshot.data?.snapshot.value == null) { return const Center(child: Text('No posts yet.')); } final postsData = (snapshot.data!.snapshot.value as Map).entries.toList(); postsData.sort((a, b) => (b.value['timestamp'] as int).compareTo(a.value['timestamp'] as int)); return ListView.builder( itemCount: postsData.length, itemBuilder: (context, index) { final post = postsData[index].value as Map; final title = post['title'] ?? 'No Title'; final content = post['content'] ?? 'No Content'; final author = post['author']?['name'] ?? 'Anonymous'; final fileUrl = post['fileUrl'] as String?; return GestureDetector( onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => PostDetailPage(post: post), // Navigate to detail page ), ); }, child: Card( margin: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 8), Text(content), const SizedBox(height: 8), if (fileUrl != null) Image.network(fileUrl, width: double.infinity, height: 200, // Set a limit fit: BoxFit.contain,), const SizedBox(height: 8), Text('By: $author', style: const TextStyle(fontSize: 12, color: Colors.grey)), ], ), ), ), ); }, ); }, ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Forum')), body: Column( children: [ _buildCreatePostSection(), Expanded(child: _buildPostList()), ], ), ); } }
测试结果截图

错误信息
The following NetworkImageLoadException was thrown resolving an image stream completer:
HTTP request failed, statusCode: 0, https://firebasestorage.googleapis.com/v0/b/traveler-teaming-up-app.appspot.com/o/forum_files%2F1737360405321.jpg?alt=media&token=91d8d438-5f60-425a-8a48-ee12f79a37fc
Image provider: NetworkImage("https://firebasestorage.googleapis.com/v0/b/traveler-teaming-up-app.appspot.com/o/forum_files%2F1737360405321.jpg?alt=media&token=91d8d438-5f60-425a-8a48-ee12f79a37fc", scale: 1.0)
Image key: NetworkImage("https://firebasestorage.googleapis.com/v0/b/traveler-teaming-up-app.appspot.com/o/forum_files%2F1737360405321.jpg?alt=media&token=91d8d438-5f60-425a-8a48-ee12f79a37fc", scale: 1.0)
如需完整代码可告知。
内容的提问来源于stack exchange,提问作者hugoleung

