Flutter Scalify ๐Ÿš€

pub package License: MIT Flutter Dart Tests Platform

The Intelligent Scaling Engine for Flutter.

A complete, high-performance responsive system โ€” not just a sizing tool. Scale your entire UI across Mobile, Web, and Desktop with simple extensions, smart container queries, and zero overhead.

Scalify Responsive Demo


Why Scalify? โšก๏ธ

Feature Scalify
O(1) Inline Math (vm:prefer-inline) โœ…
Container Queries (Rebuild by parent size) โœ…
4K/Ultra-Wide Smart Dampening โœ…
Responsive Grid System (6-Tier) โœ…
Builder Pattern (Above MaterialApp) โœ…
InheritedModel (Granular Rebuild) โœ…
Debounce on Resize (Desktop/Web) โœ…
Section Scaling (ScalifySection) โœ…
Local Scaler (ScalifyBox) โœ…
6 Screen Types (Watch โ†’ Large Desktop) โœ…
Theme Auto-Scaling (One line) โœ…
Percentage Scaling (.pw .hp) โœ…
Zero External Dependencies โœ…
208 Tests Passing โœ…

Features โœจ

  • ๐ŸŽฏ Simple API โ€” 16.fz, 20.s, 24.iz, 300.w โ€” just add an extension
  • ๐Ÿ“ Responsive Layouts โ€” Built-in ResponsiveGrid, ResponsiveFlex, ResponsiveLayout
  • ๐Ÿ“ฆ Container Queries โ€” ContainerQuery & AdaptiveContainer rebuild based on parent size
  • ๐Ÿ›ก๏ธ 4K Protection โ€” Smart dampening prevents UI explosion on ultra-wide screens
  • ๐Ÿ“ฑ 6-Tier System โ€” Watch, Mobile, Tablet, Small Desktop, Desktop, Large Desktop
  • โšก Hyper Performance โ€” vm:prefer-inline, Quantized IDs, InheritedModel, Debounce
  • ๐Ÿ”ก Font Clamping โ€” Configurable min/max font bounds (never too small or too big)
  • ๐ŸŽจ Theme Scaling โ€” ThemeData.scale(context) โ€” one line, entire theme scaled
  • ๐Ÿงฑ Local Scaling โ€” ScalifyBox scales elements relative to their container
  • ๐Ÿงฉ Section Scaling โ€” ScalifySection creates independent scaling per section for split layouts
  • ๐Ÿ“Š Percentage Scaling โ€” 50.pw = 50% of screen width, 25.hp = 25% of height

Responsive Preview

Responsive Design Screenshots


Installation

dependencies:
  flutter_scalify: ^3.0.0
flutter pub get

Quick Start

This is the best practice for maximum performance. Placing ScalifyProvider above MaterialApp prevents cascading rebuilds when screen size changes.

import 'package:flutter/material.dart';
import 'package:flutter_scalify/flutter_scalify.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return ScalifyProvider(
      config: const ScalifyConfig(
        designWidth: 375,    // Your Figma/XD design width
        designHeight: 812,   // Your Figma/XD design height
      ),
      builder: (context, child) => MaterialApp(
        theme: ThemeData.light().scale(context), // ๐ŸŽจ Auto-scale theme
        home: child,
      ),
      child: const HomeScreen(),
    );
  }
}

Alternative: Child Pattern (ScalifyProvider inside MaterialApp)

MaterialApp(
  builder: (context, child) {
    return ScalifyProvider(
      config: const ScalifyConfig(designWidth: 375, designHeight: 812),
      child: child ?? const SizedBox(),
    );
  },
  home: const HomeScreen(),
);

๐Ÿ’ก Tip: The builder pattern is recommended because it puts ScalifyProvider above MaterialApp, so changing window size doesn't rebuild MaterialApp itself.


๐Ÿ“š API Cheat Sheet

1. Size & Font

Extension Example Description
.w 100.w Width โ€” Scales based on screen width ratio
.h 50.h Height โ€” Scales based on screen height ratio
.s 20.s Size โ€” General scaling (min of width/height)
.fz 16.fz Font Size โ€” Scaled + clamped + accessibility
.iz 24.iz Icon Size โ€” Proportional icon scaling
.r 12.r Radius โ€” Based on min(scaleWidth, scaleHeight)
.si 10.si Scaled Int โ€” Rounded integer for native APIs
.sc 16.sc Scale โ€” Alias for .s
.ui 16.ui UI โ€” Alias for .s

2. Percentage Scaling

Extension Example Description
.pw 50.pw 50% of screen width
.hp 25.hp 25% of screen height

3. Spacing (SizedBox)

Extension Example Output
.sbh 20.sbh SizedBox(height: 20.h)
.sbw 10.sbw SizedBox(width: 10.w)
.sbhw() 20.sbhw(width: 10) SizedBox(height: 20.h, width: 10.w)
.sbwh() 10.sbwh(height: 20) SizedBox(width: 10.w, height: 20.h)

4. Padding (EdgeInsets)

Extension Example Output
.p 16.p EdgeInsets.all(16.s)
.ph 16.ph EdgeInsets.symmetric(horizontal: 16.w)
.pv 16.pv EdgeInsets.symmetric(vertical: 16.h)
.pt 16.pt EdgeInsets.only(top: 16.h)
.pb 16.pb EdgeInsets.only(bottom: 16.h)
.pl 16.pl EdgeInsets.only(left: 16.w)
.pr 16.pr EdgeInsets.only(right: 16.w)

List Shorthand:

[20, 10].p      // EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h)
[10, 5, 10, 5].p // EdgeInsets.fromLTRB(10.w, 5.h, 10.w, 5.h)

5. Border Radius

Extension Example Output
.br 12.br BorderRadius.circular(12.r)
.brt 12.brt Top corners only
.brb 12.brb Bottom corners only
.brl 12.brl Left corners only
.brr 12.brr Right corners only

6. Context API (for const widgets)

When widgets are inside a const tree and can't use global extensions, use context:

context.w(100)    // Width scaling
context.h(50)     // Height scaling
context.r(12)     // Radius (min of scaleWidth/scaleHeight)
context.sp(16)    // Scale factor
context.fz(18)    // Font size (clamped + accessibility)
context.iz(24)    // Icon size
context.s(10)     // General scale
context.pw(50)    // 50% of screen width
context.hp(25)    // 25% of screen height

๐Ÿ’ป Complete Example

Container(
  padding: [20, 10].p,           // Symmetric padding
  width: 300.w,                   // Responsive width
  height: 200.h,                  // Responsive height
  decoration: BoxDecoration(
    color: Colors.white,
    borderRadius: 20.brt,         // Top border radius
    boxShadow: [
      BoxShadow(
        color: Colors.black12,
        offset: Offset(0, 4.s),   // Scaled offset
        blurRadius: 10.s,
      )
    ],
  ),
  child: Column(
    children: [
      Icon(Icons.star, size: 32.iz),                                  // Scaled icon
      16.sbh,                                                          // Spacing
      Text("Scalify", style: TextStyle(fontSize: 24.fz)),             // Scaled font
      8.sbh,
      Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text("Start", style: TextStyle(fontSize: 14.fz)),
          8.sbw,                                                       // Width spacing
          Icon(Icons.arrow_forward, size: 16.iz),
        ],
      )
    ],
  ),
)

๐Ÿš€ Responsive Widgets

ResponsiveGrid โ€” The Ultimate Grid

Supports Manual Columns (per screen type) and Auto-Fit (dynamic data).

Manual Mode:

ResponsiveGrid(
  spacing: 16,
  runSpacing: 16,
  watch: 1,
  mobile: 2,
  tablet: 3,
  smallDesktop: 3,
  desktop: 4,
  largeDesktop: 5,
  children: [/* widgets */],
)

Auto-Fit Mode (API data):

ResponsiveGrid(
  minItemWidth: 150,
  itemCount: products.length,
  itemBuilder: (context, index) => ProductCard(data: products[index]),
)

Sliver Mode (infinite scroll):

CustomScrollView(
  slivers: [
    ResponsiveGrid(
      useSliver: true,
      mobile: 2,
      desktop: 4,
      itemCount: 100,
      itemBuilder: (context, i) => ItemCard(i),
    ),
  ],
)

ResponsiveFlex โ€” Row โ†” Column

Automatically switches between Row and Column based on screen width.

ResponsiveFlex(
  switchOn: ScreenType.mobile,    // Column on mobile, Row on larger
  spacing: 20,
  flipOnRtl: true,                // RTL support
  children: [
    UserAvatar(),
    UserInfo(),
  ],
)

Custom breakpoint:

ResponsiveFlex(
  breakpoint: 600,                // Column when width < 600
  children: [Widget1(), Widget2()],
)

ResponsiveLayout โ€” Orientation Switch

ResponsiveLayout(
  portrait: Column(children: [Image(), Bio()]),
  landscape: Row(children: [Image(), Bio()]),
)

ResponsiveVisibility โ€” Show/Hide by Screen Type

// Whitelist: Show ONLY on mobile
ResponsiveVisibility(
  visibleOn: [ScreenType.mobile],
  child: MobileNavBar(),
)

// Blacklist: Hide on desktop
ResponsiveVisibility(
  hiddenOn: [ScreenType.desktop, ScreenType.largeDesktop],
  replacement: DesktopSidebar(),   // Optional replacement widget
  child: MobileDrawer(),
)

ResponsiveBuilder โ€” Direct Data Access

ResponsiveBuilder(
  builder: (context, data) {
    return Text("Screen: ${data.screenType} โ€” ${data.width.toInt()}px");
  },
)

Conditional Logic โ€” valueByScreen

final columns = context.valueByScreen<int>(
  mobile: 2,
  tablet: 3,
  desktop: 4,
  largeDesktop: 6,
);

๐Ÿ“ฆ Container Queries

ContainerQuery โ€” Rebuild by Parent Size

Unlike MediaQuery which reads the screen size, ContainerQuery reads the parent widget's size. Perfect for reusable components.

ContainerQuery(
  breakpoints: [200, 400, 800],
  onChanged: (prev, current) => print("Tier: ${current.tier}"),
  builder: (context, query) {
    if (query.isMobile) return CompactCard();
    if (query.isTablet) return MediumCard();
    return FullCard();
  },
)

QueryTier values: xs, sm, md, lg, xl, xxl


AdaptiveContainer โ€” Tier-Based Widgets

AdaptiveContainer(
  breakpoints: [200, 500],
  xs: Icon(Icons.home),                    // < 200px
  sm: Column(children: [Icon(), Text()]),  // 200-500px
  lg: Row(children: [Icon(), Text(), Button()]),  // > 500px
)

๐Ÿงฑ ScalifyBox โ€” Local Scaling

Scale elements relative to a specific container instead of the screen. Perfect for credit cards, game UIs, and embedded components.

ScalifyBox(
  referenceWidth: 320,
  referenceHeight: 200,
  fit: ScalifyFit.contain,    // width | height | contain | cover
  builder: (context, ls) {
    return Container(
      width: ls.w(300),         // Local width scaling
      height: ls.h(180),        // Local height scaling
      padding: ls.p(20),        // Local padding
      decoration: BoxDecoration(
        borderRadius: ls.br(12), // Local border radius
      ),
      child: Column(
        children: [
          Text("VISA", style: TextStyle(fontSize: ls.fz(18))),
          ls.sbh(10),            // Local spacing
        ],
      ),
    );
  },
)

LocalScaler API:

Method Description
ls.w(value) Local width scaling
ls.h(value) Local height scaling
ls.s(value) Local general scaling
ls.fz(value) Local font size (clamped)
ls.iz(value) Local icon size
ls.si(value) Rounded int
ls.p(value) EdgeInsets.all
ls.ph(value) Horizontal padding
ls.pv(value) Vertical padding
ls.br(value) BorderRadius.circular
ls.r(value) Radius.circular
ls.sbh(value) SizedBox(height:)
ls.sbw(value) SizedBox(width:)
ls.scaleFactor Current scale value

๐Ÿงฉ ScalifySection โ€” Independent Section Scaling

Creates an independent scaling context for any part of your UI. Essential for split-screen / master-detail layouts where each section should scale based on its own width.

Row(
  children: [
    // Sidebar: scales based on 300px width
    SizedBox(
      width: 300,
      child: ScalifySection(child: Sidebar()),
    ),
    // Main content: scales based on remaining width
    Expanded(
      child: ScalifySection(child: MainContent()),
    ),
  ],
)

๐Ÿ’ก Tip: Inside a ScalifySection, use context-based extensions (context.fz(16), context.w(100)) instead of global extensions (16.fz, 100.w) for accurate section-local scaling.

Full Master-Detail Example:

class MasterDetailLayout extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final width = MediaQuery.sizeOf(context).width;

    // Mobile: bottom navigation
    if (width < 900) {
      return Scaffold(
        body: MainPages(),
        bottomNavigationBar: BottomNav(),
      );
    }

    // Desktop: sidebar + content โ€” each scales independently
    return Row(
      children: [
        SizedBox(
          width: 300,
          child: ScalifySection(child: Sidebar()),
        ),
        Expanded(
          child: ScalifySection(child: MainPages()),
        ),
      ],
    );
  }
}

๐Ÿ›ก๏ธ AppWidthLimiter โ€” Ultra-Wide Protection

Centers and constrains your app on ultra-wide monitors.

AppWidthLimiter(
  maxWidth: 1400,
  minWidth: 360,               // Enables horizontal scroll below 360px
  horizontalPadding: 16,
  backgroundColor: Color(0xFFE2E8F0),
  child: YourApp(),
)

๐Ÿ“ Best Practices โ€” Consistent UI

Use the right extension for each element to maintain a consistent UI across all screen sizes:

Element Use Why
Text / Fonts .fz Scaled + clamped + accessibility
Icons .iz / .s Proportional to screen
Button height .s โŒ Never .h โ€” distorts on wide screens
Input field height .s โŒ Never .h โ€” text overflows
Container width .w Follows screen width
Container height .s Stays proportional
Horizontal padding .w Follows width
Vertical spacing .h Adapts to screen height
General spacing .s Balanced proportional
Border radius .r / .br Uses min(scaleW, scaleH)

โš ๏ธ Common Mistake: Using .h for button/input heights causes them to shrink on wide screens (where height < width), making text overflow.

โœ… Fix: Use .s โ€” it uses min(scaleWidth, scaleHeight) which stays balanced.

// โŒ Wrong โ€” height shrinks on wide screens
SizedBox(height: 48.h, child: ElevatedButton(...))

// โœ… Correct โ€” stays proportional everywhere
SizedBox(height: 48.s, child: ElevatedButton(...))

๐ŸŽจ Theme Auto-Scaling

Scale your entire app theme with one line โ€” no need to add .fz to every text widget.

ScalifyProvider(
  builder: (context, child) => MaterialApp(
    theme: ThemeData.light().scale(context),  // โœจ One line!
    home: child,
  ),
  child: const HomeScreen(),
)

๐Ÿ’ก Automatically skips scaling when scaleFactor == 1.0 for zero overhead.


๐Ÿ”„ Live Resizing (Desktop & Web)

For instant UI updates while dragging the window:

Option 1: ResponsiveBuilder (Recommended)

ResponsiveBuilder(
  builder: (context, data) {
    return Scaffold(
      body: Center(
        child: Text("${data.width.toInt()}px", style: TextStyle(fontSize: 20.fz)),
      ),
    );
  },
)

Option 2: Direct Subscription

@override
Widget build(BuildContext context) {
  context.responsiveData;  // ๐Ÿ‘ˆ Subscribe to resize events
  return Scaffold(/* ... */);
}

โš™๏ธ ScalifyConfig โ€” Full Reference

ScalifyConfig(
  // ๐Ÿ“ Design Baseline
  designWidth: 375.0,             // Figma/XD design width
  designHeight: 812.0,            // Figma/XD design height

  // ๐Ÿ“ฑ Breakpoints (Customizable)
  watchBreakpoint: 300.0,         // < 300 = Watch
  mobileBreakpoint: 600.0,        // 300-600 = Mobile
  tabletBreakpoint: 900.0,        // 600-900 = Tablet
  smallDesktopBreakpoint: 1200.0,  // 900-1200 = Small Desktop
  desktopBreakpoint: 1800.0,      // 1200-1800 = Desktop
  //                               // > 1800 = Large Desktop

  // ๐Ÿ”ก Font Control
  minFontSize: 6.0,               // Floor for .fz
  maxFontSize: 256.0,             // Ceiling for .fz
  respectTextScaleFactor: true,   // System accessibility support

  // ๐Ÿ“ Scale Bounds
  minScale: 0.5,                  // Minimum scale factor
  maxScale: 4.0,                  // Maximum scale factor

  // ๐Ÿ›ก๏ธ 4K Protection
  memoryProtectionThreshold: 1920.0,  // Where dampening kicks in
  highResScaleFactor: 0.65,           // Dampening strength (0-1)

  // โšก Performance
  debounceWindowMillis: 120,          // Resize debounce (ms)
  rebuildScaleThreshold: 0.01,        // Min scale change to rebuild
  rebuildWidthPxThreshold: 4.0,       // Min px change to rebuild
  enableGranularNotifications: false, // InheritedModel aspects

  // ๐Ÿ”„ Orientation
  autoSwapDimensions: false,          // Swap design W/H in landscape

  // ๐Ÿ”ง Minimum Window Width
  minWidth: 0.0,                      // Enables horizontal scroll below this

  // ๐Ÿท๏ธ Legacy
  legacyContainerTierMapping: false,  // v1 compatibility
  showDeprecationBanner: true,        // Debug banner for legacy mode
)

๐Ÿ“ฑ Screen Breakpoints

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Screen Type โ”‚   Width Range   โ”‚        Enum Value        โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  Watch       โ”‚     < 300px     โ”‚  ScreenType.watch        โ”‚
โ”‚  Mobile      โ”‚  300px - 600px  โ”‚  ScreenType.mobile       โ”‚
โ”‚  Tablet      โ”‚  600px - 900px  โ”‚  ScreenType.tablet       โ”‚
โ”‚  Small DT    โ”‚  900px - 1200px โ”‚  ScreenType.smallDesktop โ”‚
โ”‚  Desktop     โ”‚ 1200px - 1800px โ”‚  ScreenType.desktop      โ”‚
โ”‚  Large DT    โ”‚    > 1800px     โ”‚  ScreenType.largeDesktop โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

๐Ÿง  Advanced: How the Engine Works

Configurable Rebuild Tolerance

Scalify uses a dual-tolerance system to prevent unnecessary rebuilds:

  • rebuildWidthPxThreshold (default 4.0px) โ€” Ignores sub-pixel size changes
  • rebuildScaleThreshold (default 0.01) โ€” Ignores scale changes < 1%
Screen: 375px โ†’ 377px (2px diff < 4px threshold)
Scale:  1.000 โ†’ 1.005 (0.005 diff < 0.01 threshold)
โ†’ No rebuild! โœ…

Internally, Quantized IDs (ร—1000) are still used for InheritedModel aspect-based comparisons to ensure fast integer equality checks.

InheritedModel Aspects

Enable granular notifications to rebuild widgets only when specific data changes:

// Only rebuilds when screen TYPE changes (not on every pixel resize)
ScalifyProvider.of(context, aspect: ScalifyAspect.type);

// Only rebuilds when scale FACTOR changes
ScalifyProvider.of(context, aspect: ScalifyAspect.scale);

// Only rebuilds when text scale changes (accessibility)
ScalifyProvider.of(context, aspect: ScalifyAspect.text);

Enable in config:

ScalifyConfig(enableGranularNotifications: true)

Debounce on Resize

On Desktop/Web, window resizing fires hundreds of events per second. Scalify debounces platform-driven resize events with a configurable window (debounceWindowMillis, default 120ms), calculating the layout once after the user stops dragging. Parent-driven updates (e.g., didChangeDependencies) remain synchronous for instant response.

// Disable debounce for instant updates:
ScalifyConfig(debounceWindowMillis: 0)

// Increase debounce for weaker devices:
ScalifyConfig(debounceWindowMillis: 200)

Nested Providers & Performance (observeMetrics)

When nesting ScalifyProvider (e.g., for a split-screen section), the inner provider should not listen to window resize events directly, as this creates a "double debounce" race condition with the parent provider, causing UI lag.

To fix this, set observeMetrics: false on the nested provider:

ScalifyProvider(
  config: sectionConfig,
  observeMetrics: false, // โšก๏ธ Disables internal resize listener
  child: SectionContent(),
)

This ensures the inner provider updates synchronously when its parent rebuilds, resulting in 60fps performance during window resizing. ScalifySection handles this automatically.

4K Smart Dampening

For screens wider than memoryProtectionThreshold (default 1920px):

Normal: scale = screenWidth / designWidth
4K:     scale = thresholdScale + (excessWidth / designWidth ร— dampFactor)

This prevents text from becoming 5ร— the intended size on ultra-wide monitors.


๐Ÿ“‹ Migration from v2.x โ†’ v3.0.0

- MaterialApp(
-   builder: (context, child) => ScalifyProvider(child: child),
-   home: HomeScreen(),
- )
+ ScalifyProvider(
+   builder: (context, child) => MaterialApp(home: child),
+   child: const HomeScreen(),
+ )

2. Context API for const Widgets

- // Won't update in const trees
- Text("Hi", style: TextStyle(fontSize: 16.fz))

+ // Always updates via context
+ Builder(
+   builder: (context) => Text(
+     "Hi",
+     style: TextStyle(fontSize: context.sp(16)),
+   ),
+ )

3. Percentage Scaling (New)

SizedBox(width: 50.pw)   // 50% of screen width
SizedBox(height: 25.hp)  // 25% of screen height

4. Theme Scaling (New)

MaterialApp(
  theme: ThemeData.light().scale(context),
)

๐Ÿงช Testing

The package includes 208 comprehensive tests covering all widgets, extensions, and edge cases:

flutter test --reporter compact
# 00:02 +208: All tests passed!

Author

Alaa Hassan Damad


License

MIT License โ€” see LICENSE for details.