Contents

Flutter Best Practices

Quick Start

Installation & Version

Current version is Flutter 3.41.6.

macOS

# Download Flutter SDK
cd ~/Downloads
git clone https://github.com/flutter/flutter.git -b 3.41.6 --depth 1

# Add to PATH (edit ~/.zshrc)
export PATH="$PATH:$HOME/Downloads/flutter/bin"

# Verify installation
flutter doctor
flutter --version

Or use Homebrew:

brew install flutter
flutter doctor

Windows

  1. Download latest stable SDK from Flutter Downloads
  2. Extract to a directory (e.g., C:\src\flutter)
  3. Add flutter\bin to system PATH
  4. Open command prompt to verify:
flutter doctor
flutter --version

Linux (Ubuntu)

# Install dependencies
sudo apt update
sudo apt install -y curl git unzip xz-utils zip libglu1-mesa

# Download Flutter SDK
cd ~/development
git clone https://github.com/flutter/flutter.git -b 3.41.6 --depth 1

# Add to PATH
export PATH="$PATH:$HOME/development/flutter/bin"

# Verify
flutter doctor

Create Project

flutter create my_app --org com.example

cd my_app
flutter run

Project Structure

lib/
├── main.dart
├── app.dart
├── core/
│   ├── constants/
│   │   └── app_constants.dart
│   ├── theme/
│   │   └── app_theme.dart
│   ├── utils/
│   │   └── extensions.dart
│   └── errors/
│       └── failures.dart
├── data/
│   ├── models/
│   │   └── user_model.dart
│   ├── repositories/
│   │   └── user_repository_impl.dart
│   └── datasources/
│       └── user_remote_datasource.dart
├── domain/
│   ├── entities/
│   │   └── user.dart
│   ├── repositories/
│   │   └── user_repository.dart
│   └── usecases/
│       └── get_users.dart
└── presentation/
    ├── blocs/
    │   └── user/
    │       ├── user_bloc.dart
    │       ├── user_event.dart
    │       └── user_state.dart
    ├── pages/
    │   └── user_list_page.dart
    └── widgets/
        └── user_card.dart

Clean Architecture

domain/       # Business logic layer - pure Dart
data/         # Data layer - repository implementation
presentation/ # Presentation layer - UI and state management

Dart Basics

Null Safety

// Non-nullable type
String name = 'Alice';

// Nullable type
String? nullableName;

// Null check
if (nullableName != null) {
  print(nullableName.length); // Safe
}

// Null assertion (use carefully)
print(nullableName!.length);

// Null coalescing
String displayName = nullableName ?? 'Anonymous';

// Null assignment
nullableName ??= 'Default';

Async Programming

// Future
Future<String> fetchData() async {
  final response = await http.get(Uri.parse('https://api.example.com'));
  return response.body;
}

// Stream
Stream<int> countStream() async* {
  for (int i = 1; i <= 5; i++) {
    await Future.delayed(Duration(seconds: 1));
    yield i;
  }
}

// Usage
final stream = countStream();
stream.listen((value) => print('Count: $value'));

Collection Operations

// List
final numbers = [1, 2, 3, 4, 5];
final doubled = numbers.map((n) => n * 2).toList();
final sum = numbers.reduce((a, b) => a + b);
final filtered = numbers.where((n) => n > 2).toList();

// Map
final users = [
  {'name': 'Alice', 'age': 30},
  {'name': 'Bob', 'age': 25}
];
final names = users.map((u) => u['name'] as String).toList();

// Set
final unique = numbers.toSet();

// Collection if/for
final buildMode = true;
final items = [
  'Home',
  if (buildMode) 'Settings',
  for (var i in [1, 2]) 'Item $i',
];

State Management

flutter pub add flutter_riverpod

Basic Usage

import 'package:flutter_riverpod/flutter_riverpod.dart';

// Define provider
final counterProvider = StateProvider<int>((ref) => 0);

// Async provider
final userProvider = FutureProvider<User>((ref) async {
  final repository = ref.read(userRepositoryProvider);
  return repository.getUser(1);
});

// Usage
class CounterPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);

    return Scaffold(
      body: Center(child: Text('$count')),
      floatingActionButton: FloatingActionButton(
        onPressed: () => ref.read(counterProvider.notifier).state++,
        child: Icon(Icons.add),
      ),
    );
  }
}

NotifierProvider

class CounterNotifier extends Notifier<int> {
  @override
  int build() => 0;

  void increment() => state++;
  void decrement() => state--;
  void reset() => state = 0;
}

final counterProvider = NotifierProvider<CounterNotifier, int>(
  CounterNotifier.new,
);

// Usage
ref.read(counterProvider.notifier).increment();

Widget Conventions

Stateless vs Stateful

// Simple widget - use StatelessWidget
class UserCard extends StatelessWidget {
  final String name;
  final String email;

  const UserCard({
    super.key,
    required this.name,
    required this.email,
  });

  @override
  Widget build(BuildContext context) {
    return Card(
      child: ListTile(
        title: Text(name),
        subtitle: Text(email),
      ),
    );
  }
}

Widget Reuse

// Extract reusable widgets
class LoadingIndicator extends StatelessWidget {
  final String? message;

  const LoadingIndicator({super.key, this.message});

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          CircularProgressIndicator(),
          if (message != null) ...[
            SizedBox(height: 16),
            Text(message!),
          ],
        ],
      ),
    );
  }
}

const Constructors

// Use const to reduce rebuilds
class MyWidget extends StatelessWidget {
  const MyWidget({super.key, this.title});

  final String? title;

  @override
  Widget build(BuildContext context) {
    return Text(title ?? '');
  }
}

// Usage
const MyWidget(title: 'Hello') // const constructor

flutter pub add go_router

Configuration

import 'package:go_router/go_router.dart';

final router = GoRouter(
  initialLocation: '/',
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => HomePage(),
    ),
    GoRoute(
      path: '/users/:id',
      builder: (context, state) {
        final id = state.pathParameters['id']!;
        return UserDetailPage(id: int.parse(id));
      },
    ),
    GoRoute(
      path: '/settings',
      builder: (context, state) => SettingsPage(),
    ),
  ],
);

// Usage
context.go('/users/123');
context.push('/settings');

Networking

Dio

flutter pub add dio

Basic Usage

import 'package:dio/dio.dart';

class ApiClient {
  late final Dio _dio;

  ApiClient() {
    _dio = Dio(BaseOptions(
      baseUrl: 'https://api.example.com',
      connectTimeout: Duration(seconds: 10),
      receiveTimeout: Duration(seconds: 10),
    ));

    _dio.interceptors.add(LogInterceptor());
  }

  Future<Response<T>> get<T>(
    String path, {
    Map<String, dynamic>? queryParameters,
  }) {
    return _dio.get<T>(path, queryParameters: queryParameters);
  }

  Future<Response<T>> post<T>(
    String path, {
    dynamic data,
  }) {
    return _dio.post<T>(path, data: data);
  }
}

Local Storage

SharedPreferences

flutter pub add shared_preferences
import 'package:shared_preferences/shared_preferences.dart';

// Save
final prefs = await SharedPreferences.getInstance();
await prefs.setString('token', 'abc123');
await prefs.setInt('count', 42);

// Read
final token = prefs.getString('token');
final count = prefs.getInt('count');

// Delete
await prefs.remove('token');

SQLite

flutter pub add sqflite path
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';

class DatabaseHelper {
  static Database? _database;

  Future<Database> get database async {
    _database ??= await _initDatabase();
    return _database!;
  }

  Future<Database> _initDatabase() async {
    final path = join(await getDatabasesPath(), 'myapp.db');
    return openDatabase(
      path,
      version: 1,
      onCreate: (db, version) async {
        await db.execute('''
          CREATE TABLE users(
            id INTEGER PRIMARY KEY,
            name TEXT,
            email TEXT
          )
        ''');
      },
    );
  }
}

Testing

Unit Tests

import 'package:flutter_test/flutter_test.dart';

int add(int a, int b) => a + b;

void main() {
  group('add()', () {
    test('adds two positive numbers', () {
      expect(add(1, 2), 3);
    });

    test('adds negative numbers', () {
      expect(add(-1, -1), -2);
    });

    test('adds zero', () {
      expect(add(0, 0), 0);
    });
  });
}

Widget Tests

import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/widgets/counter.dart';

void main() {
  testWidgets('Counter displays initial value', (tester) async {
    await tester.pumpWidget(Counter());

    expect(find.text('0'), findsOneWidget);
  });

  testWidgets('Counter increments on tap', (tester) async {
    await tester.pumpWidget(Counter());

    await tester.tap(find.byIcon(Icons.add));
    await tester.pump();

    expect(find.text('1'), findsOneWidget);
  });
}

Performance Optimization

Avoid Unnecessary Rebuilds

// Use const widgets
const Text('Hello')

// Avoid creating new objects in build
class MyWidget extends StatelessWidget {
  final items = ['a', 'b', 'c']; // Class-level constant

  @override
  Widget build(BuildContext context) {
    return Column(
      children: items.map((item) => Text(item)).toList(),
    );
  }
}

ListView Optimization

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

// Use ListView only if scrolling is needed

Image Caching

// Use cached_network_image
flutter pub add cached_network_image

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