Ionic Cheatsheet¶
Ionic - Cross-Platform Mobile Apps with Web Technologies
Ionic is an open-source mobile app development framework that enables developers to build high-quality cross-platform mobile apps using web technologies like HTML, CSS, and JavaScript. It provides native mobile components and tools for building hybrid mobile applications.
Table of Contents¶
- Installation
- Getting Started
- Project Structure
- Components
- Navigation
- Styling
- Native Features
- State Management
- HTTP and Data
- Storage
- Testing
- Building and Deployment
- Capacitor Integration
- Performance Optimization
- Best Practices
- Troubleshooting
Installation¶
Prerequisites¶
# Install Node.js (LTS version recommended)
# Download from https://nodejs.org/
# Install npm or yarn
npm --version
yarn --version
Ionic CLI Installation¶
# Install Ionic CLI globally
npm install -g @ionic/cli
# Verify installation
ionic --version
# Install Cordova CLI (for Cordova integration)
npm install -g cordova
# Install Capacitor CLI (for Capacitor integration)
npm install -g @capacitor/cli
Development Environment Setup¶
# For Android development
# Install Android Studio from https://developer.android.com/studio
# Set up Android SDK and emulator
# For iOS development (macOS only)
# Install Xcode from Mac App Store
# Install CocoaPods
sudo gem install cocoapods
Getting Started¶
Create New Project¶
# Create a new Ionic project
ionic start myApp tabs --type=angular
# Available templates:
# - blank: A blank starter project
# - tabs: A starting project with a simple tabbed interface
# - sidemenu: A starting project with a side menu with navigation
# Framework options:
# - angular (default)
# - react
# - vue
# Create with specific framework
ionic start myApp tabs --type=react
ionic start myApp tabs --type=vue
# Navigate to project directory
cd myApp
# Serve the app in development mode
ionic serve
# Serve with specific port
ionic serve --port=8100
# Serve with lab mode (iOS and Android preview)
ionic serve --lab
Project Structure¶
myApp/
├── src/
│ ├── app/
│ │ ├── pages/ # Page components
│ │ ├── components/ # Reusable components
│ │ ├── services/ # Services and providers
│ │ ├── models/ # Data models
│ │ └── app.module.ts # Main app module
│ ├── assets/ # Static assets
│ ├── theme/ # Global styles
│ └── index.html # Main HTML file
├── android/ # Android platform files
├── ios/ # iOS platform files
├── www/ # Built web assets
├── ionic.config.json # Ionic configuration
├── capacitor.config.json # Capacitor configuration
└── package.json # Dependencies
Basic App Setup (Angular)¶
// src/app/app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';
import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
IonicModule.forRoot(),
AppRoutingModule
],
providers: [
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }
],
bootstrap: [AppComponent],
})
export class AppModule {}
// src/app/app.component.ts
import { Component } from '@angular/core';
import { Platform } from '@ionic/angular';
@Component({
selector: 'app-root',
templateUrl: 'app.component.html',
styleUrls: ['app.component.scss'],
})
export class AppComponent {
constructor(private platform: Platform) {
this.initializeApp();
}
initializeApp() {
this.platform.ready().then(() => {
console.log('Platform is ready');
});
}
}
Components¶
Basic Components¶
// Home page component
import { Component } from '@angular/core';
@Component({
selector: 'app-home',
templateUrl: 'home.page.html',
styleUrls: ['home.page.scss'],
})
export class HomePage {
title = 'Welcome to Ionic';
items = [
{ id: 1, name: 'Item 1', description: 'Description 1' },
{ id: 2, name: 'Item 2', description: 'Description 2' },
{ id: 3, name: 'Item 3', description: 'Description 3' },
];
constructor() {}
onItemClick(item: any) {
console.log('Item clicked:', item);
}
doRefresh(event: any) {
setTimeout(() => {
console.log('Async operation has ended');
event.target.complete();
}, 2000);
}
}
<!-- home.page.html -->
<ion-header [translucent]="true">
<ion-toolbar>
<ion-title>{{ title }}</ion-title>
<ion-buttons slot="end">
<ion-button>
<ion-icon name="settings-outline"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content [fullscreen]="true">
<!-- Refresher -->
<ion-refresher slot="fixed" (ionRefresh)="doRefresh($event)">
<ion-refresher-content></ion-refresher-content>
</ion-refresher>
<!-- Header -->
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">{{ title }}</ion-title>
</ion-toolbar>
</ion-header>
<!-- Content -->
<div class="ion-padding">
<!-- Cards -->
<ion-card>
<ion-card-header>
<ion-card-title>Card Title</ion-card-title>
<ion-card-subtitle>Card Subtitle</ion-card-subtitle>
</ion-card-header>
<ion-card-content>
This is the card content. You can put any content here.
</ion-card-content>
</ion-card>
<!-- List -->
<ion-list>
<ion-list-header>
<ion-label>Items</ion-label>
</ion-list-header>
<ion-item *ngFor="let item of items" (click)="onItemClick(item)">
<ion-avatar slot="start">
<img src="https://via.placeholder.com/40" />
</ion-avatar>
<ion-label>
<h2>{{ item.name }}</h2>
<p>{{ item.description }}</p>
</ion-label>
<ion-icon name="chevron-forward-outline" slot="end"></ion-icon>
</ion-item>
</ion-list>
<!-- Buttons -->
<ion-button expand="block" fill="solid" color="primary">
Primary Button
</ion-button>
<ion-button expand="block" fill="outline" color="secondary">
Secondary Button
</ion-button>
<!-- Input -->
<ion-item>
<ion-label position="floating">Email</ion-label>
<ion-input type="email" placeholder="Enter email"></ion-input>
</ion-item>
<!-- Checkbox -->
<ion-item>
<ion-checkbox slot="start"></ion-checkbox>
<ion-label>Accept terms and conditions</ion-label>
</ion-item>
<!-- Toggle -->
<ion-item>
<ion-label>Enable notifications</ion-label>
<ion-toggle slot="end"></ion-toggle>
</ion-item>
<!-- Range -->
<ion-item>
<ion-label>Volume</ion-label>
<ion-range min="0" max="100" value="50" slot="end">
<ion-icon name="volume-low" slot="start"></ion-icon>
<ion-icon name="volume-high" slot="end"></ion-icon>
</ion-range>
</ion-item>
</div>
<!-- Floating Action Button -->
<ion-fab vertical="bottom" horizontal="end" slot="fixed">
<ion-fab-button>
<ion-icon name="add"></ion-icon>
</ion-fab-button>
</ion-fab>
</ion-content>
Advanced Components¶
// Modal component
import { Component, Input } from '@angular/core';
import { ModalController } from '@ionic/angular';
@Component({
selector: 'app-modal',
template: `
<ion-header>
<ion-toolbar>
<ion-title>Modal</ion-title>
<ion-buttons slot="end">
<ion-button (click)="dismiss()">Close</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<div class="ion-padding">
<p>{{ data }}</p>
</div>
</ion-content>
`,
})
export class ModalComponent {
@Input() data: string = '';
constructor(private modalController: ModalController) {}
dismiss() {
this.modalController.dismiss();
}
}
// Using modal in a page
import { ModalController } from '@ionic/angular';
import { ModalComponent } from './modal.component';
export class HomePage {
constructor(private modalController: ModalController) {}
async presentModal() {
const modal = await this.modalController.create({
component: ModalComponent,
componentProps: {
data: 'Hello from modal!'
}
});
return await modal.present();
}
}
// Popover component
import { Component } from '@angular/core';
import { PopoverController } from '@ionic/angular';
@Component({
selector: 'app-popover',
template: `
<ion-content>
<ion-list>
<ion-item button (click)="close('option1')">
<ion-label>Option 1</ion-label>
</ion-item>
<ion-item button (click)="close('option2')">
<ion-label>Option 2</ion-label>
</ion-item>
</ion-list>
</ion-content>
`,
})
export class PopoverComponent {
constructor(private popoverController: PopoverController) {}
close(data?: any) {
this.popoverController.dismiss(data);
}
}
// Action Sheet
import { ActionSheetController } from '@ionic/angular';
export class HomePage {
constructor(private actionSheetController: ActionSheetController) {}
async presentActionSheet() {
const actionSheet = await this.actionSheetController.create({
header: 'Albums',
cssClass: 'my-custom-class',
buttons: [
{
text: 'Delete',
role: 'destructive',
icon: 'trash',
handler: () => {
console.log('Delete clicked');
}
},
{
text: 'Share',
icon: 'share',
handler: () => {
console.log('Share clicked');
}
},
{
text: 'Cancel',
icon: 'close',
role: 'cancel',
handler: () => {
console.log('Cancel clicked');
}
}
]
});
await actionSheet.present();
}
}
// Alert
import { AlertController } from '@ionic/angular';
export class HomePage {
constructor(private alertController: AlertController) {}
async presentAlert() {
const alert = await this.alertController.create({
header: 'Alert',
subHeader: 'Subtitle',
message: 'This is an alert message.',
buttons: ['OK']
});
await alert.present();
}
async presentConfirm() {
const alert = await this.alertController.create({
header: 'Confirm!',
message: 'Are you sure you want to delete this item?',
buttons: [
{
text: 'Cancel',
role: 'cancel',
cssClass: 'secondary',
handler: () => {
console.log('Confirm Cancel');
}
},
{
text: 'Okay',
handler: () => {
console.log('Confirm Okay');
}
}
]
});
await alert.present();
}
}
// Toast
import { ToastController } from '@ionic/angular';
export class HomePage {
constructor(private toastController: ToastController) {}
async presentToast() {
const toast = await this.toastController.create({
message: 'Your settings have been saved.',
duration: 2000,
position: 'bottom'
});
toast.present();
}
}
// Loading
import { LoadingController } from '@ionic/angular';
export class HomePage {
constructor(private loadingController: LoadingController) {}
async presentLoading() {
const loading = await this.loadingController.create({
message: 'Please wait...',
duration: 2000
});
await loading.present();
const { role, data } = await loading.onDidDismiss();
console.log('Loading dismissed!');
}
}
Navigation¶
Router Navigation¶
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';
const routes: Routes = [
{
path: 'home',
loadChildren: () => import('./home/home.module').then(m => m.HomePageModule)
},
{
path: 'details/:id',
loadChildren: () => import('./details/details.module').then(m => m.DetailsPageModule)
},
{
path: '',
redirectTo: 'home',
pathMatch: 'full'
},
];
@NgModule({
imports: [
RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
],
exports: [RouterModule]
})
export class AppRoutingModule { }
// Navigation in component
import { Router } from '@angular/router';
import { NavController } from '@ionic/angular';
export class HomePage {
constructor(
private router: Router,
private navController: NavController
) {}
// Using Angular Router
navigateToDetails(id: number) {
this.router.navigate(['/details', id]);
}
// Using Ionic NavController
navigateWithNavController() {
this.navController.navigateForward('/details/123');
}
goBack() {
this.navController.back();
}
}
Tab Navigation¶
// tabs.page.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-tabs',
templateUrl: 'tabs.page.html',
styleUrls: ['tabs.page.scss']
})
export class TabsPage {
constructor() {}
}
<!-- tabs.page.html -->
<ion-tabs>
<ion-tab-bar slot="bottom">
<ion-tab-button tab="home">
<ion-icon name="home"></ion-icon>
<ion-label>Home</ion-label>
</ion-tab-button>
<ion-tab-button tab="search">
<ion-icon name="search"></ion-icon>
<ion-label>Search</ion-label>
</ion-tab-button>
<ion-tab-button tab="profile">
<ion-icon name="person"></ion-icon>
<ion-label>Profile</ion-label>
</ion-tab-button>
</ion-tab-bar>
</ion-tabs>
Side Menu Navigation¶
// app.component.ts
import { Component } from '@angular/core';
import { MenuController } from '@ionic/angular';
@Component({
selector: 'app-root',
templateUrl: 'app.component.html',
styleUrls: ['app.component.scss'],
})
export class AppComponent {
public appPages = [
{ title: 'Home', url: '/home', icon: 'home' },
{ title: 'Profile', url: '/profile', icon: 'person' },
{ title: 'Settings', url: '/settings', icon: 'settings' },
];
constructor(private menu: MenuController) {}
openMenu() {
this.menu.enable(true, 'first');
this.menu.open('first');
}
closeMenu() {
this.menu.close();
}
}
<!-- app.component.html -->
<ion-app>
<ion-split-pane contentId="main-content">
<ion-menu contentId="main-content" type="overlay">
<ion-content>
<ion-list id="inbox-list">
<ion-list-header>Menu</ion-list-header>
<ion-menu-toggle auto-hide="false" *ngFor="let p of appPages; let i = index">
<ion-item routerDirection="root" [routerLink]="[p.url]" lines="none" detail="false">
<ion-icon slot="start" [name]="p.icon"></ion-icon>
<ion-label>{{ p.title }}</ion-label>
</ion-item>
</ion-menu-toggle>
</ion-list>
</ion-content>
</ion-menu>
<ion-router-outlet id="main-content"></ion-router-outlet>
</ion-split-pane>
</ion-app>
Styling¶
CSS Variables and Theming¶
// src/theme/variables.scss
:root {
/** Primary colors **/
--ion-color-primary: #3880ff;
--ion-color-primary-rgb: 56, 128, 255;
--ion-color-primary-contrast: #ffffff;
--ion-color-primary-contrast-rgb: 255, 255, 255;
--ion-color-primary-shade: #3171e0;
--ion-color-primary-tint: #4c8dff;
/** Secondary colors **/
--ion-color-secondary: #3dc2ff;
--ion-color-secondary-rgb: 61, 194, 255;
--ion-color-secondary-contrast: #ffffff;
--ion-color-secondary-contrast-rgb: 255, 255, 255;
--ion-color-secondary-shade: #36abe0;
--ion-color-secondary-tint: #50c8ff;
/** Custom colors **/
--ion-color-custom: #ff6b6b;
--ion-color-custom-rgb: 255, 107, 107;
--ion-color-custom-contrast: #ffffff;
--ion-color-custom-contrast-rgb: 255, 255, 255;
--ion-color-custom-shade: #e05e5e;
--ion-color-custom-tint: #ff7a7a;
}
.ion-color-custom {
--ion-color-base: var(--ion-color-custom);
--ion-color-base-rgb: var(--ion-color-custom-rgb);
--ion-color-contrast: var(--ion-color-custom-contrast);
--ion-color-contrast-rgb: var(--ion-color-custom-contrast-rgb);
--ion-color-shade: var(--ion-color-custom-shade);
--ion-color-tint: var(--ion-color-custom-tint);
}
// Dark mode
@media (prefers-color-scheme: dark) {
:root {
--ion-color-primary: #428cff;
--ion-color-primary-rgb: 66, 140, 255;
--ion-color-primary-contrast: #ffffff;
--ion-color-primary-contrast-rgb: 255, 255, 255;
--ion-color-primary-shade: #3a7be0;
--ion-color-primary-tint: #5598ff;
}
}
Component Styling¶
// home.page.scss
.welcome-card {
ion-card-header {
background: linear-gradient(135deg, var(--ion-color-primary), var(--ion-color-secondary));
color: white;
}
ion-card-title {
font-size: 1.5rem;
font-weight: bold;
}
}
.custom-button {
--background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
--border-radius: 25px;
--padding-start: 2rem;
--padding-end: 2rem;
margin: 1rem 0;
}
.item-list {
ion-item {
--border-color: var(--ion-color-light-shade);
--padding-start: 1rem;
&:hover {
--background: var(--ion-color-light);
}
}
}
// Responsive design
@media (min-width: 768px) {
.desktop-only {
display: block;
}
.mobile-only {
display: none;
}
}
@media (max-width: 767px) {
.desktop-only {
display: none;
}
.mobile-only {
display: block;
}
}
Global Styles¶
// src/global.scss
// Custom utility classes
.text-center {
text-align: center;
}
.margin-top {
margin-top: 1rem;
}
.padding-horizontal {
padding-left: 1rem;
padding-right: 1rem;
}
// Custom animations
@keyframes slideIn {
from {
transform: translateX(-100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.slide-in {
animation: slideIn 0.3s ease-in-out;
}
// Custom scrollbar
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: var(--ion-color-light);
}
::-webkit-scrollbar-thumb {
background: var(--ion-color-medium);
border-radius: 4px;
}
Native Features¶
Camera¶
// Install Capacitor Camera plugin
// npm install @capacitor/camera
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
export class CameraService {
async takePicture() {
try {
const image = await Camera.getPhoto({
quality: 90,
allowEditing: true,
resultType: CameraResultType.Uri,
source: CameraSource.Camera
});
return image.webPath;
} catch (error) {
console.error('Error taking picture:', error);
throw error;
}
}
async selectFromGallery() {
try {
const image = await Camera.getPhoto({
quality: 90,
allowEditing: true,
resultType: CameraResultType.Uri,
source: CameraSource.Photos
});
return image.webPath;
} catch (error) {
console.error('Error selecting from gallery:', error);
throw error;
}
}
}
Geolocation¶
// Install Capacitor Geolocation plugin
// npm install @capacitor/geolocation
import { Geolocation } from '@capacitor/geolocation';
export class LocationService {
async getCurrentPosition() {
try {
const coordinates = await Geolocation.getCurrentPosition();
return {
latitude: coordinates.coords.latitude,
longitude: coordinates.coords.longitude,
accuracy: coordinates.coords.accuracy
};
} catch (error) {
console.error('Error getting location:', error);
throw error;
}
}
async watchPosition() {
const watchId = await Geolocation.watchPosition({
enableHighAccuracy: true,
timeout: 10000
}, (position, err) => {
if (err) {
console.error('Error watching position:', err);
return;
}
console.log('Position updated:', position);
});
return watchId;
}
async clearWatch(watchId: string) {
await Geolocation.clearWatch({ id: watchId });
}
}
Device Information¶
// Install Capacitor Device plugin
// npm install @capacitor/device
import { Device } from '@capacitor/device';
export class DeviceService {
async getDeviceInfo() {
try {
const info = await Device.getInfo();
return {
model: info.model,
platform: info.platform,
operatingSystem: info.operatingSystem,
osVersion: info.osVersion,
manufacturer: info.manufacturer,
isVirtual: info.isVirtual,
webViewVersion: info.webViewVersion
};
} catch (error) {
console.error('Error getting device info:', error);
throw error;
}
}
async getBatteryInfo() {
try {
const info = await Device.getBatteryInfo();
return {
batteryLevel: info.batteryLevel,
isCharging: info.isCharging
};
} catch (error) {
console.error('Error getting battery info:', error);
throw error;
}
}
}
Push Notifications¶
// Install Capacitor Push Notifications plugin
// npm install @capacitor/push-notifications
import { PushNotifications } from '@capacitor/push-notifications';
export class PushNotificationService {
async initializePushNotifications() {
// Request permission to use push notifications
await PushNotifications.requestPermissions();
// Register with Apple / Google to receive push via APNS/FCM
await PushNotifications.register();
// On success, we should be able to receive notifications
PushNotifications.addListener('registration', (token) => {
console.log('Push registration success, token: ' + token.value);
});
// Some issue with our setup and push will not work
PushNotifications.addListener('registrationError', (error) => {
console.error('Error on registration: ' + JSON.stringify(error));
});
// Show us the notification payload if the app is open on our device
PushNotifications.addListener('pushNotificationReceived', (notification) => {
console.log('Push received: ' + JSON.stringify(notification));
});
// Method called when tapping on a notification
PushNotifications.addListener('pushNotificationActionPerformed', (notification) => {
console.log('Push action performed: ' + JSON.stringify(notification));
});
}
}
State Management¶
Angular Services¶
// services/data.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
export interface User {
id: number;
name: string;
email: string;
}
@Injectable({
providedIn: 'root'
})
export class DataService {
private usersSubject = new BehaviorSubject<User[]>([]);
public users$: Observable<User[]> = this.usersSubject.asObservable();
private loadingSubject = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
constructor() {}
getUsers(): User[] {
return this.usersSubject.value;
}
setUsers(users: User[]): void {
this.usersSubject.next(users);
}
addUser(user: User): void {
const currentUsers = this.usersSubject.value;
this.usersSubject.next([...currentUsers, user]);
}
updateUser(updatedUser: User): void {
const currentUsers = this.usersSubject.value;
const index = currentUsers.findIndex(user => user.id === updatedUser.id);
if (index !== -1) {
currentUsers[index] = updatedUser;
this.usersSubject.next([...currentUsers]);
}
}
deleteUser(id: number): void {
const currentUsers = this.usersSubject.value;
const filteredUsers = currentUsers.filter(user => user.id !== id);
this.usersSubject.next(filteredUsers);
}
setLoading(loading: boolean): void {
this.loadingSubject.next(loading);
}
}
// Using the service in a component
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { DataService, User } from '../services/data.service';
@Component({
selector: 'app-users',
templateUrl: './users.page.html',
styleUrls: ['./users.page.scss'],
})
export class UsersPage implements OnInit, OnDestroy {
users: User[] = [];
loading = false;
private subscriptions: Subscription[] = [];
constructor(private dataService: DataService) {}
ngOnInit() {
// Subscribe to users
this.subscriptions.push(
this.dataService.users$.subscribe(users => {
this.users = users;
})
);
// Subscribe to loading state
this.subscriptions.push(
this.dataService.loading$.subscribe(loading => {
this.loading = loading;
})
);
}
ngOnDestroy() {
this.subscriptions.forEach(sub => sub.unsubscribe());
}
addUser() {
const newUser: User = {
id: Date.now(),
name: 'New User',
email: 'user@example.com'
};
this.dataService.addUser(newUser);
}
deleteUser(id: number) {
this.dataService.deleteUser(id);
}
}
NgRx (Advanced State Management)¶
// store/user.actions.ts
import { createAction, props } from '@ngrx/store';
import { User } from '../models/user.model';
export const loadUsers = createAction('[User] Load Users');
export const loadUsersSuccess = createAction(
'[User] Load Users Success',
props<{ users: User[] }>()
);
export const loadUsersFailure = createAction(
'[User] Load Users Failure',
props<{ error: any }>()
);
export const addUser = createAction(
'[User] Add User',
props<{ user: User }>()
);
// store/user.reducer.ts
import { createReducer, on } from '@ngrx/store';
import { User } from '../models/user.model';
import * as UserActions from './user.actions';
export interface UserState {
users: User[];
loading: boolean;
error: any;
}
export const initialState: UserState = {
users: [],
loading: false,
error: null
};
export const userReducer = createReducer(
initialState,
on(UserActions.loadUsers, state => ({
...state,
loading: true
})),
on(UserActions.loadUsersSuccess, (state, { users }) => ({
...state,
loading: false,
users
})),
on(UserActions.loadUsersFailure, (state, { error }) => ({
...state,
loading: false,
error
})),
on(UserActions.addUser, (state, { user }) => ({
...state,
users: [...state.users, user]
}))
);
// store/user.selectors.ts
import { createSelector, createFeatureSelector } from '@ngrx/store';
import { UserState } from './user.reducer';
export const selectUserState = createFeatureSelector<UserState>('users');
export const selectAllUsers = createSelector(
selectUserState,
(state: UserState) => state.users
);
export const selectUsersLoading = createSelector(
selectUserState,
(state: UserState) => state.loading
);
export const selectUsersError = createSelector(
selectUserState,
(state: UserState) => state.error
);
HTTP and Data¶
HTTP Client¶
// services/api.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, retry, map } from 'rxjs/operators';
export interface ApiResponse<T> {
data: T;
message: string;
success: boolean;
}
@Injectable({
providedIn: 'root'
})
export class ApiService {
private baseUrl = 'https://api.example.com';
private httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json'
})
};
constructor(private http: HttpClient) {}
// GET request
get<T>(endpoint: string): Observable<T> {
return this.http.get<T>(`${this.baseUrl}/${endpoint}`, this.httpOptions)
.pipe(
retry(3),
catchError(this.handleError)
);
}
// POST request
post<T>(endpoint: string, data: any): Observable<T> {
return this.http.post<T>(`${this.baseUrl}/${endpoint}`, data, this.httpOptions)
.pipe(
catchError(this.handleError)
);
}
// PUT request
put<T>(endpoint: string, data: any): Observable<T> {
return this.http.put<T>(`${this.baseUrl}/${endpoint}`, data, this.httpOptions)
.pipe(
catchError(this.handleError)
);
}
// DELETE request
delete<T>(endpoint: string): Observable<T> {
return this.http.delete<T>(`${this.baseUrl}/${endpoint}`, this.httpOptions)
.pipe(
catchError(this.handleError)
);
}
// Error handling
private handleError(error: HttpErrorResponse) {
let errorMessage = 'Unknown error!';
if (error.error instanceof ErrorEvent) {
// Client-side errors
errorMessage = `Error: ${error.error.message}`;
} else {
// Server-side errors
errorMessage = `Error Code: ${error.status}\nMessage: ${error.message}`;
}
console.error(errorMessage);
return throwError(errorMessage);
}
// Upload file
uploadFile(endpoint: string, file: File): Observable<any> {
const formData = new FormData();
formData.append('file', file);
return this.http.post(`${this.baseUrl}/${endpoint}`, formData)
.pipe(
catchError(this.handleError)
);
}
}
// Using the API service
import { Component, OnInit } from '@angular/core';
import { ApiService } from '../services/api.service';
import { LoadingController, ToastController } from '@ionic/angular';
@Component({
selector: 'app-data',
templateUrl: './data.page.html',
styleUrls: ['./data.page.scss'],
})
export class DataPage implements OnInit {
data: any[] = [];
constructor(
private apiService: ApiService,
private loadingController: LoadingController,
private toastController: ToastController
) {}
ngOnInit() {
this.loadData();
}
async loadData() {
const loading = await this.loadingController.create({
message: 'Loading data...'
});
await loading.present();
this.apiService.get('users').subscribe({
next: (response: any) => {
this.data = response.data || response;
loading.dismiss();
},
error: async (error) => {
loading.dismiss();
const toast = await this.toastController.create({
message: 'Error loading data: ' + error,
duration: 3000,
color: 'danger'
});
toast.present();
}
});
}
async saveData(item: any) {
const loading = await this.loadingController.create({
message: 'Saving...'
});
await loading.present();
this.apiService.post('users', item).subscribe({
next: async (response) => {
loading.dismiss();
const toast = await this.toastController.create({
message: 'Data saved successfully!',
duration: 2000,
color: 'success'
});
toast.present();
this.loadData(); // Refresh data
},
error: async (error) => {
loading.dismiss();
const toast = await this.toastController.create({
message: 'Error saving data: ' + error,
duration: 3000,
color: 'danger'
});
toast.present();
}
});
}
}
Storage¶
Ionic Storage¶
// services/storage.service.ts
import { Injectable } from '@angular/core';
import { Storage } from '@ionic/storage-angular';
@Injectable({
providedIn: 'root'
})
export class StorageService {
private _storage: Storage | null = null;
constructor(private storage: Storage) {
this.init();
}
async init() {
const storage = await this.storage.create();
this._storage = storage;
}
// Store data
public async set(key: string, value: any): Promise<any> {
return this._storage?.set(key, value);
}
// Get data
public async get(key: string): Promise<any> {
return this._storage?.get(key);
}
// Remove data
public async remove(key: string): Promise<any> {
return this._storage?.remove(key);
}
// Clear all data
public async clear(): Promise<void> {
return this._storage?.clear();
}
// Get all keys
public async keys(): Promise<string[]> {
return this._storage?.keys() || [];
}
// Get length
public async length(): Promise<number> {
return this._storage?.length() || 0;
}
// Store object
public async setObject(key: string, object: any): Promise<any> {
return this._storage?.set(key, JSON.stringify(object));
}
// Get object
public async getObject(key: string): Promise<any> {
const data = await this._storage?.get(key);
return data ? JSON.parse(data) : null;
}
}
// app.module.ts
import { IonicStorageModule } from '@ionic/storage-angular';
@NgModule({
imports: [
IonicStorageModule.forRoot()
]
})
export class AppModule {}
// Using storage service
import { Component, OnInit } from '@angular/core';
import { StorageService } from '../services/storage.service';
@Component({
selector: 'app-settings',
templateUrl: './settings.page.html',
styleUrls: ['./settings.page.scss'],
})
export class SettingsPage implements OnInit {
settings = {
notifications: true,
darkMode: false,
language: 'en'
};
constructor(private storageService: StorageService) {}
async ngOnInit() {
await this.loadSettings();
}
async loadSettings() {
const savedSettings = await this.storageService.getObject('app-settings');
if (savedSettings) {
this.settings = { ...this.settings, ...savedSettings };
}
}
async saveSettings() {
await this.storageService.setObject('app-settings', this.settings);
console.log('Settings saved');
}
async resetSettings() {
await this.storageService.remove('app-settings');
this.settings = {
notifications: true,
darkMode: false,
language: 'en'
};
console.log('Settings reset');
}
}
Capacitor Preferences¶
// Install Capacitor Preferences
// npm install @capacitor/preferences
import { Preferences } from '@capacitor/preferences';
export class PreferencesService {
// Set a value
async setValue(key: string, value: string): Promise<void> {
await Preferences.set({
key: key,
value: value,
});
}
// Get a value
async getValue(key: string): Promise<string | null> {
const { value } = await Preferences.get({ key: key });
return value;
}
// Remove a value
async removeValue(key: string): Promise<void> {
await Preferences.remove({ key: key });
}
// Clear all values
async clearAll(): Promise<void> {
await Preferences.clear();
}
// Get all keys
async getAllKeys(): Promise<string[]> {
const { keys } = await Preferences.keys();
return keys;
}
// Store object
async setObject(key: string, value: any): Promise<void> {
await this.setValue(key, JSON.stringify(value));
}
// Get object
async getObject(key: string): Promise<any> {
const value = await this.getValue(key);
return value ? JSON.parse(value) : null;
}
}
Testing¶
Unit Testing¶
// home.page.spec.ts
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { IonicModule } from '@ionic/angular';
import { HomePage } from './home.page';
describe('HomePage', () => {
let component: HomePage;
let fixture: ComponentFixture<HomePage>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [HomePage],
imports: [IonicModule.forRoot()]
}).compileComponents();
fixture = TestBed.createComponent(HomePage);
component = fixture.componentInstance;
fixture.detectChanges();
}));
it('should create', () => {
expect(component).toBeTruthy();
});
it('should have correct title', () => {
expect(component.title).toBe('Welcome to Ionic');
});
it('should handle item click', () => {
spyOn(console, 'log');
const item = { id: 1, name: 'Test Item' };
component.onItemClick(item);
expect(console.log).toHaveBeenCalledWith('Item clicked:', item);
});
});
// Service testing
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { ApiService } from './api.service';
describe('ApiService', () => {
let service: ApiService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [ApiService]
});
service = TestBed.inject(ApiService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpMock.verify();
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should fetch users', () => {
const mockUsers = [
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Doe', email: 'jane@example.com' }
];
service.get('users').subscribe(users => {
expect(users).toEqual(mockUsers);
});
const req = httpMock.expectOne('https://api.example.com/users');
expect(req.request.method).toBe('GET');
req.flush(mockUsers);
});
});
E2E Testing¶
// e2e/src/app.e2e-spec.ts
import { AppPage } from './app.po';
import { browser, logging } from 'protractor';
describe('new App', () => {
let page: AppPage;
beforeEach(() => {
page = new AppPage();
});
it('should display welcome message', () => {
page.navigateTo();
expect(page.getTitleText()).toContain('Welcome to Ionic');
});
it('should navigate to details page', () => {
page.navigateTo();
page.clickFirstItem();
expect(browser.getCurrentUrl()).toContain('/details');
});
afterEach(async () => {
// Assert that there are no errors emitted from the browser
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
expect(logs).not.toContain(jasmine.objectContaining({
level: logging.Level.SEVERE,
} as logging.Entry));
});
});
// e2e/src/app.po.ts
import { browser, by, element } from 'protractor';
export class AppPage {
navigateTo(): Promise<unknown> {
return browser.get(browser.baseUrl) as Promise<unknown>;
}
getTitleText(): Promise<string> {
return element(by.css('app-home ion-title')).getText() as Promise<string>;
}
clickFirstItem(): Promise<void> {
return element(by.css('app-home ion-item:first-child')).click() as Promise<void>;
}
}
Building and Deployment¶
Build Commands¶
# Build for production
ionic build --prod
# Build with specific configuration
ionic build --configuration=staging
# Build for specific platform
ionic capacitor build android
ionic capacitor build ios
# Build and run
ionic capacitor run android
ionic capacitor run ios
# Build with live reload
ionic capacitor run android --livereload --external
ionic capacitor run ios --livereload --external
Android Deployment¶
# Add Android platform
ionic capacitor add android
# Sync changes
ionic capacitor sync android
# Open in Android Studio
ionic capacitor open android
# Build APK
cd android
./gradlew assembleDebug
./gradlew assembleRelease
# Build AAB (Android App Bundle)
./gradlew bundleRelease
# Install on device
adb install app/build/outputs/apk/debug/app-debug.apk
iOS Deployment¶
# Add iOS platform
ionic capacitor add ios
# Sync changes
ionic capacitor sync ios
# Open in Xcode
ionic capacitor open ios
# Build from command line
xcodebuild -workspace ios/App/App.xcworkspace -scheme App -configuration Release -destination generic/platform=iOS -archivePath ios/App/App.xcarchive archive
# Export IPA
xcodebuild -exportArchive -archivePath ios/App/App.xcarchive -exportPath ios/App/App.ipa -exportOptionsPlist ios/App/ExportOptions.plist
Web Deployment¶
# Build for web
ionic build --prod
# Deploy to Firebase Hosting
npm install -g firebase-tools
firebase login
firebase init hosting
firebase deploy
# Deploy to Netlify
npm install -g netlify-cli
netlify deploy --prod --dir=www
# Deploy to Vercel
npm install -g vercel
vercel --prod
Capacitor Integration¶
Capacitor Configuration¶
// capacitor.config.json
{
"appId": "com.example.myapp",
"appName": "My App",
"webDir": "www",
"bundledWebRuntime": false,
"plugins": {
"Camera": {
"permissions": ["camera", "photos"]
},
"Geolocation": {
"permissions": ["location"]
},
"PushNotifications": {
"presentationOptions": ["badge", "sound", "alert"]
}
},
"server": {
"androidScheme": "https"
}
}
Custom Capacitor Plugin¶
// Create a custom plugin
npm init @capacitor/plugin my-plugin
// src/definitions.ts
export interface MyPluginPlugin {
echo(options: { value: string }): Promise<{ value: string }>;
getDeviceId(): Promise<{ deviceId: string }>;
}
// src/web.ts
import { WebPlugin } from '@capacitor/core';
import type { MyPluginPlugin } from './definitions';
export class MyPluginWeb extends WebPlugin implements MyPluginPlugin {
async echo(options: { value: string }): Promise<{ value: string }> {
console.log('ECHO', options);
return options;
}
async getDeviceId(): Promise<{ deviceId: string }> {
return { deviceId: 'web-device-id' };
}
}
// src/index.ts
import { registerPlugin } from '@capacitor/core';
import type { MyPluginPlugin } from './definitions';
const MyPlugin = registerPlugin<MyPluginPlugin>('MyPlugin', {
web: () => import('./web').then(m => new m.MyPluginWeb()),
});
export * from './definitions';
export { MyPlugin };
// Using the plugin
import { MyPlugin } from 'my-plugin';
export class HomePage {
async usePlugin() {
const result = await MyPlugin.echo({ value: 'Hello World' });
console.log(result.value);
const deviceInfo = await MyPlugin.getDeviceId();
console.log(deviceInfo.deviceId);
}
}
Performance Optimization¶
Lazy Loading¶
// app-routing.module.ts
const routes: Routes = [
{
path: 'home',
loadChildren: () => import('./home/home.module').then(m => m.HomePageModule)
},
{
path: 'profile',
loadChildren: () => import('./profile/profile.module').then(m => m.ProfilePageModule)
}
];
// Preloading strategy
@NgModule({
imports: [
RouterModule.forRoot(routes, {
preloadingStrategy: PreloadAllModules
})
],
exports: [RouterModule]
})
export class AppRoutingModule { }
Virtual Scrolling¶
<!-- For large lists -->
<ion-content>
<ion-virtual-scroll [items]="items" approxItemHeight="70px">
<ion-item *virtualItem="let item; let itemBounds = bounds;">
<ion-avatar slot="start">
<img [src]="item.avatar" />
</ion-avatar>
<ion-label>
<h2>{{ item.name }}</h2>
<p>{{ item.description }}</p>
</ion-label>
</ion-item>
</ion-virtual-scroll>
</ion-content>
Image Optimization¶
// Lazy loading images
export class ImageOptimizationPage {
images = [
{ src: 'assets/images/image1.jpg', loaded: false },
{ src: 'assets/images/image2.jpg', loaded: false },
{ src: 'assets/images/image3.jpg', loaded: false }
];
onImageLoad(image: any) {
image.loaded = true;
}
onImageError(image: any) {
image.src = 'assets/images/placeholder.jpg';
image.loaded = true;
}
}
<!-- Lazy loading template -->
<ion-content>
<div *ngFor="let image of images" class="image-container">
<img
[src]="image.src"
[class.loaded]="image.loaded"
(load)="onImageLoad(image)"
(error)="onImageError(image)"
loading="lazy"
/>
<ion-spinner *ngIf="!image.loaded" name="crescent"></ion-spinner>
</div>
</ion-content>
Bundle Analysis¶
# Analyze bundle size
npm install -g webpack-bundle-analyzer
# Build with stats
ionic build --prod --stats-json
# Analyze
npx webpack-bundle-analyzer www/stats.json
Best Practices¶
Code Organization¶
src/
├── app/
│ ├── core/ # Core functionality (guards, interceptors)
│ ├── shared/ # Shared components, pipes, directives
│ ├── features/ # Feature modules
│ │ ├── auth/ # Authentication feature
│ │ ├── profile/ # Profile feature
│ │ └── settings/ # Settings feature
│ ├── services/ # Global services
│ ├── models/ # Data models and interfaces
│ ├── utils/ # Utility functions
│ └── constants/ # App constants
├── assets/ # Static assets
├── environments/ # Environment configurations
└── theme/ # Global styles and themes
Performance Guidelines¶
// Use OnPush change detection strategy
@Component({
selector: 'app-optimized',
templateUrl: './optimized.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OptimizedComponent {
@Input() data: any;
constructor(private cdr: ChangeDetectorRef) {}
updateData(newData: any) {
this.data = newData;
this.cdr.markForCheck();
}
}
// Use trackBy functions for ngFor
trackByFn(index: number, item: any): any {
return item.id || index;
}
// Unsubscribe from observables
export class ComponentWithSubscriptions implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
ngOnInit() {
this.dataService.getData()
.pipe(takeUntil(this.destroy$))
.subscribe(data => {
// Handle data
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
// Use async pipe when possible
@Component({
template: `
<div *ngFor="let item of items$ | async">
{{ item.name }}
</div>
`
})
export class AsyncPipeComponent {
items$ = this.dataService.getItems();
constructor(private dataService: DataService) {}
}
Security Best Practices¶
// Sanitize user input
import { DomSanitizer } from '@angular/platform-browser';
export class SecurityComponent {
constructor(private sanitizer: DomSanitizer) {}
sanitizeHtml(html: string) {
return this.sanitizer.sanitize(SecurityContext.HTML, html);
}
sanitizeUrl(url: string) {
return this.sanitizer.sanitize(SecurityContext.URL, url);
}
}
// Use environment variables for sensitive data
// environments/environment.prod.ts
export const environment = {
production: true,
apiUrl: 'https://api.production.com',
apiKey: process.env['API_KEY'] // Use environment variables
};
// Implement proper error handling
export class ErrorHandlerService {
handleError(error: any): void {
// Log error to external service
console.error('An error occurred:', error);
// Don't expose sensitive information
const userMessage = this.getUserFriendlyMessage(error);
this.showToast(userMessage);
}
private getUserFriendlyMessage(error: any): string {
if (error.status === 404) {
return 'Resource not found';
} else if (error.status === 500) {
return 'Server error. Please try again later.';
}
return 'An unexpected error occurred';
}
}
Troubleshooting¶
Common Issues and Solutions¶
Build Issues¶
# Clear cache and reinstall
rm -rf node_modules
rm package-lock.json
npm install
# Clear Ionic cache
ionic cache clear
# Reset Capacitor
npx cap clean
npx cap sync
Platform-Specific Issues¶
# Android issues
# Clean and rebuild
cd android
./gradlew clean
cd ..
ionic capacitor sync android
# iOS issues
# Clean derived data
rm -rf ~/Library/Developer/Xcode/DerivedData
cd ios/App
pod install
cd ../..
ionic capacitor sync ios
Runtime Errors¶
// Handle platform-specific code
import { Platform } from '@ionic/angular';
export class PlatformService {
constructor(private platform: Platform) {}
isNative(): boolean {
return this.platform.is('capacitor');
}
isWeb(): boolean {
return !this.platform.is('capacitor');
}
isIOS(): boolean {
return this.platform.is('ios');
}
isAndroid(): boolean {
return this.platform.is('android');
}
async runOnPlatform(callback: () => void): Promise<void> {
await this.platform.ready();
callback();
}
}
// Error boundary component
@Component({
selector: 'app-error-boundary',
template: `
<div *ngIf="hasError; else content" class="error-container">
<ion-icon name="warning-outline"></ion-icon>
<h2>Something went wrong</h2>
<p>{{ errorMessage }}</p>
<ion-button (click)="retry()">Try Again</ion-button>
</div>
<ng-template #content>
<ng-content></ng-content>
</ng-template>
`
})
export class ErrorBoundaryComponent {
hasError = false;
errorMessage = '';
@HostListener('window:error', ['$event'])
handleError(event: ErrorEvent) {
this.hasError = true;
this.errorMessage = event.message || 'An unexpected error occurred';
}
retry() {
this.hasError = false;
this.errorMessage = '';
window.location.reload();
}
}
Summary¶
Ionic is a powerful framework for building cross-platform mobile applications using web technologies. Its key advantages include:
- Cross-Platform Development: Write once, run on iOS, Android, and web
- Web Technologies: Use familiar HTML, CSS, and JavaScript/TypeScript
- Native Performance: Access native device features through Capacitor
- Rich UI Components: Comprehensive library of mobile-optimized components
- Framework Flexibility: Works with Angular, React, and Vue
- Rapid Development: Fast development cycle with live reload
- Strong Ecosystem: Extensive plugin ecosystem and community support
- Cost-Effective: Reduce development time and maintenance costs
Ionic excels at enabling web developers to build native-quality mobile applications while leveraging existing web development skills and maintaining a single codebase across multiple platforms.