Android Development Key Considerations
Android Development Key Considerations
Introduction
Android development is a complex ecosystem spanning diverse devices, OS versions, and hardware capabilities. This blog summarizes the most important considerations developers should keep in mind to build robust, performant, and maintainable Android applications.
1. Lifecycle Management
Why It Matters
Improper lifecycle handling is one of the most common sources of crashes, memory leaks, and unexpected behavior.
Key Points
- Always respect the Activity/Fragment lifecycle: Never assume a component is visible when performing UI updates.
- Use lifecycle-aware components:
LifecycleObserver,LiveData, andViewModelhelp decouple business logic from the view layer. - Avoid memory leaks: Long-running tasks (threads, callbacks, handlers) can hold references to destroyed Activities. Use
WeakReferenceor cancel tasks inonDestroy(). - Handle configuration changes: Screen rotations recreate the Activity by default. Persist state via
ViewModeloronSaveInstanceState()instead of member variables.
Common Pitfall
Performing network requests in onCreate() without checking whether the Activity is still active before updating the UI leads to crashes after rotation.
2. Memory Management
Why It Matters
Android devices have limited memory. The system aggressively kills background apps to reclaim resources.
Key Points
- Avoid static references to Context: Holding a static reference to an Activity or its Context prevents garbage collection and causes severe leaks.
- Use
ApplicationContext when appropriate: For long-lived objects (singletons, utility classes), usegetApplicationContext()instead of an Activity Context. - Release heavy resources in
onStop()oronDestroy(): Bitmaps, file handles, database cursors, and media players should be explicitly released. - Monitor memory with Profiler: Use Android Studio’s Memory Profiler to detect leaks and excessive allocations.
- Use
WeakReference/SoftReferencefor caches: This allows the GC to reclaim memory under pressure.
Bitmap Handling
- Decode with
inSampleSizeto downsample large images before loading into memory. - Use
BitmapFactory.Options.inJustDecodeBoundsfirst to measure dimensions without allocating memory. - Prefer Glide or Coil for image loading; they handle caching, downsampling, and lifecycle automatically.
3. Background Work
Why It Matters
Since Android 8.0 (Oreo), background execution is heavily restricted. Improper background work results in silent failures or app kills.
Key Points
- Use
WorkManagerfor deferrable, guaranteed work: It handles persistence, retries, and constraints (network, charging, idle) across all OS versions. - Use
ForegroundServicefor user-visible long-running tasks: Must show a persistent notification. - Avoid
AsyncTask: It is deprecated and error-prone with lifecycle changes. Use Coroutines or RxJava instead. - Respect Doze mode and App Standby: Schedule work with
WorkManagerconstraints rather than polling. - Use Coroutines with
lifecycleScope/viewModelScope: Automatically cancels when the scope is destroyed.
Coroutines Best Practice
class MyViewModel : ViewModel() {
fun loadData() {
viewModelScope.launch {
try {
val result = repository.fetchData()
_uiState.value = Success(result)
} catch (e: Exception) {
_uiState.value = Error(e)
}
}
}
}
4. UI and Layout Performance
Why It Matters
Janky UI directly hurts user experience. Frames taking longer than 16ms cause visible stutter.
Key Points
- Keep the view hierarchy flat: Deep nesting slows measure/layout passes. Use
ConstraintLayoutto flatten complex layouts. - Avoid
LinearLayoutnesting: Replace withConstraintLayoutorRelativeLayoutwhere possible. - Use
RecyclerViewinstead ofListView: Efficient view recycling and built-in diffing viaDiffUtil. - Prefer Jetpack Compose: Declarative UI with automatic recomposition tracking reduces boilerplate and improves performance when used correctly.
- Optimize overdraw: Remove unnecessary backgrounds. Debug with “Show GPU Overdraw” in developer options.
- Use
ViewStubfor rarely-shown views: Inflated only when needed. - Cache expensive computations: Memoize results in the ViewModel to avoid recomputing on every recomposition.
Compose-Specific Tips
- Mark stable classes with
@Immutableor@Stableto skip unnecessary recompositions. - Use
key()inLazyColumnitems to preserve state across list changes. - Avoid creating lambdas in hot paths; hoist stable callbacks.
5. Threading and Concurrency
Why It Matters
Blocking the main thread freezes the UI and triggers an ANR (Application Not Responding) dialog after 5 seconds.
Key Points
- Never do I/O on the main thread: Database, network, and file operations must run on background dispatchers.
- Use structured concurrency: Coroutines with
CoroutineScopeensure cancellation propagates correctly. - Switch dispatchers appropriately:
Dispatchers.Mainfor UI updates.Dispatchers.IOfor network and database.Dispatchers.Defaultfor CPU-intensive work.
- Avoid
Thread.sleep()on main: Usedelay()in coroutines instead, which is non-blocking. - Handle cancellation cooperatively: Check
isActivein long loops, or useensureActive().
6. Data Storage
Why It Matters
Choosing the wrong storage strategy leads to data loss, poor performance, or security issues.
Options Overview
| Use Case | Recommended Solution |
|---|---|
| Key-value (small) | SharedPreferences / DataStore |
| Structured local data | Room (SQLite abstraction) |
| Sensitive data | EncryptedSharedPreferences |
| Large files | Internal/External storage with scoped access |
| Cache | CacheDir with eviction policy |
Key Points
- Prefer
DataStoreoverSharedPreferences: Async, type-safe, and avoids blocking the main thread on disk reads. - Use Room with Coroutines/RxJava: Synchronous DB calls on the main thread crash on Android 9+.
- Apply scoped storage (Android 10+): Use
MediaStoreand Storage Access Framework instead of direct file paths. - Never store plaintext secrets: Use the Android Keystore system for cryptographic keys and
EncryptedSharedPreferencesfor sensitive values.
7. Network Communication
Why It Matters
Mobile networks are unreliable, metered, and high-latency. Naive networking causes battery drain and poor UX.
Key Points
- Use Retrofit + OkHttp: Industry standard with interceptors, logging, and caching support.
- Set reasonable timeouts: Connect (10-30s), read (30-60s), and write timeouts prevent infinite hangs.
- Implement exponential backoff with jitter: Avoid thundering-herd retries when the server is overloaded.
- Cache responses: Use HTTP cache headers and OkHttp’s
Cacheto reduce bandwidth and latency. - Handle offline state: Detect connectivity with
ConnectivityManager.NetworkCallbackand queue requests via WorkManager. - Compress payloads: Use Gzip and prefer binary formats (Protobuf) for large datasets.
- Avoid polling: Use Firebase Cloud Messaging (FCM) or WebSockets for real-time updates.
8. Security
Why It Matters
Android apps run in a hostile environment with rooted devices, repackaging attacks, and MITM threats.
Key Points
- Enable
ProGuard/R8obfuscation: Removes unused code and obfuscates classes, raising reverse-engineering cost. - Use
android:allowBackup="false"for sensitive apps: Prevents backup extraction viaadb. - Enforce certificate pinning: Protects against compromised CAs and MITM attacks.
- Store secrets in the Keystore, not in code: Hardcoded keys are trivially extracted from APKs.
- Validate all inputs: Sanitize data from Intents, deep links, and external storage to prevent injection.
- Use
FLAG_SECUREon sensitive screens: Prevents screenshots and screen recording. - Sign release builds with a protected keystore: Never commit the keystore or its password to version control.
9. Permissions
Why It Matters
Runtime permissions (Android 6+) and one-time permissions (Android 11+) changed how apps request access.
Key Points
- Request permissions at the point of need: Do not request all permissions on launch; ask when the feature is used.
- Explain why before asking: Show a rationale before the system dialog if permission was previously denied.
- Handle “Don’t ask again”: Check
shouldShowRequestPermissionRationale()and guide the user to settings if permanently denied. - Use scoped storage instead of
READ_EXTERNAL_STORAGE: Avoid broad file permissions on Android 10+. - Declare minimal permissions: Remove unused permissions from the manifest to reduce review friction and attack surface.
10. Compatibility and Versioning
Why It Matters
Android fragmentation means apps run on OS versions from old releases to the latest.
Key Points
- Set
minSdkVersionbased on real distribution data: Don’t support versions with negligible market share. - Use version checks for new APIs: Wrap in
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.X)blocks. - Leverage AndroidX (Jetpack): Backward-compatible libraries reduce manual version handling.
- Test on multiple API levels: Use Android Studio’s virtual devices covering min, target, and latest SDKs.
- Handle vendor-specific quirks: Some OEMs customize permission behavior, background limits, and notifications. Test on real devices from major OEMs.
- Use
AppCompatthemes and components: Ensures consistent Material Design across versions.
11. Testing
Why It Matters
Manual testing does not scale. Automated tests catch regressions and enable confident refactoring.
Key Points
- Follow the testing pyramid: Many unit tests, fewer integration tests, fewest UI tests.
- Use JUnit + Mockito/MockK for unit tests: Test ViewModel and repository logic in isolation.
- Use Robolectric for Android-dependent unit tests: Runs Android code on the JVM without an emulator.
- Use Espresso/UI Automator for UI tests: Verify user flows end-to-end.
- Use Hilt for testable DI: Replace bindings in tests with fakes easily.
- Write integration tests for Room: Use an in-memory database to verify queries and migrations.
- Test edge cases: Empty states, network failures, permission denials, and low memory.
12. App Size and Build Optimization
Why It Matters
Large APKs reduce install conversion rates and increase update friction.
Key Points
- Enable App Bundles (
*.aab): Google Play generates optimized APKs per device, stripping unused densities and ABIs. - Use R8 full mode: Aggressive shrinking and optimization.
- Remove unused resources:
shrinkResources truein the release build type. - Optimize images: Use WebP instead of PNG/JPEG for lossless and lossy compression.
- Lazy-load feature modules: Use Dynamic Feature Modules for rarely-used features.
- Monitor the R8/ProGuard output: Ensure required classes are kept; over-aggressive shrinking can break reflection.
13. Observability and Debugging
Why It Matters
You cannot fix what you cannot see. Production issues require remote visibility.
Key Points
- Integrate Crashlytics or a similar crash reporter: Capture stack traces, device info, and breadcrumbs.
- Log structured, not ad-hoc: Use consistent tags and log levels; strip verbose logs in release builds.
- Use Timber for logging: Automatically strips logs in release and supports tree-based redaction.
- Add analytics for key funnels: Understand where users drop off, but respect privacy and GDPR/CCPA.
- Use StrictMode in debug builds: Detects disk and network access on the main thread, and leaked SQLite cursors.
- Profile before optimizing: Premature optimization wastes effort. Use CPU Profiler and Systrace/Perfetto to find real bottlenecks.
14. Accessibility
Why It Matters
Accessible apps reach more users, including those with visual, motor, or cognitive impairments, and are often required by law.
Key Points
- Set meaningful
contentDescriptionon ImageViews: Empty for decorative images, descriptive for meaningful ones. - Ensure sufficient touch target size: Minimum 48dp x 48dp for interactive elements.
- Support font scaling: Use
spfor text sizes; test with the largest system font setting. - Maintain color contrast: Minimum 4.5:1 for normal text per WCAG guidelines.
- Use
TalkBackto test navigation: Ensure focus order is logical and all interactive elements are reachable. - Label form fields: Associate labels with inputs via
labelForso screen readers announce them correctly.
15. Dependency Management
Why It Matters
Bloated or outdated dependencies increase build time, attack surface, and maintenance burden.
Key Points
- Pin dependency versions: Avoid transitive version surprises; use Gradle’s
resolutionStrategyor a version catalog. - Audit dependencies regularly: Check for known vulnerabilities with dependency scanning tools.
- Prefer AndroidX over support libraries: The support libraries are deprecated.
- Minimize dependency count: Each dependency adds APK size, build time, and potential conflict risk.
- Use Gradle’s build cache and configuration caching: Significantly reduces build times for large projects.
Conclusion
Android development rewards careful attention to lifecycle, memory, threading, and the evolving platform constraints. The common thread across all these considerations is: do not assume the happy path. Plan for rotation, low memory, network loss, permission denial, and aggressive background limits from the start. Building these edge cases into your architecture from day one is far cheaper than patching them after users hit them in production.
Keep your dependencies lean, your tests meaningful, and your observability sharp. The rest follows from disciplined application of the principles above.
- 点赞
- 收藏
- 关注作者
评论(0)