简体   繁体   中英

Flutter: How to read file from assets, asynchronously, without blocking the UI

In flutter, rootBundle.load() gives me a ByteData object.

What exactly is a ByteData object in dart? Can It be used to read files asynchronously?

I don't really understand the motive behind this.

Why not just give me a good ol' File object, or better yet the full path of the asset?

In my case, I want to read bytes from an asset file asynchronously, byte by byte and write to a new file. (to build an XOR decryption thingy that doesn't hang up the UI)

This is the best I could do, and it miserably hangs up the UI.

loadEncryptedPdf(fileName, secretKey, cacheDir) async {
  final lenSecretKey = secretKey.length;

  final encryptedByteData = await rootBundle.load('assets/$fileName');

  final outputFilePath = cacheDir + '/' + fileName;
  final outputFile = File(outputFilePath);

  if (!await outputFile.exists()) {
    Stream decrypter() async* {
      // read bits from encryptedByteData, and stream the xor inverted bits

      for (var index = 0; index < encryptedByteData.lengthInBytes; index++)
        yield encryptedByteData.getUint8(index) ^
        secretKey.codeUnitAt(index % lenSecretKey);
      print('done!');
    }

    print('decrypting $fileName using $secretKey ..');
    await outputFile.openWrite(encoding: AsciiCodec()).addStream(decrypter());
    print('finished');
  }

  return outputFilePath;
}

In Dart a ByteData is similar to a Java ByteBuffer . It wraps a byte array, providing getter and setter functions for 1, 2 and 4 byte integers (both endians).

Since you want to manipulate bytes it's easiest to just work on the underlying byte array (a Dart Uint8List ). RootBundle.load() will have already read the whole asset into memory, so change it in memory and write it out.

Future<String> loadEncryptedPdf(
    String fileName, String secretKey, String cacheDir) async {
  final lenSecretKey = secretKey.length;

  final encryptedByteData = await rootBundle.load('assets/$fileName');

  String path = cacheDir + '/' + fileName;
  final outputFile = File(path);

  if (!await outputFile.exists()) {
    print('decrypting $fileName using $secretKey ..');

    Uint8List bytes = encryptedByteData.buffer.asUint8List();
    for (int i = 0; i < bytes.length; i++) {
      bytes[i] ^= secretKey.codeUnitAt(i % lenSecretKey);
    }

    await outputFile.writeAsBytes(bytes);
    print('finished');
  }

  return path;
}

If you are doing work that is expensive and you don't want to block the UI, use the compute method from package:flutter/foundation.dart . This will run the provided function in a separate isolate and return the results to you asynchronously. loadEncryptedPdf must be a top level or static function to use it here though, and you are limited to passing one argument (but you can put them in a Map).

import 'package:flutter/foundation.dart';

Future<String> loadEncryptedPdf(Map<String, String> arguments) async {
  // this runs on another isolate
  ...
} 

final String result = await compute(loadEncryptedPdf, {'fileName': /*.../*});

So, while the answers posted by @Jonah Williams and @Richard Heap don't suffice on their own, I tried a combination of both, and it works good for me.

Here is a complete solution -

uses path_provider package to get the cache directory

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

// Holds a Future to a temporary cache directory
final cacheDirFuture =
    (() async => (await (await getTemporaryDirectory()).createTemp()).path)();

_xorDecryptIsolate(args) {
  Uint8List pdfBytes = args[0].buffer.asUint8List();
  String secretKey = args[1];
  File outputFile = args[2];

  int lenSecretKey = secretKey.length;

  for (int i = 0; i < pdfBytes.length; i++)
    pdfBytes[i] ^= secretKey.codeUnitAt(i % lenSecretKey);

  outputFile.writeAsBytesSync(pdfBytes, flush: true);

}


/// decrypt a file from assets using XOR,
/// and return the path to a cached-temporary decrypted file.
xorDecryptFromAssets(String assetFileName, String secretKey) async {
  final pdfBytesData = await rootBundle.load('assets/$assetFileName');
  final outputFilePath = (await pdfCacheDirFuture) + '/' + assetFileName;
  final outputFile = File(outputFilePath);

  if ((await outputFile.stat()).size > 0) {
    print('decrypting $assetFileName using $secretKey ..');
    await compute(_xorDecryptIsolate, [pdfBytesData, secretKey, outputFile]);
    print('done!');
  }

  return outputFilePath;
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM