Flutter Best Practices
Contents
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 --versionOr use Homebrew:
brew install flutter
flutter doctorWindows
- Download latest stable SDK from Flutter Downloads
- Extract to a directory (e.g.,
C:\src\flutter) - Add
flutter\binto system PATH - Open command prompt to verify:
flutter doctor
flutter --versionLinux (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 doctorCreate Project
flutter create my_app --org com.example
cd my_app
flutter runProject Structure
Recommended Directory 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.dartClean Architecture
domain/ # Business logic layer - pure Dart
data/ # Data layer - repository implementation
presentation/ # Presentation layer - UI and state managementDart 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
Riverpod (Recommended)
flutter pub add flutter_riverpodBasic 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 constructorNavigation
Go Router (Recommended)
flutter pub add go_routerConfiguration
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 dioBasic 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_preferencesimport '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 pathimport '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 neededImage 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),
)