DEV Community

Cover image for Responsive UI Architecture: Designing for Phone, Tablet, and Desktop in One Codebase
Biwesh
Biwesh

Posted on

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

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.

1. The Core Utility: Proportional Scaling

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.

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

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.

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

2. Handling Large Screens: Constraint-Based Layouts

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:

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
  1. Mobile: The screen width is usually < 500px, so the maxWidth: 500 constraint does nothing. The column fills the width (minus padding).
  2. 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.

3. Adaptive Orientation

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.

Conclusion

Responsive design in Flutter isn’t just about using MediaQuery. It’s about a strategy:

  1. Scale proportionally for small variations (different phone sizes).

  2. 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.

Top comments (0)