Kotlin Multiplatform’da ‘expect/actual’ Yaklaşımı: Varsayılan Tercih Neden Yanlış Olabilir?
Kotlin Multiplatform (KMP) ile tek bir kod tabanından birden fazla platforma uygulama geliştirmek heyecan verici bir vaat sunar. Bu mimarinin temel taşlarından biri olan expect/actual mekanizması, platforma özgü kodları yönetmek için güçlü bir çözüm gibi görünse de, her zaman varsayılan tercih olmaması gerektiğini savunuyoruz. Bu makalede, expect/actual yaklaşımının inceliklerini, potansiyel tuzaklarını ve alternatif çözüm yollarını derinlemesine inceleyeceğiz.
Kotlin Multiplatform Nedir ve ‘expect/actual’ Mekanizması Nasıl Çalışır?
Kotlin Multiplatform (KMP), JetBrains tarafından geliştirilen ve geliştiricilerin Android, iOS, Web (JavaScript), masaüstü (JVM) ve hatta gömülü sistemler gibi farklı platformlarda kod paylaşmasına olanak tanıyan yenilikçi bir teknolojidir. KMP’nin temel amacı, iş mantığı, veri modelleri, ağ katmanı gibi kritik uygulama bileşenlerini tek bir ortak (common) modülde yazarak geliştirme sürecini hızlandırmak ve tutarlılığı artırmaktır. Ancak, her platformun kendine özgü API’leri (uygulama programlama arayüzleri) ve yetenekleri olduğundan, bu ortak kodun platforma özel özelliklerle etkileşime girmesi gerektiği durumlar ortaya çıkar. İşte tam bu noktada expect/actual mekanizması devreye girer.
expect/actual, KMP’nin platforma özgü bağımlılıkları yönetmek için sunduğu bir dildir. Ortak modülde, bir işlevin, sınıfın, arayüzün veya tür takma adının (type alias) belirli bir platformda nasıl davranması gerektiğini “beklersiniz” (expect anahtar kelimesiyle tanımlarsınız). Bu beklenti, ilgili platforma özgü modüllerde “gerçekleştirilir” (actual anahtar kelimesiyle implemente edilir). Bu sayede, ortak kodunuz platforma özgü detayları bilmek zorunda kalmadan soyutlamalar üzerinden çalışabilir. Örneğin, bir cihazın platform adını almak istediğinizde, ortak modülde sadece bir fonksiyon beklersiniz ve her platform kendi adını döndürecek şekilde bu fonksiyonu implemente eder.
Bu mekanizmanın temel faydası, platforma özgü kodun ortak koddan izole edilmesini sağlamasıdır. Bu izolasyon, ortak kodun platformdan bağımsız olarak test edilmesine ve daha temiz bir mimariye sahip olmasına yardımcı olur. Geliştiriciler, platforma özgü API’leri doğrudan ortak kodda çağırmak yerine, expect/actual çiftleri aracılığıyla bir soyutlama katmanı oluştururlar. Bu, özellikle dosya sistemi erişimi, kullanıcı arayüzü (UI) bileşenleri, platforma özgü sensör erişimi veya ağ istekleri gibi alanlarda büyük kolaylık sağlar. Ancak, bu kolaylığın her zaman en iyi çözüm olup olmadığını sorgulamak, daha sürdürülebilir ve esnek KMP uygulamaları geliştirmek için kritik öneme sahiptir.
Bir örnekle açıklayalım. Uygulamanızda platforma özgü bir loglama (kayıt tutma) mekanizması kullanmak istediğinizi varsayalım. Ortak kodunuzda bir mesajı loglamak için bir fonksiyona ihtiyacınız var. Bu durumda expect/actual nasıl kullanılır?
// commonMain/kotlin/com/example/mylibrary/PlatformLogger.kt
package com.example.mylibrary
expect fun logMessage(tag: String, message: String)
// commonMain/kotlin/com/example/mylibrary/MyBusinessLogic.kt
package com.example.mylibrary
fun doSomeWork() {
logMessage("MyFeature", "İşlem başlatılıyor...")
// ... iş mantığı ...
logMessage("MyFeature", "İşlem tamamlandı.")
}
Şimdi bu beklentiyi platforma özgü modüllerde gerçekleştirmemiz gerekiyor:
// androidMain/kotlin/com/example/mylibrary/PlatformLoggerActual.kt
package com.example.mylibrary
import android.util.Log
actual fun logMessage(tag: String, message: String) {
Log.d(tag, message)
}
// iosMain/kotlin/com/example/mylibrary/PlatformLoggerActual.kt
package com.example.mylibrary
import platform.Foundation.NSLog
actual fun logMessage(tag: String, message: String) {
NSLog("[%@] %@", tag, message)
}
Bu basit örnekte, ortak kod olan doSomeWork() fonksiyonu, hangi platformda çalıştığını bilmeden logMessage fonksiyonunu çağırabilir. Derleme zamanında, Kotlin derleyicisi her platform için uygun actual implementasyonunu bağlar. Bu, KMP’nin temel gücünü ve platformlar arası kod paylaşımını nasıl mümkün kıldığını gösteren güzel bir örnektir. Ancak, bu basitliğin ardında yatan potansiyel karmaşıklıkları ve “varsayılan” olarak kullanılmasının getirebileceği sorunları anlamak, daha karmaşık projeler için hayati önem taşır.
‘expect/actual’ Varsayılan Tercih Olduğunda Karşılaşılan Zorluklar ve Tuzaklar
expect/actual mekanizması, KMP’nin platformlar arası kod paylaşımını kolaylaştıran güçlü bir araç olsa da, her platforma özgü ihtiyaç için otomatik olarak bu yaklaşımı benimsemek, zamanla projenizin karmaşıklığını ve bakım maliyetini artırabilir. Bu durum, özellikle projenin ölçeği büyüdükçe veya yeni platformlar desteklenmeye başlandıkça daha belirgin hale gelir. İşte expect/actual‘ın varsayılan tercih olarak kullanılmasının getirdiği başlıca zorluklar ve tuzaklar:
Tekrarlayan Kod (Boilerplate) ve Bakım Yükü
Her platforma özgü küçük bir farklılık için bile bir expect bildirimi ve ardından her desteklenen platform için ayrı bir actual implementasyonu yazmak, hızla ciddi bir tekrarlayan kod (boilerplate) yığını oluşturabilir. Örneğin, uygulamanızda platforma özgü birkaç farklı depolama mekanizması, ağ konfigürasyonu veya sensör erişimi varsa, her biri için ayrı ayrı expect/actual çiftleri oluşturmanız gerekecektir. Bu, kod tabanının büyümesine, okunabilirliğinin azalmasına ve özellikle yeni bir platform eklendiğinde veya mevcut bir platformda değişiklik yapıldığında bakım yükünün artmasına neden olur. Her değişiklik, tüm actual implementasyonlarını gözden geçirmeyi gerektirebilir, bu da hata yapma olasılığını artırır ve geliştirme hızını yavaşlatır.
Sıkı Bağlantı (Tight Coupling) ve Esneklik Kaybı
Ortak kodunuzda çok fazla expect bildirimi kullanmak, ortak modülünüz ile platforma özgü modüller arasında sıkı bir bağlantı (tight coupling) oluşturabilir. Ortak kod, platforma özgü detaylardan tamamen soyutlanmış gibi görünse de, aslında her bir expect bildirimi, belirli bir actual implementasyonunun varlığını “bekler”. Bu durum, bir platformu kaldırmak veya yeni bir platform eklemek istediğinizde, tüm expect bildirilerini ve bunların bağımlılıklarını gözden geçirmenizi gerektirir. Örneğin, uygulamanız başlangıçta Android ve iOS için tasarlanmış ve her platform için ayrı ayrı expect/actual kullanarak bir dosya indirme yöneticisi implemente ettiğinizi düşünün. Sonradan bir web (JavaScript) sürümü eklemeye karar verdiğinizde, mevcut tüm expect bildirimleri için bir JS actual implementasyonu sağlamak zorunda kalırsınız. Bu, projenin mimarisinin esnekliğini azaltır ve gelecekteki değişikliklere karşı dirençli hale getirir.
Test Edilebilirlik Sorunları
expect/actual mekanizması, ortak kodun test edilebilirliğini bir miktar zorlaştırabilir. Ortak kodunuzdaki bir expect fonksiyonunu doğrudan test etmek istediğinizde, bu fonksiyonun bir actual implementasyonuna sahip olması gerekir. Bu, birim testleri yazarken sahte (mock) veya taklit (fake) implementasyonlar sağlamanız gerektiği anlamına gelir. Her bir test ortamı (JVM için JUnit, iOS için XCTest vb.) için ayrı ayrı sahte actual implementasyonları oluşturmak, test kodunun karmaşıklığını artırır ve test sürecini uzatır. Bağımlılık enjeksiyonu (Dependency Injection – DI) gibi yaklaşımlar, bu tür senaryolarda test edilebilirlik açısından daha esnek çözümler sunabilir.
API Tasarımının Kısıtlanması
expect/actual kullanırken, ortak API’nizi genellikle tüm platformların desteklediği “en düşük ortak payda” (lowest common denominator) prensibine göre tasarlamak zorunda kalırsınız. Bu, belirli bir platformda bulunan zengin ve güçlü API’leri veya özellikleri kullanamayacağınız anlamına gelebilir. Örneğin, iOS’ta çok gelişmiş bir animasyon API’si varken, Android’deki karşılığı daha basitse, expect bildirimi her iki platformun da desteklediği temel animasyon özellikleriyle sınırlı kalmak zorunda kalır. Bu durum, platforma özgü deneyimi (native experience) tam olarak sunma yeteneğinizi kısıtlayabilir ve ortak kodunuzun potansiyelini düşürebilir.
Gerçek dünya senaryosu olarak, bir e-ticaret uygulamasının KMP ile geliştirildiğini ve başlangıçta sadece mobil (Android ve iOS) platformları hedeflediğini düşünelim. Uygulama, platforma özgü olarak bildirim gönderme, cihazın konumuna erişme ve kamera kullanma gibi özelliklere sahip. Geliştiriciler, her bir özellik için expect/actual çiftleri oluşturmuşlar. Bir yıl sonra, şirket uygulamanın bir masaüstü (Desktop) versiyonunu çıkarmaya karar veriyor. Bu durumda, geliştiricilerin mevcut tüm expect bildirimleri için masaüstü platformunda da actual implementasyonları sağlaması gerekiyor. Masaüstü ortamında konum veya kamera erişimi mobil kadar yaygın veya aynı API’lerle olmayabilir. Bu, ya masaüstü için sahte (dummy) implementasyonlar yazmayı ya da ortak kodda büyük değişiklikler yaparak bu özelliklerin isteğe bağlı hale getirilmesini gerektirecektir. Bu senaryo, expect/actual‘ın varsayılan olarak her yerde kullanılmasının uzun vadede ne kadar büyük bir bakım ve uyumluluk sorunu yaratabileceğini açıkça göstermektedir.
‘expect/actual’ Yerine Kullanılabilecek Alternatif Yaklaşımlar Nelerdir?
expect/actual mekanizması KMP’de belirli durumlar için vazgeçilmez olsa da, her platforma özgü ihtiyaç için varsayılan çözüm olarak kullanılması, yukarıda bahsedilen zorlukları beraberinde getirebilir. Neyse ki, daha esnek, bakımı kolay ve test edilebilir KMP uygulamaları geliştirmek için kullanabileceğimiz çeşitli alternatif yaklaşımlar mevcuttur. Bu alternatifler, özellikle iş mantığı veya soyutlama seviyesi yüksek olan durumlar için daha uygun olabilir.
Bağımlılık Enjeksiyonu (Dependency Injection – DI)
Bağımlılık enjeksiyonu, KMP projelerinde platforma özgü bağımlılıkları yönetmek için belki de en güçlü ve esnek yaklaşımlardan biridir. Bu yöntemde, ortak modülünüzde platforma özgü bir hizmetin veya bileşenin soyut bir tanımını (arayüz veya soyut sınıf) oluşturursunuz. Daha sonra, her platforma özgü modülde bu arayüzün somut bir implementasyonunu sağlarsınız. Ortak kodunuz, bu implementasyonları doğrudan çağırmak yerine, bir DI çerçevesi (örneğin Koin, Dagger/Hilt’in KMP versiyonları veya basit bir el ile DI çözümü) aracılığıyla veya bir fabrika (factory) deseniyle bu bağımlılıkları alır.
DI’nin temel avantajı, ortak kodunuzun platforma özgü implementasyonlardan tamamen ayrışmasını sağlamasıdır. Ortak kod sadece arayüzleri bilir ve implementasyonlar çalışma zamanında (runtime) sağlanır. Bu durum, test edilebilirliği büyük ölçüde artırır, çünkü testler sırasında arayüzlerin sahte (mock) implementasyonlarını kolayca enjekte edebilirsiniz. Ayrıca, yeni bir platform eklendiğinde, sadece yeni platforma özgü modülde arayüzün bir implementasyonunu sağlamanız yeterli olur, ortak kodda herhangi bir değişiklik yapmanıza gerek kalmaz.
Önceki loglama örneğini DI kullanarak yeniden düzenleyelim:
// commonMain/kotlin/com/example/mylibrary/ILogger.kt
package com.example.mylibrary
// Ortak modülde soyut bir arayüz tanımlıyoruz
interface ILogger {
fun log(tag: String, message: String)
}
// commonMain/kotlin/com/example/mylibrary/CommonServiceLocator.kt
package com.example.mylibrary
// Basit bir servis bulucu veya DI konteyneri
object CommonServiceLocator {
lateinit var logger: ILogger
private set // Dışarıdan sadece okunabilir
fun init(platformLogger: ILogger) {
if (::logger.isInitialized) {
println("Logger zaten başlatılmış!")
return
}
logger = platformLogger
}
}
// commonMain/kotlin/com/example/mylibrary/MyBusinessLogic.kt
package com.example.mylibrary
fun doSomeWorkWithDI() {
// Ortak kod, logger'ı CommonServiceLocator üzerinden alır
CommonServiceLocator.logger.log("MyFeatureDI", "DI ile işlem başlatılıyor...")
// ... iş mantığı ...
CommonServiceLocator.logger.log("MyFeatureDI", "DI ile işlem tamamlandı.")
}
Şimdi platforma özgü implementasyonları oluşturalım:
// androidMain/kotlin/com/example/mylibrary/AndroidLogger.kt
package com.example.mylibrary
import android.util.Log
// Android'e özgü logger implementasyonu
class AndroidLogger : ILogger {
override fun log(tag: String, message: String) {
Log.d(tag, message)
}
}
// Android uygulamasının başlangıç noktasında (örneğin Application sınıfında)
// CommonServiceLocator.init(AndroidLogger()) çağrılır.
// iosMain/kotlin/com/example/mylibrary/IOSLogger.kt
package com.example.mylibrary
import platform.Foundation.NSLog
// iOS'a özgü logger implementasyonu
class IOSLogger : ILogger {
override fun log(tag: String, message: String) {
NSLog("[%@] %@", tag, message)
}
}
// iOS uygulamasının başlangıç noktasında (örneğin AppDelegate'de)
// CommonServiceLocator.init(IOSLogger()) çağrılır.
Bu yaklaşım, ortak kodun platforma özgü detaylardan tamamen bağımsız olmasını sağlar. expect/actual‘da olduğu gibi derleme zamanı bir bağlama yerine, çalışma zamanında bir implementasyon sağlanır. Bu, özellikle büyük ve karmaşık projelerde modülerliği ve esnekliği artırır.
KMP’nin Kendi Modül Yapısı ve Çoklu Modüller
KMP, birden fazla ortak modül (örneğin common-core, common-data, common-ui-shared) ve platforma özgü modüller (android-app, ios-app) oluşturarak da platform farklılıklarını yönetme esnekliği sunar. Ortak modüller, genellikle iş mantığını ve veri modellerini içerirken, platforma özgü modüller UI veya platform API’leri ile etkileşimden sorumlu olur. Bu yapı, expect/actual kullanmadan da platforma özgü kodun izole edilmesine yardımcı olur. Örneğin, bir platforma özgü veritabanı erişimi için, ortak modülde sadece bir arayüz tanımlayıp, her platformda bu arayüzü implemente eden bir depolama modülü (android-db, ios-db) oluşturabilirsiniz. Ortak kodunuz, bu depolama modülüne bağımlılık enjeksiyonu aracılığıyla erişebilir.
Yapılandırma (Configuration) ve Özellik Bayrakları (Feature Flags)
Küçük ve önemsiz platform farklılıkları için expect/actual kullanmak yerine, basit yapılandırma dosyaları veya özellik bayrakları (feature flags) kullanmak daha pratik olabilir. Örneğin, bir API’nin temel URL’si platforma göre değişiyorsa, bunu expect/actual ile yapmak yerine, her platformun kendi build konfigürasyonunda (build.gradle.kts veya Info.plist gibi) tanımlayıp çalışma zamanında okuyabilirsiniz. Bu, daha az tekrarlayan kod ve daha az derleme zamanı bağımlılığı anlamına gelir.
Bu alternatif yaklaşımlar, expect/actual‘ın güçlü bir araç olmasına rağmen, her durumda en iyi çözüm olmadığını ve projenin ihtiyaçlarına göre farklı stratejilerin benimsenmesi gerektiğini göstermektedir. Doğru aracı doğru senaryoda kullanmak, KMP projelerinizin başarısı için kritik öneme sahiptir.
Ne Zaman ‘expect/actual’ Kullanmalıyız? Doğru Kullanım Senaryoları
Önceki bölümlerde expect/actual‘ın varsayılan tercih olarak kullanılmasının getirebileceği zorlukları ve alternatif yaklaşımları ele aldık. Ancak bu, expect/actual‘ın tamamen kötü bir araç olduğu anlamına gelmez. Aksine, belirli senaryolarda son derece güçlü ve hatta vazgeçilmez bir çözüm olabilir. Önemli olan, bu mekanizmayı ne zaman ve neden kullanacağımızı doğru bir şekilde anlamaktır. İşte expect/actual‘ın en uygun olduğu durumlar ve doğru kullanım senaryoları:
Gerçekten Platforma Özgü API’leri Sarmalama İhtiyacı Olduğunda
expect/actual‘ın en temel ve doğru kullanım alanı, ortak kodun doğrudan erişemediği, platforma özgü temel sistem API’lerini (Application Programming Interface – Uygulama Programlama Arayüzü) sarmalamaktır. Bu tür API’ler genellikle işletim sistemi (OS) seviyesinde işlevsellik sunar ve farklı platformlarda tamamen farklı implementasyonlara sahiptir. Örnek olarak:
- Dosya Sistemi Erişimi: Bir dosya oluşturma, okuma, yazma veya silme işlemleri her platformda farklı API’lerle yapılır (Android’de Java NIO, iOS’ta Foundation framework’ü gibi).
- Ağ İstekleri: Temel HTTP istemcileri veya soket işlemleri platforma göre değişebilir (her ne kadar
ktor-clientgibi kütüphaneler bunu soyutlasa da, altta yatan implementasyonlarexpect/actualkullanır). - Veritabanı Erişimi: SQLite veritabanlarına erişim veya diğer yerel depolama mekanizmaları.
- Platform Bilgileri: Cihazın modeli, işletim sistemi sürümü, dil ayarları gibi bilgilere erişim.
- Kriptografi ve Güvenli Depolama: Platformun sağladığı güvenli depolama anahtarları veya kriptografik algoritmalar.
Bu durumlarda, expect/actual kullanmak, ortak kodunuzun bu platforma özgü detaylardan soyutlanmasını sağlar ve her platformun kendi en verimli ve güvenli yolunu kullanmasına olanak tanır. Ortak kodunuz sadece “bir dosya kaydet” der, nasıl kaydedileceği platformun sorumluluğundadır.
Çok Küçük, İzole Edilmiş ve Nadiren Değişen Platform Farklılıkları
Eğer platformlar arasındaki fark çok küçük, izole edilmiş ve gelecekte değişme olasılığı düşükse, expect/actual basit ve etkili bir çözüm sunabilir. Örneğin, bir platforma özgü sabit bir değer (örneğin bir API anahtarı veya bir yapılandırma bayrağı) veya çok basit bir yardımcı fonksiyon (utility function) için kullanılabilir. Bu tür senaryolarda, bağımlılık enjeksiyonu gibi daha karmaşık bir mekanizma kurmak, gereksiz bir mühendislik yükü yaratabilir. expect/actual, bu tür basit durumlar için “just enough” (yeterli) soyutlama sağlar.
Derleme Zamanı (Compile-Time) Garantileri Gerektiğinde
expect/actual, derleme zamanı (compile-time) garantileri sağlar. Yani, ortak modülde beklenen her şeyin, derleme anında ilgili platform modüllerinde bir implementasyona sahip olması gerekir. Eğer bir actual implementasyonu eksikse, derleyici hata verir. Bu, özellikle kritik sistem bileşenleri veya temel altyapı kodları için önemlidir, çünkü eksik implementasyonların gözden kaçırılmasını engeller ve uygulamanın çalışma zamanında beklenmedik hatalarla karşılaşma riskini azaltır. Bağımlılık enjeksiyonu gibi çalışma zamanı (runtime) çözümleri, bu tür hataları ancak uygulama çalıştırıldığında veya test edildiğinde ortaya çıkarabilir.
Kotlin’in Kendisi Tarafından Sağlanan Platforma Özgü Sınıfları/Fonksiyonları Kullanırken
Kotlin ekosistemindeki bazı kütüphaneler ve dil özellikleri, zaten dahili olarak expect/actual mekanizmasını kullanır. Örneğin, kotlinx.coroutines kütüphanesindeki Dispatchers sınıfı, farklı platformlarda iş parçacığı (thread) yönetimi için expect/actual kullanır. Bu tür durumlarda, mevcut kütüphanelerin mimarisine uyum sağlamak için sizin de expect/actual kullanmanız doğal ve doğru bir yaklaşımdır. Benzer şekilde, platforma özgü bir UUID (Evrensel Tekil Tanımlayıcı) oluşturma fonksiyonu gibi standart kütüphane benzeri işlevler için expect/actual iyi bir seçenektir.
Vaka Analizi: Platforma Özgü Dosya Kaydetme İşlevi
Bir uygulamanın, kullanıcı tarafından oluşturulan bir raporu PDF formatında cihazın yerel depolama alanına kaydetmesi gerektiğini varsayalım. Bu işlevsellik, her platformda farklı API’ler gerektirecektir. Android’de FileOutputStream veya Storage Access Framework kullanılırken, iOS’ta FileManager veya DocumentDirectory gibi yapılar kullanılır. Bu senaryoda expect/actual kullanmak oldukça mantıklıdır:
// commonMain/kotlin/com/example/files/FileSaver.kt
package com.example.files
expect class PlatformFileSaver {
fun saveFile(filename: String, content: ByteArray): Boolean
}
// commonMain/kotlin/com/example/reports/ReportGenerator.kt
package com.example.reports
class ReportGenerator {
private val fileSaver = PlatformFileSaver() // expect class'ı burada kullanırız
fun generateAndSavePdf(reportData: String, filename: String): Boolean {
val pdfContent = generatePdf(reportData) // PDF içeriğini oluştur
return fileSaver.saveFile(filename, pdfContent)
}
private fun generatePdf(data: String): ByteArray {
// Basit bir örnek, gerçek PDF üretimi daha karmaşık olacaktır
return "PDF İçeriği: $data".encodeToByteArray()
}
}
Platforma özgü implementasyonlar:
// androidMain/kotlin/com/example/files/PlatformFileSaverActual.kt
package com.example.files
import java.io.File
import java.io.FileOutputStream
import android.os.Environment
actual class PlatformFileSaver {
actual fun saveFile(filename: String, content: ByteArray): Boolean {
return try {
val documentsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val file = File(documentsDir, filename)
FileOutputStream(file).use { it.write(content) }
true
} catch (e: Exception) {
e.printStackTrace()
false
}
}
}
// iosMain/kotlin/com/example/files/PlatformFileSaverActual.kt
package com.example.files
import platform.Foundation.*
actual class PlatformFileSaver {
actual fun saveFile(filename: String, content: ByteArray): Boolean {
return try {
val documentsPath = NSSearchPathForDirectoriesInDomains(
NSSearchPathDirectory.NSDocumentDirectory,
NSSearchPathDomainMask.NSUserDomainMask,
true
).first() as String
val filePath = NSURL.fileURLWithPath(documentsPath).URLByAppendingPathComponent(filename)
val nsData = content.toNSData()
nsData.writeToFile(filePath.path!!, atomically = true)
} catch (e: Exception) {
println("Dosya kaydetme hatası: ${e.localizedDescription}")
false
}
}
// ByteArray'ı NSData'ya dönüştürmek için yardımcı fonksiyon
private fun ByteArray.toNSData(): NSData {
val data = NSMutableData.dataWithLength(this.size.toULong())
data?.appendBytes(this.refTo(0), this.size.toULong())
return data as NSData
}
}
Bu örnekte, PlatformFileSaver sınıfı, her platformun kendi dosya sistemi API’lerini kullanarak dosyaları kaydetme işlevini soyutlar. Ortak kod (ReportGenerator), bu soyutlamayı kullanır ve platforma özgü detaylarla ilgilenmez. Bu, expect/actual‘ın en uygun ve verimli kullanım alanlarından biridir.
KMP Projelerinde Esnek ve Bakımı Kolay Bir Yapı Nasıl Oluşturulur?
KMP projelerinin uzun vadede başarılı olması için esnek, bakımı kolay ve ölçeklenebilir bir mimariye sahip olması kritik öneme sahiptir. Bu, sadece doğru araçları seçmekle değil, aynı zamanda belirli tasarım prensiplerini ve en iyi uygulamaları benimsemekle de mümkündür. İşte KMP projelerinizde bu hedeflere ulaşmanıza yardımcı olacak bazı stratejiler:
Modüler Tasarım ve Sorumlulukların Ayrılması
KMP projelerinde modüler tasarım, karmaşıklığı yönetmenin temel yollarından biridir. Uygulamanızın farklı katmanlarını ve işlevselliklerini ayrı modüllere ayırın. Örneğin:
common-core: Temel iş mantığı, veri modelleri, yardımcı fonksiyonlar. Platforma özgü hiçbir şeye bağımlı olmamalıdır.common-data: Veri depolama arayüzleri, API istemcileri için soyutlamalar.common-domain: İş kuralları ve kullanım senaryoları (use cases).- Platforma özgü modüller (
android-app,ios-app,web-app): UI katmanı, platforma özgü API implementasyonları ve bağımlılık enjeksiyonu ayarları.
Bu modüler yapı, her modülün belirli bir sorumluluğa sahip olmasını sağlar ve değişikliklerin etkisini sınırlar. Ortak modüller, platforma özgü detayları bilmemeli, sadece soyutlamalarla çalışmalıdır. Bu, “Bağımlılıkların Tersine Çevrilmesi” (Dependency Inversion Principle) prensibini destekler.
Arayüz Odaklı Geliştirme (Interface-Oriented Development)
Ortak kodunuzda platforma özgü davranışları soyutlamak için mümkün olduğunca arayüzler (interfaces) veya soyut sınıflar kullanın. Ortak modül sadece bu arayüzleri tanımlar ve bu arayüzlerin implementasyonları platforma özgü modüllerde sağlanır. Bu yaklaşım, ortak kodunuzun platform detaylarından tamamen ayrışmasını sağlar ve gelecekteki değişikliklere karşı daha dirençli hale getirir. Örneğin, bir ağ istemcisi için common-data modülünde bir NetworkClient arayüzü tanımlayıp, android-data ve ios-data modüllerinde bu arayüzün ktor-client veya platforma özgü HTTP kütüphaneleriyle implementasyonlarını sağlayabilirsiniz.
Bağımlılık Enjeksiyonu (DI) Prensiplerini Benimsemek
Bağımlılık enjeksiyonu, KMP projelerinde esnekliği ve test edilebilirliği artıran temel bir desendir. Bir DI çerçevesi (Koin, Kodein gibi) kullanarak veya manuel olarak basit bir servis bulucu (Service Locator) uygulayarak, platforma özgü implementasyonları ortak modülünüze enjekte edin. Bu, ortak kodunuzun platforma özgü implementasyonları doğrudan oluşturmak yerine, dışarıdan almasını sağlar. Böylece, testlerde sahte (mock) implementasyonlar enjekte etmek çok daha kolay hale gelir ve farklı platformlar için farklı implementasyonlar sağlamak esnekleşir.
Test Stratejileri ve Kapsamlı Testler
KMP’nin en büyük avantajlarından biri, iş mantığınızın çoğunu ortak kodda yazabilmeniz ve bu kodu bir kez test ederek tüm platformlarda güvenilirliğini sağlamanızdır. Ortak kodunuz için kapsamlı birim testleri ve entegrasyon testleri yazın. Platforma özgü modüller için ise, UI testleri ve platforma özgü API entegrasyon testlerine odaklanın. Bağımlılık enjeksiyonu kullanarak, ortak kodun testlerini platforma özgü bağımlılıklardan izole edebilirsiniz. Bu, test sürecini hızlandırır ve hataları erken aşamada tespit etmenize yardımcı olur.
Kod İncelemesi ve Ekip İçi Standartlar
KMP projelerinde tutarlılık, özellikle birden fazla geliştiricinin çalıştığı ekiplerde hayati öneme sahiptir. Düzenli kod incelemeleri (code reviews) yaparak, expect/actual kullanımının doğru senaryolarda yapıldığından ve alternatif yaklaşımların gerektiğinde tercih edildiğinden emin olun. Ekip içi kodlama standartları ve mimari prensipler belirlemek, projenin uzun vadede sürdürülebilirliğini artırır. Hangi durumlarda expect/actual, hangi durumlarda DI veya başka bir yaklaşımın kullanılacağına dair net yönergeler oluşturmak, geliştiricilerin doğru kararları vermesine yardımcı olacaktır.
Deneyimli kullanıcılar için ek ipuçları: Büyük KMP projelerinde, mimari desenler (örneğin MVI – Model-View-Intent veya MVVM – Model-View-ViewModel) ve çoklu modül yapıları, kodun daha iyi organize edilmesine yardımcı olabilir. CI/CD (Sürekli Entegrasyon/Sürekli Dağıtım) süreçlerinizi, tüm platformlar için otomatik testler ve derlemeler içerecek şekilde yapılandırın. Özellikle yeni bir platform eklediğinizde veya büyük bir mimari değişiklik yaptığınızda, otomatik testler ve derleme pipeline’ları, potansiyel sorunları erkenden yakalamanızı sağlar. Ayrıca, expect/actual bildirimlerini minimumda tutmak ve bunları genellikle sadece platforma özgü API’leri sarmalayan düşük seviyeli kütüphaneler için ayırmak, genel proje karmaşıklığını önemli ölçüde azaltacaktır.
Sonuç ve Sıkça Sorulan Sorular
Kotlin Multiplatform (KMP), tek bir kod tabanından birden fazla platforma uygulama geliştirme vizyonuyla mobil uygulama geliştirme dünyasında devrim yaratmaya devam ediyor. Bu mimarinin temel taşı olan expect/actual mekanizması, platforma özgü kodları yönetmek için güçlü ve kullanışlı bir araçtır. Ancak, bu makalede detaylıca ele aldığımız gibi, expect/actual yaklaşımını her platforma özgü ihtiyaç için varsayılan tercih olarak kullanmak, uzun vadede projenizin karmaşıklığını, bakım maliyetini ve esnekliğini olumsuz etkileyebilir.
expect/actual‘ın güçlü yönleri, özellikle gerçekten platforma özgü temel sistem API’lerini sarmalama, derleme zamanı garantileri sağlama ve çok küçük, izole edilmiş farklılıkları yönetme durumlarında ortaya çıkar. Ancak, tekrarlayan kod (boilerplate), sıkı bağlantı (tight coupling), test edilebilirlik zorlukları ve API tasarım kısıtlamaları gibi potansiyel tuzakları göz ardı etmemek gerekir. Bu zorlukların üstesinden gelmek için bağımlılık enjeksiyonu (DI), modüler tasarım ve arayüz odaklı geliştirme gibi alternatif yaklaşımları benimsemek, KMP projelerinizin daha sürdürülebilir ve ölçeklenebilir olmasını sağlayacaktır.
Unutmayın ki, doğru aracı doğru senaryoda kullanmak, başarılı yazılım geliştirmenin anahtarıdır. expect/actual, KMP araç kutunuzdaki değerli bir öğedir, ancak akıllıca ve stratejik olarak kullanılması gereken bir öğedir. Projenizin ihtiyaçlarını dikkatlice değerlendirerek, esnek, bakımı kolay ve performanslı KMP uygulamaları inşa edebilirsiniz.
Sıkça Sorulan Sorular
1. expect/actual performansı etkiler mi?
Genel olarak, expect/actual mekanizmasının kendisi performansı doğrudan ve önemli ölçüde etkilemez. Derleme zamanında uygun actual implementasyonu bağlandığı için, çalışma zamanında ek bir soyutlama katmanı maliyeti olmaz. Performans üzerindeki olası etkiler, actual implementasyonunun kendisinin verimliliğinden kaynaklanır; yani platforma özgü kodun ne kadar iyi yazıldığına bağlıdır.
2. Her platforma özgü kod için expect/actual kullanmalı mıyım?
Hayır, kesinlikle kullanmamalısınız. Bu makalede vurgulandığı gibi, expect/actual‘ı varsayılan tercih olarak kullanmak, tekrarlayan kod, sıkı bağlantı ve bakım zorluklarına yol açabilir. Sadece gerçekten platforma özgü temel API’leri sarmalamanız gerektiğinde, derleme zamanı garantileri aradığınızda veya çok küçük, izole edilmiş farklılıklar için kullanmalısınız. Diğer durumlarda bağımlılık enjeksiyonu gibi alternatifleri değerlendirmek daha iyidir.
3. Büyük bir projede expect/actual‘ı DI’ye dönüştürmek mümkün mü?
Evet, mümkündür ancak projenin büyüklüğüne ve mevcut expect/actual kullanımının yaygınlığına bağlı olarak zaman ve çaba gerektirebilir. Genellikle, her expect bildirimi için ortak modülde bir arayüz tanımlanır ve platforma özgü actual implementasyonları bu arayüzü implemente edecek sınıflara dönüştürülür. Daha sonra bu sınıflar, bir DI çerçevesi veya manuel DI kullanılarak enjekte edilir. Bu, kademeli olarak yapılabilir, her bir expect/actual çifti ayrı ayrı dönüştürülerek risk minimize edilebilir.
4. Hangi KMP kütüphaneleri expect/actual kullanır?
Birçok popüler KMP kütüphanesi, platforma özgü işlevselliği soyutlamak için dahili olarak expect/actual kullanır. Örnekler arasında kotlinx.coroutines (Dispatchers için), ktor-client (HTTP istemci implementasyonları için), kotlinx.serialization (platforma özgü serileştirme/deserileştirme için) ve SQLDelight (veritabanı sürücüleri için) bulunmaktadır. Bu kütüphaneler, expect/actual‘ın doğru ve stratejik kullanımına iyi örnekler teşkil eder.
5. expect/actual olmadan KMP’de platforma özgü kod nasıl yazılır?
expect/actual olmadan platforma özgü kod yazmanın temel yolu, ortak modülde arayüzler tanımlamak ve bu arayüzlerin implementasyonlarını platforma özgü modüllerde sağlamaktır. Ortak kod, bu implementasyonları doğrudan çağırmak yerine, bir bağımlılık enjeksiyonu mekanizması (bir DI çerçevesi veya basit bir fabrika deseni) aracılığıyla alır. Bu, ortak kodun platforma özgü detaylardan tamamen ayrışmasını sağlar ve test edilebilirliği artırır. Ayrıca, platforma özgü build konfigürasyonları veya özellik bayrakları da küçük farklılıklar için kullanılabilir.
#KotlinMultiplatform #KMP #expectActual #YazılımMimarisi #BağımlılıkEnjeksiyonu #ÇokluPlatformGeliştirme #KodPaylaşımı
