Jetpack Compose için Kotlin

Jetpack Compose, Kotlin etrafında geliştirildi. Bazı durumlarda Kotlin, iyi Compose kodu yazmayı kolaylaştıran özel deyimler sağlar. Başka bir programlama dilinde düşünür ve o dili zihinsel olarak Kotlin'e çevirirseniz Compose'un gücünden bir kısmını kaçırabilirsiniz ve deyimsel olarak yazılmış Kotlin kodunu anlamak zor olabilir. Kotlin'in tarzına daha fazla alışmak, bu tehlikelerden kaçınmanıza yardımcı olabilir.

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

Bir Kotlin işlevi yazarken, çağrıyı yapan kişi bu değerleri açıkça iletmezse kullanılan işlev bağımsız değişkenleri için varsayılan değerleri belirtebilirsiniz. Bu özellik, aşırı yüklenmiş işlevler ihtiyacını azaltır.

Örneğin, kare çizen bir işlev yazmak istediğinizi varsayalım. Bu işlevde, her kenarın uzunluğunu belirten sideLength adlı tek bir zorunlu parametre olabilir. thickness ve edgeColor gibi isteğe bağlı parametrelere sahip olabilir. Arayan kişi bunları belirtmezse işlev varsayılan değerleri kullanır. Başka dillerde, birkaç fonksiyon yazabilirsiniz:

// 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, hem birden fazla gereksiz işlev yazma zahmetinden kurtarır hem de kodunuzun daha kolay okunmasını sağlar. Çağrıyı yapanın bir bağımsız değişken için değer belirtmemesi, varsayılan değeri kullanmak istediğini gösterir. Ayrıca, adlandırılmış parametreler olayın ne olduğunu görmeyi çok daha kolay hale getirir. 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 anlayamayabilirsiniz:

drawSquare(30, 5, Color.Red);

Bunun aksine, bu kod kendini belgelemektedir:

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

Çoğu Compose kitaplığı varsayılan bağımsız değişkenleri kullanır ve yazdığınız composable işlevler için aynısını yapmak iyi bir uygulamadır. Bu uygulama, composable'larınızı özelleştirilebilir hale getirir ancak yine de varsayılan davranışın çağrılmasını basit hale getirir. Dolayısıyla, örneğin, şunun gibi basit bir metin öğesi oluşturabilirsiniz:

Text(text = "Hello, Android!")

Bu kod, daha fazla Text parametresinin 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 okunması kolay olmasının yanı sıra kendi kendine belgelenmesine yardımcı olur. 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, bu diğer parametrelerin değerlerini açık bir şekilde ayarlamak istediğinizi ima eder. Ancak belirlediğiniz değerler, işlev için varsayılan değerler olur.

Ü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 onClick lambda parametresi sağlar. Bu parametrenin değeri, kullanıcı tıkladığında düğmenin çağırdığı bir işlevdir:

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

Daha yüksek düzeyli işlevler, bir işleve dönüşen ifadeler lambda ifadeleriyle doğal bir şekilde eşlenir. İşleve yalnızca bir kez ihtiyaç duyarsanız daha üst düzey işleve aktarmak için başka bir yerde tanımlamanız gerekmez. Bunun yerine, işlevi hemen orada bir lambda ifadesiyle tanımlayabilirsiniz. Önceki örnekte myClickFunction() öğesinin başka bir yerde tanımlandığı varsayılmıştır. Ancak bu işlevi yalnızca burada kullanırsanız işlevi bir lambda ifadesiyle satır içi olarak tanımlamak daha basit olur:

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

Sondaki lambdalar

Kotlin, son parametresi lambda olan üst düzey işlevleri çağırmak için özel bir söz dizimi sunar. Bir lambda ifadesini bu parametre olarak iletmek isterseniz trailing lambda söz dizimi kullanabilirsiniz. lambda ifadesini parantez içine almak yerine arkasına koyabilirsiniz. Bu, Compose'da sık karşılaşılan bir durumdur. Dolayısıyla, kodun nasıl göründüğü hakkında bilgi sahibi olmanız gerekir.

Örneğin, tüm düzenlerin son parametresi (ör. Column() composable işlevi), alt kullanıcı arayüzü öğeleri yayınlayan bir işlev olan content'dır. Üç metin öğesi içeren bir sütun oluşturmak istediğinizi ve birkaç biçimlendirme uygulamanız gerektiğini varsayalım. Bu kod çalışır ama çok kültüreldir:

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

content parametresi, fonksiyon imzasındaki son parametre olduğu ve değerini lambda ifadesi olarak aktardığımız için parametreyi parantezlerden çıkarabiliriz:

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

İki örneğin anlamı tamamen aynıdır. Küme ayraçları, content parametresine iletilen lambda ifadesini tanımlar.

Aslında ilettiğiniz tek parametre sondaki lambda ise, yani nihai 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 küme ayraçları içinde belirtilir.

Nişan dürbünleri ve alıcılar

Bazı yöntemler ve özellikler yalnızca belirli bir kapsamda kullanılabilir. Sınırlı kapsam, gerektiğinde işlevleri sunmanızı ve uygun olmadığı durumlarda bu işlevin yanlışlıkla kullanılmasını önlemenizi sağlar.

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

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 dokümanlarındaki alıcıyla işlev değişmez değerleri konusuna bakın.

Yetki verilen mülkler

Kotlin, yetki verilmiş mülkleri destekler. Bu özellikler, alanlarmış gibi çağrılır ancak değerleri, bir ifade değerlendirilerek dinamik bir şekilde belirlenir. Bu özellikleri, by söz dizimini kullanarak tanıyabilirsiniz:

class DelegatingClass {
    var name: String by nameGetterFunction()

    // ...
}

Diğer kodlar aşağıdaki gibi bir kodla mülke 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 yetki verilmiş özellikler özellikle eyalet destekli mülklerle ç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ı imha etme

Bir veri sınıfı tanımlarsanız verilere yıkıcı bir bildirim ile kolayca 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 bir kod 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ı, birçok başka faydalı 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.

Singleton nesneleri

Kotlin, her zaman bir ve yalnızca bir örneği olan sınıfları (singleton) bildirmeyi kolaylaştırır. Bu single'lar, object anahtar kelimesiyle belirtilir. Oluşturma işleminde genellikle bu tür nesneler kullanılır. Örneğin, MaterialTheme tekli nesne olarak tanımlanır. MaterialTheme.colors, shapes ve typography özelliklerinin tümü geçerli temanın değerlerini içerir.

Tür güvenli derleyiciler ve DSL'ler

Kotlin, tür güvenli derleyicilerle alana özgü diller (DSL'ler) oluşturulmasına olanak tanır. DSL'ler, karmaşık hiyerarşik veri yapılarının daha sürdürülebilir ve okunabilir bir şekilde oluşturulmasına 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ılı işlev değişmez değerlerini kullanarak tür açısından güvenli derleyiciler oluşturur. Canvas composable'ı örnek olarak ele alırsak bu, parametre olarak alıcı onDraw: DrawScope.() -> Unit olarak DrawScope bulunan bir işlev alır ve 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)
        }
    }
}

Kotlin belgelerinde tür güvenli oluşturucular ve DSL'ler hakkında daha fazla bilgi edinebilirsiniz.

Kotlin eş yordamları

Eş yordamlar, Kotlin'de dil düzeyinde eşzamansız programlama desteği sunar. Eş yordamlar, ileti dizilerini 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ğırma işlevleri yerine API düzeyinde eş yordamları benimseyerek bu sorunu çözer.

Jetpack Compose, kullanıcı arayüzü katmanında eş yordamların güvenli bir şekilde kullanılmasını sağlayan API'ler sunar. rememberCoroutineScope işlevi, etkinlik işleyicilerde eş yordamlar oluşturabileceğiniz ve "Compose askıya alma API'lerini" çağırabileceğiniz bir CoroutineScope döndürür. ScrollState animateScrollTo API'sini kullanan aşağıdaki örneğe göz atı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 geri dönene kadar yürütmesini askıya alır. Askıya alma işlevi, yürütmeyi farklı bir CoroutineDispatcher öğesine taşısa bile bu durum geçerlidir. Yukarıdaki örnekte, loadData askıya alma işlevi animateScrollTo dönene kadar yürütülmez.

Kodu eşzamanlı olarak yürütmek için yeni eş yordamların oluşturulması gerekir. Yukarıdaki örnekte, kaydırma işlemini ekranın üst kısmına paralel yapmak ve viewModel ürünündeki verileri yüklemek 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'lerin birleştirilmesini 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
                        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)
        )
    }

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