おつかれさまです。すぺきよです。
今回は、AndroidというかFlutterというか、たぶん私が悪いんでしょうけれど、想定外のエラーではまったお話です。
数時間悩んで調べて、不具合の原因がわかったときはここかよ!って脱力しました。
ほぼ、戯言のようなものですが、お付き合いください。
こんな話でもきっと誰かのお役に立つと信じて記事にします。
今回はFlutterのバージョンなどの環境は関係がないので記載しません。
何があったのか
ある日、ステージング環境で動作確認中に納品先担当者様が利用している特定のAndroid端末で動作させたときにのみ発生する問題が存在することが発覚。
そのアプリはGoogle Play Storeの内部アプリ共有機能を利用して納品先担当者様に渡すルールで運用しています。
原因がわからないので、ステージング環境向けのアプリで発生した例外とスタックトレースをサーバーに送信するというデバッグコードを仕込んで、納品先担当者様と動作検証をしていました。
仕込んだデバッグコードにより、エラーの原因はすぐに特定でき、機能を修正しこれで問題解決だと安心していたところ、担当者様より。
「起動直後にアプリ全体がグレーで表示されて使えなくなっている」
との指摘が。
そんなはずはないと、デバッグビルドでアプリをインストールして動作確認すると問題なく動作する。
この問題も特定端末での問題と判断し、「これだからAndroidは・・・」とAndroidに内心悪態をつきながら調査を開始しました。
その特定端末と同じ問題が発生する機種が手元にないために、Google先生に聞いたり、ChatGPTに聞いたり、Perplexityに聞いたりと手あたり次第調べましたが、原因は不明。
そもそも例外があったら、キャッチしてサーバーに送信するようにデバッグコードを組んでいる状況で、例外が飛んできていないので何が起こっているかわからない。
自分の開発環境でも再現できないので、調べようもない・・・。
完全に暗礁に乗り上げました。
現象が再現した
どうもこうもうまくいかないので、納品先担当者様がダウンロードしているステージング環境のアプリを自分のテスト機にインストール。
するとあら不思議、さっきまでデバッグビルドだったら動いていたはずのアプリの画面が一面グレーで表示されます。
このことから、デバッグビルドではなく、リリース用ビルドをしたときにのみ発生する厄介な問題と判断。
改めて調査を開始します。
ソースをロールバックしながら動かす
以前のバージョンではリリース用ビルドでうまく動作していたことは確認済み。
そして、ソースはGitで管理しているので、コマンド一発で、過去のソースに戻すことができます。
そこで、少し戻してはビルドして、動作確認。少し戻してはビルドして、動作確認。。。
を繰り返し、一つ古いバージョンまで戻しながら、どこで問題が発生したか調査を行うこととしました。
少し戻してはビルドして、動作確認、ダメ。。。少し戻してはビルドして、動作確認、ダメ。。。
とうとう、一つ古いバージョンまで戻ってきて。。。ビルドして。。。動作確認。。。ダメ!?
何ということでしょう。
以前にリリースしたバージョンのアプリも動かなくなっているじゃないですか。
このことから、アプリが利用しているパッケージ(部品)をアップデートしたときにエラーが発生してしまったと判断。
パッケージを一つ一つ見直すのか・・・。
とうなだれながら調査を続行。
解決の糸口
パッケージ(部品)を一つ一つ見直すのは、大変だし嫌だなぁ。
せめて何かヒントはないかなぁ。
でも、エラーログが確認できる開発環境じゃエラーが出ないし、リリースビルドだとデバッグコードでもエラーが来ないし、何かエラーが発生しているとしてもわからないよなぁ・・・。
と、途方に暮れていた時、私の記憶の奥底からささやく声が。
???「僕を使えば、デバッグビルドじゃなくても、リリース用ビルドでもエラーが見れるかもしれないよ。」
私「あなたは!?」
Logcat「僕だよ、Logcatだよ」
そう、Android Studioに付属しているかわいい猫のアイコンのLogCatです。
実機を接続すると、アプリに関係あるのもないのも含めて大量にログを出力してくれている、とても読みづらいLogCatです。
いつもはログがうるさすぎて基本的には見ませんが、今回はそんなことを言ってられません。
もちろんエラーが取れない可能性もありますが、可能性が0でない以上、試してみる価値ありです。
祈りながら実機をつないで、Logcatをとおして、動作しないアプリを起動し、ログを見てみます。
エラーが取れた
案の定、大量のログが出力、ほかのログが出力されて画面から消えないようにすぐにコピーして、読みやすいようにエディタに貼り付けて1行ずつ調査開始です。
そうすると、すぐにエラーメッセージとスタックトレースに遭遇します。
そのエラーメッセージとは・・・。
「FormatException: Invalid radix-10 number (at character 1)」(書式エラー:正しい10進数の数字じゃないよ)
エラーを出している処理を誰が呼び出しているのか!これがわかればパッケージがわかる!
すぐさまスタックトレースをチェック!
「呼び出しているpackage:(自分のアプリ名)」
!?
なんとエラーを出している処理を呼び出しているのは自分の開発したアプリ本体じゃないですか。
10進数の数字じゃないって、いったい何が・・・。と調べてみると、該当箇所はバージョン番号を処理しているところでした。
自身のアプリのバージョン番号とサーバーで管理している最新のバージョン番号を比較する処理のバージョン番号を分離する処理です。
final List versions = version.split('.').map((String part) => int.parse(part)).toList();
原因が判明
アプリのバージョン番号は1.0.0のようにピリオドで区切られた3つの数値で構成されているはずなので、数字以外が来るはずがありません。
実際に何が来ているのか、バージョン番号をデバッグ出力を確認してみると。
「1.0.0(d1)」
開発環境では「1.0.0」を設定しているはずなのに、なぜかデバッグ版を示す「(d1)」がついている。
そう、このデバッグ版を示す「(d1)」がついているために、バージョン番号が正しく数値化ができず「Invalid radix-10 number」と言われているのでした。
では、開発環境ではついていないバージョン番号の「(d1)」がどこから来たかというと。。。
なんとGoogle Play Consoleに内部アプリ共有でアプリをアップした際に入力を求められる「バージョン名」です。
てっきりここで入力するバージョン番号は管理用の名前で、何をつけてもいいものだと思っていましたが、まさかアプリのバージョンにそのまま適用される値だったとは・・・。
まとめ
今回のエラーは内部アプリ共有のバージョン番号は何を入れてもいいと思っていた、私の思い込みによるエラーでした。
しかも起動してすぐ、アプリのWidgetを構築する際にChangeNotifierProviderのModel内でバージョン番号をチェックしていたので、Widget構築中に例外が出て構築処理が停止してグレーで表示されてい待っていたのでしょうね。
例外をキャッチしていたのも、今回発生した問題はMain関数内の初期所内部でここにtry catchを仕込んで例外を撮っていましたが、Widgetのビルド処理内で発生した例外はキャッチできていないことに気づいていませんでした。
今回の教訓は
- 本番向けのアプリであっても、Logcatを使えばログからある程度ヒントを見れる可能性があるのでまずは見てみよう。
- Google Play Consoleの内部アプリ共有で入力するアプリバージョンは本当にアプリのバージョン番号になるので注意しよう。
- FlutterのMain関数とBuild処理は別スレッドで、どうやらMain関数でTry catchしてもダメだとうこと。
ってことですね。
Flutterアプリって、例外が発生してもクラッシュせず、操作できないままゾンビのように起動しっぱなしになるので、厄介ですね。