Android Paging 3实现分页加载

修改待办事项demo,使用Paging 3实现分页加载。

顺便练习下新建远程分支,在远程分支上开发。

1、确认下状态,需要保持当前main分支干净:

2、拉去远程代码,防止有差异

3、创建一个新的本地分支并切换到该分支

4、把该本地分支推送到远程并建立关联(-u = --set-upstream ,让本地分支与远程分支绑定)

ok. 远程分支创建好了。 或者也可以直接在github上创建远程分支:

ok. 以上是练习创建远程分支。下面是在新分支上修改:

依赖项:

修改ToDoDao:

Kotlin 复制代码
package com.example.testcompose1.data

import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
import kotlinx.coroutines.flow.Flow

// 数据访问对象
@Dao
interface TodoDao {
    @Query("SELECT * FROM todo_items ORDER BY createdAt DESC")
    fun getAllTodos(): Flow<List<TodoEntity>> // 返回数据流,需要持续监听数据,不加suspend关键字

    /** 分页查询,供 Paging 使用(每页条数由 PagingConfig.pageSize 决定) */
    @Query("SELECT * FROM todo_items ORDER BY createdAt DESC")
    fun getTodosPaged(): PagingSource<Int, TodoEntity>

    @Insert
    suspend fun insert(todo: TodoEntity)

    @Delete
    suspend fun delete(todo: TodoEntity)

    @Update
    suspend fun update(todo: TodoEntity)
}

修改TodoRepository:

Kotlin 复制代码
package com.example.testcompose1.data

import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.PagingSource
import androidx.paging.PagingState
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow

// 数据仓库
class TodoRepository(private val dao: TodoDao) {

    companion object {
        /** 每页条数:上拉每次加载的数量 */
        const val PAGE_SIZE = 7

        /**
         * 模拟网络/慢查询:每次分页 `load` 前暂停(毫秒)。
         * 设为 0 即关闭模拟耗时。
         */
        const val SIMULATED_PAGE_LOAD_DELAY_MS = 1_200L
    }

    fun getAllTodos(): Flow<List<TodoEntity>> = dao.getAllTodos()

    fun getTodosPaged(): Flow<PagingData<TodoEntity>> = Pager(
        config = PagingConfig(
            pageSize = PAGE_SIZE,
            initialLoadSize = PAGE_SIZE,
            prefetchDistance = 1,
            enablePlaceholders = false
        ),
        pagingSourceFactory = {
            DelayingPagingSource(
                delegate = dao.getTodosPaged(),
                delayMs = SIMULATED_PAGE_LOAD_DELAY_MS
            )
        }
    ).flow

    suspend fun addTodo(title: String) {
        dao.insert(TodoEntity(title = title))
    }

    suspend fun deleteTodo(todo: TodoEntity) {
        dao.delete(todo)
    }

    suspend fun updateTodo(todo: TodoEntity) {
        dao.update(todo)
    }
}

/**
 * 在 Room 的 [PagingSource] 外包一层,每次真正加载前 [delay],便于观察 Loading UI。
 * 并把 delegate 的失效转发出来,保证数据库变更后仍能刷新列表。
 */
private class DelayingPagingSource(
    private val delegate: PagingSource<Int, TodoEntity>,
    private val delayMs: Long
) : PagingSource<Int, TodoEntity>() {

    init {
        delegate.registerInvalidatedCallback { invalidate() }
    }

    override suspend fun load(params: PagingSource.LoadParams<Int>): PagingSource.LoadResult<Int, TodoEntity> {
        if (delayMs > 0) delay(delayMs)
        return delegate.load(params)
    }

    override fun getRefreshKey(state: PagingState<Int, TodoEntity>): Int? =
        delegate.getRefreshKey(state)

    override val jumpingSupported: Boolean
        get() = delegate.jumpingSupported

    override val keyReuseSupported: Boolean
        get() = delegate.keyReuseSupported
}

修改TodoViewModel:

Kotlin 复制代码
package com.example.testcompose1

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.example.testcompose1.data.TodoEntity
import com.example.testcompose1.data.TodoRepository
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch

class TodoViewModel(
    private val repository: TodoRepository
) : ViewModel() {

    /** 分页待办列表(懒加载,每页 [TodoRepository.PAGE_SIZE] 条) */
    val todoPagingFlow: Flow<PagingData<TodoEntity>> =
        repository.getTodosPaged().cachedIn(viewModelScope)

    fun addTodo(title: String) {
        if (title.isNotBlank()) {
            viewModelScope.launch {
                repository.addTodo(title)
            }
        }
    }

    fun deleteTodo(todo: TodoEntity) {
        viewModelScope.launch {
            repository.deleteTodo(todo)
        }
    }

    fun toggleComplete(todo: TodoEntity) {
        viewModelScope.launch {
            repository.updateTodo(todo.copy(isCompleted = !todo.isCompleted))
        }
    }
}

修改TodoListScreen:

Kotlin 复制代码
package com.example.testcompose1

import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import androidx.paging.LoadState
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.itemKey
import com.example.testcompose1.data.TodoEntity
import com.example.testcompose1.data.TodoRepository

@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class, ExperimentalMaterialApi::class)
@Composable
fun TodoListScreen(
    viewModel: TodoViewModel,
    settingsViewModel: SettingsViewModel,
    onNavigateToDetail: (Int) -> Unit = {}
) {
    var showInfiniteList by remember { mutableStateOf(false) }
    if (showInfiniteList) {
        Scaffold(
            topBar = {
                TopAppBar(
                    title = { Text("无限滚动列表") },
                    navigationIcon = {
                        IconButton(onClick = { showInfiniteList = false }) {
                            Icon(Icons.Default.ArrowBack, contentDescription = "返回")
                        }
                    }
                )
            }
        ) { innerPadding ->
            Box(modifier = Modifier.padding(innerPadding)) {
                InfiniteListPage()
            }
        }
    } else {
        val lazyPagingItems = viewModel.todoPagingFlow.collectAsLazyPagingItems()

        // 已有数据时的下拉刷新才显示指示器,避免首屏空白时一直转圈
        val pullRefreshing =
            lazyPagingItems.loadState.refresh is LoadState.Loading && lazyPagingItems.itemCount > 0
        val pullRefreshState = rememberPullRefreshState(
            refreshing = pullRefreshing,
            onRefresh = { lazyPagingItems.refresh() }
        )

        var text by remember { mutableStateOf("") }

        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(16.dp)
        ) {
            ThemeSwitch(settingsViewModel)
            Spacer(modifier = Modifier.height(8.dp))
            TextField(
                value = text,
                onValueChange = { text = it },
                label = { Text("输入待办事项") },
                colors = TextFieldDefaults.colors(
                    focusedContainerColor = MaterialTheme.colorScheme.surface,
                    unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant,
                    focusedIndicatorColor = MaterialTheme.colorScheme.primary,
                    unfocusedIndicatorColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f)
                ),
                modifier = Modifier.fillMaxWidth()
            )
            Button(
                onClick = {
                    viewModel.addTodo(text)
                    text = ""
                },
                shape = MaterialTheme.shapes.small,
                colors = ButtonDefaults.buttonColors(
                    containerColor = MaterialTheme.colorScheme.primary,
                    contentColor = MaterialTheme.colorScheme.onPrimary
                ),
                modifier = Modifier.padding(top = 8.dp)
            ) {
                Text("添加")
            }

            Spacer(modifier = Modifier.height(16.dp))
            Text("待办列表(每页 ${TodoRepository.PAGE_SIZE} 条,上拉加载更多)", style = MaterialTheme.typography.titleMedium)

            Box(
                modifier = Modifier
                    .weight(1f)
                    .fillMaxWidth()
            ) {
                LazyColumn(
                    modifier = Modifier
                        .fillMaxSize()
                        .pullRefresh(pullRefreshState)
                ) {
                    items(
                        count = lazyPagingItems.itemCount,
                        key = lazyPagingItems.itemKey { it.id }
                    ) { index ->
                        val todo = lazyPagingItems[index]
                        if (todo != null) {
                            TodoItemRow(
                                todo = todo,
                                onDelete = { viewModel.deleteTodo(todo) },
                                onToggle = { viewModel.toggleComplete(todo) },
                                onClick = { onNavigateToDetail(todo.id) }
                            )
                        } else {
                            Spacer(Modifier.height(72.dp))
                        }
                    }

                    item {
                        when (val append = lazyPagingItems.loadState.append) {
                            is LoadState.Loading -> {
                                Box(
                                    modifier = Modifier
                                        .fillMaxWidth()
                                        .padding(16.dp),
                                    contentAlignment = Alignment.Center
                                ) {
                                    CircularProgressIndicator()
                                }
                            }

                            is LoadState.Error -> {
                                Text(
                                    text = "加载更多失败:${append.error.localizedMessage}",
                                    color = MaterialTheme.colorScheme.error,
                                    modifier = Modifier.padding(16.dp)
                                )
                            }

                            else -> {}
                        }
                    }
                }

                PullRefreshIndicator(
                    refreshing = pullRefreshing,
                    state = pullRefreshState,
                    modifier = Modifier.align(Alignment.TopCenter)
                )

                if (lazyPagingItems.loadState.refresh is LoadState.Loading && lazyPagingItems.itemCount == 0) {
                    CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
                }

                val refreshError = lazyPagingItems.loadState.refresh as? LoadState.Error
                if (refreshError != null && lazyPagingItems.itemCount == 0) {
                    Text(
                        text = "加载失败:${refreshError.error.localizedMessage}",
                        color = MaterialTheme.colorScheme.error,
                        modifier = Modifier.align(Alignment.Center)
                    )
                }
            }
        }
    }
}

@Composable
fun TodoItemRow(
    todo: TodoEntity,
    onDelete: () -> Unit,
    onToggle: () -> Unit,
    onClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    Card(
        modifier = modifier
            .fillMaxWidth()
            .padding(vertical = 4.dp)
            .clickable { onClick() },
        elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
        shape = MaterialTheme.shapes.medium,
        colors = CardDefaults.cardColors(
            containerColor = MaterialTheme.colorScheme.surface
        )
    ) {
        Row(
            verticalAlignment = Alignment.CenterVertically,
            modifier = Modifier
                .fillMaxWidth()
                .padding(vertical = 8.dp),
            horizontalArrangement = Arrangement.SpaceBetween
        ) {
            Checkbox(
                checked = todo.isCompleted,
                onCheckedChange = { onToggle() }
            )
            Text(
                text = todo.title,
                style = MaterialTheme.typography.bodyLarge,
                color = MaterialTheme.colorScheme.onSurface,
                textDecoration = if (todo.isCompleted) TextDecoration.LineThrough else null
            )
            IconButton(onClick = onDelete) {
                Icon(
                    Icons.Default.Delete,
                    contentDescription = "删除",
                    tint = MaterialTheme.colorScheme.error
                )
            }
        }
    }
}

@Composable
fun ThemeSwitch(settingsViewModel: SettingsViewModel) {
    val isDarkTheme by settingsViewModel.isDarkTheme.collectAsState()
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .clip(MaterialTheme.shapes.medium)
            .background(MaterialTheme.colorScheme.surface)
            .padding(horizontal = 16.dp, vertical = 8.dp),
        horizontalArrangement = Arrangement.SpaceBetween,
        verticalAlignment = Alignment.CenterVertically
    ) {
        Text(
            text = "深色模式",
            style = MaterialTheme.typography.bodyLarge,
            color = MaterialTheme.colorScheme.onSurface
        )
        Switch(
            checked = isDarkTheme,
            onCheckedChange = { settingsViewModel.toggleDarkMode() }
        )
    }
}

运行:

下载:

上拉:

下拉:

ok. 准备提交.

查看分支状态:

暂存+提交:

推送到github仓库:

查看远程仓库:

ok. 已推送到远程仓库。现在想把feature/branch1分支的改动合入到main分支。

点击github页面上的Compare & pull request按钮,再按照下面操作:

ok. 代码合入主分支了。 如果不想要这个远程分支了,可以点击右边的delete branch按钮。

相关推荐
器灵科技30 分钟前
AI视频工具实测:Seedance/可灵/HappyHorse谁最能打?
java·运维·数据库·人工智能·github
BreezeDove34 分钟前
【Android】AS项目自动连接mumu模拟器配置
android
DogDaoDao1 小时前
【GitHub】 Headroom 深度解析:AI Agent 上下文压缩层的完整技术拆解
人工智能·深度学习·程序员·github·ai agent·智能体·agent skill
dominciyue1 小时前
当 judge 们吵起来时,别再投票了:用执行结果给 code eval 一个 ground truth
github
IT 行者2 小时前
GitHub Spec Kit 实战(六):/speckit.implement 怎么用、怎么审、怎么发现 spec 阶段的遗漏——五部曲收官
java·驱动开发·github·ai编程·claude
带娃的IT创业者2 小时前
深度解析:从 GitHub 热门项目看 SEO 自动化的技术架构演进
架构·自动化·github·seo·技术架构·反爬虫
SCandL1522 小时前
自动化ai测试
github
IT 行者3 小时前
GitHub Spec Kit 实战(四):读懂和干预 /speckit.plan——AI 最自由发挥的一步
java·人工智能·github·ai编程·claude
乐世东方客3 小时前
备份脚本记录(binlog文件+mysql+mongo)
android·数据库·mysql
私人珍藏库4 小时前
[Android] 视频下载鸟 v20.02 会员
android·人工智能·智能手机·app·工具·多功能