App Android ortu — Kotlin + Jetpack Compose, repo ZX-Parenting-ortu
App ortu pantau anak — simple, fast, reliable.
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 ReleasePlay Store: in-app update (FLEXIBLE), Credential Manager (Google Sign-In)
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-layarMainActivity (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.
20 layar Composable (ui/layar/Layar*.kt):
Auth (2):
LayarLogin — email+password, Google Sign-In (Credential Manager)LayarDaftar — nama, email, password, setuju consentBottom Tab (5):
LayarBeranda (tab 0) — dashboard: nama, tier, saldo coin, quick tiles (14 menu), list anak+tugasLayarAnak (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 coinLayarProfil (tab 4) — nama, logout, hapus akunSub-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)*
3 komponen data:
1. Simpanan (data/Simpanan.kt) — DataStore Preferences
token, nama, userId (Flow<String?>)simpanSesi(token, nama, uid) — save loginhapus() — clear (logout)zxortu2. Klien (api/Klien.kt) — Retrofit2 singleton
https://zxparenting.zomet.my.id/api/val api: ApiZx by lazy { ... }3. ApiZx (api/Api.kt) — Retrofit interface, ~40 endpoints
Data classes: ~40 data class (request/response models), Gson @SerializedName untuk snake_case
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 constructionZxVm (ui/ZxVm.kt, 757 lines):
val state = MutableStateFlow(UiState())init {} — cek DataStore token, auto-login jika adasuksesLogin(body) — simpan sesi, muatSemua(token)muatSemua(token) — 8 parallel fetch: devices, anak, tugas, market, coin, quest, forum, langgananmuatAktivitas, muatInsights, muatPinjam, muatChallenges, muatLeaderboard, muatThread, muatForumDetailZxVmFactory(simpanan) — factory untuk ViewModel constructionTheme system (ui/tema/):
Palet (Palet.kt) — JSON-driven color palette
#2563EB, bg #EFF6FFTemaRepo — load dari assets/tema/*.json, swap runtime via WarnaAktif.ganti(palet)Palet.dariJson() — parse JSON ke PaletTema (Tema.kt):
WarnaAktif — singleton mutable palet, auto-recomposeTemaZX — Material3 wrapper, auto light/dark via luminansiTipografi — titleLarge 19sp Bold, titleMedium 15sp Bold, bodyMedium 13sp, bodySmall 11spi18n (Teks.kt):
enum class Bahasa { ID, EN }object Teks — dictionary map, setBahasa(), operator []build.gradle.kts (app/):
namespace = "com.zxparenting.ortu"compileSdk = 35, minSdk = 24, targetSdk = 35applicationId = baca dari alamat.json (dynamic)versiNama/versiKode = baca dari alamat.jsonVersion 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
⚠ 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".
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)
https://imgur.com/yGxZcSr
https://imgur.com/631sZeT
https://imgur.com/ynz4vy7
https://imgur.com/S3OGvlg
https://imgur.com/BN50YZ2
https://zxparenting.zomet.my.id/design/icon-download.html
