Flutter Best Practices

Write clean, maintainable, and efficient Flutter code

✨ Why Follow Best Practices?

Following Flutter best practices helps you write clean, maintainable, and scalable code. These guidelines improve app performance, reduce bugs, and make collaboration easier with other developers.


// Good practice: Use const constructors
const Text('Hello Flutter');

// Avoid: Non-const when possible
Text('Hello Flutter');
                                    

Best Practice Categories

📁

Project Structure

Organize your Flutter project with clear folder structure, separating widgets, screens, models, and services for better maintainability and scalability.

Folders Organization
🎨

Widget Design

Create reusable, composable widgets with single responsibility. Break complex UIs into smaller widgets for better code reusability and testing.

Reusability Composition

State Management

Choose appropriate state management solutions based on app complexity. Use Provider, Riverpod, or Bloc for scalable state handling across your application.

Provider Bloc
🔒

Code Quality

Write clean, readable code with proper naming conventions, comments, and documentation. Use linting rules and format code consistently throughout your project.

Linting Formatting

🔹 Project Structure Best Practices

🔸 Recommended Folder Structure

lib/
├── main.dart
├── screens/          # App screens/pages
│   ├── home_screen.dart
│   └── profile_screen.dart
├── widgets/          # Reusable widgets
│   ├── custom_button.dart
│   └── user_card.dart
├── models/           # Data models
│   └── user_model.dart
├── services/         # API, database services
│   └── api_service.dart
├── providers/        # State management
│   └── user_provider.dart
└── utils/            # Helper functions
    └── constants.dart

🔸 Separate Concerns

Keep business logic separate from UI code. Use services for API calls, providers for state, and widgets for UI only.

// Good: Separate service
class ApiService {
  Future> fetchUsers() async {
    // API logic here
  }
}

// Good: Widget uses service
class UserList extends StatelessWidget {
  final ApiService apiService = ApiService();
  
  @override
  Widget build(BuildContext context) {
    // UI code only
  }
}

🔹 Widget Best Practices

🔸 Use Const Constructors

Using const constructors improves performance by preventing unnecessary widget rebuilds. Flutter can reuse const widgets instead of creating new ones.

// Good: Use const
const Text('Hello');
const Icon(Icons.home);
const SizedBox(height: 20);

// Avoid: Non-const when possible
Text('Hello');
Icon(Icons.home);

🔸 Break Down Large Widgets

// Bad: One large widget
class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          // 100+ lines of code here
        ],
      ),
    );
  }
}

// Good: Smaller, reusable widgets
class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          HeaderWidget(),
          ContentWidget(),
          FooterWidget(),
        ],
      ),
    );
  }
}

🔸 Extract Reusable Widgets

// Create reusable custom widgets
class CustomButton extends StatelessWidget {
  final String text;
  final VoidCallback onPressed;
  
  const CustomButton({
    required this.text,
    required this.onPressed,
  });
  
  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: onPressed,
      child: Text(text),
    );
  }
}

// Use it anywhere
CustomButton(
  text: 'Click Me',
  onPressed: () => print('Clicked'),
)

🔹 Naming Conventions

🔸 Follow Dart Style Guide

// Classes: UpperCamelCase
class UserProfile {}
class HomeScreen {}

// Variables & functions: lowerCamelCase
String userName = 'John';
void fetchData() {}

// Constants: lowerCamelCase
const double padding = 16.0;
const String apiUrl = 'https://api.example.com';

// Private members: prefix with underscore
class _PrivateWidget extends StatelessWidget {}
String _privateVariable = 'secret';

🔸 Meaningful Names

// Good: Descriptive names
String userEmailAddress;
void calculateTotalPrice() {}
bool isUserLoggedIn;

// Avoid: Unclear names
String e;
void calc() {}
bool flag;

🔹 State Management Best Practices

🔸 Choose the Right Solution

  • setState: Simple, local state in single widget
  • Provider: App-wide state, easy to learn
  • Riverpod: Modern, type-safe Provider alternative
  • Bloc: Complex apps, predictable state changes

🔸 Keep State Minimal

// Good: Only store necessary state
class CounterState {
  final int count;
  CounterState(this.count);
}

// Avoid: Storing derived values
class CounterState {
  final int count;
  final int doubleCount; // Can be calculated
  final bool isEven;     // Can be calculated
}

🔸 Use Provider Example

// Define provider
class CounterProvider extends ChangeNotifier {
  int _count = 0;
  int get count => _count;
  
  void increment() {
    _count++;
    notifyListeners();
  }
}

// Use in widget
class CounterWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final counter = Provider.of(context);
    return Text('Count: ${counter.count}');
  }
}

🔹 Error Handling

🔸 Handle Async Errors

// Good: Proper error handling
Future fetchData() async {
  try {
    final response = await http.get(Uri.parse(url));
    if (response.statusCode == 200) {
      // Process data
    } else {
      throw Exception('Failed to load data');
    }
  } catch (e) {
    print('Error: $e');
    // Show error to user
  }
}

🔸 Validate User Input

// Use Form validation
TextFormField(
  validator: (value) {
    if (value == null || value.isEmpty) {
      return 'Please enter some text';
    }
    if (value.length < 6) {
      return 'Must be at least 6 characters';
    }
    return null;
  },
)

🔹 Performance Best Practices

🔸 Use ListView.builder for Long Lists

// Good: Lazy loading
ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) {
    return ListTile(title: Text(items[index]));
  },
)

// Avoid: Loading all items at once
ListView(
  children: items.map((item) => ListTile(title: Text(item))).toList(),
)

🔸 Optimize Images

// Use cached network images
CachedNetworkImage(
  imageUrl: 'https://example.com/image.jpg',
  placeholder: (context, url) => CircularProgressIndicator(),
  errorWidget: (context, url, error) => Icon(Icons.error),
)

// Specify image dimensions
Image.network(
  'url',
  width: 100,
  height: 100,
  fit: BoxFit.cover,
)

🔸 Avoid Expensive Operations in Build

// Bad: Heavy computation in build
@override
Widget build(BuildContext context) {
  final result = expensiveCalculation(); // Runs every rebuild
  return Text(result);
}

// Good: Compute once, store result
class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State {
  late String result;
  
  @override
  void initState() {
    super.initState();
    result = expensiveCalculation(); // Runs once
  }
  
  @override
  Widget build(BuildContext context) {
    return Text(result);
  }
}

🔹 Code Quality Practices

🔸 Enable Linting

# analysis_options.yaml
include: package:flutter_lints/flutter.yaml

linter:
  rules:
    - prefer_const_constructors
    - avoid_print
    - prefer_single_quotes
    - always_declare_return_types

🔸 Format Code Consistently

# Format all Dart files
flutter format lib/

# Format specific file
flutter format lib/main.dart

🔸 Write Comments and Documentation

/// Fetches user data from the API.
/// 
/// Returns a [User] object if successful.
/// Throws an [Exception] if the request fails.
Future fetchUser(String userId) async {
  // Implementation
}

// Use inline comments for complex logic
void complexFunction() {
  // Step 1: Validate input
  if (input.isEmpty) return;
  
  // Step 2: Process data
  final result = processData(input);
}

🔹 Testing Best Practices

🔸 Write Unit Tests

// test/counter_test.dart
import 'package:flutter_test/flutter_test.dart';

void main() {
  test('Counter increments', () {
    final counter = Counter();
    counter.increment();
    expect(counter.value, 1);
  });
}

🔸 Write Widget Tests

testWidgets('Button displays text', (WidgetTester tester) async {
  await tester.pumpWidget(
    MaterialApp(home: CustomButton(text: 'Click')),
  );
  
  expect(find.text('Click'), findsOneWidget);
});

🔹 Security Best Practices

🔸 Never Hardcode Secrets

// Bad: Hardcoded API key
const String apiKey = 'abc123secret';

// Good: Use environment variables
import 'package:flutter_dotenv/flutter_dotenv.dart';

final apiKey = dotenv.env['API_KEY'];

🔸 Validate All Inputs

Always validate and sanitize user inputs before processing or sending to backend services to prevent security vulnerabilities.

🧠 Test Your Knowledge

Which is better for performance?