Files
travelplanning/android_build_plan.md

18 KiB

Plan: Build ứng dụng Android 11+ từ Codebase hiện tại

Tổng quan

Codebase hiện tại là một web app React + Vite (frontend) và NestJS (backend) theo mô hình monorepo. Chiến lược được đề xuất là dùng Capacitor.js để đóng gói web app thành native Android APK/AAB mà không cần viết lại code, đồng thời bổ sung các tính năng native (camera, GPS, notifications...) qua Capacitor plugins.


So sánh các lựa chọn

Phương pháp Ưu điểm Nhược điểm Phù hợp?
Capacitor.js Tái sử dụng 100% React code, hỗ trợ Vite, ít học thêm Cần build native mỗi lần release Phù hợp nhất
React Native Performance tốt hơn, native feel Phải viết lại toàn bộ UI/components Tốn quá nhiều công
PWA (Add to Home Screen) Không cần build APK Bị giới hạn API trình duyệt, không lên Google Play Không phù hợp
Cordova/PhoneGap Tương tự Capacitor Cũ hơn, ít được duy trì Không nên dùng

→ Quyết định: Sử dụng Capacitor.js (do Ionic team phát triển), hỗ trợ Android API 30+ (Android 11).


Kiến trúc triển khai

┌─────────────────────────────────────────────────────┐
│              Android APK / AAB                      │
│  ┌─────────────────────────────────────────────┐   │
│  │         Capacitor WebView                   │   │
│  │   (Chứa toàn bộ frontend React/Vite)        │   │
│  └──────────────────┬──────────────────────────┘   │
│                     │ HTTPS API calls               │
└─────────────────────┼───────────────────────────────┘
                      ▼
         ┌────────────────────────┐
         │   NestJS Backend       │
         │   (Deployed server /   │
         │    localhost dev)      │
         └────────────────────────┘

Thông tin đã xác nhận

Hạng mục Quyết định
Tên app YoTrip
App ID com.yotrip.app
Phát hành Google Play Store
Camera Native (chụp ảnh trực tiếp từ app)
Backend Docker trên Debian Homelab, proxy qua Nginx
Domain yotrip.labz.io.vn
DNS động Cloudflare DDNS

Điều kiện tiên quyết

Important

Trước khi thực hiện, cần chuẩn bị:

  1. Java JDK 17+ — Android build tool yêu cầu
  2. Android Studio — để build APK, quản lý Android SDK và tạo/chạy Emulator
  3. Android SDK với API Level 30+ (Android 11 / API 30)
  4. Android Virtual Device (AVD) — tạo trong Android Studio AVD Manager
  5. Domain + HTTPS cho Homelab — bắt buộc cho Google Play (xem Phần Bên dưới)

Warning

Google Play bắt buộc HTTPS — Android 9+ chặn HTTP rõ ràng mặc định. Homelab phải có SSL certificate hợp lệ (Let's Encrypt qua Nginx) và domain name trỏ vào homelab server.

Note

Trên emulator, localhost của emulator khác với localhost của máy tính. Phải dùng địa chỉ đặc biệt 10.0.2.2 thay thế khi test emulator (xem Phase 5b).


Proposed Changes

Phase 1a: Cấu hình Backend Homelab (Docker + Nginx)


Important

Đây là điều kiện tiên quyết của toàn bộ kế hoạch. App không thể gửi lên Google Play nếu API chưa có HTTPS.

Homelab của bạn chạy Docker + Nginx là đủ điều kiện kết nối, nhưng cần đảm bảo:

Checklist Homelab/Nginx

Yêu cầu Mô tả
Domain name yotrip.labz.io.vn — đã xác nhận
Port forwarding Router cài đặt forward port 80 và 443 vào Nginx server
SSL Certificate Let's Encrypt (miễn phí) qua Certbot: certbot --nginx -d yotrip.labz.io.vn
Nginx reverse proxy Forward HTTPS → NestJS container port 3001
Docker Compose Backend container luôn restart khi Debian reboot
CORS Backend phải cho phép origin từ app Capacitor (có thể mở rộng * ban đầu)
IP động Dùng Cloudflare DDNS để giữ domain yotrip.labz.io.vn luôn trỏ đúng IP

Mẫu cấu hình Nginx reverse proxy

# /etc/nginx/sites-available/yotrip-api
server {
    listen 443 ssl;
    server_name yotrip.labz.io.vn;

    ssl_certificate /etc/letsencrypt/live/yotrip.labz.io.vn/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yotrip.labz.io.vn/privkey.pem;

    location / {
        proxy_pass http://localhost:3001;
        proxy_http_version 1.1;
        # Cần thiết cho WebSocket (Socket.IO)
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
}

# Redirect HTTP sang HTTPS
server {
    listen 80;
    server_name yotrip.labz.io.vn;
    return 301 https://$host$request_uri;
}

File cấu hình môi trường sẽ dùng

# frontend/.env.production
VITE_API_BASE_URL=https://yotrip.labz.io.vn

# frontend/.env.emulator (test local)
VITE_API_BASE_URL=http://10.0.2.2:3001

Phase 1b: Tập trung API URL trong Frontend

[MODIFY] vite.config.ts

  • Dev proxy hiện tại trỏ về http://localhost:3001 — chỉ hoạt động khi chạy trên browser máy tính.
  • Tạo biến môi trường VITE_API_BASE_URL để frontend biết trỏ đến đâu.

[NEW] .env.production trong frontend/

VITE_API_BASE_URL=https://your-backend-server.com

[NEW] .env.development trong frontend/

# Dùng ngrok hoặc IP máy tính cho mobile dev
VITE_API_BASE_URL=http://192.168.x.x:3001

[MODIFY] Toàn bộ các file gọi /api/v1/...

  • Thay fetch('/api/v1/...') bằng fetch(\${import.meta.env.VITE_API_BASE_URL}/api/v1/...`)`
  • Ưu tiên: Tạo một file src/lib/api.ts (helper) để tập trung URL, tránh sửa từng file.
// src/lib/api.ts
export const API_BASE = import.meta.env.VITE_API_BASE_URL || '';

export const apiFetch = (path: string, options?: RequestInit) =>
  fetch(`${API_BASE}${path}`, options);

Phase 2: Tích hợp Capacitor


Cài đặt Capacitor vào frontend

# Trong thư mục frontend/
npm install @capacitor/core @capacitor/cli
npx cap init "YoTrip" "com.yotrip.app" --web-dir dist
npm install @capacitor/android
npx cap add android

[NEW] frontend/capacitor.config.ts

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

const config: CapacitorConfig = {
  appId: 'com.yotrip.app',      // ✅ App ID chính thức
  appName: 'YoTrip',            // ✅ Tên app
  webDir: 'dist',
  server: {
    // Production: không cần cấu hình, dùng VITE_API_BASE_URL trong build
    // Dev/Emulator: uncomment dòng dưới
    // url: 'http://10.0.2.2:3002',
    // cleartext: true,
  },
  android: {
    minSdkVersion: 30,          // Android 11 (API 30)
    targetSdkVersion: 34,       // Android 14
    buildOptions: {
      keystorePath: 'release-key.keystore',
      keystoreAlias: 'yotrip',
    }
  },
  plugins: {
    SplashScreen: {
      launchAutoHide: false,
      backgroundColor: '#0f172a',
      androidSplashResourceName: 'splash',
    },
    // Camera plugin config (bắt buộc vì app cần chụp ảnh)
    Camera: {
      presentationStyle: 'fullscreen',
    }
  }
};

export default config;

Phase 3: Android Project Setup


Build flow

# Bước 1: Build React app thành static files
npm run build -w frontend

# Bước 2: Copy static files vào Android project
npx cap copy android

# Bước 3: Sync plugins và dependencies
npx cap sync android

# Bước 4: Mở Android Studio để build APK/AAB
npx cap open android

[MODIFY] android/app/src/main/AndroidManifest.xml (tự sinh bởi Capacitor)

Thêm các permissions cần thiết:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<!-- Android 11+ scoped storage -->
<uses-permission android:name="android.permission.MANAGE_MEDIA" />

Phase 4: Native Plugins (Bắt buộc)

Plugin Mức độ Mục đích Package
@capacitor/camera Bắt buộc Chụp ảnh trực tiếp từ camera + gallery @capacitor/camera
@capacitor/geolocation Bắt buộc GPS thiết bị cho bản đồ @capacitor/geolocation
@capacitor/splash-screen Bắt buộc Màn hình khởi động @capacitor/splash-screen
@capacitor/status-bar Bắt buộc Màu status bar đồng bộ UI @capacitor/status-bar
@capacitor/push-notifications 🔲 Tùy chọn Thông báo bình luận, tour @capacitor/push-notifications
@capacitor/network 🔲 Tùy chọn Kiểm tra kết nối mạng @capacitor/network

Cách dùng Camera plugin trong code (thay thế input file)

// Thay thế <input type="file"> bằng Capacitor Camera API
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';

const takePhoto = async () => {
  const photo = await Camera.getPhoto({
    resultType: CameraResultType.DataUrl, // Hoặc Uri cho hiệu năng tốt hơn
    source: CameraSource.Prompt,          // Hỏi: Camera hay Gallery?
    quality: 85,
  });
  // photo.dataUrl → upload lên backend
};

Phase 5: Điều chỉnh UI cho Mobile

Một số thành phần hiện tại đã có responsive CSS, nhưng cần kiểm tra thêm:

  • PublicPhotoModal.tsx: Đã có kế hoạch fix mobile layout (từ plan cũ), cần implement trước khi build.
  • ExploreMap.tsx: Leaflet map cần đảm bảo touch events hoạt động (thường OK trên mobile).
  • TourDetailPage.tsx: Kiểm tra scroll behavior, fixed headers.
  • Safe Area Insets: Dùng env(safe-area-inset-*) cho các thiết bị có notch/dynamic island.
/* Thêm vào index.css */
:root {
  --safe-top: env(safe-area-inset-top, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
}

Phase 5b: Test trên Android Emulator (AVD)

Important

Đây là bước bắt buộc trước khi test trên thiết bị thật. Emulator giúp phát hiện lỗi layout, API call, và native permissions mà không cần thiết bị vật lý.

Tạo Android Virtual Device (AVD)

  1. Mở Android Studio → Tools → Device Manager → Create Device
  2. Chọn Pixel 6 (hoặc tương đương) → chọn hệ thống Android 11.0 (API 30)
  3. Cấp RAM 2GB+, Storage 4GB+ cho emulator
  4. Khởi động emulator, đảm bảo hiện "Emulator is running"

Địa chỉ đặc biệt trong Emulator

10.0.2.2  →  Trỏ đến localhost (127.0.0.1) của máy tính host

Khi chạy trong emulator, backend ở localhost:3001 của máy tính phải được gọi bằng 10.0.2.2:3001.

Cấu hình capacitor.config.ts cho Emulator Dev

// Tạm thời uncomment khi test trên emulator
server: {
  url: 'http://10.0.2.2:3002', // Trỏ đến Vite dev server trên máy host
  cleartext: true,             // Cho phép HTTP (không dùng trong production)
},

Hoặc dùng biến môi trường .env.emulator:

VITE_API_BASE_URL=http://10.0.2.2:3001

Chạy app trên Emulator

# Bước 1: Đảm bảo emulator đang chạy
adb devices
# Phải thấy: emulator-5554  device

# Bước 2: Build & sync
npm run build -w frontend
npx cap copy android && npx cap sync android

# Bước 3: Chạy trực tiếp trên emulator
npx cap run android
# hoặc trong Android Studio: Run ▶ chọn emulator

Checklist kiểm tra trên Emulator

Chức năng Test case Kết quả mong đợi
Khởi động Mở app Splash screen → Landing page
Đăng nhập Nhập email/password Vào ExploreMap
Bản đồ Zoom/pan trên emulator Leaflet map hoạt động
Upload ảnh Chọn ảnh từ gallery giả lập Ảnh được upload thành công
Bình luận Nhập & gửi bình luận Hiện real-time qua WebSocket
Safe area Xoay màn hình Layout không bị che
Responsive Portrait/Landscape UI không bị vỡ

Phase 6: Test trên thiết bị Android thật

Note

Chỉ tiến hành Phase này sau khi toàn bộ Phase 5b đã pass. Thiết bị thật giúp phát hiện lỗi về hiệu năng, cảm biến thực tế, và hành vi network.

Kết nối thiết bị

# Bật Developer Mode + USB Debugging trên điện thoại
# Cắm cáp USB → xác nhận "Allow USB Debugging"
adb devices
# Phải thấy: <serial_number>  device

Cấu hình backend cho thiết bị thật

Thiết bị thật phải dùng IP LAN hoặc ngrok (không thể dùng 10.0.2.2):

# Tùy chọn A: Dùng IP LAN (điện thoại và máy tính cùng WiFi)
VITE_API_BASE_URL=http://192.168.x.x:3001

# Tùy chọn B: Dùng ngrok (tiện hơn, không cần cùng mạng)
ngrok http 3001
# → VITE_API_BASE_URL=https://xxxx.ngrok-free.app

Cài debug APK lên thiết bị thật

# Build debug APK
cd frontend/android && ./gradlew assembleDebug

# Cài APK lên thiết bị
adb install app/build/outputs/apk/debug/app-debug.apk

Phase 7: Ký APK và phát hành

Debug build (dev testing)

cd android && ./gradlew assembleDebug
# Output: android/app/build/outputs/apk/debug/app-debug.apk

Release build (production)

# Tạo keystore lần đầu
keytool -genkey -v -keystore release-key.keystore -alias yotrip \
  -keyalg RSA -keysize 2048 -validity 10000

# Build release
cd android && ./gradlew bundleRelease
# Output: .aab file để upload Google Play

Checklist thực hiện

Phase 1 - Backend URL
[ ] Tạo file src/lib/api.ts (centralised fetch helper)
[ ] Refactor tất cả fetch('/api/v1/...) sang apiFetch()
[ ] Tạo .env.production với VITE_API_BASE_URL
[ ] Tạo .env.emulator với VITE_API_BASE_URL=http://10.0.2.2:3001

Phase 2 - Capacitor Setup
[ ] npm install @capacitor/core @capacitor/cli @capacitor/android
[ ] npx cap init với App ID và tên app
[ ] Tạo capacitor.config.ts
[ ] npx cap add android

Phase 3 - Build & Sync
[ ] npm run build -w frontend (kiểm tra không có lỗi TypeScript)
[ ] npx cap copy android && npx cap sync android
[ ] Cấu hình AndroidManifest.xml với permissions

Phase 4 - Native Plugins
[ ] Cài @capacitor/geolocation (thay navigator.geolocation)
[ ] Cài @capacitor/camera (upload ảnh từ điện thoại)
[ ] Cài @capacitor/splash-screen, @capacitor/status-bar

Phase 5 - Mobile UI Polish
[ ] Implement mobile layout fixes cho PublicPhotoModal.tsx
[ ] Kiểm tra safe-area-inset cho Android

Phase 5b - Test trên Android Emulator (AVD)
[ ] Tạo AVD Android 11 (API 30) trong Android Studio
[ ] Cấu hình capacitor.config.ts server.url = http://10.0.2.2:3002
[ ] Khởi động emulator → adb devices xác nhận kết nối
[ ] npx cap run android → kiểm tra app khởi động
[ ] Kiểm tra toàn bộ checklist: Login, Map, Upload, Comment, SafeArea
[ ] Sửa tất cả lỗi phát sinh trên emulator

Phase 6 - Test trên thiết bị Android 11 thật
[ ] Bật Developer Mode + USB Debugging trên điện thoại
[ ] adb devices xác nhận thiết bị kết nối
[ ] Cấu hình VITE_API_BASE_URL = IP LAN hoặc ngrok
[ ] Build debug APK → adb install
[ ] Kiểm tra lại toàn bộ checklist trên thiết bị thật
[ ] Kiểm tra performance, pin, cảm biến GPS thực tế

Phase 7 - Build Release
[ ] Build debug APK để test
[ ] Tạo keystore và build release AAB
[ ] Chuẩn bị lên Google Play (nếu cần)

Verification Plan

Automated

  • npm run build -w frontend — không có lỗi TypeScript/build
  • adb devices — xác nhận emulator/thiết bị thật đang kết nối

Stage 1: Test trên Android Emulator

  1. App khởi động không crash, hiện Landing page đúng.
  2. Đăng nhập / Đăng ký hoạt động (kết nối 10.0.2.2:3001).
  3. Bản đồ Leaflet zoom/pan bằng cảm ứng mô phỏng.
  4. Upload ảnh từ gallery giả lập của emulator.
  5. Bình luận real-time qua WebSocket hoạt động.
  6. Layout không bị vỡ khi xoay màn hình (portrait/landscape).
  7. Safe-area-inset không bị che bởi status bar.

Stage 2: Test trên thiết bị Android 11 thật

  1. Lặp lại toàn bộ Stage 1 trên thiết bị thật.
  2. GPS thực tế hoạt động và hiện đúng vị trí trên bản đồ.
  3. Camera native chụp và upload ảnh thành công.
  4. Performance mượt mà (scroll, animation không giật).
  5. WebSocket giữ kết nối ổn định trên mobile network (4G/WiFi).
  6. Kiểm tra pin consumption không bất thường.

Các quyết định đã xác nhận

Câu hỏi Trả lời
Backend deploy ở đâu? Docker trên Debian homelab, proxy qua Nginx
Tên app và App ID? YoTripcom.yotrip.app
Domain? yotrip.labz.io.vn (Cloudflare DDNS)
Phát hành? Đưa lên Google Play Store
Camera? Native camera — chụp ảnh trực tiếp từ app

Note

Lưu ý quan trọng về Homelab + Google Play:

  • Homelab + Docker + Nginx là đủ điều kiện kết nối cho app Android.
  • Domain yotrip.labz.io.vn dùng Cloudflare DDNS — IP homelab thay đổi sẽ được cập nhật tự động.
  • WebSocket (Socket.IO) cần Nginx được cấu hình proxy_set_header Upgrade (xem Phase 1a).
  • Certbot cấp SSL cho yotrip.labz.io.vn: certbot --nginx -d yotrip.labz.io.vn