Key points for considerations for UI component packaging
To encapsulate a UI component, you usually need to consider the following three points:
- How to define an interface: that is, what input parameters are received by the component to control the appearance and behavior of the component;
- Separation from business: UI components should only be responsible for the interface, not the business, and the specific business should be completed by the business layer;
- Simple and easy to use: Because it is a UI component, it should be as simple and easy to use as possible, making it easy for users to get started quickly.
Text input box interface definition
First, let’s take a look at the code we used in the previous article. Here, we actually just call a function to return a new component. The reason for doing this is because the user name and password use member attributes, and different behaviors need to be set according to different member attributes. The main ones are:
- Text box decorations are different: including placeholders, front icons, and the behavior of the rear icons is bound to member attributes and different TextEditingCongtrollers.
- The onChanged event callback content is different.
- The keyboard type is different from whether to hide input.
- The fields corresponding to the form are different.
Widget _getPasswordInput() { return _getInputTextField( , obscureText: true, controller: _passwordController, decoration: InputDecoration( hintText: "Enter Password", icon: Icon( Icons.lock_open, size: 20.0, ), suffixIcon: GestureDetector( child: Offstage( child: Icon(), offstage: _password == '', ), onTap: () { (() { _password = ''; _passwordController.clear(); }); }, ), border: , ), onChanged: (value) { (() { _password = value; }); }, ); }
The actual reason for the difference is the difference between member attributes. If you continue to use member attributes, this problem cannot be solved. At this time, we can consider putting the entire form into a map, configuring differentiated properties corresponding to different fields in the map, and then we can achieve a common interface. We can define the encapsulated component interface:
Widget _getInputTextFieldNew( String formKey, String value, { TextInputType keyboardType = , FocusNode focusNode, controller: TextEditingController, onChanged: Function, String hintText, IconData prefixIcon, onClear: Function, bool obscureText = false, height = 50.0, margin = 10.0, }) { //...... }
The newly added parameters are as follows:
- formKey: indicates which key of the form map corresponds to the text box;
- value: The value of the current form, used to control whether to display the clear button
- onClear: Defines the behavioral response of the clear button on the right
- onChanged: input content change ratio callback
Code implementation
The extracted code has nothing to do with the business page, so it is necessary to extract the code. Add a new components directory to the lib directory and add a form_util.dart file to store common form components. The implemented code is as follows:
class FormUtil { static Widget textField( String formKey, String value, { TextInputType keyboardType = , 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: (margin), child: Column( children: [ TextField( keyboardType: keyboardType, focusNode: focusNode, obscureText: obscureText, controller: controller, decoration: InputDecoration( hintText: hintText, icon: Icon( prefixIcon, size: 20.0, ), border: , suffixIcon: GestureDetector( child: Offstage( child: Icon(), offstage: value == null || value == '', ), onTap: () { onClear(formKey); }, ), ), onChanged: (value) { onChanged(formKey, value); }), Divider( height: 1.0, color: [400], ), ], ), ); } }
Component usage
The first is to use Map to define the form content of the current page to control the presentation of different form fields.
Map<String, Map<String, Object>> _formData = { 'username': { 'value': '', 'controller': TextEditingController(), 'obsecure': false, }, 'password': { 'value': '', 'controller': TextEditingController(), 'obsecure': true, }, };
The second is to define the unified text box onChanged and onClear methods, corresponding to _handleTextFieldChanged and _handleClear. The content of the corresponding _formData can be updated through the field returned by formKey. Note here that the usage of as is used to convert an Object to a TextEditingController. If the corresponding type of Object is TextEditingController, it can be successfully converted, and the subsequent clear() method can be executed normally. However, if it is null, directly execute the clear() method at this time, and a null pointer exception will be reported. Therefore, a question mark is added after the conversion result, which means that if null, the method will not be executed, so that there will be no null pointer exception. This is the null safety feature introduced by Flutter 2.0. In fact, this special effect has been used in PHP 7 and Swift languages for a long time.
_handleTextFieldChanged(String formKey, String value) { (() { _formData[formKey]['value'] = value; }); } _handleClear(String formKey) { (() { _formData[formKey]['value'] = ''; (_formData[formKey]['controller'] as TextEditingController)?.clear(); }); }
Then use the encapsulated textbox where textField is used directly:
//... ( 'username', _formData['username']['value'], controller: _formData['username']['controller'], hintText: 'Please enter your mobile phone number', prefixIcon: Icons.mobile_friendly, onChanged: _handleTextFieldChanged, onClear: _handleClear, ), ( 'password', _formData['password']['value'], controller: _formData['password']['controller'], obscureText: true, hintText: 'Please enter your password', prefixIcon: Icons.lock_open, onChanged: _handleTextFieldChanged, onClear: _handleClear, ), //...
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 encapsulated text boxes can also be used for other form pages. The maintenance and reusability of the entire code have been greatly improved compared to the previous article.
Trapped record
When encapsulating the text box, the onClear function is directly copied to the onTap property of GesureDetector. As a result, onClear will be triggered to automatically clear the text box contents every time you enter. Later I found out that it should actually be a callback function. The following are the differences in the two ways:
//... //Wrong wayonTap:onClear, //... //... //The correct wayonTap:() { onClear(formKey); }, //...
Summarize
Encapsulated UI components are very common in actual development. Generally speaking, when we see a design draft, we first split the design draft into multiple components, and then consider whether some of the components will be used in other occasions. If it is possible, packaging can be considered. When encapsulating, first consider external interface parameters, and then note that UI components should not involve business, and then be as simple as possible (for example, there are some default values to reduce the required parameters). Of course, if the company can set a set of components from the beginning, pre-packaging will make the subsequent development more efficient, but this depends on the urgency of the project time. If there is enough time, you can consider this.
The above is the detailed content of how Flutter encapsulates text input boxes. For more information about Flutter encapsulates text input boxes, please pay attention to my other related articles!