gmana_flutter_extensions 0.0.1
gmana_flutter_extensions: ^0.0.1 copied to clipboard
Flutter extension methods and small services for colors, layout, context, icons, time, and theme mode.
gmana_flutter_extensions #
Focused Flutter extension methods and small services for the Gmana ecosystem.
import 'package:gmana_flutter_extensions/gmana_flutter_extensions.dart';
Table of contents #
Color #
Extensions on Color (ColorExt) and String (StringColorExtension), backed by ColorService.
Parsing #
// From hex string — supports #RGB, #RRGGBB, #AARRGGBB, with or without #
final color = '#FF5500'.toColor();
final transparent = '#80FF5500'.toColor(); // 50% alpha
final shorthand = '#F50'.toColor(); // expands to #FF5500
// With opacity applied after parsing
final faded = '#FF5500'.toColorWithOpacity(0.5);
// Nullable parse (returns null on invalid input)
final maybeColor = ColorService.tryParseHex('not-a-color'); // null
Hex output #
final color = Colors.deepOrange;
color.toHexRGB(); // '#FF5722'
color.toHexRGB(withHashSign: false); // 'FF5722'
color.toHexARGB(); // '#FFFF5722'
Lightness & saturation #
color.darken(); // 10% darker (default)
color.darken(0.3); // 30% darker
color.lighten(); // 10% lighter
color.lighten(0.2); // 20% lighter
color.saturate(); // 10% more saturated
color.desaturate(0.5); // 50% less saturated
color.greyscale; // fully desaturated
Mixing #
// Blend two colors — t=0 returns this, t=1 returns other
color.mix(Colors.white, 0.3); // 30% toward white
// Shade: mix with black; tint: mix with white
color.shade(0.2); // 20% darker via black mix
color.tint(0.4); // 40% lighter via white mix
color.withAlphaOpacity(0.5); // 50% transparent; throws on invalid value
Harmony #
color.complementary; // hue + 180°
color.triadic; // (hue + 120°, hue + 240°)
color.splitComplementary; // (hue + 150°, hue + 210°)
// Returns 2 * count colors: [left1, right1, left2, right2, …]
color.analogous(); // 4 colors, ±15° steps (count: 2, spread: 30°)
color.analogous(count: 3, spreadDegrees: 60); // 6 colors, ±20° steps
Contrast & accessibility #
color.isDark; // luminance < 0.179
color.isLight; // luminance >= 0.179
// Highest-contrast color for text on this background
color.contrastText; // white or black
color.bestContrast([Colors.red, Colors.blue, Colors.white]); // custom candidates
color.contrastRatio(Colors.white); // WCAG contrast ratio (e.g. 4.73)
color.meetsWcagAA(Colors.white); // ratio >= 4.5
color.meetsWcagAAA(Colors.white); // ratio >= 7.0
Material swatch #
// Shade 500 = the input color itself; 50–400 approach white, 600–900 approach black
final swatch = color.toMaterialColor();
swatch[300]; // lighter variant
swatch[700]; // darker variant
Responsive layout #
Breakpoint thresholds: mobile < 730, tablet 730–1199, desktop 1200–1599, widescreen ≥ 1600.
From BuildContext (ResponsiveContext) #
// Current breakpoint
context.breakpoint; // Breakpoint.mobile / .tablet / .desktop / .widescreen
context.isMobile;
context.isTablet;
context.isDesktop;
context.isWidescreen;
context.isAtLeastTablet; // tablet, desktop, or widescreen
context.isAtLeastDesktop; // desktop or widescreen
// Resolve a value for the current breakpoint; larger tiers fall back to smaller ones
final padding = context.responsive<double>(mobile: 16, tablet: 24, desktop: 32);
final columns = context.responsive<int>(mobile: 1, tablet: 2, desktop: 4);
From BoxConstraints (BreakpointUtils) #
Useful inside LayoutBuilder — reacts to the widget's available width, not the screen width.
LayoutBuilder(
builder: (context, constraints) {
final cols = constraints.resolve<int>(mobile: 1, tablet: 2, desktop: 4);
return GridView.count(crossAxisCount: cols, children: [...]);
},
);
// Individual checks
constraints.isMobile;
constraints.isAtLeastTablet;
constraints.breakpoint; // Breakpoint enum value
// Geometry helpers
constraints.largestSize; // Size(maxWidth, maxHeight) — may be infinity
constraints.smallestSize; // Size(minWidth, minHeight)
constraints.isTight; // both axes tightly constrained
constraints.isUnboundedWidth; // maxWidth == double.infinity
constraints.isUnboundedHeight;
// Shrink max dimensions by insets (clamps to minWidth / minHeight)
constraints.deflate(const EdgeInsets.all(16));
// Clamp max dimensions to a specific size
constraints.tightenMaxSize(const Size(400, 300));
Breakpoint enum #
// Pattern-match a value per breakpoint
final iconSize = context.breakpoint.when(
mobile: () => 24.0,
tablet: () => 28.0,
desktop: () => 32.0, // widescreen falls back to desktop when omitted
);
// Optional per-tier, with a required fallback
final badge = context.breakpoint.maybeWhen(
desktop: () => const DesktopBadge(),
orElse: () => const SmallBadge(),
);
// Predicate helpers
context.breakpoint.isAtLeastTablet;
context.breakpoint.isDesktop; // true for desktop AND widescreen
Build context #
All helpers available via ContextExt on BuildContext.
Theme & media #
context.theme; // ThemeData
context.colorScheme; // ColorScheme
context.textTheme; // TextTheme
context.screenSize; // Size — reacts only to size changes
context.screenWidth; // double
context.screenHeight; // double
context.devicePixelRatio; // double
context.textScaleFactor; // double
context.isLandscape;
context.isPortrait;
context.safeAreaPadding; // EdgeInsets — notch + home indicator
context.topSafeArea; // double
context.bottomSafeArea; // double
context.viewInsets; // keyboard insets
context.viewPadding;
// Full MediaQueryData when you need something not covered above
context.mediaQuery;
Navigation #
context.canPop; // bool
context.pop(); // safe pop — no-op if nothing to pop
context.pop('result'); // pop with a typed result
context.push(const ProfilePage()); // returns Future<T?>
context.pushReplacement(const HomePage());
context.pushAndRemoveUntil(const LoginPage()); // clears entire stack
context.popToRoot(); // pops until route.isFirst
context.popUntil((route) => route.settings.name == '/home');
Dialogs & sheets #
// Generic dialog
context.showAppDialog<bool>(dialog: const MyDialog());
// Confirm dialog — returns true / false
final confirmed = await context.showConfirmDialog(
title: 'Delete item',
message: 'This cannot be undone.',
confirmLabel: 'Delete',
cancelLabel: 'Keep',
destructive: true, // styles confirm button with colorScheme.error
);
// Bottom sheet
context.showAppBottomSheet(
child: const FilterSheet(),
isScrollControlled: true,
isDismissible: true,
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
);
Snack bars #
context.showSnackBar(message: 'Saved');
context.showSnackBar(
message: 'Custom',
textColor: Colors.white,
backgroundColor: Colors.indigo,
duration: const Duration(seconds: 5),
behavior: SnackBarBehavior.fixed,
action: SnackBarAction(label: 'Undo', onPressed: () {}),
);
context.showSuccessSnackBar(message: 'Profile updated'); // colorScheme.primary
context.showErrorSnackBar(message: 'Upload failed'); // colorScheme.error
context.showWarningSnackBar(message: 'Low storage'); // colorScheme.tertiary
context.hideSnackBar();
Focus #
context.hasFocus; // bool
context.unfocus(); // dismiss keyboard / clear focus
context.requestFocus(myFocusNode);
Theme mode #
Extensions on ThemeMode (ThemeModeExt) and String (ThemeModeStringExt), backed by ThemeModeService.
// ThemeMode → display values
ThemeMode.dark.toIcon(); // Icons.dark_mode
ThemeMode.light.toLabel(); // 'Light Mode'
ThemeMode.system.toKey(); // 'system'
// String → ThemeMode (unknown keys fall back to ThemeMode.system)
'dark'.toThemeMode(); // ThemeMode.dark
'invalid'.toThemeMode(); // ThemeMode.system
// String → display values directly
'light'.toThemeIcon(); // Icons.light_mode
'system'.toThemeLabel(); // 'System Mode'
// All available keys — useful for building a picker
ThemeModeService.getThemeKeys(); // ['system', 'light', 'dark']
Persistence example #
// Save
prefs.setString('themeMode', currentMode.toKey());
// Restore
final saved = prefs.getString('themeMode') ?? 'system';
final mode = saved.toThemeMode();
Icon serialization #
IconDataExt (static parser) and IconDataSerialization (extension on IconData).
// Serialize
final json = Icons.star.toJsonString();
// '{"codePoint":57493,"fontFamily":"MaterialIcons"}'
// null fields and matchTextDirection:false are omitted for compact output
// Parse — returns null on failure
final icon = IconDataExt.tryParse(json);
// Parse — returns a fallback on failure
final safeIcon = IconDataExt.parse(json, fallback: Icons.question_mark);
IconDataExt.parse('bad input'); // Icons.question_mark (default fallback)
Storage / database example #
// Write
row['icon'] = selectedIcon.toJsonString();
// Read
final icon = IconDataExt.tryParse(row['icon'] as String? ?? '') ?? Icons.label;
Time of day #
Extension on TimeOfDay (TimeOfDayExtensions).
TimeOfDay.now().toCustomString();
// '02:30 PM'
const TimeOfDay(hour: 9, minute: 5).toCustomString();
// '09:05 AM'