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

Flutter使用Provider实现收藏列表增删的问题及解决

问题:Flutter Provider收藏功能重复添加实例

我用Flutter的Provider开发收藏旅程列表,现有代码能添加旅程,但再次点击收藏时following.list.contains(favorite)永远返回false,导致重复添加新的Favorite实例。原因是每次创建新对象,即使内容相同,对象引用不同,contains判断失效,求正确的增删实现方式。


相关代码与日志

Favorite.dart

import 'package:flutter/material.dart';

class Favorite extends ChangeNotifier {
  final Text date;
  final Text time;
  final Text source;
  final Text destination;
  final Text price;

  Favorite(this.date, this.time, this.source, this.destination, this.price);
}

class Following extends ChangeNotifier {
  List<Favorite> list = [];
  add(favorite) {
    list.add(favorite);
    notifyListeners();
  }

  remove(favorite) {
    list.remove(favorite);
    notifyListeners();
  }
}

journeys.dart(旅程列表页面)

FirebaseAnimatedList(
  shrinkWrap: true,
  query: Consts.journeyRef.child("journeys"),
  itemBuilder: (BuildContext context, DataSnapshot snapshot,
      Animation animation, int index) {
    try {
      return Consumer<Following>(
        builder:
            (BuildContext context, value, child) {
          return Dismissible(
            key: UniqueKey(),
            secondaryBackground: buildSwipeActionRight(),
            background: buildSwipeActionLeft(),
            child: ListView(
                scrollDirection: Axis.vertical,
                shrinkWrap: true,
                children: <Widget>[
                  eachTile(
                    following,
                    Favorite(
                    Text(Map<String, dynamic>.from(
                      snapshot.value as Map)[Consts.pathDateJourney]),
                    Text(Map<String, dynamic>.from(
                        snapshot.value as Map)[Consts.pathTimeJourney]),
                    Text(Map<String, dynamic>.from(
                        snapshot.value as Map)[Consts.pathSourceCity]),
                    Text(Map<String, dynamic>.from(
                        snapshot.value as Map)[Consts.pathDestinationCity]),
                    Text(Map<String, dynamic>.from(
                        snapshot.value as Map)[Consts.pathPriceJourney]),),)

                ]),
            onDismissed: (direction) =>
                dismissItem(context, index, direction),
          );
        },
      );
    } catch (e) {
      customSnackBar(context, e.toString(), 3, Colors.white24, Colors.brown, 17);
      return const Text(
          "We Can't show you information disabled by the Administrator");
    }
  }),

eachTile.dart

ListTile eachTile(following, favorite) {
  return ListTile(
    leading: Column(
      children: [
        favorite.date,
        const SizedBox(
          height: 10,
        ),
        favorite.time,
      ],
    ),
    title: Row(
      children: [
        favorite.source,
        const SizedBox(
          width: 50,
        ),
        favorite.price
      ],
    ),
    subtitle: favorite.destination,
    trailing: IconButton(
      icon: following.list.contains(favorite)
          ? const Icon(Icons.favorite)
          : const Icon(Icons.favorite_border_outlined),
      onPressed: () {
        print(following.list.contains(favorite));
// 该打印语句始终返回false
        if (following.list.contains(favorite)) {
          following.remove(favorite);
        } else {
          following.add(favorite);
        }
        print(following.list.contains(favorite));
// 该打印语句始终返回true
        print(following.list);
// 该打印语句显示每次执行都会添加新实例
      },
    ),
  );
}

打印日志

I/flutter (  578): false
I/flutter (  578): true
I/flutter (  578): [Instance of 'Favorite']
V/AutofillManager(  578): requestHideFillUi(null): anchor = null
I/flutter (  578): false
I/flutter (  578): true
I/flutter (  578): [Instance of 'Favorite', Instance of 'Favorite']
V/AutofillManager(  578): requestHideFillUi(null): anchor = null
I/flutter (  578): false
I/flutter (  578): true
I/flutter (  578): [Instance of 'Favorite', Instance of 'Favorite', Instance of 'Favorite']

解决方案

问题核心是对象引用不同导致contains判断失效:每次调用eachTile时都会创建新的Favorite实例,即使内容和已收藏的一致,它们的内存地址不同,contains(默认比较对象引用)会认为是不同对象。

步骤1:修改Favorite类,重写相等判断方法

让Favorite根据内容而非引用判断是否相等,同时把数据模型和UI组件分离:

import 'package:flutter/material.dart';

// 数据模型不需要继承ChangeNotifier,只负责存储数据
class Favorite {
  final String date;
  final String time;
  final String source;
  final String destination;
  final String price;

  Favorite(this.date, this.time, this.source, this.destination, this.price);

  // 重写==,根据内容判断是否相等
  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is Favorite &&
          runtimeType == other.runtimeType &&
          date == other.date &&
          time == other.time &&
          source == other.source &&
          destination == other.destination &&
          price == other.price;

  // 配合==重写hashCode
  @override
  int get hashCode =>
      date.hashCode ^
      time.hashCode ^
      source.hashCode ^
      destination.hashCode ^
      price.hashCode;
}

class Following extends ChangeNotifier {
  List<Favorite> list = [];
  
  // 自定义contains方法,根据内容判断是否存在
  bool contains(Favorite favorite) {
    return list.any((item) => item == favorite);
  }

  void add(Favorite favorite) {
    if (!contains(favorite)) {
      list.add(favorite);
      notifyListeners();
    }
  }

  void remove(Favorite favorite) {
    // 根据内容移除匹配的实例
    list.removeWhere((item) => item == favorite);
    notifyListeners();
  }
}

步骤2:修改journeys.dart中创建Favorite的逻辑

从Firebase提取字符串数据,直接创建Favorite实例(不再用Text包裹):

FirebaseAnimatedList(
  shrinkWrap: true,
  query: Consts.journeyRef.child("journeys"),
  itemBuilder: (BuildContext context, DataSnapshot snapshot,
      Animation animation, int index) {
    try {
      final journeyData = Map<String, dynamic>.from(snapshot.value as Map);
      // 先提取字符串数据
      final date = journeyData[Consts.pathDateJourney];
      final time = journeyData[Consts.pathTimeJourney];
      final source = journeyData[Consts.pathSourceCity];
      final destination = journeyData[Consts.pathDestinationCity];
      final price = journeyData[Consts.pathPriceJourney];
      // 创建Favorite实例
      final favorite = Favorite(date, time, source, destination, price);

      return Consumer<Following>(
        builder: (BuildContext context, following, child) {
          return Dismissible(
            key: UniqueKey(),
            secondaryBackground: buildSwipeActionRight(),
            background: buildSwipeActionLeft(),
            child: ListView(
              scrollDirection: Axis.vertical,
              shrinkWrap: true,
              children: <Widget>[
                eachTile(following, favorite)
              ],
            ),
            onDismissed: (direction) => dismissItem(context, index, direction),
          );
        },
      );
    } catch (e) {
      customSnackBar(context, e.toString(), 3, Colors.white24, Colors.brown, 17);
      return const Text("管理员已禁用该信息,无法显示");
    }
  }),

步骤3:修改eachTile.dart,在UI层创建Text组件

把数据转换成UI组件的逻辑移到UI层:

ListTile eachTile(Following following, Favorite favorite) {
  return ListTile(
    leading: Column(
      children: [
        Text(favorite.date),
        const SizedBox(height: 10),
        Text(favorite.time),
      ],
    ),
    title: Row(
      children: [
        Text(favorite.source),
        const SizedBox(width: 50),
        Text(favorite.price)
      ],
    ),
    subtitle: Text(favorite.destination),
    trailing: IconButton(
      icon: following.contains(favorite)
          ? const Icon(Icons.favorite)
          : const Icon(Icons.favorite_border_outlined),
      onPressed: () {
        if (following.contains(favorite)) {
          following.remove(favorite);
        } else {
          following.add(favorite);
        }
      },
    ),
  );
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.17 01:10:27