Android 開發者指南片段導航模式
已發表: 2022-03-11多年來,我在 Android 中看到了許多不同的導航模式實現。 一些應用程序僅使用活動,而其他活動與片段和/或自定義視圖混合。
我最喜歡的導航模式實現之一是基於“One-Activity-Multiple-Fragments”理念,或者簡稱為 Fragment Navigation Pattern,其中應用程序中的每個屏幕都是一個全屏 Fragment,並且所有或大部分這些 Fragment 都包含在一項活動。
這種方法不僅簡化了導航的實現方式,而且具有更好的性能,從而提供更好的用戶體驗。
在這篇文章中,我們將看看Android中一些常見的導航模式實現,然後介紹基於Fragment的導航模式,並與其他導航模式進行比較和對比。 實現此模式的演示應用程序已上傳到 GitHub。
活動世界
僅使用活動的典型 Android 應用程序被組織成樹狀結構(更準確地說是有向圖),其中根活動由啟動器啟動。 當您在應用程序中導航時,操作系統會維護一個活動回棧。
一個簡單的例子如下圖所示:
Activity A1 是我們應用程序的入口點(例如,它表示啟動屏幕或主菜單),用戶可以從中導航到 A2 或 A3。 當您需要在活動之間進行通信時,您可以使用startActivityForResult()或者您可以在它們之間共享一個全局可訪問的業務邏輯對象。
當您需要添加新活動時,您需要執行以下步驟:
- 定義新活動
- 在AndroidManifest.xml中註冊
- 用另一個活動的startActivity()打開它
當然,這個導航圖是一種相當簡單的方法。 當您需要操作返回堆棧或必須多次重用相同的活動時,它可能會變得非常複雜,例如,當您希望通過一些教程屏幕導航用戶但實際上每個屏幕都使用相同的活動作為根據。
幸運的是,我們有用於它的工具,稱為任務和一些用於正確返回堆棧導航的指南。
然後,API 級別 11 出現了片段……
碎片世界
Android 在 Android 3.0(API 級別 11)中引入了 Fragment,主要是為了在平板電腦等大屏幕上支持更動態和靈活的 UI 設計。 因為平板電腦的屏幕比手機大得多,所以有更多的空間來組合和交換 UI 組件。 片段允許這樣的設計,而無需您管理對視圖層次結構的複雜更改。 通過將 Activity 的佈局劃分為片段,您可以在運行時修改 Activity 的外觀,並將這些更改保存在由 Activity 管理的後退堆棧中。 – 引用自 Google 的片段 API 指南。
這個新玩具允許開發人員構建多窗格 UI 並在其他活動中重用組件。 一些開發人員喜歡這一點,而其他開發人員則不喜歡。 是否使用片段是一個流行的爭論,但我想每個人都會同意片段帶來了額外的複雜性,開發人員確實需要了解它們才能正確使用它們。
Android中的全屏片段噩夢
我開始看到越來越多的示例,其中片段不僅代表屏幕的一部分,而且實際上整個屏幕是包含在活動中的片段。 有一次我什至看到一個設計,其中每個活動都只有一個全屏片段,僅此而已,而這些活動存在的唯一原因就是託管這些片段。 除了明顯的設計缺陷之外,這種方法還有另一個問題。 從下面看一下圖表:
A1如何與F1通信? 好吧,A1 完全控制了 F1,因為它創建了 F1。 A1 可以傳遞一個包,例如,在創建 F1 時,也可以調用它的公共方法。 F1如何與A1通信? 好吧,這更複雜,但可以通過 A1 訂閱 F1 並且 F1 通知 A1 的回調/觀察者模式來解決。
但是 A1 和 A2 如何相互通信呢? 這已經涵蓋了,例如通過startActivityForResult() 。
而現在真正的問題來了:F1 和 F2 如何相互通信? 即使在這種情況下,我們也可以擁有一個全局可用的業務邏輯組件,因此它可以用於傳遞數據。 但這並不總能帶來優雅的設計。 如果 F2 需要以更直接的方式將一些數據傳遞給 F1 怎麼辦? 好吧,使用回調模式,F2 可以通知 A2,然後 A2 以結果結束,該結果由通知 F1 的 A1 捕獲。
這種方法需要大量樣板代碼,很快就會成為錯誤、痛苦和憤怒的根源。
如果我們可以擺脫所有的活動並只保留其中一個來保留其餘的片段怎麼辦?
片段導航模式
多年來,我開始在我的大多數應用程序中使用“One-Activity-Multiple-Fragments”模式,並且我仍然使用它。 有很多關於這種方法的討論,例如這里和這裡。 然而,我錯過的是一個具體的例子,我可以看到並測試自己。
讓我們看一下下面的圖表:
現在我們只有一個容器活動,並且我們有多個片段,它們又具有樹狀結構。 它們之間的導航由FragmentManager處理,它有自己的後台堆棧。
請注意,現在我們沒有startActivityForResult()但我們可以實現回調/觀察者模式。 讓我們看看這種方法的一些優點和缺點:
優點:
1.更乾淨、更易維護的AndroidManifest.xml
現在我們只有一個 Activity,我們不再需要在每次添加新屏幕時更新清單。 與活動不同,我們不必聲明片段。
這似乎是一件小事,但對於有 50 多個活動的大型應用程序,這可以顯著提高AndroidManifest.xml文件的可讀性。
查看具有多個屏幕的示例應用程序的清單文件。 清單文件仍然非常簡單。
<?xml version="1.0" encoding="utf-8"?> package="com.exarlabs.android.fragmentnavigationdemo.ui" > <application android:name= ".FragmentNavigationDemoApplication" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name="com.exarlabs.android.fragmentnavigationdemo.ui.MainActivity" android:label="@string/app_name" android:screenOrientation="portrait" android:theme="@style/AppTheme.NoActionBar" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
2. 集中導航管理
在我的代碼示例中,您將看到我使用NavigationManager ,在我的例子中它被注入到每個片段中。 該管理器可用作日誌記錄、回棧管理等的集中位置,因此導航行為與其他業務邏輯分離,並且不會分散在不同屏幕的實現中。

讓我們想像一種情況,我們想要啟動一個屏幕,用戶可以在其中從人員列表中選擇一些項目。 您還想傳遞一些過濾參數,例如年齡、職業和性別。
如果是活動,你會寫:
Intent intent = new Intent(); intent.putExtra("age", 40); intent.putExtra("occupation", "developer"); intent.putExtra("gender", "female"); startActivityForResult(intent, 100);
然後你必須在下面的某個地方定義onActivityResult並處理結果。
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); }
我個人對這種方法的問題是這些參數是“額外的”並且它們不是強制性的,所以我必須確保接收活動在缺少額外的情況下處理所有不同的情況。 稍後,當進行一些重構並且不再需要例如“年齡”額外內容時,我必須在開始此活動的代碼中的所有位置進行搜索,並確保所有額外內容都是正確的。
此外,如果結果(人員列表)以 _List 的形式到達不是更好嗎
在基於片段的導航的情況下,一切都更加簡單。 您所要做的就是在NavigationManager中編寫一個名為startPersonSelectorFragment()的方法,其中包含必要的參數和回調實現。
mNavigationManager.startPersonSelectorFragment(40, "developer", "female", new PersonSelectorFragment.OnPersonSelectedListener() { @Override public boolean onPersonsSelected(List<Person> selection) { [do something] return false; } });
或使用 RetroLambda
mNavigationManager.startPersonSelectorFragment(40, "developer", "female", selection -> [do something]);
3. 更好的屏幕間交流方式
在活動之間,我們只能共享一個可以保存原語或序列化數據的 Bundle。 現在有了片段,我們可以實現一個回調模式,例如,F1 可以監聽 F2 傳遞任意對象。 請查看前面示例的回調實現,它返回一個 _List
4.構建片段比構建活動更便宜
當您使用具有例如 5 個菜單項的抽屜時,這一點變得很明顯,並且在每個頁面上都應該再次顯示抽屜。
在純活動導航的情況下,每個頁面都應該膨脹並初始化抽屜,這當然是昂貴的。
在下圖中,您可以看到幾個根片段(FR*),它們是可以直接從抽屜中訪問的全屏片段,並且只有在顯示這些片段時才能訪問抽屜。 圖中虛線右側的所有內容都是任意導航方案的示例。
由於容器活動包含抽屜,我們只有一個抽屜實例,因此在抽屜應該可見的每個導航步驟中,您不必再次膨脹和初始化它。 仍然不相信所有這些是如何工作的? 看看我的示例應用程序,它演示了抽屜的使用。
缺點
我最大的恐懼一直是,如果我在項目中使用基於片段的導航模式,在某個地方我會遇到一個無法預料的問題,圍繞片段增加的複雜性、3rd 方庫和不同的操作系統版本很難解決。 如果我必須重構到目前為止所做的一切怎麼辦?
事實上,我必須解決嵌套片段的問題,第 3 方庫也使用片段,例如 ShinobiControls、ViewPagers 和 FragmentStatePagerAdapters。
我必須承認,獲得足夠的片段經驗來解決這些問題是一個相當漫長的過程。 但在每種情況下,問題都不是哲學不好,而是我對片段的理解不夠好。 也許如果你比我更了解 Fragments,你甚至不會遇到這些問題。
我現在可以提到的唯一缺點是,我們仍然會遇到難以解決的問題,因為沒有成熟的庫可以展示基於片段導航的複雜應用程序的所有復雜場景。
結論
在本文中,我們看到了在 Android 應用程序中實現導航的另一種方法。 我們將它與使用活動的傳統導航理念進行了比較,我們已經看到了使用它優於傳統方法的一些充分理由。
如果您還沒有,請查看上傳到 GitHub 實現的演示應用程序。 隨意分叉或貢獻更好的例子,這將更好地展示它的用法。