Android System Integrations: Health Connect, WorkManager, and More
October 15, 2025 · 8 min read
Android, Kotlin, Health Connect, WorkManager, Notifications, Interview Prep
Part 8 of the Android Deep Dive series
Android provides rich system integrations that set native apps apart from cross-platform alternatives. Health Connect for standardized health data, WorkManager for reliable background tasks, and notifications for user engagement are increasingly important interview topics in 2025-2026.
2025-2026 Trend
Health Connect is Android's answer to Apple HealthKit. With wearables booming, interviewers expect familiarity with health data APIs, especially for fitness and health-related positions.
Health Connect
Setup
// AndroidManifest.xml permissions
<uses-permission android:name="android.permission.health.READ_NUTRITION" />
<uses-permission android:name="android.permission.health.WRITE_NUTRITION" />
<uses-permission android:name="android.permission.health.READ_STEPS" />
<uses-permission android:name="android.permission.health.READ_ACTIVE_CALORIES_BURNED" />
<uses-permission android:name="android.permission.health.READ_SLEEP" />
Health Connect Manager
class HealthConnectManager @Inject constructor(
@ApplicationContext private val context: Context
) {
private val healthConnectClient by lazy {
HealthConnectClient.getOrCreate(context)
}
// Required permissions
val permissions = setOf(
HealthPermission.getReadPermission(NutritionRecord::class),
HealthPermission.getWritePermission(NutritionRecord::class),
HealthPermission.getReadPermission(StepsRecord::class),
HealthPermission.getReadPermission(ActiveCaloriesBurnedRecord::class),
HealthPermission.getReadPermission(SleepSessionRecord::class)
)
// Check if Health Connect is available
fun isAvailable(): Boolean {
return HealthConnectClient.getSdkStatus(context) ==
HealthConnectClient.SDK_AVAILABLE
}
// Check granted permissions
suspend fun hasAllPermissions(): Boolean {
val granted = healthConnectClient.permissionController.getGrantedPermissions()
return permissions.all { it in granted }
}
// Read daily steps
suspend fun readSteps(date: LocalDate): Long {
val startTime = date.atStartOfDay(ZoneId.systemDefault()).toInstant()
val endTime = date.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant()
val request = ReadRecordsRequest(
recordType = StepsRecord::class,
timeRangeFilter = TimeRangeFilter.between(startTime, endTime)
)
return healthConnectClient.readRecords(request).records
.sumOf { it.count }
}
// Aggregate data (more efficient)
suspend fun getStepsAggregate(date: LocalDate): Long {
val startTime = date.atStartOfDay(ZoneId.systemDefault()).toInstant()
val endTime = date.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant()
val response = healthConnectClient.aggregate(
AggregateRequest(
metrics = setOf(StepsRecord.COUNT_TOTAL),
timeRangeFilter = TimeRangeFilter.between(startTime, endTime)
)
)
return response[StepsRecord.COUNT_TOTAL] ?: 0L
}
// Read sleep sessions
suspend fun readSleep(date: LocalDate): List<SleepSessionRecord> {
// Sleep usually spans two days
val startTime = date.minusDays(1).atTime(18, 0)
.atZone(ZoneId.systemDefault()).toInstant()
val endTime = date.atTime(18, 0)
.atZone(ZoneId.systemDefault()).toInstant()
val request = ReadRecordsRequest(
recordType = SleepSessionRecord::class,
timeRangeFilter = TimeRangeFilter.between(startTime, endTime)
)
return healthConnectClient.readRecords(request).records
}
}
Interview Tip
Health Connect uses aggregates for efficient daily totals. Don't fetch individual records then sum - use aggregate() instead.
WorkManager
Background Sync Worker
@HiltWorker
class SyncWorker @AssistedInject constructor(
@Assisted context: Context,
@Assisted workerParams: WorkerParameters,
private val mealRepository: MealRepository,
private val healthConnectManager: HealthConnectManager
) : CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result {
return try {
// Sync pending local changes
mealRepository.syncPendingChanges()
// Sync from Health Connect
if (healthConnectManager.hasAllPermissions()) {
syncHealthData()
}
Result.success()
} catch (e: Exception) {
if (runAttemptCount < 3) {
Result.retry()
} else {
Result.failure()
}
}
}
private suspend fun syncHealthData() {
val today = LocalDate.now()
val steps = healthConnectManager.getStepsAggregate(today)
// Store steps...
}
companion object {
const val WORK_NAME = "nutrition_sync"
}
}
Scheduling Work
class SyncScheduler @Inject constructor(
@ApplicationContext private val context: Context
) {
private val workManager = WorkManager.getInstance(context)
fun schedulePeriodicSync() {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
val syncRequest = PeriodicWorkRequestBuilder<SyncWorker>(
repeatInterval = 1,
repeatIntervalTimeUnit = TimeUnit.HOURS
)
.setConstraints(constraints)
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
10,
TimeUnit.MINUTES
)
.build()
workManager.enqueueUniquePeriodicWork(
SyncWorker.WORK_NAME,
ExistingPeriodicWorkPolicy.KEEP,
syncRequest
)
}
fun scheduleImmediateSync() {
val syncRequest = OneTimeWorkRequestBuilder<SyncWorker>()
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build()
workManager.enqueue(syncRequest)
}
fun cancelSync() {
workManager.cancelUniqueWork(SyncWorker.WORK_NAME)
}
}
Hilt + WorkManager Integration
// Application class
@HiltAndroidApp
class NutritionApplication : Application(), Configuration.Provider {
@Inject lateinit var workerFactory: HiltWorkerFactory
override val workManagerConfiguration: Configuration
get() = Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()
}
Common Mistake
Don't use @Inject on Worker constructor directly. Use @HiltWorker with @AssistedInject for proper Hilt integration.
Notifications
Notification Channels
class NotificationHelper @Inject constructor(
@ApplicationContext private val context: Context
) {
private val notificationManager = context.getSystemService<NotificationManager>()
companion object {
const val CHANNEL_REMINDERS = "meal_reminders"
const val CHANNEL_ALERTS = "health_alerts"
const val CHANNEL_SYNC = "sync_status"
}
fun createChannels() {
val reminderChannel = NotificationChannel(
CHANNEL_REMINDERS,
"Meal Reminders",
NotificationManager.IMPORTANCE_HIGH
).apply {
description = "Reminders to log meals"
enableVibration(true)
}
val alertChannel = NotificationChannel(
CHANNEL_ALERTS,
"Health Alerts",
NotificationManager.IMPORTANCE_HIGH
).apply {
description = "Important health alerts"
enableVibration(true)
enableLights(true)
lightColor = Color.RED
}
val syncChannel = NotificationChannel(
CHANNEL_SYNC,
"Sync Status",
NotificationManager.IMPORTANCE_LOW
).apply {
description = "Background sync notifications"
}
notificationManager?.createNotificationChannels(
listOf(reminderChannel, alertChannel, syncChannel)
)
}
fun showMealReminder(mealType: MealType) {
val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
putExtra("destination", "log_meal")
putExtra("meal_type", mealType.name)
}
val pendingIntent = PendingIntent.getActivity(
context,
mealType.ordinal,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val notification = NotificationCompat.Builder(context, CHANNEL_REMINDERS)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle("Time for ${mealType.displayName}")
.setContentText("Don't forget to log your meal!")
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.addAction(
R.drawable.ic_camera,
"Take Photo",
createPhotoIntent(mealType)
)
.addAction(
R.drawable.ic_edit,
"Log Manually",
pendingIntent
)
.build()
notificationManager?.notify(mealType.ordinal, notification)
}
}
Meal Reminder Scheduler
@HiltWorker
class MealReminderWorker @AssistedInject constructor(
@Assisted context: Context,
@Assisted workerParams: WorkerParameters,
private val notificationHelper: NotificationHelper,
private val preferencesRepository: PreferencesRepository
) : CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result {
val mealTypeName = inputData.getString("meal_type") ?: return Result.failure()
val mealType = MealType.valueOf(mealTypeName)
// Check if reminders are enabled
val prefs = preferencesRepository.getPreferences()
if (!prefs.mealRemindersEnabled) return Result.success()
notificationHelper.showMealReminder(mealType)
return Result.success()
}
}
class MealReminderScheduler @Inject constructor(
@ApplicationContext private val context: Context,
private val preferencesRepository: PreferencesRepository
) {
private val workManager = WorkManager.getInstance(context)
fun scheduleMealReminders() {
val prefs = runBlocking { preferencesRepository.getPreferences() }
if (!prefs.mealRemindersEnabled) {
cancelAllReminders()
return
}
scheduleMealReminder(MealType.BREAKFAST, prefs.breakfastReminderTime)
scheduleMealReminder(MealType.LUNCH, prefs.lunchReminderTime)
scheduleMealReminder(MealType.DINNER, prefs.dinnerReminderTime)
}
private fun scheduleMealReminder(mealType: MealType, time: LocalTime) {
val now = LocalDateTime.now()
var scheduledTime = now.toLocalDate().atTime(time)
// If time already passed today, schedule for tomorrow
if (scheduledTime.isBefore(now)) {
scheduledTime = scheduledTime.plusDays(1)
}
val delay = Duration.between(now, scheduledTime).toMillis()
val inputData = Data.Builder()
.putString("meal_type", mealType.name)
.build()
val request = PeriodicWorkRequestBuilder<MealReminderWorker>(
repeatInterval = 1,
repeatIntervalTimeUnit = TimeUnit.DAYS
)
.setInitialDelay(delay, TimeUnit.MILLISECONDS)
.setInputData(inputData)
.build()
workManager.enqueueUniquePeriodicWork(
"reminder_${mealType.name}",
ExistingPeriodicWorkPolicy.REPLACE,
request
)
}
fun cancelAllReminders() {
MealType.entries.forEach { mealType ->
workManager.cancelUniqueWork("reminder_${mealType.name}")
}
}
}
Interview Questions
Q: What is Health Connect and why use it?
Health Connect is Android's unified platform for health data:
- Standardized APIs - Same code works with all health apps
- User control - Centralized permission management
- Data privacy - Encrypted on-device storage
- Interoperability - Share data between apps (Fitbit, Samsung Health, etc.)
// Read from any app that writes to Health Connect
val records = healthConnectClient.readRecords(
ReadRecordsRequest(
recordType = StepsRecord::class,
timeRangeFilter = TimeRangeFilter.between(start, end)
)
)
Q: How do you handle WorkManager with Hilt?
Use @HiltWorker annotation and @AssistedInject:
@HiltWorker
class SyncWorker @AssistedInject constructor(
@Assisted context: Context,
@Assisted workerParams: WorkerParameters,
private val repository: Repository // Injected by Hilt
) : CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result {
repository.sync()
return Result.success()
}
}
Also add HiltWorkerFactory to Application.
Q: When should you use WorkManager vs other background options?
| Option | Use Case |
|---|---|
| WorkManager | Guaranteed execution (sync, uploads) |
| Coroutines | In-app async operations |
| Foreground Service | Long-running user-visible tasks |
| AlarmManager | Exact-time scheduling (rare) |
// WorkManager - Will complete even if app killed
workManager.enqueue(uploadWorkRequest)
// Coroutine - Cancelled if ViewModel cleared
viewModelScope.launch { uploadData() }
// Foreground Service - Continues with notification
startForegroundService(intent)
Q: How do notification channels work?
Channels are required on Android 8+ (API 26). They let users control notification settings per category:
// Create channel BEFORE showing notification
val channel = NotificationChannel(
"reminders",
"Meal Reminders",
NotificationManager.IMPORTANCE_HIGH
).apply {
description = "Daily meal logging reminders"
enableVibration(true)
}
notificationManager.createNotificationChannel(channel)
// Then use channel ID in notification
NotificationCompat.Builder(context, "reminders")
.setContentTitle("Time for lunch!")
.build()
Common Mistakes
1. Not Handling Health Connect Unavailability
// BAD - Crashes on devices without Health Connect
val client = HealthConnectClient.getOrCreate(context)
// GOOD - Check availability first
if (HealthConnectClient.getSdkStatus(context) == SDK_AVAILABLE) {
val client = HealthConnectClient.getOrCreate(context)
} else {
// Show message or hide health features
}
2. Missing Notification Channels
// BAD - Notification won't show on Android 8+
notificationManager.notify(id, notification)
// GOOD - Create channel first
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(id, name, importance)
notificationManager.createNotificationChannel(channel)
}
notificationManager.notify(id, notification)
3. WorkManager Without Constraints
// BAD - Syncs even on metered/low battery
val request = PeriodicWorkRequestBuilder<SyncWorker>(1, TimeUnit.HOURS)
.build()
// GOOD - Add appropriate constraints
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
val request = PeriodicWorkRequestBuilder<SyncWorker>(1, TimeUnit.HOURS)
.setConstraints(constraints)
.build()
4. Forgetting PendingIntent Flags
// BAD - Security issue on Android 12+
val pendingIntent = PendingIntent.getActivity(
context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT
)
// GOOD - Add FLAG_IMMUTABLE or FLAG_MUTABLE
val pendingIntent = PendingIntent.getActivity(
context, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
Summary Table
| Concept | Purpose | Key Points |
|---|---|---|
| Health Connect | Unified health data | Check availability first |
aggregate() | Efficient totals | Better than summing records |
| WorkManager | Reliable background | Survives app/device restart |
| Constraints | Optimize battery | Network, battery, charging |
@HiltWorker | DI in Workers | With @AssistedInject |
| Notification Channel | Android 8+ required | Per-category user control |
FLAG_IMMUTABLE | Android 12+ required | Security requirement |
Next in series: Part 9 - Testing covers unit testing, UI testing, and debugging strategies.