import 'package:flutter/material.dart'; class ValidatedNumberInput extends StatefulWidget { final int? minValue; final int? maxValue; final ValueChanged? onValueChanged; final int? defaultValue; final ValueChanged? onValidityChanged; const ValidatedNumberInput({ super.key, this.minValue, this.maxValue, this.onValueChanged, this.defaultValue, this.onValidityChanged, }); @override _ValidatedNumberInputState createState() => _ValidatedNumberInputState(); } class _ValidatedNumberInputState extends State { late int currentValue; late TextEditingController _controller; bool isValid = true; @override void initState() { super.initState(); currentValue = widget.defaultValue ?? 0; _controller = TextEditingController(text: currentValue.toString()); _validateInput(currentValue); } @override void didUpdateWidget(covariant ValidatedNumberInput oldWidget) { super.didUpdateWidget(oldWidget); if (widget.defaultValue != null && widget.defaultValue != currentValue) { setState(() { currentValue = widget.defaultValue!; _controller.text = currentValue.toString(); _validateInput(currentValue); }); } } void _incrementValue() { setState(() { currentValue++; _controller.text = currentValue.toString(); _validateInput(currentValue); widget.onValueChanged?.call(currentValue); }); } void _decrementValue() { setState(() { currentValue--; _controller.text = currentValue.toString(); _validateInput(currentValue); widget.onValueChanged?.call(currentValue); }); } void _updateValueFromText(String value) { String sanitizedValue = value.replaceAll(RegExp(r'[^0-9]'), ''); int? newValue = int.tryParse(sanitizedValue); if (newValue != null) { setState(() { currentValue = newValue; _controller.text = currentValue.toString(); _controller.selection = TextSelection.fromPosition( TextPosition(offset: _controller.text.length), ); _validateInput(currentValue); widget.onValueChanged?.call(currentValue); }); } else { _controller.text = currentValue.toString(); _controller.selection = TextSelection.fromPosition( TextPosition(offset: _controller.text.length), ); } } void _validateInput(int value) { int minValue = widget.minValue ?? double.negativeInfinity.toInt(); int maxValue = widget.maxValue ?? double.maxFinite.toInt(); bool newIsValid = value >= minValue && value <= maxValue; if (newIsValid != isValid) { isValid = newIsValid; WidgetsBinding.instance.addPostFrameCallback((_) { widget.onValidityChanged?.call(isValid); }); } } @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8.0), decoration: BoxDecoration( color: Colors.grey[800], borderRadius: BorderRadius.circular(30), border: Border.all( color: isValid ? Colors.grey : Colors.red, width: 2.0, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( icon: const Icon(Icons.remove), onPressed: _decrementValue, constraints: const BoxConstraints(), padding: EdgeInsets.zero, ), SizedBox( width: 60, child: TextFormField( controller: _controller, textAlign: TextAlign.center, keyboardType: TextInputType.number, decoration: const InputDecoration( border: InputBorder.none, contentPadding: EdgeInsets.symmetric(vertical: 8.0), ), onChanged: (value) { _updateValueFromText(value); }, ), ), IconButton( icon: const Icon(Icons.add), onPressed: _incrementValue, constraints: const BoxConstraints(), padding: EdgeInsets.zero, ), ], ), ); } }