Jetpack Compose için Kotlin

Jetpack Compose, Kotlin üzerine kurulmuştur. Bazı durumlarda Kotlin, iyi Compose kodu yazmayı kolaylaştıran özel deyimler sağlar. Başka bir programlama dilinde düşünüp bu dili zihinsel olarak Kotlin'e çevirirseniz Compose'un bazı avantajlarını kaçırabilir ve deyimsel olarak yazılmış Kotlin kodunu anlamakta zorlanabilirsiniz. Kotlin'in stiline daha fazla aşina olmak bu sorunlardan kaçınmanıza yardımcı olabilir.

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

Bir Kotlin işlevi yazdığınızda, arayan bu değerleri açıkça iletmezse kullanılan 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 kenarın uzunluğunu belirten tek bir zorunlu parametresi (sideLength) olabilir. thickness, edgeColor gibi birkaç isteğe bağlı parametresi olabilir. Arayan bu parametreleri 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, birden fazla gereksiz işlev yazmanızı engellemenin yanı sıra kodunuzun okunmasını da kolaylaştırır. Arayan, bir bağımsız değişken için değer belirtmezse varsayılan değeri kullanmaya istekli olduğunu gösterir. Ayrıca, adlandırılmış parametreler sayesinde neler olduğunu görmek çok daha kolaydır. Koda baktığınızda aşağıdaki gibi 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, bu 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ı yöntemi kullanmanız önerilir. Bu uygulama, composable işlevlerinizi özelleştirilebilir hale getirir ancak varsayılan davranışın basit bir şekilde çağrılmasını sağlar. Örneğin, aşağıdaki gibi basit bir metin öğesi oluşturabilirsiniz:

Text(text = "Hello, Android!")

Bu kod, aşağıdaki çok daha ayrıntılı kodla aynı etkiye sahiptir. Bu kodda, Text parametrelerinin daha fazlası açıkça ayarlanmıştır:

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

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

Yüksek sıralı işlevler ve lambda ifadeleri

Kotlin, yüksek sıralı işlevleri (parametre olarak başka işlevler alan işlevler) destekler. Compose, bu yaklaşıma dayanı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
)
// ...

Yüksek sıralı işlevler, doğal olarak lambda ifadeleri ile eşleşir. Bu ifadeler, bir işlev olarak değerlendirilir. İşlevi yalnızca bir kez kullanmanız gerekiyorsa yüksek sıralı işlevlere aktarmak için başka bir yerde tanımlamanız gerekmez. Bunun yerine, işlevi lambda ifadesiyle doğrudan 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çi olarak tanımlamak daha basittir:

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

Sondaki lambda ifadeleri

Kotlin, son parametresi lambda olan yüksek sıralı 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 parantezlerin içine koymak yerine sonrasına koyarsınız. Bu, Compose'da sık karşılaşılan 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 yayan 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ğundan ve değerini lambda ifadesi olarak ilettiğimizden, bu parametreyi parantezlerin dışına çıkarabiliriz:

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

İki örnek de tam olarak aynı anlama geliyor. Küme parantezleri, content parametresine aktarılan lambda ifadesini tanımlar.

Hatta ilettiğiniz tek parametre sondaki lambda ise (yani son parametre bir lambda ise ve başka parametre iletmiyorsanız) parantezleri tamamen atlayabilirsiniz. Örneğin, Column için 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 küme parantezleri içinde belirtilir.

Dürbünler ve alıcılar

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

Oluşturma'da kullanılan bir örneği inceleyelim. Row düzenini çağırdığınızda içerik lambda'nız otomatik olarak RowScope içinde çağrılır. Bu, Row sitesinin yalnızca Row içinde geçerli olan işlevleri göstermesini sağlar. Aşağıdaki örnekte, Row'nın 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ı içinde ç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, devredilmiş özellikler'i destekler. Bu özellikler alanmış gibi adlandı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, mülke aşağıdaki gibi bir kodla erişebilir:

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

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

Bu temsilci özellikleri, özellikle devlet destekli mülklerle çalışırken faydalıdır:

var showDialog by remember { mutableStateOf(false) }

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

Veri sınıflarını yapılarını bozma

Veri sınıfı tanımlarsanız destructuring declaration ile verilere kolayca erişebilirsiniz. Örneğin, 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

Bu tür kodları genellikle Compose işlevlerinde 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 yararlı işlevleri otomatik olarak tanımlar. Daha fazla bilgiyi data classes belgelerinde bulabilirsiniz.

Tekil nesneler

Kotlin, her zaman tek bir örneği olan sınıflar olan singleton'ları kolayca tanımlamanızı sağlar. Bu tekil nesneler, object anahtar kelimesiyle tanımlanır. Oluşturma özelliği genellikle bu tür nesneleri kullanır. Örneğin, MaterialTheme tek öğeli bir nesne olarak tanımlanır. MaterialTheme.colors, shapes ve typography özellikleri, mevcut temanın değerlerini içerir.

Tür güvenli oluşturucular ve DSL'ler

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

Jetpack Compose, LazyRow ve LazyColumn gibi bazı API'ler için DSL'ler 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ılı işlev değişmezleri kullanarak tür güvenli oluşturucuları garanti eder. Örnek olarak Canvas birleştirilebilir işlevini ele alırsak, alıcı olarak DrawScope ile bir işlevi parametre olarak alır. onDraw: DrawScope.() -> Unit, kod bloğunun DrawScope içinde tanımlanan üye işlevlerini çağırmasına olanak tanır.

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)
        }
    }
}

Tür güvenli derleyiciler ve DSL'ler hakkında daha fazla bilgiyi Kotlin dokümanlarında bulabilirsiniz.

Kotlin eş yordamları

Coroutines, Kotlin'de dil düzeyinde eşzamansız programlama desteği sunar. Coroutine'ler, 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 ve Jetpack Compose, geri çağırmaları kullanmak yerine API düzeyinde eş yordamları kullanarak bu sorunu çözer.

Jetpack Compose, kullanıcı arayüzü katmanında coroutine'lerin güvenli bir şekilde kullanılmasını sağlayan API'ler sunar. rememberCoroutineScope işlevi, etkinlik işleyicilerinde eş yordamlar oluşturabileceğiniz ve Compose askıya alma API'lerini çağırabileceğiniz bir CoroutineScope döndürür. ScrollState'nin 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()
        }
    }
) { /* ... */ }

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

Kodu eşzamanlı olarak yürütmek için yeni eş yordamlar oluşturulması gerekir. Yukarıdaki örnekte, ekranın en üstüne kaydırma ve viewModel'dan veri yükleme işlemlerini paralel hale getirmek için iki eşzamanlı rutin 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()
        }
    }
) { /* ... */ }

Coroutines, eşzamansız API'leri birleştirmeyi kolaylaştırır. Aşağıdaki örnekte, kullanıcı ekrana dokunduğunda bir öğenin konumunu canlandırmak 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
                        val offset = awaitPointerEventScope {
                            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)
        )
    }

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