Ktor Android
Herkese merhaba, mediumdaki ilk yazımda sizlere Android dünyasında Retrofit’e alternatif olarak kullanabileceğimiz Ktor’dan bahsedeceğim. Sonrasında ufak bir demo uygulama ile de kapanışı yapacağız 😊
Ktor; web uygulamaları, http servisleri, mikroservisler, mobil ve tarayıcı uygulamaları gibi birbiriyle bağlantılı uygulamaları kolayca oluşturabileceğimiz Jetbrains tarafından %100 kotlin dili ile geliştirilmiş ve coroutine tabanlı çalışan asenkron bir framework’tür. Kısacası Kotlin multiplatformlar ile birlikte çalışarak farklı platformlar arasında networking işlemleri yapmamıza olanak sağlar.
Neden Ktor sorusuna yanıt olarak ise Retrofit yalnızca Android/JVM’e yönelik bir kütüphane iken Ktor ile hem Android hem de IOS için client oluşturulabilir, diyebiliriz (Kotlin Multiplatform).
Ktor yalnızca http istemcisi olmaktan da fazlasıdır. Genel olarak baktığımızda iki ana bölümden oluşur:
Birincisi JVM üzerinde çalışan http server framework’ü, ikincisi ise multiplatform ve native Http clientıdır.
Biz bu yazıda Ktor’un native Http clientı olarak nasıl kullanıldığını ele alacağız. Hazırsanız başlayalım 😊
Ktor Http Client Oluşturma
Ktor’u kullanabilmek için modül bazındaki gradle dosyamıza ilgili dependency’leri ekliyoruz. Güncel versiyon için Ktor’un sitesini ziyaret edebilirsiniz.
dependencies {
def ktor_version = “1.6.3”
implementation “io.ktor:ktor-client-core:$ktor_version”
implementation “io.ktor:ktor-client-cio:$ktor_version”}
Dependency’lerden ilki Ktor’un temel client işlevselliğini sağlar. İkincisi ise network requestlerini işleyen Engine’dir. Yukarıda CIO(Coroutine Input/Output) engine için dependency ekledik fakat Ktor’da network requestlerini işleyen yalnızca CIO değil bir çok http engine bulunur. Hangisini kullanmak istiyorsak modül seviyesindeki gradle’ımıza ilgili engine’i ekleyebiliriz.
Kullanabileceğimiz diğer engineler de aşağıdaki gibidir.
implementation("io.ktor:ktor-client-apache:$ktor_version")
implementation("io.ktor:ktor-client-java:$ktor_version")
implementation("io.ktor:ktor-client-jetty:$ktor_version")
implementation("io.ktor:ktor-client-cio:$ktor_version")
implementation("io.ktor:ktor-client-android:$ktor_version")
implementation("io.ktor:ktor-client-okhttp:$ktor_version")
implementation("io.ktor:ktor-client-js:$ktor_version")
Ktor Http clientını JVM, Android, JavaScript ve Native (iOS ve masaüstü) dahil olmak üzere farklı platformlarda kullanabildiğimiz için belirli bir platform, network requestlerini işleyen belirli bir engine gerektirir. Örneğin yukardaki engine’lerden JVM için Apache, Jetty veya CIO; Android için OkHttp vb. kullanabiliriz. Farklı engine’lerin belirli özellikleri olabilir ve farklı yapılandırma seçenekleri sunabilir.
Ayrıca Ktor’un bize sunduğu, MockEngine ise bir API endpointine gerçek bir bağlantı olmadan Http call’larını simüle eden test amaçlı özel bir http enginedir. Bunu da yine ilgili dependecy’leri ekleyip projemizde test amaçlı kullanabiliriz.
Ktor ile basit bir Http get requesti yapmak istediğimizde;
Seçtiğimiz bir Http engine’i HttpClient classının consructor’ına parametre olarak geçerek bu sınıftan bir Ktor clientı oluştururuz. Eğer hiç parametre geçmezsek HttpClient’ın parametresiz consructor’ı çağrılır ve Ktor’un kullanacağı Http engine compile time’da otomatik olarak seçilir.
Ayrıca burada dikkat edilmesi gereken nokta Ktor coroutine tabanlı olduğu için ancak suspend fonksiyonların içinde kullanılabilir bu sebeple suspend fun main() şeklinde kullanım yaptık, aksi takdirde ide uyarı verecektir. Gördüğünüz gibi sunucuya yaptığımız basit bir get requestinin sonucu başarılı döndü (200 OK).
Ktor bize oluşturduğumuz clientı kendi bloğu içerisinde yapılandırma ve sağladığı çeşitli özellikleri de kullanma fırsatı sunar. Engine konfigürasyonunu aşağıdaki gibi yapabiliriz. Install bloğu ile de loglama, serileştirme gibi özellikleri kullanabiliriz.
suspend fun main() {
val client = HttpClient(Android) {
expectSuccess = false
install(Logging) {
logger = Logger.DEFAULT
level = LogLevel.HEADERS
}
install(JsonFeature) {
serializer = GsonSerializer()
}
engine {
connectTimeout = 100_000
socketTimeout = 100_000
}
}
Response objectimizi de aynı şekilde lambda bloğunu kullanarak yapılandırabiliriz.
val REQUEST_URL = "https://ktor.io/"
val response = client.request<MyResponseClass>(REQUEST_URL) {
method = HttpMethod.Get
}
}
Ktor ile ilgili temel bilgilerden sonra şimdi demo uygulamamıza geçebiliriz. Uygulamada https://jsonplaceholder.typicode.com/ adresinden verilen ücretsiz API’yi kullanacağız.
Resource kısmında https://jsonplaceholder.typicode.com/comments linkine tıkladığımızda aşağıdaki gibi gelen Json response’u kullanacağız (Bu şekilde 500 adet comment).
Ktor’u ve çeşitli başka özellikleri kullanabilmek için gradle’a aşağıdaki dependency’leri ekliyoruz.
def ktor_version = "1.6.3"
implementation "io.ktor:ktor-client-core:$ktor_version"
implementation "io.ktor:ktor-client-android:$ktor_version"
implementation "io.ktor:ktor-client-serialization:$ktor_version"
implementation "io.ktor:ktor-client-logging:$ktor_version"
implementation "ch.qos.logback:logback-classic:1.2.3"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.0"
Manifest dosyamıza internet iznini ekliyoruz.
<uses-permission android:name="android.permission.INTERNET" />
CommentResponse adlı data classımızı oluşturuyoruz.
import kotlinx.serialization.Serializable
@Serializable
data class CommentResponse(
val postId: Int,
val id: Int,
val name: String,
val email: String,
val body: String
)
Base url ve endpointimizi de aşağıdaki gibi tanımlıyoruz.
object HttpRoutes {
const val BASE_URL = "https://jsonplaceholder.typicode.com"
const val COMMENTS = "$BASE_URL/comments"
}
Şimdi ise Ktor için api servisini oluşturma adımına geçebiliriz. Fakat öncesinde aralarındaki farkı daha iyi anlamak için bu işlemi Retrofit kullanarak yapsaydık nasıl olurdu ona bir bakalım.
Api servisimizi Ktor ile oluşturduğumuzda ise Retrofit’teki gibi GET notasyonunu kullanmamıza gerek kalmıyor. Ancak client oluştururken kullanmak istediğimiz Http enginee’i seçmemiz gerekiyor. Bunun için gradle’a eklediğimiz android enginee’i HttpClient’a parametre olarak geçtik. Ayrıca HttpClientConfig lambda bloğu içerisinde clientımıza çeşitli özellikler ekleyebiliriz. Örneğin biz burada herhangi bir şey ters giderse daha kolay gözlemlemek için loglama özelliğini dahil ettik ve serialization işlemi için de KotlinxSerializer kullandık.
import io.ktor.client.*
import io.ktor.client.engine.android.*
import io.ktor.client.features.json.*
import io.ktor.client.features.json.serializer.*
import io.ktor.client.features.logging.*
interface CommentAPI {
suspend fun getComments(): List<CommentResponse>
companion object {
fun create(): CommentAPI {
return CommentAPIImpl(
client = HttpClient(Android) {
install(Logging) {
level = LogLevel.ALL
}
install(JsonFeature) {
serializer = KotlinxSerializer()
}
}
)
}
}
}
Aşağıda ise api servisimizin implementasyon classını oluşturduk. Böylece ktor clientı ile endpointimize request gönderip bu request sırasında da yaşanabilecek exceptionları handle ettik. Eğer verileri çekerken herhangi bir hata durumu meydana gelirse uygulama crash yemeden boş liste döndürecek şeklinde. Bu implementasyon sınıfını da interface’imizde ktor servis instance’ı dönmesi için kullandık.
import io.ktor.client.*
import io.ktor.client.features.*
import io.ktor.client.request.*
class CommentAPIImpl(private val client: HttpClient) : CommentAPI {
override suspend fun getComments(): List<CommentResponse> {
return try {
client.get { url(HttpRoutes.COMMENTS) }
} catch(e: RedirectResponseException) {
// 3xx - responses
println("Error: ${e.response.status.description}")
emptyList()
} catch(e: ClientRequestException) {
// 4xx - responses
println("Error: ${e.response.status.description}")
emptyList()
} catch(e: ServerResponseException) {
// 5xx - responses
println("Error: ${e.response.status.description}")
emptyList()
} catch(e: Exception) {
println("Error: ${e.message}")
emptyList()
}
}
}
Artık MainActivity’de ktor instance’ını kullanarak network requestimizi yapabiliriz.
Uygulamanın tasarımını compose ile yaptığımız için dikkat ettiyseniz RecyclerView yapısını kullanmadık. Onun yerine Compose’da RecyclerView’a karşılık gelen LazyColumn yapısını kullandık. Compose başka bir yazıda anlatılacak kadar geniş bir konu olduğundan burada fazla detaya inmeden kısaca bahsedelim. CommentAPI interface’inde bulunan create() metodu ile api call yapabilmek için kullanmamız gereken ktorService nesnesini oluşturduk.
class MainActivity : ComponentActivity() {
private val ktorService = CommentAPI.create()
setContent bloğu içinde de comments adlı liste değişkeninin tipini Compose’da internetten çekilen verileri tutmak için kullandığımız yapılardan biri olan produceState yapısının içinde List<CommentResponse> olarak tanımladık ve böylece comments listesinin value değerine ktorService.getComments() sonucunda dönen listeyi aktarmış olduk.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val comments = produceState<List<CommentResponse>>(
initialValue = emptyList(),
producer = {
value = ktorService.getComments()
}
)
Liste elimizde olduğuna göre(comments.value) LazyColumn yapısında bulunan items metoduna bu listeyi vererek bizim için tek tek listedeki her bir elemanı oluşturduğumuz card tasarımına eklemesini sağladık.
LazyColumn {
items(comments.value) {
Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Center
) {
Card(modifier = Modifier
.padding(all = 5.dp)
.fillMaxWidth(),
elevation = 10.dp,
backgroundColor = Color.Gray,
shape = RoundedCornerShape(corner=CornerSize(8.dp)),
border = BorderStroke(2.dp,Color.DarkGray)
) {
Listedeki her bir CommentResponse nesnemizin değişkenlerini hatırlayacak olursak id, name, email ve body. Bu değişkenleri ise ekranda alt alta sıralı görünmeleri için Column scopu altında topladık.
Column(horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(all = 10.dp)) {
Text(text = it.id.toString(), fontSize = 20.sp, modifier = Modifier.align(Alignment.CenterHorizontally))
Spacer(modifier = Modifier.height(4.dp))
Text(text = it.name, fontSize = 20.sp, fontWeight = FontWeight.Bold)
Spacer(modifier = Modifier.height(4.dp))
Text(text = it.email, fontSize = 20.sp, fontStyle = FontStyle.Italic)
Spacer(modifier = Modifier.height(4.dp))
Text(text = it.body, fontSize = 14.sp)
}
Uygulamayı çalıştırdığımızda ise liste aşağıdaki gibi ekrana geldi.
Ayrıca CommenAPI’da client’a eklediğimiz loglama özelliği sayesinde de çektiğimiz dataları Logcat’ten de gözlemleyebiliriz.
Demo uygulamamızda kısaca Ktor kullanarak basit bir Http get requesti nasıl yapılır anlatmaya çalıştım. Umarım faydalı olmuştur. Okuyan herkese çok teşekkürler, başka bir yazıda görüşmek üzere :)