add price policy
This commit is contained in:
772
CODE_EXAMPLES.md
Normal file
772
CODE_EXAMPLES.md
Normal file
@@ -0,0 +1,772 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user