Compose:状态更新

Recompose 重组与 remember

我们先上一段代码,你觉得下面代码最终执行结果是什么:

setContent {
	// 注意:这里将 MutableState 放在 setContent {} 内
    var name by mutableStateOf("name") 
    Text(name)

    lifecycleScope.launch {
        delay(3000)
        name = "vincent"
    }
}

Text() 最终显示的结果还是 name,并没有在 3s 后更新为设置的值。为什么会这样呢?

实际上上面的代码经过编译器编译后,代码类似是这样的:

// 伪代码
setContent {
	WrapperFunction {
		var name by mutableStateOf("name")
		Text(name)
    	
    	lifecycleScope.launch {
        	delay(3000)
        	name = "vincent"
    	}
	}
}

被通知刷新的 Text(name) 会被处理为例如被一个 WrapperFunction 包裹起来标记,在后续 name 更新的时候就会执行 WrapperFunction 代码块中的代码;因为 name 也被包裹在 WrapperFunction,所以 name 已经不是原先的那个 name,而是一个新的 MutableState,所以刷新失效了。

这种 重复重新执行初始化(例如重复执行 mutableStateOf)的方式称为 Recompose 重组,经过上面的案例分析也能知道,Recompose 也是有作用范围的,案例中就是 WrapperFunction 包裹的范围,这个 作用范围也称为 Recompose Scope 重组作用域

解决上面的问题也很简单,其中一种解决方案是将被通知更新的 Composable 再包裹一层 Composable 即可:

setContent {
	var name by mutableStateOf("name")
	Button(onClick = {}) {
		Text(name) // Text() 被包多一层 Button()
	}

   	lifecycleScope.launch {
       	delay(3000)
       	name = "vincent"
   	}
}

上面的代码经过编译器编译会生成类似如下代码:

// 伪代码
setContent {
	WrapperFunction {
		var name by mutableStateOf("name")
		Button(onClick = {}) {
			WrapperFunction {
				Text(name)
			}
		}
	}
	
	lifecycleScope.launch {
       	delay(3000)
       	name = "vincent"
   	}
}

不过这种并不是解决方案。所以 Compose 为我们提供了 remember。

setContent {
	var name by remember { mutableStateOf("name") }
	Text(name)

	lifecycleScope.launch {
		delay(3000)
		name = "vincent"
	}
}

remember 起到缓存的作用,当第一次执行的时候,会将 lambda 代码块的内容执行然后返回结果;同时也会保存这个结果,后续再调用它的时候,如果返回结果相同就不会重复执行直接返回。

remember 主要就是用于 Recompose 的场景,因为 Composable 可能在内部或外部都有可能存在 Recompose,所以解决 Recompose 有以下处理:

  • 在每个 Composable 里面可能会重复计算的处理上(例如 mutableStateOf)都加上 remember

  • 放在 Composable 外面的就不用,因为放在外面的 mutableStateOf 不会重复初始化,也不能用 remember,因为 remember 也是一个 Composable

@Composable
fun ShowCharCount(value: String) {
	val length = remember { value.length }
	Text("字符串的长度是 $length")
}

假设 value.length 是一个可能会重复计算且会影响性能的操作,上面的代码使用了 remember 避免了 Recompose 重复计算的问题,刚刚也说过 remember 相当于一个缓存,那如果 value 的值改变了,按上面的写法其实还是获取 remember 缓存的旧值,这就存在准确性的问题。

所以这时候就要使用 remember 带 key 的函数,在改变时 key 不一样就会重新执行然后再缓存起来

fun ShowCharCount(value: String) {
	// 以 value 作为 key,如果 value 和上一次 remember 的缓存值不一样了,重新执行 lambda
	val length = remember(value) { value.length }
	// remember(value, ....) 支持多个 key
	Text("字符串的长度是 $length")
}

无状态与状态提升(State Hoisting)

Compose 它是无状态的(Stateless),这里指的 “状态“ 可以认为是属性,例如 TextView 我们可以通过 getText()/setText() 设置 text 属性,而 在 Compose 通过 Text(name) 设置了 name 后,你会发现通过 Text() 拿不到 name 的,这就是所谓的无状态。它是 Compose 的一个特点,并不是一个功能。但需要注意的是,无状态说的是 Composable 可以无状态,而不是不能有状态

setContent {
	Hello()
}

@Composable
fun Hello() {
	// text 对 Hello() 而言就是属性,也就是 text 是 Hello() 的内部状态
	// tet 对于 Text() 而言是外部状态
	var text = "Hello"
	Text(text)
}

text 对于 Hello() 就是一个属性,一个仅仅它自己可见的变量,你可以说现在 Hello() 它是有状态的,text 是 Hello() 的内部状态。

但在 Hello() 外部还是不能获取到 text,因为它是 Hello() 函数的局部变量,Hello() 函数外部是拿不到的。

所以对于无状态其实还需要做进一步的解释:内部无状态

比如 Text() 无法拿到 text 给它的数值,但是 Text() 外部(即 Hello() 函数内)text 是能获取到的,text 是 Text() 的外部状态。

那么如果想拿 Hello() 函数的 text,就是把 text 从内部状态变成外部状态,也就是 text 变成参数公开给外部访问:

setContent {
	Box {
		var text = "Hello" // text 要让外部也可访问,将它从 Hello() 抽离出来
		Hello(text)
	}
}

@Composable
fun Hello(text: String) {
	Text(text)
}

这种将内部状态改成外部状态可以公开给外部访问的操作称为状态提升(State Hoisting)

当然这种操作并不是让你将一个变量变成一个全局变量,而是将变量提到一个合适的外部,状态尽量不往上提,这样能尽可能的降低出错的概率。比如上面的 demo 只想 text 在 Box() 内可访问,Box() 外部是不可访问的。

状态提升在业务场景应该怎么很好的使用呢?我们通过 TextField 了解下:

setContent {
	var name by remember { mutableStateOf("") }
	TextField(name, { 
		// 要手动修改 name 才会更新输入框的内容
		name = it 
	})
}

TextField 相当于 EditText 输入框,它是 material 包下的,foundation 的是 BasicTextField。

可以看到 name 作为状态提升到外层,按照我们的想法,既然是输入框那我输入文本就应该自动改变,但 Compose 并没有为我们这么做,而是要我们自己将修改 name 的操作手动在一个函数类型的 lamba 实现。

看着挺麻烦的,其实这提供了更多的可操作性,name 不会被 TextField() 内部修改导致信息源不单一,通过将交互操作也暴露出来统一让上层处理,在复杂的 Composable 能很好的对状态统一处理,也满足无状态的特点。

所以 对于有交互功能的 Composable,除了要将状态外提,还需要将操作也外提,这样才是完整的状态提升。例如上面的 TextField() 除了 name 状态提升了,输入操作用函数类型的参数也暴露出来提升状态,由程序员自己处理 name,这样才是完整的状态提升。

List 状态更新

var numbers by mutableStateOf(mutableListOf(1, 2, 3))
setContent {
	Column {
		Button(onClick = { numbers.add(numbers.last() + 1) }) {
			Text("加 1")
		}
		for (number in numbers) {
			Text("第 $number 块文字")
		}
	}
}

上面的 UI 是在一个 Column 显示一个按钮和多个文字,按钮点击时 Column 就累加一个文字。但运行上面的结果你会发现并没有发生更新。

导致这个问题的原因是 by mutableStateOf 是代理的 numbers 的 setValue 和 getValue,即 MutableState 是代理的 MutableList 本身的值被替换了,而不是 MutableList 内部元素的变更。Button 点击事件中的 numbers.add() 并不是触发 numbers 的 setValue,不会触发 Recompose,所以并不会触发订阅调用了 getValue 的地方更新

要解决上面的问题有三种解决方案。

List 状态更新解决方案:利用 Recompose 重组作用域

var flag by mutableStateOf(1)
setContent {
	Column {
		// flag 的状态更新了,触发 Recompose 更新 Text,也会更新 Column 内的其他 Composable,达到刷新的目的
		Text("$flag", Modifier.clickable { flag++ })
		Button(onClick = { numbers.add(numbers.last() + 1)}) {
			Text("加 1")
		}
		for (number in numbers) {
			Text("第 $number 块文字")
		}
	}
}

第一种解决方案是利用 Recompose 重组作用域,添加一个可观察的数据 flag,Recompose 时内部状态更新,Column 内的 Composable 也随之 Recompose。

如果是 Button 先点击几次列表添加了数据没更新,然后再点击 flag,此时会触发 Recompose,随之更新 Column 下的 Composable,之前点击添加到 numbers 的数据就会显示出来,所以这也证实了确实是因为没有 Recompose 才没有更新。

List 状态更新解决方案:替换数据源对象

var numbers by mutableStateOf(mutableListOf(1, 2, 3))
setContent {
    Column {
        Button(onClick = {
            // 或许你会觉得每次都创建对象会有性能问题,但其实性能影响并不大
            // 但是每次都这样写很麻烦
            numbers = numbers.toMutableList().apply {
                add(numbers.last() + 1)
            }
        }) {
            Text("加 1")
        }
        for (number in numbers) {
            Text("第 $number 块文字")
        }
    }
}

既然 by mutableStateOf 是通过 setValue 触发的更新,那 mutableListOf() 对象改变了那也就可以触发更新了。虽然不会影响多少性能,但每次这样写也太麻烦了。

List 状态更新解决方案:mutableStateListOf / mutableStateMapOf

// 注意这里是 =,不用 by
val numbers = mutableStateListOf(1, 2, 3)
setContent {
    Column {
        Button(onClick = {
            numbers.add(numbers.last() + 1)
        }) {
            Text("加 1")
        }
        for (number in numbers2) {
            Text("第 $number 块文字")
        }
    }
}

val map = mutableStateMapOf(1 to "One", 2 to "Two")
setContent {
    Column {
        Button(onClick = {
            map[3] = "Three"
        }) {
            Text("加 1")
        }
        for ((key, value) in map) {
			Text("$key 对应的是 $value")
		}
    }
}

Compose 提供的 mutableStateListOf(如果使用 Map 就是 mutableStateMapOf),mutableStateListOf 是内部的元素被观测,不是它本身被观测,所以 mutableStateListOf 内部数据更新了就会通知更新

Recompose 性能风险与智能优化、@Stable

Recompose 智能优化

var name by mutableStateOf("test1")
setContent {
	Column {
		Text(name, Modifier.clickable { name = "test2" })	
	}
}

上面的代码会在点击的时候更新 Text(),我们上面有讲过这是 Recompose 过程,Column 内的 lambda 就是 Recompose 的范围。但如果你看 Column 或其他 Composable 函数的源码会发现,它们都是 inline 内联函数,也就是在编译时 Column 内的 lambda 代码都会被抽离出来铺平,Recompose 影响的范围就不仅仅是 lambda 内。可以写个 demo 测试:

var name by mutableStateOf("test1")
setContent {
	println("Recompose 范围测试:1")
	Column {
		println("Recompose 范围测试:2")
		Text(name, Modifier.clickable { name = "test2" })	
	}
}

输出结果:

第一次执行:
Recompose 范围测试:1
Recompose 范围测试:2

点击后 Recompose 执行:
Recompose 范围测试:1
Recompose 范围测试:2

上面的代码会在点击 Text() 时都打印输出。

看到这或许你会有疑问:这样被动 Recompose 是否会有性能问题?

var name by mutableStateOf("test1")
setContent {
	println("Recompose 范围测试:1")
	Column {
		println("Recompose 范围测试:2")
		Heavy()
		Text(name, Modifier.clickable { name = "test2" })	
	}
}

@Composable
fun Heavy() {
    print("Recompose 范围测试:Heavy")
    Text("Heavy")
}

输出结果:
Recompose 范围测试:1
Recompose 范围测试:2
Recompose 范围测试:Heavy

点击后 Recompose 执行:
Recompose 范围测试:1
Recompose 范围测试:2

我们写了一个 Heavy(),当点击 Text() 时会触发 Recompose,其他的 Composable 和操作也会被动的 Recompose。但上面的案例运行后你会发现,Heavy() 内部的代码执行完一次后你再点击没有再执行了。

其实 Heavy() 已经被调用,但是不执行。原因是 Compose 在编译时会额外对函数添加一些处理,其中就有对被动 Recompose 的条件判断,执行到 Heavy() 时,编译器添加的条件判断到 Heavy() 的参数没有更新,那就直接跳过;否则有参数且参数改变了,就会重复执行.因为这里 Heavy() 是没有参数的,所以也就只会执行一次。

var name by mutableStateOf("test1")
setContent {
	println("Recompose 范围测试:1")
	Column {
		println("Recompose 范围测试:2")
		Heavy(name)
		Text(name, Modifier.clickable { name = "test2" })	
	}
}

@Composable
fun Heavy(text: String) {
    print("Recompose 范围测试:Heavy")
    Text("Heavy: $text")
}

输出结果:
Recompose 范围测试:1
Recompose 范围测试:2
Recompose 范围测试:Heavy

点击后 Recompose 执行:
Recompose 范围测试:1
Recompose 范围测试:2
Recompose 范围测试:Heavy

如果 Heavy() 添加一个参数,参数改变时被动 Recompose 时就会被重复执行(Heavy() 内部有用到这个参数才会重复执行,否则也是不会调用)。这是 Compose 的性能优化,避免了 Recompose 时的被动执行。

需要注意的是,这种性能优化是为了解决 Compose 自动更新所带来的更新范围过大超过需求的问题,跳过没必要的更新,相对于传统写法是不存在的,因为传统 UI 都是人工手动控制更新,这个性能优化并不是和传统 UI 能对比的优势

@Stable

上面节点讲到 Recompose 是做了性能优化会智能跳过参数没改动过的函数避免重复执行,如果传入的是一个对象类型是否也可以呢?

var user = User("vincent")
var name by mutableStateOf("test1")
setContent {
	println("Recompose 范围测试:1")
	Column {
		println("Recompose 范围测试:2")
		Heavy(user)
		Text(name, Modifier.clickable { name = "test2" })	
	}
}

@Composable
fun Heavy(user: User) {
    print("Recompose 范围测试:Heavy")
    Text("Hello ${user.name}")
}

data class User(val name: String)

输出结果:
Recompose 范围测试:1
Recompose 范围测试:2
Recompose 范围测试:Heavy

点击后 Recompose 执行:
Recompose 范围测试:1
Recompose 范围测试:2

上面的代码运行后同样的也是能做到智能跳过,验证了 Compose 的性能优化不仅是针对基本数据类型,也包含对象类型。但是还需要再进一步验证 Compose 是通过哪种方式判断对象是否改变的。在 kotlin 中 == 与 Java 的 equals() 相同,而 kotlin 中 === 与 Java 的 == 相同,主要就是验证用的哪一种。

首先验证 kotlin 的 == 是否是 Compose 的判断依据:

var user = User("vincent")
var name by mutableStateOf("test1")
setContent {
	println("Recompose 范围测试:1")
	Column {
		println("Recompose 范围测试:2")
		Heavy(user) // 点击时 user 不是同一个对象
		Text(name, Modifier.clickable { 
			name = "test2" 
			user = User("vincent") // 替换 user 为新对象,但字段数值没变
		})	
	}
}

@Composable
fun Heavy(user: User) {
    print("Recompose 范围测试:Heavy")
    Text("Hello ${user.name}")
}

data class User(val name: String)

输出结果:
Recompose 范围测试:1
Recompose 范围测试:2
Recompose 范围测试:Heavy

点击后 Recompose 执行:
Recompose 范围测试:1
Recompose 范围测试:2

上面的执行结果是 Heavy() 没有在 Recompose 二次执行,说明 Compose 是用的 kotlin 的 == 也就是 equals 这种结构性相等判断的

var user = User("vincent")
var name by mutableStateOf("test1")
setContent {
	println("Recompose 范围测试:1")
	Column {
		println("Recompose 范围测试:2")
		Heavy(user) // 点击时 user 不是同一个对象
		Text(name, Modifier.clickable { 
			name = "test2" 
			user = User("vincent") // 替换 user 为新对象,但字段数值没变
		})	
	}
}

@Composable
fun Heavy(user: User) {
    print("Recompose 范围测试:Heavy")
    Text("Hello ${user.name}")
}

data class User(var name: String) // 改成了 var

输出结果:
Recompose 范围测试:1
Recompose 范围测试:2
Recompose 范围测试:Heavy

点击后 Recompose 执行:
Recompose 范围测试:1
Recompose 范围测试:2
Recompose 范围测试:Heavy

在刚才的基础上把 User 对象的 name 属性由 val 改成 var,运行结果是又会二次执行了。这是因为 当你把 name 改成 var 时,Compose 就会认为 User 是一个不可靠的类,此时就会使用 kotlin 的 === 引用判断,User 不是同一个对象就会重复执行

如果将属性改成 var 在未来被修改了而没有执行,这将会导致 bug,所以 Compose 这么做是为了让程序达到足够的准确性,因为准确性比性能更重要

如果你 想让不可靠的类也可以让 Compose 认定为可靠继而跳过执行,可以通过添加 @Stable 注解让 Compose 用结构性相等 equals 判断就行了

var user = User("vincent")
var name by mutableStateOf("test1")
setContent {
	println("Recompose 范围测试:1")
	Column {
		println("Recompose 范围测试:2")
		Heavy(user)
		Text(name, Modifier.clickable { 
			name = "test2" 
		})	
	}
}

@Composable
fun Heavy(user: User) {
    print("Recompose 范围测试:Heavy")
    Text("Hello ${user.name}")
}

// 添加了注解,告知 Compose 用 equals 判断就行了
// 而且要把 data class 换成 class,为了保证 hashCode 和 equals 没有被改动
@Stable 
class User(var name: String) 

输出结果:
Recompose 范围测试:1
Recompose 范围测试:2
Recompose 范围测试:Heavy

点击后 Recompose 执行:
Recompose 范围测试:1
Recompose 范围测试:2

@Stable 的使用遵循三个原则:

  • 现在相等就永远相等(确保 equals 没有被重写,例如 data class 就会重写)

  • 当公开属性改变的时候,通知到用到这个属性的 Composition(公开属性即 Java 的 public,在 kotlin 属性默认都是公开的)

  • 公开属性需要全部是可靠属性(即类中的对象类型也要保证是可靠的,对象类型内的属性也是可靠的)

但在 Compose 实际上只会以第二条作为判断依据,不满足第二条就会认为是不可靠不稳定的会在 Recompose 重复执行。

var user = User("vincent")
var name by mutableStateOf("test1")
setContent {
	println("Recompose 范围测试:1")
	Column {
		println("Recompose 范围测试:2")
		Heavy(user)
		Text(name, Modifier.clickable { 
			name = "test2" 
		})	
	}
}

@Composable
fun Heavy(user: User) {
    print("Recompose 范围测试:Heavy")
    Text("Hello ${user.name}")
}

// 添加 @Stable 注解更多是用在有声明了 var 属性的接口上
// @Stable 
class User(name: String) {
	// 用代理这种方式注解也可以不用了,也能被 Compose 识别为可靠的类
	// 这种写法在 Compose 很常用,这样写也不用去写 @Stable 注解
	var name by mutableStateOf(name) 
}

class User(name: String, company: Company) {
	var name by mutableStateOf(name)
	var company by mutableStateOf(company) // 因为 Company 不可靠,这么写 Compose 也会认为 User 类不可靠
}

// 这个类是不可靠的
class Company(var address: String)

小结

  • 当类中属性都是 val 声明时,Compose 会认为这是一个可靠的类,此时 Recompose 是通过 equals() 判断是否是同一个对象,如果是同一个对象就不会重复执行

  • 当类中属性存在 var 声明时,Compose 会认为这是一个不可靠的类,此时 Recompose 是通过 == 引用判断,Compose 基本都会重新执行一遍,这是为了确保准确性

  • 当类中属性存在 var 声明但希望 Compose 识别是可以可靠的类,可以对类添加 @Stable 注解,但类不能是 data class,要保留原始的 hashCode 和 equals,由程序员保证可靠性让 Recompose 跳过不重复执行;或者将 var 属性通过 mutableStateOf 代理也能让 Compose 识别为可靠的类

derivedStateOf() 和 remember 的区别

关于 derivedStateOf() 官方的解释是:

convert one or multiple state objects into another state
将一个或多个状态转成另一个状态

看到这个说明可能你会有点懵,再结合一下官方的代码理解下:

// highPriorityKeywords 这里虽然是 List 类型,但其实它是有可能是状态对象
// 也就是传参时它可能是 mutableStateListOf(mutableStateListOf 是 List 的子类)
@Composable
fun TodoList(highPriorityKeywords: List<String> = listof("Review", "Unblock", "Compose")) {
	val todoTasks = remember { mutableStateListOf<String>() }

	// todoTasks 和 highPriorityKeywords 是 Compose 的状态对象
	// 简单理解它们是 mutableStateOf/mutableStateListOf/mutableStateMapOf 对象
	
	// highPriorityTasks 的结果会因为 highPriorityKeywords 的改变而重新获取
	// 也就是 derivedStateOf 所依赖的 todoTasks 和 highPriorityKeywords 发生改变时
	// derivedStateOf {} 的 lambda 会重新执行一次
	// derivedSateOf 依赖的状态对象在它的 lambda 内是 todoTasks 和 highPriorityKeywords
	// 如果有多个依赖对象,其中一个有改变就会将 lambda 重新执行一次
	val highPriorityTasks by remember(highPriorityKeywords) {
		derivedStateOf { todoTasks.filter { it.containsWord(highPriorityKeywords) } }
	}
	
	Box(Modifier.fillMaxSize()) {
		LazyColumn {
			items(highPriorityTasks) { /* ... */ }
			items(todoTasks) { /* ... */ }
		}
	}
}

上面的例子 derivedStateOf 会因为依赖的对象 todoTasks 和 highPriorityKeywords 两个状态对象发生改变而重新执行 lambda 内的代码,同样的为了 Recompose 时不会重复执行初始化,derivedStateOf 也需要用 remember 包裹住。

那 derivedStateOf 到底怎么做到的?

接下来写个简单的例子验证 derivedStateOf 的使用:

setContent {
	// 验证 derivedStateOf(),name 通过点击更新时同时会触发 processName 也跟着更新
	var name by remember { mutableStateOf("name") }
	val processedName by remember { derivedStateOf { name.uppercase() } }
	Text(processedName, Modifier.clickable { name = "vincent" }
}

上面的例子当我们点击 Text() 时修改了 name,就会触发 Recompose,继而 derivedStateOf 因为依赖 name 这个状态对象,所以也会重新执行,最终发生更改。

上面的场景其实用 remember 也可以实现:

setContent {
	var name by remember { mutableStateOf("name") }
	val processedName = remember(name) { name.uppercase() }
	Text(processedName, Modifier.clickable { name = "vincent" }
}

那么问题来了:都能实现同样的功能,那什么时候我该用 remember,什么时候用 derivedStateOf?

我们对比下两种写法的区别:

				  1		2			3
val processedName by remember { derivedStateOf { name.uppercase() } }
val processedName = remember(name) { name.uppercase() }
  • 第一个区别是一个用了 by 另一个是 =,by 在这里是委托 getter 和 setter,和 = 的区别不大可以忽略

  • 第二个区别是 remember 没有带参数,remember 内部要重新执行就需要提供给 Compose 可以判断的依据,remember(name) 带了参数所以 name 变更时也就能让 Compose 识别到需要重新执行

  • 那么第一种没有参数又怎么能让 Compose 识别需要重新执行呢?这就是第三个区别 derivedStateOf 的作用了,它监听着 lambda 内的状态对象变更,有更新了就会触发 Compose 重新执行,所以 remember 没有参数也是可以的

说明二者的区别,或许你还是没明白哪种场景该使用 derivedStateOf 还是 remember,我们将上面的例子修改下:

setContent {
	val names = remember { mutableStateListOf("name1", "name2") }
	// Recompose 时 names 返回的还是相同的对象(names 是由 remember 返回的)
	// 因为 names 没有改变所以 remember(names) 不会重新执行里面的代码
	val processedNames = remember(names) {
		names.map {
			it.uppercase()
		}
	}
	
	Column {
		for (processedName in processedNames) {
			Text(processedName, Modifier.clickable {
				names.add("Android")
			})
		}
	}
}

上面的例子将状态对象修改为 mutableStateListOf,remember(names) 传了参数,Column 显示多个文本,每个文本都加上点击更新 names。代码运行尝试点击你会发现已经 Recompose 但 Column 没有更新。

在上面的章节有提到,remember 带参数时 Compose 的判断依据是结构性相等,也就是 Java 的 equals() / kotlin 的 ==,names 在 Recompose 时从 remember 拿到的还是那个对象,并没有发生变更,所以 Column 没有更新。

知道问题原因,你或许会想到解决方案,那就把 remember(names) 去掉:

setContent {
	val names = remember { mutableStateListOf("name1", "name2") }
	// val processedNames = remember(names) {
	//	names.map {
	//		it.uppercase()
	//	}
	// }
	// remember(names) 去掉了,Recompose 刷新了但存在性能问题
	val processedNames = 
		names.map {
			it.uppercase()
		}
	
	Column {
		for (processedName in processedNames) {
			Text(processedName, Modifier.clickable {
				names.add("Android")
			})
		}
	}
}

虽然运行正常了,但是 remember 所带来的性能也都去掉了,我们加 remember 就是为了提高性能在 Recompose 时如果 names 没有发生变更,processedNames 就不重复执行。

当然你还能想到另一种解决方案,把 names 替换了:

setContent {
	val names by remember { mutableStateOf(listOf("name", "name2"))) }
	val processedNames = remember(names) {
		names.map {
			it.uppercase()
		}
	}
	
	Column {
		for (processedName in processedNames) {
			Text(processedName, Modifier.clickable {
				// 替换 names 很麻烦
				names = names.toMutableList().apply { 
					add("Android")
				}
			})
		}
	}
}

但是这种写法太麻烦。

这时候就轮到 derivedStateOf 的使用场景:

setContent {
	val names = remember { mutableStateListOf("name1", "name2") }
	val processedNames = remember {
		derivedStateOf {
			names.map {
				it.uppercase()
			}
		}
	}
	
	Column {
		for (processedName in processedNames) {
			Text(processedName, Modifier.clickable {
				names.add("Android")
			})
		}
	}
}

通过 derivedStateOf 就能够监听到 names 内部元素更新时也能刷新,而且和 remember 各司其职,derivedStateOf 负责监听改变重新执行,remember 负责 Recompose 时不会重复执行,这类似于 remember 和 mutableStateOf/mutableStateListOf/mutableStateMapOf 的组合使用方式。

所以 derivedStateOf 的使用场景就是这种列表状态对象内部元素变更的监听。是这样理解吗?并不是的,这还不是 derivedStateOf 和 remember 的本质区别

为了更好的理解它们的本质区别,我们看下面的例子:

setContent {
	var name by remember { mutableStateOf("name1") }
	ProcessedName(name) { name = "name2" }
}

// name 虽然是 mutableStateOf 状态对象
// 但在 ProcessedName() 传参进来的时候,name 的代理就被拆除了,变成单纯的 name 文本类型

// 因为 name 已经变成了文本类型,所以 derivedStateOf 所监听的 name 已经不是状态对象了
// 而且还是一个无参数的 remember,所以就不会发生更新
@Composable
private fun ProcessedName(name: String, onClick: () -> Unit) {
	val processedName by remember { derivedStateOf { name.uppercase() } }
	Text(processedName, Modifier.clickable(onClick = onClick))
}

// 上面的写法等价于:
setContent {
	var name = remember { mutableStateOf("name1") }
	ProcessedName(name.value) { name.value = "name2" }
}

上面的执行结果是,使用 derivedStateOf 点击没有更新,remember 带参数的更新了。这又是什么原因了,怎么又不行了?

实际上是因为 name 状态对象在作为 ProcessName() 的参数传入时,name 的代理就已经被拆除变成了一个纯文本的类型,所以 derivedStateOf 所监听的 name 已经不是状态对象,所以失效。

我们再改为带参数的 remember,又正常生效了:

setContent {
	var name by remember { mutableStateOf("name1") }
	ProcessedName(name) { name = "name2" }
}

@Composable
private fun ProcessedName(name: String, onClick: () -> Unit) {
	// remember(name) 能更新仅仅是因为 name 在点击时它的值被修改了
	val processedName = remember(name) { name.uppercase() }
	Text(processedName, Modifier.clickable(onClick = onClick))
}

那 derivedStateOf 在这种状态对象作为参数传递,状态对象代理失效的场景该怎么处理?我们继续分析:

setContent {
	// var name by remember { mutableStateOf("name1") }
	// 改为 =
	var name = remember { mutableStateOf("name1") }
	ProcessedName(name) { name.value = "name2" }
}

// name 参数变成 State
@Composable
private fun ProcessedName(name: State<String>, onClick: () -> Unit) {
	val processedName by remember { derivedStateOf { name.value.uppercase() } }
	Text(processedName, Modifier.clickable(onClick = onClick))
}

上面有两处修改点,主要看 ProcessName() 将传参的类型变成了 State,指明了传入的参数是一个状态对象。

虽然这种方式可以解决,但一般情况不会用上面的处理方案,因为将 name 参数改为 State 实际上会缩减 Composable 的通用型也不实用

其实在这种函数传参基本类型的状态对象的场景,只能使用带参数的 remember。

接下来我们小结下 remember 和 derivedStateOf:

  • 监听状态变化从而自动刷新有两种写法:带参数的 remember、不带参数的 remember + derivedStateOf

  • 但第一点不全对,对于状态对象来说(mutableStateListOf/mutableStateOf),带参数的 remember 不能监听到,只能用 derivedStateOf

  • 对于函数传参如果是基本类型的状态对象(String、Int 等)会存在监听链条被掐断,所以不能用 derivedStateOf,只能用带参数的 remember

  • 对于函数传参如果是列表类型的状态对象(List、Map 等),带参数的 remember 不能监听到,还是要使用不带参数的 remember + derivedStateOf;但还有一种特殊情况,函数传参的列表类型的状态对象也变更成新对象了,这时候要使用带参数的 remember + derivedStateOf

所以 remember 和 derivedStateOf 的使用场景如下:

  • 带参数的 remember:可以判断对象的重新赋值,而 derivedStateOf 不能完美做到,所以带参数的 remember 的使用场景是函数参数

  • derivedStateOf:适用于监听状态对象

  • 函数参数即是状态对象又是内部元素会变更的(List、Map):两个都用,带参数的 remember + derivedStateOf

第三点也是官方 demo 的处理场景,我们回到一开始的官方 demo 代码,根据上面对 derivedStateOf 和 remember 的分析,现在再看这份代码应该就很清楚了:

// 函数参数是 List 内部元素可能会变更
@Composable
fun TodoList(highPriorityKeywords: List<String> = listof("Review", "Unblock", "Compose")) {
	val todoTasks = remember { mutableStateListOf<String>() }

	// 使用了带参数的 remember + derivedStateOf
	val highPriorityTasks by remember(highPriorityKeywords) {
		derivedStateOf { todoTasks.filter { it.containsWord(highPriorityKeywords) } }
	}
	
	Box(Modifier.fillMaxSize()) {
		LazyColumn {
			items(highPriorityTasks) { /* ... */ }
			items(todoTasks) { /* ... */ }
		}
	}
}

CompositionLocal

setContent {
	val name = "name"
	User(name)
}

@Composable
fun User(name: String) {
	Text(name)
}

上面的例子是一个简单的文本显示,name 传参到 User(),这时候 User() 就能使用 name 参数显示文本。但如果需要做到 User() 即使没有传参 name,User() 内部也不影响对 name 的使用应该怎么处理呢?

// lambda 提供默认值,这里设置没有提供默认值就抛异常
val LocalName = compositionLocalOf<String> { error("name no value") }

class MainActivity : ComponentActivity() {
	override fun onCreate(savedInstanceState: Bundle?) {
		super.onCreate(savedInstanceState)
		setContent {
			// 要用 CompositionLocalProvider 把 CompositionLocal 对象传进去
			// provides 是一个中缀函数,等价于 LocalName.provides("name")
			CompositionLocalProvider(LocalName provides "name") {
				User()
				
				// 这样也是可以穿透 User() 使用 LocalName
				// Button(onclick = { }) {
				 	// User()
				// }
			}
		}
	}
}

@Composable
fun User() {
	// 获取 CompositionLocal 的值要 .current
	// current 只能在 Composable 读取
	Text(LocalName.current) 
}

CompositionLocal 就是 Composition 的局部变量,一般我们讲到局部变量就是指的函数内的局部变量,它只能在这个函数内使用,函数外就不能访问了,除非使用函数参数传参。而 CompositionLocal 能够穿透由 CompositionLocalProvider 包裹的 Composable(这里的穿透指的不仅是一层,而是被包裹住的所有的 Composable 都能访问到这个 CompositionLocal),让这些 Composable 的内部能使用。

CompositionLocal 是具有穿透函数功能的局部变量,也可以更简单的理解为它是不需要显式传递的函数参数。

在 Compose 中对 CompositionLocal 变量的命名有规范,要以 Local 作为前缀方便看代码也能在 IDE 有提示,例如全局变量就是 LocalName,只是函数内使用的就是 localName。

那什么场景才使用 CompositionLocal?

val LocalName = compositionLocalOf<String> { error("name no value") }

class MainActivity : ComponentActivity() {
	override fun onCreate(savedInstanceState: Bundle?) {
		super.onCreate(savedInstanceState)
		setContent {
			Button(onClick = { }) {
				// 为了不影响范围 CompositionLocal 只包裹 User()
				// 但这样又显得很多余,我传参给 User() 不更简单?
				CompositionLocalProvider(LocalName provides "name") {
					User()
				}
				Details()
			}

			// 而这样又觉得会影响我不想穿透的范围
			CompositionLocalProvider(LocalName provides "name") {
				Button(onClick = { }) {
					User()
					Details()
				}
			}
		}
	}
}

@Composable
fun User() {
	Text(LocalName.current) 
}

@Composable
fun CapitalizedUser() {
	Text(LocalName.current) 
}

@Composable
fun Details() {
	CapitalizedUser()
}

在创建 CompositionLocal 时就把它创建为不怕更大范围影响的对象。

// 定义 LocalActivity 就是当前的 Activity
val LocalActivity = compositionLocalOf<Activity> { error("LocalActivity no value") }

class MainActivity : ComponentActivity() {
	override fun onCreate(savedInstanceState: Bundle?) {
		super.onCreate(savedInstanceState)
		setContent {
			// 根据 LocalActivity 的定义提供对象
			CompositionLocalProvider(LocalActivity provides this) {
				// 根据 LocalActivity 的定义来使用 LocalActivity.current
			}
		}
	}
}

CompositionLocal 的作用是由自身来定义的,比如 LocalActivity 是当前的 Activity,定义完之后提供它的地方需要按照定义提供这个 Activity,里面使用它的也要规规矩矩的把它当成当前 Activity 来使用。

简单说就是先定义好了 CompositionLocal 要怎么用,后续 CompositionLocalProvider 提供定义的对象和使用 CompositionLocal 就和定义时共识好的来就没问题了

所以 CompositionLocal 的使用场景适用于提供上下文环境、打造主题这类全局的场景。我不知道内部会不会用到它,我只保证我所提供的数据是正确的。比如 Compose 已经提供了上下文对象 LocalContext。

val LocalBackground = compositionLocalOf<Color> { error("LocalBackground no value") }

setContent {
	// MeterialTheme 内部就是提供了 Meterial Design 的主题样式
	// 里面的 CompositionLocal 要怎么用呢?实际上就是内部都使用 meterial 包下的 Composable
	// meterial 包的 Composable 内部都用了 MeterialTheme 定义的 CompositionLocal
	// 例如 Button
	MeterialTheme {
		Button(onClick = {}) {
		}
	}

	CompositionLocalProvider(LocalBackground provides Color.Green) {
		TextWithBackground()
	}
}

@Composable
fun TextWithBackground() {
	Surface(color = LocalBackground.current) {
		Text("text with background")
	}
}

还有一种情况是,既可以是函数参数又可以是 CompositionLocal,这时候就要做好处理的准备,例如文字的颜色:

// Text() 源码
Text(...) {
	// color 如果有传函数参数就优先使用,否则就是 style.color 或 CompositionLocal
	val textColor = color.takeOrElse {
		style.color.takeOrElse {
			LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
		}
	}
	...
}

CompositionLocal 的使用范围只会对所在 lambda 有效:

val LocalBackground = compositionLocalOf<Color> { error("LocalBackground no value") }

setContent {
	TextWithBackground() // 取 CompositionLocal 默认值,这里是抛出异常
	CompositionLocalProvider(LocalBackground provides Color.Yellow) {
		TextWithBackground() // 黄色背景
		CompositinLocalProvider(LocalBackground provides Color.Blue) {
			TextWithBackground() // 蓝色背景
		}
		TextWithBackground() // 黄色背景
	}
}

@Composable
fun TextWithBackground() {
	Surface(color = LocalBackground.current) {
		Text("text with background")
	}
}

CompositionLocal 除了 compositionLocalOf 还有另外一个创建方式:staticCompositionLocalOf,它和 compositionLocalOf 的区别是,compositionLocalOf 会对使用的地方做标记,后续 CompositionLocal 变更的时候会让使用的地方都失效进行 Recompose;而 staticCompositionLocalOf 不会做标记,而是全量更新。

val LocalBackground = compositionLocalOf<Color> { error("LocalBackground no value") }

class MainActivity : ComponentActivity() {
	var themeBackground by mutableStateOf(Color.Blue)
	
	override fun onCreate(savedInstanceState: Bundle?) {
		setContent {
			CompositionLocalProvider(LocalBackground provides Color.Yellow) {
				TextWithBackground() // 黄色背景
				// 在这个 CompositionLocal 范围 Recompose
				// 如果 LocalBackground 是 staticCompositionLocalOf 创建
				// 会让所在 lambda 全量 Recompose
				CompositionLocalProvider(LocalBackground provides themeBackground) {
					// LocalBackground 是 compositionLocalOf 创建时
					// themeBackground 变更会触发 Recompose
					// 只对这个范围的 Composable 进行 Recompose
					TextWithBackground() 
					CompositionLocalProvider(LocalBackground provides Color.Green) {
						TextWithBackground()
					}
				}
				TextWithBackground() // 黄色背景
			}
		}		
	}
}

staticCompositionLocalOf 虽然没有了记录的性能消耗,但是提高了刷新的性能消耗。

那该怎么选择 compositionLocalOf 还是 staticCompositionLocalOf?

  • 如果提供 CompositionLocal 的值会经常改变,那应该使用 compositionLocalOf 来提高刷新时的性能减少 Recompose 范围

  • 如果提供的 CompositionLocal 的值不经常改变甚至不改变,那应该使用 staticCompositionLocalOf 减少记录的性能消耗

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 这个错误通常是因为在使用docker-compose命令之前,该文件已经被其他进程锁定了。您可以尝试重新启动您的机器,或者使用以下命令来解决该问题: 1. 查找当前正在使用该文件的进程: ``` sudo lsof /usr/local/bin/docker-compose ``` 2. 杀死正在使用该文件的进程: ``` sudo kill -9 <PID> ``` 其中 `<PID>` 是上一步骤中找到的进程号。 3. 再次尝试运行docker-compose命令。 ### 回答2: 出现"-bash: /usr/local/bin/docker-compose: Text file busy"这个错误是因为在执行该命令时,该文件正在被其他进程占用,无法被读取或修改。 要解决这个问题,可以尝试以下几种方法: 1. 重新启动电脑:有时候,文件被某个进程占用,重新启动计算机可以释放该文件的锁定状态,使其变得可用。 2. 结束占用进程:使用lsof命令找到正在使用该文件的进程,并使用kill命令结束掉这些进程。具体操作步骤如下: a. 打开终端,输入命令:lsof /usr/local/bin/docker-compose b. 针对显示的进程列表,找到对应的PID(进程ID) c. 输入命令:kill <PID>,其中<PID>为进程ID 3. 修改文件访问权限:有时候,文件的权限设置不正确,导致无法读取或修改。可以使用chmod命令修改文件的权限,使其变得可用。具体操作步骤如下: a. 打开终端,输入命令:sudo chmod +x /usr/local/bin/docker-compose b. 输入管理员密码,确认权限修改 4. 确认文件所在目录是否挂载为只读模式:如果文件所在目录是在只读模式下挂载的,则无法修改该文件。可以使用mount命令查看目录的挂载信息,并确保其可写。 如果以上方法都无法解决问题,可能是其他原因导致该文件不可用,建议参考官方文档或寻求相关技术支持。 ### 回答3: "-bash: /usr/local/bin/docker-compose: Text file busy" 是一个错误提示,它意味着无法执行 `/usr/local/bin/docker-compose` 文件,因为该文件正在被占用或处于忙碌状态。 通常情况下,该错误出现的原因可能有以下几种: 1. 该文件正在被其他程序或进程使用,导致无法执行。你可以通过查看当前运行的进程来确定是否有其他程序正在使用该文件。 2. 当前用户权限不足以执行该文件。你可以以管理员身份运行执行文件的命令,或者确保当前用户具备适当的权限。 3. 文件系统可能出现问题,导致文件处于忙碌状态。此时,建议重新启动系统并再次尝试执行文件。 如果你确定没有其他应用程序正在使用该文件,并且你具备足够的权限,你可以尝试以下解决方法: 1. 首先,尝试使用 `lsof` 命令来查看当前正在使用该文件的进程,并尝试终止这些进程。 ``` lsof /usr/local/bin/docker-compose ``` 2. 如果上述方法无效,你可以尝试删除该文件并重新安装 Docker Compose。请先备份文件,然后使用以下命令删除该文件: ``` sudo rm /usr/local/bin/docker-compose ``` 然后,重新安装 Docker Compose。 无论是以上哪种情况,建议在执行任何操作之前,先备份重要文件,以免造成数据丢失或其他不可逆的问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值