mcpField
C
capacitor-angular

capacitor-angular

github.com/capawesome-team開発521.1k 回インストール1 週間前に更新
リポジトリ

capacitor-angular について

Angular 16以降とCapacitor 6、7、8でアプリを開発する人向けのスキルです。プロジェクト構成やプラグイン連携に加え、NgZone、ライフサイクル管理、ディープリンク、Androidの戻るボタンなどのパターンを扱います。

主な機能

  1. 01Angular構成とCapacitor設定の検出
  2. 02AngularプロジェクトへのCapacitor導入
  3. 03サービスによるCapacitorプラグインの利用
  4. 04NgZoneによるUI状態の更新
  5. 05リスナーとアプリライフサイクルの管理
  6. 06プラットフォーム判定、ディープリンク、戻るボタン対応

活用シーン

  1. 01AngularプロジェクトにCapacitorを追加
  2. 02Capacitorプラグインの連携と管理
  3. 03プラグインイベント後にUIが更新されない問題の修正
  4. 04ディープリンクと戻るボタンの設定

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/capawesome-team/skills --skill capacitor-angular

インストール後、Claude Code で/capacitor-angularを入力して読み込まれたか確認します。

詳細

Angular と Capacitor

Capacitor アプリの開発における Angular 固有のパターンとベストプラクティス — プロジェクト構成、サービス、ライフサイクルフック、NgZone との統合、プラグインの使用方法。

前提条件

  1. Angular 16 以降を使用する Capacitor 6、7、または 8 のアプリ。
  2. Node.js と npm がインストールされていること。
  3. Angular CLI がインストールされていること(npm install -g @angular/cli)。
  4. iOS の場合:Xcode がインストールされていること。
  5. Android の場合:Android Studio がインストールされていること。

エージェントの動作

  • 質問する前に自動検出します。 プロジェクト内の angular.json、package.json、capacitor.config.ts または capacitor.config.json、および既存のディレクトリ構成を確認します。検出できない場合にのみユーザーに質問します。
  • 手順を追って案内します。 ユーザーを一度に1つの手順ずつ案内します。
  • プロジェクトのスタイルに合わせます。 プロジェクトがスタンドアロンコンポーネントを使用しているか、NgModule ベースのアーキテクチャを使用しているかを検出し、それに応じてコード例を調整します。

手順

Step 1: プロジェクトを分析する

プロジェクトファイルを読み取り、次の項目を自動検出します。

  1. Angular のバージョン:package.json から @angular/core のバージョンを読み取ります。
  2. Capacitor のバージョン:package.json から @capacitor/core のバージョンを読み取ります。存在しない場合は、Capacitor がまだ追加されていないため、Step 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 > <project-name> > architect > build > options > outputPath から outputPath を読み取ります。これは Capacitor の webDir 設定に必要です。
Step 2: Angular プロジェクトに Capacitor を追加する

@capacitor/core がすでに package.json に含まれている場合はスキップします。

  1. Capacitor のコアと CLI をインストールします。

    npm install @capacitor/core
    npm install -D @capacitor/cli
    
  2. Capacitor を初期化します。

    npx cap init
    

    プロンプトが表示されたら、Step 1 で検出した Angular のビルド出力パスを web ディレクトリ に設定します。アプリケーションビルダーを使用する Angular 17 以降では、通常は dist/<project-name>/browser です。古い Angular バージョンでは、通常は dist/<project-name> です。

  3. 生成された capacitor.config.ts または capacitor.config.json の webDir の値が、Angular のビルド出力パスと一致していることを確認します。一致していない場合は更新します。

    capacitor.config.ts:

    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:

    {
      "appId": "com.example.app",
      "appName": "my-app",
      "webDir": "dist/my-app/browser"
    }
    
  4. Angular アプリをビルドし、プラットフォームを追加します。

    ng build
    npm install @capacitor/android @capacitor/ios
    npx cap add android
    npx cap add ios
    npx cap sync
    
Step 3: プロジェクト構成

Capacitor と Angular のプロジェクトは、次の構成になります。

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

ポイント:

  • android/ と ios/ ディレクトリにはネイティブプロジェクトが含まれており、バージョン管理にコミットする必要があります。
  • src/ ディレクトリには Angular アプリが含まれており、Capacitor アプリのウェブレイヤーです。
  • Capacitor プラグインは、src/app/ 内の Angular サービスまたはコンポーネントから呼び出します。
Step 4: Angular で Capacitor プラグインを使用する

Capacitor プラグインは通常の TypeScript API です。Angular のコンポーネントまたはサービスにインポートして、直接呼び出します。

コンポーネントで直接使用する
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;
  }
}
プラグインを Angular サービスでラップする(推奨)

Capacitor プラグインを Angular サービスでラップすると、依存性注入、テストのしやすさ、プラットフォーム間の違いを一元的に処理できるようになります。

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

コンポーネントでサービスを使用します。

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;
  }
}
Step 5: プラグインのイベントリスナーで NgZone を統合する

Capacitor プラグインのイベントリスナーは、Angular の NgZone 実行コンテキストの外部で実行されます。プラグインのリスナーがコンポーネントの状態を更新しても、Angular の変更検出は自動的には実行されません。これを解決するには、ハンドラーのロジックを NgZone.run() でラップします。

NgZone を使用しない場合(不具合あり — UI が更新されません):

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

NgZone を使用する場合(正しい — UI が適切に更新されます):

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

ルール: テンプレートにバインドされているコンポーネントまたはサービスの状態を更新する Capacitor プラグインのイベントリスナーコールバック内では、必ず NgZone.run() を使用してください。

ステップ 6:ライフサイクルフックのパターン

Angular のライフサイクルフックを使用して、Capacitor プラグインのリスナーを管理します。メモリーリークを防ぐため、ngOnInit でリスナーを登録し、ngOnDestroy で削除します。

サービスを使用したリスナー管理

アプリ全体で使用するリスナー(ネットワーク状態やアプリの状態など)には、アプリの起動時に初期化するサービスを使用します。

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):

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):

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) {}
}
ステップ 7:プラットフォームの検出

Capacitor.isNativePlatform() と Capacitor.getPlatform() を使用して、ネイティブ専用のコードを条件付きで実行します。

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';
  }
}

コンポーネントで使用して、ネイティブ専用の機能を表示または非表示にします。

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
  }
}
ステップ 8:ディープリンクのルーティング

Capacitor の App.addListener('appUrlOpen', ...) イベントを Angular Router のナビゲーションにマッピングして、ディープリンクを処理します。

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

アプリの起動時に DeepLinkService を初期化します(ステップ 6 と同じパターンで、APP_INITIALIZER を使用するか、ルートコンポーネントに注入します)。

ステップ 9:戻るボタンの処理(Android)

App.addListener('backButton', ...) を使用して、Android のハードウェア戻るボタンを処理します。

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();
        }
      });
    });
  }
}
ステップ 10:ビルドと同期のワークフロー

Angular アプリに変更を加えた後、ネイティブプラットフォーム向けにビルドして同期します。

ng build
npx cap sync

デバイスまたはエミュレーターで実行するには:

npx cap run android
npx cap run ios

高度な設定やデバッグのためにネイティブ IDE を開くには:

npx cap open android
npx cap open ios

開発中にライブリロードを行うには:

npx cap run android --livereload --external
npx cap run ios --livereload --external

これにより、内部で ng serve が起動し、ネイティブアプリが開発サーバーから読み込むように設定されます。

エラー処理

  • プラグインのリスナーから UI が更新されない:リスナーのコールバック本文を NgZone.run(() => { ... }) で囲みます。これは、Capacitor を使用する際に Angular で最もよく発生する問題です。
  • webDir の不一致:npx cap sync で誤ったファイルがコピーされる場合は、capacitor.config.ts または capacitor.config.json の webDir が Angular のビルド出力パスと一致していることを確認します。アプリケーションビルダーを使用する Angular 17 以降では、パスは dist/<project-name>/browser です。古い Angular バージョンでは、dist/<project-name> です。
  • 実行時にプラグインが見つからない:新しいプラグインをインストールした後は、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/<project-name>/browser に変更されています。

関連スキル

  • capacitor-app-creation — Capacitor アプリを一から作成します。
  • capacitor-app-development — Angular に特化しない、Capacitor の一般的な開発ガイダンスです。
  • capacitor-plugins — 公式およびコミュニティのソースから Capacitor プラグインをインストールして設定します。
  • capacitor-react — Capacitor アプリ開発における React 固有のパターンとベストプラクティスです。
  • ionic-angular — Angular と Ionic Framework(Capacitor を基盤とする UI コンポーネント、ナビゲーション、テーマ設定)です。
  • capacitor-app-upgrades — Capacitor アプリを新しいメジャーバージョンにアップグレードします。

コメント