lzyprime 博客 (github)
創建時間:2020.11.24
qq及郵箱:2383518170
kotlin & android 筆記
λ:
navigation 組件
是 Android Jetpack
重要組成部分,推出3年左右,2018谷歌I/O大會也曾介紹過。主要用于組織Fragment
,通過Fragment
來實現不同內容片段的顯示。包括同級之間切換,不同級之間跳轉(如 列表item跳詳情頁),代替以往跳轉Activity
的方式,推出單Activity模式
。
與Activity
相比好處:
- 拿到同一份
Activity ViewModel
。ViewModel
以Activity
為單位共享,同一Activity
下的Fragment
可以拿到同一份ViewModel
,所以如果直接跳轉Activity
數據共享要自己解決,傳遞或者全局緩存里取,同時還要考慮副本一致性,也就是我在一個頁面修改數據,其他用到此數據的頁面也應該同步修改。
flutter Navigator1.0
里以Route
組織頁面,用Provider
插件實現狀態管理時,除非把Provider
包在的MaterialApp
的外層,否則,跳轉其他Route
就無法通過Provider.of(context)
獲取,數據處理就如同Android Activity
, 包在最外層好使是因為MaterialApp
生成時會構造一個Navigator
來組織所有頁面。通過List<_RouteEntry> _history
和GlobalKey<OverlayState> _overlayKey
保存信息。所以所有頁面都是MaterialApp
的子節點。最近的
flutter 1.22
推出Navigator2.0
, 可以用List<Page<dynamic>> pages
組織頁面,也就類似Android navigation
組件,flutter Page
好比Android Fragment
導航圖可以靠可視化工具拖框完成。每一個頁面稱作“目的地”, 通過之間連線、設置參數來實現頁面跳轉約束,同時可以設置過渡動畫等。所有導航資源存在資源文件夾下的
navigation
文件夾里。也支持Kotlin DSL
代碼完成導航設置。Safe Args
傳遞數據。Safe Arges 官網地址。保證安全的傳遞數據。
有句話: "通常情況下,強烈建議您僅在目的地之間傳遞最少量的數據。例如,您應該傳遞鍵來檢索對象而不是傳遞對象本身,因為在 Android 上用于保存所有狀態的總空間是有限的。", 這同樣適用于
flutter
。在flutter
里并沒有太好的副本一致性方案,所以我在Bean
也就是數據解析時做了緩存,同一數據只會構造一次,之后全從緩存中取或者更新。利用factory
構造函數將數據緩存、生成、更新、獲取等操作隱形,達到簡單的Loc
的效果。(TODO: 有空再總結)
需要注意:
- 系統返回按鈕和事件的處理。跳轉
Fragment
時,要攔截并設置好Activity
的返回按鈕事件,否則整個Activity
就關閉了。同時其他組件的狀態更新也需要自己維護, 參考:使用 NavigationUI 更新界面組件
這個問題在
flutter Navigator2.0
里同樣存在。
- 同級
Fragment
切換需要重新構建,并不記錄狀態。通過把當前的FragmentManager
交給navigation
來實現頁面切換時,每切換一次都要重建Fragment
。
demo: 添加登錄頁,詳情頁
從之前demo繼續開發。
# android navigation demo
# 倉庫地址: https://github.com/lzyprime/android_demos
# branch: navigation
git clone -b navigation https://github.com/lzyprime/android_demos
1. 導入
使用入門 官網地址
如果用Safe Args
則要在最頂層引入插件,或者用Bundle
代替Safe Args
實現傳遞
// project gradle
buildscript {
repositories {
google()
}
// Safe Args
dependencies {
...
def nav_version = "2.3.1"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
}
}
// module gradle
plugin {
...
id "androidx.navigation.safeargs.kotlin"
}
dependencies {
...
// navigation
def nav_version = "2.3.1"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
// Feature module Support
//implementation "androidx.navigation:navigation-dynamic-features-fragment:$nav_version"
// Testing Navigation
//androidTestImplementation "androidx.navigation:navigation-testing:$nav_version"
// Jetpack Compose Integration
//implementation "androidx.navigation:navigation-compose:1.0.0-alpha01"
}
2. 導航圖與目的地
參考 設計導航圖
和 條件導航(官網地址)。
添加新的android resource
, 類型選擇Navigation
, 同時會生成navigation目錄。
點擊添加新的目的地(Fragment
), 如果已經存在,列表里會顯示。注意一定要以Fragment
為單位
添加3個目的地。 LoginFragment
在登錄成功后會由PhotoListFragment
取代,因此要設置popUpTo
參數。參考導航到目的地
每一條導航規則都有自己的id。 用于NavController.navigate
實現跳轉。
LoginFragment
為起始目的地,名字前會顯示“主頁”圖標。
2. 為Activity
添加NavHost
在layout文件中添加NavHostFragment
, 同時會報一個警告?,提示用FragmentContainerView
作為Fragment
容器。點擊Fix應用該建議。
3. 登錄功能及Activity
目的地切換
此時app已經可以啟動了,會顯示起始目的地也就是LoginFragment
。
demo用到的https://api.unsplash.com/實際只需要access_key, 所以登錄頁只需要一個輸入框和登錄按鈕。點擊登錄時會請求列表,若成功則替換為PhotoListFragment
頁。
// LoginFragment
class LoginFragment : Fragment(R.layout.fragment_login) {
private val viewModel: ListPhotoViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
login_btn.setOnClickListener {
val text = access_key_eidt_text.text.toString()
if (text.isEmpty()) {
Toast.makeText(context, "不能為空!", Toast.LENGTH_SHORT).show()
} else {
Net.ACCESS_KEY = text
viewModel.refreshListPhotos()
}
}
}
}
參考導航到目的地, 用NavHost
的NavController
來實現目的地跳轉。
參考使用 NavigationUI 更新界面組件,設置頂部appBar和系統返回事件相應
// MainActivity
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val viewModel: ListPhotoViewModel by viewModels()
private var loginSuccess = false
private lateinit var appBarConfiguration: AppBarConfiguration
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val navHost = supportFragmentManager.findFragmentById(R.id.mainNavHost) as NavHostFragment
val navController = navHost.findNavController()
appBarConfiguration = AppBarConfiguration(setOf(R.id.loginFragment, R.id.photoListFragment))
// 頂部appBar
setupActionBarWithNavController(navController, appBarConfiguration)
viewModel.listPhotos.observe(this) {
// 列表不為空,登錄成功
if (!loginSuccess && it.isNotEmpty()) {
loginSuccess = true
Toast.makeText(this, "登錄成功", Toast.LENGTH_SHORT).show()
navController.navigate(R.id.action_loginFragment_to_photoListFragment)
}
}
}
override fun onSupportNavigateUp(): Boolean {
// 是否顯示返回按鈕
return mainNavHost.findNavController().navigateUp(appBarConfiguration)
|| super.onSupportNavigateUp()
}
override fun onBackPressed() {
// 系統返回事件
if (!mainNavHost.findNavController().popBackStack()) finish()
}
}
4. 點擊圖片進入詳情頁,利用Safe Args
傳遞圖片鏈接
參考在目的地之間傳遞數據, 在導航圖編輯頁面可視化編輯要傳遞的參數。
SpecifyAmountFragmentDirections
, ConfirmationFragmentArgs
為插件自動生成,可在java(generated)目錄找到
//PhotoListFragment 跳轉邏輯
class PhotoListFragment : Fragment(R.layout.fragment_photo_list) {
...
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val photo = photos[position]
with(holder.itemView as ImageView) {
Glide.with(this).load(photo.urls.raw).into(this)
setOnClickListener {
val directions = PhotoListFragmentDirections.actionPhotoListFragmentToDetailFragment(photo.urls.raw)
this@PhotoListFragment.findNavController().navigate(directions)
}
}
}
...
}
如果使用-ktx
版本,可以用by navArgs()
來獲取傳遞的參數
// DetailFragment
class DetailFragment : Fragment(R.layout.fragment_detail) {
private val args by navArgs<DetailFragmentArgs>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Glide.with(imageView).load(args.imageSrc).into(imageView)
}
}