🥣

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

2022/07/31に公開
CHANGELOG
  • 2022.08.28
    • 現地時間2022年8月15日の「Android 13」の正式リリース発表を受けて関連箇所を修正
    • firebase_messaging プラグインが POST_NOTIFICATION 対応されたのを受けて関連箇所を追記

はじめに

数ヶ月前に 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年8月15日に、「Android 13」の正式版の提供が発表されました。Android の正式版は、Android 8.0と9が8月、Android 10と11が9月に、Android 12が10月にリリースされている ので、Android 13も遅くとも秋頃までには Final Release を迎えるのではないかと予想していましたが比較的早くリリースを迎えたようです。

Android のプレビュープログラムについて

2022年7月末時点では、Android 13は ベータ版 3.2まで進んでおり(ベータ3版以降はプラットフォーム安定版となります)、正式版に向けて着々と準備を進めてる状況でした。Android のプレビュープログラムを利用することで正式版リリース前でも先んじて手元で動かすことができます。

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

パッケージの使い方はとても簡単で、後述の harmonized() を忘れず呼ぶ点だけ注意が必要 ですが、基本的には 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 プラグインですが、v13.0.0にて対応されましたので そちらを利用するのがおすすめです(こちら が当時の Issue)。

firebase_messaging now includes this permission: Manifest.permission.POST_NOTIFICATIONS in its AndroidManifest.xml
https://pub.dev/packages/firebase_messaging/changelog#1300

こちらの CHANGELOG 記載の通り、プラグイン側に本来追記が必要な POST_NOTIFICATIONS の permission が定義されているので AndroidManifest.xml への追記は必要なく、許可を取りたい場面で以下メソッドを呼ぶだけです。

await FirebaseMessaging.instance.requestPermission();
permission_handler プラグインを利用する場合

何らかの理由 firebase_messaging が使えなかったり、本記事の最初の執筆時点でいち早く対応されていた permission_handler | Flutter Package パッケージを使った方法も一応紹介します。Android 13への対応は以下の Issue で進められていますが、2022年7月末現在対応が完了しているのは POST_NOTIFICATIONS だけで、v10.0.0で対応 されています。

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

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