Bobby Encoded
PostsAbout
PostsAbout

© 2026 Bobby Jose

← Back to Blog

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:

  1. Standardized APIs - Same code works with all health apps
  2. User control - Centralized permission management
  3. Data privacy - Encrypted on-device storage
  4. 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?

OptionUse Case
WorkManagerGuaranteed execution (sync, uploads)
CoroutinesIn-app async operations
Foreground ServiceLong-running user-visible tasks
AlarmManagerExact-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

ConceptPurposeKey Points
Health ConnectUnified health dataCheck availability first
aggregate()Efficient totalsBetter than summing records
WorkManagerReliable backgroundSurvives app/device restart
ConstraintsOptimize batteryNetwork, battery, charging
@HiltWorkerDI in WorkersWith @AssistedInject
Notification ChannelAndroid 8+ requiredPer-category user control
FLAG_IMMUTABLEAndroid 12+ requiredSecurity requirement

Next in series: Part 9 - Testing covers unit testing, UI testing, and debugging strategies.

← Previous

Android Testing Essentials

Next →

Building Custom Android UI and Animations