干貨-Jectpack Compose 通過Navigation 傳遞 Serializable / Parcelable三種實(shí)現(xiàn)

880481ecbba9baf5d97b4d02ce42b5b4.jpeg

掘金遷移地址

在Jetpack Compose中導(dǎo)航可以使用Jetpack中的Navigation數(shù)據(jù)傳輸組件進(jìn)行數(shù)據(jù)傳輸。

先決條件

在app的build.gradle中引入Navigation依賴即可,如下:

dependencies {
    //導(dǎo)航依賴庫(kù) 
    implementation "androidx.navigation:navigation-compose:2.4.2"
    
    //Gson解析,后邊用到
    implementation 'com.google.code.gson:gson:2.9.0'  
}

備注:上述導(dǎo)航組件是沒有動(dòng)畫的,如果需要增加跳轉(zhuǎn)動(dòng)畫,則需要引入google開發(fā)的帶動(dòng)畫的導(dǎo)航庫(kù),如下:

//帶動(dòng)畫的導(dǎo)航依賴庫(kù)
implementation "com.google.accompanist:accompanist-navigation-animation:0.24.3-alpha"

本文主要是通過navigation-compose導(dǎo)航來實(shí)現(xiàn)跳轉(zhuǎn),至于帶動(dòng)畫的,可以自行查看別的文章,實(shí)現(xiàn)大體一致。

使用Navigation導(dǎo)航用到兩個(gè)比較重要的對(duì)象NavHost和NavController。

  • NavHost用來承載頁(yè)面,和管理導(dǎo)航圖
  • NavController用來控制如何導(dǎo)航還有參數(shù)回退棧等

在官方給的例子中都是通過傳遞常用數(shù)據(jù)類型來實(shí)現(xiàn)跳轉(zhuǎn)時(shí)的參數(shù)傳遞。

我們先用compose實(shí)現(xiàn)需要路由導(dǎo)航的兩個(gè)界面FirstScreen、SecondScreen,代碼如下

@Composable
fun FirstScreen(navigateTo: () -> Unit) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.Red),

        horizontalAlignment = Alignment.CenterHorizontally,  //橫向居中
        verticalArrangement = Arrangement.Center,  //縱向居中
    ) {
        Text(text = "這是第一個(gè)界面")

        Button(onClick = {
            navigateTo.invoke()
        }) {
            Text(text = "跳轉(zhuǎn)到第二頁(yè)")
        }
    }
}

@Composable
fun SecondScreen(name: String?, age: Int?, navigateTo: () -> Unit) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.Green),

        horizontalAlignment = Alignment.CenterHorizontally,  //橫向居中
        verticalArrangement = Arrangement.Center,  //縱向居中
    ) {
        Text(text = "這是第二個(gè)界面 傳遞的參數(shù)為:姓名:$name; 年齡:${age}歲")

        Button(onClick = {
            navigateTo.invoke()
        }) {
            Text(text = "跳轉(zhuǎn)到第三頁(yè)")
        }
    }
}


@Composable
fun ThirdScreen(carName: String?, navigateTo: () -> Unit) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.Gray),

        horizontalAlignment = Alignment.CenterHorizontally,  //橫向居中
        verticalArrangement = Arrangement.Center,  //縱向居中
    ) {
        Text(text = "這是第三個(gè)界面 carName: $carName")

        Button(onClick = {
            navigateTo.invoke()
        }) {
            Text(text = "回到第一頁(yè)")
        }
    }
}

上述代碼中我們定義了兩個(gè)界面,界面只包含一個(gè)Text和一個(gè)Button,另外為了避免navController在各個(gè)界面中的傳遞,我們定義了一個(gè)函數(shù)navigateTo,用來回調(diào)跳轉(zhuǎn)操作,同時(shí)統(tǒng)一管理跳轉(zhuǎn)實(shí)現(xiàn),便于管理路由。

為了路由不容易出錯(cuò),我們定義兩個(gè)常量,如下

const val ROUTE_FIRST = "routeFirst"

const val ROUTE_SECOND = "routeSecond"

const val ROUTE_THIRD = "routeThird"

另外還需要一個(gè)實(shí)體類,如下

data class User(val name: String? = null, val age: Int = 0): Serializable

到這里我們的準(zhǔn)備工作就做完了,接下來干貨走起

無參數(shù)跳轉(zhuǎn)

我們通過一個(gè)小例子來感受一下

val navController = rememberNavController()  //導(dǎo)航控制器
NavHost(
    navController = navController,
    startDestination = Route.ROUTE_FIRST, //啟始頁(yè),該參數(shù)和`route`相對(duì)應(yīng),比如我現(xiàn)在啟始頁(yè)是第一個(gè)頁(yè)面,也就是`First` 
    builder = {
        composable(route = Route.ROUTE_FIRST) {  //route: 表示路由名稱,跳轉(zhuǎn)時(shí)需要
            FirstScreen {
                navController.navigate(Route.ROUTE_SECOND)
            }
        }

        composable(route = Route.ROUTE_SECOND) {

            SecondScreen("name", 0) {
                navController.navigate("Third?carName=五菱宏光 Mini")
            }
        }
    }
)
  • 通過 rememberNavController() 方法創(chuàng)建一個(gè)navController對(duì)象。

  • 創(chuàng)建 NavHost 對(duì)象,傳入navController并通過startDestination指定啟動(dòng)頁(yè)

  • 通過 composable() 方法往NavHost中添加頁(yè)面,構(gòu)造方法中的route就代表該頁(yè)面的路徑,后面的函數(shù)就是具體的頁(yè)面。

  • 通過navControllernavigate()實(shí)現(xiàn)最終路由跳轉(zhuǎn)

通過上面的代碼我們就實(shí)現(xiàn)了一個(gè)最簡(jiǎn)單的跳轉(zhuǎn),但是實(shí)際項(xiàng)目中頁(yè)面之間的跳轉(zhuǎn)免不了傳參。那么Compose是如何傳參的呢?

帶參(基本數(shù)據(jù)類型)跳轉(zhuǎn)

參數(shù)傳遞肯定有發(fā)送端和接受端,在Compose中,navController就是發(fā)送端,通過navController.navigate(路由名+參數(shù)值)發(fā)送,接受端通過NavHostcomposable(route=路由名+參數(shù)名, arugments = listOf())的route定義參數(shù)名以及通過arguments定義參數(shù)類型。如下偽代碼

NavHost(
    navController = navController,
    startDestination = "啟動(dòng)頁(yè)路由名A"
    builder = {
        composable(route = "啟動(dòng)頁(yè)路由名A") {
            FirstScreen {
                navController.navigate("路由名B/待傳遞參數(shù)b")
            }
        }

        composable(route = "路由名B/{參數(shù)名b}", arguments = listOf(
                    navArgument("參數(shù)名b") {
                        type = NavType.IntType   //參數(shù)類型
                        defaultValue = 18        //默認(rèn)值
                        nullable = true          //是否可空
                    }
                )
        ) {
        
            val name = it.arguments?.getString("參數(shù)名b")  //通過`參數(shù)名b`獲取參數(shù)返回值
        }
    }
)

完整示例:

@Composable
fun NavSample() {
    val navController = rememberNavController()  //導(dǎo)航控制器
    NavHost(
        navController = navController,
        startDestination = Route.ROUTE_FIRST,  //啟始頁(yè),該參數(shù)和`route`相對(duì)應(yīng),比如我現(xiàn)在啟始頁(yè)是第一個(gè)頁(yè)面,也就是`First`
        builder = {
            composable(route = Route.ROUTE_FIRST) {  //route: 表示路由名稱,跳轉(zhuǎn)時(shí)需要
                FirstScreen {
                    navController.navigate("${Route.ROUTE_SECOND}/Kevin/10")
                }
            }

            composable(
                    route = "${Route.ROUTE_SECOND}/{name}/{age}", arguments = listOf(
                    navArgument("age") {
                        type = NavType.IntType  //類型
                    }
                )
            ) {
                val name = it.arguments?.getString("name")
                val age = it.arguments?.getInt("age")
                SecondScreen(name, age) {
            
                }
            }
        }
    )
}

帶參(可選基本數(shù)據(jù)類型參數(shù))跳轉(zhuǎn)

上面?zhèn)鬟f的參數(shù)為必傳參數(shù),Navigation Compose還支持可選參數(shù)。可選參數(shù)和必傳參數(shù)有以下兩點(diǎn)不同:

  • 可選參數(shù)必須使用查詢參數(shù)語(yǔ)法?argName={argName} 來添加
  • 可選參數(shù)必須具有 defaultValuenullability = true (將默認(rèn)值設(shè)置為 null)

這意味著,所有可選參數(shù)都必須以及列表的形式顯式添加到 composable 方法中

即使沒有傳遞任何參數(shù),系統(tǒng)也會(huì)使用 Default Value 來作為參數(shù)值傳遞到目的地頁(yè)面

完整示例:

@Composable
fun NavSample() {
    val navController = rememberNavController()  //導(dǎo)航控制器
    NavHost(
        navController = navController,
        startDestination = Route.ROUTE_FIRST,  //啟始頁(yè),該參數(shù)和`route`相對(duì)應(yīng),比如我現(xiàn)在啟始頁(yè)是第一個(gè)頁(yè)面,也就是`First`
        builder = {
            composable(route = Route.ROUTE_FIRST) {  //route: 表示路由名稱,跳轉(zhuǎn)時(shí)需要
                FirstScreen {
                    navController.navigate("${Route.ROUTE_SECOND}/Kevin/10")
                }
            }

            composable(
                    route = "${Route.ROUTE_SECOND}/{name}/{age}", arguments = listOf(
                    navArgument("age") {
                        type = NavType.IntType  //類型
//                    defaultValue = 18  //默認(rèn)值
//                    nullable = true //是否可空
                    }
                )
            ) {
                val name = it.arguments?.getString("name")
                val age = it.arguments?.getInt("age")
                SecondScreen(name, age) {
                    navController.navigate("${Route.ROUTE_THIRD}?carName=五菱宏光 Mini")
                }
            }

            composable(
                route = "${Route.ROUTE_THIRD}?carName={carName}", arguments = listOf(
                    navArgument("carName") {
                        defaultValue = "保時(shí)捷卡宴"
                    }
                )
            ) {
                val carName = it.arguments?.getString("carName")
                ThirdScreen(carName) {
                    navController.popBackStack(
                        route = Route.ROUTE_FIRST,
                        inclusive = false //是否包含要跳轉(zhuǎn)的路由頁(yè)
                    )
                }
            }
        }
    )
}

帶參(序列化data class)跳轉(zhuǎn)

上述跳轉(zhuǎn)以及帶基本數(shù)據(jù)類型的跳轉(zhuǎn)其實(shí)Google官網(wǎng)說的比我清楚,更多客參考 使用Compose 進(jìn)行導(dǎo)航,但是我們的目的不止如此,我們最終的目的是如何實(shí)現(xiàn) Compose Navigation 傳遞 data class 實(shí)體類?

方案一 使用NavController自帶的Argument屬性

我們先來看看最終代碼,如下

@Composable
fun NavGraphSample1() {
    val navController = rememberNavController()
    NavHost(
        navController = navController,
        startDestination = Route.ROUTE_FIRST
    ) {
        composable(Route.ROUTE_FIRST) { navBackStackEntry ->
            FirstScreen1 {
                val args = listOf(Pair("intentText", User("Kevin", 10)))
                navController.navigateAndArgument(
                    Route.ROUTE_SECOND,
                    args = args
                )
            }
        }

        composable(Route.ROUTE_SECOND) {
            val intentText = it.arguments?.get("intentText") as User
            SecondScreen1(name = intentText.name, intentText.age) {

            }
        }
    }
}

fun NavController.navigateAndArgument(
    route: String,
    args: List<Pair<String, Any>>? = null,
    navOptions: NavOptions? = null,
    navigatorExtras: Navigator.Extras? = null,

    ) {
    navigate(route = route, navOptions = navOptions, navigatorExtras = navigatorExtras)

    if (args == null && args?.isEmpty() == true) {
        return
    }

    val bundle = backQueue.lastOrNull()?.arguments
    if (bundle != null) {
        bundle.putAll(bundleOf(*args?.toTypedArray()!!))
    } else {
        println("The last argument of NavBackStackEntry is NULL")
    }
}

基本上就是這樣獲取和添加Route對(duì)應(yīng)的NavBackStackEntry。

  • ? :當(dāng)調(diào)用 NavController#navigate 時(shí),根據(jù)傳遞的路由找到匹配的 DeepLinkbackQueue并添加。
  • ? : 獲取操作?添加的 NavBackStackEntry 的 Argument。
  • ?:將必要的數(shù)據(jù)添加到 Argument。

代碼說明

NavController 內(nèi)部包含我們添加到 BackStack 的 Entry。我們得到這里包含的 NavBackStackEntry 并使用它。

public open class NavController { 
        // ... 
        /** 
        * Retrieve the current back stack. 
        * 
        * @return The current back stack. 
        * @hide 
        */ 
        @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public open val backQueue: ArrayDeque<NavBackStackEntry> = ArrayDeque()
        // ... 
}

backQueue 屬性是 LIBRARY_GROUP,因此將來可能無法從外部訪問它。

讓我們看看在 NavBackStackEntry 中添加數(shù)據(jù)的參數(shù)。arguments 屬性定義為可空且只讀,因此實(shí)際訪問時(shí)有可能為空,因此數(shù)據(jù)傳遞可能會(huì)失敗。

public class NavBackStackEntry private constructor( 
        // ...
        /** 
        * The arguments used for this entry 
        * @return The arguments used when this entry was created 
        */
        public val arguments: Bundle? = null,
)

因此,該方案可能出現(xiàn)參數(shù)傳遞為空的情況。

方案二 共享 ViewModel

共享ViewModel相對(duì)就比較簡(jiǎn)單了,直接看代碼

/**
 * ViewModel共享數(shù)據(jù)
 */
@Composable
fun NavGraphSample2() {

    val viewModel: Sample2ViewModel = viewModel()

    val navController = rememberNavController()

    NavHost(navController = navController, startDestination = Route.ROUTE_FIRST) {

        composable(route = Route.ROUTE_FIRST) {

            FirstScreen1 {

                viewModel.user = User("Kevin", 11)

                navController.navigate(route = Route.ROUTE_SECOND)
            }
        }

        composable(route = Route.ROUTE_SECOND) { navBackStackEntry ->

            println("NavGraphSample2 print: user: ${viewModel.user}")
        }
    }
}

class Sample2ViewModel: ViewModel() {
    var user: User? = User()
}

通過對(duì)ViewModel中的user對(duì)象賦值和取值來達(dá)到效果

注意:上述ViewModel只能實(shí)例化一次,也就是賦值和取值應(yīng)該用同一個(gè)ViewModel,否則數(shù)據(jù)將無法共享。

方案三 自定義 NavType 實(shí)現(xiàn)

我們將通過 Serializable/Parcelable。而且,序列化處理使用Kotlin Serialization。使用下面建模的類實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的形式。

我們定義兩個(gè)實(shí)體類,如下

@kotlinx.serialization.Serializable
data class UserSerializable(val name: String? = null, val age: Int = 0) : Serializable

@Parcelize
data class UserParcelable(val name: String? = null, val age: Int = 0) : Parcelable

然后,創(chuàng)建自定義 NavType,出于封裝的考慮,我們還為 Serializable/Parcelable 類型創(chuàng)建了工廠函數(shù)。下面的函數(shù)在每次調(diào)用時(shí)創(chuàng)建并返回一個(gè)新的 NavType。

inline fun <reified T : Serializable> createSerializableNavType(
    isNullableAllowed: Boolean = false
): NavType<T> {
    return object : NavType<T>(isNullableAllowed) {

        override val name: String
            get() = "SupportSerializable"

        override fun get(bundle: Bundle, key: String): T? {  //從Bundle中檢索 Serializable類型
            return bundle.getSerializable(key) as? T
        }

        override fun put(bundle: Bundle, key: String, value: T) {  //作為 Serializable 類型添加到 Bundle
            bundle.putSerializable(key, value)
        }

        override fun parseValue(value: String): T {  //定義傳遞給 String 的 Parsing 方法
            return Gson().fromJson(value, T::class.java)
        }
    }
}

inline fun <reified T : Parcelable> createParcelableNavType(isNullableAllowed: Boolean = false): NavType<T> {
    return object : NavType<T>(isNullableAllowed) {

        override val name: String
            get() = "SupportParcelable"

        override fun get(bundle: Bundle, key: String): T? {  //從Bundle中檢索 Parcelable類型
            return bundle.getParcelable(key)
        }

        override fun parseValue(value: String): T {  //定義傳遞給 String 的 Parsing 方法
            return Gson().fromJson(value, T::class.java)
        }

        override fun put(bundle: Bundle, key: String, value: T) {  //作為 Parcelable 類型添加到 Bundle
            bundle.putParcelable(key, value)
        }

    }
}

接下來,我們使用前面自定義的 NavTypeNavGraphBuilder 中定義 Composable。

@Composable
fun NavGraphSample3() {

    val navController = rememberNavController()

    NavHost(
        navController = navController,
        startDestination = Route.ROUTE_FIRST,
        builder = {
            composable(Route.ROUTE_FIRST) {
                FirstScreen1 {
                    val jsonSerializable = Gson().toJson(UserSerializable("KevinSerializable", 100))
                    val jsonParcelable = Gson().toJson(UserParcelable("KevinParcelable", 200))
                    navController.navigate(Route.ROUTE_SECOND + "?key=${Uri.encode(jsonSerializable)}&key1=${Uri.encode(jsonParcelable)}")
                }
            }
            composable(
                Route.ROUTE_SECOND + "?key={test_serializable}&key1={test_parcelable}",
                arguments = listOf(
                    navArgument("test_serializable") {
                        type = createSerializableNavType<UserSerializable>()
                    },
                    navArgument( "test_parcelable") {
                        type = createParcelableNavType<UserParcelable>()
                    }
                )
            ) { navBackStackEntry ->  //根據(jù)導(dǎo)航規(guī)范定義路由和參數(shù)

                val arguments = navBackStackEntry.arguments

                val userBean = arguments?.getSerializable("test_serializable") as? UserSerializable

                val userParcelableBean = arguments?.getParcelable<UserParcelable>("test_parcelable")

                println("NavGraphSample3 serializable print: name: ${userBean?.name}; age: ${userBean?.age}")

                println("NavGraphSample3 parcelable print: name: ${userParcelableBean?.name}; age: ${userParcelableBean?.age}")

                SecondScreen1(name = userBean?.name, age = userBean?.age) {

                }
            }
        }
    )
}
  • 首先將 data class實(shí)體類轉(zhuǎn)換成json,通過navigate(路由名?key={參數(shù)1}&key1={參數(shù)2})執(zhí)行數(shù)據(jù)傳遞

  • 其次通過composable進(jìn)行路由定位,并且通過不同的arguments定義不同的類型

  • 最后使用NavBackStackEntry提供的argumentsgetSerializable()或者getParcelable()來獲取實(shí)體數(shù)據(jù)

通過上述做法,我們就可以很方便的實(shí)現(xiàn)攜帶Serializable/Parcelable參數(shù)數(shù)據(jù)跳轉(zhuǎn)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容