Are .of(context) calls that expensive?

Hi,

I’ve been reading an interesting article from a few months back about how large number of calls to Theme.of() and MediaQuery.of() can really effect performance. I’ve read a few other comments on Reddit saying the same thing, and it’s recommended to cache the Theme and MediaQuery at the top of the build method. Does anyone know if this is a documented best practice?

If this is true, does it apply to all .of() methods including the hundreds of L.of() calls I have in my app for language strings.

The article in question can be found here: How a Common Flutter Mistake Slowed Our App by 30% – And the Fix | by Deepak Sharma | Medium

I need to invest some time learning how to dive into devtools, but wondered if this is a documented issue.

Many thanks!

1 Like

Every use of .of binds the rebuilding of this widget to the discovered parent widget. Use it when you need it, but be aware of the rebuilding. In particular, avoid MediaQuery.of because it will trigger when any of dozens of parameters fire. There are specific calls like .sizeOf that triggers only on size changes.

2 Likes

The golden rule in ANY language is:

  1. It is a property? You can use it wherever you want *
  2. It is a function? Cache it (i.e.: final theme = Theme.of(context) in the beginning of build method)

*: There are some cases where there are violations about this rule, and even Google break it (example: Beware of Color.value's performance)

2 Likes

Thanks Randal. I’ll definitely review all the MediaQuery calls.

Thanks! That’s a good rule to remember.

I don’t call it directly anymore I just do context.width or what ever thanks to this

// context_plus.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

extension BuildContextX on BuildContext {
  // ==== SCREEN & DEVICE INFO ====

  double get width => MediaQuery.widthOf(this);
  double get height => MediaQuery.heightOf(this);
  Size get screenSize => MediaQuery.sizeOf(this);

  bool get isLandscape =>
      MediaQuery.orientationOf(this) == Orientation.landscape;
  bool get isPortrait => !isLandscape;

  double get pixelRatio => MediaQuery.devicePixelRatioOf(this);

  T customTheme<T>() {
    assert(T != dynamic, 'T must be a specific type, not dynamic');
    return Theme.of(this).extension<T>()!;
  }

  // Safe area and insets
  EdgeInsets get viewInsets => MediaQuery.viewInsetsOf(this);
  EdgeInsets get padding => MediaQuery.paddingOf(this);
  EdgeInsets get safeAreaPadding => MediaQuery.paddingOf(this);
  EdgeInsets get viewPadding => MediaQuery.viewPaddingOf(this);
  TextScaler get textScaler => MediaQuery.textScalerOf(this);

  double get topPadding => padding.top;
  double get bottomPadding => padding.bottom;
  double get leftPadding => padding.left;
  double get rightPadding => padding.right;

  double get statusBarHeight => padding.top;
  double get navigationBarHeight => padding.bottom;

  bool get isKeyboardVisible => viewInsets.bottom > 0;
  double get keyboardHeight => viewInsets.bottom;

  // Screen categories
  bool get isSmallScreen => width < 600;
  bool get isMediumScreen => width >= 600 && width < 1200;
  bool get isLargeScreen => width >= 1200;

  bool get isTablet => width > 600;
  bool get isDesktop => width > 1200;
  bool get isMobile => width <= 600;

  // Aspect ratio helpers
  double get aspectRatio => width / height;
  bool get isSquareScreen => (aspectRatio - 1.0).abs() < 0.1;
  bool get isWideScreen => aspectRatio > 1.5;
  bool get isTallScreen => aspectRatio < 0.75;

  // ==== NAVIGATION ====

  NavigatorState get navigator => Navigator.of(this);

  Future<T?> push<T>(final Route<T> route) => navigator.push(route);

  Future<T?> pushNamed<T>(final String routeName, {final Object? arguments}) =>
      navigator.pushNamed<T>(routeName, arguments: arguments);

  void pop<T>([final T? result]) => navigator.pop<T>(result);

  void popUntil(final bool Function(Route<dynamic>) predicate) =>
      navigator.popUntil(predicate);

  /// Push and remove everything else.
  Future<T?> pushAndRemoveUntil<T>(final Route<T> route) =>
      navigator.pushAndRemoveUntil(route, (final _) => false);

  /// Push a named route and remove everything else.
  Future<T?> pushNamedAndRemoveUntil<T>(
    final String routeName, {
    final Object? arguments,
  }) => navigator.pushNamedAndRemoveUntil<T>(
    routeName,
    (final _) => false,
    arguments: arguments,
  );

  /// Replace the current route with a new one.
  Future<T?> pushReplacement<T>(final Route<T> newRoute) =>
      navigator.pushReplacement(newRoute);

  /// Push a named route and replace current
  Future<T?> pushReplacementNamed<T extends Object?, TO extends Object?>(
    final String routeName, {
    final TO? result,
    final Object? arguments,
  }) => navigator.pushReplacementNamed<T, TO>(
    routeName,
    result: result,
    arguments: arguments,
  );

  /// Pop to root (first route)
  void popToRoot() =>
      navigator.popUntil((final Route<dynamic> route) => route.isFirst);

  /// Explicit return type for linter
  bool get canPop => navigator.canPop();

  /// Pop with safety check
  void safePop<T>([final T? result]) {
    if (canPop) navigator.pop<T>(result);
  }


  // ==== THEME & STYLING ====

  ThemeData get theme => Theme.of(this);
  TextTheme get textTheme => Theme.of(this).textTheme;
  ColorScheme get colorScheme => Theme.of(this).colorScheme;

  // Colors
  Color get primaryColor => colorScheme.primary;
  Color get secondaryColor => colorScheme.secondary;
  Color get backgroundColor => colorScheme.surface;
  Color get surfaceColor => colorScheme.surface;
  Color get errorColor => colorScheme.error;
  Color get onPrimaryColor => colorScheme.onPrimary;
  Color get onSecondaryColor => colorScheme.onSecondary;
  Color get onBackgroundColor => colorScheme.onSurface;
  Color get onSurfaceColor => colorScheme.onSurface;
  Color get onErrorColor => colorScheme.onError;

  // Brightness
  bool get isDarkMode => theme.brightness == Brightness.dark;
  bool get isLightMode => !isDarkMode;
  Brightness get brightness => theme.brightness;

  // Text styles
  TextStyle? get displayLarge => textTheme.displayLarge;
  TextStyle? get displayMedium => textTheme.displayMedium;
  TextStyle? get displaySmall => textTheme.displaySmall;
  TextStyle? get headlineLarge => textTheme.headlineLarge;
  TextStyle? get headlineMedium => textTheme.headlineMedium;
  TextStyle? get headlineSmall => textTheme.headlineSmall;
  TextStyle? get titleLarge => textTheme.titleLarge;
  TextStyle? get titleMedium => textTheme.titleMedium;
  TextStyle? get titleSmall => textTheme.titleSmall;
  TextStyle? get bodyLarge => textTheme.bodyLarge;
  TextStyle? get bodyMedium => textTheme.bodyMedium;
  TextStyle? get bodySmall => textTheme.bodySmall;
  TextStyle? get labelLarge => textTheme.labelLarge;
  TextStyle? get labelMedium => textTheme.labelMedium;
  TextStyle? get labelSmall => textTheme.labelSmall;

  // ==== FOCUS & KEYBOARD ====

  void hideKeyboard() => FocusScope.of(this).unfocus();
  void showKeyboard() => FocusScope.of(this).requestFocus();

  FocusScopeNode get focusScope => FocusScope.of(this);
  bool get hasFocus => focusScope.hasFocus;
  bool get hasChildFocus => focusScope.hasPrimaryFocus;

  void nextFocus() => focusScope.nextFocus();
  void previousFocus() => focusScope.previousFocus();
  void unfocus() => focusScope.unfocus();

  // ==== SCAFFOLD & MATERIAL ====

  ScaffoldState get scaffold => Scaffold.of(this);
  ScaffoldMessengerState get scaffoldMessenger => ScaffoldMessenger.of(this);

  void openDrawer() => scaffold.openDrawer();
  void openEndDrawer() => scaffold.openEndDrawer();
  void closeDrawer() => scaffold.closeDrawer();
  void closeEndDrawer() => scaffold.closeEndDrawer();

  // SnackBar helpers
  ScaffoldFeatureController<SnackBar, SnackBarClosedReason> showSnackBar(
    final SnackBar snackBar,
  ) => scaffoldMessenger.showSnackBar(snackBar);

  void showTextSnackBar(
    final String text, {
    final Duration? duration,
    final SnackBarAction? action,
  }) {
    showSnackBar(
      SnackBar(
        content: Text(text),
        duration: duration ?? const Duration(seconds: 4),
        action: action,
      ),
    );
  }

  void showErrorSnackBar(final String message) {
    showSnackBar(SnackBar(content: Text(message), backgroundColor: errorColor));
  }

  void showSuccessSnackBar(final String message) {
    showSnackBar(
      SnackBar(
        content: Text(message),
        backgroundColor: Colors.green,
        duration: const Duration(seconds: 3),
      ),
    );
  }

  void hideCurrentSnackBar() => scaffoldMessenger.hideCurrentSnackBar();
  void clearSnackBars() => scaffoldMessenger.clearSnackBars();

  // ==== DIALOGS & MODALS ====

  Future<T?> showCustomDialog<T>({
    required final Widget Function(BuildContext) builder,
    final bool barrierDismissible = true,
    final Color? barrierColor,
    final String? barrierLabel,
    final bool useSafeArea = true,
  }) {
    return showDialog<T>(
      context: this,
      builder: builder,
      barrierDismissible: barrierDismissible,
      barrierColor: barrierColor,
      barrierLabel: barrierLabel,
      useSafeArea: useSafeArea,
    );
  }

  Future<T?> showConfirmDialog<T>({
    required final String title,
    required final String content,
    final String confirmText = 'OK',
    final String cancelText = 'Cancel',
    final bool barrierDismissible = true,
  }) {
    return showDialog<T>(
      context: this,
      barrierDismissible: barrierDismissible,
      builder: (final BuildContext context) => AlertDialog(
        title: Text(title),
        content: Text(content),
        actions: <Widget>[
          TextButton(
            onPressed: () => navigator.pop(false),
            child: Text(cancelText),
          ),
          TextButton(
            onPressed: () => navigator.pop(true),
            child: Text(confirmText),
          ),
        ],
      ),
    );
  }

  Future<T?> showBottomSheetModal<T>({
    required final Widget Function(BuildContext) builder,
    final bool isScrollControlled = false,
    final bool useRootNavigator = false,
    final bool isDismissible = true,
    final bool enableDrag = true,
    final Color? backgroundColor,
    final double? elevation,
  }) {
    return showModalBottomSheet<T>(
      context: this,
      builder: builder,
      isScrollControlled: isScrollControlled,
      useRootNavigator: useRootNavigator,
      isDismissible: isDismissible,
      enableDrag: enableDrag,
      backgroundColor: backgroundColor,
      elevation: elevation,
    );
  }

  // ==== RESPONSIVE HELPERS ====

  T responsive<T>({
    required final T mobile,
    final T? tablet,
    final T? desktop,
  }) {
    if (isDesktop && desktop != null) return desktop;
    if (isTablet && tablet != null) return tablet;
    return mobile;
  }

  double responsiveValue({
    required final double mobile,
    final double? tablet,
    final double? desktop,
  }) {
    if (isDesktop && desktop != null) return desktop;
    if (isTablet && tablet != null) return tablet;
    return mobile;
  }

  // Padding helpers
  EdgeInsets get defaultPadding => const EdgeInsets.all(16.0);
  EdgeInsets get smallPadding => const EdgeInsets.all(8.0);
  EdgeInsets get largePadding => const EdgeInsets.all(24.0);

  EdgeInsets get horizontalPadding =>
      const EdgeInsets.symmetric(horizontal: 16.0);
  EdgeInsets get verticalPadding => const EdgeInsets.symmetric(vertical: 16.0);

  // ==== SYSTEM INTEGRATION ====

  void hideSystemUI() {
    SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
  }

  void showSystemUI() {
    SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
  }

  void setSystemUIOverlay({
    final Color? statusBarColor,
    final Brightness? statusBarBrightness,
    final Brightness? statusBarIconBrightness,
    final Color? systemNavigationBarColor,
    final Brightness? systemNavigationBarIconBrightness,
  }) {
    SystemChrome.setSystemUIOverlayStyle(
      SystemUiOverlayStyle(
        statusBarColor: statusBarColor,
        statusBarBrightness: statusBarBrightness,
        statusBarIconBrightness: statusBarIconBrightness,
        systemNavigationBarColor: systemNavigationBarColor,
        systemNavigationBarIconBrightness: systemNavigationBarIconBrightness,
      ),
    );
  }

  void vibrate() {
    HapticFeedback.lightImpact();
  }

  void vibrateHeavy() {
    HapticFeedback.heavyImpact();
  }

  void vibrateMedium() {
    HapticFeedback.mediumImpact();
  }

  void vibrateSelection() {
    HapticFeedback.selectionClick();
  }

  // ==== UTILITY METHODS ====

  /// Get locale information
  Locale get locale => Localizations.localeOf(this);
  String get languageCode => locale.languageCode;
  String? get countryCode => locale.countryCode;

  /// Form state helper
  FormState? get form => Form.maybeOf(this);
  bool get hasForm => form != null;
  bool validateForm() => form?.validate() ?? false;
  void resetForm() => form?.reset();
  void saveForm() => form?.save();

  /// Get the nearest widget of type T
  T? findAncestor<T extends Widget>() => findAncestorWidgetOfExactType<T>();

  /// Check if widget is mounted
  bool get isMounted => mounted;

  /// Get render object
  RenderObject? get renderObject => findRenderObject();

  /// Media query shorthand
  MediaQueryData get mediaQuery => MediaQuery.of(this);

  /// Get text direction
  TextDirection get textDirection => Directionality.of(this);
  bool get isRTL => textDirection == TextDirection.rtl;
  bool get isLTR => !isRTL;

  /// Platform helpers
  TargetPlatform get platform => theme.platform;
  bool get isAndroid => platform == TargetPlatform.android;
  bool get isIOS => platform == TargetPlatform.iOS;
  bool get isWeb =>
      platform == TargetPlatform.linux ||
      platform == TargetPlatform.macOS ||
      platform == TargetPlatform.windows;
}
3 Likes

I don’t think it actually matters whether or not you call .of(context) multiple times in the same build method that much. Once the underlying dependsOnInheritedWidgetOfExactType is called, it keeps a reference to the ancestor type in a map on the current context. There are no multiple references to the same type.

It might technically reduce the code it has to execute a bit, but it doesn’t look like it could have a big impact. It will not change the amount of rebuilds for example.

However, I do think it can be a good practice to keep the widget tree part of your build method more readable if you keep the theme in a local variable first.

1 Like

I’d like to believe it does and it make sense when thinking about it, function calling is surely more expensive than accessing a cached value. In doubts, I asked Gemini to write a benchmark

Also I came across this proposal a year ago Inherited Theme: zero rebuilds · Issue #154567 · flutter/flutter · GitHub give a look at it, it sounds pretty in theory :slight_smile:

benchmark results for 100k Text widget in a simple ListView.

Performance are negligible to be honest, widget tree(s) are kept small in general, unless your tree rebuild a lot and very frequently I wouldn’t worry too much.

1 Like