file_picker_writable 1.1.1+4

  • Readme
  • Changelog
  • Example
  • Installing
  • 83

Pub Version

file_picker_writable #

Flutter plugin to choose files which can be read, referenced and written back at a later time (persistent permissions on android, secure bookmarks on iOS).

It also offers handlers for open intents when the user wants to open associated files from other apps. In the same way it will also handle arbitrary URLs and pass them back to dart.

Requirements #

iOS #

  • iOS 8 + Swift 5
  • Only tested on iOS 13+, so let me know ;-)

Support for file handlers #

  1. Configure an OTI Type: https://developer.apple.com/library/archive/qa/qa1587/_index.html
  2. Add to plist file:
     <key>UISupportsDocumentBrowser</key>
     <false/>
     <key>LSSupportsOpeningDocumentsInPlace</key>
     <true/>
    

Android #

  • Android 4.4 (API Level 4.4)
  • Only supports plugin api v2.
    • (v1 is implemented but untested)

Support for file handlers #

AndroidManifest.xlm

            <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 #

Is currently not supported. The only thing the plugin will do is listen for URL Events and pass them through to the dart side.

Getting Started #

See the example on how to implement it in a simple application.

Future<void> readFile() async {
  final fileInfo = await FilePickerWritable().openFilePicker();
  _logger.fine('Got picker result: $fileInfo');
  if (fileInfo == null) {
    _logger.info('User canceled.');
    return;
  }
  // now do something useful with the selected file...
  _logger.info('Got file contents in temporary file: ${fileInfo.file}');
  _logger.info('fileName: ${fileInfo.fileName}');
  _logger.info('Identifier which can be persisted for later retrieval:'
      '${fileInfo.identifier}');
}

The returned fileInfo.identifier can be used later to write or read from the data, even after an app restart.

Future<void> persistChanges(FileInfo fileInfo, Uint8List newContent) async {
  // create a new temporary file inside your apps sandbox.
  final File newFile = _createNewTempFile();
  await newFile.writeBytes(newContent);

  // tell FilePickerWritable plugin to write the new contents over the user selected file
  await FilePickerWritable()
     .writeFileWithIdentifier(fileInfo.identifier, newFile);
}

1.1.1+4 #

  • Android: better error handling, which previously might have caused crashes in previous version.
  • iOS: Fix handling of Copy to use case. (ie. imported files, vs. opened files). & cleanup of Inbox folder. Again thanks https://github.com/amake

1.1.1+2 #

1.1.1+1 #

  • iOS: Fix universal links handling.

1.1.1 #

  • Implement the Uri handling part of the plugin for macos.

1.1.0 #

  • Handle All URLs from intents or custom URL schemas, and propagate it to url handler.

1.0.1 #

  • Android: make sure all file operations happen outside the main UI thread.
    • Everything uses coroutines now to correctly dispatch everything to a worker thread.

1.0.0+1 #

  • Improved documentation & comments.
  • Add toJsonString and fromJsonString to FileInfo for easier serialization.
  • Loosen package dependency version constraint for convert package.

1.0.0 #

  • Only handle file urls on iOS and file, content URLs on android.
  • Send native logs to dart to make debugging easier.

1.0.0-rc.2 #

  • Add support for handling "file open" intents on on android and iOS (openUrl).
    • (This will handle all incoming URLs and intents)

1.0.0-rc.1 Feature complete for iOS and Android 🎉️ #

  • Show "Create file" dialog.
  • Show "Open file" dialog.
  • (re)read files using Uri identifier.
  • write new contents to user selected files.

0.0.1 #

  • Initial experiments

example/lib/main.dart

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

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:path_provider/path_provider.dart';
import 'package:simple_json_persistence/simple_json_persistence.dart';
import 'package:convert/convert.dart';
import 'package:path/path.dart' as p;

final _logger = Logger('main');

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

  runApp(MyApp());
}

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

class AppData implements HasToJson {
  AppData({@required this.files}) : assert(files != null);
  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
  Map<String, dynamic> toJson() => <String, dynamic>{
        'files': files,
      };

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

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

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

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

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

  @override
  _MainScreenState createState() => _MainScreenState();
}

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

  @override
  void initState() {
    super.initState();
    final state = FilePickerWritable().init();
    state.registerFileInfoHandler((fileInfo) async {
      _logger.fine('got file info. we are mounted:$mounted');
      if (!mounted) {
        return false;
      }
      await SimpleAlertDialog.readFileContentsAndShowDialog(
        fileInfo,
        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;
    });
  }

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

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

  Future<void> _openFilePickerForCreate() async {
    final tempDirectory = await getTemporaryDirectory();
    final rand = Random().nextInt(10000000);
    final temp = File(p.join(tempDirectory.path, 'newfile.$rand.codeux'));
    final content = 'File created at ${DateTime.now()}\n\n';
    await temp.writeAsString(content);
    final fileInfo = await FilePickerWritable().openFilePickerForCreate(temp);
    if (fileInfo == null) {
      _logger.info('User canceled.');
      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,
  })  : assert(fileInfo != null),
        assert(appDataBloc != null),
        super(key: key);

  final AppDataBloc appDataBloc;
  final FileInfo fileInfo;

  @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: <Widget>[
              const Text('Selected File:'),
              Text(
                fileInfo.file.path,
                maxLines: 4,
                overflow: TextOverflow.ellipsis,
                style: theme.textTheme.caption.apply(fontSizeFactor: 0.75),
              ),
              Text(
                fileInfo.identifier,
                maxLines: 1,
                overflow: TextOverflow.ellipsis,
              ),
              Text(
                'uri:${fileInfo.uri}',
                style: theme.textTheme.bodyText2
                    .apply(fontSizeFactor: 0.7)
                    .copyWith(fontWeight: FontWeight.bold),
              ),
              Text(
                'fileName: ${fileInfo.fileName}',
                style: theme.textTheme.bodyText2
                    .apply(fontSizeFactor: 0.7)
                    .copyWith(fontWeight: FontWeight.bold),
              ),
              ButtonBar(
                alignment: MainAxisAlignment.end,
                children: <Widget>[
                  FlatButton(
                    onPressed: () async {
                      final fi = fileInfo.file.existsSync()
                          ? fileInfo
                          : await FilePickerWritable()
                              .readFileWithIdentifier(fileInfo.identifier);
                      await SimpleAlertDialog.readFileContentsAndShowDialog(
                          fi, context);
                    },
                    child: const Text('Read'),
                  ),
                  FlatButton(
                    onPressed: () async {
                      final fi = fileInfo.file.existsSync()
                          ? fileInfo
                          : await FilePickerWritable()
                              .readFileWithIdentifier(fileInfo.identifier);
                      final content =
                          'New Content written at ${DateTime.now()}.\n\n';
                      await fi.file.writeAsString(content);
                      await FilePickerWritable()
                          .writeFileWithIdentifier(fi.identifier, fi.file);
                      SimpleAlertDialog(
                        bodyText: 'Wriitten: $content',
                      ).show(context);
                    },
                    child: const Text('Overwrite'),
                  ),
                  IconButton(
                    onPressed: () async {
                      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, this.bodyText})
      : super(key: key);
  final String titleText;
  final String bodyText;

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

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

  static Future readFileContentsAndShowDialog(
    FileInfo fi,
    BuildContext context, {
    String bodyTextPrefix = '',
  }) async {
    final dataList = await fi.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';
    SimpleAlertDialog(
      titleText: 'Read first ${data.length} bytes of file',
      bodyText: '$bodyTextPrefix $fileContentExample',
    ).show(context);
  }
}

Use this package as a library

1. Depend on it

Add this to your package's pubspec.yaml file:


dependencies:
  file_picker_writable: ^1.1.1+4

2. Install it

You can install packages from the command line:

with Flutter:


$ flutter pub get

Alternatively, your editor might support flutter pub get. Check the docs for your editor to learn more.

3. Import it

Now in your Dart code, you can use:


import 'package:file_picker_writable/file_picker_writable.dart';
  
Popularity:
Describes how popular the package is relative to other packages. [more]
65
Health:
Code health derived from static analysis. [more]
100
Maintenance:
Reflects how tidy and up-to-date the package is. [more]
100
Overall:
Weighted score of the above. [more]
83
Learn more about scoring.

We analyzed this package on Jul 7, 2020, and provided a score, details, and suggestions below. Analysis was completed with status completed using:

  • Dart: 2.8.4
  • pana: 0.13.13
  • Flutter: 1.17.5

Analysis suggestions

Package does not support Flutter platform linux

Because of import path [package:file_picker_writable/file_picker_writable.dart] that declares support for platforms: android, ios, macos

Package does not support Flutter platform web

Because of import path [package:file_picker_writable/file_picker_writable.dart] that declares support for platforms: android, ios, macos

Package does not support Flutter platform windows

Because of import path [package:file_picker_writable/file_picker_writable.dart] that declares support for platforms: android, ios, macos

Package not compatible with SDK dart

because of import path [file_picker_writable] that is in a package requiring null.

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=2.7.0 <3.0.0
convert >=2.1.0 <3.0.0 2.1.1
flutter 0.0.0
logging >=0.11.0 <1.0.0 0.11.4
synchronized >=2.2.0 <3.0.0 2.2.0+1
Transitive dependencies
charcode 1.1.3
collection 1.14.12 1.14.13
meta 1.1.8 1.2.1
sky_engine 0.0.99
typed_data 1.1.6 1.2.0
vector_math 2.0.8
Dev dependencies
flutter_test
pedantic ^1.9.0