Jetpack Compose로 적응형 앱 빌드

1. 소개

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

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

적응성

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

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

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

학습할 내용

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

필요한 항목

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

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

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

빌드할 항목

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

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

2. 설정

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

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

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

기본 브랜치의 코드로 시작하고 각자의 속도에 맞게 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: 목록 및 세부정보 화면을 제공하기 위한 컴포저블이 포함되어 있습니다.

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

스마트폰의 초기 화면

태블릿의 초기 확대 뷰

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

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

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

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

창 크기

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

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

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

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

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

접힘 상태

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

폴더블 상태(평평하게 펼쳐진 상태, 절반을 펼친 상태)

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

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

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

적응형 정보 가져오기

Material3 adaptive 라이브러리는 앱이 실행 중인 창에 관한 정보에 편리하게 액세스할 수 있도록 지원합니다.

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

gradle/libs.versions.toml

[versions]
material3Adaptive = "1.0.0"

[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(
                    WindowInsets.safeDrawing.asPaddingValues()
                )
            )
        }
    }
}

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

4. 동적 탐색

이제 앱을 더 쉽게 사용할 수 있도록 기기 상태와 크기가 변경될 때 앱의 탐색을 조정해 보겠습니다.

사용자가 휴대전화를 잡을 때는 일반적으로 손가락이 화면 하단에 있습니다. 사용자가 열린 폴더블 기기나 태블릿을 들고 있을 때 손가락은 일반적으로 양옆에 가까이 있습니다. 사용자는 극단적인 손 위치나 손 위치를 바꾸지 않고도 앱을 탐색하거나 상호작용을 시작할 수 있어야 합니다.

앱을 디자인하고 레이아웃에서 상호작용 UI 요소를 배치할 위치를 결정할 때는 화면의 여러 영역이 인체공학에 미치는 영향을 고려하세요.

  • 기기를 들고 있을 때 편안하게 도달할 수 있는 영역은 어디인가요?
  • 손가락을 뻗어야 도달할 수 있어 불편할 수 있는 영역은 어디인가요?
  • 도달하기 어려운 영역이나 사용자가 기기를 들고 있는 곳에서 멀리 떨어진 영역은 어디인가요?

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

하단 탐색

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

항목이 있는 하단 탐색 메뉴

중형 너비의 창 크기의 경우 탐색 레일은 엄지손가락이 기기 측면을 따라 자연스럽게 위치하기 때문에 도달 가능성에 이상적입니다. 탐색 레일을 탐색 창과 결합하여 추가 정보를 표시할 수도 있습니다.

항목이 있는 탐색 레일

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

모달 탐색 창

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

항목이 있는 모달 탐색 창

영구 탐색 창

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

항목이 있는 영구 탐색 창

동적 탐색 구현

이제 기기 상태와 크기가 변함에 따라 다양한 유형의 탐색 간에 전환해 보겠습니다.

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

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

gradle/libs.versions.toml

[versions]
material3AdaptiveNavSuite = "1.3.0"

[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

[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. 현재 ReplyListPane()를 호출하여 목록 창만 표시하는 ReplyApp.kt에서 ReplyAppContent() 컴포저블 함수를 찾습니다. 다음 코드를 삽입하여 이 구현을 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 값을 사용합니다.

나머지 필수 매개변수는 창의 컴포저블 람다입니다. 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에 설명된 권장사항이 통합된 여러 앱의 모음입니다.

참조 문서