本文已授權(quán) 微信公眾號(hào) 玉剛說 (@任玉剛)獨(dú)家發(fā)布跛十。
前言
在不久前的Google 2018 I/O大會(huì)上校读,Google正式推出了AndroidJetpack ——這是一套組件娘摔、工具和指導(dǎo)浸间,可以幫助開發(fā)者構(gòu)建出色的 Android 應(yīng)用叹卷,這其中就包含了去年推出的 Lifecycle, ViewModel, LiveData 以及 Room抚太。除此之外,AndroidJetpack 還隆重推出了一個(gè)新的架構(gòu)組件:Navigation营曼。
從名字來看乒验,我翻譯它叫導(dǎo)航, 我們來看看Google官方對(duì)它的描述:
今天,我們宣布推出Navigation組件蒂阱,作為構(gòu)建您的應(yīng)用內(nèi)界面的框架锻全,重點(diǎn)是讓單 Activity 應(yīng)用成為首選架構(gòu)。利用Navigation組件對(duì) Fragment 的原生支持录煤,您可以獲得架構(gòu)組件的所有好處(例如生命周期和 ViewModel)鳄厌,同時(shí)讓此組件為您處理 FragmentTransaction 的復(fù)雜性。此外妈踊,Navigation組件還可以讓您聲明我們?yōu)槟幚淼霓D(zhuǎn)場(chǎng)了嚎。它可以自動(dòng)構(gòu)建正確的“向上”和“返回”行為,包含對(duì)深層鏈接的完整支持廊营,并提供了幫助程序歪泳,用于將導(dǎo)航關(guān)聯(lián)到合適的 UI 小部件,例如抽屜式導(dǎo)航欄和底部導(dǎo)航露筒。
拋開比較性的話題不談(StoryBoard VS Navigation?)呐伞,Navigation的發(fā)布讓我意識(shí)到 這是一個(gè)契機(jī),我覺得我有必要花時(shí)間去深入了解它——既能 學(xué)習(xí)新的技術(shù)及理念 邀窃,同時(shí)又能 查漏補(bǔ)缺荸哟,完善自己的Android知識(shí)體系(Fragment的管理)。
這件事立即被我列上日程瞬捕,過去的一周鞍历,我閑暇之際仔細(xì)研究了 Navigation, 并略有心得,我嘗試寫下本文肪虎,在總結(jié)的同時(shí)劣砍,希望能夠給后來的朋友們一些 系統(tǒng)性的指導(dǎo)建議 。如果可能扇救,我甚至希望這篇文章能夠做到:
本文不是詳細(xì)的API說明文檔刑枝,但僅通過閱讀本文,能夠?qū)?Navigation 有一個(gè)系統(tǒng)性地學(xué)習(xí)—— 了解它迅腔,理解它装畅,最后搞懂它。
這對(duì)讀寫雙方都是 一次挑戰(zhàn)沧烈。完成它的第一步是做到:知道Navigation這個(gè)導(dǎo)航組件 怎么用掠兄。
了解Navigation
1.官方文檔
官方文檔 永遠(yuǎn)是最接近 正確 和 核心理念 的參考資料 —— 在不久之后,本文可能會(huì)因?yàn)榭蚣鼙旧鞟PI的迭代更新而 毫無(wú)意義,但官方文檔不會(huì)蚂夕,即使在最惡劣的風(fēng)暴中迅诬,它依然是最可靠的 指明燈:
https://developer.android.com/topic/libraries/architecture/navigation/
其次,一個(gè)好的Demo能夠起到重要的啟發(fā)作用婿牍, 這里我推薦 Google實(shí)驗(yàn)室 的這個(gè)Sample:
項(xiàng)目地址:https://github.com/googlecodelabs/android-navigation
項(xiàng)目教程:https://codelabs.developers.google.com/codelabs/android-navigation/#0
這個(gè)教程Demo的優(yōu)勢(shì)在于侈贷,官方為這個(gè)Demo提供了 一系列詳細(xì)的教程,通過一步步等脂,引導(dǎo)學(xué)習(xí)每一個(gè)類或者組件的應(yīng)用場(chǎng)景俏蛮,最終完全上手 Navigation。
因?yàn)閯倓偘l(fā)布的原因慎菲,目前Navigation的中文教程 極其匱乏嫁蛇,許多資料的查閱可能需要開發(fā)者 自備梯子。不過請(qǐng)不必?fù)?dān)心露该,本文會(huì)力爭(zhēng)做到比其它同類文章講解的 更加全面。
2.Sample展示
我寫了一個(gè)Navigation的sample第煮,它最終的效果是這樣:
這是3個(gè)簡(jiǎn)單的Fragment之間跳轉(zhuǎn)的情景解幼,經(jīng)過 轉(zhuǎn)場(chǎng)動(dòng)畫 的修飾,它們之前的切換非常 流暢 且 自然包警。在展示的最后撵摆,我們可以看到,F(xiàn)ragment2 -> Fragment1的時(shí)候害晦,實(shí)際上是由 用戶 點(diǎn)擊手機(jī)Back鍵 觸發(fā)的特铝。
項(xiàng)目結(jié)構(gòu)圖如下,這可以幫你盡快了解sample的結(jié)構(gòu):
我把這個(gè)sample的源碼托管在了我的github上壹瘟,你可以通過 點(diǎn)我查看源碼 鲫剿。
3.嘗試使用Navigation
Navigation目前僅AndroidStudio 3.2以上版本支持,如果您的版本不足3.2稻轨,請(qǐng)點(diǎn)此下載預(yù)覽版AndroidStudio
首先介紹Navigation的使用:
無(wú)論是否認(rèn)可灵莲,我們都必須承認(rèn),Google已經(jīng)在嘗試讓Kotlin上位殴俱,無(wú)論是今年IO大會(huì)的 數(shù)據(jù)展示政冻,還是官方文檔上的 代碼示例片段,亦或是Google最新 開源Demo的源碼线欲,使用語(yǔ)言清一色 Kotlin明场,本文亦然。
① 在Module下的build.gradle中添加以下依賴:
dependencies {
def nav_version = '1.0.0-alpha01'
implementation "android.arch.navigation:navigation-fragment:$nav_version"
implementation "android.arch.navigation:navigation-ui:$nav_version"
}
② 新建三個(gè)Fragment:
//3個(gè)Fragment李丰,它們除了layout不同苦锨,沒有其它區(qū)別
class MainPage1Fragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View {
return inflater.inflate(R.layout.fragment_main_page1, container, false)
}
}
class MainPage2Fragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_main_page2, container, false)
}
}
class MainPage3Fragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_main_page3, container, false)
}
}
③ 新建導(dǎo)航視圖文件(nav_graph)
在res目錄下新建navigation文件夾,然后新建一個(gè)navigation的resource文件,我叫它 nav_graph_main.xml :
打開導(dǎo)航視圖文件逆屡,我們可以在AndroidStudio 3.2版本上圾旨,進(jìn)行可視化編輯,包括選擇新增Fragment魏蔗,或者拖拽砍的,連接Fragment:
④ 編輯導(dǎo)航視圖文件
我們打開Text標(biāo)簽,進(jìn)入xml編輯的頁(yè)面莺治,并這樣配置:
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
app:startDestination="@id/page1Fragment">
<fragment
android:id="@+id/page1Fragment"
android:name="com.qingmei2.samplejetpack.ui.main.MainPage1Fragment"
android:label="fragment_page1"
tools:layout="@layout/fragment_main_page1">
<action
android:id="@+id/action_page2"
app:destination="@id/page2Fragment" />
</fragment>
<fragment
android:id="@+id/page2Fragment"
android:name="com.qingmei2.samplejetpack.ui.main.MainPage2Fragment"
android:label="fragment_page2"
tools:layout="@layout/fragment_main_page2">
<action
android:id="@+id/action_page1"
app:popUpTo="@id/page1Fragment" />
<action
android:id="@+id/action_page3"
app:destination="@id/nav_graph_page3" />
</fragment>
<navigation
android:id="@+id/nav_graph_page3"
app:startDestination="@id/page3Fragment">
<fragment
android:id="@+id/page3Fragment"
android:name="com.qingmei2.samplejetpack.ui.main.MainPage3Fragment"
android:label="fragment_page3"
tools:layout="@layout/fragment_main_page3" />
</navigation>
</navigation>
注意:請(qǐng)保證fragment標(biāo)簽下廓鞠,android:name屬性內(nèi)包名的正確聲明。
⑤ 編輯MainActivity
在Activity中配置 Navigation 非常簡(jiǎn)單谣旁,我們首先編輯Activity的布局文件床佳,并在布局文件中添加一個(gè) NavHostFragment :
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<fragment
android:id="@+id/my_nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph_main" />
</android.support.constraint.ConstraintLayout>
這是一個(gè)寬和高都 match_parent 的Fragment,它的作用就是 導(dǎo)航界面的容器榄审。
這并不難以理解砌们,我們需要在Activity中通過 Navigation 展示一系列的Fragment,但是我們需要告訴Navigation 和Activity搁进,這一系列的 Fragment 展示在哪——NavHostFragment應(yīng)運(yùn)而生浪感,我把它的作用歸納為 導(dǎo)航界面的容器。
這之后饼问,在Activity中添加如下代碼:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
override fun onSupportNavigateUp() =
findNavController(this, R.id.my_nav_host_fragment).navigateUp()
}
onSupportNavigateUp()方法的重寫影兽,意味著Activity將它的 back鍵點(diǎn)擊事件的委托出去,如果當(dāng)前并非棧中頂部的Fragment, 那么點(diǎn)擊back鍵莱革,返回上一個(gè)Fragment峻堰。
⑥ 最后,配置不同F(xiàn)ragment對(duì)應(yīng)的跳轉(zhuǎn)事件
class MainPage1Fragment : Fragment() {
//隱藏了onCreateView()方法的實(shí)現(xiàn)盅视,下同
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
btn.setOnClickListener {
//點(diǎn)擊跳轉(zhuǎn)page2
Navigation.findNavController(it).navigate(R.id.action_page2)
}
}
}
class MainPage2Fragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
btn.setOnClickListener {
//點(diǎn)擊返回page1
Navigation.findNavController(it).navigateUp()
}
btn2.setOnClickListener {
//點(diǎn)擊跳轉(zhuǎn)page3
Navigation.findNavController(it).navigate(R.id.action_page3)
}
}
}
class MainPage3Fragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//點(diǎn)擊返回page2
btn.setOnClickListener { Navigation.findNavController(it).navigateUp() }
}
}
可以看到捐名,我們對(duì)于Fragment 并非是通過原生的 FragmentManager 和 FragmentTransaction 進(jìn)行控制的。而是通過以下API進(jìn)行的控制:
- Navigation.findNavController(params).navigateUp()
- Navigation.findNavController(params).navigate(actionId)
到這里左冬,Navigation最基本的使用就已經(jīng)講解完畢了桐筏。您可以通過運(yùn)行預(yù)覽和示例 基本一致 的效果,如果遇到問題拇砰,或者有疑問梅忌,可以點(diǎn)我查看源碼 。
理解Navigation
我對(duì)于 通過博客歸納總結(jié) 的學(xué)習(xí)方式已近兩年除破,我不斷反思牧氮,一篇優(yōu)秀的文章不僅是做到 完整敘述,同時(shí)瑰枫,它更應(yīng)該體現(xiàn)的是 對(duì)思路的整理 并 簡(jiǎn)潔干凈地闡述它們踱葛。
做到這點(diǎn)并不容易丹莲,首先需要做到的就是 不要僅局限于API的使用——最初的學(xué)習(xí)中,通過上面的代碼尸诽,我已經(jīng) 實(shí)現(xiàn)了Fragment的導(dǎo)航甥材。但是,上面的代碼中性含,除了Activity 和 Fragment洲赵,其它的東西我一個(gè)都不認(rèn)識(shí)。
我感覺很難受商蕴, 所謂 行百里路半九十叠萍,別說九十,這個(gè)Navigation绪商,我一竅不通苛谷。
僅有上述示例代碼毫無(wú)意義,通過它們格郁,更應(yīng)該將其理解為 入門腹殿;接下來我們需要做到 了解每一個(gè)類的職責(zé),理解框架設(shè)計(jì)者的思想例书。
我們先思考這樣一個(gè)問題:如果讓我們實(shí)現(xiàn)一個(gè)Fragment的導(dǎo)航庫(kù)赫蛇,首先要實(shí)現(xiàn)什么?
1.NavGraphFragment:導(dǎo)航界面的容器
答案近在眼前雾叭。
即使我們使用原生的API,想展示一個(gè)Fragment落蝙,我們首先也需要 定義一個(gè)容器承載它织狐。以往,它可能是一個(gè) RelativeLayout 或者 FrameLayout筏勒,而現(xiàn)在移迫,它被替換成了 NavGraphFragment。
這也就說明了管行,我們?yōu)槭裁匆鵄ctivity的layout文件中提前扔進(jìn)去一個(gè)NavGraphFragment厨埋,因?yàn)槲覀冃枰獙?dǎo)航的這些Fragment都展示在NavGraphFragment上面。
實(shí)際上它做了什么呢捐顷?來看一下NavGraphFragment的onCreateView()方法:
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
FrameLayout frameLayout = new FrameLayout(inflater.getContext());
frameLayout.setId(getId());
return frameLayout;
}
NavGraphFragment內(nèi)部實(shí)例化了一個(gè)FrameLayout, 作為ViewGroup的載體荡陷,導(dǎo)航并展示其它Fragment。
除此之外迅涮,你 應(yīng)當(dāng)注意 到在layout文件中废赞,它還聲明了另外兩個(gè)屬性:
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph_main"
app:defaultNavHost="true"這個(gè)屬性意味著你的NavGraphFragment將會(huì) 攔截系統(tǒng)Back鍵的點(diǎn)擊事件(因?yàn)橄到y(tǒng)的back鍵會(huì)直接關(guān)閉Activity而非切換Fragment),你同時(shí) 必須重寫 Activity的 onSupportNavigateUp() 方法叮姑,類似這樣:
override fun onSupportNavigateUp()
= findNavController(R.id.nav_host_fragment).navigateUp()
app:navGraph="@navigation/nav_graph_main"這個(gè)屬性就很好理解了唉地,它會(huì)指向一個(gè)navigation_graph的xml文件,這之后,NavGraphFragment就會(huì) 導(dǎo)航并展示對(duì)應(yīng)的Fragment。
在我們使用Navigation的第一步耘沼,我們需要:
在Activity的布局文件中顯示聲明NavGraphFragment极颓,并配置 app:defaultNavHost 和 app:navGraph屬性。
2.nav_graph.xml:聲明導(dǎo)航結(jié)構(gòu)圖
NavGraphFragment作為Activity導(dǎo)航的 容器 群嗤,然后菠隆,其 app:navGraph 屬性指向一個(gè)navigation_graph的xml文件,以聲明其 導(dǎo)航的結(jié)構(gòu)骚烧。
NavGraphFragment在 獲取 并 解析 完這個(gè)xml資源文件后浸赫,它首先需要知道的是:
類似APP的home界面,NavGraphFragment首先要導(dǎo)航到哪里?
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
app:startDestination="@id/page1Fragment">
<fragment
android:id="@+id/page1Fragment"
android:name="com.qingmei2.samplejetpack.ui.main.MainPage1Fragment"
android:label="fragment_page1"
tools:layout="@layout/fragment_main_page1">
<action
android:id="@+id/action_page2"
app:destination="@id/page2Fragment" />
</fragment>
//省略...
</navigation>
在navigation的根節(jié)點(diǎn)下赃绊,我們需要處理這樣一個(gè)屬性:
app:startDestination="@id/page1Fragment"
Destination 是一個(gè)很關(guān)鍵的單詞既峡,它的直譯是 目的地。app:startDestination屬性便是聲明這個(gè)id對(duì)應(yīng)的 Destination 會(huì)被作為 默認(rèn)布局 加載到Activity中碧查。這也就說明了运敢,為什么我們的sample,默認(rèn)會(huì)顯示 MainPage1Fragment忠售。
現(xiàn)在传惠,我們的app默認(rèn)展示了MainPage1Fragment, 那么接下來,我們?nèi)绾螌?shí)現(xiàn)跳轉(zhuǎn)邏輯的處理呢稻扬?
3.Action標(biāo)簽:聲明導(dǎo)航的行為
我們聲明了這樣一個(gè)Action標(biāo)簽卦方,這是一個(gè) 導(dǎo)航的行為:
<action
android:id="@+id/action_page2"
app:destination="@id/page2Fragment" />
app:destination的屬性,聲明了這個(gè)行為導(dǎo)航的 destination(目的地)泰佳,我們可以看到盼砍,它會(huì)指印跳轉(zhuǎn)到 id 為 page2Fragment 的Fragment(也就是 MainPage2Fragment)。
android:id 這個(gè)id作為Action唯一的 標(biāo)識(shí)逝她,在Fragment的某個(gè)點(diǎn)擊事件中浇坐,我們通過id指向對(duì)應(yīng)的行為,就像這樣:
btn.setOnClickListener {
//點(diǎn)擊跳轉(zhuǎn)page2Fragment
Navigation.findNavController(it).navigate(R.id.action_page2)
}
此外黔宛,Navigation還提供了一個(gè) app:popUpTo 屬性近刘,它的作用是聲明導(dǎo)航行為 將 返回到 id對(duì)應(yīng)的Fragment,比如臀晃,直接從Page3 返回到 Page1觉渴。
此外,Navigation 對(duì)導(dǎo)航行為還提供了 轉(zhuǎn)場(chǎng)動(dòng)畫 的支持积仗,它可以通過代碼這樣實(shí)現(xiàn):
<action
android:id="@+id/confirmationAction"
app:destination="@id/confirmationFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right" />
篇幅原因疆拘,這些anim的xml文件我并未展示在文中,如有需求寂曹,請(qǐng)參考Sample代碼哎迄。
其實(shí)Navigation 還提供了對(duì)Destination之間 參數(shù)傳遞 的支持回右,以及對(duì)SubNavigation標(biāo)簽的支持,以方便開發(fā)者在xml文件中 復(fù)用fragment標(biāo)簽 ——甚至是對(duì) Deep Link 的支持漱挚,但這些拓展功能本文不再敘述翔烁。
4.Fragment:通過代碼聲明導(dǎo)航
其實(shí)在3中我們已經(jīng)講解了導(dǎo)航代碼的使用,我們以Page2為例,它包含了2個(gè)按鈕旨涝,分別對(duì)應(yīng) 返回Page1 和 進(jìn)入Page3 兩個(gè)事件:
btn.setOnClickListener {
Navigation.findNavController(it).navigateUp()
}
btn2.setOnClickListener {
Navigation.findNavController(it).navigate(R.id.action_page3)
}
Navigation.findNavController(View) 返回了一個(gè) NavController ,它是整個(gè) Navigation 架構(gòu)中 最重要的核心類蹬屹,我們所有的導(dǎo)航行為都由 NavController 處理,這個(gè)我們后面再講白华。
我們通過獲取 NavController慨默,然后調(diào)用 NavController.navigate()方法進(jìn)行導(dǎo)航。
我們更多情況下通過傳入ActionId弧腥,指定對(duì)應(yīng)的 導(dǎo)航行為 厦取;同時(shí)可以通過傳入Bundle以 數(shù)據(jù)傳遞;或者是再傳入一個(gè) NavOptions配置更多(比如 轉(zhuǎn)場(chǎng)動(dòng)畫管搪,它也可以通過這種方式進(jìn)行代碼的動(dòng)態(tài)配置)虾攻。
NavController.navigate()方法更多時(shí)候應(yīng)用在 向下導(dǎo)航 或者 指定向上導(dǎo)航(比如Page3 直接返回 Page1,跳過返回Page2的這一步)更鲁;如果我們處理back事件霎箍,我們應(yīng)該使用 NavController.
navigateUp()。
恭喜您澡为,已經(jīng)能夠游刃有余的使用Navigation!
恭喜您漂坏,您已對(duì) Navigation 十分熟悉,并能通過熟練使用其 暴露的API媒至,靈活地處理您應(yīng)用中的 頁(yè)面導(dǎo)航 行為樊拓。
我美滋滋的在個(gè)人履歷上填上了這樣一條:
- 熟練使用Google官方組件Navigation實(shí)現(xiàn)Fragment的管理,并掌握其原理
面試官對(duì)此十分感動(dòng)塘慕,然后讓我談?wù)?對(duì)它架構(gòu)設(shè)計(jì)的一些個(gè)人觀點(diǎn)。
到了這一步蒂胞,我們算得上是 API的搬運(yùn)工 图呢,我們已經(jīng) 了解每一個(gè)類的職責(zé),還沒有完全 理解框架設(shè)計(jì)者的思想骗随。
徹底搞懂Navigation
在我們熟悉Navigation的API之后蛤织,我們整裝待發(fā),準(zhǔn)備 源碼級(jí)攻克 Navigation鸿染。
正如我所說的指蚜,在這之前,您首先需要達(dá)到 熟練使用Navigation涨椒,本文地初衷并非是 一步到位摊鸡,而是嘗試 循序漸進(jìn)绽媒。
1.對(duì)源碼分析說NO
聲明 —— 我拒絕 大段大段地源碼分析,我認(rèn)為這種行為 嚴(yán)重降低 了文章的 質(zhì)量 和 深度免猾。
我花了一些時(shí)間繪制了 Navigation的UML類圖是辕,我堅(jiān)信,這種方式能幫助你我 更深刻的理解 Navigation的整體架構(gòu):
讓我們換個(gè)角度猎提,我們的身份不再是 源碼的觀眾获三,而是 架構(gòu)的設(shè)計(jì)者。
2. 設(shè)計(jì) NavHostFragment
NavHostFragment 應(yīng)當(dāng)有兩個(gè)作用:
- 作為Activity導(dǎo)航界面的載體
- 管理并控制導(dǎo)航的行為
前者的作用我們已經(jīng)說過了锨苏,我們通過在NavHostFragment的創(chuàng)建時(shí)疙教,為它創(chuàng)建一個(gè)對(duì)應(yīng)的FrameLayout作為 導(dǎo)航界面的載體:
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
FrameLayout frameLayout = new FrameLayout(inflater.getContext());
frameLayout.setId(getId());
return frameLayout;
}
我們都知道代碼設(shè)計(jì)應(yīng)該遵循 單一職責(zé)原則,因此伞租,我們應(yīng)該將 管理并控制導(dǎo)航的行為 交給另外一個(gè)類贞谓,這個(gè)類的作用應(yīng)該僅是 控制導(dǎo)航行為,因此我們命名為 NavController肯夏。
Fragment理應(yīng)持有這個(gè)NavController的實(shí)例经宏,并將導(dǎo)航行為 委托 給它,這里我們將 NavController 的持有者抽象為一個(gè) 接口驯击,以便于以后的拓展烁兰。
于是我們創(chuàng)造了 NavHost 接口,并讓NavHostFragment實(shí)現(xiàn)了這個(gè)接口:
public interface NavHost {
NavController getNavController();
}
為了保證導(dǎo)航的 安全徊都,NavHostFragment 在其 作用域 內(nèi)沪斟,理應(yīng) 有且僅有一個(gè)NavController 的實(shí)例。
這里我們駐足一下暇矫,請(qǐng)注意API的設(shè)計(jì)主之,似乎 Navigation.findNavController(View),參數(shù)中傳遞任意一個(gè) view的引用似乎都可以獲取 NavController——如何保證 NavController 的局部單例呢李根?
事實(shí)上槽奕,findNavController(View)內(nèi)部實(shí)現(xiàn)是通過 遍歷 View樹,直到找到最底部 NavHostFragment 中的NavController對(duì)象房轿,并將其返回的:
private static NavController findViewNavController(@NonNull View view) {
while (view != null) {
NavController controller = getViewNavController(view);
if (controller != null) {
return controller;
}
ViewParent parent = view.getParent();
view = parent instanceof View ? (View) parent : null;
}
return null;
}
3.設(shè)計(jì) NavController
站在 設(shè)計(jì)者 的角度粤攒,NavController 的職責(zé)是:
- 1.對(duì)navigation資源文件夾下nav_graph.xml的 解析
- 2.通過解析xml,獲取所有 Destination(目標(biāo)點(diǎn))的 引用 或者 Class的引用
- 3.記錄當(dāng)前棧中 Fragment的順序
- 3.管理控制 導(dǎo)航行為
NavController 持有了一個(gè) NavInflater ,并通過 NavInflater 解析xml文件囱持。
這之后夯接,獲取了所有 Destination(在本文中即Page1Fragment , Page2Fragment , Page3Fragment ) 的 Class對(duì)象,并通過反射的方式纷妆,實(shí)例化對(duì)應(yīng)的 Destination盔几,通過一個(gè)隊(duì)列保存:
private NavInflater mInflater; //NavInflater
private NavGraph mGraph; //解析xml,得到NavGraph
private int mGraphId; //xml對(duì)應(yīng)的id掩幢,比如 nav_graph_main
//所有Destination的隊(duì)列,用來處理回退棧
private final Deque<NavDestination> mBackStack = new ArrayDeque<>();
這看起來沒有任何問題逊拍,但是站在 設(shè)計(jì)者 的角度上上鞠,還略有不足,那就是顺献,Navigation并非只為Fragment服務(wù)旗国。
先不去吐槽Google工程師的野心,因?yàn)楝F(xiàn)在我們就是他注整,從拓展性的角度考慮能曾,Navigation是一個(gè)導(dǎo)航框架,今后可能 并非只為Fragment導(dǎo)航肿轨。
我們應(yīng)該為要將導(dǎo)航的 Destination 抽象出來寿冕,這個(gè)類叫做 NavDestination ——無(wú)論 Fragment 也好,Activity 也罷椒袍,只要實(shí)現(xiàn)了這個(gè)接口驼唱,對(duì)于NavController 來講,他們都是 Destination(目標(biāo)點(diǎn))而已驹暑。
對(duì)于不同的 NavDestination 來講玫恳,它們之間的導(dǎo)航方式是不同的,這完全有可能(比如Activity 和 Fragment)优俘,如何根據(jù)不同的 NavDestination 進(jìn)行不同的 導(dǎo)航處理 呢京办?
4. NavDestination 和 Navigator
有同學(xué)說,我可以這樣設(shè)計(jì)帆焕,通過 instanceof 關(guān)鍵字惭婿,對(duì) NavDestination 的類型進(jìn)行判斷,并分別做出處理叶雹,比如這樣:
if (destination instanceof Fragment) {
//對(duì)應(yīng)Fragment的導(dǎo)航
} else if (destination instanceof Activity) {
//對(duì)應(yīng)Activity的導(dǎo)航
}
這是OK的财饥,但是不夠優(yōu)雅,Google的方式是通過抽象出一個(gè)類折晦,這個(gè)類叫做 Navigator :
public abstract class Navigator<D extends NavDestination> {
//省略很多代碼,包括部分抽象方法钥星,這里僅闡述設(shè)計(jì)的思路!
//導(dǎo)航
public abstract void navigate(@NonNull D destination, @Nullable Bundle args,
@Nullable NavOptions navOptions);
//實(shí)例化NavDestination(就是Fragment)
public abstract D createDestination();
//后退導(dǎo)航
public abstract boolean popBackStack();
}
Navigator(導(dǎo)航者) 的職責(zé)很單純:
- 1.能夠?qū)嵗瘜?duì)應(yīng)的 NavDestination
- 2.能夠指定導(dǎo)航
- 3.能夠后退導(dǎo)航
你看满着,我的 NavController 獲取了所有 NavDestination 的Class對(duì)象打颤,但是我不負(fù)責(zé)它 如何實(shí)例化 ,也不負(fù)責(zé) 如何導(dǎo)航 漓滔,也不負(fù)責(zé)
如何后退 ——我僅僅持有向上的引用,然后調(diào)用它的接口方法乖篷,它的實(shí)現(xiàn)我不關(guān)心响驴。
以 FragmentNavigator為例,我們來看看它是如何執(zhí)行的職責(zé):
public class FragmentNavigator extends Navigator<FragmentNavigator.Destination> {
//省略大量非關(guān)鍵代碼撕蔼,請(qǐng)以實(shí)際代碼為主豁鲤!
@Override
public boolean popBackStack() {
return mFragmentManager.popBackStackImmediate();
}
@NonNull
@Override
public Destination createDestination() {
// 實(shí)際執(zhí)行了好幾層秽誊,但核心代碼如下,通過反射實(shí)例化Fragment
Class<? extends Fragment> clazz = getFragmentClass();
return clazz.newInstance();
}
@Override
public void navigate(@NonNull Destination destination, @Nullable Bundle args,
@Nullable NavOptions navOptions) {
// 實(shí)際上還是通過FragmentTransaction進(jìn)行的跳轉(zhuǎn)處理
final Fragment frag = destination.createFragment(args);
final FragmentTransaction ft = mFragmentManager.beginTransaction();
ft.replace(mContainerId, frag);
ft.commit();
mFragmentManager.executePendingTransactions();
}
}
不同的 Navigator 對(duì)應(yīng)不同的 NavDestination琳骡,FragmentNavigator 對(duì)應(yīng)的是 FragmentNavigator.Destination锅论,你可以把他理解為案例中的 Fragment ,有興趣的朋友可以自己研究一下楣号。
5.至此
至此最易,Navigation 整體的架構(gòu)設(shè)計(jì) 也已經(jīng)通過 UML類圖 + 設(shè)計(jì)的角度分析 的方式學(xué)習(xí)完了。
當(dāng)然炫狱,Navigation 還有很多其它的類我沒有去闡述藻懒,它們已經(jīng)無(wú)法阻攔你我的腳步。
我更建議 讀者在這之后视译,能夠嘗試自己閱讀源碼嬉荆,通過借鑒上文中的 UML類圖,當(dāng)然酷含,自己通過思路的整理鄙早,自己繪制出一份,會(huì)對(duì)理解它更有幫助椅亚。
總結(jié)
Navigation 是一個(gè)優(yōu)秀的庫(kù)限番,這從API上無(wú)法體現(xiàn),因?yàn)樗推渌鼉?yōu)秀的三方 Fragment 管理庫(kù) 都能達(dá)到 固定的目標(biāo)什往。
并且扳缕,隨著技術(shù)的不斷發(fā)展,它們也早晚會(huì)被歷史所淹沒别威,我們能夠做到的躯舔,就是使用API的同時(shí),學(xué)習(xí)它的思想省古,并收為己用粥庄。
--------------------------廣告分割線------------------------------
系列文章
爭(zhēng)取打造 Android Jetpack 講解的最好的博客系列:
- Android官方架構(gòu)組件Lifecycle:生命周期組件詳解&原理分析
- Android官方架構(gòu)組件ViewModel:從前世今生到追本溯源
- Android官方架構(gòu)組件LiveData: 觀察者模式領(lǐng)域二三事
- Android官方架構(gòu)組件Paging:分頁(yè)庫(kù)的設(shè)計(jì)美學(xué)
- Android官方架構(gòu)組件Paging-Ex:為分頁(yè)列表添加Header和Footer
- Android官方架構(gòu)組件Paging-Ex:列表狀態(tài)的響應(yīng)式管理
- Android官方架構(gòu)組件Navigation:大巧不工的Fragment管理框架
- Android官方架構(gòu)組件DataBinding-Ex:雙向綁定篇
Android Jetpack 實(shí)戰(zhàn)篇:
關(guān)于我
Hello,我是卻把清梅嗅,如果您覺得文章對(duì)您有價(jià)值波势,歡迎 ??橄教,也歡迎關(guān)注我的個(gè)人博客或者Github。
如果您覺得文章還差了那么點(diǎn)東西训堆,也請(qǐng)通過關(guān)注督促我寫出更好的文章——萬(wàn)一哪天我進(jìn)步了呢?