Flutter 最佳实践
目录
快速入门
安装与版本
当前使用版本为 Flutter 3.41.6。
macOS
# 下载 Flutter SDK
cd ~/Downloads
git clone https://github.com/flutter/flutter.git -b 3.41.6 --depth 1
# 添加到 PATH(编辑 ~/.zshrc)
export PATH="$PATH:$HOME/Downloads/flutter/bin"
# 验证安装
flutter doctor
flutter --version或者使用 Homebrew:
brew install flutter
flutter doctorWindows
- 从 Flutter Downloads 下载最新稳定版 SDK
- 解压到指定目录(如
C:\src\flutter) - 在系统环境变量 PATH 中添加
flutter\bin - 打开命令提示符验证:
flutter doctor
flutter --versionLinux (Ubuntu)
# 安装依赖
sudo apt update
sudo apt install -y curl git unzip xz-utils zip libglu1-mesa
# 下载 Flutter SDK
cd ~/development
git clone https://github.com/flutter/flutter.git -b 3.41.6 --depth 1
# 添加到 PATH
export PATH="$PATH:$HOME/development/flutter/bin"
# 验证
flutter doctor创建项目
# 创建新项目
flutter create my_app --org com.example
# 进入目录
cd my_app
# 运行应用
flutter run运行应用
# 在 Chrome 运行
flutter run -d chrome
# 在 iOS 模拟器运行(macOS)
flutter run -d iphone
# 在 Android 模拟器运行
flutter run -d emulator-5554
# 查看所有设备
flutter devices项目结构
推荐的目录结构
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/ # 业务逻辑层 - 纯 Dart
data/ # 数据层 - 实现 repository
presentation/ # 表现层 - UI 和状态管理Dart 基础
空安全
// 非空类型
String name = 'Alice';
// 可空类型
String? nullableName;
// 空检查
if (nullableName != null) {
print(nullableName.length); // 安全
}
// 空断言(谨慎使用)
print(nullableName!.length);
// 空合并
String displayName = nullableName ?? 'Anonymous';
// 空赋值
nullableName ??= 'Default';异步编程
// 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;
}
}
// 使用
final stream = countStream();
stream.listen((value) => print('Count: $value'));集合操作
// 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',
];状态管理
Riverpod(推荐)
flutter pub add flutter_riverpod基本用法
import 'package:flutter_riverpod/flutter_riverpod.dart';
// 定义 provider
final counterProvider = StateProvider<int>((ref) => 0);
// 异步 provider
final userProvider = FutureProvider<User>((ref) async {
final repository = ref.read(userRepositoryProvider);
return repository.getUser(1);
});
// 使用
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,
);
// 使用
ref.read(counterProvider.notifier).increment();Widget 规范
Stateless vs Stateful
// 简单 widget - 使用 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 复用
// 提取可复用组件
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 构造函数
// 使用 const 减少重建
class MyWidget extends StatelessWidget {
const MyWidget({super.key, this.title});
final String? title;
@override
Widget build(BuildContext context) {
return Text(title ?? '');
}
}
// 调用
const MyWidget(title: 'Hello') // const 构造导航
Go Router(推荐)
flutter pub add go_router配置
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(),
),
],
);
// 使用
context.go('/users/123');
context.push('/settings');网络请求
Dio
flutter pub add dio基本用法
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);
}
}本地存储
SharedPreferences
flutter pub add shared_preferencesimport 'package:shared_preferences/shared_preferences.dart';
// 保存
final prefs = await SharedPreferences.getInstance();
await prefs.setString('token', 'abc123');
await prefs.setInt('count', 42);
// 读取
final token = prefs.getString('token');
final count = prefs.getInt('count');
// 删除
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
)
''');
},
);
}
}测试
单元测试
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 测试
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);
});
}性能优化
避免不必要的重建
// 使用 const widget
const Text('Hello')
// 避免在 build 中创建新对象
class MyWidget extends StatelessWidget {
final items = ['a', 'b', 'c']; // 类级别常量
@override
Widget build(BuildContext context) {
return Column(
children: items.map((item) => Text(item)).toList(),
);
}
}ListView 优化
// 使用 ListView.builder
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(title: Text(items[index]));
},
);
// 如果不需要滚动,用 Column 或 ListView图片缓存
// 使用 cached_network_image
flutter pub add cached_network_image
// 使用
CachedNetworkImage(
imageUrl: 'https://example.com/image.jpg',
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, url, error) => Icon(Icons.error),
)