23
#100DaysOfCodeChallenge - Crop Management Information System - Day 6
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
In this post we will receive user input data via our form and save the form data in cloud firestore.
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.
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.
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
.
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.
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.
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.
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,
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.
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.
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.
We used FocusNode
, TextEditingController
, TextFormField
, showDatePicker
, Checkbox
and Form
to accept user input and send the Form Date to cloud firestore.
23