myroute Android AppでのJetpack Compose
はじめに
初めまして、KINTOテクノロジーズ株式会社でmy routeのAndroid開発をしているソミです。my routeは'おでかけ情報'(お出かけ·交通情報)・'地図で探す' (地図検索)・'おでかけメモ' (メモ)などの様々な機能を提供して、移動の体験を豊かにしているアプリです。
現在、my routeのAndroidチームはUI/UXの改善を目指してJetpack Composeを積極的に活用しています。このUIツールキットはコードの可読性を高め、迅速かつ柔軟なUI開発を可能にします。さらに、宣言的UIアプローチにより開発プロセスを簡素化し、UIコンポーネントの再利用性を向上させます。この背景を踏まえて、my routeのAndroidアプリで使用されているJetpack Composeの機能について、いくつかの例を通じて紹介したいと思います。今回は4つの機能を紹介します。
機能紹介
1. drawRectとdrawRoundRect
Jetpack Composeでは、Canvasを利用して特定の範囲での描画を可能にします。そしてdrawRectとdrawRoundRectはCanvasの内部で定義できる図形に関連する関数です。drawRectは、指定されたオフセットとサイズで長方形を描画します。一方、drawRoundRectは、drawRectのすべての機能を含むと同時に、cornerRadiusパラメーターを追加し、角の丸みを調整することができます。
my routeには、端末のカメラでテキスト形式のクーポンコードを読み取る機能があります。コードを正確に認識するためには、テキストを認識する部分のみを透明にし、残りの部分を暗くする必要がありました。そのため、drawRectとdrawRoundedRectでUIを実装しました。
@Composable
fun TextScanCameraOverlayCanvas() {
val overlayColor = MaterialTheme.colors.onSurfaceHighEmphasis.copy(alpha = 0.7f)
...
Canvas(
modifier = Modifier.fillMaxSize()
) {
with(drawContext.canvas.nativeCanvas) {
val checkPoint = saveLayer(null, null)
drawRect(color = overlayColor)
drawRoundRect(
color = Color.Transparent,
size = Size(width = layoutWidth.toPx(), height = 79.dp.toPx()),
blendMode = BlendMode.Clear,
cornerRadius = CornerRadius(7.dp.toPx()),
topLeft = Offset(x = screenWidth.toPx(), y = rectHeight.toPx())
)
restoreToCount(checkPoint)
}
}
}
上のコードは下記のUIで実装されます。
コードを説明すると、overlayColorで色が指定されたdrawRectを使用して画面全体を暗くします。さらに、drawRoundRectを利用して角の丸い透明な四角形を作成し、テキストを認識できる領域であることを明確にしました。
2. KeyboardActionsとKeyboardOptions
KeyboardActionsとKeyboardOptionsはTextFieldコンポーネントに属するクラスです。TextFieldは入力を処理するUI要素で、KeyboardOptionsを使用して入力フィールドに現れるキーボードの種類を設定することができます。そして、KeyboardActionsは、Enterキーを押した時の動作を定義できます。
my routeのアカウント画面には、支払いのためクレジットカードの情報を保存するところがあります。カード番号を入力する部分は端末キーボードと関わっているので、KeyboardActionsとKeyboardOptionsで実装しました。
@Composable
fun CreditCardNumberInputField(
value: String,
onValueChange: (String) -> Unit,
placeholderText: String,
onNextClick: () -> Unit = {}
) {
ThinOutlinedTextField(
...
singleLine = true,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Next
),
keyboardActions = KeyboardActions(
onNext = { onNextClick() }
)
)
}
上のコードは下記のUIで実装されます。
クレジットカード番号のみを入力するため、KeyboardActionsではKeyboardTypeをNumberに設定し、入力時に次に移動できるようにImeAction.Nextを設定しました。また、KeyboardOptionsでは、キーボードの「次へ」ボタンを押したときにonNextClick()メソッドが実行されるようにしています。ちなみに、onNextClick()はFragment内で以下のように設定されています。
CreditCardNumberInputField(
...
onNextClick = {
binding.creditCardHolderName.requestFocus()
}
)
この設定により、「次へ」ボタンを押すと、クレジットカード番号の入力から次のステップ、氏名を入力する部分に進みます。
3. LazyVerticalGrid
LazyVerticalGridは、アイテムをグリッド形式で表示します。このグリッドは縦スクロールが可能で、多数のアイテム(または長さが不明なリスト)を表示します。また、画面のサイズに応じて列の数が調整され、様々な画面で効果的にアイテムを表示することができます。
my routeのおでかけ情報の「今月のイベント」セクションは現在の位置が属するエリアで開催される多くのイベント情報を提供します。このように大量のイベント情報(タイトル、画像、開催期間)は、Columnで実装するには限界があるため、LazyVerticalGridを使って数列にわたって上下にスクロール可能なコンテナにイベントアイテムを表示しました。
private const val COLUMNS = 2
LazyVerticalGrid(
columns = GridCells.Fixed(COLUMNS),
modifier = Modifier
.padding(start = 16.dp, end = 16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalArrangement = Arrangement.spacedBy(20.dp)
) {
items(eventList.size) { index ->
val item = eventList[index]
EventItem(
event = item,
modifier = Modifier.singleClickable { onItemClicked(item) }
)
}
}
上のコードは下記のUIで実装されます。著作権のため画像とタイトルは削除しました。
eventListに含まれるデータのサイズに基づいて、アイテムを一定の間隔でグリッドに表示し、絶えずイベント情報を見ることができるようになりました。
4. Drag And Drop
draggable修飾子は、画面のコンポーネント内部に何かをドラッグアンドドロップする機能です。ドラッグの流れ全体を制御する必要がある場合は、pointerInputを使います。
my routeには「myステーション」という、最大12個まで自分だけの駅またはバス停を登録する機能があります。カードリスト形式で表示されるため、一目で確認することができます。このカードリストは自由に順番を変更することができて、実装ためにはドラッグ&ドロップ操作が必要です。
itemsIndexed(stationList) { index, detail ->
val isDragged = index == lazyColumnDragDropState.draggedItemIndex
MyStationDraggableItem(
detail = detail,
draggableModifier = Modifier.pointerInput(Unit) {
detectDragGestures(
onDrag = { change, offset ->
lazyColumnDragDropState.onDrag(scrollAmount = offset.y)
lazyColumnDragDropState.scrollIfNeed()
},
onDragStart = { lazyColumnDragDropState.onDragStart(index) },
onDragEnd = { lazyColumnDragDropState.onDragInterrupted() },
onDragCancel = { lazyColumnDragDropState.onDragInterrupted() }
)
},
modifier = Modifier.graphicsLayer {
val offsetOrNull = lazyColumnDragDropState.draggedItemY.takeIf { isDragged }
translationY = offsetOrNull ?: 0f
}
.zIndex(if (isDragged) 1f else 0f)
)
val isPinned = lazyColumnDragDropState.initialDraggedItem?.index == index
if (isPinned) {
val pinContainer = LocalPinnableContainer.current
DisposableEffect(pinContainer) {
val pinnedHandle = pinContainer?.pin()
onDispose {
pinnedHandle?.release()
}
}
}
}
上のコードは下記のUIで実装されます。
drag操作はpointerInputによって検出され、detectDragGestures関数はdragイベントを処理します。アイテムをdragする際、lazyColumnDragDropStateオブジェクトのonDrag、onDragStart、onDragEnd、onDragCancelメソッドを呼び出してドラッグ状態を管理し、drag中のアイテムのY軸位置を更新して視覚的に移動する効果を提供します。また、drag中のアイテムがスクロールによって画面外に出るのを防ぐために、このコードはisPinned変数とLocalPinnableContainerを活用しています。
まとめ
簡潔に書いたため、すぐに理解できない部分もあるかもしれませんが、my routeの活用方法をご紹介させて頂きました。最初、私はJetpack Composeに慣れていなかったため、my routeのUIをXMLレイアウトから書き換えるのがやや複雑に感じることもありましたが、Jetpack Composeで書かれたコードはすぐに理解することができ、可読性やメンテナンスの面で非常に効率的な方法だと感じています。今後もさまざまなJetpack Composeを基にしてmy routeのUX改善に努めたいです。最後までお読みいただき、ありがとうございました。
関連記事 | Related Posts
We are hiring!
【プロジェクトマネージャー】モバイルアプリ開発G/大阪
モバイルアプリ開発GについてKINTOテクノロジーズにおける、モバイルアプリ開発のスペシャリストが集まっているグループです。KINTOやmy routeなどのサービスを開発・運用しているグループと協調しながら品質の高いモバイルアプリを開発し、サービスの発展に貢献する事を目標としています。
【iOS/Androidエンジニア】モバイルアプリ開発G/東京
モバイルアプリ開発GについてKINTOテクノロジーズにおける、モバイルアプリ開発のスペシャリストが集まっているグループです。KINTOやmy routeなどのサービスを開発・運用しているグループと協調しながら品質の高いモバイルアプリを開発し、サービスの発展に貢献する事を目標としています。