I have created custom MobileField but Validator is not working for mobile field, Globalkey is defined in the cubit.
class MobileNumberField extends FormField<PhoneNumberWithCode?> {
final String label;
final TextEditingController? controller;
final ValueChanged<PhoneNumberWithCode?>? onChanged;
final Color? leadingBackgroundColor;
final String hint;
static bool _fakeValidator = true;
MobileNumberField({
super.key,
required this.label,
required this.hint,
this.controller,
this.leadingBackgroundColor = const Color(0xFFFCFDFD),
this.onChanged,
super.enabled,
super.initialValue,
}) : super(
autovalidateMode: AutovalidateMode.disabled,
validator: (phoneNumberWithCode) {
if (_fakeValidator) return null;
final currentContext = AppRouter.navigatorKey.currentContext;
final effectiveLabel = label.replaceAll("*", "");
if (currentContext != null) {
if (phoneNumberWithCode?.code == null ||
phoneNumberWithCode?.number == null ||
phoneNumberWithCode?.code?.trim().isEmpty == true ||
phoneNumberWithCode?.number?.trim().isEmpty == true) {
return currentContext.localizations.emptyValidationError(
effectiveLabel,
);
} else if (!_validatePhoneNumber(phoneNumberWithCode!.number!)) {
return currentContext.localizations.invalidValidationError(
effectiveLabel,
);
}
}
return null;
},
builder: (mobileNumberField) {
final state = mobileNumberField as _MobileNumberFieldState;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
label,
style: CoreTextStyle.mRegular,
),
4.height,
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Builder(
builder: (context) {
return GestureDetector(
onTap: enabled
? () async {
final RenderBox button = context
.findRenderObject()! as RenderBox;
final RenderBox overlay =
Navigator.of(context)
.overlay!
.context
.findRenderObject()! as RenderBox;
final offset =
Offset(0, 4 + button.size.height);
final RelativeRect position =
RelativeRect.fromRect(
Rect.fromPoints(
button.localToGlobal(
offset,
ancestor: overlay,
),
button.localToGlobal(
button.size.bottomRight(Offset.zero) +
offset,
ancestor: overlay,
),
),
Offset.zero & overlay.size,
);
final country = await showMenu<Country>(
context: context,
position: position,
color: CoreColor.neutral25,
surfaceTintColor: Colors.transparent,
constraints:
const BoxConstraints.tightFor(
height: 180,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
side: const BorderSide(
color: CoreColor.neutral200,
),
),
elevation: 4,
items: countries
.map(
(e) => PopupMenuItem(
value: e,
height: 28,
padding: const EdgeInsets.all(12),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
CountryFlag.fromCountryCode(
e.iso!,
height: 20,
width: 30,
),
8.width,
Text("+${e.code}"),
],
),
),
)
.toList(),
);
if (country == null) return;
state.didChange(
state.value?.copyWith(
code: country.code,
iso: country.iso,
),
);
onChanged?.call(state.value);
}
: null,
child: CustomPaint(
painter: _MobileCountryCodeBorderPainter(
borderWidth: 1,
borderColor: CoreColor.neutral200,
insetShadow: true,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(4),
bottomLeft: Radius.circular(4),
),
fillColor: CoreColor.neutral0,
),
child: Container(
height: 40,
padding: const EdgeInsets.all(10),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CountryFlag.fromCountryCode(
state.value?.iso ??
countries.first.iso ??
"IN",
height: 20,
width: 30,
),
6.width,
Text(
"+${state.value?.code ?? countries.first.code!}",
style: CoreTextStyle.mRegular.copyWith(
color: CoreColor.neutral500,
),
),
10.width,
const PhosphorIcon(
PhosphorIconsRegular.caretDown,
color: CoreColor.neutral500,
size: 18,
),
],
),
),
),
);
},
),
Expanded(
child: TextField(
enabled: enabled,
controller: state._effectiveController,
style: CoreTextStyle.mRegular.copyWith(
color: CoreColor.neutral900,
),
decoration: InputDecoration(
hintStyle: CoreTextStyle.mRegular
.copyWith(color: CoreColor.neutral500),
hintText: hint,
isDense: true,
constraints: const BoxConstraints(minHeight: 40),
border: CoreInputBorder(
fillColor: enabled
? CoreColor.neutral0
: CoreColor.neutral100,
borderColor: state.hasError
? CoreColor.dangerMain
: CoreColor.neutral200,
),
focusedBorder: CoreInputBorder(
fillColor: enabled
? CoreColor.neutral0
: CoreColor.neutral100,
borderColor: state.hasError
? CoreColor.dangerFocus
: CoreColor.neutral200,
borderWidth: 3,
),
),
onChanged: (mobile) {
_fakeValidator = true;
state.didChange(
state.value?.copyWith(number: mobile.trim()),
);
state.validate();
onChanged?.call(state.value);
_fakeValidator = false;
},
onTapOutside: (_) => dismissKeyboard(),
autofillHints: const [
AutofillHints.telephoneNumberLocal,
],
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
),
),
],
),
),
if (state.hasError) ...{
Padding(
padding: const EdgeInsets.only(left: 10, top: 4),
child: Text(
state.errorText!,
style: CoreTextStyle.sRegular.copyWith(
color: CoreColor.dangerMain,
),
),
),
},
],
);
},
);
@override
FormFieldState<PhoneNumberWithCode?> createState() =>
_MobileNumberFieldState();
static bool _validatePhoneNumber(String input) {
return input.isNotEmpty &&
RegExp(r'^d+$').hasMatch(input) &&
input.length >= 7 &&
input.length <= 15;
}
}
class _MobileNumberFieldState extends FormFieldState<PhoneNumberWithCode?> {
/// The currently selected country code.
Country? selectedCountryCode;
TextEditingController? _controller;
TextEditingController get _effectiveController =>
_mobileNumberField.controller ?? _controller!;
MobileNumberField get _mobileNumberField => super.widget as MobileNumberField;
@override
void initState() {
_setPhoneNumberWithCode();
_controller = TextEditingController();
super.initState();
}
@override
void didUpdateWidget(covariant FormField<PhoneNumberWithCode?> oldWidget) {
if (widget.initialValue != oldWidget.initialValue &&
widget.initialValue != null) {
_effectiveController.text = widget.initialValue!.number!;
setValue(_mobileNumberField.initialValue);
}
super.didUpdateWidget(oldWidget);
}
void _setPhoneNumberWithCode() {
final countryCode = context.read<GlobalCubit>().state.ipData?.countryCode ??
WidgetsBinding.instance.platformDispatcher.locale.countryCode;
selectedCountryCode = countries.firstWhere(
(element) => element.iso?.toLowerCase() == countryCode?.toLowerCase(),
orElse: () => countries.first,
);
setValue(
PhoneNumberWithCode(
code: selectedCountryCode?.code,
),
);
}
}
class _MobileCountryCodeBorderPainter extends CustomPainter {
final double borderWidth;
final Color borderColor;
final Color? fillColor;
final bool insetShadow;
final BorderRadius borderRadius;
_MobileCountryCodeBorderPainter({
required this.borderWidth,
required this.borderColor,
this.fillColor,
required this.insetShadow,
required this.borderRadius,
});
@override
void paint(Canvas canvas, Size size) {
final Rect rect = Offset.zero & size;
if (fillColor != null) {
final fillPaint = Paint()
..color = fillColor!
..style = PaintingStyle.fill;
canvas.drawRRect(
RRect.fromRectAndCorners(
rect,
topLeft: borderRadius.topLeft,
topRight: borderRadius.topRight,
bottomLeft: borderRadius.bottomLeft,
bottomRight: borderRadius.bottomRight,
),
fillPaint,
);
}
// Draw the inset shadow
if (insetShadow) {
final shadowPaint = Paint()
..color =
const Color(0x1F101828).withOpacity(0.12) // Shadow color with alpha
..maskFilter =
const MaskFilter.blur(BlurStyle.normal, 2.0); // Shadow blur
// Top shadow
canvas.drawRect(
Rect.fromLTRB(
rect.left + 4, // Start 1px inside the left edge
rect.top + 1, // Start 1px inside the top edge
rect.right - 4, // End 1px inside the right edge
rect.top + 3, // Small shadow area height
),
shadowPaint,
);
// Left shadow
canvas.drawRect(
Rect.fromLTRB(
rect.left + 1, // Start 1px inside the left edge
rect.top + 1, // Start 1px inside the top edge
rect.left + 3, // Small shadow area width
rect.bottom - 1, // End 1px inside the bottom edge
),
shadowPaint,
);
}
// Paint for the borders
final Paint borderPaint = Paint()
..color = borderColor
..style = PaintingStyle.stroke
..strokeWidth = borderWidth;
// Create a path for the border, skipping the right border
final Path borderPath = Path();
final Rect borderRect = rect.deflate(borderWidth / 2);
// Start from the bottom left corner
borderPath.moveTo(
borderRect.right + borderWidth,
rect.bottom - borderWidth / 2,
);
// Bottom border
borderPath.lineTo(
borderRect.left + borderRadius.bottomLeft.y,
borderRect.bottom,
);
// Bottom left corner
borderPath.arcToPoint(
Offset(borderRect.left, borderRect.bottom - borderRadius.bottomLeft.y),
radius: Radius.circular(borderRadius.bottomLeft.x),
);
// Left border
borderPath.lineTo(borderRect.left, borderRect.top + borderRadius.topLeft.y);
// Top left corner
borderPath.arcToPoint(
Offset(borderRect.left + borderRadius.topLeft.x, borderRect.top),
radius: Radius.circular(borderRadius.topLeft.x),
);
// Top border
borderPath.lineTo(
borderRect.right + borderWidth / 2,
borderRect.top,
);
// Top right corner
borderPath.arcToPoint(
Offset(borderRect.right, borderRect.top + borderRadius.topRight.y),
radius: Radius.circular(borderRadius.topRight.x),
);
// Skip the right border by moving the path down without drawing
borderPath.moveTo(
borderRect.right,
borderRect.bottom - borderRadius.bottomRight.y,
);
// Draw the path on the canvas
canvas.drawPath(borderPath, borderPaint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}
PhoneNumber code modal class
part of '../models.dart';
class PhoneNumberWithCode extends Equatable {
final String? number;
final String? code;
final String? iso;
const PhoneNumberWithCode({
this.number,
this.code,
this.iso,
});
PhoneNumberWithCode copyWith({
String? number,
String? code,
String? iso,
}) =>
PhoneNumberWithCode(
number: number ?? this.number,
code: code ?? this.code,
iso: iso ?? this.iso,
);
factory PhoneNumberWithCode.fromRawJson(String str) =>
PhoneNumberWithCode.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory PhoneNumberWithCode.fromJson(Map<String, dynamic> json) =>
PhoneNumberWithCode(
number: json["number"],
code: json["code"],
iso: json["iso"],
);
Map<String, dynamic> toJson() => {
"number": number,
"code": code,
"iso": iso,
};
String get formattedString =>
MobileNumberFormatters.formatNumber("$code$number");
@override
List<Object?> get props => [code, number, iso];
}
This is how i’m using FormWidget
Form(
key: loginCubit.loginFormKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
24.height,
Text(
context.localizations.login.capitalizeFirstLetter,
style: CoreTextStyle.headingS,
),
16.height,
MobileNumberField(
label: context.localizations.mobileNumber.mandatoryField,
hint: context.localizations.mobileNumberHint,
onChanged: (phoneNumberWithCode) {
if (phoneNumberWithCode != null) {
loginCubit.addNumber(phoneNumberWithCode);
}
},
),
16.height,
PasswordField(
label: context.localizations.password
.capitalizeFirstLetter.mandatoryField,
controller: loginCubit.passwordController,
validator: (text) => Validators.emptyValidator(
context,
text,
context.localizations.password.capitalizeFirstLetter,
),
hint: context.localizations.loginPasswordHint,
autofillHints: const [AutofillHints.password],
),
24.height,
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
BlocBuilder<LoginCubit, LoginState>(
builder: (context, state) {
return CoreCheckboxTile(
text: context.localizations.keepMeSignedIn,
value: state.shouldSaveCredentials,
onChanged: (_) {
context
.read<LoginCubit>()
.toggleShouldSaveCredentials();
},
textStyle: CoreTextStyle.mRegular,
);
},
),
CoreTextButton(
text: context.localizations.forgotPassword,
onTap: () {
context.push(Routes.FORGOT_PASSWORD);
},
),
],
),
24.height,
CorePrimaryButton(
buttonText:
context.localizations.login.capitalizeFirstLetter,
onPressed: loginCubit.handleLogin,
),
12.height,
CoreSilentButton(
text: context.localizations.dontHaveAccountRegister,
onTap: () {
context.push(Routes.REGISTER);
},
),
],
),
),