Android Jetpack 架構元件之 Navigation

任縹緲發表於2020-12-31

一、關鍵欄位的解釋

Kotlin語言環境下在Fragment裡面操作控制元件的注意事項:

onCreateView()方法和onStart()方法裡面直接通過id操作控制元件的話,會要求必須加findViewById()獲取控制元件id,否則就會報"must not be null"這個錯誤。

但是在onViewCreated()方法裡面就不用這樣。另外自定義的方法裡面也是必須獲取控制元件id。

可以這麼理解:
在Fragment裡面,除了在onViewCreated()方法裡面直接操作控制元件或者呼叫操作控制元件的方法外,其他任何地方操作控制元件或者呼叫操作控制元件的方法都是需要通過findViewById()獲取控制元件id,否者就會報上面的錯誤。

Navigation各欄位含義:

1、Activity對應的xml裡面,fragment標籤示例以及幾個關鍵字的含義:

<fragment
    android:id="@+id/nav_fragment"    	
    android:layout_width="match_parent"
    android:layout_height=" match_parent "   
    android:name="androidx.navigation.fragment.NavHostFragment"
    app:navGraph="@navigation/nav_host"    		
    app:defaultNavHost="true"/>

android:id:在Activity裡面用於通過supportFragmentManager獲取NavHostFragment物件。

android:name:是NavHostFragment,它實現了NavHost,這是一個用於放置管理destination的空檢視。指向NavHost的實現類NavHostFragment。

app:navGraph:指向Navigation graph配置檔案,用於將NavHostFragment和nav_graph.xml關聯起來。

app:defaultNavHost:預設值為false,當該值為false的時候,當前Activity中的Fragment使用了Navigation,且使用Navigation跳轉到下一個Fragment,在下一個Fragment頁面中點選了Back鍵會退出當前Activity。為true的時候是回到上一個Fragment中,如果上一個Fragment為null才會退出當前Activity。類似於我們處理WebView的back事件。

2、navigation資料夾下nav_host.xml檔案示例及裡面關鍵欄位的含義:

<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
    android:id="@+id/nav_host"
    app:startDestination="@id/homeFragment"
    tools:ignore="UnusedNavigation">
    
    <fragment
        android:id="@+id/homeFragment"
        android:name="com.androidjetpack.navigation.HomeFragment"
        android:label="home_dest"
        tools:layout="@layout/home_dest" >
        <action
            android:id="@+id/action_homeFragment_to_oneFragment"
            app:destination="@id/oneFragment" />
    </fragment>
    
    <fragment
        android:id="@+id/oneFragment"
        android:name="com.androidjetpack.navigation.OneFragment"
        android:label="OneFragment"
        tools:layout="@layout/flow_step_one_dest">
        
        <!-- action標籤不是必須的,比如後面的fragment標籤就沒有action標籤,尤其是最後一個fragment -->
		<!-- action裡面id的規則:action_本fragment的id_to_下一個fragment的id -->		
        <action
            android:id="@+id/action_oneFragment_to_deepLinkFragment"
            app:destination="@+id/deepLinkFragment"/>
    </fragment>
    
    <fragment
        android:id="@+id/deepLinkFragment"
        android:name="com.androidjetpack.navigation.DeepLinkFragment"
        android:label="DeepLinkFragment"
        tools:layout="@layout/deep_link_dest">
        
        <!-- argument標籤不是必須的 -->
        <argument
            android:name="flowStepNumber"
            app:argType="integer"
            android:defaultValue="1"/>
    </fragment>
</navigation>

name:指定Fragment的路徑。

tools:layout:指定佈局檔案,如果不配置該屬性在Design皮膚會看不見預覽。

action:字面理解就是動作,作用是fragment之間進行切換。

app:startDestination:指定這個Fragment是start-destination。

app:destination:目的地,指定要跳轉到Fragment的id。

app:id:定義這個action的id,程式碼裡執行跳轉時要用到。

enterAnim、exitAnim、popEnterAnim、popExitAnim:是頁面切換和彈窗動畫

如果某個Fragment是最後一個Fragment,那麼我們就可以直接省略他的action。

二、Navigation的使用步驟

1、app的build.gradle匯入依賴:

def nav_version = "2.3.0"
implementation "androidx.navigation:navigation-fragment:$nav_version"
implementation "androidx.navigation:navigation-ui:$nav_version"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
implementation "androidx.navigation:navigation-dynamic-features-fragment:$nav_version"
androidTestImplementation "androidx.navigation:navigation-testing:$nav_version"

並在這個build.gradle裡面新增:

apply plugin: "androidx.navigation.safeargs"

2、在專案工程的build.gradle新增如下依賴:

classpath "android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0"

3、新建幾個Fragment並關聯佈局:

ZeroFragment及對應的xml佈局檔案:

xml檔案佈局

<TextView xmlns:android=http://schemas.android.com/apk/res/android
    xmlns:tools=http://schemas.android.com/tools
    android:layout_width="match_parent"     
    android:layout_height="match_parent"
    android:orientation="vertical"    		
    android:text="@string/deep"
    android:textSize="32sp"			
    tools:ignore="HardcodedText" />

kotlin程式碼實現:

class ZeroFragment :Fragment(){
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.fragment_zero, container, false)
    }
}

OneFragment及對應的xml佈局檔案:

xml檔案佈局:

<LinearLayout xmlns:android=http://schemas.android.com/apk/res/android
    xmlns:tools=http://schemas.android.com/tools
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical"
    tools:ignore="HardcodedText">
    
    <TextView
        android:layout_width="wrap_content"       
        android:layout_height="wrap_content"
        android:layout_marginBottom="50dp"        
        android:text="@string/one"
        android:textSize="32sp" />
</LinearLayout>

kotlin程式碼實現:

class OneFragment :Fragment(){
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.fragment_one, container, false)
    }
}

HomeFragment及對應的xml佈局檔案:

xml檔案佈局:

<LinearLayout xmlns:android=http://schemas.android.com/apk/res/android    
	xmlns:tools=http://schemas.android.com/tools
    android:layout_width="match_parent"    
    android:layout_height="match_parent"
    android:gravity="center"		    
    android:orientation="vertical"
    tools:context="com.androidjetpack.navigation.HomeFragment"    
    tools:ignore="HardcodedText">
    
    <TextView
        android:layout_width="wrap_content"         
        android:layout_height="wrap_content"
        android:layout_marginBottom="50dp"        
        android:text="@string/home"
        android:textSize="32sp" />
        
    <TextView
        android:id="@+id/tvTitle"		        
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"         
        android:textSize="18sp"
        android:textColor="@color/black"	        
        android:padding="12dp"/>
</LinearLayout>

kotlin程式碼實現:

class HomeFragment :Fragment(){
    var mActivity:NavigationPrimaryActivity? = null
    override fun onAttach(context: Context) {
        super.onAttach(context)
        mActivity = (context as NavigationPrimaryActivity)
    }
    
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.fragment_home, container, false)
    }
    
    //在onCreateView()方法和onStart()方法裡面直接通過id操作控制元件的話,會要求必須加findViewById()獲取控制元件id,
    //否則就會報"must not be null"這個錯誤。
    //但是在onViewCreated()方法裡面就不用這樣。另外自定義的方法裡面也是必須獲取控制元件id。
    //可以這麼理解:在Fragment裡面,除了在onViewCreated()方法裡面直接操作控制元件或者呼叫操作控制元件的方法外,
    //其他任何地方操作控制元件或者呼叫操作控制元件的方法都是需要通過findViewById()獲取控制元件id,否者就會報上面的錯誤。
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        operationView()
    }
    
    fun operationView() {
        open1.setOnClickListener {            
        	mActivity?.printLoginfo("不能滑動,沒什麼鳥用")        
        }
        tvTitle.text = "皇者號天令"
    }
}

4、新建Navigation graph資原始檔

1)、在工程的res資料夾下建立新的資料夾:navigation
2)、單擊navigation資料夾右鍵選單選擇New->Navigation Resource File,在File name後面輸入檔名:nav_host,選擇Resource type為Navigation,點選ok即可建立Navigation graph資原始檔:nav_host.xml
3)、把新建的Fragment引入到該資原始檔內:
在這裡插入圖片描述
按照上面截圖的順序,在上面第二步建立的nav_host.xml檔案裡面依次操作。在標記有3的搜尋框處搜尋要新增的Fragment,然後從Design模式切換到Split模式,在裡面編輯功能:跳轉的順序、action行為活動細節等。最終程式碼如下:

<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
    android:id="@+id/nav_host"
    app:startDestination="@id/homeFragment"
    tools:ignore="UnusedNavigation">
    
    <fragment
        android:id="@+id/homeFragment"   
        android:name="com.androidjetpack.navigation.HomeFragment"
        android:label="home_dest"		    
        tools:layout="@layout/fragment_home" >
        
        <action
            android:id="@+id/action_homeFragment_to_oneFragment"
            app:destination="@id/oneFragment" />
    </fragment>
    
    <fragment
        android:id="@+id/oneFragment"
        android:name="com.androidjetpack.navigation.OneFragment"
        android:label="OneFragment"           
        tools:layout="@layout/fragment_one">
        
        <!--action標籤不是必須的,比如後面的fragment標籤就沒有action標籤,尤其是最後一個fragment-->
        <!--action裡面id的規則:action_本fragment的id_to_下一個fragment的id-->
        <action
            android:id="@+id/action_oneFragment_to_deepLinkFragment"
            app:destination="@+id/deepLinkFragment"/>
    </fragment>
    
    <fragment
        android:id="@+id/deepLinkFragment"
        android:name="com.androidjetpack.navigation.ZeroFragment"
        android:label="DeepLinkFragment"           
        tools:layout="@layout/fragment_zero">
        
        <!-- 下面的argument標籤不是必須的 -->
        <!--argument:類似於Activity的跳轉傳參,只不過傳參取參更加方便簡單,
        	如果某個Fragment要接收從別的Fragment傳過來的引數,那麼可以通過argument標籤處理,
        	如果我們在kotlin程式碼裡面沒有傳參,那麼我們獲取到的就是通過argument標籤設定的預設值,具體見下面。-->
		<!--下面defaultValue的型別必須與欄位argType必須保持一致,比如下面argType="integer",defaultValue="1"-->
        <argument
            android:name="flowStepNumber"            
            app:argType="integer"            
            android:defaultValue="1"/>
    </fragment>
</navigation>

argument
類似於Activity的跳轉傳參,只不過傳參取參更加方便簡單,如果某個Fragment要接收從別的Fragment傳過來的引數,那麼可以通過argument標籤處理,如果我們在kotlin程式碼裡面沒有傳參,那麼我們獲取到的就是通過argument標籤設定的預設值,具體如下:

//傳值且跳轉
val flowStepNumberArg=1
val action = HomeFragmentDirections.nextAction(flowStepNumberArg)
findNavController().navigate(action)

//取值
val safeArgs: FlowStepFragmentArgs by navArgs()
val flowStepNumber = safeArgs.flowStepNumber

5、建立menu選單:bottom_nav_menu.xml

<menu xmlns:android=http://schemas.android.com/apk/res/android
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <!--這裡的id對應著nav_host.xml裡面的fragment標籤的id-->
    <!--這裡的item的順序對應著nav_host.xml裡面的fragment的順序-->
    <!--showsAction屬性解釋:
        1、alaways:這個值會使選單項一直顯示在ActionBar上。
        2、ifRoom:如果有足夠的空間,這個值會使選單顯示在ActionBar上。
        3、never:這個值選單永遠不會出現在ActionBar是。
        4、withText:這個值使選單和它的圖示,選單文字一起顯示。
        5、collapseActionView 宣告瞭這個操作視窗應該被摺疊到一個按鈕中,
           當使用者選擇這個按鈕時,這個操作視窗展開。否則,這個操作視窗在預設的情況下是可見的,
           並且即便在用於不適用的時候,也要佔據操作欄的有效空間。-->
    <item
        android:id="@+id/homeFragment"        
        android:icon="@mipmap/home"
        android:title="home"        		
        app:showAsAction="ifRoom"/>
        
    <item
        android:id="@+id/oneFragment"        
        android:icon="@mipmap/focus"        
        android:title="one" />
        
    <item
        android:id="@+id/deepLinkFragment"   
        android:icon="@mipmap/fire"        
        android:title="deep" />
</menu>

注意:

這裡的item.id和Navigation graph資原始檔中的fragment.id必須保持一致,否則,恭喜入坑。

6、最後是Fragment的寄主NavigationPrimaryActivity和activity_navigation_primary.xml的處理:

<LinearLayout
    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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/container"
    android:orientation="vertical"
    tools:context=".navigation.NavigationPrimaryActivity">
    
	<!--android:name:是NavHostFragment,它實現了NavHost,這是一個用於放置管理destination的空檢視。指向NavHost的實現類NavHostFragment-->
	<!--app:navGraph:指向Navigation graph配置檔案,用於將NavHostFragment和nav_graph.xml關聯起來。-->
	<!--app:defaultNavHost:預設值為false,當該值為false的時候,當前Activity中的Fragment使用了Navigation,
		且使用Navigation跳轉到下一個Fragment,在下一個Fragment頁面中點選了Back鍵會退出當前Activity。
		為true的時候是回到上一個Fragment中,如果上一個Fragment為null才會退出當前Activity。類似於我們處理WebView的back事件。-->
		
    <fragment
        android:id="@+id/nav_fragment"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:name="androidx.navigation.fragment.NavHostFragment"
        app:navGraph="@navigation/nav_host"
        app:defaultNavHost="true"/>
        
    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottom_nav_view"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:background="@color/write"
        app:menu="@menu/bottom_nav_menu"/>
</LinearLayout>

上面fragment標籤的id在Activity裡面將會用到,這一點要特別注意,其餘注意事項以及說明,程式碼裡面解釋的很清楚了。

接下來就是Activity裡面的邏輯處理,一共分四步

class NavigationPrimaryActivity : BaseActivity() {
	
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_navigation_primary)
        
        //第一步獲取到NavHostFragment,這裡的nav_fragment就是本Activity對應的xml裡面Fragment標籤的id。
        val host: NavHostFragment = supportFragmentManager.findFragmentById(R.id.nav_fragment) as NavHostFragment? ?: return
        
        //第二步獲取到NavController,這一步的內容幾乎都是死的。
        val navController = host.navController
        
        //第三步配置BottomNavigationView,這裡的bottom_nav_view就是Activity對應的xml裡面BottomNavigationView控制元件的id。
        val bottomNav = findViewById<BottomNavigationView>(R.id.bottom_nav_view)
        //這裡,將導航控制元件BottomNavigationView與navController關聯起來。
        bottomNav?.setupWithNavController(navController)
        
        //第四步新增路由監聽,這一步的內容幾乎都是死的。
        navController.addOnDestinationChangedListener { _, destination, _ ->
            val dest: String = try {   
            	resources.getResourceName(destination.id)   
            } catch (e: Resources.NotFoundException) {
                destination.id.toString()
            }
            toast("Navigated to $dest")
        }
    }
}

要注意一個細節:

第三部其實分兩個細節,一是初始化BottomNavigationView的id,二是將導航控制元件BottomNavigationView與navController關聯起來。執行效果如下:
在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述

7、跳轉傳參

雖然Fragment之間傳遞引數的機率比較低,但是多少還是會用到。

1)、無參跳轉

在HomeFragment中的onViewCreated(view: View, savedInstanceState: Bundle?)函式中新增以下程式碼:

//其中,id值action_homeFragment_to_oneFragment是我們在nav_host.xml資源內新增的action屬性。
//這裡是定義了一個無參跳轉:homeFragment_to_oneFragment
open1.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.action_homeFragment_to_oneFragment))

上面定義了一個從homeFragment跳轉到oneFragment的動作。
其中R.id. action_homeFragment_to_oneFragment是我們在nav_host資源內新增的action屬性的id

R.id. action_homeFragment_to_oneFragment中:
action:意指活動;
homeFragment_to_oneFragment:從homeFragment到oneFragment

2)、帶參跳轉

這個目前尚未研究成功,無法編譯生成帶參跳轉的過載方法,待後面成功了在完成這部分內容。

最後來說一個關於帶參跳轉的問題的解決方案,問題如下:

Cannot inline bytecode built with JVM target 1.8 into bytecode that is being built with JVM target 1.6. Please specify proper '-jvm-target' option.
android {
    ......
    compileOptions {        
	    sourceCompatibility = 1.8        
	    targetCompatibility = 1.8    
    }
    
    kotlinOptions {        
    	jvmTarget = "1.8"    
    }
}

新增上面加粗標紅的程式碼。然後按下圖操作即可完成:
在這裡插入圖片描述

相關文章