Phase 05

Clean Architecture & Feature Structure

Calculating reading time... Module 1: Foundations

🎯 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

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.

Folder Tree Production Project Folder Structure
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.

1. Presentation Layer (UI & State)
ConsumerWidget
Flutter Screens & Components
@riverpod Notifier
AsyncNotifier & Controllers
↓ Uses ref.watch() & ref.read()
2. Domain Layer (Business Entities)
Entity / Models
Immutable Freezed Data Models
Repository Interface
Abstract Data Contract
↓ Calls Repository Methods
3. Data Layer (Data Sources & Network)
Repository Impl
Http / Hive Data Source
Dio ApiClient
REST Backend Endpoint Calls
🚨 Cardinal Architectural Rule

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():

Dart Dependency Injection Pattern
@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():

Dart lib/features/shipments/screens/shipment_list_screen.dart
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

Where does this code belong?
HTTP / Database / DTO Parse
Data Layer
Put in lib/features/x/repositories/
State Management & Actions
Presentation Layer
Put in lib/features/x/providers/

6. Knowledge Check & Mini Challenge

❓ Question: How should UI widgets consume asynchronous provider states safely?
A) Call .then() inside the build method
B) Use asyncState.when(data: ..., loading: ..., error: ...)
C) Wrap the widget in a StatefulWidget

🎓 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.