Compose?的?Navigation組件使用示例詳解
正文
Navigation 組件支持 Jetpack Compose 應用。我們可以在利用 Navigation 組件的基礎架構和功能,在可組合項之間導航。然而,在項目中使用之后,我發(fā)現(xiàn)這個組件真的不好用:
- 耦合:導航需要持有NavHostController,在可組合函數(shù)中,必須傳遞NavHostController才能導航,導致所有需要導航的可組合函數(shù)都要持有NavHostController的引用。傳遞
callback
也是同樣的問題。 - 重構和封裝變得困難:有的項目并不是一個全新的 Compose 項目,而是部分功能重寫,在這種情況下,很難將NavHostController 提供給這些可組合項。
- 跳轉功能麻煩,許多時候并不是單純的導航到下一個頁面,可能伴隨
replace
、pop
、清除導航棧等,需要大量代碼實現(xiàn)。 ViewModel
等非可組合函數(shù)不能獲取NavHostController。- 拼接路由名麻煩:導航組件的路由如果傳遞參數(shù)的話,需要按照規(guī)則拼接。
看了很多關于如何實現(xiàn)導航的討論,并且找到了一些非常棒的庫,appyx、compose-router、Decompose、compose-backstack和使用者最多的compose-destinations,但是都不能滿足我,畢竟導航是重中之重,所以就準備對 Navigation 組件改造,封裝一個方便使用的組件庫。
Jetpack Compose Clean Navigation
如果使用單例或者Hilt
提供一個單例的自定義導航器,每個ViewModel
和Compose
里均可以直接使用,通過調用導航器的函數(shù),實現(xiàn)導航到不同的屏幕。所有導航事件能收集在一起,這樣就不需要傳遞回調或傳遞navController
給其他屏幕。達到下面一句話的簡潔用法,就問你香不香?
AppNav.to(ThreeDestination("來自Two")) AppNav.replace(ThreeDestination("replace來自Two")) AppNav.back()
實現(xiàn)一個自定義導航器,首先用接口聲明出需要的函數(shù),一般來說,前兩個出棧、導航函數(shù)就可以滿足應用中需要的場景,后面兩個函數(shù)的功能也可以用前兩個函數(shù)實現(xiàn)出來,但是參數(shù)略多,另外實際使用的場景也很多,為了簡潔,利用后面兩個函數(shù)擴展一下:
interface INav { /** * 出棧 * @param route String * @param inclusive Boolean */ fun back( route: String? = null, inclusive: Boolean = false, ) /** * 導航 * @param route 目的地路由 * @param popUpToRoute 彈出路由? * @param inclusive 是否也彈出popUpToRoute * @param isSingleTop Boolean */ fun to( route: String, popUpToRoute: String? = null, inclusive: Boolean = false, isSingleTop: Boolean = false, ) /** * 彈出當前棧并導航到 * @param route String * @param isSingleTop Boolean */ fun replace( route: String, isSingleTop: Boolean = false, ) /** * 清空導航棧然后導航到route * @param route String */ fun offAllTo( route: String, ) }
AppNav
實現(xiàn)了上面的四個導航功能。非常簡單,因為要用單例,這里使用object
,其中只是多了一個私有函數(shù),發(fā)送導航意圖,:
object AppNav : INav { private fun navigate(destination: NavIntent) { NavChannel.navigate(destination) } override fun back(route: String?, inclusive: Boolean) { navigate(NavIntent.Back( route = route, inclusive = inclusive, )) } override fun to( route: String, popUpToRoute: String?, inclusive: Boolean, isSingleTop: Boolean, ) { navigate(NavIntent.To( route = route, popUpToRoute = popUpToRoute, inclusive = inclusive, isSingleTop = isSingleTop, )) } override fun replace(route: String, isSingleTop: Boolean) { navigate(NavIntent.Replace( route = route, isSingleTop = isSingleTop, )) } override fun offAllTo(route: String) { navigate(NavIntent.OffAllTo(route)) } }
NavIntent
就是導航的意圖,和導航器的每個函數(shù)對應,同導航器一樣,兩個函數(shù)足以,多的兩個函數(shù)同樣是為了簡潔:
sealed class NavIntent { /** * 返回堆棧彈出到指定目標 * @property route 指定目標 * @property inclusive 是否彈出指定目標 * @constructor * 【"4"、"3"、"2"、"1"】 Back("2",true)->【"4"、"3"】 * 【"4"、"3"、"2"、"1"】 Back("2",false)->【"4"、"3"、"2"】 */ data class Back( val route: String? = null, val inclusive: Boolean = false, ) : NavIntent() /** * 導航到指定目標 * @property route 指定目標 * @property popUpToRoute 返回堆棧彈出到指定目標 * @property inclusive 是否彈出指定popUpToRoute目標 * @property isSingleTop 是否是棧中單實例模式 * @constructor */ data class To( val route: String, val popUpToRoute: String? = null, val inclusive: Boolean = false, val isSingleTop: Boolean = false, ) : NavIntent() /** * 替換當前導航/彈出當前導航并導航到指定目的地 * @property route 當前導航 * @property isSingleTop 是否是棧中單實例模式 * @constructor */ data class Replace( val route: String, val isSingleTop: Boolean = false, ) : NavIntent() /** * 清空導航棧并導航到指定目的地 * @property route 指定目的地 * @constructor */ data class OffAllTo( val route: String, ) : NavIntent() }
要實現(xiàn)在多個地方(ViewMdeol
、可組合函數(shù))發(fā)送和集中在一個地方接收處理導航命令,就要使用 Flow 或者Channel
實現(xiàn),這里使用Channel
,同樣是object
,如果使用Hilt
的話,可以提供出去一個單例:
internal object NavChannel { private val channel = Channel<NavIntent>( capacity = Int.MAX_VALUE, onBufferOverflow = BufferOverflow.DROP_LATEST, ) internal var navChannel = channel.receiveAsFlow() internal fun navigate(destination: NavIntent) { channel.trySend(destination) } }
實現(xiàn)接收并執(zhí)行對應功能:
fun NavController.handleComposeNavigationIntent(intent: NavIntent) { when (intent) { is NavIntent.Back -> { if (intent.route != null) { popBackStack(intent.route, intent.inclusive) } else { currentBackStackEntry?.destination?.route?.let { popBackStack() } } } is NavIntent.To -> { navigate(intent.route) { launchSingleTop = intent.isSingleTop intent.popUpToRoute?.let { popUpToRoute -> popUpTo(popUpToRoute) { inclusive = intent.inclusive } } } } is NavIntent.Replace -> { navigate(intent.route) { launchSingleTop = intent.isSingleTop currentBackStackEntry?.destination?.route?.let { popBackStack() } } } is NavIntent.OffAllTo -> navigate(intent.route) { popUpTo(0) } } }
自定義NavHost
和composable
. NavigationEffects
只需收集navigationChannel
并導航到所需的屏幕。這里可以看到,它很干凈干凈,我們不必傳遞任何回調或navController
.
@Composable fun NavigationEffect( startDestination: String, builder: NavGraphBuilder.() -> Unit, ) { val navController = rememberNavController() val activity = (LocalContext.current as? Activity) val flow = NavChannel.navChannel LaunchedEffect(activity, navController, flow) { flow.collect { if (activity?.isFinishing == true) { return@collect } navController.handleComposeNavigationIntent(it) navController.backQueue.forEachIndexed { index, navBackStackEntry -> Log.e( "NavigationEffects", "index:$index=NavigationEffects: ${navBackStackEntry.destination.route}", ) } } } NavHost( navController = navController, startDestination = startDestination, builder = builder ) }
導航封裝完成,還有一步就是路由間的參數(shù)拼接,最初的實現(xiàn)是使用者自己實現(xiàn):
sealed class Screen( path: String, val arguments: List<NamedNavArgument> = emptyList(), ) { val route: String = path.appendArguments(arguments) object One : Screen("one") object Two : Screen("two") object Four : Screen("four", listOf( navArgument("user") { type = NavUserType() nullable = false } )) { const val ARG = "user" fun createRoute(user: User): String { return route.replace("{${arguments.first().name}}", user.toString()) } } object Three : Screen("three", listOf(navArgument("channelId") { type = NavType.StringType })) { const val ARG = "channelId" fun createRoute(str: String): String { return route.replace("{${arguments.first().name}}", str) } } }
優(yōu)點是使用密封類實現(xiàn)路由聲明,具有約束作用。后來考慮到減少客戶端樣板代碼,就聲明了一個接口,appendArguments
是拼接參數(shù)的擴展方法,無需自己手動拼接:
abstract class Destination( path: String, val arguments: List<NamedNavArgument> = emptyList(), ) { val route: String = if (arguments.isEmpty()) path else path.appendArguments(arguments) } private fun String.appendArguments(navArguments: List<NamedNavArgument>): String { val mandatoryArguments = navArguments.filter { it.argument.defaultValue == null } .takeIf { it.isNotEmpty() } ?.joinToString(separator = "/", prefix = "/") { "{${it.name}}" } .orEmpty() val optionalArguments = navArguments.filter { it.argument.defaultValue != null } .takeIf { it.isNotEmpty() } ?.joinToString(separator = "&", prefix = "?") { "${it.name}={${it.name}}" } .orEmpty() return "$this$mandatoryArguments$optionalArguments" }
使用
首先聲明路由,繼承Destination
,命名采用page
+Destination
:
object OneDestination : Destination("one") object TwoDestination : Destination("two") object ThreeDestination : Destination("three", listOf(navArgument("channelId") { type = NavType.StringType })) { const val ARG = "channelId" operator fun invoke(str: String): String = route.replace("{${arguments.first().name}}", str) } object FourDestination : Destination("four", listOf( navArgument("user") { type = NavUserType() nullable = false } )) { const val ARG = "user" operator fun invoke(user: User): String = route.replace("{${arguments.first().name}}", user.toString()) } object FiveDestination : Destination("five", listOf(navArgument("age") { type = NavType.IntType }, navArgument("name") { type = NavType.StringType })) { const val ARG_AGE = "age" const val ARG_NAME = "name" operator fun invoke(age: Int, name: String): String = route.replace("{${arguments.first().name}}", "$age") .replace("{${arguments.last().name}}", name) }
傳遞普通參數(shù),String、Int
使用navArgument
生命參數(shù)名和類型,然后用傳參替換對應的參數(shù)名,這里使用invoke
簡化寫法:
object ThreeDestination : Destination("three", listOf(navArgument("channelId") { type = NavType.StringType })) { const val ARG = "channelId" operator fun invoke(str: String): String = route.replace("{${arguments.first().name}}", str) }
傳遞多個參數(shù)
用傳參去去替換路由里面對應的參數(shù)名。
object FiveDestination : Destination("five", listOf(navArgument("age") { type = NavType.IntType }, navArgument("name") { type = NavType.StringType })) { const val ARG_AGE = "age" const val ARG_NAME = "name" operator fun invoke(age: Int, name: String): String = route.replace("{${arguments.first().name}}", "$age") .replace("{${arguments.last().name}}", name) }
傳遞序列化參數(shù)
DataBean 要序列化,這里用了兩個注解,Serializable
是因為使用了kotlinx.serialization
,如果使用 Gson 則不需要,重寫toString
是因為拼接參數(shù)的時候可以直接用。
@Parcelize @kotlinx.serialization.Serializable data class User( val name: String, val phone: String, ) : Parcelable{ override fun toString(): String { return Uri.encode(Json.encodeToString(this)) } }
然后自定義NavType
:
class NavUserType : NavType<User>(isNullableAllowed = false) { override fun get(bundle: Bundle, key: String): User? = bundle.getParcelable(key) override fun put(bundle: Bundle, key: String, value: User) = bundle.putParcelable(key, value) override fun parseValue(value: String): User { return Json.decodeFromString(value) } override fun toString(): String { return Uri.encode(Json.encodeToString(this)) } }
傳遞自定義的NavType
:
object FourDestination : Destination("four", listOf( navArgument("user") { type = NavUserType() nullable = false } )) { const val ARG = "user" operator fun invoke(user: User): String = route.replace("{${arguments.first().name}}", user.toString()) }
注冊
使用NavigationEffect
替換原生的NavHost
:
NavigationEffect(OneDestination.route) { composable(OneDestination.route) { OneScreen() } composable(TwoDestination.route) { TwoScreen() } composable(FourDestination.route, arguments = FourDestination.arguments) { val user = it.arguments?.getParcelable<User>(FourDestination.ARG) ?: return@composable FourScreen(user) } composable(ThreeDestination.route, arguments = ThreeDestination.arguments) { val channelId = it.arguments?.getString(ThreeDestination.ARG) ?: return@composable ThreeScreen(channelId) } composable(FiveDestination.route, arguments = FiveDestination.arguments) { val age = it.arguments?.getInt(FiveDestination.ARG_AGE) ?: return@composable val name = it.arguments?.getString(FiveDestination.ARG_NAME) ?: return@composable FiveScreen(age, name) } }
導航
看下現(xiàn)在的導航是有多簡單:
Button(onClick = { AppNav.to(TwoDestination.route) }) { Text(text = "去TwoScreen") } Button(onClick = { AppNav.to(ThreeDestination("來自首頁")) }) { Text(text = "去ThreeScreen") } Button(onClick = { AppNav.to(FourDestination(User("來著首頁", "110"))) }) { Text(text = "去FourScreen") } Button(onClick = { AppNav.to(FiveDestination(20, "來自首頁")) }) { Text(text = "去FiveScreen") }
完成上述操作后,我們已經(jīng)能夠在模塊化應用程序中實現(xiàn) Jetpack Compose 導航。并且使我們能夠集中導航邏輯,在這樣做的同時,我們可以看到一系列優(yōu)勢:
- 我們不再需要將 NavHostController 傳遞給我們的可組合函數(shù),消除了我們的功能模塊依賴于 Compose Navigation 依賴項的需要,同時還簡化了我們的構造函數(shù)以進行測試。
- 我們添加了對于
ViewModel
中進行導航的支持,可以在普通函數(shù)中進行導航。 - 簡化了替換、出棧等操作,一句話簡單實現(xiàn)。
Compose 中的導航仍處于早期階段,隨著官方的改進,也許我們會不需要封裝,但是目前來說我對自己實現(xiàn)的這種方法很滿意。
我已經(jīng)把這個倉庫發(fā)布到Maven Central了,大家可以直接依賴使用:
implementation 'io.github.yuexunshi:Nav:1.0.1'
以上就是Compose 的 Navigation組件使用示例詳解的詳細內容,更多關于Compose Navigation組件的資料請關注腳本之家其它相關文章!
相關文章
Android應用中通過Layout_weight屬性用ListView實現(xiàn)表格
這篇文章主要介紹了Android應用中通過Layout_weight屬性用ListView實現(xiàn)表格的方法,文中對Layout_weight屬性先有一個較為詳細的解釋,需要的朋友可以參考下2016-04-04Android scheme 跳轉的設計與實現(xiàn)詳解
這篇文章主要介紹了Android scheme 跳轉的設計與實現(xiàn),本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-06-06Android自定義ScrollView實現(xiàn)阻尼回彈
這篇文章主要為大家詳細介紹了Android自定義ScrollView實現(xiàn)阻尼回彈,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-04-04Android動效Compose貝塞爾曲線動畫規(guī)格詳解
這篇文章主要為大家介紹了Android動效Compose貝塞爾曲線動畫規(guī)格詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-11-11android實現(xiàn)手機App實現(xiàn)拍照功能示例
本篇文章主要介紹了android實現(xiàn)手機App實現(xiàn)拍照功能示例,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-02-02