This is the third day of my participation in Gwen Challenge

In my previous post on Flutter, I mentioned that the text box was not packaged enough, resulting in low code reuse. In the actual development process, it often starts with completing the functional level of development, and then considers component packaging and code optimization. Of course, the sooner component encapsulation is done, the better, because it makes development more standardized and efficient for the entire team.

Considerations for UI component encapsulation

When encapsulating a UI component, there are usually three points to consider:

  1. How interfaces are defined: that is, what input parameters a component receives to control the appearance and behavior of the component;
  2. Separation from business: UI components should only be responsible for the interface, not the business, which should be done by the business layer;
  3. Easy to use: because it is a UI component, it should be as simple and easy to use as possible, so that users can get started quickly.

Text input box interface definition

First of all, let’s take a look at our last text box code. In fact, we just call a function to return a new component. The reason for doing this is that the user name and password use member attributes, and we need to set different behaviors according to different member attributes.

  • The text box decorations are different: placeholders, front ICONS, the behavior of the back icon is bound to the member properties and the different TextEditingCongtroller.
  • OnChanged Event callback content is different.
  • The keyboard type and whether to hide input are different.
  • The fields in the corresponding form are different.
Widget _getPasswordInput() {
    return _getInputTextField(
      TextInputType.text,
      obscureText: true,
      controller: _passwordController,
      decoration: InputDecoration(
        hintText: "Enter your password",
        icon: Icon(
          Icons.lock_open,
          size: 20.0,
        ),
        suffixIcon: GestureDetector(
          child: Offstage(
            child: Icon(Icons.clear),
            offstage: _password == ' ',
          ),
          onTap: () {
            this.setState(() {
              _password = ' ';
              _passwordController.clear();
            });
          },
        ),
        border: InputBorder.none,
      ),
      onChanged: (value) {
        this.setState(() { _password = value; }); }); }Copy the code

But the actual reason for the difference is the difference between member attributes, if it is to continue to use this way of member attributes, can not solve this problem. In this case, we can consider putting the entire form into a Map, where the different attributes of the different fields are configured. Then we can achieve a common interface. We can define the encapsulated component interface:

Widget _getInputTextFieldNew(
    String formKey,
    String value, {
    TextInputType keyboardType = TextInputType.text,
    FocusNode focusNode,
    controller: TextEditingController,
    onChanged: Function.String hintText,
    IconData prefixIcon,
    onClear: Function.bool obscureText = false,
    height = 50.0,
    margin = 10.0, {})/ /...
}
Copy the code

The new parameters are as follows:

  • FormKey: indicates which key of the form Map the text box corresponds to;
  • Value: The value of the current form, which controls whether the clear button is displayed
  • OnClear: Defines the behavioral response of the right clear button
  • OnChanged: The input content changes to callback

Code implementation

The extracted code is irrelevant to the business page, so you need to extract the code to create a components directory in the lib directory and add a form_util.dart file to store common form components. The code is as follows:

class FormUtil {
  static Widget textField(
    String formKey,
    String value, {
    TextInputType keyboardType = TextInputType.text,
    FocusNode focusNode,
    controller: TextEditingController,
    onChanged: Function.String hintText,
    IconData prefixIcon,
    onClear: Function.bool obscureText = false,
    height = 50.0,
    margin = 10.0, {})return Container(
      height: height,
      margin: EdgeInsets.all(margin),
      child: Column(
        children: [
          TextField(
              keyboardType: keyboardType,
              focusNode: focusNode,
              obscureText: obscureText,
              controller: controller,
              decoration: InputDecoration(
                hintText: hintText,
                icon: Icon(
                  prefixIcon,
                  size: 20.0,
                ),
                border: InputBorder.none,
                suffixIcon: GestureDetector(
                  child: Offstage(
                    child: Icon(Icons.clear),
                    offstage: value == null || value == ' ',
                  ),
                  onTap: () {
                    onClear(formKey);
                  },
                ),
              ),
              onChanged: (value) {
                onChanged(formKey, value);
              }),
          Divider(
            height: 1.0,
            color: Colors.grey[400[, [, [, [, [, [ }}Copy the code

Components use

The first step is to use a Map to define the form content of the current page in order to control how different form fields are rendered.

Map<String.Map<String.Object>> _formData = {
    'username': {
      'value': ' '.'controller': TextEditingController(),
      'obsecure': false,},'password': {
      'value': ' '.'controller': TextEditingController(),
      'obsecure': true,}};Copy the code

The next step is to define the unified textbox onChanged and onClear methods, which correspond to _handleTextFieldChanged and _handleClear. The fields returned by formKey can update the contents of _formData. Note the use of as to convert an Object to a TextEditingController. This conversion succeeds if the Object corresponds to a TextEditingController, and the clear() method is executed normally. If it is null, the clear() method will return a null pointer exception. Therefore, a question mark is placed after the result of the conversion, indicating that methods following null will not be executed, and thus null pointer exceptions will not occur. This is the null safety feature introduced into Flutter 2.0. This effect was already available in PHP 7, Swift.

_handleTextFieldChanged(String formKey, String value) {
    this.setState(() {
      _formData[formKey]['value'] = value;
    });
  }

  _handleClear(String formKey) {
    this.setState(() {
      _formData[formKey]['value'] = ' ';
      (_formData[formKey]['controller'] asTextEditingController)? .clear(); }); }Copy the code

We then use the formUtil. textField method directly to use the wrapped text box where textField is used:

/ /...
FormUtil.textField(
    'username',
    _formData['username'] ['value'],
    controller: _formData['username'] ['controller'],
    hintText: 'Please enter your mobile phone number',
    prefixIcon: Icons.mobile_friendly,
    onChanged: _handleTextFieldChanged,
    onClear: _handleClear,
  ),
FormUtil.textField(
    'password',
    _formData['password'] ['value'],
    controller: _formData['password'] ['controller'],
    obscureText: true,
    hintText: 'Please enter your password',
    prefixIcon: Icons.lock_open,
    onChanged: _handleTextFieldChanged,
    onClear: _handleClear,
),
/ /...
Copy the code

As you can see, the username and password form fields reuse _handleTextFieldChanged and _handleClear. The entire code length has also been reduced by nearly 50%, and the FormUtil. TextField text box can also be used in other form pages. The whole code is much more maintainable and reusable than the previous one.

Record on pit

When encapsulating the text box, the onClear function is directly copied to GesureDetector’s onTap property. As a result, onClear automatically clears the text box with each input. It turns out that instead of passing a callback function, here are the differences:

/ /...
// The wrong way
onTap:onClear,
/ /...

/ /...
// The right way
onTap:() {
  onClear(formKey);
},
/ /...
Copy the code

conclusion

Wrapping UI components is a common practice in actual development. When you look at a design, you start by breaking it up into components, and then consider whether some of those components might be used in other contexts. If possible, consider encapsulation. When encapsulating, consider the external interface parameters first, then note that the UI components should not be involved in the business, and then make it as simple as possible (such as having some default values and fewer required parameters). Of course, if the company can define a set of components from product, design, and development at the beginning, early encapsulation will make later development more efficient, but it depends on the urgency of the project and if there is enough time.