Tools: Building a Persistent Dark Mode in HarmonyOS with ArkTS Preferences

Tools: Building a Persistent Dark Mode in HarmonyOS with ArkTS Preferences

Source: Dev.to

Problem Description ## Background Knowledge ## Troubleshooting Process ## Analysis Conclusion ## Solution ## Verification Result ## Related Documents or Links ## Written by Hatice Akyel Read the original article:Building a Persistent Dark Mode in HarmonyOS with ArkTS Preferences Many HarmonyOS applications need to store small amounts of data locally—for example, user preferences, recently viewed items, or simple state flags. Developers often rely on in-memory variables, but those values disappear when the app restarts. We need a persistent and lightweight way to save and retrieve key–value pairs. HarmonyOS provides the @kit.ArkData (new package name) module, which contains the preferences API for simple key–value storage. Common pitfalls when working with preferences: By using the preferences API with proper initialization and explicit flush(), you ensure that user settings (like dark mode) persist between app launches. This approach is lightweight and avoids race conditions when writing to storage. Below is a single .ets file that implements a persistent Dark Mode toggle using @kit.ArkData preferences. It stores the darkMode boolean value, updates the UI instantly, and remembers the setting after app restarts. Launch the app and press Turn ON → background switches to black, text becomes white. Close and relaunch the app → Dark Mode state is remembered. https://developer.huawei.com/consumer/en/doc/harmonyos-guides/data-persistence-by-preferences#preferences-constraints Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse COMMAND_BLOCK: import { preferences } from '@kit.ArkData'; import { BusinessError } from '@kit.BasicServicesKit'; import { common } from '@kit.AbilityKit'; class PreferencesUtil { private static instance: PreferencesUtil | null = null; private dataPreferences: preferences.Preferences | null = null; private readonly PREFERENCES_NAME = 'userSettings'; private readonly DARK_MODE_KEY = 'IsDarkMode'; private constructor() {} static getInstance(): PreferencesUtil { if (!PreferencesUtil.instance) { PreferencesUtil.instance = new PreferencesUtil(); } return PreferencesUtil.instance; } async initPreferences(context: common.UIAbilityContext): Promise<void> { const options: preferences.Options = { name: this.PREFERENCES_NAME }; this.dataPreferences = preferences.getPreferencesSync(context, options); console.info('Preferences initialized'); } getDarkModePreference(): boolean { if (!this.dataPreferences) return false; try { return this.dataPreferences.getSync(this.DARK_MODE_KEY, false) as boolean; } catch (err) { console.error('Failed to read preference:', err); return false; } } saveDarkModePreference(isDarkMode: boolean, callback?: () => void): void { if (!this.dataPreferences) return; try { this.dataPreferences.putSync(this.DARK_MODE_KEY, isDarkMode); this.dataPreferences.flush((err: BusinessError) => { if (err) { console.error(`Flush failed: ${err.code} - ${err.message}`); } else { console.info('DarkMode saved:', isDarkMode); callback?.(); } }); } catch (err) { console.error('Failed to save preference:', err); } } toggleDarkMode(callback?: (newState: boolean) => void): void { const newState = !this.getDarkModePreference(); this.saveDarkModePreference(newState, () => callback?.(newState)); } } @Entry @Component struct DarkModePage { @State isDarkMode: boolean = false; private preferencesUtil: PreferencesUtil = PreferencesUtil.getInstance(); async aboutToAppear() { const ctx = this.getUIContext().getHostContext() as common.UIAbilityContext; await this.preferencesUtil.initPreferences(ctx); this.isDarkMode = this.preferencesUtil.getDarkModePreference(); } private toggleTheme() { this.preferencesUtil.toggleDarkMode((newState: boolean) => { this.isDarkMode = newState; }); } build() { Column() { Text(`Dark Mode is ${this.isDarkMode ? 'ON' : 'OFF'}`) .fontSize(20) .margin({ bottom: 20 }) .fontColor(this.isDarkMode ? '#FFFFFF' : '#000000') Button(this.isDarkMode ? 'Turn OFF' : 'Turn ON') .fontSize(18) .fontColor(this.isDarkMode ? '#000000' : '#FFFFFF') .backgroundColor(this.isDarkMode ? '#BBBBBB' : '#333333') .borderRadius(20) .padding({ left: 16, right: 16, top: 10, bottom: 10 }) .onClick(() => this.toggleTheme()) } .width('100%') .height('100%') .backgroundColor(this.isDarkMode ? '#000000' : '#FFFFFF') .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) } } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: import { preferences } from '@kit.ArkData'; import { BusinessError } from '@kit.BasicServicesKit'; import { common } from '@kit.AbilityKit'; class PreferencesUtil { private static instance: PreferencesUtil | null = null; private dataPreferences: preferences.Preferences | null = null; private readonly PREFERENCES_NAME = 'userSettings'; private readonly DARK_MODE_KEY = 'IsDarkMode'; private constructor() {} static getInstance(): PreferencesUtil { if (!PreferencesUtil.instance) { PreferencesUtil.instance = new PreferencesUtil(); } return PreferencesUtil.instance; } async initPreferences(context: common.UIAbilityContext): Promise<void> { const options: preferences.Options = { name: this.PREFERENCES_NAME }; this.dataPreferences = preferences.getPreferencesSync(context, options); console.info('Preferences initialized'); } getDarkModePreference(): boolean { if (!this.dataPreferences) return false; try { return this.dataPreferences.getSync(this.DARK_MODE_KEY, false) as boolean; } catch (err) { console.error('Failed to read preference:', err); return false; } } saveDarkModePreference(isDarkMode: boolean, callback?: () => void): void { if (!this.dataPreferences) return; try { this.dataPreferences.putSync(this.DARK_MODE_KEY, isDarkMode); this.dataPreferences.flush((err: BusinessError) => { if (err) { console.error(`Flush failed: ${err.code} - ${err.message}`); } else { console.info('DarkMode saved:', isDarkMode); callback?.(); } }); } catch (err) { console.error('Failed to save preference:', err); } } toggleDarkMode(callback?: (newState: boolean) => void): void { const newState = !this.getDarkModePreference(); this.saveDarkModePreference(newState, () => callback?.(newState)); } } @Entry @Component struct DarkModePage { @State isDarkMode: boolean = false; private preferencesUtil: PreferencesUtil = PreferencesUtil.getInstance(); async aboutToAppear() { const ctx = this.getUIContext().getHostContext() as common.UIAbilityContext; await this.preferencesUtil.initPreferences(ctx); this.isDarkMode = this.preferencesUtil.getDarkModePreference(); } private toggleTheme() { this.preferencesUtil.toggleDarkMode((newState: boolean) => { this.isDarkMode = newState; }); } build() { Column() { Text(`Dark Mode is ${this.isDarkMode ? 'ON' : 'OFF'}`) .fontSize(20) .margin({ bottom: 20 }) .fontColor(this.isDarkMode ? '#FFFFFF' : '#000000') Button(this.isDarkMode ? 'Turn OFF' : 'Turn ON') .fontSize(18) .fontColor(this.isDarkMode ? '#000000' : '#FFFFFF') .backgroundColor(this.isDarkMode ? '#BBBBBB' : '#333333') .borderRadius(20) .padding({ left: 16, right: 16, top: 10, bottom: 10 }) .onClick(() => this.toggleTheme()) } .width('100%') .height('100%') .backgroundColor(this.isDarkMode ? '#000000' : '#FFFFFF') .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) } } COMMAND_BLOCK: import { preferences } from '@kit.ArkData'; import { BusinessError } from '@kit.BasicServicesKit'; import { common } from '@kit.AbilityKit'; class PreferencesUtil { private static instance: PreferencesUtil | null = null; private dataPreferences: preferences.Preferences | null = null; private readonly PREFERENCES_NAME = 'userSettings'; private readonly DARK_MODE_KEY = 'IsDarkMode'; private constructor() {} static getInstance(): PreferencesUtil { if (!PreferencesUtil.instance) { PreferencesUtil.instance = new PreferencesUtil(); } return PreferencesUtil.instance; } async initPreferences(context: common.UIAbilityContext): Promise<void> { const options: preferences.Options = { name: this.PREFERENCES_NAME }; this.dataPreferences = preferences.getPreferencesSync(context, options); console.info('Preferences initialized'); } getDarkModePreference(): boolean { if (!this.dataPreferences) return false; try { return this.dataPreferences.getSync(this.DARK_MODE_KEY, false) as boolean; } catch (err) { console.error('Failed to read preference:', err); return false; } } saveDarkModePreference(isDarkMode: boolean, callback?: () => void): void { if (!this.dataPreferences) return; try { this.dataPreferences.putSync(this.DARK_MODE_KEY, isDarkMode); this.dataPreferences.flush((err: BusinessError) => { if (err) { console.error(`Flush failed: ${err.code} - ${err.message}`); } else { console.info('DarkMode saved:', isDarkMode); callback?.(); } }); } catch (err) { console.error('Failed to save preference:', err); } } toggleDarkMode(callback?: (newState: boolean) => void): void { const newState = !this.getDarkModePreference(); this.saveDarkModePreference(newState, () => callback?.(newState)); } } @Entry @Component struct DarkModePage { @State isDarkMode: boolean = false; private preferencesUtil: PreferencesUtil = PreferencesUtil.getInstance(); async aboutToAppear() { const ctx = this.getUIContext().getHostContext() as common.UIAbilityContext; await this.preferencesUtil.initPreferences(ctx); this.isDarkMode = this.preferencesUtil.getDarkModePreference(); } private toggleTheme() { this.preferencesUtil.toggleDarkMode((newState: boolean) => { this.isDarkMode = newState; }); } build() { Column() { Text(`Dark Mode is ${this.isDarkMode ? 'ON' : 'OFF'}`) .fontSize(20) .margin({ bottom: 20 }) .fontColor(this.isDarkMode ? '#FFFFFF' : '#000000') Button(this.isDarkMode ? 'Turn OFF' : 'Turn ON') .fontSize(18) .fontColor(this.isDarkMode ? '#000000' : '#FFFFFF') .backgroundColor(this.isDarkMode ? '#BBBBBB' : '#333333') .borderRadius(20) .padding({ left: 16, right: 16, top: 10, bottom: 10 }) .onClick(() => this.toggleTheme()) } .width('100%') .height('100%') .backgroundColor(this.isDarkMode ? '#000000' : '#FFFFFF') .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) } } - Data is stored as key–value pairs (string, number, boolean, etc.) inside a preferences file on the device. - You can access values using synchronous or asynchronous methods. - flush() ensures that changes are physically written to disk. - Not calling or awaiting flush() after put()—changes may be lost if the app is killed before the buffer is written. - Trying to read a key before initialization—getPreferencesSync or getPreferences must be called first. - Forgetting to handle errors—always check for BusinessError in callbacks.