普通视图

发现新文章,点击刷新页面。
昨天 — 2026年3月21日首页

06-Flutter动画从零到炫酷-让你的App动起来

作者 一枚菜鸟_
2026年3月21日 17:37

✨ Flutter 动画从零到炫酷:让你的 App 动起来

同样的功能,加上动画后用户好感度提升 60%。Flutter 内置了完整的动画引擎, 从简单的淡入淡出到复杂的交错动画,甚至物理弹簧效果 — 全都开箱即用。

本文目标:从最简单的隐式动画开始,逐步进阶到显式动画、Hero 动画、交错动画, 最后用 Lottie 实现设计师级别的炫酷效果。每个知识点都有可运行的完整代码


📊 Flutter 动画体系总览

层级 类型 难度 典型场景 代表 Widget
1️⃣ 隐式动画 颜色渐变、尺寸变化、透明度 AnimatedContainer / AnimatedOpacity
2️⃣ 显式动画 ⭐⭐ 循环旋转、自定义曲线、序列动画 AnimationController + Tween
3️⃣ Hero 动画 ⭐⭐ 页面转场、图片放大 Hero
4️⃣ 交错动画 ⭐⭐⭐ 列表项依次入场、引导页 Interval + Stagger
5️⃣ 物理动画 ⭐⭐⭐ 弹簧效果、惯性滑动 SpringSimulation / physics
6️⃣ Lottie ⭐⭐ 设计师级复杂动画 lottie

🎯 1. 隐式动画:最简单的动画方式

隐式动画 = 你只需要改变目标值,Flutter 自动帮你补间过渡。 零学习成本,适合 90% 的 UI 动效需求。

AnimatedContainer — 万能隐式动画

class AnimatedBox extends StatefulWidget {
  const AnimatedBox({super.key});

  @override
  State<AnimatedBox> createState() => _AnimatedBoxState();
}

class _AnimatedBoxState extends State<AnimatedBox> {
  bool _expanded = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => setState(() => _expanded = !_expanded),
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 400),
        curve: Curves.easeInOutCubic,     // 缓动曲线
        width: _expanded ? 300 : 150,      // 宽度动画
        height: _expanded ? 200 : 100,     // 高度动画
        decoration: BoxDecoration(
          color: _expanded ? Colors.indigo : Colors.teal,  // 颜色动画
          borderRadius: BorderRadius.circular(
            _expanded ? 24 : 12,           // 圆角动画
          ),
          boxShadow: [
            BoxShadow(
              color: (_expanded ? Colors.indigo : Colors.teal).withValues(alpha: 0.4),
              blurRadius: _expanded ? 20 : 8,
              offset: const Offset(0, 8),
            ),
          ],
        ),
        child: Center(
          child: Text(
            _expanded ? '收起 ↑' : '展开 ↓',
            style: const TextStyle(color: Colors.white, fontSize: 16),
          ),
        ),
      ),
    );
  }
}

常用隐式动画 Widget 速查

Widget 动画属性 用途
AnimatedContainer 尺寸、颜色、边距、圆角、阴影 万能容器动画
AnimatedOpacity opacity 淡入/淡出
AnimatedScale scale 缩放
AnimatedRotation turns 旋转
AnimatedSlide offset 滑动位移
AnimatedAlign alignment 对齐位置变化
AnimatedPadding padding 内边距变化
AnimatedCrossFade 两个子 Widget 交叉切换 内容切换
AnimatedSwitcher 子 Widget 替换时自动过渡 任意内容切换
AnimatedDefaultTextStyle 字号、颜色、粗细 文字样式变化

AnimatedSwitcher — 内容切换动画

AnimatedSwitcher(
  duration: const Duration(milliseconds: 300),
  transitionBuilder: (child, animation) {
    return FadeTransition(
      opacity: animation,
      child: ScaleTransition(scale: animation, child: child),
    );
  },
  child: Text(
    '$_count',
    key: ValueKey<int>(_count),  // key 变化才触发动画
    style: const TextStyle(fontSize: 48, fontWeight: FontWeight.bold),
  ),
)

🎡 2. 显式动画:完全掌控每一帧

当隐式动画满足不了需求(循环、反复、自定义曲线),就需要显式动画。

AnimationController 核心三件套

AnimationController(控制器)→ 决定时间和控制
      ↓
Tween(补间)→ 定义值的起止范围
      ↓
Widget(渲染)→ 用动画值构建 UI

脉冲呼吸灯效果

class PulsingDot extends StatefulWidget {
  const PulsingDot({super.key});

  @override
  State<PulsingDot> createState() => _PulsingDotState();
}

class _PulsingDotState extends State<PulsingDot>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;
  late final Animation<double> _scaleAnimation;
  late final Animation<double> _opacityAnimation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(milliseconds: 1500),
      vsync: this,  // 绑定帧回调,节省 GPU
    );

    _scaleAnimation = Tween<double>(begin: 0.8, end: 1.2).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
    );

    _opacityAnimation = Tween<double>(begin: 0.4, end: 1.0).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
    );

    _controller.repeat(reverse: true);  // 无限循环,自动反向
  }

  @override
  void dispose() {
    _controller.dispose();  // ⚠️ 必须释放,否则内存泄漏!
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) {
        return Transform.scale(
          scale: _scaleAnimation.value,
          child: Opacity(
            opacity: _opacityAnimation.value,
            child: Container(
              width: 24,
              height: 24,
              decoration: BoxDecoration(
                color: Colors.green,
                shape: BoxShape.circle,
                boxShadow: [
                  BoxShadow(
                    color: Colors.green.withValues(alpha: 0.6),
                    blurRadius: 12 * _scaleAnimation.value,
                  ),
                ],
              ),
            ),
          ),
        );
      },
    );
  }
}

常用缓动曲线

曲线 效果 适用场景
Curves.linear 匀速 进度条
Curves.easeInOut 慢-快-慢 通用过渡
Curves.easeOutCubic 快速减速 弹窗弹出
Curves.easeInBack 先后退再前进 强调出场
Curves.elasticOut 弹簧抖动 趣味反馈
Curves.bounceOut 落地弹跳 下落效果

🦸 3. Hero 动画:页面转场魔法

Hero 让同一个 Widget 在两个页面间无缝飞行,最适合图片预览和详情页转场。

// 列表页 — 商品卡片
class ProductCard extends StatelessWidget {
  final Product product;
  const ProductCard({super.key, required this.product});

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => Navigator.push(
        context,
        MaterialPageRoute(
          builder: (_) => ProductDetailPage(product: product),
        ),
      ),
      child: Hero(
        tag: 'product-${product.id}',  // 两端 tag 必须一致!
        child: ClipRRect(
          borderRadius: BorderRadius.circular(12),
          child: Image.network(product.image, height: 180, fit: BoxFit.cover),
        ),
      ),
    );
  }
}

// 详情页 — 大图
class ProductDetailPage extends StatelessWidget {
  final Product product;
  const ProductDetailPage({super.key, required this.product});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          Hero(
            tag: 'product-${product.id}',  // 与列表页 tag 一致
            child: Image.network(
              product.image,
              width: double.infinity,
              height: 300,
              fit: BoxFit.cover,
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(16),
            child: Text(product.name, style: const TextStyle(fontSize: 24)),
          ),
        ],
      ),
    );
  }
}

🎭 4. 交错动画:列表项依次入场

class StaggeredList extends StatefulWidget {
  final List<String> items;
  const StaggeredList({super.key, required this.items});

  @override
  State<StaggeredList> createState() => _StaggeredListState();
}

class _StaggeredListState extends State<StaggeredList>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(milliseconds: 200 * widget.items.length + 400),
      vsync: this,
    );
    _controller.forward();  // 页面进入时播放
  }

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

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: widget.items.length,
      itemBuilder: (context, index) {
        // 每个 item 有自己的时间窗口
        final start = index * 0.1;
        final end = start + 0.4;

        final slideAnimation = Tween<Offset>(
          begin: const Offset(0.5, 0),  // 从右侧滑入
          end: Offset.zero,
        ).animate(CurvedAnimation(
          parent: _controller,
          curve: Interval(start.clamp(0, 1), end.clamp(0, 1), curve: Curves.easeOutCubic),
        ));

        final fadeAnimation = Tween<double>(begin: 0, end: 1).animate(
          CurvedAnimation(
            parent: _controller,
            curve: Interval(start.clamp(0, 1), end.clamp(0, 1)),
          ),
        );

        return SlideTransition(
          position: slideAnimation,
          child: FadeTransition(
            opacity: fadeAnimation,
            child: Card(
              margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
              child: ListTile(
                leading: CircleAvatar(child: Text('${index + 1}')),
                title: Text(widget.items[index]),
              ),
            ),
          ),
        );
      },
    );
  }
}

🎬 5. Lottie 动画:设计师级别的视觉效果

# pubspec.yaml
dependencies:
  lottie: ^3.1.0
// 从网络加载 Lottie 动画
Lottie.network(
  'https://assets.lottiefiles.com/packages/lf20_success.json',
  width: 200,
  height: 200,
  repeat: false,  // 只播放一次
)

// 从本地资源加载
Lottie.asset(
  'assets/animations/loading.json',
  width: 120,
  height: 120,
)

// 控制播放(配合 AnimationController)
class LottieDemo extends StatefulWidget {
  @override
  State<LottieDemo> createState() => _LottieDemoState();
}

class _LottieDemoState extends State<LottieDemo>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this);
  }

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

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        _controller.reset();
        _controller.forward();  // 点击播放一次
      },
      child: Lottie.asset(
        'assets/animations/like.json',
        controller: _controller,
        onLoaded: (composition) {
          _controller.duration = composition.duration;
        },
      ),
    );
  }
}

🧩 6. 实用动画模式速查

页面转场动画

// 自定义页面转场
Navigator.push(context, PageRouteBuilder(
  pageBuilder: (_, animation, __) => DetailPage(),
  transitionsBuilder: (_, animation, __, child) {
    return FadeTransition(
      opacity: animation,
      child: SlideTransition(
        position: Tween<Offset>(
          begin: const Offset(0, 0.1),
          end: Offset.zero,
        ).animate(CurvedAnimation(
          parent: animation,
          curve: Curves.easeOutCubic,
        )),
        child: child,
      ),
    );
  },
  transitionDuration: const Duration(milliseconds: 400),
));

骨架屏闪烁效果

class ShimmerEffect extends StatefulWidget {
  final double width;
  final double height;
  const ShimmerEffect({super.key, required this.width, required this.height});

  @override
  State<ShimmerEffect> createState() => _ShimmerEffectState();
}

class _ShimmerEffectState extends State<ShimmerEffect>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;

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

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

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (_, __) {
        return Container(
          width: widget.width,
          height: widget.height,
          decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(8),
            gradient: LinearGradient(
              begin: Alignment(-1.0 + 2.0 * _controller.value, 0),
              end: Alignment(-0.5 + 2.0 * _controller.value, 0),
              colors: const [
                Color(0xFF1E293B),
                Color(0xFF334155),
                Color(0xFF1E293B),
              ],
            ),
          ),
        );
      },
    );
  }
}

💉 7. 动画性能优化

错误 后果 修复
动画中重建整个 Widget 树 帧率暴跌 AnimatedBuilder 精确重建动画部分
忘记 dispose() Controller 内存泄漏 dispose() 中调用 _controller.dispose()
无节制使用 Opacity Widget GPU 离屏渲染 简单场景用 FadeTransition,或设 alwaysIncludeSemantics
同时运行 20+ 动画 主线程卡顿 控制同屏动画数量,离屏元素停止动画
使用 setState 驱动高频动画 整个 Widget 重建 AnimationController + AnimatedBuilder

✅ Flutter 动画 Checklist

入门必会

  • 掌握 AnimatedContainer 基本用法
  • 会用 AnimatedOpacity / AnimatedScale 做简单过渡
  • 理解 durationcurve 的作用

进阶技能

  • 掌握 AnimationController + Tween 显式动画
  • 会用 Hero 做页面转场动画
  • 能实现交错动画(列表入场)
  • 集成 Lottie 动画

性能优化

  • 养成 dispose() Controller 的习惯
  • 使用 AnimatedBuilder 减少重建范围
  • 用 Flutter DevTools 检查帧率

动画不是锦上添花,而是用户体验的基础设施。 一个按钮点击后没有反馈,用户会怀疑"点到了吗?"; 一个页面切换没有过渡,用户会感觉"卡了一下"。 好的动画让用户感觉不到动画的存在 — 一切都自然流畅。

04-Flutter状态管理终极指南-Riverpod3.x从入门到精通

作者 一枚菜鸟_
2026年3月21日 17:35

🧠 Flutter 状态管理终极指南:Riverpod 3.x 从入门到精通

状态管理是 Flutter 开发的分水岭 — 入门用 setState,专业用 Riverpod。 Riverpod 3.x 带来了 Mutation、Ref.mounted、自动重试等重磅特性, 本文带你彻底掌握生产级状态管理。

写给谁:已有 Flutter 基础,想从 setState / Provider 升级到 Riverpod 的开发者。 读完你将掌握:Provider 类型选择、Notifier 模式、异步数据流、依赖注入、测试、以及 3.x 新特性。


📊 状态管理方案对比

方案 复杂度 学习曲线 可测试性 适合规模 2026 状态
setState 🟢 极低 🔴 差 单组件 ✅ 持续可用
Provider ⭐⭐ 🟢 低 🟡 中 小型 ⚠️ 维护模式
Riverpod 3.x ⭐⭐⭐ 🟡 中 🟢 优秀 全规模 推荐
Bloc / Cubit ⭐⭐⭐⭐ 🟠 较高 🟢 优秀 大型企业级 ✅ 稳定
GetX ⭐⭐ 🟢 低 🔴 差 快速原型 ⚠️ 争议大

🔑 为什么选 Riverpod? 它是 Provider 作者的"重写版",解决了 Provider 的所有缺陷: 编译期安全、不依赖 BuildContext、完美的可测试性、灵活的依赖注入。


🏗 1. Riverpod 3.x 核心概念

三大角色

Provider(数据源)→ Ref(连接器)→ Widget(消费者)

📦 Provider:声明"数据从哪来"
🔗 Ref:读取和操作 Provider
🖥 Widget:监听 Provider 变化并重建 UI

环境搭建

# pubspec.yaml
dependencies:
  flutter_riverpod: ^3.2.1
  riverpod_annotation: ^4.0.2

dev_dependencies:
  riverpod_generator: ^4.0.3
  build_runner: ^2.4.13
  custom_lint:
  riverpod_lint:
// main.dart — 根组件包裹 ProviderScope
void main() {
  runApp(const ProviderScope(child: MyApp()));
}

🧩 2. Provider 类型全解析

用代码生成(推荐方式)

Riverpod 3.x 推荐使用 @riverpod 注解 + 代码生成,编译器自动推断 Provider 类型。

import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'my_providers.g.dart';

// ① 简单值 Provider(只读,无状态)
@riverpod
String appTitle(Ref ref) => 'My App';

// ② 计算 / 派生 Provider(依赖其他 Provider)
@riverpod
String greeting(Ref ref) {
  final title = ref.watch(appTitleProvider);
  return 'Welcome to $title!';
}

// ③ 异步 Provider(API 请求)
@riverpod
Future<List<Product>> products(Ref ref) async {
  final dio = ref.watch(dioProvider);
  final response = await dio.get('/api/products');
  return (response.data as List).map((e) => Product.fromJson(e)).toList();
}

// ④ Stream Provider(实时数据)
@riverpod
Stream<int> countdown(Ref ref) {
  return Stream.periodic(const Duration(seconds: 1), (i) => 10 - i).take(11);
}

Notifier(有状态,可修改)

// ⑤ 同步 Notifier
@riverpod
class Counter extends _$Counter {
  @override
  int build() => 0;

  void increment() => state++;
  void decrement() => state--;
  void reset() => state = 0;
}

// ⑥ 异步 Notifier(生产级常用)
@riverpod
class ProductList extends _$ProductList {
  @override
  Future<List<Product>> build() async {
    return _fetchProducts(page: 1);
  }

  Future<List<Product>> _fetchProducts({required int page}) async {
    final dio = ref.read(dioProvider);
    final response = await dio.get('/api/products', queryParameters: {'page': page});
    return (response.data['list'] as List).map((e) => Product.fromJson(e)).toList();
  }

  Future<void> refresh() async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() => _fetchProducts(page: 1));
  }

  Future<void> loadMore(int page) async {
    final current = state.value ?? [];
    final more = await _fetchProducts(page: page);
    state = AsyncData([...current, ...more]);
  }
}

Provider 选型决策树

你需要什么样的状态?
│
├── 只读数据,无需修改?
│   ├── 同步数据 → @riverpod 函数(简单值)
│   ├── 异步数据(API)→ @riverpod Future 函数
│   └── 实时流数据 → @riverpod Stream 函数
│
└── 可修改状态?
    ├── 同步状态 → @riverpod class(Notifier)
    └── 异步状态 → @riverpod class + Future(AsyncNotifier)

⚡ 3. Riverpod 3.x 新特性深度解析

3.1 Mutation(副作用状态追踪)

3.x 最重磅特性!让 UI 能追踪"提交/删除/更新"等操作的 loading/success/error 状态。

@riverpod
class CartController extends _$CartController {
  @override
  List<CartItem> build() => [];

  // 标记为 @mutation,UI 可追踪此操作的状态
  @mutation
  Future<void> addItem(CartItem item) async {
    await ref.read(cartRepositoryProvider).addItem(item);
    state = [...state, item];
  }

  @mutation
  Future<void> removeItem(String itemId) async {
    await ref.read(cartRepositoryProvider).removeItem(itemId);
    state = state.where((e) => e.id != itemId).toList();
  }
}

// UI 中使用 — 每个 mutation 有独立的状态
class AddToCartButton extends ConsumerWidget {
  final CartItem item;
  const AddToCartButton({super.key, required this.item});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 监听 addItem 这个 mutation 的状态
    final addMutation = ref.watch(
      cartControllerProvider.addItem,
    );

    return ElevatedButton(
      onPressed: addMutation.isLoading
          ? null  // 加载中禁用按钮
          : () => ref.read(cartControllerProvider.notifier).addItem(item),
      child: addMutation.isLoading
          ? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2))
          : const Text('加入购物车'),
    );
  }
}
对比维度 2.x 做法 3.x Mutation
追踪按钮加载态 手动维护 isLoading 变量 自动追踪,mutation.isLoading
多个操作并行 状态互相冲突 每个 mutation 独立状态
错误处理 手动 try/catch + 状态同步 mutation.hasError 自动管理

3.2 Ref.mounted(安全检查)

@riverpod
class SearchController extends _$SearchController {
  @override
  List<SearchResult> build() => [];

  Future<void> search(String query) async {
    state = const AsyncLoading();
    final results = await ref.read(searchRepositoryProvider).search(query);

    // 3.x 新增:检查 Provider 是否还活着(防止内存泄漏)
    if (!ref.mounted) return;  // 如果已销毁,直接返回

    state = AsyncData(results);
  }
}

3.3 自动重试(Automatic Retry)

// 3.x 默认开启:异步 Provider 失败后自动重试
// 无需手动配置,框架自动处理瞬时错误(如网络抖动)

// 如果要自定义重试策略
@Riverpod(retry: myRetryLogic)
Future<UserProfile> userProfile(Ref ref) async {
  return ref.read(userRepositoryProvider).getProfile();
}

Duration? myRetryLogic(int retryCount, Object error) {
  // 最多重试 3 次,指数退避
  if (retryCount > 3) return null;  // 停止重试
  return Duration(seconds: math.pow(2, retryCount).toInt());
}

3.4 统一 Ref(告别泛型)

// 2.x(旧写法)— 需要泛型
// String myProvider(Ref<String> ref) => 'hello';

// 3.x(新写法)— 统一 Ref,无泛型
@riverpod
String myProvider(Ref ref) => 'hello';

🔗 4. 依赖注入与 Provider 组合

Provider 之间的依赖

// 基础设施层
@riverpod
Dio dio(Ref ref) {
  final token = ref.watch(authTokenProvider);
  return Dio(BaseOptions(
    baseUrl: 'https://api.example.com',
    headers: token != null ? {'Authorization': 'Bearer $token'} : null,
  ));
}

// 数据层 — 依赖 Dio
@riverpod
ProductRepository productRepository(Ref ref) {
  return ProductRepositoryImpl(dio: ref.watch(dioProvider));
}

// 业务层 — 依赖 Repository
@riverpod
Future<List<Product>> featuredProducts(Ref ref) async {
  final repo = ref.watch(productRepositoryProvider);
  return repo.getFeatured();
}

// 当 authToken 变化 → Dio 重建 → Repository 重建 → 数据自动刷新
// 🔥 这就是 Riverpod 的响应式依赖链!

依赖链可视化

authTokenProvider(登录状态变化)
    ↓ ref.watch
dioProvider(重建 Dio 实例,带新 Token)
    ↓ ref.watch
productRepositoryProvider(重建 Repository)
    ↓ ref.watch
featuredProductsProvider(自动重新请求数据)
    ↓ ref.watch
UI(自动重建展示新数据)

Family Provider(参数化)

// 3.x 新写法:参数通过构造函数传入
@riverpod
class ProductDetail extends _$ProductDetail {
  @override
  Future<Product> build({required String productId}) async {
    final repo = ref.watch(productRepositoryProvider);
    return repo.getById(productId);
  }
}

// 使用时
ref.watch(productDetailProvider(productId: 'prod-123'));

🖥 5. UI 集成模式

ConsumerWidget(推荐)

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

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

    return Scaffold(
      body: productsAsync.when(
        data: (products) => ProductListView(products: products),
        loading: () => const ShimmerLoading(),
        error: (e, st) => ErrorView(
          message: e.toString(),
          onRetry: () => ref.invalidate(productListProvider),
        ),
      ),
    );
  }
}

Consumer(局部监听,减少重建范围)

class ProfileScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // 这部分不需要状态,不会重建
        const HeaderWidget(),

        // 只有这部分监听状态,精确重建
        Consumer(
          builder: (context, ref, child) {
            final user = ref.watch(userProvider);
            return Text(user.value?.name ?? '加载中...');
          },
        ),

        // 这部分也不会重建
        const FooterWidget(),
      ],
    );
  }
}

ref.listen(副作用监听)

class LoginScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 监听状态变化执行副作用(不影响 UI 重建)
    ref.listen(loginControllerProvider, (prev, next) {
      if (next.hasError) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('登录失败: ${next.error}')),
        );
      }
      if (next.hasValue && next.value != null) {
        context.go('/home');  // 登录成功跳转
      }
    });

    final loginState = ref.watch(loginControllerProvider);

    return ElevatedButton(
      onPressed: loginState.isLoading ? null : () {
        ref.read(loginControllerProvider.notifier).login(
          email: emailController.text,
          password: passwordController.text,
        );
      },
      child: loginState.isLoading
          ? const CircularProgressIndicator()
          : const Text('登录'),
    );
  }
}

🧪 6. 测试:Riverpod 的杀手级优势

Provider 单元测试

// test/providers/counter_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:riverpod/riverpod.dart';

void main() {
  group('CounterProvider', () {
    test('初始值为 0', () {
      final container = ProviderContainer();
      addTearDown(container.dispose);

      expect(container.read(counterProvider), 0);
    });

    test('increment 增加 1', () {
      final container = ProviderContainer();
      addTearDown(container.dispose);

      container.read(counterProvider.notifier).increment();
      expect(container.read(counterProvider), 1);
    });

    test('多次操作', () {
      final container = ProviderContainer();
      addTearDown(container.dispose);

      final notifier = container.read(counterProvider.notifier);
      notifier.increment();
      notifier.increment();
      notifier.decrement();
      expect(container.read(counterProvider), 1);
    });
  });
}

Mock 依赖(Override)

// 测试时替换真实 API 为 Mock
test('获取商品列表', () async {
  final mockRepo = MockProductRepository();
  when(mockRepo.getAll()).thenAnswer((_) async => [
    Product(id: '1', name: '测试商品', price: 99.0),
  ]);

  final container = ProviderContainer(
    overrides: [
      // 🔥 核心能力:用 Mock 替换真实依赖
      productRepositoryProvider.overrideWithValue(mockRepo),
    ],
  );
  addTearDown(container.dispose);

  // 等待异步 Provider 完成
  await container.read(productListProvider.future);
  final products = container.read(productListProvider).value!;

  expect(products.length, 1);
  expect(products[0].name, '测试商品');
});

Widget 测试

testWidgets('商品列表展示正确', (tester) async {
  await tester.pumpWidget(
    ProviderScope(
      overrides: [
        productListProvider.overrideWith((ref) async {
          return [
            Product(id: '1', name: 'Flutter 实战', price: 59.0),
            Product(id: '2', name: 'Dart 入门', price: 39.0),
          ];
        }),
      ],
      child: const MaterialApp(home: ProductScreen()),
    ),
  );

  await tester.pumpAndSettle();

  expect(find.text('Flutter 实战'), findsOneWidget);
  expect(find.text('Dart 入门'), findsOneWidget);
});

📋 7. 生产级最佳实践

项目结构

lib/
├── core/
│   └── providers/
│       ├── dio_provider.dart       # 网络层
│       └── storage_provider.dart   # 缓存层
├── features/
│   └── product/
│       ├── data/
│       │   └── product_repository.dart
│       ├── domain/
│       │   └── product.dart
│       └── presentation/
│           ├── controllers/
│           │   └── product_controller.dart  # @riverpod Notifier
│           ├── screens/
│           │   └── product_screen.dart      # ConsumerWidget
│           └── widgets/
│               └── product_card.dart        # StatelessWidget

常见错误速查

错误 后果 修复
build() 中用 ref.read 状态变化 UI 不更新 读数据用 ref.watch,写操作用 ref.read
callback 中用 ref.watch 不必要的监听和重建 callback 中用 ref.read
忘记 ref.mounted 检查 异步完成后 Provider 已销毁 异步操作后加 if (!ref.mounted) return
ProviderScope 嵌套混乱 状态隔离不符合预期 全局只用一个根 ProviderScope
不用代码生成手写 Provider 易出错,写法冗长 统一用 @riverpod + build_runner

✅ Riverpod 掌握 Checklist

基础掌握

  • 理解 Provider / Notifier / Ref 三大角色
  • 能用 @riverpod 创建各种类型 Provider
  • 掌握 ref.watch vs ref.read vs ref.listen 的区别
  • 理解 AsyncValue.when() 三态处理
  • 能运行 build_runner 生成代码

进阶掌握

  • 掌握 Provider 依赖链和响应式刷新
  • 使用 Family Provider 传参
  • 使用 Mutation 追踪副作用状态
  • 使用 ref.invalidate() 手动刷新
  • 理解 keepAlive 和自动销毁机制

生产级

  • 能用 ProviderContainer + Override 写单元测试
  • 能用 ProviderScope overrides 写 Widget 测试
  • 配置 riverpod_lint 静态检查
  • 使用 Riverpod DevTools 调试

Provider 是"推"模型 — 你告诉 Widget 数据在哪,Widget 被动接收。 Riverpod 是"拉"模型 — Widget 主动声明"我需要什么",框架负责送达和更新。 从"推"到"拉"的思维转变,就是从初级到高级 Flutter 开发者的跨越。

❌
❌