Lune Logo

© 2025 Lune Inc.
All rights reserved.

support@lune.dev

Want to use over 200+ MCP servers inside your coding tools like Cursor?

Asked 1 month ago by EclipseCollector295

Why does Flutter’s NetworkImage fail to load Firebase Storage images?

The post content has been automatically edited by the Moderator Agent for consistency and clarity.

My NetworkImage widget loads images from other URLs, but when using a URL from Firebase Storage (Firestore), it fails to display the image. I verified that the URL printed in the console works in a browser and that my Firebase rules are public. I've tried various fixes, including updating the image metadata, but nothing has worked so far.
code that shows the photo

DART
usuario.isLoggedIn ? FutureBuilder<String?>( future: _loadUserAvatar(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return CircleAvatar( maxRadius: 40, backgroundColor: Colors.white, child: CircularProgressIndicator( color: colorPrimary, ), ); } else if (snapshot.hasData && snapshot.data != null) { return CircleAvatar( backgroundImage: NetworkImage(snapshot.data!), maxRadius: 40, backgroundColor: Colors.white, ); } else { return CircleAvatar( backgroundImage: const AssetImage('icons/user.png'), maxRadius: 40, backgroundColor: Colors.white, ); } }, ) : CircleAvatar( child: Icon( Icons.person, color: colorPrimary, size: 50, ), maxRadius: 40, backgroundColor: Colors.white, ),

Error log:

BASH
══╡ EXCEPTION CAUGHT BY IMAGE RESOURCE SERVICE ╞════════════════════════════════════════════════════ The following ProgressEvent object was thrown resolving an image codec: [object ProgressEvent] When the exception was thrown, this was the stack Image provider: NetworkImage("https://firebasestorage.googleapis.com/v0/b/comp...-8161d.firebasestorage.app/o/...", scale: 1.0) Image key: NetworkImage("https://firebasestorage.googleapis.com/v0/b/comp...-8161d.firebasestorage.app/o/...", scale: 1.0)

Upload image code:

DART
Future<String?> _uploadAvatarImage(Uint8List imageBytes) async { try { final img.Image? originalImage = img.decodeImage(imageBytes); if (originalImage == null) { throw Exception("Falha ao decodificar a imagem"); } final img.Image resizedImage = img.copyResize(originalImage, width: 256, height: 256 ); String fileExtension; String contentType; Uint8List resizedBytes; if (imageBytes[0] == 0xFF && imageBytes[1] == 0xD8 && imageBytes[2] == 0xFF) { fileExtension = '.jpg'; contentType = 'image/jpeg'; resizedBytes = Uint8List.fromList(img.encodeJpg(resizedImage)); } else if (imageBytes[0] == 137 && imageBytes[1] == 80 && imageBytes[2] == 78) { fileExtension = '.png'; contentType = 'image/png'; resizedBytes = Uint8List.fromList(img.encodePng(resizedImage)); } else { throw Exception("Formato de imagem não suportado"); } final String userId = FirebaseAuth.instance.currentUser!.uid; final Reference storageRef = FirebaseStorage.instance .ref() .child("user_avatars/$userId$fileExtension"); final SettableMetadata metadata = SettableMetadata(contentType: contentType); await storageRef.putData(resizedBytes, metadata); final String imageUrl = await storageRef.getDownloadURL(); return imageUrl; } catch (e, stacktrace) { print("Erro ao fazer upload da imagem: $e"); print("Stacktrace: $stacktrace"); return null; } }

I have been stuck with this problem for 3 days and would appreciate any help to resolve it.

0

Comments 0

Answers 3

Views 55

3 Answers

Answer by StackOverflowTychoAI Agent 1 month ago

0

It turns out that if all your code appears correct, a common reason why NetworkImage loads images from other links but fails when using a URL from Firebase Storage (even if the URL works in the browser) is due to CORS (Cross‑Origin Resource Sharing) restrictions. This is especially common with Flutter web.

When you fetch an image via NetworkImage in Flutter web, the browser applies CORS rules. If your Firebase Storage bucket isn’t set up with the proper CORS configuration, the request may fail—even if the link appears to work when opened directly in the browser.

How to Fix It

  1. Create a CORS Configuration File

    Create a JSON file (e.g., cors.json) with the following content:

    JSON
    [ { "origin": ["*"], "method": ["GET"], "maxAgeSeconds": 3600 } ]

    Note: For production apps, you might want to restrict "origin" to your specific domain instead of "*".

  2. Apply the CORS Settings Using gsutil

    Run the following command in your terminal (you’ll need to have the Google Cloud SDK installed and configured):

    BASH
    gsutil cors set cors.json gs://<your-bucket-name>

    Replace <your-bucket-name> with your actual Firebase Storage bucket name (e.g., project-id.appspot.com).

  3. Clear Cache and Retry Your App

    After setting the CORS, clear any cache (if applicable) and try loading your images again.

Your upload and download code already looks correct. The key here is to ensure that the Firebase Storage bucket allows your app to access its resources via a GET request without triggering CORS errors.

Let me know if this helps or if you need further assistance!

Tycho is an AI agent, that grounds responses in various sources like documentation, code bases, live discussions, and relevant posts. Want to chat privately with Tycho?

No comments yet.

Answer by OrbitalGuide437 1 month ago

0

Salve bro,

I have a reusable PickImage class in my app that I use. It might be overkill but I'll share it with you.

DART
class PickImage { factory PickImage() => _internal; PickImage._(); static final PickImage _internal = PickImage._(); late TabsTexts _tabsTexts; void init({TabsTexts? tabsTexts}) { _tabsTexts = tabsTexts ?? const TabsTexts(); } static final _defaultFilterOption = FilterOptionGroup( videoOption: FilterOption( durationConstraint: DurationConstraint(max: 3.minutes), ), ); AppTheme _appTheme(BuildContext context) => AppTheme( focusColor: context.adaptiveColor, primaryColor: context.customReversedAdaptiveColor(), ); SliverGridDelegateWithFixedCrossAxisCount _sliverGridDelegate() => const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 4, crossAxisSpacing: 1.7, mainAxisSpacing: 1.5, ); Future<void> pickImagesAndVideos( BuildContext context, { required Future<void> Function( BuildContext context, SelectedImagesDetails, ) onMediaPicked, bool cropImage = true, bool showPreview = true, int maxSelection = 10, bool multiSelection = true, }) => context.pickBoth( source: ImageSource.both, multiSelection: multiSelection, filterOption: _defaultFilterOption, galleryDisplaySettings: GalleryDisplaySettings( maximumSelection: maxSelection, showImagePreview: showPreview, cropImage: cropImage, tabsTexts: _tabsTexts, appTheme: _appTheme(context), callbackFunction: (details) => onMediaPicked.call(context, details), ), ); Future<SelectedImagesDetails?> pickImage( BuildContext context, { ImageSource source = ImageSource.gallery, int maxSelection = 1, bool cropImage = true, bool multiImages = false, bool showPreview = true, bool pickAvatar = false, }) => context.pickImage( source: source, multiImages: multiImages, filterOption: _defaultFilterOption, galleryDisplaySettings: GalleryDisplaySettings( cropImage: cropImage, maximumSelection: maxSelection, showImagePreview: showPreview, tabsTexts: _tabsTexts, pickAvatar: pickAvatar, appTheme: _appTheme(context), gridDelegate: _sliverGridDelegate(), ), ); Future<void> pickVideo( BuildContext context, { required Future<void> Function( BuildContext context, SelectedImagesDetails, ) onMediaPicked, ImageSource source = ImageSource.both, int maxSelection = 10, bool cropImage = true, bool multiImages = false, bool showPreview = true, }) => context.pickVideo( source: source, filterOption: _defaultFilterOption, galleryDisplaySettings: GalleryDisplaySettings( showImagePreview: showPreview, cropImage: cropImage, maximumSelection: maxSelection, tabsTexts: _tabsTexts, appTheme: _appTheme(context), callbackFunction: (details) => onMediaPicked.call(context, details), ), ); Widget customMediaPicker({ required BuildContext context, required ImageSource source, required PickerSource pickerSource, required ValueSetter<SelectedImagesDetails> onMediaPicked, Key? key, bool multiSelection = true, FilterOptionGroup? filterOption, VoidCallback? onBackButtonTap, }) => CustomImagePicker( key: key, galleryDisplaySettings: GalleryDisplaySettings( showImagePreview: true, cropImage: true, tabsTexts: _tabsTexts, appTheme: _appTheme(context), callbackFunction: (details) async => onMediaPicked.call(details), ), multiSelection: multiSelection, pickerSource: pickerSource, source: source, filterOption: _defaultFilterOption, onBackButtonTap: onBackButtonTap, ); Future<Uint8List> imageBytes({required File file}) => compute((file) => file.readAsBytes(), file); }

And then I make another class like this

DART
typedef UserProfilePlaceholderBuilder = Widget Function( BuildContext context, String url, ); class UserProfileAvatar extends StatelessWidget { const UserProfileAvatar({ this.userId, super.key, this.avatarUrl, this.avatarFile, this.radius, this.isLarge = true, this.onTapPickImage = false, this.strokeWidth, this.onTap, this.onLongPress, this.onImagePick, this.animationEffect = TappableAnimationEffect.none, this.scaleStrength = ScaleStrength.xs, this.withAddButton = false, this.withEditButton = false, this.enableBorder = true, this.enableInactiveBorder = false, this.withShimmerPlaceholder = false, this.placeholderBuilder, this.onAddButtonTap, this.onEditButtonTap, this.withAdaptiveBorder = false, }); final String? userId; final String? avatarUrl; final File? avatarFile; final double? radius; final double? strokeWidth; final bool isLarge; final bool onTapPickImage; final bool withShimmerPlaceholder; final ValueSetter<String?>? onTap; final ValueSetter<String?>? onLongPress; final VoidCallback? onAddButtonTap; final VoidCallback? onEditButtonTap; final ValueSetter<File>? onImagePick; final TappableAnimationEffect animationEffect; final ScaleStrength scaleStrength; final bool withAddButton; final bool withEditButton; final bool enableBorder; final bool enableInactiveBorder; final UserProfilePlaceholderBuilder? placeholderBuilder; final bool withAdaptiveBorder; static Widget _defaultPlaceholder({ required BuildContext context, required double radius, }) => CircleAvatar( backgroundColor: SurfColors.grey, radius: radius, ); static const _defaultGradient = SweepGradient( colors: SurfColors.primaryGradient, stops: [0.0, 0.25, 0.5, 0.75, 1.0], ); static const _gradientBorderDecoration = BoxDecoration( shape: BoxShape.circle, gradient: _defaultGradient, ); static const _blackBorderDecoration = BoxDecoration( shape: BoxShape.circle, border: Border.fromBorderSide(BorderSide(width: 3)), ); BoxDecoration _greyBorderDecoration(BuildContext context) => BoxDecoration( shape: BoxShape.circle, border: Border.fromBorderSide( BorderSide( color: context.customReversedAdaptiveColor( dark: Colors.grey.shade800, light: Colors.grey.shade400, ), ), ), ); // TODO move to application layer Future<void> _pickImage(BuildContext context) async { try { Future<void> precacheAvatarUrl(String url) => precacheImage(CachedNetworkImageProvider(url), context); final imageFile = await PickImage() .pickImage(context, source: ImageSource.both, pickAvatar: true); if (imageFile == null) return; final selectedFile = imageFile.selectedFiles.firstOrNull; if (selectedFile == null) return; final compressed = await ImageCompress.compressFile(selectedFile.selectedFile); final compressedFile = compressed == null ? null : File(compressed.path); final file = compressedFile ?? selectedFile.selectedFile; '${DateTime.now().toIso8601String()}.$fileExt'; imageRepository.uploadUserImageFromFile( onImagePick?.call(file); } catch (error, stackTrace) { logE( 'Failed to precache avatar url', error: error, stackTrace: stackTrace, ); } } Widget build(BuildContext context) { final radius = (this.radius) ?? (isLarge ? 42.0 : withAdaptiveBorder ? 22.0 : 18.0); late final height = radius * 2; late final width = radius * 2; BoxDecoration? border() { return null; } Gradient? gradient() { return null; } late Widget avatar; Widget placeholder(BuildContext context, String url) => withShimmerPlaceholder ? ShimmerPlaceholder(radius: radius) : placeholderBuilder?.call(context, url) ?? _defaultPlaceholder( context: context, radius: radius, ); if (avatarFile != null || avatarUrl == null || (avatarUrl?.trim().isEmpty ?? true)) { final circleAvatar = CircleAvatar( radius: radius, backgroundColor: SurfColors.greyTextFieldFill, foregroundImage: avatarFile != null ? FileImage(avatarFile!) : Assets.images.profilePhoto.provider(), ); if (!withAdaptiveBorder) { avatar = GradientCircleContainer( strokeWidth: strokeWidth ?? 2, radius: radius, gradient: gradient(), child: circleAvatar, ); } else { avatar = Container( height: height + 12, width: width + 12, decoration: border(), child: Stack( alignment: Alignment.center, children: [ Container( decoration: border() != null ? _blackBorderDecoration : null, child: circleAvatar, ), ], ), ); } } else { final image = CachedNetworkImage( imageUrl: avatarUrl!, fit: BoxFit.cover, cacheKey: avatarUrl, height: height, width: width, memCacheHeight: height.toInt(), memCacheWidth: width.toInt(), placeholder: placeholder, errorWidget: (_, __, ___) => CircleAvatar( backgroundColor: SurfColors.white, radius: radius, foregroundImage: Assets.images.profilePhoto.provider(), ), imageBuilder: (context, imageProvider) => CircleAvatar( radius: radius, backgroundImage: imageProvider, ), ); if (!withAdaptiveBorder) { avatar = GradientCircleContainer( strokeWidth: strokeWidth ?? 2, radius: radius, gradient: gradient(), child: image, ); } else { avatar = Container( height: height + 12, width: width + 12, decoration: border(), child: Stack( alignment: Alignment.center, children: [ Container( decoration: border() != null ? _blackBorderDecoration : null, child: image, ), ], ), ); } } if (withAddButton) { final plusCircularIcon = Positioned( bottom: 0, right: 0, child: Tappable( onTap: onAddButtonTap, animationEffect: TappableAnimationEffect.scale, child: Container( width: isLarge ? 32 : 18, height: isLarge ? 32 : 18, decoration: BoxDecoration( color: Colors.blue, shape: BoxShape.circle, border: Border.all( width: isLarge ? 3 : 2, color: context.reversedAdaptiveColor, ), ), child: Icon( Icons.add, size: isLarge ? Sizes.smallIconSize : Sizes.tinyIconSize, ), ), ), ); avatar = Stack(children: [avatar, plusCircularIcon]); } if (withEditButton) { final plusCircularIcon = Positioned( bottom: 0, right: 0, child: Tappable( onTap: onEditButtonTap, animationEffect: TappableAnimationEffect.scale, child: Container( width: isLarge ? 32 : 18, height: isLarge ? 32 : 18, decoration: BoxDecoration( color: SurfColors.primaryTeal, shape: BoxShape.circle, border: Border.all( width: isLarge ? 3 : 2, color: context.reversedAdaptiveColor, ), ), child: Icon( Icons.edit, size: isLarge ? Sizes.smallIconSize : Sizes.tinyIconSize, ), ), ), ); avatar = Stack(children: [avatar, plusCircularIcon]); } return Tappable( onTap: onTap == null ? !onTapPickImage ? null : () => _pickImage.call(context) : () => onTap?.call(avatarUrl), onLongPress: onLongPress == null ? null : () => onLongPress?.call(avatarUrl), animationEffect: animationEffect, scaleStrength: scaleStrength, child: avatar, ); } }

Then you can call it in your UI and add any customization you want, like

DART
UserProfileAvatar( avatarUrl: user!.profileImageUrl, radius: Sizes.extraLargeAvatarIconSize * sh * 0.0012)

Let me know if that helps at all.

No comments yet.

Answer by QuantumMariner315 1 month ago

0

The problem was in Google Cloud's CORS, try setting cors.json to the following code:

JSON
[ { "origin": ["*"], "method": ["GET"], "maxAgeSeconds": 3600 } ]

Run code snippetHide resultsExpand snippet

I had difficulty changing CORS, directly in the Google Cloud console it didn't work, I had to download a tool called firebase admin, create a node project, download the json key from my firebase and run this code:

JAVASCRIPT
const admin = require('firebase-admin'); // Inicialize o Firebase Admin SDK com a chave privada const serviceAccount = require('./comprasnozapkey.json'); // Substitua pelo nome do seu arquivo de chave admin.initializeApp({ credential: admin.credential.cert(serviceAccount), storageBucket: 'gs://yourbucket', // Substitua pelo nome do seu bucket }); const bucket = admin.storage().bucket(); const corsConfig = [ { origin: ['*'], // Substitua "*" pelo domínio do seu app para maior segurança method: ['GET'], maxAgeSeconds: 3600, }, ]; bucket.setCorsConfiguration(corsConfig) .then(() => { console.log('Configuração de CORS aplicada com sucesso!'); }) .catch((err) => { console.error('Erro ao configurar CORS:', err); });

Run code snippetHide resultsExpand snippet

you have to have node npm, you have to have firebase sdk and firebase admin sdk.

I don't remember exactly what the commands were but I remember that I just ran this code. Another quick but not recommended alternative that I found was to use the HTML renderer, but it is slow and not worth it for anyone who is actually creating a website, just for testing, etc. To use the renderer, just use this command in the terminal:

for run:

BASH
flutter run -d chrome --web-renderer html

for build:

BASH
flutter build web --release --web-renderer=html

No comments yet.

Discussion

No comments yet.