🐻‍❄️

[Jetpack Compose]プレビューのインタラクティブモードを積極的に利用してみる

2023/04/22に公開

インタラクティブモードとは?

Jetpack Composeのプレビューにはインタラクティブモードというモードがあります。このインタラクティブモードではAndroidエミュレータを起動せず、Android Studioのプレビュー内でデバイス上での操作を試すことができます。

デバイス上での操作とは?

例えば以下のような操作の動作確認をインタラクティブモードで試すことができます。

  • 要素をクリックしたときの動作
  • 要素に対して入力したときの動作
  • コンポーザブルが異なる状態に変化する際の動作
  • ジェスチャーを入力したときの動作
  • アニメーションしたときの動作

メリット

インタラクティブモードはAndroid Studio内でプレビューが実行されるので以下のメリットがあります。

  • 操作に対しての動作確認をAndroid Studio内でできるので、アプリを起動する手間がなく効率良く動作確認できる
  • インタラクティブモード中であれば、操作に対する変更内容を即時反映できる

以下の例ではインタラクティブモードでクリック時のエフェクトの確認をしてみています。あと途中でクリック時のエフェクトを非表示にして動作確認してみています。このような感じで操作に対する変更内容の動作確認を迅速にできるというのがインタラクティブモードの強みになります。

デメリット

インタラクティブモードはAndroid Studio内でプレビューが実行されるからか以下の制約が設けられています。なのでインタラクティブモードだけでは確認できないコンポーザブルもあるというのがデメリットになりそうです。

  • ネットワークに接続ができない
  • ファイルアクセスができない
  • 一部のContext APIは利用できない場合がある

以下の例ではAccompanistのWebViewをインタラクティブモードで表示してみています。インタラクティブモードではネットワークに接続できないのでこのような感じでWebページを読み込むことができません。という感じになっておりアプリで完結しない表示に関するところは、インタラクティブモードでは確認できないので、アプリを起動して確認するしかないです。

インタラクティブモードで動作確認すると良さそうなケース

その1 : クリックしたときに表示が変わるケース

Composable

このようなトグルボタンでクリックしたときの動作を確認すると良さそうです。
とはいえ2つの状態を確認するケースでは、各々の状態の表示するプレビューを2つ並べるで、十分そうとも感じます。

@Composable
fun CirclePowerButton(enabled: Boolean, onClick: (enabled: Boolean) -> Unit, modifier: Modifier = Modifier) {
    val color = if (enabled) Color.Red else Color.LightGray
    val text = if (enabled) "ON" else "OFF"
    Box(modifier) {
        Box(
            modifier = Modifier
                .size(64.dp)
                .background(color = color, shape = CircleShape)
                .clip(CircleShape)
                .clickable { onClick(!enabled) }
        ) {
            Text(text = text, modifier = Modifier.align(Alignment.Center))
        }
    }
}

Preview

@Preview(showBackground = true)
@Composable
fun SampleWebViewPreview() {
    var enabled by remember { mutableStateOf(false) }
    CirclePowerButton(
        enabled = enabled,
        onClick = { enabled = it }
    )
}

その2 : スワイプなどのジェスチャーをしたら表示が変わる系

Composable

このようなViewPagerをスワイプなどのジェスチャーしたときの動作を確認すると良さそうです。
特にViewPagerは見切れの範囲など、細かく調整することがあるので、インタラクティブモードを使うと確認が捗りそうです。

data class News(val content: String)

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun NewsViewPager(
    items: List<News>,
    modifier: Modifier = Modifier,
) {
    HorizontalPager(pageCount = items.count(), modifier = modifier) { page ->
        Card(modifier = Modifier.fillMaxSize()) {
            Box(modifier = Modifier.fillMaxSize()) {
                Text(
                    text = items[page].content,
                    fontSize = 50.sp,
                    modifier = Modifier.align(Alignment.Center)
                )
            }
        }
    }
}

Preview

@Preview
@Composable
private fun NewsViewPagerPreview() {
    NewsViewPager(
        items = listOf(News("A"), News("B"), News("C")),
        modifier = Modifier.size(300.dp).padding(32.dp)
    )
}

その3 : 選択状況と連動して、特定の要素の表示が更新される系

このような左側のLazyColumnの選択状況と連動して、右側の内容を更新するときの動作を確認すると良さそうです。またLazyColumnでは選択状態がどのように変化するか、背景色を調整することがあるので、インタラクティブモードを使うと確認が捗りそうです。

data class Content(
    val id: String = UUID.randomUUID().toString(),
    val title: String,
    val content: String
) {
    companion object {
        val CONTENTS = listOf(
            Content(title = "2023/04/22", content = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
            Content(title = "2023/04/23", content = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"),
            Content(title = "2023/04/24", content = "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"),
            Content(title = "2023/04/22", content = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
            Content(title = "2023/04/23", content = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"),
            Content(title = "2023/04/24", content = "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"),
            Content(title = "2023/04/22", content = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
            Content(title = "2023/04/23", content = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"),
            Content(title = "2023/04/24", content = "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"),
            Content(title = "2023/04/22", content = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
            Content(title = "2023/04/23", content = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"),
            Content(title = "2023/04/24", content = "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"),
            Content(title = "2023/04/22", content = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
            Content(title = "2023/04/23", content = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"),
            Content(title = "2023/04/24", content = "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"),
            Content(title = "2023/04/22", content = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
            Content(title = "2023/04/23", content = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"),
            Content(title = "2023/04/24", content = "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"),
            Content(title = "2023/04/22", content = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
            Content(title = "2023/04/23", content = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"),
            Content(title = "2023/04/24", content = "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"),
            Content(title = "2023/04/22", content = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
            Content(title = "2023/04/23", content = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"),
            Content(title = "2023/04/24", content = "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"),
        )

    }
}

@Composable
fun ContentViewer(
    items: List<Content>,
    selectedItem: Content,
    onSelectItem: (Content) -> Unit,
    modifier: Modifier = Modifier,
) {
    Row(modifier = modifier) {
        LazyColumn(
            modifier = Modifier.fillMaxHeight()
        ) {
            items(items = items, key = { it.id }) { item ->
                val background by remember(selectedItem) {
                    derivedStateOf {
                        if (selectedItem.id == item.id)  Color.LightGray else Color.White
                    }
                }

                Text(text = item.title, modifier = Modifier
                    .background(background)
                    .clickable { onSelectItem(item) }
                    .padding(horizontal = 16.dp))
            }
        }

        Spacer(modifier = Modifier
            .width(2.dp)
            .fillMaxHeight()
            .background(Color.DarkGray))

        Column(
            modifier = Modifier
                .fillMaxSize()
                .background(Color.White)
                .padding(horizontal = 16.dp)
        ) {
            Text(text = selectedItem.title)
            Text(text = selectedItem.content)
        }
    }
}


@Preview(device = "spec:width=400px,height=400px,dpi=213")
@Composable
private fun ContentViewerPreview() {
    var selectedItem by remember { mutableStateOf(Content.CONTENTS.first()) }
    ContentViewer(
        items = Content.CONTENTS,
        selectedItem = selectedItem,
        onSelectItem = {
            selectedItem = it
        },
        modifier = Modifier
            .fillMaxSize()
            .background(Color.White)
    )
}

インタラクティブモードは積極的に使うべき?

個人的にはインタラクティブモードがあると、作業確認の手間が少し減るので、使うべきかなーと感じました。もしインタラクティブモードを使えるようにする手間がかかるのであれば使わなくてもOKな気がするのですが、プレビューを作るときに意識してrememberで情報を保持するようにするだけでインタラクティブモードでの動作確認できるようになるのでそこまでの手間じゃない気がしています。なのでインタラクティブモードでできるだけ動作確認可能なプレビューを負担が少ない範囲で日頃から作っていくのが良さそうと思います。

参考資料

https://developer.android.com/jetpack/compose/tooling/previews

Discussion