Android官方架構(gòu)組件Navigation:大巧不工的Fragment管理框架

本文已授權(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第煮,它最終的效果是這樣:

sample.gif

這是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 并非是通過原生的 FragmentManagerFragmentTransaction 進(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):

UML類圖

讓我們換個(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 Jetpack 實(shí)戰(zhàn)篇


關(guān)于我

Hello,我是卻把清梅嗅,如果您覺得文章對(duì)您有價(jià)值波势,歡迎 ??橄教,也歡迎關(guān)注我的個(gè)人博客或者Github

如果您覺得文章還差了那么點(diǎn)東西训堆,也請(qǐng)通過關(guān)注督促我寫出更好的文章——萬(wàn)一哪天我進(jìn)步了呢?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末白嘁,一起剝皮案震驚了整個(gè)濱河市坑鱼,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌絮缅,老刑警劉巖鲁沥,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件呼股,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡画恰,警方通過查閱死者的電腦和手機(jī)彭谁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來允扇,“玉大人缠局,你說我怎么就攤上這事“剑” “怎么了甩鳄?”我有些...
    開封第一講書人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)额划。 經(jīng)常有香客問我妙啃,道長(zhǎng),這世上最難降的妖魔是什么俊戳? 我笑而不...
    開封第一講書人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任揖赴,我火速辦了婚禮,結(jié)果婚禮上抑胎,老公的妹妹穿的比我還像新娘燥滑。我一直安慰自己,他們只是感情好阿逃,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開白布铭拧。 她就那樣靜靜地躺著,像睡著了一般恃锉。 火紅的嫁衣襯著肌膚如雪搀菩。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,111評(píng)論 1 285
  • 那天破托,我揣著相機(jī)與錄音肪跋,去河邊找鬼。 笑死土砂,一個(gè)胖子當(dāng)著我的面吹牛州既,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播萝映,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼吴叶,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了序臂?” 一聲冷哼從身側(cè)響起蚌卤,我...
    開封第一講書人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后造寝,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡吭练,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年诫龙,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鲫咽。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡签赃,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出分尸,到底是詐尸還是另有隱情锦聊,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布箩绍,位于F島的核電站孔庭,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏材蛛。R本人自食惡果不足惜圆到,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望卑吭。 院中可真熱鬧芽淡,春花似錦、人聲如沸豆赏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)掷邦。三九已至白胀,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間耙饰,已是汗流浹背纹笼。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留苟跪,地道東北人廷痘。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像件已,于是被迫代替她去往敵國(guó)和親笋额。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,518評(píng)論 25 707
  • Fragment要點(diǎn) 1篷扩、Fragment作為Activity界面的一部分組成出現(xiàn) 2兄猩、可以在一個(gè)Activity...
    玉圣閱讀 1,221評(píng)論 0 16
  • 又回到了學(xué)生時(shí)代。 頂著困意起床,洗臉?biāo)⒀廊ゲ賵?chǎng)跑步枢冤,不時(shí)地碰到我的同學(xué)鸠姨,彼此紛紛的打著招呼,新的一天就是...
    拾夢(mèng)齋齋主閱讀 222評(píng)論 0 0
  • 最近睡眠質(zhì)量不好,醒的早淹真,起的晚讶迁,還老做夢(mèng),夢(mèng)醒了核蘸,還是想睡懶覺巍糯,一大堆事情等著做,不要忘記最初的夢(mèng)想啊
    張嚴(yán)閱讀 276評(píng)論 0 0
  • 好像不管發(fā)生了什么客扎,時(shí)間永遠(yuǎn)向前總被認(rèn)為是顯而易見的祟峦,回首那段時(shí)間總會(huì)慶幸自己堅(jiān)持下來! 接觸過各式各樣的人徙鱼,也有...
    廖小魚閱讀 505評(píng)論 1 3