Jetpack Compose State:修改类属性

Posted

技术标签:

【中文标题】Jetpack Compose State:修改类属性【英文标题】:Jetpack Compose State: Modify class property 【发布时间】:2021-01-05 09:51:44 【问题描述】:

以下两个示例只是在给定的默认值上添加一个“a”。使用的compose_version1.0.0-alpha03,这是迄今为止最新的(据我所知)。

这个例子与我在研究中发现的大多数例子最相似。

示例 1

@Composable
fun MyScreen() 
    val (name, setName) = remember  mutableStateOf("Ma") 

    Column 
        Text(text = name) // 'Ma'
        Button(onClick = 
                setName(name + "a") // change it to 'Maa'
        ) 
            Text(text = "Add an 'a'")
        
    

但是,这并不总是可行的。例如,数据比单个字段更复杂。例如一个类,甚至是一个Roomdata class

示例 2

// the class to be modified
class MyThing(var name: String = "Ma");


@Composable
fun MyScreen() 
    val (myThing, setMyThing) = remember  mutableStateOf(MyThing()) 

    Column 
        Text(text = myThing.name) // 'Ma'
        Button(onClick = 
                var nextMyThing = myThing
                nextMyThing.name += "a" // change it to 'Maa'
                setMyThing(nextMyThing)
        ) 
            Text(text = "Add an 'a'")
        
    

当然,示例 1 有效,但 示例 2 无效。这是我的一个简单错误,还是我错过了关于如何修改这个类实例的大图?

编辑:

在某种程度上找到了一种方法来完成这项工作,但它似乎效率低下。然而,它确实符合 React 管理状态的方式,所以也许这是正确的做法。

示例 2 中的问题很明显是 myNextThing 不是原始 myThing 的副本,而是对其的引用。就像 React 一样,Jetpack Compose 在修改状态时似乎想要一个全新的对象。这可以通过以下两种方式之一完成:

    创建MyThing 类的新实例,更改需要更改的内容,然后使用新类实例调用setMyThing()class MyThing 更改为data class MyThing 并使用copy() 函数创建具有相同属性的新实例。然后,更改所需的属性并调用setMyThing()。鉴于我明确表示我想使用它来修改 android Room 使用的给定 data class 上的数据,因此这是我的问题的最佳方式。

示例 3 (函数式)

// the class to be modified
data class MyThing(var name: String = "Ma");


@Composable
fun MyScreen() 
    val (myThing, setMyThing) = remember  mutableStateOf(MyThing()) 

    Column 
        Text(text = myThing.name) // 'Ma'
        Button(onClick = 
                var nextMyThing = myThing.copy() // make a copy instead of a reference
                nextMyThing.name += "a" // change it to 'Maa'
                setMyThing(nextMyThing)
        ) 
            Text(text = "Add an 'a'")
        
    

【问题讨论】:

【参考方案1】:

确实,在我看来,最好的方法是复制()一个数据类。

在使用自定义data classremember() 的特定情况下,这可能确实是最好的选择,尽管可以通过在copy() 函数上使用命名参数来更简洁地完成:

// the class to be modified
data class MyThing(var name: String = "Ma", var age: Int = 0)

@Composable
fun MyScreen() 
  val (myThing, myThingSetter) = remember  mutableStateOf(MyThing()) 

  Column 
    Text(text = myThing.name)
    // button to add "a" to the end of the name
    Button(onClick =  myThingSetter(myThing.copy(name = myThing.name + "a")) ) 
      Text(text = "Add an 'a'")
    
    // button to increment the new "age" field by 1
    Button(onClick =  myThingSetter(myThing.copy(age = myThing.age + 1)) ) 
      Text(text = "Increment age")
    
  

但是,我们仍将更新视图模型并观察它们的结果(LiveDataStateFlow、RxJava Observable 等)。我希望remember mutableStateOf() 将在本地用于尚未准备好提交到视图模型但需要多位用户输入的数据,因此需要表示为状态。您是否觉得需要 data class 取决于您自己。

这是我的一个简单错误,还是我错过了关于如何修改这个类实例的大局?

Compose 无法知道对象已更改,因此它不知道需要重新组合。

总的来说,Compose 是围绕对不可变数据流做出反应而设计的。 remember mutableStateOf() 创建一个本地流。

然而,另一种方法是最受欢迎的。

你不限于单个remember

@Composable
fun MyScreen() 
  val name = remember  mutableStateOf("Ma") 
  val age = remember  mutableStateOf(0) 

  Column 
    Text(text = name.value)
    // button to add "a" to the end of the name
    Button(onClick =  name.value = name.value + "a") 
      Text(text = "Add an 'a'")
    
    // button to increment the new "age" field by 1
    Button(onClick =  age.value = age.value + 1 ) 
      Text(text = "Increment age")
    
  

【讨论】:

我目前正在为您回答的最后一部分而苦苦挣扎。我猜您建议使用多个状态对象来反映每个数据类的属性,以避免在只有一个属性更改时重新组合整个可组合树。但是对于更大的数据类,这确实使我的代码膨胀,有几行专门用于使数据类的每个属性都可观察。我想知道是否可以使用反射创建数据类的转换,以便其所有属性都暴露为状态。有点把它变成自己的状态持有者版本【参考方案2】:

好的,对于任何对此感到疑惑的人,有一种更简单的方法可以解决此问题。当您像这样定义可变状态属性时:

//There is a second paremeter wich defines the policy of the changes on de state if you
//set this value to neverEqualPolicy() you can make changes and then just set the value
class Vm : ViewModel() 
 val dummy = mutableStateOf(value = Dummy(), policy= neverEqualPolicy())

 //Update the value like this 
 fun update()
 dummy.value.property = "New value"
//Here is the key since it has the never equal policy it will treat them as different no matter the changes
 dummy.value = dummy.value
 

有关可用政策的更多信息: https://developer.android.com/reference/kotlin/androidx/compose/runtime/SnapshotMutationPolicy

【讨论】:

这确实是一个不错的方法。我只是不必在最后将值设置为自身 arghhh .. 一段时间后,我更喜欢在模型中使用属性作为可变状态,这样更易​​于管理。 我希望我能做到,但我的对象是嵌套的混乱【参考方案3】:

确实,在我看来,解决此问题的最佳方式是copy()data class

使用反射的完整且有用的示例(允许修改我的不同类型的属性可能如下所示:

// the class to be modified
data class MyThing(var name: String = "Ma", var age: Int = 0);


@Composable
fun MyScreen() 
    val (myThing, setMyThing) = remember  mutableStateOf(MyThing()) 

    // allow the `onChange()` to handle any property of the class
    fun <T> onChange(field: KMutableProperty1<MyThing, T>, value: T) 
        // copy the class instance
        val next = myThing.copy()
        // modify the specified class property on the copy
        field.set(next, value)
        // update the state with the new instance of the class
        setMyThing(next)
    

    Column 
        Text(text = myThing.name)
        // button to add "a" to the end of the name
        Button(onClick =  onChange(MyThing::name, myThing.name + "a") ) 
            Text(text = "Add an 'a'")
        
        // button to increment the new "age" field by 1
        Button(onClick =  onChange(MyThing::age, myThing.age + 1) ) 
            Text(text = "Increment age")
        
    


虽然每次单击按钮(或者在实际用例中使用TextField 而不是按钮按下键盘)时都在状态中实例化类的副本可能有点浪费对于较大的类,通常 Compose 框架似乎更喜欢这种方法。如前所述,这符合 React 做事的方式:状态永远不会被修改或附加,它总是被完全替换。

然而,另一种方法是最受欢迎的。

【讨论】:

但假设您有一个大数据类(如用户配置文件类)并且您正在屏幕上显示用户配置文件。这种方法将触发整个可组合树的重组,而实际上可能只有一个文本字段发生了变化。这应该对性能产生影响吧?【参考方案4】:

Annotation 存储数据类@AsState

好吧,我仍然不确定是否可以简单地 .copy(changedValue = "...") 一个大型数据类,或者这是否因为它触发了不必要的重组而效率低下。我从经验中知道,在处理更改数据类中的哈希图和列表时,它可能会导致一些繁琐的代码。一方面,@CommonsWare 提到的替代方法听起来确实是正确的方法:即跟踪可以独立更改为State 的数据类的每个属性。然而这让我的代码和 ViewModel 变得非常冗长。想象一下向数据类添加一个新属性;然后你需要为这个属性创建一个可变和不可变的状态持有者,而且它只是非常乏味。

我的解决方案: 我朝着与@foxtrotuniform6969 试图做的类似的方向前进。我写了一个AnnotationProcessor,它接受了我的data classes,并创建了一个可变和不可变版本的类,将所有属性都保存为状态。它支持列表和映射,但很浅(意味着它不会对嵌套类重复相同的过程)。这里是 Test.class 的示例,带有注释和生成的类。如您所见,您可以使用原始数据类轻松实例化状态持有者类,并反过来从状态持有者类中获取修改后的数据类。

如果您认为这有助于在可组合中显示/编辑数据类时更清晰地跟踪状态(如果您不这样做),请告诉我

原来的类

@AsState
data class Test(val name:String, val age:Int, val map:HashMap<String,Int>, val list:ArrayList<String>)

具有自定义构造函数和 rootClass getter 的类的可变版本

public class TestMutableState 
  public val name: MutableState<String>

  public val age: MutableState<Int>

  public val map: SnapshotStateMap<String, Int>

  public val list: SnapshotStateList<String>

  public constructor(rootObject: Test) 
    this.name=mutableStateOf(rootObject.name) 
    this.age=mutableStateOf(rootObject.age) 
    this.map=rootObject.map.mapPair(it.key,it.value).toMutableStateMap()
    this.list=rootObject.list.toMutableStateList()
  

  public fun getTest(): Test = Test(name = this.name.value,
  age = this.age.value,
  map = HashMap(this.map),
  list = ArrayList(this.list),
  )

可以在 ViewModel 中公开的不可变版本

public class TestState 
  public val name: State<String>

  public val age: State<Int>

  public val map: SnapshotStateMap<String, Int>

  public val list: SnapshotStateList<String>

  public constructor(mutableObject: TestMutableState) 
    this.name=mutableObject.name
    this.age=mutableObject.age
    this.map=mutableObject.map
    this.list=mutableObject.list
  

TL;DR

接下来,我将粘贴注释处理器的源代码,以便您实现它。我基本上遵循this 文章并基于艰苦的谷歌搜索实现了我自己的一些更改。我可能会在未来将此作为一个模块,以便其他人可以更轻松地在他们的项目中实现它,我有任何兴趣:

注解类

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
public annotation class AsState

注解处理器

@AutoService(Processor::class)
class AnnotationProcessor : AbstractProcessor() 
    companion object 
        const val KAPT_KOTLIN_GENERATED_OPTION_NAME = "kapt.kotlin.generated"
    

    override fun getSupportedAnnotationTypes(): MutableSet<String> 
        return mutableSetOf(AsState::class.java.name)
    

    override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latest()

    override fun process(annotations: MutableSet<out TypeElement>?, roundEnv: RoundEnvironment): Boolean 
        roundEnv.getElementsAnnotatedWith(AsState::class.java)
            .forEach 
                if (it.kind != ElementKind.CLASS) 
                    processingEnv.messager.printMessage(Diagnostic.Kind.ERROR, "Only classes can be annotated")
                    return true
                
                processAnnotation(it)
            
        return false
    

    @OptIn(KotlinPoetMetadataPreview::class, com.squareup.kotlinpoet.DelicateKotlinPoetApi::class)
    private fun processAnnotation(element: Element) 
        val className = element.simpleName.toString()
        val pack = processingEnv.elementUtils.getPackageOf(element).toString()
        val kmClass = (element as TypeElement).toImmutableKmClass()

        //create vessel for mutable state class
        val mutableFileName = "$classNameMutableState"
        val mutableFileBuilder= FileSpec.builder(pack, mutableFileName)
        val mutableClassBuilder = TypeSpec.classBuilder(mutableFileName)
        val mutableConstructorBuilder= FunSpec.constructorBuilder()
            .addParameter("rootObject",element.asType().asTypeName())
        var helper="return $element.simpleName("

        //create vessel for immutable state class
        val stateFileName = "$classNameState"
        val stateFileBuilder= FileSpec.builder(pack, stateFileName)
        val stateClassBuilder = TypeSpec.classBuilder(stateFileName)
        val stateConstructorBuilder= FunSpec.constructorBuilder()
            .addParameter("mutableObject",ClassName(pack,mutableFileName))

        //import state related libraries
        val mutableStateClass= ClassName("androidx.compose.runtime","MutableState")
        val stateClass=ClassName("androidx.compose.runtime","State")
        val snapshotStateMap= ClassName("androidx.compose.runtime.snapshots","SnapshotStateMap")
        val snapshotStateList=ClassName("androidx.compose.runtime.snapshots","SnapshotStateList")


        fun processMapParameter(property: ImmutableKmValueParameter) 
            val clName =
                ((property.type?.abbreviatedType?.classifier) as KmClassifier.TypeAlias).name
            val arguments = property.type?.abbreviatedType?.arguments?.map 
                ClassInspectorUtil.createClassName(
                    ((it.type?.classifier) as KmClassifier.Class).name
                )
            
            val paramClass = ClassInspectorUtil.createClassName(clName)
            val elementPackage = clName.replace("/", ".")
            val paramName = property.name

            arguments?.let 
                mutableClassBuilder.addProperty(
                    PropertySpec.builder(
                        paramName,
                        snapshotStateMap.parameterizedBy(it), KModifier.PUBLIC
                    )
                        .build()
                )
            
            arguments?.let 
                stateClassBuilder.addProperty(
                    PropertySpec.builder(
                        paramName,
                        snapshotStateMap.parameterizedBy(it), KModifier.PUBLIC
                    )
                        .build()
                )
            

            helper = helper.plus("$paramName = $paramClass.simpleName(this.$paramName),\n")

            mutableConstructorBuilder
                .addStatement("this.$paramName=rootObject.$paramName.mapPair(it.key,it.value).toMutableStateMap()")

            stateConstructorBuilder
                .addStatement("this.$paramName=mutableObject.$paramName")
        

        fun processListParameter(property: ImmutableKmValueParameter) 
            val clName =
                ((property.type?.abbreviatedType?.classifier) as KmClassifier.TypeAlias).name
            val arguments = property.type?.abbreviatedType?.arguments?.map 
                ClassInspectorUtil.createClassName(
                    ((it.type?.classifier) as KmClassifier.Class).name
                )
            
            val paramClass = ClassInspectorUtil.createClassName(clName)
            val elementPackage = clName.replace("/", ".")
            val paramName = property.name

            arguments?.let 
                mutableClassBuilder.addProperty(
                    PropertySpec.builder(
                        paramName,
                        snapshotStateList.parameterizedBy(it), KModifier.PUBLIC
                    )
                        .build()
                )
            
            arguments?.let 
                stateClassBuilder.addProperty(
                    PropertySpec.builder(
                        paramName,
                        snapshotStateList.parameterizedBy(it), KModifier.PUBLIC
                    )
                        .build()
                )
            

            helper = helper.plus("$paramName = $paramClass.simpleName(this.$paramName),\n")

            mutableConstructorBuilder
                .addStatement("this.$paramName=rootObject.$paramName.toMutableStateList()")

            stateConstructorBuilder
                .addStatement("this.$paramName=mutableObject.$paramName")
        

        fun processDefaultParameter(property: ImmutableKmValueParameter) 
            val clName = ((property.type?.classifier) as KmClassifier.Class).name
            val paramClass = ClassInspectorUtil.createClassName(clName)
            val elementPackage = clName.replace("/", ".")
            val paramName = property.name

            mutableClassBuilder.addProperty(
                PropertySpec.builder(
                    paramName,
                    mutableStateClass.parameterizedBy(paramClass), KModifier.PUBLIC
                ).build()
            )
            stateClassBuilder.addProperty(
                PropertySpec.builder(
                    paramName,
                    stateClass.parameterizedBy(paramClass),
                    KModifier.PUBLIC
                ).build()
            )

            helper = helper.plus("$paramName = this.$paramName.value,\n")

            mutableConstructorBuilder
                .addStatement(
                    "this.$paramName=mutableStateOf(rootObject.$paramName) "
                )

            stateConstructorBuilder
                .addStatement("this.$paramName=mutableObject.$paramName")
        

        for (property in kmClass.constructors[0].valueParameters) 
            val javaPackage = (property.type!!.classifier as KmClassifier.Class).name.replace("/", ".")
            val javaClass=try 
                Class.forName(javaPackage)
            catch (e:Exception)
                String::class.java
            

            when
                Map::class.java.isAssignableFrom(javaClass) -> //if property is of type map
                    processMapParameter(property)
                
                List::class.java.isAssignableFrom(javaClass) -> //if property is of type list
                    processListParameter(property)
                
                else -> //all others
                    processDefaultParameter(property)
                
            
        

        helper=helper.plus(")") //close off method

        val getRootBuilder= FunSpec.builder("get$className")
            .returns(element.asClassName())
        getRootBuilder.addStatement(helper.toString())
        mutableClassBuilder.addFunction(mutableConstructorBuilder.build()).addFunction(getRootBuilder.build())
        stateClassBuilder.addFunction(stateConstructorBuilder.build())

        val kaptKotlinGeneratedDir = processingEnv.options[KAPT_KOTLIN_GENERATED_OPTION_NAME]

        val mutableFile = mutableFileBuilder
            .addImport("androidx.compose.runtime", "mutableStateOf")
            .addImport("androidx.compose.runtime","toMutableStateMap")
            .addImport("androidx.compose.runtime","toMutableStateList")
            .addType(mutableClassBuilder.build())
            .build()
        mutableFile.writeTo(File(kaptKotlinGeneratedDir))

        val stateFile = stateFileBuilder
            .addType(stateClassBuilder.build())
            .build()
        stateFile.writeTo(File(kaptKotlinGeneratedDir))
    

gradle 注释

plugins 
    id 'java-library'
    id 'org.jetbrains.kotlin.jvm'


dependencies 
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"


java 
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8

gradle 处理器

plugins 
    id 'kotlin'
    id 'kotlin-kapt'


dependencies 
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation project(':annotations')
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.10"
    // https://mvnrepository.com/artifact/com.squareup/kotlinpoet
    implementation 'com.squareup:kotlinpoet:1.10.2'
    implementation "com.squareup:kotlinpoet-metadata:1.7.1"
    implementation "com.squareup:kotlinpoet-metadata-specs:1.7.1"
    implementation "com.google.auto.service:auto-service:1.0.1"
    // https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-metadata-jvm
    implementation "org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.4.2"
    implementation 'org.json:json:20211205'

    kapt "com.google.auto.service:auto-service:1.0.1"


java 
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8

【讨论】:

以上是关于Jetpack Compose State:修改类属性的主要内容,如果未能解决你的问题,请参考以下文章

Jetpack Compose 深入探索系列五:State Snapshot System

如何在 Jetpack compose 中制作可重用的组件?

Jetpack Compose 学习汇总

如何从 Jetpack Compose TextField 关闭虚拟键盘?

Jetpack Compose LazyColumn 以编程方式滚动到项目

修改器取决于 Jetpack Compose 中其他修改器的值?