1. 소개
Wear OS 카드는 사용자가 작업 처리에 필요한 정보와 동작에 쉽게 액세스할 수 있게 해 줍니다. 사용자는 시계 화면에서 간단히 스와이프하여 최근 일기예보를 찾거나 타이머를 시작할 수 있습니다.
카드는 자체 애플리케이션 컨테이너에서 실행되지 않고 시스템 UI의 일부로 실행됩니다. Service를 사용하여 카드의 레이아웃과 콘텐츠를 설명합니다. 그러면 시스템 UI가 필요에 따라 카드를 렌더링합니다.
실행할 작업
최근 대화를 표시하는 메시지 앱의 카드를 빌드합니다. 이 표시 영역에서 사용자는 다음 3가지 일반적인 작업 중 하나로 바로 이동할 수 있습니다.
- 대화 열기
- 대화 검색
- 새 메시지 쓰기
학습할 내용
이 Codelab에서는 다음을 포함하여 자체 Wear OS 카드를 작성하는 방법을 알아봅니다.
TileService
를 만드는 방법- 기기에서 카드를 테스트하는 방법
- Android 스튜디오에서 카드 UI를 미리 보는 방법
- 카드 UI를 개발하는 방법
- 이미지를 추가하는 방법
- 상호작용 처리
기본 요건
- Kotlin에 관한 기본적 이해
2. 설정
이 단계에서는 환경을 설정하고 시작 프로젝트를 다운로드해 보겠습니다.
필요한 항목
- Android 스튜디오 Koala 기능 출시 | 2024.1.2 Canary 1 및 이후 버전
- Wear OS 기기 또는 에뮬레이터
Wear OS 사용에 익숙하지 않은 경우 시작하기 전에 이 빠른 가이드를 읽어보는 것이 좋습니다. Wear OS 에뮬레이터 설정에 관한 안내가 포함되어 있으며 시스템 탐색 방법을 설명합니다.
코드 다운로드
git이 설치되어 있으면 아래 명령어를 실행하여 이 저장소의 코드를 클론하면 됩니다.
git clone https://github.com/android/codelab-wear-tiles.git cd codelab-wear-tiles
git이 없는 경우 다음 버튼을 클릭하여 이 Codelab을 위한 모든 코드를 다운로드할 수 있습니다.
Android 스튜디오에서 프로젝트 열기
'Welcome to Android Studio' 창에서 Open an Existing Project 또는 File > Open을 선택하고 [다운로드 위치] 폴더를 선택합니다.
3. 기본 카드 만들기
카드의 진입점은 카드 서비스입니다. 이 단계에서는 카드 서비스를 등록하고 카드의 레이아웃을 정의합니다.
HelloWorldTileService
TileService
를 구현하는 클래스는 다음 두 메서드를 지정해야 합니다.
onTileResourcesRequest(requestParams: ResourcesRequest): ListenableFuture<Resources>
onTileRequest(requestParams: TileRequest): ListenableFuture<Tile>
첫 번째 메서드는 문자열 ID를 카드에서 사용할 이미지 리소스에 매핑하는 Resources
객체를 반환합니다.
두 번째 메서드는 레이아웃을 포함하여 카드에 관한 설명을 반환합니다. 여기에서 카드의 레이아웃과 데이터가 이 레이아웃에 결합되는 방법을 정의합니다.
start
모듈에서 HelloWorldTileService.kt
를 엽니다. 모든 변경사항은 이 모듈에 적용됩니다. 이 Codelab의 결과를 확인하려는 경우 finished
모듈도 있습니다.
HelloWorldTileService
는 Horologist 카드 라이브러리의 Kotlin 코루틴 친화적인 래퍼 SuspendingTileService
를 확장합니다. Horologist는 Google에서 제공하는 라이브러리 그룹으로, 개발자에게 일반적으로 필요하지만 아직 Jetpack에서 사용할 수 없는 기능을 제공하여 Wear OS 개발자를 돕는 것을 목표로 합니다.
SuspendingTileService
는 TileService
의 코루틴 버전 함수인 정지 함수 두 개를 제공합니다.
suspend resourcesRequest(requestParams: ResourcesRequest): Resources
suspend tileRequest(requestParams: TileRequest): Tile
코루틴에 관한 자세한 내용은 Android의 Kotlin 코루틴 문서를 참고하세요.
HelloWorldTileService
가 아직 완료되지 않았습니다. 매니페스트에 서비스를 등록해야 하며 tileLayout
의 구현도 제공해야 합니다.
카드 서비스 등록
매니페스트에 카드 서비스가 등록되면 사용자가 추가할 수 있도록 카드 목록에 표시됩니다.
<application>
요소 내에 <service>
를 추가합니다.
start/src/main/AndroidManifest.xml
<service
android:name="com.example.wear.tiles.hello.HelloWorldTileService"
android:icon="@drawable/ic_waving_hand_24"
android:label="@string/hello_tile_label"
android:description="@string/hello_tile_description"
android:exported="true"
android:permission="com.google.android.wearable.permission.BIND_TILE_PROVIDER">
<intent-filter>
<action android:name="androidx.wear.tiles.action.BIND_TILE_PROVIDER" />
</intent-filter>
<!-- The tile preview shown when configuring tiles on your phone -->
<meta-data
android:name="androidx.wear.tiles.PREVIEW"
android:resource="@drawable/tile_hello" />
</service>
카드가 처음 로드될 때, 또는 카드 로드 중 오류가 발생하면, 아이콘과 라벨이 자리표시자로 사용됩니다. 끝에 있는 meta-data는 사용자가 카드를 추가할 때 캐러셀에 표시되는 미리보기 이미지를 정의합니다.
카드 레이아웃 정의
HelloWorldTileService
에는 TODO()
가 본문으로 있는 tileLayout
이라는 함수가 있습니다. 이제 이 함수를 카드의 레이아웃을 정의하고 데이터를 결합하는 구현으로 대체해 보겠습니다.
start/src/main/java/com/example/wear/tiles/hello/HelloWorldTileService.kt
private fun tileLayout(): LayoutElement {
val text = getString(R.string.hello_tile_body)
return LayoutElementBuilders.Box.Builder()
.setVerticalAlignment(LayoutElementBuilders.VERTICAL_ALIGN_CENTER)
.setWidth(DimensionBuilders.expand())
.setHeight(DimensionBuilders.expand())
.addContent(
LayoutElementBuilders.Text.Builder()
.setText(text)
.build()
)
.build()
}
기본 정렬을 실행할 수 있도록 Text
요소를 만들고 Box
내부에 설정합니다.
첫 번째 Wear OS 카드를 만들었습니다. 이 카드를 설치하고 어떻게 표시되는지 살펴보겠습니다.
4. 기기에서 카드 테스트
실행 구성 드롭다운에서 시작 모듈을 선택하면 기기나 에뮬레이터에 앱(start
모듈)을 설치하고 사용자와 마찬가지로 카드를 수동으로 설치할 수 있습니다.
여기서는 Android 스튜디오 Dolphin에서 도입된 기능인 Direct Surface Launch를 사용하여 새로운 실행 구성을 만들어 Android 스튜디오에서 바로 카드를 실행해 보겠습니다. 상단 패널의 드롭다운에서 'Edit Configurations...'를 선택합니다.
'Add new configuration' 버튼을 클릭하고 'Wear OS Tile'을 선택합니다. 구체적인 이름을 추가한 다음 Tiles_Code_Lab.start
모듈과 HelloWorldTileService
카드를 선택합니다.
'OK'를 눌러 종료합니다.
Direct Surface Launch를 사용하면 Wear OS 에뮬레이터나 실제 기기에서 카드를 빠르게 테스트할 수 있습니다. 'HelloTile'을 실행하여 테스트해 보세요. 아래 스크린샷과 같이 표시됩니다.
5. 메시지 카드 빌드
빌드할 메시지 카드는 실제 카드와 더 비슷합니다. HelloWorld 예와 달리 이 예는 로컬 저장소에서 데이터를 로드하고, 표시할 이미지를 네트워크에서 가져오며, 카드에서 직접 앱을 여는 상호작용을 처리합니다.
MessagingTileService
MessagingTileService
는 앞에서 본 SuspendingTileService
클래스를 확장합니다.
이 예와 이전 예의 주요 차이점은 이제 저장소에서 데이터를 관찰하고 네트워크에서 이미지 데이터도 가져온다는 것입니다.
MessagingTileRenderer
MessagingTileRenderer
는 SingleTileLayoutRenderer
클래스(Horologist 카드의 또 다른 추상화)를 확장합니다. MessagingTileRenderer는 완전히 동기식입니다. 상태는 렌더기 함수로 전달되므로 테스트와 Android 스튜디오 미리보기에서 더 쉽게 사용할 수 있습니다.
다음 단계에서는 카드의 Android 스튜디오 미리보기를 추가하는 방법을 살펴봅니다.
6. 미리보기 함수 추가
Jetpack Tiles 라이브러리 버전 1.4(현재 알파 버전)에서 출시된 카드 미리보기 함수를 사용하여 Android 스튜디오에서 카드 UI를 미리 볼 수 있습니다. 이렇게 하면 UI를 개발할 때 피드백 루프가 단축되어 개발 속도가 빨라집니다.
파일 끝에 MessagingTileRenderer
의 카드 미리보기를 추가합니다.
start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt
@Preview(device = WearDevices.SMALL_ROUND)
@Preview(device = WearDevices.LARGE_ROUND)
fun messagingTileLayoutPreview(context: Context): TilePreviewData {
return TilePreviewData { request ->
MessagingTileRenderer(context).renderTimeline(
MessagingTileState(knownContacts),
request
)
}
}
@Composable
주석은 제공되지 않습니다. 카드는 컴포저블 함수와 동일한 미리보기 UI를 사용하지만 Compose를 사용하지 않으며 컴포저블이 아닙니다.
'Split' 편집기 모드를 사용하여 카드의 미리보기를 확인합니다.
다음 단계에서는 Tiles Material을 사용하여 레이아웃을 업데이트합니다.
7. Tiles Material 추가
Tiles Material은 사전 빌드된 Material 구성요소와 레이아웃을 제공하므로 Wear OS용 최신 Material Design을 수용하는 카드를 만들 수 있습니다.
Tiles Material 종속 항목을 build.gradle
파일에 추가합니다.
start/build.gradle
implementation "androidx.wear.protolayout:protolayout-material:$protoLayoutVersion"
렌더기 파일 하단에 버튼의 코드를 추가하고 미리보기도 추가합니다.
start/src/main/java/MessagingTileRenderer.kt
private fun searchLayout(
context: Context,
clickable: ModifiersBuilders.Clickable,
) = Button.Builder(context, clickable)
.setContentDescription(context.getString(R.string.tile_messaging_search))
.setIconContent(MessagingTileRenderer.ID_IC_SEARCH)
.setButtonColors(ButtonColors.secondaryButtonColors(MessagingTileTheme.colors))
.build()
연락처 레이아웃을 빌드하는 것과 비슷한 작업도 할 수 있습니다.
start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt
private fun contactLayout(
context: Context,
contact: Contact,
clickable: ModifiersBuilders.Clickable,
) = Button.Builder(context, clickable)
.setContentDescription(contact.name)
.apply {
if (contact.avatarUrl != null) {
setImageContent(contact.imageResourceId())
} else {
setTextContent(contact.initials)
setButtonColors(ButtonColors.secondaryButtonColors(MessagingTileTheme.colors))
}
}
.build()
구성요소만 Tiles Material에 포함되어 있는 것은 아닙니다. 일련의 중첩된 열과 행을 사용하는 대신 Tiles Material의 레이아웃을 사용하여 원하는 모양을 빠르게 만들 수 있습니다.
여기서는 PrimaryLayout
과 MultiButtonLayout
을 사용하여 연락처 4개와 검색 버튼을 정렬할 수 있습니다. 다음 레이아웃으로 MessagingTileRenderer
의 messagingTileLayout()
함수를 업데이트합니다.
start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt
private fun messagingTileLayout(
context: Context,
deviceParameters: DeviceParametersBuilders.DeviceParameters,
state: MessagingTileState
) = PrimaryLayout.Builder(deviceParameters)
.setResponsiveContentInsetEnabled(true)
.setContent(
MultiButtonLayout.Builder()
.apply {
// In a PrimaryLayout with a compact chip at the bottom, we can fit 5 buttons.
// We're only taking the first 4 contacts so that we can fit a Search button too.
state.contacts.take(4).forEach { contact ->
addButtonContent(
contactLayout(
context = context,
contact = contact,
clickable = emptyClickable
)
)
}
}
.addButtonContent(searchLayout(context, emptyClickable))
.build()
)
.build()
MultiButtonLayout
은 버튼을 최대 7개 지원하고 적절한 간격으로 배치합니다.
messagingTileLayout()
함수에 'New' CompactChip을 PrimaryLayout의 'primary' 칩으로 추가하겠습니다.
start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt
.setPrimaryChipContent(
CompactChip.Builder(
/* context = */ context,
/* text = */ context.getString(R.string.tile_messaging_create_new),
/* clickable = */ emptyClickable,
/* deviceParameters = */ deviceParameters
)
.setChipColors(ChipColors.primaryChipColors(MessagingTileTheme.colors))
.build()
)
다음 단계에서는 누락된 이미지를 수정합니다.
8. 이미지를 추가하는 방법
카드는 크게 두 가지 요소로 구성됩니다. 바로 (문자열 ID로 리소스를 참조하는) 레이아웃 요소와 리소스 자체(이미지일 수 있음)입니다.
로컬 이미지를 사용할 수 있도록 하는 것은 간단한 작업입니다. Android 드로어블 리소스를 직접 사용할 수는 없지만 Horologist에서 제공하는 편의 함수를 사용하여 필요한 형식으로 간단하게 변환할 수 있습니다. 그런 다음 addIdToImageMapping
함수를 사용하여 이미지를 리소스 식별자에 연결합니다. 예:
start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt
addIdToImageMapping(
ID_IC_SEARCH,
drawableResToImageResource(R.drawable.ic_search_24)
)
원격 이미지의 경우 Kotlin 코루틴 기반 이미지 로더인 Coil을 사용하여 네트워크를 통해 로드합니다.
다음은 이미 작성된 코드입니다.
start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileService.kt
override suspend fun resourcesRequest(requestParams: ResourcesRequest): Resources {
val avatars = imageLoader.fetchAvatarsFromNetwork(
context = this@MessagingTileService,
requestParams = requestParams,
tileState = latestTileState()
)
return renderer.produceRequestedResources(avatars, requestParams)
}
카드 렌더기는 완전히 동기식이므로 카드 서비스가 네트워크에서 비트맵을 가져옵니다. 이전과 마찬가지로 이미지 크기에 따라 WorkManager를 사용하여 미리 이미지를 가져오는 것이 더 적절할 수 있지만 이 Codelab에서는 이미지를 직접 가져옵니다.
avatars
맵(Contact
에서 Bitmap
으로)을 리소스의 '상태'로 렌더기에 전달합니다. 이제 렌더기는 이러한 비트맵을 카드의 이미지 리소스로 변환할 수 있습니다.
이 코드도 이미 작성되어 있습니다.
start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt
override fun ResourceBuilders.Resources.Builder.produceRequestedResources(
resourceState: Map<Contact, Bitmap>,
deviceParameters: DeviceParametersBuilders.DeviceParameters,
resourceIds: List<String>
) {
addIdToImageMapping(
ID_IC_SEARCH,
drawableResToImageResource(R.drawable.ic_search_24)
)
resourceState.forEach { (contact, bitmap) ->
addIdToImageMapping(
/* id = */ contact.imageResourceId(),
/* image = */ bitmap.toImageResource()
)
}
}
서비스가 비트맵을 가져오고 렌더기가 이러한 비트맵을 이미지 리소스로 변환한다면 카드에 이미지가 표시되지 않는 이유는 무엇일까요?
이유는 다음과 같습니다. 기기(인터넷에 액세스할 수 있음)에서 카드를 실행하면 이미지가 실제로 로드됩니다. TilePreviewData()
에 리소스를 전달하지 않았으므로 문제는 미리보기에만 있습니다.
실제 카드의 경우 네트워크에서 비트맵을 가져오고 이를 다른 연락처에 매핑하지만, 미리보기와 테스트의 경우에는 네트워크에 연결할 필요가 전혀 없습니다.
두 가지를 변경해야 합니다. 첫째, Resources
객체를 반환하는 previewResources()
함수를 만듭니다.
start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt
private fun previewResources() = Resources.Builder()
.addIdToImageMapping(ID_IC_SEARCH, drawableResToImageResource(R.drawable.ic_search_24))
.addIdToImageMapping(knownContacts[1].imageResourceId(), drawableResToImageResource(R.drawable.ali))
.addIdToImageMapping(knownContacts[2].imageResourceId(), drawableResToImageResource(R.drawable.taylor))
.build()
둘째, messagingTileLayoutPreview()
를 업데이트하여 리소스를 전달합니다.
start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt
@Preview(device = WearDevices.SMALL_ROUND)
@Preview(device = WearDevices.LARGE_ROUND)
fun messagingTileLayoutPreview(context: Context): TilePreviewData {
return TilePreviewData({ previewResources() }) { request ->
MessagingTileRenderer(context).renderTimeline(
MessagingTileState(knownContacts),
request
)
}
}
이제 미리보기를 새로고침하면 이미지가 표시됩니다.
다음 단계에서는 각 요소에 대한 클릭을 처리합니다.
9. 상호작용 처리
카드로 할 수 있는 가장 유용한 작업 중 하나는 중요한 사용자 여정에 대한 바로가기를 제공하는 것입니다. 이는 단순히 앱을 여는 앱 런처와는 다릅니다. 여기서는 앱의 특정 화면에 컨텍스트 바로가기를 제공할 공간이 있습니다.
지금까지는 칩과 각 버튼에 emptyClickable
을 사용했습니다. 이는 대화형이 아닌 미리보기에는 적합하지만 이제는 요소의 작업을 추가하는 방법을 살펴보겠습니다.
'ActionBuilders' 클래스의 두 빌더는 클릭 가능한 작업(LoadAction
및 LaunchAction
)을 정의합니다.
LoadAction
LoadAction
은 사용자가 요소를 클릭할 때 카드 서비스에서 로직을 실행(예: 카운터 증가)하려는 경우에 사용할 수 있습니다.
.setClickable(
Clickable.Builder()
.setId(ID_CLICK_INCREMENT_COUNTER)
.setOnClick(ActionBuilders.LoadAction.Builder().build())
.build()
)
)
이를 클릭하면 onTileRequest
가 서비스(SuspendingTileService
의 tileRequest
)에서 호출되므로 카드 UI를 새로고침할 수 있습니다.
override suspend fun tileRequest(requestParams: TileRequest): Tile {
if (requestParams.state.lastClickableId == ID_CLICK_INCREMENT_COUNTER) {
// increment counter
}
// return an updated tile
}
LaunchAction
LaunchAction
은 활동을 시작하는 데 사용할 수 있습니다. MessagingTileRenderer
에서 검색 버튼의 클릭 가능 항목을 업데이트해 보겠습니다.
검색 버튼은 MessagingTileRenderer
의 searchLayout()
함수로 정의됩니다. 이미 Clickable
을 매개변수로 사용하지만 지금까지는 버튼이 클릭되면 아무 작업도 하지 않는 노옵(no-op) 구현인 emptyClickable
을 전달했습니다.
실제 클릭 작업을 전달하도록 messagingTileLayout()
을 업데이트해 보겠습니다.
- 새 매개변수
searchButtonClickable
(ModifiersBuilders.Clickable
유형)을 추가합니다. - 이 매개변수를 기존
searchLayout()
함수에 전달합니다.
start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt
private fun messagingTileLayout(
context: Context,
deviceParameters: DeviceParametersBuilders.DeviceParameters,
state: MessagingTileState,
searchButtonClickable: ModifiersBuilders.Clickable
...
.addButtonContent(searchLayout(context, searchButtonClickable))
방금 새 매개변수(searchButtonClickable
)를 추가했으므로 messagingTileLayout
을 호출하는 renderTile
도 업데이트해야 합니다. launchActivityClickable()
함수를 사용하여 새 클릭 가능 항목을 만들어 openSearch()
ActionBuilder
를 작업으로 전달합니다.
start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt
override fun renderTile(
state: MessagingTileState,
deviceParameters: DeviceParametersBuilders.DeviceParameters
): LayoutElementBuilders.LayoutElement {
return messagingTileLayout(
context = context,
deviceParameters = deviceParameters,
state = state,
searchButtonClickable = launchActivityClickable("search_button", openSearch())
)
}
launchActivityClickable
을 열고 이러한 함수(이미 정의됨)가 어떻게 작동하는지 확인합니다.
start/src/main/java/com/example/wear/tiles/messaging/tile/ClickableActions.kt
internal fun launchActivityClickable(
clickableId: String,
androidActivity: ActionBuilders.AndroidActivity
) = ModifiersBuilders.Clickable.Builder()
.setId(clickableId)
.setOnClick(
ActionBuilders.LaunchAction.Builder()
.setAndroidActivity(androidActivity)
.build()
)
.build()
LoadAction
과 매우 유사합니다. 주요 차이점은 setAndroidActivity
가 호출된다는 점입니다. 같은 파일에 다양한 ActionBuilder.AndroidActivity
예가 있습니다.
이 클릭 가능 항목에 사용되는 openSearch
의 경우 setMessagingActivity
를 호출하고 문자열 추가 항목을 전달하여 어떤 버튼 클릭이 발생했는지 식별합니다.
start/src/main/java/com/example/wear/tiles/messaging/tile/ClickableActions.kt
internal fun openSearch() = ActionBuilders.AndroidActivity.Builder()
.setMessagingActivity()
.addKeyToExtraMapping(
MainActivity.EXTRA_JOURNEY,
ActionBuilders.stringExtra(MainActivity.EXTRA_JOURNEY_SEARCH)
)
.build()
...
internal fun ActionBuilders.AndroidActivity.Builder.setMessagingActivity(): ActionBuilders.AndroidActivity.Builder {
return setPackageName("com.example.wear.tiles")
.setClassName("com.example.wear.tiles.messaging.MainActivity")
}
카드를 실행하고('hello' 카드가 아닌 'messaging' 카드를 실행해야 함) 검색 버튼을 클릭합니다. MainActivity
가 열리고 텍스트가 표시되어 검색 버튼이 클릭되었음을 확인할 수 있습니다.
다른 버튼의 작업을 추가하는 것도 비슷합니다. ClickableActions
에는 필요한 함수가 포함되어 있습니다. 힌트가 필요하다면 finished
모듈에서 MessagingTileRenderer
를 확인하세요.
10. 축하합니다
축하합니다. 지금까지 Wear OS용 카드를 빌드하는 방법을 알아보았습니다.
다음 단계는 무엇일까요?
자세한 내용은 GitHub의 Golden Tiles 구현, Wear OS 카드 가이드, 디자인 가이드라인을 참고하세요.