responsive_metrics 0.0.3
responsive_metrics: ^0.0.3 copied to clipboard
A lightweight, dependency-free responsive design toolkit for Flutter — pixel scaling, breakpoint checks, and per-device values.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:responsive_metrics/responsive_metrics.dart';
void main() => runApp(const ExampleApp());
// Define RV constants once, like you would in a real app's design-tokens
// file, then reuse them anywhere a widget has a BuildContext.
class AppFontSize {
// A plain double like this just scales normally with .sp — no RV needed.
static const body = 14.0;
// These are genuinely different values per device class (not scaled
// versions of each other), so RV is the right tool here.
static const cardTitle = RV(mobile: 18.0, tablet: 22.0, desktop: 26.0);
static const pageHeader = RV(mobile: 22.0, tablet: 30.0); // no desktop
// value given, so it falls back to tablet's 30.0 on desktop too.
}
class AppSpacing {
// Padding that should be noticeably roomier on tablet/desktop, not just
// a proportional scale-up of the mobile value.
static const cardPadding = RV(mobile: 12.0, tablet: 20.0, desktop: 28.0);
}
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'responsive_metrics example',
// Initialize ScreenScaler once here so it stays in sync on resize.
builder: (context, child) {
ScreenScaler.instance.init(MediaQuery.sizeOf(context));
return child!;
},
home: const HomePage(),
routes: {'/rv': (context) => const RvExamplePage()},
);
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: [
// Layout decision: only shown on desktop.
if (context.isDesktop)
Container(width: 220.w, color: Colors.grey.shade200),
Expanded(
child: Padding(
padding: EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Hello, responsive_metrics',
style: TextStyle(
fontSize: 22.sp,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8.h),
Container(
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(12.r),
),
child: Text(
context.pick(
mobile: 'Mobile layout',
tablet: 'Tablet layout',
desktop: 'Desktop layout',
),
style: TextStyle(fontSize: 14.sp),
),
),
SizedBox(height: 16.h),
OutlinedButton(
onPressed: () => Navigator.pushNamed(context, '/rv'),
child: const Text('See RV example'),
),
],
),
),
),
],
),
bottomNavigationBar: context.isMobile
? BottomNavigationBar(
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.settings),
label: 'Settings',
),
],
)
: null,
);
}
}
/// Shows RV used the way you'd actually use it in a real screen: constants
/// defined once (see [AppFontSize] / [AppSpacing] above), then resolved
/// against `context` at the point of use via `.sp(context)`, `.w(context)`,
/// `.h(context)`, `.r(context)`, or `.value(context)`.
class RvExamplePage extends StatelessWidget {
const RvExamplePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('RV example')),
body: SingleChildScrollView(
padding: EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header size genuinely jumps per class, not just scales.
Text(
'Page header',
style: TextStyle(
fontSize: AppFontSize.pageHeader.sp(context),
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 16.h),
// A card whose internal padding is deliberately different per
// device class (roomier on larger screens), combined with a
// title size that is also RV-driven.
Container(
width: double.infinity,
padding: EdgeInsets.all(AppSpacing.cardPadding.w(context)),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(16.r),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Card title',
style: TextStyle(
fontSize: AppFontSize.cardTitle.sp(context),
fontWeight: FontWeight.w600,
),
),
SizedBox(height: 8.h),
Text(
'Body text uses a plain 14.sp — it scales normally, '
'no RV needed since it is the same value everywhere.',
style: TextStyle(fontSize: AppFontSize.body.sp),
),
],
),
),
SizedBox(height: 16.h),
// Reading the raw resolved number, e.g. to feed into your own
// calculation instead of one of the .w/.h/.r/.sp shortcuts.
Text(
'Raw pageHeader value for this screen: '
'${AppFontSize.pageHeader.value(context)}',
style: TextStyle(fontSize: 12.sp, color: Colors.grey.shade700),
),
],
),
),
);
}
}