0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Android】Jetpack ComposeでLaunchedEffectとrememberCoroutineScopeを使い分ける実践ガイド

0
Posted at

はじめに

Jetpack Compose で非同期処理やコルーチンを扱っていると、必ずと言っていいほど登場するのが LaunchedEffectrememberCoroutineScope です。

どちらも「Compose の中でコルーチンを起動する」ための仕組みですが、起動されるタイミングライフサイクルが根本的に異なります。この違いを理解せずに使うと、

  • 画面回転のたびに API が再度呼ばれてしまう
  • ボタンを押したのにアニメーションが動かない
  • スナックバーが表示されない/意図せず何度も表示される

といった不具合を引き起こしがちです。本記事では、両者の違いを実際のユースケースを交えながら整理します。

結論を先に

LaunchedEffect rememberCoroutineScope
起動タイミング Composition 時に 自動で 起動 ユーザー操作などの イベントに応じて 明示的に起動
呼び出し場所 Composable 関数の直下(コンポーザブルスコープ) onClick などのラムダ(非コンポーザブルスコープ)
典型用途 画面表示時のデータ取得、状態変化の監視 ボタン押下時のアニメーション実行、Snackbar表示
スコープの寿命 key が変わるかコンポジションから離脱するとキャンセル 呼び出し元のコンポーザブルがコンポジションから離脱するとキャンセル

一言でまとめると、

「Compose が勝手にやってほしい処理」は LaunchedEffect、「ユーザーが何かした時にやりたい処理」は rememberCoroutineScope

です。

LaunchedEffect: Composition に連動して自動実行

LaunchedEffect は、指定した key が変化したとき(または初回コンポジション時)にコルーチンを起動する Composable 関数です。onClick のような通常のラムダの中では呼び出せず、あくまで Composable 関数のスコープ内でのみ使用できます。

実例: 画面表示時にデータを取得する

@Composable
fun UserProfileScreen(userId: String, viewModel: UserProfileViewModel) {
    val uiState by viewModel.uiState.collectAsState()

    // userId が変わるたびに再取得したい
    LaunchedEffect(userId) {
        viewModel.loadUser(userId)
    }

    when (uiState) {
        is UiState.Loading -> LoadingIndicator()
        is UiState.Success -> UserProfileContent(uiState.user)
        is UiState.Error -> ErrorMessage(uiState.message)
    }
}

ポイントは keyuserId を渡している点です。もし key1 = Unit にしてしまうと、画面回転などの再コンポジションが起きても再実行されませんが、userId が実際に変わったときには再実行したい、というケースにはこのように意味のある key を渡します。

もう1つ典型的なのが「状態の変化を監視して副作用を起こす」パターンです。

@Composable
fun SearchScreen(viewModel: SearchViewModel) {
    val query by viewModel.query.collectAsState()

    LaunchedEffect(query) {
        // 入力が止まってから少し待ってから検索する(デバウンス)
        delay(300)
        viewModel.search(query)
    }

    SearchTextField(query = query, onQueryChange = viewModel::onQueryChange)
}

query が更新されるたびに前回の LaunchedEffect は自動的にキャンセルされ、新しいコルーチンが起動します。これにより、デバウンス処理が簡潔に書けます。手動でキャンセル処理を書く必要がない点が LaunchedEffect の大きな利点です。

rememberCoroutineScope: イベント起点で明示的に実行

一方、rememberCoroutineScope は「コンポジションに紐づいた CoroutineScope を覚えておく」だけの仕組みです。それ自体はコルーチンを起動しません。実際に scope.launch { } を呼んだタイミングでコルーチンが起動します。

実例: ボタン押下でSnackbarを表示する

@Composable
fun SubmitButton(snackbarHostState: SnackbarHostState) {
    val scope = rememberCoroutineScope()

    Button(
        onClick = {
            scope.launch {
                snackbarHostState.showSnackbar("送信しました")
            }
        }
    ) {
        Text("送信")
    }
}

showSnackbar は suspend 関数なので、onClick のような通常のラムダから直接呼び出すことはできません。onClick は Composable 関数ではないため、そこで LaunchedEffect を使うこともできません。そこで、あらかじめ rememberCoroutineScope で取得しておいたスコープを使ってコルーチンを起動する、という書き方になります。

実例: アニメーションをユーザー操作に応じて実行する

@Composable
fun ExpandableCard() {
    val scope = rememberCoroutineScope()
    val rotation = remember { Animatable(0f) }

    Card(
        modifier = Modifier.clickable {
            scope.launch {
                rotation.animateTo(
                    targetValue = if (rotation.value == 0f) 180f else 0f,
                    animationSpec = tween(300)
                )
            }
        }
    ) {
        Icon(
            imageVector = Icons.Default.ArrowDropDown,
            contentDescription = null,
            modifier = Modifier.rotate(rotation.value)
        )
    }
}

これも「タップされた瞬間にだけ」アニメーションを起動したいケースです。もし仮にこれを LaunchedEffect で書こうとすると、クリックという「イベント」を何らかの State に変換してその変化を監視する、という遠回りな実装が必要になり、コードの見通しが悪くなります。

使い分けを間違えるとどうなるか

アンチパターン1: onClick の中で LaunchedEffect を使おうとする

Button(onClick = {
    LaunchedEffect(Unit) { // コンパイルエラー
        viewModel.submit()
    }
})

LaunchedEffect@Composable 関数なので、通常のラムダの中には書けません。ボタン押下時の処理は rememberCoroutineScope を使うのが正解です。

アンチパターン2: 副作用のトリガーに rememberCoroutineScope を使う

@Composable
fun UserProfileScreen(userId: String, viewModel: UserProfileViewModel) {
    val scope = rememberCoroutineScope()

    // 初回にしか呼ばれず、userId が変わっても再取得されない
    scope.launch {
        viewModel.loadUser(userId)
    }
    ...
}

これは一見動きそうに見えますが、Composable 関数の本体が再コンポジションされるたびに scope.launch が無条件で呼ばれてしまい、意図しない多重実行を招きます。また userId の変化を検知して再取得する、という目的も果たせません。画面表示に連動した処理は LaunchedEffect(key) で書くべきです。

まとめ

  • LaunchedEffectComposition のライフサイクルに紐づく副作用(データ取得、状態監視など)に使う
  • rememberCoroutineScopeユーザー操作などの単発イベントに応じてコルーチンを起動したいときに使う
  • 「起動するトリガーが State/key の変化か、それともイベントか」で判断すると迷いにくい

どちらも Compose の宣言的なライフサイクルと相性の良い設計になっているので、GlobalScope.launch のような自前のスコープ管理をなるべく避け、Compose が用意しているこれらの仕組みに乗ることが、安全な非同期処理への近道です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?