Search code examples
flutterflutter-provider

Changing property in one Provider changes the property in another Provider as a List Flutter


UPDATE - I actually found that it is a Flutter Issue.


I have two Providers, 1 is an EntriesProvider and another is an EntryProvider. I use my EntryProvider when creating an entry and my EntriesProvider to load all the entries saved to the database. I'm having an issue which I believe might be my understanding of how to use Providers. Once I load my database data into my EntriesProvider I load that data into a ListView. Once an item is clicked, I pass the Entry from that list into my View to populate and edit.

My issue is that when I edit the Entry without saving it, I can see the changes happening in the ListView which is not what I want. I tried clearing the EntryProvider as I believed the data belonging to that was separate from the EntriesProvider. But now I have no idea after trying multiple things. Why am I updating the list when I'm only asking the EntryProvider to update its listeners?

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,
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Solution

  • Yes, objects are passed by reference. Therefore, you are modifying the same object. Since there is no reflection in Flutter, you cannot really make a copy automatically.

    One way around the issue is implementing your own copyWith method. This is what Flutter does internally in the case of styles, for example.

    Update: it is important to note that List and Map are passed by reference as well. Therefore you need to use either List.from or the spread operator in your own implementation of copyWith.

    Example:

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