Android ile yaygın Kotlin kalıplarını kullanma

Bu konu, Android için geliştirme yaparken Kotlin dilinin en faydalı yönlerinden bazılarına odaklanmaktadır.

Parçalarla çalışma

Aşağıdaki bölümlerde Kotlin'in en iyi özelliklerinden bazılarını öne çıkarmak için Fragment örnekleri kullanılmaktadır.

Devralındı

class anahtar kelimesini kullanarak Kotlin'de bir sınıf bildirebilirsiniz. Aşağıdaki örnekte LoginFragment, Fragment sınıfının bir alt sınıfıdır. Devralınması, : operatörünü alt sınıf ile üst sınıfı arasında kullanarak belirtebilirsiniz:

class LoginFragment : Fragment()

Bu sınıf beyanında LoginFragment, üst sınıfının oluşturucusunu (Fragment) çağırmaktan sorumludur.

LoginFragment içinde, Fragment içindeki durum değişikliklerine yanıt vermek için bir dizi yaşam döngüsü geri çağırmasını geçersiz kılabilirsiniz. Bir işlevi geçersiz kılmak için aşağıdaki örnekte gösterildiği gibi override anahtar kelimesini kullanın:

override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
): View? {
    return inflater.inflate(R.layout.login_fragment, container, false)
}

Üst sınıftaki bir işleve başvuruda bulunmak için aşağıdaki örnekte gösterildiği gibi super anahtar kelimesini kullanın:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
}

Boş değer atanabilirlik ve başlatma

Önceki örneklerde, geçersiz kılınan yöntemlerdeki bazı parametrelerde soru işareti ? ile soneklenen türler vardır. Bu, bu parametreler için iletilen bağımsız değişkenlerin boş olabileceği anlamına gelir. Boş değerlerini güvenli bir şekilde işlediğinizden emin olun.

Kotlin'de, bir nesneyi tanımlarken nesnenin özelliklerini başlatmanız gerekir. Bu, bir sınıfın bir örneğini elde ettiğinizde, erişilebilir özelliklerinden herhangi birine hemen referans verebileceğiniz anlamına gelir. Ancak Fragment içindeki View nesneleri, Fragment#onCreateView çağrılana kadar şişirmeye hazır değildir. Bu nedenle, View için özellik başlatmayı ertelemenin bir yolunu bulmanız gerekir.

lateinit, mülk başlatmayı ertelemenize olanak tanır. lateinit kullanırken mülkünüzü en kısa sürede başlatmanız gerekir.

Aşağıdaki örnek, onViewCreated içinde View nesneleri atamak için lateinit kullanımını gösterir:

class LoginFragment : Fragment() {

    private lateinit var usernameEditText: EditText
    private lateinit var passwordEditText: EditText
    private lateinit var loginButton: Button
    private lateinit var statusTextView: TextView

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        usernameEditText = view.findViewById(R.id.username_edit_text)
        passwordEditText = view.findViewById(R.id.password_edit_text)
        loginButton = view.findViewById(R.id.login_button)
        statusTextView = view.findViewById(R.id.status_text_view)
    }

    ...
}

SAM dönüşümü

OnClickListener arayüzünü uygulayarak Android'de tıklama etkinliklerini dinleyebilirsiniz. Button nesneleri, OnClickListener uygulamasını üstlenen setOnClickListener() işlevi içerir.

OnClickListener, uygulamanız gereken tek bir soyut yönteme (onClick()) sahip. setOnClickListener(), OnClickListener öğesini her zaman bağımsız değişken olarak aldığından ve OnClickListener daima aynı soyut yönteme sahip olduğundan bu uygulama, Kotlin'de anonim bir işlev kullanılarak temsil edilebilir. Bu işlem, Tekli Soyut Yöntem dönüşümü veya SAM dönüşümü olarak bilinir.

SAM dönüşümü, kodunuzu önemli ölçüde daha temiz hale getirebilir. Aşağıdaki örnekte, Button için OnClickListener uygulamak üzere SAM dönüşümünün nasıl kullanılacağı gösterilmektedir:

loginButton.setOnClickListener {
    val authSuccessful: Boolean = viewModel.authenticate(
            usernameEditText.text.toString(),
            passwordEditText.text.toString()
    )
    if (authSuccessful) {
        // Navigate to next screen
    } else {
        statusTextView.text = requireContext().getString(R.string.auth_failed)
    }
}

setOnClickListener() işlevine iletilen anonim işlev içindeki kod, bir kullanıcı loginButton öğesini tıkladığında yürütülür.

Tamamlayıcı nesneler

Tamamlayıcı nesneler, kavramsal olarak bir türe bağlı olan, ancak belirli bir nesneye bağlı olmayan değişkenleri veya işlevleri tanımlamak için bir mekanizma sunar. Tamamlayıcı nesneler, değişkenler ve yöntemler için Java'nın static anahtar kelimesini kullanmaya benzer.

Aşağıdaki örnekte TAG, String sabitidir. Her LoginFragment örneği için benzersiz bir String örneğine ihtiyacınız yoktur. Bu nedenle, bunu bir tamamlayıcı nesnede tanımlamanız gerekir:

class LoginFragment : Fragment() {

    ...

    companion object {
        private const val TAG = "LoginFragment"
    }
}

TAG öğesini dosyanın en üst düzeyinde tanımlayabilirsiniz ancak dosyada, üst düzeyde de tanımlanmış çok sayıda değişken, işlev ve sınıf bulunabilir. Tamamlayıcı nesneler, söz konusu sınıfın belirli bir örneğine başvurmadan değişkenlerin, işlevlerin ve sınıf tanımının bağlanmasına yardımcı olur.

Mülk yetkisi

Özellikleri başlatırken Android'in daha yaygın kalıplarından bazılarını tekrarlayabilirsiniz (örneğin, bir Fragment içindeki ViewModel öğesine erişme). Fazla sayıda yinelenen koddan kaçınmak için Kotlin'in özellik yetkisi söz dizimini kullanabilirsiniz.

private val viewModel: LoginViewModel by viewModels()

Mülk yetkilendirme, uygulamanız genelinde tekrar kullanabileceğiniz yaygın bir uygulama sağlar. Android KTX, size mülk için yetki verilmiş bazı kullanıcılar sağlar. Örneğin viewModels, kapsamı geçerli Fragment öğesine ayarlanmış bir ViewModel getirir.

Mülk yetkisi, yansımayı kullanır ve bu da bir miktar performans ek yüküne neden olur. Ödün verme, geliştirme süresinden tasarruf sağlayan kısa ve öz bir söz dizimidir.

Boş değer atanabilirliği

Kotlin, uygulamanızın genelinde tür güvenliğini koruyan katı null kullanılabilirlik kuralları sağlar. Kotlin'de nesne referansları varsayılan olarak null değerler içeremez. Bir değişkene null değer atamak için temel türün sonuna ? ekleyerek nullable değişken türünü bildirmeniz gerekir.

Örneğin, aşağıdaki ifade Kotlin'de yasa dışıdır. name, String türündedir ve boş bırakılamaz:

val name: String = null

Boş değere izin vermek için, aşağıdaki örnekte gösterildiği gibi null özellikli bir String türü (String?) kullanmanız gerekir:

val name: String? = null

Birlikte çalışabilirlik

Kotlin'in katı kuralları, kodunuzu daha güvenli ve özlü hale getirir. Bu kurallar, uygulamanızın kilitlenmesine neden olacak NullPointerException olasılığını azaltır. Ayrıca, kodunuzda yapmanız gereken boş denetimlerin sayısını da azaltır.

Çoğu Android API, Java programlama dilinde yazıldığından, bir Android uygulaması yazarken Kotlin dışı kodları da çağırmanız gerekir.

Boş değer atanabilirliği, Java ve Kotlin'in davranış açısından farklılık gösterdiği önemli bir alandır. Java, null değer söz dizimiyle daha az katıdır.

Örnek olarak, Account sınıfının name adlı String özelliği de dahil olmak üzere birkaç özelliği vardır. Java, null ile ilgili Kotlin kurallarına sahip değildir. Bunun yerine, null değer atayıp atayamayacağınızı açıkça belirtmek için isteğe bağlı null değer ek açıklamalarını kullanır.

Android çerçevesi birincil olarak Java'da yazıldığından nullability ek açıklamaları olmadan API'leri çağırırken bu senaryoyla karşılaşabilirsiniz.

Platform türleri

Java Account sınıfında tanımlanmış ek açıklamalı bir name üyesine referans vermek için Kotlin kullanırsanız derleyici, String öğesinin Kotlin'deki bir String ile mi yoksa String? ile mi eşlendiğini bilemez. Bu belirsizlik, bir platform türü (String!) ile temsil edilir.

String!, Kotlin derleyici için özel bir anlama sahip değildir. String!, String veya String? öğesini temsil edebilir. Derleyici, her iki türde değer atamanıza olanak tanır. Türü String olarak temsil eder ve boş değer atarsanız bir NullPointerException atama riskiyle karşı karşıya olduğunuzu unutmayın.

Bu sorunu çözmek için Java'da her kod yazdığınızda null değer ek açıklamalarını kullanmanız gerekir. Bu ek açıklamalar hem Java hem de Kotlin geliştiricilerine yardımcı olur.

Örneğin, Java'da tanımlandığı şekliyle Account sınıfı şu şekildedir:

public class Account implements Parcelable {
    public final String name;
    public final String type;
    private final @Nullable String accessId;

    ...
}

Üye değişkenlerinden biri olan accessId, boş değer alabileceğini belirten @Nullable ek açıklamasına sahiptir. Bu durumda Kotlin, accessId öğesini String? olarak değerlendirir.

Bir değişkenin hiçbir zaman boş olamayacağını belirtmek için @NonNull ek açıklamasını kullanın:

public class Account implements Parcelable {
    public final @NonNull String name;
    ...
}

Bu senaryoda name, Kotlin'de null olmayan bir String olarak kabul edilir.

Tüm yeni Android API'lerinde ve mevcut birçok Android API'sinde boş değer atanabilirlik ek açıklamaları yer almaktadır. Birçok Java kitaplığı, hem Kotlin hem de Java geliştiricilerini daha iyi desteklemek için null değer ek açıklamaları eklemiştir.

Boş değer kullanılabilirliğini işleme

Bir Java türünden emin değilseniz boş değer olarak kabul etmelisiniz. Örneğin, Account sınıfının name üyesine ek açıklama eklenmemiştir. Bu nedenle, bunun boş değer atan bir String olduğunu varsaymanız gerekir.

name değerini, başında veya sonunda boşluk olmayacak şekilde kırpmak isterseniz Kotlin'in trim işlevini kullanabilirsiniz. String? öğesini birkaç farklı şekilde güvenle kırpabilirsiniz. Bu yöntemlerden biri, aşağıdaki örnekte gösterildiği gibi not-null onaylama operatörünü (!!) kullanmaktır:

val account = Account("name", "type")
val accountName = account.name!!.trim()

!! operatörü, sol tarafındaki her şeyi null olmayan değer olarak değerlendirir. Bu nedenle bu durumda name, boş olmayan bir String olarak ele alınır. Soldaki ifadenin sonucu null ise uygulamanız NullPointerException döndürür. Bu operatör hızlı ve kolay bir işlemdir ancak kodunuza NullPointerException örneklerini yeniden ekleyebileceği için dikkatli kullanılmalıdır.

Aşağıdaki örnekte gösterildiği gibi güvenli arama operatörünü (?.) kullanmak daha güvenli bir seçenektir:

val account = Account("name", "type")
val accountName = account.name?.trim()

Güvenli arama operatörünü kullandığınızda name null değilse name?.trim() işlevinin sonucu, başında veya sonunda boşluk olmayan bir ad değeridir. name null ise name?.trim() işlevinin sonucu null olur. Bu durum, uygulamanızın bu ifadeyi yürütürken hiçbir zaman NullPointerException işlemi gönderemeyeceği anlamına gelir.

Güvenli arama operatörü sizi olası bir NullPointerException işleminden kurtarsa da sonraki ifadeye boş bir değer iletir. Bunun yerine, aşağıdaki örnekte gösterildiği gibi Elvis operatörü (?:) kullanarak null değerleri hemen işleyebilirsiniz:

val account = Account("name", "type")
val accountName = account.name?.trim() ?: "Default name"

Elvis operatörünün sol tarafındaki ifadenin sonucu null ise sağ taraftaki değer accountName değerine atanır. Bu teknik, aksi takdirde boş olacak bir varsayılan değer sağlamak için yararlıdır.

Aşağıdaki örnekte gösterildiği gibi, Elvis operatörünü bir işlevden erken geri dönmek için de kullanabilirsiniz:

fun validateAccount(account: Account?) {
    val accountName = account?.name?.trim() ?: "Default name"

    // account cannot be null beyond this point
    account ?: return

    ...
}

Android API değişiklikleri

Android API'leri giderek daha Kotlin dostu oluyor. Android'in AppCompatActivity ve Fragment gibi en yaygın API'lerinin çoğu boş değer atanabilirlik ek açıklamaları içerir ve Fragment#getContext gibi bazı çağrıların Kotlin dostu daha alternatifleri vardır.

Örneğin, bir Fragment öğesinin Context öğesine erişim neredeyse her zaman null değildir. Bunun nedeni, Fragment içinde yaptığınız çağrıların çoğunun Fragment öğesi bir Activity'ye (Context alt sınıfı) bağlı olduğu halde gerçekleşmesidir. Bununla birlikte, Fragment öğesinin bir Activity öğesine eklenmediği senaryolar olduğu için Fragment#getContext her zaman null olmayan bir değer döndürmez. Bu nedenle, Fragment#getContext dönüş türü null olabilir.

Fragment#getContext öğesinden döndürülen Context null özellikli olduğundan (ve ek açıklama @Nullable olarak eklendiğinden) Kotlin kodunuzda Context? olarak değerlendirmeniz gerekir. Bu, özelliklerine ve işlevlerine erişmeden önce boş değer sorununu ele almak için daha önce bahsedilen operatörlerden birinin uygulanması anlamına gelir. Bu senaryoların bazılarında Android, bu kolaylığı sağlayan alternatif API'ler içerir. Örneğin Fragment#requireContext, null olmayan bir Context döndürür ve bir Context null olduğunda çağrılırsa bir IllegalStateException döndürür. Bu şekilde, güvenli arama operatörlerine veya geçici çözümlere gerek kalmadan sonuçta ortaya çıkan Context sonucunu null olmayan değer olarak değerlendirebilirsiniz.

Mülk başlatma

Kotlin'deki özellikler varsayılan olarak başlatılmaz. Bunlar, çevreleyen sınıf başlatıldığında başlatılmalıdır.

Özellikleri birkaç farklı şekilde başlatabilirsiniz. Aşağıdaki örnek, sınıf bildiriminde bir index değişkeninin nasıl değer atanarak başlatılacağını gösterir:

class LoginFragment : Fragment() {
    val index: Int = 12
}

Bu başlatma, bir başlatıcı bloğunda da tanımlanabilir:

class LoginFragment : Fragment() {
    val index: Int

    init {
        index = 12
    }
}

Yukarıdaki örneklerde index, bir LoginFragment oluşturulduğunda başlatılır.

Bununla birlikte, nesne oluşturma sırasında başlatılamayan bazı özellikleriniz olabilir. Örneğin, Fragment içinden bir View öğesine referans vermek isteyebilirsiniz. Bu, düzenin önce genişletilmesi gerektiği anlamına gelir. Fragment oluşturulurken enflasyon meydana gelmez. Bunun yerine, Fragment#onCreateView çağrılırken ses şişirilmiş olur.

Bu senaryoyu ele almanın bir yolu, görünümü boş değerli olarak bildirmek ve görünümü en kısa sürede aşağıdaki örnekte gösterildiği gibi başlatmaktır:

class LoginFragment : Fragment() {
    private var statusTextView: TextView? = null

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)

            statusTextView = view.findViewById(R.id.status_text_view)
            statusTextView?.setText(R.string.auth_failed)
    }
}

Bu beklendiği gibi çalışsa da, artık her başvuruda bulunduğunuzda View öğesinin null değerini yönetmeniz gerekir. Daha iyi bir çözüm, aşağıdaki örnekte gösterildiği gibi View başlatma için lateinit kullanılmasıdır:

class LoginFragment : Fragment() {
    private lateinit var statusTextView: TextView

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)

            statusTextView = view.findViewById(R.id.status_text_view)
            statusTextView.setText(R.string.auth_failed)
    }
}

lateinit anahtar kelimesi, bir nesne oluşturulduğunda bir özelliği başlatmaktan kaçınmanızı sağlar. Mülkünüze başlatılmadan önce referans verilirse Kotlin bir UninitializedPropertyAccessException bildirir. Bu nedenle, mülkünüzü en kısa sürede başlattığınızdan emin olun.