2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Androidで学ぶ:callbackFlowとRoomでセンサー値を完全記録&リアルタイム表示

Posted at

📖 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を利用

💻 ソースコード全文

// 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を渡す
    • SensorManagerRoom.databaseBuilderに利用
  • CoroutineScopeの渡し方

    • ServiceではCoroutineScope(SupervisorJob() + Dispatchers.Default)を生成
    • ViewModelではviewModelScopeを利用
    • scope.launch(Dispatchers.IO)でバックグラウンド処理を安全に開始
  • 完全記録とリアルタイム表示の両立

    • DB保存で全イベントを残しつつ、EventBusでUIに即時通知
    • UIは最新値と履歴を同時に表示可能

✅ まとめ

この記事では、

  • callbackFlow を使ったセンサー値の完全記録
  • Room データベースへの保存
  • EventBus を使ったリアルタイム表示
  • Context と CoroutineScope の渡し方
    を学びました。

この設計は教育用デモにも研究用途にも応用でき、「完全記録」と「リアルタイム表示」を両立するアーキテクチャの良い例になります。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?