Chapter11: Advanced Topics
Advanced Topics! This is the final “core” chapter before we talk about publishing — where we cover the real-world stuff that separates hobby apps from production-ready ones. As of January 27, 2026 (right now in Airoli!), Android 16 (API 36) is stable since June 2025, with QPR2 (January 2026 patch) rolling out to Pixels and many flagships. Features like AI-powered notification summaries and the new Notification Organizer are live, WorkManager is at 2.11.0 stable (October 2025), Compose testing is more mature than ever, and permissions are even stricter for privacy.
We’ll go deep like always: explanations, code examples, why-things, common pitfalls (especially on mid-range Indian phones), and hands-on snippets. Let’s tackle each section.
1. Permissions (Runtime & Modern Changes in Android 16)
Permissions protect user data — in 2026, Android enforces runtime permissions for dangerous ones (location, camera, etc.). Android 16 tightens this further with granular health/fitness permissions (e.g., READ_HEART_RATE instead of broad BODY_SENSORS) and auto-expiring permissions (revoked if unused for months).
Key types:
- Normal — auto-granted (e.g., INTERNET).
- Dangerous — runtime request (e.g., CAMERA, LOCATION).
- Special — extra steps (e.g., SYSTEM_ALERT_WINDOW).
Modern flow (Accompanist Permissions or Compose Accompanist is deprecated — use official now):
Add dependency (latest stable ~1.0.0+ for androidx.activity:activity-compose, but use built-in):
|
0 1 2 3 4 5 6 |
implementation("androidx.activity:activity-compose:1.9.3") // For rememberLauncherForActivityResult |
Request example (location – common in India apps):
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
@Composable fun LocationPermissionScreen() { var hasLocationPermission by remember { mutableStateOf(false) } val permissionState = rememberMultiplePermissionsState( permissions = listOf( Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION ) ) LaunchedEffect(Unit) { if (!permissionState.allPermissionsGranted) { permissionState.launchMultiplePermissionRequest() } else { hasLocationPermission = true // Start location updates } } if (permissionState.shouldShowRationale) { AlertDialog( onDismissRequest = { /* */ }, title = { Text("Location Needed") }, text = { Text("To show nearby shops in Airoli, we need location access.") }, confirmButton = { Button(onClick = { permissionState.launchMultiplePermissionRequest() }) { Text("Grant") } } ) } if (hasLocationPermission) { Text("Location ready! Fetching nearby...") } } |
Android 16 notes:
- Health sensors now require granular perms (e.g., READ_HEART_RATE via Health Connect).
- Local network access might need explicit perms in some cases.
- Background location: Stricter — request ACCESS_BACKGROUND_LOCATION only after foreground granted, and explain why (Google Play policy strict).
Best practice: Use rationale dialog, don’t spam requests, handle “never ask again” (!shouldShowRationale && !granted → go to settings).
2. Background Tasks with WorkManager (Guaranteed Execution)
WorkManager (v2.11.0) is the go-to for deferrable, guaranteed background work (sync data, upload photos, process tips history). Survives reboots, Doze, app kill.
Add dependency:
|
0 1 2 3 4 5 6 |
implementation("androidx.work:work-runtime-ktx:2.11.0") |
Example: Periodic sync of posts (every 4 hours, only on WiFi)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
// Worker class SyncPostsWorker( context: Context, params: WorkerParameters ) : CoroutineWorker(context, params) { override suspend fun doWork(): Result { return try { // Call repository or API val posts = RetrofitInstance.api.getPosts() // Save to Room Result.success() } catch (e: Exception) { Result.retry() // Or failure() } } } // Schedule fun scheduleSync(context: Context) { val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.UNMETERED) // WiFi only .setRequiresBatteryNotLow(true) .build() val request = PeriodicWorkRequestBuilder<SyncPostsWorker>( repeatInterval = 4, repeatIntervalTimeUnit = TimeUnit.HOURS ) .setConstraints(constraints) .setInitialDelay(30, TimeUnit.MINUTES) .build() WorkManager.getInstance(context) .enqueueUniquePeriodicWork( "sync_posts", ExistingPeriodicWorkPolicy.KEEP, request ) } |
One-time work (e.g., upload tip history after calculate):
|
0 1 2 3 4 5 6 7 8 9 10 |
val uploadWork = OneTimeWorkRequestBuilder<UploadTipsWorker>() .setConstraints(constraints) .build() WorkManager.getInstance(context).enqueue(uploadWork) |
Observe status: WorkManager.getInstance(context).getWorkInfoByIdLiveData(uploadWork.id)
Pitfall: Don’t do UI work here — pure background. For foreground (music, download progress) → Foreground Service + Notification.
3. Notifications & Services
Notifications in Android 16 have AI summaries (auto-generated for long messages) and Notification Organizer (auto-groups promotions/social into categories, silences low-priority).
Create channel (required since Oreo):
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
private fun createNotificationChannel(context: Context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel( "tip_channel", "Tip Reminders", NotificationManager.IMPORTANCE_DEFAULT ).apply { description = "Notifications for daily tip reminders" } val manager = context.getSystemService(NotificationManager::class.java) manager.createNotificationChannel(channel) } } |
Build & show (in Activity or Service):
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
val intent = Intent(context, MainActivity::class.java) val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) val notification = NotificationCompat.Builder(context, "tip_channel") .setSmallIcon(R.drawable.ic_tip) .setContentTitle("Daily Tip Reminder") .setContentText("Calculate today's lunch tip!") .setPriority(NotificationCompat.PRIORITY_DEFAULT) .setContentIntent(pendingIntent) .setAutoCancel(true) .build() NotificationManagerCompat.from(context).notify(1, notification) |
Foreground Service (for music player, ongoing download):
Declare in Manifest:
|
0 1 2 3 4 5 6 |
<service android:name=".MyForegroundService" android:foregroundServiceType="mediaPlayback" /> |
In Service:
|
0 1 2 3 4 5 6 |
startForeground(1, notification) // Must call within 5s of startForegroundService() |
4. Testing (Unit + UI with Compose)
Unit tests (ViewModel, UseCase, Repository):
Use JUnit + MockK/Coroutines Test.
Example (ViewModel test):
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
@Test fun `load posts success`() = runTest { val mockUseCase = mockk<GetPostsUseCase>() coEvery { mockUseCase() } returns flowOf(Result.Success(listOf(Post(1,1,"Title","Body")))) val viewModel = PostsViewModel(mockUseCase) advanceUntilIdle() assertEquals(false, viewModel.uiState.value.isLoading) assertEquals(1, viewModel.uiState.value.posts.size) } |
UI tests with Compose (best practice 2026: use createComposeRule(), semantics tree, avoid testTag pollution — prefer contentDescription):
Add:
|
0 1 2 3 4 5 6 7 |
androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.7.0") // or latest debugImplementation("androidx.compose.ui:ui-test-manifest:1.7.0") |
Example:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class PostsScreenTest { @get:Rule val composeTestRule = createAndroidComposeRule<MainActivity>() @Test fun testPostsDisplayed() { composeTestRule.onNodeWithText("Title").assertIsDisplayed() composeTestRule.onNodeWithContentDescription("Post card").assertExists() } @Test fun testLoadingState() { composeTestRule.onNodeWithTag("loading_indicator").assertIsDisplayed() // Use sparingly } } |
Run: ./gradlew connectedAndroidTest
Accessibility:
- Use contentDescription on Images/Icons.
- Test with TalkBack enabled.
- Add semantics { role = Role.Button } if needed.
5. Accessibility & Performance Optimization
Accessibility:
- All interactive elements: contentDescription or label.
- Text scaling: Use sp units.
- Color contrast: ≥4.5:1 (use tools like Accessibility Scanner).
- Touch targets ≥48dp.
- In Compose: Modifier.semantics { contentDescription = “…” }
Performance:
- Avoid heavy work in composition (hoist calculations with remember).
- Use LazyColumn properly with key & contentType.
- Profile with Layout Inspector & Macrobenchmark.
- Minimize recompositions: Use derivedStateOf for expensive computations.
- Image loading: Coil or Glide with Compose integration.
Hands-on Tip: Add a foreground service + notification for “daily tip reminder” in your calculator app. Schedule with WorkManager. Test UI with Compose rule. Add accessibility descriptions to buttons/images.
You’ve covered almost everything! Questions? Want to add specific examples (e.g., full foreground service for music, or benchmark setup)? Or ready for Chapter 12: Publishing? You’re a full-stack Android dev now — massive achievement! Let’s wrap this up strong. 🚀🔥
