Jetpack Compose için Kotlin

Jetpack Compose, Kotlin'e göre tasarlanmıştır. 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ı güçlü yönlerini kaçırabilir ve idiomatik olarak yazılmış Kotlin kodunu anlamakta zorluk çekebilirsiniz. Kotlin'in tarzını daha iyi öğrenmek bu tür hatalardan kaçınmanıza yardımcı olabilir.

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

Kotlin işlevi yazarken, işlev bağımsız değişkenleri için varsayılan değerler belirtebilirsiniz. Bu değerler, çağıran bu değerleri açıkça iletmezse kullanılır. 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 tarafın uzunluğunu belirten tek bir zorunlu parametresi (sideLength) olabilir. thickness, edgeColor gibi çeşitli isteğe bağlı parametreleri olabilir. Çağırıcı 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 yazmak zorunda kalmanızın yanı sıra kodunuzun okunabilirliğini de artırır. Arayan bir bağımsız değişken için değer belirtmezse varsayılan değeri kullanmak istediğini gösterir. Ayrıca, adlandırılmış parametreler neler olduğunu daha kolay görmenizi sağlar. Kodu incelediğinizde 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 açıklayan bir koddur:

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

Çoğu Compose kitaplığı varsayılan bağımsız değişkenler kullanır. Yazdığınız birleştirilebilir işlevler için de aynısını yapmanız önerilir. Bu uygulama, bileşenlerinizi ö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 okunması kolay olmasının yanı sıra kendi kendini açıklayan bir koddur. Yalnızca text parametresini belirterek diğer tüm parametreler için varsayılan değerleri kullanmak istediğinizi belirtirsiniz. Buna karşılık, ikinci snippet, diğer parametrelerin değerlerini açıkça ayarlamak istediğinizi ima eder. Ancak belirlediğiniz değerler işlevin varsayılan değerleridir.

Yüksek düzey işlevler ve lambda ifadeleri

Kotlin, parametre olarak diğer işlevleri alan üst düzey işlevleri destekler. Oluşturma, bu yaklaşımı temel alır. Örneğin, Button birleşebilir 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 dereceli işlevler, doğal olarak bir işlev olarak değerlendirilen lambda ifadeleriyle eşlenir. İşleve yalnızca bir kez ihtiyacınız varsa işlevi daha yüksek düzeyli işleve iletmek için başka bir yerde tanımlamanız gerekmez. Bunun yerine, işlevi doğrudan lambda ifadesiyle tanımlayabilirsiniz. Önceki örnekte, myClickFunction()'ün başka bir yerde tanımlandığı varsayılmaktadır. Ancak burada yalnızca bu işlevi kullanıyorsanız işlevi bir 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 daha yüksek düzeyli işlevleri çağırmak için özel bir söz dizimi sunar. Bu parametre olarak bir lambda ifadesi iletmek istiyorsanız sonraki lambda söz dizimini kullanabilirsiniz. Lambda ifadesini parantez içine koymak yerine, ifadeyi parantezin sonuna koyarsınız. Bu, Oluştur'da sık karşılaşılan bir durumdur. Bu nedenle, kodun nasıl göründüğünü bilmeniz gerekir.

Örneğin, tüm düzenler için son parametre (ör. Column() birleşik işlevi) content'dır. Bu işlev, alt kullanıcı arayüzü öğelerini yayınlar. Üç metin öğesi içeren bir sütun oluşturmak istediğinizi ve bazı biçimlendirmeler uygulamanız gerektiğini varsayalım. Bu kod işe yarar ancak çok zahmetli:

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

content parametresi işlev imzasında son parametre olduğundan ve değerini lambda ifadesi olarak ilettiğimizde parantezlerden çıkarabiliriz:

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

İki örnek de aynı anlama sahiptir. Köşeli parantezler, content parametresine iletilen lambda ifadesini tanımlar.

Aslında, ilettiğiniz tek parametre bu son 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, özellikle Column gibi düzen öğeleri için Oluştur'da 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öşeli 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şlevselliği gerektiği yerde sunmanıza ve uygun olmadığı yerlerde yanlışlıkla kullanmanıza engel olur.

Oluştur'da kullanılan bir örneği ele alalım. Row düzen bileşenini çağırdığınızda içeriğiniz lambda, RowScope içinde otomatik olarak çağrılır. Bu, Row'ün yalnızca Row içinde geçerli olan işlevleri göstermesini sağlar. Aşağıdaki örnekte, Row'ü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 lambdaları kabul eder. Bu lambdalar, parametre beyanına 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ında alıcı içeren işlev literalleri bölümüne bakın.

Yetki verilmiş tesisler

Kotlin, devredilmiş özellikleri destekler. Bu mülkler alan gibi çağrılır ancak değerleri bir ifadenin değerlendirilmesiyle dinamik olarak belirlenir. Bu mülkleri by söz dizimini kullanmalarından tanıyabilirsiniz:

class DelegatingClass {
    var name: String by nameGetterFunction()

    // ...
}

Diğer kod, mülküne şu 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 yetki verilmiş mülkler, özellikle devlet 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ı yapılandırma

Bir veri sınıfı tanımlarsanız yapı bozma 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ü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 Oluştur 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ı 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 bilgi için veri sınıfları dokümanlarını inceleyin.

Tekil nesneler

Kotlin, her zaman tek bir örneği olan sınıflar olan tekil nesneleri tanımlamayı kolaylaştırır. Bu tekil türler object anahtar kelimesiyle tanımlanır. Oluşturma işlemi genellikle bu tür nesnelerden yararlanı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.

Tür açısından güvenli oluşturucular ve DSL'ler

Kotlin, tür açısından 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ıyla işlev literalleri kullanarak tür açısından güvenli oluşturucular sağlar. Canvas bileşenini örnek olarak alırsak bu bileşen, onDraw: DrawScope.() -> Unit alıcı olarak DrawScope içeren bir işlevi parametre olarak alır. 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)
        }
    }
}

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

Kotlin eş yordamları

Koşullu yan rutinler, Kotlin'de dil düzeyinde eşzamansız programlama desteği sunar. Görev akışları, iş parçacıklarını engellemeden yürütmeyi askıya alabilir. Duyarlı kullanıcı arayüzü doğası gereği asenkrondur. Jetpack Compose, geri çağırma işlevleri kullanmak yerine API düzeyinde coroutine'leri benimseyerek bu sorunu çözer.

Jetpack Compose, kullanıcı arayüzü katmanında iş parçacıklarını güvenli bir şekilde kullanmanızı sağlayan API'ler sunar. rememberCoroutineScope işlevi, etkinlik işleyicilerinde iş parçacığı oluşturabileceğiniz ve Compose askıya alma API'lerini çağırabileceğ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()
        }
    }
) { /* ... */ }

Görev akışları, kod bloğunu varsayılan olarak sıralı olarak 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ütmesini askıya alır. Bu durum, askıya alma işlevi yürütmeyi farklı bir CoroutineDispatcher'ye taşısa bile geçerlidir. Önceki örnekte, animateScrollTo işlevi döndürülene kadar loadData yürütülmez.

Kodu eşzamanlı olarak yürütmek için yeni iş parçacığı ortak işlemleri oluşturulmalıdır. Yukarıdaki örnekte, ekranın üst kısmına kaydırma ve viewModel kaynağından veri yükleme işlemlerini paralelleştirmek için iki coroutine 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()
        }
    }
) { /* ... */ }

Koşullu yan rutinler, eşzamansız API'leri birleştirmeyi kolaylaştırır. Aşağıdaki örnekte, kullanıcı ekrana dokunduğunda bir öğenin konumunu animasyonlu hale getirmek için pointerInput değiştiriciyi 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ı kılavuzuna göz atın.