Jetpack Compose için Kotlin

Jetpack Compose, Kotlin'i temel alır. Bazı durumlarda Kotlin, iyi Compose kodu yazmayı kolaylaştıran özel deyimler sunar. Başka bir programlama dilinde düşünüp bu dili zihninizde Kotlin'e çevirirseniz Compose'un bazı güçlü yönlerini kaçırabilir ve deyimsel olarak yazılmış Kotlin kodunu anlamakta zorlanabilirsiniz. Kotlin'in stiline daha fazla aşina olmak, bu tür sorunlardan kaçınmanıza yardımcı olabilir.

Varsayılan bağımsız değişkenler

Kotlin işlevi yazarken, çağıran bu değerleri açıkça iletmezse kullanılacak işlev bağımsız değişkenleri için varsayılan değerler belirtebilirsiniz. Bu özellik, aşırı yüklenmiş işlevlere olan ihtiyacı azaltır.

Örneğin, kare çizen bir işlev yazmak istediğinizi varsayalım. Bu işlevin, her bir kenarın uzunluğunu belirten tek bir zorunlu parametresi (sideLength) olabilir. thickness, edgeColor gibi birkaç isteğe bağlı parametresi olabilir. Çağıran bunları belirtmezse işlev varsayılan değerleri kullanır. Diğer dillerde birkaç işlev yazmanız gerekebilir:

// We don't need to do this in Kotlin!
void drawSquare(int sideLength) { }

void drawSquare(int sideLength, int thickness) { }

void drawSquare(int sideLength, int thickness, Color edgeColor) { }

Kotlin'de tek bir işlev yazabilir ve bağımsız değişkenler için varsayılan değerleri belirtebilirsiniz:

fun drawSquare(
    sideLength: Int,
    thickness: Int = 2,
    edgeColor: Color = Color.Black
) {
}

Bu özellik, sizi birden fazla gereksiz işlev yazmaktan kurtarmanın yanı sıra kodunuzun çok daha kolay okunmasını sağlar. Arayan, bir bağımsız değişken için değer belirtmezse varsayılan değeri kullanmak istediğini belirtmiş olur. Ayrıca, adlandırılmış parametreler neler olup bittiğini görmeyi çok daha kolaylaştırır. Koda baktığınızda bu şekilde bir işlev çağrısı görürseniz drawSquare() kodunu kontrol etmeden parametrelerin ne anlama geldiğini bilemeyebilirsiniz:

drawSquare(30, 5, Color.Red);

Buna karşılık, aşağıdaki kod kendi kendini belgelemektedir:

drawSquare(sideLength = 30, thickness = 5, edgeColor = Color.Red)

Çoğu Compose kitaplığı varsayılan bağımsız değişkenleri kullanır. Yazdığınız composable işlevler için de aynı şeyi yapmanız önerilir. Bu yöntem, composable'larınızı özelleştirilebilir hale getirir ancak varsayılan davranışın çağrılmasını kolaylaştırır. Örneğin, aşağıdaki gibi basit bir metin öğesi oluşturabilirsiniz:

Text(text = "Hello, Android!")

Bu kod, Text parametrelerinin daha fazlasının açıkça ayarlandığı aşağıdaki çok daha ayrıntılı kodla aynı etkiye sahiptir:

Text(
    text = "Hello, Android!",
    color = Color.Unspecified,
    fontSize = TextUnit.Unspecified,
    letterSpacing = TextUnit.Unspecified,
    overflow = TextOverflow.Clip
)

İlk kod snippet'i çok daha basit ve kolay okunur olmasının yanı sıra kendi kendini açıklayan bir yapıya sahiptir. Yalnızca text parametresini belirterek diğer tüm parametreler için varsayılan değerleri kullanmak istediğinizi belirtmiş olursunuz. Buna karşılık, ikinci snippet, diğer parametrelerin değerlerini açıkça ayarlamak istediğinizi gösterir. Ancak ayarladığınız değerler, işlevin varsayılan değerleridir.

Üst düzey işlevler ve lambda ifadeleri

Kotlin, diğer işlevleri parametre olarak alan üst düzey işlevleri destekler. Compose bu yaklaşımı temel alır. Örneğin, Button composable işlevi bir onClick lambda parametresi sağlar. Bu parametrenin değeri, kullanıcı düğmeyi tıkladığında düğmenin çağırdığı bir işlevdir:

Button(
    // ...
    onClick = myClickFunction
)
// ...

Üst düzey işlevler, bir işlev olarak değerlendirilen lambda ifadeleriyle doğal olarak eşleşir. İşleve yalnızca bir kez ihtiyacınız varsa üst düzey işleve iletmek için başka bir yerde tanımlamanız gerekmez. Bunun yerine, fonksiyonu lambda ifadesiyle hemen orada tanımlayabilirsiniz. Önceki örnekte, myClickFunction() öğesinin başka bir yerde tanımlandığı varsayılmaktadır. Ancak bu işlevi yalnızca burada kullanıyorsanız işlevi lambda ifadesiyle satır içinde tanımlamak daha kolaydır:

Button(
    // ...
    onClick = {
        // do something
        // do something else
    }
) { /* ... */ }

Sondaki lambda'lar

Kotlin, son parametresi lambda olan üst düzey işlevleri çağırmak için özel bir söz dizimi sunar. Bu parametre olarak bir lambda ifadesi iletmek istiyorsanız sondaki lambda söz dizimini kullanabilirsiniz. Lambda ifadesini parantez içine koymak yerine parantezden sonra koyarsınız. Bu, Compose'da yaygın bir durumdur. Bu nedenle, kodun nasıl göründüğünü bilmeniz gerekir.

Örneğin, Column() composable işlevi gibi tüm düzenlerin son parametresi, alt kullanıcı arayüzü öğelerini oluşturan bir işlev olan content'dir. Üç metin öğesi içeren bir sütun oluşturmak istediğinizi ve bazı biçimlendirmeler uygulamanız gerektiğini varsayalım. Bu kod çalışır ancak çok zahmetlidir:

Column(
    modifier = Modifier.padding(16.dp),
    content = {
        Text("Some text")
        Text("Some more text")
        Text("Last text")
    }
)

content parametresi, işlev imzasındaki son parametre olduğu ve değerini lambda ifadesi olarak ilettiğimiz için parantezlerin dışına çıkarabiliriz:

Column(modifier = Modifier.padding(16.dp)) {
    Text("Some text")
    Text("Some more text")
    Text("Last text")
}

İki örnek de tamamen aynı anlama gelir. Süslü parantezler, content parametresine iletilen lambda ifadesini tanımlar.

Aslında, ilettiğiniz tek parametre sondaki lambda ise (yani son parametre bir lambda ise ve başka parametre iletmiyorsanız) parantezleri tamamen çıkarabilirsiniz. Örneğin, Column öğesine bir değiştirici iletmeniz gerekmediğini varsayalım. Kodu şu şekilde yazabilirsiniz:

Column {
    Text("Some text")
    Text("Some more text")
    Text("Last text")
}

Bu söz dizimi, Compose'da özellikle Column gibi düzen öğeleri için oldukça yaygındır. Son parametre, öğenin alt öğelerini tanımlayan bir lambda ifadesidir. Bu alt öğeler, işlev çağrısından sonra parantez içinde belirtilir.

Kapsamlar ve alıcılar

Bazı yöntemler ve özellikler yalnızca belirli bir kapsamda kullanılabilir. Sınırlı kapsam, işlevleri gerektiği yerde sunmanıza ve bu işlevleri uygun olmayan yerlerde yanlışlıkla kullanmaktan kaçınmanıza olanak tanır.

Compose'da kullanılan bir örneği inceleyelim. Row düzen composable'ını çağırdığınızda içerik lambda'nız otomatik olarak bir RowScope içinde çağrılır. Bu sayede Row, yalnızca Row içinde geçerli olan işlevleri kullanabilir. Aşağıdaki örnekte, Row işlevinin align değiştiricisi için satıra özgü bir değeri nasıl gösterdiği açıklanmaktadır:

Row {
    Text(
        text = "Hello world",
        // This Text is inside a RowScope so it has access to
        // Alignment.CenterVertically but not to
        // Alignment.CenterHorizontally, which would be available
        // in a ColumnScope.
        modifier = Modifier.align(Alignment.CenterVertically)
    )
}

Bazı API'ler, alıcı kapsamında çağrılan lambda'ları kabul eder. Bu lambda'lar, parametre bildirimine göre başka bir yerde tanımlanan özelliklere ve işlevlere erişebilir:

Box(
    modifier = Modifier.drawBehind {
        // This method accepts a lambda of type DrawScope.() -> Unit
        // therefore in this lambda we can access properties and functions
        // available from DrawScope, such as the `drawRectangle` function.
        drawRect(
            /*...*/
            /* ...
        )
    }
)

Daha fazla bilgi için Kotlin belgelerindeki alıcı içeren işlev değişmezleri bölümüne bakın.

Yetki verilmiş mülkler

Kotlin, yetki verilmiş özellikleri destekler. Bu özellikler alanmış gibi çağrılır ancak değerleri bir ifade değerlendirilerek dinamik olarak belirlenir. Bu özellikleri, by söz dizimini kullanmalarından tanıyabilirsiniz:

class DelegatingClass {
    var name: String by nameGetterFunction()

    // ...
}

Diğer kodlar, özelliğe aşağıdaki gibi bir kodla erişebilir:

val myDC = DelegatingClass()
println("The name property is: " + myDC.name)

println() yürütüldüğünde, dize değerini döndürmek için nameGetterFunction() çağrılır.

Bu yetki verilmiş özellikler, özellikle durum destekli özelliklerle çalışırken kullanışlıdır:

var showDialog by remember { mutableStateOf(false) }

// Updating the var automatically triggers a state change
showDialog = true

Veri sınıflarını parçalama

Bir veri sınıfı tanımlarsanız verilere yapı bozma bildirimi ile erişebilirsiniz. Örneğin, bir Person sınıfı tanımladığınızı varsayalım:

data class Person(val name: String, val age: Int)

Bu türde bir nesneniz varsa değerlerine aşağıdaki gibi bir kodla erişebilirsiniz:

val mary = Person(name = "Mary", age = 35)

// ...

val (name, age) = mary

Oluşturma işlevlerinde genellikle bu tür kodlar görürsünüz:

Row {

    val (image, title, subtitle) = createRefs()

    // The `createRefs` function returns a data object;
    // the first three components are extracted into the
    // image, title, and subtitle variables.

    // ...
}

Veri sınıfları, başka birçok yararlı işlev sunar. Örneğin, bir veri sınıfı tanımladığınızda derleyici, equals() ve copy() gibi faydalı işlevleri otomatik olarak tanımlar. Veri sınıfları belgelerinde daha fazla bilgi bulabilirsiniz.

Tekil nesneler

Kotlin, her zaman tek bir örneği olan sınıflar olan singleton'lar tanımlamanıza olanak tanır. Bu singleton'lar object anahtar kelimesiyle tanımlanır. Compose, bu tür nesneleri sıklıkla kullanır. Örneğin, MaterialTheme tekil nesne olarak tanımlanır; MaterialTheme.colors, shapes ve typography özelliklerinin tümü geçerli temanın değerlerini içerir.

Tip güvenli derleyiciler ve DSL'ler

Kotlin, türe göre güvenli oluşturucularla alana özgü diller (DSL'ler) oluşturmanıza olanak tanır. DSL'ler, karmaşık hiyerarşik veri yapılarını daha sürdürülebilir ve okunabilir bir şekilde oluşturmanıza olanak tanır.

Jetpack Compose, LazyRow ve LazyColumn gibi bazı API'ler için DSL'leri kullanır.

@Composable
fun MessageList(messages: List<Message>) {
    LazyColumn {
        // Add a single item as a header
        item {
            Text("Message List")
        }

        // Add list of messages
        items(messages) { message ->
            Message(message)
        }
    }
}

Kotlin, alıcı içeren işlev değişmezlerini kullanarak tür güvenli oluşturucular sağlar. Canvas composable'ını örnek olarak ele alırsak bu composable, alıcı olarak DrawScope içeren bir işlevi parametre olarak alır (onDraw: DrawScope.() -> Unit). Bu sayede kod bloğu, DrawScope içinde tanımlanan üye işlevlerini çağırabilir.

Canvas(Modifier.size(120.dp)) {
    // Draw grey background, drawRect function is provided by the receiver
    drawRect(color = Color.Gray)

    // Inset content by 10 pixels on the left/right sides
    // and 12 by the top/bottom
    inset(10.0f, 12.0f) {
        val quadrantSize = size / 2.0f

        // Draw a rectangle within the inset bounds
        drawRect(
            size = quadrantSize,
            color = Color.Red
        )

        rotate(45.0f) {
            drawRect(size = quadrantSize, color = Color.Blue)
        }
    }
}

Tip güvenli derleyiciler ve DSL'ler hakkında daha fazla bilgi edinmek için Kotlin'in dokümanlarını inceleyin.

Kotlin eş yordamları

Eş yordamlar, Kotlin'de dil düzeyinde eşzamansız programlama desteği sunar. Eş yordamlar, iş parçacıklarını engellemeden yürütmeyi askıya alabilir. Duyarlı bir kullanıcı arayüzü, doğası gereği eşzamansızdır. Jetpack Compose, geri çağırmalar yerine API düzeyinde eş yordamları kullanarak bu sorunu çözer.

Jetpack Compose, eş yordamların kullanıcı arayüzü katmanında güvenli bir şekilde kullanılmasını sağlayan API'ler sunar. rememberCoroutineScope işlevi, etkinlik işleyicilerde eş yordamlar oluşturmak ve Compose askıya alma API'lerini çağırmak için kullanabileceğiniz bir CoroutineScope döndürür. ScrollState'ın animateScrollTo API'sini kullanan aşağıdaki örneğe bakın.

// Create a CoroutineScope that follows this composable's lifecycle
val composableScope = rememberCoroutineScope()
Button(
    // ...
    onClick = {
        // Create a new coroutine that scrolls to the top of the list
        // and call the ViewModel to load data
        composableScope.launch {
            scrollState.animateScrollTo(0) // This is a suspend function
            viewModel.loadData()
        }
    }
) { /* ... */ }

Eş yordamlar, kod bloğunu varsayılan olarak sırayla yürütür. Askıya alma işlevini çağıran çalışan bir eş yordam, askıya alma işlevi dönene kadar yürütülmesini askıya alır. Bu durum, askıya alma işlevi yürütmeyi farklı bir CoroutineDispatcher öğesine taşısa bile geçerlidir. Önceki örnekte, loadData, askıya alma işlevi animateScrollTo döndürülene kadar yürütülmez.

Kodun eşzamanlı olarak yürütülmesi için yeni eş yordamlar oluşturulması gerekir. Yukarıdaki örnekte, ekranın üst kısmına kaydırma ve viewModel kaynağından veri yükleme işlemlerini paralel hale getirmek için iki eş yordam gerekir.

// Create a CoroutineScope that follows this composable's lifecycle
val composableScope = rememberCoroutineScope()
Button( // ...
    onClick = {
        // Scroll to the top and load data in parallel by creating a new
        // coroutine per independent work to do
        composableScope.launch {
            scrollState.animateScrollTo(0)
        }
        composableScope.launch {
            viewModel.loadData()
        }
    }
) { /* ... */ }

Eş yordamlar, eşzamansız API'leri birleştirmeyi kolaylaştırır. Aşağıdaki örnekte, kullanıcı ekrana dokunduğunda bir öğenin konumuna animasyon eklemek için pointerInput değiştiricisini animasyon API'leriyle birleştiriyoruz.

@Composable
fun MoveBoxWhereTapped() {
    // Creates an `Animatable` to animate Offset and `remember` it.
    val animatedOffset = remember {
        Animatable(Offset(0f, 0f), Offset.VectorConverter)
    }

    Box(
        // The pointerInput modifier takes a suspend block of code
        Modifier
            .fillMaxSize()
            .pointerInput(Unit) {
                // Create a new CoroutineScope to be able to create new
                // coroutines inside a suspend function
                coroutineScope {
                    while (true) {
                        // Wait for the user to tap on the screen and animate
                        // in the same block
                        awaitPointerEventScope {
                            val offset = awaitFirstDown().position

                            // Launch a new coroutine to asynchronously animate to
                            // where the user tapped on the screen
                            launch {
                                // Animate to the pressed position
                                animatedOffset.animateTo(offset)
                            }
                        }
                    }
                }
            }
    ) {
        Text("Tap anywhere", Modifier.align(Alignment.Center))
        Box(
            Modifier
                .offset {
                    // Use the animated offset as the offset of this Box
                    IntOffset(
                        animatedOffset.value.x.roundToInt(),
                        animatedOffset.value.y.roundToInt()
                    )
                }
                .size(40.dp)
                .background(Color(0xff3c1361), CircleShape)
        )
    }

Eş yordamlar hakkında daha fazla bilgi edinmek için Android'de Kotlin eş yordamları kılavuzuna göz atın.