38 Files, 0 Patience
A Claude Code Story
Introduction - set the stage for a story about using Claude Code to tackle a tedious migration task.
Who am I?
Quick intro - I work on Android at Signal.
What is Claude?
Claude is an AI assistant made by Anthropic. Claude Code is their CLI tool for software engineering tasks.
I Built a DSL to Avoid Tedium
configure {
switchPref(
title = DSLSettingsText.from(R.string.title),
isChecked = state.enabled,
onClick = { viewModel.toggle() }
)
dividerPref()
clickPref(
title = DSLSettingsText.from(R.string.setting),
onClick = { navigate() }
)
}
We were targeting Android API 19 - way too old for Compose, which didn't exist yet.
Android's built-in PreferenceScreen was painful boilerplate.
So I built this Kotlin DSL on top of RecyclerView. It looked declarative, felt nice to use.
Problem solved... for now.
Then Compose Happened
@Discouraged("The DSL API can be completely replaced
by compose. See ComposeFragment or
ComposeBottomSheetFragment for an
alternative to this API")
fun configure(init: DSLConfiguration.() -> Unit)
When we bumped our min SDK, we could finally use Compose.
So I added this annotation to my own code.
But we didn't start migrating until 2025 - when Signal began adding large screen support.
Two-column settings layouts, fully in Compose. Time to migrate.
38 files. All using my DSL. All needing to change.
The Old Way
class MySettingsFragment : DSLSettingsFragment() {
private val viewModel: MyViewModel by viewModels()
override fun bindAdapter(adapter: MappingAdapter) {
viewModel.state.observe(viewLifecycleOwner) { state ->
adapter.submitList(getConfiguration(state).toMappingModelList())
}
}
private fun getConfiguration(state: State): DSLConfiguration {
return configure {
sectionHeaderPref(R.string.header)
// switchPref, dividerPref, clickPref, etc.
}
}
}
Here's what a typical DSL settings screen looked like.
You extend the base class, override bindAdapter, observe your state, and build a configuration.
The configure block has these pref functions - sectionHeader, switch, divider, etc.
It worked well, but it's all custom framework code sitting on top of RecyclerView.
The New Way
class MySettingsFragment : ComposeFragment() {
private val viewModel: MyViewModel by viewModels()
@Composable
override fun FragmentContent() {
val state by viewModel.state.observeAsState()
val callbacks = remember { DefaultMySettingsScreenCallback(...) }
MySettingsScreen(state = state, callbacks = callbacks)
}
}
The Compose version looks simpler on the surface.
But notice the screen is extracted out, with a callback interface.
This decouples the UI from the fragment entirely.
For Each of 38 Files...
MySettingsScreen // Screen composable
MySettingsScreenPreview // @Preview function
MySettingsScreenCallback // Callback interface
DefaultMySettingsScreenCallback // Default impl
For every single fragment, I needed to create these four things plus update the fragment.
38 files times 5 deliverables... that's a lot of tedious, mechanical work.
Sequential Prompts
Human architects + Human orchestrates
Me → Claude: "Generate DefaultCallback"
Me → Claude: "Generate Callback interface"
Me → Claude: "Create Screen from getConfiguration"
Me → Claude: "Swap base class, wire it up"
×38 files
The first approach: I broke it into 4 prompts, ran each one manually.
I was both the architect (deciding WHAT to build) and the orchestrator (deciding the ORDER).
Claude was basically a typist. I managed everything.
Deliverables List
Human architects → Claude orchestrates
Migrate PrivacySettingsFragment to ComposeFragment.
Key mappings:
- clickPref → Rows.TextRow
- switchPref → Rows.ToggleRow
Create:
1. PrivacySettingsScreenCallback interface
2. DefaultPrivacySettingsScreenCallback
3. PrivacySettingsScreen composable
4. PrivacySettingsScreenPreview
5. Update fragment to extend ComposeFragment
Better: I define the deliverables, but Claude figures out the order.
I'm still the architect - I'm telling Claude WHAT to create.
But Claude orchestrates - it decides HOW to sequence the work.
One prompt instead of four, but I'm still doing the thinking.
History as the Prompt
Claude architects + Claude orchestrates
Migrate PrivacySettingsFragment to ComposeFragment.
Look at commit 683da1f167 ("Convert expire timer
settings fragment to compose") for the pattern.
The real unlock: use git history as context.
I don't list the deliverables - Claude figures them out from the example commit.
Claude reads the diff, understands the pattern, and architects the solution.
The commit IS the spec. Show, don't tell.
This is something a lot of people don't think to do!
The Role Evolution
Architect
Orchestrator
Sequential
Human
Human
Deliverables
Human
Claude
History
Claude
Claude
Here's the progression visualized.
We go from doing everything ourselves, to delegating orchestration, to delegating both.
The human moves from manager to reviewer.
Each step requires more trust - and delivers more leverage.
What Worked Well
clickPref → Rows.TextRow ✓
switchPref → Rows.ToggleRow ✓
dividerPref → Dividers.Default ✓
customPref → ???
The 1:1 mappings were a natural task for Claude.
Pre-built components with clear direction made this easy.
But you need a plan for custom or missing components.
Good news: Claude can often auto-convert these too.
The Tricky Bits
onClick = { showDialog() } // Not just a mapping
onClick = { viewModel.doThing() } // Needs callback wiring
Misaligned mappings - when the DSL and Compose don't line up cleanly.
UI actions tied to callbacks - dialogs, navigation, things beyond simple state.
These need extra Compose code, not just a 1:1 swap.
Make sure Claude knows what to do at these edges.
Iterate, Iterate, Iterate
/plan → plan.md → refine → execute → repeat
The key takeaway: iterate.
Plan mode gives you a file you can modify as you hit edges.
Way better than maintaining separate prompts and dealing with context loss.
Start small, refine the plan, then scale up.
Your Intuition Will Be Wrong
(And That's OK)
As we explore these new AI systems, our intuition can be upside down sometimes.
And that's OK. As we learn, we adapt.
We come up with more intuitive patterns for working with these systems.
They become force multipliers - taking out the tedium, keeping us fresh for what really matters.
Questions?
This presentation was built with:
reveal.js
Time for questions! This presentation was built using reveal.js and Claude Code.