You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Paging3的Flow<PagingData<T>>中添加单个元素?聊天场景适配

问题描述

我正在用Paging3实现Jetpack Compose聊天界面,列表包含消息和日期模块。创建新消息后能收到接口响应,但想把这条消息直接加到列表开头,且不想重新拉取整个列表,目前没找到合适的解决办法。

相关代码如下:

ViewModel代码

@HiltViewModel
class TasklogsViewModel @Inject constructor(
    private val tasklogsRepository: TasklogsRepository,
    savedStateHandle: SavedStateHandle
) : BaseViewModel() {

    private val taskId: Int? = savedStateHandle[TASK_ID_KEY]

    val tasklogsPager = Pager(
        PagingConfig(pageSize = 10)
    ) {
        TasklogsPagingSource(taskId = taskId, tasklogsRepository)
    }.flow.cachedIn(viewModelScope)

    private val _uiState = MutableStateFlow<TasklogsUiState>(TasklogsUiState())
    val uiState: StateFlow<TasklogsUiState> = _uiState

    fun createTasklogs(message: String) = launchIO({

    }) {
        try {
            val request = CreateTasklogRequest(
                content = message,
                type = "TEXT_MESSAGE",
                taskId = taskId!!,
                dateCreated = System.currentTimeMillis()
            )
            when (val res = tasklogsRepository.createTasklog(request)) {
                is NetworkResponse.Error -> {
                    _uiState.update { currentState ->
                        currentState.copy(errorMessage = res.body.message)
                    }
                }

                is NetworkResponse.Success -> {

                }
            }
        } catch (e: Exception) {
            _uiState.update { currentState ->
                currentState.copy(errorMessage = e.message)
            }
        }
    }
}

PagingSource代码

class TasklogsPagingSource(
    private val taskId: Int?,
    private val tasklogsRepo: TasklogsRepository
) : PagingSource<Int, TasklogsComponentViewData>() {

    override fun getRefreshKey(state: PagingState<Int, TasklogsComponentViewData>): Int? {
        return state.anchorPosition?.let { position ->
            val page = state.closestPageToPosition(position)
            page?.prevKey?.minus(1) ?: page?.nextKey?.plus(1)
        }
    }

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, TasklogsComponentViewData> {
        return try {
            val page = params.key ?: 0
            if (taskId == null) throw Exception("TaskId is invalid")
            when (val response = tasklogsRepo.getTasklogs(taskId, page)) {
                is NetworkResponse.Error -> {
                    LoadResult.Error(Exception(response.body.message))
                }

                is NetworkResponse.Success -> {
                    val tasklogsList = mutableListOf<TasklogsComponentViewData>()
                    response.successBody.data.forEach { moduleData ->
                        when (moduleData.moduleId) {
                            "task_logs" -> {
                                val taskLog =
                                    (moduleData.data as LogEntity.TasklogEntity).toTasklogViewData()
                                tasklogsList.add(taskLog)
                            }

                            "log_date" -> {
                                val logDate =
                                    (moduleData.data as LogEntity.LogDateEntity).toLogDateViewData()
                                tasklogsList.add(logDate)
                            }
                        }
                    }
                    LoadResult.Page(
                        data = tasklogsList,
                        prevKey = null,
                        nextKey = if (response.successBody.data.isNotEmpty()) response.successBody.paginationKey else null
                    )
                }
            }
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }
}

解决方案

通过本地维护新增消息列表 + 合并Paging流与本地流的方式,可实现新消息快速插入列表开头且无需拉取全量数据,具体实现步骤如下:

1. 改造ViewModel,添加本地消息状态

在ViewModel中新增一个MutableStateFlow存储刚创建的本地消息,再将Paging流与本地流合并,把本地消息插入到列表开头:

@HiltViewModel
class TasklogsViewModel @Inject constructor(
    private val tasklogsRepository: TasklogsRepository,
    savedStateHandle: SavedStateHandle
) : BaseViewModel() {

    private val taskId: Int? = savedStateHandle[TASK_ID_KEY]
    // 存储本地新增的消息,用于插入列表开头
    private val _localNewMessages = MutableStateFlow<List<TasklogsComponentViewData>>(emptyList())
    private val localNewMessages = _localNewMessages.asStateFlow()

    // 合并Paging流与本地消息流,将本地消息插入到列表开头
    val tasklogsPager = combine(
        Pager(
            PagingConfig(pageSize = 10)
        ) {
            TasklogsPagingSource(taskId = taskId, tasklogsRepository)
        }.flow.cachedIn(viewModelScope),
        localNewMessages
    ) { pagingData, localMessages ->
        pagingData.insertRefresh(localMessages)
    }.cachedIn(viewModelScope)

    private val _uiState = MutableStateFlow<TasklogsUiState>(TasklogsUiState())
    val uiState: StateFlow<TasklogsUiState> = _uiState

    fun createTasklogs(message: String) = launchIO({

    }) {
        try {
            val request = CreateTasklogRequest(
                content = message,
                type = "TEXT_MESSAGE",
                taskId = taskId!!,
                dateCreated = System.currentTimeMillis()
            )
            when (val res = tasklogsRepository.createTasklog(request)) {
                is NetworkResponse.Error -> {
                    _uiState.update { currentState ->
                        currentState.copy(errorMessage = res.body.message)
                    }
                }

                is NetworkResponse.Success -> {
                    // 将接口返回的新消息转换为ViewData格式
                    val newMessage = res.successBody.data
                        .firstOrNull { it.moduleId == "task_logs" }
                        ?.let { (it.data as LogEntity.TasklogEntity).toTasklogViewData() }
                    
                    newMessage?.let {
                        // 将新消息添加到本地消息列表的开头
                        _localNewMessages.update { current ->
                            listOf(it) + current
                        }
                    }
                }
            }
        } catch (e: Exception) {
            _uiState.update { currentState ->
                currentState.copy(errorMessage = e.message)
            }
        }
    }
}

2. 处理日期模块的适配(可选)

如果新消息的日期与列表顶部的日期模块不匹配,需要额外插入对应的日期模块,可在添加本地消息时补充逻辑:

newMessage?.let { tasklog ->
    // 将消息日期转换为日期模块格式
    val dateViewData = tasklog.date.toLogDateViewData()
    _localNewMessages.update { current ->
        // 检查本地消息列表顶部是否已有相同日期的模块,没有则添加
        if (current.firstOrNull() is LogDateViewData && 
            (current.first() as LogDateViewData).date == tasklog.date
        ) {
            listOf(tasklog) + current
        } else {
            listOf(dateViewData, tasklog) + current
        }
    }
}

3. 同步后端数据(可选)

若希望后续分页加载时能获取到这条新消息(避免重复显示),可在添加本地消息后触发PagingSource刷新:

// 在ViewModel中新增刷新触发器
private val _refreshTrigger = MutableSharedFlow<Unit>()

// 修改合并流逻辑,加入刷新触发器
val tasklogsPager = combine(
    Pager(
        PagingConfig(pageSize = 10),
        pagingSourceFactory = { TasklogsPagingSource(taskId = taskId, tasklogsRepository) }
    ).flow.cachedIn(viewModelScope),
    localNewMessages,
    _refreshTrigger
) { pagingData, localMessages, _ ->
    pagingData.insertRefresh(localMessages)
}.cachedIn(viewModelScope)

// 在createTasklogs的Success分支触发刷新
is NetworkResponse.Success -> {
    // ... 新增本地消息的代码
    _refreshTrigger.emit(Unit)
}

关键说明

  • insertRefresh是Paging3提供的扩展方法,可将数据插入到PagingData的开头,且不会影响已加载的分页数据。
  • 本地消息列表仅作为临时显示,当PagingSource刷新后,后端返回的最新数据会自动替换本地临时数据,避免重复。

内容的提问来源于stack exchange,提问作者Karan Sharma

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 00:22:39