アクティビティはスクリーンが回転した時に破棄されます。また、アクティビティのライフサイクルは複雑です。そして、フラグメントを使うとエラーが起こりやすくなり、ライフサイクルはさらに複雑になります。回転しても破棄されないスクリーンとgoTo(screen)
を呼ぶだけのシンプルなナビゲーションを使ってみませんか?それはモダンなAndroid開発のあるべき姿なのです。
イントロダクション (0:00)
こんにちは、Fabienです。私はWealthfrontでAndroidの開発責任者をしています。この講演では、Wealthfrontで私のチームが、どのようにして古い形式のViewからフラグメントや私が”モダンAndroid”と呼んでいるものを使うに至ったかを、皆さんとともに見ていきたいと思います。
ライフサイクル (0:50)
ライフサイクルはAndroid開発における基本的な教えです。まず初めにアクティビティがcreateされて、それから startされます。そして、resumeされますが、それは見えるようになるということを意味します。それから、pauseされることもあります、それは部分的に見えている状態を意味しています。そして、stopされて、最後にdestroyされます。とても分かりやすいですよね。でも、ひとつだけ小さな問題があります。それは、最初のダイアグラムが嘘っぱちだということです!
このライフサイクルは素晴らしいものに聞こえますが、実際にはたくさんの厄介なケースが存在するのです。
例えば、onDestroy
が確実に呼ばれるわけではないということが判明します。新しいアクティビティが破棄される過程でプロセスが殺されることによって、アクティビティが殺されることがあります。onPause
とonStop
は呼ばれることが保障されていますが、onDestroy
は保障されていないのです。
さらに悪いことに、そのような厄介なケースに対応するための開発者それぞれによる解決方法があります。
それから、フラグメントがありますが、これは全く非常識なものです。ある特定の状況では、期待通りに動かないだけでなく、現状をより悪くしていまいます。そして、フラグメントのライフサイクルはAndroidのバージョンによって変わってしまいます。
Wealthfrontで始めた新しいプロジェクトではもっとシンプルなものが欲しいと思っていました。そして、私は別の方法に挑戦することに決めました。私たちが初めに見たシンプルなライフサイクルのようなものがあれば良いと思いませんか?
問題 (3:54)
複雑なライフサイクル
この問題のある面については解決することができるかもしれませんが、理解すべきことがとても多く、そしてたいてい複数の問題を抱えています。
シリアライズが必要なオブジェクト
アプリによりますが、常にオブジェクトをシリアライズすることが必要というわけではありません。メモリにそれらを保持しておいて、次の画面にそれをただ渡すだけにしたいと思うことが時々あります。
回転による破棄
フラグなどを設定する方法は知っていますが、それは特有の問題を作り出します。それは本当の解決策ではありません。
バックスタックの貧弱な制御
あなたはこう言いたくなると思います。「これは私のアプリだ、私はあの画面に遷移させたい」、あるいは「あの画面を忘れろ、それは一時的な画面でバックスタックや他の場面で必要とすることはない」。その作業はごく簡単であるべきなのですが、実際にはそうではありません。
解決策 (5:42)
同じ問題を解決しようとしている素晴らしいプロジェクトが他にもたくさんありますし、コミュニティは実際にその方向に向かっていると思います。
これが私の提案する解決策です。
- 単一のアクティビティによるアーキテクチャー
- Viewからロジックを分離する
- スクリーンは回転しても死なないが、Viewはそうならない
- 独自のナビゲーションシステムを実装する
必要なアクティビティは一つだけで、独自にViewを扱います。Viewとロジックを分離するのは、良いアーキテクチャーを構成するのに非常に重要です。また、スクリーンは回転しても死にませんが、Viewは違います。画面が回転した時、ロジックとデータ部分は残っていてほしいと思います。正しい寸法で再描画するために、Viewは再生成します。最後に私たちは独自のナビゲーションシステムを実装しなければいけませんでした。
単一のアクティビティによるアーキテクチャー (7:12)
単一のアクティビティによるアーキテクチャーでは、シンプルに一つのアクティビティだけを扱います。画面Aのためのビューがあります。次に画面Bのために新しいアクティビティを作る代わりに単にビューを差し替えます。とても簡単です。
スクリーンとビュー (7:39)
このアーキテクチャーにおいては、アプリのロジック部分をスクリーンと呼びます。わたしが思いついたものは、モデル、ビュー、プレゼンター(MVP)パターンのようなものです。プレゼンターはモデルとビューの間に位置します。モデルはプレゼンターと会話し、プレゼンターはビューと会話します。
これは私たちが何世代にも渡って目にしてきた非常に古いアーキテクチャーのパターンと似ています。それはオニオンモデルです。コードをレイヤーとして設計し、それぞれのレイヤーはその上のレイヤーか下のレイヤーとだけ会話します。それは多くのアプリケーションで採用されてきたとても良いモデルです。
ですが、私たちはそのモデル、プレゼンターなどの呼び方を捨ててしまって、代わりにデータ、スクリーン、ビューパターンという用語を使います。データがあり、真ん中にロジックがあります。そして、そのデータを表示するためのビューがあります。
ロジックはスクリーンが持っています。それは、ビューが愚か者であるということを意味しています。繰り返します、ビューは愚か者です。ビューにロジックを置いてはいけません。そうでないと単一責任の原則と関心の分離の原則を破ってしまいます。ビューが愚かであるというのは良い実装をする上で重要なことです。
スクリーンは標準のJavaオブジェクトから実装できます。それはスクリーンはテストがしやすいということを意味します。それらはAndroidフレームワークに縛られていません。シンプルなJavaオブジェクトなのでインスタンス化して、テストすることができます。
DIを使いたい場合、スクリーンがインジェクションしやすい構造であるというのは副次的な効用になります。
ナビゲーション (11:04)
Wealthfrontのシステムを設計した時、私はいくつかの核となる目標を念頭に置いていました。バックスタックを完全にコントロールすること、アニメーションを自動的にハンドリングできること、そしてスクリーンをアクティビティのライフサイクルに接続すること、です。
アクティビティを完全に捨て去ることはできなかったので、一つのアクティビティからなるアーキテクチャーにすることにしました。スクリーンに何かを描画するのと、Androidシステムとスクリーンをつなげるためにアクティビティが必要でした。そのため、スクリーンはアクティビティのライフサイクルに縛られています。
ナビゲーションはgoTo(thisScreen)
と呼ぶぐらい簡単にしたいと思っていました。それなら難しくないでしょう?
私たちはそれをマゼランプロジェクトと呼ぶことにしました。なぜならプロジェクトの主な目標はナビゲーターを開発することだったからです。ナビゲーターの役割は各スクリーンを追跡することとビューの間のアニメーションを提供することです。
スクリーンは回転によって破棄されませんが、ビューはそうではありません。ナビゲーターはスクリーンのスタックを保持し、メインアクティビティと結びつけます。ナビゲーターはスクリーンにビューの取得を依頼します。スクリーンがビューを保持しているからです。そして、スクリーンはメインアクティビティ内で生存しています。端末が回転すると、メインアクティビティはシステムによって破棄されてしまい、ビューも破棄されて再描画されます。
しかし、スクリーンは生き残ります。メインアクティビティとビューは新たに生成されますが、スクリーンは新たに生成されません。あるスクリーンから別のスクリーンに進む際にはスタックにスクリーンが追加されます。ナビゲーターによって、新たなスクリーンが追加されます。この新しいスクリーンには新しいビューを追加することができます。そのビューを取得して、良い感じに遷移させて、またビューを差し替えます。もし前の画面に戻りたければ、スタックにある直近のスクリーンからビューを取得するだけです。
私たちは実際にこのアーキテクチャーをプロダクションで使いました。だから、これは仮説ではありません。
navigator.goTo(new MySimpleScreen(stuff));
これを見て、スクリーンが通常のコンストラクタを持ったJavaオブジェクトだと分かるでしょう。そして、それは生成するオブジェクトに参照を渡すことができるということを意味しています。ごく簡単なJavaの書き方で必要なデータをスクリーンオブジェクトに直接渡すことができます。
もし、もっと凝ったことがしたければ、transitionをオーバーライドすると良いです。実装したいクールなことは全てできます。
navigator.overrideTransition(transition);
ナビゲーターを使うにはルートスクリーンをセットします。常にスタックには1つのスクリーンがあります。これはAPIによって矯正されるものなので、違反するとナビゲーターは例外をスローします。
Navigator
.withRoot(new HomeScreen())
.loggingEnabled(BuildConfig.DEBUG)
.build();
ナビゲーターにはいくつかのメソッドがあります。goBack
を呼び出すことで特定のスクリーンに戻ることができます。
navigator.goBack();
navigator.goBackTo(screen);
スクリーンを差し替えることもできます。
navigator.replace(screen);
ユーザがログインしていなければ、ログインスクリーンを表示し、ログインしていれば、ホームスクリーンを表示したいと思うでしょう。
navigator.rewriteHistory(new HistoryRewriter() {
@Override
public void rewriteHistory(Deque<Screen> history) {
if (!authenticator.isUserLoggedIn()) {
history.clear();
history.push(loginScreen);
} else {
history.push(homeScreen);
}
}
}
);
それでは、スクリーンはどのように実装すれば良いでしょうか?
public class MySimpleScreen extends Screen<MySimpleView> {
@Override
protected MySimpleView createView(Context context) {
return new MySimpleView(context);
}
@Override
public String getTitle(Context context) {
return context.getString(R.string.my_screen_title);
}
@Override
protected void onViewCreated() {
getView().displayStuff(stuff);
}
}
Screen
を継承し、タイプセーフにするためにViewの型を型パラメーターとして指定します。
そして、インスタンス化できて、欲しい時に手に入るViewを作ります。getTitle
のような、たくさんの役立つメソッドもあります。getTitle
はアクションバーに自動でタイトルを設定できるメソッドです。
もちろん、私たちはまだアクティビティのライフサイクルに縛られています。しかし、私たちはonResume
とonPause
のような本当に必要なメソッドだけに制限しました。
public class MySimpleScreen extends Screen<MySimpleView> {
@Override
protected void onResume(Context context) {
}
@Override
public String onPause(Context context) {
}
...
}
onResume
とonPause
は実に適切なタイミングで呼び出されます。私たちはこのメソッドをあるスクリーンからもう一つのスクリーンに遷移する時に利用します。標準のonResume
がアクティビティで呼ばれていなかったとしても問題ありません。
実装 (18:18)
実装は難しい部分です。なぜなら、私たちが見ることができるのはAPIだけだからです。これはちょっとトリッキーな方法ですが、いくつかのtipsとコツをお見せします。
ナビゲーターはひとつだけにしましょう
ナビゲーターが複数あるのは良くありません。シングルトンとしてナビゲーターを注入するか、あるいはナビゲーターを保持し続けるためにonRetainNonConfigurationInstance()
を使ってください。ナビゲーターは生き残る必要があります。もし、アクティビティと一緒に破棄されそうになったら、それに打ち勝ってください。
バックスタックには単純なデキュー(基本的なスタック)を使いましょう
バックスタックは、こうすることでポップとプッシュで操作できます。
遷移の途中でユーザにバックスタックの中身を変えさせないようにしましょう
このような方法はそもそも、画面の履歴を操作するのにあまり良い方法ではなかったのです。裏技みたいなものです。システムを整合性のない状態にしてしまうような操作をユーザにして欲しくはないでしょう
常に対になるライフサイクルメソッドを呼ぶようにしましょう
onViewCreated
を呼び出したら、確実にonViewDestroyed
も呼ぶ必要があります。通常のアクティビティはそうはなっていません。たとえば、onDestroy
が呼ばれることは保証されていません。しかし、その挙動を知る必要があります。これはそんなに難しくはなくて、そのためのテストがあることを確認して、自分のしていることを見失わないようにすれば良いのです。
バックスタックの操作のために関数プログラミングを使いましょう
バックスタックの操作は基本的に同じ形式になります。バックスタックに対して操作を行い、あるスクリーンを現在のスタックの一番上に表示する、という流れです。この操作をするために関数ついて考える必要があります。関数について考えることがAPIへとたどり着くための始まりです。必要でなければ凝った遷移を実装する必要はありません。たとえそれが実装の詳細段階であってもです。実際にある機能を持つ画面に行く時には、単純にスタックに新しいスクリーンをプッシュするだけです。戻る時にはただスタックをポップするための関数があります。関数プログラミングの観点で実装を考えると、実装はより良くなります。
全てを同期処理にすべきだが、画面遷移時にポストしなければならない
これを回避する方法はありません。すでに試してみたのですが。特にダイアログが表示されていて、ボタンを押して別のスクリーンに行くような場合にはポストする必要があります。ビューが移動し始めるからです。システムがポップアップを閉じようとするため、実際にはクラッシュします。
ビューのサイズが決まるのを待つためにViewTreeObserver
を使いましょう
これは良く起こる厄介なことのひとつです。ビューのサイズが決まるまでに、アニメーションを始める必要があります。このオブザーバーを削除するのを忘れなければ、問題はありません。
onDestroy
が呼ばれる保証はない
ひとつの解決策としてはアクティビティへの参照を弱い参照にすることです。アクティビティ内で後処理をするのにonDestroy
を信用することはできません。
今後の予定 (22:56)
このライブラリはまだ問題が多すぎると思ったかもしれません。確かに完璧ではありません。まだ実装途中の多くの機能があります。例えば、アプリが落とされた時にバックスタックを自動的に復元する機能がありません。また、マルチスクリーンにも対応できていないので、左右に2ペインを持つレイアウトを表示する、ということを簡単に実現する方法がありません。また、マテリアルデザイン風のアニメーションもまだライブラリに含めていません。しかし、既に実装したコードはあるので、近々入れることができると思います。
なお、私たちはこのプロジェクトをすぐにオープンソースにするつもりです。あなたたちはこれをライブラリとして使う必要はありません。もし、私たちが皆さんのプルリクエストを素早く取り込めていなかったり、あるいは、自分でカスタマイズしたければ、ソースをフォークしてください。
私たちの実際のアプリでは、おそらく、オープンソースの最初のバージョンには入れていないたくさんの対応を実施しました。私たちはRxJavaのobservableを自動で解除してくれるRxスクリーンのようなとてもシンプルなものを作りました。また、タブがあれば自動的にタブレイアウトを使うことができるタブスクリーンや、あるいはタイトルや背景色を変更するといったことが簡単にできるツールバーを作りました。このツールバーはオープンソースの最初のバージョンに組み込む予定です。
以上です。
About the content
This content has been published here with the express permission of the author.