From f70c79c7957e6731aed9e53a82f1e6e53d033a7c Mon Sep 17 00:00:00 2001 From: Victor Carreras <34163765+vicajilau@users.noreply.github.com> Date: Mon, 5 Oct 2026 17:50:43 +0200 Subject: [PATCH 1/6] Migrate native platforms to cross_file 0.4.0 cross_file 0.4.0 removes the XFile(path, name:, bytes:) constructor and resolves every XFile through a registered CrossFilePlatform. The Android, Darwin, Linux and Windows platform files now build their XFile lazily with XFile.fileSystem for file URIs and XFile.scopedStorage otherwise, instead of eagerly in their factories. The unused bytes parameter of LinuxPlatformFile.fromPath and WindowsPlatformFile.fromPath is removed. cross_file 0.4.0 also raises the minimums to Flutter 3.41, Dart 3.11, Android SDK 24 and macOS 10.15, so the constraints, minSdk and deployment targets follow. Each package gets a test that reads a real temporary file through cross_file_io. Refs #2224 --- example/pubspec.yaml | 2 +- packages/file_picker/pubspec.yaml | 6 +-- .../android/build.gradle.kts | 2 +- .../lib/src/android_platform_file.dart | 14 +++---- packages/file_picker_android/pubspec.yaml | 7 ++-- .../test/platform_file_read_test.dart | 38 +++++++++++++++++++ .../darwin/file_picker_darwin.podspec | 2 +- .../darwin/file_picker_darwin/Package.swift | 2 +- .../lib/src/darwin_platform_file.dart | 14 +++---- packages/file_picker_darwin/pubspec.yaml | 7 ++-- .../test/platform_file_read_test.dart | 38 +++++++++++++++++++ .../lib/src/linux_platform_file.dart | 22 ++++------- packages/file_picker_linux/pubspec.yaml | 7 ++-- .../test/file_picker_linux_test.dart | 9 ++--- .../test/platform_file_read_test.dart | 35 +++++++++++++++++ .../pubspec.yaml | 6 +-- packages/file_picker_web/pubspec.yaml | 6 +-- .../lib/src/windows_platform_file.dart | 22 ++++------- packages/file_picker_windows/pubspec.yaml | 7 ++-- .../test/file_picker_windows_test.dart | 8 ++-- .../test/platform_file_read_test.dart | 35 +++++++++++++++++ pubspec.yaml | 4 +- 22 files changed, 209 insertions(+), 84 deletions(-) create mode 100644 packages/file_picker_android/test/platform_file_read_test.dart create mode 100644 packages/file_picker_darwin/test/platform_file_read_test.dart create mode 100644 packages/file_picker_linux/test/platform_file_read_test.dart create mode 100644 packages/file_picker_windows/test/platform_file_read_test.dart diff --git a/example/pubspec.yaml b/example/pubspec.yaml index b766b9a9..0a48c46f 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -5,7 +5,7 @@ publish_to: 'none' resolution: workspace environment: - sdk: ">=3.10.0 <4.0.0" + sdk: ">=3.11.0 <4.0.0" formatter: page_width: 80 diff --git a/packages/file_picker/pubspec.yaml b/packages/file_picker/pubspec.yaml index 964c09b6..513d14c2 100644 --- a/packages/file_picker/pubspec.yaml +++ b/packages/file_picker/pubspec.yaml @@ -14,8 +14,8 @@ version: 13.1.0 resolution: workspace environment: - sdk: ">=3.10.0 <4.0.0" - flutter: ">=3.38.0" + sdk: ">=3.11.0 <4.0.0" + flutter: ">=3.41.0" dependencies: flutter: @@ -27,7 +27,7 @@ dependencies: file_picker_linux: ^2.0.0 windows_file_picker: ^2.0.0 file_picker_web: ^4.0.0 - cross_file: ^0.3.5+4 + cross_file: ^0.4.0 dev_dependencies: flutter_test: diff --git a/packages/file_picker_android/android/build.gradle.kts b/packages/file_picker_android/android/build.gradle.kts index 558e3b3a..434f0e85 100644 --- a/packages/file_picker_android/android/build.gradle.kts +++ b/packages/file_picker_android/android/build.gradle.kts @@ -64,7 +64,7 @@ configure { namespace = "com.mr.flutter.plugin.filepicker" defaultConfig { - minSdk = 21 + minSdk = 24 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("proguard-rules.pro") } diff --git a/packages/file_picker_android/lib/src/android_platform_file.dart b/packages/file_picker_android/lib/src/android_platform_file.dart index 71078cc9..f3a06ac6 100644 --- a/packages/file_picker_android/lib/src/android_platform_file.dart +++ b/packages/file_picker_android/lib/src/android_platform_file.dart @@ -51,7 +51,6 @@ base class AndroidPlatformFile extends PlatformFile { name: name, uri: uri, safHandle: safHandle, - xFile: path.isNotEmpty ? XFile(path, name: name) : null, bytesLength: data['size'] as int?, ); } @@ -69,14 +68,11 @@ base class AndroidPlatformFile extends PlatformFile { final int? _bytesLength; @override - XFile get xFile { - final file = _xFile; - if (file != null) return file; - if (uri.scheme == 'file') { - return XFile(uri.toFilePath(), name: name); - } - return XFile(uri.toString(), name: name); - } + XFile get xFile => + _xFile ?? + (uri.scheme == 'file' + ? XFile.fileSystem(path: uri.toFilePath()) + : XFile.scopedStorage(uri: uri.toString())); /// The size Android already reported for this file when it was picked. @override diff --git a/packages/file_picker_android/pubspec.yaml b/packages/file_picker_android/pubspec.yaml index 4b364614..9d0526d7 100644 --- a/packages/file_picker_android/pubspec.yaml +++ b/packages/file_picker_android/pubspec.yaml @@ -10,8 +10,8 @@ topics: resolution: workspace environment: - sdk: ">=3.10.0 <4.0.0" - flutter: ">=3.38.0" + sdk: ">=3.11.0 <4.0.0" + flutter: ">=3.41.0" flutter: plugin: @@ -26,10 +26,11 @@ dependencies: flutter: sdk: flutter file_picker_platform_interface: ^4.0.0 - cross_file: ^0.3.5+4 + cross_file: ^0.4.0 path: ^1.9.0 dev_dependencies: + cross_file_io: ^1.0.0 flutter_test: sdk: flutter flutter_lints: ^6.0.0 diff --git a/packages/file_picker_android/test/platform_file_read_test.dart b/packages/file_picker_android/test/platform_file_read_test.dart new file mode 100644 index 00000000..35f5dea0 --- /dev/null +++ b/packages/file_picker_android/test/platform_file_read_test.dart @@ -0,0 +1,38 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:cross_file/cross_file.dart'; +import 'package:cross_file_io/cross_file_io.dart'; +import 'package:android_file_picker/android_file_picker.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + late Directory dir; + late String path; + + setUpAll(CrossFileIO.registerWith); + + setUp(() { + dir = Directory.systemTemp.createTempSync('file_picker_test'); + path = '${dir.path}${Platform.pathSeparator}notes.txt'; + File(path).writeAsStringSync('hello file_picker'); + }); + + tearDown(() => dir.deleteSync(recursive: true)); + + test('reads a picked file through cross_file', () async { + final file = AndroidPlatformFile.fromMap({ + 'path': path, + 'name': 'notes.txt', + }); + + expect(file.xFile, isA()); + expect((file.xFile as FileSystemXFile).path, path); + expect(utf8.decode(await file.readAsBytes()), 'hello file_picker'); + expect( + utf8.decode(await file.readAsByteStream().expand((c) => c).toList()), + 'hello file_picker', + ); + expect(await file.length(), 17); + }); +} diff --git a/packages/file_picker_darwin/darwin/file_picker_darwin.podspec b/packages/file_picker_darwin/darwin/file_picker_darwin.podspec index e8f97164..5d4a36f7 100644 --- a/packages/file_picker_darwin/darwin/file_picker_darwin.podspec +++ b/packages/file_picker_darwin/darwin/file_picker_darwin.podspec @@ -21,7 +21,7 @@ iOS and macOS implementation of file_picker plugin s.ios.dependency 'Flutter' s.osx.dependency 'FlutterMacOS' s.ios.deployment_target = '14.0' - s.osx.deployment_target = '10.13' + s.osx.deployment_target = '10.15' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } s.swift_version = '5.0' end diff --git a/packages/file_picker_darwin/darwin/file_picker_darwin/Package.swift b/packages/file_picker_darwin/darwin/file_picker_darwin/Package.swift index dad3ad4d..2b166941 100644 --- a/packages/file_picker_darwin/darwin/file_picker_darwin/Package.swift +++ b/packages/file_picker_darwin/darwin/file_picker_darwin/Package.swift @@ -7,7 +7,7 @@ let package = Package( name: "file_picker_darwin", platforms: [ .iOS("14.0"), - .macOS("10.13") + .macOS("10.15") ], products: [ .library(name: "file-picker-darwin", targets: ["file_picker_darwin"]) diff --git a/packages/file_picker_darwin/lib/src/darwin_platform_file.dart b/packages/file_picker_darwin/lib/src/darwin_platform_file.dart index a56e5491..e6e8bc1b 100644 --- a/packages/file_picker_darwin/lib/src/darwin_platform_file.dart +++ b/packages/file_picker_darwin/lib/src/darwin_platform_file.dart @@ -40,7 +40,6 @@ base class DarwinPlatformFile extends PlatformFile { return DarwinPlatformFile( name: name, uri: uri, - xFile: path.isNotEmpty ? XFile(path, name: name) : null, bytesLength: data['size'] as int?, ); } @@ -55,14 +54,11 @@ base class DarwinPlatformFile extends PlatformFile { final int? _bytesLength; @override - XFile get xFile { - final file = _xFile; - if (file != null) return file; - if (uri.scheme == 'file') { - return XFile(uri.toFilePath(), name: name); - } - return XFile(uri.toString(), name: name); - } + XFile get xFile => + _xFile ?? + (uri.scheme == 'file' + ? XFile.fileSystem(path: uri.toFilePath()) + : XFile.scopedStorage(uri: uri.toString())); /// The size iOS/macOS already reported for this file when it was picked. @override diff --git a/packages/file_picker_darwin/pubspec.yaml b/packages/file_picker_darwin/pubspec.yaml index 442b61a3..dd50fe45 100644 --- a/packages/file_picker_darwin/pubspec.yaml +++ b/packages/file_picker_darwin/pubspec.yaml @@ -12,8 +12,8 @@ topics: resolution: workspace environment: - sdk: ">=3.10.0 <4.0.0" - flutter: ">=3.38.0" + sdk: ">=3.11.0 <4.0.0" + flutter: ">=3.41.0" flutter: plugin: @@ -32,10 +32,11 @@ dependencies: flutter: sdk: flutter file_picker_platform_interface: ^4.0.0 - cross_file: ^0.3.5+4 + cross_file: ^0.4.0 path: ^1.9.0 dev_dependencies: + cross_file_io: ^1.0.0 flutter_test: sdk: flutter flutter_lints: ^6.0.0 diff --git a/packages/file_picker_darwin/test/platform_file_read_test.dart b/packages/file_picker_darwin/test/platform_file_read_test.dart new file mode 100644 index 00000000..53b847a0 --- /dev/null +++ b/packages/file_picker_darwin/test/platform_file_read_test.dart @@ -0,0 +1,38 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:cross_file/cross_file.dart'; +import 'package:cross_file_io/cross_file_io.dart'; +import 'package:file_picker_darwin/file_picker_darwin.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + late Directory dir; + late String path; + + setUpAll(CrossFileIO.registerWith); + + setUp(() { + dir = Directory.systemTemp.createTempSync('file_picker_test'); + path = '${dir.path}${Platform.pathSeparator}notes.txt'; + File(path).writeAsStringSync('hello file_picker'); + }); + + tearDown(() => dir.deleteSync(recursive: true)); + + test('reads a picked file through cross_file', () async { + final file = DarwinPlatformFile.fromMap({ + 'path': path, + 'name': 'notes.txt', + }); + + expect(file.xFile, isA()); + expect((file.xFile as FileSystemXFile).path, path); + expect(utf8.decode(await file.readAsBytes()), 'hello file_picker'); + expect( + utf8.decode(await file.readAsByteStream().expand((c) => c).toList()), + 'hello file_picker', + ); + expect(await file.length(), 17); + }); +} diff --git a/packages/file_picker_linux/lib/src/linux_platform_file.dart b/packages/file_picker_linux/lib/src/linux_platform_file.dart index 1444efcb..e894bf2b 100644 --- a/packages/file_picker_linux/lib/src/linux_platform_file.dart +++ b/packages/file_picker_linux/lib/src/linux_platform_file.dart @@ -15,7 +15,7 @@ base class LinuxPlatformFile extends PlatformFile { }) : _xFile = xFile, _bytesLength = bytesLength; - factory LinuxPlatformFile.fromPath(String path, {Uint8List? bytes}) { + factory LinuxPlatformFile.fromPath(String path) { if (path.isEmpty) { throw ArgumentError( 'path cannot be empty when creating LinuxPlatformFile', @@ -23,12 +23,7 @@ base class LinuxPlatformFile extends PlatformFile { } final uri = Uri.file(path); final name = p.posix.basename(path); - return LinuxPlatformFile( - name: name, - uri: uri, - xFile: XFile(path, name: name, bytes: bytes), - bytesLength: bytes?.lengthInBytes, - ); + return LinuxPlatformFile(name: name, uri: uri); } @override @@ -41,14 +36,11 @@ base class LinuxPlatformFile extends PlatformFile { final int? _bytesLength; @override - XFile get xFile { - final file = _xFile; - if (file != null) return file; - if (uri.scheme == 'file') { - return XFile(uri.toFilePath(), name: name); - } - return XFile(uri.toString(), name: name); - } + XFile get xFile => + _xFile ?? + (uri.scheme == 'file' + ? XFile.fileSystem(path: uri.toFilePath()) + : XFile.scopedStorage(uri: uri.toString())); /// Only known when this file was created with its bytes already in hand. /// diff --git a/packages/file_picker_linux/pubspec.yaml b/packages/file_picker_linux/pubspec.yaml index 562f80f6..ba2b8d43 100644 --- a/packages/file_picker_linux/pubspec.yaml +++ b/packages/file_picker_linux/pubspec.yaml @@ -10,8 +10,8 @@ topics: resolution: workspace environment: - sdk: ">=3.10.0 <4.0.0" - flutter: ">=3.38.0" + sdk: ">=3.11.0 <4.0.0" + flutter: ">=3.41.0" flutter: plugin: @@ -24,11 +24,12 @@ dependencies: flutter: sdk: flutter file_picker_platform_interface: ^4.0.0 - cross_file: ^0.3.5+4 + cross_file: ^0.4.0 dbus: ^0.8.0 path: ^1.9.0 dev_dependencies: + cross_file_io: ^1.0.0 flutter_test: sdk: flutter flutter_lints: ^6.0.0 diff --git a/packages/file_picker_linux/test/file_picker_linux_test.dart b/packages/file_picker_linux/test/file_picker_linux_test.dart index f9a86545..6ae1eb72 100644 --- a/packages/file_picker_linux/test/file_picker_linux_test.dart +++ b/packages/file_picker_linux/test/file_picker_linux_test.dart @@ -1,5 +1,3 @@ -import 'dart:typed_data'; - import 'package:file_picker_linux/file_picker_linux.dart'; import 'package:file_picker_platform_interface/file_picker_platform_interface.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -29,9 +27,10 @@ void main() { final withoutBytes = LinuxPlatformFile.fromPath('/tmp/test.png'); expect(withoutBytes.lengthSync(), isNull); - final withBytes = LinuxPlatformFile.fromPath( - '/tmp/test.png', - bytes: Uint8List.fromList([1, 2, 3]), + final withBytes = LinuxPlatformFile( + name: 'test.png', + uri: Uri.file('/tmp/test.png'), + bytesLength: 3, ); expect(withBytes.lengthSync(), equals(3)); }); diff --git a/packages/file_picker_linux/test/platform_file_read_test.dart b/packages/file_picker_linux/test/platform_file_read_test.dart new file mode 100644 index 00000000..42157dd2 --- /dev/null +++ b/packages/file_picker_linux/test/platform_file_read_test.dart @@ -0,0 +1,35 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:cross_file/cross_file.dart'; +import 'package:cross_file_io/cross_file_io.dart'; +import 'package:file_picker_linux/file_picker_linux.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + late Directory dir; + late String path; + + setUpAll(CrossFileIO.registerWith); + + setUp(() { + dir = Directory.systemTemp.createTempSync('file_picker_test'); + path = '${dir.path}${Platform.pathSeparator}notes.txt'; + File(path).writeAsStringSync('hello file_picker'); + }); + + tearDown(() => dir.deleteSync(recursive: true)); + + test('reads a picked file through cross_file', () async { + final file = LinuxPlatformFile.fromPath(path); + + expect(file.xFile, isA()); + expect((file.xFile as FileSystemXFile).path, path); + expect(utf8.decode(await file.readAsBytes()), 'hello file_picker'); + expect( + utf8.decode(await file.readAsByteStream().expand((c) => c).toList()), + 'hello file_picker', + ); + expect(await file.length(), 17); + }); +} diff --git a/packages/file_picker_platform_interface/pubspec.yaml b/packages/file_picker_platform_interface/pubspec.yaml index 3a8a0318..f15f506b 100644 --- a/packages/file_picker_platform_interface/pubspec.yaml +++ b/packages/file_picker_platform_interface/pubspec.yaml @@ -10,14 +10,14 @@ topics: resolution: workspace environment: - sdk: ">=3.10.0 <4.0.0" - flutter: ">=3.38.0" + sdk: ">=3.11.0 <4.0.0" + flutter: ">=3.41.0" dependencies: flutter: sdk: flutter plugin_platform_interface: ^2.1.8 - cross_file: ^0.3.5+4 + cross_file: ^0.4.0 path: ^1.9.1 dev_dependencies: diff --git a/packages/file_picker_web/pubspec.yaml b/packages/file_picker_web/pubspec.yaml index 1c9ed9cf..4263550b 100644 --- a/packages/file_picker_web/pubspec.yaml +++ b/packages/file_picker_web/pubspec.yaml @@ -10,8 +10,8 @@ topics: resolution: workspace environment: - sdk: ">=3.10.0 <4.0.0" - flutter: ">=3.38.0" + sdk: ">=3.11.0 <4.0.0" + flutter: ">=3.41.0" flutter: plugin: @@ -27,7 +27,7 @@ dependencies: flutter_web_plugins: sdk: flutter file_picker_platform_interface: ^4.0.0 - cross_file: ^0.3.5+4 + cross_file: ^0.4.0 meta: ^1.17.0 path: ^1.9.1 web: ^1.1.1 diff --git a/packages/file_picker_windows/lib/src/windows_platform_file.dart b/packages/file_picker_windows/lib/src/windows_platform_file.dart index 1edcbd04..11d10cd8 100644 --- a/packages/file_picker_windows/lib/src/windows_platform_file.dart +++ b/packages/file_picker_windows/lib/src/windows_platform_file.dart @@ -15,7 +15,7 @@ base class WindowsPlatformFile extends PlatformFile { }) : _xFile = xFile, _bytesLength = bytesLength; - factory WindowsPlatformFile.fromPath(String path, {Uint8List? bytes}) { + factory WindowsPlatformFile.fromPath(String path) { if (path.isEmpty) { throw ArgumentError( 'path cannot be empty when creating WindowsPlatformFile', @@ -23,12 +23,7 @@ base class WindowsPlatformFile extends PlatformFile { } final uri = Uri.file(path, windows: true); final name = p.windows.basename(path); - return WindowsPlatformFile( - name: name, - uri: uri, - xFile: XFile(path, name: name, bytes: bytes), - bytesLength: bytes?.lengthInBytes, - ); + return WindowsPlatformFile(name: name, uri: uri); } @override @@ -41,14 +36,11 @@ base class WindowsPlatformFile extends PlatformFile { final int? _bytesLength; @override - XFile get xFile { - final file = _xFile; - if (file != null) return file; - if (uri.scheme == 'file') { - return XFile(uri.toFilePath(), name: name); - } - return XFile(uri.toString(), name: name); - } + XFile get xFile => + _xFile ?? + (uri.scheme == 'file' + ? XFile.fileSystem(path: uri.toFilePath()) + : XFile.scopedStorage(uri: uri.toString())); /// Only known when this file was created with its bytes already in hand. /// diff --git a/packages/file_picker_windows/pubspec.yaml b/packages/file_picker_windows/pubspec.yaml index 74f1bc01..09fbc887 100644 --- a/packages/file_picker_windows/pubspec.yaml +++ b/packages/file_picker_windows/pubspec.yaml @@ -10,8 +10,8 @@ topics: resolution: workspace environment: - sdk: ">=3.10.0 <4.0.0" - flutter: ">=3.38.0" + sdk: ">=3.11.0 <4.0.0" + flutter: ">=3.41.0" flutter: plugin: @@ -24,13 +24,14 @@ dependencies: flutter: sdk: flutter file_picker_platform_interface: ^4.0.0 - cross_file: ^0.3.5+4 + cross_file: ^0.4.0 ffi: ^2.2.0 meta: ^1.17.0 path: ^1.9.1 win32: ^6.3.0 dev_dependencies: + cross_file_io: ^1.0.0 flutter_test: sdk: flutter flutter_lints: ^6.0.0 diff --git a/packages/file_picker_windows/test/file_picker_windows_test.dart b/packages/file_picker_windows/test/file_picker_windows_test.dart index f6eaadc2..26481fa0 100644 --- a/packages/file_picker_windows/test/file_picker_windows_test.dart +++ b/packages/file_picker_windows/test/file_picker_windows_test.dart @@ -1,5 +1,4 @@ import 'dart:isolate'; -import 'dart:typed_data'; import 'package:file_picker_platform_interface/file_picker_platform_interface.dart'; import 'package:windows_file_picker/src/open_save_file_args.dart'; @@ -35,9 +34,10 @@ void main() { ); expect(withoutBytes.lengthSync(), isNull); - final withBytes = WindowsPlatformFile.fromPath( - r'C:\Users\Test\file.txt', - bytes: Uint8List.fromList([1, 2, 3]), + final withBytes = WindowsPlatformFile( + name: 'file.txt', + uri: Uri.file(r'C:\Users\Test\file.txt', windows: true), + bytesLength: 3, ); expect(withBytes.lengthSync(), equals(3)); }, diff --git a/packages/file_picker_windows/test/platform_file_read_test.dart b/packages/file_picker_windows/test/platform_file_read_test.dart new file mode 100644 index 00000000..93a9cb1f --- /dev/null +++ b/packages/file_picker_windows/test/platform_file_read_test.dart @@ -0,0 +1,35 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:cross_file/cross_file.dart'; +import 'package:cross_file_io/cross_file_io.dart'; +import 'package:windows_file_picker/windows_file_picker.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + late Directory dir; + late String path; + + setUpAll(CrossFileIO.registerWith); + + setUp(() { + dir = Directory.systemTemp.createTempSync('file_picker_test'); + path = '${dir.path}${Platform.pathSeparator}notes.txt'; + File(path).writeAsStringSync('hello file_picker'); + }); + + tearDown(() => dir.deleteSync(recursive: true)); + + test('reads a picked file through cross_file', () async { + final file = WindowsPlatformFile.fromPath(path); + + expect(file.xFile, isA()); + expect((file.xFile as FileSystemXFile).path, path); + expect(utf8.decode(await file.readAsBytes()), 'hello file_picker'); + expect( + utf8.decode(await file.readAsByteStream().expand((c) => c).toList()), + 'hello file_picker', + ); + expect(await file.length(), 17); + }); +} diff --git a/pubspec.yaml b/pubspec.yaml index 5229ebdc..1257e67f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,8 +42,8 @@ melos: description: Run the web integration_test suite against the example app. environment: - sdk: ">=3.10.0 <4.0.0" - flutter: ">=3.38.0" + sdk: ">=3.11.0 <4.0.0" + flutter: ">=3.41.0" dev_dependencies: flutter_lints: ^6.0.0 From 90bb6295c2637f0798ec95acb68d2f89b8ebdb0b Mon Sep 17 00:00:00 2001 From: Victor Carreras <34163765+vicajilau@users.noreply.github.com> Date: Mon, 5 Oct 2026 17:54:19 +0200 Subject: [PATCH 2/6] Remove eager reading options on web and read through cross_file PlatformFile does not expose preloaded bytes, so withData, withReadStream and readSequential only decided when the content was read, and the eager paths were the source of the large file failures. They are removed from FilePickerWebOptions, which keeps cancelUploadOnWindowBlur only. Picked files are no longer read at pick time. Each File is wrapped in a cross_file_web XFile created from the Blob, and WebPlatformFile delegates readAsBytes(), readAsByteStream() and length() to it. This replaces the hand written FileReader, chunked stream and fetch helpers, and drops the bytes and readStream parameters of WebPlatformFile. Read errors still propagate. The object URL is created without auto revocation so PlatformFile.uri stays valid for as long as the page is open, as before. Refs #2224 --- .../file_picker/test/file_picker_test.dart | 2 +- .../test/platform_file_test.dart | 2 +- .../lib/src/file_picker_web.dart | 124 ++++-------------- .../lib/src/file_picker_web_options.dart | 23 +--- .../lib/src/indexed_task_runner.dart | 25 ---- .../lib/src/platform_file_web_fetch.dart | 105 --------------- .../lib/src/preload_policy.dart | 13 -- .../lib/src/web_platform_file.dart | 69 +++------- packages/file_picker_web/pubspec.yaml | 1 + .../test/file_picker_web_pick_test.dart | 6 +- .../test/indexed_task_runner_test.dart | 63 --------- .../test/preload_policy_test.dart | 18 --- .../test/web_options_test.dart | 14 +- .../test/web_platform_file_read_test.dart | 4 + 14 files changed, 59 insertions(+), 410 deletions(-) delete mode 100644 packages/file_picker_web/lib/src/indexed_task_runner.dart delete mode 100644 packages/file_picker_web/lib/src/platform_file_web_fetch.dart delete mode 100644 packages/file_picker_web/lib/src/preload_policy.dart delete mode 100644 packages/file_picker_web/test/indexed_task_runner_test.dart delete mode 100644 packages/file_picker_web/test/preload_policy_test.dart diff --git a/packages/file_picker/test/file_picker_test.dart b/packages/file_picker/test/file_picker_test.dart index bf7e965e..a48f1ad9 100644 --- a/packages/file_picker/test/file_picker_test.dart +++ b/packages/file_picker/test/file_picker_test.dart @@ -16,7 +16,7 @@ base class TestPlatformFile extends PlatformFile { final Uri uri; @override - XFile get xFile => XFile(path ?? ''); + XFile get xFile => XFile.fileSystem(path: path ?? ''); @override int? lengthSync() => 100; diff --git a/packages/file_picker_platform_interface/test/platform_file_test.dart b/packages/file_picker_platform_interface/test/platform_file_test.dart index af7481e9..5dc32547 100644 --- a/packages/file_picker_platform_interface/test/platform_file_test.dart +++ b/packages/file_picker_platform_interface/test/platform_file_test.dart @@ -14,7 +14,7 @@ base class _TestPlatformFile extends PlatformFile { Uri get uri => Uri.file(name); @override - XFile get xFile => XFile(name); + XFile get xFile => XFile.fileSystem(path: name); @override int? lengthSync() => null; diff --git a/packages/file_picker_web/lib/src/file_picker_web.dart b/packages/file_picker_web/lib/src/file_picker_web.dart index 8dce7f50..6503a034 100644 --- a/packages/file_picker_web/lib/src/file_picker_web.dart +++ b/packages/file_picker_web/lib/src/file_picker_web.dart @@ -2,14 +2,14 @@ import 'dart:async'; import 'dart:js_interop'; import 'dart:typed_data'; +import 'package:cross_file/cross_file.dart'; +import 'package:cross_file_web/cross_file_web.dart'; import 'package:file_picker_platform_interface/file_picker_platform_interface.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'package:path/path.dart' as p; import 'package:web/web.dart'; import 'file_picker_web_options.dart'; -import 'indexed_task_runner.dart'; -import 'preload_policy.dart'; import 'web_file_input_session.dart'; import 'web_platform_file.dart'; @@ -20,7 +20,6 @@ import 'web_platform_file.dart'; /// in browser environments. class FilePickerWeb extends FilePickerPlatform { static const String _kFilePickerInputsDomId = '__file_picker_web-file-input'; - static const int _readStreamChunkSize = 1000 * 1000; // 1 MB late Element _target; @@ -141,85 +140,36 @@ class FilePickerWeb extends FilePickerPlatform { return files ?? []; } - /// Processes the selected [FileList] according to [webOptions] and returns - /// a list of [PlatformFile] instances. + /// Wraps each selected [File] in a [WebPlatformFile] without reading it. /// - /// Reads files one at a time when [FilePickerWebOptions.readSequential] is - /// `true`, or concurrently otherwise; either way the result preserves the - /// original selection order. + /// The content is read on demand through [PlatformFile.readAsBytes] and + /// [PlatformFile.readAsByteStream]. Future> _processSelectedFiles( FileList files, FilePickerWebOptions webOptions, ) async { - Future processFileAt(int index) async { - final file = files.item(index); - if (file == null) return null; - - if (webOptions.withReadStream) { - return _createWebPlatformFile( - file: file, - readStream: _openFileReadStream(file), - ); - } - - if (!shouldPreloadBytes(file.size, withData: webOptions.withData)) { - return _createWebPlatformFile(file: file); - } - - final bytes = await _readSingleFileBytes(file); - return _createWebPlatformFile(file: file, bytes: bytes); - } - - final results = await runIndexedTasks( - files.length, - processFileAt, - sequential: webOptions.readSequential, - ); - return results.whereType().toList(); - } - - /// Reads an HTML [File] content into a [Uint8List] using [FileReader]. - Future _readSingleFileBytes(File file) async { - final completer = Completer(); - final reader = FileReader(); - - reader.addEventListener( - 'loadend', - ((Event _) { - if (!completer.isCompleted) { - final byteBuffer = (reader.result as JSArrayBuffer?)?.toDart; - completer.complete(byteBuffer?.asUint8List()); - } - }).toJS, - ); - - reader.addEventListener( - 'error', - ((Event _) { - if (!completer.isCompleted) { - completer.complete(null); - } - }).toJS, - ); - - reader.readAsArrayBuffer(file); - return completer.future; + return [ + for (var i = 0; i < files.length; i++) + if (files.item(i) case final file?) _createWebPlatformFile(file), + ]; } - /// Creates a [WebPlatformFile] from an HTML [File], resolving its `blob:` URI. + /// Creates a [WebPlatformFile] backed by the picked [file] itself. /// - /// The URI always points at the picked [file] itself, which avoids creating a copy. - WebPlatformFile _createWebPlatformFile({ - required File file, - Uint8List? bytes, - Stream? readStream, - }) { + /// Its `blob:` URI is not revoked automatically, so it stays valid for as + /// long as the page is open. + WebPlatformFile _createWebPlatformFile(File file) { + final xFile = ScopedStorageXFile.fromCreationParams( + WebScopedStorageXFileCreationParams.fromBlob( + file, + autoRevokeObjectUrl: false, + ), + ); return WebPlatformFile( name: file.name, - uri: Uri.parse(URL.createObjectURL(file)), - bytesLength: bytes != null ? bytes.length : file.size, - bytes: bytes, - readStream: readStream, + uri: Uri.parse(xFile.uri), + xFile: xFile, + bytesLength: file.size, ); } @@ -285,34 +235,4 @@ class FilePickerWeb extends FilePickerPlatform { ), }; } - - /// Opens a chunked byte stream reader for a web [File]. - Stream _openFileReadStream(File file) async* { - final reader = FileReader(); - - int start = 0; - while (start < file.size) { - final end = start + _readStreamChunkSize > file.size - ? file.size - : start + _readStreamChunkSize; - final blob = file.slice(start, end); - reader.readAsArrayBuffer(blob); - await EventStreamProviders.loadEvent.forTarget(reader).first; - final JSAny? readerResult = reader.result; - if (readerResult == null) { - continue; - } - - if (readerResult.isA()) { - yield (readerResult as JSArrayBuffer).toDart.asUint8List(); - start += _readStreamChunkSize; - continue; - } - - if (readerResult.isA()) { - yield Uint8List.fromList((readerResult as JSArray).toDart.cast()); - start += _readStreamChunkSize; - } - } - } } diff --git a/packages/file_picker_web/lib/src/file_picker_web_options.dart b/packages/file_picker_web/lib/src/file_picker_web_options.dart index b7a6f7a1..6d7f9153 100644 --- a/packages/file_picker_web/lib/src/file_picker_web_options.dart +++ b/packages/file_picker_web/lib/src/file_picker_web_options.dart @@ -2,30 +2,9 @@ import 'package:file_picker_platform_interface/file_picker_platform_interface.da /// Configuration options specific to the Web platform. final class FilePickerWebOptions extends WebOptions { - /// Whether to read each picked file into memory at pick time. - /// - /// Files larger than 2 GB are never preloaded, since browsers cannot hold - /// them in a single buffer. Their content can be read on demand through - /// `PlatformFile.readAsBytes()` and `readAsByteStream()` instead. - final bool withData; - - /// Whether to create a read stream for each picked file. - final bool withReadStream; - - /// Whether to read multiple files one at a time instead of concurrently. - /// - /// The result always preserves the original selection order, regardless - /// of this setting. - final bool readSequential; - /// Whether to cancel upload when window loses focus. final bool cancelUploadOnWindowBlur; /// Creates an instance of [FilePickerWebOptions]. - const FilePickerWebOptions({ - this.withData = true, - this.withReadStream = false, - this.readSequential = false, - this.cancelUploadOnWindowBlur = true, - }); + const FilePickerWebOptions({this.cancelUploadOnWindowBlur = true}); } diff --git a/packages/file_picker_web/lib/src/indexed_task_runner.dart b/packages/file_picker_web/lib/src/indexed_task_runner.dart deleted file mode 100644 index c8f4243b..00000000 --- a/packages/file_picker_web/lib/src/indexed_task_runner.dart +++ /dev/null @@ -1,25 +0,0 @@ -/// Runs [task] once for each index in `[0, length)`, either one at a time -/// ([sequential] `true`) or concurrently ([sequential] `false`). -/// -/// Returns the results in index order regardless of completion order, so -/// callers can pick concurrency without also giving up on ordering. -Future> runIndexedTasks( - int length, - Future Function(int index) task, { - required bool sequential, -}) async { - if (length == 0) return []; - - if (sequential) { - final results = []; - for (var i = 0; i < length; i++) { - results.add(await task(i)); - } - return results; - } - - // Future.wait() already returns results in the order the futures were - // passed in, not completion order, so no manual index bookkeeping is - // needed here. - return Future.wait([for (var i = 0; i < length; i++) task(i)]); -} diff --git a/packages/file_picker_web/lib/src/platform_file_web_fetch.dart b/packages/file_picker_web/lib/src/platform_file_web_fetch.dart deleted file mode 100644 index c416adfa..00000000 --- a/packages/file_picker_web/lib/src/platform_file_web_fetch.dart +++ /dev/null @@ -1,105 +0,0 @@ -import 'dart:js_interop'; -import 'dart:typed_data'; - -@JS('fetch') -external JSPromise _fetchJs(JSString url); - -/// Interop extension type representing a Web `Response` JS object. -extension type _Response(JSObject _) implements JSObject { - external JSPromise arrayBuffer(); - external JSObject? get body; -} - -/// Interop extension type representing a Web `ReadableStream` JS object. -extension type _ReadableStream(JSObject _) implements JSObject { - external JSObject getReader(); -} - -/// Interop extension type representing a Web `ReadableStreamDefaultReader` JS object. -extension type _Reader(JSObject _) implements JSObject { - external JSPromise read(); -} - -/// Interop extension type representing a Web `ReadableStreamReadResult` JS object. -extension type _ReadResult(JSObject _) implements JSObject { - external bool get done; - external JSUint8Array? get value; -} - -/// Fetches the bytes of a web-only path (`blob:` or `data:` URL). -/// -/// Returns the full file bytes (`Uint8List`) using `fetch(...).arrayBuffer()` -/// for `blob:` URLs, or parses `data:` URIs. Returns `null` if [path] is not a -/// web URL, so the caller can fall back to another source. Throws if the -/// content of a web URL cannot be read. -Future fetchBytesFromWebPath(String path) async { - if (!_isWebPath(path)) return null; - - if (path.startsWith('data:')) { - final uriData = Uri.parse(path).data; - - if (uriData == null) { - return null; - } - - return uriData.contentAsBytes(); - } - - final response = _Response(await _fetchJs(path.toJS).toDart); - final buffer = await response.arrayBuffer().toDart; - return buffer.toDart.asUint8List(); -} - -bool _isWebPath(String path) => - path.startsWith('blob:') || path.startsWith('data:'); - -/// Attempts to create a streaming `Stream` from a web-only path -/// (`blob:` or `data:` URL). -/// -/// Returns `null` if [path] is not a web URL, so the caller can fall back to -/// another source. Read failures are emitted as errors on the stream. -Stream? fetchStreamFromWebPath(String path) { - if (!_isWebPath(path)) return null; - - return _streamFromWebPath(path); -} - -/// Reads a `blob:` or `data:` URL and emits its bytes as a stream. -/// -/// Uses `Response.body` (`ReadableStream`) when available; otherwise falls -/// back to a single in-memory `arrayBuffer()` chunk. -Stream _streamFromWebPath(String path) async* { - if (path.startsWith('data:')) { - final uriData = Uri.parse(path).data; - - if (uriData == null) { - throw FormatException('Invalid data: URL', path); - } - - yield uriData.contentAsBytes(); - return; - } - - final response = _Response(await _fetchJs(path.toJS).toDart); - final body = response.body; - - // If there's no streaming body, fallback to arrayBuffer() - if (body == null) { - final buffer = await response.arrayBuffer().toDart; - yield buffer.toDart.asUint8List(); - return; - } - - final readable = _ReadableStream(body); - final reader = _Reader(readable.getReader()); - - while (true) { - final result = _ReadResult(await reader.read().toDart); - if (result.done) break; - - final arr = result.value; - if (arr == null) break; - - yield arr.toDart; - } -} diff --git a/packages/file_picker_web/lib/src/preload_policy.dart b/packages/file_picker_web/lib/src/preload_policy.dart deleted file mode 100644 index d9780de4..00000000 --- a/packages/file_picker_web/lib/src/preload_policy.dart +++ /dev/null @@ -1,13 +0,0 @@ -/// The largest file, in bytes, whose content is preloaded into memory when -/// `FilePickerWebOptions.withData` is `true`. -/// -/// Browsers cannot reliably hold a single buffer above 2 GB: Chrome fails the -/// read (and can crash the tab when a debugger is attached) and Firefox -/// rejects buffers that large. Larger files are still read on demand through -/// `PlatformFile.readAsBytes()` and `readAsByteStream()`. -const int maxPreloadBytes = 2 * 1024 * 1024 * 1024; - -/// Whether a picked file of [size] bytes should be read into memory at pick -/// time. -bool shouldPreloadBytes(int size, {required bool withData}) => - withData && size <= maxPreloadBytes; diff --git a/packages/file_picker_web/lib/src/web_platform_file.dart b/packages/file_picker_web/lib/src/web_platform_file.dart index d1747558..19569292 100644 --- a/packages/file_picker_web/lib/src/web_platform_file.dart +++ b/packages/file_picker_web/lib/src/web_platform_file.dart @@ -4,29 +4,23 @@ import 'dart:typed_data'; import 'package:cross_file/cross_file.dart'; import 'package:file_picker_platform_interface/file_picker_platform_interface.dart'; -import 'platform_file_web_fetch.dart'; - /// A Web-specific implementation of [PlatformFile]. /// -/// Wraps files selected or processed in a web environment, providing -/// access to file metadata, underlying bytes, stream readers, and `blob:` or -/// `data:` URIs. +/// Wraps a file selected in a web environment. Its content is read on demand +/// through [xFile], from a `blob:` or `data:` URI. base class WebPlatformFile extends PlatformFile { /// Creates a new [WebPlatformFile] instance. /// - /// Requires a file [name] and [uri]. Optional parameters include preloaded - /// [bytes], a custom [readStream], file size in [bytesLength], or an underlying [xFile]. + /// Requires a file [name] and [uri]. Optional parameters include the file + /// size in [bytesLength] and an underlying [xFile]. Without [xFile], the + /// content is read from [uri]. WebPlatformFile({ required this.name, required this.uri, XFile? xFile, int? bytesLength, - Uint8List? bytes, - Stream? readStream, }) : _xFile = xFile, - _bytesLength = bytesLength, - _bytes = bytes, - _readStream = readStream { + _bytesLength = bytesLength { if (name.isEmpty) { throw ArgumentError('name cannot be empty'); } @@ -45,23 +39,16 @@ base class WebPlatformFile extends PlatformFile { final XFile? _xFile; final int? _bytesLength; - final Uint8List? _bytes; - final Stream? _readStream; /// Returns an [XFile] instance representing this web file. @override - XFile get xFile { - final file = _xFile; - if (file != null) return file; - return XFile(uri.toString(), name: name, bytes: _bytes); - } + XFile get xFile => _xFile ?? XFile.scopedStorage(uri: uri.toString()); - /// The browser's `File.size`, or the loaded bytes' length. + /// The browser's `File.size`, if known. @override int? lengthSync() { final len = _bytesLength; - if (len != null && len > 0) return len; - return _bytes?.length; + return (len != null && len > 0) ? len : null; } /// Asynchronously calculates and returns the size of the file in bytes. @@ -69,8 +56,6 @@ base class WebPlatformFile extends PlatformFile { Future length() async { final len = _bytesLength; if (len != null && len > 0) return len; - final bytes = _bytes; - if (bytes != null) return bytes.length; try { return await xFile.length(); } catch (_) { @@ -78,39 +63,17 @@ base class WebPlatformFile extends PlatformFile { } } - /// Asynchronously reads the file content as a byte array (`Uint8List`). + /// Reads the file content as a byte array (`Uint8List`). /// - /// If bytes are cached or readable from a `blob:`/`data:` URL, they will be - /// retrieved directly; otherwise falls back to [xFile]. + /// Throws if the content cannot be read. @override - Future readAsBytes() async { - final bytes = _bytes; - if (bytes != null) return bytes; - - // Try fetching from blob or data URL - final fetched = await fetchBytesFromWebPath(uri.toString()); - if (fetched != null) return fetched; - - return xFile.readAsBytes(); - } + Future readAsBytes() => xFile.readAsBytes(); - /// Asynchronously opens a stream to read the file content in chunks. + /// Opens a stream to read the file content in chunks. + /// + /// Read failures are emitted as errors on the stream. @override - Stream readAsByteStream() async* { - final readStream = _readStream; - if (readStream != null) { - yield* readStream; - return; - } - - final webStream = fetchStreamFromWebPath(uri.toString()); - if (webStream != null) { - yield* webStream; - return; - } - - yield* xFile.openRead(); - } + Stream readAsByteStream() => xFile.openRead(); @override bool operator ==(Object other) { diff --git a/packages/file_picker_web/pubspec.yaml b/packages/file_picker_web/pubspec.yaml index 4263550b..eceac944 100644 --- a/packages/file_picker_web/pubspec.yaml +++ b/packages/file_picker_web/pubspec.yaml @@ -28,6 +28,7 @@ dependencies: sdk: flutter file_picker_platform_interface: ^4.0.0 cross_file: ^0.4.0 + cross_file_web: ^1.0.0 meta: ^1.17.0 path: ^1.9.1 web: ^1.1.1 diff --git a/packages/file_picker_web/test/file_picker_web_pick_test.dart b/packages/file_picker_web/test/file_picker_web_pick_test.dart index 9fb704b1..d8c8561b 100644 --- a/packages/file_picker_web/test/file_picker_web_pick_test.dart +++ b/packages/file_picker_web/test/file_picker_web_pick_test.dart @@ -5,13 +5,17 @@ import 'dart:convert'; import 'dart:js_interop'; import 'package:file_picker_platform_interface/file_picker_platform_interface.dart'; +import 'package:cross_file_web/cross_file_web.dart'; import 'package:file_picker_web/file_picker_web.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'package:web/web.dart'; void main() { - setUpAll(() => FilePickerWeb.registerWith(webPluginRegistrar)); + setUpAll(() { + FilePickerWeb.registerWith(webPluginRegistrar); + CrossFileWeb.registerWith(webPluginRegistrar); + }); Future> pick(List files) { final result = FilePickerPlatform.instance.pickFiles(); diff --git a/packages/file_picker_web/test/indexed_task_runner_test.dart b/packages/file_picker_web/test/indexed_task_runner_test.dart deleted file mode 100644 index 24f0569e..00000000 --- a/packages/file_picker_web/test/indexed_task_runner_test.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:file_picker_web/src/indexed_task_runner.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - group('runIndexedTasks', () { - test('sequential runs one at a time and preserves order', () async { - var concurrent = 0; - var maxConcurrent = 0; - final startOrder = []; - - final results = await runIndexedTasks(5, (index) async { - concurrent++; - maxConcurrent = concurrent > maxConcurrent ? concurrent : maxConcurrent; - startOrder.add(index); - // Earlier indices are slower, so if the next task started before - // this one finished, sequential mode would still be broken. - await Future.delayed(Duration(milliseconds: 5 * (5 - index))); - concurrent--; - return index; - }, sequential: true); - - expect(maxConcurrent, 1); - expect(startOrder, [0, 1, 2, 3, 4]); - expect(results, [0, 1, 2, 3, 4]); - }); - - test( - 'concurrent runs more than one task at a time and still preserves order', - () async { - var concurrent = 0; - var maxConcurrent = 0; - final completionOrder = []; - - final results = await runIndexedTasks(5, (index) async { - concurrent++; - maxConcurrent = concurrent > maxConcurrent - ? concurrent - : maxConcurrent; - // Reverse delays so completion order is the opposite of index - // order, proving the result list isn't just built in completion - // order. - await Future.delayed(Duration(milliseconds: 5 * (5 - index))); - concurrent--; - completionOrder.add(index); - return index; - }, sequential: false); - - expect(maxConcurrent, greaterThan(1)); - expect(completionOrder, [4, 3, 2, 1, 0]); - expect(results, [0, 1, 2, 3, 4]); - }, - ); - - test('returns an empty list for zero length', () async { - final results = await runIndexedTasks( - 0, - (index) async => index, - sequential: false, - ); - expect(results, isEmpty); - }); - }); -} diff --git a/packages/file_picker_web/test/preload_policy_test.dart b/packages/file_picker_web/test/preload_policy_test.dart deleted file mode 100644 index 69334dfd..00000000 --- a/packages/file_picker_web/test/preload_policy_test.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:file_picker_web/src/preload_policy.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - test('preloads files up to 2 GB when withData is true', () { - expect(shouldPreloadBytes(0, withData: true), isTrue); - expect(shouldPreloadBytes(maxPreloadBytes, withData: true), isTrue); - }); - - test('does not preload files larger than 2 GB', () { - expect(shouldPreloadBytes(maxPreloadBytes + 1, withData: true), isFalse); - expect(shouldPreloadBytes(2400 * 1024 * 1024, withData: true), isFalse); - }); - - test('never preloads when withData is false', () { - expect(shouldPreloadBytes(1, withData: false), isFalse); - }); -} diff --git a/packages/file_picker_web/test/web_options_test.dart b/packages/file_picker_web/test/web_options_test.dart index e4b8578d..b881d668 100644 --- a/packages/file_picker_web/test/web_options_test.dart +++ b/packages/file_picker_web/test/web_options_test.dart @@ -2,11 +2,13 @@ import 'package:file_picker_web/src/file_picker_web_options.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { - test('FilePickerWebOptions initializes with default properties', () { - const options = FilePickerWebOptions(withData: false, withReadStream: true); - expect(options.withData, isFalse); - expect(options.withReadStream, isTrue); - expect(options.readSequential, isFalse); - expect(options.cancelUploadOnWindowBlur, isTrue); + test('FilePickerWebOptions cancels on window blur by default', () { + expect(const FilePickerWebOptions().cancelUploadOnWindowBlur, isTrue); + expect( + const FilePickerWebOptions( + cancelUploadOnWindowBlur: false, + ).cancelUploadOnWindowBlur, + isFalse, + ); }); } diff --git a/packages/file_picker_web/test/web_platform_file_read_test.dart b/packages/file_picker_web/test/web_platform_file_read_test.dart index e25c0d46..6a3df53c 100644 --- a/packages/file_picker_web/test/web_platform_file_read_test.dart +++ b/packages/file_picker_web/test/web_platform_file_read_test.dart @@ -5,11 +5,15 @@ import 'dart:convert'; import 'dart:js_interop'; import 'dart:typed_data'; +import 'package:cross_file_web/cross_file_web.dart'; import 'package:file_picker_web/file_picker_web.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'package:web/web.dart'; void main() { + setUpAll(() => CrossFileWeb.registerWith(webPluginRegistrar)); + final content = Uint8List.fromList(utf8.encode('hello file_picker')); String createBlobUrl() => URL.createObjectURL(Blob([content.toJS].toJS)); From f863c12f04e2cc241059f3840479eff4e04ba58b Mon Sep 17 00:00:00 2001 From: Victor Carreras <34163765+vicajilau@users.noreply.github.com> Date: Mon, 5 Oct 2026 17:58:16 +0200 Subject: [PATCH 3/6] Prepare the 14.0.0 release Bump every package to a new major for the cross_file 0.4.0 migration: file_picker 14.0.0, file_picker_platform_interface 5.0.0, android_file_picker, file_picker_darwin, file_picker_linux and windows_file_picker 3.0.0, and file_picker_web 5.0.0, with their changelogs and raised constraints. Add the Migrating to v14 guide covering the new minimum versions, the new XFile API and the removed web reading options. The unreleased 4.0.1, 2.0.1 and 2.0.2 sections are kept so those patches can still be published from main before this lands. Refs #2224 --- README.md | 1 + example/pubspec.yaml | 12 ++++++------ packages/file_picker/CHANGELOG.md | 7 +++++++ packages/file_picker/README.md | 16 +++++++++++++++- packages/file_picker/pubspec.yaml | 14 +++++++------- packages/file_picker_android/CHANGELOG.md | 4 ++++ packages/file_picker_android/pubspec.yaml | 4 ++-- packages/file_picker_darwin/CHANGELOG.md | 4 ++++ packages/file_picker_darwin/pubspec.yaml | 4 ++-- packages/file_picker_linux/CHANGELOG.md | 5 +++++ packages/file_picker_linux/pubspec.yaml | 4 ++-- .../file_picker_platform_interface/CHANGELOG.md | 4 ++++ .../file_picker_platform_interface/pubspec.yaml | 2 +- packages/file_picker_web/CHANGELOG.md | 6 ++++++ packages/file_picker_web/pubspec.yaml | 4 ++-- packages/file_picker_windows/CHANGELOG.md | 5 +++++ packages/file_picker_windows/pubspec.yaml | 4 ++-- 17 files changed, 75 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 41e9e94f..2be604a5 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ See [`packages/file_picker/README.md`](packages/file_picker/README.md) for the f ## Migration +- **Upgrading to v14?** See the [Migrating to v14 Guide](packages/file_picker/README.md#migrating-to-v14) for the `cross_file` 0.4.0 migration, the raised minimum versions and the removed web reading options. - **Upgrading to v13?** See the [Migrating to v13 Guide](packages/file_picker/README.md#migrating-to-v13) for details on `PlatformFile.length()` returning `Future` and the removal of legacy v12-deprecated parameters. - **Upgrading from pre-v12?** See the [Migrating to v12 Guide](packages/file_picker/README.md#migrating-to-v12) for the federated architecture changes (`FilePicker.pickFiles()` returning `List`, platform options, and streaming). diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 0a48c46f..3d03c9bc 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -13,12 +13,12 @@ formatter: dependencies: flutter: sdk: flutter - file_picker: ^13.0.0 - android_file_picker: ^2.0.0 - file_picker_darwin: ^2.0.0 - file_picker_linux: ^2.0.0 - file_picker_web: ^4.0.0 - windows_file_picker: ^2.0.0 + file_picker: ^14.0.0 + android_file_picker: ^3.0.0 + file_picker_darwin: ^3.0.0 + file_picker_linux: ^3.0.0 + file_picker_web: ^5.0.0 + windows_file_picker: ^3.0.0 file: ^7.0.1 dev_dependencies: diff --git a/packages/file_picker/CHANGELOG.md b/packages/file_picker/CHANGELOG.md index f78c4478..6f5d3c42 100644 --- a/packages/file_picker/CHANGELOG.md +++ b/packages/file_picker/CHANGELOG.md @@ -1,3 +1,10 @@ +## 14.0.0 + +- **BREAKING CHANGE**: Migrated to `cross_file` 0.4.0. `PlatformFile.xFile` now returns the new `XFile` API. See the [migration guide](https://github.com/vicajilau/flutter_file_picker/tree/main/packages/file_picker#migrating-to-v14). [#2224](https://github.com/vicajilau/flutter_file_picker/issues/2224) +- **BREAKING CHANGE**: Requires Flutter 3.41, Dart 3.11, Android SDK 24 and macOS 10.15. +- **BREAKING CHANGE**: Removed `withData`, `withReadStream` and `readSequential` from `FilePickerWebOptions` (`file_picker_web 5.0.0`). +- Raised lower bounds of all platform implementation packages: `file_picker_platform_interface ^5.0.0`, `android_file_picker ^3.0.0`, `file_picker_darwin ^3.0.0`, `file_picker_linux ^3.0.0`, `windows_file_picker ^3.0.0`, and `file_picker_web ^5.0.0`. + ## 13.1.0 - Added support for UIScene on iOS. diff --git a/packages/file_picker/README.md b/packages/file_picker/README.md index 76e94bcf..61f27e48 100644 --- a/packages/file_picker/README.md +++ b/packages/file_picker/README.md @@ -53,7 +53,21 @@ See the [official API reference on pub.dev](https://pub.dev/documentation/file_p ### Darwin implementation notes -The iOS and macOS native implementations live under the shared Darwin source tree (`file_picker_darwin`). The iOS implementation requires iOS 14.0 or newer because it uses `PHPickerViewController` and `PHPickerResult`. +The iOS and macOS native implementations live under the shared Darwin source tree (`file_picker_darwin`). The iOS implementation requires iOS 14.0 or newer because it uses `PHPickerViewController` and `PHPickerResult`. The macOS implementation requires macOS 10.15 or newer. + +## Migrating to v14 + +1. **Minimum Versions Raised**: Flutter 3.41, Dart 3.11, Android SDK 24 and macOS 10.15, required by `cross_file` 0.4.0. + +2. **`PlatformFile.xFile` Uses `cross_file` 0.4.0**: + * `XFile.path`, `XFile.mimeType`, `XFile.saveTo()` and `XFile.fromData` no longer exist, and `XFile.name` is now `Future name()`. + * Use `PlatformFile.path`, `PlatformFile.name`, `readAsBytes()` and `readAsByteStream()` when you do not need an `XFile`. For a file system path, check for a `FileSystemXFile`: `final path = switch (file.xFile) { FileSystemXFile(:final path) => path, _ => null };` (`FileSystemXFile` comes from `package:cross_file/cross_file.dart`). + * See the [`cross_file` changelog](https://pub.dev/packages/cross_file/changelog) for the full list. + +3. **Web Reading Options Removed**: + * `withData`, `withReadStream` and `readSequential` are removed from `FilePickerWebOptions`. Picked files are no longer read at pick time, and `readAsBytes()`/`readAsByteStream()` read them on demand. + * **v13**: `FilePickerWebOptions(withData: false, withReadStream: true, cancelUploadOnWindowBlur: false)` + * **v14**: `FilePickerWebOptions(cancelUploadOnWindowBlur: false)` ## Migrating to v13 diff --git a/packages/file_picker/pubspec.yaml b/packages/file_picker/pubspec.yaml index 513d14c2..87103650 100644 --- a/packages/file_picker/pubspec.yaml +++ b/packages/file_picker/pubspec.yaml @@ -9,7 +9,7 @@ topics: - storage - desktop - web -version: 13.1.0 +version: 14.0.0 resolution: workspace @@ -21,12 +21,12 @@ dependencies: flutter: sdk: flutter - file_picker_platform_interface: ^4.0.0 - android_file_picker: ^2.0.0 - file_picker_darwin: ^2.1.0 - file_picker_linux: ^2.0.0 - windows_file_picker: ^2.0.0 - file_picker_web: ^4.0.0 + file_picker_platform_interface: ^5.0.0 + android_file_picker: ^3.0.0 + file_picker_darwin: ^3.0.0 + file_picker_linux: ^3.0.0 + windows_file_picker: ^3.0.0 + file_picker_web: ^5.0.0 cross_file: ^0.4.0 dev_dependencies: diff --git a/packages/file_picker_android/CHANGELOG.md b/packages/file_picker_android/CHANGELOG.md index 2f67a197..8cba0dd3 100644 --- a/packages/file_picker_android/CHANGELOG.md +++ b/packages/file_picker_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.0.0 + +- **BREAKING CHANGE**: Migrated to `cross_file` 0.4.0. `PlatformFile.xFile` now returns the new `XFile` API. Requires Flutter 3.41, Dart 3.11 and Android SDK 24. [#2224](https://github.com/vicajilau/flutter_file_picker/issues/2224) + ## 2.0.1 - Fixed `pickFileAndDirectoryPaths()` returning percent-encoded paths that do not exist. [#2225](https://github.com/vicajilau/flutter_file_picker/issues/2225) diff --git a/packages/file_picker_android/pubspec.yaml b/packages/file_picker_android/pubspec.yaml index 9d0526d7..847bf38b 100644 --- a/packages/file_picker_android/pubspec.yaml +++ b/packages/file_picker_android/pubspec.yaml @@ -1,6 +1,6 @@ name: android_file_picker description: Android implementation of the file_picker plugin, supporting file picking, saving, and Storage Access Framework (SAF) URI grants. -version: 2.0.1 +version: 3.0.0 homepage: https://github.com/vicajilau/flutter_file_picker/tree/main/packages/file_picker_android repository: https://github.com/vicajilau/flutter_file_picker/tree/main/packages/file_picker_android topics: @@ -25,7 +25,7 @@ flutter: dependencies: flutter: sdk: flutter - file_picker_platform_interface: ^4.0.0 + file_picker_platform_interface: ^5.0.0 cross_file: ^0.4.0 path: ^1.9.0 diff --git a/packages/file_picker_darwin/CHANGELOG.md b/packages/file_picker_darwin/CHANGELOG.md index cb8d7a7f..fc6b0e96 100644 --- a/packages/file_picker_darwin/CHANGELOG.md +++ b/packages/file_picker_darwin/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.0.0 + +- **BREAKING CHANGE**: Migrated to `cross_file` 0.4.0. `PlatformFile.xFile` now returns the new `XFile` API. Requires Flutter 3.41, Dart 3.11 and macOS 10.15. [#2224](https://github.com/vicajilau/flutter_file_picker/issues/2224) + ## 2.1.2 - Fixed `initialDirectory` having no effect on macOS and iOS for `pickFile()`, `pickFiles()`, and `getDirectoryPath()`. [#2216](https://github.com/vicajilau/flutter_file_picker/issues/2216) diff --git a/packages/file_picker_darwin/pubspec.yaml b/packages/file_picker_darwin/pubspec.yaml index dd50fe45..93133252 100644 --- a/packages/file_picker_darwin/pubspec.yaml +++ b/packages/file_picker_darwin/pubspec.yaml @@ -1,6 +1,6 @@ name: file_picker_darwin description: Darwin (iOS and macOS) implementation of the file_picker plugin, supporting native file picking, saving, and directory selection. -version: 2.1.2 +version: 3.0.0 homepage: https://github.com/vicajilau/flutter_file_picker/tree/main/packages/file_picker_darwin repository: https://github.com/vicajilau/flutter_file_picker/tree/main/packages/file_picker_darwin topics: @@ -31,7 +31,7 @@ flutter: dependencies: flutter: sdk: flutter - file_picker_platform_interface: ^4.0.0 + file_picker_platform_interface: ^5.0.0 cross_file: ^0.4.0 path: ^1.9.0 diff --git a/packages/file_picker_linux/CHANGELOG.md b/packages/file_picker_linux/CHANGELOG.md index afd36637..ba200921 100644 --- a/packages/file_picker_linux/CHANGELOG.md +++ b/packages/file_picker_linux/CHANGELOG.md @@ -1,3 +1,8 @@ +## 3.0.0 + +- **BREAKING CHANGE**: Migrated to `cross_file` 0.4.0. `PlatformFile.xFile` now returns the new `XFile` API. Requires Flutter 3.41 and Dart 3.11. [#2224](https://github.com/vicajilau/flutter_file_picker/issues/2224) +- **BREAKING CHANGE**: Removed the unused `bytes` parameter of `LinuxPlatformFile.fromPath()`. + ## 2.0.2 - Fixed `pickFileAndDirectoryPaths()` returning percent-encoded paths that do not exist. [#2225](https://github.com/vicajilau/flutter_file_picker/issues/2225) diff --git a/packages/file_picker_linux/pubspec.yaml b/packages/file_picker_linux/pubspec.yaml index ba2b8d43..9e77fddf 100644 --- a/packages/file_picker_linux/pubspec.yaml +++ b/packages/file_picker_linux/pubspec.yaml @@ -1,6 +1,6 @@ name: file_picker_linux description: Linux implementation of the file_picker plugin using GTK3 and XDG Desktop Portals for native file and directory picking. -version: 2.0.2 +version: 3.0.0 homepage: https://github.com/vicajilau/flutter_file_picker/tree/main/packages/file_picker_linux repository: https://github.com/vicajilau/flutter_file_picker/tree/main/packages/file_picker_linux topics: @@ -23,7 +23,7 @@ flutter: dependencies: flutter: sdk: flutter - file_picker_platform_interface: ^4.0.0 + file_picker_platform_interface: ^5.0.0 cross_file: ^0.4.0 dbus: ^0.8.0 path: ^1.9.0 diff --git a/packages/file_picker_platform_interface/CHANGELOG.md b/packages/file_picker_platform_interface/CHANGELOG.md index e81d9275..cb796102 100644 --- a/packages/file_picker_platform_interface/CHANGELOG.md +++ b/packages/file_picker_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 5.0.0 + +- **BREAKING CHANGE**: Migrated to `cross_file` 0.4.0. `PlatformFile.xFile` now returns the new `XFile` API. Requires Flutter 3.41 and Dart 3.11. [#2224](https://github.com/vicajilau/flutter_file_picker/issues/2224) + ## 4.0.0 - **BREAKING CHANGE**: `PlatformFile.length()` now returns `Future` instead of `Future`, matching `lengthSync()`. `null` means the length could not be determined (e.g. a failed disk read), distinct from a genuinely empty file, which still returns `0`. Previously both cases returned `0`, with no way to tell them apart. [#2197](https://github.com/vicajilau/flutter_file_picker/issues/2197) diff --git a/packages/file_picker_platform_interface/pubspec.yaml b/packages/file_picker_platform_interface/pubspec.yaml index f15f506b..590a2141 100644 --- a/packages/file_picker_platform_interface/pubspec.yaml +++ b/packages/file_picker_platform_interface/pubspec.yaml @@ -1,6 +1,6 @@ name: file_picker_platform_interface description: A common platform interface for the file_picker plugin, defining the shared API surface and data contracts across platforms. -version: 4.0.0 +version: 5.0.0 homepage: https://github.com/vicajilau/flutter_file_picker/tree/main/packages/file_picker_platform_interface repository: https://github.com/vicajilau/flutter_file_picker/tree/main/packages/file_picker_platform_interface topics: diff --git a/packages/file_picker_web/CHANGELOG.md b/packages/file_picker_web/CHANGELOG.md index b79de54f..2de7ab6f 100644 --- a/packages/file_picker_web/CHANGELOG.md +++ b/packages/file_picker_web/CHANGELOG.md @@ -1,3 +1,9 @@ +## 5.0.0 + +- **BREAKING CHANGE**: Removed `withData`, `withReadStream` and `readSequential` from `FilePickerWebOptions`. Picked files are no longer read at pick time, use `readAsBytes()`/`readAsByteStream()`. [#2224](https://github.com/vicajilau/flutter_file_picker/issues/2224) +- **BREAKING CHANGE**: Removed the `bytes` and `readStream` parameters of `WebPlatformFile`. +- **BREAKING CHANGE**: Migrated to `cross_file` 0.4.0. `PlatformFile.xFile` now returns the new `XFile` API. Requires Flutter 3.41 and Dart 3.11. + ## 4.0.1 - Fixed picking files on Safari (macOS and iOS) never returning the selection. [#2222](https://github.com/vicajilau/flutter_file_picker/issues/2222) diff --git a/packages/file_picker_web/pubspec.yaml b/packages/file_picker_web/pubspec.yaml index eceac944..d9655e44 100644 --- a/packages/file_picker_web/pubspec.yaml +++ b/packages/file_picker_web/pubspec.yaml @@ -1,6 +1,6 @@ name: file_picker_web description: Web platform implementation of the file_picker plugin, providing browser file selection, file streaming, and file loading. -version: 4.0.1 +version: 5.0.0 homepage: https://github.com/vicajilau/flutter_file_picker/tree/main/packages/file_picker_web repository: https://github.com/vicajilau/flutter_file_picker/tree/main/packages/file_picker_web topics: @@ -26,7 +26,7 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter - file_picker_platform_interface: ^4.0.0 + file_picker_platform_interface: ^5.0.0 cross_file: ^0.4.0 cross_file_web: ^1.0.0 meta: ^1.17.0 diff --git a/packages/file_picker_windows/CHANGELOG.md b/packages/file_picker_windows/CHANGELOG.md index 16a77191..d5f11675 100644 --- a/packages/file_picker_windows/CHANGELOG.md +++ b/packages/file_picker_windows/CHANGELOG.md @@ -1,3 +1,8 @@ +## 3.0.0 + +- **BREAKING CHANGE**: Migrated to `cross_file` 0.4.0. `PlatformFile.xFile` now returns the new `XFile` API. Requires Flutter 3.41 and Dart 3.11. [#2224](https://github.com/vicajilau/flutter_file_picker/issues/2224) +- **BREAKING CHANGE**: Removed the unused `bytes` parameter of `WindowsPlatformFile.fromPath()`. + ## 2.0.1 - Fixed `pickFileAndDirectoryPaths()` returning URI paths instead of Windows paths. [#2225](https://github.com/vicajilau/flutter_file_picker/issues/2225) diff --git a/packages/file_picker_windows/pubspec.yaml b/packages/file_picker_windows/pubspec.yaml index 09fbc887..a6268b10 100644 --- a/packages/file_picker_windows/pubspec.yaml +++ b/packages/file_picker_windows/pubspec.yaml @@ -1,6 +1,6 @@ name: windows_file_picker description: Windows implementation of the file_picker plugin using Win32 COM APIs for native file and directory picking. -version: 2.0.1 +version: 3.0.0 homepage: https://github.com/vicajilau/flutter_file_picker/tree/main/packages/file_picker_windows repository: https://github.com/vicajilau/flutter_file_picker/tree/main/packages/file_picker_windows topics: @@ -23,7 +23,7 @@ flutter: dependencies: flutter: sdk: flutter - file_picker_platform_interface: ^4.0.0 + file_picker_platform_interface: ^5.0.0 cross_file: ^0.4.0 ffi: ^2.2.0 meta: ^1.17.0 From 0005210c454ffbae00ff22df57054066e33273fb Mon Sep 17 00:00:00 2001 From: Victor Carreras <34163765+vicajilau@users.noreply.github.com> Date: Wed, 7 Oct 2026 07:27:53 +0200 Subject: [PATCH 4/6] Stream web files in evenly sized chunks xFile.openRead() reads through Blob.stream(), whose chunk sizes are irregular. Consumers that rely on evenly sized buffers, such as RamFileData.fromStream from the archive package, used to get them from withReadStream, which sliced the file into fixed chunks. readAsByteStream() now slices the picked Blob into WebPlatformFile.streamChunkSize (1 MiB) pieces with slice() and arrayBuffer(), so every chunk but the last has the same size. It falls back to openRead() for an XFile without a Blob. Refs #2223 --- packages/file_picker_web/CHANGELOG.md | 1 + .../lib/src/web_platform_file.dart | 25 +++++++++++++++++-- .../test/file_picker_web_pick_test.dart | 11 ++++++++ .../test/web_platform_file_read_test.dart | 25 +++++++++++++++++++ 4 files changed, 60 insertions(+), 2 deletions(-) diff --git a/packages/file_picker_web/CHANGELOG.md b/packages/file_picker_web/CHANGELOG.md index 2de7ab6f..28ea0f3c 100644 --- a/packages/file_picker_web/CHANGELOG.md +++ b/packages/file_picker_web/CHANGELOG.md @@ -2,6 +2,7 @@ - **BREAKING CHANGE**: Removed `withData`, `withReadStream` and `readSequential` from `FilePickerWebOptions`. Picked files are no longer read at pick time, use `readAsBytes()`/`readAsByteStream()`. [#2224](https://github.com/vicajilau/flutter_file_picker/issues/2224) - **BREAKING CHANGE**: Removed the `bytes` and `readStream` parameters of `WebPlatformFile`. +- `readAsByteStream()` emits chunks of `WebPlatformFile.streamChunkSize` (1 MiB), except the last one, which may be shorter. - **BREAKING CHANGE**: Migrated to `cross_file` 0.4.0. `PlatformFile.xFile` now returns the new `XFile` API. Requires Flutter 3.41 and Dart 3.11. ## 4.0.1 diff --git a/packages/file_picker_web/lib/src/web_platform_file.dart b/packages/file_picker_web/lib/src/web_platform_file.dart index 19569292..694108db 100644 --- a/packages/file_picker_web/lib/src/web_platform_file.dart +++ b/packages/file_picker_web/lib/src/web_platform_file.dart @@ -1,7 +1,10 @@ import 'dart:async'; +import 'dart:js_interop'; +import 'dart:math'; import 'dart:typed_data'; import 'package:cross_file/cross_file.dart'; +import 'package:cross_file_web/cross_file_web.dart'; import 'package:file_picker_platform_interface/file_picker_platform_interface.dart'; /// A Web-specific implementation of [PlatformFile]. @@ -69,11 +72,29 @@ base class WebPlatformFile extends PlatformFile { @override Future readAsBytes() => xFile.readAsBytes(); + /// The size of the chunks emitted by [readAsByteStream]. + static const int streamChunkSize = 1024 * 1024; + /// Opens a stream to read the file content in chunks. /// - /// Read failures are emitted as errors on the stream. + /// Every chunk is [streamChunkSize] bytes long except the last one, which + /// may be shorter, so consumers can rely on evenly sized buffers. Read + /// failures are emitted as errors on the stream. @override - Stream readAsByteStream() => xFile.openRead(); + Stream readAsByteStream() async* { + final extension = xFile.getExtension(); + if (extension == null) { + yield* xFile.openRead(); + return; + } + + final blob = await extension.getBlob(); + for (var start = 0; start < blob.size; start += streamChunkSize) { + final end = min(start + streamChunkSize, blob.size); + final buffer = await blob.slice(start, end).arrayBuffer().toDart; + yield buffer.toDart.asUint8List(); + } + } @override bool operator ==(Object other) { diff --git a/packages/file_picker_web/test/file_picker_web_pick_test.dart b/packages/file_picker_web/test/file_picker_web_pick_test.dart index d8c8561b..7d47029d 100644 --- a/packages/file_picker_web/test/file_picker_web_pick_test.dart +++ b/packages/file_picker_web/test/file_picker_web_pick_test.dart @@ -49,4 +49,15 @@ void main() { expect(files.single.uri.scheme, 'blob'); expect(await files.single.readAsBytes(), isEmpty); }); + + test('picked files stream evenly sized chunks', () async { + final content = 'a' * (WebPlatformFile.streamChunkSize + 10); + final files = await pick([fileWith(name: 'big.txt', content: content)]); + + final chunks = await files.single.readAsByteStream().toList(); + expect(chunks.map((chunk) => chunk.length), [ + WebPlatformFile.streamChunkSize, + 10, + ]); + }); } diff --git a/packages/file_picker_web/test/web_platform_file_read_test.dart b/packages/file_picker_web/test/web_platform_file_read_test.dart index 6a3df53c..c4c482b0 100644 --- a/packages/file_picker_web/test/web_platform_file_read_test.dart +++ b/packages/file_picker_web/test/web_platform_file_read_test.dart @@ -63,4 +63,29 @@ void main() { final chunks = await fileAt(url).readAsByteStream().toList(); expect(chunks.expand((chunk) => chunk).toList(), content); }); + + test('readAsByteStream() emits evenly sized chunks', () async { + const size = + WebPlatformFile.streamChunkSize * 2 + + WebPlatformFile.streamChunkSize ~/ 2; + final large = Uint8List.fromList(List.generate(size, (i) => i % 251)); + final url = URL.createObjectURL(Blob([large.toJS].toJS)); + addTearDown(() => URL.revokeObjectURL(url)); + + final chunks = await fileAt(url).readAsByteStream().toList(); + + expect(chunks.map((chunk) => chunk.length), [ + WebPlatformFile.streamChunkSize, + WebPlatformFile.streamChunkSize, + WebPlatformFile.streamChunkSize ~/ 2, + ]); + expect(chunks.expand((chunk) => chunk).toList(), large); + }); + + test('readAsByteStream() emits nothing for an empty file', () async { + final url = URL.createObjectURL(Blob([].toJS)); + addTearDown(() => URL.revokeObjectURL(url)); + + expect(await fileAt(url).readAsByteStream().toList(), isEmpty); + }); } From 9ced346feb8e52a01e2fc346c306c6d992e1b7ff Mon Sep 17 00:00:00 2001 From: Victor Carreras <34163765+vicajilau@users.noreply.github.com> Date: Thu, 8 Oct 2026 08:11:51 +0200 Subject: [PATCH 5/6] Update packages/file_picker/CHANGELOG.md Co-authored-by: Navaron Bracke --- packages/file_picker/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/file_picker/CHANGELOG.md b/packages/file_picker/CHANGELOG.md index 6f5d3c42..cbbf6d1b 100644 --- a/packages/file_picker/CHANGELOG.md +++ b/packages/file_picker/CHANGELOG.md @@ -2,7 +2,7 @@ - **BREAKING CHANGE**: Migrated to `cross_file` 0.4.0. `PlatformFile.xFile` now returns the new `XFile` API. See the [migration guide](https://github.com/vicajilau/flutter_file_picker/tree/main/packages/file_picker#migrating-to-v14). [#2224](https://github.com/vicajilau/flutter_file_picker/issues/2224) - **BREAKING CHANGE**: Requires Flutter 3.41, Dart 3.11, Android SDK 24 and macOS 10.15. -- **BREAKING CHANGE**: Removed `withData`, `withReadStream` and `readSequential` from `FilePickerWebOptions` (`file_picker_web 5.0.0`). +- **BREAKING CHANGE**: Removed `withData`, `withReadStream` and `readSequential` from `FilePickerWebOptions`. - Raised lower bounds of all platform implementation packages: `file_picker_platform_interface ^5.0.0`, `android_file_picker ^3.0.0`, `file_picker_darwin ^3.0.0`, `file_picker_linux ^3.0.0`, `windows_file_picker ^3.0.0`, and `file_picker_web ^5.0.0`. ## 13.1.0 From 476d9a4752b807ac0591d33479bdfd6afa251dc9 Mon Sep 17 00:00:00 2001 From: Victor Carreras <34163765+vicajilau@users.noreply.github.com> Date: Thu, 8 Oct 2026 08:13:41 +0200 Subject: [PATCH 6/6] Address review feedback on the 14.0.0 release Drop the redundant lower bounds line from the file_picker changelog, show the FileSystemXFile path lookup in the migration guide as a code block, document that AndroidPlatformFile.xFile wraps a content URI outside the file scheme, and explain why the chunking test uses a prime modulus. --- packages/file_picker/CHANGELOG.md | 1 - packages/file_picker/README.md | 11 ++++++++++- .../lib/src/android_platform_file.dart | 2 ++ .../test/web_platform_file_read_test.dart | 2 ++ 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/file_picker/CHANGELOG.md b/packages/file_picker/CHANGELOG.md index cbbf6d1b..c3b5e3c2 100644 --- a/packages/file_picker/CHANGELOG.md +++ b/packages/file_picker/CHANGELOG.md @@ -3,7 +3,6 @@ - **BREAKING CHANGE**: Migrated to `cross_file` 0.4.0. `PlatformFile.xFile` now returns the new `XFile` API. See the [migration guide](https://github.com/vicajilau/flutter_file_picker/tree/main/packages/file_picker#migrating-to-v14). [#2224](https://github.com/vicajilau/flutter_file_picker/issues/2224) - **BREAKING CHANGE**: Requires Flutter 3.41, Dart 3.11, Android SDK 24 and macOS 10.15. - **BREAKING CHANGE**: Removed `withData`, `withReadStream` and `readSequential` from `FilePickerWebOptions`. -- Raised lower bounds of all platform implementation packages: `file_picker_platform_interface ^5.0.0`, `android_file_picker ^3.0.0`, `file_picker_darwin ^3.0.0`, `file_picker_linux ^3.0.0`, `windows_file_picker ^3.0.0`, and `file_picker_web ^5.0.0`. ## 13.1.0 diff --git a/packages/file_picker/README.md b/packages/file_picker/README.md index b9312593..c0ec23b1 100644 --- a/packages/file_picker/README.md +++ b/packages/file_picker/README.md @@ -61,7 +61,16 @@ The iOS and macOS native implementations live under the shared Darwin source tre 2. **`PlatformFile.xFile` Uses `cross_file` 0.4.0**: * `XFile.path`, `XFile.mimeType`, `XFile.saveTo()` and `XFile.fromData` no longer exist, and `XFile.name` is now `Future name()`. - * Use `PlatformFile.path`, `PlatformFile.name`, `readAsBytes()` and `readAsByteStream()` when you do not need an `XFile`. For a file system path, check for a `FileSystemXFile`: `final path = switch (file.xFile) { FileSystemXFile(:final path) => path, _ => null };` (`FileSystemXFile` comes from `package:cross_file/cross_file.dart`). + * Use `PlatformFile.path`, `PlatformFile.name`, `readAsBytes()` and `readAsByteStream()` when you do not need an `XFile`. For a file system path, check for a `FileSystemXFile`: + + ```dart + import 'package:cross_file/cross_file.dart'; + + final path = switch (file.xFile) { + FileSystemXFile(:final path) => path, + _ => null, + }; + ``` * See the [`cross_file` changelog](https://pub.dev/packages/cross_file/changelog) for the full list. 3. **Web Reading Options Removed**: diff --git a/packages/file_picker_android/lib/src/android_platform_file.dart b/packages/file_picker_android/lib/src/android_platform_file.dart index f3a06ac6..1af26b67 100644 --- a/packages/file_picker_android/lib/src/android_platform_file.dart +++ b/packages/file_picker_android/lib/src/android_platform_file.dart @@ -67,6 +67,8 @@ base class AndroidPlatformFile extends PlatformFile { final XFile? _xFile; final int? _bytesLength; + /// A [FileSystemXFile] for a picked file cached on disk, or a + /// [ScopedStorageXFile] for a Storage Access Framework `content://` URI. @override XFile get xFile => _xFile ?? diff --git a/packages/file_picker_web/test/web_platform_file_read_test.dart b/packages/file_picker_web/test/web_platform_file_read_test.dart index c4c482b0..6cb30659 100644 --- a/packages/file_picker_web/test/web_platform_file_read_test.dart +++ b/packages/file_picker_web/test/web_platform_file_read_test.dart @@ -68,6 +68,8 @@ void main() { const size = WebPlatformFile.streamChunkSize * 2 + WebPlatformFile.streamChunkSize ~/ 2; + // 251 is prime, so the byte pattern does not line up with the chunk + // boundaries (a multiple of 256) and swapped or repeated chunks still fail. final large = Uint8List.fromList(List.generate(size, (i) => i % 251)); final url = URL.createObjectURL(Blob([large.toJS].toJS)); addTearDown(() => URL.revokeObjectURL(url));