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