Flutter Scale Kit
Design once at 375Γ812. Ship to phones, tablets, desktop, and web.
A high-performance responsive engine with drop-in widgets that scale automatically, smart limits so nothing explodes on a big monitor, and caching that makes it ~3x faster than a plain map lookup.
π± Mobile
|
π² Tablet
|
π₯οΈ Desktop
One codebase. One design size. Every screen above is the same widget tree.
π― Try the live demo in your browser β
The difference in one screen
Before β the usual responsive code, extension noise on every number:
Container(
width: 200.w,
height: 100.h,
padding: EdgeInsets.all(16.w),
margin: EdgeInsets.only(bottom: 12.h),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.r),
),
child: Text('Hello', style: TextStyle(fontSize: 16.sp)),
)
After β write your design numbers, the widget scales them:
SKContainer(
width: 200,
height: 100,
padding: EdgeInsets.all(16),
margin: EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12), // safely clamped
),
child: SKText('Hello', fontSize: 16),
)
Prefer extensions? They still work, and you can mix both in the same file:
Container(width: 200.w, child: Text('Hi', style: TextStyle(fontSize: 16.sp)))
Why developers pick it
| π§ Zero config | Detects device, orientation, aspect ratio, and density. Picks scale limits automatically. ~95% of projects never tune anything. |
| π οΈ Drop-in widgets | Container β SKContainer, Text β SKText. Search-and-replace migration, no refactor. |
| β‘ ~3x faster scalars | .w / .h / .sp skip the map cache and use precomputed multipliers. |
| π§Ή ~80% fewer rebuilds | Listens to size and orientation only. Opening the keyboard does not rebuild your app. |
| π‘οΈ No memory leaks | LRU cache capped at 2000 entries, cleared automatically on resize. |
| π₯οΈ Desktop & web first-class | Lock desktop to tablet or mobile layouts, override per widget, branch on platform. |
| π€ Language-aware fonts | Arabic, Japanese, or any group gets its own font automatically across every text style. |
| π¨ Optional look companion | Add flutter_scale_theme_kit for colors and ThemeData. |
| π€ AI agent skill | Your coding agent uses the package correctly in Cursor, Claude Code, Copilot, and more. |
If this package helps you, please like it on pub.flutter-io.cn β it genuinely improves discoverability.
π The whole package in 60 seconds
Every feature, one example each. Nothing here needs a click β the links only take you to the longer treatment further down.
Sizing
Scale any number β details
200.w; // width 100.h; // height
16.sp; // font size 12.rSafe; // radius, gently clamped
0.5.sw; // 50% of screen width
16.spClamp(14, 20); // scaled, but never outside 14β20 (great for desktop)
Scale whole geometry objects and gaps β no field-by-field conversion β details
EdgeInsets.only(left: 12, right: 12).w;
BoxConstraints(maxWidth: 200, minHeight: 80).r;
Row(children: [avatar, 12.horizontalSpace, title]); // or HSpace(12.w)
Column(children: [header, 16.verticalSpace, body]);
Widgets
Drop-in widgets that scale themselves β pass raw design numbers, details
SKContainer(
width: 200,
padding: EdgeInsets.all(16),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(12)),
child: SKText('No .w anywhere', fontSize: 16),
);
22 replacements in total β SKCard, SKAppBar, SKTextField, SKElevatedButton, SKListTile,
SKIcon, SKDivider, the chips, and more. Migration is search-and-replace.
Images from any source, scaled β details
SKImage.asset('assets/logo.png', width: 100, height: 100);
SKImage.network(url, width: 200, height: 200, cacheWidth: 400);
Layout helpers for the code you write most β details
SKit.padding(start: 24, end: 12, child: widget); // RTL-aware, one definition
SKit.roundedContainer(all: 16, color: Colors.white, borderWidth: 1, child: widget);
SKit.vSpace(12);
Design system
Tokens instead of magic numbers β details
setPaddingSizes(SizeValues.custom(sm: 8, md: 16, lg: 24));
SKit.paddingSize(all: SKSize.md, child: SKit.text('Hi', textSize: SKTextSize.s16));
Compute a whole screen's tokens in one call β details
const appDesign = ScaleKitDesignValues(textMd: 14, paddingMd: 16, radiusMd: 12);
final v = appDesign.compute(); // ready EdgeInsets, BorderRadius, TextStyle
SKPadding(padding: v.paddingMd!, child: Text('Hi', style: v.textMd));
Full text control in one call β every Text attribute, all scaled β details
SKit.textFull('Hello', fontSize: 18, color: Colors.blue, letterSpacing: 0.5, maxLines: 2);
final style = SKit.textStyleFull(fontSize: 24, fontWeight: FontWeight.bold, height: 1.5);
Look
Fonts that follow the locale β set once, applied everywhere β details
FontConfig.instance
..setDefaultFont(googleFont: GoogleFonts.inter)
..setLanguageGroupFont(
const LanguageGroupFontConfig(
languageCodes: ['ar', 'fa', 'ur'],
googleFont: GoogleFonts.almarai,
),
);
A responsive ThemeData β details
MaterialApp(
theme: ResponsiveThemeData.create(context: context, useMaterial3: true),
);
Or add the look companion for semantic colors and dark mode β details
// flutter_scale_theme_kit: colors + ThemeData, Scale Kit keeps the sizing
context.st.primary; // also .surface, .text, .error, .radius, .card β¦
appST.light.copyWith(
textTheme: appST.light.createResponsiveTextTheme(appST.light.textTheme),
);
Responsive layout
Responsive values β when only a number changes β details
final columns = SKit.responsiveInt(
context: context,
mobile: 2, tablet: 4, desktop: 6,
);
GridView.count(crossAxisCount: columns, children: [...]);
Responsive widgets β when the layout changes β details
SKResponsive(
mobile: (_) => const BottomNavShell(),
tablet: (_) => const NavigationRailShell(),
desktop: (_) => const SplitViewShell(),
);
Desktop and web on your terms β details
// Globally: make desktop and web reuse your tablet layout
ScaleKitBuilder(lockDesktopPlatforms: true, lockDesktopAsTablet: true, ...);
// Or per widget, without the global lock
SKResponsiveBuilder(desktopAs: DesktopAs.tablet, ...);
Ask the device anything β details
if (context.isTablet) ... // layout classification
if (context.isIOSPlatform) ... // OS chrome, web and Wasm safe
if (context.isDesktopAtLeastTablet) // width only, no layout swap
β¦even without a BuildContext β details
class _S extends State<Page> with DeviceMetricsMixin<Page> {
@override
Widget build(BuildContext context) => isDesktop ? const Wide() : const Narrow();
}
ScaleManager.instance.screenWidth; // or read the engine directly
Tuning the engine
Guard rails so nothing explodes on a big screen β details
ScaleKitBuilder(minScale: 0.9, maxScale: 1.2, ...);
Your own breakpoints β defaults are 600 / 1200 / 1600 / 1920 β details
ScaleKitBuilder(
breakpoints: const ScaleBreakpoints(mobileMaxWidth: 540, tabletMaxWidth: 1100),
...
);
Orientation boosts, applied after clamping β details
ScaleKitBuilder(
autoScaleLandscape: true, // on by default
mobileLandscapeFontBoost: 1.2, // readability without inflating containers
mobileLandscapeSizeBoost: 1.1,
...
);
Leave all of the above unset and auto-configuration picks a range per device, orientation, and aspect ratio β including foldables, ultrawide monitors, and small windows.
Rebuild sensitivity β details
ScaleKitBuilder(sizeChangeThreshold: 0.01, ...); // rebuild after a 1% window change
Tooling
Compare against raw Flutter at runtime β details
ScaleKitBuilder(enabledListenable: myToggle, ...); // flip scaling on and off live
Test scaled widgets at any screen size β details
tester.view.physicalSize = const Size(1200, 800);
await tester.pumpWidget(ScaleKitBuilder(designWidth: 375, designHeight: 812, child: ...));
Works inside Device Preview β details
ScaleManager.setDevicePreviewPlatformGetter((c) => DevicePreview.platformOf(c));
π€ Don't want to read all this? Let your AI agent set it up
npx skills add fodilfliti/flutter_scale_kit
Then ask your agent: "Init Flutter Scale Kit with defaults."
It installs the dependency, creates lib/core/scale_kit.dart, wraps main.dart, and from then on
writes Scale Kit code correctly β tokens instead of magic numbers, no double-scaling. Works in
Cursor, Claude Code, GitHub Copilot, Codex, Windsurf, Gemini CLI, and any tool that supports the
open Agent Skills format.
β Full agent skill details, options, and manual install
π Table of contents
Get running
- Install
- π Starter app β copy a real, runnable app: Scale Kit + Theme Kit in 4 files
- Quick start β runnable
main.dart, everyScaleKitBuilderoption, fonts, tokens - Choose your approach β which API for which job
- β οΈ Gotchas β read before you ship
Core APIs
- Extension methods β
.w.h.sp.rand constraints - Drop-in SK widgets β full list and what each scales
- SKit helpers β padding, containers, spacing, text
- Design system tokens β
SKSize,ScaleKitDesignValues - Typography & fonts β
FontConfig, language-aware fonts - ThemeData integration
- π¨ Look companion (theme kit)
Responsive layout
- Responsive widgets & values β
SKResponsive,responsiveInt - Desktop & web freedom β locks, overrides, platform branching
- Device queries β context extensions,
DeviceMetricsMixin - Breakpoints & size classes
How it works
- Scale limits β
minScale/maxScale - Orientation boosts
- Auto-configuration
- Performance
Extras
- Testing
- Enable/disable at runtime
- Device Preview integration
- π€ AI agent skill
- Migrating from flutter_screenutil
- Advanced guides Β· FAQ Β· Support Β· License
π¦ Install
dependencies:
flutter_scale_kit: ^2.0.0
flutter pub get
Optional look companion for colors and ThemeData (see below):
dependencies:
flutter_scale_kit: ^2.0.0
flutter_scale_theme_kit: ^1.0.1
π Starter app β copy a real app, not snippets
β doc/starter-app.md is a complete first-time setup you can copy file by file into a new project. Not fragments β an app that runs.
| File | What it holds |
|---|---|
pubspec.yaml |
Both dependencies |
lib/core/design.dart |
Colors and radius tokens (STTheme) |
lib/core/scale_kit.dart |
Size tokens, defaults, and fonts in one initScaleKit() |
lib/main.dart |
Both packages wired together, in the order that actually works |
lib/pages/home_page.dart |
A real dashboard β responsive grid, themed cards, dark-mode toggle |
It also writes the same card in all four usage styles (design_system, drop_in,
extensions, hybrid) so you can compare them side by side, and lists the rules that keep sizing
and theming from stepping on each other.
Using Scale Kit on its own? The same files apply β just skip design.dart and the context.st
colors.
π Quick start
In a hurry? npx skills add fodilfliti/flutter_scale_kit, then tell your agent
"Init Flutter Scale Kit with defaults." β details below. Otherwise, read on.
1. Wrap your app
A complete, runnable main.dart. Copy it, change the design size to your Figma canvas, done.
import 'package:flutter/material.dart';
import 'package:flutter_scale_kit/flutter_scale_kit.dart';
void main() {
// Optional: define what 'md' means for your app. Skip it and defaults apply.
setPaddingSizes(SizeValues.custom(xs: 4, sm: 8, md: 16, lg: 24, xl: 32, xxl: 48));
setRadiusSizes(SizeValues.custom(xs: 4, sm: 8, md: 12, lg: 16, xl: 20, xxl: 28));
setSpacingSizes(SizeValues.custom(xs: 4, sm: 8, md: 16, lg: 24, xl: 32, xxl: 48));
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
// ScaleKitBuilder goes ABOVE MaterialApp so every route inherits scaling.
return ScaleKitBuilder(
designWidth: 375,
designHeight: 812,
designType: DeviceType.mobile,
child: MaterialApp(
title: 'My App',
home: const HomePage(),
),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: SKAppBar(title: const Text('Scale Kit')),
body: SKit.paddingSize(
all: SKSize.md,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SKText('Welcome', fontSize: 24, fontWeight: FontWeight.bold),
SKit.vSpaceSize(SKSize.sm),
SKText('This text scales on every device.', fontSize: 14),
SKit.vSpaceSize(SKSize.md),
SKit.roundedContainerSize(
all: SKSize.md,
color: Colors.blue.shade50,
child: SKit.paddingSize(
all: SKSize.md,
child: SKText('A card that scales too.', fontSize: 16),
),
),
],
),
),
);
}
}
No MediaQuery wiring. No manual breakpoints. No setState on resize.
2. ScaleKitBuilder options
Only designWidth, designHeight, and child are required. Everything else is optional β
each one is auto-detected or has a sensible default.
| Parameter | Default | What it does |
|---|---|---|
designWidth Β· designHeight |
required | Your Figma canvas size, e.g. 375 Γ 812 |
child |
required | Your app β put MaterialApp here |
designType |
DeviceType.mobile |
Which device your design was drawn for |
deviceTypeOverride |
auto | Force a classification instead of detecting it |
minScale Β· maxScale |
auto | Clamp range, so nothing explodes on a big screen β details |
autoScale |
true |
Master switch for automatic limits |
autoScaleLandscape |
true |
Apply landscape boosts on rotate |
autoScalePortrait |
false |
Apply portrait boosts too |
breakpoints |
600 / 1200 / 1600 / 1920 |
Your own device thresholds β details |
lockDesktopPlatforms |
false |
Pin desktop and web to a chosen breakpoint |
lockDesktopAsTablet |
false |
β¦specifically the tablet branch |
lockDesktopAsMobile |
false |
β¦specifically the mobile branch |
enabled |
true |
Turn scaling off to compare against raw Flutter |
enabledListenable |
null |
ValueListenable<bool> for a runtime toggle β details |
sizeChangeThreshold |
0.05 mobile, 0 desktop |
How much the window must change before a rebuild |
{mobile,tablet,desktop}{Landscape,Portrait}{Font,Size}Boost |
auto | 12 fine-grained multipliers β details |
A tuned example β strict scale range, tablet layout reused on desktop, custom breakpoints:
ScaleKitBuilder(
designWidth: 375,
designHeight: 812,
designType: DeviceType.mobile,
// Keep the UI close to spec instead of the auto range
minScale: 0.9,
maxScale: 1.2,
// Desktop and web borrow the tablet layout
lockDesktopPlatforms: true,
lockDesktopAsTablet: true,
// Your own thresholds
breakpoints: const ScaleBreakpoints(
mobileMaxWidth: 540,
tabletMaxWidth: 1100,
desktopMaxWidth: 1650,
largeDesktopMaxWidth: 2200,
),
// Bump readability in landscape without inflating containers
mobileLandscapeFontBoost: 1.2,
mobileLandscapeSizeBoost: 1.1,
child: const MyApp(),
);
3. Optional setup
All four steps below are optional β the package works without any of them. Put them in main()
before runApp, or in a single lib/core/scale_kit.dart file.
a. Design tokens β decide what sm / md / lg mean once:
setPaddingSizes(SizeValues.custom(xs: 4, sm: 8, md: 16, lg: 24, xl: 32, xxl: 48));
setMarginSizes(SizeValues.custom(xs: 2, sm: 4, md: 8, lg: 12, xl: 16, xxl: 24));
setRadiusSizes(SizeValues.custom(xs: 2, sm: 4, md: 8, lg: 12, xl: 16, xxl: 24));
setSpacingSizes(SizeValues.custom(xs: 4, sm: 8, md: 12, lg: 16, xl: 20, xxl: 24));
setTextSizes(TextSizeValues.material3());
Defaults if you skip it: xs: 2, sm: 4, md: 8, lg: 12, xl: 16, xxl: 24.
b. Default values β so shorthand helpers need no arguments:
setDefaultPadding(16);
setDefaultMargin(8);
setDefaultRadius(12);
setDefaultSpacing(8);
setDefaultTextSize(14);
Now SKit.pad(), SKit.margin(), SKit.rounded(), SKit.h(), and SKit.v() work bare.
c. Fonts β set once, applied to every scaled text style automatically. No wiring at the call site, and the right font is picked from the active locale:
import 'package:google_fonts/google_fonts.dart';
FontConfig.instance
..setDefaultFont(googleFont: GoogleFonts.inter)
..setLanguageFont(
const LanguageFontConfig(
languageCode: 'ja',
googleFont: GoogleFonts.notoSansJp,
),
)
..setLanguageGroupFont(
const LanguageGroupFontConfig(
languageCodes: ['ar', 'fa', 'ur'],
googleFont: GoogleFonts.almarai,
),
);
Extensions, SKText, SKit.text*, and responsive themes all pick this up. A custom family works
the same way: setDefaultFont(customFontFamily: 'Cairo'). google_fonts is not a dependency
of this package β you pass the builders in yourself. More on fonts
d. Screen tokens β for screens with many values, define them once as a const and compute the
whole set in one call instead of scaling each number separately:
// lib/core/design.dart β const, so it costs nothing to hold
const appDesign = ScaleKitDesignValues(
textSm: 12, textMd: 14, textLg: 16, textXl: 24,
paddingSm: 8, paddingMd: 16, paddingLg: 24,
spacingSm: 8, spacingMd: 16, spacingLg: 24,
radiusSm: 6, radiusMd: 12, radiusLg: 16,
);
@override
Widget build(BuildContext context) {
final v = appDesign.compute(); // one calculation for the whole screen
return SKPadding(
padding: v.paddingMd!,
child: SKContainer(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: v.borderRadiusMd,
),
child: Text('Already scaled', style: v.textMd),
),
);
}
compute() hands back ready-made EdgeInsets, BorderRadius, and TextStyle objects.
More on tokens
π§ Choose your approach
Four ways to use Scale Kit. Mix them freely β pick per situation, not per project.
| Your situation | Use this | Example |
|---|---|---|
| New app, want consistency | Tokens + SK* widgets |
SKit.paddingSize(all: SKSize.md, child: SKText('Hi', fontSize: 16)) |
| Migrating an app fast | SK* drop-ins, raw numbers |
SKContainer(width: 120, child: SKText('Hi', fontSize: 16)) |
| Coming from screenutil | Extensions on Flutter widgets | Container(width: 120.w, child: Text('Hi', style: TextStyle(fontSize: 16.sp))) |
| One odd Figma number (73px) | Extension, inline | SizedBox(width: 73.w) |
| Value gets huge on desktop | Constraint extension | 200.wMax(360), 16.spClamp(14, 20) |
| Grid columns / item counts | Responsive value | SKit.responsiveInt(context: context, mobile: 2, tablet: 4, desktop: 6) |
| Layout structure changes | Responsive builder | SKResponsiveBuilder(mobile: ..., desktop: ...) |
What NOT to reach for
| Don't | Because |
|---|---|
SKResponsive just to change padding or font size |
Tokens and SK* already scale β you are adding a rebuild for nothing |
.w on every single number |
That is the screenutil habit; SK* widgets remove it |
A second ScaleKitBuilder deeper in the tree |
One at the root is enough; nesting creates conflicting scopes |
Full MediaQuery subscriptions for sizing |
The builder already listens to size and orientation efficiently |
Recommended stack for a new app
ScaleKitBuilder+ token setup in one file (lib/core/scale_kit.dart)SKSizetokens for padding, margin, radius, spacingSK*drop-in widgets with raw design numbers.w/.sp/.rSafeonly as escape hatches
π See all four styles side by side: the starter app writes the same card as
design_system,drop_in,extensions, andhybrid, so you can compare and pick one. Those are also the style names the AI agent skill understands.
β οΈ Gotchas
Four rules. They cover almost every issue people hit.
1. Never double-scale
SK* widgets scale internally. Passing an already-scaled value scales it twice.
SKContainer(width: 120) // β
correct
SKContainer(width: 120.w) // β scaled twice
SKText('Hi', fontSize: 16) // β
correct
SKText('Hi', fontSize: 16.sp) // β scaled twice
2. Pick the right radius
| Extension | Use for |
|---|---|
.rSafe |
Cards, sheets, containers β the safe default |
.r |
Pills, circular avatars, fully rounded chips |
.rFixed |
Hairlines that must never change |
SK* widgets already apply rSafe to borderRadius for you.
3. ScaleKitBuilder goes above MaterialApp
ScaleKitBuilder(child: MaterialApp(...)) // β
MaterialApp(home: ScaleKitBuilder(...)) // β routes above it won't scale
4. Theme needs a context under the builder
ResponsiveThemeData.create reads the active scale, so it needs a BuildContext below
ScaleKitBuilder. Wrap it in a Builder:
ScaleKitBuilder(
designWidth: 375,
designHeight: 812,
child: Builder(
builder: (context) => MaterialApp(
theme: ResponsiveThemeData.create(context: context, useMaterial3: true),
home: const HomePage(),
),
),
)
π Extension methods
Familiar if you come from flutter_screenutil. Works on any num.
200.w; // scaled width
100.h; // scaled height
16.sp; // scaled font size
16.spf; // font size Γ system text scale factor
12.r; // fully scaled radius
12.rSafe; // radius with gentle clamping (use for cards)
12.rFixed; // never scaled
0.5.sw; // 50% of screen width
0.3.sh; // 30% of screen height
Constraints (min / max / clamp)
Every extension has Min, Max, and Clamp forms. Essential for desktop, where an unclamped
value can get enormous.
200.wMax(300); // scaled width, never above 300
100.hClamp(50, 150); // scaled height, kept in 50β150
16.spClamp(12, 24); // font size, kept in 12β24
12.rSafeClamp(8, 20); // safe radius, kept in 8β20
0.5.swClamp(200, 400); // 50% of width, kept in 200β400
Full set: .wMin/.wMax/.wClamp, .hMin/.hMax/.hClamp, .spMin/.spMax/.spClamp,
.rMin/.rMax/.rClamp, .rSafeClamp, .swMin/.swMax/.swClamp, .shMin/.shMax/.shClamp.
On geometry objects
Skip converting each field by hand:
EdgeInsets.only(left: 12, right: 12).w; // both sides scaled
EdgeInsetsDirectional.only(start: 16).h; // RTL-aware
BoxConstraints(maxWidth: 200, minHeight: 80).r;
Radius.circular(24).w;
BorderRadius.circular(16).h;
Gap extensions
Row(
children: [
const Avatar(),
12.horizontalSpace, // β HSpace(12.w)
const Expanded(child: Title()),
],
);
Column(
children: [
const Header(),
16.verticalSpace, // β VSpace(16.h)
const Body(),
],
);
β‘ All of this is cached. Base extensions and constraint variants alike. A value is computed once per unique combination and reused across rebuilds. The cache clears on resize and rotation.
π§© Drop-in SK widgets
Replacements for Flutter widgets that scale their own properties. Pass raw design numbers.
Migration is a search-and-replace: Container β SKContainer, Text β SKText,
Padding β SKPadding, TextField β SKTextField, ElevatedButton β SKElevatedButton.
Your code keeps working, now with scaling.
SKPadding(
padding: EdgeInsets.all(16),
child: SKMargin(
margin: EdgeInsets.only(bottom: 12),
child: SKContainer(
width: 200,
height: 100,
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(12), // uses rSafe automatically
),
child: SKText('Everything here scales', fontSize: 14, fontWeight: FontWeight.bold),
),
),
)
Complete list and what each one scales
| Widget | Scaled properties |
|---|---|
SKContainer |
width, height, padding, margin, borderRadius (via rSafe), boxShadow, border width, constraints |
SKPadding |
EdgeInsets and EdgeInsetsDirectional values |
SKMargin |
EdgeInsets and EdgeInsetsDirectional values |
SKText |
fontSize, plus FontConfig font when configured |
SKIcon |
size |
SKCard |
margin, elevation, shape.borderRadius |
SKDivider |
thickness, indent, endIndent, height |
SKAppBar |
toolbarHeight, elevation, titleSpacing, leadingWidth |
SKListTile |
contentPadding, minLeadingWidth, minVerticalPadding |
SKSwitch |
splashRadius |
SKSwitchListTile |
contentPadding, splashRadius |
SKTextField |
fontSize, InputDecoration padding, borderRadius, cursorWidth, cursorHeight |
SKTextFormField |
Same as SKTextField, plus form validation |
SKElevatedButton |
padding, minimumSize, fixedSize, borderRadius, elevation |
SKTextButton |
padding, minimumSize, fixedSize, borderRadius |
SKOutlinedButton |
padding, minimumSize, fixedSize, borderRadius |
SKIconButton |
iconSize, padding, constraints |
SKActionChip |
padding, labelPadding, avatarPadding, borderRadius, elevation, deleteIconSize |
SKFilterChip |
Same as SKActionChip |
SKChoiceChip |
padding, labelPadding, avatarPadding, borderRadius, elevation |
SKInputChip |
Same as SKActionChip |
SKImage |
width, height |
More examples
SKIcon(Icons.home, size: 24);
SKCard(
margin: EdgeInsets.all(16),
elevation: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Text('Card content'),
);
SKDivider(thickness: 1, indent: 16, endIndent: 16);
SKListTile(
leading: SKIcon(Icons.person, size: 24),
title: SKText('Title', fontSize: 16),
contentPadding: EdgeInsets.all(16),
minLeadingWidth: 40,
);
SKImage β four sources
SKImage.asset('assets/images/logo.png', width: 100, height: 100, fit: BoxFit.cover);
SKImage.network(
'https://example.com/image.jpg',
width: 200,
height: 200,
headers: {'Authorization': 'Bearer token'},
cacheWidth: 400, // memory optimization
cacheHeight: 400,
);
SKImage.file(File('/path/to/image.png'), width: 150, height: 150);
SKImage.memory(imageBytes, width: 120, height: 120);
Const spacing widgets
| Widget | Purpose |
|---|---|
SKSizedBox({width, height}) |
SizedBox wrapper usable in const contexts |
HSpace(double width) |
Horizontal gap |
VSpace(double height) |
Vertical gap |
SSpace(double size) |
Square gap on both axes |
π‘ Mixing is safe. Every SK widget inspects incoming numbers and only scales unmarked ones, so
width: 120,width: 120.wMax(240), andborderRadius: BorderRadius.circular(24.r)all work on the same widget. That said, the habit to build is: raw numbers insideSK*widgets.
π§ SKit helpers
Shortcuts for the layout code you write most often.
Padding and margin
SKit.padding(all: 16, child: widget);
SKit.padding(horizontal: 24, vertical: 12, child: widget);
SKit.paddingSize(all: SKSize.md, child: widget);
SKit.paddingSize(horizontal: SKSize.lg, vertical: SKSize.sm, child: widget);
SKit.margin(12, child: widget);
SKit.marginSize(all: SKSize.md, child: widget);
// Raw EdgeInsets for your own widgets
final insets = SKit.paddingEdgeInsets(all: 16);
final marginInsets = SKit.marginEdgeInsetsSize(all: SKSize.md);
start and end resolve against Directionality, so one definition serves both LTR and RTL:
SKit.padding(start: 24, end: 12, vertical: 16, child: content);
Rounded containers
SKit.roundedContainer(
all: 16,
color: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 16.h),
borderColor: Colors.black12,
borderWidth: 1,
radiusMode: SKRadiusMode.safe, // default
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SKit.text('Dashboard', textSize: SKTextSize.s20, fontWeight: FontWeight.w600),
SizedBox(height: 12.h),
SKit.text('All metrics auto-scale.', textSize: SKTextSize.s16),
],
),
);
Border options:
| Parameter | Purpose |
|---|---|
borderColor |
Color for all sides when individual sides are unset |
borderWidth |
Width for all sides (scaled) |
borderTop / borderBottom / borderLeft / borderRight |
Enable a specific side |
borderTopColor / borderBottomColor / borderLeftColor / borderRightColor |
Per-side colors |
borderTopWidth / borderBottomWidth / borderLeftWidth / borderRightWidth |
Per-side widths (scaled) |
// Border on specific sides only
SKit.roundedContainer(
all: 12,
color: Colors.green.shade50,
borderTop: true,
borderBottom: true,
borderColor: Colors.green,
borderWidth: 2,
child: Text('Content'),
);
When individual sides use different colors, Flutter's
BoxDecorationcannot combine a non-uniform border withborderRadius. Scale Kit omits the radius in that case to avoid a render error.
Spacing
SKit.hSpace(8); // horizontal gap
SKit.vSpace(8); // vertical gap
SKit.sSpace(8); // square gap
SKit.hSpaceSize(SKSize.md); // token-based
SKit.vSpaceSize(SKSize.sm);
Shorthand (uses your setDefault* values)
SKit.pad() // padding 16
SKit.margin() // margin 8
SKit.rounded() // safe radius 12
SKit.h() // horizontal gap 8
SKit.v() // vertical gap 8
SKit.rounded takes positional arguments: rounded([size, child, color, borderColor, borderWidth, radiusMode]).
π¨ Design system tokens
Define your scale once, then use names instead of numbers everywhere.
Size enums
SKit.paddingSize(all: SKSize.md, child: widget);
SKit.marginSize(all: SKSize.md, child: widget);
SKit.roundedContainerSize(all: SKSize.lg, color: Colors.blue);
SKit.hSpaceSize(SKSize.md);
SKSize: xs, sm, md, lg, xl, xxl. Text uses SKTextSize: s6 through s52.
Preset scales
setPaddingSizes(SizeValues.material3()); // xs 4, sm 8, md 12, lg 16, xl 24, xxl 32
setPaddingSizes(SizeValues.tailwind());
setTextSizes(TextSizeValues.material3());
ScaleKitDesignValues β compute once per screen
Introduced in Quick start step 3d: define a const token set, call compute()
once per build, and use the results directly. Beyond text*, padding*, spacing*, and
radius*, the class also carries margin*, paddingHorizontal / paddingVertical,
marginHorizontal / marginVertical, width*, height*, and radiusAll.
const cardDesign = ScaleKitDesignValues(
textMd: 14,
textLg: 18,
paddingMd: 16,
marginSm: 8,
radiusMd: 12,
widthMd: 280,
heightMd: 160,
);
final v = cardDesign.compute();
SKMargin(
margin: v.marginSm!,
child: SKContainer(
width: v.widthMd, // already scaled
height: v.heightMd,
padding: v.paddingMd,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: v.borderRadiusMd,
),
child: Text('Card title', style: v.textLg),
),
);
compute() returns a ScaleKitDesignValuesSet where padding* and margin* are
EdgeInsetsGeometry, borderRadius* are BorderRadius, text* are TextStyle, and
width* / height* / spacing* / radius* are plain scaled doubles. Why: one calculation
per screen, centralized tokens, no magic numbers.
Values from compute() are already marked as scaled, so passing them into SK* widgets is safe β
they get resolved, not scaled twice.
SKitTheme/SKitThemeValuesare the deprecated old names forScaleKitDesignValues/ScaleKitDesignValuesSet. Existing code still compiles; use the new names in new code.
π€ Typography & fonts
Text widgets
// Drop-in
SKText('Hello', fontSize: 16, fontWeight: FontWeight.w600);
// Token-based
SKit.text('Hello', textSize: SKTextSize.s16, fontWeight: FontWeight.w600);
// Every Flutter Text attribute, all scaled
SKit.textFull(
'Hello World',
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.blue,
letterSpacing: 0.5,
decoration: TextDecoration.underline,
shadows: [Shadow(color: Colors.black26, offset: Offset(1, 1))],
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
);
// Reusable style object
final headerStyle = SKit.textStyleFull(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
height: 1.5,
);
textFull and textStyleFull cover style (fontSize, fontWeight, fontStyle, color,
backgroundColor, fontFamily), spacing (letterSpacing, wordSpacing, height), decoration,
layout (textAlign, maxLines, overflow, softWrap), and effects (shadows, foreground).
Language-aware fonts
FontConfig is optional and set up once in main() β see
Quick start step 3c for the full example. This section covers how it resolves.
Resolution order for a given locale:
- An exact
setLanguageFontmatch onlanguageCode('ja'β Noto Sans JP) - A
setLanguageGroupFontwhoselanguageCodescontain that code ('ar','fa','ur'β Almarai) - The
setDefaultFontfont - Flutter's default font, if nothing is configured
The result reaches every scaled text path automatically β no wiring at the call site:
Text('Hello', style: TextStyle(fontSize: 16.sp)); // font already applied
SKText('Hello', fontSize: 16); // same
SKit.text('Hello', textSize: SKTextSize.s16); // same
Each setter accepts either a Google Font builder or a bundled family, so mixing is fine:
FontConfig.instance
..setDefaultFont(customFontFamily: 'Cairo')
..setLanguageFont(
const LanguageFontConfig(languageCode: 'en', googleFont: GoogleFonts.inter),
);
google_fontsis not a dependency of this package. You passGoogleFonts.*builders in yourself, so apps that don't need it carry no extra weight.
ποΈ ThemeData integration
Use ResponsiveThemeData.create when you are not using flutter_scale_theme_kit. The context
must sit under ScaleKitBuilder, so wrap it in a Builder:
ScaleKitBuilder(
designWidth: 375,
designHeight: 812,
child: Builder(
builder: (context) {
return MaterialApp(
theme: ResponsiveThemeData.create(
context: context,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const HomePage(),
);
},
),
);
π¨ Look companion (theme kit)
Scale Kit is size. For semantic colors, light/dark, and Material look, add the sibling package
flutter_scale_theme_kit.
Neither package depends on the other at runtime β use either alone, or both together.
If you add Theme Kit, use it for all color (STTheme, context.st) and keep Scale Kit for size.
Merge them by scaling only the text theme:
import 'package:flutter_scale_kit/flutter_scale_kit.dart';
import 'package:flutter_scale_theme_kit/flutter_scale_theme_kit.dart';
import 'core/design.dart'; // final appST = STTheme(colors: ..., radius: ...);
ScaleKitBuilder(
designWidth: 375,
designHeight: 812,
child: Builder(
builder: (context) {
return STThemeModeScope(
builder: (context, mode) {
return MaterialApp(
theme: appST.light.copyWith(
textTheme: appST.light.createResponsiveTextTheme(
appST.light.textTheme,
),
),
darkTheme: appST.dark.copyWith(
textTheme: appST.dark.createResponsiveTextTheme(
appST.dark.textTheme,
),
),
themeMode: mode.mode,
home: const HomePage(),
);
},
);
},
),
);
β οΈ Do not pass
ResponsiveThemeData.create(...)astheme:when using Theme Kit β it replaces the whole theme and drops Theme Kit's card, button, and input styling.
Keep radius aligned across both: setRadiusSizes(SizeValues.custom(md: 12)) and STRadius(md: 12).
Using them in a widget β colors from context.st, sizes from Scale Kit:
SKContainer(
padding: EdgeInsets.all(16), // size β raw number, auto-scaled
decoration: BoxDecoration(
color: context.st.surface, // look β Theme Kit
borderRadius: BorderRadius.circular(12), // size β clamped via rSafe
border: Border.all(color: context.st.border),
),
child: SKText('Revenue', fontSize: 16, color: context.st.text),
);
Light/dark toggling belongs to Theme Kit too:
IconButton(
icon: Icon(context.stMode.isDark(context) ? Icons.light_mode : Icons.dark_mode),
onPressed: () => context.stMode.toggle(),
);
π Copy a working setup: the starter app walks through both packages in four files β
pubspec.yaml, colors, Scale Kit config,main.dart, and a real dashboard screen β plus the rules that keep the two from fighting.
Theme Kit skill: npx skills add fodilfliti/flutter_scale_theme_kit
π± Responsive widgets & values
Responsive values β for numbers
Grid columns, item counts, spacing. Prefer this over a responsive builder when only a number changes.
final columns = SKit.responsiveInt(
context: context,
mobile: 2, // required base
tablet: 4, // optional
desktop: 6, // optional
mobileLandscape: 3, // optional
);
GridView.count(crossAxisCount: columns, children: [...]);
final spacing = SKit.responsiveDouble(
context: context,
mobile: 8.0,
tablet: 16.0,
desktop: 24.0,
);
Always pass context so values refresh on resize and rotation.
SKResponsive β different widget per device
Use when the layout structure changes: a navigation rail instead of a bottom bar, a split view instead of a list.
SKResponsive(
mobile: (_) => const BottomNavShell(),
mobileLandscape: (_) => const NavigationRailShell(),
tablet: (_) => const NavigationRailShell(),
tabletLandscape: (_) => const SplitViewShell(),
desktop: (_) => const SplitViewShell(),
);
SKResponsiveBuilder β two styles
Style 1 β one builder that receives device and orientation:
SKResponsiveBuilder(
builder: (context, device, orientation) {
if (device == DeviceType.mobile && orientation == Orientation.landscape) {
return const MobileLandscapeView();
}
if (device == DeviceType.tablet) return const TabletView();
return const DesktopView();
},
);
Style 2 β separate builders, like SKResponsive:
SKResponsiveBuilder(
mobile: (_) => const MobileView(),
tablet: (_) => const TabletView(),
desktop: (_) => const DesktopView(),
);
If you supply both, device-specific builders win over the general builder.
Fallback rules
Shared by every responsive widget and value helper:
- Device: desktop β tablet β mobile; tablet β mobile
- Orientation: landscape β portrait of the same device
mobileLandscapeβ falls back tomobiletabletLandscapeβ falls back totabletβmobileLandscapeβmobile
π₯οΈ Desktop & web freedom
Desktop and web are harder than phones: users resize windows constantly, ultrawide monitors sit next to tiny browser panes, and some screens read better borrowing a tablet or even a mobile layout. Scale Kit gives you switches instead of one fixed breakpoint.
Global locks
ScaleKitBuilder(
designWidth: 375,
designHeight: 812,
lockDesktopPlatforms: true, // pin desktop/web to a chosen breakpoint
lockDesktopAsTablet: true, // ...specifically the tablet branch
// lockDesktopAsMobile: true,
child: MaterialApp(home: HomePage()),
);
Locks still respect live window width. lockDesktopAsMobile only routes to the mobile branch when
the window is actually below the mobile breakpoint β a wide window keeps the desktop builder.
Per-widget overrides
Every responsive widget and value helper accepts the same flags, so one route can deviate:
SKResponsiveBuilder(
desktop: (_) => DesktopDashboard(),
tablet: (_) => TabletDashboard(),
mobile: (_) => MobileDashboard(),
lockDesktopAsTablet: true, // reuse tablet on smaller windows
deviceTypeOverride: DeviceType.desktop, // or force one classification
desktopAs: DesktopAs.tablet, // reuse tablet values without the global lock
);
Platform branching inside a breakpoint
Two devices can share a width and still deserve different chrome:
Widget buildToolbar(BuildContext context) {
if (context.isAndroidPlatform) return const AndroidToolbar();
if (context.isIOSPlatform) return const IOSToolbar();
if (context.isDesktopPlatform) return const DesktopToolbar();
if (context.isWebPlatform) return const WebToolbar();
return const UniversalToolbar();
}
Width-only decisions
Tweak spacing for ultrawide monitors without swapping the widget tree:
if (context.isDesktopAtLeastTablet) {
// widen the content column, keep the same layout
}
Available: context.isDesktopSize, isDesktopAtLeastTablet, isDesktopAtLeastDesktop,
isDesktopMobileSize, isDesktopTabletSize, isDesktopDesktopOrLarger.
CSS-like behavior
On Android and iOS, devices classify as mobile or tablet by width only β desktop logic does not
apply. On desktop (width β₯ 1200 by default), desktop values apply, and desktopAs lets you reuse
tablet or mobile values to mimic CSS breakpoints.
// 2 / 4 / 8 columns, but desktop reuses the tablet count
final cols = SKit.responsiveInt(
context: context,
mobile: 2,
tablet: 4,
desktop: 8,
desktopAs: DesktopAs.tablet,
);
π Device queries
Context extensions
// Layout structure β respects overrides and desktop locks
context.isMobile context.isTablet context.isDesktop
// OS chrome β web and Wasm safe
context.isMobilePlatform context.isAndroidPlatform context.isIOSPlatform
context.isDesktopPlatform context.isWebPlatform
// Width only
context.screenSizeClass context.isMobileSize context.isTabletSize context.isDesktopSize
Scaling helpers are on context too:
Container(
padding: context.scalePadding(start: 24, end: 12, vertical: 16),
margin: context.scaleMargin(all: 8),
decoration: BoxDecoration(
borderRadius: context.scaleBorderRadius(all: 12),
),
child: const Text('Content'),
);
Also: context.scaleWidth(200), context.scaleHeight(100), context.scaleFontSize(16),
context.scaleSize(12).
Choosing the classification source
context.isTypeOfMobile(source: DeviceClassificationSource.responsive); // default
context.isTypeOfTablet(source: DeviceClassificationSource.platform);
context.isTypeOfDesktop(source: DeviceClassificationSource.size, includeWeb: true);
DeviceMetricsMixin β no BuildContext needed
class DashboardState extends State<Dashboard>
with DeviceMetricsMixin<Dashboard> {
@override
Widget build(BuildContext context) {
if (isDesktop) return const DesktopDashboard();
if (isTablet) return const TabletDashboard();
return const MobileDashboard();
}
@override
void onDeviceMetricsChanged(Size previous, Size current) {
debugPrint('Metrics changed: $current');
}
}
Exposes logicalScreenSize, devicePixelRatio, isMobile, isTablet, isDesktop,
isMobilePlatform, isDesktopPlatform, isWeb, isAndroidPlatform, isIOSPlatform, and the
onDeviceMetricsChanged hook. Values stay in sync with WidgetsBinding.
ScaleManager β direct access
final scale = ScaleManager.instance;
scale.getWidth(200); scale.getHeight(100);
scale.getFontSize(16); scale.getRadius(12);
scale.screenWidth; scale.screenHeight;
scale.orientation; scale.deviceType;
scale.statusBarHeight; scale.bottomBarHeight;
scale.safeAreaHeight; scale.textScaleFactor;
scale.platformCategory; scale.screenSizeClass;
π Breakpoints & size classes
Defaults are 600 / 1200 / 1600 / 1920. Override them to match your design system:
const customBreakpoints = ScaleBreakpoints(
mobileMaxWidth: 540,
tabletMaxWidth: 1100,
desktopMaxWidth: 1650,
largeDesktopMaxWidth: 2200,
);
ScaleKitBuilder(
breakpoints: customBreakpoints,
designWidth: 375,
designHeight: 812,
child: MaterialApp(home: HomePage()),
);
DeviceSizeClass then reports against your thresholds: smallMobile, mobile, largeMobile,
tablet, largeTablet, desktop, largeDesktop, extraLargeDesktop.
ποΈ Scale limits
minScale and maxScale are guard rails that stop a layout from exploding on a big screen. You
can leave them unset β Scale Kit picks a range per device type and orientation.
Why they matter
Design 375Γ812 (iPhone 13 mini) shown on an iPad Pro portrait (1024Γ1366):
Without limits:
scaleWidth = 1024 / 375 = 2.73x β a 100px button becomes 273px (too big)
With minScale 0.8, maxScale 1.2:
clamped = clamp(2.73, 0.8, 1.2) = 1.2x β 100px button becomes 120px β
Override recipes
ScaleKitBuilder(
designWidth: 375,
designHeight: 812,
minScale: 0.9,
maxScale: 1.2,
child: MaterialApp(home: HomePage()),
);
| Use case | minScale |
maxScale |
Why |
|---|---|---|---|
| Strict design match | 0.95 |
1.05 |
Locks UI to spec for brand or compliance work |
| Extra accessibility | 0.6 |
2.0 |
Lets system text scaling dominate |
| Locked tablet range | 0.9 |
1.2 |
Makes tablets feel like large phones |
| Desktop cap | 0.7 |
1.3 |
Stops desktop widgets from getting huge |
Good starting point for a gentle tablet clamp that leaves phones untouched: 0.9 / 1.4.
π Orientation boosts
Gentle multipliers applied after clamping, so content stays readable when a device rotates. Limits always win first.
finalSize = designValue Γ clampedScale Γ orientationSizeBoost
finalFontSize = designFontSize Γ clampedScale Γ orientationFontBoost Γ systemTextScale
| Device | Portrait | Landscape | Why |
|---|---|---|---|
| Mobile | 1.0Γ | 1.2Γ | Wider view needs breathing room |
| Tablet | 1.0Γ | 1.2Γ | Horizontal layouts feel airy |
| Desktop | 1.0Γ | 1.0Γ | Desktops rarely need a boost |
Fonts and sizes use separate multipliers, so you can raise readability without inflating containers.
ScaleKitBuilder(
designWidth: 375,
designHeight: 812,
autoScaleLandscape: true, // default
autoScalePortrait: false, // default
mobileLandscapeFontBoost: 1.2,
mobileLandscapeSizeBoost: 1.2,
);
Worked example β iPhone 14 landscape (852Γ390) against a 375Γ812 design:
// 1. Raw scale: width 2.27x 2. Auto limits: clamped to 1.25x 3. Boost: 1.2x
16.sp; // 16 Γ 1.25 Γ 1.2 = 24.0
100.w; // 100 Γ 1.25 Γ 1.2 = 150.0
Autoscale enabled
|
Autoscale disabled
|
π§ Auto-configuration
Scale Kit detects the environment and picks limits and boosts for you. About 95% of projects never override anything.
What it detects: device type (phone, tablet, desktop, browser), width, height, orientation, aspect ratio, pixel density, plus special cases β foldables like the Galaxy Fold, ultrawide monitors above 2560px, small windows under 800px, and tall 21:9 notched screens.
What it chooses:
| Device | Portrait limits | Landscape limits |
|---|---|---|
| Mobile | 0.85 β 1.15Γ | 0.85 β 1.25Γ |
| Tablet | 0.8 β 1.3Γ | 0.75 β 1.4Γ |
| Desktop | 0.7 β 1.8Γ | 0.6 β 2.0Γ |
It also adapts to your design intent: a mobile design shown on a tablet widens the allowed range, while on desktop it caps upscaling instead. Resizable windows are handled continuously.
When to override: only for tighter compliance than the defaults (for example Β±5% variance on brand-critical screens), or a deliberately different feel.
β‘ Performance
| Optimization | Result |
|---|---|
Direct math for scalars (.w, .h, .sp, .r) |
~3x faster than a map cache |
| Integer cache keys + flyweight reuse | ~10x faster than recalculating |
Listens to MediaQuery.sizeOf / orientationOf only |
~80% fewer rebuilds |
| LRU cache capped at 2000 entries | No unbounded memory growth |
Hybrid caching. Scalars skip the map entirely and use precomputed multipliers. Remaining cache
operations use integer keys instead of strings. A value like 12.sp is computed and marked once,
then served from cache.
Memory safety. The cache evicts least-recently-used entries at the 2000 cap, and both the cache and the auto-scale tracker clear on resize or rotation.
Rebuild optimization. Keyboard insets (viewInsets) and system gesture insets do not
trigger rebuilds. Tune sensitivity with sizeChangeThreshold β default 5% on mobile and tablet,
0% on desktop and web:
ScaleKitBuilder(
designWidth: 375,
designHeight: 812,
sizeChangeThreshold: 0.01, // rebuild after a 1% change
);
Architecture: ScaleManager (singleton, device state) β ScaleValueFactory (creates and
resolves values) β ScaleValueCache (flyweight, LRU-capped).
π§ͺ Testing
Widgets that use .w, .sp, or SK* need a ScaleKitBuilder ancestor:
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_scale_kit/flutter_scale_kit.dart';
Future<void> pumpScaled(WidgetTester tester, Widget child) {
return tester.pumpWidget(
MaterialApp(
home: ScaleKitBuilder(
designWidth: 375,
designHeight: 812,
designType: DeviceType.mobile,
child: Scaffold(body: child),
),
),
);
}
void main() {
testWidgets('card renders', (tester) async {
await pumpScaled(tester, const ProductCard());
expect(find.byType(ProductCard), findsOneWidget);
});
}
Test a specific screen size by setting the surface before pumping:
tester.view.physicalSize = const Size(1200, 800);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.reset);
Want raw numbers in a test? Use ScaleKitBuilder(enabled: false, ...).
π Enable/disable at runtime
Useful for comparing Scale Kit against vanilla Flutter sizing:
final enabled = ValueNotifier<bool>(true);
ScaleKitBuilder(
designWidth: 375,
designHeight: 812,
enabled: enabled.value, // initial
enabledListenable: enabled, // runtime switch
child: MaterialApp(home: const HomePage()),
);
enabled.value = false; // .w / .h / .sp now return raw values
Package enabled (scaling on)
|
Package disabled (raw Flutter)
|
Toggle both live in the example app via the tune icon.
π² Device Preview integration
If you use device_preview, share its simulated
platform so detection stays correct inside the preview surface:
import 'package:device_preview/device_preview.dart';
void main() {
ScaleManager.setDevicePreviewPlatformGetter((context) {
try {
return DevicePreview.platformOf(context);
} catch (_) {
return null; // fall back to normal detection when preview is off
}
});
runApp(const MyApp());
}
Optional β the package works fine without it.
π€ AI agent skill
Your coding agent can use Scale Kit like a design-system expert β init ScaleKitBuilder, use
tokens, avoid double-scaling. It follows the open Agent Skills format, so
it works in Cursor, Claude Code, GitHub Copilot, Codex, OpenCode, Windsurf, Gemini CLI, and
other compatible tools.
# Current project (shared with the team)
npx skills add fodilfliti/flutter_scale_kit
# All projects on this machine
npx skills add fodilfliti/flutter_scale_kit -g
Then ask: "Init Flutter Scale Kit with defaults."
The agent checks for scale_kit.yaml in your project. If it's missing, it asks once which style
you want (design_system recommended, or drop_in / extensions / hybrid), saves the file,
adds the dependency, creates lib/core/scale_kit.dart, and wraps main.dart. Later chats follow
that style automatically. Say "defaults" to skip the interview.
If your app also has flutter_scale_theme_kit, the skill uses it for look and merges
createResponsiveTextTheme onto appST.light / appST.dark.
Manual install: copy skills/flutter-scale-kit/ into your agent's skills folder
(.agents/skills/, .claude/skills/, or ~/.cursor/skills/).
π Migrating from flutter_screenutil
Most screenutil code keeps working after a mechanical swap.
1. Replace the root widget:
// Before
ScreenUtilInit(
designSize: const Size(375, 812),
minTextAdapt: true,
builder: (context, child) => MaterialApp(home: const HomePage()),
);
// After
ScaleKitBuilder(
designWidth: 375,
designHeight: 812,
designType: DeviceType.mobile,
child: MaterialApp(home: const HomePage()),
);
2. Change .r to .rSafe. This is the one real trap. In screenutil, .r is a general scale
factor. Here, .r is fully scaled with no clamp, which can over-round a card on a large screen.
.rSafe is the closest match.
3. That's it. .w, .h, .sp, .sw, .sh behave the same.
| flutter_screenutil | Scale Kit |
|---|---|
designSize: Size(375, 812) |
designWidth: 375, designHeight: 812 |
builder: (context, child) => ... |
child: ... |
minTextAdapt, splitScreenMode |
Automatic |
.r |
.rSafe |
ScreenUtil().setWidth(x) |
x.w |
ScreenUtil().screenWidth |
ScaleManager.instance.screenWidth |
Optionally continue by swapping widgets (Container β SKContainer) and removing the extension
suffixes as you go β SKContainer(width: 200), not SKContainer(width: 200.w).
Full guide with strategies: doc/migration-screenutil.md
π Advanced guides
Everything you need is above. These go deeper on specific topics:
| Guide | Covers |
|---|---|
| π Starter app | A real app in 4 files with Scale Kit + Theme Kit, and the same card in all four usage styles |
| API reference | Every extension, widget, helper, property, and enum |
| Scaling engine | Full formulas, per-device boost tuning, benchmark detail |
| Design system | Deeper token and font recipes |
| Desktop & web | Every lock, override, and fallback combination |
| Recipes | Cards, grids, RTL, testing patterns |
| Migration guide | Three migration strategies, gotcha list |
β FAQ
Why choose Scale Kit over flutter_screenutil?
Compute-once patterns, automatic font selection by language, orientation-aware scaling controls,
drop-in widgets that remove .w noise, and a runtime toggle to compare scaled vs raw Flutter. If
your current setup works, flutter_screenutil remains an excellent choice.
Do I have to use SK* widgets?
No. Extensions alone work fine, and you can mix both in the same file.
Does it work on web and desktop?
Yes, all six platforms. Platform detection is web and Wasm safe (it checks kIsWeb first).
How do I disable scaling to compare with raw Flutter?
ScaleKitBuilder(enabled: false), or pass enabledListenable for a runtime switch.
Can I control autoscale separately for portrait and landscape?
Yes. autoScaleLandscape (default true) and autoScalePortrait (default false), plus
per-device font and size boosts.
Do all TextStyles get my configured font automatically?
Yes, once FontConfig is set. Extensions, SKText, SKit.text*, and responsive themes all apply
it. Without configuration, Flutter's default font is used.
How do I test widgets that use .w or SK*?
Wrap them in ScaleKitBuilder inside pumpWidget β see Testing.
Why is borderRadius dropped when I use different border colors per side?
A Flutter limitation: BoxDecoration cannot combine a non-uniform border with borderRadius.
Scale Kit omits the radius in that case to avoid a render error.
Will this increase my app size?
Barely. Only code you reference from lib/ is compiled in β the example app, screenshots, and docs
in the archive are never bundled into your build. google_fonts is not a dependency either; you
pass builders in yourself, so apps that don't need it carry no extra weight.
π Support
If Scale Kit helps you, please like it on pub.flutter-io.cn β it genuinely improves discoverability and ranking.
- β Star on GitHub
- π Buy me a coffee β supports continued development
- π Report a bug
- π‘ Suggest a feature, or share the package with other developers
Contributing
Contributions are welcome. Please open an issue or submit a pull request.
Acknowledgements
Huge thanks to the authors and contributors of flutter_screenutil and similar responsive
packages. We used them extensively, learned from their ideas, and built Scale Kit as an alternative
tuned for our own apps' performance and developer experience.
License
MIT β see LICENSE.