Pantrix UI

Hangi pencere yardımcısı, ne zaman

Bir ekran ya da bileşen yazarken pencere yardımcılarından hangisinin nasıl kullanılacağı üç örnek vaka üzerinden.

Buradaki her örnek, örnek uygulamada çalışır hâlde durur ve AdaptiveSamplesTest ile sınanır:

SayfaNe gösterirDosyaWeb adresi
Giriş ekranıVaka 1: tanıtım ve giriş ekranıAdaptiveLoginSamples.kt#adaptive-login
TabloVaka 2: tablo bileşeniAdaptiveTableSamples.kt#adaptive-table
Göster ve gizleVaka 3: göstermek, gizlemek, taşımakAdaptiveVisibilitySamples.kt#adaptive-visibility
Pencere sınıflarıPencerenin ve her yardımcının o anki cevabıAdaptiveWindowSamples.kt#adaptive-window

Dosyalar app/shared/src/commonMain/kotlin/com/pantrixui/cmp/demo/ altındadır. Sayfalar örnek uygulamada "Uyarlanabilir" bölümündedir; her sayfa başlığı ve açıklaması olan örneklerden oluşur ve her örneğin kodu yanındaki "Kod" görünümünde durur. Aşağıda tırnak içinde yazan adlar o örneklerin başlıklarıdır.

1. Yazmadan önce iki soru

Ekran mı yazıyorum, bileşen mi?

Pencerenin tamamını kaplamayan bir ekran (yanında geniş bir sidebar varsa, bir panelin içinde açılıyorsa) için pencerenin sınıfı, ona kalan yeri olduğundan geniş gösterir. Öyle bir ekran da bileşen gibi davranır: kendi yerini ölçer.

Pencereye mi bakıyorum, platforma mı, girdiye mi?

2. Hangi yardımcı, ne zaman

İhtiyaçYazÖrnek
Genişliğe göre bir değer: sütun sayısı, boşluk, pay, yazı stiliwindow.byWidth(...)window.byWidth(compact = 2, medium = 4)
Aynısı, pencere elde değilken, composable içinderesponsive(...)responsive(compact = 1, medium = 2, large = 3)
Sayfanın kenar boşluğuPantrixTheme.layout.marginModifier.padding(horizontal = PantrixTheme.layout.margin)
Sütunların ve panellerin arasıPantrixTheme.layout.gutterArrangement.spacedBy(PantrixTheme.layout.gutter)
Yüksekliğe göre bir değerwindow.byHeight(...)window.byHeight(compact = 44.dp, medium = 56.dp)
Katlanabilir duruşuna göre bir değerwindow.byPosture(...)window.byPosture(flat = 1, book = 2, tabletop = 2)
"Telefon düzeni mi?"window.isNarrowif (window.isNarrow) BottomBar()
"Alta çubuk sığar mı?" (yatay telefon)window.isShortif (window.isShort) HeaderAction() else BottomBar()
"Şu sınıftan itibaren"widthClass >= …if (window.widthClass >= WindowWidthClass.Expanded) DetailPane()
"Şu iki sınıf arasında"widthClass in a..bwindow.widthClass in WindowWidthClass.Medium..WindowWidthClass.Expanded
Sınıflara uymayan bir genişlik eşiğirememberWindowIsAtLeast(dp)val threeColumns = rememberWindowIsAtLeast(1000.dp)
Bir kez verilip saklanan bir kararwindow.isSizeKnownif (window.isSizeKnown) App()
Bileşenin kendi yeriBoxWithPantrixWindow { box -> … }BoxWithPantrixWindow(modifier) { box -> if (box.isNarrow) List() else Table() }
Elle sınıflanan bir boyutPantrixWindow(size, breakpoints = …)PantrixWindow(paneSize, breakpoints = window.breakpoints)
Ekranın bir parçasına başka bir pencereLocalPantrixWindow provides …
Platforma özgü bir parçaPantrixTheme.platform.typeif (platform == PlatformType.Web) DownloadBanner()
Parmağa ya da fareye özgü bir parçaPantrixTheme.platform.isTouchif (platform.isTouch) PullToRefreshHint()
Koşula bağlı bir modifierModifier.thenIf(...)Modifier.thenIf(window.isNarrow) { fillMaxWidth() }

Kısa kural: değer gerekiyorsa by… ailesi, evet/hayır gerekiyorsa is… ya da sınıf karşılaştırması.

Tek tek

Örnek uygulamada nerede

YardımcıKullanıldığı yer
byWidthGiriş ekranı: tanıtımın payı ve başlığın yazı stili. Pencere sınıfları: "Sınıfa göre sütun sayısı", "Bu pencerede bir düzenin seçeceği"
byHeightGiriş ekranı: çerçevenin boyu, formun satır aralığı. Pencere sınıfları: "Yüksekliğe göre değer"
byPosturePencere sınıfları: "Katlanma duruşu" ve "Bu pencerede bir düzenin seçeceği"
isNarrowGiriş ekranı: tanıtım. Tablo: liste ya da tablo (kutuya sorulur). Göster ve gizle: pencereye göre gösterilenler, düğme, sayaçlar. Pencere sınıfları: "Dar mı, kısa mı"
isShortGiriş ekranı: tanıtımın maddeleri. Göster ve gizle: "Pencereye göre". Tablo: "Genişlik kutudan, yükseklik pencereden"
isSizeKnownPencere sınıfları: "Pencerenin boyutu"
responsiveGiriş ekranı: tanıtımın ve formun iç boşluğu. Pencere sınıfları: "Kısa yazım: responsive"
PantrixTheme.layout.marginHer sayfanın yanlardaki boşluğu ve başlığın hizası (DemoPage, DemoTopBar). Pencere sınıfları: "Düzen ölçüleri"
PantrixTheme.layout.gutterPencere sınıfları: "Sınıfa göre sütun sayısı"nda kutuların arası, ve "Düzen ölçüleri". Tablo: "Kutunun ölçüleri"
rememberWindowIsAtLeastPencere sınıfları: "Sınıflara uymayan bir eşik"
Sınıf karşılaştırması ve aralıkTablo: tarih sütunu. Göster ve gizle: "Pencereye göre"
BoxWithPantrixWindowTablo: bileşenin kendi kutusu, ve sayfanın öteki örnekleri
PantrixWindow(size, breakpoints)Örnek kartlarındaki pencere seçicisi (gerçek pencerenin sınırlarıyla). Göster ve gizle: "Bir parçaya kendi penceresi". Tablo: "Kutunun sınırları"
LocalPantrixWindowÖrnek kartlarındaki pencere seçicisi. Göster ve gizle: "Bir parçaya kendi penceresi". Tablo: "Kutuyu içindekilere pencere olarak vermek"
PantrixTheme.platform.typeGöster ve gizle: "Platforma göre"
PantrixTheme.platform.isTouchGöster ve gizle: "Girdiye göre"
Modifier.thenIfGöster ve gizle: "Aynı bileşen, değişen değer"
movableContentOfGöster ve gizle: "Aynı içerik, başka dizilim"

3. Örnek vaka 1: tanıtım ve giriş ekranı

Solda tanıtım, sağda giriş formu. Dar pencerede tanıtım gider, form kalır.

Dar (dikey telefon)     Geniş                             Kısa (yatay telefon)
┌──────────────┐        ┌───────────────┬──────────────┐  ┌──────────────┬─────────────┐
│  Giriş yap   │        │ Ekibinle aynı │  Giriş yap   │  │ Ekibinle     │ Giriş yap   │
│  [e-posta ]  │        │ sayfada ol    │  [e-posta ]  │  │ aynı sayfada │ [e-posta ]  │
│  [şifre   ]  │        │ ✓ stok        │  [şifre   ]  │  │ ol           │ [şifre   ]  │
│  [Giriş yap] │        │ ✓ fatura      │  [Giriş yap] │  └──────────────┴─────────────┘
└──────────────┘        └───────────────┴──────────────┘   maddeler yok, form kayar

Bu bir ekrandır: pencereye sorar.

@Composable
fun SignInScreen(modifier: Modifier = Modifier) {
    val window = PantrixTheme.window
    Row(modifier = modifier.fillMaxSize()) {
        // Gelip giden, hatırlayacak bir şeyi olmayan bir ek: düz `if` yeter.
        if (!window.isNarrow) {
            MarketingPanel(
                modifier = Modifier
                    // Sınıfa göre bir değer: Large'dan itibaren tanıtımın payı büyür.
                    .weight(window.byWidth(compact = 1f, large = 1.25f))
                    .fillMaxHeight()
            )
        }
        // Form her düzende aynı çağrıdır.
        SignInForm(modifier = Modifier.weight(1f).fillMaxHeight())
    }
}

Tanıtımın içi:

Column(
    modifier = Modifier
        .fillMaxSize()
        // Pencere elde olmadan tek bir değer.
        .padding(responsive(compact = 16.dp, medium = 24.dp, large = 40.dp))
) {
    PantrixText(
        text = "Ekibinle aynı sayfada ol",
        // Yazı stili de sınıfa göre seçilen bir değerdir.
        style = window.byWidth(compact = typography.title, large = typography.headline)
    )
    // Yatay telefon: genişlik yeter, yükseklik yetmez. Maddeler gider.
    if (!window.isShort) {
        Benefits()
    }
}

Formun satır aralığı ve ekranın yüksekliği yüksekliğe göre seçilir:

verticalArrangement = Arrangement.spacedBy(window.byHeight(compact = spacing.sm, medium = spacing.md))
KullanılanNe için
isNarrowTanıtım gösterilsin mi
byWidthTanıtımın payı, başlığın yazı stili
responsiveİç boşluklar
isShortYatay telefonda maddeleri kaldırmak
byHeightSatır aralığı, ekranın boyu

Form genişlemez: widthIn(max = 360.dp) ile en çok 360 dp'dir ve kendi yarısının ortasında durur. Her şey sınıfla çözülmez; okunacak ve doldurulacak içerik için bir üst genişlik çoğu zaman yeter.

Bu örneğin asıl dersi: form tek yerden çağrılır

Compose bir composable'ın hatırladıklarını çağrıldığı yere bağlar. Yukarıda SignInForm() her düzende aynı satırdır; tanıtım gelip giderken e-posta alanına yazılan metin yerinde kalır. Şöyle yazılsaydı kaybolurdu:

// YANLIŞ: form iki ayrı yerden çağrılıyor.
if (window.isNarrow) {
    SignInForm()
} else {
    Row {
        MarketingPanel()
        SignInForm()      // başka bir çağrı yeri: pencere genişleyince form sıfırdan başlar
    }
}

Kullanıcı telefonu çevirdiğinde ya da pencereyi büyüttüğünde yazdığı e-posta silinir. what_is_typed_stays_as_the_window_changes_its_class testi doğru yazımı tutar; formu iki yerden çağırınca kırmızıya döner.

Formun alanları rememberSaveable ile hatırlar: Android Activity'yi yeniden yarattığında da (koyu temaya geçiş, dil değişimi) yazılan, çağrı yeri aynı kaldığı için geri gelir (6. bölüm).

4. Örnek vaka 2: tablo bileşeni

Yer genişken düz tablo, dar yerde liste.

Dar yer (< 600 dp)            Geniş yer (≥ 600 dp); ≥ 840 dp'de tarih sütunu da gelir
┌────────────────────────┐    ┌────────┬──────────────┬──────────┬────────┐
│ Ayşe Yılmaz   1.240 TL │    │ Sipariş│ Müşteri      │ Durum    │  Tutar │
│ #1042 · 02.10  [Hazır] │    ├────────┼──────────────┼──────────┼────────┤
├────────────────────────┤    │ #1042  │ Ayşe Yılmaz  │ Hazır    │  1.240 │
│ Mehmet Demir    380 TL │    │ #1041  │ Mehmet Demir │ Kargoda  │    380 │
│ #1041 · 02.10 [Kargo]  │    └────────┴──────────────┴──────────┴────────┘
└────────────────────────┘

Bu bir bileşendir: pencereye değil kendi yerine bakar. Aynı tablo geniş bir pencerede dar bir panelin içine konabilir; pencereye sorsaydı orada da tablo çizer ve sığmazdı. Örnek sayfada aynı bileşen iki kez durur, bir kez sayfanın genişliğinde ve bir kez 320 dp'lik bir kutuda: geniş pencerede üstteki tablo, alttaki listedir.

@Composable
fun OrdersTable(
    orders: List<Order>,
    selectedId: String?,
    onSelect: (String) -> Unit,
    modifier: Modifier = Modifier
) {
    // Bileşenin kendi kutusu, pencere gibi: yardımcıların hepsi onun üstünde çalışır.
    BoxWithPantrixWindow(modifier = modifier) { box ->
        if (box.isNarrow) {
            OrderList(orders, selectedId, onSelect)
        } else {
            // Tarih sütunu yalnızca 840 dp'den geniş yerde.
            OrderGrid(orders, selectedId, onSelect, showDate = box.widthClass >= WindowWidthClass.Expanded)
        }
    }
}

BoxWithPantrixWindow, BoxWithConstraints'in üstüne kurulu bir kutudur: içeriğine alabileceği en fazla yeri bir PantrixWindow olarak verir.

Aynı şey elle de yazılır; kutunun yaptığı budur, üstüne sınırları pencereden alır ve içeriği gereksiz yere yeniden kurmaz:

BoxWithConstraints(modifier) {
    val box = PantrixWindow(DpSize(maxWidth, maxHeight), breakpoints = PantrixTheme.window.breakpoints)
    …
}

Seçim bileşenin dışında durur

Tablo ile liste iki ayrı görünümdür ve farklı çağrı yerleridir. Seçili satır onların içinde hatırlansaydı görünüm değişince kaybolurdu. Seçim dışarıdan gelir ve dışarıya bildirilir:

var selectedId by rememberSaveable { mutableStateOf<String?>(null) }
OrdersTable(orders = orders, selectedId = selectedId, onSelect = { selectedId = it })

Kural: iki görünüm arasında yaşaması gereken durum, iki görünümün üstünde tutulur. Örnek sayfada listedeki bir karta basınca aynı sipariş üstteki tabloda da seçili görünür.

Tablo hep pencereyi dolduruyorsa

Bileşen yalnızca pencerenin tam genişliğinde kullanılacaksa pencereye sormak da çalışır ve daha kısadır:

if (PantrixTheme.window.isNarrow) OrderList(…) else OrderGrid(…)

Bedeli, bileşenin bir panelin içine ya da bir yan menünün yanına konduğu gün yanlış görünümü çizmesidir. Yeniden kullanılacak bir bileşende kutuyu ölç.

5. Örnek vaka 3: göstermek ve gizlemek

Bir parçanın yalnızca mobilde, ya da yalnızca masaüstünde ve web'de görünmesi.

Pencereye göre

Düz if yeter:

val window = PantrixTheme.window

if (window.isNarrow) StickyAddToCartBar()                                  // yalnızca dar pencerede
if (!window.isNarrow) HeaderAction()                                       // dar olmayan her pencerede
if (window.widthClass >= WindowWidthClass.Expanded) DetailPane()           // Expanded'dan itibaren
if (window.widthClass in WindowWidthClass.Medium..WindowWidthClass.Expanded) LabelledRail()
if (window.isShort) CompactTopBar()                                        // yatay telefonda
PencereGörünenler
Dikey telefon (402 × 874)yalnızca dar
Dikey tablet (820 × 1180)dar olmayan, Medium–Expanded
Yatay telefon (874 × 402)dar olmayan, Expanded'dan itibaren, Medium–Expanded, yalnızca kısa
Masaüstü (1280 × 800)dar olmayan, Expanded'dan itibaren

Platforma göre

val platform = PantrixTheme.platform.type

if (platform == PlatformType.Web) DownloadAppBanner()
if (platform == PlatformType.Android || platform == PlatformType.Ios) RateInStoreLink()

Geniş bir Android tableti de Android'dir; daraltılmış bir tarayıcı penceresi de web'dir. "Mobilde görünsün" derken hangisini kastettiğine karar ver: çoğu zaman pencere, bazen platform.

Girdiye göre

Parmakla mı kullanılıyor, fareyle mi: bunu platformun türü söylemez. Android ve iOS hep parmakladır, masaüstü hep fareyledir; web ise ikisi de olur.

val platform = PantrixTheme.platform

if (platform.isTouch) PullToRefreshHint()          // Android, iOS, telefon ve tablet tarayıcısı
if (!platform.isTouch) KeyboardShortcutsHint()     // masaüstü, masadaki tarayıcı
Neredeplatform.typeplatform.isTouchDokunma hedefi
Android telefon ve tabletAndroidevet48 dp
iPhone ve iPadIosevet44 dp
Masaüstü uygulamasıDesktophayır32 dp
Masadaki tarayıcıWebhayır32 dp
Telefondaki tarayıcıWebevet48 dp

Dokunma hedefini bileşenler temadan kendileri alır; ekran yazarken bir şey yapmak gerekmez. Tarayıcıda cevap canlıdır: tablete bir trackpad takılınca değişir. Ayrıntısı ADAPTIVE_GUIDE.md'nin 11. bölümündedir.

Gizlemek yerine değiştirmek

Aynı bileşen iki düzende de varsa onu gizleyip başka bir yerde yeniden çağırma; ona verilen değeri değiştir:

PantrixButton(
    text = "Devam et",
    onClick = onContinue,
    // Dar pencerede satırı doldurur, genişte kendi genişliğindedir.
    modifier = Modifier.thenIf(window.isNarrow) { fillMaxWidth() }
)

Gizlenen parçanın durumu

if'in içindeki bir parça gizlenince kompozisyondan çıkar: yer tutmaz, okunmaz, hatırladıklarını da bırakır. Bir ek için (ipucu, afiş, ikinci bir düğme) bu sorun değildir. Aynı içerik için sorundur:

// YANLIŞ: iki ayrı sayaç. Sınıf değişince sayı sıfırlanır.
if (window.isNarrow) Counter(label = "dar") else Counter(label = "geniş")

// DOĞRU: tek sayaç, değişen etiket. Sayı korunur.
Counter(label = if (window.isNarrow) "dar" else "geniş")

İçeriğin yeri değişmek zorundaysa: taşı

Dar pencerede alt alta, genişte yan yana duran içerik için tek bir çağrı yeri yetmez: Column ile Row iki ayrı yerdir. movableContentOf içeriği taşır; aynı kompozisyon yeni yerine gider ve hatırladıkları yerinde kalır:

// Bir kez kurulur ve hatırlanır; çağrıldığı yer değişince içerik oraya taşınır.
val counters = remember {
    movableContentOf {
        Counter(label = "Taşınan A")
        Counter(label = "Taşınan B")
    }
}
if (window.isNarrow) {
    Column(verticalArrangement = Arrangement.spacedBy(spacing.sm)) { counters() }
} else {
    Row(horizontalArrangement = Arrangement.spacedBy(spacing.md)) { counters() }
}

movableContentOf androidx.compose.runtime paketindedir, Compose'un kendi aracıdır. Aynı satırı düz bir lambda ile yazmak sayıları sıfırlar; content_moved_from_a_column_into_a_row_keeps_its_count testi bunu tutar.

Taşınan içerik de kompozisyon yaşadıkça hatırlar. Android Activity'yi yeniden yarattığında ondan geriye yalnızca rememberSaveable ile kaydettiği kalır (6. bölüm).

Örnek kartındaki pencere seçicisi

Dört sayfada da pencereyi okuyan her örneğin kartında bir pencere seçicisi vardır (Gerçek / Dar / Orta / Kısa / Geniş) ve o örneğe kendi penceresini verir; öteki örnekler kendi pencerelerinde kalır. Pencere sınıfları › "Katlanma duruşu"nda seçici yoktur: sabit pencereler düzdür, duruş yalnızca cihazdan okunur. "Dar" 360 × 800 dp'lik bir telefon, "Orta" 768 × 1024 dp'lik dik bir tablet, "Kısa" 880 × 400 dp'lik yatay bir telefon, "Geniş" 1280 × 800 dp'lik bir masaüstü penceresidir; dördü dört ayrı genişlik sınıfına düşer (Compact, Medium, Expanded, Large). "Gerçek" uygulamanınkidir; seçicinin altındaki "Şu an" satırı örneğe verilen pencerenin sınıflarını yazar. Örnek seçilen pencerenin genişliğinde kurulur. Karta sığmayan pencere sıkıştırılmaz: küçültülerek bütünüyle çizilir ve satır ölçeği söyler; %60'tan küçük çizilmesi gerekiyorsa kendi boyutunda kalır, çerçevesi yana kayar ve altında bir kaydırma çubuğu çıkar. Telefonda geniş pencerenin, masaüstünde dar pencerenin hâli pencereye dokunmadan görülür. "Gizlenince durum ne olur" örneğinde sayaçları artırıp seçicide "Dar" ile "Geniş" arasında geçince "iki yerden" çağrılan sıfırlanır, "tek yerden" çağrılan kalır; "Aynı içerik, başka dizilim"de taşınanlar da kalır.

// Sabit bir pencere, gerçeğinin sınırlarıyla sınıflanır.
val wide = PantrixWindow(DpSize(1280.dp, 800.dp), breakpoints = PantrixTheme.window.breakpoints)
CompositionLocalProvider(LocalPantrixWindow provides wide) {
    Examples()   // içindeki her `PantrixTheme.window` ve `responsive(...)` bu pencereye sorar
}

Aynı teknik, bir önizlemeyi ya da bir testi belli bir boyutta kurmak için de kullanılır. Platform ve girdi bundan etkilenmez: "Platforma göre" ve "Girdiye göre" örneklerinin bu yüzden seçicisi yoktur.

6. Durumu korumak

Pencere sınıf değiştirdiğinde düzen değişir. İçeriğin hatırladıkları (yazılan metin, seçim, sayaç) şu üç yoldan biriyle korunur:

YolNe zamanÖrnek
Tek çağrı yeri: içerik hep aynı satırdan çağrılır, değişen ona verilen değerdirYapı aynı kalıyorsa: yanına bir şey gelip gidiyor, boyutu ya da etiketi değişiyorGiriş formu, "tek yerden" sayaç
Durumu yukarıda tutmak: durum iki görünümün üstünde durur, onlara parametreyle inerİki ayrı görünüm aynı veriyi gösteriyorsaTablonun ve listenin seçili siparişi
Taşımak: movableContentOfAynı içerik başka bir ebeveynin içine geçmek zorundaysaAlt alta ve yan yana duran sayaçlar

Önce ilkini dene; en ucuzu odur ve hiçbir şey taşınmaz. İkincisinde görünümlerin kendi içinde hatırladıkları (kaydırma konumu gibi) yine sıfırlanır: yalnızca yukarıda tutulan durum korunur.

Android: activity yeniden yaratılınca ne kalır

Masaüstünde, web'de ve iOS'ta pencerenin boyutu değişince kompozisyon yaşamaya devam eder; yukarıdaki üç yol yeter. Android'de döndürme, katlama ve çoklu pencerede boyut değişimi birer yapılandırma değişimidir: uygulama bunları üstlenmediyse Activity, kompozisyonla birlikte yeniden yaratılır ve geriye yalnızca kaydedilen durum kalır.

Örnek uygulama pencere değişimlerini üstlenir. Manifestindeki satır şudur:

android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|density"

Bu satırla Android de öteki platformlar gibi davranır: cihaz katlanınca ya da dönünce Activity yaşar, PantrixTheme.window yeni sınıfı söyler ve içerik yalnızca yeniden kurulur. Listede olmayan değişimlerde (koyu tema, dil, yazı boyutu, klavye) ve sistem süreci öldürüp geri getirdiğinde Activity yine yeniden yaratılır; koyu tema bilerek dışarıda bırakıldı, çünkü enableEdgeToEdge sistem çubuklarının ikon rengini onCreate'te seçer.

Durum neredeKompozisyon yaşarken sınıf değişirActivity yeniden yaratılır
remember, tek çağrı yeriKorunurGider
rememberSaveable, tek çağrı yeriKorunurKorunur
rememberSaveable, iki çağrı yeriSıfırlanırSıfırlanır
movableContentOf içinde rememberKorunurGider
movableContentOf içinde rememberSaveableKorunurKorunur

7. Tuzaklar

TuzakNe olurDoğrusu
Aynı içeriği sınıfa göre iki yerden çağırmakSınıf değişince durum (yazılan metin, seçim, kaydırma) sıfırlanırTek yerden çağır, değişeni parametreyle ver; yeri değişecekse taşı
İki görünümün (tablo / liste) ortak durumunu içlerinde tutmakGörünüm değişince kaybolurDurumu ikisinin üstünde tut
Kalması gereken durumu remember ile tutmakAndroid Activity'yi yeniden yaratınca gider: tema ve dil değişimi, süreç ölümü; pencere değişimlerini üstlenmeyen uygulamada döndürme ve katlama darememberSaveable ya da bir ViewModel
Bileşende pencereye sormakDar bir panelde geniş düzeni çizerBoxWithPantrixWindow ile kendi kutusuna sor
Kutuyu elle kurarken sınırları unutmak: PantrixWindow(DpSize(maxWidth, maxHeight))Ürünün kendi sınırları bileşene ulaşmazBoxWithPantrixWindow, ya da breakpoints = window.breakpoints
Sayfa kenar boşluğunu elle seçmek: responsive(16.dp, 24.dp, 32.dp)Her ekran kendi değerini seçer, ürünün ölçüsü tek yerden değişmezPantrixTheme.layout.margin
window.size'ı düzen kararı için okumakHer pikselde yeniden kurulurSınıfı oku; özel eşik için rememberWindowIsAtLeast
Sınıfı remember ile saklamakMasaüstünde ve web'de ilk kompozisyonun Compact'ı kalırHer seferinde pencereden oku; saklanacaksa isSizeKnown'ı bekle
"Mobil" için platforma bakmakDaraltılmış masaüstü penceresi masaüstü düzeninde kalırYer için pencereye, platforma özgü şey için platforma bak
"Dokunmatik" için platformun türüne bakmakTelefonun tarayıcısı web'dir: parmağa göre olan şey orada görünmezPantrixTheme.platform.isTouch
isNarrow'u "telefon" sanmakYatay telefon isNarrow değildir (874 dp, Expanded)Yatay telefon için isShort
Kaydırılan sayfadaki kutuya isShort sormakHep false: kutunun yüksekliği sınırsızdırYükseklik için pencereye sor

8. Test etmek

Testin kendi penceresi hep aynı boyuttadır (Skiko'da 1024 × 768, yani Expanded). Bir düzeni belli bir pencerede sınamak için pencereyi temaya ver; pencere bir durumsa test içinde değiştirilebilir:

var window by mutableStateOf(PantrixWindow(DpSize(402.dp, 874.dp)))
setContent {
    PantrixTheme(darkTheme = false, window = window) { SignInScreen(Modifier.fillMaxSize()) }
}
onNodeWithTag("TextField_sign_in_email").performTextInput("ada@pantrix.io")

window = PantrixWindow(DpSize(1280.dp, 800.dp))        // pencere genişledi
waitForIdle()
onNodeWithTag("TextField_sign_in_email").assertTextContains("ada@pantrix.io")

Kendi yerine bakan bir bileşene pencere değil kutu verilir:

var width by mutableStateOf(320.dp)
setContent {
    PantrixTheme(darkTheme = false) {
        Box(Modifier.width(width)) { OrdersTable(ORDERS, selectedId = null, onSelect = {}) }
    }
}
// 320 dp: liste. Sonra `width = 700.dp`: tablo, tarih sütunu yok. `width = 900.dp`: tarih sütunu da var.

Ürünün kendi sınırlarının bileşene ulaştığı da böyle sınanır: pencere o sınırlarla verilir, kutu aynı kalır.

// Medium 300 dp'de başlıyorsa 320 dp'lik kutu liste değil tablodur.
val window = PantrixWindow(DpSize(1280.dp, 800.dp), breakpoints = PantrixBreakpoints(medium = 300.dp))
PantrixTheme(darkTheme = false, window = window) {
    Box(Modifier.width(320.dp)) { OrdersTable(ORDERS, selectedId = null, onSelect = {}) }
}

Platforma ve girdiye göre gösterilenler için platform da temaya verilir:

PantrixTheme(darkTheme = false, platform = PantrixPlatform.forType(PlatformType.Android)) { … }

// Telefondaki tarayıcı: platform web, kullanım parmakla
PantrixTheme(darkTheme = false, platform = PantrixPlatform.forType(PlatformType.Web, isTouch = true)) { … }

Android'in Activity'yi yeniden yaratması JVM testinde de görülür: içerik, testin kendi SaveableStateRegistry'siyle kaydedilir, indirilir ve kaydedilenden yeniden kurulur. AdaptiveSamplesTest'teki SavedScreen bunu yapar (Compose'un StateRestorationTester'ı Android dışında çalışmaz):

val screen = SavedScreen()
var window by mutableStateOf(DESKTOP)
setContent {
    screen.Content {
        PantrixTheme(darkTheme = false, window = window) { ComponentExamples(DemoComponent.AdaptiveVisibility) }
    }
}
// … sayaçlar artırılır …
screen.buildAgain(this) { window = PHONE }             // yeniden yaratılan Activity, bu kez dar pencerede
onNodeWithText("Tek yerden (dar): 2").assertExists()   // kaydedilen, çağrı yeri aynı: geri geldi
onNodeWithText("İki yerden (dar): 0").assertExists()   // başka bir çağrı yeri: sıfırdan

Örneklerin testleri app/shared/src/jvmTest/kotlin/com/pantrixui/cmp/demo/AdaptiveSamplesTest.kt dosyasındadır:

./gradlew :app:shared:jvmTest --tests 'com.pantrixui.cmp.demo.AdaptiveSamplesTest'