Podstawy aplikacji

Skalowanie aplikacji na każdy ekran za pomocą Compose

Głośniki

Informacje

Informacje o wystąpieniu

Ekosystem Androida jest ogromny, a użytkownicy płynnie przełączają się między telefonami, tabletami, urządzeniami składanymi i komputerami. Teraz, gdy Compose jest naszym głównym narzędziem, podczas tej sesji dowiesz się, jak zaprojektować aplikację pod kątem okien o zmiennym rozmiarze i różnych formatów obrazu, w przypadku których tradycyjne założenia dotyczące układu nie sprawdzają się, co prowadzi do rozciągnięcia interfejsu i utraty stanu.

Dowiedz się, jak najnowsze funkcje Jetpack Compose, Navigation 3 i WindowManager mogą pomóc Ci tworzyć dynamiczne układy, które naturalnie skalują się od kompaktowych ekranów do paneli podwójnych. Poznasz strategie przyspieszania procesu programowania za pomocą Gemini i naszych umiejętności, takich jak umiejętność adaptacyjna, które pozwalają automatycznie refaktoryzować starsze bazy kodu pod kątem większych ekranów.

Podsumowanie

  • Platformy Androida wymagają aplikacji adaptacyjnych, ponieważ od Androida 16 system ignoruje blokady orientacji i formatu obrazu na większych ekranach, a w Androidzie 17 usunięto opcje rezygnacji w pliku manifestu.
  • Eksperymentalny interfejs API MediaQuery deklaratywnie śledzi aktywne funkcje interfejsu, takie jak rozmiar okna i pozycje składania. Zastępuje łączenie wielu starszych menedżerów platformy w stan Compose.
  • Stabilne elementy podstawowe Grid i FlexBox zapewniają elementy składowe układu adaptacyjnego. Grid umożliwia parametryzowane wielowymiarowe układy w ramach jednego elementu kompozycyjnego, a FlexBox zapewnia jednowymiarowe rozmieszczenie i zawijanie.
  • Nawigacja Jetpack 3 porządkuje przepływy z wieloma panelami na poziomie aplikacji wokół stanu i scen. Niestandardowe strategie scen oceniają możliwości interfejsu i klasy rozmiaru okna, aby dynamicznie przełączać się między prezentacjami z jednym i wieloma panelami.
  • Umiejętności Compose Adaptive dla Gemini automatyzują migrację aplikacji do nowoczesnych komponentów responsywnych. Zaktualizowana umiejętność refaktoryzuje układy, rozwiązuje błędy związane z utratą stanu i konfiguruje dane wejściowe na komputerze, takie jak skróty klawiszowe i kursory.

Zasoby

Gemini Notebook

Tematy

  • jetpack-compose
  • adaptive-layouts
  • mediaquery
  • grid-layout
  • flexbox
  • navigation-3
  • android-skills

Transkrypcja

Pobierz transkrypcję

Wprowadzenie do skalowania aplikacji na różnych ekranach

  • Francesco Romano

    Cześć wszystkim.

  • Dziś szczegółowo omówimy, jak skalować aplikację na każdy ekran za pomocą Jetpack Compose.

  • Dlaczego warto skalować na każdy ekran?

  • Ekosystem Androida rozszerzył się na wiele różnych formatów.

  • Plik APK może działać na telefonach, urządzeniach składanych, tabletach, Chromebookach i Googlebookach, telewizorach, systemach multimedialnych w samochodach, urządzeniach z XR itp.

  • Po pierwsze, wychodź naprzeciw potrzebom użytkowników.

  • Po drugie, przejście platformy Android na aplikacje adaptacyjne nie jest tymczasowe.

  • Nie wrócimy do świata, w którym były tylko telefony. To już z nami zostanie.

  • Mam tu krótką oś czasu.

  • W 2022 roku zaczęliśmy kłaść pod nie fundamenty.

  • Wprowadziliśmy Androida 12L, pierwszą wersję Jetpack WindowManager, udostępniliśmy interfejsy API, takie jak klasy rozmiaru okna, i opublikowaliśmy pierwszą wersję wskazówek dotyczących jakości aplikacji adaptacyjnych.

  • Zaktualizowaliśmy też Google Play, aby oceny były podzielone według typu urządzenia, i zaczęliśmy uwzględniać wskazówki dotyczące jakości aplikacji adaptacyjnych w decyzjach o rankingu.

  • W Androidzie 15 wprowadziliśmy też tryb okien na pulpicie w AOSP i stabilną wersję Compose Material 3 Adaptive.

  • Od Androida 16 (SDK 36) system po prostu ignoruje blokady orientacji, blokady formatu obrazu i flagi możliwości zmiany rozmiaru na większych ekranach.

  • Wraz z wprowadzeniem połączonych wyświetlaczy plik APK może teraz działać w praktycznie dowolnym rozmiarze okna.

  • W pakiecie SDK 36 wprowadziliśmy tymczasową flagę manifestu, która pozwalała zrezygnować z zastępowania orientacji i formatu obrazu.

  • W Androidzie 17 ta opcja rezygnacji została usunięta.

  • Dlatego tworzenie adaptacyjnych aplikacji jest podstawowym wymaganiem platformy.

  • W ciągu ostatniej dekady ściśle współpracowaliśmy z zespołami z całego ekosystemu i dowiedzieliśmy się, dlaczego tworzenie aplikacji adaptacyjnych może być trudne.

  • Możesz utracić stan, interfejsy API okien są rozdrobnione, sygnały dotyczące tego, kiedy przełączyć układ, są niejasne, a w bazie kodu są głęboko zakorzenione stare założenia.

  • Wykorzystaliśmy więc wszystko, czego nauczyliśmy się przez tę dekadę wyzwań dla deweloperów, i wdrożyliśmy to bezpośrednio w 2 miejscach: po pierwsze w samym zestawie narzędzi UI.

  • Dzisiaj przyjrzymy się kilku ważnym nowym funkcjom w Jetpack Compose: zapytaniu o media, siatce, FlexBox i Nawigacji 3.

  • Obejmuje to też narzędzia oparte na agentach, które piszą kod razem z Tobą, dzięki oficjalnym umiejętnościom Androida w Gemini w Android Studio i interfejsie wiersza poleceń Androida.

  • Na przykład zobaczymy funkcje adaptacyjne Compose, które zostały udostępnione podczas konferencji Google I/O na początku tego roku.

  • Już wcześniej umożliwiała ona przenoszenie układów telefonów do większych rozmiarów okien.

  • Teraz może też rozwiązać problem z utratą stanu, wdrożyć funkcje unikalne dla urządzeń składanych, takie jak tryb stołowy, oraz ulepszyć obsługę danych wejściowych przez wykrywanie i wdrażanie skrótów klawiszowych, niestandardowych kursorów, etykietek itp.

  • Zacznijmy od Jetpack Compose, który jest podstawą tego wszystkiego.

  • Wersja stabilna Jetpack Compose została wydana w 2021 r., więc w tym roku obchodzimy 5 urodziny.

  • W ciągu tych 5 lat zaobserwowaliśmy w tysiącach aplikacji produkcyjnych, o ile łatwiejsze i bardziej niezawodne jest tworzenie aplikacji adaptacyjnych za pomocą Compose.

  • Programowanie na Androida jest teraz oficjalnie oparte na Compose.

  • Oznacza to, że wszystkie nowe, nowoczesne układy adaptacyjne, style i komponenty nawigacyjne są tworzone pod kątem Compose.

  • Z perspektywy architektury systemu największą zaletą Compose w przypadku aplikacji adaptacyjnych w porównaniu ze starszym systemem widoków jest sposób interakcji stanu i interfejsu.

  • Aby to wyjaśnić, chcę szybko porównać go z systemem widoków.

  • Zastanówmy się, jak działał system widoków.

  • Interfejs był drzewem zmiennych obiektów, które były jednorazowo rozwijane ze statycznych plików XML.

  • Masz różne kwalifikatory zasobów. Zacznijmy od głównego problemu: duplikacji w różnych kwalifikatorach zasobów.

  • Gdy tworzysz adaptacyjny interfejs użytkownika w XML, w pliku układu nie ma żadnej logiki warunkowej.

  • Zamiast tego wystarczy powielić całą hierarchię w folderach zasobów, takich jak układ, układ najmniejszego okna 600 dp, układ poziomy itd.

  • Spójrz na przykład EditText po prawej stronie.

    <LinearLayout android:orientation="vertical">
      <EditText
          android:id="@+id/text_msg"
          android:inputType="textCapCharacters"
          android:maxLength="12" />
    </LinearLayout>
    <ConstraintLayout>
      <TextInputEditText
        android:id="@+id/text_input"
        android:inputType="text" />
    </ConstraintLayout>
  • To dokładnie ten sam komponent logiczny, który obsługuje wprowadzanie tekstu, i jest on rozproszony w różnych plikach XML.

  • Jeśli zaktualizujesz atrybut taki jak inputType lub maxLength w układzie telefonu, kompilator nie wymusi na Tobie zaktualizowania go również w układzie tabletu.

  • Ponieważ komponenty są rozdzielone w różnych plikach, z czasem niemal na pewno dojdzie do dryfu strukturalnego.

  • To powielanie jest też związane z innym problemem, jakim jest utrata stanu podczas zmian konfiguracji.

  • Pamiętaj, że w systemie widoków komponenty interfejsu są stanowe.

  • EditText jest właścicielem wewnętrznego bufora Editable, niestandardowej pozycji kursora, stanu aktywności i niektórych stanów.

  • Dlatego gdy użytkownik na przykład rozłoży urządzenie lub zmieni rozmiar okna powyżej określonego progu, Android niszczy całą hierarchię widoków i od nowa wypełnia nowe pliki XML.

  • Aby zapisać dane wejściowe użytkownika, platforma korzysta z metody onSaveInstanceState, która pakuje stan powiązany z identyfikatorem tego widoku.

  • To jednak stwarza 3 możliwe rodzaje błędów.

  • Po pierwsze, jeśli ktoś przypadkowo nazwał identyfikator text_input zamiast text_msg w innym pliku XML, przywracanie stanu po cichu odrzuci tekst wpisany przez użytkownika bez żadnych ostrzeżeń.

  • Po drugie, onRestoreInstanceState jest uruchamiane po onViewCreated, co może często kolidować z obserwatorami ViewModel i powodować pętle wywołań zwrotnych.

  • Po trzecie, jeśli zmienisz typ widżetu w różnych kwalifikatorach, na przykład na mniejszym ekranie chcesz mieć spinner, a na szerszym RadioGroup, to jeśli mają ten sam identyfikator, spowoduje to błąd ClassCastException w czasie działania.

  • W Jetpack Compose interfejs nie jest obiektem, który tworzysz raz.

  • Interfejs jest czystą funkcją stanu.

  • To całkowicie zmienia definicję zmiany konfiguracji.

  • Czym jest zmiana konfiguracji w Compose?

  • Mam podpowiedź.

Obsługa zmian konfiguracji i funkcji interfejsu

  • Francesco Romano

    Tak.

  • W Compose zmiana konfiguracji to po prostu kolejna rekompozycja, a rekompozycje zdarzają się cały czas.

  • Gdy okno się poszerzy, na przykład po rozłożeniu urządzenia przez użytkownika, funkcje kompozycyjne odczytają nowy rozmiar, a te, których dane wyjściowe od niego zależą, zostaną ponownie uruchomione.

  • To całe wydarzenie.

  • Teraz zobaczmy, co oznacza tworzenie nowoczesnych aplikacji adaptacyjnych przy użyciu najnowszych narzędzi i komponentów Compose.

  • W świecie XML trzeba było zadawać pytania typu „Czy aplikacja działa na telefonie, tablecie czy komputerze?”, a następnie rozgałęziać interfejs na podstawie kategorii urządzenia.

  • Istnieje jednak ważny powód, dla którego nie należy tego robić.

  • Nawet w przypadku tego samego urządzenia fizycznego kontekst aplikacji i możliwości interfejsu mogą się dynamicznie zmieniać w czasie działania.

  • Chcę przekazać, że decyzje dotyczące interfejsu powinny być podejmowane na podstawie aktualnie aktywnych funkcji: bieżącego rozmiaru okna, pozycji i niektórych cech sprzętu.

  • Przyjrzyjmy się klasycznemu przykładowi, czyli trybowi okien na pulpicie na zewnętrznym monitorze.

  • Jeśli podłączysz telefon Pixel do zewnętrznego monitora na biurku, dokładnie ten sam proces aplikacji, który właśnie działał na 6-calowym ekranie w orientacji pionowej, będzie teraz działać na 32-calowym monitorze panoramicznym z podłączoną klawiaturą fizyczną i myszą.

  • Dlatego sprawdzenie typu urządzenia, na którym działa aplikacja, nie gwarantuje niczego w kwestii ekranu, na którym jest ona wyświetlana, ani sposobu wprowadzania danych.

  • Co więcej, Android obsługuje wiele okien, więc można zmieniać ich rozmiar.

  • Nawet jeśli ekran Googlebooka jest większy niż ekran telefonu, aplikacja może działać w oknie o szerokości 400 dp razem z innymi aplikacjami.

  • Dlatego wysyłanie zapytań o statyczne dane dotyczące wyświetlacza fizycznego może być bezcelowe.

  • Aby więc projektować aplikacje z myślą o Compose, musimy najpierw zrozumieć, jak wysyłać zapytania do środowiska.

  • Zacznijmy od próby sformalizowania pojęcia możliwości interfejsu.

  • Możliwości interfejsu obejmują nie tylko szerokość i wysokość okna czy pozycję składanego urządzenia, ale też aktualnie aktywne tryby wejścia i wyjścia.

  • Zauważyliśmy, że w środowisku komputerowym użytkownicy nie zawsze korzystają z ekranu dotykowego.

  • Można używać klawiatury i myszy.

  • W systemie samochodowym niektóre samochody mają pokrętło lub inne wejście, inny sposób wprowadzania sygnałów.

  • Oprócz rozmiaru ekranu i metody wprowadzania danych uwzględnia też odległość użytkownika od wyświetlacza, czyli odległość oglądania, oraz to, czy kamera lub mikrofon są obecnie podłączone.

  • To wszystkie funkcje interfejsu.

Wykrywanie pozycji za pomocą interfejsu mediaQuery API

  • Francesco Romano

    Do tej pory, aby pobrać wszystkie sygnały, trzeba było połączyć 5–6 różnych interfejsów API z różnych źródeł.

  • Potrzebny był obiekt konfiguracji, WindowMetricsCalculator, WindowInfoTracker, InputManager, PackageManager, a następnie trzeba było ręcznie przekonwertować każdy z nich na stan, stan Compose.

  • Czy podobało Ci się to?

  • Nie.

  • Aby rozwiązać ten problem, w androidx.compose.ui wprowadziliśmy interfejs MediaQuery API.

    mediaQuery { ... }
    derivedMediaQuery { ... }
  • Inspiracją dla nich były zapytania o media CSS.

  • Słowo „multimedia” nie oznacza odtwarzania dźwięku ani obrazu.

  • Odnosi się do medium, czyli środowiska, w którym renderowany jest interfejs.

  • MediaQuery zapewnia pojedynczy, deklaratywny, natywny dla Compose sposób wysyłania zapytań o możliwości interfejsu.

  • Przyjrzyjmy się właściwościom udostępnianym w obiekcie UiMediaScope.

    sealed interface UiMediaScope {
        @get:FrequentlyChangingValue val windowWidth: Dp
        @get:FrequentlyChangingValue val windowHeight: Dp
    
        val windowPosture: Posture // Flat, Tabletop, Book
        val pointerPrecision: PointerPrecision // Fine, Coarse, Blunt, None
        val keyboardKind: KeyboardKind // Physical, Virtual, None
    
        val hasCamera: Boolean
        val hasMicrophone: Boolean
        val viewingDistance: ViewingDistance // Near, Medium, Far
    }
  • windowWidth i windowHeight w Dp.

  • windowPosture, który jest obecnie przeznaczony dla urządzeń składanych i może być płaski, na stole lub w trybie książki.

  • pointerPrecision informuje o precyzji głównego urządzenia wskazującego: Fine (dokładne) w przypadku myszy, rysika lub trackpada; Coarse (zgrubne) w przypadku ekranu dotykowego; Blunt (niedokładne) w przypadku joysticka lub kontrolerów; None (brak).

  • keyboardKind może być fizyczny, wirtualny lub żaden.

  • hasCamera i hasMicrophone dynamicznie śledzą, czy ten sprzęt jest dostępny.

  • Zastanów się nad kamerą zewnętrzną, którą można podłączyć.

  • viewingDistance zgłasza, że jest blisko, średnio daleko lub daleko.

  • Myślę jednak, że łatwiej będzie to zrozumieć na prawdziwym przykładzie, czyli wykrywaniu, czy urządzenie jest w trybie stołu.

  • Tryb stołu to pozycja, w której użytkownicy mogą złożyć urządzenie do połowy i zwykle położyć je na powierzchni.

  • Świetnie sprawdza się podczas dłuższego odtwarzania filmów, rozmów wideo czy nagrywania filmów w trybie poklatkowym.

  • Jest też bardzo przydatny, gdy chcesz na przykład postępować zgodnie z przepisem lub obejrzeć pokaz slajdów.

  • Do tej pory, aby wykryć pozycję stołową przed MediaQuery, trzeba było napisać cały ten kod.

    @Composable
    fun currentVideoPlayerLayout(): VideoPlayerLayout {
       val context = LocalContext.current
       val windowLayoutInfo by WindowInfoTracker.getOrCreate(context).windowLayoutInfo(context)
           .collectAsStateWithLifecycle(null)
       val isTabletopPosture = windowLayoutInfo?.displayFeatures?.any { displayFeature ->
           displayFeature is FoldingFeature &&
                   displayFeature.state == FoldingFeature.State.HALF_OPENED &&
                   displayFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
       } ?: false
       return if (isTabletopPosture) {
           VideoPlayerLayout.Tabletop
       } else {
           VideoPlayerLayout.Default
       }
    }
  • Trzeba było uzyskać kontekst, pobrać instancję WindowInfoTracker, zebrać windowLayoutInfo, czyli przepływ, stan Compose uwzględniający cykl życia, a następnie iterować po displayFeatures, aby sprawdzić, czy istnieje FoldingFeature, który jest częściowo otwarty i poziomy.

  • A to tylko po to, aby wykryć postawę.

  • To dużo schematycznych zadań.

  • Dzięki nowemu interfejsowi MediaQuery API wszystko to można skrócić do kilku wierszy kodu.

    @Composable
    fun currentVideoPlayerLayout(): VideoPlayerLayout {
       return mediaQuery {
           when(windowPosture) {
               UiMediaScope.Posture.Tabletop -> VideoPlayerLayout.Tabletop
               else -> VideoPlayerLayout.Default
           }
       }
    }
  • Funkcja mediaQuery przyjmuje pojedynczy parametr lambda, a w zakresie odbiornika tej funkcji lambda udostępnia zestaw właściwości, które właśnie widzieliśmy.

  • W tym przykładzie, sprawdzając windowPosture w wyrażeniu when, możemy natychmiast zwrócić odpowiedni element kompozycyjny.

  • Oto 2 inne przykłady użycia interfejsu API.

    @Composable
    fun AdaptiveChatControls(
        onStartVideoCall: () -> Unit,
        onSendMessage: () -> Unit,
    ) {
        // 1. Return a Dp value directly based on pointer precision
        val chipPadding = mediaQuery {
            if (pointerPrecision == PointerPrecision.Fine) 2.dp else 8.dp
        }
    
        // 2. Return a Boolean combining hardware capabilities
        val canVideoCall = mediaQuery { hasCamera && hasMicrophone }
        val hasPhysicalKeyboard = mediaQuery {
            keyboardKind == KeyboardKind.Physical
        }
    
        Row(modifier = Modifier.padding(chipPadding)) {
            if (canVideoCall) {
                IconButton(onClick = onStartVideoCall) {
                    Icon(Icons.Default.Videocam, contentDescription = "Video call")
                }
            }
            Button(onClick = onSendMessage) {
                Text(if (hasPhysicalKeyboard) "Send (Ctrl+Enter)" else "Send")
            }
        }
    }
  • Pierwszą z nich jest gęstość informacji.

  • Gdy użytkownik korzysta z laptopa lub komputera stacjonarnego z myszą, a precyzja wskaźnika jest wysoka, możemy zmniejszyć dopełnienie komponentów interfejsu, na przykład z 8 dp do 2 dp, aby wygodnie wyświetlać więcej treści. Ponieważ użytkownicy nie używają palców, docelowe elementy dotykowe mogą być mniejsze.

  • Działania oparte na możliwościach.

  • Załóżmy, że mamy pasek wprowadzania czatu. Możemy sprawdzić hasCamera i hasMicrophone, aby dynamicznie wyświetlać lub ukrywać przycisk polecenia rozmowy wideo, albo możemy połączyć typ klawiatury fizycznej, aby wyświetlać lub nie wyświetlać wskazówek dotyczących skrótów klawiszowych, takich jak naciśnięcie Ctrl + Enter, aby wysłać.

  • Wszystkie te informacje są pobierane w czasie działania.

  • MediaQuery świetnie sprawdza się też podczas testowania i generowania podglądu.

    @Preview(name = "Tabletop + Fine Pointer", widthDp = 840, heightDp = 700)
    @Composable
    fun TabletopPlayerPreview() {
        val simulatedMediaScope = object : UiMediaScope {
            override val windowWidth = 840.dp
            override val windowHeight = 700.dp
            override val windowPosture = UiMediaScope.Posture.Tabletop
            override val pointerPrecision = UiMediaScope.PointerPrecision.Fine
            override val keyboardKind = UiMediaScope.KeyboardKind.Physical
            override val hasCamera = true
            override val hasMicrophone = true
            override val viewingDistance = UiMediaScope.ViewingDistance.Medium
        }
    
        CompositionLocalProvider(LocalUiMediaScope provides simulatedMediaScope) {
            VideoPlayerScreen()
        }
    }
  • MediaQuery rozwiązuje odbiorcę za pomocą lokalnej kompozycji o nazwie LocalUiMediaScope.

  • Możesz więc po prostu podać fałszywy obiekt UiMediaScope za pomocą CompositionLocalProvider w podglądzie i nie musisz uruchamiać urządzeń składanych i ustawiać ich pod kątem 90° ani podłączać myszy, aby przeprowadzić test interfejsu Compose.

Elastyczne układy ekranu z siatką i FlexBoxem

  • Francesco Romano

    Wiemy już, jak wysyłać zapytania o możliwości interfejsu.

  • Teraz zobaczmy, jak utworzyć rzeczywisty układ.

  • W tym celu chcę przedstawić siatkę, która jest nowym elementem podstawowym układu w androidx.compose.foundation.layout, który jest teraz stabilny.

    Grid(
        modifier = Modifier.fillMaxSize().padding(16.dp),
        config = {
            val maxWidthDp = constraints.maxWidth.toDp()
            val columnCount = if (maxWidthDp < 600.dp) 2 else 4
    
            repeat(columnCount) {
                column(1.fr)
            }
            row(GridTrackSize.Auto)
            row(1.fr)
    
            gap(row = 16.dp, column = 8.dp)
            flow = GridFlow.Row
        }
    ) {
        // GridScope children...
    }
  • Siatka umożliwia wdrożenie układu dwuwymiarowego przez wyświetlanie elementów w kolumnach i wierszach, które dostosowują się do dostępnego rozmiaru kontenera.

  • Zobaczmy, co można zrobić w bloku konfiguracji.

  • Możesz dołączać nowe elementy lub ścieżki, wywołując kolumnę lub wiersz, i używać standardowych pętli Kotlin, takich jak repeat.

  • Możesz ustawić odstępy między ścieżkami.

  • Kierunek automatycznego rozmieszczania możesz kontrolować za pomocą właściwości flow-direction: GridFlow.Row lub GridFlow.Column.

  • Oto kluczowy szczegół architektury.

  • Blok konfiguracji jest uruchamiany podczas etapu pomiaru, a zakres konfiguracji siatki implementuje gęstość i udostępnia ograniczenia.

  • Oznacza to, że możesz sprawdzić constraints.maxWidth.toDp() bezpośrednio w konfiguracji, aby obliczyć ścieżki bez dodatkowych etapów rekompozycji.

  • W siatce, gdy dodasz element podrzędny bez modyfikatora, automatycznie przejdzie on do następnej komórki.

    Grid(
        config = {
            repeat(3) { column(1.fr) }
            repeat(3) { row(100.dp) }
            gap(8.dp)
        }
    ) {
        // Spans 2x2 area (rows 1..2, columns 1..2)
        HeroCard(modifier = Modifier.gridItem(rows = 1..2, columns = 1..2))
    
        // Overlaps on top of HeroCard in cell (1, 1) aligned to TopStart!
        LiveBadge(
            modifier = Modifier.gridItem(
                row = 1,
                column = 1,
                alignment = Alignment.TopStart
            )
        )
    
        // Negative index: placed in last row (-1), spanning all 3 columns
        FooterBar(modifier = Modifier.gridItem(row = -1, column = 1, columnSpan = 3))
    }
  • Jeśli chcesz mieć pełną kontrolę, możesz użyć Modifier.gridItem i współrzędnych zaczynających się od 1.

  • Wiersz 1, kolumna 1 to komórka w lewym górnym rogu.

  • Obsługuje indeksowanie ujemne, więc wiersz -1 jest ostatnim wierszem itd.

  • Możesz użyć właściwości rowSpan, aby rozciągnąć element na obszar dwuwymiarowy, a następnie nałożyć na niego inny element.

  • Jeśli więc 2 elementy zostaną umieszczone w nakładających się komórkach, Grid ułoży je jeden na drugim w tych granicach, a kolejność osi Z zostanie określona przez kolejność kompozycji.

  • Późniejsze elementy będą więc rysowane na innych, tak jak w przypadku Box.

  • Znamy już podstawy siatki.

  • Przeanalizujmy przykład odtwarzacza wideo.

  • W zależności od tego, czy urządzenie jest rozłożone na płasko czy do połowy, musimy przełączać się między układem domyślnym a układem stołu.

  • Przed wprowadzeniem siatki musieliśmy utworzyć 2 różne funkcje kompozycyjne, DefaultLayout i TabletopLayout, a następnie rozgałęzić kod w wyrażeniu when.

    @Composable
    fun VideoPlayerLayout(
       player: @Composable () -> Unit,
       controller: @Composable () -> Unit,
       modifier: Modifier = Modifier
    ) {
       when(currentVideoPlayerLayout()) {
           VideoPlayerLayout.Default -> DefaultLayout(player, controller, modifier)
           VideoPlayerLayout.Tabletop -> TabletopLayout(player, controller, modifier)
       }
    }
  • Możesz pomyśleć: „Co w tym złego?”.

  • Jeśli przyjrzysz się tym układom, zobaczysz, że ich zawartość jest identyczna.

  • Na obu wyświetla się dokładnie ten sam odtwarzacz i dokładnie ten sam kontroler.

  • Jedyną różnicą między nimi jest sposób rozmieszczenia tych 2 elementów na ekranie.

  • Do tej pory nie mieliśmy możliwości przekazania samego układu jako parametru, więc musieliśmy zdefiniować 2 osobne funkcje kompozycyjne.

  • Teraz możemy sparametryzować konfigurację układu i obsłużyć wszystko w ramach jednej funkcji typu „composable”.

  • Zobaczmy, jak to zrobić.

  • Dzięki interfejsowi Grid API możemy definiować nazwane obszary jako typy wyliczeniowe.

    enum class VideoPlayerArea { Player, Controller }
    
    Grid(
        config = { ... },
        modifier = modifier
    ) {
        Box(modifier = Modifier.gridItem(areaId = VideoPlayerArea.Player)) {
            player()
        }
        Box(modifier = Modifier.gridItem(areaId = VideoPlayerArea.Controller)) {
           controller()
       }
    }
  • Następnie możemy określić elementy kompozycyjne podrzędne, odtwarzacz i kontroler, które mają zostać umieszczone w siatce, i powiązać każdy element podrzędny z nazwanym obszarem, wywołując Modifier.gridItem i przekazując areaId.

  • W tym miejscu informujemy Grid, że odtwarzacz ma zostać umieszczony w obszarze o nazwie VideoPlayerArea.Player, a kontroler – w obszarze VideoPlayerArea.Controller.

  • Tutaj definiujemy 2 lambdy konfiguracji w VideoPlayerGridConfiguration.

    object VideoPlayerGridConfiguration {
       val Default: GridConfigurationScope.() -> Unit = {
           row(1f)
           column(1f)
           area(areaId = VideoPlayerArea.Player, row = 1, column = 1)
           area(areaId = VideoPlayerArea.Controller, row = 1, column = 1)
       }
    
       val Tabletop: GridConfigurationScope.() -> Unit = {
           row(0.5f)
           row(0.5f)
           column(1f)
           area(areaId = VideoPlayerArea.Player, row = 1, column = 1)
           area(areaId = VideoPlayerArea.Controller, row = 2, column = 1)
       }
    }
  • W przypadku domyślnego układu odtwarzacza wideo konfigurujemy siatkę z 1 wierszem i 1 kolumną, ale zauważ, że nawet jeśli jest tylko 1 komórka, możemy przypisać do niej 2 nazwy obszarów.

  • W tym przypadku, jak już widzieliśmy, kontroler jest nałożony na odtwarzacz.

  • W przypadku trybu stołowego definiujemy tylko 2 wiersze i 1 kolumnę, umieszczając odtwarzacz w górnej połowie, a kontroler w dolnej, mniej więcej tak.

  • Po zdefiniowaniu tych funkcji lambda konfiguracji możemy użyć MediaQuery bezpośrednio w konfiguracji siatki.

    @Composable
    fun VideoPlayerLayout(
       player: @Composable () -> Unit,
       controller: @Composable () -> Unit,
       modifier: Modifier = Modifier
    ) {
       Grid(
           config = { mediaQuery {
           if (isTabletopPosture) { VideoPlayerGridConfiguration.Tabletop }
     else { VideoPlayerGridConfiguration.Default }
    }
       }
           modifier = modifier
       ) {
           Box(modifier = Modifier.gridItem(areaId = VideoPlayerArea.Player)) {
               player()
           }
           Box(modifier = Modifier.gridItem(areaId = VideoPlayerArea.Controller)) {
               controller()
           }
       }
    }
  • Możemy teraz mieć jeden układ odtwarzacza, który można komponować z interfejsem i który dynamicznie przełącza się między 2 konfiguracjami.

  • Kolejny element podstawowy, o którym chcę opowiedzieć, to FlexBox.

    @Composable
    fun AdaptiveActionHeader(tags: List<String>) {
            FlexBox(
            config = {
                direction( if (isCompact)
    FlexDirection.Column else FlexDirection.Row )
                wrap(FlexWrap.Wrap)
                alignItems(FlexAlignItems.Center)
                gap(row = 8.dp, column = 12.dp)
            },
            modifier = Modifier.fillMaxWidth()
        ) {
            Text(
                text = "Home",
                modifier = Modifier.flex {
                    basis(FlexBasis.Auto)
                    grow(1f)
                    shrink(1f)
                }
            )
            tags.forEach { tag ->
                FilterChipItem(
                    label = tag,
                    modifier = Modifier.flex { shrink(0f) }
                )
            }
        }
    }
  • Podczas gdy Grid obsługuje dwuwymiarowe układy wierszy i kolumn, FlexBox, który jest teraz stabilny, obsługuje układy jednowymiarowe, które nadal wymagają inteligentnego rozmieszczania, zmniejszania i zawijania wierszy.

  • Jest to wygodny, elastyczny układ, którego można używać zamiast 2 osobnych funkcji kompozycyjnych, takich jak FlowRow i FlowColumn, ponieważ jest to pojedynczy kontener, którego kierunek może zmieniać się dynamicznie w bloku konfiguracji bez konieczności ponownego tworzenia podrzędnych funkcji kompozycyjnych.

  • Implementuje też pełny algorytm CSS FlexBox, dzięki czemu masz do dyspozycji wrap, justify-content, align-items i gap.

  • Każdy element podrzędny ma właściwość Modifier.flex, która określa podstawę, względny wzrost, współczynniki zmniejszania, alignSelf itd.

Architektura układu na poziomie aplikacji i Navigation 3

  • Francesco Romano

    Teraz przejdźmy od definicji na poziomie ekranu do układów na poziomie aplikacji, czyli sposobu, w jaki strukturyzujemy nawigację najwyższego poziomu i układy ekranu z wieloma panelami w całej aplikacji, a nie tylko na jednym ekranie.

  • MediaQuery podaje dokładną szerokość i wysokość okna w dp, ale w przypadku architektury na poziomie aplikacji potrzebne są ustandaryzowane, ujednolicone punkty przerwania widocznego obszaru, czyli klasy rozmiaru okna.

  • Zasadniczo chcesz, aby każdy ekran był elastyczny w ramach każdego z tych przedziałów, ale możesz wprowadzać więcej zmian strukturalnych i aktualizacji oraz różnic między różnymi przedziałami.

  • Może na przykład wyświetlać tylko listę na kompaktowym ekranie lub listę i szczegóły w rozwiniętym oknie.

  • Możesz tworzyć klasy wielkości okna bezpośrednio na podstawie UiMediaScope w bardzo przejrzysty sposób, czyli definiując właściwość rozszerzenia w UiMediaScope.

    val UiMediaScope.windowSizeClass: WindowSizeClass
        @Composable get() = derivedMediaQuery {
            WindowSizeClass.BREAKPOINTS_V2.computeWindowSizeClass(
                widthDp = windowWidth.value,
                heightDp = windowHeight.value
            )
        }
    
    @Composable
    fun AppRoot() {
        val sizeClass = mediaQuery { windowSizeClass }
        val isExpandedWidth = sizeClass.isWidthAtLeastBreakpoint(
            WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND // 600dp
        )
        // Drive app-level navigation and scene strategies...
    }
  • Zwróć uwagę, że używa on derivedMediaQuery, a nie standardowego MediaQuery.

  • Dlaczego?

  • Ponieważ szerokość i wysokość okna to wartości, które często się zmieniają.

  • Gdy użytkownicy zmieniają rozmiar aplikacji, np. na komputerze, te wartości mogą się zmieniać w każdej klatce.

  • Prawdopodobnie nie chcesz rekomponować rusztowania na poziomie aplikacji w każdej klatce, ale tylko wtedy, gdy okno faktycznie przekroczy punkt przerwania.

  • Gdy mamy już klasy rozmiaru okna, jak możemy przejść z przepływu z jednym panelem do układów z wieloma panelami, gdy mamy więcej miejsca?

  • W tym przypadku świetnie sprawdza się Jetpack Navigation 3, ponieważ ten komponent został od podstaw zaprojektowany pod kątem stanu i scen Compose.

  • Po pierwsze, masz stos wsteczny, który jest po prostu listą kluczy nawigacyjnych, dzięki czemu jest bardzo elastyczny i łatwy do zapisania jako stan.

  • Każdy klucz na stosie jest powiązany z obiektem NavEntry zawierającym funkcję kompozycyjną i metadane.

  • NavDisplay nie ma zakodowanego na stałe sposobu rysowania wpisów. Zamiast tego ocenia listę strategii scen.

  • W ramach strategii możesz sprawdzić możliwości interfejsu, np. klasy rozmiaru okna.

    if (isExpandedWidth) {
        /* return a multi-pane scene */
    }
    else {
        /* return a single pane scene */
    }
  • Możesz sprawdzić stos wsteczny, a następnie zdecydować, czy chcesz renderować jeden element NavEntry w jednym panelu, czy kilka elementów jednocześnie lub jeden na drugim.

  • To bardzo elastyczne rozwiązanie.

  • Oprócz strategii wielopanelowej klasy rozmiaru okna określają też, jakich komponentów nawigacyjnych chcesz używać, aby zmaksymalizować ergonomię i wykorzystanie miejsca na ekranie.

  • Na przykład na urządzeniach o szerokości kompaktowej chcesz mieć dolny pasek nawigacyjny, na urządzeniach o szerokości średniej – kolumnę nawigacji, a na urządzeniach o szerokości rozszerzonej – pełny panel nawigacji.

  • Połączenie Nav3 do podejmowania decyzji dotyczących układu na poziomie aplikacji i komponentów elastycznych do definiowania na poziomie ekranu pozwala uzyskać aplikacje, które wyglądają tak i świetnie prezentują się w praktycznie każdym rozmiarze okna i środowisku.

  • Jeśli chcesz dowiedzieć się więcej o najnowszych aktualizacjach Nav 3, w tym o obsłudze precyzyjnych linków, obejrzyj prezentację Tworzenie bezpiecznych precyzyjnych linków za pomocą Jetpack Navigation 3.

Wersja demonstracyjna programowania opartego na agentach i automatyzacji

  • Francesco Romano

    Omówiliśmy dziś wiele nowych interfejsów API: MediaQuery, Grid, FlexBox, Nav3.

  • To już wszystkie informacje o tym, jak Jetpack Compose pomaga tworzyć aplikacje adaptacyjne.

  • Jednak w erze rozwoju opartego na agentach Jetpack Compose ma jeszcze jedną niezwykle ważną zaletę: jest znacznie skuteczniejszy.

  • Prawdopodobnie słyszeliście podczas prezentacji, że po przejściu na Compose aplikacje używają o ponad 30% mniej tokenów. Pracujemy nad jeszcze większą optymalizacją Agentic Workflows w aplikacjach Compose.

  • Dzisiaj chcę wyróżnić duże aktualizacje umiejętności Compose Adaptive.

    android skills list
    android skills add adaptive
  • Jak już wspominałem, oprócz migracji układów do Grid, Flexbox i MediaQuery, teraz wyraźnie obsługuje on kompleksowe zarządzanie stanem w całej aplikacji. Testujemy tę umiejętność pod kątem rzeczywistych błędów związanych z utratą stanu: zachowywanie nawigacji, pozycji przewijania, aktywnego odtwarzania multimediów, częściowo wpisanego tekstu itd.

  • Umiejętność obejmuje teraz również optymalizację pozycji urządzenia składanego, np. automatyczne refaktoryzowanie ekranów multimediów i aparatu do trybu stołu, ale gdy aplikacja działa na Googlebooku lub zewnętrznym monitorze z myszą i klawiaturą, układ to tylko połowa sukcesu.

  • Umiejętność adaptacyjna może teraz sprawdzać interfejs użytkownika, aby znajdować i wdrażać skróty klawiszowe, niestandardowe kursory, stany najechania, etykietki itp.

  • Zobaczmy, jak to działa.

  • Nagrałem wersję demonstracyjną.

  • Mamy już aplikację.

  • Możemy popatrzeć na nasze zwierzęta.

  • Możemy wysyłać im wiadomości.

  • Możemy obejrzeć film o nich.

  • Tak, widzimy najpopularniejsze zwierzęta.

  • Ale już widzisz, że nie jest to najlepsze rozwiązanie.

  • Teraz zobaczmy, czy uda mi się znaleźć zdjęcie z kotami i odrobiną dramatyzmu.

  • Chcę zobaczyć je w większym rozmiarze.

  • Dah, to powrót do domu.

  • OK, powiem o tym koledze.

  • No tak, „Buddy”, hm.

  • Aplikacja nie jest zbyt dobra.

  • Jest też za szeroki.

  • Widzisz, że jest za szeroki.

  • Spróbujmy zmienić jego rozmiar.

  • Co?

  • No dobrze.

  • Wpisany tekst został utracony.

  • Teraz poprosimy Gemini o naprawienie tego błędu.

  • Dzięki umiejętności adaptacyjnej prompt może być tak prosty jak „dostosuj aplikację”.

  • Jak widzisz, używam modelu Gemini 3.0 Flash, który wraz z umiejętnością jest w stanie to zrobić.

  • Została ona wykryta i teraz zaczyna wprowadzać zmiany.

  • Zada kilka pytań, np. „MediaQuery jest eksperymentalny.

  • Czy naprawdę chcesz go użyć?”

  • I kontynuuj.

  • Musiałem przyspieszyć, żeby coś wyciąć, ale jeśli przyjdziesz do obszaru demonstracyjnego, możesz samodzielnie wypróbować umiejętności.

  • A to tylko jedna z umiejętności, które udostępniliśmy.

  • Mamy też na przykład nową umiejętność, która pomoże Ci tworzyć niesamowite funkcje rysika.

  • Przyjdź na miejsce, aby wypróbować tę funkcję.

  • Wszystko gotowe.

  • Zmodyfikowano 11 plików.

  • Zobaczmy, jak to działa.

  • OK.

  • Grid już działa.

  • Nawigacja została przeniesiona na bok.

  • Wypróbujmy filtry.

  • Myślę, że teraz będą działać.

  • Tak, mają.

  • OK, wyślę wiadomość do znajomego i powiem mu, że aplikacja jest teraz świetna.

  • Zobaczmy, jak to wygląda, gdy mamy więcej miejsca.

  • Tak, został przeniesiony do układu szczegółowej listy.

  • To uruchomienie trwało około 20 minut i wykorzystało około 200 tys. tokenów.

  • OK.

ROI aplikacji adaptacyjnych i najważniejsze wnioski

  • Francesco Romano

    Zwykle zaczynam od wskaźników biznesowych i przypadków sukcesu, ale w przypadku ADS chciałem od razu przejść do kodu.

  • Chcę jednak poświęcić kilka minut na omówienie zwrotu z inwestycji w inżynierię.

  • Obecnie aplikacja na Androida może działać na ponad 580 milionach aktywnych urządzeń z dużym ekranem. Wiemy też, że użytkownicy urządzeń składanych i użytkownicy korzystający z wielu urządzeń należą do najbardziej zaangażowanych segmentów na Androidzie.

  • Przyjrzyjmy się 2 konkretnym studiom przypadków, które pokazują, co się dzieje, gdy udostępniasz kompilację adaptacyjną.

  • Pierwsza to Gratitude, popularna aplikacja do dbania o siebie i prowadzenia dziennika.

  • Dzięki zastosowaniu adaptacyjnych projektów ekranów firma Gratitude uczyniła z tabletów i urządzeń składanych kanał pozyskiwania klientów o wysokim potencjale wzrostu.

  • Po pierwsze, podwoili tempo pozyskiwania użytkowników na dużych ekranach.

  • Dzienna liczba instalacji na tabletach i urządzeniach składanych wzrosła o 100%.

  • Zwiększyli udział dużych ekranów w łącznej liczbie użytkowników z 7% do 10%.

  • Drugą aplikacją jest BoxBoxClub, która jest aplikacją towarzyszącą dla Formuły 1 i sportów motorowych.Po zaktualizowaniu jej pod kątem urządzeń składanych i większych ekranów dane telemetryczne wykazały ogromne wzrosty: o 7,6% w przypadku ogólnej liczby aktywnych użytkowników i o 54% w przypadku czasu zaangażowania.

  • OK.

  • Zbliżamy się do końca prezentacji. Chcę, abyście zapamiętali 3 ważne rzeczy.

  • Dzięki temu aplikacja może obsługiwać więcej urządzeń i użytkowników, a jednocześnie jest przygotowana na przyszłość w tym rozwijającym się ekosystemie.

  • Programowanie na Androida opiera się na Compose.

  • Wszystkie nowe narzędzia, biblioteki i umiejętności są tworzone z myślą o Compose.

  • Compose doskonale nadaje się do układów adaptacyjnych i programowania agentowego.

  • Bazy kodu są mniejsze i łatwiejsze do zrozumienia, a agenty używają mniej tokenów podczas pracy z kodem Compose.

  • Wypróbuj wszystkie nowe narzędzia, umiejętności i interfejsy API.

  • Dziękujemy.

Kod

Fragmenty kodu

Fragment kodu XML w 05:17 w filmie „Wprowadzenie do skalowania aplikacji na różnych ekranach”
<LinearLayout android:orientation="vertical">
  <EditText
      android:id="@+id/text_msg"
      android:inputType="textCapCharacters"
      android:maxLength="12" />
</LinearLayout>
Fragment kodu XML w 05:17 w filmie „Wprowadzenie do skalowania aplikacji na różnych ekranach”
<ConstraintLayout>
  <TextInputEditText
    android:id="@+id/text_input"
    android:inputType="text" />
</ConstraintLayout>
Fragment kodu w języku Kotlin w 11:31 w filmie „Detecting postures with the mediaQuery API”
mediaQuery { ... }
derivedMediaQuery { ... }
Fragment kodu w języku Kotlin w 11:57 w filmie „Detecting postures with the mediaQuery API”
sealed interface UiMediaScope {
    @get:FrequentlyChangingValue val windowWidth: Dp
    @get:FrequentlyChangingValue val windowHeight: Dp

    val windowPosture: Posture // Flat, Tabletop, Book
    val pointerPrecision: PointerPrecision // Fine, Coarse, Blunt, None
    val keyboardKind: KeyboardKind // Physical, Virtual, None

    val hasCamera: Boolean
    val hasMicrophone: Boolean
    val viewingDistance: ViewingDistance // Near, Medium, Far
}
Fragment kodu w Kotlinie w 13:11 w filmie „Detecting postures with the mediaQuery API”
@Composable
fun currentVideoPlayerLayout(): VideoPlayerLayout {
   val context = LocalContext.current
   val windowLayoutInfo by WindowInfoTracker.getOrCreate(context).windowLayoutInfo(context)
       .collectAsStateWithLifecycle(null)
   val isTabletopPosture = windowLayoutInfo?.displayFeatures?.any { displayFeature ->
       displayFeature is FoldingFeature &&
               displayFeature.state == FoldingFeature.State.HALF_OPENED &&
               displayFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
   } ?: false
   return if (isTabletopPosture) {
       VideoPlayerLayout.Tabletop
   } else {
       VideoPlayerLayout.Default
   }
}
Fragment kodu w języku Kotlin w 13:42 w filmie „Detecting postures with the mediaQuery API”
@Composable
fun currentVideoPlayerLayout(): VideoPlayerLayout {
   return mediaQuery {
       when(windowPosture) {
           UiMediaScope.Posture.Tabletop -> VideoPlayerLayout.Tabletop
           else -> VideoPlayerLayout.Default
       }
   }
}
Fragment kodu w Kotlinie w 14:07 w filmie „Detecting postures with the mediaQuery API”
@Composable
fun AdaptiveChatControls(
    onStartVideoCall: () -> Unit,
    onSendMessage: () -> Unit,
) {
    // 1. Return a Dp value directly based on pointer precision
    val chipPadding = mediaQuery {
        if (pointerPrecision == PointerPrecision.Fine) 2.dp else 8.dp
    }

    // 2. Return a Boolean combining hardware capabilities
    val canVideoCall = mediaQuery { hasCamera && hasMicrophone }
    val hasPhysicalKeyboard = mediaQuery {
        keyboardKind == KeyboardKind.Physical
    }

    Row(modifier = Modifier.padding(chipPadding)) {
        if (canVideoCall) {
            IconButton(onClick = onStartVideoCall) {
                Icon(Icons.Default.Videocam, contentDescription = "Video call")
            }
        }
        Button(onClick = onSendMessage) {
            Text(if (hasPhysicalKeyboard) "Send (Ctrl+Enter)" else "Send")
        }
    }
}
Fragment kodu w języku Kotlin w 14:59 w filmie „Detecting postures with the mediaQuery API”
@Preview(name = "Tabletop + Fine Pointer", widthDp = 840, heightDp = 700)
@Composable
fun TabletopPlayerPreview() {
    val simulatedMediaScope = object : UiMediaScope {
        override val windowWidth = 840.dp
        override val windowHeight = 700.dp
        override val windowPosture = UiMediaScope.Posture.Tabletop
        override val pointerPrecision = UiMediaScope.PointerPrecision.Fine
        override val keyboardKind = UiMediaScope.KeyboardKind.Physical
        override val hasCamera = true
        override val hasMicrophone = true
        override val viewingDistance = UiMediaScope.ViewingDistance.Medium
    }

    CompositionLocalProvider(LocalUiMediaScope provides simulatedMediaScope) {
        VideoPlayerScreen()
    }
}
Fragment kodu w języku Kotlin w 15:40 w filmie „Responsive screen layouts with Grid and FlexBox”
Grid(
    modifier = Modifier.fillMaxSize().padding(16.dp),
    config = {
        val maxWidthDp = constraints.maxWidth.toDp()
        val columnCount = if (maxWidthDp < 600.dp) 2 else 4

        repeat(columnCount) {
            column(1.fr)
        }
        row(GridTrackSize.Auto)
        row(1.fr)

        gap(row = 16.dp, column = 8.dp)
        flow = GridFlow.Row
    }
) {
    // GridScope children...
}
Fragment kodu w języku Kotlin w 16:50 w filmie „Responsive screen layouts with Grid and FlexBox”
Grid(
    config = {
        repeat(3) { column(1.fr) }
        repeat(3) { row(100.dp) }
        gap(8.dp)
    }
) {
    // Spans 2x2 area (rows 1..2, columns 1..2)
    HeroCard(modifier = Modifier.gridItem(rows = 1..2, columns = 1..2))

    // Overlaps on top of HeroCard in cell (1, 1) aligned to TopStart!
    LiveBadge(
        modifier = Modifier.gridItem(
            row = 1,
            column = 1,
            alignment = Alignment.TopStart
        )
    )

    // Negative index: placed in last row (-1), spanning all 3 columns
    FooterBar(modifier = Modifier.gridItem(row = -1, column = 1, columnSpan = 3))
}
Fragment kodu w języku Kotlin w 17:59 w filmie „Responsive screen layouts with Grid and FlexBox”
@Composable
fun VideoPlayerLayout(
   player: @Composable () -> Unit,
   controller: @Composable () -> Unit,
   modifier: Modifier = Modifier
) {
   when(currentVideoPlayerLayout()) {
       VideoPlayerLayout.Default -> DefaultLayout(player, controller, modifier)
       VideoPlayerLayout.Tabletop -> TabletopLayout(player, controller, modifier)
   }
}
Fragment kodu w języku Kotlin w 18:51 w filmie „Responsive screen layouts with Grid and FlexBox”
enum class VideoPlayerArea { Player, Controller }

Grid(
    config = { ... },
    modifier = modifier
) {
    Box(modifier = Modifier.gridItem(areaId = VideoPlayerArea.Player)) {
        player()
    }
    Box(modifier = Modifier.gridItem(areaId = VideoPlayerArea.Controller)) {
       controller()
   }
}
Fragment kodu w języku Kotlin w 19:20 w filmie „Responsive screen layouts with Grid and FlexBox”
object VideoPlayerGridConfiguration {
   val Default: GridConfigurationScope.() -> Unit = {
       row(1f)
       column(1f)
       area(areaId = VideoPlayerArea.Player, row = 1, column = 1)
       area(areaId = VideoPlayerArea.Controller, row = 1, column = 1)
   }

   val Tabletop: GridConfigurationScope.() -> Unit = {
       row(0.5f)
       row(0.5f)
       column(1f)
       area(areaId = VideoPlayerArea.Player, row = 1, column = 1)
       area(areaId = VideoPlayerArea.Controller, row = 2, column = 1)
   }
}
Fragment kodu w języku Kotlin w 20:00 w filmie „Responsive screen layouts with Grid and FlexBox”
@Composable
fun VideoPlayerLayout(
   player: @Composable () -> Unit,
   controller: @Composable () -> Unit,
   modifier: Modifier = Modifier
) {
   Grid(
       config = { mediaQuery {
       if (isTabletopPosture) { VideoPlayerGridConfiguration.Tabletop }
 else { VideoPlayerGridConfiguration.Default }
}
   }
       modifier = modifier
   ) {
       Box(modifier = Modifier.gridItem(areaId = VideoPlayerArea.Player)) {
           player()
       }
       Box(modifier = Modifier.gridItem(areaId = VideoPlayerArea.Controller)) {
           controller()
       }
   }
}
Fragment kodu w języku Kotlin w 20:16 w filmie „Responsive screen layouts with Grid and FlexBox”
@Composable
fun AdaptiveActionHeader(tags: List<String>) {
        FlexBox(
        config = {
            direction( if (isCompact)
FlexDirection.Column else FlexDirection.Row )
            wrap(FlexWrap.Wrap)
            alignItems(FlexAlignItems.Center)
            gap(row = 8.dp, column = 12.dp)
        },
        modifier = Modifier.fillMaxWidth()
    ) {
        Text(
            text = "Home",
            modifier = Modifier.flex {
                basis(FlexBasis.Auto)
                grow(1f)
                shrink(1f)
            }
        )
        tags.forEach { tag ->
            FilterChipItem(
                label = tag,
                modifier = Modifier.flex { shrink(0f) }
            )
        }
    }
}
Fragment kodu w języku Kotlin w 22:09 w filmie „Architektura układu na poziomie aplikacji i Navigation 3”
val UiMediaScope.windowSizeClass: WindowSizeClass
    @Composable get() = derivedMediaQuery {
        WindowSizeClass.BREAKPOINTS_V2.computeWindowSizeClass(
            widthDp = windowWidth.value,
            heightDp = windowHeight.value
        )
    }

@Composable
fun AppRoot() {
    val sizeClass = mediaQuery { windowSizeClass }
    val isExpandedWidth = sizeClass.isWidthAtLeastBreakpoint(
        WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND // 600dp
    )
    // Drive app-level navigation and scene strategies...
}
Fragment kodu w języku Kotlin w 23:33 w filmie „Architektura układu na poziomie aplikacji i Navigation 3”
if (isExpandedWidth) {
    /* return a multi-pane scene */
}
else {
    /* return a single pane scene */
}
Fragment kodu powłoki w 25:18 w filmie „Agentic development and automation demo”
android skills list
android skills add adaptive