経緯

Androidアプリ開発の仕事で「1日1回だけ実行できる」という処理を作る必要が出てきました。
スマホゲームの「デイリーイベント」みたいなヤツで、1回実行するとある特定の時刻を過ぎるまで再実行できない、という仕様です。

よくある仕様なので「誰かが作って公開してるでしょ」と思って軽い気持ちで検索したんですが、

  • デイリーイベント
  • 1日1回

みたいなワードを「開発」と合わせて検索しても、ほとんど情報が出てこず。。。

仕方ない、じゃあ作るか!と軽い気持ちで考え始めました。
そうすると、これは結構面倒だということに気づきまして。。。

  • デイリーの “しきい” となる時刻が 0 時 0 分なのであれば、『日付が変わったかどうか』を検出すればいいだけなのでカンタン
  • しかし、”しきい” となる時刻を自由に設定できるようにする場合、日付をデイリーの変わり目と考えることができない

上司に相談したら「あ、0時切り替えでいいよー」という話になったので、仕事上で制作していたアプリでは日付の変化を見るだけで済みました。

が!
もし 「”しきい” となる時刻を変えられるようにしたい」という場合どうしたら良いんでしょう?
そこがどうしても気になったので、個人的に制作してみました。

作ったもの

以下のような機能を持つ DailyEventController クラスを作りました。

  • しきいとなる時・分を保持する thresholdHour, thresholdMinute (Int 型) を持つ。クラスの初期化時に引数として指定・取得する。
  • 「前回実行した日時 executeTime (Calendar 型)」をフィールドとして持つ
  • イベントの実行タイミングを記録するメソッド execute メソッドを持つ。実行するとその時間を executeTime に保存する
  • デイリー範囲内で実行済みかどうかを返す isDoneDaily メソッドを持つ。

環境

Android Studio 3.3.2
Kotlin 1.3.21

使い方

詳しい使い方は github の README に記載したのでそちらを参照してください。

https://github.com/Idenon/DailyEvent

カンタンに書くと、

  • 「1日1回」を判定したいイベント1つにつき、DailyEventController クラスのインスタンスを1つ生成する。この時、デイリーの境目となる時・分を渡す
    • 例: val event = DailyEventController(5, 0)
  • そのイベントを実行した時、このクラスの execute() メソッドを呼ぶ。実行時間が executeTime に保存される
    • 例: event.execute()
  • 「そのイベントがデイリー内で実行済みかどうか?」を知りたい時は isDoneDaily() メソッドを呼ぶ。実行済みなら true, 未実行なら false が返る。
    • 例: if (event.isDoneDaily()) { 【実行済みの場合の処理】 }

という感じです。

考えたこと

問題点はなにか

考えるべき問題は、以下2種類の時間データの位置関係をどうやって把握するかです。
(正確には「現在時刻」もありますが、これは必ず一番新しい時刻になるはずなので、問題は以下の2種です)

① 前回にイベントを実行した時間
② しきい値となる時間

この2つを比較して、
①<②なら「しきい値をまたいでいる」=「デイリーではまだ実行されていない」
②<①なら「しきい値をまたいでいない」=「デイリーで実行済みである」
とそれぞれ判断できるわけです。

①に関しては execute メソッドを実行時の時刻を取得して executeTime に保存しておけば良いだけですが、問題は ②です。
これはユーザから取得した情報として、時と分を整数型で持っていますが、実際に比較しなければならないのは
「その時と分を持つ、現在時刻から見て『直近であり』『過去である』時刻」
となります。

つまり、時・分という整数から、上記のような時刻データを生成する必要がありました。

「直近であり」「過去である」しきい日時を計算する

そこでプライベートメソッドとして calculateThresholdDate メソッドを作成し、現在時刻・しきい値の時・しきい値の分から、直近の過去のしきい日時のデータを計算するようにしました。

やり方はわりと単純で、

  • 現在時刻の時・分を、しきい値の時・分に入れ替える
  • しきい日時が現在時刻より後の日時になってしまったら、1日戻す

だけです。
設定したい時・分は数値として分かっているので、現在時刻の時・分をとりあえずそれに差し替えます。
ただその時刻が現在時刻より後だった場合、未来のしきい日時が取得できてしまうので、その場合だけは1日戻すということです。
時刻データをDate型でなくCalendar型で持つようにしたのは、ここで時・分だけの差し替えをしたかったからです。
Calendar.set(Calendar.HOUR, 【差し替えたい値】) みたいな感じで差し替えできる)
after, before メソッドを使って、どちらが後でどちらが先かもカンタンに判別できますね。

しきい日時と過去のイベント実行時間の前後関係を調べる

ここまで来たら後はカンタンです。

先に挙げた、
① 前回にイベントを実行した時間
② しきい値となる時間
がCalendar型の時刻データとして手に入ったので、これを比較してどちらが先でどちらが後か調べるだけです。
先述の通り、Calendar型の比較にはafterメソッド、beforeメソッドを使いましょう。
今回は executeTime.after(thresholdTime) として、これが true だったら未実行、false だったら実行済みと判定するようにしました。

完成したコード

こちらのコードです。
使いたい人はコピペして使ってください。
(反響が大きいようであれば gradle による配布を検討します)

https://github.com/Idenon/DailyEvent/blob/develop/app/src/main/java/jp/idenon/dailyevent/DailyEventController.kt

大変だったこと

この開発で最も大変だったのは、「しきい日時をまたいだかどうか」を判定する手法を考えることでした。
当初、「0時0分をしきい日時とする仕様を、しきい日時を変更できるようにするにはどうすればいいか?」というのが出発点だったため、どうしても「日や時・分を個別に比較してどうにかする」という発想が頭にこびりついていて、そこから抜け出せない状態でした。
そうではなく、「時刻データといてしきい日時を計算して、それを前回実行日時の時刻データと比較する」というやり方で実際にコードを書いてみたところ、結果として当初の想像よりも単純なコードになりました。

最初の想像の時点でこの形を思いついていればカンタンな話なのでしょうが、私の経験量が足りず、コードを書いてみて初めてこの形に辿り着きました。
まだまだ修行が必要なようです。

このコードができないこと

  • 初期化時に設定したしきい日時をあとから変更する
  • なんらかの方法で executeTime や thresholdTime に未来のデータを格納されてしまった時の対処
  • その他、私が想像できていない何か

参考資料

github
https://github.com/Idenon/DailyEvent

元記事はこちら

デイリーイベントを作ろう 〜「1日1回かどうか」を判定するクラスを作りました〜