flutter flutter

Flutter开发技能,涵盖Riverpod状态管理、Freezed数据模型、go_router导航和Mocktail测试,专注于构建高效、可维护的Flutter应用

移动开发 0 次安装 0 次浏览 更新于 3/5/2026

Flutter 开发技能

加载于:base.md

项目结构

project/
├── lib/
│   ├── core/                           # 核心工具
│   │   ├── constants/                  # 应用常量
│   │   ├── extensions/                 # Dart 扩展
│   │   ├── router/                     # go_router 配置
│   │   │   └── app_router.dart
│   │   └── theme/                      # 应用主题
│   │       └── app_theme.dart
│   ├── data/                           # 数据层
│   │   ├── models/                     # Freezed 数据模型
│   │   ├── repositories/               # 仓库实现
│   │   └── services/                   # API 服务
│   ├── domain/                         # 领域层
│   │   ├── entities/                   # 商业实体
│   │   └── repositories/               # 仓库接口
│   ├── presentation/                   # UI层
│   │   ├── common/                     # 共享组件
│   │   ├── features/                   # 功能模块
│   │   │   └── feature_name/
│   │   │       ├── providers/          # Riverpod 提供者
│   │   │       ├── widgets/            # 特定功能组件
│   │   │       └── feature_screen.dart
│   │   └── providers/                  # 全局提供者
│   ├── main.dart
│   └── app.dart
├── test/
│   ├── unit/                           # 单元测试
│   ├── widget/                         # 组件测试
│   └── integration/                    # 集成测试
├── pubspec.yaml
├── analysis_options.yaml
└── CLAUDE.md

Riverpod 状态管理

提供者类型

// 简单值提供者
final appNameProvider = Provider<String>((ref) => '我的应用');

// StateProvider 用于简单可变状态
final counterProvider = StateProvider<int>((ref) => 0);

// NotifierProvider 用于复杂状态逻辑
final userProvider = NotifierProvider<UserNotifier, User?>(() => UserNotifier());

// AsyncNotifierProvider 用于异步操作
final usersProvider = AsyncNotifierProvider<UsersNotifier, List<User>>(
  () => UsersNotifier(),
);

// FutureProvider 用于简单异步数据
final configProvider = FutureProvider<Config>((ref) async {
  return await ref.watch(configServiceProvider).loadConfig();
});

// StreamProvider 用于实时数据
final messagesProvider = StreamProvider<List<Message>>((ref) {
  return ref.watch(messageServiceProvider).watchMessages();
});

// Family 提供者用于参数化数据
final userByIdProvider = FutureProvider.family<User, String>((ref, userId) async {
  return await ref.watch(userRepositoryProvider).getUser(userId);
});

通知者模式

@riverpod
class Users extends _$Users {
  @override
  Future<List<User>> build() async {
    return await _fetchUsers();
  }

  Future<List<User>> _fetchUsers() async {
    final repository = ref.read(userRepositoryProvider);
    return await repository.getUsers();
  }

  Future<void> refresh() async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() => _fetchUsers());
  }

  Future<void> addUser(User user) async {
    final repository = ref.read(userRepositoryProvider);
    await repository.addUser(user);
    ref.invalidateSelf();
  }
}

AsyncValue 处理

class UsersScreen extends ConsumerWidget {
  const UsersScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final usersAsync = ref.watch(usersProvider);

    return usersAsync.when(
      data: (users) => UsersList(users: users),
      loading: () => const Center(child: CircularProgressIndicator()),
      error: (error, stack) => ErrorDisplay(
        error: error,
        onRetry: () => ref.invalidate(usersProvider),
      ),
    );
  }
}

// 模式匹配替代方案
Widget build(BuildContext context, WidgetRef ref) {
  final usersAsync = ref.watch(usersProvider);

  return switch (usersAsync) {
    AsyncData(:final value) => UsersList(users: value),
    AsyncLoading() => const LoadingIndicator(),
    AsyncError(:final error) => ErrorDisplay(error: error),
  };
}

ref 方法

// watch - 当提供者变化时重建
final users = ref.watch(usersProvider);

// read - 一次性读取,不重建
void onButtonPressed() {
  ref.read(counterProvider.notifier).state++;
}

// listen - 反应变化而不重建
ref.listen(authProvider, (previous, next) {
  if (next == null) {
    context.go('/login');
  }
});

// invalidate - 强制刷新
ref.invalidate(usersProvider);

// keepAlive - 防止自动释放
final link = ref.keepAlive();
// 稍后:link.close() 允许释放

Freezed 数据模型

模型定义

import 'package:freezed_annotation/freezed_annotation.dart';

part 'user.freezed.dart';
part 'user.g.dart';

@freezed
class User with _$User {
  const factory User({
    required String id,
    required String name,
    required String email,
    @Default(false) bool isActive,
    DateTime? createdAt,
  }) = _User;

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}

// 联合类型用于状态
@freezed
sealed class AuthState with _$AuthState {
  const factory AuthState.initial() = _Initial;
  const factory AuthState.loading() = _Loading;
  const factory AuthState.authenticated(User user) = _Authenticated;
  const factory AuthState.unauthenticated() = _Unauthenticated;
  const factory AuthState.error(String message) = _Error;
}

使用 Freezed 联合

Widget build(BuildContext context, WidgetRef ref) {
  final authState = ref.watch(authProvider);

  return authState.when(
    initial: () => const SplashScreen(),
    loading: () => const LoadingScreen(),
    authenticated: (user) => HomeScreen(user: user),
    unauthenticated: () => const LoginScreen(),
    error: (message) => ErrorScreen(message: message),
  );
}

go_router 导航

路由器配置

final routerProvider = Provider<GoRouter>((ref) {
  final authState = ref.watch(authProvider);

  return GoRouter(
    initialLocation: '/',
    refreshListenable: authState,
    redirect: (context, state) {
      final isLoggedIn = authState.valueOrNull != null;
      final isLoggingIn = state.matchedLocation == '/login';

      if (!isLoggedIn && !isLoggingIn) return '/login';
      if (isLoggedIn && isLoggingIn) return '/';
      return null;
    },
    routes: [
      GoRoute(
        path: '/',
        builder: (context, state) => const HomeScreen(),
        routes: [
          GoRoute(
            path: 'user/:id',
            builder: (context, state) => UserScreen(
              userId: state.pathParameters['id']!,
            ),
          ),
        ],
      ),
      GoRoute(
        path: '/login',
        builder: (context, state) => const LoginScreen(),
      ),
    ],
    errorBuilder: (context, state) => ErrorScreen(error: state.error),
  );
});

导航

// 跳转到路由
context.go('/user/123');

// 推到堆栈
context.push('/user/123');

// 弹出当前路由
context.pop();

// 替换当前路由
context.pushReplacement('/home');

// 命名路由
context.goNamed('user', pathParameters: {'id': '123'});

组件模式

ConsumerWidget vs ConsumerStatefulWidget

// 无状态与 Riverpod
class UserCard extends ConsumerWidget {
  const UserCard({super.key, required this.userId});

  final String userId;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final user = ref.watch(userByIdProvider(userId));
    return user.when(
      data: (user) => Card(child: Text(user.name)),
      loading: () => const CardSkeleton(),
      error: (e, _) => ErrorCard(error: e),
    );
  }
}

// 有状态与 Riverpod
class SearchScreen extends ConsumerStatefulWidget {
  const SearchScreen({super.key});

  @override
  ConsumerState<SearchScreen> createState() => _SearchScreenState();
}

class _SearchScreenState extends ConsumerState<SearchScreen> {
  final _controller = TextEditingController();

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final results = ref.watch(searchProvider(_controller.text));
    return Column(
      children: [
        TextField(
          controller: _controller,
          onChanged: (_) => setState(() {}),
        ),
        Expanded(child: SearchResults(results: results)),
      ],
    );
  }
}

HookConsumerWidget (with flutter_hooks)

class AnimatedCounter extends HookConsumerWidget {
  const AnimatedCounter({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final controller = useAnimationController(duration: const Duration(milliseconds: 300));
    final count = ref.watch(counterProvider);

    useEffect(() {
      controller.forward(from: 0);
      return null;
    }, [count]);

    return ScaleTransition(
      scale: controller,
      child: Text('$count'),
    );
  }
}

测试与 Mocktail

单元测试

import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:riverpod/riverpod.dart';

class MockUserRepository extends Mock implements UserRepository {}

void main() {
  late MockUserRepository mockRepository;
  late ProviderContainer container;

  setUp(() {
    mockRepository = MockUserRepository();
    container = ProviderContainer(
      overrides: [
        userRepositoryProvider.overrideWithValue(mockRepository),
      ],
    );
  });

  tearDown(() {
    container.dispose();
  });

  test('usersProvider 返回用户列表', () async {
    final users = [User(id: '1', name: 'John', email: 'john@example.com')];
    when(() => mockRepository.getUsers()).thenAnswer((_) async => users);

    final result = await container.read(usersProvider.future);

    expect(result, equals(users));
    verify(() => mockRepository.getUsers()).called(1);
  });
}

组件测试

void main() {
  testWidgets('UserCard 显示用户名', (tester) async {
    final user = User(id: '1', name: 'John', email: 'john@example.com');

    await tester.pumpWidget(
      ProviderScope(
        overrides: [
          userByIdProvider('1').overrideWith((_) => AsyncData(user)),
        ],
        child: const MaterialApp(home: UserCard(userId: '1')),
      ),
    );

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

  testWidgets('UserCard 显示加载指示器', (tester) async {
    await tester.pumpWidget(
      ProviderScope(
        overrides: [
          userByIdProvider('1').overrideWith((_) => const AsyncLoading()),
        ],
        child: const MaterialApp(home: UserCard(userId: '1')),
      ),
    );

    expect(find.byType(CircularProgressIndicator), findsOneWidget);
  });
}

pubspec.yaml

name: my_app
description: 一个 Flutter 应用
publish_to: 'none'
version: 1.0.0+1

environment:
  sdk: '>=3.2.0 <4.0.0'

dependencies:
  flutter:
    sdk: flutter

  # 状态管理
  flutter_riverpod: ^2.4.9
  riverpod_annotation: ^2.3.3

  # 数据模型
  freezed_annotation: ^2.4.1
  json_annotation: ^4.8.1

  # 导航
  go_router: ^13.0.0

  # 网络
  dio: ^5.4.0

  # 存储
  shared_preferences: ^2.2.2

  # 工具
  intl: ^0.19.0

dev_dependencies:
  flutter_test:
    sdk: flutter

  # 代码生成
  build_runner: ^2.4.8
  freezed: ^2.4.6
  json_serializable: ^6.7.1
  riverpod_generator: ^2.3.9

  # 测试
  mocktail: ^1.0.2

  # 代码检查
  flutter_lints: ^3.0.1

GitHub Actions

name: Flutter CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.16.0'
          channel: 'stable'
          cache: true

      - name: 安装依赖
        run: flutter pub get

      - name: 生成代码
        run: dart run build_runner build --delete-conflicting-outputs

      - name: 分析
        run: flutter analyze --fatal-infos

      - name: 运行测试
        run: flutter test --coverage

      - name: 构建 APK
        run: flutter build apk --release

analysis_options.yaml

include: package:flutter_lints/flutter.yaml

analyzer:
  exclude:
    - "**/*.g.dart"
    - "**/*.freezed.dart"
  errors:
    invalid_annotation_target: ignore
  language:
    strict-casts: true
    strict-inference: true
    strict-raw-types: true

linter:
  rules:
    - always_declare_return_types
    - avoid_dynamic_calls
    - avoid_print
    - avoid_type_to_string
    - cancel_subscriptions
    - close_sinks
    - prefer_const_constructors
    - prefer_const_declarations
    - prefer_final_locals
    - require_trailing_commas
    - unawaited_futures
    - use_super_parameters

Flutter 反模式

  • 没有 autoDispose 的 Provider - 使用 .autoDispose 防止内存泄漏
  • 在回调中 watch - 在 onPressed/回调中使用 ref.read(),而不是 ref.watch()
  • 业务逻辑在组件中 - 移动到 Notifiers/providers
  • 在提供者中使用可变状态 - 使用 Freezed 实现不可变模型
  • 不使用 AsyncValue - 使用 when() 处理加载/错误状态
  • 使用 Riverpod 的 setState - 使用提供者共享状态
  • 将 ref 传递给函数 - 保持 ref 使用在组件/提供者内部
  • 深层嵌套的 Consumer - 使用 ConsumerWidget 替代
  • 不使用 family 参数 - 使用 .family 为参数化提供者
  • 全局 GoRouter 实例 - 使用 Provider 为路由器提供重定向逻辑
  • 跨异步的 BuildContext - 在 await 之前存储值,而不是上下文
  • 忽略 dispose - 在 ConsumerStatefulWidget 中清理控制器