欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Compose?的?Navigation組件使用示例詳解

 更新時(shí)間:2022年10月17日 15:25:34   作者:艾維碼  
這篇文章主要為大家介紹了Compose?的?Navigation組件使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

正文

Navigation 組件支持 Jetpack Compose 應(yīng)用。我們可以在利用 Navigation 組件的基礎(chǔ)架構(gòu)和功能,在可組合項(xiàng)之間導(dǎo)航。然而,在項(xiàng)目中使用之后,我發(fā)現(xiàn)這個(gè)組件真的不好用:

  • 耦合:導(dǎo)航需要持有NavHostController,在可組合函數(shù)中,必須傳遞NavHostController才能導(dǎo)航,導(dǎo)致所有需要導(dǎo)航的可組合函數(shù)都要持有NavHostController的引用。傳遞callback也是同樣的問題。
  • 重構(gòu)和封裝變得困難:有的項(xiàng)目并不是一個(gè)全新的 Compose 項(xiàng)目,而是部分功能重寫,在這種情況下,很難將NavHostController 提供給這些可組合項(xiàng)。
  • 跳轉(zhuǎn)功能麻煩,許多時(shí)候并不是單純的導(dǎo)航到下一個(gè)頁面,可能伴隨 replacepop、清除導(dǎo)航棧等,需要大量代碼實(shí)現(xiàn)。
  • ViewModel等非可組合函數(shù)不能獲取NavHostController
  • 拼接路由名麻煩:導(dǎo)航組件的路由如果傳遞參數(shù)的話,需要按照規(guī)則拼接。

看了很多關(guān)于如何實(shí)現(xiàn)導(dǎo)航的討論,并且找到了一些非常棒的庫,appyx、compose-routerDecompose、compose-backstack和使用者最多的compose-destinations,但是都不能滿足我,畢竟導(dǎo)航是重中之重,所以就準(zhǔn)備對 Navigation 組件改造,封裝一個(gè)方便使用的組件庫。

Jetpack Compose Clean Navigation

如果使用單例或者Hilt提供一個(gè)單例的自定義導(dǎo)航器,每個(gè)ViewModelCompose里均可以直接使用,通過調(diào)用導(dǎo)航器的函數(shù),實(shí)現(xiàn)導(dǎo)航到不同的屏幕。所有導(dǎo)航事件能收集在一起,這樣就不需要傳遞回調(diào)或傳遞navController給其他屏幕。達(dá)到下面一句話的簡潔用法,就問你香不香?

            AppNav.to(ThreeDestination("來自Two"))
            AppNav.replace(ThreeDestination("replace來自Two"))
            AppNav.back()

實(shí)現(xiàn)一個(gè)自定義導(dǎo)航器,首先用接口聲明出需要的函數(shù),一般來說,前兩個(gè)出棧、導(dǎo)航函數(shù)就可以滿足應(yīng)用中需要的場景,后面兩個(gè)函數(shù)的功能也可以用前兩個(gè)函數(shù)實(shí)現(xiàn)出來,但是參數(shù)略多,另外實(shí)際使用的場景也很多,為了簡潔,利用后面兩個(gè)函數(shù)擴(kuò)展一下:

interface INav {
    /**
     * 出棧
     * @param route String
     * @param inclusive Boolean
     */
    fun back(
        route: String? = null,
        inclusive: Boolean = false,
    )
    /**
     * 導(dǎo)航
     * @param route 目的地路由
     * @param popUpToRoute 彈出路由?
     * @param inclusive 是否也彈出popUpToRoute
     * @param isSingleTop Boolean
     */
    fun to(
        route: String,
        popUpToRoute: String? = null,
        inclusive: Boolean = false,
        isSingleTop: Boolean = false,
    )
    /**
     * 彈出當(dāng)前棧并導(dǎo)航到
     * @param route String
     * @param isSingleTop Boolean
     */
    fun replace(
        route: String,
        isSingleTop: Boolean = false,
    )
    /**
     * 清空導(dǎo)航棧然后導(dǎo)航到route
     * @param route String
     */
    fun offAllTo(
        route: String,
    )
}

AppNav實(shí)現(xiàn)了上面的四個(gè)導(dǎo)航功能。非常簡單,因?yàn)橐脝卫?,這里使用object,其中只是多了一個(gè)私有函數(shù),發(fā)送導(dǎo)航意圖,:

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就是導(dǎo)航的意圖,和導(dǎo)航器的每個(gè)函數(shù)對應(yīng),同導(dǎo)航器一樣,兩個(gè)函數(shù)足以,多的兩個(gè)函數(shù)同樣是為了簡潔:

sealed class NavIntent {
    /**
     * 返回堆棧彈出到指定目標(biāo)
     * @property route 指定目標(biāo)
     * @property inclusive 是否彈出指定目標(biāo)
     * @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()
    /**
     * 導(dǎo)航到指定目標(biāo)
     * @property route 指定目標(biāo)
     * @property popUpToRoute 返回堆棧彈出到指定目標(biāo)
     * @property inclusive 是否彈出指定popUpToRoute目標(biāo)
     * @property isSingleTop 是否是棧中單實(shí)例模式
     * @constructor
     */
    data class To(
        val route: String,
        val popUpToRoute: String? = null,
        val inclusive: Boolean = false,
        val isSingleTop: Boolean = false,
    ) : NavIntent()
    /**
     * 替換當(dāng)前導(dǎo)航/彈出當(dāng)前導(dǎo)航并導(dǎo)航到指定目的地
     * @property route 當(dāng)前導(dǎo)航
     * @property isSingleTop 是否是棧中單實(shí)例模式
     * @constructor
     */
    data class Replace(
        val route: String,
        val isSingleTop: Boolean = false,
    ) : NavIntent()
    /**
     * 清空導(dǎo)航棧并導(dǎo)航到指定目的地
     * @property route 指定目的地
     * @constructor
     */
    data class OffAllTo(
        val route: String,
    ) : NavIntent()
}

要實(shí)現(xiàn)在多個(gè)地方(ViewMdeol、可組合函數(shù))發(fā)送和集中在一個(gè)地方接收處理導(dǎo)航命令,就要使用 Flow 或者Channel實(shí)現(xiàn),這里使用Channel,同樣是object,如果使用Hilt的話,可以提供出去一個(gè)單例:

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

實(shí)現(xiàn)接收并執(zhí)行對應(yīng)功能:

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

自定義NavHostcomposable. NavigationEffects只需收集navigationChannel并導(dǎo)航到所需的屏幕。這里可以看到,它很干凈干凈,我們不必傳遞任何回調(diào)或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
    )
}

導(dǎo)航封裝完成,還有一步就是路由間的參數(shù)拼接,最初的實(shí)現(xiàn)是使用者自己實(shí)現(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)點(diǎn)是使用密封類實(shí)現(xiàn)路由聲明,具有約束作用。后來考慮到減少客戶端樣板代碼,就聲明了一個(gè)接口,appendArguments是拼接參數(shù)的擴(kuò)展方法,無需自己手動(dòng)拼接:

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ù)名和類型,然后用傳參替換對應(yīng)的參數(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)
}

傳遞多個(gè)參數(shù)

用傳參去去替換路由里面對應(yīng)的參數(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 要序列化,這里用了兩個(gè)注解,Serializable是因?yàn)槭褂昧?code>kotlinx.serialization,如果使用 Gson 則不需要,重寫toString是因?yàn)槠唇訁?shù)的時(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)
                        }
                    }

導(dǎo)航

看下現(xiàn)在的導(dǎo)航是有多簡單:

   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)能夠在模塊化應(yīng)用程序中實(shí)現(xiàn) Jetpack Compose 導(dǎo)航。并且使我們能夠集中導(dǎo)航邏輯,在這樣做的同時(shí),我們可以看到一系列優(yōu)勢:

  • 我們不再需要將 NavHostController 傳遞給我們的可組合函數(shù),消除了我們的功能模塊依賴于 Compose Navigation 依賴項(xiàng)的需要,同時(shí)還簡化了我們的構(gòu)造函數(shù)以進(jìn)行測試。
  • 我們添加了對于ViewModel中進(jìn)行導(dǎo)航的支持,可以在普通函數(shù)中進(jìn)行導(dǎo)航。
  • 簡化了替換、出棧等操作,一句話簡單實(shí)現(xiàn)。

Compose 中的導(dǎo)航仍處于早期階段,隨著官方的改進(jìn),也許我們會(huì)不需要封裝,但是目前來說我對自己實(shí)現(xiàn)的這種方法很滿意。

我已經(jīng)把這個(gè)倉庫發(fā)布到Maven Central了,大家可以直接依賴使用:

implementation 'io.github.yuexunshi:Nav:1.0.1'

附上源碼

以上就是Compose 的 Navigation組件使用示例詳解的詳細(xì)內(nèi)容,更多關(guān)于Compose Navigation組件的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評論