I use Claude Code, Codex, and Antigravity in my Android work. They have helped me through a tedious multi-module migration, shortened the path from a feature idea to working code, and made it cheaper to solve bugs promptly.
The benefit is straightforward: I can get more done. The risk is less obvious: I can produce code faster than I can understand it.
That gap creates comprehension debt. The code may compile and behave as expected, but if I cannot explain its decisions, I will struggle when I need to debug or extend it later. I still own generated code after the agent has moved on, so maintaining a mental model of the system matters.
My answer is not “review everything”
Manually reviewing every generated line protects comprehension, but it can also erase much of the speed I gained. Instead, I vary the depth of review according to the risk of the change.
An agent produces a change. If the change affects architecture, introduces a new feature, or has a costly failure mode, perform a full manual review. Otherwise, trust the already reviewed behavioral contract and its tests.
I manually review architecture changes, new feature implementations, and critical bug fixes where the consequences justify the time. For lower-risk work, strong automated tests let me move faster—but only when I already understand and trust the behavior those tests describe.
The boundary is contextual. No checklist can make the decision for every codebase. The developer responsible for shipping the change must decide what needs direct inspection.
I review the contract first
My test-driven workflow starts with me defining the behavior. An agent can turn that definition into tests, but I review those tests before treating a green build as evidence. A test that runs is not necessarily a test that protects anything.
Consider a simplified offline-sync component. This test is green if sync()
does not throw:
@Testfun `sync completes`() = runTest { val repository = SyncRepository( remote = FakeRemoteDataSource(), local = FakeLocalDataSource(), )
repository.sync()}It does not describe what the application owes the user. An implementation that drops unsent data could still pass.
A meaningful test states the behavior that must survive future changes:
@Testfun `failed upload remains pending for retry`() = runTest { val local = FakeLocalDataSource( records = listOf(pendingRecord(id = "record-1")), ) val remote = FakeRemoteDataSource( uploadResult = Result.failure(IOException("Offline")), ) val repository = SyncRepository(remote = remote, local = local)
repository.sync()
assertEquals( SyncStatus.Pending, local.record("record-1").syncStatus, )}This example is deliberately generic, but the distinction is central to my workflow. I decide that a failed upload must remain retryable. The agent may write the test and implementation. I inspect the test to ensure it expresses that requirement rather than merely exercising a method.
Once I have reviewed the feature and its behavioral contract, those tests become leverage. During later mechanical work—such as moving code between modules—I do not need to rediscover every implementation detail. The tests tell me whether the behavior I understood and approved has changed.
Tests preserve understanding; they do not create it
This is the limit I keep in mind. Passing tests are useful because the behavior behind them was defined and reviewed by a person. They are not a substitute for understanding unfamiliar generated code, and they cannot judge whether a new architecture is appropriate.
Coding agents have made parts of Android development substantially faster for me. I can delegate repetitive migration work, get to feature implementations sooner, and explore bug fixes without paying the full cost of each attempt. But speed is valuable only while I can remain responsible for the result.
My working rule is simple: define the behavior, review the contract, and spend manual attention where misunderstanding would cost the most.