Clean Architecture & Feature Structure
🎯 Learning Objective
Master Feature-First project organization, Clean Architecture layer separation (Presentation, Domain, Data), dependency injection with Riverpod providers, and safe UI consumption with AsyncValue.when().
📋 Prerequisites
- Completion of Phase 4: Code Generation with @riverpod.
1. Feature-First Folder Architecture
In production Flutter applications, organize code by Feature rather than by layer. This keeps related screens, widgets, notifiers, and repositories together in self-contained directories.
lib/
├── core/ # Global Core Services (Network, Theme, Router)
│ ├── network/ # Centralized Dio ApiClient & Error Parsers
│ └── theme/ # AppTheme constants & colors
│
├── features/ # Self-contained Application Features
│ ├── auth/ # Authentication Feature
│ │ ├── models/ # Auth User DTOs & Models
│ │ ├── repositories/ # AuthRepository & REST API calls
│ │ ├── providers/ # LoginController & AuthNotifier
│ │ ├── screens/ # LoginScreen, RegisterScreen
│ │ └── widgets/ # AuthButton, SocialLoginTile
│ │
│ └── shipments/ # Shipments Feature
│ ├── models/ # Shipment entity & filter classes
│ ├── repositories/ # ShipmentRepository
│ ├── providers/ # ShipmentListNotifier
│ ├── screens/ # ShipmentListScreen, ShipmentDetailScreen
│ └── widgets/ # ShipmentCard, StatusBadge
│
└── shared/ # Cross-cutting Shared Models & Providers
├── models/ # User entity
└── providers/ # CurrentUserNotifier & AuthStateStream
2. Layer Separation & Data Flow
Clean Architecture enforces a unidirectional dependency flow: UI Widgets → Notifiers → Repositories → ApiClients.
UI widgets must NEVER call HTTP endpoints or Repositories directly. Widgets exclusively interact with Riverpod Notifiers, keeping UI completely decoupled from API contracts.
3. Dependency Injection with Riverpod
Riverpod acts as a powerful, type-safe Dependency Injection (DI) framework. Repositories and network clients are injected into Notifiers using ref.watch():
@riverpod
Dio dioClient(Ref ref) => Dio(BaseOptions(baseUrl: 'https://api.example.com'));
@riverpod
ShipmentRepository shipmentRepository(Ref ref) {
return ShipmentRepository(ref.watch(dioClientProvider));
}
@riverpod
class ShipmentListNotifier extends _$ShipmentListNotifier {
@override
Future<List<Shipment>> build() async {
// Injects ShipmentRepository via ref.watch()
final repo = ref.watch(shipmentRepositoryProvider);
return repo.fetchShipments();
}
}
4. AsyncValue UI Safety Pattern (.when)
Whenever a screen reads an asynchronous provider (e.g. AsyncNotifier), Riverpod wraps the result in an AsyncValue<T>. Handle all 3 UI states cleanly using .when():
class ShipmentListScreen extends ConsumerWidget {
const ShipmentListScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final asyncShipments = ref.watch(shipmentListNotifierProvider);
return Scaffold(
appBar: AppBar(title: const Text('Shipments')),
body: asyncShipments.when(
data: (shipments) => ListView.builder(
itemCount: shipments.length,
itemBuilder: (_, i) => ShipmentTile(shipment: shipments[i]),
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, stack) => Center(child: Text('Error loading shipments: $err')),
),
);
}
}
5. Architecture Decision Tree
lib/features/x/repositories/lib/features/x/providers/6. Knowledge Check & Mini Challenge
🎓 Milestone 2 Final Challenge: Complete Feature Blueprint
Review the architectural flow above. You are now fully prepared to analyze real Transwave production providers in Milestone 3, beginning with auth_provider.dart and current_user_provider.dart!
Phase 05 & Milestone 2 Summary
You have completed all foundational modules of Riverpod 3: Getting Started, Core Ref Concepts, Provider Taxonomy, Code Generation with @riverpod, and Clean Feature Architecture.