CustomTextField

A beautiful, animated, and highly customizable TextField widget for Flutter with smooth animations, elegant design, and comprehensive features.

Features

  • Beautiful Animations: Smooth border color, width, and fill color transitions
  • Fully Customizable: Every aspect can be customized including colors, borders, typography
  • Theme Adaptive: Automatically adapts to light/dark themes
  • Password Support: Built-in obscure text functionality
  • Input Types: Support for all keyboard types and input actions
  • Form Validation: Built-in validator support with error styling
  • Icons Support: Prefix and suffix icons with proper spacing
  • Multi-line Support: Configurable for single or multi-line input
  • Focus Management: Advanced focus handling with animations
  • Performance Optimized: Efficient animations with proper resource management

Installation

Add the following to your pubspec.yaml:

dependencies:
  simple_dynamic_textfield: ^1.0.0

Then run:

flutter pub get

Quick Start

Import the package

import 'package:simple_dynamic_textfield/custom_textfield.dart';

Basic Usage

class MyForm extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: CustomTextField(
          label: 'Enter your name',
          hint: 'John Doe',
        ),
      ),
    );
  }
}

Complete Examples

1. Email Input Field

import 'package:flutter/material.dart';
import 'package:simple_dynamic_textfield/custom_textfield.dart'; // Adjust the import as needed

class EmailInputExample extends StatelessWidget {
  final TextEditingController emailController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Email Input')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: CustomTextField(
          label: 'Email Address',
          hint: 'Enter your email',
          controller: emailController,
          keyboardType: TextInputType.emailAddress,
          prefixIcon: Icon(Icons.email_outlined),
          validator: (value) {
            if (value?.isEmpty ?? true) return 'Email is required';
            if (!value!.contains('@')) return 'Invalid email format';
            return null;
          },
          onChanged: (value) {
            print('Email: $value');
          },
        ),
      ),
    );
  }
}

void main() {
  runApp(MaterialApp(
    home: EmailInputExample(),
  ));
}

2. Password Field with Toggle

class PasswordField extends StatefulWidget {
  @override
  _PasswordFieldState createState() => _PasswordFieldState();
}

class _PasswordFieldState extends State<PasswordField> {
  bool _obscureText = true;
  final TextEditingController _passwordController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return CustomTextField(
      label: 'Password',
      hint: 'Enter your password',
      controller: _passwordController,
      obscureText: _obscureText,
      prefixIcon: Icon(Icons.lock_outline),
      suffixIcon: IconButton(
        icon: Icon(_obscureText ? Icons.visibility : Icons.visibility_off),
        onPressed: () {
          setState(() {
            _obscureText = !_obscureText;
          });
        },
      ),
      validator: (value) {
        if (value?.isEmpty ?? true) return 'Password is required';
        if (value!.length < 6) return 'Password must be at least 6 characters';
        return null;
      },
    );
  }
}

3. Search Field with Custom Styling

import 'package:flutter/material.dart';
import 'package:simple_dynamic_textfield/custom_textfield.dart'; // Ensure this matches your package name

class SearchFieldExample extends StatefulWidget {
  @override
  _SearchFieldExampleState createState() => _SearchFieldExampleState();
}

class _SearchFieldExampleState extends State<SearchFieldExample> {
  String _searchQuery = '';

  void _performSearch(String query) {
    setState(() {
      _searchQuery = query;
    });
    print('Searching for: $query');
    // Implement additional search logic here
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Search Field')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            CustomTextField(
              label: 'Search',
              hint: 'Search products, brands...',
              prefixIcon: Icon(Icons.search, color: Colors.blue),
              borderRadius: 25.0,
              fillColor: Colors.blue.shade50,
              focusedBorderColor: Colors.blue.shade400,
              enabledBorderColor: Colors.blue.shade200,
              animationDuration: Duration(milliseconds: 300),
              onChanged: _performSearch,
            ),
            SizedBox(height: 20),
            Text('You searched for: $_searchQuery'),
          ],
        ),
      ),
    );
  }
}

void main() {
  runApp(MaterialApp(
    home: SearchFieldExample(),
  ));
}

4. Multi-line Text Area

import 'package:flutter/material.dart';
import 'package:simple_dynamic_textfield/custom_textfield.dart'; // Adjust based on your setup

class DescriptionForm extends StatefulWidget {
  @override
  _DescriptionFormState createState() => _DescriptionFormState();
}

class _DescriptionFormState extends State<DescriptionForm> {
  final _formKey = GlobalKey<FormState>();
  final _descriptionController = TextEditingController();

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

  void _submit() {
    if (_formKey.currentState!.validate()) {
      print('Submitted: ${_descriptionController.text}');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Multi-line Input Example')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Form(
          key: _formKey,
          child: Column(
            children: [
              CustomTextField(
                label: 'Description',
                hint: 'Tell us about yourself...',
                controller: _descriptionController,
                maxLines: 5,
                textCapitalization: TextCapitalization.sentences,
                contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 20),
                validator: (value) {
                  if (value?.isEmpty ?? true) return 'Description is required';
                  if (value!.length < 10) return 'Please provide more details';
                  return null;
                },
              ),
              SizedBox(height: 24),
              ElevatedButton(
                onPressed: _submit,
                child: Text('Submit'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

void main() {
  runApp(MaterialApp(
    home: DescriptionForm(),
  ));
}

5. Phone Number Field

import 'package:flutter/material.dart';
import 'package:simple_dynamic_textfield/custom_textfield.dart'; // Adjust if needed

class PhoneNumberForm extends StatefulWidget {
  @override
  _PhoneNumberFormState createState() => _PhoneNumberFormState();
}

class _PhoneNumberFormState extends State<PhoneNumberForm> {
  final _formKey = GlobalKey<FormState>();
  final _phoneController = TextEditingController();

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

  void _submit() {
    if (_formKey.currentState!.validate()) {
      print('Phone number: ${_phoneController.text}');
      // Add your submission logic here
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Phone Input')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Form(
          key: _formKey,
          child: Column(
            children: [
              CustomTextField(
                label: 'Phone Number',
                hint: '+1 (555) 123-4567',
                controller: _phoneController,
                keyboardType: TextInputType.phone,
                prefixIcon: Icon(Icons.phone_outlined),
                textInputAction: TextInputAction.done,
                validator: (value) {
                  if (value?.isEmpty ?? true) return 'Phone number is required';
                  if (value!.length < 10) return 'Invalid phone number';
                  return null;
                },
              ),
              SizedBox(height: 24),
              ElevatedButton(
                onPressed: _submit,
                child: Text('Submit'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

void main() {
  runApp(MaterialApp(
    home: PhoneNumberForm(),
  ));
}

6. Complete Login Form

class LoginForm extends StatefulWidget {
  @override
  _LoginFormState createState() => _LoginFormState();
}

class _LoginFormState extends State<LoginForm> {
  final _formKey = GlobalKey<FormState>();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  bool _obscurePassword = true;

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          CustomTextField(
            label: 'Email',
            hint: 'Enter your email',
            controller: _emailController,
            keyboardType: TextInputType.emailAddress,
            prefixIcon: Icon(Icons.email_outlined),
            textInputAction: TextInputAction.next,
            validator: (value) {
              if (value?.isEmpty ?? true) return 'Email is required';
              if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value!)) {
                return 'Invalid email format';
              }
              return null;
            },
          ),
          
          SizedBox(height: 20),
          
          CustomTextField(
            label: 'Password',
            hint: 'Enter your password',
            controller: _passwordController,
            obscureText: _obscurePassword,
            prefixIcon: Icon(Icons.lock_outline),
            suffixIcon: IconButton(
              icon: Icon(_obscurePassword ? Icons.visibility : Icons.visibility_off),
              onPressed: () {
                setState(() {
                  _obscurePassword = !_obscurePassword;
                });
              },
            ),
            textInputAction: TextInputAction.done,
            validator: (value) {
              if (value?.isEmpty ?? true) return 'Password is required';
              if (value!.length < 6) return 'Password must be at least 6 characters';
              return null;
            },
            onSubmitted: (value) {
              _handleLogin();
            },
          ),
          
          SizedBox(height: 30),
          
          ElevatedButton(
            onPressed: _handleLogin,
            child: Text('Login'),
          ),
        ],
      ),
    );
  }

  void _handleLogin() {
    if (_formKey.currentState!.validate()) {
      // Process login
      print('Email: ${_emailController.text}');
      print('Password: ${_passwordController.text}');
    }
  }

  @override
  void dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }
}

Customization Options

All Available Properties

import 'package:flutter/material.dart';
import 'package:simple_dynamic_textfield/custom_textfield.dart';

class AllOptionsExample extends StatelessWidget {
  final TextEditingController _controller = TextEditingController();
  final FocusNode _focusNode = FocusNode();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Full CustomTextField Example')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: CustomTextField(
          // Content
          label: 'Field Label',
          hint: 'Placeholder text',
          controller: _controller,

          // Input Configuration
          obscureText: false,
          keyboardType: TextInputType.text,
          textInputAction: TextInputAction.done,
          textCapitalization: TextCapitalization.none,
          maxLines: 1,
          maxLength: null,

          // Behavior
          enabled: true,
          readOnly: false,
          autofocus: false,

          // Styling - Colors
          fillColor: Colors.grey.shade50,
          focusedBorderColor: Colors.blue,
          enabledBorderColor: Colors.grey.shade300,
          labelColor: Colors.grey.shade700,
          hintColor: Colors.grey.shade500,
          textColor: Colors.black,

          // Styling - Dimensions
          borderRadius: 16.0,
          borderWidth: 1.5,
          contentPadding: EdgeInsets.all(16),

          // Styling - Typography
          labelStyle: TextStyle(fontSize: 16),
          hintStyle: TextStyle(color: Colors.grey),
          textStyle: TextStyle(fontWeight: FontWeight.bold),

          // Icons
          prefixIcon: Icon(Icons.person),
          suffixIcon: Icon(Icons.clear),

          // Animation
          animationDuration: Duration(milliseconds: 200),

          // Validation & Callbacks
          validator: (value) => null,
          onChanged: (value) {
            print('Changed: $value');
          },
          onTap: () {
            print('Field tapped');
          },
          onSubmitted: (value) {
            print('Submitted: $value');
          },

          // Advanced
          focusNode: _focusNode,
          errorText: 'Error message',
          floatingLabelBehavior: FloatingLabelBehavior.auto,
        ),
      ),
    );
  }
}

void main() {
  runApp(MaterialApp(
    home: AllOptionsExample(),
  ));
}

Theming

The widget automatically adapts to your app's theme:

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  final ThemeData lightTheme = ThemeData.light().copyWith(
    colorScheme: ColorScheme.light(
      primary: Colors.blue,
      surface: Colors.white,
      onSurface: Colors.black,
    ),
  );

  final ThemeData darkTheme = ThemeData.dark().copyWith(
    colorScheme: ColorScheme.dark(
      primary: Colors.blueAccent,
      surface: Colors.grey.shade900,
      onSurface: Colors.white,
    ),
  );

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Theming Example',
      theme: lightTheme,
      darkTheme: darkTheme,
      themeMode: ThemeMode.system, // Automatically switches based on system setting
      home: Scaffold(
        appBar: AppBar(title: Text('Custom Theme Example')),
        body: Center(
          child: Text('Hello Themed World!'),
        ),
      ),
    );
  }
}

Best Practices

1. Form Validation

// Always use Form widget for validation
import 'package:flutter/material.dart';
import 'package:simple_dynamic_textfield/custom_textfield.dart';

class SimpleFormExample extends StatefulWidget {
  @override
  _SimpleFormExampleState createState() => _SimpleFormExampleState();
}

class _SimpleFormExampleState extends State<SimpleFormExample> {
  final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
  final TextEditingController _fieldController = TextEditingController();

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

  void _submit() {
    if (_formKey.currentState!.validate()) {
      print('Submitted value: ${_fieldController.text}');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Form Example')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Form(
          key: _formKey,
          child: Column(
            children: [
              CustomTextField(
                label: 'Required Field',
                hint: 'Enter something...',
                controller: _fieldController,
                validator: (value) {
                  if (value?.isEmpty ?? true) return 'This field is required';
                  return null;
                },
              ),
              SizedBox(height: 20),
              ElevatedButton(
                onPressed: _submit,
                child: Text('Submit'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

void main() {
  runApp(MaterialApp(
    home: SimpleFormExample(),
  ));
}

2. Controller Management

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  final TextEditingController _controller = TextEditingController();

  @override
  void dispose() {
    _controller.dispose(); // Always dispose controllers
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return CustomTextField(controller: _controller);
  }
}

3. Focus Management

class FocusExample extends StatefulWidget {
  @override
  _FocusExampleState createState() => _FocusExampleState();
}

class _FocusExampleState extends State<FocusExample> {
  final FocusNode _emailFocus = FocusNode();
  final FocusNode _passwordFocus = FocusNode();

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        CustomTextField(
          label: 'Email',
          focusNode: _emailFocus,
          textInputAction: TextInputAction.next,
          onSubmitted: (value) {
            _passwordFocus.requestFocus();
          },
        ),
        CustomTextField(
          label: 'Password',
          focusNode: _passwordFocus,
          textInputAction: TextInputAction.done,
        ),
      ],
    );
  }

  @override
  void dispose() {
    _emailFocus.dispose();
    _passwordFocus.dispose();
    super.dispose();
  }
}

Advanced Usage

Custom Animation Curves

// For different animation feels
import 'package:flutter/material.dart';
import 'package:simple_dynamic_textfield/custom_textfield.dart';

class AnimatedTextFieldExample extends StatelessWidget {
  final TextEditingController _controller = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Custom Animation Duration')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: CustomTextField(
          label: 'Name',
          hint: 'Enter your name',
          controller: _controller,
          animationDuration: Duration(milliseconds: 400),
          // Internally uses Curves.easeInOut by default
        ),
      ),
    );
  }
}

void main() {
  runApp(MaterialApp(
    home: AnimatedTextFieldExample(),
  ));
}

Conditional Styling

import 'package:flutter/material.dart';
import 'package:simple_dynamic_textfield/custom_textfield.dart';

class ConditionalStyleExample extends StatefulWidget {
  @override
  _ConditionalStyleExampleState createState() => _ConditionalStyleExampleState();
}

class _ConditionalStyleExampleState extends State<ConditionalStyleExample> {
  final _controller = TextEditingController();
  final _formKey = GlobalKey<FormState>();
  bool isError = false;

  void _validate() {
    setState(() {
      isError = _controller.text.isEmpty;
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Dynamic Error Styling')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Form(
          key: _formKey,
          child: Column(
            children: [
              CustomTextField(
                label: 'Username',
                hint: 'Enter your username',
                controller: _controller,
                fillColor: isError ? Colors.red.shade50 : Colors.grey.shade50,
                focusedBorderColor: isError ? Colors.red : Colors.blue,
                enabledBorderColor: isError ? Colors.red.shade300 : Colors.grey.shade300,
                validator: (value) {
                  if (value?.isEmpty ?? true) return 'Username is required';
                  return null;
                },
              ),
              SizedBox(height: 20),
              ElevatedButton(
                onPressed: _validate,
                child: Text('Validate'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

void main() {
  runApp(MaterialApp(
    home: ConditionalStyleExample(),
  ));
}

Common Issues & Solutions

Issue: Animation not working

Solution: Make sure you're using the widget in a context where setState can be called.

Issue: Validation not showing

Solution: Wrap your CustomTextField in a Form widget and use a GlobalKey<FormState>.

Issue: Controller not updating

Solution: Ensure you're disposing controllers properly and not creating new instances on each build.

Platform Support

  • Android
  • iOS
  • Web
  • Desktop (Windows, macOS, Linux)

Migration Guide

From basic TextField

import 'package:flutter/material.dart';
import 'package:simple_dynamic_textfield/custom_textfield.dart';

class MigrationExample extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Migration Example')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            // Before
            TextField(
              decoration: InputDecoration(
                labelText: 'Email',
                hintText: 'Enter email',
              ),
            ),

            SizedBox(height: 20),

            // After
            CustomTextField(
              label: 'Email',
              hint: 'Enter email',
            ),
          ],
        ),
      ),
    );
  }
}

void main() {
  runApp(MaterialApp(
    home: MigrationExample(),
  ));
}

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is licensed under the MIT License - see the LICENSE file for details.