Overview
Android Injector Best Practices: Clean Architecture & Modular Code covers how to design dependency injection (DI) in Android apps to keep code modular, testable, and maintainable while respecting separation of concerns.
Goals
- Enforce clear boundaries between layers (UI, domain, data).
- Minimize coupling by providing dependencies through well-defined interfaces.
- Make components small, focused, and easily testable.
- Keep DI configuration declarative and centralized where appropriate.
Principles
- Single Responsibility: Each module/component provides one responsibility.
- Invert Dependencies: High-level modules should not depend on low-level modules; depend on abstractions.
- Explicit Wiring: Prefer explicit bindings over pervasive global/static access.
- Constructor Injection First: Favor constructor injection for required dependencies; use method/setter injection only for optional or framework-managed cases.
- Limit Scope: Bind dependencies with the narrowest lifecycle (singleton, activity, fragment, per-request) to avoid memory leaks and improper sharing.
- Interface over Implementation: Expose interfaces from modules and keep implementations internal.
Architecture Patterns
- Use Clean Architecture layers:
- Presentation (ViewModels, UI) — depends on Domain.
- Domain (use-cases, business logic) — pure Kotlin, no Android framework.
- Data (repositories, network, persistence) — implements domain interfaces.
- Keep DI modules aligned to these layers and expose only the abstractions consumers need.
Modularization Guidelines
- Split code into Gradle modules by feature or layer (feature modules, core, network, ui-common).
- Each module should:
- Define its public interfaces and DI entry points.
- Not depend on feature modules of the same layer.
- Include a small DI module that exposes bindings needed by others.
- Avoid sharing Android framework classes across module boundaries; use abstractions or small wrapper interfaces.
DI Setup Best Practices
- Prefer Hilt (or Dagger) for compile-time DI; Koin for simpler runtime DI if desired.
- Keep component/graph creation predictable: create app-wide graph in Application, tie activity/fragment scopes to lifecycle.
- Use qualifiers to distinguish multiple implementations of the same interface.
- Keep module files small and purpose-driven (e.g., NetworkModule, DatabaseModule, FeatureModule).
Scoping & Lifecycle
- Map DI scopes to Android lifecycles (Singleton -> Application, ActivityRetained/Activity -> Activity/ViewModel, Fragment -> Fragment).
- Use ViewModel injection for UI-scoped dependencies to survive configuration changes safely.
- Avoid leaking Context by injecting ApplicationContext where needed; do not inject Activity or View contexts into long-lived singletons.
Testing Strategy
- Design modules so dependencies can be swapped with fakes/mocks easily.
- Provide test-specific DI modules or component builders to inject test doubles.
- Write unit tests for domain/use-cases (no Android). Use Robolectric or instrumentation tests only when necessary.
- Use dependency injection to make UI tests more deterministic (inject fake repositories, controlled schedulers).
Security & Stability
- Validate inputs at boundaries (repositories, network layer).
- Avoid reflective runtime wiring for critical components—prefer compile-time DI.
- Keep third-party SDKs isolated in their own modules to limit blast radius.
Common Pitfalls & How to Avoid Them
- Over-scoping singletons — bind narrowly.
- Service locator anti-pattern — prefer explicit injection.
- Large monolithic modules — split by responsibility.
- Tight coupling between features — use interfaces and module-defined entry points.
Quick Checklist
- Constructor injection for required deps.
- Modules align with Clean Architecture layers.
- Narrow scopes mapped to lifecycles.
- Interfaces for cross-module boundaries.
- Test DI graph or use test modules.
- Avoid leaking Context in singletons.
- Use qualifiers for multiple bindings.
If you want, I can generate an example DI module structure (Hilt or Dagger
Leave a Reply