Building a Persistent Dark Mode in HarmonyOS with ArkTS Preferences

Published: (February 20, 2026 at 01:32 AM EST)
4 min read
Source: Dev.to

Source: Dev.to

Original article:
Building a Persistent Dark Mode in HarmonyOS with ArkTS Preferences

Problem Description

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.

Background Knowledge

HarmonyOS provides the @kit.ArkData (new package name) module, which contains the preferences API for simple key‑value storage.

  • 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.

Troubleshooting Process

Common pitfalls when working with preferences:

PitfallWhy it matters
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 initializationgetPreferencesSync or getPreferences must be called first.
Forgetting to handle errorsAlways check for BusinessError in callbacks.

Analysis Conclusion

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.

Solution

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.

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 {
    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);
  }
}

Verification Result

  1. Launch the app and press Turn ON → background switches to black, text becomes white.
  2. Close and relaunch the app → Dark Mode state is remembered.

Verification Screenshot

![3.png](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y8m9dxhab3ukqlpz8obg.png)

[![3.png](https://media2.dev.to/dynamic/image/width=800,height=,fit=scale-down,gravity=auto,format=auto/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hnokpbpeldslax9ohrgb.png)](https://media2.dev.to/dynamic/image/width=800,height=,fit=scale-down,gravity=auto,format=auto/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hnokpbpeldslax9ohrgb.png)

---

### Related Documents or Links

- 

---

*Written by Hatice Akyel*
0 views
Back to Blog

Related posts

Read more »

LIVO Next-Level Flutter State Management

Flutter is great, but managing state can quickly become messy. You’ve probably used setState for small projects or Provider, Riverpod, or BLoC for bigger apps—b...