ExtensionResoft
A comprehensive toolkit of extensions, widgets, and utilities to accelerate Flutter development with less code. ExtensionResoft optimizes common tasks, enhances UI components, and provides robust architecture patterns.
Features
📝 Enhanced Text/Form Field Input
Build sophisticated form fields with extensive customization:
- CustomTextField: A highly customizable text input field with validation, dropdown support, and accessibility features.
- Fixed Height Support: Unique
heightparameter that automatically calculates internal padding to ensure consistency with other widgets (like buttons) without manual calculation. - Validation System: Built-in validation with error, warning, and info severity levels.
- Password Visibility: Configurable password visibility toggle with custom icons
- Dropdown Support: Seamless integration with dropdown menus
- Accessibility: Comprehensive semantics and validation announcements
- Performance Optimized: Efficient state management and decoration caching
- Theming Support: Automatic light/dark mode adaptation
- Feedback Widget: Customizable validation messages with icons and animations
🎚️ Interactive UI Components
Create modern, responsive user interfaces:
-
CustomRangeSlider: Highly customizable dual-handle range slider with:
- Custom theming for track, thumb, and overlay colors
- Optional haptic feedback and accessibility labels
- Discrete or continuous value selection with validation
- Animated thumb scaling with Material 3 inspired design
- Elevation shadow effects and rounded track shapes
- Value clamping and interactive feedback
-
CustomRatingBar: Advanced star rating system with:
- Support for half-ratings and custom icons
- Interactive drag and tap gestures
- Smooth animations for rating changes
- Highly customizable appearance (colors, size, spacing)
- Optional rating text display and custom item builders
🧬 Functional Programming Support
Write more expressive and type-safe code:
-
Either<L, R>: Comprehensive error handling with Left/Right pattern for failure/success scenarios
- Transformation methods (map, bimap, flatMap, fold)
- Error handling utilities (tryCatch, tryCatchAsync)
- Combining operations (zip, zipWith) and recovery mechanisms
- Future integration with async operation extensions
- EitherUtils for sequence, traverse, and firstRight operations
-
Option: Elegant optional value handling with Some/None pattern
- Core operations (fold, map, flatMap, getOrElse)
- Conversion utilities to Either types
- Null-safe programming patterns
-
Unit: Type-safe representation of absence of value in generic contexts
📦 Moved out in 2.0.0
Internet checks and network images need extra packages, so they are separate packages now and extensionresoft depends on Flutter only:
internet_status: real internet-access checks (DNS, socket, HTTP), a status stream and an offline banner (InternetConnectionChecker,InternetResult,ConnectionBanner,ConnectionBannerStyle).any_source_image: one image widget for a network URL, an asset or a file (AppImage,AppCircleImage,ImageSourceType,ImageBackground, and the'path'.img()andcircleImage()helpers).
Upgrading from 1.x
Only code that used the classes above is affected. CustomTextField, PinEntry, CustomRangeSlider, CustomRatingBar, the animations, Either/Option and the other extensions are unchanged.
-
Add the package or packages you need next to
extensionresoft:dependencies: extensionresoft: ^2.0.0 internet_status: ^1.0.0 # if you used the connectivity classes any_source_image: ^1.0.0 # if you used the image classes -
Update the imports in files that use the moved classes:
// before import 'package:extensionresoft/extensionresoft.dart'; // after (keep the line above too if the file uses other extensionresoft features) import 'package:internet_status/internet_status.dart'; // InternetConnectionChecker, InternetResult, ConnectionBanner import 'package:any_source_image/any_source_image.dart'; // AppImage, AppCircleImage, ImageBackground, 'path'.img() -
Run
flutter pub get. The moved APIs are the same, so no other code changes are needed.
connectivity_plus, rxdart and cached_network_image are no longer pulled in by extensionresoft. If your app imports them directly, add them to your own pubspec.yaml. Staying on ^1.5.1 keeps everything as it was.
🎭 Animation Utilities
Create smooth, performant animations with minimal code:
-
AnimatedFadeScale: Combined fade and scale animation with configurable timing and curves
- Customizable start/end scales
- Optional delay before animation
- Manual animation control via
valueparameter - Material transparency support
-
FadeSlideTransition: High-performance view transitions with fade and slide effects
- Smooth content transitions with queuing support
- Configurable slide direction and distance
- Key-based transition triggering
- Optimized for rapid content changes
- Predefined fast/slow duration presets
🔐 Secure PIN Authentication UI
Build secure user authentication flows:
- PinEntry Widget: Customizable PIN entry with multiple security configurations
- Visual Customization: Style input fields and keyboard components separately
- Validation Support: Custom handlers for completion and validation events
🧰 UI Extension Toolkit
Write less code for common UI patterns:
- Spacing Extensions: Clean spacer syntax with both method and getter options
- Custom Cards: Create styled cards with simple radius-based extensions
- Text Styling: Format text with fluent extensions for improved readability
- List Extensions: Calculate item margins with position-based utilities returning named records
🧠 Logic & Functional Extensions
Enhance code clarity and maintenance:
- Conditional Functions: Widget-friendly alternatives to ternary operators
- Path Extensions: Apply transformations to values with clean syntax
- Value Management: Robust value retrieval with fallback handling
Getting Started
Installation
Add the package to your pubspec.yaml:
dependencies:
extensionresoft: ^2.0.0
Then run:
flutter pub get
Basic Usage
Import the package:
import 'package:extensionresoft/extensionresoft.dart';
Usage Examples
Enhanced Text Input
// Password field with visibility toggle
CustomTextField(
margin: EdgeInsets.symmetric(vertical: 8.0),
labelText: 'Password',
isPassword: true,
passwordVisibilityConfig: PasswordVisibilityConfig(
iconSize: 22,
visibilityOnTooltip: 'Hide password',
visibilityOffTooltip: 'Show password',
),
);
// Dropdown field
CustomTextField<int>(
labelText: 'Age Group',
items: [
DropdownMenuItem(value: 1, child: Text('Under 18')),
DropdownMenuItem(value: 2, child: Text('18-25')),
DropdownMenuItem(value: 3, child: Text('26-35')),
],
onDropdownChanged: (value) {
print('Selected age group: $value');
},
);
// Styled text field with custom feedback
CustomTextField(
labelText: 'Email',
hintText: 'your@email.com',
keyboardType: TextInputType.emailAddress,
borderRadius: BorderRadius.circular(12),
fillColor: Colors.grey[100],
borderColor: Colors.blueGrey,
focusColor: Colors.blue,
errorStyle: TextStyle(color: Colors.red[800]),
warningStyle: TextStyle(color: Colors.orange[800]),
helperStyle: TextStyle(color: Colors.grey),
helperText: 'Enter a valid email address',
feedbackShowIcons: true,
feedbackPadding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
);
// Field with validation controller integration
final validationController = ValidationController();
// Standard height (48) matches many standard buttons
CustomTextField(
labelText: 'Username',
height: 50, // Ensures consistent height and handles internal padding
prefixIcon: Icon(Icons.person),
);
// Can also be used for multiple fields wrapped in a `Form`.
CustomTextField(
labelText: 'Order Number',
validationController: validationController,
validator: (value) {
if (value == null || value.isEmpty) return 'Required field';
if (!RegExp(r'^ORD-\d{4}$').hasMatch(value)) {
return 'Format: ORD-1234';
}
return null;
},
);
// Later in your code
if (validationController.validate()) {
// All fields valid
}
Range Slider
Build elegant dual-handle range selectors with Material 3 design:
// Basic range slider with custom styling
CustomRangeSlider(
values: RangeValues(20, 80),
min: 0,
max: 100,
onChanged: (values) {
setState(() {
_currentRange = values;
});
},
trackHeight: 4.0,
thumbRadius: 16.0,
enableHaptics: true,
);
// Custom themed range slider
CustomRangeSlider(
values: RangeValues(30, 70),
min: 0,
max: 100,
customTheme: CustomRangeSliderTheme(
activeTrackColor: Colors.deepPurple,
inactiveTrackColor: Colors.grey[300],
thumbColor: Colors.white,
overlayColor: Colors.deepPurple.withValues(alpha: 0.12),
thumbBorderColor: Colors.deepPurple,
),
onChanged: (values) {
print('Range: ${values.start.round()} - ${values.end.round()}');
},
);
// Discrete range slider with divisions
CustomRangeSlider(
values: RangeValues(25, 75),
min: 0,
max: 100,
divisions: 20, // Creates 20 discrete steps
trackHeight: 6.0,
thumbRadius: 20.0,
onChanged: (values) {
_updatePriceFilter(values.start, values.end);
},
onChangeEnd: (values) {
// Called when user finishes dragging
_savePricePreferences(values);
},
);
// Price range selector with custom formatting
CustomRangeSlider(
values: RangeValues(_minPrice, _maxPrice),
min: 0,
max: 1000,
divisions: 100,
customTheme: CustomRangeSliderTheme(
activeTrackColor: Colors.green[600],
thumbColor: Colors.white,
thumbBorderColor: Colors.green[600],
overlayColor: Colors.green.withValues(alpha: 0.1),
),
onChanged: (values) {
setState(() {
_minPrice = values.start;
_maxPrice = values.end;
});
},
semanticFormatter: (value) => '\$${value.round()}',
enableHaptics: true,
);
// Safe constructor with automatic value clamping
CustomRangeSlider.safe(
values: RangeValues(-10, 150), // Values outside bounds
min: 0,
max: 100,
// Automatically clamps to RangeValues(0, 100)
customTheme: CustomRangeSliderTheme(
activeTrackColor: Colors.blue[700],
inactiveTrackColor: Colors.blue[100],
thumbColor: Colors.white,
thumbBorderColor: Colors.blue[700],
),
onChanged: (values) {
// Handle validated range changes
},
);
// Age range selector with custom feedback
RangeValues _ageRange = RangeValues(18, 65);
Column(
children: [
Text(
'Age Range: ${_ageRange.start.round()} - ${_ageRange.end.round()} years',
style: Theme.of(context).textTheme.titleMedium,
),
16.spY, // Using ExtensionResoft spacing
CustomRangeSlider(
values: _ageRange,
min: 16,
max: 100,
divisions: 84, // One division per year
trackHeight: 5.0,
thumbRadius: 18.0,
customTheme: CustomRangeSliderTheme(
activeTrackColor: Colors.orange[600],
inactiveTrackColor: Colors.grey[300],
thumbColor: Colors.white,
overlayColor: Colors.orange.withValues(alpha: 0.15),
thumbBorderColor: Colors.orange[600],
),
onChanged: (values) {
setState(() => _ageRange = values);
},
semanticFormatter: (value) => '${value.round()} years old',
),
],
);
// Performance monitoring range (0-100%)
CustomRangeSlider(
values: RangeValues(_lowThreshold, _highThreshold),
min: 0.0,
max: 100.0,
divisions: 100,
trackHeight: 3.0,
thumbRadius: 14.0,
customTheme: CustomRangeSliderTheme(
activeTrackColor: _getPerformanceColor(),
thumbColor: Colors.white,
thumbBorderColor: _getPerformanceColor(),
overlayColor: _getPerformanceColor().withValues(alpha: 0.1),
),
onChanged: (values) {
setState(() {
_lowThreshold = values.start;
_highThreshold = values.end;
});
_updatePerformanceThresholds(values);
},
semanticFormatter: (value) => '${value.round()}% performance',
enableHaptics: false, // Disable for frequent updates
);
// Time range selector (hours)
CustomRangeSlider(
values: RangeValues(_startHour.toDouble(), _endHour.toDouble()),
min: 0,
max: 23,
divisions: 23,
trackHeight: 4.0,
thumbRadius: 16.0,
customTheme: CustomRangeSliderTheme(
activeTrackColor: Colors.indigo[600],
inactiveTrackColor: Colors.indigo[100],
thumbColor: Colors.white,
thumbBorderColor: Colors.indigo[600],
),
onChanged: (values) {
setState(() {
_startHour = values.start.round();
_endHour = values.end.round();
});
},
semanticFormatter: (value) {
final hour = value.round();
return '${hour.toString().padLeft(2, '0')}:00';
},
);
Rating Bar
Create interactive and animated rating systems:
// Standard star rating
CustomRatingBar(
initialRating: 3.5,
onRatingChanged: (rating) => print('Rating: $rating'),
);
// Highly customized rating bar
CustomRatingBar(
maxRating: 10,
iconSize: 32,
activeColor: Colors.deepOrange,
inactiveColor: Colors.grey[300],
allowHalfRating: true,
showRatingText: true,
activeIcon: Icons.favorite,
inactiveIcon: Icons.favorite_border,
onRatingChanged: (rating) => _updateUserPreference(rating),
);
// Read-only display
CustomRatingBar(
initialRating: 4.8,
isInteractive: false,
allowHalfRating: true,
activeColor: Colors.amber,
);
Functional Programming Support
Handle errors and optional values with type-safe functional patterns:
// Either for error handling
Either<String, int> parseNumber(String input) {
try {
return Either.right(int.parse(input));
} catch (e) {
return Either.left('Invalid number: $input');
}
}
// Chain operations with flatMap
final result = parseNumber('42')
.flatMap((n) => n > 0 ? Either.right(n * 2) : Either.left('Negative'))
.fold((error) => 'Error: $error', (value) => 'Result: $value');
// Async error handling
final apiResult = await Either.tryCatchAsync<String, UserData>(
(error, stackTrace) => 'API Error: ${error.toString()}',
() => fetchUserData(userId),
);
// Option for nullable values
Option<User> findUser(String id) {
final user = users.firstWhereOrNull((u) => u.id == id);
return user != null ? Option.some(user) : Option.none();
}
// Transform optional values
final greeting = findUser('123')
.map((user) => 'Hello, ${user.name}!')
.getOrElse(() => 'User not found');
// Convert between types
final userEither = findUser('123')
.toEither(() => 'User not found');
Animation Utilities
// Basic fade and scale animation
AnimatedFadeScale(
duration: Duration(milliseconds: 500),
child: Container(
width: 200,
height: 200,
color: Colors.blue,
child: Center(child: Text('Hello!')),
),
);
// Staggered animations with delay
Column(
children: [
AnimatedFadeScale(
delay: Duration(milliseconds: 100),
child: Card(child: /*...*/),
),
AnimatedFadeScale(
delay: Duration(milliseconds: 200),
child: Card(child: /*...*/),
),
AnimatedFadeScale(
delay: Duration(milliseconds: 300),
child: Card(child: /*...*/),
),
],
);
// Manual animation control
double _animationValue = 0.0;
AnimatedFadeScale(
value: _animationValue,
child: FloatingActionButton(
onPressed: () {
setState(() {
_animationValue = _animationValue == 0.0 ? 1.0 : 0.0;
});
},
),
);
// FadeSlideTransition with key-based triggering
final _transitionKey = UniqueKey();
FadeSlideTransition(
transitionKey: _transitionKey,
duration: Duration(milliseconds: 400),
initialSlideOffset: Offset(0, 0.1), // Slide up from bottom
child: _buildContentWidget(),
);
// Later when content needs to change
setState(() {
_transitionKey = UniqueKey(); // Triggers new transition
});
// Using predefined durations
FadeSlideTransition.fast(
child: NotificationBadge(count: 5),
);
FadeSlideTransition.slow(
child: HeroImage(url: imageUrl),
);
// Transition with completion callback
FadeSlideTransition(
child: ProfileHeader(user: user),
onTransitionComplete: () {
print('Transition finished!');
},
);
PIN Authentication
PinEntry(
pinLength: 6,
onInputComplete: (pin) {
// Handle PIN validation
},
inputFieldConfiguration: InputFieldConfiguration(
obscureText: true,
fieldFillColor: Colors.grey[200],
focusedBorderColor: Colors.blue,
),
keyboardConfiguration: KeyboardConfiguration(
keyBackgroundColor: Colors.white,
keyTextStyle: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
)
UI Extensions
// Spacing utilities
16.spaceX() // SizedBox(width: 16)
24.spaceY() // SizedBox(height: 24)
32.spaceXY() // SizedBox(width: 32, height: 32)
// Getters for even cleaner code
20.spX // SizedBox(width: 20)
30.spY // SizedBox(height: 30)
// Custom card with styling
12.radius(
child: Padding(
padding: EdgeInsets.all(16),
child: Text('Rounded Card Example'),
),
elevation: 2,
color: Colors.blue[50],
strokeColor: Colors.black12,
)
// Text styling
'Hello Flutter'.edit(
textStyle: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.teal,
),
textAlign: TextAlign.center,
)
Logic & Functional Extensions
// Path extension to transform values
final doubled = 16.p((n) => n * 2); // 32
// Widget-friendly conditional logic
final result = condition(isActive, 'Active', 'Inactive');
// Function-based conditionals (lazy evaluation)
final message = conditionFunction(
hasPermission,
() => 'Access granted',
() => 'Access denied: ${getErrorMessage()}',
);
// Safe value retrieval with fallback
final displayName = get(user.name, 'Guest User');
Advanced Usage
See the API Reference for comprehensive documentation of all available extensions and utilities.
Migration from 1.0.0 to 1.1.0
While version 1.1.0 maintains backward compatibility, we recommend the following changes:
- Replace
assetFallbackwithfallbackImageparameter in image widgets for improved naming consistency - Take advantage of the new
Fileobject support in image widgets - Utilize new
borderRadiusandbackgroundColorparameters for enhanced customization
Screenshots
(None applicable in this current release)
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the Project
- Create your Feature Branch (
git checkout -b feature/amazing-feature) - Commit your Changes (
git commit -m 'Add some amazing feature') - Push to the Branch (
git push origin feature/amazing-feature) - Open a Pull Request
License
This project is licensed under the BSD 3-Clause License - see the LICENSE file for details.