简体   繁体   English

是否为 LazyColumn Key 缓存了 rememberDismissState?

[英]Is rememberDismissState cached for LazyColumn Key?

I have composable我有可组合的

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

Which is rendered like this这是这样呈现的

@Composable
fun ProductsScreen(vm: ProductsListViewModel = ProductsListViewModel()){
    ShowList(
        vm.products,
        { x -> vm.removeProduct(x) },
        vm.isLoadingProducts.value,
        {
            vm.refresh()
        }
    )
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ShowList(
    products: List<Product>,
    onDismissed: (productId: String) -> Unit,
    refreshing: Boolean,
    onRefreshRequested: () -> Unit
) {
    val haptic = LocalHapticFeedback.current

    PullToRefreshCompose(
        refreshing,
        onRefreshRequested = onRefreshRequested,
        onRefreshDistanceReached = {
            haptic.performHapticFeedback(HapticFeedbackType.LongPress)
        }) {
        LazyColumn(modifier = Modifier.fillMaxSize()) {
            items(items = products, key = { product -> product.id }) { product ->
                ShowProduct(
                    product.name,
                    product.image,
                    onDismissed = { onDismissed.invoke(product.id) });
            }
        }
    }

}

ViewModel:视图模型:

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

If I dismiss an item and then call the refresh() function in my ViewModel, the dismissed keys will be displayed already in the dismissed state. Should I use completely unique keys for the entire lifetime of LazyColumn if I delete and add the same item?如果我关闭一个项目然后在我的 ViewModel 中调用 refresh() function,被关闭的键将已经显示在已关闭的 state 中。如果我删除和添加相同的项目,我是否应该在 LazyColumn 的整个生命周期中使用完全唯一的键?

For example例如


i.toString() + System.currentTimeMillis()

Since your posted code is incomplete, I just assumed some parts of it, and when I filled the missing parts, I only created 2 items instead of 50 as I can't differentiate every red Box that I created with that many items.由于您发布的代码不完整,我只是假定了它的某些部分,当我填充缺失的部分时,我只创建了2项目而不是50 ,因为我无法区分我创建的每个红色Box都有那么多项目。 What I encountered was a crash, based on the GIF showing the actions being done.我遇到的是崩溃,基于显示正在执行的操作的 GIF。 After swipe-deleting the 2 items and clicking the "Refresh" button, it crashes with the stacktrace below.在滑动删除 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)

The issue was coming from here问题来自这里

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

It seems like when you clear the SnapshotStateList and add another set of items, the moment the first item is added and the LazyColumn has performed an update, the onDismissed() is being called which also calls the remove function in your ViewModel immediately with the previous state and it always refers to the last item removed.似乎当您清除SnapshotStateList并添加另一组项目时,在添加first项目并且LazyColumn执行更新时,正在调用onDismissed() ,它还会立即调用 ViewModel 中的remove function 与前一个state,它总是指最后删除的项目。 In my attempt when the size was 2 , I get an Index 1 out of bounds, when the size is 3 I'm getting an Index 2 out of bounds and so on..在我的尝试中,当大小为2时,我得到一个索引 1 越界,当大小为 3 时,我得到一个索引 2 越界等等。

It may seem weird at first as everyone might expect that rememberDismissState will create a new state when your ShowList re-composes, but if you dig it a little bit, you'll see its API as a rememberSaveable{…} and AFAIK , it survives re-composition.乍一看似乎很奇怪,因为每个人都可能认为rememberDismissState会在您的ShowList重新组合时创建一个新的 state ,但是如果您稍微挖掘一下,您会看到它的 API 作为rememberSaveable{…}AFAIK ,它仍然存在重新组合。

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

The fix on my encountered issue , is just to create a remembered{..} DismissState .遇到问题的修复只是创建一个remembered{..} DismissState (Note I'm not sure if there would be any other repercussions doing this aside from not surviving config changes such as screen rotation, but it solves the crash I encountered, might also solve yours) (请注意,除了屏幕旋转等配置更改无法幸存之外,我不确定这样做是否会有任何其他影响,但它解决了我遇到的崩溃问题,也可能解决了你的问题)

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

Another thing I did is (not a fix but maybe an optimization) is I wrapped your dismissState function calls inside a derivedStateOf , because not doing so(like yours) will execute multiple re-compositions on its enclosing composable我做的另一件事是(不是修复,但可能是优化)是我将您的 dismissState function 调用包装在derivedStateOf ,因为不这样做(就像您的那样)将对其封闭的可组合项执行多次重新组合

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

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

And these are your modified components (all codes you posted).这些是您修改过的组件(您发布的所有代码)。

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

All of them were used like this都是这样用的

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

Output: (The gif doesn't show the crashing, but if you use rememberDismissState instead, it will crash after clicking the button) Output:(gif没有显示崩溃,但是如果你使用rememberDismissState代替,点击按钮后会崩溃)

在此处输入图像描述

Note: Everything I did is to fix the crash issue that I encountered, but since I'm using your code and I just filled the missing parts, maybe it will solve yours.注意:我所做的一切都是为了修复遇到的crash问题,但由于我使用的是您的代码并且我只是填补了缺失的部分,也许它会解决您的问题。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM