Flutter文件选择及可写操作插件file_picker_writable的使用

Flutter 文件选择及可写操作插件 file_picker_writable 的使用

概述

file_picker_writable 是一个用于在 Flutter 应用程序中选择文件并进行读写操作的插件。它支持持久权限(在 Android 上)和安全书签(在 iOS 上),允许用户打开关联文件或处理任意 URL。

要求

iOS

  • 需要 iOS 8 及以上版本,Swift 5。
  • 目前仅在 iOS 13+ 上进行了测试,请告知其他版本的测试结果。
支持文件处理器
  1. 配置 OTI 类型:参考文档
  2. Info.plist 文件中添加以下配置:
    <key>UISupportsDocumentBrowser</key>
    <false/>
    <key>LSSupportsOpeningDocumentsInPlace</key>
    <true/>
    

Android

  • 需要 Android 4.4 (API Level 19) 及以上版本。
  • 仅支持插件 API v2。
支持文件处理器
  1. AndroidManifest.xml 中添加以下配置:
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="file" />
        <data android:scheme="content" />
        <data android:host="*" />
        <data android:mimeType="*/*" />
        <!-- https://stackoverflow.com/a/52384331/109219 ?? -->
        <data android:pathPattern=".*\.codeux" />
        <data android:pathPattern=".*\..*\codeux" />
        <data android:pathPattern=".*\..*\..*\codeux" />
        <data android:pathPattern=".*\..*\..*\..*\codeux" />
        <data android:pathPattern=".*\..*\..*\..*\..*\codeux" />
        <data android:pathPattern=".*\..*\..*\..*\.*\..*\codeux" />
    </intent-filter>
    

MacOS

  • 当前不支持。

开始使用

首先,确保你已经在项目中添加了 file_picker_writable 插件:

dependencies:
  file_picker_writable: ^版本号

然后运行 flutter pub get 来获取依赖项。

示例代码

以下是一个简单的示例,展示了如何使用 file_picker_writable 插件来打开文件、创建新文件,并进行读写操作。

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';

import 'package:convert/convert.dart';
import 'package:file_picker_writable/file_picker_writable.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:logging_appenders/logging_appenders.dart';
import 'package:simple_json_persistence/simple_json_persistence.dart';

final _logger = Logger('main');

Future<void> main() async {
  Logger.root.level = Level.ALL;
  PrintAppender().attachToLogger(Logger.root);

  runApp(const MyApp());
}

class AppDataBloc {
  final store = SimpleJsonPersistence.getForTypeWithDefault(
    (json) => AppData.fromJson(json),
    defaultCreator: () => AppData(files: []),
    name: 'AppData',
  );
}

class AppData implements HasToJson {
  AppData({required this.files});
  final List<FileInfo> files;

  static AppData fromJson(Map<String, dynamic> json) => AppData(
        files: (json['files'] as List<dynamic>)
            .where((dynamic element) => element != null)
            .map((dynamic e) => FileInfo.fromJson(e as Map<String, dynamic>))
            .toList(),
      );

  [@override](/user/override)
  Map<String, dynamic> toJson() => {'files': files};

  AppData copyWith({required List<FileInfo> files}) => AppData(files: files);
}

class MyApp extends StatefulWidget {
  const MyApp({Key? key}) : super(key: key);

  [@override](/user/override)
  MyAppState createState() => MyAppState();
}

class MyAppState extends State<MyApp> {
  final AppDataBloc _appDataBloc = AppDataBloc();

  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MainScreen(
        appDataBloc: _appDataBloc,
      ),
    );
  }
}

class MainScreen extends StatefulWidget {
  const MainScreen({Key? key, required this.appDataBloc}) : super(key: key);
  final AppDataBloc appDataBloc;

  [@override](/user/override)
  MainScreenState createState() => MainScreenState();
}

class MainScreenState extends State<MainScreen> {
  AppDataBloc get _appDataBloc => widget.appDataBloc;

  [@override](/user/override)
  void initState() {
    super.initState();
    final state = FilePickerWritable().init();
    state.registerFileOpenHandler((fileInfo, file) async {
      _logger.fine('got file info. we are mounted:$mounted');
      if (!mounted) {
        return false;
      }
      await SimpleAlertDialog.readFileContentsAndShowDialog(
        fileInfo,
        file,
        context,
        bodyTextPrefix: 'Should open file from external app.\n\n'
            'fileName: ${fileInfo.fileName}\n'
            'uri: ${fileInfo.uri}\n\n\n',
      );
      return true;
    });
    state.registerUriHandler((uri) {
      SimpleAlertDialog(
        titleText: 'Handling Uri',
        bodyText: 'Got a uri to handle: $uri',
      ).show(context);
      return true;
    });
    state.registerErrorEventHandler((errorEvent) async {
      _logger.fine('Handling error event, mounted: $mounted');
      if (!mounted) {
        return false;
      }
      await SimpleAlertDialog(
        titleText: 'Received error event',
        bodyText: errorEvent.message,
      ).show(context);
      return true;
    });
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('File Picker Example'),
      ),
      body: SingleChildScrollView(
        child: SizedBox(
          width: double.infinity,
          child: StreamBuilder<AppData>(
            stream: _appDataBloc.store.onValueChangedAndLoad,
            builder: (context, snapshot) => Column(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: [
                Wrap(
                  children: [
                    ElevatedButton(
                      onPressed: _openFilePicker,
                      child: const Text('Open File Picker'),
                    ),
                    const SizedBox(width: 32),
                    ElevatedButton(
                      onPressed: _openFilePickerForCreate,
                      child: const Text('Create New File'),
                    ),
                    const SizedBox(width: 32),
                    ElevatedButton(
                      onPressed: FilePickerWritable().disposeAllIdentifiers,
                      child: const Text('Dispose All IDs'),
                    ),
                  ],
                ),
                ...?(!snapshot.hasData
                    ? null
                    : snapshot.data!.files.map((fileInfo) => FileInfoDisplay(
                          fileInfo: fileInfo,
                          appDataBloc: _appDataBloc,
                        ))),
              ],
            ),
          ),
        ),
      ),
    );
  }

  Future<void> _openFilePicker() async {
    final fileInfo = await FilePickerWritable().openFile((fileInfo, file) async {
      _logger.fine('Got picker result: $fileInfo');
      final data = await _appDataBloc.store.load();
      await _appDataBloc.store
          .save(data.copyWith(files: data.files + [fileInfo]));
      return fileInfo;
    });
    if (fileInfo == null) {
      _logger.fine('User cancelled.');
    }
  }

  Future<void> _openFilePickerForCreate() async {
    final rand = Random().nextInt(10000000);
    final fileInfo = await FilePickerWritable().openFileForCreate(
      fileName: 'newfile.$rand.codeux',
      writer: (file) async {
        final content = 'File created at ${DateTime.now()}\n\n';
        await file.writeAsString(content);
      },
    );
    if (fileInfo == null) {
      _logger.info('User cancelled.');
      return;
    }
    final data = await _appDataBloc.store.load();
    await _appDataBloc.store
        .save(data.copyWith(files: data.files + [fileInfo]));
  }
}

class FileInfoDisplay extends StatelessWidget {
  const FileInfoDisplay({
    Key? key,
    required this.fileInfo,
    required this.appDataBloc,
  }) : super(key: key);

  final AppDataBloc appDataBloc;
  final FileInfo fileInfo;

  [@override](/user/override)
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Card(
        elevation: 2,
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            children: [
              const Text('Selected File:'),
              Text(
                fileInfo.fileName ?? 'null',
                maxLines: 4,
                overflow: TextOverflow.ellipsis,
                style: theme.textTheme.bodySmall?.apply(fontSizeFactor: 0.75),
              ),
              Text(
                fileInfo.identifier,
                maxLines: 1,
                overflow: TextOverflow.ellipsis,
              ),
              Text(
                'uri:${fileInfo.uri}',
                style: theme.textTheme.bodyMedium
                    ?.apply(fontSizeFactor: 0.7)
                    .copyWith(fontWeight: FontWeight.bold),
              ),
              Text(
                'fileName: ${fileInfo.fileName}',
                style: theme.textTheme.bodyMedium
                    ?.apply(fontSizeFactor: 0.7)
                    .copyWith(fontWeight: FontWeight.bold),
              ),
              ButtonBar(
                alignment: MainAxisAlignment.end,
                children: [
                  TextButton(
                    onPressed: () async {
                      try {
                        await FilePickerWritable().readFile(
                            identifier: fileInfo.identifier,
                            reader: (fileInfo, file) async {
                              await SimpleAlertDialog.readFileContentsAndShowDialog(
                                  fileInfo, file, context);
                            });
                      } on Exception catch (e) {
                        if (!context.mounted) {
                          return;
                        }
                        await SimpleAlertDialog.showErrorDialog(e, context);
                      }
                    },
                    child: const Text('Read'),
                  ),
                  TextButton(
                    onPressed: () async {
                      await FilePickerWritable().writeFile(
                          identifier: fileInfo.identifier,
                          writer: (file) async {
                            final content =
                                'New Content written at ${DateTime.now()}.\n\n';
                            await file.writeAsString(content);
                            // ignore: use_build_context_synchronously
                            await SimpleAlertDialog(
                              bodyText: 'Written: $content',
                            ).show(context);
                          });
                    },
                    child: const Text('Overwrite'),
                  ),
                  IconButton(
                    onPressed: () async {
                      try {
                        await FilePickerWritable()
                            .disposeIdentifier(fileInfo.identifier);
                      } on Exception catch (e) {
                        if (!context.mounted) {
                          return;
                        }
                        await SimpleAlertDialog.showErrorDialog(e, context);
                      }
                      final appData = await appDataBloc.store.load();
                      await appDataBloc.store.save(appData.copyWith(
                          files: appData.files
                              .where((element) => element != fileInfo)
                              .toList()));
                    },
                    icon: const Icon(Icons.remove_circle_outline),
                  ),
                ],
              )
            ],
          ),
        ),
      ),
    );
  }
}

class SimpleAlertDialog extends StatelessWidget {
  const SimpleAlertDialog({Key? key, this.titleText, required this.bodyText})
      : super(key: key);
  final String? titleText;
  final String bodyText;

  Future<void> show(BuildContext context) =>
      showDialog<void>(context: context, builder: (context) => this);

  [@override](/user/override)
  Widget build(BuildContext context) {
    return AlertDialog(
      scrollable: true,
      title: titleText == null ? null : Text(titleText!),
      content: Text(bodyText),
      actions: [
        TextButton(
            child: const Text('Ok'),
            onPressed: () {
              Navigator.of(context).pop();
            }),
      ],
    );
  }

  static Future<void> readFileContentsAndShowDialog(
    FileInfo fi,
    File file,
    BuildContext context, {
    String bodyTextPrefix = '',
  }) async {
    final dataList = await file.openRead(0, 64).toList();
    final data = dataList.expand((element) => element).toList();
    final hexString = hex.encode(data);
    final utf8String = utf8.decode(data, allowMalformed: true);
    final fileContentExample = 'hexString: $hexString\n\nutf8: $utf8String';

    // ignore: use_build_context_synchronously
    await SimpleAlertDialog(
      titleText: 'Read first ${data.length} bytes of file',
      bodyText: '$bodyTextPrefix $fileContentExample',
    ).show(context);
  }

  static Future<void> showErrorDialog(Exception e, BuildContext context) async {
    await SimpleAlertDialog(
      titleText: 'Error',
      bodyText: e.toString(),
    ).show(context);
  }
}

更多关于Flutter文件选择及可写操作插件file_picker_writable的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter文件选择及可写操作插件file_picker_writable的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


file_picker_writable 是一个 Flutter 插件,它允许用户从设备中选择文件,并且还支持在选定的文件中进行写入操作。这个插件适用于需要读取和写入文件的应用场景,如文本编辑器、文件管理器等。

1. 添加依赖

首先,你需要在 pubspec.yaml 文件中添加 file_picker_writable 插件的依赖:

dependencies:
  flutter:
    sdk: flutter
  file_picker_writable: ^1.0.0  # 请查看最新版本

然后运行 flutter pub get 来安装依赖。

2. 导入插件

在你的 Dart 文件中导入插件:

import 'package:file_picker_writable/file_picker_writable.dart';

3. 请求权限

在 Android 上,你可能需要请求读写外部存储的权限。你可以使用 permission_handler 插件来请求权限:

import 'package:permission_handler/permission_handler.dart';

Future<void> requestPermissions() async {
  if (await Permission.storage.request().isGranted) {
    // 权限已授予
  } else {
    // 权限被拒绝
  }
}

4. 选择文件

使用 FilePickerWritable 来让用户选择文件:

Future<void> pickFile() async {
  try {
    final fileInfo = await FilePickerWritable().pickFile();
    if (fileInfo != null) {
      // 用户选择了一个文件,fileInfo 包含文件的信息
      print('Selected file: ${fileInfo.filePath}');
    }
  } catch (e) {
    print('Error picking file: $e');
  }
}

5. 写入文件

你可以使用 FilePickerWritable 来将数据写入用户选择的文件:

Future<void> writeToFile(FileInfo fileInfo, String content) async {
  try {
    await FilePickerWritable().writeToFile(fileInfo, content);
    print('Content written to file successfully.');
  } catch (e) {
    print('Error writing to file: $e');
  }
}

6. 示例代码

以下是一个完整的示例代码,展示了如何选择文件并将内容写入文件:

import 'package:flutter/material.dart';
import 'package:file_picker_writable/file_picker_writable.dart';
import 'package:permission_handler/permission_handler.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      home: FilePickerDemo(),
    );
  }
}

class FilePickerDemo extends StatefulWidget {
  [@override](/user/override)
  _FilePickerDemoState createState() => _FilePickerDemoState();
}

class _FilePickerDemoState extends State<FilePickerDemo> {
  FileInfo? _selectedFile;

  Future<void> requestPermissions() async {
    if (await Permission.storage.request().isGranted) {
      print('Permissions granted');
    } else {
      print('Permissions denied');
    }
  }

  Future<void> pickFile() async {
    try {
      final fileInfo = await FilePickerWritable().pickFile();
      if (fileInfo != null) {
        setState(() {
          _selectedFile = fileInfo;
        });
        print('Selected file: ${fileInfo.filePath}');
      }
    } catch (e) {
      print('Error picking file: $e');
    }
  }

  Future<void> writeToFile(String content) async {
    if (_selectedFile == null) return;

    try {
      await FilePickerWritable().writeToFile(_selectedFile!, content);
      print('Content written to file successfully.');
    } catch (e) {
      print('Error writing to file: $e');
    }
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('File Picker Writable Demo'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: requestPermissions,
              child: Text('Request Permissions'),
            ),
            ElevatedButton(
              onPressed: pickFile,
              child: Text('Pick File'),
            ),
            ElevatedButton(
              onPressed: () => writeToFile('Hello, World!'),
              child: Text('Write to File'),
            ),
          ],
        ),
      ),
    );
  }
}
回到顶部