我已经用头撞墙近一周了。我遇到了 Flutter 重建整个 widget 树的非常低效的问题。我知道状态是如何工作的,而且我知道为了得到我想要的东西,我需要正确地分离出小部件(来自 Vuejs 的小部件非常麻烦,我必须首先这样做),但我不知道如何,考虑到我的具体要求。我尝试过的没有任何效果。我已经用尽了 ChatGPT 的所有建议,并且我在网上阅读的数百万篇文章来解决该问题并不适用于我正在做的事情。
所以,问题来了。在附图中(抱歉它太大了),您在屏幕上看到的所有内容都来自检查记录。顶部部分包含文本字段,从结构系统开始,它是一个手风琴、子手风琴和孙子手风琴。因此,将有一个手风琴列表(结构系统是父手风琴之一),然后是一个子手风琴列表(基金会是子手风琴之一),并且子孩子有子手风琴(基础类型是其中之一)。当我单击“检查日期”或“检查时间”打开日期/时间选择器时,它会关闭并重建所有手风琴,大概是因为即使我分离出实际的手风琴,该小部件与日期选择器位于同一树中。但我该如何解决这个问题??
除了手风琴本身之外,我需要所有东西都是有状态的(包括手风琴中的数据)。下面的代码是我能做的最接近我想做的事情的代码。在我像我一样分离出所有小部件之前,每当我单击手风琴内的复选框时,手风琴就会关闭。我的代码还有一个问题。当我单击“评论”旁边的加号按钮打开模式时,它会关闭“基础”手风琴。我也不能这样。
我尝试过KeyedSubtree、AutomaticKeepAliveClientMixin,在顶层制作一个无状态小部件,然后在图像的顶部部分放置一个有状态小部件,为手风琴放置一个无状态小部件,添加用于状态管理的提供程序(这仍然只是状态管理)并且与必须更新 UI 时使用 setState 具有相同的效果)以及许多其他内容。我知道这是一个很遥远的事情,但希望有人能帮忙。如果您对我的代码有任何疑问,请告诉我。缺少一些小部件。我无法发布整个文件的代码。
import 'package:flutter/material.dart';
import 'package:flutter_app/database/daos/inspection_category_dao.dart';
import 'package:intl/intl.dart';
import 'package:flutter_app/database/database.dart';
import 'package:flutter_app/models/inspection.dart' as model;
import 'package:flutter_app/models/inspection_category.dart' as model;
import 'package:flutter_app/models/inspection_sub_category.dart' as model;
import 'package:flutter_app/models/inspection_sub_category_category.dart' as model;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_app/utilities/helper_functions.dart';
import 'package:flutter_app/services/inspection_service.dart';
import 'package:flutter_app/database/daos/inspection_dao.dart';
import 'package:flutter_app/widgets/custom_text_form_field.dart';
import 'package:accordion/accordion.dart';
import 'package:accordion/controllers.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:flutter_app/services/quick_comment_service.dart';
import 'package:flutter_app/models/quick_comment.dart' as model;
class NewEditInspectionScreen extends StatefulWidget {
final int? inspectionId;
NewEditInspectionScreen({this.inspectionId});
@override
_NewEditInspectionScreenState createState() => _NewEditInspectionScreenState();
}
class _NewEditInspectionScreenState extends State<NewEditInspectionScreen> {
late AppDatabase db;
late InspectionDao inspectionDao;
late InspectionCategoryDao inspectionCategoryDao;
late model.Inspection inspection;
late model.InspectionCategory inspectionCategory;
late TextEditingController clientNameController;
late TextEditingController inspectionAddressController;
late TextEditingController additionalInformationController;
late TextEditingController inspectorNameController;
late TextEditingController inspectorLicenseController;
bool inspectionIsDirty = false;
DateTime selectedDate = DateTime.now();
TimeOfDay selectedTime = TimeOfDay.fromDateTime(DateTime.now());
int? _inspectionId;
@override
void initState() {
super.initState();
clientNameController = TextEditingController();
inspectionAddressController = TextEditingController();
additionalInformationController = TextEditingController();
inspectorNameController = TextEditingController();
inspectorLicenseController = TextEditingController();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_inspectionId = (ModalRoute.of(context)?.settings.arguments as int?) ?? -1;
db = AppDatabase();
inspectionDao = InspectionDao(db);
inspectionCategoryDao = InspectionCategoryDao(db);
inspection = getBlankInspectionData();
_loadInspection();
}
bool _initialLoad = true;
Future<void> _loadInspection() async {
model.Inspection fetchedInspection = inspection;
if (_inspectionId != -1) {
fetchedInspection = await getInspection(_inspectionId!);
}
final prefs = await SharedPreferences.getInstance();
setState(() {
inspection = fetchedInspection;
if (_initialLoad) {
selectedDate = _inspectionId != -1 ? inspection.inspectionDate : DateTime.now();
selectedTime = _inspectionId != -1 ? TimeOfDay.fromDateTime(inspection.inspectionDate) : TimeOfDay.fromDateTime(DateTime.now());
inspection.inspectionDate = DateTime(selectedDate.year, selectedDate.month, selectedDate.day, selectedTime.hour, selectedTime.minute);
clientNameController.text = inspection.clientName;
inspectionAddressController.text = inspection.inspectionAddress;
additionalInformationController.text = inspection.additionalInformation;
inspectorNameController.text = prefs.getString('name') ?? '';
inspectorLicenseController.text = prefs.getString('trecLicense') ?? '';
_initialLoad = false;
}
});
}
void saveInspectionReport() async {
inspection.inspectionDate = DateTime(selectedDate.year, selectedDate.month, selectedDate.day, selectedTime.hour, selectedTime.minute);
if (_inspectionId == -1) {
await createInspection(inspection);
} else {
await updateInspection(inspection);
}
Navigator.pop(context);
}
bool canSaveInspection() {
return inspection.clientName != '' && inspection.inspectionAddress != '' && inspectionIsDirty;
}
Future<void> selectDate(BuildContext context) async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: selectedDate,
firstDate: DateTime(2000),
lastDate: DateTime(2101),
);
if (picked != null && picked != selectedDate) {
selectedDate = picked;
inspectionIsDirty = true;
}
}
Future<void> selectTime(BuildContext context) async {
final TimeOfDay? picked = await showTimePicker(
context: context,
initialTime: selectedTime,
);
if (picked != null && picked != selectedTime) {
selectedTime = picked;
inspectionIsDirty = true;
}
}
void setSubCategoryCommentsValue(int categoryIndex, int subCategoryIndex, String value) {
inspection.inspectionCategories[categoryIndex].inspectionSubCategories[subCategoryIndex].comments = value;
inspectionIsDirty = true;
}
void setSubCategoryCategoryValueValue(int categoryIndex, int subCategoryIndex, int subCategoryCategoryIndex, String value) {
inspection.inspectionCategories[categoryIndex].inspectionSubCategories[subCategoryIndex].inspectionSubCategoryCategories[subCategoryCategoryIndex].value = value;
inspectionIsDirty = true;
}
void setSubCategoryInspectionStatusValue(int categoryIndex, int subCategoryIndex, String field, bool value) {
switch (field) {
case 'inspected':
inspection.inspectionCategories[categoryIndex].inspectionSubCategories[subCategoryIndex].inspectionStatus.inspected = value;
case 'notInspected':
inspection.inspectionCategories[categoryIndex].inspectionSubCategories[subCategoryIndex].inspectionStatus.notInspected = value;
case 'notPresent':
inspection.inspectionCategories[categoryIndex].inspectionSubCategories[subCategoryIndex].inspectionStatus.notPresent = value;
case 'deficient':
inspection.inspectionCategories[categoryIndex].inspectionSubCategories[subCategoryIndex].inspectionStatus.deficient = value;
default:
print('Unknown inspection status.');
}
inspectionIsDirty = true;
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text('Inspection'),
backgroundColor: Colors.blue,
leading: IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () {
Navigator.pop(context); // Back button functionality
},
),
actions: [
IconButton(
icon: Icon(Icons.save),
onPressed: saveInspectionReport, // Enable if data is changed
),
IconButton(
icon: Icon(Icons.picture_as_pdf),
onPressed: () {
// Handle viewing PDF logic here
},
),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomTextFormField(
label: 'Client Name *',
hintText: '',
controller: clientNameController,
onChanged: (value) {
inspection.clientName = value;
inspectionIsDirty = true;
},
),
SizedBox(height: 4),
Row(
children: [
Expanded(
child: GestureDetector(
onTap: () => selectDate(context),
child: InputDecorator(
decoration: InputDecoration(
labelText: 'Inspection Date *',
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
),
child: Text(
DateFormat.yMd().format(selectedDate),
style: TextStyle(
fontSize: 16.0,
),
),
),
),
),
SizedBox(width: 16),
Expanded(
child: GestureDetector(
onTap: () => selectTime(context),
child: InputDecorator(
decoration: InputDecoration(
labelText: 'Inspection Time *',
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
),
child: Text(
selectedTime.format(context),
style: TextStyle(
fontSize: 16.0,
),
),
),
),
),
],
),
SizedBox(height: 4),
CustomTextFormField(
label: 'Inspection Address *',
hintText: '',
controller: inspectionAddressController,
onChanged: (value) {
inspection.inspectionAddress = value;
inspectionIsDirty = true;
},
),
SizedBox(height: 4),
CustomTextFormField(
label: 'Inspector Name',
controller: inspectorNameController,
readOnly: true,
),
SizedBox(height: 4),
CustomTextFormField(
label: 'Inspector TREC License #',
controller: inspectorLicenseController,
readOnly: true,
),
SizedBox(height: 4),
CustomTextFormField(
label: 'Additional Information',
hintText: 'Enter comments',
minLines: 1,
maxLines: 6,
controller: additionalInformationController,
onChanged: (value) {
inspection.additionalInformation = value;
inspectionIsDirty = true;
},
),
SizedBox(height: 4),
InspectionAccordion(
inspection: inspection,
inspectedChanged: (isInspected, categoryIndex, subCategoryIndex) {
setSubCategoryInspectionStatusValue(categoryIndex, subCategoryIndex, 'inspected', isInspected);
},
notInspectedChanged: (isNotInspected, categoryIndex, subCategoryIndex) {
setSubCategoryInspectionStatusValue(categoryIndex, subCategoryIndex, 'notInspected', isNotInspected);
},
notPresentChanged: (isNotPresent, categoryIndex, subCategoryIndex) {
setSubCategoryInspectionStatusValue(categoryIndex, subCategoryIndex, 'notPresent', isNotPresent);
},
deficientChanged: (isDeficient, categoryIndex, subCategoryIndex) {
setSubCategoryInspectionStatusValue(categoryIndex, subCategoryIndex, 'deficient', isDeficient);
},
commentsChanged: (comments, categoryIndex, subCategoryIndex) {
setSubCategoryCommentsValue(categoryIndex, subCategoryIndex, comments);
},
valueChanged: (value, categoryIndex, subCategoryIndex, subCategoryCategoryIndex) {
setSubCategoryCategoryValueValue(categoryIndex, subCategoryIndex, subCategoryCategoryIndex, value);
},
),
],
),
),
),
);
}
@override
void dispose() {
clientNameController.dispose();
inspectionAddressController.dispose();
additionalInformationController.dispose();
inspectorNameController.dispose();
inspectorLicenseController.dispose();
super.dispose();
}
}
class InspectionAccordion extends StatelessWidget {
final model.Inspection inspection;
final Function(bool, int, int) inspectedChanged;
final Function(bool, int, int) notInspectedChanged;
final Function(bool, int, int) notPresentChanged;
final Function(bool, int, int) deficientChanged;
final Function(String, int, int) commentsChanged;
final Function(String, int, int, int) valueChanged;
const InspectionAccordion({
Key? key,
required this.inspection,
required this.inspectedChanged,
required this.notInspectedChanged,
required this.notPresentChanged,
required this.deficientChanged,
required this.commentsChanged,
required this.valueChanged,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Accordion(
headerBackgroundColor: Colors.grey[200],
contentBackgroundColor: Colors.white,
contentBorderColor: Colors.grey[200],
openAndCloseAnimation: false,
contentBorderWidth: 1,
contentHorizontalPadding: 5,
headerPadding: const EdgeInsets.symmetric(vertical: 7, horizontal: 15),
sectionOpeningHapticFeedback: SectionHapticFeedback.heavy,
sectionClosingHapticFeedback: SectionHapticFeedback.light,
paddingBetweenClosedSections: 1,
paddingBetweenOpenSections: 1,
paddingListHorizontal: 0,
paddingListTop: 0,
rightIcon: Icon(Icons.keyboard_arrow_down, color: Colors.black),
disableScrolling: true,
children: [
for (int i = 0; i < inspection.inspectionCategories.length; i++) ...[
AccordionSection(
isOpen: false,
header: Text(inspection.inspectionCategories[i].name),
contentVerticalPadding: 0,
headerBorderRadius: 0,
content: Accordion(
headerBackgroundColor: Colors.grey[200],
contentBackgroundColor: Colors.white,
contentBorderColor: Colors.grey[200],
openAndCloseAnimation: false,
contentBorderWidth: 1,
contentHorizontalPadding: 5,
headerPadding: const EdgeInsets.symmetric(vertical: 7, horizontal: 7),
sectionOpeningHapticFeedback: SectionHapticFeedback.heavy,
sectionClosingHapticFeedback: SectionHapticFeedback.light,
paddingBetweenClosedSections: 1,
paddingBetweenOpenSections: 1,
paddingListHorizontal: 0,
paddingListTop: 10,
rightIcon: Icon(Icons.keyboard_arrow_down, color: Colors.black),
disableScrolling: true,
children: [
for (int j = 0; j < inspection.inspectionCategories[i].inspectionSubCategories.length; j++) ...[
AccordionSection(
isOpen: false,
header: Text(inspection.inspectionCategories[i].inspectionSubCategories[j].name),
contentVerticalPadding: 0,
headerBorderRadius: 0,
content: Column(
children: [
InspectionSubCategorySection(
subCategory: inspection.inspectionCategories[i].inspectionSubCategories[j],
categoryIndex: i,
subCategoryIndex: j,
inspectedChanged: inspectedChanged,
notInspectedChanged: notInspectedChanged,
notPresentChanged: notPresentChanged,
deficientChanged: deficientChanged,
commentsChanged: commentsChanged,
),
Accordion(
headerBackgroundColor: Colors.grey[200],
contentBackgroundColor: Colors.white,
contentBorderColor: Colors.grey[200],
openAndCloseAnimation: false,
contentBorderWidth: 1,
contentHorizontalPadding: 5,
headerPadding: const EdgeInsets.symmetric(vertical: 7, horizontal: 15),
sectionOpeningHapticFeedback: SectionHapticFeedback.heavy,
sectionClosingHapticFeedback: SectionHapticFeedback.light,
paddingBetweenClosedSections: 1,
paddingBetweenOpenSections: 1,
paddingListHorizontal: 0,
paddingListTop: 10,
rightIcon: Icon(Icons.keyboard_arrow_down, color: Colors.black),
disableScrolling: true,
children: [
for (int k = 0; k < inspection.inspectionCategories[i].inspectionSubCategories[j].inspectionSubCategoryCategories.length; k++) ...[
AccordionSection(
isOpen: false,
header: Text(inspection.inspectionCategories[i].inspectionSubCategories[j].inspectionSubCategoryCategories[k].name),
contentVerticalPadding: 0,
headerBorderRadius: 0,
content: InspectionSubCategoryCategorySection(
subCategoryCategory: inspection.inspectionCategories[i].inspectionSubCategories[j].inspectionSubCategoryCategories[k],
categoryIndex: i,
subCategoryIndex: j,
subCategoryCategoryIndex: k,
valueChanged: valueChanged
)
),
]
],
)
],
)
),
]
],
)
),
]
],
);
}
}
这不是答案。这是我的其余代码。我没有其他方法可以发布它。
class InspectionSubCategorySection extends StatefulWidget {
final model.InspectionSubCategory subCategory;
final int categoryIndex;
final int subCategoryIndex;
final Function(bool, int, int) inspectedChanged;
final Function(bool, int, int) notInspectedChanged;
final Function(bool, int, int) notPresentChanged;
final Function(bool, int, int) deficientChanged;
final Function(String, int, int) commentsChanged;
const InspectionSubCategorySection({
Key? key,
required this.subCategory,
required this.categoryIndex,
required this.subCategoryIndex,
required this.inspectedChanged,
required this.notInspectedChanged,
required this.notPresentChanged,
required this.deficientChanged,
required this.commentsChanged,
}) : super(key: key);
@override
_InspectionSubCategorySectionState createState() => _InspectionSubCategorySectionState();
}
class _InspectionSubCategorySectionState extends State<InspectionSubCategorySection> {
TextEditingController commentsController = TextEditingController();
TextEditingController attachmentsController = TextEditingController();
@override
void initState() {
super.initState();
commentsController = TextEditingController(text: widget.subCategory.comments);
attachmentsController = TextEditingController(text: widget.subCategory.attachments.isNotEmpty ? 'Items Attached' : '');
}
void _openQuickCommentsModal(BuildContext context) async {
final selectedComment = await showModalBottomSheet<String>(
context: context,
isScrollControlled: true,
builder: (BuildContext context) {
return QuickCommentsModal();
},
);
if (selectedComment != null) {
// Assuming you have a controller for your comments text field
setState(() {
final existingComments = commentsController.text;
commentsController.text = existingComments.isNotEmpty
? "$existingComments\n$selectedComment"
: selectedComment;
});
}
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(left: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Flexible(
child: Padding(
padding: EdgeInsets.zero,
child: Row(
children: [
Checkbox(
value: widget.subCategory.inspectionStatus.inspected,
onChanged: (bool? value) {
setState(() {
widget.inspectedChanged(value ?? false, widget.categoryIndex, widget.subCategoryIndex);
});
},
activeColor: Colors.blue,
),
Text('Inspected'),
],
),
),
),
Flexible(
child: Padding(
padding: EdgeInsets.zero,
child: Row(
children: [
Checkbox(
value: widget.subCategory.inspectionStatus.notInspected,
onChanged: (bool? value) {
setState(() {
widget.notInspectedChanged(value ?? false, widget.categoryIndex, widget.subCategoryIndex);
});
},
activeColor: Colors.blue,
),
Text('Not Inspected'),
],
),
),
),
],
),
Row(
children: [
Flexible(
child: Padding(
padding: EdgeInsets.zero,
child: Row(
children: [
Checkbox(
value: widget.subCategory.inspectionStatus.notPresent,
onChanged: (bool? value) {
setState(() {
widget.notPresentChanged(value ?? false, widget.categoryIndex, widget.subCategoryIndex);
});
},
activeColor: Colors.blue,
),
Text('Not Present'),
],
),
),
),
Flexible(
child: Padding(
padding: EdgeInsets.zero,
child: Row(
children: [
Checkbox(
value: widget.subCategory.inspectionStatus.deficient,
onChanged: (bool? value) {
setState(() {
widget.deficientChanged(value ?? false, widget.categoryIndex, widget.subCategoryIndex);
});
},
activeColor: Colors.blue,
),
Text('Deficient'),
],
),
),
),
],
),
Stack(
children: [
CustomTextFormField(
label: 'Comments',
hintText: 'Enter comment',
minLines: 1,
maxLines: 6,
controller: commentsController,
onChanged: (String value) {
widget.commentsChanged(value, widget.categoryIndex, widget.subCategoryIndex);
},
),
Positioned(
right: 0,
top: 0,
bottom: 0,
child: IconButton(
icon: Icon(Icons.add_circle, color: Colors.blue),
onPressed: () {
_openQuickCommentsModal(context);
},
),
),
],
),
SizedBox(height: 4),
Stack(
children: [
CustomTextFormField(
readOnly: true,
controller: attachmentsController,
label: 'Attachments',
hintText: 'Select attachment',
),
Positioned(
right: 70,
top: 0,
bottom: 0,
child: IconButton(
icon: Icon(Icons.camera_alt, color: Colors.blue),
onPressed: () {
// Add your onPressed functionality here
},
),
),
Positioned(
right: 10,
top: 0,
bottom: 0,
child: Center(
child: GestureDetector(
onTap: () => {
},
child: Text(
'SELECT', // This is the "Select" text in place of an icon
style: TextStyle(
color: Colors.blue,
fontSize: 16.0,
fontWeight: FontWeight.bold,
),
),
),
),
),
],
),
// Container(
// margin: EdgeInsets.only(
// bottom: widget.subCategory.attachments.isEmpty && widget.subCategory.inspectionSubCategoryCategories.isNotEmpty
// ? 20.0
// : 10.0,
// ),
// child: Row(
// children: [
// Expanded(
// child: CustomTextFormField(
// readOnly: true,
// controller: attachmentsController,
// label: 'Attachments',
// hintText: 'Select attachment',
// ),
// ),
// const SizedBox(width: 8),
// IconButton(
// icon: Icon(Icons.camera_alt),
// color: Colors.blue,
// onPressed: () {
// // getImage(widget.subCategory, 'camera');
// },
// ),
// const SizedBox(width: 8),
// ElevatedButton(
// onPressed: () {
// // getImage(widget.subCategory, 'file');
// },
// // style: ElevatedButton.styleFrom(primary: Colors.blue),
// child: Text('Select'),
// ),
// ],
// ),
// ),
// Grid to show attached images (if any)
if (widget.subCategory.attachments.isNotEmpty)
Container(
margin: EdgeInsets.only(top: 10.0),
child: GridView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 1.0,
crossAxisSpacing: 10.0,
mainAxisSpacing: 10.0,
),
itemCount: widget.subCategory.attachments.length,
itemBuilder: (BuildContext context, int index) {
final attachment = widget.subCategory.attachments[index];
return GestureDetector(
onTap: () {
// setImagePickerOpen(widget.subCategory, attachment);
},
// child: Image.network(
// getAttachmentSrc(attachment),
// fit: BoxFit.cover,
// ),
);
},
),
),
],
),
);
}
}
class InspectionSubCategoryCategorySection extends StatefulWidget {
final model.InspectionSubCategoryCategory subCategoryCategory;
final int categoryIndex;
final int subCategoryIndex;
final int subCategoryCategoryIndex;
final Function(String, int, int, int) valueChanged;
const InspectionSubCategoryCategorySection({
Key? key,
required this.subCategoryCategory,
required this.categoryIndex,
required this.subCategoryIndex,
required this.subCategoryCategoryIndex,
required this.valueChanged,
}) : super(key: key);
@override
_InspectionSubCategoryCategorySectionState createState() => _InspectionSubCategoryCategorySectionState();
}
class _InspectionSubCategoryCategorySectionState extends State<InspectionSubCategoryCategorySection> {
TextEditingController value = TextEditingController();
@override
void initState() {
super.initState();
value = TextEditingController(text: widget.subCategoryCategory.value);
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(left: 16.0, bottom: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomTextFormField(
label: 'Notes',
hintText: 'Enter notes',
minLines: 1,
maxLines: 6,
controller: value,
onChanged: (String value) {
widget.valueChanged(value, widget.categoryIndex, widget.subCategoryIndex, widget.subCategoryCategoryIndex);
},
),
],
),
);
}
}
class QuickCommentsModal extends StatefulWidget {
@override
_QuickCommentsModalState createState() => _QuickCommentsModalState();
}
class _QuickCommentsModalState extends State<QuickCommentsModal> {
bool reorderMode = false;
late List<model.QuickComment> quickComments;
final FocusNode commentFocusNode = FocusNode();
TextEditingController commentController = TextEditingController();
@override
void initState() {
super.initState();
_loadComments();
}
Future<void> _loadComments() async {
final commentList = await getQuickComments();
setState(() {
quickComments = commentList;
});
}
void _openEditCommentModal(model.QuickComment quickComment) async {
_showCommentModal(quickComment: quickComment);
}
void _openAddCommentModal() async {
_showCommentModal();
}
void _deleteComment(int id) async {
await deleteQuickComment(id);
_loadComments();
}
void toggleReorderMode() {
setState(() {
reorderMode = !reorderMode;
});
}
void _onReorder(int oldIndex, int newIndex) async {
setState(() {
if (newIndex > oldIndex) newIndex -= 1;
final item = quickComments.removeAt(oldIndex);
quickComments.insert(newIndex, item);
});
for (int i = 0; i < quickComments.length; i++) {
await reorderComment(quickComments[i], i + 1);
}
_loadComments();
}
void _showCommentModal({model.QuickComment? quickComment}) {
if (quickComment != null) {
commentController.text = quickComment.commentText;
} else {
commentController.clear();
}
void createComment() async {
model.QuickComment quickComment = model.QuickComment(
order: 1,
commentText: commentController.text,
isDefault: false,
);
await createQuickComment(quickComment);
_loadComments();
}
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) {
bool isSaveEnabled = commentController.text.isNotEmpty;
// Attach the listener here only when modal is open
commentController.addListener(() {
if (mounted) { // Check if widget is still mounted
// Trigger UI update only if modal is still mounted
setState(() {
isSaveEnabled = commentController.text.isNotEmpty;
});
}
});
return Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: StatefulBuilder(
builder: (BuildContext context, StateSetter modalSetState) {
bool isSaveEnabled = commentController.text.isNotEmpty;
commentController.addListener(() {
if (mounted) {
modalSetState(() {
isSaveEnabled = commentController.text.isNotEmpty;
});
}
});
return Container(
padding: const EdgeInsets.all(16.0),
height: MediaQuery.of(context).size.height * 0.4, // Make the modal wider
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
quickComment != null ? 'Edit Comment' : 'Add Comment',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
SizedBox(height: 16),
CustomTextFormField(
minLines: 1,
maxLines: 6,
label: 'Comment',
hintText: 'Enter comment',
controller: commentController,
setFocus: true,
),
SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {
commentController.removeListener(() {});
Navigator.pop(context);
},
child: Text('CANCEL', style: TextStyle(color: Colors.grey[800])),
),
TextButton(
onPressed: () => isSaveEnabled ? () {
createComment();
commentController.removeListener(() {});
Navigator.pop(context, commentController.text);
} : null, // Close modal
child: Text('SAVE', style: isSaveEnabled ? TextStyle(color: Colors.grey[800]) : TextStyle(color: Colors.grey)),
),
],
)
],
),
);
},
),
);
},
).whenComplete(() {
commentController.removeListener(() {});
});
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text('Quick Comments'),
actions: [
IconButton(
icon: Icon(Icons.close, color: Colors.blue),
onPressed: () {
Navigator.pop(context);
},
),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: reorderMode ? ReorderableListView(
onReorder: _onReorder,
children: quickComments.map((quickComment) {
return ListTile(
key: ValueKey(quickComment.id),
title: Text(quickComment.commentText),
leading: Text('${quickComment.order}'),
trailing: Icon(Icons.drag_handle),
);
}).toList(),
) : Column(
children: quickComments.map((quickComment) {
return Slidable(
key: ValueKey(quickComment.id), // Ensure unique key for each item
endActionPane: ActionPane(
motion: ScrollMotion(),
children: [
SlidableAction(
onPressed: (context) => _openEditCommentModal(quickComment),
backgroundColor: Colors.blue,
icon: Icons.edit,
label: 'Edit',
),
SlidableAction(
onPressed: (context) => _deleteComment(quickComment.id!),
backgroundColor: Colors.red,
icon: Icons.delete,
label: 'Delete',
),
],
),
child: ListTile(
key: ValueKey(quickComment.id),
title: Text(quickComment.commentText),
leading: Text('${quickComment.order}'),
onTap: () {
Navigator.pop(context, quickComment.commentText);
},
),
);
}).toList(),
),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
floatingActionButton: Padding(
padding: const EdgeInsets.only(bottom: 20, right: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
// Reorder button (list icon) as CircleAvatar
GestureDetector(
onTap: toggleReorderMode,
child: CircleAvatar(
backgroundColor: Colors.blue,
child: Icon(
Icons.list,
color: Colors.white,
),
),
),
SizedBox(width: 16),
// Add comment button (plus icon) as CircleAvatar
GestureDetector(
onTap: _openAddCommentModal,
child: CircleAvatar(
backgroundColor: Colors.blue,
child: Icon(
Icons.add,
color: Colors.white,
),
),
),
],
)
),
)
);
}
@override
void dispose() {
commentController.dispose();
commentFocusNode.dispose();
super.dispose();
}
}