Files
minhthu/lib/features/warehouse
2025-10-28 00:09:46 +07:00
..
2025-10-28 00:09:46 +07:00
2025-10-28 00:09:46 +07:00
2025-10-28 00:09:46 +07:00
2025-10-28 00:09:46 +07:00
2025-10-28 00:09:46 +07:00
2025-10-28 00:09:46 +07:00
2025-10-28 00:09:46 +07:00

Warehouse Feature

Complete implementation of the warehouse feature following Clean Architecture principles.

Architecture Overview

This feature follows a three-layer clean architecture pattern:

Presentation Layer (UI)
    ↓ (uses)
Domain Layer (Business Logic)
    ↓ (uses)
Data Layer (API & Data Sources)

Key Principles

  • Separation of Concerns: Each layer has a single responsibility
  • Dependency Inversion: Outer layers depend on inner layers, not vice versa
  • Testability: Each layer can be tested independently
  • Maintainability: Changes in one layer don't affect others

Project Structure

lib/features/warehouse/
├── data/
│   ├── datasources/
│   │   └── warehouse_remote_datasource.dart    # API calls using ApiClient
│   ├── models/
│   │   └── warehouse_model.dart                # Data transfer objects with JSON serialization
│   └── repositories/
│       └── warehouse_repository_impl.dart      # Repository implementation
├── domain/
│   ├── entities/
│   │   └── warehouse_entity.dart               # Pure business models
│   ├── repositories/
│   │   └── warehouse_repository.dart           # Repository interface/contract
│   └── usecases/
│       └── get_warehouses_usecase.dart         # Business logic use cases
├── presentation/
│   ├── pages/
│   │   └── warehouse_selection_page.dart       # Main warehouse selection screen
│   ├── providers/
│   │   └── warehouse_provider.dart             # Riverpod state management
│   └── widgets/
│       └── warehouse_card.dart                 # Reusable warehouse card widget
├── warehouse_exports.dart                       # Barrel file for clean imports
├── warehouse_provider_setup_example.dart        # Provider setup guide
└── README.md                                    # This file

Layer Details

1. Domain Layer (Core Business Logic)

The innermost layer that contains business entities, repository interfaces, and use cases. No dependencies on external frameworks or packages (except dartz for Either).

Entities

domain/entities/warehouse_entity.dart

  • Pure Dart class representing a warehouse
  • No JSON serialization logic
  • Contains business rules and validations
  • Extends Equatable for value comparison
class WarehouseEntity extends Equatable {
  final int id;
  final String name;
  final String code;
  final String? description;
  final bool isNGWareHouse;
  final int totalCount;

  bool get hasItems => totalCount > 0;
  bool get isNGType => isNGWareHouse;
}

Repository Interface

domain/repositories/warehouse_repository.dart

  • Abstract interface defining data operations
  • Returns Either<Failure, T> for error handling
  • Implementation is provided by the data layer
abstract class WarehouseRepository {
  Future<Either<Failure, List<WarehouseEntity>>> getWarehouses();
}

Use Cases

domain/usecases/get_warehouses_usecase.dart

  • Single responsibility: fetch warehouses
  • Encapsulates business logic
  • Depends only on repository interface
class GetWarehousesUseCase {
  final WarehouseRepository repository;

  Future<Either<Failure, List<WarehouseEntity>>> call() async {
    return await repository.getWarehouses();
  }
}

2. Data Layer (External Data Management)

Handles all data operations including API calls, JSON serialization, and error handling.

Models

data/models/warehouse_model.dart

  • Extends domain entity
  • Adds JSON serialization (fromJson, toJson)
  • Maps API response format to domain entities
  • Matches API field naming (PascalCase)
class WarehouseModel extends WarehouseEntity {
  factory WarehouseModel.fromJson(Map<String, dynamic> json) {
    return WarehouseModel(
      id: json['Id'] ?? 0,
      name: json['Name'] ?? '',
      code: json['Code'] ?? '',
      description: json['Description'],
      isNGWareHouse: json['IsNGWareHouse'] ?? false,
      totalCount: json['TotalCount'] ?? 0,
    );
  }
}

Data Sources

data/datasources/warehouse_remote_datasource.dart

  • Interface + implementation pattern
  • Makes API calls using ApiClient
  • Parses ApiResponse wrapper
  • Throws typed exceptions (ServerException, NetworkException)
class WarehouseRemoteDataSourceImpl implements WarehouseRemoteDataSource {
  Future<List<WarehouseModel>> getWarehouses() async {
    final response = await apiClient.get('/warehouses');
    final apiResponse = ApiResponse.fromJson(
      response.data,
      (json) => (json as List).map((e) => WarehouseModel.fromJson(e)).toList(),
    );

    if (apiResponse.isSuccess && apiResponse.value != null) {
      return apiResponse.value!;
    } else {
      throw ServerException(apiResponse.errors.first);
    }
  }
}

Repository Implementation

data/repositories/warehouse_repository_impl.dart

  • Implements domain repository interface
  • Coordinates data sources
  • Converts exceptions to failures
  • Maps models to entities
class WarehouseRepositoryImpl implements WarehouseRepository {
  @override
  Future<Either<Failure, List<WarehouseEntity>>> getWarehouses() async {
    try {
      final warehouses = await remoteDataSource.getWarehouses();
      final entities = warehouses.map((model) => model.toEntity()).toList();
      return Right(entities);
    } on ServerException catch (e) {
      return Left(ServerFailure(e.message));
    } on NetworkException catch (e) {
      return Left(NetworkFailure(e.message));
    }
  }
}

3. Presentation Layer (UI & State Management)

Handles UI rendering, user interactions, and state management using Riverpod.

State Management

presentation/providers/warehouse_provider.dart

  • WarehouseState: Immutable state class

    • warehouses: List of warehouses
    • selectedWarehouse: Currently selected warehouse
    • isLoading: Loading indicator
    • error: Error message
  • WarehouseNotifier: StateNotifier managing state

    • loadWarehouses(): Fetch warehouses from API
    • selectWarehouse(): Select a warehouse
    • refresh(): Reload warehouses
    • clearError(): Clear error state
class WarehouseState {
  final List<WarehouseEntity> warehouses;
  final WarehouseEntity? selectedWarehouse;
  final bool isLoading;
  final String? error;
}

class WarehouseNotifier extends StateNotifier<WarehouseState> {
  Future<void> loadWarehouses() async {
    state = state.setLoading();
    final result = await getWarehousesUseCase();
    result.fold(
      (failure) => state = state.setError(failure.message),
      (warehouses) => state = state.setSuccess(warehouses),
    );
  }
}

Pages

presentation/pages/warehouse_selection_page.dart

  • ConsumerStatefulWidget using Riverpod
  • Loads warehouses on initialization
  • Displays different UI states:
    • Loading: CircularProgressIndicator
    • Error: Error message with retry button
    • Empty: No warehouses message
    • Success: List of warehouse cards
  • Pull-to-refresh support
  • Navigation to operations page on selection

Widgets

presentation/widgets/warehouse_card.dart

  • Reusable warehouse card component
  • Displays:
    • Warehouse name (title)
    • Code (with QR icon)
    • Total items count (with inventory icon)
    • Description (if available)
    • NG warehouse badge (if applicable)
  • Material Design 3 styling
  • Tap to select functionality

API Integration

Endpoint

GET /warehouses

Request

curl -X GET https://api.example.com/warehouses \
  -H "Authorization: Bearer {access_token}"

Response Format

{
  "Value": [
    {
      "Id": 1,
      "Name": "Kho nguyên vật liệu",
      "Code": "001",
      "Description": "Kho chứa nguyên vật liệu",
      "IsNGWareHouse": false,
      "TotalCount": 8
    },
    {
      "Id": 2,
      "Name": "Kho bán thành phẩm công đoạn",
      "Code": "002",
      "Description": null,
      "IsNGWareHouse": false,
      "TotalCount": 12
    }
  ],
  "IsSuccess": true,
  "IsFailure": false,
  "Errors": [],
  "ErrorCodes": []
}

Setup & Integration

1. Install Dependencies

Ensure your pubspec.yaml includes:

dependencies:
  flutter_riverpod: ^2.4.9
  dio: ^5.3.2
  dartz: ^0.10.1
  equatable: ^2.0.5
  flutter_secure_storage: ^9.0.0

2. Set Up Providers

Create or update your provider configuration file (e.g., lib/core/di/providers.dart):

import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../features/warehouse/warehouse_exports.dart';

// Core providers (if not already set up)
final secureStorageProvider = Provider<SecureStorage>((ref) {
  return SecureStorage();
});

final apiClientProvider = Provider<ApiClient>((ref) {
  final secureStorage = ref.watch(secureStorageProvider);
  return ApiClient(secureStorage);
});

// Warehouse data layer providers
final warehouseRemoteDataSourceProvider = Provider<WarehouseRemoteDataSource>((ref) {
  final apiClient = ref.watch(apiClientProvider);
  return WarehouseRemoteDataSourceImpl(apiClient);
});

final warehouseRepositoryProvider = Provider((ref) {
  final remoteDataSource = ref.watch(warehouseRemoteDataSourceProvider);
  return WarehouseRepositoryImpl(remoteDataSource);
});

// Warehouse domain layer providers
final getWarehousesUseCaseProvider = Provider((ref) {
  final repository = ref.watch(warehouseRepositoryProvider);
  return GetWarehousesUseCase(repository);
});

// Warehouse presentation layer providers
final warehouseProvider = StateNotifierProvider<WarehouseNotifier, WarehouseState>((ref) {
  final getWarehousesUseCase = ref.watch(getWarehousesUseCaseProvider);
  return WarehouseNotifier(getWarehousesUseCase);
});

3. Update WarehouseSelectionPage

Replace the TODO comments in warehouse_selection_page.dart:

@override
void initState() {
  super.initState();
  WidgetsBinding.instance.addPostFrameCallback((_) {
    ref.read(warehouseProvider.notifier).loadWarehouses();
  });
}

@override
Widget build(BuildContext context) {
  final state = ref.watch(warehouseProvider);

  // Rest of the implementation...
}

4. Add to Router

Using go_router:

GoRoute(
  path: '/warehouses',
  name: 'warehouses',
  builder: (context, state) => const WarehouseSelectionPage(),
),
GoRoute(
  path: '/operations',
  name: 'operations',
  builder: (context, state) {
    final warehouse = state.extra as WarehouseEntity;
    return OperationSelectionPage(warehouse: warehouse);
  },
),

5. Navigate to Warehouse Page

// From login page after successful authentication
context.go('/warehouses');

// Or using Navigator
Navigator.of(context).pushNamed('/warehouses');

Usage Examples

Loading Warehouses

// In a widget
ElevatedButton(
  onPressed: () {
    ref.read(warehouseProvider.notifier).loadWarehouses();
  },
  child: const Text('Load Warehouses'),
)

Watching State

// In a ConsumerWidget
@override
Widget build(BuildContext context, WidgetRef ref) {
  final state = ref.watch(warehouseProvider);

  if (state.isLoading) {
    return const CircularProgressIndicator();
  }

  if (state.error != null) {
    return Text('Error: ${state.error}');
  }

  return ListView.builder(
    itemCount: state.warehouses.length,
    itemBuilder: (context, index) {
      final warehouse = state.warehouses[index];
      return ListTile(title: Text(warehouse.name));
    },
  );
}

Selecting a Warehouse

// Select warehouse and navigate
void onWarehouseTap(WarehouseEntity warehouse) {
  ref.read(warehouseProvider.notifier).selectWarehouse(warehouse);
  context.push('/operations', extra: warehouse);
}

Pull to Refresh

RefreshIndicator(
  onRefresh: () => ref.read(warehouseProvider.notifier).refresh(),
  child: ListView(...),
)

Accessing Selected Warehouse

// In another page
final state = ref.watch(warehouseProvider);
final selectedWarehouse = state.selectedWarehouse;

if (selectedWarehouse != null) {
  Text('Current: ${selectedWarehouse.name}');
}

Error Handling

The feature uses dartz's Either type for functional error handling:

// In use case or repository
Future<Either<Failure, List<WarehouseEntity>>> getWarehouses() async {
  try {
    final warehouses = await remoteDataSource.getWarehouses();
    return Right(warehouses); // Success
  } on ServerException catch (e) {
    return Left(ServerFailure(e.message)); // Failure
  }
}

// In presentation layer
result.fold(
  (failure) => print('Error: ${failure.message}'),
  (warehouses) => print('Success: ${warehouses.length} items'),
);

Failure Types

  • ServerFailure: API errors, HTTP errors
  • NetworkFailure: Connection issues, timeouts
  • CacheFailure: Local storage errors (if implemented)

Testing

Unit Tests

Test Use Case:

test('should get warehouses from repository', () async {
  // Arrange
  when(mockRepository.getWarehouses())
      .thenAnswer((_) async => Right(mockWarehouses));

  // Act
  final result = await useCase();

  // Assert
  expect(result, Right(mockWarehouses));
  verify(mockRepository.getWarehouses());
});

Test Repository:

test('should return warehouses when remote call is successful', () async {
  // Arrange
  when(mockRemoteDataSource.getWarehouses())
      .thenAnswer((_) async => mockWarehouseModels);

  // Act
  final result = await repository.getWarehouses();

  // Assert
  expect(result.isRight(), true);
});

Test Notifier:

test('should emit loading then success when warehouses are loaded', () async {
  // Arrange
  when(mockUseCase()).thenAnswer((_) async => Right(mockWarehouses));

  // Act
  await notifier.loadWarehouses();

  // Assert
  expect(notifier.state.isLoading, false);
  expect(notifier.state.warehouses, mockWarehouses);
});

Widget Tests

testWidgets('should display warehouse list when loaded', (tester) async {
  // Arrange
  final container = ProviderContainer(
    overrides: [
      warehouseProvider.overrideWith((ref) => MockWarehouseNotifier()),
    ],
  );

  // Act
  await tester.pumpWidget(
    UncontrolledProviderScope(
      container: container,
      child: const WarehouseSelectionPage(),
    ),
  );

  // Assert
  expect(find.byType(WarehouseCard), findsWidgets);
});

Best Practices

  1. Always use Either for error handling - Don't throw exceptions across layers
  2. Keep domain layer pure - No Flutter/external dependencies
  3. Use value objects - Entities should be immutable
  4. Single responsibility - Each class has one reason to change
  5. Dependency inversion - Depend on abstractions, not concretions
  6. Test each layer independently - Use mocks and test doubles

Common Issues

Provider Not Found

Error: ProviderNotFoundException

Solution: Make sure you've set up all providers in your provider configuration file and wrapped your app with ProviderScope.

Null Safety Issues

Error: Null check operator used on a null value

Solution: Always check for null before accessing optional fields:

if (warehouse.description != null) {
  Text(warehouse.description!);
}

API Response Format Mismatch

Error: ServerException: Invalid response format

Solution: Verify that the API response matches the expected format in ApiResponse.fromJson and WarehouseModel.fromJson.

Future Enhancements

  • Add caching with Hive for offline support
  • Implement warehouse search functionality
  • Add warehouse filtering (by type, name, etc.)
  • Add pagination for large warehouse lists
  • Implement warehouse CRUD operations
  • Add warehouse analytics and statistics
  • Authentication: /lib/features/auth/ - User login and token management
  • Operations: /lib/features/operation/ - Import/Export selection
  • Products: /lib/features/products/ - Product listing per warehouse

References


Last Updated: 2025-10-27 Version: 1.0.0 Author: Claude Code