「課金ライブラリを上げてください」の一言が、Flutter 本体アップグレードの大工事になった話

個人開発(SINQWELL)で公開している Flutter アプリのうち3本に、Google Play から同じ内容のメールが届いた。

お使いのアプリは古いバージョンの Google Play Billing Library を使用しています。2026/08/31 までに 8.0.0 以上へ更新してください。

対象は3本。月謝管理アプリ「月謝袋」、リズム練習アプリ「Rhythm Academia」、英熟語学習アプリ「イラスト英熟語」。Play Console には赤いポリシー違反の警告。締切つき。気持ち悪いので早く消したい。

「バージョンを1つ上げるだけでしょ」と思って着手した。……そこから丸2日、ドミノが倒れ続けた。この記事はその忘備録である。


目次

そもそも何をすればいいのか

Flutter で課金を実装している場合、Google Play Billing Library のバージョンは自分で直接書くものではなく、課金プラグインが内部で抱えている。だから、やることはプラグインのバージョンを上げること。

  • in_app_purchase を使っている → ^3.3.0 に上げる(内部の in_app_purchase_android が 0.5.2 になり、これが Billing Library 8 系)
  • RevenueCat(purchases_flutter)を使っている → v9.0.0 に上げる(v9 が Billing 8 対応)

3本のうち、月謝袋は RevenueCat と in_app_purchase の両方、残り2本は in_app_purchase を持っていた。方針は立った。あとは上げるだけ——のはずだった。


ドミノ1枚目:in_app_purchase 3.3.0 は Dart 3.10 を要求する

flutter pub add in_app_purchase:^3.3.0 を叩くと、いきなり弾かれた。

in_app_purchase >=3.2.4 requires SDK version >=3.10.0, version solving failed

課金プラグインを上げるだけのつもりが、Dart SDK のバージョン要件に引っかかった。当時の環境は Dart 3.9。つまり Flutter 本体を上げないと、このプラグインは入らない。

flutter upgrade。ここが分岐点だった。この一手が、後続のドミノをすべて倒すことになる。


ドミノ2枚目:NDK のダウンロードが壊れる

アップグレード後、最初のビルドがこれで落ちた。

[CXX1101] NDK at .../ndk/28.2.13676358 did not have a source.properties file

flutter upgrade の過程で NDK が中途半端にダウンロードされ、壊れたらしい。対処は単純で、壊れた NDK フォルダを消して再取得させるだけ。

rm -rf ~/Library/Android/sdk/ndk/28.2.13676358

再ビルドで自動的に落とし直され、これは解決。まだ序の口だった。


ドミノ3枚目:IconData が final class になり、Font Awesome が壊れる

新しい Flutter でビルドすると、今度は自分のコードでもプラグインでもなく、font_awesome_flutter の内部でコンパイルエラー。

The class 'IconData' can't be extended outside of its library because it's a final class

Flutter 3.27 で IconDatafinal class になった(公式の breaking change)。旧 font_awesome_flutter(10.7.0)は IconDataBrands extends IconData のように IconData を継承する作りだったので、継承が禁止されて全滅する。

厄介だったのは、10.x 系にこの修正版が無いこと。最新の 10.12.0 でもまだ継承のまま。修正されたのは 11.0.0 からで、しかも 11.0.0 は「FontAwesomeIcons.fooIconData ではなく FaIconData になる」という破壊的変更を伴う。

月謝袋は FlutterFlow で作ったアプリで、生成コードの奥で Font Awesome を使っている。ネット上には「11.0.0 は FlutterFlow の生成ウィジェットを壊す」という報告もあり、身構えた。

だが実際に 11.0.0 の中身を読んでみると、移行はそこまで大きくなかった。

  • ページ側の FaIcon(FontAwesomeIcons.stamp) のような呼び出しは、そのまま通る。定数自体が 11.0 で FaIconData になったので、FaIcon が要求する型と一致するから。
  • 壊れるのは、生の IconDataFaIcon に渡している箇所だけ。FlutterFlow 生成の2ファイル(flutter_flow_icon_button.dartflutter_flow_widgets.dart)がそれで、ここだけ FaIconData(...) でラップしてやれば直る。
// Before(新Flutterでコンパイル不可)
FaIcon(widget.iconData!, ...)

// After
FaIcon(FaIconData(widget.iconData!), ...)

「全部書き換え」に見えて、実際に手を入れたのは2ファイル。エラーメッセージの見た目より、原因の切り分けが効く典型例だった。


ドミノ4枚目:page_transition が消えた constructor を参照して死ぬ

font_awesome を直したら、次はこれ。

page_transition-2.2.1/.../page_transition.dart:99:
Error: Couldn't find constructor 'CupertinoPageTransitionsBuilder'.

これも自分のコードではなく、プラグイン内部。Flutter 3.43/3.44 で CupertinoPageTransitionsBuildermaterial ライブラリから cupertino ライブラリへ移動した(公式 breaking change)。page_transition は material しか import していないので、デフォルト引数に書かれた const CupertinoPageTransitionsBuilder() が解決できなくなった。

そして今回いちばん困ったのが、page_transition には修正版が存在しないこと。最新の 2.2.1 が 2024年12月で、以降更新が止まっている。upstream 待ちはできない。

そこで、アプリ内での実際の使われ方を調べた。すると——全ルートが hasTransition: false で、PageTransition(...) を通る分岐は一度も実行されないデッドコードだった。ならば話は早い。依存ごと削除して、ネイティブの FadeTransition に置き換えた。

transitionsBuilder: (context, animation, secondaryAnimation, child) =>
    FadeTransition(opacity: animation, child: child),

挙動は一切変わらず、更新の止まった依存を1つ減らせた。壊れた依存を無理に生かすより、使っていないなら外すのがいちばん健全だった。


ドミノ5枚目:RevenueCat 9 の戻り値が変わっていた

月謝袋だけは RevenueCat も使っている。purchases_flutter を 9 に上げたら、公式ブログには「コード変更不要」とあったのに、1箇所だけ現実は違った。Purchases.purchasePackage() の戻り値が CustomerInfo から PurchaseResult に変わっていた。

// Before
final customerInfo = await Purchases.purchasePackage(package);
final isPremium = customerInfo.entitlements.active.containsKey(id);

// After
final result = await Purchases.purchasePackage(package);
final isPremium = result.customerInfo.entitlements.active.containsKey(id);

getCustomerInfo()restorePurchases() は従来通り CustomerInfo を返すので、直すのは購入処理だけ。flutter analyze がちゃんと entitlements isn't defined for PurchaseResult と教えてくれたので、そこを頼りに修正。


罠:内部テストにアップロードしても、新バージョンが降ってこない

ビルドが通ったので内部テストにアップロード。しかし端末には古いバージョンのまま。キャッシュを消してもストアを開き直しても更新されない。

調べると、内部テストトラックの反映はそもそも不安定で、待っても来ないことがある。「ソースを変えないと反映されない」といった俗説もあるが、これは不正確(versionCode さえ上げれば別ビルドとして扱われる)。

結論として、確実なのは製品版の段階的な公開(20%)に直接上げるルートだった。まず 20% で出し、自分で購入 → 返金、またはライセンステスターで Billing 8 の課金が通ることを確認してから 100% へ引き上げる。この方が読める。


もうひとつの罠:Android Studio が「赤い波線だらけ」になる

全部直してビルドも成功したのに、Android Studio を開くと lib 配下が赤い波線だらけ。一瞬ヒヤッとする。

だがこれはIDE の表示のズレだった。Flutter 本体アップグレードと pubspec の書き換えで、IDE 内蔵の解析サーバーが古いパッケージ解決情報を掴んだまま「import が解決できない → 全ファイルがエラー扱い」になっていた。

判断の軸はシンプルで、flutter build が成功した = 全コードが実機用にコンパイルされた、という事実がいちばん強い。コマンドラインの flutter analyze が info/warning だけなら、コードは正しい。IDE の赤は File → Invalidate Caches / Restart で消える。IDE の赤より、CLI の結果を信じる。


教訓(次に同じ通知が来た自分へ)

  • 「1つ上げるだけ」を疑う。 課金プラグインのマイナー更新が、Dart SDK 要件 → Flutter 本体 → アイコン → 画面遷移 → と5枚のドミノを倒した。ストアの一言要件は、依存グラフ全体を揺らすことがある。
  • エラーは”見た目の量”より”原因の場所”で切る。 font_awesome は全滅に見えて実際は2ファイル。切り分ければ小さい。
  • 更新の止まった依存は、生かすより外す。 使っていない依存(page_transition)は、壊れたのを機に削除。健全化になった。
  • ビルド成功 > IDE の赤。 最終的な正解はコンパイルが通るかどうか。IDE のキャッシュに振り回されない。
  • 反映されない内部テストで消耗しない。 段階的な公開(20%)に直接乗せる方が確実なこともある。
  • CHANGELOG と終了ルーティンは効く。 何を・なぜ変えたかを都度残しておくと、こういう連鎖作業の後でも自分の判断を追える。

おわりに

3本すべて審査に提出し、Play Console の赤い警告は消えた。所要は丸2日。やったことを一言でまとめれば「課金プラグインをBilling 8対応版に上げた」だけなのに、間に挟まった Flutter 本体アップグレードが、これだけの副作用を連れてきた。

個人開発は、こういう”店側の都合で始まる保守作業”を一人で全部受け止めることになる。派手さはないが、アプリを生かし続けるというのはこういう地味な連鎖を捌くことなんだな、と改めて思った次第。同じメールを受け取った誰かの、ドミノを1枚でも減らせれば幸い。

— SINQWELL(個人開発)

目次