Dart Tutorial

Flutter Lesson 51 of 83 4 min read

Reading and Writing Files in Flutter with path_provider

Read and write files in a Flutter app: find the right folder with path_provider, save text and JSON, and store downloaded images.

On this page

For data that is too large or too structured for preferences, but does not need a database, write a file. An app cannot write wherever it likes. Each platform gives it private folders, and the path_provider package finds them.

flutter pub add path_provider

The file operations themselves come from dart:io, covered in the Dart chapter on files. dart:io does not work in a web build.

The folders #

FunctionUse forCleared by the system?
getApplicationDocumentsDirectory()Files the user created and would miss: notes, exportsNo
getApplicationSupportDirectory()Files the app needs but the user does not see: databases, settingsNo
getTemporaryDirectory()Caches and scratch files that can be recreatedYes, at any time
getApplicationCacheDirectory()Larger cachesYes

Reading and writing text #

import 'dart:io';
import 'package:path_provider/path_provider.dart';

Future<File> _notesFile() async {
  final dir = await getApplicationDocumentsDirectory();
  return File('${dir.path}/notes.txt');
}

Future<void> saveNotes(String text) async {
  final file = await _notesFile();
  await file.writeAsString(text);
}

Future<String> loadNotes() async {
  try {
    final file = await _notesFile();
    return await file.readAsString();
  } on PathNotFoundException {
    return ''; // first run: nothing saved yet
  }
}

Always handle the case where the file does not exist yet.

A notes screen #

import 'package:flutter/material.dart';

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

  @override
  State<NotesPage> createState() => _NotesPageState();
}

class _NotesPageState extends State<NotesPage> {
  final _controller = TextEditingController();

  @override
  void initState() {
    super.initState();
    loadNotes().then((text) {
      if (mounted) _controller.text = text;
    });
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  Future<void> _save() async {
    await saveNotes(_controller.text);
    if (!mounted) return;
    ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Saved')));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Notes'),
        actions: [IconButton(onPressed: _save, icon: const Icon(Icons.save))],
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: TextField(
          controller: _controller,
          maxLines: null,
          expands: true,
          decoration: const InputDecoration(
            hintText: 'Write something...',
            border: InputBorder.none,
          ),
        ),
      ),
    );
  }
}

Saving a list as JSON #

import 'dart:convert';
import 'dart:io';
import 'package:path_provider/path_provider.dart';

class TodoStore {
  Future<File> get _file async {
    final dir = await getApplicationSupportDirectory();
    return File('${dir.path}/todos.json');
  }

  Future<List<Todo>> load() async {
    try {
      final text = await (await _file).readAsString();
      final list = jsonDecode(text) as List<dynamic>;
      return [for (final item in list) Todo.fromJson(item as Map<String, dynamic>)];
    } on PathNotFoundException {
      return [];
    } on FormatException {
      return []; // the file is damaged: start again
    }
  }

  Future<void> save(List<Todo> todos) async {
    final file = await _file;
    final temp = File('${file.path}.tmp');
    await temp.writeAsString(jsonEncode([for (final t in todos) t.toJson()]));
    await temp.rename(file.path); // replace in one step, so a crash cannot leave half a file
  }
}

This is a perfectly good store for a few hundred items. Every save rewrites the whole file, so for thousands of records, or for searching and sorting, use SQLite.

Downloading and saving a file #

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

Future<File> downloadImage(String url, String fileName) async {
  final response = await http.get(Uri.parse(url));
  if (response.statusCode != 200) {
    throw Exception('Download failed');
  }
  final dir = await getTemporaryDirectory();
  final file = File('${dir.path}/$fileName');
  return file.writeAsBytes(response.bodyBytes);
}

// Display it
Image.file(file)

Building paths #

Add the path package to join paths correctly on every platform.

import 'package:path/path.dart' as p;

final file = File(p.join(dir.path, 'exports', 'report.csv'));
await file.parent.create(recursive: true); // make sure the folder exists

Letting the user see or share a file #

The app’s folders are private. Other apps and the user’s file manager cannot see them. To hand a file over:

GoalPackage
Share through another appshare_plus
Let the user choose where to save, or pick a file to openfile_picker
Save a photo to the galleryA gallery saver package

On the web #

There is no file system. Use shared_preferences (which uses the browser’s local storage), IndexedDB through a package such as hive or drift, or trigger a download.

Try it yourself #

Build a small journal app. Each entry has a date and text. Store all entries as a JSON file in the support directory, load them at start-up, and save after every change. Add an export button that writes the entries to a readable text file in the documents directory.

Practise in the playground Updated by Santosh Adhikari