alpha Lounge

20%の技術記事とオタクネタ

【Flutter】AIにお任せ!楽してMaestroでE2Eテストを作成

アプリを作っていて、動作確認めんどいな〜と思うこと多いですよね。数行修正しただけのつもりなのにあちこち動かなくなってしまうこともあります。単体テストを書いてロジックを担保できても、動かしてみると思わぬところでバグってしまったり...

だったらE2Eテストを整備すればいいじゃないか、と思いますが、そもそも書くのも面倒だし仕様変更に追従して一つ一つ追加・修正するのも手間だなぁと感じます。

もー全部AIがやってくれよ〜

ということで、本記事はAIでE2Eテストを書いてみるテストです。

技術選定

FlutterでE2Eテストを構築できるツールはいくつかあり、好みもあるかと思いますが、以下の観点から技術選定を進めました。

  • 資料が豊富か。
    • インターネット上での資料が多ければ多いほど、AIの知識が多くなる・参照しやすくなる。
  • 導入までの設定が少ないか。
  • Flutterに限らず、ネイティブ実装に移行した場合にも転用できる。
  • チーム開発において非エンジニア(QAやPdM、PLなど)も理解しやすいツール。

これらの選定基準を考慮し、今回はMaestroを使用します。

Maestroとは

Maestroは、モバイル/Web向けのUIテストをYAMLで記述できるオープンソースのE2Eフレームワークです。

docs.maestro.dev

YAMLで直感的な命令文で記述できるため、非エンジニアにも比較的理解しやすいツールです。

Sample

GitHub - alpha2048/maestro_e2e_sample

ベースは、RiverpodのチュートリアルのジョークAPIの実装を元にしています。
Your first Riverpod app | Riverpod

このサンプルでは、以下の2フローを用意しています。

  • maestro/joke_flow.yaml: ジョークの表示更新を検証
  • maestro/notification_flow.yaml: 通知タップでアプリ復帰を検証

実装手順

Maestroのインストール

手順に従ってインストールします。Macの場合はHomebrewでもインストール可能。

Installing Maestro | Maestro

※Homebrewには本ツールとは別物のMaestroというAI agentツールがcaskで提供されています。インストールを間違わないように気をつけましょう。そちらのツールも面白そうですが。

また、MaestroにはJava 17+も必要なので未インストールの場合は準備しておきましょう。

構成説明

MaestroのフローはYAMLで書きます。
最初にappIdを定義し、launchApptapOnなどのコマンドを並べていく構成です。

例: joke_flow.yaml

appId: com.example.maestroe2esample
---
- launchApp
- waitForAnimationToEnd:
    timeout: 5000
- copyTextFrom:
    id: "joke-text"
- evalScript: ${output.firstJoke = maestro.copiedText}
- tapOn:
    id: "get-another-joke"

evalScriptでJSを使えるので、比較やループも可能です。
サンプルではrepeatで最大10回リトライし、ジョークが変わったことを検証しています。

例: notification_flow.yaml

- runFlow:
    when:
      platform: ios
    commands:
      - pressKey: Home
      - swipe:
          start: 50%,1%
          end: 50%,55%
      - tapOn: "Joke"

platformiOS/Androidの分岐が書けるので、通知や戻り操作の差異も吸収できます。

構築

AIに投げて作ります。今回はCodexに投げて作成していきます。
流れは以下の通りです。

  1. 画面仕様と期待動作を整理
  2. AIにYAMLの草案を作ってもらう
  3. maestro testで実行し、エラーをAIに投げて修正

複雑な手順の場合は期待動作をきっちり文章に落とし込みましょう。
整理できたら、以下のように依頼していきます。

FlutterアプリのE2EをMaestroで書きたい。
「Get another joke」をタップするとジョークが変わる、ことを検証する。
実装を進めて。

あとはAIにがんがん作成してもらいます。
MaestroはSemanticsウィジェットidentifierを識別子として使用するのですが、AIにそちらを整備してもらえます。

Semantics(
  identifier: 'get-another-joke',
  button: true,
  child: ElevatedButton(
  ..
  ),
),

サンプル説明

ジョーク更新フロー

ボタンを押してテキストが変わるだけのシンプルなテストです。

AIさん曰く: copyTextFromでジョーク本文を取得し、tapOn後に再取得して変化を確認します。通信遅延を想定して、repeat + evalScript で安全側に倒しています。

アプリ外部との連携 通知で試す

若干無理矢理ではありますが、アプリ外との連携を確認するため、ローカル通知を開くことを確認しています。
通知を発火→ホームへ戻る→通知をタップして復帰、という外部連携まで検証します。

通知トレイの操作はiOS/Androidで違うため、platformで分岐しています。この辺りは使用しているシミュレータのサイズやOSによって調整が必要になりますのでご注意ください。

実行

実行方法

まずはテスト用ビルドを作成します。

iOS (Simulator用)

flutter build ios --simulator --no-codesign -t lib/main.dart

Android

flutter build apk -t lib/main.dart

ビルドしたapk/ipaをシミュレータへインストールした上で、以下を実行します。

maestro --verbose --device <DEVICE_ID> \
  test maestro/ \
  --debug-output ./maestro-debug \
  --test-output-dir ./maestro-artifacts

実行すると、以下のように動きます。

サンプルGIF

その他

CIに組み込むならMaestro Cloudが便利ですが、料金はそれなりに高いです。
CIへの組み込み方もいずれ別記事で検証したいところ。

おわりに

今回Maestro側の実装やコマンドの整備までAIに作成してもらいましたが、概ね狙った通りの実装ができています。

複雑なテストを実装する場合は人力で調整が必要そうですが、とにかく初手の導入に関してはAIと共に構築すればすぐにできるでしょう。

(この記事をFlutter Advent Calendar 2025辺りに載せる予定でしたが間に合いませんでした😇)

参考

【Flutter】コピペOK!Flutterでバージョンアップ+リリースブランチを作成するGitHub Actions

Flutterでpubspec.yamlのバージョンを自動的にアップデートしてリリースブランチを作成する自動化スクリプトを作成しました。

ちなみに解説を参考にAIに依頼すれば簡単に実装可能です。カスタマイズもお好みでどうぞ。
今回の実装もAIにほとんど作ってもらいました(動作は確認済)。

実装

  • FVMを使用している設定になります。
  • 手動でmainブランチからリリースブランチを作成します。
name: Release Branch Automation

on:
  workflow_dispatch:
    inputs:
      version_update_type:
        description: 'Select version bump type'
        required: true
        type: choice
        options:
          - major
          - minor
          - patch

jobs:
  create_release_branch:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write
    if: github.ref == 'refs/heads/main'

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Flutter (via FVM)
        uses: kuhnroyal/flutter-fvm-config-action/setup@v3

      - name: Install dependencies
        run: flutter pub get

      - name: Install Cider
        run: dart pub global activate cider

      - name: Bump version
        run: dart pub global run cider bump ${{ github.event.inputs.version_update_type }} --bump-build

      - name: Get new version
        id: version
        run: |
          NEW_VERSION=$(dart pub global run cider version)
          echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT

      - name: Create release pull request
        uses: peter-evans/create-pull-request@v7
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          commit-message: "chore: bump version to ${{ steps.version.outputs.new_version }}"
          title: "Release v${{ steps.version.outputs.new_version }}"
          body: |
            This pull request prepares release **v${{ steps.version.outputs.new_version }}**.
            - Version bumped automatically using Cider.
          branch: "release/v${{ steps.version.outputs.new_version }}"
          base: main

解説

GitHub Actionsの権限セッティング

事前のGitHub Actionsにプルリクエストを作成する権限を与えておきましょう。
Settings -> Actions -> General から、Read and write permissionsAllow GitHub Actions to create and approve pull requestsを有効にしておきましょう。

cider ライブラリについて

バージョンのアップデートにciderを使用します。

cider | Dart package

ライブラリの管理向けのライブラリ、といった立ち位置のライブラリですが、通常のアプリ開発でも使用できます。

今回はバージョンのアップデート用のコマンド、以下の2つを使用します。

  • cider bump <part> [options]
    • バージョンをbumpします。
    • 通常の開発では、partにmajor、minor、patchを指定すればOK。
    • optionに--bump-buildを指定するとBuildバージョンも上がるため、こちらも忘れず指定しましょう。
  • cider version
    • 現在のバージョンを取得します。
    • 今回はbump後のバージョンを取得してプルリクに反映するために使用。

あとはよく使われるActionsを組み合わせれば簡単に実装できます。ぜひ導入あれ。

IntelliJ IDEAを利用していたJetBrains信者が、VSCodeエディタに完全に切り替えちゃった件🚀

この記事はAIに既存のブログ記事を学習させて書いてもらったものを調整しています。いつもよりおかしな部分があったらすいません。


長年JetBrains製IDEを使ってきましたが、ここ最近はVSCode、とりわけCursor中心に開発しています。
元々自分はFlutter/Androidの開発がメインだったためAndroid Studioを中心に使っており、その流れで他のプロジェクトではIntelliJ IDEAを利用していました。しかし、ここ最近の開発スタイルの変化とコスト感の見直しをきっかけに、VSCode中心の開発に移行しました。特にCursorをよく使用しています。
なぜ乗り換えたのかを自分用に整理します。

※この記事は個人の体験に基づくメモです。価格や機能は時期や環境により変動するかもしれません。

要点

  • IDEのサブスク費用の負担が増し、投資先を生成AIに振り向けたいという動機があった
  • VSCodeベースのAI開発体験(Cursor、Kiroなど)に適応するメリットが大きいと判断した
  • JetBrainsのAI周り(Junie)に関するコミュニティ評価は賛否あり、現状ではVSCode系の方が運用しやすいと感じた
  • 特にCursor中心で運用。差分表示と対話形式の調整が視覚的で分かりやすく、こだわって開発したい自分と相性が良い

乗り換え理由

IntelliJ IDEAの料金が高い

継続課金の負担感が無視できなくなってきました。例として、国内代理店であるサムライズムの年額で、3年継続していても約15,000円がかかります(プランや時点で変動し得ます)。

IntelliJ IDEA Ultimate - JetBrains公式パートナー | 株式会社サムライズム

便利なIDEであることは間違いありませんが、同額を生成AI利用(APIやツール)に回した方が中長期の生産性向上につながると判断しました。特に個人開発や小規模チームでは、コストパフォーマンスを慎重に検討する必要があります。

IDEから脱却し、VSCodeベースに慣れる意義

最近のAI系プロダクトはVSCodeベースが主流です。Cursor、Kiroなど、VSCode拡張として提供されるものが増えています。

CLI系のアシストを活用したVibe Codingも広がっており、VSCode系やCLIを中心に据えるとAI主導の開発フローへ移行しやすくなります。エディタをVSCode系に統一することで、拡張・設定・キーバインドをAIワークフローに最適化しやすくなりました。

Junieの評判が安定していない

JunieはJetBrains IDEに統合されたAIアシスタントです。

JetBrains の AI コーディングエージェント「Junie」

一定の評価を得ているとは思われるものの、実行が遅い、クォータ制限が厳しい、(IDEへの密結合ゆえに)アップデートで壊れる、といった指摘がコミュニティにあります。1

クォータを上げるには別途AI Pro、AI UltimateのようなAIプランに加入する必要があります。しかし、下記のブログを見るとAI UltimateプランでもVibe Codingを中心にする場合は厳しい印象。。

不透明なJunieのクォータ制限を実際に使ってみて計測してみる - kinoko dev

私も少し触ってみましたが、日本語設定がなさそう?だったり、実際の出力も特別優れた点もないなと思いました。

これらの状況を踏まえ、Junieを利用するよりは環境自体を切り替えてしまおう、という決断に至りました。

Cursorを中心に使っている理由

Cursorは既にお馴染みになっているAIコードエディタです。VSCodeがベースとなっています。

cursor.com

提案された変更を差分で見せてくれるため、適用前に影響範囲を視覚的に確認できます。さらに、対話形式で「ここはこうしてほしい」と段階的に調整できるので、微妙なニュアンスまで詰めやすいです。自分のように細部にこだわって開発したい人には非常に相性が良いと感じています。

  • 差分表示で提案内容を確認し、必要箇所だけ選択的に適用できる
  • 対話形式での再提案・再修正がスムーズ(やり取りの履歴が文脈として効く)
  • 大きな変更でも可視化されるため、不意の巻き込みを避けやすい
  • エディタ内で完結するため、集中が途切れにくい

基本的にはClaude4を中心に使用していますが、Autoモードも調整程度であれば十分に使える出力であるため、意識すればコストも安く済ませることが可能です。

おわりに

現状、特に大きな不便はなく、AI開発の恩恵を日常的に受けられているため、しばらくはVSCode/Cursor中心でやっていく予定です。

書きやすさを重視してIDEに課金する時代には終わりが来たのかもしれません。変化が激しいので今後どうなるかはわかりませんが、エディタ環境の動向についても注目していきたいです。

【Flutter】とにかく楽にURLのファイルをスマホのダウンロードフォルダに保存するパッケージ「flutter_file_dialog」

Flutterのアプリ開発で、とにかく楽にURLのファイルをダウンロードフォルダへ保存する方法です。

今回は以下の要件に合うライブラリを探しました。

  • ダウンロードフォルダへファイルを保存できる。
  • 最低限の労力とプロジェクト設定で実装できる。
  • ダウンロードしたファイルがファイルアプリなどで閲覧できる。(メディアスキャンが動作する、または気にする必要がないこと)

これらの条件を満たすライブラリとして、flutter_file_dialogを使用します。
iOS/Androidのみに対応するライブラリになるため注意してください。

pub.dev

実装

final url = "https://placehold.jp/150x150.png";

ElevatedButton(
  onPressed: () async {
    final dio = Dio();
    final fileName = Uri.parse(url).pathSegments.last;

    final response = await dio.get<List<int>>(
      url,
      options: Options(
        responseType: ResponseType.bytes,
        followRedirects: false,
      ),
    );

    if (response.statusCode == 200) {
      final data = response.data;
      if (data != null) {
        final params = SaveFileDialogParams(
          data: Uint8List.fromList(data),
          fileName: fileName,
        );
        final savedPath = await FlutterFileDialog.saveFile(
          params: params,
        );

        if (savedPath != null) {
          final snackBar = SnackBar(content: Text("$url を保存しました"));
          ScaffoldMessenger.of(context).showSnackBar(snackBar);
        }
      }
    }
  },
  child: Text("$url を保存"),
),

ファイルをバイナリでダウンロードするためにdioを使用していますが、お好みのネットワークライブラリを使用してください。

Android iOS

おわりに

ライブラリを作ってくださっている作者様に感謝🙏

【Flutter】FlutterのネイティブコードにKotlin Multiplatformを導入し、連携してみる【KMP】

この記事はFlutter Advent Calendar 2024の19日目の記事です。

前置き

Google謹製のクロスプラットフォーム技術であるFlutterは素晴らしいツールです。しかし、Flutter(Dart)の実装のみでアプリの全てのユースケースをカバーできるわけではありません。場合によってはネイティブコード(SwiftやKotlin)を書く機会もあります。下記は一例です。

  • 音楽や動画再生において、マッチするライブラリがなく自作する場合
  • 電話、SMS、ネットワーク、NFCなど低層の技術を扱う場合
  • ウィジェットを作成する場合

アプリを作り始める前にこれらを実装することがわかっている場合、Flutterを採用せずに最初からKotlin、Swiftで作ることが大半だと思います。しかし、Flutterアプリに後からこれらのようなネイティブコードを追加していく、といったケースもあるでしょう。そうした場合に、できればコードをある程度共通化したい!と思うかもしれません。

今回はFlutterのネイティブコードを書く時に参考にできるかもしれない?FlutterのネイティブコードにKotlin Multiplatformを混ぜ込んでみる実装を試してみました。

Kotlin Multiplatform(KMP)について

FlutterやReact Nativeと同様なクロスプラットフォーム技術の選択肢として、Kotlin Multiplatformが近年注目されています。

kotlinlang.org

Kotlin MultiplatformはKotlinを使用して実装を行った後、コンパイルでネイティブのフレームワークに変換されます。これによりiOSでもネイティブで書いたアプリと同じようなパフォーマンスを担保しやすいです。また、実行前にコンパイルされたものを使用するため、Flutterとは別軸で開発することも可能です。

For example, in mobile applications, shared code written in Kotlin is compiled to JVM bytecode for Android with Kotlin/JVM and compiled to native binaries for iOS with Kotlin/Native. It makes the integration with Kotlin Multiplatform seamless on both platforms.

https://www.jetbrains.com/help/kotlin-multiplatform-dev/faq.html#what-is-kotlin-native-and-how-does-it-relate-to-kotlin-multiplatform

Sample Code

github.com

アプリ説明

Flutter側で日時のデータを保存して、KMPから呼び出すことができます。

iOS

Android

解説

それでは、FlutterプロジェクトにKMPを混ぜ込む流れを説明します。

プロジェクト設定

Flutterプロジェクトの作成

通常のFlutter開発と同様に、Flutterプロジェクトを作成します。

Dart and Flutter learning pathway

Kotlin Multiplatform WizardでKMPのベースを作る

以下のサイトからプロジェクトの雛形を作ります。なお、Android Studioの機能など、他に雛形を作れる方法があればそちらを使っても問題ありません。

Kotlin Multiplatform Wizard | JetBrains

iOSの選択では、今回はUIはFlutterで表示するためDo not share UI (use only SwiftUI)でOKです。

KMPのベースをFlutterへ配置する

前段で作成したKMPのプロジェクトをFlutterプロジェクトへ持っていきます。 今回はFlutterプロジェクトからの使用を想定しているためsharedフォルダとその設定用のGradleファイル以外は必要ありません。

FlutterプロジェクトにKMPのshared周りのファイルを埋め込み。本来はもっと整頓したほうがよいです。

Gradleの設定をGroovy向けに修正

KMPのプロジェクトのGradle設定はKotlin DSLですが、Flutterは執筆時点での安定版(3.24.5)のデフォルトはGroovyを使用しています。そのため、今回はKMP側のbuild.gradle.ktsをGroovyでも動作するように修正を行なっています。
主な変更点は以下です。

  • androidTarget()android()に変更
  • sourceSetsの書き方を調整
  • jvmToolchainを追加
    • こちらはお使いのJDKに合わせてバージョンを設定してください。

元の設定もコメントとして残していますので、詳しくはGradleファイルをご確認ください。

kmm_flutter_sample/shared/build.gradle.kts at main · alpha2048/kmm_flutter_sample · GitHub


なお、既にKotlin DSLへの対応を済ませている方は上記の変更は必要ないかもしれません。

先日発表された3.27のリリースでは、Kotlin DSLへの対応についてアナウンスされています。いずれはKotlin DSLがデフォルトになりそうですね!

medium.com

FlutterのiOS設定を編集する

Wizardで生成されたiOSプロジェクトを参考に、KMPのコードをビルドするRun ScriptをFlutterのXcodeプロジェクトに追加します。


ここまでの設定で、FlutterでKMPのsharedモジュールを読み込む準備ができました。

(何か書き忘れた設定があればお知らせください🙏)
(実際の運用では動かないパターンがあるかもしれません。その場合はご自身で設定の調整をお願いします。)

実装

Method Channelを使用して連携

FlutterからMethod Channelを使用してiOS/Androidのネイティブコードを呼び出し、さらにKMPの処理を呼び出します。

AppDelegateMainActivityにFlutterから呼び出すためのMethod Channelを実装し、Flutter側 <--> Kotlin、Swiftとの連携箇所を作ります。そして、KMPのデフォルト実装のGreetingクラスを呼び出しています。

static const methodChannel = MethodChannel('platform_method/kmp');
...
final res = await methodChannel.invokeMethod('getGreeting');

kmm_flutter_sample/lib/main.dart at main · alpha2048/kmm_flutter_sample · GitHub

        let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
        let kmpChannel = FlutterMethodChannel(name:  "platform_method/kmp", binaryMessenger: controller as! FlutterBinaryMessenger)
        kmpChannel.setMethodCallHandler({
            (methodCall: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
            if (methodCall.method == "getGreeting") {
                let greet = Greeting().platformNameWithString(context: NSObject())
                result(greet)
            } else {
                result(FlutterMethodNotImplemented)
            }
        })

kmm_flutter_sample/android/app/src/main/kotlin/com/example/kmp_flutter_sample/MainActivity.kt at main · alpha2048/kmm_flutter_sample · GitHub

        val channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "platform_method/kmp")
        channel.setMethodCallHandler { methodCall: MethodCall, result: MethodChannel.Result ->
            if (methodCall.method == "getGreeting") {
                val greet = Greeting().platformNameWithString(this)
                result.success(greet)
            }
            else {
                result.notImplemented()
            }
        }

kmm_flutter_sample/ios/Runner/AppDelegate.swift at main · alpha2048/kmm_flutter_sample · GitHub

これによってKMP側で共通化されたコードを呼び出すことができました👍

shared_preferenceを使用してデータを同期する

ただKMPの処理にアクセスができただけでは味気ないので、今回はFlutterアプリで保存したデータをKMPから呼び出す以下の実装を行なっています。

まず、Flutterからshared_preferencesで日時を保存します。

final data = DateTime.now().toIso8601String();
final prefs = await SharedPreferences.getInstance();
await prefs.setString('savedDataFromFlutter', "保存日: $data");

そして、KMP側でUserDefaults/SharedPreferencesから保存したデータを取り出します。iOSはUserDefaults(platform.Foundation.NSUserDefaults)で、AndroidはSharedPreferencesでshared_preferencesのキーから情報を取得しています。

共通部分

class Greeting {
    private val platform = getPlatform()

    fun platformNameWithString(context: SPref): String {
        val savedData = context.getFlutterSavedData()
        return "Hello, ${platform.name}! with data: $savedData"
    }
}

expect class SPref
expect fun SPref.getFlutterSavedData(): String

Androidの実装部分

import android.app.Activity
import android.content.Context

actual typealias SPref = Activity
actual fun SPref.getFlutterSavedData(): String {
    val preferences = this.getSharedPreferences(
        "FlutterSharedPreferences",
        Context.MODE_PRIVATE
    )
    return preferences.getString("flutter.savedDataFromFlutter", "データなし") ?: "データなし"
}

iOSの実装部分

import platform.Foundation.NSUserDefaults
import platform.darwin.NSObject

actual typealias SPref = NSObject
actual fun SPref.getFlutterSavedData(): String {
    return NSUserDefaults.standardUserDefaults.stringForKey("flutter.savedDataFromFlutter") ?: ""
}

なお、shared_preferencesパッケージに新しく追加されたSharedPreferencesAsyncなどは、Androidで内部でDataStoreを使っており、同じプロセス内で複数のインスタンスを作成できないことからKMP側でアクセスできないため、古いAPIを使用しています。 今回はshared_preferencesを利用していますが、実運用を考えた場合、独自のDBなどを使って管理することを検討したほうがよいかもしれません。

おわりに

ということで、今回はFlutterにKMPを埋め込んでみる挑戦でした。

今回は綺麗に整理して埋め込んだわけではないため、実プロジェクト等で協調して動かすにはコツがいりそうですが、思ったよりも簡単に導入できそうですね。何かの参考になれば幸いです。

Xはじめてます

URLがXになったということで、ちょっとだけ宣伝。
この記事にプレビュー貼っておきます↓

昔から趣味活動向けのアカウントは持っていたのですが、業務用のアカウントがないためご迷惑をおかけする場面も多々ありました。
今後はブログには書くほどではないけど技術的に気になるな、と思ったことはXに書いていきます。どうぞよしなに。

x.com

【Flutter】(2024年4月時点) プライバシーマニフェスト対応についての細かいポイントまとめ

Appleのプライバシーマニフェスト対応の適用日である5/1までもうすぐと迫りました。この記事では、プライバシーマニフェストの説明は省き、具体的に困りどころになるであろう細かいポイントをまとめます。追加情報が出てきたら順次加筆・修正します。

4/26のAppleアナウンス

Appleからプライバシーマニフェストに関して追加の情報が発表されました。

developer.apple.com

まず、以下の対応が必要であることが書かれています。

  1. 記載が必要なAPIを載せる
  2. プライバシーマニフェストを追加する
  3. バイナリSDKは署名が必要

そして、以下の場合にはアプリを提出しても却下されます。

  1. 記載が必要なAPIを載せていない
  2. 埋め込まれた動的フレームワークの一部であること
  3. 一般的なサードパーティSDKリストに記載のあるサードパーティSDK

事前にやるべきだと思われていた内容からかなり譲歩されて、リストに記載されたサードパーティSDKのみに限定されたようです。さらに、動的フレームワークのみ対象となり、アプリ本体のバイナリに組み込まれる静的フレームワークは対象外になったようです。
実際にこの制限に緩和されたのかはアプリをアップロードして試してみないとわかりませんが、公式からの情報なので参考になりそうですね。おせーよ。

将来的にはアプリ全体への適用が示唆されていますが、5/1に向けてはかなり安心できる内容になったかと思います。

注意点まとめ

flutterfire ライブラリは最新まで上げる

Firebase Apple SDKの最新版である10.24.0のリリースが4/9に行われています。 このリリース内で、Crashlyticsのプライバシーマニフェスト記載必須のmach_absolute_timeが削除されています。

Flutterでは、このSDKに対応したflutterfireのfirebase_coreは4/16にリリースされています。ギリギリの対応になっていますが、アップデート未対応の方は急ぎましょう。

https://github.com/firebase/flutterfire/blob/master/CHANGELOG.md#firebase_core---v2300

iOSのコードを含むライブラリのバージョンアップ

注意が必要な点として、iOSのコードを含むライブラリのバージョンを上げる必要があります。

例えば、flutter.devが公開しているshared_preferenceは子ライブラリであるshared_preferences_foundationが実際のiOSの処理を担当していますが、shared_preference側からの最小バージョンの依存関係は更新されていませんでした。そのため、pubspec.yaml上で更新しても、pubspec.lockのshared_preferences_foundationが古いままというケースがありえます。

この依存関係の解決のPRが4/11に行われていますので、flutter.devのライブラリ群も最新にしておくと良いでしょう。

Update multiple packages to depend on versions with iOS privacy manifest included by vbuberen · Pull Request #6355 · flutter/packages · GitHub

空のプライバシーマニフェストを追加する必要はない

iOSのコードを含むライブラリで、特に記載が必要なAPIを使っておらず、何もデータ収集をしていない場合、空のプライバシーマニフェストを追加する必要があるのではないか?と思うかもしれません。こちらはAppleのフォーラム上でDTSエンジニアから回答がありました。

IS PrivacyManifest.xcprivacy still… | Apple Developer Forums

If your framework doesn't require a privacy manifest, do nothing. Avoid adding an empty privacy manifest to your framework.

つまり、空のプライバシーマニフェストを入れる必要はないということです。もしまだプライバシーマニフェストに対応していないライブラリがあった場合、追加するべきだと早合点でず、落ち着いて製作者に問い合わせたり自身でAPIを調べるなりしましょう。

プライバシーマニフェストに空のDictionaryを入れない

プライバシーマニフェストにTracking Domeinなどを追加する項目がありますが、ここに空の<dict/>が入っていると、無効なプライバシーマニフェストと言われるみたいです。無駄に厳しくない?

https://www.reddit.com/r/iOSProgramming/comments/1br2n2z/unexplained_issue_with_privacy_manifest_file/

Flutterとして各パッケージのマニフェストを調査・処理する方法は検討中

プライバシーマニフェストに未対応のライブラリがある場合、どのライブラリがプライバシーマニフェスト要件に合致していないのかを通知してくれます。しかし、Flutterで静的フレームワークなどが「Runner」として丸め込まれてしまい、調査が困難になるケースが発生しています。

Flutter側としては現在解決策を持っておらず、下記のコメントの通り自力でどうにかするしかないというのが現状です。

Flutter iOS documentation to address "ITMS-91053: Missing API declaration" · Issue #145269 · flutter/flutter · GitHub

Determine how to handle privacy manifests in packages · Issue #131940 · flutter/flutter · GitHub

静的フレームワークへの対応について

上記に関連します。躓きやすいポイントです。

ライブラリを静的フレームワーク(Static linking)として読み込むと、アプリのバイナリに直接コードが組み込まれます。ですが、Apple側のプライバシーマニフェスト判定ではアプリ内のコードとして判定されてしまいます。 静的フレームワークを使用する場合、静的フレームワークのプライバシーマニフェストをアプリに記載する必要がありそうです。

但し、4/26のAppleからのアナウンスを見ると、こちらの対応は必須ではなくなったようです。