#100DaysOfCodeChallenge - Crop Management Information System - Day 6

Recap

On Day 5 we refactored the Farmer Registration form to improve the readability of our code. On Day 2 we created the FarmerService() class and the AddFarmerCommand() class in keeping with gskinner MVC+S architecture

Overview

In this post we will receive user input data via our form and save the form data in cloud firestore.

Going Deeper

We'll look at how all the various pieces fit together. For brevity, we'll select representative widgets and methods to show how the pieces fit together. NB: These code snippets were reused with appropriate variations to build out the form logics.

AddFarmerScreenController

class AddFarmerScreen extends StatefulWidget {
  static String routeName = 'AddFarmerScreen';
  const AddFarmerScreen({
    Key? key,
  }) : super(key: key);

  
  _AddFarmerScreenController createState() => _AddFarmerScreenController();
}

class _AddFarmerScreenController extends State<AddFarmerScreen> {
  
  Widget build(BuildContext context) => _AddFarmerScreenView(this);
  FarmerServiceModel farmer = FarmerServiceModel.form();
  final GlobalKey<FormState> _formkey = GlobalKey<FormState>();
  bool _formChanged = false;
  ...
}

This controller is responsible for all the AddFarmerScreen logic.
It contain an instance of the FarmerServiceModel() called farmer. This class will be sent to cloud Firestore via the FarmerService class.

_formKey tracks the state of the form. It allows us to validate and save the form data.

_formChanged this variable becomes true when the user first interacts with the form.

FocusNode

class _AddFarmerScreenController extends State<AddFarmerScreen> {
  
  Widget build(BuildContext context) => _AddFarmerScreenView(this);
  ...
  late TextEditingController dateOfBirthController;
  late FocusNode nationalIdFocusNode;
  late FocusNode lastNameFocusNode;
  late FocusNode isHeadOfHouseholdFocusNode;
  // FocusNode was created for all form fields. Not shown for 
  // brevity.
  ...
}

By creating FocusNode for each form fields, I can programmatically control which form field should should he highlighted as the user interacts with the form. Similarly, I can programmatically control the text of the DateOfBirthTextField by the DateOfBirthController.

initState

class _AddFarmerScreenController extends State<AddFarmerScreen> {
  
  Widget build(BuildContext context) => _AddFarmerScreenView(this);
    ...

  void initState() {
    super.initState();
    // Initialize the focusNode
    nationalIdFocusNode = FocusNode();
    lastNameFocusNode = FocusNode();
    dateOfBirthFocusNode = FocusNode();
    dateOfBirthController = TextEditingController();
    isHeadOfHouseholdFocusNode = FocusNode();
  }
}
...

The focusNodes and controllers are initialized when the AddFarmerScreen is added to the widget tree.

Dispose

class _AddFarmerScreenController extends State<AddFarmerScreen> {
  
  Widget build(BuildContext context) => _AddFarmerScreenView(this);

...


  void dispose() {
    super.dispose();

    nationalIdFocusNode.dispose();
    lastNameFocusNode.dispose();
    dateOfBirthFocusNode.dispose();
    isHeadOfHouseholdFocusNode.dispose();
    dateOfBirthController.dispose();
  }
 ...
  }

FocusNodes and controller are destroyed when the AddFarmerScreen is removed from the widget tree.

Handle Register Farmer

class _AddFarmerScreenController extends State<AddFarmerScreen> {
  
  Widget build(BuildContext context) => _AddFarmerScreenView(this);

...
  void _handleRegisterFarmer() async {
    if (_formkey.currentState!.validate()) {
      _formkey.currentState!.save();
      AddFarmerCommand(context)
          .run(farmerServiceModel: farmer, context: context);
    }
 ...
  }

This method is called when the user press the RegisterFarmer button. The form is first validated, if validation is successful the form data is added to the FarmerServiceModel. The FarmerServiceModel is then passed to the FarmerService().addFarmer() method via the AddFarmerCommand().run() method. These two methods were discussed in details on Day 2.

The screenshots below show the User input data being submitted via the form followed by the User Input data being stored in cloud firestore.
Farmer Form
Farmer Form

Handle on Will Pop

class _AddFarmerScreenController extends State<AddFarmerScreen> {
  
  Widget build(BuildContext context) => _AddFarmerScreenView(this);

  ...
Future<bool> _handleOnWillPop() async {
    if (_formChanged) {
      return await showDialog<bool>(
          context: context,
          builder: (BuildContext context) {
            return AlertDialog(
              title: Text('Warning'),
              content: Text(
                  'Are you sure you want to abandon the form? Any changes will be lost.'),
              actions: <Widget>[
                TextButton(
                    onPressed: () {
                      Navigator.pop(context, false);
                    },
                    child: Text('Cancel')),
                TextButton(
                    onPressed: () {
                      Navigator.pop(context, true);
                    },
                    child: Text(
                      'Abandon',
                      style: TextStyle(color: Colors.red),
                    )),
              ],
            );
          }).then((value) => value!);
    } else {
      return Future<bool>.value(true);
    }
  }

To ensure good user experience, the _handleOnWillPop() method will be called if the user partially fill out the form but decided to leave the page before registering the Farmer. When this method is called an alertDialog() will be displayed asking to the confirm there intention to leave the page. Screenshot of this dialog is shown below,
Farmer Form

Handle dropdown on changed

class _AddFarmerScreenController extends State<AddFarmerScreen> {
  
  Widget build(BuildContext context) => _AddFarmerScreenView(this);

  ...
  void _handleDropdownOnChanged(FocusNode focusNode) {
    focusNode.requestFocus();
  }

This method assign focus or highlight the Dropdown Form Field the user is currently interacting with. A screenshot of the Marital Status Dropdown field is show below.
Farmer Form

ShowDatePicker

class _AddFarmerScreenController extends State<AddFarmerScreen> {
  
  Widget build(BuildContext context) => _AddFarmerScreenView(this);
  ...
  late DateTime _date;
  void _showDatePicker() {
    showDatePicker(
      context: context,
      initialDate: DateTime(1960),
      firstDate: DateTime(1900),
      lastDate: DateTime.now(),
    ).then(
      (value) {
        if (value != null) {
          setState(() {
            dateOfBirthController.text = DateFormat.yMMMd().format(value);

            _date = value;
          });
        }
      },
    );
  }

This methods displays a date picker when the user taps on the dateOfBirthDropdownFormField(). Screenshot of the Date picker shown below.
Date Picker Dialog

HandleIsHeadOfHousehold

class _AddFarmerScreenController extends State<AddFarmerScreen> {
  
  Widget build(BuildContext context) => _AddFarmerScreenView(this);
  ...
  bool _isHeadOfHousehold = false;
  bool? _handleIsHeadOfHouseholdOnChange(bool? value) {
    operationScaleFocusNode.unfocus();
    if (_isHeadOfHousehold == true) {
      setState(() {
        _isHeadOfHousehold = false;
      });
    } else {
      setState(() {
        setState(() {
          _isHeadOfHousehold = true;
        });
      });
    }
  }

This method allows the user to toggle between the isHeadOfHousehold checkbox.

Wrap Up

We used FocusNode, TextEditingController, TextFormField, showDatePicker, Checkbox and Form to accept user input and send the Form Date to cloud firestore.

Connect with me

Thank you for reading my post. Feel free to subscribe below to join me on the #100DaysOfCodeChallenge or connect with me on LinkedIn and Twitter.

23