Inspired by Chapter 23 of "Flutter Engineering" by Majid Hajian - A Flutter Book Club Deep Dive
You know that moment when you're reading a technical book and suddenly everything just clicks? That's exactly what happened during our Flutter Book Club session on Chapter 23: Responsive UI Techniques from Majid's "Flutter Engineering."
As Majid walked us through the principles of responsive design from MediaQuery to LayoutBuilder, from breakpoints to orientation handling; I found myself nodding along, thinking about all the times I'd wrestled with screen size variations in my own apps. The chapter was brilliant, comprehensive, and thorough. But there was something nagging at me.
The theory was solid. The patterns were clear. But the implementation? That's where things got repetitive, verbose and honestly, a bit tedious.
Flutter already provides powerful responsive tools like MediaQuery and LayoutBuilder, but scaling responsive UI across large applications often introduces repetitive boilerplate, duplicated breakpoint logic, inconsistent sizing patterns, and difficult design-to-code translation.
That realization became the birthplace of Vize.
But before we dive into the solution, let's talk about why this even matters. In today's diverse device landscape, responsive design isn't just a nice-to-have feature, it's a fundamental requirement for survival.
Users expect seamless experiences whether they're using a compact smartphone, a foldable device, a tablet, or even a desktop browser. And they're ruthless when those expectations aren't met.
Why Responsiveness is Non-Negotiable
The Multi-Device Reality (And Why You Can't Ignore It)
Picture this: You've just launched your beautifully crafted Flutter app. The screenshots look stunning on your iPhone 13. You're proud. You hit publish.
Then the reviews start rolling in:
"Buttons are cut off on my phone!" - iPhone SE user
"Why is everything so tiny?" - iPad user
"This looks terrible in landscape!" - Every gamer ever
"The text is unreadable!" - Anyone over 40
Welcome to the multi-device nightmare that every Flutter developer faces. Modern applications run on devices with vastly different screen specifications:
- Screen sizes range from compact 4-inch phones to expansive 13-inch tablets and desktop monitors
- Aspect ratios vary from traditional 16:9 to ultra-wide 21:9 and even foldable devices with dynamic ratios
- Pixel densities span from standard 1x displays to ultra-high 4x retina screens
- Orientations switch between portrait and landscape, sometimes mid-session
An application that fails to adapt to these variations creates friction, frustration, and ultimately, user abandonment. And here's the kicker: your users won't tell you they're leaving because of responsive issues. They'll just... leave.
Consider these real-world horror stories (yes, I've seen all of these in production apps):
- The Overflow Problem: Text or buttons that work perfectly on an iPhone 14 suddenly overflow and become unusable on an iPhone SE
- The Wasted Space Issue: A layout optimized for mobile looks cramped and inefficient on a tablet, failing to leverage the available screen real estate
- The Readability Crisis: Fixed font sizes that are readable on a phone become uncomfortably large or frustratingly small on other devices
Business Impact
Poor responsiveness directly impacts your bottom line:
- Reduced User Engagement: Users abandon apps that feel broken or awkward on their device
- Negative Reviews: Device-specific issues lead to poor ratings and complaints
- Maintenance Burden: Fixing responsive issues after launch is far more expensive than building responsively from the start
- Market Limitation: Non-responsive apps effectively exclude entire device categories from your potential user base
Flutter's Responsive Design Approaches: The Good, The Verbose, and The Opportunity
As Majid brilliantly explains in Chapter 23, Flutter provides two primary approaches to building responsive interfaces. Each has its place, but as I discovered during our book club discussions, each also has its... quirks.
Approach 1: MediaQuery-Based Responsiveness
This is the approach you probably learned first. It's straightforward, uses device-level screen information, and gets the job done.
The MediaQuery approach uses device-level screen information to make layout decisions:
Widget build(BuildContext context) {
var screenWidth = MediaQuery.sizeOf(context).width;
if (screenWidth < 600) {
return mobileLayout();
} else if (screenWidth < 840) {
return tabletLayout();
} else {
return desktopLayout();
}
}
Advantages:
- Simple and straightforward for device-level decisions
- Access to orientation, safe areas, and system UI information
- Works well for top-level layout switching
The Reality Check:
- Those magic numbers (600, 840) end up scattered everywhere
- You're writing
MediaQuery.sizeOf(context).widthso many times you see it in your sleep - Maintaining consistency becomes a nightmare when you change your breakpoints
- Doesn't account for parent widget constraints (that dialog that looks perfect on mobile but terrible as a card? Yeah, MediaQuery can't help you there)
Approach 2: LayoutBuilder-Based Responsiveness
The LayoutBuilder approach provides parent widget constraints, enabling context-aware layouts:
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth < 600) {
return mobileLayout();
} else {
return desktopLayout();
}
},
);
}
Advantages:
- Responds to actual available space, not just screen size
- Enables truly adaptive nested widgets
- Better for component-level responsiveness
The Reality Check:
- You're still manually checking constraints everywhere
- Breakpoint logic gets duplicated across components
- Where are my scaling utilities? Where's my design-to-code workflow?
- "Okay, but how do I actually size this button consistently?" (Spoiler: more manual calculations)
The "Aha!" Moment: Why Vize Needed to Exist
Here's what hit me during our book club discussion: Majid's chapter was teaching us what to do and why it mattered, but the how was still... clunky.
I kept thinking about my Figma designs sitting in one tab, and my Flutter code in another. In Figma: "This button is 200px wide." In Flutter: "Okay, so that's... MediaQuery.of(context).size.width * 0.513... wait, no..."
And then there was the text sizing. Oh, the text sizing. You want to support user font preferences? Cool, now wrap your entire app in a MediaQuery override, or... wait, you want it to work with your responsive text calculations too? Good luck.
The principles in Chapter 23 were sound. The patterns were solid. But the implementation required so much boilerplate that responsive design felt like a chore rather than a feature. As projects grow, these small repetitive patterns compound into larger maintenance problems, especially when multiple developers are implementing responsiveness differently across the app as I've experienced while working across teams.
The issue wasn't that Flutter lacked responsive capabilities. The issue was maintaining responsive consistency across growing applications without repeating sizing logic, breakpoint checks, and scaling calculations throughout the codebase.
That's when I realized: we needed a layer on top of these approaches. Something that would respect the principles Majid outlined but make them actually enjoyable to implement.
Enter Vize (Visual Size).
Introducing Vize: Making Responsive Design Feel Like Magic (Not Homework)
Vize isn't trying to replace Flutter's responsive widgets, it's enhancing them. Think of it as a developer-experience layer built on top of Flutter's existing responsive primitives like MediaQuery and LayoutBuilder; taking everything Majid taught us and packaging it into something you'll actually want to use.
The core philosophy came straight from frustrations I've experienced building fintech apps where pixel-perfect designs and responsive behavior weren't optional but were requirements.
Core Philosophy (Or: Three Things I Wish I Had Years Ago)
Vize is built on three key principles that came directly from real project pain points:
Design-Driven Development: Your designer handed you a Figma file with precise measurements. You shouldn't need a calculator and three breakpoint checks to translate "200px" into responsive Flutter code.
Centralized Configuration: Define your breakpoints, scaling logic, and text preferences ONCE. Then forget about them and just build.
Developer Ergonomics: If typing
MediaQuery.of(context).size.width * 0.5makes you want to throw your laptop, you're going to love typing50.winstead.
Setup: The "Initialize Once, Use Everywhere" Approach
Here's the beautiful part, you set up Vize once in your app's root, and then you never think about initialization again:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
builder: (context, child) {
// Initialize Vize with your Figma design dimensions
Vize.init(
context,
figmaWidth: 390, // Your Figma frame width
figmaHeight: 844, // Your Figma frame height
textScalar: 1.0, // Global text size multiplier
);
return child!;
},
home: HomeScreen(),
);
}
}
This single initialization makes Vize's responsive utilities available throughout your entire app via the singleton Vize.I. No prop drilling. No context passing. No "wait, did I initialize this widget tree?" anxiety.
Real Talk: Practical Responsive Patterns That Actually Work
Enough theory. Let's see how Vize turns the responsive design principles from Chapter 23 into code you'll actually enjoy writing.
Pattern 1: The "Figma-to-Flutter in 3 Seconds" Technique
Remember that design handoff meeting where the designer said "This button is 24 by 48 pixels" and you immediately started calculating ratios? Yeah, forget all that:
// In Figma: 24px width, 48px height button
Container(
width: 24.fw, // Scales based on Figma width
height: 48.fh, // Scales based on Figma height
child: Icon(Icons.menu),
)
// Or using the functional API
Container(
width: fw(24),
height: fh(48),
child: Icon(Icons.menu),
)
How it works: Vize calculates the ratio between your Figma design dimensions and the actual device screen, then scales all values proportionally. A 24px element in a 390px Figma frame automatically becomes the correct percentage of any device width.
Why this matters: You're no longer translating between design and code. You're just... copying numbers. Design says 24px? You write 24.fw. Done. Shipped. Next feature.
Pattern 2: The "80% of the Screen Width" One-Liner
You know how you want something to take up most of the screen width but not quite all of it? Here's the MediaQuery way:
// The "I've been doing Flutter too long" way
Container(
width: MediaQuery.of(context).size.width * 0.8,
height: MediaQuery.of(context).size.height * 0.3,
child: Text('Why am I doing this to myself?'),
)
// The Vize way
Container(
width: 80.w, // 80% of screen width
height: 30.h, // 30% of screen height
padding: ps(h: 4, v: 2), // Scaled symmetric padding
child: Text('This is so much better'),
)
This is particularly useful for:
- Cards that should take most of the screen width on mobile but be constrained on tablet (without writing two different widgets)
- Modal dialogs that scale relative to screen size (so they don't look tiny on iPads)
- Hero sections in onboarding flows (where the visual impact depends on screen real estate)
Pattern 3: Typography That Respects Your Users (And Your Sanity)
Here's a fun fact: if your app doesn't support user font preferences, you're losing users over 40. And users with visual impairments. And basically anyone who's changed their device text size settings (which is a lot of people).
The traditional approach? Wrap your entire app in some MediaQuery gymnastics. The Vize approach? Two lines of code:
Text(
'Welcome Back',
style: TextStyle(
fontSize: 24.ts, // Responsive text size
fontWeight: FontWeight.bold,
),
)
The .ts (text size) method applies intelligent scaling with built-in clamping to prevent text from becoming too large or too small on extreme device sizes. Even better, the textScalar parameter allows users to adjust app-wide text sizing based on their preferences:
// In your settings screen
Vize.init(
context,
textScalar: userPreference.fontSize, // 0.85, 1.0, 1.15, etc.
);
// In your settings screen
Vize.init(
context,
textScalar: userPreference.fontSize, // 0.85, 1.0, 1.15, etc.
);
// Everywhere else in your app
Text(
'Welcome Back',
style: TextStyle(
fontSize: 24.ts, // Automatically respects user preference
fontWeight: FontWeight.bold,
),
)
Every single text element across your entire app instantly respects this preference. No additional code; no widgets to remember; no "oops, I forgot to apply the text scale to this screen."
Pattern 4: The 8px Grid System (That Actually Stays 8px)
Every good designer loves their spacing system. Usually based on an 8px grid. But in responsive design, "8px" isn't really 8px anymore, right?
Well, it can be:
// 8px grid system
Column(
children: [
Text('Title'),
fhs(16), // 16px vertical space (2 × 8px grid)
Text('Subtitle'),
fhs(8), // 8px vertical space (1 × 8px grid)
Text('Description'),
],
)
// Or using the grid step helper
sp(2) // Returns scaled value for 2 × 8px = 16px
This enforces a consistent spacing system derived from design principles, automatically scaled for different device sizes. Your designer will love you. Your code reviewers will love you. Future you will love you.
Pattern 5: Device-Specific Layouts (Without the Boilerplate)
Sometimes, mobile and desktop really do need different layouts. Majid covers this extensively in Chapter 23, but here's how Vize makes it painless:
// Using VizeBuilder widget
VizeBuilder(
mobile: (context) => MobileLayout(),
tablet: (context) => TabletLayout(),
desktop: (context) => DesktopLayout(),
)
// Or using conditional logic
if (Vize.I.isMobile) {
return SingleColumnLayout();
} else if (Vize.I.isTablet) {
return TwoColumnLayout();
} else {
return ThreeColumnLayout();
}
// Helper function for adaptive values
GridView.count(
crossAxisCount: adaptiveColumns(
mobile: 2,
tablet: 4,
desktop: 6,
),
children: items,
)
Notice what's NOT here? No breakpoint checks. No constraint calculations. No nested ternary operators that make your eyes bleed.
Pattern 6: Radius and Borders That Scale (Because Details Matter)
Here's something subtle that most people miss: border radius should scale with screen size. A 12px radius looks great on a phone but weirdly sharp on a tablet. Vize handles this:
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.r), // Responsive radius
border: Border.all(color: Colors.grey),
),
child: content,
)
It's a small detail. But small details compound into apps that feel polished versus apps that feel "good enough."
Let's Get Real: A Complete Settings Screen Refactor
Enough snippets. Let's build something real. Here's a settings screen that demonstrates everything we've learned from Chapter 23, implemented with Vize.
(And yes, this is based on actual production code from one of my apps, simplified slightly for readability. The patterns work.)
// Real settings screen from production - simplified for clarity
class SettingsScreen extends StatelessWidget {
const SettingsScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: backgroundColor,
body: SafeArea(
child: Column(
children: [
// Header with responsive padding and text
Container(
padding: ps(h: 4, v: 4), // Scaled symmetric padding
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Settings',
style: TextStyle(
fontSize: 24.ts, // Responsive text
fontWeight: FontWeight.bold,
),
),
fhs(4), // 4px vertical spacing
Text(
'Customize your experience',
style: TextStyle(fontSize: 14.ts),
),
],
),
),
// Responsive content area
Expanded(
child: SingleChildScrollView(
padding: pa(16), // Scaled all-around padding
child: Center(
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: isTablet ? 700 : double.infinity,
),
child: _buildSettingsSections(),
),
),
),
),
],
),
),
);
}
Widget _buildSettingsSections() {
return Column(
children: [
_buildTextSizeSection(),
fhs(24), // Responsive spacing between sections
_buildThemeSection(),
fhs(24),
_buildDisplayOptionsSection(),
],
);
}
Widget _buildTextSizeSection() {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.r), // Responsive radius
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 6.r, // Responsive blur
offset: Offset(0, 2.fh), // Responsive offset
),
],
),
child: Column(
children: [
Padding(
padding: pa(16), // Consistent scaled padding
child: Row(
children: [
Icon(Icons.text_fields, size: 24.fw), // Responsive icon
fws(12), // Responsive horizontal spacing
Text('Text Size', style: TextStyle(fontSize: 18.ts)),
],
),
),
_buildTextSizeOptions(),
],
),
);
}
Widget _buildTextSizeOptions() {
return Padding(
padding: pa(16),
child: VizeBuilder(
// Mobile: 2 columns, Tablet+: 4 columns
mobile: (context) => _buildGrid(crossAxisCount: 2),
tablet: (context) => _buildGrid(crossAxisCount: 4),
desktop: (context) => _buildGrid(crossAxisCount: 4),
),
);
}
Widget _buildGrid({required int crossAxisCount}) {
return GridView.count(
crossAxisCount: crossAxisCount,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
mainAxisSpacing: sp(1.5), // 12px (1.5 × 8px grid)
crossAxisSpacing: sp(1.5),
childAspectRatio: 2.5,
children: textSizes.map(_buildTextSizeOption).toList(),
);
}
}
Taking It Further: Advanced Responsive Techniques
Once you've got the basics down, here's where things get interesting. These are the techniques that separate "it works on my phone" from "it works beautifully everywhere."
Orientation Awareness (Because People Do Rotate Their Phones)
Majid dedicates a whole section to orientation in Chapter 23, and for good reason. Here's how Vize makes it effortless:
VizeLayout(
builder: (context, info) {
final isPortrait = info.orientation == Orientation.portrait;
return GridView.count(
crossAxisCount: isPortrait
? adaptiveColumns(mobile: 2, tablet: 3, desktop: 4)
: adaptiveColumns(mobile: 3, tablet: 4, desktop: 6),
children: items,
);
},
)
Custom Breakpoints (Because One Size Doesn't Fit All Apps)
Maybe your app targets enterprise users with large tablets. Or maybe you're building for watches. Default breakpoints won't cut it:
Vize.init(
context,
breakpoints: VizeBreakpoints(
mobile: 480, // Custom mobile breakpoint
tablet: 1024, // Custom tablet breakpoint
),
);
Context-Aware Widget Responsiveness (The Secret Weapon)
Here's something cool that combines LayoutBuilder principles with Vize's ergonomics. Sometimes you need a widget to respond to its parent's size, not the screen size. This becomes especially important in modular applications where reusable widgets may appear inside cards, dialogs, side panels, or split-screen layouts with varying constraints.
VizeLayout(
builder: (context, info) {
// info.vizeWidget gives you the parent widget's dimensions
// info.vizeScreen gives you the full screen dimensions
return Container(
width: info.vizeWidget.width * 0.8, // 80% of parent width
child: content,
);
},
)
The Battle-Tested Best Practices (I Learned These the Hard Way)
After building multiple production apps with Vize (and making every mistake possible), here's what actually works:
1. Initialize Once, Use Everywhere (Seriously, Just Once)
I've seen developers try to initialize Vize in every screen. Don't do that. It's wasteful, unnecessary, and you'll hate yourself during code review:
MaterialApp(
builder: (context, child) {
Vize.init(context, textScalar: getTextScalarFromSettings());
return child!;
},
// ...
)
2. Combine Approaches (Vize + Flutter = Better Than Either Alone)
Here's a truth bomb: Vize doesn't replace Flutter's responsive widgets. It makes them better. This is exactly what Majid was teaching in Chapter 23; use the right tool for the job:
LayoutBuilder(
builder: (context, constraints) {
// Use LayoutBuilder for structural decisions
final useCompactLayout = constraints.maxWidth < 600;
// Use Vize for precise sizing within that structure
return useCompactLayout
? Container(
padding: pa(16),
width: 100.fw,
child: compactContent,
)
: Container(
padding: pa(24),
width: 60.w, // 60% of screen
child: expandedContent,
);
},
)
3. Avoid Hard-Coded Values (They're the Enemy of Responsive Design)
I'll say this once: if you're typing raw numbers into your Flutter code, you're probably doing it wrong. Majid emphasizes this throughout Chapter 23, and he's absolutely right:
// Bad: Hard-coded values
Container(
width: 200,
height: 150,
padding: EdgeInsets.all(20),
child: Text('Hello', style: TextStyle(fontSize: 16)),
)
// Good: Responsive values
Container(
width: 200.fw, // Or use percentage: 50.w
height: 150.fh, // Or use percentage: 20.h
padding: pa(20), // Scaled padding
child: Text('Hello', style: TextStyle(fontSize: 16.ts)),
)
4. Test Across Device Sizes (Or Accept That Users Will Find Your Bugs)
You can't just test on your device. You can't even just test on your device and one tablet. You need to see your app on the extremes:
// In your tests or during development
void testResponsiveness() {
final testSizes = [
Size(320, 568), // iPhone SE
Size(390, 844), // iPhone 13
Size(428, 926), // iPhone 13 Pro Max
Size(768, 1024), // iPad
Size(1920, 1080), // Desktop
];
for (final size in testSizes) {
// Test your layouts with each size
}
}
5. Respect User Preferences (It's Not Just Good UX, It's Required)
This is from Majid's tips section, and it's crucial. Your app doesn't exist in isolation. It exists on a device with system settings, user preferences, and accessibility requirements:
// Respect user's text size preferences
final userTextScale = settings.textSize == 'large' ? 1.2 : 1.0;
Vize.init(
context,
textScalar: userTextScale,
);
// Respect safe areas
SafeArea(
child: yourContent,
)
// Adapt to keyboard visibility
final keyboardHeight = MediaQuery.viewInsetsOf(context).bottom;
The Mistakes I Made (So You Don't Have To)
Let me save you some pain. Here are the three mistakes I made when I first started using responsive design patterns from Chapter 23:
Mistake 1: Forgetting Initialization (The Classic)
What happened: App crashed with a "LateInitializationError" on Vize.I.
Why: I tried to use Vize before calling init.
The fix: ALWAYS initialize in MaterialApp.builder. Set a reminder. Write it in blood. Just don't forget.
Mistake 2: Over-Scaling (The "Why Is Everything Huge?" Problem)
What happened: Elements were sized correctly, then sized correctly AGAIN, resulting in massive UI elements.
Why: I applied Vize utilities to already-scaled values.
Solution: Apply Vize utilities to base values only, not to already-responsive values.
// Wrong: Double scaling
Container(
width: 50.w.fw, // Don't combine percentage and Figma scaling
)
// Right: Choose one approach
Container(
width: 50.w, // Percentage approach
// OR
width: 200.fw, // Figma scaling approach
)
Pitfall 3: Ignoring Constraints
Problem: Widgets overflow because they don't respect parent constraints.
Solution: Combine Vize with Flutter's constraint-aware widgets:
Flexible(
child: Container(
width: 80.w, // Will respect Flexible's constraints
child: content,
),
)
Tradeoffs and Considerations
Like any abstraction layer, Vize introduces its own conventions and patterns that teams must adopt consistently.
For smaller projects, Flutter's native responsive APIs may already be sufficient. However, as applications scale, centralized sizing systems and reusable responsive utilities can significantly reduce maintenance overhead and improve consistency across teams.
It's also important to avoid over-scaling every UI element blindly. Responsive utilities work best when combined thoughtfully with Flutter's native layout and constraint system.
Performance Considerations
Vize is designed with performance in mind:
-
Singleton Pattern:
Vize.Iis a single instance with O(1) access - Lazy Evaluation: Calculations happen only when methods are called
- No Widget Overhead: Extension methods have zero widget tree cost
- Efficient Clamping: Text size and radius clamping use optimized algorithms
For optimal performance:
- Initialize once in app root
- Avoid re-initializing Vize inside
buildmethods of frequently rebuilding widgets - Use
constconstructors where possible - Cache calculated values in stateful widgets if used repeatedly
Conclusion
Responsive design is no longer optional, it's a fundamental requirement for professional Flutter applications. Users expect seamless experiences across their diverse devices, and meeting this expectation directly impacts engagement, retention, and revenue.
Flutter already provides powerful responsive building blocks. The challenge is maintaining scalable, consistent responsive behavior across increasingly complex applications and device categories.
The Vize package provides a comprehensive, developer-friendly solution that:
- Simplifies responsive implementation with intuitive APIs
- Centralizes configuration for consistency across your app
- Scales directly from design tools like Figma
- Enhances developer productivity with minimal syntax
- Maintains performance with efficient algorithms
By adopting Vize and the responsive patterns outlined in this article, you can build Flutter applications that feel native and polished on every device—from the smallest smartphone to the largest desktop display.
Responsive design isn't just about making your app work on different screen sizes. It's about making your app feel like it was specifically designed for each user's device. That's the standard modern users expect, and with Vize, it's a standard you can consistently deliver.
Resources:
- Vize: Vize package
- Flutter Responsive Design Docs: flutter.dev/docs/development/ui/layout/responsive
- Material Design Breakpoints: m3.material.io/foundations/layout
Top comments (0)