#夏日挑战赛#【FFH】挑战ArkUI复刻手机备忘录(rdb数据库) 原创 精华

FFH_PETSJ
发布于 2022-7-29 10:28
浏览
3收藏

本文正在参加星光计划3.0–夏日挑战赛

关系型数据库实战之 ArkUI 复刻手机备忘录

前言

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

部分效果展示

这是主界面(是不是有点像:-):
#夏日挑战赛#【FFH】挑战ArkUI复刻手机备忘录(rdb数据库)-鸿蒙开发者社区

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

#夏日挑战赛#【FFH】挑战ArkUI复刻手机备忘录(rdb数据库)-鸿蒙开发者社区

代码实现流程

代码结构
#夏日挑战赛#【FFH】挑战ArkUI复刻手机备忘录(rdb数据库)-鸿蒙开发者社区

实现思路

首先把繁多的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

import featureAbility from '@ohos.ability.featureAbility'
import data_rdb from '@ohos.data.rdb'
const STORE_CONFIG = { name: "rdbStore.db" }
export default class rbdStoreModel {
    rdbStore
    #tableList = []
    constructor(SQL_CREATE_TABLE_LIST) { //---在构造时传入准备好的sql语句
        for (let i in SQL_CREATE_TABLE_LIST) {//---可插入多条SQL语句,一个库建多个表
            this.#tableList.push(SQL_CREATE_TABLE_LIST[i])
        }
    }
    createKvStore(cb) {
        if (typeof (this.rdbStore) === 'undefined') {
            let self = this;
            let context = featureAbility.getContext(); //---获取上下文
            let promise = data_rdb.getRdbStore(context, STORE_CONFIG, 1)
            promise.then((rdbStore) => {
                self.rdbStore = rdbStore;
                for (let i in this.#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(() => {
            let promise = 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)}`)
            let predicates = new data_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}`)
            let predicates = new data_rdb.RdbPredicates(table)
            predicates.equalTo(field, value)
            this.rdbStore.delete(predicates, (err, rows) => {
                if (rows === 0) {
                    console.info("xxx--- rdbStore.delete rows: -1")
                }
                else console.info("xxx--- rdbStore.delete rows: " + rows)
                cb()
            })
        })
    }
    queryValue(table,callback) { //---所有columns值查找
        this.createKvStore(() => {
            let predicates = new data_rdb.RdbPredicates(table)
            let promise = 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(()=>{
            let predicates = new data_rdb.RdbPredicates(table)
            predicates.contains(field,value)
            let promise = this.rdbStore.query(predicates)
            console.info("xxx--- rdbStore search start")
            promise.then((resultSet) => {
                callback(resultSet);
            })
        })
    }
}

index.js

import router from '@ohos.router';
import rdbStore from '../../common/model/rdbServer'
const SQL_CREATE_TABLE = ["CREATE TABLE IF NOT EXISTS NOTES (ID INTEGER PRIMARY KEY AUTOINCREMENT, TITLE TEXT NOT NULL, CONTENT TEXT NOT NULL)"]
export default {
    data: {
        rdbStore: new rdbStore(SQL_CREATE_TABLE),
        list: [],
        length: 0,
    },
    onShow() {
  	     this.query()
    },
    query() {
        let self = this
        this.rdbStore.queryValue('NOTES', (resultSet) => {//查找操作与查询操作不同但对结果集处理相同
            self.resultSetServer(resultSet)
        })
    },
    onChange(e) { 
        let val = e.value
        var self = this
        console.info('xxx--- search value change ' + val)
        this.rdbStore.search('NOTES', 'TITLE', val, (resultSet) => {//查找操作与查询操作不同但对结果集处理相同
            self.resultSetServer(resultSet)
        })
    }
    resultSetServer(resultSet) { //---结果集处理
        let contactList = []
        var self = this
        if (resultSet.rowCount > 0) {
            while (resultSet.goToNextRow()) {
                let id = resultSet.getLong(resultSet.getColumnIndex("ID"));
                let title = resultSet.getString(resultSet.getColumnIndex("TITLE"));
                let content = resultSet.getString(resultSet.getColumnIndex("CONTENT"));
                let obj = {
                    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;
    },
}




二. 主界面调用增删改查

(0) ——页面初始化——

两级UI:

第一级页面:
1、主界面index的UI是熟悉的组件list列表渲染和block的条件渲染,展示数据库里的每条数据和提供增删改查交互,这里不多说;
2、在onShow生命周期事件加入查询操作,每一次页面展示(添加和更新操作完成后跳转回来时)都会查询一次,以保证数据是最新的;
3、渲染时,单条数据分为标题和内容、标题最大长度为5、内容溢出的用冒号…来省略;
4、第一级页面集成了删除和查询操作。

    <div class="listContainer">
        <list class="list">
            <block if="{{flag}}" for="{{ list }}">
                <list-item onlongpress="deletePress($idx,$item.id)" class="item">
                    <div onclick="click($item.title,$item.content,$item.id)">
                        <text class="item-title">{{ $item.title }}</text>
                        <text class="item-content">{{ $item.content }}</text>
                        <block if="{{deleteListener}}">
                            <input class="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组件链接

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

(1) ——增—— code:1

增加和更新与主页面之间的页面通信依靠路由通信
index.js:(onShow和路由跳转放在后面的更新操作一起说)

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

#夏日挑战赛#【FFH】挑战ArkUI复刻手机备忘录(rdb数据库)-鸿蒙开发者社区
(顺带提一句:里面的黑色数字软键盘是我上篇那个自定义组件,响应比百度输入法快一点我就用了)

(2) ——改—— 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() {
        let params = router.getParams() //---getParams方法接收路由传参
        if (params) {
            let info = params.info
            let code = info.code
            let id = info.id
            let title = info.title
            let content = info.content
            console.info(`xxx--- info receive ${code} ${title} ${content}`)
            let obj = {
                title: title,
                content: content
            }
            if (code == 0) {
                this.update(obj, id, () => {
                    setTimeout(() => { //---采用延时查询,避免数据不同步
                        this.query()
                    }, 400)
                })
            }
            else {
                this.insert(obj, () => { //---采用延时查询,避免数据不同步
                    setTimeout(() => {
                        this.query()
                    }, 400)
                })
            }
        }
        else this.query()
    },

writeNotes.js

    update() {
        console.info(`xxx--- info submit ${this.code} ${this.title} ${this.inputText} `)
        router.clear() //---清除之前多余的页面
        let info
        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
            }
        })
    },

#夏日挑战赛#【FFH】挑战ArkUI复刻手机备忘录(rdb数据库)-鸿蒙开发者社区

(3) ——删——

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

删除实现思路:

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

    checkboxOnChange(id, e) { //---选择删除
        if (e.checked) {
            this.deleteList.push(id)
        } else {
            let index = 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 (let i in this.deleteList) {
                this.rdbStore.deleteValue('NOTES', 'ID', this.deleteList[i])
            }
            this.deleteList.splice(0)
            setTimeout(() => { //---采用延时查询,避免数据不同步
                this.query()
            }, 400)
        }
    },

#夏日挑战赛#【FFH】挑战ArkUI复刻手机备忘录(rdb数据库)-鸿蒙开发者社区

(4) ——查——

查询操作:
主要涉及到一些简单的UI交互,只需调用封装好的search接口即可,不多解释,直接放图。
#夏日挑战赛#【FFH】挑战ArkUI复刻手机备忘录(rdb数据库)-鸿蒙开发者社区

总结

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

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
memorandum.zip 4.56M 68次下载
已于2022-7-29 10:28:19修改
6
收藏 3
回复
举报
1条回复
按时间正序
/
按时间倒序
红叶亦知秋
红叶亦知秋

现在个人主要用笔记记录一些备忘的事情,楼主可以考虑再推出个内容字体大小调整功能。

回复
2022-7-29 14:23:38
回复
    相关推荐