Jetpack Composeで DisposableEffect に任せるべき処理

Jetpack Compose では UI のライフサイクル管理がかなり簡単になりました。

しかし、それでも明示的な後始末が必要な処理があります。

たとえば、

・ Listener を登録する
・ Connection を開始する
・ 外部リソースを取得する
・ 後から停止する必要がある処理を開始する
・ UI が消えたときに登録解除やリソース解放を行う

といった処理です。

このような処理を Composition のライフサイクルに結び付けるのが DisposableEffect です。

基本的には、次のように考えると分かりやすいです。


Composition に入る
       ↓
register / start / acquire
       ↓
Composition に存在している間
       ↓
Composition から出る
       ↓
onDispose
       ↓
unregister / stop / release

 

🤔 remember はリソースを解放しない

例えば、次のコードを考えてみます。


@Composable
fun PlayerScreen() {
    val player = remember {
        MediaPlayer()
    }
}

remember は再コンポジションをまたいでオブジェクトを保持します。

しかし、MediaPlayer が何なのか、いつ release() すべきなのかまでは知りません。

明示的な解放の API を持つオブジェクトなら、その解放処理を適切なライフサイクルに結び付ける必要があります。


@Composable
fun PlayerScreen() {
    val player = remember {
        MediaPlayer()
    }

    DisposableEffect(player) {
        onDispose {
            player.release()
        }
    }
}

ここで重要なのは、


remember
  ↓
オブジェクトを保持する


DisposableEffect
  ↓
オブジェクトに関連する副作用のライフサイクルを管理する

という違いです。

つまり、remember 自体がメモリーリークを起こすわけではありません。

問題になるのは、明示的な解放が必要なリソースを、適切なタイミングで終了させないことです。

 

🤔 よくあるパターン

DisposableEffect を使う処理は、ほとんど同じ形になります。


DisposableEffect(key) {

    resource.register()

    onDispose {
        resource.unregister()
    }
}

名前は変わっても、考え方は同じです。


// LifecycleObserver

DisposableEffect(lifecycle) {
    lifecycle.addObserver(observer)

    onDispose {
        lifecycle.removeObserver(observer)
    }
}


// BroadcastReceiver

DisposableEffect(receiver) {
    context.registerReceiver(receiver, filter)

    onDispose {
        context.unregisterReceiver(receiver)
    }
}


// Sensor

DisposableEffect(sensor) {
    sensorManager.registerListener(
        listener,
        sensor,
        SensorManager.SENSOR_DELAY_NORMAL
    )

    onDispose {
        sensorManager.unregisterListener(listener)
    }
}


// Location

DisposableEffect(locationCallback) {
    locationClient.requestLocationUpdates(
        request,
        locationCallback,
        Looper.getMainLooper()
    )

    onDispose {
        locationClient.removeLocationUpdates(locationCallback)
    }
}


// NetworkCallback

DisposableEffect(networkCallback) {
    connectivityManager.registerNetworkCallback(
        request,
        networkCallback
    )

    onDispose {
        connectivityManager.unregisterNetworkCallback(networkCallback)
    }
}


// MediaPlayer

val player = remember {
    MediaPlayer()
}

DisposableEffect(player) {
    onDispose {
        player.release()
    }
}


// WebSocket

val socket = remember {
    createWebSocket()
}

DisposableEffect(socket) {
    socket.connect()

    onDispose {
        socket.close()
    }
}


// Listener

DisposableEffect(manager) {
    manager.addListener(listener)

    onDispose {
        manager.removeListener(listener)
    }
}


// TextWatcher

DisposableEffect(textView) {
    textView.addTextChangedListener(watcher)

    onDispose {
        textView.removeTextChangedListener(watcher)
    }
}

 

🤔 ViewModel の場合はどうなる?

Composition が所有するリソースなら、DisposableEffect で解放できます。


Composition
    │
    └── resource
          │
          └── DisposableEffect
                  ↓
               onDispose()

一方、ViewModel が所有するリソースは、ViewModel のライフサイクルに従います。


ViewModel
    │
    └── resource
          │
          └── onCleared()

例えば、


class PlayerViewModel : ViewModel() {

    private val player = MediaPlayer()

    override fun onCleared() {
        player.release()
        super.onCleared()
    }
}

ViewModel がリソースを保持すること自体に問題があるわけではありません。

重要なのは、

「このリソースの所有者は誰で、その所有者の寿命はいつ終わるのか?」

ということです。

MediaPlayerが 画面に属するなら、DisposableEffect は自然な選択です。

また、Composition が破棄されても再生を継続したいなど、MediaPlayer を ViewModel の寿命まで保持したいのであれば、ViewModel が所有する設計も考えられます。

 

🤔 メモリーリークとは別の話

ここで、リソースリークメモリーリーク を混同しないことも重要です。

典型的なメモリーリークは、長く生存するオブジェクトが、本来なら破棄されるはずのオブジェクトを参照し続けることで起こります。

例えば、ViewModel が Activity への参照を保持しているとします。

Activity が画面から破棄された後も ViewModel がその Activity を参照していれば、Activity を GC できなくなる可能性があります。

これはメモリーリークです。

一方、リソースリークは少し違います。


Object
   │
   └──→ MediaPlayer
            │
            └── 外部リソース

もう使わないのに release() を呼ばなければ、外部リソースを明示的に解放していないことが問題になります。

両者が同時に発生することはありますが、同じものではありません。

 

🤔 気づくための簡単なルール

Composable の中に、次のような処理があったら考えてみてください。

・ register()
・ addListener()
・ connect()
・ start()
・ acquire()

そして、

「これに対応するクリーンアップはどこにある?」

と考えます。

例えば、


register()    →  unregister()
addListener() →  removeListener()
connect()     →  close()
start()       →  stop()
acquire()     →  release()

です。

もし、その処理が Composable が Composition に存在している間だけ必要なら、DisposableEffect は有力な選択肢です。

 

🤔 DisposableEffect の本質

DisposableEffect は、Composition のライフサイクルに合わせて副作用の開始と終了を管理します。

remember:
「再コンポジションをまたいで オブジェクトを保持する」

DisposableEffect:
「Compositionの存在期間に合わせて副作用を管理する」

onDispose:
「もう必要ないので明示的に後始末する」

結局、DisposableEffect を使うかどうかを考えるときに重要なのは、

「このリソースの所有者は誰で、その寿命はいつ終わるのか?」

という1つの質問です。

この視点を持つと、DisposableEffect を単なる API としてではなく、リソースのライフサイクルを Composition に接続するための仕組みとして理解できます。


Related Categories :  AndroidDevelopmemtJetpackComposeKotlinNewbie