capacitor-angular について
Angular 16以降とCapacitor 6、7、8でアプリを開発する人向けのスキルです。プロジェクト構成やプラグイン連携に加え、NgZone、ライフサイクル管理、ディープリンク、Androidの戻るボタンなどのパターンを扱います。
主な機能
- 01Angular構成とCapacitor設定の検出
- 02AngularプロジェクトへのCapacitor導入
- 03サービスによるCapacitorプラグインの利用
- 04NgZoneによるUI状態の更新
- 05リスナーとアプリライフサイクルの管理
- 06プラットフォーム判定、ディープリンク、戻るボタン対応
活用シーン
- 01AngularプロジェクトにCapacitorを追加
- 02Capacitorプラグインの連携と管理
- 03プラグインイベント後にUIが更新されない問題の修正
- 04ディープリンクと戻るボタンの設定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ capawesome-team/ skills --skill capacitor-angularインストール後、Claude Code で/capacitor-angularを入力して読み込まれたか確認します。
詳細
Angular と Capacitor
Capacitor アプリの開発における Angular 固有のパターンとベストプラクティス — プロジェクト構成、サービス、ライフサイクルフック、NgZone との統合、プラグインの使用方法。
前提条件
- Angular 16 以降を使用する Capacitor 6、7、または 8 のアプリ。
- Node.js と npm がインストールされていること。
- Angular CLI がインストールされていること(
npm install -g @angular/cli)。 - iOS の場合:Xcode がインストールされていること。
- Android の場合:Android Studio がインストールされていること。
エージェントの動作
- 質問する前に自動検出します。 プロジェクト内の
angular.json、package.json、capacitor.config.tsまたはcapacitor.config.json、および既存のディレクトリ構成を確認します。検出できない場合にのみユーザーに質問します。 - 手順を追って案内します。 ユーザーを一度に1つの手順ずつ案内します。
- プロジェクトのスタイルに合わせます。 プロジェクトがスタンドアロンコンポーネントを使用しているか、NgModule ベースのアーキテクチャを使用しているかを検出し、それに応じてコード例を調整します。
手順
Step 1: プロジェクトを分析する
プロジェクトファイルを読み取り、次の項目を自動検出します。
- Angular のバージョン:
package.jsonから@angular/coreのバージョンを読み取ります。 - Capacitor のバージョン:
package.jsonから@capacitor/coreのバージョンを読み取ります。存在しない場合は、Capacitor がまだ追加されていないため、Step 2 に進みます。 - アーキテクチャのスタイル:
src/main.tsでbootstrapApplication(スタンドアロン)とplatformBrowserDynamic().bootstrapModule(NgModule)のどちらが使われているか確認します。追加の確認としてangular.jsonも確認します。 - プラットフォーム:存在するディレクトリ(
android/、ios/)を確認します。 - Capacitor の設定形式:
capacitor.config.ts(TypeScript)またはcapacitor.config.json(JSON)があるか確認します。 - ビルド出力ディレクトリ:
angular.jsonのprojects > <project-name> > architect > build > options > outputPathからoutputPathを読み取ります。これは Capacitor のwebDir設定に必要です。
Step 2: Angular プロジェクトに Capacitor を追加する
@capacitor/core がすでに package.json に含まれている場合はスキップします。
-
Capacitor のコアと CLI をインストールします。
npm install @capacitor/core npm install -D @capacitor/cli -
Capacitor を初期化します。
npx cap initプロンプトが表示されたら、Step 1 で検出した Angular のビルド出力パスを web ディレクトリ に設定します。アプリケーションビルダーを使用する Angular 17 以降では、通常は
dist/<project-name>/browserです。古い Angular バージョンでは、通常はdist/<project-name>です。 -
生成された
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" } -
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 アプリを新しいメジャーバージョンにアップグレードします。