はじめに
まずはこちらの動画を見ていただきたいです。
アプリ起動時は音が鳴るのに、ダークテーマに変更すると、音が鳴らなくなってしまいました。 これはどのような現象なのでしょうか??デバイス構成の変更
まず、「デバイスのテーマを変更する」ことは、デバイス構成を変更するといいます。
デバイス構成を変更すると、既存のActivityは破棄され、新しいActivityが再生成されます。
Activityが処理するデバイス構成の変更には、以下のものがあります。
(Android Developers : android:configChanges)
画面回転するとonDestroy()
→onCreate()
が実行されることは知っていたのですが、テーマ変更やフォントサイズの変更なども同じ構成変更の一つなのですね!
(本課題に直面したときに作成していたアプリでは画面回転をできないように設定していたので、この課題に気づくのが遅れました…)
どういう実装をしていたのか
音楽の再生にはExoPlayer Media3を使用しています。
実装コードは次のとおりです。
SoundPlayerModule.kt
@Module
@InstallIn(SingletonComponent::class)
class SoundPlayerModule {
@Singleton
@Provides
fun provideMediaPlayer(@ApplicationContext context: Context) = ExoPlayer.Builder(context).build()
}
@Module
@InstallIn(SingletonComponent::class)
abstract class BindSoundPlayerModule {
@Singleton
@Binds
abstract fun soundPlayer(soundPlayerImpl: SoundPlayerImpl): SoundPlayer
}
SoundPlayer.kt
interface SoundPlayer {
fun play()
fun release()
}
@UnstableApi
class SoundPlayerImpl @Inject constructor(private val exoPlayer: ExoPlayer) : SoundPlayer {
override fun play() {
val media = MediaItem.fromUri(RawResourceDataSource.buildRawResourceUri(R.raw.level_up))
exoPlayer.setMediaItem(media)
exoPlayer.prepare()
exoPlayer.play()
}
override fun release() {
exoPlayer.release()
}
}
MainActivity.kt
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@Inject
lateinit var soundPlayer: SoundPlayer
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
SampleMediaPlayerTheme {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
){
Button(onClick = { soundPlayer.play() }) {
Text(text = "Play Sound")
}
}
}
}
}
override fun onDestroy() {
super.onDestroy()
soundPlayer.release()
}
}
なぜ構成変更後に音楽が再生できないのか
ログを確認したところ、(もちろんですが)onDestroy()
→ onCreate()
が実行されていたので、soundPlayer.release()
した後に、ExoPlayerインスタンスの再生成?が上手くいっていなさそうだと考えました。
ExoPlayerインスタンスの生成はHiltを使用して行っていたので、Injectのタイミングが良くない??上手くできていない??
じゃあExoPlayerインスタンスはいつ生成されているのでしょうか??
Hiltで生成したインスタンスのライフサイクル
Hiltで生成するコンポーネントごとに、作成・破棄のタイミングが決まっています。
(Android Developers:Hilt を使用した依存関係の注入-コンポーネントのライフタイム-)
私の実装では、SingletonComponent
を使用していました。したがって、ExoPlayerインスタンスはApplication#onCreate()のタイミングで作成され、Applicationが破棄されるタイミングで一緒に破棄されます。
つまり、冒頭の動画の状態は、「Activityは破棄されているが、Applicationは生きている」→「releaseされたExoPlayerインスタンスが生きているため、音楽再生ができない」状態だと考えられます。
私の実現したいことは、「ExoPlayerインスタンスの生成・破棄のタイミングを、Activityのライフサイクルと合わせる」ことです。
ちょうど良さそうな!ActivityComponent
というものが!ありますね!
ActivityComponentを使用して書き直してみる
ActivityComponent
を使用して、SoundPlayerModule.kt
を書き直してみます。
@Module
@InstallIn(ActivityComponent::class)
class SoundPlayerModule {
@ActivityScoped
@Provides
fun provideMediaPlayer(@ActivityContext context: Context) = ExoPlayer.Builder(context).build()
}
@Module
@InstallIn(ActivityComponent::class)
abstract class BindSoundPlayerModule {
@ActivityScoped
@Binds
abstract fun soundPlayer(soundPlayerImpl: SoundPlayerImpl): SoundPlayer
}
できたものがこちらになります。
テッテレー!!!!!!テーマ変更後も音楽再生できました!!!!!!!!これで適切にExoPlayerインスタンスを処理することができたのではないでしょうか。
まとめ
今まで深く考えずSingletonComponent
を使用していたのですが、Hiltにより生成したComponentのライフサイクルを意識し、適切なComponentを選択することの大切さを学びました。
また、システム構成の変更には画面回転だけでなく、テーマ変更など、さまざまな種類があることを知りました。
最後に
公式に構成変更に関するベストプラクティスが乗っているので、そちらを参照することをオススメします。
(Android Developers: 構成の変更に対処する)
おまけ
GitHubリポジトリを見ていると、release()
を実行していないコードもいくつか見られました。
そこで、ChatGPTに質問してみた結果がこちらです。
たしかに、Hiltによってコンポーネントを生成する際に、指定したライフサイクルに応じてよしなに処理してくれる旨の記載が公式にありました。
なんとなく理論はわかるけれど、果たして本当にいいのか…?の気持ちです。もしご存知の方がいらっしゃったら、教えてくださると嬉しいです。