この図では、水深の古代の住民のスケルトンや一部の大都市のメトロマップではなく、これは非常にリアルなAndroidアプリケーションの画面上の遷移のマップです。 しかし、複雑ではありますが、実装に成功し、小さなライブラリの形でソリューションを設計することができました。これについては記事で説明します。
名前に関する質問を事前に回避するために、私は明確にします:Cicerone( "chi-che-ro-not" )はイタリア語を起源とする古い言葉で、「外国人向けガイド」を意味します。
私たちのプロジェクトでは、ロジックをディスプレイから分離できるようにするアーキテクチャのアプローチを厳守しようとしています。
私はこの点でMVPを好むので、テキスト全体で「プレゼンター」という言葉がよく使われますが、ここで紹介するソリューションは、アーキテクチャの選択を制限するものではないことに注意してください(クラシックアプローチでは、そして、それでも、Ciceroneは利益を上げます!)。
ナビゲーションはビジネスロジックに近いため、プレゼンターに移行の責任を割り当てることを好みます。 しかし、Androidでは、すべてがそれほどスムーズではありません。アクティビティ間の移行、フラグメントの切り替え、コンテナ内のビューの変更
- コンテキストに依存せずに行うことはできません。コンテキストはロジック層に転送したくないため、プラットフォームにリンクされ、 テストが複雑になり、 メモリリークのリスクがあります (リンクのクリアを忘れた場合)。
- コンテナのライフサイクルを考慮する必要があります(たとえば、 java.lang.IllegalStateException: FragmentsのonSaveInstanceStateの後にこのアクションを実行できません )。
したがって、Ciceroneに実装されたソリューションがありました。
構造から始める価値があると思います。
構造
図には4つのエンティティがあります。
- コマンドは、ナビゲーターが実行する最も単純なナビゲーションコマンドです。
- ナビゲーター-コンテナー内の「画面の切り替え」の直接実装。
- ルーターは、高レベルのプレゼンターナビゲーション呼び出しをコマンドセットに変換するクラスです。
- CommandBuffer-呼び出されたナビゲーションコマンドの安全性を担当します。呼び出し時に移行する可能性がない場合。
次に、それぞれについて詳しく説明します。
紹介コマンド
遷移マップ(最初の画像のように非常に複雑なものでも)は、4つの基本的な遷移を使用して実装でき、それらを組み合わせて必要な動作が得られることに気付きました。
進む
Forward(String screenKey、Object transitionData) -新しい画面に移行し、現在の画面のチェーンに追加するコマンド。
screenKey-各画面の一意のキー。
transitionData-新しい画面に必要なデータ。
文字Rはルート画面を示し、その機能は、この画面を終了すると、アプリケーションを終了することです。
戻る
戻る() -最後のアクティブな画面をチェーンから削除し、前の画面に戻るコマンド。 ルート画面で呼び出されると、アプリケーションは終了することが期待されています。
戻る
BackTo(String screenKey) -キーを指定するだけで、チェーン内の任意の画面に戻ることができるコマンド。 同じキーを持つチェーンに2つの画面がある場合、最後の画面(最も「正しい」画面)が選択されます。
指定された画面が見つからない場合、またはキーパラメータにnullが渡された場合、ルート画面への移行が実行されることに注意してください。
実際には、このコマンドは非常に便利です。 たとえば、承認の場合:2つの画面。 電話-> SMS、そして認証が開始されたものへのアクセス。
交換
Replace(String screenKey、Object transitionData) -アクティブな画面を新しい画面に置き換えるコマンド。
誰かが連続してBackおよびForwardコマンドを呼び出すことでこの結果を達成できることに反対するかもしれませんが、その後、ルート画面でアプリケーションを終了します!
以上です! 実際にこれらの4つのチームは、移行を構築するのに十分です。 しかし、ナビゲーションに適用されない別のチームがありますが、実際には非常に役立ちます。
システムメッセージ
SystemMessage(文字列メッセージ) -システムメッセージ(アラート、トースト、スナックなど)を表示するコマンド。
画面を終了して、ユーザーにメッセージを表示する必要がある場合があります。 たとえば、行った変更を保存したとします。 しかし、私たちが戻るスクリーンは、他の誰かのロジックを知る必要がないので、そのようなメッセージの表示を別のコマンドに入れます。 とても便利です!
すべてのコマンドには、コマンドマーカーインターフェイスが付いています。 何らかの理由で新しいチームが必要な場合は、チームを作成するだけで、制限はありません!
ナビゲーター
チーム自体は切り替え画面を実装せず、これらの移行についてのみ説明します。 Navigatorが実装を担当します。
public interface Navigator { void applyCommand(Command command); }
タスクに応じて、Navigatorはさまざまな方法で実装されますが、常に切り替え可能な画面のコンテナが配置される場所になります。
- フラグメントを切り替えるアクティビティ。
- ネストされた(子)フラグメントを切り替えるフラグメント内。
- ...あなたのオプション。
大部分のAndroidアプリケーションでは、ナビゲーションは同じタイプのコードを記述しないようにアクティビティ内のフラグメントの切り替えに依存しているため、ライブラリには、提示されたコマンドを実装する既製のFragmentNavigator(およびSupportFragmentsのSupportFragmentNavigator)が既にあります。
十分な:
1)コンテナIDとFragmentManagerをコンストラクタに渡します。
2)アプリケーションを終了し、システムメッセージを表示するためのメソッドを実装します。
3)screenKeyによるフラグメントの作成を実装します。
より詳細な例については、サンプルアプリケーションをご覧になることをお勧めします。
アプリケーションにナビゲータが1つある必要はありません。 例(ちなみに実際にも):アクティビティにはBottomBarがあり、これは常にユーザーが利用できます。 ただし、各タブには独自のナビゲーションがあり、BottomBarでタブを切り替えるときに保存されます。
これは、タブを切り替えるActivity内の単一のナビゲーターと、各Fragmentタブ内のローカルナビゲーターによって解決されます。
したがって、個々のプレゼンターは、自分がいる場所(タブのチェーン内または別のアクティビティ内)に拘束されません。 彼に正しいルーターを提供するだけで十分です。 1つのルーターは、常に1つのナビゲーターにのみ関連付けられます。 これについてもう少し。
ルーター
前述のように、 コマンドを組み合わせて 、任意の遷移を実装できます。 これがルーターの機能です。
たとえば、プレゼンターに何らかのイベントのタスクがある場合:
1)チェーン全体をルート画面に投げます。
2)ルート画面を新しいものに置き換えます。
3)それでもシステムメッセージを表示します。
その後、RouteBufferにメソッドが追加され、実行のために3つのコマンドのシーケンスがCommandBufferに渡されます。
public void navigateToNewRootWithMessage(String screenKey, Object data, String message) { executeCommand(new BackTo(null)); executeCommand(new Replace(screenKey, data)); executeCommand(new SystemMessage(screenKey, data)); }
プレゼンター自身がこれらのメソッドを呼び出した場合、最初のBackTo()コマンドの後、それは破棄され(正確にはそうではありませんが、本質を伝えます)、作業を正しく完了しませんでした。
ライブラリには既製のルーターがあり、デフォルトで使用され、最も必要な遷移がありますが、ナビゲーターと同様に、独自の実装の作成を禁止するものはありません。
navigateTo() -新しい画面に移行します。
newScreenChain() -チェーンをルート画面にリセットし、新しい画面を開きます。
newRootScreen() -チェーンをリセットし、ルート画面を置き換えます。
replaceScreen() -現在の画面を置き換えます。
backTo() -チェーン内の任意の画面に戻ります。
exit() -画面を終了します。
exitWithMessage() -画面を終了してメッセージを表示します。
showSystemMessage() -システムメッセージを表示します。
CommandBuffer
CommandBuffer-Navigatorにナビゲーションコマンドを配信する役割を担うクラス。 ナビゲーターインスタンスへの参照がCommandBufferに格納されるのは論理的です。 NavigatorHolderインターフェースを介してそこに到達します。
public interface NavigatorHolder { void setNavigator(Navigator navigator); void removeNavigator(); }
また、コマンドがCommandBufferで受信され、現在ナビゲータに含まれていない場合、それらはキューに保存され、新しいナビゲータがインストールされるとすぐに実行されます。 CommandBufferのおかげで、彼はライフサイクルのすべての問題を解決することができました。
アクティビティの具体例:
@Override protected void onResume() { super.onResume(); SampleApplication.INSTANCE.getNavigatorHolder().setNavigator(navigator); } @Override protected void onPause() { SampleApplication.INSTANCE.getNavigatorHolder().removeNavigator(); super.onPause(); }
なぜ正確にonResumeとonPauseなのか? フラグメントの安全なトランザクションおよびシステムメッセージのアラートとしての表示。
理論から実践へ。 シセロンの使用方法
MainActivityのフラグメントにナビゲーションを実装するとします。
build.gradleに依存関係を追加します
repositories { maven { url 'https://dl.bintray.com/terrakok/terramaven/' } } dependencies { //Cicerone compile 'ru.terrakok.cicerone:cicerone:1.0' }
SampleApplicationクラスで、完成したルーターを初期化します
public class SampleApplication extends Application { public static SampleApplication INSTANCE; private Cicerone<Router> cicerone; @Override public void onCreate() { super.onCreate(); INSTANCE = this; cicerone = Cicerone.create(); } public NavigatorHolder getNavigatorHolder() { return cicerone.getNavigatorHolder(); } public Router getRouter() { return cicerone.getRouter(); } }
MainActivityで、ナビゲーターを作成します。
private Navigator navigator = new SupportFragmentNavigator(getSupportFragmentManager(), R.id.main_container) { @Override protected Fragment createFragment(String screenKey, Object data) { switch(screenKey) { case LIST_SCREEN: return ListFragment.getNewInstance(data); case DETAILS_SCREEN: return DetailsFragment.getNewInstance(data); default: throw new RuntimeException(“Unknown screen key!”); } } @Override protected void showSystemMessage(String message) { Toast.makeText(MainActivity.this, message, Toast.LENGTH_SHORT).show(); } @Override protected void exit() { finish(); } }; @Override protected void onResume() { super.onResume(); SampleApplication.INSTANCE.getNavigatorHolder().setNavigator(navigator); } @Override protected void onPause() { super.onPause(); SampleApplication.INSTANCE.getNavigatorHolder().removeNavigator(); }
これで、アプリケーション内のどこからでも(理想的にはプレゼンターから)、ルーターのメソッドを呼び出すことができます。
SampleApplication.INSTANCE.getRouter().backTo(...);
特殊なケースとその解決策
単一のアクティビティ?
いや! ただし、アクティビティは画面としてではなく、コンテナとしてのみ考慮します。 参照:ルーターはApplicationクラスで作成されているため、あるアクティビティから別のアクティビティに切り替えると、アクティブなナビゲータが単に変更されるため、アプリケーションを独立したアクティビティに分割することは可能です。 もちろん、この場合、画面のチェーンは別々のアクティビティに結び付けられ、BackTo()コマンドは1つのアクティビティのコンテキストでのみ機能することを理解する必要があります。
ネストされたナビゲーション
上記の例を挙げましたが、もう一度繰り返します。
タブ付きのアクティビティがあります。 タスクは、タブが変更されたときに残る各タブ内の画面の独立したチェーンを持つことです。
これは、グローバルとローカルの2種類のナビゲーションによって解決されます。
GlobalRouterは、Activityナビゲーターに関連付けられたアプリケーションルーターです。
タブのクリックを処理するプレゼンターは、GlobalRouterからコマンドを呼び出します。
LocalRouter-各フラグメントコンテナ内のルーター。 LocalRouterのナビゲーターは、フラグメントコンテナー自体を実装します。
タブ内のローカルチェーンに関連するプレゼンターは、ナビゲーションのためにLocalRouterを取得します。
接続はどこですか? フラグメントコンテナーはグローバルナビゲーターにアクセスできます! タブ内のローカルチェーンが終了し、Back()コマンドが呼び出された瞬間に、Fragmentはそれをグローバルナビゲーターに渡します。
ヒント:Dagger 2を使用してコンポーネント間の依存関係を構成し、そのCustomScopesを使用してライフサイクルを管理します。
戻るシステムボタンはどうですか?
この問題は、ライブラリでは特に対処されていません。 [戻る]ボタンを押すことは、ユーザーの操作として理解され、単にイベントとしてプレゼンターに送信される必要があります。
しかし、フローまたはコンダクターはありますか?
他のソリューションを検討しましたが、主なタスクの1つは最も標準的なアプローチを使用し、FragmentManagerとBackStackで別のフレームワークを作成することではなかったため、それらを放棄しました。
第一に、新しい開発者がサードパーティのフレームワークを勉強する必要なく、プロジェクトにすばやく接続できるようにします。
第二に、複雑なサードパーティのソリューションに完全に依存する必要がないため、サポートが困難です。
まとめ
Ciceroneライブラリ:
- フラグメントに結び付けられていない;
- フレームワークではありません。
- 短い通話を提供します。
- 簡単に拡張できます。
- テストに適合。
- ライフサイクルから独立しています!
Github
Ciceroneは、Androidアプリのナビゲーションを簡単にする軽量ライブラリです。