Androidで歩数計アプリを作ってみた話

### はじめに

[ラクス Advent Calendar 2020](https://qiita.com/advent-calendar/2020/rakus)、14日目の記事です。
昨日の記事は@choreiさんの「[開発3年目のgitエイリアスを整理する](https://qiita.com/chorei/items/1ee6b8529b33c4cc4d11)」でした。

今回の記事は、Android向けの歩数計アプリを作ってみたというお話です。

### きっかけ
今年から配属先が変更になり、楽楽精算のスマートフォン向けアプリを開発しているチームへ異動しました。
以前からアプリ開発に興味があり、良い機会なのでアプリ開発ができるエンジニアとしてキャラ立ちしようと思っていたのですが、他案件との兼ね合いもあり、なかなかアプリ開発を行うことができていませんでした。

一方で、今年の健康診断で日々の運動不足について、注意を受けてしまったということがありました。前々から改善しなくてはと思っていたのですが、一度運動を始めてもなかなか継続できないという状態が続いていました。

一見何の関係もない2つの出来事ですが、とある発表が2つを結び付けました。Developers Boost 2020での[凡人エンジニアの生存戦略](https://speakerdeck.com/takaichi00/developers-boost-2020-fan-ren-enziniafalsesheng-cun-zhan-lue)という発表です。

その中で、「実施した習慣を記録してグラフ化する」ことで、退屈な作業でも継続できたということを聞き、

- 外に発信していけば、退屈な運動でも続けられるかもしれない
- 記録、発信用のスマホアプリを開発すれば、キャラ立ちの一歩目にできるかもしれない

ということに思い至りました。

### Let's アプリ開発
#### 要件、仕様について
アプリ開発にあたり、まずは簡単に要件をまとめるところから始めました。

- 要件①：行う運動はジョギング、ウォーキング等、運動した距離で計測できるものとした  
   運動した距離を基に消費したカロリーを計算できるため、スマートフォンでの計測、記録が容易であるためです。
- 要件②：外部への発信はTwitterへの投稿とした  
   自分がすでに使っているサービスで使い勝手が分かっており、かつ他の人の目につきやすいのではと考えた為です。

上記の二点から今回の要件は「**ジョギングした距離をTwitterで発信できるスマホアプリ**」と決めました。

次に、具体的な仕様をまとめました。

- 仕様①：運動した距離の記録を開始、停止できる機能  
  アプリで常に運動した距離を記録していると、運動中とそうでないものの区別がつかないため、計測中かそうでないかを区別できるようにしました。
- 仕様②：記録を停止した際に、運動量をツイートする機能  
  その日の運動量をツイートすることも考えましたが、今回はあまり時間がなかったので単純な機能にしました

また、私が持っているスマートフォンがAndroidなので、Android向けのアプリとして開発することにしました。

その他にも、消費カロリーや目標値までの運動量も記録、発信しようかと思いましたが、今回は最低限必要な機能[^1]に絞りました。

#### 実装してみた
##### プロジェクト構成

Android Studioから空のアクティビティを選択して構成しました。
ボタンを押すだけのアプリなので、１画面のみのアプリで十分であるためです。

具体的には以下の構成になりました。

```
app/src/main
│  AndroidManifest.xml
│
├─java
│  └─com
│      └─example
│          └─tweet_jogging_distance
│                  MainActivity.kt
│
├─res
│  ├─drawable
│  │      ic_launcher_background.xml
│  │
│  ├─drawable-v24
│  │      ic_launcher_foreground.xml
│  │
│  ├─layout
│  │      activity_main.xml
│  │
│  ├─mipmap-anydpi-v26
│  │      ic_launcher.xml
│  │      ic_launcher_round.xml
│  │
│  ├─mipmap-hdpi
│  │      ic_launcher.png
│  │      ic_launcher_round.png
│  │
│  ├─mipmap-mdpi
│  │      ic_launcher.png
│  │      ic_launcher_round.png
│  │
│  ├─mipmap-xhdpi
│  │      ic_launcher.png
│  │      ic_launcher_round.png
│  │
│  ├─mipmap-xxhdpi
│  │      ic_launcher.png
│  │      ic_launcher_round.png
│  │
│  ├─mipmap-xxxhdpi
│  │      ic_launcher.png
│  │      ic_launcher_round.png
│  │
│  ├─values
│  │      colors.xml
│  │      strings.xml
│  │      themes.xml
│  │
│  └─values-night
│          themes.xml
│
└─resources
        twitter4j.properties
```
##### 実装①：運動した距離の記録を開始、停止できる機能

今回はAndroidの[**Sensor**](https://developer.android.com/reference/android/hardware/Sensor?hl=ja)を使って計測した歩数を、運動した距離の記録としました。  

運動したデータは[Google Fit API](https://developers.google.com/fit/android)で取得できるようなのですが、何度やっても運動した距離がうまく取れなかった[^2]ためです。

処理の流れは以下の通りです。

1. `Sensor.TYPE_STEP_DETECTOR`が反応した際に、カウント用の変数をインクリメントする
2. ボタンを押した際、カウント用の変数の値をスタート時の歩数用の変数に格納する
3. もう一度ボタンを押すと、再度カウント用の変数の値を取得する
4. 2.の値から3.の値を引いたものがStartからFinishの間での歩数なので、それを画面に表示する

実際のコードは以下のようになりました。
実装内容は[AndroidのTYPE_STEP_COUNTER/TYPE_STEP_DETECTORセンサーの使い方](https://www.hiroom2.com/2016/02/18/android%E3%81%AEtype-step-counter-type-step-detector%E3%82%BB%E3%83%B3%E3%82%B5%E3%83%BC%E3%81%AE%E4%BD%BF%E3%81%84%E6%96%B9/)を参考にしています。

```MainActivity.kt
class MainActivity : AppCompatActivity() , CoroutineScope, SensorEventListener {
    private lateinit var mManager: SensorManager
    private lateinit var mSensor: Sensor

    private var isJogging = false
    private var steps = 0
    private var startSteps = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        mManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
        mSensor = mManager.getDefaultSensor(Sensor.TYPE_STEP_DETECTOR)

        val textView = findViewById<TextView>(R.id.textView)
        val button = findViewById<Button>(R.id.button)
        button.setOnClickListener {
            if (isJogging) {
                finishJogging(it as Button, textView)
            } else {
                startJogging(it as Button, textView)
            }
            isJogging = !isJogging
        }
    }

    private fun startJogging(button: Button, textView:TextView) {
        textView.text =""
        startSteps = this.steps
        button.text = resources.getText(R.string.finish)
    }

    private fun finishJogging(button: Button, textView:TextView) {
        button.text = resources.getText(R.string.now_sending)

        launch {
            val message = "${steps - startSteps}歩、歩きました！"
            val df = SimpleDateFormat("yyyy年MM月dd日HH持mm分", Locale.JAPAN)
            // ツイートする機能を入れる場所
            textView.text = message
            button.text = resources.getText(R.string.start)
        }
    }

    override fun onPause() {
        super.onPause()
        mManager.unregisterListener(this)
    }

    override fun onResume() {
        super.onResume()
        mManager.registerListener(this, mSensor, SensorManager.SENSOR_DELAY_NORMAL)
    }

    override fun onDestroy() {
        job.cancel()
        super.onDestroy()
    }

    override fun onSensorChanged(event: SensorEvent?) {
        steps++
    }

    override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
    }
}
```

初期表示ではボタンのラベルが「Start」となっており、ボタンを押下するとラベルが「Finish」になります。
もう一度押下するとラベル「Now Sending」になった後、運動した歩数の表示を行い、ラベルは「Start」に戻ります。

画面遷移のデモは以下の通りです。
※デモなので端末を振って歩数を稼いでいます

<blockquote class="twitter-tweet" data-lang="ja" data-theme="light"><p lang="ja" dir="ltr">歩数計アプリのデモ <a href="https://t.co/0MjXYXhMfH">pic.twitter.com/0MjXYXhMfH</a></p>&mdash; FM_Kz (@FMKz8) <a href="https://twitter.com/FMKz8/status/1338186438735585280?ref_src=twsrc%5Etfw">2020年12月13日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

##### 実装②：記録を停止した際に、運動量をツイートする機能  

[Twitter4j](http://twitter4j.org/ja/index.html)というライブラリを利用して実装しました。  
本来はツイート用のトークンを取得すべきなのですが、今回は割愛してTwitter APIで発行できるトークンをそのまま利用しました。

実装方法は[【初心者向け】Android×KotlinでTwitter4Jを使ってみる](https://qiita.com/yuta-tsussy/items/e6ff450667fcaeffa2ed)を参考にしています。

``` app/build.gradle
dependencies {
    ...
    implementation 'org.twitter4j:twitter4j-core:4.0.7'
    // 通信などの別スレッドで動かすタスクをCoroutinesで行う
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2'
    ...
}
```

``` AndroidManifest.xml
    ...
    package="com.example.tweet_jogging_distance">

    // アプリにインターネットの利用許可を与える
    <uses-permission android:name="android.permission.INTERNET"/>

    <application
    ...
```
```resources/twitter4j.properties
debug=true
oauth.consumerKey=xxxxx
oauth.consumerSecret=xxxxx
oauth.accessToken=xxxxx
oauth.accessTokenSecret=xxxxx
```

先程のMainActivity.ktで`// ツイートする機能を入れる場所`としていた場所を実装します。
Coroutinesを利用してツイートが完了するまで、ボタン押下後の処理を待つようにしています。

また、Twitter API経由だと同一の内容のツイートは連投できない[^2]ため、投稿日時をツイートに含めて内容が重複しないようにしています。

```MainActivity.kt
    private fun finishJogging(button: Button, textView:TextView) {
        button.text = resources.getText(R.string.now_sending)

        launch {
            val message = "${steps - startSteps}歩、歩きました！"
            val df = SimpleDateFormat("yyyy年MM月dd日HH持mm分", Locale.JAPAN)
            tweetJoggingDistance("$message:${df.format(Date())}")
            textView.text = message
            button.text = resources.getText(R.string.start)
        }
    }

    private suspend fun tweetJoggingDistance(message: String) = coroutineScope {
        async(context = Dispatchers.IO)  {
            val twitter = TwitterFactory().instance
            return@async twitter.updateStatus(message)
        }.await()
    }
```

「Finish」ボタンを押すと、以下のようにツイートされます。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">15歩、歩きました！:2020年12月14日01持17分</p>&mdash; FM_Kz (@FMKz8) <a href="https://twitter.com/FMKz8/status/1338156213968666627?ref_src=twsrc%5Etfw">December 13, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

### 感想

画面が少なく、ツイート投稿や別スレッドでの処理などはライブラリに任せているので、手軽に実装できました。
（Twitter APIの申請やGoogle Fit APIの利用について調べていた時間の方が長かったです）

特にCoroutinesは業務で見たことはあったのですが、どういったものなのか全く知らなかったので、この機会に触ってみて学習できたのは良かったと思います。

今回作成したアプリで運動を継続し、生活習慣を改善できればと思います。

[^1]: 走行距離 ≒ 消費カロリーらしいので、運動した距離を記録、発信すれば十分だろうと考えました
      参考：http://www.blue-mag.com/diary/bsc008/
[^2]: データは取得できるが、運動した距離や歩数が0で取れてしまう
[^3]: `Any attempt that would result in duplication will be blocked, resulting in a 403 error. `
      参考：https://developer.twitter.com/en/docs/twitter-api/v1/tweets/post-and-engage/api-reference/post-statuses-update