基于鸿蒙5的离线笔记应用开发实践

暗雨OL
发布于 2025-6-30 02:44
浏览
0收藏

引言
随着鸿蒙操作系统(HarmonyOS)的不断发展,鸿蒙5带来了更强大的开发工具和性能优化。本文将介绍如何使用鸿蒙开发工具和ArkCompiler开发一个功能完善的离线笔记应用,该应用采用本地数据库和文件存储相结合的方式,确保数据安全且不依赖网络连接。

技术栈概述
​​鸿蒙5​​:华为最新的分布式操作系统
​​ArkCompiler​​:鸿蒙的高性能编译器
​​本地数据库​​:使用轻量级的对象关系映射数据库
​​文件存储​​:用于存储笔记中的附件和多媒体内容
项目结构
OfflineNotes/
├── entry/
│ ├── src/
│ │ ├── main/
│ │ │ ├── ets/
│ │ │ │ ├── pages/ // 页面组件
│ │ │ │ ├── model/ // 数据模型
│ │ │ │ ├── database/ // 数据库相关
│ │ │ │ ├── utils/ // 工具类
│ │ │ │ └── App.ets // 应用入口
│ │ │ └── resources/ // 资源文件
数据库设计
我们使用@ohos.data.relationalStore模块来创建本地关系型数据库。

// database/NoteDatabase.ets
import relationalStore from ‘@ohos.data.relationalStore’;

const STORE_CONFIG: relationalStore.StoreConfig = {
name: ‘NotesDB.db’,
securityLevel: relationalStore.SecurityLevel.S1
};

const SQL_CREATE_TABLE = CREATE TABLE IF NOT EXISTS notes ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, is_pinned INTEGER DEFAULT 0, category TEXT );

class NoteDatabase {
private rdbStore: relationalStore.RdbStore | null = null;

async initDatabase(): Promise<void> {
try {
this.rdbStore = await relationalStore.getRdbStore(globalThis.context, STORE_CONFIG);
await this.rdbStore.executeSql(SQL_CREATE_TABLE);
console.info(‘Database initialized successfully’);
} catch (err) {
console.error(Failed to initialize database: ${err});
}
}

getRdbStore(): relationalStore.RdbStore {
if (!this.rdbStore) {
throw new Error(‘Database not initialized’);
}
return this.rdbStore;
}
}

export const noteDatabase = new NoteDatabase();
数据模型
// model/Note.ets
export class Note {
id?: number;
title: string;
content: string;
createdAt: number;
updatedAt: number;
isPinned: boolean;
category?: string;

constructor(
title: string,
content: string,
isPinned: boolean = false,
category?: string
) {
this.title = title;
this.content = content;
const now = new Date().getTime();
this.createdAt = now;
this.updatedAt = now;
this.isPinned = isPinned;
this.category = category;
}
}
数据访问层
// database/NoteRepository.ets
import { Note } from ‘…/model/Note’;
import { noteDatabase } from ‘./NoteDatabase’;

class NoteRepository {
async addNote(note: Note): Promise<number> {
const rdbStore = noteDatabase.getRdbStore();
const valueBucket = {
‘title’: note.title,
‘content’: note.content,
‘created_at’: note.createdAt,
‘updated_at’: note.updatedAt,
‘is_pinned’: note.isPinned ? 1 : 0,
‘category’: note.category
};

try {
  const insertId = await rdbStore.insert('notes', valueBucket);
  console.info(`Added note with id: ${insertId}`);
  return insertId;
} catch (err) {
  console.error(`Failed to add note: ${err}`);
  throw err;
}

}

async getNoteById(id: number): Promise<Note | undefined> {
const rdbStore = noteDatabase.getRdbStore();
const predicates = new relationalStore.RdbPredicates(‘notes’);
predicates.equalTo(‘id’, id);

try {
  const resultSet = await rdbStore.query(predicates, ['id', 'title', 'content', 'created_at', 'updated_at', 'is_pinned', 'category']);
  if (resultSet.rowCount > 0) {
    await resultSet.goToFirstRow();
    return {
      id: resultSet.getDouble(resultSet.getColumnIndex('id')),
      title: resultSet.getString(resultSet.getColumnIndex('title')),
      content: resultSet.getString(resultSet.getColumnIndex('content')),
      createdAt: resultSet.getLong(resultSet.getColumnIndex('created_at')),
      updatedAt: resultSet.getLong(resultSet.getColumnIndex('updated_at')),
      isPinned: resultSet.getDouble(resultSet.getColumnIndex('is_pinned')) === 1,
      category: resultSet.getString(resultSet.getColumnIndex('category'))
    };
  }
} catch (err) {
  console.error(`Failed to get note: ${err}`);
  throw err;
}
return undefined;

}

async getAllNotes(): Promise<Note[]> {
const rdbStore = noteDatabase.getRdbStore();
const predicates = new relationalStore.RdbPredicates(‘notes’);
predicates.orderByDesc(‘is_pinned’).orderByDesc(‘updated_at’);

try {
  const resultSet = await rdbStore.query(predicates, ['id', 'title', 'content', 'created_at', 'updated_at', 'is_pinned', 'category']);
  const notes: Note[] = [];
  while (resultSet.goToNextRow()) {
    notes.push({
      id: resultSet.getDouble(resultSet.getColumnIndex('id')),
      title: resultSet.getString(resultSet.getColumnIndex('title')),
      content: resultSet.getString(resultSet.getColumnIndex('content')),
      createdAt: resultSet.getLong(resultSet.getColumnIndex('created_at')),
      updatedAt: resultSet.getLong(resultSet.getColumnIndex('updated_at')),
      isPinned: resultSet.getDouble(resultSet.getColumnIndex('is_pinned')) === 1,
      category: resultSet.getString(resultSet.getColumnIndex('category'))
    });
  }
  return notes;
} catch (err) {
  console.error(`Failed to get all notes: ${err}`);
  throw err;
}

}

async updateNote(note: Note): Promise<void> {
if (!note.id) {
throw new Error(‘Note id is required for update’);
}

const rdbStore = noteDatabase.getRdbStore();
const valueBucket = {
  'title': note.title,
  'content': note.content,
  'updated_at': new Date().getTime(),
  'is_pinned': note.isPinned ? 1 : 0,
  'category': note.category
};
const predicates = new relationalStore.RdbPredicates('notes');
predicates.equalTo('id', note.id);

try {
  await rdbStore.update(valueBucket, predicates);
  console.info(`Updated note with id: ${note.id}`);
} catch (err) {
  console.error(`Failed to update note: ${err}`);
  throw err;
}

}

async deleteNote(id: number): Promise<void> {
const rdbStore = noteDatabase.getRdbStore();
const predicates = new relationalStore.RdbPredicates(‘notes’);
predicates.equalTo(‘id’, id);

try {
  await rdbStore.delete(predicates);
  console.info(`Deleted note with id: ${id}`);
} catch (err) {
  console.error(`Failed to delete note: ${err}`);
  throw err;
}

}
}

export const noteRepository = new NoteRepository();
文件存储管理
对于笔记中的附件,我们使用文件系统API进行存储:

// utils/FileManager.ets
import fileio from ‘@ohos.fileio’;
import file from ‘@ohos.file.fs’;

const NOTES_DIR = ‘notes_attachments’;

class FileManager {
async init(): Promise<void> {
try {
const dirPath = globalThis.context.filesDir + ‘/’ + NOTES_DIR;
await file.mkdir(dirPath);
} catch (err) {
if (err.code !== 13900015) { // 目录已存在的错误码
console.error(Failed to create notes directory: ${err});
throw err;
}
}
}

async saveAttachment(noteId: number, fileName: string, data: ArrayBuffer): Promise<string> {
const dirPath = ${globalThis.context.filesDir}/${NOTES_DIR}/${noteId};
const filePath = ${dirPath}/${fileName};

try {
  await file.mkdir(dirPath);
  await file.write(filePath, data);
  return filePath;
} catch (err) {
  console.error(`Failed to save attachment: ${err}`);
  throw err;
}

}

async getAttachment(filePath: string): Promise<Uint8Array> {
try {
return await file.read(filePath);
} catch (err) {
console.error(Failed to read attachment: ${err});
throw err;
}
}

async deleteAttachment(filePath: string): Promise<void> {
try {
await file.unlink(filePath);
} catch (err) {
console.error(Failed to delete attachment: ${err});
throw err;
}
}

async deleteNoteAttachments(noteId: number): Promise<void> {
const dirPath = ${globalThis.context.filesDir}/${NOTES_DIR}/${noteId};
try {
await file.rmdir(dirPath);
} catch (err) {
console.error(Failed to delete note attachments: ${err});
throw err;
}
}
}

export const fileManager = new FileManager();
主界面实现
// pages/NoteListPage.ets
import { Note } from ‘…/model/Note’;
import { noteRepository } from ‘…/database/NoteRepository’;

@Entry
@Component
struct NoteListPage {
@State notes: Note[] = [];

async aboutToAppear() {
this.loadNotes();
}

async loadNotes() {
try {
this.notes = await noteRepository.getAllNotes();
} catch (err) {
console.error(Failed to load notes: ${err});
}
}

build() {
Column() {
List({ space: 10 }) {
ForEach(this.notes, (note: Note) => {
ListItem() {
NoteItem({ note: note })
}
}, (note: Note) => note.id?.toString() ?? ‘new’)
}
.layoutWeight(1)
.width(‘100%’)

  Button('Add Note')
    .onClick(() => {
      router.pushUrl({ url: 'pages/EditNotePage' });
    })
}
.width('100%')
.height('100%')
.padding(10)

}
}

@Component
struct NoteItem {
@Prop note: Note;

build() {
Column() {
Row() {
if (this.note.isPinned) {
Image($r(‘app.media.ic_pin’))
.width(20)
.height(20)
.margin({ right: 5 })
}
Text(this.note.title)
.fontSize(18)
.fontWeight(FontWeight.Bold)
}
.justifyContent(FlexAlign.Start)
.width(‘100%’)

  Text(this.note.content.length > 50 ? 
       this.note.content.substring(0, 50) + '...' : this.note.content)
    .fontSize(14)
    .margin({ top: 5 })
    .width('100%')
    .textAlign(TextAlign.Start)

  Row() {
    Text(new Date(this.note.updatedAt).toLocaleDateString())
      .fontSize(12)
      .fontColor(Color.Gray)

    if (this.note.category) {
      Text(this.note.category)
        .fontSize(12)
        .fontColor(Color.Blue)
        .margin({ left: 10 })
    }
  }
  .justifyContent(FlexAlign.Start)
  .width('100%')
  .margin({ top: 5 })
}
.padding(10)
.borderRadius(10)
.backgroundColor(Color.White)
.shadow({ radius: 5, color: Color.Gray, offsetX: 1, offsetY: 1 })
.onClick(() => {
  router.pushUrl({ url: `pages/EditNotePage?id=${this.note.id}` });
})

}
}
编辑笔记页面
// pages/EditNotePage.ets
import { Note } from ‘…/model/Note’;
import { noteRepository } from ‘…/database/NoteRepository’;
import { fileManager } from ‘…/utils/FileManager’;

@Entry
@Component
struct EditNotePage {
@State note: Note = new Note(‘’, ‘’);
@State isNewNote: boolean = true;
@State attachments: string[] = [];

async aboutToAppear() {
const params = router.getParams();
if (params && params[‘id’]) {
const noteId = parseInt(params[‘id’]);
const existingNote = await noteRepository.getNoteById(noteId);
if (existingNote) {
this.note = existingNote;
this.isNewNote = false;
// 加载附件列表
await this.loadAttachments();
}
}
}

async loadAttachments() {
if (!this.note.id) return;

const dirPath = `${globalThis.context.filesDir}/notes_attachments/${this.note.id}`;
try {
  const files = await file.listFile(dirPath);
  this.attachments = files.map(file => `${dirPath}/${file}`);
} catch (err) {
  console.error(`Failed to load attachments: ${err}`);
}

}

async saveNote() {
this.note.updatedAt = new Date().getTime();

try {
  if (this.isNewNote) {
    const newId = await noteRepository.addNote(this.note);
    this.note.id = newId;
    this.isNewNote = false;
  } else {
    await noteRepository.updateNote(this.note);
  }
  router.back();
} catch (err) {
  console.error(`Failed to save note: ${err}`);
}

}

async addAttachment() {
if (!this.note.id) {
// 如果是新笔记,先保存以获取ID
const newId = await noteRepository.addNote(this.note);
this.note.id = newId;
this.isNewNote = false;
}

try {
  // 这里应该是从文件选择器获取文件,简化示例使用模拟数据
  const mockFile = new ArrayBuffer(1024);
  const filePath = await fileManager.saveAttachment(
    this.note.id, 
    `attachment_${new Date().getTime()}.txt`, 
    mockFile
  );
  this.attachments.push(filePath);
} catch (err) {
  console.error(`Failed to add attachment: ${err}`);
}

}

async deleteAttachment(index: number) {
try {
await fileManager.deleteAttachment(this.attachments[index]);
this.attachments.splice(index, 1);
} catch (err) {
console.error(Failed to delete attachment: ${err});
}
}

build() {
Column() {
TextInput({ placeholder: ‘Title’ })
.text(this.note.title)
.onChange((value: string) => {
this.note.title = value;
})
.height(50)
.width(‘100%’)

  TextInput({ placeholder: 'Content' })
    .text(this.note.content)
    .onChange((value: string) => {
      this.note.content = value;
    })
    .height(200)
    .width('100%')
    .margin({ top: 10 })

  Row() {
    Toggle({ type: ToggleType.Checkbox, isOn: this.note.isPinned })
      .onChange((isOn: boolean) => {
        this.note.isPinned = isOn;
      })
    Text('Pin Note')
      .margin({ left: 5 })
  }
  .margin({ top: 10 })

  TextInput({ placeholder: 'Category' })
    .text(this.note.category || '')
    .onChange((value: string) => {
      this.note.category = value;
    })
    .height(40)
    .width('100%')
    .margin({ top: 10 })

  // 附件列表
  if (this.attachments.length > 0) {
    Text('Attachments:')
      .fontSize(16)
      .margin({ top: 20, bottom: 5 })
      .width('100%')
      .textAlign(TextAlign.Start)

    List() {
      ForEach(this.attachments, (filePath: string, index: number) => {
        ListItem() {
          Row() {
            Text(filePath.substring(filePath.lastIndexOf('/') + 1))
              .layoutWeight(1)
            Button('Delete')
              .onClick(() => this.deleteAttachment(index))
          }
        }
      })
    }
    .height(150)
    .width('100%')
  }

  Button('Add Attachment')
    .onClick(() => this.addAttachment())
    .margin({ top: 10 })

  Button('Save')
    .onClick(() => this.saveNote())
    .width('80%')
    .margin({ top: 20 })
}
.padding(15)
.width('100%')
.height('100%')

}
}
应用入口
// App.ets
import { noteDatabase } from ‘./database/NoteDatabase’;
import { fileManager } from ‘./utils/FileManager’;

@Entry
@Component
struct App {
async aboutToAppear() {
// 初始化数据库和文件系统
await noteDatabase.initDatabase();
await fileManager.init();
}

build() {
Column() {
Navigator({ target: ‘pages/NoteListPage’ }) {
Text(‘Offline Notes’)
.fontSize(30)
.fontWeight(FontWeight.Bold)
.margin({ top: 50 })
}
}
.width(‘100%’)
.height(‘100%’)
}
}
性能优化与ArkCompiler
鸿蒙5的ArkCompiler为我们的应用提供了卓越的性能:

​​AOT编译​​:ArkCompiler的提前编译技术将TypeScript代码编译为高效的机器码
​​内存管理​​:自动内存管理减少内存泄漏风险
​​多线程优化​​:数据库操作自动在后台线程执行,不阻塞UI
总结
本文展示了如何利用鸿蒙5的开发工具和ArkCompiler构建一个功能完善的离线笔记应用。通过结合本地数据库和文件存储,我们创建了一个不依赖网络连接、数据安全可靠的应用。鸿蒙的分布式能力还可以进一步扩展,实现跨设备同步等功能,这将是未来迭代的方向。

这个应用展示了鸿蒙开发的核心技术:

关系型数据库操作
文件系统管理
UI组件开发
状态管理
应用生命周期控制
开发者可以根据实际需求进一步扩展功能,如添加笔记搜索、Markdown支持、云备份等高级特性。

分类
标签
收藏
回复
举报
回复
    相关推荐