Jetpack Compose Serisi — 2. Responsive Screen

Mert Toptas
8 min readFeb 10, 2022

--

Selamlar, geçenlerde Jetpack Compose ile ilgili yazı serisinin ilk yazısını yayımlamıştım. İlk yazıda tema hakkında konuşmuştuk.

Bu seride işlenecek konular sırasıyla Theming, Responsive Ekran Oluşturma, Component Kullanımı ve Navigation olacak.

Android’de bildiğiniz üzere farklı çözünürlükte onlarca cihaz var ve geliştirilen uygulamanın tüm cihazlara uyumlu olması gerekmektedir. İyi bir kullanıcı deneyimi için de küçük ekran çözünürlüğüne sahip bir telefonda da yüksek çözünürlüğe sahip telefonda da UI’ın uygun bir şekilde gözükmesini isteriz. Bu sebeple UI oluştururken responsive olmasına dikkat ederiz.

ompose ile geliştirirken birçok yolla ekranın tasarımı yapabilirsiniz. Column ve Rowlarla çalışıp direkt view elementleri ekleyebilir veya weight ile ekranı oransal birkaç parçaya ayırıp o oranlara göre view’ı oluşturabilir veya Constraint ile koordinat üzerinde vererek ekranı oluşturabilirsiniz.

Burada ekrana çizilecek tasarımın ne kadar kompleks olup olmadığı da bence önemli bir parametre. Şöyle ki bir tasarımda 1–2 buton ve text’den oluşan bir ekranı kolayca Column oluşturarak çizebilirsiniz. Bunun için çok fazla çözüm düşünmenize gerek yok. Çünkü tasarımın gerektirdiği alan hemen hemen her ekran için ideal olacağı için kolayca oluşturabilirsiniz.

Hem kompleks hem de basit bir ekran geliştirirken yaklaşım farklılık gösterebilir. Bu yazıda iki farklı ekranın tasarımını yapacağız.

Bloom

Bu yazıda referans alacağımız tasarım geçenlerde Android DevChallange ile geliştiricilerin Compose ile geliştirmesini yaptığı yarışma olan Bloom’u alacağız. Detaya ve tasarıma buradan ulaşabilirsiniz.

İlk Tasarım -Welcome Screen

Welcome Screen

Welcome Screen ekranında görüldüğü üzere background, sağ tarafa yerleştirilmiş şekilde bir görsel son olarak da text ve buttondan oluşmakta. Ekranın yerleşim değerleri ise şöyle;

Haydi şimdi elimizdeki bu bilgilere göre ekranı tasarlayalım;

İlk olarak en yalın haliyle yazacağım bakalım küçük ve büyük ekranlarda tasarım nasıl gözüküyor?

Kodu incelemek gerekirse, tasarımda bulunan her bir view elementi kullanılmak üzere bir Composable fonksiyon haline getirdim. Bu sayede content bölümünde kodun okunabilirliğini attırdık.

İlk olarak background image’i ayarlamamız gerekiyor. Layoutun en arkasında bu image olmalı ve diğer tüm view elementleri bunun üzerinde olmalı ki tasarımı elde edebilelim. Bunun için Surcafe içerisinde ekledim.

Surface(color = MaterialTheme.colors.primary, modifier = Modifier.fillMaxSize()) {
WelcomeBackground(modifier = Modifier) }

Layout yapısı gereği en üste eklenen view ilk eklenir ve sonraki eklediğiniz buton, image veya textler bu view’ın üzerinde gözükecektir. Bunun için Column, Row gibi layout scope kullanmadan önce eklenmesi gerekir.

Column, bir kolon açar ve column içerisinde alt alta component ekleyebilirsiniz. XML’e benzetmek gerekirse orientation’ı horizontal olan LinearLayout’a denk gelir.

Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
)

Modifier.fillMaxSize ile tüm ekranı kaplamasını, Alignment.CenterHorizontally ile de column içindeki componentlerin ortalanmasını ayarlıyoruz. Böylelikle her bir component için tekrar tekrar ekrana ortalama işini tek seferde yapmış olduk.

Devam edelim..

Şimdi bu kodun iki farklı cihazdaki çıktısını görelim. Cihazlar, 6.0" ekrana ve 1080x2340 çözünürlüğe sahip Pixel 5 ve 4.0" ekrana ve 480x800 çözünürlüğe sahip Nexus S modeli. Küçük ve büyük ekranlarda nasıl göründüğünü anlamak için bu iki cihazı seçtim.

Pixel 5 ve Nexus S Görünümü

Bu istediğimiz bir sonuç muydu? Hiç sanmıyorum. Pixel 5'de iyi gözükürken, Nexus S’de ise ekrana sığmadı ve butonlar gözükmedi. Sebebi ise componentleri static olarak verip dp değerlerinin piksele dönüştüğü için oransızlık ortaya çıktı.

Peki şimdi nasıl yapabiliriz?

Jetpack Compose’da Modifier içerisinde weight özelliğini kullanarak ekranı oranlayarak daha responsive hale getirebiliriz. Weight, Column veya Row içerisinde yükseklik veya genişliği ekrana göre oranlayarak yeniden boyutlandırır.

Weight, öğenin yüksekliğini, Column’daki diğer öğelere göre orantılı olarak boyutlandırılmasıdır.

Kaynak: jetpackcompose.net

Üstteki görsele göre ekranın genişliğini 4'e bölüp weight oranına göre nasıl oranladığını iyi bir şekilde ifade etmiş. Weight 2 olan yeşil kutu genişliğin yarısını aldığını fark etmişsinizdir.

Tasarıma göre yaprak görselini 1 ile diğer componentleri de weight 1 ile oranlayıp yapabiliriz.

Dikkat etmişseniz WelcomeBackground, Column dışarısında bir Box içerisinde oranlamadan hariç tutulduğunu görmüşsünüzdür.

Box(
modifier = Modifier
.fillMaxWidth()
) {
WelcomeBackground(modifier = Modifier)
}

Sebebi ise background görselinin oranlamadan muaf olup ekrana yerleşmesi gerektiği için Box içerisinde olması gerekir. Box sayesinde stack şeklinde üstüne view eklememizi sağladı.

Şimdi nasıl gözüktüğünü görelim;

Gördüğünüz üzere iki farklı ekranda responsive olacak şekilde kodu yeniden düzenledik.

İkinci Tasarım — Home Screen

Bottom Bar Harici Home Screen

Home Screen ekranında görüldüğü üzere search bar, horiztontal list ve vertical bir listeden oluşmakta. Ekranın yerleşim değerleri ise şöyle;

Peki burada nasıl davranabiliriz?

Hiyerarşik olarak Column içerisinde LazyColumn oluşturup, her bir component bir item olacak şekilde oluşturabiliriz. Alt kısımdaki listenin tasarımını da ConstraintLayout ile yapabiliriz.

Hiyerşik Şeması

ConstraintLayout öğelerin ekranda diğer öğelere göre yerleştirilmesinde yardımcı olur. Birden çok Colum, Row ve Box kullanılmasına alternatiftir. ConstraintLayout, daha karmaşık hizalama gereksinimleri olan daha büyük düzenler uygularken kullanışlıdır.

ConstraintLayout için detaylara buradan ulaşabilirsiniz.

Jetpack Compose öncesinde bir listeyi göstermek için RecyclerView ve bir adapter oluşturduk. Şimdi ise burada göreceğimiz gibi LazyColumn veya LazyRow kullanarak çok sayıda listeyi dikey veya yatay olarak birkaç satır kodla gösterebiliriz.

LazyColumn ve LazyRow için buradan detaylı şekilde bakabilirsiniz.

LazyColumn ve LazyRow arasındaki fark öğelerin ekranda yerleştirme ve kaydırma (swipe) yönüdür. LazyColumn dikey iken LazyRow yatay şekilde hizalar ve kaydırılır.

İlk olarak Search Text tarafını yapalım.

Çıktısı;

SearchEditText’i OutlinedTextField’i kullanarak oluşturduk. Sonraki yazıda böyle fonksiyon içerisinde tek tek uzun bir şekilde buton, edittext gibi viewleri component haline getirip tek bir fonksiyonla kullanabilmeyi konuşacağız. O sebeple şimdilik fonksiyon içerinde sıfırdan oluşturduk.

Tema listesini oluşturalım. Buradaki componenti de ayrı bir fonksiyon içerisinde oluşturdum. Üstte belirttiğim gibi sonraki yazıda bunun nasıl ayrı bir custom component haline getirip uygulama üzerinde her yerde nasıl kullanabileceğini konuşacağız.

item {
BrowseThemesText(Modifier.paddingFromBaseline(top = 32.dp, bottom = 8.dp))
}

item {
BloomThemesCard(list = DummyData.themes)
}

BrowseThemesText ile title i oluşturduk.

@Composable
fun BrowseThemesText(modifier: Modifier) {
Text(text = "Browse themes", style = MaterialTheme.typography.h1, modifier = modifier)
}

Şimdi BloomThemesCard’a bakalım.

Listenin horizontal olması için LazyRow kullanmamız gerekmekte. LazyRow ile horizontal olarak bir liste yapabilirsiniz. LazyRow için items içerisinde bir listeye ihtiyacınız var. Listeyi verdikten sonra scope içerisinde ise bu listenin uzunluğu kadar hangi item’ın basılması gerektiğini yazmamız gerekiyor.

Aşağıda bir tane Card item’i oluşturduk. Yani list’in uzunluğu kadar Card’ı basacaktır.

Not: Örnekte kullanılan dummy datasını yazının sonunda vereceğim Github reposunda görebilirsiniz.

LazyRow(
contentPadding = PaddingValues(vertical = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
items(list) { theme ->
Card(
modifier = Modifier
.size(136.dp)
.clickable { },
elevation = 5.dp,
shape = MaterialTheme.shapes.small,
)
}

Çıktısına bakalım;

Gördüğünüz üzere LazyRow ile kolayca horizontal şekilde listeyi yapabildik. Şimdi ise alt kısmın tasarımını yapacağız. Burada da ConstraintLayout’u dahil edeceğiz.

item {
PlantListTitle(modifier = Modifier.fillMaxWidth())
}

items(DummyData.plants) { plant ->
Plant(
Modifier
.fillMaxWidth()
.padding(vertical = 4.dp), plant
)
}

DummyData.plants bir plant listesi verir. Daha önce belirttiğimiz gibi plants listesinin uzunluğu kadar Plant item’ı ekrana basacaktır.

Şimdi ConstraintLayout ile Plant item’ını oluşturalım.

ConstraintLayout’u kullanabilmek adına bunu implement etmeyi unutmayın. implementation "androidx.constraintlayout:constraintlayout-compose:1.0.0-beta02"

İlk olarak kodu buradan inceleyebilirsiniz, devamında ise parça parça kodu anlatacağım.

Compose’da, View System’daki Id’ye benzer şekilde referanslar oluşturmak için createRefs() işlevini kullanırız . ConstraintLayout işlevinin içinde view içerisinde kullanılacak viewlere göre id verip oluşturabiliriz.

ConstraintLayout(modifier = modifier) {
val (image, title, description, checkbox, divider) = createRefs()
}

Ref idleri bu şekilde oluşturduk. Her bir id tasarımdaki bir view’a denk gelmektedir. Peki nasıl çalışıyor? XML’de ConstraintLayout kullanıma aşınaysanız aslında temeldeki mantık aynı. Ref id’lere göre view’lerin layouttaki yerini kolayca ayarlabiliyorsunuz. Bu sadece karmaşık tasarımları kolayca yapabilirsiniz.

Tasarıma bakarsak, image solda olmalı, title image’in sağında olmalı ve 16 dplik bir uzaklıkta olmalı. Description, title’in hemen altında olmalı. Divider, bottom’da olmalı ve image’in bittiği yer ile checkbox’ın bittiği yerde olmalı. Checkbox ise en sağda olmalı. Şimdi bunu biraz koda dökelim.

val (image, title, description, checkbox, divider) = createRefs()
Image(
painter = rememberImagePainter(plant.image),
contentDescription = null,
modifier = Modifier
.size(64.dp)
.clip(MaterialTheme.shapes.small)
.constrainAs(image) {
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
start.linkTo(parent.start)
},
contentScale = ContentScale.Crop,
)

Modifier içerisinde constrainAs’a yakından bakalım.

fun Modifier.constrainAs(
ref: ConstrainedLayoutReference,
constrainBlock: ConstrainScope.() -> Unit
) = this.then(ConstrainAsModifier(ref, constrainBlock))

Bizden bir tane oluşturduğumuz ref id istemekte. Bu ref id’ye göre constrainti ayarlayabiliyor. Top, bottom, start, end, centerTo özellikleriyle yerleşimi yapabiliyoruz. ConstrainAs kapsamında ConstrainScope’a erişimimiz var. Artık image’ın başlangıç/bitiş/üst/alt kısmını image’in referans nesnesi aracılığıyla bağlayabiliriz. Ayrıca, üst nesneyle üst ConstraintLayout’a da bağlanabilirsiniz . Parent.top ile var layout’un parent’a ulaşıp buna göre hizalama yapabilirsiniz. Yani view, top, bottom ve start’a göre konum almış oldu.

            top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
start.linkTo(parent.start)

Çıktı;

Image’ı. başarıyla sol tarafa ekledik, şimdi ne yapmamız gerekiyor? Sağ tarafına title ve description’u 16 dplik uzaklıkta eklememiz gerekiyor.

Text(
text = plant.name,
modifier = Modifier
.constrainAs(title) {
top.linkTo(parent.top)
start.linkTo(image.end, margin = 16.dp)
}
.paddingFromBaseline(top = 24.dp),
style = MaterialTheme.typography.h2
)
Text(
text = plant.description,
modifier = Modifier
.constrainAs(description) {
top.linkTo(title.bottom)
start.linkTo(title.start)
},
style = MaterialTheme.typography.body1
)

Şimdi title’ı image’in sağında konumlandırmamız gerektiği için, ve image’dan 16 dp uzaklıkta olması gerektiğinden dolayı, start.linkTo(image.end, margin = 16.dp) diyerek bunu yapabiliriz. .paddingFromBaseline(top = 24.dp) ise base layouttan 24 dp uzaklıkta olması gerektiğini belirtiliyoruz.

Description’un yeri içinde title’ın altında ve title’ın startına göre ayarlarsanız istediğiniz yerde konumlanacaktır.

Çıktı;

Title ve Description Konumlandırılması

Evet, Checkbox ve divider’ın eklenmesine gelelim. Checkbox oldukça kolay şekilde en sağa yerleşmeli ve divider da en altta ve arada olacak şekilde olmalı.

Checkbox(
checked = plant.isChecked,
onCheckedChange = { },
modifier = Modifier.constrainAs(checkbox) {
top.linkTo(parent.top, margin = 16.dp)
bottom.linkTo(parent.bottom, margin = 24.dp)
end.linkTo(parent.end)
}
)

Divider(
modifier = Modifier
.height(1.dp)
.constrainAs(divider) {
end.linkTo(parent.end)
bottom.linkTo(parent.bottom)
},
thickness = 0.5.dp,
color = MaterialTheme.colors.secondary,
startIndent = 72.dp
)

Checkbox’da end.linkTo(parent.end) diyerek en sağa konumlandırabilirsiniz.

Divider’da thickness, divider kalınlığını, startIndent ise ne kadar uzaklıkta başlaması gerektiğini padding olarak belirtmemizi sağlamaktadır.

Çıktısı:

Ve evet! İstediğimiz tasarımı sonunda yaptık.

Şimdi bunu küçük ekranda da test edelim ve responsive olup olmadığına emin olalım.

Pixel 5 x Nexus S

Ve istediğimiz gibi oldu.

Sonuç

B u yazıda, birbirinden farklı ekran çözünürlüğe sahip cihazlarda bir tasarımını nasıl responsive yapabileceğimiz konusunu konuştuk ve birden fazla yolla yapabileceğimizi öğrendik. LazyColumn ve LazyRow kullanarak bir öğe listesinin nasıl görüntüleneceğini öğrendik. ConstaintLayout, kullanmayı ve karmaşık tasarımlarda referans id’lere göre kullanmayı öğrendik. Bu yazının ardından, bu uygulamayı kullanarak Jetpack Compose’da component oluşturma ve yönetmeyi konuşacağız.

Github Reposu:

Kaynaklar:

1- https://developer.android.com/jetpack/compose/layouts/constraintlayout

2- https://developer.android.com/jetpack/compose/layouts/adaptive

--

--

Mert Toptas

ex part-time law full-time mobile application#Flutter #Kotlin Android Developer at @Loodos linkedin.com/in/mertcantoptas/