Jetpack Compose 状态:修改 class 属性

Jetpack Compose State: Modify class property

下面的两个示例只是将 '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'")
        }
    }
}

然而,这并不总是可行的。例如,数据比单个字段更复杂。例如 class,甚至 Room data 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 无效。这是我的一个简单错误,还是我错过了关于我应该如何修改这个 class 实例的大图?

编辑:

有点找到了一种方法来完成这项工作,但似乎效率不高。然而,它确实与 React 管理状态的方式一致,所以这可能是正确的方式。

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

  1. 正在创建 MyThing class 的新实例,更改需要更改的内容,然后使用新的 class 实例调用 setMyThing()
  2. class MyThing 更改为 data class MyThing 并使用 copy() 函数创建具有相同属性的新实例。然后,更改所需的 属性 并调用 setMyThing()。考虑到我明确表示我想用它来修改 Android 房间使用的给定 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'")
        }
    }
}

确实,在我看来,解决此问题的最佳方法是 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")
        }
    }
}

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

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

Indeed, it appears to me that the best way to go about this is to copy() a data class.

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

// 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 取决于您。

Is this a simple error on my part, or am I missing the larger picture about how I should go about modifying this class instance?

Compose 无法知道对象发生了变化,因此它不知道需要重新组合。

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

An alternative approach, however, would be most welcome.

你不限单个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")
    }
  }
}

好的,对于任何想知道这个问题的人来说,有一种更简单的方法可以解决这个问题。当你像这样定义可变状态 属性 时:

//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

存储数据的注释class @AsState

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

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

请告诉我您是否认为这有用,以便在数据 class 在可组合项中 displayed/edited 时更清晰地跟踪状态(如果您不这样做)

原class

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

带有自定义构造函数和 rootClass getter

的 class 的可变版本
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.map{Pair(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的不可变版本
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 文章,并根据艰苦的谷歌搜索实现了我自己的一些更改。我将来可能会把它做成一个模块,这样其他人就可以更容易地在他们的项目中实现它我有任何兴趣:

注解class

@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 = "${className}MutableState"
        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 = "${className}State"
        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}.map{Pair(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
}