Abrova
☰ Docs menu · Flutter
Coming soon. Abrova Storage is coming soon. This page describes how it will work; it can’t be used yet.

Flutter SDK

Native Dart SDK for Flutter mobile and web. Sign users in with an Abrova user token (tokenProvider), upload, download, list, delete, image transforms, and a profile-image example with image_picker via the abrova_storage package.

Flutter 0.2.1Last updated: 27 Oct 2026

Native Dart SDK for Flutter mobile and web applications. Covers file uploads, downloads and image transformations.

Installation#

Install from Abrova Center in two steps.

yaml
# 1. pubspec.yaml: install from Abrova Center
dependencies:
  abrova_storage:
    hosted: https://center.abrova.ir/api/packages/abrova/pub
    version: ^0.2.1
bash
# 2. Install
flutter pub get

Initialization#

dart
import 'package:abrova_storage/abrova_storage.dart';

final storage = AbrovaStorage(
  apiKey: 'ab_live_xxxxxxxxxxxx',
  // The signed-in user's Abrova user token, see "Sign users in" below.
  // Return null when no one is signed in.
  tokenProvider: () => myBackend.fetchAbrovaToken(),
);

// Dispose when done (e.g., in your State's dispose())
@override
void dispose() {
  storage.dispose(); // calls still waiting for a user fail with code 'disposed'
  super.dispose();
}
Client-side only.

The Flutter SDK uses only the apiKey. File upload, read, list and delete work from the app and follow the bucket's access rules. Creating and changing buckets and access rules, and making signed links, belong on your backend (Node.js SDK with serverSecret). The SDK always connects to Abrova Storage over HTTPS; there is no baseUrl option.

Sign users in (one token for every Abrova product)#

File requests from apps always need a signed Abrova user token. There is no setting to turn this off. Bucket rules that use {userId} match the user in that token. It is the same token Abrova Sync, Chat, Push and A/B Testing accept.

With Abrova Auth, no backend is needed. Its token is this token:

dart
final auth = AbrovaAuth.instance;
final storage = AbrovaStorage(
  apiKey: 'ab_live_xxxxxxxxxxxx',
  tokenProvider: auth.tokenProvider,
);

// After sign-in or sign-out
auth.authStateChanges.listen((_) => storage.refreshUserToken());

With your own server, your backend mints the token with POST https://auth.abrova.ir/users/token and your server secret (it never ships in an app), and tokenProvider fetches it, as in step 2.

dart
// When the user signs in or out
await storage.refreshUserToken();

// Or pass a token you already hold (null signs out)
await storage.setUserToken(token);

// Or set / replace the provider later
await storage.setTokenProvider(() => myBackend.fetchAbrovaToken());

The SDK sends the token as x-user-token, asks the provider before the first request, and asks again (retrying the request once) when the token has expired or is refused.

Waiting for a user

With no token (no one signed in), file calls (upload, list, get, download, delete) are not sent and do not fail: they wait. They go on by themselves, in order, once a user signs in. This is by design: show your sign-in screen while they wait. Bucket reads (listBuckets, getBucket, getBucketByName) and getPublicUrl need no user and never wait.

dart
final storage = AbrovaStorage(
  apiKey: 'ab_live_xxxxxxxxxxxx',
  tokenProvider: auth.tokenProvider,
);
storage.onAwaitingUserToken(() => showSignIn()); // once per wait
storage.isAwaitingUserToken; // true while file calls wait for a user

A token the server keeps refusing throws the same exception with token_expired or token_invalid.

Don't wait on file calls before sign-in. A file call made while no one is signed in finishes only after sign-in. Don't wait for it in code the user is waiting on (a button handler, a startup check): start it in the background, or after sign-in.

Leave the timeout unset. By default a call waits until sign-in, which is right for background work. Set userTokenWaitTimeout only when a screen waits for the answer and can send the user to sign-in:

dart
final storage = AbrovaStorage(
  apiKey: 'ab_live_xxxxxxxxxxxx',
  tokenProvider: auth.tokenProvider,
  userTokenWaitTimeout: const Duration(seconds: 30), // throws token_required after 30 s
);

A call that waited that long throws an AbrovaStorageTokenException with code == 'token_required' (status 401). The timeout option is a different setting: it limits each network request (default 30 seconds) and never covers the wait for a user.

With Abrova Auth, turn on the sign-in method you use (for example Anonymous) in Abrova Auth › Sign-in methods. If it is off, sign-in fails with METHOD_DISABLED, no one is signed in, and file calls wait without an error. While developing, log from onAwaitingUserToken (debugPrint('Storage: waiting for a user')) so a silent wait is easy to spot.

Upgrading from 0.2.0: userId / setUserId() are removed. Use tokenProvider.

Upload Files#

dart
import 'dart:io';
import 'dart:typed_data';
import 'package:abrova_storage/abrova_storage.dart';

// Upload from Uint8List (bytes)
final Uint8List imageBytes = await File('path/to/image.jpg').readAsBytes();

final file = await storage.upload(
  'my-bucket',
  'images/photo.jpg',
  imageBytes,
  options: UploadOptions(
    contentType: 'image/jpeg',
    metadata: {'source': 'camera'},
  ),
  // Optional: about every 50 ms or 1%; the last call has sent == total
  onProgress: (sent, total) => setState(() => _progress = sent / total),
);

print('Uploaded: ${file.id}');
print('Size: ${file.size} bytes');

total is the whole upload request: your file plus a few hundred bytes of form fields. On Flutter web the browser takes the whole file at once, so progress jumps to 100% when the upload starts. When you pass no contentType, the SDK sends the type for the file's extension (photo.HEIC as image/heic, notes.md as text/markdown).

Paths. Pass paths as you write them: spaces, #, ? and non-Latin characters are encoded for you. A . or .. part is refused with invalid_path before anything is sent, and a new file's path can't contain ?, # or %.

Download Files#

dart
// Download file content as Uint8List
final Uint8List bytes = await storage.download(fileId);

// Save to local file
final file = File('${directory.path}/downloaded.jpg');
await file.writeAsBytes(bytes);

// Or display directly
Image.memory(bytes);

List Files#

dart
// List files in a bucket
final result = await storage.listFiles(
  'my-bucket',
  options: ListFilesOptions(
    prefix: 'images/',
    limit: 50,
  ),
);

for (final file in result.files) {
  print('${file.path}: ${file.size} bytes');
}

// Pagination: nextCursor is null on the last page
if (result.nextCursor != null) {
  final nextPage = await storage.listFiles(
    'my-bucket',
    options: ListFilesOptions(cursor: result.nextCursor),
  );
}

A StorageFile has id, bucketId, path, fileName, mimeType, size, checksum, metadata, uploadedBy, uploadedAt, createdAt and updatedAt (checksum, metadata, uploadedBy and uploadedAt are null when empty). A Bucket has id, name, description, visibility, allowedMimeTypes (null = every type), maxFileSize, blockExecutables, createdAt and updatedAt. Dates are DateTime (UTC).

Delete Files#

dart
// Deletes follow the bucket's delete rules
// Delete a file by ID
await storage.delete(fileId);

// Delete a file by path
await storage.deleteByPath('my-bucket', 'images/photo.jpg');

// Delete multiple files at once
final result = await storage.deleteMany([
  'file_id_1',
  'file_id_2',
  'file_id_3',
]);
print('Deleted ${result.deleted} files');

Show Images#

For a file in a public bucket, getPublicUrl returns a link that needs no key, token or header. With image options it returns a resized or converted image, ready for a bare Image.network(url):

dart
// A 600 px wide WebP, straight into Image.network
final url = await storage.getPublicUrl(file.id,
    transforms: const ImageTransforms(width: 600, format: 'webp'));
Image.network(url);

// A public link you stored earlier: the same URL, no request
final thumb = AbrovaStorage.publicImageUrl(savedLink,
    transforms: const ImageTransforms(width: 200, height: 200, fit: 'cover', format: 'webp'));
  • Cached, so use a few fixed sizes. The same options always give the same URL, made once and then served from the cache. One image keeps at most 50 variants; a new one beyond that is refused with too_many_variants (429). Pick a few sizes (for example 200, 600 and 1200 wide).
  • Usage. When the month's 500 free transforms are used up and your wallet cannot pay for more, an image URL with options answers plan_feature_locked (403). The plain public link always works.
  • Busy. transform_busy (503) comes with a Retry-After header in seconds. Image.network calls its errorBuilder: load the image again after that (for example on a retry tap).
  • Private buckets have no public link (getPublicUrl fails with 403). download the bytes, or load getTransformUrl with your headers:
dart
// The transform address needs the key and user token headers
final thumbnailUrl = storage.getTransformUrl(
  fileId,
  transforms: const ImageTransforms(width: 200, height: 200, fit: 'cover', format: 'webp'),
);
Image.network(
  thumbnailUrl,
  headers: {
    'x-api-key': 'ab_live_xxxxxxxxxxxx',
    'x-user-token': userToken, // the signed-in user's token
  },
);

// All available options
const options = ImageTransforms(
  width: 800,      // 1 or more (above 4096 sent as 4096)
  height: 600,
  fit: 'contain',  // cover, contain, fill, inside, outside
  format: 'webp',  // jpeg (default), png, webp, avif
  quality: 85,     // 1-100
  blur: 5,         // 0-20
  sharpen: 2,      // 0-10
  rotate: 90,      // degrees (multiples of 90 recommended)
);

getTransformUrl and publicImageUrl throw invalid_transform for any other value, before a URL is built. A signed link for a private file comes from your server (getSignedUrl in the Node.js SDK).

Complete Example: Profile Image Upload#

dart
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:abrova_storage/abrova_storage.dart';

class ProfileService {
  final AbrovaStorage _storage;

  ProfileService(String apiKey, AbrovaStorageTokenProvider tokenProvider)
      : _storage = AbrovaStorage(apiKey: apiKey, tokenProvider: tokenProvider);

  // Uploads to the public "avatars" bucket and returns a 200 px thumbnail link
  Future<String?> uploadProfileImage(String userId) async {
    final picker = ImagePicker();
    final pickedFile = await picker.pickImage(
      source: ImageSource.gallery,
      maxWidth: 1024,
      maxHeight: 1024,
      imageQuality: 85,
    );
    if (pickedFile == null) return null;

    final bytes = await pickedFile.readAsBytes();
    final file = await _storage.upload(
      'avatars',
      'users/$userId/avatar.jpg',
      bytes,
      options: UploadOptions(contentType: 'image/jpeg'),
    );

    return _storage.getPublicUrl(
      file.id,
      transforms: const ImageTransforms(width: 200, height: 200, fit: 'cover', format: 'webp'),
    );
  }

  // No headers needed for a public link
  Widget avatar(String url) => Image.network(url);

  void dispose() {
    _storage.dispose();
  }
}

Error Handling#

Every AbrovaStorageException has the server's message, the HTTP statusCode and, when the server named the reason, its code (constants in StorageErrorCode). code is null when the server gave no reason (a 404, a 429), and statusCode is null when no answer arrived.

dart
try {
  await storage.upload('photos', 'clips/a.mov', bytes);
} on AbrovaStorageTokenException catch (e) {
  // token_required / token_expired / token_invalid: send the user to sign in
  print('Sign-in needed: ${e.code}');
} on AbrovaStorageException catch (e) {
  if (e.isTypeNotAllowed) {
    showError("This bucket doesn't take this file type");
  } else if (e.isFileTooLarge) {
    showError('The file is too big');
  } else if (e.code == StorageErrorCode.pathTaken) {
    showError('A file with this name already exists');
  } else if (e.code == StorageErrorCode.accessDenied) {
    showError("You can't write here");
  } else {
    showError(e.message);
  }
}
StatuscodeWhen
401token_requiredNo user signed in within userTokenWaitTimeout.
401token_expired, token_invalidThe user token was refused and the provider had no other one.
403access_deniedThe bucket's access rules refused the request.
400type_not_allowed, file_too_large, executable_blockedThe upload doesn't fit the bucket's file settings.
409path_takenA file already exists at this path.
400invalid_pathThe path was refused (see Upload Files).
400invalid_transformAn image option is not allowed.
403plan_feature_lockedNo transforms left: the free monthly transforms are used up and the wallet cannot pay for more.
429too_many_variantsThis image already has 50 sizes / formats.
503transform_busyImage processing is busy. Retry after Retry-After seconds.
-disposeddispose() was called while the call waited for a user.

API Reference#

OptionTypeRequiredDescription
listBuckets()--List all buckets in the project
getBucket(id)--Get bucket by ID
getBucketByName(name)--Get bucket by name
upload(bucket, path, data, options:, onProgress:)--Upload a file to a bucket, with optional progress
listFiles(bucket, options:)--List files in a bucket
getFile(id)--Get file metadata by ID
getFileByPath(bucket, path)--Get file metadata by path
download(id)--Download file content as bytes
delete(id)--Delete a file by ID
deleteByPath(bucket, path)--Delete a file by path
deleteMany(ids)--Delete multiple files at once
getPublicUrl(id, transforms:)--Public link (public buckets only), optionally a resized / converted image; needs no headers
AbrovaStorage.publicImageUrl(publicUrl, transforms:)--Build the image link from a saved public link, without a request
getUrl(id)--Address of the public-link endpoint; needs x-api-key and answers { url } for files in a public bucket
getTransformUrl(id, transforms:)--Transform address; needs the x-api-key and x-user-token headers
getDownloadUrl(id)--Download address; needs the x-api-key and x-user-token headers
refreshUserToken()--Ask the token provider again (call on sign-in / sign-out)
setUserToken(token)--Use this Abrova user token; null signs out
setTokenProvider(provider)--Set or replace the token provider
isAwaitingUserToken--True while file calls wait for a user
onAwaitingUserToken(callback)--Called once each time file calls start waiting for a user
dispose()--Close the HTTP client; waiting calls fail with code disposed
Was this page helpful?
Report a problem