1. Anuncie Aqui ! Entre em contato fdantas@4each.com.br

[Flutter] Flutter state management with accordions

Discussão em 'Mobile' iniciado por Stack, Outubro 3, 2024 às 13:52.

  1. Stack

    Stack Membro Participativo

    I have been banging my head against a wall for almost a week. I've run into issues with the very inefficient way that Flutter rebuilds the entire widget tree. I know how state works, and I know that to get what I want I need to properly separate out widgets (which, coming from Vuejs is so cumbersome that I have to do this in the first place), but I cannot figure out how to, given my specific requirements. Nothing I've tried has worked. I have exhausted all of ChatGPT's suggestions and the million articles I've read online to fix the issue don't apply to what I'm doing.

    So, here's the problem. In the attached image (sorry it's so big), everything you see on the screen comes from an inspection record. The top portion contains text fields and starting at Structural Systems it's an accordion, child accordion, and grandchild accordion. So, there will be a list of accordions (Structural System being one of the parent accordions), then a list of child accordions (Foundations being one of the children), and the child has children accordions (Type of Foundations being one). When I click Inspection Date or Inspection Time to open a date/time picker, it closes and rebuilds all the accordions, presumably because even though I separated out the actual accordions, the widget is in the same tree as the date picker. But how can I get around this??

    I need everything to be stateful (including the data in the accordion) except the accordion, itself. The below code was the closest I could get to doing what I wanted. Before I separated out all the widgets like I have them, whenever I clicked on the checkboxes inside the accordions, the accordion closed. There is still one more issue with the code I have. When I click the plus button next to Comments to open a modal, it closes the Foundations accordion. I can't have that, either.

    I've tried KeyedSubtree, AutomaticKeepAliveClientMixin, making a Stateless widget at the top level then inside putting a Stateful widget for the top section of the image and a Stateless widget for the accordions, adding Provider for state management (which is still just state management and has the same effect as using setState when the UI has to be updated), and a lot of other things. I know this is a long shot, but hopefully someone can't help. Let me know if you have any questions about my code. I posted the rest of the file's code in an answer. I am aware this is not correct, but felt that the entire code was needed if someone is going to be able to adequately help..

    [​IMG]
    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.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.inspectionSubCategories.length; j++) ...[
    AccordionSection(
    isOpen: false,
    header: Text(inspection.inspectionCategories.inspectionSubCategories[j].name),
    contentVerticalPadding: 0,
    headerBorderRadius: 0,
    content: Column(
    children: [
    InspectionSubCategorySection(
    subCategory: inspection.inspectionCategories.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.inspectionSubCategories[j].inspectionSubCategoryCategories.length; k++) ...[
    AccordionSection(
    isOpen: false,
    header: Text(inspection.inspectionCategories.inspectionSubCategories[j].inspectionSubCategoryCategories[k].name),
    contentVerticalPadding: 0,
    headerBorderRadius: 0,
    content: InspectionSubCategoryCategorySection(
    subCategoryCategory: inspection.inspectionCategories.inspectionSubCategories[j].inspectionSubCategoryCategories[k],
    categoryIndex: i,
    subCategoryIndex: j,
    subCategoryCategoryIndex: k,
    valueChanged: valueChanged
    )
    ),
    ]
    ],
    )
    ],
    )
    ),
    ]
    ],
    )
    ),
    ]
    ],
    );
    }
    }

    Continue reading...

Compartilhe esta Página