I solved it natively in Kotlin and published it as a package. Since you're using NetworkImage and want to trigger the image download only after it's loaded, use Image.network with the loadingBuilder callback. This lets you detect when the image finishes loading and then trigger the download using 3 dependencies:
- permission_handler 11.0.1
- device_info_plus 10.0.0
- flutter_universal_downloader 0.0.3
Example that also handles Android permissions based on the SDK version:
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_universal_downloader/flutter_universal_downloader.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:permission_handler/permission_handler.dart';
class DownloadableAvatar extends StatefulWidget {
final String imageUrl;
const DownloadableAvatar({super.key, required this.imageUrl});
@override
State<DownloadableAvatar> createState() => _DownloadableAvatarState();
}
class _DownloadableAvatarState extends State<DownloadableAvatar> {
bool _downloadStarted = false;
int _androidSdkVersion = 0;
@override
void initState() {
super.initState();
_getAndroidSdkVersion();
}
Future<void> _getAndroidSdkVersion() async {
if (Platform.isAndroid) {
try {
final androidInfo = await DeviceInfoPlugin().androidInfo;
if (mounted) {
setState(() => _androidSdkVersion = androidInfo.version.sdkInt);
}
} catch (e) {
debugPrint('Error getting SDK version: $e');
}
}
}
Future<void> _downloadImageWithPermission(String url) async {
try {
if (Platform.isAndroid) {
final PermissionStatus status;
if (_androidSdkVersion >= 33) {
status = await Permission.notification.request();
} else if (_androidSdkVersion >= 29) {
status = PermissionStatus.granted;
} else {
status = await Permission.storage.request();
}
if (!status.isGranted) {
_showSnackBar('❌ Permission denied. Cannot download.');
return;
}
}
final result = await FlutterUniversalDownloader.foregroundDownload(
url,
fileName: 'profile_image.jpg',
);
_showSnackBar(
result ? '⬇️ Download started!' : '❌ Failed to start download.',
);
} on PlatformException catch (e) {
debugPrint('Platform error: ${e.message}');
_showSnackBar('⚠️ ${e.message}');
} catch (e) {
debugPrint('Error: $e');
_showSnackBar('⚠️ Unexpected error: $e');
}
}
void _showSnackBar(String message) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message)));
}
}
@override
Widget build(BuildContext context) {
return CircleAvatar(
radius: 25,
child: ClipOval(
child: Image.network(
widget.imageUrl,
fit: BoxFit.cover,
width: 50,
height: 50,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) {
if (!_downloadStarted) {
_downloadStarted = true;
_downloadImageWithPermission(widget.imageUrl);
}
return child;
} else {
return const Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
);
}
},
errorBuilder: (context, error, stackTrace) =>
const Icon(Icons.error),
),
),
);
}
}
Update AndroidManifest.xml:
<!-- For Android < 10 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
<!-- For Android 13+ notifications -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />