Multi-Versioning Android User Interfaces
どうして"最新の Android だけに対応する"ということができないのでしょうか?
円グラフを見ると、ICS 以降の部分は少なく、大部分のユーザーを取り込むには
Froyo + Gingerbread + ICS + Jelly Bean
に対応しないといけません
(個人的には、このグラフをみると 2.1 も対応してもいいような気もするけど、、、)
進化を完全に避ける開発者がいます。新しいパーツが用意されていてもそれを忘れて古いパーツを使い続けます。
アプリが一貫性を持つのは大事なことですが、その一貫性を理由にするのはナンセンスです(その他の場所ではその一貫性(古いパーツのこと)は廃止されているので)。
バージョン毎の APK を用意するのはどうでしょうか?
multiple APK は普遍的によくないアプローチだと言っているわけではありません。この機能は各バージョン毎の APK を持つために乱用されるべきではない、ということです。
なるべく1つの APK にした方がいいことがわかったが、レイアウトやリソースをバージョン毎に分ける開発者がいます。
バージョン毎にレイアウトやコードを分けるのはクレイジーな方法です。我々はこれを推奨しません。
(メジャーなバージション毎にレイアウトやコードを分けるのはいいですが、全部に対してやる、やりすぎるのはよくないという意味)
■ Parallel Activity Pattern
基本的な multiversioning skills として parallel activity と呼ばれる非常に一般的な方法があります。
Intent i = null;
if(android.os.Build.VERSION>SDK_INT >=
android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
i = new Intent(this, ShinyCoolActivity.class);
}
else {
i = new Intent(this, LegacyAnctivity.class);
}
プラットフォームのバージョンによって起動する Activity を変える簡単でシンプルな方です。
これは Activity が本当に本当に多くの新しいプラットフォームの機能に依存していて、他のバージョン用のまともなフォールバック(代替)を持ちたい場合にはいい方法です。
ICS 以前・以後のようなメジャーなプラットフォームの境界をまたぐ必要がある場合のみ使うべきです。なぜなら、あまりにも多くの分岐があると急速に複雑になるからです。
複雑さについて話をすると、ときどき開発者は特定の機能を複数のプラットフォームバージョンに実装するために別々のクラスに書きますが、ほとんど同じコードで異なるのはほんの数行だったりします。
こういう状況では、あなたのコードと正気を同時に維持するのは不可能です。選択しなければなりません。
これはいいアイディアではありません。
if を使うのは恥ずかしいことではありません。if は素晴らしいです。
これらのプラットフォームバージョンの定数が古いプラットフォームバージョンで壊れる(なぜならその時にはその定数はなかったから)ことを心配していますか? その通り。これは確かに心配することの1つです。
しかしこの場合は問題ありません。なぜならこれらはコンパイル時に純粋な数字に置き換わるからです。
1つめのアドバイスは if を使うことを恐れないこと。2つめのアドバイスは if を恐れることです。
これらのコードの分岐があまりにも大きくなったら、よりよいアプローチが必要です。
■ lazy
lazy loading をつかいます。
数年前に Android Developer's blog にこのアイディアの詳細を書きました(
How to have your (Cup)cake and eat it too)。
Abstraction and lazy loading
・abstract class
・implementations
・load when needed
まず、アプリケーションが行いたいことを記述する abstract class もしくは interface を定義するところから始めます。
つぎに、新しい機能が使えるようになったプラットフォームバージョン毎にそのインタフェースの実装を書きます。
シンプルなファクトリーメソッドや、それに似たメカニズムで、プラットフォームバージョンにあった正しい実装を取得するようにします。こうすると、アプリの中で読んでいるコードはバージョンの違いを考慮しなくてすむようになります。
public abstract class VersionedLoremIpsum {
public abstract String doLorem();
public abstract int doIpsum();
}
public class EclairLoremIpsum extends VersionedLoremIpsum {
public String doLorem() {
// do lorem, Eclair-style
}
public abstract int doIpsum() {
// deliver ipsum, a la Eclair
}
}
古いバージョン用のフォールバックでは、合理的にエミュレートやバックポートできない機能を落とすことがあります。また、合理的にできることがない場合、これらの互換性実装がただのスタブや no ops になってもいいでしょう。
public class FroyoLoremIpsum extends EclairLoremIpsum {
public string doLorem() {
String l = super.doLorem();
// additional processing;
return l;
}
public abstract int doIpsum() {
...
新しいバージョンのプラットフォームでは、ユーザー体験を拡張する付加的な機能を加えることができます。ここでは super class の実装を呼んでいます。実装の重複を減らして、ベースの振る舞いに付加機能を重ねるのにいい方法です(特に、新しいものに全体を入れ替える場合でないのなら)。
VersionedLoremIpsum li;
int sdk = Build.VERSION.SDK_INT;
if (sdk <= Build.VERSION_CODES.ECLAIR) {
li = new EclairLoremIpsum();
}
else if (sdk <= Build.VERSION_CODES.FROYO) {
li = new FroyoLoremIpsum();
}
else {
li = new GingerbreadLoremIpsum();
}
簡潔なバージョンチェックで正しいバージョンのインスタンスを生成することができ、残りのコードはバージョンの違いを無視することができます。
Android Support library はまさにこれと同じ戦術を使っています。
これらの、非常に焦点をあてている特別な場合(利用できる場合に、より高度な機能を重ねる場合)には、このセッションで紹介した multiversioning のアプローチを使って提供します。
もし、これらをテクニックをアプリのより小さく独立したコンポーネントに適用したい場合でも、多くの重複を含む大きな包括的アプローチを使うよりはずっと簡単に利用できます。
■ resource system
-vN リソース識別子を使う。
res/layout-v11/foo.xml
res/layout/foo.xm
layouts, drawables, styles などをプラットフォームバージョンの応じて切り替えますが、これを使うには加減が必要です。やり過ぎると多くの XML の重複が起こります。これはコードの重複と同じように悪いことです。
これを避ける戦略はなにかあるでしょうか?
最初の戦略は boolean リソースを使うことです。
res/values-v14/bools.xml
true
false
values-v14 なので、ICS 以降はこのリソースを使います。
res/values/bools.xml
false
true
残り、つまり ICS 未満ではこのリソースを使います。
次のようのコードをの切り替えにも使えますが、プラットフォームバージョンに応じてコンポーネントの有効・無効を切り替えるのに使うことができます。
Resources res = getResources();
boolean postICS = res.getBoolean(R.bool.postICS);
if(postICS) {
// do something cool and cutting-edge
}
else {
// do something old-school but elegant!
}
例えば AppWidget に使えます。AppWidget は基本的には特別なデータを持っているブロードキャストレシーバーです。
<receiver android:name="MyAppWidget">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/my_appwidget_info" />
/>
このように書くと、全てのプラットフォームバージョンで同じ見た目、同じ振るまいになります。
しかしバージョンによって異なるものを提供したい場合はどうしたらいいでしょう?ここで boolean リソースを使います。
<receiver android:name="MyPreICSAppWidget"
android:enabled="@bool/preICS">
<intent-filter ... />
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/my_pre_ics_info" />
/>
<receiver android:name="MyPostICSAppWidget"
android:enabled="@bool/postICS">
<intent-filter ... />
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/my_post_ics_info" />
/>
デバイスで動いているプラットフォームのバージョンによって自動的にこれらのブロードキャストレシーバーの有効・無効が切り替わります。
■ Activity UI fallbacks
ICS 以降では Switch を使い、それ以前では Checkbox を使いたい場合はどうすればいいでしょう?
res/layout/main.xml
include を使ってバージョンに依存する部分だけ分割するようにします。
res/layout/desserts.xml
<merge ...>
<CheckBox .../>
<CheckBox .../>
<CheckBox .../>
...
</merge>
res/layout-v14/desserts.xml
<merge ...>
<Switch .../>
<Switch .../>
<Switch .../>
...
</merge>
(セッションのスライドでは layout-v11 となっているが、
Switch は API Level 14 から)
CheckBox も Switch も CompoundButton を継承しているので、次のように単一のコードで対応できます。
CompoundButton cb = (CompoundButton) findViewById(R.id.myoption);
checked = cb.isChecked();
include は重要なテクニックです。プラットフォームのバージョンだけでなく画面サイズなどいろいろな場合に使えます。
■ Multiversioning Themes
値をハードコーディングしないこと。文字サイズ、余白などなど。
× android:textSize="20sp"
○ android:textAppearance="?android:attr/textAppearanceLarge"
これでもいいですが、テーマを使えばデフォルトの値を設定することができます。
テーマでは元のスタイルを継承しましょう。
× <style name="MyButtonStyle">
○ <style name="MyButtonStyle" parent="@android:style/Widget.Holo.Button">
問題は Holo と DeviceDefault テーマは Honeycomb 以前には存在しないということです。
次のように ICS 以前と以後で見た目を変えたい場合、テーマの継承先を変えます。
res/values/styles.xml
res/values-v11/styles.xml
これだと各コンポーネントを定義しないといけなくなります。よりよい方法は、ベーステーマで継承先を変えることです。
res/values/styles.xml
res/values-v11/styles.xml
res/values/styles.xml
■ creating your own theme attributes
以前のバージョンでは存在しない属性を利用したい場合、独自の属性を使って切り替えるようにします。
res/values/attrs.xml
res/values/themes.xml
- @drawable/fallback_item_background
res/values-v11/themes.xml
- ?android:attr/selectableItemBackground
どのテーマを継承すべきでしょうか?
・Holo - stable/easy
・Device Default - tighter integration
・Activity Content: holo-ish
■ Support Library and Beyond
App Compat は support library に新しく追加される部分です。既存の v4 と v13 のコンポーネントに追加されます。
これまでにリリースしている support library はあなたのアプリを multiversion 対応にするための abstract tools を提供しています。
App Compat のエレメントは、複数のプラットフォームにまたがって Android Design ガイドラインに沿う手助けをするようになっています。
support library を使うと ICS 以前でも Fragment を使うことができます。
Fragment を継承したクラスを作る場合や、レイアウト XML で Fragment を定義する方法は Honeycomb 以降と support library で同じです。
ただし、Fragment を使う Activity は注意が必要です。
support library では Activity ではなく、FragmentActivity を継承する必要があります。
■ Notification
Notification は新しいバージョンではさまざまな機能が追加されています。
Honeycomb 以前は直接パラメータをセットし、manager にセットしていました。
Notification notif = new Notification(icon, tickerText, when);
notif.setLatestEventInfo(context, contentTitle, contentText, contentIntent);
mNotificationManager.notify(MY_NOTIF_ID, notif);
Honeycomb 以降は、より複雑になっていて、Notification.Builder を使って作成します。
Notification notif = new Notification.Builder(this)
.setSmallIcon(R.drawable.ic_stat_notify_example)
.setAutoCancel(true)
.setTicker(getString(R.string.notification_text))
.setContentIntent(myContentIntent)
.setNumber(7)
.setContentTitle(getString(R.string.app_name))
.setContentText(getString(R.string.notif_text))
.getNotification();
support library の NotificationCompat.Builder を使えば Honeycomb 以前でも Notification.Buidler を使うことができます。
android.support.v4.app.NotificationCompat.Builder
しかし JellyBean についてはどうでしょうか?
JellyBean では Notification に多くの機能が追加されています。ボタンが追加できるようになっていたり、スタイルがセットできるようになっています。
builder.setStyle(style);
・Notification.BigTextStyle
・Notification.InboxStyle
・Notification.BigPictureStyle
他にも
・setPriority
・setUsesChronometer
・setSubText
・etc.
残念ながら、Jelly Bean の Notification 機能はまだ support library に入っていません。よって Jelly Bean のいいところを使いつつ、後方互換性も確保するなら、次のようにバージョンに応じて使い分ける必要があります。
・if version >= JellyBean
・use Notification.Builder
・else
・use NotificationCompat.Builder
Notification の他の重要なポイントとしては、アイコンがあります。
Foryo 以前は黒い背景に白でした、Gingerbread では灰色、Honeycomb 以降では白です。
Android Asset Studio (
j.mp/androidassetstudio)を使うのがいい方法です。
■ Action Bar
Acition Bar は2つの部分からなります。1つめが Navigation、ここは通常タブやスピナー、up ボタンなどです。2つめ Actions です。Actions は action ボタンに分かれます。これらはバーの上に常に表示されており、action overflow は右側の3つのドットボタンの後ろに隠れています。
デザインガイドラインの FIT ルールを思い出してください。frequent(頻繁に使う)、important(重要)、typical(典型的な機能)なものは画面上に表示するべきです。これに当てはまらない場合は常に、画面上の貴重な場所を取らない、どこか他の場所に挟み込めないか考えてください。overflow はそのための場所の1つです。
Honeycomb 以前では ActionBar を使うことができません。
自分でそのバージョン用のを実装するときのいいスタートポイントは action bar の compat sample です。
Action Bar Sherlock のような third-party library を使うこともできます。
いいニュースがあります。まもなく action bar を Honeycomb 以前でも作れるようになる AppCompat library が使えるようになります。support library で Fragment を使うのと同じくらい簡単になります。
FragmentActivity と同じような方法で使います。ActionBarActivity は FragmentActivity を継承しており、これらの間のコンフリクトなどを心配する必要はありません。
Action Bar は App Compat library のパーツとして利用できるようにしようと我々が計画しているものの始まりの1つにすぎません。
・Styles and themes
・Common layouts
・Extra theme attributes to query
我々が今日話したことは便利ですが、心からコードの重複を望んでいる人はいません。これらをまとめて、単にアプリに追加して利用できるようにしたいのです。
■ Miscellaneous tips
Option panel を変えない
Option panel はシステムで一貫して使われるものなので、スタイルが異なるとユーザーが混乱してしまいます。
システムが用意したスタイルに合うアセットを常に提供してください。
Options menu は Action Overflow です。
ほとんどの新しいデバイスは Menu キーがありません。すべての action は FIT ルールに沿うかレイアウト上においてください。それ以外はプラットフォームのバージョンに応じて、 options panel か action overflow に置いてください。
アンカーとなるエレメントについてです。ダイアログはその1つです。これはシステムレベルのインタラクションを示しています。クリティカルな yes/no などです。これらを使う場合は控えめにしてください。ポップアップはすごくうるさいものです。ダイアログがユーザー体験の中心部分になってはいけません。プラットフォームに応じて適用されるデフォルトのスタイルを変えないでください。
特定の理由によってダイアログのスタイルを変える誘惑に逆らえなかった場合、本来ダイアログですべきではないことをダイアログで達成しようとしていることを示しているのかもしれません。
それは実際は Activiy の一部であったり、別の Activity であるべきものでしょう。つまり、ダイアログのスタイルを変えないでくださいということです。
ICS 以前と以後で positive ボタンと negative ボタンの順番が変わっています。
後方互換性を保つためには、プラットフォームでのボタンの順番と同じになるように気をつけてください。
これらのボタンの順番の問題は、App Compat library で我々が提供したいと思っているものの1つです。
システムが提供しているダイアログとカスタムレイアウトのダイアログを使っているアプリなどでは、アプリ内で一貫性が保たれていないことがあります。
■ set min and target SDK versions
・android:minSdkVersion
・android:targetSdkVersion
常に minSdkVersion と targetSdkVersion を指定するようにしてください。minSdkVersion にはアプリが動く最小のバージョンを指定してください。
targetSdkVersion は指定するのを忘れる人や、古すぎるバージョンを指定する人が多いです。しかし target SDK はアプリが動作するのに必要な 最小の SDK には影響しません。targetSdkVersion にはアプリが動くことをテスト済みの最新のバージョンを指定してください。つまり今なら JellyBean(16)です。
システムは targetSdkVersion に応じて多くの互換性パーツの有効・無効を切り替えるからです。例えばデフォルトのテーマやメニューキーなどに影響します。