大家好,我是不想掉发的鸿蒙开发工程师 城中的雾,经过前三期的交流,在 Page 和 Ability 里玩转 Context 应该没问题了,但开发过程中很多开发者朋友在封装工具类时,经常会遇到这种尴尬:

  • “我就想在 HttpUtils 捕获异常时弹个 Toast,结果发现找不到 promptAction。”
  • “我就想在 LogUtils 里拿个 filesDir 写日志,结果没有 context 没法调 API。”

这一期,我们跳出 UI 的舒适区,聊聊在 纯逻辑代码(架构层) 中如何优雅地使用 Context。

1. 痛点:为什么 Utils 里没有 Context?

在鸿蒙(ArkTS)中,Context 是系统运行环境的引用。它必须依附于某个具体的组件(Ability 或 UI)才能存在。

当你创建一个普通的 .ets.ts 文件时,没有办法直接获取到上下文。

错误示范

// HttpUtil.ets
export class HttpUtil {
  static request(url: string) {
    // ❌ 报错:这里是静态方法,哪来的 this.context?
    // 也不能 new Context(),因为 Context 是系统创建的
    // this.context.filesDir 
  }
}

我们要解决的,就是如何把 Context 运送到工具类里去

2. 方案 A:参数传递法

这是最朴素、也是最推荐的方法。谁调用工具类,谁就负责把 Context 传进来。

场景:获取资源字符串

我们封装一个工具函数,用于读取 resources 目录下的 JSON 或字符串。

工具类写法

// ResUtils.ets
import { common } from '@kit.AbilityKit';

export class ResUtils {
  // 显式要求传入 Context
  static getString(context: common.Context, resId: number): string {
    // 即使传入的是 AbilityContext 或 UIContext,都能用 resourceManager
    return context.resourceManager.getStringSync(resId);
  }
}

调用方写法

// Index.ets
@Entry
@Component
struct Index {
  build() {
    Button('读取文字')
      .onClick(() => {
        // 把当前的 Context 传过去
        let str = ResUtils.getString(getContext(this), $r('app.string.test_str').id);
      })
  }
}
  • 优点:内存安全,生命周期清晰,不会持有过期的 Context。
  • 缺点:每个方法都要多传一个参数,写起来有点累(参数透传地狱)。

3. 方案 B:全局单例法

如果你觉得每次传参太麻烦,而且你需要的是 ApplicationContext(比如写日志、存首选项,这些操作不需要依赖特定 UI),那么可以搞一个全局单例来持有它。

警告:

不要在全局单例中持有 AbilityContext 或 UIContext!会导致严重的内存泄漏。

只能持有 ApplicationContext。

第一步:封装单例类

// GlobalContext.ets
import { common } from '@kit.AbilityKit';

export class GlobalContext {
  private static instance: GlobalContext;
  private _appContext: common.ApplicationContext | undefined;

  private constructor() {}

  public static get(): GlobalContext {
    if (!GlobalContext.instance) {
      GlobalContext.instance = new GlobalContext();
    }
    return GlobalContext.instance;
  }

  // 初始化方法
  public init(context: common.Context) {
    this._appContext = context.getApplicationContext();
  }

  // 获取 ApplicationContext
  public getAppContext(): common.ApplicationContext {
    if (!this._appContext) {
      throw new Error("GlobalContext not initialized! Call init() in EntryAbility.");
    }
    return this._appContext;
  }
}

第二步:在入口初始化

EntryAbility.etsonCreate 中尽早初始化。

// EntryAbility.ets
import { GlobalContext } from '../utils/GlobalContext';

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    // 初始化全局上下文
    GlobalContext.get().init(this.context);
  }
}

第三步:随处使用

现在,你可以在任何 .ets 文件中使用了。

// LogUtils.ets
import { GlobalContext } from './GlobalContext';

export function writeLog(msg: string) {
  // 不需要传参,直接拿
  let context = GlobalContext.get().getAppContext();
  let dir = context.filesDir;
  // ... 写入文件逻辑 ...
}

4. 终极难题:如何在 Utils 里弹窗?

这是架构篇最大的坑。

很多同学想在 HttpUtil 的拦截器里写:

if (code == 401) showToast(“请登录”)

尝试 1:用 GlobalContext 弹窗?(失败)

// 错误
GlobalContext.get().getAppContext().getPromptAction() // 报错!ApplicationContext 没有界面能力

尝试 2:传 UIContext 进去?(可行但麻烦)

需要把 uiContext 一路传到网络请求层,代码侵入性太强。

推荐方案:事件总线 (EventHub)

利用我们在第一期讲过的 EventHub,让 Utils 发送“信号”,让 UI 层自己去弹窗。这样既解耦,又安全。

工具类 (HttpUtil.ets)

import { GlobalContext } from './GlobalContext';

function handleByInterceptor(error: Error) {
    // 1. 获取全局上下文
    let appCtx = GlobalContext.get().getAppContext();
    // 2. 发送一个全局事件,比如 "SHOW_TOAST"
    appCtx.eventHub.emit('SHOW_TOAST', '网络连接超时');
}

基类 UI (BasePage.ets 或 EntryAbility):

在应用的主界面或者 Ability 中监听这个事件。

// Index.ets
aboutToAppear() {
    let appCtx = getContext(this).getApplicationContext();
    // 监听全局弹窗事件
    appCtx.eventHub.on('SHOW_TOAST', (msg: string) => {
        // 这里是在 UI 内部,可以安全地使用 UIContext 弹窗
        this.getUIContext().getPromptAction().showToast({ message: msg });
    });
}

这样,HttpUtil 只需要负责通知,具体怎么弹窗、在哪弹窗,交给 UI 层去处理。

5. 总结

  1. 原则一:尽量使用 参数传递。虽然麻烦,但最干净,不会有副作用。
  2. 原则二:如果必须使用全局单例,只能存 ApplicationContext。存 AbilityContext 就是在给内存泄漏埋雷。
  3. 原则三非 UI 不弹窗。不要试图在 Utils 里强行持有 UIContext 来弹窗。请使用 EventHub 发送事件,或者返回错误码让 UI 层处理。

全系列结语:

至此,《HarmonyOS 上下文》系列就告一段落了。

📚 充电时间

如果有想加入鸿蒙生态的大佬们,快来加入鸿蒙认证吧!初高级证书还没获取的,点这里:

🔗 HarmonyOS第一课:官方认证培训

如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,欢迎在评论、私信中提出,非常感谢您的支持。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐