Skip to content

Conversation

@LongCatIsLooong
Copy link
Contributor

@LongCatIsLooong LongCatIsLooong commented Oct 2, 2019

Description

  • CupertinoTheme & CupertinoTextTheme dark mode
  • Added IconThemeData.resolve & CupertinoIconThemeData
  • Added isSameColorAs matcher
  • Updated API docs

Related Issues

#35541

Tests

I added the following tests:

  • CupertinoTheme.of resolves colors

updated goldens: flutter/goldens#49

Checklist

Before you create this PR confirm that it meets all requirements listed below by checking the relevant checkboxes ([x]). This will ensure a smooth and quick review process.

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I signed the CLA.
  • I read and followed the Flutter Style Guide, including Features we expect every widget to implement.
  • I updated/added relevant documentation (doc comments with ///).
  • All existing and new tests are passing.
  • The analyzer (flutter analyze --flutter-repo) does not report any problems on my PR.
  • I am willing to follow-up on review comments in a timely manner.

Breaking Change

Does your PR require Flutter developers to manually update their apps to accommodate your change?

@fluttergithubbot fluttergithubbot added a: tests "flutter test", flutter_test, or one of our tests f: cupertino flutter/packages/flutter/cupertino repository f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels. labels Oct 2, 2019
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Breaking change. brightness is no longer used anywhere and would love to get rid of it in the near future.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

per our style guide: Please add a comment to all // ignore: explaining why the ignore is there and when (if at all) it can be removed.

@LongCatIsLooong LongCatIsLooong added c: API break Backwards-incompatible API changes will affect goldens Changes to golden files labels Oct 2, 2019
Copy link
Contributor

@justinmc justinmc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Comment on lines 546 to 548
/// The Cupertino Library provides built-in support for [CupertinoDynamicColor],
/// there are a few scenarios where you can skip writing the [CupertinoDynamicColor.resolve]
/// boilerplate.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this sentence needs a conjunction or to be split into 2 sentences, but this is obviously a nit.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rephrased the paragraph a bit.

/// When a Cupertino widget is provided with a `CupertinoDynamicColor`, either
/// directly in its constructor, or from an [InheritedWidget] it depends on (for example,
/// [DefaultTextStyle]), the widget will automatically resolve the color using
/// [CupertinoDynamicColor.resolve] against their own [BuildContext], on a best-effort
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"against its own"

/// return Container(color: resolvedColor);
/// })
/// ```
/// {@end-tool}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tell me if my understanding is correct. Natively, systemBlue changes slightly depending on dark/light mode (and maybe other things like high contrast mode). In Flutter CupertinoColors.systemBlue will always return some static default systemBlue color. Using CupertinoDynamicColor.resolve returns the right systemBlue color for the current dark/light mode etc. Also, if passing CupertinoColors.systemBlue to a Cupertino widget, it will automatically resolve the color to the right systemBlue color as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes exactly. As of now all Cupertino widgets (with 1 or 2 exceptions I think, some PRs are blocked or still waiting to be merged) resolve the colors given to them in their build or createRenderObject/updateRenderObject methods. But if a Cupertino widget takes a Border things get complicated because it can be a subclass of Border. I feel this is going to be a big gotcha but not sure how I can better document it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if you do pass an unresolved CupertinoDynamicColor to Border, does it use the light mode color as default? Would it be possible to log a warning in that case, or does it happen too commonly in other situations?

Now that I understand the problem the examples above are pretty clear, though. Maybe bring this up to the team and see if anyone has any ideas if you're still worried about it.

Copy link
Contributor Author

@LongCatIsLooong LongCatIsLooong Oct 4, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea! It sounds wrong to me to use a CupertinoDynamicColor that has never been resolved directly. Maybe putting some asserts in the CupertinoDynamicColor.value override to ensure no one does something like Paint()..color = CupertinoColors.systemBlue will be a viable approach. I'll try this out in a different PR to see if that breaks anything.

// This does not change the shadow color, etc.
Paint foreground, background;
if (style?.foreground != null)
foreground..color = CupertinoDynamicColor.resolve(style?.foreground?.color, context, nullOk: nullOk);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might as well be a single dot, right? Or am I missing something... Same goes for below.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just realized this is not working because Paint is not storing the color as is:

  set color(Color value) {
    assert(value != null);
    final int encoded = value.value ^ _kColorDefault;
    _data.setInt32(_kColorOffset, encoded, _kFakeHostEndian);
  }

I'll get rid of foreground and background.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh nice, sounds good!

Copy link
Member

@goderbauer goderbauer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple of nits, the approach seems fine.

// desired effect.
/// [CupertinoDynamicColor.color] will be the default effective color.
///
/// Sometimes manually resolving a [CupertinoDynamicColor] is not necessary, because
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sentence leaves me wondering: How do I as a developer know when to manually resolve and when not?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The next few sections are intended to answer that question. Should I replace the . at the end with : ?

///
/// ### Using a [CupertinoDynamicColor] in a Cupertino widget
///
/// When a Cupertino widget is provided with a `CupertinoDynamicColor`, either
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be enclosed in [] and not in`?

/// When a Cupertino widget is provided with a `CupertinoDynamicColor`, either
/// directly in its constructor, or from an [InheritedWidget] it depends on (for example,
/// [DefaultTextStyle]), the widget will automatically resolve the color using
/// [CupertinoDynamicColor.resolve] against its own [BuildContext], on a best-effort
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why best-effort? What are the failure cases?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're given an instance of a class that may contain CupertinoDynamicColors (e.g. Border or TextStyle), we can't reliably determine what needs to be resolved in that class since we don't know beforehand what its runtime type is.

/// })
/// ```
///
/// ```dart
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does having two ```dart sections in the same {@tool ...} render correctly in dartdocs?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it ended up looking like this:

Builder(builder: (BuildContext context) {
  final Color resolvedColor = CupertinoDynamicColor.resolve(CupertinoColors.systemBlue, context);
  return CupertinoNavigationBar(
    // CupertinoNavigationBar does not know how to resolve colors used in
    // a Border class.
    border: Border(bottom: BorderSide(color: resolvedColor)),
  );
})

// ...

Builder(builder: (BuildContext context) {
  final Color resolvedColor = CupertinoDynamicColor.resolve(CupertinoColors.systemBlue, context);
  // Container is not a Cupertino widget.
  return Container(color: resolvedColor);
})

Thanks for pointing this out!

/// * [CupertinoUserInterfaceLevel], an [InheritedWidget] that may affect color
/// resolution of a `CupertinoDynamicColor`.
/// * https://developer.apple.com/documentation/uikit/uicolor/3238042-resolvedcolor.
///
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: removed this blank line

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

per our style guide: Please add a comment to all // ignore: explaining why the ignore is there and when (if at all) it can be removed.

this._navActionTextStyle,
this._pickerTextStyle,
this._dateTimePickerTextStyle,
) : assert((_navActionTextStyle != null && _actionTextStyle != null) || _primaryColor != null);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Document this requirement on the public constructor?


/// Returns a copy of the current [CupertinoTextThemeData] with all the colors
/// resolved against the given [BuildContext].
CupertinoTextThemeData resolveFrom(BuildContext context, { bool nullOk = false }) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I know this was already missing before you made your change, but:): It would be cool to document what the nullOk parameters does.

/// specified overrides.
CupertinoTextThemeData copyWith({
Color primaryColor,
@deprecated
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move the annotation to the same line as the property that is deprecated?

final Color labelColor;
final Color inactiveGrayColor;

static TextStyle applyLabelColor(TextStyle original, Color color) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this method be private?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The class is private and since the implementation of it depends on the TextStyle constant defaults, it should probably never be made public so I guess its fine?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've made a habit of making private class methods public, I think this is OK.

Copy link
Contributor

@HansMuller HansMuller left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Progress report. I'm still working through this PR.

///
/// {@tool sample}
/// The following snippet builds a [CupertinoButton] that appears white in light
/// mode, and changes automatically to black in dark mode.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't want developers to think that they must do this to support dark mode. Maybe briefly explain that CupertinoButton's color is a dynamic color by default...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does

By default a [CupertinoButton] has no background color. The following sample
code shows how to build a [CupertinoButton] that appears white in light mode,
and changes automatically to black in dark mode.

Sound?

/// color: CupertinoColors.white,
/// darkColor: CupertinoColors.black,
/// ),
/// onPressed: () => null,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please reserve the => notation for one-liners whose return type is not void.

onPressed: () { },

/// [MediaQuery], etc.
///
/// ```dart
/// Builder(builder:(BuildContext context) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since code fragments like this are really just about the Builder's return value, I think it would be better to leave out the Builder. I realize that there's a small (acceptable) risk that the developer will put the fragment in a context that doesn't inherit what they expect. Here and elsewhere.

///
/// ### Manually Resolving a [CupertinoDynamicColor]
///
/// When used outside of a Cupertino component, or wrapped in an object opaque to
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe describe this situation as creating a custom Cupertino component that configures ordinary widgets with dynamic colors.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to

When used to configure a non-Cupertino widget, or wrapped in an object opaque
to the receiving Cupertino component, a [CupertinoDynamicColor] may need to be
manually resolved using [CupertinoDynamicColor.resolve], before it can used
to paint. For example, to use a custom [Border] in a [CupertinoNavigationBar],
the colors used in the [Border] have to be resolved manually before being passed
to [CupertinoNavigationBar]'s constructor.

/// that fits the given [BuildContext].
///
/// This method gives the ambient [IconThemeData] a chance to update itself,
/// after retrieved by [IconTheme.of], and before being returned as the final
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

after retrieved => after it has been retrieved

/// * [CupertinoIconThemeData.resolve] an implementation that resolves
/// [CupertinoIconThemeData.color] before returning.
@protected
IconThemeData resolve(BuildContext context) => this;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this a breaking change? I don't think we call adding a method to a public class a breaking change (even though it could cause a name collision).

//
// Values derived from https://developer.apple.com/design/resources/.
const TextStyle _kDefaultLightTextStyle = TextStyle(
const TextStyle _kDefaultTextStyle = TextStyle(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we code-generate all of these values with an iOS app?

/// Returns a default [CupertinoThemeData] if no [CupertinoTheme] widgets
/// exist in the ancestry tree.
/// Retrieve the [CupertinoThemeData] from an ancestor [CupertinoTheme] widget,
/// or a default [CupertinoThemeData] if no [CupertinoTheme] widgets exist
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if no [CupertinoTheme] ancestors exist. Resolve all of the colors defined ...

);

/// Applies a visual styling theme to descendant Cupertino widgets.
///
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be helpful if we explained here (and probably elsewhere) that the CupertinoTheme can be overriden and that the default one is provided by CupertinoApp. Along the same lines: explain that MaterialApp includes a MaterialBasedCupertinoTheme, it will be visible to Cupertino widgets created within the MaterialApp, and it can be overridden with a CupertinoTheme.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CupertinoApp and Theme are briefly mentioned in the see also section. Expanded it a little bit:

See also:

  • [CupertinoThemeData], specifies the theme's visual styling.
  • [CupertinoApp], which will automatically add a [CupertinoTheme] based on the
    value of [CupertinoApp.theme].
  • [Theme], a Material theme which will automatically add a [CupertinoTheme]
    with a [CupertinoThemeData] derived from the Material [ThemeData].

Copy link
Contributor

@HansMuller HansMuller left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This generally looks good, most of the feedback is about docs.

///
/// If no ancestral [CupertinoTheme] widget with explicit brightness value could
/// be found, the method will resort to the closest ancestor [MediaQuery] widget.
/// If no [CupertinoTheme] ancestor with explicit brightness value could be
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with explicit => with an explicit

bool get _isLight => _brightness != Brightness.dark;

final TextStyle _textStyle;
/// Typography of general text content for Cupertino widgets.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably describe these values as "text styles" not typography. If for no other reason than https://api.flutter.dev/flutter/material/Typography-class.html

final Color labelColor;
final Color inactiveGrayColor;

static TextStyle applyLabelColor(TextStyle original, Color color) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've made a habit of making private class methods public, I think this is OK.

/// [CupertinoColors.activeOrange] when [brightness] is light or dark.
/// elements. Defaults to [CupertinoColors.activeBlue].
///
/// If coming from a Material [Theme] and unspecified, [primaryColor] will be
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would help to refer to MaterialBasedCupertinoThemeData here; assuming that class has a good explanation for why it exists, how it's created, etc.

Color get primaryColor => _primaryColor ?? _defaults.primaryColor;
final Color _primaryColor;

/// A color used for content that must contrast against a [primaryColor] background.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A color that must be easy to see when rendered on a [primaryColor] background.

/// Derived from [brightness] and [primaryColor] if unspecified, including
/// [brightness] and [primaryColor] of a Material [ThemeData] if coming
/// from a Material [Theme].
/// Derived [primaryColor] if unspecified, including [primaryColor] of a Material
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is difficult to parse. Is the text color for all of the textTheme TextStyles the [primaryColor]?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not all of them, just the ones that used for interactive elements (e.g., the back button on an ios navigation bar).

return _barBackgroundColor ??
(_isLight ? _kDefaultBarLightBackgroundColor : _kDefaultBarDarkBackgroundColor);
}
/// on the brightness of the [BuildContext] it last resolved against.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[BuildContext] doesn't have a brightness.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to

Defaults to a light gray in light mode, or a dark translucent gray color in
dark mode.

/// Return a new `CupertinoThemeData` with all its colors resolved aginst the
/// given [BuildContext].
///
/// It will be called in [CupertinoTheme.of].
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you mean that this method is called by [CupertinoTheme.of]?

/// Create a copy of [CupertinoThemeData] with specified attributes overridden.
///
/// Only the current instance's specified attributes are copied instead of
/// derived values. For instance, if the current [primaryColor] is implied
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit confusing. What does it mean to imply the primaryColor in the context of copyWith? This comment seems to imply that myTheme.copyWith(primaryColor: foo) will create a copy of myTheme with the textTheme's color changed to foo? Most of our copyWith methods don't change dependent values and this one doesn't seem to?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah sorry I should've updated the example

LongCatIsLooong added a commit to flutter/goldens that referenced this pull request Oct 18, 2019
Copy link
Contributor

@HansMuller HansMuller left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM


/// Returns a copy of the current [CupertinoTextThemeData] with all the colors
/// resolved against the given [BuildContext].
/// resolved against the given [BuildContext]. Throws an exception if any of the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need a blank line here. Like

  /// Returns a copy of the current [CupertinoTextThemeData] with all the colors
  /// resolved against the given [BuildContext].
  ///
  /// Throws an exception...

@Piinks
Copy link
Contributor

Piinks commented Oct 28, 2019

The flutter/goldens repo is no longer in play here, which is why the golden file tests are failing.
It looks like an ignore needs to be put in place for the affected tests to pass.
There is a guide laid out in Writing a golden file test for package:flutter, let me know if I can help. :)

@LongCatIsLooong LongCatIsLooong merged commit 4ac2daf into flutter:master Oct 28, 2019
@LongCatIsLooong LongCatIsLooong deleted the dark-theme branch October 28, 2019 22:50
@Piinks
Copy link
Contributor

Piinks commented Oct 29, 2019

I think this is all set now and triaged? I am deleting the ignore from the Flutter Gold dashboard.

@Hixie
Copy link
Contributor

Hixie commented Nov 11, 2019

What should people use if they were using brightness before?

@LongCatIsLooong
Copy link
Contributor Author

@Hixie

What should people use if they were using brightness before?

They can keep using whatever value they're using. CupertinoTextThemeData.brightness is only useful when generating default values for CupertinoTextThemeData. Now that the default values themselves are brightness-aware, this parameter can be entirely ignored and its value does not affect anything.

Inconnu08 pushed a commit to Inconnu08/flutter that referenced this pull request Nov 26, 2019
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Aug 3, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

a: tests "flutter test", flutter_test, or one of our tests c: API break Backwards-incompatible API changes f: cupertino flutter/packages/flutter/cupertino repository f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels. will affect goldens Changes to golden files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants