この記事では、ProcessLifecycleOwnerとDefaultLifecycleObserverを使用したクラスの実装と、Robolectricを使用したユニットテストの書き方について説明します。
目次
AndroidのProcessLifecycleOwnerの概要
ProcessLifecycleOwner
はAndroidアプリケーション全体のライフサイクルを表す、シングルトンのLifecycleOwnerです。通常のActivityやFragmentのライフサイクルとは異なり、ProcessLifecycleOwnerはアプリ全体が前面に表示されているか(フォアグラウンド)、背面に隠れているか(バックグラウンド)を追跡します。
主なイベントは以下のように処理されます:
- ON_START: アプリがフォアグラウンドに移行したとき
- ON_STOP: アプリがバックグラウンドに移行したとき
重要な点として、ProcessLifecycleOwnerは内部的に約700msの遅延を使用して、新しいアクティビティが作成されていないことを確認してからON_STOPイベントを発火させます。
AppLifecycleObserverの実装
まず、ProcessLifecycleOwnerを使用してアプリのライフサイクルを監視するクラスを実装してみます。
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import javax.inject.Inject
import javax.inject.Singleton
/**
* アプリケーションのライフサイクル状態を監視し、アプリがフォアグラウンドにあるか
* バックグラウンドにあるかを追跡するクラス
*/
@Singleton
class AppLifecycleObserver @Inject constructor() : DefaultLifecycleObserver {
// アプリのフォアグラウンド状態を表すFlow
private val _appInForeground = MutableStateFlow(false)
val appInForeground: StateFlow<Boolean> = _appInForeground.asStateFlow()
// 最後にフォアグラウンドに入った時間
private var lastForegroundTime: Long = 0
// アプリが起動されてからの合計フォアグラウンド時間
private var totalForegroundTime: Long = 0
init {
// ProcessLifecycleOwnerに自身をオブザーバーとして登録
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
}
/**
* アプリがフォアグラウンドに入ったときに呼ばれる
*/
override fun onStart(owner: LifecycleOwner) {
_appInForeground.value = true
lastForegroundTime = System.currentTimeMillis()
}
/**
* アプリがバックグラウンドに移行したときに呼ばれる
*/
override fun onStop(owner: LifecycleOwner) {
_appInForeground.value = false
val currentTime = System.currentTimeMillis()
val sessionDuration = currentTime - lastForegroundTime
totalForegroundTime += sessionDuration
}
/**
* 現在のセッション時間を取得する
* アプリがフォアグラウンドにある場合は現在の時間から計算し、
* バックグラウンドにある場合は0を返す
*/
fun getCurrentSessionDurationInSeconds(): Long {
return if (_appInForeground.value) {
(System.currentTimeMillis() - lastForegroundTime) / 1000
} else {
0
}
}
/**
* アプリケーションの合計フォアグラウンド時間を秒単位で取得する
*/
fun getTotalForegroundTimeInSeconds(): Long {
return totalForegroundTime / 1000 + getCurrentSessionDurationInSeconds()
}
}
このクラスは、アプリがフォアグラウンドにあるかどうかの状態を追跡し、アプリの使用時間を計測する機能を提供します。
Applicationクラスでの使用例は以下の通りです:
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject
@HiltAndroidApp
class MyApplication : Application() {
@Inject
lateinit var appLifecycleObserver: AppLifecycleObserver
override fun onCreate() {
super.onCreate()
// AppLifecycleObserverの初期化は、コンストラクタのProcessLifecycleOwner.get()で行われます
}
}
Robolectricを使用したテスト
Robolectricは、JVM上でAndroidテストを実行できるフレームワークで、エミュレータやデバイスなしでテストを高速に実行できます。
今回はRobolectricでProcessLifecycleOwnerをテストする方法を紹介します。
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.testing.TestLifecycleOwner
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows
import org.robolectric.annotation.Config
import org.robolectric.shadows.ShadowLooper
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
@ExperimentalCoroutinesApi
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [28]) // 対象のSDKバージョンを指定
class AppLifecycleObserverTest {
// JUnitルール: テスト中にアーキテクチャコンポーネントの操作を同期的に実行
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
// テスト対象
private lateinit var appLifecycleObserver: AppLifecycleObserver
// TestLifecycleOwner - ライフサイクルイベントを制御するために使用
private lateinit var testLifecycleOwner: TestLifecycleOwner
@Before
fun setup() {
// TestLifecycleOwnerの初期化
testLifecycleOwner = TestLifecycleOwner()
// AppLifecycleObserverの初期化
appLifecycleObserver = AppLifecycleObserver()
// 対象クラスをTestLifecycleOwnerに登録
testLifecycleOwner.lifecycle.addObserver(appLifecycleObserver)
}
@Test
fun `アプリ起動時にフォアグラウンド状態がtrueになること`() = runTest {
// アプリ起動時のライフサイクルイベントをシミュレート
testLifecycleOwner.currentState = Lifecycle.State.STARTED
// フォアグラウンド状態を確認
val isInForeground = appLifecycleObserver.appInForeground.first()
assertTrue(isInForeground)
}
@Test
fun `アプリがバックグラウンドに移行したときにフォアグラウンド状態がfalseになること`() = runTest {
// 最初にアプリをフォアグラウンドにする
testLifecycleOwner.currentState = Lifecycle.State.STARTED
// バックグラウンドへの移行をシミュレート
testLifecycleOwner.currentState = Lifecycle.State.CREATED
// バックグラウンド状態を確認
val isInForeground = appLifecycleObserver.appInForeground.first()
assertFalse(isInForeground)
}
@Test
fun `バックグラウンド時のセッション時間が0になること`() = runTest {
// アプリをバックグラウンドにする
testLifecycleOwner.currentState = Lifecycle.State.STARTED
testLifecycleOwner.currentState = Lifecycle.State.CREATED
// バックグラウンド時のセッション時間をチェック
assertEquals(0, appLifecycleObserver.getCurrentSessionDurationInSeconds())
}
@Test
fun `フォアグラウンド時にセッション時間が増加すること`() {
// アプリをフォアグラウンドにする
testLifecycleOwner.currentState = Lifecycle.State.STARTED
// 最初のセッション時間を記録
val initialDuration = appLifecycleObserver.getCurrentSessionDurationInSeconds()
// 時間の経過をシミュレート(Robolectricの機能を使用)
ShadowLooper.idleMainLooper(1000) // 1秒進める
// セッション時間が増加しているか確認
val newDuration = appLifecycleObserver.getCurrentSessionDurationInSeconds()
assertTrue(newDuration > initialDuration)
}
@Test
fun `合計フォアグラウンド時間がセッション間で累積されること`() {
// 1回目のフォアグラウンドセッション
testLifecycleOwner.currentState = Lifecycle.State.STARTED
ShadowLooper.idleMainLooper(1000) // 1秒進める
testLifecycleOwner.currentState = Lifecycle.State.CREATED
// 1回目のセッション後の合計時間を記録
val totalAfterFirstSession = appLifecycleObserver.getTotalForegroundTimeInSeconds()
// 2回目のフォアグラウンドセッション
testLifecycleOwner.currentState = Lifecycle.State.STARTED
ShadowLooper.idleMainLooper(1000) // 1秒進める
testLifecycleOwner.currentState = Lifecycle.State.CREATED
// 2回目のセッション後の合計時間を確認
val totalAfterSecondSession = appLifecycleObserver.getTotalForegroundTimeInSeconds()
// 合計時間が累積しているか確認
assertTrue(totalAfterSecondSession > totalAfterFirstSession)
}
@Test
fun `複数のライフサイクル遷移で状態が正しく更新されること`() = runTest {
// 初期状態チェック - バックグラウンド
assertFalse(appLifecycleObserver.appInForeground.first())
// フォアグラウンドへ
testLifecycleOwner.currentState = Lifecycle.State.STARTED
assertTrue(appLifecycleObserver.appInForeground.first())
// バックグラウンドへ
testLifecycleOwner.currentState = Lifecycle.State.CREATED
assertFalse(appLifecycleObserver.appInForeground.first())
// 再びフォアグラウンドへ
testLifecycleOwner.currentState = Lifecycle.State.STARTED
assertTrue(appLifecycleObserver.appInForeground.first())
}
@Test
fun `ProcessLifecycleOwnerの遅延動作を再現するテスト`() = runTest {
// この例では、ProcessLifecycleOwnerの700ms遅延をRobolectricでシミュレート
// アプリをフォアグラウンドにする
testLifecycleOwner.currentState = Lifecycle.State.STARTED
assertTrue(appLifecycleObserver.appInForeground.first())
// バックグラウンドへの移行
testLifecycleOwner.currentState = Lifecycle.State.CREATED
// 時間の経過をシミュレート(ProcessLifecycleOwnerの遅延よりも短い時間)
ShadowLooper.idleMainLooper(500) // 500ms進める
// バックグラウンド状態を確認
assertFalse(appLifecycleObserver.appInForeground.first())
// 実際のProcessLifecycleOwnerでは、この時点でまだON_STOPは発火していないが
// テスト環境ではすでに状態が変更されている
}
}
モックを使用した別のテストアプローチ
TestLifecycleOwnerが使用できない場合や、より詳細な検証が必要な場合は、LifecycleRegistryをモックする方法もあります。
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.`when`
import org.mockito.Mockito.mock
import org.mockito.MockitoJUnitRunner
import org.mockito.Mockito.verify
import org.mockito.Spy
import kotlin.test.assertFalse
import kotlin.test.assertTrue
@ExperimentalCoroutinesApi
@RunWith(MockitoJUnitRunner::class)
class AppLifecycleObserverWithMockTest {
// テスト対象
@Spy
private lateinit var appLifecycleObserver: AppLifecycleObserver
// LifecycleOwnerをモック
@Mock
private lateinit var lifecycleOwner: LifecycleOwner
// LifecycleRegistryをモック
private lateinit var lifecycleRegistry: LifecycleRegistry
@Before
fun setup() {
// LifecycleRegistryの作成
lifecycleRegistry = LifecycleRegistry(mock(LifecycleOwner::class.java))
// モックの設定
`when`(lifecycleOwner.lifecycle).thenReturn(lifecycleRegistry)
// AppLifecycleObserverの初期化
appLifecycleObserver = AppLifecycleObserver()
// オブザーバーをLifecycleRegistryに登録
lifecycleRegistry.addObserver(appLifecycleObserver)
}
@Test
fun `onStartが呼ばれるとフォアグラウンド状態がtrueになること`() = runTest {
// onStartメソッドを直接呼び出す
appLifecycleObserver.onStart(lifecycleOwner)
// フォアグラウンド状態を確認
assertTrue(appLifecycleObserver.appInForeground.first())
}
@Test
fun `onStopが呼ばれるとフォアグラウンド状態がfalseになること`() = runTest {
// まずフォアグラウンドに設定
appLifecycleObserver.onStart(lifecycleOwner)
// onStopメソッドを呼び出す
appLifecycleObserver.onStop(lifecycleOwner)
// バックグラウンド状態を確認
assertFalse(appLifecycleObserver.appInForeground.first())
}
@Test
fun `ライフサイクルイベントで適切なオブザーバーメソッドが呼ばれること`() {
// ライフサイクルイベントをシミュレート
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
// onStartが呼ばれたか確認
verify(appLifecycleObserver).onStart(lifecycleRegistry.observerUnderObservation)
// ライフサイクルイベントをシミュレート
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
// onStopが呼ばれたか確認
verify(appLifecycleObserver).onStop(lifecycleRegistry.observerUnderObservation)
}
}
この方法では、LifecycleRegistry
のhandleLifecycleEvent
メソッドを使用して、特定のライフサイクルイベントを発火させ、オブザーバーの対応するメソッドが呼び出されるかを検証します。
必要な依存関係
これらのクラスとテストを実行するために必要なGradle依存関係は以下の通りです:
// app/build.gradle.kts
dependencies {
// ライフサイクルコンポーネント
implementation("androidx.lifecycle:lifecycle-process:2.6.2")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
// Kotlinコルーチン
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
// Dagger Hilt(必要な場合)
implementation("com.google.dagger:hilt-android:2.48")
kapt("com.google.dagger:hilt-android-compiler:2.48")
// テスト依存関係
testImplementation("junit:junit:4.13.2")
testImplementation("org.jetbrains.kotlin:kotlin-test:1.9.10")
testImplementation("org.mockito:mockito-core:5.5.0")
testImplementation("org.mockito:mockito-inline:5.5.0")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
testImplementation("androidx.arch.core:core-testing:2.2.0")
testImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.6.2") // TestLifecycleOwnerを使用するため
}
特に重要なのは、androidx.lifecycle:lifecycle-runtime-testing
依存関係で、これによってTestLifecycleOwner
クラスが使用可能になります。