【Flutter】PathProviderで各OSのディレクトリを取得してみた

対象者

  • Flutterを使用して複数のプラットフォームでアプリ開発を行っているエンジニア
  • PathProviderパッケージの具体的な使用方法や各ディレクトリの用途を知りたい開発者
  • 効率的なデータ管理方法を学び、プロジェクトを円滑に進めたいと考えている方

はじめに

Flutter開発者にとって、複数のプラットフォームでのファイルシステムディレクトリの管理は、重要でありながらも頭を悩ませる課題の一つです。そこで、今回はFlutterのパッケージ「path_provider」を使用し、各プラットフォームで具体的にどのディレクトリが取得できるのかを実際に実験してみました。この実験を通じて、Windows、Android、iOS、そしてmacOSでのディレクトリのパスを明確に把握し、より一貫したデータ管理が可能になります。
具体的なコード例と共に、各OS毎の実際のディレクトリパスを一挙に紹介してます。

path_providerについて

path_providerは、Flutterでプラットフォーム固有のファイルシステムディレクトリのパスを取得するためのパッケージです。アプリケーションのデータ保存先を簡単に特定することができ、複数のプラットフォームで一貫したディレクトリ管理を実現します。

ドキュメントディレクトリの取得

ユーザーが生成したデータやアプリケーションによって再生成できないデータを配置するディレクトリへのパスを取得します。

Directory docDir = await getApplicationDocumentsDirectory();
  • Windows: C:\Users\[user_name]\Documents
  • Android: /data/user/0/[package_name]/app_flutter
  • iOS (Mac): /Users/[user_name]/Library/Containers/[app_uuid]/Data/Documents
  • iOS (iPhone): /var/mobile/Containers/Data/Application/[app_uuid]/Documents
  • macOS: /Users/[user_name]/Library/Containers/[package_name]/Data/Documents

一時ディレクトリの取得

バックアップされず、一時的にダウンロードファイルのキャッシュを保存するのに適したデバイス上の一時ディレクトリへのパスを取得します。このディレクトリの内容は、システムによっていつでも削除される可能性があります。

Directory tempDir = await getTemporaryDirectory();
  • Windows: C:\Users\[user_name]\AppData\Local\Temp
  • Android: /data/user/0/[package_name]/cache
  • iOS (Mac): /Users/[user_name]/Library/Containers/[app_uuid]/Data/Library/Caches
  • iOS (iPhone): /var/mobile/Containers/Data/Application/[app_uuid]/Library/Caches
  • macOS: /Users/[user_name]/Library/Containers/[package_name]/Data/Library/Caches

サポートディレクトリの取得

アプリケーションサポートファイルを配置するためのディレクトリへのパスを取得します。このディレクトリは、ユーザーが直接アクセスすることを意図していないデータに使用します。

Directory appSupportDir = await getApplicationSupportDirectory();
  • Windows: C:\Users\[user_name]\AppData\Roaming\[organization_name]\[app_name]
  • Android: /data/user/0/[package_name]/files
  • iOS (Mac): /Users/[user_name]/Library/Containers/[app_uuid]/Data/Library/Application Support
  • iOS (iPhone): /var/mobile/Containers/Data/Application/[app_uuid]/Library/Application Support
  • macOS: /Users/[user_name]/Library/Containers/[package_name]/Data/Library/Application Support/[package_name]

ライブラリディレクトリの取得

アプリケーションが永続的に保存し、バックアップされ、ユーザーに表示されないファイル(例:sqlite.db)を保存するディレクトリへのパスを取得します。

Directory appLibraryDir = await getLibraryDirectory();
  • iOS (Mac): /Users/[user_name]/Library/Containers/[app_uuid]/Data/Library
  • iOS (iPhone): /var/mobile/Containers/Data/Application/[app_uuid]/Library
  • macOS: /Users/[user_name]/Library/Containers/[package_name]/Data/Library

キャッシュディレクトリの取得

アプリケーション固有のキャッシュファイルを配置できるディレクトリへのパスを取得します。

Directory appCacheDir = await getApplicationCacheDirectory();
  • Windows: C:\Users\[user_name]\AppData\Local\[organization_name]\[app_name]
  • Android: /data/user/0/[package_name]/cache
  • iOS (Mac): /Users/[user_name]/Library/Containers/[app_uuid]/Data/Library/Caches
  • iOS (iPhone): /var/mobile/Containers/Data/Application/[app_uuid]/Library/Caches
  • macOS: /Users/[user_name]/Library/Containers/[package_name]/Data/Library/Caches/[package_name]

外部ストレージディレクトリの取得

アプリケーションがトップレベルのストレージにアクセスできるディレクトリへのパスを取得します。

Directory extStorageDir = await getExternalStorageDirectory();
  • Android: /storage/emulated/0/Android/data/[package_name]/files

外部キャッシュディレクトリの取得

アプリケーション固有のキャッシュデータを外部に保存できるディレクトリへのパスを取得します。

List<Directory> extCacheDirs = await getExternalCacheDirectories();
  • Android: /storage/emulated/0/Android/data/[package_name]/cache

外部ストレージディレクトリ(複数)の取得

アプリケーション固有のデータを外部に保存できるディレクトリへのパスを取得します。

List<Directory> extStorageDirs = await getExternalStorageDirectories();
  • Android: /storage/emulated/0/Android/data/[package_name]/files

ダウンロードディレクトリの取得

ダウンロードされたファイルを保存できるディレクトリへのパスを取得します。

Directory downloadsDir = await getDownloadsDirectory();
  • Windows: C:\Users\[user_name]\Downloads
  • Android: /storage/emulated/0/Android/data/[package_name]/files/downloads
  • iOS (Mac): /Users/[user_name]/Library/Containers/[app_uuid]/Data/Downloads
  • iOS (iPhone): /var/mobile/Containers/Data/Application/[app_uuid]/Downloads
  • macOS: /Users/[user_name]/Library/Containers/[package_name]/Data/Downloads

余談

MacでFlutterアプリを実行した際、ユーザーが簡単にアクセスできる場所にファイルを保存する必要があり、今回保存場所の一覧を作成してみました。
PathProviderパッケージを利用して各プラットフォームでのディレクトリパスを取得し、データを保存しようと試みましたが、実際に実行してみると、AndroidやiOSでは一般のユーザーが簡単にアクセスできる場所にファイルを保存することができないという問題が判明しました。

そのため、file_pickerという別のパッケージを使用することにしました。このパッケージを使うことで、Macアプリとしてはダウンロードフォルダーに保存でき、ユーザーが簡単にアクセスできます。しかし、FlutterアプリでFlavorを使っている関係で、Macのアプリとして使用することができず、Mac上で動作するiOSアプリ(ビバ、Mac Sillicon)として使用する必要がありました。そのため、ユーザーが簡単にアクセスできる場所に保存することは実現できませんでした。

この問題に対する解決策として、シェアのためのパッケージを併用する必要があると結論付けました。今後は、これらのパッケージを組み合わせて、ユーザーがより簡単にアクセスできるようなアプローチを模索していきます。

Q&A

Q1: PathProviderパッケージを使うとどのようなメリットがありますか?

A1: PathProviderパッケージを使うことで、Flutterアプリケーションが各プラットフォームで適切なファイル保存場所を簡単に特定できます。これにより、一貫したデータ管理が可能になり、開発効率が向上します。また、ユーザーデータやキャッシュデータの保存場所を自動的に取得できるため、開発者が異なるOSごとにファイルパスを調整する必要がなくなります。

Q2: PathProviderで取得できるドキュメントディレクトリはどのような用途に適していますか?

A2: ドキュメントディレクトリは、ユーザーが生成したデータやアプリケーションによって再生成できない重要なデータの保存に適しています。例えば、ユーザーの設定ファイルや重要なドキュメントをこのディレクトリに保存することで、アプリのアンインストール後もデータが保持される可能性が高くなります。

Q3: 各プラットフォームで一時ディレクトリを使用する際の注意点は何ですか?

A3: 一時ディレクトリは、システムによっていつでも削除される可能性があるため、重要なデータを保存しないように注意する必要があります。このディレクトリは、キャッシュや一時的なファイル保存に適しており、アプリの再起動やシステムの再起動後にはデータが失われる可能性があることを念頭に置いて使用してください。

まとめ

この記事では、Flutterのpath_providerパッケージを使って、各プラットフォームでどのディレクトリパスが取得できるかを実際に実験し、詳細に解説しました。具体的なコード例を通じて、ドキュメントディレクトリ、一時ディレクトリ、サポートディレクトリなど、多様なディレクトリの取得方法を学びました。また、各OS毎にどのようなパスが得られるかを確認し、アプリのデータ管理が一貫して行えることを理解しました。これらの知識を活用して、今後のFlutter開発がよりスムーズに進むことを願っています。

参考

ソース(main.dartにコピペして動作確認用)

import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';
import 'package:flutter/services.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Path Provider Example'),
        ),
        body: DirectoryPaths(),
      ),
    );
  }
}

class DirectoryPaths extends StatefulWidget {
  @override
  _DirectoryPathsState createState() => _DirectoryPathsState();
}

class _DirectoryPathsState extends State<DirectoryPaths> {
  String applicationDocumentsPath = '';
  String temporaryDirectoryPath = '';
  String applicationSupportPath = '';
  String applicationLibraryPath = '';
  String applicationCachePath = '';
  String externalStoragePath = '';
  String externalCachePath = '';
  String externalStorageDirsPath = '';
  String downloadsDirectoryPath = '';
  String operatingSystem = '';

  @override
  void initState() {
    super.initState();
    _setPaths();
  }

  Future<Directory?> _getDirectory(
      Future<Directory?> Function() getDirFunc) async {
    try {
      return await getDirFunc();
    } catch (ex) {
      return null;
    }
  }

  Future<List<Directory>?> _getDirectoryList(
      Future<List<Directory>?> Function() getDirListFunc) async {
    try {
      return await getDirListFunc();
    } catch (ex) {
      return null;
    }
  }

  Future<void> _setPaths() async {
    final os = Platform.operatingSystem;

    final appDocDir = await _getDirectory(getApplicationDocumentsDirectory);
    final tempDir = await _getDirectory(getTemporaryDirectory);
    final appSupportDir = await _getDirectory(getApplicationSupportDirectory);
    final appLibraryDir = await _getDirectory(getLibraryDirectory);
    final appCacheDir = await _getDirectory(getApplicationCacheDirectory);
    final extStorageDir = await _getDirectory(getExternalStorageDirectory);
    final extCacheDirs = await _getDirectoryList(getExternalCacheDirectories);
    final extStorageDirs =
        await _getDirectoryList(getExternalStorageDirectories);
    final downloadsDir = await _getDirectory(getDownloadsDirectory);

    setState(() {
      operatingSystem = os;
      applicationDocumentsPath = appDocDir?.path ?? 'Not available';
      temporaryDirectoryPath = tempDir?.path ?? 'Not available';
      applicationSupportPath = appSupportDir?.path ?? 'Not available';
      applicationLibraryPath = appLibraryDir?.path ?? 'Not available';
      applicationCachePath = appCacheDir?.path ?? 'Not available';
      externalStoragePath =
          extStorageDir?.path ?? 'External storage not available';
      externalCachePath = extCacheDirs != null && extCacheDirs.isNotEmpty
          ? extCacheDirs.first.path
          : 'External cache not available';
      externalStorageDirsPath =
          extStorageDirs != null && extStorageDirs.isNotEmpty
              ? extStorageDirs.first.path
              : 'External storage dirs not available';
      downloadsDirectoryPath =
          downloadsDir?.path ?? 'Downloads directory not available';
    });
  }

  Future<void> _createTestFile(String path) async {
    final testFile = File('$path/test.txt');
    await testFile.writeAsString('test');
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(
      content: Text('test.txt created in $path'),
    ));
  }

  void _copyAllToClipboard() {
    final allPaths = '''
Operating System: $operatingSystem
Application Documents Directory: $applicationDocumentsPath
Temporary Directory: $temporaryDirectoryPath
Application Support Directory: $applicationSupportPath
Application Library Directory: $applicationLibraryPath
Application Cache Directory: $applicationCachePath
External Storage Directory: $externalStoragePath
External Cache Directory: $externalCachePath
External Storage Directories: $externalStorageDirsPath
Downloads Directory: $downloadsDirectoryPath
''';
    Clipboard.setData(ClipboardData(text: allPaths));
    ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
      content: Text('All paths copied to clipboard'),
    ));
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: SingleChildScrollView(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Text('Operating System: $operatingSystem'),
            SizedBox(height: 16),
            _buildPathDisplay(
                'Application Documents Directory', applicationDocumentsPath),
            const SizedBox(height: 8),
            _buildPathDisplay('Temporary Directory', temporaryDirectoryPath),
            const SizedBox(height: 8),
            _buildPathDisplay(
                'Application Support Directory', applicationSupportPath),
            const SizedBox(height: 8),
            _buildPathDisplay(
                'Application Library Directory', applicationLibraryPath),
            const SizedBox(height: 8),
            _buildPathDisplay(
                'Application Cache Directory', applicationCachePath),
            const SizedBox(height: 8),
            _buildPathDisplay(
                'External Storage Directory', externalStoragePath),
            const SizedBox(height: 8),
            _buildPathDisplay('External Cache Directory', externalCachePath),
            const SizedBox(height: 8),
            _buildPathDisplay(
                'External Storage Directories', externalStorageDirsPath),
            const SizedBox(height: 8),
            _buildPathDisplay('Downloads Directory', downloadsDirectoryPath),
            const SizedBox(height: 16),
            FilledButton(
              onPressed: _copyAllToClipboard,
              child: const Text('Copy All Paths'),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildPathDisplay(String title, String path) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text('$title:'),
        SizedBox(height: 4),
        Row(
          children: [
            Expanded(
              child: Text(
                '   $path',
                style: TextStyle(fontSize: 14, color: Colors.black87),
              ),
            ),
            IconButton(
              icon: Icon(Icons.create),
              onPressed: () => _createTestFile(path),
            ),
          ],
        ),
      ],
    );
  }
}