Android ekosistemi çok geniştir. Kullanıcılar telefonlar, tabletler, katlanabilir cihazlar ve masaüstü ortamları arasında sorunsuz bir şekilde geçiş yapabilir. Artık Compose First yaklaşımını benimsediğimiz için bu oturumda, uygulamanızı yeniden boyutlandırılabilir pencereler ve farklı en-boy oranları için nasıl tasarlayacağınızı inceleyeceğiz. Bu sayede, geleneksel düzen varsayımlarının geçerliliğini yitirmesiyle ortaya çıkan, kullanıcı arayüzlerinin uzaması ve durumun kaybolması gibi sorunları önleyebilirsiniz.
Jetpack Compose, Navigation 3 ve WindowManager'daki en yeni özelliklerin, kompakt ekranlardan bölünmüş ekranlara doğal olarak ölçeklenen dinamik düzenler oluşturmanıza nasıl yardımcı olabileceğini keşfedin. Gemini'ı ve uyarlanabilir beceri gibi becerilerimizi kullanarak geliştirme iş akışınızı hızlandırma stratejilerini öğrenin. Bu sayede, eski kod tabanlarını daha büyük ekranlar için otomatik olarak yeniden düzenleyebilirsiniz.
MediaQuery API, pencere boyutu ve katlama duruşları gibi etkin kullanıcı arayüzü özelliklerini bildirimli olarak izler. Birden fazla eski platform yöneticisini birleştirerek oluşturma durumuna getirir.Grid ve FlexBox temel öğeleri, uyarlanabilir düzen yapı taşları sağlar. Grid, tek bir composable içinde parametreleştirilmiş çok boyutlu düzenlemelere olanak tanırken FlexBox, tek boyutlu alan dağıtımı ve sarmalama sağlar.Transkript
Francesco Romano
Herkese merhaba.
Bugün, Jetpack Compose ile uygulamanızı her ekrana nasıl ölçeklendireceğiniz konusunda ayrıntılı bir teknik inceleme yapacağız.
Neden her ekrana ölçeklendirme?
Çünkü Android ekosistemi çok farklı form faktörlerini kapsayacak şekilde genişledi.
APK'nız telefonlar, katlanabilir cihazlar, tabletler, Chromebook'lar ve Googlebook'lar, TV'ler, otomotiv bilgi-eğlence sistemleri, XR cihazları vb. üzerinde çalışabilir.
İlk olarak, kullanıcılarınıza bulundukları yerde ulaşın.
İkinci olarak, Android platformunun uyarlanabilir uygulamalara geçişi geçici bir durum değildir.
Sadece telefonların kullanıldığı bir dünyaya geri dönmeyeceğiz. Bu teknoloji kalıcı.
Burada kısa bir zaman çizelgem var.
2022'de bu temeli atmaya başladık.
Jetpack WindowManager'ın ilk sürümü olan Android 12L'yi kullanıma sunduk, pencere boyutu sınıfları gibi API'ler sağladık ve uyarlanabilir uygulama kalitesi yönergelerinin ilk sürümünü yayınladık.
Ardından, Google Play'i de güncelleyerek puanların cihaz türüne göre ayrılmasını sağladık ve uyarlanabilir uygulama kalitesi yönergelerini sıralama kararlarına dahil etmeye başladık.
Android 15'te, masaüstü pencereli görüntüleme özelliğini AOSP'ye ekledik ve Compose Material 3 Adaptive'in kararlı sürümünü yayınladık.
Android 16, SDK 36'dan itibaren sistem, büyük ekranlarda yön kilitlerini, en boy oranı kilitlerini ve yeniden boyutlandırılabilirlik işaretlerini yoksayacak.
Bağlı ekranların kullanıma sunulmasıyla birlikte APK'nız artık neredeyse her pencere boyutunda çalışabilir.
SDK 36'da ise yön ve en-boy oranı geçersiz kılma özelliğini devre dışı bırakmak için geçici bir manifest işaretçisi sunulmuştu.
Android 17'de bu kapsamı dışında kalmayı seçme seçeneği kaldırıldı.
Bu nedenle, uyarlanabilir tasarım oluşturmak platformun temel şartlarından biridir.
Son on yılda ekosistemdeki ekiplerle yakın bir şekilde çalıştık ve uyarlanabilir uygulama geliştirmenin neden zor olabileceğini ilk elden öğrendik.
Durum kaybı, parçalı pencere API'leri, düzenin ne zaman değiştirileceğine dair belirsiz sinyaller ve kod tabanının derinliklerine gömülü bazı eski varsayımlar var.
Bu on yıllık süreçte geliştiricilerin karşılaştığı zorluklardan öğrendiğimiz her şeyi doğrudan iki yere entegre ettik: İlk olarak, kullanıcı arayüzü araç setinin kendisine.
Bugün Jetpack Compose'daki bazı önemli yeni özellikleri inceleyeceğiz: medya sorgusu, tablo, FlexBox ve Navigation 3.
Ayrıca Android Studio'da Gemini için resmi Android becerileri ve Android CLI aracılığıyla sizinle birlikte kod yazan temsilci araçlarında da kullanılabilir.
Örneğin, bu yılın başlarında Google I/O'da kullanıma sunulan Compose uyarlanabilir becerilerini inceleyeceğiz.
Telefon düzenlerini daha büyük pencere boyutlarına taşıyabiliyordu.
Artık durum kaybınızı da düzeltebilir, masaüstü gibi katlanabilir cihazlara özgü deneyimler uygulayabilir ve klavye kısayolları, özel imleçler, ipuçları vb. keşfedip uygulayarak giriş desteğini iyileştirebilir.
Tüm bunların temelini oluşturan Jetpack Compose ile başlayalım.
Jetpack Compose 2021'de kararlı sürüme geçti. Bu yıl beşinci yaşını kutluyor.
Bu beş yıl boyunca, binlerce üretim uygulamasında Compose ile uyarlanabilir uygulamalar geliştirmenin ne kadar kolay ve güvenilir olduğunu gördük.
Android geliştirme artık resmi olarak Compose öncelikli.
Yani tüm yeni modern uyarlanabilir düzenler, stiller ve gezinme bileşenleri Compose için oluşturulmuştur.
Sistem mimarisi açısından, uyarlanabilir uygulamalar için Compose'un eski görünüm sistemine kıyasla en büyük avantajı, durum ve kullanıcı arayüzünün etkileşim şeklidir.
Bunu açıklamak için görünüm sistemiyle hızlı bir karşılaştırma yapmak istiyorum.
Görünüm sisteminin nasıl çalıştığını düşünün.
Kullanıcı arayüzünüz, statik XML dosyalarından bir kez şişirdiğiniz değişken nesnelerden oluşan bir ağaçtı.
Farklı kaynak niteleyicileriniz vardı. Buradaki asıl sorunla başlayalım: Farklı kaynak niteleyicilerde tekrarlama.
XML'de uyarlanabilir bir kullanıcı arayüzü oluşturduğunuzda düzen dosyasında koşullu mantık olmaz.
Bunun yerine, hiyerarşinin tamamını kaynak klasörleri (ör. layout, layout smallest window 600 dp, layout land vb.) arasında kopyalamanız yeterlidir.
Şimdi sağdaki EditText örneğine bakalım.
<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>Bu, metin girişini işleyen mantıksal bileşenin aynısıdır ve farklı XML dosyalarına dağılmıştır.
Telefon düzeninde inputType veya maxLength gibi bir özelliği güncellerseniz derleyicideki hiçbir şey sizi bu özelliği tablet düzeninde de güncellemeye zorlamaz.
Bileşenler farklı dosyalara ayrıldığı için zaman içinde yapısal sapma neredeyse kaçınılmazdır.
Bu kopyalama, yapılandırma değişiklikleri sırasında durum kaybı sorunuyla da bağlantılıdır.
Görünüm sisteminde kullanıcı arayüzü bileşenlerinin durum bilgisi içerdiğini unutmayın.
EditText, kendi dahili düzenlenebilir arabelleğine, özel imleç konumuna, odak durumuna ve bazı durumlara sahiptir.
Örneğin, kullanıcı cihazı açtığında veya pencereyi belirli bir eşiğin üzerinde yeniden boyutlandırdığında Android, tüm görünüm hiyerarşisini yok eder ve yeni XML dosyalarını sıfırdan şişirir.
Çerçeve, kullanıcı girişini kaydetmek için onSaveInstanceState'i kullanır. Bu yöntem, söz konusu görünümün kimliğine bağlı durumu paketler.
Ancak bu durumda üç olası hata modu ortaya çıkar.
İlk olarak, başka bir XML'de kimlik yanlışlıkla text_msg yerine text_input olarak adlandırılırsa durum geri yükleme, kullanıcının giriş metnini sıfır uyarıyla sessizce siler.
İkinci olarak, onRestoreInstanceState, onViewCreated'dan sonra çalışır. Bu durum, ViewModel gözlemcileriyle sık sık çakışabilir ve geri çağırma döngülerine neden olabilir.
Üçüncü olarak, widget türünü niteleyiciler arasında değiştirirseniz (örneğin, kompakt bir ekranda bir spinner, daha geniş bir ekranda ise RadioGroup kullanmak isterseniz) ve bunlar aynı kimliği paylaşıyorsa çalışma zamanında ClassCastException ile kilitlenir.
Jetpack Compose'da ise kullanıcı arayüzünüz bir kez oluşturduğunuz bir nesne değildir.
Kullanıcı arayüzünüz, durumun saf bir işlevidir.
Bu da yapılandırma değişikliğinin ne olduğunu tamamen değiştirir.
Compose'da yapılandırma değişikliği nedir?
Bir ipucum var.
Francesco Romano
Evet.
Compose'da yapılandırma değişikliği, başka bir yeniden oluşturma işlemidir ve yeniden oluşturma işlemleri her zaman gerçekleşir.
Kullanıcı cihazı açtığı için pencere genişlediğinde, composable'larınız yeni boyutu okur ve çıkışı buna bağlı olanlar yeniden çalıştırılır.
Etkinlik bu kadar.
Şimdi, en yeni Compose araçlarını ve bileşenlerini kullanarak modern uyarlanabilir uygulamalar yazmanın ne anlama geldiğini görelim.
XML dünyasında "Uygulamam telefonda mı, tablette mi yoksa masaüstünde mi çalışıyor?" gibi sorular sormanız ve kullanıcı arayüzünü cihaz kategorisine göre dallandırmanız gerekiyordu.
Ancak bunu yapmaktan kaçınmak için temel bir neden var.
Tamamen aynı fiziksel cihazda çalıştırılsa bile uygulama bağlamı ve kullanıcı arayüzü özellikleri, çalışma zamanında dinamik olarak değişebilir.
Dolayısıyla, vermek istediğim mesaj, kullanıcı arayüzü kararlarının mevcut etkin özelliklere göre verilmesi gerektiğidir: mevcut pencere boyutu, duruş, bazı donanım özellikleri.
Şimdi klasik bir örneğe bakalım: Harici monitörde pencereli görüntüleme.
Pixel telefonunuzu masanızdaki harici bir monitöre bağlarsanız 6 inçlik dikey ekranda çalışan uygulama süreci, fiziksel klavye ve fare bağlı 32 inçlik geniş bir monitörde çalışmaya başlar.
Bu nedenle, uygulamanın çalıştığı cihaz türünü kontrol etmek, uygulamanın gerçekte görüntülendiği ekran veya giriş yöntemleri hakkında hiçbir şeyi garanti etmez.
Ayrıca Android, çoklu pencereyi desteklediğinden pencereler yeniden boyutlandırılabilir.
Googlebook'un ekranı telefondan daha büyük olsa bile uygulamanız diğer uygulamalarla birlikte 400 dp genişliğinde bir pencerede çalışıyor olabilir.
Bu nedenle, statik fiziksel ekran metriklerini sorgulamak da anlamsız olabilir.
Bu nedenle, uygulamaları Compose için tasarlamak istiyorsak öncelikle ortamı nasıl sorgulayacağımızı anlamamız gerekir.
Öncelikle kullanıcı arayüzü özelliklerini tanımlayalım.
Kullanıcı arayüzü özellikleri yalnızca pencere genişliği ve yüksekliği ya da katlanabilir duruşu değil, aynı zamanda şu anda etkin olan giriş ve çıkış yöntemlerini de içerir.
Masaüstü ortamında kullanıcıların her zaman dokunmatik ekran kullanmadığını gördük.
Klavye ve fare kullanabilirler.
Otomotiv sisteminde, bazı araçlarda döner düğme veya farklı giriş, farklı sinyal giriş yöntemi vardır.
Ayrıca, ekran boyutu ve girişin yanı sıra kullanıcının ekrana ne kadar uzak olduğu (ör. izleme mesafesi) veya şu anda bir donanım kamerası ya da mikrofonun bağlı olup olmadığı da önemlidir.
Kullanıcı arayüzü özellikleri bunlardan ibaret.
Francesco Romano
Tüm sinyalleri almak istiyorsanız farklı kaynaklardan 5-6 farklı API'yi birleştirmeniz gerekiyordu.
Yapılandırma nesnesi, WindowMetricsCalculator, WindowInfoTracker, InputManager, PackageManager ve ardından her birini manuel olarak bir duruma, bir Compose durumuna dönüştürmeniz gerekiyordu.
Bunu yapmaktan hoşlandınız mı?
Hayır, yapmadım.
Bu sorunu çözmek için androidx.compose.ui sürümünde MediaQuery API'yi kullanıma sunduk.
mediaQuery { ... }
derivedMediaQuery { ... }CSS medya sorgularından esinlenilmiştir.
"Medya" kelimesi, ses veya video oynatma anlamına gelmez.
Kullanıcı arayüzünüzün oluşturulduğu ortamı ifade eder.
MediaQuery ise kullanıcı arayüzü özelliklerini sorgulamak için tek bir açıklayıcı Compose yerel yöntemi sunar.
UiMediaScope içinde gösterilen özelliklere bakalım.
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 ve windowHeight (dp cinsinden).
windowPosture: Şu anda katlanabilir cihazlar için tasarlanmıştır ve Düz, Masaüstü veya Kitap olabilir.
pointerPrecision, birincil işaretleme cihazının hassasiyetini gösterir: Fare, ekran kalemi veya dokunmatik yüzey için hassas; dokunma için kaba; joystick, kumandalar veya hiçbiri için belirsiz.
keyboardKind fiziksel, sanal veya hiçbiri olabilir.
hasCamera ve hasMicrophone, bu donanımın kullanılabilir olup olmadığını dinamik olarak izler.
Takılabilen harici bir kamera kullanmayı düşünün.
viewingDistance ise Yakın, Orta veya Uzak değerini bildirir.
Ancak cihazın masaüstü modunda olup olmadığını algılayan gerçek bir örneğe bakarsak daha kolay anlaşılacağını düşünüyorum.
Masaüstü modu, kullanıcıların cihazı yarıya kadar katlayıp genellikle bir yüzeye yerleştirmesine olanak tanıyan bir duruştur.
Ayrıca uzun süreli video oynatma, görüntülü görüşme ve zaman atlamalı fotoğrafçılık için de idealdir.
Ayrıca yemek tarifi takip etmek veya slayt gösterisi izlemek için de çok kullanışlı.
Şimdi, MediaQuery'den önce masaüstü duruşunu algılamak istiyorsanız tüm bu kodu yazmanız gerekiyordu.
@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
}
}Bağlamı almanız, WindowInfoTracker örneği oluşturmanız, bir akış ve yaşam döngüsüne duyarlı Compose durumu olan windowLayoutInfo öğesini toplamanız ve ardından displayFeatures öğelerini iterasyona tabi tutarak FoldingFeature, yarı açık ve yatay olan bir öğe olup olmadığını kontrol etmeniz gerekiyordu.
Bu sadece duruşu algılamak için.
Bu çok fazla standart kod demek.
Yeni MediaQuery API ile tüm bunlar yalnızca birkaç satır koda indirgenir.
@Composable
fun currentVideoPlayerLayout(): VideoPlayerLayout {
return mediaQuery {
when(windowPosture) {
UiMediaScope.Posture.Tabletop -> VideoPlayerLayout.Tabletop
else -> VideoPlayerLayout.Default
}
}
}mediaQuery işlevi tek bir lambda parametresi alır ve bu lambda'nın alıcı kapsamı içinde, az önce gördüğümüz özellikler kümesini gösterir.
Bu örnekte, bir when ifadesinde windowPosture değerini kontrol ederek doğru composable'ı hemen döndürebiliriz.
API'nin nasıl kullanılacağına dair iki örnek daha verelim.
@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")
}
}
}İlk olarak bilgi yoğunluğundan bahsedelim.
Örneğin, kullanıcı fareyle dizüstü bilgisayar veya masaüstü monitör kullanıyorsa ve işaretçi hassasiyeti "İnce" ise kullanıcı arayüzü bileşenlerimizin dolgusunu 8 dp'den 2 dp'ye düşürerek daha fazla içeriği rahatça gösterebiliriz. Kullanıcılar parmaklarını kullanmadığı için dokunma hedefleri daha küçük olabilir.
Ayrıca, yetenek odaklı işlemler de vardır.
Örneğin, bir sohbet giriş çubuğumuz varsa görüntülü görüşme işlem düğmesini dinamik olarak göstermek veya gizlemek için hasCamera ve hasMicrophone değerlerini kontrol edebiliriz. Alternatif olarak, "Gönder" için Ctrl+Enter tuşlarına basmak gibi bazı klavye kısayolu ipuçlarını göstermek veya gizlemek için klavye türünü fiziksel olarak birleştirebiliriz.
Bunların tümü çalışma zamanında alınır.
MediaQuery, test ve önizleme oluşturma için de idealdir.
@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()
}
}Yani MediaQuery, alıcısını LocalUiMediaScope adlı bir CompositionLocal aracılığıyla çözümler.
Yani önizlemenizde CompositionLocalProvider aracılığıyla sahte bir UiMediaScope sağlayabilirsiniz. Sırf Compose kullanıcı arayüzü testi çalıştırmak için katlanabilir cihazları açıp 90°'ye getirmeniz veya fare bağlamanız gerekmez.
Francesco Romano
Artık kullanıcı arayüzü özelliklerini nasıl sorgulayacağımızı biliyoruz.
Şimdi, asıl düzeni nasıl oluşturacağımıza bakalım.
Bunun için, androidx.compose.foundation.layout sürümünde yeni bir düzen ilkel öğesi olan ve artık kararlı sürümde bulunan Grid'i tanıtmak istiyorum.
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...
}Izgara, öğeleri mevcut kapsayıcı boyutuna uyum sağlayan sütun ve satırlarda göstererek iki boyutlu bir düzen uygulamanıza olanak tanır.
Yapılandırma bloğunda neler yapabileceğinize bakalım.
Sütun veya satır çağırarak yeni öğeler ya da parçalar ekleyebilir ve repeat gibi standart Kotlin döngülerini kullanabilirsiniz.
Parçalar arasında boşluklar ayarlayabilirsiniz.
Otomatik yerleştirme yönünü flow-- GridFlow.Row veya GridFlow.Column ile kontrol edebilirsiniz.
Mimariyle ilgili önemli bir ayrıntıdan bahsedelim.
Yapılandırma bloğu, ölçüm aşamasında çalışır. Izgara yapılandırma kapsamı, yoğunluğu uygular ve kısıtlamaları gösterir.
Bu sayede, parçaları sıfır ek yeniden oluşturma geçişiyle hesaplamak için yapılandırmanın içindeki constraints.maxWidth.toDp() öğesini inceleyebilirsiniz.
Grid'de, değiştirici olmadan bir alt öğe eklediğinizde bu öğe otomatik olarak bir sonraki hücreye geçer.
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))
}Açık kontrol istediğinizde Modifier.gridItem kullanabilirsiniz. Bu durumda, bir tabanlı koordinatlarınız olur.
Yani 1. satır, 1. sütun sol üstteki hücredir.
Negatif dizin oluşturmayı destekler. Bu nedenle, -1. satır son satırdır ve bu şekilde devam eder.
Bir öğeyi 2D alana yaymak için rowSpan'i kullanabilirsiniz. Ayrıca çakışma da vardır.
Dolayısıyla, iki öğe çakışan hücrelere yerleştirilirse Grid, bu öğeleri bu sınırlar içinde üst üste yığar ve Z sıralaması, oluşturma sırasına göre belirlenir.
Bu nedenle, sonraki öğeler Box gibi diğerlerinin üzerine çizilir.
Artık Grid'in temel özelliklerini biliyoruz.
Video oynatıcı örneğine bakalım.
Cihazın düz veya yarı açık olmasına bağlı olarak varsayılan düzen ile masaüstü düzeni arasında geçiş yapmamız gerekir.
Grid'den önce DefaultLayout ve TabletopLayout olmak üzere iki farklı composable oluşturmamız ve ardından kodu bir when ifadesiyle dallandırmamız gerekiyordu.
@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)
}
}"Bunda ne sorun var?" diye düşünebilirsiniz.
Bu düzenlere bakarsanız içeriklerinin aynı olduğunu görürsünüz.
Her ikisinde de aynı oynatıcı ve aynı kumanda gösterilir.
Bu iki düzen arasındaki tek fark, iki öğenin ekranda nasıl düzenlendiğidir.
Şimdiye kadar, düzen düzenlemesini parametre olarak iletmenin bir yolu olmadığı için iki ayrı composable tanımlamak zorunda kalıyorduk.
Artık düzen yapılandırmasını parametreleştirebilir ve her şeyi tek bir composable işlev içinde işleyebiliriz.
Bunu nasıl yapacağınızı göreceğiz.
Grid API ile adlandırılmış alanları enum olarak tanımlayabiliriz.
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()
}
}Ardından, ızgaraya yerleştirilecek alt composable'ları (oynatıcı ve kontrolör) belirtebilir ve her alt öğeyi Modifier.gridItem çağrısı yapan adlandırılmış bir alanla ilişkilendirebiliriz. areaId parametresini de iletiriz.
Burada da Grid'e, oynatıcıyı VideoPlayerArea.Player adlı alana, kumandayı ise VideoPlayerArea.Controller adlı alana yerleştirmesini söylüyoruz.
Burada, iki yapılandırma lambda'sını bir VideoPlayerGridConfiguration içinde tanımlıyoruz.
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)
}
}Varsayılan video oynatıcı düzeni için bir satır ve bir sütundan oluşan bir tablo yapılandırıyoruz. Ancak tek bir hücre olsa bile aynı hücreye iki alan adı atayabileceğimizi unutmayın.
Bu örnekte, daha önce gördüğümüz gibi, denetleyici oynatıcının üzerine yerleştirilmiştir.
Masaüstü için iki satır ve bir sütun tanımlayıp oynatıcıyı üst yarıya, kumandayı ise alt yarıya yerleştiriyoruz. Temel olarak bu şekilde.
Bu yapılandırma lambda'larını tanımladıktan sonra MediaQuery'yi doğrudan ızgaranın yapılandırmasında kullanabiliriz.
@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()
}
}
}Böylece, iki yapılandırma arasında dinamik olarak geçiş yapan kullanıcı arayüzüyle birleştirilebilen tek bir video oynatıcı düzenine sahip olabiliriz.
Bahsetmek istediğim bir sonraki temel öğe 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) }
)
}
}
}Grid, 2D satır ve sütun düzenlerini işlerken artık kararlı olan FlexBox, akıllı alan dağıtımı, küçültme ve çok satırlı sarmalama gerektiren tek boyutlu düzenleri işler.
FlowRow ve FlowColumn gibi iki ayrı composable yerine kullanabileceğiniz kullanışlı ve duyarlı bir düzendir. Alt composable'ları yeniden oluşturmadan yapılandırma bloğunda yönü dinamik olarak değişebilen tek bir kapsayıcıdır.
Ayrıca tam CSS FlexBox algoritmasını uyguladığı için wrap, justify-content, align-items ve gap özelliklerini kullanabilirsiniz.
Her alt öğede ise size temel, göreceli büyüme, küçülme faktörleri, alignSelf vb. bilgileri veren Modifier.flex bulunur.
Francesco Romano
Şimdi ekran düzeyindeki tanımdan uygulama düzeyindeki düzenlere geçelim. Üst düzey gezinme ve çok bölmeli ekran düzenlemelerimizi yalnızca tek bir ekranda değil, tüm uygulamada nasıl yapılandırdığımıza bakalım.
MediaQuery, pencerenin tam genişliğini ve yüksekliğini dp cinsinden verir. Ancak uygulama düzeyindeki mimari için standartlaştırılmış ve belirli bir amaca yönelik görüntü alanı kesme noktaları (pencere boyutu sınıfları) gerekir.
Temel olarak, her ekranın bu grupların her birinde duyarlı olmasını istersiniz. Farklı gruplar arasında daha fazla yapısal değişiklik, güncelleme ve farklılık olabilir.
Örneğin, küçük bir ekranda yalnızca bir liste göstererek veya genişletilmiş bir pencerede hem listeyi hem de ayrıntıları göstererek.
Pencere boyutu sınıflarını doğrudan UiMediaScope üzerinde çok temiz bir şekilde oluşturabilirsiniz. Bu, UiMediaScope üzerinde bir uzantı özelliği tanımlar.
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...
}Burada dikkat edilmesi gereken bir nokta, standart MediaQuery yerine derivedMediaQuery kullanılmasıdır.
Neden?
Pencere genişliği ve yüksekliği sık sık değişen değerler olduğundan
Kullanıcılar uygulamanızı yeniden boyutlandırırken (ör. masaüstünde) bu değerler her karede değişebilir.
Uygulama düzeyindeki iskelenizi her karede değil, yalnızca pencere bir kesme noktasını geçtiğinde yeniden oluşturmak istersiniz.
Pencere boyutu sınıflarını belirledikten sonra, daha fazla alanımız olduğunda uygulamamızı tek bölmeli akıştan çok bölmeli düzenlere nasıl geçirebiliriz?
Jetpack Navigation 3, Compose durumu ve sahneleri temel alınarak sıfırdan tasarlandığı için bu konuda öne çıkıyor.
İlk olarak, eski yığın size aittir. Bu, NavKey'lerin bir listesidir. Bu nedenle çok esnektir ve durum olarak kaydedilmesi kolaydır.
Yığındaki her anahtar, composable'ını ve meta verilerini içeren bir NavEntry'ye çözümlenir.
NavDisplay, girişlerin nasıl çizileceğini sabit kodlamaz. Bunun yerine, bir sahne stratejileri listesini değerlendirir.
Strateji kapsamında, kullanıcı arayüzü özelliklerini (ör. pencere boyutu sınıfları) inceleyebilirsiniz.
if (isExpandedWidth) {
/* return a multi-pane scene */
}
else {
/* return a single pane scene */
}Eski yığını inceleyebilir ve ardından tek bir NavEntry'yi tek bir bölmede mi, birden fazla öğeyi aynı anda mı yoksa üst üste mi oluşturacağınıza karar verebilirsiniz.
Çok esnek bir ajan.
Çok bölmeli stratejinin yanı sıra pencere boyutu sınıfları, ergonomiyi ve ekran alanı kullanımını en üst düzeye çıkarmak için hangi gezinme bileşenlerini kullanmak istediğinizi de belirler.
Örneğin, dar genişliklerde alt gezinme çubuğu, orta genişliklerde gezinme çubuğu, geniş ekranlarda ise tam gezinme çekmecesi kullanmak isteyebilirsiniz.
Uygulama düzeyinde düzen kararları için Nav3'ü ve ekran düzeyinde tanımlama için duyarlı bileşenleri birleştirdiğinizde, bu şekilde görünen ve neredeyse her pencere boyutunda ve ortamda parlayan uygulamalar elde edersiniz.
Özellikle Nav 3 için, derin bağlantı desteği de dahil olmak üzere en son güncellemeler hakkında daha fazla bilgi edinmek istiyorsanız Jetpack Navigation 3 ile Tür Güvenli Derin Bağlantılar Oluşturma başlıklı konuşmaya göz atın.
Francesco Romano
Bugün MediaQuery, Grid, FlexBox ve Nav3 gibi birçok yeni API'yi ele aldık.
Jetpack Compose'un uyarlanabilir uygulamalar oluşturmanıza nasıl yardımcı olduğunu ele aldık.
Ancak temsilci tabanlı geliştirme çağında Jetpack Compose'u kullanmanın son derece önemli bir avantajı daha var: Çok daha etkili.
Açılış konuşmasında da duymuş olabileceğiniz gibi, Compose'a geçiş yapan uygulamaların %30'dan daha az jeton kullandığını gördük. Temsilci tabanlı iş akışınızı Compose uygulamaları için daha da optimize etmek üzere çalışıyoruz.
Bugün, Compose Adaptive becerisindeki önemli güncellemeleri vurgulamak istiyorum.
android skills list
android skills add adaptiveDaha önce de belirttiğim gibi, düzenleri Grid, Flexbox ve MediaQuery'ye taşımanın yanı sıra artık uygulama genelinde uçtan uca durum yönetimini açıkça ele alıyor. Ayrıca, beceriyi gerçek dünyadaki durum kaybı hatalarına karşı test ediyoruz: gezinme, kaydırma konumu, etkin medya oynatma, yarı yazılmış metin girişleri vb. korunuyor.
Bu beceri artık katlanabilir duruş optimizasyonunu da içeriyor. Örneğin, medya ve kamera ekranlarını otomatik olarak masaüstü moduna yeniden düzenliyor. Ancak uygulamanız bir Googlebook'ta veya fare ve klavye ile harici bir monitörde çalışırken düzen, hikayenin yalnızca yarısıdır.
Uyarlanabilir beceri artık kullanıcı arayüzünüzü denetleyerek klavye kısayollarını, özel işaretçi imleçlerini, fareyle üzerine gelme durumlarını, ipuçlarını vb. bulup uygulayabilir.
Şimdi bunu uygulamalı olarak görelim.
Bunun demosunu kaydettim.
Uygulamamız hazır.
Evcil hayvanlarımıza bakabiliriz.
Onlarla mesajlaşabiliriz.
Bununla ilgili bir videoya göz atabiliriz.
Evet, en popüler evcil hayvanları görebiliyoruz.
Ancak bunun pek iyi olmadığını zaten görüyorsunuz.
Şimdi, dramatik bir hava katan ve kedilerin olduğu bir resme bakalım.
Tamam, daha büyük görmek istiyorum.
Ana sayfaya dönmek için "Dah" deyin.
Tamam, arkadaşıma bundan bahsedeyim.
Evet, "Buddy", hm.
Uygulama pek iyi değil.
Ayrıca çok geniş.
Çok geniş olduğunu görüyorsunuz.
Yeniden boyutlandırmayı deneyelim.
Ne?
Evet, tamam.
Yazdığım metin de kayboldu.
Şimdi Gemini'dan bu sorunu düzeltmesini isteyelim.
Uyarlanabilir beceri sayesinde istem, "uygulamayı uyarlanabilir hale getir" kadar basit olabilir.
Gördüğünüz gibi, beceriyle birlikte bunu yapabilen Gemini 3.0 Flash'ı kullanıyorum.
Beceriyi aldı ve şimdi değişiklikleri uygulamaya başlıyor.
"MediaQuery deneysel bir özellik.
Bu uygulamayı kullanmak istiyor musun?"
Devam edin.
Bazı kısımları kesmek için videoyu hızlandırmam gerekti ancak demo alanına gelirseniz becerileri kendiniz deneyebilirsiniz.
Bu, kullanıma sunduğumuz becerilerden yalnızca biri.
Örneğin, muhteşem ekran kalemi deneyimleri oluşturmanıza yardımcı olacak yeni bir becerimiz de var.
Denemek için alana gelin.
İşlem tamamlandı.
11 dosyayı değiştirdi.
Şimdi bunu uygulamalı olarak görelim.
Anladım.
Grid, işini zaten yapıyor.
Gezinme menüsü yana taşındı.
Filtreleri deneyelim.
Şimdi çalışacaklarını düşünüyorum.
Evet, öyle.
Tamam, arkadaşıma mesaj atıp uygulamanın artık harika olduğunu söyleyeyim.
Daha fazla alanla nasıl göründüğüne bakalım.
Evet, liste-ayrıntı düzenine taşındı.
Bu çalıştırma yaklaşık 20 dakika sürdü ve yaklaşık 200.000 jeton kullanıldı.
Anladım.
Francesco Romano
Normalde konuşmalarıma işletme metrikleri ve başarı öyküleriyle başlarım ancak ADS için doğrudan koda geçmek istedim.
Ancak mühendislik yatırım getirisini paylaşmak için birkaç dakika ayırmak istiyorum.
Android uygulamanız şu anda 580 milyondan fazla etkin büyük ekranlı cihazda çalışabilir. Ayrıca, çeşitli cihazlar ve katlanabilir cihaz kullanıcılarının Android'de en yüksek etkileşime sahip segmentler arasında olduğunu biliyoruz.
Uyarlanabilir bir derleme yayınladığınızda neler olduğunu gösteren iki somut örnek olay incelemesine bakalım.
İlk olarak, popüler bir öz bakım ve günlük tutma uygulaması olan Gratitude'dan bahsedelim.
Gratitude, uyarlanabilir ekran tasarımlarını benimseyerek tabletleri ve katlanabilir cihazları yüksek büyüme sağlayan bir edinme kanalı haline getirdi.
İlk olarak, büyük ekranlarda edinme hızını iki katına çıkardılar.
Özellikle tablet ve katlanabilir cihazlardan gelen günlük yükleme sayısı %100 arttı.
Toplam kullanıcı tabanlarındaki büyük ekran payını, toplam yüklemelerin %7'sinden %10'una çıkardılar.
İkincisi ise Formula 1 ve motor sporları için tamamlayıcı bir uygulama olan BoxBoxClub.Uygulamanın katlanabilir cihazlar ve daha büyük ekranlar için güncellenmesinin ardından telemetri verileri, genel etkin kullanıcı sayısında %7,6 ve etkileşim süresinde %54 artış gibi büyük kazanımlar gösterdi.
Anladım.
Konuşmamızın sonuna geldik. Size üç önemli noktayı hatırlatmak istiyorum.
Uyarlanabilir uygulama geliştirmek, uygulamanızın daha fazla cihaza ve kullanıcıya hizmet vermesini sağlarken uygulamanızı bu genişleyen ekosistem için geleceğe hazır hale getirir.
Android geliştirme, Compose önceliklidir.
Tüm yeni araçlar, kitaplıklar ve beceriler Compose için geliştirilmiştir.
Compose, uyarlanabilir düzenler ve temsilci tabanlı geliştirme için idealdir.
Kod tabanları daha küçük ve anlaşılması daha kolaydır. Temsilciler, Compose koduyla çalışırken daha az jeton kullanır.
Yeni araçları, becerileri ve API'leri deneyip keyfini çıkarın.
Teşekkür ederiz.
<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>mediaQuery { ... }
derivedMediaQuery { ... }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
}@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
}
}@Composable
fun currentVideoPlayerLayout(): VideoPlayerLayout {
return mediaQuery {
when(windowPosture) {
UiMediaScope.Posture.Tabletop -> VideoPlayerLayout.Tabletop
else -> VideoPlayerLayout.Default
}
}
}@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")
}
}
}@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()
}
}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...
}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))
}@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)
}
}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()
}
}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)
}
}@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()
}
}
}@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) }
)
}
}
}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...
}if (isExpandedWidth) {
/* return a multi-pane scene */
}
else {
/* return a single pane scene */
}android skills list
android skills add adaptive