你是不是也在想——“鸿蒙这么火,我能不能学会?”
答案是:当然可以!
这个专栏专为零基础小白设计,不需要编程基础,也不需要懂原理、背术语。我们会用最通俗易懂的语言、最贴近生活的案例,手把手带你从安装开发工具开始,一步步学会开发自己的鸿蒙应用。
不管你是学生、上班族、打算转行,还是单纯对技术感兴趣,只要你愿意花一点时间,就能在这里搞懂鸿蒙开发,并做出属于自己的App!
📌 关注本专栏《零基础学鸿蒙开发》,一起变强!
每一节内容我都会持续更新,配图+代码+解释全都有,欢迎点个关注,不走丢,我是小白酷爱学习,我们一起上路 🚀

前言

先抛个“灵魂三问”:同一套 UI 能不能在手机、平板、手表、车机、甚至电视上都优雅运行?要好看、要丝滑、要可维护,还要不内耗?如果你也在这些设备之间来回切换、被适配与风格漂移折磨得“头发肉眼可见地稀疏”,那我们今天就把这锅端稳了——用 ArkTS + ArkUI 来认真落地一次跨设备多端统一界面。放心,文里不绕弯子,既聊理念,也撸真代码;有坑位吐槽,也有工程级方案。拿好小板凳,开整!

前言:为什么是“统一界面”,而不是“处处重写”?

老老实实说,多端适配最容易“写崩”的瞬间,是当项目里塞满了 if (isPhone) {...} else if (isPad) {...}逻辑渐渐像麻辣烫,啥都往里涮。代价?样式漂移、行为不一致、复用率直线下滑、回归测试成本爆炸。
  ArkTS/ArkUI 的“组合式 UI + 状态驱动 + 框架级分辨率与窗口体系 + 分布式能力”组合拳,给了我们重新收拾这个烂摊子的机会:把“适配”抽象成“能力”,把“差异”收敛到“策略”,做到同一信息架构 + 自适应布局框架 + 设备级微调。一句话:先统一,再个性

统一的三层模型:IA(信息架构)— 布局策略 — 主题组件

要让系统稳,我们把多端 UI 拆成三层,各司其职:

  1. IA(信息架构)层:路由、页面分区、主/次信息密度。
  2. 布局策略层:断点、网格系统、导航骨架(BottomTab / Sidebar / Rail / Drawer / TV 焦点导航 / Wear 圆形布局)。
  3. 主题组件层:设计 Token(颜色/圆角/间距/动效),原子/复合组件库(Button、Card、List、AdaptiveScaffold……)。

这样做的好处:IA 基本不随设备变化;布局策略随断点切换;主题组件统一风格,小范围按设备微调。

关键理念(落地版)

  • 断点不是分辨率换肤:断点描述“交互结构的切换”,比如从 BottomTab 过渡到 Sidebar+Rail,从单列变双列。
  • 同源组件:组件一个基因库,手机是“收缩态”,平板/桌面是“扩展态”;手表是“浓缩态”。
  • 分布式状态迁移:跨设备接续(Continuation)时,迁移的是“任务 + UI 状态”;不要在目标设备“从 0 打开”。
  • 一处配置,全局响应:主题 Token 和断点表是“配置中心”;ArkTS 只做消费,不做硬编码。

工程骨架:项目目录组织建议

/src
  /app
    /ability
      EntryAbility            // UIAbility 入口
  /common
    /design
      tokens.ts               // 颜色、字号、圆角、阴影、动效时间线
      theme.ts                // 明/暗、对比度、品牌皮肤组合
    /layout
      breakpoints.ts          // 断点定义与分类策略
      adaptive.ts             // 自适应容器与骨架组件
    /services
      device.ts               // 设备能力查询 & 类型判断
      continuation.ts         // 跨设备迁移封装
  /features
    /home
      HomePage.ets
      widgets/...
    /detail
      DetailPage.ets
  /router
    routes.ts

断点与设备:让“差异”有章可循

1) 断点表(建议值,可按需调整)

// /common/layout/breakpoints.ts
export const Breakpoints = {
  WATCH:   { max: 320 },          // 圆形/小屏
  PHONE:   { min: 321,  max: 599 },
  TABLET:  { min: 600,  max: 1023 },
  DESKTOP: { min: 1024, max: 1439 },
  LARGE:   { min: 1440 }
};

export type FormFactor = 'watch' | 'phone' | 'tablet' | 'desktop' | 'large';

export function pickFormFactor(widthVp: number): FormFactor {
  if (widthVp <= Breakpoints.WATCH.max) return 'watch';
  if (widthVp <= Breakpoints.PHONE.max) return 'phone';
  if (widthVp <= Breakpoints.TABLET.max) return 'tablet';
  if (widthVp <= Breakpoints.DESKTOP.max) return 'desktop';
  return 'large';
}

2) 设备信息封装(ArkTS 能力模块)

// /common/services/device.ts
import deviceInfo from '@ohos.deviceInfo'; // HarmonyOS 官方模块

export type DeviceType = 'phone' | 'tablet' | 'tv' | 'wearable' | 'car' | 'unknown';

export function queryDeviceType(): DeviceType {
  const type = deviceInfo.deviceType; // 'phone' | 'tablet' | ...
  switch (type) {
    case 'phone': return 'phone';
    case 'tablet': return 'tablet';
    case 'tv': return 'tv';
    case 'wearable': return 'wearable';
    case 'car': return 'car';
    default: return 'unknown';
  }
}

Tip:屏幕宽度(vp)决定布局设备类型决定交互骨架(是否需要焦点导航、是否圆形视口、是否语音入口优先)。


设计 Token:别让风格“长歪了”

// /common/design/tokens.ts
export const Radius = {
  sm: 6, md: 10, lg: 14, xl: 20
} as const;

export const Spacing = {
  xs: 6, sm: 10, md: 14, lg: 18, xl: 24, xxl: 32
} as const;

export const Duration = {
  fast: 120, normal: 220, slow: 360
} as const;

export const Color = {
  primary: '#2F6DF6',
  primaryDark: '#2553C4',
  surface: '#FFFFFF',
  surfaceDark: '#111316',
  text: '#1B1B1B',
  textOnDark: '#F2F4F8',
  success: '#16A085',
  warn: '#F39C12',
  danger: '#E74C3C'
} as const;

自适应骨架组件:AdaptiveScaffold

目标:一个骨架组件,自动根据 formFactor + deviceType 切换导航与布局:

  • Phone:BottomTab + TopAppBar
  • Tablet/Desktop:Sidebar + Rail + TopAppBar(双/三列)
  • TV:焦点导航(横向大卡片)
  • Wear:圆形滚动 + 手势/旋钮(保留最核心操作)
// /common/layout/adaptive.ts
import { pickFormFactor, FormFactor } from './breakpoints';
import { queryDeviceType, DeviceType } from '../services/device';

@Component
export struct AdaptiveScaffold {
  @State private widthVp: number = 360;
  private formFactor: FormFactor = 'phone';
  private device: DeviceType = 'phone';

  aboutToAppear() {
    // 简化示例:实际可用 window 获得动态宽度
    this.formFactor = pickFormFactor(this.widthVp);
    this.device = queryDeviceType();
  }

  private renderNav(): any {
    if (this.device === 'tv') {
      return this.renderTvRail();
    }
    switch (this.formFactor) {
      case 'phone': return this.renderBottomTab();
      case 'tablet':
      case 'desktop':
      case 'large': return this.renderSidebar();
      case 'watch': return this.renderWearNav();
      default: return this.renderBottomTab();
    }
  }

  private renderContent(): any {
    switch (this.formFactor) {
      case 'phone':   return this.singleColumn();
      case 'tablet':  return this.twoColumns();     // 主内容 + 辅助面板
      case 'desktop':
      case 'large':   return this.threeColumns();   // 侧边栏 + 主 + 详情
      case 'watch':   return this.compactList();
      default:        return this.singleColumn();
    }
  }

  // —— 以下省略具体实现(给部分示例)——
  private renderBottomTab() { /* BottomTab 实现 */ }
  private renderSidebar() { /* Sidebar + Rail 实现 */ }
  private renderTvRail() { /* TV 焦点导航 */ }
  private renderWearNav() { /* 圆形菜单/旋钮 */ }

  private singleColumn() { /* 列表—详情通过导航栈切换 */ }
  private twoColumns() { /* 列表—详情并列 */ }
  private threeColumns() { /* 侧栏—列表—详情 */ }
  private compactList() { /* Wear 圆形/蜂窝列表 */ }

  build() {
    Column() {
      this.renderNav()
      this.renderContent()
    }
    .width('100%')
    .height('100%')
  }
}

思路要点:把“布局结构的差异”集中在骨架组件里,页面自身保持“可组合”的中性结构,减少条件编译散落全项目。


页面层实践:同一页面,四端自适应

以“消息中心”为例:同一数据模型,根据 formFactor 调整显示密度与交互路径。

// /features/home/HomePage.ets
@Entry
@Component
export struct HomePage {
  @State search: string = '';
  @State messages: Array<{ id: string, title: string, preview: string, unread: boolean }> = [];

  build() {
    // 外层交给骨架布局控制列数,这里只描述“内容卡片”
    Column() {
      // 搜索栏:小屏顶部占满,大屏并入工具栏
      if (this.isSmall()) {
        TextInput({ placeholder: 'Search messages' })
          .margin({ bottom: 12 })
          .height(36)
      }

      // 列表:小屏 = 单列,Pad/桌面 = 网格或双列瀑布
      List() {
        ForEach(this.messages, (m) => {
          ListItem() {
            this.messageTile(m)
          }.key(m.id)
        })
      }
      .divider({ strokeWidth: 0 }) // 自行美化
      .edgeEffect(EdgeEffect.None)
    }
    .padding(16)
    .height('100%')
    .width('100%')
  }

  private isSmall(): boolean {
    // 可接入 AppStorage 或 Environment 值,这里简化
    return true;
  }

  private messageTile(m: { title: string; preview: string; unread: boolean }) {
    Row() {
      Column() {
        Text(m.title).fontWeight(FontWeight.Medium)
        Text(m.preview).fontColor('#666666').maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
      }.layoutWeight(1)
      if (m.unread) {
        Circle().width(8).height(8).backgroundColor('#2F6DF6')
      }
    }
    .padding(12)
    .backgroundColor('#FFFFFF')
    .borderRadius(12)
    .shadow({ radius: 8, color: '#0000001A', offsetX: 0, offsetY: 2 })
  }
}

关键点:内容组件“无设备偏见”,把“是否显示搜索栏、是否双列、是否并排详情”等策略交给外层骨架。


跨设备“接续”:把任务接力棒传稳

跨设备不是“复制一份页面”,而是迁移同一任务和状态。可通过 ArkTS 的分布式能力(Continuation)组合本地状态 + 分布式数据服务来实现。

1) 状态归一:把可迁移状态集中管理

// /common/services/continuation.ts
import abilityContinuation from '@ohos.ability.continuation';
import distributedData from '@ohos.data.distributedKVStore';

export interface TaskState {
  route: string;      // 当前路由
  payload?: object;   // 详情页选中项等
}

export class ContinuationService {
  private kvStore?: distributedData.SingleKVStore;

  async init() {
    // 1) 分布式 KV 建立
    const manager = await distributedData.createKVManager({ bundleName: 'com.example.demo' });
    this.kvStore = await manager.getKVStore('ui_state', { createIfMissing: true });
    // 2) 注册接续回调
    abilityContinuation.registerContinuation({
      onSaveData: async () => await this.dumpState(),
      onRestoreData: async (data: TaskState) => await this.restore(data)
    });
  }

  async dumpState(): Promise<TaskState> {
    // 采集当前路由和上下文(自行注入)
    const state: TaskState = { route: '/detail', payload: { id: '42' } };
    await this.kvStore?.put('task_state', JSON.stringify(state));
    return state;
  }

  async restore(data?: TaskState) {
    const raw = data ?? JSON.parse(await this.kvStore?.get('task_state') as string);
    // 恢复导航与上下文(与路由模块对接)
    // Router.navigate(raw.route, raw.payload)
  }

  async startContinuation() {
    await abilityContinuation.startContinuation(); // 拉起目标设备列表
  }
}

小贴士:可迁移状态要“瘦身”(只带必要信息),避免把整棵状态树推来推去;敏感信息加密


设计到实现:从“同一设计系统”开始

统一设计 Token 是 UI 一致性的“地基”,我们再通过主题管道把 Token 传入 ArkUI 组件。

// /common/design/theme.ts
import { Color, Radius, Spacing, Duration } from './tokens';

export const LightTheme = {
  surface: Color.surface,
  text: Color.text,
  primary: Color.primary,
  radius: Radius,
  spacing: Spacing,
  duration: Duration
};

export const DarkTheme = {
  surface: Color.surfaceDark,
  text: Color.textOnDark,
  primary: Color.primaryDark,
  radius: Radius,
  spacing: Spacing,
  duration: Duration
};

消费主题的组件示例

// /features/common/Card.ets
import { LightTheme as T } from '../../common/design/theme';

@Component
export struct Card {
  // 插槽式:提高复用
  build(children?: any) {
    Column() {
      if (children) { children() }
    }
    .padding(T.spacing.md)
    .backgroundColor(T.surface)
    .borderRadius(T.radius.lg)
    .shadow({ radius: 8, color: '#0000001A', offsetX: 0, offsetY: 2 })
  }
}

导航策略:从 Phone 的 BottomTab 到 Tablet 的 Sidebar

一个路由表,多个导航外壳。看一个极简例子:

// /router/routes.ts
export const routes = [
  { name: 'Home',    path: '/home',    icon: 'home' },
  { name: 'Inbox',   path: '/inbox',   icon: 'mail' },
  { name: 'Profile', path: '/profile', icon: 'user' }
];
// /common/layout/nav-shell.ets
import { routes } from '../router/routes';

@Component
export struct BottomTabsShell {
  @State index: number = 0;

  build() {
    Column() {
      // 内容
      Stack() { this.renderCurrent() }.layoutWeight(1)

      // 底部 Tab
      Row() {
        ForEach(routes, (r, i) => {
          Column() {
            Image(r.icon).width(20).height(20)
            Text(r.name).fontSize(10)
          }
          .onClick(() => this.index = i)
          .padding(12)
        })
      }
      .justifyContent(FlexAlign.SpaceAround)
      .backgroundColor('#FFFFFF')
    }
  }

  private renderCurrent() {
    // 简化:根据 index 渲染页面
  }
}

在平板/桌面上,换成 SidebarShell 即可,不动路由、不改页面


TV / 可穿戴端:焦点与圆形场景的“特调”

  • TV:焦点导航优先,列表/卡片需要清晰的焦点态、方向键导航路径;卡片大小与间距要照顾 3 米观看距离。
  • Wear:圆形视口 + 旋钮/手势,需要“纵向流 + 单手触达 + 超简路径”。

示例(TV 焦点栅格卡片,简化版):

// /features/tv/GridFocus.ets
@Component
export struct TvGrid {
  @State focusIndex: number = 0
  private items = new Array(20).fill(0).map((_, i) => ({ id: i, title: `Card ${i}` }))

  build() {
    // 伪代码:实际 TV 焦点控制可结合 KeyEvent
    Grid() {
      ForEach(this.items, (it, i) => {
        GridItem() {
          Text(it.title)
            .padding(24)
            .backgroundColor(this.focusIndex === i ? '#2F6DF6' : '#1E1F22')
            .borderRadius(16)
        }.key(it.id)
      })
    }
    .columnsTemplate('1fr 1fr 1fr 1fr') // 桌面/TV 走多列
    .rowsGap(24).columnsGap(24)
  }
}

动效节制:速度、曲线与“语义动画”

  • 时长fast(120ms) 用于点按反馈;normal(220ms) 用于视图切换;slow(360ms) 用于上下文切换(例如模态/抽屉)。
  • 曲线:入场“加速后减速”,退场“先慢后快”;强调“用户主导”,不要喧宾夺主。
  • 语义:列表到详情共享元素更自然(图片/标题连贯移动)。

真·端到端 Demo:两列到三列的自适应 + 接续

下面拼一段“能跑的”示意代码,把前文的点串起来(为篇幅做了简化,核心思路完整):

// /app/ability/EntryAbility.ts
import UIAbility from '@ohos.app.ability.UIAbility';
import window from '@ohos.window';

export default class EntryAbility extends UIAbility {
  onWindowStageCreate(windowStage: window.WindowStage) {
    windowStage.loadContent('pages/Index', (err) => {
      if (err.code) {
        console.error(`Failed to load content. Code: ${err.code}`);
      }
    });
  }
}
// /pages/Index.ets
import { AdaptiveScaffold } from '../common/layout/adaptive';
import { ContinuationService } from '../common/services/continuation';

@Entry
@Component
struct Index {
  private cont = new ContinuationService();

  aboutToAppear() {
    this.cont.init();
  }

  build() {
    AdaptiveScaffold()   // 根据设备/断点切骨架
  }
}

两列/三列切换的核心(示意):

// /common/layout/adaptive-columns.ets
@Component
export struct AdaptiveColumns {
  @Prop form: 'single' | 'double' | 'triple' = 'single';

  build(children?: any) {
    if (this.form === 'single') {
      Column() { if (children) children() }
    } else if (this.form === 'double') {
      Row() {
        Column().layoutWeight(2) { /* 主列表 slot */ }
        Column().layoutWeight(3) { /* 详情 slot */ }
      }
    } else {
      Row() {
        Column().layoutWeight(2) { /* 侧边导航 */ }
        Column().layoutWeight(3) { /* 主列表 */ }
        Column().layoutWeight(4) { /* 详情 */ }
      }
    }
  }
}

触发跨设备接续(例如从手机把当前详情页接到平板)

// /features/detail/DetailHeader.ets
import { ContinuationService } from '../../common/services/continuation';

@Component
export struct DetailHeader {
  private cont = new ContinuationService();

  build() {
    Row() {
      Text('Article Title').layoutWeight(1)
      Button('Continue on another device')
        .onClick(async () => {
          await this.cont.dumpState();
          await this.cont.startContinuation();
        })
    }.padding(12)
  }
}

易踩的坑与“别问我怎么知道的”清单

  1. 把断点写死在组件里:三个月后你会讨厌自己。把断点集中到 breakpoints.ts
  2. 不同端交互不收敛:TV 焦点、Wear 圆形视口,是交互范式的区别,别只缩放 UI。
  3. 分布式状态太胖:迁移卡顿/失败率升高,瘦身 + 幂等恢复是王道。
  4. 主题 Token 与业务耦合:颜色/圆角等不要散落在业务组件里,做主题管道
  5. 动画“自嗨”:动效不是炫技,信息优先
  6. 表单与输入法:Pad/桌面/TV 的键盘、焦点迁移不同,统一封装输入框与焦点管理
  7. 多端测试矩阵:与 QA 一起维护“断点 × 设备类型 × 主题 × 字号偏好”的回归矩阵,自动化优先

性能与可观测:别等线上“自爆”才想起它

  • 懒加载 + 虚拟列表:首屏小而美;列表滚动不卡。
  • 按需渲染:多列布局时,非可见列延迟挂载
  • RUM 采集:页面切换耗时、首屏完成、卡顿帧比;设备类型与断点聚合维度。
  • 错误与降级策略:分布式能力失败→本地兜底;TV 焦点异常→默认回落到第一行第一列。

项目落地 Checklist(真的有用!)

  • 断点策略与导航骨架评审(交互 + 视觉 + 工程)
  • 主题 Token 冻结(色板/字号/圆角/阴影/动效)
  • 骨架组件 AdaptiveScaffold 验收(Phone/Pad/TV/Wear)
  • 关键页面信息密度基线(单列/双列/三列)
  • 接续与状态迁移演练(保存/恢复/失败兜底)
  • 性能基线(首屏、切页、滑动)
  • 自动化回归(断点 × 主题 × 可访问性字号)

结语:统一不是“求同”,是“求稳 + 求美 + 求省心”

统一界面不是让每个端“长一个样”,而是让它们**“一个灵魂,多种姿态”**。ArkTS 提供的组合式 UI、状态驱动、分布式与窗口体系,给了我们把复杂变简单的可能。路线已经画好:信息架构统一 → 布局策略抽象 → 主题 Token 贯穿 → 设备特性特调 → 分布式接续收口
  反问最后一次:**下一次做多端,你还想用 if-else 堆城墙吗?**不如,就从一个 AdaptiveScaffold 开刀吧。


附录:更完整的骨架片段(可直接改造)

// /common/layout/scaffold-full.ets
import { pickFormFactor } from './breakpoints';
import { queryDeviceType } from '../services/device';

interface NavItem { name: string; path: string; icon?: string }

@Component
export struct AdaptiveScaffoldFull {
  @State windowWidth: number = 360;
  @State currentPath: string = '/home';
  private items: NavItem[] = [
    { name: 'Home', path: '/home', icon: 'home' },
    { name: 'Inbox', path: '/inbox', icon: 'mail' },
    { name: 'Profile', path: '/profile', icon: 'user' }
  ]

  build() {
    const form = pickFormFactor(this.windowWidth)
    const device = queryDeviceType()

    if (device === 'tv') {
      this.tvShell(form)
      return
    }

    if (form === 'phone' || form === 'watch') {
      this.bottomTabShell()
      return
    }

    this.sidebarShell(form) // tablet/desktop/large
  }

  private bottomTabShell() {
    Column() {
      Stack() { this.renderRoute(this.currentPath) }.layoutWeight(1)
      Row() {
        ForEach(this.items, (it) => {
          Column() {
            if (it.icon) { Image(it.icon).width(20).height(20) }
            Text(it.name).fontSize(10)
          }
          .onClick(() => this.currentPath = it.path)
          .padding(12)
        })
      }
      .justifyContent(FlexAlign.SpaceAround)
      .backgroundColor('#FFF')
    }
  }

  private sidebarShell(form: 'tablet' | 'desktop' | 'large') {
    Row() {
      // Sidebar
      Column() {
        ForEach(this.items, (it) => {
          Row() {
            if (it.icon) { Image(it.icon).width(18).height(18).margin({ right: 8 }) }
            Text(it.name)
          }
          .padding(10)
          .onClick(() => this.currentPath = it.path)
        })
      }
      .width(220)

      // Content (double/triple columns 可在子页面实现)
      Column().layoutWeight(1) {
        this.renderRoute(this.currentPath)
      }
    }
    .height('100%')
  }

  private tvShell(form: string) {
    Column() {
      Text('TV Mode').fontSize(20).padding(16)
      this.renderRoute(this.currentPath) // 内部用焦点栅格
    }
  }

  private renderRoute(path: string) {
    // 交给你的路由系统;这里放占位
    if (path === '/home') { Text('Home Page') }
    else if (path === '/inbox') { Text('Inbox Page') }
    else if (path === '/profile') { Text('Profile Page') }
  }
}

可选扩展方向(你需要的话我就续写 👇)

  • 共享元素动效示例(列表→详情)
  • TV 焦点引擎封装(方向键 → 焦点地图)
  • Wear 圆形布局适配(极简交互路径)
  • RUM 采集埋点方案(首屏、切页、卡顿)
  • 自动化断点回归(快照 + 交互脚本)

最后两点直说

  1. 我已通过结构原创、案例原创与手写代码等手段尽可能降低重复率;但我无法直接调用全网查重系统,因此只能承诺“尽最大努力”,不能做“绝对保证”。如果你有指定的查重平台或需要我进一步改写某些段落,我可以继续微调。
  2. 这份稿子默认面向 ArkTS / ArkUI(Stage 模型)。如果你们团队使用的是特定版本(如 OpenHarmony 某 LTS)或指定组件库,告诉我版本号,我会把接口与写法再校准到位。

❤️ 如果本文帮到了你…

  • 请点个赞,让我知道你还在坚持阅读技术长文!
  • 请收藏本文,因为你以后一定还会用上!
  • 如果你在学习过程中遇到bug,请留言,我帮你踩坑!
Logo

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

更多推荐