Gestión de Archivos en el Sandbox de HarmonyOS
Para la manipulación directa de archvios de texto dentro del sandbox de la aplicación, HarmonyOS provee el módulo @ohos.file.fs. El siguiente componente ilustra cómo capturar texto del usuario, persistirlo en un archivo local y posteriormente recuperarlo para su visualización en la interfaz.
// FileHandler.ets
import promptAction from '@ohos.promptAction'
import fs from '@ohos.file.fs'
@Entry
@Component
struct FileHandler {
@State userInput: string = ''
@State displayedContent: string = 'El contenido aparecerá aquí'
private appContext = getContext(this)
private storageFile: string = 'datos_usuario.txt'
build() {
Column() {
TextInput({ placeholder: 'Escribe el texto a guardar' })
.width('85%')
.height(50)
.onChange((val: string) => { this.userInput = val })
Button('Guardar en Archivo')
.width('85%')
.margin({ top: 15 })
.onClick(() => { this.persistData() })
Button('Leer desde Archivo')
.width('85%')
.margin({ top: 15 })
.onClick(() => { this.fetchData() })
Text(this.displayedContent)
.width('85%')
.margin({ top: 15 })
.padding(12)
.backgroundColor('#eeeeee')
.borderRadius(6)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
private async persistData() {
try {
const targetPath = this.appContext.filesDir + '/' + this.storageFile
let fileDescriptor = await fs.open(targetPath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE | fs.OpenMode.TRUNCATE)
await fs.write(fileDescriptor.fd, this.userInput)
await fs.close(fileDescriptor.fd)
promptAction.showToast({ message: 'Escritura exitosa', duration: 1500 })
} catch (err) {
promptAction.showToast({ message: 'Error al escribir', duration: 1500 })
}
}
private async fetchData() {
try {
const targetPath = this.appContext.filesDir + '/' + this.storageFile
if (!await fs.access(targetPath)) {
promptAction.showToast({ message: 'Archivo inexistente', duration: 1500 })
return
}
let fileDescriptor = await fs.open(targetPath, fs.OpenMode.READ_ONLY)
let fileStats = await fs.stat(fileDescriptor.fd)
let buffer = new ArrayBuffer(fileStats.size)
await fs.read(fileDescriptor.fd, buffer)
await fs.close(fileDescriptor.fd)
this.displayedContent = String.fromCharCode.apply(null, new Uint8Array(buffer))
} catch (err) {
promptAction.showToast({ message: 'Error de lectura', duration: 1500 })
}
}
}
Arquitectura de Memos con SQLite y Preferencias
Para una gestión estructurada de datos relacionales, como en una aplicación de notas o memos, se emplea SQLite a través del módulo @ohos.data.relationalStore. Adicionalmente, las configuraciones de interfaz del usuario, como los tamaños de fuente, se preservan eficientemente utilizando el módulo @ohos.data.preferences.
Modelo de Datos
// MemoItem.ets
export class MemoItem {
recordId?: number
header: string = ''
body: string = ''
creationTime: string = ''
updateTime: string = ''
constructor(header: string, body: string, creationTime: string, updateTime: string) {
this.header = header
this.body = body
this.creationTime = creationTime
this.updateTime = updateTime
}
}
Gestor de Base de Datos Relacional
Se unifica la lógica de consulta filtrando mediante un parámetro opcional, simplificando las interacciones con la base de datos.
// RdbManager.ets
import relationalStore from '@ohos.data.relationalStore';
import { MemoItem } from './MemoItem';
const DB_SETTINGS = {
name: 'MemosDB.db',
securityLevel: relationalStore.SecurityLevel.S1
};
const SQL_INIT = `CREATE TABLE IF NOT EXISTS memos (
recordId INTEGER PRIMARY KEY AUTOINCREMENT,
header TEXT NOT NULL,
body TEXT,
creationTime TEXT NOT NULL,
updateTime TEXT NOT NULL
)`;
export class RdbManager {
private dbInstance: relationalStore.RdbStore | null = null;
async setupDatabase(ctx: Context): Promise<void> {
this.dbInstance = await relationalStore.getRdbStore(ctx, DB_SETTINGS);
await this.dbInstance.executeSql(SQL_INIT);
}
async insertMemo(item: MemoItem): Promise<number> {
if (!this.dbInstance) return -1;
const dataBucket = { 'header': item.header, 'body': item.body, 'creationTime': item.creationTime, 'updateTime': item.updateTime };
return await this.dbInstance.insert('memos', dataBucket);
}
async modifyMemo(item: MemoItem): Promise<boolean> {
if (!this.dbInstance || !item.recordId) return false;
const dataBucket = { 'header': item.header, 'body': item.body, 'updateTime': item.updateTime };
const condition = new relationalStore.RdbPredicates('memos');
condition.equalTo('recordId', item.recordId);
const affectedRows = await this.dbInstance.update(dataBucket, condition);
return affectedRows > 0;
}
async removeMemo(id: number): Promise<boolean> {
if (!this.dbInstance) return false;
const condition = new relationalStore.RdbPredicates('memos');
condition.equalTo('recordId', id);
const deletedRows = await this.dbInstance.delete(condition);
return deletedRows > 0;
}
async fetchMemos(filterTerm: string = ''): Promise<Array<MemoItem>> {
if (!this.dbInstance) return [];
const condition = new relationalStore.RdbPredicates('memos');
if (filterTerm) {
condition.contains('header', filterTerm);
}
const resultSet = await this.dbInstance.query(condition, ['recordId', 'header', 'body', 'creationTime', 'updateTime']);
const itemList: Array<MemoItem> = [];
while (resultSet.goToNextRow()) {
const memo = new MemoItem(
resultSet.getString(resultSet.getColumnIndex('header')),
resultSet.getString(resultSet.getColumnIndex('body')),
resultSet.getString(resultSet.getColumnIndex('creationTime')),
resultSet.getString(resultSet.getColumnIndex('updateTime'))
);
memo.recordId = resultSet.getLong(resultSet.getColumnIndex('recordId'));
itemList.push(memo);
}
resultSet.close();
return itemList;
}
}
export const rdbMgr = new RdbManager();
Gestor de Preferencias de Usuario
// SettingsManager.ets
import preferences from '@ohos.data.preferences';
const PREF_FILE = 'app_settings';
const HEADER_FONT_SIZE_KEY = 'headerSize';
const BODY_FONT_SIZE_KEY = 'bodySize';
const DEFAULT_FONT_SIZE = 16;
export class SettingsManager {
private settingsInstance: preferences.Preferences | null = null;
async setupPreferences(ctx: Context): Promise<void> {
this.settingsInstance = await preferences.getPreferences(ctx, PREF_FILE);
}
async getHeaderFontSize(): Promise<number> {
if (!this.settingsInstance) return DEFAULT_FONT_SIZE;
const val = await this.settingsInstance.get(HEADER_FONT_SIZE_KEY, DEFAULT_FONT_SIZE);
return typeof val === 'number' ? val : DEFAULT_FONT_SIZE;
}
async setHeaderFontSize(size: number): Promise<void> {
if (!this.settingsInstance) return;
await this.settingsInstance.put(HEADER_FONT_SIZE_KEY, size);
await this.settingsInstance.flush();
}
async getBodyFontSize(): Promise<number> {
if (!this.settingsInstance) return DEFAULT_FONT_SIZE;
const val = await this.settingsInstance.get(BODY_FONT_SIZE_KEY, DEFAULT_FONT_SIZE);
return typeof val === 'number' ? val : DEFAULT_FONT_SIZE;
}
async setBodyFontSize(size: number): Promise<void> {
if (!this.settingsInstance) return;
await this.settingsInstance.put(BODY_FONT_SIZE_KEY, size);
await this.settingsInstance.flush();
}
}
export const settingsMgr = new SettingsManager();
Interfaz Principal: Listado y Búsqueda
La pantalla de inicio despliega los registros usando un diseño basado en Stack para superponer el botón de eliminación y habliitar el gesto de deslizamiento lateral.
// MemoList.ets
import router from '@ohos.router';
import { rdbMgr } from './RdbManager';
import { MemoItem } from './MemoItem';
import { settingsMgr } from './SettingsManager';
@Entry
@Component
struct MemoList {
@State memoList: Array<MemoItem> = [];
@State filterTerm: string = '';
@State headerSize: number = 16;
@State bodySize: number = 16;
@State activeSwipeRow: number = -1;
@State dragOffset: number = 0;
aboutToAppear() {
this.initializeApp();
}
onPageShow() {
this.loadMemos();
}
async initializeApp() {
await rdbMgr.setupDatabase(getContext(this));
await settingsMgr.setupPreferences(getContext(this));
this.headerSize = await settingsMgr.getHeaderFontSize();
this.bodySize = await settingsMgr.getBodyFontSize();
this.loadMemos();
}
async loadMemos() {
this.memoList = await rdbMgr.fetchMemos(this.filterTerm);
}
build() {
Column() {
Search({ value: this.filterTerm, placeholder: 'Filtrar por encabezado' })
.onChange((val: string) => { this.filterTerm = val; this.loadMemos(); })
.width('90%')
.margin(10)
List({ space: 12 }) {
ForEach(this.memoList, (item: MemoItem, idx: number) => {
ListItem() {
Stack({ alignContent: Alignment.End }) {
Button('Eliminar')
.type(ButtonType.Normal)
.backgroundColor(Color.Red)
.height('100%')
.width(70)
.onClick(async () => { await rdbMgr.removeMemo(item.recordId!); this.loadMemos(); this.activeSwipeRow = -1; })
Row() {
Column() {
Text(item.header).fontSize(this.headerSize).fontWeight(FontWeight.Bold)
Text(item.body).fontSize(this.bodySize).maxLines(2).textOverflow({ overflow: TextOverflow.Ellipsis })
Text(`Creado: ${item.creationTime} | Modificado: ${item.updateTime}`).fontSize(11).opacity(0.6).margin({ top: 4 })
}.padding(10).width('100%')
}
.backgroundColor(Color.White)
.borderRadius(8)
.shadow({ radius: 3, color: '#cccccc', offsetX: 1, offsetY: 1 })
.offset({ x: this.activeSwipeRow === idx ? this.dragOffset : 0 })
.onClick(() => {
if (this.activeSwipeRow !== -1) { this.activeSwipeRow = -1; this.dragOffset = 0; return; }
router.pushUrl({ url: 'pages/MemoEditor', params: { editMode: 'modify', recordId: item.recordId?.toString() } });
})
.gesture(
PanGesture({ direction: PanDirection.Horizontal })
.onActionStart(() => { if (this.activeSwipeRow !== idx) this.activeSwipeRow = idx; })
.onActionUpdate((e: GestureEvent) => {
if (this.activeSwipeRow === idx && e.offsetX < 50 && e.offsetX > -120) this.dragOffset = e.offsetX;
})
.onActionEnd(() => {
this.dragOffset = this.dragOffset < -30 ? -80 : 0;
if (this.dragOffset === 0) this.activeSwipeRow = -1;
})
)
}.height(90).width('100%').clip(true)
}
}, (item: MemoItem) => item.recordId?.toString() ?? '')
}.layoutWeight(1).width('100%')
Button('Nuevo Memo')
.onClick(() => { router.pushUrl({ url: 'pages/MemoEditor', params: { editMode: 'create' } }); })
.width('90%').margin(10)
}.width('100%').height('100%')
}
}
Interfaz de Edición y Configuración
Esta vista permite crear o actualizar un registro, e incluye controles deslizantes que guardan las preferencias tipográficas del usuario de manera instantánea.
// MemoEditor.ets
import router from '@ohos.router';
import { rdbMgr } from './RdbManager';
import { settingsMgr } from './SettingsManager';
import { MemoItem } from './MemoItem';
@Entry
@Component
struct MemoEditor {
@State header: string = '';
@State body: string = '';
@State headerSize: number = 16;
@State bodySize: number = 16;
private operationMode: string = 'create';
private targetRecordId: number = 0;
aboutToAppear() {
const navParams = router.getParams() as Record<string, string>;
this.operationMode = navParams?.editMode ?? 'create';
if (this.operationMode === 'modify' && navParams?.recordId) {
this.targetRecordId = parseInt(navParams.recordId);
this.populateFields();
}
this.loadSettings();
}
async loadSettings() {
this.headerSize = await settingsMgr.getHeaderFontSize();
this.bodySize = await settingsMgr.getBodyFontSize();
}
async populateFields() {
const items = await rdbMgr.fetchMemos();
const target = items.find(m => m.recordId === this.targetRecordId);
if (target) { this.header = target.header; this.body = target.body; }
}
async commitMemo() {
if (!this.header.trim()) return;
const timestamp = new Date().toLocaleString();
const currentMemo = new MemoItem(this.header, this.body, timestamp, timestamp);
if (this.operationMode === 'create') {
await rdbMgr.insertMemo(currentMemo);
} else {
currentMemo.recordId = this.targetRecordId;
await rdbMgr.modifyMemo(currentMemo);
}
router.back();
}
build() {
Column() {
TextInput({ text: this.header, placeholder: 'Encabezado' })
.onChange((v: string) => { this.header = v; })
.fontSize(this.headerSize).margin(10).width('90%')
TextArea({ text: this.body, placeholder: 'Cuerpo del memo' })
.onChange((v: string) => { this.body = v; })
.fontSize(this.bodySize).margin(10).width('90%').height(180)
Button('Guardar')
.onClick(() => this.commitMemo())
.width('90%').margin(10)
Row() {
Text('Fuente Título:').fontSize(13)
Slider({ value: this.headerSize, min: 12, max: 30, step: 1, style: SliderStyle.OutSet })
.onChange((v: number) => { this.headerSize = v; settingsMgr.setHeaderFontSize(v); })
.width('55%')
}.margin(10).width('90%').justifyContent(FlexAlign.SpaceBetween)
Row() {
Text('Fuente Cuerpo:').fontSize(13)
Slider({ value: this.bodySize, min: 12, max: 30, step: 1, style: SliderStyle.OutSet })
.onChange((v: number) => { this.bodySize = v; settingsMgr.setBodyFontSize(v); })
.width('55%')
}.margin(10).width('90%').justifyContent(FlexAlign.SpaceBetween)
}.width('100%').height('100%')
}
}