dart-flutter-patterns

dart-flutter-patterns

热门

生产就绪的 Dart 和 Flutter 模式,涵盖空安全、不可变状态、异步组合、Widget 架构、主流状态管理框架(BLoC、Riverpod、Provider)、GoRouter 导航、Dio 网络请求、Freezed 代码生成和整洁架构。

23万Star
3.5万Fork
更新于 2026/7/19
SKILL.md
readonly只读
name
dart-flutter-patterns
description

Production-ready Dart and Flutter patterns covering null safety, immutable state, async composition, widget architecture, popular state management frameworks (BLoC, Riverpod, Provider), GoRouter navigation, Dio networking, Freezed code generation, and clean architecture.

Dart/Flutter 模式

何时使用

在以下场景使用此技能:

  • 开始一个新的 Flutter 功能,需要状态管理、导航或数据访问的惯用模式
  • 审查或编写 Dart 代码,需要空安全、密封类型或异步组合的指导
  • 设置新的 Flutter 项目,在 BLoC、Riverpod 或 Provider 之间选择
  • 实现安全的 HTTP 客户端、WebView 集成或本地存储
  • 为 Flutter Widget、Cubit 或 Riverpod Provider 编写测试
  • 配置带有身份验证守卫的 GoRouter

工作原理

此技能提供按关注点组织的、可复制粘贴的 Dart/Flutter 代码模式:

  1. 空安全 — 避免 !,优先使用 ?./??/模式匹配
  2. 不可变状态 — 密封类、freezedcopyWith
  3. 异步组合 — 并发 Future.waitawait 后的安全 BuildContext
  4. Widget 架构 — 提取为类(而非方法)、const 传播、作用域重建
  5. 状态管理 — BLoC/Cubit 事件、Riverpod Notifier 和派生 Provider
  6. 导航 — 通过 refreshListenable 实现响应式身份验证守卫的 GoRouter
  7. 网络请求 — 带拦截器的 Dio、带一次性重试守卫的令牌刷新
  8. 错误处理 — 全局捕获、ErrorWidget.builder、Crashlytics 集成
  9. 测试 — 单元测试(BLoC 测试)、Widget 测试(ProviderScope 覆盖)、使用 Fake 而非 Mock

示例

// 密封状态 — 防止不可能的状态
sealed class AsyncState<T> {}
final class Loading<T> extends AsyncState<T> {}
final class Success<T> extends AsyncState<T> { final T data; const Success(this.data); }
final class Failure<T> extends AsyncState<T> { final Object error; const Failure(this.error); }

// 带响应式身份验证重定向的 GoRouter
final router = GoRouter(
  refreshListenable: GoRouterRefreshStream(authCubit.stream),
  redirect: (context, state) {
    final authed = context.read<AuthCubit>().state is AuthAuthenticated;
    if (!authed && !state.matchedLocation.startsWith('/login')) return '/login';
    return null;
  },
  routes: [...],
);

// 带安全 firstWhereOrNull 的 Riverpod 派生 Provider
@riverpod
double cartTotal(Ref ref) {
  final cart = ref.watch(cartNotifierProvider);
  final products = ref.watch(productsProvider).valueOrNull ?? [];
  return cart.fold(0.0, (total, item) {
    final product = products.firstWhereOrNull((p) => p.id == item.productId);
    return total + (product?.price ?? 0) * item.quantity;
  });
}

适用于 Dart 和 Flutter 应用程序的实用、生产就绪模式。尽可能与库无关,并明确覆盖最常见的生态系统包。


1. 空安全基础

优先使用模式而非感叹号操作符

// 不好 — 运行时若为 null 则崩溃
final name = user!.name;

// 好 — 提供回退值
final name = user?.name ?? 'Unknown';

// 好 — Dart 3 模式匹配(复杂情况首选)
final display = switch (user) {
  User(:final name, :final email) => '$name <$email>',
  null => 'Guest',
};

// 好 — 提前返回守卫
String getUserName(User? user) {
  if (user == null) return 'Unknown';
  return user.name; // 检查后提升为非空
}

避免过度使用 late

// 不好 — 将空错误推迟到运行时
late String userId;

// 好 — 可空并显式初始化
String? userId;

// 可以 — 仅在首次访问前保证初始化时使用 late
// (例如,在任何 Widget 交互之前的 initState() 中)
late final AnimationController _controller;

@override
void initState() {
  super.initState();
  _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 300));
}

2. 不可变状态

状态层次结构的密封类

sealed class UserState {}

final class UserInitial extends UserState {}

final class UserLoading extends UserState {}

final class UserLoaded extends UserState {
  const UserLoaded(this.user);
  final User user;
}

final class UserError extends UserState {
  const UserError(this.message);
  final String message;
}

// 穷尽 switch — 编译器强制所有分支
Widget buildFrom(UserState state) => switch (state) {
  UserInitial() => const SizedBox.shrink(),
  UserLoading() => const CircularProgressIndicator(),
  UserLoaded(:final user) => UserCard(user: user),
  UserError(:final message) => ErrorText(message),
};

使用 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 isAdmin,
  }) = _User;

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

// 使用
final user = User(id: '1', name: 'Alice', email: 'alice@example.com');
final updated = user.copyWith(name: 'Alice Smith'); // 不可变更新
final json = user.toJson();
final fromJson = User.fromJson(json);

3. 异步组合

使用 Future.wait 的结构化并发

Future<DashboardData> loadDashboard(UserRepository users, OrderRepository orders) async {
  // 并发运行 — 不要顺序 await
  final (userList, orderList) = await (
    users.getAll(),
    orders.getRecent(),
  ).wait; // Dart 3 记录解构 + Future.wait 扩展

  return DashboardData(users: userList, orders: orderList);
}

流模式

// 仓库暴露响应式流以获取实时数据
Stream<List<Item>> watchCartItems() => _db
    .watchTable('cart_items')
    .map((rows) => rows.map(Item.fromRow).toList());

// 在 Widget 层 — 声明式,无需手动订阅
StreamBuilder<List<Item>>(
  stream: cartRepository.watchCartItems(),
  builder: (context, snapshot) => switch (snapshot) {
    AsyncSnapshot(connectionState: ConnectionState.waiting) =>
        const CircularProgressIndicator(),
    AsyncSnapshot(:final error?) => ErrorWidget(error.toString()),
    AsyncSnapshot(:final data?) => CartList(items: data),
    _ => const SizedBox.shrink(),
  },
)

Await 后的 BuildContext

// 关键 — 在 StatefulWidget 中任何 await 后始终检查 mounted
Future<void> _handleSubmit() async {
  setState(() => _isLoading = true);
  try {
    await authService.login(_email, _password);
    if (!mounted) return; // ← 使用 context 前守卫
    context.go('/home');
  } on AuthException catch (e) {
    if (!mounted) return;
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.message)));
  } finally {
    if (mounted) setState(() => _isLoading = false);
  }
}

4. Widget 架构

提取为类,而非方法

// 不好 — 返回 Widget 的私有方法,阻止优化
Widget _buildHeader() {
  return Container(
    padding: const EdgeInsets.all(16),
    child: Text(title, style: Theme.of(context).textTheme.headlineMedium),
  );
}

// 好 — 单独的 Widget 类,支持 const,元素复用
class _PageHeader extends StatelessWidget {
  const _PageHeader(this.title);
  final String title;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(16),
      child: Text(title, style: Theme.of(context).textTheme.headlineMedium),
    );
  }
}

const 传播

// 不好 — 每次重建新实例
child: Padding(
  padding: EdgeInsets.all(16.0),       // 不是 const
  child: Icon(Icons.home, size: 24.0), // 不是 const
)

// 好 — const 阻止重建传播
child: const Padding(
  padding: EdgeInsets.all(16.0),
  child: Icon(Icons.home, size: 24.0),
)

作用域重建

// 不好 — 每次计数器变化时整个页面重建
class CounterPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider); // 重建所有内容
    return Scaffold(
      body: Column(children: [
        const ExpensiveHeader(), // 不必要地重建
        Text('$count'),
        const ExpensiveFooter(), // 不必要地重建
      ]),
    );
  }
}

// 好 — 隔离重建部分
class CounterPage extends StatelessWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Column(children: [
        ExpensiveHeader(),        // 从不重建(const)
        _CounterDisplay(),        // 仅此部分重建
        ExpensiveFooter(),        // 从不重建(const)
      ]),
    );
  }
}

class _CounterDisplay extends ConsumerWidget {
  const _CounterDisplay();

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);
    return Text('$count');
  }
}

5. 状态管理:BLoC/Cubit

// Cubit — 同步或简单异步状态
class AuthCubit extends Cubit<AuthState> {
  AuthCubit(this._authService) : super(const AuthState.initial());
  final AuthService _authService;

  Future<void> login(String email, String password) async {
    emit(const AuthState.loading());
    try {
      final user = await _authService.login(email, password);
      emit(AuthState.authenticated(user));
    } on AuthException catch (e) {
      emit(AuthState.error(e.message));
    }
  }

  void logout() {
    _authService.logout();
    emit(const AuthState.initial());
  }
}

// 在 Widget 中
BlocBuilder<AuthCubit, AuthState>(
  builder: (context, state) => switch (state) {
    AuthInitial() => const LoginForm(),
    AuthLoading() => const CircularProgressIndicator(),
    AuthAuthenticated(:final user) => HomePage(user: user),
    AuthError(:final message) => ErrorView(message: message),
  },
)

6. 状态管理:Riverpod

// 自动释放异步 Provider
@riverpod
Future<List<Product>> products(Ref ref) async {
  final repo = ref.watch(productRepositoryProvider);
  return repo.getAll();
}

// 带复杂变异的 Notifier
@riverpod
class CartNotifier extends _$CartNotifier {
  @override
  List<CartItem> build() => [];

  void add(Product product) {
    final existing = state.where((i) => i.productId == product.id).firstOrNull;
    if (existing != null) {
      state = [
        for (final item in state)
          if (item.productId == product.id) item.copyWith(quantity: item.quantity + 1)
          else item,
      ];
    } else {
      state = [...state, CartItem(productId: product.id, quantity: 1)];
    }
  }

  void remove(String productId) =>
      state = state.where((i) => i.productId != productId).toList();

  void clear() => state = [];
}

// 派生 Provider(选择器模式)
@riverpod
int cartCount(Ref ref) => ref.watch(cartNotifierProvider).length;

@riverpod
double cartTotal(Ref ref) {
  final cart = ref.watch(cartNotifierProvider);
  final products = ref.watch(productsProvider).valueOrNull ?? [];
  return cart.fold(0.0, (total, item) {
    // firstWhereOrNull(来自 collection 包)避免产品缺失时的 StateError
    final product = products.firstWhereOrNull((p) => p.id == item.productId);
    return total + (product?.price ?? 0) * item.quantity;
  });
}

7. 使用 GoRouter 导航

final router = GoRouter(
  initialLocation: '/',
  // refreshListenable 在身份验证状态变化时重新评估重定向
  refreshListenable: GoRouterRefreshStream(authCubit.stream),
  redirect: (context, state) {
    final isLoggedIn = context.read<AuthCubit>().state is AuthAuthenticated;
    final isGoingToLogin = state.matchedLocation == '/login';
    if (!isLoggedIn && !isGoingToLogin) return '/login';
    if (isLoggedIn && isGoingToLogin) return '/';
    return null;
  },
  routes: [
    GoRoute(path: '/login', builder: (_, __) => const LoginPage()),
    ShellRoute(
      builder: (context, state, child) => AppShell(child: child),
      routes: [
        GoRoute(path: '/', builder: (_, __) => const HomePage()),
        GoRoute(
          path: '/products/:id',
          builder: (context, state) =>
              ProductDetailPage(id: state.pathParameters['id']!),
        ),
      ],
    ),
  ],
);

8. 使用 Dio 进行 HTTP 请求

final dio = Dio(BaseOptions(
  baseUrl: const String.fromEnvironment('API_URL'),
  connectTimeout: const Duration(seconds: 10),
  receiveTimeout: const Duration(seconds: 30),
  headers: {'Content-Type': 'application/json'},
));

// 添加身份验证拦截器
dio.interceptors.add(InterceptorsWrapper(
  onRequest: (options, handler) async {
    final token = await secureStorage.read(key: 'auth_token');
    if (token != null) options.headers['Authorization'] = 'Bearer $token';
    handler.next(options);
  },
  onError: (error, handler) async {
    // 防止无限重试循环:每个请求仅尝试刷新一次
    final isRetry = error.requestOptions.extra['_isRetry'] == true;
    if (!isRetry && error.response?.statusCode == 401) {
      final refreshed = await attemptTokenRefresh();
      if (refreshed) {
        error.requestOptions.extra['_isRetry'] = true;
        return handler.resolve(await dio.fetch(error.requestOptions));
      }
    }
    handler.next(error);
  },
));

// 使用 Dio 的仓库
class UserApiDataSource {
  const UserApiDataSource(this._dio);
  final Dio _dio;

  Future<User> getById(String id) async {
    final response = await _dio.get<Map<String, dynamic>>('/users/$id');
    return User.fromJson(response.data!);
  }
}

9. 错误处理架构

// 全局错误捕获 — 在 main() 中设置
void main() {
  FlutterError.onError = (details) {
    FlutterError.presentError(details);
    crashlytics.recordFlutterFatalError(details);
  };

  PlatformDispatcher.instance.onError = (error, stack) {
    crashlytics.recordError(error, stack, fatal: true);
    return true;
  };

  runApp(const App());
}

// 生产环境自定义 ErrorWidget
class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    ErrorWidget.builder = (details) => ProductionErrorWidget(details);
    return MaterialApp.router(routerConfig: router);
  }
}

10. 测试快速参考

// 单元测试 — 用例
test('GetUserUseCase returns null for missing user', () async {
  final repo = FakeUserRepository();
  final useCase = GetUserUseCase(repo);
  expect(await useCase('missing-id'), isNull);
});

// BLoC 测试
blocTest<AuthCubit, AuthState>(
  'emits loading then error on failed login',
  build: () => AuthCubit(FakeAuthService(throwsOn: 'login')),
  act: (cubit) => cubit.login('user@test.com', 'wrong'),
  expect: () => [const AuthState.loading(), isA<AuthError>()],
);

// Widget 测试
testWidgets('CartBadge shows item count', (tester) async {
  await tester.pumpWidget(
    ProviderScope(
      overrides: [cartNotifierProvider.overrideWith(() => FakeCartNotifier(count: 3))],
      child: const MaterialApp(home: CartBadge()),
    ),
  );
  expect(find.text('3'), findsOneWidget);
});

参考