Question #283MediumState Management

Provide me the clean architechture stucture along with feature first folder structure along with models , entities , api calls, remote - for offline first , riverpod statemanagement ?

#state#riverpod#api#clean

Answer

Overview

This is a complete clean architecture + feature-first folder structure with Riverpod, including offline-first data layer, API calls, entities, models, and remote/local data sources.


Folder Structure

text
lib/
├── core/
│   ├── constants/
│   │   └── api_constants.dart
│   ├── errors/
│   │   └── failures.dart
│   ├── network/
│   │   ├── api_client.dart          ← Dio setup
│   │   └── network_info.dart        ← Check connectivity
│   └── utils/
│       └── result.dart              ← Either<Failure, T>
├── features/
│   └── products/
│       ├── data/
│       │   ├── datasources/
│       │   │   ├── product_remote_datasource.dart  ← API calls
│       │   │   └── product_local_datasource.dart   ← SQLite/Hive cache
│       │   ├── models/
│       │   │   └── product_model.dart     ← JSON serializable
│       │   └── repositories/
│       │       └── product_repository_impl.dart  ← offline-first logic
│       ├── domain/
│       │   ├── entities/
│       │   │   └── product_entity.dart    ← Pure Dart class
│       │   ├── repositories/
│       │   │   └── product_repository.dart ← Abstract interface
│       │   └── usecases/
│       │       ├── get_products_usecase.dart
│       │       └── get_product_detail_usecase.dart
│       └── presentation/
│           ├── providers/
│           │   └── products_provider.dart  ← Riverpod providers
│           ├── screens/
│           │   └── products_screen.dart
│           └── widgets/
│               └── product_card.dart
├── providers.dart                          ← Barrel file
└── main.dart

Domain Layer — Pure Dart, No Dependencies

dart
// features/products/domain/entities/product_entity.dart
class ProductEntity {
  final int id;
  final String name;
  final double price;
  final String imageUrl;
  ProductEntity({required this.id, required this.name, required this.price, required this.imageUrl});
}

// features/products/domain/repositories/product_repository.dart
abstract class ProductRepository {
  Future<Either<Failure, List<ProductEntity>>> getProducts();
  Future<Either<Failure, ProductEntity>> getProductById(int id);
}

// features/products/domain/usecases/get_products_usecase.dart
class GetProductsUseCase {
  final ProductRepository repository;
  GetProductsUseCase(this.repository);

  Future<Either<Failure, List<ProductEntity>>> call() {
    return repository.getProducts();
  }
}

Data Layer — Models + DataSources

dart
// features/products/data/models/product_model.dart
class ProductModel extends ProductEntity {
  ProductModel({required super.id, required super.name, required super.price, required super.imageUrl});

  factory ProductModel.fromJson(Map<String, dynamic> json) => ProductModel(
    id: json['id'], name: json['name'],
    price: json['price'].toDouble(), imageUrl: json['imageUrl'],
  );
  Map<String, dynamic> toJson() => {'id': id, 'name': name, 'price': price, 'imageUrl': imageUrl};
}

// Remote datasource — API calls
abstract class ProductRemoteDataSource {
  Future<List<ProductModel>> getProducts();
}

class ProductRemoteDataSourceImpl implements ProductRemoteDataSource {
  final Dio dio;
  ProductRemoteDataSourceImpl(this.dio);

  
  Future<List<ProductModel>> getProducts() async {
    final res = await dio.get('/products');
    return (res.data as List).map((e) => ProductModel.fromJson(e)).toList();
  }
}

// Local datasource — offline cache
abstract class ProductLocalDataSource {
  Future<List<ProductModel>> getCachedProducts();
  Future<void> cacheProducts(List<ProductModel> products);
}

Repository — Offline-First

dart
class ProductRepositoryImpl implements ProductRepository {
  final ProductRemoteDataSource remoteDataSource;
  final ProductLocalDataSource localDataSource;
  final NetworkInfo networkInfo;

  ProductRepositoryImpl({
    required this.remoteDataSource,
    required this.localDataSource,
    required this.networkInfo,
  });

  
  Future<Either<Failure, List<ProductEntity>>> getProducts() async {
    if (await networkInfo.isConnected) {
      try {
        final products = await remoteDataSource.getProducts();
        await localDataSource.cacheProducts(products); // Cache for offline
        return Right(products);
      } catch (e) {
        return Left(ServerFailure(e.toString()));
      }
    } else {
      // Offline — return cached data
      try {
        final cached = await localDataSource.getCachedProducts();
        return Right(cached);
      } catch (e) {
        return Left(CacheFailure('No cached data available'));
      }
    }
  }
}

Riverpod Providers

dart
// features/products/presentation/providers/products_provider.dart
final dioProvider = Provider((ref) => Dio(BaseOptions(baseUrl: ApiConstants.baseUrl)));

final productRemoteDataSourceProvider = Provider(
  (ref) => ProductRemoteDataSourceImpl(ref.watch(dioProvider)),
);

final productLocalDataSourceProvider = Provider(
  (ref) => ProductLocalDataSourceImpl(), // Hive/SQLite
);

final productRepositoryProvider = Provider(
  (ref) => ProductRepositoryImpl(
    remoteDataSource: ref.watch(productRemoteDataSourceProvider),
    localDataSource: ref.watch(productLocalDataSourceProvider),
    networkInfo: ref.watch(networkInfoProvider),
  ),
);

final productsProvider = AsyncNotifierProvider<ProductsNotifier, List<ProductEntity>>(
  ProductsNotifier.new,
);

class ProductsNotifier extends AsyncNotifier<List<ProductEntity>> {
  
  Future<List<ProductEntity>> build() async {
    final result = await ref.watch(productRepositoryProvider).getProducts();
    return result.fold(
      (failure) => throw failure,
      (products) => products,
    );
  }
}

Screen — Consume Provider

dart
class ProductsScreen extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.watch(productsProvider);
    return state.when(
      loading: () => CircularProgressIndicator(),
      error: (e, _) => Text('Error: $e'),
      data: (products) => ListView.builder(
        itemCount: products.length,
        itemBuilder: (_, i) => ProductCard(product: products[i]),
      ),
    );
  }
}

Key Principle: Domain layer has zero dependencies on Flutter or external packages. Data layer implements domain interfaces. Presentation only calls use cases through providers — never touches data layer directly.