Skip to content

Riverpod 最佳实践:WidgetsBinding + Provider Family 完整指南

本文综合讲解如何在实际项目中正确使用 Riverpod,结合 WidgetsBinding 实现优雅的状态初始化。

经典场景:首帧后初始化

问题描述

dart
// ❌ 常见错误写法
class MyPage extends ConsumerStatefulWidget {
  @override
  ConsumerState<MyPage> createState() => _MyPageState();
}

class _MyPageState extends ConsumerState<MyPage> {
  @override
  void initState() {
    super.initState();
    // 错误1:可能还没有布局信息
    ref.read(provider).init();
    
    // 错误2:在 initState 中调用 setState
    setState(() {});
    
    // 错误3:在 initState 中显示对话框
    showDialog(...); // 报错!
  }
  
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

正确解决方案

dart
class MyPage extends ConsumerStatefulWidget {
  const MyPage({super.key, required this.id});
  final String id;

  @override
  ConsumerState<MyPage> createState() => _MyPageState();
}

class _MyPageState extends ConsumerState<MyPage> {
  @override
  void initState() {
    super.initState();

    // ✅ 正确:使用 addPostFrameCallback
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (!mounted) return; // 必须检查
      
      ref.read(myProvider(widget.id).notifier).init();
    });
  }

  @override
  Widget build(BuildContext context) {
    final state = ref.watch(myProvider(widget.id));
    
    return Scaffold(
      body: state.loading
          ? const CircularProgressIndicator()
          : Text(state.data),
    );
  }
}

完整实战示例

场景:商品编辑页面

这是一个真实项目中的典型场景:根据商品ID加载数据并编辑。

1. 定义状态和控制器

dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// ===== 状态定义 ===== //

@immutable
class ProductState {
  const ProductState({
    this.inited = false,
    this.loading = false,
    this.error,
    this.name = '',
    this.price = 0.0,
    this.description = '',
  });

  final bool inited;
  final bool loading;
  final String? error;
  final String name;
  final double price;
  final String description;

  ProductState copyWith({
    bool? inited,
    bool? loading,
    String? error,
    String? name,
    double? price,
    String? description,
  }) {
    return ProductState(
      inited: inited ?? this.inited,
      loading: loading ?? this.loading,
      error: error,
      name: name ?? this.name,
      price: price ?? this.price,
      description: description ?? this.description,
    );
  }
}

// ===== 控制器定义 ===== //

class ProductController extends FamilyNotifier<ProductState, String> {
  @override
  ProductState build(String productId) {
    // 为每个 productId 创建独立的初始状态
    return const ProductState();
  }

  // 复杂逻辑:初始化加载,确保幂等性
  Future<void> init() async {
    // 防止重复初始化
    if (state.inited) {
      print('已初始化,跳过: $arg');
      return;
    }

    print('开始初始化: $arg');
    state = state.copyWith(loading: true);

    try {
      // 模拟网络请求
      final data = await _loadProduct(arg);
      
      state = state.copyWith(
        inited: true,
        loading: false,
        name: data['name'],
        price: data['price'],
        description: data['description'],
      );
      
      print('初始化完成: $arg');
    } catch (e) {
      state = state.copyWith(
        loading: false,
        error: e.toString(),
      );
      
      print('初始化失败: $arg, $e');
    }
  }

  Future<Map<String, dynamic>> _loadProduct(String productId) async {
    // 模拟API调用延迟
    await Future.delayed(const Duration(seconds: 1));
    
    // 模拟返回数据
    return {
      'name': '商品-$productId',
      'price': 99.99,
      'description': '这是商品 $productId 的描述',
    };
  }

  // 更新字段
  void updateName(String name) {
    state = state.copyWith(name: name);
  }

  void updatePrice(double price) {
    state = state.copyWith(price: price);
  }

  void updateDescription(String description) {
    state = state.copyWith(description: description);
  }

  // 提交保存
  Future<void> save() async {
    state = state.copyWith(loading: true, error: null);

    try {
      await _saveProduct();
      
      state = state.copyWith(loading: false);
      print('保存成功: $arg');
    } catch (e) {
      state = state.copyWith(
        loading: false,
        error: e.toString(),
      );
      
      print('保存失败: $arg, $e');
      rethrow;
    }
  }

  Future<void> _saveProduct() async {
    await Future.delayed(const Duration(seconds: 1));
    print('提交数据: 商品ID=$arg, 名称=${state.name}, 价格=${state.price}');
  }

  // 重置状态
  void reset() {
    state = const ProductState();
  }
}

// ===== Provider 定义 ===== //

final productControllerProvider =
    NotifierProvider.family<ProductController, ProductState, String>(
  ProductController.new,
);

2. 实现页面

dart
class ProductEditPage extends ConsumerStatefulWidget {
  const ProductEditPage({super.key, required this.productId});
  
  final String productId;

  @override
  ConsumerState<ProductEditPage> createState() => _ProductEditPageState();
}

class _ProductEditPageState extends ConsumerState<ProductEditPage> {
  // 表单控制器
  late final TextEditingController _nameController;
  late final TextEditingController _priceController;
  late final TextEditingController _descController;

  @override
  void initState() {
    super.initState();

    // 初始化控制器
    _nameController = TextEditingController();
    _priceController = TextEditingController();
    _descController = TextEditingController();

    // 复杂逻辑:等首帧绘制完成后初始化数据
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (!mounted) return;

      // 复杂逻辑:触发数据加载
      ref.read(productControllerProvider(widget.productId).notifier).init();
    });

    // 复杂逻辑:监听状态变化,同步到表单控制器
    ref.listenManual(productControllerProvider(widget.productId), (prev, next) {
      if (!mounted) return;
      
      // 数据加载完成后,更新表单
      if (next.inited && !next.loading) {
        _nameController.text = next.name;
        _priceController.text = next.price.toString();
        _descController.text = next.description;
      }
      
      // 显示错误提示
      if (next.error != null) {
        _showError(next.error!);
      }
    });
  }

  @override
  void dispose() {
    _nameController.dispose();
    _priceController.dispose();
    _descController.dispose();
    super.dispose();
  }

  void _showError(String message) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text(message)),
    );
  }

  Future<void> _handleSave() async {
    try {
      await ref
          .read(productControllerProvider(widget.productId).notifier)
          .save();
      
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('保存成功')),
        );
        Navigator.pop(context);
      }
    } catch (e) {
      // 错误已在 controller 中处理
    }
  }

  @override
  Widget build(BuildContext context) {
    final state = ref.watch(productControllerProvider(widget.productId));

    return Scaffold(
      appBar: AppBar(
        title: Text('编辑商品 ${widget.productId}'),
        actions: [
          if (state.loading)
            const Center(
              child: Padding(
                padding: EdgeInsets.symmetric(horizontal: 16.0),
                child: SizedBox(
                  width: 20,
                  height: 20,
                  child: CircularProgressIndicator(strokeWidth: 2),
                ),
              ),
            ),
        ],
      ),
      body: !state.inited
          ? const Center(child: CircularProgressIndicator())
          : SingleChildScrollView(
              padding: const EdgeInsets.all(16.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  TextField(
                    controller: _nameController,
                    decoration: const InputDecoration(
                      labelText: '商品名称',
                      border: OutlineInputBorder(),
                    ),
                    onChanged: (value) {
                      ref
                          .read(productControllerProvider(widget.productId).notifier)
                          .updateName(value);
                    },
                  ),
                  const SizedBox(height: 16),
                  TextField(
                    controller: _priceController,
                    decoration: const InputDecoration(
                      labelText: '价格',
                      border: OutlineInputBorder(),
                      prefixText: '¥ ',
                    ),
                    keyboardType: TextInputType.number,
                    onChanged: (value) {
                      final price = double.tryParse(value) ?? 0.0;
                      ref
                          .read(productControllerProvider(widget.productId).notifier)
                          .updatePrice(price);
                    },
                  ),
                  const SizedBox(height: 16),
                  TextField(
                    controller: _descController,
                    decoration: const InputDecoration(
                      labelText: '描述',
                      border: OutlineInputBorder(),
                    ),
                    maxLines: 5,
                    onChanged: (value) {
                      ref
                          .read(productControllerProvider(widget.productId).notifier)
                          .updateDescription(value);
                    ),
                  ),
                  const SizedBox(height: 32),
                  ElevatedButton(
                    onPressed: state.loading ? null : _handleSave,
                    child: Padding(
                      padding: const EdgeInsets.all(12.0),
                      child: Text(
                        state.loading ? '保存中...' : '保存',
                        style: const TextStyle(fontSize: 16),
                      ),
                    ),
                  ),
                ],
              ),
            ),
    );
  }
}

3. 使用示例

dart
// 在路由中使用
Navigator.push(
  context,
  MaterialPageRoute(
    builder: (context) => const ProductEditPage(productId: 'prod-123'),
  ),
);

// 在列表中使用
class ProductListPage extends ConsumerWidget {
  const ProductListPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final products = ['prod-1', 'prod-2', 'prod-3'];

    return Scaffold(
      appBar: AppBar(title: const Text('商品列表')),
      body: ListView.builder(
        itemCount: products.length,
        itemBuilder: (context, index) {
          final productId = products[index];
          
          return ListTile(
            title: Text('商品 $productId'),
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => ProductEditPage(
                    productId: productId,
                  ),
                ),
              );
            },
          );
        },
      ),
    );
  }
}

最佳实践总结

1. 初始化时机

dart
// ✅ 推荐:addPostFrameCallback + mounted 检查
@override
void initState() {
  super.initState();
  WidgetsBinding.instance.addPostFrameCallback((_) {
    if (!mounted) return;
    ref.read(provider.notifier).init();
  });
}

// ❌ 避免:直接在 initState
@override
void initState() {
  super.initState();
  ref.read(provider.notifier).init(); // 可能太早
}

// ❌ 避免:在 build 中
@override
Widget build(BuildContext context) {
  ref.read(provider.notifier).init(); // 每次 rebuild 都调用!
  return Container();
}

2. ref 使用规则

dart
// watch:监听状态,自动重建
final state = ref.watch(provider);

// read:一次性读取,调用方法
ref.read(provider.notifier).doSomething();

// listen:监听变化,执行副作用
ref.listen(provider, (prev, next) {
  if (next.error != null) {
    showError(next.error!);
  }
});

// listenManual:手动管理监听器
ref.listenManual(provider, (prev, next) {
  // 不会自动取消,需要在 dispose 中处理
});

3. 幂等性保护

dart
class MyController extends Notifier<MyState> {
  @override
  MyState build() => const MyState();

  Future<void> init() async {
    // ✅ 防止重复初始化
    if (state.inited) return;
    
    // ✅ 使用标志位
    if (_isInitializing) return;
    _isInitializing = true;
    
    try {
      // 执行初始化...
      state = state.copyWith(inited: true);
    } finally {
      _isInitializing = false;
    }
  }
  
  bool _isInitializing = false;
}

4. 错误处理

dart
class MyController extends Notifier<MyState> {
  @override
  MyState build() => const MyState();

  Future<void> loadData() async {
    state = state.copyWith(loading: true, error: null);

    try {
      final data = await api.fetchData();
      state = state.copyWith(
        loading: false,
        data: data,
      );
    } catch (e, stackTrace) {
      // ✅ 记录错误
      print('加载失败: $e');
      print(stackTrace);
      
      // ✅ 更新状态
      state = state.copyWith(
        loading: false,
        error: e.toString(),
      );
      
      // ✅ 可选:重新抛出
      rethrow;
    }
  }
}

5. 资源清理

dart
class MyPage extends ConsumerStatefulWidget {
  @override
  ConsumerState<MyPage> createState() => _MyPageState();
}

class _MyPageState extends ConsumerState<MyPage> {
  late final TextEditingController _controller;

  @override
  void initState() {
    super.initState();
    _controller = TextEditingController();
  }

  @override
  void dispose() {
    // ✅ 清理控制器
    _controller.dispose();
    
    // ✅ 可选:清理 Provider(如果使用 autoDispose 可省略)
    // ref.invalidate(myProvider);
    
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

常见陷阱与解决方案

陷阱 1:忘记检查 mounted

dart
// ❌ 错误
WidgetsBinding.instance.addPostFrameCallback((_) {
  ref.read(provider.notifier).init();
  setState(() {}); // 可能报错!
});

// ✅ 正确
WidgetsBinding.instance.addPostFrameCallback((_) {
  if (!mounted) return; // 必须检查
  ref.read(provider.notifier).init();
});

陷阱 2:在 build 中调用方法

dart
// ❌ 错误
@override
Widget build(BuildContext context) {
  ref.read(provider.notifier).init(); // 每次 rebuild 都调用
  return Container();
}

// ✅ 正确
@override
void initState() {
  super.initState();
  WidgetsBinding.instance.addPostFrameCallback((_) {
    if (!mounted) return;
    ref.read(provider.notifier).init();
  });
}

陷阱 3:循环依赖

dart
// ❌ 错误
final providerA = Provider((ref) {
  final b = ref.watch(providerB); // 依赖 B
  return A(b);
});

final providerB = Provider((ref) {
  final a = ref.watch(providerA); // 依赖 A,循环!
  return B(a);
});

// ✅ 正确:重新设计依赖关系
final baseProvider = Provider((ref) => Base());

final providerA = Provider((ref) {
  final base = ref.watch(baseProvider);
  return A(base);
});

final providerB = Provider((ref) {
  final base = ref.watch(baseProvider);
  return B(base);
});

陷阱 4:过度使用 Family

dart
// ❌ 不推荐:参数是可变对象
final provider = Provider.family<State, List<String>>(...);

// ✅ 推荐:参数是不可变值
final provider = Provider.family<State, String>(...);

// ✅ 推荐:使用 record 或不可变类
final provider = Provider.family<State, ({String keyword, int page})>(...);

总结

核心原则

  1. 等首帧完成:使用 addPostFrameCallback
  2. 检查 mounted:避免访问已销毁的组件
  3. 幂等性保护:防止重复初始化
  4. 错误处理:完善的错误捕获和提示
  5. 资源清理:及时释放资源

代码清单

dart
// 标准初始化模式
@override
void initState() {
  super.initState();
  
  WidgetsBinding.instance.addPostFrameCallback((_) {
    if (!mounted) return;
    ref.read(provider(id).notifier).init();
  });
}

// 标准控制器模式
class MyController extends FamilyNotifier<State, String> {
  @override
  State build(String id) => const State();

  Future<void> init() async {
    if (state.inited) return; // 幂等性
    
    state = state.copyWith(loading: true);
    
    try {
      final data = await loadData(arg);
      state = state.copyWith(inited: true, loading: false, data: data);
    } catch (e) {
      state = state.copyWith(loading: false, error: e.toString());
      rethrow;
    }
  }
}

下一步学习


相关资源:

本站总访问