Native Kotlin + Jetpack Compose app for BetterLectio.
See NATIVE_ANDROID_PLAN.md for the full product/architecture plan.
- Android Studio (current stable / 2026.x)
- JDK 17+ (Studio embedded JBR is fine)
- Android SDK 36
- Open the
android/folder in Android Studio (not the monorepo root). - Wait for Gradle sync (Studio writes
local.propertieswithsdk.dir). - Optional: copy
local.properties.example→local.propertiesand set PostHog / Supabase overrides. - Run the app configuration on an emulator or device.
CLI:
cd android
# local.properties must contain sdk.dir=… (Studio creates this)
./gradlew :app:assembleDebug
./gradlew :app:testDebugUnitTestSigning keys, key.properties, local.properties, and build outputs are gitignored — see .gitignore and SIGNING.md.
| Application ID | dk.betterlectio.android |
| Namespace | dk.betterlectio.android |
| Min SDK | 29 |
| Target / compile SDK | 36 |
Debug builds append .debug to the application id.
app/src/main/java/dk/betterlectio/android/
├── BetterLectioApp.kt # Hilt Application + Timber
├── MainActivity.kt
├── core/
│ ├── di/ # Hilt modules (OkHttp, …)
│ ├── model/ # Domain models (Student, School)
│ └── result/ # AppResult / AppError
└── ui/
├── components/
├── navigation/ # Bottom bar + NavHost (iOS tabs)
├── screens/ # Feature placeholders
└── theme/ # Brand Material 3 + Inter/Fraunces
- Kotlin, Jetpack Compose, Material 3 (AGP 9.2 / Compose BOM 2026.02)
- Navigation Compose
- Hilt 2.60+ + KSP (required for AGP 9)
- Coroutines / Flow
- OkHttp, Jsoup (ready for Lectio client)
- DataStore, Security Crypto
- Coil 3
- Timber
- Supabase Kotlin (postgrest / auth / functions) — optional enhancement layer, iOS parity
Defaults match iOS Info.plist (public project URL + publishable key). Core Lectio works without a session.
| Capability | Implementation |
|---|---|
| School list | schools table via PostgREST |
| Auth | Edge Function token-for-auth → magic-link OTP; cookie rotation synced to encrypted store |
| Homework done | RPCs + LWW merge (clientUpdatedAt); only numeric Lectio abs ids sync |
| Schedule | Best-effort week sync → lessons / student_lessons / week_sync |
| Lesson content | Best-effort lessons.content after detail open |
| Subjects | Mappings v2 RPCs; pull after session ready; push on edit |
| Session gate | Remote RPCs wait (≤15s) for login/cold-start mint so RLS does not race |
Override in local.properties or env:
SUPABASE_URL=https://….supabase.co
SUPABASE_ANON_KEY=sb_publishable_…
# or SUPABASE_PUBLISHABLE_KEY=…Studio generated AGP 9 with built-in Kotlin. We set android.disallowKotlinSourceSets=false so KSP/Hilt can add generated sources. Prefer Hilt ≥ 2.59.
Package: dk.betterlectio.android.core.lectio
| Component | Role |
|---|---|
LectioClient |
Public façade: get / postForm / postback / getBytes |
LectioHttpEngine |
Manual redirects, cookie rotation, retries, serial limiter |
CredentialStore |
Encrypted session + student persistence |
SessionController |
AuthState + session-expired handling |
AuthSessionInstaller |
WebView cookies → validate → install session |
AspNetForm / StudentIdentityParser |
Scrape helpers for future endpoints |
class ScheduleRepository @Inject constructor(
private val client: LectioClient,
) {
suspend fun week(year: Int, week: Int): AppResult<...> {
val html = client.get("SkemaNy.aspx?...")
// parse with Jsoup — do not reimplement cookies/redirects
}
}Session rules (iOS parity):
- Cookie header is the single source of truth (no system cookie jar)
- Empty
autologinkeyV2/ASP.NET_SessionIdSet-Cookie values are ignored - UniLogin broker host ⇒ session expired
- Requests are serialized with a short cooldown
Run foundation tests:
./gradlew :app:testDebugUnitTestMessage reactions use ordinary Lectio replies and row-scoped message edits;
there is no reaction backend or Supabase storage. The app implements the shared
blr1 carrier protocol, resolves reactions onto their target messages, and
keeps malformed or unresolved carriers visible. Changing or removing a
reaction edits the same carrier, and removal stores no copy of the old emoji.
The parser turns Lectio's automatic Redigeret af … audit line into a compact,
localized edited-time label and also tolerates it on valid reaction carriers.
Sent-message editing uses the native two-ViewState flow documented in
../extension/docs/message-editing-protocol.md, with Lectio-native eligibility.
The protocol is intentionally kept byte-compatible with the web extension and iOS implementation.
- MitID login UI (uses
AuthSessionInstaller+WebViewCookieExtractor) - Schedule scrape + UI
- Remaining tabs (messages, homework, assignments, more)
See SIGNING.md. Short version:
# Once: create upload keystore (if package never published)
keytool -genkeypair -v -keystore upload-keystore.jks \
-keyalg RSA -keysize 2048 -validity 10000 -alias upload -storetype JKS
cp key.properties.example key.properties # fill passwords
./gradlew :app:bundleRelease # → app/build/outputs/bundle/release/app-release.aab- Do not commit
local.properties,key.properties, or*.jks/ signing secrets. - Plan document lives at
NATIVE_ANDROID_PLAN.md.