飴屋

Kotlin/Composableに移行する2

前回、やっとのことでレイアウトXMLをsetContentViewで読み込む方式から脱却し、setContentで@Composableなレイアウト部品を読み込めるようになりました。というわけで、旧部品を一つずつ、@Composableなレイアウト部品に置き換えていこうと思います。調べてみると本家サイトで「既存の View ベースのアプリを移行する」という記事が用意されていました。これを見ながら作業したらいいのかもしれない。ここでは旧来のやり方を「Viewベースのアプリ」と呼んでますね。とりあえず旧来のViewがfindViewByIdで探せなくなったので、onCreateの中でViewを操作しようとしていた部分を一旦コメントアウトしておきました。

DrawerLayout

まず、よく使っていた左からドロワーを引っ張り出せたレイアウトをCompose化してみましょう。どうもModalNavigationDrawerというのが使えそうです。

ModalNavigationDrawer(
    drawerContent = {
        ModalDrawerSheet {
            Text("Drawer title", modifier = Modifier.padding(16.dp))
            HorizontalDivider()
            NavigationDrawerItem(
                label = { Text(text = "Drawer Item") },
                selected = false,
                onClick = { /*TODO*/ }
            )
        }
    }
) {}

こんなサンプルをそのまま配置したら本当にドロワーが出るようになりました。ドロワーの中身はTextのタイトルとNavigationDrawerItemのアイテムですね。タイトルは今回要らないので削除して、必要な分だけNavigationDrawerItemを並べておきます。onCreateでドロワーがクリックされたときの処理を書いていましたが、NavigationDrawerItemのonClickに書き換えます。

Text

文字列を表すTextって多言語対応したstringなリソースをおうひょうじするんだろう?と思ったら、idを指定すればよさそうでした。

Text(text = stringResource( id = R.string.text_no_id))

IconButton と Image

アイコンを使ったボタンは今までImageButtonというViewで作ってましたが、Compose版ではIconButtonがよさそうでした。

IconButton(onClick = {}) {
    Image(
        painter = painterResource(id = R.drawable.ic_custom_icon),
        contentDescription = "Custom Icon",
        colorFilter = ColorFilter.tint(Color.Red)
    )
}

クリック時の動作をonClickに書いて、中身にImageコンポーネントを入れて画像リソースをボタンにできるようです。

IconButton(
    modifier = Modifier.size(80.dp).background(color = Color(0xFFFF9696), shape = CircleShape),
    onClick = {}
) {
    Image(
        painter = painterResource(id = android.R.drawable.ic_btn_speak_now),
        contentDescription = "Start Button",
        colorFilter = ColorFilter.tint(Color.White),
        modifier = Modifier.size(80.dp)
    )
}

最終的にmodifierでボタンのサイズや背景色、形状をして元のボタンに近づけました。

ConstraintLayout

ConstraintLayout(
    modifier = Modifier.fillMaxSize()
) {
    val (text1, button, text2) = createRefs()
    ...
}

ConstraintLayoutは同名のCompose コンポーネントが用意されていました。中の要素に制約を課すときにcreateRefsを呼び出すようです。

modifier = Modifier.constrainAs(button) {
        start.linkTo(parent.start)
        end.linkTo(parent.end)
        bottom.linkTo(ad.top, margin = 16.dp)
    },

buttonと制約名をつけたボタンがあったら、そのmodifierにconstrainAsで制約名を渡します。後は四方(top, bottom, start, end)をどこと紐づける(制約する)かlinkToでリンクします。マージンとかも一緒に指定できるようですね。親ビューの制約名としてparentも指定できる模様。

AdView

admobの広告表示viewは、composeではAndroidViewを使うのが一般的なんだそうです。え~、本当かな~?私ですらComposeを使い始めたのに?というわけで、admobの開発者向けサイトを見に行ったら、広告のコンテナに広告を挿入する操作だけ書かれてました。広告の表示レイアウトはこっちのマターってスタンスなのか。viewベースの時はGUIを用意してくれていたので、「貼り付ければいい」って感じでしたが、そういう感覚ではなさそう?

AndroidView(
    modifier = modifier, // 必要に応じて何か入れる
    factory = { context: Context ->
        AdView(context).apply {
            setAdSize(AdSize.BANNER) // 広告サイズを設定
            this.adUnitId = adUnitId // AdMobの広告ユニットID
            loadAd(AdRequest.Builder().build()) // 広告リクエストを送信
        }
    }
)

↑こんな感じのものをもっと汎用性を持たせて自分用に@Composableを作っておけば、あとあと楽になるかな。

カスタムView

自作のビューは広告ビューと一緒でAndroidViewを使うようです。

ActionBar

アプリの上端のタイトル(画面名)が書いてあったり、ドロワーを開くボタンがあったり、一つ前の画面に戻るボタンがあったりするビュー領域がありますが、アレをあのまま使えるようなCompose部品はなさそうでした。いやそもそもviewベースのときにviewとしてレイアウトXML配置した記憶がないので、あれはちょっと特殊な扱いになっていたのかな?Compose版で同じようなことをする方法は用意されていました。

Scaffold(
    topBar = {
        CenterAlignedTopAppBar(
            title = { "タイトル" },
            navigationIcon = {
                IconButton(onClick = {/* ナビゲーションの動作 */ }) {
                    Icon(Icons.Default.Menu, contentDescription = "Menu")
                }
            },
            actions = {
                  IconButton(onClick = { /* アクションの動作 */ }) {
                      Icon(Icons.Default.Settings, contentDescription = "Settings")
                  }
            }
        )
    }
) { innerPadding ->
...
}

Scaffoldは「足場」的な意味だそうです。setContentの中でテーマの直下に置いて、よくあるアプリの画面構成を作る時なんかに使えるそうです。この中のtopBar部分にAppBarを配置するとコンテンツよりも上部(画面上部)に張り付いて、コンテンツがスクロールしても常に一番上に表示され続けてくれるようです。ちなみにbottomBarとかfloatingActionButtonなんかの画面内固定要素も同様に扱えるみたいです。AIさんに聞いたときはドロワーも同じように書けるって言ってたんですが、現時点ではないっぽかったです。(昔はあったとか?)Scaffoldの内側、つまりコンテンツ部分にはinnerPaddingが渡されます。ここには固定要素の表示領域分の余白サイズが入っているようで、Modifier.padding(innerPadding)を直下の要素につけてあげると適切な表示領域に調整してくれました。

CenterAlignedTopAppBarというのを使ってみて、navigationIconがクリックされたらドロワーが開閉枢要に書いてみました。

val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val navCoroutineScope = rememberCoroutineScope()
Scaffold(
    topBar = {
        CenterAlignedTopAppBar(
            navigationIcon = {
                IconButton(onClick = {
                    navCoroutineScope.launch {
                        if (drawerState.isOpen) {
                            drawerState.close()
                        } else {
                            drawerState.open()
                        }
                    }
                }) {
                    Icon(Icons.Default.Menu, contentDescription = "Menu")
                }
            },
...

MediaRouteButton

Google CastのボタンをTopAppBarに追加する場合もAndroidViewを使うらしい。TopAppBarのactionsのところが右肩のボタン群の実体で、ここにAndroidViewを配置し、その中でMediaRouteButtonを展開すればよいとのことでした。しかし、AndroidViewの中のviewはCompose畑にないため、modifierとかで操作できないってことなんですね。MediaRouteButtonの色が変えられなくて困りました。変え方をググったり、複数のAIに聞いてみたりしましたが、答えがみんな違うし、実際に真似してみてもうまく変えられませんでした。こんなことも簡単にできないのか、ってちょっと思う反面、全部がComposableになったらもっと楽ができるんじゃないかと思わなくもないです。

Viewベースの画面レイアウトからの移行は知らないことが多すぎて、時間はかかりました。次はもうちょっと早くなるかなと思いつつ、しばらく別のことがやりたい気持ちです。

一覧へ