Responsive Wrapper

A comprehensive Flutter package for building responsive UIs that adapt to different screen sizes, device types, and orientations. This package provides a clean and flexible API for creating responsive layouts with automatic device detection and orientation handling.

Features

  • Device Type Detection: Automatically detects phone, tablet, and desktop devices
  • Orientation Support: Handle portrait and landscape orientations with different layouts
  • InheritedWidget Propagation: ScreenInfo flows down the tree — access it anywhere via context.screenInfo
  • Custom Breakpoints: Configure your own breakpoints or use built-in presets (Material, Bootstrap)
  • Parameterized Widgets: Pass data and state to your responsive layouts
  • Pre-builders: Wrap responsive content with state management, themes, and more
  • Responsive Values: Define different values for different device types and orientations
  • ResponsiveVisibility: Show or hide widgets based on device type without conditionals
  • Comprehensive Tests: Full test suite for reliable production use

Getting Started

Add this to your pubspec.yaml:

dependencies:
  responsive_wrapper: ^2.0.0

Then run:

flutter pub get

Migration Guide (1.x → 2.0)

Breaking Changes

1. ResponsiveLayout defaults changed

treatLandscapePhoneAsTablet and treatPortraitTabletAsPhone now default to false (consistent with all other widgets). If you relied on the old behavior, add the flags explicitly:

// Before (implicit true)
ResponsiveLayout(phone: ..., tablet: ...)

// After — add flags if you need the old behavior
ResponsiveLayout(
  treatLandscapePhoneAsTablet: true,
  treatPortraitTabletAsPhone: true,
  phone: ...,
  tablet: ...,
)

2. ResponsiveOrientationLayoutBuilder typedef removed

Replace with ResponsiveLayoutBuilder — they were identical types.


Usage

Basic Responsive Wrapper

import 'package:responsive_wrapper/responsive_wrapper.dart';

ResponsiveWrapper(
  builder: (context, screenInfo) {
    return Container(
      padding: EdgeInsets.all(
        screenInfo.isPhone ? 16.0 : 24.0,
      ),
      child: Text(
        'Device: ${screenInfo.deviceType.name}',
        style: TextStyle(
          fontSize: screenInfo.isPhone ? 16.0 : 20.0,
        ),
      ),
    );
  },
)

Inside or outside a ResponsiveWrapper, use the convenience extensions directly on BuildContext:

// Checks
if (context.isPhone) { ... }
if (context.isTablet) { ... }
if (context.isDesktop) { ... }
if (context.isPortrait) { ... }
if (context.isLandscape) { ... }

// Full ScreenInfo
final info = context.screenInfo;
print(info.width);       // e.g. 390.0
print(info.shortestSide); // e.g. 390.0
print(info.aspectRatio);  // e.g. 0.46

// Device type
final type = context.deviceType; // DeviceType.phone

When inside a ResponsiveWrapper, these use the wrapper's already-resolved ScreenInfo for consistency and efficiency.

Responsive Layout

Define different layouts for different device types:

ResponsiveLayout(
  phone: (context) => PhoneLayout(),
  tablet: (context) => TabletLayout(),
  desktop: (context) => DesktopLayout(),
)

Orientation-Aware Layouts

Handle different orientations with specific layouts:

ResponsiveOrientationLayout(
  phonePortrait: (context) => PhonePortraitLayout(),
  phoneLandscape: (context) => PhoneLandscapeLayout(),
  tabletPortrait: (context) => TabletPortraitLayout(),
  tabletLandscape: (context) => TabletLandscapeLayout(),
  desktop: (context) => DesktopLayout(),
)

Parameterized Widgets

Pass data to your responsive layouts:

ResponsiveWrapperWith<UserData>(
  initialParam: userData,
  builder: (context, screenInfo, userData) {
    return UserProfile(user: userData);
  },
)

ResponsiveVisibility

Show or hide widgets based on device type without writing if statements:

// Only visible on tablet and desktop (hidden on phone)
ResponsiveVisibility(
  visibleOnPhone: false,
  child: SideNavigationPanel(),
)

// Only visible on phone
ResponsiveVisibility(
  visibleOnTablet: false,
  visibleOnDesktop: false,
  child: MobileBottomNav(),
)

// Custom widget shown when hidden
ResponsiveVisibility(
  visibleOnDesktop: false,
  replacement: SizedBox(height: 8),
  child: DesktopSpacer(),
)

Responsive Values

Define different values for different device types and orientations.

// Simple device-only values
final padding = context.getResponsiveValueSimple<double>(
  phone: 16.0,
  tablet: 24.0,
  desktop: 32.0,
);

// With orientation variants
final fontSize = context.getResponsiveFontSize(
  phonePortrait: 16.0,
  phoneLandscape: 14.0,
  tabletPortrait: 20.0,
  tabletLandscape: 18.0,
  desktop: 24.0,
);

// Generic value with orientation
final padding = context.getResponsiveValue<EdgeInsets>(
  phonePortrait: EdgeInsets.all(16.0),
  phoneLandscape: EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0),
  tabletPortrait: EdgeInsets.all(24.0),
  desktop: EdgeInsets.all(32.0),
);

Using ResponsiveValue

// Resolve directly from context
final spacing = ResponsiveValue<double>(
  phone: 16.0,
  tablet: 24.0,
  desktop: 32.0,
).resolve(context);

// Or resolve by device type
final spacing = ResponsiveValue<double>(
  phone: 16.0,
  tablet: 24.0,
).getValue(DeviceType.tablet); // 24.0

Custom Breakpoints

Configure your own breakpoints or use a named preset:

// Custom breakpoints
ResponsiveWrapper(
  breakpoints: const ResponsiveBreakpoints(
    phone: 480,
    tablet: 800,
  ),
  builder: (context, screenInfo) { ... },
)

// Built-in presets
// Material Design 3: phone < 600, tablet 600–1240, desktop >= 1240
ResponsiveWrapper(
  breakpoints: ResponsiveBreakpoints.material,
  builder: (context, screenInfo) { ... },
)

// Bootstrap-inspired: phone < 576, tablet 576–992, desktop >= 992
ResponsiveWrapper(
  breakpoints: ResponsiveBreakpoints.bootstrap,
  builder: (context, screenInfo) { ... },
)

// copyWith for small adjustments
ResponsiveWrapper(
  breakpoints: ResponsiveBreakpoints.material.copyWith(tablet: 1024),
  builder: (context, screenInfo) { ... },
)

InheritedWidget Propagation

ResponsiveWrapper automatically sets up a ResponsiveData InheritedWidget. Any descendant can read the resolved ScreenInfo without recalculating it:

// Access anywhere in the ResponsiveWrapper subtree
final screenInfo = ResponsiveData.of(context);       // throws if not found
final screenInfo = ResponsiveData.maybeOf(context);  // returns null if not found

// Or via extension methods (preferred)
if (context.isPhone) { ... }

This is particularly useful for deeply nested widgets that need responsive behavior:

ResponsiveWrapper(
  builder: (context, screenInfo) {
    return Column(
      children: [
        Header(),        // can call context.isPhone inside
        Content(),       // same
        DeepWidget(),    // same — no props drilling needed
      ],
    );
  },
)

Pre-builders

Pre-builders wrap responsive content with additional functionality like state management, themes, or other wrapper widgets.

Basic Pre-builder

ResponsiveWrapper(
  preBuilder: (context, child) => Container(
    decoration: BoxDecoration(
      gradient: LinearGradient(
        colors: [Colors.blue.shade100, Colors.purple.shade100],
      ),
    ),
    child: child,
  ),
  builder: (context, screenInfo) {
    return Text('Content with gradient background');
  },
)

State Management with Pre-builder

ResponsiveWrapper(
  preBuilder: (context, child) => BlocBuilder<AppCubit, AppState>(
    builder: (context, state) {
      return state.isLoading
        ? CircularProgressIndicator()
        : child;
    },
  ),
  builder: (context, screenInfo) {
    return Text('Content that depends on app state');
  },
)

Parameterized Pre-builder

ResponsiveLayoutWith<String>(
  preBuilder: (context, childBuilder) {
    final userName = UserService.getCurrentUser()?.name ?? 'Guest';
    return Container(
      decoration: BoxDecoration(border: Border.all(color: Colors.blue)),
      child: childBuilder(userName),
    );
  },
  phone: (context, userName) => Text('Hello $userName on phone!'),
  tablet: (context, userName) => Text('Hello $userName on tablet!'),
  desktop: (context, userName) => Text('Hello $userName on desktop!'),
)

Common Pre-builder Patterns

Loading States:

ResponsiveWrapper(
  preBuilder: (context, child) => FutureBuilder<Data>(
    future: dataService.fetchData(),
    builder: (context, snapshot) {
      if (!snapshot.hasData) return const CircularProgressIndicator();
      return child;
    },
  ),
  builder: (context, screenInfo) => DataWidget(),
)

Authentication Wrappers:

ResponsiveWrapper(
  preBuilder: (context, child) => Consumer<AuthProvider>(
    builder: (context, auth, _) {
      return auth.isAuthenticated ? child : LoginScreen();
    },
  ),
  builder: (context, screenInfo) => AuthenticatedContent(),
)

Understanding Pre-builders

Pre-builders run before the main responsive builder and provide a way to inject state, context, or styling.

Aspect Pre-builder Regular Builder
Purpose Wrap/enhance responsive content Build responsive content
Execution Runs first Runs after pre-builder
Access to Context only Context + ScreenInfo
Use Case State, theming, loading Device-specific layouts

API Reference

Core Widgets

Class Description
ResponsiveWrapper Core widget; sets up ResponsiveData + provides ScreenInfo via builder
ResponsiveWrapperWith<T> Parameterized version
ResponsiveLayout Device-specific layout selection
ResponsiveLayoutWith<T> Parameterized version
ResponsiveOrientationLayout Device + orientation layout selection
ResponsiveOrientationLayoutWith<T> Parameterized version
ResponsiveVisibility Show/hide child by device type

Utility Classes

Class Description
ResponsiveData InheritedWidget propagating ScreenInfo down the tree
ResponsiveBreakpoints Breakpoint configuration with .material and .bootstrap presets
ScreenInfo Immutable screen info: deviceType, width, height, shortestSide, aspectRatio, etc.
ResponsiveValue<T> Device-specific value with fallback; has .resolve(context)
ResponsiveOrientationValue<T> Orientation-aware value with fallback
DeviceType phone, tablet, desktop

BuildContext Extensions

Extension Description
context.screenInfo Current ScreenInfo (from tree or MediaQuery)
context.deviceType Current DeviceType
context.isPhone true if phone
context.isTablet true if tablet
context.isDesktop true if desktop
context.isPortrait true if portrait orientation
context.isLandscape true if landscape orientation
context.getResponsiveValue<T>(...) Generic responsive value with orientation
context.getResponsiveValueSimple<T>(...) Device-only responsive value
context.getResponsivePadding(...) EdgeInsets responsive value
context.getResponsiveFontSize(...) double font size responsive value

Builder Types

Typedef Signature
ResponsiveBuilder Widget Function(BuildContext, ScreenInfo)
ResponsivePreBuilder Widget Function(BuildContext, Widget child)
ResponsiveLayoutBuilder Widget Function(BuildContext)
ResponsiveBuilderWith<T> Widget Function(BuildContext, ScreenInfo, T)
ResponsivePreBuilderWith<T> Widget Function(BuildContext, Widget Function(T) childBuilder)

Real-World Examples

Complete App with Pre-builders

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: ResponsiveLayoutWith<UserData>(
        preBuilder: (context, childBuilder) {
          return BlocBuilder<AuthCubit, AuthState>(
            builder: (context, authState) {
              if (authState.isLoading) return LoadingScreen();
              if (!authState.isAuthenticated) return LoginScreen();
              return FutureBuilder<UserData>(
                future: UserService.getUserData(authState.userId),
                builder: (context, snapshot) {
                  if (!snapshot.hasData) return LoadingScreen();
                  if (snapshot.hasError) return ErrorScreen(error: snapshot.error);
                  return childBuilder(snapshot.data!);
                },
              );
            },
          );
        },
        phone: (context, userData) => PhoneDashboard(userData: userData),
        tablet: (context, userData) => TabletDashboard(userData: userData),
        desktop: (context, userData) => DesktopDashboard(userData: userData),
      ),
    );
  }
}

Adaptive Navigation

Scaffold(
  bottomNavigationBar: ResponsiveVisibility(
    visibleOnTablet: false,
    visibleOnDesktop: false,
    child: BottomNavBar(),
  ),
  body: Row(
    children: [
      ResponsiveVisibility(
        visibleOnPhone: false,
        child: SideNav(),
      ),
      Expanded(child: MainContent()),
    ],
  ),
)

E-commerce Product Page

class ProductPage extends StatelessWidget {
  final String productId;

  @override
  Widget build(BuildContext context) {
    return ResponsiveWrapper(
      preBuilder: (context, child) => BlocBuilder<ProductCubit, ProductState>(
        builder: (context, state) {
          if (state.isLoading) return const Center(child: CircularProgressIndicator());
          if (state.hasError) return ErrorWidget(state.error);
          return child;
        },
      ),
      builder: (context, screenInfo) {
        return screenInfo.isPhone
          ? PhoneProductLayout(productId: productId)
          : screenInfo.isTablet
          ? TabletProductLayout(productId: productId)
          : DesktopProductLayout(productId: productId);
      },
    );
  }
}

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is licensed under the MIT License - see the LICENSE file for details.