更改一个 Provider 中的属性会将另一个 Provider 中的属性更改为 List Flutter
Posted
技术标签:
【中文标题】更改一个 Provider 中的属性会将另一个 Provider 中的属性更改为 List Flutter【英文标题】:Changing property in one Provider changes the property in another Provider as a List Flutter 【发布时间】:2021-04-26 23:00:49 【问题描述】:更新 - 我实际上发现它是一个Flutter Issue。
我有两个 Provider,一个是 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,
),
],
),
),
),
);
【问题讨论】:
尝试使用 Bloc 模式,它使用流来更新 ui。 Provider 主要用于跨多个屏幕提供数据。由于它具有更新两个属性的错误,您可能想尝试 bloc。 【参考方案1】:是的,对象是通过引用传递的。因此,您正在修改同一个对象。因为there is no reflection in Flutter,你不能真正自动复制。
解决此问题的一种方法是实现您自己的copyWith 方法。例如,这就是 Flutter 在内部对样式所做的事情。
更新:重要的是要注意 List 和 Map 也是通过引用传递的。因此,您需要在自己的copyWith
实现中使用List.from 或spread operator。
例子:
Entry(
images: images ?? List.from(this.images),
);
【讨论】:
我不知道为什么“是的,对象是通过引用传递的。因此,您正在修改同一个对象。”对我来说太难了。我猜其他语言和框架在某种程度上会宠爱开发人员。好的,我要试试copyWith。迄今为止,它对我来说一直是一个巨大的障碍,我不明白为什么它不起作用。 表示对象本身并没有被复制,只有指向它的内存地址。但是由于地址的副本指向内存中的同一个地方,所以同一个对象正在被修改。 哦,我明白这意味着什么,我只是不明白为什么 Dart/Flutter 工程师会这样做。不过还是谢谢你的解释。我按照你的解释尝试了实现,我注意到一开始它看起来很有效,但是当我回到项目卡时,我看到变化仍然存在,然后当我按下时,就像原来的一样问题? 我已经在我的代码中实现了 copyWith 更改,我已经添加了我的 Entry 类并显示了在 ListView 上所做的编辑。但我仍然遇到问题。实施的方式是正确的方式吗? 感谢您花时间帮助我理解这一点!我要离开,再好好想想!以上是关于更改一个 Provider 中的属性会将另一个 Provider 中的属性更改为 List Flutter的主要内容,如果未能解决你的问题,请参考以下文章
使用 AJAX/PHP 将另一个站点表中的内容插入我的表单?
按值传递结构,将另一个结构作为其成员之一,更改此成员的成员的值
mybatis映射文件,当从XXXDao.java中传入的参数是一个对象Provider的时候,那在XXXDao.xml中的Provider的属性id的时候需要怎么写