Jetpack Compose로 적응형 앱 빌드

1. 소개

이 Codelab에서는 스마트폰, 태블릿, 폴더블용 적응형 앱을 빌드하는 방법과 Jetpack Compose를 사용하여 도달 가능성을 개선하는 방법을 알아봅니다. Material 3 구성요소 및 테마 사용에 관한 권장사항도 알아봅니다.

본격적으로 시작하기 전에 적응성의 의미를 이해하는 것이 중요합니다.

적응성

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

자세한 내용은 적응형 디자인을 참고하세요.

이 Codelab에서는 Jetpack Compose를 사용할 때 적응성을 사용하고 고려하는 방법을 살펴봅니다. 모든 화면에 맞게 적응성을 구현하는 방법과 사용자에게 최적의 환경을 제공하기 위해 적응성과 도달 가능성이 어떻게 함께 작동하는지 보여주는 Reply라는 애플리케이션을 빌드합니다.

학습할 내용

  • Jetpack Compose를 사용하여 모든 창 크기를 타겟팅하도록 앱을 설계하는 방법
  • 다양한 폴더블을 위해 앱을 타겟팅하는 방법
  • 다양한 유형의 탐색을 사용하여 도달 가능성과 접근성을 개선하는 방법
  • Material 3 구성요소를 사용하여 모든 창 크기에 맞는 최적의 환경을 제공하는 방법

필요한 항목

이 Codelab에서는 다양한 유형의 기기와 창 크기 간에 전환할 수 있는 크기 조절 가능한 에뮬레이터를 사용합니다.

스마트폰, 펼친 상태의 폴더블, 태블릿, 데스크톱 옵션이 있는 크기 조절 가능한 에뮬레이터

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

빌드할 항목

  • 적응형 디자인, 다양한 Material 탐색, 최적의 화면 공간 사용을 위한 권장사항을 사용하는 대화형 이메일 클라이언트 앱입니다.

이 Codelab에서 달성할 다양한 기기 지원 사례

2. 설정

이 Codelab의 코드를 가져오려면 명령줄에서 GitHub 저장소를 클론합니다.

git clone https://github.com/android/codelab-android-compose.git
cd codelab-android-compose/AdaptiveUiCodelab

또는 저장소를 ZIP 파일로 다운로드할 수도 있습니다.

main 브랜치의 코드로 시작하고 각자의 속도에 맞게 Codelab을 단계별로 따라하는 것이 좋습니다.

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

  1. Welcome to Android Studio 창에서 c01826594f360d94.pngOpen an Existing Project를 선택합니다.
  2. <Download Location>/AdaptiveUiCodelab 폴더를 선택합니다(build.gradle이 포함된 AdaptiveUiCodelab 디렉터리를 선택해야 함).
  3. Android 스튜디오에서 프로젝트를 가져오면 main 브랜치를 실행할 수 있는지 테스트합니다.

시작 코드 살펴보기

main 브랜치 코드에는 ui 패키지가 포함되어 있습니다. 이 패키지에 있는 다음 파일로 작업하게 됩니다.

  • MainActivity.kt: 앱을 시작하는 진입점 활동입니다.
  • ReplyApp.kt: 기본 화면 UI 컴포저블이 포함되어 있습니다.
  • ReplyHomeViewModel.kt: 앱 콘텐츠의 데이터와 UI 상태를 제공합니다.
  • ReplyListContent.kt: 목록 및 세부정보 화면을 제공하기 위한 컴포저블이 포함되어 있습니다.

먼저 MainActivity.kt부터 살펴보겠습니다.

MainActivity.kt

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

    setContent {
        ReplyTheme {
            val uiState by viewModel.uiState.collectAsStateWithLifecycle()
            ReplyApp(
                replyHomeUIState = uiState,
                onEmailClick = viewModel::setSelectedEmail
            )
        }
    }
}

크기 조절 가능한 에뮬레이터에서 이 앱을 실행하고 스마트폰 또는 태블릿과 같은 다른 기기 유형을 사용해 보면 UI가 화면 공간을 활용하거나 도달 가능성 인체공학을 제공하는 대신 주어진 공간으로만 확장됩니다.

스마트폰의 초기 화면

태블릿의 초기 확대 뷰

화면 공간을 활용하고 사용성을 높이며 전반적인 사용자 환경을 개선하기 위해 앱을 업데이트합니다.

3. 앱을 적응형으로 만들기

이 섹션에서는 앱을 적응형으로 만드는 것의 의미와 이를 쉽게 하기 위해 Material 3이 제공하는 구성요소를 소개합니다. 또한 스마트폰, 태블릿, 대형 태블릿, 폴더블 등 타겟팅할 화면 유형과 상태를 다룹니다.

먼저 창 크기, 접힘 상태, 다양한 탐색 옵션 유형에 관한 기본사항을 살펴봅니다. 그런 다음 앱에서 이러한 API를 사용하여 앱의 적응성을 높일 수 있습니다.

창 크기

Android 기기는 스마트폰에서 폴더블, 태블릿, ChromeOS 기기에 이르기까지 다양한 모양과 크기로 제공됩니다. 가능한 한 많은 창 크기를 지원하려면 UI가 반응형 및 적응형이어야 합니다. 앱의 UI를 변경하기 위한 올바른 임곗값을 찾을 수 있도록 창 크기 클래스라는 사전 정의된 크기 클래스 (소형, 중형, 확장형)로 기기를 분류하는 데 도움이 되는 중단점 값을 정의했습니다. 이는 반응형 및 적응형 애플리케이션 레이아웃을 디자인, 개발, 테스트하는 데 도움이 되는 체계적인 표시 영역 중단점입니다.

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

소형, 중형, 확장 후 너비를 위한 WindowWidthSizeClass

소형, 중형, 확장 후 높이의 WindowHeightSizeClass

너비와 높이는 모두 개별적으로 분류되므로 언제라도 앱에는 두 가지 창 크기 클래스(너비 창 크기 클래스, 높이 창 크기 클래스)가 있습니다. 세로 스크롤이 보편적이기 때문에 사용 가능한 너비가 사용 가능한 높이보다 일반적으로 더 중요합니다. 따라서 이 경우에는 너비 크기 클래스도 사용합니다.

접힘 상태

폴더블 기기는 다양한 크기와 힌지가 있기 때문에 앱이 적응할 수 있는 더 많은 상황을 제공합니다. 힌지가 디스플레이의 일부를 가릴 수 있으므로 해당 영역이 콘텐츠를 표시하기에 적합하지 않을 수 있습니다. 또한 분리될 수도 있습니다. 즉, 기기를 펼치면 두 개의 별도의 실제 디스플레이가 있습니다.

폴더블의 평평한 상태와 반만 열린 상태

또한 사용자가 힌지가 부분적으로 열려 있는 상태에서 내부 디스플레이를 볼 수도 있습니다. 따라서 접힘 방향에 따라 탁자 모드 (위 이미지에서 오른쪽에 보이는 수평 접힘)와 책 모드 (수직으로 접은 상태)에 따라 실제 상태가 달라질 수 있습니다.

접힘 상태 및 힌지에 관해 자세히 알아보세요.

폴더블을 지원하는 적응형 레이아웃을 구현할 때 이러한 모든 사항을 고려해야 합니다.

적응형 정보 가져오기

Material 3 adaptive 라이브러리를 사용하면 앱이 실행되는 창에 관한 정보에 편리하게 액세스할 수 있습니다.

  1. 이 아티팩트 및 버전에 대한 항목을 버전 카탈로그 파일에 추가합니다.

gradle/libs.versions.toml

[versions]
material3Adaptive = "1.0.0-beta01"

[libraries]
androidx-material3-adaptive = { module = "androidx.compose.material3.adaptive:adaptive", version.ref = "material3Adaptive" }
  1. 앱 모듈의 빌드 파일에 새 라이브러리 종속 항목을 추가한 후 Gradle 동기화를 수행합니다.

app/build.gradle.kts를 참고하세요.

dependencies {

    implementation(libs.androidx.material3.adaptive)
}

이제 모든 컴포저블 범위에서 currentWindowAdaptiveInfo()를 사용하여 현재 창 크기 클래스 및 기기가 탁자 상태와 같은 폴더블 상태인지 여부 등의 정보가 포함된 WindowAdaptiveInfo 객체를 가져올 수 있습니다.

지금 MainActivity에서 시도해 볼 수 있습니다.

  1. ReplyTheme 블록 내부의 onCreate()에서 창 적응형 정보를 가져오고 Text 컴포저블에 크기 클래스를 표시합니다 (ReplyApp() 요소 뒤에 추가할 수 있음).

MainActivity.kt

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

    setContent {
        ReplyTheme {
            val uiState by viewModel.uiState.collectAsStateWithLifecycle()
            ReplyApp(
                replyHomeUIState = uiState,
                onEmailClick = viewModel::setSelectedEmail
            )

            val adaptiveInfo = currentWindowAdaptiveInfo()
            val sizeClassText =
                "${adaptiveInfo.windowSizeClass.windowWidthSizeClass}\n" +
                "${adaptiveInfo.windowSizeClass.windowHeightSizeClass}"
            Text(
                text = sizeClassText,
                color = Color.Magenta,
                modifier = Modifier.padding(20.dp)
            )
        }
    }
}

이제 앱을 실행하면 앱 콘텐츠에 출력된 창 크기 클래스가 표시됩니다. 창 적응형 정보에 제공되는 다른 정보는 자유롭게 살펴보세요. 이후에 이 Text는 앱 콘텐츠를 가리며 다음 단계에 필요하지 않으므로 삭제해도 됩니다.

4. 동적 탐색

이제 연결 가능성을 개선하기 위해 기기 상태와 크기가 변경됨에 따라 앱의 탐색을 조정합니다.

도달 가능성은 극단적인 손 위치나 손 위치를 바꾸지 않고도 앱을 탐색하거나 앱과 상호작용을 시작할 수 있는 기능입니다. 사용자가 휴대전화를 들고 있을 때 손가락은 일반적으로 화면 하단에 있습니다. 사용자가 열린 폴더블 기기나 태블릿을 들고 있을 때 손가락은 일반적으로 양옆에 가까이 있습니다. 앱을 디자인하고 레이아웃에서 대화형 UI 요소를 배치할 위치를 결정할 때 화면의 여러 영역이 인체공학적으로 미치는 영향을 고려하세요.

  • 기기를 들고 편하게 손이 닿는 부위는 어디인가요?
  • 손가락을 뻗어야 할 수 있는 불편한 영역은 무엇인가요?
  • 손이 닿기 어렵거나 사용자가 기기를 들고 있는 장소에서 멀리 떨어져 있는 영역은 어디인가요?

탐색은 사용자가 가장 먼저 상호작용하는 요소로, 중요한 사용자 여정과 관련된 중요도가 높은 작업을 포함하므로 가장 쉽게 도달할 수 있는 영역에 배치해야 합니다. Material은 기기의 창 크기 클래스에 따라 탐색을 구현하는 데 도움이 되는 여러 구성요소를 제공합니다.

하단 탐색

엄지손가락으로 모든 하단 탐색 터치 포인트에 쉽게 도달할 수 있도록 기기를 잡고 사용하는 경우가 일반적이므로 하단 탐색은 소형 크기에 적합합니다. 기기가 소형이거나 폴더블이 소형으로 접힌 상태일 때 사용합니다.

항목이 있는 하단 탐색 메뉴

중간 너비의 창 크기의 경우 엄지손가락이 자연스럽게 기기 측면에 떨어지므로 탐색 레일은 도달 가능성에 이상적입니다. 탐색 레일을 탐색 창과 결합하여 자세한 정보를 표시할 수도 있습니다.

항목이 있는 탐색 레일

탐색 창을 사용하면 탐색 탭에 관한 자세한 정보를 쉽게 확인할 수 있으며 태블릿 또는 대형 기기를 사용하는 경우 탐색 창에 쉽게 액세스할 수 있습니다. 사용할 수 있는 탐색 창에는 모달 탐색 창과 영구 탐색 창이 있습니다.

모달 탐색 창

모달 탐색 창은 콘텐츠의 오버레이로 확장되거나 숨겨질 수 있으므로 소형에서 중형 크기 스마트폰 및 태블릿에서 사용할 수 있습니다. 경우에 따라 탐색 레일과 결합할 수 있습니다.

항목이 있는 모달 탐색 창

영구 탐색 창

영구 탐색 창은 대형 태블릿, Chromebook, 데스크톱에서 고정 탐색용으로 사용할 수 있습니다.

항목이 있는 영구 탐색 창

동적 탐색 구현

이제 기기 상태와 크기가 변경됨에 따라 다양한 유형의 탐색 간에 전환합니다.

현재 앱에서는 기기 상태와 관계없이 항상 화면 콘텐츠 아래에 NavigationBar를 표시합니다. 대신 Material NavigationSuiteScaffold 구성요소를 사용하여 현재 창 크기 클래스와 같은 정보에 따라 다양한 탐색 구성요소 간에 자동으로 전환할 수 있습니다.

  1. 버전 카탈로그와 앱의 빌드 스크립트를 업데이트하여 이 구성요소를 가져오도록 Gradle 종속 항목을 추가한 후 Gradle 동기화를 수행합니다.

gradle/libs.versions.toml

[versions]
material3AdaptiveNavSuite = "1.3.0-beta01"

[libraries]
androidx-material3-adaptive-navigation-suite = { module = "androidx.compose.material3:material3-adaptive-navigation-suite", version.ref = "material3AdaptiveNavSuite" }

app/build.gradle.kts를 참고하세요.

dependencies {

    implementation(libs.androidx.material3.adaptive.navigation.suite)
}
  1. ReplyApp.kt에서 구성 가능한 ReplyNavigationWrapper() 함수를 찾아 Column 및 그 콘텐츠를 NavigationSuiteScaffold로 바꿉니다.

ReplyApp.kt

@Composable
private fun ReplyNavigationWrapperUI(
    content: @Composable () -> Unit = {}
) {
    var selectedDestination: ReplyDestination by remember {
        mutableStateOf(ReplyDestination.Inbox)
    }

    NavigationSuiteScaffold(
        navigationSuiteItems = {
            ReplyDestination.entries.forEach {
                item(
                    selected = it == selectedDestination,
                    onClick = { /*TODO update selection*/ },
                    icon = {
                        Icon(
                            imageVector = it.icon,
                            contentDescription = stringResource(it.labelRes)
                        )
                    },
                    label = {
                        Text(text = stringResource(it.labelRes))
                    },
                )
            }
        }
    ) {
        content()
    }
}

navigationSuiteItems 인수는 LazyColumn에 항목을 추가하는 것과 마찬가지로 item() 함수를 사용하여 항목을 추가할 수 있는 블록입니다. 후행 람다 내에서 이 코드는 ReplyNavigationWrapperUI()에 인수로 전달된 content()를 호출합니다.

에뮬레이터에서 앱을 실행하고 휴대전화, 폴더블, 태블릿 간에 크기를 변경해 보면 탐색 메뉴가 탐색 레일로 변경되었다가 뒤로 변경됩니다.

가로 모드의 태블릿과 같이 매우 넓은 창에서는 영구 탐색 창을 표시하는 것이 좋습니다. NavigationSuiteScaffold는 영구 창 표시를 지원하지만 현재 WindowWidthSizeClass 값에는 표시되지 않습니다. 그러나 약간의 변화를 주면 이 작업을 수행할 수 있습니다.

  1. NavigationSuiteScaffold 호출 직전에 다음 코드를 추가합니다.

ReplyApp.kt

@Composable
private fun ReplyNavigationWrapperUI(
    content: @Composable () -> Unit = {}
) {
    var selectedDestination: ReplyDestination by remember {
        mutableStateOf(ReplyDestination.Inbox)
    }

    val windowSize = with(LocalDensity.current) {
        currentWindowSize().toSize().toDpSize()
    }
    val layoutType = if (windowSize.width >= 1200.dp) {
        NavigationSuiteType.NavigationDrawer
    } else {
        NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(
            currentWindowAdaptiveInfo()
        )
    }

    NavigationSuiteScaffold(
        layoutType = layoutType,
        ...
    ) {
        content()
    }
}

이 코드는 먼저 창 크기를 가져와 currentWindowSize()LocalDensity.current를 사용하여 DP 단위로 변환한 다음 창 너비를 비교하여 탐색 UI의 레이아웃 유형을 결정합니다. 창 너비가 1200.dp 이상이면 NavigationSuiteType.NavigationDrawer를 사용합니다. 그렇지 않으면 기본 계산으로 대체됩니다.

크기 조절 가능한 에뮬레이터에서 앱을 다시 실행하고 다양한 유형을 사용해 보면 화면 구성이 변경되거나 접힌 기기를 펼칠 때마다 탐색이 해당 크기에 적합한 유형으로 변경됩니다.

다양한 크기의 기기에 대한 적응성 변화를 보여줍니다.

수고하셨습니다. 다양한 유형의 창 크기와 상태를 지원하는 다양한 탐색 유형을 알아봤습니다.

다음 섹션에서는 동일한 목록 항목을 가장자리까지 늘리는 대신 남은 화면 영역을 활용하는 방법을 살펴봅니다.

5. 화면 공간 사용

작은 태블릿이든 펼친 기기든 큰 태블릿이든 앱을 어디에서 실행하든 상관없이 화면이 늘어나서 남은 공간을 채웁니다. 이 화면 공간을 활용하여 추가 정보를 표시할 수 있어야 합니다. 예를 들어 이 앱의 경우 같은 페이지에서 이메일과 대화목록을 사용자에게 표시합니다.

Material 3은 각각 소형, 중형, 펼침 창 크기 클래스의 구성이 포함된 세 가지 표준 레이아웃을 정의합니다. 목록 세부정보 표준 레이아웃은 이 사용 사례에 적합하며 Compose에서 ListDetailPaneScaffold로 사용할 수 있습니다.

  1. 다음 종속 항목을 추가하고 Gradle 동기화를 실행하여 이 구성요소를 가져옵니다.

gradle/libs.versions.toml

[versions]
material3Adaptive = "1.0.0-beta01"

[libraries]
androidx-material3-adaptive-layout = { module = "androidx.compose.material3.adaptive:adaptive-layout", version.ref = "material3Adaptive" }
androidx-material3-adaptive-navigation = { module = "androidx.compose.material3.adaptive:adaptive-navigation", version.ref = "material3Adaptive" }

app/build.gradle.kts를 참고하세요.

dependencies {

    implementation(libs.androidx.material3.adaptive.layout)
    implementation(libs.androidx.material3.adaptive.navigation)
}
  1. ReplyApp.kt에서 구성 가능한 ReplyAppContent() 함수를 찾습니다. 이 함수는 현재 ReplyListPane()를 호출하여 목록 창만 표시합니다. 다음 코드를 삽입하여 이 구현을 ListDetailPaneScaffold로 바꿉니다. 실험용 API이므로 ReplyAppContent() 함수에 @OptIn 주석도 추가합니다.

ReplyApp.kt

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ReplyAppContent(
    replyHomeUIState: ReplyHomeUIState,
    onEmailClick: (Email) -> Unit,
) {
    val navigator = rememberListDetailPaneScaffoldNavigator<Long>()

    ListDetailPaneScaffold(
        directive = navigator.scaffoldDirective,
        value = navigator.scaffoldValue,
        listPane = {
            ReplyListPane(replyHomeUIState, onEmailClick)
        },
        detailPane = {
            ReplyDetailPane(replyHomeUIState.emails.first())
        }
    )
}

이 코드는 먼저 rememberListDetailPaneNavigator()를 사용하여 탐색기를 만듭니다. 탐색기는 표시할 창과 이 창에 표시해야 하는 콘텐츠를 제어할 수 있는 기능을 제공합니다. 이에 대해서는 나중에 설명하겠습니다.

창 너비 크기 클래스가 펼쳐지면 ListDetailPaneScaffold는 두 개의 창을 표시합니다. 그렇지 않으면 Scaffold 지시어와 Scaffold 값 등 두 매개변수에 제공된 값에 따라 창 하나 또는 다른 창이 표시됩니다. 기본 동작을 가져오기 위해 이 코드에서는 Scaffold 지시어와 탐색기에서 제공하는 Scaffold 값을 사용합니다.

나머지 필수 매개변수는 창의 구성 가능한 람다입니다. ReplyListPane()ReplyDetailPane() (ReplyListContent.kt에 있음)는 각각 목록 창과 세부정보 창의 역할을 채우는 데 사용됩니다. ReplyDetailPane()는 이메일 인수를 예상하므로 현재 이 코드는 ReplyHomeUIState에 있는 이메일 목록의 첫 번째 이메일을 사용합니다.

앱을 실행하고 에뮬레이터 뷰를 폴더블 또는 태블릿으로 전환 (방향을 변경해야 할 수도 있음)하여 두 개의 창 레이아웃을 확인합니다. 이전보다 훨씬 나아졌군요!

이제 이 화면에서 원하는 동작 중 일부를 처리해 보겠습니다. 사용자가 목록 창에서 이메일을 탭하면 모든 답장과 함께 세부정보 창에 표시되어야 합니다. 현재 앱은 어떤 이메일이 선택되었는지 추적하지 않으며 항목을 탭해도 아무 작업도 수행되지 않습니다. 이 정보를 보관하기에 가장 좋은 위치는 ReplyHomeUIState의 나머지 UI 상태와 함께 보관하는 것입니다.

  1. ReplyHomeViewModel.kt를 열고 ReplyHomeUIState 데이터 클래스를 찾습니다. 선택한 이메일에 대해 기본값 null을 사용하여 속성을 추가합니다.

ReplyHomeViewModel.kt

data class ReplyHomeUIState(
    val emails : List<Email> = emptyList(),
    val selectedEmail: Email? = null,
    val loading: Boolean = false,
    val error: String? = null
)
  1. 같은 파일에서 ReplyHomeViewModel에는 사용자가 목록 항목을 탭할 때 호출되는 setSelectedEmail() 함수가 있습니다. UI 상태를 복사하고 선택한 이메일을 기록하도록 이 함수를 수정하세요.

ReplyHomeViewModel.kt

fun setSelectedEmail(email: Email) {
    _uiState.update {
        it.copy(selectedEmail = email)
    }
}

사용자가 항목을 탭하기 전에 어떤 일이 일어나고 선택된 이메일이 null인지 고려해야 합니다. 세부정보 창에는 무엇을 표시해야 하나요? 이 문제를 처리하는 방법은 여러 가지가 있습니다(예: 기본적으로 목록의 첫 번째 항목 표시).

  1. 같은 파일에서 observeEmails() 함수를 수정합니다. 이메일 목록이 로드될 때 이전 UI 상태에 선택된 이메일이 없는 경우 첫 번째 항목으로 설정합니다.

ReplyHomeViewModel.kt

private fun observeEmails() {
    viewModelScope.launch {
        emailsRepository.getAllEmails()
            .catch { ex ->
                _uiState.value = ReplyHomeUIState(error = ex.message)
            }
            .collect { emails ->
                val currentSelection = _uiState.value.selectedEmail
                _uiState.value = ReplyHomeUIState(
                    emails = emails,
                    selectedEmail = currentSelection ?: emails.first()
                )
            }
    }
}
  1. ReplyApp.kt로 돌아가서 선택한 이메일(사용 가능한 경우)을 사용하여 세부정보 창 콘텐츠를 채웁니다.

ReplyApp.kt

ListDetailPaneScaffold(
    // ...
    detailPane = {
        if (replyHomeUIState.selectedEmail != null) {
            ReplyDetailPane(replyHomeUIState.selectedEmail)
        }
    }
)

앱을 다시 실행하고 에뮬레이터를 태블릿 크기로 전환한 후 목록 항목을 탭하면 세부정보 창의 콘텐츠가 업데이트되는 것을 확인할 수 있습니다.

이는 두 창이 모두 표시될 때 잘 작동하지만 창에 창 하나만 표시할 공간이 있는 경우 항목을 탭해도 아무 일도 발생하지 않는 것처럼 보입니다. 에뮬레이터 뷰를 휴대전화 또는 세로 모드의 폴더블 기기로 전환해 보면 항목을 탭한 후에도 목록 창만 표시됩니다. 선택한 이메일이 업데이트되더라도 ListDetailPaneScaffold가 이러한 구성의 목록 창에 포커스를 유지하기 때문입니다.

  1. 이 문제를 해결하려면 다음 코드를 ReplyListPane에 전달된 람다로 삽입합니다.

ReplyApp.kt

ListDetailPaneScaffold(
    // ...
    listPane = {
        ReplyListPane(
            replyHomeUIState = replyHomeUIState,
            onEmailClick = { email ->
                onEmailClick(email)
                navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, email.id)
            }
        )
    },
    // ...
)

이 람다는 앞에서 만든 탐색기를 사용하여 항목을 클릭할 때의 동작을 추가합니다. 이 함수에 전달된 원본 람다를 호출한 다음 표시할 창을 지정하는 navigator.navigateTo()도 호출합니다. Scaffold의 각 창에는 연결된 역할이 있으며 세부정보 창의 경우 ListDetailPaneScaffoldRole.Detail입니다. 작은 창에서는 앱이 앞으로 이동한 것처럼 보입니다.

앱은 사용자가 세부정보 창에서 뒤로 버튼을 누를 때 발생하는 동작도 처리해야 합니다. 이 동작은 창이 한 개 있는지 두 개 표시되는지에 따라 달라집니다.

  1. 다음 코드를 추가하여 뒤로 탐색을 지원합니다.

ReplyApp.kt

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ReplyAppContent(
    replyHomeUIState: ReplyHomeUIState,
    onEmailClick: (Email) -> Unit,
) {
    val navigator = rememberListDetailPaneScaffoldNavigator<Long>()

    BackHandler(navigator.canNavigateBack()) {
        navigator.navigateBack()
    }

    ListDetailPaneScaffold(
        directive = navigator.scaffoldDirective,
        value = navigator.scaffoldValue,
        listPane = {
            AnimatedPane {
                ReplyListPane(
                    replyHomeUIState = replyHomeUIState,
                    onEmailClick = { email ->
                        onEmailClick(email)
                        navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, email.id)
                    }
                )
            }
        },
        detailPane = {
            AnimatedPane {
                if (replyHomeUIState.selectedEmail != null) {
                    ReplyDetailPane(replyHomeUIState.selectedEmail)
                }
            }
        }
    )
}

탐색기는 ListDetailPaneScaffold의 전체 상태, 뒤로 탐색이 가능한지 여부, 이러한 모든 시나리오에서 해야 할 작업을 알고 있습니다. 이 코드는 탐색기가 뒤로 이동할 수 있을 때마다 사용 설정되고 람다 내에서 navigateBack()를 호출하는 BackHandler를 만듭니다. 또한 창 간의 전환이 더 원활하게 이루어지도록 각 창은 AnimatedPane() 컴포저블로 래핑됩니다.

크기 조절 가능한 에뮬레이터에서 모든 다양한 유형의 기기를 대상으로 앱을 다시 실행하면 화면 구성이 변경되거나 접힌 기기를 펼칠 때마다 기기 상태 변경에 응답하여 탐색 및 화면 콘텐츠가 동적으로 변경됩니다. 또한 목록 창에서 이메일을 탭하면 레이아웃이 여러 화면에서 어떻게 동작하는지 확인하여 두 창을 나란히 표시하거나 창 사이에 부드럽게 애니메이션으로 표시할 수 있습니다.

다양한 크기의 기기에 대한 적응성 변화를 보여줍니다.

수고하셨습니다. 모든 종류의 기기 상태 및 크기에 맞게 앱이 조정되도록 했습니다. 폴더블, 태블릿 또는 다른 휴대기기에서 앱을 실행해 보세요.

6. 축하합니다

수고하셨습니다 이 Codelab을 완료하고 Jetpack Compose로 앱을 적응형으로 만드는 방법을 알아봤습니다.

기기의 크기와 접힘 상태를 확인하고 이에 맞게 앱의 UI, 탐색, 기타 기능을 업데이트하는 방법을 알아봤습니다. 적응성이 어떻게 도달 가능성을 개선하고 사용자 환경을 개선하는지도 배웠습니다.

다음 단계

Compose 개발자 과정의 다른 Codelab을 확인하세요.

샘플 앱

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

참조 문서