該文章將是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}次")
}
}
先是點擊兩下的效果,由于state
為false
時,沒有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}次")
}
}
點擊三下的效果,LaunchedEffect
的key
唯一,重復觸發重組,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
內部變量的引用
rememberUpdatedState
在remember
的基礎上做了更新值處理,每次調用到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
可以將 Compose
的 State
轉為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
中有一些效應(如 LaunchedEffect
、produceState
或 DisposableEffect
)會采用可變數量的參數和鍵來取消運行效應,并使用新的鍵啟動一個新的效應。在實際開發中,靈活運用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
中調用ComposeView
的setContent()
方法,并使用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
為例,通過deepLinks
將url
進行綁定:
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)
...
}