🥣

FlutterアプリをAndroid 13に対応させる

2022/07/31に公開約11,300字

はじめに

数ヶ月前にGoogle Pixel 6を入手して以降、その操作性やインタフェースが心地よく、長らくiPhoneユーザーだった私も最近はAndroidを触る機会が多くなりました(あくまで検証端末としての域は出ず、プライベートはiPhoneですが)。AQUOSやXperiaに代表される国産メーカのAndroidスマートフォンは、やはりどこか扱いづらさがあって苦手だったのですが、Pixelはその観念が180度ひっくり返るぐらい良い体験で、その過程で当時はプレビュー版だったAndroid 13にも関心を持つようになり、情報を追ってきました。

機能とAPIの概要は以下のリンク先から確認できます。本記事は、この中から特にアプリ開発時にケアが必要そうな「Themed App Icons」と「Notification runtime permission」を中心にFlutter目線で記載しています(さすがにAndroid 12のMaterial You程のインパクトは無いですね)。

https://developer.android.com/about/versions/13/features

Android 13

APIレベル33、コードネームはT, Tiramisu(ティラミス)と発表されています。

https://developer.android.com/about/versions/13?hl=ja

2022年7月末現在、Android 13はベータ版 3.2まで進んでおり(ベータ 3版以降はプラットフォーム安定版となります)、正式版に向けて着々と準備を進めています。Androidの正式版は、Android 8.0と9が8月、Android 10と11が9月に、Android 12が10月にリリースされているので、Android 13も遅くとも秋頃までにはFinal Releaseを迎えるのではないかと予想しています。

Androidのプレビュープログラムについてはこちらに記載されています。

Themed App Icons

Androidの端末設定でTheme iconsを有効にすると、ユーザーが選択した壁紙やその他のテーマの色がアプリアイコンに反映されるようになる機能です(左2つの図)。

ライトモード ダークモード 通常アイコン

こちらが公式ドキュメントでページ内のgifを見るのが分かりやすいです。

https://developer.android.com/about/versions/13/features#themed-app-icons

こちらも手順が細かく記載されていて対応イメージが掴みやすいです。

https://proandroiddev.com/implement-themed-icons-android-13-d20b89233681

Flutterアプリで対応する場合

1. まずは素材を作成

素材の生成も簡単で、公式ドキュメントよりサイズの推奨サイズの規定があるので、その通りに作ります。既にアプリアイコンをアダプティブアイコンで設定している場合は、特に必要ありません。

ドキュメントより

2. monochromeを指定

Flutterアプリ開発者の中には flutter_launcher_icons | Dart Package を利用されている方が多いと思います。しかし、残念ながらThemed App Iconsへの対応はまだされておりません (ざっと探してみてもIssueも見つかりませんでした)。
待っていればいずれ対応されるとは思いますが、ひとまず既存アプリに適用させるには自動生成される android/res/mipmap-anydpi-v26/ic_launcher.xmlmonochromeを以下のように追記すれば対応できます。

ic_launcher.xml
  <?xml version="1.0" encoding="utf-8" ?>
  <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
    <background android:drawable="@color/ic_launcher_background" />
    <foreground android:drawable="@drawable/ic_launcher_foreground" />
+   <monochrome android:drawable="@drawable/ic_launcher_foreground" />
  </adaptive-icon>

adaptive_icon_backgroundadaptive_icon_foregroundを使った、通常のアダプティブアイコンの対応がまだの方は、一度表示を確認してからmonochromeに対応すると良いと思います。

pubspec.yaml
flutter_icons:
  android: true
  image_path: "assets/ic_triangle.png"
+ adaptive_icon_background: "#FFFFFF" # 指定のカラー
+ adaptive_icon_foreground: "assets/ic_triangle.png" # 指定のパス

以上で、端末で選択した壁紙(Wallpaper colors)にアプリアイコンのカラーが追従するようになりました🙌

ブルー系の壁紙 グリーン系の壁紙 イエロー系の壁紙

Dynamic Color

こちらはAndroid 12での発表ではありますが、前述の Themed App Icons と関連があると思うのでついでに紹介します。

Material 3のFlutter対応に向けて ☂️ Bring Material 3 to Flutter · Issue #91605 · flutter/flutter の対応は着々と進んでいるものの、まだ未対応のチェックボックスが結構残っている状況ですね。ここに記載されていますが、Dynamic color対応については dynamic_color | Flutter Package にてサポートする方針となっています。

https://pub.dev/packages/dynamic_color

Dynamic color support
While not part of the Flutter library, dynamic color support on Android is available:
Support dynamic color and color harmonization through the dynamic_color package

Dynamic Colorって何?という方はこちらをどうぞ。

https://qiita.com/degudegu2510/items/6d86f0651e913c6d146b

パッケージの使い方はとても簡単でDynamicColorBuilderで包むだけです。builderの引数にはそれぞれ、ライトテーマとダークテーマのColorScheme?(Android 12未満または端末テーマ未設定時はnull)を受け取ることができます。

main.dart
import 'package:dynamic_color/dynamic_color.dart';

return DynamicColorBuilder(
      builder: ((lightDynamic, darkDynamic) {
        return MaterialApp(
          // 省略...
        );
      }),
    );

Harmonization

HarmonizationはMaterial 3で紹介されている概念で、色相と彩度をわずかにDynamic Colorのテーマに寄せることでユーザーの違和感を軽減し、プロダクトと端末のカラーが自然にまとまるように調整されるというものです。昔から、同じ黒色でもテーマカラーが青系の場合は少し青の色相を足したり、エラーメッセージもコントラストが強すぎないようにするなど細かい調整をしてきた経験もあると思いますが、それらが自動的に行われると言った感じですかね。ユーザーフレンドリーなケアだと思うので、個人的には積極的に採用していこうと思っています。

ColorSchemeをharmonized

実装に戻ります。
exampleを見ると分かりますが、引数のColorSchemeをそのまま使うのではなくharmonized()を呼び出してDynamic Colorのprimaryと調和する実装がなされており、推奨とされています。

DynamicColorBuilder(
  builder: ((lightDynamic, darkDynamic) {
    return MaterialApp(
      theme: ThemeData(
        colorScheme: lightDynamic.harmonized(),
      ),
    );
  }),
);

harmonized()の内部実装は以下の通りで、主にerror関連のカラーをPrimaryで調和していることが分かりますね。

// ref. https://github.com/material-foundation/material-dynamic-color-flutter/blob/3dfdefc00ca69efb6de4017e8d34e188cd866dc8/lib/src/harmonization.dart#L53
ColorScheme harmonized() {
    return copyWith(
      error: _harmonizeWithPrimary(error),
      onError: _harmonizeWithPrimary(onError),
      errorContainer: _harmonizeWithPrimary(errorContainer),
      onErrorContainer: _harmonizeWithPrimary(onErrorContainer),
    );
  }

Material Designでのerrorはエラー状態を表現する赤色で設定されています。このような従来の意味を持つ表現色を「セマンティックカラー」と言い、色の持つ意味は維持しつつ全体を壁紙の配色に調和することがHarmonizationによって実現されています。

https://material.io/blog/dynamic-color-harmony

独自カラーもharmonized

また、任意ですが規定のColorSchemeだけではなく独自カラー(ThemeExtensionの継承クラス)に対しても同様に調和することができます。本記事の最後にリポジトリを掲載していますが一部抜粋すると以下のような形で対応しています。

app_colors.dart
class AppColors extends ThemeExtension<AppColors> {
  // 省略...
  factory AppColors.light(ColorScheme? dynamic) {
    const appColors = AppColors(
      accent: Color(0xFFEF61E1),
    );
    return dynamic == null ? appColors : appColors._harmonized(dynamic);
  }

  // 内部的には`material_color_utilities`パッケージの`Blend`クラスの`harmonize()`メソッドを使って調和している
  AppColors _harmonized(ColorScheme dynamic) {
    return copyWith(
      custom: accent.harmonizeWith(dynamic.primary),
    );
  }
}

Notification runtime permission

Androidアプリの権限にはいくつか種類がありますが、今回のPOST_NOTIFICATIONはユーザー許諾が必要な実行時権限となります。つまり、これまでAndroidではユーザーの許諾を取ることなくプッシュ通知を配信できていましたが、iOS同様にアプリ起動後にユーザーによる許諾が必要になる、ということですね。異なる点としては、Androidの場合はダイアログを閉じることができ判断を延長することができます。

ユーザーがダイアログをスワイプして閉じた場合
ユーザーがダイアログをスワイプして閉じた場合(つまり、[許可] と [許可しない] のどちらも選択しなかった場合)、通知権限の状態は変わりません。
https://developer.android.com/about/versions/13/changes/notification-permission?hl=ja#user-swipe-away

iOSとの違いも含めて、私が把握している挙動をまとめると以下の表となります。

権限ダイアログの挙動 Android 13以上[1] Android 12L以下 iOS
タイミング 任意に設定可 アプリ起動直後のみ[2] 任意に設定可
回数 基本的には制限なし[3] 「許可しない」を選択されない限り制限なし[4] 1回限り

https://developer.android.com/about/versions/13/changes/notification-permission?hl=ja

権限ダイアログの表示タイミングについては、コンテキストや用途をユーザーに十分に説明した後に要求することが推奨されています。これはiOSと同様ですね。

  • ユーザーが「警報ベル」ボタンをタップしたとき。
  • ユーザーが誰かのソーシャル メディア アカウントをフォローすることを選択したとき。
  • ユーザーが料理の宅配を注文したとき。

Flutterアプリで対応する場合

FlutterFireのfirebase_messaging | Flutter Package プラグインですが、残念ながらまだ未対応のようです。イシューは上がっており、来たる正式版に向けて対応を待ちわびているコメントが見受けられます。

https://github.com/firebase/flutterfire/issues/8720

代わりに、前述のイシューにコメントされていた permission_handler | Flutter Package パッケージを使った方法を紹介します。Android 13への対応は以下のIssueで進められていますが、2022年7月末現在対応が完了しているのはPOST_NOTIFICATIONSだけのようです。

https://github.com/Baseflow/flutter-permission-handler/issues/859

ちなみにPOST_NOTIFICATIONSv10.0.0で対応されていますので、今回はこれを使って通知権限の挙動を確認していきます。

1. compileSdkVersion / targetSdkVersionを33にする

プラグインを動かすだけであればcompileSdkVersionのみの変更でOKですが、ユーザーに任意のタイミングで権限ダイアログを表示する処理を入れたい場合はターゲットも変更が必要です。

build.gradle
android {
+   compileSdkVersion 33
    ndkVersion flutter.ndkVersion

    defaultConfig {
+     targetSdkVersion 33
    }
}

2. POST_NOTIFICATIONSを追記する

今回の肝ですね。

AndroidManifest.xml
  <!-- ref. https://developer.android.com/about/versions/13/changes/notification-permission#user-select-allow -->
  <manifest ...>
+     <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
      <application ...>
          ...
      </application>
  </manifest>

3. 任意のタイミングでリクエストを要求する

後は適宜お好きなタイミングで権限ダイアログを表示するだけです。

await Permission.notification.request();
権限ダイアログ表示の様子

Android 13をターゲットとする際の変更点

また、targetSdkVersion: 33に引き上げる際の注意点については、こちらに記載があります。

https://developer.android.com/about/versions/13/behavior-changes-13

まとめ

今回は、あと数ヶ月で正式版がリリースされるであろうAndroid 13の対応について特に重要そうだと思った点に絞って記載しました。ベータ 3版(プラットフォーム安定版)ではありますが、やはりまだFlutterのパッケージ/プラグインの対応は未着手or進行中というものが多かったです。Androidネイティブであればこの辺りの対応は早いのでしょうか?良くも悪くもネイティブ実装を隠蔽してくれるFlutterですが、プラットフォームに依存が強い分、今回のように先んじた対応が必要となった場合はなかなか大変な印象を受けました(ネイティブ実装をゴリゴリ進められる人は除く)。iOSもAndroidも安定版が世に出て、ある程度普及したプラットフォームの上で高速にプロダクトを開発するには絶大な効果を発揮する一方、稀に足枷となるケース(ARなどのネイティブ機能を使うなどと同じ)もあると今回検証してみて感じました。

作業途中の各イシューはwatchしたので、今後改善がなされたら本記事も追記/修正して更新していければと思います。最後に、こちらが検証したリポジトリです。

https://github.com/HTsuruo/flutter_android_13

参考

脚注
  1. 「Android 13以上」は端末のOSではなくターゲットの話で、具体的にはtargetSdkVersionが33以上を意味しています。対して「Android 12L以下」はtargetSdkVersionが32以下ですね。 ↩︎

  2. 厳密には通知チャンネルの作成後にアプリが最初にアクティビティを開始したとき、または、アプリがアクティビティを開始して最初の通知チャンネルを作成したとき、との記載があります。 ↩︎

  3. iOSでは一度「許可しない」を選択されてしまうとその後アプリから許諾を取る方法がなくなってしまうため、その点Androidは複数のタッチポイントに権限ダイアログを表示することができそうですね。 ↩︎

  4. 一度でも「許可しない」を選択すると「アプリ再インストール」or「Android13のターゲットにアプリが更新される」の条件に合致しない限り表示されなくなります(iOSの仕様と似ていますね)。 ↩︎

Discussion

ログインするとコメントできます