Jetpack Compose로 앱을 조정하고 액세스 가능하도록 만들기

1. 소개

이 Codelab에서는 스마트폰, 태블릿, 폴더블을 위한 적응형 앱을 빌드하는 방법과 Jetpack Compose를 사용하여 접근성을 핵심으로 유지하는 방법을 알아봅니다. 또한 머티리얼 3 구성요소 및 테마 설정을 위한 권장사항도 알아봅니다.

자세히 살펴보기 전에 적응성과 접근성의 의미를 이해하는 것이 중요합니다.

조정 가능성

앱의 UI는 다양한 화면 크기, 방향, 폼 팩터를 처리할 수 있도록 반응해야 합니다. 적응형 레이아웃은 사용할 수 있는 화면 공간에 따라 변경됩니다. 이러한 변경은 간단한 레이아웃 조정에서 공간을 채우는 것, 각 탐색 스타일을 선택하는 것부터 추가 공간을 활용할 수 있도록 레이아웃을 완전히 변경하는 것까지 다양합니다.

접근성

Android 앱은 접근성 기능이 필요한 사용자를 비롯하여 모두에게 유용한 앱이 되어야 합니다. 앱은 다양한 대비에 맞춰 색상 대비, 연결 가능성 등 최고의 사용자 환경을 제공해야 합니다.

이 Codelab에서는 Jetpack Compose를 사용할 때 유연성과 접근성을 사용하는 방법을 알아보고 생각해 봅니다. 모든 종류의 화면에 맞게 유연성을 구현하는 방법을 보여주는 REPLY라는 애플리케이션을 빌드합니다. 적응성과 접근성이 어떻게 함께 작동하여 사용자에게 최적의 환경을 제공하는지 살펴보겠습니다.

과정 내용

  • Jetpack Compose로 모든 화면 크기를 타겟팅하도록 앱을 디자인하는 방법
  • 다양한 폴더블에 앱을 타겟팅하는 방법
  • 연결성과 접근성을 개선하기 위해 다양한 유형의 탐색을 사용하는 방법
  • 머티리얼 3 색상 체계와 동적 테마 설정을 디자인하여 최적의 접근성 환경을 제공하는 방법
  • 머티리얼 3 구성요소를 사용하여 모든 화면 크기에 최상의 환경을 제공하는 방법

필요한 항목

  • Android 스튜디오 Bumblebee.
  • Kotlin 지식
  • Compose에 관한 기본 이해 (예: @Composable 주석)
  • Compose 레이아웃 관련 기본 지식 (예: Row, Column).
  • 수정자에 대한 기본 지식 (예: Modifier.padding).

Compose에 익숙하지 않다면 이 Codelab을 완료하기 전에 Jetpack Compose 기본사항 Codelab을 먼저 들어 보세요.

빌드할 항목

  • 머티리얼 3, 동적 테마 설정 및 조정 가능한 디자인을 위한 권장사항을 사용하는 대화형 답장 이메일 클라이언트 앱입니다.

이 Codelab에서 달성할 다양한 기기 지원 쇼케이스

2 설정

샘플 앱을 다운로드하려면 다음 중 하나를 실행하세요.

또는 다음 명령어로 명령줄에서 GitHub 저장소를 클론합니다.

git clone https://github.com/googlecodelabs/android-compose-codelabs.git
cd android-compose-codelabs/ReplyAdaptabilityCodelab

툴바에서 실행 구성을 변경하여 언제든지 Android 스튜디오에서 둘 중 하나의 모듈을 실행할 수 있습니다.

b059413b0cf9113a.png

Android 스튜디오로 프로젝트 열기

  1. Welcome to Android Studio 창에서 c01826594f360d94.png Open an Existing Project를 선택합니다.
  2. [Download Location]/ReplyAdaptabilityCodelab 폴더를 선택합니다. build.gradle가 포함된 ReplyAdaptabilityCodlab 디렉터리를 선택해야 합니다.
  3. Android 스튜디오에서 프로젝트를 가져오면 startfinished 모듈을 실행할 수 있는지 테스트합니다.

시작 코드 살펴보기

시작 코드에는 패키지가 네 개 포함되어 있습니다.

  • MainActivity - ReplyApp을 시작하는 진입점 활동입니다. 이 파일을 변경합니다.
  • ui - Compose UI가 시작되는 테마, 구성요소, ReplyApp을 포함합니다. 이 패키지를 변경합니다.
  • util - 프로젝트의 도우미 코드를 포함합니다. 이 패키지는 수정할 필요가 없습니다.

이 Codelab에서는 reply 패키지의 파일에 중점을 둡니다. start 모듈에 있는 여러 파일에 익숙해져야 합니다.

ui 패키지에서 수정할 파일

  • MainActivity.kt - ReplyApp을 시작하고 접기 상태, 크기, 레이아웃 정보와 같은 필요한 정보를 전달하는 시작 지점이 될 Android 활동입니다.
  • ReplyApp.kt - 기본 앱 UI 구조는 작업하게 될 ReplyApp.kt 파일에 있습니다.
  • ReplyAppContent.kt - 앱 콘텐츠 및 목록 세부정보의 Compose 구현으로 이동합니다.

먼저 MainActivity.kt에 중점을 둡니다. start 모듈에는 이미 코드에서 작동하는 코드가 있어야 합니다.

MainActicity.kt

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)

   setContent {
       ReplyTheme {
           val uiState = viewModel.uiState.collectAsState().value
           ReplyApp(uiState)
       }
   }
}

기기 크기에서 이 앱을 실행하면 UI 요소를 변경하지 않고 최대 영역을 채우도록 동일한 화면 스트레치가 표시됩니다. 변경 없이 초기 ReplyApp 설정걸음

화면 공간을 활용하고 사용자 경험을 개선하면서도 접근성을 핵심으로 유지하도록 개선해 봅시다.

3. 앱 조정 가능

이 섹션에서는 앱을 조정할 수 있게 만드는 방법과 머티리얼 3에서 제공하는 편리한 구성요소를 소개합니다.

또한 스마트폰, 태블릿, 대형 태블릿 및 폴더블 등 타겟팅할 화면 유형과 상태를 알아봅니다.

창 크기 처리

Reply 앱을 알아보기 전에 사용자가 앱을 사용할 수 있는 시장의 규모와 기기 종류를 살펴보겠습니다.

휴대전화는 4인치에서 7인치까지 있습니다. 이후 작은 태블릿부터 태블릿에 가까운 크기의 태블릿까지 다양한 태블릿이 준비되어 있습니다.

먼저 WIndowSizeClass에 따라 다른 크기를 3개의 카테고리로 구분해 보겠습니다. 카테고리는 특수한 사례에 맞춰 앱을 최적화할 수 있는 유연성과 레이아웃 단순성 사이의 균형을 맞추기 위해 특별히 선택되었습니다. 창 크기 클래스는 항상 앱에서 사용할 수 있는 화면 공간에 의해 결정되며, 멀티태스킹이나 기타 세분화를 위한 전체 실제 화면이 아닐 수도 있습니다.

WindowSizeClass에 따른 기기 크기 분포

WindowStateUtils**.kt**

enum class WindowSize { COMPACT, MEDIUM, EXPANDED }

fun getWindowSizeClass(windowDpSize: DpSize): WindowSize = when {
   windowDpSize.width < 0.dp -> throw IllegalArgumentException("Dp value cannot be negative")
   windowDpSize.width < 600.dp -> WindowSize.COMPACT
   windowDpSize.width < 840.dp -> WindowSize.MEDIUM
   else -> WindowSize.EXPANDED
}

WindowStateUtils.kt는 크기가 저장된 Compose가 있을 때마다 새 트리에 따라 UI 트리가 다시 렌더링되도록 Compose의 저장된 상태를 가져오는 rememberWindowSizeClass(),를 제공합니다.

WindowStateUtils.kt를 참조하세요.

fun Activity.rememberWindowSizeClass(): WindowSize {
   // Get the size (in pixels) of the window
   val windowSize = rememberWindowSize()

   // Convert the window size to [Dp]
   val windowDpSize = with(LocalDensity.current) {
       windowSize.toDpSize()
   }

   // Calculate the window size class
   return getWindowSizeClass(windowDpSize)
}

조정 가능한 크기를 지원하기 위해 개발자는 rememberWindowSizeClass()를 Compose UI의 시작 부분에 추가하고 ReplyApp에 전달하기만 하면 됩니다. 이제 MainActivity.kt를 다음과 같이 변경할 수 있습니다.

MainActivity.kt

setContent {
   ReplyTheme(dynamicColor = false, darkTheme = false) {
       val windowSize = rememberWindowSizeClass()
       ReplyApp(windowSize, uiState)
   }
}

이 변경사항으로 ReplyApp에 스페이스를 올바르게 사용하는 최신 창 크기에 관한 정보가 있음을 확인할 수 있습니다.

4. 접는 상태 처리

또한 앱이 화면 크기뿐 아니라 접힌 상태 변경에 반응하도록 하려고 합니다. 접을 수 있는 상태가 많을 수 있지만 먼저 일부 케이스를 타겟팅해야 합니다. 이는 이미 유틸리티 클래스에 정의되어 있습니다.

WindowStateUtils.kt를 참조하세요.

/**
* Information about the posture of the device
*/
sealed interface DevicePosture {
   object NormalPosture : DevicePosture

   data class TableTopPosture(
       val hingePosition: Rect
   ) : DevicePosture

   data class BookPosture(
       val hingePosition: Rect
   ) : DevicePosture
}

접힌 위치에서 펼친 위치로 이동할 때 UI가 반응하도록 하는 것이 좋습니다. 또한 힌지 위치에서 텍스트 또는 다른 유용한 정보를 렌더링하고 싶지 않기 때문에 BookPosture와 TableTopPosture도 힌지 위치로 고려해야 합니다.

활동 수명 주기에서 접는 자세를 살펴보겠습니다. setContent()를 호출하기 전에 활동의 onCreate() 메서드에 이 코드를 추가합니다.

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)

    /* Flow of [DevicePosture] that emits every time there is a change in the windowLayoutInfo
    */
   val devicePostureFlow =  WindowInfoTracker.getOrCreate(this).windowLayoutInfo(this)
       .flowWithLifecycle(this.lifecycle)
       .map { layoutInfo ->
           val foldingFeature =
               layoutInfo.displayFeatures.filterIsInstance<FoldingFeature>().firstOrNull()
           when {
               isTableTopPosture(foldingFeature) ->
                   DevicePosture.TableTopPosture(foldingFeature.bounds)
               isBookPosture(foldingFeature) ->
                   DevicePosture.BookPosture(foldingFeature.bounds)
               isSeparating(foldingFeature) ->
                   DevicePosture.Separating(foldingFeature.bounds, foldingFeature.orientation)
               else -> DevicePosture.NormalPosture
           }
       }
       .stateIn(
           scope = lifecycleScope,
           started = SharingStarted.Eagerly,
           initialValue = DevicePosture.NormalPosture
       )

이제 기기 상태 흐름을 Compose 상태로 관찰하기만 하면 UI가 접힌 상태 변경에 반응하는 데 도움이 됩니다. 이러한 변경사항을 setContent()에 추가합니다.

MainActivity.kt

setContent {
   ReplyTheme(dynamicColor = false, darkTheme = false) {
       val devicePosture = devicePostureFlow.collectAsState().value
       ReplyApp(windowSize, devicePosture, uiState)
   }
}

Compose UI가 이제 기기 크기 및 접힌 상태 변경에 모두 반응할 준비가 되었습니다. 여기서 계속 다양한 상태의 UI를 디자인할 수 있습니다. 접기 상태가 변경될 때마다 UI가 이와 같이 반응하길 원합니다.

폴더블 UI 조정

5 동적 탐색

마지막 섹션에서는 UI가 크기, 구성, 접기 상태 변경에 반응하도록 했습니다. 이제 다양한 상태를 거칠 때 기기에 맞게 사용자 상호작용을 조정하는 방법을 파악해야 합니다.

사용자가 첫 번째로 상호작용할 것이므로 탐색부터 시작하겠습니다. 사용자마다 사용하는 기기 유형이 다릅니다. 몇 가지 머티리얼 탐색 Components.nav를 살펴보겠습니다.

하단 탐색

하단 탐색은 작은 크기에도 적합합니다. 엄지손가락으로 하단 하단 탐색 터치 포인트에 쉽게 도달할 수 있는 기기를 자연스럽게 장착하기 때문입니다. 기기가 컴팩트한 상태일 때나 폴더블이 있을 때마다 사용합니다.

중간 크기의 기기 또는 가로 모드 휴대전화의 대부분의 휴대전화에서 탐색 레일은 엄지손가락이 기기 왼쪽 상단에 자연스럽게 닿기 때문에 탐색이 쉽고 사용하기 쉽습니다. 탐색 레일과 함께 탐색 창을 사용하여 추가 정보를 표시할 수도 있습니다.

탐색 창을 사용하면 탐색 탭에 대한 세부정보를 쉽게 확인할 수 있으며, 태블릿이나 대형 기기를 사용할 때 쉽게 액세스할 수 있습니다. 탐색 레일 및 하단 탐색과 함께 탐색 창을 사용하고 매우 넓은 기기의 고정 탐색에 상시 탐색 창을 사용할 수 있습니다.

이제 기기 상태 및 크기가 변경될 때 여러 유형의 탐색 간에 전환하면서 사용자 상호작용과 접근성을 핵심으로 유지합니다.

앱에 동적 탐색을 추가해 보겠습니다. ReplyApp.kt을 열고 ReplyApp 컴포저블에 추가합니다.

ReplyApp.kt를 참조하세요.

/**
* This will help us select type of navigation depending on window size and
* fold state of the device.
*/
val navigationType: ReplyNavigationType

when (windowSize) {
   WindowSize.COMPACT -> {
       navigationType = ReplyNavigationType.BOTTOM_NAVIGATION
   }
   WindowSize.MEDIUM -> {
       navigationType = ReplyNavigationType.NAVIGATION_RAIL
   }
   WindowSize.EXPANDED -> {
       navigationType = if (foldingDevicePosture is DevicePosture.BookPosture) {
           ReplyNavigationType.NAVIGATION_RAIL
       } else {
           ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
       }
   }
}

탐색 창은 ReplyAppContent의 컨테이너 UI 역할을 하므로 ,이와 같이 navigationType에 따라 영구 또는 모달 탐색 창으로 래핑합니다. ,

ReplyApp.kt를 참조하세요.

if (navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER) {
   PermanentNavigationDrawer(drawerContent = {                    NavigationDrawerContent(selectedDestination) }) {
       ReplyAppContent(navigationType, contentType, replyHomeUIState)
   }
} else {
   ModalNavigationDrawer(
       drawerContent = {
           NavigationDrawerContent(
               selectedDestination,
               onDrawerClicked = {
                   scope.launch {
                       drawerState.close()
                   }
               }
           )
       },
       drawerState = drawerState
   ) {
       ReplyAppContent(navigationType, contentType, replyHomeUIState,
           onDrawerClicked = {
               scope.launch {
                   drawerState.open()
               }
           }
       )
   }
}

이제 구성이 변경될 때마다 탐색을 변경하는 데 사용할 수 있는 동적 NavigationType이 있습니다. ReplyAppContent()navigationType를 추가하여 동적으로 탐색을 만듭니다.

ReplyApp.kt를 참조하세요.

@Composable
fun ReplyAppContent(
   navigationType: ReplyNavigationType,
   contentType: ReplyContentType,
   replyHomeUIState: ReplyHomeUIState,
   onDrawerClicked: () -> Unit = {}
) {
   Row(modifier = Modifier.fillMaxSize()) {
       AnimatedVisibility(visible = navigationType == ReplyNavigationType.NAVIGATION_RAIL) {
           ReplyNavigationRail(
               onDrawerClicked = onDrawerClicked
           )
       }
       Column(modifier = Modifier
           .fillMaxSize()
           .background(MaterialTheme.colorScheme.inverseOnSurface)
       ) {
           // Reply List content

           AnimatedVisibility(visible = navigationType == ReplyNavigationType.BOTTOM_NAVIGATION) {
               ReplyBottomNavigationBar()
           }
       }
   }
}

앱을 다시 실행하여 동적 탐색 사용해 보기

앱을 다시 실행하면 화면 구성이 변경되거나 접는 기기를 펼칠 때마다 탐색이 해당 크기에 적절한 유형으로 변경됩니다.

다양한 크기의 기기에 맞게 조정 가능한 변경사항을 표시합니다.

축하합니다. 다양한 유형의 화면 크기와 상태를 지원하는 다양한 유형의 탐색 방법을 알아봤습니다.

다음 섹션에서는 동일한 목록 항목 가장자리를 가장자리로 넓히지 않고 나머지 화면 영역을 활용하는 방법을 살펴봅니다.

6. 화면 공간 사용

앱에서 작은 태블릿, 펼친 기기, 큰 태블릿 등 남은 공간을 채우기 위해 화면이 늘어나는 것을 확인할 수 있습니다. 화면 공간을 활용하여 사용자에게 더 많은 정보를 표시하려면

navigationType과 마찬가지로 목록 콘텐츠 중에서 선택하거나 목록 콘텐츠와 세부정보 콘텐츠를 동적으로 표시할 때 결정하는 데 도움이 되는 contentType를 만들 수 있습니다. 화면 상태 변경,

ReplyApp.kt를 참조하세요.

val contentType: ReplyContentType
when (windowSize) {
   WindowSize.COMPACT -> {
       contentType = ReplyContentType.LIST_ONLY
   }
   WindowSize.MEDIUM -> {
       contentType = if (foldingDevicePosture != DevicePosture.NormalPosture) {
           ReplyContentType.LIST_AND_DETAIL
       } else {
           ReplyContentType.LIST_ONLY
       }
   }
   WindowSize.EXPANDED -> {
       contentType = ReplyContentType.LIST_AND_DETAIL
   }
}

이제 이 콘텐츠 유형을 ReplyAppContent,에 전달할 수 있으며, 구성이 변경될 때마다 올바른 레이아웃에 맞게 조정됩니다. 접힌 상태와 힌지 위치를 고려하여 목록 및 세부정보 레이아웃의 위치를 결정하고 힌지 위치에 콘텐츠를 배치하지 않을 수 있습니다.

ReplyApp.kt를 참조하세요.

@Composable
fun ReplyAppContent(
   navigationType: ReplyNavigationType,
   contentType: ReplyContentType,
   replyHomeUIState: ReplyHomeUIState,
   onDrawerClicked: () -> Unit = {}
) {
   Row(modifier = Modifier.fillMaxSize()) {
       Column(modifier = Modifier
           .fillMaxSize()
           .background(MaterialTheme.colorScheme.inverseOnSurface)
       ) {
           if (contentType == ReplyContentType.LIST_AND_DETAIL) {
               ReplyListAndDetailContent(
                   replyHomeUIState = replyHomeUIState,
                   modifier = Modifier.weight(1f),
               )
           } else {
               ReplyListOnlyContent(replyHomeUIState = replyHomeUIState, modifier = Modifier.weight(1f))
           }
       }
   }
}

모든 변경사항 추가 후 ReplyApp의 최종 보기

앱을 다시 실행하여 완전히 조정 가능한 앱 사용해 보기

앱을 다시 실행하면 화면 구성이 변경되거나 접이식 기기를 펼칠 때마다 기기 상태 변경에 따라 탐색 및 화면 콘텐츠가 동적으로 변경됩니다. Jetpack Compose를 사용하면 이러한 종류의 변경사항을 선언적 패턴으로 아주 쉽게 작성할 수 있습니다.

축하합니다. 앱이 모든 종류의 기기 상태와 크기에 맞게 조정되도록 만들었습니다. 폴더블, 태블릿 또는 기타 휴대기기에서 앱을 실행하여 계속 진행하세요.

다음 몇 개의 섹션에서는 적응성을 위한 이러한 변경사항으로 인해 접근성의 구조도 배치하는 방법을 살펴보겠습니다.

7 접근성 향상

연결 가능성

접근성은 극한의 자세를 취하거나 손을 쓰지 않고도 앱과 상호작용을 시작하기 위해 기기를 탐색하거나 사용할 수 있는 기능입니다.

Reply 앱의 동적 탐색 섹션에서 화면 상태에 따라 사용할 여러 이동 모드를 추가했습니다. 하단 탐색 메뉴, 탐색 레일 및 탐색 창과 같은 머티리얼 구성요소를 사용하면 다양한 폼 팩터의 기기를 보유하는 방식에 따라 탐색이 쉽게 도달할 수 있습니다.

다양한 태블릿 크기의 탐색 레일 및 탐색 창을 보여주는 연결 가능성 데모입니다.

또한 사용자가 스레드를 쉽게 전환하고 게재위치를 변경하지 않고 왼쪽과 오른쪽 손을 사용하여 큰 기기에서 스레드를 스크롤할 수 있는 목록 및 세부정보 폼 팩터를 추가했습니다.

색상 대비

Reply 앱은 Android 12 이상의 동적 테마 설정을 지원합니다. 동적 테마는 배경화면 선택 및 기타 맞춤설정 설정을 통해 색 구성표가 생성됩니다. 동적 색상을 사용하는 제품은 최종 사용자가 경험할 수 있는 알고리즘 조합이 이러한 표준을 충족하도록 설계되었기 때문에 접근성 요구사항을 충족합니다.

자세한 내용은 동적 색상을 참고하세요.

밝은 모드 및 어두운 모드를 위한 머티리얼 3 색 구성표

또한 이 앱에서는 색상 대비 접근성 표준을 충족하도록 설계된 머티리얼 3 색 구성표도 사용합니다. 색조 팔레트 시스템은 기본적으로 모든 색 구성표에 대한 액세스를 가능하게 합니다.

머티리얼 3 테마 설정을 사용한 색상 대비 데모.

16진수 값 또는 색조가 아닌 색조를 기준으로 색상을 조합하는 것은 모든 색상 출력에 액세스할 수 있는 주요 시스템 중 하나입니다. 언제든지 적절한 기본, 보조, 3차 색상 세트를 선택한 다음 머티리얼 테마 빌더를 사용하여 밝은 변형과 어두운 변형에 모두 머티리얼 3 색 구성표를 만들어 완전한 머티리얼 3 색 구성표를 만들 수 있습니다. 생성된 변형이 이미 색상 대비 접근성 표준을 준수합니다.

Android 11 이하에서는 동적 테마 설정을 사용할 수 없는 경우 머티리얼 테마 빌더로 생성된 고정 머티리얼 3 색 구성표로 돌아갑니다.

머티리얼 테마 빌더를 사용하여 새로운 색상 테마를 사용해 볼 수 있습니다.

생성된 색상을 ui/theme/Color.kt 파일에 직접 배치하여 실제 동작을 확인할 수 있습니다.

8 축하합니다

축하합니다. 이 Codelab을 완료하고 Jetpack Compose를 사용하여 조정 가능한 접근성 앱을 설계하는 방법을 배웠습니다.

기기의 크기와 접기 상태를 확인하고 그에 따라 앱의 UI, 탐색 및 기타 기능을 업데이트하는 방법을 알아보았습니다. 또한 머티리얼 3 색 구성표와 서체를 활용하여 사용자 환경과 접근성을 개선하는 방법도 배웠습니다.

다음 단계

Compose 과정에 관한 다른 Codelab을 확인하세요.

샘플 앱

  • 샘플 앱은 Codelab에 설명된 권장사항이 통합된 여러 앱의 모음입니다.

참조 문서