日本のコロナウィルス感染数などを表示するAndroidアプリをFlutterで作ったけど審査通らなかった話

想像力の欠如

tl; dr

  • アプリのガイドラインはちゃんと読みましょう
  • 感染症対策に関わっている皆さん 本当にありがとうございます

それはそう

developers-jp.googleblog.com

ストアの掲載に関するガイドライン

Google Play は、ユーザーに正確で関連性のある情報を提供するという責任を果たすことに真摯に取り組んでいます。 そのため、現在、ストアの掲載情報で 新型コロナウイルス感染症(COVID-19) とそれに関連する表現を使用できるのは、政府機関または公的医療機関によって公開、委託、または承認されたアプリのみとさせていただいています。加えて、広告、アプリ内アイテム購入、アプリ内寄付などの収益化メカニズムが含まれていないアプリであることも条件です。なお、ストアの掲載情報には、アプリ名、説明、リリースノート、スクリーン ショットなどが含まれます。 この条件に該当しないアプリが 新型コロナウイルス感染症(COVID-19) や関連する表現を使用された場合、ストアへの掲載が承認されなかったり、ストアから削除される場合がありますのでご注意ください。

はい...

f:id:osamtimizer:20200508002518p:plain
すみませんでした

リジェクトされた時の文言がかなり強めでギョッとしたんだけど、このガイドライン自体は心の底から正しいと思う。

なんで作ったの?

コロナウィルスに関する情報を閲覧できるWebサービスは結構あるんだけど、モバイルアプリが全然なかった。その一方で、公開されていてかつ利用可能なデータが多かったので、じゃあ自分で作るかとなって作った。*1

iOS版は作らなかったの?

AppStore Connectのサブスクリプションが高い(11,800円)のでそもそも申請してない。もちろんAppleガイドラインGoogleと同じ。

https://developer.apple.com/jp/news/?id=03142020a

Flutterで作ったんすよ

ソース自体はMITライセンスで公開しているので、供養としてここに記す。

github.com

Firebaseの設定ファイルが無いとビルド通らないので注意。試してないけど、pubspec.yamlからFirebaseに関するパッケージを消せば動くと思う。

dependencies:
  // 以下の3つを消して flutter pub getを実行
  firebase_core: ^0.4.4+3
  firebase_analytics: ^5.0.11
  firebase_crashlytics: ^0.1.3+3

アプリの画面はこんな感じ。全国と都道府県別の情報を閲覧できる。

f:id:osamtimizer:20200508002734p:plain f:id:osamtimizer:20200508002749p:plain

表示しているデータはどこから取ってきてるの?

東洋経済オンラインの新型コロナウイルス国内感染の状況というページに表示されているものと同じデータを使用させてもらった。

toyokeizai.net

データ自体はGitHubのこちらのリポジトリで毎日更新されているようだった。

github.com

  1. データやソースコードを自分のSNSやブログで使ってもよいですか?

  2. 商用・非商用を問わずご自由にお使いください。具体的な基準はMITライセンスに準拠します。許可を取る必要はありませんが、事後報告でよいのでご連絡をいただくと「今後も続けていきましょう」と社内に言いやすいので助かります。

Androidアプリ公開に関する一連のフローを体験できた

審査は通らなかったけど、Flutterでイチから自分でアプリ完成までコード書けたのと、Google Play Consoleでアプリの申請に必要なものごとを体験できたので、思っていたより収穫があった。*2

Flutterの知見や、審査を申請する上で知らなかったものが結構あったので、ここに記しておく。

Flutter

エラーハンドリング

iOSネイティブだったらエラーはViewControllerの層までrethrowし続けて、ユーザーに伝える必要があったらエラー用のViewを表示するといった処理を書くことが多い。これは手続き型UIプログラミングの強みである何かが起こったら、こうするといった記述がしやすいため、問題になることは多くないと思う。

一方で、Flutterは宣言的にUIを記述するため、このようなアプローチがうまくいかないケースが出てくる。

FlutterにはScaffoldという、画面上部のナビゲーションバーを伴った一枚の画面を表示するためのウィジェットがあって、 このScaffoldにはshowSnackBarというメソッドを呼ぶことで、画面下部にスナックバーを表示する機能がある。

flutter.dev

表示するためにはScaffold.of(context).showSnackBarといった感じで、現在のウィジェットよりも上位のScaffoldに対してメソッドを呼び出す必要がある。

api.flutter.dev

このアプリはProvider パターンで作っていたので、APIへのリクエストを要求する層ととエラーを表示したい画面が一致しないことが多く、単純に例外をrethrowするだけでエラーメッセージを伴ったスナックバーを表示するのは難しかった。

具体例としては、APIへリクエストを要求する処理をScaffoldの生成より先に行いたい場合などが挙げられる。

どうしたものかと割と後半までエラーハンドリングを放置していたんだけど、ある時ふと、APIから取得したデータと同じように、エラーメッセージを購読可能にすればいいと気づいた。

で、エラーメッセージを表示したいウィジェットの層でエラーを購読することでうまくハンドリングができるようになった。

// store

class TabBarStore extends ChangeNotifier {
  int _currentIndex = 0;
  int get currentIndex => _currentIndex;
  String _errorMessage;
  String get errorMessage => _errorMessage;
  ...
}

// subscription

    final tabBarStore = Provider.of<TabBarStore>(context, listen: false);
    tabBarStore.addListener(() {
      if (tabBarStore.errorMessage != null) {
        final snackBar = SnackBar(
          content: Text("エラーが発生しました。通信環境を確認するか、時間を置いてから再度試してみて下さい。"),
          action: SnackBarAction(
            label: "閉じる",
            onPressed: () {
              tabBarStore.clearError();
            },
          ),
        );
        // エラーを表示したいウィジェットに最も近い上位のScaffoldに対してスナックバーの表示を要求する
        Scaffold.of(context).showSnackBar(snackBar);
      }
    });

購読が少し回りくどいように見えるかもしれないが、Provider.of<TabBarStore>(context, listen: false) といったように、明示的にlisten: falseを指定しない場合、エラーメッセージが更新された瞬間にリビルドがかかってしまう。

Flutterではbuildメソッドの途中でリビルドが発生すると例外を吐く上に、buildメソッド自体は頻繁に呼ばれるので、不要なリビルド要求は避けていく必要がある。

api.flutter.dev

エラーメッセージが更新されたら、こんな感じにスナックバーが出るようになる。

f:id:osamtimizer:20200508010835p:plain

具体的な実装はこの辺に書かれているので、気になった方は確認してみてほしい。

covid_19_viewer/tab_bar_store.dart at cee49e63f9537dbbd358e52ea33a6b06eb0549b6 · osamtimizer/covid_19_viewer · GitHub

covid_19_viewer/nation_wide.dart at cee49e63f9537dbbd358e52ea33a6b06eb0549b6 · osamtimizer/covid_19_viewer · GitHub

審査

プライバシーポリシーが記述されたWebページが必要

アプリの審査に申請するには、プライバシーポリシーが掲載されたWebページのURLが必要だった。ので作った。

https://covid-19-viewer-document.now.sh/

Flutterに結構慣れてきたので、Flutterに似た概念のReactを採用しているNext.jsに入門して、その勢いでプライバシーポリシーのページを作った。

作ったページはVercelにデプロイされるようにした。*3

vercel.com

VercelにはGitHubのmasterブランチへのpushやmergeをトリガーにしてデプロイする機能があったのでそれでデプロイした。 ドメイン.now.shが末尾につくだけなので、特別に何かを管理することがなくてとても楽。

おわりに

Googleのアプリ審査の時間をいたずらに浪費させてしまったことは素直に申し訳なかったと思う。 その一方で、Play ConsoleやFlutterに関する理解は深まったので、それはそれで良しとしたい。

*1:モバイルアプリがないのはもちろん審査が通らないからです

*2:しかし、Developerアカウント取得に支払った$25は返ってこない

*3:作っている途中でZeitから名前が変わっていた