Tools: Responsive UI Architecture: Designing for Phone, Tablet, and Desktop in One Codebase

Tools: Responsive UI Architecture: Designing for Phone, Tablet, and Desktop in One Codebase

Source: Dev.to

1. The Core Utility: Proportional Scaling ## 2. Handling Large Screens: Constraint-Based Layouts ## 3. Adaptive Orientation ## Conclusion In the world of Flutter development, “write once, run everywhere” is a promise that comes with a caveat: just because code runs on a tablet doesn’t mean it looks good on a tablet. At our software house, ensuring our mobile app provided a seamless experience across a myriad of devices—from small Android phones to large iPad Pros—was a priority. We adopted a hybrid approach to responsive design that combines Proportional Scaling with Constraint-Based Layouts. Here is a deep dive into the architecture we use to handle responsiveness in our Flutter codebase. We use a SizeUtils class to establish a baseline. Most designs are created by UI/UX teams at a specific resolution (e.g., iPhone 11 Pro at 375×812). We treat this as our “Design Size.” Our Sizer widget wraps the entire application, utilizing LayoutBuilder and OrientationBuilder to determine the current device’s effective width and height. This allows us to use extension methods on standard numbers to scale UI elements proportionally. Instead of hardcoding fontSize: 14, we write fontSize: 14.fSize. Whether on a tiny iPhone SE or a massive Samsung Ultra, the text remains readable and proportionally correct. However, proportional scaling isn’t enough. If you scale a login button proportionally on an iPad, it looks comically large. This is where Constraints come in. For screens like our Login Page, we want the content to be centered and readable, not stretched edge-to-edge. We achieve this using a specific pattern: LayoutBuilder + SingleChildScrollView + ConstrainedBox. The Login Screen Pattern Here is how we implemented the Login Screen to look great on both phones and tablets: Using the OrientationBuilder inside our Sizer, we can also trigger full UI changes. For dashboards, we might switch from a Column (vertical list) in Portrait mode to a Row (side-by-side view) in Landscape mode. Responsive design in Flutter isn’t just about using MediaQuery. It’s about a strategy: Scale proportionally for small variations (different phone sizes). Constrain layout for large variations (tablets and desktops). By combining SizeUtils for micro-adjustments and LayoutBuilder for macro-layout decisions, we ensure a consistent, professional look across every device our users use. Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK: dart // lib/core/utils/size_utils.dart class Sizer extends StatelessWidget { const Sizer({super.key, required this.builder}); final ResponsiveBuild builder; @override Widget build(BuildContext context) { return LayoutBuilder(builder: (context, constraints) { return OrientationBuilder(builder: (context, orientation) { SizeUtils.setScreenSize(constraints, orientation); return builder(context, orientation, SizeUtils.deviceType); }); }); } } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: dart // lib/core/utils/size_utils.dart class Sizer extends StatelessWidget { const Sizer({super.key, required this.builder}); final ResponsiveBuild builder; @override Widget build(BuildContext context) { return LayoutBuilder(builder: (context, constraints) { return OrientationBuilder(builder: (context, orientation) { SizeUtils.setScreenSize(constraints, orientation); return builder(context, orientation, SizeUtils.deviceType); }); }); } } CODE_BLOCK: dart // lib/core/utils/size_utils.dart class Sizer extends StatelessWidget { const Sizer({super.key, required this.builder}); final ResponsiveBuild builder; @override Widget build(BuildContext context) { return LayoutBuilder(builder: (context, constraints) { return OrientationBuilder(builder: (context, orientation) { SizeUtils.setScreenSize(constraints, orientation); return builder(context, orientation, SizeUtils.deviceType); }); }); } } COMMAND_BLOCK: dart extension ResponsiveExtension on num { // Simple scaling logic based on design width (375) double get h => (this * SizeUtils.width) / 375; double get w => (this * SizeUtils.width) / 375; double get fSize => (this * SizeUtils.width) / 375; } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: dart extension ResponsiveExtension on num { // Simple scaling logic based on design width (375) double get h => (this * SizeUtils.width) / 375; double get w => (this * SizeUtils.width) / 375; double get fSize => (this * SizeUtils.width) / 375; } COMMAND_BLOCK: dart extension ResponsiveExtension on num { // Simple scaling logic based on design width (375) double get h => (this * SizeUtils.width) / 375; double get w => (this * SizeUtils.width) / 375; double get fSize => (this * SizeUtils.width) / 375; } CODE_BLOCK: dart // features/auth/presentation/login_screen.dart @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: LayoutBuilder( builder: (context, constraints) { return SingleChildScrollView( child: ConstrainedBox( // Ensure the scroll view takes at least the full screen height constraints: BoxConstraints(minHeight: constraints.maxHeight), child: IntrinsicHeight( child: Center( // LIMIT WIDTH: This is the key for tablets/desktop! child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 500), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 24.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // Form fields go here... ], ), ), ), ), ), ), ); }, ), ), ); } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: dart // features/auth/presentation/login_screen.dart @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: LayoutBuilder( builder: (context, constraints) { return SingleChildScrollView( child: ConstrainedBox( // Ensure the scroll view takes at least the full screen height constraints: BoxConstraints(minHeight: constraints.maxHeight), child: IntrinsicHeight( child: Center( // LIMIT WIDTH: This is the key for tablets/desktop! child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 500), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 24.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // Form fields go here... ], ), ), ), ), ), ), ); }, ), ), ); } CODE_BLOCK: dart // features/auth/presentation/login_screen.dart @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: LayoutBuilder( builder: (context, constraints) { return SingleChildScrollView( child: ConstrainedBox( // Ensure the scroll view takes at least the full screen height constraints: BoxConstraints(minHeight: constraints.maxHeight), child: IntrinsicHeight( child: Center( // LIMIT WIDTH: This is the key for tablets/desktop! child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 500), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 24.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // Form fields go here... ], ), ), ), ), ), ), ); }, ), ), ); } - Mobile: The screen width is usually < 500px, so the maxWidth: 500 constraint does nothing. The column fills the width (minus padding). - Tablet/Web: The screen width is > 500px. The ConstrainedBox kicks in, capping the form width at 500px. The Center widget then places this 500px box perfectly in the middle of the screen. - Scale proportionally for small variations (different phone sizes). - Constrain layout for large variations (tablets and desktops). By combining SizeUtils for micro-adjustments and LayoutBuilder for macro-layout decisions, we ensure a consistent, professional look across every device our users use.