# Flutter Development Rules Essential rules for Flutter mobile app development. ## Code Style - Use `final` and `const` wherever possible - Follow Dart naming conventions - Use trailing commas for better auto-formatting - Keep widgets small and focused - Use meaningful variable names ```dart // ✅ Good class UserList extends StatelessWidget { const UserList({ super.key, required this.users, this.onUserTap, }); final List users; final VoidCallback(User)? onUserTap; @override Widget build(BuildContext context) { return ListView.builder( itemCount: users.length, itemBuilder: (context, index) { final user = users[index]; return UserTile( user: user, onTap: onUserTap, ); }, ); } } // ❌ Bad class UserList extends StatelessWidget { UserList(this.users, {this.onUserTap}); // Missing const final List users; final Function(User)? onUserTap; // Use VoidCallback instead @override Widget build(BuildContext context) { return ListView(children: users.map((u) => UserTile(u)).toList()); // No const } } ``` ## Widget Architecture - Prefer stateless widgets when possible - Split large widgets into smaller ones - Use composition over inheritance - Pass data through constructors - Keep build methods pure ```dart // ✅ Good: Split into small widgets class ProfileScreen extends StatelessWidget { const ProfileScreen({super.key, required this.user}); final User user; @override Widget build(BuildContext context) { return Scaffold( appBar: ProfileAppBar(user: user), body: ProfileBody(user: user), ); } } // ❌ Bad: Everything in one widget class ProfileScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Profile')), body: Column( children: [ // 100+ lines of nested widgets ], ), ); } } ``` ## State Management - Use Riverpod, Bloc, or Provider (project choice) - Keep state close to where it's used - Separate business logic from UI - Use immutable state classes ```dart // ✅ Good: Riverpod state management final userProvider = StateNotifierProvider((ref) { return UserNotifier(); }); class UserNotifier extends StateNotifier { UserNotifier() : super(const UserState.initial()); Future loadUser(String id) async { state = const UserState.loading(); try { final user = await _userRepository.getUser(id); state = UserState.loaded(user); } catch (e) { state = UserState.error(e.toString()); } } } // ✅ Good: Immutable state with freezed @freezed class UserState with _$UserState { const factory UserState.initial() = _Initial; const factory UserState.loading() = _Loading; const factory UserState.loaded(User user) = _Loaded; const factory UserState.error(String message) = _Error; } ``` ## Error Handling - Use Result/Either types for async operations - Never silently catch errors - Show user-friendly error messages - Log errors to monitoring service ```dart // ✅ Good Future loadData() async { state = const AsyncValue.loading(); state = await AsyncValue.guard(() async { final result = await _repository.fetchData(); if (result.isError) { throw ServerException(result.message); } return result.data; }); } // ❌ Bad Future loadData() async { try { final data = await _repository.fetchData(); state = data; } catch (e) { // Silently swallowing error } } ``` ## API & Network - Use dio for HTTP requests - Implement request interceptors - Handle connectivity changes - Cache responses when appropriate ```dart // ✅ Good class ApiClient { final Dio _dio; ApiClient(this._dio) { _dio.interceptors.addAll([ AuthInterceptor(), LoggingInterceptor(), RetryInterceptor(), ]); } Future get(String path, {Map? queryParameters}) async { try { return await _dio.get(path, queryParameters: queryParameters); } on DioException catch (e) { throw _handleError(e); } } } class AuthInterceptor extends Interceptor { @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { options.headers['Authorization'] = 'Bearer ${_getToken()}'; handler.next(options); } } ``` ## Navigation - Use go_router for declarative routing - Define routes as constants - Pass data through route parameters - Handle deep links ```dart // ✅ Good: go_router setup final router = GoRouter( routes: [ GoRoute( path: '/', builder: (context, state) => const HomeScreen(), ), GoRoute( path: '/user/:id', builder: (context, state) { final id = state.pathParameters['id']!; return UserDetailScreen(userId: id); }, ), GoRoute( path: '/settings', builder: (context, state) => const SettingsScreen(), ), ], errorBuilder: (context, state) => const ErrorScreen(), ); ``` ## Testing - Write unit tests for business logic - Write widget tests for UI components - Use mocks for dependencies - Test edge cases and error states ```dart // ✅ Good: Unit test void main() { group('UserNotifier', () { late UserNotifier notifier; late MockUserRepository mockRepository; setUp(() { mockRepository = MockUserRepository(); notifier = UserNotifier(mockRepository); }); test('loads user successfully', () async { // Arrange final user = User(id: '1', name: 'Test'); when(mockRepository.getUser('1')).thenAnswer((_) async => user); // Act await notifier.loadUser('1'); // Assert expect(notifier.state, equals(UserState.loaded(user))); }); test('handles error gracefully', () async { // Arrange when(mockRepository.getUser('1')).thenThrow(NetworkException()); // Act await notifier.loadUser('1'); // Assert expect(notifier.state, isA()); }); }); } // ✅ Good: Widget test void main() { testWidgets('UserTile displays user name', (tester) async { // Arrange final user = User(id: '1', name: 'John Doe'); // Act await tester.pumpWidget(MaterialApp( home: Scaffold( body: UserTile(user: user), ), )); // Assert expect(find.text('John Doe'), findsOneWidget); }); } ``` ## Performance - Use const constructors - Avoid rebuilds with Provider/InheritedWidget - Use ListView.builder for long lists - Lazy load images with cached_network_image - Profile with DevTools ```dart // ✅ Good class UserTile extends StatelessWidget { const UserTile({ super.key, required this.user, }); // const constructor final User user; @override Widget build(BuildContext context) { return ListTile( leading: CachedNetworkImage( imageUrl: user.avatarUrl, placeholder: (context, url) => const CircularProgressIndicator(), errorWidget: (context, url, error) => const Icon(Icons.error), ), title: Text(user.name), ); } } ``` ## Platform-Specific Code - Use separate files with `.dart` and `.freezed.dart` extensions - Use conditional imports for platform differences - Follow Material (Android) and Cupertino (iOS) guidelines ```dart // ✅ Good: Platform-specific styling Widget buildButton(BuildContext context) { return Platform.isIOS ? CupertinoButton.filled( onPressed: onPressed, child: Text(label), ) : ElevatedButton( onPressed: onPressed, child: Text(label), ); } ``` ## Project Structure ``` lib/ ├── main.dart ├── app.dart ├── core/ │ ├── constants/ │ ├── theme/ │ ├── utils/ │ └── errors/ ├── features/ │ ├── auth/ │ │ ├── data/ │ │ │ ├── datasources/ │ │ │ ├── models/ │ │ │ └── repositories/ │ │ ├── domain/ │ │ │ ├── entities/ │ │ │ ├── repositories/ │ │ │ └── usecases/ │ │ └── presentation/ │ │ ├── pages/ │ │ ├── widgets/ │ │ └── providers/ │ └── user/ ├── shared/ │ ├── widgets/ │ └── services/ └── injection_container.dart ``` ## Security - Never store sensitive data in plain text - Use flutter_secure_storage for tokens - Validate all user inputs - Use certificate pinning for APIs - Obfuscate release builds ```dart // ✅ Good final storage = FlutterSecureStorage(); Future saveToken(String token) async { await storage.write(key: 'auth_token', value: token); } Future buildRelease() async { await Process.run('flutter', [ 'build', 'apk', '--release', '--obfuscate', '--split-debug-info=$debugInfoPath', ]); } // ❌ Bad Future saveToken(String token) async { await SharedPreferences.setString('auth_token', token); // Insecure! } ``` ## Localization - Use intl package for translations - Generate localization files - Support RTL languages - Use message formatting for dynamic content ```dart // ✅ Good Widget build(BuildContext context) { return Text(AppLocalizations.of(context).hello(userName)); } // Generated in l10n.yaml arb-dir: lib/l10n template-arb-file: app_en.arb output-localization-file: app_localizations.dart ``` ## Dependencies - Keep dependencies up to date - Use exact versions in pubspec.yaml - Run `flutter pub outdated` regularly - Use `flutter analyze` before committing ```yaml # ✅ Good: Exact versions dependencies: flutter: sdk: flutter riverpod: 2.4.9 go_router: 13.1.0 dio: 5.4.0 # ❌ Bad: Version ranges dependencies: flutter: sdk: flutter riverpod: ^2.4.0 # Unpredictable dio: any # Dangerous ``` ## Clean Architecture - Separate layers: presentation, domain, data - Use dependency injection - Keep business logic in use cases - Entities should be pure Dart classes ```dart // Domain layer abstract class UserRepository { Future getUser(String id); Future saveUser(User user); } class GetUser { final UserRepository repository; GetUser(this.repository); Future call(String id) async { return repository.getUser(id); } } // Data layer class UserRepositoryImpl implements UserRepository { final UserRemoteDataSource remoteDataSource; final UserLocalDataSource localDataSource; UserRepositoryImpl({ required this.remoteDataSource, required this.localDataSource, }); @override Future getUser(String id) async { try { final remoteUser = await remoteDataSource.getUser(id); await localDataSource.cacheUser(remoteUser); return remoteUser; } catch (e) { return localDataSource.getUser(id); } } } ``` ## Build & Release - Use flavors for different environments - Configure build variants - Sign releases properly - Upload symbols for crash reporting ```bash # ✅ Good: Build commands flutter build apk --flavor production --release flutter build ios --flavor production --release flutter build appbundle --flavor production --release ``` ## Prohibitions - DO NOT use `setState` in production code (use state management) - DO NOT put business logic in widgets - DO NOT use dynamic types - DO NOT ignore lint warnings - DO NOT skip testing for critical paths - DO NOT use hot reload as a development strategy - DO NOT embed secrets in code