4.5 KiB
4.5 KiB
Hybrid Android Reference (Capacitor + Ionic / React)
When to Use Hybrid
✅ Good fit:
- Web team building a companion Android app
- Content-heavy apps (news, docs, forms)
- PWA upgrade to installable app
- Rapid prototyping
❌ Avoid for:
- Real-time games / heavy animations
- Deep native sensor / hardware access
- Apps requiring 60fps custom animations
- Bluetooth/NFC intensive apps (use plugins, but complex)
Stack Options
| Option | UI Framework | Best For |
|---|---|---|
| Capacitor + Ionic | Ionic components | Full mobile-optimized UI |
| Capacitor + React | React + Tailwind | Web team reuse |
| Capacitor + Vue | Vue + Ionic | Vue teams |
| Capacitor + Angular | Angular + Ionic | Enterprise Angular teams |
Project Structure (Capacitor + React)
src/
├── App.tsx
├── pages/ # Screen components
├── components/ # Shared UI components
├── hooks/ # Business logic hooks
├── services/ # API, storage services
└── store/ # State management
android/ # Native Android project (generated)
├── app/src/main/
│ ├── AndroidManifest.xml
│ └── java/.../MainActivity.kt
capacitor.config.ts # Capacitor configuration
Capacitor Config
// capacitor.config.ts
import { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.example.app',
appName: 'My App',
webDir: 'dist',
server: {
androidScheme: 'https',
},
android: {
buildOptions: {
releaseType: 'APK', // or AAB for Play Store
},
},
plugins: {
SplashScreen: {
launchShowDuration: 0,
backgroundColor: '#FFFFFF',
},
PushNotifications: {
presentationOptions: ['badge', 'sound', 'alert'],
},
},
};
Native Plugin Usage
import { Camera, CameraResultType } from '@capacitor/camera';
import { Preferences } from '@capacitor/preferences';
import { PushNotifications } from '@capacitor/push-notifications';
import { Geolocation } from '@capacitor/geolocation';
// Camera
const takePhoto = async () => {
const photo = await Camera.getPhoto({
quality: 90,
allowEditing: false,
resultType: CameraResultType.Uri,
});
return photo.webPath;
};
// Secure storage: do not store auth tokens in Capacitor Preferences.
// Use a platform-backed secure storage plugin such as
// @aparajita/capacitor-secure-storage, Ionic Identity Vault, or an
// equivalent Android Keystore-backed plugin.
const saveToken = async (token: string) => {
await SecureStorage.set({ key: 'auth_token', value: token });
};
const getToken = async (): Promise<string | null> => {
const { value } = await SecureStorage.get({ key: 'auth_token' });
return value;
};
// Push notifications
const initPush = async () => {
const permission = await PushNotifications.requestPermissions();
if (permission.receive === 'granted') {
await PushNotifications.register();
}
PushNotifications.addListener('registration', ({ value: token }) => {
console.log('FCM Token:', token);
});
};
Performance Best Practices
- Ensure hardware acceleration is enabled for the application in AndroidManifest.xml (default in Capacitor)
- Enable HTTP caching in Android WebView settings
- Lazy-load routes with React.lazy / dynamic imports
- Avoid
setTimeout/setIntervalfor animations; use CSS transitions - Use
@ionic/reactcomponents — they handle mobile-specific touch handling - Ionic virtual scroll for long lists
Build & Deploy
# Build web assets
npm run build
# Sync to native
npx cap sync android
# Open in Android Studio
npx cap open android
# Build release APK/AAB via Android Studio or:
cd android && ./gradlew bundleRelease
Custom Native Plugin (when built-in plugins don't cover it)
// android/app/src/main/java/.../MyPlugin.kt
@CapacitorPlugin(name = "MyPlugin")
class MyPlugin : Plugin() {
@PluginMethod
fun doNativeWork(call: PluginCall) {
val value = call.getString("input") ?: return call.reject("No input")
// Do native work
val result = JSObject()
result.put("output", "processed: $value")
call.resolve(result)
}
}
// TypeScript usage
import { registerPlugin } from '@capacitor/core';
const MyPlugin = registerPlugin<{ doNativeWork: (opts: { input: string }) => Promise<{ output: string }> }>('MyPlugin');
const result = await MyPlugin.doNativeWork({ input: 'hello' });