【Flutter】(Androidユーザーが思う)アプリに対応してほしい機能と対応方法について

# はじめに

日常生活や仕事において、スマホアプリを頻繁に利用する方は、iPhoneユーザーが多いように感じます。

それによってAndroid向けの機能が軽視されているというわけではありませんが、優先順位はやや低い傾向にあると思います。

そこで、普段からAndroidを利用している身として、
**「対応されていたら嬉しい！」** と思える機能を、Flutterを用いた実現方法も含めていくつかご紹介します。

# 🔨アイコン - Themed App Icons

|テーマ|ライトモード|ダークモード|
|---|---|---|
|デフォルト|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510027/47e34b2a-a197-b193-d7ce-df7962197604.png" alt="デフォルト" width="200px"/>|-|
|青系の壁紙|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510027/bb029119-8591-fbea-7207-36d5f908b098.png" alt="青系の壁紙" width="200px"/>|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510027/674e4c54-ef23-48f5-36cf-00936fb89cc4.png" alt="青系の壁紙(ダークモード)" width="200px"/>|
|赤系の壁紙|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510027/ecd24df9-78a1-a049-7f51-3fff211d34b7.png" alt="赤系の壁紙" width="200px"/>|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510027/e978c8b8-9266-4a29-8aec-12d16b71ae7c.png" alt="青系の壁紙(ダークモード)" width="200px"/>|

Android13から追加された機能です。

設定を行うことで、壁紙や設定した色、テーマモードに応じてアイコンが切り替わります。
少し前の内容にはなりますが、概要は[ツルオカ](https://twitter.com/h_tsuruo)さんが記事にまとめて下さっています。
そちらをご覧下さい。

https://zenn.dev/tsuruo/articles/eae9dbbad8372f

筆者にとっては、アプリをホームに配置する動機となる機能です。

## 実装

概要は前述した記事で掲載されていますが、
現在ではパッケージ開発が進んだこともあり、[flutter_launcher_icons](https://pub.dev/packages/flutter_launcher_icons)パッケージでも対応されました。

ただ、私は環境ごと(開発/本番)のThemed App Iconへの対応など、細かい設定が好みで[icons_launcher](https://pub.dev/packages/icons_launcher)を利用しています。

私の個人開発アプリを例に設定していきます。

### 1. アプリアイコン画像の作成

まずはアプリアイコンの画像を作成します。
私はFigmaのテンプレートを利用してアプリアイコンを作成しています。

https://www.figma.com/community/file/824894885635013369

作成する画像は2種類です
- 通常のアプリアイコン
- モノクロ化されたアプリアイコン

|通常|モノクロ|
|---|---|
|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510027/65334427-ed6f-7b6b-c472-13f9616a6e45.png" width="128px" alt="通常のアプリアイコン"/>|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510027/49e17c51-26bd-e454-ce8c-d220f03e624f.png" width="128px" alt="モノクロのアプリアイコン"/>|

任意のフォルダに作成したアプリアイコンを配置します。

### 2. `icons_launcher`パッケージのセットアップ

セットアップはパッケージに記載されている方法の通り対応していきます。

https://pub.dev/packages/icons_launcher

### 3. アイコンの配置/生成

Themed App Iconへの対応は、`adaptive_monochrome_image:`の記載を追加することで対応できます。

```yaml:icons_launcher.yaml
icons_launcher:
  image_path: "assets/launcher_icon/icon.png"
  platforms:
    android:
      enable: true
      image_path: "assets/launcher_icon/icon.png"
      adaptive_background_color: '#FFFFFF'
      adaptive_foreground_image: "assets/launcher_icon/icon_adaptive.png"
      adaptive_round_image: "assets/launcher_icon/icon_adaptive.png"
      # 用意したモノクロ写真の設定を追加
      adaptive_monochrome_image: "assets/launcher_icon/icon_adaptive_monochrome.png"
    ios:
      enable: true
      image_path: "assets/launcher_icon/icon.png"

```

設定が完了したらコマンドを叩いてプラットフォーム別のアイコンを生成します。
```sh:ターミナル
dart run icons_launcher:create
```

Themed App Iconに対応した画像ファイルが生成されました🎉

https://github.com/yakitama5/flutter_laboratory/tree/main/apps/themed_app_icon

# 🎨テーマカラー - Dynamic Color

|アプリテーマ|テーマカラー緑|
|---|---|
|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510027/8c69e593-be55-af73-ebc5-b7931cc3447a.png" alt="アプリテーマ" width="240px"/>|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510027/c59f4e47-e5a3-5eef-bc36-a455335bf8b7.png" alt="緑色を選択" width="240px"/>|
|**青系の壁紙**|**赤系の壁紙**|
|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510027/5718bb74-3530-9a83-e14d-c9e6623de216.png" alt="青系の壁紙" width="240px"/>|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510027/70745f16-8599-29fa-155e-54da9da39a76.png" alt="赤系の壁紙" width="240px"/>|

Android13から追加された機能です。

設定を行うことで、壁紙や設定した色、テーマモードに応じてアプリのテーマカラーが切り替わります。
前述した機能のアプリテーマカラー版です。

こちらの機能は賛否両論あり、
大手ではGoogle製アプリしか対応されていない印象です。
私も **「企業(アプリ)のテーマカラーが定着し辛い」** と思い、最初は躊躇していました。

しかし、**[monoca2](https://monoca.sola-air.com/Home/Ja)** の影響を受け、今はとても気に入ってます。
monoca2では、テーマカラーを選択でき、その一種としてダイナミックカラーが提供されています。

https://monoca.sola-air.com/Home/Ja

## 実装

基本は[dynamic_color](https://pub.dev/packages/dynamic_color)パッケージで対応されています。
しかし、Flutter 3.22.0で行われた`ColorScheme`の変更[^1]に対応されていません。

実際にアプリに組み込むことを想定し、2つの対応を行った例を記載します。
- Flutter 3.22.0で行われた破壊的変更への対応
- テーマカラーの一種として提供する方法

例では状態管理として[riverpod](https://pub.dev/packages/riverpod)を利用しています。

### 1. `dynamic_color`パッケージの追加

公式の手順を参考にパッケージの追加を行います。

https://pub.dev/packages/dynamic_color

### 2. アプリのテーマカラーを選択可能にする

まずはアプリのテーマカラーを選択できるように対応します。
テーマカラーを`Enum`で定義し、`Notifier`によって状態管理を行います。

```dart:theme_color.dart
/// ユーザーが選択可能なテーマカラー
enum ThemeColor {
  appColor(null, 'アプリ内テーマ'),
  dynamicColor(null, 'デバイスのテーマカラー'),
  blue(Colors.blue, 'ブルー'),
  purple(Colors.purple, 'パープル'),
  pink(Colors.pink, 'ピンク'),
  red(Colors.red, 'レッド'),
  orange(Colors.orange, 'オレンジ'),
  yellow(Colors.yellow, 'イエロー'),
  green(Colors.green, 'グリーン');

  const ThemeColor(this.seedColor, this.description);

  static List<ThemeColor> get colorValues =>
      values.where((i) => i.seedColor != null).toList();
  static List<ThemeColor> get systemValues =>
      values.where((i) => i.seedColor == null).toList();

  final Color? seedColor;
  final String description;
}
```

```dart:theme_color_notifier_provider.dart
/// アプリ内テーマカラー
/// 本来は`SharedPreferences`などで情報を永続化する
/// サンプルプログラムのためインメモリで保持
@riverpod
class ThemeColorNotifier extends _$ThemeColorNotifier {
  @override
  ThemeColor build() => ThemeColor.appColor;

  void update({required ThemeColor themeColor}) {
    state = themeColor;
  }
}

```


今回はインメモリで管理していますが、実際には[shared_preferences](https://pub.dev/packages/shared_preferences)などを利用して、設定を永続化させます。


### 3. デバイスのテーマカラーを管理するProviderの定義

テーマカラーの一種として提供するのに管理し辛いため、`DynamicBuilder`は利用しません。

`DynamicBuilder`の内部実装を参考にします。
デバイスのテーマカラーを取得するためには、`DynamicColorPlugin.getCorePalette()`という処理を呼びします。

取得したテーマカラーをProviderで管理します。

```dart:dynamic_core_pallete_provider.dart
/// DynamicColorのCorePaletteを管理
@Riverpod(keepAlive: true)
Future<CorePalette?> dynamicCorePalette(Ref ref) async {
  // 未対応のWebでは取得処理を呼び出さない
  if (kIsWeb) {
    return null;
  }

  return DynamicColorPlugin.getCorePalette();
}
```

### 4. 利用する`ColorScheme`を管理するProviderを定義する

アプリ内のテーマカラーを切り替えるために、アプリ内の`ColorScheme`をProviderで管理します。
前述した2つのProviderを用いて、利用される`ColorScheme`を決定する役割です。

まずは完成コードを記載します。

```dart:app_color_scheme_provider.dart
/// アプリ内のカラースキーマを管理
@riverpod
ColorScheme appColorScheme(Ref ref, {required Brightness brightness}) {
  // 「選択されたテーマカラー」と「デバイスのテーマカラー」を基に設定
  final themeColor = ref.watch(themeColorNotifierProvider);
  final dynamicCorePalette = ref.watch(dynamicCorePaletteProvider).value;

  // テーマカラーに応じたカラースキーマを取得
  final colorScheme = _colorScheme(brightness, themeColor, dynamicCorePalette);

  // Dynamic Colorに対応している場合、背景色とエラー関連の色を調和する
  final isDynamicColorSupported = dynamicCorePalette != null;
  return isDynamicColorSupported ? colorScheme.harmonized() : colorScheme;
}
```

詳細を記載していきます。

#### ColorSchemeの決定

```dart
// テーマカラーに応じたカラースキーマを取得
final colorScheme = _colorScheme(brightness, themeColor, dynamicCorePalette);
```

ここでは以下の情報を基に利用される`ColorScheme`を取得しています。
- テーマモード
- 選択されたテーマカラー
- デバイスのテーマカラー

詳細な条件は下記の通り記載しています。

```dart
/// テーマカラーに応じたカラースキーマ
ColorScheme _colorScheme(
  Brightness brightness,
  ThemeColor themeColor,
  CorePalette? dynamicCorePalette,
) {
  switch (themeColor) {
    case ThemeColor.appColor:
      return _defaultColorScheme(brightness);
    case ThemeColor.dynamicColor:
      // DynamicColorに対応していない場合は、アプリのテーマカラーを設定
      if (dynamicCorePalette == null) {
        return _defaultColorScheme(brightness);
      }

      // デバイスのテーマカラーを基にカラースキーマを生成
      final dynamicColorScheme =
          dynamicCorePalette.toColorScheme(brightness: brightness);

      // [暫定対応] Flutter3.22.0で行われた`ColorScheme`の破壊的変更への対応
      return generateDynamicColourSchemes(dynamicColorScheme);

    case ThemeColor.blue:
    case ThemeColor.purple:
    case ThemeColor.pink:
    case ThemeColor.red:
    case ThemeColor.orange:
    case ThemeColor.yellow:
    case ThemeColor.green:
      // テーマカラーが選択された場合は、対応する色を基に生成
      return ColorScheme.fromSeed(
        seedColor: themeColor.seedColor!,
        brightness: brightness,
      );
  }
}
```

`generateDynamicColourSchemes`は前述した破壊的変更への対応です。

https://github.com/material-foundation/flutter-packages/issues/582#issuecomment-2081174158

#### デバイスのテーマカラーとの調和

```dart
// Dynamic Colorに対応している場合、エラー色を調和する
final isDynamicColorSupported = dynamicCorePalette != null;
return isDynamicColorSupported ? colorScheme.harmonized() : colorScheme;
```

最後に行っている処理は、エラー色の調和です。
今回は対応方法の記載なので、詳細は割愛させください。

前述したツルオカさんの記事で説明されていますので、興味のある方はぜひご覧ください。

https://zenn.dev/tsuruo/articles/eae9dbbad8372f#harmonization

### 5. アプリが利用するテーマを変更する

`MaterialApp`に対してProviderで管理している`ColorScheme`を設定すれば完了です。

```dart:app.dart
class DynamicColorApp extends ConsumerWidget {
  const DynamicColorApp({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final lightColorScheme =
        ref.watch(appColorSchemeProvider(brightness: Brightness.light));
    final darkColorScheme =
        ref.watch(appColorSchemeProvider(brightness: Brightness.dark));

    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: const SamplePage(),
      // Providerで管理した`ColorScheme`を基にアプリテーマを設定
      theme: ThemeData(colorScheme: lightColorScheme),
      darkTheme:
          ThemeData(colorScheme: darkColorScheme, brightness: Brightness.dark),
    );
  }
}
```

`themeColorNotifierProvider`で管理しているテーマカラーを切り替えることで、アプリの配色が変更されました🎉

https://github.com/yakitama5/flutter_laboratory/tree/main/apps/dynamic_color_app

# 🕶️【番外編】色のコントラスト - Color contrast

|デフォルト|中|高|
|---|---|---|
|<img width="200" alt="色のコントラスト：デフォルト" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510027/a4b37fd5-6564-34c5-5ec1-0eb6484320d4.png">|<img width="200" alt="色のコントラスト：中" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510027/e3693ecb-6a99-0089-5b5c-e0d19cf44beb.png">|<img width="200" alt="色のコントラスト：高" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510027/1dd043e1-af76-6d8e-8854-5f7136770ff6.png">|

Android14から追加された機能です。

設定を行うことで、自分の見やすいコントラストに合わせてアプリのテーマカラーを切り替えることができます。

この機能は対応してほしいというわけではありません。
アクセシビリティのため、**今後対応したい機能**となります。

現状、Flutterやパッケージなどで対応されていないようですが、ネイティブ機能を呼び出すことで対応可能です。

こちらは別記事でまとめています。
興味のある方はぜひご覧下さい。

https://zenn.dev/yakuran1/scraps/2f1826b56bee7b

# 🗨️プッシュ通知 - Notification Channels

|YouTube|Google Play Store|X (Twitter)|
|---|---|---|
|<img width="200" alt="通知チャネル：Youtube" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510027/ce25264c-f059-79a4-917e-c94bf6781be3.png">|<img width="200" alt="通知チャネル：Google Play Store" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510027/8d3e0ff9-cfed-bb83-6f10-5ce5d8ac68d1.png">|<img width="200" alt="通知チャネル：X (Twitter)" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510027/11b504d3-a49d-c641-196d-0d785e54b6c0.png">|


Android8から追加された機能です。

対応することで、種類に応じた通知設定をAndroidの設定アプリから切り替えることができます。

通知の種類まで分かれていると理想ですが、対応規模が前述したものと比べて肥大化します。
ユーザーへの通知要件から整理しないといけないですからね。

取り上げたいのは、
**「一種類しかなくともユーザーには見える」** という点です。
ユーザーにとって意味が分からない内容を表示することは避け、ユーザーフレンドリーな名称を設定したいです。

私もこちらの記事でハッとさせられました。

https://qiita.com/Takkesan/items/0b614d678f92af63b9d5

## 実装

今回はカンタンに対応できる後者への対応を例に記載します。

PUSH通知を導入する場合、[firebase_messaging](https://pub.dev/packages/firebase_messaging)パッケージを利用することが多いと思います。
ですが、Androidの通知チャネルやフォアグラウンド通知は多少メンドウです。

そこで、要件を満たすのであれば[fcm_config](https://pub.dev/packages/fcm_config)パッケージをおすすめします。

通知の設定自体は趣旨が異なりますので、パッケージを参照して下さい。

https://pub.dev/packages/fcm_config

本題となる通知チャネル名の設定自体は下記の1行で設定可能です。
設定の手間はほぼないので、適当な値を設定することは避けましょう。**(自戒)**

```dart
await FCMConfig.instance.init(
  defaultAndroidForegroundIcon: '@mipmap/ic_launcher',
  defaultAndroidChannel: AndroidNotificationChannel(
    'channel_id',
    // 本題となるチャネル名
    '[アプリ名]からのお知らせ',
    importance: Importance.high,
    sound: RawResourceAndroidNotificationSound('notification'),
  ),
);
```

# 🙇あとがき

**最後まで読んでいただきありがとうございます！**

主観強めの内容ではありましたが、
いちAndroidユーザーの声としてまとめてみました。

他にも「この機能が対応されていると嬉しい！」などあればコメント頂けると幸いです。
教えて頂いた機能についても、実現方法を調べたいと思います。


[^1]:https://docs.flutter.dev/release/breaking-changes/new-color-scheme-roles