AndroidOS向けUI開発、いままでとこれから

AndroidOS向けのUI開発といえば、今でも最初に思い浮かぶのは、XMLでしょうか。 長い間AndroidUIの基盤を支えてきて、使い慣れていたものであり、AndroidOSアプリ開発プロジェクトを携わることがある者は誰もが通ってきた道でしょう。 AndroidStudioでは、WYSIWYG形式のデザイナーUIはありますが、あまり利用されていないと思います。

XML形式のUI設計は使い慣れればそれなりに使えますが、やはりレイアウトのViewGroupを中心に考えがちで、レイアウトの種類が多くある分、使い慣れていなければ無駄の多い設計になりがちが、大きな欠点とも言えます。 XML形式での設計によく言われるものは、なるべくフラットなレイアウトを作って、軽量な画面設計を目指そう、ですね。ただし、いくら頑張っても多少の無駄な要素を含まなければいけないケースもあります。 その他、ビュー関連のデバッグが難しく、数多くの開発者を悩ませる問題の一つになります。 これに対して、なるべくコードとの連携をやりやすくするためのViewBindingが提供され、実際のビューの現在のプロパティを確認できるLayout Inspectorも提供されますが、まだまだ難点が残っています。

これらは、命令型、または手続型プログラミングの名残で、UI要素ごとの「見せ方」に着目するものです。 そのため、要素の再利用や役割分担など、分離して作ることはなかなか難しく、同じものを繰り返しがちなレイアウトファイルが多く、ファイルの数自体も多くなります。 最近では、このような問題に対して、管理や運用が難しくなる側面も認識され、代用となるものを開発し始めました。 着目点として、命令型ではなく宣言型にし、UI要素ごとの「役割」、「用途」にします。 Androidには、サードパーティが開発されるツールキットは様々ありますが、ファーストパーティからはJetpack Composeで、この最近で正式リリースされるようになりました。

Jetpack Composeは、宣言型UIツールキットであり、UI要素ごとを関数で定義し、Composable関数といい、UI状態の管理やそれぞれのUI関連の機能も同梱され、今までのViewBindingで実現できないコードとUIの精密の連携が可能です。 その分、考え方が今までのXML形式と全く逆なので、慣れるまでには違和感があると思います。

実際にXML形式の既存プロジェクトをJetpack Composeに移行してみようと思いますので、その過程と成果から、解決できた問題、ぶつかった壁、残っていた課題まで、順を追って説明したいと思います。

準備

まず、既存プロジェクトにJetpack Composeを利用できるようにするため、以下の条件を満たす必要があります。

  • minSdkVersionが21以上
  • Javaバージョンが8以上
  • Kotlinを利用する環境 Composeバージョンによって、対応するKotlinバージョンがあるため、要注意
  • Android Studioバージョンが4以上

Jetpack Composeを利用するには、以下のGradle設定を追加する必要があります。

android {
    buildFeatures {
        // Jetpack Composeを有効化
        compose true
    }
    ...

    composeOptions {
        kotlinCompilerExtensionVersion '1.1.1'
    }
}

また、以下の依存パッケージを追加する必要があります。アニメーション、ViewModelのライブラリは任意で追加します。

dependencies {
    // Integration with activities
    implementation 'androidx.activity:activity-compose:1.4.0'
    // Compose Material Design
    implementation 'androidx.compose.material:material:1.1.1'
    // Animations
    implementation 'androidx.compose.animation:animation:1.1.1'
    // Tooling support (Previews, etc.)
    implementation 'androidx.compose.ui:ui-tooling:1.1.1'
    // Integration with ViewModels
    implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.4.1'
    // UI Tests
    androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.1.1'
}

また、テーマ関連のパッケージ、以下のパッケージを追加します。

dependencies {
    // Materialテーマを利用する場合
    implementation "com.google.android.material:compose-theme-adapter:1.1.1"

    // アプリのAppCompatテーマを利用する場合
    implementation "com.google.accompanist:accompanist-appcompat-theme:0.16.0"
}

これで環境の準備ができました。

Jetpack Composeへ移行

まず、こちらの画面を見てください。

XML形式で作成された画面
Jetpack Composeで作成された画面

元々のXMLは以下の通りです。DataBindingを利用して、viewModelを受け取ってハンドラとして繋いでいる状態です。

XMLコード
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:tools="http://schemas.android.com/tools"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        <variable
            name="viewModel"
            type="jp.co.example.upft.obrolan_kotlin.ui.main.chat.ChatBodyViewModel" />
        <import type="android.view.View" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".ui.main.chat.ChatBodyActivity">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/chats"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:clickable="true"
            android:clipToPadding="true"
            android:focusable="true"
            android:scrollbars="vertical"
            app:adapter="@{viewModel.adapter}" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="#F0F0F0"
            android:gravity="center_vertical"
            android:orientation="horizontal">

            <LinearLayout
                android:id="@+id/actionButtons"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:orientation="horizontal">

                <ImageButton
                    android:id="@+id/btnCameraOrBack"
                    android:layout_width="30dp"
                    android:layout_height="30dp"
                    android:layout_marginStart="8dp"
                    android:layout_marginEnd="4dp"
                    android:adjustViewBounds="true"
                    android:background="@android:color/transparent"
                    android:backgroundTint="@android:color/darker_gray"
                    android:contentDescription="@string/take_picture_desc"
                    android:cropToPadding="true"
                    android:scaleType="fitCenter"
                    android:src="@drawable/ic_camera"
                    android:visibility="@{viewModel.cameraAvailable ? View.VISIBLE : View.GONE}" />

                <ImageButton
                    android:id="@+id/btnPicture"
                    android:layout_width="30dp"
                    android:layout_height="30dp"
                    android:layout_marginStart="4dp"
                    android:layout_marginEnd="8dp"
                    android:adjustViewBounds="true"
                    android:background="@android:color/transparent"
                    android:backgroundTint="@android:color/darker_gray"
                    android:contentDescription="@string/add_picture_desc"
                    android:cropToPadding="true"
                    android:scaleType="fitCenter"
                    android:src="@drawable/ic_photo" />
            </LinearLayout>

            <EditText
                android:id="@+id/txtMessage"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginStart="8dp"
                android:layout_marginTop="10dp"
                android:layout_marginEnd="5dp"
                android:layout_marginBottom="10dp"
                android:layout_weight="1"
                android:background="@drawable/round_edittext"
                android:gravity="top"
                android:hint="@string/message_hint"
                android:importantForAutofill="no"
                android:inputType="textMultiLine"
                android:maxLines="5"
                android:paddingStart="@dimen/textPaddingHorizontal"
                android:paddingTop="@dimen/textPaddingVertical"
                android:paddingEnd="@dimen/textPaddingHorizontal"
                android:paddingBottom="@dimen/textPaddingVertical"
                android:scrollbars="vertical"
                android:text="@={viewModel.textBody}"
                android:textColor="@color/textBoxForeground"
                android:textColorHint="@color/textBoxHintColor"
                android:textSize="@dimen/textSize" />

            <ImageButton
                android:id="@+id/btnSend"
                android:layout_width="30dp"
                android:layout_height="30dp"
                android:layout_marginStart="5dp"
                android:layout_marginEnd="14dp"
                android:adjustViewBounds="true"
                android:background="@android:color/transparent"
                android:backgroundTint="@android:color/darker_gray"
                android:contentDescription="@string/submit"
                android:cropToPadding="true"
                android:rotation="45"
                android:scaleType="fitCenter"
                android:clickable="@{viewModel.textBody.length() > 0}"
                android:onClick="@{() -> viewModel.sendMessage()}"
                android:src="@drawable/send" />

        </LinearLayout>
    </LinearLayout>
</layout>

その過程を見てみましょう!

準備段階として、情報を整理します。

  1. まず、大まかな要素を定めます。この画面には、チャットリストとなるRecyclerView、画面下端のツールバーと、大きく2つに分けることができます。
  2. ツールバーの内訳に、ボタンを3つとテキストボックスを1つ、計3つの要素があります。
  3. RecylerViewに内蔵するアイテムをヘッダー、メッセージと2種類にわけることもできます。
  4. ここまで切り分けたら、次はそれぞれの用途と表示するためのパラメータを見ていきます。
要素データ方向用途パラメータ
RecyclerView受信のみメッセージとヘッダーを表示チャットメッセージのリスト
メッセージヘッダー受信のみメッセージを日付ごとにグループごとに表示日付
メッセージアイテム受信のみメッセージ内容を表示メッセージ内容と関連情報(送信者、向きなど)
カメラボタン送受信カメラを起動カメラの使用可能フラグ、クリックハンドラ
画像ボタン送信のみ画像選択画面を表示クリックハンドラ
送信ボタン送信のみメッセージ送信をトリガークリックハンドラ
メッセージ本文送受信送信する予定のメッセージを表示記入するテキストの状態、テキスト変更時のハンドラ
現在の構成とそれぞれの役割・パラメータ

これらの情報をもとに、UI要素の設計を行い、Activityクラスで構築します。

  1. まず、ボタン系の要素は特別なものがないため、通常なボタンで十分でしょう。
  2. 次に、ボタンらとテキストボックスからなるツールバーですが、これを一つにまとめる要素を一つ作成し、パラメータとしては内蔵する要素のパラメータを引き継ぐものとなります。
ツールバー要素(カメラボタン、画像ボタン、送信ボタン、メッセージ本文)
@Composable
fun Toolbar(
    onCameraClick: () -> Unit,
    onPictureClick: () -> Unit,
    onSendClick: () -> Unit,
    textState: State<String>,
    onTextChanged: (String) -> Unit,
) {
    // 横並びのため、Rowで整理する
    Row {
        // カメラボタン
        // Jetpack ComposeではデフォルトでMaterialDesignになるので、色々デフォルト設定を変更する必要がある(色、elevationなど)
        Button(
            colors = ButtonDefaults.buttonColors(
                backgroundColor = colorResource(id = android.R.color.transparent),
            ),
            contentPadding = PaddingValues(0.dp),
            elevation = ButtonDefaults.elevation(defaultElevation = 0.dp),
            modifier = Modifier
                .padding(horizontal = 5.dp)
                .size(30.dp),
            onClick = onCameraClick,
        ) {
            Image(
                contentDescription = "Camera button",
                painter = painterResource(id = R.drawable.ic_camera),
                contentScale = ContentScale.Fit,
            )
        }
        // 画像ボタン
        Button(
            colors = ButtonDefaults.buttonColors(
                backgroundColor = colorResource(id = android.R.color.transparent),
            ),
            contentPadding = PaddingValues(0.dp),
            elevation = ButtonDefaults.elevation(defaultElevation = 0.dp),
            modifier = Modifier
                .padding(horizontal = 5.dp)
                .size(30.dp),
            onClick = onPictureClick,
        ) {
            Image(
                contentDescription = "Picture button",
                painter = painterResource(id = R.drawable.ic_photo),
                contentScale = ContentScale.Fit,
            )
        }
        // メッセージ本文
        BasicTextField(
            value = textBody.value,
            onValueChange = onTextChanged,
            textStyle = TextStyle(fontSize = 16.sp),
            modifier = Modifier
                .align(Alignment.CenterVertically)
                .padding(horizontal = 6.dp, vertical = 5.dp)
                .background(shape = RoundedCornerShape(12.dp), color = Color.LightGray)
                .padding(horizontal = 6.dp, vertical = 5.dp)
                .verticalScroll(state = rememberScrollState())
                .weight(1f),
        )
        // 送信ボタン
        Button(
            colors = ButtonDefaults.buttonColors(
                backgroundColor = colorResource(id = android.R.color.transparent),
            ),
            contentPadding = PaddingValues(0.dp),
            elevation = ButtonDefaults.elevation(defaultElevation = 0.dp),
            modifier = Modifier
                .padding(horizontal = 5.dp)
                .size(width = 43.dp, height = 30.dp),
            onClick = onSendClick,
        ) {
            Image(
                contentDescription = "Send button",
                painter = painterResource(id = R.drawable.send),
                contentScale = ContentScale.Fit,
                modifier = Modifier.rotate(45f),
            )
        }
    }
}
  1. 次に、チャットメッセージを表示するためのアイテムやヘッダーのビューを作成します。ヘッダーから作ります。日付をフォーマットして、背景をつけるだけです。ここで、Rowを使って、画面の中央に表示するようにModifierなどを設定します。日付表示用のビューも一応作っておいて、後に作成するメッセージアイテムのビューにも流用できるようにします。
ヘッダーと日付要素
@Composable
fun TimestampText(
    formattedTimestamp: String,
    modifier: Modifier = Modifier,
    color: Color = Color.Unspecified,
) {
    Text(
        text = formattedTimestamp,
        modifier = modifier,
        color = color,
        fontSize = 12.sp,
        textAlign = TextAlign.Center,
    )
}

@Composable
fun ChatMessageDateHeader(formattedTimestamp: String) {
    Row(
        horizontalArrangement = Arrangement.Center,
        verticalAlignment = Alignment.CenterVertically,
        modifier = Modifier.fillMaxWidth()
    ) {
        Surface(
            shape = RoundedCornerShape(12.dp),
            color = Color(0xff919191),
            modifier = Modifier.padding(10.dp),
        ) {
            TimestampText(
                formattedTimestamp = formattedTimestamp,
                modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
                color = Color.White,
            )
        }
    }
}
  1. さらに、メッセージアイテムを作成します。その前に、アイテムのパーツを整理します。
    ユーザーのプロファイル画像を表示したいので、画像表示と、チャットの本文表示が必要です。また、チャット本文もテキスト以外に画像もあり得るので、これも考慮します。それ以外、送受信の日付も表示したいです。そうすると、以下の構造で再現できます。
    なお、プロファイル画像表示用のビューを通常のXMLカスタムビューを利用します。この通り、従来のビューもCompose内で利用できます。また、送受信を区別するため、受信は左揃え、送信は右揃えになるようにModifier等を設定もします。日付表示は以前作成したパーツを流用します。
メッセージアイテム要素
@Composable
fun ChatItemMessage(message: Message) {
    // メッセージの送受信によって、横のalignmentを決める
    val horizontalArrangement = if (message.isInbound) Arrangement.Start else Arrangement.End
    Row(
        horizontalArrangement = horizontalArrangement,
        modifier = Modifier
            .padding(horizontal = 8.dp)
            .padding(bottom = 6.dp)
            .fillMaxWidth()
    ) {
        // プロファイル画像は受信側にのみ表示
        if (message.isInbound) {
            AndroidView(
                modifier = Modifier
                    .padding(start = 2.dp)
                    .size(26.dp),
                factory = ::AvatarView,
                update = {
                    it.userProfile = message.fromUser
                })
        }

        Column(
            modifier = Modifier.fillMaxWidth()
        ) {
            // ユーザー名は受信側にのみ表示
            if (message.isInbound) {
                Text(
                    text = message.fromUser.name,
                    modifier = Modifier.padding(start = 8.dp, bottom = 4.dp),
                    color = Color(0xff717171),
                )
            }

            Row(
                horizontalArrangement = horizontalArrangement,
                verticalAlignment = Alignment.Bottom,
                modifier = Modifier.fillMaxWidth(),
            ) {
                // 送信側の日付は左側に
                if (message.isOutbound) {
                    TimestampText(
                        formattedTimestamp = message.timestampFormatted,
                        modifier = Modifier.padding(horizontal = 6.dp),
                        color = Color(0xff717171),
                    )
                }

                if (message.isPictureMessage) {
                    AndroidView(
                        factory = ::ImageView,
                        update = {
                            // 画像読み込みをGlideで利用する
                            Glide.with(it)
                                .load(message.chatPicsUrl)
                                .fitCenter()
                                .override(640)
                                .into(it)
                        })
                } else {
                    // テキストの背景を送受信で分ける
                    val bubbleColor = if (message.isInbound) {
                        colorResource(id = R.color.inboundBackground)
                    } else {
                        colorResource(id = R.color.outboundBackground)
                    }
                    // テキストの背景は丸角にする
                    Surface(
                        shape = RoundedCornerShape(5.dp),
                        color = bubbleColor,
                    ) {
                        Text(
                            text = message.text,
                            fontSize = 16.sp,
                            color = Color.Black,
                            modifier = Modifier
                                .padding(
                                    horizontal = 12.dp,
                                    vertical = 10.dp,
                                )
                        )
                    }
                }

                // 受信側の日付は左側に
                if (message.isInbound) {
                    TimestampText(
                        formattedTimestamp = message.timestampFormatted,
                        modifier = Modifier.padding(horizontal = 6.dp),
                        color = Color(0xff717171),
                    )
                }
            }
        }
    }
}
  1. Messageのクラスは、メッセージ内容を保持するほか、送信するユーザー情報、送受信の種類、メッセージの種類などのメタデータも保持します。大まかに以下のように定義できます。
メッセージアイテムのモーデルクラス
data class Message(
    val itemType: Message.ViewType,
    val direction: Message.Direction,
    val timestamp: Timestamp,
    val textMessage: String? = null,
    val picMessage: Uri? = null,
) {
    val isInbound get() = direction == Message.Direction.Inbound
    val isOutbound get() = direction == Message.Direction.Outbound
    val isPictureMessage get() = picMessage != null
    val timestampFormatted get() = DateFormat.getInstance(DateFormat.SHORT).format(timestamp)

    enum class Direction { Inbound, Outbound }
    enum class ViewType { Header, Content }
}
  1. これらの定義をもとに、画面の構造を作ります。ActivityのonCreateに、setContentのラムダメソッドを使ってComposeによる画面構築ができます。
ActivityのonCreateでビューを構築
override onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    // ViewModelに val messages: StateFlow<List<Message>> があります。
    val messages = viewModel.messages.collectAsState()
    // ViewModelに val textBody: LiveData<String> があります。
    val textBody = viewModel.textBody.observeAsState(initial = "")

    setContent {
        val listState = rememberLazyListState()

        // アプリのテーマを利用する
        // そのほか、MaterialDesignのテーマもある
        AppCompatTheme(context = this) {
            // 読み込み時に、メッセージを一番最後までスクロールする
            LaunchedEffect(key1 = messages.value) {
                if (messages.value.isNotEmpty()) {
                    listState.animateScrollToItem(messages.value.size - 1)
                }
            }

            Column(
                modifier = Modifier
                    .fillMaxHeight()
                    .fillMaxWidth()
            ) {
                // RecyclerView相当のリスト表示用コラム。縦にアイテムを入れ続ける。
                LazyColumn(
                    state = listState,
                    modifier = Modifier
                        .fillMaxWidth()
                        .weight(1f)
                ) {
                    messages.value.forEach { message ->
                        when (message.itemType) {
                            Message.ViewType.Header -> {
                                // stickyHeaderを利用する。普通のヘッダーだけであればitemで十分
                                stickyHeader {
                                    ChatMessageDateHeader(formattedTimestamp = message.timestampFormatted)
                                }
                            }
                            Message.ViewType.Content -> {
                                item {
                                    ChatItemMessage(message = message)
                                }
                            }
                        }
                    }
                }

                // Toolbar追加
                Toolbar(
                    onCameraClick = {
                        openCamera()
                    },
                    onPictureClick = {
                        openPictureSelection()
                    },
                    onSendClick = {
                        viewModel.sendMessage()
                    },
                    textState = textBody,
                    onTextChanged = { text ->
                        viewModel.textBody.postValue(text)
                    },
                )
            }
        }
    }
}

これでJetpack Composeによる画面の見た目(UI)と機能(ロジック)連携が出来上がりです!XML版では、DataBindingなどを駆使して作りましたが、Jetpack Composeを使うとロジックとUI部分が一緒にあってみやすいと思いますね。デバッガーも利用できるので、ビュー描画や処理のデバッグがしやすく便利です。

Jetpack Composeの考え方

XML形式との最も根本的な違いは、やはり考え方にあります。 UI要素を考えて組み込む時に、まず覚えておきたいことは、各要素の役割から考えることです。 なるべく再利用できるために、各要素の必要最低限の情報を絞り、組み込みます。

また、UI制御の考え方も重要です。 Composeで作成されるビューは不変となっているため、基本的には表示されたら終わりです。 変化を起こすには、Composable関数に渡す状態・パラメータの変化を感知して再構築を行うのみです。 一方、ビューの操作結果をイベントとしてコード層に伝達されます。 このような、データの流れが一方向のみになる概念が、Unidirectional Data Flow (UDF)といい、ComposeのUI制御の基本的な考え方の一つです。 公式サイトから拝借したグラフでUIと状態の関係性をわかりやすく表しています。

Unidirectional Data Flowの概要

XMLビューとJetpack Composeのビューの対応

Jetpack Composeが正式リリースされるだけあって、ほとんどのビューは対応済みです。 よく使用されるビューの互換表を記載します。

XML View Jetpack Compose UI
Button Button
TextView Text
EditText TextField
ImageView Image
Toolbar TopAppBar
ButtomNavigationBar ButtomNavigation
RecyclerView LazyRow / LazyColumn
LinearLayout Row / Column
ConstraintLayout ConstraintLayout

ビュー並びを制御するRowかColumnを使用しないと、XMLでFrameLayoutを使用するように、ビューが重なっているようになります。 リソースはそのまま読み込めますが、注意として、Composeでは、shapeのdrawableを利用できません。

Jetpack Composeで解決できた問題

Jetpack Composeでは、画面単位ではなく、要素単位の役割に着目するため、シンプルなアプリでは恩恵が少ないが画面が増えるほど、再利用可能な場所が増え、ビューの管理がしやすくなると考えられます。 また、ビューのテストとデバッグがしやすくなり、今までビュー関連のデバッグが大変だったのが解決できたかと思います。 不変性によって想定可能なビューの作成もでき、カスタムビューの作成もしやすい上に、Kotlinベースなので、様々なKotlinの機能を満遍なく利用できます。

Jetpack Composeへの移行時にぶつかる壁・難点

既存アプリにJetpack Composeを取り入れる際、最も難しい問題は、どうすれば既存の画面の要素をそれぞれの用途ごとに分離するかです。 根付いた今までの考え方や先入観が邪魔して、なかなか効率的に進められないことも多々あります。

特に難しいのはRecyclerViewの移行です。 今までのXML形式では、RecyclerViewを用意して、それに使用されるアイテムのレイアウトを用意して、それらを紐付けるRecyclerView.Adapterを実装して、データ更新や表示ロジックをAdapterで実装しますが、Jetpack ComposeのLazyColumn / LazyRowでは、アイテムのビューを用意する必要があるものの、データ表示や更新ロジックがビュー内に内蔵でき、専用のAdapter実装が不要になります。

状態管理とイベントハンドリングについては、UDFの考え方に慣れれば難しくないが、状態を保持する場所を選ぶには慣れが必要だと思います。 基本的には、その要素にのみ使われる状態なら内蔵してもいいですが、よっぽどない限りはその要素のパラメータとして受け取った方が無難ですね。

Jetpack Composeに移行した後に残された課題

Jetpack Composeを取り入れることができたとしても、いくつか課題がまだ残っています。 まず、ライブラリのビューを利用する場合、必ずしもJetpack Composeを対応したと限りません。 そのため、手放しでJetpack Composeのみにして、XMLを投げ出すなどができません。 互換性を保つために、Jetpack ComposeをXMLに用いるためのComposeView、またはXML形式のビューをComposable関数に用いるためのAndroidViewがそれぞれ用意されます。

また、XMLの方がネット上のガイドやチュートリアルが多くあって、初心者には親しみやいと考えられます。 Jetpack Composeは最近になって勢いがついてきましたが、まだまだ使用率が低いのも事実で、参考情報がまだ少ない。 そのため、初回の移行を行う際には、情報収集が最も時間を有することになりました。

そのほか、独自のShapes関連の実装があるため、shapeDrawableのリソースを利用できない点もあります。 そのため、shapeDrawableを利用するビューを移行する際、Jetpack ComposeのShape機能を使って作り直すか、shapeをvectorDrawableに変換するか、どちらかにしないと難しいですね。 場合によって、全く同じものを再現できないかもしれませんので、多少困惑はあります。 これも慣れればなんとかできると思いますが、如何せん情報が少なすぎるため確固たる結論は出せません。

最後に

ここまで色々なことを書いてきましたが、難しいことが多いですね。前述通り、Jetpack Composeの使用率が未だ少ないため情報不足気味ではありますが、この記事で一つ助けになれればと思い執筆しました。 古く良き友であるXMLを完全に見捨てず、将来性のあるJetpack Composeの手をとって、より良い開発者人生を送りましょう!



ギャップロを運営しているアップフロンティア株式会社では、一緒に働いてくれる仲間を随時、募集しています。 興味がある!一緒に働いてみたい!という方は下記よりご応募お待ちしております。
採用情報をみる