responsive_wrapper 2.0.0
responsive_wrapper: ^2.0.0 copied to clipboard
A Flutter package for building responsive UIs that adapt to different screen sizes, device types, and orientations with automatic detection.
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:
ScreenInfoflows down the tree — access it anywhere viacontext.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,
),
),
);
},
)
Shorthand Extension Methods (Recommended) #
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.
Using Extension Methods (Recommended)
// 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.