11 KiB
11 KiB
id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
| id | title | category | status | source_trust_level | verification_status | created_at | updated_at | tags | tech_stack | applied_in | aliases | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| android-ml-kit-health | Android ML Kit / Health Connect / On-device AI | Coding | draft | B | conceptual | 2026-05-09 | 2026-05-09 |
|
|
|
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
implementation("com.google.mlkit:text-recognition:16.0.0")
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
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
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
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
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)
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+)
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
implementation("androidx.health.connect:connect-client:1.1.0-alpha07")
<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.health.READ_STEPS" />
<uses-permission android:name="android.permission.health.WRITE_STEPS" />
<uses-permission android:name="android.permission.health.READ_HEART_RATE" />
<queries>
<package android:name="com.google.android.apps.healthdata" />
</queries>
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
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
healthConnectClient.insertRecords(listOf(
StepsRecord(
count = 5000,
startTime = Instant.now().minusSeconds(3600),
endTime = Instant.now(),
startZoneOffset = ZoneOffset.UTC,
endZoneOffset = ZoneOffset.UTC,
)
))
Aggregation
val agg = healthConnectClient.aggregate(
AggregateRequest(
metrics = setOf(StepsRecord.COUNT_TOTAL),
timeRangeFilter = TimeRangeFilter.between(start, end)
)
)
val total = agg[StepsRecord.COUNT_TOTAL] ?: 0L
Background sync
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<HealthSyncWorker>(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
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 모델)
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
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)
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
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)
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.