Jetpack Compose için Kotlin

Jetpack Compose, Kotlin temel alınarak tasarlanmıştır. Bazı durumlarda Kotlin, iyi Compose kodu yazmayı kolaylaştıran özel deyimler sunar. Başka bir programlama dilinde düşünür ve bu dili zihinsel olarak Kotlin'e çevirirseniz Compose'un güçlü özelliklerinden bazılarını kaçırırsınız ve deyimsel olarak yazılmış Kotlin kodunu anlamayı zor bulabilirsiniz. Kotlin'in tarzını daha iyi tanımak bu tuzaklardan kaçınmanıza yardımcı olabilir.

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

Bir Kotlin işlevi yazarken işlev bağımsız değişkenleri için varsayılan değerler belirtebilirsiniz. Bu değerler, çağrıyı yapanın açıkça iletemediği durumlarda kullanılır. Bu özellik aşırı yüklü işlevlere olan ihtiyacı azaltır.

Örneğin, kare çizen bir işlev yazmak istediğinizi varsayalım. Bu işlevde, her kenarın uzunluğunu belirten tek bir gerekli parametre (sideLength) olabilir. Bu parametrenin thickness, edgeColor vb. gibi isteğe bağlı parametreleri olabilir. Çağrı yapan kişi bunları belirtmezse işlev varsayılan değerleri kullanır. Diğer dillerde ise 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 gereksiz birden fazla işlev yazma zahmetinden kurtarmanın yanı sıra, kodunuzu çok daha anlaşılır bir şekilde okumanızı 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 olup bitenleri çok daha kolay bir şekilde görmenizi sağlar. Koda baktığınızda şuna benzer bir işlev çağrısı görürseniz drawSquare() kodunu kontrol etmeden parametrelerin ne anlama geldiğini öğrenemeyebilirsiniz:

drawSquare(30, 5, Color.Red);

Buna karşılık, bu kod kendi kendini belgelendirir:

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ısını yapmak iyi bir uygulamadır. Bu uygulama, composable'larınızı özelleştirilebilir hale getirir ancak varsayılan davranışın çağrılmasını kolaylaştırır. Dolayısıyla, örneğin, şunun gibi basit bir metin öğesi oluşturabilirsiniz:

Text(text = "Hello, Android!")

Bu kod aşağıdakiyle aynı etkiye sahiptir ve çok daha ayrıntılı bir koddur. Bu kodda daha çok Text parametresi açık bir şekilde ayarlanır:

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

İlk kod snippet'i çok daha kolay ve okunması kolay olmanın yanı sıra kendi kendine belgeleme imkanı da sunar. 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, diğer parametrelerin değerlerini açık bir şekilde ayarlamak istediğinizi ima eder. Ancak, ayarladığınız değerler işlevin varsayılan değerleri olabilir.

Üst düzey işlevler ve lambda ifadeleri

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

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

Üst düzey işlevler, bir işlevi değerlendiren ifadeler olan lambda ifadeleri ile doğal olarak eşlenir. İşleve yalnızca bir kez ihtiyaç duyarsanız, üst düzey işleve geçirmek için başka bir yerde tanımlamanız gerekmez. Bunun yerine, işlevi lambda ifadesiyle hemen orada tanımlayabilirsiniz. Önceki örnekte, myClickFunction() başka bir yerde tanımlandığı varsayılır. Ancak burada yalnızca bu işlevi kullanırsanız işlevi lambda ifadesiyle satır içi olarak tanımlamak daha basittir:

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

Sonraki lambdalar

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 ifadesini iletmek istiyorsanız trailing lambda söz dizimini kullanabilirsiniz. Lambda ifadesini parantezin 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üğü hakkında bilgi sahibi olmanız gerekir.

Örneğin, Column() composable işlevi gibi tüm düzenlerin son parametresi content, alt kullanıcı arayüzü öğelerini yayınlayan bir işlevdir. Üç metin öğesi içeren bir sütun oluşturmak istediğinizi ve biçimlendirme uygulamanız gerektiğini varsayalım. Bu kod işe yarar, ama ç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 geçirdiğimiz için bunu parantezin dışına çıkarabiliriz:

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

İki örnek tamamen aynı anlama gelmektedir. Küme ayracı, content parametresine geçirilen lambda ifadesini tanımlar.

Aslında, iletmekte olduğunuz tek parametre sondaki lambda ise (yani, son parametre bir lambda ise ve başka hiçbir parametre iletmiyorsanız) parantezleri tamamen çıkarabilirsiniz. Örneğin, Column öğesine bir değiştirici aktarmanız 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 ve bu alt öğeler işlev çağrısından sonra süslü ayraç içinde belirtilir.

Dürbünler ve alıcılar

Bazı yöntem ve özellikler yalnızca belirli bir kapsamda kullanılabilir. Sınırlı kapsam, gereken yerlerde işlevselliği sunmanıza ve uygun olmadığı yerlerde yanlışlıkla bu işlevin kullanılmasını önlemenize olanak tanır.

Compose'da kullanılan bir örneği düşünün. Row düzenini composable olarak adlandırdığınızda, içeriğinizle ilgili lambda otomatik olarak bir RowScope içinde çağrılır. Bu, Row ürününün yalnızca bir Row içinde geçerli olan işlevleri göstermesini sağlar. Aşağıdaki örnekte, Row işlevinin 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ın parametre bildirimine göre başka bir yerde tanımlanmış özelliklere ve işlevlere erişimi vardır:

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ı ile işlev değişmez değerlerini inceleyin.

Yetki verilmiş mülkler

Kotlin, yetki verilmiş mülkleri destekler. Bu özellikler alan 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 kod, 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 durum destekli mülklerle çalışırken yararlıdır:

var showDialog by remember { mutableStateOf(false) }

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

Veri sınıflarını yok etme

Bir veri sınıfı tanımlarsanız yapılandırma beyanı ile verilere 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ürden 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ı birçok başka faydalı 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. Veri sınıfları belgelerinde daha fazla bilgi bulabilirsiniz.

Tekil nesneler

Kotlin, her zaman bir ve yalnızca bir örneğe sahip olan sınıfların (singleton) beyan edilmesini kolaylaştırır. Bu tekilleştirmeler, object anahtar kelimesi ile tanımlanır. Oluşturma işleminde genellikle bu tür nesneler kullanılır. Örneğin, MaterialTheme bir tekil nesne olarak tanımlanır; MaterialTheme.colors, shapes ve typography özelliklerinin tümü mevcut temanın değerlerini içerir.

Tür için güvenli derleyiciler ve DSL'ler

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

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ıyla işlev değişmez değerlerini kullanarak tür açısından güvenli derleyicileri garanti eder. Örnek olarak Canvas composable'ı ele alırsak onDraw: DrawScope.() -> Unit alıcısı olarak DrawScope olan bir işlevi parametre olarak 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)
        }
    }
}

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

Kotlin eş yordamları

Eş yordamlar, Kotlin'de dil düzeyinde eşzamansız programlama desteği sunar. Ortaklar, ileti dizilerini engellemeden yürütmeyi askıya alabilir. Duyarlı bir kullanıcı arayüzü, yapısı gereği eşzamansızdır ve Jetpack Compose bu sorunu geri çağırmalar kullanmak yerine API düzeyinde eş yordamları benimseyerek çözer.

Jetpack Compose, kullanıcı arayüzü katmanında eş yordamların kullanılmasını güvenli hale getiren API'ler sunar. rememberCoroutineScope işlevi, etkinlik işleyicilerde ve Compose askıya alma API'lerinde eş yordamlar oluşturabileceğiniz bir CoroutineScope döndürür. ScrollState animateScrollTo API'sini kullanarak 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ıralı yürütür. Askıya alma işlevini çağıran çalışan bir eş arı, 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. Ö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ın oluşturulması gerekir. Yukarıdaki örnekte, ekranın üst kısmına kaydırma işlemini paralel yapmak ve viewModel konumundan veri yüklemek için iki eş yordam gereklidir.

// 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şzamanlılar, eşzamansız API'lerin birleştirilmesini 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)
        )
    }

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