【Flutter 实战】文件系统目录

老孟导读:Flutter 中获取文件路径,我们都知道使用 path_provider,但对其目录对含义不是很清楚,此文介绍 Android、iOS 系统的文件目录,不同场景下建议使用的目录。

不同的平台对应的文件系统是不同的,比如文件路径,因此 Flutter 中获取文件路径需要原生支持,原生端通过 MethodChannel 传递文件路径到 Flutter,如果没有特殊的需求,推荐大家使用 Google 官方维护的插件 path_provider

pub 地址:https://pub.flutter-io.cn/packages/path_provider

Github 地址:https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider

添加依赖

在项目的 pubspec.yaml 文件中添加依赖:

dependencies:
  path_provider: ^1.6.14

执行命令:

flutter pub get

文件路径

path_provider(版本:1.6.14)提供了8个方法获取不同的文件路径,目前 Flutter(Flutter 1.20.1 • channel stable )只发布了正式版本的 Android 和 iOS,因此下面仅介绍 Android 和 iOS 平台的文件路径。

  • getTemporaryDirectory

    临时目录,适用于下载的缓存文件,此目录随时可以清除,此目录为应用程序私有目录,其他应用程序无法访问此目录。

    Android 上对应getCacheDir

    iOS上对应NSCachesDirectory

  • getApplicationSupportDirectory

    应用程序可以在其中放置应用程序支持文件的目录的路径。

    将此文件用于您不想向用户公开的文件。 您的应用不应将此目录用于存放用户数据文件。

    在iOS上,对应NSApplicationSupportDirectory ,如果此目录不存在,则会自动创建。
    在Android上,对应getFilesDir

  • getLibraryDirectory

    应用程序可以在其中存储持久性文件,备份文件以及对用户不可见的文件的目录路径,例如storage.sqlite.db。

    在Android上,此函数抛出[UnsupportedError]异常,没有等效项路径存在。

  • getApplicationDocumentsDirectory

    应用程序可能在其中放置用户生成的数据或应用程序无法重新创建的数据的目录路径。

    在iOS上,对应NSDocumentDirectory API。 如果数据不是用户生成的,考虑使用[getApplicationSupportDirectory]。

    在Android上,对应getDataDirectory API。 如果要让用户看到数据,请考虑改用[getExternalStorageDirectory]。

  • getExternalStorageDirectory

    应用程序可以访问顶级存储的目录的路径。由于此功能仅在Android上可用,因此应在发出此函数调用之前确定当前操作系统。

    在iOS上,此功能会引发[UnsupportedError]异常,因为无法在应用程序的沙箱外部访问。

    在Android上,对应getExternalFilesDir(null)

  • getExternalCacheDirectories

    存储特定于应用程序的外部缓存数据的目录的路径。 这些路径通常位于外部存储(如单独的分区或SD卡)上。 电话可能具有多个可用的存储目录。
    由于此功能仅在Android上可用,因此应在发出此函数调用之前确定当前操作系统。
    在iOS上,此功能会抛出UnsupportedError,因为这是不可能的在应用程序的沙箱外部访问。

    在Android上,对应Context.getExternalCacheDirs()或API Level 低于19的Context.getExternalCacheDir()

  • getExternalStorageDirectories

    可以存储应用程序特定数据的目录的路径。 这些路径通常位于外部存储(如单独的分区或SD卡)上。
    由于此功能仅在Android上可用,因此应在发出此函数调用之前确定当前操作系统。
    在iOS上,此功能会抛出UnsupportedError,因为这是不可能的在应用程序的沙箱外部访问。
    在Android上,对应Context.getExternalFilesDirs(String type)或API Level 低于19的Context.getExternalFilesDir(String type)

  • getDownloadsDirectory

    存储下载文件的目录的路径,这通常仅与台式机操作系统有关。
    在Android和iOS上,此函数将引发[UnsupportedError]异常。

如果没有 Android 或者 iOS开发经验,看完上面的说明应该是一脸懵逼的,这么多路径到底用哪个?有什么区别?下面从 Android 和 iOS 平台的角度介绍其文件路径,最后给出路径使用的建议以及使用过程中需要注意的事项。

Android 文件存储

Android 文件存储分为内部存储外部存储

内部存储

用于保存应用的私有文件,其他应用无法访问这些数据,创建的文件在此应用的包名目录下,没有 root 权限 的手机无法在手机的 文件管理 应用中看到此目录,不过可以通过 Android Studio 工具查看,路径为:data/data/包名:

看下包名下具体的目录结构:

  • cache 目录:对应 getTemporaryDirectory 方法,用于缓存文件,此目录随时可能被系统清除。
  • files 目录:对应 getApplicationSupportDirectory 方法。
  • code_cache:此目录存储 Flutter 相关代码和资源。
    • flutter_engine/skia:Flutter 渲染引擎。
    • flutter_guidePVWGWK/flutter_guide/build/flutter_assets:Flutter 资源文件。
  • shared_prefs: SharePreferences 的默认路径。
  • app_flutter:对应 getApplicationDocumentsDirectory方法。
  • app_flutter/dbName:使用 sqlite 的默认路径,sqlite 也可以指定位置。

SharePreferencessqlite 是两种保存数据的第三方插件。

内部存储的特点:

  • 安全性,其他应用无法访问这些数据。
  • 当应用卸载的时候,这些数据也会被删除,避免垃圾文件。
  • 不需要申请额外权限。
  • 存储的空间有限,此目录数据随时可能被系统清除,也可以通过 设置 中的 清除数据 可以清除此目录数据。
  • 国内特色,不同手机厂商对此目录做了不同的限制,比如总体大小限制、单个应用程序所占空间大小限制、清除数据策略不同等。

外部存储

外部存储可以通过手机的 文件管理 应用查看,

这里面有一个特殊的目录:Android/data/包名:

看到这个目录是不是觉得和内部存储目录非常相似,一个包名代表一个应用程序:

  • cache:缓存目录,对应 getExternalCacheDirectories 方法。
  • files:对应 getExternalStorageDirectories 方法。

此目录的特点:

  • 当应用卸载的时候,这些数据也会被删除,避免垃圾文件。
  • 不需要申请额外权限。
  • 空间大且不会被系统清除,通过 设置 中的 清除数据 可以清除此目录数据。
  • 用户可以直接对文件进行删除、导入操作。

外部存储除了 Android/data/ 目录,还有和此目录同级的目录,特点:

  • 所有应用程序均可访问。
  • 用户可以直接对文件进行删除、导入操作。
  • 需要申请读写权限

Android 官方对此目录的管理越来越严格, Android 11 系统已经开始强制执行分区存储,详情见:https://developer.android.com/preview/privacy/storage?hl=zh-cn

上面说了这么多,总结如下:

  • SharePreferencessqlite 数据建议存放在内部存储,插件已经帮我们完成了,无需手动处理。
  • 严格保密的数据,比如用户数据,建议存放在内部存储,对应 getApplicationSupportDirectory 方法。
  • 其余所有的数据建议存放 Android/data/包名/ ,对应 getExternalCacheDirectoriesgetExternalStorageDirectories 方法。

iOS 文件存储

iOS 文件存储相比 Android 要简单的多,因为 iOS 对用户隐私保护非常严格,每个 iOS 应用程序都有一个单独的文件系统,而且只能在对应的文件系统中进行操作,此区域被称为沙盒。

每个应用沙盒含有3个文件夹:Documents, Library 和 tmp:

  • Documents:应用程序数据文件写入到这个目录下。这个目录用于存储用户数据。保存应用程序的重要数据文件和用户数据文件等。iTunes 同步时会备份该目录,对应 getApplicationDocumentsDirectory 方法。
  • Library:对应 getLibraryDirectory 方法。
    • Caches:保存应用程序使用时产生的支持文件、缓存文件、日志文件等,比如下载的音乐,视频,SDWebImage缓存等。对应 getTemporaryDirectory 方法。
    • Preferences:包含应用程序的偏好设置文件,iCloud会备份设置信息。
    • Application Support:对应 getApplicationSupportDirectory 方法。
  • tmp:存放临时文件,不会被备份,而且这个文件下的数据有可能随时被清除的可能,按照官方说法每三天清理一次缓存数据。

path_provider 使用

import 'dart:io';

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

///
/// desc:
///

class PathProviderDemo extends StatefulWidget {
  @override
  _PathProviderDemoState createState() => _PathProviderDemoState();
}

class _PathProviderDemoState extends State<PathProviderDemo> {
  Future<Directory> _tempDirectory;
  Future<Directory> _appSupportDirectory;
  Future<Directory> _appLibraryDirectory;
  Future<Directory> _appDocumentsDirectory;
  Future<Directory> _externalStorageDirectory;
  Future<List<Directory>> _externalStorageDirectories;
  Future<List<Directory>> _externalCacheDirectories;
  Future<Directory> _downloadDirectory;

  @override
  void initState() {
    super.initState();
    setState(() {
      _tempDirectory = getTemporaryDirectory();
      _appSupportDirectory = getApplicationSupportDirectory();
      _appLibraryDirectory = getLibraryDirectory();
      _appDocumentsDirectory = getApplicationDocumentsDirectory();
      _externalStorageDirectory = getExternalStorageDirectory();
      _externalCacheDirectories = getExternalCacheDirectories();
      _externalStorageDirectories = getExternalStorageDirectories();
      _downloadDirectory = getDownloadsDirectory();
    });
  }

  Widget _buildDirectory(
      BuildContext context, AsyncSnapshot<Directory> snapshot) {
    Text text = const Text('');
    if (snapshot.connectionState == ConnectionState.done) {
      if (snapshot.hasError) {
        text = Text('Error: ${snapshot.error}');
      } else if (snapshot.hasData) {
        text = Text('path: ${snapshot.data.path}');
      } else {
        text = const Text('path unavailable');
      }
    }
    return Padding(padding: EdgeInsets.symmetric(horizontal: 16), child: text);
  }

  Widget _buildDirectories(
      BuildContext context, AsyncSnapshot<List<Directory>> snapshot) {
    Text text = const Text('');
    if (snapshot.connectionState == ConnectionState.done) {
      if (snapshot.hasError) {
        text = Text('Error: ${snapshot.error}');
      } else if (snapshot.hasData) {
        final String combined =
            snapshot.data.map((Directory d) => d.path).join(', ');
        text = Text('paths: $combined');
      } else {
        text = const Text('path unavailable');
      }
    }
    return Padding(
        padding: const EdgeInsets.symmetric(horizontal: 16), child: text);
  }

  Widget _buildItem(String title, Future<Directory> future) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16),
          child: Text(title),
        ),
        FutureBuilder<Directory>(future: future, builder: _buildDirectory),
      ],
    );
  }

  Widget _buildItem1(String title, Future<List<Directory>> future) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16),
          child: Text(title),
        ),
        FutureBuilder<List<Directory>>(
            future: future,
            builder: _buildDirectories),
      ],
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: ListView(
          itemExtent: 120,
          children: <Widget>[
            _buildItem('getTemporaryDirectory', _tempDirectory),
            _buildItem('getApplicationSupportDirectory', _appSupportDirectory),
            _buildItem('getLibraryDirectory', _appLibraryDirectory),
            _buildItem(
                'getApplicationDocumentsDirectory', _appDocumentsDirectory),
            _buildItem(
                'getExternalStorageDirectory', _externalStorageDirectory),
            _buildItem('getDownloadsDirectory', _downloadDirectory),

            _buildItem1('getExternalStorageDirectories',_externalStorageDirectories),
            _buildItem1('getExternalCacheDirectories',_externalCacheDirectories),

          ],
        ),
      ),
    );
  }
}

Android 系统各个路径:

iOS 系统各个路径:

交流

老孟Flutter博客(330个控件用法+实战入门系列文章):http://laomengit.com

欢迎加入 Flutter威信交流群 :laomengit

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,204评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,091评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,548评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,657评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,689评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,554评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,302评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,216评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,661评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,851评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,977评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,697评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,306评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,898评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,019评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,138评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,927评论 2 355