From 4a54ca828583bb281ebe42fe684bf1758fb500ca Mon Sep 17 00:00:00 2001 From: Amal Krishna Date: Wed, 4 Sep 2024 21:57:48 +0530 Subject: [PATCH] Add static `of` accessor methods to `ColorScheme` and `TextTheme` (#154073) The most common use case to lookup a `ThemeData` instance using `Theme.of(context)` is to access either the `ColorScheme`, the `TextTheme`, or both. Before this change: ```dart final colors = Theme.of(context).colorScheme; final textTheme = Theme.of(context).textTheme; final primaryTextTheme = Theme.of(context).primaryTextTheme; ``` or ```dart final ThemeData( :colorScheme, :textTheme, :primaryTextTheme, ) = Theme.of(context); ``` After this change: ```dart final colors = ColorScheme.of(context); final textTheme = TextTheme.of(context); final primaryTextTheme = TextTheme.primaryOf(context); ``` ### Primary Changes This PR adds static `of` convenience methods to `ColorScheme` and `TextTheme` that delegate to the `ThemeData`'s respective properties. The methods added are: * `ColorScheme.of(context)` that returns `Theme.of(context).colorScheme`. * `TextTheme.of(context)` that returns `Theme.of(context).textTheme`. * `TextTheme.primaryOf(context)` that returns `Theme.of(context).primaryTextTheme`. ### Side-effects To allow the above changes to function, this PR adds: * A `theme.dart` import to `color_scheme.dart` to access to `Theme`. * A `theme.dart` import to `text_theme.dart` to access to `Theme`. * A `package:flutter/widgets.dart` import to `text_theme.dart` to access `BuildContext`. * The above import allowed getting rid of the same `@docImport` from `text_theme.dart`. ### Documentation updates This PR also updates the following documentation elements: * Adds docs to the newly added members. * Updates `TextTheme`'s docs to instruct using `TextTheme.of(context)` instead of `Theme.of(context).textTheme`. * Updates `Theme.of` to add a "See also" section to `ColorScheme.of` and `TextTheme.of` since these use cases are among the most common ones for `Theme.of(context)`. Fixes #72201. --- .../lib/src/material/color_scheme.dart | 7 ++- .../flutter/lib/src/material/text_theme.dart | 29 +++++++++--- packages/flutter/lib/src/material/theme.dart | 11 +++++ .../test/material/color_scheme_test.dart | 19 ++++++++ .../test/material/text_theme_test.dart | 45 +++++++++++++++++++ 5 files changed, 105 insertions(+), 6 deletions(-) diff --git a/packages/flutter/lib/src/material/color_scheme.dart b/packages/flutter/lib/src/material/color_scheme.dart index 9e9c43c7913..bd114d8095f 100644 --- a/packages/flutter/lib/src/material/color_scheme.dart +++ b/packages/flutter/lib/src/material/color_scheme.dart @@ -14,7 +14,7 @@ import 'package:flutter/widgets.dart'; import 'package:material_color_utilities/material_color_utilities.dart'; import 'colors.dart'; -import 'theme_data.dart'; +import 'theme.dart'; /// The algorithm used to construct a [ColorScheme] in [ColorScheme.fromSeed]. /// @@ -1920,4 +1920,9 @@ class ColorScheme with Diagnosticable { DynamicSchemeVariant.fruitSalad => SchemeFruitSalad(sourceColorHct: sourceColor, isDark: isDark, contrastLevel: contrastLevel), }; } + + /// The [ThemeData.colorScheme] of the ambient [Theme]. + /// + /// Equivalent to `Theme.of(context).colorScheme`. + static ColorScheme of(BuildContext context) => Theme.of(context).colorScheme; } diff --git a/packages/flutter/lib/src/material/text_theme.dart b/packages/flutter/lib/src/material/text_theme.dart index f4ff8410fbc..e363a822299 100644 --- a/packages/flutter/lib/src/material/text_theme.dart +++ b/packages/flutter/lib/src/material/text_theme.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -/// @docImport 'package:flutter/widgets.dart'; -/// /// @docImport 'elevated_button.dart'; /// @docImport 'material.dart'; /// @docImport 'outlined_button.dart'; @@ -13,8 +11,9 @@ library; import 'package:flutter/foundation.dart'; -import 'package:flutter/painting.dart'; +import 'package:flutter/widgets.dart'; +import 'theme.dart'; import 'typography.dart'; /// Material design text theme. @@ -23,8 +22,9 @@ import 'typography.dart'; /// (e.g., labelLarge, bodySmall). Rather than creating a [TextTheme] directly, /// you can obtain an instance as [Typography.black] or [Typography.white]. /// -/// To obtain the current text theme, call [Theme.of] with the current -/// [BuildContext] and read the [ThemeData.textTheme] property. +/// To obtain the current text theme, call [TextTheme.of] with the current +/// [BuildContext]. This is equivalent to calling [Theme.of] and reading +/// the [ThemeData.textTheme] property. /// /// The names of the TextTheme properties match this table from the /// [Material Design spec](https://m3.material.io/styles/typography/tokens). @@ -597,6 +597,25 @@ class TextTheme with Diagnosticable { ); } + /// The [ThemeData.textTheme] property of the ambient [Theme]. + /// + /// Equivalent to `Theme.of(context).textTheme`. + /// + /// See also: + /// * [TextTheme.primaryOf], which returns the [ThemeData.primaryTextTheme] property of + /// the ambient [Theme] instead. + static TextTheme of(BuildContext context) => Theme.of(context).textTheme; + + /// The [ThemeData.primaryTextTheme] property of the ambient [Theme]. + /// + /// + /// Equivalent to `Theme.of(context).primaryTextTheme`. + /// + /// See also: + /// * [TextTheme.of], which returns the [ThemeData.textTheme] property of the ambient + /// [Theme] instead. + static TextTheme primaryOf(BuildContext context) => Theme.of(context).primaryTextTheme; + @override bool operator ==(Object other) { if (identical(this, other)) { diff --git a/packages/flutter/lib/src/material/theme.dart b/packages/flutter/lib/src/material/theme.dart index b1b9844e125..5088253e60e 100644 --- a/packages/flutter/lib/src/material/theme.dart +++ b/packages/flutter/lib/src/material/theme.dart @@ -3,6 +3,8 @@ // found in the LICENSE file. /// @docImport 'app.dart'; +/// @docImport 'color_scheme.dart'; +/// @docImport 'text_theme.dart'; library; import 'package:flutter/cupertino.dart'; @@ -104,6 +106,15 @@ class Theme extends StatelessWidget { /// ); /// } /// ``` + /// + /// See also: + /// + /// * [ColorScheme.of], a convenience method that returns [ThemeData.colorScheme] + /// from the closest [Theme] ancestor. (equivalent to `Theme.of(context).colorScheme`). + /// * [TextTheme.of], a convenience method that returns [ThemeData.textTheme] + /// from the closest [Theme] ancestor. (equivalent to `Theme.of(context).textTheme`). + /// * [IconTheme.of], that returns [ThemeData.iconTheme] from the closest [Theme] or + /// [IconThemeData.fallback] if there is no [IconTheme] ancestor. static ThemeData of(BuildContext context) { final _InheritedTheme? inheritedTheme = context.dependOnInheritedWidgetOfExactType<_InheritedTheme>(); final MaterialLocalizations? localizations = Localizations.of(context, MaterialLocalizations); diff --git a/packages/flutter/test/material/color_scheme_test.dart b/packages/flutter/test/material/color_scheme_test.dart index 6668e18d873..fda22b5c90d 100644 --- a/packages/flutter/test/material/color_scheme_test.dart +++ b/packages/flutter/test/material/color_scheme_test.dart @@ -901,6 +901,25 @@ void main() { colorsMatchDynamicSchemeColors(schemeVariant, Brightness.dark, 0.5); } }); + + testWidgets('ColorScheme.of(context) is equivalent to Theme.of(context).colorScheme', (WidgetTester tester) async { + const Key sizedBoxKey = Key('sizedBox'); + final ColorScheme colorScheme = ColorScheme.fromSeed(seedColor: Colors.red); + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData.from(colorScheme: colorScheme), + home: const SizedBox(key: sizedBoxKey), + ), + ); + + final BuildContext context = tester.element(find.byKey(sizedBoxKey)); + final ColorScheme colorSchemeOfTheme = Theme.of(context).colorScheme; + final ColorScheme colorSchemeFromContext = ColorScheme.of(context); + + expect(colorSchemeOfTheme, colorScheme); + expect(colorSchemeFromContext, colorScheme); + }); } Future _testFilledButtonColor(WidgetTester tester, ColorScheme scheme, Color expectation) async { diff --git a/packages/flutter/test/material/text_theme_test.dart b/packages/flutter/test/material/text_theme_test.dart index d3d508f67d4..db59616805f 100644 --- a/packages/flutter/test/material/text_theme_test.dart +++ b/packages/flutter/test/material/text_theme_test.dart @@ -246,4 +246,49 @@ void main() { expect(fullLerp.horizontal, 2.0); expect(fullLerp.vertical, 1.0); }); + + testWidgets('TextTheme.of(context) is equivalent to Theme.of(context).textTheme', (WidgetTester tester) async { + const Key sizedBoxKey = Key('sizedBox'); + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + textTheme: const TextTheme( + displayLarge: TextStyle(color: Colors.blue, fontSize: 30.0), + ), + ), + home: const SizedBox(key: sizedBoxKey), + ), + ); + final BuildContext context = tester.element(find.byKey(sizedBoxKey)); + + final ThemeData themeData = Theme.of(context); + final TextTheme expectedTextTheme = themeData.textTheme; + final TextTheme actualTextTheme = TextTheme.of(context); + + expect(actualTextTheme, equals(expectedTextTheme)); + + }); + + testWidgets('TextTheme.primaryOf(context) is equivalent to Theme.of(context).primaryTextTheme', (WidgetTester tester) async { + const Key sizedBoxKey = Key('sizedBox'); + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + primaryTextTheme: const TextTheme( + displayLarge: TextStyle(backgroundColor: Colors.green, fontStyle: FontStyle.italic), + ), + ), + home: const SizedBox(key: sizedBoxKey), + ), + ); + + final BuildContext context = tester.element(find.byKey(sizedBoxKey)); + final ThemeData themeData = Theme.of(context); + final TextTheme expectedTextTheme = themeData.primaryTextTheme; + final TextTheme actualTextTheme = TextTheme.primaryOf(context); + + expect(actualTextTheme, equals(expectedTextTheme)); + }); }