skip to Main Content

I have a app that I am working for in flutter that right now has 2 pages a form page for people to enter data and a summary page that allows them to look over that data the entered one more before they submit the information to a sql database. I am storing data in local storage and then having the summary page load that data in from local storage. my issue is the following:
it is only loading information from the 1st data column. it should be loading to each data column.

here is the code

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

void main() => runApp(MaterialApp(home: MyApp()));

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  List<MyCustomForm> forms = [];
  String? _selectedSchool;
  final List<String> _schools = [
    'Gentry Middle School',
    'Oakland Middle School',
    'Jefferson Middle School',
    'West Middle School',
  ];

  @override
  void initState() {
    super.initState();
    forms.add(MyCustomForm(onCpsNumberTyped: addNewForm));
  }

  void addNewForm() {
    setState(() {
      forms.add(MyCustomForm(onCpsNumberTyped: addNewForm));
    });
  }

  void reviewData(BuildContext context) async {
    final prefs = await SharedPreferences.getInstance();
    List<String> formDataList = [];
    forms.forEach((form) {
      formDataList.add('${_selectedSchool ?? 'No School Selected'}|${form.cpsNumber}|${form.itemDescription}|${form.selectedReason}');
    });
    await prefs.setStringList('formData', formDataList);
    Navigator.push(context, MaterialPageRoute(builder: (context) => SummaryPage()));
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Surplus Asset Disposal Form'),
        ),
        body: Column(
          children: [
            Padding(
              padding: EdgeInsets.all(16.0),
              child: DropdownButtonFormField<String>(
                value: _selectedSchool,
                decoration: InputDecoration(
                  labelText: 'Select School',
                  border: OutlineInputBorder(),
                ),
                onChanged: (String? newValue) {
                  setState(() {
                    _selectedSchool = newValue;
                  });
                },
                items: _schools.map<DropdownMenuItem<String>>((String value) {
                  return DropdownMenuItem<String>(
                    value: value,
                    child: Text(value),
                  );
                }).toList(),
              ),
            ),
            Expanded(
              child: ListView.builder(
                itemCount: forms.length,
                itemBuilder: (context, index) => forms[index],
              ),
            ),
            Padding(
              padding: const EdgeInsets.symmetric(vertical: 16.0),
              child: ElevatedButton(
                onPressed: () => reviewData(context),
                child: Text('REVIEW'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class MyCustomForm extends StatefulWidget {
  final VoidCallback onCpsNumberTyped;

  MyCustomForm({required this.onCpsNumberTyped});

  @override
  _MyCustomFormState createState() => _MyCustomFormState();

  String get cpsNumber => '';
  String get itemDescription => '';
  String get selectedReason => '';
}

class _MyCustomFormState extends State<MyCustomForm> {
  final TextEditingController _cpsNumberController = TextEditingController();
  final TextEditingController _itemDescriptionController = TextEditingController();
  String? selectedReason;
  bool isFirstTime = true;

  final List<String> _reasonsForSurplus = [
    'Obsolete Equipment',
    'Replacement',
    'Damage',
    'Others',
  ];

  String get cpsNumber => _cpsNumberController.text;
  String get itemDescription => _itemDescriptionController.text;
  // ignore: recursive_getters

  @override
  void initState() {
    super.initState();
    _cpsNumberController.addListener(() {
      if (isFirstTime && _cpsNumberController.text.isNotEmpty) {
        widget.onCpsNumberTyped();
        isFirstTime = false;
      }
    });
  }

  @override
  void dispose() {
    _cpsNumberController.dispose();
    _itemDescriptionController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Card(
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              TextFormField(
                controller: _cpsNumberController,
                decoration: InputDecoration(
                  labelText: 'CPS Number',
                  border: OutlineInputBorder(),
                ),
              ),
              SizedBox(height: 24.0),
              TextFormField(
                controller: _itemDescriptionController,
                decoration: InputDecoration(
                  labelText: 'Item Description',
                  border: OutlineInputBorder(),
                ),
                maxLines: null,
              ),
              SizedBox(height: 24.0),
              DropdownButtonFormField<String>(
                value: selectedReason,
                decoration: InputDecoration(
                  labelText: 'Reason for Surplus',
                  border: OutlineInputBorder(),
                ),
                onChanged: (String? newValue) {
                  setState(() {
                    selectedReason = newValue;
                  });
                },
                items: _reasonsForSurplus.map<DropdownMenuItem<String>>((String value) {
                  return DropdownMenuItem<String>(
                    value: value,
                    child: Text(value),
                  );
                }).toList(),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class SummaryPage extends StatefulWidget {
  @override
  _SummaryPageState createState() => _SummaryPageState();
}

class _SummaryPageState extends State<SummaryPage> {
  Future<List<DataRow>> _loadData() async {
    final prefs = await SharedPreferences.getInstance();
    final List<String>? formDataList = prefs.getStringList('formData');

    List<DataRow> rows = (formDataList ?? []).map((dataString) {
      List<String> dataParts = dataString.split('|');
      if (dataParts.length >= 4) {
        return DataRow(
          cells: [
            DataCell(Text(dataParts[0])),
            DataCell(Text(dataParts[1])),
            DataCell(Text(dataParts[2])),
            DataCell(Text(dataParts[3])),
          ],
        );
      } else {
        return null;
      }
    }).where((row) => row != null).map<DataRow>((row) => row!).toList();

    return rows;
  }

  // Make sure this method is inside the _SummaryPageState class
  Future<void> clearFormData() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.remove('formData');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Summary'),
      ),
      body: FutureBuilder<List<DataRow>>(
        future: _loadData(),
        builder: (BuildContext context, AsyncSnapshot<List<DataRow>> snapshot) {
          if (snapshot.connectionState == ConnectionState.done) {
            if (snapshot.hasError) {
              return Center(child: Text('Error loading data'));
            }

            return ListView(
              children: <Widget>[
                DataTable(
                  columns: const [
                    DataColumn(label: Text('School')),
                    DataColumn(label: Text('CPS Number')),
                    DataColumn(label: Text('Item Description')),
                    DataColumn(label: Text('Reason For Surplus')),
                  ],
                  rows: snapshot.data ?? [],
                ),
                Padding(
                  padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 24.0),
                  child: ElevatedButton(
                    onPressed: () async {
                      await clearFormData();
                      Navigator.of(context).popUntil((route) => route.isFirst);
                    },
                    child: Text('Submit & Start Over'),
                  ),
                ),
              ],
            );
          } else {
            return Center(child: CircularProgressIndicator());
          }
        },
      ),
    );
  }
}
  Future<List<DataRow>> _loadData() async {
    final prefs = await SharedPreferences.getInstance();
    final List<String>? formDataList = prefs.getStringList('formData');

    List<DataRow> rows = (formDataList ?? []).map((dataString) {
      List<String> dataParts = dataString.split('|');
      if (dataParts.length >= 4 && dataParts[1].isNotEmpty) {
        return DataRow(
          cells: [
            DataCell(Text(dataParts[0])),
            DataCell(Text(dataParts[1])),
            DataCell(Text(dataParts[2])),
            DataCell(Text(dataParts[3])),
          ],
        );
      } else {
        return null;
      }
    }).where((row) => row != null).map<DataRow>((row) => row!).toList();

    return rows;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Summary'),
      ),
      body: FutureBuilder<List<DataRow>>(
        future: _loadData(),
        builder: (BuildContext context, AsyncSnapshot<List<DataRow>> snapshot) {
          if (snapshot.connectionState == ConnectionState.done) {
            if (snapshot.hasError) {
              return Center(child: Text('Error loading data'));
            }

            return ListView(
              children: <Widget>[
                DataTable(
                  columns: const [
                    DataColumn(label: Text('School')),
                    DataColumn(label: Text('CPS Number')),
                    DataColumn(label: Text('Item Description')),
                    DataColumn(label: Text('Reason For Surplus')),
                  ],
                  rows: snapshot.data ?? [],
                ),
                 Padding(
                  padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 24.0),
                  child: ElevatedButton(
                    onPressed: () async {
                      await clearFormData();
                      Navigator.of(context).popUntil((route) => route.isFirst);
                    },
                    child: Text('Submit'),
                  ),
                ),
              ],
            );
          } else {
            return Center(child: CircularProgressIndicator());
          }
        },
      ),
    );
  }
  
  clearFormData() {
  }
  

any help would be amazing! still new to flutter so I am sorry if this is a dumb question to ask…thank you in advance for your time and help 🙂

2

Answers


  1. I see the beginning of your problem because a StatefulWidget attribute cannot change its value:

    
    void reviewData(BuildContext context) async {
        final prefs = await SharedPreferences.getInstance();
        List<String> formDataList = [];
        print("_selectedSchool $_selectedSchool");
        forms.forEach((form) {
          formDataList.add('
    ${_selectedSchool ?? 'No School Selected'}|
    ${form.cpsNumber /*error value can't change*/}|
    ${form.itemDescription /*error value can't change*/}|
    ${form.selectedReason /*error value can't change*/}');
        });
        await prefs.setStringList('formData', formDataList);
        Navigator.push(context, MaterialPageRoute(builder: (context) => SummaryPage()));
      }
    
    class MyCustomForm extends StatefulWidget {
      final VoidCallback onCpsNumberTyped;
    
      MyCustomForm({required this.onCpsNumberTyped});
    
      @override
      _MyCustomFormState createState() => _MyCustomFormState();
    
      // create attibute here is equivalent of const
      String get cpsNumber => ''; // always ''
      String get itemDescription => ''; // always ''
      String get selectedReason => ''; // always ''
    }
    

    if you want to be able to record dynamically send a function instead

    class MyCustomForm extends StatefulWidget {
      final VoidCallback onCpsNumberTyped;
    
      MyCustomForm({required this.onCpsNumberTyped,required this.youFunction});
    
      @override
      _MyCustomFormState createState() => _MyCustomFormState();
    
      // create attibute here is equivalent of const (old)
      String get cpsNumber => ''; // always ''
      String get itemDescription => ''; // always ''
      String get selectedReason => ''; // always ''
      
      // create function (new)
      final Function youFunction;
    }
    
    class _MyCustomFormState extends State<MyCustomForm> {
    
      @override
      Widget build(BuildContext context) {
        // use function in your code
        widget.youFunction()
      }
    }
    

    if this is too vague please tell me for further explanation.

    Login or Signup to reply.
  2. The reason for your code was provided by Mr.Munsch. Your values were always empty strings. I’ve changed your code a bit, so instead of keeping a list of Widgets (forms), now I keep a list of FormData (I’ve created a class to keep the data). Based on that list, I create a list of Widgets. With that change, you always have access to your data, which is updated after every change in the form. It is not the best solution overall, but requires the smallest amount of changes in your code.

    I’ve also changed how onCpsNumberTyped is called, by removing isFirstTime, and replacing it with a listener which removes itself. It was not required as your solution was also fine.

    import 'package:flutter/material.dart';
    import 'package:shared_preferences/shared_preferences.dart';
    
    void main() => runApp(MaterialApp(home: MyApp()));
    
    class MyApp extends StatefulWidget {
      @override
      _MyAppState createState() => _MyAppState();
    }
    
    class _MyAppState extends State<MyApp> {
      List<FormData> forms = [FormData()];
      
    // removed initState
    
      void addNewForm() {
        setState(() => forms.add(FormData()));
      }
    
      void reviewData(BuildContext context) async {
        final prefs = await SharedPreferences.getInstance();
        List<String> formDataList = forms
            // not necessary, but results are cleaner
            .where((element) => element.cpsNumber.isNotEmpty)
            .map((form) =>
                '${_selectedSchool ?? 'No School Selected'}|${form.cpsNumber}|${form.itemDescription}|${form.selectedReason}')
            .toList();
        await prefs.setStringList('formData', formDataList);
        Navigator.push(context, MaterialPageRoute(builder: (context) => SummaryPage()));
      }
    
      @override
      Widget build(BuildContext context) {
        return // the same as earlier with Scaffold etc
                  child: ListView.builder(
                    itemCount: forms.length,
                    itemBuilder: (context, index) => MyCustomForm(
                      formData: forms[index],
                      onCpsNumberTyped: addNewForm,
                      onDataUpdated: (data) {
                        setState(() => forms[index] = data);
                      },
                    ),
                  ),
                ),
      }
    }
    
    class FormData {
      final String cpsNumber;
      final String itemDescription;
      final String? selectedReason;
    
      FormData({
        this.cpsNumber = "",
        this.itemDescription = "",
        this.selectedReason,
      });
    
      FormData copyWith({String? cpsNumber, String? itemDescription, String? selectedReason}) => FormData(
            cpsNumber: cpsNumber ?? this.cpsNumber,
            itemDescription: itemDescription ?? this.itemDescription,
            selectedReason: selectedReason ?? this.selectedReason,
          );
    }
    
    class MyCustomForm extends StatefulWidget {
      final VoidCallback onCpsNumberTyped;
      final FormData formData;
      final Function(FormData) onDataUpdated;
    
      MyCustomForm({required this.onCpsNumberTyped, required this.onDataUpdated, required this.formData});
    
      @override
      _MyCustomFormState createState() => _MyCustomFormState();
    }
    
    class _MyCustomFormState extends State<MyCustomForm> {
      late final TextEditingController _cpsNumberController = TextEditingController(text: widget.formData.cpsNumber);
      late final TextEditingController _itemDescriptionController =
          TextEditingController(text: widget.formData.itemDescription);
      late String? selectedReason = widget.formData.selectedReason;
      late FormData _data = widget.formData;
    
      final List<String> _reasonsForSurplus = [
        'Obsolete Equipment',
        'Replacement',
        'Damage',
        'Others',
      ];
    
      String get cpsNumber => _cpsNumberController.text;
    
      String get itemDescription => _itemDescriptionController.text;
    
      @override
      void initState() {
        super.initState();
        _cpsNumberController.addListener(_onAddNewFormListener);
        _cpsNumberController.addListener(() {
          _data = _data.copyWith(cpsNumber: cpsNumber);
          widget.onDataUpdated(_data);
        });
        _itemDescriptionController.addListener(() {
          _data = _data.copyWith(itemDescription: itemDescription);
          widget.onDataUpdated(_data);
        });
      }
    
      void _onAddNewFormListener(){
        if (_cpsNumberController.text.isNotEmpty) {
          widget.onCpsNumberTyped();
          _cpsNumberController.removeListener(_onAddNewFormListener);
        }
      }
    
      @override
      void dispose() {
        _cpsNumberController.dispose();
        _itemDescriptionController.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return Padding(
          padding: const EdgeInsets.all(16.0),
          child: Card(
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  TextFormField(
                    controller: _cpsNumberController,
                    decoration: InputDecoration(
                      labelText: 'CPS Number',
                      border: OutlineInputBorder(),
                    ),
                  ),
                  SizedBox(height: 24.0),
                  TextFormField(
                    controller: _itemDescriptionController,
                    decoration: InputDecoration(
                      labelText: 'Item Description',
                      border: OutlineInputBorder(),
                    ),
                    maxLines: null,
                  ),
                  SizedBox(height: 24.0),
                  DropdownButtonFormField<String>(
                    value: selectedReason,
                    decoration: InputDecoration(
                      labelText: 'Reason for Surplus',
                      border: OutlineInputBorder(),
                    ),
                    onChanged: (String? newValue) {
                      setState(() {
                        selectedReason = newValue;
                        _data = _data.copyWith(selectedReason: newValue);
                        widget.onDataUpdated(_data);
                      });
                    },
                    items: _reasonsForSurplus.map<DropdownMenuItem<String>>((String value) {
                      return DropdownMenuItem<String>(
                        value: value,
                        child: Text(value),
                      );
                    }).toList(),
                  ),
                ],
              ),
            ),
          ),
        );
      }
    }
    
    

    I don’t know your requirements, but I think you can skip the use of SharedPreferences, and then your code is more simple.

    To do that, replace reviewData with:

      void reviewData(BuildContext context) async {
        final formDataList = forms.where((element) => element.cpsNumber.isNotEmpty).toList();
        Navigator.push(
          context,
          MaterialPageRoute(
            builder: (context) => SummaryPage(
              formData: formDataList,
              school: _selectedSchool ?? "",
            ),
          ),
        );
      }
    

    Now your SummaryPage looks like this:

    class SummaryPage extends StatelessWidget {
      final List<FormData> formData;
      final String school;
    
      const SummaryPage({super.key, required this.formData, required this.school});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(
              title: Text('Summary'),
            ),
            body: ListView(
              children: <Widget>[
                DataTable(
                  columns: const [
                    DataColumn(label: Text('School')),
                    DataColumn(label: Text('CPS Number')),
                    DataColumn(label: Text('Item Description')),
                    DataColumn(label: Text('Reason For Surplus')),
                  ],
                  rows: formData
                      .map((e) => DataRow(cells: [
                            DataCell(Text(school)),
                            DataCell(Text(e.cpsNumber)),
                            DataCell(Text(e.itemDescription)),
                            DataCell(Text(e.selectedReason ?? "")),
                          ]))
                      .toList(),
                ),
                Padding(
                  padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 24.0),
                  child: ElevatedButton(
                    onPressed: () async {
                      Navigator.of(context).popUntil((route) => route.isFirst);
                    },
                    child: Text('Submit & Start Over'),
                  ),
                ),
              ],
            ));
      }
    }
    

    Hope this helps

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search