Jetpack Compose ile Performans

Ömer Durmaz
8 min readOct 24, 2023

Merhabalar bu yazımızda geliştirdiğimiz projelerimizde uygulamamızın performansını iyi yönde etkileyecek dikkat edilmesi gereken şeylerden bahsedeceğiz. Öncelikle şunu söylemeliyim ki bu yazıyı okumak için ilk başta jetpack compose da uzman olmanıza gerek yok. Yeni öğrenmeye başlayan birisi de bu bilgileri dikkate alarak en baştan daha iyi performanslı uygulamalar geliştirebilir. Hadi başlayalım;

Recomposition

Öncelikle birazdan çok fazla değineceğimiz recomposition nedir ondan bahsedelim. Recomposition bir süreçtir ve composable methodlar eğer compose oluşturulan değişkenlerde bir değişiklik olduğunu anlar ise bu süreci başlatır. Süreç boyunca bu değişkenleri kullanan composable methodlar kontrol edilir ve eğer bu değişkenlerin değiştiği anlaşılırsa bu componentlar tekrar çizilir yani recompose olurlar. Ancak eğer bir değişiklik yok ise bu composable methodlar tekrar çizilmez ve skipped olurlar.

Layout Inspector

Android Studio Layout Inspector, Android uygulama geliştiricilerinin kullanabileceği bir araçtır. Bu araç, Android uygulamalarının kullanıcı arayüzlerini görsel olarak incelemelerine ve hata ayıklamalarına yardımcı olur. Layout Inspector, geliştiricilere uygulamalarının kullanıcı arayüzlerini anlamalarına ve sorunları tanımlamalarına yardımcı olur.

Layout Inspector’ün temel özellikleri şunlardır:

  1. Arayüz İnceleme: Layout Inspector, çalışan bir Android uygulamasının arayüzünü görsel olarak görüntüler. Geliştiriciler, uygulamanın kullanıcı arayüzünün her bileşenini (örneğin düğmeler, metin alanları, görünüm grupları) inceleyebilirler.
  2. Geliştirme ve Hata Ayıklama: Uygulama geliştiricileri, Layout Inspector’ü kullanarak bir uygulamanın kullanıcı arayüzünün nasıl düzenlendiğini anlayabilirler. Bu, düzgün yerleştirme, boyutlandırma ve stillendirme sorunlarını tespit etmelerine yardımcı olabilir.
  3. Özellikler ve Değerler: Layout Inspector, her bileşenin özelliklerini ve değerlerini görüntüler. Bu, bileşenlerin hangi özelliklere sahip olduğunu ve bu özelliklerin nasıl ayarlandığını anlamak için kullanışlıdır.
  4. Öğe Ağacı: Layout Inspector, bir uygulamanın kullanıcı arayüzünü hiyerarşik bir şekilde görüntüler. Bu, uygulamanın arayüzünün nasıl oluşturulduğunu ve öğelerin birbirleriyle ilişkisini anlamak için kullanışlıdır.

Ayrıca bunların dışında compose ile geliştirilen projelerde composable methodların kaç defa recompose olduğunu ve kaç defa skipped olduğunu görebiliriz. Layout inspectore aşağıdaki başlıklarda da değineceğimiz için ufak bir açıklamak istedim.

Layout inspector görünümü

Hesaplamaların Composable Dışında Yapılması

@Composable
fun MessagesCard(cardExpired: Boolean) {
val result = remember { mutableStateOf("") }

if (cardExpired) {
result.value = "Süresi Dolmuş"
} else {
result.value = "Süresi Dolmamış"
}

Text(text = result.value)
}

Yukarıdaki örnekteki gibi eğer hesaplama işlemi direkt Composable methodun içerisine yazılırsa her recomposition’ da bu if sorgusuna girecek ve ana akışı işlem çok hızlı gerçekleşse de etkileyecek.

@Composable
fun MessagesCard(cardExpired: Boolean) {
val result = remember { mutableStateOf("") }

LaunchedEffect(key1 = Unit){
if (cardExpired) {
result.value = "Süresi Dolmuş"
} else {
result.value = "Süresi Dolmamış"
}
}

Text(text = result.value)
}

Ancak yukarıdaki gibi bir Side Effect ile işlem recomposition akışından ayrı şekilde yapılırsa bu uygulamanızın performansını olumlu yönde etkileyecektir.

Döngü yerine LazyList kullanımı

Uygulamalarımızda bazı ekranlarda listeler kullanmamız ve her bir liste elemanı için ekrana bir composable çizmemiz gerekebilir.

@Composable
fun MessagesCard(list: List<String>) {
Column {
list.forEach { listItem ->
Text(text = listItem)
}
}
}

Eğer yukarıdaki gibi forEach ile ya da for ile bu listeyi çizmek istediğimizde compose aynı anda bütün liste item’ larını çizmek için ana akışı bekletecektir. Bu listede 1000 item da olabilir ve her bir item’ ın içi çok büyük de olabilir. Bu yüzden de çizilme esnasında ekranda takılmalar meydana gelmesi kaçınılmazdır.

@Composable
fun MessagesCard(list: List<String>) {
LazyColumn(state = rememberLazyListState()) {
items(list) { listItem ->
Text(text = listItem)
}
}
}

Ancak yukarıdaki şekilde LazyColumn ya da LazyRow ya da LazyGrid kullanırsak bu composable methodlar sadece ekranda görünen itemları çizecek ve bunu da asenkron yapacaktır. Bu sayede performans açısından ana akışınız kitlenmez ve daha akıcı bir görünüme sahip olursunuz. Ayrıca rememberLazyListState sayesinde lazy listinizin mevcut state’ ini takip edebilir, kaydırmak istediğiniz bir item’ a kaydırabilir ya da bir çok işlem uygulayabilirsiniz.

Data class içerisinde sadece ‘val’ kullanılması

Bazı durumlarda data classlar içerisine sonradan güncellemek üzere value parametresi değil de variable parametresi tanımlayabiliyoruz. Ancak bu konuda dikkat etmemiz gereken şey. Eğer doğrudan bu Data Class’ ıcomposable methoda gönderiyorsak ve içerisindeki variable parametresini güncellersek, composable methodlar bu data classın içerisinde hangi alanın güncellendiğini bilemeyeceğinden classın parametrelerini kullanan bütün composable methodları recompose eder. Bu da performans açısından uygulamanızı kötü etkileyebilir.

Farklı modüllerde bulunan data classları composable methodlarda kullanmak

Büyük projelerde modüler yapı kullanıldığı için örneğin data classlarınız data modülünde dururken compose ui elementlerini başka bir modülde bulunabiliyor. Burada dikkat etmemiz gereken şey farklı modülden gelen data classı direkt bir composable methoda verirsek composable method bu data classın stable olma durumunu yani değişip değişmeyeceğini anlayamayacağı için her içerisinde bir alan değiştiğinde, bütün data classı kullanan alanlar recompose olacaktır.

Bu durumdan kaçınmak için composable methodlarımızın olduğu modüle ui modeller oluşturarak farklı modülden gelen data classın verilerini bu ui modüle maplemek gerekmektedir. Bu sayede o ui modeli composable methoda verseniz bile aynı modülde oldukları için composable method bu data classın sadece güncellenen alanlarını recompose yapar.

Listelerde konum değiştirme

Uygulamamızda bir liste kullanmamız gerektiğini varsayalım. Bu listenin item’ larını da kendi oluşturduğumuz custom layout ile göstereceğiz. Eğer herhangi bir sebepten ötürü listemizin item’ larının index’ lerinin değiştirmemiz gerekirse. Bu her index değiştirme işleminde listeleme yaptığımız yerin recompose olması ve bütün item’ ları tekrar tekrar çizmesi demek. Bunun sebebi ise compose listenin item’ larının aynı kaldığının ancak sıralarının değiştiğini anlayamamasıdır.

Eğer LazyColumn kullanıyorsak otomatik olarak bu sorunu kendisi kontrol ediyor. Ancak kendi listemizi çizmemiz gerekirse bunun için aşağıdaki şekilde listemizin her bir item’ ına eşsiz keyler vermek liste item’ larının sırası değişse bile recompose olmasını önler.

@Composable
fun NewsList() {
Column{
newsList.forEach { newsItem ->
key(newsItem.id){ // bu sayede sadece itemların değerleri değiştiğinde item güncellenecek. Sırası değiştiğinde değil.
NewsItemLayout(newsItem)
}
}
}
}

Direkt instance değil de değer paslamak

Diyelim ki bir composable method içerisinde iki adet state’ imiz var ve bu statelere erişen iki farklı child composable methodumuz var. Eğer bu child composable methodlar direkt olarak state parametresinin instanceına erişebiliyorlarsa, her ekran recompose olduğunda bu stateler değişmese bile child composable methodlar da recompositiona girecektir. Recomposition sürecine girmeleri de bir maliyet olduğu için biz sadece gerekli alanların recompositiona girmesini istiyoruz. Bu anlattığım durum biraz kafa karıştırıcı gibi olduğu için örnekle inceleyelim;

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RecompositionTestWithoutLambda() {
var message by remember { mutableStateOf("") }
var count by remember { mutableStateOf(0) }
Column {
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text("$count")
Button(onClick = { count += 1 }) {
Text(text = "Arttir")
}
}
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
TextField(value = message, onValueChange = { message = it })
Button(onClick = { count += 1 }) {
Text(text = "Send")
}
}
}
}
direkt stateler alındığı için her seferinde recompose olan örnek

Yukarıda gördüğünüz gibi Text ve TextField componentleri direkt olarak state değerine eriştikleri için, örneğin butona tıklandığında count değeri değişince composable method içinde bulunan ve statelere doğrudan erişen componentler de recompositiona giriyor ve değeri değişen recompose olurken diğeri skip oluyor. Bu da istenilmeyen bir durum ki sadece ilgili alanların recompositiona olması bizim için önemli.

@Composable
fun RecompositionTest() {
var message by remember { mutableStateOf("") }
var count by remember { mutableStateOf(0) }
Column {
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
RecompositionTestText { count }
Button(onClick = { count += 1 }) {
Text(text = "Arttir")
}
}
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
RecompositionTestTextField(message = { message }, onValueChange = {
message = it
})
Button(onClick = { count += 1 }) {
Text(text = "Send")
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RecompositionTestTextField(message: () -> String, onValueChange: (String) -> Unit) {
TextField(value = message.invoke(), onValueChange = onValueChange)
}
@Composable
fun RecompositionTestText(count: () -> Int) {
Text("${count.invoke()}")
}

Ancak bu örneğe bakacak olursak. Burada state parametrelerinin instancelarına doğrudan erişmek yerine onların value değerlerine eriştik. Bunun için de kullandığımız componentleri gruplara ayırıp. Direkt state parametresini değil de lambda fonksiyonu olarak state’in değeri almasını bekledik. Bu sayede herhangi bir text state değişikliğinde sadece onun değerine erişen composable methodlar recompositiona girecektir.

lambda yoluyla sadece value değerini aktararak recompose önlenen örnek

Bu şekilde siz de componentlerinizi gruplandırarak ve paslayacağınız state değerlerini lambda içerisinde alırsanız. Büyük projelerde oluşabilecek recomposition problemlerinin önüne geçmiş olursunuz.

Lambda fonksiyonlar

Örneğin bir Composable method oluşturduk ve bu methodumuz içerisinde lambda fonksiyon barındırıyor. Bu lambda fonksiyonun ise Composable methodumuz içinde birden fazla yerden çağırıldığını düşünelim. Jetpack Compose lambda fonksiyon içerisine yazılan methodların stable olup olmadıklarını yorumlayamadığı için. Eğer lambda fonksiyon içerisinde bir ViewModel methodu ya da başka bir classın methodunu çalıştırmaya çalışırsak. Bu yorumlayamamadan kaynaklı olarak her lambda fonksiyon çağırıldığında recompose işlemi gerçekleştirilir. Bu lambda fonksiyonu çağıran bütün child composable methodlar da bu recompose olur.

Bu sorunu önlemek için birden fazla yöntemimiz mevcut. Öncelikle hatalı kullanıma bir göz atalım;

@Composable
fun LambdaFunctionProblemTest() {
var message by remember { mutableStateOf("") }
val viewModel = viewModel<LambdaFunctionViewModel>()
CustomView(
message = { message },
onValueChange = {
viewModel.changeLayout()
}
)
}

Yukarıdaki gibi direkt süslü parantezler içerisinde viewmodel işlemi yapmak önceden bahsettiğimiz gibi recompositiona sebep olacaktır.

Yöntem 1:

@Composable
fun LambdaFunctionProblemTest() {
var message by remember { mutableStateOf("") }
val viewModel = viewModel<LambdaFunctionViewModel>()
CustomView(
message = { message },
onValueChange = viewModel::changeLayout
)
}

Ancak bu şekilde süslü parantezleri kullanmadan direkt olarak viewModel methodunu referans etmek. Bu sorunumuzu çözmenin basit yollarından birisidir. Ancak bu her zaman çözüm olmaz. Çünkü bazen viewmodel methodunu çağırmadan önce bir kaç işlem yapmak ardından viewmodel methodunu çağırmak gerekebilir. Bu gibi durumlarda da;

Yöntem 2:

@Composable
fun LambdaFunctionProblemTest() {
var message by remember { mutableStateOf("") }
val viewModel = viewModel<LambdaFunctionViewModel>()
val changeLayoutLambda = remember<() -> Unit> {
{
viewModel.changeLayout()
}
}

CustomView(
message = { message },
onValueChange = changeLayoutLambda
)
}
}

Bu şekilde lambda fonksiyonumuzu remember ile stable bir alana taşırsak ve custom viewlerimizde bu remember olan alanımızı çağırırsak da recomposition sorununu çözmüş oluruz. Çünkü compose remember içerisindeki alanın stable olduğunu düşünerek recompositiona sokmaz ve biz de bu rememberlı methodumuzu rahatlıkla kullanabiliriz. Ancak bu kullanım çok çok gerekmediği durumlarda kullanılmamalı şeklinde uyarılar mevcut. Elimizden geldiğinde referans çözmeye çalışmamız daha mantıklı görünüyor.

Bu yazımızda da Jetpack Compose geliştirirken performans açısından bizim için önemli olabilecek bazı konulara değindik. Bu yöntemler sayesinde önemli büyük kapsamlı projelerimiz daha akıcı ve performanslı hale getirebiliriz. Bir sonraki yazımda görüşmek üzere!

--

--