flutter_custom_textfields 0.0.4 copy "flutter_custom_textfields: ^0.0.4" to clipboard
flutter_custom_textfields: ^0.0.4 copied to clipboard

A Flutter package with customizable text fields, regex validation, and OTP input support with leading/trailing icons.

🧩 Flutter Custom TextFields #

Pub Version License: MIT

A Flutter package that provides customizable text fields with built-in validation for common input types. Features include email, phone number, username, full name, OTP, password, and confirm password fields with configurable validation and UI options.

✨ Features #

  • πŸ“§ TextFields with leading icons for:

    • Email
    • Phone Number
    • Username
    • Full Name
    • OTP (4 digit)
  • πŸ”’ TextFields with both leading and trailing icons for:

    • Password
    • Confirm Password
  • πŸ”’ OTP Input with customizable length (4 or 6 digits)

  • βœ… Built-in Regex-based validation for all input types

  • 🧩 Easy integration and customization

πŸš€ Getting Started #

Add this to your pubspec.yaml:

...dart

class FormExample extends StatefulWidget { const FormExample({super.key});

@override State

class _FormExampleState extends State

final FocusNode _emailNode = FocusNode(); final FocusNode _fullnameNode = FocusNode(); final FocusNode _usernameNode = FocusNode(); final FocusNode _phoneNode = FocusNode();

@override void dispose() { _emailController.dispose(); _usernameController.dispose(); _fullnameController.dispose(); _phoneController.dispose(); _emailNode.dispose(); _fullnameNode.dispose(); _usernameNode.dispose(); _phoneNode.dispose(); super.dispose(); }

void _submitForm() { if (_formKey.currentState!.validate()) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Form submitted successfully')), ); print('Email: ${_emailController.text}'); print('Username: ${_usernameController.text}'); print('Full Name: ${_fullnameController.text}'); print('Phone: ${_phoneController.text}'); } else { // Show error message when form is invalid on submit ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Please fix the errors in the form'), backgroundColor: Colors.red, ), ); } }

@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Flutter Custom Fields Demo')), body: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Email Field const Text( 'Email Field with Validation', style: TextStyle(fontWeight: FontWeight.bold), ), const SizedBox(height: 8), EmailTextField( controller: _emailController, focusNode: _emailNode, iconColor: Theme.of(context).primaryColor, autovalidateMode: AutovalidateMode.onUserInteraction, invalidEmailMessage: 'Please enter a valid email address', requiredEmailMessage: 'Email is required', ), const SizedBox(height: 16),

          const Text(
            'Full Name with Validation',
            style: TextStyle(fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          FullNameTextField(
            focusNode: _fullnameNode,
            controller: _fullnameController,
            iconColor: Theme.of(context).primaryColor,
            autovalidateMode: AutovalidateMode.onUserInteraction,
            invalidNameMessage: 'Please enter a valid name',
            requiredNameMessage: 'Full name is required',
          ),
          const SizedBox(height: 16),

          const Text(
            'Phone Number',
            style: TextStyle(fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          FlexiblePhoneField(
            controller: _phoneController,
            focusNode: _phoneNode,
            style: PhoneFieldStyle.dropdown,
            isShowError: true,
          ),
          const SizedBox(height: 15),

          const Text(
            'Fixed Country Code Style',
            style: TextStyle(fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          FlexiblePhoneField(
            controller: TextEditingController(),
            focusNode: FocusNode(),
            style: PhoneFieldStyle.simple,
            isShowError: true,
          ),
          const SizedBox(height: 15),

          const Text(
            'Phone number with Icons',
            style: TextStyle(fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          FlexiblePhoneField(
            controller: TextEditingController(),
            focusNode: FocusNode(),
            style: PhoneFieldStyle.withIcons,
            isShowError: true,
          ),
          const SizedBox(height: 15),

          const Text(
            'Phone number with Normal',
            style: TextStyle(fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          FlexiblePhoneField(
            controller: TextEditingController(),
            focusNode: FocusNode(),
            style: PhoneFieldStyle.integrated,
            isShowError: true,
          ),
          const SizedBox(height: 15),

          const Text(
            'User Name TextField',
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          UsernameTextfield(
            controller: _usernameController,
            focusNode: _usernameNode,
            autovalidateMode: AutovalidateMode.onUserInteraction,
            patternErrorMessage:
                'Username must be 3-20 characters and can only contain letters, numbers, and underscore',
          ),
          const SizedBox(height: 24),

          const Text(
            'OTP Field',
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 8),
          SizedBox(width: double.infinity, child: AdvancedOTPDemoScreen()),

          // AdvancedOTPField(
          //   length: 6,
          //   onCompleted: (value) {
          //     print('Completed: $value');
          //   },
          //   fieldStyle: const OTPFieldStyle(
          //     width: 45,
          //     height: 55,
          //     margin: EdgeInsets.symmetric(horizontal: 4),
          //     textStyle: TextStyle(
          //       fontSize: 20,
          //       fontWeight: FontWeight.bold,
          //     ),
          //   ),
          //   fieldDecoration: const OTPFieldDecoration(
          //     borderRadius: BorderRadius.all(Radius.circular(12)),
          //     borderWidth: 1.5,
          //   ),
          // ),
          const SizedBox(height: 24),

          ElevatedButton(
            onPressed: _submitForm,
            style: ElevatedButton.styleFrom(
              padding: const EdgeInsets.symmetric(vertical: 16),
            ),
            child: const Text('Submit'),
          ),
        ],
      ),
    ),
  ),
);

} }

...dart

class PhonenumberTf extends StatefulWidget { const PhonenumberTf({super.key});

@override State

class _PhonenumberTfState extends State

      const SizedBox(height: 15),

      const Text(
        'Fixed Country Code Style ',
        style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
      ),
      const SizedBox(height: 8),
      FlexiblePhoneField(
        controller: _phoneController_fixedCode,
        onSubmitted: () {
          print(
            'Flexible Phone Number in Fixed Country code: ${_phoneController_fixedCode.text}',
          );
        },
        focusNode: phonefixedfocus,
        hintText: "Enter your mobile number",
        countryCode: "+91",
        style: PhoneFieldStyle.integrated,
      ),

      // FlexiblePhoneField(
      //   controller: _phoneController_fixedCode,
      //   initialCountryCode: '+91',
      //   hint: 'Enter your mobile number',
      //   displayMode: PhoneFieldDisplayMode.fixed,
      //   separator: "",
      //   maxLength: 10,
      //   onPhoneChanged: (fullPhoneNumber, phoneNumber, countryCode) {
      //     setState(() {
      //       print(
      //         'Flexible Phone Number in Fixed Country code: $fullPhoneNumber, $phoneNumber, $countryCode',
      //       );
      //     });
      //   },
      // ),
      const SizedBox(height: 15),

      const Text(
        'Phone number with Icons',
        style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
      ),
      const SizedBox(height: 8),
      FlexiblePhoneField(
        controller: _phoneController_icon,
        onSubmitted: () {
          print(
            'Flexible Phone Number in ICON Country code: ${_phoneController_icon.text}',
          );
        },
        focusNode: phoneIconFocus,
        hintText: "Enter your mobile number",
        countryCode: "+91",
        style: PhoneFieldStyle.withIcons,
      ),

      const Text(
        'Phone number with Normal',
        style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
      ),
      const SizedBox(height: 8),
      FlexiblePhoneField(
        controller: _phoneController_normal,
        onSubmitted: () {
          setState(() {
            print(
              'Flexible Phone Number in Normal Country code: ${_phoneController_normal.text}',
            );
          });
        },
        focusNode: phoneNormalFocus,
        hintText: "Enter your mobile number",
        countryCode: "+91",
        style: PhoneFieldStyle.simple,
      ),
      // FlexiblePhoneField(
      //   regexPattern: r'^[6-9]\d{9}$',
      //   controller: _phoneController_icon,
      //   hint: 'Enter your phone number',
      //   label: 'Mobile Number',
      //   maxLength: 10,
      //   displayMode: PhoneFieldDisplayMode.phoneOnly,
      //   separator: "",
      //   decoration: InputDecoration(
      //     labelText: 'Mobile Number',
      //     hintText: 'Mobile number (10 digits)',
      //     prefixIcon: const Icon(Icons.smartphone),
      //     border: OutlineInputBorder(
      //       borderRadius: BorderRadius.circular(12),
      //     ),
      //     filled: true,
      //     fillColor: Colors.grey.shade100,
      //   ),
      //   onPhoneChanged: (fullPhoneNumber, phoneNumber, countryCode) {
      //     setState(() {
      //       print(
      //         'Flexible Phone Number only: $fullPhoneNumber, $phoneNumber, $countryCode',
      //       );
      //     });
      //   },
      // ),
    ],
  ),
);

} }

class AdvancedOTPDemoScreen extends StatefulWidget { const AdvancedOTPDemoScreen({super.key});

@override State

class _AdvancedOTPDemoScreenState extends State

// Configuration options OTPAnimationType _animationType = OTPAnimationType.scale; OTPInputType _inputType = OTPInputType.numeric; String _placeholderText = '*'; bool _obscureText = false; bool _enableActiveFill = true;

@override void initState() { super.initState(); _focusNode.addListener(() { setState(() { _isFocused = _focusNode.hasFocus; }); }); }

@override void dispose() { _focusNode.dispose(); super.dispose(); }

@override Widget build(BuildContext context) { final screenWidth = MediaQuery.of(context).size.width; final availableWidth = screenWidth - (24 * 2); final fieldWidth = (availableWidth - (24 * 3)) / 4;

return SingleChildScrollView(
  padding: const EdgeInsets.all(0.0),
  child: Column(
    children: [
      GestureDetector(
        onTap: () {
          setState(() {
            _isFocused = true;
            _focusNode.requestFocus();
          });
        },
        child: AdvancedOTPField(
          key: _otpFieldKey,
          length: 4,
          focusNode: _focusNode,
          inputType: _inputType,
          animationType: _animationType,
          obscureText: _obscureText,
          enableActiveFill: _enableActiveFill,
          autoFocus: false,
          fieldStyle: OTPFieldStyle(
            width: fieldWidth,
            height: 70,
            margin: const EdgeInsets.symmetric(horizontal: 10),
            placeholderText: _placeholderText,
            textStyle: const TextStyle(
              fontSize: 24,
              fontWeight: FontWeight.bold,
            ),
            placeholderStyle: TextStyle(
              fontSize: 20,
              fontWeight: FontWeight.w300,
              color: Colors.grey[400],
            ),
            showCursor: _isFocused,
            cursorColor: Colors.blue,
            cursorWidth: 2,
            cursorHeight: 32,
            showPlaceholderOnlyForEmptyBox: true,
          ),
          fieldDecoration: OTPFieldDecoration(
            borderRadius: BorderRadius.circular(12),
            borderWidth: 1.5,
            activeColor:
                Colors
                    .grey[300], // _isFocused ? Colors.blue :Colors.grey[300],
            inactiveColor: Colors.grey[300],
            selectedColor: _isFocused ? Colors.blue : Colors.grey[300],
            errorColor: Colors.red[400],
            backgroundColor: Colors.grey[50],
            boxShadow: BoxShadow(
              color: Colors.black.withOpacity(0.05),
              blurRadius: 8,
              offset: const Offset(0, 2),
            ),
          ),
          errorText: _errorMessage,
          onChanged: (value) {
            setState(() {
              _enteredOTP = value;
              _errorMessage = null;
              _activeIndex = value.length;
              if (_isFirstInput && value.isNotEmpty) {
                _isFirstInput = false;
              }
            });
          },
          onCompleted: (value) {
            _verifyOTP(value);
          },
        ),
      ),
      const SizedBox(height: 32),
      _buildVerifyButton(),
      // const SizedBox(height: 24),
      // _buildActionButtons(),
    ],
  ),
);

}

Widget _buildVerifyButton() { return SizedBox( width: double.infinity, height: 56, child: ElevatedButton( onPressed: _enteredOTP.length == 4 && !_isVerifying // Enable when 4 digits entered ? () => _verifyOTP(_enteredOTP) : null, style: ElevatedButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.primary, foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), elevation: 0, shadowColor: Theme.of(context).colorScheme.primary.withOpacity(0.3), ), child: _isVerifying ? const SizedBox( height: 20, width: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation

Widget _buildActionButtons() { return Row( children: [ Expanded( child: OutlinedButton.icon( onPressed: () { _otpFieldKey.currentState?.clear(); setState(() { _enteredOTP = ''; _errorMessage = null; _activeIndex = 0; }); }, icon: const Icon(Icons.refresh_rounded), label: const Text('Clear'), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), ), const SizedBox(width: 12), Expanded( child: OutlinedButton.icon( onPressed: () { _showSnackBar('New OTP sent successfully!', Colors.green); _otpFieldKey.currentState?.clear(); setState(() { _activeIndex = 0; }); }, icon: const Icon(Icons.send_rounded), label: const Text('Resend'), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), ), ], ); }

Widget _buildFeaturesDemo() { return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.grey[200]!), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Demo Features', style: Theme.of( context, ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), ), const SizedBox(height: 16), Wrap( spacing: 8, runSpacing: 8, children: [ _buildFeatureChip('Auto-fill Support', Icons.auto_fix_high), _buildFeatureChip('Paste Support', Icons.content_paste), _buildFeatureChip('Custom Animations', Icons.animation), _buildFeatureChip('Error Handling', Icons.error_outline), _buildFeatureChip('Customizable UI', Icons.palette), _buildFeatureChip('Accessibility', Icons.accessibility), ], ), ], ), ); }

Widget _buildFeatureChip(String label, IconData icon) { return Chip( avatar: Icon(icon, size: 16), label: Text(label, style: const TextStyle(fontSize: 12)), backgroundColor: Theme.of( context, ).colorScheme.primaryContainer.withOpacity(0.3), side: BorderSide.none, ); }

void _showConfigDialog() { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Configuration'), content: StatefulBuilder( builder: (context, setDialogState) => Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ // Animation Type const Text( 'Animation Type:', style: TextStyle(fontWeight: FontWeight.w600), ), const SizedBox(height: 8), DropdownButton

                  // Input Type
                  const Text(
                    'Input Type:',
                    style: TextStyle(fontWeight: FontWeight.w600),
                  ),
                  const SizedBox(height: 8),
                  DropdownButton<OTPInputType>(
                    value: _inputType,
                    isExpanded: true,
                    items:
                        OTPInputType.values.map((type) {
                          return DropdownMenuItem(
                            value: type,
                            child: Text(type.name.toUpperCase()),
                          );
                        }).toList(),
                    onChanged: (value) {
                      setDialogState(() {
                        _inputType = value!;
                      });
                    },
                  ),
                  const SizedBox(height: 16),

                  // Placeholder Text
                  const Text(
                    'Placeholder:',
                    style: TextStyle(fontWeight: FontWeight.w600),
                  ),
                  const SizedBox(height: 8),
                  TextFormField(
                    initialValue: _placeholderText,
                    decoration: const InputDecoration(
                      hintText: 'Enter placeholder character',
                      border: OutlineInputBorder(),
                      contentPadding: EdgeInsets.symmetric(
                        horizontal: 12,
                        vertical: 8,
                      ),
                    ),
                    maxLength: 1,
                    onChanged: (value) {
                      setDialogState(() {
                        _placeholderText = value;
                      });
                    },
                  ),
                  const SizedBox(height: 16),

                  // Toggle Options
                  SwitchListTile(
                    title: const Text('Obscure Text'),
                    value: _obscureText,
                    onChanged: (value) {
                      setDialogState(() {
                        _obscureText = value;
                      });
                    },
                    contentPadding: EdgeInsets.zero,
                  ),
                  SwitchListTile(
                    title: const Text('Active Fill'),
                    value: _enableActiveFill,
                    onChanged: (value) {
                      setDialogState(() {
                        _enableActiveFill = value;
                      });
                    },
                    contentPadding: EdgeInsets.zero,
                  ),
                ],
              ),
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('Cancel'),
          ),
          ElevatedButton(
            onPressed: () {
              setState(() {
                // Apply changes
              });
              Navigator.pop(context);
              _otpFieldKey.currentState?.clear();
            },
            child: const Text('Apply'),
          ),
        ],
      ),
);

}

void _verifyOTP(String otp) { setState(() { _isVerifying = true; _errorMessage = null; });

// Simulate API call
Future.delayed(const Duration(seconds: 2), () {
  setState(() {
    _isVerifying = false;
  });

  if (otp == _correctOTP) {
    _showSnackBar('OTP verified successfully!', Colors.green);
  } else {
    setState(() {
      _errorMessage = 'Invalid OTP. Please try again.';
    });
    _otpFieldKey.currentState?.clear(); // Clear the field on invalid OTP
    _activeIndex = 0;
    _otpFieldKey.currentState?.animateError(); // Show error animation
  }
});

}

void _showSnackBar(String message, Color color) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Row( children: [ Icon( color == Colors.green ? Icons.check_circle : Icons.error, color: Colors.white, size: 20, ), const SizedBox(width: 8), Expanded(child: Text(message)), ], ), backgroundColor: color, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), margin: const EdgeInsets.all(16), ), ); } }

dependencies:
  flutter_custom_textfields: ^0.0.4
0
likes
0
points
15
downloads

Publisher

unverified uploader

Weekly Downloads

A Flutter package with customizable text fields, regex validation, and OTP input support with leading/trailing icons.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter

More

Packages that depend on flutter_custom_textfields