寫在前面
Google在2018年就推出了Jetpack組件庫,但是直到今天我才給重視起來,這真的不得不說是一件讓人遺憾的事。過去幾年的空閑時間里,我一直在嘗試做一套自己的組件庫,幫助自己快速開發(fā),雖然也聽說過Jetpack,但是壓根兒也沒去了解,但是其實自己已經(jīng)無形之中用到過很多Jetpack中的庫了,只是自己不知道,比如說databinding、viewmodel、camerax等等
所以我打算推出一個Jetpack的學(xué)習(xí)記錄,今天是第一個組件:Navigation
老規(guī)矩,文末有demo的源碼(永久0積分)
demo效果
正文
關(guān)于Navigation
據(jù)通義千問的說法:
Android Jetpack Navigation組件是Google推出的一個用于簡化Android應(yīng)用導(dǎo)航的庫。旨在提供一種結(jié)構(gòu)化和統(tǒng)一的方式來管理應(yīng)用程序中的屏幕切換和導(dǎo)航流程,特別是對于基于Fragment的應(yīng)用。
關(guān)于Navigation,我覺得可能大家在生活里對于它的功能并不會陌生,拿微信來說,底部有四個按鈕,分別是“微信”、“通訊錄”、“發(fā)現(xiàn)”、“我”,如下圖
當(dāng)我們分別點擊四個按鈕的時候,界面區(qū)域也會隨之切換到對應(yīng)的頁面。這就是一種比較常見的底部導(dǎo)航功能。當(dāng)然這種結(jié)合底部導(dǎo)航欄的fragment切換只是Navigation能夠?qū)崿F(xiàn)的其中一種,還有其他的并不需要底部導(dǎo)航欄的,比如說登錄模塊,登錄模塊可能包含著登錄、注冊和重置密碼這三個子模塊,那么按照UI的設(shè)計,就需要三張頁面去實現(xiàn),我們知道可以使用一個Activity去嵌套三個Fragment去實現(xiàn),那么顯然這種純fragment切換,這也是Navigation可以實現(xiàn)的范疇
整體設(shè)計思路
今天展示Navigation的使用的demo的整體設(shè)計思路為:LoginActivity+MainActivity
其中LoginActivity包含著LoginFragment、RegisterFragment、ResetPasswordFragment
其中MainActivity包含HomeFragment、ContactFragment、FindFragment、MeFragment
編碼開始
我的環(huán)境:AndroidStudio 4.2.2
(1)引用
項目級build.gradle
dependencies {
...
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.5.0"
...
}
模塊級build.gradle
plugins {
....
id 'androidx.navigation.safeargs.kotlin'
}
dependencies {
...
//Navigation
implementation "androidx.navigation:navigation-fragment-ktx:2.4.2"
implementation "androidx.navigation:navigation-ui-ktx:2.4.2"
}
(2)創(chuàng)建導(dǎo)航文件
步驟如下:
按照這樣的步驟,即可創(chuàng)建login_nav_graph.xml和main_nav_graph.xml兩個導(dǎo)航文件,如圖:
此時,兩份文件里內(nèi)容還是空的,具體內(nèi)容還需要自行添加
AndroidStudio支持可視化添加fragment以及相互之間的導(dǎo)航關(guān)系,這點真的是非常方便
1. 點擊加號,可以添加fragment到導(dǎo)航文件
我們首先把使用到的fragment全部添加到導(dǎo)航文件中,如下圖:
這里先看一下AndroidStudio自動為我們生成的代碼
<fragment
android:id="@+id/loginFragment"
android:name="com.swy.navigationdemo.login.LoginFragment"
android:label="fragment_login"
tools:layout="@layout/fragment_login" />
?這個fragment標(biāo)簽,與我們拖進(jìn)來的三個fragment是一一對應(yīng)的,它包含了四個參數(shù)
- id:比唯一標(biāo)識符,可供本文件其他地方調(diào)用
- name:是對應(yīng)Fragment的路徑
- label:是對應(yīng)Fragment的一個標(biāo)簽
- tools:layout:對應(yīng)Fragment的布局文件
還有另一個屬性也是值得關(guān)注的,就是最外層navigation標(biāo)簽的
app:startDestination="@id/loginFragment"
這表明了,在LoginActivity中,默認(rèn)優(yōu)先顯示的是LoginFragment,
說明:我們首次添加的Fragment會被默認(rèn)為優(yōu)先顯示的Fragment,即如果我這里優(yōu)先添加LoginFragment到導(dǎo)航圖,navigation自動生成app:startDestination="@id/loginFragment",那么如果我首先把RegisterFragment添加進(jìn)導(dǎo)航圖,那么這個屬性就會是app:startDestination="@id/registerFragment"
2.增加Fragment間的導(dǎo)航關(guān)系?
點LoginFragment,框體右邊中部會出現(xiàn)一個圓環(huán)
點擊該圓環(huán),并拖動,就會出現(xiàn)一條藍(lán)線,將該藍(lán)線指向RegisterFragment后松手
此時就會看到,LoginFragment到RegisterFragment的導(dǎo)航關(guān)系被建立了,觀察新增的action代碼
<action
android:id="@+id/action_loginFragment_to_registerFragment"
app:destination="@id/registerFragment" />
- ?id:唯一標(biāo)識符,可供Activity調(diào)用
- destination:用來表示跳轉(zhuǎn)的目的地,可見此時這個action表示的是跳轉(zhuǎn)到registerFragment。需要說明的是,這個跳轉(zhuǎn)每次都會新建實例,也就是我可以從LoginFragment跳轉(zhuǎn)到LoginFragment,但是這兩個LoginFragment是不同的實例,也就是棧內(nèi)會同時存在兩個不同的LoginFragment。
其他的屬性:
- app:launchSingleTop:類似于Android活動中
singleTop
啟動模式,當(dāng)該屬性為true時,如果目標(biāo)Fragment已經(jīng)在回退棧的頂部(即用戶最近訪問的Fragment),那么Navigation組件將不會創(chuàng)建新的Fragment實例,而是重用已經(jīng)存在的那個Fragment實例。如果目標(biāo)Fragment已經(jīng)在回退棧中,但不在棧頂,那么app:launchSingleTop
屬性將不會起作用。在這種情況下,即使app:launchSingleTop
設(shè)置為true
,Navigation組件也會創(chuàng)建一個新的目標(biāo)Fragment實例并將其推送到回退棧的頂部。app:popUpTo:
出棧直到某個元素。比如目前棧內(nèi)有fragment1 - fragment2 - fragment3,當(dāng)我在fragment4中定義了app:popUpTo:"@id/fragment1"時,那么fragment2和fragment3會被出棧,最終棧內(nèi)情況為fragment1 - fragment4。- app:popUpToInclusive:這個屬性是配合上面的
app:popUpTo使用的,
用來判斷到達(dá)指定元素時是否把指定元素也出棧。還是以上面的例子來說明,如果該值為true,那么作為指定元素,fragment1也會被出棧,最終棧內(nèi)只剩下fragment4.
- app:enterAnim、app:exitAnim、app:popEnterAnim、app:popExitAnim:這四個屬性都是跳轉(zhuǎn)動畫相關(guān)的,前兩個用來配置移動到目的地的動畫,后兩個配置離開目的地的動畫。
舉例說明fragment1跳轉(zhuǎn)到fragment2:
(1)enterAnim和exitAnim發(fā)生于fragment1跳轉(zhuǎn)到fragment2的過程中:
enterAnim是fragment2的入場動畫、exitAnim是fragment1的離場動畫
(2)popEnterAnim和popExitAnim發(fā)生于fragment2返回到fragment1的過程中:
popEnterAnim為返回發(fā)生后,fragment1的入場動畫
popExitAnim為返回時,fragment2的離場動畫
按照我們最開始設(shè)計的跳轉(zhuǎn)關(guān)系去完成導(dǎo)航文件,最終是這樣的:
即:默認(rèn)展示登錄頁面,登錄頁面可以跳轉(zhuǎn)到注冊頁面或者是重置密碼頁面,同時在注冊頁面和重置密碼頁面也可以返回到登陸頁面
同理,我們完成main_nav_graph的導(dǎo)航關(guān)系,如下
因為我們仿照的是微信的底部導(dǎo)航欄,home、contact、find、me四個fragment其實是平級的,它們之間并不存在導(dǎo)航關(guān)系。當(dāng)然我們也可以根據(jù)自己的實際業(yè)務(wù)使用不同的action來設(shè)計不同維度和復(fù)雜度的導(dǎo)航關(guān)系。?
至此,導(dǎo)航文件也有了,剩下的就是創(chuàng)建一個支持導(dǎo)航關(guān)系的容器了
(3)導(dǎo)航主機(jī)
以LoginActivity為例
布局文件:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".LoginActivity">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/login_nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/login_nav_graph"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
創(chuàng)建FragmentContainerView作為導(dǎo)航主機(jī)(Navigation Host),這里有幾個地方需要說明:
-
android:name="androidx.navigation.fragment.NavHostFragment"是固定的寫法
- app:defaultNavHost的作用是將該
FragmentContainerView
標(biāo)記為默認(rèn)的導(dǎo)航主機(jī),這意味著這個FragmentContainerView
會攔截系統(tǒng)的后退按鈕事件。當(dāng)用戶點擊后退按鈕時,Navigation組件會按照導(dǎo)航圖中的歷史記錄進(jìn)行后退操作,而不是直接關(guān)閉Activity。并且在一個Activity中,通常只需要一個NavHostFragment
作為導(dǎo)航主機(jī)。設(shè)置app:defaultNavHost="true"
可以確保只有這一個NavHostFragment
響應(yīng)導(dǎo)航操作和后退按鈕事件,避免多個NavHostFragment
之間的沖突。 -
app:navGraph是將導(dǎo)航文件與導(dǎo)航主機(jī)相關(guān)聯(lián)
LoginActivity:
class LoginActivity : AppCompatActivity() {
private lateinit var binding: ActivityLoginBinding
private lateinit var navController:NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding.root)
val navHostFragment = supportFragmentManager.findFragmentById(R.id.login_nav_host_fragment) as NavHostFragment
navController = navHostFragment.navController
}
public fun getNavController(): NavController {
return navController
}
override fun onSupportNavigateUp(): Boolean {
return findNavController(R.id.login_nav_host_fragment).navigateUp()
}
}
核心代碼:獲取NavController
說明,這里聲明了一個方法,將獲取到的navController返回了出去,主要是供Fragment中進(jìn)行調(diào)用,因為這里Activity只是容器,具體的UI交互,是在對應(yīng)的Fragment上面的,以LoginFragment為例:
class LoginFragment : Fragment() {
private lateinit var binding: FragmentLoginBinding
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentLoginBinding.inflate(layoutInflater)
val activity = requireActivity() as LoginActivity
navController = activity.getNavController()
initListener()
return binding.root
}
private fun initListener() {
binding.login.setOnClickListener {
val intent = Intent(activity, MainActivity::class.java)
startActivity(intent)
}
binding.register.setOnClickListener {
navController.navigate(R.id.action_loginFragment_to_resetPasswordFragment)
}
binding.reset.setOnClickListener {
navController.navigate(R.id.action_loginFragment_to_resetPasswordFragment)
}
}
}
通過這兩行代碼,fragment獲取到了activity的navcontroller
val activity = requireActivity() as LoginActivity
navController = activity.getNavController()
之后就可以操作導(dǎo)航事件了,如下
binding.register.setOnClickListener {
navController.navigate(R.id.action_loginFragment_to_resetPasswordFragment)
}
binding.reset.setOnClickListener {
navController.navigate(R.id.action_loginFragment_to_resetPasswordFragment)
}
說明,這里面的
R.id.action_loginFragment_to_resetPasswordFragment
和
R.id.action_loginFragment_to_resetPasswordFragment
就是login_nav_graph.xml文件中定義的兩個action的id
我相信寫到這里,你基本上就能夠看出來整個的調(diào)用機(jī)制了
?(4)Navigation+BottomNavigationView實現(xiàn)底部導(dǎo)航
上面講了普通的fragment切換,那么關(guān)于帶底部導(dǎo)航欄的切換,也還是很有必要說明以下的
主界面布局
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/main_nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@+id/bottomNavigationView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0"
app:navGraph="@navigation/main_nav_graph" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottomNavigationView"
android:layout_width="match_parent"
android:layout_height="60dp"
app:menu="@menu/main_menu"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
顯然,頁面中只是增加了BottomNavigationView,對應(yīng)的UI結(jié)構(gòu)如下
?說明:
我這里使用了menu來實現(xiàn)了底部導(dǎo)航欄的幾個item內(nèi)容的導(dǎo)入,代碼如下
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/homeFragment"
android:icon="@mipmap/message"
android:title="首頁"
app:showAsAction="ifRoom" />
<item
android:id="@+id/contactFragment"
android:icon="@mipmap/contact"
android:title="聯(lián)系人"
app:showAsAction="ifRoom" />
<item
android:id="@+id/findFragment"
android:icon="@mipmap/find"
android:title="發(fā)現(xiàn)"
app:showAsAction="ifRoom" />
<item
android:id="@+id/meFragment"
android:icon="@mipmap/me"
android:title="我"
app:showAsAction="ifRoom" />
</menu>
重點說明:這里面四個item的id并不是隨意定義的,一定要與main_nav_graph.xml文件中對應(yīng)的幾個fragment的id保持一致,否則,點擊底部導(dǎo)航欄的按鈕,是無法觸發(fā)對應(yīng)的fragment切換的?。?!如下
?這里面只是UI上對應(yīng)了,如何讓bottomnavigationview與navcontroller也關(guān)聯(lián)到一起呢
MainActivity.class
class MainActivity : AppCompatActivity() {
private lateinit var binding:ActivityMainBinding
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val navHostFragment = supportFragmentManager.findFragmentById(R.id.main_nav_host_fragment) as NavHostFragment
navController = navHostFragment.navController
binding.bottomNavigationView.setupWithNavController(navController)
}
override fun onSupportNavigateUp(): Boolean {
return findNavController(R.id.main_nav_host_fragment).navigateUp()
}
}
核心代碼就是這一句了:
binding.bottomNavigationView.setupWithNavController(navController)
補(bǔ)充內(nèi)容
Fragment間數(shù)據(jù)通信的兩種方式
先看效果
說明:
LoginFragment跳轉(zhuǎn)到RegisterFragment使用SafeArgs方式
LoginFragment跳轉(zhuǎn)到ResetpasswordFragment使用Bundle方式?
(1)SafeArgs(推薦)
Android官方推薦使用Safe Args來實現(xiàn)Fragment間數(shù)據(jù)通信,原因主要包括以下幾個方面:
類型安全: Safe Args提供了類型安全的方式來傳遞參數(shù)。在navigation graph XML文件中定義的每個參數(shù)都有明確的數(shù)據(jù)類型(例如字符串、整數(shù)、布爾值等)。這將自動為這些參數(shù)生成對應(yīng)的Args類,并提供get和set方法,從而確保在編譯時就能捕獲到類型不匹配的問題,而不是在運行時才出現(xiàn)崩潰。
清晰性與可讀性: 在navigation graph中直接指定參數(shù)及其類型使得整個應(yīng)用導(dǎo)航結(jié)構(gòu)更加清晰。通過查看XML文件,開發(fā)者可以很容易地了解哪些參數(shù)在Fragment之間傳遞,以及它們的類型是什么。
減少代碼量和錯誤: 使用Safe Args不需要手動創(chuàng)建和解析Bundle對象來傳遞數(shù)據(jù),這大大減少了出錯的可能性。自動生成的Args類簡化了參數(shù)傳遞過程,使得開發(fā)者可以直接操作對象而非鍵值對,提高了編碼效率。
生命周期感知: Safe Args配合Navigation組件一起使用時,能夠更好地適應(yīng)Android組件的生命周期變化。即使目標(biāo)Fragment因為配置更改(如屏幕旋轉(zhuǎn))而重新創(chuàng)建,傳遞的參數(shù)也能得到妥善保存和恢復(fù)。
易于維護(hù): 隨著項目規(guī)模的增長,Safe Args能幫助保持代碼的整潔和組織有序。當(dāng)需要修改或添加新的參數(shù)時,只需要在navigation graph文件中更新即可,同時會自動反映到相關(guān)的Args類中,無需在多個地方手動同步修改。
具體的使用過程如下:
1.引用插件
項目級build.gradle
buildscript {
...
dependencies {
...
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.5.3"
}
}
模塊級build.gradle
plugins {
...
id 'androidx.navigation.safeargs.kotlin'
}
引用完成之后,Sync項目,Rebuild項目
2.實際使用:修改login_nav_graph.xml文件,增加argument參數(shù)
<fragment
android:id="@+id/loginFragment"
android:name="com.swy.navigationdemo.login.LoginFragment"
android:label="fragment_login"
tools:layout="@layout/fragment_login">
<action
android:id="@+id/action_loginFragment_to_registerFragment"
app:destination="@id/registerFragment">
<argument
android:name="data1"
app:argType="string"
android:defaultValue=""/>
</action>
<action
android:id="@+id/action_loginFragment_to_resetPasswordFragment"
app:destination="@id/resetPasswordFragment">
</action>
</fragment>
說明:我在自己環(huán)境上調(diào)試的時候,發(fā)現(xiàn)我定義的argument屬性在定義name的時候,總是會提示'xxx' is not a valid destination for tag 'argument'這樣的錯誤,網(wǎng)上也沒有找到相關(guān)的解釋和解決方法,但是經(jīng)過實際測試,這個地方報紅并不影響使用,如圖
LoginFragment.java
binding.register.setOnClickListener {
val data1 = "這是使用safe args方式從登錄界面?zhèn)鬟f的數(shù)據(jù)"
navController.navigate(LoginFragmentDirections.actionLoginFragmentToRegisterFragment(data1))
}
RegisterFragment.java
val data1 = arguments?.getString("data1")
binding.textData1.text = data1
?說明:
上面展示的是單一參數(shù),多參數(shù)也是支持的,如下:
比如這里,我又增加了一個data3,那么在LoginFragment中,使用逗號隔開兩個參數(shù)即可,如下
binding.register.setOnClickListener {
val data1 = "這是使用safe args方式從登錄界面?zhèn)鬟f的數(shù)據(jù)"
val data3 = "data3"
navController.navigate(LoginFragmentDirections.actionLoginFragmentToRegisterFragment(data1,data3))
}
RegisterFragment
val data1 = arguments?.getString("data1")
val data3 = arguments?.getString("data3")
binding.textData1.text = data1+data3
最終的效果?
可見, 上面說的safeargs的優(yōu)點,確實是做到了易于維護(hù)
易于維護(hù): 隨著項目規(guī)模的增長,Safe Args能幫助保持代碼的整潔和組織有序。當(dāng)需要修改或添加新的參數(shù)時,只需要在navigation graph文件中更新即可,同時會自動反映到相關(guān)的Args類中,無需在多個地方手動同步修改。
(2)Bundle
LoginFragment
binding.reset.setOnClickListener {
val data2 = "這是使用普通Bundle方式從登錄界面?zhèn)鬟f的數(shù)據(jù)"
val bundle = Bundle();
bundle.putString("data2",data2)
navController.navigate(R.id.action_loginFragment_to_resetPasswordFragment,bundle)
}
ResetPasswordFragment
val bundle = arguments
val data2 = bundle?.getString("data2")
binding.textData2.text = data2
至此,Navigation fragment間數(shù)據(jù)通信的兩種方式的簡單介紹就結(jié)束了
說明:關(guān)于safeargs的使用,前面講解的并不是全部的實現(xiàn)方式,只是其中的一種,就比如我這里從LoginFragment跳轉(zhuǎn)到RegisterFragment,我的argument是在LoginFragment里面定義的,我看網(wǎng)上還有講解的是也可以在RegisterFragment里面定義,包括Bundle也可以與argument屬性有聯(lián)動關(guān)系等等之類的吧,大家有興趣可以都了解一下
到目前為止,簡單的demo算是初具雛形,源碼如下文章來源:http://www.zghlxwxcb.cn/news/detail-785962.html
demo 源碼文章來源地址http://www.zghlxwxcb.cn/news/detail-785962.html
到了這里,關(guān)于Android Jetpack學(xué)習(xí)系列——Navigation的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!