---
id: android-ml-kit-health
title: Android ML Kit / Health Connect / On-device AI
category: Coding
status: draft
source_trust_level: B
verification_status: conceptual
created_at: 2026-05-09
updated_at: 2026-05-09
tags: [android, mlkit, health, on-device, vibe-coding]
tech_stack: { language: "Kotlin", applicable_to: ["Android"] }
applied_in: []
aliases: [ML Kit, Health Connect, MediaPipe, on-device ML, AICore, Gemini Nano]
---
# Android ML Kit / Health Connect / On-device AI
> Google ML Kit (built-in ML), Health Connect (cross-app health), MediaPipe (advanced ML), Gemini Nano (on-device LLM, Pixel 9+).
## π ν΅μ¬ κ°λ
- ML Kit: μΌλ° ML task λΉ λ₯Έ μ¬μ©.
- Health Connect: data ν΅ν© + permissions.
- MediaPipe: vision / LLM μ체 λͺ¨λΈ.
- Gemini Nano: AICore β on-device LLM.
## π» μ½λ ν¨ν΄
### ML Kit β Text recognition
```kotlin
implementation("com.google.mlkit:text-recognition:16.0.0")
```
```kotlin
import com.google.mlkit.vision.text.TextRecognition
import com.google.mlkit.vision.text.latin.TextRecognizerOptions
val recognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS)
val image = InputImage.fromBitmap(bitmap, rotation)
val result = recognizer.process(image).await() // suspend extension
for (block in result.textBlocks) {
for (line in block.lines) {
println(line.text)
}
}
```
β OCR. μμμ¦ / λͺ
ν¨.
### ML Kit β Barcode
```kotlin
implementation("com.google.mlkit:barcode-scanning:17.2.0")
val scanner = BarcodeScanning.getClient()
val barcodes = scanner.process(image).await()
for (barcode in barcodes) {
when (barcode.valueType) {
Barcode.TYPE_URL -> println("URL: ${barcode.url?.url}")
Barcode.TYPE_WIFI -> println("WiFi: ${barcode.wifi?.ssid}")
Barcode.TYPE_TEXT -> println(barcode.rawValue)
}
}
```
### ML Kit β Face detection
```kotlin
val options = FaceDetectorOptions.Builder()
.setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_FAST)
.setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_ALL)
.setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_ALL)
.build()
val detector = FaceDetection.getClient(options)
val faces = detector.process(image).await()
for (face in faces) {
val bounds = face.boundingBox
val smilingProb = face.smilingProbability ?: 0f
val leftEye = face.getLandmark(FaceLandmark.LEFT_EYE)?.position
}
```
### ML Kit β Translation
```kotlin
implementation("com.google.mlkit:translate:17.0.2")
val options = TranslatorOptions.Builder()
.setSourceLanguage(TranslateLanguage.KOREAN)
.setTargetLanguage(TranslateLanguage.ENGLISH)
.build()
val translator = Translation.getClient(options)
translator.downloadModelIfNeeded().await()
val translation = translator.translate("μλ
").await()
// β "Hello"
```
### ML Kit β Pose / Body
```kotlin
implementation("com.google.mlkit:pose-detection:18.0.0-beta3")
val options = PoseDetectorOptions.Builder()
.setDetectorMode(PoseDetectorOptions.STREAM_MODE)
.build()
val detector = PoseDetection.getClient(options)
val pose = detector.process(image).await()
val nose = pose.getPoseLandmark(PoseLandmark.NOSE)?.position
val leftWrist = pose.getPoseLandmark(PoseLandmark.LEFT_WRIST)?.position
```
β Fitness app.
### MediaPipe (advanced)
```kotlin
implementation("com.google.mediapipe:tasks-vision:0.10.20")
val options = ImageClassifier.ImageClassifierOptions.builder()
.setBaseOptions(BaseOptions.builder().setModelAssetPath("model.tflite").build())
.setMaxResults(5)
.build()
val classifier = ImageClassifier.createFromOptions(context, options)
val result = classifier.classify(MPImage.fromBitmap(bitmap))
for (cat in result.classifications()[0].categories()) {
println("${cat.categoryName()}: ${cat.score()}")
}
```
β Custom TFLite λͺ¨λΈ.
### Gemini Nano (on-device, Pixel 9+)
```kotlin
implementation("com.google.ai.edge.aicore:aicore:0.0.1-exp01")
val options = generationConfig {
context = context // Activity / Application
temperature = 0.2f
topK = 16
maxOutputTokens = 256
}
val generativeModel = GenerativeModel(generationConfig = options)
val response = generativeModel.generateContent("Summarize this article: ...").text
```
β Cloud νΈμΆ μμ΄. Privacy + offline + free.
β β οΈ Pixel 9 / μΌλΆ device λ§. Compatibility check.
### Health Connect setup
```kotlin
implementation("androidx.health.connect:connect-client:1.1.0-alpha07")
```
```xml
```
```kotlin
import androidx.health.connect.client.HealthConnectClient
import androidx.health.connect.client.records.StepsRecord
import androidx.health.connect.client.permission.HealthPermission
val healthConnectClient = HealthConnectClient.getOrCreate(context)
val permissions = setOf(
HealthPermission.getReadPermission(StepsRecord::class),
HealthPermission.getWritePermission(StepsRecord::class),
)
// Request
val launcher = registerForActivityResult(
PermissionController.createRequestPermissionResultContract()
) { granted -> /* ... */ }
launcher.launch(permissions)
```
### Read steps
```kotlin
val response = healthConnectClient.readRecords(
ReadRecordsRequest(
recordType = StepsRecord::class,
timeRangeFilter = TimeRangeFilter.between(
Instant.now().minusSeconds(3600 * 24),
Instant.now()
)
)
)
val totalSteps = response.records.sumOf { it.count }
```
### Write steps
```kotlin
healthConnectClient.insertRecords(listOf(
StepsRecord(
count = 5000,
startTime = Instant.now().minusSeconds(3600),
endTime = Instant.now(),
startZoneOffset = ZoneOffset.UTC,
endZoneOffset = ZoneOffset.UTC,
)
))
```
### Aggregation
```kotlin
val agg = healthConnectClient.aggregate(
AggregateRequest(
metrics = setOf(StepsRecord.COUNT_TOTAL),
timeRangeFilter = TimeRangeFilter.between(start, end)
)
)
val total = agg[StepsRecord.COUNT_TOTAL] ?: 0L
```
### Background sync
```kotlin
class HealthSyncWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {
override suspend fun doWork(): Result {
val steps = readSteps()
syncToServer(steps)
return Result.success()
}
}
// Schedule
val request = PeriodicWorkRequestBuilder(15, TimeUnit.MINUTES).build()
WorkManager.getInstance(ctx).enqueueUniquePeriodicWork("health-sync", KEEP, request)
```
### Privacy
```
- λͺ
μμ μ¬μ©μ consent
- Data minimization (read only what needed)
- μ¬μ©μ κ° access / delete κ°λ₯
- GDPR / HIPAA compliance (US)
```
### CameraX + ML Kit
```kotlin
val analyzer = ImageAnalysis.Analyzer { imageProxy ->
val mediaImage = imageProxy.image ?: return@Analyzer
val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
barcodeScanner.process(image)
.addOnSuccessListener { barcodes -> /* ... */ }
.addOnCompleteListener { imageProxy.close() }
}
```
β Real-time camera + ML.
### TFLite (custom λͺ¨λΈ)
```kotlin
implementation("org.tensorflow:tensorflow-lite:2.16.1")
val interpreter = Interpreter(loadModelFile())
val input = ByteBuffer.allocateDirect(...)
val output = ByteBuffer.allocateDirect(...)
interpreter.run(input, output)
```
β μ체 λͺ¨λΈ (TF / PyTorch β TFLite).
### Audio classification
```kotlin
implementation("com.google.mediapipe:tasks-audio:0.10.20")
val options = AudioClassifier.AudioClassifierOptions.builder()
.setBaseOptions(BaseOptions.builder().setModelAssetPath("yamnet.tflite").build())
.build()
val classifier = AudioClassifier.createFromOptions(context, options)
val result = classifier.classify(MPAudioData.create(buffer, sampleRate))
for (cat in result.classifications()[0].categories()) {
println("${cat.categoryName()}: ${cat.score()}")
// "Music", "Speech", "Bark", ...
}
```
### Subject segmentation (BG removal)
```kotlin
implementation("com.google.mlkit:subject-segmentation:16.0.0-beta1")
val segmenter = SubjectSegmentation.getClient()
val result = segmenter.process(image).await()
val foregroundBitmap = result.foregroundBitmap
// λ°°κ²½ X β μ¬μ©μ / μ¬λ λ§
```
### Document scanner
```kotlin
implementation("com.google.android.gms:play-services-mlkit-document-scanner:16.0.0-beta1")
val options = GmsDocumentScannerOptions.Builder()
.setGalleryImportAllowed(true)
.setPageLimit(5)
.setResultFormats(RESULT_FORMAT_PDF, RESULT_FORMAT_JPEG)
.build()
GmsDocumentScanning.getClient(options)
.getStartScanIntent(activity)
.addOnSuccessListener { intent -> startActivityForResult(intent, ...) }
```
β Document scan + auto crop + PDF.
### Smart reply (chat)
```kotlin
implementation("com.google.mlkit:smart-reply:17.0.4")
val smartReply = SmartReply.getClient()
val conversation = listOf(
TextMessage.createForRemoteUser("Hi", System.currentTimeMillis(), "user_1"),
)
val result = smartReply.suggestReplies(conversation).await()
for (suggestion in result.suggestions) {
println(suggestion.text)
// "Hi!", "Hello", "Hey there"
}
```
### Battery / performance
```
On-device ML = λΉ λ¦ + free + private.
But:
- Battery μ¬μ©
- Memory
- Model size (10-100 MB)
β μΈ‘μ + throttle.
```
### Cloud vs on-device
```
On-device:
+ Free (no API cost)
+ Private (no upload)
+ Offline
+ Low latency
- Limited model size
- Battery / memory
Cloud (Vertex AI / Gemini API):
+ Bigger / better model
+ Always updated
- Cost
- Privacy
- Latency
```
β μΌλ° task = on-device. Complex / accuracy critical = cloud.
### Edge AI (modern stack)
```
1. Quick task: ML Kit (built-in)
2. Custom: MediaPipe + TFLite
3. LLM: Gemini Nano (Pixel 9+)
4. Cloud fallback: Gemini API
```
## π€ μμ¬κ²°μ κΈ°μ€
| μμ
| μΆμ² |
|---|---|
| OCR / barcode / face | ML Kit |
| μ체 λͺ¨λΈ | MediaPipe / TFLite |
| On-device LLM | Gemini Nano (Pixel 9+) |
| Health data | Health Connect |
| μΌλ° LLM | Cloud (Gemini API) |
| Real-time | CameraX + ML Kit |
## β μν°ν¨ν΄
- **λͺ¨λ κ±° cloud LLM**: cost / privacy.
- **Health Connect κΆν ν λ² + λͺ¨λ κ±°**: minimum access.
- **PII model νμ΅ μΈλΆ send**: privacy violation.
- **Gemini Nano + λͺ¨λ device**: compatibility check.
- **Battery 무μ**: μ¬μ©μ λκΈ°.
- **λͺ¨λΈ download ν° + first launch**: progressive.
## π€ LLM νμ© ννΈ
- ML Kit = κ°μ₯ λ¨μ + λΉ λ₯Έ μμ.
- Health Connect = cross-app data.
- MediaPipe = custom + advanced.
- Gemini Nano = privacy-friendly LLM.
## π κ΄λ ¨ λ¬Έμ
- [[Android_CameraX_Patterns]]
- [[AI_Local_LLM_Inference]]
- [[Mobile_Push_Deep]]