Files
worker/docs/CODE_EXAMPLES.md
Phuoc Nguyen 192c322816 a
2025-11-17 17:56:34 +07:00

773 lines
18 KiB
Markdown

# Flutter Code Examples & Patterns
This document contains all Dart code examples and patterns referenced in `CLAUDE.md`. Use these as templates when implementing features in the Worker app.
---
## Table of Contents
- [Best Practices](#best-practices)
- [UI/UX Components](#uiux-components)
- [State Management](#state-management)
- [Performance Optimization](#performance-optimization)
- [Offline Strategy](#offline-strategy)
- [Localization](#localization)
- [Deployment](#deployment)
---
## Best Practices
### Hive Box Type Management
**✅ CORRECT - Use Box<dynamic> with type filtering**
```dart
Box<dynamic> get _box {
return Hive.box<dynamic>(HiveBoxNames.favoriteBox);
}
Future<List<FavoriteModel>> getAllFavorites(String userId) async {
try {
final favorites = _box.values
.whereType<FavoriteModel>() // Type-safe filtering
.where((fav) => fav.userId == userId)
.toList();
return favorites;
} catch (e) {
debugPrint('[DataSource] Error: $e');
rethrow;
}
}
Future<List<FavoriteModel>> getAllFavorites() async {
return _box.values
.whereType<FavoriteModel>() // Type-safe!
.where((fav) => fav.userId == userId)
.toList();
}
```
**❌ INCORRECT - Will cause HiveError**
```dart
Box<FavoriteModel> get _box {
return Hive.box<FavoriteModel>(HiveBoxNames.favoriteBox);
}
```
**Reason**: Hive boxes are opened as `Box<dynamic>` in the central HiveService. Re-opening with a specific type causes `HiveError: The box is already open and of type Box<dynamic>`.
### AppBar Standardization
**Standard AppBar Pattern** (reference: `products_page.dart`):
```dart
AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.black),
onPressed: () => context.pop(),
),
title: const Text('Page Title', style: TextStyle(color: Colors.black)),
elevation: AppBarSpecs.elevation,
backgroundColor: AppColors.white,
foregroundColor: AppColors.grey900,
centerTitle: false,
actions: [
// Custom actions here
const SizedBox(width: AppSpacing.sm), // Always end with spacing
],
)
```
**For SliverAppBar** (in CustomScrollView):
```dart
SliverAppBar(
pinned: true,
backgroundColor: AppColors.white,
foregroundColor: AppColors.grey900,
elevation: AppBarSpecs.elevation,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.black),
onPressed: () => context.pop(),
),
title: const Text('Page Title', style: TextStyle(color: Colors.black)),
centerTitle: false,
actions: [
// Custom actions
const SizedBox(width: AppSpacing.sm),
],
)
```
**Standard Pattern (Recent Implementation)**:
```dart
AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.black),
onPressed: () => context.pop(),
),
title: const Text('Title', style: TextStyle(color: Colors.black)),
elevation: AppBarSpecs.elevation,
backgroundColor: AppColors.white,
foregroundColor: AppColors.grey900,
centerTitle: false,
actions: [..., const SizedBox(width: AppSpacing.sm)],
)
```
---
## UI/UX Components
### Color Palette
```dart
// colors.dart
class AppColors {
// Primary
static const primaryBlue = Color(0xFF005B9A);
static const lightBlue = Color(0xFF38B6FF);
static const accentCyan = Color(0xFF35C6F4);
// Status
static const success = Color(0xFF28a745);
static const warning = Color(0xFFffc107);
static const danger = Color(0xFFdc3545);
static const info = Color(0xFF17a2b8);
// Neutrals
static const grey50 = Color(0xFFf8f9fa);
static const grey100 = Color(0xFFe9ecef);
static const grey500 = Color(0xFF6c757d);
static const grey900 = Color(0xFF343a40);
// Tier Gradients
static const diamondGradient = LinearGradient(
colors: [Color(0xFF4A00E0), Color(0xFF8E2DE2)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
static const platinumGradient = LinearGradient(
colors: [Color(0xFF7F8C8D), Color(0xFFBDC3C7)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
static const goldGradient = LinearGradient(
colors: [Color(0xFFf7b733), Color(0xFFfc4a1a)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
}
```
### Typography
```dart
// typography.dart
class AppTypography {
static const fontFamily = 'Roboto';
static const displayLarge = TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
fontFamily: fontFamily,
);
static const headlineLarge = TextStyle(
fontSize: 24,
fontWeight: FontWeight.w600,
fontFamily: fontFamily,
);
static const titleLarge = TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
fontFamily: fontFamily,
);
static const bodyLarge = TextStyle(
fontSize: 16,
fontWeight: FontWeight.normal,
fontFamily: fontFamily,
);
static const labelSmall = TextStyle(
fontSize: 12,
fontWeight: FontWeight.normal,
fontFamily: fontFamily,
);
}
```
### Member Card Design
```dart
class MemberCardSpecs {
static const double width = double.infinity;
static const double height = 200;
static const double borderRadius = 16;
static const double elevation = 8;
static const EdgeInsets padding = EdgeInsets.all(20);
// QR Code
static const double qrSize = 80;
static const double qrBackgroundSize = 90;
// Points Display
static const double pointsFontSize = 28;
static const FontWeight pointsFontWeight = FontWeight.bold;
}
```
### Status Badges
```dart
class StatusBadge extends StatelessWidget {
final String status;
final Color color;
static Color getColorForStatus(OrderStatus status) {
switch (status) {
case OrderStatus.pending:
return AppColors.info;
case OrderStatus.processing:
return AppColors.warning;
case OrderStatus.shipping:
return AppColors.lightBlue;
case OrderStatus.completed:
return AppColors.success;
case OrderStatus.cancelled:
return AppColors.danger;
}
}
}
```
### Bottom Navigation
```dart
class BottomNavSpecs {
static const double height = 72;
static const double iconSize = 24;
static const double selectedIconSize = 28;
static const double labelFontSize = 12;
static const Color selectedColor = AppColors.primaryBlue;
static const Color unselectedColor = AppColors.grey500;
}
```
### Floating Action Button
```dart
class FABSpecs {
static const double size = 56;
static const double elevation = 6;
static const Color backgroundColor = AppColors.accentCyan;
static const Color iconColor = Colors.white;
static const double iconSize = 24;
static const Offset position = Offset(16, 16); // from bottom-right
}
```
### AppBar Specifications
```dart
class AppBarSpecs {
// From ui_constants.dart
static const double elevation = 0.5;
// Standard pattern for all pages
static AppBar standard({
required String title,
required VoidCallback onBack,
List<Widget>? actions,
}) {
return AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.black),
onPressed: onBack,
),
title: Text(title, style: const TextStyle(color: Colors.black)),
elevation: elevation,
backgroundColor: AppColors.white,
foregroundColor: AppColors.grey900,
centerTitle: false,
actions: [
...?actions,
const SizedBox(width: AppSpacing.sm),
],
);
}
}
```
---
## State Management
### Authentication Providers
```dart
final authProvider = AsyncNotifierProvider<AuthNotifier, AuthState>
final otpTimerProvider = StateNotifierProvider<OTPTimerNotifier, int>
```
### Home Providers
```dart
final memberCardProvider = Provider<MemberCard>((ref) {
final user = ref.watch(authProvider).user;
return MemberCard(
tier: user.memberTier,
name: user.name,
memberId: user.id,
points: user.points,
qrCode: generateQRCode(user.id),
);
});
```
### Loyalty Providers
```dart
final loyaltyPointsProvider = AsyncNotifierProvider<LoyaltyPointsNotifier, LoyaltyPoints>
```
**Rewards Page Providers**:
```dart
// Providers in lib/features/loyalty/presentation/providers/
@riverpod
class LoyaltyPoints extends _$LoyaltyPoints {
// Manages 9,750 available points, 1,200 expiring
}
@riverpod
class Gifts extends _$Gifts {
// 6 mock gifts matching HTML design
}
@riverpod
List<GiftCatalog> filteredGifts(ref) {
// Filters by selected category
}
final selectedGiftCategoryProvider = StateNotifierProvider...
final hasEnoughPointsProvider = Provider.family<bool, int>...
```
### Referral Provider
```dart
final referralProvider = AsyncNotifierProvider<ReferralNotifier, Referral>
```
### Products Providers
```dart
final productsProvider = AsyncNotifierProvider<ProductsNotifier, List<Product>>
final productSearchProvider = StateProvider<String>
final selectedCategoryProvider = StateProvider<String?>
```
### Cart Providers
```dart
final cartProvider = NotifierProvider<CartNotifier, List<CartItem>>
final cartTotalProvider = Provider<double>
```
**Dynamic Cart Badge**:
```dart
// Added provider in cart_provider.dart
@riverpod
int cartItemCount(CartItemCountRef ref) {
final cartState = ref.watch(cartProvider);
return cartState.items.fold(0, (sum, item) => sum + item.quantity);
}
// Used in home_page.dart and products_page.dart
final cartItemCount = ref.watch(cartItemCountProvider);
QuickAction(
badge: cartItemCount > 0 ? '$cartItemCount' : null,
)
```
### Orders Providers
```dart
final ordersProvider = AsyncNotifierProvider<OrdersNotifier, List<Order>>
final orderFilterProvider = StateProvider<OrderStatus?>
final paymentsProvider = AsyncNotifierProvider<PaymentsNotifier, List<Payment>>
```
### Projects Providers
```dart
final projectsProvider = AsyncNotifierProvider<ProjectsNotifier, List<Project>>
final projectFormProvider = StateNotifierProvider<ProjectFormNotifier, ProjectFormState>
```
### Chat Providers
```dart
final chatProvider = AsyncNotifierProvider<ChatNotifier, ChatRoom>
final messagesProvider = StreamProvider<List<Message>>
final typingIndicatorProvider = StateProvider<bool>
```
### Authentication State Implementation
```dart
@riverpod
class Auth extends _$Auth {
@override
Future<AuthState> build() async {
final token = await _getStoredToken();
if (token != null) {
final user = await _getUserFromToken(token);
return AuthState.authenticated(user);
}
return const AuthState.unauthenticated();
}
Future<void> loginWithPhone(String phone) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
await ref.read(authRepositoryProvider).requestOTP(phone);
return AuthState.otpSent(phone);
});
}
Future<void> verifyOTP(String phone, String otp) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
final response = await ref.read(authRepositoryProvider).verifyOTP(phone, otp);
await _storeToken(response.token);
return AuthState.authenticated(response.user);
});
}
}
```
---
## Performance Optimization
### Image Caching
```dart
// Use cached_network_image for all remote images
CachedNetworkImage(
imageUrl: product.images.first,
placeholder: (context, url) => const ShimmerPlaceholder(),
errorWidget: (context, url, error) => const Icon(Icons.error),
fit: BoxFit.cover,
memCacheWidth: 400, // Optimize memory usage
fadeInDuration: const Duration(milliseconds: 300),
)
```
### List Performance
```dart
// Use ListView.builder with RepaintBoundary for long lists
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return RepaintBoundary(
child: ProductCard(product: items[index]),
);
},
cacheExtent: 1000, // Pre-render items
)
// Use AutomaticKeepAliveClientMixin for expensive widgets
class ProductCard extends StatefulWidget {
@override
State<ProductCard> createState() => _ProductCardState();
}
class _ProductCardState extends State<ProductCard>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context);
return Card(...);
}
}
```
### State Optimization
```dart
// Use .select() to avoid unnecessary rebuilds
final userName = ref.watch(authProvider.select((state) => state.user?.name));
// Use family modifiers for parameterized providers
@riverpod
Future<Product> product(ProductRef ref, String id) async {
return await ref.read(productRepositoryProvider).getProduct(id);
}
// Keep providers outside build method
final productsProvider = ...;
class ProductsPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final products = ref.watch(productsProvider);
return ...;
}
}
```
---
## Offline Strategy
### Data Sync Flow
```dart
@riverpod
class DataSync extends _$DataSync {
@override
Future<SyncStatus> build() async {
// Listen to connectivity changes
ref.listen(connectivityProvider, (previous, next) {
if (next == ConnectivityStatus.connected) {
syncData();
}
});
return SyncStatus.idle;
}
Future<void> syncData() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
// Sync in order of dependency
await _syncUserData();
await _syncProducts();
await _syncOrders();
await _syncProjects();
await _syncLoyaltyData();
await ref.read(settingsRepositoryProvider).updateLastSyncTime();
return SyncStatus.success;
});
}
Future<void> _syncUserData() async {
final user = await ref.read(authRepositoryProvider).getCurrentUser();
await ref.read(authLocalDataSourceProvider).saveUser(user);
}
Future<void> _syncProducts() async {
final products = await ref.read(productRepositoryProvider).getAllProducts();
await ref.read(productLocalDataSourceProvider).saveProducts(products);
}
// ... other sync methods
}
```
### Offline Queue
```dart
// Queue failed requests for retry when online
class OfflineQueue {
final HiveInterface hive;
late Box<Map> _queueBox;
Future<void> init() async {
_queueBox = await hive.openBox('offline_queue');
}
Future<void> addToQueue(ApiRequest request) async {
await _queueBox.add({
'endpoint': request.endpoint,
'method': request.method,
'body': request.body,
'timestamp': DateTime.now().toIso8601String(),
});
}
Future<void> processQueue() async {
final requests = _queueBox.values.toList();
for (var i = 0; i < requests.length; i++) {
try {
await _executeRequest(requests[i]);
await _queueBox.deleteAt(i);
} catch (e) {
// Keep in queue for next retry
}
}
}
}
```
---
## Localization
### Setup
```dart
// l10n.yaml
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
// lib/l10n/app_vi.arb (Vietnamese)
{
"@@locale": "vi",
"appTitle": "Worker App",
"login": "Đăng nhập",
"phone": "Số điện thoại",
"enterPhone": "Nhập số điện thoại",
"continue": "Tiếp tục",
"verifyOTP": "Xác thực OTP",
"enterOTP": "Nhập mã OTP 6 số",
"resendOTP": "Gửi lại mã",
"home": "Trang chủ",
"products": "Sản phẩm",
"loyalty": "Hội viên",
"account": "Tài khoản",
"points": "Điểm",
"cart": "Giỏ hàng",
"checkout": "Thanh toán",
"orders": "Đơn hàng",
"projects": "Công trình",
"quotes": "Báo giá",
"myGifts": "Quà của tôi",
"referral": "Giới thiệu bạn bè",
"pointsHistory": "Lịch sử điểm"
}
// lib/l10n/app_en.arb (English)
{
"@@locale": "en",
"appTitle": "Worker App",
"login": "Login",
"phone": "Phone Number",
"enterPhone": "Enter phone number",
"continue": "Continue",
...
}
```
### Usage
```dart
class LoginPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: Text(l10n.login),
),
body: Column(
children: [
TextField(
decoration: InputDecoration(
labelText: l10n.phone,
hintText: l10n.enterPhone,
),
),
ElevatedButton(
onPressed: () {},
child: Text(l10n.continue),
),
],
),
);
}
}
```
---
## Deployment
### Android
```gradle
// android/app/build.gradle
android {
compileSdkVersion 34
defaultConfig {
applicationId "com.eurotile.worker"
minSdkVersion 21
targetSdkVersion 34
versionCode 1
versionName "1.0.0"
}
signingConfigs {
release {
storeFile file(RELEASE_STORE_FILE)
storePassword RELEASE_STORE_PASSWORD
keyAlias RELEASE_KEY_ALIAS
keyPassword RELEASE_KEY_PASSWORD
}
}
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
```
### iOS
```ruby
# ios/Podfile
platform :ios, '13.0'
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0'
end
end
end
```
---
## Quick Reference
### Key Requirements for All Code
- ✅ Black back arrow with explicit color
- ✅ Black text title with TextStyle
- ✅ Left-aligned title (`centerTitle: false`)
- ✅ White background (`AppColors.white`)
- ✅ Use `AppBarSpecs.elevation` (not hardcoded values)
- ✅ Always add `SizedBox(width: AppSpacing.sm)` after actions
- ✅ For SliverAppBar, add `pinned: true` property
- ✅ Use `Box<dynamic>` for Hive boxes with `.whereType<T>()` filtering
- ✅ Clean architecture (data/domain/presentation)
- ✅ Riverpod state management
- ✅ Hive for local persistence
- ✅ Material 3 design system
- ✅ Vietnamese localization
- ✅ CachedNetworkImage for all remote images
- ✅ Proper error handling
- ✅ Loading states (CircularProgressIndicator)
- ✅ Empty states with helpful messages