HarmonyOS 上下文的使用: 脱离 UI 怎么用 Context?

大家好,我是不想掉发的鸿蒙开发工程师 城中的雾,经过前三期的交流,在 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.ets 的 onCreate 中尽早初始化。

复制代码
// 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第一课:官方认证培训

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

相关推荐
小雨爱测10 小时前
Photoshop 实战入门:从界面认知到第一张成品图
ui·photoshop
贾伟康12 小时前
【共创稿事节】HarmonyOS 7.0 AOT编译优化:从接入到生产验证
harmonyos·arkts·路由·navigation·navdestination
2601_9494800612 小时前
开源鸿蒙平台 CMP 三方库「shadcn」适配全流程
华为·开源·harmonyos
螺丝钉tina14 小时前
[Playwright] [02]常用命令与调试:Debug UI、Mode和 Trace Viewer
开发语言·javascript·测试工具·ui·自动化
Liaiyang6614 小时前
空圈容错视角-HarmonyOS 7 架构体检:从被动容错到主动免疫的真伪验证与生态反推
操作系统·harmonyos·架构设计·ai-agent·端云协同·鸿蒙7·空圈容错
贾伟康14 小时前
【共创稿事节】HarmonyOS 7.0 Native XComponent与FFI:从接入到生产验证
harmonyos·profiler·hitracemeter·性能追踪·时间线
redtank200016 小时前
纯血鸿蒙本地电子书阅读器实测:九格式支持、沉浸阅读与零联网隐私设计
华为·harmonyos
贾伟康16 小时前
【共创稿事节】HarmonyOS 7.0 HiSysEvent系统事件订阅:从接入到生产验证
harmonyos·可观测性·hisyevent·系统事件·后台优化
郝学胜-神的一滴16 小时前
游戏设计:原型与开发 18:Unity入门的第一课
人工智能·程序人生·游戏·ui·unity·游戏引擎
huainingning17 小时前
华为E1专线使用与配置案例
华为