Archive
Everything published here, newest year first.
356 posts
2026 341
- When a local model beats the API, and the three numbers that decide it
- The scoping conversation that stops a project going wrong in month three
- Most productivity advice fails engineers, and the reason is measurable
- A bigger context window gave me a worse answer, repeatably
- 100 days of Compose: the eight ideas the other ninety-nine posts kept circling 100 Days of Compose · Day 100
- Glance looks like Compose and compiles to RemoteViews, which explains every limitation 100 Days of Compose · Day 99
- Notifications are the one UI you can't build in Compose 100 Days of Compose · Day 98
- Predictive back turns a commitment into a preview, and your BackHandler can break it 100 Days of Compose · Day 97
- There are three kinds of inset, and using the wrong one is why your keyboard fights your layout 100 Days of Compose · Day 96
- Edge-to-edge stopped being a choice, and most apps found out from a bug report 100 Days of Compose · Day 95
- The checks that run without you asking are the ones that scale 100 Days of Compose · Day 94
- The Layout Inspector shows you a tree you didn't write 100 Days of Compose · Day 93
- A preview that needs a ViewModel is telling you something 100 Days of Compose · Day 92
- Two navigation systems can share one back stack, which is the only reason this works 100 Days of Compose · Day 91
- Porting a RecyclerView deletes four classes and keeps one idea 100 Days of Compose · Day 90
- AndroidView has two lambdas, and putting work in the wrong one is the bug 100 Days of Compose · Day 89
- ComposeView is easy; deciding where to put it is the migration 100 Days of Compose · Day 88
- What a Compose test suite should actually contain 100 Days of Compose · Day 87
- A flaky test is a timing assumption you didn't know you made 100 Days of Compose · Day 86
- Writing my own performance review honestly, for once
- Find, act, assert — and the rule that decides which test to write at all 100 Days of Compose · Day 85
- The meeting that should have been a pull request, and vice versa
- I measured my own tooling for a month and deleted half of it
- Your UI tests query the accessibility tree, which is why testTag is a last resort 100 Days of Compose · Day 84
- The handover that actually worked, and the four I botched first
- The rate conversation I kept getting wrong
- Four tools, four questions, and none of them answers another's 100 Days of Compose · Day 83
- Scope creep is a contract problem, not a communication problem
- The fixed-price project that cost me three weeks of unpaid work
- Nine pillars have quietly been teaching you one optimisation 100 Days of Compose · Day 82
- I wrote 400 lines to avoid learning one SQL feature
- The onboarding doc that doubled as a test of our own setup
- Baseline Profiles fix a problem recomposition counts can't see 100 Days of Compose · Day 81
- The keyboard covered the field, and the fix was not imePadding
- The bug was in the requirements, and three people implemented it faithfully
- A route is a string until you make it a type
- Publishing a KMP library commits you to every target you listed
- Strong skipping made most of yesterday's advice unnecessary 100 Days of Compose · Day 80
- The compiler decides your types are unstable, and it writes down why 100 Days of Compose · Day 79
- The dependency with one maintainer, and the week it went away
- It was not a timezone bug, it was a day-boundary bug
- The index I added made the query slower, and the planner was right
- Rotating the key was the easy part
- Compose performance is one question asked in four places 100 Days of Compose · Day 78
- Drag-and-drop crosses app boundaries, which is why it isn't just a drag gesture 100 Days of Compose · Day 77
- Adding a parameter with a default broke the ABI
- Seven modules, seven copies of the same Gradle block
- An animation is state, and that is why it restarts
- Your semantics tree is your test API and your accessibility API at once
- Focus is the input model you don't test until someone plugs in a keyboard 100 Days of Compose · Day 76
- Nested scroll is a negotiation, and there are four moments to intervene 100 Days of Compose · Day 75
- The null that meant three different things
- Six weeks of optimisation, and the fix was two lines
- Pinch, pan and rotate arrive together, and the centroid is what ties them 100 Days of Compose · Day 74
- The health check that returned 200 while the service was useless
- The queue had no maximum, so the failure was memory and not latency
- Swipe-to-dismiss is a state machine with anchors, not a drag handler 100 Days of Compose · Day 73
- Offset pagination loses rows while you read, and the user never knows
- The webhook that arrived twice, and the order that shipped twice
- Running a model on-device is a memory decision first
- Streaming a response changes your UI contract, not just your latency
- clickable does six things, and detectTapGestures does one 100 Days of Compose · Day 72
- Two agents, one working directory, and a merge nobody asked for
- Every gesture in Compose is a coroutine reading a stream of events 100 Days of Compose · Day 71
- Your prompt is a config file, and it needs the same discipline
- Automated checks find the missing labels. They can't tell you the label is wrong. 100 Days of Compose · Day 70
- The interface with one implementation, and the three years it cost
- The architecture decision nobody wrote down, re-litigated four times
- We shipped it, measured it, and deleted it eleven weeks later
- Engagement went up, and it was because the app got slower
- Reading order is layout order, until your layout stops matching your meaning 100 Days of Compose · Day 69
- One card, four announcements — merging is the fix 100 Days of Compose · Day 68
- The lock held across a network call, and the deadlock that followed
- The rollout with no off switch, discovered at 2am
- Compose builds a second tree, and it's the one your users hear 100 Days of Compose · Day 67
- The config change that was really a deploy, and had none of the safeguards
- The N+1 query that looked fast in development
- Context is a budget, and what you load first wins
- Take the clock away and animations become testable 100 Days of Compose · Day 66
- The retrieval worked and returned the wrong file
- A shared element transition is one element pretending to be two 100 Days of Compose · Day 65
- The agent fixed the test, not the bug, and the test was right
- The eval that passed because the function returned a plausible stub
- An agent loop without a terminal condition is a budget with no bottom
- You cannot fade out something that has already left the composition 100 Days of Compose · Day 64
- Your agent's tool output is untrusted input
- The cache that returned someone else's data
- Every timeout you set independently adds up to one that is wrong
- updateTransition is a state machine that happens to animate 100 Days of Compose · Day 63
- You don't animate from A to B, you declare B and Compose handles the rest 100 Days of Compose · Day 62
- Your data class is unstable and Compose told you by recomposing everything
- Text is the most expensive composable you use without thinking
- Compose has a dozen animation APIs and one decision tree 100 Days of Compose · Day 61
- derivedStateOf exists because you were recomposing on every keystroke
- State hoisting has a limit, and you have probably passed it
- Four bytes a pixel is the number that decides whether your app gets killed 100 Days of Compose · Day 60
- An estimate is a distribution, and you reported the mode
- The code review that should have been a conversation
- The most valuable pull request that quarter deleted 4,000 lines
- Idempotency is a product decision before it is a technical one
- A shadow needs a shape, and Compose won't guess one for you 100 Days of Compose · Day 59
- Your gradient is measured in pixels, and that's the bug 100 Days of Compose · Day 58
- Logs are for the questions you have not thought of yet
- The dashboard nobody looked at, and the alert that fired every day
- A Painter is the thing that knows how big it wants to be 100 Days of Compose · Day 57
- Your error message is an API, and changing it breaks someone
- The test that broke on every refactor was testing the wrong thing
- A vector costs CPU once, a bitmap costs memory forever 100 Days of Compose · Day 56
- The migration that could not be rolled back, and nobody noticed until it mattered
- The retry that turned a blip into an outage
- Constraints flow down, sizes flow up, and fillMaxSize means something specific
- contentScale and Modifier.size are answering different questions 100 Days of Compose · Day 55
- remember is not a cache, and the key is the whole contract
- LaunchedEffect(Unit) runs once, and that is usually the wrong once
- Two lines of autofill are worth more than a week of form polish 100 Days of Compose · Day 54
- A LazyColumn without keys loses your scroll position and your animations
- Modifier order is not style, it is semantics
- Recomposition is not re-rendering, and that is why your list stutters
- Your custom font has a fallback, and you didn't choose it 100 Days of Compose · Day 53
- iosTest needs a booted simulator, and CI does not have one by default
- Image loading is the least portable thing in your shared UI
- A feature flag read on four platforms needs one source of truth and four caches
- A dependency that resolves in commonMain and only exists on Android
- Shared navigation has no back stack owner, and that is the hard part
- KSP runs once per target, and your generated code can disagree with itself
- TextFieldState is a document, and edits are transactions on it 100 Days of Compose · Day 52
- There is no File in commonMain, and that is a feature
- Hashing a password in shared code is the one thing you should not share
- Throwing across the bridge loses your exception type
- There is no UUID in the Kotlin standard library, and the fixes differ per platform
- Your list rows are different heights because text measurement is honest 100 Days of Compose · Day 51
- @Preview is Android-only, and your shared UI has no preview at all
- The desktop target is the cheapest bug oracle you are not using
- AnnotatedString is why you don't need three Text composables in a Row 100 Days of Compose · Day 50
- A version catalog is a contract, and a transitive bump breaks it quietly
- A shared ViewModel has no lifecycle, so you have to give it one
- A Flow is cold, and that is why your ViewModel fires three network calls
- There is no Logcat in commonMain, and println is not a logger
- Compose is growing a styles API, and it's for the gap theming never covered 100 Days of Compose · Day 49
- runTest skips your delays, which is why the flaky test passed
- Source sets are a graph, and iosMain is not where you think it is
- StateFlow drops what you needed and keeps what you didn't
- One failed child cancelled the whole screen, and the scope was the reason
- commonTest runs on every target, which is the point and the cost
- composeResources is not Android resources, and qualifiers resolve differently
- Extending MaterialTheme beats replacing it, until it doesn't 100 Days of Compose · Day 48
- kotlinx-datetime has no Calendar, and the timezone database ships per platform
- expect/actual is not an interface, and the difference shows up at link time
- The iOS framework you export is an API, and nobody reviews it
- Your shared database needs a driver per platform, and that is the easy half
- A DI module that compiles on every target and only works on one
- Your Kotlin class becomes an Objective-C class, which is why the name is wrong
- Kotlin/Native memory is not the JVM's, and a retain cycle crosses the bridge
- One Gradle build, five outputs, and the cache that only helps three
- One Ktor client, four engines, and the timeouts that don't mean the same thing
- A platform bridge is an interface with a lifetime problem
- kotlinx.serialization is compile-time, which is why your polymorphism fails at runtime
- Your suspend function becomes a callback in Swift, and cancellation does not survive
- The third time this filled up, I diagnosed it as an outage
- Wasm has no threads, and that decides your dispatcher
- Corner radius is a signal, and Material has five of them 100 Days of Compose · Day 47
- Fifteen named text styles are fewer decisions than five 100 Days of Compose · Day 46
- Dynamic colour takes your brand palette away, and that's the deal 100 Days of Compose · Day 45
- MaterialTheme is three CompositionLocals and nothing else 100 Days of Compose · Day 44
- SearchBar has two states, and the second one takes over the screen 100 Days of Compose · Day 43
- Date pickers are a timezone bug with a calendar attached 100 Days of Compose · Day 42
- The spinner you show for 200ms is worse than no spinner 100 Days of Compose · Day 41
- A ceiling in a conditional is one someone deletes
- A command that checks its own contract
- Not every step needs the expensive model
- Bar, rail, drawer — the same destinations, chosen by window width 100 Days of Compose · Day 40
- Loading every rule up front is how none of them apply
- Two agents, one working tree, one disaster
- Reading back is not optional
- A dialog is state, not a function you call 100 Days of Compose · Day 39
- One warm context beats five cold ones
- A step that never ran is not a step that passed
- Is this derived thing current? Make it a comparison
- Which artifact is actually the design
- The running app and the spec disagree, and only one is documented
- Print the list before you touch anything
- The orchestrator that wrote the code itself
- Editing the generated thing is how the spec stops meaning anything
- Every generated file should say what made it
- The post that went to the wrong account
- A status column is wrong within a week
- Twenty-four of my tags file exactly one post
- The TextField overload you're using is probably the deprecated one 100 Days of Compose · Day 38
- It returned an identifier and produced nothing
- One of the two links is always missing
- Three hundred and three of three hundred and four
- The audit was green, specific, and about the wrong artifact
- Never attempted, and the summary said complete
- A warning is not a refusal, and the driver could not tell
- Name the fault, and rule out the wrong fix
- The two fields that only exist in the interface
- A generator given the wrong context does not stop
- Two places that both think they are the truth
- Checkbox, radio, switch: three controls, three different questions 100 Days of Compose · Day 37
- The export engine that exported nothing
- A retry queue with nothing to run it is a graveyard
- The corpus your generator reads is production code
- An agent drove a KMP app to the Play Store. The codegen was the easy half.
- Five buttons, one hierarchy, and the rule that decides which 100 Days of Compose · Day 36
- Syndicate to a 404 and you donate the SEO
- It compiles, the test is green, and the screen is a grey box
- Last write wins is a decision, not a default
- A screen nothing navigates to still compiles
- Your meta description is cut mid-sentence
- A hardcoded dp is a token you did not name
- The ad slots on this blog are finished and render nothing
- A tag feed is not a list of your posts
- The banking API that deliberately will not tell you things
- The only screenshots worth committing
- A hundred posts is the easy half
- The module that compiled and was never installed
- Stop hand-registering what a processor can see
- Partial translation is a number, not a feeling
- The numbers worth watching, and the ones that flatter
- The string literal that cannot be translated
- One social card for every post is one card nobody shares
- Your list flickers because the page swap is not atomic
- Compose is doing what you told it; the instruction was wrong
- An app store is a discovery surface. A website is a building.
- A testTag is a contract between the screen and its test
- Three repositories, one change, and the order that stops it corrupting
- Every .env file in my projects is gone and nothing broke
- What act can reproduce, and what it silently cannot
- A hand-typed branch name skips the machinery
- Where a secret lives should never be a decision
- Write it in commonMain until the compiler stops you
- A task that captures Project is a bug, not a gap
- A number in CI is the only budget that holds
- Open it as a draft and let a human merge
- Every expect declaration is a promise you made twice
- The Play Store checks you only fail once
- Thirty-six modules, one sync from deletion
- Your deploy said zero changed because it never looked
- Pin a commit, not a branch, when someone rehosts your images
- The keychain repair that broke signing for a day
- Killing the Gradle daemon costs you thirty minutes
- Setting a secret on the repo is how it stops having an owner
- Refusing the write is cheaper than recovering the work
- Forty-five minutes per CI guess is not a feedback loop
- A release is phases, not a button
- The Scaffold parameter everyone ignores is the one that breaks the layout 100 Days of Compose · Day 35
- Your sitemap ping has a daily budget
- Two prefixes decide who owns a credential
- Signing state has exactly one owner, or it has none
- Screenshots are a build artifact, not a design deliverable
- The placeholder listing that almost went live
- Five ways a cache and its writes drift apart
- A read path that cannot ask for fresh data
- The submodule pointer you accidentally reverted
- Fixing it only in your app is how the template keeps the bug
- The command that leaked a secret was not on the list
- A documented workflow nothing could execute
- An auth gate you do not resent
- Version state belongs in exactly one file
- Your deploy output is a verification surface
- Two modules with the same name, one is a demo shell
- What a local model does to your battery
- The first inference is the one users judge you on
- An external display is a second window, not a bigger one 100 Days of Compose · Day 34
- Core ML and LiteRT solve the same problem differently
- On-device semantic search over the user's own data
- Designing the fallback for a device that cannot run your model
- Gemini Nano on-device: what it actually costs you
- Routing between a local model and a remote one
- One inference API, two platform runtimes
- How to know your on-device model got worse
- Wiring ML Kit GenAI into a Compose screen without blocking it
- Shipping a model in your APK without breaking the size budget
- Updating a model without shipping an app release
- An AI feature that still works on a plane
- The on-device privacy argument, stated honestly
- Prompt injection does not go away because the model is local
- Quantization has a quality cliff, and you can see it
- Streaming tokens into Compose without recomposing the world
- How do you test a screen whose text is generated?
- What a small on-device model simply cannot do
- The latency maths that decides on-device versus an API call
- adb install -r doesn't kill your app, so your screenshot is a lie
- Where your AI coding bill actually goes: I measured one long session
- An agent-driven refactor, with the receipts
- Running an AI agent in CI without it spending your budget
- Agent memory that is just files
- What should an AI agent never be allowed to do?
- The most useful thing my agent does is refuse
- The agent that writes the check that blocks it
- An agent that installs the app and looks at the screen
- The context window is not your constraint — what you put in it is
- Put the control flow in code, not in the prompt
- A hinge is a physical object, and your layout should know where it is 100 Days of Compose · Day 33
- Where to put the human
- MCP, or a shell script? The honest comparison
- I swept the parameter instead of arguing about it
- The same rule in two files is how agent-written code drifts
- One level of fan-out, and why that is enough
- The prompt is not the artifact — the runtime is
- 429 is not an error, it is a schedule
- A gate that fixes what it finds
- Six guardrails that stop my AI agent mid-command
- Skills, tools, and the difference that matters
- Stop parsing your AI's prose. Make it return a schema you can assert on.
- Subagents are context isolation, not parallelism
- What to log when the actor is a model
- One tool call can destroy your agent's context window
- A verification step that can pass while the bug is present is not a verification step
- Write down what you did to an external service
- Three kinds of code I stopped letting an agent write
- Teaching an agent when to stop
- SupportingPaneScaffold is for content that is about other content 100 Days of Compose · Day 32
- ListDetailPaneScaffold: the back button is the whole reason it exists 100 Days of Compose · Day 31
- There are three adaptive layouts, and you probably don't need a fourth 100 Days of Compose · Day 30
- Screen size is the wrong thing to branch on. Window size is the right one. 100 Days of Compose · Day 29
- Porting FlexboxLayout: every flex property maps onto something, but not onto one thing 100 Days of Compose · Day 28
- Sometimes the right grid is a Column of Rows 100 Days of Compose · Day 27
- FlowRow is the chip group you kept writing a custom Layout for 100 Days of Compose · Day 26
- HorizontalPager is a lazy list that snaps — and PagerState is where the bugs live 100 Days of Compose · Day 25
- A grid is not a list with two columns 100 Days of Compose · Day 24
- LazyColumn is not a faster Column, and items(key = …) is not optional 100 Days of Compose · Day 23
- Write a custom modifier only after you've ruled out a function 100 Days of Compose · Day 22
- Why fillMaxWidth() sometimes does nothing 100 Days of Compose · Day 21
- padding().background() and background().padding() are not the same picture 100 Days of Compose · Day 20
- A Modifier is a linked list, and that explains almost everything about it 100 Days of Compose · Day 19
- Column, Row, Box — and the single-pass rule that makes them fast 100 Days of Compose · Day 18
- Not every state holder is a ViewModel 100 Days of Compose · Day 17
- The UI layer has two halves, and merging them is why screens become untestable 100 Days of Compose · Day 16
- CompositionLocal is not dependency injection, and using it that way hurts later 100 Days of Compose · Day 15
- rememberSaveable survives what remember doesn't — and it isn't a bigger remember 100 Days of Compose · Day 14
- Six parameters deep and none of them are used here 100 Days of Compose · Day 13
- The ViewModel is not the default place for state. It's the place for state that outlives the screen. 100 Days of Compose · Day 12
- State hoisting is one question: who else needs to know? 100 Days of Compose · Day 11
- Composition, layout, drawing — and why reading a value in the wrong one costs frames 100 Days of Compose · Day 10
- Your composable ran four times. So did your network call. 100 Days of Compose · Day 9
- A composable's lifecycle is three events, and none of them is onDestroy 100 Days of Compose · Day 8
- Recomposition doesn't re-run your screen. It re-runs the scopes that read what changed. 100 Days of Compose · Day 7
- What @Composable actually does to your function 100 Days of Compose · Day 6
- Thinking in Compose: you can't reach into the UI any more, and that's the point 100 Days of Compose · Day 5
- Compose has two version problems, and the BOM only solves one 100 Days of Compose · Day 4
2024 3
- Basics of Jetpack Compose 100 Days of Compose · Day 3
- What is Jetpack Compose? 100 Days of Compose · Day 2
- Analyse your Android knowledge before you learn anything new 100 Days of Compose · Day 1
Undated 12
- Asking AI to check its own work does not work. Ask it to disprove it.
- The agent said 'fixed'. The list said otherwise
- An agent that checks the page itself
- A test that cannot fail is not a test. Mine passed twice.
- Design for the API call you cannot take back
- Telling an AI agent your rules does not make it follow them
- 100 technical posts, and the gates that stopped them being slop
- Every action your AI agent takes should be safe to run twice
- Read the status code before you believe the README
- The command that leaked a secret was not cat
- A stale doc does more damage than a missing one
- Why every command runtime is capped at 200 lines