mre_fields 0.3.3
mre_fields: ^0.3.3 copied to clipboard
Flutter form fields that follow the language typed (auto RTL/LTR text and alignment), phone numbers validated for every country with a picker, and image paste.
mre_fields example #
A small app that shows every piece of the package. The code below is what each card in the app is built from.
cd example
flutter run # phone, desktop or web
The app bar has a light and dark switch and an English and Arabic switch. A
slider changes the field radius for the whole app through MREFieldsTheme.

1. Register the theme once #
Do this where you build your MaterialApp. Register it on both themes so dark
mode keeps the values.
const fields = MREFieldsTheme(fieldBorderRadius: 16);
final app = MaterialApp(
theme: ThemeData(extensions: const [fields]),
darkTheme: ThemeData(
brightness: Brightness.dark,
extensions: const [fields],
),
home: const HomePage(),
);
2. Text that follows its language #
MREAutoText takes every parameter of Text. The first word with a letter
decides the direction, so 123 مرحبا is right to left and (#1) Hello is left
to right.
final column = Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: const [
MREAutoText('مرحبا بالعالم'), // reads right to left
MREAutoText('Hello world'), // reads left to right
MREAutoText('مرحبا', textDirection: TextDirection.ltr), // fixed
],
);
Already have a Text? Add the direction and the alignment to it:
final text = Text(
name,
style: const TextStyle(fontSize: 20),
).autoDirection().autoAlign();
3. A field that follows the language #
final field = MRETextField(
labelText: 'Name',
hintText: 'Type in any language',
showClearButton: true,
);
Lock the direction for emails, links and numbers:
final email = MRETextField(
labelText: 'Email',
keyboardType: TextInputType.emailAddress,
textDirection: TextDirection.ltr,
);
4. Suggestions and select on focus #
final field = MRETextField(
labelText: 'City',
suggestions: const ['Cairo', 'Alexandria', 'Giza', 'القاهرة'],
maxSuggestions: 5,
);
final field = MRETextField(
labelText: 'Title',
initialValue: 'Draft',
selectTextOnFocus: true,
);
5. Validation in a form #
final form = Form(
key: formKey,
child: MRETextFormField(
labelText: 'Name',
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Enter a name';
}
return null;
},
),
);
6. Change one field #
A parameter beats the theme for that field only.
final field = MRETextField(
labelText: 'Search',
borderRadius: 28, // this field only; the theme keeps its own radius
contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
);
7. A phone number field #
It checks the number for every country, switches the country when a number with its dial code is pasted, and opens a searchable picker.

final field = MREPhoneFormField(
labelText: 'Phone number',
onChanged: (number) {
if (number.isValid) {
saveNumber(number.e164!); // '+201012345678'
}
},
);
Accept only some countries, with favorites on top:
const selection = MRECountrySelection(
include: {'SA', 'AE', 'KW', 'QA', 'BH', 'OM'},
favorites: ['SA', 'AE'],
initial: 'SA',
);
final field = MREPhoneFormField(selection: selection);
Read a number in code, with no widget:
final number = MREPhoneNumber.parse('+20 101 234 5678');
final country = number.country!.name; // 'Egypt'
final e164 = number.e164; // '+201012345678'
final nice = number.international; // '+20 10 12345678'
final valid = number.isValid; // true
Translate the picker and the error messages:
final field = MREPhoneFormField(
labelText: 'رقم الهاتف',
countryNameBuilder: (country) =>
arabicNames[country.isoCode] ?? country.name,
pickerTitle: 'اختر الدولة',
pickerSearchHint: 'ابحث عن دولة أو رمز',
);
// The error messages come from MREFieldsStrings, registered once in the theme:
const strings = MREFieldsStrings(
phoneEmpty: 'اكتب رقم الهاتف',
phoneTooShort: 'الرقم قصير',
phoneInvalid: 'رقم غير صحيح',
);
8. Paste images #

A field takes one of three behaviours: nothing, a callback with the image, or thumbnails under the field with open, remove and replace.
// Text only. This is the default.
const textOnly = MRETextField(labelText: 'Message');
// Your code gets each image. Nothing is shown.
final callback = MRETextField(
labelText: 'Message',
imagePaste: MREImageCallbackPaste(
onImagePasted: (image) => upload(image.bytes),
),
);
// Images appear inside the field.
const attachments = MRETextField(
labelText: 'Message',
imagePaste: MREImageAttachmentPaste(),
);
Read and change the images from your own code:
// Create the controller in initState and dispose it in dispose.
final field = MRETextField(
labelText: 'Message',
imagePaste: MREImageAttachmentPaste(controller: controller),
);
// Later: controller.images, controller.removeAt(0), controller.clear().
9. Translate the texts #
Every text a field shows is in MREFieldsStrings.
/// Returns the field texts for [locale]. English is the fallback.
MREFieldsStrings stringsFor(Locale locale) {
return switch (locale.languageCode) {
'ar' => const MREFieldsStrings(
clearTooltip: 'مسح',
countryPickerTitle: 'اختر الدولة',
countrySearchHint: 'ابحث عن دولة أو رمز',
noCountriesFound: 'لا توجد دول',
removeImageTooltip: 'إزالة الصورة',
replaceImageTooltip: 'استبدال الصورة',
closeViewerTooltip: 'إغلاق',
),
_ => const MREFieldsStrings(),
};
}
/// Swaps the strings whenever the app locale changes.
Widget localizedFields(BuildContext context, Widget child) {
final theme = Theme.of(context);
final fields = MREFieldsTheme.of(
context,
).copyWith(strings: stringsFor(Localizations.localeOf(context)));
return Theme(
data: theme.copyWith(
extensions: [
...theme.extensions.values.where((e) => e is! MREFieldsTheme),
fields,
],
),
child: child,
);
}
Widget localizedApp() {
return MaterialApp(
localizationsDelegates: GlobalMaterialLocalizations.delegates,
supportedLocales: const [Locale('en'), Locale('ar')],
builder: (context, child) => localizedFields(context, child!),
home: const HomePage(),
);
}
10. Use the rules without a widget #
final direction = detectTextDirection(name, fallback: TextDirection.ltr);
final strong = detectStrongTextDirection(name); // null when no letter
final side = name.autoTextAlign;
final clean = safeDisplayText(pasted); // lone surrogates become U+FFFD
const cities = ['Cairo', 'Alexandria', 'Giza', 'Luxor', 'Aswan'];
final matches = mreFilterSuggestions(cities, query, limit: 3);
final type = mreSniffImageMimeType(
upload,
); // 'image/png', 'image/jpeg', ... or null
final isPng = type == 'image/png';
