mobile-ai-agents 1.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +228 -0
- package/CLAUDE.md +236 -0
- package/LICENSE +21 -0
- package/README.md +652 -0
- package/agents/android/android-crash-analyzer/README.md +134 -0
- package/agents/android/android-crash-analyzer/agent.md +206 -0
- package/agents/android/android-crash-analyzer/example-input.md +138 -0
- package/agents/android/android-crash-analyzer/example-output.md +225 -0
- package/agents/android/anr-investigation/README.md +78 -0
- package/agents/android/anr-investigation/agent.md +117 -0
- package/agents/android/anr-investigation/example-input.md +62 -0
- package/agents/android/anr-investigation/example-output.md +103 -0
- package/agents/android/axiom/agent.md +197 -0
- package/agents/android/code-reviewer/README.md +108 -0
- package/agents/android/code-reviewer/agent.md +210 -0
- package/agents/android/code-reviewer/example-input.md +116 -0
- package/agents/android/code-reviewer/example-output.md +115 -0
- package/agents/android/compose-navigation/README.md +76 -0
- package/agents/android/compose-navigation/agent.md +121 -0
- package/agents/android/compose-navigation/example-input.md +49 -0
- package/agents/android/compose-navigation/example-output.md +325 -0
- package/agents/android/compose-screen-builder/README.md +95 -0
- package/agents/android/compose-screen-builder/agent.md +170 -0
- package/agents/android/compose-screen-builder/example-input.md +40 -0
- package/agents/android/compose-screen-builder/example-output.md +568 -0
- package/agents/android/compose-ui-reviewer/README.md +73 -0
- package/agents/android/compose-ui-reviewer/agent.md +253 -0
- package/agents/android/compose-ui-reviewer/example-input.md +84 -0
- package/agents/android/compose-ui-reviewer/example-output.md +102 -0
- package/agents/android/crash-analyzer/README.md +53 -0
- package/agents/android/crash-analyzer/agent.md +295 -0
- package/agents/android/memory-leak-analyzer/README.md +79 -0
- package/agents/android/memory-leak-analyzer/agent.md +116 -0
- package/agents/android/memory-leak-analyzer/example-input.md +78 -0
- package/agents/android/memory-leak-analyzer/example-output.md +128 -0
- package/agents/cross-platform/accessibility-auditor/README.md +47 -0
- package/agents/cross-platform/accessibility-auditor/agent.md +268 -0
- package/agents/cross-platform/accessibility-auditor/example-input.md +84 -0
- package/agents/cross-platform/accessibility-auditor/example-output.md +132 -0
- package/agents/cross-platform/appforge/agent.md +303 -0
- package/agents/cross-platform/ci-cd-generator/README.md +65 -0
- package/agents/cross-platform/ci-cd-generator/agent.md +422 -0
- package/agents/cross-platform/ci-cd-generator/example-input.md +69 -0
- package/agents/cross-platform/ci-cd-generator/example-output.md +261 -0
- package/agents/cross-platform/crasher/agent.md +221 -0
- package/agents/cross-platform/figma/agent.md +247 -0
- package/agents/cross-platform/launchpad/agent.md +330 -0
- package/agents/cross-platform/mobile-harness/agent.md +422 -0
- package/agents/cross-platform/mobile-memory/agent.md +429 -0
- package/agents/cross-platform/perf/agent.md +261 -0
- package/agents/cross-platform/pipeline/agent.md +288 -0
- package/agents/cross-platform/release-notes-generator/README.md +63 -0
- package/agents/cross-platform/release-notes-generator/agent.md +218 -0
- package/agents/cross-platform/release-notes-generator/example-input.md +71 -0
- package/agents/cross-platform/release-notes-generator/example-output.md +96 -0
- package/agents/cross-platform/scribe/agent.md +180 -0
- package/agents/cross-platform/security-scanner/README.md +61 -0
- package/agents/cross-platform/security-scanner/agent.md +287 -0
- package/agents/cross-platform/security-scanner/example-input.md +77 -0
- package/agents/cross-platform/security-scanner/example-output.md +141 -0
- package/agents/cross-platform/sentinel/agent.md +217 -0
- package/agents/cross-platform/store-listing-writer/README.md +58 -0
- package/agents/cross-platform/store-listing-writer/agent.md +380 -0
- package/agents/cross-platform/store-listing-writer/example-input.md +67 -0
- package/agents/cross-platform/store-listing-writer/example-output.md +131 -0
- package/agents/flutter/bloc-feature-builder/README.md +101 -0
- package/agents/flutter/bloc-feature-builder/agent.md +225 -0
- package/agents/flutter/bloc-feature-builder/example-input.md +47 -0
- package/agents/flutter/bloc-feature-builder/example-output.md +838 -0
- package/agents/flutter/dart/agent.md +223 -0
- package/agents/flutter/widget-generator/README.md +66 -0
- package/agents/flutter/widget-generator/agent.md +264 -0
- package/agents/flutter/widget-generator/example-input.md +71 -0
- package/agents/flutter/widget-generator/example-output.md +163 -0
- package/agents/gaming/forge/agent.md +197 -0
- package/agents/gaming/unreal/agent.md +200 -0
- package/agents/ios/crash-analyzer/README.md +69 -0
- package/agents/ios/crash-analyzer/agent.md +300 -0
- package/agents/ios/crash-analyzer/example-input.md +83 -0
- package/agents/ios/crash-analyzer/example-output.md +126 -0
- package/agents/ios/swift/agent.md +206 -0
- package/agents/ios/swift-reviewer/README.md +68 -0
- package/agents/ios/swift-reviewer/agent.md +231 -0
- package/agents/ios/swift-reviewer/example-input.md +100 -0
- package/agents/ios/swift-reviewer/example-output.md +127 -0
- package/agents/react-native/bridge/agent.md +212 -0
- package/agents/react-native/performance-optimizer/README.md +81 -0
- package/agents/react-native/performance-optimizer/agent.md +284 -0
- package/agents/react-native/performance-optimizer/example-input.md +104 -0
- package/agents/react-native/performance-optimizer/example-output.md +132 -0
- package/agents/unity/shader-generator/README.md +77 -0
- package/agents/unity/shader-generator/agent.md +270 -0
- package/agents/unity/shader-generator/example-input.md +70 -0
- package/agents/unity/shader-generator/example-output.md +160 -0
- package/agents/unreal/blueprint-advisor/README.md +71 -0
- package/agents/unreal/blueprint-advisor/agent.md +311 -0
- package/agents/unreal/blueprint-advisor/example-input.md +73 -0
- package/agents/unreal/blueprint-advisor/example-output.md +217 -0
- package/cli/index.js +1091 -0
- package/docs/agent-guide.md +138 -0
- package/docs/chatgpt-integration.md +219 -0
- package/docs/core-loops.md +143 -0
- package/docs/cursor-integration.md +143 -0
- package/docs/getting-started.md +132 -0
- package/docs/index.html +711 -0
- package/docs/installation.md +271 -0
- package/docs/mobile-harness.md +160 -0
- package/docs/mobile-mcp.md +172 -0
- package/docs/mobile-memory.md +349 -0
- package/docs/release-process.md +85 -0
- package/docs/releases/release-v1.0.0.md +165 -0
- package/docs/roadmap/github-issues.md +239 -0
- package/docs/skill-categories.md +125 -0
- package/docs/vscode-copilot-integration.md +104 -0
- package/docs/wiki.md +615 -0
- package/install.sh +507 -0
- package/mobile-karpathy.md +79 -0
- package/package.json +57 -0
- package/skills/android/README.md +31 -0
- package/skills/android/android-tdd.md +89 -0
- package/skills/android/anr-investigation.md +251 -0
- package/skills/android/code-review.md +98 -0
- package/skills/android/compose-migration.md +48 -0
- package/skills/android/compose-review.md +78 -0
- package/skills/android/kotlin-modernize.md +51 -0
- package/skills/android/memory-leak-investigation.md +256 -0
- package/skills/android/proguard-rules.md +48 -0
- package/skills/cross-platform/accessibility-audit.md +63 -0
- package/skills/cross-platform/api-versioning.md +59 -0
- package/skills/cross-platform/clean-code-audit.md +239 -0
- package/skills/cross-platform/crash-triage.md +53 -0
- package/skills/cross-platform/deeplink-debug.md +57 -0
- package/skills/cross-platform/feature-slice.md +48 -0
- package/skills/cross-platform/grill-mobile.md +64 -0
- package/skills/cross-platform/mobile-mcp-qa.md +193 -0
- package/skills/cross-platform/mobile-memory-graph.md +179 -0
- package/skills/cross-platform/mobile-memory-save.md +210 -0
- package/skills/cross-platform/mobile-memory-search.md +154 -0
- package/skills/cross-platform/perf-audit.md +61 -0
- package/skills/cross-platform/prd-verification.md +324 -0
- package/skills/cross-platform/release-prep.md +64 -0
- package/skills/cross-platform/security-audit.md +281 -0
- package/skills/cross-platform/store-listing.md +75 -0
- package/skills/flutter/README.md +32 -0
- package/skills/flutter/dart-modernize.md +47 -0
- package/skills/flutter/flutter-review.md +59 -0
- package/skills/flutter/flutter-tdd.md +73 -0
- package/skills/flutter/widget-extract.md +41 -0
- package/skills/flutter/widget-gen.md +131 -0
- package/skills/gaming/blueprint-to-cpp.md +51 -0
- package/skills/gaming/game-perf.md +54 -0
- package/skills/gaming/shader-gen.md +44 -0
- package/skills/gaming/unity-tdd.md +85 -0
- package/skills/ios/README.md +45 -0
- package/skills/ios/data-persistence.md +151 -0
- package/skills/ios/ios-tdd.md +90 -0
- package/skills/ios/networking.md +139 -0
- package/skills/ios/performance.md +122 -0
- package/skills/ios/swift-concurrency.md +57 -0
- package/skills/ios/swift-review.md +124 -0
- package/skills/ios/swiftui-review.md +49 -0
- package/skills/ios/swiftui-state.md +122 -0
- package/skills/ios/unit-testing.md +144 -0
- package/skills/ios/xcode-warnings.md +37 -0
- package/skills/react-native/README.md +42 -0
- package/skills/react-native/bridge-audit.md +104 -0
- package/skills/react-native/expo-optimize.md +55 -0
- package/skills/react-native/new-arch-migrate.md +53 -0
- package/skills/react-native/performance.md +124 -0
- package/skills/react-native/rn-review.md +53 -0
- package/skills/react-native/rn-tdd.md +76 -0
- package/skills/shared/README.md +46 -0
- package/skills/shared/accessibility-audit.md +132 -0
- package/skills/shared/crash-analysis.md +110 -0
- package/skills/shared/security-scan.md +149 -0
- package/skills/unity/README.md +31 -0
- package/skills/unity/shader-review.md +110 -0
- package/templates/agent-template.md +88 -0
- package/templates/prompt-template.md +50 -0
- package/templates/skill-template.md +64 -0
- package/templates/workflow-template.md +48 -0
- package/workflows/app-launch.md +66 -0
- package/workflows/appforge-workflow.md +232 -0
- package/workflows/ci-setup.md +77 -0
- package/workflows/crash-to-fix.md +71 -0
- package/workflows/feature-ship.md +61 -0
- package/workflows/game-level.md +72 -0
- package/workflows/mobile-harness.md +319 -0
- package/workflows/mobile-mcp-qa.md +228 -0
- package/workflows/mobile-memory-workflow.md +243 -0
- package/workflows/new-project-setup.md +93 -0
- package/workflows/new-screen.md +68 -0
- package/workflows/perf-sprint.md +78 -0
- package/workflows/release-cycle.md +71 -0
|
@@ -0,0 +1,838 @@
|
|
|
1
|
+
# Example Output — Flutter BLoC Feature Builder
|
|
2
|
+
|
|
3
|
+
Generated from [`example-input.md`](example-input.md).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## File: domain/entities/product.dart
|
|
8
|
+
|
|
9
|
+
```dart
|
|
10
|
+
import 'package:equatable/equatable.dart';
|
|
11
|
+
import 'package:meta/meta.dart';
|
|
12
|
+
|
|
13
|
+
@immutable
|
|
14
|
+
class Product extends Equatable {
|
|
15
|
+
const Product({
|
|
16
|
+
required this.id,
|
|
17
|
+
required this.name,
|
|
18
|
+
required this.price,
|
|
19
|
+
required this.imageUrl,
|
|
20
|
+
required this.rating,
|
|
21
|
+
required this.category,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
final String id;
|
|
25
|
+
final String name;
|
|
26
|
+
final double price;
|
|
27
|
+
final String imageUrl;
|
|
28
|
+
final double rating;
|
|
29
|
+
final String category;
|
|
30
|
+
|
|
31
|
+
@override
|
|
32
|
+
List<Object?> get props => [id, name, price, imageUrl, rating, category];
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## File: domain/repositories/product_catalog_repository.dart
|
|
39
|
+
|
|
40
|
+
```dart
|
|
41
|
+
import 'package:dartz/dartz.dart';
|
|
42
|
+
import '../entities/product.dart';
|
|
43
|
+
import '../failures/failure.dart';
|
|
44
|
+
|
|
45
|
+
abstract class ProductCatalogRepository {
|
|
46
|
+
Future<Either<Failure, ProductPage>> getProducts({
|
|
47
|
+
String? category,
|
|
48
|
+
required int page,
|
|
49
|
+
required int limit,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// Value object returned by getProducts.
|
|
54
|
+
class ProductPage {
|
|
55
|
+
const ProductPage({
|
|
56
|
+
required this.products,
|
|
57
|
+
required this.total,
|
|
58
|
+
required this.currentPage,
|
|
59
|
+
required this.hasMore,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
final List<Product> products;
|
|
63
|
+
final int total;
|
|
64
|
+
final int currentPage;
|
|
65
|
+
final bool hasMore;
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## File: domain/failures/failure.dart
|
|
72
|
+
|
|
73
|
+
```dart
|
|
74
|
+
abstract class Failure {
|
|
75
|
+
const Failure();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
class ServerFailure extends Failure {
|
|
79
|
+
const ServerFailure(this.message);
|
|
80
|
+
final String message;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
class NetworkFailure extends Failure {
|
|
84
|
+
const NetworkFailure();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
class CacheFailure extends Failure {
|
|
88
|
+
const CacheFailure();
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## File: domain/usecases/get_products_usecase.dart
|
|
95
|
+
|
|
96
|
+
```dart
|
|
97
|
+
import 'package:dartz/dartz.dart';
|
|
98
|
+
import '../entities/product.dart';
|
|
99
|
+
import '../failures/failure.dart';
|
|
100
|
+
import '../repositories/product_catalog_repository.dart';
|
|
101
|
+
|
|
102
|
+
class GetProductsParams {
|
|
103
|
+
const GetProductsParams({
|
|
104
|
+
this.category,
|
|
105
|
+
required this.page,
|
|
106
|
+
this.limit = 20,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
final String? category;
|
|
110
|
+
final int page;
|
|
111
|
+
final int limit;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
class GetProductsUseCase {
|
|
115
|
+
const GetProductsUseCase(this.repository);
|
|
116
|
+
|
|
117
|
+
final ProductCatalogRepository repository;
|
|
118
|
+
|
|
119
|
+
Future<Either<Failure, ProductPage>> call(GetProductsParams params) =>
|
|
120
|
+
repository.getProducts(
|
|
121
|
+
category: params.category,
|
|
122
|
+
page: params.page,
|
|
123
|
+
limit: params.limit,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## File: data/models/product_model.dart
|
|
131
|
+
|
|
132
|
+
```dart
|
|
133
|
+
import '../../domain/entities/product.dart';
|
|
134
|
+
|
|
135
|
+
class ProductModel extends Product {
|
|
136
|
+
const ProductModel({
|
|
137
|
+
required super.id,
|
|
138
|
+
required super.name,
|
|
139
|
+
required super.price,
|
|
140
|
+
required super.imageUrl,
|
|
141
|
+
required super.rating,
|
|
142
|
+
required super.category,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
factory ProductModel.fromJson(Map<String, dynamic> json) => ProductModel(
|
|
146
|
+
id: json['id'] as String,
|
|
147
|
+
name: json['name'] as String,
|
|
148
|
+
price: (json['price'] as num).toDouble(),
|
|
149
|
+
imageUrl: json['imageUrl'] as String,
|
|
150
|
+
rating: (json['rating'] as num).toDouble(),
|
|
151
|
+
category: json['category'] as String,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
Map<String, dynamic> toJson() => {
|
|
155
|
+
'id': id,
|
|
156
|
+
'name': name,
|
|
157
|
+
'price': price,
|
|
158
|
+
'imageUrl': imageUrl,
|
|
159
|
+
'rating': rating,
|
|
160
|
+
'category': category,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// Converts to the pure domain entity.
|
|
164
|
+
Product toEntity() => Product(
|
|
165
|
+
id: id,
|
|
166
|
+
name: name,
|
|
167
|
+
price: price,
|
|
168
|
+
imageUrl: imageUrl,
|
|
169
|
+
rating: rating,
|
|
170
|
+
category: category,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
class ProductPageModel {
|
|
175
|
+
const ProductPageModel({
|
|
176
|
+
required this.products,
|
|
177
|
+
required this.total,
|
|
178
|
+
required this.currentPage,
|
|
179
|
+
required this.hasMore,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
final List<ProductModel> products;
|
|
183
|
+
final int total;
|
|
184
|
+
final int currentPage;
|
|
185
|
+
final bool hasMore;
|
|
186
|
+
|
|
187
|
+
factory ProductPageModel.fromJson(Map<String, dynamic> json) {
|
|
188
|
+
final data = json['data'] as List<dynamic>;
|
|
189
|
+
final meta = json['meta'] as Map<String, dynamic>;
|
|
190
|
+
return ProductPageModel(
|
|
191
|
+
products: data.map((e) => ProductModel.fromJson(e as Map<String, dynamic>)).toList(),
|
|
192
|
+
total: meta['total'] as int,
|
|
193
|
+
currentPage: meta['page'] as int,
|
|
194
|
+
hasMore: meta['hasMore'] as bool,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## File: data/datasources/product_catalog_remote_datasource.dart
|
|
203
|
+
|
|
204
|
+
```dart
|
|
205
|
+
import 'package:dio/dio.dart';
|
|
206
|
+
import '../models/product_model.dart';
|
|
207
|
+
import '../../domain/usecases/get_products_usecase.dart';
|
|
208
|
+
|
|
209
|
+
class ServerException implements Exception {
|
|
210
|
+
const ServerException(this.message);
|
|
211
|
+
final String message;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
class ProductCatalogRemoteDataSource {
|
|
215
|
+
const ProductCatalogRemoteDataSource(this.dio);
|
|
216
|
+
|
|
217
|
+
final Dio dio;
|
|
218
|
+
|
|
219
|
+
Future<ProductPageModel> getProducts(GetProductsParams params) async {
|
|
220
|
+
try {
|
|
221
|
+
final queryParams = <String, dynamic>{
|
|
222
|
+
'page': params.page,
|
|
223
|
+
'limit': params.limit,
|
|
224
|
+
};
|
|
225
|
+
if (params.category != null) {
|
|
226
|
+
queryParams['category'] = params.category;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
final response = await dio.get<Map<String, dynamic>>(
|
|
230
|
+
'/v1/products',
|
|
231
|
+
queryParameters: queryParams,
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
if (response.data == null) {
|
|
235
|
+
throw const ServerException('Empty response body');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return ProductPageModel.fromJson(response.data!);
|
|
239
|
+
} on DioException catch (e) {
|
|
240
|
+
final message = (e.response?.data as Map<String, dynamic>?)?['message'] as String?
|
|
241
|
+
?? e.message
|
|
242
|
+
?? 'Request failed';
|
|
243
|
+
throw ServerException(message);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## File: data/repositories/product_catalog_repository_impl.dart
|
|
252
|
+
|
|
253
|
+
```dart
|
|
254
|
+
import 'dart:io';
|
|
255
|
+
import 'package:dartz/dartz.dart';
|
|
256
|
+
import '../../domain/entities/product.dart';
|
|
257
|
+
import '../../domain/failures/failure.dart';
|
|
258
|
+
import '../../domain/repositories/product_catalog_repository.dart';
|
|
259
|
+
import '../../domain/usecases/get_products_usecase.dart';
|
|
260
|
+
import '../datasources/product_catalog_remote_datasource.dart';
|
|
261
|
+
|
|
262
|
+
class ProductCatalogRepositoryImpl implements ProductCatalogRepository {
|
|
263
|
+
const ProductCatalogRepositoryImpl(this.remoteDataSource);
|
|
264
|
+
|
|
265
|
+
final ProductCatalogRemoteDataSource remoteDataSource;
|
|
266
|
+
|
|
267
|
+
@override
|
|
268
|
+
Future<Either<Failure, ProductPage>> getProducts({
|
|
269
|
+
String? category,
|
|
270
|
+
required int page,
|
|
271
|
+
required int limit,
|
|
272
|
+
}) async {
|
|
273
|
+
try {
|
|
274
|
+
final pageModel = await remoteDataSource.getProducts(
|
|
275
|
+
GetProductsParams(category: category, page: page, limit: limit),
|
|
276
|
+
);
|
|
277
|
+
return Right(ProductPage(
|
|
278
|
+
products: pageModel.products.map((m) => m.toEntity()).toList(),
|
|
279
|
+
total: pageModel.total,
|
|
280
|
+
currentPage: pageModel.currentPage,
|
|
281
|
+
hasMore: pageModel.hasMore,
|
|
282
|
+
));
|
|
283
|
+
} on ServerException catch (e) {
|
|
284
|
+
return Left(ServerFailure(e.message));
|
|
285
|
+
} on SocketException {
|
|
286
|
+
return const Left(NetworkFailure());
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## File: presentation/cubit/product_catalog_state.dart
|
|
295
|
+
|
|
296
|
+
```dart
|
|
297
|
+
import 'package:equatable/equatable.dart';
|
|
298
|
+
import '../../domain/entities/product.dart';
|
|
299
|
+
|
|
300
|
+
sealed class ProductCatalogState extends Equatable {
|
|
301
|
+
const ProductCatalogState();
|
|
302
|
+
|
|
303
|
+
@override
|
|
304
|
+
List<Object?> get props => [];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
final class ProductCatalogInitial extends ProductCatalogState {
|
|
308
|
+
const ProductCatalogInitial();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
final class ProductCatalogLoading extends ProductCatalogState {
|
|
312
|
+
const ProductCatalogLoading();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/// Emitted while more pages are being fetched (pagination).
|
|
316
|
+
/// The existing [products] list is still shown.
|
|
317
|
+
final class ProductCatalogLoadingMore extends ProductCatalogState {
|
|
318
|
+
const ProductCatalogLoadingMore({
|
|
319
|
+
required this.products,
|
|
320
|
+
required this.selectedCategory,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
final List<Product> products;
|
|
324
|
+
final String? selectedCategory;
|
|
325
|
+
|
|
326
|
+
@override
|
|
327
|
+
List<Object?> get props => [products, selectedCategory];
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
final class ProductCatalogLoaded extends ProductCatalogState {
|
|
331
|
+
const ProductCatalogLoaded({
|
|
332
|
+
required this.products,
|
|
333
|
+
required this.selectedCategory,
|
|
334
|
+
required this.currentPage,
|
|
335
|
+
required this.hasMore,
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
final List<Product> products;
|
|
339
|
+
final String? selectedCategory;
|
|
340
|
+
final int currentPage;
|
|
341
|
+
final bool hasMore;
|
|
342
|
+
|
|
343
|
+
ProductCatalogLoaded copyWith({
|
|
344
|
+
List<Product>? products,
|
|
345
|
+
String? selectedCategory,
|
|
346
|
+
int? currentPage,
|
|
347
|
+
bool? hasMore,
|
|
348
|
+
}) =>
|
|
349
|
+
ProductCatalogLoaded(
|
|
350
|
+
products: products ?? this.products,
|
|
351
|
+
selectedCategory: selectedCategory ?? this.selectedCategory,
|
|
352
|
+
currentPage: currentPage ?? this.currentPage,
|
|
353
|
+
hasMore: hasMore ?? this.hasMore,
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
@override
|
|
357
|
+
List<Object?> get props => [products, selectedCategory, currentPage, hasMore];
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
final class ProductCatalogEmpty extends ProductCatalogState {
|
|
361
|
+
const ProductCatalogEmpty({this.category});
|
|
362
|
+
final String? category;
|
|
363
|
+
|
|
364
|
+
@override
|
|
365
|
+
List<Object?> get props => [category];
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
final class ProductCatalogError extends ProductCatalogState {
|
|
369
|
+
const ProductCatalogError(this.message);
|
|
370
|
+
final String message;
|
|
371
|
+
|
|
372
|
+
@override
|
|
373
|
+
List<Object?> get props => [message];
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
## File: presentation/cubit/product_catalog_cubit.dart
|
|
380
|
+
|
|
381
|
+
```dart
|
|
382
|
+
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
383
|
+
import '../../domain/entities/product.dart';
|
|
384
|
+
import '../../domain/failures/failure.dart';
|
|
385
|
+
import '../../domain/usecases/get_products_usecase.dart';
|
|
386
|
+
import 'product_catalog_state.dart';
|
|
387
|
+
|
|
388
|
+
class ProductCatalogCubit extends Cubit<ProductCatalogState> {
|
|
389
|
+
ProductCatalogCubit(this._getProducts) : super(const ProductCatalogInitial());
|
|
390
|
+
|
|
391
|
+
final GetProductsUseCase _getProducts;
|
|
392
|
+
|
|
393
|
+
static const int _pageSize = 20;
|
|
394
|
+
|
|
395
|
+
Future<void> loadProducts({String? category}) async {
|
|
396
|
+
emit(const ProductCatalogLoading());
|
|
397
|
+
final result = await _getProducts(
|
|
398
|
+
GetProductsParams(category: category, page: 1, limit: _pageSize),
|
|
399
|
+
);
|
|
400
|
+
result.fold(
|
|
401
|
+
(failure) => emit(ProductCatalogError(_mapFailure(failure))),
|
|
402
|
+
(page) => page.products.isEmpty
|
|
403
|
+
? emit(ProductCatalogEmpty(category: category))
|
|
404
|
+
: emit(ProductCatalogLoaded(
|
|
405
|
+
products: page.products,
|
|
406
|
+
selectedCategory: category,
|
|
407
|
+
currentPage: 1,
|
|
408
|
+
hasMore: page.hasMore,
|
|
409
|
+
)),
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
Future<void> loadMore() async {
|
|
414
|
+
final current = state;
|
|
415
|
+
if (current is! ProductCatalogLoaded || !current.hasMore) return;
|
|
416
|
+
|
|
417
|
+
emit(ProductCatalogLoadingMore(
|
|
418
|
+
products: current.products,
|
|
419
|
+
selectedCategory: current.selectedCategory,
|
|
420
|
+
));
|
|
421
|
+
|
|
422
|
+
final result = await _getProducts(
|
|
423
|
+
GetProductsParams(
|
|
424
|
+
category: current.selectedCategory,
|
|
425
|
+
page: current.currentPage + 1,
|
|
426
|
+
limit: _pageSize,
|
|
427
|
+
),
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
result.fold(
|
|
431
|
+
// On load-more failure, restore the previous loaded state without losing existing items.
|
|
432
|
+
(failure) => emit(current),
|
|
433
|
+
(page) => emit(current.copyWith(
|
|
434
|
+
products: List.of(current.products)..addAll(page.products),
|
|
435
|
+
currentPage: page.currentPage,
|
|
436
|
+
hasMore: page.hasMore,
|
|
437
|
+
)),
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
Future<void> refresh() => loadProducts(
|
|
442
|
+
category: state is ProductCatalogLoaded
|
|
443
|
+
? (state as ProductCatalogLoaded).selectedCategory
|
|
444
|
+
: null,
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
Future<void> filterByCategory(String? category) => loadProducts(category: category);
|
|
448
|
+
|
|
449
|
+
String _mapFailure(Failure failure) => switch (failure) {
|
|
450
|
+
ServerFailure f => f.message,
|
|
451
|
+
NetworkFailure _ => 'No internet connection. Check your network and try again.',
|
|
452
|
+
CacheFailure _ => 'Local data error. Please try again.',
|
|
453
|
+
_ => 'An unexpected error occurred.',
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
## File: presentation/pages/product_catalog_page.dart
|
|
461
|
+
|
|
462
|
+
```dart
|
|
463
|
+
import 'package:flutter/material.dart';
|
|
464
|
+
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
465
|
+
import '../cubit/product_catalog_cubit.dart';
|
|
466
|
+
import '../cubit/product_catalog_state.dart';
|
|
467
|
+
import '../widgets/product_card.dart';
|
|
468
|
+
import '../widgets/category_filter_chips.dart';
|
|
469
|
+
|
|
470
|
+
class ProductCatalogPage extends StatefulWidget {
|
|
471
|
+
const ProductCatalogPage({super.key});
|
|
472
|
+
|
|
473
|
+
@override
|
|
474
|
+
State<ProductCatalogPage> createState() => _ProductCatalogPageState();
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
class _ProductCatalogPageState extends State<ProductCatalogPage> {
|
|
478
|
+
final _scrollController = ScrollController();
|
|
479
|
+
|
|
480
|
+
@override
|
|
481
|
+
void initState() {
|
|
482
|
+
super.initState();
|
|
483
|
+
context.read<ProductCatalogCubit>().loadProducts();
|
|
484
|
+
_scrollController.addListener(_onScroll);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
@override
|
|
488
|
+
void dispose() {
|
|
489
|
+
_scrollController
|
|
490
|
+
..removeListener(_onScroll)
|
|
491
|
+
..dispose();
|
|
492
|
+
super.dispose();
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
void _onScroll() {
|
|
496
|
+
if (_scrollController.position.pixels >=
|
|
497
|
+
_scrollController.position.maxScrollExtent - 200) {
|
|
498
|
+
context.read<ProductCatalogCubit>().loadMore();
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
@override
|
|
503
|
+
Widget build(BuildContext context) {
|
|
504
|
+
return Scaffold(
|
|
505
|
+
appBar: AppBar(
|
|
506
|
+
title: const Text('Products'),
|
|
507
|
+
centerTitle: true,
|
|
508
|
+
),
|
|
509
|
+
body: Column(
|
|
510
|
+
children: [
|
|
511
|
+
// Category filter chips
|
|
512
|
+
BlocBuilder<ProductCatalogCubit, ProductCatalogState>(
|
|
513
|
+
buildWhen: (prev, curr) =>
|
|
514
|
+
curr is ProductCatalogLoaded || curr is ProductCatalogLoading,
|
|
515
|
+
builder: (context, state) {
|
|
516
|
+
final selected = state is ProductCatalogLoaded
|
|
517
|
+
? state.selectedCategory
|
|
518
|
+
: null;
|
|
519
|
+
return CategoryFilterChips(
|
|
520
|
+
selected: selected,
|
|
521
|
+
onSelected: context.read<ProductCatalogCubit>().filterByCategory,
|
|
522
|
+
);
|
|
523
|
+
},
|
|
524
|
+
),
|
|
525
|
+
// Main content
|
|
526
|
+
Expanded(
|
|
527
|
+
child: BlocBuilder<ProductCatalogCubit, ProductCatalogState>(
|
|
528
|
+
builder: (context, state) => switch (state) {
|
|
529
|
+
ProductCatalogInitial() || ProductCatalogLoading() =>
|
|
530
|
+
const Center(child: CircularProgressIndicator()),
|
|
531
|
+
|
|
532
|
+
ProductCatalogEmpty(:final category) => _EmptyView(
|
|
533
|
+
category: category,
|
|
534
|
+
onRefresh: context.read<ProductCatalogCubit>().refresh,
|
|
535
|
+
),
|
|
536
|
+
|
|
537
|
+
ProductCatalogError(:final message) => _ErrorView(
|
|
538
|
+
message: message,
|
|
539
|
+
onRetry: context.read<ProductCatalogCubit>().refresh,
|
|
540
|
+
),
|
|
541
|
+
|
|
542
|
+
ProductCatalogLoaded(:final products, :final hasMore) ||
|
|
543
|
+
ProductCatalogLoadingMore(:final products) => RefreshIndicator(
|
|
544
|
+
onRefresh: context.read<ProductCatalogCubit>().refresh,
|
|
545
|
+
child: GridView.builder(
|
|
546
|
+
controller: _scrollController,
|
|
547
|
+
padding: const EdgeInsets.all(12),
|
|
548
|
+
gridDelegate:
|
|
549
|
+
const SliverGridDelegateWithFixedCrossAxisCount(
|
|
550
|
+
crossAxisCount: 2,
|
|
551
|
+
crossAxisSpacing: 12,
|
|
552
|
+
mainAxisSpacing: 12,
|
|
553
|
+
childAspectRatio: 0.75,
|
|
554
|
+
),
|
|
555
|
+
itemCount: products.length + (state is ProductCatalogLoadingMore ? 1 : 0),
|
|
556
|
+
itemBuilder: (context, index) {
|
|
557
|
+
if (index >= products.length) {
|
|
558
|
+
return const Center(child: CircularProgressIndicator());
|
|
559
|
+
}
|
|
560
|
+
final product = products[index];
|
|
561
|
+
return ProductCard(
|
|
562
|
+
key: ValueKey(product.id),
|
|
563
|
+
product: product,
|
|
564
|
+
onTap: () =>
|
|
565
|
+
Navigator.of(context).pushNamed('/product/${product.id}'),
|
|
566
|
+
);
|
|
567
|
+
},
|
|
568
|
+
),
|
|
569
|
+
),
|
|
570
|
+
|
|
571
|
+
_ => const SizedBox.shrink(),
|
|
572
|
+
},
|
|
573
|
+
),
|
|
574
|
+
),
|
|
575
|
+
],
|
|
576
|
+
),
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
class _EmptyView extends StatelessWidget {
|
|
582
|
+
const _EmptyView({required this.onRefresh, this.category});
|
|
583
|
+
final String? category;
|
|
584
|
+
final VoidCallback onRefresh;
|
|
585
|
+
|
|
586
|
+
@override
|
|
587
|
+
Widget build(BuildContext context) {
|
|
588
|
+
final message = category != null
|
|
589
|
+
? 'No products found in "$category"'
|
|
590
|
+
: 'No products found';
|
|
591
|
+
return Center(
|
|
592
|
+
child: Column(
|
|
593
|
+
mainAxisAlignment: MainAxisAlignment.center,
|
|
594
|
+
children: [
|
|
595
|
+
const Icon(Icons.inventory_2_outlined, size: 72),
|
|
596
|
+
const SizedBox(height: 16),
|
|
597
|
+
Text(message, style: Theme.of(context).textTheme.titleMedium),
|
|
598
|
+
const SizedBox(height: 24),
|
|
599
|
+
FilledTonalButton(onPressed: onRefresh, child: const Text('Refresh')),
|
|
600
|
+
],
|
|
601
|
+
),
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
class _ErrorView extends StatelessWidget {
|
|
607
|
+
const _ErrorView({required this.message, required this.onRetry});
|
|
608
|
+
final String message;
|
|
609
|
+
final VoidCallback onRetry;
|
|
610
|
+
|
|
611
|
+
@override
|
|
612
|
+
Widget build(BuildContext context) {
|
|
613
|
+
return Center(
|
|
614
|
+
child: Padding(
|
|
615
|
+
padding: const EdgeInsets.all(24),
|
|
616
|
+
child: Column(
|
|
617
|
+
mainAxisAlignment: MainAxisAlignment.center,
|
|
618
|
+
children: [
|
|
619
|
+
const Icon(Icons.error_outline, size: 48),
|
|
620
|
+
const SizedBox(height: 16),
|
|
621
|
+
Text('Failed to load products',
|
|
622
|
+
style: Theme.of(context).textTheme.titleMedium),
|
|
623
|
+
const SizedBox(height: 8),
|
|
624
|
+
Text(message,
|
|
625
|
+
style: Theme.of(context).textTheme.bodySmall,
|
|
626
|
+
textAlign: TextAlign.center),
|
|
627
|
+
const SizedBox(height: 24),
|
|
628
|
+
FilledButton(onPressed: onRetry, child: const Text('Retry')),
|
|
629
|
+
],
|
|
630
|
+
),
|
|
631
|
+
),
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
---
|
|
638
|
+
|
|
639
|
+
## File: presentation/widgets/product_card.dart
|
|
640
|
+
|
|
641
|
+
```dart
|
|
642
|
+
import 'package:cached_network_image/cached_network_image.dart';
|
|
643
|
+
import 'package:flutter/material.dart';
|
|
644
|
+
import '../../domain/entities/product.dart';
|
|
645
|
+
|
|
646
|
+
class ProductCard extends StatelessWidget {
|
|
647
|
+
const ProductCard({super.key, required this.product, required this.onTap});
|
|
648
|
+
|
|
649
|
+
final Product product;
|
|
650
|
+
final VoidCallback onTap;
|
|
651
|
+
|
|
652
|
+
@override
|
|
653
|
+
Widget build(BuildContext context) {
|
|
654
|
+
return Card(
|
|
655
|
+
clipBehavior: Clip.antiAlias,
|
|
656
|
+
child: InkWell(
|
|
657
|
+
onTap: onTap,
|
|
658
|
+
child: Column(
|
|
659
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
660
|
+
children: [
|
|
661
|
+
Expanded(
|
|
662
|
+
child: CachedNetworkImage(
|
|
663
|
+
imageUrl: product.imageUrl,
|
|
664
|
+
fit: BoxFit.cover,
|
|
665
|
+
width: double.infinity,
|
|
666
|
+
placeholder: (_, __) =>
|
|
667
|
+
const ColoredBox(color: Color(0xFFE0E0E0)),
|
|
668
|
+
errorWidget: (_, __, ___) =>
|
|
669
|
+
const Icon(Icons.broken_image_outlined),
|
|
670
|
+
),
|
|
671
|
+
),
|
|
672
|
+
Padding(
|
|
673
|
+
padding: const EdgeInsets.all(8),
|
|
674
|
+
child: Column(
|
|
675
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
676
|
+
children: [
|
|
677
|
+
Text(
|
|
678
|
+
product.name,
|
|
679
|
+
style: Theme.of(context).textTheme.bodyMedium,
|
|
680
|
+
maxLines: 2,
|
|
681
|
+
overflow: TextOverflow.ellipsis,
|
|
682
|
+
),
|
|
683
|
+
const SizedBox(height: 4),
|
|
684
|
+
Text(
|
|
685
|
+
'\$${product.price.toStringAsFixed(2)}',
|
|
686
|
+
style: Theme.of(context)
|
|
687
|
+
.textTheme
|
|
688
|
+
.titleSmall
|
|
689
|
+
?.copyWith(color: Theme.of(context).colorScheme.primary),
|
|
690
|
+
),
|
|
691
|
+
const SizedBox(height: 4),
|
|
692
|
+
Row(
|
|
693
|
+
children: [
|
|
694
|
+
const Icon(Icons.star_rounded, size: 14, color: Color(0xFFFFC107)),
|
|
695
|
+
const SizedBox(width: 2),
|
|
696
|
+
Text(
|
|
697
|
+
product.rating.toStringAsFixed(1),
|
|
698
|
+
style: Theme.of(context).textTheme.bodySmall,
|
|
699
|
+
),
|
|
700
|
+
],
|
|
701
|
+
),
|
|
702
|
+
],
|
|
703
|
+
),
|
|
704
|
+
),
|
|
705
|
+
],
|
|
706
|
+
),
|
|
707
|
+
),
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
---
|
|
714
|
+
|
|
715
|
+
## File: test/product_catalog_cubit_test.dart (stub)
|
|
716
|
+
|
|
717
|
+
```dart
|
|
718
|
+
import 'package:bloc_test/bloc_test.dart';
|
|
719
|
+
import 'package:dartz/dartz.dart';
|
|
720
|
+
import 'package:flutter_test/flutter_test.dart';
|
|
721
|
+
import 'package:mocktail/mocktail.dart';
|
|
722
|
+
|
|
723
|
+
// Replace with your actual import paths
|
|
724
|
+
import '../lib/domain/entities/product.dart';
|
|
725
|
+
import '../lib/domain/failures/failure.dart';
|
|
726
|
+
import '../lib/domain/repositories/product_catalog_repository.dart';
|
|
727
|
+
import '../lib/domain/usecases/get_products_usecase.dart';
|
|
728
|
+
import '../lib/presentation/cubit/product_catalog_cubit.dart';
|
|
729
|
+
import '../lib/presentation/cubit/product_catalog_state.dart';
|
|
730
|
+
|
|
731
|
+
class MockGetProductsUseCase extends Mock implements GetProductsUseCase {}
|
|
732
|
+
|
|
733
|
+
void main() {
|
|
734
|
+
late MockGetProductsUseCase mockUseCase;
|
|
735
|
+
late ProductCatalogCubit cubit;
|
|
736
|
+
|
|
737
|
+
const testProducts = [
|
|
738
|
+
Product(id: '1', name: 'Widget A', price: 9.99, imageUrl: '', rating: 4.2, category: 'tools'),
|
|
739
|
+
Product(id: '2', name: 'Widget B', price: 14.99, imageUrl: '', rating: 3.8, category: 'tools'),
|
|
740
|
+
];
|
|
741
|
+
|
|
742
|
+
setUp(() {
|
|
743
|
+
mockUseCase = MockGetProductsUseCase();
|
|
744
|
+
cubit = ProductCatalogCubit(mockUseCase);
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
tearDown(() => cubit.close());
|
|
748
|
+
|
|
749
|
+
group('loadProducts', () {
|
|
750
|
+
blocTest<ProductCatalogCubit, ProductCatalogState>(
|
|
751
|
+
'emits [Loading, Loaded] when products are returned',
|
|
752
|
+
build: () {
|
|
753
|
+
when(() => mockUseCase(any())).thenAnswer((_) async => Right(ProductPage(
|
|
754
|
+
products: testProducts, total: 2, currentPage: 1, hasMore: false)));
|
|
755
|
+
return cubit;
|
|
756
|
+
},
|
|
757
|
+
act: (c) => c.loadProducts(),
|
|
758
|
+
expect: () => [
|
|
759
|
+
isA<ProductCatalogLoading>(),
|
|
760
|
+
isA<ProductCatalogLoaded>().having((s) => s.products.length, 'length', 2),
|
|
761
|
+
],
|
|
762
|
+
);
|
|
763
|
+
|
|
764
|
+
blocTest<ProductCatalogCubit, ProductCatalogState>(
|
|
765
|
+
'emits [Loading, Empty] when API returns empty list',
|
|
766
|
+
build: () {
|
|
767
|
+
when(() => mockUseCase(any())).thenAnswer((_) async => Right(ProductPage(
|
|
768
|
+
products: [], total: 0, currentPage: 1, hasMore: false)));
|
|
769
|
+
return cubit;
|
|
770
|
+
},
|
|
771
|
+
act: (c) => c.loadProducts(),
|
|
772
|
+
expect: () => [isA<ProductCatalogLoading>(), isA<ProductCatalogEmpty>()],
|
|
773
|
+
);
|
|
774
|
+
|
|
775
|
+
blocTest<ProductCatalogCubit, ProductCatalogState>(
|
|
776
|
+
'emits [Loading, Error] on NetworkFailure',
|
|
777
|
+
build: () {
|
|
778
|
+
when(() => mockUseCase(any()))
|
|
779
|
+
.thenAnswer((_) async => const Left(NetworkFailure()));
|
|
780
|
+
return cubit;
|
|
781
|
+
},
|
|
782
|
+
act: (c) => c.loadProducts(),
|
|
783
|
+
expect: () => [
|
|
784
|
+
isA<ProductCatalogLoading>(),
|
|
785
|
+
isA<ProductCatalogError>()
|
|
786
|
+
.having((s) => s.message, 'message', contains('internet')),
|
|
787
|
+
],
|
|
788
|
+
);
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
---
|
|
794
|
+
|
|
795
|
+
## Dependency Injection (get_it)
|
|
796
|
+
|
|
797
|
+
```dart
|
|
798
|
+
// injection_container.dart — add these registrations
|
|
799
|
+
|
|
800
|
+
void _registerProductCatalog(GetIt sl) {
|
|
801
|
+
// Data sources
|
|
802
|
+
sl.registerLazySingleton<ProductCatalogRemoteDataSource>(
|
|
803
|
+
() => ProductCatalogRemoteDataSource(sl<Dio>()),
|
|
804
|
+
);
|
|
805
|
+
|
|
806
|
+
// Repositories
|
|
807
|
+
sl.registerLazySingleton<ProductCatalogRepository>(
|
|
808
|
+
() => ProductCatalogRepositoryImpl(sl<ProductCatalogRemoteDataSource>()),
|
|
809
|
+
);
|
|
810
|
+
|
|
811
|
+
// Use cases
|
|
812
|
+
sl.registerLazySingleton(
|
|
813
|
+
() => GetProductsUseCase(sl<ProductCatalogRepository>()),
|
|
814
|
+
);
|
|
815
|
+
|
|
816
|
+
// Cubit — registerFactory so each page gets a fresh instance
|
|
817
|
+
sl.registerFactory(
|
|
818
|
+
() => ProductCatalogCubit(sl<GetProductsUseCase>()),
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
---
|
|
824
|
+
|
|
825
|
+
## pubspec.yaml additions
|
|
826
|
+
|
|
827
|
+
```yaml
|
|
828
|
+
dependencies:
|
|
829
|
+
flutter_bloc: ^8.1.6
|
|
830
|
+
equatable: ^2.0.5
|
|
831
|
+
dartz: ^0.10.1
|
|
832
|
+
dio: ^5.7.0
|
|
833
|
+
cached_network_image: ^3.4.1
|
|
834
|
+
|
|
835
|
+
dev_dependencies:
|
|
836
|
+
bloc_test: ^9.1.7
|
|
837
|
+
mocktail: ^1.0.4
|
|
838
|
+
```
|