這是一個(gè)新的系列文章,我們稱之為 "Modern Android Development 技巧"势篡,簡稱為 "MAD Skills"。本系列文章致力于幫助開發(fā)者們打造更好的現(xiàn)代 Android 開發(fā)體驗(yàn)祟蚀,敬請關(guān)注可霎。
今天為大家發(fā)布本系列文章中的第三篇: 在應(yīng)用中導(dǎo)航時(shí)使用 SafeArgs。如果您想回顧過去發(fā)布的內(nèi)容密强,請參考下面鏈接查看:
這篇文章主要介紹 SafeArgs茅郎,它屬于導(dǎo)航組件,并且可以在應(yīng)用不同的目的地 (界面) 之間提供更加便捷的數(shù)據(jù)傳遞功能或渤。
簡介
當(dāng)您在應(yīng)用中導(dǎo)航到不同目的地的時(shí)候系冗,可能會(huì)需要傳遞數(shù)據(jù)。為了避免使用全局對象引用薪鹦,通過數(shù)據(jù)傳遞可以實(shí)現(xiàn)更好的代碼封裝結(jié)構(gòu)掌敬,這樣不同的 fragment 或者 activity 僅需要分享它們所需的數(shù)據(jù)即可。
導(dǎo)航組件可以通過 Bundles 傳遞數(shù)據(jù)池磁,這個(gè)機(jī)制也可用于 Android 中跨 activity 傳遞數(shù)據(jù)奔害。
這里我們也可以使用同樣的方式,為要傳遞的數(shù)據(jù)創(chuàng)建一個(gè) Bundle地熄,然后在接收側(cè)將數(shù)據(jù)提取出來华临。
不過導(dǎo)航組件有更好的方法: SafeArgs。
SafeArgs 是一個(gè) gradle 插件端考,它可以幫助您在 導(dǎo)航圖 中輸入需要傳遞的數(shù)據(jù)信息雅潭。然后它會(huì)生成代碼幫您解決創(chuàng)建 Bundle 時(shí)所需完成的冗長的過程,并且在接收側(cè)提取數(shù)據(jù)却特。
您也可以直接使用 Bundle扶供,但是我們建議使用 SafeArgs。不僅僅是為了代碼更簡潔核偿,更多的是它為數(shù)據(jù)增加了類型安全的保障诚欠,使得代碼具備更好的健壯性。
為了向大家展示 SafeArgs 的效果漾岳,我將繼續(xù)使用之前在 Dialog Destinations 演示過的 Donut Tracker (甜甜圈追蹤) 應(yīng)用轰绵。如果您希望隨著文章的講解進(jìn)行同步操作,請下載 應(yīng)用源碼尼荆,并在 Android Studio 中打開左腔。
制作甜甜圈的時(shí)候到了
我們的 donut tracking 應(yīng)用又來了:
Donut Tracker 會(huì)顯示甜甜圈的列表捅儒,每個(gè)列表項(xiàng)含有名稱液样、描述和評分信息振亮,這些內(nèi)容有些是我添加的,有些是通過在點(diǎn)擊 懸浮操作按鈕 (FAB) 彈出的對話框中填寫的鞭莽。
僅僅可以添加新的甜甜圈的信息是不夠的坊秸,我還希望可以修改已有甜甜圈的信息。沒準(zhǔn)我拿到一張?zhí)鹛鹑Φ恼掌炫蛘呶蚁M嵘暗脑u分褒搔。
比較自然的實(shí)現(xiàn)方法是點(diǎn)擊列表項(xiàng),然后打開之前添加甜甜圈時(shí)的對話框喷面,然后我可以在這里修改甜甜圈的信息星瘾。但是應(yīng)用如何知道對話框里顯示哪個(gè)甜甜圈的信息呢?代碼里需要傳遞所點(diǎn)擊的列表項(xiàng)的信息惧辈。在這里琳状,它需要將對應(yīng)表項(xiàng)的 id 從列表所在的 fragment 傳遞到對話框所在的 fragment,然后對話框可以根據(jù) id 從數(shù)據(jù)庫里找到對應(yīng)甜甜圈的信息盒齿,并且填充到表單里念逞。
要傳遞 id,這里我們使用 SafeArgs
來實(shí)現(xiàn)边翁。
使用 SafeArgs
這里我需要說明一下肮柜,我已經(jīng)完成了全部的代碼,大家可以在 GitHub 的 示例 中找到完整的代碼倒彰。所以接下來我會(huì)給大家講解每個(gè)步驟审洞,并且讓大家看到示例代碼的效果,而不是簡簡單單帶著大家完成代碼待讳。
首先芒澜,我需要添加一些依賴庫。
SafeArgs 和導(dǎo)航組件的其它模塊不太一樣创淡,它本身并不是一個(gè) API痴晦,而是一個(gè)可以生成代碼的 gradle 插件。所以需要將它設(shè)置為 gradle 依賴琳彩,并且在構(gòu)建時(shí)使其能夠正確運(yùn)行來生成所需的代碼誊酌。
首先我在項(xiàng)目級的 build.gradle 文件的依賴部分中添加了下面的內(nèi)容:
def nav_version = "2.3.0"
// 獲取最新的版本號 https://developer.android.google.cn/jetpack/androidx/releases/navigation
classpath “androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version”
這里用到了 2.3.0 版本。如果您看到這篇文章的時(shí)候較晚露乏,那么應(yīng)該會(huì)有一個(gè)更新的版本供您使用碧浊。只要和您所使用的導(dǎo)航組件 API 的其它模塊的版本一致就可以了。
然后我添加了下面的內(nèi)容到 app 模塊的 build.gradle 文件中瘟仿。它使得在調(diào)用 SafeArgs 的時(shí)候可以生成所需的代碼箱锐。
apply plugin: "androidx.navigation.safeargs.kotlin"
這里,gradle 提示需要同步劳较,所以我點(diǎn)擊一下 "Sync Now"驹止。
接下來浩聋,在導(dǎo)航圖中創(chuàng)建并傳遞所需的數(shù)據(jù)。
需要數(shù)據(jù)的目標(biāo)界面是對話框 donutEntryDialogFragment
臊恋,它需要知道所需顯示的對象的信息衣洁。點(diǎn)擊目標(biāo)界面會(huì)在右側(cè)顯示相關(guān)屬性。
在 Arguments 窗格點(diǎn)擊 + 可以添加數(shù)據(jù)闸与,會(huì)彈出下面所示的對話框。這里我希望傳遞的是所需顯示的甜甜圈信息岸售,所以數(shù)據(jù)類型設(shè)置為 Long,和數(shù)據(jù)庫里的 id 的數(shù)據(jù)類型一致厂画。
需要注意的是當(dāng)我定義數(shù)據(jù)類型為 Long 的時(shí)候袱院,Nullable 的位置會(huì)變成灰色屎慢。這是因?yàn)?Java 編程語言中,基礎(chǔ)數(shù)據(jù)類型 (Integer
忽洛、Boolean
腻惠、Float
、Long
) 是基于原始數(shù)據(jù)類型 (int
欲虚、bool
集灌、float
、long
) 進(jìn)行封裝的复哆,而原始數(shù)據(jù)類型不可為空欣喧,所以我們在使用基礎(chǔ)數(shù)據(jù)類型的時(shí)候需要保證數(shù)據(jù)非空。
另外需要注意的是梯找,應(yīng)用現(xiàn)在使用該對話框添加新的元素 (我在上一篇文章 使用導(dǎo)航組件: 對話框目的地 | MAD Skills 中已經(jīng)介紹)唆阿,同時(shí)也使用該對話框編輯已有元素。所以并不一定會(huì)傳遞元素 id锈锤,當(dāng)用戶創(chuàng)建新元素的時(shí)候驯鳖,代碼應(yīng)該能夠判斷當(dāng)前并無元素信息需要顯示。所以我在對話框中 Default Value (默認(rèn)值) 的位置輸入了 -1久免,因?yàn)?-1 并不是一個(gè)有效的索引值浅辙。當(dāng)代碼導(dǎo)航至該界面并且沒有數(shù)據(jù)傳遞的時(shí)候,-1 就會(huì)作為默認(rèn)值傳遞阎姥,接收端的代碼需要使用該值判斷用戶現(xiàn)在需要?jiǎng)?chuàng)建一個(gè)新的甜甜圈摔握。
到這里,我們執(zhí)行 build 操作丁寄,gradle 就會(huì)針對所輸入的數(shù)據(jù)生成相應(yīng)的代碼氨淌。這一點(diǎn)很重要泊愧,因?yàn)椴皇沁@樣的話,Android Studio 就無法知道想要調(diào)用的函數(shù)在自動(dòng)生成代碼中的位置盛正。
您可以在項(xiàng)目結(jié)構(gòu)樹的 "java(generated)" 分支下找到上面過程中生成的代碼的執(zhí)行結(jié)果删咱。在子目錄中,可以看到有新文件生成豪筝,它們負(fù)責(zé)傳遞和獲取數(shù)據(jù)痰滋。
在 DonutListDirections
中,您可以找到 companion
對象续崖,它是用于導(dǎo)航至對話框的 API敲街。
companion object {
fun actionDonutListToDonutEntryDialogFragment(
itemId: Long = -1L): NavDirections =
ActionDonutListToDonutEntryDialogFragment(itemId)
}
這里 navigate()
并沒有使用最初的 Action
,而是使用了 NavDirections
對象严望。它既封裝了 action (我們可以通過 action 導(dǎo)航至對話框)多艇,同時(shí)還封裝了早期創(chuàng)建的變量。
需要注意的是上面的 actionDonutListToDonutEntryDialogFragment()
函數(shù)需要一個(gè) Long 類型的參數(shù)像吻,我們之前創(chuàng)建了相關(guān)變量峻黍,并且給它賦值為 -1。所以如果我們在調(diào)用該函數(shù)的時(shí)候不加參數(shù)拨匆,該方法會(huì)返回一個(gè) NavDirections
對象姆涩,并且它的 itemId 為 -1。
在另一個(gè)生成的文件 DonutEntryDialogFragmentArgs
中惭每,您可以看到 fromBundle()
函數(shù)包含從目標(biāo)對話框獲取數(shù)據(jù)的代碼:
fun fromBundle(bundle: Bundle): DonutEntryDialogFragmentArgs {
// ...
return DonutEntryDialogFragmentArgs(__itemId)
}
現(xiàn)在我可以利用生成的代碼成功傳遞和獲取數(shù)據(jù)了骨饿。首先,我在 DonutEntryDialogFragment
類中編寫代碼來獲取 itemId
數(shù)據(jù)台腥,并且確定用戶的意圖是添加一個(gè)新的甜甜圈還是編輯一個(gè)已有的甜甜圈:
val args: DonutEntryDialogFragmentArgs by navArgs()
val editingState =
if (args.itemId > 0) EditingState.EXISTING_DONUT
else EditingState.NEW_DONUT
第一行代碼用到了一個(gè)屬性委托样刷,它由 Navigation 組件庫提供,這樣寫可以簡化從 bundle 獲取數(shù)據(jù)的過程览爵。通過它可以在 args
變量中直接找到數(shù)據(jù)所對應(yīng)的名稱置鼻。
如果用戶正在編輯一個(gè)已有的甜甜圈信息,那么這里的代碼會(huì)獲取該元素的信息蜓竹,并且使用獲取到的信息填充 UI:
if (editingState == EditingState.EXISTING_DONUT) {
donutEntryViewModel.get(args.itemId).observe(
viewLifecycleOwner,
Observer { donutItem ->
binding.name.setText(donutItem.name)
binding.description.setText(donutItem.description)
binding.ratingBar.rating = donutItem.rating.toFloat()
donut = donutItem
}
)
}
需要注意的是這里的代碼是從數(shù)據(jù)庫請求信息箕母,并且我們希望整個(gè)請求過程能夠在 UI 線程之外進(jìn)行。所以代碼里會(huì)監(jiān)聽 ViewModel
所提供的 LiveData
對象俱济,并且異步處理請求嘶是,當(dāng)數(shù)據(jù)返回時(shí)填充視圖。
當(dāng)用戶點(diǎn)擊對話框里的 Done 按鈕時(shí)蛛碌,就需要存儲(chǔ)用戶所輸入的信息了聂喇。下面這段代碼會(huì)更新數(shù)據(jù)庫里相應(yīng)的數(shù)據(jù),并且關(guān)閉對話框:
binding.doneButton.setOnClickListener {
donutEntryViewModel.addData(
donut?.id ?: 0,
binding.name.text.toString(),
binding.description.text.toString(),
binding.ratingBar.rating.toInt()
)
dismiss()
}
上面的這些代碼主要側(cè)重于在目的界面里處理數(shù)據(jù),現(xiàn)在我們來看一下如何將數(shù)據(jù)發(fā)送到目標(biāo)界面希太。
在 DonutList
中一共有兩種途徑可以轉(zhuǎn)向?qū)υ捒蚩巳摹F渲幸环N是當(dāng)用戶點(diǎn)擊 懸浮操作按鈕
(FAB) 的時(shí)候:
binding.fab.setOnClickListener { fabView ->
fabView.findNavController().navigate(DonutListDirections
.actionDonutListToDonutEntryDialogFragment())
}
需要注意的是,這段代碼里在創(chuàng)建 NavDirections
對象的時(shí)候調(diào)用了無參數(shù)的構(gòu)造函數(shù)誊辉,所以變量會(huì)被默認(rèn)賦值為 -1 (以表明這是一個(gè)新的甜甜圈)矾湃,這也是我們希望通過點(diǎn)擊懸浮操作按鈕所要實(shí)現(xiàn)的效果。
另一個(gè)途徑是當(dāng)用戶點(diǎn)擊列表中已有元素的時(shí)候堕澄,會(huì)打開對話框邀跃。可以通過下面的 lambda 表達(dá)式實(shí)現(xiàn)蛙紫,它將在 DonutListAdapter
的構(gòu)建過程中傳入 (即 onEdit
參數(shù))拍屑,然后會(huì)在每個(gè)表項(xiàng)的 onClick
被觸發(fā)的時(shí)候被調(diào)用:
donut ->
findNavController().navigate(DonutListDirections
.actionDonutListToDonutEntryDialogFragment(donut.id))
這里的代碼和用戶點(diǎn)擊懸浮操作按鈕的代碼相似,只不過這里將表項(xiàng)的 id 傳了進(jìn)去坑傅,告訴對話框它要編輯一個(gè)已有的元素僵驰。而且和我們之前的代碼看到的一樣,它會(huì)用已有元素的信息填充對話框裁蚁,并且對該表項(xiàng)所做的修改也會(huì)相應(yīng)更新數(shù)據(jù)庫里的對應(yīng)項(xiàng)。
總結(jié)
這就是 SafeArgs 的全部內(nèi)容继准。使用起來非常簡單 (比起 Bundle 要簡單很多)枉证,因?yàn)橐蕾噹鞎?huì)幫您生成代碼來簡化數(shù)據(jù)傳遞,并且保障了數(shù)據(jù)類型安全移必。通過這樣的方式室谚,您可以更好地利用數(shù)據(jù)封裝,在目的地之間僅僅傳遞所需的數(shù)據(jù)而無需在更大的范圍內(nèi)暴露數(shù)據(jù)崔泵。
請繼續(xù)關(guān)注我們后續(xù)的關(guān)于導(dǎo)航組件的內(nèi)容秒赤,接下來我們會(huì)介紹如何使用 Deep Link。
更多信息
更多關(guān)于導(dǎo)航組件的詳情憎瘸,請查看 導(dǎo)航組件使用入門文檔
DonutTracker 應(yīng)用的完整代碼入篮,請查看 Github 示例
更多現(xiàn)代 Android 開發(fā)技巧 (MAD Skills) 系列內(nèi)容,請查看 Android Developers 頻道