|
# CursorRules for Flutter Developers |
|
|
|
## Overview |
|
|
|
This document provides a comprehensive set of cursor rules (development practices) for cr eating high-quality Flutter applications. These rules emphasize clean code, maintainability, and performance while leveraging modern Flutter and Dart features. The principles outlined here can be applied across projects to ensure consistency and quality. |
|
|
|
## Core Principles |
|
|
|
All cursor rules are based on these four fundamental principles: |
|
|
|
1. **Readability**: Code must be easily understood by other developers. |
|
2. **Predictability**: Functions, widgets, and components should behave as expected. |
|
3. **Cohesion**: Related code should be kept together with a clear purpose. |
|
4. **Coupling**: Minimize dependencies between different parts of the codebase. |
|
|
|
## Project Structure |
|
|
|
``` |
|
lib/ |
|
├── core/ # Core utilities and framework |
|
│ ├── config/ # App configuration, constants |
|
│ ├── di/ # Dependency injection |
|
│ ├── network/ # Network utilities |
|
│ ├── storage/ # Storage utilities |
|
│ ├── theme/ # App theme |
|
│ └── utils/ # Utilities and helpers |
|
│ |
|
├── features/ # Feature modules |
|
│ ├── auth/ # Authentication feature |
|
│ │ ├── data/ # Data layer (repositories, models) |
|
│ │ │ ├── datasources/ |
|
│ │ │ ├── models/ |
|
│ │ │ └── repositories/ |
|
│ │ ├── domain/ # Domain layer (entities, usecases) |
|
│ │ │ ├── entities/ |
|
│ │ │ └── usecases/ |
|
│ │ ├── presentation/ # UI components |
|
│ │ │ ├── screens/ |
|
│ │ │ └── widgets/ |
|
│ │ └── providers/ # State management |
|
│ │ |
|
│ └── feature_name/ # Other features follow the same structure |
|
│ |
|
└── main.dart # Entry point |
|
``` |
|
|
|
### Cursor Rules for Project Structure: |
|
|
|
- **CR-PS-01**: Group code by feature, not by type. |
|
- **CR-PS-02**: Core module should only contain framework code and shared utilities. |
|
- **CR-PS-03**: Feature modules should be self-contained with minimal dependencies. |
|
- **CR-PS-04**: Place related files close to each other to minimize navigation. |
|
- **CR-PS-05**: Use clean architecture principles within each feature. |
|
|
|
## Readability |
|
|
|
### Named Constants |
|
|
|
**CR-R-01**: Replace magic numbers and string literals with named constants to improve clarity. |
|
|
|
```dart |
|
// ❌ Poor practice |
|
Future<void> refreshData() async { |
|
await Future.delayed(const Duration(milliseconds: 300)); |
|
await fetchItems(); |
|
} |
|
|
|
// ✅ Good practice |
|
const Duration kRefreshDebounce = Duration(milliseconds: 300); |
|
|
|
Future<void> refreshData() async { |
|
await Future.delayed(kRefreshDebounce); |
|
await fetchItems(); |
|
} |
|
``` |
|
|
|
### Widget Composition |
|
|
|
**CR-R-02**: Break complex widgets into smaller, focused components. |
|
|
|
```dart |
|
// ❌ Poor practice: One large widget with multiple responsibilities |
|
class ProductDetailScreen extends StatelessWidget { |
|
@override |
|
Widget build(BuildContext context) { |
|
return Scaffold( |
|
appBar: AppBar(title: Text('Product')), |
|
body: ListView( |
|
children: [ |
|
// 50+ lines of image gallery... |
|
// 40+ lines of product details... |
|
// 60+ lines of reviews section... |
|
// 40+ lines of related products... |
|
], |
|
), |
|
); |
|
} |
|
} |
|
|
|
// ✅ Good practice: Small, focused widgets |
|
class ProductDetailScreen extends StatelessWidget { |
|
@override |
|
Widget build(BuildContext context) { |
|
return Scaffold( |
|
appBar: AppBar(title: Text('Product')), |
|
body: ListView( |
|
children: [ |
|
ProductImageGallery(images: product.images), |
|
ProductDetailCard(product: product), |
|
ProductReviewsSection(productId: product.id), |
|
RelatedProductsList(category: product.category), |
|
], |
|
), |
|
); |
|
} |
|
} |
|
``` |
|
|
|
### Specialized Widgets for Different States |
|
|
|
**CR-R-03**: Create separate widgets for UI states instead of complex conditionals. |
|
|
|
```dart |
|
// ❌ Poor practice: Complex conditionals in a single widget |
|
class OrderStatus extends StatelessWidget { |
|
final Order order; |
|
|
|
@override |
|
Widget build(BuildContext context) { |
|
if (order.status == OrderStatus.processing) { |
|
return Row(children: [ |
|
CircularProgressIndicator(), |
|
Text('Processing order...'), |
|
]); |
|
} else if (order.status == OrderStatus.shipped) { |
|
return Row(children: [ |
|
Icon(Icons.local_shipping), |
|
Text('Shipped on ${order.shippedDate}'), |
|
]); |
|
} else if (order.status == OrderStatus.delivered) { |
|
return Row(children: [ |
|
Icon(Icons.check_circle), |
|
Text('Delivered on ${order.deliveryDate}'), |
|
]); |
|
} else { |
|
return Text('Unknown status'); |
|
} |
|
} |
|
} |
|
|
|
// ✅ Good practice: Delegate to specialized widgets |
|
class OrderStatus extends StatelessWidget { |
|
final Order order; |
|
|
|
@override |
|
Widget build(BuildContext context) { |
|
return switch (order.status) { |
|
OrderStatus.processing => ProcessingOrderStatus(order: order), |
|
OrderStatus.shipped => ShippedOrderStatus(order: order), |
|
OrderStatus.delivered => DeliveredOrderStatus(order: order), |
|
_ => UnknownOrderStatus(), |
|
}; |
|
} |
|
} |
|
``` |
|
|
|
### Name Complex Conditions |
|
|
|
**CR-R-04**: Give meaningful names to complex boolean conditions. |
|
|
|
```dart |
|
// ❌ Poor practice |
|
if (user.subscription != null && |
|
user.subscription!.isActive && |
|
DateTime.now().isBefore(user.subscription!.expiryDate) && |
|
user.subscription!.plan == SubscriptionPlan.premium) { |
|
showPremiumFeatures(); |
|
} |
|
|
|
// ✅ Good practice |
|
bool get isPremiumActive { |
|
if (user.subscription == null) return false; |
|
|
|
final isActive = user.subscription!.isActive; |
|
final isNotExpired = DateTime.now().isBefore(user.subscription!.expiryDate); |
|
final isPremiumPlan = user.subscription!.plan == SubscriptionPlan.premium; |
|
|
|
return isActive && isNotExpired && isPremiumPlan; |
|
} |
|
|
|
// Usage |
|
if (isPremiumActive) { |
|
showPremiumFeatures(); |
|
} |
|
``` |
|
|
|
## Predictability |
|
|
|
### Consistent Return Types |
|
|
|
**CR-P-01**: Use consistent return types for similar functions and methods. |
|
|
|
```dart |
|
// ❌ Poor practice: Inconsistent return types |
|
Future<User?> getUser() async { |
|
final response = await api.getUser(); |
|
return response.isSuccess ? User.fromJson(response.data) : null; |
|
} |
|
|
|
Future<List<Product>> getProducts() async { |
|
final response = await api.getProducts(); |
|
if (!response.isSuccess) throw Exception('Failed to load products'); |
|
return response.data.map((json) => Product.fromJson(json)).toList(); |
|
} |
|
|
|
// ✅ Good practice: Consistent return types using Result pattern |
|
Future<Result<User>> getUser() async { |
|
try { |
|
final response = await api.getUser(); |
|
if (!response.isSuccess) { |
|
return Result.failure(ApiError(response.errorMessage)); |
|
} |
|
return Result.success(User.fromJson(response.data)); |
|
} catch (e) { |
|
return Result.failure(UnexpectedError(e.toString())); |
|
} |
|
} |
|
|
|
Future<Result<List<Product>>> getProducts() async { |
|
try { |
|
final response = await api.getProducts(); |
|
if (!response.isSuccess) { |
|
return Result.failure(ApiError(response.errorMessage)); |
|
} |
|
final products = response.data |
|
.map((json) => Product.fromJson(json)) |
|
.toList(); |
|
return Result.success(products); |
|
} catch (e) { |
|
return Result.failure(UnexpectedError(e.toString())); |
|
} |
|
} |
|
``` |
|
|
|
### Clear Function Naming |
|
|
|
**CR-P-02**: Functions should have names that clearly indicate their purpose and behavior. |
|
|
|
```dart |
|
// ❌ Poor practice: Ambiguous names |
|
Future<void> process() async { /* ... */ } |
|
void handle(User user) { /* ... */ } |
|
|
|
// ✅ Good practice: Clear, specific names |
|
Future<void> processPayment(Order order) async { /* ... */ } |
|
void handleUserRegistration(User user) { /* ... */ } |
|
``` |
|
|
|
### Single Responsibility |
|
|
|
**CR-P-03**: Functions should have a single responsibility and avoid hidden side effects. |
|
|
|
```dart |
|
// ❌ Poor practice: Function with hidden side effects |
|
Future<User> getUser() async { |
|
final user = await api.fetchUser(); |
|
analytics.logEvent('user_fetched'); // Hidden side effect |
|
cache.save('user', user); // Hidden side effect |
|
return user; |
|
} |
|
|
|
// ✅ Good practice: Explicit functions for each responsibility |
|
Future<User> fetchUser() async { |
|
return await api.fetchUser(); |
|
} |
|
|
|
Future<void> logUserFetch() async { |
|
analytics.logEvent('user_fetched'); |
|
} |
|
|
|
Future<void> cacheUser(User user) async { |
|
await cache.save('user', user); |
|
} |
|
|
|
// When using, all operations are explicit |
|
Future<User> getUserWithLoggingAndCaching() async { |
|
final user = await fetchUser(); |
|
await Future.wait([ |
|
logUserFetch(), |
|
cacheUser(user), |
|
]); |
|
return user; |
|
} |
|
``` |
|
|
|
## Cohesion |
|
|
|
### Form Validation Approaches |
|
|
|
**CR-C-01**: Choose the appropriate form validation approach based on form complexity. |
|
|
|
Field-level validation (for simple forms with independent fields): |
|
|
|
```dart |
|
class SimpleForm extends StatefulWidget { |
|
@override |
|
_SimpleFormState createState() => _SimpleFormState(); |
|
} |
|
|
|
class _SimpleFormState extends State<SimpleForm> { |
|
final _formKey = GlobalKey<FormState>(); |
|
|
|
@override |
|
Widget build(BuildContext context) { |
|
return Form( |
|
key: _formKey, |
|
child: Column( |
|
children: [ |
|
TextFormField( |
|
decoration: InputDecoration(labelText: 'Email'), |
|
validator: (value) { |
|
if (value == null || value.isEmpty) { |
|
return 'Please enter an email'; |
|
} |
|
if (!value.contains('@')) { |
|
return 'Please enter a valid email'; |
|
} |
|
return null; |
|
}, |
|
), |
|
ElevatedButton( |
|
onPressed: () { |
|
if (_formKey.currentState!.validate()) { |
|
// Submit form |
|
} |
|
}, |
|
child: Text('Submit'), |
|
), |
|
], |
|
), |
|
); |
|
} |
|
} |
|
``` |
|
|
|
Form-level validation (for complex forms with interdependent fields): |
|
|
|
```dart |
|
// Using a validation library like formz |
|
class PasswordInput extends FormzInput<String, PasswordValidationError> { |
|
const PasswordInput.pure() : super.pure(''); |
|
const PasswordInput.dirty([String value = '']) : super.dirty(value); |
|
|
|
@override |
|
PasswordValidationError? validator(String value) { |
|
if (value.isEmpty) return PasswordValidationError.empty; |
|
if (value.length < 8) return PasswordValidationError.tooShort; |
|
return null; |
|
} |
|
} |
|
|
|
class ConfirmPasswordInput extends FormzInput<String, ConfirmPasswordValidationError> { |
|
const ConfirmPasswordInput.pure({this.password = ''}) : super.pure(''); |
|
const ConfirmPasswordInput.dirty({ |
|
required this.password, |
|
String value = '', |
|
}) : super.dirty(value); |
|
|
|
final String password; |
|
|
|
@override |
|
ConfirmPasswordValidationError? validator(String value) { |
|
if (value.isEmpty) return ConfirmPasswordValidationError.empty; |
|
if (value != password) return ConfirmPasswordValidationError.mismatch; |
|
return null; |
|
} |
|
} |
|
``` |
|
|
|
### Keep Constants with Related Logic |
|
|
|
**CR-C-02**: Define constants close to the related logic. |
|
|
|
```dart |
|
// ❌ Poor practice: Constants defined far from related logic |
|
class Constants { |
|
static const Duration animationDuration = Duration(milliseconds: 300); |
|
static const Duration tooltipDelay = Duration(milliseconds: 500); |
|
static const Duration refreshInterval = Duration(minutes: 5); |
|
} |
|
|
|
// Much later in codebase... |
|
class AnimatedButton extends StatelessWidget { |
|
@override |
|
Widget build(BuildContext context) { |
|
return AnimatedContainer( |
|
duration: Constants.animationDuration, // Hard to trace relationship |
|
// ... |
|
); |
|
} |
|
} |
|
|
|
// ✅ Good practice: Constants defined near related logic |
|
class AnimatedButton extends StatelessWidget { |
|
// Duration clearly associated with this animation |
|
static const Duration _animationDuration = Duration(milliseconds: 300); |
|
|
|
@override |
|
Widget build(BuildContext context) { |
|
return AnimatedContainer( |
|
duration: _animationDuration, |
|
// ... |
|
); |
|
} |
|
} |
|
``` |
|
|
|
## State Management |
|
|
|
**CR-SM-01**: Choose a consistent state management solution based on project complexity and team familiarity. |
|
|
|
### BLoC Pattern |
|
|
|
```dart |
|
// Events |
|
abstract class CounterEvent {} |
|
class IncrementEvent extends CounterEvent {} |
|
class DecrementEvent extends CounterEvent {} |
|
|
|
// State |
|
class CounterState { |
|
final int count; |
|
const CounterState(this.count); |
|
} |
|
|
|
// BLoC |
|
class CounterBloc extends Bloc<CounterEvent, CounterState> { |
|
CounterBloc() : super(const CounterState(0)) { |
|
on<IncrementEvent>((event, emit) { |
|
emit(CounterState(state.count + 1)); |
|
}); |
|
|
|
on<DecrementEvent>((event, emit) { |
|
emit(CounterState(state.count - 1)); |
|
}); |
|
} |
|
} |
|
|
|
// Usage in UI |
|
class CounterPage extends StatelessWidget { |
|
@override |
|
Widget build(BuildContext context) { |
|
return BlocProvider( |
|
create: (_) => CounterBloc(), |
|
child: CounterView(), |
|
); |
|
} |
|
} |
|
``` |
|
|
|
### Riverpod |
|
|
|
```dart |
|
// State class with freezed |
|
@freezed |
|
class CounterState with _$CounterState { |
|
const factory CounterState({@Default(0) int count}) = _CounterState; |
|
} |
|
|
|
// Notifier with code generation |
|
@riverpod |
|
class CounterNotifier extends _$CounterNotifier { |
|
@override |
|
CounterState build() { |
|
return const CounterState(); |
|
} |
|
|
|
void increment() { |
|
state = state.copyWith(count: state.count + 1); |
|
} |
|
|
|
void decrement() { |
|
state = state.copyWith(count: state.count - 1); |
|
} |
|
} |
|
|
|
// Usage in UI |
|
class CounterPage extends ConsumerWidget { |
|
@override |
|
Widget build(BuildContext context, WidgetRef ref) { |
|
final counter = ref.watch(counterNotifierProvider); |
|
|
|
return Scaffold( |
|
appBar: AppBar(title: const Text('Counter')), |
|
body: Center( |
|
child: Text( |
|
'${counter.count}', |
|
style: Theme.of(context).textTheme.displayLarge, |
|
), |
|
), |
|
); |
|
} |
|
} |
|
``` |
|
|
|
### Focused State Management |
|
|
|
**CR-SM-02**: Break state into small, focused units to reduce coupling. |
|
|
|
```dart |
|
// ❌ Poor practice: Monolithic state management |
|
@riverpod |
|
class SuperAppStateNotifier extends _$SuperAppStateNotifier { |
|
@override |
|
SuperAppState build() { |
|
return SuperAppState(); |
|
} |
|
|
|
// User profile methods |
|
void updateUserName(String name) { /* ... */ } |
|
void updateUserAvatar(String url) { /* ... */ } |
|
|
|
// Cart methods |
|
void addToCart(Product product) { /* ... */ } |
|
void removeFromCart(String productId) { /* ... */ } |
|
void updateQuantity(String productId, int qty) { /* ... */ } |
|
|
|
// Payment methods |
|
void updatePaymentMethod(PaymentMethod method) { /* ... */ } |
|
void applyPromoCode(String code) { /* ... */ } |
|
|
|
// Order methods |
|
Future<void> placeOrder() async { /* ... */ } |
|
Future<void> cancelOrder(String orderId) async { /* ... */ } |
|
} |
|
|
|
// ✅ Good practice: Split state into focused units |
|
@riverpod |
|
class UserProfileNotifier extends _$UserProfileNotifier { |
|
@override |
|
UserProfileState build() { |
|
return const UserProfileState(); |
|
} |
|
|
|
void updateName(String name) { |
|
state = state.copyWith(name: name); |
|
} |
|
|
|
void updateAvatar(String url) { |
|
state = state.copyWith(avatarUrl: url); |
|
} |
|
} |
|
|
|
@riverpod |
|
class CartNotifier extends _$CartNotifier { |
|
@override |
|
CartState build() { |
|
return const CartState(); |
|
} |
|
|
|
void addItem(Product product) { /* ... */ } |
|
void removeItem(String productId) { /* ... */ } |
|
void updateQuantity(String productId, int quantity) { /* ... */ } |
|
} |
|
``` |
|
|
|
## Performance Optimization |
|
|
|
### Optimized ListView |
|
|
|
**CR-PO-01**: Use efficient list rendering methods. |
|
|
|
```dart |
|
// ❌ Poor practice: Inefficient list rendering |
|
ListView( |
|
children: products.map((product) => ProductListItem(product: product)).toList(), |
|
); |
|
|
|
// ✅ Good practice: Efficient list rendering |
|
ListView.builder( |
|
itemCount: products.length, |
|
itemBuilder: (context, index) => ProductListItem(product: products[index]), |
|
); |
|
``` |
|
|
|
### Image Optimization |
|
|
|
**CR-PO-02**: Optimize image loading and caching. |
|
|
|
```dart |
|
// ❌ Poor practice: Unoptimized image loading |
|
Image.network(product.imageUrl) |
|
|
|
// ✅ Good practice: Optimized image loading |
|
CachedNetworkImage( |
|
imageUrl: product.imageUrl, |
|
placeholder: (context, url) => const Center( |
|
child: CircularProgressIndicator(), |
|
), |
|
errorWidget: (context, url, error) => const Icon(Icons.error), |
|
fit: BoxFit.cover, |
|
memCacheWidth: 400, // Size adjustment for memory efficiency |
|
) |
|
``` |
|
|
|
### Computation Optimization |
|
|
|
**CR-PO-03**: Memoize expensive computations. |
|
|
|
```dart |
|
// ❌ Poor practice: Expensive computation in build method |
|
@override |
|
Widget build(BuildContext context) { |
|
final sortedProducts = products |
|
.where((p) => p.category == selectedCategory) |
|
.toList() |
|
..sort((a, b) => a.price.compareTo(b.price)); |
|
|
|
return ListView.builder( |
|
itemCount: sortedProducts.length, |
|
itemBuilder: (context, index) { |
|
return ProductItem(product: sortedProducts[index]); |
|
}, |
|
); |
|
} |
|
|
|
// ✅ Good practice: Memoized computation |
|
final filteredProductsProvider = Provider<List<Product>>((ref) { |
|
final products = ref.watch(productsProvider); |
|
final selectedCategory = ref.watch(selectedCategoryProvider); |
|
|
|
return products |
|
.where((p) => p.category == selectedCategory) |
|
.toList() |
|
..sort((a, b) => a.price.compareTo(b.price)); |
|
}); |
|
|
|
@override |
|
Widget build(BuildContext context, WidgetRef ref) { |
|
final sortedProducts = ref.watch(filteredProductsProvider); |
|
|
|
return ListView.builder( |
|
itemCount: sortedProducts.length, |
|
itemBuilder: (context, index) { |
|
return ProductItem(product: sortedProducts[index]); |
|
}, |
|
); |
|
} |
|
``` |
|
|
|
## Error & Loading State Handling |
|
|
|
**CR-EL-01**: Properly handle loading, error, and empty states for asynchronous operations. |
|
|
|
```dart |
|
// Riverpod example |
|
class ProductListView extends ConsumerWidget { |
|
@override |
|
Widget build(BuildContext context, WidgetRef ref) { |
|
final productsAsync = ref.watch(productsProvider); |
|
|
|
return productsAsync.when( |
|
loading: () => const Center(child: CircularProgressIndicator()), |
|
error: (error, stack) => ErrorView( |
|
message: 'Failed to load products', |
|
onRetry: () => ref.refresh(productsProvider), |
|
), |
|
data: (products) { |
|
if (products.isEmpty) { |
|
return const EmptyStateView( |
|
message: 'No products available', |
|
icon: Icons.inventory_2_outlined, |
|
); |
|
} |
|
|
|
return ListView.builder( |
|
itemCount: products.length, |
|
itemBuilder: (context, index) => ProductListItem( |
|
product: products[index], |
|
), |
|
); |
|
}, |
|
); |
|
} |
|
} |
|
``` |
|
|
|
## Modern Dart Features |
|
|
|
**CR-DF-01**: Leverage modern Dart features for cleaner, more expressive code. |
|
|
|
```dart |
|
// Pattern matching with switch expressions |
|
String getStatusMessage(OrderStatus status) => switch(status) { |
|
OrderStatus.pending => 'Order is pending', |
|
OrderStatus.processing => 'Order is being processed', |
|
OrderStatus.shipped => 'Order has been shipped', |
|
OrderStatus.delivered => 'Order has been delivered', |
|
OrderStatus.cancelled => 'Order has been cancelled', |
|
}; |
|
|
|
// Records for related return values |
|
(int count, double total) getCartSummary() { |
|
final items = _getCartItems(); |
|
return ( |
|
items.length, |
|
items.fold(0.0, (sum, item) => sum + item.price * item.quantity), |
|
); |
|
} |
|
|
|
// Extension types for type safety |
|
extension type EmailAddress(String value) { |
|
bool get isValid => |
|
value.isNotEmpty && value.contains('@') && value.contains('.'); |
|
|
|
String get domain => value.split('@').last; |
|
} |
|
|
|
// Enhanced enums |
|
enum PaymentMethod { |
|
creditCard(icon: Icons.credit_card, label: 'Credit Card'), |
|
paypal(icon: Icons.payments, label: 'PayPal'), |
|
applePay(icon: Icons.apple, label: 'Apple Pay'), |
|
googlePay(icon: Icons.android, label: 'Google Pay'); |
|
|
|
final IconData icon; |
|
final String label; |
|
|
|
const PaymentMethod({required this.icon, required this.label}); |
|
|
|
bool get isDigitalWallet => |
|
this == PaymentMethod.applePay || this == PaymentMethod.googlePay; |
|
} |
|
``` |
|
|
|
## Testing |
|
|
|
**CR-T-01**: Implement a comprehensive testing pyramid with unit, widget, and integration tests. |
|
|
|
```dart |
|
// Unit tests |
|
test('Counter increments correctly', () { |
|
final counter = Counter(); |
|
counter.increment(); |
|
expect(counter.value, 1); |
|
}); |
|
|
|
// Widget tests |
|
testWidgets('Counter UI displays correct value', (tester) async { |
|
await tester.pumpWidget(MaterialApp(home: CounterWidget())); |
|
expect(find.text('0'), findsOneWidget); |
|
}); |
|
|
|
// Integration tests |
|
testWidgets('Full login flow', (tester) async { |
|
await tester.pumpWidget(MyApp()); |
|
await tester.enterText(find.byType(TextField).first, 'user@example.com'); |
|
await tester.enterText(find.byType(TextField).last, 'password'); |
|
await tester.tap(find.byType(ElevatedButton)); |
|
await tester.pumpAndSettle(); |
|
expect(find.text('Welcome'), findsOneWidget); |
|
}); |
|
``` |
|
|
|
## Resource Management |
|
|
|
### Asset Management |
|
|
|
**CR-RM-01**: Centralize asset paths. |
|
|
|
```dart |
|
// ❌ Poor practice: Hardcoded asset paths |
|
Image.asset('assets/images/logo.png') |
|
|
|
// ✅ Good practice: Centralized asset paths |
|
class AppAssets { |
|
static const String logo = 'assets/images/logo.png'; |
|
static const String onboarding1 = 'assets/images/onboarding/step1.png'; |
|
static const String onboarding2 = 'assets/images/onboarding/step2.png'; |
|
static const String placeholder = 'assets/images/placeholder.png'; |
|
} |
|
|
|
// Usage |
|
Image.asset(AppAssets.logo) |
|
``` |
|
|
|
### String Management |
|
|
|
**CR-RM-02**: Use a centralized approach for strings. |
|
|
|
```dart |
|
// ❌ Poor practice: Hardcoded strings |
|
Text('Welcome back!') |
|
|
|
// ✅ Good practice: Centralized strings |
|
class AppStrings { |
|
static const String welcomeBack = 'Welcome back!'; |
|
static const String signIn = 'Sign In'; |
|
static const String emailHint = 'Enter your email'; |
|
static const String passwordHint = 'Enter your password'; |
|
static const String forgotPassword = 'Forgot password?'; |
|
} |
|
|
|
// Usage |
|
Text(AppStrings.welcomeBack) |
|
``` |
|
|
|
## Internationalization |
|
|
|
**CR-I-01**: Design your app for internationalization from the start. |
|
|
|
```dart |
|
// Using the intl package with ARB files |
|
// lib/l10n/app_en.arb (English) |
|
{ |
|
"helloWorld": "Hello World!", |
|
"welcome": "Welcome {name}", |
|
"@welcome": { |
|
"placeholders": { |
|
"name": { |
|
"type": "String" |
|
} |
|
} |
|
}, |
|
"itemCount": "{count, plural, =0{No items} =1{1 item} other{{count} items}}", |
|
"@itemCount": { |
|
"placeholders": { |
|
"count": { |
|
"type": "int" |
|
} |
|
} |
|
} |
|
} |
|
|
|
// Usage in code |
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; |
|
|
|
class HomePage extends StatelessWidget { |
|
@override |
|
Widget build(BuildContext context) { |
|
final localizations = AppLocalizations.of(context)!; |
|
|
|
return Scaffold( |
|
appBar: AppBar( |
|
title: Text(localizations.helloWorld), |
|
), |
|
body: Center( |
|
child: Column( |
|
mainAxisAlignment: MainAxisAlignment.center, |
|
children: [ |
|
Text(localizations.welcome('John')), |
|
Text(localizations.itemCount(5)), |
|
], |
|
), |
|
), |
|
); |
|
} |
|
} |
|
``` |
|
|
|
## CI/CD |
|
|
|
**CR-CD-01**: Implement a CI/CD pipeline for your Flutter project. |
|
|
|
```yaml |
|
# GitHub Actions workflow example |
|
name: Flutter CI |
|
|
|
on: |
|
push: |
|
branches: [ main, develop ] |
|
pull_request: |
|
branches: [ main, develop ] |
|
|
|
jobs: |
|
build: |
|
runs-on: ubuntu-latest |
|
steps: |
|
- uses: actions/checkout@v3 |
|
|
|
- name: Setup Flutter |
|
uses: subosito/flutter-action@v2 |
|
with: |
|
flutter-version: '3.29.2' |
|
channel: 'stable' |
|
|
|
- name: Install dependencies |
|
run: flutter pub get |
|
|
|
- name: Check formatting |
|
run: dart format --set-exit-if-changed . |
|
|
|
- name: Analyze project source |
|
run: flutter analyze |
|
|
|
- name: Run tests |
|
run: flutter test --coverage |
|
``` |
|
|
|
## Environment Configuration |
|
|
|
**CR-EC-01**: Manage different environments (development, staging, production). |
|
|
|
```dart |
|
// config.dart |
|
enum Environment { dev, staging, prod } |
|
|
|
class Config { |
|
static Environment environment = Environment.dev; |
|
|
|
static String get baseUrl { |
|
switch (environment) { |
|
case Environment.dev: |
|
return 'https://dev-api.example.com'; |
|
case Environment.staging: |
|
return 'https://staging-api.example.com'; |
|
case Environment.prod: |
|
return 'https://api.example.com'; |
|
} |
|
} |
|
|
|
static bool get enableLogging => environment != Environment.prod; |
|
|
|
static Future<void> initialize(Environment env) async { |
|
environment = env; |
|
// Initialize services based on environment |
|
} |
|
} |
|
``` |
|
|
|
## Feature Flags |
|
|
|
**CR-FF-01**: Use feature flags for gradual feature rollout and A/B testing. |
|
|
|
```dart |
|
// Service class for feature flags |
|
class FeatureFlagService { |
|
final FirebaseRemoteConfig _remoteConfig; |
|
|
|
FeatureFlagService(this._remoteConfig); |
|
|
|
bool get isDarkModeEnabled => _remoteConfig.getBool('enable_dark_mode'); |
|
bool get isNewPaymentUIEnabled => _remoteConfig.getBool('new_payment_ui'); |
|
bool get areBetaFeaturesEnabled => _remoteConfig.getBool('beta_features'); |
|
String get minimumAppVersion => _remoteConfig.getString('minimum_app_version'); |
|
} |
|
|
|
// Usage |
|
class PaymentScreen extends StatelessWidget { |
|
@override |
|
Widget build(BuildContext context) { |
|
final featureFlags = ref.watch(featureFlagServiceProvider); |
|
|
|
return featureFlags.isNewPaymentUIEnabled |
|
? NewPaymentUI() |
|
: LegacyPaymentUI(); |
|
} |
|
} |
|
``` |
|
|
|
--- |
|
|
|
Following these cursor rules will help you create maintainable, performant, and high-quality Flutter applications. These principles establish a consistent approach across teams and projects, allowing for more efficient delivery of quality software. |