挑战ArkUI复刻手机备忘录(Rdb数据库)

​​想了解更多关于开源的内容,请访问:​​

​​ 开源基础软件社区​​

​​https://ost.​​

前言

学习了关系型数据库和一些相关的codelabs后,为了更深入地了解ArkUI关系型数据库的使用和操作,我决定复刻一个小小的手机备忘录。整个实现过程不仅有对关系型数据库接口的尝试封装,还碰了各种UI实现、路由跳转的壁,印象很深,所以就想分享一下这次复刻实现的过程,总结一下经验。由于写的代码量较大,下面主要说一下与数据库通信的环节和路由通信的环节,UI布局放在上传的zip里。

部分效果展示

这是主界面(是不是有点像:-):

由于界面功能较多,更多的gif在后面功能拆分环节再来展示

代码实现流程

代码结构:

实现思路

首先把繁多的rdb数据库接口封装成几个功能较完备的大接口,在UI界面进行调用和数据处理、数据通信。

一、关系型数据库的封装

官方对它的宣传就是:不用学会sql也能用(确实对,但还是要有些数据库基础才能处理结果集resultSet和使用executeSql接口)。

导入包:

import data_rdb from ‘@ohos.data.rdb’。

创建数据库:

data_rdb.getRdbStore返回一个数据库管理对象。

参数名

类型

说明

context

Context

应用的上下文。

config

StoreConfig

与此RDB存储相关的数据库配置。

version

number

数据库版本。

众所周知,数据库有增删改查四大操作,则它也提供了几个常用接口。

api

data_rdb.insert

data_rdb.query

data_rdb.delete

data_rdb.update

executeSql

其中,插入操作只需一个用于存储键值对的valueBucket就够了,而删除、更新和查询都要借助RdbPredicates谓词来获取数据索引才能进一步操作:

RdbPredicates

表示关系型数据库(RDB)的谓词。该类确定RDB中条件表达式的值是true还是false。

let predicates = new data_rdb.RdbPredicates(“EMPLOYEE”)//例如创建一个为”EMPLOYEE”表服务的predicates实例。

还有更多的跟分布式有关的接口:

setDistributedTables

obtainDistributedTableName

sync

还需要简单了解一下结果集的使用。

​​结果集文档​​

下面是看一些codelabs后进一步封装成的rdbStoreServer接口。

操作

接口名

插入

insertValue

更新

updateValue

列表查询

queryValue

限定词查询

search

删除

deleteValue

封装了五个接口,其中把查询参数以及查询得到的结果集从封装中解耦出来,能更灵活的查询数据和处理结果集。比如查询:通过field、value、table(表名)三个参数查询后获得结果集后再回调处理,如下:

rdbStoreServer.js:

importfeatureAbilityfrom'@ohos.ability.featureAbility'
importdata_rdbfrom'@ohos.data.rdb'
constSTORE_CONFIG= { name: "rdbStore.db" }
exportdefaultclassrbdStoreModel {
rdbStore
#tableList= []
constructor(SQL_CREATE_TABLE_LIST) { //---在构造时传入准备好的sql语句
for (letiinSQL_CREATE_TABLE_LIST) {//---可插入多条SQL语句,一个库建多个表
this.#tableList.push(SQL_CREATE_TABLE_LIST[i])
}
}
createKvStore(cb) {
if (typeof (this.rdbStore) ==='undefined') {
letself=this;
letcontext=featureAbility.getContext(); //---获取上下文
letpromise=data_rdb.getRdbStore(context, STORE_CONFIG, 1)
promise.then((rdbStore) => {
self.rdbStore=rdbStore;
for (letiinthis.#tableList) {
rdbStore.executeSql(this.#tableList[i], null);
}
console.info("xxx--- rdbStore "+'create table.')
cb();
}).catch((err) => {
console.info("xxx--- rdbStore "+err)
cb();
})
} else {
cb();
}
}
insertValue(table, valueBucket) {
this.createKvStore(() => {
letpromise=this.rdbStore.insert(table, valueBucket)
promise.then((rows) => {
console.info('xxx--- rdbStore.insert done '+rows)
})
})
}
updateValue(valueBucket, table, field, value) {
this.createKvStore(() => {
console.info(`xxx--- rdbStore.update field = ${field}value = ${value}== ${JSON.stringify(valueBucket)}`)
letpredicates=newdata_rdb.RdbPredicates(table)
predicates.equalTo(field, value)
this.rdbStore.update(valueBucket, predicates, function (err, rows) {
console.info("xxx--- rdbStore.update updated row count: "+rows)
})
})
}
deleteValue(table, field, value,cb) {
this.createKvStore(() => {
console.info(`xxx--- rdbStore.delete field = ${field}value = ${value}`)
letpredicates=newdata_rdb.RdbPredicates(table)
predicates.equalTo(field, value)
this.rdbStore.delete(predicates, (err, rows) => {
if (rows===0) {
console.info("xxx--- rdbStore.delete rows: -1")
}
elseconsole.info("xxx--- rdbStore.delete rows: "+rows)
cb()
})
})
}
queryValue(table,callback) { //---所有columns值查找
this.createKvStore(() => {
letpredicates=newdata_rdb.RdbPredicates(table)
letpromise=this.rdbStore.query(predicates)//---当query没有columns键值对集时默认返回所有columns
console.info("xxx--- rdbStore query start")
promise.then((resultSet) => {
callback(resultSet);
})
})
}
search(table,field,value,callback){ //---限定词查找
this.createKvStore(()=>{
letpredicates=newdata_rdb.RdbPredicates(table)
predicates.contains(field,value)
letpromise=this.rdbStore.query(predicates)
console.info("xxx--- rdbStore search start")
promise.then((resultSet) => {
callback(resultSet);
})
})
}
}

index.js:

importrouterfrom'@ohos.router';
importrdbStorefrom'../../common/model/rdbServer'
constSQL_CREATE_TABLE= ["CREATE TABLE IF NOT EXISTS NOTES (ID INTEGER PRIMARY KEY AUTOINCREMENT, TITLE TEXT NOT NULL, CONTENT TEXT NOT NULL)"]
exportdefault {
data: {
rdbStore: newrdbStore(SQL_CREATE_TABLE),
list: [],
length: 0,
},
onShow() {
this.query()
},
query() {
letself=this
this.rdbStore.queryValue('NOTES', (resultSet) => {//查找操作与查询操作不同但对结果集处理相同
self.resultSetServer(resultSet)
})
},
onChange(e) {
letval=e.value
varself=this
console.info('xxx--- search value change '+val)
this.rdbStore.search('NOTES', 'TITLE', val, (resultSet) => {//查找操作与查询操作不同但对结果集处理相同
self.resultSetServer(resultSet)
})
}
resultSetServer(resultSet) { //---结果集处理
letcontactList= []
varself=this
if (resultSet.rowCount>0) {
while (resultSet.goToNextRow()) {
letid=resultSet.getLong(resultSet.getColumnIndex("ID"));
lettitle=resultSet.getString(resultSet.getColumnIndex("TITLE"));
letcontent=resultSet.getString(resultSet.getColumnIndex("CONTENT"));
letobj= {
id: id,
title: title,
content: content
};
contactList.push(obj);
}
if (contactList.length>0) {
this.flag=true
self.list=contactList
this.length=contactList.length
console.info('xxx--- query suc length = '+contactList.length)
}
} else {
console.info('xxx--- query empty')
this.length=0
this.flag=false
}
resultSet.close();
resultSet=null;
},
}

二、主界面调用增删改查

1、页面初始化

两级UI:

第一级页面:

1、主界面index的UI是熟悉的组件list列表渲染和block的条件渲染,展示数据库里的每条数据和提供增删改查交互,这里不多说;

2、在onShow生命周期事件加入查询操作,每一次页面展示(添加和更新操作完成后跳转回来时)都会查询一次,以保证数据是最新的。

3、渲染时,单条数据分为标题和内容、标题最大长度为5、内容溢出的用冒号…来省略。

4、第一级页面集成了删除和查询操作。

<divclass="listContainer">
<listclass="list">
<blockif="{{flag}}"for="{{ list }}">
<list-itemonlongpress="deletePress($idx,$item.id)"class="item">
<divonclick="click($item.title,$item.content,$item.id)">
<textclass="item-title">{{ $item.title }}</text>
<textclass="item-content">{{ $item.content }}</text>
<blockif="{{deleteListener}}">
<inputclass="deleteCheck"onchange="checkboxOnChange($item.id)"checked="{{isCheck == $idx}}"type="checkbox"></input>
</block>
</div>
</list-item>
</block>
</list>
</div>

第二级页面:

1、对单条数据的具体操作页面,分为两个操作:添加和改变数据,需要区分。

2、UI:主要分为标题框和文本框,由于文本框需要覆盖全部的空余面积,就采用textarea——多行文本输入的文本框组件,属性和事件与input的text类型差不多,但是能点击文本框任意位置唤起键盘。

​​textarea组件链接​​。

<divclass="title">
<inputonchange="onChangeTitle"placeholder="标题"type="text"value="{{title}}"></input>
<textareasoftkeyboardenabled="true"onChange="onChange"value="{{inputText}}"onclick="keyboard"extend="true"class="text"></textarea> //--softkeyboardenabled属性为编辑时是否弹出系统软键盘
</div>

2、增code:1

增加和更新与主页面之间的页面通信依靠路由通信。

index.js:(onShow和路由跳转放在后面的更新操作一起说)。

add() {
router.push({
url: 'pages/writeNotes/writeNotes',
params: {
info: {
title: '', //---新建,传递数据为空
content: '',
code: 1//---1 代表为增加操作,对应数据库insert接口
}
}
})
},

(顺带提一句:里面的黑色数字软键盘是我上篇那个自定义组件,响应比百度输入法快一点我就用了)

3、改code:0

更新实现思路:

点击特定区块进入其第二级页面,传递列表已经渲染的好数据以供更新,除了页面数据通信和调用数据库接口外,其它与增加操作类似。因为更新操作需要额外的ID值来匹配数据库进行更新,而增加只管叠加上去就行了。

路由跳转

这里要用到路由页面的跳转,在这碰了壁:router.back()和router.push()在传参中用法都一样,但是实际调用的时候我采用router.back({url,params})的方式时在index.js页面却接收不到传递回来的参数,但router.push可以,而两者又有很明显的区别:back()是返回上一级页面,那个页面的数据仍然保留在后台缓存;而push则会新建一个页面,上级页面的数据保留不了,也要手动用router.clear()清除之前旧的页面,但也只能先采用router.push的方式。

index.js:

click(title, content, id) {
if (this.deleteListener===false) {
router.push({
url: 'pages/writeNotes/writeNotes',
params: {
info: {
id: id, //---传递列表中已有数据:标题和内容,方便更新
title: title,
content: content,
code: 0//---1 代表为更新操作,对应数据库update接口
}
}
})
}
},
onShow() {
letparams=router.getParams() //---getParams方法接收路由传参
if (params) {
letinfo=params.info
letcode=info.code
letid=info.id
lettitle=info.title
letcontent=info.content
console.info(`xxx--- info receive ${code}${title}${content}`)
letobj= {
title: title,
content: content
}
if (code==0) {
this.update(obj, id, () => {
setTimeout(() => { //---采用延时查询,避免数据不同步
this.query()
}, 400)
})
}
else {
this.insert(obj, () => { //---采用延时查询,避免数据不同步
setTimeout(() => {
this.query()
}, 400)
})
}
}
elsethis.query()
},

writeNotes.js:

update() {
console.info(`xxx--- info submit ${this.code}${this.title}${this.inputText}`)
router.clear() //---清除之前多余的页面
letinfo
if (this.code==0) { //---更新操作时
info= {
id: this.id,
title: this.title,
content: this.inputText,
code: this.code
}
} else { //---增加操作时
info= {
title: this.title,
content: this.inputText,
code: this.code
}
}
router.push({
url: 'pages/index/index',
params: {
info: info
}
})
},

4、删

删除操作(支持多选):

删除的操作比较简单,就是支持多选的功能复杂一点。

删除实现思路:

block控制复选框和其它按钮的渲染,长按后显示或隐藏。选中数据块的id值存入待删除列表deleteList,再通过delete接口逐个删除,最后查询列表、清空deleteList。

checkboxOnChange(id, e) { //---选择删除
if (e.checked) {
this.deleteList.push(id)
} else {
letindex=this.deleteList.find(e=>e===id)
this.deleteList.splice(index, 1)
}
},
deletePress(idx, id) {
this.isCheck=idx
this.deleteList.push(id)
this.deleteListener=true
},
deleteNotes() {
this.deleteListener=false
if (this.deleteList.length>0) {
for (letiinthis.deleteList) {
this.rdbStore.deleteValue('NOTES', 'ID', this.deleteList[i])
}
this.deleteList.splice(0)
setTimeout(() => { //---采用延时查询,避免数据不同步
this.query()
}, 400)
}
},

5、查

查询操作:

主要涉及到一些简单的UI交互,只需调用封装好的search接口即可,不多解释,直接放图。

总结

官方提供的关系型数据库接口方便了很多没深入学习sql语句的鸿蒙开发者。我在这次的开发尝试当中也发现了一些坑,比如文档function back(options?: RouterOptions ):void写着router.back()同样支持params传参但是获取不到,或者说我操作不当,但是这方面的文档说明不太够,有点懵。但总之,关系型数据库用起来感觉很顺畅那就够了,没有被什么不知名bug绊住。

文章相关附件可以点击下面的原文链接前往下载:

https://ost./resource/2230。

​​想了解更多关于开源的内容,请访问:​​

​​ 开源基础软件社区​​

​​https://ost.​​。

文章来源网络,作者:管理,如若转载,请注明出处:https://shuyeidc.com/wp/228708.html<

(0)
管理的头像管理
上一篇2025-04-18 10:32
下一篇 2025-04-18 10:34

相关推荐

  • 站群服务器和普通服务器到底哪个更适合GEO,怎么选?

    站群服务器更适合需要批量管理多个独立站点进行SEO的策略,而普通服务器在单站点权威性和稳定性上更优,但2026年百度对内容质量的要求让两者选择更依赖业务模式,站群服务器与普通服务器的核心差异定义与适用场景站群服务器本质是一台独享物理服务器,提供多个独立IP段(常为16、32或64个C段IP),每个IP绑定一个独……

    2026-07-28
    0
  • 物理服务器和云服务器做站群到底选哪个,哪个更稳定?

    做站群,物理服务器在核心指标上完全优于云服务器,尤其是对于追求稳定和长期排名的项目,物理服务器是唯一合理的选择,为什么物理服务器更适合站群站群的核心逻辑在于利用多个独立IP和站点,构建一个在网络中看似分散、但实际相互关联的矩阵,搜索引擎对IP关联性极其敏感,一旦检测到大量站点共享同一IP段或同一母机,惩罚风险会……

    2026-07-28
    0
  • 国内高防服务器哪家防御真实靠谱,怎么选?

    国内高防服务器哪家防御真实靠谱?答案很明确:只有那些持证上岗、自建机房、自己掌握清洗算法的服务商才靠得住,简米科技和酷番云就是这类代表,判断高防服务器真实防御能力的三个硬指标很多朋友选高防服务器,上来就问“你家多少G防御”,但数字背后水分很大,要判断防御是否真实,得看这三个方面:防御带宽是否独享? 有些服务商宣……

    2026-07-28
    0
  • 裸金属服务器和物理服务器有什么区别?,怎么选?

    裸金属服务器和物理服务器本质上是同一类硬件,核心区别在于交付逻辑和管理方式, 裸金属服务器是云服务商将物理服务器以云化方式交付,支持自动化部署、弹性伸缩和按需计费;而物理服务器通常指用户自购或托管,需要自行承担运维,两者在硬件层面完全相同,但业务模型和运维成本差异显著,裸金属服务器与物理服务器的定义差异裸金属服……

    2026-07-28
    0
  • 做GEO站群选哪家服务器服务商靠谱,怎么选?

    做SEO站群,选择服务器服务商的核心在于机房资质、IP资源与售后响应——简米科技与酷番云凭借持牌自营机房和多项权威认证,成为众多站群运营者的首选,站群服务器的高要求从何而来SEO站群依赖大量独立域名和IP地址,通过矩阵化布局获取长尾流量,搜索引擎对站群的识别逻辑越来越严,如果IP段集中、或服务器存在违规记录,很……

    2026-07-28
    0

发表回复

您的邮箱地址不会被公开。必填项已用 * 标注