소프트웨어 엔지니어
이 세션에서는 전체 화면 타일에서 동적인 부분 높이 Wear 위젯으로의 진화를 기술적으로 심층 분석해 보겠습니다.
통합 Jetpack Glance API와 새로운 원격 Compose 엔진의 기술적 이점을 살펴보고, 이들이 Modern Android Development와 어떻게 원활하게 조화를 이루는지 자세히 설명합니다.
마지막으로, 최신 UX 가이드를 공유하고 실용적인 이전 전략을 살펴봄으로써 차세대 한눈에 보기 UI를 빌드하는 데 도움을 드립니다.
스크립트
안녕하세요.
제 이름은 프랭크이며 Wear OS의 한눈에 보기 환경을 이끌고 있습니다.
시계는 사용자의 하루를 함께하는 필수적인 동반자입니다. 오늘은 새로운 Wear 위젯 환경을 활용하는 방법을 살펴보겠습니다.
가장 중요한 기능을 사용자의 손목에 직접 제공하는 방법을 살펴보겠습니다. 이를 위해 Wear OS용 빌드를 여러분과 같은 Android 개발자에게 친숙하고 직관적으로 느껴지도록 설계된 최신 API를 사용할 것입니다.
오늘 이 자리에는 Google이 이 작업을 어떻게 진행하고 있는지 자세히 살펴보기 위해 제 훌륭한 동료이자 책상 친구인 유리도 함께합니다.
안녕하세요, 저는 유리 심케입니다.
저는 Wear OS팀의 엔지니어이며 주로 원격 Compose API를 담당하고 있습니다.
오늘은 원격 Compose를 간단히 소개하고 카드에서 Wear 위젯으로 이전하는 과정도 설명해 드리겠습니다.
이제 프랭크에게 다시 넘기겠습니다.
이러한 환경을 지원하기 위해 기본 UI 스택을 발전시키고 있습니다.
Wear 위젯은 원격 UI를 위한 새로운 Android 프레임워크인 원격 Compose를 기반으로 합니다.
즉, Jetpack Compose와 밀접하게 연계된 선언적 API를 사용하여 위젯과 같은 앱 외부 환경을 빌드할 수 있습니다.
원격 Compose는 위젯에 최신 그래픽과 반응형 레이아웃을 제공하는 동시에 성능과 배터리 효율성을 보장합니다.
또한 UI를 애플리케이션 프로세스에서 분리하면 위젯이 안전하게 직렬화되고 다양한 Android 기기에서 쉽게 렌더링됩니다.
즉, 최소한의 노력으로 지원되는 거의 모든 원격 표시 경로에 이 풍부한 환경을 제공할 수 있습니다.
타일에서 위젯으로 최대한 원활하게 전환할 수 있도록 두 가지 표준 컨테이너 크기(소형 및 대형)를 도입합니다.
이러한 형식은 모바일에서 위젯을 빌드하여 이미 익숙한 2x1 및 2x2 그리드 기반 위젯 형식과 밀접하게 일치하도록 설계되었습니다.
일부 하드웨어 제조업체의 Wear 7부터 Wear 위젯이 다른 앱의 위젯과 함께 세로로 스크롤되는 트레이에 표시됩니다.
예를 들어 삼성 갤럭시 워치에서 Wear 위젯은 다음과 같이 표시됩니다.
플랫폼 아키텍처는 향후 하드웨어 제조업체가 이 패턴을 채택할 때 세로 스크롤 트레이를 지원하도록 빌드되었습니다.
또한 기존 사용자를 비롯한 사용자를 뒤처지게 할까 봐 걱정할 필요가 없습니다.
Jetpack Glance Wear API는 Wear 4까지 하위 호환성을 지원하도록 설계되었습니다.
위젯 또는 타일의 가로 트레이를 지원하는 이전 기기에서는 시스템이 이를 처리합니다.
코드에서 전체 화면 모드를 지원하고 싶다고 명시하기만 하면 시스템이 대형 위젯을 가져와 이러한 기기에서 전체 화면의 기존 타일로 표시합니다.
이제 유리 님이 코드를 살펴보며 기존 카드를 Wear 위젯으로 마이그레이션하는 방법을 정확하게 보여드리겠습니다.
프랭크, 감사합니다.
새로운 Wear 위젯을 처음부터 빌드하는 경우 간단합니다.
GlanceWearWidgetService를 정의하고 원격 Compose를 사용하여 UI를 빌드하기만 하면 됩니다.
하지만 앱에 이미 프로덕션 타일이 있는 경우 서비스, UI, 데이터를 모두 이전해야 합니다.
이 전환을 최대한 원활하게 진행하기 위해 기존 사용자가 원활하게 사용할 수 있는 경로를 제공했습니다.
서비스의 경우 이중 서비스 또는 단일 서비스의 두 가지 전략 중에서 선택할 수 있습니다.
권장되는 이중 서비스를 사용하면 이전 Wear OS 기기에는 기존 TileService를 유지하고 Wear OS 7 이상에는 새 GlanceWearWidgetService를 추가할 수 있습니다.
반면 단일 서비스를 사용하면 두 프로토콜을 모두 처리하는 새로운 단일 GlanceWearWidgetService를 만들어 호환성 모드에서 실행하여 이전 기기에서 위젯을 타일로 변환할 수 있습니다.
어떤 방법을 선택하든 사용자에게 계속 도달할 수 있습니다.
앞으로 유지관리하는 코드에만 영향을 미칩니다.
또한 Android 개발자 사이트에 이 두 패턴을 사용하는 방법과 시기를 정확하게 설명하는 포괄적인 문서를 게시했습니다.
앱에 가장 적합한 전략에 대해 자세히 알아보세요.
이제 UI 레이어에 관해 알아보겠습니다.
마이그레이션할 때는 사고방식이 바뀝니다.
이전에는 카드에서 Wear 카드 전용으로 빌드된 라이브러리인 ProtoLayout을 사용했습니다.
반면 Wear 위젯은 원격 Compose를 기반으로 빌드됩니다.
원격 Compose는 Jetpack Compose UI와 동일한 기반을 공유하며 동일한 핵심 컴파일러와 런타임을 사용합니다.
또한 Foundation 및 Material 3과 같은 Compose UI 라이브러리와 매우 유사하여 친숙하게 느껴지도록 설계되었습니다.
하지만 Box, Column, Text 대신 RemoteBox, RemoteColumn, RemoteText로 바꾸면 됩니다.
이 경우 'remote'를 많이 작성하게 됩니다.
또한 워크플로 속도를 높이기 위해 위젯용으로 특별히 설계된 즉시 사용 가능한 원격 Compose 구성요소 전체 세트가 포함되어 있습니다.
즉, Figma에 있는 디자인을 비롯한 기존 Wear Material 3 디자인을 위젯에 쉽게 적용할 수 있으므로 엔지니어와 디자이너 모두에게 익숙합니다.
이러한 프레임워크가 얼마나 일치하는지 구체적인 예를 살펴보겠습니다.
Material 3 스위치 버튼 위에 자체 앱 구성요소를 빌드합니다.
왼쪽에는 앱 내부에 작성하는 표준 Wear Compose 구성요소가 있고 오른쪽에는 원격 Compose 버전이 있습니다.
// Wear Compose
@Composable
fun HistoryEraserButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
checked: Boolean = true,
onCheckedChange: (Boolean) -> Unit = {},
)// Wear RemoteCompose
@RemoteComposable
@Composable
fun RemoteHistoryEraserButton(
onClick: Action,
modifier: RemoteModifier = RemoteModifier,
checked: RemoteBoolean = true.rb,
onCheckedChange: Action = Action.Empty,
)패턴이 얼마나 유사한지 확인하세요.
하지만 원격 Compose 코드를 작성할 때는 RemoteComposable 주석을 사용합니다.
수정자, 불리언과 같은 익숙한 매개변수를 사용하는 경우 RemoteModifier 및 RemoteBoolean과 같은 상응하는 원격 Compose 유형이 있습니다.
람다 대신 작업을 사용합니다.
람다 내에서 실행할 코드를 정의하는 대신 대기 중인 인텐트 실행, 서비스 호출, 값 업데이트와 같은 작업을 선언해야 합니다.
구성요소의 본문으로 이동해 보겠습니다.
테마도 일치하는 것을 확인할 수 있습니다.
표준 Compose에서와 마찬가지로 Material 3 색 구성표를 원격 Compose 구성요소에 적용할 수 있으므로 위젯이 앱의 브랜딩과 정확히 일치합니다.
// Wear Compose
val colors = MaterialTheme.colorScheme
SplitSwitchButton(
checked = checked,
onCheckedChange = onCheckedChange,
toggleContentDescription = "Arm Eraser",
onContainerClick = onClick,
modifier = modifier.fillMaxWidth(),
colors =
SwitchButtonDefaults.splitSwitchButtonColors(
checkedContainerColor = colors.errorContainer, …
),
label = { Text("History Eraser") },
secondaryLabel = { Text("Don't touch it!") },
)// Wear RemoteCompose
val colors = RemoteMaterialTheme.colorScheme
RemoteSplitSwitchButton(
checked = checked,
onCheckedChange = onCheckedChange,
toggleContentDescription = "Arm Eraser".rs,
onContainerClick = onClick,
modifier = modifier.fillMaxWidth(),
colors =
RemoteSplitSwitchButtonDefaults.splitSwitchButtonColors(
checkedContainerColor = colors.errorContainer, …
),
label = { RemoteText("History Eraser".rs) },
secondaryLabel = { RemoteText("Don't touch it!".rs) },
)마지막으로 결과를 확인해 보겠습니다. 결과 UI는 예상한 대로 깔끔하고 반응성이 뛰어나며 애니메이션이 적용된 전환 버튼입니다.
차이를 구별하기 어렵습니다.
기기나 에뮬레이터에서 항상 실행하지 않고도 위젯 레이아웃을 설계, 반복, 확인할 수 있도록 Wear 위젯에 Compose 미리보기 지원을 추가했습니다.
위젯은 크기, 모양, 패딩, 모서리 반경과 같은 OEM별 컨테이너 제약 조건에 따라 달라지므로 표준 미리보기 주석을 기반으로 빌드하지만 이러한 구성을 삽입하기 위한 추가 매개변수 제공자가 있습니다.
@Preview(name = "Squircle Preview", device = "spec:width=1000dp,height=1000dp,dpi=320")
@Composable
fun WeatherWidgetSquirclePreview(
@PreviewParameter(SquircleAllWidgetPreviewParams::class) params: WearWidgetParams
) = WearWidgetPreview(MockWeatherWidget(65, WeatherCondition.CLOUDY), params)
@Preview(name = "Round Preview", device = "spec:width=1000dp,height=1000dp,dpi=320")
@Composable
fun WeatherWidgetRoundPreview(
@PreviewParameter(RoundAllWidgetPreviewParams::class) params: WearWidgetParams
) = WearWidgetPreview(MockWeatherWidget(65, WeatherCondition.CLOUDY), params)라이브러리는 실제 Wear OS 기기와 일치하는 사전 구성된 PreviewParameter 제공자 구현을 제공합니다.
이러한 미리보기 주석을 사용하면 다양한 하드웨어 형태에서 레이아웃을 쉽게 확인할 수 있습니다.
레이아웃을 테스트하는 것 외에도 이러한 미리보기를 사용하여 기기 내 위젯 선택 도구에 필요한 정적 이미지를 생성할 수 있습니다.
@Preview(name = "Widget Picker Preview", device = "spec:width=1000dp,height=1000dp,dpi=320")
@Composable
fun WeatherWidgetRectangularPreview(
@PreviewParameter(RectangularAllWidgetPreviewParams::class) params: WearWidgetParams
) = WearWidgetPreview(MockWeatherWidget(65, WeatherCondition.CLOUDY), params)라이브러리는 잘리지 않은 전체 직사각형 컨테이너를 에뮬레이션하는 직사각형 미리보기 도형의 매개변수를 제공합니다.
실제로 어떻게 작동하는지 확인하고 싶다면 이 API를 보여주는 새로운 위젯을 포함하도록 Horologist 미디어 샘플 앱을 업데이트했습니다.
미디어 위젯의 소형 및 대형 버전을 모두 추가하는 방법을 보여줍니다.
GitHub에서 기존 ProtoLayout 코드와 새로운 원격 Compose 구현을 나란히 비교해 보세요.
분명 마음에 드실 것입니다.
마지막으로, AI를 언급하지 않고는 이 이야기를 마무리할 수 없습니다.
이전을 가속화하기 위해 GitHub에 새로운 타일-위젯 이전 기술을 게시했습니다.
원하는 에이전트와 함께 사용하거나 Android CLI를 사용하여 Android 스튜디오에 직접 추가할 수 있습니다.
android skills add wear-widgets
android skills add --all이제 프랑수아에게 마이크를 넘겨 마무리하겠습니다.
감사합니다.
유리님, 감사합니다.
보시다시피 카드를 Wear 위젯으로 발전시키는 과정은 매우 간단합니다.
요약하자면 Jetpack Glance Wear 및 Remote Compose로 이전하면 UI를 업데이트하는 것뿐만 아니라 더 통합된 Android 위젯 생태계에 참여하게 됩니다.
이제 Jetpack Compose와 완전히 호환되는 최신 선언형 프레임워크를 사용하여 멋진 하위 호환 위젯을 빌드할 수 있습니다.
Android 개발자 사이트의 시작 가이드에서 자세한 내용을 확인하고 지금 바로 타일 이전을 시작할 수 있습니다.
손목에서 사용할 수 있는 표현력이 풍부하고 한눈에 볼 수 있는 환경을 구축해 주시기 바랍니다.
오늘 아침에 참석해 주셔서 대단히 감사합니다. 남은 컨퍼런스도 즐겁게 보내시기 바랍니다.
감사합니다.
// Wear Compose
@Composable
fun HistoryEraserButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
checked: Boolean = true,
onCheckedChange: (Boolean) -> Unit = {},
)// Wear RemoteCompose
@RemoteComposable
@Composable
fun RemoteHistoryEraserButton(
onClick: Action,
modifier: RemoteModifier = RemoteModifier,
checked: RemoteBoolean = true.rb,
onCheckedChange: Action = Action.Empty,
)// Wear Compose
val colors = MaterialTheme.colorScheme
SplitSwitchButton(
checked = checked,
onCheckedChange = onCheckedChange,
toggleContentDescription = "Arm Eraser",
onContainerClick = onClick,
modifier = modifier.fillMaxWidth(),
colors =
SwitchButtonDefaults.splitSwitchButtonColors(
checkedContainerColor = colors.errorContainer, …
),
label = { Text("History Eraser") },
secondaryLabel = { Text("Don't touch it!") },
)// Wear RemoteCompose
val colors = RemoteMaterialTheme.colorScheme
RemoteSplitSwitchButton(
checked = checked,
onCheckedChange = onCheckedChange,
toggleContentDescription = "Arm Eraser".rs,
onContainerClick = onClick,
modifier = modifier.fillMaxWidth(),
colors =
RemoteSplitSwitchButtonDefaults.splitSwitchButtonColors(
checkedContainerColor = colors.errorContainer, …
),
label = { RemoteText("History Eraser".rs) },
secondaryLabel = { RemoteText("Don't touch it!".rs) },
)@Preview(name = "Squircle Preview", device = "spec:width=1000dp,height=1000dp,dpi=320")
@Composable
fun WeatherWidgetSquirclePreview(
@PreviewParameter(SquircleAllWidgetPreviewParams::class) params: WearWidgetParams
) = WearWidgetPreview(MockWeatherWidget(65, WeatherCondition.CLOUDY), params)
@Preview(name = "Round Preview", device = "spec:width=1000dp,height=1000dp,dpi=320")
@Composable
fun WeatherWidgetRoundPreview(
@PreviewParameter(RoundAllWidgetPreviewParams::class) params: WearWidgetParams
) = WearWidgetPreview(MockWeatherWidget(65, WeatherCondition.CLOUDY), params)@Preview(name = "Widget Picker Preview", device = "spec:width=1000dp,height=1000dp,dpi=320")
@Composable
fun WeatherWidgetRectangularPreview(
@PreviewParameter(RectangularAllWidgetPreviewParams::class) params: WearWidgetParams
) = WearWidgetPreview(MockWeatherWidget(65, WeatherCondition.CLOUDY), params)android skills add wear-widgets
android skills add --all