このページを正しく表示するにはJavascriptを有効にしてください。
バックグラウンドでも動くタイマーを作る
今作ろうとしているAndroidアプリで、
バックグラウンドでも動き続けるタイマー
が必要になったので試行錯誤の上実装してみました。
## タイマー要件
* スタート時点からの経過時間を表示
* バックグラウンドでも動く
* タスクキルされない
* アプリがアクティブ(表示されている)時は1秒毎に時間表示
* なるべく省電力
要件としては割とシンプルですが、実装しようとすると結構複雑です。
とりわけタスクキルされないことと、1秒毎に画面表示更新を
組み込むのは結構工夫がいりました。
## 実現方法
タイマーの実現方法としてAndroidのサービスを活用することにしました。
サービスであればバックエンドでも動き続けることが可能です。
ただアプリがフロントで動いている時は画面表示も更新しなくてはいけないので
サービスと上手く連携させる必要があります。
### 計測サービス
スタート時の時間(System.currentTimeMillis)と現在の時間との差分を取得する
サービスを実装し、アプリ側で計測開始と同時にStartServiceするようにしました。
これでバックグラウンドでも計測を続けるという要件が満たせます。
ただデフォルトではサービスであってもタスクキルされてしまうので、
通知バーに表示するNotificationを作成し
startForeground(NOTIFICATION_ID, notification);
で計測時は常駐するよう設定してあげます。
こうすることで余程のことがない限り生き続けることが出来ます。
### サービスバインド
次にこのサービスとの通信部分を作る必要があります。
StartServiceでサービスを始めた場合、そのままでは
起動したサービスと呼び出した側では通信が出来ません。
そこで呼び出し側とBindする処理を追加します。
具体的には
* 計測開始時 バインドする
* 計測終了時 アンバインドする
* 計測中アプリ終了 アンバインドする
* 計測中アプリ起動時 再バインドする
と言った形で適切にバインド処理をします。
:::java
@Override
public void onResume() {
super.onResume();
bindTImerService();
myIconLoader.startLoading();
}
@Override
public void onPause() {
unbindTimerService();
super.onPause();
}
private void bindTImerService() {
bindService(timerServiceIntent, timerServiceConnection, Context.BIND_NOT_FOREGROUND);
}
private void unbindTImerService() {
try {
unbindService(timerServiceConnection);
} catch (IllegalArgumentException e) {
}
}
ライフサイクルをちゃんと意識してこの辺を書かないと
NullPointerExceptionが頻発するので注意です。
アプリ起動時にサービスが起動中かどうかは、簡単には取れそうになかったのですが、
バインド時のフラグを```BIND_NOT_FOREGROUND```としたら、サービス起動中のみ
バインドするようになりました。
```BIND_AUTO_CREATE```のように勝手にサービスが立ち上がってしまう事はありません。
バインド後はServiceのインスタンスにアクセスすることが出来るようになります。
### 1秒毎表示更新する
時刻表示をほぼリアルタイム(毎秒)でしたいという要件もちょっと厄介です。
ご存知の通り、UI Threadで時計を更新し続けるというのは
作法的にも効率的にも好ましくないです。
今回は1秒に1回更新できればいいので、
AsyncTaskを使用してTextViewの内容を更新することにしました。
protected Long doInBackground(Void... void) {
while(mTimer.is_running()) {
try {
publishProgress(mTimer.getTimeMs());
Thread.sleep(100);
if (isCancelled()) {
break;
}
} catch (InterruptedException e) {
}
}
return mTimer.getTimeMs();
}
0.1秒毎スリープして、現在時刻をUIThreadに通知するようにしています。
1秒ではなく0.1秒なのは更新タイミングの都合等でずれが発生するのを抑えるためです。
あとループ内でAsyncがキャンセルされていないかもチェックしています。
アプリがResumeした時にAsyncも一緒に止めてあげないと
再Bind時におかしなことになってしまうのを防ぐためです。
## 最速のカウントアップタイマー
[https://play.google.com/store/apps/details?id=com.appspot.pistatium.countup](https://play.google.com/store/apps/details?id=com.appspot.pistatium.countup)
実際にアプリとして動かしてみるとこんな感じになります。
作りたいアプリは別なのですが、このタイマー部分だけでも
利用価値がありそうなので切り出してみました。