├── .fvm
└── fvm_config.json
├── .github
└── workflows
│ ├── analysis.yml
│ ├── backend.yml
│ ├── examples.yml
│ ├── format.yml
│ └── test.yml
├── .gitignore
├── .metadata
├── CHANGELOG.md
├── LICENSE
├── README.md
├── analysis_options.yaml
├── android
├── .gitignore
├── build.gradle
├── gradle.properties
├── settings.gradle
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── bluechilli
│ │ └── flutteruploader
│ │ ├── CountProgressListener.java
│ │ ├── CountingRequestBody.java
│ │ ├── FileItem.java
│ │ ├── FlutterUploaderInitializer.java
│ │ ├── FlutterUploaderPlugin.java
│ │ ├── MethodCallHandlerImpl.java
│ │ ├── SharedPreferenceHelper.java
│ │ ├── UploadExecutorService.java
│ │ ├── UploadStatus.java
│ │ ├── UploadTask.java
│ │ ├── UploadWorker.java
│ │ └── plugin
│ │ ├── CachingStreamHandler.java
│ │ ├── StatusListener.java
│ │ └── UploadObserver.java
│ └── res
│ ├── drawable-hdpi
│ └── ic_upload.png
│ ├── drawable-mdpi
│ └── ic_upload.png
│ ├── drawable-xhdpi
│ └── ic_upload.png
│ ├── drawable-xxhdpi
│ └── ic_upload.png
│ ├── drawable-xxxhdpi
│ └── ic_upload.png
│ └── xml
│ └── provider_path.xml
├── example
├── .gitignore
├── .metadata
├── README.md
├── android
│ ├── app
│ │ ├── build.gradle
│ │ └── src
│ │ │ ├── androidTest
│ │ │ └── java
│ │ │ │ └── com
│ │ │ │ └── bluechilli
│ │ │ │ └── flutteruploaderexample
│ │ │ │ └── FlutterActivityTest.java
│ │ │ ├── debug
│ │ │ └── AndroidManifest.xml
│ │ │ ├── main
│ │ │ ├── AndroidManifest.xml
│ │ │ ├── java
│ │ │ │ └── io
│ │ │ │ │ └── flutter
│ │ │ │ │ └── app
│ │ │ │ │ └── FlutterMultiDexApplication.java
│ │ │ └── res
│ │ │ │ ├── drawable
│ │ │ │ └── launch_background.xml
│ │ │ │ ├── mipmap-hdpi
│ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-mdpi
│ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-xhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-xxhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-xxxhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ │ ├── values
│ │ │ │ └── styles.xml
│ │ │ │ └── xml
│ │ │ │ └── network_security_config.xml
│ │ │ └── profile
│ │ │ └── AndroidManifest.xml
│ ├── build.gradle
│ ├── gradle.properties
│ ├── gradle
│ │ └── wrapper
│ │ │ └── gradle-wrapper.properties
│ └── settings.gradle
├── backend
│ ├── .firebaserc
│ ├── .gitignore
│ ├── README.md
│ ├── firebase.json
│ ├── functions
│ │ ├── .eslintrc.json
│ │ ├── .gitignore
│ │ ├── index.js
│ │ ├── package-lock.json
│ │ └── package.json
│ └── package-lock.json
├── integration_test
│ └── flutter_uploader_integration_test.dart
├── ios
│ ├── .swiftlint.yml
│ ├── Flutter
│ │ ├── AppFrameworkInfo.plist
│ │ ├── Debug.xcconfig
│ │ └── Release.xcconfig
│ ├── Runner.xcodeproj
│ │ ├── project.pbxproj
│ │ ├── project.xcworkspace
│ │ │ └── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── xcschemes
│ │ │ └── Runner.xcscheme
│ ├── Runner.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ └── WorkspaceSettings.xcsettings
│ └── Runner
│ │ ├── AppDelegate.swift
│ │ ├── Assets.xcassets
│ │ ├── AppIcon.appiconset
│ │ │ ├── Contents.json
│ │ │ ├── Icon-App-1024x1024@1x.png
│ │ │ ├── Icon-App-20x20@1x.png
│ │ │ ├── Icon-App-20x20@2x.png
│ │ │ ├── Icon-App-20x20@3x.png
│ │ │ ├── Icon-App-29x29@1x.png
│ │ │ ├── Icon-App-29x29@2x.png
│ │ │ ├── Icon-App-29x29@3x.png
│ │ │ ├── Icon-App-40x40@1x.png
│ │ │ ├── Icon-App-40x40@2x.png
│ │ │ ├── Icon-App-40x40@3x.png
│ │ │ ├── Icon-App-60x60@2x.png
│ │ │ ├── Icon-App-60x60@3x.png
│ │ │ ├── Icon-App-76x76@1x.png
│ │ │ ├── Icon-App-76x76@2x.png
│ │ │ └── Icon-App-83.5x83.5@2x.png
│ │ └── LaunchImage.imageset
│ │ │ ├── Contents.json
│ │ │ ├── LaunchImage.png
│ │ │ ├── LaunchImage@2x.png
│ │ │ ├── LaunchImage@3x.png
│ │ │ └── README.md
│ │ ├── Base.lproj
│ │ ├── LaunchScreen.storyboard
│ │ └── Main.storyboard
│ │ ├── GoogleService-Info.plist
│ │ ├── Info.plist
│ │ └── Runner-Bridging-Header.h
├── lib
│ ├── main.dart
│ ├── responses_screen.dart
│ ├── server_behavior.dart
│ ├── upload_item.dart
│ ├── upload_item_view.dart
│ └── upload_screen.dart
└── pubspec.yaml
├── ios
├── .gitignore
├── Assets
│ └── .gitkeep
├── Classes
│ ├── CachingStreamHandler.swift
│ ├── EngineManager.swift
│ ├── FlutterUploaderPlugin.h
│ ├── FlutterUploaderPlugin.m
│ ├── Key.swift
│ ├── MimeType.swift
│ ├── SwiftFlutterUploaderPlugin.swift
│ ├── URLSessionTask.State+statusText.swift
│ ├── URLSessionUploader.swift
│ ├── UploadFileInfo.swift
│ ├── UploadResultDatabase.swift
│ ├── UploadTask.swift
│ ├── UploaderDefaults.swift
│ └── UploaderDelegate.swift
└── flutter_uploader.podspec
├── lib
├── flutter_uploader.dart
└── src
│ ├── file_item.dart
│ ├── flutter_uploader.dart
│ ├── upload.dart
│ ├── upload_method.dart
│ ├── upload_task_progress.dart
│ ├── upload_task_response.dart
│ └── upload_task_status.dart
├── pubspec.yaml
├── script
└── format.sh
└── test
├── flutter_uploader_test.dart
└── flutter_uploader_test.mocks.dart
/.fvm/fvm_config.json:
--------------------------------------------------------------------------------
1 | {
2 | "flutterSdkVersion": "stable"
3 | }
--------------------------------------------------------------------------------
/.github/workflows/analysis.yml:
--------------------------------------------------------------------------------
1 | name: analysis
2 | on: pull_request
3 |
4 | jobs:
5 | package-analysis:
6 | runs-on: ubuntu-latest
7 | steps:
8 | - uses: actions/checkout@v2 # required
9 | - uses: axel-op/dart-package-analyzer@v3
10 | with:
11 | # Required:
12 | githubToken: ${{ secrets.GITHUB_TOKEN }}
--------------------------------------------------------------------------------
/.github/workflows/backend.yml:
--------------------------------------------------------------------------------
1 | name: Node.js Backend
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | strategy:
15 | matrix:
16 | node-version: [12.x, 14.x]
17 |
18 | steps:
19 | - uses: actions/checkout@v2
20 | - name: Use Node.js ${{ matrix.node-version }}
21 | uses: actions/setup-node@v1
22 | with:
23 | node-version: ${{ matrix.node-version }}
24 | - run: cd example/backend/functions && npm ci
25 | - run: cd example/backend/functions && npm run-script lint
--------------------------------------------------------------------------------
/.github/workflows/examples.yml:
--------------------------------------------------------------------------------
1 | name: examples
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | example_android:
11 | runs-on: ubuntu-18.04
12 | steps:
13 | - uses: actions/checkout@v2
14 | - uses: actions/setup-java@v1
15 | with:
16 | java-version: 11
17 | - uses: subosito/flutter-action@v1
18 | with:
19 | channel: "stable"
20 |
21 | - name: build
22 | run: |
23 | cd example && flutter build apk --debug
24 |
25 | example_ios:
26 | runs-on: macos-latest
27 | steps:
28 | - uses: actions/checkout@v2
29 | - uses: subosito/flutter-action@v1
30 | with:
31 | channel: "stable"
32 |
33 | - name: build
34 | run: |
35 | cd example && flutter build ios --debug --no-codesign
36 |
--------------------------------------------------------------------------------
/.github/workflows/format.yml:
--------------------------------------------------------------------------------
1 | name: format + publishable
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | format_java:
11 | runs-on: ubuntu-18.04
12 | steps:
13 | - uses: actions/checkout@v2
14 | - uses: actions/setup-java@v1
15 | with:
16 | java-version: 11
17 | - uses: axel-op/googlejavaformat-action@v3
18 | - uses: subosito/flutter-action@v1
19 | with:
20 | channel: 'stable'
21 |
22 | - name: Format
23 | run: |
24 | flutter format --set-exit-if-changed .
25 |
26 | format_swift:
27 | runs-on: ubuntu-latest
28 | steps:
29 | - uses: actions/checkout@v2
30 | - name: GitHub Action for SwiftLint
31 | uses: norio-nomura/action-swiftlint@3.2.1
32 |
33 | publishable:
34 | runs-on: ubuntu-18.04
35 | steps:
36 | - uses: actions/checkout@v2
37 |
38 | - uses: subosito/flutter-action@v1
39 | with:
40 | channel: 'stable'
41 |
42 | - name: publish checks
43 | run: |
44 | flutter pub get
45 | flutter pub publish -n
46 | dart pub global activate tuneup
47 | dart pub global run tuneup check
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: test
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | test:
11 | runs-on: ubuntu-18.04
12 | steps:
13 | - uses: actions/checkout@v2
14 |
15 | - uses: subosito/flutter-action@v1
16 | with:
17 | channel: 'stable'
18 |
19 | - name: Test
20 | run: |
21 | flutter pub get
22 | flutter test
23 |
24 | drive_ios:
25 | strategy:
26 | matrix:
27 | device:
28 | - "iPhone 12 Pro"
29 | fail-fast: false
30 | runs-on: macos-latest
31 | steps:
32 | - uses: futureware-tech/simulator-action@v1
33 | with:
34 | model: '${{ matrix.device }}'
35 | - uses: actions/checkout@v2
36 | - uses: subosito/flutter-action@v1
37 | with:
38 | channel: 'stable'
39 | # Run flutter integrate tests
40 | - name: Run Flutter integration tests
41 | run: cd example && flutter test integration_test/flutter_uploader_integration_test.dart
42 |
43 | # Skipped until Flutter supports `--multidex` for integration tests
44 | # drive_android:
45 | # runs-on: macos-latest
46 | # #creates a build matrix for your jobs
47 | # strategy:
48 | # #set of different configurations of the virtual environment.
49 | # matrix:
50 | # api-level: [29]
51 | # # api-level: [21, 29]
52 | # target: [default]
53 | # needs: test
54 | # steps:
55 | # - uses: actions/checkout@v2
56 | # - uses: subosito/flutter-action@v1
57 | # with:
58 | # channel: 'stable'
59 | # - name: Run Flutter Driver tests
60 | # uses: reactivecircus/android-emulator-runner@v2
61 | # with:
62 | # api-level: ${{ matrix.api-level }}
63 | # target: ${{ matrix.target }}
64 | # arch: x86_64
65 | # profile: Nexus 6
66 | # script: cd example && flutter test integration_test/flutter_uploader_integration_test.dart
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .atom/
3 | .idea/
4 | .vscode/
5 |
6 | .packages
7 | .pub/
8 | .dart_tool/
9 | pubspec.lock
10 | flutter_export_environment.sh
11 |
12 | examples/all_plugins/pubspec.yaml
13 |
14 | Podfile
15 | Podfile.lock
16 | Pods/
17 | .symlinks/
18 | **/Flutter/App.framework/
19 | **/Flutter/ephemeral/
20 | **/Flutter/Flutter.framework/
21 | **/Flutter/Generated.xcconfig
22 | **/Flutter/flutter_assets/
23 |
24 | ServiceDefinitions.json
25 | xcuserdata/
26 | **/DerivedData/
27 |
28 | local.properties
29 | keystore.properties
30 | .gradle/
31 | gradlew
32 | gradlew.bat
33 | gradle-wrapper.jar
34 | .flutter-plugins-dependencies
35 | *.iml
36 |
37 | generated_plugin_registrant.dart
38 | GeneratedPluginRegistrant.h
39 | GeneratedPluginRegistrant.m
40 | GeneratedPluginRegistrant.java
41 | GeneratedPluginRegistrant.swift
42 | build/
43 | .flutter-plugins
44 |
45 | .project
46 | .classpath
47 | .settings
48 | .last_build_id
49 | .fvm/flutter_sdk
50 |
--------------------------------------------------------------------------------
/.metadata:
--------------------------------------------------------------------------------
1 | # This file tracks properties of this Flutter project.
2 | # Used by Flutter tool to assess capabilities and perform upgrades etc.
3 | #
4 | # This file should be version controlled and should not be manually edited.
5 |
6 | version:
7 | revision: fec01201b1a491d1d404825829cfd4e5992dbbaa
8 | channel: master
9 |
10 | project_type: plugin
11 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 3.0.0-beta.4
2 |
3 | - Restore compatibility with Android 12
4 | - Ensure plugin is compatible with Flutter 2.10
5 |
6 | ## 3.0.0-beta.3
7 |
8 | - Add flag to restrict uploads to wifi only (#188)
9 | - Fix ClassCastException on Android (#196)
10 | - Do not persist nil values in result database (#190)
11 |
12 | ## 3.0.0-beta.2
13 |
14 | - Android: Restore concurrency setting for uploads (#174).
15 |
16 | ## 3.0.0-beta.1
17 |
18 | - Migrate to nullsafety
19 |
20 | ## 2.0.0-beta.6
21 |
22 | - Android: Ensure to properly unregister upload observers
23 | - Android: Call back to UI on the main thread (#132)
24 |
25 | ## 2.0.0-beta.5
26 |
27 | - Resolves crashes on iOS when using multiple concurrent uploads
28 | - Additional documentation on results stream
29 | - Android: Clear "progress" as well when a `clearUploads` is called
30 |
31 | ## 2.0.0-beta.4
32 |
33 | - Bump Flutter & Android dependencies, which also resolves the multi-file selection issue in the example
34 | - Android: Ensure `clearUploads` also clears the cache held in memory (#119)
35 | - Added more documentation for the `result` stream
36 | - Correct homepage field in `pubspec.yaml`
37 | - Android: Set `compile` & `target` (for example app) SDK versions to 30
38 |
39 | ## 2.0.0-beta.3
40 |
41 | - Update maintainer field in `pubspec.yaml`
42 |
43 | ## 2.0.0-beta.2
44 |
45 | - Moved package to Flutter Community
46 |
47 | ## 2.0.0-beta.1
48 |
49 | - Runs a Flutter isolate in the background while a upload is running. The entry point can be set using `setBackgroundHandler`.
50 | - Notification handling has been removed from this plugin entirely. Developers can use `FlutterLocalNotifications` using the new isolate mechanism.
51 | - Extends the example & test backend with simulations for various HTTP responses, status codes etc.
52 | - Adds multi-file picking support to the example.
53 | - Adds E2E tests including CI config for iOS/Android
54 | - Adds basic unit tests to confirm message passing Dart <-> Native
55 | - Android: Support Androids Flutters V2 embedding
56 | - Simplify FileItem and replace the `savedDir`/`filename` parameters with `path`
57 | - Uploads with unknown mime type will default to `application/octet-stream`
58 |
59 | ## 1.2.0
60 |
61 | - iOS: fix multipartform upload to be able upload large files
62 | - iOS: fix multipartform upload to be able upload multiple files in one upload task
63 |
64 | ## 1.1.0
65 |
66 | - iOS: define clang module
67 | - iOS: upgrade example project xcode version & compatibility
68 |
69 | ## 1.0.6
70 |
71 | - fix #21 - handle other successful status code (from http spec) in iOS
72 |
73 | ## 1.0.5+1
74 |
75 | - Android: update AGP and various dependencies
76 | - Android: fixes memory leaks in the example project due to old image_picker dependency
77 | - Android: fix memory leak due not unregistering ActivityLifecycleCallbacks
78 | - Android: fix memory leak due to not unregistering WorkManager observers
79 |
80 | ## 1.0.3+2
81 |
82 | - fix bug that upon cancellation it was cancelling the work request however it wasn't cancelling the already progressing upload request (android);
83 |
84 | ## 1.0.3+1
85 |
86 | - remove Accept-Encoding header because OkHttp transparently adds it (android)
87 | - documentation update
88 | - clean up some code
89 |
90 | ## 1.0.3
91 |
92 | - prevent from start uploading when directory is passed as file path
93 | - update androidx workmanager to 2.0.0
94 | - use observable for tracking progress instead of localbroadcastmanager (deprecation) on android
95 | - fixed few typoes in code
96 | - fixes few typoes in document
97 |
98 | ## 1.0.2
99 |
100 | Thanks @ened for pull requests
101 |
102 | - Prevent basic NPE when Activity is not set
103 | - Upgrade example dependencies
104 | - Use the latest gradle plugin
105 |
106 | ## 1.0.1
107 |
108 | - updated licence
109 |
110 | ## 1.0.0
111 |
112 | - initial release
113 | - feature constists of: enqueue, cancel, cancelAll
114 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 Blue Chilli Technology Pty Ltd and the contributors
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/fluttercommunity/community)
2 |
3 | # Flutter Uploader
4 |
5 | A plugin for creating and managing upload tasks. Supports iOS and Android.
6 |
7 | This plugin is based on [`WorkManager`][1] in Android and [`NSURLSessionUploadTask`][2] in iOS to run upload task in background mode.
8 |
9 | This plugin is inspired by [`flutter_downloader`][5]. Thanks to Hung Duy Ha & Flutter Community for great plugins and inspiration.
10 |
11 | ## iOS integration
12 |
13 | - Enable background mode.
14 |
15 |
16 |
17 | ### AppDelegate changes
18 |
19 | The plugin supports a background isolate. In order for plugins to work, you need to adjust your AppDelegate as follows:
20 |
21 | ```swift
22 | import flutter_uploader
23 |
24 | func registerPlugins(registry: FlutterPluginRegistry) {
25 | GeneratedPluginRegistrant.register(with: registry)
26 | }
27 |
28 | @UIApplicationMain
29 | @objc class AppDelegate: FlutterAppDelegate {
30 | override func application(
31 | _ application: UIApplication,
32 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
33 | ) -> Bool {
34 | GeneratedPluginRegistrant.register(with: self)
35 |
36 | SwiftFlutterUploaderPlugin.registerPlugins = registerPlugins
37 |
38 | // Any further code.
39 | }
40 | }
41 | ```
42 |
43 | ### Optional configuration:
44 |
45 | - **Configure maximum number of connection per host:** the plugin allows 3 simultaneous http connection per host running at a moment by default. You can change this number by adding following codes to your `Info.plist` file.
46 |
47 | ```xml
48 |
49 | FUMaximumConnectionsPerHost
50 | 3
51 | ```
52 |
53 | - **Configure maximum number of concurrent upload operation:** the plugin allows 3 simultaneous upload operation running at a moment by default. You can change this number by adding following codes to your `Info.plist` file.
54 |
55 | ```xml
56 |
57 | FUMaximumUploadOperation
58 | 3
59 | ```
60 |
61 | - **Configure request timeout:** controls how long (in seconds) a task should wait for additional data to arrive before giving up `Info.plist` file.
62 |
63 | ```xml
64 |
65 | FUTimeoutInSeconds
66 | 3600
67 | ```
68 |
69 | ## Android integration
70 |
71 | ### Optional configuration:
72 |
73 | - **Configure maximum number of concurrent tasks:** the plugin depends on `WorkManager` library and `WorkManager` depends on the number of available processor to configure the maximum number of tasks running at a moment. You can setup a fixed number for this configuration by adding following codes to your `AndroidManifest.xml`:
74 |
75 | ```xml
76 |
81 |
82 |
86 |
87 |
90 |
91 |
92 |
93 |
94 | ```
95 |
96 | ## Usage
97 |
98 | #### Import package:
99 |
100 | ```dart
101 | import 'package:flutter_uploader/flutter_uploader.dart';
102 | ```
103 |
104 | #### Configure a background isolate entry point
105 |
106 | First, define a top-level function:
107 |
108 | ```dart
109 | void backgroundHandler() {
110 | // Needed so that plugin communication works.
111 | WidgetsFlutterBinding.ensureInitialized();
112 |
113 | // This uploader instance works within the isolate only.
114 | FlutterUploader uploader = FlutterUploader();
115 |
116 | // You have now access to:
117 | uploader.progress.listen((progress) {
118 | // upload progress
119 | });
120 | uploader.result.listen((result) {
121 | // upload results
122 | });
123 | }
124 | ```
125 |
126 | > The backgroundHandler function needs to be either a static function or a top level function to be accessible as a Flutter entry point.
127 |
128 | Once you have a function defined, configure it in your main `FlutterUploader` object like so:
129 |
130 | ```dart
131 | FlutterUploader().setBackgroundHandler(backgroundHandler);
132 | ```
133 |
134 | To see how it all works, check out the example.
135 |
136 | #### Create new upload task:
137 |
138 | **multipart/form-data:**
139 |
140 | ```dart
141 | final taskId = await FlutterUploader().enqueue(
142 | MultipartFormDataUpload(
143 | url: "your upload link", //required: url to upload to
144 | files: [FileItem(path: '/path/to/file', fieldname:"file")], // required: list of files that you want to upload
145 | method: UploadMethod.POST, // HTTP method (POST or PUT or PATCH)
146 | headers: {"apikey": "api_123456", "userkey": "userkey_123456"},
147 | data: {"name": "john"}, // any data you want to send in upload request
148 | tag: 'my tag', // custom tag which is returned in result/progress
149 | ),
150 | );
151 | ```
152 |
153 | **binary uploads:**
154 |
155 | ```dart
156 | final taskId = await FlutterUploader().enqueue(
157 | RawUpload(
158 | url: "your upload link", // required: url to upload to
159 | path: '/path/to/file', // required: list of files that you want to upload
160 | method: UploadMethod.POST, // HTTP method (POST or PUT or PATCH)
161 | headers: {"apikey": "api_123456", "userkey": "userkey_123456"},
162 | tag: 'my tag', // custom tag which is returned in result/progress
163 | ),
164 | );
165 | ```
166 |
167 | The plugin will return a `taskId` which is unique for each upload. Hold onto it if you in order to cancel specific uploads.
168 |
169 | ### listen for upload progress
170 |
171 | ```dart
172 | final subscription = FlutterUploader().progress.listen((progress) {
173 | //... code to handle progress
174 | });
175 | ```
176 |
177 | ### listen for upload result
178 |
179 | ```dart
180 | final subscription = FlutterUploader().result.listen((result) {
181 | //... code to handle result
182 | }, onError: (ex, stacktrace) {
183 | // ... code to handle error
184 | });
185 | ```
186 |
187 | > when tasks are cancelled, it will send on onError handler as exception with status = cancelled
188 |
189 | Upload results are persisted by the plugin and will be submitted on each `.listen`.
190 | It is advised to keep a list of processed uploads in App side and call `clearUploads` on the FlutterUploader plugin once they can be removed.
191 |
192 | #### Cancel an upload task:
193 |
194 | ```dart
195 | FlutterUploader().cancel(taskId: taskId);
196 | ```
197 |
198 | #### Cancel all upload tasks:
199 |
200 | ```dart
201 | FlutterUploader().cancelAll();
202 | ```
203 |
204 | #### Clear Uploads
205 |
206 | ```dart
207 | FlutterUploader().clearUploads()
208 | ```
209 |
210 | [1]: https://developer.android.com/topic/libraries/architecture/workmanager
211 | [2]: https://developer.apple.com/documentation/foundation/nsurlsessionuploadtask?language=objc
212 | [3]: https://medium.com/@guerrix/info-plist-localization-ad5daaea732a
213 | [4]: https://developer.android.com/training/basics/supporting-devices/languages
214 | [5]: https://pub.dartlang.org/packages/flutter_downloader
215 |
--------------------------------------------------------------------------------
/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | include: package:flutter_lints/flutter.yaml
2 |
3 | linter:
4 | rules:
5 | - public_member_api_docs
--------------------------------------------------------------------------------
/android/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/workspace.xml
5 | /.idea/libraries
6 | .DS_Store
7 | /build
8 | /captures
9 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | group 'com.bluechilli.flutteruploader'
2 | version '1.0-SNAPSHOT'
3 |
4 | buildscript {
5 | repositories {
6 | google()
7 | mavenCentral()
8 | }
9 |
10 | dependencies {
11 | classpath 'com.android.tools.build:gradle:4.2.1'
12 | }
13 | }
14 |
15 | rootProject.allprojects {
16 | repositories {
17 | google()
18 | mavenCentral()
19 | }
20 | }
21 |
22 | apply plugin: 'com.android.library'
23 |
24 | android {
25 | compileSdkVersion 31
26 |
27 | defaultConfig {
28 | minSdkVersion 16
29 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
30 | }
31 |
32 | compileOptions {
33 | sourceCompatibility JavaVersion.VERSION_1_8
34 | targetCompatibility JavaVersion.VERSION_1_8
35 | }
36 |
37 | lintOptions {
38 | disable 'InvalidPackage'
39 | }
40 | }
41 |
42 | dependencies {
43 | implementation "androidx.work:work-runtime:2.7.1"
44 | implementation "androidx.concurrent:concurrent-futures:1.1.0"
45 | implementation "androidx.annotation:annotation:1.2.0"
46 | implementation "androidx.lifecycle:lifecycle-livedata:2.3.1"
47 | implementation "androidx.core:core:1.5.0"
48 | implementation "com.squareup.okhttp3:okhttp:4.9.0"
49 | implementation "com.google.code.gson:gson:2.8.6"
50 | }
51 |
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx1536M
2 | android.useAndroidX=true
3 | android.enableJetifier=true
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'flutter_uploader'
2 | android.enableJetifier=true
3 | android.useAndroidX=true
--------------------------------------------------------------------------------
/android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/android/src/main/java/com/bluechilli/flutteruploader/CountProgressListener.java:
--------------------------------------------------------------------------------
1 | package com.bluechilli.flutteruploader;
2 |
3 | public interface CountProgressListener {
4 |
5 | void OnProgress(String taskId, long bytesWritten, long contentLength);
6 |
7 | void OnError(String taskId, String code, String message);
8 | }
9 |
--------------------------------------------------------------------------------
/android/src/main/java/com/bluechilli/flutteruploader/CountingRequestBody.java:
--------------------------------------------------------------------------------
1 | package com.bluechilli.flutteruploader;
2 |
3 | import androidx.annotation.NonNull;
4 | import java.io.IOException;
5 | import okhttp3.MediaType;
6 | import okhttp3.RequestBody;
7 | import okio.Buffer;
8 | import okio.BufferedSink;
9 | import okio.ForwardingSink;
10 | import okio.Okio;
11 | import okio.Sink;
12 |
13 | public class CountingRequestBody extends RequestBody {
14 |
15 | protected final RequestBody _body;
16 | protected final CountProgressListener _listener;
17 | protected final String _taskId;
18 | protected CountingSink _countingSink;
19 |
20 | public CountingRequestBody(RequestBody body, String taskId, CountProgressListener listener) {
21 | _body = body;
22 | _taskId = taskId;
23 | _listener = listener;
24 | }
25 |
26 | @Override
27 | public MediaType contentType() {
28 | return _body.contentType();
29 | }
30 |
31 | @Override
32 | public long contentLength() throws IOException {
33 | return _body.contentLength();
34 | }
35 |
36 | @Override
37 | public void writeTo(@NonNull BufferedSink sink) throws IOException {
38 | try {
39 | _countingSink = new CountingSink(this, sink);
40 | BufferedSink bufferedSink = Okio.buffer(_countingSink);
41 | _body.writeTo(bufferedSink);
42 |
43 | bufferedSink.flush();
44 | } catch (IOException ex) {
45 | sendError(ex);
46 | }
47 | }
48 |
49 | public void sendProgress(long bytesWritten, long totalContentLength) {
50 | if (_listener != null) {
51 | _listener.OnProgress(_taskId, bytesWritten, totalContentLength);
52 | }
53 | }
54 |
55 | public void sendError(Exception ex) {
56 | if (_listener != null) {
57 | _listener.OnError(_taskId, "upload_task_error", ex.toString());
58 | }
59 | }
60 |
61 | protected static class CountingSink extends ForwardingSink {
62 | private long _bytesWritten;
63 | private final CountingRequestBody _parent;
64 |
65 | public CountingSink(CountingRequestBody parent, Sink sink) {
66 | super(sink);
67 | _parent = parent;
68 | }
69 |
70 | @Override
71 | public void write(@NonNull Buffer source, long byteCount) throws IOException {
72 | try {
73 | super.write(source, byteCount);
74 |
75 | _bytesWritten += byteCount;
76 |
77 | if (_parent != null) {
78 | _parent.sendProgress(_bytesWritten, _parent.contentLength());
79 | }
80 | } catch (IOException ex) {
81 | if (_parent != null) {
82 | _parent.sendError(ex);
83 | }
84 | }
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/android/src/main/java/com/bluechilli/flutteruploader/FileItem.java:
--------------------------------------------------------------------------------
1 | package com.bluechilli.flutteruploader;
2 |
3 | import java.util.Map;
4 |
5 | public class FileItem {
6 |
7 | private String fieldname;
8 | private String path;
9 |
10 | public FileItem(String path) {
11 | this.path = path;
12 | }
13 |
14 | public FileItem(String path, String fieldname) {
15 | this.fieldname = fieldname;
16 | this.path = path;
17 | }
18 |
19 | public static FileItem fromJson(Map map) {
20 | return new FileItem(map.get("path"), map.get("fieldname"));
21 | }
22 |
23 | public String getFieldname() {
24 | return fieldname;
25 | }
26 |
27 | public String getPath() {
28 | return path;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/android/src/main/java/com/bluechilli/flutteruploader/FlutterUploaderInitializer.java:
--------------------------------------------------------------------------------
1 | package com.bluechilli.flutteruploader;
2 |
3 | import android.content.ComponentName;
4 | import android.content.ContentProvider;
5 | import android.content.ContentValues;
6 | import android.content.Context;
7 | import android.content.pm.PackageManager;
8 | import android.content.pm.ProviderInfo;
9 | import android.database.Cursor;
10 | import android.net.Uri;
11 | import android.os.Bundle;
12 | import android.util.Log;
13 | import androidx.annotation.NonNull;
14 | import androidx.annotation.Nullable;
15 | import androidx.work.Configuration;
16 | import androidx.work.WorkManager;
17 | import java.util.concurrent.Executors;
18 |
19 | public class FlutterUploaderInitializer extends ContentProvider {
20 |
21 | private static final String TAG = "UploaderInitializer";
22 | private static final int DEFAULT_MAX_CONCURRENT_TASKS = 3;
23 | private static final int DEFAULT_UPLOAD_CONNECTION_TIMEOUT = 3600;
24 |
25 | @Override
26 | public boolean onCreate() {
27 | int maximumConcurrentTask = getMaxConcurrentTaskMetadata(getContext());
28 | WorkManager.initialize(
29 | getContext(),
30 | new Configuration.Builder()
31 | .setExecutor(Executors.newFixedThreadPool(maximumConcurrentTask))
32 | .build());
33 | return true;
34 | }
35 |
36 | @Nullable
37 | @Override
38 | public Cursor query(
39 | @NonNull Uri uri,
40 | @Nullable String[] strings,
41 | @Nullable String s,
42 | @Nullable String[] strings1,
43 | @Nullable String s1) {
44 | return null;
45 | }
46 |
47 | @Nullable
48 | @Override
49 | public String getType(@NonNull Uri uri) {
50 | return null;
51 | }
52 |
53 | @Nullable
54 | @Override
55 | public Uri insert(@NonNull Uri uri, @Nullable ContentValues contentValues) {
56 | return null;
57 | }
58 |
59 | @Override
60 | public int delete(@NonNull Uri uri, @Nullable String s, @Nullable String[] strings) {
61 | return 0;
62 | }
63 |
64 | @Override
65 | public int update(
66 | @NonNull Uri uri,
67 | @Nullable ContentValues contentValues,
68 | @Nullable String s,
69 | @Nullable String[] strings) {
70 | return 0;
71 | }
72 |
73 | public static int getMaxConcurrentTaskMetadata(Context context) {
74 | try {
75 | ProviderInfo pi =
76 | context
77 | .getPackageManager()
78 | .getProviderInfo(
79 | new ComponentName(
80 | context, "com.bluechilli.flutteruploader.FlutterUploaderInitializer"),
81 | PackageManager.GET_META_DATA);
82 | Bundle bundle = pi.metaData;
83 | int max =
84 | bundle.getInt(
85 | "com.bluechilli.flutteruploader.MAX_CONCURRENT_TASKS", DEFAULT_MAX_CONCURRENT_TASKS);
86 | Log.d(TAG, "MAX_CONCURRENT_TASKS = " + max);
87 | return max;
88 | } catch (PackageManager.NameNotFoundException e) {
89 | Log.e(TAG, "Failed to load meta-data, NameNotFound: " + e.getMessage());
90 | } catch (NullPointerException e) {
91 | Log.e(TAG, "Failed to load meta-data, NullPointer: " + e.getMessage());
92 | }
93 | return DEFAULT_MAX_CONCURRENT_TASKS;
94 | }
95 |
96 | public static int getConnectionTimeout(Context context) {
97 | try {
98 | ProviderInfo pi =
99 | context
100 | .getPackageManager()
101 | .getProviderInfo(
102 | new ComponentName(
103 | context, "com.bluechilli.flutteruploader.FlutterUploaderInitializer"),
104 | PackageManager.GET_META_DATA);
105 | Bundle bundle = pi.metaData;
106 | int max =
107 | bundle.getInt(
108 | "com.bluechilli.flutteruploader.UPLOAD_CONNECTION_TIMEOUT_IN_SECONDS",
109 | DEFAULT_UPLOAD_CONNECTION_TIMEOUT);
110 | Log.d(TAG, "UPLOAD_CONNECTION_TIMEOUT_IN_SECONDS = " + max);
111 | return max;
112 | } catch (PackageManager.NameNotFoundException e) {
113 | Log.e(TAG, "Failed to load meta-data, NameNotFound: " + e.getMessage());
114 | } catch (NullPointerException e) {
115 | Log.e(TAG, "Failed to load meta-data, NullPointer: " + e.getMessage());
116 | }
117 |
118 | return DEFAULT_UPLOAD_CONNECTION_TIMEOUT;
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/android/src/main/java/com/bluechilli/flutteruploader/FlutterUploaderPlugin.java:
--------------------------------------------------------------------------------
1 | package com.bluechilli.flutteruploader;
2 |
3 | import static com.bluechilli.flutteruploader.MethodCallHandlerImpl.FLUTTER_UPLOAD_WORK_TAG;
4 |
5 | import android.content.Context;
6 | import androidx.annotation.NonNull;
7 | import androidx.annotation.Nullable;
8 | import androidx.lifecycle.LiveData;
9 | import androidx.work.WorkInfo;
10 | import androidx.work.WorkManager;
11 | import com.bluechilli.flutteruploader.plugin.CachingStreamHandler;
12 | import com.bluechilli.flutteruploader.plugin.StatusListener;
13 | import com.bluechilli.flutteruploader.plugin.UploadObserver;
14 | import io.flutter.embedding.engine.plugins.FlutterPlugin;
15 | import io.flutter.plugin.common.BinaryMessenger;
16 | import io.flutter.plugin.common.EventChannel;
17 | import io.flutter.plugin.common.MethodChannel;
18 | import io.flutter.plugin.common.PluginRegistry.Registrar;
19 | import java.util.ArrayList;
20 | import java.util.Arrays;
21 | import java.util.Collections;
22 | import java.util.HashMap;
23 | import java.util.List;
24 | import java.util.Map;
25 |
26 | /** FlutterUploaderPlugin */
27 | public class FlutterUploaderPlugin implements FlutterPlugin, StatusListener {
28 |
29 | private static final String CHANNEL_NAME = "flutter_uploader";
30 | private static final String PROGRESS_EVENT_CHANNEL_NAME = "flutter_uploader/events/progress";
31 | private static final String RESULT_EVENT_CHANNEL_NAME = "flutter_uploader/events/result";
32 |
33 | private MethodChannel channel;
34 | private MethodCallHandlerImpl methodCallHandler;
35 | private UploadObserver uploadObserver;
36 |
37 | private EventChannel progressEventChannel;
38 | private final CachingStreamHandler