Implementing Completely Custom Input Widgets in Flutter

Flutter provides a rich set of pre-built widgets for various input methods, like text fields, checkboxes, and sliders. However, there are situations where you need to create a completely custom input widget to meet specific design or functional requirements. Implementing custom input widgets in Flutter can give your app a unique look and feel, and offer specialized interactions tailored to your use case.

Why Create Custom Input Widgets in Flutter?

  • Unique Design: Implement designs not achievable with standard widgets.
  • Specialized Functionality: Add custom input handling or validation.
  • Accessibility: Tailor the widget for improved accessibility.
  • Performance: Optimize the widget for specific performance needs.

Steps to Implement Completely Custom Input Widgets in Flutter

Step 1: Extend StatelessWidget or StatefulWidget

Determine if your widget requires maintaining state. If it does, use StatefulWidget; otherwise, StatelessWidget is sufficient.

Stateless Custom Input Widget
import 'package:flutter/material.dart';

class CustomInputWidget extends StatelessWidget {
  final String hintText;

  const CustomInputWidget({Key? key, required this.hintText}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(16.0),
      decoration: BoxDecoration(
        border: Border.all(color: Colors.grey),
        borderRadius: BorderRadius.circular(8.0),
      ),
      child: Text(hintText),
    );
  }
}
Stateful Custom Input Widget
import 'package:flutter/material.dart';

class CustomInputWidget extends StatefulWidget {
  const CustomInputWidget({Key? key}) : super(key: key);

  @override
  _CustomInputWidgetState createState() => _CustomInputWidgetState();
}

class _CustomInputWidgetState extends State<CustomInputWidget> {
  String _inputValue = '';

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        children: [
          Text('Entered Value: $_inputValue'),
          TextField(
            onChanged: (text) {
              setState(() {
                _inputValue = text;
              });
            },
            decoration: InputDecoration(
              hintText: 'Enter text here',
              border: OutlineInputBorder(),
            ),
          ),
        ],
      ),
    );
  }
}

Step 2: Implement the build Method

The build method returns the visual representation of your widget. This is where you compose existing widgets to create the desired UI.

@override
Widget build(BuildContext context) {
  return GestureDetector(
    onTap: () {
      // Custom action
      print('Custom input widget tapped!');
    },
    child: Container(
      padding: EdgeInsets.all(16.0),
      decoration: BoxDecoration(
        border: Border.all(color: Colors.blue),
        borderRadius: BorderRadius.circular(8.0),
        color: Colors.white,
      ),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(Icons.star, color: Colors.amber),
          SizedBox(width: 8.0),
          Text('Tap Me!', style: TextStyle(fontSize: 16.0)),
        ],
      ),
    ),
  );
}

Step 3: Add Custom Styling

Use properties like padding, margin, color, borderRadius, and TextStyle to style your widget.

Container(
  padding: EdgeInsets.symmetric(horizontal: 20.0, vertical: 12.0),
  decoration: BoxDecoration(
    color: Colors.purple[100],
    borderRadius: BorderRadius.circular(25.0),
    boxShadow: [
      BoxShadow(
        color: Colors.grey.withOpacity(0.5),
        spreadRadius: 2,
        blurRadius: 5,
        offset: Offset(0, 3),
      ),
    ],
  ),
  child: Text(
    'Custom Style',
    style: TextStyle(
      color: Colors.deepPurple,
      fontSize: 18.0,
      fontWeight: FontWeight.bold,
    ),
  ),
)

Step 4: Handle User Input

Use widgets like GestureDetector, InkWell, or dedicated input widgets like TextField or Slider inside your custom widget. Manage the state and callbacks accordingly.

String _value = '';

@override
Widget build(BuildContext context) {
  return Padding(
    padding: const EdgeInsets.all(16.0),
    child: Column(
      children: [
        Text('Current value: $_value'),
        TextField(
          onChanged: (text) {
            setState(() {
              _value = text;
            });
          },
          decoration: InputDecoration(
            hintText: 'Enter value',
            border: OutlineInputBorder(),
          ),
        ),
      ],
    ),
  );
}

Step 5: Implement Callbacks

To communicate changes to the parent widget, use callbacks (functions) that are called when the input changes. These callbacks can pass the new value or other relevant data.

class CustomInputWidget extends StatefulWidget {
  final ValueChanged onChanged;

  const CustomInputWidget({Key? key, required this.onChanged}) : super(key: key);

  @override
  _CustomInputWidgetState createState() => _CustomInputWidgetState();
}

class _CustomInputWidgetState extends State<CustomInputWidget> {
  String _value = '';

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: TextField(
        onChanged: (text) {
          setState(() {
            _value = text;
          });
          widget.onChanged(text); // Calling the callback
        },
        decoration: InputDecoration(
          hintText: 'Enter value',
          border: OutlineInputBorder(),
        ),
      ),
    );
  }
}

Example: Building a Rating Widget

Let’s create a custom rating widget with stars:

import 'package:flutter/material.dart';

class RatingWidget extends StatefulWidget {
  final int initialRating;
  final ValueChanged onRatingChanged;

  const RatingWidget({Key? key, this.initialRating = 0, required this.onRatingChanged})
      : super(key: key);

  @override
  _RatingWidgetState createState() => _RatingWidgetState();
}

class _RatingWidgetState extends State<RatingWidget> {
  int _currentRating = 0;

  @override
  void initState() {
    super.initState();
    _currentRating = widget.initialRating;
  }

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: List.generate(5, (index) {
        return IconButton(
          icon: Icon(
            index < _currentRating ? Icons.star : Icons.star_border,
            color: Colors.amber,
          ),
          onPressed: () {
            setState(() {
              _currentRating = index + 1;
            });
            widget.onRatingChanged(_currentRating);
          },
        );
      }),
    );
  }
}
Usage
class ExamplePage extends StatefulWidget {
  @override
  _ExamplePageState createState() => _ExamplePageState();
}

class _ExamplePageState extends State<ExamplePage> {
  int _rating = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Custom Rating Widget')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Current Rating: $_rating', style: TextStyle(fontSize: 20)),
            RatingWidget(
              onRatingChanged: (rating) {
                setState(() {
                  _rating = rating;
                });
              },
            ),
          ],
        ),
      ),
    );
  }
}

Conclusion

Creating custom input widgets in Flutter can significantly enhance the user experience and visual appeal of your applications. By extending StatelessWidget or StatefulWidget, implementing the build method, handling user input, applying custom styles, and implementing callbacks, you can create highly tailored and interactive input widgets that meet specific needs.