Upload migration
The API has been simplified by internalizing the UploadStateNotifier class. All functionality remains the same, but the API is now cleaner and easier to use.
// Accessing state through notifier property
manager.notifier.stream.listen((state) {
print('Progress: ${state.inProgressCount}');
});
print('Total: ${manager.notifier.totalCount}');
manager.notifier.clearErrors();
// Direct access to stream and stats
manager.stateStream.listen((stats) {
print('Progress: ${stats.inProgressCount}');
});
print('Total: ${manager.currentStats.totalCount}');
manager.clearErrors(); // Now a direct method
Before:
uploadManager.notifier.stream.listen((state) { ... });
After:
uploadManager.stateStream.listen((stats) { ... });
Before:
manager.notifier.totalCount
manager.notifier.inProgressCount
manager.notifier.completedCount
manager.notifier.failedCount
manager.notifier.waitingCount
manager.notifier.hasErrors
manager.notifier.errorCount
manager.notifier.isFull
After:
manager.currentStats.totalCount
manager.currentStats.inProgressCount
manager.currentStats.completedCount
manager.currentStats.failedCount
manager.currentStats.waitingCount
manager.currentStats.hasErrors
manager.currentStats.errorCount
manager.currentStats.isFull
Before:
uploadManager.notifier.clearErrors();
After:
uploadManager.clearErrors();
For clarity, rename state → stats in stream listeners:
Before:
manager.notifier.stream.listen((state) {
print('${state.inProgressCount} uploads');
});
After:
manager.stateStream.listen((stats) {
print('${stats.inProgressCount} uploads');
});
Before:
StreamBuilder(
stream: uploadManager.notifier.stream,
builder: (context, snapshot) {
if (!snapshot.hasData) return SizedBox();
final state = snapshot.data!;
return Text('${state.inProgressCount} uploads');
},
)
After:
StreamBuilder(
stream: uploadManager.stateStream,
builder: (context, snapshot) {
if (!snapshot.hasData) return SizedBox();
final stats = snapshot.data!;
return Text('${stats.inProgressCount} uploads');
},
)
Before:
if (manager.notifier.hasErrors) {
showDialog(
context: context,
builder: (_) => AlertDialog(
title: Text('Upload Errors'),
content: Text('${manager.notifier.errorCount} uploads failed'),
actions: [
TextButton(
onPressed: () {
manager.notifier.clearErrors();
Navigator.pop(context);
},
child: Text('Dismiss'),
),
],
),
);
}
After:
if (manager.currentStats.hasErrors) {
showDialog(
context: context,
builder: (_) => AlertDialog(
title: Text('Upload Errors'),
content: Text('${manager.currentStats.errorCount} uploads failed'),
actions: [
TextButton(
onPressed: () {
manager.clearErrors();
Navigator.pop(context);
},
child: Text('Dismiss'),
),
],
),
);
}
Before:
manager.notifier.stream.listen((state) {
final progress = state.completedCount / state.totalCount;
updateProgressBar(progress);
if (state.completedCount == state.totalCount) {
showCompletionNotification();
}
});
After:
manager.stateStream.listen((stats) {
final progress = stats.completedCount / stats.totalCount;
updateProgressBar(progress);
if (stats.completedCount == stats.totalCount) {
showCompletionNotification();
}
});
The following APIs remain unchanged:
- ✅
uploadSync()- fire-and-forget uploads - ✅
uploadAsync()- awaitable uploads - ✅
cancelTask(taskId)- cancel individual task - ✅
cancelAll()- cancel all tasks - ✅
setUser(uid)- change user session - ✅
dispose()- cleanup resources - ✅
getNextTask()- get next ready task
// Before: Confusing - what's the difference?
manager.uploadSync(...)
manager.notifier.addTask(...) // ❌ Don't expose this!
// After: Clear single way to do things
manager.uploadSync(...) // ✅ Only way
- Internal state management methods are now private
- Users can’t accidentally misuse
addTask(),nextReadyTask(), etc. - Implementation details hidden
UploadStatsis immutable and read-only- No way to accidentally modify state
- Clear separation: actions (methods) vs observations (stream)
- Fewer public classes to learn
- Simpler documentation
- Easier maintenance
The stream now emits UploadStats instead of UploadState:
class UploadStats {
final int waitingCount;
final int inProgressCount;
final int completedCount;
final int failedCount;
final int totalCount;
final bool isFull;
bool get hasErrors => failedCount > 0;
int get errorCount => failedCount;
}
Key Difference: UploadStats is read-only, preventing accidental mutations.
Use these patterns in your IDE:
Find:
\.notifier\.stream
Replace:.stateStreamFind:
\.notifier\.
Replace:.currentStats.Find:
(state)
Replace:(stats)(in stream listeners)Find:
final state = snapshot.data
Replace:final stats = snapshot.dataFind:
manager\.notifier\.clearErrors\(\)
Replace:manager.clearErrors()
Q: Do I need to change my test code?
A: Yes, update manager.notifier.stream → manager.stateStream in tests.
Q: What happened to UploadStateNotifier?
A: It’s now internal to UploadTaskManager. You shouldn’t need to reference it directly.
Q: Can I still listen to multiple streams?
A: Yes! stateStream is still a broadcast stream.
Q: Will my old code break?
A: Yes, but it’s a simple find-and-replace fix. See the search patterns above.
Q: Do I lose any functionality?
A: No! Everything works the same, just with a cleaner API.
If you encounter issues during migration:
- Check that all
notifier.stream→stateStream - Check that all
notifier.someProperty→currentStats.someProperty - Check that
statevariables in listeners are renamed tostats - Check that
notifier.clearErrors()→clearErrors()
The compiler will catch most issues with helpful error messages.
- ✅ Simpler: One way to access state instead of two
- ✅ Safer: Read-only stats, no accidental mutations
- ✅ Cleaner: Internal methods are truly private
- ✅ Same Power: All functionality preserved
Happy uploading! 🚀