当前位置: 首页 > news >正文

用一个RecyclerView实现二级评论

先上个效果图(没有UI,将就看吧),写代码的整个过程花了4个小时左右,相比当初自己开发需求已经快了很多了哈。

给产品估个两天时间,摸一天半的鱼不过分吧(手动斜眼)

需求拆分

这种大家常用的评论功能其实也就没啥好拆分的了,简单列一下:

  • 默认展示一级评论和二级评论中的热评,可以上拉加载更多。
  • 二级评论超过两条时,可以点击展开加载更多二级评论,展开后可以点击收起折叠到初始状态。
  • 回复评论后插入到该评论的下方。

技术选型

前面我在给掘友的评论中,也提到了技术选型的要点:

单RecyclerView + 多ItemType + ListAdapter

这是基本的UI框架。

为啥要只用一个RecyclerView?最重要的原因,就是在RecyclerView中嵌套同方向RecyclerView,会有性能问题和滑动冲突。其次,当下声明式UI正是各方大佬推崇的最佳开发实践之一,虽然我们没有使用声明式UI基础上开发的Compose/Flutter技术,但其构建思想仍然对我们的开发具有一定的指导意义。我猜测,androidx.recyclerview.widget.ListAdapter可能也是响应声明式UI号召的一个针对RecyclerView的解决方案吧。

数据源的转换

数据驱动UI

既然选用了ListAdapter,那么我们就不应该再手动操作adapter的数据,再用各种notifyXxx方法来更新列表了。更提倡的做法是,基于data class的**浅拷贝

**,用Collection操作符对数据源的进行转换,然后将转换后的数据提交到adapter。为了提高数据转换性能,我们可以基于协程进行异步处理。

要点::

  • 浅拷贝

低成本生成一个全新的对象,以保证数据源的安全性。

data class Foo(val id: Int, val content: String)val foo1 = Foo(0, "content")
val foo2 = foo1.copy(content = "updated content")
  • Collection操作符

Kotlin中提供了大量非常好用的Collection操作符,能灵活使用的话,非常有利于咱们向声明式UI转型。

前面我提到了groupByflatMap这两个操作符。怎么使用呢?

以这个需求为例,我们需要显示一级评论、二级评论和展开更多按钮,想要分别用一个data class来表示,但是后端返回的数据中又没有“展开更多”这样的数据,就可以这样处理:

// 从后端获取的数据List,包括有一级评论和二级评论,二级评论的parentId就等于一级评论的id
val loaded: List<CommentItem> = ... 
val grouped = loaded.groupBy { // (1) 以一级评论的id为key,把源list分组为一个Map<Int, List<CommentItem>>(it as? CommentItem.Level1)?.id ?: (it as? CommentItem.Level2)?.parentId?: throw IllegalArgumentException("invalid comment item")
}.flatMap { // (2) 展开前面的map,展开时就可以在每级一级评论的二级评论后面添加一个控制“展开更多”的Itemit.value + CommentItem.Folding(parentId = it.key,)
}
  • 异步处理

前面我们描述的数据源的转换过程,在Kotlin中,可以简单地被抽象为一个操作:

List<CommentItem>.() -> List<CommentItem>

对于这个需求,数据源转换操作就包括了:分页加载,展开二级评论,收起二级评论,回复评论等。按照惯例,抽象一个接口出来。既然我们要在协程框架下进行异步处理,需要给这个操作加一个suspend关键字。

interface Reducer {val reduce: suspend List<CommentItem>.() -> List<CommentItem>
}

为啥我给这个接口取名Reducer?如果你知道它的意思,说明你可能已经了解过MVI架构了;如果你还不知道它的意思,说明你可以去了解一下MVI了。哈哈!

不过今天不谈MVI,对于这样一个小Demo,完全没必要上架构。但是,优秀架构为我们提供的代码构建思路是有必要的!

这个Reducer,在这里就算是咱们的小小业务架构了。

  • 异步2.0

前面谈到异步,我们印象中可能主要是网络请求、数据库/文件读写等IO操作。

这里我想要延伸一下。

ActivitystartActivityForResult/onActivityResultDialog的拉起/回调,其实也可以看着是异步操作。异步与是否在主线程无关,而在于是否是实时返回结果。毕竟在主线程上跳转到其他页面,获取数据再回调回去使用,也是花了时间的啊。所以在协程的框架下,有一个更适合描述异步的词语:挂起(suspend)

说这有啥用呢?仍以这个需求为例,我们点击“回复”后拉起一个对话框,输入评论确认后回调给Activity,再进行网络请求:

class ReplyDialog(context: Context, private val callback: (String) -> Unit) : Dialog(context) {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.dialog_reply)val editText = findViewById<EditText>(R.id.content)findViewById<Button>(R.id.submit).setOnClickListener {if (editText.text.toString().isBlank()) {Toast.makeText(context, "评论不能为空", Toast.LENGTH_SHORT).show()return@setOnClickListener}callback.invoke(editText.text.toString())dismiss()}}
}suspend List<CommentItem>.() -> List<CommentItem> = {val content = withContext(Dispatchers.Main) {// 由于整个转换过程是在IO线程进行,Dialog相关操作需要转换到主线程操作suspendCoroutine { continuation ->ReplyDialog(context) {continuation.resume(it)}.show()}}...进行其他操作,如网络请求
}

技术选型,或者说技术框架,咱们就实现了,甚至还谈到了部分细节了。接下来进行完整实现细节分享。

实现细节

MainActivity

基于上一章节的技术选型,咱们的MainActivity的完整代码就是这样了。

class MainActivity : AppCompatActivity() {private lateinit var commentAdapter: CommentAdapteroverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)commentAdapter = CommentAdapter {lifecycleScope.launchWhenResumed {val newList = withContext(Dispatchers.IO) {reduce.invoke(commentAdapter.currentList)}val firstSubmit = commentAdapter.itemCount == 1commentAdapter.submitList(newList) {// 这里是为了处理submitList后,列表滑动位置不对的问题if (firstSubmit) {recyclerView.scrollToPosition(0)} else if (this@CommentAdapter is FoldReducer) {val index = commentAdapter.currentList.indexOf(this@CommentAdapter.folding)recyclerView.scrollToPosition(index)}}}}recyclerView.adapter = commentAdapter}
}

RecyclerView设置一个CommentAdapter就行了,回调时也只需要把回调过来的Reducer调度到IO线程跑一下,得到新的数据listsubmitList就完事了。如果不是submitList后有列表的定位问题,代码还能更精简。如果有知道更好的解决办法的朋友,麻烦留言分享一下,感谢!

CommentAdapter

别以为我把逻辑处理扔到adapter中了哦!

AdapterViewHolder都是UI组件,我们也需要尽量保持它们的清洁。

贴一下CommentAdapter

class CommentAdapter(private val reduceBlock: Reducer.() -> Unit) :ListAdapter<CommentItem, VH>(object : DiffUtil.ItemCallback<CommentItem>() {override fun areItemsTheSame(oldItem: CommentItem, newItem: CommentItem): Boolean {return oldItem.id == newItem.id}override fun areContentsTheSame(oldItem: CommentItem, newItem: CommentItem): Boolean {if (oldItem::class.java != newItem::class.java) return falsereturn (oldItem as? CommentItem.Level1) == (newItem as? CommentItem.Level1)|| (oldItem as? CommentItem.Level2) == (newItem as? CommentItem.Level2)|| (oldItem as? CommentItem.Folding) == (newItem as? CommentItem.Folding)|| (oldItem as? CommentItem.Loading) == (newItem as? CommentItem.Loading)}}) {init {submitList(listOf(CommentItem.Loading(page = 0, CommentItem.Loading.State.IDLE)))}override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {val inflater = LayoutInflater.from(parent.context)return when (viewType) {TYPE_LEVEL1 -> Level1VH(inflater.inflate(R.layout.item_comment_level_1, parent, false),reduceBlock)TYPE_LEVEL2 -> Level2VH(inflater.inflate(R.layout.item_comment_level_2, parent, false),reduceBlock)TYPE_LOADING -> LoadingVH(inflater.inflate(R.layout.item_comment_loading,parent,false), reduceBlock)else -> FoldingVH(inflater.inflate(R.layout.item_comment_folding, parent, false),reduceBlock)}}override fun onBindViewHolder(holder: VH, position: Int) {holder.onBind(getItem(position))}override fun getItemViewType(position: Int): Int {return when (getItem(position)) {is CommentItem.Level1 -> TYPE_LEVEL1is CommentItem.Level2 -> TYPE_LEVEL2is CommentItem.Loading -> TYPE_LOADINGelse -> TYPE_FOLDING}}companion object {private const val TYPE_LEVEL1 = 0private const val TYPE_LEVEL2 = 1private const val TYPE_FOLDING = 2private const val TYPE_LOADING = 3}
}

可以看到,就是一个简单的多ItemTypeAdapter,唯一需要注意的就是,在Activity里传入的reduceBlock: Reducer.() -> Unit,也要传给每个ViewHolder

ViewHolder

篇幅原因,就只贴其中一个:

abstract class VH(itemView: View, protected val reduceBlock: Reducer.() -> Unit) :ViewHolder(itemView) {abstract fun onBind(item: CommentItem)
}class Level1VH(itemView: View, reduceBlock: Reducer.() -> Unit) : VH(itemView, reduceBlock) {private val avatar: TextView = itemView.findViewById(R.id.avatar)private val username: TextView = itemView.findViewById(R.id.username)private val content: TextView = itemView.findViewById(R.id.content)private val reply: TextView = itemView.findViewById(R.id.reply)override fun onBind(item: CommentItem) {avatar.text = item.userName.subSequence(0, 1)username.text = item.userNamecontent.text = item.contentreply.setOnClickListener {reduceBlock.invoke(ReplyReducer(item, itemView.context))}}
}

也是很简单,唯一特别一点的处理,就是在onClickListener中,让reduceBlockinvoke一个Reducer实现。

Reducer

刚才在技术选型章节,已经提前展示了“回复评论”这一操作的Reducer实现了,其他Reducer也差不多,比如展开评论操作,也封装在一个Reducer实现ExpandReducer中,以下是完整代码:

data class ExpandReducer(val folding: CommentItem.Folding,
) : Reducer {private val mapper by lazy { Entity2ItemMapper() }override val reduce: suspend List<CommentItem>.() -> List<CommentItem> = {val foldingIndex = indexOf(folding)val loaded =FakeApi.getLevel2Comments(folding.parentId, folding.page, folding.pageSize).getOrNull()?.map(mapper::invoke) ?: emptyList()toMutableList().apply {addAll(foldingIndex, loaded)}.map {if (it is CommentItem.Folding && it == folding) {val state =if (it.page > 5) CommentItem.Folding.State.LOADED_ALL else CommentItem.Folding.State.IDLEit.copy(page = it.page + 1, state = state)} else {it}}}}

短短一段代码,我们做了这些事:

  • 请求网络数据Entity list(假数据)
  • 通过mapper转换成显示用的Item数据list
  • Item数据插入到“展开更多”按钮前面
  • 最后,根据二级评论加载是否完成,将“展开更多”的状态置为IDLELOADED_ALL

一个字:丝滑!

用于转换EntityItemmapper的代码也贴一下吧:

// 抽象
typealias Mapper<I, O> = (I) -> O
// 实现
class Entity2ItemMapper : Mapper<ICommentEntity, CommentItem> {override fun invoke(entity: ICommentEntity): CommentItem {return when (entity) {is CommentLevel1 -> {CommentItem.Level1(id = entity.id,content = entity.content,userId = entity.userId,userName = entity.userName,level2Count = entity.level2Count,)}is CommentLevel2 -> {CommentItem.Level2(id = entity.id,content = if (entity.hot) entity.content.makeHot() else entity.content,userId = entity.userId,userName = entity.userName,parentId = entity.parentId,)}else -> {throw IllegalArgumentException("not implemented entity: $entity")}}}
}

细心的朋友可以看到,在这里我顺便也将热评也处理了:

if (entity.hot) entity.content.makeHot() else entity.content

makeHot()就是用buildSpannedString来实现的:

fun CharSequence.makeHot(): CharSequence {return buildSpannedString {color(Color.RED) {append("热评  ")}append(this@makeHot)}
}

这里可以提一句:尽量用CharSequence来抽象表示字符串,可以方便我们灵活地使用Span来减少UI代码。

data class

也贴一下相关的数据实体得了。

  • 网络数据(假数据)
interface ICommentEntity {val id: Intval content: CharSequenceval userId: Intval userName: CharSequence
}data class CommentLevel1(override val id: Int,override val content: CharSequence,override val userId: Int,override val userName: CharSequence,val level2Count: Int,
) : ICommentEntity
  • RecyclerView Item数据
sealed interface CommentItem {val id: Intval content: CharSequenceval userId: Intval userName: CharSequencedata class Loading(val page: Int = 0,val state: State = State.LOADING) : CommentItem {override val id: Int=0override val content: CharSequenceget() = when(state) {State.LOADED_ALL -> "全部加载"else -> "加载中..."}override val userId: Int=0override val userName: CharSequence=""enum class State {IDLE, LOADING, LOADED_ALL}}data class Level1(override val id: Int,override val content: CharSequence,override val userId: Int,override val userName: CharSequence,val level2Count: Int,) : CommentItemdata class Level2(override val id: Int,override val content: CharSequence,override val userId: Int,override val userName: CharSequence,val parentId: Int,) : CommentItemdata class Folding(val parentId: Int,val page: Int = 1,val pageSize: Int = 3,val state: State = State.IDLE) : CommentItem {override val id: Intget() = hashCode()override val content: CharSequenceget() = when  {page <= 1 -> "展开20条回复"page >= 5 -> ""else -> "展开更多"}override val userId: Int = 0override val userName: CharSequence = ""enum class State {IDLE, LOADING, LOADED_ALL}}
}

这部分没啥好说的,可以注意两个点:

  • data class也是可以抽象的。但这边我处理不是很严谨,比如CommentItem我把userIduserName也抽象出来了,其实不应该抽象出来。
  • 在基于Reducer的框架下,最好是把data class的属性都定义为val

总结一下实现心得:

  • 数据驱动UI
  • 对业务的精准抽象
  • 对异步的延伸理解
  • 灵活使用Collection操作符
  • 没有UI和PM,写代码真TM爽!

Android 学习笔录

Android 性能优化篇:https://qr18.cn/FVlo89
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集:https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap

相关文章:

用一个RecyclerView实现二级评论

先上个效果图&#xff08;没有UI&#xff0c;将就看吧&#xff09;&#xff0c;写代码的整个过程花了4个小时左右&#xff0c;相比当初自己开发需求已经快了很多了哈。 给产品估个两天时间&#xff0c;摸一天半的鱼不过分吧&#xff08;手动斜眼&#xff09; 需求拆分 这种大家…...

音视频 SDL简介

一、SDL简介 SDL&#xff08;Simple DirectMedia Layer&#xff09;是一套开放源代码的跨平台多媒体开发库&#xff0c;使用C语言写成。SDL提供了数种控制图像、声音、输出入的函数&#xff0c;让开发者只要用相同或是相似的代码就可以开发出跨多个平台&#xff08;Linux、Win…...

7.前端·新建子模块与开发(自动生成)

文章目录 学习地址视频笔记自动代码生成模式开发增删改查功能调试功能权限分配 脚本实现权限分配 学习地址 https://www.bilibili.com/video/BV13g411Y7GS/?p15&spm_id_frompageDriver&vd_sourceed09a620bf87401694f763818a31c91e 视频笔记 自动代码生成模式开发 …...

Linux 创建目录

语法&#xff1a;mkdir xxx Linux路径 在当前目录下创建文件夹 在/目录下创建文件夹 如果想要一次性创建多个层级的目录&#xff0c;如下图 会报错&#xff0c;因为上级目录test并不存在&#xff0c;所以无法创建test目录 可以通过-p选项&#xff0c;将一整个链条都创建完成…...

【DIY小记】修复Win10启动出现蓝屏0xc0000185错误的一些方法

近些日子想到自己尘封已久的笔记本电脑没有开机了&#xff0c;很多软件驱动之类的没有更新&#xff0c;就打算把电脑开起来做一轮批量升级。但开电脑的时候很久没有进入Win10桌面&#xff0c;等了很长一段时间蓝屏提示0xc0000185错误&#xff0c;说系统需要恢复。经历了一番折腾…...

Linux 下的 10 个 PDF 软件

本文[1]是我们正在进行的有关 Linux 顶级工具系列的延续&#xff0c;在本系列中&#xff0c;我们将向您介绍最著名的 Linux 系统开源工具。 随着互联网上越来越多地使用可移植文档格式 (PDF) 文件来获取在线书籍和其他相关文档&#xff0c;拥有 PDF 查看器/阅读器对于桌面 Linu…...

浅谈redis分布式锁

浅谈redis分布式锁 分布式锁介绍 分布式锁&#xff0c;顾名思义&#xff0c;分布式系统中的锁&#xff0c;当多个进程不在同一个系统中时&#xff0c;用分布式锁控制各个进程对共享资源的访问&#xff0c;通过互斥来保持一致性。 使用场景&#xff1a;电商中某商品的秒杀活动…...

【Python保姆级教程】List容器

文章目录 前言一、列表是什么二、列表的定义2.1 有初始值2.2 空列表使用方括号创建空列表使用list()函数创建空列表 三、list列表常用操作3.1 添加元素3.2 删除元素3.3 修改元素3.4 列表长度 四、遍历操作4.1 使用for循环4.2 使用while循环和索引 总结 前言 Python是一种广泛使…...

微服务保护-授权规则

个人名片&#xff1a; 博主&#xff1a;酒徒ᝰ. 个人简介&#xff1a;沉醉在酒中&#xff0c;借着一股酒劲&#xff0c;去拼搏一个未来。 本篇励志&#xff1a;三人行&#xff0c;必有我师焉。 本项目基于B站黑马程序员Java《SpringCloud微服务技术栈》&#xff0c;SpringCloud…...

v-if失效原因

一般v-if失效都是和绑定变量有关&#xff0c;我所知道的一般有两种 1.绑定的变量为String类型或者其他类型 就是返回的变量类型与所需要的布尔类型不匹配。 <template><div><div id"container" ref"container" v-iftype></div>&l…...

Chrome 基于 Wappalyzer 查看网站所用的前端技术栈

1. 找到谷歌商店 https://chrome.google.com/webstore/search/wappalyzer?utm_sourceext_app_menu 2. 搜索 Wappalyzer 3. 添加至Chrome 4. 使用 插件 比如打开 https://www.bilibili.com/ 就可以看到其所以用的前端技术栈了...

python的装饰器

作用:在不改变原来函数的代码情况下,进行修改,或者增加函数的功能装饰器本质上就是一个闭包雏形:def wrapper(fn): wrapper: 装饰器 , fn: 目标函数def inner():# 在目标函数执行前的一些动作fn()# 在目标函数执行后的一些动作return inner #千万别加(),这里是返回一…...

P2P协议的传输艺术

TP 采用两个 TCP 连接来传输一个文件。 控制连接&#xff1a;服务器以被动的方式&#xff0c;打开众所周知用于 FTP 的端口 21&#xff0c;客户端则主动发起连接。该连接将命令从客户端传给服务器&#xff0c;并传回服务器的应答。常用的命令有&#xff1a;list——获取文件目…...

辅助驾驶功能开发-功能规范篇(21)-4-XP行泊一体方案功能规范

XPilot Parking 自动泊车系统 • 超级自动泊车辅助(Super AutoParking Assist)、语音控制泊车辅助(Autoparking with Speech) - 产品定义 超级自动泊车辅助是⼀个增强的自动泊车辅助系统。在超级自动泊车辅助系统中,识别车位将会变得实时可见, 并且不可泊入的⻋位也将…...

家政服务小程序上门服务小程序预约上门服务维修保洁上门服务在线派单技师入口

套餐一:源码=1500元 套餐二:全包服务 包服务器+域名+认证小程序+搭建+售后=2000元 主要功能: 1、服务商入驻 支持个人或企业入驻成为平台服务商; 2、发布商品 入驻服务商后,可以发布服务商品,用户可以在线下单,预约服务; 3、发布需求 用户可以发布一口价或竞价需求…...

LeetCode精选100题-【3数之和】-2

这里写自定义目录标题 解法1:解法2: 给你一个整数数组 nums &#xff0c;判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i ! j、i ! k 且 j ! k &#xff0c;同时还满足 nums[i] nums[j] nums[k] 0 。请你返回所有和为 0 且不重复的三元组。注意&#xff1a;答案中不…...

springboot集成mybatis-plus

一、在spring boot中配置mybatis-plus 1、创建一个spring boot项目&#xff0c;注意勾选mysql 2、在pom.xml文件中添加mybatis-plus的依赖包 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0&qu…...

再想一想GPT

一 前言 花了大概两天时间看完《这就是ChatGPT》&#xff0c;触动还是挺大的&#xff0c;让我静下来&#xff0c;认真地想一想&#xff0c;是否真正理解了ChatGPT&#xff0c;又能给我们以什么样的启发。 二 思考 在工作和生活中&#xff0c;使用ChatGPT或文心一言&#xff0c;…...

Blazor前后端框架Known-V1.2.15

V1.2.15 Known是基于C#和Blazor开发的前后端分离快速开发框架&#xff0c;开箱即用&#xff0c;跨平台&#xff0c;一处代码&#xff0c;多处运行。 Gitee&#xff1a; https://gitee.com/known/KnownGithub&#xff1a;https://github.com/known/Known 概述 基于C#和Blazo…...

Tomcat 的部署和优化

目录 1、什么是Tomcat 1.1、静态页面的选择 2、Tomcat是怎么运行的 3、安装jdk &#xff06; 部署jdk环境 & Tomcat 安装 1、安装jdk 2、配置jdk环境变量 3、tomcat安装 4、Tomcat启动 5.优化tomcat启动速度 6.Tomcat的主要命令 7.Tomcat 配置虚拟主机 8.Tomca…...

CTF show Web 红包题第六弹

提示 1.不是SQL注入 2.需要找关键源码 思路 进入页面发现是一个登录框&#xff0c;很难让人不联想到SQL注入&#xff0c;但提示都说了不是SQL注入&#xff0c;所以就不往这方面想了 ​ 先查看一下网页源码&#xff0c;发现一段JavaScript代码&#xff0c;有一个关键类ctfs…...

三维GIS开发cesium智慧地铁教程(5)Cesium相机控制

一、环境搭建 <script src"../cesium1.99/Build/Cesium/Cesium.js"></script> <link rel"stylesheet" href"../cesium1.99/Build/Cesium/Widgets/widgets.css"> 关键配置点&#xff1a; 路径验证&#xff1a;确保相对路径.…...

FastAPI 教程:从入门到实践

FastAPI 是一个现代、快速&#xff08;高性能&#xff09;的 Web 框架&#xff0c;用于构建 API&#xff0c;支持 Python 3.6。它基于标准 Python 类型提示&#xff0c;易于学习且功能强大。以下是一个完整的 FastAPI 入门教程&#xff0c;涵盖从环境搭建到创建并运行一个简单的…...

django filter 统计数量 按属性去重

在Django中&#xff0c;如果你想要根据某个属性对查询集进行去重并统计数量&#xff0c;你可以使用values()方法配合annotate()方法来实现。这里有两种常见的方法来完成这个需求&#xff1a; 方法1&#xff1a;使用annotate()和Count 假设你有一个模型Item&#xff0c;并且你想…...

质量体系的重要

质量体系是为确保产品、服务或过程质量满足规定要求&#xff0c;由相互关联的要素构成的有机整体。其核心内容可归纳为以下五个方面&#xff1a; &#x1f3db;️ 一、组织架构与职责 质量体系明确组织内各部门、岗位的职责与权限&#xff0c;形成层级清晰的管理网络&#xf…...

相机从app启动流程

一、流程框架图 二、具体流程分析 1、得到cameralist和对应的静态信息 目录如下: 重点代码分析: 启动相机前,先要通过getCameraIdList获取camera的个数以及id,然后可以通过getCameraCharacteristics获取对应id camera的capabilities(静态信息)进行一些openCamera前的…...

在鸿蒙HarmonyOS 5中使用DevEco Studio实现录音机应用

1. 项目配置与权限设置 1.1 配置module.json5 {"module": {"requestPermissions": [{"name": "ohos.permission.MICROPHONE","reason": "录音需要麦克风权限"},{"name": "ohos.permission.WRITE…...

tree 树组件大数据卡顿问题优化

问题背景 项目中有用到树组件用来做文件目录&#xff0c;但是由于这个树组件的节点越来越多&#xff0c;导致页面在滚动这个树组件的时候浏览器就很容易卡死。这种问题基本上都是因为dom节点太多&#xff0c;导致的浏览器卡顿&#xff0c;这里很明显就需要用到虚拟列表的技术&…...

有限自动机到正规文法转换器v1.0

1 项目简介 这是一个功能强大的有限自动机&#xff08;Finite Automaton, FA&#xff09;到正规文法&#xff08;Regular Grammar&#xff09;转换器&#xff0c;它配备了一个直观且完整的图形用户界面&#xff0c;使用户能够轻松地进行操作和观察。该程序基于编译原理中的经典…...

MySQL 8.0 事务全面讲解

以下是一个结合两次回答的 MySQL 8.0 事务全面讲解&#xff0c;涵盖了事务的核心概念、操作示例、失败回滚、隔离级别、事务性 DDL 和 XA 事务等内容&#xff0c;并修正了查看隔离级别的命令。 MySQL 8.0 事务全面讲解 一、事务的核心概念&#xff08;ACID&#xff09; 事务是…...