こんにちは。 開発部第一グループの若松です。
Android アプリで歩数計を開発する機会があったので実装してみました。
歩数を取得する実装は SensorManagaer、ヘルスコネクト、Recording API などいくつかありますが、今回は SensorManagaer を使って、歩数データを取得して JetpackCompose で表示する形で実装します。 リアルタイムで歩数を計測しやすいため、SensorManagaer を採用しました。
AndroidManifest に権限を付与
早速実装について解説していきます。
まずは AndroidManifest ファイルに以下権限を要求する内容を追加します。
<manifest> […] <!-- 歩数計を使用するためのパーミッション --> <uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" /> </manifest>
ACTIVITY_RECOGNITION はユーザーが行動を開始や終了をした時に検知される仕組みのためのパーミッションです。歩数計測の開始/終了で使用します。
権限付与の確認
アプリに対して、上記権限を付与しているかの確認をします。
センサー機能を使用して歩数をカウントするため、その権限をチェックするようにしました。
ソースコードは以下の通りです。
/** * アプリの身体活動の権限付与チェック */ @Composable fun PermissionCheckOnLaunch() { val context = LocalContext.current var showDialog by remember { mutableStateOf(false) } // 端末の設定画面へ遷移 fun goToAppSettings() { val intent = Intent( Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", context.packageName, null) ) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) context.startActivity(intent) } // 権限チェック val granted = ContextCompat.checkSelfPermission( context, Manifest.permission.ACTIVITY_RECOGNITION ) == PackageManager.PERMISSION_GRANTED if (!granted) { showDialog = true } // ダイアログ表示 if (showDialog) { AlertDialog( onDismissRequest = { showDialog = false }, confirmButton = { Button(onClick = { goToAppSettings() showDialog = false }) { Text("権限付与へ") } }, title = { Text("歩数センサーの利用には権限が必要です") }, text = { Text("設定画面から「身体活動」へのアクセスを許可してください。") } ) } }
権限を付与していない状態だとダイアログを表示し続けるようにしました。
権限を付与するとダイアログは消えます。
歩数カウント処理
MainActivity
MainActivity.kt は以下になります。アプリ全体の構成とセンサーマネージャーの取得、センサーの監視をします。
class MainActivity : ComponentActivity(), SensorEventListener { private val viewModel by viewModels<StepCounterViewModel>() // センサーマネージャー lateinit var sensorManager: SensorManager // 歩数をカウントするセンサー var stepSensor: Sensor? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // センサーマネージャーを初期化 sensorManager = getSystemService(SENSOR_SERVICE) as SensorManager stepSensor = sensorManager.getDefaultSensor(Sensor.TYPE_STEP_DETECTOR) setContent { MaterialTheme { PermissionCheckOnLaunch() StepCounterScreen( viewModel, onClickStart = { // 歩数計測を開始 if (stepSensor != null && viewModel.state != CounterState.RECORDING) { sensorManager.registerListener( this, stepSensor, SensorManager.SENSOR_DELAY_UI ) viewModel.updateState(CounterState.RECORDING) } }, onClickPause = { // 歩数計測の一時中断 viewModel.updateState(CounterState.PAUSE) }, onClickStop = { // 歩数計測を終了 sensorManager.unregisterListener(this, stepSensor) viewModel.resetSteps() viewModel.updateState(CounterState.NONE) } ) } } } override fun onDestroy() { super.onDestroy() sensorManager.unregisterListener(this, stepSensor) viewModel.updateState(CounterState.NONE) } override fun onSensorChanged(event: SensorEvent?) { if (event?.sensor?.type == Sensor.TYPE_STEP_DETECTOR) { viewModel.incrementSteps() } } // 登録されたセンサーの精度が変更されたときに呼び出されます override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { // 今回は何もしない } }
画面:: StepCounterScreen
StepCounterScreen.kt は以下になります。開始、中断、終了のボタンを並べたシンプルなものになります。
/** * 歩数計の実際の画面 */ @Composable fun StepCounterScreen( viewModel: StepCounterViewModel, onClickStart: () -> Unit, onClickPause: () -> Unit, onClickStop: () -> Unit, ) { Column( modifier = Modifier .fillMaxSize() .padding(24.dp), verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically), horizontalAlignment = Alignment.CenterHorizontally ) { Text(text = "状態: ${viewModel.state.text}", fontSize = 28.sp, fontWeight = FontWeight.Bold) Text(text = "歩数: ${viewModel.steps}", fontSize = 28.sp, fontWeight = FontWeight.Bold) Button(onClick = { onClickStart() }, modifier = Modifier.fillMaxWidth()) { Text("開始") } Button(onClick = { onClickPause() }, modifier = Modifier.fillMaxWidth()) { Text("中断") } Button(onClick = { onClickStop() }, modifier = Modifier.fillMaxWidth()) { Text("終了") } } }
ViewModel:: StepCounterViewModel
StepCounterViewModel.kt は 歩数と計測状態を持っているステートホルダーとして実装しました。
/** * 歩数計画面のViewModel */ class StepCounterViewModel() : ViewModel() { // 実際の歩数 var steps by mutableIntStateOf(0) private set // 歩数計測の状態管理 var state by mutableStateOf(CounterState.NONE) private set // 計測状態を更新 fun updateState(newState: CounterState) { state = newState } // 歩数を追加 fun incrementSteps() { if (state == CounterState.RECORDING) { steps += 1 } } // 歩数をリセット fun resetSteps() { steps = 0 } }
実際に歩いてみた
以下は挙動のキャプチャになります。 実際に歩いてみて歩数がカウントされていることを確認できました。
端末の縦・横移動から歩数を検知してカウントしています。
実装してみての感想
端末内の機能を使ってのアプリなのでハードルが高いのかなと思っていましたが、意外と簡単に実装できました。
今回は単純に歩数をカウントするだけの内容でしたが、期間指定や歩いた距離に応じてなど様々な場面、やりたいことに合わせてアプリを実装できると思います。
今回はサービス起動中でしか歩数カウントできませんが、継続してバックグラウンド実行するのであれば、フォアグラウンドサービスかワークマネージャーを使用する必要があります。
関連記事: Google Fitと連携して歩数を取得する - アイリッジ開発者ブログ
※Google Fitは 2026年非推奨になるのでご注意ください