Responsive Scaler
A Flutter package that offers a simple, automatic, and boilerplate-free way to make your app's UI (text, icons, spacing) responsive across different screen sizes.
π Why Responsive Scaler?
Ever feel like making your app responsive involves too much setup?
Many solutions, like screen_util
, are powerful but require you to wrap every single value with special units (.sp
, .w
, .h
).
Others like responsive_framework
rely on breakpoint-based scaling, which can cause sudden jumps in size.
Responsive Scaler takes a different approach:
- Zero Boilerplate for Text: Initialize once, and all your standard
Text
widgets become responsive automatically. - Developer in Control: You define your app's
designWidth
, giving you a predictable baseline for scaling. - Smooth, Linear Scaling: No jarring jumps between breakpoints. UI elements scale smoothly as the screen size changes.
- Respects Accessibility: Automatically honors the user's system-level font size settings while providing a safeguard against excessively large text.
- Easy Integration: Designed to be dropped into existing production apps with minimal code changes.
π Comparison with Other Packages
Feature | Responsive Scaler | ScreenUtil | Responsive Framework |
---|---|---|---|
Text Scaling | β
Automatic for all Text widgets (no boilerplate) |
β Manual (16.sp ) everywhere |
β οΈ Breakpoints only (not automatic) |
Design Width | β
Developer defines once (designWidth ) |
β
Developer defines (designSize ) |
β Breakpoints only (no single baseline) |
Boilerplate | β Minimal (init once, wrap MaterialApp) | β High (every value wrapped) | β οΈ Medium (define breakpoints, wrap UI in ResponsiveWrapper ) |
Scaling Style | β Smooth linear scaling | β Linear scaling but manual | β Step jumps at breakpoints |
Accessibility Respect | β
Built-in (TextScaler + clamp) |
β Developer must handle manually | β οΈ Limited (breakpoints don't always sync with accessibility) |
Best Use Case | Apps that want drop-in, automatic responsiveness | Pixel-perfect manual scaling | Apps with different layouts per screen width |
π The One-Shot Combo: Scaler + Framework
responsive_scaler
handles scaling of text, icons, and spacing automatically.
responsive_framework
handles layout changes at breakpoints (like moving widgets around, showing sidebars, or swapping grids).
Together, they cover both sides of responsiveness:
β
Scaler = Smooth scaling for text, icons, and spacing
β
Framework = Adaptive layouts at breakpoints
Example: Responsive Login Page
Here's how the combo shines in a real-world layout.
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'package:responsive_scaler/responsive_scaler.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
ResponsiveScaler.init(designWidth: 390, maxAccessibilityScale: 1.5);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Responsive Login Demo',
theme: ThemeData(primarySwatch: Colors.blue, useMaterial3: true),
builder: (context, child) {
final scaledChild = ResponsiveScaler.scale(
context: context,
child: child!,
);
return ResponsiveBreakpoints.builder(
breakpoints: [
const Breakpoint(start: 0, end: 450, name: MOBILE),
const Breakpoint(start: 451, end: 800, name: TABLET),
const Breakpoint(start: 801, end: 1920, name: DESKTOP),
],
child: scaledChild,
);
},
home: const LoginPage(),
);
}
}
class LoginPage extends StatelessWidget {
const LoginPage({super.key});
@override
Widget build(BuildContext context) {
bool isMobile = ResponsiveBreakpoints.of(context).smallerThan(TABLET);
return Scaffold(
body: SingleChildScrollView(
child: Center(
child: Padding(
padding: EdgeInsets.all(ResponsiveSpacing.wLarge),
child: ResponsiveRowColumn(
layout: isMobile
? ResponsiveRowColumnType.COLUMN
: ResponsiveRowColumnType.ROW,
rowMainAxisAlignment: MainAxisAlignment.center,
columnCrossAxisAlignment: CrossAxisAlignment.center,
children: [
// Illustration
ResponsiveRowColumnItem(
rowFlex: 1,
child: Padding(
padding: EdgeInsets.only(
bottom: isMobile
? ResponsiveSpacing.hLarge
: 0,
right: isMobile
? 0
: ResponsiveSpacing.wLarge,
),
child: SvgPicture.asset(
"assets/login_illustration.svg",
height: isMobile
? scaled(100)
: scaled(200),
),
),
),
// Login Form
ResponsiveRowColumnItem(
rowFlex: 1,
child: _buildLoginForm(context),
),
],
),
),
),
),
);
}
Widget _buildLoginForm(BuildContext context) {
return SingleChildScrollView(
child: Container(
padding: EdgeInsets.all(ResponsiveSpacing.wMedium),
//constraints: const BoxConstraints(maxWidth: 400),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 12,
offset: const Offset(0, 6),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text("Welcome Back π", style: AppTextStyles.headlineMedium),
SizedBox(height: ResponsiveSpacing.hMedium),
// Email
TextField(
decoration: InputDecoration(
labelText: "Email",
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
SizedBox(height: ResponsiveSpacing.hMedium),
// Password
TextField(
obscureText: true,
decoration: InputDecoration(
labelText: "Password",
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
SizedBox(height: ResponsiveSpacing.hLarge),
// Button
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(
vertical: ResponsiveSpacing.hMedium,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text("Login", style: AppTextStyles.bodyLarge),
),
),
],
),
),
);
}
}
What happens here:
- Responsive Framework changes the layout from column (mobile) to row (desktop)
- Responsive Scaler ensures all text, icons, and spacing scale smoothly on every screen size
- Best of both worlds: Layout adaptation + automatic scaling
Installation
Add this to your package's pubspec.yaml
file:
dependencies:
responsive_scaler: ^latest_version
Then, run flutter pub get
in your terminal.
How to Use
Step 1: Initialize the Scaler
In your lib/main.dart
file, call ResponsiveScaler.init()
before runApp()
. This is the most important step.
Provide the designWidth
of the device you are basing your UI on. For example, if you are making and testing the app on Pixel 9 which has a width of 412
.
// filepath: lib/main.dart
import 'package:flutter/material.dart';
import 'package:responsive_scaler/responsive_scaler.dart';
void main() {
// Initialize the scaler with your design width.
ResponsiveScaler.init(
designWidth: 393,
// Optional: Set min/max scale factors for UI elements.
minScale: 0.8,
maxScale: 1.2,
// Optional: Clamp the final text size for accessibility to prevent it from getting too large.
maxAccessibilityScale: 1.8,
);
runApp(const MyApp());
}
Step 2: Apply Scaling to the App
In your MyApp
widget, use the MaterialApp.builder
property to wrap your app with ResponsiveScaler.scale
.
// filepath: lib/main.dart
// ... (main function from above)
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Responsive Scaler Example',
// Use the builder to apply scaling to the entire app.
builder: (context, child) {
// The scale method now reads the global config set in main().
return ResponsiveScaler.scale(context: context, child: child!);
},
home: const MyHomePage(),
);
}
}
Step 3: Build Your UI Naturally
Now you can build your UI as you normally would. All text, and any sizes calculated with the helpers, will be responsive.
IMPORTANT
: Don't use const Text() here, as const widgets won't scale properly!!.
Text Scaling (Automatic)
All Text
widgets are now automatically responsive.
// This scales automatically based on the init() config.
// IMPORTANT: Don't use const Text() here, as const widgets don't rebuild with scaling.
Text(
'Headline from AppTextStyles',
style: AppTextStyles.headlineMedium,
)
// Even manually styled text scales automatically.
// IMPORTANT: Don't use const Text() here, as const widgets don't rebuild with scaling.
Text(
'Manually styled text also works!',
style: TextStyle(fontSize: 16),
)
Responsive Icons, Spacing, and Sizes
Use the helper functions for consistent scaling on non-text elements.
Column(
children: [
// Responsive Icon Size
Icon(
Icons.favorite,
size: AppIconSizes.size32(context),
),
// Responsive Spacing
SizedBox(height: ResponsiveSpacing.hMedium),
// Responsive Custom Size (e.g., for an SVG)
SvgPicture.asset(
'assets/star.svg',
width: scaled(50),
),
],
)
ResponsiveSpacing Usage Guide
The ResponsiveSpacing
class provides a set of pre-calculated, responsive spacing constants that are based on the screen's height and width. This allows you to create vertical and horizontal gaps in your UI that adapt gracefully to different device sizes.
π Height-Based Spacing
Property | Description | Dart Example Usage |
---|---|---|
ResponsiveSpacing.hXSmall |
Extra Small (0.5% of screen height) | SizedBox(height: ResponsiveSpacing.hXSmall) |
ResponsiveSpacing.hSmall |
Small (1% of screen height) | SizedBox(height: ResponsiveSpacing.hSmall) |
ResponsiveSpacing.hMedium |
Medium (1.5% of screen height) | SizedBox(height: ResponsiveSpacing.hMedium) |
ResponsiveSpacing.hLarge |
Large (2% of screen height) | SizedBox(height: ResponsiveSpacing.hLarge) |
ResponsiveSpacing.hXLarge |
Extra Large (3% of screen height) | SizedBox(height: ResponsiveSpacing.hXLarge) |
ResponsiveSpacing.hCustom(factor) |
Custom height (factor * screenHeight ) |
SizedBox(height: ResponsiveSpacing.hCustom(0.5)) // 50% of screen height |
π Width-Based Spacing
Property | Description | Dart Example Usage |
---|---|---|
ResponsiveSpacing.wXSmall |
Extra Small (1% of screen width) | SizedBox(width: ResponsiveSpacing.wXSmall) |
ResponsiveSpacing.wSmall |
Small (2% of screen width) | SizedBox(width: ResponsiveSpacing.wSmall) |
ResponsiveSpacing.wMedium |
Medium (4% of screen width) | SizedBox(width: ResponsiveSpacing.wMedium) |
ResponsiveSpacing.wLarge |
Large (6% of screen width) | SizedBox(width: ResponsiveSpacing.wLarge) |
ResponsiveSpacing.wXLarge |
Extra Large (8% of screen width) | SizedBox(width: ResponsiveSpacing.wXLarge) |
ResponsiveSpacing.wCustom(factor) |
Custom width (factor * screenWidth ) |
SizedBox(width: ResponsiveSpacing.wCustom(0.5)) // 50% of screen width |
Example Usage
Column(
children: [
const Text('Title'),
// Medium vertical space
SizedBox(height: ResponsiveSpacing.hMedium),
const Text('Subtitle'),
// Custom vertical space (5% of screen height)
SizedBox(height: ResponsiveSpacing.hCustom(0.05)),
const Text('Body Text...'),
],
);
Row(
children: [
const Icon(Icons.info),
// Small horizontal space
SizedBox(width: ResponsiveSpacing.wSmall),
const Expanded(child: Text('Information here')),
],
);
Scaling Sizes Usage Guide
The scaling system provides a consistent way to define sizes that automatically scale up or down depending on the screen size. This ensures your UI elements remain proportional across devices.
Scaled Sizes
The main method is:
scaled(baseSize)
Example Usage
Container(
width: scaled(50),
height: scaled(50),
color: Colors.blue,
);
SvgPicture.asset(
'assets/star.svg',
width: scaled(50),
);
Pre-defined Text Styles
The package includes a set of Material 3-aligned text styles in AppTextStyles
. Using these helps maintain a consistent look and feel. Since text scaling is automatic, you use them just like you normally would.
import 'package:responsive_scaler/responsive_scaler.dart';
Text(
'Welcome to my app!',
style: AppTextStyles.displaySmall, // This will be scaled automatically
)
Customizing Styles with copyWith
Need to change the color or make a specific text bold? You can easily customize the pre-defined styles using the .copyWith()
method. This gives you full control while reusing the base font size and weight.
Text(
'This is an important title!',
// Start with a base style and then customize it
style: AppTextStyles.titleLarge.copyWith(
color: Colors.deepPurple,
fontWeight: FontWeight.w900, // Make it extra bold
),
)
AppTextStyles Class Guide
Display Styles
Display styles are the largest text on the screen, reserved for short, important text or numerals. They are best used sparingly.
Style Name | Font Size (Base) | Font Weight | Typical Use Case |
---|---|---|---|
displayLarge |
57 | w400 (Normal) |
Hero text on a landing page, key metric on a dashboard. |
displayMedium |
45 | w400 (Normal) |
A secondary, but still very prominent, piece of text. |
displaySmall |
36 | w400 (Normal) |
Important but smaller display text. |
Headline Styles
Headlines are suitable for high-emphasis text that is shorter than a full paragraph. They work well for page and section titles.
Style Name | Font Size (Base) | Font Weight | Typical Use Case |
---|---|---|---|
headlineLarge |
32 | w700 (Bold) |
Main page titles. |
headlineMedium |
28 | w600 (Semi-Bold) |
Sub-sections or slightly less important headlines. |
headlineSmall |
24 | w600 (Semi-Bold) |
Smaller section titles or prominent list item titles. |
Title Styles
Titles are smaller than headlines and are typically used for medium-emphasis text that introduces a component or a block of content.
Style Name | Font Size (Base) | Font Weight | Typical Use Case |
---|---|---|---|
titleLarge |
22 | w500 (Medium) |
Titles of dialogs, cards, or important components. |
titleMedium |
18 | w500 (Medium) |
Subtitles within cards, titles for list items. |
titleSmall |
14 | w500 (Medium) |
Less prominent titles, field labels in forms. |
Body Styles
Body styles are used for the main text content of your application, such as descriptions, articles, and long-form text.
Style Name | Font Size (Base) | Font Weight | Typical Use Case |
---|---|---|---|
bodyLarge |
16 | w400 (Normal) |
The primary text style for readability in long paragraphs. |
bodyMedium |
14 | w400 (Normal) |
Default body text, captions, or secondary information. |
bodySmall |
12 | w400 (Normal) |
Tertiary text, legal disclaimers, or fine print. |
Label Styles
Label styles are used for utility text inside components like buttons, navigation items, or tags. They are typically short and actionable.
Style Name | Font Size (Base) | Font Weight | Typical Use Case |
---|---|---|---|
labelLarge |
14 | w500 (Medium) |
Text inside large buttons. |
labelMedium |
12 | w500 (Medium) |
Text inside standard buttons, navigation bar items. |
labelSmall |
11 | w500 (Medium) |
Text for small buttons, chips, or overline text. |
Custom Weight-Based Styles
For situations where the pre-defined styles don't fit, you can use these helper methods to create a TextStyle
with a specific font weight. You provide the fontSize
you want, and the method applies the correct weight.
Note: The fontSize
you provide to these methods will still be scaled automatically by the ResponsiveScaler
package.
Method Name | Font Weight | Example Usage |
---|---|---|
thin(size) |
w100 (Thin) |
AppTextStyles.thin(20) |
extraLight(size) |
w200 (Extra Light) |
AppTextStyles.extraLight(20) |
light(size) |
w300 (Light) |
AppTextStyles.light(20) |
regular(size) |
w400 (Regular) |
AppTextStyles.regular(16) |
medium(size) |
w500 (Medium) |
AppTextStyles.medium(16) |
semiBold(size) |
w600 (Semi-Bold) |
AppTextStyles.semiBold(18) |
bold(size) |
w700 (Bold) |
AppTextStyles.bold(18) |
extraBold(size) |
w800 (Extra Bold) |
`AppTextStyles.extraBold(22) |
How It Works (Under the Hood)
The logic is based on a simple, powerful idea: calculate a single scale factor and apply it consistently.
1. Global Initialization
When you call ResponsiveScaler.init(designWidth: 393, ...)
, the package stores your designWidth
, minScale
, maxScale
, and maxAccessibilityScale
values in global static variables. This configuration is done only once and is available throughout the app's lifecycle.
2. The Scaling Calculation
Every time a responsive size is needed, the getScaleFactor
function is called. It performs two key calculations:
A. Ratio Calculation
It calculates a raw scale factor by dividing the device's current width by your design width.
scale = currentWidth / designWidth
B. Clamping for Control
An unconstrained ratio can lead to ridiculously large or tiny UI elements. To prevent this, the raw scale
is "clamped" to stay within your minScale
and maxScale
limits.
finalScale = min(maxScale, max(minScale, rawScale))
Example Walkthrough
Let's assume you did this in main.dart
:
ResponsiveScaler.init(designWidth: 390, minScale: 0.8, maxScale: 1.5);
And you want an icon with a base size of 30
.
-
On a small phone (width: 320px):
scale = 320 / 390
which is~0.82
.- This is within the clamp range
[0.8, 1.5]
. - Final icon size =
30 * 0.82
=24.6
. The icon shrinks.
-
On a large tablet (width: 900px):
scale = 900 / 390
which is~2.3
.- This is outside the clamp range. It gets clamped down to the
maxScale
. - Final icon size =
30 * 1.5
=45.0
. The icon grows, but it doesn't become excessively large.
3. Applying the Scale Factor
-
For Text: The
ResponsiveScaler.scale()
widget wraps your app in a newMediaQuery
. It creates a customTextScaler
by first multiplying the clampedfinalScale
with the user's system accessibility font size. To prevent text from becoming unreadably large, this combined value is then clamped again using themaxAccessibilityScale
you provided ininit()
. This final, safe value is used to draw allText
widgets. -
For Icons and Sizes: When you call
scaled(50)
, it simply fetches the same clampedfinalScale
and returns50 * finalScale
. This guarantees that your icons and spacing scale with the exact same logic as your