Imported from twichai/pos-monkey (
AGENTS.md). Install upstream withnpx skills add twichai/pos-monkey. Copyright stays with the author.
AGENTS.md - Flutter Project Contributor Guide
Welcome to the POS (Point of Sale) Flutter application repository. This guide outlines essential information for new contributors and AI assistants working on Flutter/Dart POS projects.
Repository overview
- Assets:
lib/assets/for images, icons, fonts, and other static resources. - Core:
lib/core/for common utilities, error handling, base classes, network configuration, and constants. - Data Layer:
lib/data/for persistent storage and data access, including local database implementation (database/), DAOs, seeds, tables, and main database setup. - Features:
lib/features/[feature]/for self-contained feature modules following Clean Architecture, each with its own data, domain, and presentation layers:- Data:
data/for datasources and concrete repository implementations. - Domain:
domain/for entities, use cases, and abstract repository interfaces (contracts). - Presentation:
presentation/for UI pages, widgets, and Riverpod providers.
- Data:
- Tests:
test/withunit/, andintegration/directories for comprehensive testing. - App Entry:
main.dartas the application entry point. - Routing:
routes.dartfor navigation and route definitions. - Integration testing: Integration tests verify complete user flows and interactions across multiple widgets and layers. Place integration tests in the
integration_test/feature/[feature name]/directory.
Working steps
Follow these steps when contributing to a new feature:
-
Domain Layer - Entities
Start with pure business entities inlib/features/[feature]/domain/entities/. Use Freezed for immutability:@freezed class Product with _$Product { ... } -
Domain Layer - Repository Contracts
Create abstract repository interfaces inlib/features/[feature]/domain/repositories/. Define the contract without implementation details. -
Domain Layer - Use Cases
Define use case classes inlib/features/[feature]/domain/usecases/. Each use case should have a single responsibility (e.g.,GetProductsUseCase,AddProductUseCase). -
Data Layer - Repository Implementation
Implement repository contracts inlib/features/[feature]/data/repositories/. Use data sources to fetch data and convert to domain entities. -
Data Layer - Data Sources
Create data sources inlib/features/[feature]/data/datasources/.*_local_data_source.dartfor local database/cache (using DAOs)*_remote_data_source.dartfor API calls (using Dio)
-
Data Layer - Models (Optional)
If working with APIs, create models inlib/features/[feature]/data/models/. Use Freezed + JSON serialization for API DTOs. -
Presentation Layer - Providers
Create Riverpod providers inlib/features/[feature]/presentation/providers/. Set up state management and dependency injection with proper separation:
- State Providers: For simple state values using
StateProviderorProvider - Notifier Providers: For complex state logic using
NotifierProviderwith business logic encapsulation - Use
@riverpodannotations for type-safe, auto-generated providers - Apply
autodisposemodifier to prevent memory leaks - Implement proper error handling and loading states
-
Dependency Injection Setup
Configure dependency injection inlib/core/di/injector.dartusing Riverpod providers. Register all dependencies (repositories, use cases, data sources) for the feature. -
Presentation Layer - UI
Build pages inpresentation/pages/and reusable widgets inpresentation/widgets/. Pages consume providers and display data. -
Routes & Navigation
Add routes tolib/routes/app_router.dartusing GoRouter. -
Testing
Write unit tests for use cases and repositories intest/unit/features/[feature]/. Write widget tests for UI intest/widget/features/[feature]/. Write integration tests inintegration_test/features/[feature]/.
Directory Structure
lib/
├── core/ # Common utilities & base classes
│ ├── error/ # Error handling
│ │ ├── failures.dart # Failure classes for error handling
│ │ └── exceptions.dart # Custom exception classes
│ ├── usecase/ # Base use case
│ │ └── usecase.dart # Abstract base use case class
│ ├── utils/ # Helpers, constants, formatters
│ │ ├── constants.dart # App-wide constants
│ │ ├── formatters.dart # Data formatters (date, currency, etc.)
│ │ └── helpers.dart # Utility helper functions
│ ├── di/ # Dependency Injection
│ │ └── injector.dart # Provider / Riverpod DI setup
│ ├── theme/ # App theming
│ │ ├── app_theme.dart # Main theme configuration
│ │ └── color_schemes.dart # Color schemes (light/dark)
│ └── l10n/ # Localization extensions
│ └── l10n_extensions.dart # Helper extensions for i18n
│
├── data/ # Shared data layer (database)
│ └── database/ # Local database implementation (Drift/SQLite)
│ ├── daos/ # Data Access Objects: CRUD operations
│ ├── seeds/ # Initial data population scripts
│ ├── tables/ # Database table definitions
│ ├── entities/ # Database entity classes (JOIN results)
│ └── app_database.dart # Main database setup and configuration
│
├── features/ # Feature modules (Clean Architecture)
│ ├── product/ # Example: Product feature
│ │ ├── data/
│ │ │ ├── datasources/ # Data sources
│ │ │ │ ├── product_local_data_source.dart
│ │ │ │ └── product_remote_data_source.dart
│ │ │ ├── models/ # Data models (if needed for API)
│ │ │ │ └── product_model.dart # Freezed + JSON serialization
│ │ │ └── repositories/ # Repository implementations
│ │ │ └── product_repository_impl.dart
│ │ │
│ │ ├── domain/ # Business logic layer
│ │ │ ├── entities/ # Business entities (pure Dart/Freezed)
│ │ │ │ └── product.dart # Pure Freezed class
│ │ │ ├── repositories/ # Repository contracts (abstracts)
│ │ │ │ └── product_repository.dart # Abstract interface
│ │ │ └── usecases/ # Application business logic
│ │ │ ├── get_products.dart
│ │ │ └── add_product.dart
│ │ │
│ │ └── presentation/ # UI layer
│ │ ├── providers/ # Riverpod providers & state management
│ │ │ ├── product_state_provider.dart # State providers
│ │ │ └── product_notifier_provider.dart # Notifier providers
│ │ ├── pages/ # Full-screen pages
│ │ │ └── product_page.dart
│ │ └── widgets/ # Feature-specific reusable widgets
│ │ └── product_list_item.dart
│ │
│ ├── order/ # Order feature (same structure)
│ │ ├── data/
│ │ │ ├── datasources/
│ │ │ ├── models/
│ │ │ └── repositories/
│ │ ├── domain/
│ │ │ ├── entities/
│ │ │ ├── repositories/
│ │ │ └── usecases/
│ │ └── presentation/
│ │ ├── providers/
│ │ ├── pages/
│ │ └── widgets/
│ │
│ └── payment/ # Payment feature (same structure)
│ └── ... # (same structure as above)
│
├── l10n/ # Internationalization (i18n)
│ ├── app_en.arb # English translations
│ ├── app_localizations.dart # Generated localization class
│ └── README.md # i18n documentation
│
├── routes/ # Navigation & routing
│ └── app_router.dart # GoRouter setup and route definitions
│
├── app.dart # App widget (MaterialApp, theme, router)
└── main.dart # App entry point (runApp, ProviderScope)
assets/ # Static resources
├── images/ # App images
├── icons/ # Icons
└── fonts/ # Custom fonts
integration_test/ # Integration tests
└── features/ # Feature-based integration tests
└── product/
└── product_flow_test.dart
test/ # Unit & widget tests
├── unit/ # Unit tests
│ └── features/
│ └── product/
│ ├── domain/
│ │ └── usecases/
│ └── data/
│ └── repositories/
└── widget/ # Widget tests
└── features/
└── product/
└── presentation/
Tech Stack
- Framework: Flutter: 3.35.4
- Language: Dart: 3.9.2
- State Management: flutter_riverpod: ^2.5.1 + riverpod_annotation: ^2.3.5
- Data Classes: freezed_annotation: ^2.4.4
- JSON Serialization: json_annotation: ^4.9.0
- Backend: drift: ^2.19.1+1 (SQLite) sqlite3_flutter_libs: ^0.5.0 with supporting packages
- Navigation: go_router: ^14.2.7
- HTTP Client: dio: ^5.7.0
- Image Handling: cached_network_image: ^3.4.1
- Code Generation: build_runner
- Testing: flutter_test, mockito
Local workflow
-
Set up Flutter environment and dependencies:
flutter doctor # Check Flutter installation flutter pub get # Get dependencies flutter pub run build_runner build # Generate code (if using code generation) -
Format, analyze and check your changes:
./format.sh # Format Dart code dart analyze # Static analysis flutter test # Run all tests -
Code Generation Tips:
- Run
flutter pub run build_runner watch --delete-conflicting-outputs - Add generated files (
*.g.dart,*.freezed.dart) to.gitignoreif not checked in
Testing guidelines
Use Flutter's built-in testing framework with comprehensive coverage:
flutter test --coverage # Generate coverage report
flutter test test/unit/ # Run unit tests only
flutter test integration_test/ # Run integration tests
- Test all public methods and critical user flows
- Implement golden tests for visual regression testing
- Test state management (Riverpod providers)
- Include integration tests for complete user scenarios
test('loadProducts loads items correctly', () async {
final repo = MockProductRepository();
when(() => repo.getProducts()).thenAnswer((_) async => [Product(...)]);
final notifier = ProductNotifier(repo);
await notifier.loadProducts();
expect(notifier.state.products, isNotEmpty);
});
Style notes
- Follow Effective Dart guidelines for code style.
- Use
constconstructors wherever possible for performance. - Prefer Stateless widgets over Stateful when state is not needed.
- Use meaningful names following Dart naming conventions.
- Implement proper error handling with try-catch blocks.
- Use
async/awaitfor asynchronous operations.
Commit message format
Use conventional commit format:
type(scope): description
Examples:
feat(auth): implement biometric authentication
fix(ui): resolve overflow issue on small screens
perf(list): optimize ListView performance with builder
refactor(state): migrate to Riverpod 2.0 providers
test(models): add comprehensive user model tests
style(widgets): update theme consistency across app
Pull request expectations
PRs should include:
- Summary: Clear description of functionality and user experience changes
- Screenshots: Visual proof on multiple platforms (iOS/Android/Web)
- Performance impact: Frame rate and memory usage considerations
- Platform compatibility: Testing across target platforms
- Accessibility: Screen reader and navigation accessibility verification
Before submitting, ensure:
- All tests pass (
flutter test) - No analyzer warnings (
dart analyze) - Code is formatted (
dart format .) - App builds successfully on target platforms
- UI is responsive across different screen sizes
- Accessibility features work properly
- Performance is acceptable (60fps target)
What reviewers look for
- Widget architecture: Proper widget composition and separation of concerns.
- State management: Effective use of Riverpod providers and state handling.
- Performance: Efficient rendering and memory management.
- Platform compliance: Following Material Design and Cupertino guidelines.
- Accessibility: Proper Semantics widget usage and navigation support.
- Code quality: Null safety compliance and error handling.
Flutter architecture guidelines
- Follow Clean Architecture principles with clear layer separation.
- Use Feature-First directory structure for scalability.
- Implement Repository pattern for data access abstraction.
- Use dependency injection for better testability.
Clean Architecture Flow
UI → Provider → UseCase → Repository → DataSource → DAO/API
Performance Optimization
Database Query Optimization - Avoiding N+1 Problem
The N+1 query problem is one of the most common performance issues in database-driven applications. It occurs when you execute one query to fetch a list of items, then execute N additional queries (one for each item) to fetch related data.
❌ The N+1 Problem (Anti-Pattern)
Example of BAD code:
Future<List<OrderWithDetails>> _mapToOrderWithDetails(
List<OrderEntity> orderEntities,
) async {
final List<OrderWithDetails> result = [];
for (final order in orderEntities) {
// N queries: One for each order
final orderItems = await orderItemDao.getOrderItemsByOrderId(order.orderId);
for (final orderItem in orderItems) {
// N×M queries: One for each item in each order
final item = await itemDao.getItemById(orderItem.itemId);
}
}
return result;
}
Performance Impact:
For 20 orders with 3 items each:
1 query - Fetch 20 orders
+ 20 queries - Fetch items for each order
+ 60 queries - Fetch item details for each item
────────────────────────────────────────
= 81 queries! 😱
✅ Solution: Bulk Fetching with JOINs
1. Create Bulk Fetch Methods in DAOs
// In OrderItemDao
@DriftAccessor(tables: [OrderItems, Items])
class OrderItemDao extends DatabaseAccessor<AppDatabase> {
// Bulk fetch order items with menu item details for multiple Order IDs
Future<List<OrderItemWithItem>> getOrderItemsWithItemsByOrderIds(
List<int> orderIds,
) async {
if (orderIds.isEmpty) return [];
final query = select(orderItems).join([
innerJoin(items, items.itemId.equalsExp(orderItems.itemId)),
])..where(orderItems.orderId.isIn(orderIds));
final results = await query.get();
return results.map((row) {
return OrderItemWithItem(
orderItem: row.readTable(orderItems),
item: row.readTable(items),
);
}).toList();
}
}
SQL Generated:
SELECT order_items.*, items.*
FROM order_items
INNER JOIN items ON items.item_id = order_items.item_id
WHERE order_items.order_id IN (?, ?, ?, ..., ?)
2. Use Bulk Fetching in Repository
Future<List<OrderWithDetails>> _mapToOrderWithDetails(
List<OrderEntity> orderEntities,
) async {
if (orderEntities.isEmpty) return [];
// Step 1: Collect all order IDs
final orderIds = orderEntities.map((o) => o.orderId).toList();
// Step 2: ONE bulk query for all order items + item details
final allOrderItemsWithItems = await orderItemDao
.getOrderItemsWithItemsByOrderIds(orderIds);
// Step 3: Group by order ID in memory (O(n) operation)
final orderItemsMap = <int, List<OrderItemWithItem>>{};
for (var item in allOrderItemsWithItems) {
orderItemsMap
.putIfAbsent(item.orderItem.orderId, () => [])
.add(item);
}
// Step 4: Stitch data together in memory (no more DB queries!)
final List<OrderWithDetails> result = [];
for (final order in orderEntities) {
final items = orderItemsMap[order.orderId] ?? [];
// Build OrderWithDetails using in-memory data
result.add(/* ... */);
}
return result;
}
Performance Impact:
For 20 orders with 3 items each:
1 query - Fetch 20 orders
+ 1 query - Fetch ALL items with JOIN
────────────────────────────────────────
= 2 queries! 🚀 (97.5% reduction!)
Performance Comparison Table
| Orders | Items Each | Old Queries | New Queries | Improvement |
|---|---|---|---|---|
| 1 | 1 | 3 | 2 | 33% ⬇️ |
| 1 | 5 | 7 | 2 | 71% ⬇️ |
| 5 | 3 | 21 | 2 | 90% ⬇️ |
| 20 | 3 | 81 | 2 | 97.5% ⬇️ |
| 100 | 5 | 601 | 2 | 99.7% ⬇️ |
Best Practices for Database Optimization
1. Always Use Batch Operations
// ❌ Bad: N queries
for (var id in itemIds) {
await itemDao.getItemById(id);
}
// ✅ Good: 1 query
final items = await itemDao.getItemsByIds(itemIds);
2. Leverage Database JOINs
// ❌ Bad: Separate queries then join in code
final orders = await orderDao.getOrders();
for (var order in orders) {
final items = await orderItemDao.getItemsByOrderId(order.id);
}
// ✅ Good: Database-level JOIN
final ordersWithItems = await orderDao.getOrdersWithItemsJoin();
3. Use WHERE IN for Multiple IDs
// DAO method
Future<List<ItemEntity>> getItemsByIds(List<int> ids) {
if (ids.isEmpty) return Future.value([]);
return (select(items)..where((t) => t.itemId.isIn(ids))).get();
}
4. Fetch Reference Data Once
// ❌ Bad: Fetching tags map N times
for (var item in items) {
final tagMap = await itemDao.getItemTagsMap(); // Called N times!
}
// ✅ Good: Fetch once, use many times
final tagMap = await itemDao.getItemTagsMap();
for (var item in items) {
final tags = tagMap[item.id];
}
5. Use Pagination for Large Datasets
// Repository method with pagination
Future<List<OrderEntity>> getOrders({
int limit = 20,
int offset = 0,
}) async {
return (select(orders)
..orderBy([(t) => OrderingTerm.desc(t.orderDate)])
..limit(limit, offset: offset))
.get();
}
Query Optimization Checklist
When implementing repository methods, always ask:
- Am I querying inside a loop? → Use batch operations
- Am I fetching related data separately? → Use JOINs
- Am I calling the same query multiple times? → Cache the result
- Am I loading all data at once? → Implement pagination
- Can the database do this work instead of Dart? → Use SQL features
Real-World Examples from This Project
Example 1: Load Order Items (main_order feature)
Before (2*N + 1 queries):
final orderItems = await orderItemDao.getOrderItemsByOrderId(orderId);
for (var orderItem in orderItems) {
final item = await itemDao.getItemById(orderItem.itemId);
final tags = await itemDao.getItemTagsMap(); // Called N times!
}
After (2 queries):
final orderItemsWithItems = await orderItemDao
.getOrderItemsWithItemsByOrderId(orderId);
final tagMap = await itemDao.getItemTagsMap(); // Called once
Example 2: Order Status List (order_status feature)
Before (81 queries for 20 orders):
for (final order in orders) {
final items = await orderItemDao.getOrderItemsByOrderId(order.id);
for (final item in items) {
final details = await itemDao.getItemById(item.itemId);
}
}
After (2 queries for 20 orders):
final orderIds = orders.map((o) => o.orderId).toList();
final allItems = await orderItemDao.getOrderItemsWithItemsByOrderIds(orderIds);
final itemsMap = _groupByOrderId(allItems);
Monitoring Performance
Use Drift's logging to monitor query performance:
// Enable query logging in debug mode
AppDatabase() : super(_openConnection()) {
if (kDebugMode) {
logStatements = true;
}
}
Watch the console for:
- Multiple similar queries (indicates N+1 problem)
- Long query execution times
- Queries inside loops
Additional Performance Tips
- Use Indexes - Add indexes to frequently queried columns
- Avoid SELECT * - Only select needed columns
- Use Transactions - Group multiple writes in transactions
- Cache Static Data - Cache tags, categories, payment methods
- Profile Queries - Use Drift's query logging in development
Widget best practices
- Prefer composition over inheritance for widget design.
- Use
constconstructors to improve performance. - Implement proper
Keyusage for widget identity. - Create reusable widgets with clear, focused responsibilities.
- Use
Builderwidgets to manage context scope appropriately. - Implement proper disposal of resources in
dispose()methods.
Riverpod state management
- Design providers with appropriate granularity.
- Use
StateProviderfor simple state,StateNotifierProviderfor complex state. - Apply
autodisposemodifier to prevent memory leaks. - Use
familymodifier for parameterized providers. - Implement proper error handling in providers.
- Test providers in isolation with appropriate mocking.
Performance optimization
- Use
ListView.builderfor large lists instead ofListView. - Implement
RepaintBoundaryfor expensive widgets. - Apply
AutomaticKeepAliveClientMixinfor preserving state. - Use
ValueListenableBuilderfor granular rebuilds. - Implement image caching and optimization strategies.
- Monitor performance with Flutter Inspector and DevTools.
Platform-specific considerations
- Follow Material Design guidelines for Android.
- Implement Cupertino design patterns for iOS.
- Use
Platform.isIOSandPlatform.isAndroidfor platform-specific code. - Implement proper platform channels for native functionality.
- Test on real devices, not just simulators/emulators.
- Handle platform-specific permissions appropriately.
Data management
- Use
json_annotationwithjson_serializablefor JSON serialization. - Implement proper local storage with Hive or shared_preferences.
- Use HTTP clients with proper error handling and timeouts.
- Apply caching strategies for improved performance.
- Handle offline scenarios gracefully.
- Implement proper data validation and sanitization.
UI/UX guidelines
- Follow platform design guidelines (Material/Cupertino).
- Implement responsive design for different screen sizes.
- Support both light and dark themes.
- Use proper spacing and typography from design system.
- Implement smooth animations and transitions.
- Provide proper loading states and error handling.
Accessibility best practices
- Use
Semanticswidget for screen reader support. - Implement proper focus management and navigation.
- Ensure sufficient color contrast ratios.
- Provide alternative text for images and icons.
- Test with TalkBack (Android) and VoiceOver (iOS).
- Support dynamic font sizing and high contrast modes.
Security considerations
- Validate all user inputs and API responses.
- Use secure storage for sensitive data (flutter_secure_storage).
- Implement proper authentication and session management.
- Protect against common vulnerabilities (XSS, injection attacks).
- Use HTTPS for all network communications.
- Implement proper certificate pinning for production apps.
Testing strategies
- Write unit tests for business logic and utility functions.
- Implement integration tests for complete user flows.
- Use golden tests for visual regression testing.
- Mock external dependencies appropriately.
- Test error scenarios and edge cases thoroughly.
🧩 Example 1 – State Management with Riverpod
❌ Bad Code
final counterProvider = StateProvider<int>((ref) => 0);
class CounterPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Scaffold(
body: Center(child: Text('$count')),
floatingActionButton: FloatingActionButton(
onPressed: () {
ref.read(counterProvider.notifier).state++;
},
),
);
}
}
❌ Problems:
- Business logic is tightly coupled to the UI.
- Hard to test and maintain.
- Lacks separation between presentation and state management.
✅ Good Code (Clean Architecture + Riverpod Annotation)
// State model for counter feature
import 'package:freezed_annotation/freezed_annotation.dart';
part 'counter_state.freezed.dart';
@freezed
class CounterState with _$CounterState {
const factory CounterState({
@Default(0) int count,
@Default(false) bool isLoading,
String? error,
}) = _CounterState;
}
// Notifier implementation
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'counter_notifier.g.dart';
@riverpod
class CounterNotifier extends _$CounterNotifier {
@override
CounterState build() {
return const CounterState();
}
Future<void> increment() async {
state = state.copyWith(isLoading: true, error: null);
try {
// Simulate business logic
await Future.delayed(const Duration(milliseconds: 500));
state = state.copyWith(
count: state.count + 1,
isLoading: false,
);
} catch (e) {
state = state.copyWith(
isLoading: false,
error: e.toString(),
);
}
}
void reset() {
state = const CounterState();
}
}
// UI Implementation
class CounterPage extends ConsumerWidget {
const CounterPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final counterState = ref.watch(counterNotifierProvider);
final counterNotifier = ref.read(counterNotifierProvider.notifier);
return Scaffold(
appBar: AppBar(title: Text(context.l10n.counter)),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (counterState.isLoading)
const CircularProgressIndicator()
else
Text(
'${counterState.count}',
style: Theme.of(context).textTheme.headlineMedium,
),
if (counterState.error != null)
StatusMessage.error(message: counterState.error!),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: counterState.isLoading ? null : counterNotifier.increment,
child: Text(context.l10n.increment),
),
const SizedBox(width: 16),
TextButton(
onPressed: counterNotifier.reset,
child: Text(context.l10n.reset),
),
],
),
],
),
),
);
}
}
✅ Advantages:
- Logic isolated in a Notifier class with proper state management
- Testable via unit tests with clear state transitions
- Riverpod annotations generate boilerplate-free providers
- Proper error handling and loading states
- Clean separation of concerns between UI and business logic
🧩 Example 2 – Data Model with Freezed + JSON Serialization
❌ Bad Code
class Product {
int id;
String name;
double price;
Product({required this.id, required this.name, required this.price});
}
❌ Problems:
- No immutability.
- No equality or copyWith.
- Manual serialization needed.
✅ Good Code
import 'package:freezed_annotation/freezed_annotation.dart';
part 'product.freezed.dart';
part 'product.g.dart';
@freezed
class Product with _$Product {
const factory Product({
required int id,
required String name,
required double price,
}) = _Product;
factory Product.fromJson(Map<String, dynamic> json) => _$ProductFromJson(json);
}
✅ Advantages:
- Immutable and type-safe.
copyWith, equality, andtoJson/fromJsonauto-generated.- Works perfectly with Drift and Dio.
🧩 Example 3 – Drift Table and DAO Layer
❌ Bad Code
class ProductTable extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
RealColumn get price => real()();
}
Future<List<Product>> getAllProducts(Database db) async {
return await db.select(db.productTable).get();
}
❌ Problems:
- Logic directly tied to database instance.
- No abstraction → hard to mock/test.
- Missing repository layer.
✅ Good Code (DAO + Repository Pattern)
// tables/products.dart
@DataClassName('ProductEntity')
class Products extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text().withLength(min: 1, max: 255)();
RealColumn get price => real().check(price.isBiggerThanValue(0))();
}
// dao/product_dao.dart
@DriftAccessor(tables: [Products])
class ProductDao extends DatabaseAccessor<AppDatabase> with _$ProductDaoMixin {
ProductDao(AppDatabase db) : super(db);
Future<List<ProductEntity>> getAll() => select(products).get();
Future<int> insertProduct(ProductsCompanion entity) => into(products).insert(entity);
}
// repositories/product_repository.dart
class ProductRepository {
final ProductDao _dao;
ProductRepository(this._dao);
Future<List<ProductEntity>> fetchProducts() => _dao.getAll();
}
✅ Advantages:
- Clear separation (Table → DAO → Repository).
- Easier testing and migration.
- Follows Clean Architecture.
🧩 Example 4 – Navigation with go_router
❌ Bad Code
Navigator.push(context, MaterialPageRoute(builder: (_) => ProductPage()));
❌ Problems:
- Imperative navigation.
- Hard to manage deep links or named routes.
✅ Good Code
final router = GoRouter(
routes: [
GoRoute(path: '/', builder: (context, state) => const HomePage()),
GoRoute(path: '/product/:id', builder: (context, state) {
final id = int.parse(state.pathParameters['id']!);
return ProductPage(productId: id);
}),
],
);
✅ Advantages:
- Declarative, scalable routing.
- Handles deep links, web, and desktop easily.
🧩 Example 5 – Complete Feature Implementation
This example shows the complete Clean Architecture flow based on the main_item_management feature.
Step 1: Domain Entity (Freezed)
// lib/features/item_management/domain/entities/item_entity.dart
@freezed
class Item with _$Item {
const factory Item({
required int itemId,
required String itemName,
required double price,
@Default(true) bool isActive,
}) = _Item;
}
Step 2: Repository Contract
// lib/features/item_management/domain/repositories/item_repository.dart
abstract class ItemRepository {
Future<Either<Failure, List<Item>>> getAllItems();
Future<Either<Failure, Item>> getItemById(int id);
}
Step 3: Use Case
// lib/features/item_management/domain/usecases/get_all_items_usecase.dart
class GetAllItemsUseCase extends UseCase<List<Item>, NoParams> {
final ItemRepository _repository;
GetAllItemsUseCase(this._repository);
@override
Future<Either<Failure, List<Item>>> call(NoParams params) async {
return await _repository.getAllItems();
}
}
Step 4: Repository Implementation
// lib/features/item_management/data/repositories/item_repository_impl.dart
class ItemRepositoryImpl implements ItemRepository {
final AppDatabase _database;
ItemRepositoryImpl(this._database);
@override
Future<Either<Failure, List<Item>>> getAllItems() async {
try {
final entities = await _database.itemDao.getAllItems();
final items = entities.map((e) => Item(
itemId: e.itemId,
itemName: e.itemName,
price: e.price,
isActive: e.isActive,
)).toList();
return Right(items);
} catch (e) {
return Left(RepositoryFailure(e.toString()));
}
}
}
Step 5: State (Freezed)
// lib/features/item_management/presentation/providers/item_state.dart
@freezed
class ItemState with _$ItemState {
const factory ItemState({
@Default([]) List<Item> items,
@Default(false) bool isLoading,
String? errorMessage,
}) = _ItemState;
}
Step 6: Notifier (Riverpod 2.0 with @riverpod)
// lib/features/item_management/presentation/providers/item_notifier.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'item_notifier.g.dart';
@riverpod
class ItemNotifier extends _$ItemNotifier {
late final GetAllItemsUseCase _getAllItemsUseCase;
@override
ItemState build() {
// Initialize dependencies in build method
_getAllItemsUseCase = ref.watch(getAllItemsUseCaseProvider);
// Return initial state
return const ItemState();
}
Future<void> loadItems() async {
state = state.copyWith(isLoading: true, errorMessage: null);
// Use the initialized dependency
final result = await _getAllItemsUseCase(const NoParams());
result.fold(
(failure) => state = state.copyWith(
isLoading: false,
errorMessage: failure.message,
),
(items) => state = state.copyWith(
items: items,
isLoading: false,
),
);
}
}
Step 7: Dependency Injection
// lib/features/item_management/di/item_providers.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'item_providers.g.dart';
// Repository Provider
@riverpod
ItemRepository itemRepository(ItemRepositoryRef ref) {
final database = ref.watch(databaseProvider);
return ItemRepositoryImpl(database);
}
// Use Case Provider
@riverpod
GetAllItemsUseCase getAllItemsUseCase(GetAllItemsUseCaseRef ref) {
final repository = ref.watch(itemRepositoryProvider);
return GetAllItemsUseCase(repository);
}
// ✨ Notifier provider is AUTO-GENERATED by @riverpod annotation!
// Just run: flutter pub run build_runner build --delete-conflicting-outputs
// Then use: itemNotifierProvider in your UI
Step 8: UI
// lib/features/item_management/presentation/pages/item_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
class ItemPage extends HookConsumerWidget {
const ItemPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Watch the auto-generated provider
final state = ref.watch(itemNotifierProvider);
final notifier = ref.read(itemNotifierProvider.notifier);
// Load items when widget is first built
useEffect(() {
Future.microtask(() => notifier.loadItems());
return null;
}, []);
if (state.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (state.errorMessage != null) {
return Center(
child: StatusMessage.error(message: state.errorMessage!),
);
}
return ListView.builder(
itemCount: state.items.length,
itemBuilder: (context, index) {
final item = state.items[index];
return ListTile(
title: Text(item.itemName),
subtitle: Text(L10nHelpers.formatCurrency(context, item.price)),
);
},
);
}
}
Architecture Flow
UI (ItemPage - HookConsumerWidget)
↓ watches
Provider (itemNotifierProvider) ← AUTO-GENERATED by @riverpod
↓ creates
Notifier (ItemNotifier extends _$ItemNotifier)
↓ uses ref.read() to access
UseCase Provider (getAllItemsUseCaseProvider)
↓ provides
UseCase (GetAllItemsUseCase)
↓ calls
Repository Interface (ItemRepository)
↑ implemented by
Repository Impl (ItemRepositoryImpl)
↓ uses
Database (AppDatabase + DAO)
Code Generation Command
After creating your providers, run:
flutter pub run build_runner build --delete-conflicting-outputs
This generates:
item_notifier.g.dart- Contains_$ItemNotifierbase class anditemNotifierProvideritem_providers.g.dart- Contains all provider implementations
Key Benefits
- ✅ Clean separation - Each layer has single responsibility
- ✅ Testable - Easy to mock dependencies at each layer
- ✅ Type-safe - Freezed entities with immutability
- ✅ Error handling - Either<Failure, Success> pattern
- ✅ DI ready - Provider-based dependency injection with @riverpod
- ✅ Performance - Optimistic updates, efficient state management
- ✅ Auto-dispose - Providers automatically clean up when not in use
- ✅ Less boilerplate - Code generation handles provider setup
Theme & Design System
The app uses a comprehensive theme system based on DaisyUI with status colors for consistent UI design.
Quick Theme Usage
// Status colors
Container(color: AppTheme.success) // Green for success
Container(color: AppTheme.error) // Red for errors
Container(color: AppTheme.warning) // Yellow for warnings
Container(color: AppTheme.info) // Blue for info
// Pre-built widgets
StatusBadge.success(label: 'Active')
StatusMessage.error(message: 'Failed to load')
StatusButton.warning(label: 'Retry', onPressed: () {})
Theme Best Practices
- ✅ Use
AppThemeconstants instead of manual colors - ✅ Always pair colors with their content colors for proper contrast
- ✅ Prefer pre-built status widgets for consistency
- ✅ Use status colors semantically (green = success, red = error, etc.)
👉 See docs/THEME_GUIDE.md for complete theme tokens, color palette, design system guidelines, and detailed usage examples.
🌍 Internationalization (i18n) Usage
The app uses Flutter's official i18n system with ARB (Application Resource Bundle) files for translations.
i18n Structure
Location: lib/l10n/
app_en.arb- English translations (primary)README.md- Complete i18n documentation
Location: lib/core/l10n/
l10n_extensions.dart- Helper extensions
Setup (Already Done)
The app is configured with:
flutter_localizationspackageintlpackage- ARB file generation enabled
- English (en) as the primary language
How to Use Translations
Step 1: Import the Extension
import 'package:pos/core/l10n/l10n_extensions.dart';
Step 2: Use in Your Code
// Simple translations
Text(context.l10n.appTitle) // "POS System"
Text(context.l10n.newOrder) // "New Order"
Text(context.l10n.total) // "Total"
Text(context.l10n.currentOrder) // "Current Order"
// Buttons
ElevatedButton(
onPressed: () {},
child: Text(context.l10n.placeOrder), // "Place Order"
)
TextButton(
onPressed: () {},
child: Text(context.l10n.cancel), // "Cancel"
)
// Status messages
StatusMessage.success(
message: context.l10n.orderCompleted, // "Order completed successfully!"
)
StatusMessage.error(
message: context.l10n.orderFailed, // "Order failed. Please try again."
)
Translations with Parameters
Currency Formatting
Text(context.l10n.currency('100.00')) // Output: ฿100.00
// Or use helper
Text(L10nHelpers.formatCurrency(context, 100.50)) // Output: ฿100.50
Item Count
Text(context.l10n.itemCount(5)) // Output: 5 items
// Or use helper
Text(L10nHelpers.formatItemCount(context, items.length))
Quantity Display
Text(context.l10n.quantityValue(2)) // Output: Qty: 2
// Or use helper
Text(L10nHelpers.formatQuantity(context, 2))
Order Number
Text(context.l10n.orderNumber('001')) // Output: Order #001
// Or use helper
Text(L10nHelpers.formatOrderNumber(context, '001'))
Available Translation Keys
Common Labels
appTitle, newOrder, currentOrder, items, total, subtotal,
tax, quantity, price, search, categories, allCategories
Actions
addItem, removeItem, clearOrder, placeOrder, cancel, confirm,
save, delete, edit
Status & Messages
success, error, warning, info, processing,
orderCompleted, orderFailed, noItems, emptyCart,
addItemsToStart
Status Labels
active, pending, completed, failed, paid, unpaid
Real-World Examples
Order List Screen
class OrderListScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(
title: Text(context.l10n.currentOrder),
),
body: orders.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(context.l10n.emptyCart),
Text(context.l10n.addItemsToStart),
],
),
)
: ListView.builder(
itemCount: orders.length,
itemBuilder: (context, index) {
final order = orders[index];
return ListTile(
title: Text(context.l10n.orderNumber(order.id)),
subtitle: Text(L10nHelpers.formatCurrency(context, order.total)),
);
},
),
);
}
}
Confirmation Dialog
void showDeleteConfirmation(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(context.l10n.warning),
content: Text('Are you sure you want to delete this item?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(context.l10n.cancel),
),
ElevatedButton(
onPressed: () {
// Delete logic
Navigator.pop(context);
showStatusSnackbar(
context,
message: 'Item deleted',
type: StatusType.success,
);
},
child: Text(context.l10n.delete),
),
],
),
);
}
Search Field
TextField(
decoration: InputDecoration(
labelText: context.l10n.search,
prefixIcon: Icon(Icons.search),
),
)
i18n Best Practices
-
Never hardcode strings
// ❌ Bad Text('New Order') // ✅ Good Text(context.l10n.newOrder) -
Use helpers for formatted values
// ❌ Bad Text('฿${amount.toStringAsFixed(2)}') // ✅ Good Text(L10nHelpers.formatCurrency(context, amount)) -
Always add context descriptions in ARB files
{ "newOrder": "New Order", "@newOrder": { "description": "New order screen title" } } -
Use meaningful key names
// ❌ Bad context.l10n.text1 // ✅ Good context.l10n.placeOrder
Adding New Translations
- Open
lib/l10n/app_en.arb - Add your key and translation:
{ "myNewKey": "My New Text", "@myNewKey": { "description": "Description of this text" } } - Run
flutter pub getto regenerate - Use in code:
context.l10n.myNewKey
Adding More Languages (Future)
To add Thai language support:
-
Create
lib/l10n/app_th.arb:{ "@@locale": "th", "appTitle": "ระบบขายหน้าร้าน", "newOrder": "คำสั่งซื้อใหม่", "total": "รวมทั้งสิ้น" } -
Add to supported locales in
main.dart:supportedLocales: const [ Locale('en'), // English Locale('th'), // Thai ], -
Run
flutter pub get
Troubleshooting i18n
If translations don't work:
- Run
flutter pub getto generate files - Restart your IDE/editor
- Check that imports are correct
- Verify ARB file has valid JSON syntax
For complete documentation, see: lib/l10n/README.md and I18N_SETUP.md
Documentation
What NOT to Create
- Do not create new Markdown documents solely to summarize code changes
- Do not create redundant documentation
- Do not document obvious code
Recommended File Editing Approaches
- Use proper text editors - VSCode, Vim, Nano for text files
- Always backup before editing - Create copies of important files
- Use version control - Git for tracking changes
- Test changes locally - Verify modifications work as expected
Post-Development Workflow
Always Run Format Script After Changes
After completing any development work, always run the format script to ensure code consistency:
./format.sh
This script will:
- Format all Dart code with
./format.sh - Run static analysis with
dart analyze - Generate code if needed with
build_runner - Ensure consistent code style across the project
