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.
Files changed (194) hide show
  1. package/AGENTS.md +228 -0
  2. package/CLAUDE.md +236 -0
  3. package/LICENSE +21 -0
  4. package/README.md +652 -0
  5. package/agents/android/android-crash-analyzer/README.md +134 -0
  6. package/agents/android/android-crash-analyzer/agent.md +206 -0
  7. package/agents/android/android-crash-analyzer/example-input.md +138 -0
  8. package/agents/android/android-crash-analyzer/example-output.md +225 -0
  9. package/agents/android/anr-investigation/README.md +78 -0
  10. package/agents/android/anr-investigation/agent.md +117 -0
  11. package/agents/android/anr-investigation/example-input.md +62 -0
  12. package/agents/android/anr-investigation/example-output.md +103 -0
  13. package/agents/android/axiom/agent.md +197 -0
  14. package/agents/android/code-reviewer/README.md +108 -0
  15. package/agents/android/code-reviewer/agent.md +210 -0
  16. package/agents/android/code-reviewer/example-input.md +116 -0
  17. package/agents/android/code-reviewer/example-output.md +115 -0
  18. package/agents/android/compose-navigation/README.md +76 -0
  19. package/agents/android/compose-navigation/agent.md +121 -0
  20. package/agents/android/compose-navigation/example-input.md +49 -0
  21. package/agents/android/compose-navigation/example-output.md +325 -0
  22. package/agents/android/compose-screen-builder/README.md +95 -0
  23. package/agents/android/compose-screen-builder/agent.md +170 -0
  24. package/agents/android/compose-screen-builder/example-input.md +40 -0
  25. package/agents/android/compose-screen-builder/example-output.md +568 -0
  26. package/agents/android/compose-ui-reviewer/README.md +73 -0
  27. package/agents/android/compose-ui-reviewer/agent.md +253 -0
  28. package/agents/android/compose-ui-reviewer/example-input.md +84 -0
  29. package/agents/android/compose-ui-reviewer/example-output.md +102 -0
  30. package/agents/android/crash-analyzer/README.md +53 -0
  31. package/agents/android/crash-analyzer/agent.md +295 -0
  32. package/agents/android/memory-leak-analyzer/README.md +79 -0
  33. package/agents/android/memory-leak-analyzer/agent.md +116 -0
  34. package/agents/android/memory-leak-analyzer/example-input.md +78 -0
  35. package/agents/android/memory-leak-analyzer/example-output.md +128 -0
  36. package/agents/cross-platform/accessibility-auditor/README.md +47 -0
  37. package/agents/cross-platform/accessibility-auditor/agent.md +268 -0
  38. package/agents/cross-platform/accessibility-auditor/example-input.md +84 -0
  39. package/agents/cross-platform/accessibility-auditor/example-output.md +132 -0
  40. package/agents/cross-platform/appforge/agent.md +303 -0
  41. package/agents/cross-platform/ci-cd-generator/README.md +65 -0
  42. package/agents/cross-platform/ci-cd-generator/agent.md +422 -0
  43. package/agents/cross-platform/ci-cd-generator/example-input.md +69 -0
  44. package/agents/cross-platform/ci-cd-generator/example-output.md +261 -0
  45. package/agents/cross-platform/crasher/agent.md +221 -0
  46. package/agents/cross-platform/figma/agent.md +247 -0
  47. package/agents/cross-platform/launchpad/agent.md +330 -0
  48. package/agents/cross-platform/mobile-harness/agent.md +422 -0
  49. package/agents/cross-platform/mobile-memory/agent.md +429 -0
  50. package/agents/cross-platform/perf/agent.md +261 -0
  51. package/agents/cross-platform/pipeline/agent.md +288 -0
  52. package/agents/cross-platform/release-notes-generator/README.md +63 -0
  53. package/agents/cross-platform/release-notes-generator/agent.md +218 -0
  54. package/agents/cross-platform/release-notes-generator/example-input.md +71 -0
  55. package/agents/cross-platform/release-notes-generator/example-output.md +96 -0
  56. package/agents/cross-platform/scribe/agent.md +180 -0
  57. package/agents/cross-platform/security-scanner/README.md +61 -0
  58. package/agents/cross-platform/security-scanner/agent.md +287 -0
  59. package/agents/cross-platform/security-scanner/example-input.md +77 -0
  60. package/agents/cross-platform/security-scanner/example-output.md +141 -0
  61. package/agents/cross-platform/sentinel/agent.md +217 -0
  62. package/agents/cross-platform/store-listing-writer/README.md +58 -0
  63. package/agents/cross-platform/store-listing-writer/agent.md +380 -0
  64. package/agents/cross-platform/store-listing-writer/example-input.md +67 -0
  65. package/agents/cross-platform/store-listing-writer/example-output.md +131 -0
  66. package/agents/flutter/bloc-feature-builder/README.md +101 -0
  67. package/agents/flutter/bloc-feature-builder/agent.md +225 -0
  68. package/agents/flutter/bloc-feature-builder/example-input.md +47 -0
  69. package/agents/flutter/bloc-feature-builder/example-output.md +838 -0
  70. package/agents/flutter/dart/agent.md +223 -0
  71. package/agents/flutter/widget-generator/README.md +66 -0
  72. package/agents/flutter/widget-generator/agent.md +264 -0
  73. package/agents/flutter/widget-generator/example-input.md +71 -0
  74. package/agents/flutter/widget-generator/example-output.md +163 -0
  75. package/agents/gaming/forge/agent.md +197 -0
  76. package/agents/gaming/unreal/agent.md +200 -0
  77. package/agents/ios/crash-analyzer/README.md +69 -0
  78. package/agents/ios/crash-analyzer/agent.md +300 -0
  79. package/agents/ios/crash-analyzer/example-input.md +83 -0
  80. package/agents/ios/crash-analyzer/example-output.md +126 -0
  81. package/agents/ios/swift/agent.md +206 -0
  82. package/agents/ios/swift-reviewer/README.md +68 -0
  83. package/agents/ios/swift-reviewer/agent.md +231 -0
  84. package/agents/ios/swift-reviewer/example-input.md +100 -0
  85. package/agents/ios/swift-reviewer/example-output.md +127 -0
  86. package/agents/react-native/bridge/agent.md +212 -0
  87. package/agents/react-native/performance-optimizer/README.md +81 -0
  88. package/agents/react-native/performance-optimizer/agent.md +284 -0
  89. package/agents/react-native/performance-optimizer/example-input.md +104 -0
  90. package/agents/react-native/performance-optimizer/example-output.md +132 -0
  91. package/agents/unity/shader-generator/README.md +77 -0
  92. package/agents/unity/shader-generator/agent.md +270 -0
  93. package/agents/unity/shader-generator/example-input.md +70 -0
  94. package/agents/unity/shader-generator/example-output.md +160 -0
  95. package/agents/unreal/blueprint-advisor/README.md +71 -0
  96. package/agents/unreal/blueprint-advisor/agent.md +311 -0
  97. package/agents/unreal/blueprint-advisor/example-input.md +73 -0
  98. package/agents/unreal/blueprint-advisor/example-output.md +217 -0
  99. package/cli/index.js +1091 -0
  100. package/docs/agent-guide.md +138 -0
  101. package/docs/chatgpt-integration.md +219 -0
  102. package/docs/core-loops.md +143 -0
  103. package/docs/cursor-integration.md +143 -0
  104. package/docs/getting-started.md +132 -0
  105. package/docs/index.html +711 -0
  106. package/docs/installation.md +271 -0
  107. package/docs/mobile-harness.md +160 -0
  108. package/docs/mobile-mcp.md +172 -0
  109. package/docs/mobile-memory.md +349 -0
  110. package/docs/release-process.md +85 -0
  111. package/docs/releases/release-v1.0.0.md +165 -0
  112. package/docs/roadmap/github-issues.md +239 -0
  113. package/docs/skill-categories.md +125 -0
  114. package/docs/vscode-copilot-integration.md +104 -0
  115. package/docs/wiki.md +615 -0
  116. package/install.sh +507 -0
  117. package/mobile-karpathy.md +79 -0
  118. package/package.json +57 -0
  119. package/skills/android/README.md +31 -0
  120. package/skills/android/android-tdd.md +89 -0
  121. package/skills/android/anr-investigation.md +251 -0
  122. package/skills/android/code-review.md +98 -0
  123. package/skills/android/compose-migration.md +48 -0
  124. package/skills/android/compose-review.md +78 -0
  125. package/skills/android/kotlin-modernize.md +51 -0
  126. package/skills/android/memory-leak-investigation.md +256 -0
  127. package/skills/android/proguard-rules.md +48 -0
  128. package/skills/cross-platform/accessibility-audit.md +63 -0
  129. package/skills/cross-platform/api-versioning.md +59 -0
  130. package/skills/cross-platform/clean-code-audit.md +239 -0
  131. package/skills/cross-platform/crash-triage.md +53 -0
  132. package/skills/cross-platform/deeplink-debug.md +57 -0
  133. package/skills/cross-platform/feature-slice.md +48 -0
  134. package/skills/cross-platform/grill-mobile.md +64 -0
  135. package/skills/cross-platform/mobile-mcp-qa.md +193 -0
  136. package/skills/cross-platform/mobile-memory-graph.md +179 -0
  137. package/skills/cross-platform/mobile-memory-save.md +210 -0
  138. package/skills/cross-platform/mobile-memory-search.md +154 -0
  139. package/skills/cross-platform/perf-audit.md +61 -0
  140. package/skills/cross-platform/prd-verification.md +324 -0
  141. package/skills/cross-platform/release-prep.md +64 -0
  142. package/skills/cross-platform/security-audit.md +281 -0
  143. package/skills/cross-platform/store-listing.md +75 -0
  144. package/skills/flutter/README.md +32 -0
  145. package/skills/flutter/dart-modernize.md +47 -0
  146. package/skills/flutter/flutter-review.md +59 -0
  147. package/skills/flutter/flutter-tdd.md +73 -0
  148. package/skills/flutter/widget-extract.md +41 -0
  149. package/skills/flutter/widget-gen.md +131 -0
  150. package/skills/gaming/blueprint-to-cpp.md +51 -0
  151. package/skills/gaming/game-perf.md +54 -0
  152. package/skills/gaming/shader-gen.md +44 -0
  153. package/skills/gaming/unity-tdd.md +85 -0
  154. package/skills/ios/README.md +45 -0
  155. package/skills/ios/data-persistence.md +151 -0
  156. package/skills/ios/ios-tdd.md +90 -0
  157. package/skills/ios/networking.md +139 -0
  158. package/skills/ios/performance.md +122 -0
  159. package/skills/ios/swift-concurrency.md +57 -0
  160. package/skills/ios/swift-review.md +124 -0
  161. package/skills/ios/swiftui-review.md +49 -0
  162. package/skills/ios/swiftui-state.md +122 -0
  163. package/skills/ios/unit-testing.md +144 -0
  164. package/skills/ios/xcode-warnings.md +37 -0
  165. package/skills/react-native/README.md +42 -0
  166. package/skills/react-native/bridge-audit.md +104 -0
  167. package/skills/react-native/expo-optimize.md +55 -0
  168. package/skills/react-native/new-arch-migrate.md +53 -0
  169. package/skills/react-native/performance.md +124 -0
  170. package/skills/react-native/rn-review.md +53 -0
  171. package/skills/react-native/rn-tdd.md +76 -0
  172. package/skills/shared/README.md +46 -0
  173. package/skills/shared/accessibility-audit.md +132 -0
  174. package/skills/shared/crash-analysis.md +110 -0
  175. package/skills/shared/security-scan.md +149 -0
  176. package/skills/unity/README.md +31 -0
  177. package/skills/unity/shader-review.md +110 -0
  178. package/templates/agent-template.md +88 -0
  179. package/templates/prompt-template.md +50 -0
  180. package/templates/skill-template.md +64 -0
  181. package/templates/workflow-template.md +48 -0
  182. package/workflows/app-launch.md +66 -0
  183. package/workflows/appforge-workflow.md +232 -0
  184. package/workflows/ci-setup.md +77 -0
  185. package/workflows/crash-to-fix.md +71 -0
  186. package/workflows/feature-ship.md +61 -0
  187. package/workflows/game-level.md +72 -0
  188. package/workflows/mobile-harness.md +319 -0
  189. package/workflows/mobile-mcp-qa.md +228 -0
  190. package/workflows/mobile-memory-workflow.md +243 -0
  191. package/workflows/new-project-setup.md +93 -0
  192. package/workflows/new-screen.md +68 -0
  193. package/workflows/perf-sprint.md +78 -0
  194. 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
+ ```