DEV Community

Cover image for Convert Image URL to File in Flutter — Exact Code
Gulshan Yadav
Gulshan Yadav

Posted on Originally published at misar.blog

Convert Image URL to File in Flutter — Exact Code

So, in this article, I will be showing you how you can convert an image URL to a File in Flutter.

The situation comes up more often than you would think. You download an avatar, a product photo, or an attachment, and you need a real file on disk — because you want to upload it somewhere, share it with the system share sheet, cache it offline, or attach it to a form. Image.network alone cannot do any of that. It renders pixels; it does not give you a file.

A few weeks ago I was wiring this exact feature into a client's logistics app — drivers snap a delivery photo, it uploads to the server, and the server returns a URL. On the pickup screen the driver needed to re-attach that same image to a dispute form, and the form only accepted files. The URL was useless to it. So I wrote the downloader, and this article is exactly what I shipped, plus every pitfall I hit along the way.

Let's jump into the coding part.

Step 1: Add the Dependencies

Two packages cover the whole job: http for downloading the bytes, and path_provider for finding a writable directory on disk.

dependencies:
  flutter:
    sdk: flutter
  http: ^1.2.0
  path_provider: ^2.1.0
Enter fullscreen mode Exit fullscreen mode
  • http downloads the image bytes over HTTP.
  • path_provider gives you a real, writable directory path (getTemporaryDirectory or getApplicationDocumentsDirectory), because on iOS and Android you cannot just write to / or a hardcoded path.

Step 2: Download and Write the File

Here is the complete function. It downloads the bytes, picks a filename, writes them to disk, and returns the File:

import 'dart:io';
import 'dart:typed_data';
import 'package:http/http.dart' as http;
import 'package:path_provider/path_provider.dart';

Future<File> downloadImageToFile(String url, {String? fileName}) async {
  final response = await http.get(Uri.parse(url));

  if (response.statusCode != 200) {
    throw HttpException(
      'Failed to download image: HTTP ${response.statusCode}',
    );
  }

  final Uint8List bytes = response.bodyBytes;
  final Directory dir = await getTemporaryDirectory();
  final String name = fileName ??
      'image_${DateTime.now().millisecondsSinceEpoch}.jpg';
  final File file = File('${dir.path}/$name');

  return file.writeAsBytes(bytes, flush: true);
}
Enter fullscreen mode Exit fullscreen mode

That is the whole feature in eleven lines. Call it like this:

final File imageFile = await downloadImageToFile(
  'https://example.com/photo.jpg',
  fileName: 'receipt.jpg',
);
print('Saved to ${imageFile.path}');
Enter fullscreen mode Exit fullscreen mode

A few details worth noting:

  • getTemporaryDirectory() is correct for throwaway files (attachments you are about to upload). Use getApplicationDocumentsDirectory() if the file must survive a restart and live in app storage.
  • writeAsBytes(bytes, flush: true) guarantees the bytes are physically written before the function returns. Without flush: true, a crash can leave you with a zero-byte file.
  • Throwing on a non-200 status is deliberate. I have seen tutorials that skip it, and then the app writes an HTML error page to disk and calls it an image.

Step 3: Decode the Filename Sensibly

One thing that bit me: image URLs often end in a path like /media/photo?id=1234 with no extension, or the extension is wrong. If the filename matters to your downstream upload, derive it from the URL's path segment rather than the raw string:

String nameFromUrl(String url) {
  final Uri uri = Uri.parse(url);
  final String lastSegment = uri.pathSegments.isEmpty
      ? 'image.jpg'
      : uri.pathSegments.last;
  return lastSegment.contains('.') ? lastSegment : 'image_$lastSegment.jpg';
}
Enter fullscreen mode Exit fullscreen mode

If the URL gives you nothing usable, fall back to a timestamped default — which is what the main function above already does.

Step 4: Make It Faster with a Cache (Optional)

If you are downloading the same image repeatedly (a user's avatar across several screens), a bare download is wasteful. Wrap the lookup in a cache check before you hit the network:

Future<File> cachedImageToFile(String url) async {
  final dir = await getTemporaryDirectory();
  final File cached = File('${dir.path}/${_hash(url)}.img');
  if (cached.existsSync()) return cached;
  return downloadImageToFile(url, fileName: '${_hash(url)}.img');
}

String _hash(String input) =>
    input.hashCode.toRadixString(16); // simple cache key
Enter fullscreen mode Exit fullscreen mode

For production, use flutter_cache_manager instead of my 10-line hash — it handles eviction and staleness properly. My version is fine for a demo; a cache that never evicts is a disk leak waiting for a QA report.

Large Images: Stream to Disk Instead of Buffering

The http.get(...).bodyBytes approach loads the whole image into RAM before writing anything. For a 4 KB avatar that is nothing. For a 15 MB camera photo on a low-end Android device, it is a real spike that can crash older phones. When the files are big, stream the bytes straight to disk using dart:io:

import 'dart:io';

Future<File> streamImageToFile(String url, {String? fileName}) async {
  final request = await HttpClient().getUrl(Uri.parse(url));
  final response = await request.close().timeout(const Duration(seconds: 30));

  if (response.statusCode != HttpStatus.ok) {
    throw HttpException('HTTP ${response.statusCode}');
  }

  final dir = await getTemporaryDirectory();
  final file = File('${dir.path}/${fileName ?? 'streamed_${DateTime.now().millisecondsSinceEpoch}.img'}');
  final sink = file.openWrite();

  await response.pipe(sink);
  await sink.close();
  return file;
}
Enter fullscreen mode Exit fullscreen mode

response.pipe(sink) writes each chunk as it arrives, so peak memory stays flat no matter how big the file is. This is the version I reach for when a client says "drivers will attach photos from the camera." Note it uses dart:io, which means it does not work on the web — for web, keep the buffered http version, since web files are bounded by what the browser already loaded.

Which Function Should You Use?

A quick decision table, because I get asked this constantly:

Situation Use
Small image, need a one-off file downloadImageToFile (buffered)
Large image, mobile, low RAM streamImageToFile (streamed)
Same image downloaded repeatedly cachedImageToFile or flutter_cache_manager
Web target Buffered http version only
Need upload progress Pair with dio and onSendProgress

The buffered version is the correct default. Stream when you measure a problem, not preemptively — but know it exists, because the memory spike is the bug that shows up in the Play Store crash report after you ship.

One more integration note. This downloader pairs cleanly with file_picker and the share sheet: pick a file → it is already a File; download one from a URL → it becomes a File; then both feed the same upload or share code path. That symmetry — one type, File, for everything — is why the feature exists at all. If a URL cannot produce a File, your pick/share/upload code has to branch, and branches are where the bugs live.

Important Notes and Pitfalls

  1. Check the status code, always. A 404 or a 302 that redirects to a login page produces HTML bytes, and your "image file" will be an HTML file with an image extension. My HttpException guard exists precisely because this happened in testing.

  2. Some servers need headers. Images behind authentication return 401 to a bare GET. Add the header if you need it:

final response = await http.get(
  Uri.parse(url),
  headers: {'Authorization': 'Bearer $token'},
);
Enter fullscreen mode Exit fullscreen mode
  1. Large images can exhaust memory. response.bodyBytes loads the whole image into RAM. A 12 MB photo on a low-end Android device can spike memory noticeably. For very large files, stream to disk instead of buffering — or better, cap the image size server-side.

  2. The extension on the URL is not a promise. The content type is what matters. If you control the server, set Content-Type: image/jpeg and, if you need to be strict, sniff the magic bytes rather than trusting the filename.

  3. Image.network does NOT create a file. This is the most common confusion in the comments on this exact problem. Image.network decodes and renders pixels in memory. It never touches disk. If your requirement says "give me a file," you must download the bytes yourself, which is what this function does.

  4. Permissions on iOS/Android are not needed for getTemporaryDirectory() or getApplicationDocumentsDirectory() — those are your app's own sandboxed directories. You only need permission APIs (photo library / storage) when writing to user-visible locations like the gallery, which is a different feature entirely.

  5. Timeout handling. A hung server will hang your await. Wrap the call in a .timeout():

final response = await http
    .get(Uri.parse(url))
    .timeout(const Duration(seconds: 15));
Enter fullscreen mode Exit fullscreen mode
  1. Don't forget flush: true. I said it above and I will say it again because it is the one line people delete. A file written without it can be zero bytes after an abrupt kill.

FAQ (the questions I actually get in comments)

  • "How do I save it to the gallery?" That is not this feature. Use image_gallery_saver or saver_gallery for gallery access, which requires platform permissions and user-visible storage.
  • "My file is 0 bytes." Almost always the missing flush: true, or a non-200 response that was written anyway. Add the status check and the flush.
  • "Can I use it for videos?" The same function works for any binary URL — the name is just about your use case. Rename it downloadToFile and it handles anything.
  • "Temporary vs. documents directory?" Temporary = files you can lose (re-downloadable). Documents = files you must keep. Choose deliberately; do not default.
  • "What about flutter_cache_manager?" Use it when you need caching with eviction and TTL. Use this function when you just need a file, once.
  • "The URL redirects before the image loads." http.get follows redirects by default. The status code you check is the final one, so your 200-check still works. Just be aware the downloaded bytes are from the redirect target, and the fileName you pass may not match the final URL.
  • "Can I create a File object without ever hitting the network?" Yes — for bytes you already have, skip the download: File('${dir.path}/name.jpg').writeAsBytes(bytes). The downloader in this article is only for the URL → bytes → file chain.
  • "Does this work for base64 image strings?" Convert the base64 string with base64Decode first, then write the resulting bytes. Same file logic, no HTTP involved.

The Quick Checklist

  • [ ] http and path_provider in pubspec.
  • [ ] Status code checked and non-200 throws.
  • [ ] Correct directory chosen (temporary vs. documents).
  • [ ] writeAsBytes(..., flush: true).
  • [ ] Auth headers added when the URL needs them.
  • [ ] .timeout() on the request.
  • [ ] Filename derived from the URL path, with a fallback.

That is the entire flow: bytes down, directory resolved, file written, File returned. No Image.network tricks, no platform channels, no native code — just Dart, and it runs identically on Android and iOS.

I have also covered downloading files to local storage and sharing them via the system share sheet — comment below with the exact feature you are stuck on (uploads, gallery saving, offline caching) and I'll cover it next.


*Gulshan Yad

Top comments (0)