flutter_handle_file 0.2.0

  • Readme
  • Changelog
  • Example
  • Installing
  • 56

Flutter Handle File #

Travis' Continuous Integration build status

A Flutter plugin project to help with associating files with your app and handling the opening of such files.

Make sure you read both the Installation and the Usage guides.

This work is more than heavily derived from https://github.com/avioli/uni_links.

Installation #

To use the plugin, add flutter_handle_file as a dependency in your pubspec.yaml file.

Permission #

Android will need to be able to read and write from local storage. You need to add the following permissions to your manifest:

<uses-permission android:name="android.permission.WRITE_INTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

Setup #

The plugin can add the required entries both in your AndroidManifest.xml and Info.plist files. In order to do this, you need to add a specific flutter_handle_file section to your pubspec.yaml file:

flutter_handle_file:
  bundle_identifier: <iOS bundle identifier>
  bundle_type_name: <Description of your file (iOS only)>
  extensions:
    - <extension1>: <associated mime type>
    - <extension2>: <associated mime type>

In most cases you should be able to use $(PRODUCT_BUNDLE_IDENTIFIER) for the bundle_identifier key.

For instance, your final configuration could be:

flutter_handle_file:
  bundle_identifier: $(PRODUCT_BUNDLE_IDENTIFIER)
  bundle_type_name: Portable Document Format
  extensions:
    - pdf: application/pdf

Once this is ready, you can ask flutter_handle_file to add the appropriate entries in AndroidManifest.xml and Info.plist:

flutter pub run flutter_handle_file:main

Specific platform configuration #

You can also specify the in_place option for iOS. By default the LSSupportsOpeningDocumentsInPlace key will be created with the false value. Please check this page for more details about this value.

Usage #

There are two ways your app will receive a file - from cold start and brought.

Initial File #

Returns the link that the app was started with, if any.

import 'dart:async';
import 'dart:io';

import 'package:flutter_handle_file/flutter_handle_file.dart';

// ...

  Future<Null> initHandleFile() async {
    // Platform messages may fail, so we use a try/catch PlatformException.
    try {
      String initialFile = await getInitialFile();
      if (initialFile != null) {
        // do something with the file
      }
    } on PlatformException {
      // Handle exception by warning the user their action did not succeed
      // return?
    }
  }

// ...

On change event (String) #

Usually you would check the getInitialFile and also listen for changes.

import 'dart:async';
import 'dart:io';

import 'package:flutter_handle_file/flutter_handle_file.dart';

// ...

  StreamSubscription _sub;

  Future<Null> initHandleFile() async {
    // ... check initialFile

    // Attach a listener to the stream
    _sub = getFilesStream().listen((String link) {
      if (file != null) {
        // do something with the file
      }
    }, onError: (err) {
      // Handle exception by warning the user their action did not succeed
    });

    // NOTE: Don't forget to call _sub.cancel() in dispose()
  }

// ...

If the app was terminated (or rather not running in the background) and the OS must start it anew - that's a cold start. In that case, getInitialFile will have the link that started your app and the Stream won't produce a link (at that point in time).

Alternatively - if the app was running in the background and the OS must bring it to the foreground the Stream will be the one to produce the link, while getInitialFile will be either null, or the initial link, with which the app was started.

Because of these two situations - you should always add a check for the initial link (or URI) and also subscribe for a Stream of links (or URIs).

Tools for launching files #

Android #

On Android, you need to use adb to push a local file to the device.

adb push <local_file> /sdcard/

You can then use the Files application on the device (or emulator) to click on the file.

iOS #

Assuming you've got Xcode already installed:

/usr/bin/xcrun simctl openurl booted "file://<local_file>"

If you've got xcrun (or simctl) in your path, you could invoke it directly.

The flag booted assumes an open simulator (you can start it via open -a Simulator) with a booted device. You could target specific device by specifying its UUID (found via xcrun simctl list or flutter devices), replacing the booted flag.

Contributing #

For help on editing plugin code, view the documentation.

[0.2.0] - 2020-04-20 #

  • Support for in-place flag for iOS
  • Info.plist update with delimiters as XCode can strip them

[0.1.2] - 2020-04-17 #

  • Flutter format

[0.1.1] - 2020-04-17 #

  • Description fix
  • String/Uri fixes
  • Example project

[0.1.0] - 2020-04-16 #

  • Initial release.

example/lib/main.dart

import 'dart:async';
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_handle_file/flutter_handle_file.dart';

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

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

class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
  String _latestFile = 'Unknown';
  Uri _latestUri;

  StreamSubscription _sub;

  final List<String> _cmds = getCmds();
  final TextStyle _cmdStyle = const TextStyle(
    fontFamily: 'Courier',
    fontSize: 12.0,
    fontWeight: FontWeight.w700,
  );
  final _scaffoldKey = new GlobalKey<ScaffoldState>();

  @override
  initState() {
    super.initState();
    initPlatformState();
  }

  @override
  dispose() {
    if (_sub != null) _sub.cancel();
    super.dispose();
  }

  // Platform messages are asynchronous, so we initialize in an async method.
  initPlatformState() async {
    await initPlatformStateForStringHandleFile();
    await initPlatformStateForUriHandleFile();
  }

  /// An implementation using a [String] link
  initPlatformStateForStringHandleFile() async {
    // Attach a listener to the links stream
    _sub = getFilesStream().listen((String file) {
      if (!mounted) return;
      setState(() {
        _latestFile = file ?? 'Unknown';
      });
    }, onError: (err) {
      if (!mounted) return;
      setState(() {
        _latestFile = 'Failed to get latest link: $err.';
      });
    });

    // Attach a second listener to the stream
    getFilesStream().listen((String link) {
      print('got link: $link');
    }, onError: (err) {
      print('got err: $err');
    });

    // Get the latest link
    String initialFile;
    Uri initialUri;
    // Platform messages may fail, so we use a try/catch PlatformException.
    try {
      initialFile = await getInitialFile();
      initialUri = await getInitialUri();
      print('initial link: $initialFile');
    } on PlatformException {
      initialFile = 'Failed to get initial link.';
      initialUri = null;
    } on FormatException {
      initialFile = 'Failed to parse the initial link as Uri.';
      initialUri = null;
    }

    // If the widget was removed from the tree while the asynchronous platform
    // message was in flight, we want to discard the reply rather than calling
    // setState to update our non-existent appearance.
    if (!mounted) return;

    setState(() {
      _latestFile = initialFile;
      _latestUri = initialUri;
    });
  }

  /// An implementation using the [Uri] convenience helpers
  initPlatformStateForUriHandleFile() async {
    // Attach a listener to the Uri links stream
    _sub = getUriFilesStream().listen((Uri uri) {
      if (!mounted) return;
      setState(() {
        _latestUri = uri;
      });
    }, onError: (err) {
      if (!mounted) return;
      setState(() {
        _latestUri = null;
      });
    });

    // Attach a second listener to the stream
    getUriFilesStream().listen((Uri uri) {
      print('got uri: ${uri?.path} ${uri?.queryParametersAll}');
    }, onError: (err) {
      print('got err: $err');
    });

    // Get the latest Uri
    Uri initialUri;
    String initialFile;
    // Platform messages may fail, so we use a try/catch PlatformException.
    try {
      initialUri = await getInitialUri();
      print('initial uri: ${initialUri?.path}'
          ' ${initialUri?.queryParametersAll}');
      initialFile = await getInitialFile();
    } on PlatformException {
      initialUri = null;
      initialFile = 'Failed to get initial uri.';
    } on FormatException {
      initialUri = null;
      initialFile = 'Bad parse the initial link as Uri.';
    }

    // If the widget was removed from the tree while the asynchronous platform
    // message was in flight, we want to discard the reply rather than calling
    // setState to update our non-existent appearance.
    if (!mounted) return;

    setState(() {
      _latestUri = initialUri;
      _latestFile = initialFile;
    });
  }

  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      home: new Scaffold(
        key: _scaffoldKey,
        appBar: new AppBar(
          title: new Text('Plugin example app'),
        ),
        body: new ListView(
          shrinkWrap: true,
          padding: const EdgeInsets.all(8.0),
          children: <Widget>[
            new ListTile(
              title: const Text('String'),
              subtitle: new Text('$_latestFile'),
            ),
            new ListTile(
              title: const Text('Uri Path'),
              subtitle: new Text('${_latestUri.toString()}'),
            ),
            _cmdsCard(_cmds),
          ],
        ),
      ),
    );
  }

  Widget _cmdsCard(commands) {
    Widget platformCmds;

    if (commands == null) {
      platformCmds = const Center(
        child: const Text('Unsupported platform'),
      );
    } else {
      platformCmds = new Column(
        children: <List<Widget>>[
          [
            const Text(
                'To populate above fields open a terminal shell and run:\n'),
          ],
          intersperse(
            commands.map<Widget>(
              (cmd) => new InkWell(
                onTap: () => _printAndCopy(cmd),
                child: new Text(
                  '\n$cmd\n',
                  style: _cmdStyle,
                ),
              ),
            ),
            const Text('or'),
          ),
          [
            new Text(
                '(tap on any of the above commands to print it to'
                ' the console/logger and copy to the device clipboard.)',
                textAlign: TextAlign.center,
                style: Theme.of(context).textTheme.caption),
          ]
        ].expand((el) => el).toList(),
      );
    }

    return new Card(
      margin: const EdgeInsets.only(top: 20.0),
      child: new Padding(
        padding: const EdgeInsets.all(10.0),
        child: platformCmds,
      ),
    );
  }

  _printAndCopy(String cmd) async {
    print(cmd);
    await Clipboard.setData(new ClipboardData(text: cmd));
    _scaffoldKey.currentState.showSnackBar(new SnackBar(
      content: const Text('Copied to Clipboard'),
    ));
  }
}

List<String> getCmds() {
  if (Platform.isIOS) {
    return [
      '/usr/bin/xcrun simctl openurl booted "file://\$(pwd)/data/test.pdf"'
    ];
  } else if (Platform.isAndroid) {
    return ['adb push ./data/test.pdf /sdcard', 'Use Files app on device'];
  } else {
    return null;
  }
}

List<Widget> intersperse(Iterable<Widget> list, Widget item) {
  List<Widget> initialValue = [];
  return list.fold(initialValue, (all, el) {
    if (all.length != 0) all.add(item);
    all.add(el);
    return all;
  });
}

Use this package as a library

1. Depend on it

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


dependencies:
  flutter_handle_file: ^0.2.0

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:flutter_handle_file/flutter_handle_file.dart';
  
Popularity:
Describes how popular the package is relative to other packages. [more]
12
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]
56
Learn more about scoring.

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

  • Dart: 2.8.4
  • pana: 0.13.15
  • Flutter: 1.17.5

Analysis suggestions

Package does not support Flutter platform Linux

Because:

  • package:flutter_handle_file/flutter_handle_file.dart that declares support for platforms: Android, iOS

Package does not support Flutter platform Web

Because:

  • package:flutter_handle_file/flutter_handle_file.dart that declares support for platforms: Android, iOS

Package does not support Flutter platform Windows

Because:

  • package:flutter_handle_file/flutter_handle_file.dart that declares support for platforms: Android, iOS

Package does not support Flutter platform macOS

Because:

  • package:flutter_handle_file/flutter_handle_file.dart that declares support for platforms: Android, iOS

Package not compatible with SDK dart

Because:

  • flutter_handle_file that is a package requiring null.

Health suggestions

Format lib/install.dart.

Run flutter format to format lib/install.dart.

Format lib/string_templates.dart.

Run flutter format to format lib/string_templates.dart.

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=2.0.0-dev.28.0 <3.0.0
flutter 0.0.0
yaml ^2.1.16 2.2.1
Transitive dependencies
charcode 1.1.3
collection 1.14.12 1.14.13
meta 1.1.8 1.2.2
path 1.7.0
sky_engine 0.0.99
source_span 1.7.0
string_scanner 1.0.5
term_glyph 1.1.0
typed_data 1.1.6 1.2.0
vector_math 2.0.8 2.1.0-nullsafety
Dev dependencies
flutter_test