extensionresoft
透過可重複使用的組件、實用工具和擴充功能,提升您的 Flutter 開發效率,實現高階影像處理、安全 PIN 輸入、網路連線狀態檢查等功能。
一組用於加速開發的 Dart/Flutter 擴充函數和輔助函數。此項目旨在協助開發者撰寫更簡潔但功能強大的程式碼。它提升了程式碼的可重複使用性,並使編碼閱讀更加容易。
{"sdk":"flutter"}^7.3.1^0.28.0^3.4.1{"sdk":"flutter"}^6.0.0以下為英文專案原文快照,最新內容請造訪 GitHub。
pub package pub points Pub Monthly Downloads likes Star on Github Awesome Flutter License: BSD
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.
Build sophisticated form fields with extensive customization:
height parameter that automatically calculates internal padding to ensure consistency with other widgets (like buttons) without manual calculation.Create modern, responsive user interfaces:
CustomRangeSlider: Highly customizable dual-handle range slider with:
CustomRatingBar: Advanced star rating system with:
Write more expressive and type-safe code:
Either<L, R>: Comprehensive error handling with Left/Right pattern for failure/success scenarios
Option: Elegant optional value handling with Some/None pattern
Unit: Type-safe representation of absence of value in generic contexts
Detect and respond to network changes reliably:
Create robust image components with minimal effort:
Create smooth, performant animations with minimal code:
AnimatedFadeScale: Combined fade and scale animation with configurable timing and curves
value parameterFadeSlideTransition: High-performance view transitions with fade and slide effects
Build secure user authentication flows:
Write less code for common UI patterns:
Enhance code clarity and maintenance:
Add the package to your pubspec.yaml:
dependencies:
extensionresoft: ^1.5.1
Then run:
flutter pub get
Import the package:
import 'package:extensionresoft/extensionresoft.dart';
// 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
}
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';
},
);
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,
);
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');
// Create a checker instance
final connectionChecker = InternetConnectionChecker();
// Check current connection status
final isConnected = await connectionChecker.isInternetConnected;
print('Internet connected: $isConnected');
// Listen to detailed connectivity changes
connectionChecker.onInternetConnectivityChanged.listen((result) {
print('Connection Status: ${result.hasInternetAccess}');
print('Connection Type: ${result.connectionType}');
// Update UI based on connectivity
if (result.hasInternetAccess) {
// Load online data
} else {
// Show offline mode UI
}
});
// Simplified boolean stream for quick status checks
connectionChecker.onIsInternetConnected.listen((isConnected) {
print('Internet status changed: $isConnected');
});
// Network image with fallback and border radius
AppImage(
'https://example.com/profile.jpg',
width: 150,
height: 150,
fit: BoxFit.cover,
borderRadius: BorderRadius.circular(8),
backgroundColor: Colors.grey[200],
errorWidget: Icon(Icons.broken_image),
fallbackImage: 'assets/default_image.png',
)
// Circle avatar from network or asset
AppCircleImage(
'assets/profile_photo.jpg', // or network URL or File object
radius: 40,
fit: BoxFit.cover,
placeholder: CircularProgressIndicator(strokeWidth: 2),
errorWidget: Icon(Icons.person),
)
// As decoration image
Container(
decoration: BoxDecoration(
image: AppImage('https://example.com/bg.jpg')
.toDecorationImage(
decorationFit: BoxFit.cover,
fallbackImage: 'assets/default_bg.png',
),
),
)
// Path-based image extensions
'assets/image.png'.img(width: 100, height: 100)
'assets/avatar.png'.circleImage(fit: BoxFit.cover)
// 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!');
},
);
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),
),
)
// 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,
)
// 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');
See the API Reference for comprehensive documentation of all available extensions and utilities.
While version 1.1.0 maintains backward compatibility, we recommend the following changes:
assetFallback with fallbackImage parameter in image widgets for improved naming consistencyFile object support in image widgetsborderRadius and backgroundColor parameters for enhanced customization(None applicable in this current release)
Contributions are welcome! Please feel free to submit a Pull Request.
git checkout -b feature/amazing-feature)git commit -m 'Add some amazing feature')git push origin feature/amazing-feature)This project is licensed under the BSD 3-Clause License - see the LICENSE file for details.