Compose随记 - 关于LazyColumn的列表项更新

5,272 阅读4分钟

说一点题外话

自从学习了MVI架构,我便爱上了这样的开发模式:

  • 所有的页面所需的model都集中在UiState
  • 所有的视图都跟随UiState的变化而变化
  • 所有视图产生的事件都统一成UiAction,在中间层统一解决

整个MVI架构将视图所需的数据与视图事件后续的处理从视图层更好地剥离了出来,而且代码上更加简洁。

Compose和MVI的初体验

而在最初学习Compose以及MVI架构的时候,我的数据通常都是十分基本的类型,Int类型数据自增自减、String类型接受用户的输入然后进行一个SnackBar的弹等等。对于这些数据的变化,我会选择使用状态提升数据事件从视图层面抽离出来。

大的要来了(指列表)

终于,我用上了极为先进的LazyColumn,并模仿着之前的方式来编写UiState

  • 一个接受泛型参数的data class,在这里这个泛型当然是List<ClassOfData>
  • 有加载状态和错误状态区分

第一次尝试

“这些应该足够了”,我想。于是我按着这条思路编写了一个本机已安装应用的列表界面。因为目前只需要知道应用的图标和应用名称,所以我没有直接使用PackageInfo对象作为我的ClassOfData,而是自己单独写了一个data class

data class InstalledAppInfo(
    val iconDrawable: Drawable?,
    var name: String = BaseApplication.appContext.getString(R.string.default_installed_app_name),
    var isSelected: Boolean = false
)

结构也相当简单,不是吗。这里面还有一些默认值,这里就不详细展开说了。当然,一开始是没有isSelected字段的。程序运行起来,展示的效果也还挺不错,各个应用都按着图标 - 名称的方式排列着。

整个界面大概就长这样,除了那个复选框

我很满意,然后不紧不慢地加上了isSelected字段和复选框,并补上了onUiAction()方法,想着要一个position和一个shouldSelect属性来修改列表当中的元素应该就可以了:

fun onUiAction(action: IconSelectUiAction) {
    when (action) {
        is Select -> {
            val list =  mutableUiState.value.result
            val i = action.position
            kotlin.runCatching {
                list!!.apply {
                    get(i).isSelected = action.shouldSelect
                }
            }.onSuccess {
                mutableUiState.value = mutableUiState.value.copy(result = it)
            }
        }
    }
}

// 这里都是界面事件的定义
sealed interface IconSelectUiAction
class Select(val position: Int, val shouldSelect: Boolean) : IconSelectUiAction

我尝试这样来更新列表项,也就是先拿到旧列表数据,然后根据UiAction中所带的position来修改旧列表中对应位置的值的属性。结果是怎么试都没有反应,但是列表项再离屏后重绘可以展示选中状态。这让我苦恼不已。

毕竟第一次,多少寄了。解决一下吧,应该不难

好在之前也看过一些Compose的CodeLab,依稀记得有一个CodeLab就是关于列表项中数据的修改。一番查找之后终于找到了:在 Jetpack Compose 中使用状态

这个Todo-List项目中,整个列表使用了mutableStateListOf<>(),所有列表的增删改都要围绕这样一个列表来进行。那么这个列表的底层是什么呢?:

/**
 * Create a instance of MutableList<T> that is observable and can be snapshot.
 *
 * @sample androidx.compose.runtime.samples.stateListSample
 *
 * @see mutableStateOf
 * @see mutableListOf
 * @see MutableList
 * @see Snapshot.takeSnapshot
 */
fun <T> mutableStateListOf() = SnapshotStateList<T>()

其实这个函数就是返回了一个SnapshotStateList对象,于是我就在想,那我直接将我的UiState泛型定义为SnapshotStateList<InstalledAppInfo>应该就能实现列表更新了吧。但事与愿违,只是泛型的修改并没有起到刷新列表的作用。

之后我在上面提到的CodeLab里又发现了一些更新列表相关的代码:

fun onEditItemChange(item: TodoItem) {
    val currentItem = requireNotNull(currentEditItem)
    require(currentItem.id == item.id) {
        "You can only change an item with the same id as currentEditItem"
    }
    todoItems[currentEditPosition] = item
}

这里并没有全量更新UiState中的列表数据,而是单独更新对应position的item。

总之是成了,细节还得后续看看

于是我修改了我的onUiAction()方法:

fun onUiAction(action: IconSelectUiAction) {
    when (action) {
        is Select -> {
            val list =  mutableUiState.value.result ?: return
            val i = action.position
            kotlin.runCatching {
                list[i] = list[i].apply { isSelected = action.shouldSelect }
            }
        }
    }
}

经过这些修改,我的列表终于可以通过点击事件来改变复选框的样式了(无论点击列表项还是复选框都可以)!

总结下来:至少能用,问就是不懂(啊对对对

(P.S.: 如果有兴趣不妨来我的GitHub项目仓库看一看)

(P.P.S: 明天应该要回归真正的业务优化上了,其实这个仓库也是一个Library的Demo,顺带忙里偷闲学一点Compose罢了🤪)