Android
Kotlin

Androidアプリを強制アップデート機能に対応したときの話


背景


リニューアルに向けて強制アップデートの仕組みを入れておきたい

よくあることですね。

要件は以下になります。


  • 強制アップデート要求バージョンを満たさない場合はアラートを出す。

  • バックグラウンド(タスクキルされている状態も含む)→フォアグラウンドをアラート表示のトリガーとする。

  • 一旦フォアグラウンドになった場合は以降アラートを出さない。


起動とかForegroundの検知ってどうするんだっけ?

iOSだとアプリケーションクラスにそもそもライフサイクルメソッドがあるのでいいんですが、Androidにはそれがない。

あんまりピンポイントでやってくれてる記事も見当たらなかったので作ってみました。


ProcessLifecycleOwnerを使う

Android Architecture ComponentsにProcessLifecycleOwnerという、アプリケーションの起動と終了イベントを通知してくれるクラスがいるのでこいつを使います。


実装


  • Applicationクラスを継承して以下を実装する


MyApplication.kt

override fun onCreate() {

super.onCreate()

ProcessLifecycleOwner.get().lifecycle.addObserver(this)
}

@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun onForegrounded() {
Log.d(TAG, "onStart")
// 強制アップデートのチェック処理
ForceUpdate.instance().check()
}

@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun onResumed() {
Log.d("TAG", "onResume")
}

@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
fun onPaused() {
Log.d("TAG", "onPause")
}

@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun onStop() {
Log.d("TAG", "onStop")
}



しかし、これでは不十分


アラートを表示するにはActivityが必要

アプリがforegroundになったからといって必ずActivityが表示されているわけではありません。


Activity無しでshowするとクラッシュします。


root Activityでは不十分

Activityで他全部フラグメントならいいかもしれませんが、ルート以外のActivity表示されてたら検知できません。これではまりました。


Application.ActivityLifecycleCallbacksでActivityの生成を検知する

Activityの生成を監視して、1つ目のActivityがスタートされたタイミングを検知します。

そのActivityを使ってアラートを表示させます。


実装


  • ActivityLifecycleCallbacksを継承したActivityLifecycleHandlerを追加


ActivityLifecycleHandler.kt

class ActivityLifecycleHandler : Application.ActivityLifecycleCallbacks {

private val activityStack: MutableList<Activity> = mutableListOf()
val topActivityOnStartedEvent: BehaviorProcessor<Activity> = BehaviorProcessor.create()

override fun onActivityPaused(activity: Activity?) {
}

override fun onActivityResumed(activity: Activity?) {
}

override fun onActivityStarted(activity: Activity?) {
activity?.let {
val previousCnt = activityStack.count()
activityStack.add(it)

// Activityのスタックが0→1になったときのみ通知する
if (previousCnt == 0) {
topActivityOnStartedEvent.onNext(it)
}
}
}

override fun onActivityDestroyed(activity: Activity?) {
}

override fun onActivitySaveInstanceState(activity: Activity?, outState: Bundle?) {
}

override fun onActivityStopped(activity: Activity?) {
activity?.let {
activityStack.remove(it)
}
}

override fun onActivityCreated(activity: Activity?, savedInstanceState: Bundle?) {
}
}


アプリがforegroundにいる間は必ず1つ以上のActivityがスタートされているので、Activityカウントが0→1になるのは起動時以外無いはず。 (もう少しスマートなやり方はありそう)


  • Applicationクラスに以下を追加


MyApplication.kt

override fun onCreate() {

super.onCreate()

ProcessLifecycleOwner.get().lifecycle.addObserver(this)

// 以下を追加
lifecycleHandler = ActivityLifecycleHandler()
registerActivityLifecycleCallbacks(lifecycleHandler)
}



これでタイミングは検知できる


  • アプリケーションがforegroundになったとき

  • 最初のActivityが生成されたとき


アラートを表示する

「アプリがforegroundになる」 + 「Activityが生成される」の2つのタイミングを検知してからアラートを「一度だけ」表示します。


実装


  • rxjavaを使って2つの処理が完了するまで待ちます。


MyApplication.kt

    @OnLifecycleEvent(Lifecycle.Event.ON_START)

fun onAppForegrounded() {
Log.d("StudyplusApp", "App in ON_START")

// 強制アップデートチェック
ForceUpdate.instance().check()

compositeDisposable = CompositeDisposable()

val compositeDisposable = CompositeDisposable()

Flowable.combineLatest<Boolean, Activity, Pair<Boolean, Activity>>(
ForceUpdate.instance().rxIsNeedForceUpdate,
lifecycleHandler.topActivityOnStartedEvent,
BiFunction<Boolean, Activity, Pair<Boolean, Activity>> { first, second -> Pair(first, second) })
.filter { s -> s.first }
.take(1)
.subscribe { s -> ForceUpdate.showAlert(s.second as AppCompatActivity, null) }.addTo(compositeDisposable)
this.compositeDisposable = compositeDisposable
}

@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun onStop() {
Log.d("TAG", "onStop")
compositeDisposable.dispose
}


これでアプリの起動時/background→foregroundのタイミング且つ強制アップデートの対象だった場合にアラートが表示されますね!


終わり

以前はとても頭を悩ませていたアプリのbackground→foregroundの検知ですが、ProcessLifecycleOwnerによって簡単にイベントを取ることが出来ます。


最近Androidはどんどん作る側が楽になっているなーと感じて嬉しい限りです。

次回は最近試したRoomのことでも書こうと思います。


反省

色々ともう少しスマートな書き方があるんじゃなかろうか


でも忙しいからこのへんでいいですか (駄目