🎨

【Flutter】Flutter3.27.0で指摘される'withOpacity' is deprecated'への対応とその背景を深ぼる

2025/01/01に公開

はじめに

FlutterのColorクラスには、色の透明度(アルファ値)を変更するための便利なメソッドwithOpacityがあります。このメソッドを使うと、既存の色に透明度を加えたり変更したりすることができます。

この度、2024年12月にFlutter3.27.0がリリースされました。
それに伴い今まで使用できていたwithOpacityが非推奨になりました。

この記事ではwithOpacityの移行方法についての解説と、
なぜ非推奨になったのかを自分なりに深掘りしていきます。

記事の対象者

  • withOpacityの移行方法について知りたい方
  • なんでwithOpacityが非推奨になったかを知りたい方
  • opacity?alpha?何それ美味しいの?っていう方

記事を執筆時点での筆者の環境

[✓] 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)

サンプルプロジェクト

ソースコード

タイトル
import 'package:flutter/material.dart';

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

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

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

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

  
  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('普通のカラー'),
                tileColor: color,
              ),
              ListTile(
                title: const Text('withOpacityは非推奨ですが、まだ使える'),
                tileColor: color.withOpacity(0.5),
              ),
              ListTile(
                title: const Text('withAlphaを使った場合'),
                tileColor: color.withAlpha(128),
              ),
              ListTile(
                title: const Text('公式が推奨しているwithValuesを使い、alphaに0.5を指定'),
                tileColor: color.withValues(alpha: 0.5),
              ),
              ListTile(
                title: const Text('公式が推奨しているwithValuesを使い、alphaに128を指定'),
                //
                tileColor: color.withValues(alpha: 128),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

1. 移行方法:withValues(alpha:)を使う

身も蓋もないですが、公式ですでに答えを書いてくれています。

使用するメソッドをそのまま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

サンプルコードでも正確に動作することを確認しました。

ListTile(
    title: const Text('公式が推奨しているwithValuesを使い、alphaに0.5を指定'),
    tileColor: color.withValues(alpha: 0.5),
    ),

たったこれだけなので、「とりあえず移行方法を知りたいんだ!」と言う方はここでそっと閉じてください。
あ、いいねは押してもらえると嬉しいです☺️

これ以降では個人的に気になった「なんで非推奨になったのか?」を深掘りしていきます。

ただ先に結論を述べておくと
iPhoneやMacのようなP3対応のデバイスにもちゃんと対応して、綺麗なUIを描画させるための準備のため
と考えます。

順をおって説明していきます。

2. withValuesとwithAlphaの存在

withValuesの実装を見てみると引数は他にもあります。

  Color withValues(
      {double? alpha,
      double? red,
      double? green,
      double? blue,
      ColorSpace? colorSpace}) {
    // 一旦省略
  }

しかも全部null許容。なんなら透明度だけに限らずredだけ変更する、または2、3こ変更することもできそうです。
ただ、Colorクラスには透明度だけを変更するメソッドwithAlphaがあります。

「なんだよ、公式はこっちを教えてくれればいいのに!」
そう思ったのですが、そのまま使ってみるとエラが出ます。

ListTile(
    title: const Text('withAlphaを使った場合'),
    tileColor: color.withAlpha(0.5),
),

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

要するに、intを入れなさいと言っています。

正しくは以下です。

ListTile(
    title: const Text('withAlphaを使った場合'),
    tileColor: color.withAlpha(128),
),

あれ、透明度を示す値であるalphaってdoubleじゃないの?
withAlphaの実装をみると、以下のように記述があります。

  /// 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);
  }

アルファチャンネルを a で置き換えた(0 から 255 の範囲)この色に一致する新しい色を返します。

推奨してきているwithValues(alpha:)の引数はdouble型だったのにここでは0~255のint型を求めてきている。
どうゆうことでしょう?

これはさらに実装を深掘りしていくと理解できます。

3. なぜwithAlphaの引数はint型なのか?

結論

withAlphaは内部でfromARGBを呼び出しそこに値を渡すが、
その入力形式が ARGBに沿っているから。
そしておそらくwithAlphaは使わない方が良い。

どうゆうことか少しづつ深掘りしていきます。

単語の説明

RGB
RGB は、デジタルで色を表現する基本的な形式の一つで、赤(Red)、緑(Green)、青(Blue)の3色の組み合わせで色を生成します。
それぞれの成分は 0~255の範囲で指定されます。透明度(Alpha)は含まれず、不透明な色を表現するのが特徴です。

ARGB
ARGB は、透明度(Alpha)を先頭に追加した形式で、Alpha、Red、Green、Blueの順に指定されます。
透明度は 0~255の整数で表され、0が完全に透明、255が完全に不透明を意味します。
この形式は低レベルのグラフィックAPIや画像処理で使用されることが多く、ピクセル単位で効率的な操作が可能です。

RGBA
RGBA は、透明度(Alpha)を末尾に追加した形式で、Red、Green、Blue、Alphaの順に指定されます。
透明度は一般的に 0.0~1.0の小数で表現され、直感的で扱いやすいため、CSSやUIフレームワークなどで広く使用されています。

Flutterでは色を入力する基本形式をARGBとしている

Flutterでは、ARGB形式が採用されており、Color.fromARGBメソッドを使って色を指定します。たとえば、Color.fromARGB(128, 255, 0, 0) は赤色(R=255, G=0, B=0)の透明度50%を表します。
結論でも述べたようにwithAlphaはその引数のint型の値を内部でfromARGBの引数aに渡しています。
では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
[和訳]
4つの整数の下位8ビットからsRGBカラーを構築します。
a はアルファ値で、0は透明、255は完全に表示される

つまり、fromARGBはARGBで入れた値から指定されたColorSpaceに変換する_fromARGBCを呼び出しています。
ColorSpaceについては後述しますが、ひとまずこの段階ではまだalphaの値は整数です。

次にその中身をみると_fromRGBOCを呼び出すのですが、ここに引数alphaとして渡された値を以下のように計算して渡しています。

(alpha & 0xff) / 255
この式は、「整数の透明度(0~255)を小数(0.0~1.0)に変換し、安全な値範囲に制限する」処理です。

alpha & 0xff

  • & はビット単位のAND演算子です。
  • 0xff は16進数で「11111111」(8ビットの全ビットが1)を意味し、値としては 10進数の255 です。
  • この処理は、alpha の最下位8ビット(1バイト分)だけを取り出します。
  • alpha が仮に 0~255 を超える範囲(例えば256や-1など)だった場合、最下位8ビットだけを有効な値として取り出すことで、範囲外の値を切り捨てます。
  • これにより、alpha が意図しない範囲の値になっても、透明度の計算が安全に行えます。

/ 255

  • 取り出した8ビットの値(0~255)を 255で割る ことで、透明度を 小数(0.0~1.0) に正規化します。

そしてさらに中身を追うと_fromRGBOCはこうなっています。

  /// 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.
[和訳]
赤、緑、青、不透明度からsRGBカラーを作成します。
CSSのrgba()と同様です。

そのままですが、CSSと同様のRGBAの引数としてalphaもdoubleにして渡しているのです。
fromRGBOCfrom red, green, blue, and opacityということですね。
ここでは最終的に色成分であるr,g,bも小数点表示に変換しています。

つまり最終的にalphaの値は浮動小数点型であるdoubleに変換して渡されるのです。

公式でfromARGBfromに置き換えるように推奨している

ここまで説明してきたfromARGBメソッドは前述の通りARGB形式での入力でした。
しかし、公式ではこのメソッドを以下のようなfromメソッドに置き換えるように推奨しています。

// 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.
[和訳]
Color.fromARGBのようなコンストラクタは変更されず、引き続きサポートされています。
ディスプレイP3の色を利用するには、正規化された浮動小数点カラーコンポーネントを使用する新しいColor.fromコンストラクタを使用する必要があります

この公式の発表から推測するに、ディスプレイP3に対応するにあたって色に関わる数値に関しては浮動小数点で入力するように、移行してほしいという意思が伝わってきます。

まとめ

withAlpha はなんらかの理由でたまたま残されているAPIの可能性が高いです。
今後はwithValues を使うように推奨していることからいずれ非推奨なAPIになる可能性が高いので使わない方が良いと考えます。
同じ理由でその他の単体で変更するメソッドである、withRed,withGreen,withBlueも使用を控えた方が良さそうです。

4. DisplayP3対応準備に向けたColorSpaceの導入による影響

ColorSpace概要

Flutter3.27.0では新たにColorSpaceという列挙型を導入したと発表がありました。
https://docs.flutter.dev/release/breaking-changes/wide-gamut-framework#color-space-support

Flutter ColorSpace Documentation

この列挙型には色空間として以下の3つの種類が定義されています。

enum ColorSpace {

  sRGB,

  extendedSRGB,

  displayP3,
}

色空間とは、コンピュータやディスプレイ、印刷物などで色を表現するための基準やモデルです。
色の成分(例: 赤、緑、青など)を数値で表し、それらを特定の規則で組み合わせて色を再現します。
その基準モデルの中からFlutterでは以下の3タイプを定義したと言っています。

sRGB
デジタル機器で標準的に使用される色空間で、赤(Red)、緑(Green)、青(Blue)の成分を0~255の範囲で指定し、色を表現します。
人間の視覚に基づいて設計され、Web、ディスプレイ、プリンターなど幅広い用途で採用されています。
普及率が非常に高く、非対応デバイスはほとんど存在しないため、一貫性のある色再現が可能です。

extendedSRGB
Flutter独自の方式?
sRGBと後方互換性があり、その範囲外の色を[0..1]以外の値で表現できる色空間。
拡張された値を表示するには、[ImageByteFormat.rawExtendedRgba128]のような[ImageByteFormat]を使用する必要があります。

Display P3
sRGBより広い色域を持つ広色域(Wide Gamut)色空間で、特に赤と緑の色表現が豊かです。Appleが推進しており、iPhoneやMacなどのデバイスで広く採用されています。DCI-P3をベースにディスプレイ用に最適化されており、非対応デバイスでは自動的にsRGBにクランプされます。写真や映像制作など、鮮やかな色再現が求められる場面で使われます。

このissueがことの発端のようです。

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

和訳

まず、私のアプリは画像でいっぱいです。
iPhone(iPhone 7以降)、iPad、Mac、Androidデバイスは、数年前からDisplay P3ワイドファイマンを使用しています。
携帯電話が25%広い色空間を提供するより広いP3色域をサポートしているときに、ユーザーに古いsRGB色を提供するのは前世紀のようなものです。
特に画像にとって重要です。

iOSとAndroidの色の不一致

第二に、同じ問題に関連しているかどうかはわかりませんが、同じFlutterコードベースから実行する場合、AndroidとiOSの色に不一致があります。
詳細はこちら:#39113
フラッターはネイティブコードと同じ品質を達成する必要があります

第三に、ネイティブ iOS アプリを作成する場合、アプリを書き込むカラー プロファイル (sRGB/P3) を選択できます。
したがって、フラッターアプリ(ネイティブアプリではなく)を作成することを選択した場合、制限され、同じ結果が得られません。
ネイティブと同じフラッターで達成したい!
提案
Flutterアプリにどの色のプロファイルを書きたいかを選択するオプションを追加してください。そうすれば、それらは意図したとおりに本当に美しいです。それはフラッターの柱の1つですよね?

withValueには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;

これをみる限りだと、withValuesの内部で呼ばれるColor.fromには現状何も値を入れなければ問答無用でColorSpace.sRGBが入るようになっています。
ではwithValuesの引数にColorSpace.displayP3を渡した場合はどうなるのか?

その場合はwithValuesのif文の中で書かれている_getColorTransform(this.colorSpace, colorSpace);によって現在のColorSpaceと引数で指定された変更すべきColorSpaceに対応した変換ロジックを取得しています。

_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();
      }
  }
}

そこからさらにtransform.transform(updatedComponents ?? this, colorSpace);では変数の
transformで取得したロジック(クラス)に該当する変換メソッドであるtransformを使ってColor型に変換して成果物を返却しています。

変数の`transform`はこの中のどれかのクラスに当てはまり、そのクラスの中の`transform`メソッドを実行している
class _IdentityColorTransform implements _ColorTransform {
  const _IdentityColorTransform();
  
  Color transform(Color color, ColorSpace resultColorSpace) => color;
}

class _ClampTransform implements _ColorTransform {
  const _ClampTransform(this.child);
  final _ColorTransform child;
  
  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;

  
  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);
  }
}

まとめ

Flutter公式が言うようにiPhoneやMacのようなP3にちゃんと対応して綺麗なUIを描画したいのならwithValuesを使いましょう。
現状はまだ移行期間のため、withValuesの恩恵は受けづらいです。
しかし、ほぼほぼこの実装に切り替えてくる予感がします。

終わりに

Flutter 3.27.0のリリースに伴い、withOpacityが非推奨となり、新たにwithValuesを使用することが推奨されています。
今回の記事では、その移行方法だけでなく、背景にある理由や色空間への対応について深掘りしました。

現状ではwithValuesを使用することで、sRGB基準での描画を維持しながら透明度や色成分を柔軟に操作できます。
そして、将来的にはDisplayP3などの広色域対応デバイスでの色再現も視野に入れた設計となっていることがわかりました。

Flutterの進化により、より多様なデバイスや色空間に対応できるようになる期待が高まります。
この移行期間中にwithValuesへの対応を進めることで、将来の変更にもスムーズに適応できるでしょう。
この記事がその一助になれば幸いです。

最後までお読みいただきありがとうございました!質問やご意見があれば、ぜひコメントで教えてください🙇‍♂️

Discussion