1. クリーンなアーキテクチャ
lib/
├── core/
│ ├── error/ # Failures, Exceptions
│ ├── usecases/ # Base UseCase abstract
│ ├── network/ # Network info
│ └── di/ # Dependency injection
├── features/
│ └── product/
│ ├── domain/
│ │ ├── entities/ # Product entity
│ │ ├── repositories/ # Abstract repository
│ │ └── usecases/ # GetProducts, CreateProduct
│ ├── data/
│ │ ├── models/ # ProductModel extends Product
│ │ ├── datasources/ # Remote & Local datasource
│ │ └── repositories/ # Repository implementation
│ └── presentation/
│ ├── bloc/ # ProductBloc
│ ├── pages/ # ProductListPage
│ └── widgets/ # ProductCard
2. ドメイン層
// Entity
class Product {
final String id;
final String name;
final double price;
const Product({required this.id, required this.name, required this.price});
}
// Repository interface
abstract class ProductRepository {
Future<Either<Failure, List<Product>>> getProducts();
Future<Either<Failure, Product>> getProductById(String id);
Future<Either<Failure, void>> createProduct(Product product);
}
// Use Case
abstract class UseCase<Type, Params> {
Future<Either<Failure, Type>> call(Params params);
}
class GetProducts implements UseCase<List<Product>, NoParams> {
final ProductRepository repository;
GetProducts(this.repository);
@override
Future<Either<Failure, List<Product>>> call(NoParams params) {
return repository.getProducts();
}
}
// Failure
abstract class Failure {
final String message;
const Failure(this.message);
}
class ServerFailure extends Failure { const ServerFailure(super.message); }
class CacheFailure extends Failure { const CacheFailure(super.message); }
3. データ層
// Model
class ProductModel extends Product {
const ProductModel({required super.id, required super.name, required super.price});
factory ProductModel.fromJson(Map<String, dynamic> json) => ProductModel(
id: json['id'], name: json['name'], price: (json['price'] as num).toDouble(),
);
Map<String, dynamic> toJson() => {'id': id, 'name': name, 'price': price};
}
// Repository implementation
class ProductRepositoryImpl implements ProductRepository {
final ProductRemoteDataSource remoteDataSource;
final ProductLocalDataSource localDataSource;
final NetworkInfo networkInfo;
ProductRepositoryImpl({
required this.remoteDataSource,
required this.localDataSource,
required this.networkInfo,
});
@override
Future<Either<Failure, List<Product>>> getProducts() async {
if (await networkInfo.isConnected) {
try {
final products = await remoteDataSource.getProducts();
await localDataSource.cacheProducts(products);
return Right(products);
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
}
} else {
try {
final products = await localDataSource.getCachedProducts();
return Right(products);
} on CacheException {
return Left(const CacheFailure('No cached data'));
}
}
}
}
4. get_it による依存関係の注入
final sl = GetIt.instance;
Future<void> initDependencies() async {
// Bloc
sl.registerFactory(() => ProductBloc(getProducts: sl()));
// Use Cases
sl.registerLazySingleton(() => GetProducts(sl()));
// Repository
sl.registerLazySingleton<ProductRepository>(() => ProductRepositoryImpl(
remoteDataSource: sl(),
localDataSource: sl(),
networkInfo: sl(),
));
// Data Sources
sl.registerLazySingleton<ProductRemoteDataSource>(
() => ProductRemoteDataSourceImpl(dio: sl()),
);
// External
sl.registerLazySingleton(() => Dio());
}
次の記事: コード生成とフリーズ。