📖 Qiita記事全体
この記事をまとめるために、Copilotを利用しました。
タイトル
Androidで学ぶ:callbackFlowとRoomでセンサー値を完全記録&リアルタイム表示
はじめに
Androidでセンサー値を扱うとき、
「リアルタイム表示したい」「データベースに保存して分析したい」
というニーズはよくあります。
しかし、センサーイベントは高頻度で発生するため、
- UIが過負荷になる
- データ保存が追いつかない
といった問題が起こりがちです。
この記事では、Kotlin Flow の callbackFlow を使ってセンサー値を完全記録しつつ、Room データベースに保存し、さらに EventBus を使ってリアルタイム表示を両立する設計を紹介します。
📚 この記事で学べること
- Kotlin Coroutines Flow の基礎
-
callbackFlowを使ったセンサーイベントの非同期ストリーム化 -
buffer(Channel.UNLIMITED)によるセンサー値の完全記録 - SensorRepository と DatabaseRepository の責務分離
- Room データベースとの連携方法
- EventBus を使ったリアルタイム表示の仕組み
- Context の渡し方(ServiceやUIからRepositoryへ)
- CoroutineScope の渡し方(Serviceでは
serviceScope、ViewModelではviewModelScope)
🏗 設計ポイント
-
責務分離
- SensorRepository → センサー値取得
- DatabaseRepository → DB保存・履歴取得
- Coordinator → 加工・保存・通知
-
完全記録
-
callbackFlow+buffer(Channel.UNLIMITED)で全イベントを順次処理
-
-
リアルタイム表示
- EventBusでUIに即時通知
-
ContextとCoroutineScopeの扱い
- Repository生成時に
Contextを渡す - Serviceでは独自
CoroutineScope、ViewModelではviewModelScopeを利用
- Repository生成時に
💻 ソースコード全文
// SensorData.kt
@Entity(tableName = "sensor_data")
data class SensorData(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val x: Float,
val y: Float,
val z: Float,
val timestamp: Long
)
// SensorDao.kt
@Dao
interface SensorDao {
@Insert
suspend fun insert(data: SensorData)
@Query("SELECT * FROM sensor_data ORDER BY timestamp DESC")
fun getAll(): Flow<List<SensorData>>
}
// AppDatabase.kt
@Database(entities = [SensorData::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun sensorDao(): SensorDao
companion object {
fun create(context: Context): AppDatabase =
Room.databaseBuilder(context, AppDatabase::class.java, "sensor.db").build()
}
}
// SensorEventBus.kt
object SensorEventBus {
private val _events = MutableSharedFlow<SensorData>()
val events: SharedFlow<SensorData> = _events
suspend fun post(data: SensorData) {
_events.emit(data)
}
}
// SensorDataSource.kt
class SensorDataSource(context: Context) {
private val sensorManager =
context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
private val accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
fun observeAccelerometer(): Flow<SensorData> = callbackFlow {
val listener = object : SensorEventListener {
override fun onSensorChanged(event: SensorEvent) {
trySend(
SensorData(
x = event.values[0],
y = event.values[1],
z = event.values[2],
timestamp = System.currentTimeMillis()
)
)
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
}
sensorManager.registerListener(listener, accelerometer, SensorManager.SENSOR_DELAY_GAME)
awaitClose { sensorManager.unregisterListener(listener) }
}.buffer(Channel.UNLIMITED) // 全イベントを完全記録
}
// SensorRepository.kt
class SensorRepository(context: Context) {
private val dataSource = SensorDataSource(context)
fun sensorFlow(): Flow<SensorData> = dataSource.observeAccelerometer()
}
// DatabaseRepository.kt
class DatabaseRepository(context: Context) {
private val db = AppDatabase.create(context)
private val dao = db.sensorDao()
suspend fun save(data: SensorData) = dao.insert(data)
fun history(): Flow<List<SensorData>> = dao.getAll()
}
// SensorCoordinator.kt
class SensorCoordinator(
private val sensorRepo: SensorRepository,
private val dbRepo: DatabaseRepository
) {
fun start(scope: CoroutineScope) {
scope.launch(Dispatchers.IO) {
sensorRepo.sensorFlow().collect { raw ->
val processed = raw.copy(
x = raw.x.coerceIn(-20f, 20f),
y = raw.y.coerceIn(-20f, 20f),
z = raw.z.coerceIn(-20f, 20f)
)
dbRepo.save(processed) // DB保存
SensorEventBus.post(processed) // リアルタイム通知
}
}
}
fun history() = dbRepo.history()
}
// SensorService.kt
class SensorService : Service() {
private lateinit var coordinator: SensorCoordinator
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
override fun onCreate() {
super.onCreate()
val sensorRepo = SensorRepository(this)
val dbRepo = DatabaseRepository(this)
coordinator = SensorCoordinator(sensorRepo, dbRepo)
coordinator.start(serviceScope)
}
override fun onBind(intent: Intent?): IBinder? = null
}
// SensorViewModel.kt
class SensorViewModel(private val coordinator: SensorCoordinator) : ViewModel() {
init { coordinator.start(viewModelScope) }
val sensorEvents = SensorEventBus.events
val history = coordinator.history()
}
// SensorScreen.kt (Jetpack Compose)
@Composable
fun SensorScreen(viewModel: SensorViewModel) {
val latest by viewModel.sensorEvents.collectAsState(initial = null)
val history by viewModel.history.collectAsState(initial = emptyList())
Column {
Text("最新の加速度値:")
latest?.let { Text("x=${it.x}, y=${it.y}, z=${it.z}") }
Spacer(modifier = Modifier.height(16.dp))
Text("履歴:")
history.forEach { Text("${it.timestamp}: x=${it.x}, y=${it.y}, z=${it.z}") }
}
}
🔎 解説
-
Contextの渡し方
- ServiceやUIからRepositoryを生成するときに
Contextを渡す -
SensorManagerやRoom.databaseBuilderに利用
- ServiceやUIからRepositoryを生成するときに
-
CoroutineScopeの渡し方
- Serviceでは
CoroutineScope(SupervisorJob() + Dispatchers.Default)を生成 - ViewModelでは
viewModelScopeを利用 -
scope.launch(Dispatchers.IO)でバックグラウンド処理を安全に開始
- Serviceでは
-
完全記録とリアルタイム表示の両立
- DB保存で全イベントを残しつつ、EventBusでUIに即時通知
- UIは最新値と履歴を同時に表示可能
✅ まとめ
この記事では、
-
callbackFlowを使ったセンサー値の完全記録 - Room データベースへの保存
- EventBus を使ったリアルタイム表示
- Context と CoroutineScope の渡し方
を学びました。
この設計は教育用デモにも研究用途にも応用でき、「完全記録」と「リアルタイム表示」を両立するアーキテクチャの良い例になります。