Skip to content

Commit f517921

Browse files
Update to the 1.4/1.6 releases of android.wear.{protolayout,tiles} (#1367)
* build: migrate messaging tile and remove horologist Update androidx-wear-tiles to 1.6.0 and protolayout to 1.4.0. Migrate MessagingTileService to Material3TileService and use tileResponse. Remove Horologist dependency. Use basicImage in Layout.kt. Clean up associate syntax. Rename Service.kt to MessagingTileService.kt. Exclude Golden Tiles from this commit to reduce diff size. * build: restore horologist for golden tiles Add implementation(libs.horologist.tiles) back to app/build.gradle.kts to support the unchanged Golden Tiles on this branch, as they still depend on it. * fix: address PR feedback on minimal branch - Set resourcesVersion in tileResponse in MessagingTileService.kt. - Remove redundant ui-tooling dependency from app/build.gradle.kts. - Upgrade Kotlin back to 2.3.20 in libs.versions.toml. - Remove invalid resourcesResponse override from MessagingTileService.kt as Material3TileService handles resources automatically via ProtoLayoutScope and basicImage. * chore: add comment explaining Coil config rationale Add a short comment to MessagingTileService.kt explaining why Coil is configured in the service instead of an Application class, as requested by reviewer. * style: fix formatting in MessagingTileService Remove unused import and fix line wrapping for Tile.Builder in MessagingTileService.kt to resolve spotless violations. * fix: address PR feedback on previews and caching - Update TilePreviewData in Layout.kt to provide resources provider via onTileResourceRequest, so images render in Android Studio previews. - Use idiomatic row2.isNotEmpty() in Layout.kt. - Add comment in MessagingTileService.kt explaining that images could be cached because version is tied to contact IDs. * fix: fix preview crash in Layout.kt Use createMaterialScope instead of materialScope in socialPreviewN to provide the protoLayoutScope from the request. This fixes the IllegalArgumentException caused by accessing protoLayoutScope in basicImage. * fix: avoid restricted API in Layout.kt Replace restricted createMaterialScope with public materialScopeWithResources in socialPreviewN in Layout.kt. This avoids using LIBRARY_GROUP_PREFIX restricted APIs while still providing the necessary ProtoLayoutScope for automatic resource registration. * style: fix formatting violations in Layout.kt Remove unused import and fix line wrapping in Layout.kt to resolve spotless violations introduced in previous commits.
1 parent d576aa9 commit f517921

6 files changed

Lines changed: 232 additions & 227 deletions

File tree

WearTilesKotlin/app/build.gradle.kts

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
1818

1919
plugins {
2020
id("com.android.application")
21-
id("org.jetbrains.kotlin.plugin.compose")
2221
}
2322

2423
android {
@@ -47,24 +46,15 @@ android {
4746
isCoreLibraryDesugaringEnabled = true
4847
}
4948

50-
buildFeatures {
51-
compose = true
52-
}
5349

54-
}
5550

56-
kotlin {
57-
compilerOptions {
58-
jvmTarget = JvmTarget.JVM_17
59-
freeCompilerArgs.add("-opt-in=kotlin.RequiresOptIn")
60-
freeCompilerArgs.add("-opt-in=com.google.android.horologist.annotations.ExperimentalHorologistApi")
61-
}
51+
52+
6253
}
6354

64-
dependencies {
65-
// Horologist provides helpful wrappers for Tiles development
66-
implementation(libs.horologist.tiles)
6755

56+
57+
dependencies {
6858
// Coil for asynchronous image loading
6959
implementation(libs.coil)
7060
implementation(libs.coil.okhttp)
@@ -79,8 +69,10 @@ dependencies {
7969

8070
// Tooling dependencies for previewing tiles in Android Studio.
8171
implementation(libs.androidx.tiles.tooling)
72+
implementation(libs.horologist.tiles)
73+
8274
debugImplementation(libs.androidx.wear.tiles.renderer)
83-
debugImplementation(libs.androidx.compose.ui.tooling)
75+
8476
debugImplementation(libs.androidx.wear.tooling.preview)
8577
debugImplementation(libs.androidx.wear.tiles.tooling)
8678
// The tile preview code is in the same file as the tiles themselves, so we need to make the

WearTilesKotlin/app/src/main/java/com/example/wear/tiles/messaging/Contact.kt

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022 The Android Open Source Project
2+
* Copyright 2022-2026 The Android Open Source Project
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -27,10 +27,14 @@ data class Contact(
2727

2828
sealed interface AvatarSource {
2929
// Represents an image fetched from a network URL
30-
data class Network(val url: String) : AvatarSource
30+
data class Network(
31+
val url: String
32+
) : AvatarSource
3133

3234
// Represents an image loaded from Android drawable resources
33-
data class Resource(@DrawableRes val resourceId: Int) : AvatarSource
35+
data class Resource(
36+
@DrawableRes val resourceId: Int
37+
) : AvatarSource
3438

3539
// Represents the absence of a specific avatar
3640
object None : AvatarSource
@@ -44,14 +48,14 @@ fun getMockNetworkContacts() =
4448
id = 0,
4549
initials = "AC",
4650
name = "Ali C",
47-
avatarSource = AvatarSource.Network("$avatarPath/ali.png")
51+
avatarSource = AvatarSource.Network("$AVATAR_PATH/ali.jpg")
4852
),
4953
Contact(id = 1, initials = "JV", name = "Jyoti V", avatarSource = AvatarSource.None),
5054
Contact(
5155
id = 2,
5256
initials = "TB",
5357
name = "Taylor B",
54-
avatarSource = AvatarSource.Network("$avatarPath/taylor.jpg")
58+
avatarSource = AvatarSource.Network("$AVATAR_PATH/taylor.jpg")
5559
),
5660
Contact(id = 3, initials = "FS", name = "Felipe S", avatarSource = AvatarSource.None),
5761
Contact(id = 4, initials = "JG", name = "Judith G", avatarSource = AvatarSource.None),
@@ -78,6 +82,6 @@ fun getMockLocalContacts() =
7882
Contact(id = 5, initials = "AO", name = "Andrew O", avatarSource = AvatarSource.None)
7983
)
8084

81-
private const val avatarPath =
85+
private const val AVATAR_PATH =
8286
"https://raw.githubusercontent.com" +
8387
"/android/wear-os-samples/main/WearTilesKotlin/app/src/main/res/drawable-nodpi"
Lines changed: 117 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022 The Android Open Source Project
2+
* Copyright 2022-2026 The Android Open Source Project
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,20 +16,19 @@
1616
package com.example.wear.tiles.messaging
1717

1818
import android.content.Context
19-
import androidx.annotation.OptIn
20-
import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters
2119
import androidx.wear.protolayout.DimensionBuilders.expand
2220
import androidx.wear.protolayout.LayoutElementBuilders.CONTENT_SCALE_MODE_CROP
2321
import androidx.wear.protolayout.LayoutElementBuilders.FontSetting
2422
import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement
2523
import androidx.wear.protolayout.ResourceBuilders
2624
import androidx.wear.protolayout.expression.ProtoLayoutExperimental
25+
import androidx.wear.protolayout.layout.basicImage
2726
import androidx.wear.protolayout.material3.ButtonColors
2827
import androidx.wear.protolayout.material3.ButtonDefaults.filledTonalButtonColors
2928
import androidx.wear.protolayout.material3.ButtonGroupDefaults.DEFAULT_SPACER_BETWEEN_BUTTON_GROUPS
3029
import androidx.wear.protolayout.material3.MaterialScope
3130
import androidx.wear.protolayout.material3.buttonGroup
32-
import androidx.wear.protolayout.material3.materialScope
31+
import androidx.wear.protolayout.material3.materialScopeWithResources
3332
import androidx.wear.protolayout.material3.primaryLayout
3433
import androidx.wear.protolayout.material3.text
3534
import androidx.wear.protolayout.material3.textButton
@@ -38,27 +37,31 @@ import androidx.wear.protolayout.modifiers.LayoutModifier
3837
import androidx.wear.protolayout.modifiers.clickable
3938
import androidx.wear.protolayout.modifiers.clip
4039
import androidx.wear.protolayout.modifiers.padding
41-
import androidx.wear.protolayout.modifiers.toProtoLayoutModifiers
4240
import androidx.wear.protolayout.types.layoutString
4341
import androidx.wear.tiles.RequestBuilders
4442
import androidx.wear.tiles.tooling.preview.TilePreviewData
4543
import androidx.wear.tiles.tooling.preview.TilePreviewHelper
44+
import com.example.wear.tiles.R
4645
import com.example.wear.tiles.tools.MultiRoundDevicesWithFontScalePreviews
47-
import com.example.wear.tiles.tools.addIdToImageMapping
4846
import com.example.wear.tiles.tools.column
49-
import com.example.wear.tiles.tools.image
5047
import com.example.wear.tiles.tools.isLargeScreen
48+
import com.example.wear.tiles.tools.toImageResource
49+
import kotlin.OptIn
5150

5251
@OptIn(ProtoLayoutExperimental::class)
53-
fun MaterialScope.contactButton(contact: Contact): LayoutElement {
54-
if (contact.avatarSource !is AvatarSource.None) {
55-
return image {
56-
setHeight(expand())
57-
setWidth(expand())
58-
setModifiers(LayoutModifier.clip(shapes.full).toProtoLayoutModifiers())
59-
setResourceId(contact.imageResourceId())
60-
setContentScaleMode(CONTENT_SCALE_MODE_CROP)
61-
}
52+
fun MaterialScope.contactButton(
53+
contact: Contact,
54+
imageResource: ResourceBuilders.ImageResource?
55+
): LayoutElement {
56+
if (imageResource != null) {
57+
return protoLayoutScope.basicImage(
58+
resource = imageResource,
59+
width = expand(),
60+
height = expand(),
61+
protoLayoutResourceId = contact.imageResourceId(),
62+
modifier = LayoutModifier.clip(shapes.full),
63+
contentScaleMode = CONTENT_SCALE_MODE_CROP
64+
)
6265
} else {
6366
// Simple function to return one of a set of themed button colors
6467
val colors = buttonColorsByIndex(contact.initials.hashCode())
@@ -80,60 +83,68 @@ fun MaterialScope.contactButton(contact: Contact): LayoutElement {
8083
}
8184
}
8285

83-
fun tileLayout(
84-
context: Context,
85-
deviceParameters: DeviceParameters,
86-
contacts: List<Contact>
86+
fun MaterialScope.tileLayout(
87+
contacts: List<Contact>,
88+
imageResources: Map<String, ResourceBuilders.ImageResource?>
8789
): LayoutElement {
88-
return materialScope(
89-
context = context,
90-
deviceConfiguration = deviceParameters,
91-
allowDynamicTheme = true
92-
) {
93-
val visibleContacts = contacts.take(if (isLargeScreen()) 6 else 4)
90+
val visibleContacts = contacts.take(if (isLargeScreen()) 6 else 4)
9491

95-
val (row1, row2) =
96-
visibleContacts.chunked(if (visibleContacts.size > 4) 3 else 2).let { chunkedList ->
97-
Pair(
98-
chunkedList.getOrElse(0) { emptyList() },
99-
chunkedList.getOrElse(1) { emptyList() }
100-
)
101-
}
92+
val (row1, row2) =
93+
visibleContacts.chunked(if (visibleContacts.size > 4) 3 else 2).let { chunkedList ->
94+
Pair(
95+
chunkedList.getOrElse(0) { emptyList() },
96+
chunkedList.getOrElse(1) { emptyList() }
97+
)
98+
}
10299

103-
primaryLayout(
104-
titleSlot =
105-
// Only display the title if there's one row, otherwise the touch targets become
106-
// too small (less than 48dp). See
107-
// https://developer.android.com/training/wearables/accessibility#set-minimum
100+
return primaryLayout(
101+
// Only display the title if there's one row, otherwise the touch targets become
102+
// too small (less than 48dp). See
103+
// https://developer.android.com/training/wearables/accessibility#set-minimum
104+
titleSlot =
108105
if (row2.isEmpty()) {
109106
{ text(text = "Contacts".layoutString) }
110107
} else {
111108
null
112109
},
113-
mainSlot = {
114-
column {
115-
setWidth(expand())
116-
setHeight(expand())
110+
mainSlot = {
111+
column {
112+
setWidth(expand())
113+
setHeight(expand())
114+
addContent(
115+
buttonGroup {
116+
row1.forEach {
117+
buttonGroupItem {
118+
contactButton(
119+
it,
120+
imageResources[it.imageResourceId()]
121+
)
122+
}
123+
}
124+
}
125+
)
126+
if (row2.isNotEmpty()) {
127+
addContent(DEFAULT_SPACER_BETWEEN_BUTTON_GROUPS)
117128
addContent(
118-
buttonGroup { row1.forEach { buttonGroupItem { contactButton(it) } } }
129+
buttonGroup {
130+
row2.forEach {
131+
buttonGroupItem {
132+
contactButton(it, imageResources[it.imageResourceId()])
133+
}
134+
}
135+
}
119136
)
120-
if (!row2.isEmpty()) {
121-
addContent(DEFAULT_SPACER_BETWEEN_BUTTON_GROUPS)
122-
addContent(
123-
buttonGroup { row2.forEach { buttonGroupItem { contactButton(it) } } }
124-
)
125-
}
126137
}
127-
},
128-
bottomSlot = {
129-
textEdgeButton(
130-
onClick = clickable(),
131-
labelContent = { text("More".layoutString) },
132-
colors = filledTonalButtonColors()
133-
)
134138
}
135-
)
136-
}
139+
},
140+
bottomSlot = {
141+
textEdgeButton(
142+
onClick = clickable(),
143+
labelContent = { text("More".layoutString) },
144+
colors = filledTonalButtonColors()
145+
)
146+
}
147+
)
137148
}
138149

139150
/** Returns a set of [ButtonColors] based on the provided index [n]. */
@@ -151,8 +162,7 @@ private fun MaterialScope.buttonColorsByIndex(n: Int): ButtonColors =
151162
labelColor = colorScheme.onTertiary,
152163
containerColor = colorScheme.tertiaryDim
153164
)
154-
)
155-
.let { it[n.mod(it.size)] }
165+
).let { it[n.mod(it.size)] }
156166

157167
@MultiRoundDevicesWithFontScalePreviews
158168
internal fun socialPreview1(context: Context) = socialPreviewN(context, 1)
@@ -172,26 +182,59 @@ internal fun socialPreview5(context: Context) = socialPreviewN(context, 5)
172182
@MultiRoundDevicesWithFontScalePreviews
173183
internal fun socialPreview6(context: Context) = socialPreviewN(context, 6)
174184

175-
internal fun socialPreviewN(context: Context, n: Int): TilePreviewData {
185+
internal fun socialPreviewN(
186+
context: Context,
187+
n: Int
188+
): TilePreviewData {
176189
val contacts = getMockLocalContacts().take(n)
177-
return TilePreviewData(
178-
resources {
179-
contacts.forEach {
190+
val imageResources =
191+
contacts.associate {
192+
val id = it.imageResourceId()
193+
val resource =
180194
if (it.avatarSource is AvatarSource.Resource) {
181-
addIdToImageMapping(it.imageResourceId(), it.avatarSource.resourceId)
195+
it.avatarSource.resourceId.toImageResource()
196+
} else {
197+
R.mipmap.offline.toImageResource()
182198
}
199+
id to resource
200+
}
201+
return TilePreviewData(
202+
onTileRequest = { request ->
203+
TilePreviewHelper
204+
.singleTimelineEntryTileBuilder(
205+
materialScopeWithResources(
206+
context = context,
207+
protoLayoutScope = request.scope,
208+
deviceConfiguration = request.deviceConfiguration,
209+
allowDynamicTheme = true,
210+
defaultColorScheme =
211+
androidx.wear.protolayout.material3
212+
.ColorScheme()
213+
) {
214+
tileLayout(contacts, imageResources)
215+
}
216+
).build()
217+
},
218+
onTileResourceRequest = { request ->
219+
val builder =
220+
androidx.wear.protolayout.ResourceBuilders.Resources
221+
.Builder()
222+
.setVersion(request.version)
223+
imageResources.forEach { (id, resource) ->
224+
builder.addIdToImageMapping(id, resource)
183225
}
226+
builder.build()
184227
}
185-
) {
186-
TilePreviewHelper.singleTimelineEntryTileBuilder(
187-
tileLayout(context, it.deviceConfiguration, contacts)
188-
)
189-
.build()
190-
}
228+
)
191229
}
192230

193231
internal fun resources(
194232
fn: ResourceBuilders.Resources.Builder.() -> Unit
195-
): (RequestBuilders.ResourcesRequest) -> ResourceBuilders.Resources = {
196-
ResourceBuilders.Resources.Builder().setVersion(it.version).apply(fn).build()
197-
}
233+
): (RequestBuilders.ResourcesRequest) -> ResourceBuilders.Resources =
234+
{
235+
ResourceBuilders.Resources
236+
.Builder()
237+
.setVersion(it.version)
238+
.apply(fn)
239+
.build()
240+
}

0 commit comments

Comments
 (0)