flutter
/

BLoC File Upload & Download: Progress Tracking & Cancellation

Last Sync: Today

On this page

9
0%
Advanced
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutterAdvanced

BLoC File Upload & Download: Progress Tracking & Cancellation

File upload and download are common tasks in many Flutter apps. Managing progress, cancellations, and errors while keeping the UI responsive can be challenging. By combining BLoC with Dio (or http), you can build a clean, reactive system that handles upload/download operations with ease. This guide covers everything from setting up a repository with progress tracking to managing cancellation and showing real‑time progress in the UI.

Why Use BLoC for Upload/Download?

  • Reactive UI – Progress updates automatically drive UI changes.
  • Separation of concerns – Network logic stays in repositories, UI only reacts to state.
  • Cancellation support – BLoC can easily cancel ongoing requests.
  • Error handling – Dedicated error states provide user feedback.
  • Testability – Repositories and blocs can be unit‑tested with mocked clients.

Dependencies

YAMLRead-only
1
dependencies:
  flutter:
    sdk: flutter
  flutter_bloc: ^8.1.5
  equatable: ^2.0.5
  dio: ^5.4.0
  get_it: ^7.6.7
  file_picker: ^6.0.0  # for picking files
  path_provider: ^2.1.0
  permission_handler: ^11.0.0

  1. Upload Implementation

We'll create a repository that uses Dio to upload a file. The repository will expose a stream for progress updates and return a Future that completes when the upload finishes.

DARTRead-only
1
import 'package:dio/dio.dart';
import 'package:path/path.dart' as path;

abstract class UploadRepository {
  Future<String> uploadFile(String filePath, Function(double progress) onProgress);
  void cancelUpload();
}

class UploadRepositoryImpl implements UploadRepository {
  final Dio dio;
  CancelToken? _cancelToken;

  UploadRepositoryImpl(this.dio);

  @override
  Future<String> uploadFile(String filePath, Function(double progress) onProgress) async {
    _cancelToken = CancelToken();
    final file = await MultipartFile.fromFile(filePath,
        filename: path.basename(filePath));
    final formData = FormData.fromMap({
      'file': file,
    });

    try {
      final response = await dio.post(
        'https://your-api.com/upload',
        data: formData,
        cancelToken: _cancelToken,
        onSendProgress: (sent, total) {
          final progressPercent = sent / total;
          onProgress(progressPercent);
        },
      );
      return response.data['url'] as String;
    } catch (e) {
      if (CancelToken.isCancel(e)) {
        throw Exception('Upload cancelled');
      }
      rethrow;
    } finally {
      _cancelToken = null;
    }
  }

  @override
  void cancelUpload() {
    _cancelToken?.cancel('Upload cancelled by user');
  }
}
DARTRead-only
1
import 'package:equatable/equatable.dart';

abstract class UploadState extends Equatable {
  const UploadState();
  @override List<Object?> get props => [];
}

class UploadInitial extends UploadState {}

class UploadInProgress extends UploadState {
  final double progress;
  const UploadInProgress(this.progress);
  @override List<Object?> get props => [progress];
}

class UploadSuccess extends UploadState {
  final String url;
  const UploadSuccess(this.url);
  @override List<Object?> get props => [url];
}

class UploadFailure extends UploadState {
  final String message;
  const UploadFailure(this.message);
  @override List<Object?> get props => [message];
}
DARTRead-only
1
import 'package:equatable/equatable.dart';

abstract class UploadEvent extends Equatable {
  const UploadEvent();
  @override List<Object?> get props => [];
}

class StartUpload extends UploadEvent {
  final String filePath;
  const StartUpload(this.filePath);
  @override List<Object?> get props => [filePath];
}

class CancelUpload extends UploadEvent {}
DARTRead-only
1
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../repositories/upload_repository.dart';
import 'upload_event.dart';
import 'upload_state.dart';

class UploadBloc extends Bloc<UploadEvent, UploadState> {
  final UploadRepository repository;

  UploadBloc(this.repository) : super(UploadInitial()) {
    on<StartUpload>(_onStartUpload);
    on<CancelUpload>(_onCancelUpload);
  }

  Future<void> _onStartUpload(StartUpload event, Emitter<UploadState> emit) async {
    emit(UploadInProgress(0.0));
    try {
      final url = await repository.uploadFile(
        event.filePath,
        (progress) => add(UploadProgressEvent(progress)), // custom internal event
      );
      emit(UploadSuccess(url));
    } catch (e) {
      emit(UploadFailure(e.toString()));
    }
  }

  void _onCancelUpload(CancelUpload event, Emitter<UploadState> emit) {
    repository.cancelUpload();
    emit(UploadInitial());
  }

  // Internal event for progress updates (optional, can be done via stream inside repository)
  void updateProgress(double progress) {
    if (state is UploadInProgress) {
      emit(UploadInProgress(progress));
    }
  }
}

// Alternative: use a stream inside the repository to emit progress

  1. Download Implementation

DARTRead-only
1
import 'package:dio/dio.dart';
import 'dart:io';

abstract class DownloadRepository {
  Future<String> downloadFile(String url, String savePath, Function(double) onProgress);
  void cancelDownload();
}

class DownloadRepositoryImpl implements DownloadRepository {
  final Dio dio;
  CancelToken? _cancelToken;

  DownloadRepositoryImpl(this.dio);

  @override
  Future<String> downloadFile(String url, String savePath, Function(double) onProgress) async {
    _cancelToken = CancelToken();
    try {
      await dio.download(
        url,
        savePath,
        cancelToken: _cancelToken,
        onReceiveProgress: (received, total) {
          if (total != -1) {
            final progress = received / total;
            onProgress(progress);
          }
        },
      );
      return savePath;
    } catch (e) {
      if (CancelToken.isCancel(e)) {
        throw Exception('Download cancelled');
      }
      rethrow;
    } finally {
      _cancelToken = null;
    }
  }

  @override
  void cancelDownload() {
    _cancelToken?.cancel('Download cancelled by user');
  }
}
DARTRead-only
1
abstract class DownloadState extends Equatable {
  const DownloadState();
  @override List<Object?> get props => [];
}

class DownloadInitial extends DownloadState {}

class DownloadInProgress extends DownloadState {
  final double progress;
  const DownloadInProgress(this.progress);
  @override List<Object?> get props => [progress];
}

class DownloadSuccess extends DownloadState {
  final String filePath;
  const DownloadSuccess(this.filePath);
  @override List<Object?> get props => [filePath];
}

class DownloadFailure extends DownloadState {
  final String message;
  const DownloadFailure(this.message);
  @override List<Object?> get props => [message];
}
DARTRead-only
1
abstract class DownloadEvent extends Equatable {
  const DownloadEvent();
  @override List<Object?> get props => [];
}

class StartDownload extends DownloadEvent {
  final String url;
  final String savePath;
  const StartDownload(this.url, this.savePath);
  @override List<Object?> get props => [url, savePath];
}

class CancelDownload extends DownloadEvent {}
DARTRead-only
1
class DownloadBloc extends Bloc<DownloadEvent, DownloadState> {
  final DownloadRepository repository;

  DownloadBloc(this.repository) : super(DownloadInitial()) {
    on<StartDownload>(_onStartDownload);
    on<CancelDownload>(_onCancelDownload);
  }

  Future<void> _onStartDownload(StartDownload event, Emitter<DownloadState> emit) async {
    emit(DownloadInProgress(0.0));
    try {
      final filePath = await repository.downloadFile(
        event.url,
        event.savePath,
        (progress) => add(DownloadProgressEvent(progress)), // internal
      );
      emit(DownloadSuccess(filePath));
    } catch (e) {
      emit(DownloadFailure(e.toString()));
    }
  }

  void _onCancelDownload(CancelDownload event, Emitter<DownloadState> emit) {
    repository.cancelDownload();
    emit(DownloadInitial());
  }

  void updateProgress(double progress) {
    if (state is DownloadInProgress) {
      emit(DownloadInProgress(progress));
    }
  }
}

UI Integration

Now we can use these blocs in our UI. For upload, we can pick a file and start the upload with progress indicator and cancel button.

DARTRead-only
1
class UploadPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Upload File')),
      body: BlocConsumer<UploadBloc, UploadState>(
        listener: (context, state) {
          if (state is UploadSuccess) {
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(content: Text('Uploaded: ${state.url}')),
            );
          }
          if (state is UploadFailure) {
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(content: Text(state.message)),
            );
          }
        },
        builder: (context, state) {
          if (state is UploadInProgress) {
            return Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  LinearProgressIndicator(value: state.progress),
                  SizedBox(height: 10),
                  Text('${(state.progress * 100).toStringAsFixed(0)}%'),
                  ElevatedButton(
                    onPressed: () => context.read<UploadBloc>().add(CancelUpload()),
                    child: Text('Cancel'),
                  ),
                ],
              ),
            );
          }
          return Center(
            child: ElevatedButton(
              onPressed: () async {
                final result = await FilePicker.platform.pickFiles();
                if (result != null) {
                  context.read<UploadBloc>().add(StartUpload(result.files.single.path!));
                }
              },
              child: Text('Pick File and Upload'),
            ),
          );
        },
      ),
    );
  }
}

Handling Multiple Files

For multiple uploads/downloads, you may want to have separate blocs per item or keep a list of states in a single bloc. Using a map of CancelTokens and progress streams can help manage concurrent operations.

DARTRead-only
1
// Example: Managing multiple uploads
class MultiUploadState {
  final Map<String, UploadStatus> statuses; // key = file name
}

// You can use a map of CancelTokens in the repository and use an ID to cancel specific uploads.

Best Practices

  • Use CancelToken – Always allow cancellation for long‑running uploads/downloads.
  • Handle network errors – Show user‑friendly messages and allow retry.
  • Provide progress feedback – Use LinearProgressIndicator or similar to keep users informed.
  • Dispose of resources – Cancel any pending requests in dispose of the bloc or widget.
  • Use Equatable – To avoid unnecessary UI rebuilds.
  • Test with mocked Dio – Use MockClient or Dio with HttpClientAdapter for testing.
  • Request permissions – For Android, you may need WRITE_EXTERNAL_STORAGE or scoped storage permissions for downloads.

Common Mistakes

  • ❌ Not cancelling tokens when the screen is disposed – May cause memory leaks or crashes. ✅ Cancel in close() or dispose of bloc.
  • ❌ Not handling CancelToken.isCancel – May show error messages for cancelled requests. ✅ Check and handle gracefully.
  • ❌ Not checking storage permissions – Download may fail on Android 10+. ✅ Request permissions or use scoped storage.
  • ❌ Blocking UI with large files – Use onSendProgress and onReceiveProgress to keep UI responsive.
  • ❌ Using setState inside progress callbacks – May cause rebuilds outside the widget tree. ✅ Emit states from the bloc and rebuild via BlocBuilder.

Conclusion

BLoC provides an elegant way to handle file uploads and downloads in Flutter. By combining it with Dio's progress and cancellation capabilities, you can build responsive, user‑friendly interfaces that keep the user informed about long‑running operations. The pattern scales well for multiple files and can be easily tested. Remember to always provide feedback and cancellation options for a great user experience.

Test Your Knowledge

Q1
of 3

What is the purpose of `CancelToken` in Dio?

A
To authenticate requests
B
To cancel ongoing requests
C
To set request timeouts
D
To add headers
Q2
of 3

How do you update UI with upload progress using BLoC?

A
By calling `setState` inside the progress callback
B
By emitting a new state with the progress value
C
By using a `StreamBuilder` directly
D
By passing a callback to the widget
Q3
of 3

What should you do in the bloc's `close` method for an ongoing upload?

A
Nothing, it's automatic
B
Cancel the upload using the repository's cancel method
C
Wait for the upload to finish
D
Restart the upload

Frequently Asked Questions

Can I use `http` instead of `dio` for upload/download with progress?

The built-in http package does not support progress callbacks. You would need to use dio or http with custom streaming. Dio is recommended for such use cases.

How do I save files to external storage on Android?

On Android 10+, you should use scoped storage. You can use path_provider to get a directory like getExternalStorageDirectory() and request appropriate permissions. Alternatively, use file_picker to let the user choose a save location.

How to handle upload of very large files?

Use dio with onSendProgress to monitor progress. You might also consider chunked uploads or using a background service to keep the upload alive if the app is backgrounded.

How to test upload/download blocs?

Use bloc_test and mock the repository. You can simulate progress callbacks and verify that states are emitted correctly. For integration tests, you can use a local test server.

Can I show multiple simultaneous uploads in a list?

Yes. You can have a single bloc that holds a list of upload items, each with its own progress and cancel token. Each item can be identified by a unique ID, and you can update its status accordingly.

Previous

bloc search filter

Next

bloc offline support

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.