Skip to content

Instantly share code, notes, and snippets.

@AndrewDongminYoo
Last active January 15, 2026 09:19
Show Gist options
  • Select an option

  • Save AndrewDongminYoo/dc7a8b87d6cc85f576062c7942800253 to your computer and use it in GitHub Desktop.

Select an option

Save AndrewDongminYoo/dc7a8b87d6cc85f576062c7942800253 to your computer and use it in GitHub Desktop.
Debug helpers for theme inspection.
// 🐦 Flutter imports:
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
// 🌎 Project imports:
import 'package:mirae/gen/colors.gen.dart';
extension ColorSchemeDebugExtension on ColorScheme {
/// Human-friendly dump of the ColorScheme.
///
/// - Debug: prints each field using Color.debug (optionally with aliases).
/// - Release/Profile: returns a lightweight identifier only.
String get debug {
if (!kDebugMode) {
return 'ColorScheme(${brightness.name})';
}
return _debugImpl();
}
String _debugImpl() {
final items = <(String, Color)>[
('error', error),
('errorContainer', errorContainer),
('onError', onError),
('onErrorContainer', onErrorContainer),
('onInverseSurface', onInverseSurface),
('outline', outline),
('outlineVariant', outlineVariant),
('primary', primary),
('inversePrimary', inversePrimary),
('primaryContainer', primaryContainer),
('primaryFixed', primaryFixed),
('primaryFixedDim', primaryFixedDim),
('onPrimary', onPrimary),
('onPrimaryContainer', onPrimaryContainer),
('onPrimaryFixed', onPrimaryFixed),
('onPrimaryFixedVariant', onPrimaryFixedVariant),
('scrim', scrim),
('secondary', secondary),
('secondaryContainer', secondaryContainer),
('secondaryFixed', secondaryFixed),
('secondaryFixedDim', secondaryFixedDim),
('onSecondary', onSecondary),
('onSecondaryContainer', onSecondaryContainer),
('onSecondaryFixed', onSecondaryFixed),
('onSecondaryFixedVariant', onSecondaryFixedVariant),
('shadow', shadow),
('surface', surface),
('inverseSurface', inverseSurface),
('surfaceBright', surfaceBright),
('surfaceContainer', surfaceContainer),
('surfaceContainerHigh', surfaceContainerHigh),
('surfaceContainerHighest', surfaceContainerHighest),
('surfaceContainerLow', surfaceContainerLow),
('surfaceContainerLowest', surfaceContainerLowest),
('surfaceDim', surfaceDim),
('surfaceTint', surfaceTint),
('onSurface', onSurface),
('onSurfaceVariant', onSurfaceVariant),
('tertiary', tertiary),
('tertiaryContainer', tertiaryContainer),
('tertiaryFixed', tertiaryFixed),
('tertiaryFixedDim', tertiaryFixedDim),
('onTertiary', onTertiary),
('onTertiaryContainer', onTertiaryContainer),
('onTertiaryFixed', onTertiaryFixed),
('onTertiaryFixedVariant', onTertiaryFixedVariant),
];
final b = StringBuffer()..writeln('ColorScheme.${brightness.name}(');
for (final (name, color) in items) {
b.writeln(' $name: ${color.debug},');
}
b.writeln(')');
return b.toString();
}
}
extension ColorDebugExtension on Color {
/// Debug output verbosity toggle.
/// - true: `Primary /* aka: a, b, c (+N more) */`
/// - false: `Primary`
static const bool _includeAliasesInDebugString = true;
/// Limits alias spam for very-collided colors (e.g., pure white used everywhere).
static const int _maxAliasesToShow = 5;
static const Map<String, Color> _appColors = {
'black': AppColors.black,
'white': AppColors.white,
'transparent': AppColors.transparent,
'primary': AppColors.primary,
'primaryLight': AppColors.primaryLight,
'primaryDark': AppColors.primaryDark,
'secondary': AppColors.secondary,
'secondaryLight': AppColors.secondaryLight,
'secondaryDark': AppColors.secondaryDark,
'background': AppColors.background,
'backgroundDark': AppColors.backgroundDark,
'surface': AppColors.surface,
'surfaceDark': AppColors.surfaceDark,
'textPrimary': AppColors.textPrimary,
'textSecondary': AppColors.textSecondary,
'textTertiary': AppColors.textTertiary,
'textOnPrimary': AppColors.textOnPrimary,
'textDark': AppColors.textDark,
'accent': AppColors.accent,
'accentLight': AppColors.accentLight,
'success': AppColors.success,
'warning': AppColors.warning,
'error': AppColors.error,
'info': AppColors.info,
'divider': AppColors.divider,
'dividerDark': AppColors.dividerDark,
'disabled': AppColors.disabled,
'disabledDark': AppColors.disabledDark,
'shadow': AppColors.shadow,
'shadowDark': AppColors.shadowDark,
'gradientStart': AppColors.gradientStart,
'gradientMiddle': AppColors.gradientMiddle,
'gradientEnd': AppColors.gradientEnd,
'recording': AppColors.recording,
'playing': AppColors.playing,
'paused': AppColors.paused,
'cardLight': AppColors.cardLight,
'cardDark': AppColors.cardDark,
'cardElevated': AppColors.cardElevated,
'cardElevatedDark': AppColors.cardElevatedDark,
'overlay': AppColors.overlay,
'scrim': AppColors.scrim,
'alarmActive': AppColors.alarmActive,
'alarmInactive': AppColors.alarmInactive,
'todayHighlight': AppColors.todayHighlight,
};
static const Map<String, Color> _colorless = {
'transparent': Color(0x00000000),
'black': Color(0xFF000000),
'black87': Color(0xDD000000),
'black54': Color(0x8A000000),
'black45': Color(0x73000000),
'black38': Color(0x61000000),
'black26': Color(0x42000000),
'black12': Color(0x1F000000),
'white': Color(0xFFFFFFFF),
'white70': Color(0xB3FFFFFF),
'white60': Color(0x99FFFFFF),
'white54': Color(0x8AFFFFFF),
'white38': Color(0x62FFFFFF),
'white30': Color(0x4DFFFFFF),
'white24': Color(0x3DFFFFFF),
'white12': Color(0x1FFFFFFF),
'white10': Color(0x1AFFFFFF),
};
/// The Material Design primary color swatches.
static const Map<String, MaterialColor> _primaries = {
'red': Colors.red,
'pink': Colors.pink,
'purple': Colors.purple,
'deepPurple': Colors.deepPurple,
'indigo': Colors.indigo,
'blue': Colors.blue,
'lightBlue': Colors.lightBlue,
'cyan': Colors.cyan,
'teal': Colors.teal,
'green': Colors.green,
'lightGreen': Colors.lightGreen,
'lime': Colors.lime,
'yellow': Colors.yellow,
'amber': Colors.amber,
'orange': Colors.orange,
'deepOrange': Colors.deepOrange,
'brown': Colors.brown,
'grey': Colors.grey,
'blueGrey': Colors.blueGrey,
};
/// The Material Design accent color swatches.
static const Map<String, MaterialAccentColor> _accents = {
'redAccent': Colors.redAccent,
'pinkAccent': Colors.pinkAccent,
'purpleAccent': Colors.purpleAccent,
'deepPurpleAccent': Colors.deepPurpleAccent,
'indigoAccent': Colors.indigoAccent,
'blueAccent': Colors.blueAccent,
'lightBlueAccent': Colors.lightBlueAccent,
'cyanAccent': Colors.cyanAccent,
'tealAccent': Colors.tealAccent,
'greenAccent': Colors.greenAccent,
'lightGreenAccent': Colors.lightGreenAccent,
'limeAccent': Colors.limeAccent,
'yellowAccent': Colors.yellowAccent,
'amberAccent': Colors.amberAccent,
'orangeAccent': Colors.orangeAccent,
'deepOrangeAccent': Colors.deepOrangeAccent,
};
/// Precomputed reverse mapping:
/// - primaryByArgb: the representative name chosen by the *original priority*
/// - aliasesByArgb: additional names that share the same ARGB (all priorities)
static final _ResolvedNames _resolved = _buildResolved();
static String _hexFromArgb(int argb) => '0x${argb.toRadixString(16).padLeft(8, '0').toUpperCase()}';
/// Debug-friendly string for this color.
///
/// Priority for representative name (same as original code):
/// 1) `AppColors.*`
/// 2) `Colors.*` constants (colorless)
/// 3) `Colors.<primary swatch>(.shadeX)`
/// 4) `Colors.<accent swatch>(.shadeX)`
///
/// Release/Profile: always returns hex to keep it cheap.
String get debug {
final argb = toARGB32();
if (!kDebugMode) {
return 'const Color(${_hexFromArgb(argb)})';
}
final primary = _resolved.primaryByArgb[argb];
if (primary == null) {
return 'const Color(${_hexFromArgb(argb)})';
}
if (!_includeAliasesInDebugString) return primary;
final aliases = _resolved.aliasesByArgb[argb];
if (aliases == null || aliases.isEmpty) return primary;
if (aliases.length <= _maxAliasesToShow) {
return '$primary /* aka: ${aliases.join(', ')} */';
}
final shown = aliases.take(_maxAliasesToShow).join(', ');
final remaining = aliases.length - _maxAliasesToShow;
return '$primary /* aka: $shown (+$remaining more) */';
}
static _ResolvedNames _buildResolved() {
// Representative name and its priority tier (lower is higher priority).
final primaryByArgb = <int, String>{};
final tierByArgb = <int, int>{};
// Aliases in insertion order, de-duplicated per ARGB.
final aliasesByArgb = <int, List<String>>{};
final seenNamesByArgb = <int, Set<String>>{};
void register({
required int argb,
required String name,
required int tier,
}) {
final existing = primaryByArgb[argb];
if (existing == null) {
primaryByArgb[argb] = name;
tierByArgb[argb] = tier;
(seenNamesByArgb[argb] ??= <String>{}).add(name);
return;
}
// De-dup exact repeats.
final seen = seenNamesByArgb[argb] ??= <String>{};
if (!seen.add(name)) return;
final existingTier = tierByArgb[argb] ?? 999;
// This should never trigger if we build in strict priority order,
// but keeping it makes the priority rule explicit and future-proof.
if (tier < existingTier) {
// Demote old primary into aliases.
(aliasesByArgb[argb] ??= <String>[]).add(existing);
// Promote new primary.
primaryByArgb[argb] = name;
tierByArgb[argb] = tier;
return;
}
// Otherwise, keep the original representative and add as alias.
(aliasesByArgb[argb] ??= <String>[]).add(name);
}
// Tier 0: AppColors (highest)
for (final entry in _appColors.entries) {
register(
argb: entry.value.toARGB32(),
name: 'AppColors.${entry.key}',
tier: 0,
);
}
// Tier 1: Colors constants
for (final entry in _colorless.entries) {
register(
argb: entry.value.toARGB32(),
name: 'Colors.${entry.key}',
tier: 1,
);
}
// Tier 2: Material primaries + shades
for (final swatchEntry in _primaries.entries) {
final swatchName = swatchEntry.key;
final swatch = swatchEntry.value;
for (final shade in swatch.keys) {
final color = swatch[shade];
if (color == null) continue;
final name = shade == 500 ? 'Colors.$swatchName' : 'Colors.$swatchName.shade$shade';
register(
argb: color.toARGB32(),
name: name,
tier: 2,
);
}
}
// Tier 3: Material accents + shades
for (final swatchEntry in _accents.entries) {
final swatchName = swatchEntry.key;
final swatch = swatchEntry.value;
for (final shade in swatch.keys) {
final color = swatch[shade];
if (color == null) continue;
final name = shade == 200 ? 'Colors.$swatchName' : 'Colors.$swatchName.shade$shade';
register(
argb: color.toARGB32(),
name: name,
tier: 3,
);
}
}
// Optional: very light debug visibility (no throwing).
assert(() {
final collided = aliasesByArgb.entries.where((e) => e.value.isNotEmpty).toList()
..sort((a, b) => b.value.length.compareTo(a.value.length));
if (collided.isNotEmpty) {
final top = collided.take(10);
final msg = top
.map((e) => '${_hexFromArgb(e.key)}: ${primaryByArgb[e.key]} + ${e.value.length} aliases')
.join('\n');
debugPrint('ColorDebugExtension: ARGB collisions (expected):\n$msg');
}
return true;
}(), 'Just for count');
return _ResolvedNames(primaryByArgb, aliasesByArgb);
}
}
final class _ResolvedNames {
const _ResolvedNames(this.primaryByArgb, this.aliasesByArgb);
final Map<int, String> primaryByArgb;
final Map<int, List<String>> aliasesByArgb;
}
<!-- assets/colors/colors.xml -->
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="black">#000000</color>
<color name="white">#FFFFFF</color>
<color name="transparent">#00000000</color>
<!-- Primary -->
<color name="primary">#FFB4AB</color>
<color name="primary_light">#FFDAD6</color>
<color name="primary_dark">#FF8B81</color>
<!-- Secondary -->
<color name="secondary">#A8C7FA</color>
<color name="secondary_light">#D3E3FD</color>
<color name="secondary_dark">#7CACF8</color>
<!-- Background / Surface -->
<color name="background">#FFFBFE</color>
<color name="background_dark">#1C1B1F</color>
<color name="surface">#FFFFFF</color>
<color name="surface_dark">#2B2930</color>
<!-- Text -->
<color name="text_primary">#1C1B1F</color>
<color name="text_secondary">#49454F</color>
<color name="text_tertiary">#79747E</color>
<color name="text_on_primary">#FFFFFF</color>
<color name="text_dark">#E6E1E5</color>
<!-- Accent -->
<color name="accent">#F2B8B5</color>
<color name="accent_light">#FFF5F4</color>
<color name="success">#81C995</color>
<color name="warning">#FABD2F</color>
<color name="error">#FF6B6B</color>
<color name="info">#7CB9E8</color>
<!-- Functional -->
<color name="divider">#E7E0EC</color>
<color name="divider_dark">#49454F</color>
<color name="disabled">#CAC4D0</color>
<color name="disabled_dark">#49454F</color>
<color name="shadow">#1F000000</color>
<color name="shadow_dark">#3F000000</color>
<!-- Alarm Ring Screen Gradient -->
<color name="gradient_start">#FFF5F5</color>
<color name="gradient_middle">#FFF0F5</color>
<color name="gradient_end">#FFF8F0</color>
<!-- Recording / Playing -->
<color name="recording">#FF6B6B</color>
<color name="playing">#81C995</color>
<color name="paused">#FABD2F</color>
<!-- Card -->
<color name="card_light">#FFFBFE</color>
<color name="card_dark">#2B2930</color>
<color name="card_elevated">#FFFFFF</color>
<color name="card_elevated_dark">#37343B</color>
<!-- Overlay -->
<color name="overlay">#66000000</color>
<color name="scrim">#99000000</color>
<!-- Semantic -->
<color name="alarm_active">#81C995</color>
<color name="alarm_inactive">#CAC4D0</color>
<color name="today_highlight">#FFDAD6</color>
</resources>
name: flutter_gen_example
description: Example app for securely managing assets used in your app using the flutter_gen tool
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: none # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
environment:
sdk: ^3.10.4
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
flutter:
sdk: flutter
dev_dependencies:
build_runner: ^2.10.5
flutter_gen_runner: ^5.12.0
flutter_test:
sdk: flutter
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is included with your application,
# so that you can use the icons in the material Icons class.
uses-material-design: true
generate: true
assets:
- assets/colors/
# https://pub.dev/packages/flutter_gen_runner
flutter_gen:
line_length: 120 # Optional (default: 80)
colors:
enabled: true
outputs:
class_name: AppColors
inputs:
- assets/colors/colors.xml
// lib/debug/theme_extension.dart
// 🐦 Flutter imports:
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
// 🌎 Project imports:
import 'package:mirae/gen/colors.gen.dart';
/// Debug helpers for theme inspection.
///
/// Notes:
/// - Expensive stringification is gated behind debug-only paths.
/// - Color name resolution uses O(1) lookup via an ARGB->name reverse map.
extension ColorSchemeDebugExtension on ColorScheme {
/// Human-friendly dump of the ColorScheme.
///
/// In release/profile builds, returns a lightweight identifier only.
String get debug {
if (!kDebugMode) {
// Keep release/profile output cheap and predictable.
return 'ColorScheme.${brightness.name}';
}
return _debugImpl();
}
String _debugImpl() {
final items = <(String, Color)>[
('error', error),
('errorContainer', errorContainer),
('onError', onError),
('onErrorContainer', onErrorContainer),
('onInverseSurface', onInverseSurface),
('outline', outline),
('outlineVariant', outlineVariant),
('primary', primary),
('inversePrimary', inversePrimary),
('primaryContainer', primaryContainer),
('primaryFixed', primaryFixed),
('primaryFixedDim', primaryFixedDim),
('onPrimary', onPrimary),
('onPrimaryContainer', onPrimaryContainer),
('onPrimaryFixed', onPrimaryFixed),
('onPrimaryFixedVariant', onPrimaryFixedVariant),
('scrim', scrim),
('secondary', secondary),
('secondaryContainer', secondaryContainer),
('secondaryFixed', secondaryFixed),
('secondaryFixedDim', secondaryFixedDim),
('onSecondary', onSecondary),
('onSecondaryContainer', onSecondaryContainer),
('onSecondaryFixed', onSecondaryFixed),
('onSecondaryFixedVariant', onSecondaryFixedVariant),
('shadow', shadow),
('surface', surface),
('inverseSurface', inverseSurface),
('surfaceBright', surfaceBright),
('surfaceContainer', surfaceContainer),
('surfaceContainerHigh', surfaceContainerHigh),
('surfaceContainerHighest', surfaceContainerHighest),
('surfaceContainerLow', surfaceContainerLow),
('surfaceContainerLowest', surfaceContainerLowest),
('surfaceDim', surfaceDim),
('surfaceTint', surfaceTint),
('onSurface', onSurface),
('onSurfaceVariant', onSurfaceVariant),
('tertiary', tertiary),
('tertiaryContainer', tertiaryContainer),
('tertiaryFixed', tertiaryFixed),
('tertiaryFixedDim', tertiaryFixedDim),
('onTertiary', onTertiary),
('onTertiaryContainer', onTertiaryContainer),
('onTertiaryFixed', onTertiaryFixed),
('onTertiaryFixedVariant', onTertiaryFixedVariant),
];
final b = StringBuffer()..writeln('ColorScheme.${brightness.name}(');
for (final (name, color) in items) {
b.writeln(' $name: ${color.debug},');
}
b.writeln(')');
return b.toString();
}
}
extension ColorDebugExtension on Color {
static const Map<String, Color> _appColors = {
'black': AppColors.black,
'white': AppColors.white,
'transparent': AppColors.transparent,
'primary': AppColors.primary,
'primaryLight': AppColors.primaryLight,
'primaryDark': AppColors.primaryDark,
'secondary': AppColors.secondary,
'secondaryLight': AppColors.secondaryLight,
'secondaryDark': AppColors.secondaryDark,
'background': AppColors.background,
'backgroundDark': AppColors.backgroundDark,
'surface': AppColors.surface,
'surfaceDark': AppColors.surfaceDark,
'textPrimary': AppColors.textPrimary,
'textSecondary': AppColors.textSecondary,
'textTertiary': AppColors.textTertiary,
'textOnPrimary': AppColors.textOnPrimary,
'textDark': AppColors.textDark,
'accent': AppColors.accent,
'accentLight': AppColors.accentLight,
'success': AppColors.success,
'warning': AppColors.warning,
'error': AppColors.error,
'info': AppColors.info,
'divider': AppColors.divider,
'dividerDark': AppColors.dividerDark,
'disabled': AppColors.disabled,
'disabledDark': AppColors.disabledDark,
'shadow': AppColors.shadow,
'shadowDark': AppColors.shadowDark,
'gradientStart': AppColors.gradientStart,
'gradientMiddle': AppColors.gradientMiddle,
'gradientEnd': AppColors.gradientEnd,
'recording': AppColors.recording,
'playing': AppColors.playing,
'paused': AppColors.paused,
'cardLight': AppColors.cardLight,
'cardDark': AppColors.cardDark,
'cardElevated': AppColors.cardElevated,
'cardElevatedDark': AppColors.cardElevatedDark,
'overlay': AppColors.overlay,
'scrim': AppColors.scrim,
'alarmActive': AppColors.alarmActive,
'alarmInactive': AppColors.alarmInactive,
'todayHighlight': AppColors.todayHighlight,
};
static const Map<String, Color> _colorless = {
'transparent': Color(0x00000000),
'black': Color(0xFF000000),
'black87': Color(0xDD000000),
'black54': Color(0x8A000000),
'black45': Color(0x73000000),
'black38': Color(0x61000000),
'black26': Color(0x42000000),
'black12': Color(0x1F000000),
'white': Color(0xFFFFFFFF),
'white70': Color(0xB3FFFFFF),
'white60': Color(0x99FFFFFF),
'white54': Color(0x8AFFFFFF),
'white38': Color(0x62FFFFFF),
'white30': Color(0x4DFFFFFF),
'white24': Color(0x3DFFFFFF),
'white12': Color(0x1FFFFFFF),
'white10': Color(0x1AFFFFFF),
};
/// The Material Design primary color swatches.
static const Map<String, MaterialColor> _primaries = {
'red': Colors.red,
'pink': Colors.pink,
'purple': Colors.purple,
'deepPurple': Colors.deepPurple,
'indigo': Colors.indigo,
'blue': Colors.blue,
'lightBlue': Colors.lightBlue,
'cyan': Colors.cyan,
'teal': Colors.teal,
'green': Colors.green,
'lightGreen': Colors.lightGreen,
'lime': Colors.lime,
'yellow': Colors.yellow,
'amber': Colors.amber,
'orange': Colors.orange,
'deepOrange': Colors.deepOrange,
'brown': Colors.brown,
'grey': Colors.grey,
'blueGrey': Colors.blueGrey,
};
/// The Material Design accent color swatches.
static const Map<String, MaterialAccentColor> _accents = {
'redAccent': Colors.redAccent,
'pinkAccent': Colors.pinkAccent,
'purpleAccent': Colors.purpleAccent,
'deepPurpleAccent': Colors.deepPurpleAccent,
'indigoAccent': Colors.indigoAccent,
'blueAccent': Colors.blueAccent,
'lightBlueAccent': Colors.lightBlueAccent,
'cyanAccent': Colors.cyanAccent,
'tealAccent': Colors.tealAccent,
'greenAccent': Colors.greenAccent,
'lightGreenAccent': Colors.lightGreenAccent,
'limeAccent': Colors.limeAccent,
'yellowAccent': Colors.yellowAccent,
'amberAccent': Colors.amberAccent,
'orangeAccent': Colors.orangeAccent,
'deepOrangeAccent': Colors.deepOrangeAccent,
};
/// O(1) reverse lookup from ARGB to the "best" debug name.
///
/// Priority: AppColors > Colors constants > Colors primaries > Colors accents.
static final Map<int, String> _nameByArgb = _buildNameByArgb();
static Map<int, String> _buildNameByArgb() {
final out = <int, String>{};
// Track duplicates only in debug to avoid any runtime overhead in release.
final duplicates = <int, Set<String>>{};
void putIfAbsent(int argb, String name) {
final existing = out[argb];
if (existing == null) {
out[argb] = name;
} else if (existing != name) {
// Keep the earlier (higher-priority) name. Record duplicate in debug.
assert(() {
(duplicates[argb] ??= <String>{}).add(existing);
duplicates[argb]!.add(name);
return true;
}(), 'Keep the earlier (higher-priority) name. Record duplicate in debug.');
}
}
// 1) App-specific palette: highest priority.
for (final entry in _appColors.entries) {
putIfAbsent(entry.value.toARGB32(), 'AppColors.${entry.key}');
}
// 2) Core Colors constants: next priority.
for (final entry in _colorless.entries) {
putIfAbsent(entry.value.toARGB32(), 'Colors.${entry.key}');
}
// 3) Material primaries and shades.
for (final swatchEntry in _primaries.entries) {
final swatchName = swatchEntry.key;
final swatch = swatchEntry.value;
for (final shade in swatch.keys) {
final color = swatch[shade];
if (color == null) continue;
final name = shade == 500 ? 'Colors.$swatchName' : 'Colors.$swatchName.shade$shade';
putIfAbsent(color.toARGB32(), name);
}
}
// 4) Material accents and shades.
for (final swatchEntry in _accents.entries) {
final swatchName = swatchEntry.key;
final swatch = swatchEntry.value;
for (final shade in swatch.keys) {
final color = swatch[shade];
if (color == null) continue;
// Accent defaults are typically 200.
final name = shade == 200 ? 'Colors.$swatchName' : 'Colors.$swatchName.shade$shade';
putIfAbsent(color.toARGB32(), name);
}
}
assert(() {
if (duplicates.isNotEmpty) {
// Fail fast in debug: duplicates can make logs ambiguous.
final lines = duplicates.entries
.map(
(e) =>
'0x${e.key.toRadixString(16).padLeft(8, '0').toUpperCase()}: '
'${e.value.toList()..sort()}',
)
.join('\n');
throw FlutterError(
'Duplicate color ARGB mappings detected in ColorDebugExtension:\n$lines',
);
}
return true;
}(), 'Duplicate color ARGB mappings detected.');
return out;
}
static String _hexFromArgb(int argb) => '0x${argb.toRadixString(16).padLeft(8, '0').toUpperCase()}';
/// Debug-friendly string for this color.
///
/// - In debug builds: resolves to `AppColors.*` or `Colors.*` where possible.
/// - In release/profile: always returns a cheap, deterministic hex form.
String get debug {
final argb = toARGB32();
if (!kDebugMode) {
return 'const Color(${_hexFromArgb(argb)})';
}
final name = _nameByArgb[argb];
if (name != null) return name;
return 'const Color(${_hexFromArgb(argb)})';
}
}
@AndrewDongminYoo
Copy link
Author

AndrewDongminYoo commented Jan 15, 2026

EXAMPLE USAGE:

@override
Widget build(BuildContext context) {
  if (kDebugMode) {
    ref.listen<ThemeMode>(
      effectiveThemeModeProvider,
      (prev, next) {
        // ignore: always_put_control_body_on_new_line
        if (prev == next) return;
        WidgetsBinding.instance.addPostFrameCallback((_) {
          // ignore: always_put_control_body_on_new_line
          if (!mounted) return;
          final scheme = Theme.of(context).colorScheme;
          log('[Theme] effectiveThemeMode=$next, colorScheme=${scheme.debug}');
        });
      },
    );
  }

DEBUG CONSOLE:

ColorScheme.dark(
  error: AppColors.error /* aka: AppColors.recording */,
  errorContainer: AppColors.error /* aka: AppColors.recording */,
  onError: AppColors.textOnPrimary /* aka: AppColors.cardElevated, AppColors.surface, Colors.white, AppColors.white */,
  onErrorContainer: AppColors.textOnPrimary /* aka: AppColors.cardElevated, AppColors.surface, Colors.white, AppColors.white */,
  onInverseSurface: AppColors.cardDark /* aka: AppColors.surfaceDark */,
  outline: AppColors.textSecondary /* aka: AppColors.disabledDark, AppColors.dividerDark */,
  outlineVariant: AppColors.textOnPrimary /* aka: AppColors.cardElevated, AppColors.surface, Colors.white, AppColors.white */,
  primary: AppColors.primary,
  inversePrimary: AppColors.textOnPrimary /* aka: AppColors.cardElevated, AppColors.surface, Colors.white, AppColors.white */,
  primaryContainer: AppColors.primaryDark,
  primaryFixed: AppColors.primary,
  primaryFixedDim: AppColors.primary,
  onPrimary: AppColors.textOnPrimary /* aka: AppColors.cardElevated, AppColors.surface, Colors.white, AppColors.white */,
  onPrimaryContainer: AppColors.textOnPrimary /* aka: AppColors.cardElevated, AppColors.surface, Colors.white, AppColors.white */,
  onPrimaryFixed: AppColors.textOnPrimary /* aka: AppColors.cardElevated, AppColors.surface, Colors.white, AppColors.white */,
  onPrimaryFixedVariant: AppColors.textOnPrimary /* aka: AppColors.cardElevated, AppColors.surface, Colors.white, AppColors.white */,
  scrim: Colors.black /* aka: AppColors.black */,
  secondary: AppColors.secondary,
  secondaryContainer: AppColors.secondaryDark,
  secondaryFixed: AppColors.secondary,
  secondaryFixedDim: AppColors.secondary,
  onSecondary: AppColors.textOnPrimary /* aka: AppColors.cardElevated, AppColors.surface, Colors.white, AppColors.white */,
  onSecondaryContainer: AppColors.textOnPrimary /* aka: AppColors.cardElevated, AppColors.surface, Colors.white, AppColors.white */,
  onSecondaryFixed: AppColors.textOnPrimary /* aka: AppColors.cardElevated, AppColors.surface, Colors.white, AppColors.white */,
  onSecondaryFixedVariant: AppColors.textOnPrimary /* aka: AppColors.cardElevated, AppColors.surface, Colors.white, AppColors.white */,
  shadow: AppColors.shadowDark,
  surface: AppColors.cardDark /* aka: AppColors.surfaceDark */,
  inverseSurface: AppColors.textDark,
  surfaceBright: AppColors.cardDark /* aka: AppColors.surfaceDark */,
  surfaceContainer: AppColors.cardDark /* aka: AppColors.surfaceDark */,
  surfaceContainerHigh: AppColors.cardDark /* aka: AppColors.surfaceDark */,
  surfaceContainerHighest: AppColors.cardDark /* aka: AppColors.surfaceDark */,
  surfaceContainerLow: AppColors.cardDark /* aka: AppColors.surfaceDark */,
  surfaceContainerLowest: AppColors.cardDark /* aka: AppColors.surfaceDark */,
  surfaceDim: AppColors.cardDark /* aka: AppColors.surfaceDark */,
  surfaceTint: AppColors.primary,
  onSurface: AppColors.textDark,
  onSurfaceVariant: AppColors.textDark,
  tertiary: AppColors.secondary,
  tertiaryContainer: AppColors.secondary,
  tertiaryFixed: AppColors.secondary,
  tertiaryFixedDim: AppColors.secondary,
  onTertiary: AppColors.textOnPrimary /* aka: AppColors.cardElevated, AppColors.surface, Colors.white, AppColors.white */,
  onTertiaryContainer: AppColors.textOnPrimary /* aka: AppColors.cardElevated, AppColors.surface, Colors.white, AppColors.white */,
  onTertiaryFixed: AppColors.textOnPrimary /* aka: AppColors.cardElevated, AppColors.surface, Colors.white, AppColors.white */,
  onTertiaryFixedVariant: AppColors.textOnPrimary /* aka: AppColors.cardElevated, AppColors.surface, Colors.white, AppColors.white */,
)

@AndrewDongminYoo
Copy link
Author

AndrewDongminYoo commented Jan 15, 2026

File: lib/presentation/screens/settings/settings_screen.dart

final l10n = AppLocalizations.of(context);
final state = ref.watch(themeModeProvider);

unawaited(
  showDialog(
    context: context,
    builder: (dialogContext) => AlertDialog(
      title: Text(l10n.selectTheme),
      content: Column(
        mainAxisSize: MainAxisSize.min,
        children: AppThemeMode.values.map((mode) {
          return RadioListTile<AppThemeMode>(
            value: mode,
            groupValue: state,
            title: Text(mode.displayName),
            secondary: Icon(mode.icon),
            onChanged: (AppThemeMode? value) {
              if (value != null) {
                ref.read(themeModeProvider.notifier).setThemeMode(value);
                if (kDebugMode) {
                  final scheme = Theme.of(context).colorScheme;
                  log('[Theme] AppThemeMode=$state, colorScheme=${scheme.debug}');
                }
                Navigator.pop(dialogContext);
              }
            },
          );
        }).toList(),
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(dialogContext),
          child: Text(l10n.cancel),
        ),
      ],
    ),
  ),
);
[log] [Theme] AppThemeMode=Dark, colorScheme=ColorScheme.dark(
        error: AppColors.error /* aka: AppColors.recording */,
        errorContainer: AppColors.error /* aka: AppColors.recording */,
        onError: AppColors.white /* aka: AppColors.surface, AppColors.textOnPrimary, AppColors.cardElevated, Colors.white */,
        onErrorContainer: AppColors.white /* aka: AppColors.surface, AppColors.textOnPrimary, AppColors.cardElevated, Colors.white */,
        onInverseSurface: AppColors.surfaceDark /* aka: AppColors.cardDark */,
        outline: AppColors.textSecondary /* aka: AppColors.dividerDark, AppColors.disabledDark */,
        outlineVariant: AppColors.white /* aka: AppColors.surface, AppColors.textOnPrimary, AppColors.cardElevated, Colors.white */,
        primary: AppColors.primary,
        inversePrimary: AppColors.white /* aka: AppColors.surface, AppColors.textOnPrimary, AppColors.cardElevated, Colors.white */,
        primaryContainer: AppColors.primaryDark,
        primaryFixed: AppColors.primary,
        primaryFixedDim: AppColors.primary,
        onPrimary: AppColors.white /* aka: AppColors.surface, AppColors.textOnPrimary, AppColors.cardElevated, Colors.white */,
        onPrimaryContainer: AppColors.white /* aka: AppColors.surface, AppColors.textOnPrimary, AppColors.cardElevated, Colors.white */,
        onPrimaryFixed: AppColors.white /* aka: AppColors.surface, AppColors.textOnPrimary, AppColors.cardElevated, Colors.white */,
        onPrimaryFixedVariant: AppColors.white /* aka: AppColors.surface, AppColors.textOnPrimary, AppColors.cardElevated, Colors.white */,
        scrim: AppColors.black /* aka: Colors.black */,
        secondary: AppColors.secondary,
        secondaryContainer: AppColors.secondaryDark,
        secondaryFixed: AppColors.secondary,
        secondaryFixedDim: AppColors.secondary,
        onSecondary: AppColors.white /* aka: AppColors.surface, AppColors.textOnPrimary, AppColors.cardElevated, Colors.white */,
        onSecondaryContainer: AppColors.white /* aka: AppColors.surface, AppColors.textOnPrimary, AppColors.cardElevated, Colors.white */,
        onSecondaryFixed: AppColors.white /* aka: AppColors.surface, AppColors.textOnPrimary, AppColors.cardElevated, Colors.white */,
        onSecondaryFixedVariant: AppColors.white /* aka: AppColors.surface, AppColors.textOnPrimary, AppColors.cardElevated, Colors.white */,
        shadow: AppColors.shadowDark,
        surface: AppColors.surfaceDark /* aka: AppColors.cardDark */,
        inverseSurface: AppColors.textDark,
        surfaceBright: AppColors.surfaceDark /* aka: AppColors.cardDark */,
        surfaceContainer: AppColors.surfaceDark /* aka: AppColors.cardDark */,
        surfaceContainerHigh: AppColors.surfaceDark /* aka: AppColors.cardDark */,
        surfaceContainerHighest: AppColors.surfaceDark /* aka: AppColors.cardDark */,
        surfaceContainerLow: AppColors.surfaceDark /* aka: AppColors.cardDark */,
        surfaceContainerLowest: AppColors.surfaceDark /* aka: AppColors.cardDark */,
        surfaceDim: AppColors.surfaceDark /* aka: AppColors.cardDark */,
        surfaceTint: AppColors.primary,
        onSurface: AppColors.textDark,
        onSurfaceVariant: AppColors.textDark,
        tertiary: AppColors.secondary,
        tertiaryContainer: AppColors.secondary,
        tertiaryFixed: AppColors.secondary,
        tertiaryFixedDim: AppColors.secondary,
        onTertiary: AppColors.white /* aka: AppColors.surface, AppColors.textOnPrimary, AppColors.cardElevated, Colors.white */,
        onTertiaryContainer: AppColors.white /* aka: AppColors.surface, AppColors.textOnPrimary, AppColors.cardElevated, Colors.white */,
        onTertiaryFixed: AppColors.white /* aka: AppColors.surface, AppColors.textOnPrimary, AppColors.cardElevated, Colors.white */,
        onTertiaryFixedVariant: AppColors.white /* aka: AppColors.surface, AppColors.textOnPrimary, AppColors.cardElevated, Colors.white */,
      )

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment