Logo

dev-resources.site

for different kinds of informations.

Offline file uploading in Flutter

Published at
12/2/2024
Categories
flutter
uploading
offline
Author
remejuan
Categories
3 categories in total
flutter
open
uploading
open
offline
open
Author
8 person written this
remejuan
open
Offline file uploading in Flutter

The Problem

Previously image storage was a pretty low usage feature of our application, only storing small digital copies so a signature that where often under 10kb each, more recently we had clients wanting to capture photos and even multiple photos per order.

Our current storage option posed a problem, as users of Firebase for it’s realtime capabilities and backed in offline support, we simply stored our images in a dedicated collection as base64 string, when the files are less than 10kb and there is always only ever 1 of them, this is not an issue, however Firebase has a document size limit of 1024kb.

Anyone using a modern phone can see the issue here, even capturing photos at 20% quality still resulted in many users being able to successfully store 0 photos. Both iPhone 15 owners on the team had that problem.

The solution (Part 1)

When starting to solution this we went with Firebase storage as it also had a level of offline and automated resume functionality, but to be sure we where happy with it, we literally tested in in production for about 4 months as an additional storage option alongside Firestore, this way assuming all went well this clients using newer phones would still have access to images, albeit via a support channel.

This worked well, or so we thought as in testing everything worked great whether the device was on or offline, so we went ahead and began updating the rest of the system to stop using the Firestore for accessing the files.

The Curve ball

Soon after starting this process one of our clients reported issues with this file uploading, the one thing we where never really be able to test, and honestly never thought of, was testing under garbage conditions, while Firebase Storage worked well with working internet and no internet, it was a nightmare when the internet was trash, what we had no noticed is that as long as there was a connection it would wait and continuously retry to start the upload, before allowing any form of actual offline/resume support.

This meant many drivers where stuck at clients for considerable amounts of time, some reporting up to 30min simply trying to upload small images, looking at the storage many of which around 500kb, not a problem under normal circumstances, but a nightmare when the internet is complete garbage.

Back to the drawing board

We decided to rework the solution to be completely offline first and support scheduled resume and isolate running.

To pull this off I started by adding Drift and Workmanager to the project, Drift being a pretty nice and easy to use local database option and Workmanager a background schedule, like a CRON.

The process was pretty simple, instead of sending the pending uploads directly to Firebase Storage, we first wrote it to Drift, storing all relevant data along with the Uint8List (BlobColumn in drift) image data.

The actual upload workflow was conditioned based on exiting connectivity logic that relies both on the device being connected and successful pings to Google to verify a working connection.

Uploading

Once we had everything on the DB and where in a position to begin uploading, we used one of Drift’s built in capabilities of DB Isolates, so spin off an instance of the DB into an isolated and begin the uploading within that instead of our own isolate or on the UI thread.

Here we have the uploadFiles method which makes use of Drifts computeWithDatabse method, this is typically used for dealing with computational queries that can lock up the UI, but works equally as well for dealing with background and scheduled tasks.

@pragma('vm:entry-point')
Future<void> uploadFiles(AppDatabase database) async {
  final token = RootIsolateToken.instance!;

  await database.computeWithDatabase(
    computation: (database) async {
      BackgroundIsolateBinaryMessenger.ensureInitialized(token);

      await databaseFileUpload(database);

      return Future.value(true);
    },
    connect: (connection) => AppDatabase(connection),
  );
}
Enter fullscreen mode Exit fullscreen mode

There are 2 methods here, computation which handles the processing of your DB transactions and connect which naturally handles connecting to your database.

You can follow Drifts docs for the setup, but the small change that needs to be made to facilitate this is your database needs to optionally take a QueryExecutor

AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());
Enter fullscreen mode Exit fullscreen mode

This allows you to pass in an existing connection instead of it opening up it’s own one which would be the _openConnection function you will see above.

This was working great online, but for reasons I never had time to understand, Firebase Storage’s uploading was doing something that simply broke the isolates so we where never able to upload images successfully offline or via the scheduler.

As for Workmanager their docs detail the setup and usage pretty well, to support the background process I am calling the uploadFiles function above and wherein an instance of the AppDatabase is passed in, for the rest of the app I found it simplest to register AppDatabase as a singleton on get_it as apposed to a Riverpod provider within the app, I was running into warning about opening multiple instances and while there is probably a correct way to have done it with Riverpod as Drift themselves has examples using it, time crunch…

Plan C??

Luckily I know just enough NodeJS/NestJS to be dangerous and kinda useful on the BE, so Saturday morning before 5am, unable to let this bug and really entertaining challenge go, I spent up a very empty endpoint on our BE to test with, all it did was simply logout the data.

A few quick tests via Postman to make sure it worked, I went back into the app and pulled out firebase Storage and connected up our REST endpoint and a few small code tweaks I was uploading online, offline and in the background.


Off to Seapoint for a run with the dogs… (No I did not take this particular Saturday off)


Photo of a dog

So while Firebase Storage appears to be quite unhappy running in a background Isolate, http was very happy to oblige.

The HTTP flow to support background workflows is pretty straight forward, but there are some caveats.

@pragma('vm:entry-point')
Future<String?> _backgroundFileUpload(
  UploadRequestData data,
  Uint8List imageBuffer,
) async {
  final sharedPreferences = await SharedPreferences.getInstance()
    ..reload();

  final bearerToken = sharedPreferences.getString(BEARER_TOKEN_KEY)!;
  final url = sharedPreferences.getString(API_URL)!;

  final headers = {
    "content-type": "application/json",
    "Authorization": "Bearer $bearerToken",
    "x-app-version": APP_VERSION,
  };

  final uri = Uri.https(url, '/api/upload/pod');

  final request = http.MultipartRequest('POST', uri);

  request.headers.addAll(headers);

  request.files.add(
    http.MultipartFile.fromBytes(
      'image', // The key name as expected by the API
      Uint8List.fromList(imageBuffer),
      contentType: MediaType('image', 'jpeg'),
      filename: data.file_name,
    ),
  );

  request.fields.addAll({
    'task_id': data.task_id,
    'file_name': data.file_name,
    'type': data.type,
    'status': data.status,
    'timestamp': data.timestamp,
  });

  final response = await request.send();

  if (response.statusCode != 201) {
    return null;
  }

  return response.stream.bytesToString();
}
Enter fullscreen mode Exit fullscreen mode

As with most apps we make use of environment variables for the different APIs we need to connect to (Dev/Staging/Prod), SharedPreferences is one of the simplest ways to bring these variables into an Isolate, being stored in a file on device you simple need to ensure you reload before accessing any data to ensure that you not only have the data, but also the latest version of it in that Isolate.

From there, everything works as it would for nay MultipartFile upload, while we are storing the Uint8List in the BlobColumn in Drift, I found that simply passing that to the API resulted in errors and had to convert the BlobColumn into a Uint8List again.

Wrapping Up

From here on out it really was just clean up and some more testing, solving a few issues in code that only showed up in the release build, making sure the API actually saved the files and that all relevant DB documents where updated in Firebase and the cleanup process for successful uploads was done within the app.

offline Article's
30 articles in total
Favicon
Offline file uploading in Flutter
Favicon
It`s time to ditch the Thunder Client VSCode Extension! đź’Ą
Favicon
Local First from Scratch - How to make a web app with local data
Favicon
How having a Data Layer simplified Offline Mode in my frontend app - Part 1
Favicon
Nesktop: Offline "Desktop" Next.js App
Favicon
Flutter:Hive With Api
Favicon
VScode For Android.
Favicon
-STORYTIME- Il tente de déployer sans Internet, ça tourne mal
Favicon
Transform your React Native app with offline audio & video downloads!
Favicon
Angular PWA & Service Workers (install app, push notifications, offline cache and updates)
Favicon
React Query Mutations Offline React-Native
Favicon
How can I play surf.jackbuehner.com offline?
Favicon
PrivateGPT - Running "ChatGPT" offline on local documents
Favicon
Epson Printer Offline Mac Fix Epson Offline on Mac
Favicon
Why Is My Printer Offline When I Print?
Favicon
CometChat Offline Support in React Native
Favicon
iOS — Designing Data Layer For Offline-first Apps
Favicon
Eight Best Free Offline Android Games of 2022
Favicon
How to create a Offline Internationalization App:Source code and real app
Favicon
How to create a Offline Internationalization App:Support multiple languages
Favicon
How to create a Offline Internationalization App:Use Sqlite database
Favicon
How to create a Offline Internationalization App:Data modeling
Favicon
How to create a Offline Internationalization App:Build the project structure
Favicon
How to create a Offline Internationalization App:Technology
Favicon
⚙️Install anything without Admin rights
Favicon
Regarding cross platform offline data transfer between 2 mobile devices
Favicon
Challenges with Offline First Framework
Favicon
How to convert jpg/jpeg (images) to pdf offline?
Favicon
React Navigator Status
Favicon
Build a PWA using Workbox

Featured ones: