Androidアプリ開発を進めていくに当たって、いろいろな「決めごと」があります。はじめはどのような意味があるのか分からないまま決めてしまい、後になって後悔することも多いので、どのようなことが取り返しがつかないのか、どのように決めればよいのか、をまとめてみようと思います。
前半は当たり前のことを書いていますが、後半ほど意外と意識していない人も多いのではないかと思います。
※取り返しがつかない。といっても、全く別のアプリとして公開しなおすことまで許容してしまえば何でもありです。ここでは、同一のアプリとしてアップデートができない。もしくはアップデート時に何らかの問題が生じる可能性がある。などの意味になります。
アプリの公開のための情報関係
基本的なところなので問題が起こることは少ないとは思いますが、変更するとアプリの更新ができなくなるというものになります。
アプリのパッケージ名(ApplicationID)
アプリの開発を始めるに当たってまず最初に決めなければいけないものです。基本中の基本ですが、Android Studioを立ち上げ、プロジェクトを作成しようとした段階で入力を求められるものです。つまり、完全に初めての開発の場合、全く意味も分からない段階で決断が求められるものであります。
アプリのパッケージ名は一度公開すると変更することができません。どうしても変更する必要がある場合は別のアプリとして公開する必要があります。
公開した後での変更はできませんが、公開前であれば変更は可能なので、開発初期段階では仮の名前でもよいです。しかし、多くの場合リファクタリング機能を使い自動で書き換え可能とはいえ、アプリの動作の基本となるIDの変更ですので、公開版のテストが始まる前には決めておく必要があるでしょう。
アプリのパッケージ名はほとんど一般ユーザには見えないところで使用するため、変更せざるを得ないなんてことは、まずないでしょう。たまに、アプリ名などからは想像もつかないようなパッケージ名を見かけると、ああ、初期段階では方向性違ってたんだ、みたいな暖かい目で見てしまいます。
一応使われるかもしれない場所を書いておくと
- ストアのアドレス
market://details?id=<パッケージ名>
https://play.google.com/store/apps/details?id=<パッケージ名>
ぐらいでしょうか。あとはアプリが外部にインターフェースを提供する際、ユニーク性を担保するためにパッケージ名をベースとした名前をつけることになるぐらいでしょう。基本的にはエンジニア以外には見えない情報です。
アプリのパッケージ名の決め方
アプリのパッケージ名は、絶対にこうしなければならない、というルールがあるわけではなく、単に他のアプリとかぶらないものを使っていればどんなものでも使えはします。ただ、勝手な名前をつけていると他の人にも迷惑なので、通常はJavaのパッケージ名と同様に、所有するドメイン名を逆から書いたものをベースとします。
私の場合は、mm2d.net
というドメインを所有しているので、net.mm2d.*
というパッケージ名を使用しています。
企業の場合、somecompany.co.jp
などというドメインを所有しているでしょうからjp.co.somecompany.*
というパッケージ名を使えばよいでしょう。
ドメインは持っていない、という場合は、別の方法でユニークな名前を決める必要があります。Githubのアカウントを持っているなら、<user name>.github.io
とか<organization name>.github.io
いうドメインが使用できますので、これを利用するというのも手の一つでしょう。io.github.<user name>.*
といった具合です。
では、そのドメイン名ベースの後はどうすべきか、ですが、よく見かけるのはandroid
をつけるというものです。
とはいえ、android以外の世界で同じ名前が使われても実質困ることはまずないので、個人的見解としてはandroid
をつける必要はないと思います。自明であったり意味のない情報は名前に含めるべきではないと考えているので(私が公開しているアプリにはandroid
つきのものがあります。よく分からなかった頃につけたものです)
話戻って、ドメイン名ベースの次にくる名前ですが、個人、もしくは小さな組織であれば、「アプリ名」もしくはその「コードネーム」みたいなものを端的に表す単語をつければよいでしょう。Android Studioなどでプロジェクトを作成すると自動的にアプリ名が使われますが、必ずしもその名前にする必要はありません。そのままだと非常に読みにくい名前になってしまうことも多いでしょうし。ただし、同じ名前はつけられませんので、あまりに単純すぎる名前をつけてしまうと、名前がかぶる別のアプリを作るときに困るかもしれません。
多数の開発チームを抱え、大量のアプリを開発している非常に大きな組織の場合は、別のチームが似たようなアプリを同じ名前で出してしまう危険性がある(それ自体どうかとは思いますがw)ので、ドメインとアプリ名の間にチーム名みたいなものを一つ二つ挟むのもよいかもしれません。
アプリ内部ではあちこちで使うことになるため、ユニーク性を担保し、必要な情報を含みつつ、長くなりすぎないパッケージ名をつけたいところです。
アプリの署名
公開する場合、apkに署名を施さなければなりません。
署名の作り方はほかに譲るとして、これは一度つけてしまうと変更することができません。
変更する場合はアプリのパッケージ名を変更し、全く別のアプリとしてリリースするしかありません。
なのでkeystoreファイルは絶対になくさないようにしましょう!
また、署名は通常、アプリごとに用意するのではなく、管理主体が同一のアプリについてはすべて共通の署名を利用した方がいいでしょう。
共通の署名を行ったアプリ同士であれば、同一プロセスとして実行できるなど、異なる署名のアプリ同士よりも密な連携ができます。将来的に複数のアプリを公開し、互いに連携する機能を作ろうと思ったときに異なる署名をしていると後悔することになるかもしれません。
さて、そうなるとAndroidアプリの開発をしている間はずっと同じ署名を使い続けることになるので、署名の寿命も十分に長くとっておく必要があるでしょう。アプリケーションへの署名に書かれているように、最低でも25年にしておきましょう。25年後もAndroidが生き残っていて、開発を続ける必要があったら・・・まあそのとき考えましょう。
AndroidManifest.xmlに記述した情報
AndroidManifest.xmlに記述した情報の多くは外部から参照されうるものであるため、安易に変更できないものが多いです。
特にクラス名やパッケージ名が変更できなくなると、リファクタリングにも支障が出てきます。
寿命の長いアプリであれば、アーキテクチャの変更なども途中で入るでしょうから、その枠組みの外など、アーキテクチャを破壊しない薄い層として、AndroidManifest.xmlに記述したクラスを踏み台として置いておく場所などを用意するなどが必要かもしれません。
Launcher Activity のパッケージ名&クラス名
正確になんと呼ぶのかちょっと分らないので、Launcher Activityと表現しましたが、要するにCategoryがLAUNCHERになっている、AndroidManifest.xmlに
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
というintent-filter付きで書いているActivityです。
これを変更してもアップデート自体は問題なく行えますし、AppDrawerからの起動も問題なく行えます。
では、何が問題かというと、AppDrawerからのドラッグ&ドロップなどでユーザやホームアプリ等によって設置されたショートカットで問題が発生します。
AppDrawerのアプリアイコンは、Category=LAUNCHERであるActivityの情報を探して設置されますが、
ショートカットは作成された時点のLauncher Activityの情報(ComponentInfo)を保持しています。要するにタップされたときに明示的Intentを飛ばすための情報です。この情報はアプリのアップデートで同時に更新されたりはせず、作成されたときの情報のまま保持されます。
アップデートでActivityの名前やパッケージが変わってしまった場合、具体的にどのような動作になるかはホームアプリにもよりますが、ショートカットが自動的に削除されたり、ショートカットは残るが、タップしてもアプリを起動できなくなるといった動作になります。
つまり、せっかく作成された起動起点を失うことになります。
クラス名やパッケージ名はリファクタリングによって安易に変えられやすい上、そのアプリ単体ではなんの問題もなく動作することもあり、開発に関わるエンジニア全員が理解していないといけないことになります。
Activityに関しては activity-alias を使用すれば実際のクラス名とは異なる名前に見せることができるので、これを利用するのも一つの手です。
例えば、以下のMainActivityをHomeActivityとリネームしたとすると
<activity
android:name=".MainActivity"
android:label="${app_name}"
android:theme="@style/AppTheme"
>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
以下のようにactivity-aliasとして旧バージョンのActivity名にしておけば、外部からは旧クラス名で起動できますので、起動できなくなったり、ショートカットが削除されるのを避けることできます。また、activity-aliasで指定した名前のクラスは存在している必要はないので、はじめからLauncherActivityをactivity-aliasにしておけば、知らずにリネームしてしまうという事故を防ぐことができるでしょう。
<activity-alias
android:name=".MainActivity"
android:label="${app_name}"
android:targetActivity=".HomeActivity"
>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity-alias>
<activity
android:name=".HomeActivity"
android:label="${app_name}"
android:theme="@style/AppTheme"
>
外部から呼び出される Activity / Service / BroadcastReceiver のパッケージ名&クラス名
ほとんど前項と同じですが、Launcher Activity以外にもアプリの外部から呼び出される可能性のあるActivity / Service / BroadcastReceiver のパッケージ名、クラス名を変更してしまうと呼び出しができなくなり、作り方によっては呼び出し元がクラッシュしてしまったりします。
BroadcastReceiverは関係ないだろと思うかもしれませんが、BroadcastIntentも受信するBroadcastReceiverを指定して投げる明示的ブロードキャストというものがありますので侮れません。
(Oreo以降は明示的ブロードキャストしか使えないですし)
さて、外部とはなんでしょう?
- ホームアプリに作成するショートカット
- ホームアプリ依存ですが、任意のActivityを直接呼び出すショートカットを作成することができます。
- その他アプリから明示的Intentを投げられるもの
- 別で開発しているアプリとか、インターフェースを定義して外部から呼び出せるようにしているものなどがあり得るでしょう。(外部からの呼び出しはできる限り暗黙的Intentを使いたいところです)
- AppWidget / Alarm / Notification などのPendingIntentから呼び出されうるもの
- これはアプリ自身が作成するものですが、実際にPendingIntentからIntentを作成して投げているのは外部のアプリ(SystemUIとか、ホームアプリとか)ですので、アプリをアップデートしてからアプリがその情報を更新するまでの間は古い情報が利用される可能性があります。
といったものが考えられます。
AppWidgetProviderのパッケージ名&クラス名
こいつもBroadcastReceiverの一種なので前項と同じですが、ちょっと使われ方が特殊なので別項目にしました。
AppWidgetProviderはウィジェットを作成する場合に登録するものですが、貼り付けられたAppWidgetHost側は、どのウィジェットをどこに貼り付けているかの情報をAppWidgetProviderのパッケージ名&クラス名で識別しています。当然、この情報が保持されるのはアプリの外部になりますので、アップデートによってこの情報が変化してしまった場合、保持された情報からウィジェットを作成することができなくなります。
つまり、貼り付けられたウィジェットが表示できなくなる。または、ホームアプリから剥がされてしまうという事態に陥ります。
ウィジェットは元々ユーザ操作によってしか貼り付けることができず、設置までのハードルが高いものです。なのに、せっかくそのハードルを乗り越えた設置場所を自ら失うことになります。
アプリ間プロトコル関係
アプリ間通信プロトコル
これまで出てきたものも、これに集約されるでしょうが、アプリ間通信プロトコルは一度決めてしまうと変更は極めて困難です。使用しているアプリをすべてアップデートする必要があるので、旧バージョンを利用しているユーザがいなくなるまで下位バージョンのプロトコルに対応し続ける必要があります。アプリの浸透率が100%になるのは相当小規模なアプリであっても非現実的なので、変更してからも数年以上はサポートを続ける必要があるでしょう。
可能であれば一番最初のプロトコル作成時点で、互換性を持たせた拡張ができるように設計するとともに、互換性が保たなくなった場合に備え、プロトコルバージョンの交換などを行い、下位バージョンであった場合の対応(アップデート促すなど)をあらかじめ決めておくのがよいでしょう。
バージョンをまたいで利用されうるIntentに付与されたSerializable / Parcelable
結局これもプロトコルになるのですが、多くの人にとって想定以上の被害を発生させうる案件だと思うので別項目にしました。
バージョンをまたいで利用されうる。つまり、アプリ間の通信プロトコルとして使用するIntentや、PendingIntentとしてAlarm / AppWidget / Notificationなどに登録したIntentですが、このIntentにSerializableやParcelableを登録した場合、そのSerializableやParcelableは、使用しなくなったとしても、削除を含め、一度使用したバージョンのシリアライズデータがデシリアライズできなくなる変更を行うことができません。
Intentに付与されたExtraですが、これはIntent内部では一つのBundleで保持されています。
このBundleはそれ自体のインスタンスが作成された時点では内部情報のデシリアライズが行われておらず、何らかの値が読み書きされた時にすべてのデータのデシリアライズが行われます。
つまり、受け取ったIntentの中に、利用するしないにかかわらず、一つでもその時点のアプリでデシリアライズできないものが含まれている場合、どのExtraの値も読み出しができなくなってしまいます。Extraのこの値が○○だから無視しよう、とかもできなくなります。
データの変更であれば互換性を気にする場合もあるかと思いますが、一切使用しなくなったクラスを削除してはいけないとはなかなか気づかないでしょう。
基本的にはそういう属性のIntentには、(システムライブラリが提供しているもの以外の)Serializable / Parcelableを利用するのは控えるのがよいと思います。
使用する場合、プロトコルの変更が入ったときはAction名を変更するなどして、Extraに触る前に旧Intentを無視できるように慎重に設計する必要があるでしょう。
永続化データ関係
永続化データはバージョンアップしても残り続けるので、これも安易には変更できません。
当然マイグレーション処理を入れることで変更を加えることはできますが、旧バージョンのユーザがいなくなるまで、その処理を削除することができません。ユーザーが取り出せる場所に書き出すデータの場合は残り続けるのでいつかはサポート終了宣言などを行う必要などが出てくるでしょう。
そしてその宣言をしたところで、旧バージョンのデータが読み込まれた場合は最低限クラッシュなどせずに正常なエラー処理が行われるための処理は永遠に削除することはできないでしょう。
単純なファイル出力やSQLiteなどのDB、SharedPreferencesなども含めて、機能的に必要だから書き出す。ではなく、アプリの将来性まで考えた戦略を練る必要があります。
SharedPreferencesのKey
SharedPreferencesで陥りがちなのが、あるデータを保存しておいて、あるバージョンからその値は使用しなくなった。さらにその後のバージョンで、別の目的で同じKeyを使用してしまった。というパターンです。
読み出す型が違っていればClassCastExceptionにつながりますし、型があっていたとしても、違う意味の値を読み出すことになるため何らかの不具合につながる可能性が高くなります。
これは、その値を保存していたバージョンから使い続けている場合でのみ問題が発生するため、テストでは非常に見つけにくく、かつ、利用を続けている、すなわち利用ユーザの大部分で問題が起こるという非常に怖い問題につながります。
一度作ったKeyは削除せず残し続け、絶対に使い回さない、などの運用ルールが必要でしょう。KeyをEnumで管理すれば重複はコンパイラレベルでチェックできるのでおすすめです。