Android SDKのソースコードを読みながら開発する
この記事はAndroid Advent Calendar 2014の14日目です。
Androidアプリケーション開発をiOSのそれと比べると、SDKのソースコードが公開されていることがアドバンテージの一つになると思います。自分は半年ほど前から、開発時に時々SDKのソースコードを参照するようになり、それからSDKへの理解が深まって、開発効率が高まったと感じています。
この記事では、自分がSDKのソースコードを読む際に使っている方法をまとめます。たぶんよく知られている方法ばかりです。
1. ブラウザで見る
GrepCode
特定のクラス名でぐぐっていたりすると、GrepCode というサイトが時々引っかかります。Javaのソースコードを集めて検索可能にしてくれているサイトですが、ちょっとSDKのコードを読みたいというときは、このサイトで読むのがおすすめです。
Android SDKの各バージョンが置かれているので大変便利。SDKに同梱されているSupport Libraryも置いてあります(例えば新しく追加されたToolbarとか)。クラス名がリンクになっていて他のクラスにすぐ飛べるので、コードの流れを追いたいときにも便利です。コードレビュー時にリンクを貼る時にも重宝します。
ただし最新のSDKの反映は遅めなので、その点だけ注意が必要です(先週ようやく5.0のコードが反映されました)。
GitHub
オリジナルのgoogle codeのリポジトリをGitHubにミラーしたリポジトリがあります。最新のソースをWebで見たい場合は、こちらの方が便利です。GitHubのソースブラウザや検索が使えるというメリットもあります。こちらはコード名のリンクのような機能はありません。
2. SDK Managerでダウンロードして見る
SDK Managerを起動して、各バージョンのソースをダウンロードできます。ソースは$ANDROID_HOME/sources/android-21/support/
のようなパスに保存されます。落としたソースは自動的にリンクされ、Andrdoid Studioを使っていればCommand+クリックでジャンプできるようになります*1。開発に使っているSDKと対応するものは、入れておいて損はないと思います。
Support Libraryのソースも自動的に入ります($ANDROID_HOME/sources/android-21/support/
)。しかしここにあるのはSDKリリース時点のもので、Support Library単体のアップデートには追従していないようです。そこで最新のSupport Libraryを読むには次の方法を使っています。
3. gitでcloneして見る
google codeからSDKやSupport Libraryのソースがgit cloneできます*2。恐らくこちらにあるソースが、アプリケーション開発者が手に入れられる一番最新のものだと思います*3。そのままcloneするとかなり時間がかかるので注意。 リリースバージョンにはtagが振られているので、特定のバージョンのソースを見たければtagをcheckoutする必要があります。
gitのローカルリポジトリになるのでgit grepでコードを追うことができます。自分はSDKが提供しているパーツがSDK内でどう使われているかを探す場合によく使います。(たとえばIntentのflagの使われ方の例を探す、とか)
2の方法で手に入れたソースをgit initしたり、何らかのindexを作っておくだけでも検索はできるので、grep用途だけならこの方法は必要ないかもしれません。ただ自分はghq+percol/pecoでリポジトリを横断するフローに慣れてしまったため、他のgithubプロジェクトと同じ方法でソースにアクセスできるこの方法をよく使っています。
おわりに
色々書いてみましたが、自分が一番主張したいのは、Android SDKのソースを読みながら開発すると捗る!ということです。自分としては、これはもっと早くに知っておけば良かったと思っています*4
先日勉強会で、他社のAndroidエンジニアさんたちに「SDKのソース読んでますか?」と聞いてみたところ、話したほとんどのエンジニアは多かれ少なかれSDKのコードを読んだ経験を持っていました*5。その席で聞いてみたところでは、よく読むものとしてはFragment、Activity、Viewなどが人気があるようでした。
自分の経験では、Support LibraryのFragmentとViewPagerは読みづらいけど役に立つ鉄板、PagerTitleStripはカスタムViewの勉強になりました。
Android SDKのコードベースはとても大きく、知らないクラスを読む度新たな発見があります。SDKの語りかけに耳を澄ませて、彼の気持ちを理解するのです。そのとき、SDKは大いなる力、新たな可能性をあなた開示することでしょう。
という感じで雑に締めたいと思います。
*1:自分の環境ではたまにSDKのディレクトリへのリンクが切れることがあるので、その都度Attache Sources…で指定し直しています
*2:他にも多数のリポジトリがありますが、NDKを使わない自分の用途としては、ほぼplatform/frameworks/baseとplatform/frameworks/supportだけで足りています
*3:SDK MangerでSDKやSupport Libraryのリリースがあっても、その直後にはまだ反映されていなかったりすることがあるので、その場合はじっと待つしかないです。
*4:しかしこれは自分がiOS脳だったので思いつかなかっただけで、OSSなプラットフォームならフレームワークを読むのは当たり前のことではあります
*5:ぐぐったりして見たことがある人はほぼ全員、積極的に読むという人もそれなりに居る様子
【翻訳】Android Fragmentへの反対声明
- Original: Advocating Against Android Fragments
- by @Piwai
- Translated by @ninjinkun
- Reviewed by @hotchemi
最近私はDroidcon Parisでテックトーク(フランス語)を行い、SquareがAndroidのFragmentを利用して直面した問題と、Fragmentを避ける方法について説明した。
2011年に我々は以下の理由でFragmentを使う決断をした。
- この時点で我々はタブレットをサポートしていなかった。しかしいつかは対応することがわかっていた。FragmentトはレスポンシブなUIを作るのを助けてくれる。
- Fragmentはビューコントローラーだ。ビジネスロジックを単位ごとに分離してテスト可能にしてくれる。
- FragmentのAPIはバックスタックのマネジメントを提供してくれる(すなわち一つのActivityの中でActivityスタックの挙動を再現してくれる)
- GoogleがFragmentを推奨していた。我々もスタンダードに従ったコードにしたかった。
この2011年以来、我々はSquareにとってより良い選択肢を発見した。
両親がFragmentについて教えてくれなかったこと
The lolcycle
AndroidではContext
は神オブジェクトだ。そしてActivity
はライフサイクルを持っているContextだ。ライフサイクルを持つ神?アイロニックだ。Fragmentは神ではない。しかし非常に複雑なライフサイクルを持っている。
Steve Pomeryは完全なライフサイクルのダイアグラムを作った。このライフサイクルはわかりにくいだろう。
Created by Steve Pomeroy, modified to remove the activity lifecycle, shared under the CC BY-SA 4.0 license.
このライフサイクルは、それぞれのコールバックで何をするべきかを分かりづらくする。同期的に呼ばれるのか、一度にまとめてか?どんな順番で?
デバッグの難しさ
アプリの中でバグが起こったとき、何が起こったかを正確に理解するために、デバッガを使ってコードをステップバイステップで実行するだろう。これはたいていうまく行く…FragmentManagerImpl
に当たるまでは。地雷だ!
このコードは追いづらくデバッグしづらい。これはバグをきちんと直すのを難しくする。
switch (f.mState) { case Fragment.INITIALIZING: if (f.mSavedFragmentState != null) { f.mSavedViewState = f.mSavedFragmentState.getSparseParcelableArray( FragmentManagerImpl.VIEW_STATE_TAG); f.mTarget = getFragment(f.mSavedFragmentState, FragmentManagerImpl.TARGET_STATE_TAG); if (f.mTarget != null) { f.mTargetRequestCode = f.mSavedFragmentState.getInt( FragmentManagerImpl.TARGET_REQUEST_CODE_STATE_TAG, 0); } f.mUserVisibleHint = f.mSavedFragmentState.getBoolean( FragmentManagerImpl.USER_VISIBLE_HINT_TAG, true); if (!f.mUserVisibleHint) { f.mDeferStart = true; if (newState > Fragment.STOPPED) { newState = Fragment.STOPPED; } } } // ... }
回転の際にFragmentがunattachされたのを見たことがあるなら、私の言っていることがわかるだろう。(そして私にFragmentのネストの話を始めさせないでくれ)
Coding Horrorにあるように、私も今このマンガにリンクされた法則を要求しよう。
コードの品質を正しく計測する唯一の方法: このクソは何だ? / 分
数年に渡る綿密な分析の後、私はWTFs/min = 2^fragment count
という結論を得た。
ビューコントローラー?まあちょっと待て
ビューの作成、バインド、構成などにより、Fragmentはビューに関連したたくさんのコードを含んでいる。これは事実上ビジネスロジックがビューのコードから切り離されていないことを意味している。この事がFragmentに逆らってユニットテストを書くのを難しくしている。
Fragment トランザクション
Fragmentトランザクションは複数のFragmentの操作を1つの集合として扱えるようにしてくれる。残念ながらトランザクションのコミットは非同期で、メインスレッドのハンドラキューの最後に詰まれる。これは複数のクリックを受け付ける時や、設定の変更中にアプリを未知の状態にしてしまう。
class BackStackRecord extends FragmentTransaction { int commitInternal(boolean allowStateLoss) { if (mCommitted) throw new IllegalStateException("commit already called"); mCommitted = true; if (mAddToBackStack) { mIndex = mManager.allocBackStackIndex(this); } else { mIndex = -1; } mManager.enqueueAction(this, allowStateLoss); return mIndex; } }
Fragmentを生成する魔法
Fragmentのインスタンスは、あなたかFragment Managerによって作られる。このコードはとてもわかりやすい。
DialogFragment dialogFragment = new DialogFragment() { @Override public Dialog onCreateDialog(Bundle savedInstanceState) { ... } }; dialogFragment.show(fragmentManager, tag);
しかしながら、ActivityのInstance Stateが復元されるとき、Fragment Managerはリクレクションを使ってFragmentクラスのインスタンスを再生成しようとする。このDialogFragmentは匿名クラスなので、外側クラスの参照を見えないコンストラクタの引数として持つ。(訳注: 非staticな匿名クラスは外側クラスへの参照を持つ[1][2]。リフレクションで再生成するとこの外側クラスが無くなっているのでクラッシュする)
android.support.v4.app.Fragment$InstantiationException: Unable to instantiate fragment com.squareup.MyActivity$1: make sure class name exists, is public, and has an empty constructor that is public
Fragmentから学んだこと
その欠点にも関わらず、Fragmentはとても有益な授業を教えてくれた。それは今我々がアプリを書く際に再適用できるものだ。
- 単一のActivityインターフェイス: 画面毎に1つのActivityを使う必要はない。我々はアプリをウィジェットごとに分け、好きなように組み合わせることができる。これはアニメーションとライフサイクルを簡単にする。我々はウィジェットをビューのコードとコントローラーのコードに分割することができる。
- バックスタックはActivity特有の概念ではない。バックスタックは一つのActivityの中にも実装できる。
- 新しいAPIは必要ない。必要なものは全て初めからあった。Activity、ビュー、そしてLayout Infraterだ。
レスポンシブUI: Fragment vs カスタムビュー
Fragment
Fragmentの基本的な例を見てみよう。これはリストと詳細画面のUIを持っている。
HeadlinesFragment
はとても素直に書かれたリストだ。
public class HeadlinesFragment extends ListFragment { OnHeadlineSelectedListener mCallback; public interface OnHeadlineSelectedListener { public void onArticleSelected(int position); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setListAdapter( new ArrayAdapter<String>(getActivity(), R.layout.fragment_list, Ipsum.Headlines)); } @Override public void onAttach(Activity activity) { super.onAttach(activity); mCallback = (OnHeadlineSelectedListener) activity; } @Override public void onListItemClick(ListView l, View v, int position, long id) { mCallback.onArticleSelected(position); getListView().setItemChecked(position, true); } }
これは興味深い。ListFragmentActivity
は 詳細画面が同じスクリーンにあるかどうかをハンドリングしている。
public class ListFragmentActivity extends Activity implements HeadlinesFragment.OnHeadlineSelectedListener { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.news_articles); if (findViewById(R.id.fragment_container) != null) { if (savedInstanceState != null) { return; } HeadlinesFragment firstFragment = new HeadlinesFragment(); firstFragment.setArguments(getIntent().getExtras()); getFragmentManager() .beginTransaction() .add(R.id.fragment_container, firstFragment) .commit(); } } public void onArticleSelected(int position) { ArticleFragment articleFrag = (ArticleFragment) getFragmentManager() .findFragmentById(R.id.article_fragment); if (articleFrag != null) { articleFrag.updateArticleView(position); } else { ArticleFragment newFragment = new ArticleFragment(); Bundle args = new Bundle(); args.putInt(ArticleFragment.ARG_POSITION, position); newFragment.setArguments(args); getFragmentManager() .beginTransaction() .replace(R.id.fragment_container, newFragment) .addToBackStack(null) .commit(); } } }
カスタムビュー
ビューだけを使って同じものを実装してみよう
まず、Container
という概念を導入する。これは要素を表示して、バック操作をハンドリングするものだ。
Activityは一つのContainerと複数のただのDelegateだけが存在することを前提とする。
public class MainActivity extends Activity { private Container container; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main_activity); container = (Container) findViewById(R.id.container); } public Container getContainer() { return container; } @Override public void onBackPressed() { boolean handled = container.onBackPressed(); if (!handled) { finish(); } } }
リストはかなり短くなる。
public class ItemListView extends ListView { public ItemListView(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onFinishInflate() { super.onFinishInflate(); final MyListAdapter adapter = new MyListAdapter(); setAdapter(adapter); setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { String item = adapter.getItem(position); MainActivity activity = (MainActivity) getContext(); Container container = activity.getContainer(); container.showItem(item); } }); } }
この実装の肝は、リソース修飾子に基づいて、異なるXMLレイアウトをロードすることだ。
res/layout/main_activity.xml
<com.squareup.view.SinglePaneContainer xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/container" > <com.squareup.view.ItemListView android:layout_width="match_parent" android:layout_height="match_parent" /> </com.squareup.view.SinglePaneContainer>
res/layout-land/main_activity.xml
<com.squareup.view.DualPaneContainer xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" android:id="@+id/container" > <com.squareup.view.ItemListView android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="0.2" /> <include layout="@layout/detail" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="0.8" /> </com.squareup.view.DualPaneContainer>
ここに、これらのContainerのとてもシンプルな実装がある。
public class DualPaneContainer extends LinearLayout implements Container { private MyDetailView detailView; public DualPaneContainer(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onFinishInflate() { super.onFinishInflate(); detailView = (MyDetailView) getChildAt(1); } public boolean onBackPressed() { return false; } @Override public void showItem(String item) { detailView.setItem(item); } }
public class SinglePaneContainer extends FrameLayout implements Container { private ItemListView listView; public SinglePaneContainer(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onFinishInflate() { super.onFinishInflate(); listView = (ItemListView) getChildAt(0); } public boolean onBackPressed() { if (!listViewAttached()) { removeViewAt(0); addView(listView); return true; } return false; } @Override public void showItem(String item) { if (listViewAttached()) { removeViewAt(0); View.inflate(getContext(), R.layout.detail, this); } MyDetailView detailView = (MyDetailView) getChildAt(0); detailView.setItem(item); } private boolean listViewAttached() { return listView.getParent() != null; } }
これらのContainerを抽象化して、この方法でアプリを作るのを想像するのは難しくない。Fragmentが不要になるだけでなく、コードがより理解しやすくなるのだ。
ビューとプレゼンター
カスタムビューは良い感じに動く。しかし我々はビジネスロジックとコントローラーの分離にも熱意を注ぐべきだ。これをプレゼンターと呼ぶ。これはコードを読みやすく、テストし易くする。前の例にあったMyDetailView
は以下の様になる。
public class MyDetailView extends LinearLayout { TextView textView; DetailPresenter presenter; public MyDetailView(Context context, AttributeSet attrs) { super(context, attrs); presenter = new DetailPresenter(); } @Override protected void onFinishInflate() { super.onFinishInflate(); presenter.setView(this); textView = (TextView) findViewById(R.id.text); findViewById(R.id.button).setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { presenter.buttonClicked(); } }); } public void setItem(String item) { textView.setText(item); } }
Square Registerのコードから、割引の設定を編集する画面を見てみよう。
プレゼンターはビューを上位から操作する。
class EditDiscountPresenter { // ... public void saveDiscount() { EditDiscountView view = getView(); String name = view.getName(); if (isBlank(name)) { view.showNameRequiredWarning(); return; } if (isNewDiscount()) { createNewDiscountAsync(name, view.getAmount(), view.isPercentage()); } else { updateNewDiscountAsync(discountId, name, view.getAmount(), view.isPercentage()); } close(); } }
プレゼンターのテストを書くのは楽勝だ。
@Test public void cannot_save_discount_with_empty_name() { startEditingLoadedPercentageDiscount(); when(view.getName()).thenReturn(""); presenter.saveDiscount(); verify(view).showNameRequiredWarning(); assertThat(isSavingInBackground()).isFalse(); }
バックスタックの管理
バックスタックを管理するのに非同期なトランザクションは必要ない。我々はFlowという小さなライブラリをリリースした。Ray Ryanが既にFlowについて素晴らしいブログ記事を書いてくれている。
私はFragmentスパゲッティコードの深みの中にいる。どうすれば脱出できる?
Fragmentを抜け殻にしてしまおう。Fragmentのビューのコードを取り出して、カスタムビュークラスを作るのだ。そしてビジネスロジックをカスタムビューとのやりとりを知っているプレゼンターに押し込もう。するとFraments空っぽに近くなる。カスタムビューをinflateして、自身をプレゼンターと繋ぐ。
public class DetailFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.my_detail_view, container, false); } }
ここまで来れば、Fragmentを削除できる。
Fragmentから逃れることは容易ではない。しかし我々はやり切った。 Dimitris KoutsogiorgasとRay Ryanの素晴らしい働きに感謝する。
DaggerとMortarについては?
DaggerとMortarはFragmentsと直行している。Fragmentsと一緒でも、一緒でなくても使う事ができる。
Daggerはアプリをモジュール化して、コンポーネントを分離したグラフにするのを助けてくれる。その結果、依存を抜き出して、1つのことに集中したオブジェクトを書くことが、簡単にできる。
MotarはDaggerの上で動き、2つの大きな利点を持つ。
- Injectされたコンポーネントにシンプルなライフサイクルコールバックを提供する。これにより、回転した時に破棄されなかったり、プロセスが死んでも状態が保存されて生き残る、シングルトンプレゼンターを書くことができる。
- Daggerのサブグラフを管理し、アクティビティのライフサイクルと紐付けることができる。これにより効率的にスコープの概念を実装できる。ビューが表示され、プレゼンターと依存がサブグラフとして作られる。ビューが消えると、簡単にスコープを破棄してガベージコレクターを動かす事ができる。
終わりに
我々はFragmentを徹底的に使い、最終的に考えを変えた。
- 我々にとって解明が困難だったクラッシュのうち、最も多かったのがFragmentのライフサイクルに関連したものだった
- 我々が必要としていたのは、レスポンシブなUIを構築するためのビューとバックスタック、スクリーン間のトランジションだけだった
訳注
親知らず
30分くらいずっとガリガリやられて、最後の方は痛くなって、生きた心地がしなかった。昨日のうちにPull Request出しておいて良かったと思った。
愛・地球博 #地元発見伝
愛知万博のために道路が拡張されて、地元の風景は変わってしまった。友だちの家のお店も移転した。万博許さん。
「記憶に残る風景」 #地元発見伝
Hirokoji Dori, Nagoya, Aichi Prefecture
地元の魅力を発見しよう!特別企画「地元発見伝」
失敗するプレゼンの夢
勉強会で発表する予定だが、資料が一枚もできていない。アプリのUIの話で、ボタンの角丸の部分を作る際の問題点について話す予定だった。とりあえず話は決まっているし、ライブでなんとかなるだろうと本番を向かえる。
アプリを見せながら、「ここに角丸がありますね」と言って話を続けようとする。しかし特に問題点が見つからない。何が問題だったのだろうと考えながら、冷や汗が流れ続ける。
YAPC::Asia 2014でモバイルアプリ開発について発表しました #yapcasia
もう先週のことになりますが、YAPC::Asia 2014でモバイルアプリ開発について発表しました。YAPCはエンジニアとして仕事を始めてからずっと憧れだったので、初めて登壇できてとても嬉しかったです。
自分の発表15分、gfxさん15分、二人でディスカッション10分という、ちょっとイレギュラーな構成の発表でした。その辺りの経緯を以下に書きます。
コンセプト
お客さんはサーバーサイドのエンジニアの方が多いと予想していたので、モバイル開発をするエンジニアが普段考えていることを知ってもらうことをゴールにしました。この発表の内容は、バックエンドのエンジニアが直接使える知識ではないかもしれません。しかしモバイルエンジニアと関わる仕事をしているなら、彼らがどう考えているかを知っておくことは、どこかで役に立つのではないかと思います。このため、モバイルエンジニアである自分たちが今現場で直面していて、面白いと思っている問題を共有しようというのが基本コンセプトでした。
gfxさんとのコラボレーション
自分もgfxさんモバイル開発に携わっていますが、自分はUI寄りで、gfxさんはミドル領域というかDeveloper Productivity寄りということで、やっていることは少し違います。この対比が面白いと思ったので、あまり耳慣れない Client-Fontend Engineering、 Cliend-Backend Engineering という言葉を使って、それぞれ違う話をしました。打ち合わせでgfxさんとお話ししていて、モバイル開発が拡大して、ミドル領域の仕事が生まれている現状がとても面白いと感じたので、それをお伝えしたくてこのトーク構成になりました。
モバイル開発の中でも、自分にとっては苦手な分野だったリリース作業やテストをGood Problemと捉えるgfxさんの姿勢を見ていると、UI大好きおじさん以外のエンジニアもモバイル開発にもっと入ってきて欲しいと強く思います。
初めはなんとなくの流れで決まったコラボレーションでしたが、gfxさんと組めて良かったです。自分としても、今後の議論の足がかりができたと思っています。
おわりに・YAPCの感想
自分が聞いていた中では、cho45のトークが面白かったです。あとはあらたまさんのモバイルAPIのトークは今月のWeb+DB Pressのzigorouさんの記事と(たぶん)繋がっていて、記事をAPI側の話、トークをアプリ側の話として聞くと倍楽しいと思います。GitHubのDelcambreさんによる、gitとの連携をどうやって進化させてきたかの話も面白かったです。
今回発表してみて、改めてYAPCの幅の広さと面白さを実感しました。自分は今はPerlを書いていない人間なのですが、それでも楽しめるし、受け入れてもらえたと思えました。HUBでだらだらビールを飲みながら、エンジニアというのは集まって技術の話をするのがどうしようもなく好きな人種であるなあ、というのを再認識した次第です。足を運んでくださった方々、運営スタッフの皆さま、そしてgfxさん、ありがとうございました。
Scala Matsuri
Scalaを作ったMartin Odersky先生が来られるというので、生Odersk先生目当てでScala Matsuriに出かけた。特にScalaを仕事で書く機会があるわけでないけれど、CourseraでScalaの講義を受講していたとき、毎回Odersky先生のビデオを見ていたので、近くで見られるなら一度見てみたいと思った次第。
Scalaの歴史を全然知らなかったので、Pizzaという言語があったことや、Scala拡張期に入れた機能を今は制限しようとしているとか、そういう話が聞けて楽しかった。急に流行ってしまうと、その後の進化のハンドリングは難しくなるだろうと感じた(当たり前だけど)。
Twitterの丹羽さんの発表の中でバルス対策の話が出たのだけど、バルス画像が出た瞬間にOdersk先生が爆笑していたのを、後ろから目撃した。
オダスキー先生がバルスでめっちゃうけてる #ScalaMatsuri
— ninjinkun (@ninjinkun) 2014, 9月 6
Odersk先生はバルスのことを知ってるのか…。何だか変な風に繋がってしまった気がして、不思議な気分で帰宅した。
Androidアプリの段階的リリース
Androidアプリは全体の5%のユーザーに公開するというような、段階的公開が可能です。会社でこの機能を使っているので、知見をまとめました。
目的
- 致命的な問題 (e.g. 商品が出品、購入できない)に全ユーザーを巻き込むのを避ける
- 不具合を減らしつつ、リリースサイクルのスピードを保つ
問題
- 母集団が少なすぎると問題が見つからない or 報告されない場合がある
- 変更できるのは公開するユーザーの割合のみ
- できる限り不具合にユーザーを巻き込まないようにしながら、早めに段階を上げるという問題を考える
戦略
プラン
- 松
- 変更点が多い場合、致命的なバグが発生する可能性がある場合
- 5 or 10%からリリース
- 最短で5日で全ユーザーにリリース
- (竹は考えてなかった…)
- 梅
- 変更点が少ない場合
- 20%からリリース
- 最短で3日で全ユーザーにリリース
運用
- リリースの翌日に不具合の有無を確認して、大丈夫そうなら公開の段階を一つあげる
- サポートメール、Crashlyticsを都度確認
- 土日のリリースは避ける
- 不安なら段階を上げる間隔を2日くらいにする
- これはあくまで基本プラン。リリースの優先度、緊急度によって臨機応変に対応
運用してみて
- 毎日リリース作業があるのが地味に面倒
- 不具合の確認作業に時間を取られる
- 15分とかだけど、まあ仕方ない
- 手作業なので、注意していないと作業自体を忘れる
- このあたりは自動化の余地有りかも
- 不具合の確認作業に時間を取られる
- 不具合は5~20%くらいで顕在化する印象
- 50->100はそんなに怖くない、というかあまり意味が無さそうなので飛ばしてもいいかも
- 不具合が出たら修正してapkを再アップロード。対象ユーザーだけにアップデートが配布される。よくできてる
他社の事例も知りたいっす。