目录

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 doctor

Windows

  1. Flutter Downloads 下载最新稳定版 SDK
  2. 解压到指定目录(如 C:\src\flutter
  3. 在系统环境变量 PATH 中添加 flutter\bin
  4. 打开命令提示符验证:
flutter doctor
flutter --version

Linux (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.dart

Clean 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_preferences
import '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 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
          )
        ''');
      },
    );
  }
}

测试

单元测试

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),
)

相关资源