喷气背包Compose @Stable List<T>参数重组

12

@Composable函数会被重新组合:

  • 如果其中一个参数被更改,或者
  • 如果其中一个参数不是@Stable/@Immutable

当将items: List<Int>作为参数传递时,compose始终会进行重新组合,无论List是否是不可变的并且无法更改。(List是没有@Stable注释的接口)。因此,任何接受List<T>作为参数的Composable函数都将被重新组合,没有智能的重新组合。

如何将List<T>标记为稳定,以便编译器知道List是不可变的,并且由于它的存在,函数永远不需要重新组合?

我发现的唯一方法是像这样包装:@Immutable data class ImmutableList<T>(val items: List<T>)。演示 (当Child1重新组成Parent时,具有相同List的Child2也会重新组成)

class TestActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeBasicsTheme {
                Parent()
            }
        }
    }
}

@Composable
fun Parent() {
    Log.d("Test", "Parent Draw")
    val state = remember { mutableStateOf(false) }
    val items = remember { listOf(1, 2, 3) }

    Column {
        // click forces recomposition of Parent
        Child1(value = state.value,
            onClick = { state.value = !state.value })

        //
        Child2(items)
    }
}

@Composable
fun Child1(
    value: Boolean,
    onClick: () -> Unit
) {
    Log.d("Test", "Child1 Draw")
    Text(
        "Child1 ($value): Click to recompose Parent",
        modifier = Modifier
            .clickable { onClick() }
            .padding(8.dp)
    )
}

@Composable
fun Child2(items: List<Int>) {
    Log.d("Test", "Child2 Draw")
    Text(
        "Child 2 (${items.size})",
        modifier = Modifier
            .padding(8.dp)
    )
}

不应重新组合。请提供您的问题的最小可重现示例:您使用items参数的可组合项长什么样,您如何从另一个可组合项调用它以及导致顶层函数重新组合的原因是什么。 - Phil Dukhov
@PhilipDukhov 添加了 - Jemshit Iskenderov
看起来你做的一切都是正确的,可能是个 bug。我建议你在 Compose 的问题跟踪器中报告它。 - Phil Dukhov
总的来说,重组并不是一件坏事。当然,如果你能减少它,就应该这样做(包括报告像这样的错误),但即使代码被重组多次,它也应该正常工作。在某些动画中,重组可能会发生一次帧。避免在视图构建器中进行任何重计算或直接更改视图状态。 - Phil Dukhov
@PhilipDukhov 如果我错了,请纠正我,但我认为这不是一个错误。尽管列表是不可变的,但其中的元素可能是可变的。在这种情况下,它们是Int原语,恰好是不可变的,但通常它们可以是任何东西,因此在我看来,在列表接口上注释稳定是没有意义的。 - kng
显示剩余2条评论
4个回答

3

你主要有两个选项:

  1. 使用一个被注解为 @Immutable 或 @Stable 的包装类(就像你已经做的那样)。
  2. Compose 编译器 v1.2 添加了对 Kotlinx Immutable Collections 库的支持。

对于 Option 2,你只需要将 List 替换为 ImmutableList。Compose 将从库中获取的集合类型视为真正的不可变对象,因此不会触发不必要的重新组合。

请注意:在撰写本文时,该库仍处于 alpha 阶段。

我强烈建议阅读这篇文章,以更好地掌握 Compose 如何处理稳定性问题(以及如何调试稳定性问题)。


1

另一个解决方法是传递一个 SnapshotStateList

具体来说,如果您像 Android codelabs 中建议的那样在您的 ViewModel 中使用后备值,那么您将面临同样的问题。

private val _myList = mutableStateListOf(1, 2, 3)
val myList: List<Int> = _myList

使用myList的可组合项即使_myList未更改也会被重新组合。相反,最好直接传递可变列表(当然,您仍应将列表视为只读,但现在编译器不会再帮助您)。

同时使用包装器不可变列表的示例:

@Immutable
data class ImmutableList<T>(
    val items: List<T>
)

var itemsList = listOf(1, 2, 3)
var itemsImmutable = ImmutableList(itemsList)

@Composable
fun Parent() {
    Log.d("Test", "Parent Draw")
    val state = remember { mutableStateOf(false) }
    val itemsMutableState = remember { mutableStateListOf(1, 2, 3) }

    Column {
        // click forces recomposition of Parent
        Child1(state.value, onClick = { state.value = !state.value })
        ChildList(itemsListState)                   // Recomposes every time
        ChildImmutableList(itemsImmutableListState) // Does not recompose
        ChildSnapshotStateList(itemsMutableState)   // Does not recompose
    }
}

@Composable
fun Child1(
    value: Boolean,
    onClick: () -> Unit
) {
    Text(
        "Child1 ($value): Click to recompose Parent",
        modifier = Modifier
            .clickable { onClick() }
            .padding(8.dp)
    )
}

@Composable
fun ChildList(items: List<Int>) {
    Log.d("Test", "List Draw")
    Text(
        "List (${items.size})",
        modifier = Modifier
            .padding(8.dp)
    )
}

@Composable
fun ChildImmutableList(items: ImmutableList<Int>) {
    Log.d("Test", "ImmutableList Draw")
    Text(
        "ImmutableList (${items.items.size})",
        modifier = Modifier
            .padding(8.dp)
    )
}

@Composable
fun ChildSnapshotStateList(items: SnapshotStateList<Int>) {
    Log.d("Test", "SnapshotStateList Draw")
    Text(
        "SnapshotStateList (${items.size})",
        modifier = Modifier
            .padding(8.dp)
    )
}

1
因为Compose在处理集合类型(如List、Set和Map)时有一些假设,这些类是不稳定的参数。您可以在您的类中添加注解@Stable()或@Immutable来解决这个问题。
@Immutable
data class Foo(val lorem: List<String>) 

-2

使用lambda,你可以这样做

@Composable
fun Parent() {
    Log.d("Test", "Parent Draw")
    val state = remember { mutableStateOf(false) }
    val items = remember { listOf(1, 2, 3) }
    val getItems = remember(items) {
        {
            items
        }
    }

    Column {
        // click forces recomposition of Parent
        Child1(value = state.value,
            onClick = { state.value = !state.value })

        //
        Child2(items)
        Child3(getItems)
    }
}

@Composable
fun Child3(items: () -> List<Int>) {
    Log.d("Test", "Child3 Draw")
    Text(
        "Child 3 (${items().size})",
        modifier = Modifier
            .padding(8.dp)
    )
}

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