喷气背包Compose状态:修改类属性

19
下面的两个示例只是在给定的默认值中添加一个“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'")
        }
    }
}

然而,这并不总是可行的。例如,数据比单个字段更复杂。例如一个类,或者甚至是一个 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无效。这是我犯的一个简单错误,还是我未能理解如何修改此类实例的更大图景?

编辑:

我已经有点找到了一种方法使其工作,但似乎效率低下。然而它与React管理状态的方式相吻合,所以也许这就是正确的方法。

显然,示例2中的问题在于myNextThing不是原始myThing的副本,而是对它的引用。就像React一样,Jetpack Compose似乎希望在修改状态时使用全新的对象。有两种方法可以做到这一点:

  1. 创建MyThing类的新实例,更改需要更改的内容,然后使用新的类实例调用setMyThing()
  2. 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'")
        }
    }
}
6个回答

12

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

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


1
这确实是一个相当不错的方法。如果我们最后不需要将该值设置为其本身,那就更好了,唉... - quealegriamasalegre
经过一段时间,我现在更喜欢将属性用作模型中的可变状态,这样更容易管理。 - Augusto Alonso
我希望我能做到,但我的对象嵌套混乱。 - quealegriamasalegre
设置dummy.value = dummy.value会重新组合所有内容吗? - dessalines

11

实际上,我认为处理这个问题的最佳方式是复制一个数据类。

在使用自定义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")
    }
  }
}

1
我目前正在为您的最后一部分答案苦苦挣扎。我猜想您建议使用多个状态对象来反映每个数据类的属性,以避免在仅更改一个属性时重新组合整个可组合树。但对于更大的数据类,这确实使我的代码膨胀,需要几行代码来使数据类的每个属性都可观察。我想知道是否可以使用反射创建数据类的转换,以便将其所有属性公开为状态。有点像将其转化为自身的状态持有者版本。 - quealegriamasalegre
当您拥有深度嵌套的对象时,这将成为一场噩梦。 - dessalines

4

实际上,我认为解决这个问题的最好方法是使用 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的做法:状态永远不会被修改或添加,而是完全被替换。
然而,另一种方法也是非常受欢迎的。

1
但是想象一下,你有一个大型数据类(比如用户档案类),并且你要在屏幕上显示用户档案。这种方法将触发重新组合整个可组合树,而实际上可能只有一个单独的文本字段已经改变了。这应该会对性能产生影响,不是吗? - quealegriamasalegre

3

一个用于存储数据类的注释 @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.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 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
  }
}

简而言之

接下来我会贴出我的注解处理器源代码,以便您可以实现它。我基本上遵循了这篇文章,并根据艰苦的谷歌搜索实现了一些自己的改变。如果有兴趣的话,我可能会在未来将其制作成一个模块,以便其他人更轻松地在其项目中实现此功能:

注解类

@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
}

感谢分享你的有趣工作,@quealegriamasalegre。然而,在我看来,委托属性有点违背了你的注解处理器的初衷,除非你需要在构造函数中初始化这些属性,在这种情况下,你的解决方案可以节省一些重复。你可能会对我的答案感兴趣,尤其是第二部分。 - Davide Cannizzo
我也觉得你对于不可变方法可能存在的性能损失的看法非常有趣,@quealegriamasalegre。我在我的回答中引用了它(当然给了你功劳),并加入了我的考虑。 - Davide Cannizzo

2
对于一个属性委托,确实使得对可变的 MutableState 进行改变更加方便(下面有示例)。
此外,使用不可变的 data class 是最直接且总体上最好的解决方案。不可变性确实能够提供有用的保证。然而,在某些情况下这可能不适用,针对这些情况,后续将在回答中进行介绍。
通常情况下,你应该倾向于选择不可变的方式,并简单地使用 copy 生成修改后的实例,并将 MutableState 更新为最新的实例,例如(更新问题后的修改版本):
@Immutable
data class MyThing(var name: String)

@Composable
fun MyScreen() {

    var myThing by remember { mutableStateOf(MyThing(name = "Ma")) }

    Column {

        Text(text = myThing.name)

        Button(
            onClick = {
                myThing = myThing.run { copy(name = "${name}a" }
            }
        ) {
            Text(text = "Add an 'a'")
        }
    }
}

还要注意,对于 MyThing 上的 @Immutable 注释给予了 Compose 可能允许更大优化的有用保证。
请注意,copy 方法执行浅复制,因此建议采用深度不可变性(也就是,确保 data class 仅具有不可变类型的只读属性)。
然而,像这样具有结构标识的类才能使用原子更新来实现不可变性(这就是为什么特别适合使用`data class`)。
或者,在某些罕见情况下,可能只是性能问题,比如在动画的每一帧中使用。虽然这种情况很少见,但不要浪费时间进行过早的优化。如果您对此特别感兴趣(或者目前遇到性能问题),请查看本文末尾的附加说明。
无论如何,这里有一个替代方案。那就是... 简单地拥有一个保存`MutableState`的类,而不是拥有一个`MutableState`来保存类的实例:
// Not a data class, referential identity.
@Stable
class MyThing {
    // Using delegated properties for ease of use.
    var name by mutableStateOf(value = "Ma")
    // Optionally add any more properties like name.
}

@Composable
fun MyScreen() {

    // Remember the same mutable instance of MyThing.
    val myThing = remember { MyThing(name = "Ma") }

    Column {

        Text(text = myThing.name)

        Button(
            onClick = {
                // Simply mutate a property of the same instance.
                myThing.name += "a"
            }
        ) {
            Text(text = "Add an 'a'")
        }
    }
}

请注意,Snapshot系统和Compose Applier一起足够智能,可以正确地安排重新组合。因此,在单个类中将多个属性作为单独的MutableState并同时对它们进行修改不会导致不必要的组合,有效地使这些修改几乎是原子性的。

性能注释:

事实上,@quealegriamasalegre 关于使用不可变方法处理较大对象可能产生性能损失的观点是最好的(请参见他们的答案)。 因为每个依赖于这样 ``data class`` 的可变状态 ``MutableState`` 的站点在该状态的变异时都将无效,从而可能导致过多的重新组合。

然而,请注意,在一些@Composable嵌套树的某个节点处,状态可能会被解构,例如,Text只会得到MyThing.name。因此,如果name没有改变(因为跳过甜甜圈),则Text不会被重新组合,因此在大多数情况下,基于不可变性的性能考虑是不太重要的(事实上,图形本身比组合要重得多)。也就是说,除非您有一个非常深的树,有很多依赖于巨大data class实例的分支,否则我会质疑开发者的理智(笑)。


我也曾经质疑过自己的理智哈哈哈。我的课程类代表了一个由多个部分组成的英语课程地图,每个部分又包含多个里程碑,这些里程碑有许多属性... 课程>部分>里程碑>属性,因此我从Firebase获取了一个嵌套的哈希映射,并且需要独立跟踪每个分支的状态。问题在于对于这样的对象,.copy()并不起作用,因为复制包含哈希映射的对象实际上会返回相同的哈希映射实例,即使其内容已更改。 - quealegriamasalegre
我的注解处理器解决的问题是必须分解一个包含映射和列表的深度嵌套数据类以跟踪其状态。使用我的@State注解,您基本上只是节省了为类中的每个列表和映射执行此操作的样板文件,并且可以以不那么冗长的方式更改类中单个属性的状态。话虽如此,我认为我的方法的一个弱点是它是浅层的,即它实际上并没有解决在数据类中存在许多嵌套层次的映射和列表的情况。 - quealegriamasalegre
@quealegriamasalegre,实际上,有mutableStateListOfmutableStateMapOf专门处理列表和映射中的状态更改。 - Davide Cannizzo
1
我知道,如果你看我的注解处理器,它会将你的列表和映射转换为SnapshotStateList/Map,这就是mutableStateMapOf返回的内容。在我的方法中,你不必在视图模型中显式地转换数据类中的列表和映射,因为一个适当的状态持有类会在编译时自动为你创建。 - quealegriamasalegre
@quealegriamasalegre,噢耶,一开始我没有注意到。我没有考虑与“ViewModel”一起工作的必要性,因为最近我正在放弃所有那些东西,转而使用Kotlin的“StateFlow”和Compose的“State”。这也允许跨平台Compose应用程序(Jetbrain的Compose Multiplatform允许构建桌面应用程序,还有Web和iOS的beta版本)。不幸的是,不是每个人都在研究新项目(哈哈)。 - Davide Cannizzo

0

我正在使用视图模型这种方式进行操作,不确定这种方法的性能如何,但如果对象有许多属性需要更改,它可以为我节省很多代码行。在这里,我添加了另一个属性“surname”。

数据类:

data class MyThing(var name: String = "Ma", var surname: String = "Foo");

视图模型类:

class MyThingVM: ViewModel() {
   
    // It's good practice to modify values only by the owner (viewmodel)
    private val _myThing= mutableStateOf(MyThing())
    val myThing: MyThing
       get() = _myThing.value

    fun onThingValueChange(thingCopy: MyThing){
        _myThing.value = thingCopy
    }
}

可组合的:

@Composable
fun MyScreen(val vm: MyThingVM = viewModel()): {

    val myThing = vm.myThing

    Column {

        Text(text = myThing.name)
        Text(text = myThing.surname)

        Button(
            onClick = {
                // make a copy, modify property and pass it to viewModel
                var thingCopy = myThing.copy()
                    thingCopy.name = myThing.name + "a"
                    vm.onThingValueChange(thingCopy)
            }
        ) {
            Text(text = "Add an 'a' to name")
        }

        Button(
            onClick = {
                var thingCopy = myThing.copy()
                    thingCopy.surname= myThing.surname + "o"
                    vm.onThingValueChange(thingCopy)
            }
        ) {
            Text(text = "Add an 'o' to surname")
        }
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接