iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🎨

[Flutter] Deep Dive into Handling 'withOpacity is deprecated' in Flutter 3.27.0 and Its Background

に公開

Introduction

Flutter's Color class has a convenient method, withOpacity, for changing the transparency (alpha value) of a color. This method allows you to add or modify the transparency of an existing color.

Flutter 3.27.0 was released in December 2024.
Along with this release, withOpacity, which had been in use until now, has been deprecated.

This article explains how to migrate from withOpacity and
delves into why it was deprecated from my own perspective.

Target Audience

  • Those who want to know how to migrate from withOpacity
  • Those who want to know why withOpacity was deprecated
  • Those who wonder, "Opacity? Alpha? What's that, can I eat it?"

Author's Environment at the Time of Writing

[✓] Flutter (Channel stable, 3.27.1, on macOS 15.1 24B2082 darwin-arm64, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK version 35.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 16.1)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2024.2)
[✓] VS Code (version 1.96.2)

Sample Project

Source Code

Title
import 'package:flutter/material.dart';

void main() {
  runApp(const MainApp());
}

class MainApp extends StatelessWidget {
  const MainApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: HomeScreen(),
    );
  }
}

class HomeScreen extends StatelessWidget {
  const HomeScreen({
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    const color = Colors.red;
    return Scaffold(
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            spacing: 10,
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              const ListTile(
                title: Text('Normal Color'),
                tileColor: color,
              ),
              ListTile(
                title: const Text('withOpacity is deprecated but still usable'),
                tileColor: color.withOpacity(0.5),
              ),
              ListTile(
                title: const Text('When using withAlpha'),
                tileColor: color.withAlpha(128),
              ),
              ListTile(
                title: const Text('Using withValues, which is recommended by the official, with alpha: 0.5'),
                tileColor: color.withValues(alpha: 0.5),
              ),
              ListTile(
                title: const Text('Using withValues, which is recommended by the official, with alpha: 128'),
                // Using withValues, recommended by the official, with alpha: 128
                tileColor: color.withValues(alpha: 128),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

1. Migration Method: Using withValues(alpha:)

To be blunt, the official documentation already provides the answer.

Simply replace the method you're using with withValues(alpha:)!

// Before: Create a new color with the specified opacity.
final x = color.withOpacity(0.0);

// After: Create a new color with the specified alpha channel value,
// accounting for the current or specified color space.
final x = color.withValues(alpha: 0.0);

https://docs.flutter.dev/release/breaking-changes/wide-gamut-framework#migrate-opacity

I confirmed that it works correctly in the sample code as well.

ListTile(
    title: const Text('Using withValues, which is recommended by the official, with alpha: 0.5'),
    tileColor: color.withValues(alpha: 0.5),
    ),

It's that simple, so if you just wanted to know the migration method, you can gently close this article here.
Oh, I'd appreciate it if you could hit the like button! ☺️

From now on, I will delve deeper into "Why was it deprecated?", which personally interested me.

However, to state the conclusion first:
I believe it's in preparation for properly rendering beautiful UIs, even on P3-compatible devices like iPhones and Macs.

I will explain this step by step.

2. The Existence of withValues and withAlpha

Looking at the implementation of withValues, it has other arguments as well.

  Color withValues(
      {double? alpha,
      double? red,
      double? green,
      double? blue,
      ColorSpace? colorSpace}) {
    // Omitted for now
  }

And all of them are nullable. It seems possible to change only red or change two or three components, not just transparency.
However, the Color class has a method withAlpha that changes only transparency.

"What, the official documentation should have told us about this one!"
I thought that, but when I tried to use it as is, I got an error.

ListTile(
    title: const Text('When using withAlpha'),
    tileColor: color.withAlpha(0.5),
),

The argument type 'double' can't be assigned to the parameter type 'int'.

Essentially, it's saying to provide an int.

The correct usage is as follows:

ListTile(
    title: const Text('When using withAlpha'),
    tileColor: color.withAlpha(128),
),

Wait, isn't the alpha value, which indicates transparency, a double?
Looking at the implementation of withAlpha, it states the following:

  /// Returns a new color that matches this color with the alpha channel
  /// replaced with `a` (which ranges from 0 to 255).
  ///
  /// Out of range values will have unexpected effects.
  Color withAlpha(int a) {
    return Color.fromARGB(a, red, green, blue);
  }

Returns a new color that matches this color with the alpha channel replaced with a (which ranges from 0 to 255).

While the alpha argument of the recommended withValues(alpha:) was a double type, here it expects an int type ranging from 0 to 255.
What does this mean?

This can be understood by delving deeper into the implementation.

3. Why is the withAlpha argument an int type?

Conclusion

withAlpha internally calls fromARGB and passes the value to it,
because its input format aligns with ARGB.
And probably withAlpha should not be used.

Let's delve into what this means, little by little.

Terminology Explanation

RGB
RGB is one of the basic formats for representing colors digitally, generating colors from a combination of three colors: Red, Green, and Blue.
Each component is specified in the range of 0-255. It does not include transparency (Alpha) and is characterized by representing opaque colors.

ARGB
ARGB is a format that adds transparency (Alpha) to the beginning, specified in the order of Alpha, Red, Green, and Blue.
Transparency is represented by an integer from 0-255, where 0 is fully transparent and 255 is fully opaque.
This format is often used in low-level graphics APIs and image processing, allowing efficient manipulation at the pixel level.

RGBA
RGBA is a format that adds transparency (Alpha) to the end, specified in the order of Red, Green, Blue, and Alpha.
Transparency is generally expressed as a decimal from 0.0-1.0, which is intuitive and easy to handle, so it is widely used in CSS and UI frameworks.

Flutter uses ARGB as the basic format for inputting colors

Flutter adopts the ARGB format, and colors are specified using the Color.fromARGB method. For example, Color.fromARGB(128, 255, 0, 0) represents a red color (R=255, G=0, B=0) with 50% transparency.
As mentioned in the conclusion, withAlpha passes its int type argument to the a argument of fromARGB internally.
Let's look at the implementation of fromARGB().

  /// Construct an sRGB color from the lower 8 bits of four integers.
  ///
  /// * `a` is the alpha value, with 0 being transparent and 255 being fully
  ///   opaque.
  /// * `r` is [red], from 0 to 255.
  /// * `g` is [green], from 0 to 255.
  /// * `b` is [blue], from 0 to 255.
  ///
  /// Out of range values are brought into range using modulo 255.
  ///
  /// See also [fromRGBO], which takes the alpha value as a floating point
  /// value.
  const Color.fromARGB(int a, int r, int g, int b)
      : this._fromARGBC(a, r, g, b, ColorSpace.sRGB);

  const Color._fromARGBC(
      int alpha, int red, int green, int blue, ColorSpace colorSpace)
      : this._fromRGBOC(
            red, green, blue, (alpha & 0xff) / 255, colorSpace);

/// Construct an sRGB color from the lower 8 bits of four integers.
///
/// * a is the alpha value, with 0 being transparent and 255 being fully
[Translation]
Constructs an sRGB color from the lower 8 bits of four integers.
a is the alpha value, where 0 is transparent and 255 is fully opaque.

In other words, fromARGB calls _fromARGBC, which converts the value input in ARGB to the specified ColorSpace.
ColorSpace will be discussed later, but for now, the alpha value is still an integer.

Next, looking at its content, it calls _fromRGBOC, and passes the alpha argument to it after calculating it as follows:

(alpha & 0xff) / 255
This expression is a process that "converts integer transparency (0-255) to a decimal (0.0-1.0) and restricts it to a safe value range."

alpha & 0xff

  • & is a bitwise AND operator.
  • 0xff is hexadecimal for "11111111" (all 8 bits are 1), which is decimal 255.
  • This operation extracts only the lowest 8 bits (1 byte) of alpha.
  • If alpha were to exceed the 0-255 range (e.g., 256 or -1), taking only the lowest 8 bits as a valid value discards out-of-range values.
  • This ensures that transparency calculations can be performed safely, even if alpha has an unintended value.

/ 255

  • Dividing the extracted 8-bit value (0-255) by 255 normalizes the transparency to a decimal (0.0-1.0).

And further tracing its contents, _fromRGBOC looks like this:

  /// Create an sRGB color from red, green, blue, and opacity, similar to
  /// `rgba()` in CSS.
  ///
  /// * `r` is [red], from 0 to 255.
  /// * `g` is [green], from 0 to 255.
  /// * `b` is [blue], from 0 to 255.
  /// * `opacity` is alpha channel of this color as a double, with 0.0 being
  ///   transparent and 1.0 being fully opaque.
  ///
  /// Out of range values are brought into range using modulo 255.
  ///
  /// See also [fromARGB], which takes the opacity as an integer value.
  const Color.fromRGBO(int r, int g, int b, double opacity)
      : this._fromRGBOC(r, g, b, opacity, ColorSpace.sRGB);

  const Color._fromRGBOC(int r, int g, int b, double opacity, this.colorSpace)
      : a = opacity,
        r = (r & 0xff) / 255,
        g = (g & 0xff) / 255,
        b = (b & 0xff) / 255;

/// Create an sRGB color from red, green, blue, and opacity, similar to
/// rgba() in CSS.
[Translation]
Creates an sRGB color from red, green, blue, and opacity.
Similar to CSS rgba().

As it says, it passes alpha as a double, as an argument for RGBA similar to CSS.
fromRGBOC means from red, green, blue, and opacity.
Here, the color components r, g, b are also finally converted to floating-point representation.

This means that the final alpha value is converted to a floating-point double type before being passed.

The official documentation recommends replacing fromARGB with from

The fromARGB method explained so far used the ARGB format for input.
However, the official documentation recommends replacing this method with a from method like the following:

// Before: Constructing an sRGB color from the lower 8 bits of four integers.
final magenta = Color.fromARGB(0xff, 0xff, 0x0, 0xff);

// After: Constructing a color with normalized floating-point components.
final magenta = Color.from(alpha: 1.0, red: 1.0, green: 0.0, blue: 1.0);

Constructors like Color.fromARGB remain unchanged and have continued support. To take advantage of Display P3 colors, you must use the new Color.from constructor that takes normalized floating-point color components.
[Translation]
Constructors like Color.fromARGB remain unchanged and have continued support.
To take advantage of Display P3 colors, you must use the new Color.from constructor that takes normalized floating-point color components.

Inferring from this official announcement, it seems that Flutter intends for developers to transition to using floating-point numbers for color-related values when working with Display P3 compatibility.

Summary

withAlpha is most likely an API that happened to be left over for some reason.
Since withValues is now recommended, it is highly probable that withAlpha will eventually become a deprecated API, so it's best not to use it.
For the same reason, it seems prudent to refrain from using other methods that change single components, such as withRed, withGreen, and withBlue.

4. Impact of ColorSpace Introduction for Display P3 Compatibility Preparation

ColorSpace Overview

Flutter 3.27.0 announced the introduction of a new ColorSpace enum.
https://docs.flutter.dev/release/breaking-changes/wide-gamut-framework#color-space-support

Flutter ColorSpace Documentation

This enum defines the following three types of color spaces:

enum ColorSpace {

  sRGB,

  extendedSRGB,

  displayP3,
}

Color space refers to a standard or model for representing colors in computers, displays, and printed materials.
It expresses color components (e.g., red, green, blue) as numerical values and combines them according to specific rules to reproduce colors.
Flutter states that it has defined the following three types from these standard models:

sRGB
It is a standard color space used in digital devices, where color is represented by specifying red, green, and blue components in the range of 0-255.
Designed based on human vision, it is adopted for a wide range of uses including web, displays, and printers.
Its adoption rate is very high, and there are almost no non-compliant devices, allowing for consistent color reproduction.

extendedSRGB
Flutter's unique method?
A color space that is backward compatible with sRGB and can represent colors outside its range with values other than [0..1].
To display extended values, you must use an [ImageByteFormat] such as [ImageByteFormat.rawExtendedRgba128].

Display P3
It is a wide gamut color space with a broader color range than sRGB, particularly rich in red and green color representation. Promoted by Apple, it is widely adopted in devices like iPhones and Macs. Optimized for displays based on DCI-P3, it automatically clamps to sRGB on non-compliant devices. It is used in situations where vivid color reproduction is desired, such as photography and video production.

This issue seems to be the origin of the problem:

https://github.com/flutter/flutter/issues/55092

Translation

Firstly, my app is full of images.
iPhones (iPhone 7 and later), iPads, Macs, and Android devices have been using Display P3 wide gamut for several years.
Providing users with old sRGB colors when their phone supports a 25% wider color gamut is like something from the last century.
Especially important for images.

Color mismatch between iOS and Android

Secondly, I don't know if it's related to the same issue, but there are color mismatches between Android and iOS when running from the same Flutter codebase.
More details here: #39113
Flutter needs to achieve the same quality as native code.

Thirdly, when creating a native iOS app, you can choose which color profile (sRGB/P3) to write the app in.
Therefore, if you choose to create a Flutter app (not a native app), you are restricted and cannot achieve the same results.
I want to achieve the same as native with Flutter!
Suggestion
Please add an option to choose which color profile you want to write your Flutter app in. That way, they are truly beautiful as intended. That's one of Flutter's pillars, isn't it?

withValues already has processing defined for ColorSpace

  /// Returns a new color that matches this color with the passed in components
  /// changed.
  ///
  /// Changes to color components will be applied before applying changes to the
  /// color space.
  Color withValues(
      {double? alpha,
      double? red,
      double? green,
      double? blue,
      ColorSpace? colorSpace}) {
    Color? updatedComponents;
    if (alpha != null || red != null || green != null || blue != null) {
      updatedComponents = Color.from(
          alpha: alpha ?? a,
          red: red ?? r,
          green: green ?? g,
          blue: blue ?? b,
          colorSpace: this.colorSpace);
    }
    if (colorSpace != null && colorSpace != this.colorSpace) {
      final _ColorTransform transform =
          _getColorTransform(this.colorSpace, colorSpace);
      return transform.transform(updatedComponents ?? this, colorSpace);
    } else {
      return updatedComponents ?? this;
    }
  }

  /// Construct a color with normalized color components.
  ///
  /// Normalized color components allows arbitrary bit depths for color
  /// components to be be supported. The values will be normalized relative to
  /// the [ColorSpace] argument.
  const Color.from(
      {required double alpha,
      required double red,
      required double green,
      required double blue,
      this.colorSpace = ColorSpace.sRGB})
      : a = alpha,
        r = red,
        g = green,
        b = blue;

According to this, Color.from, which is called inside withValues, currently defaults to ColorSpace.sRGB if no value is passed. What happens if ColorSpace.displayP3 is passed as an argument to withValues?

In that case, _getColorTransform(this.colorSpace, colorSpace); within the if statement of withValues obtains the conversion logic corresponding to the current ColorSpace and the target ColorSpace specified by the argument.

_getColorTransform
_ColorTransform _getColorTransform(ColorSpace source, ColorSpace destination) {
  // The transforms were calculated with the following octave script from known
  // conversions. These transforms have a white point that matches Apple's.
  //
  // p3Colors = [
  //   1, 0, 0, 0.25;
  //   0, 1, 0, 0.5;
  //   0, 0, 1, 0.75;
  //   1, 1, 1, 1;
  // ];
  // srgbColors = [
  //   1.0930908918380737,  -0.5116420984268188, -0.0003518527664709836, 0.12397786229848862;
  //   -0.22684034705162048, 1.0182716846466064,  0.00027732315356843174,  0.5073589086532593;
  //   -0.15007957816123962, -0.31062406301498413, 1.0420056581497192,  0.771118700504303;
  //   1,       1,       1,       1;
  // ];
  //
  // format long
  // p3ToSrgb = srgbColors * inv(p3Colors)
  // srgbToP3 = inv(p3ToSrgb)
  const _MatrixColorTransform srgbToP3 = _MatrixColorTransform(<double>[
    0.808052267214446, 0.220292047628890, -0.139648846160100,
    0.145738111193222, //
    0.096480880462996, 0.916386732581291, -0.086093928394828,
    0.089490172325882, //
    -0.127099563510240, -0.068983484963878, 0.735426667591299, 0.233655661600230
  ]);
  const _ColorTransform p3ToSrgb = _MatrixColorTransform(<double>[
    1.306671048092539, -0.298061942172353, 0.213228303487995,
    -0.213580156254466, //
    -0.117390025596251, 1.127722006101976, 0.109727644608938,
    -0.109450321455370, //
    0.214813187718391, 0.054268702864647, 1.406898424029350, -0.364892765879631
  ]);
  switch (source) {
    case ColorSpace.sRGB:
      switch (destination) {
        case ColorSpace.sRGB:
          return const _IdentityColorTransform();
        case ColorSpace.extendedSRGB:
          return const _IdentityColorTransform();
        case ColorSpace.displayP3:
          return srgbToP3;
      }
    case ColorSpace.extendedSRGB:
      switch (destination) {
        case ColorSpace.sRGB:
          return const _ClampTransform(_IdentityColorTransform());
        case ColorSpace.extendedSRGB:
          return const _IdentityColorTransform();
        case ColorSpace.displayP3:
          return const _ClampTransform(srgbToP3);
      }
    case ColorSpace.displayP3:
      switch (destination) {
        case ColorSpace.sRGB:
          return const _ClampTransform(p3ToSrgb);
        case ColorSpace.extendedSRGB:
          return p3ToSrgb;
        case ColorSpace.displayP3:
          return const _IdentityColorTransform();
      }
  }
}

From there, transform.transform(updatedComponents ?? this, colorSpace); uses the transform method of the logic (class) obtained in the transform variable to convert to Color type and return the result.

The `transform` variable corresponds to one of these classes, and executes the `transform` method within that class.
class _IdentityColorTransform implements _ColorTransform {
  const _IdentityColorTransform();
  @override
  Color transform(Color color, ColorSpace resultColorSpace) => color;
}

class _ClampTransform implements _ColorTransform {
  const _ClampTransform(this.child);
  final _ColorTransform child;
  @override
  Color transform(Color color, ColorSpace resultColorSpace) {
    return Color.from(
      alpha: clampDouble(color.a, 0, 1),
      red: clampDouble(color.r, 0, 1),
      green: clampDouble(color.g, 0, 1),
      blue: clampDouble(color.b, 0, 1),
      colorSpace: resultColorSpace);
  }
}

class _MatrixColorTransform implements _ColorTransform {
  /// Row-major.
  const _MatrixColorTransform(this.values);

  final List<double> values;

  @override
  Color transform(Color color, ColorSpace resultColorSpace) {
    return Color.from(
        alpha: color.a,
        red: values[0] * color.r +
            values[1] * color.g +
            values[2] * color.b +
            values[3],
        green: values[4] * color.r +
            values[5] * color.g +
            values[6] * color.b +
            values[7],
        blue: values[8] * color.r +
            values[9] * color.g +
            values[10] * color.b +
            values[11],
        colorSpace: resultColorSpace);
  }
}

Summary

As the Flutter official documentation states, if you want to properly support P3 on devices like iPhones and Macs and render beautiful UIs, you should use withValues.
Currently, it's still a transition period, so the benefits of withValues are hard to fully realize.
However, I have a strong feeling that this implementation will be adopted almost universally.

Conclusion

With the release of Flutter 3.27.0, withOpacity has been deprecated, and the use of withValues is now recommended.
In this article, I not only explained the migration method but also delved into the underlying reasons and the support for color spaces.

Currently, using withValues allows for flexible manipulation of transparency and color components while maintaining sRGB-based rendering.
Furthermore, it became clear that the design anticipates color reproduction on wide-gamut devices like Display P3 in the future.

Flutter's evolution raises expectations for compatibility with a wider variety of devices and color spaces.
By adopting withValues during this transition period, you will be able to adapt smoothly to future changes.
I hope this article has been helpful.

Thank you for reading to the end! If you have any questions or comments, please let me know. 🙇‍♂️

Discussion