Android navigation組件

lzyprime 博客 (github)
創建時間:2020.11.24
qq及郵箱:2383518170

kotlin & android 筆記


λ:

navigation 組件Android Jetpack重要組成部分,推出3年左右,2018谷歌I/O大會也曾介紹過。主要用于組織Fragment,通過Fragment來實現不同內容片段的顯示。包括同級之間切換,不同級之間跳轉(如 列表item跳詳情頁),代替以往跳轉Activity的方式,推出單Activity模式

navigation 官網地址

Activity相比好處:

  1. 拿到同一份Activity ViewModelViewModelActivity為單位共享,同一Activity下的Fragment可以拿到同一份ViewModel,所以如果直接跳轉Activity數據共享要自己解決,傳遞或者全局緩存里取,同時還要考慮副本一致性,也就是我在一個頁面修改數據,其他用到此數據的頁面也應該同步修改。

flutter Navigator1.0里以Route組織頁面,用Provider插件實現狀態管理時,除非把Provider包在的MaterialApp的外層,否則,跳轉其他Route就無法通過Provider.of(context)獲取,數據處理就如同Android Activity, 包在最外層好使是因為MaterialApp生成時會構造一個Navigator來組織所有頁面。通過List<_RouteEntry> _historyGlobalKey<OverlayState> _overlayKey保存信息。所以所有頁面都是MaterialApp的子節點。

最近的flutter 1.22推出Navigator2.0, 可以用List<Page<dynamic>> pages組織頁面,也就類似Android navigation組件,flutter Page好比Android Fragment

  1. 導航圖可以靠可視化工具拖框完成。每一個頁面稱作“目的地”, 通過之間連線、設置參數來實現頁面跳轉約束,同時可以設置過渡動畫等。所有導航資源存在資源文件夾下的navigation文件夾里。也支持Kotlin DSL代碼完成導航設置。

  2. Safe Args傳遞數據。Safe Arges 官網地址。保證安全的傳遞數據。

有句話: "通常情況下,強烈建議您僅在目的地之間傳遞最少量的數據。例如,您應該傳遞鍵來檢索對象而不是傳遞對象本身,因為在 Android 上用于保存所有狀態的總空間是有限的。", 這同樣適用于flutter。在flutter里并沒有太好的副本一致性方案,所以我在Bean也就是數據解析時做了緩存,同一數據只會構造一次,之后全從緩存中取或者更新。利用factory構造函數將數據緩存、生成、更新、獲取等操作隱形,達到簡單的Loc的效果。(TODO: 有空再總結)

需要注意:

  1. 系統返回按鈕和事件的處理。跳轉Fragment時,要攔截并設置好Activity的返回按鈕事件,否則整個Activity就關閉了。同時其他組件的狀態更新也需要自己維護, 參考:使用 NavigationUI 更新界面組件

這個問題在flutter Navigator2.0里同樣存在。

  1. 同級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目錄。

2.png

點擊添加新的目的地(Fragment), 如果已經存在,列表里會顯示。注意一定要以Fragment為單位

3.png

添加3個目的地。 LoginFragment在登錄成功后會由PhotoListFragment取代,因此要設置popUpTo參數。參考導航到目的地

每一條導航規則都有自己的id。 用于NavController.navigate實現跳轉。

LoginFragment為起始目的地,名字前會顯示“主頁”圖標。

4.png

2. 為Activity添加NavHost

在layout文件中添加NavHostFragment, 同時會報一個警告?,提示用FragmentContainerView作為Fragment容器。點擊Fix應用該建議。

5.png
6.png

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()
            }
        }
    }
}

參考導航到目的地, 用NavHostNavController來實現目的地跳轉。

參考使用 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)目錄找到

7.png
//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)
    }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容