赞
踩
该文章将是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() {
- // 0cd5ba83-97c1-485e-87ff-272e4fd6afa7
- 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)
- ...
- }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。