该文章将是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)
...
}