Skip to main content

Angular Capacitor Integration

Introduction

Capacitor is a modern native runtime that makes it easy to build web apps that run natively on iOS, Android, and the web. In this tutorial, we'll explore how to integrate Capacitor with Angular applications to build cross-platform mobile apps using your existing web development skills.

Capacitor provides a compatibility layer that allows your Angular application to access native device features (camera, geolocation, storage, etc.) using a consistent API across platforms. This integration enables you to leverage your Angular expertise to create mobile applications without learning platform-specific languages like Swift or Kotlin.

Prerequisites

Before we get started, make sure you have:

  • Node.js (version 14.x or higher)
  • npm or yarn package manager
  • Angular CLI installed globally
  • Basic knowledge of Angular
  • Android Studio (for Android development)
  • Xcode (for iOS development, macOS only)

Setting Up a New Angular Project with Capacitor

Let's start by creating a new Angular project and integrating Capacitor from scratch.

Step 1: Create a New Angular Project

bash
# Create a new Angular project
ng new angular-capacitor-demo
cd angular-capacitor-demo

# Build the project
ng build

Step 2: Add Capacitor to Your Project

bash
# Install Capacitor Core and CLI
npm install @capacitor/core @capacitor/cli

# Initialize Capacitor in your project
npx cap init

When running the initialization command, you'll be prompted for:

  • App name (e.g., "Angular Capacitor Demo")
  • App ID (e.g., "com.example.angularcapacitor")

Step 3: Add Platforms

Now, let's add the platforms you want to target:

bash
# Add Android platform
npm install @capacitor/android
npx cap add android

# Add iOS platform (macOS only)
npm install @capacitor/ios
npx cap add ios

Configuring Your Angular Application for Capacitor

For optimal results, we need to make some adjustments to our Angular configuration.

Update Angular Configuration

Modify your angular.json file to output the build to the correct directory:

json
{
"projects": {
"angular-capacitor-demo": {
"architect": {
"build": {
"options": {
"outputPath": "dist/angular-capacitor-demo",
"baseHref": "./",
"sourceMap": true
}
}
}
}
}
}

Configure capacitor.config.ts

Create or update the capacitor.config.ts file in your project root:

typescript
import { CapacitorConfig } from '@capacitor/cli';

const config: CapacitorConfig = {
appId: 'com.example.angularcapacitor',
appName: 'Angular Capacitor Demo',
webDir: 'dist/angular-capacitor-demo',
bundledWebRuntime: false,
server: {
androidScheme: 'https'
}
};

export default config;

Working with Native Device Features

One of the main advantages of Capacitor is access to native device features. Let's implement a simple example that uses the Camera API.

Step 1: Install the Camera Plugin

bash
npm install @capacitor/camera
npx cap sync

Step 2: Create a Camera Service

Create a new service to handle camera operations:

bash
ng generate service services/camera

Update the camera.service.ts file:

typescript
import { Injectable } from '@angular/core';
import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera';

@Injectable({
providedIn: 'root'
})
export class CameraService {

constructor() { }

async takePicture(): Promise<string> {
try {
const image = await Camera.getPhoto({
quality: 90,
allowEditing: false,
resultType: CameraResultType.Uri,
source: CameraSource.Camera
});

// Return the webPath for display in our app
return image.webPath || '';
} catch (error) {
console.error('Error taking photo', error);
return '';
}
}
}

Step 3: Create a Component to Use the Camera

Let's create a component to use our camera service:

bash
ng generate component components/photo-taker

Update the photo-taker.component.ts file:

typescript
import { Component } from '@angular/core';
import { CameraService } from '../../services/camera.service';

@Component({
selector: 'app-photo-taker',
template: `
<div class="container">
<h2>Take a Picture</h2>
<button (click)="capturePhoto()" class="capture-btn">
Capture Photo
</button>

<div *ngIf="photoUrl" class="photo-container">
<img [src]="photoUrl" alt="Captured photo" class="captured-img">
</div>
</div>
`,
styles: [`
.container {
text-align: center;
padding: 20px;
}
.capture-btn {
padding: 10px 20px;
background-color: #4285f4;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
margin-bottom: 20px;
}
.photo-container {
margin-top: 20px;
}
.captured-img {
max-width: 100%;
max-height: 300px;
border-radius: 8px;
}
`]
})
export class PhotoTakerComponent {
photoUrl: string = '';

constructor(private cameraService: CameraService) { }

async capturePhoto() {
this.photoUrl = await this.cameraService.takePicture();
}
}

Step 4: Add the Component to Your App

Update your app.component.html to include the new component:

html
<div class="content">
<h1>Angular Capacitor Demo</h1>
<p>Access native features from your Angular app</p>

<app-photo-taker></app-photo-taker>
</div>

Android Platform-Specific Configuration

For Android, we need to add permissions to the Android Manifest file.

Update Android Manifest

Open android/app/src/main/AndroidManifest.xml and add the camera permissions:

xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="false" />

<application>
<!-- Your application config -->
</application>
</manifest>

iOS Platform-Specific Configuration

For iOS, we need to update the Info.plist file to request camera permissions.

Update Info.plist

Open ios/App/App/Info.plist and add:

xml
<dict>
<!-- Other configuration -->
<key>NSCameraUsageDescription</key>
<string>We need access to your camera to take photos.</string>
<!-- Other configuration -->
</dict>

Testing and Running Your App

Building Your Angular App

Whenever you make changes to your Angular code, you need to rebuild and sync with Capacitor:

bash
# Build your Angular app
ng build

# Sync with Capacitor
npx cap sync

Running on Android

bash
# Open in Android Studio
npx cap open android

Then use Android Studio to run the app on an emulator or physical device.

Running on iOS (macOS only)

bash
# Open in Xcode
npx cap open ios

Then use Xcode to run the app on a simulator or physical device.

Creating a Production Build

When you're ready to create a production build:

bash
# Build Angular app in production mode
ng build --configuration production

# Sync with Capacitor
npx cap sync

# Open in Android Studio or Xcode to create the final app bundle/APK
npx cap open android
npx cap open ios

Real-World Application: Building a Location Tracker

Let's create a practical example that uses Capacitor's Geolocation plugin to track a user's location:

Step 1: Install the Geolocation Plugin

bash
npm install @capacitor/geolocation
npx cap sync

Step 2: Create a Location Service

bash
ng generate service services/location

Update the location.service.ts file:

typescript
import { Injectable } from '@angular/core';
import { Geolocation, Position } from '@capacitor/geolocation';
import { BehaviorSubject } from 'rxjs';

@Injectable({
providedIn: 'root'
})
export class LocationService {
private locationSubject = new BehaviorSubject<Position | null>(null);
public location$ = this.locationSubject.asObservable();
private watchId: string | null = null;

constructor() { }

async getCurrentPosition(): Promise<Position> {
try {
const position = await Geolocation.getCurrentPosition({
enableHighAccuracy: true
});
this.locationSubject.next(position);
return position;
} catch (error) {
console.error('Error getting current position', error);
throw error;
}
}

startWatchingPosition() {
if (this.watchId) {
return;
}

Geolocation.watchPosition(
{ enableHighAccuracy: true },
(position, err) => {
if (err) {
console.error('Error watching position', err);
return;
}

if (position) {
this.locationSubject.next(position);
}
}
).then(id => {
this.watchId = id;
});
}

stopWatchingPosition() {
if (this.watchId) {
Geolocation.clearWatch({ id: this.watchId });
this.watchId = null;
}
}
}

Step 3: Create a Location Tracker Component

bash
ng generate component components/location-tracker

Update the location-tracker.component.ts file:

typescript
import { Component, OnInit, OnDestroy } from '@angular/core';
import { LocationService } from '../../services/location.service';
import { Subscription } from 'rxjs';
import { Position } from '@capacitor/geolocation';

@Component({
selector: 'app-location-tracker',
template: `
<div class="location-container">
<h2>Location Tracker</h2>

<div class="controls">
<button (click)="getLocation()" class="location-btn">
Get Current Location
</button>

<button (click)="toggleTracking()" class="tracking-btn"
[ngClass]="{'tracking-active': isTracking}">
{{ isTracking ? 'Stop Tracking' : 'Start Tracking' }}
</button>
</div>

<div *ngIf="currentPosition" class="position-info">
<h3>Current Position:</h3>
<p><strong>Latitude:</strong> {{ currentPosition.coords.latitude.toFixed(6) }}</p>
<p><strong>Longitude:</strong> {{ currentPosition.coords.longitude.toFixed(6) }}</p>
<p><strong>Accuracy:</strong> {{ currentPosition.coords.accuracy.toFixed(2) }} meters</p>
<p><strong>Timestamp:</strong> {{ formatTimestamp(currentPosition.timestamp) }}</p>
</div>

<div *ngIf="errorMessage" class="error">
{{ errorMessage }}
</div>
</div>
`,
styles: [`
.location-container {
padding: 20px;
background-color: #f5f5f5;
border-radius: 8px;
max-width: 500px;
margin: 0 auto;
}

.controls {
display: flex;
gap: 10px;
margin-bottom: 20px;
}

.location-btn, .tracking-btn {
padding: 10px 15px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
flex: 1;
}

.location-btn {
background-color: #4285f4;
color: white;
}

.tracking-btn {
background-color: #34a853;
color: white;
}

.tracking-active {
background-color: #ea4335;
}

.position-info {
background-color: white;
padding: 15px;
border-radius: 8px;
margin-top: 15px;
}

.error {
color: #ea4335;
margin-top: 15px;
padding: 10px;
background-color: rgba(234, 67, 53, 0.1);
border-radius: 4px;
}
`]
})
export class LocationTrackerComponent implements OnInit, OnDestroy {
currentPosition: Position | null = null;
isTracking = false;
errorMessage = '';
private locationSubscription: Subscription | null = null;

constructor(private locationService: LocationService) { }

ngOnInit() {
this.locationSubscription = this.locationService.location$.subscribe(
position => {
if (position) {
this.currentPosition = position;
this.errorMessage = '';
}
}
);
}

ngOnDestroy() {
this.locationService.stopWatchingPosition();
if (this.locationSubscription) {
this.locationSubscription.unsubscribe();
}
}

async getLocation() {
try {
this.currentPosition = await this.locationService.getCurrentPosition();
this.errorMessage = '';
} catch (error: any) {
this.errorMessage = `Error getting location: ${error.message}`;
}
}

toggleTracking() {
if (this.isTracking) {
this.locationService.stopWatchingPosition();
} else {
try {
this.locationService.startWatchingPosition();
} catch (error: any) {
this.errorMessage = `Error starting location tracking: ${error.message}`;
}
}
this.isTracking = !this.isTracking;
}

formatTimestamp(timestamp: number): string {
return new Date(timestamp).toLocaleTimeString();
}
}

Step 4: Update Platform Permissions

For Android, add location permissions to the AndroidManifest.xml:

xml
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

For iOS, update Info.plist:

xml
<key>NSLocationWhenInUseUsageDescription</key>
<string>We need your location to show it on the map.</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>We need your location to track your position even when the app is in the background.</string>

Step 5: Add the Location Tracker to Your App

Update your app.component.html:

html
<div class="content">
<h1>Angular Capacitor Demo</h1>
<p>Access native features from your Angular app</p>

<app-photo-taker></app-photo-taker>

<div class="divider"></div>

<app-location-tracker></app-location-tracker>
</div>

Add some CSS to app.component.css:

css
.divider {
height: 2px;
background-color: #e0e0e0;
margin: 20px 0;
}

Best Practices for Angular Capacitor Integration

  1. Update Regularly: Keep Capacitor and its plugins updated to benefit from the latest features and security fixes.

  2. Optimize for Mobile:

    • Use responsive design principles
    • Implement touch-friendly UI components
    • Ensure fast loading times
  3. Handle Platform Differences: Use Capacitor's platform detection to handle platform-specific logic:

    typescript
    import { Capacitor } from '@capacitor/core';

    if (Capacitor.getPlatform() === 'ios') {
    // iOS-specific code
    } else if (Capacitor.getPlatform() === 'android') {
    // Android-specific code
    } else {
    // Web-specific code
    }
  4. Test on Real Devices: Emulators/simulators can't fully replicate the experience on real devices, especially for features like camera, geolocation, etc.

  5. Use Live Reload for Development: Enable live reload to speed up your development process:

    bash
    npx cap run android -l --external
    npx cap run ios -l --external
  6. Bundle Size Management: Keep your Angular bundle size small to ensure fast startup times on mobile devices.

Common Issues and Troubleshooting

Issue: App Not Syncing with Capacitor

Solution: Make sure you run ng build before npx cap sync to ensure the latest Angular build is copied to the native projects.

Issue: Plugin Not Found

Solution: After installing a plugin, always run npx cap sync to update the native projects.

Issue: Android Build Errors

Solution: Check your Android Studio setup, Gradle version compatibility, and make sure your path doesn't contain special characters.

Issue: iOS Build Errors

Solution: Verify your Xcode installation, CocoaPods setup, and ensure you're running on macOS for iOS development.

Summary

In this tutorial, we've learned how to integrate Capacitor with Angular to build cross-platform mobile applications. We covered:

  1. Setting up a new Angular project with Capacitor
  2. Configuring Angular and Capacitor for optimal integration
  3. Accessing native device features like the camera
  4. Platform-specific configurations for Android and iOS
  5. Creating a real-world location tracker application
  6. Best practices and troubleshooting common issues

Capacitor provides a powerful bridge between your Angular web application and native mobile platforms, allowing you to build feature-rich mobile apps using your existing web development skills. By following this guide, you now have the foundation to build professional-grade mobile applications with Angular and Capacitor.

Additional Resources

Exercises

  1. Basic: Add the Capacitor Storage plugin to store and retrieve the last photo taken in the photo-taker component.
  2. Intermediate: Implement a dark mode toggle that uses Capacitor's Preferences API to remember the user's choice.
  3. Advanced: Create a note-taking app that uses Capacitor plugins for camera, geolocation, and local storage.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)