首页 > 解决方案 > 更改一个 Provider 中的属性会将另一个 Provider 中的属性更改为 List Flutter

问题描述

更新 - 我实际上发现这是一个颤振问题


我有两个提供者,一个是 EntriesProvider,另一个是 EntryProvider。我在创建条目时使用我的 EntryProvider 并使用我的 EntriesProvider 来加载保存到数据库中的所有条目。我遇到了一个问题,我认为这可能是我对如何使用 Providers 的理解。一旦我将数据库数据加载到我的 EntriesProvider 中,我就会将该数据加载到 ListView 中。单击一个项目后,我将该列表中的条目传递到我的视图中以进行填充和编辑。

我的问题是,当我编辑条目而不保存它时,我可以看到 ListView 中发生的更改不是我想要的。我尝试清除 EntryProvider,因为我认为属于它的数据与 EntriesProvider 是分开的。但是现在我尝试了多种方法后不知道。当我只要求 EntryProvider 更新其侦听器时,为什么要更新列表?

class EntryProvider extends ChangeNotifier {
  Entry _entry;
  BuildContext context;

  EntryProvider();

  Entry get getEntry {
    return _entry;
  }

  void setEntryContext(Entry entryToBeSet, BuildContext context) {
    this._entry = entryToBeSet;
    this.context = context;
    notifyListeners();
  }

  void clearEntryContext() {
    this._entry = null;
    this.context = null;
    notifyListeners();
  }

  void addImageToEntry(String imagePath) {
    getEntry.images.add(imagePath);
    notifyListeners();
  }

  void removeImageAt(int index) {
    getEntry.images.removeAt(index);
    notifyListeners();
  }

  void addTagToEntry(String tagText) {
    getEntry.tags.add(tagText);
    notifyListeners();
  }

  void removeTagAt(int index) {
    getEntry.tags.removeAt(index);
    notifyListeners();
  }

  Future<void> saveEntry() async {
    if (getEntry.id != null) {
      await Provider.of<EntriesProvider>(context, listen: false)
          .updateEntry(getEntry);
    } else {
      await Provider.of<EntriesProvider>(context, listen: false)
          .addEntry(getEntry);
    }
  }
}
class EntriesProvider extends ChangeNotifier {
  List<Entry> _entries = [];

  EntriesProvider(this._entries);

  UnmodifiableListView<Entry> get entries => UnmodifiableListView(_entries);

  int get length => _entries.length;

  List<Entry> get getEntriesSortedByDateReversed {
    List<Entry> entriesCopy = entries;
    entriesCopy.sort((a, b) => a.entryDate.compareTo(b.entryDate));

    return entriesCopy.reversed.toList();
  }

  List<Entry> getEntries(DateTime dateTime) {
    List<Entry> entriesToBeSorted = entries
        .where(
          (entry) => DateFormat.yMMMd().format(entry.entryDate).contains(
                DateFormat.yMMMd().format(dateTime),
              ),
        )
        .toList();

    entriesToBeSorted.sort((a, b) {
      return a.entryDate.compareTo(b.entryDate);
    });

    return entriesToBeSorted;
  }
}
class JournalListView extends StatefulWidget {
  bool isDrawerOpen;
  final TransformData transformData;

  JournalListView(this.isDrawerOpen, this.transformData);

  @override
  _JournalListScreenState createState() => _JournalListScreenState();
}

class _JournalListScreenState extends State<JournalListView> {
  List<Entry> entries = [];
  List<Entry> filteredEntries = [];
  DateTime dateTimeSet;

  AppDataModel appDataModel;

  @override
  void initState() {
    super.initState();
    dateTimeSet = DateTime.now();
  }

  Widget _buildEntryList(BuildContext context) {
    return Consumer<EntriesProvider>(builder: (context, entryModel, child) {
      print(entryModel.entries);
      List<Entry> entries = entryModel.getEntries(dateTimeSet);
      return Container(
        constraints: BoxConstraints(
          maxHeight: 650,
          maxWidth: double.infinity,
        ),
        child: Container(
          child: entries.length > 0
              ? ListView.builder(
                  itemCount: entries.length,
                  padding: EdgeInsets.all(2.0),
                  itemBuilder: (context, index) {
                    return InkWell(
                      onTap: () {
                        if (widget.isDrawerOpen) {
                          closeDrawer();
                        } else {
                          Navigator.of(context).push(
                            PageRouteBuilder(
                                transitionDuration: Duration(milliseconds: 650),
                                pageBuilder:
                                    (context, animation, secondaryAnimation) {
                                  final Entry copiedEntry = entries[index]
                                      .copyWith(
                                          id: entries[index].id,
                                          title: entries[index].title,
                                          description:
                                              entries[index].description,
                                          entryDate: entries[index].entryDate,
                                          feelingOnEntry:
                                              entries[index].feelingOnEntry,
                                          images: entries[index].images,
                                          location: entries[index].location,
                                          tags: entries[index].tags,
                                          time: entries[index].time,
                                          weather: entries[index].weather);
                                  Provider.of<EntryProvider>(context, listen: false)
        .setEntryContext(entry, context);
                                  return JournalEntryView(copiedEntry);
                                }),
                          );
                        }
                      },
                      child: Hero(
                        tag: '${entries[index].entryDate}${entries[index].id}',
                        child: _buildEntryLayout(context, entries[index]),
                      ),
                    );
                  },
                )
              : JournalEmpty(
                  'lib/assets/emojis/empty-folder.png',
                  MyLocalizations.of(context).journalListEmpty,
                ),
        ),
      );
    });
  }

  Widget _buildEntryLayout(BuildContext context, Entry entry) {
    int entryLayout = appDataModel.entryLayout;
    Widget entryLayoutWidget;

    switch (entryLayout) {
      case 1:
        entryLayoutWidget = EntryCard1(entry);
        break;
      case 2:
        entryLayoutWidget = EntryCard2(entry);
        break;
      default:
        entryLayoutWidget = EntryCard1(entry);
        break;
    }

    return entryLayoutWidget;
  }

  Widget _buildCalenderStrip(BuildContext context) {
    return Container(
      height: 64,
      margin: const EdgeInsets.all(2.0),
      child: Consumer<EntriesProvider>(
        builder: (context, entryModel, child) {
          return Calendarro(
              startDate: DateUtils.getFirstDayOfMonth(DateTime(2020, 09)),
              endDate: DateUtils.getLastDayOfCurrentMonth(),
              selectedSingleDate: DateTime.now(),
              displayMode: DisplayMode.WEEKS,
              dayTileBuilder: CustomDayBuilder(entryModel.entries),
              onTap: (datetime) {
                if (widget.isDrawerOpen) {
                  closeDrawer();
                }
                setState(() {
                  dateTimeSet = datetime;
                });
              });
        },
      ),
    );
  }

  Widget _buildSearchEntryWidget(BuildContext context) {
    return Consumer<EntriesProvider>(builder: (context, entries, child) {
      return IconButton(
        onPressed: () => showSearch(
          context: context,
          delegate: SearchPage<Entry>(
            items: entries.entries,
            searchLabel: MyLocalizations.of(context).journalListSearchEntries,
            suggestion: Center(
              child: Text(MyLocalizations.of(context).journalListFilterEntries),
            ),
            failure: JournalEmpty(
              'lib/assets/emojis/no_items.png',
              MyLocalizations.of(context).journalListNoEntriesFound,
            ),
            filter: (entry) {
              List<String> filterOn = List<String>();
              filterOn.add(entry.title);
              if (entry.tags != null) {
                entry.tags.forEach((tag) => filterOn.add(tag));
              }
              return filterOn;
            },
            builder: (entry) => InkWell(
              onTap: () {
                Navigator.of(context).push(
                  MaterialPageRoute(
                    builder: (context) => JournalEntryView(entry),
                  ),
                );
              },
              child: EntryCard1(
                entry,
              ),
            ),
          ),
        ),
        icon: Icon(
          Icons.search,
          size: 30,
          color: Theme.of(context).primaryColor,
        ),
      );
    });
  }

  void closeDrawer() {
    setState(() {
      widget.transformData.xOffset = 0;
      widget.transformData.yOffset = 0;
      widget.transformData.scaleFactor = 1;
      widget.isDrawerOpen = false;
    });
  }

  bool isDateChoosenValid() {
    return dateTimeSet.compareTo(DateTime.now()) < 1;
  }

  @override
  Widget build(BuildContext context) {
    appDataModel = Provider.of<AppDataProvider>(context).appDataModel;

    return AnimatedContainer(
      transform: Matrix4.translationValues(
        widget.transformData.xOffset,
        widget.transformData.yOffset,
        0,
      )
        ..scale(widget.transformData.scaleFactor)
        ..rotateY(widget.isDrawerOpen ? -0.5 : 0),
      duration: Duration(milliseconds: 250),
      decoration: BoxDecoration(
        color: Colors.grey[200],
        borderRadius: BorderRadius.circular(
          widget.isDrawerOpen ? 25 : 0.0,
        ),
      ),
      child: GestureDetector(
        onTap: () {
          if (widget.isDrawerOpen) {
            closeDrawer();
          }
        },
        child: ClipRRect(
          borderRadius: BorderRadius.circular(25),
          child: Scaffold(
              body: Column(
                children: [
                  SizedBox(
                    height: 30,
                  ),
                  Container(
                    margin: EdgeInsets.symmetric(horizontal: 20),
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        widget.isDrawerOpen
                            ? IconButton(
                                icon: Icon(
                                  Icons.arrow_back,
                                  size: 30,
                                  color: Theme.of(context).primaryColor,
                                ),
                                onPressed: () {
                                  closeDrawer();
                                },
                              )
                            : IconButton(
                                icon: Icon(
                                  Icons.menu,
                                  size: 30,
                                  color: Theme.of(context).primaryColor,
                                ),
                                onPressed: () {
                                  setState(() {
                                    widget.transformData.xOffset = 260;
                                    widget.transformData.yOffset = 150;
                                    widget.transformData.scaleFactor = 0.7;
                                    widget.isDrawerOpen = true;
                                  });
                                }),
                        Column(
                          crossAxisAlignment: CrossAxisAlignment.end,
                          children: [
                            Text(
                              Constants.APP_NAME,
                              style: TextStyle(
                                fontSize: 28,
                                color: Theme.of(context).primaryColor,
                                fontWeight: FontWeight.w500,
                              ),
                            ),
                          ],
                        ),
                        _buildSearchEntryWidget(context)
                      ],
                    ),
                  ),
                  SizedBox(
                    height: 5,
                  ),
                  _buildCalenderStrip(context),
                  _buildEntryList(context),
                ],
              ),
              floatingActionButtonLocation:
                  FloatingActionButtonLocation.endFloat,
              floatingActionButton: isDateChoosenValid()
                  ? OpenContainer(
                      transitionDuration: Duration(milliseconds: 600),
                      closedBuilder: (BuildContext c, VoidCallback action) =>
                          FloatingActionButton(
                        onPressed: null,
                        child: Icon(
                          Icons.edit,
                          size: 30,
                        ),
                        tooltip:
                            MyLocalizations.of(context).journalListAddEntry,
                        backgroundColor: isDateChoosenValid()
                            ? Theme.of(context).primaryColor
                            : Colors.grey[500],
                        elevation: 8.0,
                      ),
                      closedShape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(100)),
                      openBuilder: (BuildContext c, VoidCallback action) {
                        final entry = Entry(
                          entryDate: dateTimeSet,
                          images: List<Object>(),
                          tags: List<String>(),
                        );
                        return JournalEntryView(entry);
                      },
                      tappable: isDateChoosenValid(),
                    )
                  : SizedBox()),
        ),
      ),
    );
  }
}

class CustomDayBuilder extends DayTileBuilder {
  final List<Entry> entries;
  CustomDayBuilder(this.entries);

  @override
  Widget build(BuildContext context, DateTime date, onTap) {
    Entry entry = entries.firstWhere(
      (entryInEntries) => DateFormat.yMMMd()
          .format(entryInEntries.entryDate)
          .contains(DateFormat.yMMMd().format(date)),
      orElse: () => Entry(),
    );
    return CustomDateTile(
      date: date,
      entry: entry,
      calendarroState: Calendarro.of(context),
      onTap: onTap,
    );
  }
}
class JournalEntryView extends StatefulWidget {
  final Entry entry;

  JournalEntryView(this.entry);

  @override
  _JournalEntryScreenState createState() => _JournalEntryScreenState();
}

class _JournalEntryScreenState extends State<JournalEntryView> {
  GlobalKey _scaffoldKey = GlobalKey<ScaffoldState>();

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

  @override
  Widget build(BuildContext context) {
    Entry entry = widget.entry;
    Provider.of<EntryProvider>(context, listen: false)
        .setEntryContext(entry, context);
    return Hero(
      tag: '${entry.entryDate}${entry.id}',
      child: Form(
        child: Builder(
          builder: (ctx) {
            return WillPopScope(
              child: Scaffold(
                key: _scaffoldKey,
                resizeToAvoidBottomPadding: true,
                backgroundColor: Theme.of(context).primaryColor,
                appBar: AppBar(
                  actionsIconTheme: IconThemeData(color: Colors.white),
                  iconTheme: IconThemeData(color: Colors.white),
                  actions: <Widget>[
                    IconButton(
                      onPressed: () async {
                        Form.of(ctx).save();
                        if (!Form.of(ctx).validate()) {
                          return;
                        }

                        if (Provider.of<EmojiListProvider>(context,
                                    listen: false)
                                .getChosenFeeling ==
                            null) {
                          _showFormError(
                            MyLocalizations.of(context).journalEntryNeedMood,
                          );
                          return;
                        } else {
                          entry.feelingOnEntry = entry.getFeeling(
                              Provider.of<EmojiListProvider>(context,
                                      listen: false)
                                  .getChosenFeeling
                                  .url);
                        }

                        if (entry.time == null) {
                          entry.time = DateFormat.Hm().format(DateTime.now());
                        }

                        entry.weather = 'Sunny';
                        Provider.of<EntryProvider>(context, listen: false)
                            .saveEntry();
                        Navigator.of(context).pop();
                      },
                      padding: EdgeInsets.only(right: 16),
                      icon: Icon(
                        Icons.save,
                        color: Colors.white,
                        size: 25,
                      ),
                    )
                  ],
                  backgroundColor: Theme.of(context).primaryColor,
                  elevation: 0.0,
                  shadowColor: Theme.of(context).primaryColor,
                  bottomOpacity: 0.0,
                ),
                body: Stack(
                  children: <Widget>[
                    Column(
                      children: <Widget>[
                        Expanded(
                          child: Container(
                            color: Theme.of(context).primaryColor,
                            alignment: Alignment.topCenter,
                            child: Container(
                              child: Column(
                                children: [
                                  Container(
                                    margin:
                                        EdgeInsets.only(left: 20, bottom: 5),
                                    child: Text(
                                      MyLocalizations.of(context)
                                          .journalEntryFeeling,
                                      style: TextStyle(
                                        color: Colors.white,
                                        fontSize: 22,
                                        fontWeight: FontWeight.bold,
                                      ),
                                    ),
                                    alignment: Alignment.topLeft,
                                  ),
                                  FeelingsList(entry.feelingOnEntry),
                                ],
                              ),
                            ),
                          ),
                        ),
                      ],
                    ),
                    Container(
                      alignment: Alignment.bottomCenter,
                      padding: EdgeInsets.only(top: 115),
                      child: Container(
                        width: double.infinity,
                        child: ClipRRect(
                          borderRadius: BorderRadius.only(
                            topLeft: Radius.circular(80),
                          ),
                          child: EntryScreenData(entry),
                        ),
                      ),
                    )
                  ],
                ),
              ),
              onWillPop: () {
                Provider.of<EntryProvider>(context, listen: false)
                    .clearEntryContext();
                Provider.of<EmojiListProvider>(context, listen: false)
                    .setEmojiList();
                Navigator.pop(context);
                return;
              },
            );
          },
        ),
      ),
    );
  }

  void _showFormError(String errorText) {
    final snackBar = SnackBar(
      backgroundColor: Colors.red[400],
      content: Text(errorText),
    );
  }
}

class EntryScreenData extends StatefulWidget {
  final Entry entry;
  List<Object> images;

  EntryScreenData(this.entry);

  @override
  _EntryScreenDataState createState() => _EntryScreenDataState();
}

class _EntryScreenDataState extends State<EntryScreenData> {
  final SettingsDataModel settingsDataModel =
      SettingsDataModel.fromJson(jsonDecode(sharedPrefs.settingsData));
  final _titleController = TextEditingController();
  final _descriptionController = TextEditingController();
  final Geolocator geolocator = Geolocator()..forceAndroidLocationManager;

  DateTime datePicked;

  @override
  void dispose() {
    _titleController.dispose();
    _descriptionController.dispose();
    super.dispose();
  }

  @override
  void initState() {
    if (widget.entry.weather == null) {
      widget.entry.weather = 'Sunny';
    }

    _titleController.value = TextEditingValue(
      text: widget.entry.title != null ? widget.entry.title : '',
      selection: TextSelection.collapsed(
        offset: widget.entry.title != null ? widget.entry.title.length : 0,
      ),
    );

    _descriptionController.value = TextEditingValue(
      text: widget.entry.description != null ? widget.entry.description : '',
      selection: TextSelection.collapsed(
        offset: widget.entry.description != null
            ? widget.entry.description.length
            : 0,
      ),
    );

    widget.entry.entryDate != null
        ? datePicked = widget.entry.entryDate
        : datePicked = DateTime.now();

    widget.entry.tags != null
        ? widget.entry.tags = widget.entry.tags
        : widget.entry.tags = List<dynamic>();

    super.initState();
  }

  Future<String> getImage(int type) async {
    PickedFile pickedImage = await ImagePicker().getImage(
        source: type == 1 ? ImageSource.camera : ImageSource.gallery,
        imageQuality: 50);
    return pickedImage.path;
  }

  _imgFromCamera() async {
    final imagePath = await getImage(1);
    Provider.of<EntryProvider>(context, listen: false)
        .addImageToEntry(imagePath);
  }

  // HERE FOR INSTANCE IS WHERE I@M MAKING A CHANGE TO THE ENTRY THAT SHOWS ON THE LIST
  _imgFromGallery() async {
    final imagePath = await getImage(2);
    Provider.of<EntryProvider>(context, listen: false)
        .addImageToEntry(imagePath);
  }

  
  Widget _buildTagList() {
    return Container(
      height: 71,
      margin: EdgeInsets.only(top: 5, bottom: 5),
      child: Column(
        children: <Widget>[
          Container(
            alignment: Alignment.topLeft,
            child: Text(MyLocalizations.of(context).entryScreenTags,
                style: TextStyle(fontSize: 18)),
          ),
          Consumer<EntryProvider>(
            builder: (context, entryProvider, child) => CreateHashtags(
              entryProvider.getEntry.tags,
              _addTag,
              _removeTag,
            ),
          ),
        ],
      ),
    );
  }

  void _addTag(String tagText) {
    Provider.of<EntryProvider>(context, listen: false).addTagToEntry(tagText);
  }

  void _removeTag(int index) {
    Provider.of<EntryProvider>(context, listen: false).removeTagAt(index);
  }

  void _removeImage(int index) {
    Provider.of<EntryProvider>(context, listen: false).removeImageAt(index);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      resizeToAvoidBottomPadding: true,
      body: Container(
        alignment: Alignment.topCenter,
        color: Colors.white,
        padding: EdgeInsets.only(
          left: 20,
          right: 20,
        ),
        child: SingleChildScrollView(
          child: Column(
            children: <Widget>[
              EntryMetaTags(widget.entry, _getAddressFromLatLng),
              SizedBox(
                height: 10,
              ),
              Container(
                alignment: Alignment.topLeft,
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    InkWell(
                      onTap: _presentDatePicker,
                      child: Text(
                        DateFormat.yMMMd().format(
                          widget.entry.entryDate != null
                              ? widget.entry.entryDate
                              : DateTime.now(),
                        ),
                        style: TextStyle(fontSize: 24),
                      ),
                    ),
                    if (widget.entry.id != null)
                      IconButton(
                        onPressed: () {
                          _showDeleteDialog(context);
                        },
                        icon: Icon(
                          Icons.delete,
                          color: Theme.of(context).primaryColor,
                        ),
                      ),
                  ],
                ),
              ),
              Container(
                alignment: Alignment.topLeft,
                child: TextFormField(
                  onSaved: (String title) {
                    Provider.of<EntryProvider>(context, listen: false)
                        .getEntry
                        .title = title;
                  },
                  textCapitalization: TextCapitalization.sentences,
                  controller: _titleController,
                  decoration: InputDecoration(
                    hintText: MyLocalizations.of(context).entryScreenEnterTitle,
                    contentPadding: EdgeInsets.all(0),
                    border: InputBorder.none,
                    focusedBorder: OutlineInputBorder(
                      borderSide: BorderSide(width: 0, color: Colors.white),
                    ),
                  ),
                  style: TextStyle(fontSize: 20),
                ),
              ),
              Container(
                height: 190,
                margin: EdgeInsets.only(top: 5),
                alignment: Alignment.topLeft,
                child: TextFormField(
                  onSaved: (String description) {
                    Provider.of<EntryProvider>(context, listen: false)
                        .getEntry
                        .description = description;
                  },
                  validator: (description) {
                    if (description.isEmpty) {
                      return MyLocalizations.of(context)
                          .entryScreenEnterDescriptionWarn;
                    }

                    return null;
                  },
                  maxLines: 8,
                  keyboardType: TextInputType.text,
                  textCapitalization: TextCapitalization.sentences,
                  controller: _descriptionController,
                  decoration: InputDecoration(
                    hintText:
                        MyLocalizations.of(context).entryScreenEnterDescription,
                    contentPadding: EdgeInsets.all(0),
                    border: InputBorder.none,
                    focusedBorder: OutlineInputBorder(
                      borderSide: BorderSide(width: 0, color: Colors.white),
                    ),
                  ),
                  style: TextStyle(fontSize: 18),
                ),
              ),
              _buildTagList(),
              SizedBox(
                height: 3,
              ),
              Container(
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: <Widget>[
                    Text(
                      MyLocalizations.of(context).entryScreenImages,
                      style: TextStyle(fontSize: 18),
                    ),
                  ],
                ),
              ),
              Consumer<EntryProvider>(
                builder: (context, entryProvider, child) => ImageList(
                  entryProvider.getEntry.images,
                  _removeImage,
                  _showPicker,
                  _showImageDialog,
                ),
              ),
              SizedBox(
                height: 5,
              ),
            ],
          ),
        ),
      ),
    );
  }
}

标签: flutterflutter-provider

解决方案


是的,对象是通过引用传递的。因此,您正在修改同一个对象。由于Flutter 中没有反射,因此您无法真正自动制作副本。

解决此问题的一种方法是实现您自己的copyWith方法。例如,这就是 Flutter 在内部对样式所做的事情。

更新:重要的是要注意 List 和 Map 也是通过引用传递的。因此,您需要在自己的实现中使用List.from扩展运算符copyWith

例子:

Entry(
 images: images ?? List.from(this.images),
);

推荐阅读