The start of the year is a great time to clean up your Android project. A stable build and a clear structure
make day-to-day development faster and reduce “mystery” regressions.
Quick checklist
- Pin Kotlin/AGP/Gradle versions and align key libraries (use a BOM when it makes sense).
- Split modules by feature (for larger apps) to avoid a giant “app” module.
- Define dependency rules (e.g., domain → data → app/ui) and enforce them.
Practical tip: if build times are painful, measure first and prioritize changes with proven impact (build
caching, configuration cache, and reducing unnecessary annotation processing).
Compose helps you move quickly, but moving fast without breaking things requires conventions. Focus on
keeping UI pure (stateless when possible), making state explicit, and avoiding scattered side effects.
Suggested rules
- Separate screen state (UI) from domain state (business logic).
- Centralize side effects (navigation, analytics) to keep screens testable.
- Adopt design tokens for typography, spacing, and color to keep UI consistent.
If your codebase is still heavy on XML Views, migrate incrementally: use Compose for new screens first, then
move the highest-churn screens next.
Flow is powerful, but without “rules of the road” a project becomes unpredictable: where retries happen,
how errors propagate, and what cancellation does in practice.
Simple conventions that scale
- Repositories return a consistent shape (e.g., Result-like or sealed states such as Success/Failure/Loading).
- Keep retry/backoff in the data layer; UI should only render state.
- Define a dispatcher policy (IO for I/O, Default for CPU, Main for UI).
Offline-first does not have to be a “big rewrite”. The minimum goal is simple: users see data instantly,
then the app syncs in the background when connectivity is available.
Implementation ideas
- Use Room as the UI’s source of truth.
- Trigger sync via user intent (pull-to-refresh), app start, and/or background work.
- Track local mutations (dirty flags) when you need two-way sync.
Network failures are often intermittent. What you want is controlled retries, caching in the right places,
and enough observability to understand where things go wrong.
- Retry only transient failures (timeouts, 5xx); avoid retrying 4xx.
- Cache responses when UX benefits (lists and low-churn content).
- Log with context: endpoint, HTTP status, and correlation IDs when available.
WorkManager solves “reliable execution” under Android constraints. The tricky part is choosing the right
constraints and not overusing periodic work.
- Use one-time work for event-driven sync; periodic work only for truly recurring tasks.
- Set constraints (network, charging, battery-not-low) when appropriate.
- Use backoff to avoid spamming jobs when the server is failing.
Optimizing by gut feeling wastes time. A better loop is: measure → find bottlenecks → optimize → measure
again.
Three metrics to start with
- App start time (cold/warm).
- Jank during scrolling and lists.
- Network + DB latency on your main screens.
Effective tests cover what breaks most often. Instead of testing everything, prioritize business logic,
data mapping, and the critical UI flows.
- Unit tests for use-cases/domain and DTO → model mapping.
- UI tests for the happy path plus a few common failures.
- Contract tests (or a mock server) for key endpoints.
A few small changes can reduce risk dramatically: avoid storing sensitive data in plaintext, define logging
rules, and harden your release builds.
- Tokens/secrets: avoid plaintext; consider Keystore-backed encryption.
- Logging: do not log PII; do not log auth headers.
- Release: enable shrinking/minification when appropriate; review exported components.
A solid release process lets you experiment safely: staged rollouts, crash/ANR monitoring, and fast rollback
when needed.
- Standardize versioning and release notes.
- Use staged rollouts.
- Track crash/ANR by version, device, and OS.
Accessibility is more than adding content descriptions. It’s intentional design: logical focus order,
adequate touch targets, and sufficient contrast.
- Ensure minimum touch targets; avoid tiny buttons.
- Test with TalkBack: reading order and labels.
- For Compose/XML, verify semantics and roles.
End of year is a good time to consolidate lessons: reduce coupling, clarify boundaries, and make the codebase
easier to change next year.
Three signs you should refactor
- A small change requires edits across many screens/modules.
- UI depends directly on DTOs/network types.
- Business logic is mixed into ViewModels or UI code.
Aim for a stable domain, flexible data implementations, and UI focused on presentation. Clear boundaries
make testing easier and onboarding faster.