Swiftify brings the elegance of SwiftUI's modifier syntax to Flutter! This package introduces a streamlined way to build Flutter UIs by extending widgets with chainable, modifier-style methods. Say goodbye to deeply nested widget trees and hello to clean, readable, and maintainable code.
- SwiftUI-like Modifiers β Apply common widget properties (padding, text styles, shadows, transforms, etc.) with intuitive
.modifier()methods. - Chainable Methods β Combine multiple modifications into a single statement for a clean, declarative style.
- Cleaner Codebase β Reduce widget nesting and improve code readability.
- Comprehensive Coverage β 27 extension files covering sizing, layout, decoration, animation, navigation, theming, accessibility, and more.
- Debug-Friendly Assertions β Detailed assert messages catch invalid parameters early in development.
Add the package to your pubspec.yaml:
dependencies:
swiftify: ^1.5.0Run flutter pub get, then import:
import 'package:swiftify/swiftify.dart';Center(
child: Opacity(
opacity: 0.8,
child: Container(
decoration: BoxDecoration(
boxShadow: [BoxShadow(blurRadius: 10)],
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: EdgeInsets.all(16),
child: Text("Hello"),
),
),
),
)Text("Hello")
.padding()
.shadow(blurRadius: 10)
.borderRadius(radius: 12)
.opacity(0.8)
.center()Extension on Widget
| Modifier | Description |
|---|---|
.frame(width:, height:) |
Wraps in SizedBox with given dimensions |
.expanded(flex:) |
Wraps in Expanded β must be inside Row/Column/Flex |
.flexible(flex:, fit:) |
Wraps in Flexible β must be inside Row/Column/Flex |
.intrinsicWidth(stepWidth:, stepHeight:) |
Sizes to intrinsic width |
.intrinsicHeight() |
Sizes to intrinsic height |
.aspectRatio(ratio) |
Enforces an aspect ratio |
.fittedBox(fit:, alignment:) |
Scales child within parent |
.constrainedBox(minWidth:, maxWidth:, minHeight:, maxHeight:) |
Applies box constraints |
.unconstrained(constrainedAxis:, alignment:) |
Removes parent constraints |
.limitedBox(maxWidth:, maxHeight:) |
Limits size in unbounded layouts |
Icon(Icons.star)
.frame(width: 100, height: 100)
.expanded(flex: 2)
.aspectRatio(16 / 9)Extension on Widget
| Modifier | Description |
|---|---|
.padding(paddingStyle:, padding:, top:, bottom:, left:, right:, vertical:, horizontal:) |
Adds padding with flexible style options |
Supports PaddingStyle.all, PaddingStyle.only, PaddingStyle.symmetric, and PaddingStyle.fromViewPadding.
Text("Hello").padding() // default 8.0 all
Text("Hello").padding(paddingStyle: PaddingStyle.only, top: 16) // top-only
Text("Hello").padding(paddingStyle: PaddingStyle.symmetric, vertical: 12, horizontal: 24)Extension on Widget
| Modifier | Description |
|---|---|
.center() |
Wraps in Center widget |
Text("Centered").center()Extension on Widget
| Modifier | Description |
|---|---|
.align(alignment) |
Wraps in Align with given alignment |
.positioned(top:, bottom:, left:, right:, width:, height:) |
Wraps in Positioned for use inside Stack |
.positionedFill(top:, bottom:, left:, right:) |
Fills a Stack with optional insets |
.centerLeft() |
Aligns to center-left |
.centerRight() |
Aligns to center-right |
Text("Top Left").align(Alignment.topLeft)
widget.positioned(top: 10, left: 20)
widget.positionedFill()Extension on Widget
| Modifier | Description |
|---|---|
.opacity(value) |
Sets opacity (0.0β1.0) |
.visible(isVisible, maintainSize:, ...) |
Controls visibility with layout options |
.offstage(offstage) |
Removes from layout without removing from tree |
.ignorePointer(ignoring:) |
Lets pointer events pass through |
.absorbPointer(absorbing:) |
Absorbs pointer events, blocking those below |
Text("Faded").opacity(0.5)
widget.visible(showWidget, maintainSize: true)
button.ignorePointer(ignoring: isLoading)Extension on Widget
| Modifier | Description |
|---|---|
.backgroundColor(color) |
Adds a background color |
.container(color:, decoration:, padding:, margin:, alignment:, width:, height:) |
Full Container wrapper |
Text("Hello").backgroundColor(Colors.blue)
widget.container(padding: EdgeInsets.all(16), margin: EdgeInsets.all(8))Extension on Widget
| Modifier | Description |
|---|---|
.shadow(color:, blurRadius:, spreadRadius:, offset:, borderRadius:) |
Adds a box shadow |
.border(color:, width:, style:, radius:) |
Adds a border |
.gradient(gradient, borderRadius:) |
Applies a gradient background |
.shape(boxShape, color:) |
Applies a box shape (circle, rectangle) |
.decorated(BoxDecoration) |
Full decoration control |
.foregroundDecoration(Decoration) |
Overlay decoration |
widget.shadow(blurRadius: 10, offset: Offset(0, 4))
widget.border(color: Colors.grey, width: 2, radius: 12)
widget.gradient(LinearGradient(colors: [Colors.blue, Colors.purple]))
Image.asset('avatar.png').shape(BoxShape.circle)Extension on Widget
| Modifier | Description |
|---|---|
.borderRadius(radius:, topLeft:, topRight:, bottomLeft:, bottomRight:) |
Clips with rounded corners |
widget.borderRadius() // default 25
widget.borderRadius(radius: 16) // uniform
widget.borderRadius(topLeft: 20, topRight: 20) // per-cornerExtension on Widget
| Modifier | Description |
|---|---|
.clipOval(clipper:, clipBehavior:) |
Clips to oval/ellipse |
.clipRect(clipper:, clipBehavior:) |
Clips to rectangle |
.clipPath(clipper:, clipBehavior:) |
Clips to custom path |
.clipCircle() |
Convenience for circular clip |
.clipRoundedRect(radius:, borderRadius:, clipBehavior:) |
Clips to rounded rectangle |
Image.network(url).clipCircle()
widget.clipRoundedRect(radius: 16)Extension on Widget
| Modifier | Description |
|---|---|
.rotate(angle:, alignment:, origin:) |
Rotates by radians |
.rotateDegrees(degrees, alignment:, origin:) |
Rotates by degrees |
.scale(all:, scaleX:, scaleY:, alignment:, origin:) |
Scales uniformly or per-axis |
.translate(offset:, transformHitTests:) |
Translates position |
.flip(flipX:, flipY:, alignment:) |
Mirrors horizontally/vertically |
Icon(Icons.refresh).rotateDegrees(45)
widget.scale(all: 1.5)
Icon(Icons.arrow_back).flip(flipX: true)Extension on Widget
| Modifier | Description |
|---|---|
.scrollable(direction:, reverse:, physics:, padding:, controller:) |
Wraps in SingleChildScrollView |
.scrollbar(controller:, thumbVisibility:, thickness:, radius:) |
Adds a Scrollbar |
Column(children: [...]).scrollable()
ListView(...).scrollbar(thumbVisibility: true)Extension on Widget
| Modifier | Description |
|---|---|
.respectSafeArea() |
Wraps in SafeArea |
Image.asset('bg.png').respectSafeArea()Extension on Widget
| Modifier | Description |
|---|---|
.onGesture(onTap:, onDoubleTap:, onLongPress:, behavior:) |
Adds gesture detection |
widget.onGesture(
onTap: () => print("tapped"),
onLongPress: () => print("long press"),
)Extension on Widget
| Modifier | Description |
|---|---|
.card(color:, elevation:, shape:, margin:, ...) |
Wraps in Card |
.material(type:, elevation:, color:, shape:, borderRadius:, ...) |
Wraps in Material |
.inkWell(onTap:, splashColor:, borderRadius:, ...) |
Adds Material ripple via InkWell |
ListTile(title: Text("Item")).card(elevation: 4)
widget.inkWell(onTap: () => print("tap"), borderRadius: BorderRadius.circular(12))Extension on Text
| Modifier | Description |
|---|---|
.fontSize(size) |
Sets font size |
.fontWeight(weight) |
Sets font weight |
.fontFamily(family) |
Sets font family |
.color(color) |
Sets text color |
.italic() |
Makes text italic |
.bold() |
Makes text bold |
.align(textAlign) |
Sets text alignment |
Text("Hello")
.fontSize(24)
.bold()
.color(Colors.blue)
.italic()
.align(TextAlign.center)Extension on Text
Apply theme-based text styles directly:
| Modifier | TextTheme Style |
|---|---|
.displayLarge(context) |
textTheme.displayLarge |
.displayMedium(context) |
textTheme.displayMedium |
.displaySmall(context) |
textTheme.displaySmall |
.headlineLarge(context) |
textTheme.headlineLarge |
.headlineMedium(context) |
textTheme.headlineMedium |
.headlineSmall(context) |
textTheme.headlineSmall |
.titleLarge(context) |
textTheme.titleLarge |
.titleMedium(context) |
textTheme.titleMedium |
.titleSmall(context) |
textTheme.titleSmall |
.bodyLarge(context) |
textTheme.bodyLarge |
.bodyMedium(context) |
textTheme.bodyMedium |
.bodySmall(context) |
textTheme.bodySmall |
.labelLarge(context) |
textTheme.labelLarge |
.labelMedium(context) |
textTheme.labelMedium |
.labelSmall(context) |
textTheme.labelSmall |
Text("Page Title").headlineLarge(context).bold()
Text("Body content").bodyMedium(context).color(Colors.grey)Extension on Widget
| Modifier | Description |
|---|---|
.hero(tag:, ...) |
Wraps in Hero for shared element transitions |
.animatedSwitcher(duration:, ...) |
Wraps in AnimatedSwitcher |
.animatedOpacity(opacity:, duration:, curve:, ...) |
Animated opacity changes |
.animatedScale(scale:, duration:, curve:, ...) |
Animated scale changes |
.animatedSlide(offset:, duration:, curve:, ...) |
Animated position changes |
.animatedRotation(turns:, duration:, curve:, ...) |
Animated rotation changes |
.animatedContainer(duration:, padding:, decoration:, ...) |
Animated container properties |
.animatedPadding(padding:, duration:, curve:, ...) |
Animated padding changes |
Image.asset('photo.png').hero(tag: 'photo-1')
widget.animatedOpacity(
opacity: isVisible ? 1.0 : 0.0,
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
)
Icon(Icons.refresh).animatedRotation(turns: isLoading ? 1.0 : 0.0)Extension on Widget
| Modifier | Description |
|---|---|
.onTapNavigate(context, page:, fullscreenDialog:) |
Pushes a page on tap |
.onTapPushNamed(context, routeName:, arguments:) |
Pushes a named route on tap |
.showAsDialog(context, dialog:, ...) |
Shows a dialog on tap |
.showAsBottomSheet(context, builder:, ...) |
Shows a modal bottom sheet on tap |
.onTapPop(context, result:) |
Pops the current route on tap |
ListTile(title: Text("Settings"))
.onTapNavigate(context, page: SettingsPage())
Icon(Icons.info).showAsDialog(
context,
dialog: AlertDialog(title: Text("Info"), content: Text("Details")),
)
Icon(Icons.arrow_back).onTapPop(context)Extension on Widget
| Modifier | Description |
|---|---|
.withTheme(ThemeData) |
Overrides theme for subtree |
.overrideTheme((current) => ...) |
Copies and modifies current theme |
widget.withTheme(ThemeData.dark())
widget.overrideTheme((theme) => theme.copyWith(primaryColor: Colors.purple))Extension on Widget
| Modifier | Description |
|---|---|
.overlay(child:, alignment:, fit:, ...) |
Places widget on top via Stack |
.stackBackground(child:, alignment:, fit:, ...) |
Places widget behind via Stack |
.badge(child:, backgroundColor:, size:, alignment:, ...) |
Adds a badge overlay |
Image.asset('product.png')
.overlay(child: Icon(Icons.favorite), alignment: Alignment.topRight)
Icon(Icons.shopping_cart)
.badge(child: Text("3", style: TextStyle(color: Colors.white, fontSize: 10)))Extension on Widget
| Modifier | Description |
|---|---|
.applyIf(condition, modifier) |
Applies modifier only when condition is true |
.applyUnless(condition, modifier) |
Applies modifier only when condition is false |
.let(transform) |
Pipes widget through a transform function |
.withBuilder((context, child) => ...) |
Builder with context access |
.when(condition, ifTrue:, ifFalse:) |
Applies one of two modifiers |
Text("Welcome")
.applyIf(isLoggedIn, (w) => w.bold())
.applyIf(isHighlighted, (w) => w.backgroundColor(Colors.yellow))
.applyUnless(isEnabled, (w) => w.opacity(0.5))
widget.when(
isDarkMode,
ifTrue: (w) => w.backgroundColor(Colors.black),
ifFalse: (w) => w.backgroundColor(Colors.white),
)Extension on List<T>
| Modifier | Description |
|---|---|
.asListViewBuilder(itemBuilder:, ...) |
Converts list to ListView.builder |
.asSeparatedListView(itemBuilder:, separatorBuilder:, ...) |
Converts list to ListView.separated |
final items = ["Apple", "Banana", "Cherry"];
items.asListViewBuilder(
itemBuilder: (context, index, item) => ListTile(title: Text(item)),
)
items.asSeparatedListView(
itemBuilder: (context, index, item) => ListTile(title: Text(item)),
separatorBuilder: (context, index) => Divider(),
)Extension on List<T>
| Modifier | Description |
|---|---|
.asGridView(crossAxisCount:, itemBuilder:, ...) |
Fixed column count grid |
.asGridViewExtent(maxCrossAxisExtent:, itemBuilder:, ...) |
Max extent grid |
items.asGridView(
crossAxisCount: 2,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
itemBuilder: (context, index, item) => Card(child: Center(child: Text(item))),
)Extension on Image
| Modifier | Description |
|---|---|
.resizable(fit:) |
Sets the BoxFit |
.colorBlend(color, mode) |
Applies color blend |
.imageSize(width:, height:) |
Sets image dimensions |
Image.asset('photo.png')
.resizable(fit: BoxFit.cover)
.imageSize(width: 200, height: 200)
.colorBlend(Colors.blue, BlendMode.srcIn)Extension on Widget
| Modifier | Description |
|---|---|
.responsive(mobile:, tablet:, desktop:, ...) |
Breakpoint-based widget switching |
.onOrientation(portrait:, landscape:) |
Orientation-based widget switching |
.responsivePadding(fraction:, min:, max:) |
Screen-width-based padding |
.maxWidth(maxWidth) |
Centers content with max width constraint |
widget.responsive(
mobile: MobileLayout(),
tablet: TabletLayout(),
desktop: DesktopLayout(),
)
widget.maxWidth(800) // center-constrained content
widget.responsivePadding(fraction: 0.05)Extension on Widget
| Modifier | Description |
|---|---|
.tooltip(message, ...) |
Adds a tooltip |
.semanticLabel(label, ...) |
Adds a semantic label for screen readers |
.excludeSemantics(excluding:) |
Hides from accessibility tree |
.mergeSemantics() |
Merges child semantics into one node |
IconButton(icon: Icon(Icons.info), onPressed: () {})
.tooltip('More information')
Image.asset('decoration.png').excludeSemantics()Extension on Widget
| Modifier | Description |
|---|---|
.dismissKeyboardOnTap() |
Dismisses keyboard on tap |
.autoFocus(autofocus:) |
Sets autofocus |
.onFocusChange(callback) |
Focus change listener |
.focusTraversalGroup(policy:) |
Groups focus traversal |
Scaffold(body: form).dismissKeyboardOnTap()
TextField().autoFocus()Extensions on Widget and List<T>
| Modifier | Description |
|---|---|
.asSliver() |
Converts to SliverToBoxAdapter |
.sliverPadding(padding) |
Adds padding to a sliver |
.asSliverFillRemaining(...) |
Fills remaining viewport space |
.asSliverList(itemBuilder:) |
Converts list to SliverList |
.asSliverGrid(crossAxisCount:, itemBuilder:, ...) |
Converts list to SliverGrid |
CustomScrollView(
slivers: [
Text("Header").padding().asSliver(),
items.asSliverList(
itemBuilder: (context, index, item) => ListTile(title: Text(item)),
),
Center(child: Text("End")).asSliverFillRemaining(hasScrollBody: false),
],
)All modifiers include detailed assertions that catch invalid parameters during development:
// β
These work fine
widget.opacity(0.5)
widget.frame(width: 100, height: 100)
widget.constrainedBox(minWidth: 50, maxWidth: 200)
// β These throw helpful assertion errors in debug mode
widget.opacity(1.5)
// β 'Opacity must be between 0.0 and 1.0, got 1.5'
widget.frame(width: -10)
// β 'Frame width must be non-negative, got -10.0.'
widget.constrainedBox(minWidth: 200, maxWidth: 50)
// β 'minWidth (200.0) must be less than or equal to maxWidth (50.0).'
widget.flip()
// β 'At least one of "flipX" or "flipY" must be true.'
Contributions are welcome! Feel free to open issues or submit pull requests on GitHub.
This project is licensed under the MIT License β see the LICENSE file for details.