capacitor-angular

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Capacitor with Angular

Capacitor 与 Angular 结合开发

Angular-specific patterns and best practices for Capacitor app development — project structure, services, lifecycle hooks, NgZone integration, and plugin usage.
适用于Capacitor应用开发的Angular专属模式与最佳实践 —— 项目结构、服务、生命周期钩子、NgZone集成及插件使用方法。

Prerequisites

前置条件

  1. Capacitor 6, 7, or 8 app with Angular 16+.
  2. Node.js and npm installed.
  3. Angular CLI installed (
    npm install -g @angular/cli
    ).
  4. For iOS: Xcode installed.
  5. For Android: Android Studio installed.
  1. Capacitor 6、7或8版本搭配Angular 16+的应用。
  2. 已安装Node.js和npm。
  3. 已安装Angular CLI(
    npm install -g @angular/cli
    )。
  4. 开发iOS应用:需安装Xcode。
  5. 开发Android应用:需安装Android Studio。

Agent Behavior

Agent 行为规范

  • Auto-detect before asking. Check the project for
    angular.json
    ,
    package.json
    ,
    capacitor.config.ts
    or
    capacitor.config.json
    , and existing directory structure. Only ask the user when something cannot be detected.
  • Guide step-by-step. Walk the user through the process one step at a time.
  • Adapt to project style. Detect whether the project uses standalone components or NgModule-based architecture and adapt code examples accordingly.
  • 先自动检测,再提问:检查项目中的
    angular.json
    package.json
    capacitor.config.ts
    capacitor.config.json
    文件,以及现有目录结构。仅在无法自动检测到信息时才向用户提问。
  • 分步引导:将流程拆分为多个步骤,逐步引导用户操作。
  • 适配项目架构:检测项目是否使用独立组件(standalone components)或基于NgModule的架构,并据此调整代码示例。

Procedures

操作流程

Step 1: Analyze the Project

步骤1:分析项目

Auto-detect the following by reading project files:
  1. Angular version: Read
    @angular/core
    version from
    package.json
    .
  2. Capacitor version: Read
    @capacitor/core
    version from
    package.json
    . If not present, Capacitor has not been added yet — proceed to Step 2.
  3. Architecture style: Check
    src/main.ts
    for
    bootstrapApplication
    (standalone) vs.
    platformBrowserDynamic().bootstrapModule
    (NgModule). Check
    angular.json
    for further confirmation.
  4. Platforms: Check which directories exist (
    android/
    ,
    ios/
    ).
  5. Capacitor config format: Check for
    capacitor.config.ts
    (TypeScript) or
    capacitor.config.json
    (JSON).
  6. Build output directory: Read
    outputPath
    from
    angular.json
    under
    projects > <project-name> > architect > build > options > outputPath
    . This is needed for Capacitor's
    webDir
    setting.
通过读取项目文件自动检测以下信息:
  1. Angular版本:从
    package.json
    中读取
    @angular/core
    的版本号。
  2. Capacitor版本:从
    package.json
    中读取
    @capacitor/core
    的版本号。若未找到,则说明尚未添加Capacitor,继续执行步骤2。
  3. 架构类型:检查
    src/main.ts
    中的代码,判断是使用
    bootstrapApplication
    (独立组件模式)还是
    platformBrowserDynamic().bootstrapModule
    (NgModule模式)。可结合
    angular.json
    进一步确认。
  4. 目标平台:检查是否存在
    android/
    ios/
    目录。
  5. Capacitor配置格式:检查项目中是
    capacitor.config.ts
    (TypeScript格式)还是
    capacitor.config.json
    (JSON格式)。
  6. 构建输出目录:从
    angular.json
    projects > <项目名称> > architect > build > options > outputPath
    中读取
    outputPath
    值,该路径将用于Capacitor的
    webDir
    配置。

Step 2: Add Capacitor to an Angular Project

步骤2:将Capacitor添加到Angular项目

Skip if
@capacitor/core
is already in
package.json
.
  1. Install Capacitor core and CLI:
    bash
    npm install @capacitor/core
    npm install -D @capacitor/cli
  2. Initialize Capacitor:
    bash
    npx cap init
    When prompted, set the web directory to the Angular build output path detected in Step 1. For Angular 17+ with the application builder, this is typically
    dist/<project-name>/browser
    . For older Angular versions, it is typically
    dist/<project-name>
    .
  3. Verify the
    webDir
    value in the generated
    capacitor.config.ts
    or
    capacitor.config.json
    matches the Angular build output path. If incorrect, update it:
    capacitor.config.ts
    :
    typescript
    import type { CapacitorConfig } from '@capacitor/cli';
    
    const config: CapacitorConfig = {
      appId: 'com.example.app',
      appName: 'my-app',
      webDir: 'dist/my-app/browser',
    };
    
    export default config;
    capacitor.config.json
    :
    json
    {
      "appId": "com.example.app",
      "appName": "my-app",
      "webDir": "dist/my-app/browser"
    }
  4. Build the Angular app and add platforms:
    bash
    ng build
    npm install @capacitor/android @capacitor/ios
    npx cap add android
    npx cap add ios
    npx cap sync
package.json
中已存在
@capacitor/core
,可跳过此步骤。
  1. 安装Capacitor核心库和CLI工具:
    bash
    npm install @capacitor/core
    npm install -D @capacitor/cli
  2. 初始化Capacitor:
    bash
    npx cap init
    当系统提示时,将Web目录设置为步骤1中检测到的Angular构建输出路径。对于使用应用构建器的Angular 17+版本,该路径通常为
    dist/<项目名称>/browser
    ;对于旧版本Angular,路径通常为
    dist/<项目名称>
  3. 验证生成的
    capacitor.config.ts
    capacitor.config.json
    中的
    webDir
    值是否与Angular构建输出路径一致。若不一致,请进行修改:
    capacitor.config.ts
    typescript
    import type { CapacitorConfig } from '@capacitor/cli';
    
    const config: CapacitorConfig = {
      appId: 'com.example.app',
      appName: 'my-app',
      webDir: 'dist/my-app/browser',
    };
    
    export default config;
    capacitor.config.json
    json
    {
      "appId": "com.example.app",
      "appName": "my-app",
      "webDir": "dist/my-app/browser"
    }
  4. 构建Angular应用并添加目标平台:
    bash
    ng build
    npm install @capacitor/android @capacitor/ios
    npx cap add android
    npx cap add ios
    npx cap sync

Step 3: Project Structure

步骤3:项目结构

A Capacitor Angular project has this structure:
my-app/
├── android/                  # Android native project
├── ios/                      # iOS native project
├── src/
│   ├── app/
│   │   ├── app.component.ts
│   │   ├── app.config.ts     # Standalone: app configuration
│   │   ├── app.module.ts     # NgModule: root module
│   │   ├── app.routes.ts     # Routing configuration
│   │   └── services/         # Angular services for Capacitor plugins
│   ├── environments/
│   │   ├── environment.ts
│   │   └── environment.prod.ts
│   ├── index.html
│   └── main.ts
├── angular.json
├── capacitor.config.ts       # or capacitor.config.json
├── package.json
└── tsconfig.json
Key points:
  • The
    android/
    and
    ios/
    directories contain native projects and should be committed to version control.
  • The
    src/
    directory contains the Angular app, which is the web layer of the Capacitor app.
  • Capacitor plugins are called from Angular services or components inside
    src/app/
    .
Capacitor + Angular项目的标准结构如下:
my-app/
├── android/                  # Android原生项目目录
├── ios/                      # iOS原生项目目录
├── src/
│   ├── app/
│   │   ├── app.component.ts
│   │   ├── app.config.ts     # 独立组件模式:应用配置文件
│   │   ├── app.module.ts     # NgModule模式:根模块文件
│   │   ├── app.routes.ts     # 路由配置文件
│   │   └── services/         # 用于封装Capacitor插件的Angular服务目录
│   ├── environments/
│   │   ├── environment.ts
│   │   └── environment.prod.ts
│   ├── index.html
│   └── main.ts
├── angular.json
├── capacitor.config.ts       # 或capacitor.config.json
├── package.json
└── tsconfig.json
关键说明:
  • android/
    ios/
    目录包含原生项目代码,需提交到版本控制系统。
  • src/
    目录包含Angular应用代码,是Capacitor应用的Web层。
  • Capacitor插件需在
    src/app/
    目录下的Angular服务或组件中调用。

Step 4: Using Capacitor Plugins in Angular

步骤4:在Angular中使用Capacitor插件

Capacitor plugins are plain TypeScript APIs. Import and call them directly in Angular components or services.
Capacitor插件是纯TypeScript API,可直接在Angular组件或服务中导入并调用。

Direct Usage in a Component

在组件中直接使用

typescript
import { Component } from '@angular/core';
import { Geolocation } from '@capacitor/geolocation';

@Component({
  selector: 'app-location',
  template: `
    <div>
      <p>Latitude: {{ latitude }}</p>
      <p>Longitude: {{ longitude }}</p>
      <button (click)="getCurrentPosition()">Get Location</button>
    </div>
  `,
  standalone: true,
})
export class LocationComponent {
  latitude: number | null = null;
  longitude: number | null = null;

  async getCurrentPosition() {
    const position = await Geolocation.getCurrentPosition();
    this.latitude = position.coords.latitude;
    this.longitude = position.coords.longitude;
  }
}
typescript
import { Component } from '@angular/core';
import { Geolocation } from '@capacitor/geolocation';

@Component({
  selector: 'app-location',
  template: `
    <div>
      <p>纬度: {{ latitude }}</p>
      <p>经度: {{ longitude }}</p>
      <button (click)="getCurrentPosition()">获取位置信息</button>
    </div>
  `,
  standalone: true,
})
export class LocationComponent {
  latitude: number | null = null;
  longitude: number | null = null;

  async getCurrentPosition() {
    const position = await Geolocation.getCurrentPosition();
    this.latitude = position.coords.latitude;
    this.longitude = position.coords.longitude;
  }
}

Wrapping Plugins in Angular Services (Recommended)

将插件封装到Angular服务中(推荐方案)

Wrapping Capacitor plugins in Angular services provides dependency injection, testability, and a single place to handle platform differences:
typescript
import { Injectable } from '@angular/core';
import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera';
import { Capacitor } from '@capacitor/core';

@Injectable({
  providedIn: 'root',
})
export class CameraService {
  async takePhoto(): Promise<Photo> {
    return Camera.getPhoto({
      quality: 90,
      allowEditing: false,
      resultType: CameraResultType.Uri,
      source: CameraSource.Camera,
    });
  }

  async pickFromGallery(): Promise<Photo> {
    return Camera.getPhoto({
      quality: 90,
      allowEditing: false,
      resultType: CameraResultType.Uri,
      source: CameraSource.Photos,
    });
  }

  isNativePlatform(): boolean {
    return Capacitor.isNativePlatform();
  }
}
Use the service in a component:
typescript
import { Component, inject } from '@angular/core';
import { CameraService } from '../services/camera.service';

@Component({
  selector: 'app-photo',
  template: `
    <button (click)="takePhoto()">Take Photo</button>
    <img *ngIf="photoUrl" [src]="photoUrl" alt="Captured photo" />
  `,
  standalone: true,
})
export class PhotoComponent {
  private cameraService = inject(CameraService);
  photoUrl: string | null = null;

  async takePhoto() {
    const photo = await this.cameraService.takePhoto();
    this.photoUrl = photo.webPath ?? null;
  }
}
将Capacitor插件封装到Angular服务中,可实现依赖注入、可测试性,并统一处理平台差异:
typescript
import { Injectable } from '@angular/core';
import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera';
import { Capacitor } from '@capacitor/core';

@Injectable({
  providedIn: 'root',
})
export class CameraService {
  async takePhoto(): Promise<Photo> {
    return Camera.getPhoto({
      quality: 90,
      allowEditing: false,
      resultType: CameraResultType.Uri,
      source: CameraSource.Camera,
    });
  }

  async pickFromGallery(): Promise<Photo> {
    return Camera.getPhoto({
      quality: 90,
      allowEditing: false,
      resultType: CameraResultType.Uri,
      source: CameraSource.Photos,
    });
  }

  isNativePlatform(): boolean {
    return Capacitor.isNativePlatform();
  }
}
在组件中使用该服务:
typescript
import { Component, inject } from '@angular/core';
import { CameraService } from '../services/camera.service';

@Component({
  selector: 'app-photo',
  template: `
    <button (click)="takePhoto()">拍照</button>
    <img *ngIf="photoUrl" [src]="photoUrl" alt="拍摄的照片" />
  `,
  standalone: true,
})
export class PhotoComponent {
  private cameraService = inject(CameraService);
  photoUrl: string | null = null;

  async takePhoto() {
    const photo = await this.cameraService.takePhoto();
    this.photoUrl = photo.webPath ?? null;
  }
}

Step 5: NgZone Integration for Plugin Event Listeners

步骤5:插件事件监听器的NgZone集成

Capacitor plugin event listeners run outside Angular's
NgZone
execution context. When a plugin listener updates component state, Angular's change detection does not automatically trigger. Wrap the handler logic in
NgZone.run()
to fix this.
Without NgZone (broken — UI does not update):
typescript
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Network, ConnectionStatus } from '@capacitor/network';
import { PluginListenerHandle } from '@capacitor/core';

@Component({
  selector: 'app-network',
  template: `<p>Status: {{ networkStatus }}</p>`,
  standalone: true,
})
export class NetworkComponent implements OnInit, OnDestroy {
  networkStatus = 'Unknown';
  private listenerHandle: PluginListenerHandle | null = null;

  async ngOnInit() {
    // BUG: This callback runs outside NgZone — the template will not update.
    this.listenerHandle = await Network.addListener('networkStatusChange', (status) => {
      this.networkStatus = status.connected ? 'Online' : 'Offline';
    });
  }

  async ngOnDestroy() {
    await this.listenerHandle?.remove();
  }
}
With NgZone (correct — UI updates properly):
typescript
import { Component, NgZone, OnInit, OnDestroy, inject } from '@angular/core';
import { Network, ConnectionStatus } from '@capacitor/network';
import { PluginListenerHandle } from '@capacitor/core';

@Component({
  selector: 'app-network',
  template: `<p>Status: {{ networkStatus }}</p>`,
  standalone: true,
})
export class NetworkComponent implements OnInit, OnDestroy {
  private ngZone = inject(NgZone);
  networkStatus = 'Unknown';
  private listenerHandle: PluginListenerHandle | null = null;

  async ngOnInit() {
    this.listenerHandle = await Network.addListener('networkStatusChange', (status) => {
      this.ngZone.run(() => {
        this.networkStatus = status.connected ? 'Online' : 'Offline';
      });
    });
  }

  async ngOnDestroy() {
    await this.listenerHandle?.remove();
  }
}
Rule: Always use
NgZone.run()
inside Capacitor plugin event listener callbacks that update component or service state bound to templates.
Capacitor插件的事件监听器默认运行在Angular的
NgZone
执行上下文外部。当监听器更新组件状态时,Angular的变更检测机制不会自动触发。需将处理器逻辑包裹在
NgZone.run()
中以解决此问题。
未使用NgZone(存在问题——UI不更新):
typescript
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Network, ConnectionStatus } from '@capacitor/network';
import { PluginListenerHandle } from '@capacitor/core';

@Component({
  selector: 'app-network',
  template: `<p>网络状态: {{ networkStatus }}</p>`,
  standalone: true,
})
export class NetworkComponent implements OnInit, OnDestroy {
  networkStatus = '未知';
  private listenerHandle: PluginListenerHandle | null = null;

  async ngOnInit() {
    // 问题:此回调函数运行在NgZone外部,模板不会更新
    this.listenerHandle = await Network.addListener('networkStatusChange', (status) => {
      this.networkStatus = status.connected ? '在线' : '离线';
    });
  }

  async ngOnDestroy() {
    await this.listenerHandle?.remove();
  }
}
使用NgZone(正确实现——UI正常更新):
typescript
import { Component, NgZone, OnInit, OnDestroy, inject } from '@angular/core';
import { Network, ConnectionStatus } from '@capacitor/network';
import { PluginListenerHandle } from '@capacitor/core';

@Component({
  selector: 'app-network',
  template: `<p>网络状态: {{ networkStatus }}</p>`,
  standalone: true,
})
export class NetworkComponent implements OnInit, OnDestroy {
  private ngZone = inject(NgZone);
  networkStatus = '未知';
  private listenerHandle: PluginListenerHandle | null = null;

  async ngOnInit() {
    this.listenerHandle = await Network.addListener('networkStatusChange', (status) => {
      this.ngZone.run(() => {
        this.networkStatus = status.connected ? '在线' : '离线';
      });
    });
  }

  async ngOnDestroy() {
    await this.listenerHandle?.remove();
  }
}
规则:所有会更新与模板绑定的组件或服务状态的Capacitor插件事件监听器回调,都必须包裹在
NgZone.run(() => { ... })
中。

Step 6: Lifecycle Hook Patterns

步骤6:生命周期钩子模式

Use Angular lifecycle hooks to manage Capacitor plugin listeners. Register listeners in
ngOnInit
and remove them in
ngOnDestroy
to prevent memory leaks.
使用Angular的生命周期钩子管理Capacitor插件监听器,在
ngOnInit
中注册监听器,在
ngOnDestroy
中移除监听器,以避免内存泄漏。

Service-Based Listener Management

基于服务的监听器管理

For app-wide listeners (e.g., network status, app state), use a service initialized at app startup:
typescript
import { Injectable, NgZone, OnDestroy, inject } from '@angular/core';
import { App } from '@capacitor/app';
import { PluginListenerHandle } from '@capacitor/core';
import { BehaviorSubject } from 'rxjs';

@Injectable({
  providedIn: 'root',
})
export class AppStateService implements OnDestroy {
  private ngZone = inject(NgZone);
  private listenerHandle: PluginListenerHandle | null = null;

  private isActiveSubject = new BehaviorSubject<boolean>(true);
  isActive$ = this.isActiveSubject.asObservable();

  constructor() {
    this.initListener();
  }

  private async initListener() {
    this.listenerHandle = await App.addListener('appStateChange', (state) => {
      this.ngZone.run(() => {
        this.isActiveSubject.next(state.isActive);
      });
    });
  }

  async ngOnDestroy() {
    await this.listenerHandle?.remove();
  }
}
Initialize the service at app startup to ensure it runs immediately. In standalone apps, use
APP_INITIALIZER
or inject it in the root component. In NgModule apps, inject it in
AppComponent
:
Standalone (
app.config.ts
):
typescript
import { ApplicationConfig, APP_INITIALIZER } from '@angular/core';
import { AppStateService } from './services/app-state.service';

export const appConfig: ApplicationConfig = {
  providers: [
    {
      provide: APP_INITIALIZER,
      useFactory: (appStateService: AppStateService) => () => {},
      deps: [AppStateService],
      multi: true,
    },
  ],
};
NgModule (
app.component.ts
):
typescript
import { Component } from '@angular/core';
import { AppStateService } from './services/app-state.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
})
export class AppComponent {
  constructor(private appStateService: AppStateService) {}
}
对于全局监听器(如网络状态、应用状态),可使用在应用启动时初始化的服务:
typescript
import { Injectable, NgZone, OnDestroy, inject } from '@angular/core';
import { App } from '@capacitor/app';
import { PluginListenerHandle } from '@capacitor/core';
import { BehaviorSubject } from 'rxjs';

@Injectable({
  providedIn: 'root',
})
export class AppStateService implements OnDestroy {
  private ngZone = inject(NgZone);
  private listenerHandle: PluginListenerHandle | null = null;

  private isActiveSubject = new BehaviorSubject<boolean>(true);
  isActive$ = this.isActiveSubject.asObservable();

  constructor() {
    this.initListener();
  }

  private async initListener() {
    this.listenerHandle = await App.addListener('appStateChange', (state) => {
      this.ngZone.run(() => {
        this.isActiveSubject.next(state.isActive);
      });
    });
  }

  async ngOnDestroy() {
    await this.listenerHandle?.remove();
  }
}
需在应用启动时初始化该服务,确保其立即运行。在独立组件模式应用中,可使用
APP_INITIALIZER
或在根组件中注入;在NgModule模式应用中,可在
AppComponent
中注入:
独立组件模式(
app.config.ts
):
typescript
import { ApplicationConfig, APP_INITIALIZER } from '@angular/core';
import { AppStateService } from './services/app-state.service';

export const appConfig: ApplicationConfig = {
  providers: [
    {
      provide: APP_INITIALIZER,
      useFactory: (appStateService: AppStateService) => () => {},
      deps: [AppStateService],
      multi: true,
    },
  ],
};
NgModule模式(
app.component.ts
):
typescript
import { Component } from '@angular/core';
import { AppStateService } from './services/app-state.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
})
export class AppComponent {
  constructor(private appStateService: AppStateService) {}
}

Step 7: Platform Detection

步骤7:平台检测

Use
Capacitor.isNativePlatform()
and
Capacitor.getPlatform()
to conditionally run native-only code:
typescript
import { Injectable } from '@angular/core';
import { Capacitor } from '@capacitor/core';

@Injectable({
  providedIn: 'root',
})
export class PlatformService {
  isNative(): boolean {
    return Capacitor.isNativePlatform();
  }

  getPlatform(): 'web' | 'ios' | 'android' {
    return Capacitor.getPlatform() as 'web' | 'ios' | 'android';
  }

  isIos(): boolean {
    return Capacitor.getPlatform() === 'ios';
  }

  isAndroid(): boolean {
    return Capacitor.getPlatform() === 'android';
  }

  isWeb(): boolean {
    return Capacitor.getPlatform() === 'web';
  }
}
Use it in components to show/hide native-only features:
typescript
import { Component, inject } from '@angular/core';
import { PlatformService } from '../services/platform.service';

@Component({
  selector: 'app-settings',
  template: `
    @if (platformService.isNative()) {
      <button (click)="openNativeSettings()">Open Device Settings</button>
    }
  `,
  standalone: true,
})
export class SettingsComponent {
  platformService = inject(PlatformService);

  openNativeSettings() {
    // Native-only logic
  }
}
使用
Capacitor.isNativePlatform()
Capacitor.getPlatform()
方法,有条件地执行仅适用于原生平台的代码:
typescript
import { Injectable } from '@angular/core';
import { Capacitor } from '@capacitor/core';

@Injectable({
  providedIn: 'root',
})
export class PlatformService {
  isNative(): boolean {
    return Capacitor.isNativePlatform();
  }

  getPlatform(): 'web' | 'ios' | 'android' {
    return Capacitor.getPlatform() as 'web' | 'ios' | 'android';
  }

  isIos(): boolean {
    return Capacitor.getPlatform() === 'ios';
  }

  isAndroid(): boolean {
    return Capacitor.getPlatform() === 'android';
  }

  isWeb(): boolean {
    return Capacitor.getPlatform() === 'web';
  }
}
在组件中使用该服务,显示或隐藏仅适用于原生平台的功能:
typescript
import { Component, inject } from '@angular/core';
import { PlatformService } from '../services/platform.service';

@Component({
  selector: 'app-settings',
  template: `
    @if (platformService.isNative()) {
      <button (click)="openNativeSettings()">打开设备设置</button>
    }
  `,
  standalone: true,
})
export class SettingsComponent {
  platformService = inject(PlatformService);

  openNativeSettings() {
    // 仅原生平台执行的逻辑
  }
}

Step 8: Deep Link Routing

步骤8:深度链接路由

Handle deep links by mapping Capacitor's
App.addListener('appUrlOpen', ...)
event to Angular Router navigation:
typescript
import { Injectable, NgZone, inject } from '@angular/core';
import { Router } from '@angular/router';
import { App } from '@capacitor/app';

@Injectable({
  providedIn: 'root',
})
export class DeepLinkService {
  private ngZone = inject(NgZone);
  private router = inject(Router);

  constructor() {
    this.initDeepLinkListener();
  }

  private async initDeepLinkListener() {
    await App.addListener('appUrlOpen', (event) => {
      this.ngZone.run(() => {
        const url = new URL(event.url);
        const path = url.pathname;

        // Navigate to the route matching the deep link path.
        // Adjust the path parsing logic to match the app's URL scheme.
        if (path) {
          this.router.navigateByUrl(path);
        }
      });
    });
  }
}
Initialize
DeepLinkService
at app startup (same pattern as Step 6 — via
APP_INITIALIZER
or root component injection).
通过将Capacitor的
App.addListener('appUrlOpen', ...)
事件映射到Angular Router导航,实现深度链接处理:
typescript
import { Injectable, NgZone, inject } from '@angular/core';
import { Router } from '@angular/router';
import { App } from '@capacitor/app';

@Injectable({
  providedIn: 'root',
})
export class DeepLinkService {
  private ngZone = inject(NgZone);
  private router = inject(Router);

  constructor() {
    this.initDeepLinkListener();
  }

  private async initDeepLinkListener() {
    await App.addListener('appUrlOpen', (event) => {
      this.ngZone.run(() => {
        const url = new URL(event.url);
        const path = url.pathname;

        // 导航到与深度链接路径匹配的路由
        // 根据应用的URL协议调整路径解析逻辑
        if (path) {
          this.router.navigateByUrl(path);
        }
      });
    });
  }
}
需在应用启动时初始化
DeepLinkService
(与步骤6的模式相同——通过
APP_INITIALIZER
或根组件注入)。

Step 9: Back Button Handling (Android)

步骤9:Android返回按钮处理

Handle the Android hardware back button using
App.addListener('backButton', ...)
:
typescript
import { Injectable, NgZone, inject } from '@angular/core';
import { Location } from '@angular/common';
import { App } from '@capacitor/app';
import { Capacitor } from '@capacitor/core';

@Injectable({
  providedIn: 'root',
})
export class BackButtonService {
  private ngZone = inject(NgZone);
  private location = inject(Location);

  constructor() {
    if (Capacitor.getPlatform() === 'android') {
      this.initBackButtonListener();
    }
  }

  private async initBackButtonListener() {
    await App.addListener('backButton', ({ canGoBack }) => {
      this.ngZone.run(() => {
        if (canGoBack) {
          this.location.back();
        } else {
          App.exitApp();
        }
      });
    });
  }
}
使用
App.addListener('backButton', ...)
处理Android硬件返回按钮:
typescript
import { Injectable, NgZone, inject } from '@angular/core';
import { Location } from '@angular/common';
import { App } from '@capacitor/app';
import { Capacitor } from '@capacitor/core';

@Injectable({
  providedIn: 'root',
})
export class BackButtonService {
  private ngZone = inject(NgZone);
  private location = inject(Location);

  constructor() {
    if (Capacitor.getPlatform() === 'android') {
      this.initBackButtonListener();
    }
  }

  private async initBackButtonListener() {
    await App.addListener('backButton', ({ canGoBack }) => {
      this.ngZone.run(() => {
        if (canGoBack) {
          this.location.back();
        } else {
          App.exitApp();
        }
      });
    });
  }
}

Step 10: Build and Sync Workflow

步骤10:构建与同步工作流

After making changes to the Angular app, build and sync to native platforms:
bash
ng build
npx cap sync
To run on a device or emulator:
bash
npx cap run android
npx cap run ios
To open the native IDE for advanced configuration or debugging:
bash
npx cap open android
npx cap open ios
For live reload during development:
bash
npx cap run android --livereload --external
npx cap run ios --livereload --external
This starts
ng serve
internally and configures the native app to load from the development server.
修改Angular应用后,需构建并同步到原生平台:
bash
ng build
npx cap sync
在设备或模拟器上运行应用:
bash
npx cap run android
npx cap run ios
打开原生IDE进行高级配置或调试:
bash
npx cap open android
npx cap open ios
开发期间启用热重载:
bash
npx cap run android --livereload --external
npx cap run ios --livereload --external
该命令会内部启动
ng serve
,并配置原生应用从开发服务器加载内容。

Error Handling

错误处理

  • UI not updating from plugin listeners: Wrap the listener callback body in
    NgZone.run(() => { ... })
    . This is the most common Angular-specific issue with Capacitor.
  • webDir
    mismatch
    : If
    npx cap sync
    copies the wrong files, verify that
    webDir
    in
    capacitor.config.ts
    or
    capacitor.config.json
    matches the Angular build output path. For Angular 17+ with the application builder, the path is
    dist/<project-name>/browser
    . For older Angular versions, it is
    dist/<project-name>
    .
  • Plugin not found at runtime: Run
    npx cap sync
    after installing any new plugin. Verify the plugin appears in
    package.json
    dependencies.
  • Memory leaks from listeners: Always remove plugin listeners in
    ngOnDestroy
    . Store the
    PluginListenerHandle
    returned by
    addListener
    and call
    handle.remove()
    on destroy.
  • Deep links not working: Verify the app URL scheme / universal links are configured in the native projects (
    android/app/src/main/AndroidManifest.xml
    for Android,
    ios/App/App/Info.plist
    and associated domain entitlement for iOS). Verify
    DeepLinkService
    is initialized at app startup.
  • Back button closes app unexpectedly: Ensure the back button listener checks
    canGoBack
    before calling
    App.exitApp()
    . Only exit when there is no navigation history.
  • Build output empty after
    ng build
    : Verify the
    outputPath
    in
    angular.json
    is correct. For Angular 17+, the default changed to
    dist/<project-name>/browser
    with the application builder.
  • 插件监听器导致UI不更新:将监听器回调逻辑包裹在
    NgZone.run(() => { ... })
    中,这是Angular与Capacitor结合开发时最常见的问题。
  • webDir
    路径不匹配
    :若
    npx cap sync
    复制了错误的文件,请验证
    capacitor.config.ts
    capacitor.config.json
    中的
    webDir
    值是否与Angular构建输出路径一致。对于使用应用构建器的Angular 17+版本,路径为
    dist/<项目名称>/browser
    ;旧版本Angular的路径为
    dist/<项目名称>
  • 运行时找不到插件:安装新插件后需执行
    npx cap sync
    ,并验证插件已添加到
    package.json
    的依赖中。
  • 监听器导致内存泄漏:务必在
    ngOnDestroy
    中移除插件监听器,保存
    addListener
    返回的
    PluginListenerHandle
    对象,并在销毁时调用
    handle.remove()
  • 深度链接无法工作:验证原生项目中是否配置了应用URL协议/通用链接(Android为
    android/app/src/main/AndroidManifest.xml
    ,iOS为
    ios/App/App/Info.plist
    及关联域名权限),同时确保
    DeepLinkService
    已在应用启动时初始化。
  • 返回按钮意外关闭应用:确保返回按钮监听器在调用
    App.exitApp()
    前检查
    canGoBack
    值,仅当无导航历史时才退出应用。
  • ng build
    后构建输出为空
    :验证
    angular.json
    中的
    outputPath
    值是否正确。Angular 17+版本默认使用应用构建器,路径为
    dist/<项目名称>/browser

Related Skills

相关技能

  • capacitor-app-creation
    — Create a new Capacitor app from scratch.
  • capacitor-app-development
    — General Capacitor development guidance not specific to Angular.
  • capacitor-plugins
    — Install and configure Capacitor plugins from official and community sources.
  • capacitor-react
    — React-specific patterns and best practices for Capacitor app development.
  • ionic-angular
    — Ionic Framework with Angular (UI components, navigation, theming on top of Capacitor).
  • capacitor-app-upgrades
    — Upgrade a Capacitor app to a newer major version.
  • capacitor-app-creation
    —— 从零创建新的Capacitor应用。
  • capacitor-app-development
    —— 不针对Angular的通用Capacitor开发指导。
  • capacitor-plugins
    —— 安装并配置官方及社区提供的Capacitor插件。
  • capacitor-react
    —— 适用于Capacitor应用开发的React专属模式与最佳实践。
  • ionic-angular
    —— Ionic Framework与Angular结合开发(在Capacitor基础上提供UI组件、导航、主题定制功能)。
  • capacitor-app-upgrades
    —— 将Capacitor应用升级到新版本。