This file provides guidance to coding agents when working with code in this repository.
Bible for Android (Alkitab / Quick Bible) — a 100% free, open-source Bible reader app for Android. Published on Google Play as "Alkitab" (Indonesian) and "Quick Bible" (non-Indonesian). The codebase supports multiple product flavors, 100+ downloadable Bible versions, song books, devotions, reading plans, cloud sync, and a daily verse widget.
- Official site: https://alkitab.app
- Developer docs: https://alkitab.app/developer
develop is the main integration branch. Base all work on develop and target every PR at develop. Use short-lived release branches when needed and tags to identify beta and production releases. The retired master branch is preserved by the archive/master tag.
At the end of a task, explain the consequential choices you made that the user did not explicitly specify. Do this by default, without waiting for the user to ask. Distinguish the user’s requirements from your assumptions and decisions, and briefly explain why you chose them and what they mean for the resulting behavior.
Focus on choices the user would reasonably want to review, such as how you interpreted loose requirements, UI placement and interactions, defaults, persistence, scope, and tradeoffs. Scale the detail to the ambiguity and impact of the task. Do not bury these choices in a list of changed files or a chronological work log, and do not enumerate routine implementation details. If there were no consequential choices beyond the request, do not invent any.
Include the outcome, relevant validation, and remaining limitations alongside these decisions. This reporting requirement does not create an additional approval step for work already authorized.
When the agent runs in a cloud environment or the user assigns a task in a dedicated Git worktree, finish the task by committing the changes, pushing its task branch to yukuku/androidbible, and creating or updating a pull request targeting develop, unless the user explicitly requests otherwise. The cloud-environment requirement applies even when the checkout is not a dedicated worktree. Creating or updating the PR is part of completing the task; do not ask for separate permission.
When the agent runs locally and the user assigns a task in a regular checkout rather than a dedicated worktree, treat it as interactive collaboration: the user and agents may be editing the code together. Work in the existing checkout. A PR is not required, and do not create a new worktree unless the user explicitly asks. Do not automatically commit or push shared edits merely to finish the task.
When continuing work on an existing PR, update that PR rather than opening another one. The following guidance applies when creating or updating a PR:
Use a branch in this repository so the PR triggers GitHub Actions to build signed release APKs and publish APK preview links. See the CI and preview documentation.
- Run the checks appropriate to the changes before pushing; CI supplements local verification.
- When continuing a task that already has an open PR, push updates to its branch and update its description as needed.
- Return the PR link and the observed CI status to the user.
- Leave merging the PR and publishing a production release to an explicit user request.
- Write PR descriptions for a reviewer who has not followed the conversation. Describe the problem, resulting behavior, relevant implementation details, and validation. Omit the agent's work diary, temporary computer problems, process restarts, and environment troubleshooting unless they are necessary to reproduce the issue or explain a remaining verification limitation. Validation should state what was tested, the result, and any material coverage limits.
Read-only analysis and advice do not require a PR.
For a new beta build, follow Build a new beta through GitHub CI.
Run bumpBetaNumber locally, commit and push version.properties, then dispatch
release.yml with gh workflow run and stage=beta. Build the signed APKs and
AABs on GitHub CI. A beta metadata bump does not require local APK/AAB builds or
unit tests. Use dry_run=true when publishing is not requested; use
dry_run=false when a GitHub pre-release is requested. Google Play publishing
is a separate action.
# Debug APK (open-source build, works out of the box)
./gradlew assemblePlainDebug
# Debug App Bundle
./gradlew bundlePlainDebug
# Unit tests (same as CI)
./gradlew testPlainDebugUnitTest testPlainReleaseUnitTest
# Run a single test class
./gradlew testPlainDebugUnitTest --tests "yuku.alkitab.base.util.QueryTokenizerTest"
# Run a single test method
./gradlew testPlainDebugUnitTest --tests "yuku.alkitab.base.util.QueryTokenizerTest.testQuotedPhrases"Requirements: JDK 21 and the Android SDK/NDK versions in gradle/libs.versions.toml. The NDK is required for Snappy.
Versioning: versionName comes from version.properties (versionBase +
betaNumber) combined with a stage: dev by default (5.0.0-dev.<commits since versionBase last changed>), beta via -PversionStage=beta
(5.0.0-beta.<n>), release via -PversionStage=release (5.0.0). versionCode is derived from wall-clock
time and is unrelated to the stage. Never hardcode a version in
Alkitab/build.gradle.kts; see the "Versioning" section of
docs/build-system.md.
The plain flavor is the open-source development build and works out of the box with the placeholder Alkitab/google-services.json checked into the repo (Firebase features won't function at runtime, but the app builds and runs). Production flavors (yuku_alkitab, yuku_quick_bible, sabda_alkitab) require:
$ALKITAB_PROPRIETARY_DIR/overlay/<applicationId>/text_raw/— proprietary Bible text$ALKITAB_PROPRIETARY_DIR/google-services.json— real Firebase config (one file with client entries for all production applicationIds)- Signing-key env vars:
SIGN_KEYSTORE,SIGN_ALIAS,SIGN_PASSWORD. Thesabda_alkitabflavor has its own upload key and needsSIGN_SABDA_KEYSTORE,SIGN_SABDA_ALIAS,SIGN_SABDA_PASSWORDinstead (there is no fallback toSIGN_*)
With those set, build with a plain ./gradlew assembleYuku_alkitabRelease (or any other production flavor).
Use the installed JDK and Android SDK when available. Check java -version, and read SDK/NDK requirements from gradle/libs.versions.toml. Set ANDROID_HOME or sdk.dir in the untracked local.properties file. For a VM without these tools, see Build Environment Setup.
Code changes should be verified with a local ./gradlew assemblePlainDebug (and relevant testPlainDebugUnitTest invocations) before committing — do not treat the GitHub Actions run as the first build.
Start with the documentation index. Architecture owns module boundaries, application services, ARI addressing, UI architecture, and localization. Storage owns persistence and migration constraints; feature guides describe capabilities.
Desktop converters share app-library source roots through
tools/common-jvm/common-jvm.iml; keep those roots synchronized when moving shared
code. See Tools. Parser tests and their golden fixture belong to
the app test suite; see parser compatibility tests.
-
Use
kotlinx.serializationfor JSON parsing and serialization in app code. Deserialize fixed JSON schemas into@Serializabledata classes, using an explicit@SerialNameon every serialized property, even when the Kotlin property name matches the wire name. Reserve JSON-tree parsing for genuinely dynamic structures. Do not introduce new Gson usages or extend Gson-based implementations. Migrate the JSON handling being changed to kotlinx.serialization, while keeping unrelated legacy Gson code outside the task's scope. -
Do not introduce
!!in production code. Handle nullable values explicitly. For nullable observable or delegated state, read a local snapshot and use a null check so Kotlin can smart-cast it. -
Separate logical sections of long function bodies with blank lines. Group related operations and make transitions between setup, state loading, rendering, and callbacks easy to scan.
-
IMPORTANT. Never write change-narrating comments. Code comments must describe the code as it is now, not the history of how it got there. Do not reference past revisions, removed approaches, or the act of editing: no "previously", "used to", "formerly", "earlier iterations", "an earlier version", "we changed/renamed/moved this from…", "now uses…", "no longer…", and the like. Git history is the record of change; the code and its comments describe the present. When explaining why the current design was chosen over an alternative is genuinely useful, state it as present-tense rationale about the alternative ("the cluster is centered with Spacers because a weighted slot would clip the label"), not as a story about what the code used to do. This applies to all comments, KDoc/Javadoc, and commit-adjacent code. Keep the narrative of change out of the source. The same rule applies to milestone labels: do not tag comments with the sprint or task that produced the code ("(M3)", "Out of scope for M4"), because they date instantly and mean nothing to a later reader.
-
IMPORTANT. Never use em dashes to join clauses in comments or docs. The "—" character (U+2014) is hard to read in running prose. Write two plain sentences instead, or use a comma, colon, semicolon, or parentheses, whichever reads most naturally. This applies to code comments, KDoc/Javadoc, Markdown docs, commit messages, and PR descriptions. (Existing prose in older files may still contain them; leave it alone unless you are already editing that comment or were asked to sweep the file.)
-
IMPORTANT. Do not add explaining comments except for non-obvious code. Default to writing no comment. A comment earns its place only by explaining a non-obvious why: a hidden constraint, a subtle invariant, a threading or lifecycle rule, a workaround for a specific bug, or behavior that would surprise a reader. Never restate what a well-named symbol already says, never narrate the happy path line by line, and never write multi-paragraph rationale where one or two sentences do the job. Before adding a new comment, check whether its explanation is needed to understand the code.
-
Preserve useful existing comments. Keep comment changes within the task's scope. "Default to no comments" governs adding new comments; it is not an instruction to remove existing comments throughout a touched file. Preserve comments that explain non-obvious constraints, invariants, lifecycle requirements, or design decisions. When changing code makes a comment inaccurate, update its useful explanation or remove it if it no longer applies. Do not remove useful context merely to shorten a diff.
-
Use
isprefixes for Boolean names. Name new Boolean variables, properties, and parameters with anisprefix, such asisBottom,isToolbarAtBottom,isSelectionActive, andisFinished. Apply this consistently to production code and tests. Preserve names required by overridden APIs or external contracts. Avoid unrelated renaming across the repository unless requested. -
A Compose
modifier: Modifier = Modifierparameter is an exception to any "drop the unused default" cleanup. Keep it on every composable that emits UI, even when no call site passes one and the default is the only value it ever takes. It is the Compose convention, callers expect to be able to size, pad or position a composable without editing it, and a composable that has to be reopened to accept a modifier is a worse API than one carrying an unused parameter. This applies to the parameter and its default together: leave both in place. -
Mixed Java/Kotlin codebase (Kotlin preferred for new code, many files still Java)
-
JVM toolchain 21 across all modules
-
No obfuscation in ProGuard (
-dontobfuscate), only shrinking -
EditorConfig enforces a single alphabetical import layout (
ij_kotlin_imports_layout=*, nojava.*/kotlin.*/static exceptions) and disablesno-wildcard-importsfor Kotlin -
Preference keys are defined as enum entries in
Prefkey.kt -
GIDs (globally unique IDs) are used alongside database
_idfor sync-capable entities -
Ariencoding is used universally for verse references — never store book/chapter/verse separately -
Version IDs follow format
"internal","preset/[name]", or"file/[path]" -
IMPORTANT. The app has its own in-app language setting (
pref_language), independent of the device locale. Only activities pick it up automatically:BaseActivity.attachBaseContextruns the context throughConfigurationWrapper.wrap, so anything resolving strings against an activity (including ComposestringResource, whoseLocalContextis the activity) is already correct. Everything else is not. AService, aBroadcastReceiver, aWorker,App.context, or anyapplicationContextresolves against the device locale, sogetStringthere prints the wrong language for every user whose in-app choice differs from their device. When such a component produces user-visible text, resolve it againstConfigurationWrapper.localizedContext(...)instead. Cache that context rather than rebuilding it per string, and rebuild whenConfigurationWrapper.getSerialCounter()changes, which is how a language change is signalled. Prefer keeping the string resource unresolved (a res id plus args) until it reaches whatever component knows the right context — seeAudioLogMessage.
- Prefer Kotlin backtick identifiers with descriptive sentences for test function names — they render as readable prose in JUnit output. Use full sentences, not short names.
// Good @Test fun `patchNoConflict treats add on an existing gid as an overwrite (same as mod)`() { ... } // Avoid @Test fun patchNoConflict_addExistingGid_overwrites() { ... }
- Use Robolectric if needed for tests that exercise Android framework code (Context, Intents, Parcelable, DB helpers, etc.). Pure-logic tests should stay plain JUnit. If an Android method is blocking a pure-logic test with
"Method not mocked", first consider whether a tiny test-scope shadow (e.g.src/test/java/android/util/Pair.java) is enough before reaching for Robolectric. - When writing unit tests, assume the production code is correct. If you spot what looks like an obvious bug while writing tests, flag it rather than silently working around it.
- Do not include SHAs, commit hashes, line numbers, line counts, or library version numbers in long-lived docs (
docs/tech-debt-remediation.md, its per-item REM records, thisAGENTS.md, etc.). They rot on rebase/squash-merge, refactors, and dependency bumps, and are fragile to maintain. Refer to code by symbol name (class/method), and point atgradle/libs.versions.tomlinstead of naming dependency versions. When recording that a step is done, describe what shipped (file paths, test counts, scope) and use an absolute date, never a SHA. (Older entries in these docs may still reference SHAs; leave them alone unless explicitly asked to clean up.)
IsiActivity.ktis still large, but no longer monolithic: gesture handling, the action-mode callback, and split-view management were extracted (REM-06/07/08) intoReaderGestureHandler,VerseActionModeController, andSplitViewManagerunderwidget/. Bible reading, navigation history, and volume-button handling are still inline; changes here require careful testing.yuku.kpri.model.Songis the legacy song decoder’s target type. Song storage uses Room with JSONSongDocumentpayloads; legacy Parcelable rows are converted lazily on first read. Preserve stored class-name compatibility. Seedocs/modules/songs.mdanddocs/features/portable-songs/design.md. Backend redirects andkidung-dataauthoring belong to separate repositories.- The
Snappymodule has native C++ code — NDK must be installed for builds. - A placeholder
Alkitab/google-services.jsonis checked in soplainDebugworks out of the box; Firebase features won't actually function with it. For production flavors, the realgoogle-services.jsonis sourced from$ALKITAB_PROPRIETARY_DIR/google-services.jsonat build time and copied into the gitignoredAlkitab/src/<flavor>/google-services.json(where the GMS plugin's source-set lookup finds it). - The internal Bible version data is split per flavor: the placeholder
ddd_*files live underAlkitab/src/plain/assets/internal/(used by theplainopen-source build only). Production flavors get theirtb_*/kjv_*files copied from the proprietary overlay intobuild/generated/proprietaryAssets/<flavor>/internal/at build time. - Backend host:
BuildConfig.SERVER_HOSTresolves tohttps://api.alkitab.app(defined inAlkitab/build.gradle.kts), not the marketing site atalkitab.app. The two hostnames are different origins behind separate Cloudflare cache zones — when reproducing a backend issue withcurl, hitapi.alkitab.app/...to match what the app actually requests.