|
| 1 | +# AGENTS.md |
| 2 | + |
| 3 | +Guidance for AI agents working in this repository. Read alongside `CLAUDE.md` |
| 4 | +(build/test commands, architecture, style rules). This file lists Kotlin/Android |
| 5 | +**antipatterns to avoid** when generating or modifying code. |
| 6 | + |
| 7 | +## Concurrency & Coroutines |
| 8 | + |
| 9 | +- **Do not use `GlobalScope`.** Use an injected `CoroutineScope` tied to a |
| 10 | + lifecycle (`viewModelScope`, `lifecycleScope`) or a scope owned by the class. |
| 11 | + For fire-and-forget work that must outlive the lifecycle owner, inject |
| 12 | + `@Named(APPLICATION_SCOPE) CoroutineScope` — never reach for `GlobalScope`. |
| 13 | +- **Do not use `runBlocking` on the main thread or in production code.** It is |
| 14 | + acceptable only in tests. |
| 15 | +- **Do not hardcode `Dispatchers.IO`/`Dispatchers.Main` inside classes.** Inject |
| 16 | + `CoroutineDispatcher` via constructor with `@Named(IO_THREAD)` so it can be |
| 17 | + swapped in tests. See `ReaderPostRepository.kt` for the canonical pattern. |
| 18 | +- **Do not launch coroutines from constructors or `init` blocks.** Start them |
| 19 | + from a lifecycle-aware entry point. |
| 20 | +- **Do not use `Thread.sleep`** to wait for async work. Use coroutines, `delay`, |
| 21 | + or proper synchronization. Never in tests either — use `runTest` and virtual |
| 22 | + time. |
| 23 | +- **Do not use `AsyncTask`, `Handler(Looper.getMainLooper())` for new code, or |
| 24 | + raw `Thread`.** Prefer coroutines. |
| 25 | + |
| 26 | +## Nullability & Kotlin Idioms |
| 27 | + |
| 28 | +- **Avoid `!!` (non-null assertion).** If a value is guaranteed non-null, |
| 29 | + restructure to express that in the type. If not, handle the null branch. |
| 30 | + Exception: the Fragment view-binding backing-field idiom |
| 31 | + (`private val binding get() = _binding!!`) is accepted in this codebase. |
| 32 | +- **Avoid `lateinit var` for values that have a sensible default or can be |
| 33 | + constructor-injected.** Reserve it for DI-injected fields. For view |
| 34 | + bindings, use the nullable backing-field idiom described below in |
| 35 | + *Android Lifecycle & Memory Leaks*. |
| 36 | +- **Do not use `!!` on `intent.extras`, `arguments`, or `savedInstanceState`.** |
| 37 | + Use safe calls with sensible fallbacks. |
| 38 | +- **Do not write `if (x != null) x.foo()`.** Use `x?.foo()` or `x?.let { ... }`. |
| 39 | +- **Prefer `val` over `var`.** Only use `var` when reassignment is required. |
| 40 | +- **Do not use `Any` as a type when a sealed class / interface fits.** |
| 41 | + |
| 42 | +## Android Lifecycle & Memory Leaks |
| 43 | + |
| 44 | +- **Do not store `Activity`, `Fragment`, `View`, or `Context` references in |
| 45 | + `companion object`, singletons, or `object` declarations.** Use |
| 46 | + `applicationContext` if a Context is truly required at app scope. |
| 47 | +- **Do not capture `Activity`/`Fragment` in long-lived callbacks, listeners, or |
| 48 | + coroutine scopes** without lifecycle awareness. |
| 49 | +- **Do not access `binding` after `onDestroyView` in Fragments.** Use the |
| 50 | + nullable backing-field pattern already used across the codebase |
| 51 | + (`private var _binding: FooBinding? = null` exposed via |
| 52 | + `private val binding get() = _binding!!`, nulled in `onDestroyView`). See |
| 53 | + `ReaderAuthorProfileBottomSheetFragment.kt` for an example. For |
| 54 | + ViewHolders, use the |
| 55 | + `ViewGroup.viewBinding(...)` extension in |
| 56 | + `util/extensions/ViewGroupExtensions.kt`. |
| 57 | +- **Do not start work in `onCreate` that must outlive the screen.** Use |
| 58 | + `ViewModel`, `WorkManager`, or a foreground service as appropriate. |
| 59 | +- **Do not register `BroadcastReceiver`/observers without unregistering** in |
| 60 | + the matching lifecycle callback. |
| 61 | + |
| 62 | +## Error Handling |
| 63 | + |
| 64 | +- **Do not catch broad exceptions (`Exception`, `Throwable`) without logging |
| 65 | + via `AppLog` or rethrowing.** Prefer catching the specific exception when |
| 66 | + practical. Broad catches are acceptable at network/IO boundaries as long |
| 67 | + as the error flows through the existing `AppLog`/Sentry pipeline. If you |
| 68 | + intentionally swallow an exception, add a one-line comment explaining |
| 69 | + why. |
| 70 | +- **Do not throw raw `RuntimeException`/`IllegalStateException` for control |
| 71 | + flow or expected error paths.** Use sealed result types or nullable |
| 72 | + returns. `check`/`require`/`error` for genuine precondition violations is |
| 73 | + fine. |
| 74 | +- **Do not log exceptions with `printStackTrace()`.** Use `AppLog` so output |
| 75 | + ends up in the right place in release builds. |
| 76 | + |
| 77 | +## Android APIs |
| 78 | + |
| 79 | +- **Do not use `findViewById`.** Use View Binding (existing XML) or Compose |
| 80 | + (new screens). Already in `CLAUDE.md`; restated because it is the most |
| 81 | + common regression. |
| 82 | +- **Do not use deprecated APIs** (`startActivityForResult`, `onActivityResult`, |
| 83 | + `getDrawable(int)`, `getColor(int)` without theme, `Handler()` no-arg ctor, |
| 84 | + etc.). Use Activity Result APIs, `ContextCompat`, `ResourcesCompat`, etc. |
| 85 | +- **Do not use reflection.** Already in `CLAUDE.md`; restated for emphasis. |
| 86 | +- **Do not use `SimpleDateFormat` as a static/companion field.** It is not |
| 87 | + thread-safe. Construct per use, or use `java.time` / `DateTimeFormatter`. |
| 88 | +- **Do not hardcode user-facing strings.** Add them to `strings.xml` and |
| 89 | + reference via `R.string.*` or `UiString`. |
| 90 | +- **Do not hardcode dimensions, colors, or styles in code.** Use resources |
| 91 | + and the design system. |
| 92 | +- **Do not perform I/O, network, or DB work on the main thread.** |
| 93 | + |
| 94 | +## Dependency Injection (Hilt/Dagger) |
| 95 | + |
| 96 | +- **Do not instantiate dependencies with `new`/constructor calls inside |
| 97 | + classes that should receive them via DI.** |
| 98 | +- **Do not use service locators or `object` singletons for stateful |
| 99 | + dependencies.** Use `@Inject` constructor injection. |
| 100 | +- **Do not inject `Context` directly when `@ApplicationContext` is what you |
| 101 | + need.** Annotate explicitly to avoid leaking an Activity context. |
| 102 | + |
| 103 | +## Compose |
| 104 | + |
| 105 | +- **Inside a composable, always wrap `mutableStateOf` in `remember`, and do |
| 106 | + not mutate state during composition.** Do mutations in event handlers or |
| 107 | + `LaunchedEffect`. Top-level `mutableStateOf` properties on a class |
| 108 | + (e.g., a `ViewModel`) are fine. |
| 109 | +- **Do not create `ViewModel` instances inside composables via constructors.** |
| 110 | + Use `hiltViewModel()` / `viewModel()`. |
| 111 | +- **Do not pass `ViewModel` deep into the composable tree.** Hoist state and |
| 112 | + pass plain data + lambdas. |
| 113 | +- **Do not launch coroutines from composables without `LaunchedEffect` / |
| 114 | + `rememberCoroutineScope`.** |
| 115 | +- **Do not perform expensive work in the composable body.** Wrap in |
| 116 | + `remember` / `derivedStateOf`. |
| 117 | + |
| 118 | +## Testing |
| 119 | + |
| 120 | +- **Do not write tests that depend on real time, network, or device state.** |
| 121 | + Use fakes, `runTest`, virtual time, and injected dispatchers. |
| 122 | +- **Do not assert on `toString()` or implementation details.** Assert on |
| 123 | + observable behavior. |
| 124 | +- **Do not use `Thread.sleep` in tests.** Use coroutine test utilities. |
| 125 | +- **Do not mock data classes or types you own when a real instance / fake |
| 126 | + works.** Reserve mocks for collaborators. |
| 127 | + |
| 128 | +## Code Hygiene |
| 129 | + |
| 130 | +- **Do not introduce TODOs without a tracking issue or owner.** Already covered |
| 131 | + by "no FIXME" in `CLAUDE.md`; same spirit applies to TODOs. |
| 132 | +- **Do not leave commented-out code.** Delete it; git history preserves it. |
| 133 | +- **Do not add wrapper functions, interfaces, or abstractions "for future |
| 134 | + flexibility"** when there is exactly one caller and one implementation. |
| 135 | +- **Do not duplicate strings across `strings.xml` translations.** Only edit |
| 136 | + the base `values/strings.xml`; translations are managed externally. |
| 137 | + Exception: when *removing* a string, delete it from every |
| 138 | + `values-*/strings.xml` too — lint's `ExtraTranslation` rule will fail |
| 139 | + otherwise. |
| 140 | +- **Do not add new dependencies in `build.gradle` directly.** Add to |
| 141 | + `gradle/libs.versions.toml` (already in `CLAUDE.md`). |
| 142 | + |
| 143 | +## Security & Privacy |
| 144 | + |
| 145 | +- **Do not log credentials, tokens, cookies, personal data, or full URLs that |
| 146 | + may contain auth.** Even at `DEBUG` level. |
| 147 | +- **Do not store secrets in source.** Use `secrets.properties` / Gradle |
| 148 | + properties. |
| 149 | +- **Do not disable SSL verification, hostname verification, or accept all |
| 150 | + certificates.** Even in debug builds. |
| 151 | +- **Do not use `MODE_WORLD_READABLE`/`MODE_WORLD_WRITEABLE`** for |
| 152 | + SharedPreferences or files. |
0 commit comments