Table of Contents

Naming

Class names

Use PascalCase, which starts with an uppercase letter.

class CarModel {
  final String modelName;
  CarModel(this.modelName);
}

class UserProfile {
  final int userId;
  final String userName;
  UserProfile(this.userId, this.userName);
}

class PaymentService {
  final String paymentGateway;
  PaymentService(this.paymentGateway);
}

Interface names

Use the same convention as classes. Avoid the I prefix unless it’s necessary for clarity or common in your codebase.

// Preferred
abstract class Storage {
  Future<void> store(String data);
  Future<String> retrieve();
}

abstract class Repository<T> {
  Future<T?> getById(int id);
  Future<void> save(T item);
}

// Only when necessary for clarity
abstract class IUserService {
  Future<User> getUser(int id);
}

Abstract class names

Follow the same conventions as class names, often using terms like Base or Abstract to indicate abstraction.

abstract class AbstractShape {
  double calculateArea();
}

abstract class BaseRepository<T> {
  Future<T?> findById(int id);
}

Functions and Methods

Function names: Use camelCase, starting with a lowercase letter.

Future<User> fetchUserData(int userId) async {
  // fetch user logic
}

String formatUserName(String name) {
  return name.trim().toLowerCase();
}

Extension methods: Also use camelCase, and the method name should make sense in the context of the extended class.

extension StringExtensions on String {
  bool get isEmailValid {
    return contains('@') && contains('.');
  }
}

extension IntExtensions on int {
  bool get isOdd => this % 2 != 0;
}

Variables

Variable names: Use camelCase for variables.

final String companyName = 'pelagornis';
final int totalCount = 100;
var currentUser = User();

Constants: Use camelCase for const variables, UPPER_CASE for compile-time constants.

// Compile-time constants
const String API_KEY = 'qwodciabs1';
const String BASE_URL = 'https://api.pelagornis.com';

// Runtime constants
final String databaseUrl = 'postgresql://localhost:5432/mydb';

Parameters

Function parameters: Use descriptive names in camelCase.

void setUserName(String userName) {
  print('User name set to: $userName');
}

Future<void> updateUserProfile({
  required int userId,
  String? name,
  String? email,
}) async {
  // Update logic
}

Boolean parameters: Use “is” or “has” for booleans to indicate state or possession.

bool isActive(User user) {
  return user.status == 'active';
}

Future<void> processData({
  required List<Data> data,
  bool isAsync = true,
  bool hasValidation = false,
}) async {
  // Processing logic
}

Dart Style Rules

No trailing spaces

Remove unnecessary trailing spaces from the end of lines.

Line Length

Limit lines to 120 characters for readability.

Indentation

Use 2 spaces per indentation level. Avoid using tabs.

Curly braces

Always use braces for conditionals and loops, even when the block is a single statement.

if (condition) {
  doSomething();
}

for (var item in items) {
  processItem(item);
}

Avoid nested lambdas

If a lambda expression is too nested, consider refactoring the code to improve readability.

// Instead of
final result = list.where((item) {
  return anotherList.any((otherItem) => otherItem == item);
});

// Use a helper function
bool isItemInList(Item item) => anotherList.contains(item);
final result = someList.where(isItemInList);

Visibility Modifiers

Use explicit visibility modifiers. Prefer private over default visibility.

class UserService {
  final String _apiKey;
  final ApiClient _client;

  UserService(this._apiKey) : _client = ApiClient(_apiKey);

  Future<User> _fetchUser(int id) async {
    // Private method
  }
}

Type Inference and Explicit Types

Prefer Dart’s type inference whenever possible, but provide explicit types when they improve readability or when dealing with complex types.

final totalAmount = 100.0; // inferred as double
final User user = fetchUser(); // explicit type when needed
final List<Map<String, dynamic>> complexData = fetchComplexData();

Dart Formatting Rules

Function declarations

Always use spaces between function parameters for better readability.

double calculatePrice(Item item, double discount) {
  return item.price - (item.price * discount);
}

Future<void> processUserData({
  required int userId,
  String? name,
  String? email,
  bool isActive = true,
}) async {
  // Function body
}

Single-line functions

If the function is a single expression, use arrow syntax.

bool isEven(int number) => number % 2 == 0;

String formatName(String name) => name.trim().toLowerCase();

Future<String> fetchData() async => await api.getData();

Multi-line functions

Use braces and align the body properly.

Future<List<User>> fetchUsers() async {
  final response = await api.getUsers();
  final users = response.map((json) => User.fromJson(json)).toList();
  return users;
}

Control Statements

Use proper formatting for control statements.

// if-else statements
if (user.isActive) {
  await processActiveUser(user);
} else if (user.isPending) {
  await processPendingUser(user);
} else {
  await processInactiveUser(user);
}

// switch statements
switch (user.status) {
  case UserStatus.active:
    return 'Active';
  case UserStatus.pending:
    return 'Pending';
  case UserStatus.inactive:
    return 'Inactive';
  default:
    return 'Unknown';
}

Data classes

Use classes with proper constructors for data holders.

class User {
  final int id;
  final String name;
  final String email;
  final bool isActive;

  const User({
    required this.id,
    required this.name,
    required this.email,
    this.isActive = true,
  });

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is User &&
          runtimeType == other.runtimeType &&
          id == other.id &&
          name == other.name &&
          email == other.email &&
          isActive == other.isActive;

  @override
  int get hashCode =>
      id.hashCode ^
      name.hashCode ^
      email.hashCode ^
      isActive.hashCode;
}

Modern Dart Features

Sealed Classes

Use sealed classes for representing restricted class hierarchies.

sealed class Result<T> {}

class Success<T> extends Result<T> {
  final T data;
  Success(this.data);
}

class Error<T> extends Result<T> {
  final Exception exception;
  Error(this.exception);
}

class Loading<T> extends Result<T> {}

// Usage
void handleResult(Result<User> result) {
  switch (result) {
    case Success<User>():
      handleSuccess(result.data);
    case Error<User>():
      handleError(result.exception);
    case Loading<User>():
      showLoading();
  }
}

Records

Use records for lightweight data structures.

// Simple records
(String, int) getUserInfo() => ('John', 25);
var (name, age) = getUserInfo();

// Named records
({String name, int age}) getUserInfoNamed() => (name: 'John', age: 25);
var userInfo = getUserInfoNamed();
print('${userInfo.name} is ${userInfo.age} years old');

// Records in collections
List<({String name, int age})> users = [
  (name: 'John', age: 25),
  (name: 'Jane', age: 30),
];

Pattern Matching

Use pattern matching for complex data handling.

// Switch expressions with patterns
String describeUser(User user) => switch (user) {
  User(id: 0, name: 'admin') => 'Administrator',
  User(isActive: true) => 'Active user: ${user.name}',
  User(isActive: false) => 'Inactive user: ${user.name}',
  _ => 'Unknown user',
};

// Pattern matching in if statements
if (user case User(id: var id, isActive: true) when id > 0) {
  print('Active user with ID: $id');
}

Extension Methods

Use extension methods to add functionality to existing classes.

extension StringExtensions on String {
  bool get isEmail => RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(this);

  String get capitalize => isEmpty ? this : '${this[0].toUpperCase()}${substring(1)}';

  String removeWhitespace() => replaceAll(RegExp(r'\s+'), '');
}

extension ListExtensions<T> on List<T> {
  T? get firstOrNull => isEmpty ? null : first;

  List<T> get unique => toSet().toList();

  void addIfNotNull(T? item) {
    if (item != null) add(item);
  }
}

Null Safety

Leverage Dart’s null safety features.

// Nullable and non-nullable types
String? nullableName;
String nonNullableName = 'John';

// Null-aware operators
String displayName = user?.name ?? 'Unknown';
int? length = user?.name?.length;

// Null assertion (use carefully)
String name = user!.name; // Throws if user is null

// Late initialization
late String lateInitializedName;
void initializeName() {
  lateInitializedName = 'John';
}

Asynchronous Programming

Basic Async/Await

Use async/await for asynchronous operations.

// Async functions
Future<User> fetchUserData(int userId) async {
  await Future.delayed(Duration(seconds: 1)); // Simulate network call
  return User(id: userId, name: 'User $userId');
}

// Error handling
Future<void> loadUserData() async {
  try {
    final user = await fetchUserData(123);
    updateUI(user);
  } catch (e) {
    handleError(e);
  }
}

Streams

Use streams for reactive programming.

Stream<User> observeUserUpdates() async* {
  while (true) {
    final user = await fetchLatestUser();
    yield user;
    await Future.delayed(Duration(seconds: 5));
  }
}

// Usage
void startObserving() {
  observeUserUpdates()
      .listen(
        (user) => updateUI(user),
        onError: (error) => handleError(error),
        onDone: () => print('Stream completed'),
      );
}

Future and Completer

Use Future and Completer for custom async operations.

class DataProcessor {
  final _completer = Completer<List<Data>>();

  Future<List<Data>> processData(List<Data> input) async {
    // Start processing
    _processInBackground(input);
    return _completer.future;
  }

  void _processInBackground(List<Data> input) {
    Timer(Duration(seconds: 2), () {
      final result = input.map((data) => data.process()).toList();
      _completer.complete(result);
    });
  }
}

Isolates

Use isolates for CPU-intensive tasks.

Future<List<int>> computeHeavyTask(List<int> data) async {
  return await Isolate.run(() {
    // CPU-intensive computation
    return data.map((x) => x * x * x).toList();
  });
}

// Using compute function for simpler cases
Future<List<int>> computeSimpleTask(List<int> data) async {
  return await compute(_processData, data);
}

List<int> _processData(List<int> data) {
  return data.map((x) => x * 2).toList();
}

Flutter Guidelines

Widget Composition

Break down complex widgets into smaller, focused components.

class UserProfilePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: UserProfileAppBar(),
      body: Column(
        children: [
          UserProfileHeader(),
          UserProfileContent(),
          UserProfileActions(),
        ],
      ),
    );
  }
}

State Management

Use appropriate state management patterns.

// Provider pattern
class UserProvider extends ChangeNotifier {
  User? _user;
  bool _isLoading = false;

  User? get user => _user;
  bool get isLoading => _isLoading;

  Future<void> loadUser(int id) async {
    _isLoading = true;
    notifyListeners();

    try {
      _user = await userService.getUser(id);
    } catch (e) {
      // Handle error
    } finally {
      _isLoading = false;
      notifyListeners();
    }
  }
}

// Riverpod pattern
final userProvider = FutureProvider.family<User, int>((ref, id) async {
  final userService = ref.read(userServiceProvider);
  return await userService.getUser(id);
});

Custom Widgets

Create reusable custom widgets.

class CustomButton extends StatelessWidget {
  final String text;
  final VoidCallback? onPressed;
  final ButtonStyle? style;
  final bool isLoading;

  const CustomButton({
    Key? key,
    required this.text,
    this.onPressed,
    this.style,
    this.isLoading = false,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: isLoading ? null : onPressed,
      style: style,
      child: isLoading
          ? SizedBox(
              width: 20,
              height: 20,
              child: CircularProgressIndicator(strokeWidth: 2),
            )
          : Text(text),
    );
  }
}

Use proper navigation patterns.

// Named routes
class AppRoutes {
  static const String home = '/';
  static const String profile = '/profile';
  static const String settings = '/settings';
}

// Navigation
Navigator.pushNamed(context, AppRoutes.profile, arguments: userId);

// With result
final result = await Navigator.pushNamed<bool>(
  context,
  AppRoutes.settings,
);

// Custom route transitions
Navigator.push(
  context,
  PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) => SettingsPage(),
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      return SlideTransition(
        position: animation.drive(
          Tween(begin: Offset(1.0, 0.0), end: Offset.zero),
        ),
        child: child,
      );
    },
  ),
);

Testing

Unit Testing

Write comprehensive unit tests.

import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';

@GenerateMocks([UserRepository])
void main() {
  group('UserService', () {
    late UserService userService;
    late MockUserRepository mockRepository;

    setUp(() {
      mockRepository = MockUserRepository();
      userService = UserService(mockRepository);
    });

    test('should return user when valid id provided', () async {
      // Given
      const userId = 123;
      final expectedUser = User(id: userId, name: 'John');
      when(mockRepository.findById(userId)).thenAnswer((_) async => expectedUser);

      // When
      final result = await userService.getUser(userId);

      // Then
      expect(result, equals(expectedUser));
      verify(mockRepository.findById(userId)).called(1);
    });

    test('should throw exception when user not found', () async {
      // Given
      const userId = 999;
      when(mockRepository.findById(userId)).thenAnswer((_) async => null);

      // When & Then
      expect(
        () => userService.getUser(userId),
        throwsA(isA<UserNotFoundException>()),
      );
    });
  });
}

Widget Testing

Test Flutter widgets properly.

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  group('CustomButton Widget Tests', () {
    testWidgets('should display text and handle tap', (WidgetTester tester) async {
      // Given
      bool wasTapped = false;

      // When
      await tester.pumpWidget(
        MaterialApp(
          home: Scaffold(
            body: CustomButton(
              text: 'Test Button',
              onPressed: () => wasTapped = true,
            ),
          ),
        ),
      );

      // Then
      expect(find.text('Test Button'), findsOneWidget);

      await tester.tap(find.byType(CustomButton));
      await tester.pump();

      expect(wasTapped, isTrue);
    });

    testWidgets('should show loading indicator when isLoading is true', (WidgetTester tester) async {
      // When
      await tester.pumpWidget(
        MaterialApp(
          home: Scaffold(
            body: CustomButton(
              text: 'Test Button',
              isLoading: true,
            ),
          ),
        ),
      );

      // Then
      expect(find.byType(CircularProgressIndicator), findsOneWidget);
      expect(find.text('Test Button'), findsNothing);
    });
  });
}

Integration Testing

Write integration tests for complete user flows.

import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  group('User Flow Integration Tests', () {
    testWidgets('complete user registration flow', (WidgetTester tester) async {
      // Start the app
      await tester.pumpWidget(MyApp());
      await tester.pumpAndSettle();

      // Navigate to registration
      await tester.tap(find.text('Register'));
      await tester.pumpAndSettle();

      // Fill registration form
      await tester.enterText(find.byKey(Key('email_field')), 'test@example.com');
      await tester.enterText(find.byKey(Key('password_field')), 'password123');
      await tester.enterText(find.byKey(Key('name_field')), 'John Doe');

      // Submit form
      await tester.tap(find.text('Register'));
      await tester.pumpAndSettle();

      // Verify success
      expect(find.text('Registration successful'), findsOneWidget);
    });
  });
}

Performance

Lazy Initialization

Use lazy initialization for expensive operations.

class ExpensiveService {
  late final List<Data> _heavyComputation;

  List<Data> get data {
    _heavyComputation ??= _computeHeavyData();
    return _heavyComputation;
  }

  List<Data> _computeHeavyData() {
    // Expensive computation
    return List.generate(1000000, (index) => Data(index));
  }
}

Efficient Collections

Use appropriate collection types for performance.

// Use Set for O(1) lookups
final Set<String> userIds = {'user1', 'user2', 'user3'};
bool isUserExists(String id) => userIds.contains(id);

// Use Map for key-value pairs
final Map<String, User> userCache = {};
User? getCachedUser(String id) => userCache[id];

// Use List for ordered data
final List<Event> events = [];
void addEvent(Event event) => events.add(event);

Memory Management

Be mindful of memory usage.

// Dispose resources properly
class DataController extends ChangeNotifier {
  StreamSubscription? _subscription;
  Timer? _timer;

  void startListening() {
    _subscription = dataStream.listen((data) {
      // Handle data
    });

    _timer = Timer.periodic(Duration(seconds: 1), (timer) {
      // Periodic task
    });
  }

  @override
  void dispose() {
    _subscription?.cancel();
    _timer?.cancel();
    super.dispose();
  }
}

// Use const constructors when possible
class AppColors {
  static const Color primary = Color(0xFF2196F3);
  static const Color secondary = Color(0xFF03DAC6);
  static const Color error = Color(0xFFB00020);
}