Kotlin 协程基础知识总结七 —— Flow 与 Jetpack Paging3
专题分为五大块:
- Paging3 的结构组成
- Flow 与 Paging3
- 下拉刷新
- 上拉刷新离奇 Bug
- 上游数据缓存
Demo 会还原开发迭代的过程,不会直接一步到位。
1、Paging3 加载数据流程
(P105)Paging3 的简介详情可参考官方文档 Paging 库概览,这里简单介绍下架构:
- 代码库层(Repository):主要使用 PagingSource 组件,也有可能用到 RemoteMediator 组件:
- PagingSource 定义数据源以及如何从该数据源检索数据。PagingSource 对象可以从任何单个数据源(包括网络来源和本地数据库)加载数据
- RemoteMediator 对象会处理来自分层数据源(例如具有本地数据库缓存的网络数据源)的分页,它的作用主要是协调从网络数据源获取数据并将其存储到本地数据库中
- ViewModel 层:
- Pager 组件提供公共 API,基于 PagingSource 和 PagingConfig 构造响应式流中公开的 PagingData 实例
- PagingData 会连接 ViewModel 与 UI,PagingData 对象是用于存放分页数据快照的容器。它会查询 PagingSource 对象并存储结果
- UI 层:Paging 库在本层的主要组件是 PagingDataAdapter,是用于处理分页数据的 RecyclerView 适配器。此外可以使用 AsyncPagingDataDiffer 组件构建自定义适配器
2、编码前准备工作
(P106)编码前的准备工作包括项目的配置以及公共框架的搭建。
2.1 项目配置
首先,项目配置要搞定模块的 build.gradle:
plugins {// 增加 kapt 插件,Paging 会用到id 'kotlin-kapt'
}android {// Execution failed for task ':flow-paging3:kaptGenerateStubsDebugKotlin'.//> 'compileDebugJavaWithJavac' task (current target is 1.8) and 'kaptGenerateStubsDebugKotlin'// task (current target is 17) jvm target compatibility should be set to the same Java version.compileOptions {sourceCompatibility JavaVersion.VERSION_17targetCompatibility JavaVersion.VERSION_17}// Execution failed for task ':flow-paging3:kaptGenerateStubsDebugKotlin'.//> 'compileDebugJavaWithJavac' task (current target is 1.8) and 'kaptGenerateStubsDebugKotlin'// task (current target is 17) jvm target compatibility should be set to the same Java version.kotlinOptions {jvmTarget = '17'}// 开启 viewBinding 和 dataBindingviewBinding {enabled = true}dataBinding {enabled = true}
}dependencies {def constraint_version = "2.1.3"implementation 'androidx.constraintlayout:constraintlayout:$constraint_version'def kotlin_version = "1.8.0"implementation "androidx.core:core-ktx:$kotlin_version"def material_version = "1.5.0"implementation "com.google.android.material:material:$material_version"def appcompat_version = "1.4.1"implementation "androidx.appcompat:appcompat:$appcompat_version"def coroutines_version = "1.4.2"implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"def lifecycle_version = "2.2.0"implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"def swipe_refresh_layout_version = "1.1.0"implementation "androidx.swiperefreshlayout:swiperefreshlayout:$swipe_refresh_layout_version"def retrofit_version = "2.9.0"implementation "com.squareup.retrofit2:retrofit:$retrofit_version"implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"def okhttp_version = "3.4.1"implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"def activity_version = "1.7.0"implementation "androidx.activity:activity:$activity_version"implementation "androidx.activity:activity-ktx:$activity_version"def paging_version = "3.0.0-alpha03"implementation "androidx.paging:paging-runtime:$paging_version"def picasso_version = "2.71828"implementation "com.squareup.picasso:picasso:$picasso_version"
}
注意的点:
- Paging 会用到 ‘kotlin-kapt’ 这个插件,因此需要添加
- 由于当前使用的 AS 版本支持的最低 Gradle 版本是 7.X 版本,这些版本需要的最低 JDK 版本高于 JDK 8,所以在设置中使用了 将 Build Tools -> Gradle -> Gradle JDK 版本设置为 17。在开启 kapt 插件后,compileOptions 与 kotlinOptions 内对 JDK 的设置也要保持一致,否则会报出注释上的错误
- 依赖部分要引入 activity-ktx,因为它提供了扩展方法 viewModels 可以用于便捷初始化 ViewModel 对象
然后是 AndroidManifest 配置网络相关的权限:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"><uses-permission android:name="android.permission.INTERNET" /><!-- 如果网络请求没用到 HTTP 协议,则不用配置 networkSecurityConfig --><applicationandroid:networkSecurityConfig="@xml/network_security_config"</application>
</manifest>
network_security_config.xml 是一个允许 HTTP 的配置:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config><base-config cleartextTrafficPermitted="true" />
</network-security-config>
最后要配置 gradle.properties:
# Enables the AndroidX Jetifier, which replaces usages of support library classes
# 允许第三方库使用 AndroidX,否则编译不通过
android.enableJetifier=true
实际上没配这个属性项目也能正常运行,原因是该配置设为 true 会开启 Jetifier 工具,该工具用于将 Android Support Library 依赖到 AndroidX 的工具,它可以确保第三方库和自身项目中的所有依赖都使用 AndroidX 包。如果你的项目都直接用的 AndroidX 而没有 Android Support Library,那就不用进行上述配置。
2.2 项目框架搭建
主要是跑通 Retrofit 网络请求。
课程使用的是讲师搭建的本地服务器返回电影列表信息,由于我们用不了该服务器,所以使用 WanAndroid 的【项目列表数据】代替。之所以使用该接口,是因为它的返回内容是有图片的,与课程中展示的效果相近。该接口的规格如下:
某一个分类下项目列表数据,分页展示
https://www.wanandroid.com/project/list/1/json?cid=294方法:GET 参数:cid 分类的id,上面项目分类接口页码:拼接在链接中,从1开始。
注:该接口支持传入 page_size 控制分页数量,取值为[1-40],不传则使用默认值,一旦传入了 page_size,后续该接口分页都需要带上,否则会造成分页读取错误。
可以直接访问:https://www.wanandroid.com/project/list/1/json?cid=294
我们对该接口进行了测试,发现一些情况,做如下说明:
- cid 参数并不起作用,即便不传也返回同样的数据,所以后续我们创建 Retrofit 接口方法时不会传这个参数
- 页码就是地址中在 list 之后的数字,页码从 1 开始,而不是 0
- 每一页的大小 page_size 是以参数形式拼接在地址后面的,比如想指定一页有 10 条数据,那么就请求 https://www.wanandroid.com/project/list/1/json?page_size=10
接下来我们让分页数量为 1,看一下 JSON 的数据结构:
{"data": {"curPage": 1,"datas": [{"adminAdd": false,"apkLink": "","audit": 1,"author": "sskEvan","canEdit": false,"chapterId": 294,"chapterName": "完整项目","collect": false,"courseId": 13,"desc": "joke_fun_flutter仿写自段子乐app,项目整体基于GetX实现路由跳转、依赖注入、状态管理。网络请求基于Dio+Retrofit。已实现以下功能:段子推荐列表(纯文字、多图片、视频)、段子发布、发现(仿抖音划一划功能)、搜索、评论(支持楼中楼)、登陆、个人详情、资料编辑、乐豆、关注、主题色切换...","descMd": "","envelopePic": "https://www.wanandroid.com/blogimgs/2f859d26-e80a-4f08-a62a-f1c8236333cf.png","fresh": false,"host": "","id": 27962,"isAdminAdd": false,"link": "https://www.wanandroid.com/blog/show/3619","niceDate": "2024-01-29 22:12","niceShareDate": "2024-01-29 22:12","origin": "","prefix": "","projectLink": "https://github.com/sskEvan/joke_fun_flutter","publishTime": 1706537538000,"realSuperChapterId": 293,"selfVisible": 0,"shareDate": 1706537538000,"shareUser": "","superChapterId": 294,"superChapterName": "开源项目主Tab","tags": [{"name": "项目","url": "/project/list/1?cid=294"}],"title": "flutter仿段子乐app","type": 0,"userId": -1,"visible": 1,"zan": 0}],"offset": 0,"over": false,"pageCount": 289,"size": 1,"total": 289},"errorCode": 0,"errorMsg": ""
}
根据该结构,可以创建项目的一系列 Model 类(不要说成实体 Entity,因为实体是数据库概念,一般为数据库使用所创建的 Data Class 或者 JavaBean 才称为实体):
data class Projects(val `data`: ProjectData,val errorCode: Int,val errorMsg: String
)data class ProjectData(val curPage: Int,@SerializedName("datas")val projectList: List<Project>,val offset: Int,val over: Boolean,val pageCount: Int,val size: Int,val total: Int
)data class Project(val adminAdd: Boolean,val apkLink: String,val audit: Int,val author: String,val canEdit: Boolean,val chapterId: Int,val chapterName: String,val collect: Boolean,val courseId: Int,val desc: String,val descMd: String,val envelopePic: String,val fresh: Boolean,val host: String,val id: Int,val isAdminAdd: Boolean,val link: String,val niceDate: String,val niceShareDate: String,val origin: String,val prefix: String,val projectLink: String,val publishTime: Long,val realSuperChapterId: Int,val selfVisible: Int,val shareDate: Long,val shareUser: String,val superChapterId: Int,val superChapterName: String,val tags: List<Tag>,val title: String,val type: Int,val userId: Int,val visible: Int,val zan: Int
)data class Tag(val name: String,val url: String
)
其中 ProjectData 类对应的是 JSON 的 “data”,其子属性 “datas” 表示项目列表数据,由于在代码使用 datas 含义并不明确,因此在代码中将 “datas” 重命名为 projectList,但是为了不影响反序列化,需要将其在 JSON 中的字段名 “datas” 通过 @SerializedName 注解标明。
然后就是 Retrofit 的接口方法,选择页号也页大小作为参数:
interface ProjectApi {/*** 虽然 WanAndroid API 文档上给出的查询项目列表的地址是:* https://www.wanandroid.com/project/list/1/json?cid=294* 但是测试时发现,cid 并不起作用,即使不写也返回同样的数据,因此发送* 请求时就不带 cid 了,只使用页号 page_number 和每页大小 page_size*/@GET("project/list/{page_number}/json")suspend fun getProjects(@Path("page_number") page: Int,@Query("page_size") page_size: Int): Projects
}
最后创建 Retrofit 对象并提供创建 API 接口对象的方法:
object RetrofitClient {private val TAG = this::class.java.simpleNameprivate val instance: Retrofit by lazy {val httpLoggingInterceptor = HttpLoggingInterceptor {Log.d(TAG, it)}.also { interceptor -> interceptor.level = HttpLoggingInterceptor.Level.BODY }Retrofit.Builder().baseUrl("https://www.wanandroid.com/").addConverterFactory(GsonConverterFactory.create()).client(OkHttpClient.Builder().addInterceptor(httpLoggingInterceptor).build()).build()}fun <T> createApi(clazz: Class<T>): T {return instance.create(clazz) as T}
}
2.3 Paging 框架配置
先搭建一个 Paging 相关的代码框架,框架搭好后再慢慢实现功能。
Repository 部分搭建
(P107)PagingSource 与 Pager 配置
结合第 1 节的 Paging 框架图来看,先来搭建 Repository 部分,创建 PagingSource 的子类 ProjectPagingSource:
class ProjectPagingSource : PagingSource<Int, Project>() {// 在此函数中实现分页加载逻辑,并返回 LoadResult 这个结果override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Project> {val currentPage = 1val pageSize = 8val projects =RetrofitClient.createApi(ProjectApi::class.java).getProjects(currentPage, pageSize)// todo 返回一个 LoadResult}
}
实现抽象类 PagingSource 需要实现抽象方法 load,它就是用于触发异步数据加载的(比如从数据库或网络加载数据),它需要返回一个 LoadResult 作为加载的结果,实际上就指明是加载成功了还是发生错误了,如果成功则返回 LoadResult.Page,Page 内会带有请求的数据;如果失败则返回 LoadResult.Error,Error 内包含 Throwable 对象用于描述导致错误的异常:
sealed class LoadResult<Key : Any, Value : Any> {data class Error<Key : Any, Value : Any>(val throwable: Throwable) : LoadResult<Key, Value>()data class Page<Key : Any, Value : Any> constructor(/*** Loaded data*/val data: List<Value>,/*** [Key] for previous page if more data can be loaded in that direction, `null`* otherwise.*/val prevKey: Key?,/*** [Key] for next page if more data can be loaded in that direction, `null` otherwise.*/val nextKey: Key?,/*** Optional count of items before the loaded data.*/@IntRange(from = COUNT_UNDEFINED.toLong())val itemsBefore: Int = COUNT_UNDEFINED,/*** Optional count of items after the loaded data.*/@IntRange(from = COUNT_UNDEFINED.toLong())val itemsAfter: Int = COUNT_UNDEFINED) : LoadResult<Key, Value>() {...}
看到这里让我想起前面在系列的第 6 篇文章的第 2 节,讲解使用 Flow 下载文件的例子时,使用 DownloadStatus 这个密封类定义下载时的几种情况:
// 密封类的所有成员都是其子类
sealed class DownloadStatus {object None : DownloadStatus()data class Progress(val value: Int) : DownloadStatus()data class Error(val throwable: Throwable) : DownloadStatus()data class Done(val file: File) : DownloadStatus()
}
通过 Progress 拿到下载进度 value,Error 拿到 throwable 这个异常,Done 拿到下载好的文件。这与现在的 LoadResult 的设计思想是异曲同工的。
再回到 Paging 的框架搭建,目前在 load 中我们没有返回 LoadResult 会导致编译报错。由于我们处于框架搭建阶段,因此先不涉及 LoadResult 的具体生成,暂时先放在这,继续下一步,在 ViewModel 中创建一个 Pager 进行相关配置。
其实你仔细看,架构图中 Repository 内还有一个 RemoteMediator,它是用来从远程网络下载数据然后保存到数据库中用的。本篇作为 Paging 的入门介绍为了简化脉络没有使用 RemoteMediator,在下一篇项目实战中会使用到它。
ViewModel 部分搭建
class ProjectViewModel : ViewModel() {fun loadProject(): Flow<PagingData<Project>> {return Pager(config = PagingConfig(pageSize = 8),pagingSourceFactory = { ProjectPagingSource() }).flow}
}
Pager 的构造函数指定了 PagingConfig,每页数据为 8 个,然后还指定了 PagingSource 工厂函数,需要返回一个 PagingSource 对象,这里我们返回上一步配置的 ProjectPagingSource 的构造函数用以创建 ProjectPagingSource 对象。
Pager 配置好了,最后返回的是该 Pager 对象的 flow 属性,该属性是 Pager 的构造函数内定义的属性:
// Pager.kt
class Pager<Key : Any, Value : Any>
@JvmOverloads constructor(config: PagingConfig,initialKey: Key? = null,@OptIn(ExperimentalPagingApi::class)remoteMediator: RemoteMediator<Key, Value>? = null,pagingSourceFactory: () -> PagingSource<Key, Value>
) {/*** A cold [Flow] of [PagingData], which emits new instances of [PagingData] once they become* invalidated by [PagingSource.invalidate] or calls to [AsyncPagingDataDiffer.refresh] or* [PagingDataAdapter.refresh].*/val flow: Flow<PagingData<Value>> = PageFetcher(pagingSourceFactory,initialKey,config,remoteMediator).flow
}
而 PageFetcher.flow 实际上是 PageFetcher 内的一个成员属性,通过 channelFlow 函数构造了一个 Flow 对象,具体源码先不继续追了,知道是返回了一个 Flow 就好。
UI 部分搭建
Paging 在这一部分的主要组件是 PagingDataAdapter,也就是为 RecyclerView 提供适配器。
先看这个 PagingDataAdapter 如何实现,观察源码发现至少需要传一个参数 diffCallback 给构造函数:
abstract class PagingDataAdapter<T : Any, VH : RecyclerView.ViewHolder> @JvmOverloads constructor(diffCallback: DiffUtil.ItemCallback<T>, // 比较 RecyclerView 中两个 Item 是否相同的回调mainDispatcher: CoroutineDispatcher = Dispatchers.Main, // 主调度器,用于更新 UIworkerDispatcher: CoroutineDispatcher = Dispatchers.Default // 任务调度器用于在后台执行耗时任务
) : RecyclerView.Adapter<VH>() {private val differ = AsyncPagingDataDiffer(diffCallback = diffCallback,updateCallback = AdapterListUpdateCallback(this),mainDispatcher = mainDispatcher,workerDispatcher = workerDispatcher)
mainDispatcher 与 workerDispatcher 分别用于在主线程和子线程中执行任务,使用默认值即可。因此继承 PagingDataAdapter 时需要给构造函数传入 diffCallback 用于区分 RecyclerView 中的两个 Item 是否相同,如果相同的话可以不进行刷新,这样可以避免多余的绘制,节省资源以优化性能:
class ProjectAdapter(private val context: Context) :PagingDataAdapter<Project, ProjectViewHolder>(object : DiffUtil.ItemCallback<Project>() {/*** DiffUtil.ItemCallback 用于判断是否是同一个 item,如是,则不再重新绘制以避免不必要的绘制,* 从而提升性能。* areItemsTheSame 通过 Int 型的 id 判断是否是同一个 item* areContentsTheSame 通过 == 判断两个对象的内容是否相等,Kotlin 的 == 相当于 Java 的* equals(),而 === 才相当于 Java 的 ==*/override fun areItemsTheSame(oldItem: Project, newItem: Project) = oldItem.id == newItem.idoverride fun areContentsTheSame(oldItem: Project, newItem: Project) = oldItem == newItem}) {override fun onBindViewHolder(holder: ProjectViewHolder, position: Int) {val project = getItem(position)project?.let {val binding = holder.binding as ItemProjectBindingbinding.project = itbinding.networkImage = it.envelopePic}}// 使用 ViewBinding 后,ViewHolder 就是一个空壳了:// class ProjectViewHolder(val binding: ViewBinding) : RecyclerView.ViewHolder(binding.root)override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProjectViewHolder {val binding = ItemProjectBinding.inflate(LayoutInflater.from(context), parent, false)return ProjectViewHolder(binding)}
}
ItemProjectBinding 由 RecyclerView Item 的布局而来,item_project 使用了 DataBinding:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"><data><variablename="networkImage"type="String" /><variablename="project"type="com.coroutine.flow.paging3.model.Project" /></data><androidx.constraintlayout.widget.ConstraintLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:paddingVertical="10dp"><ImageViewandroid:id="@+id/imageView"android:layout_width="100dp"android:layout_height="100dp"app:image="@{networkImage}"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toStartOf="@id/guideline2"app:layout_constraintHorizontal_bias="0.432"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent"app:layout_constraintVertical_bias="0.054"tools:srcCompat="@tools:sample/avatars" /><TextViewandroid:id="@+id/tv_title"android:layout_width="190dp"android:layout_height="wrap_content"android:ellipsize="end"android:maxLines="2"android:text="@{project.title}"android:textSize="16sp"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintStart_toStartOf="@id/guideline"app:layout_constraintTop_toTopOf="parent"app:layout_constraintVertical_bias="0.255"app:layout_constraintWidth_max="wrap"tools:text="泰坦尼克号泰坦尼克号泰坦尼克号" /><!-- 文章发布时间,对应课程的电影评分 --><TextViewandroid:id="@+id/tv_zan"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="24dp"android:text="@{project.niceDate}"android:textSize="16sp"app:layout_constraintStart_toStartOf="@id/guideline"app:layout_constraintTop_toBottomOf="@id/tv_title"tools:text="2018-01-28 22:27" /><androidx.constraintlayout.widget.Guidelineandroid:id="@+id/guideline2"android:layout_width="wrap_content"android:layout_height="wrap_content"android:orientation="vertical"app:layout_constraintGuide_percent="0.4" /><androidx.constraintlayout.widget.Guidelineandroid:id="@+id/guideline"android:layout_width="wrap_content"android:layout_height="wrap_content"android:orientation="vertical"app:layout_constraintGuide_percent="0.5" /></androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Item 需要展示项目图片、项目名称与发布时间,其中后两个数据可以通过 project 直接获取,而图片显示虽然通过 networkImage 变量传来了图片地址,但是无法通过布局设置直接显示,需要自定义一个 DataBinding 用于显示图片的适配器 ImageViewBindingAdapter:
/*** 自定义适配器帮助 MVVM + DataBinding 的 ImageView 加载图片*/
class ImageViewBindingAdapter {companion object {/*** @JvmStatic:在使用 Kotlin 编写 BindingAdapter 时,通常推荐将其声明为静态函数。具体做法是* 在 companion object 中声明一个 @JvmStatic 方法,这样其就会编译为 Java 的静态函数** @BindingAdapter:在 Kotlin 中如果想要使用 data binding 的注解,需要在 module 的 build.gradle* 中添加 kapt 插件;* 通过该注解绑定括号内传入的属性后,在解析属性的时候,会自动调用该方法** 方法参数说明:* 使用 BindingAdapter 的函数一般需要两个参数,第一个参数是绑定的 View,第二个参数是绑定的属性值。* 在这个例子中,我们绑定的是 ImageView 下的 app:image 属性,因此第一个参数是 ImageView,第二个* 是 app:image 的属性值,也就是项目的图片地址*/@JvmStatic@BindingAdapter("image")fun setImage(imageView: ImageView, url: String) {if (!TextUtils.isEmpty(url)) {Picasso.get().load(url).placeholder(R.drawable.ic_launcher_background).into(imageView)} else {imageView.setBackgroundColor(Color.GRAY)}}}
}
代码执行顺序大概是在解析布局 ImageView 时,当解析到自定义的 app:image 属性时,就会执行与该 ImageView 的该属性绑定的方法:
- 比如 setImage 方法被 @BindingAdapter 标记,该注解的值为 image 表示将方法与 app:image 属性绑定
- 方法参数,第一个是绑定的组件,这里就是 ImageView;第二个是绑定的属性值,这里就是图片地址,也就是布局中的 networkImage
- 由于推荐这种绑定方法是一个 Java 静态方法,所以要在 companion object 内使用 @JvmStatic 标记该方法
最后在 MainActivity 中为 RecyclerView 设置 ProjectAdapter,并通过 ViewModel 执行 loadProject() 得到获取项目数据的 Flow 对象,收集数据后提交给 ProjectAdapter 即可:
class MainActivity : AppCompatActivity() {private val viewModel by viewModels<ProjectViewModel>()private val mBinding: ActivityMainBinding by lazy {ActivityMainBinding.inflate(layoutInflater)}override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(mBinding.root)val projectAdapter = ProjectAdapter(this)mBinding.apply {recyclerView.adapter = projectAdapter}lifecycleScope.launch {lifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) {// 由于可能会刷新多次,因此只使用最后一次刷新出来的值viewModel.loadProject().collectLatest {projectAdapter.submitData(it)}}}}
}
至此,第 1 节中展示的 Paging3 框架就算基本搭建完毕了。
3、功能实现
上一节搭建 Paging 框架时,PagingSource 的加载逻辑我们还没有实现,先来处理它。
3.1 实现分页逻辑
(P109)PagingSource 分页逻辑的实现,要完善 PagingSource 的 load():
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Project> {// API 从第 1 页开始,而不是第 0 页val currentPage = params.key ?: 1// 分页大小val pageSize = params.loadSizeval projects =RetrofitClient.createApi(ProjectApi::class.java).getProjects(currentPage, pageSize)// 当前页面的前一页:如果当前是第一页,那么前面没有就是 null,否则就是 currentPage - 1var prevKey = if (currentPage == 1) null else currentPage - 1// 当前页面的后一页:如果当前是最后一页,那么后面没有就是 null,否则就是 currentPage + 1var nextKey = if (!projects.data.over) currentPage + 1 else nullLog.d("Frank", "currentPage = $currentPage, pageSize = $pageSize, prevKey = $prevKey, nextKey = $nextKey")return LoadResult.Page(data = projects.data.projectList,prevKey = prevKey,nextKey = nextKey)}
实际上就是添加了当前页面的前后页码 prevKey 和 nextKey 的计算,在返回 LoadResult.Page 对象时需要传入当前页的数据以及前后页的页码。
这样项目数据就能呈现在页面上了:
3.2 分页数据混乱问题
(P110)仔细观察上面的效果图能发现,从第 9 个项目到第 24 个项目在第 25 到第 40 个项目重复出现了:
![]() | ![]() |
发生问题的原因是我们在配置 Paging 时将页大小设置为 8:
class ProjectViewModel : ViewModel() {fun loadProject(): Flow<PagingData<Project>> {return Pager(config = PagingConfig(pageSize = 8),pagingSourceFactory = { ProjectPagingSource() }).flow}
}
但是观察 load 内 log 的输出:
com.coroutine.flow.paging3 D currentPage = 1, pageSize = 24, prevKey = null, nextKey = 2
com.coroutine.flow.paging3 D currentPage = 2, pageSize = 8, prevKey = 1, nextKey = 3
com.coroutine.flow.paging3 D currentPage = 3, pageSize = 8, prevKey = 2, nextKey = 4
发现第一次加载时 pageSize = 24,是我们设置的 pageSize 的 3 倍,第二次加载时 pageSize 才变成我们设置的 8,并且是在 currentPage = 2 的情况下加载 8 个,也就是说页面展示的第 25 ~ 32 个 Item 的数据实际上是第 9 ~ 16 个,Log 与实际展示的页面是能对上的。
实际上,造成这种情况的根本原因是在加载第 1 页时,nextKey 的计算出现了问题。因为 currentPage = 1, pageSize = 24,相当于在第 1 页一次性加载了 3 页的数据,那么下一次加载的 nextKey 应该是 1 + 3 = 4 才对。
我们查看源码能看到 PagingConfig 内确实通过 initialLoadSize 控制着首次加载的尺寸为 pageSize 的 3 倍:
class PagingConfig @JvmOverloads constructor(@JvmField@IntRange(from = 1)val initialLoadSize: Int = pageSize * DEFAULT_INITIAL_PAGE_MULTIPLIER,
) {companion object {/*** When [maxSize] is set to [MAX_SIZE_UNBOUNDED], the maximum number of items loaded is* unbounded, and pages will never be dropped.*/@Suppress("MinMaxConstant")const val MAX_SIZE_UNBOUNDED = Int.MAX_VALUEinternal const val DEFAULT_INITIAL_PAGE_MULTIPLIER = 3}
}
结合前面的原因分析,解决方法可以有两种。最简单的方法就是在配置 Pager 时,让 initialLoadSize 刚好就是 pageSize:
class ProjectViewModel : ViewModel() {fun loadProject(): Flow<PagingData<Project>> {return Pager(config = PagingConfig(pageSize = 8, initialLoadSize = 8),pagingSourceFactory = { ProjectPagingSource() }).flow}
}
但是倘若想让首次加载的数据多一些,假如初始加载 2 页数据:
class ProjectViewModel : ViewModel() {fun loadProject(): Flow<PagingData<Project>> {return Pager(config = PagingConfig(pageSize = Constants.PAGING_PAGE_SIZE, // 8initialLoadSize = Constants.PAGING_INITIAL_LOAD_SIZE // 16),pagingSourceFactory = { ProjectPagingSource() }).flow}
}
然后在加载第 1 页时对 nextKey 做一个特殊处理:
class ProjectPagingSource : PagingSource<Int, Project>() {override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Project> {// 从第 1 页开始,而不是第 0 页val currentPage = params.key ?: 1// 分页大小,第 1 页是 PagingConfig.initialLoadSize,第 2 页开始是 PagingConfig.pageSizeval pageSize = params.loadSizeval projects =RetrofitClient.createApi(ProjectApi::class.java).getProjects(currentPage, pageSize)val prevKey: Int?val nextKey: Int?// 如果当前是第一页,那么向前一次加载的页号没有就是 null,向后加载一次应该增加首次加载的页数if (currentPage == 1) {prevKey = nullnextKey = currentPage + Constants.PAGING_INITIAL_LOAD_SIZE / Constants.PAGING_PAGE_SIZE} else {// 非首页的前一次加载的页号是当前页号减 1,向后加载,如果不是最后一页的话应该是// 当前页号加 1,否则为空prevKey = currentPage - 1nextKey = if (!projects.data.over) currentPage + 1 else null}Log.d("Frank","currentPage = $currentPage, pageSize = $pageSize, prevKey = $prevKey, nextKey = $nextKey")return try {LoadResult.Page(data = projects.data.projectList,prevKey = prevKey,nextKey = nextKey)} catch (e: Exception) {e.printStackTrace()return LoadResult.Error(e)}}
}
这样再看 UI 页面和 Log 就正常了:
com.coroutine.flow.paging3 D currentPage = 1, pageSize = 16, prevKey = null, nextKey = 3
com.coroutine.flow.paging3 D currentPage = 3, pageSize = 8, prevKey = 2, nextKey = 4
com.coroutine.flow.paging3 D currentPage = 4, pageSize = 8, prevKey = 3, nextKey = 5
com.coroutine.flow.paging3 D currentPage = 5, pageSize = 8, prevKey = 4, nextKey = 6
编码时需要注意一点,load() 的 LoadParams.loadSize 是本次加载实际加载的数据量,它并不是我们设置的页长 8,而是第一次加载的是 initialLoadSize = 16,第二次加载时才是 pageSize = 8。
3.3 上滑刷新与下拉加载
(P111)通过 LoadStateFooter 实现上滑刷新,需要给 RecyclerView 的 PagingDataAdapter 指定 LoadStateFooter:
class MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(mBinding.root)val projectAdapter = ProjectAdapter(this)mBinding.apply {// 通过 withLoadStateFooter 添加上滑加载更多的 FooterrecyclerView.adapter =projectAdapter.withLoadStateFooter(ProjectLoadMoreAdapter(this@MainActivity))}lifecycleScope.launch {lifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) {// 由于可能会刷新多次,因此只使用最后一次刷新出来的值viewModel.loadProject().collectLatest {projectAdapter.submitData(it)}}}}
}
ProjectLoadMoreAdapter 需要继承 LoadStateAdapter,返回 Footer 布局对应的 ViewHolder:
class ProjectLoadMoreAdapter(private val context: Context) : LoadStateAdapter<ProjectViewHolder>() {override fun onBindViewHolder(holder: ProjectViewHolder, loadState: LoadState) {}override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): ProjectViewHolder {val binding = ProjectLoadmoreBinding.inflate(LayoutInflater.from(context), parent, false)return ProjectViewHolder(binding)}
}
ProjectLoadmoreBinding 来自于布局文件 project_loadmore:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"android:layout_width="match_parent"android:layout_height="50dp"android:padding="10dp"><ProgressBarandroid:id="@+id/progressBar"android:layout_width="20dp"android:layout_height="20dp"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toStartOf="@id/tv_loading" /><TextViewandroid:id="@+id/tv_loading"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="正在加载数据"android:textSize="18sp"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
为了能看清 Footer 的状态,在加载数据时延迟 2s,否则数据加载太快看不清现象:
class ProjectPagingSource : PagingSource<Int, Project>() {override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Project> {// 为了能看清上滑刷新加载效果,延迟 2 秒delay(2000)...}
}
(P112)下拉刷新需要在布局中将 RecyclerView 放到 SwipeRefreshLayout 中:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".activity.MainActivity"><androidx.swiperefreshlayout.widget.SwipeRefreshLayoutandroid:id="@+id/swipeRefreshLayout"android:layout_width="match_parent"android:layout_height="match_parent"><!-- 注意 height 被换成 wrap_content 了 --><androidx.recyclerview.widget.RecyclerViewandroid:id="@+id/recyclerView"android:layout_width="match_parent"android:layout_height="wrap_content"app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /></androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
然后在 MainActivity 中给 SwipeRefreshLayout 设置刷新的监听,并且在刷新完成后要为其更新 isRefreshing 的值以隐藏刷新进度条:
override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(mBinding.root)val projectAdapter = ProjectAdapter(this)mBinding.apply {recyclerView.adapter =projectAdapter.withLoadStateFooter(ProjectLoadMoreAdapter(this@MainActivity))// 设置 SwipeRefreshLayout 的刷新监听,触发 ProjectAdapter 的刷新swipeRefreshLayout.setOnRefreshListener {projectAdapter.refresh()}}lifecycleScope.launch {lifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) {viewModel.loadProject().collectLatest {projectAdapter.submitData(it)}}}lifecycleScope.launch {lifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) {// 通过 ProjectAdapter 的 loadStateFlow 获取到 Adapter 的刷新状态流projectAdapter.loadStateFlow.collectLatest { state ->mBinding.swipeRefreshLayout.isRefreshing = state.refresh is LoadState.Loading}}}}
ProjectAdapter 的 loadStateFlow 会返回该 Adapter 的刷新状态流:
@OptIn(FlowPreview::class)val loadStateFlow: Flow<CombinedLoadStates> = differ.loadStateFlow
收集这个流的最新值,可以获取到 LoadState:
sealed class LoadState(val endOfPaginationReached: Boolean
) {class NotLoading(endOfPaginationReached: Boolean) : LoadState(endOfPaginationReached)object Loading : LoadState(false)class Error(val error: Throwable) : LoadState(false)
}
密封类的三个子类表示三种加载状态,如果不是 Loading 表名不在加载状态,那么就可以将状态同步给 SwipeRefreshLayout 的 isRefreshing 属性,告诉布局加载已经完成,不用再显示加载进度条了。
一个比较完备的效果展示图:
3.4 其他问题
(P113)下拉刷新隐藏 Bug,需要先对 Paging 进行再一次的配置:
class ProjectViewModel : ViewModel() {fun loadProject(): Flow<PagingData<Project>> {return Pager(config = PagingConfig(pageSize = Constants.PAGING_PAGE_SIZE,initialLoadSize = Constants.PAGING_INITIAL_LOAD_SIZE,prefetchDistance = 1),pagingSourceFactory = { ProjectPagingSource() }).flow}
}
将 prefetchDistance 设置为 1,同时将 initialLoadSize 调小至与 pageSize 相同:
class Constants {companion object {const val PAGING_INITIAL_LOAD_SIZE = 8const val PAGING_PAGE_SIZE = 8}
}
运行应用,可以看到上滑是正常的,可以加载更多数据。但是在进行一次下拉刷新后,再向上滑,滑到第一页数据结束就划不动了:
想要解决这个 Bug,要么将 prefetchDistance 设置大一点,设置成 2 就可以了;或者将 initialLoadSize 设置的大一点,比如原来使用的 16。实际上看一下这两个属性的注释也能了解到相关信息:
/*** Prefetch distance定义了从加载内容边缘多远的访问将触发进一步加载。通常应设置为屏幕上可见项目数* 量的数倍。* 例如,如果此值设置为50,PagingData将尝试提前加载已经访问的数据之外50个项目。* 数值为0表示直到明确请求时才会加载列表项。一般不建议这样做,以免用户在滚动时看到占位符* 项目(带有占位符)或列表末尾(不带占位符)。*/@JvmField@IntRange(from = 0)val prefetchDistance: Int = pageSize,/*** 定义了从 PagingSource 进行初始加载的请求加载大小,通常比 pageSize 大,因此在首次加载数据时,* 加载的内容范围足够大,可以覆盖小幅滚动。*/@JvmField@IntRange(from = 1)val initialLoadSize: Int = pageSize * DEFAULT_INITIAL_PAGE_MULTIPLIER,
initialLoadSize 的注释其实暗示了,其值足够大才能够覆盖小幅滚动避免 Footer 滑动不出来的 Bug。
(P114)分页数据 cacheIn
旋转屏幕,你会发现 RecyclerView 的内容会重新通过网络加载(旋转时屏幕内容会消失一会儿再重新出现,并且出现后是新加载的状态),这说明 ViewModel 没起作用。查看代码发现确实如此,通过 Flow 请求的数据没有被保存:
class ProjectViewModel : ViewModel() {fun loadProject(): Flow<PagingData<Project>> {return Pager(config = PagingConfig(pageSize = Constants.PAGING_PAGE_SIZE,initialLoadSize = Constants.PAGING_INITIAL_LOAD_SIZE,// 至少是 2 避免 Foot 刷不出来的 BugprefetchDistance = 2),pagingSourceFactory = { ProjectPagingSource() }).flow}
}
尝试将流保存到变量中:
class ProjectViewModel : ViewModel() {private val projects by lazy {Pager(config = PagingConfig(pageSize = Constants.PAGING_PAGE_SIZE,initialLoadSize = Constants.PAGING_INITIAL_LOAD_SIZE,// 至少是 2 避免 Foot 刷不出来的 BugprefetchDistance = 2),pagingSourceFactory = { ProjectPagingSource() }).flow}fun loadProject(): Flow<PagingData<Project>> = projects
}
发现仍然不行,需要再进一步改造,在 flow 后接一个 cachedIn:
class ProjectViewModel : ViewModel() {private val projects by lazy {Pager(config = PagingConfig(pageSize = Constants.PAGING_PAGE_SIZE,initialLoadSize = Constants.PAGING_INITIAL_LOAD_SIZE,// 至少是 2 避免 Foot 刷不出来的 BugprefetchDistance = 2),pagingSourceFactory = { ProjectPagingSource() }).flow.cachedIn(viewModelScope)}fun loadProject(): Flow<PagingData<Project>> = projects
}
cachedIn 方法内容如下:
/**
* 将 PagingData 缓存,以便从此流中的任何下游集合都共享相同的 PagingData。
* 只要给定的作用域处于活动状态,该流就会保持活动状态。为避免泄漏,请确保使用已经受管的
* 作用域(如 ViewModel 作用域)或在不再需要分页时手动取消它。
* 这种缓存的常见用例是在 ViewModel 中缓存 PagingData。这可以确保在配置更改(例如旋转)时,
* 新的 Activity 将立即接收现有数据,而不是从头开始获取数据。
* 请注意,这不会将 Flow<PagingData> 转换为热流。除非被收集,它不会执行任何不必要的代码。
* 参数:
* scope - 此页面缓存将保持活动状态的协程作用域。
*/
@CheckResult
fun <T : Any> Flow<PagingData<T>>.cachedIn(scope: CoroutineScope
) = cachedIn(scope, null)
注意 cachedIn 的参数要传一个合适的 CoroutineScope,这里我们是在 ViewModel 中缓存数据,因此使用的是 viewModelScope。
相关文章:
Kotlin 协程基础知识总结七 —— Flow 与 Jetpack Paging3
专题分为五大块: Paging3 的结构组成Flow 与 Paging3下拉刷新上拉刷新离奇 Bug上游数据缓存 Demo 会还原开发迭代的过程,不会直接一步到位。 1、Paging3 加载数据流程 (P105)Paging3 的简介详情可参考官方文档 Paging 库概览&…...
使用JMeter玩转tidb压测
作者: du拉松 原文来源: https://tidb.net/blog/3f1ada39 一、前言 tidb是mysql协议的,所以在使用过程中使用tidb的相关工具连接即可。因为jmeter是java开发的相关工具,直接使用mysql的jdbc驱动包即可。 二、linux下安装jmet…...
音视频入门基础:MPEG2-PS专题(3)——MPEG2-PS格式简介
一、引言 本文对MPEG2-PS格式进行简介。 进行简介之前,请各位先下载MPEG2-PS的官方文档。ITU-T和ISO/IEC都分别提供MPEG2-PS的官方文档。但是ITU提供的文档是免费的,ISO/IEC是付费的,所以我们主要阅读ITU提供的官方文档,比如较新…...
ETCD渗透利用指南
目录 未指定使用put操作报错 未指定操作版本使用get报错 首先etcd分为两个版本v2和v3,不同的API结果无论是访问URL还是使用etcdctl进行通信,都会导致问题,例如使用etcdctl和v3进行通信,如果没有实名ETCDCTL_API3指定API版本会直接…...
Python安装(新手详细版)
前言 第一次接触Python,可能是爬虫或者是信息AI开发的小朋友,都说Python 语言简单,那么多学一些总是有好处的,下面从一个完全不懂的Python 的小白来安装Python 等一系列工作的记录,并且遇到的问题也会写出,…...
SQL 中复杂 CASE WHEN 嵌套逻辑优化
目标:优化复杂的 CASE WHEN 逻辑,提升 SQL 语句的可读性与执行效率,减少多层嵌套带来的复杂性。 1. CASE WHEN 的常见问题 嵌套过深:多个条件判断嵌套,难以阅读和维护。重复逻辑:相似逻辑在多个分支中重复…...
【专题】2024年出口跨境电商促销趋势白皮书报告汇总PDF洞察(附原数据表)
原文链接:https://tecdat.cn/?p38722 在当今全球化加速演进、数字经济蓬勃发展的大背景下,跨境电商行业正以前所未有的态势重塑国际贸易格局,成为各方瞩目的焦点领域。 根据亚马逊发布的《2024年出口跨境电商促销趋势白皮书》,…...
C# 设计模式(结构型模式):代理模式
C# 设计模式(结构型模式):代理模式 在软件开发中,有时我们需要通过某种方式间接地访问一个对象,这时就可以使用代理模式(Proxy Pattern)。代理模式通过引入一个代理对象来控制对目标对象的访问…...
单片机复位电路基本理解教程文章·含上拉电阻理解电容开路理解!!!
目录 常见复位电路种类 复位电路电阻上拉理解 电容储能断路理解 编写不易,仅供学习,请勿搬运,感谢理解 常见元器件驱动电路文章专栏连接 LM7805系列降压芯片驱动电路降压芯片驱动电路详解-…...
深入浅出:事件监听中的适配器模式
1. 为什么需要适配器模式? 在Java的事件监听器设计中,许多接口有多个抽象方法。例如,MouseListener 接口有 5 个方法,KeyListener 接口有 3 个方法。如果我们只关心其中的一个方法(例如,鼠标点击事件&…...
常用LabVIEW算法及应用
在LabVIEW项目中,算法的应用是提高系统性能、实现特定功能、完成复杂任务的核心。LabVIEW作为一种图形化编程语言,允许用户通过直观的图形编程来实现各种复杂的算法。这些算法广泛应用于控制系统、数据采集、信号处理、图像处理、机器学习等领域。了解常…...
VTK知识学习(28)-区域提取
1、感兴趣区域(Volume ofInterest,VOI) 它是图像内部的一块子区域。在VTK中,vtkExtractVOI 类可根据用户指定的区域范围提取子图像。该Filter 的输入和输出都是一个vtkImageData,因此其结果可以直接作为图像保存。 代码: private void Test…...
基于Spring Boot + Vue3实现的在线汽车保养维修预约管理系统源码+文档
前言 基于Spring Boot Vue3实现的在线汽车保养维修预约管理系统是一种前后端分离架构的应用,它结合了Java后端开发框架Spring Boot和现代JavaScript前端框架Vue.js 3.0的优势。这样的系统可以为汽车服务站提供一个高效的平台来管理客户的预约请求 技术选型 系统…...
CAN201 Introduction to Networking(计算机网络)Pt.4 链路层
文章目录 5. Link Layer(链路层)5.1 Services of link layer(链路层的服务)5.2 Error detection and correction(错误检测和纠正)5.2.1 Partity Checks(奇偶检验)5.2.2 Checksum&…...
Python视频处理:噪声矩阵与并行计算的完美融合
噪声级别对视频质量有显著的影响,主要体现在以下几个方面: 1. 视觉质量 低噪声级别:当噪声级别较低时,视频的视觉质量较好。噪声对图像细节的干扰较小,画面看起来较为清晰和自然。观众可以更容易地识别图像中的细节和…...
wordpress开发之实现使用第三方库qrcode-generator生成二维码并上传和展示
文章目录 一、需求二、技术实现 - 利用qrcode-generator库三、代码实现 一、需求 客户的需求是能将特定的url生成二维码,以便将二维码分享或贴到合同纸上给他的客户扫描查看信息。 这个url包含的内容类似于如下格式: https://www.example.com/contrac…...
计算机网络——物理层
一、通信基础 1.相关术语: • 数据(data)——运送消息的实体。 • 信号(signal)——数据的电气的或电磁的表现。 • “模拟的”(analogous)——代表消息的参数的取值是连续的。 • “数字的”(digital)——代表消息的参数的取值是离散的。 • 码元(code)——在…...
网络IP协议
IP(Internet Protocol,网际协议)是TCP/IP协议族中重要的协议,主要负责将数据包发送给目标主机。IP相当于OSI(图1)的第三层网络层。网络层的主要作用是失陷终端节点之间的通信。这种终端节点之间的通信也叫点…...
Unity UGUI使用技巧与经验总结(不定期更新)
Text自动缩放参考连接: Unity -UGUI中Text文本框的自动调整,字体大小的自适应调节_unity添加的字体大小锁定-CSDN博客 Toggle按钮选择时,显示对应的UI界面: 为Toggle组件的On Value Change事件添加对需要显示的对象的SetActive…...
Tailwind CSS 使用简介
参考网站安装 - Tailwind CSS 中文网 号称是开始使用 Tailwind CSS 通过 npm 安装 tailwindcss,并创建你的 tailwind.config.js 文件。 npm install -D tailwindcss npx tailwindcss init 在 tailwind.config.js 文件中添加所有模板文件的路径。 /** type {im…...
嵌入式linux中socket控制与实现
一、概述 1、首先网络,一看到这个词,我们就会想到IP地址和端口号,那IP地址和端口各有什么作用呢? (1)IP地址如身份证一样,是标识的电脑的,一台电脑只有一个IP地址。 (2)端口提供了一种访问通道,服务器一般都是通过知名端口号来识别某个服务。例如,对于每个TCP/IP实…...
Go语言的 的数据封装(Data Encapsulation)核心知识
Go语言的数据封装(Data Encapsulation)核心知识 引言 在现代编程语言中,数据封装是一个重要的编程概念。它不仅帮助开发者管理复杂性,还提高了代码的可维护性和安全性。Go语言(Golang)作为一种注重简洁性…...
25/1/5 算法笔记<强化学习> MPC,交叉熵法,PETS算法
MPC 一个棋手下棋,会根据当前的局势来推演落子几步可能发生的局势,然后选择局势最好的一种情况来决定当前落子位置。 模型预测控制方法MPC,就是这样一种迭代的、基于模型的控制方法。值得注意的是MPC中不存在一个显示的策略。具体而言就是MPC在每次采取…...
最新版Chrome浏览器加载ActiveX控件之CFCA安全输入控件
背景 CFCA安全输入控件用于保证用户在浏览器、桌面客户端、移动客户端中输入信息的安全性,防止运行在用户系统上的病毒、木马等恶意程序入侵窃取用户输入的敏感信息。确保用户输入、本地缓存、网络传输整个流程中,输入的敏感信息不被窃取。广泛应用于银行…...
vue 项目集成 electron 和 electron 打包及环境配置
vue electron 开发桌面端应用 安装 electron npm i electron -D记得加上-D,electron 需添加到devDependencies,如果添加到dependencies后面运行可能会报错 根目录创建electron文件夹,在electron文件夹创建main.js(或者backgrou…...
计算机网络--UDP和TCP课后习题
【5-05】 试举例说明有些应用程序愿意采用不可靠的UDP, 而不愿意采用可靠的TCP。 解答: 这可能有以下几种情况。 首先,在互联网上传输实时数据的分组时,有可能会出现差错甚至丢失。如果利用 TCP 协议对这些出错或丢失的分组进行重传&…...
【算法不挂科】算法期末考试题库(带解析)【选择题53道&填空题36道&算法填空题7道&问答题33道】
前言 大家好吖,欢迎来到 YY 滴算法不挂科系列 ,热烈欢迎! 本章主要内容面向接触过C的老铁 下面是相关传送门 【算法不挂科】算法期末考试题库1(带解析)【选择题53道&填空题36道&算法填空题7道&a…...
Java+maven+selenium3+testng 自动化测试环境IDEA
软件测试资料领取:[内部资源] 想拿年薪40W的软件测试人员,这份资料必须领取~ 软件测试面试刷题工具领取:软件测试面试刷题【800道面试题答案免费刷】 idea 、java环境变量jdk maven安装及环境变量配置这里就不多说了,网上有很多…...
【踩坑指南2.0 2025最新】Scala中如何在命令行传入参数以运行主函数
这个地方基本没有任何文档记录,在学习的过程中屡屡碰壁,因此记录一下这部分的内容,懒得看可以直接跳到总结看结论。 踩坑步骤 首先来看看书上让我们怎么写: //main.scala object Start {def main(args:Array[String]) {try {v…...
vue3-watchEffect异步依赖收集
当 b 更新时 a 并不会更新,因为watchEffect的依赖收集在该案例中停止于await asyncFn(),也就是只会收集同步代码的依赖,await 之后的异步代码的依赖并不会收集到 <template> <div>a: {{ a }} <br>b: {{ b }} <br>&l…...
【Go研究】Go语言脚本化的可行性——yaegi项目体验
0x01 背景——云计算中脚本化困境 作为云基础设施管理中,大量需要跟文件系统、容器等相关的操作,这些操作实现通常用脚本来实现。 现在探讨下,这些脚本为什么一定要用脚本语言来实现,以及目前实现中的常见的问题。 常见的两个场…...
Genome Research | 俄亥俄州立于忠堂组-结合深度学习与蛋白质数据库系统探究反刍动物真核微生物...
结合深度学习与蛋白质数据库系统探究反刍动物真核微生物 Probing the eukaryotic microbes of ruminants with a deep-learning classifier and comprehensive protein databases 期刊:Genome Research DOI:https://doi.org/10.1101/gr.279825.124 第一作…...
centos7yum安装mysql5.7
1、安装mysql5.7 (1) 正常安装 [rootBrianZhu /]# wget -i -c http://dev.mysql.com/get/mysql57-community-release-el7-10.noarch.rpm [rootBrianZhu /]# yum -y install mysql57-community-release-el7-10.noarch.rpm [rootBrianZhu /]# yum -y install mysql-community-se…...
JavaScript系列(8)-- Array高级操作
JavaScript Array高级操作 📚 在前七篇文章中,我们探讨了JavaScript的语言特性、ECMAScript标准、引擎工作原理、数值类型、字符串处理、Symbol类型和Object高级特性。今天,让我们深入了解JavaScript中的Array高级操作。数组是最常用的数据结…...
蓝牙架构介绍
架构1:hostcontroller双芯片标准架构 这个标准把蓝牙协议栈分成host和controller两部分,其中host跑在AP上,controller跑在蓝牙模块上,两者之间通过HCI协议进行通信,AP芯片厂商一般会直接采用开源的Bluez来实现Host功能…...
青少年编程与数学 02-006 前端开发框架VUE 08课题、列表渲染
青少年编程与数学 02-006 前端开发框架VUE 08课题、列表渲染 一、列表渲染v-for 指令:key 属性遍历对象响应式更新列表渲染的作用 二、应用示例项目结构public/index.htmlsrc/components/TodoApp.vuesrc/main.jspackage.json构建和运行项目 课题摘要:本文介绍了Vue.js中的列表渲…...
12.3【hardware][day3]
关于使用硬件 DSP 资源实现乘法的含义 在 Xilinx 7 Series FPGA(现场可编程门阵列)中,乘法运算可以通过专门的数字信号处理(DSP)硬件资源来完成。当使用 Verilog 语言编写代码进行乘法运算时,直接使用乘号&…...
降维算法之PCA(PrincipalComponent Analysis,主成分分析)
降维是指在保留数据特征的前提下,以少量的变量表示有许多变量的数据,这有助于降低多变量数据分析的复杂度。比如在分析有 100 个变量的数据时,与其直接分析数据,不如使用 5 个变量表示数据,这样可以使后续分析比较容易…...
【JVM】总结篇-类的加载篇之 类的加载器 和ClassLoader分析
文章目录 类的加载器ClassLoader自定义类加载器双亲委派机制概念源码分析优势劣势如何打破Tomcat 沙箱安全机制JDK9 双亲委派机制变化 类的加载器 获得当前类的ClassLoader clazz.getClassLoader() 获得当前线程上下文的ClassLoader Thread.currentThread().getContextClassLoa…...
Android:文件管理:打开文件意图
三步走: 一、先在AndroidManifest.xml声明provider: <providerandroid:name"androidx.core.content.FileProvider"android:authorities"${applicationId}.FileProvider"android:exported"false"android:grantUriPermi…...
《计算机网络A》单选题(详解)
《计算机网络A》单选题-复习题库 1、计算机网络最突出的优点是( D ) A、存储容量大 B、将计算机技术与通信技术相结合 C、集中计算 D、资源共享 解析:算机网络最突出的优点是 D、资源共享。通过计算机网络&…...
【SpringBoot3】Spring Boot 3.0 集成 Mybatis Plus
在Spring Boot 3.0中,你可以使用MyBatis Plus来简化数据库操作。以下是一个基本的集成示例: 1.添加依赖到你的pom.xml: <dependencies> <!-- Spring Boot Starter --> <dependency> <groupId>org.springframework.…...
第147场双周赛:子字符串匹配模式、设计任务管理器、最长相邻绝对差递减子序列、删除所有值为某个元素后的最大子数组和
Q1、子字符串匹配模式 1、题目描述 给你一个字符串 s 和一个模式字符串 p ,其中 p 恰好 包含 一个 * 符号。 p 中的 * 符号可以被替换为零个或多个字符组成的任意字符序列。 如果 p 可以变成 s 的子字符串,那么返回 true ,否则返回 false…...
数据结构C语言描述9(图文结合)--二叉树和特殊书的概念,二叉树“最傻瓜式创建”与前中后序的“递归”与“非递归遍历”
前言 这个专栏将会用纯C实现常用的数据结构和简单的算法;有C基础即可跟着学习,代码均可运行;准备考研的也可跟着写,个人感觉,如果时间充裕,手写一遍比看书、刷题管用很多,这也是本人采用纯C语言…...
开源存储详解-分布式存储与ceph
ceph体系结构 rados:reliable, autonomous, distributed object storage, rados rados采用c开发 对象存储 ceph严格意义讲只提供对象存储能力,ceph的块存储能力实际是基于对象存储库librados的rbd 对象存储特点 对象存储采用put/get/delete…...
Vue 快速入门:开启前端新征程
在当今的 Web 开发领域,Vue.js 作为一款极具人气的 JavaScript 前端框架,正被广泛应用于各类项目之中。它以简洁的语法、高效的数据绑定机制以及强大的组件化开发模式,为开发者们带来了前所未有的开发体验。如果你渴望踏入前端开发的精彩世界…...
GPT系统重大升级,开创国内先河:o1支持图片识别功能正式上线
文章目录 零、前言一、授权码登录体验优化:一步直达聊天界面二、全新“项目”功能:让工作更有条理三、语音功能升级:全新交互体验四、o1支持图片识别五、总结 零、前言 我是虚竹哥,目标是带十万人玩转ChatGPT。 亲爱的用户&…...
常用的数据结构API概览
List ArrayList 1、在初始化一个ArrayList的时候,如果我想同时set一些值 比如存放int[ ] List<int[]> list new ArrayList(Arrays.asList(new int[]{intervals[0][0],intervals[0][1]}));//或者int[] temp new int[]{intervals[0][0],intervals[0][1]}…...
《探秘计算机视觉与深度学习:开启智能视觉新时代》
《探秘计算机视觉与深度学习:开启智能视觉新时代》 一、追溯起源:从萌芽到崭露头角二、核心技术:解锁智能视觉的密码(一)卷积神经网络(CNN):图像识别的利器(二࿰…...
Linux:操作系统不朽的传说
操作系统是计算机的灵魂,它掌控着计算机的硬件和软件资源,为用户和应用程序提供了一个稳定、高效、安全的运行环境。 在众多操作系统中,Linux 的地位举足轻重。它被广泛应用于服务器、云计算、物联网、嵌入式设备等领域。Linux 的成功离不开…...