Twórz adaptacyjne aplikacje za pomocą Jetpack Compose

1. Wstęp

Z tego ćwiczenia w Codelabs dowiesz się, jak tworzyć aplikacje adaptacyjne na telefony, tablety i urządzenia składane, a także jak zwiększyć osiągalność w Jetpack Compose. Poznasz też sprawdzone metody korzystania z komponentów i motywów Material 3.

Zanim przejdziemy do szczegółów, musimy zrozumieć, co rozumiemy przez adaptację.

Dostosowanie

Interfejs aplikacji powinien dostosowywać się do różnych rozmiarów okien, orientacji i formatów. Układ adaptacyjny zmienia się w zależności od dostępnego miejsca na ekranie. Mogą to być różne zmiany układu, od prostych korekt układu, przez wypełnianie przestrzeni, po wybór odpowiednich stylów nawigacji, aż po całkowitą zmianę układu w celu wykorzystania dodatkowego miejsca.

Więcej informacji znajdziesz w artykule Projektowanie adaptacyjne.

Z tego ćwiczenia w Codelabs dowiesz się, jak używać Jetpack Compose i myśleć o adaptacji. Tworzysz aplikację o nazwie Reply, w której pokazujesz, jak wdrożyć adaptację do różnych urządzeń i jak możliwości adaptacji i osiągania współpracują ze sobą, by zapewnić użytkownikom optymalne wrażenia.

Czego się nauczysz

  • Jak zaprojektować aplikację tak, aby kierowała reklamy na wszystkie rozmiary okien w Jetpack Compose.
  • Jak kierować aplikację na różne urządzenia składane.
  • Jak korzystać z różnych typów nawigacji, aby zwiększyć dostępność i ułatwienia dostępu.
  • Jak używać komponentów Material 3, aby zapewnić najlepszą jakość obrazu dla każdego rozmiaru okna.

Czego potrzebujesz

W tym ćwiczeniu w programowaniu użyj emulatora z możliwością zmiany rozmiaru, który pozwala przełączać się między różnymi typami urządzeń i rozmiarami okien.

Emulator z możliwością zmiany rozmiaru – na telefonie, na tablecie, komputerze i w trybie rozłożonym.

Jeśli nie znasz funkcji Compose, przed ukończeniem tego ćwiczenia z programowania możesz ukończyć podstawowe ćwiczenia z programowania w Jetpack Compose.

Co utworzysz

  • Interaktywna aplikacja kliencka poczty e-mail wykorzystująca sprawdzone metody dostosowywania projektów, różnych nawigacji w stylu Material Design i optymalnego wykorzystania miejsca na ekranie.

W ramach tego ćwiczenia z programowania dowiesz się, jak obsługiwać różne urządzenia

2. Konfiguracja

Aby pobrać kod do tego ćwiczenia z programowania, skopiuj repozytorium GitHub za pomocą wiersza poleceń:

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

Możesz też pobrać repozytorium jako plik ZIP:

Warto zacząć od kodu w gałęzi main i wykonywać ćwiczenia z programowania krok po kroku we własnym tempie.

Otwórz projekt w Android Studio

  1. W oknie Welcome to Android Studio (Witamy w Android Studio) wybierz c01826594f360d94.pngOtwórz istniejący projekt.
  2. Wybierz folder <Download Location>/AdaptiveUiCodelab (pamiętaj, by wybrać katalog AdaptiveUiCodelab zawierający folder build.gradle).
  3. Gdy Android Studio zaimportuje projekt, sprawdź, czy możesz uruchomić gałąź main.

Poznawanie kodu startowego

Główny kod oddziału zawiera pakiet ui. W tym pakiecie będziesz pracować z tymi plikami:

  • MainActivity.kt – aktywność związana z punktem wejścia, w którym uruchamiasz aplikację.
  • ReplyApp.kt – zawiera elementy kompozycyjne interfejsu użytkownika na ekranie głównym.
  • ReplyHomeViewModel.kt – podaje dane i stan UI dotyczący zawartości aplikacji.
  • ReplyListContent.kt – zawiera elementy kompozycyjne służące do udostępniania list i ekranów z informacjami.

Najpierw skupisz się na: 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
            )
        }
    }
}

Jeśli uruchomisz tę aplikację na emulatorze z możliwością zmiany rozmiaru i wypróbujesz różne typy urządzeń, takie jak telefon czy tablet, interfejs po prostu rozwinie się do danej przestrzeni, nie wykorzystując powierzchni na ekranie czy zapewniając ergonomię osiągalności.

Ekran początkowy na telefonie

Początkowy widok rozciągnięty na tablecie

Zaktualizujesz ją, aby wykorzystać miejsce na ekranie, zwiększyć łatwość obsługi i poprawić ogólne wrażenia użytkowników.

3. Dostosowywanie aplikacji

W tej sekcji omawiamy, co oznacza dostosowywanie aplikacji i jakie komponenty zapewnia Material 3, aby to ułatwić. Dotyczy to też typów ekranów i stanów, na które kierujesz reklamy, w tym telefonów, tabletów, dużych tabletów i urządzeń składanych.

Na początek przyjrzymy się podstawom dotyczącym rozmiarów okien, stanu po przewinięciu i różnych opcji nawigacji. Następnie możesz użyć tych interfejsów API, aby dostosować aplikację do własnych potrzeb.

Rozmiary okien

Urządzenia z Androidem są dostępne we wszystkich kształtach i rozmiarach – od telefonów, przez składane, po tablety i urządzenia z ChromeOS. Aby obsługiwać jak najwięcej rozmiarów okien, interfejs musi być elastyczny i adaptacyjny. Aby pomóc Ci określić właściwy próg, po którym musisz zmienić interfejs aplikacji, zdefiniowaliśmy wartości punktów przerwania, które pomagają klasyfikować urządzenia do wstępnie zdefiniowanych klas rozmiarów (kompaktowych, średnich i rozwiniętych), nazywanych klasami rozmiaru okna. To zestaw sprawdzonych punktów przerwania widocznego obszaru, które pomogą Ci projektować, tworzyć i testować elastyczne i adaptacyjne układy aplikacji.

Kategorie zostały wybrane specjalnie z myślą o prostocie układu i elastyczności, która pozwala zoptymalizować aplikację pod kątem unikalnych przypadków. Klasa rozmiaru okna jest zawsze określana na podstawie przestrzeni ekranu dostępnej dla aplikacji, która nie musi pokrywać się z pełnym ekranem na potrzeby wielozadaniowości lub innych podziałów na segmenty.

WindowWindowSizeClass w przypadku rozmiaru kompaktowego, średniego i rozwiniętego.

WindowHeightSizeClass dla kompaktowych, średnich i po rozwinięciu o wysokości.

Zarówno szerokość, jak i wysokość są klasyfikowane oddzielnie, więc w każdej chwili aplikacja ma 2 klasy rozmiaru okna – jedną dla szerokości i jedną dla wysokości. Dostępna szerokość jest zwykle ważniejsza niż wysokość ze względu na powszechność przewijania w pionie, więc w tym przypadku należy również użyć klas rozmiaru szerokości.

Stany zwijania

Urządzenia składane oferują jeszcze więcej sytuacji, do których aplikacja może dostosować się dzięki różnym rozmiarom i możliwością zawiasów. Zawiasy mogą zasłaniać część wyświetlacza, przez co nie będzie można w nim wyświetlać treści. Mogą się też rozdzielić, co oznacza, że po rozłożeniu urządzenia dostępne są 2 oddzielne fizyczne wyświetlacze.

Składany stan, płaska i w połowie otwarta

Poza tym, gdy zawias jest częściowo otwarty, użytkownik może patrzeć na wewnętrzny wyświetlacz. W rezultacie w zależności od orientacji urządzenia może on mieć różne ustawienia fizyczne: na stole (położenie w poziomie – na zdjęciu powyżej) i położenie książki (zawinięcie w pionie).

Dowiedz się więcej o ustawieniach i zawiasach po złożeniu.

Wszystkie te kwestie należy wziąć pod uwagę przy wdrażaniu układów adaptacyjnych, które obsługują urządzenia składane.

Uzyskaj informacje adaptacyjne

Biblioteka Material3 adaptive zapewnia wygodny dostęp do informacji o oknie, w którym działa Twoja aplikacja.

  1. Dodaj wpisy dotyczące tego artefaktu i jego wersji do pliku katalogu wersji:

gradle/libs.versions.toml

[versions]
material3Adaptive = "1.0.0-beta01"

[libraries]
androidx-material3-adaptive = { module = "androidx.compose.material3.adaptive:adaptive", version.ref = "material3Adaptive" }
  1. W pliku kompilacji modułu aplikacji dodaj nową zależność biblioteki, a następnie przeprowadź synchronizację Gradle:

app/build.gradle.kts

dependencies {

    implementation(libs.androidx.material3.adaptive)
}

Teraz w każdym zakresie kompozycyjnym możesz użyć narzędzia currentWindowAdaptiveInfo(), aby uzyskać obiekt WindowAdaptiveInfo zawierający informacje takie jak bieżąca klasa rozmiaru okna i informacje o tym, czy urządzenie jest w stanie złożonym, takim jak stan stołu.

Możesz to teraz wypróbować w aplikacji MainActivity.

  1. W polu onCreate() w bloku ReplyTheme uzyskaj informacje o dostosowaniu okna i wyświetl klasy rozmiaru w funkcji kompozycyjnej Text (możesz ją dodać po elemencie 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)
            )
        }
    }
}

Po uruchomieniu aplikacji wyświetlą się klasy rozmiaru okna wydrukowane nad treścią aplikacji. Możesz sprawdzić, co jeszcze jest dostępne w informacjach adaptacyjnych okna. Później możesz usunąć ten element Text, ponieważ zakrywa on zawartość aplikacji i nie będzie potrzebny w kolejnych krokach.

4. Dynamiczna nawigacja

Aby poprawić osiągalność, dostosuj teraz elementy nawigacyjne aplikacji do zmian stanu i rozmiaru urządzenia.

Osiągalność to zdolność nawigacji lub inicjowania interakcji z aplikacją bez konieczności utrzymywania skrajnej pozycji dłoni czy zmiany pozycji dłoni. Gdy użytkownik trzyma telefon, jego palce zazwyczaj znajdują się u dołu ekranu. Gdy użytkownik trzyma otwarte urządzenie składane lub tablet, jego palce zazwyczaj znajdują się blisko boków. Projektując aplikację i decydując, gdzie umieścić interaktywne elementy interfejsu w układzie, weź pod uwagę wpływ ergonomii na różne obszary ekranu.

  • Wskaż obszary, w których możesz wygodnie sięgać, trzymając urządzenie.
  • Do których obszarów można dotrzeć jedynie przez rozłożenie palców, co może być niewygodne?
  • Do jakich obszarów trudno jest dotrzeć lub które są oddalone od miejsca, w którym użytkownik trzyma urządzenie?

Nawigacja jest pierwszą rzeczą, z którą użytkownicy wchodzą w interakcję. Obejmuje ona ważne działania związane z najważniejszymi ścieżkami użytkownika. Należy ją więc umieścić w miejscach, w których jest łatwo dotrzeć. Material ma kilka komponentów, które pomagają wdrożyć nawigację w zależności od klasy rozmiaru okna na urządzeniu.

Dolna nawigacja

Dolna nawigacja sprawdza się idealnie w przypadku kompaktowych urządzeń, ponieważ kciukiem w naturalny sposób trzyma się urządzenie w miejscu, w którym kciuk może łatwo dotrzeć do wszystkich dolnych punktów nawigacji. Używaj, gdy Twoje urządzenie jest kompaktowe lub składane.

Dolny pasek nawigacyjny z elementami

W przypadku okna o średniej szerokości kolumna nawigacji jest idealna, jeśli chodzi o zasięg, ponieważ kciuk naturalnie umieszcza się z boku urządzenia. Możesz też połączyć ją z panelem nawigacji, by wyświetlać więcej informacji.

Kolumna nawigacji z elementami

Panel nawigacji pozwala w łatwy sposób przeglądać szczegółowe informacje o kartach nawigacyjnych. Jest on też łatwo dostępny, gdy korzystasz z tabletów lub większych urządzeń. Dostępne są 2 rodzaje szuflad nawigacji: modalny i stały.

Panel nawigacji modowej

W przypadku kompaktowych i średnich telefonów i tabletów można użyć modalnego panelu nawigacji, który można rozwinąć lub ukryć jako nakładka na treści. Czasem może ona być połączona z szynami nawigacyjnymi.

Modalny panel nawigacji z elementami

Stały panel nawigacji

Możesz użyć stałej szuflady nawigacji, aby zapewnić stałą nawigację na dużych tabletach, Chromebookach i komputerach.

Panel nawigacji z produktami

Implementacja dynamicznej nawigacji

Teraz możesz przełączać się między różnymi typami nawigacji w zależności od stanu i rozmiaru urządzenia.

Obecnie pod treścią ekranu aplikacja zawsze wyświetla ikonę NavigationBar niezależnie od stanu urządzenia. Zamiast tego możesz użyć komponentu Materiał NavigationSuiteScaffold, aby automatycznie przełączać się między różnymi komponentami nawigacji na podstawie informacji takich jak bieżąca klasa rozmiaru okna.

  1. Dodaj zależność Gradle, aby pobrać ten komponent przez zaktualizowanie katalogu wersji i skryptu kompilacji aplikacji, a następnie przeprowadź synchronizację 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. Znajdź funkcję kompozycyjną ReplyNavigationWrapper() w ReplyApp.kt i zastąp element Column i jego zawartość 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()
    }
}

Argument navigationSuiteItems to blok, który umożliwia dodawanie elementów za pomocą funkcji item(), podobnie jak dodawanie elementów w LazyColumn. W końcowej funkcji lambda ten kod wywołuje metodę content() przekazaną jako argument funkcji ReplyNavigationWrapperUI().

Uruchom aplikację w emulatorze i spróbuj zmienić rozmiar między telefonem, urządzeniem składanym i tabletem. Pasek nawigacyjny zmieni się na pasek nawigacyjny i z powrotem.

W przypadku bardzo szerokich okien, na przykład na tablecie w orientacji poziomej, dobrze jest pokazywać panel nawigacji. NavigationSuiteScaffold umożliwia wyświetlanie stałego panelu, jednak nie jest on wyświetlany w żadnej z bieżących wartości WindowWidthSizeClass. Możesz to jednak zrobić, wprowadzając niewielką zmianę.

  1. Dodaj następujący kod tuż przed połączeniem z numerem 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()
    }
}

Ten kod najpierw pobiera rozmiar okna i konwertuje go na jednostki DP za pomocą elementów currentWindowSize() i LocalDensity.current, a potem porównuje szerokość okna, aby wybrać typ układu interfejsu nawigacyjnego. Jeśli szerokość okna wynosi co najmniej 1200.dp, używane jest NavigationSuiteType.NavigationDrawer. W przeciwnym razie przywraca wartość domyślną.

Gdy ponownie uruchomisz aplikację na emulatorze z możliwością zmiany rozmiaru i wypróbujesz różne typy, zwróć uwagę, że po każdej zmianie konfiguracji ekranu lub rozłożeniu urządzenia składanego nawigacja zmienia się na odpowiedni dla tego rozmiaru.

Wyświetlam zmiany adaptacyjne dla urządzeń o różnej wielkości.

Gratulacje, znasz już różne sposoby nawigacji dla różnych rozmiarów i stanów okien.

W następnej sekcji pokażemy, jak wykorzystać pozostały obszar ekranu, zamiast rozciągać cały obszar od krawędzi do krawędzi tego samego elementu listy.

5. Użycie miejsca na ekranie

Ekran zostanie rozciągnięty, aby wypełnić pozostałe miejsce – niezależnie od tego, czy używasz aplikacji na małym, rozłożonym czy dużym tablecie. Chcesz wykorzystać tę przestrzeń na ekranie, aby wyświetlać więcej informacji, np. o tej aplikacji, oraz e-maile i wątki użytkownikom na tej samej stronie.

Material 3 definiuje 3 układy kanoniczne, z których każdy ma konfigurację klasy kompaktowego, średniego i rozwiniętego rozmiaru okna. W tym przypadku idealnie nadaje się układ kanoniczny Szczegóły listy. Jest on dostępny podczas tworzenia wiadomości jako ListDetailPaneScaffold.

  1. Aby pobrać ten komponent, dodaj te zależności i przeprowadź synchronizację 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. Znajdź funkcję kompozycyjną ReplyAppContent() w komórce ReplyApp.kt, która obecnie wyświetla tylko panel listy, wywołując funkcję ReplyListPane(). Zastąp tę implementację implementacją ListDetailPaneScaffold, wstawiając ten kod. To jest eksperymentalny interfejs API, więc musisz też dodać adnotację @OptIn do funkcji ReplyAppContent():

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())
        }
    )
}

Ten kod najpierw tworzy nawigator za pomocą funkcji rememberListDetailPaneNavigator(). Nawigator zapewnia pewną kontrolę nad tym, który panel jest wyświetlany i jaka zawartość ma się w nim znajdować. Omówię to później.

Gdy klasa rozmiaru okna jest rozwinięta, ListDetailPaneScaffold wyświetla 2 panele. W przeciwnym razie jeden lub drugi panel wyświetla się na podstawie wartości podanych dla 2 parametrów: dyrektywy scaffold i wartości scaffold. Aby uzyskać domyślne działanie, ten kod wykorzystuje dyrektywę scaffold i wartość scaffold dostarczaną przez nawigator.

Pozostałe wymagane parametry to składowe lambda dla paneli. Listy ReplyListPane() i ReplyDetailPane() (dostępne w języku ReplyListContent.kt) służą do wypełniania ról odpowiednio okien listy i szczegółów. Funkcja ReplyDetailPane() oczekuje argumentu e-mail, więc na razie ten kod używa pierwszego adresu e-mail z listy adresów e-mail w usłudze ReplyHomeUIState.

Uruchom aplikację i przełącz widok emulatora na składany lub tablet (może być też konieczna zmiana orientacji), aby zobaczyć układ z dwoma panelami. Już wygląda znacznie lepiej niż wcześniej!

Teraz przyjrzyjmy się niektórym elementom, które mogą być przydatne na tym ekranie. Gdy użytkownik kliknie e-maila na liście, powinien się on wyświetlić w okienku szczegółów wraz ze wszystkimi odpowiedziami. Obecnie aplikacja nie śledzi, który adres e-mail został wybrany, a kliknięcie elementu nic nie robi. Te informacje najlepiej przechowywać w pozostałych elementach interfejsu w interfejsie ReplyHomeUIState.

  1. Otwórz ReplyHomeViewModel.kt i znajdź klasę danych ReplyHomeUIState. Dodaj właściwość do wybranego adresu e-mail z wartością domyślną null:

ReplyHomeViewModel.kt

data class ReplyHomeUIState(
    val emails : List<Email> = emptyList(),
    val selectedEmail: Email? = null,
    val loading: Boolean = false,
    val error: String? = null
)
  1. W tym samym pliku funkcja ReplyHomeViewModel zawiera funkcję setSelectedEmail(), która jest wywoływana, gdy użytkownik kliknie element listy. Zmodyfikuj tę funkcję, aby skopiować stan UI i zarejestrować wybrany e-mail:

ReplyHomeViewModel.kt

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

Należy wziąć pod uwagę to, co się dzieje, zanim użytkownik kliknie dowolny element, a wybrany adres e-mail to null. Co powinno być wyświetlane w panelu szczegółów? Można to zrobić na kilka sposobów, np. domyślnie wyświetlić pierwszą pozycję na liście.

  1. W tym samym pliku zmodyfikuj funkcję observeEmails(). Jeśli podczas wczytywania listy adresów e-mail w poprzednim stanie interfejsu nie wybrano wybranego adresu e-mail, ustaw go na pierwszy element:

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. Wróć do ReplyApp.kt i użyj wybranego adresu e-mail, jeśli jest dostępny, aby wypełnić treść panelu szczegółów:

ReplyApp.kt

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

Uruchom aplikację jeszcze raz i zmień emulator na rozmiar tabletu. Zobaczysz, że kliknięcie elementu na liście powoduje zaktualizowanie zawartości panelu szczegółów.

Świetnie się to sprawdza, gdy widoczne są oba panele, ale gdy okno ma miejsce tylko na jeden, po kliknięciu elementu nic się nie dzieje. Przełącz widok emulatora na telefon lub urządzenie składane pionowo. Po kliknięciu elementu widoczny będzie tylko panel listy. Dzieje się tak, ponieważ wybrany adres e-mail zostanie zaktualizowany, ale ListDetailPaneScaffold będzie nadal skupiać się na panelu listy w tych konfiguracjach.

  1. Aby rozwiązać ten problem, wstaw ten kod jako parametr lambda przekazywana do funkcji ReplyListPane:

ReplyApp.kt

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

Ta funkcja lambda używa utworzonego wcześniej nawigatora, aby dodać dodatkowe zachowanie po kliknięciu elementu. Wywołuje ona pierwotną funkcję lambda przekazaną do tej funkcji, a następnie wywołuje funkcję navigator.navigateTo(), określającą, który panel ma być wyświetlany. Z każdym panelem rusztowania powiązana jest rola, a w panelu szczegółów to ListDetailPaneScaffoldRole.Detail. W mniejszych oknach aplikacja będzie wyglądała, jakby przeszła do przodu.

Aplikacja musi również obsługiwać to, co się dzieje, gdy użytkownik naciśnie przycisk Wstecz w panelu szczegółów. To działanie będzie się różnić w zależności od tego, czy są widoczne jeden panel czy dwa.

  1. Obsługuj nawigację wstecz, dodając ten kod.

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)
                }
            }
        }
    )
}

Nawigator zna pełny stan elementu ListDetailPaneScaffold, czy możliwe jest przejście wstecz i co zrobić w każdym z tych scenariuszy. Ten kod tworzy BackHandler, który jest włączony za każdym razem, gdy nawigator może przejść wstecz, a wewnątrz funkcji lambda wywołuje metodę navigateBack(). Aby przejście między panelami było płynniejsze, każdy z nich jest zapakowany w kompozycję AnimatedPane().

Uruchom aplikację ponownie na emulatorze z możliwością zmiany rozmiaru na wszystkich typach urządzeń i zauważ, że po każdej zmianie konfiguracji ekranu lub w rozkładaniu urządzenia składanego zawartość ekranu i nawigacja zmieniają się dynamicznie w odpowiedzi na stan urządzenia. Możesz też kliknąć e-maile w panelu listy i zobaczyć, jak układ zachowuje się na różnych ekranach, wyświetlając oba panele obok siebie lub płynnie animując je między nimi.

Wyświetlam zmiany adaptacyjne dla urządzeń o różnej wielkości.

Gratulujemy. Udało Ci się dostosować swoją aplikację do różnych stanów i rozmiarów urządzeń. Możesz wypróbować aplikację na urządzeniach składanych, tabletach lub innych urządzeniach mobilnych.

6. Gratulacje

Gratulacje! Udało Ci się ukończyć to szkolenie i dowiedzieć się, jak dostosować aplikacje do potrzeb użytkowników dzięki Jetpack Compose.

Wiesz już, jak sprawdzić rozmiar i stan po złożeniu urządzenia oraz odpowiednio zaktualizować UI, nawigację i inne funkcje aplikacji. Wiesz już również, jak elastyczność poprawia osiągalność i poprawia wrażenia użytkowników.

Co dalej?

Zapoznaj się z pozostałymi ćwiczeniami z programowania dotyczącymi ścieżki tworzenia wiadomości.

Przykładowe aplikacje

Dokumenty referencyjne