UploadTaskManager API Simplification - Migration Guide

What Changed?

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.

Before vs After

Before (Old API)

// Accessing state through notifier property
manager.notifier.stream.listen((state) {
  print('Progress: ${state.inProgressCount}');
});

print('Total: ${manager.notifier.totalCount}');
manager.notifier.clearErrors();

After (New Simplified API)

// 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

Migration Steps

1. Replace notifier.stream with stateStream

Before:

uploadManager.notifier.stream.listen((state) { ... });

After:

uploadManager.stateStream.listen((stats) { ... });

2. Replace notifier getters with currentStats

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

3. Replace notifier.clearErrors() with clearErrors()

Before:

uploadManager.notifier.clearErrors();

After:

uploadManager.clearErrors();

4. Update Stream Variable Names

For clarity, rename statestats in stream listeners:

Before:

manager.notifier.stream.listen((state) {
  print('${state.inProgressCount} uploads');
});

After:

manager.stateStream.listen((stats) {
  print('${stats.inProgressCount} uploads');
});

Complete Examples

Widget Integration

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');
  },
)

Error Handling UI

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'),
        ),
      ],
    ),
  );
}

Progress Tracking

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();
  }
});

What’s the Same?

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

Benefits of New API

1. Cleaner Public Interface

// 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

2. Better Encapsulation

  • Internal state management methods are now private
  • Users can’t accidentally misuse addTask(), nextReadyTask(), etc.
  • Implementation details hidden

3. Type Safety

  • UploadStats is immutable and read-only
  • No way to accidentally modify state
  • Clear separation: actions (methods) vs observations (stream)

4. Less API Surface

  • Fewer public classes to learn
  • Simpler documentation
  • Easier maintenance

New Type: UploadStats

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.

Search & Replace Guide

Use these patterns in your IDE:

  1. Find: \.notifier\.stream
    Replace: .stateStream

  2. Find: \.notifier\.
    Replace: .currentStats.

  3. Find: (state)
    Replace: (stats) (in stream listeners)

  4. Find: final state = snapshot.data
    Replace: final stats = snapshot.data

  5. Find: manager\.notifier\.clearErrors\(\)
    Replace: manager.clearErrors()

FAQs

Q: Do I need to change my test code?
A: Yes, update manager.notifier.streammanager.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.

Need Help?

If you encounter issues during migration:

  1. Check that all notifier.streamstateStream
  2. Check that all notifier.somePropertycurrentStats.someProperty
  3. Check that state variables in listeners are renamed to stats
  4. Check that notifier.clearErrors()clearErrors()

The compiler will catch most issues with helpful error messages.

Summary

  • 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! 🚀