kotlin 是否为LazyColumn键缓存了rememberDismissState?

bq3bfh9z  于 2022-12-13  发布在  Kotlin
关注(0)|答案(1)|浏览(205)

我有可组合的

fun ShowProduct(name: String, image: String, onDismissed: () -> Unit) {
    var state = rememberDismissState();

    if (state.isDismissed(DismissDirection.EndToStart)) {
        onDismissed()
    }

    SwipeToDismiss(
        state = state,
        background = { ShowSwipableActions(name) },
        modifier = Modifier
            .padding(10.dp, 10.dp)
            .height(75.dp),
        directions = setOf(DismissDirection.EndToStart),
        dismissThresholds = { _ -> FractionalThreshold(0.5f) }
    ) {
        /* Content */
    }
}

就像这样
第一次
视图模型:

class ProductsListViewModel() : ViewModel() {
    private var _products = mutableStateListOf<Product>()
    val products: List<Product>
        get() = _products

    var isLoadingProducts = mutableStateOf(false);

    fun removeProduct(id: String) {
        _products.removeIf { x -> x.id == id }
    }

    fun refresh() {
            isLoadingProducts.value = true

            if(_products.any()){
                _products.clear()
            }

            for (i in 0..50) {
                _products.add(
                    Product(
                        i.toString(),
                        "Product $i",
                        "*image url*"
                    )
                );
            }

            isLoadingProducts.value = false
    }
}

如果我在ViewModel中删除一个项,然后调用refresh()函数,被删除的键将显示为已删除状态。如果我删除和添加同一项,是否应该在LazyColumn的整个生命周期中使用完全唯一的键?
比如说

i.toString() + System.currentTimeMillis()
nwwlzxa7

nwwlzxa71#

由于你发布的代码不完整,我只是假设了其中的一些部分,当我填充缺失的部分时,我只创建了2项而不是50,因为我无法区分我创建的每个红色Box项。基于显示正在执行的操作的GIF。在滑动删除2个项目并单击“刷新”按钮后,它崩溃并显示以下堆栈跟踪。

E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.stackoverflowcomposeproject, PID: 24991
    java.lang.IndexOutOfBoundsException: Index 1, size 1
        at androidx.compose.foundation.lazy.layout.MutableIntervalList.checkIndexBounds(IntervalList.kt:177)
        at androidx.compose.foundation.lazy.layout.MutableIntervalList.get(IntervalList.kt:160)
        at androidx.compose.foundation.lazy.layout.DefaultLazyLayoutItemsProvider.getKey(LazyLayoutItemProvider.kt:236)

问题是从这里来的

if (state.isDismissed(DismissDirection.EndToStart)) {
    onDismissed()
}

似乎当您清除SnapshotStateList并添加另一组项时,在添加first项且LazyColumn执行了更新的那一刻,onDismissed()被调用,它也会立即调用ViewModel中的remove函数,并返回先前的状态,它总是引用最后一个被删除的项。我得到的索引1超出界限,当大小为3时,我得到的索引2超出界限,依此类推。
一开始看起来可能很奇怪,因为每个人都可能认为当ShowList重新组合时,rememberDismissState会创建一个新的状态,但如果你稍微挖掘一下,你会看到它的API是rememberSaveable{…}AFAIK,它在重新组合后仍然存在。

@Composable
@ExperimentalMaterialApi
fun rememberDismissState(
    initialValue: DismissValue = Default,
    confirmStateChange: (DismissValue) -> Boolean = { true }
): DismissState {
    return rememberSaveable(saver = DismissState.Saver(confirmStateChange)) {
        DismissState(initialValue, confirmStateChange)
    }
}

修复我的遇到的问题,只是创建一个remembered{..}DismissState。(注意,我不确定这样做是否会有任何其他影响,除了不能生存的配置更改,如屏幕旋转,但它解决了我遇到的崩溃,也可能解决你的)

val state = remember {
      DismissState(
         initialValue = DismissValue.Default
      )
}

我所做的另一件事是(不是修复,但可能是优化)将dismissState函数调用 Package 在derivedStateOf中,因为不这样做(像您的)将在其封闭的可组合函数上执行多次重新组合

val isDismissed by remember {
     derivedStateOf {
          state.isDismissed(DismissDirection.EndToStart)
     }
}

// used like this
if (isDismissed) {
    onDismissed()
}

这些是您修改的组件(您发布的所有代码)。

// your Data class that I assumed
data class Product(
    val id : String,
    val name: String
)

// your Screen where I removed unnecessary codes to reproduce the issue
@Composable
fun ProductsScreen(vm: ProductsListViewModel = ProductsListViewModel()){
    ShowList(
        vm,
        { x -> vm.removeProduct(x) },
        { vm.refresh() }
    )
}

// your ViewModel where I removed unnecessary codes to reproduce the issue
class ProductsListViewModel : ViewModel() {

     var products = mutableStateListOf<Product>()

    fun removeProduct(id: String) {
        products.removeIf { x -> x.id == id }
    }

    fun refresh() {

        if(products.any()) {
            products.clear()
        }

        for (i in 0..1) {
            products.add(
                Product(
                    i.toString(),
                    "Product $i"
                )
            )
        }
    }
}

// your ShowProduct where I removed unnecessary codes to reproduce the issue
// added swipe back background and a red rectangular box to see an item
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun ShowProduct(
    onDismissed: () -> Unit
) {

    val state = remember {
        DismissState(
            initialValue = DismissValue.Default
        )
    }

    val isDismissed by remember {
        derivedStateOf {
            state.isDismissed(DismissDirection.EndToStart)
        }
    }

    if (isDismissed) {
        onDismissed()
    }

    SwipeToDismiss(
        state = state,
        background = {
            Text("SomeSwipeContent")
        },
        modifier = Modifier
            .padding(10.dp, 10.dp)
            .height(75.dp),
        directions = setOf(DismissDirection.EndToStart),
        dismissThresholds = { _ -> FractionalThreshold(0.5f) }
    ) {
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .height(50.dp)
                .background(Color.Red)
        )
    }
}

// your ShowList where I removed unnecessary codes to reproduce the issue
// and added a button to make it work
@Composable
fun ShowList(
    viewModel: ProductsListViewModel,
    onDismissed: (String) -> Unit,
    onRefreshRequested: () -> Unit
) {

    Column {

        Button(onClick = { onRefreshRequested() }) {
            Text("Refresh")
        }

        LazyColumn(
            modifier = Modifier.fillMaxSize()) {
            items(items = viewModel.products, key = { product -> product.id } ) { product ->

                ShowProduct(
                    onDismissed = {
                        onDismissed(product.id)
                    }
                )
            }
        }
    }
}

都是这样用的

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
        setContent {
            ProductsScreen()
        }
}

输出:(gif没有显示崩溃,但如果使用rememberDismissState,单击按钮后会崩溃)

注意:我所做的一切都是为了修复遇到的crash问题,但由于我使用的是你的代码,我只是填补了缺失的部分,也许它会解决你的问题。

相关问题