compose--附帶效應、傳統項目集成、導航

該文章將是compose基礎系列中最后一篇,附帶效應是這篇文章的重點,其余補充內容為如何在傳統xml中集成compose、compose導航的使用

一、附帶效應

有了前面的了解,我們知道compose中是由State狀態發生改變來使得可組函數發生重組,狀態的改變應該是在可組合函數作用域中,但有時我們需要它發生在別的作用域,如定時彈出一個消息,這就需要附帶效應出場了,compose定義了一系列附帶效應API,來運用在可組合函數作用域內外,發生狀態改變的不同場景

1.LaunchedEffect

LaunchedEffect我們之前就已經使用過了,特別是在低級別動畫時,LaunchedEffect用于安全地調用掛起函數,本質就是啟動一個協程,LaunchedEffect的調用需要在可組合函數作用域內

LaunchedEffect的執行分為以下三種,優先級由上到下:

  • 當發生重組時LaunchedEffect退出組合,將取消協程
  • 當發生重組時如果LaunchedEffect使用的同一個key,只會執行第一次,如果上次LaunchedEffect沒執行結束,不重新執行
  • 當發生重組時如果LaunchedEffect使用的不同的key,并且上次LaunchedEffect沒執行結束,則取消上次執行,啟動新的協程執行該次任務

例子:

@Preview
@Composable
fun MyLaunchEffect() {
    var state by remember { mutableStateOf(false) }
    var count by remember { mutableStateOf(0) }

    if (state) {
        // key為Unit唯一值
        LaunchedEffect(Unit) {
            delay(3000)
            count++
        }
    }

    Box(modifier = Modifier
        .size(50.dp)
        .background(Color.Cyan)
        .clickable { state = !state }
    ) {
        Text("執行了${count}次")
    }
}

先是點擊兩下的效果,由于statefalse時,沒有LaunchedEffect的代碼塊,此時LaunchedEffect會取消:

稍微改變下例子的代碼,一旦狀態發生改變,那么重復執行LaunchedEffect

@Preview
@Composable
fun MyLaunchEffect2() {
    var state by remember { mutableStateOf(0) }
    var count by remember { mutableStateOf(0) }

    if (state > 0) {
        // key為Unit唯一值
        LaunchedEffect(Unit) {
            delay(3000)
            count++
        }
    }

    Box(modifier = Modifier
        .size(50.dp)
        .background(Color.Cyan)
        .clickable { state++ }
    ) {
        Text("執行了${count}次")
    }
}

點擊三下的效果,LaunchedEffectkey唯一,重復觸發重組,key唯一時只會執行第一次的LaunchedEffect

改變例子代碼,每次執行的key不同:

@Preview
@Composable
fun MyLaunchEffect3() {
    var state by remember { mutableStateOf(0) }
    var count by remember { mutableStateOf(0) }

    if (state > 0) {
        // key為隨機值
        LaunchedEffect(UUID.randomUUID()) {
            delay(3000)
            // 置為0,防止不斷重組導致一直執行LaunchedEffect
            state = 0
            count++
        }
    }

    Box(modifier = Modifier
        .size(50.dp)
        .background(Color.Cyan)
        .clickable { state++ }
    ) {
        Text("執行了${count}次")
    }
}

效果,取消了之前的LaunchedEffect,隔了3秒后才發生count狀態改變:

2.rememberCoroutineScope

rememberCoroutineScope也是使用過的,它返回一個remember的協程作用域,可以在可組合函數外使用,調用幾次執行幾次

例子:

@Preview
@Composable
fun MyRememberCoroutineScope() {
    val scope = rememberCoroutineScope()
    var count by remember { mutableStateOf(0) }

    Box(modifier = Modifier
        .size(50.dp)
        .background(Color.Cyan)
        .clickable {
            scope.launch {
                delay(3000)
                count++;
            }
        }
    ) {
        Text("執行了${count}次")
    }
}

效果:

3.rememberUpdatedState

LaunchedEffect一旦啟動,同一個key其內部的方法調用和引用都是final的,即無法更改,如果LaunchedEffect內使用的外部引用可能發生改變,應該使用rememberUpdatedState

3.1 不使用remember

先來看一個例子,我在重組時生成一個隨機數,并作為onTimeout()的打印參數,將onTimeout()傳給MyRememberUpdatedState,LaunchedEffect內調用onTimeout()打印這個隨機數:

@Preview
@Composable
fun MyTimeout() {
    var state by remember { mutableStateOf(false) }

    Column {
        // 1.生成隨機數
        val random = Random.nextInt()
        Log.i("onTimeout", "return : $random")
        MyRememberUpdatedState(state) {
            // 4.打印隨機數
            Log.i("onTimeout", "onTimeout() return : $random")
        }

        Button(onClick = { state = !state }) {
            Text("click")
        }
    }
}

@Composable
fun MyRememberUpdatedState(enable: Boolean, onTimeout: () -> Unit) {
    // 使用rememberUpdatedState
//    val rememberUpdatedState by rememberUpdatedState(onTimeout)
    val rememberUpdatedState = onTimeout

    // 2.key唯一發生重組,不會重新執行
    LaunchedEffect(true) {
        delay(5000)
        // 3.延遲5s,調用外部傳入的onTimeout()
        rememberUpdatedState()
    }

    if (enable)
        Text("hi")
    else
        Text("hello")
}

我點擊多次,這次的效果直接看日志即可:

可以看到最后打印的結果,是第一次生成的隨機數

3.2 使用remember

我們嘗試使用remember,將onTimeout作為State狀態并記住,并以onTimeout作為key使得每次onTimeout發生改變,觸發值的更新:

@Preview
@Composable
fun MyTimeout() {
    var state by remember { mutableStateOf(false) }

    Column {
        // 1.生成隨機數
        val random = Random.nextInt()
        Log.i("onTimeout", "return : $random")
        MyRememberUpdatedState(state) {
            // 4.打印隨機數
            Log.i("onTimeout", "onTimeout() return : $random")
        }

        Button(onClick = { state = !state }) {
            Text("click")
        }
    }
}

@Composable
fun MyRememberUpdatedState(enable: Boolean, onTimeout: () -> Unit) {
    // 使用rememberUpdatedState
//    val rememberUpdatedState by rememberUpdatedState(onTimeout)
    val rememberUpdatedState by remember(onTimeout) { mutableStateOf(onTimeout) }
//    val rememberUpdatedState = onTimeout

    // 2.key唯一發生重組,不會重新執行
    LaunchedEffect(true) {
        delay(5000)
        // 3.延遲5s,調用外部傳入的onTimeout()
        rememberUpdatedState()
    }

    if (enable)
        Text("hi")
    else
        Text("hello")
}

打印的結果,依然是第一次生成的隨機數:

3.3 使用rememberUpdatedState

rememberUpdatedState可以始終保持最新的值,從而改變LaunchedEffect運行時的引用的值

@Preview
@Composable
fun MyTimeout() {
    var state by remember { mutableStateOf(false) }

    Column {
        // 1.生成隨機數
        val random = Random.nextInt()
        Log.i("onTimeout", "return : $random")
        MyRememberUpdatedState(state) {
            // 4.打印隨機數
            Log.i("onTimeout", "onTimeout() return : $random")
        }

        Button(onClick = { state = !state }) {
            Text("click")
        }
    }
}

@Composable
fun MyRememberUpdatedState(enable: Boolean, onTimeout: () -> Unit) {
    // 使用rememberUpdatedState
    val rememberUpdatedState by rememberUpdatedState(onTimeout)
//    val rememberUpdatedState by remember{ mutableStateOf(onTimeout) }
//    val rememberUpdatedState = onTimeout

    // 2.key唯一發生重組,不會重新執行
    LaunchedEffect(true) {
        delay(5000)
        // 3.延遲5s,調用外部傳入的onTimeout()
        rememberUpdatedState()
    }

    if (enable)
        Text("hi")
    else
        Text("hello")
}

打印結果:

原理:首先我們知道remember相當于創建了一個靜態變量,如果不指定key,只會初始化一次,重復調用remember并不會更新引用,指定key時,當key發生變化,則會更新引用
LaunchedEffect運行時會復制引用,新建變量指向傳入的引用,所以此時無論外部變量的引用發生如何改變,并不會改變LaunchedEffect內部變量的引用
rememberUpdatedStateremember的基礎上做了更新值處理,每次調用到rememberUpdatedState時,將值更新,也就是引用的值的更新,此時不管外部變量還是LaunchedEffect內部變量的值引用都會發生變化,LaunchedEffect調用的自然就是最新的方法了,下面是rememberUpdatedState的源碼:

@Composable
fun <T> rememberUpdatedState(newValue: T): State<T> = remember {
    mutableStateOf(newValue)
}.apply { value = newValue }

4.DisposableEffect

DisposableEffect可以在key變化和移除時做一些善后工作,需實現onDispose

例子:

@Preview
@Composable
fun MyDisposableEffect() {
    var state by remember { mutableStateOf(false) }
    var text by remember { mutableStateOf("click") }
    val scope = rememberCoroutineScope()

    if (state) {
        // 重組或移除時會調用onDispose
        DisposableEffect(Unit) {
            val job = scope.launch { 
                delay(3000)
                text = "點了"
            }

            onDispose {
                job.cancel()
                text = "取消了"
            }
        }
    }

    Button(onClick = { state = !state }) {
        Text(text)
    }
}

效果,在3s內點擊了兩次,導致重組時移除DisposableEffect而觸發onDispose

5.SideEffect

SideEffect會在可組合函數重組完成時調用,可以進行用戶行為分析、日志記錄等操作

例子:

@OptIn(ExperimentalAnimationApi::class)
@Preview
@Composable
fun MySideEffect() {
    var enable by remember { mutableStateOf(false) }

    Column {
        AnimatedVisibility(
            visible = enable,
            enter = scaleIn(tween(2000)),
            exit = scaleOut(tween(2000))
        ) {
            MySideEffectText("hello world")
        }

        Button(onClick = { enable = !enable }) {
            Text("click")
        }
    }
}

@Composable
fun MySideEffectText(text: String) {
    SideEffect {
        Log.i("SideEffect", "重組完成")
    }

    Text(text)
}

效果,如果組件重組完成了,連續點擊導致動畫重復執行,則不會觸發重組:

6.produceState

produceState 會啟動一個協程,并返回一個State對象,用來將非 Compose 狀態轉換為 Compose 狀態,即執行一些耗時操作,如網絡請求,并將結果作為State對象返回

例子:

@Preview
@Composable
fun MyProduceState() {
    var visiable by remember { mutableStateOf(false) }

    Column {
        if (visiable)
            Text(load().value)

        Button(onClick = { visiable = !visiable }) {
            Text("load")
        }
    }
}

@Composable
fun load(): State<String> {
    return produceState(initialValue = "", producer = {
        delay(2000);

        value = "hi"
    })
}

效果:

7.derivedStateOf

derivedStateOf可以將一個或多個狀態對象轉變為其他的狀態對象,一旦狀態發生改變,只會在用到該derivedStateOf狀態的地方進行重組

例子,根據傳入的list,過濾高亮的元素,并展示到列表中:

val alpha = arrayOf("a", "b", "c", "d", "e", "f", "g", "h")

@Preview
@Composable
fun MyDerivedStateOf() {
    val items = remember { mutableStateListOf<String>() }

    Column {
        Button(onClick = { items.add(alpha[Random.nextInt(alpha.size)]) }) {
            Text("Add")
        }

        DerivedStateOf(items, highPriorityKeywords = listOf("a", "b"))
    }
}

/**
 * 擁有highPriorityKeywords的優先顯示
 */
@Composable
fun DerivedStateOf(
    lists: List<String>,
    highPriorityKeywords: List<String> = listOf("Review", "Unblock", "Compose")
) {
    // 需要高亮置頂的items
    val highPriorityLists by remember(highPriorityKeywords) {
        derivedStateOf { lists.filter { it in highPriorityKeywords } }
    }

    LazyColumn {
        items(highPriorityLists) { value ->
            Text(value, color = Color.Red)
        }
        items(lists) { value ->
            Text(value)
        }
    }
}

效果:

8.snapshotFlow

snapshotFlow可以將 ComposeState 轉為Flow,當在 snapshotFlow 塊中讀取的 State 對象之一發生變化時,如果新值與之前發出的值不相等,Flow 會向其收集器發出新值

@Preview
@Composable
fun MySnapshotFlow() {
    val listState = rememberLazyListState()
    val list = remember {
        mutableListOf<Int>().apply {
            repeat(1000) { index ->
                this += index
            }
        }
    }

    LazyColumn(state = listState) {
        items(list) {
            Text("hi:${it}")
        }
    }

    LaunchedEffect(Unit) {
        snapshotFlow {
            listState.firstVisibleItemIndex
        }.collect { index ->
            Log.i("collect", "${index}")
        }
    }
}

滾動查看日志:

9.重啟效應

Compose 中有一些效應(如 LaunchedEffectproduceStateDisposableEffect)會采用可變數量的參數和鍵來取消運行效應,并使用新的鍵啟動一個新的效應。在實際開發中,靈活運用key是否唯一來使得是否需要重啟效應

二、傳統項目集成

官方推薦一次性替換整個布局,也可以替換部分布局,本身compose就兼容傳統xml的方式,所以在傳統的項目上集成compose很容易

1.xml中使用compose

xml中使用ComposeView,表示一個加載compose的控件:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ComposeIntegrateActivity">

    <TextView
        android:id="@+id/textView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="hello android"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <androidx.compose.ui.platform.ComposeView
        android:id="@+id/composeView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/textView2" />

</androidx.constraintlayout.widget.ConstraintLayout>

Activity中調用ComposeViewsetContent()方法,并使用compose:

class ComposeIntegrateActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_compose_integrate)

        val composeView = findViewById<ComposeView>(R.id.composeView)
        composeView.setContent {
            MyComposeApplicationTheme {
                MyText1()
            }
        }
    }

    @Composable
    fun MyText1() {
        Text("hi compose")
    }
}

啟動效果:

2.fragment中使用

fragment中要多一步綁定View樹生命周期:

class BlankFragment : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val disposeOnViewTreeLifecycleDestroyed =
            ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
        val root = inflater.inflate(R.layout.fragment_blank, container, false)
        root.findViewById<ComposeView>(R.id.fragment_composeView).apply {
            setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
            setContent {
                MaterialTheme() {
                    // In Compose world
                    Text("Hello Compose!")
                }
            }
        }
        return root
    }
}

三、導航

compose定義了全新的導航API,下面來開始使用它

1.導入依賴

    def nav_version = "2.5.3"

    implementation "androidx.navigation:navigation-compose:$nav_version"

2.創建 NavHost

NavHost需要一個navController用于控制導航到那個可組合項,startDestination 初始的可組合項,以及NavGraphBuilder導航關系圖

class NaviActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyComposeApplicationTheme {
                MyNavi()
            }
        }
    }
}

@Preview
@Composable
fun MyNavi() {
    val navController = rememberNavController()

    NavHost(navController = navController, startDestination = "home") {
        composable("home") { Home() }
        composable("message") { Message() }
        composable("mine") { Mine() }
    }
}

@Composable
fun Home() {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Text("Home")
    }
}

@Composable
fun Message() {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Text("Message")
    }
}

@Composable
fun Mine() {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Text("Mine")
    }
}

效果:

3.navController

接下來使用navController來導航到不同的可組合項,下面是官方給出的示例的幾種方式:

  • 在導航到“friendslist”并加到返回堆棧中
navController.navigate("friendslist")
  • 在導航到“friendslist”之前,將所有內容從后堆棧中彈出到“home”(不包含home)
navController.navigate("friendslist") {
    popUpTo("home")
}
  • 在導航到“friendslist”之前,從堆棧中彈出所有內容,包括“home”
navController.navigate("friendslist") {
    popUpTo("home") { inclusive = true }
}
  • 只有當我們還不在“search”時,才能導航到“search”目標地,避免在后堆棧的頂部有多個副本
navController.navigate("search") {
    launchSingleTop = true
}

例子:

我們給App添加上Scaffold,并在底部導航欄進行navController導航的控制

class NaviActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyComposeApplicationTheme {
                Scene()
            }
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Scene() {
    val navController = rememberNavController()

    Surface(Modifier.background(MaterialTheme.colorScheme.surface)) {
        Scaffold(
            topBar = {
                TopAppBar(
                    title = {
                        Text(
                            stringResource(id = R.string.app_name),
                            color = MaterialTheme.colorScheme.onPrimaryContainer
                        )
                    },
                    colors = TopAppBarDefaults.smallTopAppBarColors(
                        containerColor = MaterialTheme.colorScheme.primaryContainer
                    )
                )
            },
            bottomBar = {
                Row(
                    modifier = Modifier
                        .fillMaxWidth()
                        .background(MaterialTheme.colorScheme.primaryContainer)
                        .padding(10.dp),
                    horizontalArrangement = Arrangement.SpaceAround
                ) {
                    CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onPrimaryContainer) {
                        Icon(
                            Icons.Rounded.Home, contentDescription = null,
                            modifier = Modifier.clickable {
                                navController.navigate("home") {
                                    launchSingleTop = true
                                    popUpTo("home")
                                }
                            }
                        )
                        Icon(
                            Icons.Rounded.Email, contentDescription = null,
                            modifier = Modifier.clickable {
                                navController.navigate("message") {
                                    launchSingleTop = true
                                    popUpTo("message")
                                }
                            }
                        )
                        Icon(
                            Icons.Rounded.Face, contentDescription = null,
                            modifier = Modifier.clickable {
                                navController.navigate("mine") {
                                    launchSingleTop = true
                                    popUpTo("mine")
                                }
                            }
                        )
                    }
                }
            }
        ) { paddings ->
            MyNavi(
                modifier = Modifier.padding(paddings),
                navController = navController,
                startDestination = "home"
            ) {
                composable("home") { Home() }
                composable("message") { Message() }
                composable("mine") { Mine() }
            }
        }
    }
}

@Composable
fun MyNavi(
    modifier: Modifier = Modifier,
    navController: NavHostController,
    startDestination: String,
    builder: NavGraphBuilder.() -> Unit
) {
    NavHost(
        modifier = modifier,
        navController = navController,
        startDestination = startDestination
    ) {
        builder()
    }
}

@Composable
fun Home() {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Text("Home")
    }
}

@Composable
fun Message() {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Text("Message")
    }
}

@Composable
fun Mine() {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Text("Mine")
    }
}

效果:

4.參數傳遞

Navigation Compose 還支持在可組合項目的地之間傳遞參數,方式為Restful風格,這種風格的參數為必填:

MyNavi(
modifier = Modifier.padding(paddings),
navController = navController,
startDestination = "home/b1254"
) {
    composable("home/{userId}") { Home() }
    composable("message/{count}") { Message() }
    composable("mine/{userId}") { Mine() }
}

...

// 導航時帶入參數
navController.navigate("mine/a1587")

參數類型默認為字符串,也可以通過navArgument指定參數的類型:

composable(
"home/{userId}",
arguments = listOf(navArgument("userId") { type = NavType.StringType })
) { Home() }

通過 lambda 中提供的NavBackStackEntry中提取這些參數:

composable(
"home/{userId}",
arguments = listOf(navArgument("userId") { type = NavType.StringType })
) {navBackStackEntry ->
    navBackStackEntry.arguments?.getString("userId")
    Home()
}

可選參數可以使用:?argName={argName} 來添加:

composable(
"message?count={count}",
arguments = listOf(navArgument("count") {
    type = NavType.IntType
    defaultValue = 0
})
) { Message() }

5.深層鏈接

深層鏈接照搬了官方文檔:深層鏈接

如果你想要將特定的網址、操作或 MIME 類型與導航綁定,實現對外提供跳轉應用的功能,那么使用深層鏈接可以很方便的實現這個功能

url為例,通過deepLinksurl進行綁定:

val uri = "https://www.example.com"

composable(
    "profile?id={id}",
    deepLinks = listOf(navDeepLink { uriPattern = "$uri/{id}" })
) { backStackEntry ->
    Profile(navController, backStackEntry.arguments?.getString("id"))
}

manifest中注冊配置:

<activity …>
  <intent-filter>
    ...
    <data android:scheme="https" android:host="www.example.com" />
  </intent-filter>
</activity>

外部通過PendingIntent進行跳轉:

val id = "exampleId"
val context = LocalContext.current
val deepLinkIntent = Intent(
    Intent.ACTION_VIEW,
    "https://www.example.com/$id".toUri(),
    context,
    MyActivity::class.java
)

val deepLinkPendingIntent: PendingIntent? = TaskStackBuilder.create(context).run {
    addNextIntentWithParentStack(deepLinkIntent)
    getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
}

6.封裝導航圖

隨著業務的越來越復雜,導航圖也可能分為模塊化,可以在NavHost作用域中使用navigation進行封裝:

NavHost(navController, startDestination = "home") {
    ...
    // Navigating to the graph via its route ('login') automatically
    // navigates to the graph's start destination - 'username'
    // therefore encapsulating the graph's internal routing logic
    navigation(startDestination = "username", route = "login") {
        composable("username") { ... }
        composable("password") { ... }
        composable("registration") { ... }
    }
    ...
}

使用擴展函數將更好的對模塊進行封裝:

fun NavGraphBuilder.loginGraph(navController: NavController) {
    navigation(startDestination = "username", route = "login") {
        composable("username") { ... }
        composable("password") { ... }
        composable("registration") { ... }
    }
}
NavHost(navController, startDestination = "home") {
    ...
    loginGraph(navController)
    ...
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容