You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter结合Riverpod:如何仅更新模型中的单个属性?

Riverpod 实现产品多选状态管理方案

从 GetX 切换到 Riverpod 核心要适应不可变状态优先的设计思路,不能像 GetX 那样直接修改对象的 Rx 属性。针对你异步加载产品列表并实现多选的需求,提供两种实用方案:

方案一:将产品列表与选中状态绑定管理

这种方式适合需要将选中状态和产品数据耦合的场景,通过 StateNotifier 统一管理异步加载状态和选中状态:

1. 改造不可变的 ProductModel

添加 copyWith 方法,用于生成新的对象(Riverpod 要求状态不可变,不能直接修改原对象属性):

class ProductModel {
  final String name;
  final bool isSelected;

  ProductModel({
    required this.name,
    required this.isSelected,
  });

  // 生成新对象的拷贝方法,用于修改选中状态
  ProductModel copyWith({
    String? name,
    bool? isSelected,
  }) {
    return ProductModel(
      name: name ?? this.name,
      isSelected: isSelected ?? this.isSelected,
    );
  }
}

2. 创建 StateNotifier 管理状态

class ProductsState {
  final AsyncValue<List<ProductModel>> productsAsync;

  ProductsState({required this.productsAsync});
}

class ProductsNotifier extends StateNotifier<ProductsState> {
  ProductsNotifier() : super(ProductsState(productsAsync: const AsyncLoading()));

  // 异步加载产品列表
  Future<void> loadProducts(String orderId) async {
    state = state.copyWith(productsAsync: const AsyncLoading());
    try {
      // 替换为你的接口调用逻辑
      final apiProducts = await fetchProductsFromApi(orderId);
      // 初始化所有产品为未选中状态
      final initialProducts = apiProducts.map((p) => ProductModel(name: p.name, isSelected: false)).toList();
      state = state.copyWith(productsAsync: AsyncData(initialProducts));
    } catch (e) {
      state = state.copyWith(productsAsync: AsyncError(e, StackTrace.current));
    }
  }

  // 切换指定索引产品的选中状态
  void toggleProductSelection(int index) {
    state.productsAsync.whenData((products) {
      final updatedProducts = List<ProductModel>.from(products);
      updatedProducts[index] = updatedProducts[index].copyWith(
        isSelected: !updatedProducts[index].isSelected,
      );
      state = state.copyWith(productsAsync: AsyncData(updatedProducts));
    });
  }
}

// 关联 orderId 的家族 Provider
final productsNotifierProvider = StateNotifierProvider.family<ProductsNotifier, ProductsState, String>((ref, orderId) {
  final notifier = ProductsNotifier();
  notifier.loadProducts(orderId);
  return notifier;
});

3. UI 层调用

@override
Widget build(BuildContext context, WidgetRef ref) {
  final productsState = ref.watch(productsNotifierProvider(orderId));

  return productsState.productsAsync.when(
    loading: () => const CircularProgressIndicator(),
    error: (error, stack) => Text('加载失败: $error'),
    data: (products) {
      return Scaffold(
        body: ListView.builder(
          itemCount: products.length,
          itemBuilder: (context, index) {
            final product = products[index];
            return ExpansionTile(
              leading: InkWell(
                customBorder: const CircleBorder(),
                onTap: () {
                  // 调用 notifier 切换选中状态
                  ref.read(productsNotifierProvider(orderId).notifier).toggleProductSelection(index);
                },
                child: Icon(
                  product.isSelected ? Icons.check_circle_rounded : Icons.circle_outlined,
                  color: Colors.green,
                ),
              ),
              title: Text(product.name),
            );
          },
        ),
      );
    },
  );
}

方案二:分离产品数据与选中状态(推荐)

这种方式更符合 Riverpod 的单一职责原则,用单独的 StateProvider 管理选中的产品 ID 集合,产品模型不需要包含选中状态:

1. 简化 ProductModel

class ProductModel {
  final String id; // 产品唯一标识,必须要有
  final String name;

  ProductModel({
    required this.id,
    required this.name,
  });
}

2. 创建两个独立的 Provider

// 仅负责异步加载产品列表的 FutureProvider
final productsProvider = FutureProvider.family<List<ProductModel>, String>((ref, orderId) async {
  return fetchProductsFromApi(orderId); // 替换为你的接口调用
});

// 管理选中产品 ID 集合的 StateProvider
final selectedProductIdsProvider = StateProvider.family<Set<String>, String>((ref, orderId) => {});

3. UI 层调用

@override
Widget build(BuildContext context, WidgetRef ref) {
  final productsAsync = ref.watch(productsProvider(orderId));
  final selectedIds = ref.watch(selectedProductIdsProvider(orderId));

  return productsAsync.when(
    loading: () => const CircularProgressIndicator(),
    error: (error, stack) => Text('加载失败: $error'),
    data: (products) {
      return Scaffold(
        body: ListView.builder(
          itemCount: products.length,
          itemBuilder: (context, index) {
            final product = products[index];
            final isSelected = selectedIds.contains(product.id);
            
            return ExpansionTile(
              leading: InkWell(
                customBorder: const CircleBorder(),
                onTap: () {
                  final notifier = ref.read(selectedProductIdsProvider(orderId).notifier);
                  if (isSelected) {
                    notifier.state.remove(product.id);
                  } else {
                    notifier.state.add(product.id);
                  }
                },
                child: Icon(
                  isSelected ? Icons.check_circle_rounded : Icons.circle_outlined,
                  color: Colors.green,
                ),
              ),
              title: Text(product.name),
            );
          },
        ),
      );
    },
  );
}

方案对比

  • 方案一:状态耦合,适合简单场景,修改选中状态时需要生成新的产品列表。
  • 方案二:状态分离,更灵活易维护,后续扩展选中逻辑(如全选、反选)更方便,推荐使用。

内容的提问来源于stack exchange,提问作者Naografix

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 10:02:52