Files
worker/docs/md/CODE_EXAMPLES.md
Phuoc Nguyen 65f6f825a6 update md
2025-11-28 15:16:40 +07:00

18 KiB

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

Hive Box Type Management

CORRECT - Use Box with type filtering

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

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):

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):

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):

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

// 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

// 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

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

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

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

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

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

final authProvider = AsyncNotifierProvider<AuthNotifier, AuthState>
final otpTimerProvider = StateNotifierProvider<OTPTimerNotifier, int>

Home Providers

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

final loyaltyPointsProvider = AsyncNotifierProvider<LoyaltyPointsNotifier, LoyaltyPoints>

Rewards Page Providers:

// 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

final referralProvider = AsyncNotifierProvider<ReferralNotifier, Referral>

Products Providers

final productsProvider = AsyncNotifierProvider<ProductsNotifier, List<Product>>
final productSearchProvider = StateProvider<String>
final selectedCategoryProvider = StateProvider<String?>

Cart Providers

final cartProvider = NotifierProvider<CartNotifier, List<CartItem>>
final cartTotalProvider = Provider<double>

Dynamic Cart Badge:

// 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

final ordersProvider = AsyncNotifierProvider<OrdersNotifier, List<Order>>
final orderFilterProvider = StateProvider<OrderStatus?>
final paymentsProvider = AsyncNotifierProvider<PaymentsNotifier, List<Payment>>

Projects Providers

final projectsProvider = AsyncNotifierProvider<ProjectsNotifier, List<Project>>
final projectFormProvider = StateNotifierProvider<ProjectFormNotifier, ProjectFormState>

Chat Providers

final chatProvider = AsyncNotifierProvider<ChatNotifier, ChatRoom>
final messagesProvider = StreamProvider<List<Message>>
final typingIndicatorProvider = StateProvider<bool>

Authentication State Implementation

@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

// 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

// 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

// 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

@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

// 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

// 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

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

// 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

# 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