This new unified API system eliminates 90% of the boilerplate code while maintaining clean architecture principles.
To add a single API endpoint, you needed to create:
features/my_feature/
├── data/
│ ├── datasources/
│ │ └── my_remote_data_source.dart (50+ lines)
│ ├── models/
│ │ └── my_model.dart (30+ lines)
│ └── repositories/
│ └── my_repository_impl.dart (60+ lines)
├── domain/
│ ├── entities/
│ │ └── my_entity.dart (20+ lines)
│ ├── repositories/
│ │ └── my_repository.dart (15+ lines)
│ └── usecases/
│ ├── get_my_items.dart (15+ lines)
│ ├── create_my_item.dart (15+ lines)
│ └── delete_my_item.dart (15+ lines)
└── presentation/
├── providers/
│ ├── my_providers.dart (40+ lines)
│ └── my_state_provider.dart (80+ lines)
└── pages/
└── my_page.dart (100+ lines)
Total: ~12 files, 440+ lines of boilerplate code! 😱
Now you only need:
// 1. Define your model (20 lines)
class MyItem {
final int id;
final String name;
MyItem({required this.id, required this.name});
factory MyItem.fromJson(Map<String, dynamic> json) => MyItem(
id: json['id'],
name: json['name'],
);
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
};
}
// 2. Configure the API (5 lines)
final myApiConfig = ApiConfig<MyItem>(
endpoint: '/my-items',
fromJson: MyItem.fromJson,
toJson: (item) => item.toJson(),
getId: (item) => item.id,
);
// 3. Create providers (2 lines)
final myItemsProvider = ApiServiceFactory.createListProvider(myApiConfig);
final myItemProvider = (int id) => ApiServiceFactory.createItemProvider(myApiConfig, id);
// 4. Use in your UI (30 lines)
class MyPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(myItemsProvider);
return Scaffold(
body: state.isLoading
? CircularProgressIndicator()
: ListView.builder(
itemCount: state.items.length,
itemBuilder: (context, index) => ListTile(
title: Text(state.items[index].name),
onTap: () => ref.read(myItemsProvider.notifier).delete(state.items[index]),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(myItemsProvider.notifier).create({
'name': 'New Item',
}),
),
);
}
}Total: 1 file, ~57 lines of actual code! 🎉
- No more data sources, repository implementations, use cases
- No more complex provider setup
- Just define your model and configuration
final notifier = ref.read(myItemsProvider.notifier);
// All these work automatically:
await notifier.loadData(); // GET /my-items
await notifier.refresh(); // GET /my-items (with refresh indicator)
await notifier.create(data); // POST /my-items
await notifier.update(item, data); // PUT /my-items/{id}
await notifier.delete(item); // DELETE /my-items/{id}final state = ref.watch(myItemsProvider);
// Automatic state handling:
state.items; // List of your items
state.isLoading; // Loading indicator
state.isRefreshing; // Pull-to-refresh state
state.error; // Error messagesEverything is fully type-safe with generics. No casting or runtime errors.
Automatic error handling with proper user-friendly messages:
- Network timeouts
- Server errors
- Connection issues
- Validation errors
// Read-only API (GET operations only)
final readOnlyConfig = ApiConfig<MyItem>(
endpoint: '/my-items',
fromJson: MyItem.fromJson,
// No toJson or getId needed
);
// Full CRUD API
final crudConfig = ApiConfig<MyItem>(
endpoint: '/my-items',
fromJson: MyItem.fromJson,
toJson: (item) => item.toJson(),
getId: (item) => item.id,
);class ItemsPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(itemsProvider);
if (state.isLoading) return LoadingWidget();
if (state.error != null) return ErrorWidget(state.error);
return RefreshIndicator(
onRefresh: () => ref.read(itemsProvider.notifier).refresh(),
child: ListView.builder(
itemCount: state.items.length,
itemBuilder: (context, index) => ItemTile(state.items[index]),
),
);
}
}// Create new item
ref.read(itemsProvider.notifier).create({
'name': 'New Item',
'description': 'Item description',
});
// Update existing item
ref.read(itemsProvider.notifier).update(item, {
'name': 'Updated Name',
});
// Delete item
ref.read(itemsProvider.notifier).delete(item);// If you need custom operations beyond CRUD
final apiService = ref.read(itemApiServiceProvider);
// Custom endpoint call
final result = await apiService.getList(
endpoint: '/my-items/featured',
queryParams: {'limit': 10},
);Don't delete anything yet. The new system works alongside the old one.
For each feature, create a simple model with fromJson/toJson methods.
Create ApiConfig instances for your endpoints.
Use ApiServiceFactory to create providers with one line each.
Replace old provider usage with new simplified providers.
Once everything works, delete the old feature folders.
- Check out the working examples in
lib/pages/users_page.dartandlib/pages/posts_page.dart - Look at the model definitions in
lib/models/user.dartandlib/models/post.dart - Review the centralized providers in
lib/core/providers/api_providers.dart - Try creating your own API integration using the same pattern
The new system maintains all the benefits of clean architecture while dramatically reducing complexity!