# Performance Optimization Standards for Flutter
This document outlines the coding standards specifically focused on performance optimization for Flutter applications. Adhering to these standards will help create Flutter applications that are fast, responsive, and resource-efficient.
## 1. General Principles
### 1.1. Optimize for the Target Platform
* **Do This:** Profile your application on the target devices (Android/iOS). Performance characteristics can vary significantly. Use "flutter profile" and DevTools. Actively examine CPU, Memory, and GPU usage.
* **Don't Do This:** Rely solely on emulator performance, as it might not accurately reflect real-world device behavior. Optimize for the lowest common denominator device, keeping in mind the target user base.
### 1.2. Asynchronous Operations
* **Do This:** Use "async" and "await" to perform long-running operations (e.g., network requests, file I/O) without blocking the UI thread. Use "Future" for asynchronous tasks. Consider using "Stream" for continuous data flow.
* **Don't Do This:** Perform synchronous, blocking operations on the main thread, which leads to UI freezes.
"""dart
// Do This: Asynchronous network request
Future fetchData() async {
final response = await http.get(Uri.parse('https://example.com/data'));
if (response.statusCode == 200) {
return response.body;
} else {
throw Exception('Failed to load data');
}
}
// Don't Do This: Synchronous network request (bad for UI)
String fetchDataSync() {
// This will block the UI. Avoid.
final response = http.get(Uri.parse('https://example.com/data')); // no await
if (response.statusCode == 200) {
return response.body;
} else {
throw Exception('Failed to load data');
}
}
"""
### 1.3. Minimize Rebuilds
* **Do This:** Use "const" constructors for widgets that don't change, significantly reducing rebuilds. Employ "ValueListenableBuilder", "StreamBuilder", and "FutureBuilder" strategically to rebuild only parts of the UI that depend on changing data. Use "shouldRebuild" in "StatefulWidget" to prevent unnecessary rebuilds. Understand the widget lifecycle.
* **Don't Do This:** Rebuild entire widget trees unnecessarily by calling "setState" on parent widgets when only a small portion needs updating.
"""dart
// Do This: Using const constructor
class MyWidget extends StatelessWidget {
const MyWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const Text('This text will not rebuild unnecessarily.');
}
}
// Do This: ValueListenableBuilder for targeted rebuilds
final ValueNotifier _counter = ValueNotifier(0);
ValueListenableBuilder(
valueListenable: _counter,
builder: (BuildContext context, int value, Widget? child) {
// This builder will only be called when _counter's value changes.
return Text('Counter: $value');
},
)
"""
### 1.4. Lazy Loading and Pagination
* **Do This:** Implement lazy loading for long lists or grids. Use pagination to fetch and display data in chunks. Libraries like "flutter_staggered_grid_view" help.
* **Don't Do This:** Load all data at once, particularly for large datasets.
"""dart
// Do This: Lazy loading and pagination with ListView.builder
ListView.builder(
itemCount: data.length + (hasMoreData ? 1 : 0), // Add 1 for the loading indicator
itemBuilder: (context, index) {
if (index < data.length) {
return ListTile(title: Text(data[index]));
} else if (hasMoreData) {
// Display a loading indicator
return const Center(child: CircularProgressIndicator());
} else {
return null; // No more data
}
},
);
"""
### 1.5. Image Optimization
* **Do This:** Optimize images for the target device resolution. Use appropriate image formats (e.g., WebP, JPEG). Use image caching. Consider using "CachedNetworkImage" package.
* **Don't Do This:** Use excessively large images that consume significant memory and bandwidth. Avoid loading full-resolution images when thumbnails suffice.
"""dart
// Do This: Using CachedNetworkImage for optimized image loading and caching
CachedNetworkImage(
imageUrl: "http://example.com/myimage.jpg",
placeholder: (context, url) => const CircularProgressIndicator(),
errorWidget: (context, url, error) => const Icon(Icons.error),
)
"""
## 2. Code-Level Optimization
### 2.1. Minimize Widget Tree Depth
* **Do This:** Flatten the widget tree where possible to reduce rendering overhead. Use "Wrap", "Row", and "Column" efficiently. Utilize custom rendering with "CustomPaint" when appropriate.
* **Don't Do This:** Create excessively nested widget hierarchies without a clear purpose.
"""dart
// Good: Flat widget tree
Column(
children: [
Text('Title'),
Text('Subtitle'),
ElevatedButton(onPressed: () {}, child: Text('Button')),
],
);
// Potentially Problematic: Deeply nested tree
Container(
padding: EdgeInsets.all(16.0),
child: Container(
decoration: BoxDecoration(border: Border.all()),
child: Padding(
padding: EdgeInsets.all(8.0),
child: Text('Some text'),
),
),
);
"""
### 2.2. Use "ListView.builder" and "GridView.builder"
* **Do This:** Use "ListView.builder" and "GridView.builder" for dynamic lists and grids to only build widgets that are currently visible on the screen. Provide an "itemCount". Utilize "AutomaticKeepAliveClientMixin" for widgets that need to maintain state when off-screen within a "ListView" or similar. Carefully manage the "cacheExtent" property.
* **Don't Do This:** Use "ListView" or "GridView" with a pre-built list of widgets for large datasets, as this builds all widgets at once, which is extremely inefficient.
"""dart
// Do This: Efficiently render a large list
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(title: Text(items[index]));
},
);
"""
### 2.3. Avoid Expensive Operations in "build()" Methods
* **Do This:** Pre-calculate complex values and store them in variables rather than recomputing them in every "build()" call. Use "initState" or "didChangeDependencies" to perform initialization tasks. Use memoization techniques.
* **Don't Do This:** Perform complex calculations, network requests, or file I/O directly in the "build()" method, as this will be executed every time the widget rebuilds.
"""dart
// Do This: Pre-calculate values
class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State {
late String _calculatedValue;
@override
void initState() {
super.initState();
_calculatedValue = calculateValue(); // Pre-calculate the value
}
String calculateValue() {
// Complex calculation here
return 'Result of complex calculation';
}
@override
Widget build(BuildContext context) {
return Text(_calculatedValue); // Use the pre-calculated value
}
}
"""
### 2.4. String Concatenation
* **Do This:** Use "StringBuffer" for efficiently building strings, particularly within loops. Use string interpolation for simple string creations.
* **Don't Do This:** Use "+" operator repeatedly for string concatenation, especially in loops, as this creates many intermediate string objects.
"""dart
// Do This: Efficient string building with StringBuffer
StringBuffer buffer = StringBuffer();
for (int i = 0; i < 1000; i++) {
buffer.write('Item $i, ');
}
String result = buffer.toString();
// String interpolation
String name = 'Alice';
int age = 30;
String message = 'Hello, my name is $name and I am $age years old.';
"""
### 2.5. Use "const" Where Possible
* **Do This:** Use the "const" keyword for widgets, values, and constructors that are known at compile time. This allows Flutter to reuse these objects and avoid unnecessary allocations.
* **Don't Do This:** Neglect to use "const" when it is applicable.
"""dart
// Do This: Using const for widgets that don't change
Container(
decoration: const BoxDecoration(color: Colors.blue),
child: const Text('Hello'),
);
"""
### 2.6. Avoid "Opacity" and "ClipRect" where possible
* **Do This:** Use alternatives such as fully opaque containers with specific background colors where you do not need to have transparency. Use "shaderMask" to clip widgets.
* **Don't Do This:** Use "Opacity" and "ClipRect" excessively as they can be expensive operations, especially when animated. Note that these are often fine for static content, but should be carefully considered in animations or frequently updating sections.
### 2.7. Consider "Transform.translate"
* **Do This:** Prefer "Transform.translate" over changing padding or margin when animating the position of a widget. Translations are generally less expensive.
* **Don't Do This:** Rely on padding or margin animations when "Transform.translate" provides an alternative.
## 3. State Management
### 3.1. Choose an Appropriate State Management Technique
* **Do This:** Select a state management solution that aligns with the complexity of your application (Provider, Riverpod, BLoC, GetX). Consider alternatives to state management such as "InheritedWidget" or "ValueNotifier" for very simple cases.
* **Don't Do This:** Overuse complex state management solutions for simple UI updates, or use an underpowered solution for a complex application.
### 3.2. Optimize Data Streams
* **Do This:** Use "StreamTransformer" to process data streams efficiently. Use "debounce" or "throttle" to reduce the frequency of updates.
* **Don't Do This:** Overload streams with unnecessary data or process streams inefficiently.
### 3.3. Immutability
* **Do This:** Make your data immutable whenever possible. Immutable data enables more efficient change detection and reduces the risk of bugs caused by unintended state modifications. Use libraries like "freezed" to help generate immutable data classes.
* **Don't Do This:** Modify data directly, as this can lead to unpredictable behavior and makes debugging harder and render performance profiling harder.
## 4. Rendering
### 4.1. Avoid Excessive "RepaintBoundary"
* **Do This:** Use "RepaintBoundary" strategically to isolate parts of the UI that need to be repainted independently. This can improve performance by preventing unnecessary repaints of other parts of the screen.
* **Don't Do This:** Wrap every widget in a "RepaintBoundary", as this can hurt performance by creating too many layers. Apply judiciously.
### 4.2. Use "CustomPaint" Judiciously
* **Do This:** Use "CustomPaint" for complex, custom drawing operations. It offers fine-grained control over rendering. Consider caching results using techniques like persisting the "Picture" object.
* **Don't Do This:** Overuse "CustomPaint" for simple UI elements that can be achieved with standard widgets.
### 4.3. Understand Rasterization
Flutter uses Skia for rendering. Understand the rasterization process to avoid offscreen rendering which can be very expensive.
* **Do This:** Profile rasterization times along with CPU and GPU usage to get the whole picture.
* **Don't Do This:** Assume that widgets that appear simple don't impact rasterization.
## 5. Tooling and Debugging
### 5.1. Flutter DevTools
* **Do This:** Utilize Flutter DevTools extensively for profiling CPU usage, memory allocation, and rendering performance. Inspect the widget tree to identify unnecessary rebuilds.
* **Don't Do This:** Neglect to use DevTools to diagnose performance problems.
### 5.2. Flutter Analyze and Linting
* **Do This:** Use "flutter analyze" along with custom linting rules (e.g., using "lints") to identify potential performance issues and enforce coding standards.
* **Don't Do This:** Ignore analyzer warnings and linter errors.
### 5.3. Performance Profiling
* **Do This:** Use the "flutter profile" command to build a release version of your app and profile its performance on a real device.
* **Don't Do This:** Rely solely on debug builds for performance testing, as they are not optimized.
## 6. Specific Widgets and Techniques
### 6.1. Animations
* **Do This:** Use "AnimatedBuilder" for animations that depend on changing data. Minimize rebuilds by only rebuilding the animated part of the UI. Use "ImplicitlyAnimatedWidget" classes for simple animations.
* **Don't Do This:** Use "setState" to trigger animations, as this rebuilds the entire widget tree and can cause performance issues.
### 6.2. Gradient Performance
* **Do This:** Gradients can be expensive. If possible, pre-render gradient images and then display them in the app rather than using "Gradient" directly.
### 6.3. Text Rendering
* **Do This:** Pay attention to font size, font family, and text styling, as these can impact rendering performance. Consider caching text styles. Reduce the amount of text rendered on screen at same time.
* **Don't Do This:** Use excessively large font sizes or complex text styles without considering the performance implications.
## 7. Memory Management
### 7.1. Dispose of Resources
* **Do This:** Always dispose of resources (e.g., "StreamController"s, "AnimationController"s) in the "dispose()" method to prevent memory leaks.
* **Don't Do This:** Forget to dispose of resources, as this can lead to memory leaks and application crashes.
"""dart
class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this);
}
@override
void dispose() {
_controller.dispose(); // Dispose of the AnimationController
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container();
}
}
"""
### 7.2. Avoid Creating Excessive Objects
* **Do This:** Reuse objects where possible to reduce memory allocation overhead. Use object pooling for frequently created and destroyed objects. Understand the garbage collector.
* **Don't Do This:** Create unnecessary objects, especially within loops or frequently called functions.
### 7.3. Be Mindful of Large Data Structures
* **Do This:** If you need to store large amounts of data in memory, consider using data structures that are optimized for memory efficiency (e.g., "Int64List" instead of "List").
## 8. Platform Channels
* **Do This:** Only use platform channels when absolutely necessary. Code running on the native side can be significantly harder to profile and debug. Consider alternatives such as plugins written in Dart. Minimize data transfer across the platform channel.
## 9. Package Dependencies
* **Do This:** Carefully vet dependencies to avoid including code that is itself inefficient. Look at the number of direct and transitive dependencies that a package adds to your app. Consider forking and optimizing packages if necessary.
## 10. Code Reviews
* **Do This:** Subject all performance sensitive code to rigorous code review. This can help to catch potential issues early on. Use automated tools that can analyze code for performance issues.
## 11. Documentation
* **Do This:** Document all performance optimizations that have been applied to the code. This will help future developers understand the reasoning behind the optimizations and avoid accidentally undoing them. Document code hotspots.
By following these coding standards, you can significantly improve the performance of your Flutter applications, resulting in a better user experience. Regularly review and update these standards to reflect the latest best practices and Flutter framework improvements.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# Core Architecture Standards for Flutter This document outlines the core architecture standards for Flutter applications, essential for building scalable, maintainable, and performant apps. These standards are designed to guide developers in structuring their Flutter projects effectively and are intended to act as context for AI coding assistants. ## 1. Fundamental Architectural Patterns ### 1.1 Layered Architecture **Standard:** Adopt a layered architecture to separate concerns and improve maintainability. Common layers include: * **Presentation Layer (UI):** Widgets, screens, and UI-related logic. * **Business Logic Layer (BLL):** Handles the application's core logic, state management, and orchestrates data flow. * **Data Layer:** Repositories, data sources (local/remote), and data models. **Do This:** * Clearly define responsibilities for each layer. * Ensure layers communicate through well-defined interfaces. * Keep the presentation layer "dumb" -- primarily displaying data and handling user input. * Utilize dependency injection to decouple layers. **Don't Do This:** * Directly access data sources (e.g., databases, network calls) from the UI. * Mix UI logic with business logic within widgets. * Create tight coupling between layers, hindering testability and reusability. **Why:** Layered architecture promotes separation of concerns, making code easier to understand, test, and modify. It also allows for independent scaling and evolution of different parts of the application. **Code Example:** """dart // Data Layer - Repository abstract class UserRepository { Future<User> getUser(String id); } class UserRepositoryImpl implements UserRepository { final UserRemoteDataSource remoteDataSource; final UserLocalDataSource localDataSource; // Optional cache UserRepositoryImpl({required this.remoteDataSource, required this.localDataSource}); @override Future<User> getUser(String id) async { // Check local cache first try { final cachedUser = await localDataSource.getUser(id); if (cachedUser != null) { return cachedUser; } } catch (e) { // Handle cache read errors. Continue to remote data source. Logging recommended. } final userDto = await remoteDataSource.getUser(id); final user = User.fromDto(userDto); // Cache the data try { await localDataSource.saveUser(user); } catch (e) { // handle cache write errors. Logging recommended since this isn't critical failure. } return user; } } // Data Transfer Object (DTO) class UserDto { final String id; final String name; UserDto({required this.id, required this.name}); factory UserDto.fromJson(Map<String, dynamic> json) { return UserDto( id: json['id'], name: json['name'], ); } } // Domain Model class User { final String id; final String name; User({required this.id, required this.name}); factory User.fromDto(UserDto dto) { return User(id: dto.id, name: dto.name); } } // Business Logic Layer - Use Case/Service class GetUserUseCase { final UserRepository userRepository; GetUserUseCase({required this.userRepository}); Future<User> execute(String id) async { return await userRepository.getUser(id); } } // Presentation Layer - ViewModel/Bloc/Provider class UserViewModel extends ChangeNotifier { final GetUserUseCase getUserUseCase; User? _user; User? get user => _user; bool _isLoading = false; bool get isLoading => _isLoading; UserViewModel({required this.getUserUseCase}); Future<void> fetchUser(String id) async { _isLoading = true; notifyListeners(); _user = await getUserUseCase.execute(id); _isLoading = false; notifyListeners(); } } // UI Layer - Widget class UserProfileScreen extends StatelessWidget { final String userId; UserProfileScreen({required this.userId}); @override Widget build(BuildContext context) { final userViewModel = Provider.of<UserViewModel>(context, listen: false); return Scaffold( appBar: AppBar(title: Text('User Profile')), body: Consumer<UserViewModel>( builder: (context, viewModel, child) { if(viewModel.isLoading) { return Center(child: CircularProgressIndicator()); } if (viewModel.user == null) { userViewModel.fetchUser(userId); return Center(child: Text('Loading user...')); } else { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('User ID: ${viewModel.user!.id}'), Text('User Name: ${viewModel.user!.name}'), ], ), ); } }, ) ); } } """ ### 1.2 State Management **Standard:** Choose a state management solution appropriate for the application's complexity. Options include: * **Provider:** Simple dependency injection and state management. Good for smaller apps or individual component state. * **Riverpod:** Inspired by Provider but addresses many of its shortcomings, such as testability and implicit dependencies. Excellent choice for medium to large apps. * **Bloc/Cubit:** Pattern for managing state in a predictable way, especially useful for complex UI interactions and business logic. * **Redux (or similar):** Predictable state container; powerful but can introduce boilerplate. Consider only for exceptionally complex state requirements. * **GetX:** All-in-one solution for state management, dependency injection, routing, etc. Can be useful for rapid prototyping, but tends to create tightly coupled code, reducing flexibility with more complex projects. **Do This:** * Select a state management solution based on project needs and team familiarity. * Encapsulate application state within the chosen solution. * Ensure state changes are predictable and testable. * Avoid direct manipulation of UI state within widgets (use state management instead). **Don't Do This:** * Rely solely on "setState" for complex state management. This can lead to performance issues and make debugging difficult. * Overuse state management solutions, especially for simple UI updates. * Introduce unnecessary complexity with state management. **Why:** Proper state management improves code organization, testability, and maintainability. It enables predictable UI updates and prevents the "spaghetti code" that can result from uncontrolled state mutations. **Code Example (Riverpod):** """dart import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter/material.dart'; // Define a provider for the counter state. final counterProvider = StateProvider((ref) => 0); class CounterApp extends ConsumerWidget { const CounterApp({Key? key}) : super(key: key); @override Widget build(BuildContext context, WidgetRef ref) { // Read the current value of the counter. final counter = ref.watch(counterProvider); return Scaffold( appBar: AppBar(title: const Text('Riverpod Counter')), body: Center(child: Text('Counter Value: $counter')), floatingActionButton: FloatingActionButton( // Increment the counter when the button is pressed. onPressed: () => ref.read(counterProvider.notifier).state++, child: const Icon(Icons.add), ), ); } } """ ### 1.3 Modularization **Standard:** Break down the application into smaller, independent modules based on features or functionalities. **Do This:** * Create separate Dart packages or modules for distinct features. * Define clear interfaces between modules. * Use dependency injection to manage module dependencies. **Don't Do This:** * Create monolithic applications with tightly coupled code. * Duplicate code across different parts of the application. **Why:** Modularization improves code reusability, testability, and maintainability. It also enables parallel development and reduces the impact of changes in one part of the application on other parts. **Code Example:** Project Structure: """ my_app/ lib/ main.dart modules/ auth/ lib/ auth.dart // Public API for the auth module src/ auth_service.dart // Implementation details (private) models/ user.dart home/ lib/ home.dart // Public API for the home module src/ home_screen.dart // Implementation details (private) """ Code within "my_app/modules/auth/lib/auth.dart": """dart library auth; export 'src/auth_service.dart'; // Expose the AuthService (or an interface) export 'models/user.dart'; class Auth { static String getModuleName() { return "Auth Module"; } } """ ## 2. Project Structure and Organization ### 2.1 Feature-Based Organization **Standard:** Organize the project structure around features rather than technical layers. **Do This:** * Create folders for each feature (e.g., "auth", "home", "profile"). * Include all relevant components for a feature within its folder (UI, BLL, data). **Don't Do This:** * Organize the project by technical layers (e.g., "screens", "services", "models"). This makes it harder to find related code. **Why:** Feature-based organization makes it easier to understand the scope of each feature and navigate the codebase. It also promotes modularity and reduces the likelihood of unintended dependencies between features. **Code Example (Directory Structure):** """ lib/ features/ authentication/ data/ models/ user.dart data_sources/ auth_remote_data_source.dart repositories/ auth_repository_impl.dart domain/ entities/ user.dart repositories/ auth_repository.dart usecases/ login_usecase.dart presentation/ bloc/ auth_bloc.dart screens/ login_screen.dart home/ ... similar structure for the home feature """ ### 2.2 Naming Conventions **Standard:** Adopt consistent naming conventions for files, classes, variables, and functions. **Do This:** * Use descriptive names that clearly indicate the purpose of the element. * Follow the CamelCase convention for class names (e.g., "UserProfileScreen"). * Use camelCase for variable and function names (e.g., "getUserData"). * Use PascalCase for file names (e.g., "user_profile_screen.dart"). * Acronyms should be consistently upper-cased or lower-cased (e.g., "HTTPService" or "HttpService", but NOT both). **Don't Do This:** * Use single-letter variable names or abbreviations that are difficult to understand. * Use inconsistent naming conventions across the project. **Why:** Consistent naming conventions improve code readability and make it easier to understand the purpose of different elements. ### 2.3 Dependency Injection **Standard:** Use dependency injection to manage dependencies between classes and modules. **Do This:** * Use a dependency injection framework like "get_it" or "riverpod". * Inject dependencies through constructor parameters. * Register dependencies in a central location (e.g., a service locator). **Don't Do This:** * Create dependencies directly within classes. This makes it harder to test and reuse the classes. Avoid the "new" keyword within classes instantiating its dependencies. * Use global variables to store dependencies. **Why:** Dependency injection improves testability, reusability, and maintainability. It allows you to easily swap out dependencies for testing or different environments. **Code Example (get_it):** """dart import 'package:get_it/get_it.dart'; final sl = GetIt.instance; // sl is short for service locator Future<void> init() async { // Data sources sl.registerLazySingleton<UserRemoteDataSource>(() => UserRemoteDataSourceImpl()); sl.registerLazySingleton<UserLocalDataSource>(() => UserLocalDataSourceImpl()); //Optional // Repository sl.registerLazySingleton<UserRepository>(() => UserRepositoryImpl(remoteDataSource: sl(), localDataSource: sl())); // Use cases sl.registerLazySingleton(() => GetUserUseCase(userRepository: sl())); // View models/Blocs/Providers sl.registerFactory(() => UserViewModel(getUserUseCase: sl())); } // Usage in a Widget: class MyWidget extends StatelessWidget { @override Widget build(BuildContext context) { final userViewModel = sl<UserViewModel>(); // ... userViewModel.fetchUser("someId"); // ... } } """ ## 3. Modern Approaches and Patterns ### 3.1 Functional Programming **Standard:** Embrace functional programming principles to write cleaner and more maintainable code. **Do This:** * Use immutable data structures. * Use pure functions (functions that have no side effects). * Use higher-order functions (functions that take other functions as arguments). * Favor list comprehensions and the spread operator ("...") **Don't Do This:** * Mutate data directly. * Write functions with side effects that modify global state. **Why:** Functional programming promotes code that is easier to test, reason about, and parallelize. **Code Example:** """dart // Immutable data structure class ImmutableUser { final String name; final int age; const ImmutableUser({required this.name, required this.age}); ImmutableUser copyWith({String? name, int? age}) { return ImmutableUser( name: name ?? this.name, age: age ?? this.age, ); } } // Pure function int add(int a, int b) { return a + b; } // Higher-order function List<int> transformList(List<int> list, int Function(int) transform) { return list.map(transform).toList(); } void main() { final numbers = [1, 2, 3, 4, 5]; final doubledNumbers = transformList(numbers, (number) => number * 2); print(doubledNumbers); // Output: [2, 4, 6, 8, 10] final user1 = ImmutableUser(name: "Alice", age: 30); final user2 = user1.copyWith(age: 31); print(user1.age); // Output: 30 print(user2.age); // Output: 31 } """ ### 3.2 Asynchronous Programming **Standard:** Use "async"/"await" for asynchronous operations to write cleaner and more readable code. **Do This:** * Use "async" for functions that perform asynchronous operations. * Use "await" to wait for the completion of asynchronous operations. * Handle errors using "try"/"catch" blocks. * Return "Future" or "Stream" objects from asynchronous functions. **Don't Do This:** * Use callbacks for complex asynchronous operations. This can lead to "callback hell". * Block the main thread with synchronous operations. **Why:** "async"/"await" simplifies asynchronous code, making it easier to read, write, and maintain.It is the modern approach to asynchronous programming in Dart. **Code Example:** """dart Future<String> fetchData() async { try { await Future.delayed(Duration(seconds: 2)); // Simulate network delay return 'Data from the internet'; } catch (e) { print('Error fetching data: $e'); return 'Error: Unable to fetch data'; } } void main() async { print('Fetching data...'); final data = await fetchData(); print('Data received: $data'); } """ ### 3.3 Reactive Programming **Standard:** Leverage reactive programming principles using "Stream"s and "StreamBuilder"s for handling asynchronous data streams. **Do This:** * Use "Stream"s to represent asynchronous data streams. * Use "StreamBuilder" widgets to display data from streams. * Use libraries like "rxdart" for advanced stream manipulation. *Note: Flutter's "ValueNotifier" fulfills some, but not all, reactive programming patterns. **Don't Do This:** * Overuse streams for simple data updates. * Ignore errors in streams. **Why:** Reactive programming provides a powerful way to handle asynchronous data and build responsive UIs. **Code Example:** """dart import 'dart:async'; import 'package:flutter/material.dart'; Stream<int> countStream() { return Stream.periodic(Duration(seconds: 1), (count) => count); } class CounterStreamExample extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('StreamBuilder Example')), body: Center( child: StreamBuilder<int>( stream: countStream(), builder: (BuildContext context, AsyncSnapshot<int> snapshot) { if (snapshot.hasError) { return Text('Error: ${snapshot.error}'); } if (snapshot.connectionState == ConnectionState.waiting) { return CircularProgressIndicator(); } return Text('Count: ${snapshot.data}'); }, ), ), ); } } """ ## 4. Common Anti-Patterns ### 4.1 God Class An anti-pattern where a single class becomes excessively large and responsible for too many things. **Avoid:** Classes with hundreds or thousands of lines of code. **Solution:** Break down the class into smaller, more focused classes with specific responsibilities. ### 4.2 Spaghetti Code Unstructured and difficult-to-follow code caused by excessive branching, lack of modularity, and poor naming. **Avoid:** Deeply nested conditional statements and complex control flow. **Solution:** Use functions, classes, and design patterns to organize the code and simplify the control flow. ### 4.3 Copy-Paste Programming Duplicating code across multiple locations instead of creating reusable components. **Avoid:** Repeated blocks of code with minor variations. **Solution:** Create reusable functions, widgets, or classes to encapsulate the common logic. ### 4.4 Premature Optimization Optimizing code before it's necessary, often based on assumptions rather than actual performance measurements. **Avoid:** Making complex optimizations without profiling the code first. **Solution:** Focus on writing clear and maintainable code first. Profile the code to identify performance bottlenecks before attempting optimizations. ## 5. Technology-Specific Details ### 5.1 Widget Lifecycle Management Understand the Flutter Widget lifecycle ("initState", "didChangeDependencies", "build", "didUpdateWidget", "dispose") and use it correctly to manage resources and state. Always dispose of resources in the "dispose()" method to prevent memory leaks (especially "StreamSubscription"s and "AnimationController"s). ### 5.2 Performance Considerations * **Minimize Widget Rebuilds:** Use "const" constructors for widgets that don't change their state. Use "shouldRebuild" in "StatefulWidget"s and "StatelessWidget" extensions (implements "DiagnosticableTree") to avoid unnecessary rebuilds (especially relevant in "AnimatedBuilder"). * **Use "ListView.builder":** For displaying large lists, "ListView.builder" efficiently builds only the visible items. * **Image Optimization:** Optimize images for different screen densities and formats. Use "ImageProvider"s (e.g., "CachedNetworkImage") for efficient image caching. * **Avoid Heavy Computations in "build()":** Move computationally expensive operations to background isolates or use caching mechanisms. * **Profile Your App:** Use the Flutter Performance Profiler to identify performance bottlenecks. ### 5.3 Platform Channels Use platform channels judiciously for platform-specific functionality. Abstract platform channel calls behind interfaces to maintain platform independence in the core application logic. Implement error handling for platform channel invocations. ### 5.4 Internationalization (i18n) and Localization (l10n) * Use the "flutter_localizations" package for internationalization. * Externalize all text strings into localization files. * Support multiple locales. * Use "BuildContext" to access localized strings ("Localizations.of<AppLocalizations>(context, AppLocalizations)!.someText"). These standards provide a solid foundation for building maintainable, scalable, and performant Flutter applications, while also aligning with best practices for AI-assisted coding. They promote cleaner code, better architecture, and faster development cycles. This document should evolve with new releases of Flutter and emerging best practices.
# State Management Standards for Flutter This document outlines the coding standards for state management in Flutter applications. These standards aim to promote maintainability, scalability, testability, and performance. They are based on the latest Flutter best practices and are designed to be used by developers and AI coding assistants. ## 1. Introduction to State Management in Flutter State management is a critical aspect of Flutter development. It involves managing the data that changes over time in an application and ensuring that the UI reflects these changes correctly. Choosing the right state management approach is crucial for building robust and maintainable Flutter apps. Flutter offers various approaches (Provider, Riverpod, BLoC/Cubit, GetX, etc.) and understanding their pros and cons is vital. ### 1.1 Principles of Effective State Management * **Separation of Concerns:** Decouple UI logic from business logic and data management. * **Unidirectional Data Flow:** Data should flow in a single direction, making it easier to track changes and debug issues. * **Immutability:** Prefer immutable data structures to prevent unexpected side effects and simplify state tracking. * **Testability:** Design state management solutions that are easily testable, allowing for thorough unit and widget testing. * **Performance:** Optimize state updates to minimize UI rebuilds and avoid performance bottlenecks. ## 2. Choosing a State Management Solution Flutter offers several popular state management solutions. Each has its strengths and weaknesses, making them suitable for different use cases. ### 2.1 Provider * **Description:** A simple and lightweight dependency injection and state management solution built on "InheritedWidget". It's officially recommended by the Flutter team for simple to medium-sized applications. * **Pros:** * Easy to learn and use. * Minimal boilerplate code. * Well-integrated with Flutter widgets. * Good for simple to medium complexity applications. * **Cons:** * Can become complex as the application grows. * May require additional patterns for complex state logic (combining with ChangeNotifier, for example). * **When to Use:** Small to medium-sized apps, prototypes, or when you need a simple solution. ### 2.2 Riverpod * **Description:** A reactive state-management framework. Fully type-safe alternative to Provider. Can be used to refactor existing Provider-based app. * **Pros:** * Compile-time safety (type-safe). * More powerful and flexible composition than Provider. * Testable. * Easier to debug than Provider. * **Cons:** * Slightly steeper learning curve compared to Provider. * More verbose than Provider. * **When to Use:** Medium to large-sized applications with a focus on type safety and testability, and an improved developer experience ### 2.3 BLoC/Cubit * **Description:** Architectures using Business Logic Components (BLoC) or Cubits separate the presentation layer from the business logic. BLoC uses streams and events, whereas Cubit uses simpler synchronous methods to emit states. * **Pros:** * Excellent separation of concerns. * Highly testable. * Suitable for complex business logic. * Clear unidirectional data flow. * **Cons:** * More boilerplate code compared to Provider. * Steeper learning curve. * **When to Use:** Large-scale applications with complex business logic and a need for strong separation of concerns. ### 2.4 GetX * **Description:** A microframework that provides state management, dependency injection, and route management. * **Pros:** * All-in-one solution. * Minimal boilerplate code. * Easy to learn and use. * **Cons:** * Can lead to tight coupling if not used carefully. * Less control over individual components. * **When to Use:** Rapid prototyping, small to medium-sized apps where strict architecture isn't critical. ### 2.5 ValueNotifier / ValueListenableBuilder * **Description:** A simple way to manage single values that need to trigger UI updates. "ValueNotifier" holds a single value, and "ValueListenableBuilder" rebuilds when that value changes. * **Pros:** * Very lightweight and simple to use. * Minimal boilerplate. * **Cons:** * Only suitable for managing a single value. * Not appropriate for complex state. * **When to Use:** Simple UI elements that depend on a single, easily managed value (e.g., a counter, a toggle). ### 2.6 setState (Avoid when possible, but understand it) * **Description:** The most basic form of state management in Flutter. Calling "setState" triggers a rebuild of the widget. * **Pros:** * Simple and easy to understand. * Built into Flutter. * **Cons:** * Inefficient for large or complex widgets. * Can lead to performance issues if overused. * Doesn't encourage separation of concerns. * **When to Use:** Very simple UI elements or quick prototyping. Avoid for production applications with any complexity. Understand how it works so you can recognize when it's being misused. * **Alternatives:** Refactor to one of the other state management techniques listed above when "setState" usage becomes complex or performance becomes a bottleneck. ### 2.7 Recommendation For most applications, **Riverpod** offers the best balance of power, type safety, and maintainability. **Provider** is a reasonable alternative for less complex apps or teams already familiar with it. Avoid "setState" except for the simplest cases. Carefully consider the trade-offs of **GetX** before using it in a large project. BLoC is a good alternative if you need to strictly enforce layer separation and unidirectional data flow. ## 3. Coding Standards for Provider ### 3.1 Do This: Use "ChangeNotifierProvider" for Mutable State Use "ChangeNotifierProvider" to provide a "ChangeNotifier" instance to the widget tree. """dart import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class Counter extends ChangeNotifier { int _count = 0; int get count => _count; void increment() { _count++; notifyListeners(); } } class CounterScreen extends StatelessWidget { const CounterScreen({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return ChangeNotifierProvider( create: (context) => Counter(), child: Scaffold( appBar: AppBar(title: const Text('Counter')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text('You have pushed the button this many times:'), Consumer<Counter>( builder: (context, counter, child) => Text( '${counter.count}', style: Theme.of(context).textTheme.headlineMedium, ), ), ], ), ), floatingActionButton: FloatingActionButton( onPressed: () => Provider.of<Counter>(context, listen: false).increment(), tooltip: 'Increment', child: const Icon(Icons.add), ), ), ); } } """ * **Why:** "ChangeNotifier" makes it easy to manage mutable state and notify listeners when the state changes. "ChangeNotifierProvider" links this to the widget hierarchy. ### 3.2 Do This: Use "Consumer" or "Selector" for Widget Rebuilds Use "Consumer" or "Selector" to limit widget rebuilds to only the parts of the UI that depend on the state. """dart Consumer<Counter>( builder: (context, counter, child) => Text( '${counter.count}', style: Theme.of(context).textTheme.headlineMedium, ), ) """ Or: """dart Selector<Counter, int>( selector: (context, counter) => counter.count, builder: (context, count, child) => Text( '$count', style: Theme.of(context).textTheme.headlineMedium, ), ) """ * **Why:** "Consumer" and "Selector" prevent unnecessary widget rebuilds, improving performance. "Selector" provides even finer-grained control by only rebuilding when the selected value changes. ### 3.3 Don't Do This: Calling "notifyListeners" Excessively Avoid calling "notifyListeners" too often, as it can trigger unnecessary UI rebuilds. Batch state updates where possible. """dart class Counter extends ChangeNotifier { int _count = 0; int get count => _count; // BAD: Calling notifyListeners after each increment void incrementMultiple() { _count++; notifyListeners(); // Avoid this pattern _count++; notifyListeners(); // Avoid this pattern } // GOOD: Batch updates and call notifyListeners once void incrementMultipleGood() { _count += 2; notifyListeners(); // This is the preferred pattern. } } """ * **Why:** Excessive calls to "notifyListeners" can degrade performance. Batching updates reduces rebuilds. ### 3.4 Do This: Use "Provider.of<T>(context, listen: false)" for Non-Rebuilding Access When you only need to access the state and don't need the widget to rebuild, use "Provider.of<T>(context, listen: false)". """dart FloatingActionButton( onPressed: () => Provider.of<Counter>(context, listen: false).increment(), tooltip: 'Increment', child: const Icon(Icons.add), ) """ * **Why:** Prevents unnecessary widget rebuilds when only accessing the state. ## 4. Coding Standards for Riverpod ### 4.1 Do This: Use "StateProvider", "ChangeNotifierProvider", "FutureProvider", and "StreamProvider" Select the provider type that matches the kind of data you are exposing. "StateProvider" for simple state, "ChangeNotifierProvider" for mutable state with notifications, "FutureProvider" for asynchronous data that only needs to be loaded once, and "StreamProvider" for streaming data. """dart import 'package:flutter_riverpod/flutter_riverpod.dart'; // StateProvider: Simple state final counterProvider = StateProvider((ref) => 0); // ChangeNotifierProvider: Mutable state with notifications class Counter extends ChangeNotifier { int _count = 0; int get count => _count; void increment() { _count++; notifyListeners(); } } final counterChangeNotifierProvider = ChangeNotifierProvider((ref) => Counter()); // FutureProvider: Asynchronous data that should only be loaded once. final someDataProvider = FutureProvider((ref) async { // Simulate fetching data from an API await Future.delayed(const Duration(seconds: 2)); return "Data loaded successfully!"; }); // StreamProvider: Streaming data final clockProvider = StreamProvider((ref) { return Stream.periodic(const Duration(seconds: 1), (computationCount) => DateTime.now()); }); """ * **Why:** Riverpod's providers handle different state management scenarios in a typesafe manner. Choosing the right provider ensures correct and efficient state management. ### 4.2 Do This: Use "ConsumerWidget" or "HookConsumer" for accessing providers in widgets. Use "ConsumerWidget" (or "HookConsumer" if using "flutter_hooks") to easily access providers using the "WidgetRef" parameter passed to the "build" method. """dart import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; class MyWidget extends ConsumerWidget { const MyWidget({Key? key}) : super(key: key); @override Widget build(BuildContext context, WidgetRef ref) { final counter = ref.watch(counterProvider); final someData = ref.watch(someDataProvider); return Scaffold( appBar: AppBar(title: const Text('Riverpod Example')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('Counter value: $counter'), switch (someData) { AsyncData(:final value) => Text('Data: $value'), AsyncError(:final error) => Text('Error: $error'), _ => const CircularProgressIndicator(), }, ], ), ), floatingActionButton: FloatingActionButton( onPressed: () => ref.read(counterProvider.notifier).state++, child: const Icon(Icons.add), ), ); } } """ * **Why:** "ConsumerWidget" provides a clean and concise way to interact with Riverpod providers. "ref.watch" rebuilds the widget when the provider's value changes. ### 4.3 Do This: Use "ref.read" for one-time access to a provider's value. Use "ref.read" when you only need to access the provider once (e.g. inside an event handler) and you don't want the widget to rebuild when the provider's value changes. In the code block above regarding "ConsumerWidget", see the on pressed function of the "FloatingActionButton". * **Why:** Prevents unnecessary rebuilds by only accessing the state when needed. ### 4.4 Don't Do This: Mixing Provider and Riverpod Avoid using Provider and Riverpod in the same widget tree unless you're deliberately migrating from Provider to Riverpod. * **Why:** Mixing state management solutions can lead to confusion and unexpected behavior. ### **4.5 Do This:** Use "family" to parameterize Providers Use "family" to create provider factories, passing parameters to customize their behavior. """dart final userProvider = FutureProvider.family<User, int>((ref, userId) async { // Simulate fetching user data await Future.delayed(Duration(seconds: 1)); return User(id: userId, name: 'User $userId'); }); class User { final int id; final String name; User({required this.id, required this.name}); } class UserProfile extends ConsumerWidget { final int userId; const UserProfile({Key? key, required this.userId}) : super(key: key); @override Widget build(BuildContext context, WidgetRef ref) { final user = ref.watch(userProvider(userId)); return switch (user) { AsyncData(:final value) => Text('User: ${value.name}'), AsyncError(:final error) => Text('Error: $error'), _ => const CircularProgressIndicator(), }; } } """ * **Why:** Parameterized providers make components reusable and configurable with specific arguments. ## 5. Coding Standards for BLoC/Cubit ### 5.1 Do This: Separate Events, States, and the BLoC/Cubit Create separate classes for events, states, and the BLoC/Cubit itself. """dart // Events abstract class CounterEvent {} class IncrementEvent extends CounterEvent {} // States abstract class CounterState {} class CounterInitial extends CounterState { CounterInitial(); } class CounterUpdated extends CounterState { final int count; CounterUpdated(this.count); } // Cubit import 'package:flutter_bloc/flutter_bloc.dart'; class CounterCubit extends Cubit<CounterState> { int counter = 0; CounterCubit() : super(CounterInitial()); void increment() { counter++; emit(CounterUpdated(counter)); } } """ * **Why:** Enforces a clear separation of concerns, making the code more organized and maintainable. If using BLoC, also create Event classes. ### 5.2 Do This: Use "BlocProvider" to Provide the BLoC/Cubit Use "BlocProvider" to provide the BLoC/Cubit to the widget tree. """dart import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class CounterScreen extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( create: (context) => CounterCubit(), child: Scaffold( appBar: AppBar(title: const Text('Counter')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text('You have pushed the button this many times:'), BlocBuilder<CounterCubit, CounterState>( builder: (context, state) { if (state is CounterUpdated) { return Text( '${state.count}', style: Theme.of(context).textTheme.headlineMedium, ); } return const Text('0'); }, ), ], ), ), floatingActionButton: FloatingActionButton( onPressed: () => BlocProvider.of<CounterCubit>(context).increment(), tooltip: 'Increment', child: const Icon(Icons.add), ), ), ); } } """ * **Why:** Integrates the BLoC/Cubit into the widget tree, making it accessible to descendant widgets. ### 5.3 Do This: Use "BlocBuilder" or "BlocListener" for UI Updates Use "BlocBuilder" to rebuild widgets based on state changes or "BlocListener" to perform side effects based on state changes (e.g., navigation, showing a snackbar). """dart BlocBuilder<CounterCubit, CounterState>( builder: (context, state) { if (state is CounterUpdated) { return Text( '${state.count}', style: Theme.of(context).textTheme.headlineMedium, ); } return const Text('0'); }, ) """ * **Why:** "BlocBuilder" and "BlocListener" provide a granular way to update the UI based on state changes, improving performance. ### 5.4 Don't Do This: Performing Business Logic in the UI Avoid performing business logic directly in the UI. Delegate business logic to the BLoC/Cubit. * **Why:** Violates separation of concerns and makes the UI harder to test and maintain. ### 5.5 Do This: Proper Error Handling Within BLoC/Cubit Implement proper error handling within the BLoC/Cubit, emitting error states to notify the UI of any errors. """dart //State class CounterError extends CounterState { final String errorMessage; CounterError(this.errorMessage); } //Cubit class CounterCubit extends Cubit<CounterState> { int counter = 0; CounterCubit() : super(CounterInitial()); void increment() { try { // Simulate an error if (counter > 5) { throw Exception('Counter cannot exceed 5'); } counter++; emit(CounterUpdated(counter)); } catch (e) { emit(CounterError(e.toString())); } } } //UI BlocBuilder<CounterCubit, CounterState>( builder: (context, state) { if (state is CounterUpdated) { return Text( '${state.count}', style: Theme.of(context).textTheme.headlineMedium, ); } if (state is CounterError) { return Text( 'Error: ${state.errorMessage}', style: TextStyle(color: Colors.red), ); } return const Text('0'); }, ) """ * **Why:** Provides a robust and maintainable way to handle errors and display them in the UI. ## 6. General State Management Standards ### 6.1 Do This: Use Immutable Data Structures Prefer immutable data structures wherever possible. Use "const", "final", and libraries like "freezed" or "built_value" to enforce immutability. """dart import 'package:freezed_annotation/freezed_annotation.dart'; part 'user.freezed.dart'; @freezed class User with _$User { const factory User({ required String id, required String name, required int age, }) = _User; } """ * **Why:** Immutability prevents unexpected side effects and simplifies state tracking. "freezed" (or "built_value") generate boilerplate for immutable classes. ### 6.2 Do This: Document State Management Logic Document the state management logic, including the purpose of each state, event, and the transitions between them with clear comments or docstrings. * **Why:** Improves code understanding and maintainability, especially in complex applications. ### 6.3 Do This: Write Unit Tests for State Logic Write thorough unit tests for the state management logic (ChangeNotifiers, BLoCs/Cubits, Riverpod providers). Use mocking frameworks like "mockito" to isolate dependencies. * **Why:** Ensures the correctness and reliability of the state management logic, reducing bugs and improving stability. ### 6.4 Don't Do This: Mutating State Directly Avoid directly mutating state. Always create new instances of state objects to trigger UI updates. """dart // BAD: Direct state mutation class Counter extends ChangeNotifier { int _count = 0; int get count => _count; void increment() { _count++; // Direct mutation -- AVOID notifyListeners(); } } // GOOD: Creating a new state class Counter extends ChangeNotifier { int _count = 0; int get count => _count; void increment() { // Creating new state _count = _count + 1; notifyListeners(); } } """ * **Why:** Direct mutation can lead to unexpected side effects and missed UI updates. Creating new instances ensures that Flutter recognizes the state change. Directly mutating data inside of a Riverpod provider invalidates the provider state, causing widgets that depend on the provider to not rebuild. ### 6.5 Do this: Design for Error handling Always consider error handling in your design. Wrap relevant processes into a try catch block and emit relevant error states or display relevant error messages. ### 6.6 Do this: Only rebuild where necessary Ensure to only rebuild widgets where necessary to optimise performance for your applications and make them responsive. ## 7. Conclusion Following these state management standards will help you build robust, maintainable, and performant Flutter applications. By choosing the right state management solution for your project's needs and adhering to these guidelines, you can create a codebase that is easy to understand, test, and evolve over time.
# Component Design Standards for Flutter This document outlines the component design standards for Flutter development. These standards aim to promote reusable, maintainable, and performant components within Flutter applications. They are based on the latest Flutter best practices and incorporate modern design patterns. ## 1. Principles of Component Design in Flutter ### 1.1 Reusability **Standard:** Components should be designed to be reusable across multiple parts of the application or even across different applications. *Why?* Reusability reduces code duplication, simplifies maintenance, and ensures consistency in the user interface. **Do This:** * Design components with configurable properties. * Avoid hardcoding values; use parameters instead. * Create generic components that can adapt to different data types or scenarios. **Don't Do This:** * Creating components tightly coupled to specific parts of the application. * Hardcoding values specific to one context within a component's implementation. * Building components that perform too many unrelated tasks. **Example:** """dart // Good: A reusable button component import 'package:flutter/material.dart'; class CustomButton extends StatelessWidget { final String text; final VoidCallback onPressed; final Color? color; const CustomButton({ Key? key, required this.text, required this.onPressed, this.color, }) : super(key: key); @override Widget build(BuildContext context) { return ElevatedButton( onPressed: onPressed, style: ElevatedButton.styleFrom( backgroundColor: color ?? Theme.of(context).primaryColor, ), child: Text(text), ); } } // Usage CustomButton( text: 'Submit', onPressed: () { // Handle submit action }, ), CustomButton( text: 'Cancel', onPressed: () { // Handle cancel action }, color: Colors.red, ) """ """dart // Bad: A button tightly coupled to a specific use case import 'package:flutter/material.dart'; class SubmitButton extends StatelessWidget { @override Widget build(BuildContext context) { return ElevatedButton( onPressed: () { // Specific submit logic here, making it hard to reuse }, child: Text('Submit Order'), ); } } """ ### 1.2 Maintainability **Standard:** Components should be designed to be easily understood, modified, and updated. *Why?* Maintainability reduces the effort required to fix bugs, add new features, or refactor existing code. **Do This:** * Write clear and concise code with comments explaining complex logic. * Follow the Single Responsibility Principle: each component should have one specific job. * Use meaningful names for variables, functions, and classes. * Keep components small. **Don't Do This:** * Writing long, complicated functions or classes. * Lack of documentation. * Skipping unit tests. **Example:** """dart // Good: A well-structured and commented component import 'package:flutter/material.dart'; /// A custom text input field with validation. class CustomTextField extends StatefulWidget { final String label; final String? Function(String?)? validator; final TextEditingController controller; final bool obscureText; const CustomTextField({ Key? key, required this.label, this.validator, required this.controller, this.obscureText = false, }) : super(key: key); @override _CustomTextFieldState createState() => _CustomTextFieldState(); } class _CustomTextFieldState extends State<CustomTextField> { @override Widget build(BuildContext context) { return TextFormField( controller: widget.controller, decoration: InputDecoration( labelText: widget.label, border: OutlineInputBorder(), ), validator: widget.validator, obscureText: widget.obscureText, ); } } """ """dart // Bad: An overly complex and undocumented component import 'package:flutter/material.dart'; class WeirdWidget extends StatefulWidget { @override _WeirdWidgetState createState() => _WeirdWidgetState(); } class _WeirdWidgetState extends State<WeirdWidget> { int x = 0; bool y = false; @override Widget build(BuildContext context) { return Container( child: GestureDetector( onTap: () { setState(() { x++; y = !y; }); }, child: Text('Click me $x'), ), ); } } """ ### 1.3 Performance **Standard:** Components should be designed to minimize their impact on application performance. *Why?* High performance ensures a smooth and responsive user experience. **Do This:** * Use "const" constructors for immutable widgets. * Avoid unnecessary rebuilds using "const", "final", and "ValueKey". * Optimize image loading and caching. * Use the "ListView.builder" constructor for large lists and grids. **Don't Do This:** * Creating very deep widget trees. * Performing heavy calculations within the "build" method. * Ignoring memory leaks. **Example:** """dart // Good: Using const constructor for an immutable widget import 'package:flutter/material.dart'; class MyStaticText extends StatelessWidget { final String text; const MyStaticText({Key? key, required this.text}) : super(key: key); @override Widget build(BuildContext context) { return Text(text); } } // Usage (improved performance) const MyStaticText(text: 'Hello World'); // const keyword ensures this widget is only built once """ """dart // Bad: Rebuilding the widget unnecessarily import 'package:flutter/material.dart'; class MyDynamicText extends StatelessWidget { final String text; MyDynamicText({Key? key, required this.text}) : super(key: key); // Missing const @override Widget build(BuildContext context) { return Text(text); } } """ ### 1.4 Testability **Standard:** Components should be designed to be easily tested in isolation. *Why?* Testability helps to ensure the correctness of software, and find bugs early. **Do This:** * Break logic out of the UI into testable business logic classes. * Use dependency injection to provide mockable dependencies. * Write both unit and widget tests **Don't Do This:** * Having side-effects in the "build()" method. * Unnecessary coupling between components. * Failing to create tests. **Example:** """dart // Good: A component with testable extracted logic. import 'package:flutter/material.dart'; class Counter { int _count = 0; int get count => _count; void increment() { _count++; } } class CounterWidget extends StatefulWidget { final Counter counter; // Inject the counter const CounterWidget({Key? key, required this.counter}) : super(key: key); @override _CounterWidgetState createState() => _CounterWidgetState(); } class _CounterWidgetState extends State<CounterWidget> { @override Widget build(BuildContext context) { return Column( children: [ Text('Count: ${widget.counter.count}'), ElevatedButton( onPressed: () { setState(() { widget.counter.increment(); }); }, child: const Text('Increment'), ), ], ); } } """ The "Counter" logic is extracted, making possible unit tests without any UI. The UI can also be widget-tested. ### 1.5 Accessibility **Standard:** Components should be designed to be accessible to users with disabilities. *Why?* Ensuring accessibility allows a wider range of users to interact with the application effectively. **Do This:** * Use semantic labels for interactive elements. * Provide alternative text for images. * Ensure sufficient contrast between text and background colors. * Use "Semantics" widget correctly. **Don't Do This:** * Ignoring accessibility guidelines. * Relying solely on visual cues. * Using low contrast color schemes. **Example:** """dart // Good: Accessible image with semantic label import 'package:flutter/material.dart'; class AccessibleImage extends StatelessWidget { final String imageUrl; final String semanticLabel; const AccessibleImage({Key? key, required this.imageUrl, required this.semanticLabel}) : super(key: key); @override Widget build(BuildContext context) { return Semantics( label: semanticLabel, child: Image.network(imageUrl), ); } } """ ## 2. Component Structure ### 2.1 Atomic Design Principles **Standard:** Consider applying Atomic Design principles to structure larger applications/widget trees. *Why?* Atomic Design organizes UI elements into atoms, molecules, organisms, templates, and pages, mirroring a chemical structure. This structured approach encourages reusability and consistency. **Do This:** * Start with basic UI elements (atoms): buttons, labels, input fields. * Combine atoms into molecules: a search bar (input + button). * Group molecules into organisms: a header with navigation. **Example:** """dart // Atom: A simple text label import 'package:flutter/material.dart'; class Label extends StatelessWidget { final String text; const Label({Key? key, required this.text}) : super(key: key); @override Widget build(BuildContext context) { return Text(text); } } // Molecule: A search bar class SearchBar extends StatelessWidget { @override Widget build(BuildContext context) { return Row( children: [ Expanded(child: TextField(decoration: InputDecoration(hintText: "Search"))), IconButton(onPressed: (){}, icon: Icon(Icons.search)) ], ); } } // Organism: A Header with Navigation class Header extends StatelessWidget { @override Widget build(BuildContext context) { return Column( children: [ Text("My App", style: TextStyle(fontSize: 24)), SearchBar(), ], ); } } """ ### 2.2 Component Directory Structure **Standard:** Organize components into a clear and consistent directory structure. *Why?* A well-defined directory structure improves code discoverability and maintainability. **Do This:** * Create a dedicated "components" or "widgets" directory. * Group related components into subdirectories. * Follow a consistent naming convention for files and directories. **Example:** """ lib/ ├── components/ │ ├── buttons/ │ │ ├── custom_button.dart │ │ ├── icon_button.dart │ ├── text_fields/ │ │ ├── custom_text_field.dart │ │ ├── search_bar.dart │ ├── cards/ │ │ ├── product_card.dart """ ### 2.3 State Management Considerations **Standard:** Choose the correct state management solution for your component based on the component's complexity and scope. *Why?* Efficient state management is crucial for building reactive and performant Flutter applications. **Do This:** * Use "setState" for simple, localized state changes within a single widget. * Consider "Provider", "Riverpod", "Bloc/Cubit", or "GetX" for more complex state management needs. * Use value notifiers effectively. **Example:** """dart // Simple state management with setState import 'package:flutter/material.dart'; class CounterWidget extends StatefulWidget { @override _CounterWidgetState createState() => _CounterWidgetState(); } class _CounterWidgetState extends State<CounterWidget> { int _counter = 0; void _incrementCounter() { setState(() { _counter++; }); } @override Widget build(BuildContext context) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text( 'You have pushed the button this many times:', ), Text( '$_counter', style: Theme.of(context).textTheme.headlineMedium, ), ElevatedButton( onPressed: _incrementCounter, child: Text('Increment'), ), ], ); } } """ ## 3. Component Implementation ### 3.1 Naming Conventions **Standard:** Adhere to consistent naming conventions for components. *Why?* Consistent naming makes code easier to read and understand. **Do This:** * Use PascalCase for component class names (e.g., "CustomButton", "ProductCard"). * Use camelCase for variable and function names (e.g., "onPressed", "labelText"). * Use a descriptive prefix or suffix to clarify a component's purpose (e.g., "CustomButton", "TextFieldInput"). ### 3.2 Immutability **Standard:** Make components immutable whenever possible by using "const" constructors. *Why?* Immutable widgets can be efficiently rebuilt by Flutter, contributing to smoother animations and better performance. **Example:** """dart // Immutable widget with a const constructor import 'package:flutter/material.dart'; class ImmutableLabel extends StatelessWidget { final String text; const ImmutableLabel({Key? key, required this.text}) : super(key: key); @override Widget build(BuildContext context) { return Text(text); } } // Usage const ImmutableLabel(text: 'This is an immutable label'); """ ### 3.3 Handling User Input **Standard:** Utilize "TextEditingController" for input fields, properly disposing of them when the widget is disposed. *Why?* "TextEditingController" provides control over the text being edited in a "TextField". Ensuring proper disposal prevents memory leaks. **Example:** """dart // Using TextEditingController and disposing it import 'package:flutter/material.dart'; class MyTextField extends StatefulWidget { @override _MyTextFieldState createState() => _MyTextFieldState(); } class _MyTextFieldState extends State<MyTextField> { final TextEditingController _controller = TextEditingController(); @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return TextField( controller: _controller, decoration: InputDecoration(hintText: 'Enter text'), ); } } """ ### 3.4 Error Handling **Standard:** Implement proper error handling within components to prevent crashes and improve user experience. *Why?* Error handling ensures that unexpected errors are gracefully handled, preventing the application from crashing. **Do This:** * Use "try-catch" blocks to handle exceptions. * Display user-friendly error messages. * Log errors for debugging purposes. **Example:** """dart import 'package:flutter/material.dart'; class DataFetcher extends StatefulWidget { @override _DataFetcherState createState() => _DataFetcherState(); } class _DataFetcherState extends State<DataFetcher> { String? _data; String? _error; @override void initState() { super.initState(); _fetchData(); } Future<void> _fetchData() async { try { _data = await fetchDataFromApi(); // Assume this can throw an error setState(() { _error = null; }); } catch (e) { setState(() { _error = 'Failed to load data: ${e.toString()}'; _data = null; }); print('Error fetching data: $e'); } } Future<String> fetchDataFromApi() async { // Simulate API call await Future.delayed(Duration(seconds: 2)); // Simulate error throw Exception('Failed to connect to API'); //return 'Data from API'; } @override Widget build(BuildContext context) { if (_error != null) { return Text('Error: $_error'); } else if (_data != null) { return Text('Data: $_data'); } else { return CircularProgressIndicator(); } } } """ ### 3.5 Using "key" **Standard:** Understanding the proper usage of "key". *Why?* Keys are fundamental to Flutter's widget lifecycle, and understanding their different types and purposes unlocks better control over state preservation and widget rebuilding. **Do This:** * Use "ValueKey" when dealing with list item order changes. * Understanding the use of "GlobalKey" and their purpose. * Be careful: overuse of "GlobalKey" may indicate an underlying architectural problem. **Example:** """dart // Using ValueKey for list item reordering import 'package:flutter/material.dart'; class ReorderableList extends StatefulWidget { @override _ReorderableListState createState() => _ReorderableListState(); } class _ReorderableListState extends State<ReorderableList> { List<String> items = ['Item 1', 'Item 2', 'Item 3']; @override Widget build(BuildContext context) { return ReorderableListView( onReorder: (oldIndex, newIndex) { setState(() { if (oldIndex < newIndex) { newIndex -= 1; } final String item = items.removeAt(oldIndex); items.insert(newIndex, item); }); }, children: <Widget>[ for (final item in items) ListTile( key: ValueKey<String>(item), title: Text(item), ), ], ); } } """ ## 4. Testing Components ### 4.1 Types of Tests **Standard:** Implement thorough testing strategies using Unit Tests, Widget Tests, and Integration Tests. *Why?* Helps ensure the software's correctness while bugs are easily fixable. **Do This:** * Write **Unit Tests** to verify individual functions and classes. * Create **Widget Tests** to test UI components. * Don't forget **Integration Tests** for end-to-end flows. ### 4.2 Mocking Dependencies **Standard:** Use mocking to isolate components during testing. *Why?* Mocking allows you to control the behavior of dependencies, ensuring that tests are predictable and reliable. ## 5. Modern Approaches and Patterns ### 5.1 Functional Components **Standard:** While Flutter primarily uses widgets, consider using pure functions for stateless transformations of data within your component. *Why?* Pure functions are easier to test and reason about because they have no side effects and always produce the same output for the same input. This can improve code readability and maintainability. **Example:** """dart // A pure function to format a price String formatPrice(double price) { return '\$${price.toStringAsFixed(2)}'; } class ProductCard extends StatelessWidget { final double price; const ProductCard({Key? key, required this.price}) : super(key: key); @override Widget build(BuildContext context) { return Card( child: Padding( padding: const EdgeInsets.all(8.0), child: Text('Price: ${formatPrice(price)}'), // Using the pure function ), ); } } """ ### 5.2 Composition over Inheritance **Standard:** Favor composition over inheritance for code reuse in Flutter components. *Why?* Composition promotes flexibility and reduces the risk of creating rigid and brittle class hierarchies. **Do This:** * Create small, reusable components. * Combine these components to build more complex UIs. * Use mixins judiciously to share functionality across components. ### 5.3 Using the "SafeArea" Widget **Standard:** Enclose content with "SafeArea" to avoid intrusions from device notches or status bars. *Why?* "SafeArea" ensures UI elements are visible and interactable on any supported platform/device. """dart import 'package:flutter/material.dart'; class MyScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: Center( child: Text('Content within SafeArea'), ), ), ); } } """ Adhering to these component design standards will help to build high-quality, maintainable, and performant Flutter applications. Remember to adapt these guidelines to your specific project requirements and encourage continuous learning and improvement within your development team.
# Deployment and DevOps Standards for Flutter This document outlines the deployment and DevOps standards for Flutter applications. It provides guidelines for building, testing, deploying, and monitoring Flutter apps in a production environment. Following these standards ensures maintainability, scalability, and reliability. ## 1. Build Processes and CI/CD ### 1.1. Automated Builds and Testing **Do This:** Implement a Continuous Integration/Continuous Delivery (CI/CD) pipeline to automate builds, tests, and deployments whenever code changes are pushed. **Don't Do This:** Manually build and deploy applications, which is error-prone and time-consuming. **Why:** Automated builds and tests reduce human error and increase the speed of the development cycle. **Example:** """yaml # .github/workflows/flutter.yml name: Flutter CI/CD on: push: branches: [ "main" ] pull_request: branches: [ "main" ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-java@v3 with: distribution: 'zulu' java-version: '11' - uses: subosito/flutter-action@v2 with: flutter-version: '3.19.0' # Specify the Flutter version - run: flutter pub get - run: flutter analyze - run: flutter test - run: flutter build apk --split-per-abi # For Android - run: flutter build ios --no-codesign # For iOS (requires macOS) # Add steps to deploy to Firebase App Distribution, TestFlight, or other services """ **Anti-Pattern:** * Failing to integrate automated testing into the CI/CD pipeline. ### 1.2. Environment Configuration **Do This:** Use environment variables for sensitive configuration, such as API keys, database URLs, and service endpoints. Inject these variables into the Flutter app at build time. **Don't Do This:** Hardcode sensitive information in the application code or configuration files. **Why:** Environment variables improve security and allow for easy configuration changes without modifying the code. **Example:** """dart // lib/config/app_config.dart import 'package:flutter_dotenv/flutter_dotenv.dart'; class AppConfig { static String get apiBaseUrl => dotenv.env['API_BASE_URL'] ?? 'http://default-api.com'; static String get apiKey => dotenv.env['API_KEY'] ?? 'default_key'; // Provide sensible defaults } // main.dart import 'package:flutter/material.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'config/app_config.dart'; Future<void> main() async { await dotenv.load(fileName: ".env"); print('API Base URL: ${AppConfig.apiBaseUrl}'); runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter App', home: Scaffold( appBar: AppBar( title: Text('Flutter App'), ), body: Center( child: Text('Using API at: ${AppConfig.apiBaseUrl}'), ), ), ); } } """ **.env file:** """ API_BASE_URL=https://production-api.com API_KEY=your_secure_api_key """ Add ".env" to your ".gitignore" file. Install "flutter_dotenv" package. **Anti-Pattern:** * Checking ".env" files into version control. * Using different configuration approaches (e.g., .json, .xml, .plist) inconsistently across platforms. Pick one and be consistent. ### 1.3. Versioning and Tagging **Do This:** Implement a consistent versioning scheme (e.g., Semantic Versioning) for your applications. Tag each release in your version control system. **Don't Do This:** Use inconsistent versioning or fail to tag releases. **Why:** Versioning and tagging make it easy to track and manage different versions of the application. **Example:** Use Git tags to mark releases: """bash git tag -a v1.0.0 -m "Release v1.0.0" git push origin v1.0.0 """ Update the "pubspec.yaml" file with version information: """yaml version: 1.0.0+1 #Major.Minor.Patch+BuildNumber """ **Anti-Pattern:** * Using arbitrary version numbers without following a standard like Semantic Versioning. * Manually updating version numbers rather than automating the process with CI/CD. ### 1.4. Static Analysis **Do This**: Integrate static analysis tools to identify potential code quality, security, and performance issues early in the lifecycle. **Don't Do This**: Ignore analyzer warnings and lints. **Why**: Static analysis helps maintain high code quality and reduces the risk of runtime errors. **Example**: Create an "analysis_options.yaml" file in the root of your project: """yaml include: package:flutter_lints/flutter.yaml linter: rules: avoid_print: true prefer_const_constructors: true always_use_package_imports: true analyzer: exclude: [build/**, .github/**] strong-mode: implicit-casts: false implicit-dynamic: false """ Run static analysis: "flutter analyze" **Anti-Pattern**: * Not configuring the analysis options or using a very permissive configuration, therefore missing out on important code quality improvements. * Treating analyzer warnings as optional and not fixing them. ## 2. Production Considerations ### 2.1. Optimizing Build Size **Do This:** Optimize the build size of your Flutter applications by using tree shaking, code splitting, and asset compression. **Don't Do This:** Include unnecessary dependencies or assets in the application bundle. **Why:** Smaller build sizes result in faster downloads and installations, improving the user experience.. **Example:** * Enable tree shaking by ensuring you import only the necessary modules. * Use tools like "flutter_native_splash" to generate efficient splash screens. * Compress images and other assets. * Use deferred loading for routes or features that are not immediately needed. """dart //Example of Deferred Loading - Flutter 3.13+ Future<void> loadMyLibrary() async { await myLibrary.loadLibrary(); // Now you can use functions and classes from myLibrary myLibrary.someFunction(); } """ **Anti-Pattern:** * Including large asset files without compression. * Importing entire libraries when only a small subset is needed. ### 2.2. Code Obfuscation **Do This:** Obfuscate your Dart code to make it harder to reverse engineer. **Don't Do This:** Skip code obfuscation, especially for applications with sensitive logic. **Why:** Code obfuscation protects your intellectual property and makes it harder for malicious actors to understand and modify your code. **Example:** """bash flutter build apk --obfuscate --split-per-abi flutter build ios --obfuscate --export-options-plist=ExportOptions.plist """ For iOS, the "ExportOptions.plist" file should contain the release configuration. For Android, use the "release" build type in Gradle configuration. **Anti-Pattern:** * Relying solely on obfuscation as a security measure. Use multiple layers of security. * Not testing obfuscated builds to ensure they function correctly. While obfuscation makes reverse engineering harder, it's essential for maintaining app integrity. ### 2.3. Error Handling and Logging **Do This:** Implement robust error handling and logging mechanisms to capture and report errors in production. Use error reporting services like Sentry or Firebase Crashlytics. Implement structured logging to provide better traceability. Always sanitize logs to avoid leaking user PII data. **Don't Do This:** Allow unhandled exceptions to crash the application or fail to log important events. Relying on "print" statements in production code. **Why:** Effective error handling and logging allow you to identify and resolve issues quickly, improving application stability. **Example:** """dart import 'package:flutter/foundation.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; Future<void> main() async { await SentryFlutter.init( (options) { options.dsn = 'YOUR_SENTRY_DSN'; }, appRunner: () => runApp(MyApp()), ); } class MyWidget extends StatelessWidget { void doSomething() { try { // Risky operation throw Exception('Something went wrong!'); } catch (exception, stackTrace) { Sentry.captureException( exception, stackTrace: stackTrace, ); debugPrint('Error occurred: $exception'); } } @override Widget build(BuildContext context) { return ElevatedButton( onPressed: doSomething, child: Text('Trigger Error'), ); } } """ **Anti-Pattern:** * Logging sensitive user data without sanitization. * Not using an error reporting service and relying solely on local logs. * Using generic "catch" blocks without handling specific exception types. ### 2.4. Performance Monitoring **Do This:** Monitor the performance of your Flutter applications in production using tools like Firebase Performance Monitoring. **Don't Do This:** Neglect performance monitoring, which can lead to a poor user experience. **Why:** Performance monitoring helps you identify and address performance bottlenecks, ensuring smooth and responsive applications. **Example:** Integrate Firebase Performance Monitoring: """dart import 'package:firebase_performance/firebase_performance.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(); runApp(MyApp()); } class MyWidget extends StatelessWidget { @override Widget build(BuildContext context) { return ElevatedButton( onPressed: () async { final trace = FirebasePerformance.instance.newTrace('button_press'); await trace.start(); // Perform some operation await Future.delayed(Duration(seconds: 2)); await trace.stop(); }, child: Text('Press Me'), ); } } """ **Anti-Pattern:** * Waiting until users complain about performance issues before investigating. * Not setting up alerts for critical performance metrics. ### 2.5. Feature Flags **Do This:** Employ feature flags (also known as feature toggles) to enable or disable features remotely without redeploying the application. **Don't Do This:** Deploy new features directly to all users without testing or gradual rollout. **Why:** Feature flags allow you to test new features with a subset of users, roll back features quickly if issues arise, and personalize the user experience. **Example:** Use a package like "fflags": """dart // main.dart import 'package:flutter/material.dart'; import 'package:fflags/fflags.dart'; void main() async { final flags = await FFlags.fromJsonFile( 'assets/flags.json', ); runApp(MyApp(flags)); } class MyApp extends StatelessWidget { final FFlags flags; MyApp(this.flags); @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar(title: Text('Feature Flags Example')), body: Feature( name: 'new_ui', child: NewUI(), fallback: OldUI(), flags: flags, ), ), ); } } class NewUI extends StatelessWidget { @override Widget build(BuildContext context) { return Center(child: Text('New UI is enabled!')); } } class OldUI extends StatelessWidget { @override Widget build(BuildContext context) { return Center(child: Text('Old UI is enabled!')); } } """ "assets/flags.json": """json { "new_ui": "true" } """ **Anti-Pattern:** * Using feature flags for long-term branching of code. Feature flags should be temporary. * Not cleaning up feature flag code after a feature has been fully rolled out or removed. ## 3. Platform-Specific Considerations ### 3.1. Android **Do This:** Leverage Android App Bundles for dynamic feature delivery and optimized APK sizes. Implement proper handling of Android lifecycle events, especially pausing and resuming the app. Target the latest Android API level to take advantage of new features and security improvements. **Don't Do This:** Create monolithic APKs that are unnecessarily large. Ignore Android-specific best practices for background tasks and permissions. **Why:** Android App Bundles reduce the size of the downloaded APK and allow for dynamic feature delivery, enhancing the user experience. **Example:** """bash flutter build appbundle --target-platform android-arm64,android-arm,android-x64 """ Ensure your "build.gradle" is correctly configured for release builds. **Anti-Pattern:** * Failing to handle Android lifecycle events, leading to data loss or unexpected behavior. * Not optimizing APK size, resulting in larger downloads and installations. ### 3.2. iOS **Do This:** Use Xcode Cloud for CI/CD and simplify the build process. Optimize for iOS-specific hardware and software features. Code sign your application correctly and follow Apple's guidelines for app submission. **Don't Do This:** Skip code signing or use incorrect provisioning profiles. Ignore iOS-specific performance considerations. **Why:** Proper code signing is essential for distributing iOS applications. **Example:** Configure code signing in Xcode and use "fastlane" for automating the deployment process. """ruby # Fastfile lane :beta do match(type: "appstore") increment_build_number(xcodeproj: "Runner.xcodeproj") build_app(scheme: "Runner", export_method: "app-store") upload_to_testflight end """ **Anti-Pattern:** * Misconfiguring code signing, which can prevent the app from running on devices. * Using development certificates for production builds. ### 3.3. Web **Do This:** Optimize your Flutter web application for search engine optimization (SEO) by using proper meta tags, semantic HTML, and server-side rendering (SSR), if needed. Use a content delivery network (CDN) to serve static assets. **Don't Do This:** Ignore SEO considerations for web apps or fail to optimize performance for web browsers. **Why:** SEO and performance optimization are crucial for attracting and retaining web users. **Example:** Pre-render your Flutter web app using tools like "rendertron" or use server-side rendering frameworks. """html <!-- index.html --> <head> <meta charset="UTF-8"> <meta name="description" content="My Flutter Web App"> <meta name="keywords" content="Flutter, Web, Application"> <title>My Flutter Web App</title> </head> """ **Anti-Pattern:** * Creating a web app without considering SEO, resulting in low visibility. * Not pre-rendering content, resulting in a long initial load time and poor SEO. ## 4. DevOps Practices ### 4.1. Infrastructure as Code (IaC) **Do This:** Define and manage your infrastructure using code, such as Terraform or CloudFormation. **Don't Do This:** Manually configure infrastructure resources, which is error-prone and difficult to maintain. **Why:** IaC allows you to automate infrastructure provisioning and management, ensuring consistency and repeatability. **Example:** Use Terraform to create a Firebase project and configure hosting: """terraform resource "google_project" "project" { name = "my-flutter-project" project_id = "my-flutter-project-id" org_id = "your-org-id" } resource "google_project_service_identity" "gcp_sa" { project = google_project.project.project_id provider = google-beta service = "firebase.googleapis.com" } """ **Anti-Pattern:** * Manually provisioning infrastructure, making it difficult to reproduce and manage. * Not version controlling infrastructure configurations. ### 4.2. Monitoring and Alerting **Do This:** Implement comprehensive monitoring and alerting to track the health and performance of your applications and infrastructure. Use tools like Prometheus and Grafana, or cloud-specific solutions like Google Cloud Monitoring or AWS CloudWatch. Setup alerts for failures, performance regressions and security vulnerabilities. **Don't Do This:** Fail to monitor your application, which can lead to undetected issues and downtime. **Why:** Monitoring and alerting enable you to detect and respond to issues quickly, minimizing downtime and ensuring a smooth user experience. **Example:** Configure CloudWatch metrics and alarms: """json { "AlarmName": "HighCPUUtilization", "AlarmDescription": "Alarm when CPU Utilization exceeds 70%", "MetricName": "CPUUtilization", "Namespace": "AWS/EC2", "Statistic": "Average", "Period": 300, "EvaluationPeriods": 1, "Threshold": 70.0, "ComparisonOperator": "GreaterThanThreshold", "TreatMissingData": "notBreaching", "AlarmActions": [ "arn:aws:sns:us-west-2:123456789012:MyTopic" ] } """ **Anti-Pattern:** * Not setting up alerts for critical issues, resulting in delayed response times. * Ignoring alerts, leading to prolonged downtime and user dissatisfaction. ### 4.3. Security best practices **Do This:** Implement security best practices for your Flutter applications, including: * Secure data storage * Secure network communication * Authentication and authorization * Regular security audits **Don't Do This:** Neglect security considerations, which can lead to data breaches and compromise user privacy. **Why:** Security is paramount for protecting user data and maintaining trust. **Example:** Use secure storage libraries to encrypt sensitive data: """dart import 'package:flutter_secure_storage/flutter_secure_storage.dart'; final _storage = FlutterSecureStorage(); Future<void> saveValue(String key, String value) async { await _storage.write(key: key, value: value); } Future<String?> readValue(String key) async { return await _storage.read(key: key); } """ Incorporate network security protocols like HTTPS and TLS 1.3. **Anti-Pattern:** * Storing sensitive data in plain text. * Using weak encryption algorithms. By adhering to these deployment and DevOps standards, Flutter development teams can build, deploy, and maintain high-quality applications that meet the needs of their users. It is important to regularly revisit and update these standards to keep pace with the evolving Flutter ecosystem and the latest best practices.
# Tooling and Ecosystem Standards for Flutter This document outlines the standards for tooling and ecosystem usage in Flutter projects. Adhering to these standards improves developer productivity, ensures code quality, and promotes consistency across projects. ## 1. Development Environment Setup ### 1.1 Flutter SDK Management **Standard:** Use a version manager for Flutter SDK. **Do This:** Use "fvm" (Flutter Version Management) or "asdf" to manage multiple Flutter SDK versions. **Don't Do This:** Rely on a globally installed Flutter SDK without version isolation. **Why:** Enables project-specific Flutter SDK versions, avoiding compatibility issues and ensuring reproducibility across different environments. """yaml # fvm config (fvm_config.json) { "flutterSdkVersion": "3.16.0" } """ **Anti-pattern:** Forgetting to specify the Flutter SDK version for a project, leading to inconsistencies. ### 1.2 IDE and Editor Configuration **Standard:** Use VS Code or Android Studio with Flutter extensions. **Do This:** * Install the official Flutter and Dart plugins. * Configure format-on-save. * Use code snippets to generate boilerplate code. **Don't Do This:** Use basic text editors without Flutter-specific support. **Why:** Enhances developer productivity with features like code completion, debugging, hot reload, and code formatting. """json // VS Code settings.json { "editor.formatOnSave": true, "dart.flutterHotRestartOnSave": true, "files.autoSave": "afterDelay", "files.autoSaveDelay": 500 } """ **Anti-pattern:** Ignoring IDE warnings and suggestions, which can lead to potential bugs. ### 1.3 Linting and Static Analysis **Standard:** Enable and customize Dart linters. **Do This:** * Include a "analysis_options.yaml" file in the project root. * Enable "pedantic" or "flutter_lints" rule set. * Customize rules based on project needs. * Address all linting issues before committing code. **Don't Do This:** Disable all linting rules or ignore reported issues without addressing them. **Why:** Enforces consistent code style, identifies potential errors, and improves code maintainability. """yaml # analysis_options.yaml include: package:flutter_lints/flutter.yaml linter: rules: avoid_print: true prefer_const_constructors: true always_put_required_named_parameters_first: true # Recommended style """ **Anti-pattern:** Overriding useful linting rules without proper justification. For instance, disabling "avoid_print" when the project should be using logging instead. ## 2. Package Management ### 2.1 Utilizing pubspec.yaml **Standard:** Declare explicit dependencies in "pubspec.yaml". **Do This:** * Specify version constraints for all dependencies. * Use semantic versioning constraints (e.g., "^1.2.3"). * Run "flutter pub get" after modifying "pubspec.yaml". * Organize dependencies logically in different sections (e.g., dependencies, dev_dependencies). **Don't Do This:** Use wildcard versions ("any") or avoid specifying version constraints. **Why:** Ensures predictable dependency resolution and avoids breaking compilation or runtime behavior due to unexpected library updates. """yaml dependencies: flutter: sdk: flutter cupertino_icons: ^1.0.2 http: ^0.13.5 intl: ^0.17.0 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^2.0.0 """ **Anti-pattern:** Adding dependencies without considering their reliability or maintainability. ### 2.2 Choosing Packages **Standard:** Select packages with high quality and active maintenance. **Do This:** * Check the package's pub.dev score (likes, popularity, health). * Review the package's documentation and examples. * Examine the package's source code and issue tracker on GitHub or GitLab. * Prefer packages officially endorsed by the Flutter team or popular in the Flutter community. **Don't Do This:** Rely on abandoned or poorly maintained packages. **Why:** Reduces the risk of bugs, security vulnerabilities, and compatibility issues. **Recommended Packages:** * State Management: "provider", "flutter_bloc", "riverpod" * Networking: "http", "dio", "chopper" * JSON Serialization: "json_annotation", "json_serializable", "build_runner" * Image Handling: "cached_network_image", "flutter_svg" * Local Storage: "shared_preferences", "sqflite" * UI Components: "flutter_screenutil", "intl" (internationalization) * Animations: "animations" package, "flutter_animate" * Dependency Injection: "get_it", "injectable" ### 2.3 Handling Native Dependencies **Standard:** Manage platform-specific dependencies carefully. **Do This:** * Document native dependencies clearly in the "README.md" file. * Provide instructions for setting up native dependencies for each platform (Android, iOS, web, etc.). * Use conditional imports and platform checks when necessary. **Don't Do This:** Neglect platform-specific configurations or create unnecessary platform dependencies. **Why:** Ensures platform-specific code is handled correctly and avoid build or runtime errors. Example of conditional import: """dart import 'package:flutter/foundation.dart' show kIsWeb; import 'package:universal_io/io.dart'; // handles web and native Future<String> getPlatformSpecificPath() async { if (kIsWeb) { return 'web/assets/data.json'; } else if (Platform.isAndroid) { return '/data/user/0/com.example.app/files/data.json'; } else if (Platform.isIOS) { return '/var/mobile/Containers/Data/Application/.../Documents/data.json'; } else { throw UnsupportedError('Unsupported platform'); } } """ **Anti-pattern:** Forgetting platform-specific setup instructions, making it difficult for other developers to run the app. ## 3. State Management Solutions ### 3.1 Selecting State Management Approach **Standard:** Choose a state management solution appropriate for the app's complexity and team's familiarity. **Do This:** * For simple apps: Use "setState" or "ValueNotifier". * For medium-sized apps: Use "Provider" or "Riverpod". * For complex apps: Use "Bloc/Cubit" or "Redux". **Don't Do This:** Overuse complex state management solutions for simple apps or choose unfamiliar solutions without proper training. **Why:** Matching the complexity of the state management solution to the application avoids unnecessary overhead and learning curves. ### 3.2 Implementing Provider Pattern **Standard:** Use "Provider" for simple to medium state management needs. **Do This:** * Use "ChangeNotifierProvider" for mutable state. * Use "StreamProvider" for streams. * Use "FutureProvider" for futures. * Prefer "Consumer" or "Selector" for accessing state in widgets. **Don't Do This:** Directly access "Provider" without using "Consumer" or "Selector" unnecessarily. **Why:** Promotes efficient rebuilds and reduces unnecessary widget updates. """dart class Counter with ChangeNotifier { int _count = 0; int get count => _count; void increment() { _count++; notifyListeners(); } } void main() { runApp( ChangeNotifierProvider( create: (context) => Counter(), child: MyApp(), ), ); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar(title: Text('Counter App')), body: Center( child: Consumer<Counter>( builder: (context, counter, child) => Text( 'Count: ${counter.count}', style: TextStyle(fontSize: 24), ), ), ), floatingActionButton: FloatingActionButton( onPressed: () => Provider.of<Counter>(context, listen: false).increment(), child: Icon(Icons.add), ), ), ); } } """ **Anti-pattern:** Calling "notifyListeners()" unnecessarily in "ChangeNotifier", causing excessive widget rebuilds. ### 3.3 Implementing Bloc/Cubit Pattern **Standard:** Use "Bloc/Cubit" for complex state management and business logic separation. **Do This:** * Define clear events and states. * Use "emit" to update the state. * Use "BlocProvider" to provide the bloc/cubit to the widget tree. * Use "BlocBuilder" or "BlocListener" to react to state changes. * Leverage "flutter_bloc" library for seamless integration and boilerplate reduction. **Don't Do This:** Put business logic directly into widgets or emit state directly from UI events. **Why:** Separates presentation from business logic, making code more testable and maintainable. """dart // Counter Event abstract class CounterEvent {} class IncrementCounterEvent extends CounterEvent {} // Counter State class CounterState { final int count; CounterState({required this.count}); } // Counter Bloc class CounterBloc extends Bloc<CounterEvent, CounterState> { CounterBloc() : super(CounterState(count: 0)) { on<IncrementCounterEvent>((event, emit) { emit(CounterState(count: state.count + 1)); }); } } class CounterPage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( create: (context) => CounterBloc(), child: Scaffold( appBar: AppBar(title: Text('Counter App')), body: Center( child: BlocBuilder<CounterBloc, CounterState>( builder: (context, state) { return Text( 'Count: ${state.count}', style: TextStyle(fontSize: 24), ); }, ), ), floatingActionButton: FloatingActionButton( onPressed: () => BlocProvider.of<CounterBloc>(context).add(IncrementCounterEvent()), child: Icon(Icons.add), ), ), ); } } """ **Anti-pattern:** Emitting the same state multiple times in a short period, leading to unnecessary rebuilds ## 4. Asynchronous Programming ### 4.1 Using async/await **Standard:** Use "async/await" syntax for asynchronous operations. **Do This:** * Mark asynchronous functions with "async". * Use "await" to wait for "Future" values. * Handle exceptions using "try/catch" blocks. **Don't Do This:** Use ".then()" and ".catchError()" for asynchronous operations unless absolutely necessary. **Why:** Improves code readability and simplifies asynchronous logic. """dart Future<String> fetchData() async { try { final response = await http.get(Uri.parse('https://example.com/data')); if (response.statusCode == 200) { return response.body; } else { throw Exception('Failed to load data'); } } catch (e) { print('Error: $e'); return 'Error occurred'; } } """ **Anti-pattern:** Ignoring potential exceptions in asynchronous operations. ### 4.2 Handling Streams **Standard:** Use "StreamBuilder" for displaying data from streams. **Do This:** * Use "StreamController" to create and manage streams. * Handle errors and completion events in streams. * Dispose of streams and stream controllers when they are no longer needed. **Don't Do This:** Leak streams by not closing stream controllers, causing memory leaks. """dart class MyWidget extends StatefulWidget { @override _MyWidgetState createState() => _MyWidgetState(); } class _MyWidgetState extends State<MyWidget> { final _streamController = StreamController<int>(); @override void initState() { super.initState(); // Simulate data being added to the stream Future.delayed(Duration(seconds: 1), () => _streamController.sink.add(1)); Future.delayed(Duration(seconds: 2), () => _streamController.sink.add(2)); Future.delayed(Duration(seconds: 3), () => _streamController.sink.add(3)); Future.delayed(Duration(seconds: 4), () => _streamController.sink.close()); // Close the stream } @override Widget build(BuildContext context) { return StreamBuilder<int>( stream: _streamController.stream, builder: (context, snapshot) { if (snapshot.hasData) { return Text('Data: ${snapshot.data}'); } else if (snapshot.hasError) { return Text('Error: ${snapshot.error}'); } else if (snapshot.connectionState == ConnectionState.done) { return Text('Stream is done'); } else { return CircularProgressIndicator(); } }, ); } @override void dispose() { _streamController.close(); // Important: close the stream super.dispose(); } } """ **Anti-pattern:** Not handling the "ConnectionState.done" state in a "StreamBuilder", causing unexpected behavior when the stream is closed. ## 5. Logging ### 5.1 Using a Logging Package **Standard:** Utilize a logging package for detailed and structured logging. **Do This:** * Implement a logging library such as "logger" or "logging". * Configure logging levels (e.g., "debug", "info", "warning", "error"). * Use structured logging for easier analysis. **Don't Do This:** Solely rely on "print" statements for logging in production code. **Why:** Provides better control over logging output and facilitates debugging and monitoring. """dart import 'package:logger/logger.dart'; final logger = Logger(); void myMethod() { logger.d("This is a debug message"); logger.i("This is an info message"); logger.w("This is a warning message"); logger.e("This is an error message"); logger.wtf("What a terrible failure!"); } """ **Anti-pattern:** Logging sensitive information such as passwords or API keys. ## 6. Testing ### 6.1 Writing Unit Tests **Standard:** Write unit tests to verify individual components. **Do This:** * Use "flutter_test" package for unit testing. * Write tests for all critical business logic. * Use mocks and stubs to isolate components under test with "mockito". **Don't Do This:** Skip unit tests for complex or critical code. **Why:** Improves code reliability and prevents regressions. """dart import 'package:flutter_test/flutter_test.dart'; import 'package:my_app/counter.dart'; void main() { group('Counter', () { test('Counter value should start at 0', () { expect(Counter().value, 0); }); test('Counter value should be incremented', () { final counter = Counter(); counter.increment(); expect(counter.value, 1); }); test('Counter value should be decremented', () { final counter = Counter(); counter.decrement(); expect(counter.value, -1); }); }); } """ **Anti-pattern:** Writing tests that are too tightly coupled to the implementation, making them brittle and difficult to maintain. ### 6.2 Writing Widget Tests **Standard:** Write widget tests to verify UI components. **Do This:** * Use "flutter_test" package for widget testing. * Use "WidgetTester" to interact with widgets. * Verify widget properties and behavior based on user interactions. **Don't Do This:** Skip widget tests, leaving UI components untested. **Why:** Verifies that UI components render correctly and respond to user input as expected. """dart import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:my_app/main.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. await tester.pumpWidget(MyApp()); // Verify that our counter starts at 0. expect(find.text('0'), findsOneWidget); expect(find.text('1'), findsNothing); // Tap the '+' icon and trigger a frame. await tester.tap(find.byIcon(Icons.add)); await tester.pump(); // Verify that our counter has incremented. expect(find.text('0'), findsNothing); expect(find.text('1'), findsOneWidget); }); } """ **Anti-pattern:** Writing widget tests that don't accurately simulate user interactions. ### 6.3 Writing Integration Tests **Standard:** Write integration tests to verify interactions between different parts of the app. **Do This:** * Use "integration_test package". * Simulate real user scenarios * Verify that the app behaves correctly when interacting with external services. **Don't Do This:** Skip integration tests, leaving the overall app functionality untested. **Why:** Ensures that different parts of the app work together correctly and verifies that the app functions as expected in a real-world environment. ## 7. Internationalization (i18n) and Localization (l10n) ### 7.1 Using "flutter_localizations" **Standard:** Employ "flutter_localizations" for internationalization. **Do This:** * Add "flutter_localizations" and "intl" in "pubspec.yaml". * Create "l10n.yaml" to define the localization settings. * Use "AppLocalizations" class to access localized strings. * Generate localized strings from ARB files using "flutter gen-l10n". **Don't Do This:** Hard-code text directly in widgets without using localization. **Why:** Supports multiple languages and makes the app accessible to a wider audience. """yaml # pubspec.yaml dependencies: flutter_localizations: sdk: flutter intl: any flutter: generate: true """ """yaml # l10n.yaml arb-dir: lib/l10n template-arb-file: app_en.arb output-localization-file: app_localizations.dart """ """dart // lib/l10n/app_en.arb { "helloWorld": "Hello World", "@helloWorld": { "description": "The conventional newborn greeting" } } """ Accessing the localized string: """dart import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class MyWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Text(AppLocalizations.of(context)!.helloWorld); } } """ **Anti-pattern:** Lack of support for right-to-left (RTL) languages. ## 8. Version Control and Continuous Integration ### 8.1 Using Git **Standard:** Use Git for version control with a clear branching strategy. **Do This:** * Use feature branches for developing new features. * Use pull requests for code review and merging. * Use meaningful commit messages. * Follow a branching strategy like Gitflow or GitHub Flow. **Don't Do This:** Commit directly to the main branch without proper code review. **Why:** Enables collaboration, tracks changes, and simplifies code management. ### 8.2 Implementing CI/CD **Standard:** Set up a CI/CD pipeline to automate building, testing, and deploying the app. **Do This:** * Use CI/CD tools like GitHub Actions, GitLab CI, or Bitrise. * Automate running tests and linting on every commit. * Automate building and deploying the app to different environments. **Don't Do This:** Manually build, test, and deploy the app. **Why:** Reduces the risk of errors, accelerates the development lifecycle, and ensures consistent builds and deployments. ## 9. Code Generation ### 9.1 Utilizing Code Generation Tools **Standard:** Leverage code generation tools to reduce boilerplate and improve maintainability. **Do This:** * Use "json_serializable" and "build_runner" for JSON serialization. * Use "freezed" for immutable data classes. * Use "injectable" or "get_it" for dependency injection. * Use code generation for routing with packages like "auto_route". * Use "flutter_gen" to access assets, fonts, and colors in a type-safe way. **Don't Do This:** Manually write repetitive code that can be generated automatically. **Why:** Reduces the amount of boilerplate code, improves code consistency, and minimizes the risk of errors. """dart // Example using freezed import 'package:freezed_annotation/freezed_annotation.dart'; part 'user.freezed.dart'; @freezed class User with _$User { const factory User({ required String id, required String name, String? email, }) = _User; } """ By running "flutter pub run build_runner build", the "user.freezed.dart" file is automatically generated, providing all necessary methods for working with immutable data. **Anti-pattern:** Avoiding code generation tools because of the initial setup complexity - the long-term benefits far outweigh the initial cost.