Home›ZX Parenting›ZX Parenting›App Ortu (Android)
AKTIF2.9.7kotlincomposeandroidortu

App Ortu (Android)

App Android ortu — Kotlin + Jetpack Compose, repo ZX-Parenting-ortu

JSON

💡 Ide & Konsep

Tujuan

App ortu pantau anak — simple, fast, reliable.

  • Target: ortu Indonesia 25-45 thn
  • Core: tugas + reward + screen time
  • Differentiator: AI insights + gamifikasi

Ringkasan

App Android untuk orang tua pantau anak. Kotlin + Jetpack Compose.

Repo: github.com/djvpri/ZX-Parenting-ortu

Local: /opt/data/zx-ortu

Branch: main

Versi: v2.9.7 (versiKode 35)

Package: com.zxparenting.ortu

Min SDK: 24 (Android 7.0+)

Target/Compile SDK: 35 (Android 15)

Config: alamat.json (versiNama, versiKode, idPaket, beranda, api, googleWebId)

API base: https://zxparenting.zomet.my.id/api/

CI:

  • ci.yml — auto-trigger push, ./gradlew assembleDebug (compile check)
  • rilis.yml — manual workflow_dispatch input versi, build AAB + APK, tag, GitHub Release

Play Store: in-app update (FLEXIBLE), Credential Manager (Google Sign-In)

Arsitektur

Pattern: MVVM (Model-ViewModel-View) + Repository

MainActivity (ComponentActivity)
├── TemaZX (Material3 wrapper)
├── Simpanan (DataStore — token, nama, uid)
├── ZxVm : ViewModel (state = MutableStateFlow<UiState>)
│   ├── Klien.api : ApiZx (Retrofit2)
│   └── muatSemua(token) — 8 parallel fetch
└── Scaffold
    ├── NavigationBar (5 tab)
    └── Content: 5 tab + 14 sub-layar

MainActivity (ComponentActivity)

├── TemaZX (Material3 wrapper)

├── Simpanan (DataStore — token, nama, uid)

├── ZxVm : ViewModel (state = MutableStateFlow<UiState>)

│ ├── Klien.api : ApiZx (Retrofit2)

│ └── muatSemua(token) — 8 parallel fetch

└── Scaffold

├── NavigationBar (5 tab)

└── Content: 5 tab + 14 sub-layar


**State:** single `UiState` data class, `MutableStateFlow`, `collectAsState()` di Compose.

**Navigation:** tidak pakai Navigation Compose. Tab = `mutableIntStateOf`, subLayar = `mutableIntStateOf`. Beranda punya grid tile → klik set `subLayar` 1-13.

**Coroutines:** `viewModelScope.launch` untuk semua API call. `Dispatchers.IO` implicit via Retrofit suspend.

State: single UiState data class, MutableStateFlow, collectAsState() di Compose.

Navigation: tidak pakai Navigation Compose. Tab = mutableIntStateOf, subLayar = mutableIntStateOf. Beranda punya grid tile → klik set subLayar 1-13.

Coroutines: viewModelScope.launch untuk semua API call. Dispatchers.IO implicit via Retrofit suspend.

UI / Layar

20 layar Composable (ui/layar/Layar*.kt):

Auth (2):

  • LayarLogin — email+password, Google Sign-In (Credential Manager)
  • LayarDaftar — nama, email, password, setuju consent

Bottom Tab (5):

  • LayarBeranda (tab 0) — dashboard: nama, tier, saldo coin, quick tiles (14 menu), list anak+tugas
  • LayarAnak (tab 1) — list anak, form buat/edit, DialogEditAnak, DialogResetPin (v2.9.7)
  • LayarTugas (tab 2) — list tugas per anak, form buat, validasi (selesai/tolak)
  • LayarMarketplace (tab 3) — hadiah CRUD, pesanan proses (beli/tolak), saldo coin
  • LayarProfil (tab 4) — nama, logout, hapus akun

Sub-layar dari Beranda (14):

1. LayarAturan — device control: sessionLimit, cooldown, dailyLimit, curfew, appBlokir, masterUnlock

2. LayarLokasi — GPS toggle, geo-fence (lat, lng, radius), lastSeen

3. LayarLaporan — activity log per anak, ringkasan 7 hari

4. LayarCoin — saldo ZX Coin, ledger, topup/penyesuaian

5. LayarQuest — family quest list, buat quest

6. LayarForum — forum posts, detail + komentar, buat/hapus

7. LayarLangganan — tier (FREE_TRIAL/PRO/ELITE), hari sisa, checkout Midtrans

8. LayarPesan — direct chat list + thread

9. LayarInsights — AI insights per anak (Gemini/rule-based)

10. LayarPinjam — pinjam waktu + hutang

11. LayarReferral — kode referral, klaim

12. LayarLeaderboard — ranking token mingguan, scope global

13. LayarChallenges — daily challenges + claim

14. *(slot 14 reserved)*

Data Layer

3 komponen data:

1. Simpanan (data/Simpanan.kt) — DataStore Preferences

  • token, nama, userId (Flow<String?>)
  • simpanSesi(token, nama, uid) — save login
  • hapus() — clear (logout)
  • Store name: zxortu

2. Klien (api/Klien.kt) — Retrofit2 singleton

  • Base URL: https://zxparenting.zomet.my.id/api/
  • OkHttpClient: HttpLoggingInterceptor (BODY level)
  • Converter: Gson
  • val api: ApiZx by lazy { ... }

3. ApiZx (api/Api.kt) — Retrofit interface, ~40 endpoints

  • Auth: login, daftar, Google
  • Anak: list, create, detail, update, delete, resetPin
  • Tugas: list, create, validasi
  • Device: list, patch, delete
  • Coin: get, post (topup)
  • Hadiah: list, create, patch, delete, prosesPesanan
  • Quest: list, create
  • Langganan: status
  • Pesan: list, thread, kirim
  • Forum: list, create, detail, delete, komentar
  • Insights, Pinjam, Referral, Leaderboard, Challenges

Data classes: ~40 data class (request/response models), Gson @SerializedName untuk snake_case

State Management

UiState — single data class, semua state UI di satu tempat:

data class UiState(
    loading, error, loginOk, nama, token,
    devices: List<Device>,
    anakList: List<Anak>,
    tugasList: List<Tugas>,
    aktivitas: AktivitasRes?,
    market: HadiahMarketRes?,
    coin: CoinRes?,
    quests: List<Quest>,
    forumPosts: List<ForumPost>,
    forumDetail: ForumPost?,
    langganan: LanggananRes?,
    pesanList, pesanThread,
    insights: InsightsRes?,
    pinjam: PinjamRes?,
    referral: ReferralRes?,
    leaderboard: LeaderboardRes?,
    challenges: ChallengesRes?,
    claimResult: String?,
)

data class UiState(

loading, error, loginOk, nama, token,

devices: List<Device>,

anakList: List<Anak>,

tugasList: List<Tugas>,

aktivitas: AktivitasRes?,

market: HadiahMarketRes?,

coin: CoinRes?,

quests: List<Quest>,

forumPosts: List<ForumPost>,

forumDetail: ForumPost?,

langganan: LanggananRes?,

pesanList, pesanThread,

insights: InsightsRes?,

pinjam: PinjamRes?,

referral: ReferralRes?,

leaderboard: LeaderboardRes?,

challenges: ChallengesRes?,

claimResult: String?,

)


**ZxVm** (`ui/ZxVm.kt`, 757 lines):
- `val state = MutableStateFlow(UiState())`
- `init {}` — cek DataStore token, auto-login jika ada
- `suksesLogin(body)` — simpan sesi, `muatSemua(token)`
- `muatSemua(token)` — 8 parallel fetch: devices, anak, tugas, market, coin, quest, forum, langganan
- Sub-layar data lazy-load: `muatAktivitas`, `muatInsights`, `muatPinjam`, `muatChallenges`, `muatLeaderboard`, `muatThread`, `muatForumDetail`
- `ZxVmFactory(simpanan)` — factory untuk ViewModel construction

ZxVm (ui/ZxVm.kt, 757 lines):

  • val state = MutableStateFlow(UiState())
  • init {} — cek DataStore token, auto-login jika ada
  • suksesLogin(body) — simpan sesi, muatSemua(token)
  • muatSemua(token) — 8 parallel fetch: devices, anak, tugas, market, coin, quest, forum, langganan
  • Sub-layar data lazy-load: muatAktivitas, muatInsights, muatPinjam, muatChallenges, muatLeaderboard, muatThread, muatForumDetail
  • ZxVmFactory(simpanan) — factory untuk ViewModel construction

Theming & i18n

Theme system (ui/tema/):

Palet (Palet.kt) — JSON-driven color palette

  • 16 colors: bg, kartu, utama, aksen, teks, teksMuted, muted, border, biru, ungu, amber, hijau, merah, pink, biruGelap
  • Default: "Clay" — biru utama #2563EB, bg #EFF6FF
  • TemaRepo — load dari assets/tema/*.json, swap runtime via WarnaAktif.ganti(palet)
  • Palet.dariJson() — parse JSON ke Palet

Tema (Tema.kt):

  • WarnaAktif — singleton mutable palet, auto-recompose
  • TemaZX — Material3 wrapper, auto light/dark via luminansi
  • Tipografi — titleLarge 19sp Bold, titleMedium 15sp Bold, bodyMedium 13sp, bodySmall 11sp
  • Shape radius: besar 22dp, kartu 18dp, kecil 12dp

i18n (Teks.kt):

  • enum class Bahasa { ID, EN }
  • object Teks — dictionary map, setBahasa(), operator []
  • Keys: beranda, anak, tugas, aturan, profil, market, coin, quest, lokasi, laporan, forum, langganan, pesan, insights
  • ponytail: scaffold ID/EN. Upgrade: strings.xml + per-language resource

Build & CI

build.gradle.kts (app/):

  • namespace = "com.zxparenting.ortu"
  • compileSdk = 35, minSdk = 24, targetSdk = 35
  • applicationId = baca dari alamat.json (dynamic)
  • versiNama/versiKode = baca dari alamat.json
  • Java 17 (Temurin)
  • Google Sign-In: Credential Manager + credentials-play-services-auth

Version config: alamat.json

{
  "idPaket": "com.zxparenting.ortu",
  "versiNama": "2.9.7",
  "versiKode": 35,
  "beranda": "https://zxparenting.zomet.my.id",
  "api": "https://zxparenting.zomet.my.id/api",
  "googleWebId": "117197293834-..."
}

{

"idPaket": "com.zxparenting.ortu",

"versiNama": "2.9.7",

"versiKode": 35,

"beranda": "https://zxparenting.zomet.my.id",

"api": "https://zxparenting.zomet.my.id/api",

"googleWebId": "117197293834-..."

}


**CI (`ci.yml`):** auto-trigger push → `assembleDebug` (compile check, no keystore)

**Rilis (`rilis.yml`):** manual `workflow_dispatch` input `versi`:
1. Bump `alamat.json` (versiNama + versiKode+1)
2. Keystore: repo file (`keystore/rilis.keystore` + `.pw`) atau Secrets
3. `bundleRelease assembleRelease` → AAB + APK
4. Commit versi, tag `v{versi}`, GitHub Release

**In-app update:** `AppUpdateManager` (FLEXIBLE type), dialog "Update tersedia"

**Google Sign-In:** Credential Manager API, `GetGoogleIdOption` + fallback `GetSignInWithGoogleOption`

**CI (ci.yml):** auto-trigger push → assembleDebug (compile check, no keystore)

**Rilis (rilis.yml):** manual workflow_dispatch input versi:

1. Bump alamat.json (versiNama + versiKode+1)

2. Keystore: repo file (keystore/rilis.keystore + .pw) atau Secrets

3. bundleRelease assembleRelease → AAB + APK

4. Commit versi, tag v{versi}, GitHub Release

In-app update: AppUpdateManager (FLEXIBLE type), dialog "Update tersedia"

Google Sign-In: Credential Manager API, GetGoogleIdOption + fallback GetSignInWithGoogleOption

Pitfall

⚠ alamat.json = source of truth versi. Bukan build.gradle.kts. CI rilis bump versiKode+1 otomatis.

⚠ Tidak pakai Navigation Compose. Tab = mutableIntStateOf, subLayar = mutableIntStateOf. Sederhana tapi tidak scalable.

⚠ UiState single data class — semua state di satu object. Besar (20+ fields) tapi simple.

⚠ muatSemua() launch 8 coroutine parallel tanpa join. Race condition tidak di-handle tapi praktis OK karena masing-maku update field beda.

⚠ Tema via JSON assets/tema/*.json. Tambah tema = drop file JSON. Tidak download remote.

⚠ i18n via object Teks in-memory map. Bukan strings.xml. Ponytail: upgrade saat multi-negara.

⚠ Google Sign-In butuh app terinstall dari Play Store (Credential Manager constraint). Fallback: GetSignInWithGoogleOption.

⚠ GOOGLE_WEB_ID hardcoded di MainActivity.companion. Juga di alamat.json.googleWebId.

⚠ Verify via CI only — WSL Hermes tidak punya Java/Android SDK. Tidak bisa local build.

⚠ Container Coolify ID berubah tiap deploy. App ortu tidak terdampak (pakai domain zxparenting.zomet.my.id).

⚠ Reset PIN 3 iterasi bug fix — lihat child blueprint "Auth Anak (PIN)" bagian "Reset PIN Flow".

Catatan Rilis App

v2.9.7 — pisah DialogResetPin dari DialogEditAnak (3 iterasi fix reset PIN)

v2.9.6 — pesan error reset PIN langsung dari callback (sukses, pesan)

v2.9.5 — reset PIN async callback + loading spinner

v2.9.4 — edit username anak + AnakUpdateReq.username + AnakResetPinReq + anakResetPin @PATCH

v2.9.3 — fmtTgl() fix ISO 8601 → yyyy-MM-dd (substring 0,10)

v2.9.2 — DatePicker native + dropdown gender

v2.9.1 — edit + hapus anak (PATCH/DELETE anak/{id}), tanggalLahir ganti umur

v2.9.0 — referral + leaderboard + challenges + family quest

v2.8.0 — pinjam waktu + AI insights (Gemini)

v2.7.1 — i18n scaffold (Bahasa ID/EN)

SCREENSHOOT

https://imgur.com/yGxZcSr

https://imgur.com/631sZeT

https://imgur.com/ynz4vy7

https://imgur.com/S3OGvlg

https://imgur.com/BN50YZ2

ICON

https://zxparenting.zomet.my.id/design/icon-download.html

GAMBAR FITUR

Sub-blueprint

Laporandraft
Menu Tugasaktif