由于Android的開源性,在開始的幾年呈現(xiàn)出了百家齊放的盛況,層出不窮的API和以及官方的API各自大放異彩,在豐富了android生態(tài)的同時也帶來了一個很嚴(yán)重的問題,即android 的碎片化和規(guī)范化的問題。碎片化主要集中于國內(nèi)的各大手機廠商的多種屏幕尺寸、多種手機分辨率、多種機型的多樣性上,而規(guī)范化則是集中于開發(fā)生態(tài)上面,由于Google官方的弱約束,很多時候大家都隨心所欲以業(yè)務(wù)驅(qū)動技術(shù)去進行開發(fā),導(dǎo)致沒有一個很好的開發(fā)整體框架?;诖?,Google官方在2018年發(fā)布了一系列輔助開發(fā)Android開發(fā)者的實用工具,合成JetPack。
一、什么是JetPack?
根據(jù)官方介紹,JetPack是一個由多個庫組成的套件,可幫助開發(fā)者遵循最佳做法,減少樣板代碼并編寫可在各種Android版本和設(shè)備中一致運行的代碼,讓開發(fā)者可將精力集中于真正重要的編碼工作。下面是官方關(guān)于Jetpack的描述圖

1. 基礎(chǔ)組件
(1) AppCompat:使得支持較低的 Android 版本。從以前繼承 Activity 到現(xiàn)在繼承AppCompatActivity 就是屬于這一部分
(2) Android KTX:Kotlin 的擴展支持庫
(3) Multidex:多 dex 文件支持
(4) Test:測試支持庫
2. 架構(gòu)組件
(1) Data Binding:MVVM 的一種實踐
(2) Lifecycles:管理你的 Activity 和 Fragment 生命周期
(3) LiveData:通過觀察者模式感知數(shù)據(jù)變化,類比 RxJava
(4) Navigation:處理 Fragment 導(dǎo)航相關(guān)邏輯
(5) Paging:分頁數(shù)據(jù)加載方案
(6) Room:官方 ORM 庫
(7) ViewModel:通過數(shù)據(jù)驅(qū)動 V 視圖發(fā)生改變
(8) WorkManager:管理后臺任務(wù)
3. 行為組件
(1) DownloadManager:管理下載任務(wù)
(2) Media App:多媒體播放和一些向后兼容的API。主要包含 MediaPlayer 和 ExoPlayer
(3) Notifications:提供向后兼容的通知 API,支持 Wear 和 Auto
(4) Permissions:權(quán)限管理,這個應(yīng)該都接觸過。用于檢查和請求應(yīng)用權(quán)限
(5) Settings:Preference 相關(guān) API?;久總€應(yīng)用都會用到
(6) Share Action:提供分享操作。這塊在國內(nèi)使用的不多,都是自己封裝或者采用第三方方案
(7) Slices:可以讓應(yīng)用通過外部(其他 APP)顯示 APP 界面(通過設(shè)備自帶的搜索,語音助手等)
4. 界面組件
(1) Animations and Transitions:動畫,界面轉(zhuǎn)場等
(2) Auto:針對車輛的標(biāo)準(zhǔn)化界面和模式
(3) Emoji:表情符號相關(guān)
(4) Fragment:基礎(chǔ)概念
(5) Layout:基礎(chǔ)概念
(6) Palette-Colors:調(diào)色板
(7) TV:Android TV 開發(fā)相關(guān)
(8) Wear:可穿戴設(shè)備(目前主要是手表)開發(fā)相關(guān)
二、JetPack到底值不值得我們?nèi)?yīng)用?
說實話,這個問題仁者見仁智者見智,因為沒有什么東西是絕對的好或者不好,看你自己的需求而定,就像MVC、MVP、MVVM框架一樣,沒有說哪種模式一定最好,很多時候是要根據(jù)實際情況來定。所以,同等道理,Jetpack到底要不要用,怎么用看開發(fā)需求,但是對于我們開發(fā)者來說,掌握它卻是必不可少的,只有完全的掌握它,知道了它的優(yōu)點和缺點才能更好的做出理性的判斷在實際的開發(fā)者到底要不要用它。
三、進入正題——Jetpack組件之一Navigation
1.什么是Navigation
Navigation是一個可簡化的Android導(dǎo)航的庫和插件,換句話說,Navigation是用來管理Fragment的切換的,并且是通過可視化的方式來進行管理的。
2.Navigation的優(yōu)缺點
優(yōu)點
- 處理Fragment的切換
- 默認(rèn)情況下正確處理Fragment的前進和后退
- 為過渡和動畫提供標(biāo)準(zhǔn)化的資源
- 可以綁定Toolbar/BottomNavigationView/ActionBar等
- 數(shù)據(jù)傳遞時提供類型安全性(使用SafeArgs)
- ViewModel支持
缺點
- fragment切換后底層會調(diào)用replace方法導(dǎo)致會被不斷銷毀,無法保存上一次的狀態(tài)
3.Navigation的使用
Navigation的使用相對來說比較簡答,分為以下幾步:
(1)引入依賴
(2)創(chuàng)建多個要調(diào)配的Fragment
(3)在res下面創(chuàng)建navigation文件夾,并創(chuàng)建navigation文件
(4)在主Activity里面的XML文件里面引入指定的Fragment
基本上大體步驟就那么幾步,現(xiàn)在我們就一個一個來看。
dependencies {
// Java引入
implementation "androidx.navigation:navigation-fragment:2.2.2"
implementation "androidx.navigation:navigation-ui:2.2.2"
// kotlin引入
implementation 'androidx.navigation:navigation-fragment-ktx:2.2.2'
implementation 'androidx.navigation:navigation-ui-ktx:2.2.2'
}
步驟二:創(chuàng)建多個要調(diào)配的Fragment
這里我創(chuàng)建了3個Fragment,第一個是歡迎頁面的WelcomeFragment,第二個是注冊的RegisterFragment,第三個是登錄頁面的LoginFragment。分別如下:
歡迎頁面的WelcomeFragment
class WelcomeFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_welcome, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.findViewById<Button>(R.id.register_jump).setOnClickListener {
val navOption = navOptions {
anim {
enter = R.anim.common_slide_in_right
exit = R.anim.common_slide_out_left
popEnter = R.anim.common_slide_in_left
popExit = R.anim.common_slide_out_right
}
}
findNavController().navigate(R.id.registerFragment, null, navOption)
}
view.findViewById<Button>(R.id.login_jump).setOnClickListener {
val navOption = navOptions {
anim {
enter = R.anim.common_slide_in_right
exit = R.anim.common_slide_out_left
popEnter = R.anim.common_slide_in_left
popExit = R.anim.common_slide_out_right
}
}
val bundle = LoginFragmentArgs.Builder().setLoginInfo("jack").build().toBundle()
findNavController().navigate(R.id.loginFragment,bundle,navOption)
}
}
}
注冊的RegisterFragment
class RegisterFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_register, container, false)
}
}
登錄頁面的LoginFragment
class LoginFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_login, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val bundle = arguments
if (bundle!=null){
val loginTag = LoginFragmentArgs.fromBundle(bundle).loginInfo
view.findViewById<TextView>(R.id.login_info).text = loginTag
}
}
}
步驟三:在res下面創(chuàng)建navigation文件夾,并創(chuàng)建navigation文件
需要注意的是navigation文件夾必須在res文件夾下面并且名稱為固定寫法,這里我新建了一個splash_navigation.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/splash_navigation"
app:startDestination="@id/welcomeFragment">
<fragment
android:id="@+id/welcomeFragment"
android:name="com.jack.androidjetpack.login.WelcomeFragment"
android:label="fragment_welcome"
tools:layout="@layout/fragment_welcome">
<action
android:id="@+id/action_welcomeFragment_to_registerFragment"
app:destination="@id/registerFragment" />
<action
android:id="@+id/action_welcomeFragment_to_loginFragment"
app:destination="@id/loginFragment" />
</fragment>
<fragment
android:id="@+id/registerFragment"
android:name="com.jack.androidjetpack.login.RegisterFragment"
android:label="fragment_register"
tools:layout="@layout/fragment_register" />
<fragment
android:id="@+id/loginFragment"
android:name="com.jack.androidjetpack.login.LoginFragment"
android:label="fragment_login"
tools:layout="@layout/fragment_login">
<argument
android:name="loginInfo"
android:defaultValue="defaultValue"
app:argType="string" />
</fragment>
</navigation>
這個XML文件是Navigation里面一個比較重要的文件,下面我們慢慢來分析一下里面包含了哪些東西:
| 名稱 | 含義 |
|---|---|
| <navigation>/</navigation> | 文件必須以<navigation>開頭,以</navigation>結(jié)尾,標(biāo)識這是一個Navigation文件 |
| app:startDestination | 默認(rèn)的起始的fragment所對應(yīng)的id,此處意味著WelcomeFragment作為起始的Fragment |
| <fragment> | Navigation所操作的都是Fragment |
| <fragment android:id | 對應(yīng)的Fragment的id |
| <fragment android:name | 對應(yīng)的Fragment,注意這里的值必須是完整的Fragment的路徑 |
| <fragment android:layout | 對應(yīng)的Fragment的布局文件 |
| <fragment <action | Fragment對應(yīng)的跳轉(zhuǎn)行為 |
| <fragment <argument | Fragment傳參 |
| <argument | Fragment傳參 |
| <argument android:name | Fragment傳參的key |
| <argument android:defaultValue | Fragment傳參的參數(shù)默認(rèn)值 |
| <argument app:argType | Fragment傳參的參數(shù)對應(yīng)的類型 |
步驟四:在主Activity里面的XML文件里面引入指定的Fragment
主Activity很簡單,沒有寫一行代碼
/**
* 歡迎頁面
*/
class WelcomeActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_welcome)
}
}
對應(yīng)的activity_welcome文件
<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=".login.WelcomeActivity">
<fragment
android:id="@+id/welcome_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/splash_navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
不知道大家發(fā)現(xiàn)沒有,在WelcomeActivity對應(yīng)的XML文件里面我們并沒有引用我們自己創(chuàng)建的Fragment,而是引用了一個NavHostFragment,那么NavHostFragment到底是什么呢?這里就要介紹幾個概念了:
(1) NavHostFragment:NavHostFragment是我們引入的Navigation組件提供的一個Fragment,其實現(xiàn)了NavHost接口,可以將它理解為系統(tǒng)默認(rèn)的一個Fragment模板,或者是當(dāng)前Fragment的容器。
(2)app:navGraph:NavHostFragment 與導(dǎo)航圖相關(guān)聯(lián)工作由它完成,在navigation中完成到目的視圖導(dǎo)航
(3)app:defaultNavHost:是否被系統(tǒng)返回鍵攔截
需要強調(diào)的是,在fragment中android:name="androidx.navigation.fragment.NavHostFragment"是固定寫法。
基礎(chǔ)工作已經(jīng)結(jié)束了,接下來我們解決兩個問題:
- (1)Fragment的切換
- (2)Fragment的傳參
先說明的就是,fragment的切換其實就是通過replace的方法來實現(xiàn)的,相信這個做法很多人都不陌生,只不過以前是我們自己手動實現(xiàn)(包括回退棧),但是現(xiàn)在借助Navigation我們不必去手動實現(xiàn),Navigation在底部幫我們實現(xiàn)了,我們只需要按照Navigation的模板去實現(xiàn)就好了。
Fragment的切換
findNavController().navigate(R.id.registerFragment, null, navOption)
Fragment的切換很簡單,一行代碼就可以搞定,不過我這是kotlin的寫法,Java的寫法稍微復(fù)雜一點。
NavigationController navigationController = Navigation.findNavController(it)
navigationController.navigate(R.id.registerFragment, null, navOption)
但是不管是Java寫法還是kotlin寫法底層都是一樣的,這里需要用到一個很重要的東西NavigationController,顧名思義NavigationController是用來控制Navigation的操作的,最終我們切換Fragment就是依靠它來實現(xiàn)的。NavigationController提供了一個方法navigate,navigate的重載方法有很多,這里簡單介紹幾個:
// 通過傳遞我們要跳轉(zhuǎn)的Fragment來實現(xiàn)切換
// resId:目標(biāo)Fragment的id
public void navigate(@IdRes int resId) {
navigate(resId, null);
}
// 通過傳遞我們要跳轉(zhuǎn)的Fragment來實現(xiàn)切換
// resId:目標(biāo)Fragment的id
// Bundle args:傳遞給目標(biāo)Fragment的參數(shù)
public void navigate(@IdRes int resId, @Nullable Bundle args) {
navigate(resId, args, null);
}
// 通過傳遞我們要跳轉(zhuǎn)的Fragment來實現(xiàn)切換
// resId:目標(biāo)Fragment的id
// Bundle args:傳遞給目標(biāo)Fragment的參數(shù)
// NavOptions navOptions:Fragment切換時候的過渡動畫
public void navigate(@IdRes int resId, @Nullable Bundle args,
@Nullable NavOptions navOptions) {
navigate(resId, args, navOptions, null);
}
這個比較簡單,一看就懂了,關(guān)于怎么傳參放到下面去講,我們在這里先看看怎么實現(xiàn)動畫
val navOption = navOptions {
anim {
enter = R.anim.common_slide_in_right
exit = R.anim.common_slide_out_left
popEnter = R.anim.common_slide_in_left
popExit = R.anim.common_slide_out_right
}
}
實現(xiàn)動畫也很簡單,創(chuàng)建一個NavOptions在里面設(shè)置好我們想要的動畫效果
common_slide_in_right
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="200"
android:fromXDelta="100%"
android:fromYDelta="0%"
android:toXDelta="0%"
android:toYDelta="0%" />
</set>
common_slide_out_left
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="200"
android:fromXDelta="0%"
android:fromYDelta="0%"
android:toXDelta="-100%"
android:toYDelta="0%" />
</set>
common_slide_in_left
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="200"
android:fromXDelta="-100%"
android:fromYDelta="0%"
android:toXDelta="0%"
android:toYDelta="0%" />
</set>
common_slide_out_right
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="200"
android:fromXDelta="0%"
android:fromYDelta="0%"
android:toXDelta="100%"
android:toYDelta="0%" />
</set>
Fragment的傳參
Fragment的傳參方式也有兩種:
- 方式一:常規(guī)傳參
// 傳遞參數(shù)
val bundle = Bundle()
bundle.putString("loginInfo","steven")
findNavController().navigate(R.id.loginFragment,bundle,navOption)
// 獲取參數(shù)
val bundle = arguments
if (bundle!=null){
val loginTag = bundle.getString("loginInfo")
view.findViewById<TextView>(R.id.login_info).text = loginTag
}
- 方式二:使用系統(tǒng)推薦的插件Safe Args
Safe Args使用步驟:
(1)添加依賴
在項目的builder.gradle下面添加
dependencies {
...
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.0"
}
(2)在主module的builder.gradle下面添加
plugins {
...
id 'androidx.navigation.safeargs'
}
(3)在navigation文件夾下面我們創(chuàng)建的文件下面要跳轉(zhuǎn)的目標(biāo)Fragment下面設(shè)置argument。
<fragment
android:id="@+id/loginFragment"
android:name="com.jack.androidjetpack.login.LoginFragment"
android:label="fragment_login"
tools:layout="@layout/fragment_login">
<argument
// 傳遞的參數(shù)key
android:name="loginInfo"
// 默認(rèn)值
android:defaultValue="defaultValue"
// 參數(shù)對應(yīng)的類型
app:argType="string" />
</fragment>
(4)Build-> Make Project
完成后會發(fā)現(xiàn)在我們的app->build->generated->source->navigation-args->debug->com.jack.androidjetpack.login文件夾下面有一個LoginFragmentArgs文件,而這個文件正是用來傳遞參數(shù)使用的。
(5)創(chuàng)建我們的bundle并進行傳遞
val bundle = LoginFragmentArgs.Builder().setLoginInfo("jack").build().toBundle()
(6)獲取我們傳遞的值
val bundle = arguments
if (bundle != null) {
val loginTag = LoginFragmentArgs.fromBundle(bundle).loginInfo
view.findViewById<TextView>(R.id.login_info).text = loginTag
}
這樣通過插件傳值就結(jié)束了。
可以看到,可能會有人會疑惑,用Bundle不是更簡單些嗎,Safe Args 傳遞方式不僅編碼復(fù)雜,還要安裝它的插件,不會太麻煩嗎?既然是Google推薦,那當(dāng)然是有道理的這里不再啰嗦,直接貼上官方說明

感覺其實官方就說了一句話:使用Safe Args可以確保數(shù)據(jù)安全。但是說實話我還沒有太明白和我們以往直接通過bundle有什么區(qū)別。知道的同學(xué)可以在下面留言大家探討探討。